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