Sur cette page

Exemple de cadre d'annulation

Cet exemple montre comment implémenter la fonctionnalité undo/redo avec le framework undo de Qt.

Exemple de diagramme d'annulation

Dans le cadre Qt undo, toutes les actions effectuées par l'utilisateur sont mises en œuvre dans des classes qui héritent de QUndoCommand. Une classe de commande d'annulation sait à la fois comment redo() - ou simplement faire la première fois - et undo() une action. Pour chaque action effectuée par l'utilisateur, une commande est placée sur une pile QUndoStack. Comme la pile contient toutes les commandes exécutées (empilées dans l'ordre chronologique) sur le document, elle peut faire reculer et avancer l'état du document en annulant et en refaisant ses commandes. Voir le document de présentation pour une introduction de haut niveau au cadre d'annulation.

L'exemple d'annulation met en œuvre une application de diagramme simple. Il est possible d'ajouter et de supprimer des éléments, qui ont la forme d'une boîte ou d'un rectangle, et de les déplacer en les faisant glisser à l'aide de la souris. La pile d'annulation est affichée sur un site QUndoView, qui est une liste dans laquelle les commandes sont affichées sous forme d'éléments de liste. L'annulation et le rétablissement sont disponibles dans le menu d'édition. L'utilisateur peut également sélectionner une commande dans la vue d'annulation.

Nous utilisons le cadre des vues graphiques pour mettre en œuvre le diagramme. Nous ne traitons que brièvement le code correspondant, car le cadre a ses propres exemples (par exemple, l'exemple de la scène du diagramme).

L'exemple se compose des classes suivantes :

  • MainWindow est la fenêtre principale et organise les widgets de l'exemple. Il crée les commandes en fonction des entrées de l'utilisateur et les conserve dans la pile de commandes.
  • AddCommand ajoute un élément à la scène.
  • DeleteCommand supprime un élément de la scène.
  • MoveCommand Lorsqu'un élément est déplacé, la commande MoveCommand enregistre les positions de départ et d'arrêt du déplacement et déplace l'élément en fonction de celles-ci lorsque les commandes redo() et undo() sont appelées.
  • DiagramScene hérite de QGraphicsScene et émet des signaux pour MoveComands lorsqu'un élément est déplacé.
  • DiagramItem hérite de QGraphicsPolygonItem et représente un élément dans le diagramme.

Définition de la classe MainWindow

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow();

public slots:
    void itemMoved(DiagramItem *movedDiagram, const QPointF &moveStartPosition);

private slots:
    void deleteItem();
    void addBox();
    void addTriangle();
    void about();
    void updateActions();

private:
    void createActions();
    void createMenus();
    void createToolBars();
    void createUndoView();

    QAction *deleteAction = nullptr;
    QAction *addBoxAction = nullptr;
    QAction *addTriangleAction = nullptr;
    QAction *undoAction = nullptr;
    QAction *redoAction = nullptr;
    QAction *exitAction = nullptr;
    QAction *aboutAction = nullptr;

    QMenu *fileMenu = nullptr;
    QMenu *editMenu = nullptr;
    QMenu *itemMenu = nullptr;
    QMenu *helpMenu = nullptr;

    DiagramScene *diagramScene = nullptr;
    QUndoStack *undoStack = nullptr;
    QUndoView *undoView = nullptr;
};

La classe MainWindow gère la pile d'annulation, c'est-à-dire qu'elle crée des QUndoCommandet les pousse et les retire de la pile lorsqu'elle reçoit le signal triggered() de undoAction et redoAction.

Mise en œuvre de la classe MainWindow

Nous commencerons par examiner le constructeur :

MainWindow::MainWindow()
{
    undoStack = new QUndoStack(this);
    diagramScene = new DiagramScene();

    const QBrush pixmapBrush(QPixmap(":/icons/cross.png").scaled(30, 30));
    diagramScene->setBackgroundBrush(pixmapBrush);
    diagramScene->setSceneRect(QRect(0, 0, 500, 500));

    createActions();
    createMenus();
    createToolBars();

    createUndoView();

    connect(diagramScene, &DiagramScene::itemMoved,
            this, &MainWindow::itemMoved);
    connect(diagramScene, &DiagramScene::selectionChanged,
            this, &MainWindow::updateActions);

    setWindowTitle("Undo Framework");
    QGraphicsView *view = new QGraphicsView(diagramScene);
    setCentralWidget(view);
    adjustSize();
}

