構(gòu)造一個GEF應(yīng)用程序通常分為這么幾個步驟:設(shè)計模型、設(shè)計EditPart和Figure、設(shè)計EditPolicy和Command,其中EditPart是最主要的一部分,因為在實現(xiàn)它的時候不可避免的要使用到EditPolicy,而后者又涉及到Command。
現(xiàn)在我們來看個例子,它的功能非常簡單,用戶可以在畫布上增加節(jié)點(Node)和節(jié)點間的連接,可以直接編輯節(jié)點的名稱以及改變節(jié)點的位置,用戶可以撤消/重做任何操作,有一個樹狀的大綱視圖和一個屬性頁。點此下載,這是一個Eclipse的項目打包文件,在Eclipse里導(dǎo)入后運行Run-time Workbench,新建一個擴展名為"gefpractice"的文件就會打開這個編輯器。
圖1 Practice Editor的使用界面
你可以參考著代碼來看接下來的內(nèi)容了,讓我們從模型開始說起。模型是根據(jù)應(yīng)用需求來設(shè)計的,所以我們的模型包括代表整個圖的Diagram、代表節(jié)點的Node和代表連接的Connection這些對象。我們知道,模型是要負責把自己的改變通知給EditPart的,為了把這個功能分離出來,我們使用名為Element的抽象類專門來實現(xiàn)通知機制,然后讓其他模型類繼承它。Element類里包括一個PropertyChangeSupport類型的成員變量,并提供了addPropertyChangeListener()、removePropertyChangeListener()和fireXXX()方法分別用來注冊監(jiān)聽器和通知監(jiān)聽器模型改變事件。在GEF里,模型的監(jiān)聽器就是EditPart,在EditPart的active()方法里我們會把它作為監(jiān)聽器注冊到模型中。所以,總共有四個類組成了我們的模型部分。
在前面的貼子里說過,大部分GEF應(yīng)用程序都是實現(xiàn)為Editor的,這個例子也不例外,對應(yīng)的Editor名為PracticeEditor。這個Editor繼承了GraphicalEditorWithPalette類,表示它是一個具有調(diào)色板的圖形編輯器。最重要的兩個方法是configureGraphicalViewer()和initializeGraphicalViewer(),分別用來定制和初始化EditPartViewer(關(guān)于EditPartViewer的作用請查看前面的帖子),簡單查看一下GEF的代碼你會發(fā)現(xiàn),在GraphicalEditor類里會先后調(diào)用這兩個方法,只是中間插了一個hookGraphicalViewer()方法,其作用是同步選擇和把EditPartViewer作為SelectionProvider注冊到所在的site(Site是Workbench的概念,請查Eclipse幫助)。所以,與選擇無關(guān)的初始化操作應(yīng)該在前者中完成,否則放在后者完成。例子中,在這兩個方法里我們配置了RootEditPart、用于創(chuàng)建EditPart的EditPartFactory、Contents即Diagram對象和增加了拖放支持,拖動目標是當前EditPartViewer,后面會看到拖動源就是調(diào)色板。
這個Editor是帶有調(diào)色板的,所以要告訴GEF我們的調(diào)色板里都有哪些工具,這是通過覆蓋getPaletteRoot()方法來實現(xiàn)的。在這個方法里,我們利用自己寫的一個工具類PaletteFactory構(gòu)造一個PaletteRoot對象并返回,我們的調(diào)色板里需要有三種工具:選擇工具、節(jié)點工具和連接工具。在GEF里,調(diào)色板里可以有抽屜(PaletteDrawer)把各種工具歸類放置,每個工具都是一個ToolEntry,選擇工具(SelectionToolEntry)和連接工具(ConnectionCreationToolEntry)是預(yù)先定義好的幾種工具中的兩個,所以可以直接使用。對于節(jié)點工具,要使用CombinedTemplateCreationEntry,并把節(jié)點類型作為參數(shù)之一傳給它,創(chuàng)建節(jié)點工具的代碼如下所示。
ToolEntry tool = new CombinedTemplateCreationEntry("Node", "Create a new Node", Node.class, new SimpleFactory(Node.class), null, null);
在新的3.0版本GEF里還提供了一種可以自動隱藏調(diào)色板的編輯器GraphicalEditorWithFlyoutPalette,對調(diào)色板的外觀有更多選項可以選擇,以后的帖子里可能會提到如何使用。
調(diào)色板的初始化操作應(yīng)該放在initializePaletteViewer()里完成,最主要的任務(wù)是為調(diào)色板所在的EditPartViewer添加拖動源事件支持,前面我們已經(jīng)為畫布所在EditPartViewer添加了拖動目標事件,所以現(xiàn)在就可以實現(xiàn)完整的拖放操作了。這里稍微講解一下拖放的實現(xiàn)原理,以用來創(chuàng)建節(jié)點對象的節(jié)點工具為例,它在調(diào)色板里是一個CombinedTemplateCreationEntry,在創(chuàng)建這個PaletteEntry時(見上面的代碼)我們指定該對象對應(yīng)一個Node.class,所以在用戶從調(diào)色板里拖動這個工具時,內(nèi)存里有一個TemplateTransfer單例對象會記錄下Node.class(稱作template),當用戶在畫布上松開鼠標時,拖放結(jié)束的事件被觸發(fā),將由畫布注冊的DiagramTemplateTransferDropTargetListener對象來處理template對象(現(xiàn)在是Node.class),在例子中我們的處理方法是用一個名為ElementFactory的對象負責根據(jù)這個template創(chuàng)建一個對應(yīng)類型的實例。
以上我們建立了模型和用于實現(xiàn)視圖的Editor,因為模型的改變都是由Command對象直接修改的,所以下面我們先來看都有哪些Command。由需求可知,我們對模型的操作有增加/刪除節(jié)點、修改節(jié)點名稱、改變節(jié)點位置和增加/刪除連接等,所以對應(yīng)就有CreateNodeCommand、DeleteNodeCommand、RenameNodeCommand、MoveNodeCommand、CreateConnectionCommand和DeleteConnectionCommand這些對象,它們都放歸類在commands包里。一個Command對象里最重要的當然是execute()方法了,也就是執(zhí)行命令的方法。除此以外,因為要實現(xiàn)撤消/重做功能,所以在Command對象里都有Undo()和Redo()方法,同時在Command對象里要有成員變量負責保留執(zhí)行該命令時的相關(guān)狀態(tài),例如RenameNodeCommand里要有oldName和newName兩個變量,這樣才能正確的執(zhí)行Undo()和Redo()方法,要記住,每個被執(zhí)行過的Command對象實例都是被保存在EditDomain的CommandStack中的。
例子里的EditPolicy都放在policies包里,與圖形有關(guān)的(GraphicalEditPart的子類)有DiagramLayoutEditPolicy、NodeDirectEditPolicy和NodeGraphicalNodeEditPolicy,另外兩個則是與圖形無關(guān)的編輯策略。可以看到,在后一種類型的兩個類(ConnectionEditPolicy和NodeEditPolicy)中我們只覆蓋了createDeleteCommand()方法,該方法用于創(chuàng)建一個負責"刪除"操作的Command對象并返回,要搞清這個方法看似矛盾的名字里create和delete是對不同對象而言的。
有了Command和EditPolicy,現(xiàn)在可以來看看EditPart部分了。每一個模型對象都對應(yīng)一個EditPart,所以我們的三個模型對象(Element不算)分別對應(yīng)DiagramPart、ConnectionPart和NodePart。對于含有子元素的EditPart,必須覆蓋getModelChildren()方法返回子對象列表,例如DiagramPart里這個方法返回的是Diagram對象包含的Node對象列表。
每個EditPart都有active()和deactive()兩個方法,一般我們在前者里注冊監(jiān)聽器(因為實現(xiàn)了PropertyChangeListener接口,所以EditPart本身就是監(jiān)聽器)到模型對象,在后者里將監(jiān)聽器從列表里移除。在觸發(fā)監(jiān)聽器事件的propertyChange()方法里,一般是根據(jù)"事件名"稱決定使用何種方式刷新視圖,例如對于NodePart,如果是節(jié)點本身的屬性發(fā)生變化,則調(diào)用refreshVisuals()方法,若是與它相關(guān)的連接發(fā)生變化,則調(diào)用refreshTargetConnections()或refreshSourceConnections()。這里用到的事件名稱都是我們自己來規(guī)定的,在例子中比如Node.PROP_NAME表示節(jié)點的名稱屬性,Node.PROP_LOCATION表示節(jié)點的位置屬性,等等。
EditPart(確切的說是AbstractGraphicalEditpart)另外一個需要實現(xiàn)的重要方法是createFigure(),這個方法應(yīng)該返回模型在視圖中的圖形表示,是一個IFigure類型對象。一般都把這些圖形放在figures包里,例子里只有NodeFigure一個自定義圖形,Diagram對象對應(yīng)的是GEF自帶的名為FreeformLayer的圖形,它是一個可以在東南西北四個方向任意擴展的層圖形;而Connection對應(yīng)的也是GEF自帶的圖形,名為PolylineConnection,這個圖形缺省是一條用來連接另外兩個圖形的直線,在例子里我們通過setTargetDecoration()方法讓連接的目標端顯示一個箭頭。
最后,要為EditPart增加適當?shù)腅ditPolicy,這是通過覆蓋EditPart的createEditPolicies()方法來實現(xiàn)的,每一個被"安裝"到EditPart中的EditPolicy都對應(yīng)一個用來表示角色(Role)的字符串。對于在模型中有子元素的EditPart,一般都會安裝一個EditPolicy.LAYOUT_ROLE角色的EditPolicy(見下面的代碼),后者多為LayoutEditPolicy的子類;對于連接類型的EditPart,一般要安裝EditPolicy.CONNECTION_ENDPOINTS_ROLE角色的EditPolicy,后者則多為ConnectionEndpointEditPolicy或其子類,等等。
installEditPolicy(EditPolicy.LAYOUT_ROLE, new DiagramLayoutEditPolicy());
用戶的操作會被當前工具(缺省為選擇工具SelectionTool)轉(zhuǎn)換為請求(Request),請求根據(jù)類型被分發(fā)到目標EditPart所安裝的EditPolicy,后者根據(jù)請求對應(yīng)的角色來判斷是否應(yīng)該創(chuàng)建命令并執(zhí)行。
在以前的帖子里說過,Role-EditPolicy-Command這樣的設(shè)計主要是為了盡量重用代碼,例如同一個EditPolicy可以被安裝在不同EditPart中,而同一個Command可以被不同的EditPolicy所使用,等等。當然,凡事有利必有弊,我認為這種的設(shè)計也有缺點,首先在代碼上看來不夠直觀,你必須對眾多Role、EditPolicy有所了解,增加了學(xué)習(xí)周期;另外大部分不需要重用的代碼也要按照這個相對復(fù)雜的方式來寫,帶來了額外工作量。
以上就是一個GEF應(yīng)用程序里最基本的幾個組成部分,例子中還有如Direct Edit、屬性表和大綱視圖等一些功能沒有講解,下面的帖子里將介紹這些常用功能的實現(xiàn)。