雖然許多 GEF 應用都會用到連接,但也有些應用不需要用連接表達關係,我們目前的專案即是一例。此類應用中,模型對象間關係主要通過圖形包含表達,多為一對多關係。

圖1 不使用連接的 GEF 應用
專案需要一個圖形化模型編輯器,主要功能:在具有三行 N 列的表格中自由增刪節點,節點可在不同單元格間拖動,可合併相鄰節點,表格列可增減、拖動等。因 SWT/JFace 提供的表格難以實現這些功能,選擇使用 GEF 開發,效果不錯(見下圖)。這裡介紹實現過程中與圖形和佈局相關的問題。
動手前首先考慮模型構造。Draw2D 僅提供有限佈局(ToolbarLayout、FlowLayout、XYLayout),無 GridLayout,故不能把整個表格作為一個 EditPart,而應把每一列看作一個 EditPart(因對列操作多於行),才能實現列拖動。另外,節點包含在列中,但仔細研究發現,節點並非直接包含在列中,而是有單元格對象作為中間橋梁:每列包含固定三個單元格,每單元格可包含任意個節點。經分析,模型、EditPart 和 Figure 初步成形:
| 模型 | EditPart | Figure | |
|---|---|---|---|
| 畫布 | Diagram | DiagramPart | FreeformLayer |
| 列 | Column | ColumnPart | ColumnFigure |
| 單元格 | Cell | CellPart | CellFigure |
| 節點 | Node | NodePart | NodeFigure |
表中從上到下為包含關係(一對多),下圖簡單顯示這些關係:

