摘要
本文阐述了使用c#从底层开发一个带格式的文本编辑器的任务,深入剖析了其中的文档对象模型的设计,图形化用户界面的处理和用户操作的响应,说明了其中的个别技术问题和解决之道。
序言
鄙人从学院里开始接触编程也有6年了,工作4年也是干编程的活,见过不少程序,自己也编过不少,在中学编程自己认为是搞艺术品,虽然玩一些游戏,例如文明法老王星际等从个别角度看也是搞艺术品,看着自己苦心经营的建筑物和人员由少变多,由简单变复杂,心里有些成就感。编程也一样,程序从几十行讲到上万行,功能由hellowword到相当复杂而强悍,心里也有不少成就感。
结业后工作,才慢慢领悟软件开发本质上是做一个工具,这个工具给他人或则自己用。有了工具,好多问题就迎刃可解了。这么开来俺们程序员和木匠铁匠石匠是同一类人了。不过没哪些,程序员原本就没高手一等,人在社会,认认真真的工作就行了。
问题
屁话不多说了,如今说说标题提出的问题,怎样用c#编撰文本编辑器。本人有幸开发过一个比较复杂的文本编辑器,因而也算有点经验吧,在此来分享一下。这儿所指的文本编辑器不是简单的像windows自带的单行或多行文本编辑框,而是类似于word的文本编辑器。
粗看上去,一个编辑器有哪些好难的,虽然很难的,由于我们觉得容易的事对计算机来说确实天大的问题。例如你们常常上网,可以发觉近来几年好多网站登陆时不仅输入用户名和密码后还要输入所谓的验证码,而验证码则在输入框门口歪歪扭扭的画了下来,如同中学一年纪的中学生在一张脏纸上写的一样,这样做只是为了避免程序来模拟登陆,由于歪歪扭扭的文字人类可以很容易的分辨,而计算机则很不容易分辨。
反例:注册hotmail使用的验证码,其显示的字符为8uv9bkyr。
一个文本编辑器主要处理的问题有
文件保存格式的定义,文档保存为文本格式还是二补码格式的,文档中各个信息单元保存哪些信息。文档格式很重要。
和文档储存系统的交流,也就是保存和加载文档的功能,这儿的文档储存系统可以是操作系统文件子系统,数据库,网路,虽然文件格式定下了,各类文档储存系统差异不大。
文档加载后的文档对象维护,面对比较复杂的文档处理,须要使用面向对象的编程思想,认真剖析文档结构,将加载的文档数据一点点分尸掉,每一个最小的不可分割的文档数据转换为一个对象,之后使用一个对象树来保存文档内容的层次关系,这样构造一个文档对象树。文档编辑工作就是维护这个文档对象树了。
文档对象的排版,文档加载后须要处理整个文档对象树,估算每位对象的显示大小,之后在视图区中排列要显示的对象,包括段落和文档行的估算,之后估算对象在视图区域中的直角座标参数。
文档的勾画,这儿的勾画包括在计算机屏幕上勾画文档内容和在复印机上勾画。程序按照估算好的对象在视图区中的座标,进行一些座标转换,在图形输出对象上勾画对象,例如勾画一个文字或图片。因为.net框架中,操作屏幕和复印机都是基于gdi+的,二者没有本质差异,因而一些处理的勾画代码可以勾画屏幕,也可以勾画复印机。在屏幕上勾画文档还非常须要优化,尽量降低闪动。
环境消息的处理,环境消息指一些windows消息,这种消息应当改变文档内容,例如键盘鼠标消息,系统粘贴板的相关消息。程序处理那些消息,更改文档对象树,向对象树插入删掉或更改文档元素对象。文档对象树发生改变后须要重新对文档进行排版,处理进行段落估算和文档行估算,重新估算对象在视图区中的位置,之后按照须要刷新屏幕显示。据悉还有用户选择文档内容时也要处理。
文档的保存,程序按照文档对象树生成一些数据,之后保存到文档储存系统,这一步可以看作对象序列化。
应用程序的开放性,提供二次开发的能力,提供类似vba的功能
一个完整的功能不弱的文本编辑器结构是很复杂的,涉及到的问题十分广泛,没有数万行的代码是搞不定的,这种问题在本文是不可能一一列下来并进行讨论,在此只得挑一些重点来聊聊。
文档对象模型
在实际开发时毋须逐个解决问题,我是首先确定文档对象树的结构,这儿使用了文档对象模型的概念,虽然我们早已遇到好多种文档对象模型,最多的莫过分html文档对象模型,我们用Javascript来控制html页面内容时就是使用html文档对象模型,再者还有xml文档对象模型,vba操作的是word或excel文档对象模型。使用文档对象模型,可将文档中所有的内容和显存中的某个对象联系上去,当应用程序更改了显存的对象的数据,则相应的文档内容就更改了。删掉了显存中的对象也就删掉了相应的文档内容。一些文档对象模型的思想可以参考。
文档对象模型中有很常见的是对象的承继和重载。你们可以瞧瞧.net解释器的system.xml名称空间下定义的xml文档对象模型,你可以发觉无论是xml文档对象(xmldocument),xml节点(xmlelement)还是属性(xmlattribute),甚至注释(xmlcomment)纯文本数据(xmltext)都是从具象类xmlnode承继过来的。这样设计的用处是可以很便捷的遍历xml文档对象树,各类对象都是从xmlnode派生的,都依据各自须要重载一些成员方式,其他程序都可把那些对象都看作xmlnode来使用,借助对象方式的重载和多态性来实现各自不同的处理。
基础对象
在这些指导思想下,我也定义了一个具象类textelement,所有的文档对象都是从该对象派生的。该类定义了以下虚成员
left,top,width,height属性,用于表示对象在的位置和显示大小
realleft,realtop只读属性,表示对象在视图区域中的显示位置
refreshsize方式,用于重新估算对象的显示大小
refreshview方式,重新勾画对象
handlemousedown方式,处理滑鼠按钮按下风波
handlemousemove方式,处理滑鼠联通风波
handlemouseup方式,处理滑鼠按钮抬起风波
fromxml方式,从一个xml节点加载对象数据
toxml方式,向一个xml节点保存对象的所有的数据
因为文档内容是分层次的,因而还定义一个容器类型textcontainer,该类型从textelement派生的,其中进行扩充来可以保存若干个子对象,它定义了以下虚成员
maxwidth属性,对象内容的最大长度,一个文档显示长度就是纸张厚度除以左右页行距的距离,文档所有的内容被限制在这个显示长度中间,该属性和显示长度有关
childelements只读属性,返回所有子对象的集合,返回类型为system.collections.arraylist
appendchild方式,该方式参数为一个textelement对象,本方式将该对象添加到子对象集合中
removechild方式,该方式参数为一个textelement对象,本方式从子对象集合中删掉指定的文档元素对象
removechildrange方式,该方式和removechild类似,只是用于删掉一批子对象
insertbefore方式,该方式参数为两个textelement对象,第一个参数为要新增的文档元素对象,第二个为插入点所在的文档元素对象
insertrangebefore方式,该方式和insertbefore类型,只是用于插入一批文档元素对象
在个别容器对象中存在一个特殊的子元素,该子元素为最后一个元素,而且不能删掉,例如对于段落对象,在此是一种容器对象,该对象最后一个元素为一个段落结尾标记对象,该对象不能删掉,而在其他类型的容器对象中也可能存在类似的结尾对象,因而在textcontainer对象中就考虑这些情况,因而定义了一套虚成员来处理
addlastelement虚方式,想容器对象添加段落结尾标记对象来作为最后一个对象,其他派生的容器对象可以重载该方式来实现自己的最后对象
islastelement函数,该函数参数为一个textelement对象,本函数返回指定的textelement对象是否是最后对象,程序在删掉子元素前都有调用该函数,若要删掉的元素为最后元素则不应该删掉
textcontainer对象还重载refreshsize方式来重新估算所有子元素的显示大小,再者还定义了新的虚方式refreshline来进行支行处理,为了便捷支行处理,还定义了文档行对象textline,文档行对象用于保存文档内容支行信息,当文档支行完毕而内容没有发生改变时重新勾画文档内容时就无需重新估算要显示的内容的座标,文档行对象的成员有
linespacing行宽度,也就是本文档行上端和下文本行下端的距离
elements属于该文档行的所有的文档元素的集合,该属性为了编程便捷
firstelement本文档行第一个元素
lastelement文档行最后一个元素
realleft,realtop文档行左上角在文档视图区域中的位置
container本文档行所在的容器对象
contentwidth本文档行所有元素的长度和
为了保存支行信息,textcontainer对象还定义了一个lines只读属性,该属性返回system.collections.arraylist对象列表,该列表元素为属于该容器的所有文本行对象,容器对象执行refreshline进行支行的步骤为
将文本行集合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。
这些动作处理的模式还以便程序进行扩充,其他应用程序也可往动作列表中添加自定义的动作对象,这样文本编辑器能够手动应用该动作。应用程序还可更改各类动作的键位设置来实现用户操作的个性化。
虽然这些动作处理的模式我是看了sharpdevelop的文本编辑器部份的源代码而感悟的,拿过来用用,实践证明还是很不错的。
我既然做的是文本编辑器其实支持复制粘贴功能了,首先将将复制操作。程序可以同时向windows剪贴板发送多种格式的数据,那些数据可以是纯文本的,也可以是图像或则自定义格式,其他程序在进行粘贴操作是可以选择其中所需格式的数据。诸如你们在vs.net的代码窗体中复制某段代码,粘贴到word和记事本中的结果是不一致的,尽管文本内容是一样的,但粘贴到word中连代码文本的颜色也显示下来的,而记事本则是纯文本数据。你们可以用剪贴板查看器clipbrd.exe来实时查看windows剪贴板中的内容。在.net中向剪贴板发送数据还是比较便捷的,首先实例化一个system.windows.forms.dataobject对象,调用它的setdata方式,该方式第一个参数为格式的名称,第二个参数为数据,可以多次调用该方式来保存不同格式的数据,之后调用静态库函数system.windows.forms.clipboard.setdataobject方式即可。在这个文本编辑器中复制数据时同时向系统剪切板保存两种数据,首先保存文档中被选中部份的纯文本数据,之后将被选中的部份转换为一个xml字符串,之后使用自定义的格式名称保存进去。这样其他程序才能使用其中的纯文本数据了。程序在进行粘贴操作时首先调用静态库函数system.windows.forms.clipboard.getdataobject方式,获得一个实现了system.windows.forms.idataobject插口的对象,之后调用它的getdatapresent方式,若发觉其中有我自定义的数据则读取该数据,之后将其中的数据当做字符串取下来,这是一个xml字符串,解析该xml字符串,并生成一系列的文档元素对象插入到文档当前位置,这些粘贴操作能将所有的文档元素及其格式给粘贴过来。若没有自定义数据并且有纯文本数据,则读取纯文本数据,并按照文本生成一系列文本元素对象,之后插入到文档当前位置。
vba
文档对象还支持vba,.net框架支持vb.net脚本语言,.net泛型中的类microsoft.visualbasic.vsa.vsaengine及插口microsoft.vsa.ivsasite就支持脚本语言。我参照html文档对象模型,在vb.net的基础上设计一种处理文档的脚本语言,该语言中直接使用脚本全局对象document就访问了文档对象textdocument,而使用document.all才能访问文档中的个别做了标记的文档元素对象,使用dbconnection能够使用文本编辑器后台使用的数据库联接对象,使用eventobj访问文档编辑器触发的风波的信息,使用vbsystem来调用个别类库。首先定义一些类型,用于实现脚本全局对象dbconnection,eventobj,vbsystem的功能,而全局对象document的类型就是textdocument,早已实现,但document.all还未实现,因此在textdocument中新增只读属性all,该属性返回一个system.object类型的对象,因为document.all的类型中定义的数组按照文档的内容而动态改变,因而须要使用.net的反射机刹车态的创建对象类型并实例化对象,其创建过程为
新增一个system.reflection.assemblyname对象,设置其name属性为"runtimetextdocumentlib"
使用appdomain.currentdomain.definedynamicassembly来创建一个程序集生成器system.reflection.emit.assemblybuilder
使用程序集生成器的definedynamicmodule来创建一个模块生成器
使用模块生成器的definetype来创建一个类型生成器,类型名称为allelements
遍历文档内容,按照名称和特定文档对象的对应关系生成一个按名称访问的哈希列表
遍历哈希列表中的名称,使用类型生成器的definefield方式创建一个公开数组,数组类型为object类型。
使用类型生成器生成一个新的类型system.type,之后动态创建一个该类型的实例,这样动态生成了allelements对象
遍历文档元素对象哈希列表,使用system.type.invokemember向该allelements对象设置数组值
这样应用程序动态的创建了allelements类型并实例化了一个对象引用,这时vb.net脚本程序就可以直接使用document.all.文档元素对象名称来直接访问文档中特定内容了。注意当文档内容发生改变时须要重新生成allelements的类型并实例化。
以上的程序模块建好后就可以搭建vb.net脚本语言运行环境了,首先定义类型textdocumentvsasite来实现ivsasite插口,实现其中的getglobalinstance函数,该函数参数为字符串,返回一个对象,该函数实际上判定若参数"document"则返回文档对象textdocument,若参数为"eventobj"则返回刚才定义了风波对象,若为"dbconnection"则返回数据库联接对象。该对象还实现了ivsasite.oncompilererror来处理脚本编译错误。
程序还从microsoft.visualbasic.vsa.vsaengine派生了脚本引擎Vbscriptengine。该模块使用vsaengine的items.createitem来向引擎添加document,eventobj,dbconnection等全局变量,还添加一些所需的.net引用,再者还实现了对脚本代码文本的一些处理,例如加密,手动添加个别必须的代码等。
脚本环境还模拟实现了文档风波的处理,例如文档中个别元素对象支持onchange风波,这种元素是有名称的,当用户更改那些元素的内容时,程序会查询脚本引擎来看是否存在名为对象名称_onchange的过程存在,若存在则执行它,这样就模拟实现了风波处理。
在vb.net脚本环境中,全局对象的成员函数可以直接调用,因而在vbsystem中定义一些类库就可以直接调用,可以在vbsystem中定义例如alert,confirm,prompt,debugprint等成员函数,脚本中能够直接使用这种函数了。
访问数据库
因为应用须要,本文本编辑器要直接访问数据库,但该文本编辑器既使用于c/s程序又使用于b/s程序,当处于b/s架构时是不好直接联接数据库的,必须通过服务器程序来访问数据库。为了编程便捷,应当抹煞掉这两种模式之间的差距。
你们考察一下.net框架中操作数据库的类型,可以发觉无论是专门操作sqlserver的在system.data.sqlclient名称空间下边的那套对象还是操作oledb的在system.data.oledb空间下边的那套对象(其他类似有专门操作odbc和oracle),这种套对象间最大的共同点就是都遵守一套在名称空间system.data下插口。这种插口包括idatareader,idbcommand,idbconnection,idbdataparameter,idataparametercolleciton等等。若我们编了一套对象也实现了这种插口,那就相当于自定义了一套.net数据库驱动程序。于是鄙人很快按照b/s架构特点写了套对象,该套对象通过http合同和web服务器交流数据,这套对象将sql句子及其参数简单打包使用post方式发送到指定的服务器页面后等待返回,服务器页面解析出sql句子和参数查询数据库,将查询所得结果经过一定的编码返回为顾客端,而顾客端按照http返回结果进行一番处理后就可以使用一个实现idatareader的对象来访问了。这样在应用程序的其他模块若查询数据库则只要坚持使用system.data.idbconnection等插口就可以了,这么就扼杀了c/s和b/s环境下访问数据库的差异了。
这些模式也算是一种webservice了,服务器页面可以使用任何类型,可以使用asp,asp.net,php,j2e或jsp等等,只要能解析出sql句子并返回特定结构的数据就行了。鄙人的服务器为j2e,偶java不熟,勉强用jsp实现了一个。我管这些模式叫两层半,实践证明这套还是好使的。
派生对象
定义了基础对象后就开始派生对象了,首先定义字符对象类型textchar,一个文档内容中最主要的还是字符数据,在此为了实现便捷,文档中每一个字符都是一个字符对象,字符对象重载了refreshsize对象refreshsize方式,用于按照当前勾画用的绘图对象(system.drawing.graph对象)的measurestring来估算文字大小。注意默认情况下,该方式估算的字符串显示长度后回额外的附加一些空白,为了估算实际的大小则使用system.drawing.stringformat.generictypographic参数。据悉还有一个比较特殊的字符-制表符。这个字符的长度是不固定的,须要在进行排版的时侯才估算。
字符对象(textchar)还派生refreshview方式,该方式比较简单,按照left,top值进行座标转换后算出勾画地点,之后调用system.drawing.graph.drawstring方式即可。字符对象还定义了自己的成员,例如char属性返回对象表示的字符数据,font表示勾画对象使用的字体,forecolor表示勾画文本的颜色。
字符中的制表符比较特殊,由于它的长度是不定的,而是按照它在文档视图中的位置而定的,因而在textchar上在派生textchartab来转变处理这些情况,它新增了refreshtabwidth方式,来按照对象在视图区域中的上端位置估算字符间距。在此处我认定一个制表符步长等于四个下画线字符的长度,制表符的右端座标必须是制表符步长的自然数倍,因而依据制表符的位置来进行取模操作和其他操作就可以估算制表符的长度。
为了表示段落而定义了段落对象textparagraph,该对象不是容器对象,保存了段落对齐方法的信息,该元素的显示款式类似于word中的段落符(硬回车)的款式。
还定义了行结束对象textlineend,该对象模拟了word的支行符(软回车)。
可以定义图片对象,经过对word处理文档的行为观察,可以发觉在word文档中插入的图片和ole对象特点很相像,因而为了考虑文本编辑器的可扩充性,首先在textelement的基础派生出textobject具象类,该具象类表示一个在文档中的对象,该对象由其派生的类决定。
在textobject对象派生出textimage表示一个图片对象,该对象重画了refreshview方式,用于在绘图输出对象上勾画一个图片。还重载了fromxml和toxml方式来和xml节点交换数据,可以设计将图片二补码数据以base64格式保存为xml节点下。
据悉还可以依照应用的须要从textobject对象上派生其他的类型,例如直接读取数据库在界面上勾画曲线图等等,此时文档中的该对象可以动态的展示系统中最新的数据。
图形化用户界面
可以观察到word中的对象(包括图片)可以改变大小,当用滑鼠点击图片对象时,图片四个角和四个边的中点上会显示8个小点。这种小点我称为控制点。用键盘拖放这8个点可以动态的改变对象的大小。虽然在好多类型的程序中可以见到这8控制点,比如在vs.net的窗体设计器中,当前的控制周围就有这8个控制点。关于怎样实现这8个控制点也是有一套的。
控制点可以分为内控制点和外控制点两种类型,我们对这8个点进行从0到7的编号。当滑鼠光标联通到这8个控制点上方时须要设置为不同的光标式样。
内控制点
┌─────────────────┐
│■01■2■│
││
││
││
││
│■73■│
││
││
││
││
│■65■4■│
└─────────────────┘
外控制点
■■■
┌────────────────┐
│012│
││
││
││
││
■│73│■
││
││
││
││
│654│
└────────────────┘
■■■
控制点上键盘光标如下
东北-西南sizenwse南北sizens西北-东南sizenesw
■■■
┌────────────────┐
│012│
││
││
││
││
■│7西-南sizewe3│■西-南sizewe
││
││
││
││
│654│
└────────────────┘
■■■
西北-东南sizenesw南北sizens东北-西南sizenwse
按照上图所示,已知主矩形,控制点的类型(是内控制点还是外控制点)和控制点的长度可以估算出所有的控制点的位置。可以编一个类库,输入3个参数,主矩形区域的rectangle结构体,是否是内控制点(不是内控制点就是外控制点)和控制点的长度,该解释器估算所有控制点的位置,之后返回一个包含8个rectangle的字段,该字段就是0到7号的控制方形的位置和大小。
textobject对象显示后就应当晓得自己在视图区域中的位置,当它相应键盘联通消息时,就可以按照键盘光标位置和8个控制圆形进行比较,若键盘光标在某个控制圆形中时就要通知文本编辑器改变键盘光标的款式。
通常的控制点被画成一个圆形方框,控制点也被画成两种类型,一种是填充色为黄色(金色或黄色)和黑色边框,另一种是灰色边框并填充黑色。可以观察vs.net窗体设计器,可以在设计器中选择多个控制,其中有一个控件的控制点为填充色为白色和红色边框的,该控制为当前控件。而其他选择的控件的控制点为红色边框并填充黑色,这种控件为选择控件。在文本编辑器中没有这些情况,因而在此可以使用内控制点方法,控制点用红色填充,边框黑色。
当滑鼠在控制点上进行拖放操作就应该可以动态的更改对象的大小,先前我是这么实现的
在键盘按钮按下风波处理(handlemousedown)中,若滑鼠光标在某个控制点上则设置一个键盘按钮按下标记变量,并记下键盘光标位置,然退后出事件处理
在键盘联通风波中(handlemousemove),若设置了键盘按钮按下标记变量,则按照当前键盘光标位置和上一次键盘光标的位置之差就是键盘光标联通的距离,该距离的水平分量和垂直份量就是对象长度和高度的改变量,此时可以使用库函数system.windows.forms.controlpaint.drawreversibleframe在界面上勾画一个实线框,当滑鼠联通时不断的调用该库函数,这样实现了所谓的“橡皮筋”操作
在键盘按钮抬起风波(handlemousedown)处理中,依照键盘光标的当前位置和先前记下的键盘按钮按下时的键盘光标位置估算二者之差,这样就是整个键盘拖放操作中键盘光标联通的距离,程序就可以根据该距离来改变对象的大小
经过一些编程实践,发觉该操作比较麻烦,须要编撰不少代码,并且代码分散在3个风波处理过程中,多了一些全局变量,很难写出一个通用解释器四处调用,经过剖析,将这些处理模式改掉了。虽然通常的程序正在进行键盘拖放操作时,用户是不可能同时进行其他操作(不如边键盘拖放边打字),并且进行”橡皮筋“操作时程序用户界面无需重新勾画,这样可以觉得进行键盘拖放时应用程序应用程序只处理滑鼠联通消息和键盘抬起消息而不进行任何其他操作,为了编程简单,甚至连重画界面的操作也不处理了,因而可以编一个通用解释器来处理整个的键盘拖放来实现“橡皮筋”操作,该函数处理过程为
在键盘按钮按下风波处理(handlemousedown)中就调用该解释器