En esta página

Ejemplo del Framework de Deshacer

Este ejemplo muestra cómo implementar la funcionalidad deshacer/rehacer con el framework Qt undo.

Ejemplo de diagrama de deshacer

En el framework de deshacer Qt, todas las acciones que realiza el usuario se implementan en clases que heredan de QUndoCommand. Una clase de comando deshacer sabe cómo hacer tanto redo() - o simplemente hacer la primera vez - como undo() una acción. Por cada acción que realiza el usuario, se coloca un comando en una pila QUndoStack. Dado que la pila contiene todos los comandos ejecutados (apilados en orden cronológico) en el documento, puede hacer retroceder y avanzar el estado del documento deshaciendo y rehaciendo sus comandos. Consulte el documento de resumen para una introducción de alto nivel al marco de deshacer.

El ejemplo de deshacer implementa una aplicación de diagrama simple. Es posible añadir y eliminar elementos, que pueden tener forma de caja o rectangular, y mover los elementos arrastrándolos con el ratón. La pila de deshacer se muestra en un QUndoView, que es una lista en la que los comandos se muestran como elementos de lista. Deshacer y rehacer están disponibles a través del menú Edición. El usuario también puede seleccionar un comando desde la vista de deshacer.

Utilizamos el marco de vista gráfica para implementar el diagrama. Sólo tratamos brevemente el código relacionado ya que el framework tiene ejemplos propios (por ejemplo, el Ejemplo de Escena de Diagrama).

El ejemplo consta de las siguientes clases

  • MainWindow es la ventana principal y organiza los widgets del ejemplo. Crea los comandos basándose en la entrada del usuario y los mantiene en la pila de comandos.
  • AddCommand añade un elemento a la escena.
  • DeleteCommand elimina un elemento de la escena.
  • MoveCommand cuando se mueve un elemento, MoveCommand mantiene un registro de las posiciones de inicio y fin del movimiento, y mueve el elemento de acuerdo con ellas cuando se llama a redo() y undo().
  • DiagramScene hereda QGraphicsScene y emite señales para MoveComands cuando se mueve un elemento.
  • DiagramItem hereda QGraphicsPolygonItem y representa un elemento en el diagrama.

Definición de la Clase 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 clase MainWindow mantiene la pila de deshacer, es decir, crea QUndoCommands y los empuja y saca de la pila cuando recibe la señal triggered() de undoAction y redoAction.

Implementación de la clase MainWindow

Comenzaremos con un vistazo al constructor:

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();
}

En el constructor, configuramos el DiagramScene y QGraphicsView. Sólo queremos que deleteAction se active cuando hayamos seleccionado un ítem, así que conectamos la señal selectionChanged() de la escena al slot updateActions().

Aquí está la función createUndoView():

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

El QUndoView es un widget que muestra el texto, que se establece con la función setText(), para cada QUndoCommand en la pila de deshacer en una lista. Lo ponemos en un widget de acoplamiento.

Aquí está la función 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 función createActions() configura todas las acciones de los ejemplos de la manera mostrada anteriormente. Los métodos createUndoAction() y createRedoAction() nos ayudan a crear acciones que se desactivan y activan en función del estado de la pila. Además, el texto de la acción se actualizará automáticamente basándose en el text() de los comandos de deshacer. Para las otras acciones hemos implementado ranuras en la clase MainWindow.

    ...
    updateActions();
}

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

Una vez creadas todas las acciones actualizamos su estado llamando a la misma función que está conectada a la señal selectionChanged de la escena.

Las funciones createMenus() y createToolBars() añaden las acciones a los menús y barras de herramientas:

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);
}

Esta es la función itemMoved():

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

Simplemente empujamos un MoveCommand en la pila, que llama a redo() sobre él.

Este es el espacio deleteItem():

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

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

Un elemento debe estar seleccionado para ser eliminado. Tenemos que comprobar si está seleccionado como el deleteAction puede estar habilitado, incluso si un elemento no está seleccionado. Esto puede ocurrir porque no captamos una señal o evento cuando se selecciona un elemento.

Aquí está la ranura addBox():

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

La función addBox() crea un AddCommand y lo coloca en la pila de deshacer.

Aquí está la ranura addTriangle():

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

La función addTriangle() crea un AddCommand y lo coloca en la pila de deshacer.

Esta es la implementación 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."));
}

La ranura about es activada por aboutAction y muestra una caja about para el ejemplo.

Definición de la Clase 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 clase AddCommand agrega ítems gráficos DiagramItem a la DiagramScene.

Implementación de la Clase AddCommand

Comenzamos con el constructor:

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)));
}

Primero creamos el DiagramItem a agregar al DiagramScene. La función setText() nos permite establecer un QString que describe el comando. Usamos esto para obtener mensajes personalizados en el QUndoView y en el menú de la ventana principal.

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

undo() elimina el elemento de la escena.

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

Establecemos la posición del item ya que no lo hacemos en el constructor.

Definición de la Clase 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 clase DeleteCommand implementa la funcionalidad para eliminar un ítem de la escena.

Implementación de la Clase 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())));
}

Sabemos que debe haber un ítem seleccionado ya que no es posible crear un DeleteCommand a menos que el ítem a ser eliminado esté seleccionado y que sólo un ítem puede estar seleccionado en cualquier momento. El elemento debe estar deseleccionado si se vuelve a insertar en la escena.

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

El elemento simplemente se vuelve a insertar en la escena.

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

El elemento se elimina de la escena.

Definición de la Clase 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;
};

El mergeWith() es reimplementado para hacer de los movimientos consecutivos de un ítem un MoveCommand, es decir, el ítem será movido de vuelta a la posición inicial del primer movimiento.

Implementación de la clase MoveCommand

El constructor de MoveCommand tiene este aspecto:

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

Guardamos las posiciones antigua y nueva para deshacer y rehacer respectivamente.

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

Simplemente establecemos la posición antigua del ítem y actualizamos la escena.

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

Colocamos el item en su nueva posición.

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;
}

Cada vez que se crea un MoveCommand, se llama a esta función para comprobar si debe fusionarse con el comando anterior. Es el objeto comando anterior el que se mantiene en la pila. La función devuelve true si el comando está fusionado; en caso contrario, false.

Primero comprobamos si es el mismo elemento el que se ha movido dos veces, en cuyo caso fusionamos los comandos. Actualizamos la posición del ítem para que tome la última posición en la secuencia de movimiento cuando se deshaga.

Definición de la Clase 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 clase DiagramScene implementa la funcionalidad para mover un DiagramItem con el ratón. Emite una señal cuando se completa un movimiento. Esto es captado por el MainWindow, que realiza MoveCommands. No examinamos la implementación de DiagramScene ya que sólo se ocupa de cuestiones del marco gráfico.

La función main()

La función main() del programa tiene este aspecto:

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

    MainWindow mainWindow;
    mainWindow.show();

    return app.exec();
}

Dibujamos una rejilla en el fondo del DiagramScene, por lo que utilizamos un archivo de recursos. El resto de la función crea el MainWindow y lo muestra como una ventana de nivel superior.

Proyecto de ejemplo @ 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.