先看下main.cpp以及相应的输出:

#include <QDebug>

#include "command.h"

int main()

{

QString doc;

QUndoCommand *command1 = new AppendTextCommand(&doc,"This is a test");

QUndoCommand *command2 = new AppendTextCommand(&doc,"Hello World!");

QUndoCommand *command3 = new ToUpperCommand(&doc);

QUndoStack stack;

stack.push(command1);

qDebug() << doc;

stack.push(command2);

qDebug() << doc;

stack.push(command3);

qDebug() << doc;

stack.undo();

qDebug() << doc;

stack.undo();

qDebug() << doc;

stack.undo();

qDebug() << doc;

stack.redo();

qDebug() << doc;

stack.redo();

qDebug() << doc;

stack.redo();

qDebug() << doc;

return 0;

}


output:

$ ./qcommand 

"This is a test"

"This is a testHello World!"

"THIS IS A TESTHELLO WORLD!"

"This is a testHello World!"

"This is a test"

""

"This is a test"

"This is a testHello World!"

"THIS IS A TESTHELLO WORLD!"

AppendTextCommand::~AppendTextCommand

AppendTextCommand::~AppendTextCommand

ToUpperCommand::~ToUpperCommand

关于command pattern之前介绍过一点(当时还称之为命令行模式,被人指出来了。。。)。

稍微回顾下该模式标准使用下的参与者:

1. Comand

声明执行操作的接口,一般作为具体命令的父类。声明execute接口,支持取消的话还有unExecute()。

2. ConcreteCommand

将一个接收者对象绑定于一个动作。

调用接收者相应的操作。

3. Client

创建一个具体命令对象并设定它的接收者

4. Invoker

要求该命令执行这个请求。

5. Receiver

直到如何实施与执行一个请求相关的操作(命令具体执行的作用对象)。

当然接下来在使用时我们可以看到实际情况跟上面会略有不同,因为每一个模式到实际应用的时候都有一些变形。

Qt在其Qt’s Undo Framework里开篇介绍是这么说的:

Qt's Undo Framework is an implementation of the Command pattern, for implementing undo/redo functionality in applications.

对照着command pattern,按照自己的理解挨着介绍下:

1. QUndoCommand类,角色为Command

src/gui/util/qundostack.h里定义。我们看下完整的声明:

class Q_GUI_EXPORT QUndoCommand

{

QUndoCommandPrivate *d;

public:

explicit QUndoCommand(QUndoCommand *parent = 0);

explicit QUndoCommand(const QString &text, QUndoCommand *parent = 0);

virtual ~QUndoCommand();

virtual void undo();

virtual void redo();

QString text() const;

void setText(const QString &text);

virtual int id() const;

virtual bool mergeWith(const QUndoCommand *other);

int childCount() const;

const QUndoCommand *child(int index) const;

private:

Q_DISABLE_COPY(QUndoCommand)

friend class QUndoStack;

};


有的地方看不懂,没有关系,重点是这两句:

virtual void undo();

virtual void redo();

跟名为execute(),unExecute()是一样的。同时注意包括析构函数在内,很多函数都为虚函数.

顾名思义,merge函数可以实现command pattern里将多个命令装配为一个复合命令的效果,这也是command pattern的优点之一。

2. 对应的ConcreteCommand,我在程序里实现为

AppendTextCommand, ToUpperCommand,重新实现上面两个虚函数。稍后在源代码里可以看到。

3. Client

4. Invoker

QundoStack属于这一角色,也是QundoCommand*的容器(栈)。我们看下具体该类是怎么做的.

具体的声明就不贴了,贴下QundoStack的三个接口:

void push(QUndoCommand *cmd);

void undo();

void redo();

看下关于push的实现的一部分:

void QUndoStack::push(QUndoCommand *cmd)

{

Q_D(QUndoStack);//注意这条语句可以得到d指针(指向QundoStackPrivate对象,之前Qt宏定义介绍过),接下来很多地方会用到。

cmd->redo();

….

}

因此命令压栈时会执行一遍redo。

QundoStack的undo,redo命令实际上就是找到正确的QundoCommand命令然后执行该命令的undo,redo函数,其实现为:

/*!

Undoes the command below the current command by calling QUndoCommand::undo().

Decrements the current command index.

If the stack is empty, or if the bottom command on the stack has already been

undone, this function does nothing.

\sa redo() index()

*/

void QUndoStack::undo()

