用C++实现富文本控件: 撤销

本节是关于撤销重做相关实现. 项目地址: Github-RichED 本文备份地址: github

撤销重做

这就被称为UNDO/REDO之类的, 简直是'增量'的代表. 这也算富文本编辑器实现的一个难点. 虽然叫做撤销栈, 但是不完全是栈:

  • 一般栈只需要栈底、栈顶指针, 撤销栈多一个操作位置
  • 撤销是操作位-1, 栈顶不动, 直到达到栈底
  • 重做是操作位+1, 栈顶不动, 直到达到栈顶
  • 每一个操作(OP)入操作位栈, 操作位+1, 栈顶 = 操作位

具体实现中, 可以使用链表模拟栈. 然后设计上尽量使用可以直接调用free就能释放的Trivial数据, 原因后述.

装饰操作

替换操作很常见:

  • 将范围[2, +2]的'泥壕', 替换为长度为2的'你好'
  • 这个按下撤销键, 会先删除'你好'再添上'泥壕', 可以简化为两个OP:
  • (1). 将范围[2, +2]的'泥壕'删掉
  • (2). 在位置为2的地方插入长度为2的'你好'

这样就是将现有问题转换为已知的问题: 删除和插入是最基本的操作(坐下坐下这是基本操作). 但是就有一个问题: 撤销会变成两次.

Scintilla

Scintilla是一个与本项目类似的代码编辑控件, 作者提到富文本编辑器会把格式的修改也压入栈. 这个特性让他实为烦躁, 于是出现了Scintilla.

以上两点我们可以用一个装饰的标志解决, 将后面的装饰性操作打上标志:

STACK-TOP
OP                <- 独立的操作
OP   +decorator   <- 装饰操作
OP                <- 与上面的装饰操作作为一个整体的操作
OP   +decorator   <- 装饰操作
OP   +decorator   <- 装饰操作
OP                <- 与上面两个装饰操作作为一个整体的操作例子: 将'type'替换成'class ', 然后输入'if'STACK-TOP
OP: 输入' '
OP: [装饰, 词法解析器检测到关键字] 将'if'设为蓝色
OP: 插入'if'
OP: [装饰, 词法解析器检测到关键字] 将'class'设为蓝色
OP: [装饰, 实际是替换操作插入部分] 插入'class '
OP: 删除'type'

程序语言级可以用begin_opend_op之类的函数包裹实现:

void demo(void) {// 替换type->classbegin_op();remove_text(type);insert_text("class ");do_parser();end_op();// 插入ifbegin_op();insert_text("if");do_parser();end_op();// 输入' 'begin_op();insert_text(" ");do_parser();end_op();
}void insert_text(const char[]) {// 具体实现// 操作记录if (op) {// 撤销栈操作以0开始// 0到下一个0, 这个前闭后开区间为一个完整的撤销栈操作// 超过0的作为装饰操作gui_op.op = op - 1;op++;}
}void begin_op(void) {op = 1;
}void end_op(void) {op = 0;
}

当然, 这是将富文本操作作为装饰的一种实现方式, 实际上还是把富文本操作记录在撤销栈上了.

使用begin_opend_op之类的函数包裹的话, 还有一种实现就是完全将富文本操作排除在撤销栈操作外, 每次都由词法解析器进行富文本更新.

// 插入if
begin_op();
insert_text("if");
end_op();
do_parser();

好处是不会记录在撤销栈上, 坏处是撤销/重做后还要调用do_parser().

换行符

编辑器是允许中途更换换行符的, 这时候直接使用撤销栈会造成报道上的偏差.解决方法有:

  1. 修改换行符时修改撤销栈上所有OP
  2. 不直接保存绝对长度信息(总偏移), 而是相对信息(行号+行内偏移)

这里选择了后者, 方便全局编写

平平无奇撤销栈

撤销栈储存的OP必须是平平无奇的(trivial), 主要有两个特点:

  1. 没有析构操作, 或者说唯一的析构操作就是释放空间(std::free())
  2. 核心数据没有指针, 全部是偏移量
图中bytes_from_here之后没有指针, 全是连续排列的, 并且对齐RED_RICHED_ALIGNED

这样的好处有:

(1) 无需担心对象所有权. 内联对象, 特别是图片是相当消耗储存空间. 不可能显示保存一份, 撤销栈保存一份. 所以撤销栈上图片保存的仅仅是一种简略信息的引用. 比如引用计数什么的.

不过引用计数之类的会有析构之类的操作, 作为历史保存不太合适: 明明文档中不存在, 但是因为撤销栈存在引用就必须储存在内存中. 所以干脆平凡化: 储存简略信息, 比如uri字符串.