Dans le constructeur, nous configurons la DiagramScene et QGraphicsView. Nous voulons que deleteAction ne soit activé que lorsque nous avons sélectionné un élément, c'est pourquoi nous connectons le signal selectionChanged() de la scène à l'emplacement updateActions().

Voici la fonction createUndoView():

void MainWindow::createUndoView()
{
    QDockWidget *undoDockWidget = new QDockWidget;
    undoDockWidget->setWindowTitle(tr("Command List"));
    undoDockWidget->setWidget(new QUndoView(undoStack));
    addDockWidget(Qt::RightDockWidgetArea, undoDockWidget);
}

QUndoView est un widget qui affiche le texte, défini par la fonction setText(), pour chaque QUndoCommand de la pile d'annulation dans une liste. Nous le plaçons dans un widget d'ancrage.

Voici la fonction createActions():

void MainWindow::createActions()
{
    deleteAction = new QAction(QIcon(":/icons/remove.png"), tr("&Delete Item"), this);
    deleteAction->setShortcut(tr("Del"));
    connect(deleteAction, &QAction::triggered, this, &MainWindow::deleteItem);
    ...
    undoAction = undoStack->createUndoAction(this, tr("&Undo"));
    undoAction->setIcon(QIcon(":/icons/undo.png"));
    undoAction->setShortcuts(QKeySequence::Undo);

    redoAction = undoStack->createRedoAction(this, tr("&Redo"));
    redoAction->setIcon(QIcon(":/icons/redo.png"));
    redoAction->setShortcuts(QKeySequence::Redo);

La fonction createActions() met en place toutes les actions des exemples de la manière décrite ci-dessus. Les méthodes createUndoAction() et createRedoAction() nous aident à créer des actions qui sont désactivées et activées en fonction de l'état de la pile. De plus, le texte de l'action sera mis à jour automatiquement en fonction de text() des commandes d'annulation. Pour les autres actions, nous avons implémenté des slots dans la classe MainWindow.

    ...
    updateActions();
}

void MainWindow::updateActions()
{
    deleteAction->setEnabled(!diagramScene->selectedItems().isEmpty());
}

Une fois que toutes les actions sont créées, nous mettons à jour leur état en appelant la même fonction qui est connectée au signal selectionChanged de la scène.

Les fonctions createMenus() et createToolBars() ajoutent les actions aux menus et aux barres d'outils :

void MainWindow::createMenus()
{
    fileMenu = menuBar()->addMenu(tr("&File"));
    fileMenu->addAction(exitAction);

    editMenu = menuBar()->addMenu(tr("&Edit"));
    editMenu->addAction(undoAction);
    editMenu->addAction(redoAction);
    editMenu->addSeparator();
    editMenu->addAction(deleteAction);
    ...
    helpMenu = menuBar()->addMenu(tr("&About"));
    helpMenu->addAction(aboutAction);
}

void MainWindow::createToolBars()
{
    QToolBar *editToolBar = new QToolBar;
    editToolBar->addAction(undoAction);
    editToolBar->addAction(redoAction);
    editToolBar->addSeparator();
    editToolBar->addAction(deleteAction);
    ...
    addToolBar(editToolBar);
    addToolBar(itemToolBar);
}

Voici l'emplacement itemMoved():

void MainWindow::itemMoved(DiagramItem *movedItem,
                           const QPointF &oldPosition)
{
    undoStack->push(new MoveCommand(movedItem, oldPosition));
}

Nous plaçons simplement une MoveCommand sur la pile, qui appelle redo().

Voici l'emplacement deleteItem():

void MainWindow::deleteItem()
{
    if (diagramScene->selectedItems().isEmpty())
        return;

    QUndoCommand *deleteCommand = new DeleteCommand(diagramScene);
    undoStack->push(deleteCommand);
}

Un élément doit être sélectionné pour être supprimé. Nous devons vérifier s'il est sélectionné, car le site deleteAction peut être activé même si un élément n'est pas sélectionné. Cela peut se produire si nous ne recevons pas de signal ou d'événement lorsqu'un élément est sélectionné.

