I'm currently in the process of writing a paint program in java, designed to have flexible and comprehensive functionalities. It stemmed from my final project, that I wrote overnight the day before. Because of that, it's got tons and tons of bugs, which I've been tackling one by one (e.g. I can only save files that will be empty, my rectangles don't draw right but my circles do...).
This time, I've been trying to add undo/redo functionality to my program. However, I can't "undo" something that I have done. Therefore, I got an idea to save copies of my BufferedImage
each time a mouseReleased
event was fired. However, with some of the images going to 1920x1080 resolution, I figured that this wouldn't be efficient: storing them would probably take gigabytes of memory.
The reason for why I can't simply paint the same thing with the background colour to undo is because I have many different brushes, which paint based on Math.random()
, and because there are many different layers (in a single layer).
Then, I've considered cloning the Graphics
objects that I use to paint to the BufferedImage
. Like this:
ArrayList<Graphics> revisions = new ArrayList<Graphics>(); @Override public void mouseReleased(MouseEvent event) { Graphics g = image.createGraphics(); revisions.add(g); }
I haven't done this before, so I have a couple questions:
BufferedImages
?No, storing a Graphics
object is usually a bad idea. :-)
Here's why: Normally, Graphics
instances are short-lived and is used to paint or draw onto some kind of surface (typically a (J)Component
or a BufferedImage
). It holds the state of these drawing operations, like colors, stroke, scale, rotation etc. However, it does not hold the result of the drawing operations or the pixels.
Because of this, it won't help you achieve undo-functionality. The pixels belongs to the component or image. So, rolling back to a "previous" Graphics
object will not modify the pixels back to the previous state.
Here's some approaches I know works:
Use a "chain" of commands (command pattern) to modify the image. Command pattern works very nice with undo/redo (and is implemented in Swing/AWT in Action
). Render all commands in sequence, starting from the original. Pro: The state in each command is usually not so large, allowing you to have many steps of undo-buffer in memory. Con: After a lot of operations, it becomes slow...
For every operation, store the entire BufferedImage
(as you originally did). Pro: Easy to implement. Con: You'll run out of memory fast. Tip: You could serialize the images, making undo/redo taking less memory, at the cost of more processing time.
A combination of the above, using command pattern/chain idea, but optimizing the rendering with "snapshots" (as BufferedImages
) when reasonable. Meaning you won't need to render everything from the beginning for each new operation (faster). Also flush/serialize these snapshots to disk, to avoid running out of memory (but keep them in memory if you can, for speed). You could also serialize the commands to disk, for virtually unlimited undo. Pro: Works great when done right. Con: Will take some time to get right.
PS: For all of the above, you need to use a background thread (like SwingWorker
or similar) to update the displayed image, store commands/images to disk etc in the background, to keep a responsive UI.
Good luck! :-)
Idea #1, storing the Graphics
objects simply wouldn't work. The Graphics
should not be considered as "holding" some display memory, but rather as a handle to access an area of display memory. In the case of BufferedImage
, each Graphics
object will be always the handle to the same given image memory buffer, so they all will represent the same image. Even more importantly, you can't actually do anything with the stored Graphics
: As they do not store anything, there is no way whatsoever they could "re-store" anything.
Idea #2, cloning the BufferedImage
s is a much better idea, but you'll indeed be wasting memory, and quickly run out of it. It helps only to store those parts of the image affected by the draw, for example using rectangular areas, but it still costs a lot of memory. Buffering those undo images to disk could help, but it will make your UI slow and unresponsive, and that's bad; furthermore, it makes you application more complex and error-prone.
My alternative would be to store store the image modifications in a list, rendered from first to last on top of the image. An undo operation then simply consists of removing the modification from the list.
This requires you to "reify" the image modifications, i.e. create a class that implements a single modification, by providing a void draw(Graphics gfx)
method which performs the actual drawing.
As you said, random modifications pose an additional problem. However, the key problem is your use of Math.random()
to create random numbers. Instead, perform each random modification with a Random
created from a fixed seed value, so that the (pseudo-)random number sequences are the same on each invocation of draw()
, i.e., each draw has exactly the same effects. (That's why they are called "pseudo-random" -- the generated numbers look random, but they are just as deterministic as any other function.)
In contrast to the image storing technique, which has memory problems, the problem with this technique is that many modifications may make the GUI slow, especially if the modifications are computationally intensive. To prevent this, the simplest way would be to fix an appropriate maximum size of the list of undoable modifications. If this limit would be exceeded by adding a new modification, remove the oldest modification the list and apply it to the backing BufferedImage
itself.
The following simple demo application shows that (and how) this all works together. It also includes a nice "redo" feature for redoing undone actions.
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)); } } } }
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With