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