Voici l'emplacement addBox():

void MainWindow::addBox()
{
    QUndoCommand *addCommand = new AddCommand(DiagramItem::Box, diagramScene);
    undoStack->push(addCommand);
}

La fonction addBox() crée une AddCommand et la place sur la pile d'annulation.

Voici le slot addTriangle():

void MainWindow::addTriangle()
{
    QUndoCommand *addCommand = new AddCommand(DiagramItem::Triangle,
                                              diagramScene);
    undoStack->push(addCommand);
}

La fonction addTriangle() crée une AddCommand et la place sur la pile d'annulation.

Voici l'implémentation de about():

void MainWindow::about()
{
    QMessageBox::about(this, tr("About Undo"),
                       tr("The <b>Undo</b> example demonstrates how to "
                          "use Qt's undo framework."));
}

Le slot about est déclenché par la fonction aboutAction et affiche une boîte about pour l'exemple.

Définition de la classe AddCommand

class AddCommand : public QUndoCommand
{
public:
    AddCommand(DiagramItem::DiagramType addType, QGraphicsScene *graphicsScene,
               QUndoCommand *parent = nullptr);
    ~AddCommand();

    void undo() override;
    void redo() override;

private:
    DiagramItem *myDiagramItem;
    QGraphicsScene *myGraphicsScene;
    QPointF initialPosition;
};

La classe AddCommand ajoute des éléments graphiques DiagramItem à la DiagramScene.

Mise en oeuvre de la classe AddCommand

Nous commençons par le constructeur :

AddCommand::AddCommand(DiagramItem::DiagramType addType,
                       QGraphicsScene *scene, QUndoCommand *parent)
    : QUndoCommand(parent), myGraphicsScene(scene)
{
    static int itemCount = 0;

    myDiagramItem = new DiagramItem(addType);
    initialPosition = QPointF((itemCount * 15) % int(scene->width()),
                              (itemCount * 15) % int(scene->height()));
    scene->update();
    ++itemCount;
    setText(QObject::tr("Add %1")
        .arg(createCommandString(myDiagramItem, initialPosition)));
}

Nous créons d'abord le DiagramItem à ajouter à la DiagramScene. La fonction setText() nous permet de définir un QString qui décrit la commande. Nous l'utilisons pour obtenir des messages personnalisés sur le site QUndoView et dans le menu de la fenêtre principale.

void AddCommand::undo()
{
    myGraphicsScene->removeItem(myDiagramItem);
    myGraphicsScene->update();
}

undo() supprime l'élément de la scène.

void AddCommand::redo()
{
    myGraphicsScene->addItem(myDiagramItem);
    myDiagramItem->setPos(initialPosition);
    myGraphicsScene->clearSelection();
    myGraphicsScene->update();
}

Nous définissons la position de l'élément car nous ne le faisons pas dans le constructeur.

Définition de la classe DeleteCommand

class DeleteCommand : public QUndoCommand
{
public:
    explicit DeleteCommand(QGraphicsScene *graphicsScene, QUndoCommand *parent = nullptr);

    void undo() override;
    void redo() override;

private:
    DiagramItem *myDiagramItem;
    QGraphicsScene *myGraphicsScene;
};

La classe DeleteCommand met en œuvre la fonctionnalité permettant de supprimer un élément de la scène.

Mise en œuvre de la classe DeleteCommand

DeleteCommand::DeleteCommand(QGraphicsScene *scene, QUndoCommand *parent)
    : QUndoCommand(parent), myGraphicsScene(scene)
{
    QList<QGraphicsItem *> list = myGraphicsScene->selectedItems();
    list.first()->setSelected(false);
    myDiagramItem = static_cast<DiagramItem *>(list.first());
    setText(QObject::tr("Delete %1")
        .arg(createCommandString(myDiagramItem, myDiagramItem->pos())));
}

Nous savons qu'il doit y avoir un élément sélectionné car il n'est pas possible de créer une commande DeleteCommand si l'élément à supprimer n'est pas sélectionné et qu'un seul élément peut être sélectionné à la fois. L'élément doit être désélectionné s'il est réinséré dans la scène.

void DeleteCommand::undo()
{
    myGraphicsScene->addItem(myDiagramItem);
    myGraphicsScene->update();
}

