Part 9: Implementing undo commands

Part 9: Implementing undo commands


[<< Contents] [<< Prev] [Next >>]

Undo commands

Qt's undo framework uses the Command pattern for implementing undo/redo functionality in applications. The Command pattern is based on the idea that all editing in an application is done by creating instances of command objects. Each command also knows how to undo its changes. Here we will implement the undo/redo commands for adding, moving and deleting stations by implementing three new classes derived from QUndoCommand called "CommandStationAdd", "CommandStationMove" and "CommandStationDelete".

The important part to notice about the command classes is that they provide implementations for redo and undo functions. It is these functions that make the edits, not the class constructor or destructor. When an instance of a command object is pushed onto the undo stack, the redo function is called by the Qt undo framework to do the desired edit. Also in the constructor a short string describing what the command does is defined that is displayed in the undo stack view and on the undo/redo actions.

Creating the command classes

As our command classes will be quite simple they will be implemented totally within their header files, i.e. no cpp file will be needed. Therefore when adding them to the KDevelop project, add them as before using the KDevelop "New Class..." functionality but ensure the "Create only header" tick box is selected for each of them.

commandstationadd.h

Our first command class is to add new stations to the scene. The constructor takes in a pointer to the scene and the x/y coordinates as parameters and creates the station but does not add it to the scene. Adding the station to the scene is done in the redo function, and the reverse (i.e. removing the station from the scene) is done in the undo function. A class destructor is also provided to delete the created station if unused to prevent any memory leak as add station commands are created and deleted.

#ifndef COMMANDSTATIONADD_H
#define COMMANDSTATIONADD_H

#include <QUndoCommand>
#include <QGraphicsScene>

/*************************************************************************************/
/***************** Undostack command for adding a station to a scene *****************/
/*************************************************************************************/

class CommandStationAdd : public QUndoCommand
{
public:
  CommandStationAdd( QGraphicsScene* scene, qreal x, qreal y )
    {
      m_station = new Station( x, y );
      m_scene   = scene;
      setText( QString("Station add %1,%2").arg(x).arg(y) );
    }

  ~CommandStationAdd()
    {
      // if station not on scene then delete station
      if ( !m_scene->items().contains( m_station ) )
        delete m_station;
    }

  virtual void undo()    { m_scene->removeItem( m_station ); }

  virtual void redo()    { m_scene->addItem( m_station ); }

private:
  Station*         m_station;    // station being added
  QGraphicsScene*  m_scene;      // scene where station being added
};

#endif  // COMMANDSTATIONADD_H

commandstationmove.h

Our second command class is to move stations on the scene. The constructor takes in a pointer to the station and the before and after x/y coordinates. Moving the station to the after x/y coordinates is done in the redo function, and moving the station back to the before x/y coordinates is done the the undo function.

#ifndef COMMANDSTATIONMOVE_H
#define COMMANDSTATIONMOVE_H

#include "station.h"

#include <QUndoCommand>

/*************************************************************************************/
/***************** Undostack command for moving a station on a scene *****************/
/*************************************************************************************/

class CommandStationMove : public QUndoCommand
{
public:
  CommandStationMove( Station* station, qreal fromX, qreal fromY, qreal toX, qreal toY )
  {
    m_station = station;
    m_from    = QPointF( fromX, fromY );
    m_to      = QPointF( toX, toY );
    setText( QString("Move station %1,%2 -> %3,%4").arg(fromX).arg(fromY).arg(toX).arg(toY) );
  }

  virtual void undo()    { m_station->setPos( m_from ); }

  virtual void redo()    { m_station->setPos( m_to ); }

private:
  Station*   m_station;       // station being moved
  QPointF    m_from;          // original coords
  QPointF    m_to;            // new coords
};

#endif  // COMMANDSTATIONMOVE_H

commandstationdelete.h

Our third command class is to delete stations from the scene. The constructor takes in a pointer to the station and the scene. Removing the station from the scene is done in the redo function, and adding the station back to the scene is done the the undo function.

#ifndef COMMANDSTATIONDELETE_H
#define COMMANDSTATIONDELETE_H

#include <QUndoCommand>
#include <QGraphicsScene>

/*************************************************************************************/
/*************** Undostack command for deleting a station from a scene ***************/
/*************************************************************************************/

class CommandStationDelete : public QUndoCommand
{
public:
  CommandStationDelete( QGraphicsScene* scene, Station* station )
  {
    m_scene   = scene;
    m_station = station;
    setText( QString("Station delete %1,%2").arg(station->x()).arg(station->y()) );
  }

  virtual void undo()    { m_scene->addItem( m_station ); }

  virtual void redo()    { m_scene->removeItem( m_station ); }

private:
  QGraphicsScene*  m_scene;      // scene where station being deleted
  Station*         m_station;    // station being deleted
};

#endif  // COMMANDSTATIONDELETE_H

Enhancing the Scene

Now that we have created the command classes, we need to modify the scene class to use them instead of making the edit changes directly itself.

scene.cpp

Include the command classes header files.

#include "commandstationadd.h"
#include "commandstationmove.h"
#include "commandstationdelete.h"

In the mousePressEvent method replace the addItem line with the line below that creates a CommandStationAdd object and pushes it onto the undo stack.

  m_undoStack->push( new CommandStationAdd( this, x, y ) );

In the mouseReleaseEvent method just before setting m_movingStation to zero add the line below that creates a CommandStationMove object and pushes it onto the undo stack.

    m_undoStack->push( new CommandStationMove( m_movingStation,
                             m_movingFromX, m_movingFromY, x, y ) );

In the contextMenuEvent method replace the two lines that removeItem and delete the station with the single line below that creates a CommandStationDelete object and pushes it onto the undo stack.

    m_undoStack->push( new CommandStationDelete( this, station ) );

Compile and run

The new code will automatically re-compile when you attempt to run the application. Play with the application and investigate the new undo and redo functionality we have just added.


[<< Contents] [<< Prev] [Next >>]


Last updated 22-May-2008