c#如何写记事本.docx
- 文档编号:23643121
- 上传时间:2023-05-19
- 格式:DOCX
- 页数:19
- 大小:32.01KB
c#如何写记事本.docx
《c#如何写记事本.docx》由会员分享,可在线阅读,更多相关《c#如何写记事本.docx(19页珍藏版)》请在冰豆网上搜索。
c#如何写记事本
摘要
本文探讨了使用C#从底层开发一个带格式的文本编辑器的任务,深入探讨了其中的文档对象模型的设计,图形化用户界面的处理和用户操作的响应,说明了其中的某些技术问题和解决之道。
前言
小弟从大学里开始接触编程也有6年了,工作4年也是干编程的活,见过不少程序,自己也编过不少,在学校编程自己觉得是搞艺术品,其实玩一些游戏,比如文明法老王星际等从某些角度看也是搞艺术品,看着自己苦心经营的建筑物和人员由少变多,由简单变复杂,心中有些成就感。
编程也一样,程序从几十行写到上万行,功能由HellowWord到相当复杂而强大,心中也有不少成就感。
毕业后工作,才渐渐感悟软件开发本质上是做一个工具,这个工具给别人或者自己用。
有了工具,很多问题就迎刃可解了。
如此开来偶们程序员和石匠铁匠木匠是同一类人了。
不过没什么,程序员本来就没高人一等,人在社会,认认真真的工作就行了。
问题
废话不多说了,现在谈谈标题提出的问题,如何用C#编写文本编辑器。
本人有幸开发过一个比较复杂的文本编辑器,因此也算有点经验吧,在此来分享一下。
这里所指的文本编辑器不是简单的像Windows自带的单行或多行文本编辑框,而是类似于Word的文本编辑器。
粗看起来,一个编辑器有什么好难的,其实很难的,因为我们认为容易的事对计算机来说确实天大的问题。
比如大家经常上网,可以发现最近几年很多网站登录时除了输入用户名和密码后还要输入所谓的验证码,而验证码则在输入框旁边歪歪扭扭的画了出来,就像小学一年纪的学生在一张脏纸上写的一样,这样做只是为了防止程序来模拟登录,因为歪歪扭扭的文字人类可以很容易的辨认,而计算机则很不容易辨认。
例子:
注册hotmail使用的验证码
其显示的字符为 8UV9BKYR 。
一个文本编辑器主要处理的问题有
∙文件保存格式的定义,文档保存为文本格式还是二进制格式的,文档中各个信息单元保存什么信息。
文档格式很重要。
∙和文档存储系统的交流,也就是保存和加载文档的功能,这里的文档存储系统可以是操作系统文件子系统,数据库,网络,其实文件格式定下了,各种文档存储系统差别不大。
∙文档加载后的文档对象维护,面对比较复杂的文档处理,需要使用面向对象的编程思想,认真分析文档结构,将加载的文档数据一点点肢解掉,每一个最小的不可分割的文档数据转换为一个对象,然后使用一个对象树来保存文档内容的层次关系,这样构造一个文档对象树。
文档编辑工作就是维护这个文档对象树了。
∙文档对象的排版,文档加载后需要处理整个文档对象树,计算每个对象的显示大小,然后在视图区中排列要显示的对象,包括段落和文档行的计算,然后计算对象在视图区域中的直角坐标参数。
∙文档的绘制,这里的绘制包括在计算机屏幕上绘制文档内容和在打印机上绘制。
程序根据计算好的对象在视图区中的坐标,进行一些坐标转换,在图形输出对象上绘制对象,比如绘制一个文字或图片。
由于.NET框架中,操作屏幕和打印机都是基于GDI+的,两者没有本质差别,因此一些处理的绘制代码可以绘制屏幕,也可以绘制打印机。
在屏幕上绘制文档还特别需要优化,尽量减少闪烁。
∙环境消息的处理,环境消息指一些Windows消息,这些消息应该改变文档内容,比如鼠标键盘消息,系统粘贴板的相关消息。
程序处理这些消息,修改文档对象树,向对象树插入删除或修改文档元素对象。
文档对象树发生改变后需要重新对文档进行排版,处理进行段落计算和文档行计算,重新计算对象在视图区中的位置,然后根据需要刷新屏幕显示。
此外还有用户选择文档内容时也要处理。
∙文档的保存,程序根据文档对象树生成一些数据,然后保存到文档存储系统,这一步可以看作对象序列化。
∙应用程序的开放性,提供二次开发的能力,提供类似VBA的功能
一个完整的功能不弱的文本编辑器结构是很复杂的,涉及到的问题非常广泛,没有数万行的代码是搞不定的,这些问题在本文是不可能一一列出来并进行讨论,在此只好挑一些重点来说说。
文档对象模型
在实际开发时不必挨个解决问题,我是首先确定文档对象树的结构,这里使用了文档对象模型的概念,其实我们已经碰到很多种文档对象模型,最多的莫过于HTML文档对象模型,我们用JavaScript来控制HTML页面内容时就是使用HTML文档对象模型,此外还有XML文档对象模型,VBA操作的是Word或Excel文档对象模型。
使用文档对象模型,可将文档中所有的内容和内存中的某个对象联系起来,当应用程序修改了内存的对象的数据,则相应的文档内容就修改了。
删除了内存中的对象也就删除了相应的文档内容。
一些文档对象模型的思想可以参考http:
//www.w3.org。
文档对象模型中有很常见的是对象的继承和重载。
大家可以看看.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的类型并实例化。
以上的程序模块建好后就可以搭
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- c# 如何 记事本