|
Strategies for Implementing Undo/RedoThe way that undo/redo is implemented in your application will be determined by the nature of the document data and how the user manipulates that data. In our simple Doodle example, we only operate on one piece of data at a time, and our operations are very simple. In many applications, however, the user can apply operations to multiple objects. In this case, it's useful to introduce a further level of command granularity, perhaps called CommandState, holding the information for a particular object in the document. Your command class should maintain a list of these states and accept a list of them in the command class constructor. Do and Undo will iterate through the state list and apply the current command to each state. One key to implementing undo/redo is noting that the user can only traverse the command history one step at a time. Therefore, your undo/redo implementation is free to take snapshots of the document state for later restoration, with the knowledge that the stored state will always be correct no matter how many times the user has gone backward or forward in the command history. All your code has to know is how to switch between "done" and "undone" states. A common strategy is to store a copy of each document object within the command state object and also a pointer to the actual object. Do and Undo simply swap the current and stored states. Let's say the object is a shape, and the user changes the color from red to blue. The application creates a new state identical to the existing red object, but it sets the internal color attribute to blue. When the command is first executed, Do takes a copy of the current object state, applies the new command state (including the blue color) to the visible state, and repaints the object. Undo simply does the same thing. It takes a copy of the current object state, applies the current state (including red), and repaints. So code for Do and Undo is actually identical in this case. Not only that, it can be reused for other operations as well as color changes because the state of the entire object is copied. You can make this process straightforward by implementing an assignment operator and copy constructor for each class that represents an object that the user can edit. Let's make this idea more concrete. Say we have a document of shapes, and the user can change the color of all selected shapes. We might have a command handler called ShapeView::OnChangeColor, as in the following, where a new state is created for each selected object, before being applied to the document. // Changes the color of the selected shape(s) void ShapeView::OnChangeColor(wxCommandEvent& event) { wxColour col = GetSelectedColor(); ShapeCommand* cmd = new ShapeCommand(wxT("Change color")); ShapeArray arrShape; for (size_t i = 0; i < GetSelectedShapes().GetCount(); i++) { Shape* oldShape = GetSelectedShapes()[i]; Shape* newShape = new Shape(*oldShape); newShape->SetColor(col); ShapeState* state = new ShapeState(SHAPE_COLOR, newShape, oldShape); cmd->AddState(state); } GetDocument()->GetCommandProcessor()->SubmitCommand(cmd); } Because the implementation for this kind of state change is the same for both Do and Undo, we have a single DoAndUndo function in the ShapeState class, where we do state swapping: // Incomplete implementation of the state's DoAndRedo: // for some commands, do and undo share the same code void ShapeState::DoAndUndo(bool undo) { switch (m_cmd) { case SHAPE_COLOR: case SHAPE_TEXT: case SHAPE_SIZE: { Shape* tmp = new Shape(m_actualShape); (* m_actualShape) = (* m_storedShape); (* m_storedShape) = (* tmp); delete tmp; // Do redraw here ... break; } } } In this code, we have not shown the ShapeCommand::Do and ShapeCommand::Redo functions, which iterate through all the states for this command. |
|