Idea #1,仅存储
Graphics
对象是不行的。
Graphics
不应被视为“持有”某些显示内存,而应被视为访问显示内存区域的句柄。在
BufferedImage
的情况下,每个
Graphics
对象总是将是访问相同给定图像内存缓冲区的句柄,因此它们都代表相同的图像。更重要的是,
您实际上无法对存储的Graphics
进行任何操作:因为他们不存储任何内容,所以根本没有任何方法可以“重新存储”任何内容。
Idea #2,复制
BufferedImage
是一个更好的想法,但您确实会浪费内存并很快用完它。仅存储受绘制影响的图像部分(例如使用矩形区域)可以帮助减少内存消耗,但仍然需要大量内存。将这些撤消图像缓冲到磁盘可能有所帮助,但这将使您的用户界面变慢且不响应,这是
不好的;此外,这会使您的应用程序更加
复杂和容易出错。
我提出的替代方案是将图像修改存储在列表中,从首次渲染到最后渲染在图像上。撤消操作仅由列表中的修改被移除。
这需要您
"实现"图像修改,即创建一个实现单个修改的类,通过提供一个执行实际绘制的
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());
}
private final LinkedList<ImageModification> undoable = new LinkedList<>();
private final LinkedList<ImageModification> undone = new LinkedList<>();
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() {
final JPanel drawPanel = new JPanel() {
@Override
public void paintComponent(Graphics gfx) {
super.paintComponent(gfx);
gfx.drawImage(image, 0, 0, null);
for (ImageModification action: undoable) {
action.draw(gfx, image.getWidth(), image.getHeight());
}
}
@Override
public Dimension getPreferredSize() {
return new Dimension(image.getWidth(), image.getHeight());
}
};
JButton drawButton = new JButton("Draw");
JButton undoButton = new JButton("Undo");
JButton redoButton = new JButton("Redo");
drawButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
if (undoable.size() == MAX_UNDO_COUNT) {
ImageModification first = undoable.removeFirst();
Graphics imageGfx = image.getGraphics();
first.draw(imageGfx, image.getWidth(), image.getHeight());
imageGfx.dispose();
}
undoable.addLast(new ExampleRandomModification());
undone.clear();
drawPanel.repaint();
}
});
undoButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
if (!undoable.isEmpty()) {
ImageModification lastDrawn = undoable.removeLast();
undone.addLast(lastDrawn);
drawPanel.repaint();
}
}
});
redoButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
if (!undone.isEmpty()) {
ImageModification lastUndone = undone.removeLast();
undoable.addLast(lastUndone);
drawPanel.repaint();
}
}
});
JPanel buttonPanel = new JPanel(new FlowLayout());
buttonPanel.add(drawButton);
buttonPanel.add(undoButton);
buttonPanel.add(redoButton);
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);
}
private static final Random SEEDS = new Random();
private interface ImageModification
{
void draw(Graphics gfx, int width, int height);
}
private static class ExampleRandomModification implements ImageModification
{
private final long seed;
public ExampleRandomModification() {
this.seed = SEEDS.nextLong();
}
@Override
public void draw(Graphics gfx, int width, int height) {
Random random = new Random(seed);
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));
}
}
}
}
AbstractAction
类开始(如果需要,可以阅读有关命令模式的内容!)。至于图像序列化,您可以使用ImageIO
和无损格式(如BMP、PNG或TIFF)。或者,您可以将BufferedImage
的byte
或int
后备数组存储到磁盘上,假设您的程序使用标准化的颜色模型。 - Harald K