存储Graphics对象是一个好主意吗?

我目前正在用java写一个绘图程序,这个程序的devise具有灵活和全面的function。 这源自我最后的项目,我前一天写了一整晚。 正因为如此,它有吨和大量的错误,我一直在逐一处理(例如,我只能保存文件将是空的,我的矩形不正确,但我的圈子呢…)。

这一次,我一直在试图为我的程序添加撤销/重做function。 但是,我不能“撤销”我所做的一切。 因此,每次发生mouseReleased事件时,我都有一个想法来保存我的BufferedImage副本。 但是,有些图像的分辨率达到了1920×1080,我认为这样做效率不高:存储这些图像可能需要千兆字节的内存。

为什么我不能简单地用背景颜色绘制同样的东西来撤消是因为我有许多不同的画笔,基于Math.random()绘制,因为有许多不同的图层(在单个图层中) 。

然后,我考虑克隆用于绘制到BufferedImageGraphics对象。 喜欢这个:

 ArrayList<Graphics> revisions = new ArrayList<Graphics>(); @Override public void mouseReleased(MouseEvent event) { Graphics g = image.createGraphics(); revisions.add(g); } 

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

  • 难道我仍然在浪费毫无意义的记忆吗?就像克隆我的BufferedImages
  • 是否有一种不同的方式可以做到这一点?

不,存储Graphics对象通常是一个坏主意。 🙂

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

因此,它不会帮助您实现撤消function。 像素属于组件或图像。 因此,回滚到“之前的” Graphics对象不会将像素修改回到之前的状态。

以下是我所知道的一些方法:

  • 使用命令(命令模式)的“链”来修改图像。 命令模式对于撤消/重做非常好(并且在Swing / AWT in Action )。 从原始开始依次渲染所有命令。 临:每个命令中的状态通常不会太大,使您可以在内存中执行多个撤消缓冲步骤。 骗子:经过很多操作,变得很慢…

  • 对于每一个操作,存储整个BufferedImage (就像你原来那样)。 临:易于实施。 Con:你会很快用完内存。 提示:您可以序列化图像,使撤消/重做占用更less的内存,但需要更多的处理时间。

  • 上述的组合,使用命令模式/链接的想法,但合理时优化与“快照”(如BufferedImages )的渲染。 这意味着您不需要为每个新操作(更快)从一开始就渲染所有内容。 同时将这些快照刷新/序列化到磁盘,以避免内存不足(但如果可以,请将其保存在内存中)。 您也可以将这些命令序列化到磁盘,以进行几乎无限制的撤销。 Pro:正确的时候工作很好。 Con:需要一点时间才能正确的。

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

祝你好运! 🙂

想法#1,存储Graphics对象根本无法工作。 Graphics不应被视为“保存”某些显示内存,而应视为访问显示内存区域的句柄。 在BufferedImage的情况下,每个Graphics对象将总是对同一给定图像内存缓冲区的句柄,所以它们都将表示相同的图像。 更重要的是, 你实际上不能对存储的Graphics做任何事情:因为它们不存储任何东西,所以他们无法“重新存储”任何东西。

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

我的替代方法是将图像修改存储在列表中,从图像顶部的第一个到最后一个呈现。 然后撤消操作就是从列表中删除修改。

这就要求你通过提供一个执行实际绘制的void draw(Graphics gfx)方法来“修正”图像修改 ,即创build一个实现单个修改的类。

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

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

以下简单的演示应用程序显示(以及如何)这一切合作。 它还包括一个很好的“重做”function,用于重做撤消操作。

 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”“旋转angular度:30”

  • 所以你将所有这些参数保存在一个列表中,当你需要撤消时,可以将参数标记为已删除(但是因为你想重做也不要删除它们)。 然后,可以擦除整个canvas,然后根据参数减去标记为已删除的参数重新创build图像。

*像线路这样的东西,你可以将他们的位置存储在一个列表中。

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

  • 在修改之前制作图像的副本
  • 修改它
  • 比较副本与新的修改后的图像
  • 对于没有改变的每个像素,使该像素成为黑色透明像素。

这应该真的压缩在PNG。 尝试黑色和白色,看看是否有区别(我不认为会有,但确保你设置rgb值是相同的事情,不只是阿尔法值,所以它会压缩更好)。

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

然后,因为你有一个alpha通道,所以如果他们撤销,你可以把撤销的图像放回当前图像的顶部,然后设置。