小男孩‘自慰网亚洲一区二区,亚洲一级在线播放毛片,亚洲中文字幕av每天更新,黄aⅴ永久免费无码,91成人午夜在线精品,色网站免费在线观看,亚洲欧洲wwwww在线观看

分享

如何用C#編寫文本編輯器

 king9413 2007-06-30

如何用C#編寫文本編輯器【2005-8-24版】

       南京千里獨(dú)行2005版權(quán)所有,不限轉(zhuǎn)載,請保留版權(quán)聲明 作者Blog:http://blog.csdn.net/yyf9989/

摘要

   本文探討了使用C#從底層開發(fā)一個(gè)帶格式的文本編輯器的任務(wù),深入探討了其中的文檔對象模型的設(shè)計(jì),圖形化用戶界面的處理和用戶操作的響應(yīng),說明了其中的某些技術(shù)問題和解決之道。

前言

    小弟從大學(xué)里開始接觸編程也有6年了,工作4年也是干編程的活,見過不少程序,自己也編過不少,在學(xué)校編程自己覺得是搞藝術(shù)品,其實(shí)玩一些游戲,比如文明法老王星際等從某些角度看也是搞藝術(shù)品,看著自己苦心經(jīng)營的建筑物和人員由少變多,由簡單變復(fù)雜,心中有些成就感。編程也一樣,程序從幾十行寫到上萬行,功能由HellowWord到相當(dāng)復(fù)雜而強(qiáng)大,心中也有不少成就感。

    畢業(yè)后工作,才漸漸感悟軟件開發(fā)本質(zhì)上是做一個(gè)工具,這個(gè)工具給別人或者自己用。有了工具,很多問題就迎刃可解了。如此開來偶們程序員和石匠鐵匠木匠是同一類人了。不過沒什么,程序員本來就沒高人一等,人在社會,認(rèn)認(rèn)真真的工作就行了。

問題

    廢話不多說了,現(xiàn)在談?wù)剺?biāo)題提出的問題,如何用C#編寫文本編輯器。本人有幸開發(fā)過一個(gè)比較復(fù)雜的文本編輯器,因此也算有點(diǎn)經(jīng)驗(yàn)吧,在此來分享一下。這里所指的文本編輯器不是簡單的像Windows自帶的單行或多行文本編輯框,而是類似于Word的文本編輯器。

    粗看起來,一個(gè)編輯器有什么好難的,其實(shí)很難的,因?yàn)槲覀冋J(rèn)為容易的事對計(jì)算機(jī)來說確實(shí)天大的問題。比如大家經(jīng)常上網(wǎng),可以發(fā)現(xiàn)最近幾年很多網(wǎng)站登錄時(shí)除了輸入用戶名和密碼后還要輸入所謂的驗(yàn)證碼,而驗(yàn)證碼則在輸入框旁邊歪歪扭扭的畫了出來,就像小學(xué)一年紀(jì)的學(xué)生在一張臟紙上寫的一樣,這樣做只是為了防止程序來模擬登錄,因?yàn)橥嵬崤づさ奈淖秩祟惪梢院苋菀椎谋嬲J(rèn),而計(jì)算機(jī)則很不容易辨認(rèn)。

    一個(gè)文本編輯器主要處理的問題有

  • 文件保存格式的定義,文檔保存為文本格式還是二進(jìn)制格式的,文檔中各個(gè)信息單元保存什么信息。文檔格式很重要。
  • 和文檔存儲系統(tǒng)的交流,也就是保存和加載文檔的功能,這里的文檔存儲系統(tǒng)可以是操作系統(tǒng)文件子系統(tǒng),數(shù)據(jù)庫,網(wǎng)絡(luò),其實(shí)文件格式定下了,各種文檔存儲系統(tǒng)差別不大。
  • 文檔加載后的文檔對象維護(hù),面對比較復(fù)雜的文檔處理,需要使用面向?qū)ο蟮木幊趟枷?,認(rèn)真分析文檔結(jié)構(gòu),將加載的文檔數(shù)據(jù)一點(diǎn)點(diǎn)肢解掉,每一個(gè)最小的不可分割的文檔數(shù)據(jù)轉(zhuǎn)換為一個(gè)對象,然后使用一個(gè)對象樹來保存文檔內(nèi)容的層次關(guān)系,這樣構(gòu)造一個(gè)文檔對象樹。文檔編輯工作就是維護(hù)這個(gè)文檔對象樹了。
  • 文檔對象的排版,文檔加載后需要處理整個(gè)文檔對象樹,計(jì)算每個(gè)對象的顯示大小,然后在視圖區(qū)中排列要顯示的對象,包括段落和文檔行的計(jì)算,然后計(jì)算對象在視圖區(qū)域中的直角坐標(biāo)參數(shù)。
  • 文檔的繪制,這里的繪制包括在計(jì)算機(jī)屏幕上繪制文檔內(nèi)容和在打印機(jī)上繪制。程序根據(jù)計(jì)算好的對象在視圖區(qū)中的坐標(biāo),進(jìn)行一些坐標(biāo)轉(zhuǎn)換,在圖形輸出對象上繪制對象,比如繪制一個(gè)文字或圖片。由于.NET框架中,操作屏幕和打印機(jī)都是基于GDI+的,兩者沒有本質(zhì)差別,因此一些處理的繪制代碼可以繪制屏幕,也可以繪制打印機(jī)。在屏幕上繪制文檔還特別需要優(yōu)化,盡量減少閃爍。
  • 環(huán)境消息的處理,環(huán)境消息指一些Windows消息,這些消息應(yīng)該改變文檔內(nèi)容,比如鼠標(biāo)鍵盤消息,系統(tǒng)粘貼板的相關(guān)消息。程序處理這些消息,修改文檔對象樹,向?qū)ο髽洳迦雱h除或修改文檔元素對象。文檔對象樹發(fā)生改變后需要重新對文檔進(jìn)行排版,處理進(jìn)行段落計(jì)算和文檔行計(jì)算,重新計(jì)算對象在視圖區(qū)中的位置,然后根據(jù)需要刷新屏幕顯示。此外還有用戶選擇文檔內(nèi)容時(shí)也要處理。
  • 文檔的保存,程序根據(jù)文檔對象樹生成一些數(shù)據(jù),然后保存到文檔存儲系統(tǒng),這一步可以看作對象序列化。
  • 應(yīng)用程序的開放性,提供二次開發(fā)的能力,提供類似VBA的功能

    一個(gè)完整的功能不弱的文本編輯器結(jié)構(gòu)是很復(fù)雜的,涉及到的問題非常廣泛,沒有數(shù)萬行的代碼是搞不定的,這些問題在本文是不可能一一列出來并進(jìn)行討論,在此只好挑一些重點(diǎn)來說說。

