Undo/Redo履歴をスッキリ保ち、パフォーマンスを向上させる:Qt GUIにおけるQUndoCommand::mergeWith()の魔法


QUndoCommand::mergeWith() は、Qt GUIにおけるUndo/Redo機能をサポートするクラス QUndoCommand における仮想関数です。この関数は、2つの QUndoCommand オブジェクトをマージし、単一の QUndoCommand オブジェクトとして扱いやすくする役割を果たします。マージされたコマンドは、1つの操作としてUndo/Redo操作の対象となります。

機能

QUndoCommand::mergeWith() は、以下の条件を満たす場合に2つのコマンドをマージします。

  • 操作の方向が逆であること
  • 同じデータオブジェクトを操作していること
  • 同じ種類の操作を表すコマンドであること

具体的には、以下の操作がマージ対象となります。

  • プロパティ設定とプロパティ解除
  • オブジェクト追加とオブジェクト削除
  • テキスト挿入とテキスト削除

メリット

QUndoCommand::mergeWith() を使用することで、以下のメリットが得られます。

  • パフォーマンスを向上させることができる
  • スタック上のコマンド数を削減できる
  • Undo/Redo履歴を簡潔に保つことができる

以下の例は、テキスト挿入コマンドとテキスト削除コマンドをマージする例です。

class TextCommand : public QUndoCommand
{
public:
    TextCommand(QTextEdit *textEdit, const QString &text)
        : textEdit_(textEdit), text_(text)
    {
        setText(tr("Insert Text"));
    }

    virtual void undo() override
    {
        textEdit_->textCursor().insertText(text_);
    }

    virtual void redo() override
    {
        textEdit_->textCursor().insertText(text_);
    }

    virtual bool mergeWith(const QUndoCommand *command) override
    {
        if (command->isInstanceOf<TextCommand>())
        {
            const TextCommand *textCommand = static_cast<const TextCommand *>(command);
            if (textEdit_ == textCommand->textEdit_ && text_ == textCommand->text_)
            {
                text_ += textCommand->text_;
                return true;
            }
        }
        return false;
    }

private:
    QTextEdit *textEdit_;
    QString text_;
};

上記の例では、TextCommand クラスは、テキスト挿入とテキスト削除操作を表すコマンドとして定義されています。mergeWith() メンバ関数は、引数として渡されたコマンドが同じ種類の操作を表すコマンドであるかどうかを確認し、条件を満たす場合は2つのコマンドをマージします。



class TextCommand : public QUndoCommand
{
public:
    TextCommand(QTextEdit *textEdit, const QString &text)
        : textEdit_(textEdit), text_(text)
    {
        setText(tr("Insert Text"));
    }

    virtual void undo() override
    {
        textEdit_->textCursor().insertText(text_);
    }

    virtual void redo() override
    {
        textEdit_->textCursor().insertText(text_);
    }

    virtual bool mergeWith(const QUndoCommand *command) override
    {
        if (command->isInstanceOf<TextCommand>())
        {
            const TextCommand *textCommand = static_cast<const TextCommand *>(command);
            if (textEdit_ == textCommand->textEdit_ && text_ == textCommand->text_)
            {
                text_ += textCommand->text_;
                return true;
            }
        }
        return false;
    }

private:
    QTextEdit *textEdit_;
    QString text_;
};

int main()
{
    QApplication app(argc, argv);

    QTextEdit textEdit;
    QUndoStack undoStack(&textEdit);

    // テキスト挿入コマンドを作成
    TextCommand *insertCommand1 = new TextCommand(&textEdit, "Hello");
    undoStack.push(insertCommand1);
    insertCommand1->redo(); // "Hello" を挿入

    // テキスト挿入コマンドを作成
    TextCommand *insertCommand2 = new TextCommand(&textEdit, ", ");
    undoStack.push(insertCommand2);
    insertCommand2->redo(); // ", " を挿入

    // テキスト削除コマンドを作成
    TextCommand *deleteCommand = new TextCommand(&textEdit, QString("Hello, "));
    undoStack.push(deleteCommand);

    // 2つのコマンドをマージ
    deleteCommand->mergeWith(insertCommand2);
    deleteCommand->mergeWith(insertCommand1);

    // テキスト削除コマンドを実行
    deleteCommand->redo(); // "Hello, " を削除

    return app.exec();
}

このコードは、TextCommand クラスを使用して、テキスト挿入とテキスト削除コマンドを作成し、mergeWith() 関数を使用して2つのコマンドをマージする例です。

オブジェクト追加とオブジェクト削除コマンドのマージ

class ObjectCommand : public QUndoCommand
{
public:
    ObjectCommand(QGraphicsScene *scene, QGraphicsItem *item)
        : scene_(scene), item_(item)
    {
        setText(tr("Add Object"));
    }

