Ziz*_*212 44 java graphics bufferedimage memory-management awt
我目前正在用java编写绘图程序,旨在提供灵活而全面的功能.它起源于我最后一个项目,即我前一天晚上写的.因此,它有大量的bug,我一直在逐一处理(例如,我只能保存空的文件,我的矩形不能画正确但我的圈子会......).
这一次,我一直在尝试为我的程序添加撤消/重做功能.但是,我无法"撤消"我所做的事情.因此,我有一个想法,BufferedImage每次mouseReleased事件被解雇时都保存我的副本.但是,由于某些图像的分辨率为1920x1080,我认为这样效率不高:存储它们可能需要几十亿字节的内存.
为什么我不能简单地用背景颜色来绘制相同的东西以进行撤销的原因是因为我有许多不同的画笔,它们基于画笔Math.random(),并且因为有许多不同的图层(在单个图层中).
然后,我考虑克隆Graphics我用来绘制的对象BufferedImage.像这样:
ArrayList<Graphics> revisions = new ArrayList<Graphics>();
@Override
public void mouseReleased(MouseEvent event) {
    Graphics g = image.createGraphics();
    revisions.add(g);
}
我之前没有这样做,所以我有几个问题:
BufferedImages?har*_*ldK 46
不,存储Graphics对象通常是个坏主意.:-)
原因如下:通常,Graphics实例是短暂的,用于绘制或绘制到某种表面(通常是a (J)Component或a BufferedImage).它拥有这些绘图操作的状态,如颜色,中风,缩放,旋转等.但是,它不保存结果的绘制操作或像素.
因此,它无法帮助您实现撤消功能.像素属于组件或图像.因此,回滚到"上一个" Graphics对象不会将像素修改回先前的状态.
以下是我所知道的一些方法:
使用"链"命令(命令模式)来修改图像.使用undo/redo命令模式非常好用(并在Swing/AWT中实现Action).从原始开始按顺序渲染所有命令.Pro:每个命令中的状态通常不是很大,允许你在内存中有许多undo-buffer步骤.Con:经过大量的操作后,它变得缓慢......
对于每个操作,存储整个BufferedImage(如您最初所做的那样).亲:易于实施.Con:你的内存会很快耗尽.提示:您可以序列化图像,使撤消/重做占用更少的内存,但代价是处理时间更长.
上述的组合,使用命令模式/链的想法,但BufferedImages在合理时使用"快照"(as )优化渲染.这意味着您不需要为每个新操作(更快)从头开始渲染所有内容.同时将这些快照刷新/序列化到磁盘,以避免内存不足(但如果可以,请将它们保存在内存中,以提高速度).您还可以将命令序列化到磁盘,以实现几乎无限制的撤消.Pro:正确完成后效果很好.Con:需要一些时间才能做对.
PS:对于上述所有情况,您需要使用后台线程(类似SwingWorker或类似)来更新显示的图像,在后台将命令/图像存储到磁盘等,以保持响应式UI.
祝好运!:-)
想法#1,存储Graphics对象根本不起作用.本Graphics不应该被视为"持有"一些显示内存,而是作为手柄来访问显示内存的区域.在这种情况下BufferedImage,每个Graphics对象将始终是同一给定图像内存缓冲区的句柄,因此它们都将代表相同的图像.更重要的是,你实际上无法对存储做任何事情Graphics:因为它们不存储任何东西,所以它们无法"重新存储"任何东西.
想法#2,克隆BufferedImages是一个更好的主意,但你确实会浪费内存,并迅速耗尽它.它仅用于存储受绘制影响的图像部分,例如使用矩形区域,但它仍然需要大量内存.将这些撤消映像缓冲到磁盘可能会有所帮助,但它会使您的UI变得缓慢且无响应,而且这很糟糕 ; 此外,它使您的应用程序更复杂,更容易出错.
我的另一种方法是将图像修改存储在一个列表中,从头到尾呈现在图像的顶部.然后撤消操作只需从列表中删除修改.
这需要您"修改"图像修改,即通过提供void draw(Graphics gfx)执行实际绘图的方法来创建实现单个修改的类.
正如您所说,随机修改会带来额外的问题.但是,关键问题是您使用Math.random()创建随机数.相反,使用Random从固定种子值创建的每个随机修改执行每个随机修改,使得(伪)随机数序列在每次调用时是相同的draw(),即每个绘制具有完全相同的效果.(这就是为什么它们被称为"伪随机" - 生成的数字看起来是随机的,但它们与任何其他函数一样具有确定性.)
与具有存储器问题的图像存储技术相比,该技术的问题在于许多修改可能使GUI变慢,尤其是如果修改是计算密集的.为了防止这种情况,最简单的方法是修复可撤销修改列表的适当最大大小.如果通过添加新修改将超出此限制,请删除最旧的修改列表并将其应用于支持BufferedImage本身.
以下简单的演示应用程序显示(以及如何)这一切都在一起工作.它还包括一个很好的"重做"功能,用于重做未完成的操作.
package stackoverflow;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;
import java.util.LinkedList;
import java.util.Random;
import javax.swing.*;
public final class UndoableDrawDemo
        implements Runnable
{
    public static void main(String[] args) {
        EventQueue.invokeLater(new UndoableDrawDemo()); // execute on EDT
    }
    // holds the list of drawn modifications, rendered back to front
    private final LinkedList<ImageModification> undoable = new LinkedList<>();
    // holds the list of undone modifications for redo, last undone at end
    private final LinkedList<ImageModification> undone = new LinkedList<>();
    // maximum # of undoable modifications
    private static final int MAX_UNDO_COUNT = 4;
    private BufferedImage image;
    public UndoableDrawDemo() {
        image = new BufferedImage(600, 600, BufferedImage.TYPE_INT_RGB);
    }
    public void run() {
        // create display area
        final JPanel drawPanel = new JPanel() {
            @Override
            public void paintComponent(Graphics gfx) {
                super.paintComponent(gfx);
                // display backing image
                gfx.drawImage(image, 0, 0, null);
                // and render all undoable modification
                for (ImageModification action: undoable) {
                    action.draw(gfx, image.getWidth(), image.getHeight());
                }
            }
            @Override
            public Dimension getPreferredSize() {
                return new Dimension(image.getWidth(), image.getHeight());
            }
        };
        // create buttons for drawing new stuff, undoing and redoing it
        JButton drawButton = new JButton("Draw");
        JButton undoButton = new JButton("Undo");
        JButton redoButton = new JButton("Redo");
        drawButton.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                // maximum number of undo's reached?
                if (undoable.size() == MAX_UNDO_COUNT) {
                    // remove oldest undoable action and apply it to backing image
                    ImageModification first = undoable.removeFirst();
                    Graphics imageGfx = image.getGraphics();
                    first.draw(imageGfx, image.getWidth(), image.getHeight());
                    imageGfx.dispose();
                }
                // add new modification
                undoable.addLast(new ExampleRandomModification());
                // we shouldn't "redo" the undone actions
                undone.clear();
                drawPanel.repaint();
            }
        });
        undoButton.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                if (!undoable.isEmpty()) {
                    // remove last drawn modification, and append it to undone list
                    ImageModification lastDrawn = undoable.removeLast();
                    undone.addLast(lastDrawn);
                    drawPanel.repaint();
                }
            }
        });
        redoButton.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                if (!undone.isEmpty()) {
                    // remove last undone modification, and append it to drawn list again
                    ImageModification lastUndone = undone.removeLast();
                    undoable.addLast(lastUndone);
                    drawPanel.repaint();
                }
            }
        });
        JPanel buttonPanel = new JPanel(new FlowLayout());
        buttonPanel.add(drawButton);
        buttonPanel.add(undoButton);
        buttonPanel.add(redoButton);
        // create frame, add all content, and open it
        JFrame frame = new JFrame("Undoable Draw Demo");
        frame.getContentPane().add(drawPanel);
        frame.getContentPane().add(buttonPanel, BorderLayout.NORTH);
        frame.pack();
        frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }
    //--- draw actions ---
    // provides the seeds for the random modifications -- not for drawing itself
    private static final Random SEEDS = new Random();
    // interface for draw modifications
    private interface ImageModification
    {
        void draw(Graphics gfx, int width, int height);
    }
    // example random modification, draws bunch of random lines in random color
    private static class ExampleRandomModification implements ImageModification
    {
        private final long seed;
        public ExampleRandomModification() {
            // create some random seed for this modification
            this.seed = SEEDS.nextLong();
        }
        @Override
        public void draw(Graphics gfx, int width, int height) {
            // create a new pseudo-random number generator with our seed...
            Random random = new Random(seed);
            // so that the random numbers generated are the same each time.
            gfx.setColor(new Color(
                    random.nextInt(256), random.nextInt(256), random.nextInt(256)));
            for (int i = 0; i < 16; i++) {
                gfx.drawLine(
                        random.nextInt(width), random.nextInt(height),
                        random.nextInt(width), random.nextInt(height));
            }
        }
    }
}
大多数游戏(或程序)只保存必要的部分,这就是你应该做的.
矩形可以用宽度,高度,背景颜色,笔触,轮廓等表示.因此,您只需保存这些参数而不是实际的矩形."矩形颜色:红色宽度:100高度100"
对于程序的随机方面(画笔上的随机颜色),您可以保存种子或保存结果."随机种子:1023920"
如果程序允许用户导入图像,则应复制并保存图像.
填充物和效果(缩放/变换/发光)都可以像形状一样用参数表示.例如."缩放比例:2""旋转角度:30"
所以你保存列表中的所有这些参数,然后当你需要撤销你可以标记为删除(但不真正删除它们,因为你希望能够重做以及)的参数.然后,您可以擦除整个画布并根据参数减去标记为已删除的参数重新创建图像.
*对于线条之类的东西,您可以将它们的位置存储在列表中.