先看下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();
}