このページでは

Undoフレームワークの例

この例では、QtのUndoフレームワークを使用して、元に戻す/やり直し機能を実装する方法を示します。

「元に戻す」ダイアグラムの例

Qt のアンドゥフレームワークでは、ユーザーが実行するすべてのアクションは、QUndoCommand を継承するクラスで実装されます。アンドゥコマンドクラスは、アクションをredo()(初回のみ実行)およびundo()する方法の両方を認識しています。 ユーザーが実行する各操作について、コマンドがQUndoStack に格納されます。このスタックには、ドキュメント上で実行されたすべてのコマンド(時系列順に積み重ねられたもの)が含まれているため、コマンドの取り消しややり直しを行うことで、ドキュメントの状態を前後に移動させることができます。undoフレームワークの概要については、概要ドキュメントを参照してください。

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

ダイアグラムの実装には、グラフィックス・ビュー・フレームワークを使用しています。このフレームワークには独自の例(例:Diagram Scene Example)が用意されているため、関連するコードについては簡単に触れるにとどめます。

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

  • 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 は、アンドゥスタック内の各QUndoCommand について、setText()関数で設定されたテキストをリスト形式で表示するウィジェットです。これをドッキングウィジェットに配置します。

以下に、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を作成し、それをアンドゥスタックにプッシュします。

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 クラスは、DiagramSceneにDiagramItemグラフィックアイテムを追加します。

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 つだけであることから、選択されているアイテムが 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;
};

mergeWith() メソッドを再実装し、アイテムの連続した移動を 1 つの MoveCommand として扱うようにしました。つまり、アイテムは最初の移動の開始位置まで戻されることになります。

MoveCommand クラスの実装

MoveCommand のコンストラクタは次のようになっています:

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

元に戻す(undo)とやり直し(redo)のために、それぞれ以前の位置と新しい位置を保存します。

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を移動させる機能を実装しています。移動が完了するとシグナルを発行します。このシグナルはMainWindow によって捕捉され、MoveCommandが生成されます。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

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