手把手教你写Undo、Redo程序(续)
在第一篇文章“手把手教你写Undo、Redo程序”里,我介绍了如何实现一个基于图像操作的Undo, Redo框架结构。但是我们现在所讲的还只是Undo类的结构。还有一个非常重要的部分没有说明:我们的这些Undo类应该在什么地方使用呢?
还记得我们的第一个CImageData类吗?在ExcuteOperation函数的说明曾提到需要修改函数的参数。现在就是需要修改的时候了。
bool CImageData::ExecuteOperation(CImageOperation * pCmd, CUndoData * pData)
{
pCmd->Execute(this, pData);
}
bool CImageOperation::Execute(CImageData * pData, CUndoData *pData)
{
if (pData)
{
// 如果外部传入的Undo数据接口pData不空
// 我们就在进行真正的操作前先用pData构造好Undo数据
}
// 根据不同的派生类修改pData的图像数据
}
下面我们还是以图像旋转这个具体操作来看如何实现CImageRotate:
class CImageRatate : public CImageOperation
{
public:
CImageRatate(float fAngle) : m_fRotateAngel(fAngle) {}
virtual bool Execute(CImageData * pData, CUndoData * pData)
{
if (pData) // 根据不同的操作类型,pData实际上指向的是不同的派生类
pData->m_fRotateAngle = -m_fRotateAngle;
pData->Rotate(m_fRotateAngle);
// 这里假定Rotate函数是一个旋转图像的成员函数
}
private:
float m_fRotateAngle;
};
在这种结构里有许多值得改进的地方。下面我就三个方面对前面介绍的结构进行改进:
·支持单步操作的多级Undo/Redo
·支持基于图像Select的Undo/Redo
·实现Undo数据的磁盘保存
下面分别对每个方面进行单独说明。
1、 单步操作的多级Undo/Redo
先看看什么是单步操作的多级Undo/Redo: 在一些比较特殊的软件中,软件提供给用户的是执行某一步操作,但是在内部程序实现的时候会包含多个子操作。这多个子操作就形成了多级的Undo/Redo。当然,这种情况比较特殊,比如在绣花编辑软件中,用户的针脚操作在内部可能需要多个子操作,这样才能方便用户在后来根据子操作进行修改(比如针脚编辑).
特别说明:我下面所做的改进只能是体现了单步操作的多级Undo/Redo实现思想。之所以这么做主要是为了能利用前面的框架。为了把多个子操作的Undo/Redo封装起来。我们还需要一个Undo接口类。
它的基本实现方法如下:
class CUndoInfo
{
public:
// 默认值直接设置为1,因为m_pSaved至少有一个
CUndoInfo() : m_UndoNum(1) {}
virtual ~CUndoInfo() {}
virtual bool UndoAction(CImageData* pData)
{
do{
m_pSaved[m_UndoNum-1].UndoAction(pData);
m_UndoNum--;
}while(m_UndoNum>0 );
return true;
}
// 这里的CUndoData指针实际上可以是一个指针列表, 表示多个子操作
CUndoData * m_pSaved;
// 表示m_pSaved保存的指针个数
unsigned int m_UndoNum;
};
现在我们已经有了新的Undo接口类,所以给外部使用的文档接口类也需要改变。现在我们只需要把CUndoList中所有的CUndoData全部替换为CUndoInfo即可。怎么样,还算比较简单吧!
2、 基于图像Select的Undo/Redo
下面就实现带Select的Undo/Redo功能。在图像编辑时,如果用户在进行操作时,先选择了部分图像。那么我们在保存数据时就应该只保存修改的部分数据。这样才能节省空间,提高效率。
首先我们需要在添加一个矩形值成员变量,用来保存操作的范围。这个矩形值的保存位置有两个:如果你不想对前面定义的类做过多的修改,这个矩形值可直接放入CUndoInfo类;另一个位置是保存在CUndoData。
如果保存在CUndoInfo,我们就只需在CUndoInfo::UndoAction进行简单修改。
class CUndoInfo
{
// 保持原来的定义不变
RECT m_Rect;
virtual bool UndoAction(CImageData * pData)
{
// 根据m_Rect的值修改pData,也就是取pData部分数据形成新的pData
// 然后用新的数据调用内部Undo.
do{
m_pSaved[m_UndoNum-1].UndoAction(pData);
m_UndoNum--;
}while(m_UndoNum>0 );
return true;
}
};
如果保存在CUndoData抽象类中,我们就需要对每个派生类进行修改。很明显,这样的代价要比上一种方法大很多。所以我也不多做介绍。
3、 实现Undo数据的磁盘保存
节省内存空间我们才需要实现Undo数据的磁盘保存。保存在磁盘,更容易实现无穷级数的Undo、Redo操作。从前面的描述知道,可逆操作保存的是操作算法,这类操作一般占用的磁盘空间不多。我这里也就暂时不考虑实现对此类操作的磁盘保存。所以我们把保存在CFullImageUndo的CImageData数据直接保存在磁盘。我们对CFullImageUndo类做如下修改:
class CFullImageUndo
{
public:
virtual bool UndoAction(CImageData * pData)
{
// 先从m_pszFilePath读取文件的内容到临时内存
// 把读取的数据与pData的数据进行交换
// 把交换后的数据重新写入文件,必要时释放临时内存
}
public:
// 指向CImageData保存的图像数据位置
char * m_pszFilePath;
};
也许这些实现和改进都不值一提。如果能给部分初学者提供部分帮助足矣。