文檔對象模型

    在實(shí)際開發(fā)時(shí)不必挨個(gè)解決問題,我是首先確定文檔對象樹的結(jié)構(gòu),這里使用了文檔對象模型的概念,其實(shí)我們已經(jīng)碰到很多種文檔對象模型,最多的莫過于HTML文檔對象模型,我們用JavaScript來控制HTML頁面內(nèi)容時(shí)就是使用HTML文檔對象模型,此外還有XML文檔對象模型,VBA操作的是Word或Excel文檔對象模型。使用文檔對象模型,可將文檔中所有的內(nèi)容和內(nèi)存中的某個(gè)對象聯(lián)系起來,當(dāng)應(yīng)用程序修改了內(nèi)存的對象的數(shù)據(jù),則相應(yīng)的文檔內(nèi)容就修改了。刪除了內(nèi)存中的對象也就刪除了相應(yīng)的文檔內(nèi)容。一些文檔對象模型的思想可以參考http://www.

    文檔對象模型中有很常見的是對象的繼承和重載。大家可以看看.NET類庫的System.XML名稱空間下定義的XML文檔對象模型,你可以發(fā)現(xiàn)無論是XML文檔對象(XMLDocument),XML節(jié)點(diǎn)(XMLElement)還是屬性(XMLAttribute),甚至注釋(XMLComment)純文本數(shù)據(jù)(XMLText)都是從抽象類XMLNode繼承過來的。這樣設(shè)計(jì)的好處是可以很方便的遍歷XML文檔對象樹,各種對象都是從XMLNode派生的,都根據(jù)各自需要重載一些成員方法,其他程序都可把這些對象都看作XMLNode來使用,利用對象方法的重載和多態(tài)性來實(shí)現(xiàn)各自不同的處理。

