簡(jiǎn)介
繪圖編輯框架(GEF)被設(shè)計(jì)用來(lái)以圖形而不是文本的方式來(lái)編輯用戶數(shù)據(jù),一般被稱為模型(model)。當(dāng)處理包含多對(duì)多,一對(duì)多以及其他復(fù)雜關(guān)系的實(shí)體時(shí),GEF是一種很有價(jià)值的工具。隨著Eclipse Rich Client Platform 的流行,使得編輯器的開(kāi)發(fā)不僅僅局限于編程,GEF的重要性也與日俱增。比如說(shuō),數(shù)據(jù)庫(kù)schema編輯器 [7],邏輯電路編輯器和任務(wù)流管理器,這些例子都很好地展示了GEF是一種可以用于各個(gè)不同領(lǐng)域的,具有強(qiáng)大功能和靈活性的框架。
然而,任何通用框架都設(shè)計(jì)復(fù)雜,難于學(xué)習(xí),GEF也不例外。到現(xiàn)在為止,最小的例子也將涉及75個(gè)類(lèi)。即使對(duì)于最勤勉的開(kāi)發(fā)者來(lái)說(shuō),要從GEF用戶定義類(lèi)型和GEF提供的上百種類(lèi)型之間相互作用來(lái)理解GEF的獨(dú)特之處,對(duì)耐心和智力的都是一種考驗(yàn)。為了改變這種狀況,一個(gè)全新的,規(guī)模更小的編輯器例子被添加進(jìn)即將到來(lái)的Eclipse 3.1(譯:翻譯此文時(shí),Eclipse 3.1已經(jīng)發(fā)布)。這個(gè)幾何圖形編輯器(看圖1)允許你創(chuàng)建,編輯簡(jiǎn)單的圖。它處理兩種對(duì)象,矩形和橢圓。你可以在實(shí)線和虛線這兩種連接類(lèi)型中選擇一種來(lái)連接兩個(gè)對(duì)象。每一個(gè)連接都是有方向的,也就是說(shuō)從一個(gè)源對(duì)象開(kāi)始,在目標(biāo)對(duì)象處終止。箭頭用來(lái)表示連接方向。連接可以轉(zhuǎn)移,也就是通過(guò)拖動(dòng)它的源點(diǎn)或目標(biāo)點(diǎn)到一個(gè)新的對(duì)象上。編輯器中的對(duì)象可以點(diǎn)擊選中,也可以通過(guò)拖拉一個(gè)區(qū)域來(lái)選擇。選中的對(duì)象可以被刪除。所有的模型操作,比如添加,刪除對(duì)象,移動(dòng)對(duì)象,改變大小等等,都可以u(píng)ndo或redo。最后,編輯器集成了兩個(gè)Eclipse標(biāo)準(zhǔn)視圖Properties和Outline。這個(gè)編輯器的價(jià)值不是在于它的可用性,而是作為例子,通過(guò)有限的兩種用戶定義類(lèi)型來(lái)演示在一個(gè)成熟GEF編輯器中會(huì)碰到的大多數(shù)概念和技術(shù)。
將最新的Eclipse 3.1 GEF例子從GEF項(xiàng)目下載頁(yè)面下載下來(lái),并解壓縮至你的Eclipse目錄中。按Ctrl-N,會(huì)彈出創(chuàng)建向?qū)В瑢?em>Examples目錄展開(kāi),選擇Shapes Diagram。下面給出幾何圖內(nèi)部工作的詳細(xì)的全面介紹。在我們接觸代碼前,我們先來(lái)看看GEF主要思想。
GEF核心概念
GEF幫助你為數(shù)據(jù)構(gòu)造一個(gè)可視化的編輯器。數(shù)據(jù)可以是帶有簡(jiǎn)單溫度旋鈕的溫度調(diào)節(jié)器,也可以是一個(gè)包含幾百個(gè)路由器,連接和服務(wù)質(zhì)量策略的虛擬局域網(wǎng)。幸虧GEF設(shè)計(jì)者,他們?cè)O(shè)法建立一種框架,使得它能夠和任何數(shù)據(jù)一起工作,用GEF的術(shù)語(yǔ)來(lái)說(shuō),就是任何模型(model)。這是通過(guò)嚴(yán)格遵循了模型-視圖-控制器模式(MVC)來(lái)做到的。模型就是你的數(shù)據(jù)。對(duì)于GEF,模式可以是任何普通的Java對(duì)象(POJO)。模型不應(yīng)該知道任何有關(guān)于控制器或視圖的信息。視圖(view)是模型或其某一部分在屏幕上的可視化表示。它可以是矩形,線或橢圓這樣的簡(jiǎn)單圖形,也可以是彼此嵌套的邏輯電路。同時(shí),視圖也應(yīng)該對(duì)模型和控制器一無(wú)所知。雖然任何實(shí)現(xiàn)IFigure
接口的類(lèi)都可以作為視圖,但是GEF使用Draw2D可視圖形(figure)。控制器,可稱為編輯部件(edit part),是模型和視圖之間的橋梁。當(dāng)你開(kāi)始編輯你的模型時(shí),一個(gè)頂層的控制器被創(chuàng)建出來(lái)。如果模型由若干個(gè)片段組成,頂層控制器就會(huì)將這個(gè)信息通知GEF。接下來(lái),每個(gè)片段的子控制器被創(chuàng)建出來(lái)。如果它們又包含子片段,這個(gè)過(guò)程就會(huì)一直繼續(xù)下去,直到所有組成模型的對(duì)象都有它們的控制器??刂破鞯牧硪粋€(gè)任務(wù)是創(chuàng)建可視圖形來(lái)表示模型。一旦模型被設(shè)置到某個(gè)控制器,GEF就向控制器要合適的IFigure
對(duì)象。既然模型和視圖彼此都不知道對(duì)方,控制器負(fù)責(zé)監(jiān)聽(tīng)模型的修改,并更新模型的可視化表示。結(jié)果,在許多GEF編輯器中,一個(gè)常見(jiàn)的模式就是模型發(fā)PropertyChangeEvent
通知。當(dāng)一個(gè)編輯部件收到事件通知時(shí),它通過(guò)調(diào)整模型的外觀或結(jié)構(gòu)上的表示來(lái)作相應(yīng)的改變。
可視編輯的另一個(gè)方面就是對(duì)用戶動(dòng)作和鼠標(biāo),鍵盤(pán)事件作出響應(yīng)。這里的挑戰(zhàn)在于提供一種機(jī)制,提供合理的缺省行為,并且允許重新定義行為來(lái)覆蓋缺省行為,以適應(yīng)所編輯模型。比如鼠標(biāo)拖動(dòng)事件,如果我們假設(shè)每次檢測(cè)到鼠標(biāo)拖動(dòng)事件,所選中對(duì)象都被移動(dòng)的話,我們就限制編輯器開(kāi)發(fā)者的自由。很有可能有人希望在鼠標(biāo)拖動(dòng)的時(shí)候,提供放大,縮小的行為。GEF通過(guò)使用工具(tool),請(qǐng)求(request)和策略(policy)解決了這個(gè)問(wèn)題。
工具是一種有狀態(tài)的對(duì)象,它將象鼠標(biāo)按鈕被按下,被拖動(dòng)等低層事件翻譯成高層的由Request
對(duì)象表示的請(qǐng)求。發(fā)送哪個(gè)請(qǐng)求取決于所激活的工具。例如,連接工具在收到鼠標(biāo)按鈕被按下這樣的事件時(shí),會(huì)發(fā)送一個(gè)連接開(kāi)始或結(jié)束的請(qǐng)求。如果是一個(gè)創(chuàng)建工具,我們就會(huì)收到一個(gè)創(chuàng)建請(qǐng)求。GEF包含了大量預(yù)定義的工具以及創(chuàng)建應(yīng)用特定工具的方法。工具可以由程序控制激活,也可以在用戶實(shí)施一個(gè)動(dòng)作后激活。在大多數(shù)情況下,工具將請(qǐng)求發(fā)送給鼠標(biāo)位置下面的圖形的EditPart
。例如,如果你點(diǎn)擊一個(gè)代表widget的矩形,與此相關(guān)的編輯部件就會(huì)收到一個(gè)選中請(qǐng)求或者直接編輯的請(qǐng)求。有時(shí)候,請(qǐng)求會(huì)發(fā)送給區(qū)域中的所有可視圖形的編輯部件,比如MarqueeSelectionTool
就是這樣。無(wú)論一個(gè)或多個(gè)編輯部件怎樣被選擇為請(qǐng)求目標(biāo),它們自己并不處理請(qǐng)求。而是將這個(gè)任務(wù)交給所注冊(cè)的編輯策略( edit policies)。每個(gè)編輯策略都會(huì)為該請(qǐng)求提供一個(gè)命令。不希望處理請(qǐng)求的策略將返回一個(gè)null
。使用策略而不是編輯部件來(lái)響應(yīng)請(qǐng)求的機(jī)制使得策略和編輯部件都盡可能短小,功能集中。同時(shí),也意味著調(diào)試和維護(hù)代碼變得更容易。GEF的最后一個(gè)部分就是命令(command)。GEF并沒(méi)有直接修改模型,它要求你使用命令來(lái)做實(shí)際的修改。每個(gè)命令應(yīng)該實(shí)現(xiàn)執(zhí)行對(duì)模型或模型一部分的修改和撤銷(xiāo)修改。這樣,GEF編輯器自動(dòng)支持模型修改的undo/redo。
除了能夠提升你的技能以及設(shè)計(jì)模式方面的知識(shí)外,使用GEF的一個(gè)重要的優(yōu)點(diǎn)在于它能夠和Eclipse平臺(tái)完全集成在一起。在編輯器中選中的對(duì)象可以為標(biāo)準(zhǔn)Properties視圖提供屬性。Eclipse向?qū)Э梢杂脕?lái)創(chuàng)建,初始化GEF編輯器編輯的模型。Edit菜單中的Undo和Redo可以觸發(fā)GEF編輯修改的撤銷(xiāo)和重做。簡(jiǎn)單地說(shuō),GEF編輯器實(shí)現(xiàn)IEditorPart
接口,是Eclipse平臺(tái)中的一員,它和文本編輯器或其他workbench編輯器處于同樣的集成層次。
模型
創(chuàng)建GEF編輯器的第一步是創(chuàng)建模型。在我們的例子里,模型由四類(lèi)對(duì)象組成:幾何圖(包含所有的圖形),兩種類(lèi)型的圖形,和圖形間的連接。在我們?yōu)檫@些類(lèi)編寫(xiě)代碼前,我們準(zhǔn)備了一些基礎(chǔ)結(jié)構(gòu)。
核心模型類(lèi)
當(dāng)你創(chuàng)建模型時(shí),你可以參考下面的內(nèi)容:
- 模型存儲(chǔ)了所有用戶可以編輯或?yàn)g覽的數(shù)據(jù)。這同時(shí)也包括和可視化表示相關(guān)的數(shù)據(jù),比如邊界。你不能依賴編輯部件或可視圖形來(lái)保存這些數(shù)據(jù),因?yàn)檫@些對(duì)象可能根據(jù)需要?jiǎng)?chuàng)建或丟棄。如果你不喜歡將你的可視數(shù)據(jù)和你的業(yè)務(wù)數(shù)據(jù)綁定在一起,可以參考[3]中的建議。
- 提供持久化模型的方法。確信當(dāng)編輯器在關(guān)閉時(shí),你的模型被持久化。當(dāng)同樣的編輯器被打開(kāi)時(shí),實(shí)現(xiàn)方法使得模型狀態(tài)可以從持久存儲(chǔ)器中恢復(fù)。
- 模型必須保持與視圖或控制器無(wú)關(guān)。不要存儲(chǔ)任何對(duì)視圖或控制器的引用。GEF在某種條件下會(huì)丟棄視圖或控制器。如果你保持了這些引用,你可能會(huì)碰到一個(gè)失效的可視圖形或編輯部件。
- 提供方法允許別人監(jiān)聽(tīng)模型的變化。這使得控制器可以及時(shí)響應(yīng)修改,并對(duì)視圖作適當(dāng)調(diào)整。既然你不能保持對(duì)控制器的引用,唯一的方法就是為控制器提供一種途徑,使得它能夠作為一個(gè)事件接受者注冊(cè)(和撤銷(xiāo)注冊(cè))在模型上。一個(gè)好的辦法就是使用
java.beans
包中的屬性修改事件通知。
上面所列的規(guī)則對(duì)于所有模型都是相同的,為基本類(lèi)建立類(lèi)層次來(lái)強(qiáng)化這些規(guī)則是很有好處的。ModelElement
類(lèi)繼承了Java的Object
類(lèi),并提供了三個(gè)功能:持久化,屬性改變和屬性源支持。簡(jiǎn)單的模型持久化可以通過(guò)實(shí)現(xiàn)
java.io.Serializable
接口以及
readObject方法來(lái)完成。這使得你可以將編輯器的模型以二進(jìn)制格式存儲(chǔ)。當(dāng)需要和某種應(yīng)用一起工作時(shí),這并不能提供的格式的可移植性。在復(fù)雜的情況下,你需要實(shí)現(xiàn)將模型以XML或類(lèi)似的格式存儲(chǔ)。模型的改變通過(guò)屬性改變事件來(lái)通知。這個(gè)基本類(lèi)允許編輯部件
注冊(cè)和
撤銷(xiāo)注冊(cè)為屬性改變通知的接受者。屬性改變通知是通過(guò)調(diào)用
firePropertyChange方法觸發(fā)的。最后,為了幫助和workbench的Properties視圖集成,需要實(shí)現(xiàn)IPropertySource接口(細(xì)節(jié)在圖2中忽略)。
橢圓和矩形這兩類(lèi)對(duì)象,在許多方面是相同的,它們的公共功能可以被提取出來(lái)放在公共類(lèi)中。尤其是兩者都代表著占據(jù)某個(gè)位置,具有一定大小的對(duì)象。它們可以彼此連接。這些屬性的任何修改都需要通知監(jiān)聽(tīng)者。更進(jìn)一步地說(shuō),它們的位置和大小屬性都可以通過(guò)IPropertySource
接口暴露,這允許用戶通過(guò)Properties視圖來(lái)查看,和修改它們。
對(duì)象間連接的管理很值得仔細(xì)看一下。這里并沒(méi)有一個(gè)全局的用于存儲(chǔ)所有連接的地方。GEF要求模型部件報(bào)告它們之間的連接的情況,是源還是目標(biāo)。這些信息都以List對(duì)象的形式
提供。Shape
類(lèi)維護(hù)了兩個(gè)數(shù)組列表,分別存儲(chǔ)
源連接和
目標(biāo)連接。源連接是指那些以當(dāng)前圖形作為源的連接,目標(biāo)連接是指以當(dāng)前圖形作為目標(biāo)的連接。兩個(gè)包可見(jiàn)方法(
和
)使得圖形和連接可以彼此知道相互之間的關(guān)系。此外,兩個(gè)公有方法(
和
)使得model
包外面的類(lèi)知道圖形的連接情況。這些方法都會(huì)被相關(guān)的圖形(形狀)控制器所使用,具體內(nèi)容將在接下來(lái)的部分中加以介紹。
頂層模型類(lèi)
通過(guò)上面的準(zhǔn)備,我們可以開(kāi)始編寫(xiě)頂層模型類(lèi)。Connection
類(lèi)表示兩個(gè)圖形間的連接。它存儲(chǔ)連接的源和目標(biāo)。通過(guò)調(diào)用disconnect
或reconnect
方法可以修改連接。連接含有一個(gè)boolean值來(lái)表示連接是否存在。命令會(huì)使用這個(gè)值來(lái)驗(yàn)證某種操作的合法性。源連接和目標(biāo)連接都保持一個(gè)到源圖形的引用,這樣使得被斷開(kāi)的連接可以很容易地被重新連接。連接包含一個(gè)屬性,就是線的類(lèi)型。EllipticalShape
和RectangularShape
類(lèi)都擴(kuò)展了Shape
類(lèi),添加了很少的功能。
ShapeDiagram
類(lèi)是ModelElement
類(lèi)的子類(lèi),它可以作為一種容器。它維護(hù)一組圖形,并通知監(jiān)聽(tīng)器這組圖形的變化。命令可以調(diào)用
addChild
和
removeChild
方法,并檢查返回的boolean值來(lái)驗(yàn)證它們的操作。這個(gè)類(lèi)也提供了
公共方法給控制器類(lèi)。
實(shí)現(xiàn)上需要注意的地方
細(xì)心的讀者一定意識(shí)到這個(gè)模型創(chuàng)建了一個(gè)有向圖的實(shí)現(xiàn),圖形作為頂點(diǎn),連接作為邊,所有圖形,連接構(gòu)成的圖就是圖。這里所形成的表示方式稱為鄰接點(diǎn)列表表示法,它很適合稀疏圖。只要略作修改,這個(gè)模型的代碼就可以轉(zhuǎn)變?yōu)橐话愕膱D表示。這里對(duì)算法書(shū)中的圖實(shí)現(xiàn)所需要做的就是添加代碼使得圖,節(jié)點(diǎn),和邊在發(fā)生改變的時(shí)候發(fā)送事件。不象數(shù)學(xué)上的圖,節(jié)點(diǎn)不是零維的點(diǎn),而是有矩形邊框。最后,圖存儲(chǔ)了所有的邊,而圖形并沒(méi)有存儲(chǔ)連接,因?yàn)镚EF并沒(méi)有要求這么做。
值得注意的是,由上面的類(lèi)所提供的解決方案并不是唯一的方法。那些開(kāi)發(fā)計(jì)算機(jī)圖形的人更愿意用另一種方法來(lái)存儲(chǔ)連接,安排節(jié)點(diǎn)和邊之間的通信。然而,這些細(xì)節(jié)并不是那么重要。設(shè)計(jì)者可以自由地選擇他們認(rèn)為更具普遍性,更快,或者功能更強(qiáng)的模型表示。關(guān)鍵的地方在模型改變的消息通知,模型修改的維護(hù),包括對(duì)可視屬性和模型持久化的支持。其余的都取決于你的經(jīng)驗(yàn)和需要,你可以自由地進(jìn)行選擇。
視圖
由于這個(gè)圖形編輯器非常的簡(jiǎn)單,我們不必創(chuàng)建可視圖形來(lái)表示我們的模型,而是使用預(yù)定義的可視圖形。Figure
類(lèi)加上FreeformLayout
布局管理器用來(lái)表示圖。這允許我們將對(duì)象拖放到任何位置。RectangleFigure
和Ellipse
都可以表示對(duì)象。使用預(yù)定義的可視圖形來(lái)表示部分模型并不是通常的做法。即使你的視圖沒(méi)有引用模型或控制器,它都必須為每個(gè)用戶可能需要查看或修改的模型重要方面都定義可視化屬性。因此常常會(huì)定義擁有大量可視化屬性,比如顏色,文本,嵌套可視圖形等,的復(fù)雜可視圖形,每個(gè)屬性都對(duì)應(yīng)于它們所表示的模型屬性。有關(guān)創(chuàng)建復(fù)雜可視圖形的詳細(xì)處理,請(qǐng)參考 [4]。
部件(part)
對(duì)于模型的每個(gè)獨(dú)立部分,我們都必須定義控制器。所謂“獨(dú)立”,指的是這個(gè)實(shí)體都可以作為用戶操作的對(duì)象。一個(gè)比較好的原則就是任何可以被選擇,或刪除的對(duì)象都應(yīng)該有它自己的編輯部件。
編輯部件知道模型,監(jiān)聽(tīng)模型改變所產(chǎn)生的事件,然后更新視圖。由于在模型層所做的設(shè)計(jì)選擇,所有的編輯部件都必需遵循圖5所示的模式。每個(gè)部件
都實(shí)現(xiàn)PropertyChangeListener
接口。當(dāng)它被激活時(shí)
,它將自己注冊(cè)為模型的屬性修改事件的接收者。當(dāng)失活時(shí)
,它將自己從監(jiān)聽(tīng)器的列表中移除。最后,當(dāng)它收到屬性修改事件時(shí)
,它會(huì)根據(jù)屬性名和新舊值來(lái)刷新表示模型的可視圖形。事實(shí)上,這個(gè)模式使用非常普遍,在大的應(yīng)用中,它會(huì)建立一個(gè)基類(lèi)來(lái)提供這樣的行為。
DiagramEditPart 類(lèi)
當(dāng)編輯器成功載入一個(gè)幾何圖,并將它設(shè)置在一個(gè)圖形viewer上,就要求ShapesEditPartFactory創(chuàng)建一個(gè)編輯部件來(lái)控制圖。它創(chuàng)建一個(gè)新的DiagramEditPart實(shí)例,并將圖設(shè)置為它的模型。當(dāng)新創(chuàng)建的編輯部件被激活時(shí),它將自己注冊(cè)為模型的監(jiān)聽(tīng)器,并創(chuàng)建一個(gè)使用free form布局管理器的可視圖形,這種布局管理器允許通過(guò)它們的邊界來(lái)定位圖的可視圖形。DiagramEditPart通過(guò) getModelChildren方法來(lái)獲取圖中包含的所有圖形。就象前面提到的,GEF為返回的所有子模型對(duì)象都會(huì)創(chuàng)建編輯部件和可視圖形。
DiagramEditPart
類(lèi)安裝了三個(gè)策略。所有的策略都在AbstractEditPart
類(lèi)的createEditPolicies
方法中定義,同時(shí)所有繼承自AbstractGraphicalEditPart 的實(shí)類(lèi)都必需實(shí)現(xiàn)這個(gè)方法。編輯部件使用這些策略來(lái)處理工具發(fā)出的請(qǐng)求。在最簡(jiǎn)單的情況下,策略負(fù)責(zé)生成許多命令。策略使用String類(lèi)型的索引字注冊(cè)在編輯部件上,這個(gè)索引字被稱為策略角色。這些索引字對(duì)編輯部件本身來(lái)說(shuō)沒(méi)有什么意義。然而,對(duì)軟件開(kāi)放人員,就有意義了,它使得其他人,尤其是擴(kuò)展你的控制器的人,可以通過(guò)這些索引字來(lái)關(guān)閉或移除策略。就GEF而言,你的索引字可以是“foobar”。然而,你最好告訴你程序員同伴,當(dāng)布局管理器改變的時(shí)候,為了設(shè)置新的布局策略,需要安裝新的“foobar”策略。由于這可能很有趣,且不是那么顯而易見(jiàn),所以推薦你使用EditPolicy接口定義索引字,這些名字需要很好的表達(dá)該策略在編輯部件中的角色。
安裝的第一個(gè)策略
的索引字是EditPolicy.COMPONENT_ROLE
,它負(fù)責(zé)阻止模型的根被刪除。它重寫(xiě)了createDeleteCommand
方法,并返回一個(gè)不能被執(zhí)行的命令。第二個(gè)策略
的索引字是LAYOUT_ROLE
,它處理創(chuàng)建請(qǐng)求和邊界修改請(qǐng)求。當(dāng)新的圖形被放置到圖中,第一個(gè)請(qǐng)求被發(fā)送出來(lái)。布局策略返回一個(gè)命令,這個(gè)命令添加新的圖形到圖編輯器中,并把它放置在適當(dāng)?shù)奈恢?。用戶修改圖中已存在的圖形大小或移動(dòng)它時(shí),都會(huì)發(fā)出邊界修改請(qǐng)求。第三個(gè)installEditPolicy
調(diào)用
刪除一個(gè)策略。它在用戶點(diǎn)擊模型根所在區(qū)域時(shí),阻止根部件提供選擇反饋。這里也可以看出一個(gè)有意義的策略索引字的重要性。
圖編輯部件監(jiān)視子編輯部件的添加,移除事件。當(dāng)任何新的圖形添加或移除時(shí),ShapesDiagam
類(lèi)將發(fā)送這些事件。當(dāng)圖編輯部件檢測(cè)到這兩種屬性修改事件時(shí),圖編輯部件都會(huì)調(diào)用AbstractEditPart
類(lèi)中定義的refreshChildren
方法。這個(gè)方法會(huì)遍歷所有子模型對(duì)象,并相應(yīng)地添加,移除,或重新排序子編輯部件。
ShapeEditPart 類(lèi)
ShapeEditPart
類(lèi)管理所有的圖形。當(dāng)DiagramEditPart
會(huì)返回子模型列表時(shí),ShapeEditPart
由ShapesEditPartFactory
類(lèi)根據(jù)每個(gè)模型對(duì)象的類(lèi)型創(chuàng)建。工廠類(lèi)創(chuàng)建的每個(gè)部件都擁有一個(gè)它們所控制的子模型。一旦模型對(duì)象被設(shè)置,編輯部件被要求創(chuàng)建可視圖形來(lái)表示模型對(duì)象。根據(jù)模型對(duì)象的類(lèi)型,返回橢圓或矩形的編輯部件。
這個(gè)編輯部件關(guān)注四類(lèi)屬性修改事件:大小,位置,源連接,和目標(biāo)連接。如果圖形改變了大小或位置,
refreshVisual
方法會(huì)被調(diào)用。這個(gè)方法在可視圖形被創(chuàng)建的時(shí)候就會(huì)由GEF自動(dòng)調(diào)用。在這個(gè)方法中,可視圖形的可視屬性應(yīng)該根據(jù)模型的狀態(tài)做相應(yīng)調(diào)整。重用模型更新方法是 GEF編輯器中經(jīng)常碰到的又一種模式。在我們這個(gè)編輯部件類(lèi)中,新的位置和大小被獲取并儲(chǔ)存在表示圖形的可視圖形中。此外,新的邊界會(huì)傳給父控制器的布局管理器。當(dāng)源連接或目標(biāo)連接改變時(shí),源連接或目標(biāo)連接改編輯部件會(huì)調(diào)用AbstractGraphicalEditPart
類(lèi)中的方法刷新。和refreshChildren
方法相似,這些方法會(huì)遍歷所有的連接,并相應(yīng)添加,刪除,或重新定位它們的編輯部件。
由于圖形可以連接到其他圖形,圖形編輯部件重寫(xiě)了
getModelSourceConnections
方法和
getModelTargetConnections
方法。這兩個(gè)方法的任務(wù)就是要通知GEF有關(guān)該圖形的源連接和目標(biāo)連接。此外,ShapeEditPart
實(shí)現(xiàn)了
NodeEditPart 接口。通過(guò)實(shí)現(xiàn)這個(gè)接口,編輯部件可以定義源錨點(diǎn)和目標(biāo)錨點(diǎn),錨點(diǎn)就是圖形和連接接觸的連接點(diǎn)。邏輯電路編輯器的例子使用這個(gè)功能來(lái)指定線如何連接到一個(gè)邏輯門(mén)元件。既然圖形并沒(méi)有特定的連接點(diǎn),我們就使用包圍矩形錨點(diǎn),它將連接設(shè)置在可視圖形的包圍矩形上。如果你愿意,你可以為橢圓返回 EllipseAnchor,它將返回一個(gè)橢圓邊界上的點(diǎn)。對(duì)于更加復(fù)雜的圖形,你應(yīng)該繼承AbstractConnectionAnchor類(lèi),并實(shí)現(xiàn) getLocation方法。注意,有兩種方法需要實(shí)現(xiàn):一個(gè)使用ConnectionEditPart對(duì)象作為參數(shù),另一個(gè)使用Request對(duì)象。當(dāng)一個(gè)新的連接被創(chuàng)建時(shí),第二個(gè)方法
會(huì)被調(diào)用以便用戶得到反饋,而第一個(gè)方法
用于已建立的連接。
圖形編輯部件安裝了兩個(gè)策略。ShapeComponentEditPolicy
提供命令將一個(gè)圖形從圖刪除。第二個(gè)策略處理圖形間連接的創(chuàng)建和轉(zhuǎn)移,它的索引字是GRAPHICAL_NODE_ROLE
。連接創(chuàng)建工具創(chuàng)建新的連接需要兩個(gè)步驟。當(dāng)用戶點(diǎn)擊模型元素的可視圖形時(shí),該策略被要求
創(chuàng)建一個(gè)連接命令。如果這個(gè)方法返回null
,表示這個(gè)連接不能從所給的模型元素開(kāi)始。如果允許連接的話,將創(chuàng)建新的命令,并作為起始命令存儲(chǔ)在請(qǐng)求中。當(dāng)用戶點(diǎn)擊另一個(gè)可視圖形時(shí),會(huì)要求策略提供一個(gè)
連接完成命令。這是一個(gè)根據(jù)起始命令創(chuàng)建的新命令,而起始命令中包含了連接結(jié)束點(diǎn)的信息。
圖形節(jié)點(diǎn)編輯策略的另一個(gè)任務(wù)是提供連接的轉(zhuǎn)移命令。連接可以修改連接的源或目標(biāo)實(shí)現(xiàn)轉(zhuǎn)移。連接轉(zhuǎn)移命令和連接創(chuàng)建命令有同樣的規(guī)則。尤其是當(dāng)一個(gè)連接不能轉(zhuǎn)移時(shí),策略返回null。策略也可能通過(guò)canExecute
方法返回false來(lái)得到一個(gè)拒絕執(zhí)行的命令。由于篇幅限制,這些命令的細(xì)節(jié)就不多說(shuō)了,讀者可以參考代碼。
ConnectionEditPart 類(lèi)
由于連接也是用戶可編輯的模型對(duì)象,它們必須有自己的控制器。連接的控制器是由ConnectionEditPart
類(lèi)實(shí)現(xiàn),它繼承自AbstractConnectionEditPart
類(lèi)。和其他控制器類(lèi)似,它也實(shí)現(xiàn)了
PropertyChangeListener
接口,并注冊(cè)自己為模型的監(jiān)聽(tīng)器。連接部件
返回一個(gè)帶有箭頭的線作為可視圖形。它安裝了兩個(gè)編輯策略。第一個(gè)是
ConnectionComponentPolicy,它提供刪除命令給Delete菜單項(xiàng)所需要的action。第二個(gè)
比較有意思。它含有一個(gè)被選擇的連接,這個(gè)連接包括起始端和結(jié)束端的標(biāo)識(shí)。沒(méi)有這個(gè)策略,就不可能轉(zhuǎn)移連接,因?yàn)楫?dāng)一個(gè)連接被拖動(dòng)時(shí),GEF沒(méi)有辦法獲取連接兩端的標(biāo)識(shí)。GEF的設(shè)計(jì)者建議所有的ConnectionEditParts都應(yīng)該有這個(gè)策略,即使連接的兩端都不能拖動(dòng)。至少這個(gè)策略提供了一種視覺(jué)上的選擇反饋。propertyChange方法可以收到
線風(fēng)格屬性的變化,并對(duì)線figure作相應(yīng)的調(diào)整。
幾何圖形編輯器
幾何圖形編輯器繼承了GraphicalEditorWithFlyoutPalette
類(lèi)。這個(gè)類(lèi)是圖形編輯器的一種特殊形式,它本身也是一種編輯部件,并可以擁有一個(gè)提供工具的面板。使用這個(gè)類(lèi)必須實(shí)現(xiàn)兩個(gè)方法,getPaletteRoot
和getPalettePreferences
。第一個(gè)方法必須返回包含所有工具選項(xiàng)的面板的根節(jié)點(diǎn)。工具選項(xiàng)是一種特殊的面板選項(xiàng),它將工具安裝在編輯器的編輯域上。它們必須位于面板抽屜中,面板抽屜將工具選項(xiàng)很方便地組合起來(lái)。一般推薦有一個(gè)工具選項(xiàng)作為整個(gè)工具面板的缺省選項(xiàng)。一個(gè)典型的解決方法就是直接使用SelectionToolEntry
類(lèi)的實(shí)例。第二個(gè)方法返回的面板首選項(xiàng)中包含的內(nèi)容有,報(bào)告面板是可見(jiàn)還是被折疊起來(lái)了,面板停靠的位置,以及面板的寬度。通常的解決方法是將它們存在plug-in的首選項(xiàng)存儲(chǔ)區(qū)中。
我們上面提到的編輯域起了一個(gè)中心控制器的作用。它負(fù)責(zé)保存工具,載入缺省工具,維護(hù)當(dāng)前激活的工具,并將鼠標(biāo)和鍵盤(pán)事件轉(zhuǎn)發(fā)給當(dāng)前激活的工具,以及處理命令棧。GEF提供了缺省實(shí)現(xiàn),DefaultEditDomain
,你應(yīng)該在編輯器的構(gòu)造函數(shù)中設(shè)置它的實(shí)例。
圖形編輯器的另一部分工作是創(chuàng)建并初始化圖形viewer。圖形viewer是一種特殊的EditPartViewer
,它能夠做點(diǎn)擊測(cè)試。我們可以使用GraphicalEditor
類(lèi)提供的缺省viewer。然而,還是需要做一些事。在configureGraphicalViewer
方法
中設(shè)置編輯部件的工廠類(lèi)。這個(gè)工廠類(lèi)必須實(shí)現(xiàn)一個(gè)接口EditPartFactory,這個(gè)接口只有一個(gè)方法,createEditPart(EditPart, Object)。它的第一個(gè)參數(shù)是編輯部件,它一般是所創(chuàng)建的編輯部件的父部件,第二個(gè)參數(shù)是新創(chuàng)建的編輯部件所對(duì)應(yīng)的模型部件。其他要做的包括設(shè)置鍵處理器,上下文菜單等。
一旦工廠類(lèi)被設(shè)置,你應(yīng)該在圖形viewer中
設(shè)置內(nèi)容。內(nèi)容自然就是從IEditorInput
實(shí)例恢復(fù)得到的對(duì)象,IEditorInput
實(shí)例通過(guò)setInput
方法傳遞給編輯器。這個(gè)例子在圖形viewer上添加
一個(gè)目標(biāo)放置監(jiān)聽(tīng)器。它允許用戶使用拖放的方式添加新圖形,而不是選擇加點(diǎn)擊的方式。這個(gè)目標(biāo)放置監(jiān)聽(tīng)器使用TemplateTransferDropTargetListener
的子類(lèi),它使用CreateRequest
來(lái)獲得添加對(duì)象到模型的命令,這個(gè)模型當(dāng)然就是拖放動(dòng)作結(jié)束時(shí)所在的編輯部件所表示的模型。
除了上面談到的任務(wù),編輯器還負(fù)責(zé)監(jiān)視命令棧來(lái)報(bào)告當(dāng)前編輯的內(nèi)容是否被修改。這是一個(gè)比較好的解決方法,因?yàn)樗梢允惯@個(gè)標(biāo)記和用戶所做的undo和redo同步起來(lái)。注意,命令棧含有上次存儲(chǔ)的位置信息,這個(gè)信息在doSave
和doSaveAs
這兩個(gè)方法中被標(biāo)記。編輯器的其他細(xì)節(jié),比如模型的實(shí)際存儲(chǔ)和恢復(fù),這里就不討論了,因?yàn)樗鼈兒途唧w的應(yīng)用相關(guān)。接下來(lái),我們討論編輯器的如何將編輯器內(nèi)容暴露給其視圖,如何將菜單選項(xiàng)和編輯器的action聯(lián)系起來(lái),以及其他workbench協(xié)作的技術(shù)。
和workbench集成在一起
到目前為止,我們談的都是這個(gè)幾何圖形編輯器如何工作。然而,它沒(méi)有和workbench很好地集成。例如,Edit菜單動(dòng)作,比如Delete,Undo和Redo,就不能工作。其他視圖不能用其他方式顯示編輯器內(nèi)容。換句話說(shuō),目前所完成的編輯器沒(méi)有很好地利用Eclipse workbench的優(yōu)勢(shì)。在下面的三小節(jié),將解釋如何將這個(gè)孤立的編輯器變成workbench的一部分。
編輯器Action
ShapesEditor
類(lèi)創(chuàng)建了大量缺省動(dòng)作,它們?cè)诰庉嬈鞒跏蓟^(guò)程中被createActions
方法中創(chuàng)建。這些動(dòng)作是undo,redo,select all,delete,save和print。為了將標(biāo)準(zhǔn)菜單選項(xiàng)連接到這些動(dòng)作,你應(yīng)該在plugin.xml
文件中定義一個(gè)action bar contributor。在這個(gè)action bar contributor中,你需要實(shí)現(xiàn)兩個(gè)方法。第一個(gè)是
buildActions
,它可以為undo,redo和delete創(chuàng)建可重定位的動(dòng)作。如果你需要使用鍵盤(pán)選擇所有的widget,你需要在第二個(gè)方法declareGlobalActionKeys
中為所選擇的動(dòng)作
添加一個(gè)全局動(dòng)作關(guān)鍵字。
我們來(lái)仔細(xì)看一下當(dāng)用戶在Edit菜單中選擇Delete時(shí)發(fā)生了些什么(看圖12)。ShapesEditor
類(lèi)的父類(lèi)將刪除動(dòng)作添加到動(dòng)作注冊(cè)表中。當(dāng)刪除動(dòng)作被執(zhí)行時(shí),它檢查當(dāng)前的所選擇的對(duì)象是否是EditPart
類(lèi)的實(shí)例。對(duì)每個(gè)對(duì)象,它都從編輯部件中請(qǐng)求一個(gè)命令。接下來(lái),每個(gè)編輯部件檢查是否有編輯策略可以處理刪除請(qǐng)求。對(duì)幾何圖形,ShapeComponentEditPolicy
可以處理刪除請(qǐng)求,并且返回ShapeDeleteCommand
實(shí)例。動(dòng)作執(zhí)行該命令,從而將圖形從圖中刪除。圖發(fā)送一個(gè)屬性修改事件,DiagramEditPart
收到該事件,最終使得代表被刪除圖形的矩形或橢圓從顯示中被刪除。
提供屬性
每個(gè)圖形編輯器都是可以發(fā)送選擇事件。你可以建立一個(gè)視圖,并將它作為選擇監(jiān)聽(tīng)器注冊(cè)在workbench site的頁(yè)面上。每次你在圖形編輯器中選擇一個(gè)對(duì)象,你的視圖都會(huì)在selectionChanged
方法中收到一個(gè)通知。Eclipse的一個(gè)標(biāo)準(zhǔn)視圖,Properties視圖,會(huì)監(jiān)聽(tīng)選擇事件,并且每次都檢查這個(gè)對(duì)象是否實(shí)現(xiàn)了IPropertySource
接口。如果是的話,它使用這個(gè)接口的方法來(lái)查詢所選擇的對(duì)象屬性,并以表格的方式顯示出來(lái)。
通過(guò)上面所描述的,在圖形編輯器中提供對(duì)象的屬性只要實(shí)現(xiàn)IPropertySource
接口就可以了。通過(guò)查看Shape
類(lèi),你可以看到對(duì)象的位置和大小是如何在Properties視圖中顯示的。
提供Outline
Outline視圖是另一種,常常也是更簡(jiǎn)潔的查看模型對(duì)象的方式。在Java編輯器中,它可以用來(lái)顯示一個(gè)類(lèi)所import的類(lèi),所包含的變量,和方法,卻不需要用戶深入代碼。圖形編輯器也可以使用這個(gè)視圖。圖形編輯器和邏輯電路編輯器類(lèi)似,可以以樹(shù)的方式顯示所編輯的內(nèi)容(看圖1)。數(shù)據(jù)庫(kù)schema編輯器[7]也提供了類(lèi)似的視圖。
為了將所編輯的內(nèi)容提供給Outline視圖,你需要重寫(xiě)getAdapter
方法,并當(dāng)adapter類(lèi)為IContentOutlinePage
接口時(shí),返回一個(gè)outline實(shí)現(xiàn)。實(shí)現(xiàn)outline的最簡(jiǎn)單的方法是擴(kuò)展ContentOutlinePage
類(lèi),并提供適當(dāng)?shù)?code>EditPartViewer。
在我們這個(gè)例子中,編輯部件視圖是有一個(gè)TreeViewer實(shí)現(xiàn)的。你應(yīng)該和主編輯器一樣提供給它同樣的編輯域。TreeViewer,就象其他EditPartViewer
,需要一個(gè)創(chuàng)建子編輯部件的方法。編輯器和DiagramEditPart
一樣,都是設(shè)置一個(gè)編輯部件工廠。此外,outline中的選擇和主編輯窗口的選擇需要通過(guò)選擇同步器同步起來(lái),選擇同步器是一個(gè)GEF工具類(lèi),它協(xié)調(diào)兩個(gè)編輯部件的選擇狀態(tài)。ShapesTreeEditPartFactory
根據(jù)模型類(lèi)型,返回ShapeTreeEditPart
或DiagramTreeEditPart
的實(shí)例。通過(guò)這些類(lèi),讀者應(yīng)該可以輕易地發(fā)現(xiàn)這些模式很熟悉。兩個(gè)編輯部件都實(shí)現(xiàn)了PropertyChangeListener
接口,并通過(guò)調(diào)整模型的可視化表示來(lái)對(duì)屬性變化做出響應(yīng)。它們都安裝編輯策略來(lái)控制通過(guò)它們所暴露的交互類(lèi)型。
GEF用到的設(shè)計(jì)模式
GEF通過(guò)大量使用設(shè)計(jì)模式來(lái)得到它的靈活性。下面是一下經(jīng)常碰到的模式的小結(jié)。詳細(xì)內(nèi)容,請(qǐng)參考 [2]。
- 模型-視圖-控制器(Model-View-Controller )
- MVC模式被GEF用來(lái)解除用戶界面,行為和表示之間的耦合。模型可以用任何Java對(duì)象來(lái)表示。視圖必須實(shí)現(xiàn)
IFigure
接口。控制的類(lèi)型必須是EditPart
或它的子類(lèi)。
- 命令(Command )
- 命令封裝了模型修改,因此支持可撤銷(xiāo)的操作。
- 責(zé)任鏈(Chain of Responsibility )
- 責(zé)任鏈通過(guò)將請(qǐng)求傳遞給多個(gè)對(duì)象,并給這些對(duì)象機(jī)會(huì)處理請(qǐng)求,從而將請(qǐng)求的發(fā)送者和接受者解除耦合。在GEF中,多個(gè)編輯策略可以收到請(qǐng)求,返回
Commands
,這些Commands
以鏈的方式組織在一起。
- 狀態(tài)(State )
- 允許編輯器在內(nèi)部狀態(tài)發(fā)生改變的時(shí)候,修改編輯器的行為。對(duì)于GEF編輯器,用戶切換工具可以改變編輯器的狀態(tài)。例如,對(duì)于鼠標(biāo)按下事件,編輯器在激活選區(qū)工具和激活創(chuàng)建工具下的行為是截然不同的。
- 抽象工廠(Abstract Factory )
- 提供接口創(chuàng)建一系列相關(guān)或相依賴的對(duì)象。這個(gè)模式在根據(jù)模型部件創(chuàng)建編輯部件時(shí)被使用。
- 工廠方法(Factory Method )
- 定義了方法創(chuàng)建對(duì)象,但是允許子類(lèi)決定實(shí)例化的類(lèi)。這個(gè)模式?jīng)]有被單獨(dú)討論,但是它是創(chuàng)建編輯部件的另一種可選的方法。
createChild
方法允許你不使用工廠就創(chuàng)建子編輯部件。
總結(jié)
我希望能夠?qū)@個(gè)簡(jiǎn)單圖形編輯器的大多數(shù)方面作詳細(xì)的描述。提供足夠的信息使得人們能夠讀完這篇文章,去看更大的例子,比如邏輯電路編輯器。通過(guò)理解象CircuitEditPart
,AndGateFigure和其他類(lèi)的角色,你可以關(guān)注其他例子的更復(fù)雜的方面。在GEF的眾多領(lǐng)域和技術(shù)中,有很多我甚至都沒(méi)有涉及過(guò)。然而,這些技術(shù)只有在很好地理解基礎(chǔ)內(nèi)容的情況下,才可能去學(xué)習(xí)。畢竟,如果你為了使Select All菜單項(xiàng)工作都要花數(shù)小時(shí),那么設(shè)計(jì)一個(gè)拖反饋的目的又是什么呢?
感謝
我想感謝Randy Hudson,他的意見(jiàn)幫助提高了本文結(jié)構(gòu)和準(zhǔn)確性。我也感謝Jill Sueoka仔細(xì)檢查我所寫(xiě)一個(gè)又一個(gè)版本。