構造一個GEF應用程式通常分為這麼幾個步驟:設計模型、設計EditPart和Figure、設計EditPolicy和Command,其中 EditPart是最主要的一部分,因為在實現它的時候不可避免的要使用到EditPolicy,而後者又涉及到Command。
現在我們來看個例子,它的功能非常簡單,使用者可以在畫布上增加節點(Node)和節點間的串連,可以直接編輯節點的名稱以及改變節點的位置,使用者可以撤消/重做任何操作,有一個樹狀的大綱視圖和一個屬性頁面。點此下載(Update: For Eclipse 3.1的版本),這是一個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、屬性工作表和大綱視圖等一些功能沒有講解,下面的文章裡將介紹這些常用功能的實現。