存储图形对象是个好主意吗?

时间:2015-07-12 17:12:18

标签: java memory-management graphics awt bufferedimage

我目前正在使用java编写绘图程序,旨在提供灵活而全面的功能。它起源于我最后一个项目,即我前一天晚上写的。因此,它有大量的bug,我一直在逐一处理(例如我只能保存空的文件,我的矩形不会画得正确,但我的圈子做......)。

这一次,我一直在尝试为我的程序添加撤消/重做功能。但是,我无法撤消"我做过的事情。因此,每次发生BufferedImage事件时,我都会想到保存mouseReleased的副本。但是,由于某些图像的分辨率为1920x1080,我认为这不会有效:存储它们可能需要几十亿的内存。

为什么我不能简单地用背景颜色绘制相同的东西以进行撤消是因为我有许多不同的画笔,它们基于Math.random()绘画,并且因为有许多不同的图层(在一个单层)。

然后,我考虑克隆用于绘制Graphics的{​​{1}}个对象。像这样:

BufferedImage

我之前没有这样做过,所以我有几个问题:

  • 我是否仍然会通过这样做来浪费无意义的记忆,比如克隆我的ArrayList<Graphics> revisions = new ArrayList<Graphics>(); @Override public void mouseReleased(MouseEvent event) { Graphics g = image.createGraphics(); revisions.add(g); }
  • 我可以采用不同的方式吗?

4 个答案:

答案 0 :(得分:46)

不,存储Graphics对象通常是个坏主意。 : - )

这就是原因:通常情况下,Graphics个实例是短暂的,用于绘制或绘制到某种表面(通常是(J)ComponentBufferedImage) 。它保持这些绘图操作的状态,如颜色,笔触,比例,旋转等。但是,它不保存绘图操作或像素的结果

因此,它不会帮助您实现撤消功能。像素属于组件或图像。所以,回到以前的&#34; Graphics对象不会将像素修改回以前的状态。

以下是我认识的一些方法:

  • 使用&#34;链&#34;命令(命令模式)修改图像。使用undo / redo命令模式非常好(并且在Action中的Swing / AWT中实现)。从原始开始按顺序渲染所有命令。 Pro:每个命令中的状态通常不是很大,允许你在内存中有许多undo-buffer步骤。 Con:经过大量的操作,它变得很慢......

  • 对于每个操作,存储整个BufferedImage(如您最初所做的那样)。亲:易于实施。 Con:你快速耗尽内存。提示:您可以序列化图像,使撤消/重做占用更少的内存,但代价是处理时间更长。

  • 以上的组合,使用命令模式/链的想法,但使用&#34;快照&#34;来优化渲染。 (如BufferedImages)合理的时候。这意味着您不需要为每个新操作(更快)从头开始渲染所有内容。同时将这些快照刷新/序列化到磁盘,以避免内存不足(但如果可以,请将它们保存在内存中,以提高速度)。您还可以将命令序列化到磁盘,以实现几乎无限制的撤消。 Pro:正确完成后效果很好。 Con:需要一些时间才能做对。

PS:对于上述所有内容,您需要使用后台线程(如SwingWorker或类似)来更新显示的图像,在后台将命令/图像存储到磁盘等,以保持响应式UI

祝你好运! : - )

答案 1 :(得分:9)

创意#1,存储Graphics对象根本不起作用。 Graphics不应被视为&#34;持有&#34;一些显示内存,而是作为访问显示内存区域的句柄。在BufferedImage的情况下,每个Graphics对象将始终是同一给定图像内存缓冲区的句柄,因此它们都将表示相同的图像。更重要的是,您实际上无法对存储的Graphics执行任何操作:因为他们不存储任何内容,所以他们无法再存储&#34;重新存储& #34;任何东西。

想法#2,克隆BufferedImage是一个更好的主意,但你确实会浪费内存,并且很快就会耗尽它。它仅用于存储受绘制影响的图像部分,例如使用矩形区域,但它仍然需要大量内存。将这些撤消图像缓冲到磁盘可能有所帮助,但它会使您的用户界面变得缓慢且无响应,并且错误;此外,它使您的应用程序更加复杂且容易出错

我的另一种选择是将图像修改存储在列表中,从头到尾呈现在图像的顶部。然后,撤消操作只需从列表中删除修改。

这需要你&#34; reify&#34;图像修改,即通过提供执行实际绘图的void draw(Graphics gfx)方法创建实现单个修改的类。

正如您所说,随机修改会带来额外的问题。但是,关键问题是您使用Math.random()创建随机数。相反,使用从固定种子值创建的Random执行每个随机修改,以便draw()的每次调用时(伪)随机数序列相同,即每次绘制完全具有同样的效果。 (这就是为什么他们被称为&#34;伪随机&#34; - 生成的数字看起来是随机的,但它们与任何其他函数一样具有确定性。)

与具有存储器问题的图像存储技术相比,该技术的问题在于许多修改可能使GUI变慢,尤其是如果修改是计算密集的。为了防止这种情况,最简单的方法是修复适当的最大可撤销修改列表。如果通过添加新修改将超出此限制,请删除最旧的修改列表并将其应用于支持BufferedImage本身。

以下简单演示应用程序表明这些(以及如何)一起工作。它还包括一个很好的&#34;重做&#34;重做未完成操作的功能。

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));
            }
        }
    }
}

答案 2 :(得分:7)

大多数游戏(或程序)只保存必要的部分,这就是你应该做的事情。

  • 矩形可以用宽度,高度,背景颜色,笔触,轮廓等表示。因此,您只需保存这些参数而不是实际的矩形。 “矩形颜色:红色宽度:100高度100”

  • 对于程序的随机方面(画笔上的随机颜色),您可以保存种子或保存结果。 “随机种子:1023920”

  • 如果程序允许用户导入图像,则应复制并保存图像。

  • 填充物和效果(缩放/变换/发光)都可以像形状一样用参数表示。例如。 “缩放比例:2”“旋转角度:30”

  • 因此您将所有这些参数保存在列表中,然后您需要撤消,您可以将参数标记为已删除(但实际上并不删除它们,因为您也希望能够重做)。然后,您可以擦除整个画布,并根据参数减去标记为已删除的参数重新创建图像。

*对于类似于行的内容,您只需将其位置存储在列表中即可。

答案 3 :(得分:4)

你将要尝试压缩你的图像(使用PNG是一个很好的开始,它有一些很好的过滤器以及zlib压缩真的有帮助)。我认为最好的方法是

  • 在修改之前制作图像的副本
  • 修改
  • 将副本与新修改的图像进行比较
  • 对于您没有改变的每个像素,将该像素设为黑色透明像素。

那应该在PNG中非常好地压缩。尝试黑白,看看是否存在差异(我不认为会有,但请确保将rgb值设置为相同的值,而不仅仅是alpha值,因此它会更好地压缩)。

将图像裁剪到更改的部分可能会获得更好的性能,但考虑到压缩(以及您现在必须保存的事实),我不确定您从中获得了多少并记住偏移量。

然后,由于您有一个Alpha通道,如果它们撤消,您可以将撤消图像放回当前图像的顶部并重新设置。