簡介: 本文從設(shè)計(jì)模式的角度出發(fā),通過解析關(guān)鍵應(yīng)用場景,深層次地介紹了圖形編輯框架 (Graphical EditingFramework, GEF) 涉及的大量概念和技術(shù)。本文主要涉及 MVC、命令、工廠、觀察者、職責(zé)鏈、狀態(tài)等模式。通過本文,希望能夠幫助Eclipse RCP 開發(fā)者更好地理解和應(yīng)用 GEF 這一框架。
發(fā)布日期: 2010 年 6 月 03 日
級別: 中級
訪問情況 240 次瀏覽
建議: 0 (添加評論)
圖形編輯框架 (Graphical Editing Framework, GEF) ,是 Eclipse平臺下一個重要的框架,用來從應(yīng)用模型開發(fā)富圖形化的編輯器,是 Eclipse RCP 開發(fā)者的神兵利器。 GEF框架涉及大量的概念和技術(shù),有著非常陡峭的學(xué)習(xí)曲線。本文從設(shè)計(jì)模式的角度出發(fā),解析 GEF 框架中的關(guān)鍵應(yīng)用場景,希望能夠幫助 EclipseRCP 開發(fā)者更好地理解和應(yīng)用這一框架。
GEF 通過大量使用設(shè)計(jì)模式來獲取它的靈活性。除了 MVC 模式,GEF 最經(jīng)常用到的設(shè)計(jì)模式是命令、工廠、觀察者、職責(zé)鏈和狀態(tài)。
本文示例代碼來自于 GEF 的 3.4.1 版本。
模型-視圖-控制器 (Model-view-controller, MVC)
GEF 框架嚴(yán)格遵循模型-視圖-控制器模式 (MVC) 。
GEF 中的模型可以是任意的數(shù)據(jù)。模型使用一種能在模型改變時通知控制器處理的事件通知機(jī)制。這種模型可以由手工來實(shí)現(xiàn),也可以通過 EMF(Eclipse Modeling Framework) 自動生成。而對模型的修改一般由 Command 來完成。
EditParViewer 是 GEF 中的展現(xiàn)視圖的地方。常見的 EditParViewer 有兩種:GraphicalViewer 和 TreeViewer。GraphicalViewer 主要依靠 Draw2d 中的 Figure來完成的。開發(fā)人員可以通過實(shí)現(xiàn) IFigure 接口來完成復(fù)雜圖形的設(shè)計(jì)。對于 TreeViewer 而言,則由 SWT 中的 Tree 和TreeItem 來完成視圖的繪制。
EditPart 對應(yīng) MVC 模式中的控制器,它維護(hù)著視圖與模型的對應(yīng)關(guān)系。在 AbstractGraphicalEditPart中,createFigure 方法負(fù)責(zé)創(chuàng)建 Figure 圖形,refreshVisuals 方法負(fù)責(zé)對 Figure圖形進(jìn)行更新。一般情況下,模型與 EditPart 是一一對應(yīng)的。模型數(shù)據(jù)的更新由 EditPart 所安裝的編輯策略產(chǎn)生的 Command來完成。GEF 框架中的常見的 EditPart 實(shí)現(xiàn)有三種,分別是 GraphicalEditPart,ConnecitonEditPart 和TreeEditPart。
GEF 不會直接修改模型,而是要求使用命令來做實(shí)際的修改。通過命令,實(shí)現(xiàn)對模型或模型屬性的修改和撤銷。這樣,GEF 編輯器就自動支持了模型修改的 undo/redo。
Command 類是 GEF 中的一個抽象類,主要實(shí)現(xiàn)如下幾個方法:
每個編輯策略都會為請求返回一個命令,不希望處理請求的策略將返回一個 null。GEF 通過一個命令堆棧 (CommandStack) 執(zhí)行和保存 Command 對象。用戶通過命令堆??梢暂p松撤銷或重復(fù)對模型所做的操作。
工廠模式是用于將生成對象的步驟進(jìn)行封裝的創(chuàng)建型模式。常見的形態(tài)有以下幾種:
在 GEF 中,F(xiàn)igure 的創(chuàng)建應(yīng)用了工廠方法模式。抽象類 AbstractGraphicalEditPart擔(dān)當(dāng)抽象工廠角色,定義了生成 Figure 的抽象方法 createFigure()。具體工廠角色則有AbstractGraphicalEditPart 的子類擔(dān)當(dāng),負(fù)責(zé)生成具體的編輯器圖形。
EditPart 實(shí)例對象的創(chuàng)建則運(yùn)用了抽象工廠模式。所有的 EditPart 均由 EditPartFactory的子類負(fù)責(zé)創(chuàng)建,GEF 自身就提供了 RulerEditPartFactory 和 PaletteEditPartFactory兩個工廠實(shí)現(xiàn)。如果用戶自定義 EditPart,必須提供相應(yīng)的 EditPartFactory 類型才能正確創(chuàng)建用戶的 EditPart 對象。
工廠方法和抽象工廠之間的區(qū)別在于,工廠方法模式只有一個抽象產(chǎn)品類,而抽象工廠模式有多個。工廠方法模式的具體工廠類只能創(chuàng)建一個具體產(chǎn)品類的實(shí)例,而抽象工廠模式可以創(chuàng)建多個。
觀察者模式是一種對象行為型模式,它可以定義對象間的一種一對多的依賴關(guān)系,當(dāng)被依賴對象的狀態(tài)發(fā)生改變時,所有依賴于它的對象都得到通知并被自動更新。
通過選項(xiàng)欄創(chuàng)建連接時,ConnectionCreationTool工具被生成用來創(chuàng)建連接,它通過監(jiān)聽鼠標(biāo)的按下動作來設(shè)置連接的連接源,在設(shè)置連接源時,通過 addEditPartListener 方法為源EditPart 添加 deactivationListener 監(jiān)聽。
package org.eclipse.gef.tools; public class AbstractConnectionCreationTool extends TargetingTool{ ...... private EditPartListener.Stub deactivationListener = new EditPartListener.Stub() { public void partDeactivated(EditPart editpart) { handleSourceDeactivated(); } }; protected boolean handleButtonDown(int button) { if (isInState(STATE_INITIAL) && button == 1) { updateTargetRequest(); updateTargetUnderMouse(); setConnectionSource(getTargetEditPart()); Command command = getCommand(); ((CreateConnectionRequest)getTargetRequest()).setSourceEditPart( getTargetEditPart()); if (command != null) { setState(STATE_CONNECTION_STARTED); setCurrentCommand(command); viewer = getCurrentViewer(); } } if (isInState(STATE_INITIAL) && button != 1) { setState(STATE_INVALID); handleInvalidInput(); } return true; } protected void setConnectionSource(EditPart source) { if (connectionSource != null) connectionSource.removeEditPartListener(deactivationListener); connectionSource = source; if (connectionSource != null) connectionSource.addEditPartListener(deactivationListener); } protected void handleSourceDeactivated() { setState(STATE_INVALID); handleInvalidInput(); handleFinished(); } } |
deactivationListener 是 EditPartListener.Stub 類型的實(shí)例,用來觀察源 EditPart的狀態(tài)。當(dāng)源 EditPart 的狀態(tài)由激活變?yōu)榉羌せ顣r,及時通知工具 ConnectionCreationTool 做出停止創(chuàng)建連接的工作。
package org.eclipse.gef.editparts; public abstract class AbstractEditPart implements EditPart, RequestConstants, IAdaptable { ... ... protected void fireDeactivated() { Iterator listeners = getEventListeners(EditPartListener.class); while (listeners.hasNext()) ((EditPartListener)listeners.next()). partDeactivated(this); } /** * Adds an EditPartListener. * @param listener the listener */ public void addEditPartListener(EditPartListener listener) { eventListeners.addListener(EditPartListener.class, listener); } public void removeEditPartListener(EditPartListener listener) { eventListeners.removeListener(EditPartListener.class, listener); } } |
在這個過程中,EditPart 成為被觀察的目標(biāo),提供了注冊和刪除觀察者對象的接口。EditPartListener.Stub類型的實(shí)例 deactivationListener 扮演了觀察者的角色,在目標(biāo) EditPart 的狀態(tài)變成非激活時,獲取更新并通知ConnectionCreationTool 取消連接的創(chuàng)建。
職責(zé)鏈 (Chain of Responsibility)
職責(zé)鏈?zhǔn)且环N對象行為型模式。請求發(fā)出后,將在候選對象鏈 ( 職責(zé)鏈 )中進(jìn)行傳遞,并有滿足條件的對象進(jìn)行處理。職責(zé)鏈模式降低了請求的發(fā)送者和接收者之間的耦合度,允許在運(yùn)行時對職責(zé)鏈進(jìn)行動態(tài)的增加或修改以增加或改變處理請求的職責(zé)。關(guān)于職責(zé)鏈模式更詳細(xì)的描述,請參考 GOF 《設(shè)計(jì)模式》一書。
在 GEF 中,Tools 或者其他的 UI 解釋程序?qū)⒂脩舻木庉嫴僮鬓D(zhuǎn)換為一系列的請求 (Request),比如,用戶在選項(xiàng)板(Palette) 里選擇了創(chuàng)建節(jié)點(diǎn)工具 (CreationTool),然后在畫布區(qū)域按下鼠標(biāo)左鍵,這時產(chǎn)生在畫布上的鼠標(biāo)單擊事件將被CreationTool 轉(zhuǎn)換為一個 CreateRequest,它里面包含了要創(chuàng)建的對象,坐標(biāo)位置等信息。
GEF 已經(jīng)為我們提供了很多種類的 Request,其中最常用的是 CreateRequest 及其子類 CreateConnectionRequest,下圖列出了 GEF 中已經(jīng)實(shí)現(xiàn)的 Request.
Editparts 不能直接處理編輯操作產(chǎn)生的 Request,而是通過安裝的對應(yīng) EditPolicy來處理。EditPolicy 的主要功能是根據(jù)請求創(chuàng)建相應(yīng)的命令 (Command),而后者會直接操作模型對象。每個 EditPolicy專注于一個單一的編輯任務(wù)或相關(guān)任務(wù)組,這使得一些編輯操作可以在不同 EditPart 實(shí)現(xiàn)共享。EditPolicies 決定了一個EditPart 的編輯能力。
EditPart 在創(chuàng)建時,調(diào)用方法 createEditPolicies()來安裝一些適用的編輯策略。在示例代碼中,ConnectionEditPart 安裝了兩個 EditPolicy。第一個是ConnectionComponentPolicy,它給 Delete 菜單項(xiàng)所需要的 action 提供刪除命令。第二個是ConnectionEndpointEditPolicy,用來提供連接 (Connection) 轉(zhuǎn)移的策略。
class ConnectionEditPart extends AbstractConnectionEditPart implements PropertyChangeListener { ... protected void createEditPolicies() { installEditPolicy(EditPolicy.CONNECTION_ROLE, new ConnectionEditPolicy() { protected Command getDeleteCommand(GroupRequest request) { return new ConnectionDeleteCommand(getCastedModel()); } }); installEditPolicy(EditPolicy.CONNECTION_ENDPOINTS_ROLE, new ConnectionEndpointEditPolicy()); } ... } |
請求提交到選定的 editpart 后,通過 EditPolicy 的職責(zé)鏈進(jìn)行處理。從第一個 EditPolicy開始,鏈中收到請求的 EditPolicy 確定是否可以處理它,否則轉(zhuǎn)發(fā)給鏈中的下一個 editpolicy。EditPolicy的聲明順序決定了請求被傳遞的順序。多個編輯策略可以收到請求,返回 Commands 作為響應(yīng),這些 Commands以鏈的方式組織在一起。示例代碼描述了 AbstractEditPart 中的職責(zé)鏈工作方式。
package org.eclipse.gef.editparts; public abstract class AbstractEditPart implements EditPart, RequestConstants, IAdaptable { ...... public Command getCommand(Request request) { Command command = null; EditPolicyIterator i = getEditPolicyIterator(); while (i.hasNext()) { if (command != null) command = command.chain(i.next().getCommand(request)); else command = i.next().getCommand(request); } return command; } } |
同職責(zé)鏈模式一樣,狀態(tài) (State) 也是一種對象行為型模式。狀態(tài)模式允許一個對象在其內(nèi)部狀態(tài)改變時改變它的行為。上下文(context)把狀態(tài)相關(guān)的行為委托到狀態(tài)對象上。對象通過上下文引用不同的狀態(tài)對象,在運(yùn)行時根據(jù)狀態(tài)改變它的行為。關(guān)于狀態(tài)模式更詳細(xì)的描述,請參考 GOF《設(shè)計(jì)模式》一書。
在 GEF 的編輯器中,用戶在選項(xiàng)板 (Palette)切換工具可以改變編輯器的狀態(tài),從而修改編輯器的行為。例如,對于鼠標(biāo)按下事件,編輯器在激活選區(qū)工具和激活創(chuàng)建工具下的行為是截然不同的。現(xiàn)在,我們就來看一下 GEF 編輯器是如何根據(jù)當(dāng)前選中的 Tool 來改變行為的。
在每個 GEF 的 Editor 里,都需要有一個 EditDomain 的存在。EditDomain 類似于GraphicalEditor 的執(zhí)行上下文環(huán)境,維護(hù)著 GEF 中的命令棧、負(fù)責(zé)事件通知等。在 EditDomain 中,通過setActiveTool 可以設(shè)置當(dāng)前處于 Active 狀態(tài)的 Tool。
EditDomain 類維護(hù)一個表示鼠標(biāo)和鍵盤輸入的工具對象 ( 一個 Tool 接口實(shí)現(xiàn)類的實(shí)例 )。EditDomain類將所有與視圖輸入相關(guān)的請求委托給這個工具對象。EditDomain 類使用 Tool接口實(shí)現(xiàn)類的實(shí)例來執(zhí)行特定于視圖輸入的操作。在狀態(tài)模式中,EditDomain 對應(yīng)上下文環(huán)境,工具 (Tool) 對應(yīng)狀態(tài)。一旦 ActiveTool 改變,EditDomain 對象就會改變它所使用的工具對象。
需要注意的是,上圖關(guān)于 Tool 的繼承層次部分并不是嚴(yán)格按照 GEF 框架進(jìn)行描述,本文作者為了描述方便做了某種程度的簡化。具體層次請參考 GEF 框架代碼。
示例代碼描述了 EditDomain 是如何將與視圖輸入相關(guān)的請求委托給它的 Tool 實(shí)例 activeTool。
package org.eclipse.gef; public class EditDomain { ...... private Tool activeTool; private void handlePaletteToolChanged() { PaletteViewer paletteViewer = getPaletteViewer(); if (paletteViewer != null) { ToolEntry entry = paletteViewer.getActiveTool(); if (entry != null) setActiveTool(entry.createTool()); else setActiveTool(getDefaultTool()); } } public void setActiveTool(Tool tool) { if (activeTool != null) activeTool.deactivate(); activeTool = tool; if (activeTool != null) { activeTool.setEditDomain(this); activeTool.activate(); } } } |
工具會執(zhí)行某些操作,這些操作可能包括:
GEF 出現(xiàn)的模式遠(yuǎn)不止我們列出來的這么多。我們只是列出了對 GEF 框架理解有幫助的一些模式。本文作者從事 Eclipse RCP 開發(fā)多年,通過這篇文章,希望能將在 GEF 中體會到的一些設(shè)計(jì)思想與大家分享。