Home · Examples 


Undo Framework Example

Code:

This example shows how to implement undo/redo functionality with the Qt undo framework.

In the Qt undo framework, all actions that the user performs are implemented in classes that inherit QUndoCommand. An undo command class knows how to both redo() - or just do the first time - and undo() an action. For each action the user performs, a command is placed on a QUndoStack. Since the stack contains all commands executed (stacked in chronological order) on the document, it can roll the state of the document backwards and forwards by undoing and redoing its commands. See the overview document for a high-level introduction to the undo framework.

The undo example implements a simple diagram application. It is possible to add and delete items, which are either box or rectangular shaped, and move the items by dragging them with the mouse. The undo stack is shown in a QUndoView, which is a list in which the commands are shown as list items. Undo and redo are available through the edit menu. The user can also select a command from the undo view.

We use the graphics view framework to implement the diagram. We only treat the related code briefly as the framework has examples of its own (e.g., the Diagram Scene Example).

The example consists of the following classes:

The UndoFramework class

public class UndoFramework extends QMainWindow
{
    private QAction deleteAction;
    private QAction addBoxAction;
    private QAction addTriangleAction;
    private QAction undoAction;
    private QAction redoAction;
    private QAction exitAction;
    private QAction aboutAction;

    private QMenu fileMenu;
    private QMenu editMenu;
    private QMenu itemMenu;
    private QMenu helpMenu;

    private DiagramScene diagramScene;
    private QUndoStack undoStack;
    private QUndoView undoView;
The UndoFramework class maintains the undo stack, i.e., it creates
QUndoCommands and pushes and pops them from the stack when it receives the triggered() signal from undoAction and redoAction.

We will start with a look at the constructor:

    public UndoFramework()
    {
        undoStack = new QUndoStack();

        createActions();
        createMenus();

        undoStack.canRedoChanged.connect(redoAction, "setEnabled(boolean)");
        undoStack.canUndoChanged.connect(undoAction, "setEnabled(boolean)");

        createUndoView();

        diagramScene = new DiagramScene();
        diagramScene.setSceneRect(new QRectF(0, 0, 500, 500));

        diagramScene.itemMoved.connect(this, "itemMoved(UndoFramework$DiagramItem,QPointF)");

        setWindowTitle("Undo Framework");
        QGraphicsView view = new QGraphicsView(diagramScene);
        setCentralWidget(view);
        resize(700, 500);
    }
By connecting the undo stack's canRedoChanged() and canUndoChanged() signals to our undo and redo action's setEnabled() slot we make the actions disabled when the stack cannot undo and redo commands.

The rest of the constructor sets up the DiagramScene and QGraphicsView. Notice the syntax used for slots in inner classes.

Here is the createUndoView() method:

    private void createUndoView()
    {
        undoView = new QUndoView(undoStack);
        undoView.setWindowTitle(tr("Command List"));
        undoView.setAttribute(Qt.WidgetAttribute.WA_QuitOnClose, false);

        QDialog dialog = new QDialog(this);
        QVBoxLayout layout = new QVBoxLayout(dialog);
        layout.setContentsMargins(0, 0, 0, 0);
        layout.addWidget(undoView);
        dialog.show();
    }
The QUndoView is a widget that displays the text, which is set with the setText() method, for each QUndoCommand in the undo stack in a list.

Here is the createActions() method:

