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è)單一的操作捷雕。這里由于篇幅問題椒丧,不展開討論【认铮總的來說壶熏,Qt的撤銷/重做框架,很好地實(shí)現(xiàn)了命令模式浦译。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末棒假,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子精盅,更是在濱河造成了極大的恐慌帽哑,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,270評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件叹俏,死亡現(xiàn)場(chǎng)離奇詭異妻枕,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,489評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門佳头,熙熙樓的掌柜王于貴愁眉苦臉地迎上來鹰贵,“玉大人,你說我怎么就攤上這事康嘉〉锸洌” “怎么了?”我有些...
    開封第一講書人閱讀 165,630評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵亭珍,是天一觀的道長(zhǎng)敷钾。 經(jīng)常有香客問我,道長(zhǎng)肄梨,這世上最難降的妖魔是什么阻荒? 我笑而不...
    開封第一講書人閱讀 58,906評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮众羡,結(jié)果婚禮上侨赡,老公的妹妹穿的比我還像新娘。我一直安慰自己粱侣,他們只是感情好羊壹,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,928評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著齐婴,像睡著了一般油猫。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上柠偶,一...
    開封第一講書人閱讀 51,718評(píng)論 1 305
  • 那天情妖,我揣著相機(jī)與錄音,去河邊找鬼诱担。 笑死毡证,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的蔫仙。 我是一名探鬼主播料睛,決...
    沈念sama閱讀 40,442評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼匀哄!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起雏蛮,我...
    開封第一講書人閱讀 39,345評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤涎嚼,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后挑秉,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體法梯,經(jīng)...
    沈念sama閱讀 45,802評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,984評(píng)論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了立哑。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片夜惭。...
    茶點(diǎn)故事閱讀 40,117評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖铛绰,靈堂內(nèi)的尸體忽然破棺而出诈茧,到底是詐尸還是另有隱情,我是刑警寧澤捂掰,帶...
    沈念sama閱讀 35,810評(píng)論 5 346
  • 正文 年R本政府宣布敢会,位于F島的核電站,受9級(jí)特大地震影響这嚣,放射性物質(zhì)發(fā)生泄漏鸥昏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,462評(píng)論 3 331
  • 文/蒙蒙 一姐帚、第九天 我趴在偏房一處隱蔽的房頂上張望吏垮。 院中可真熱鬧,春花似錦罐旗、人聲如沸膳汪。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,011評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)旅敷。三九已至,卻和暖如春颤霎,著一層夾襖步出監(jiān)牢的瞬間媳谁,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,139評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工友酱, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留晴音,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,377評(píng)論 3 373
  • 正文 我出身青樓缔杉,卻偏偏與公主長(zhǎng)得像锤躁,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子或详,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,060評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容