(2) 上面是没有析构操作的好处, 这里是没有指针的好处: 储存偏移量几乎可以不加修改地直接将二进制信息存储在硬盘上, 即可以将撤销栈完整地、简单地存储下来.

(3) 上面这一点可能不太吸引人, 但是可以作为二进制文件编码, 将富文本以二进制形式直接储存下来.

富文本怎么保存? xml? rtf? 这些都是与其他程序交互时使用! 并且还要涉及到解析和标准什么的(光rtf解析就够吃一壶了). 自己用的, 直接储存撤销栈然后重现就行: 完毕后将全部字符串删掉, 将撤销栈最后一步保存下来(当然只是为了方便说明, 实际直接记录就行).

即: 超简单的序列化(Serialization)的实现.

当然, 没有指针的话, 空间的消耗将是最小的, 也算是一大优点. 以及也可以用于相同编辑框数据交换的类型, 例如操作系统的下的复制粘贴. 平平无奇+无指针, 强, 无敌.

大概具体的实现

富文本插入:

  • 撤销: 插入的数据删除, 需要数据: 起点与重点
  • 重做: 将插入的富文本插入回去, 需要数据: 起点, 富文本数据

富文本删除:

  • 撤销: 将插入的富文本插入回去, 需要数据: 起点, 富文本数据
  • 重做: 插入的数据删除, 需要数据: 起点与重点

内联对象插入:

  • 撤销: 将插入的数据删除, 需要数据: 起点(长度假定为1)
  • 重做: 将插入的内联对象插入回去, 需要数据: 起点, 内联对象

内联对象删除:

  • 撤销: 将插入的内联对象插入回去, 需要数据: 起点, 内联对象
  • 重做: 将插入的数据删除, 需要数据: 起点(长度假定为1)

可以看出插入与删除完全就是相反的, 我们将对象进行分解:

  • 内联对象的插入与替换
  • 富属性的修改与回退
  • 纯文本的插入
  • 范围删除
  • 无视

请注意, 这里内联对象的'替换'并不是真正的意义上的替换. 不会对文本长度造成影响, 完全是为撤销服务的. 所以自己就称之为升阶(Rank-Up), 需要调用RankUpMagic完成替换.

平平无奇的实现

  1. 装饰操作
  2. 平平无奇

综合之前所述的平平无奇撤销栈操作, 没有析构步骤, 以及'装饰物'. 算法如下:

删除:

  • 删除操作是不分对象的
  • 将范围内特殊对象按顺序记录, 装饰OP +1
  • 将范围内记录所有富属性, 装饰OP +1
  • 将范围内记录所有纯文本, 装饰OP +1
  • 范围删除

即:

  • 内联对象撤销: 内联对象的的升阶
  • 内联对象重做: 无视
  • 富属性撤销: 富属性的修改
  • 富属性重做: 无视
  • 纯文本撤销: 纯文本的插入
  • 纯文本重做: 范围删除

装饰OP的范围需要给出一个大概范围, 这里可以看出最大是+3. 之所以需要判断范围, 是因为有限撤销栈的实现:

  • 完整记录OP后(调用end_op())检查数量
  • 如果OP数量超过范围, 获取撤销栈(队列)末尾OP
  • 遍历到第二个OP的开头(用装饰OP判断)
  • 如果 当前撤销栈长度 减去 第一个OP的长度, 如果大于等于范围
  • 将第一个完整的OP释放掉, 第二个OP作为新的第一个OP

插入对象

由于插入只能插入一种, 所以后面将会很简单. 插入内联对象则是:

  • 装饰OP +1
  • 撤销: 范围删除
  • 重做: 内联对象的插入

插入文本

由于只能插入纯文本(富属性单独设置), 插入是比较简单的.

  • 装饰OP +1
  • 撤销: 范围删除
  • 重做: 文本的插入

富属性修改

同样, 修改只能将范围内修改为同一个属性, 和删除的差不多.

  • 装饰OP +1
  • 撤销: 富属性的回退
  • 重做: 富属性的修改

替换

之前提到了, 直接组合就行.

附加变化的实现

一般地, 如果连续按退格键, 撤销后会回到第一次退格前的位置. 所以针对输入单个字符和退格删除的实现, 我们需要检查一下是否可以与目前的OP进行合并.

具体实现中, 主动检查有点繁琐, 采用被动检查的方式. 好处就是降低耦合, 坏处就是效率会稍微低一点. 不过由于这些是GUI输入, 不会太频繁, 就如所谓了.

插入符号的变化

上面没有讨论插入符的更新, 这个只需要保存就行. 于是出现了一些专门为撤销栈实现的方法:

