在目前的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負責儲存格的布局。同樣是因為修改的工作量相對比較大而沒有採用,因為那樣的話行和列都要使用自訂方式處理,而這篇貼子介紹的方法只關心行的處理就可以了。當然,這裡說的也不是什麼標準實現,不過效果還是不錯的,而且確實可以實現,如果你有類似的需求可以作為參考。