程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> 關於C# >> 如何用C#編寫文本編輯器

如何用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 方法,重新繪制對象

◆andleMouseDown 方法,處理鼠標按鍵按下事件

◆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個控制點上方時需要設置為不同的光標樣式。

內控制點

┌─────────────────┐

│■0      1■       2■│

│                 │

│                 │

│                 │

│                 │

│■7              3■│

│                 │

│                 │

│                 │

│                 │

│■6      5■       4■│

└─────────────────┘

外控制點

■        ■         ■

┌────────────────┐

│0      1         2│

│                │

│                │

│                │

│                │

■│7               3│■

│                │

│                │

│                │

│                │

│6       5        4 │

└────────────────┘

■        ■         ■

控制點上鼠標光標如下:

西北-東南 SizeNWSE 南北 SizeNS   東北-西南 SizeNESW

■        ■         ■

┌────────────────┐

│0      1         2│

│                │

│                │

│                │

│                │

■│7 西-南 SizeWE         3│■ 西-南 SizeWE

│                │

│                │

│                │

│                │

│6       5        4 │

└────────────────┘

■        ■         ■

東北-西南 SizeNESW 南北 SizeNS    西北-東南 SizeNWSE

根據上圖所示,已知主矩形,控制點的類型(是內控制點還是外控制點)和控制點的寬度可以計算出所有的控制點的位置。可以編一個例程,輸入3個參數,主矩形區域的Rectangle結構體,是否是內控制點(不是內控制點就是外控制點)和控制點的寬度,該例程計算所有控制點的位置,然後返回一個包含8個Rectangle的數組,該數組就是0到7號的控制矩形的位置和大小。

TextObject對象顯示後就應該知道自己在視圖區域中的位置,當它相應鼠標移動消息時,就可以根據鼠標光標位置和8個控制矩形進行比較,若鼠標光標在某個控制矩形中時就要通知文本編輯器改變鼠標光標的樣式。

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

當鼠標在控制點上進行拖拽操作就應當可以動態的修改對象的大小,以前我是如此實現的

◆在鼠標按鍵按下事件處理(HandleMouseDown)中,若鼠標光標在某個控制點上則設置一個鼠標按鍵按下標記變量,並記下鼠標光標位置,然後退出事件處理

◆在鼠標移動事件中(HandleMouseMove),若設置了鼠標按鍵按下標記變量,則根據當前鼠標光標位置和上一次鼠標光標的位置之差就是鼠標光標移動的距離,該距離的水平分量和垂直分量就是對象寬度和高度的改變量,此時可以使用庫函數System.Windows.Forms.ControlPaint.DrawReversibleFrame在界面上繪制一個虛線框,當鼠標移動時不斷的調用該庫函數,這樣實現了所謂的“橡皮筋”操作

◆在鼠標按鍵松開事件(HandleMouseDown)處理中,根據鼠標光標的當前位置和以前記下的鼠標按鍵按下時的鼠標光標位置計算兩者之差,這樣就是整個鼠標拖拽操作中鼠標光標移動的距離,程序就可以依據該距離來改變對象的大小

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

◆在鼠標按鍵按下事件處理(HandleMouseDown)中就調用該例程

◆進入例程中,首先記下鼠標光標的當前位置,然後進入一個死循環

◆該死循環首先調用Win32API函數 WaitMessage等待Windows消息,若沒有任何Windows消息則退出該循環

◆調用Win32API函數PeekMessage來獲得當前Windows消息

◆若當前消息為鼠標按鍵松開消息則退出循環

◆若當前消息為鼠標移動消息則則獲得當前鼠標光標位置,根據開始脫拽的鼠標光標位置來繪制橡皮筋矩形

◆調用Win32API函數GetMessage將當前Windows消息給“吃”掉,然後進入下一次循環

◆例程退出該循環後就將當前鼠標光標位置和拖拽操作前的鼠標光標位置之差,也就是鼠標光標在整個拖拽操作中移動的距離作為返回值返回給主調函數(HandleMouseDown)

◆主調函數接受返回的鼠標光標移動的距離,然後根據該距離來進行其他的處理,在這裡就是修改對象的大小

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

光標的控制

光標就是文本編輯器在獲得輸入焦點時在當前插入點閃爍顯示的一個小方塊,當文本編輯器沒有獲得輸入焦點時就不會顯示光標。Windows操作系統已經提供了一套處理光標的API函數,包括創建光標CreateCaret,設置光標位置SetCaretPos,顯示光標ShowCaret和隱藏光標HideCaret。使用API創建和顯示光標後,操作系統會自動的讓光標閃爍。文本編輯器要提供處理光標的例程供其他程序模塊調用。文本編輯器首先根據當前插入點的位置計算光標在文檔視圖區域中的位置,還需要根據文檔處理插入模式還是修改模式計算光標的大小。若光標所在位置在用戶界面上沒有顯示出來則還需要滾動文本編輯器已保證光標所在區域處於可視區域。然後調用API來創建和顯示光標。文本編輯器還重載OnGotFocus過程來顯示光標,重載OnLostFocus過程隱藏光標。注意重載這兩個過程時需要在最後必須調用base.OnGotFocus和base.OnLostFocus,若不這樣則文本編輯器嵌入在網頁中運行會發生無法獲得輸入焦點的錯誤。光標控制還涉及到輸入法的控制,我們中國人使用文本編輯器會使用到各種中文輸入法。Windows操作系統也提供了一套API來控制輸入法。在本文本編輯器中重載OnKeyPress方法來獲得用戶輸入的字符,此時的字符可以是鍵盤直接輸入的ASCI字符,也可以是使用某種輸入法輸入的漢字。這些操作系統都已經實現了,為什麼還要控制輸入法。原因是使用默認處理時輸入法的浮動窗口會顯示在屏幕中間而不會隨著插入點的位置而移動。一般的中文輸入法的用戶界面為一個浮動窗口,各種建議輸入的中文字符顯示在這個浮動窗口中供人選擇。想想看,當插入點在屏幕的某個邊角中,而輸入法的浮動窗口在屏幕中央,這樣輸入中文比較累。但若輸入的浮動窗體隨著插入點的移動而移動,浮動窗體和插入點緊密的靠在一起,這樣輸入中文就不是很累了。Win32API函數ImmSetCompositionWindow能對指定的窗體控制輸入法浮動窗體的顯示位置,當插入點改變時調用該API函數就能讓輸入法浮動窗體隨著插入點跑了。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved