兩天前GEF發布了3.1M7版本,但使用下來發現和M6沒有什麼區別,是不是主要為了和 Eclipse版本相配套?希望3.1正式版早日發布,應該會新增不少內容。上一篇帖子介紹了如 何實現表格功能,在開發過程中,另一個經常用到的功能就是樹,雖然SWT提供了標准的樹控 件,但使用它完成如組織結構圖這樣的應用還是不夠直觀和方便。在目前版本(3.1M7)的 GEF中雖然沒有直接支持樹的實現,但Draw2D提供的例子程序裡卻有我們可以利用的代碼 (org.eclipse.draw2d.examples.tree.TreeExample,運行界面見下圖),通過它可以節約 不少工作量。
圖1 Draw2D例子中的TreeExample
記得數年前曾用Swing做過一個組織結構圖的編輯工具,當時的實現方式是讓畫布使用 XYLayout,在適當的時候計算和刷新每個樹節點的位置,算法的思想則是深度優先搜索,非 樹葉節點的位置由其子節點的數目和位置決定。我想這應該是比較直觀的方法吧,但是這次 看了Draw2D例子裡的實現覺得也很有道理,以前沒想到過。在這個例子裡樹節點圖形稱為 TreeBranch,它包含一個PageNode(表現為帶有折角的矩形)和一個透明容器contentsPane ,(一個Layer,用來放置子節點)。在一般情況下,TreeBranch本身使用名為NormalLayout 的布局管理器將PageNode放在子節點的正上方,而contentsPane則使用名為TreeLayout的布 局管理器計算每個子節點應在的位置。所以我們看到的整個樹實際上是由很多層子樹疊加而 成的,任何一個非葉節點對應的圖形的尺寸都等於以它為根節點的子樹所占區域的大小。
從這個例子裡我們還看到,用戶可以選擇使用橫向或縱向組織樹(見圖2),可以壓縮各 節點之間的空隙,每個節點可以橫向或縱向排列子節點,還可以展開或收起子節點,等等, 這為我們實現一個方便好用的樹編輯器提供了良好的基礎(視圖部分的工作大大簡化了)。
圖2 縱向組織的樹
這裡要插一句,Draw2D例子中提供的這些類的具體內容我沒有仔細 研究,相當於把它們當作Draw2D API的一部分來用了(包括TreeRoot、TreeBranch、 TreeLayout、BranchLayout、NormalLayout、HangingLayout、PageNode等幾個類,把代碼拷 到你的項目中即可使用),因為按照GEF 3.1的計劃表,它們很有可能以某種形式出現在正式 版的GEF 3.1裡。下面介紹一下我是如何把它們轉換為GEF應用程序的視圖部分從而實現樹編 輯器的。
首先從模型部分開始。因為樹是由一個個節點構成的,所以模型中最主要的 就是節點類(我稱為TreeNode),它和例子裡的TreeBranch圖形相對應,它應該至少包含 nodes(子節點列表)和text(顯示文本)這兩個屬性;例子裡有一個TreeRoot是TreeBranch 的子類,用來表示根節點,在TreeRoot裡多了一些屬性,如horizontal、majorSpacing等等 用來控制整個樹的外觀,所以模型裡也應該有一個繼承TreeNode的子類,而實際上這個類就 應該是編輯器的contents,它對應的圖形TreeRoot也就是一般GEF應用程序裡的畫布,這個地 方要想想清楚。同時,雖然看起來節點間有線連接,但這裡我們並不需要Connection對象, 這些線是由布局管理器繪制的,畢竟我們並不需要手動改變線的走向。所以,模型部分就是 這麼簡單,當然別忘了要實現通知機制,下面看看都有哪些EditPart。
與模型相對應 ,我們有TreeNodePart和TreeRootPart,後者和前者之間也是繼承關系。在getContentPane ()方法裡,要返回TreeBranch圖形所包含的contentsPane部分;在getModelChildren()方法 裡,要返回TreeNode的nodes屬性;在createFigure()方法裡,TreeNodePart應返回 TreeBranch實例,而TreeRootPart要覆蓋這個方法,返回TreeRoot實例;另外要注意在 refreshVisuals()方法裡,要把模型的當前屬性正確反映到圖形中,例如TreeNode裡有反映 節點當前是否展開的布爾變量expanded,則refreshVisuals()方法裡一定要把這個屬性的當 前值賦給圖形才可以。以下是TreeNodePart的部分代碼:
public IFigure getContentPane() {
return ((TreeBranch) getFigure()).getContentsPane();
}
protected List getModelChildren() {
return ((TreeNode) getModel()).getNodes();
}
protected IFigure createFigure() {
return new TreeBranch();
}
protected void createEditPolicies() {
installEditPolicy(EditPolicy.COMPONENT_ROLE, new TreeNodeEditPolicy());
installEditPolicy(EditPolicy.LAYOUT_ROLE, new TreeNodeLayoutEditPolicy ());
installEditPolicy(EditPolicy.SELECTION_FEEDBACK_ROLE, new ContainerHighlightEditPolicy());
}
上面代碼中用到了幾個EditPolicy,這裡說一下它們各自的用途。實際上,從Role中已經 可以看出來,TreeNodeEditPolicy是用來負責節點的刪除,沒有什麼特別; TreeNodeLayoutEditPolicy則復雜一些,我把它實現為ConstrainedLayoutEditPolicy的一個 子類,並實現createAddCommand()和getCreateCommand()方法,分別返回改變節點的父節點 和創建新節點的命令,另外我讓createChildEditPolicy()方法返回NonResizableEditPolicy 的實例,並覆蓋其createSelectionHandles()方法如下,以便在用戶選中一個節點時用一個 控制點表示選中狀態,不用缺省邊框的原因是,邊框會將整個子樹包住,不夠美觀,並且在 多選的時候界面比較混亂。
protected List createSelectionHandles() {
List list=new ArrayList();
list.add(new ResizeHandle((GraphicalEditPart)getHost(), PositionConstants.NORTH));
return list;
}
選中節點的效果如下圖,我根據需要改變了樹節點的顯示(修改PageNode類):
圖3 同時選中三個節點(Node2、Node3和Node8)
最後一個ContainerHighlightEditPolicy的唯一作用是當用戶拖動節點到另一個節點區域 中時,加亮顯示後者,方便用戶做出是否應該放開鼠標的選擇。它是GraphicalEditPolicy的 子類,部分代碼如下,如果你看過Logic例子的話,應該不難發現這個類就是我從那裡拿過來 然後修改一下得到的。
protected void showHighlight() {
((TreeBranch) getContainerFigure()).setSelected(true);
}
public void eraseTargetFeedback(Request request) {
((TreeBranch) getContainerFigure()).setSelected(false);
}
好了,現在樹編輯器應該已經能夠工作了。為了讓用戶使用更方便,你可以實現展開/收 起子節點、橫向/縱向排列子節點等等功能,在視圖部分Draw2D的例子代碼已經內置了這些功 能,你要做的就是給模型增加適當的屬性。我這裡的一個截圖如下所示,其中Node1是收起狀 態,Node6縱向排列子節點(以節省橫向空間)。
圖4 樹編輯器的運行界面
這個編輯器我花一天時間就完成了,但如果不是利用Draw2D的例子,相信至少要四至六天 ,而且缺陷會比較多,功能上也不會這麼完善。我感覺在GEF中遇到沒有實現過的功能前最好 先找一找有沒有可以利用的資源,比如GEF提供的幾個例子就很好,當然首先要理解它們才談 得上利用。