基礎(chǔ)對象

    在這種指導(dǎo)思想下,我也定義了一個(gè)抽象類TextElement,所有的文檔對象都是從該對象派生的。該類定義了以下虛成員

  • Left,Top,Width,Height屬性,用于表示對象在的位置和顯示大小
  • RealLeft , RealTop  只讀屬性,表示對象在視圖區(qū)域中的顯示位置
  • RefreshSize 方法,用于重新計(jì)算對象的顯示大小
  • RefreshView 方法,重新繪制對象
  • HandleMouseDown 方法,處理鼠標(biāo)按鍵按下事件
  • HandleMouseMove 方法,處理鼠標(biāo)移動事件
  • HandleMouseUp 方法,處理鼠標(biāo)按鍵松開事件
  • FromXML 方法,從一個(gè)XML節(jié)點(diǎn)加載對象數(shù)據(jù)
  • ToXML 方法,向一個(gè)XML節(jié)點(diǎn)保存對象的所有的數(shù)據(jù)

  由于文檔內(nèi)容是分層次的,因此還定義一個(gè)容器類型TextContainer,該類型從TextElement派生的,其中進(jìn)行擴(kuò)展來可以保存若干個(gè)子對象,它定義了以下虛成員

  • MaxWidth 屬性,對象內(nèi)容的最大寬度,一個(gè)文檔顯示寬度就是紙張寬度減去左右頁邊距的距離,文檔所有的內(nèi)容被限制在這個(gè)顯示寬度中間,該屬性和顯示寬度有關(guān)
  • ChildElements 只讀屬性,返回所有子對象的集合,返回類型為System.Collections.ArrayList
  • AppendChild 方法,該方法參數(shù)為一個(gè)TextElement對象,本方法將該對象添加到子對象集合中
  • RemoveChild 方法,該方法參數(shù)為一個(gè)TextElement對象,本方法從子對象集合中刪除指定的文檔元素對象
  • RemoveChildRange 方法,該方法和RemoveChild類似,只是用于刪除一批子對象
  • InsertBefore 方法,該方法參數(shù)為兩個(gè)TextElement對象,第一個(gè)參數(shù)為要新增的文檔元素對象,第二個(gè)為插入點(diǎn)所在的文檔元素對象
  • InsertRangeBefore 方法,該方法和InsertBefore類型,只是用于插入一批文檔元素對象

   在某些容器對象中存在一個(gè)特殊的子元素,該子元素為最后一個(gè)元素,并且不能刪除,比如對于段落對象,在此是一種容器對象,該對象最后一個(gè)元素為一個(gè)段落結(jié)尾標(biāo)記對象,該對象不能刪除,而在其他類型的容器對象中也可能存在類似的結(jié)尾對象,因此在TextContainer對象中就考慮這種情況,因此定義了一套虛成員來處理

  • AddLastElement 虛方法,想容器對象添加段落結(jié)尾標(biāo)記對象來作為最后一個(gè)對象,其他派生的容器對象可以重載該方法來實(shí)現(xiàn)自己的最后對象
  • IsLastElement 函數(shù),該函數(shù)參數(shù)為一個(gè)TextElement對象,本函數(shù)返回指定的TextElement對象是否是最后對象,程序在刪除子元素前都有調(diào)用該函數(shù),若要刪除的元素為最后元素則不應(yīng)當(dāng)刪除

  TextContainer對象還重載RefreshSize方法來重新計(jì)算所有子元素的顯示大小,此外還定義了新的虛方法RefreshLine來進(jìn)行分行處理,為了方便分行處理,還定義了文檔行對象TextLine,文檔行對象用于保存文檔內(nèi)容分行信息,當(dāng)文檔分行完畢而內(nèi)容沒有發(fā)生改變時(shí)重新繪制文檔內(nèi)容時(shí)就無需重新計(jì)算要顯示的內(nèi)容的坐標(biāo),文檔行對象的成員有

  • LineSpacing 行間距,也就是本文檔行下端和下文本行上端的距離
  • Elements 屬于該文檔行的所有的文檔元素的集合,該屬性為了編程方便
  • FirstElement 本文檔行第一個(gè)元素
  • LastElement 文檔行最后一個(gè)元素
  • RealLeft , RealTop 文檔行左上角在文檔視圖區(qū)域中的位置
  • Container 本文檔行所在的容器對象
  • ContentWidth 本文檔行所有元素的寬度和

   為了保存分行信息,TextContainer對象還定義了一個(gè)Lines只讀屬性,該屬性返回System.Collections.ArrayList對象列表,該列表元素為屬于該容器的所有文本行對象,容器對象執(zhí)行RefreshLine進(jìn)行分行的步驟為

  • 將文本行集合Lines清空
  • 設(shè)置所有參與分行的元素集合
  • 從前到后的遍歷所有的參與分行的元素集合中的所有子元素
  • 若子元素對象為制表符或水平線對象則重新計(jì)算它的寬度
  • 若子元素為一個(gè)容器對象則調(diào)用它的RefreshLine方法
  • 向當(dāng)前行的元素列表中添加元素,并累計(jì)元素的寬度和,若寬度和大于容器顯示寬度(我們稱為情況1)或者當(dāng)前元素單獨(dú)占據(jù)一行則取消向當(dāng)前行添加元素并結(jié)束當(dāng)前行
  • 若當(dāng)前元素是強(qiáng)制換行的則結(jié)束當(dāng)前行
  • 在結(jié)束當(dāng)前行前,若當(dāng)前元素不能出現(xiàn)在行尾或者下一個(gè)元素不能出現(xiàn)在行首則取消向當(dāng)前行添加當(dāng)前元素(這也算情況1)。按照書寫慣例,某些字符例如!),.:;?]}¨·ˇˉ―‖’”…∶、?!ā怠贰埂弧俊场剑。ⅲВ?,.:;?]`|}~¢是不能顯示在行首,而另外一些字符例如([{·‘“〈《「『【〔〖(.[{£¥是不能顯示在行尾,此外在某些特定的應(yīng)用中可能還有其他類型的元素也出現(xiàn)這種情況,這些情況需要考慮。為此在基礎(chǔ)元素對象類型TextElement中定義了方法 CanBeLineHead 來判斷元素對象是否可以出現(xiàn)在行首,定義了方法CanBeLineEnd來判斷元素對象是否可以出現(xiàn)在行尾,這樣字符元素對象和其他元素對象可以重載這兩個(gè)方法來進(jìn)行所需的判斷。在進(jìn)行這樣的判斷要特別的小心,若容器顯示寬度比較小則有可能由于這種判斷而導(dǎo)致死循環(huán),因此還需要額外的進(jìn)行反死循環(huán)的判斷(當(dāng)年為了發(fā)現(xiàn)這個(gè)錯(cuò)誤而嘔出了幾十兩血)。
  • 在結(jié)束當(dāng)前行時(shí)需要計(jì)算文檔元素在當(dāng)前行中的相對位置,若當(dāng)前行是由于情況1而導(dǎo)致結(jié)束的則需要修正元素間距,由于文檔行所有元素的寬度和不一定等于容器的顯示寬度,因此若沒有進(jìn)行修正則文檔的右邊緣參差不齊,影響美觀,因此需要計(jì)算元素寬度和和容器的顯示寬度之差,將該寬度差比較均勻的插入到各個(gè)文檔元素之間,這樣文檔的右邊緣則比較整齊。為了保存這個(gè)修正值,在TextElement中新增一個(gè)WidthFix屬性來保存該值。其實(shí)大家可以觀察到IE顯示文檔內(nèi)容時(shí)沒有進(jìn)行右邊緣的修正而Word則進(jìn)行了類似的修正
  • 若當(dāng)前行是由于最后一個(gè)元素強(qiáng)制分行而結(jié)束的則無需進(jìn)行由于情況1而導(dǎo)致的右邊緣修正,但計(jì)算文檔元素位置時(shí)需要進(jìn)行文檔對齊方式的修正。首先找到影響當(dāng)前文本行的段落對象,獲得它的對齊方式設(shè)置(左對齊,右對齊,居中對齊),根據(jù)對齊方式來計(jì)算元素見的空白,然后設(shè)置元素的WidthFix屬性
  • 此外還需要修正元素在文檔行中的頂端坐標(biāo),由于同一行的文檔元素高度不一定一致,此時(shí)需要遍歷所有的元素,以最高的元素的高度為文檔行的高度,以此計(jì)算元素在文檔行中的頂端位置,以保證各個(gè)元素的低邊緣在同一水平線上
  • 結(jié)束完畢的行對象添加到容器的Lines文檔行集合中,然后創(chuàng)建創(chuàng)建一個(gè)文檔行對象作為當(dāng)前行,如此循環(huán)直到處理了容器對象所有的內(nèi)容
  • 產(chǎn)生了所有的文檔行對象后根據(jù)容器對象的在視圖區(qū)域中的坐標(biāo)和文檔行的行間距設(shè)置來計(jì)算文檔行在視圖區(qū)域中的坐標(biāo),這樣文檔行中所有的元素的在視圖區(qū)域中的坐標(biāo)就是文檔行的坐標(biāo)和元素在文檔行中的相對坐標(biāo)的和
  • 在修改文檔行中元素的位置時(shí),需要獲得元素舊的在視圖區(qū)域中的最小外切矩形數(shù)據(jù),然后和重新計(jì)算過的最小外切矩形進(jìn)行比較,若兩者不一樣則表示元素在視圖區(qū)域中顯示的位置發(fā)生改變,將這兩個(gè)矩形添加到文本編輯器重繪矩形集合中,當(dāng)文檔重新分行完畢后,文本編輯器就將所有的重繪矩形進(jìn)行加法操作,獲得的矩形就是需要重新繪制的區(qū)域。如此這樣是為了優(yōu)化顯示操作,減少頁面閃爍;因?yàn)橛脩粜薷牧宋臋n內(nèi)容后到而導(dǎo)致的分行只是影響顯示區(qū)域中一部分,而其他部分雖然重新計(jì)算了位置但新舊位置沒有差別,因此不需要重新繪制

    其實(shí)關(guān)于分行操作應(yīng)當(dāng)還有更優(yōu)化的方法,但本人能力有限,只能提出這種方法。試驗(yàn)證明,在處理小的文檔時(shí)程序運(yùn)行速度還行,但當(dāng)文檔內(nèi)容很多,有數(shù)萬個(gè)字符時(shí),分行速度就很慢,還望高手提供解決之道。

    為了表示整個(gè)文檔對象,還定義了文檔對象TextDocument ,該對象在文檔對象模型中是個(gè)最大的對象,我沒有模仿其他文檔對象的模式將其從TextElement派生過來的,而是直接定義的。該對象用于從整體上操作文檔,并列出了一些操作文檔的基本操作,比如刪除,復(fù)制粘貼等。此外還提供一套方法來實(shí)現(xiàn)VBA的功能。

    此外還定義了文檔內(nèi)容管理對象Content ,該對象隸屬于TextDocument對象,用于管理所有的文檔元素,它定義了屬性Elements,該屬性為一個(gè)保存了文檔所有元素對象的列表。該對象還定義了屬性SelectStart來表示插入點(diǎn)的位置,SelectLength 來表示選擇區(qū)域的長度,為0表示沒有選中任何元素,為正數(shù)則表示從插入點(diǎn)向后選中了若干個(gè)元素,為負(fù)數(shù)則表示從插入點(diǎn)向前選中了若干個(gè)元素。本對象還定義了一套處理插入點(diǎn)的函數(shù),比如向左向右移動若干個(gè)元素,向上向下移動一行。大家都知道,在文本框中可以直接用光標(biāo)鍵來移動插入點(diǎn),也可以使用光標(biāo)鍵時(shí)同時(shí)按下Shift鍵來移動插入點(diǎn)并選擇文檔內(nèi)容,用戶也可以用鼠標(biāo)點(diǎn)擊操作來移動插入點(diǎn),鼠標(biāo)點(diǎn)擊的同時(shí)按下Shift鍵也能移動插入點(diǎn)選擇文檔內(nèi)容;為此在Content對象定義了屬性AutoClearSelection,當(dāng)設(shè)置了該屬性則移動插入點(diǎn)時(shí)設(shè)置SelectLength為0,若沒有設(shè)置該屬性則移動插入點(diǎn)時(shí)設(shè)置SelectLength值,使得新插入點(diǎn)和舊插入點(diǎn)之間的元素被選中,這樣文本編輯器根據(jù)用戶是否按下Shift鍵來設(shè)置AutoClearSelection屬性就行了。用戶修改了插入點(diǎn)和選擇區(qū)域,則文本編輯器需要重新繪制用戶界面,此時(shí)需要優(yōu)化,只重新繪制選擇狀態(tài)發(fā)生改變的元素。可以證明,當(dāng)選擇的元素為連續(xù)的,則無論如何的修改選擇區(qū)域和插入點(diǎn),最多只有兩片區(qū)域中的元素的選擇狀態(tài)發(fā)生改變。因此只要獲得這兩片區(qū)域的起始位置和長度,然后重新繪制這兩個(gè)區(qū)域中的元素即可。

    用戶可以對文檔進(jìn)行很多種操作,比如移動插入點(diǎn),選擇元素,設(shè)置字符的字體顏色和大小,插入文字和圖片,修改元素的設(shè)置,刪除剪切復(fù)制粘貼等等,有好幾十種操作,而且這些操作在某個(gè)時(shí)刻是不可用的,需要進(jìn)行判斷,若這些操作都在TextDocument中定義相應(yīng)的接口函數(shù),則TextDocument類代碼太多,過于臃腫,而且每新增一種操作都需要修改TextDocument,因此在此提出動作這個(gè)概念。動作就是一個(gè)實(shí)現(xiàn)某種文檔操作的類型,該類型有統(tǒng)一的接口,并使用TextDocument或其他對象提供的基本的操作來實(shí)現(xiàn)比較復(fù)雜的操作。為此定義動作基礎(chǔ)類EditorAction,該類為抽象類,它的主要接口有

  • HotKey 字段,動作對應(yīng)的熱鍵代碼,動作對象初始化的時(shí)候設(shè)置該動作對應(yīng)的熱鍵
  • KeyCode 字段,觸發(fā)動作時(shí)的鍵盤按鍵編碼
  • ShiftKey 字段,觸發(fā)動作時(shí)的Shift鍵狀態(tài)
  • ControlKey 字段,觸發(fā)動作時(shí)的Control鍵狀態(tài)
  • AltKey 字段,觸發(fā)動作時(shí)的Alt鍵狀態(tài)
  • MouseX,MouseY 字段,觸發(fā)動作時(shí)的鼠標(biāo)光標(biāo)在視圖區(qū)域中的坐標(biāo)
  • MouseButton 字段,觸發(fā)動作時(shí)的鼠標(biāo)按鍵狀態(tài)
  • Param1,Param2,Param3 字段,動作的參數(shù),其意義由具體的動作決定
  • TestHotKey 函數(shù)測試鍵盤熱鍵,本函數(shù)由文本編輯器調(diào)用來判斷是否觸發(fā)某動作
  • ActionName 只讀屬性,動作名稱
  • isEnable 動作是否可用
  • Execute 執(zhí)行動作
  • OwnerDocument 動作對象所操作的文檔對象

   各種實(shí)際的動作對象都是從EditorAction派生的,若對象有熱鍵則在初始化時(shí)設(shè)置HotKey字段,首先重載ActionName給定一個(gè)名稱,然后重載Execute來實(shí)現(xiàn)各自的動作處理過程,還可根據(jù)需要重載isEnable或TestHotKey。

   在TextDocument中有個(gè)屬性Actions,該只讀屬性為包含各種動作對象的列表,當(dāng)TextDocument初始化時(shí)就初始化該動作對象列表,當(dāng)文本編輯器獲得輸入焦點(diǎn)時(shí)按下鍵盤按鍵則程序會遍歷Actions中所有的動作,進(jìn)行熱鍵判斷,若命中熱鍵則執(zhí)行該動作,其他應(yīng)用程序也可根據(jù)各個(gè)動作的isEnable屬性來設(shè)置文本編輯功能按鈕和相應(yīng)菜單的可用性。

   比如定義復(fù)制動作對象EditorCopyAction,該類型從EditorAction派生的,重載ActionName使其返回"copy";重載isEnable,當(dāng)文檔有被選中的部分則返回True否則返回False,重載Execute來調(diào)用TextDocument中實(shí)現(xiàn)復(fù)制功能的函數(shù),該對象初始化的時(shí)候設(shè)置HotKey為 System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.C,這樣定義了該動作的熱鍵為Ctl+C。

   這種動作處理的模式還便于程序進(jìn)行擴(kuò)展,其他應(yīng)用程序也可往動作列表中添加自定義的動作對象,這樣文本編輯器就能自動應(yīng)用該動作。應(yīng)用程序還可修改各種動作的熱鍵設(shè)置來實(shí)現(xiàn)用戶操作的個(gè)性化。

派生對象

      定義了基礎(chǔ)對象后就開始派生對象了,首先定義字符對象類型TextChar,一個(gè)文檔內(nèi)容中最主要的還是字符數(shù)據(jù),在此為了實(shí)現(xiàn)方便,文檔中每一個(gè)字符都是一個(gè)字符對象,字符對象重載了RefreshSize對象RefreshSize方法,用于根據(jù)當(dāng)前繪制用的繪圖對象(System.Drawing.Graph對象)的MeasureString來計(jì)算文字大小。注意默認(rèn)情況下,該方法計(jì)算的字符串顯示寬度后回額外的附加一些空白,為了計(jì)算實(shí)際的大小則使用System.Drawing.StringFormat.GenericTypographic參數(shù)。此外還有一個(gè)比較特殊的字符-制表符。這個(gè)字符的寬度是不固定的,需要在進(jìn)行排版的時(shí)候才計(jì)算。

     字符對象(TextChar)還派生RefreshView方法,該方法比較簡單,根據(jù)Left,Top值進(jìn)行坐標(biāo)轉(zhuǎn)換后算出繪制地點(diǎn),然后調(diào)用System.Drawing.Graph.DrawString方法即可。字符對象還定義了自己的成員,比如Char屬性返回對象表示的字符數(shù)據(jù),F(xiàn)ont表示繪制對象使用的字體,F(xiàn)oreColor表示繪制文本的顏色。

    字符中的制表符比較特殊,因?yàn)樗膶挾仁遣欢ǖ模歉鶕?jù)它在文檔視圖中的位置而定的,因此在TextChar上在派生TextCharTab來轉(zhuǎn)變處理這種情況,它新增了RefreshTabWidth方法,來根據(jù)對象在視圖區(qū)域中的左端位置計(jì)算字符寬度。在此處我認(rèn)定一個(gè)制表符步長等于四個(gè)下畫線字符的寬度,制表符的右端坐標(biāo)必須是制表符步長的自然數(shù)倍,因此根據(jù)制表符的位置來進(jìn)行取模操作和其他操作就可以計(jì)算制表符的寬度。

    為了表示段落而定義了段落對象TextParagraph,該對象不是容器對象,保存了段落對齊方式的信息,該元素的顯示樣式類似于Word中的段落符(硬回車)的樣式。

    還定義了行結(jié)束對象TextLineEnd,該對象模擬了Word的分行符(軟回車)。

       可以定義圖片對象,經(jīng)過對Word處理文檔的行為觀察,可以發(fā)現(xiàn)在Word文檔中插入的圖片和OLE對象特性很相似,因此為了考慮文本編輯器的可擴(kuò)展性,首先在TextElement的基礎(chǔ)派生出TextObject抽象類,該抽象類表示一個(gè)在文檔中的對象,該對象由其派生的類決定。

     在TextObject對象派生出TextImage表示一個(gè)圖片對象,該對象重寫了RefreshView方法,用于在繪圖輸出對象上繪制一個(gè)圖片。還重載了FromXML和ToXML方法來和XML節(jié)點(diǎn)交換數(shù)據(jù),可以設(shè)計(jì)將圖片二進(jìn)制數(shù)據(jù)以Base64格式保存為XML節(jié)點(diǎn)下。

       此外還可以根據(jù)應(yīng)用的需要從TextObject對象上派生其他的類型,比如直接讀取數(shù)據(jù)庫在界面上繪制曲線圖等等,此時(shí)文檔中的該對象可以動態(tài)的展示系統(tǒng)中最新的數(shù)據(jù)。

      可以觀察到Word中的對象(包括圖片)可以改變大小,當(dāng)用鼠標(biāo)點(diǎn)擊圖片對象時(shí),圖片四個(gè)角和四個(gè)邊的中點(diǎn)上會顯示8個(gè)小點(diǎn)。這些小點(diǎn)我稱為控制點(diǎn)。用鼠標(biāo)拖拽這8個(gè)點(diǎn)可以動態(tài)的改變對象的大小。其實(shí)在很多類型的程序中可以碰到這8控制點(diǎn),例如在VS.NET的窗體設(shè)計(jì)器中,當(dāng)前的控制周圍就有這8個(gè)控制點(diǎn)。關(guān)于如何實(shí)現(xiàn)這8個(gè)控制點(diǎn)也是有一套的。

        控制點(diǎn)可以分為內(nèi)控制點(diǎn)和外控制點(diǎn)兩種類型,我們對這8個(gè)點(diǎn)進(jìn)行從0到7的編號。當(dāng)鼠標(biāo)光標(biāo)移動到這8個(gè)控制點(diǎn)上方時(shí)需要設(shè)置為不同的光標(biāo)樣式。

             內(nèi)控制點(diǎn)
┌─────────────────┐
│■0            1■             2■│
│                                  │
│                                  │
│                                  │
│                                  │
│■7                            3■│
│                                  │
│                                  │
│                                  │
│                                  │
│■6           5■              4■│
└─────────────────┘
外控制點(diǎn)
■               ■                  ■
┌────────────────┐
│0            1                 2│
│                                │
│                                │
│                                │
│                                │
■│7                              3│■
│                                │
│                                │
│                                │
│                                │
│6             5               4 │
└────────────────┘
■                ■                 ■
控制點(diǎn)上鼠標(biāo)光標(biāo)如下
西北-東南 SizeNWSE 南北 SizeNS      東北-西南 SizeNESW
■               ■                  ■
┌────────────────┐
│0            1                 2│
│                                │
│                                │
│                                │
│                                │
■│7 西-南 SizeWE                 3│■ 西-南 SizeWE
│                                │
│                                │
│                                │
│                                │
│6             5               4 │
└────────────────┘
■                ■                  ■
東北-西南 SizeNESW  南北 SizeNS        西北-東南 SizeNWSE

    根據(jù)上圖所示,已知主矩形,控制點(diǎn)的類型(是內(nèi)控制點(diǎn)還是外控制點(diǎn))和控制點(diǎn)的寬度可以計(jì)算出所有的控制點(diǎn)的位置。可以編一個(gè)例程,輸入3個(gè)參數(shù),主矩形區(qū)域的Rectangle結(jié)構(gòu)體,是否是內(nèi)控制點(diǎn)(不是內(nèi)控制點(diǎn)就是外控制點(diǎn))和控制點(diǎn)的寬度,該例程計(jì)算所有控制點(diǎn)的位置,然后返回一個(gè)包含8個(gè)Rectangle的數(shù)組,該數(shù)組就是0到7號的控制矩形的位置和大小。

    TextObject對象顯示后就應(yīng)該知道自己在視圖區(qū)域中的位置,當(dāng)它相應(yīng)鼠標(biāo)移動消息時(shí),就可以根據(jù)鼠標(biāo)光標(biāo)位置和8個(gè)控制矩形進(jìn)行比較,若鼠標(biāo)光標(biāo)在某個(gè)控制矩形中時(shí)就要通知文本編輯器改變鼠標(biāo)光標(biāo)的樣式。

    一般的控制點(diǎn)被畫成一個(gè)矩形方框,控制點(diǎn)也被畫成兩種類型,一種是填充色為深色(藍(lán)色或黑色)和白色邊框,另一種是深色邊框并填充白色。可以觀察VS.NET窗體設(shè)計(jì)器,可以在設(shè)計(jì)器中選擇多個(gè)控制,其中有一個(gè)控件的控制點(diǎn)為填充色為藍(lán)色和白色邊框的,該控制為當(dāng)前控件。而其他選擇的控件的控制點(diǎn)為藍(lán)色邊框并填充白色,這些控件為選擇控件。在文本編輯器中沒有這種情況,因此在此可以使用內(nèi)控制點(diǎn)方式,控制點(diǎn)用黑色填充,邊框白色。

   當(dāng)鼠標(biāo)在控制點(diǎn)上進(jìn)行拖拽操作就應(yīng)當(dāng)可以動態(tài)的修改對象的大小,以前我是如此實(shí)現(xiàn)的

  • 在鼠標(biāo)按鍵按下事件處理(HandleMouseDown)中,若鼠標(biāo)光標(biāo)在某個(gè)控制點(diǎn)上則設(shè)置一個(gè)鼠標(biāo)按鍵按下標(biāo)記變量,并記下鼠標(biāo)光標(biāo)位置,然后退出事件處理
  • 在鼠標(biāo)移動事件中(HandleMouseMove),若設(shè)置了鼠標(biāo)按鍵按下標(biāo)記變量,則根據(jù)當(dāng)前鼠標(biāo)光標(biāo)位置和上一次鼠標(biāo)光標(biāo)的位置之差就是鼠標(biāo)光標(biāo)移動的距離,該距離的水平分量和垂直分量就是對象寬度和高度的改變量,此時(shí)可以使用庫函數(shù)System.Windows.Forms.ControlPaint.DrawReversibleFrame在界面上繪制一個(gè)虛線框,當(dāng)鼠標(biāo)移動時(shí)不斷的調(diào)用該庫函數(shù),這樣實(shí)現(xiàn)了所謂的“橡皮筋”操作
  • 在鼠標(biāo)按鍵松開事件(HandleMouseDown)處理中,根據(jù)鼠標(biāo)光標(biāo)的當(dāng)前位置和以前記下的鼠標(biāo)按鍵按下時(shí)的鼠標(biāo)光標(biāo)位置計(jì)算兩者之差,這樣就是整個(gè)鼠標(biāo)拖拽操作中鼠標(biāo)光標(biāo)移動的距離,程序就可以依據(jù)該距離來改變對象的大小

   經(jīng)過一些編程實(shí)踐,發(fā)現(xiàn)該操作比較麻煩,需要編寫不少代碼,而且代碼分散在3個(gè)事件處理過程中,多了一些全局變量,很難寫出一個(gè)通用例程到處調(diào)用,經(jīng)過分析,將這種處理模式改掉了。其實(shí)一般的程序正在進(jìn)行鼠標(biāo)拖拽操作時(shí),用戶是不可能同時(shí)進(jìn)行其他操作(不如邊鼠標(biāo)拖拽邊打字),而且進(jìn)行”橡皮筋“操作時(shí)程序用戶界面無需重新繪制,這樣可以認(rèn)為進(jìn)行鼠標(biāo)拖拽時(shí)應(yīng)用程序應(yīng)用程序只處理鼠標(biāo)移動消息和鼠標(biāo)松開消息而不進(jìn)行任何其他操作,為了編程簡單,甚至連重繪界面的操作也不處理了,因此可以編一個(gè)通用例程來處理整個(gè)的鼠標(biāo)拖拽來實(shí)現(xiàn)“橡皮筋”操作,該函數(shù)處理過程為

  • 在鼠標(biāo)按鍵按下事件處理(HandleMouseDown)中就調(diào)用該例程
  • 進(jìn)入例程中,首先記下鼠標(biāo)光標(biāo)的當(dāng)前位置,然后進(jìn)入一個(gè)死循環(huán)
  • 該死循環(huán)首先調(diào)用Win32API函數(shù) WaitMessage等待Windows消息,若沒有任何Windows消息則退出該循環(huán)
  • 調(diào)用Win32API函數(shù)PeekMessage來獲得當(dāng)前Windows消息
  • 若當(dāng)前消息為鼠標(biāo)按鍵松開消息則退出循環(huán)
  • 若當(dāng)前消息為鼠標(biāo)移動消息則則獲得當(dāng)前鼠標(biāo)光標(biāo)位置,根據(jù)開始脫拽的鼠標(biāo)光標(biāo)位置來繪制橡皮筋矩形
  • 調(diào)用Win32API函數(shù)GetMessage將當(dāng)前Windows消息給“吃”掉,然后進(jìn)入下一次循環(huán)
  • 例程退出該循環(huán)后就將當(dāng)前鼠標(biāo)光標(biāo)位置和拖拽操作前的鼠標(biāo)光標(biāo)位置之差,也就是鼠標(biāo)光標(biāo)在整個(gè)拖拽操作中移動的距離作為返回值返回給主調(diào)函數(shù)(HandleMouseDown)
  • 主調(diào)函數(shù)接受返回的鼠標(biāo)光標(biāo)移動的距離,然后根據(jù)該距離來進(jìn)行其他的處理,在這里就是修改對象的大小

    在此插上一段,其實(shí).NET框架還是比較適合Win32的API編程,System.Windows.Form.Control的Handle屬性就是窗體的句柄,可以被其他Win32API作為參數(shù)調(diào)用,CreateParams屬性實(shí)際上就是CreateWindowEx的參數(shù),重載它就可以設(shè)置控件創(chuàng)建時(shí)的樣式;WndProc就是控件處理所有的Windows消息的默認(rèn)過程,也可以重載它自己來處理底層的Windows消息。System.Windows.Forms.Application的靜態(tài)函數(shù)AddMessageFilter和RemoveMessageFilter就可以很方便的為整個(gè)應(yīng)用程序添加或刪除"鉤子"程序。C#語言可以使用System.Runtime.InteropServices.DllImport來導(dǎo)入聲明DLL文件中的API函數(shù)。

    本站是提供個(gè)人知識管理的網(wǎng)絡(luò)存儲空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多