Qt源碼中的設(shè)計(jì)模式:撤銷/重做框架與命令模式

命令模式

命令模式是一種行為設(shè)計(jì)模式,它將請(qǐng)求封裝成一個(gè)對(duì)象,從而使我們可以將不同的請(qǐng)求、隊(duì)列或日志請(qǐng)求等參數(shù)化,同時(shí)支持可撤銷的操作。該模式的核心思想是將請(qǐng)求發(fā)送者和接收者解耦,讓它們不直接交互,而是通過命令對(duì)象進(jìn)行交互,即將請(qǐng)求封裝成類對(duì)象。命令模式通常用于以下場(chǎng)景:

  1. 需要將請(qǐng)求發(fā)送者和接收者解耦的場(chǎng)景,以便于適應(yīng)變化。

  2. 需要支持撤銷和恢復(fù)操作的場(chǎng)景。

  3. 需要將一組操作組合在一起執(zhí)行的場(chǎng)景,也稱為批處理。

命令模式包含以下角色:

  1. 命令(Command):定義命令的接口,通常包含執(zhí)行和撤銷兩個(gè)方法。

  2. 具體命令(ConcreteCommand):實(shí)現(xiàn)命令接口,包含了對(duì)應(yīng)的操作。

  3. 命令接收者(Receiver):執(zhí)行命令的對(duì)象。

  4. 命令發(fā)起者(Invoker):調(diào)用命令的對(duì)象,負(fù)責(zé)將命令發(fā)送給命令接收者。

  5. 客戶端(Client):創(chuàng)建命令對(duì)象并將其發(fā)送給命令發(fā)起者。

命令模式UML類圖

下面給出一個(gè)命令模式的C++示例:

class Command {
public:
    virtual ~Command() {}
    virtual void execute() = 0;
    virtual void undo() = 0;
};

class ConcreteCommand : public Command {
public:
    ConcreteCommand(std::shared_ptr<Receiver> receiver) : m_receiver(receiver) {}
    virtual void execute() {
        m_receiver->action();
    }
    virtual void undo() {
        m_receiver->undoAction();
    }
private:
    std::shared_ptr<Receiver> m_receiver;
};

class Receiver {
public:
    void action() {
        // 執(zhí)行操作
    }
    void undoAction() {
        // 撤銷操作
    }
};

class Invoker {
public:
    void setCommand(std::shared_ptr<Command> command) {
        m_command = command;
    }
    void executeCommand() {
        m_command->execute();
    }
    void undoCommand() {
        m_command->undo();
    }
private:
    std::shared_ptr<Command> m_command;
};

int main() {
    auto receiver = std::make_shared<Receiver>();
    auto command = std::make_shared<ConcreteCommand>(receiver);
    auto invoker = std::make_shared<Invoker>();
    invoker->setCommand(command);
    invoker->executeCommand();
    invoker->undoCommand();
    return 0;
}

在上面的示例中,Command類是命令接口,定義了execute和undo方法。ConcreteCommand類是具體命令類,實(shí)現(xiàn)了Command接口,包含了對(duì)應(yīng)的操作。Receiver類是命令接收者,執(zhí)行命令的對(duì)象。Invoker類是命令發(fā)起者,調(diào)用命令的對(duì)象,負(fù)責(zé)將命令發(fā)送給命令接收者。在客戶端代碼中,我們創(chuàng)建了一個(gè)Receiver對(duì)象和一個(gè)ConcreteCommand對(duì)象,并將其傳遞給Invoker對(duì)象。然后,我們調(diào)用Invoker對(duì)象的executeCommand方法來執(zhí)行ConcreteCommand對(duì)象的操作。如果需要撤銷操作,我們可以調(diào)用Invoker對(duì)象的undoCommand方法來執(zhí)行ConcreteCommand對(duì)象的undo操作。

撤銷/重做框架(QUndoStack、QUndoCommand等類)

Qt的撤銷/重做框架(QUndoStack、QUndoCommand等類)是命令模式的一種實(shí)現(xiàn)。在Qt的撤銷/重做框架中,每個(gè)操作都被封裝為一個(gè)QUndoCommand的子類對(duì)象。

以下是一個(gè)使用Qt的撤銷/重做框架的程序示例:

#include <QApplication>
#include <QTextEdit>
#include <QUndoStack>
#include <QPushButton>
#include <QVBoxLayout>
#include <QUndoCommand>

class MyCommand : public QUndoCommand
{
public:
    MyCommand(QTextEdit *editor, const QString &text, QUndoCommand *parent = nullptr)
        : QUndoCommand(parent), m_editor(editor), m_text(text), m_oldText(editor->toPlainText()) {}

    void undo() override
    {
        m_editor->setPlainText(m_oldText);
    }

    void redo() override
    {
        m_editor->setPlainText(m_text);
    }

private:
    QTextEdit *m_editor;
    QString m_text;
    QString m_oldText;
};

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

    QUndoStack stack;

    QTextEdit editor;
    QPushButton undoButton("Undo");
    QPushButton redoButton("Redo");

    QObject::connect(&undoButton, &QPushButton::clicked, &stack, &QUndoStack::undo);
    QObject::connect(&redoButton, &QPushButton::clicked, &stack, &QUndoStack::redo);

    QObject::connect(&editor, &QTextEdit::textChanged, [&]() {
        stack.push(new MyCommand(&editor, editor.toPlainText()));
    });

    QVBoxLayout layout;
    layout.addWidget(&editor);
    layout.addWidget(&undoButton);
    layout.addWidget(&redoButton);

    QWidget window;
    window.setLayout(&layout);
    window.show();

    return app.exec();
}

