在目前的GEF版本(3.1M6)里,可用的LayoutManager還不是很多,在新聞組里經(jīng)常會(huì)看到要求增加更多布局的帖子,有人也提供了自己的實(shí)現(xiàn),例如這個(gè)GridLayout,相當(dāng)于SWT中GridLayout的Draw2D實(shí)現(xiàn),等等。雖然可以肯定GEF的未來(lái)版本里會(huì)增加更多的布局供開(kāi)發(fā)者使用(可能需要很長(zhǎng)時(shí)間),然而目前要用GEF實(shí)現(xiàn)表格的操作還沒(méi)有很直接的辦法,這里說(shuō)說(shuō)我的做法,僅供參考。
實(shí)現(xiàn)表格的方法決定于模型的設(shè)計(jì),初看來(lái)我們似乎應(yīng)該有這些類(lèi):表格(Table)、行(Row)、列(Column)和單元格(Cell),每個(gè)模型對(duì)象對(duì)應(yīng)一個(gè)EditPart,以及一個(gè)Figure,TablePart應(yīng)該包含RowPart和ColumnPart,問(wèn)題是RowFigure和ColumnFigure會(huì)產(chǎn)生交叉,想象一下你的表格該使用什么樣的布局才能容納它們?使用這樣的模型并非不能實(shí)現(xiàn)(例如使用StackLayout),但我認(rèn)為這樣的模型需要做的額外工作會(huì)很多,所以我使用基于列的模型。
在我的表格模型里,只有三種對(duì)象:Table、Column和Cell,但Column有一個(gè)子類(lèi)HeaderColumn表示第一列,同時(shí)Cell有一個(gè)子類(lèi)HeaderCell表示位于第一列里的單元格,后面這兩個(gè)類(lèi)的作用主要是模擬實(shí)現(xiàn)對(duì)行的操作--把對(duì)行的操作都轉(zhuǎn)換為對(duì)HeaderCell的操作。例如,創(chuàng)建一個(gè)新行轉(zhuǎn)換為在第一列中增加一個(gè)新的單元格,當(dāng)然在這同時(shí)我們要讓程序給其余每一列同樣增加一個(gè)單元格。

圖1 表格編輯器
現(xiàn)在的問(wèn)題就是怎樣讓用戶(hù)察覺(jué)不到我們是在對(duì)單元格而不是對(duì)行操作。需要修改的地方有這么幾處:一是創(chuàng)建新行或改變行位置時(shí)顯示與行寬一致的插入提示線,二是在用戶(hù)點(diǎn)擊位于第一列中的單元格(HeaderCell)時(shí)顯示為整個(gè)行被選中,三是允許用戶(hù)通過(guò)鼠標(biāo)拖動(dòng)改變行高度,最后是在改變行所在位置或大小的時(shí)候顯示正確的回顯(Feedback)圖形。下面依次介紹它們的實(shí)現(xiàn)方法。
調(diào)整插入線的寬度
在我們的調(diào)色板里有一個(gè)Row工具項(xiàng),代表表格中的一個(gè)行,它的作用是創(chuàng)建新的行。注意這個(gè)工具項(xiàng)的名字雖然叫Row,實(shí)際上用它創(chuàng)建的是一個(gè)HeaderCell對(duì)象,創(chuàng)建它的代碼如下:
tool = new CombinedTemplateCreationEntry("Row", "Create a new Row", HeaderCell.class, new SimpleFactory(HeaderCell.class), CbmPlugin.getImageDescriptor(IConstants.IMG_ROW), null);
創(chuàng)建新行的方式是從調(diào)色板里拖動(dòng)它到想要的位置。在拖動(dòng)過(guò)程中,隨著鼠標(biāo)所在位置的變化,編輯器應(yīng)該能顯示一條直線,用來(lái)表示如果此時(shí)放開(kāi)鼠標(biāo)新行將插入的位置。由于這個(gè)工具代表的是一個(gè)單元格,所以缺省情況下GEF會(huì)顯示一條與單元格長(zhǎng)度相同的插入線,為了讓用戶(hù)感覺(jué)到是在插入行,我們必須改變插入線的寬度。具體的方法是在HeaderColumnPart的負(fù)責(zé)Layout的那個(gè)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代表插入線中右邊的那個(gè)點(diǎn),我們將它的橫坐標(biāo)加上一個(gè)量即可增加這條線的長(zhǎng)度,這個(gè)量和表格當(dāng)前列的數(shù)目有關(guān),和列間距也有關(guān),計(jì)算的方法看上面的代碼很清楚。這樣修改后的效果如下圖所示,拖動(dòng)行到新的位置時(shí)也會(huì)使用同樣的插入線。

