アンドゥ・フレームワークの例

この例では、Qt undo フレームワークを使って、undo/redo 機能を実装する方法を示します。

アンドゥ・ダイアグラムの例

Qt undo フレームワークでは、ユーザーが実行するすべてのアクションはQUndoCommand を継承したクラスで実装されます。アンドゥコマンドクラスは、redo ()とundo ()の両方のアクションを知っています。ユーザーが実行するアクションごとに、コマンドはQUndoStack に置かれます。スタックには、ドキュメント上で実行された(時系列順にスタックされた)すべてのコマンドが格納されているので、そのコマンドを元に戻したりやり直したりすることで、ドキュメントの状態を前後にロールバックすることができます。アンドゥ・フレームワークの高レベルな紹介については、概要文書を参照してください。

アンドゥの例では、シンプルなダイアグラム・アプリケーションを実装しています。アイテムの追加と削除が可能で、ボックスまたは長方形の形をしており、マウスでドラッグすることでアイテムを移動させることができます。元に戻すスタックはQUndoView で表示されます。これは、コマンドがリスト項目として表示されるリストです。元に戻すとやり直しは編集メニューから利用できる。ユーザーはアンドゥビューからコマンドを選択することもできます。

ダイアグラムを実装するために、グラフィックス・ビュー・フレームワークを使用します。フレームワークにはそれ自身の例(例えば、ダイアグラム・シーンの例)があるので、関連するコードについては簡単にしか扱いません。

この例は以下のクラスで構成されています:

  • MainWindow はメインウィンドウで、例のウィジェットを配置します。ユーザー入力に基づいてコマンドを作成し、コマンドスタックに保持します。
  • AddCommand シーンにアイテムを追加します。
  • DeleteCommand シーンからアイテムを削除します。
  • MoveCommand アイテムが移動されると、MoveCommand は移動の開始位置と停止位置を記録し、 と が呼び出されると、それに従ってアイテムを移動します。redo() undo()
  • DiagramScene QGraphicsScene を継承し、アイテムが移動されると のシグナルを発する。MoveComands
  • DiagramItem QGraphicsPolygonItem を継承し、ダイアグラム内のアイテムを表します。

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

MainWindow すなわち、QUndoCommandを作成し、undoAction およびredoAction からtriggered() シグナルを受け取ると、それらをスタックからプッシュおよびポップします。

MainWindowクラスの実装

まずコンストラクタから見ていきましょう:

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

コンストラクタでは、DiagramSceneとQGraphicsView を設定します。アイテムを選択したときだけdeleteAction を有効にしたいので、シーンのselectionChanged() シグナルをupdateActions() スロットに接続します。

createUndoView()

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

QUndoView は、setText() 関数で設定されたテキストを、元に戻すスタックのQUndoCommand ごとにリスト表示するウィジェットです。これをドッキング・ウィジェットに入れました。

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

createActions() 関数は、上に示した方法ですべてのアクション例を設定します。createUndoAction() とcreateRedoAction() メソッドは、スタックの状態に基づいて無効になったり有効になったりするアクションを作成するのに役立ちます。また、元に戻すコマンドのtext ()に基づいて、アクションのテキストが自動的に更新されます。その他のアクションについては、MainWindow クラスにスロットを実装した。

    ...
    updateActions();
}

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

すべてのアクションが作成されたら、シーンのselectionChanged シグナルに接続されている同じ関数を呼び出して、状態を更新します。

createMenus()createToolBars() 関数は、メニューとツールバーにアクションを追加します:

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

itemMoved()

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

単にMoveCommandをスタックにプッシュし、redo()

deleteItem()

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

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

アイテムを削除するには、アイテムが選択されていなければなりません。アイテムが選択されていなくても、deleteAction が有効になっている可能性があるので、選択されているかどうかをチェックする必要がある。これは、アイテムが選択されたときにシグナルやイベントをキャッチしていないために起こる可能性があります。

これがaddBox() スロットです:

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

addBox() 関数はAddCommandを作成し、undoスタックにプッシュします。

addTriangle()

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

addTriangle() 関数は AddCommand を作成し、アンドゥスタックにプッシュします。

以下はabout() の実装である:

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

aboutスロットはaboutAction によってトリガーされ、例のaboutボックスを表示します。

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

AddCommand クラスは、DiagramItem グラフィックス・アイテムを DiagramScene に追加します。

AddCommand クラスの実装

コンストラクターから始めます:

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

まず、DiagramSceneに追加するDiagramItemを作成します。setText() 関数で、コマンドを記述するQString を設定します。これを使用して、QUndoView とメイン・ウィンドウのメニューにカスタム・メッセージを表示します。

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

undo() は、シーンからアイテムを削除します。

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

アイテムの位置はコンストラクタでは設定しないので、ここで設定する。

DeleteCommand クラスの定義

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

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

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

DeleteCommandクラスは、シーンからアイテムを削除する機能を実装しています。

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

削除するアイテムが選択されていなければ、DeleteCommand を作成することができないので、選択されているアイテムは1つでなければなりません。アイテムがシーンに挿入される場合、アイテムは非選択でなければならない。

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

アイテムはシーンに再挿入されるだけです。

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

アイテムはシーンから削除される。

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

アイテムの連続移動を1つのMoveCommandにするために、mergeWith ()を再実装します。

MoveCommandクラスの実装

MoveCommandのコンストラクターは以下のようになっている:

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

アンドゥとリドゥのために古い位置と新しい位置をそれぞれ保存する。

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

アイテムの古い位置をセットし、シーンを更新します。

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

アイテムを新しい位置にセットする。

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

MoveCommandが作成されるたびに、この関数が呼び出され、前のコマンドとマージすべきかどうかチェックされる。スタックに保持されるのは前のコマンドオブジェクトです。この関数は、コマンドがマージされればtrueを返し、そうでなければfalseを返す。

まず、同じアイテムが2回移動されたかどうかをチェックし、その場合はコマンドをマージする。アイテムの位置を更新し、元に戻したときに移動シーケンスで最後の位置になるようにします。

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

DiagramSceneは、マウスを使ってDiagramItemを移動する機能を実装しています。移動が完了すると、シグナルを発します。これは、MoveCommands を作成するMainWindow によってキャッチされます。DiagramSceneの実装については、グラフィックス・フレームワークの問題のみを扱うため、ここでは検討しません。

main() 関数

プログラムのmain() 関数は次のようになっています:

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

    MainWindow mainWindow;
    mainWindow.show();

    return app.exec();
}

DiagramSceneの背景にグリッドを描画するので、リソースファイルを使用します。残りの関数は、MainWindow を作成し、トップレベルのウィンドウとして表示します。

プロジェクト例 @ code.qt.io

© 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.