在上述示例中,MyCommandQUndoCommand 的子類。每次 QTextEdit 的文本改變時(shí),就會(huì)創(chuàng)建一個(gè)新的 MyCommand 對(duì)象并將其壓入 QUndoStack。當(dāng)點(diǎn)擊 "Undo" 按鈕時(shí),就會(huì)撤銷棧頂?shù)拿?;?dāng)點(diǎn)擊 "Redo" 按鈕時(shí),就會(huì)重做棧頂?shù)拿睢?/p>

在這個(gè)例子中,相關(guān)的類扮演的角色如下:

  1. 命令(Command)QUndoCommand是命令接口。它定義了執(zhí)行(redo)和撤銷(undo)的方法。

  2. 具體命令(ConcreteCommand)MyCommand是具體的命令。它是 QUndoCommand的子類,實(shí)現(xiàn)了 redoundo方法。

  3. 命令接收者(Receiver)QTextEdit是命令的接收者。它是執(zhí)行命令的對(duì)象,MyCommandredoundo方法都是操作 QTextEdit。

  4. 命令發(fā)起者(Invoker)QUndoStack是命令的發(fā)起者。它負(fù)責(zé)調(diào)用和存儲(chǔ)命令。QPushButton 實(shí)際上也扮演了命令發(fā)起者的角色,因?yàn)樗鼈兪怯|發(fā)執(zhí)行或撤銷命令的實(shí)際用戶界面元素。

  5. 客戶端(Client):在這個(gè)例子中,main 函數(shù)就是客戶端。它創(chuàng)建了應(yīng)用程序,包括命令接收者(QTextEdit)、命令發(fā)起者(QUndoStackQPushButton)、并在 QTextEdit的文本變化時(shí)創(chuàng)建具體命令(MyCommand)。

下面給出相關(guān)的類在Qt源碼中的實(shí)現(xiàn)。同樣,這里隱去了很多與命令模式無關(guān)的細(xì)節(jié),但對(duì)理解命令模式應(yīng)該是足夠的。

class QUndoCommand
{
public:
    virtual ~QUndoCommand() {}
    virtual void undo() = 0;
    virtual void redo() = 0;
};

class QUndoStack
{
public:
    void push(QUndoCommand *cmd)
    {
        m_stack.push(cmd);
        cmd->redo();
    }

    void undo()
    {
        if (!m_stack.isEmpty()) {
            QUndoCommand *cmd = m_stack.pop();
            cmd->undo();
            m_undoStack.push(cmd);
        }
    }

    void redo()
    {
        if (!m_undoStack.isEmpty()) {
            QUndoCommand *cmd = m_undoStack.pop();
            cmd->redo();
            m_stack.push(cmd);
        }
    }

private:
    QStack<QUndoCommand*> m_stack;
    QStack<QUndoCommand*> m_undoStack;
};

MyCommand類我們已經(jīng)在前面實(shí)現(xiàn)了,這里不再重復(fù)。對(duì)于命令的接收者QTextEdit,我們也不需要關(guān)注它的源碼,因?yàn)樗墓δ芫褪且粋€(gè)文本編輯器,我們只需要知道它提供了setPlainText()toPlainText()等函數(shù)供我們?cè)?code>MyCommand中使用就可以了。

需要注意的是,在標(biāo)準(zhǔn)的命令模式中,通常只有一個(gè)存儲(chǔ)命令對(duì)象的容器,可以是是隊(duì)列或棧,也可以僅僅是只是單個(gè)命令對(duì)象(如我們一開始給出的命令模式的示例)。然而,在實(shí)現(xiàn)撤銷/重做功能時(shí),通常需要兩個(gè)棧結(jié)構(gòu)。

在Qt的QUndoStack中,主棧用于存儲(chǔ)執(zhí)行過的命令,當(dāng)調(diào)用undo()方法時(shí),會(huì)從主棧中彈出命令并執(zhí)行其undo操作,同時(shí)該命令會(huì)被壓入撤銷棧。撤銷棧用于存儲(chǔ)撤銷過的命令,當(dāng)調(diào)用redo()方法時(shí),會(huì)從撤銷棧中彈出命令并執(zhí)行其redo操作,同時(shí)該命令會(huì)被壓回主棧。這樣的設(shè)計(jì)使得QUndoStack能夠按正確的順序執(zhí)行和撤銷命令,同時(shí)還能在撤銷命令后重新執(zhí)行它們。

總結(jié)

Qt的撤銷/重做框架,實(shí)現(xiàn)了命令模式,并使用了兩個(gè)棧的方式維護(hù)了操作的歷史記錄,確實(shí)是很精妙的設(shè)計(jì)。除此之外,Qt的撤銷/重做框架是支持多個(gè)命令的合并的,這在文字編輯或者其他需要撤銷/重做框架的需求中,都是很有用的。QUndoCommand類提供了一個(gè)可重寫的mergeWith方法,可以用來合并連續(xù)的、類似的操作,使其在撤銷/重做時(shí)被視為一個(gè)單一的操作。這里由于篇幅問題,不展開討論??偟膩碚f,Qt的撤銷/重做框架,很好地實(shí)現(xiàn)了命令模式。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容