VC之EditBox多重UnDo/ReDo功能实现

转载 2012年03月24日 18:44:50
VC之EditBox多重UnDo/ReDo功能实现
2010-12-03 10:25

Edit Box控件提供了UnDo功能,但只能撤销一次操作,要想实现多重UnDo/ReDo功能需要自己设计。

UnDo:撤销上一次修改操作,实现时应保存最近几次修改。

ReDo:重做上一次撤销的操作,如果你撤销后后悔了,就ReDo吧。

1、数据结构

利用一个结构数组作为栈保存最近几次修改操作,定义如下:

#define UNDOMAX         30    //栈深度(最大Undo次数)

//可撤销的操作名
#define OP_DELSEL      1     //删除选择(剪切)
#define OP_REPLACE     2     //替换选择
#define OP_DELETE      3     //删除
#define OP_BACK        4     //Backspace
#define OP_INPUT       5     //输入

//Undo/Redo栈结构
typedef struct
{
     int    op;        //操作名
     int    pos;       //操作的位置
     CString str1;    //旧内容
     CString str2;    //新内容
}STACKNODE;

private:
     STACKNODE m_Stack[UNDOMAX];     //工作栈
     int utop;          //Undo栈顶指针
     int ubottom;       //Undo栈底指针
     int rtop;          //Redo栈顶指针
     int rbottom;       //Redo栈底指针
     BOOL b_DelFlag;    //删除标志

UNDOMAX是预定义的栈深度,这里定义为30,表示可撤销最近的30步操作。

STACKNODE结构用来定义栈节点,对每一次修改操作,需要纪录修改的位置,修改前的内容和修改后的内容。而且不同的修改操作在撤销时会略有不同,所以还需纪录修改操作名。

所有修改可归结为5种:

OP_DELSEL:删除选择的文本,此时str1保存被删除的文本,str2为空;
OP_REPLACE:替换选择的文本,str1为被换掉的文本,str2为新文本;
OP_DELETE:用Del键删除文本,str1保存被删除的文本,str2为空;
OP_BACK:用BackSpace键删除文本,str1保存被删除的文本,str2为空;
OP_INPUT:键盘输入新文本,str1为空,str2为新输入的文本。

其它的操作都可归纳到这5种之内,如剪切就是OP_DELSEL,粘贴就是OP_REPLACE。

m_Stack是长度为UNDOMAX的栈,它既是UnDo栈,也是ReDo栈,栈指针utop、ubottom确定UnDo栈位置,rtop、rbottom确定ReDo栈位置。

2、栈操作

①初始化工作栈

void CEditBox::InitStack()
{
     for( int i=0; i<UNDOMAX; i++ )    //栈空间
     {
         m_Stack[i].op = -1;
         m_Stack[i].str1 = _T("");
         m_Stack[i].str2 = _T("");
     }
     utop = 0;        //栈指针
     ubottom = 0;
     rtop = 0;
     rbottom = 0;
}

②入栈

void CEditBox::Push(STACKNODE *pNode)
{
     utop = (utop+1)%UNDOMAX;    //修改栈顶指针
     rtop = utop;                //清空Redo栈
     rbottom = utop;
     if( utop==ubottom )            //如果栈满
         ubottom = (ubottom+1)%UNDOMAX;    //修改栈底指针
     m_Stack[utop] = *pNode;        //入栈
}

每次修改操作时,把纪录修改的节点推入栈中。栈采用环形结构,当栈满时,新入栈的节点覆盖栈底节点,也就淘汰了最早进入栈内节点。

③UnDo出栈

STACKNODE *CEditBox::UnDoPop()
{
     if( utop==ubottom )        //栈空
         return NULL;
     STACKNODE *p = &m_Stack[utop];
     rtop = utop;              //Redo入栈
     utop = utop-1;            //退栈
     if( utop<0 )
         utop = UNDOMAX-1;
     return p;                //返回退栈节点
}

当进行UnDo操作时,从utop指示的栈顶弹出记录最近一次修改的节点,但这个节点并不删除,通过修改rtop使它进入ReDo栈,供ReDo操作时重做被撤销的操作。所以,这个操作既是UnDo出栈,也是ReDo进栈。

④ReDo出栈

STACKNODE *CEditBox::RedoPop()
{
     if( rtop == utop )
         return NULL;
     STACKNODE *p = &m_Stack[rtop];
     utop = (utop+1)%UNDOMAX;        //Undo入栈
     if( rtop==rbottom )     //如果Redo栈满
     {
         rtop = utop;        //修改栈底指针
         rbottom = utop;
     }
     else
         rtop = (rtop+1)%UNDOMAX;        //修改栈顶指针
     return p;        //返回出栈的节点
}

当进行ReDo操作时,从rtop指示的栈顶弹出记录去恢复被撤销的操作,同时通过修改utop使它重新进入UnDo栈,供UnDo操作使用。所以,这个操作执行了ReDo出栈,也执行了UnDo进栈。

这个栈的变化方式如图所示:

UnDo栈和ReDo栈共享一个数组空间,ub和ut是UnDo栈的栈底指针和栈顶指针,rb和rt是ReDo栈的栈底指针和栈顶指针。初始时它们均相等,这时为空栈(图a)。当进行修改操作时,修改结构进栈,修改ut到新的栈顶,同时rb和rt同步移动,ReDo栈仍为空(图b)。当进行撤销操作时,把ut指示的修改结构取出恢复内容,修改ut表示结构从UnDo栈退出,同时修改rt,使结构进入ReDo栈(图c)。恢复撤销时,用rt指示的结构进行恢复。如果不恢复,新的修改进入UnDo栈,同时让rt和rb跟踪ut,使ReDo栈清空。

这个空间是个环形空间,栈满时修改栈底指针淘汰陈旧内容。

⑤取栈顶

STACKNODE *CEditBox::GetTop()
{
     if( utop==ubottom )
         return NULL;
     return &m_Stack[utop];
}

取UnDo栈的栈顶元素,但不修改栈。

3、UnDo/ReDo操作

①保存可撤销的操作

void CEditBox::SetUndo(int op,int pos,LPCTSTR str1,LPCTSTR str2)
{
     STACKNODE *ptop = GetTop();
     if( op==OP_DELETE && ptop && ptop->op==OP_DELETE && ptop->pos==pos )
     {
         ptop->str1+=str1;
         return;
     }
     STACKNODE node;
     node.op = op;
     node.pos = pos;
     node.str1 = str1;
     node.str2 = str2;
     Push( &node );        //入栈
}

每做一次修改操作,通过调用SetUndo()函数把修改操作保存到栈中。参数依次为操作名、修改位置、旧内容和新内容。

函数中先根据参数填写好STACKNODE节点,再把它推入栈内即可。

这里对OP_DELETE操作做了一个特殊处理,当我们用Del键在同一位置连续删除多个字符时,应该记录为一个操作,在撤销时一次性恢复。所以当操作为OP_DELETE时,如果发现上一次操作也是OP_DELETE,且位置相同,则把新删除的字符连接到上次删除的字符串中就行了。

②处理Del键和BackSpace键

用按键消息检测是否进行了Del和BackSpace键操作,并进行相应处理。

void CEditBox::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags)
{
     CString Text, SelStr;
     int pos, SelLen;
     GetText( pos, Text, SelStr, SelLen );
     if( nChar==VK_DELETE && !Text.IsEmpty() )    //Del键
     {
         if( SelLen )
             SetUndo( OP_DELSEL, pos, SelStr, _T("") );  //保存删除内容
         else
         {
             if( Text[pos]<0 || Text[pos]==0x0D || Text[pos]==0x0A )
                 SetUndo( OP_DELETE, pos, Text.Mid(pos,2), _T("") );  //删除的是汉字或回车
             else
                 SetUndo( OP_DELETE, pos, Text.Mid(pos,1), _T("") );  //删除的是字符
         }
         b_DelFlag = true;
     }
     else if( nChar==VK_BACK && pos>=0 )    //BackSpace键
     {
         if( SelLen )
             SetUndo( OP_DELSEL, pos, SelStr, _T("") );  //保存删除的内容
         else
         {
             if( pos>0 && Text[pos-1]<127 && Text[pos-1]>=0x20 )
                 SetUndo( OP_BACK, pos-1, Text.Mid(pos-1,1), _T("") );
             else if( pos>1 && (Text[pos-2]<0 || Text[pos-1]==0x0D || Text[pos-1]==0x0A) )
                 SetUndo( OP_BACK, pos-2, Text.Mid(pos-2,2), _T("") );
         }
         b_DelFlag = true;
     }
    
     CEdit::OnKeyDown(nChar, nRepCnt, nFlags);
}