{

Q_D(QUndoStack);

if (d->index == 0)

return;

if (!d->macro_stack.isEmpty()) {

qWarning("QUndoStack::undo(): cannot undo in the middle of a macro");

return;

}

int idx = d->index - 1;

d->command_list.at(idx)->undo();

d->setIndex(idx, false);

}

/*!

Redoes the current command by calling QUndoCommand::redo(). Increments the current

command index.

If the stack is empty, or if the top command on the stack has already been

redone, this function does nothing.

\sa undo() index()

*/

void QUndoStack::redo()

{

Q_D(QUndoStack);

if (d->index == d->command_list.size())

return;

if (!d->macro_stack.isEmpty()) {

qWarning("QUndoStack::redo(): cannot redo in the middle of a macro");

return;

}

d->command_list.at(d->index)->redo();

d->setIndex(d->index + 1, false);

}


应该不会难看懂,可以看出来QundoCommandPrivate实际上在维护一个d->command_list,同时有一个d->index指向了当前的command。

5. Receiver就可以自己设定了,我在这里使用Qstring假设为一个doc文件。

Reveiver也可以写成一个虚类,这样就可以有多种Receiver了。

关于Command Pattern我理解的就这些O(∩_∩)O

还有些补充内容:

1. 从输出可以看到push到QundoStack的QundoCommand都执行了析构函数,是在QundoStack::clear()里做的,该函数在QundoStack析构时调用。

void QUndoStack::clear()

{

Q_D(QUndoStack);

if (d->command_list.isEmpty())

return;

bool was_clean = isClean();

d->macro_stack.clear();

qDeleteAll(d->command_list);

……

}

看下qDeleteAll(),在src/corelib/tools/qalgorithms.h(该文件有很多模板,容器以及迭代器的使用,对模板感兴趣的可以看下)里定义:

template <typename ForwardIterator>

Q_OUTOFLINE_TEMPLATE void qDeleteAll(ForwardIterator begin, ForwardIterator end)

{

while (begin != end) {

delete *begin;

++begin;

}

}

template <typename Container>

inline void qDeleteAll(const Container &c)

{

qDeleteAll(c.begin(), c.end());

}


哦,原来是这么执行的每个QundoCommand的析构函数,因此如果多次添加同一个QundoCommand*到QundoStack是会出现段错误的哦。

2. 同时关于撤销,删除操作我们可以设想这样一个场景:

对于一篇文章,我们键入”Hello”,再键入”World”。然后撤销,只剩”hello”,再键入”everyOne”。此时实际上我们new了三个QundoCommand命令(分别为添加Hello world everyOne)。但栈内只保存了第1,3个,第二个是否被析构了?何处析构的?怎么析构的?

答案为:是,QundoStack::push(),利用记录的d->index修改d->command_list。

当然,具体的还是自己去看会好理解一些。

最开始提到简单的测试例子:

#ifndef QCOMMAND_H

#define QCOMMAND_H

#include <QUndoCommand>

class AppendTextCommand : public QUndoCommand {

public:

AppendTextCommand(QString *doc, const QString &text);

~AppendTextCommand();

virtual void undo();

virtual void redo();

private:

QString *m_document;

QString m_text;

};

class ToUpperCommand : public QUndoCommand {

public:

ToUpperCommand(QString *doc);

~ToUpperCommand();

virtual void undo();

virtual void redo();

private:

QString *m_document;

QString originalDoc;

};

#endif//QCOMMAND_H

#include "command.h"

#include <QDebug>

AppendTextCommand::AppendTextCommand(QString *doc, const QString &text) :

m_document(doc) ,

m_text(text)

{

setText("append text");

}

AppendTextCommand::~AppendTextCommand()

{

qDebug() << "AppendTextCommand::~AppendTextCommand";

}

void AppendTextCommand::undo()

{

m_document->chop(m_text.length());

}

void AppendTextCommand::redo()

{

m_document->append(m_text);

}

ToUpperCommand::ToUpperCommand(QString *doc) :

m_document(doc)

{

setText("to upper");

}

ToUpperCommand::~ToUpperCommand()

{

qDebug() << "ToUpperCommand::~ToUpperCommand";

}

void ToUpperCommand::undo()

{

for ( int i=0; i<originalDoc.size(); ++i)

(*m_document)[i] = originalDoc[i];

}

void ToUpperCommand::redo()

{

originalDoc = *m_document;

*m_document = m_document->toUpper();

}