構造一個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、屬性表 和大綱視圖等一些功能沒有講解,下面的帖子裡將介紹這些常用功能的實現。