    virtual void undo() override
    {
        scene_->removeItem(item_);
    }

    virtual void redo() override
    {
        scene_->addItem(item_);
    }

    virtual bool mergeWith(const QUndoCommand *command) override
    {
        if (command->isInstanceOf<ObjectCommand>())
        {
            const ObjectCommand *objectCommand = static_cast<const ObjectCommand *>(command);
            if (scene_ == objectCommand->scene_ && item_ == objectCommand->item_)
            {
                return true;
            }
        }
        return false;
    }

private:
    QGraphicsScene *scene_;
    QGraphicsItem *item_;
};

int main()
{
    QApplication app(argc, argv);

    QGraphicsScene scene;
    QUndoStack undoStack(&scene);

    // オブジェクト追加コマンドを作成
    ObjectCommand *addCommand = new ObjectCommand(&scene, new QGraphicsRectItem(QRect(0, 0, 100, 100)));
    undoStack.push(addCommand);
    addCommand->redo(); // オブジェクトを追加

    // オブジェクト削除コマンドを作成
    ObjectCommand *deleteCommand = new ObjectCommand(&scene, addCommand->item());
    undoStack.push(deleteCommand);

    // 2つのコマンドをマージ
    deleteCommand->mergeWith(addCommand);

    // オブジェクト削除コマンドを実行
    deleteCommand->redo(); // オブジェクトを削除

    return app.exec();
}

このコードは、ObjectCommand クラスを使用して、オブジェクト追加とオブジェクト削除コマンドを作成し、mergeWith() 関数を使用して2つのコマンドをマージする例です。

class PropertyCommand : public QUndoCommand
{
public:
    Property


個別のコマンドを積み重ねる

最も単純な方法は、個別にコマンドを積み重ねる方法です。この方法は、常にすべての操作履歴を保持したい場合に適しています。

QUndoStack undoStack;

// テキスト挿入コマンドを作成
TextCommand *insertCommand1 = new TextCommand(&textEdit, "Hello");
undoStack.push(insertCommand1);
insertCommand1->redo(); // "Hello" を挿入

// テキスト挿入コマンドを作成
TextCommand *insertCommand2 = new TextCommand(&textEdit, ", ");
undoStack.push(insertCommand2);
insertCommand2->redo(); // ", " を挿入

// テキスト削除コマンドを作成
TextCommand *deleteCommand = new TextCommand(&textEdit, QString("Hello, "));
undoStack.push(deleteCommand);
deleteCommand->redo(); // "Hello, " を削除

この方法では、mergeWith() 関数は使用せず、個別にコマンドを積み重ねることで、Undo/Redo履歴を保持することができます。

カスタムコマンドを作成する

より複雑な操作をUndo/Redoしたい場合は、カスタムコマンドを作成する方法があります。カスタムコマンドでは、QUndoCommand クラスのサブクラスを作成し、undo()redo() メンバ関数をオーバーライドして、独自の操作を実装することができます。

class MergeTextCommand : public QUndoCommand
{
public:
    MergeTextCommand(QTextEdit *textEdit, const QString &text)
        : textEdit_(textEdit), text_(text)
    {
        setText(tr("Merge Text"));
    }

    virtual void undo() override
    {
        // 独自のUndo処理を実装
    }

    virtual void redo() override
    {
        // 独自のRedo処理を実装
    }

private:
    QTextEdit *textEdit_;
    QString text_;
};

int main()
{
    QApplication app(argc, argv);

    QTextEdit textEdit;
    QUndoStack undoStack(&textEdit);

    // テキスト挿入コマンドを作成
    TextCommand *insertCommand1 = new TextCommand(&textEdit, "Hello");
    undoStack.push(insertCommand1);
    insertCommand1->redo(); // "Hello" を挿入

    // テキスト挿入コマンドを作成
    TextCommand *insertCommand2 = new TextCommand(&textEdit, ", ");
    undoStack.push(insertCommand2);
    insertCommand2->redo(); // ", " を挿入

    // カスタムコマンドを作成
    MergeTextCommand *mergeCommand = new MergeTextCommand(&textEdit, QString("Hello, "));
    undoStack.push(mergeCommand);

    // カスタムコマンドを実行
    mergeCommand->redo(); // "Hello, " を削除

    return app.exec();
}

この方法では、mergeWith() 関数は使用せず、カスタムコマンドを作成することで、独自のUndo/Redo処理を実装することができます。

スタックサイズを制限する

Undo/Redo履歴が長くなりすぎるとパフォーマンスが低下する可能性があります。そのような場合は、スタックサイズを制限する方法があります。

QUndoStack undoStack(100); // スタックサイズを100に制限