天天看點

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

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

       南京千裡獨行2005版權所有,不限轉載,請保留版權聲明

摘要

   本文探讨了使用C#從底層開發一個帶格式的文本編輯器的任務,深入探讨了其中的文檔對象模型的設計,圖形化使用者界面的處理和使用者操作的響應,說明了其中的某些技術問題和解決之道。

前言

    小弟從大學裡開始接觸程式設計也有6年了,工作4年也是幹程式設計的活,見過不少程式,自己也編過不少,在學校程式設計自己覺得是搞藝術品,其實玩一些遊戲,比如文明法老王星際等從某些角度看也是搞藝術品,看着自己苦心經營的建築物和人員由少變多,由簡單變複雜,心中有些成就感。程式設計也一樣,程式從幾十行寫到上萬行,功能由HellowWord到相當複雜而強大,心中也有不少成就感。

    畢業後工作,才漸漸感悟軟體開發本質上是做一個工具,這個工具給别人或者自己用。有了工具,很多問題就迎刃可解了。如此開來偶們程式員和石匠鐵匠木匠是同一類人了。不過沒什麼,程式員本來就沒高人一等,人在社會,認認真真的工作就行了。

問題

    廢話不多說了,現在談談标題提出的問題,如何用C#編寫文本編輯器。本人有幸開發過一個比較複雜的文本編輯器,是以也算有點經驗吧,在此來分享一下。這裡所指的文本編輯器不是簡單的像Windows自帶的單行或多行文本編輯框,而是類似于Word的文本編輯器。

    粗看起來,一個編輯器有什麼好難的,其實很難的,因為我們認為容易的事對計算機來說确實天大的問題。比如大家經常上網,可以發現最近幾年很多網站登入時除了輸入使用者名和密碼後還要輸入所謂的驗證碼,而驗證碼則在輸入框旁邊歪歪扭扭的畫了出來,就像國小一年紀的學生在一張髒紙上寫的一樣,這樣做隻是為了防止程式來模拟登入,因為歪歪扭扭的文字人類可以很容易的辨認,而計算機則很不容易辨認。

    一個文本編輯器主要處理的問題有

  • 檔案儲存格式的定義,文檔儲存為文本格式還是二進制格式的,文檔中各個資訊單元儲存什麼資訊。文檔格式很重要。
  • 和文檔存儲系統的交流,也就是儲存和加載文檔的功能,這裡的文檔存儲系統可以是作業系統檔案子系統,資料庫,網絡,其實檔案格式定下了,各種文檔存儲系統差别不大。
  • 文檔加載後的文檔對象維護,面對比較複雜的文檔處理,需要使用面向對象的程式設計思想,認真分析文檔結構,将加載的文檔資料一點點肢解掉,每一個最小的不可分割的文檔資料轉換為一個對象,然後使用一個對象樹來儲存文檔内容的層次關系,這樣構造一個文檔對象樹。文檔編輯工作就是維護這個文檔對象樹了。
  • 文檔對象的排版,文檔加載後需要處理整個文檔對象樹,計算每個對象的顯示大小,然後在視圖區中排列要顯示的對象,包括段落和文檔行的計算,然後計算對象在視圖區域中的直角坐标參數。
  • 文檔的繪制,這裡的繪制包括在計算機螢幕上繪制文檔内容和在列印機上繪制。程式根據計算好的對象在視圖區中的坐标,進行一些坐标轉換,在圖形輸出對象上繪制對象,比如繪制一個文字或圖檔。由于.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。

   這種動作處理的模式還便于程式進行擴充,其他應用程式也可往動作清單中添加自定義的動作對象,這樣文本編輯器就能自動應用該動作。應用程式還可修改各種動作的熱鍵設定來實作使用者操作的個性化。

派生對象

      定義了基礎對象後就開始派生對象了,首先定義字元對象類型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函數。