这学期的数据结构程序设计课里有一个自制文本编辑器的小作业,文本编辑器的增删改查涉及的算法知识不是很难,不过撤销和重做的实现倒是一个难点。这里我只是浅显地在字符串处理的层面实现了一下,比较简陋。
首先思考,每按一次撤销,就将文本返回到一定操作之前的阶段,每一个可供返回的状态作为一个元素,那么使用栈来存储状态是很不错的。重做则只要把每个从撤销队列里弹出的元素压到重做栈即可。另外想到,撤销不可能无限地进行下去,撤销栈应有一个最大元素数量。当到达这个数量,应该删掉栈底的状态,那么双向队列是很方便的选择。
声明一下我们的撤销类
文本编辑器我是用 MFC 写的,MFC 的各种 dialog 类其实自带撤消重做和序列化的接口,但我太笨了不会用(
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| class MyUndo { #define ADD_TEXT 1 #define DEL_TEXT 0
private:
CStringW text; deque<pair<int, pair<int, CStringW>>> undoStack; deque<pair<int, pair<int, CStringW>>> redoStack; const int maxUndo = 100;
public: MyUndo(CStringW value); MyUndo(); CStringW GetText(); void SetText(CStringW text);
void ExternUpdateText(int state, int pos, CStringW add); void InternUpdateText(int state, int pos, CStringW add, bool isUndo); void ResetRedo(); void ResetUndo(); void init(); bool Undo(); bool Redo();
};
|
看一下我们的 ExternUpdateText 函数
这里我的逻辑是,如果这一次更改的内容恰好与上一次更改的内容连接,比如这次输入的文字恰好在上一次输入的文件末尾,或这次删除的文字恰好在上一个删除的字符之前,那么它们就是同一个可撤销元素的内容,达到长按删除一大段之后撤销,所有内容都变回来的效果。注意主窗口 edit 控件响应 ON_EN_CHANGE 消息肯定是每输入一个字符就响应一次,所以更新文本其实每次只更新了一个字符,要是每次响应都创建一个可撤销状态,撤销起来就太慢了。你也可以改成你自己的逻辑,比如每隔一段时间就算新输入的字符在上一个可撤销状态的末尾,也重新新建一个状态,简而言之就是不让一个状态包含的字符串太长。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| void MyUndo::ExternUpdateText(int state, int pos, CStringW add) { if (undoStack.empty()) {
undoStack.push_back(make_pair(state, make_pair(pos, add))); InternUpdateText(state, pos, add, false); return; }
pair<int, pair<int, CStringW>>& last = undoStack.back(); if (ADD_TEXT == state) {
if (ADD_TEXT == last.first && pos == last.second.first + last.second.second.GetLength()) {
last.second.second += add; InternUpdateText(state, pos, add, false); } else {
if (undoStack.size() > maxUndo) {
undoStack.pop_front(); }
undoStack.push_back(make_pair(state, make_pair(pos, add))); InternUpdateText(state, pos, add, false); } } else {
if (DEL_TEXT == last.first && last.second.first == pos + 1) {
last.second.first -= add.GetLength(); last.second.second = (add + last.second.second); InternUpdateText(state, pos, add, false); } else {
if (undoStack.size() > maxUndo) {
undoStack.pop_front(); }
undoStack.push_back(make_pair(state, make_pair(pos, add))); InternUpdateText(state, pos, add, false); } } }
|
观察到 InterUpdateText 函数被多次调用,我们来看看
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| void MyUndo::InternUpdateText(int state, int pos, CStringW add, bool isUndo) { CStringW firstHalf = this->text.Left(pos); CStringW secondHalf = this->text.Mid(pos);
if (!isUndo) {
if (ADD_TEXT == state) {
firstHalf += add; } else if (DEL_TEXT == state) {
secondHalf = secondHalf.Mid(add.GetLength()); } } else {
if (ADD_TEXT == state) {
secondHalf = secondHalf.Mid(add.GetLength()); } else if (DEL_TEXT == state) {
firstHalf += add; } }
text = (firstHalf + secondHalf); }
|
可以发现这里调用这个函数纯粹就是把主窗口添加的字符添加到我们的撤销类里的备份的相应位置,使两者保持统一。而撤销的时候其实也是修改的撤销类中的备份内容,然后主窗口拿到修改之后的备份内容去替代原先的内容。
结束了,其他你只要在你想要设置的撤销节点调用 ExterUpdateText 函数,然后想撤销的时候调用 Undo 函数,再取得撤销对象更新后的 text 属性内容,替换原来主窗口内容就可以了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| bool MyUndo::Undo() { if (undoStack.empty()) { return false; }
pair<int, pair<int, CStringW>>& minus = undoStack.back(); InternUpdateText(minus.first, minus.second.first, minus.second.second, true);
redoStack.push_back(minus); undoStack.pop_back();
return true; }
|