Walletfox.com

QUndoCommand example for changing color of a QGraphicsItem


This article demonstrates how to use Qt's undo/redo functionality on an example that changes the interior color of a QGraphicsItem. The situation is illustrated in the figure below:

To implement the undo/redo functionality we need to introduce:

  • QUndoStack object onto which we push all the commands. QUndoStack is responsible for storing all the commands on the stack and their manipulation.
  • two extra QActions, namely undoAction and redoAction which we connect to the QUndoStack object
  • function slot changeItemsColor() that is triggered anytime we set a color in the colorComboBox
  • ChangeItemColorCommand as a subclass of QUndoCommand. We need to implement its undo() and redo() methods.

Implementation of MainWindow

Below you can see the declaration of the MainWindow with the above-mentioned requirements. Notice the undoStack, undoAction and redoAction. Also note that we added the slot changeItemsColor().

class MainWindow : public QMainWindow
{
    Q_OBJECT
    
public:
    MainWindow(QWidget *parent = 0);
public slots:
    void changeItemsColor();
private:
    QGraphicsView* view;
    QGraphicsScene* scene;

    QUndoStack *undoStack;
    QAction *undoAction;
    QAction *redoAction;

    QToolBar* editToolBar;
    QComboBox* colorComboBox;

    void createSceneAndView();
    void createWidgets();
    void createUndoStackAndActions();
    void createToolBar();
    void createGraphicsItems();
};

In the MainWindow constructor we create the widgets, actions, view, scene and populate it with items. The details of the implementation can be found in the individual files above. The important thing is to connect the colorComboBox signal activated() to the slot changeItemsColor().

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
{
    resize(400,350);
    createSceneAndView();
    createGraphicsItems();
    createWidgets();
    createUndoStackAndActions();
    createToolBar();

    QObject::connect(colorComboBox, SIGNAL(activated(int)),
                     this, SLOT(changeItemsColor()));
}

For undo/redo functionality we only need to focus on two methods, namely createUndoStackAndActions() and changeItemsColor().

How to create QUndoStack object and undo/redo actions

In createUndoStackAndActions() we create an QUndoStack object, initialize undoAction and redoAction and connect them to the QUndoStack object. As a result, every triggering of the undo/redo action will cause a call to QUndoStack::undo() and redo() which will, in turn, call the undo() or redo() method of a specific command. Note that the name of the stack - QUndoStack - should not be understood literally. The QUndoStack object takes care of both undo and redo functionality.

void MainWindow::createUndoStackAndActions(){
    undoStack = new QUndoStack(this);
    undoAction = undoStack->createUndoAction(this, tr("&Undo"));
    undoAction->setIcon(QIcon(":/icons/undo.png"));
    redoAction = undoStack->createRedoAction(this, tr("&Redo"));
    redoAction->setIcon(QIcon(":/icons/redo.png"));
}

How to implement the slot changeItemsColor()

The slot changeItemsColor() is called every time we set a color in the colorComboBox. In situations without undo/redo, changeItemsColor() would directly change the color of the selected items. However, in the undo/redo situation we do not directly change the color, instead we push the ChangeItemColorCommand onto the undoStack.

void MainWindow::changeItemsColor(){
    if(scene->selectedItems().isEmpty())
        return;

    int nrFillItems = 0;
    foreach(QGraphicsItem* item, scene->selectedItems()){
        if(dynamic_cast<QAbstractGraphicsShapeItem*> (item))
            nrFillItems++;
    }
    if(nrFillItems == 0)
        return;

    int currentIndex = colorComboBox->currentIndex();
    QColor currrentColor = colorComboBox->itemData(
                currentIndex).value<QColor>();
    undoStack->push(new ChangeItemColorCommand(scene, currrentColor));
}

The first two parts of the code check whether the command should be pushed onto the stack. E.g. if nothing is selected we do not want the command to be constructed or appear on the stack. The same is valid for the case when there is no item with brush property among the selected items (e.g. if all the items are QGraphicsLineItem). On the other hand, if at least one of the items has the brush property, the command will be executed.

In the last part of the code you can see that if all the conditions are met, the command is constructed and pushed onto the stack. This push onto the undoStack causes simultaneously a call to:

  • ChangeItemColorCommand constructor
  • ChangeItemColorCommand::redo()

How to implement and use the ChangeItemColorCommand

Now let's look at the implementation of the ChangeItemColorCommand. This command should be able to take several items and change their color. For that we need to store:

  • a list of selected items and their original colors. We do this with the help of QMap<QAbstractGraphicsShapeItem*, QColor> oldColorMap
  • the new color stored in QColor nColor
  • pointer to the QGraphicsScene which we need for scene updates
class ChangeItemColorCommand : public QUndoCommand
{
public:
    ChangeItemColorCommand(QGraphicsScene *scene, 
                      QColor nColor, QUndoCommand *parent = 0);
    void undo();
    void redo();
private:
    QGraphicsScene* scene;
    QMap<QAbstractGraphicsShapeItem*, QColor> oldColorMap;
    QColor nColor;
    void resetColor(const int commandType);
};

In the constructor we populate the oldColorMap with pointers to the selected items and their original colors. We also store the value of the new color. Note the cast to AbstractQGraphicsShapeItem which ensures that we only handle items that have the brush property.

ChangeItemColorCommand::ChangeItemColorCommand(QGraphicsScene *scene,
                          QColor nColor, QUndoCommand *parent):
    QUndoCommand(parent)
{
    this->scene = scene;
    foreach(QGraphicsItem* item, scene->selectedItems()){
        if(QAbstractGraphicsShapeItem* aItem =
                dynamic_cast<QAbstractGraphicsShapeItem*> (item)){
            oldColorMap.insert(aItem, aItem->brush().color());
        }
    }
    this->nColor = nColor;
}

Next we have to implement undo() and redo(). Both methods call a helper method resetColor(). The command types (UndoCommandType, RedoCommandType) were declared at the top of the source file as const int.

void ChangeItemColorCommand::redo(){
    resetColor(RedoCommandType);
}

void ChangeItemColorCommand::undo(){
    resetColor(UndoCommandType);
}

In the method resetColor() we iterate over the map and store the current brush of an item in a helper variable aBrush. In the case of undo, the color of aBrush will be set to the original value stored in the oldColorMap. In the case of redo, the color of aBrush will be set to the new color stored in nColor. Last, we change the current brush of an item to aBrush.

void ChangeItemColorCommand::resetColor(const int commandType){
    QMapIterator<QAbstractGraphicsShapeItem*, QColor> i(oldColorMap);
    while (i.hasNext()) {
         i.next();
         QBrush aBrush = i.key()->brush();
         if(commandType == UndoCommandType)
             aBrush.setColor(i.value());
         else
             aBrush.setColor(nColor);
         i.key()->setBrush(aBrush);
    }
    scene->update();
}

To sum up this article, for any new type of command you need to subclass QUndoCommand, implement undo() and redo(). You also need to create an extra slot which reacts to a widget signal or an event. However, normally you only need to construct one QUndoStack object as well as one pair of undoAction, redoAction for your application.

That's it. To try out the example you first need to select one or multiple items and change their color. After that the undo/redo buttons become active.

Tagged: Qt