圖2 與表格同寬的插入線
選中整個(gè)行
缺省情況下,鼠標(biāo)點(diǎn)擊一個(gè)單元格會(huì)在這個(gè)單元格四周產(chǎn)生一個(gè)黑色的邊框,用來(lái)表示被選中的狀態(tài)。為了讓用戶(hù)能選中整個(gè)行,要修改HeaderCell上的EditPolicy。在前面一篇帖子里已經(jīng)專(zhuān)門(mén)講過(guò),單元格作為列的子元素,要修改它的EditPolicy就要在ColumnPart的EditPolicy的createChildEditPolicy()方法里返回自定義的EditPolicy,這里我返回的是自己實(shí)現(xiàn)的DragRowEditPolicy,它繼承自GEF內(nèi)置的ResizableEditPolicy類(lèi),它將被HeaderColumnPart加到子元素HeaderCellPart的EditPolicy列表。現(xiàn)在就來(lái)修改DragRowEditPolicy以實(shí)現(xiàn)整個(gè)行的選中。
首先要說(shuō)明,在GEF里一個(gè)圖形被選中時(shí)出現(xiàn)的黑邊和控制點(diǎn)稱(chēng)為Handle,其中黑邊稱(chēng)為MoveHandle,用于移動(dòng)圖形;而那些控制點(diǎn)稱(chēng)為ResizeHandle,用于改變圖形的尺寸。要改變黑邊的尺寸(由單元格的寬度擴(kuò)展為整個(gè)表格的寬度),我們得繼承MoveHandle并覆蓋它的getLocator()方法,下面的代碼是我的實(shí)現(xiàn):

public class RowMoveHandle extends MoveHandle
{

public RowMoveHandle(GraphicalEditPart owner, Locator loc)
{
super(owner, loc);
}

public RowMoveHandle(GraphicalEditPart owner)
{
super(owner);
}
//計(jì)算得到選中行所占的位置,傳給MoveHandleLocator作為參考

public Locator getLocator()
{
IFigure refFigure = new Figure();
Rectangle rect=((HeaderCellPart) getOwner()).getRowBound();
translateToAbsolute(rect);
refFigure.setBounds(rect);
return new MoveHandleLocator(refFigure);
}
}

在getLocator()方法里,我們調(diào)用了HeaderCellPart的getRowBound()方法用于得到選中行的位置和尺寸,這個(gè)方法的代碼如下(放在HeaderCellPart里是因?yàn)樵贖andle里通過(guò)getOwner()可以很容易得到EditPart對(duì)象),行尺寸的計(jì)算方法與前面插入線的情況類(lèi)似:

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;
}

有了這個(gè)RowMoveHandle,只要把它代替原來(lái)缺省的MoveHandle加到HeaderColumnCell上即可,具體的方法就是覆蓋DragRowEditPolicy的createSelectionHandles()方法,ResizableEditPolicy對(duì)這個(gè)方法的缺省實(shí)現(xiàn)是加一個(gè)黑框和八個(gè)控制點(diǎn),而我們要改成下面這樣:

protected List createSelectionHandles()
{
List l = new ArrayList();
//四周的黑色邊框
l.add(new RowMoveHandle((GraphicalEditPart) getHost()));
//下方的控制點(diǎn)
l.add(new RowResizeHandle((GraphicalEditPart) getHost(), PositionConstants.SOUTH));
return l;
}

代碼里用到的RowResizeHandle類(lèi)是控制點(diǎn)的自定義實(shí)現(xiàn),在下面很快會(huì)講到。現(xiàn)在,用戶(hù)可以看到整個(gè)行被選中的效果了。