L'élément est simplement réinséré dans la scène.

void DeleteCommand::redo()
{
    myGraphicsScene->removeItem(myDiagramItem);
}

L'élément est supprimé de la scène.

Définition de la classe MoveCommand

class MoveCommand : public QUndoCommand
{
public:
    enum { Id = 1234 };

    MoveCommand(DiagramItem *diagramItem, const QPointF &oldPos,
                QUndoCommand *parent = nullptr);

    void undo() override;
    void redo() override;
    bool mergeWith(const QUndoCommand *command) override;
    int id() const override { return Id; }

private:
    DiagramItem *myDiagramItem;
    QPointF myOldPos;
    QPointF newPos;
};

La commande mergeWith() est réimplémentée pour faire des déplacements consécutifs d'un élément une seule commande de déplacement, c'est-à-dire que l'élément sera déplacé jusqu'à la position de départ du premier déplacement.

Mise en œuvre de la classe MoveCommand

Le constructeur de MoveCommand ressemble à ceci :

MoveCommand::MoveCommand(DiagramItem *diagramItem, const QPointF &oldPos,
                         QUndoCommand *parent)
    : QUndoCommand(parent), myDiagramItem(diagramItem)
    , myOldPos(oldPos), newPos(diagramItem->pos())
{
}

Nous enregistrons l'ancienne et la nouvelle position pour les annuler et les rétablir respectivement.

void MoveCommand::undo()
{
    myDiagramItem->setPos(myOldPos);
    myDiagramItem->scene()->update();
    setText(QObject::tr("Move %1")
        .arg(createCommandString(myDiagramItem, newPos)));
}

Nous fixons simplement l'ancienne position de l'élément et mettons à jour la scène.

void MoveCommand::redo()
{
    myDiagramItem->setPos(newPos);
    setText(QObject::tr("Move %1")
        .arg(createCommandString(myDiagramItem, newPos)));
}

Nous plaçons l'élément à sa nouvelle position.

bool MoveCommand::mergeWith(const QUndoCommand *command)
{
    const MoveCommand *moveCommand = static_cast<const MoveCommand *>(command);
    DiagramItem *item = moveCommand->myDiagramItem;

    if (myDiagramItem != item)
        return false;

    newPos = item->pos();
    setText(QObject::tr("Move %1")
        .arg(createCommandString(myDiagramItem, newPos)));

    return true;
}

Chaque fois qu'une MoveCommand est créée, cette fonction est appelée pour vérifier si elle doit être fusionnée avec la commande précédente. C'est l'objet de la commande précédente qui est conservé sur la pile. La fonction renvoie vrai si la commande est fusionnée, sinon faux.

Nous vérifions d'abord s'il s'agit du même élément qui a été déplacé deux fois, auquel cas nous fusionnons les commandes. Nous mettons à jour la position de l'élément de manière à ce qu'il prenne la dernière position dans la séquence de déplacement lorsqu'il est annulé.

Définition de la classe DiagramScene

class DiagramScene : public QGraphicsScene
{
    Q_OBJECT

public:
    DiagramScene(QObject *parent = nullptr);

signals:
    void itemMoved(DiagramItem *movedItem, const QPointF &movedFromPosition);

protected:
    void mousePressEvent(QGraphicsSceneMouseEvent *event) override;
    void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override;

private:
    QGraphicsItem *movingItem = nullptr;
    QPointF oldPos;
};

La DiagramScene implémente la fonctionnalité de déplacement d'un élément de diagramme à l'aide de la souris. Elle émet un signal lorsque le déplacement est terminé. Ce signal est capté par MainWindow, qui émet des MoveCommands. Nous n'examinons pas l'implémentation de DiagramScene car elle ne traite que des questions relatives au cadre graphique.

La fonction main()

La fonction main() du programme se présente comme suit :

int main(int argv, char *args[])
{
    QApplication app(argv, args);

    MainWindow mainWindow;
    mainWindow.show();

    return app.exec();
}

Nous dessinons une grille en arrière-plan de la DiagramScene, nous utilisons donc un fichier de ressources. Le reste de la fonction crée le site MainWindow et l'affiche en tant que fenêtre de premier niveau.

Exemple de projet @ code.qt.io

© 2026 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.