    private void createActions()
    {
        deleteAction = new QAction(tr("&Delete Item"), this);
        deleteAction.setShortcut(tr("Del"));
        deleteAction.triggered.connect(this, "deleteItem()");
...
        undoAction = new QAction(tr("&Undo"), this);
        undoAction.setShortcut(tr("Ctrl+Z"));
        undoAction.setEnabled(false);
        undoAction.triggered.connect(undoStack, "undo()");

        redoAction = new QAction(tr("&Redo"), this);
        List<QKeySequence> redoShortcuts = new LinkedList<QKeySequence>();
        redoShortcuts.add(new QKeySequence(tr("Ctrl+Y")));
        redoShortcuts.add(new QKeySequence(tr("Shift+Ctrl+Z")));
        redoAction.setShortcuts(redoShortcuts);
        redoAction.setEnabled(false);
        redoAction.triggered.connect(undoStack, "redo()");
The createActions() method sets up all the examples actions in the manner shown above. We can connect our undoAction and redoAction directly to the stack's undo() and redo() slots as we disable the actions when the stack cannot undo and redo. For the other actions we have implemented slots in the UndoFramework class.

Here is the createMenues() method:

    private void createMenus()
    {
...
        editMenu = menuBar().addMenu(tr("&Edit"));
        editMenu.addAction(undoAction);
        editMenu.addAction(redoAction);
        editMenu.addSeparator();
        editMenu.addAction(deleteAction);
        editMenu.aboutToShow.connect(this, "itemMenuAboutToShow()");
        editMenu.aboutToHide.connect(this, "itemMenuAboutToHide()");

...
    }
We have to use the QMenuaboutToShow() and aboutToHide() signals since we only want deleteAction to be enabled when we have selected an item. We also want the text() to be shown in the undoAction and redoAction menu items.

Here is the itemMoved() slot:

    public void itemMoved(DiagramItem movedItem, QPointF oldPosition)
    {
        undoStack.push(new MoveCommand(movedItem, oldPosition));
    }
We push a MoveCommand on the stack, which calls redo() on it.

Here is the deleteItem() slot:

    private void deleteItem()
    {
        if (diagramScene.selectedItems().isEmpty())
            return;

        QUndoCommand deleteCommand = new DeleteCommand(diagramScene);
        undoStack.push(deleteCommand);
    }
An item must be selected to be deleted. We need to check if it is selected as the deleteAction may be enabled even if an item is not selected. This can happen as we do not catch a signal or event when an item is selected.

Here is the itemMenuAboutToShow() and itemMenuAboutToHide() slots:

    private void itemMenuAboutToHide()
    {
        deleteAction.setEnabled(true);
    }

private void itemMenuAboutToShow() { undoAction.setText(tr("Undo ") + undoStack.undoText()); redoAction.setText(tr("Redo ") + undoStack.redoText()); deleteAction.setEnabled(!diagramScene.selectedItems().isEmpty()); }
We implement itemMenuAboutToShow() and itemMenuAboutToHide() to get a dynamic item menu. These slots are connected to the aboutToShow() and aboutToHide() signals. We need this to disable or enable the deleteAction and fill the redoAction and undoAction menu item with the text from the next QUndoCommand that will be redone or undone.

Here is the addBox() slot:

    private void addBox()
    {
        QUndoCommand addCommand = new AddCommand(DiagramType.Box, diagramScene);
        undoStack.push(addCommand);
    }
The addBox() method creates an AddCommand and pushes it on the undo stack.

Here is the addTriangle() sot:

    private void addTriangle()
    {
        QUndoCommand addCommand = new AddCommand(DiagramType.Triangle,
                                                 diagramScene);
        undoStack.push(addCommand);
    }
The addTriangle() method creates an AddCommand and pushes it on the undo stack.

Here is the implementation of about():

    private void about()
    {
        QMessageBox.about(this, tr("About Undo"),
                          tr("The <b>Undo</b> example demonstrates how to " +
                          "use Qt's undo framework."));
    }
The about slot is triggered by the aboutAction and displays an about box for the example.

AddCommand Class

    class AddCommand extends QUndoCommand
    {
        private DiagramItem myDiagramItem;
        private QGraphicsScene myGraphicsScene;
        private QPointF initialPosition;
The AddCommand class adds DiagramItem graphics items to the DiagramScene. We will explain the member variables as we stumble upon them in the implementation.

We start with the constructor:

        public AddCommand(DiagramType addType, QGraphicsScene scene)
        {
            myGraphicsScene = scene;
            myDiagramItem = new DiagramItem(addType);
            initialPosition = new QPointF((UndoFramework.itemCount * 15) % (int) scene.width(),
                              (UndoFramework.itemCount * 15) % (int) scene.height());
            scene.update();
            ++UndoFramework.itemCount;
            setText(tr("Add " + UndoFramework.createCommandString(myDiagramItem, initialPosition)));
        }
We first create the DiagramItem to add to the DiagramScene. The
setText() method let us set a QString that describes the command. We use this to get custom messages in the QUndoView and in the menu of the main window.
        public void redo()
        {
            myGraphicsScene.addItem(myDiagramItem);
            myDiagramItem.setPos(initialPosition);
            myGraphicsScene.clearSelection();
            myGraphicsScene.update();
        }
We set the position of the item as we do not do this in the constructor.
        public void undo()
        {
            myGraphicsScene.removeItem(myDiagramItem);
            myGraphicsScene.update();
        }
undo() removes the item from the scene.

DeleteCommand Class Definition

    class DeleteCommand extends QUndoCommand
    {
        private DiagramItem myDiagramItem;
        private QGraphicsScene myGraphicsScene;
The DeleteCommand class implements the functionality to remove an item from the scene.
        public DeleteCommand(QGraphicsScene scene)
        {
            myGraphicsScene = scene;
            List<QGraphicsItemInterface> list = myGraphicsScene.selectedItems();
            list.get(0).setSelected(false);
            myDiagramItem = (DiagramItem) list.get(0);
            setText("Delete " + UndoFramework.createCommandString(myDiagramItem, myDiagramItem.pos()));
        }
We know that there must be one selected item as it is not possible to create a DeleteCommand unless the item to be deleted is selected and that only one item can be selected at any time. The item must be unselected if it is inserted back into the scene.
        public void undo()
        {
            myGraphicsScene.addItem(myDiagramItem);
            myGraphicsScene.update();
        }
The item is simply reinserted into the scene.
        public void redo()
        {
            myDiagramItem.setPos(newPos);
            setText(tr("Move " + UndoFramework.createCommandString(myDiagramItem, newPos)));
        }
 
The item is removed from the scene.

MoveCommand Class

    class MoveCommand extends QUndoCommand
    {
        private DiagramItem myDiagramItem;
        private QPointF myOldPos;
        private QPointF newPos;
MoveCommand implements the command for moving items.

The constructor of MoveCommand looks like this:

        public MoveCommand(DiagramItem diagramItem, QPointF oldPos)
        {
            myDiagramItem = diagramItem;
            newPos = diagramItem.pos();
            myOldPos = oldPos;
        }
We save both the old and new positions for undo and redo respectively.
        public void undo()
        {
            myDiagramItem.setPos(myOldPos);
            myDiagramItem.scene().update();
            setText(tr("Move " + UndoFramework.createCommandString(myDiagramItem, newPos)));
       }

        @Override

        public void redo()
        {
            myDiagramItem.setPos(newPos);
            setText(tr("Move " + UndoFramework.createCommandString(myDiagramItem, newPos)));
        }
We simply set the items old position and update the scene.
        public void redo()
        {
            myGraphicsScene.addItem(myDiagramItem);
            myDiagramItem.setPos(initialPosition);
            myGraphicsScene.clearSelection();
            myGraphicsScene.update();
        }
We set the item to its new position.
        public boolean mergeWith(QUndoCommand other)
        {
            MoveCommand moveCommand = (MoveCommand) other;
            DiagramItem item = moveCommand.myDiagramItem;

            if (!myDiagramItem.equals(item))
                return false;

            newPos = item.pos();
            setText(tr("Move " + UndoFramework.createCommandString(myDiagramItem, newPos)));

            return true;
        }
The
mergeWith() is reimplemented to make consecutive moves of an item one MoveCommand, i.e, the item will be moved back to the start position of the first move.

Whenever a MoveCommand is created, this method is called to check if it should be merged with the previous command. It is the previous command object that is kept on the stack. The method returns true if the command is merged; otherwise false.

We first check whether it is the same item that has been moved twice, in which case we merge the commands. We update the position of the item so that it will take the last position in the move sequence when undone.

DiagramScene Class

    class DiagramScene extends QGraphicsScene
    {
        public Signal2<DiagramItem,QPointF> itemMoved =
            new Signal2<DiagramItem,QPointF>();

        private DiagramItem movingItem;
        private QPointF oldPos;
...
    }
The DiagramScene implements the functionality to move a DiagramItem with the mouse. It emits a signal when a move is completed. This is caught by the MainWindow, which makes MoveCommands. We do not examine the implementation of DiagramScene as it only deals with graphics framework issues.

We declare a signal that is used to notify UndoFramework that an item has been moved in the scene.

We do not examine the DiagramScene class's implementation in detail, as it does not contain any functionality concerning the undo framework.

The main() Function

main() The main() method of the program looks like this:
    public static void main(String args[])
    {
        QApplication.initialize(args);

        UndoFramework mainWindow = new UndoFramework();
        mainWindow.show();

        QApplication.exec();
    }
The main method creates the MainWindow and shows it as a top level window.


Copyright © 2009 Nokia Corporation and/or its subsidiary(-ies) Trademarks
Qt Jambi 4.5.2_01