原文网址:http://www.dazzle.plus.com/linux/QtCreator/part08.htm
第8部份:实现撤销命令
撤销命令
Qt的撤销框架使用命令模式实现了应用程序的撤销/重做功能。命令模式是这样的思相在应用程序中所有的编辑操作会创建命令对象的实例。每个命令对象也知道怎么撤销它的变化。这里,我们将为添加,移动和删除电台操作实现撤销/重做功能,分别定义继承类QUndoCommand的三个子类:“CommandStationAdd”,“CommandStationMove”和“CommandStationDelete”。
注意到类的重要部份是提供了撤销/重做函数。正是这些函数用于编辑,而不是构造函数或虚构函数。当一个命令实例被压入撤销堆栈时,Qt撤销框架调用重做函数来执行所需要的编辑工作。此外,在构造函数里有一个短文本来描述撤销/重做函数执行了什么动作,显示在撤销堆栈视图上。
创建命令类
由于我们这些命类相当简单,将它们完全定义在头文件里,不需要用cpp文件。因此添加到QtCreator项目时,使用QtCreator“新建...”菜单,但是为添加的类选择“c++头文件”项。
我们添加的第一个类是在场景中添加新电台。在类的构造函数里有一个场景指针和x,y坐标参数,创建一个新电台,没有直接添加到场景里。在重做函数里添加电台到场景里,相反的操作(从场景里删除它)在撤销函数里。为了防止内存泄露,在虚构函数里也提供了删除方法,如果命令对象创建或删除电台不再被使用。
#ifndef COMMANDSTATIONADD_H
#define COMMANDSTATIONADD_H
#include <QUndoCommand>
#include <QGraphicsScene>
/*************************************************************************************/
/***************** 添加电台命令类 *****************/
/*************************************************************************************/
class CommandStationAdd : public QUndoCommand
{
public:
CommandStationAdd( QGraphicsScene* scene, qreal x, qreal y )
{
m_station = new Station( x, y );
m_scene = scene;
setText( QString("Station add %1,%2").arg(x).arg(y) );
}
~CommandStationAdd()
{
// 删除不在场景里的电台
if ( !m_scene->items().contains( m_station ) )
delete m_station;
}
virtual void undo() { m_scene->removeItem( m_station ); }
virtual void redo() { m_scene->addItem( m_station ); }
private:
Station* m_station; // 电台
QGraphicsScene* m_scene; // 场景
};
#endif // COMMANDSTATIONADD_H
我们的第二个类是在场景里移动电台。构造函数里有一个电台指针和电台移动前后的x,y坐标。在重做函数里把电台移到之后x,y坐标,在撤销函数里把电台移到之前的x,y坐标。
#ifndef COMMANDSTATIONMOVE_H
#define COMMANDSTATIONMOVE_H
#include "station.h"
#include <QUndoCommand>
/*************************************************************************************/
/***************** 移动电台命令类 *****************/
/*************************************************************************************/
class CommandStationMove : public QUndoCommand
{
public:
CommandStationMove( Station* station, qreal fromX, qreal fromY, qreal toX, qreal toY )
{
m_station = station;
m_from = QPointF( fromX, fromY );
m_to = QPointF( toX, toY );
setText( QString("Station move %1,%2 -> %3,%4").arg(fromX).arg(fromY).arg(toX).arg(toY) );
}
virtual void undo() { m_station->setPos( m_from ); }
virtual void redo() { m_station->setPos( m_to ); }
private:
Station* m_station; // 电台
QPointF m_from; // 之前坐标
QPointF m_to; // 之后坐标
};
#endif // COMMANDSTATIONMOVE_H
我们第三个命令类是从场景里删除电台。在构造函数里有一个电台指针和场景指针。在重做函数里把电台从场景中删除,而在撤销函数里把电台加回到场景里。
#ifndef COMMANDSTATIONDELETE_H
#define COMMANDSTATIONDELETE_H
#include <QUndoCommand>
#include <QGraphicsScene>
/*************************************************************************************/
/*************** 删除电台命令类 ***************/
/*************************************************************************************/
class CommandStationDelete : public QUndoCommand
{
public:
CommandStationDelete( QGraphicsScene* scene, Station* station )
{
m_scene = scene;
m_station = station;
setText( QString("Station delete %1,%2").arg(station->x()).arg(station->y()) );
}
virtual void undo() { m_scene->addItem( m_station ); }
virtual void redo() { m_scene->removeItem( m_station ); }
private:
QGraphicsScene* m_scene; // 场景
Station* m_station; // 电台
};
#endif // COMMANDSTATIONDELETE_H
增强Scene
现在我们已经创建好命令类,我们需要修改场景类去使用它们,而不是直接在自己内部编辑。
此外QGraphicsView框架允许用户可以同时移动多个元素项,我们需要实现一些功能来记录这些元素项移动前的位置。要做到这一点,我们将selectionChanged信号和添加一些新的私有变量到场景类。
添加类Station的前置定义。
class Station;
添加一个新的公共槽方法selectStations声明。
public slots:
void selectStations(); // 记录被选中的电台和它们的位置
重写保护方法mouseReleaseEvent的定义。
void mouseReleaseEvent( QGraphicsSceneMouseEvent* ); // 接收鼠标释放事件
定义一种新的私有类型,用一个QPair结构用来存储类Station指针和浮点精度的坐标,并且添加一个新的私有列表变量。
typedef QPair<Station*,QPointF> StationPos;
QList<StationPos> m_stations; // 当前被选中的电台和开始位置
引入命令类的头文件。
#include "commandstationadd.h"
#include "commandstationmove.h"
#include "commandstationdelete.h"
在构造函数里我们需要把selectionChanged信号和selectStations槽连接起来。
// 连接selectionChanged信号到selectStations槽
connect( this, SIGNAL(selectionChanged()), this, SLOT(selectStations()) );
在mousePressEvent方法里用下面单行代码替换添加项的那行代码,创建一个类CommandStationAdd对象并把它压入撤销堆栈。
m_undoStack->push( new CommandStationAdd( this, x, y ) );
在contextMenuEvent方法里用下面单行代码替换移除项和删除电台的那两行代码,创建一个类CommandStationDelette对象并把它压入撤销堆栈里。
m_undoStack->push( new CommandStationDelete( this, station ) );
添加槽方法selectStations的实现代码。在这方法里,私有变量m_stations存储着最新被选择的电台和它们的开始位置。
/********************************** selectStations ***********************************/
void Scene::selectStations()
{
// refresh record of selected stations and their starting positions
m_stations.clear();
foreach( QGraphicsItem* item, selectedItems() )
if ( dynamic_cast<Station*>( item ) )
m_stations.append( qMakePair( dynamic_cast<Station*>( item ), item->pos() ) );
}
添加函数mouseReleaseEvent的实现代码。在这个函数里,当鼠标按钮被释放时,将为私有变量m_stations列表里的每个电台创建一个类CommandStationMove对象并把它们压入undo堆栈。
/********************************* mouseReleaseEvent *********************************/
void Scene::mouseReleaseEvent( QGraphicsSceneMouseEvent* event )
{
// 如果电台移动,创建重做命令对象
foreach( StationPos station , m_stations )
if ( station.first->pos() != station.second )
m_undoStack->push( new CommandStationMove( station.first,
station.second.x(), station.second.y(),
station.first->x(), station.first->y() ) );
// 重新记录被选中的电台调用基类事件mouseReleaseEvent
selectStations();
QGraphicsScene::mouseReleaseEvent( event );
}
编译和运行
当你尝试运行程序新的代码将被编译。测试程序检查我们刚添加的撤销/重做功能。