- 将文本行集合Lines清空
- 设置所有参与分行的元素集合
- 从前到后的遍历所有的参与分行的元素集合中的所有子元素
- 若子元素对象为制表符或水平线对象则重新计算它的宽度
- 若子元素为一个容器对象则调用它的RefreshLine方法
- 向当前行的元素列表中添加元素,并累计元素的宽度和,若宽度和大于容器显示宽度(我们称为情况1)或者当前元素单独占据一行则取消向当前行添加元素并结束当前行
- 若当前元素是强制换行的则结束当前行
- 在结束当前行前,若当前元素不能出现在行尾或者下一个元素不能出现在行首则取消向当前行添加当前元素(这也算情况1)。按照书写惯例,某些字符例如!),.:;?]}¨·ˇˉ―‖’”…∶、。〃々〉》」』】〕〗!"'),.:;?]`|}~¢是不能显示在行首,而另外一些字符例如([{·‘“〈《「『【〔〖(.[{£¥是不能显示在行尾,此外在某些特定的应用中可能还有其他类型的元素也出现这种情况,这些情况需要考虑。为此在基础元素对象类型TextElement中定义了方法 CanBeLineHead 来判断元素对象是否可以出现在行首,定义了方法CanBeLineEnd来判断元素对象是否可以出现在行尾,这样字符元素对象和其他元素对象可以重载这两个方法来进行所需的判断。在进行这样的判断要特别的小心,若容器显示宽度比较小则有可能由于这种判断而导致死循环,因此还需要额外的进行反死循环的判断(当年为了发现这个错误而呕出了几十两血)。
- 在结束当前行时需要计算文档元素在当前行中的相对位置,若当前行是由于情况1而导致结束的则需要修正元素间距,由于文档行所有元素的宽度和不一定等于容器的显示宽度,因此若没有进行修正则文档的右边缘参差不齐,影响美观,因此需要计算元素宽度和和容器的显示宽度之差,将该宽度差比较均匀的插入到各个文档元素之间,这样文档的右边缘则比较整齐。为了保存这个修正值,在TextElement中新增一个WidthFix属性来保存该值。其实大家可以观察到IE显示文档内容时没有进行右边缘的修正而Word则进行了类似的修正
- 若当前行是由于最后一个元素强制分行而结束的则无需进行由于情况1而导致的右边缘修正,但计算文档元素位置时需要进行文档对齐方式的修正。首先找到影响当前文本行的段落对象,获得它的对齐方式设置(左对齐,右对齐,居中对齐),根据对齐方式来计算元素见的空白,然后设置元素的WidthFix属性
- 此外还需要修正元素在文档行中的顶端坐标,由于同一行的文档元素高度不一定一致,此时需要遍历所有的元素,以最高的元素的高度为文档行的高度,以此计算元素在文档行中的顶端位置,以保证各个元素的低边缘在同一水平线上
- 结束完毕的行对象添加到容器的Lines文档行集合中,然后创建创建一个文档行对象作为当前行,如此循环直到处理了容器对象所有的内容
- 产生了所有的文档行对象后根据容器对象的在视图区域中的坐标和文档行的行间距设置来计算文档行在视图区域中的坐标,这样文档行中所有的元素的在视图区域中的坐标就是文档行的坐标和元素在文档行中的相对坐标的和
- 在修改文档行中元素的位置时,需要获得元素旧的在视图区域中的最小外切矩形数据,然后和重新计算过的最小外切矩形进行比较,若两者不一样则表示元素在视图区域中显示的位置发生改变,将这两个矩形添加到文本编辑器重绘矩形集合中,当文档重新分行完毕后,文本编辑器就将所有的重绘矩形进行加法操作,获得的矩形就是需要重新绘制的区域。如此这样是为了优化显示操作,减少页面闪烁;因为用户修改了文档内容后到而导致的分行只是影响显示区域中一部分,而其他部分虽然重新计算了位置但新旧位置没有差别,因此不需要重新绘制
其实关于分行操作应当还有更优化的方法,但本人能力有限,只能提出这种方法。试验证明,在处理小的文档时程序运行速度还行,但当文档内容很多,有数万个字符时,分行速度就很慢,还望高手提供解决之道。
为了表示整个文档对象,还定义了文档对象TextDocument ,该对象在文档对象模型中是个最大的对象,我没有模仿其他文档对象的模式将其从TextElement派生过来的,而是直接定义的。该对象用于从整体上操作文档,并列出了一些操作文档的基本操作,比如删除,复制粘贴等。此外还提供一套方法来实现VBA的功能。
此外还定义了文档内容管理对象Content ,该对象隶属于TextDocument对象,用于管理所有的文档元素,它定义了属性Elements,该属性为一个保存了文档所有元素对象的列表。该对象还定义了属性SelectStart来表示插入点的位置,SelectLength 来表示选择区域的长度,为0表示没有选中任何元素,为正数则表示从插入点向后选中了若干个元素,为负数则表示从插入点向前选中了若干个元素。本对象还定义了一套处理插入点的函数,比如向左向右移动若干个元素,向上向下移动一行。大家都知道,在文本框中可以直接用光标键来移动插入点,也可以使用光标键时同时按下Shift键来移动插入点并选择文档内容,用户也可以用鼠标点击操作来移动插入点,鼠标点击的同时按下Shift键也能移动插入点选择文档内容;为此在Content对象定义了属性AutoClearSelection,当设置了该属性则移动插入点时设置SelectLength为0,若没有设置该属性则移动插入点时设置SelectLength值,使得新插入点和旧插入点之间的元素被选中,这样文本编辑器根据用户是否按下Shift键来设置AutoClearSelection属性就行了。用户修改了插入点和选择区域,则文本编辑器需要重新绘制用户界面,此时需要优化,只重新绘制选择状态发生改变的元素。可以证明,当选择的元素为连续的,则无论如何的修改选择区域和插入点,最多只有两片区域中的元素的选择状态发生改变。因此只要获得这两片区域的起始位置和长度,然后重新绘制这两个区域中的元素即可。
用户可以对文档进行很多种操作,比如移动插入点,选择元素,设置字符的字体颜色和大小,插入文字和图片,修改元素的设置,删除剪切复制粘贴等等,有好几十种操作,而且这些操作在某个时刻是不可用的,需要进行判断,若这些操作都在TextDocument中定义相应的接口函数,则TextDocument类代码太多,过于臃肿,而且每新增一种操作都需要修改TextDocument,因此在此提出动作这个概念。动作就是一个实现某种文档操作的类型,该类型有统一的接口,并使用TextDocument或其他对象提供的基本的操作来实现比较复杂的操作。为此定义动作基础类EditorAction,该类为抽象类,它的主要接口有
- HotKey 字段,动作对应的热键代码,动作对象初始化的时候设置该动作对应的热键
- KeyCode 字段,触发动作时的键盘按键编码
- ShiftKey 字段,触发动作时的Shift键状态
- ControlKey 字段,触发动作时的Control键状态
- AltKey 字段,触发动作时的Alt键状态
- MouseX,MouseY 字段,触发动作时的鼠标光标在视图区域中的坐标
- MouseButton 字段,触发动作时的鼠标按键状态
- Param1,Param2,Param3 字段,动作的参数,其意义由具体的动作决定
- TestHotKey 函数测试键盘热键,本函数由文本编辑器调用来判断是否触发某动作
- ActionName 只读属性,动作名称
- isEnable 动作是否可用
- Execute 执行动作
- OwnerDocument 动作对象所操作的文档对象
各种实际的动作对象都是从EditorAction派生的,若对象有热键则在初始化时设置HotKey字段,首先重载ActionName给定一个名称,然后重载Execute来实现各自的动作处理过程,还可根据需要重载isEnable或TestHotKey。
在TextDocument中有个属性Actions,该只读属性为包含各种动作对象的列表,当TextDocument初始化时就初始化该动作对象列表,当文本编辑器获得输入焦点时按下键盘按键则程序会遍历Actions中所有的动作,进行热键判断,若命中热键则执行该动作,其他应用程序也可根据各个动作的isEnable属性来设置文本编辑功能按钮和相应菜单的可用性。
比如定义复制动作对象EditorCopyAction,该类型从EditorAction派生的,重载ActionName使其返回"copy";重载isEnable,当文档有被选中的部分则返回True否则返回False,重载Execute来调用TextDocument中实现复制功能的函数,该对象初始化的时候设置HotKey为 System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.C,这样定义了该动作的热键为Ctl+C。
这种动作处理的模式还便于程序进行扩展,其他应用程序也可往动作列表中添加自定义的动作对象,这样文本编辑器就能自动应用该动作。应用程序还可修改各种动作的热键设置来实现用户操作的个性化。
派生对象
定义了基础对象后就开始派生对象了,首先定义字符对象类型TextChar,一个文档内容中最主要的还是字符数据,在此为了实现方便,文档中每一个字符都是一个字符对象,字符对象重载了RefreshSize对象RefreshSize方法,用于根据当前绘制用的绘图对象(System.Drawing.Graph对象)的MeasureString来计算文字大小。注意默认情况下,该方法计算的字符串显示宽度后回额外的附加一些空白,为了计算实际的大小则使用System.Drawing.StringFormat.GenericTypographic参数。此外还有一个比较特殊的字符-制表符。这个字符的宽度是不固定的,需要在进行排版的时候才计算。
字符对象(TextChar)还派生RefreshView方法,该方法比较简单,根据Left,Top值进行坐标转换后算出绘制地点,然后调用System.Drawing.Graph.DrawString方法即可。字符对象还定义了自己的成员,比如Char属性返回对象表示的字符数据,Font表示绘制对象使用的字体,ForeColor表示绘制文本的颜色。