OnKeyDown()函数用ClassWizard添加到CEditBox类中。当检测到Del键或BackSpace键时,如果此时有选择的文本,就保存为“删除选择OP_DELSEL”操作,如果没有选择的文本再保存为“OP_DELETE”操作或“OP_BACK”操作。另外,删除字符和删除汉字也有所区别,如果删除位置为半角字符,就取一个字符进入UnDo栈,如果删除位置处为汉字,就取两个字节进入UnDo栈。

③处理键盘输入

用ClassWizard添加OnChar()函数处理键盘输入,与OnKeyDown()函数不同,OnChar()函数只在输入可打印字符时才响应,用它可过滤掉各种控制键,而OnKeyDown()函数对任何按键都要响应,所以Del键只能用它来处理。

void CEditBox::OnChar(UINT nChar, UINT nRepCnt, UINT nFlags)
{
     CString SelStr;
     ReadSelText( SelStr );
     int pos1, end, pos2;
     GetSel( pos1, end );    //获取输入前的光标位置
     CEdit::OnChar(nChar, nRepCnt, nFlags);    //处理键盘输入
     if( !b_DelFlag )
     {
         CString Text, str;
         GetWindowText( Text );
         GetSel( pos2, end );    //获取输入后的光标位置
         str = Text.Mid(pos1,pos2-pos1);
         SetUndo( OP_INPUT, pos1, SelStr, str );  //保存到UnDo栈
     }
     b_DelFlag = false;
}

为了处理汉字的输入,在处理按键函数CEdit::OnChar前纪录光标位置,这个位置是输入文字前的位置,在CEdit::OnChar函数后再求得的光标位置是输入文字后的位置,两者之间的符号就是应该保存的撤销内容。

这里有个缺陷,就是当输入汉字词组后,撤销时只能一个字一个字的撤销,不能一下撤销整个数组,不知该如何改进。

b_DelFlag用于防止Del键和BackSpace键被重复处理。

4、接口函数

CEditBox增加了UnDo/ReDo功能后,原来的那些涉及修改控件内容的操作就需要重新设计,使它们能够被撤销,所以设计了新的接口函数。

①isCanUndo()和isCanRedo()

判断当前是否有可撤销和可重做的操作。用于激活或禁止UnDo/ReDo菜单。

//判断能否撤销
BOOL CEditBox::isCanUndo()
{
     return utop!=ubottom;
}

//判断能否重做
BOOL CEditBox::isCanRedo()
{
     return utop!=rtop;
}

当存在可撤销或可重做的操作(栈非空)时返回TRUE,否则返回FALSE。

②EditUndo()和EditRedo()

EditUndo()函数执行撤销操作,EditRedo()函数执行重做操作。供UnDo/ReDo菜单调用。

//撤销
void CEditBox::EditUndo() 
{
     STACKNODE *p = UnDoPop();    //出栈
     if( p )
     {
         SetSelText( p->pos,p->str2.GetLength() );    //选择文本
         ReplaceSel( p->str1 );  //恢复内容
     }
}

//重做
void CEditBox::EditRedo() 
{
     STACKNODE *p = RedoPop();  //出栈
     if( p )
     {
         SetSelText( p->pos,p->str1.GetLength() );   //选择文本
         ReplaceSel( p->str2 );  //恢复内容
     }
}

③EditReplace()和RepAll()

EditReplace()函数是用指定文本替换选择的文本。RepAll()是用指定文本重置编辑控件内容。

//替换选择
void CEditBox::EditReplace(LPCTSTR str)
{
     CString Text, SelStr;
     int pos, SelLen;
     GetText( pos, Text, SelStr, SelLen );
     SetUndo( OP_REPLACE, pos, SelStr, str );    //保存到Undo
     ReplaceSel( str );    //替换选择
}

//替换全部
void CEditBox::RepAll(LPCTSTR str)
{
     SetSel( 0, -1 );      //全选
     EditReplace( str );   //替换
     SetSel(0);            //设置插入点为起始位置
}

EditReplace()函数用来代替CEdit类的ReplaceSel()函数,两者功能一致,但用EditReplace()函数做的操作可以撤销,而ReplaceSel()函数做的操作不能撤销。

RepAll()函数用来代替SetWindowText()和前面定义的SetText(),用它做的操作可以撤销。

④剪切和粘贴

CEdit类的Cut()和Paste()函数都需要重新设计,而复制操作Copy()由于不修改文本,可照常使用。

