在目前的GEF版本(3.1M6)裡,可用的LayoutManager還不是很多,在新聞組裡經常會看 到要求增加更多布局的帖子,有人也提供了自己的實現,例如這個GridLayout,相當於SWT中 GridLayout的Draw2D實現,等等。雖然可以肯定GEF的未來版本裡會增加更多的布局供開發者 使用(可能需要很長時間),然而目前要用GEF實現表格的操作還沒有很直接的辦法,這裡說 說我的做法,僅供參考。
實現表格的方法決定於模型的設計,初看來我們似乎應該有這些類:表格(Table)、行 (Row)、列(Column)和單元格(Cell),每個模型對象對應一個EditPart,以及一個 Figure,TablePart應該包含RowPart和ColumnPart,問題是RowFigure和ColumnFigure會產生 交叉,想象一下你的表格該使用什麼樣的布局才能容納它們?使用這樣的模型並非不能實現 (例如使用StackLayout),但我認為這樣的模型需要做的額外工作會很多,所以我使用基於 列的模型。
在我的表格模型裡,只有三種對象:Table、Column和Cell,但Column有一個子類 HeaderColumn表示第一列,同時Cell有一個子類HeaderCell表示位於第一列裡的單元格,後 面這兩個類的作用主要是模擬實現對行的操作--把對行的操作都轉換為對HeaderCell的操作 。例如,創建一個新行轉換為在第一列中增加一個新的單元格,當然在這同時我們要讓程序 給其余每一列同樣增加一個單元格。
圖1 表格編輯器
現在的問題就是怎樣讓用戶察覺不到我們是在對單元格而不是對行操作。需要修改的地方 有這麼幾處:一是創建新行或改變行位置時顯示與行寬一致的插入提示線,二是在用戶點擊 位於第一列中的單元格(HeaderCell)時顯示為整個行被選中,三是允許用戶通過鼠標拖動 改變行高度,最後是在改變行所在位置或大小的時候顯示正確的回顯(Feedback)圖形。下 面依次介紹它們的實現方法。
調整插入線的寬度
在我們的調色板裡有一個Row工具項,代表表格中的一個行,它的作用是創建新的行。注 意這個工具項的名字雖然叫Row,實際上用它創建的是一個HeaderCell對象,創建它的代碼如 下:
tool = new CombinedTemplateCreationEntry("Row", "Create a new Row", HeaderCell.class, new SimpleFactory(HeaderCell.class), CbmPlugin.getImageDescriptor(IConstants.IMG_ROW), null);
創建新行的方式是從調色板裡拖動它到想要的位置。在拖動過程中,隨著鼠標所在位置的 變化,編輯器應該能顯示一條直線,用來表示如果此時放開鼠標新行將插入的位置。由於這 個工具代表的是一個單元格,所以缺省情況下GEF會顯示一條與單元格長度相同的插入線,為 了讓用戶感覺到是在插入行,我們必須改變插入線的寬度。具體的方法是在 HeaderColumnPart的負責Layout的那個EditPolicy(繼承FlowLayoutEditPolicy)中覆蓋 showLayoutTargetFeedback()方法,修改後的代碼如下:
protected void showLayoutTargetFeedback(Request request) {
super.showLayoutTargetFeedback(request);
// Expand feedback line's width
Diagram diagram = (Diagram) getHost().getParent().getModel();
Column column = (Column) getHost().getModel();
Point p2 = getLineFeedback().getPoints().getPoint(1);
p2.x = p2.x + (diagram.getColumns().size() - 1) * (column.getWidth() + IConstants.COLUMN_SPACING);
getLineFeedback().setPoint(p2, 1);
}
其中p2代表插入線中右邊的那個點,我們將它的橫坐標加上一個量即可增加這條線的長度 ,這個量和表格當前列的數目有關,和列間距也有關,計算的方法看上面的代碼很清楚。這 樣修改後的效果如下圖所示,拖動行到新的位置時也會使用同樣的插入線。
圖2 與表格同寬的插入線
選中整個行
缺省情況下,鼠標點擊一個單元格會在 這個單元格四周產生一個黑色的邊框,用來表示被選中的狀態。為了讓用戶能選中整個行, 要修改HeaderCell上的EditPolicy。在前面一篇帖子裡已經專門講過,單元格作為列的子元 素,要修改它的EditPolicy就要在ColumnPart的EditPolicy的createChildEditPolicy()方法 裡返回自定義的EditPolicy,這裡我返回的是自己實現的DragRowEditPolicy,它繼承自GEF 內置的ResizableEditPolicy類,它將被HeaderColumnPart加到子元素HeaderCellPart的 EditPolicy列表。現在就來修改DragRowEditPolicy以實現整個行的選中。
首先要說 明,在GEF裡一個圖形被選中時出現的黑邊和控制點稱為Handle,其中黑邊稱為MoveHandle, 用於移動圖形;而那些控制點稱為ResizeHandle,用於改變圖形的尺寸。要改變黑邊的尺寸 (由單元格的寬度擴展為整個表格的寬度),我們得繼承MoveHandle並覆蓋它的getLocator ()方法,下面的代碼是我的實現:
public class RowMoveHandle extends MoveHandle {
public RowMoveHandle(GraphicalEditPart owner, Locator loc) {
super(owner, loc);
}
public RowMoveHandle (GraphicalEditPart owner) {
super(owner);
}
//計算 得到選中行所占的位置,傳給MoveHandleLocator作為參考
public Locator getLocator() {
IFigure refFigure = new Figure();
Rectangle rect=((HeaderCellPart) getOwner()).getRowBound();
translateToAbsolute(rect);
refFigure.setBounds(rect);
return new MoveHandleLocator(refFigure);
}
}
在getLocator()方法裡,我們調用了HeaderCellPart的getRowBound()方法用於得到選中 行的位置和尺寸,這個方法的代碼如下(放在HeaderCellPart裡是因為在Handle裡通過 getOwner()可以很容易得到EditPart對象),行尺寸的計算方法與前面插入線的情況類似:
public Rectangle getRowBound(){
Rectangle rect = getFigure().getBounds().getCopy();
Diagram diagram = (Diagram) getParent().getParent().getModel();
Column column = (Column) getParent().getModel();
rect.setSize(diagram.getColumns().size() * column.getWidth() + (diagram.getColumns().size() - 1) * IConstants.COLUMN_SPACING, rect.getSize ().height);
return rect;
}
有了這個RowMoveHandle,只要把它代替原來缺省的MoveHandle加到HeaderColumnCell上 即可,具體的方法就是覆蓋DragRowEditPolicy的createSelectionHandles()方法, ResizableEditPolicy對這個方法的缺省實現是加一個黑框和八個控制點,而我們要改成下面 這樣:
protected List createSelectionHandles() {
List l = new ArrayList();
//四周的黑色邊框
l.add(new RowMoveHandle((GraphicalEditPart) getHost()));
//下方的控制點
l.add(new RowResizeHandle((GraphicalEditPart) getHost(), PositionConstants.SOUTH));
return l;
}
代碼裡用到的RowResizeHandle類是控制點的自定義實現,在下面很快會講到。現在,用 戶可以看到整個行被選中的效果了。
圖3 選中整個行
改變行的高度
改變行高度比較自然的方式是讓用戶選中行後自由拖動下面的邊。前面說過,GEF裡的 ResizeHandle具有調整圖形尺寸的功能,美中不足的是ResizeHandle表現為黑色(或白色, 非主選擇時)的小方塊,而我們希望它是一條線就好了,這樣鼠標指針只要放在選中行的下 邊上就會變成改變尺寸的樣子。這就需要我們實現剛才提到的RowResizeHandle類了,它是 ResizeHandle的子類,代碼如下:
public class RowResizeHandle extends ResizeHandle {
public RowResizeHandle(GraphicalEditPart owner, int direction) {
super(owner, direction);
//改變控制點的尺寸,使之變成一條線
setPreferredSize(new Dimension(((HeaderCellPart) owner).getRowBound ().width, 2));
}
public RowResizeHandle(GraphicalEditPart owner, Locator loc, Cursor c) {
super(owner, loc, c);
}
//缺省實現裡控制點有描邊,我們不需要,所以覆蓋這個方法
public void paintFigure(Graphics g) {
Rectangle r = getBounds();
g.setBackgroundColor(getFillColor());
g.fillRectangle(r.x, r.y, r.width, r.height);
}
//與前面RowMoveHandle類似,但返回RelativeHandleLocator以使線顯示在圖形下方
public Locator getLocator() {
IFigure refFigure = new Figure();
Rectangle rect=((HeaderCellPart) getOwner()).getRowBound();
translateToAbsolute(rect);
refFigure.setBounds(rect);
return new RelativeHandleLocator(refFigure, PositionConstants.SOUTH);
}
//不論是否為主選擇,都使用黑色填充
protected Color getFillColor() {
return ColorConstants.black;
}
}
這樣,我們就把控制點拉成了控制線,因為它的位置與選擇框(RowMoveHandle)的一部 分重合,所以在界面上感覺不到它的存在,但用戶可以通過它控制行的高度,見下圖。
圖4 改變行高的提示
正確的回顯圖形
我們知道,在拖動圖形和改變圖形尺寸的時候,GEF會顯示一個"影圖"(Ghost Shape)作 為回顯,也就是顯示圖形的新位置和尺寸信息。因為操作行時目標對象實際是單元格,所以 在缺省情況下回顯也是單元格的樣子(寬度與列寬相同)。為此,在DragRowEditPolicy裡要 覆蓋getInitialFeedbackBounds()方法,這個方法返回的Rectangle決定了鼠標開始拖動時回 顯圖形的初始狀態,見以下代碼:
protected Rectangle getInitialFeedbackBounds() {
return ((HeaderCellPart) getHost()).getRowBound();
}
這時的回顯見下圖,在拖動行時也使用同樣的回顯。
圖5 改變行高時的回顯
經過上面的修改,對HeaderCell的操作在界面上已經完全表現為對表格行的操作了。這些 操作的結果會轉換為一些Command,包括CreateHeaderCellCommand(創建新行,你也可以命 名為CreateRowCommand)、MoveHeaderCellCommand(移動行)、DeleteHeaderCellCommand (刪除行)和ChangeHeaderCellHeightCommand(改變行高)等,在這些類裡要對所有列執行 同樣的操作(例如改變HeaderCell的高度的同時改變同一行中其他單元格的高度),這樣在 界面上才能保持表格的外觀,詳細的代碼沒有必要貼在這裡了。
P.S.曾經考慮過另一種實現表格的方法,就是模型裡只有Table和Cell兩種對象,然後自 己寫一個TableLayout負責單元格的布局。同樣是因為修改的工作量相對比較大而沒有采用, 因為那樣的話行和列都要使用自定義方式處理,而這篇貼子介紹的方法只關心行的處理就可 以了。當然,這裡說的也不是什麼標准實現,不過效果還是不錯的,而且確實可以實現,如 果你有類似的需求可以作為參考。