图片 富文本 粘贴_用C++实现富文本控件(中): 撤销相关推荐

  1. 安卓imageView加载MYSQL图片_Android调用相机拍摄照片并显示到 ImageView控件中

    在前面的一篇文章中曾介绍过简单的开启相机照相功能,详见 Android简单调用相机Camera功能,实现打开照相功能 ,这一次就会将前面拍摄的照片显示到ImageView中,形成一个完整的效果 看实例 ...

  2. python 选择文件对话框插件_[ PyQt入门教程 ] PyQt5基本控件使用:消息弹出、用户输入、文件/目录选择对话框...

    本文主要介绍PyQt界面实现中常用的消息弹出对话框.提供用户输入的输入框.打开文件获取文件/目录路径的文件对话框.学习这三种控件前,先想一下它们使用的主要场景: 1.消息弹出对话框.程序遇到问题需要退 ...

  3. WPF 把图片分割成两份自动翻页 WpfFlipPageControl:CtrlBook 书控件

    原文:WPF 把图片分割成两份自动翻页 WpfFlipPageControl:CtrlBook 书控件 版权声明:本文为博主原创文章,需要转载尽管转载. https://blog.csdn.net/z ...

  4. 格式化显示在Label控件中的金额格式文本 (2)

    通过Label控件的Text属性可以设置控件内显示的文本,而通过对象的ToString方法可以格式化字符串显示的格式.本实例设定在Label控件中金额的显示格式,运行结果如图1所示. <?XML ...

  5. VB.net实现从ListView控件中异地获取文本内容源代码

    对于一些VB6工程代码来说,需要完成从VB到VB.net的转换,在转换过程中需要注意很多内容,利用VB.net的直接转换功能很少能完全成功,需要我们付出很大的努力. 下面的VB.net代码实现从其他应 ...

  6. MFC 对话框Picture Control(图片控件)中静态和动态显示Bmp图片

    最近有同学问我如何实现MFC基于对话框在图片控件中加载图片?其实使用MFC显示图片的方法各种各样,但是还是有些同学不知道怎样显示.以前在<数字图像处理>课程中完成的软件都是基于单文档的程序 ...

  7. easyui数据表格显示复选框_【Excel技巧】使用控件一键切换实现单位元和万元随意显示...

    工欲善其事,必先利其器.职场上亦是如此.Excel报表想要做得完美,首先肯定Excel要精通. 做一份Excel报表,如果涉及到金额,当金额比较大,单位到底是用元还是万元,经常是大家纠结的一个问题.我 ...

  8. Qt 实现QT控件中的QLabel显示图片并自适应显示

    一.需求 实现QT控件中的QLabel显示图片,并自适应显示. 二.代码 QImage Image; Image.load(":/image/image/logo.jpg"); Q ...

  9. 【MFC】动态加载Picture Control控件中的图片

    [MFC]动态加载Picture Control控件中的图片 前言 方法1:CBrush 方法2:SetBitmap 参考链接 方法3:重写MyPictureControl 前言 在MFC窗体中,我们 ...

最新文章

  1. composer更新_深入学习Composer原理(四)
  2. Java教程:SpringBoot常用配置
  3. 设计模式的征途—2.简单工厂(Simple Factory)模式
  4. java开发展望怎么写_Java开发趋势:2019年展望
  5. Windows下Subversion配置管理员指南
  6. JSON Schema
  7. Apple分区总体布局结构
  8. 洛谷——P1116 车厢重组
  9. c语言程序设计身高体重测评系统,C语言程序设计验.doc
  10. 为串联机械臂写一个ROS控制器
  11. ATX电源的工作原理
  12. SVCHOST启动服务实战
  13. C++STL算法equal(15)
  14. 微信小程序开屏动画组件封装以及使用示例
  15. Excel2019关闭时无响应
  16. 2009世界500强排名(2009年07月08日)
  17. 递归门控卷积HorNet(gn_conv)阅读笔记
  18. 河师大计算机学院宿舍情况哪,河师大环境学院“惊现”7个学霸宿舍
  19. 聊一聊IT培训机构的那些事!
  20. 总结——STL 常用数据结构及用法

热门文章

  1. MEF董事、中国电信云计算中心主任赵慧玲:MEF第三类网络
  2. canvas绘制字体-属性设置2
  3. JavaGUI版本销售管理系统
  4. Servlet Request
  5. 读《大学之路》有感②
  6. 异步 HttpContext.Current实现取值的方法(解决异步Application,Session,Cache...等失效的问题)...
  7. 教你10分钟搭建酷炫的个人博客
  8. k邻近算法(KNN)实例
  9. 【推荐】2016年不得不读的九本好书
  10. 九度OJ1005题 一直WA??