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