刚开始想做这个的时候,我专门去找了Qt官方的测试例子,运行起来点了点,代码翻了翻。然后照猫画虎般的写了个测试例子。
不明白,为什么每个例子旁边会有个命令的显示列表,还巨丑的那种,这如果要放在别的程序里面,岂不是很掉面。写完了那个测试例子的时候,我还是不明白它究竟是怎么运行的,为什么ctrl+z就能后退到上一步的操作,ctrl+y就能前进呢?
我看懂了官方例子中的为什么会有个命令栈,但委实没看懂他的那些命令派生。
后面多琢磨了几次,对着例子一遍一遍的撸了下代码,突然明白了,这不就是在某个需要触发的时刻记录下当前的状态和这一状态前面的那个状态,然后ctrl+z的时候设置为当前状态的前一的状态,ctrl+y的时候设置为当前状态的后一状态。
看明白了之后,你就会感觉原来他的那些命令派生的类对你没啥卵用,因为你还是要实现适合你自己状态的命令。
以一个简单的例子:
有一个绘图的需求,但是需要实现每次绘图之后可以通过快捷键快速绘图的功能。
解法相当简单,就是每一次在鼠标按下、移动、弹起的时候,将鼠标弹起之后的这一刻的图像和鼠标按下前一刻的图像通过某种方式保存起来,在需要回退的时候,直接替换成上一状态的图像即可。
这种需求,其实用两个有序的列表就可以实现,一个放当前的图像,一个放上一刻的图像,游标永远指向最后一张图像,这两个有序列表的游标其实是一样的。只不过Qt已经实现了 QUndoStack
用来存储。
The QUndoStack class is a stack of QUndoCommand objects.
先来看一下实现的效果:
首先定义一个命令的类,我这边将其定义成了一个单例,主要是因为虽然我有多个绘图的界面,但整个过程中只需要有一个回退功能就能满足,因为我在定义命令的时候多加了一个区分某个绘图控件的字段。
class CommandStack : public QObject
{
Q_OBJECT
public:
static CommandStack& instance()
{
static CommandStack instance;
return instance;
}
void push(const QImage& imgL, const QImage& imgC, const QVariant& data);
void keyEvent(QKeyEvent* event) override;
private:
CommandStack();
~CommandStack();
private:
QUndoStack* m_undoStack{ Q_NULLPTR };
QAction* m_undoAction{ Q_NULLPTR };
QAction* m_redoAction{ Q_NULLPTR };
QUndoView *m_undoView{ Q_NULLPTR };
};
m_undoStack
QUndoStack
对象指针,用来存储命令的栈;
m_undoAction
和 m_redoAction
是undo和redo action 指针
m_undoView
QUndoView
类的指针,主要用来在调试的时候显示每步操作的命令,当程序需要运行的时候,需要将其关闭或者不进行创建。
CommandStack::CommandStack()
{
m_undoStack = new QUndoStack(this);
m_undoAction = m_undoStack->createUndoAction(this, tr(""));
m_undoAction->setShortcuts(QKeySequence::Undo);
m_redoAction = m_undoStack->createRedoAction(this, tr(""));
m_redoAction->setShortcuts(QKeySequence::Redo);
#if TEST_WITH_STACK_VIEW
m_undoView = new QUndoView(m_undoStack);
m_undoView->setWindowTitle(tr("Command List"));
m_undoView->show();
m_undoView->setAttribute(Qt::WA_QuitOnClose, false);
#endif
}
上面的构造函数中,首先我们创建了一个 QUndoStack
对象,紧接着在该对象创建了redo和undo的action。并且用一个宏来控制是否需要创建 QUndoView
对象,该对象主要用来辅助测试。
Qt 帮助文档上说:
New commands are pushed on the stack using push().
所以我们申明一个push方法, Command 类是我们自定义的命令类,后面详解。
void CommandStack::push(const QImage& imgL, const QImage& imgC, const QVariant& data)
{
m_undoStack->push(new Command(imgL, imgC, data));
}
为什么要重写 keyEvent
呢?这是因为我的绘制图像的控件使用了QWidget
,并且在这个Widget
中已经重写了keyEvent
方法,导致我们的按键事件不会响应,所以我们需要手动去调用一下。
void CommandStack::keyEvent(QKeyEvent* event)
{
if (event->modifiers() == Qt::ControlModifier && event->key() == Qt::Key_Z)
{
m_undoAction->trigger();
}
if (event->modifiers() == Qt::ControlModifier && event->key() == Qt::Key_Y)
{
m_redoAction->trigger();
}
}
ctr+Z 组合键触发undo功能,ctrl+Y 组合键触发redo功能。调用 trigger方法之后,会自动调用我们自定义命令中的 undo 或者 redo方法。
构建完了 CommandStack
,我们需要定义一个命令的类,这个类需要继QUndoCommand
,然后通过重写 undo()
和 redo()
函数来实现自己的命令操作。
class Command : public QUndoCommand
{
public:
Command(const QImage& imgL, const QImage& imgC, const QVariant& data, QUndoCommand *parent = 0);
~Command();
void undo() override;
void redo() override;
private:
QImage m_imgLast{ QImage() };
QImage m_imgCurr{ QImage() };
QVariant m_data{ QVariant() };
};
因为要做的是对一个保存一个绘图过程中的状态,所以,这个命令类中我定义了三个成员变量。
m_imgLast
用来保存这一次鼠标pressed
之前的图片,在调用undo的时候进行图片设置
m_imgCurr
用来保存这一次鼠标release
之后的图片,在调用redo 的时候进行图片设置
m_data
在这个命令中我使用存储了一个 ImagePainter
的 QWidget
指针,表示这一次的图像具体是在哪一个Widget上进行替换,当然这个对象的类型可以直接声明为 void*
类型。
QVariant
储存 指针类型数据,通过下面的方式进行转换。
auto data = QVariant::fromValue(static_cast<void*>(ptr));
auto ptr = static_cast<CXXX*>(m_data.value<void*>());
自定义命令类的undo和redo方法其实非常简单,无非就是对指定的Wdiget设置当前状态或者上一状态的图像。
void Command::undo()
{
static_cast<CXXX*>(m_data.value<void*>())->updateImage(m_imgLast);
}
void Command::redo()
{
static_cast<CXXX*>(m_data.value<void*>())->updateImage( m_imgCurr);
}
至此,一个简单的 undo 和 redo功能已经完成了。