//剪切(代替Cut())
void CEditBox::EditCut()
{
     CString Text, SelStr;
     int pos, SelLen;
     GetText( pos, Text, SelStr, SelLen );
     SetUndo( OP_DELSEL, pos, SelStr, _T("") );    //保存到Undo
     Cut();        //剪切
}

//粘贴(代替Paste())
void CEditBox::EditPaste() 
{
     OpenClipboard();    //打开剪贴板

     HANDLE StrHandle;
     StrHandle = ::GetClipboardData(CF_TEXT);

     char* pMem;
     pMem = (char*)::GlobalLock(StrHandle);

     CString str;
     str = pMem;
     ::GlobalUnlock(StrHandle);
     CloseClipboard();    //关闭剪贴板

     if( !str.IsEmpty() )
         EditReplace( str );      //粘贴
}

//全部剪切
void CEditBox::EditCutAll()
{
     SetSel( 0, -1 );    //全选
     EditCut();          //剪切
}

//全部复制
void CEditBox::EditCopyAll() 
{
     SetSel( 0, -1 );   //全选
     Copy();            //复制
}

//全部删除
void CEditBox::EditClearAll()
{
     SetSel( 0, -1 );          //全选
     EditReplace( _T("") );    //清空
}

七、CEditBox类的使用:

CEditBox类与CEdit类的用法基本相同。你可以在对话框里加入一个Edit Box控件,设置属性Multiline(多行文本)、Vertical scroll(垂直滚动条)、Want return(接收回车),并取消Auto HScroll属性(自动换行);用ClassWizard为控件添加变量,类型设置为CEditBox;再加入头文件#include "EditBox.h";之后就可以根据需要编程控制这个编辑控件了。

再解决一个小问题,就是Tab键的输入问题。

在界面上,Tab键起到选择控件的功能,这导致无法在编辑控件中输入Tab符。

解决方法是:

先在资源中定义一个Tab快捷键:打开资源的Accelerator下的IDR_MAINFRAME,在快捷键表中添加一个VK_TAB的快捷键,假设ID设置为ID_KEY_TAB;

回到视类,用ClassWizard为ID_KEY_TAD添加消息函数OnKeyTab(),在其中加入代码:

//处理Tab输入
void CEditTestView::OnKeyTab()
{
     m_EditBox.EditReplace( _T("\t") );
}

这里的m_EditBox就是编辑控件的控制变量。

这样CEditBox类就做好了,它的完整代码见示例程序中的EditBox.h和EditBox.cpp。

示例程序是利用CEditBox类制作的一个文本编辑器,它具有了记事本的多数功能。它还具有以下特征:可设置编辑区的字体、颜色;利用ini文件保存设置;有字数统计功能,支持查找/替换操作等。它本身就是一个可代替记事本的好用的编辑器。在此基础上,你可以根据需要添加更多的功能。

相关文章推荐

不用写一行代码,用MFC向导实现的文本编辑器(类似Windows下的记事本)

不用写一行代码,用MFC向导实现的文本编辑器(类似Windows下的记事本) 源码下载:http://download.csdn.net/detail/gencheng/6647927   这个东...

Arcgis Engine Undo和Redo 功能实现

  • 2008年06月04日 15:07
  • 26KB
  • 下载

vc++ 做的一个redo/undo

之前要编一个小程序,需要undo/redo的功能,上网看了很多文章,提到什么Command设计模式,还有堆栈这些的,但是我不是计算机专业的,vc只学了一点点所以这些都不懂,最后参考网上的方法,自己做了...

C#做的简单的Undo、Redo功能的实现

  • 2013年06月30日 11:41
  • 296KB
  • 下载

iOS开发中的NSUndoManager的undo和redo功能

以前保存账户登录数据时用过一次CoreData,最近在研究CoreData官方demo(CoreDataBooks)的时候,发现了一个有意思的功能undo/redo,也就是给我们弥补犯下犯错的功能, ...

多步Undo_Redo的实现

  • 2012年03月01日 17:54
  • 43KB
  • 下载

undo redo 实现感想

  • 2010年05月26日 23:49
  • 215KB
  • 下载

重做(redo)和撤销(undo)的完整实现

undo-redo需要备忘录模式和命令模式做支撑,之前有学习过了command模式和memento模式的一些基本知识。这里要结合两个模式实现一个undo-redo操作的模块,巩固所学的知识。 系统框...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:VC之EditBox多重UnDo/ReDo功能实现
举报原因:
原因补充:

(最多只允许输入30个字)