圖3 選中整個(gè)行
改變行的高度
改變行高度比較自然的方式是讓用戶(hù)選中行后自由拖動(dòng)下面的邊。前面說(shuō)過(guò),GEF里的ResizeHandle具有調(diào)整圖形尺寸的功能,美中不足的是ResizeHandle表現(xiàn)為黑色(或白色,非主選擇時(shí))的小方塊,而我們希望它是一條線就好了,這樣鼠標(biāo)指針只要放在選中行的下邊上就會(huì)變成改變尺寸的樣子。這就需要我們實(shí)現(xiàn)剛才提到的RowResizeHandle類(lèi)了,它是ResizeHandle的子類(lèi),代碼如下:

public class RowResizeHandle extends ResizeHandle
{

public RowResizeHandle(GraphicalEditPart owner, int direction)
{
super(owner, direction);
//改變控制點(diǎn)的尺寸,使之變成一條線
setPreferredSize(new Dimension(((HeaderCellPart) owner).getRowBound().width, 2));
}

public RowResizeHandle(GraphicalEditPart owner, Locator loc, Cursor c)
{
super(owner, loc, c);
}
//缺省實(shí)現(xiàn)里控制點(diǎn)有描邊,我們不需要,所以覆蓋這個(gè)方法

public void paintFigure(Graphics g)
{
Rectangle r = getBounds();
g.setBackgroundColor(getFillColor());
g.fillRectangle(r.x, r.y, r.width, r.height);
}
//與前面RowMoveHandle類(lèi)似,但返回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;
}
}

這樣,我們就把控制點(diǎn)拉成了控制線,因?yàn)樗奈恢门c選擇框(RowMoveHandle)的一部分重合,所以在界面上感覺(jué)不到它的存在,但用戶(hù)可以通過(guò)它控制行的高度,見(jiàn)下圖。

圖4 改變行高的提示
正確的回顯圖形
我們知道,在拖動(dòng)圖形和改變圖形尺寸的時(shí)候,GEF會(huì)顯示一個(gè)"影圖"(Ghost Shape)作為回顯,也就是顯示圖形的新位置和尺寸信息。因?yàn)椴僮餍袝r(shí)目標(biāo)對(duì)象實(shí)際是單元格,所以在缺省情況下回顯也是單元格的樣子(寬度與列寬相同)。為此,在DragRowEditPolicy里要覆蓋getInitialFeedbackBounds()方法,這個(gè)方法返回的Rectangle決定了鼠標(biāo)開(kāi)始拖動(dòng)時(shí)回顯圖形的初始狀態(tài),見(jiàn)以下代碼:

protected Rectangle getInitialFeedbackBounds()
{
return ((HeaderCellPart) getHost()).getRowBound();
}

這時(shí)的回顯見(jiàn)下圖,在拖動(dòng)行時(shí)也使用同樣的回顯。

圖5 改變行高時(shí)的回顯
經(jīng)過(guò)上面的修改,對(duì)HeaderCell的操作在界面上已經(jīng)完全表現(xiàn)為對(duì)表格行的操作了。這些操作的結(jié)果會(huì)轉(zhuǎn)換為一些Command,包括CreateHeaderCellCommand(創(chuàng)建新行,你也可以命名為CreateRowCommand)、MoveHeaderCellCommand(移動(dòng)行)、DeleteHeaderCellCommand(刪除行)和ChangeHeaderCellHeightCommand(改變行高)等,在這些類(lèi)里要對(duì)所有列執(zhí)行同樣的操作(例如改變HeaderCell的高度的同時(shí)改變同一行中其他單元格的高度),這樣在界面上才能保持表格的外觀,詳細(xì)的代碼沒(méi)有必要貼在這里了。
P.S.曾經(jīng)考慮過(guò)另一種實(shí)現(xiàn)表格的方法,就是模型里只有Table和Cell兩種對(duì)象,然后自己寫(xiě)一個(gè)TableLayout負(fù)責(zé)單元格的布局。同樣是因?yàn)樾薷牡墓ぷ髁肯鄬?duì)比較大而沒(méi)有采用,因?yàn)槟菢拥脑捫泻土卸家褂米远x方式處理,而這篇貼子介紹的方法只關(guān)心行的處理就可以了。當(dāng)然,這里說(shuō)的也不是什么標(biāo)準(zhǔn)實(shí)現(xiàn),不過(guò)效果還是不錯(cuò)的,而且確實(shí)可以實(shí)現(xiàn),如果你有類(lèi)似的需求可以作為參考。