Undo Framework Beispiel
Dieses Beispiel zeigt, wie die Undo/Redo-Funktionalität mit dem Qt Undo-Framework implementiert werden kann.
Im Qt Undo-Framework werden alle Aktionen, die der Benutzer ausführt, in Klassen implementiert, die von QUndoCommand erben. Eine Rückgängig-Befehlsklasse weiß, wie man sowohl redo() - oder einfach das erste Mal - als auch undo() eine Aktion ausführen kann. Für jede Aktion, die der Benutzer ausführt, wird ein Befehl auf einem QUndoStack abgelegt. Da der Stapel alle Befehle enthält, die auf dem Dokument ausgeführt wurden (in chronologischer Reihenfolge), kann er den Zustand des Dokuments durch Rückgängigmachen und Wiederherstellen seiner Befehle rückwärts und vorwärts rollen. Eine allgemeine Einführung in das Undo-Framework finden Sie im Übersichtsdokument.
Das Rückgängig-Beispiel implementiert eine einfache Diagramm-Anwendung. Es ist möglich, Elemente hinzuzufügen und zu löschen, die entweder kastenförmig oder rechteckig sind, und die Elemente durch Ziehen mit der Maus zu verschieben. Der Rückgängig-Stapel wird in einer QUndoView dargestellt, die eine Liste ist, in der die Befehle als Listenelemente angezeigt werden. Rückgängig und Wiederherstellen sind über das Bearbeitungsmenü verfügbar. Der Benutzer kann auch einen Befehl in der Rückgängig-Ansicht auswählen.
Für die Implementierung des Diagramms verwenden wir das Graphics View Framework. Wir behandeln den zugehörigen Code nur kurz, da das Framework über eigene Beispiele verfügt (z. B. das Diagram Scene Example).
Das Beispiel besteht aus den folgenden Klassen:
MainWindow
ist das Hauptfenster und ordnet die Widgets des Beispiels an. Sie erstellt die Befehle auf der Grundlage von Benutzereingaben und legt sie auf dem Befehlsstapel ab.AddCommand
fügt ein Element zur Szene hinzu.DeleteCommand
löscht ein Element aus der Szene.MoveCommand
Wenn ein Element verschoben wird, speichert der MoveCommand die Start- und Stopp-Positionen der Verschiebung und verschiebt das Element entsprechend, wennredo()
undundo()
aufgerufen werden.DiagramScene
erbt QGraphicsScene und gibt Signale fürMoveComands
aus, wenn ein Element verschoben wird.DiagramItem
erbt von QGraphicsPolygonItem und stellt ein Element im Diagramm dar.
Definition der Klasse 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; };
Die Klasse MainWindow
verwaltet den Rückgängig-Stapel, d. h. sie erstellt QUndoCommandund schiebt und löscht sie vom Stapel, wenn sie das Signal triggered()
von undoAction
und redoAction
erhält.
Implementierung der MainWindow-Klasse
Wir beginnen mit einem Blick auf den Konstruktor:
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(); }
Im Konstruktor richten wir die DiagramScene und QGraphicsView ein. Wir wollen, dass deleteAction
nur aktiviert wird, wenn wir ein Element ausgewählt haben, also verbinden wir das selectionChanged()
Signal der Szene mit dem updateActions()
Slot.
Hier ist die Funktion createUndoView()
:
void MainWindow::createUndoView() { QDockWidget *undoDockWidget = new QDockWidget; undoDockWidget->setWindowTitle(tr("Command List")); undoDockWidget->setWidget(new QUndoView(undoStack)); addDockWidget(Qt::RightDockWidgetArea, undoDockWidget); }
QUndoView ist ein Widget, das den Text, der mit der Funktion setText() festgelegt wurde, für jedes QUndoCommand im Rückgängig-Stapel in einer Liste anzeigt. Wir setzen es in ein Andock-Widget ein.
Hier ist die Funktion 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);
Die Funktion createActions()
richtet alle Beispielaktionen auf die oben beschriebene Weise ein. Mit den Methoden createUndoAction() und createRedoAction() können wir Aktionen erstellen, die je nach Zustand des Stapels deaktiviert und aktiviert werden. Außerdem wird der Text der Aktion automatisch auf der Grundlage von text() der Rückgängig-Befehle aktualisiert. Für die anderen Aktionen haben wir Slots in der Klasse MainWindow
implementiert.
... updateActions(); } void MainWindow::updateActions() { deleteAction->setEnabled(!diagramScene->selectedItems().isEmpty()); }
Sobald alle Aktionen erstellt sind, aktualisieren wir ihren Zustand, indem wir die gleiche Funktion aufrufen, die mit dem selectionChanged
Signal der Szene verbunden ist.
Die Funktionen createMenus()
und createToolBars()
fügen die Aktionen zu Menüs und Symbolleisten hinzu:
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); }
Hier ist der itemMoved()
Slot:
void MainWindow::itemMoved(DiagramItem *movedItem, const QPointF &oldPosition) { undoStack->push(new MoveCommand(movedItem, oldPosition)); }
Wir schieben einfach einen MoveCommand auf den Stack, der dann redo()
aufruft.
Hier ist der deleteItem()
Slot:
void MainWindow::deleteItem() { if (diagramScene->selectedItems().isEmpty()) return; QUndoCommand *deleteCommand = new DeleteCommand(diagramScene); undoStack->push(deleteCommand); }
Ein Element muss ausgewählt sein, um gelöscht zu werden. Wir müssen überprüfen, ob es ausgewählt ist, da deleteAction
auch dann aktiviert werden kann, wenn ein Element nicht ausgewählt ist. Dies kann passieren, da wir kein Signal oder Ereignis abfangen, wenn ein Element ausgewählt ist.
Hier ist der addBox()
Steckplatz:
void MainWindow::addBox() { QUndoCommand *addCommand = new AddCommand(DiagramItem::Box, diagramScene); undoStack->push(addCommand); }
Die Funktion addBox()
erstellt einen AddCommand und schiebt ihn auf den Undo-Stapel.
Hier ist der addTriangle()
Slot:
void MainWindow::addTriangle() { QUndoCommand *addCommand = new AddCommand(DiagramItem::Triangle, diagramScene); undoStack->push(addCommand); }
Die Funktion addTriangle()
erstellt einen AddCommand und schiebt ihn auf den Undo-Stack.
Hier ist die Implementierung von about()
:
void MainWindow::about() { QMessageBox::about(this, tr("About Undo"), tr("The <b>Undo</b> example demonstrates how to " "use Qt's undo framework.")); }
Der about-Slot wird von aboutAction
ausgelöst und zeigt eine about-Box für das Beispiel an.
AddCommand Klassendefinition
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; };
Die Klasse AddCommand
fügt DiagramItem-Grafikelemente zur DiagramScene hinzu.
Implementierung der AddCommand-Klasse
Wir beginnen mit dem Konstruktor:
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))); }
Wir erstellen zunächst das DiagramItem, das der DiagramScene hinzugefügt werden soll. Mit der Funktion setText() können wir eine QString setzen, die den Befehl beschreibt. Wir verwenden dies, um benutzerdefinierte Meldungen im QUndoView und im Menü des Hauptfensters zu erhalten.
void AddCommand::undo() { myGraphicsScene->removeItem(myDiagramItem); myGraphicsScene->update(); }
undo()
entfernt das Element aus der Szene.
void AddCommand::redo() { myGraphicsScene->addItem(myDiagramItem); myDiagramItem->setPos(initialPosition); myGraphicsScene->clearSelection(); myGraphicsScene->update(); }
Wir setzen die Position des Objekts, da wir dies nicht im Konstruktor tun.
DeleteCommand Klassendefinition
class DeleteCommand : public QUndoCommand { public: explicit DeleteCommand(QGraphicsScene *graphicsScene, QUndoCommand *parent = nullptr); void undo() override; void redo() override; private: DiagramItem *myDiagramItem; QGraphicsScene *myGraphicsScene; };
Die Klasse DeleteCommand implementiert die Funktionalität zum Entfernen eines Objekts aus der Szene.
Implementierung der DeleteCommand-Klasse
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()))); }
Wir wissen, dass es ein ausgewähltes Element geben muss, da es nicht möglich ist, einen DeleteCommand zu erstellen, wenn das zu löschende Element nicht ausgewählt ist, und dass immer nur ein Element ausgewählt sein kann. Das Element muss abgewählt sein, wenn es wieder in die Szene eingefügt wird.
void DeleteCommand::undo() { myGraphicsScene->addItem(myDiagramItem); myGraphicsScene->update(); }
Das Element wird einfach wieder in die Szene eingefügt.
void DeleteCommand::redo() { myGraphicsScene->removeItem(myDiagramItem); }
Das Element wird aus der Szene entfernt.
Definition der Klasse 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; };
Die Klasse mergeWith() wurde neu implementiert, um aufeinanderfolgende Verschiebungen eines Objekts zu einem MoveCommand zu machen, d.h. das Objekt wird an die Startposition der ersten Verschiebung zurückgeschoben.
Implementierung der MoveCommand Klasse
Der Konstruktor von MoveCommand sieht wie folgt aus:
MoveCommand::MoveCommand(DiagramItem *diagramItem, const QPointF &oldPos, QUndoCommand *parent) : QUndoCommand(parent), myDiagramItem(diagramItem) , myOldPos(oldPos), newPos(diagramItem->pos()) { }
Wir speichern sowohl die alte als auch die neue Position für Rückgängig und Wiederherstellen.
void MoveCommand::undo() { myDiagramItem->setPos(myOldPos); myDiagramItem->scene()->update(); setText(QObject::tr("Move %1") .arg(createCommandString(myDiagramItem, newPos))); }
Wir setzen einfach die alte Position des Elements und aktualisieren die Szene.
void MoveCommand::redo() { myDiagramItem->setPos(newPos); setText(QObject::tr("Move %1") .arg(createCommandString(myDiagramItem, newPos))); }
Wir setzen das Element auf seine neue 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; }
Immer wenn ein MoveCommand erstellt wird, wird diese Funktion aufgerufen, um zu prüfen, ob er mit dem vorherigen Befehl zusammengeführt werden soll. Es ist das vorherige Befehlsobjekt, das auf dem Stack gehalten wird. Die Funktion gibt true zurück, wenn der Befehl zusammengeführt wird; andernfalls false.
Zunächst wird geprüft, ob es sich um denselben Gegenstand handelt, der zweimal verschoben wurde; in diesem Fall werden die Befehle zusammengeführt. In diesem Fall werden die Befehle zusammengeführt. Wir aktualisieren die Position des Elements, so dass es die letzte Position in der Verschiebungssequenz einnimmt, wenn es zurückgenommen wird.
DiagramScene Klassendefinition
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; };
Die DiagramScene implementiert die Funktionalität zum Verschieben eines Diagrammelements mit der Maus. Sie gibt ein Signal aus, wenn eine Bewegung abgeschlossen ist. Dieses wird von der MainWindow
abgefangen, die MoveCommands ausführt. Wir untersuchen die Implementierung von DiagramScene nicht, da sie sich nur mit Fragen des Grafik-Frameworks beschäftigt.
Die Funktion main()
Die main()
Funktion des Programms sieht wie folgt aus:
int main(int argv, char *args[]) { QApplication app(argv, args); MainWindow mainWindow; mainWindow.show(); return app.exec(); }
Wir zeichnen ein Raster in den Hintergrund der DiagramScene, also verwenden wir eine Ressourcendatei. Der Rest der Funktion erstellt die MainWindow
und zeigt sie als Fenster der obersten Ebene an.
© 2025 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.