雖然很多GEF應用程序裡都會用到連接(Connection),但也有一些應用是不需要用連接 來表達關系的,我們目前正在做的這個項目就是這樣一個例子。在這類應用中,模型對象間 的關系主要通過圖形的包含來表達,所以大多是一對多關系。
圖1 不使用連接的GEF應用
先簡單描述一下我們這個項目,該項目需要一個圖形化的模型編輯器,主要功能是在一個 具有三行N列的表格中自由增加/刪除節點,節點可在不同單元格間拖動,可以合並相鄰節點 ,表格列可增減、拖動等等。由於SWT/Jface提供的表格很難實現這些功能,所以我們選擇了 使用GEF開發,目前看來效果還是很不錯的(見下圖),這裡就簡單介紹一下實現過程中與圖 形和布局有關的一些問題。
在動手之前首先還是要考慮模型的構造。由於Draw2D只提供了很有限的Layout,如 ToolbarLayout、FlowLayout和XYLayout,並沒有一個GridLayout,所以不能把整個表格作為 一個EditPart,而應該把每一列看作一個EditPart(因為對列的操作比對行的操作多,所以 不把行作為EditPart),這樣才能實現列的拖動。另外,從需求中可以看出,每個節點都包 含在一個列中,但仔細再研究一下會發現,實際上節點並非直接包含在列中,而是有一個單 元格對象作為中間的橋梁,即每個列包含固定的三個單元格,每個單元格可以包含任意個節 點。經過以上分析,我們的模型、EditPart和Figure應該已經初步成形了,見下表:
模型 EditPart Figure 畫布 Diagram DiagramPart FreeformLayer 列 Column ColumnPart ColumnFigure 單元格 Cell CellPart CellFigure 節點 Node NodePart NodeFigure
表中從上到下是包含關系,也就是一對多關系,下圖簡單顯示了這些關系:
圖2 圖形包含關系圖
讓我們從畫布開始考慮。在畫布上,列顯示為一個縱向(高大於寬)的矩形,每個列有一 個頭(Header)用來顯示列名,所有列在畫布上是橫向排列的。因此,畫布應該使用 ToolbarLayout或FlowLayout中的一種。這兩種Layout有很多相似之處,尤其它們都是按指定 的方向排列顯示圖形,不同之處主要在於:當圖形太多容納不下的時候,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類覆蓋了 Figure的getPreferedSize()方法,使得它的高度只與裡面的文本有關。解決方法是構造一個 HeaderFigure,讓它維護一個Label,設置列頭高度時實際設置的是HeaderFigure的高度;或 者直接讓HeaderFiguer繼承Label並重新覆蓋getPreferedSize()也可以。我在項目裡使用的 是前者。
第二個問題花了我一些時間才搞定,一開始我是在CellPart的refreshVisuals()方法裡手 動設置CellFigure的高度為ColumnFigure下部區域高度的三分之一,但這樣很勉強,而且還 需要額外考慮spacing帶來的影響。後來通過自定義Layout的方式比較圓滿的解決了這個問題 ,我讓ColumnFigure使用自定義的ColumnLayout,這個Layout繼承自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那樣的布局管理器,所以我們要再自定義另一個layout,我暫時給它起名為 FillLayout(與SWT的FillLayout同名),還是要覆蓋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,它對應的EditPart是CellPart,其上 安裝了CellLayoutEditPolicy是FlowLayoutEditPolicy的一個子類。出現這個異常的原因是 在FlowLayoutEditPolicy的isHorizontal()方法中會將圖形的layout強制轉換為FlowLayout ,而我們使用的是ToolbarLayout。我認為這是GEF的一個疏忽,因為作者曾說過FlowLayout 可應用於ToolbarLayout。幸好解決方法也不復雜:在你的那個EditPolicy中覆蓋 isHorizontal()方法,在這個方法裡先判斷layout是ToolbarLayout還是FlowLayout,再根據 結果返回合適的boolean值即可。
最後,關於我們的畫布還有一個問題沒有解決,我們希望表格列增多到一定程度後,畫布 可以向右邊擴展尺寸,前面說過畫布使用的是FreeformLayer作為圖形。為了達到目的,還必 須在editor裡設置rootEditPart為ScalableRootEditPart,要注意不是 ScalableFreeformRootEditPart,後者在需要各個方向都能擴展的畫布的應用程序中經常被 使用。關於各種RootEditPart的用法,在後續帖子裡將會介紹到。
以上結合具體實例講解了如何在GEF中使用ToolbarLayout以及自定義簡單的布局管理器。 我們構造圖形應該遵守一個原則,那就是盡量讓布局管理器決定每個子圖形的位置和尺寸, 這樣可以避免很多麻煩。當然也有例外,比如在XYLayout這種只關心子圖形位置的布局管理 器中,就必須為每個子圖形指定尺寸,否則圖形將因為尺寸過小而不可見,這也是一個開發 人員十分容易疏忽的地方。