圖2 圖形包含關係圖
畫布上列顯示為縱向矩形,每列有頭部顯示列名,所有列橫向排列。故畫布應使用 ToolbarLayout 或 FlowLayout。兩者相似,皆按指定方向排列圖形,差異在於:圖形過多容納不下時,ToolbarLayout 犧牲部分圖形保持一行(列),FlowLayout 則允許換行(列)顯示。對我們的畫布,顯然應用 ToolbarLayout,因子圖形 ColumnFigure 不應出現換行。定義畫布圖形代碼:
Figure f = new FreeformLayer();
ToolbarLayout layout = new ToolbarLayout();
layout.setVertical(false);
layout.setSpacing(5);
layout.setStretchMinorAxis(true);
f.setLayoutManager(layout);
f.setBorder(new MarginBorder(5));
setVertical(false) 指定橫向排列子圖形,setSpacing(5) 指定子圖形間保留 5 像素距離,setStretchMinorAxis(true) 指定每個子圖形高度保持一致。
ColumnFigure 較複雜:需有頭部區域,且三個子圖形(CellFigure)合起來要能充滿下部區域並適應高度變化。一開始用 Draw2D 提供的 Label 實現列頭,但無法設置高度,因 Label 覆寫了 getPreferredSize(),高度只與文本相關。解決方法是構造 HeaderFigure,維護一個 Label,設置列頭高度時實際設置 HeaderFigure 高度;或直接讓 HeaderFigure 繼承 Label 並重新覆寫 getPreferredSize()。專案採用前者。
第二個問題花了些時間解決:一開始在 CellPart 的 refreshVisuals() 手動設置 CellFigure 高度為 ColumnFigure 下部區域高度的三分之一,但這很勉強,還需額外考慮 spacing 影響。後來通過自定義佈局較圓滿解決:讓 ColumnFigure 使用自定義 ColumnLayout,繼承 ToolbarLayout 但覆寫 layout() 方法:
class ColumnLayout extends ToolbarLayout {
public void layout(IFigure parent) {
IFigure nameFigure = (IFigure) parent.getChildren().get(0);
IFigure childrenFigure = (IFigure) parent.getChildren().get(1);
Rectangle clientArea = parent.getClientArea();
nameFigure.setBounds(new Rectangle(clientArea.x, clientArea.y, clientArea.width, 30));
childrenFigure.setBounds(new Rectangle(clientArea.x, nameFigure.getBounds().height + clientArea.y,
clientArea.width, clientArea.height - nameFigure.getBounds().height));
}
}
即在 layout 中控制列頭和下部高度分別為 30 和剩餘高度。為讓單元格正確定位,還需指定列下部圖形(childrenFigure)的佈局管理器。Draw2D 無類似 SWT FillLayout 的管理器,故再自定義 FillLayout(與 SWT 同名),覆寫 layout 方法(因用了 transposer,horizontal 和 vertical 可統一處理,transposer 僅在 horizontal 時起作用):
public void layout(IFigure parent) {
List children = parent.getChildren();
int numChildren = children.size();
Rectangle clientArea = transposer.t(parent.getClientArea());
int x = clientArea.x;
int y = clientArea.y;
for (int i = 0; i < numChildren; i++) {
IFigure child = (IFigure) children.get(i);
Rectangle newBounds = new Rectangle(x, y, clientArea.width, -1);
int divided = (clientArea.height - ((numChildren - 1) * spacing)) / numChildren;
if (i == numChildren - 1)
divided = clientArea.height - ((divided + spacing) * (numChildren - 1));
newBounds.height = divided;
child.setBounds(transposer.t(newBounds));
y += newBounds.height + spacing;
}
}
作用是將父圖形高(寬)度平均分配給每個子圖形,最後一位子圖形佔據所有剩餘空間(防止除不盡留下空白)。完成 FillLayout,讓 childrenFigure 使用它作為佈局管理器。下面是 ColumnFigure 大部分代碼,列頭圖形(HeaderFigure)和列下部圖形(ChildrenFigure)作為內部類:
private HeaderFigure name = new HeaderFigure();
private ChildrenFigure childrenFigure = new ChildrenFigure();
public ColumnFigure() {
ToolbarLayout layout = new ColumnLayout();
layout.setVertical(true);
layout.setStretchMinorAxis(true);
setLayoutManager(layout);
setBorder(new LineBorder());
setBackgroundColor(color);
setOpaque(true);
add(name);
add(childrenFigure);
setPreferredSize(100, -1);
}
class ChildrenFigure extends Figure {
public ChildrenFigure() {
ToolbarLayout layout = new FillLayout();
layout.setMinorAlignment(ToolbarLayout.ALIGN_CENTER);
layout.setStretchMinorAxis(true);
layout.setVertical(true);
layout.setSpacing(5);
setLayoutManager(layout);
}
}
class HeaderFigure extends Figure {
private String text;
private Label label;
public HeaderFigure() {
this.label = new Label();
this.add(label);
setOpaque(true);
}
public String getText() { return this.label.getText(); }
public Rectangle getTextBounds() { return this.label.getTextBounds(); }
public void setText(String text) {
this.text = text;
this.label.setText(text);
this.repaint();
}
public void setBounds(Rectangle rect) {
super.setBounds(rect);
this.label.setBounds(rect);
}
}
單元格佈局管理器同樣使用 FillLayout,因需求中:用戶向單元格添加第一個節點時,該節點充滿單元格;兩個節點時各佔二分之一高度;以此類推。下表總結各圖形使用的佈局管理器:
| 圖形 | 佈局管理器 | 直接子圖形 |
|---|---|---|
| 畫布 | ToolbarLayout | 列 |
| 列 | ColumnLayout | 列頭部、列下部 |
| -列頭部 | 無 | 無 |
| -列下部 | FillLayout | 單元格 |
| 單元格 | FillLayout | 節點 |
| 節點 | 無 | 無 |
只有包含子圖形的圖形才需佈局管理器,因佈局管理器關心和管理的是「子」圖形,請牢記此點。
特別提醒:圖形使用 ToolbarLayout 或子類作為佈局管理器時,對應 EditPart 若安裝了 FlowLayoutEditPolicy 或子類,可能得到 ClassCastException。例如 CellFigure 對應 CellPart,安裝了 CellLayoutEditPolicy(FlowLayoutEditPolicy 子類)。異常原因:FlowLayoutEditPolicy 的 isHorizontal() 會將圖形 layout 強制轉換為 FlowLayout,但我們用的是 ToolbarLayout。我認為這是 GEF 疏忽,因作者曾說 FlowLayout 可應用於 ToolbarLayout。解決方法:在你的 EditPolicy 中覆寫 isHorizontal(),先判斷 layout 是 ToolbarLayout 還是 FlowLayout,再返回合適布林值。
最後,畫布還有一問題未解:希望表格列增多到一定程度後,畫布可向右擴展尺寸。前文說畫布用 FreeformLayer,為達目的,還必須在 editor 設置 rootEditPart 為 ScalableRootEditPart,注意不是 ScalableFreeformRootEditPart,後者用於各方向都能擴展的畫布。各種 RootEditPart 用法後續將介紹。
以上結合具體實例講解了如何在 GEF 中使用 ToolbarLayout 及自定義簡單佈局管理器。構造圖形應遵守原則:盡量讓佈局管理器決定每個子圖形位置和尺寸,避免很多麻煩。當然有例外,如 XYLayout 這種只關心子圖形位置的管理器中,必須為每個子圖形指定尺寸,否則圖形因尺寸過小而不可見,這也是開發人員易疏忽的地方。