在目前的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负责单元格的布局。同样是因为修改的工作量相对比较大而没有采用,因为那样的话行和列都要使用自定义方式处理,而这篇贴子介绍的方法只关心行的处理就可以了。当然,这里说的也不是什么标准实现,不过效果还是不错的,而且确实可以实现,如果你有类似的需求可以作为参考。