GEF 学习文档 - Eclipse插件开发


SWJTU 2010 GEF 学习文档 Eclipse 插件开发 westerly WWW.SWJTU.EDU. CN 本文档内容来自网络,个人仅做收集,仅为方便查阅只用。请勿用于其他用途。 目录 GEF 进阶,第一部分: Anchor.............................................................................................................. 4 GEF 进阶,第二部分: Router ............................................................................................................ 15 GEF 进阶,第三部分: Layer ............................................................................................................... 21 GEF 进阶,第 4 部分: Locator ......................................................................................................... 26 GEF 进阶,第 5 部分: Feedback ...................................................................................................... 33 GEF 进阶,第 6 部分: Feedback ...................................................................................................... 38 GEF 入门系列(序) ........................................................................................................................... 43 GEF 入门系列(1 Draw2D) .............................................................................................................. 46 GEF 入门系列(三,应用实例) ...................................................................................................... 51 GEF 入门系列(四,其他功能) ...................................................................................................... 55 GEF 入门系列(六,添加菜单和工具条) ..................................................................................... 64 GEF 入门系列(七、XYLayout 和展开/折叠功能) ....................................................................... 71 GEF 入门系列(八、使用 EMF 构造 GEF 的模型) ....................................................................... 80 GEF 入门系列(九、增加易用性) .................................................................................................. 85 GEF 入门系列(十、表格的一个实现).......................................................................................... 96 GEF 入门系列(十一、树的一个实现)........................................................................................ 102 GEF 入门系列(十二、自定义 Request) ..................................................................................... 107 GEF 常见问题及技巧- 把 GEF 放在 ViewPart 里 ........................................................................... 114 EMF+GEF 的属性页问题.................................................................................................................... 115 自动换行的 draw2d 标签 .................................................................................................................. 117 GEF 常见问题 1:为图形编辑器设置背景图片 ............................................................................ 118 GEF 常见问题 2:具有转折点的连接线......................................................................................... 120 GEF 常见问题 3:自身连接 ............................................................................................................. 125 GEF 常见问题 4:非矩形图元 ......................................................................................................... 127 GEF 常见问题 5:自动布局 ............................................................................................................. 130 GEF 常见问题 6:使用对话框 ......................................................................................................... 134 关于 Draw2D 里的 Layout ................................................................................................................. 136 GEF 常见问题 7:计算字符串在画布上占据的空间 .................................................................... 138 GEF 常见问题 8:导出到图片 ......................................................................................................... 139 GEF 进阶系列 来自 IBMdeveloperWorks http://www.ibm.com/developerworks/cn GEF 入门系列来自 八进制的博客 http://www.cnblogs.com/bjzhanghao/ GEF 常见问题系列来自 八进制的博客 http://www.cnblogs.com/bjzhanghao/ GEF 进阶,第一部分: Anchor 级别: 初级 马 若劼 (maruojie@cn.ibm.com), 软件工程师 2006 年 11 月 30 日 GEF(Graphical Editing Framework)是 Eclipse Tools 的子项目,它在底层使 用 Draw2D 作为布局和渲染引擎,在整体上使用 MVC 模式管理模型和视图。利用 GEF,开发者可以从应用模型开始,迅速的构造一个可视化编辑环境。正如其名 字所说,它只是一个框架,很多具体的事情仍然要靠开发者完成,但这也是 GEF 灵活的一方面,只要你掌握了相关的概念,你就可以对一个 GEF 应用进行充分 的定制。本系列的目的就是介绍 GEF 的相关概念,并在 GEF 的一些示例程序的 基础上演示如何定制、扩展自己的 GEF 应用。这是本系列的第一章,主要介绍 了 Anchor(锚点)的概念,以及如何自定义一个锚点并替代 GEF 缺省实现。 Anchor(锚点) 在一个典型的 GEF 程序中,我们通常会在画板上放上一些图形,然后用一些线 连接这些图形。这些线的两个端点就是 Anchor(锚点),而锚点所在的图形叫 做锚点的 Owner。更细化的说,一条线的起点叫做 Source Anchor(源锚点), 终结点叫做 Target Anchor(目标锚点)。如图 1 中的黑色小方块所示。 图 1. 源锚点和目标锚点 不难看出,锚点的具体位置和两个图形的位置以及连线的方式有关,这两个前 提确定之后,锚点可以通过一定的方法计算得出。对于图 1 的情况,两个图形 之间的连线是由两个图形的中心点确定的,那么锚点的计算方法就是找到这条 中心线和图形边界的交点。Draw2D 缺省为我们提供了一些 Anchor 的实现,最 常用的大概是 ChopboxAnchor,它只是简单的取两个图形的中心线和图形边界 的交点做为锚点(正是图 1 的情况)。对于简单的应用来说,ChopboxAnchor 可 以满足我们的需要,但是它的锚点计算方法导致锚点在任何时候都是唯一的, 如果这两个图形之间存在多条连线,它们会相互重叠使得看上去只有一条,于 是用户不可能用鼠标选择到被覆盖的连线。 解决这个问题的办法有两个: 1. 提供一个自定义的 Connection Router(连线路由器),以便能尽量避免 线之间的重合,甚至也可以每条线都有不同的 Router。 2. 实现一个自定义的锚点,可以让用户自己拖动锚点到不同的位置,避免线 之间的重合 对于方法 1,我们在以后的系列中会有介绍。这里我们考虑方法 2。 Shapes Example GEF 的 Shapes 示例是一个很基础的 GEF 程序,用户可以在其上放置椭圆和长方 形的图形,然后可以用两种样式的线连接它们。由于其使用了 ChopboxAnchor, 它不支持在两个图形之间创建多条连线,也不能移动锚点。我们将在它的基础 上实现一个可移动的锚点。 第一步,确定锚点的表示策略 设计自定义 Anchor 的第一个问题是"我想把什么位置做为 Anchor?",比如对 于一个矩形,你可以选择图形的中心,或者四条边的中心,或者边界上的任何 点。在我们这个例子里,我们希望是椭圆边界的任何点。因此我们可以考虑用 角度来表示 Anchor 的位置,如图 2 所示: 图 2. Anchor 的表示方式 我们可以用一个变量表示角度,从而计算出中心射线与边界的交点,把这个交 点作为图形的锚点。通过这样的方式,边界上的任一点都可以成为锚点,可以 通过手工调整锚点,避免连线重叠。 第二步,修改 Model 为了表示锚点,我们需要一个表示角度的变量,这个变量应该放到模型中以便 能够把锚点信息记录到文件中。对于一条来说,它有两个锚点,所以应该在连 线对象中添加两个成员,在 Shapes 例子中,连线对象是 org.eclipse.gef.examples.shapes.model.Connection, 我们修改它添加两个 成员和相应的 Getter 和 Setter 方法: private double sourceAngle; private double targetAngle; public double getSourceAngle() { return sourceAngle; 回页首 } public void setSourceAngle(double sourceAngle) { this.sourceAngle = sourceAngle; } public double getTargetAngle() { return targetAngle; } public void setTargetAngle(double targetAngle) { this.targetAngle = targetAngle; } sourceAngle 保存了源锚点的角度,targetAngle 保存了目标锚点的角度,使用 弧度表示。 第三步,实现 ConnectionAnchor 接口 锚点的接口是由 org.eclipse.draw2d.ConnectionAnchor 定义的,我们需要实 现这个接口,但是一般来说我们不用从头开始,可以通过继承其它类来减少我 们的工作。由于存在椭圆和长方形两种图形,所以我们还需要实现两个子类。 最终我们定义了基础类 BorderAnchor 和 RectangleBorderAnchor, EllipseBorderAnchor 两个子类。BorderAnchor 的代码如下: package org.eclipse.gef.examples.shapes.anchor; import org.eclipse.draw2d.ChopboxAnchor; import org.eclipse.draw2d.IFigure; import org.eclipse.draw2d.geometry.Point; public abstract class BorderAnchor extends ChopboxAnchor { protected double angle; public BorderAnchor(IFigure figure) { super(figure); angle = Double.MAX_VALUE; } public abstract Point getBorderPoint(Point reference); public Point getLocation(Point reference) { // 如果 angle 没有被初始化,使用缺省的 ChopboxAnchor,否则计算一个边界 锚点 if(angle == Double.MAX_VALUE) return super.getLocation(reference); else return getBorderPoint(reference); } public double getAngle() { return angle; } public void setAngle(double angle) { this.angle = angle; } } 重要的是 getLocation()方法,它有一个参数"Point reference",即一个参考 点,在计算锚点时,我们可以根据参考点来决定锚点的位置,对于 ChopboxAnchor 来说,参考点就是另外一个图形的中心点。BorderAnchor 类有一个 angle 成员, 保存了锚点的角度,它会被初始化为 Double.MAX_VALUE,所以我们判断 angle 是否等于 Double.MAX_VALUE,如果是则 BorderAnchor 相当于一个 ChopboxAnchor,如果否则调用一个抽象方法 getBorderPoint()来计算我们的 锚点。BorderAnchor 的两个子类分别实现了计算椭圆和长方形锚点的算法, EllipseBorderAnchor 的代码如下所示: package org.eclipse.gef.examples.shapes.anchor; import org.eclipse.draw2d.IFigure; import org.eclipse.draw2d.geometry.Point; import org.eclipse.draw2d.geometry.PrecisionPoint; import org.eclipse.draw2d.geometry.Rectangle; public class EllipseBorderAnchor extends BorderAnchor { public EllipseBorderAnchor(IFigure figure) { super(figure); } @Override public Point getBorderPoint(Point reference) { //得到 owner 矩形,转换为绝对坐标 Rectangle r = Rectangle.SINGLETON; r.setBounds(getOwner().getBounds()); getOwner().translateToAbsolute(r); // 椭圆方程和直线方程,解 2 元 2 次方程 double a = r.width >> 1; double b = r.height >> 1; double k = Math.tan(angle); double dx = 0.0, dy = 0.0; dx = Math.sqrt(1.0 / (1.0 / (a * a) + k * k / (b * b))); if(angle > Math.PI / 2 || angle < -Math.PI / 2) dx = -dx; dy = k * dx; // 得到椭圆中心点,加上锚点偏移,得到最终锚点坐标 PrecisionPoint pp = new PrecisionPoint(r.getCenter()); pp.translate((int)dx, (int)dy); return new Point(pp); } } 值的注意的地方是我们可以通过 getOwner().getBounds()来得到 Owner 的边界 矩形,这是我们能够计算出锚点的重要前提。此外我们要注意的是必须把坐标 转换为绝对坐标,这是通过 getOwner().translateToAbsolute(r)来实现的。 最后,我们返回了锚点的绝对坐标,中间的具体计算过程只不过是根据椭圆方 程和射线方程求值而已。在我们的实现中,并没有用到参考点,如果你想有更 多的变数,可以把参考点考虑进去。 同样,RectangleBorderAnchor 也是如此,只不过求长方形边界点的方法稍微 不一样而已,我们就不一一解释了,代码如下: package org.eclipse.gef.examples.shapes.anchor; import org.eclipse.draw2d.IFigure; import org.eclipse.draw2d.geometry.Point; import org.eclipse.draw2d.geometry.PrecisionPoint; import org.eclipse.draw2d.geometry.Rectangle; public class RectangleBorderAnchor extends BorderAnchor { public RectangleBorderAnchor(IFigure figure) { super(figure); } @Override public Point getBorderPoint(Point reference) { // 得到 owner 矩形,转换为绝对坐标 Rectangle r = Rectangle.SINGLETON; r.setBounds(getOwner().getBounds()); getOwner().translateToAbsolute(r); // 根据角度,计算锚点相对于 owner 中心点的偏移 double dx = 0.0, dy = 0.0; double tan = Math.atan2(r.height, r.width); if(angle >= -tan && angle <= tan) { dx = r.width >> 1; dy = dx * Math.tan(angle); } else if(angle >= tan && angle <= Math.PI - tan) { dy = r.height >> 1; dx = dy / Math.tan(angle); } else if(angle <= -tan && angle >= tan - Math.PI) { dy = -(r.height >> 1); dx = dy / Math.tan(angle); } else { dx = -(r.width >> 1); dy = dx * Math.tan(angle); } // 得到长方形中心点,加上偏移,得到最终锚点坐标 PrecisionPoint pp = new PrecisionPoint(r.getCenter()); pp.translate((int)dx, (int)dy); return new Point(pp); } } 这样我们就完成了自定义的锚点实现。在 ConnectionAnchor 接口中,还有其他 4 个方法,虽然我们没有用到,但是有必要了解一下它们: void addAnchorListener(AnchorListener listener); void removeAnchorListener(AnchorListener listener); Point getReferencePoint(); IFigure getOwner(); addAnchorListener()和 removeAnchorListener()可以添加或删除一个锚点监 听器,这样我们可以知道锚点何时发生了移动。getOwner()则是返回锚点的 Onwer 图形,显然我们可以指定另外一个图形为锚点的 Owner,虽然这种需求可 能不太多。而 getReferencePoint()则是返回一个参考点,要注意的是,这个 参考点不是给自己用的,而是给另外一个锚点用的。比如对于源锚点来说,它 会调用目标锚点的 getReferencePoint()方法,而对于目标锚点来说,它会调 用源锚点的 getReferencePoint()方法。我们可以看看 ChopboxAnchor 的 getReferencePoint()实现,它返回的就是它的 Owner 的中心。 第四步,修改 EditPart 锚点实现完成后,我们需要修改 ShapeEditPart 使它能够使用我们定义的锚点。 EditPart 中的 getSourceConnectionAnchor(ConnectionEditPart connection) 和 getTargetConnectionAnchor(ConnectionEditPart connection)是决定使用 哪种锚点的关键方法。它们还有一个重载版本,用来处理 Reconnect 时的锚点 更新。这四个方法我们都需要修改,同时为了减少对象创建的次数,我们可以 在 ConnectionEditPart 里面添加两个成员用来保存源锚点对象和目标锚点对 象,如下: /* In ConnectionEditPart.java */ private BorderAnchor sourceAnchor; private BorderAnchor targetAnchor; public BorderAnchor getSourceAnchor() { return sourceAnchor; } public void setSourceAnchor(BorderAnchor sourceAnchor) { this.sourceAnchor = sourceAnchor; } public BorderAnchor getTargetAnchor() { return targetAnchor; } public void setTargetAnchor(BorderAnchor targetAnchor) { this.targetAnchor = targetAnchor; } 这样的话,在 ShapeEditPart 中应该检查一下 ConnectionEditPart 中的成员是 否有效,如果有效则直接返回,无效则创建一个新的锚点对象。而 Reconnect 时的代码稍微复杂一些,我们需要根据鼠标的当前位置,重新计算 angle 的值, 鼠标的当前位置是包含在 ReconnectRequest 里面的。我们给出 getSourceConnectionAnchor()的代码,对于 getTargetConnectionAnchor(), 只要将 Source 换成 Target 即可。 /* In ShapeEditPart.java */ /* * (non-Javadoc) * @see org.eclipse.gef.NodeEditPart#getSourceConnectionAnchor (org.eclipse.gef.ConnectionEditPart) */ public ConnectionAnchor getSourceConnectionAnchor (ConnectionEditPart connection) { org.eclipse.gef.examples.shapes.parts.ConnectionEditPart con = (org.eclipse.gef.examples.shapes.parts.ConnectionEditPart)connection; BorderAnchor anchor = con.getSourceAnchor(); if(anchor == null || anchor.getOwner() != getFigure()) { if(getModel() instanceof EllipticalShape) anchor = new EllipseBorderAnchor(getFigure()); else if(getModel() instanceof RectangularShape) anchor = new RectangleBorderAnchor(getFigure()); else throw new IllegalArgumentException("unexpected model"); Connection conModel = (Connection)con.getModel(); anchor.setAngle(conModel.getSourceAngle()); con.setSourceAnchor(anchor); } return anchor; } /* * (non-Javadoc) * @see org.eclipse.gef.NodeEditPart#getSourceConnectionAnchor (org.eclipse.gef.Request) */ public ConnectionAnchor getSourceConnectionAnchor(Request request) { if(request instanceof ReconnectRequest) { ReconnectRequest r = (ReconnectRequest)request; org.eclipse.gef.examples.shapes.parts.ConnectionEditPart con = (org.eclipse.gef.examples.shapes.parts.ConnectionEditPart)r. getConnectionEditPart(); Connection conModel = (Connection)con.getModel(); BorderAnchor anchor = con.getSourceAnchor(); GraphicalEditPart part = (GraphicalEditPart)r.getTarget(); if(anchor == null || anchor.getOwner() != part.getFigure()) { if(getModel() instanceof EllipticalShape) anchor = new EllipseBorderAnchor(getFigure()); else if(getModel() instanceof RectangleBorderAnchor) anchor = new RectangleBorderAnchor(getFigure()); else throw new IllegalArgumentException("unexpected model"); anchor.setAngle(conModel.getSourceAngle()); con.setSourceAnchor(anchor); } Point loc = r.getLocation(); Rectangle rect = Rectangle.SINGLETON; rect.setBounds(getFigure().getBounds()); getFigure().translateToAbsolute(rect); Point ref = rect.getCenter(); double dx = loc.x - ref.x; double dy = loc.y - ref.y; anchor.setAngle(Math.atan2(dy, dx)); conModel.setSourceAngle(anchor.getAngle()); return anchor; } else { if(getModel() instanceof EllipticalShape) return new EllipseBorderAnchor(getFigure()); else if(getModel() instanceof RectangularShape) return new RectangleBorderAnchor(getFigure()); else throw new IllegalArgumentException("unexpected model"); } } 到这里我们的修改就完成了,但是由于 Shapes 示例不允许创建多条连线,所以 我们还需要把 ConnectionCreateCommand 和 ConnectionReconnectCommand 中的 一些代码注释掉,这个内容就不做更多介绍了,大家可以下载本文附带的代码 查看具体的修改。最终,我们修改后的 Shapes 可以创建多条连线,并且可以手 动调整它们的锚点以避免重叠,如图 3 所示: 图 3. 新的 Shapes 示例 GEF 进阶,第二部分: Router 级别: 初级 马 若劼 (maruojie@cn.ibm.com), 软件工程师 2006 年 11 月 30 日 Router(连线路由器)是对连线进行布局的重要组件, 本文介绍了路由器的基本概念和应用场景,剖析了一个 连线路由器的接口并给出了一个简单实例。最后演示了 如何把自定义的路由器应用到图形中,使得不同的连线可以有不同的路由器。 ConnectionRouter(连线路由器) 图形之间连线的路线,是由连线路由器来决定的。在 Shapes Examples 中,使 用了最短路径路由器,这个路由器会帮我们绕开图形之间的障碍,选择一条最 短路径进行连接,如图 1 所示: 图 1. ShortestPathConnectionRouter 效果图 文档选项 打印本页 将此页作为电子邮件 发送 我们看到左右两边图形的连线绕过了中间的图形,在两处发生了转折。这就是 使用了最短路径路由器的效果。连线路由器可以安装到 Connection Layer(连 接层,关于层的概念我们在本系列下一篇中讲述),也可以针对某一条连接,所 以只要你愿意,每条连线都可以有不同的路由器。如果你没有为某条连线指定 一个路由器,那么缺省会使用连接层的路由器。Draw2D 自带了一些路由器的实 现,除了图 1 的 ShortestPathConnectionRouter,还有 ManhattanConnectionRouter 等路由器实现,如果这些自带的路由器不能满足 我们的需要,我们所要做的就是实现 ConnectionRouter 接口,实现一个自定义 路由器。ConnectionRouter 接口并不复杂,如下所示:  Object getConstraint(Connection connection);  void setConstraint(Connection connection, Object constraint);  void invalidate(Connection connection);  void route(Connection connection);  void remove(Connection connection); setConstraint 和 getConstraint 用来设置/得到连接上的 Constraint(约束), 所谓 Constraint 是指加在某个连线上的一些参数。我们可以看到 constraint 是一个 Object 类型,因为不同的路由器可能对 constraint 有不同的要求,对 于 ShortestPathConnectionRouter 来说,constraint 需要是一个 List 对象, 里面包含了所有的转折点。 invalidate 方法可以将一个连线置为无效,这样在下一次布局操作时,无效的 连接将被重新路由。remove 方法是将连线从路由器中删除,也就是路由器不会 再负责这条连线的布局,一般只有在删除一条连线的时候才会调用到,我们可 以在里面做一些清除工作,比如释放和连线相关的 cache。route 方法是路由操 作真正发生的地方,我们一般只需要实现 route 方法就可以了,如果你还想做 一些其他的操作,可以考虑实现其他方法。同样,一般是不推荐直接实现 ConnectionRouter 接口的,我们可以继承 AbstractRouter 类,这个类提供了 一些简单的或者空的实现,还提供了两个额外的方法 getStartPoint()和 getEndPoint()方便我们得到连线的两个端点。 SingleBendpointConnectionRouter 我们将实现一个自定义的路由器,叫做 SingleBendpointConnectionRouter, 它采用一种走直角的方式连接两个图形,如图 2 所示: 图 2. SingleBendpointConnectionRouter 效果图 也许这种路由器并没有太多的通用性,但是我们只是作为一个例子演示路由器 的实现,了解了基本方法之后,再去实现更复杂更实用的路由器也就大同小异 了。 路由器实现的前提 显然我们无法凭空的计算出线路的走向,一条连线的具体路线和很多因素有关, 比如锚点、图形的位置和大小,图形之间的相互关系,等等。所以我们需要能 够访问到这些必须的信息,在 Connection 接口中,我们有 getTargetAnchor() 和 getSourceAnchor()可以让我们得到锚点,而在 ConnectionAnchor 接口中(参 见本系列第一部分),我们有 getOwner()这样的方法,可以得到图形。这些必 要的方法为我们实现路由器提供了可能。 实现 route 方法 route 方法的代码如下: public void route(Connection conn) { // 清空连线的所有点 PointList points = conn.getPoints(); points.removeAllPoints(); // 得到目标和源参考点 Point sourceRef = conn.getSourceAnchor().getReferencePoint(); Point targetRef = conn.getTargetAnchor().getReferencePoint(); 回页首 A_POINT.setLocation(sourceRef.x, targetRef.y); // 得到起始点和结束点 Point startPoint = conn.getSourceAnchor().getLocation(A_POINT); Point endPoint = conn.getTargetAnchor().getLocation(A_POINT); // 添加起始点 A_POINT.setLocation(startPoint); conn.translateToRelative(A_POINT); points.addPoint(A_POINT); // 添加转折点 A_POINT.setLocation(sourceRef.x, targetRef.y); conn.translateToRelative(A_POINT); points.addPoint(A_POINT); // 添加结束点 A_POINT.setLocation(endPoint); conn.translateToRelative(A_POINT); points.addPoint(A_POINT); // 设置连线经过的所有点 conn.setPoints(points); } 一条连线实际上是通过一系列的点来描述的,而 route 方法的实际任务也就是 计算出这些点的位置。所以我们一开始就得到了这条连线的点序列(PointList 对象),然后清空它,重新计算这些点。在我们这个路由器的设计里,一条连 线由三个点组成:分别是起始点,转折点和结束点,它们构成了两条垂直的直 线。起始点和结束点(也就是锚点)我们都已经了解如何得到了,中间的转折 点,也很容易得出,我们就不解释了。要指出的是,我们需要把它们的坐标转 换为相对坐标再添加,同时在添加完成之后,我们还需要调用 setPoints()方 法,这样才会生效。 所以说实现一个路由器的过程是很简单的,复杂之处在于路由算法,但这已经 不属于 GEF 的范畴,所以我们就不讨论它了。 改变连接层的路由器 我们只是实现了路由器,还没有把这个路由器设置为缺省的路由器,所以我们 还要做一点小修改,在 DiagramEditPart 的 createFigure()方法里,将 ShortestPathConnectionRouter 替换为 SingleBendpointConnectionRouter 即 可。 待改进的地方 我们的自定义路由器很简单,但是它也有一点小问题,当两个图形在垂直或水 平方向有重叠时,连线看上去有点不正常,如图 3 所示: 图 3. 一点小 bug 这只是由于我们的路由器算法不是很完善,没有考虑到所有情况而已。你可以 尝试修改一下 route 的算法,改正这个问题,我们这里就不详细演示了。 为连线指定路由器 我们目前是将路由器安装到了连接层,于是所有的连线都会使用同一个路由器, 有些时候为了让布局更加灵活,我们需要为一条或多条连线指定一个不同的路 由器。由于 Connection 接口中提供了 setConnectionRouter()方法,因此这是 可以实现的。 修改 model 为了让连线知道它要使用何种路由器,我们需要修改连线的 model,把当前的 路由器种类存进去,我们在 Connection.java 里面加上一个 routerId 的成员, 同时再定义一些表示不同路由器的常量: // router id constant public static final int SHORTEST_PATH_ROUTER = 0; public static final int MANHATTAN_ROUTER = 1; public static final int SINGLE_BENDPOINT_ROUTER = 2; private int routerId; public int getRouterId() { return routerId; 回页首 } public void setRouterId(int routerId) { this.routerId = routerId; } 添加属性 为了能够随时修改连线的路由器,我们为连线添加一个 router 属性,由于这些 内容不在本文讨论范围中,所以不一一描述了。完成之后,我们可以在属性视 图中看到连线的路由器属性和可选值: 图 4. Router 属性 于是我们就可以让多种连线方式共存了,对于复杂的图形来说这样可以尽量避 免连线重叠,增加布局的美观程度。 图 5. 使用多种路由器 GEF 进阶,第三部分: Layer 级别: 初级 马 若劼 (maruojie@cn.ibm.com), 软件工程师 2006 年 11 月 30 日 在 GEF 中,画板是由多个 Layer(层)组成的,层也可以 看作是对图形进行的一种分类管理,它使图形更加明 确,层次清晰。程序结构上也更容易理解和维护。层同 样也是可以定制的,本文演示如何实现并插入一个自定义层,并指出了插入自 定义层所应该注意的一些问题。 Layer (层) GEF 的图形是可能分布在多个层上面的,比如连线是放在 Connection Layer(连 接层)上的,而普通的图形(比如 Shapes Example 里面的长方形和椭圆)是放 置在 Primary Layer(主层)上的。不同类型的图形放置在不同的层上,既易于 管理又结构清晰,因此层是一个非要重要的功能。层其实也是一个图形,和其 他图形一样都继承自 Figure 类,所以我们也可以象添加普通图形一样添加层, 只不过方式有点不同而已。 我们首先需要知道层是在哪里创建的,才好动手修改它们。在 GEF 中,每一个 文档选项 打印本页 将此页作为电子邮件 发送 EditPart Viewer 都有一个特殊的 EditPart,叫做 Root,而层的创建就在其中。 因此要添加自定义的层,首先的任务就是扩展 Root EditPart,一般来说,我 们使用的是 ScalableFreeformRootEditPart。 提示: 使用何种 Root EditPart 也是可以自己控制的,只需要在重载 GraphicalEditor 的 configureGraphicalViewer 方法,指定使用我们自己的 Root EditPart 即可。以下是一段示例代码,斜黑体部分就是具体的调用: protected void configureGraphicalViewer() { super.configureGraphicalViewer(); GraphicalViewer viewer = getGraphicalViewer(); viewer.setRootEditPart(new ScalableFreeformRootEditPart()); // more code …………… } 所以,这是我们能够自定义层的一个前提。 ScalableFreeformRootEditPart 的 createLayers()方法是创建层的关键所在, 仔细观察其实现,不难看出 GEF 缺省定义了如图 1 所示的层次结构: 图 1. ScalableFreeformRootEditPart 缺省层结构 提示: 先添加的层在下,后添加的层在上 我们看到缺省包含了很多层,而且层中还有子层,每个层都有一个关键字来标 识,我们从下到上做一个简要的描述:  Grid Layer: 网格层,用来显示一个网格,帮助你定位图形  Primary Layer: 主层,大部分的图形都放置在这个层  Connection Layer: 连接层,连线都放置在这一层  Printable Layer: 可打印层,这个层并没有实际作用,只是用来包含主 层和连接层  Scaled Feedback Layer: 扩展反馈层,所谓反馈是指操作时显示的一些 提示信息,比如你拖动一个图形时,会显示一个虚影,这就是反馈  Scalable Layer: 和 Printable Layer 一样,只是一个容器层  Handle Layer: Handle 是指一些可以拖动的小方块,比如选择一个图形 时,会显示八个用于 Resize 的 Handle  Feedback Layer: 也是一个反馈层  Guide Layer: 帮助层 所有的层我们都可以通过 getLayer()方法得到,因此我们有很大的自由去控制 这些层的属性,但是如果我们要添加一个层或者修改一个层的行为,我们必须 实现自己的 RootEditPart。 Background Layer (背景层) 出于演示的目的,我们添加一个比较简单的层,叫做 Background Layer,它的 用处就是在画布上显示一个渐进的背景,我们把这个层添加到 Primary Layer 之前,避免覆盖主层上的图形。 注意,我们添加的这个层只是出于演示目的,实际上添加一个背景色并不用费 此周章。我们只是为了说明如何添加一个自定义的层。 创建 BackgroundLayer 类 我们首先来创建类,对于层,我们只要继承 Layer 类就可以了,而 Layer 本身 继承自 Figure,它没有添加任何新的方法,如果你扩展过 Figure 类的话,对 此就应该比较熟悉了。由于我们只是画个渐进背景色,所以只需要重载一下 paintFigure 方法就可以了。如下 package org.eclipse.gef.examples.shapes.layer; 回页首 import org.eclipse.draw2d.ColorConstants; import org.eclipse.draw2d.FreeformLayer; import org.eclipse.draw2d.Graphics; public class BackgroundLayer extends FreeformLayer { public static final String BACKGROUND_LAYER = "Background Layer"; public BackgroundLayer() { setOpaque(true); } @Override protected void paintFigure(Graphics graphics) { if(isOpaque()) { graphics.setForegroundColor(ColorConstants.white); graphics.setBackgroundColor(ColorConstants.lightBlue); graphics.fillGradient(getBounds(), true); } } } 我们最终扩展的是 FreeformLayer 而不是 Layer, 如果直接扩展 Layer 的话, 我们的层会无法自动改变大小。我们推荐从 FreeformLayer 开始你的工作。 实现 MyRootEditPart 由于我们只需要插入一个层,所以我们重载 ScalableFreeformRootEditPart 中 的 createPrintableLayers 方法就可以了。再次提醒的是要注意插入的位置, 先加入的层在下,后加入的在上。 package org.eclipse.gef.examples.shapes.parts; import org.eclipse.draw2d.ConnectionLayer; import org.eclipse.draw2d.FreeformLayer; import org.eclipse.draw2d.FreeformLayeredPane; import org.eclipse.draw2d.LayeredPane; import org.eclipse.gef.editparts.ScalableFreeformRootEditPart; import org.eclipse.gef.examples.shapes.layer.BackgroundLayer; public class MyRootEditPart extends ScalableFreeformRootEditPart { @Override protected LayeredPane createPrintableLayers() { FreeformLayeredPane layeredPane = new FreeformLayeredPane(); layeredPane.add(new BackgroundLayer(), BackgroundLayer.BACKGROUND_LAYER); layeredPane.add(new FreeformLayer(), PRIMARY_LAYER); layeredPane.add(new ConnectionLayer(), CONNECTION_LAYER); return layeredPane; } } 让我们自定义的 RootEditPart 生效是相当简单的事,只要修改 ShapesEditor. configureGraphicalViewer(),将 ScalableFreeformRootEditPart 替换为 MyRootEditPart 即可。就不列出代码了。完成之后我们就可以看到画板有了一 个渐进式的背景,如图 2 所示: 图 2. 背景层 GEF 进阶,第 4 部 分: Locator 级别: 中级 马 若劼 (maruojie@cn.ibm.com), 软件工程师, IBM 中国软件开发中心 2007 年 10 月 25 日 本文是 GEF 进阶的第四部分,主要描述了 Locator 的概 念和使用方法。Locator 是 一个图形定位器,用来动态 的决定某个图形相对于另外一个图形的位置,因此可以 用来构造一些 复杂的图形或者实现一些比较有趣的功能。由于 Eclipse 3.3 已 经发布,本文的示例代码是在 Eclipse 3.3, GEF 3.3 运行调试的。 本文是 GEF 进阶的第四部分,主要描述了 Locator 的概念和使用方法。Locator 是一个图形定 位器,用来动态的决定某个图形相对于另外一个图形的位置,因 此可以用来构造一些复杂的图形或者 实现一些比较有趣的功能。由于 Eclipse 3.3 已经发布,本文的示例代码是在 Eclipse 3.3, GEF 3.3 运行调试的。 Locator Locator,顾名思义,是一个定位器。我们先来看看这个接口: 清单 1. Locator 接口 public interface Locator { void relocate(IFigure target); } 这个接口非常简单,只有一个方法,参数是一个 IFigure,所以首先可以明确 的是:Locator 是一个 图形定位器。那么一个 Locator 用在什么地方,又为什 么要用 Locator 呢?一般来讲,如果你希望一个图形 能跟着另外一个图形移 动,那么 Locator 就很有用了。因此 Locator 是相对定位的有利工具。我们下 文档选项 打印本页 将此页作为电子邮件 发送 样例代码 面来介 绍 Locator 的几个典型应用。 Connection Label 我们在运行 GEF shapes 的例子时,可以看到图形之间可以有连线,但是连线上 没有文字说明,如果有 文字说明的话,会有如下图所示的效果: 图 1. 连线上的文字说明 从图 1 看到连线上有了一个“Label”的字样,你可以把它叫做 Connection Label(连线标签)。总之, 这个标签是附着在连线上的。不光如此,如果你 拖动连线,这个标签也会跟着移动,这就是 Locator 的功劳。我们先来修改 shapes 例子的代码,给连线加上这样的标签。要修改的地方在 ConnectionEditPart 的 createFigure 方法里,修改后如下所示: 清单 2. 为连线加上 Label protected IFigure createFigure() { final PolylineConnection connection = (PolylineConnection) super.createFigure(); connection.setTargetDecoration(new PolygonDecoration()); // arrow at target endpoint connection.setLineStyle(getCastedModel().getLineStyle()); // line drawing style // add a label final Label label = new Label("Label"); label.setOpaque(true); connection.add(label, new MidpointLocator(connection, 0)); return connection; } 回页首 加这么一个 label 确实很简单,我们先创建一个 Label 对象,再将它加到连线 里面,所以本质上 Label 是连线的 一个孩子。在添加的时候,我们使用了 MidpointLocator, 这是 GEF 缺省带的一个 Locator,作用是把图形定位 到连 线的中点。所以我们看到 Label 的中点始终和连线的中点相同。而连线的中点 又叫做 MidpointLocator 的 reference point(参考点), 我们在本系列的第一 部分里面介绍过 Anchor(锚点),这个参考点的概念与其是类似的。 随之而来我们会发现一个局限性:这个 Label 永远只能在连线的中心,无法移动到其它的地 方。在有些时候这 确实是个问题:比如某个图 特别复杂,连线特别多,以致于连线上的标签 都重叠在了一起,可是由于它不能拖动,所 以 看上去很不美观。如果我们希望这个 Label 能 被拖动该怎么办呢?有两个方案: 1. 为这个 Label 创建一个 EditPart,负责 控制这个 label 的移动,改变大小,等 等。 2. 使用一个自定义的 Locator,并处理 Label 的鼠标事件,随时刷新它的位置 第一种方法需要的代码稍多且不符合本文主题。我们来看看第二种方法该如何 实施。 确定 Locator 的策略 首要的问题是我们的 Locator 如何工作呢?我们已经使用了 MidpointLocator, 它可以随时定位到连线的中点, 如果我们给它加一个偏移,不就可以实现拖动 到任何位置的功能了吗?这个概念如图 2 所示: 图 2. 使用偏移表示标签中点 MidpointOffsetLocator 策略定下来之后我们来实现一个 MidpointOffsetLocator,它直接继承自 MidpointLocator,但是我们给它添 加一个 offset 的成员变量,这样我们就可 以控制标签中点的位置了。代码如下所示: 清单 3. MidpointOffsetLocator 的实现 提示: 再仔细研究一下 add()方法会发现,add 方法 的第二个参数其实是一个 Object 类型, 参数名是 constraint. 也就是说 Locator 在这里充当了一个 constraint 的角色。 Constraint 是被布局管理器 解释的, 如果你想自定义一 个 constraint 类型,则需要 自定义一个布局管理器。本 文不详细解释这些内容。 public class MidpointOffsetLocator extends MidpointLocator { private Point offset; public MidpointOffsetLocator(Connection c, int i) { super(c, i); offset = new Point(0, 0); } @Override protected Point getReferencePoint() { Point point = super.getReferencePoint(); return point.getTranslated(offset); } public Point getOffset() { return offset; } public void setOffset(Point offset) { this.offset = offset; } } 关键的方法在于我们覆盖了 getReferencePoint(),让它返回之前加上我们的 偏移量。就这么简单,我们自己的 Locator 诞生了! 处理鼠标事件 Locator 有了,下面的问题就是如何才能在适当的时机修改这个偏移量呢?第 一想到的就是鼠标事件监听器。由于 Figure 本身 已经支持添加各种各样的监 听器,所以这一步也非常简单。我们继续修改 ConnectionEditPart 的 createFigure 方法,给我们的 Label 加上鼠标事件处理方法,如下所示: 清单 4. 添加鼠标事件监听器 protected IFigure createFigure() { // 省略其它无关代码 ...... // add a label final Label label = new Label("Label"); label.setOpaque(true); connection.add(label, new MidpointOffsetLocator(connection, 0)); label.addMouseListener(new MouseListener() { public void mouseDoubleClicked(MouseEvent me) { } public void mousePressed(MouseEvent me) { anchorX = me.x; anchorY = me.y; me.consume(); } public void mouseReleased(MouseEvent me) { me.consume(); } }); label.addMouseMotionListener(new MouseMotionListener() { public void mouseDragged(MouseEvent me) { dx += me.x - anchorX; dy += me.y - anchorY; anchorX = me.x; anchorY = me.y; Object constraint = connection.getLayoutManager().getConstraint(label); if(constraint instanceof MidpointOffsetLocator) { ((MidpointOffsetLocator)constraint).setOffset(new Point(dx, dy)); label.revalidate(); } me.consume(); } // 省略其它无关代码 ...... }); return connection; } 要注意的有三点,第一我把 MidpointLocator 替换成了我们自己的 Locator, 第二我在 ConnectionEditPart 加了四个成员变量(dx, dy, anchorX, anchorY) 来跟踪鼠标位置,第三我在鼠标拖动事件中修改偏移量并刷新它,但是为了安 全起见,我先判断了 constraint 类型。 小节总结 我们现在完成了 Label 的拖动功能,不过你可以发现一些可以继续提高的地方, 最明显的莫过于 Label 的位置不能保存。我们应该把这个偏移量保存到 Connection 模型中去。这个问题和本文无关,留给读者做个练习。本小节的示 例代码在 org.eclipse.gef.examples.shapes_locator_step1.zip 中,大家可 以看看实际的效果。 Handle Locator 另一个典型的应用是用在 handle 中,所谓 handle,就是你选择一个图 形之后,在它的边框周围出现的一些辅助性的图形,比如一些小方块。如下图 所示: 图 3. 图形周围的 8 个 handle 因为 Handle 也使用了 Locator,所以我们在拖动图形的时候,Handle 的位置也 会随之更新。回头看看上一小节中的 Label,可以发现一个美中不足的是:Label 的周围没有 handle,这样的话用户可能会不知道我们的 Label 是可以拖动的, 对用户不太直观,如果能在连线被选择的时候把 Label 的周围也加上 handle, 那么用户可以容易的发现这个 Label 原来也支持拖动,这有利于提高用户友好 度。本小节的例子就来实现这个功能。 MyConnectionEndpointEditPolicy 追究 handle 的发源地,会发现连线上的 handle 是在 ConnectionEndpointEditPolicy 中创建 的, 我们要添加 handle,自然就是要实现一个 自己的 EditPolicy 了。关于 EditPolicy 的相 关概念,这里不做赘述。观察 ConnectionEndpointEditPolicy 的实现,可以看 到一个有趣的方法 createSelectionHandles,那么我们来覆盖它,如下: 清单 5. 为 Label 添加 Handle public class MyConnectionEndpointEditPolicy extends ConnectionEndpointEditPolicy { |-------10--------20--------30--------40--------50--------60--------70-------- 80--------9| |-------- XML error: The previous line is longer than the max of 90 characters ---------| @Override protected List createSelectionHandles() { List handles = super.createSelectionHandles(); List children = (List)getHostFigure().getChildren(); for(IFigure figure : children) { if(figure instanceof Label) 回页首 提示:本系列第三部分介绍 了 Layer 的概念,Handle 也 是存在一个单独的层中的, 叫做 Handle Layer handles.add(new MoveHandle((GraphicalEditPart)getHost(), new MoveHandleLocator(figure))); } return handles; } } 我们遍历了连线的所有孩子,如果它是一个 Label,就为它创建一个 MoveHandle,MoveHandle 使用了 MoveHandleLocator,这些类都是 GEF 自带的, 不需要费什么功夫。 有了自定义的 EditPolicy,别忘了在 ConnectionEditPart 中替换原来的 EditPolicy,这部分代码过于简单,省略不提。 修改 Label 上的鼠标事件处理代码 到这里为止,我们的 Label 就有了 handle 了,如下图所示: 图 4. Label 周围的 MoveHandle MoveHandle 的效果是在 Label 周围画了一个矩形框, 表明这个图形可以移动。 不过我们还有一个小问题:点击 Label 的时候 MoveHandle 不出来,要解决这个 问题也很简单,在 Label 的鼠标点击事件中增加一行 getViewer().appendSelection(ConnectionEditPart.this)就可以了。 GEF 进阶,第 5 部 分: Feedback 级别: 中级 马 若劼 (maruojie@cn.ibm.com), 软件工程师, IBM 中国软件开发中心 2007 年 10 月 25 日 Viewer 是 GEF 中顶层的界面组件,可以认为 Viewer 就 是一块画板,里面放什么东西完全可以由你控制。在 GEF 中,这样的画板不止一块,其外观也不太相同,我们也 可以添加自己的 Viewer。Viewer 在内部应用了 MVC 的设计模式,要自定义一个 Viewer,必须完成 MVC 的所有元素,本文演示了这个基本的过程。 Viewer 是 GEF 中顶层的界面组件,可以认为 Viewer 就是一块画板,里面放什 么东西完全可以由你控制。在 GEF 中,这样的画板不止一块,其外观也不太相 同,我们也可以添加自己的 Viewer。Viewer 在内部应用了 MVC 的设计模式,要 自定义一个 Viewer,必须完成 MVC 的所有元素,本文演示了这个基本的过程。 Viewer GEF 中的一些常见的组件其实都是 Viewer,如下图所示: 图 1. GEF 中一些缺省的 Viewer 文档选项 打印本页 将此页作为电子邮件 发送 样例代码 图 1 列出了 GEF 中一些常见的 Viewer,可以看出来它们的外观上有不小的差异, 但是它们在本质上都是一样的。它们都实现了 EditPartViewer 接口,并且都是 MVC 模式的。我们不要被它们的外观所蒙蔽。 上面提到的 EditPartViewer 接口是 GEF 中的 Viewer 必须实现的一个接口,这 是一个较为庞大的接口。仔细的浏览其方法,可以粗略的了解到 Viewer 的一些 能力,比如拖放支持,上下文菜单,键盘事件等等。从这个接口派生出了很多 类,但是基本上可以分为两类:有画板支持和无画板支持的。比如从 AbstractEditPartViewer 派生出来两个子类:GraphicalViewerImpl 和 TreeViewer。GraphicalViewerImpl 也就是我们通常进行可视化编辑的那块区 域。而 TreeViewer 则是图 1 中 Outline 视图显示的内容,在 Outline 视图中, 我们没有办法进行所见即所得的编辑,也就是我说的“没有画板支持”。 GEF 自带的一些 Viewer 已经可以满足我们大部分的需要,如果你有一些特殊的 需求,需要实现一个特别的 Viewer,也可以很容易的做到。本文的其余部分就 来添加一个简单的 Viewer,逐步的解释添加 Viewer 时需要了解的概念和注意 的问题。 自定义 Viewer 回页首 我们打算在 shapes 示例代码的基础上,在编辑区域添加一个 Viewer,这个 viewer 的基本功能就是按顺序显示所有的图形,但是不显示连线,同时在选择 其中的图形的时候,主编辑区域的图形也会被选择。 模型层 由于 Viewer 是 MVC 架构的组件,因此要添加一个自定义的 Viewer,比如兼顾 MVC 的所有元素,首先是模型层。幸运的是,我们不需要自定义什么模型,当 然你可以这样做,不过在本文的例子中,我们沿用 shapes 示例代码中的模型。 表示层 从现有的类中继承是快速实现自定义 Viewer 的方法,我们可以从 ScrollingGraphicalViewer 继承出我们自己的 Viewer,如下所示: 清单 1. 继承 ScrollingGraphicalViewer public class ShapeViewer extends ScrollingGraphicalViewer { protected void hookControl() { super.hookControl(); FigureCanvas canvas = getFigureCanvas(); canvas.getViewport().setContentsTracksWidth(true); canvas.getViewport().setContentsTracksHeight(false); canvas.setHorizontalScrollBarVisibility(FigureCanvas.AUTOMATIC); canvas.setVerticalScrollBarVisibility(FigureCanvas.NEVER); } } 我定义了一个 ShapeViewer 作为我们的表示层,这个类很简单,只覆盖了父类 的 hookControl()方法。hookControl()本质上只是做一些初始化工作,比如配 置一下滚动条。我们可以覆盖或者添加更多的方法,但是我并不打算把它弄的 很复杂,为了方便我们理解这个过程,代码越少越好。 控制层 控制层是工作相对多的一块,首先的一个问题是:我们可以不可以重用 shapes 示例中已有的那些 EditPart 呢?大部分代码是可以重用的,因为我们这个 Viewer 也是有画板支持的,所以本质上也都应该继承自 AbstractGraphicalEditPart。但是有些代码,比如连线相关的代码,我们就不 需要,因为我们不支持显示连线。还有一些 EditPolicy 相关的代码,则要看你 的具体需要了,如果需要相关的角色,则可以重用。为了简单起见,我们使用 了 FlowLayout 来安排图形,因此我们为 Diagram 添加了 FlowLayoutEditPolicy。 因为我们希望代码越简单越好,所以我们不支持创建 Command,全部设为返回 null。 由于这部分的代码基本上是 shapes 已有代码的一个子集,所以我们不一一列 出,大家可以在随本文提供的源代码中看到细节。这里只提一下我为 Diagram 和 Shape 分别创建了 EditPart,名为 SimpleDiagramEditPart 和 SimpleShapeEditPart. 既然我们给自己的 Viewer 定义了一些 EditPart,就必然需要一个 EditPart 工 厂。如下: 清单 2. 为我们的 Viewer 创建 EditPart 工厂 public class SimpleEditPartFactory implements EditPartFactory { public EditPart createEditPart(EditPart context, Object model) { EditPart part = null; if(model instanceof ShapesDiagram) part = new SimpleDiagramEditPart(); else if(model instanceof Shape) part = new SimpleShapeEditPart(); if(part != null) part.setModel(model); return part; } } 到这里为止,我们的控制层就算完成了。出于简单的考虑,很多 EditPolicy 和 Command 还没有加上,但是它已经有个样子了。其它的功能作为练习留给读者 完成。 组装与初始化 可是我们的工作并没有做完,Viewer 虽然有了,可是显示在哪里呢?我们还需 要把它添加到编辑器中,而且这一部分也并非平淡如水,还是有不少工作可以 做的。我们先给出完成后的 ShapeEditor 的 createPartControl()代码: 清单 3. 组装各个部分 public void createPartControl(Composite parent) { Composite topLevel = new Composite(parent, SWT.NONE); GridLayout layout = new GridLayout(); layout.marginHeight = 0; layout.marginWidth = 0; layout.verticalSpacing = 0; topLevel.setLayout(layout); Composite top = new Composite(topLevel, SWT.NONE); top.setLayout(new FillLayout()); top.setLayoutData(new GridData(GridData.FILL_BOTH)); Composite bottom = new Composite(topLevel, SWT.BORDER); bottom.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); bottom.setLayout(new FillLayout()); super.createPartControl(top); ShapeViewer viewer = new ShapeViewer(); viewer.createControl(bottom); getEditDomain().addViewer(viewer); getSelectionSynchronizer().addViewer(viewer); viewer.setEditPartFactory(new SimpleEditPartFactory()); viewer.setRootEditPart(new ScalableRootEditPart()); viewer.setContents(getModel()); } 为了给新的 Viewer 腾一个地方,我把编辑器划分成上下两个部分,上面的部分 仍然是传统的可视化编辑区和调色板, 下面就用来放置新的 Viewer。值得注 意的是最后五行,首先,最后三行是把我们的模型层,表示层和控制层连接了 起来, 这是必须的,就好像机器要运转,零件一个也不能少的道理一样。其次, 倒数第四第五行是一些附加工作,没有也没有 关系,不过我们还是应该明白它 们是什么意思。getEditDomain().addViewer(viewer)把 我们的 Viewer 添加到 了 EditDomain 中,这样 EditDomain 会记住我们的 Viewer 并且向我们派发一些 事件,比如鼠标事件。 而 getSelectionSynchronizer().addViewer(viewer) 的作用是把我们的 Viewer 里面的选择 事件和其它 Viewer 同步起来,这样做的 效果是用户在其它 Viewer 里选择了一个图形之后,我们的 Viewer 里面也会反 映出来, 反之亦然。 还有很多其它的事可以做,比如装载键盘事件处理器,添加拖放支持,等等。 所以我把这一部分的工作总结为“组装和初始化”。具体的步骤不一一列举了, 留待读者完成。 完成后的 Viewer 如下图所示: 图 2. 自定义 Viewer 效果图 从图 2 中看到,我实现了所有计划的功能,把所有的图形按照创建的顺序排成 了一排,并且上下两个 Viewer 的选择也是同步的。 GEF 进阶,第 6 部 分: Feedback 级别: 中级 马 若劼 (maruojie@cn.ibm.com), 软件工程师, IBM 中国软件开发中心 2007 年 10 月 25 日 Feedback(反馈)指的是 GEF 中对用户操作的一种回显, 这种回显一般来说是视觉上的,但是也不一定。完全可 以由用户来定制。本文介绍 Feedback 的相关概念,并 通过实例演示其定制过程。 Feedback(反馈)指的是 GEF 中对用户操作的一种回显,这种回显一般来说是 视觉上的,但是也不一定。完全可以由用户来定制。本文介绍 Feedback 的相关 概念,并通过实例演示其定制过程。 Feedback Feedback(反馈)是对用户操作的某种响应,我们先来一点感性认识,见下图: 文档选项 打印本页 将此页作为电子邮件 发送 样例代码 图 1. GEF 中缺省的反馈形式 从图 1 看到,在拖动某个图形时,我们一般会看到一个虚影,这就叫做反馈。 它告诉了用户这个图形在松开鼠标之后将会被放置在什么地方,这是一种很好 的提高用户友好度的方式,也是反馈的主要目的。 反馈有两种:Source Feedback(源反馈)和 Target Feedback(目标反馈)。鼠标 操作的图形叫源,鼠 标移动时经过的地方叫目的。在图 1 中,那个椭圆就是源, 而那个虚影就是源反馈,而目标就是你鼠标具 体指向的地方了,如果你的鼠标 停在一个矩形上,那么那个矩形就是目标,由目标提供的反馈就叫目标反 馈。 一般我们在 GEF 中见到的反馈都是一些视觉上的效果,比如图 1 中的虚影。如 果你愿意,你可以播放一 段音乐或者别的什么,所以反馈并不限于视觉效果。 对于视觉上的效果而言,GEF 提供了一个缺省的层, 叫做 Feedback Layer(反 馈层),我们看到的图形一般都是添加到反馈层的。 还有一个问题就是反馈的触发时机。反馈到底 是在什么时候才会出现呢?对于源反馈,它是 在拖动的时候触发的,对于目标反馈,鼠标的 移动,进入或者拖动都有可能触发。但是这只是一般的情况,是 GEF 缺省的情 况。如果你需要在更多的场合显示反馈,需要多实现一些方法罢了,但是也并 不复杂。 最后一个问题是该显示什么样的反馈。我们先来看看 EditPart 接口中两个和反 馈显示有关方法的原型: 清单 1. 显示反馈的方法原型 public interface EditPart extends IAdaptable { // other methods ...... void showSourceFeedback(Request request); void showTargetFeedback(Request request); } 方法的参数为 Request 对象。所以 Request 对象是我们判断显示何种反馈的主 要依据。GEF 的缺省 Request 类型都定义在 RequestConstants 接口中,而我们 自己也可以定义 Request。于是,“显示什么样的反馈”这个问题也就清楚了。 提示: 不了解层的概念的请 回顾本系列第三部分 自定义 Feedback 概念上的东西只有这么多,现在我们来尝试定制反馈行为。我们要实现的功能 很简单,拖动一个图形的时候,显示一个字符串表示是什么图形。然后在拖动 的过程中,显示一个坐标信息表示当前拖动到了什么位置。 自定义源反馈 观察 AbstractEditPart 的代码可以发现,它实际是把 showSourceFeedback 和 showTargetFeedback 都转发给了 EditPolicy 来完成,所以自定义 Feedback 正 确的做法应该是实现一个自定义的 EditPolicy 并安装到 EditPart 中。这里又 有一个问题,应该安装到哪个角色上呢?和拖动图形相关的角色叫 Primary Drag,这个角色的 Policy 是由父类提供的。所以我们找到 DiagramEditPart, 修改它的 ShapesXYLayoutEditPolicy 内部类,覆盖其父类的 createChildEditPolicy 方法。如下所示: 清单 2. 改变孩子的 EditPolicy private static class ShapesXYLayoutEditPolicy extends XYLayoutEditPolicy { // other methods ...... @Override protected EditPolicy createChildEditPolicy(EditPart child) { return new CustomResizableEditPolicy(); } } 而 CustomResizableEditPolicy 的代码如下: 清单 3. CustomResizableEditPolicy 的实现 public class CustomResizableEditPolicy extends ResizableEditPolicy { @Override protected IFigure createDragSourceFeedbackFigure() { IFigure f = null; if(getHostFigure() instanceof Ellipse) f = new Label("Source: Ellipse"); else f = new Label("Source: Rectangle"); 回页首 addFeedback(f); return f; } } 由于我只是继承了现成的 ResizableEditPolicy, 所以留给我的工作并不多, 简单的替换成我们想显示的 Feedback 即可。从以上代码上可以看出,我返回的 是一个标签,所以我们的源反馈从一个虚影变成了一个标签。如下图所示: 图 2. 自定义源反馈效果 自定义目标反馈 现在我们再来考虑目标反馈。我们希望在拖动的时候显示一个坐标信息,那么 拖动这个动作是一个布局相关的东西,应该是归一个具有 Layout 角色的 EditPart 负责的。在我们的例子里,图形是放在 Diagram 里面的,所以对于目 标反馈,我们仍然需要修改 ShapesXYLayoutEditPolicy。代码如下所示: 清单 4. 实现目标反馈 protected void showLayoutTargetFeedback(Request request) { if(request instanceof ChangeBoundsRequest) { ChangeBoundsRequest r = (ChangeBoundsRequest)request; Point mouse = r.getLocation(); if(feedback == null) { feedback = new Label("" + mouse.x + ", " + mouse.y); addFeedback(feedback); } else ((Label)feedback).setText("" + mouse.x + ", " + mouse.y); feedback.setBounds(new Rectangle(mouse.translate(0, 20), feedback.getPreferredSize())); } } protected void eraseLayoutTargetFeedback(Request request) { if(feedback != null) removeFeedback(feedback); feedback = null; } 我们只需要覆盖两个方法,因为父类已经为我们处理了其它情况。如果你要追 根究底的话,我们来看看 LayoutEditPolicy 中的 showTargetFeedback()方法: 清单 5. 父类做了什么 public void showTargetFeedback(Request request) { if (REQ_ADD.equals(request.getType()) || REQ_CLONE.equals(request.getType()) || REQ_MOVE.equals(request.getType()) || REQ_RESIZE_CHILDREN.equals(request.getType()) || REQ_CREATE.equals(request.getType())) showLayoutTargetFeedback(request); if (REQ_CREATE.equals(request.getType())) { CreateRequest createReq = (CreateRequest)request; if (createReq.getSize() != null) showSizeOnDropFeedback(createReq); } } LayoutEditPolicy 为我们判断了很多 request 类型,和拖动相关的就是 REQ_MOVE。所以我们不用再做判断了。我们添加的方法中,一个用来创建反馈 图形,一个用来移除反馈图形,逻辑上非常简单。要注意 addFeedback 和 removeFeedback 的实现,它先得到反馈层,再把我们创建的图形添加到反馈层。 现在运行整个例子,可以看到鼠标拖动的时候有一个坐标信息显示在附近。如 图所示: 图 3. 自定义目标反馈效果 结束语 回页首 我们介绍了 Feedback 的概念并给出了具体示例。需要强调的是:学习 GEF,重 要的是了解它的流程, 了解了流程之后才能更好的理解细节。对于 Feedback 而 言,它由 EditPart 负责,转发给 EditPolicy,再根据 Request 类型创建反馈 图形,添加到反馈层。本文的例子只覆盖了一些很基本的情况,这里我提出一 些高级的功能,留给有兴趣的读者完成: 1. 实现非图形化的反馈,比如播放声音 2. 尝试用反馈实现一个浮动工具条,当鼠标移动到某个图形上时,显示这 个工具条,过一段时间后消失。 大家可以想象一下使用反馈能实现什么样的有趣功能。 GEF 入门系列(序) 前些天换了新电脑,本人一直处于兴奋中,基本是"不务正业"的状态。快过年了,虽然没什么动 力干活,但我玩游戏技术比较差,魔兽 3 打电脑一家还很费劲,干脆写写帖子就当是休息吧! 由于工作的需要,最近开始研究 GEF(Graphical Editor Framework)这个框架,它可以用 来给用户提供图形化编辑模型的功能,从而提升用户体验,典型的应用如图形化的流程设计器、 UML 类图编辑器等等。其实一年多来我们做的项目都是和它有关的,只是之前我具体负责的事 情和它没什么关系。那时也看过黄老大写的代码,EMF 和 GEF 混在一起特别晕,没能坚持看下 去。这次自己要动手做了,正好趁此机会把它搞明白,感觉 GEF 做出来的东西给人很专业的感 觉,功能也很强大,应该挺有前途的。此外,GEF 里用到了很多经典模式,最突出的如大量应 用 Command 模式,方便的实现 Undo/Redo 功能等等,通过学习 GEF,等于演练了这些模式, 比只是看看书写几个类那种学习方式的效果好很多。 现在网上关于 GEF 的文章和教程还不是很多(比起一年前还是增加了几篇),基本上都是 eclipse.org 上的那些,其中少数几篇有中文版,中文的原创就属于凤毛麟角了,市场上似乎也 没有这方面的成书。GEF SDK 里自带的文档则比较抽象,不适合入门。我觉得最好的入门方法 是结合具体的例子,一边看代码,一边对照文档,然后自己再动手做一做。当然这个例子要简单 点才好,像 GEF 的那个 logic 的例子就太复杂了,即使是 flow(运行界面见下图)我觉得也有 点大;另外例子要比较规范的,否则学成错误的路子以后还要花时间改就不值得了。 用 GEF 编写的流程编辑器 GEF 的结构决定了 GEF 应用程序的复杂性,即使最最简单的 GEF 程序也包含五六个包和十几 个类,刚开始接触时有点晕是很正常的。我找到一个还不错的例子,当然它很简单了,如果你现 在就想自己试试 GEF,可以点这里下载一个 zip 包(若已无法下载请用这个链接),展开后是 六个项目(pt1,pt2,…,pt6),每一个是在前面一个的基础上增加一些功能得到的,pt1 是最简 单的一个,这样你就可以看到那些典型的功能(例如 DirectEdit、Palette 等等)在 GEF 里应 该怎样实现了。关于这个例子的更多信息请看作者 blog 上的说明: “Back in March, I talked a little about my initial attempts writing an Eclipse Grap hical Editor Framework (GEF) application. I wanted, then, to write a tutorial that e ssentially walked the reader through the various stages of the development of m y first application. I even suggested some kind of versioned literate programmin g approach to writing the tutorial and the code at the same time. I haven't had time since then to make any progress, but I did get the GEF applic ation to the stage where I had put together a snapshot at each of six milestone s. A few people have written to me over the last six months asking the status o f my tutorial and I've sent them my six snapshots as a starting point. It makes sense for me to just to offer them here. You can download a ZIP file with the six snapshots at http://jtauber.com/2004/ gef/gef.zip. Hopefully they are still useful, even without a surrounding tutorial.” 需要注意一点,这个例子应该是在 Eclipse 2.1 里写的,所以如果你想在 Eclipse 3 里运行这个 例子,要修改 plugin.xml 里的 dependencies 为: 再修改一下 DiagramCreationWizard 这个类 finish()方法里 page.openEditor(newFile); 这句改为 page.openEditor(new FileEditorInput(newFile),"com.jtauber.river.editor");, 还有一些 warning 不太影响,可以不用管。 或者如果你不是特别着急的话,留意我这个半新手写的 GEF 入门系列帖子,说不定能引起你更 多的共鸣,也是一个办法吧。 GEF 的学习周期是比较长的,学之前应该有这个心理准备。特别是如果你没有开发过 Eclipse 插件,那么最好先花时间熟悉一下 Eclipse 的插件体系结构,这方面的文章还是很多的,也不是 很难,基本上会开发简单的 Editor 就可以了,因为 GEF 应用程序一般都是在 Editor 里进行图 形编辑的。另外,绝大多数 GEF 应用程序都是基于 Draw2D 的,可以说 GEF 离不开 Draw2D, 而后者有些概念很难搞明白,加上其文档比 GEF 更少,所以我会从 Draw2D 开始说起,当然不 能讲得很深入,因为我自己也是略知皮毛而已。 说实话,我对写这个系列不太有信心,因为自己也是刚入门而已。但要是等到几个月后再写,很 多心得怕是讲不出来了。所以还是那句话,有什么写错的请指正,并且欢迎交流。 GEF 入门系列(1 Draw2D) 鸡年第一天,首先向大家拜个年,恭祝新春快乐,万事如意。一年之计在于春,你对新的一年有 什么安排呢?好的,下面还是进入正题吧。 关于 Java2D 相信大家都不会陌生,它是基于 AWT/Swing 的二维图形处理包, JDK 附带的 示例程序向我们展示了 Java2D 十分强大的图形处理能力。在 Draw2D 出现以前,SWT 应用 程序在这方面一直处于下风,而 Draw2D 这个 SWT 世界里的 Java2D 改变了这种形势。 可能很多人还不十分了解 GEF 和 Draw2D 的关系:一些应用程序是只使用 Draw2D,看起来 却和 GEF 应用程序具有相似的外观。原因是什么,下面先简单解释一下: GEF 是具有标准 MVC(Model-View-Control)结构的图形编辑框架,其中 Model 由我们自己 根据业务来设计,它要能够提供某种模型改变通知的机制,用来把 Model 的变化告诉 Control 层;Control 层由一些 EditPart 实现,EditPart 是整个 GEF 的核心部件,关于 EditPart 的机 制和功能将在以后的帖子里介绍;而 View 层(大多数情况下)就是我们这里要说的 Draw2D 了,其作用是把 Model 以图形化的方式表现给使用者。 虽然 GEF 可以使用任何图形包作为 View 层,但实际上 GEF 对 Draw2D 的依赖是很强的。举 例来说:虽然 EditPart(org.eclipse.gef.EditPart)接口并不要求引入任何 Draw2D 的类, 但我们最常使用的 AbstractGraphicalEditPart 类的 createFigure()方法就需要返回 IFigure 类型。由于这个原因,在 GEF 的 SDK 中索性包含了 Draw2D 包就不奇怪了,同样道理,只有 先了解 Draw2D 才可能掌握 GEF。 这样,对于一开始提出的问题可以总结如下:Draw2D 是基于 SWT 的图形处理包,它适合用 作 GEF 的 View 层。如果一个应用仅需要显示图形,只用 Draw2D 就够了;若该应用的模型要 求以图形化的方式被编辑,那么最好使用 GEF 框架。 现在让我们来看看 Draw2D 里都有些什么,请看下图。 图 1 Draw2D 的结构 Draw2D 通过被称为LightweightSystem(以下简称LWS)的部件与SWT中的某一个Canvas 实例相连,这个 Canvas 在 Draw2D 应用程序里一般是应用程序的 Shell,在 GEF 应用程序里 更多是某个 Editor 的 Control(createPartControl()方法中的参数),在界面上我们虽然看 不到 LWS 的存在,但其他所有能看到的图形都是放在它里面的,这些图形按父子包含关系形成 一个树状的层次结构。 LWS 是 Draw2D 的核心部件,它包含三个主要组成部分:RootFigure 是 LWS 中所有图形的 根,也就是说其他图形都是直接或间接放在 RootFigure 里的;EventDispatcher 把 Canvas 上的各种事件分派给 RootFigure,这些事件最终会被分派给适当的图形,请注意这个 RootFigure 和你应用程序中最顶层的 IFigure 不是同一个对象,前者是看不见的被 LWS 内部 使用的,而后者通常会是一个可见的画布,它是直接放在前者中的;UpdateManager 用来重 绘图形,当 Canvas 被要求重绘时,LWS 会调用它的 performUpdate()方法。 LWS 是连接 SWT 和 Draw2D 的桥梁,利用它,我们不仅可以轻松创建任意形状的图形(不仅 仅限于矩形),同时能够节省系统资源(因为是轻量级组件)。一个典型的纯 Draw2D 应用程 序代码具有类似下面的结构: //创建 SWT 的 Canvas(Shell 是 Canvas 的子类) Shell shell = new Shell(); shell.open(); shell.setText("A Draw2d application"); //创建 LightweightSystem,放在 shell 上 LightweightSystem lws = new LightweightSystem(shell); //创建应用程序中的最顶层图形 IFigure panel = new Figure(); panel.setLayoutManager(new FlowLayout()); //把这个图形放置于 LightweightSystem 的 RootFigure 里 lws.setContents(panel); //创建应用程序中的其他图形,并放置于应用程序的顶层图形中 panel.add( ); while (!shell.isDisposed ()) { if (!display.readAndDispatch ()) display.sleep (); } 接下来说说图形,Draw2D 中的图形全部都实现 IFigure(org.eclipse.draw2d.IFigure)接 口,这些图形不仅仅是你看到的屏幕上的一块形状而已,除了控制图形的尺寸位置以外,你还可 以监听图形上的事件(鼠标事件、图形结构改变等等,来自 LWS 的 EventDispatcher)、设 置鼠标指针形状、让图形变透明、聚焦等等,每个图形甚至还拥有自己的 Tooltip,十分的灵活。 Draw2D 提供了很多缺省图形,最常见的有三类:1、形状(Shape),如矩形、三角形、椭 圆形等等;2、控件(Widget),如标签、按钮、滚动条等等;3、层(Layer),它们用来为 放置于其中的图形提供缩放、滚动等功能,在 3.0 版本的 GEF 中,还新增了 GridLayer 和 GuideLayer 用来实现"吸附到网格"功能。在以 IFigure 为根节点的类树下有相当多的类,不过 我个人感觉组织得有些混乱,幸好大部分情况下我们只用到其中常用的那一部分。 图 2 一个 Draw2D 应用程序 每个图形都可以拥有一个边框(Border),Draw2D 所提供的边框类型有 GroupBoxBorder、 TitleBarBorder、ImageBorder、ButtonBorder,以及可以组合两种边框的 CompoundBorder 等等,在 Draw2D 里还专门有一个 Insets 类用来表示边框在图形中所占 的位置,它包含上下左右四个整型数值。 我们知道,一个图形可以包含很多个子图形,这些被包含的图形在显示的时候必须以某种方式被 排列起来,负责这个任务的就是父图形的 LayoutManager。同样的,Draw2D 已经为我们提 供了一系列可以直接使用的 LayoutManager,如 FlowLayout 适合用于表格式的排列, XYLayout 适合让用户在画布上用鼠标随意改变图形的位置,等等。如果没有适合我们应用的 LayoutManager,可以自己定制。每个 LayoutManager 都包含某种算法,该算法将考虑与每 个子图形关联的 Constraint 对象,计算得出子图形最终的位置和大小。 图形化应用程序的一个常见任务就是在两个图形之间做连接,想象一下 UML 类图中的各种连接 线,或者程序流程图中表示数据流的线条,它们有着不同的外观,有些连接线还要显示名称,而 且最好能不交叉。利用 Draw2D 中的 Router、Anchor 和 Locator,可以实现多种连接样式, 其中 Router 负责连接线的外观和操作方式,最简单的是设置 Router 为 null(无 Router), 这样会使用直线连接,其他连接方式包括折线、具有控制点的折线等等(见图 3),若想控制连 接线不互相交叉也需要在 Router 中作文章。Anchor 控制连接线端点在图形上的位置,即"锚 点"的位置,最易于使用的是 ChopBoxAnchor,它先假设图形中心为连接点,然后计算这条假 想连线与图形边缘的交汇点作为实际的锚点,其他 Anchor 还有 EllipseAnchor、LabelAnchor 和 XYAnchor 等等;最后,Locator 的作用是定位图形,例如希望在连接线中点处以一个标签 显示此连线的名称/作用,就可以使用 MidpointLocator 来帮助定位这个标签,其他 Locator 还有 ArrowLocator 用于定位可旋转的修饰(Decoration,例如 PolygonDecoration)、 BendpointerLocator 用于定位连接控制点、ConnectionEndpointLocator 用于定位连接端 点(通过指定 uDistance 和 vDistance 属性的值可以设置以端点为原点的坐标)。 图 3 三种 Router 的外观 此外,Draw2D 在 org.eclipse.draw2d.geometry 包里提供了几个很方便的类型,如 Dimension、Rectangle、Insets、Point 和 PointList 等等,这些类型既在 Draw2D 内部广 泛使用,也可以被开发人员用来简化计算。例如 Rectangle 表示的是一个矩形区域,它提供 getIntersection()方法能够方便的计算该区域与另一矩形区域的重叠区域、getTransposed() 方法可以得到长宽值交换后的矩形区域、scale()方法进行矩形的拉伸等等。在自己实现 LayoutManager 的时候,由于会涉及到比较复杂的几何计算,所以更推荐使用这些类。 以上介绍了 Draw2D 提供的大部分功能,利用这些我们已经能够画出十分漂亮的图形了。但对 大多数实际应用来说这样还远远不够,我们还要能编辑它,并把对图形的修改反映到模型里去。 为了漂亮的完成这个艰巨任务,GEF 绝对是不二之选。从下一次开始,我们将正式进入 GEF 的 世界。 参考资料:  GEF Developer Guide  Eclipse Development - Using the Graphical Editing Framework and the Eclipse Modeling Framework  Displaying a UML Diagram with Draw2D GEF 入门系列(三,应用实例) 构造一个 GEF 应用程序通常分为这么几个步骤:设计模型、设计 EditPart 和 Figure、设计 EditPolicy 和 Command,其中 EditPart 是最主要的一部分,因为在实现它的时候不可避免 的要使用到 EditPolicy,而后者又涉及到 Command。 现在我们来看个例子,它的功能非常简单,用户可以在画布上增加节点(Node)和节点间的连 接,可以直接编辑节点的名称以及改变节点的位置,用户可以撤消/重做任何操作,有一个树状 的大纲视图和一个属性页。点此下载(Update: For Eclipse 3.1 的版本),这是一个 Eclipse 的项目打包文件,在 Eclipse 里导入后运行 Run-time Workbench,新建一个扩展名为 "gefpractice"的文件就会打开这个编辑器。 图 1 Practice Editor 的使用界面 你可以参考着代码来看接下来的内容了,让我们从模型开始说起。模型是根据应用需求来设计的, 所以我们的模型包括代表整个图的 Diagram、代表节 点的 Node 和代表连接的 Connection 这些对象。我们知道,模型是要负责把自己的改变通知给 EditPart 的,为了把这个功能分离出 来,我们使 用名为Element 的抽象类专门来实现通知机制,然后让其他模型类继承它。Element 类里包括一个 PropertyChangeSupport 类型 的成员变量,并提供了 addPropertyChangeListener()、removePropertyChangeListener()和 fireXXX()方法分 别用来注册监听器和通知监听器模型改变事件。在 GEF 里,模型的监听器就是 EditPart,在 EditPart 的 active ()方法里我们会把它作为监听器注册到模型中。所以,总共有四个类组成了 我们的模型部分。 在前面的贴子里说过,大部分 GEF 应用程序都是实现为 Editor 的,这个例子也不例外,对应的 Editor 名为 PracticeEditor。这 个 Editor 继承了 GraphicalEditorWithPalette 类,表示它 是一个具有调色板的图形编辑器。最重要的两个方法是 configureGraphicalViewer()和 initializeGraphicalViewer(),分别用来定制和初始化 EditPartViewer(关于 EditPartViewer 的作用请查看前面的帖子),简单查看一下 GEF 的代码你会发现,在 GraphicalEditor 类里会 先后调用这两个方法,只是中间插了一个 hookGraphicalViewer()方法,其作用是同步选择和 把 EditPartViewer 作为SelectionProvider 注册到所在的site(Site 是Workbench 的概念, 请查 Eclipse 帮 助)。所以,与选择无关的初始化操作应该在前者中完成,否则放在后者完成。 例子中,在这两个方法里我们配置了 RootEditPart、用于创建 EditPart 的 EditPartFactory、 Contents 即 Diagram 对象和增加了拖放支持,拖动目标是当前 EditPartViewer,后面会看 到拖动源就是调色板。 这个 Editor 是带有调色板的,所以要告诉 GEF 我们的调色板里都有哪些工具,这是通过覆盖 getPaletteRoot()方法来实现的。在这 个方法里,我们利用自己写的一个工具类 PaletteFactory 构造一个 PaletteRoot 对象并返回,我们的调色板里需要有三种工具:选择工 具、节点工具和连接工具。在 GEF 里,调色板里可以有抽屉(PaletteDrawer)把各种工具归 类放置,每个工具都是一个 ToolEntry,选择 工具(SelectionToolEntry)和连接工具 (ConnectionCreationToolEntry)是预先定义好的几种工具中的两个, 所以可以直接使用。 对于节点工具,要使用 CombinedTemplateCreationEntry,并把节点类型作为参数之一传给 它,创建节点工具的 代码如下所示。 ToolEntry tool = new CombinedTemplateCreationEntry("Node", "Create a new No de", Node.class, new SimpleFactory(Node.class), null, null); 在新的 3.0 版本 GEF 里还提供了一种可以自动隐藏调色板的编辑器 GraphicalEditorWithFlyoutPalette,对调色板的外观有更多选项可以选择,以后的帖子里可 能会提到如何使用。 调色板的初始化操作应该放在 initializePaletteViewer()里完成,最主要的任务是为调色板所 在的 EditPartViewer 添加拖动源事件支持,前面我们已经为画布所在 EditPartViewer 添加了 拖动目标事件,所以现在就可以实现完整的拖 放操作了。这里稍微讲解一下拖放的实现原理, 以用来创建节点对象的节点工具为例,它在调色板里是一个 CombinedTemplateCreationEntry,在创建这个 PaletteEntry 时(见上面的代码)我们指 定该对象对应一个 Node.class,所以在用户从调色板里拖动这个工具时,内存里有一个 TemplateTransfer 单例对象会记录下 Node.class(称作 template),当用户在画布上松开 鼠标时,拖放结束的事件被触发,将由画布注册的 DiagramTemplateTransferDropTargetListener 对象来处理 template 对象(现在是 Node.class), 在例子中我们的处理方法是用一个名为 ElementFactory 的对象负责根据这 个 template 创建一个对应类型的实例。 以上我们建立了模型和用于实现视图的 Editor,因为模型的改变都是由 Command 对象直接修 改的,所以下面我们先来看都有哪些 Command。由需求可知,我们对模型的操作有增加/删 除节点、修改节点名称、改变节点位置和增加/删除连接等,所以对应就有 CreateNodeCommand、DeleteNodeCommand、RenameNodeCommand、 MoveNodeCommand、 CreateConnectionCommand 和 DeleteConnectionCommand 这些对象,它们都放归类在 commands 包里。一个 Command 对象里最重要的当然是 execute()方法了,也就是执行命令的方法。除此以外,因为要实现撤消/重做功能,所以在 Command 对象里 都有 Undo()和 Redo()方法,同时在 Command 对象里要有成员变量负责 保留执行该命令时的相关状态,例如 RenameNodeCommand 里要有 oldName 和 newName 两个变量,这样才能正确的执行 Undo()和 Redo()方法,要记住,每个被执行过的 Command 对象实例都是 被保存在 EditDomain 的 CommandStack 中的。 例子里的 EditPolicy 都放在 policies 包里,与图形有关的(GraphicalEditPart 的子类)有 DiagramLayoutEditPolicy、NodeDirectEditPolicy 和 NodeGraphicalNodeEditPolicy, 另外两个则是与图形无关的编辑策略。可以看到,在后一种类型的两个类 (ConnectionEditPolicy 和 NodeEditPolicy)中我们只覆盖了 createDeleteCommand() 方法,该方法用 于创建一个负责"删除"操作的 Command 对象并返回,要搞清这个方法看似矛 盾的名字里 create 和 delete 是对不同对象而言的。 有了 Command 和 EditPolicy,现在可以来看看 EditPart 部分了。每一个模型对象都对应一 个 EditPart,所以我们的三个模 型对象(Element 不算)分别对应 DiagramPart、 ConnectionPart 和 NodePart。对于含有子元素的 EditPart,必 须覆盖 getModelChildren() 方法返回子对象列表,例如 DiagramPart 里这个方法返回的是 Diagram 对象包含的 Node 对 象列 表。 每个 EditPart 都有 active()和 deactive()两个方法,一般我们在前者里注册监听器(因为实现 了 PropertyChangeListener 接口,所以 EditPart 本身就是监听器)到模型对象,在后者里 将监听器从列表里移除。在触发监听器事件 的 propertyChange()方法里,一般是根据"事件 名"称决定使用何种方式刷新视图,例如对于 NodePart,如果是节点本身的属性发生变 化,则 调用 refreshVisuals()方法,若是与它相关的连接发生变化,则调用 refreshTargetConnections()或 refreshSourceConnections()。这里用到的事件名称都是 我们自己来规定的,在例子中比如 Node.PROP_NAME 表示节点的 名称属性, Node.PROP_LOCATION 表示节点的位置属性,等等。 EditPart(确切的说是 AbstractGraphicalEditpart)另外一个需要实现的重要方法是 createFigure(), 这个方法应该返回模型在视图中的图形表示,是一个 IFigure 类型对象。一 般都把这些图形放在 figures 包里,例子里只有 NodeFigure 一个 自定义图形,Diagram 对 象对应的是 GEF 自带的名为 FreeformLayer 的图形,它是一个可以在东南西北四个方向任意 扩展的层图形;而 Connection 对应的也是 GEF 自带的图形,名为 PolylineConnection,这 个图形缺省是一条用来连接另外两个图形的直线,在例子里 我们通过 setTargetDecoration() 方法让连接的目标端显示一个箭头。 最后,要为 EditPart 增加适当的 EditPolicy,这是通过覆盖 EditPart 的 createEditPolicies() 方法来实现 的,每一个被"安装"到 EditPart 中的 EditPolicy 都对应一个用来表示角色(Role) 的字符串。对于在模型中有子元素的 EditPart,一般都会安装一个 EditPolicy.LAYOUT_ROLE 角色的 EditPolicy(见下面的代码),后者多为 LayoutEditPolicy 的子类;对于连接类型的 EditPart,一般要安装 EditPolicy.CONNECTION_ENDPOINTS_ROLE 角色的 EditPolicy, 后者则多为 ConnectionEndpointEditPolicy 或其子类,等等。 installEditPolicy(EditPolicy.LAYOUT_ROLE, new DiagramLayoutEditPolicy()); 用户的操作会被当前工具(缺省为选择工具 SelectionTool)转换为请求(Request),请求根 据类型被分发到目标 EditPart 所安装的 EditPolicy,后者根据请求对应的角色来判断是否应该 创建命令并执行。 在以前的帖子里说过,Role-EditPolicy-Command 这样的设计主要是为了尽量重用代码,例 如同一个 EditPolicy 可以被安 装在不同 EditPart 中,而同一个 Command 可以被不同的 EditPolicy 所使用,等等。当然,凡事有利必有弊,我认为这种的设计也有缺点, 首先在代码 上看来不够直观,你必须对众多 Role、EditPolicy 有所了解,增加了学习周期;另外大部分不 需要重用的代码也要按照这个相对复杂的方 式来写,带来了额外工作量。 以上就是一个 GEF 应用程序里最基本的几个组成部分,例子中还有如 Direct Edit、属性表和大 纲视图等一些功能没有讲解,下面的帖子里将介绍这些常用功能的实现。 GEF 入门系列(四,其他功能) 最近由于实验室任务繁重,一直没有继续研究 GEF,本来已经掌握的一些东西好象又丢掉了不 少,真是无奈啊,看来还是要经常碰碰。刚刚接触 GEF 的朋友大都会有这样的印象:GEF 里概 念太多,比较绕,一些能直接实现的功能非要拐几个弯到另一个类里做,而且很多类的名字十分 相似,加上不知道他们的作用,感觉就好象一团乱麻。我觉得这种情况是由图形用户界面(GUI) 的复杂性所决定的,GUI 看似简单,实际上包含了相当多的逻辑,特别是 GEF 处理的这种图形 编辑方式,可以说是最复杂的一种。GEF 里每一个类,应该说都有它存在的理由,我们要尽可 能了解作者的意图,这就需要多看文档和好的例子。 在 Eclipse 里查看文档和代码相当便利,比如我们对某个类的用法不清楚,一般首先找它的注释 (选中类或方法按 F2),其次可以查看它在其他地方用法(选中类或方法按 Ctrl+Shift+G), 还可以找它的源代码(Ctrl+鼠标左键或 F3)来看,另外 Ctrl+Shift+T 可以按名称查找一个类 等等。学 GEF 是少不了看代码的,当然还需要时间和耐心。 好,闲话少说,下面进入正题。这篇帖子将继续上一篇内容,主要讨论如何实现 DirectEdit、 属性页和大纲视图,这些都是一个完整 GEF 应用程序需要提供的基本功能。 实现 DirectEdit 所谓 DirectEdit(也称 In-Place-Edit),就是允许用户在原本显示内容的地方直接对内容进行 修改,例如在 Windows 资源管理器里选中一个文件,然后按 F2 键就可以开始修改文件名。实 现 DirectEdit 的原理很直接:当用户发出修改请求(REQ_DIRECT_EDIT)时,就在文字内 容所在位置覆盖一个文本框(也可以是下拉框,这里我们只讨论文本的情况)作为编辑器,编辑 结束后,再将编辑器中的内容应用到模型里即可。(作为类似的功能请参考:给表格的单元格增 加编辑功能) 图 1 Direct Edit 在 GEF 里,这个弹出的编辑器由 DirectEditManager 类负责管理,在我们的 NodePart 类里, 通过覆盖 performRequest()方法响应用户的 DirectEdit 请求,在这个方法里一般要构造一个 DirectEditManager 类的实例(例子中的 NodeDirectEditManager),并传入必要的参数, 包括接受请求的 EditPart(就是自己,this)、编辑器类型(使用 TextCellEditor)以及用来 定位编辑器的 CellEditorLocator(NodeCellEditorLocator),然后用 show()方法使编辑器 显示出来,而编辑器中显示的内容已经在构造方法里得到。简单看一下 NodeCellEditorLocator 类,它的关键方法在 relocate()里,当编辑器里的内容改变时,这个方法被调用从而让编辑器 始终处于正确的坐标位置。DirectEditManager 有一个重要的 initCellEditor()方法,它的主要 作用是设置编辑器的初始值。在我们的例子里,初始值设置为被编辑 NodePart 对应模型 (Node) 的 name 属性值;这里还另外完成了设置编辑器字体和选中全部文字(selectAll)的功能,因 为这样更符合一般使用习惯。 在 NodePart 里还要增加一个角色为 DIRECT_EDIT_ROLE 的 EditPolicy,它应该继承自 DirectEditPolicy,有两个方法需要实现:getDirectEditCommand()和 showCurrentEditValue(),虽然还未遇到过,但前者的作用你不应该感到陌生--在编辑结束时 生成一个 Command 对象将修改结果作用到模型;后者的目的是更新 Figure 中的显示,虽然 我们的编辑器覆盖了 Figure 中的文本,似乎并不需要管 Figure 的显示,但在编辑中时刻保持 这两个文本的一致才不会出现"盖不住"的情况,例如当编辑器里的文本较短时。 实现属性页 在 GEF 里实现属性页和普通应用程序基本一样,例如我们希望可以通过属性视图 (PropertyView)显示和编辑每个节点的属性,则可以让 Node 类实现 IPropertySource 接 口,并通过一个 IPropertyDescriptor[]类型的成员变量描述要在属性视图里显示的那些属性。 有朋友问,要在属性页里增加一个属性都该改哪些地方,主要是三个地方:首先要在你的 IPropertyDescriptor[]变量里增加对应的描述,包括属性名和属性编辑方式(比如文本或是下 拉框,如果是后者还要指定选项列表),其次是 getPropertyValue()和 setPropertyValue() 里增加读取属性值和将结果写入的代码,这两个方法里一般都是像下面的结构(以前者为例): public Object getPropertyValue(Object id) { if (PROP_NAME.equals(id)) return getName(); if (PROP_VISIBLE.equals(id)) return isVisible() ? new Integer(0) : new Integer(1); return null; } 也就是根据要处理的属性名做不同操作。要注意的是,下拉框类型的编辑器是以 Integer 类型 数据代表选中项序号的,而不是 int 或 String,例如上面的代码根据 visible 属性返回第零项或 第一项,否则会出现 ClassCastException。 图 2 属性页 实现大纲视图 在 Eclipse 里,当编辑器(Editor)被激活时,大纲视图自动通过这个编辑器的 getAdapter() 方法寻找它提供的大纲(大纲实现 IcontentOutlinePage 接口)。GEF 提供了 ContentOutlinePage 类用来实现大纲视图,我们要做的就是实现一个它的子类,并重点实现 createControl()方法。ContentOutlinePage 是 org.eclipse.ui.part.Page 的一个子类,大 纲视图则是 PageBookView 的子类,在大纲视图中有一个 PageBook,包含了很多 Page 并可 以在它们之间切换,切换的依据就是当前活动的 Editor。因此,我们在 createControl()方法 里要做的就是构造这个 Page,简化后的代码如下所示: private Control outline; public OutlinePage() { super(new TreeViewer()); } public void createControl(Composite parent) { outline = getViewer().createControl(parent); getSelectionSynchronizer().addViewer(getViewer()); getViewer().setEditDomain(getEditDomain()); getViewer().setEditPartFactory(new TreePartFactory()); getViewer().setContents(getDiagram()); } 由于我们在构造方法里指定了使用树结构显示大纲,所以 createControl()里的第一句就会使 outline 变量得到一个 Tree(见 org.eclipse.gef.ui.parts.TreeViewer 的代码),第二句把 TreeViewer 加到选择同步器中,从而让用户不论在大纲或编辑区域里选择 EditPart 时,另一 方都能自动做出同样的选择;最后三行的作用在以前的帖子里都有介绍,总体目的是把大纲视图 的模型与编辑区域的模型联系在一起,这样,对于同一个模型我们就有了两个视图,体会到 MVC 的好处了吧。 实现大纲视图最重要的工作基本就是这些,但还没有完,我们要在 init()方法里绑定 UNDO/REDO/DELETE 等命令到 Eclipse 主窗口,否则当大纲视图处于活动状态时,主工具条 上的这些命令就会变为不可用状态;在 getControl()方法里要返回我们的 outline 成员变量, 也就是指定让这个控件出现在大纲视图中;在 dispose()方法里应该把这个 TreeViewer 从选 择同步器中移除;最后,必须在 PracticeEditor 里覆盖 getAdapter()方法,前面说过,这个 方法是在 Editor 激活时被大纲视图调用的,所以在这里必须把我们实现好的 OutlinePage 返回 给大纲视图使用,代码如下: public Object getAdapter(Class type) { if (type == IContentOutlinePage.class) return new OutlinePage(); return super.getAdapter(type); } 这样,树型大纲视图就完成了,见下图。很多 GEF 应用程序同时具有树型和缩略图两种大纲, 实现的基本思路是一样的,但代码会稍微复杂一些,因为这两种大纲一般要通过一个 PageBook 进行切换,缩略图一般由 org.eclipse.draw2d.parts.ScrollableThumbnail 负责实现,这里 暂时不讲了(也许以后会详细说),你也可以通过看 logic 例子的 LogicEditor 这个类的代码来 了解。 图 3 大纲视图 P.S.写这篇帖子的时候,我对例子又做了一些修改,都是和这篇帖子所说的内容相关的,所以如 果你以前下载过,会发现那时的代码与现在稍有不同(功能还是完全一样的,下载)。另外要说 一下,这个例子并不完善,比如删除一个节点的时候,它的连接就没同时删除,一些键盘快捷键 不起作用,还存在很多被注释掉的代码等等。如果有兴趣你可以来修改它们,也是不错的学习途 径。 GEF 入门系列(五,浅谈布局) 虽然很多 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.getBoun ds().height+clientArea.y,clientArea.width,clientArea.height-nameFigure.getBound s().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)) / numChil dren; 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 这种只关心子图形位置的布局管理器中, 就必须为每个子图形指定尺寸,否则图形将因为尺寸过小而不可见,这也是一个开发人员十分容 易疏忽的地方。 GEF 入门系列(六,添加菜单和工具条) 我发现一旦稍稍体会到 GEF 的妙处,就会很自然的被它吸引住。不仅是因为用它做出的图形界 面好看,更重要的是,UI 中最复杂和细微的问题,在 GEF 的设计中无不被周到的考虑并以适当 的模式解决,当你了解了这些,完全可以把这些解决方法加以转换,用来解决其他领域的设计问 题。去年黄老大在一个 GEF 项目结束后,仍然没有放弃对它的继续研究,现在甚至利用业余时 间开发了基于 GEF 的 SWT/JFace 增强软件包,Eclipse 和 GEF 的魅力可见一斑。我相信在未 来的两年里,由于 RCP/GEF 等技术的成熟,Java Standalone 应用程序必将有所发展,在 B/S 模式难以实现的那部分领域里扮演重要的角色。 本篇的主题是实现菜单功能,由于 Eclipse 的可扩展设计,在 GEF 应用程序中添加菜单要多几 处考虑,所以我首先介绍 Eclipse 里关于菜单的一些概念,然后再通过实例描述如何在 GEF 里 添加菜单、工具条和上下文菜单。 我们知道,Eclipse 本身只是一个平台(Platform),用户并不能直接用它来工作,它的作用是 为那些提供实际功能的部件提供一个基础环境,所有部件都通过平台指定的方式构造界面和使用 资源。在 Eclipse 里,这些部件被称为插件(Plugins),例如 Java 开发环境(JDT)、Ant 支持、CVS 客户端和帮助系统等等都是插件,由于我们从 eclipse.org 下载的 Eclipse 本身已 经包含了这些常用插件,所以不需要额外的安装,就好象 Windows 本身已经包含了记事本、画 图等等工具一样。如果我们需要新功能,就要通过下载安装或在线更新的方式把它们安装到 Eclipse 平台上,常见的如 XML 编辑器、Properties 文件编辑器,J2EE 开发支持等等,包括 GEF 开发包也是这类插件。插件一般都安装在 Eclipse 安装目录的 plugins 子目录下,也可以 使用 link 方式安装在其他位置。 Eclipse 平台的一个优秀之处在于,如此众多的插件能够完美的集成在同一个环境中,要知道, 每个插件都可能具有编辑器、视图、菜单、工具条、文件关联等等复杂元素,要让它们能够和平 共处可不是件容易事。为此,Eclipse 提供了一系列机制来解决由此带来的各种问题。由于篇幅 限制,这里只能简单讲一下菜单和工具条的部分,更多内容请参考 Eclipse 随机提供的插件开发 帮助文档。 大多数情况下,我们说开发一个基于Eclipse 的应用程序就是指开发一个Eclipse 插件(plugin), Eclipse 里的每个插件都有一个名为 plugin.xml 的文件用来定义插件里的各种元素,例如这个 插件都有哪些编辑器,哪些视图等等。在视图中使用菜单和工具条请参考以前的贴子,本篇只介 绍编辑器的情况,因为 GEF 应用程序大多数是基于编辑器的。 图 1 Eclipse 平台的几个组成部分 首先要介绍 Retarget Action 的概念,这是一种具有一定语义但没有实际功能的 Action,它唯 一的作用就是在主菜单条或主工具条上占据一个项位置,编辑器可以将具有实际功能的 Action 映射到某个 Retarget Action,当这个编辑器被激活时,主菜单/工具条上的那个 Retarget Action 就会具有那个 Action 的功能。举例来说,Eclipse 提供了 IWorkbenchActionConstants.COPY 这个 Retarget Action,它的文字和图标都是预先定义 好的,假设我们的编辑器需要一个"复制节点到剪贴板"功能,因为"复制节点"和"复制"这两个词 的语义十分相近,所以可以新建一个具有实际功能的 CopyNodeAction(extends Action), 然后在适当的位置调用下面代码实现二者的映射: IActionBars.setGlobalActionHandler(IWorkbenchActionConstants.COPY, copyNodeAction) 当这个编辑器被激活时,Eclipse 会检查到这个映射,让 COPY 项变为可用状态,并且当用户按 下它时去执行CopyNodeAction 里定义的操作,即 run()方法里的代码。Eclipse 引入 Retarget Action 的目的是为了尽量减少主菜单/工具条的重建消耗,并且有利于用户使用上的一致性。在 GEF 应用程序里,因为很可能存在多个视图(例如编辑视图和大纲视图,即使暂时只有一个视 图,也要考虑到以后扩展为多个的可能),而每个视图都应该能够完成相类似的操作,例如在树 结构的大纲视图里也应该像编辑视图一样可以删除选中节点,所以一般的操作都应以映射到 Retarget Action 的方式建立。 主菜单/主工具条 与视图窗口不同,编辑器没有自己的菜单栏和工具条,它的菜单只能加在主菜单里。由于一个编 辑器可以有多个实例,而它们应当具有相同的菜单和工具条,所以在 plugin.xml 里定义一个编 辑器的时候,元素有一个 contributorClass 属性,它的值是一个实现 IEditorActionBarContributor 接口的类的全名,该类可以称为"菜单工具条添加器"。在添加 器里可以向 Eclipse 的主菜单/主工具条里添加自己需要的项。还是以我们这个项目为例,它要 求对每个操作可以撤消/重做,对画布上的每个元素可以删除,对每个节点元素可以设置它的优 先级为高、中、低三个等级。所以我们要添加这六个 Retarget Action,以下就是 DiagramActionBarContributor 类的部分代码: public class DiagramActionBarContributor extends ActionBarContributor { protected void buildActions() { addRetargetAction(new UndoRetargetAction()); addRetargetAction(new RedoRetargetAction()); addRetargetAction(new DeleteRetargetAction()); addRetargetAction(new PriorityRetargetAction(IConstants.PRIORITY_HIG H)); addRetargetAction(new PriorityRetargetAction(IConstants.PRIORITY_MEDI UM)); addRetargetAction(new PriorityRetargetAction(IConstants.PRIORITY_LO W)); } protected void declareGlobalActionKeys() { } public void contributeToToolBar(IToolBarManager toolBarManager) { …… } public void contributeToMenu(IMenuManager menuManager) { IMenuManager mgr=new MenuManager("&Node","Node"); menuManager.insertAfter(IWorkbenchActionConstants.M_EDIT,mgr); mgr.add(getAction(IConstants.ACTION_MARK_PRIORITY_HIGH)); mgr.add(getAction(IConstants.ACTION_MARK_PRIORITY_MEDIUM)); mgr.add(getAction(IConstants.ACTION_MARK_PRIORITY_LOW)); } } 可以看到,DiagramActionBarContributor 类继承自 GEF 提供的类 ActionBarContributor, 后者是实现了 IEditorActionBarContributor 接口的一个抽象类。buildActions()方法用于创 建那些要添加到主菜单/工具条的 Retarget Actions,并把它们注册到一个专门的注册表里;而 contributeToMenu()方法里的代码把这些 Retarget Actions 实际添加到主菜单栏,使用 IMenuManager.insertAfter()是为了让新加的菜单出现在指定的系统菜单后面, contributeToToolBar()里则是添加到主工具条的代码。 图 2 添加到主菜单条和主工具条上的 Action GEF 在 ActionBarContributor 里维护了 retargetActions 和 globalActionKeys 两个列表, 其中后者是一个 Retarget Actions 的 ID 列表,addRetargetAction()方法会把一个 Retarget Action 同时加到二者中,对于已有的 Retarget Actions,我们应该在 declareGlobalActionKeys()方法里调用 addGlobalActionKey()方法来声明,在一个编辑器 被激活的时候,与 globalActionKeys 里的那些 ID 具有相同 ID 值的(具有实际功能的)Action 将被联系到该 ID 对应的 Retarget Action,因此就不需要显式的去调用 setGlobalActionHandler()方法了,只要保证二者的 ID 相同即可实现映射。 GEF 已经内置了撤消/重做和删除这三个操作的 Retarget Action(因为太常用了),它们的 ID 分别是 IWorkbenchActionConstants.UNDO、REDO 和 DELETE,所以没有什么问题。而 设置优先级这个 Action 没有语义相近的现成 Retarget Action 可用,所以我们自己要先定义一 个 PriorityRetargetAction,内容如下(没有经过国际化处理): public class PriorityRetargetAction extends LabelRetargetAction{ public PriorityRetargetAction(int priority) { super(null,null); switch (priority) { case IConstants.PRIORITY_HIGH: setId(IConstants.ACTION_MARK_PRIORITY_HIGH); setText("High Priority"); break; case IConstants.PRIORITY_MEDIUM: setId(IConstants.ACTION_MARK_PRIORITY_MEDIUM); setText("Medium Priority"); break; case IConstants.PRIORITY_LOW: setId(IConstants.ACTION_MARK_PRIORITY_LOW); setText("Low Priority"); break; default: break; } } } 接下来要在编辑器(CbmEditor)的 createActions()里建立具有实际功能的 Actions,它们 应该是 SelectionAction(GEF 提供)的子类,因为我们需要得到当前选中的节点。稍后将给 出 PriorityAction 的代码,编辑器的 createActions()方法的代码如下所示: protected void createActions() { super.createActions(); //高优先级 IAction action=new PriorityAction(this, IConstants.PRIORITY_HIGH); action.setId(IConstants.ACTION_MARK_PRIORITY_HIGH); getActionRegistry().registerAction(action); getSelectionActions().add(action.getId()); //中等优先级 action=new PriorityAction(this, IConstants.PRIORITY_MEDIUM); action.setId(IConstants.ACTION_MARK_PRIORITY_MEDIUM); getActionRegistry().registerAction(action); getSelectionActions().add(action.getId()); //低优先级 action=new PriorityAction(this, IConstants.PRIORITY_LOW); action.setId(IConstants.ACTION_MARK_PRIORITY_LOW); getActionRegistry().registerAction(action); getSelectionActions().add(action.getId()); } 请再次注意在这个方法里每个 Action 的 id 都与前面创建的 Retarget Action 的 ID 对应,否则 将无法对应到主菜单条和主工具条中的 Retarget Actions。你可能已经发现了,这里我们只创 建了设置优先级的三个 Action,而没有建立负责撤消/重做和删除的 Action。其实 GEF 在这个 类的父类(GraphicalEditor)里已经创建了这些常用 Action,包括撤消/重做、全选、保存、 打印等,所以只要别忘记调用 super.createActions()就可以了。 GEF 提供的 UNDO/REDO/DELETE 等 Action 会根据当前选择的 editpart(s)自动判断自己是 否可用,我们定义的 Action 则要自己在 Action 的 calculateEnabled()方法里计算。另外,为 了实现撤消/重做的功能,一般 Action 执行的时候要建立一个 Command,将后者加入 CommandStack 里,然后执行这个 Command 对象,而不是直接把执行代码写在 Action 的 run()方法里。下面是我们的设置优先级 PriorityAction 的部分代码,该类继承自 SelectionAction: public void run() { execute(createCommand()); } private Command createCommand() { List objects = getSelectedObjects(); if (objects.isEmpty()) return null; for (Iterator iter = objects.iterator(); iter.hasNext();) { Object obj = iter.next(); if ((!(obj instanceof NodePart)) && (!(obj instanceof NodeTreeEditPart))) return null; } CompoundCommand compoundCmd = new CompoundCommand(GEFMessag es.DeleteAction_ActionDeleteCommandName); for (int i = 0; i < objects.size(); i++) { EditPart object = (EditPart) objects.get(i); ChangePriorityCommand cmd = new ChangePriorityCommand(); cmd.setNode((Node) object.getModel()); cmd.setNewPriority(priority); compoundCmd.add(cmd); } return compoundCmd; } protected boolean calculateEnabled() { Command cmd = createCommand(); if (cmd == null) return false; return cmd.canExecute(); } 因为允许用户一次对多个选中的节点设置优先级,所以在这个 Action 里我们创建了多个 Command 对象,并把它们加到一个 CompoundCommand 对象里,好处是在撤消/重做的时 候也可以一次性完成,而不是一个节点一个节点的来。 上下文菜单 在 GEF 里实现右键弹出的上下文菜单是很方便的,只要写一个继承 org.eclipse.gef. ContextMenuProvider 的自定义类,在它的 buildContextMenu()方法里编写添加菜单项的 代码,然后在编辑器里调用 GraphicalViewer. SetContextMenu()即可。GEF 为我们预先定 义了一些菜单组(Group)用来区分不同用途的菜单项,每个组在外观上表现为一条分隔线, 例如有 UNDO 组、COPY 组和 PRINT 组等等。如果你的菜单项不适合放在任何一个组中,可 以放在 OTHERS 组里,当然如果你的菜单项很多,也可以定义新的组用来分类。 图 3 上下文菜单 假设我们要实现如上图所示的上下文菜单,并且已经创建并在 ActionRegistry 里了这些 Action (在 Editor 的 createActions()方法里完成),ContextMenuProvider 应该像下面这样写: public class CbmEditorContextMenuProvider extends ContextMenuProvider { private ActionRegistry actionRegistry; public CbmEditorContextMenuProvider(EditPartViewer viewer, ActionRegistr y registry) { super(viewer); actionRegistry = registry; } public void buildContextMenu(IMenuManager menu) { // Add standard action groups to the menu GEFActionConstants.addStandardActionGroups(menu); // Add actions to the menu menu.appendToGroup(GEFActionConstants.GROUP_UNDO,getAction(Actio nFactory.UNDO.getId())); menu.appendToGroup(GEFActionConstants.GROUP_UNDO, getAction(Actio nFactory.REDO.getId())); menu.appendToGroup(GEFActionConstants.GROUP_EDIT, getAction(Action Factory.DELETE.getId())); menu.appendToGroup(GEFActionConstants.GROUP_REST,getAction(IConst ants.ACTION_MARK_PRIORITY_HIGH)); menu.appendToGroup(GEFActionConstants.GROUP_REST,getAction(IConst ants.ACTION_MARK_PRIORITY_MEDIUM)); menu.appendToGroup(GEFActionConstants.GROUP_REST,getAction(IConst ants.ACTION_MARK_PRIORITY_LOW)); } private IAction getAction(String actionId) { return actionRegistry.getAction(actionId); } } 注意 buildContextMenu()方法里的第一句是创建缺省的那些组,如果没有忽略了这一步后面 的语句会提示组不存在的错误,你也可以通过这个方法看到 GEF 是怎样建组的以及都有哪些组。 让编辑器使用这个类的代码一般写在 configureGraphicalViewer()方法里。 因为顺便介绍了 Eclipse 的一些基本概念,加上代码比较多,所以这篇贴子看起来比较长,其实 通过查看 GEF 对内置的 UNDO/REDO 等的实现很容易就会明白菜单的使用方法。 GEF 入门系列(七、XYLayout 和展开/ 折叠功能) 前面的帖子里曾说过如何使用布局,当时主要集中在 ToolbarLayout 和 FlowLayout(统称 OrderedLayout),还有很多应用程序使用的是可以自由拖动子图形的布局,在 GEF 里称为 XYLayout,而且这样的应用多半会需要在图形之间建立一些连接线,比如下图所示的情景。连 接的出现在一定程度上增加了模型的复杂度,连接线的刷新也是 GEF 关注的一个问题,这里就 主要讨论这类应用的实现,并将特别讨论一下展开/折叠(expand/collapse)功能的实现。请 点这里下载本篇示例代码。 图 1 使用 XYLayout 的应用程序 还是从模型开始说起,使用 XYLayout 时,每个子图形对应的模型要维护自身的坐标和尺寸信 息,这就在模型里引入了一些与实际业务无关的成员变量。为了解决这个问题,一般我们是让所 有需要具有这些界面信息的模型元素继承自一个抽象类(如 Node),而这个类里提供如 point、 dimension 等变量和 getter/setter 方法: public class Node extends Element implements IPropertySource { protected Point location = new Point(0, 0);//位置 protected Dimension size = new Dimension(100, 150);//尺寸 protected String name = "Node";//标签 protected List outputs = new ArrayList(5);//节点作为起点的连接 protected List inputs = new ArrayList(5);//节点作为终点的连接 … } EditPart 方面也是一样的,如果你的应用程序里有多个需要自由拖动和改变大小的 EditPart, 那么最好提供一个抽象的 EditPart(如 NodePart),在这个类里实现 propertyChange()、 createEditPolicy()、active()、deactive()和 refreshVisuals()等常用方法的缺省实现,如 果子类需要扩展某个方法,只要先调用 super()再写自己的扩展代码即可,典型的 NodePart 代码如下所示,注意它是 NodeEditPart 的子类,后者是 GEF 专为具有连接功能的节点提供的 EditPart: public abstract class NodePart extends AbstractGraphicalEditPart implements Pr opertyChangeListener, NodeEditPart { public void propertyChange(PropertyChangeEvent evt) { if (evt.getPropertyName().equals(Node.PROP_LOCATION)) refreshVisuals(); else if (evt.getPropertyName().equals(Node.PROP_SIZE)) refreshVisuals(); else if (evt.getPropertyName().equals(Node.PROP_INPUTS)) refreshTargetConnections(); else if (evt.getPropertyName().equals(Node.PROP_OUTPUTS)) refreshSourceConnections(); } protected void createEditPolicies() { installEditPolicy(EditPolicy.COMPONENT_ROLE, new NodeEditPolicy()); installEditPolicy(EditPolicy.GRAPHICAL_NODE_ROLE, new NodeGraphicalN odeEditPolicy()); } public void activate() {…} public void deactivate() {…} protected void refreshVisuals() { Node node = (Node) getModel(); Point loc = node.getLocation(); Dimension size = new Dimension(node.getSize()); Rectangle rectangle = new Rectangle(loc, size); ((GraphicalEditPart) getParent()).setLayoutConstraint(this, getFigure(), re ctangle); } //以下是 NodeEditPart 中抽象方法的实现 public ConnectionAnchor getSourceConnectionAnchor(ConnectionEditPart con nection) { return new ChopBoxAnchor (getFigure()); } public ConnectionAnchor getSourceConnectionAnchor(Request request) { return new ChopBoxAnchor (getFigure()); } public ConnectionAnchor getTargetConnectionAnchor(ConnectionEditPart con nection) { return new ChopBoxAnchor (getFigure()); } public ConnectionAnchor getTargetConnectionAnchor(Request request) { return new ChopBoxAnchor(getFigure()); } protected List getModelSourceConnections() { return ((Node) this.getModel()).getOutgoingConnections(); } protected List getModelTargetConnections() { return ((Node) this.getModel()).getIncomingConnections(); } } 从代码里可以看到,NodePart 已经通过安装两个 EditPolicy 实现关于图形删除、移动和改变 尺寸的功能,所以具体的 NodePart 只要继承这个类就自动拥有了这些功能,当然模型得是 Node 的子类才可以。在 GEF 应用程序里我们应该善于利用继承的方式来简化开发工作。代码 后半部分中的几个 getXXXAnchor()方法是用来规定连接线锚点(Anchor)的,这里我们使用 了在 Draw2D 那篇帖子里介绍过的 ChopBoxAnchor 作为锚点,它是 Draw2D 自带的。而代 码最后两个方法的返回值则规定了以这个 EditPart 为起点和终点的连接列表,列表中每一个元 素都应该是 Connection 类型,这个类是模型的一部分,接下来就要说到。 在 GEF 里,节点间的连接线也需要有自己的模型和对应的 EditPart,所以这里我们需要定义 Connection 和 ConnectionPart 这两个类,前者和其他模型元素没有什么区别,它维护 source 和 target 两个节点变量,代表连接的起点和终点;ConnectionPart 继承于 GEF 的 AbstractConnectionPart 类,请看下面的代码: public class ConnectionPart extends AbstractConnectionEditPart { protected IFigure createFigure() { PolylineConnection conn = new PolylineConnection(); conn.setTargetDecoration(new PolygonDecoration()); conn.setConnectionRouter(new BendpointConnectionRouter()); return conn; } protected void createEditPolicies() { installEditPolicy(EditPolicy.COMPONENT_ROLE, new ConnectionEditPolicy ()); installEditPolicy(EditPolicy.CONNECTION_ENDPOINTS_ROLE, new Connecti onEndpointEditPolicy()); } protected void refreshVisuals() { } public void setSelected(int value) { super.setSelected(value); if (value != EditPart.SELECTED_NONE) ((PolylineConnection) getFigure()).setLineWidth(2); else ((PolylineConnection) getFigure()).setLineWidth(1); } } 在 getFigure()里可以指定你想要的连接线类型,箭头的样式,以及连接线的路由(走线)方式, 例如走直线或是直角折线等等。我们为 ConnectionPart 安装了一个角色为 EditPolicy.CONNECTION_ENDPOINTS_ROLE 的 ConnectionEndpointEditPolicy,安装 它的目的是提供连接线的选择、端点改变等功能,注意这个类是 GEF 内置的。另外,我们并没 有把 ConnectionPart 作为监听器,在 refreshVisuals()里也没有做任何事情,因为连接线的 刷新是在与它连接的节点的刷新里通过调用 refreshSourceConnections()和 refreshTargetConnections()方法完成的。最后,通过覆盖 setSelected()方法,我们可以定 义连接线被选中后的外观,上面代码可以让被选中的连接线变粗。 看完了模型和 Editpart,现在来说说 EditPolicy。我们知道,GEF 提供的每种 GraphicalEditPolicy 都是与布局有关的,你在容器图形(比如画布)里使用了哪种布局,一般 就应该选择对应的 EditPolicy,因为这些 EditPolicy 需要对布局有所了解,这样才能提供拖动 feedback 等功能。使用 XYLayout 作为布局时,子元素被称为节点(Node),对应的 EditPolicy 是 GraphicalNodeEditPolicy,在前面 NodePart 的代码中我们给它安装的角色为 EditPolicy.GRAPHICAL_NODE_ROLE 的 NodeGraphicalNodeEditPolicy 就是这个类的一 个子类。和所有 EditPolicy 一样,NodeGraphicalNodeEditPolicy 里也有一系列 getXXXCommand()方法,提供了用于实现各种编辑目的的命令: public class NodeGraphicalNodeEditPolicy extends GraphicalNodeEditPolicy { protected Command getConnectionCompleteCommand(CreateConnectionReq uest request) { ConnectionCreateCommand command = (ConnectionCreateCommand) req uest.getStartCommand(); command.setTarget((Node) getHost().getModel()); return command; } protected Command getConnectionCreateCommand(CreateConnectionReques t request) { ConnectionCreateCommand command = new ConnectionCreateCommand (); command.setSource((Node) getHost().getModel()); request.setStartCommand(command); return command; } protected Command getReconnectSourceCommand(ReconnectRequest reque st) { return null; } protected Command getReconnectTargetCommand(ReconnectRequest reques t) { return null; } } 因为是针对节点的,所以这里面都是和连接线有关的方法,因为只有节点才需要连接线。这些方 法名称的意义都很明显:getConnectionCreateCommand()是当用户选择了连接线工具并点 中一个节点时调用,getConnectionCompleteCommand()是在用户选择了连接终点时调用, getReconnectSourceCommand()和getReconnectTargetCommand()则分别是在用户拖 动一个连接线的起点/终点到其他节点上时调用,这里我们返回 null 表示不提供改变连接端点的 功能。关于命令(Command)本身,我想没有必要做详细说明了,基本上只要搞清了模型之 间的关系,命令就很容易写出来,请下载例子后自己查看。 下面应郭奕朋友的要求说一说如何实现容器(Container)的折叠/展开功能。在有些应用里, 画布中的图形还能够包含子图形,这种图形称为容器(画布本身当然也是容器),为了让画布看 起来更简洁,可以让容器具有"折叠"和"展开"两种状态,当折叠时只显示部分信息,不显示子图 形,展开时则显示完整的容器和子图形,见图 2 和图 3,本例中各模型元素的包含关系是 Diagram->Subject->Attribute。 图 2 容器 Subject3 处于展开状态 要为 Subject 增加展开/折叠功能主要存在两个问题需要考虑:一是如何隐藏容器里的子图形, 并改变容器的外观,我采取的方法是在需要折叠/展开的时候改变容器图形,将 contentPane 也就是包含子图形的那个图形隐藏起来,从而达到隐藏子图形的目的;二是与容器包含的子图形 相连的连接线的处理,因为子图形有可能与其他容器或容器中的子图形之间存在连接线,例如图 2 中 Attribute4 与 Attribute6 之间的连接线,这些连接线在折叠状态下应该连接到子图形所在 容器上才符合逻辑(例如在 Subject3 折叠后,原来从 Attribute4 到 Attribute6 的连接应该 变成从 Subject3 到 Atribute6 的连接,见图 3)。 图 3 容器 Subject3 处于折叠状态 现在一个一个来解决。首先,不论容器处于什么状态,都应该只是视图上的变化,而不是模型中 的变化(例如折叠后的容器中没有显示子图形不代表模型中的容器不包含子图形),但在容器模 型中要有一个表示状态的布尔型变量 collapsed(初始值为 false),用来指示 EditPart 刷新视 图。假设我们希望用户双击一个容器可以改变它的展开/折叠状态,那么在容器的 EditPart(例 子里的 SubjectPart)里要覆盖 performRequest()方法改变容器的状态值: public void performRequest(Request req) { if (req.getType() == RequestConstants.REQ_OPEN) getSubject().setCollapsed(!getSubject().isCollapsed()); } 注意这个状态值的改变是会触发所有监听器的 propertyChange()方法的,而 SubjectPart 正 是这样一个监听器,所以在它的 propertyChange()方法里要增加对这个新属性变化事件的处 理代码,判断当前状态隐藏或显示 contantPane: public void propertyChange(PropertyChangeEvent evt) { if (Subject.PROP_COLLAPSED.equals(evt.getPropertyName())) { SubjectFigure figure = ((SubjectFigure) getFigure()); if (!getSubject().isCollapsed()) { figure.add(getContentPane()); } else { figure.remove(getContentPane()); } refreshVisuals(); refreshSourceConnections(); refreshTargetConnections(); } if (Subject.PROP_STRUCTURE.equals(evt.getPropertyName())) refreshChildren(); super.propertyChange(evt); } 为了让容器显示不同的图标以反应折叠状态,在 SubjectPart 的 refreshVisuals()方法里要做 额外的工作,如下所示: protected void refreshVisuals() { super.refreshVisuals(); SubjectFigure figure = (SubjectFigure) getFigure(); figure.setName(((Node) this.getModel()).getName()); if (!getSubject().isCollapsed()) { figure.setIcon(SubjectPlugin.getImage(IConstants.IMG_FILE)); } else { figure.setIcon(SubjectPlugin.getImage(IConstants.IMG_FOLDER)); } } 因为折叠后的容器图形应该变小,所以我让 Subject 对象覆盖了 Node 对象的 getSize()方法, 在折叠状态时返回一个固定的 Dimension 对象,该值就决定了 Subject 折叠状态的图形尺寸, 如下所示: protected Dimension collapsedDimension = new Dimension(80, 50); public Dimension getSize() { if (!isCollapsed()) return super.getSize(); else return collapsedDimension; } 上面的几段代码更改解决了第一个问题,第二个问题要稍微麻烦一些。为了在不同状态下返回正 确的连接,我们要修改 getModelSourceConnections()方法和 getModelTargetConnections()方法,前面已经说过,这两个方法的作用是返回与节点相关的 连接对象列表,我们要做的就是让它们根据节点的当前状态返回正确的连接,所以作为容器的 SubjectPart 要做这样的修改: protected List getModelSourceConnections() { if (!getSubject().isCollapsed()) { return getSubject().getOutgoingConnections(); } else { List l = new ArrayList(); l.addAll(getSubject().getOutgoingConnections()); for (Iterator iter = getSubject().getAttributes().iterator(); iter.hasNext ();) { Attribute attribute = (Attribute) iter.next(); l.addAll(attribute.getOutgoingConnections()); } return l; } } 也就是说,当处于展开状态时,正常返回自己作为起点的那些连接;否则除了这些连接以外,还 要包括子图形对应的那些连接。作为子图形的 AttributePart 也要修改,因为当所在容器折叠后, 它们对应的连接也要隐藏,修改后的代码如下所示: protected List getModelSourceConnections() { Attribute attribute = (Attribute) getModel(); Subject subject = (Subject) ((SubjectPart) getParent()).getModel(); if (!subject.isCollapsed()) { return attribute.getOutgoingConnections(); } else { return Collections.EMPTY_LIST; } } 由于 getModelTargetConnections()的代码和 getModelSourceConnections()非常类似, 这里就不列出其内容了。在一般情况下,我们只让一个 EditPart 监听一个模型的变化,但是请 记住,GEF 框架并没有规定 EditPart 与被监听的模型一一对应(实际上 GEF 中的很多设计就 是为了减少对开发人员的限制),因此在必要时我们大可以根据自己的需要灵活运用。在实现展 开/折叠功能时,子元素的 EditPart 应该能够监听所在容器的状态变化,当 collapsed 值改变 时更新与子图形相关的连接线(若不进行更新则这些连接线会变成"无头线")。让子元素 EditPart 监听容器模型的变化很简单,只要在 AttributePart 的 activate()里把自己作为监听器加到容 器模型的监听器列表即可,注意别忘记在 deactivate()里注销掉,而 propertyChange()方法 里是事件发生时的处理,代码如下: public void activate() { super.activate(); ((Attribute) getModel()).addPropertyChangeListener(this); ((Subject) getParent().getModel()).addPropertyChangeListener(this); } public void deactivate() { super.deactivate(); ((Attribute) getModel()).removePropertyChangeListener(this); ((Subject) getParent().getModel()).removePropertyChangeListener(this); } public void propertyChange(PropertyChangeEvent evt) { if (evt.getPropertyName().equals(Subject.PROP_COLLAPSED)) { refreshSourceConnections(); refreshTargetConnections(); } super.propertyChange(evt); } 这样,基本上就实现了容器的展开/折叠功能,之所以说"基本上",是因为我没有做仔细的测试 (时间关系),目前的代码有可能会存在问题,特别是在 Undo/Redo 以及多重选择这些情况 下;另外,这种方法只适用于容器里的子元素不是容器的情况,如果有多层的容器关系,则每一 层都要做类似的处理才可以。 GEF 入门系列(八、使用 EMF 构造 GEF 的模型) GEF 的设计没有对模型部分做任何限制,也就是说,我们可以任意构造自己的模型,唯一须要 保证的就是模型具有某种消息机制,以便在发生变化时能够通 知 GEF(通过 EditPart)。在以 前的几个例子里,我们都是利用 java.beans 包中的 PropertyChangeSupport 和 PropertyChangeListener 来实现消息机制的,这里将介绍一下如何让 GEF 利用 EMF 构造的 模型(下载例子,可编辑.emfsubject 文件,请对比之前功能相同的非 EMF 例子),假设你对 EMF 是什么已经有所了解。 EMF 使用自己定义的 Ecore 作为元模型,在这个元模型里定义了 EPackage、EClassifier、 EFeature 等等概念,我们要定 义的模型都是使用这些概念来定义的。同时因为 ecore 中的所 有概念都可以用本身的概念循环定义,所以 ecore 又是自己的元模型,也就是元元模型。关于 ecore 的详细概念,请参考 EMF 网站上的有关资料。 利用 EMF 为我们生成模型代码可以有多种方式,例如通过 XML Schema、带有注释的 Java 接口、Rose 的 mdl 文件以及.ecore 文件等,EMF 的代码生成器需要一个扩展名为.genmodel 的文件提供 信息,这个文件可以通过上面说的几种方式生成,我推荐使用 Omondo 公司的 EclipseUML 插件来构造.ecore 文件,该插件的免费版本可以从这里下载。(也许需要使用国 外代理才能访问 omondo 网站) 图 1 示例模型 为了节约篇幅和时间,我就不详细描述构造 EMF 项目的步骤了,这里主要把使用 EMF 与非 EMF 模型的区别做一个说明。图 1 是例子中使用的模型,其中 Dimension 和 Point 是两个外部 java 类型,由于 EMF 并不了解它们,所以定义为 datatype 类型。 使用两个 Plugins 为了让模型与编辑器更好的分离,可以让 EMF 模型单独位于一个 Plugin 中(名为 SubjectModel),而让编辑器 Plugin (SubjectEditor)依赖于它。这样做的另一个好处是, 当修改模型后,如果你愿意,可以很容易的删除以前生成的代码,然后全部重新生成。 EditPart 中的修改 在以前我们的 EditPart 是实现 java.beans.PropertyChangeListener 接口的,当模型改用 EMF 实现后, EditPart 应改为实现 org.eclipse.emf.common.notify.Adapter 接口,因为 EMF 的每个模型对象都是 Notifier,它维护了一个 Adapter 列表,可以把 Adapter 作为监听 器加入到模型的这个列表中。 实现 Adapter 接口时须要实现 getTarget()和 setTarget()方法,target 代表发出消息的那个 模型对象。我的实现方式是在 EditPart 里维护一个 Notifier 类型的 target 变量,这两个方法 分别返回和设置该变量即可。 还要实现 isAdapterForType()方法,该方法返回一个布尔值,表示这个 Adapter 是否应响应 指定类型的消息,我的实现一律为"return type.equals(getModel().getClass());"。 另外,propertyChanged()方法的名称应改为 notifyChanged()方法,其实现的功能和以前是 一样的,但代码有所不同,下面是 NodePart 中的实现,看一下就应该明白了: public void notifyChanged(Notification notification) { int featureId = notification.getFeatureID(ModelPackage.class); switch (featureId) { case ModelPackage.NODE__LOCATION: case ModelPackage.NODE__SIZE: refreshVisuals(); break; case ModelPackage.NODE__INCOMING_CONNECTIONS: refreshTargetConnections(); break; case ModelPackage.NODE__OUTGOING_CONNECTIONS: refreshSourceConnections(); break; } } 还有 active()/deactive()方法中的内容需要修改,作用还是把 EditPart 自己作为 Adapter(不 是 PropertyChangeListener 了)加入模型的监听器列表,下面是 SubjectPart 的实现,其 中 eAdapters()得到监听器列 表: public void activate() { super.activate(); ((Subject)getModel().eAdapters()).add(this); } 可以看到,我们对 EditPart 所做的修改实际是在两种消息机制之间的转换,如果你对以前的那 套机制很熟悉的话,这里理解起来不应该有任何困难。 ElementFactory 的修改 这个类的作用是根据 template 创建新的模型对象实例,以前的实现都是"new XXX()"这样, 用了 EMF 以后应改为"ModelFactory.eINSTANCE.createXXX()",EMF 里的每个模型对象 实例都应该是使用工厂创建的。 public Object getNewObject() { if (template.equals(Diagram.class)) return ModelFactory.eINSTANCE.createDiagram(); else if (template.equals(Subject.class)) return ModelFactory.eINSTANCE.createSubject(); else if (template.equals(Attribute.class)) return ModelFactory.eINSTANCE.createAttribute(); else if (template.equals(Connection.class)) return ModelFactory.eINSTANCE.createConnection(); return null; } 使用自定义 CreationFactory 代替 SimpleFactory 在原先的 PaletteFactory 里定义 CreationEntry 时都是指定 SimpleFactory 作为工厂,这个 类是使用 Class.newInstance()创建新的对象实例,而用 EMF 作为模型后,创建实例的工作 应该交给 ModelFactory 来完成,所以必须定义 自己的 CreationFactory。(注意,示例代码 里没有包含这个修改。) 处理自定义数据类型 我们的 Node 类里有两个非标准数据类型:Point 和 Dimension,要让 EMF 能够正确的将它们 保存,必须提供序列化和反序列化它们的方 法。在 EMF 为我们生成的代码里,找到 ModelFactoryImpl 类,这里有形如 convertXXXToString()和 createXXXFromString()的 几个方法,分别用来序列化和反序列化这种外部数据类型。我们要把它的缺省实现改为自己的方 式,下面是我对 Point 的实现方式: public String convertPointToString(EDataType eDataType, Object instanceValue) { Point p = (Point) instanceValue; return p.x + "," + p.y; } public Point createPointFromString(EDataType eDataType, String initialValue) { Point p = new Point(); String[] values = initialValue.split(","); p.x = Integer.parseInt(values[0]); p.y = Integer.parseInt(values[1]); return p; } 注意,修改后要将方法前面的@generated 注释删除,这样在重新生成代码时才不会被覆盖掉。 要设置使用这些类型的变量的缺省值会有点问题(例 如设置Node 类的location 属性的缺省值), 在 EMF 自带的 Sample Ecore Model Editor 里设置它的 defaultValueLiteral 为"100,100" (这是我们通过 convertPointToString()方法定 义的序列化形式)会报一个错,但不管它就 可以了,在生成的代码里会得到这个缺省值。 保存和载入模型 EMF 通过 Resource 管理模型数据,几个 Resource 放在一起称为 ResourceSet。前面说过, 要想正常保存模型,必须保证每个模 型对象都被包含在 Resource 里,当然间接包含也是可以 的。比如例子这个模型,Diagram 是被包含在 Resource 里的(创建新 Diagram 时即被加入), 而 Diagram 包含 Subject,Subject 包含 Attribute,所以它们都在 Resource 里。在图 1 中 可以看到, Diagram 和 Connection 之间存在一对多的包含关系,这个关系的主要作用就是 确保在保存模型时不会出现 DanglingHREFException,因为如果没有这个包含关系,则 Connection 对象不会被包含在任何 Resource 里。 在删除一个对象的时候,一定要保证它不再包含在 Resource 里,否则保存后的文件中会出现 很多空元素。比较容易犯错的地方是对 Connection 的处理,在删除连接的时候,只是从源节 点和目标节点里删除对这个连接的引用是不够的,因为这样只是在界面上消除了两个节点间的连 接 线,而这个连接对象还是包含在 Diagram 里的,所以还要调用从 Diagram 对象里删除它 才对,DeleteConnectionCommand 中的 代码如下: public void execute() { source.getOutgoingConnections().remove(connection); target.getIncomingConnections().remove(connection); connection.getDiagram().getConnections().remove(connection); } 当然,新建连接时也不要忘记将连接添加在 Diagram 对象里(代码见 CreateConnectionCommand)。保存和载入模型的代码请 看 SubjectEditor 的 init()方法 和 doSave()方法,都是很标准的 EMF 访问资源的方法,以下是载入的代码(如果是新创建的 文件,则 在 Resource 中新建 Diagram 对象): public void init(IEditorSite site, IEditorInput input) throws PartInitException { super.init(site, input); IFile file = ((FileEditorInput) getEditorInput()).getFile(); URI fileURI = URI.createPlatformResourceURI(file.getFullPath().toString()); resource = new XMIResourceImpl(fileURI); //注意要区分 XMIResource 和 XMLResource try { resource.load(null); diagram = (Diagram) resource.getContents().get(0); } catch (IOException e) { diagram = ModelFactory.eINSTANCE.createDiagram(); resource.getContents().add(diagram); } } 虽然到目前为止我还没有机会体会 EMF 在模型交互引用方面的优势,但经过进一步的了解和在 这个例子的应用,我对 EMF 的印象已有所改观。据我目前所知,使用 EMF 模型作为 GEF 的模 型部分至少有以下几个好处: 1. 只需要定义一次模型,而不是类图、设计文档、Java 代码等等好几处; 2. EMF 为模型提供了完整的消息机制,不用我们手动实现了; 3. EMF 提供了缺省的模型持久化功能(xmi),并且允许修改持久化方式; 4. EMF 的模型便于交叉引用,因为拥有足够的元信息,等等。 此外,EMF.Edit 框架能够为模型的编辑提供了很大的帮助,由于我现在对它还不熟悉,所以例 子里也没有用到,今后我会修改这个例子以利用 EMF.Edit。 GEF 入门系列(九、增加易用性) 当一个 GEF 应用程序实现了大部分必需的业务功能后,为了能让用户使用得更方便,我们应该 在易用性方面做些考虑。从 3.0 版本开始, GEF 增加了更多这方面的新特性,开发人员很容易 利用它们来改善自己的应用程序界面。这篇帖子将介绍主要的几个功能,它们有些在 GEF 2.1 中就出现了,但因为都是关于易用性的而且以前没有提到,所以放在这里一起来说。( 下载示 例代码) 可折叠调色板 在以前的例子里,我们的编辑器都继承自 GraphicalEditorWithPalette。GEF 3.0 提供了一个 功能更加丰富的编辑器父类:GraphicalEditorWithFlyoutPalette,继承它的编辑器具有一个 可以折叠的工具条,并且能够利用 Eclipse 自带的调色板视图,当调色板视图显示时,工具条会 自动转移到这个视图中。 图 1 可折叠和配置的调色板 与以前的 GraphicalEditorWithPalette 相比,继承 GraphicalEditorWithFlyoutPalette 的 编辑器要多做一些工作。首先要实现 getPalettePreferences() 方法,它返回一个 FlyoutPreferences 实例,作用是把调色板的几个状态信息(位置、大小和是否展开)保存起 来,这样下次打开编辑器的时候就可以自动套用这些设置。下面使用偏好设置的方式保存和载入 这些状态,你也可以使用其他方法,比如保存为.properties 文件: protected FlyoutPreferences getPalettePreferences() { return new FlyoutPreferences() { public int getDockLocation() { return SubjectEditorPlugin.getDefault().getPreferenceStore().getInt(IConstants.P REF_PALETTE_DOCK_LOCATION); } public void setDockLocation(int location) { SubjectEditorPlugin.getDefault().getPreferenceStore().setValue(IConstants.PREF_ PALETTE_DOCK_LOCATION,location); } … }; } 然后要覆盖缺省的 createPaletteViewerProvider()实现,在这里为调色板增加拖放支持,即 指定调色板为拖放源(之所以用这样的方式,原因是在编辑器里没有办法得到它对应的调色板实 例),在以前这个工作通常是在 initializePaletteViewer ()方法里完成的,而现在这个方法已 经不需要了: protected PaletteViewerProvider createPaletteViewerProvider() { return new PaletteViewerProvider(getEditDomain()) { protected void configurePaletteViewer(PaletteViewer viewer) { super.configurePaletteViewer(viewer); viewer.addDragSourceListener(new TemplateTransferDragSourceListener(viewer) ); } }; } GEF 3.0 还允许用户对调色板里的各种工具进行定制,例如隐藏某个工具,或是修改工具的描 述等等,这是通过给 PaletteViewer 定义一个 PaletteCustomizer 实例实现的,但由于时间 关系,这里暂时不详细介绍了,如果需要这项功能你可以参考 Logic 例子中的实现方法。 缩放 由于 Draw2D 中的图形都具有天然的缩放功能,因此在 GEF 里实现缩放功能是很容易的,而且 缩放的效果不错。GEF 为我们提供了 ZoomInAction 和 ZoomOutAction 以及对应的 RetargetAction(ZoomInRetargetAction 和 ZoomOutRetargetAction),只要在编辑器 里构造它们的实例,然后在编辑器的 ActionBarContributer 类里将它们添加到想要的菜单或 工具条位置即可。因为 ZoomInAction 和 ZoomOutAction 的构造方法要求一个 ZoomManager 类型的参数,而后者需要从 GEF 的 RootEditPart 中获得 (ScalableRootEditPart 或 ScalableFreeformRootEditPart),所以最好在编辑器的 configureGraphicalViewer()里构造这两个 Action 比较方便,请看下面的代码: protected void configureGraphicalViewer() { super.configureGraphicalViewer(); ScalableFreeformRootEditPart root = new ScalableFreeformRootEditPart(); getGraphicalViewer().setRootEditPart(root); getGraphicalViewer().setEditPartFactory(new PartFactory()); action = new ZoomInAction(root.getZoomManager()); getActionRegistry().registerAction(action); getSite().getKeyBindingService().registerAction(action); action = new ZoomOutAction(root.getZoomManager()); getActionRegistry().registerAction(action); getSite().getKeyBindingService().registerAction(action); } 假设我们想把这两个命令添加到主工具条上,在 DiagramActionBarContributor 里应该做两 件事:在 buildActions()里构造对应的 RetargetAction,然后在 contributeToToolBar()里 添加它们到工具条(原理请参考前面关于菜单和工具条的 帖子): protected void buildActions() { //其他命令 … //缩放命令 addRetargetAction(new ZoomInRetargetAction()); addRetargetAction(new ZoomOutRetargetAction()); } public void contributeToToolBar(IToolBarManager toolBarManager) { //工具条中的其他按钮 … //缩放按钮 toolBarManager.add(getAction(GEFActionConstants.ZOOM_IN)); toolBarManager.add(getAction(GEFActionConstants.ZOOM_OUT)); toolBarManager.add(new ZoomComboContributionItem(getPage())); } 请注意,在 contributeToToolBar()方法里我们额外添加了一个 ZoomComboContributionItem 的实例,这个类也是 GEF 提供的,它的作用是显示一个缩放 百分比的下拉框,用户可以选择或输入想要的数值。为了让这个下拉框能与编辑器联系在一起, 我们要修改一下编辑器的 getAdapter()方法,增加对它的支持: public Object getAdapter(Class type) { … if (type == ZoomManager.class) return getGraphicalViewer().getProperty(ZoomManager.class.toString()); return super.getAdapter(type); } 现在,打开编辑器后主工具条中将出现下图所示的两个按钮和一个下拉框: 图 2 缩放工具条 有时候我们想让程序把用户当前的缩放值记录下来,以便下次打开时显示同样的比例。这就须要 在画布模型里增加一个 zoom 变量,在编辑器的初始化过程中增加下面的语句,其中 diagram 是我们的画布实例: ZoomManager manager = (ZoomManager) getGraphicalViewer().getProperty(ZoomManager.class.toString()); if (manager != null) manager.setZoom(diagram.getZoom()); 在保存模型前得到当前的缩放比例放在画布模型里一起保存: ZoomManager manager = (ZoomManager) getGraphicalViewer().getProperty(ZoomManager.class.toString()); if (manager != null) diagram.setZoom(manager.getZoom()); 辅助网格 你可能用过一些这样的应用程序,画布里可以显示一个灰色的网格帮助定位你的图形元素,当被 拖动的节点接近网格线条时会被"吸附"到网格上,这样可以很容易的把画布上的图形元素排列整 齐,GEF 3.0 里就提供了显示这种辅助网格的功能。 图 3 辅助编辑网格 是否显示网格以及是否打开吸附功能是由 GraphicalViewer 的两个布尔类型的属性(property) 值决定的,它们分别是 SnapToGrid.PROPERTY_GRID_VISIBLE 和 SnapToGrid.PROPERTY_GRID_ENABLED,这些属性是通过 GriaphicalViewer.getProperty()和 setProperty()方法来操作的。GEF 为我们提供了一个 ToggleGridAction 用来同时切换它们的值(保持这两个值同步确实符合一般使用习惯),但没 有像缩放功能那样提供对应的 RetargetAction,不知道 GEF 是出于什么考虑。另外因为这个 Action 没有预先设置的图标,所以把它直接添加到工具条上会很不好看,所以要么把它只放在 菜单中,要么为它设置一个图标,至于添加到菜单的方法这里不赘述了。 要想在保存模型时同时记录当前网格线是否显示,必须在画布模型里增加一个布尔类型变量,并 在打开模型和保存模型的方法中增加处理它的代码。 几何对齐 这个功能也是为了方便用户排列图形元素的,如果打开了此功能,当用户拖动的图形有某个边靠 近另一图形的某个平行边延长线时,会自动吸附到这条延长线上;若两个图形的中心线(通过图 形中心点的水平或垂直线)平行靠近时也会产生吸附效果。例如下图中,Subject1 的左边与 Subject2 的右边是吸附在一起的,Subject3 原本是与 Subject2 水平中心线吸附的,而用户 在拖动的过程中它的上边吸附到 Subject1 的底边。 图 4 几何对齐 几何对齐也是通过 GraphicalViewer 的属性来控制是否打开的,属性的名称是 SnapToGeometry.PROPERTY_SNAP_ENABLED,值为布尔类型。在程序里增加吸附对齐 切换的功能和前面说的增加网格切换功能基本是一样的,记住 GEF 为它提供的 Action 是 ToggleSnapToGeometryAction。 要实现对齐功能,还有一个重要的步骤,那就是在画布所对应的 EditPart 的 getAdapter()方 法里增加对 SnapToHelper 类的回应,像下面这样: public Object getAdapter(Class adapter) { if (adapter == SnapToHelper.class) { List snapStrategies = new ArrayList(); Boolean val = (Boolean)getViewer().getProperty(RulerProvider.PROPERTY_R ULER_VISIBILITY); if (val != null && val.booleanValue()) snapStrategies.add(new SnapToGuides(this)); val = (Boolean)getViewer().getProperty(SnapToGeometry.PROPERTY_SNAP _ENABLED); if (val != null && val.booleanValue()) snapStrategies.add(new SnapToGeometry(this)); val = (Boolean)getViewer().getProperty(SnapToGrid.PROPERTY_GRID_ENAB LED); if (val != null && val.booleanValue()) snapStrategies.add(new SnapToGrid(this)); if (snapStrategies.size() == 0) return null; if (snapStrategies.size() == 1) return (SnapToHelper)snapStrategies.get(0); SnapToHelper ss[] = new SnapToHelper[snapStrategies.size()]; for (int i = 0; i < snapStrategies.size(); i++) ss[i] = (SnapToHelper)snapStrategies.get(i); return new CompoundSnapToHelper(ss); } return super.getAdapter(adapter); } 标尺和辅助线 标尺位于画布的上部和左侧,在每个标尺上可以建立很多与标尺垂直的辅助线,这些显示在画布 上的虚线具有吸附功能。 图 5 标尺和辅助线 标尺和辅助线的实现要稍微复杂一些。首先要修改原有的模型,新增加标尺和辅助线这两个类, 它们之间的关系请看下图:< /p> 图 6 增加标尺和辅助线后的模型 与上篇帖子里的 模型图比较后可以发现,在 Diagram 类里增加了四个变量,其中除 rulerVisibility 以外三个的作用都在前面部分做过介绍,而 rulerVisibility 和它们类似,作用记 录标尺的可见性,当然只有在标尺可见的时候辅助线才是可见的。我们新增了 Ruler 和 Guide 两个类,前者表示标尺,后者表示辅助线。因为辅助线是建立在标尺上的,所以 Ruler 到 Guide 有一个包含关系(黑色菱形);画布上有两个标尺,分别用 topRuler 和 leftRuler 这两个变量 引用,也是包含关系,也就是说,画布上只能同时具有这两个标尺;Node 到 Guide 有两个引 用,表示 Node 吸附到的两条辅助线(为了简单起见,在本文附的例子中并没有实际使用到它 们,Guide 类中定义的几个方法也没有用到)。Guide 类里的 map 变量用来记录吸附在自己上 的节点和对应的吸附边。要让画布上能够显示标尺,首先要将原先的 GraphicalViewer 改放在 一个 RulerComposite 实例上(而不是直接放在编辑器上),后者是 GEF 提供的专门用于显 示标尺的组件,具体的改变方法如下: //定义一个 RulerComposite 类型的变量 private RulerComposite rulerComp; //创建 RulerComposite,并把 GraphicalViewer 创建在其上< span style="color: #008000;"> protected void createGraphicalViewer(Composite parent) { rulerComp = new RulerComposite(parent, SWT.NONE); super.createGraphicalViewer(rulerComp); rulerComp.setGraphicalViewer((ScrollingGraphicalViewer) getGraphicalViewer()); } //覆盖 getGraphicalControl 返回 RulerComposite 实例< span style="color: #008000;"> protected Control getGraphicalControl() { return rulerComp; } 然后,要设置 GraphicalViewer 的几个有关属性,如下所示,其中前两个分别表示左侧和上方 的标尺,而最后一个表示标尺的可见性: getGraphicalViewer().setProperty(RulerProvider.PROPERTY_VERTICAL_RULER,ne w SubjectRulerProvider(diagram.getLeftRuler())); getGraphicalViewer().setProperty(RulerProvider.PROPERTY_HORIZONTAL_RULER, new SubjectRulerProvider(diagram.getTopRuler())); getGraphicalViewer().setProperty(RulerProvider.PROPERTY_RULER_VISIBILITY,ne w Boolean(diagram.isRulerVisibility())); 在前两个方法里用到了 SubjectRulerProvider 这个类,它是我们从 RulerProvider 类继承过 来的, RulerProvider 是一个比较特殊的类,其作用有点像 EditPolicy,不过除了一些 getXXXCommand()方法以外,还有其他几个方法要实现。需要返回 Command 的方法包括: getCreateGuideCommand()、getDeleteGuideCommand()和 getMoveGuideCommand(),分别返回创建辅助线、删除辅助线和移动辅助线的命令,下面 列出创建辅助线的命令,其他两个的实现方式是类似的,你可以在本文所附例子中找到它们的代 码: public class CreateGuideCommand extends Command { private Guide guide; private Ruler ruler; private int position; public CreateGuideCommand(Ruler parent, int position) { setLabel("Create Guide"); this.ruler = parent; this.position = position; } public void execute() { guide = ModelFactory.eINSTANCE.createGuide();//创建一条新的辅助线 guide.setHorizontal(!ruler.isHorizontal()); guide.setPosition(position); ruler.getGuides().add(guide); } public void undo() { ruler.getGuides().remove(guide); } } 接下来再看看 RulerProvider 的其他方法,SubjectRulerProvider 维护一个 Ruler 对象,在 构造方法里要把它的值传入。此外,在构造方法里还应该给 Ruler 和 Guide 模型对象增加监听 器用来响应标尺和辅助线的变化,下面是 Ruler 监听器的主要代码(因为使用了 EMF 作为模型, 所以监听器实现为 Adapter。如果你不用 EMF,可以使用 PropertyChangeListener 实现): public void notifyChanged(Notification notification) { switch (notification.getFeatureID(ModelPackage.class)) { case ModelPackage.RULER__UNIT: for (int i = 0; i < listeners.size(); i++) ((RulerChangeListener) listeners.get(i)).notifyUnitsChanged(ruler.getUnit()); break; case ModelPackage.RULER__GUIDES: Guide guide = (Guide) notification.getNewValue(); if (getGuides().contains(guide)) guide.eAdapters().add(guideAdapter); else guide.eAdapters().remove(guideAdapter); for (int i = 0; i < listeners.size(); i++) ((RulerChangeListener) listeners.get(i)).notifyGuideReparented(guide); break; } } 可以看到监听器在被触发时所做的工作实际上是触发这个 RulerProvider 的监听器列表 (listeners)里的所有监听器,而这些监听器就是 RulerEditPart 或 GuideEditPart,而我们 不需要去关心这两个类。Ruler 的事件有两种,一是单位(象素、厘米、英寸)改变,二是创建 辅助线,在创建辅助线的情况要给这个辅助线增加监听器。下面是 Guide 监听器的主要代码: public void notifyChanged(Notification notification) { Guide guide = (Guide) notification.getNotifier(); switch (notification.getFeatureID(ModelPackage.class)) { case ModelPackage.GUIDE__POSITION: for (int i = 0; i < listeners.size(); i++) ((RulerChangeListener) listeners.get(i)).notifyGuideMoved(guide); break; case ModelPackage.GUIDE__MAP: for (int i = 0; i < listeners.size(); i++) ((RulerChangeListener) listeners.get(i)).notifyPartAttachmentChanged(notification.getNewValue(),guide); break; } } Guide 监听器也有两种事件,一是辅助线位置改变,二是辅助线上吸附的图形的增减变化。请 注意,这里的循环一定不要用 iterator 的方式,而应该用上面列出的下标方式,否则会出现 ConcurrentModificationException 异常,原因和 RulerProvider 的 notifyXXX()实现有关。 我们的 SubjectRulerProvider 构造方法如下所示,它的主要工作就是增加监听器: public SubjectRulerProvider(Ruler ruler) { this.ruler = ruler; ruler.eAdapters().add(rulerAdapter); //载入模型的情况下,ruler 可能已经包含一些 guides,所以要给它们增加监听器< span style="color: #008000;"> for (Iterator iter = ruler.getGuides().iterator(); iter.hasNext();) { Guide guide = (Guide) iter.next(); guide.eAdapters().add(guideAdapter); } } 在 RulerProvider 里还有几个方法要实现才能正确使用标尺:getRuler()返回 RulerProvider 维护的 Ruler 实例,getGuides()返回辅助线列表,getGuidePosition(Object)返回某条辅助 线在标尺上的位置(以 pixel 为单位),getPositions()返回标尺上所有辅助线位置构成的整 数数组。以下是本例中的实现方式: public Object getRuler() { return ruler; } public List getGuides() { return ruler.getGuides(); } public int[] getGuidePositions() { List guides = getGuides(); int[] result = new int[guides.size()]; for (int i = 0; i < guides.size(); i++) { result[i] = ((Guide) guides.get(i)).getPosition(); } return result; } public int getGuidePosition(Object arg0) { return ((Guide) arg0).getPosition(); } 有了这个自定义的 RulerProvider 类,再通过把该类的两个实例被放在 GraphicalViewer 的两 个属性(PROPERTY_VERTICAL_RULER 和 PROPERTY_HORIZONTAL_RULER)中,画布 就具有标尺的功能了。GEF 提供了用于切换标尺可见性的命令:ToggleRulerVisibilityAction, 我们使用和前面同样的方法把它加到主菜单即可控制显示或隐藏标尺和辅助线。 位置和尺寸对齐 图形编辑工具大多具有这样的功能:选中两个以上图形,再按一下按钮就可以让它们以某一个边 或中心线对齐,或是调整它们为同样的宽度高度。GEF 提供 AlignmentAction 和 MatchSizeAction 分别用来实现位置对齐和尺寸对齐,使用方法很简单,在编辑器的 createActions()方法里构造需要的对齐方式 Action(例如对齐到上边、下边等等),然后在 编辑器的 ActionBarContributor 里通过这些 Action 对应的 RetargetAction 将它们添加到菜 单或工具条即可。编辑器里的代码如下,注意最后一句的作用是把它们加到 selectionAction 列表里以响应选择事件: IAction action=new AlignmentAction((IWorkbenchPart)this,PositionConstants.LEFT); getActionRegistry().registerAction(action); getSelectionActions().add(action.getId()); … AlignmentAction 的构造方法的参数是编辑器本身和一个代表对齐方式的整数,后者可以是 PositionConstants.LEFT、CENTER、RIGHT、TOP、MIDDLE、BOTTOM 中的一个; MatchSizeAction 有两个子类,MatchWidthAction 和 MatchHeightAction,你可以使用它 们达到只调整宽度或高度的目的。下图是添加在工具条中的按钮,左边六个为位置对齐,最后两 个为尺寸对齐,请注意,当选择多个图形时,被六个黑点包围的那个称为"主选择",对齐时以该 图形所在位置和大小为准做调整。 图 7 位置对齐和尺寸对齐 GEF 入门系列(十、表格的一个实现) 在目前的 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() + ICo nstants.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.g etColumns().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 负责单元格的布局。同样是因为修改的工作量相对比较大而没有采用,因为 那样的话行和列都要使用自定义方式处理,而这篇贴子介绍的方法只关心行的处理就可以了。当 然,这里说的也不是什么标准实现,不过效果还是不错的,而且确实可以实现,如果你有类似的 需求可以作为参考。 GEF 入门系列(十一、树的一个实现) 两天前 GEF 发布了 3.1M7 版本,但使用下来发现和 M6 没有什么区别,是不是主要为了和 Eclipse 版本相配套?希望 3.1 正式版早日发布,应该会新增不少内容。上一篇帖子介绍了如何 实现表格功能,在开发过程中,另一个经常用到的功能就是树,虽然 SWT 提供了标准的树控件, 但使用它完成如组织结构图这样的应用还是不够直观和方便。在目前版本(3.1M7)的 GEF 中 虽然没有直接支持树的实现,但 Draw2D 提供的例子程序里却有我们可以利用的代码 (org.eclipse.draw2d.examples.tree.TreeExample,运行界面见下图),通过它可以节约 不少工作量。 图 1 Draw2D 例子中的 TreeExample 记得数年前曾用 Swing 做过一个组织结构图的编辑工具,当时的实现方式是让画布使用 XYLayout,在适当的时候计算和刷新每个树节点的位置,算法的思想则是深度优先搜索,非树 叶节点的位置由其子节点的数目和位置决定。我想这应该是比较直观的方法吧,但是这次看了 Draw2D 例子里的实现觉得也很有道理,以前没想到过。在这个例子里树节点图形称为 TreeBranch,它包含一个 PageNode(表现为带有折角的矩形)和一个透明容器 contentsPane, (一个 Layer,用来放置子节点)。在一般情况下,TreeBranch 本身使用名为 NormalLayout 的布局管理器将 PageNode 放在子节点的正上方,而 contentsPane 则使用名为 TreeLayout 的布局管理器计算每个子节点应在的位置。所以我们看到的整个树实际上是由很多层子树叠加而 成的,任何一个非叶节点对应的图形的尺寸都等于以它为根节点的子树所占区域的大小。 从这个例子里我们还看到,用户可以选择使用横向或纵向组织树(见图 2),可以压缩各节点之 间的空隙,每个节点可以横向或纵向排列子节点,还可以展开或收起子节点,等等,这为我们实 现一个方便好用的树编辑器提供了良好的基础(视图部分的工作大大简化了)。 图 2 纵向组织的树 这里要插一句,Draw2D 例子中提供的这些类的具体内容我没有仔细研究,相当于把它们当作 Draw2D API 的一部分来用了(包括 TreeRoot、TreeBranch、TreeLayout、BranchLayout、 NormalLayout、HangingLayout、PageNode 等几个类,把代码拷到你的项目中即可使用), 因为按照 GEF 3.1 的计划表,它们很有可能以某种形式出现在正式版的 GEF 3.1 里。下面介绍 一下我是如何把它们转换为 GEF 应用程序的视图部分从而实现树编辑器的。 首先从模型部分开始。因为树是由一个个节点构成的,所以模型中最主要的就是节点类(我称为 TreeNode),它和例子里的 TreeBranch 图形相对应,它应该至少包含 nodes(子节点列表) 和 text(显示文本)这两个属性;例子里有一个 TreeRoot 是 TreeBranch 的子类,用来表示 根节点,在 TreeRoot 里多了一些属性,如 horizontal、majorSpacing 等等用来控制整个树 的外观,所以模型里也应该有一个继承 TreeNode 的子类,而实际上这个类就应该是编辑器的 contents,它对应的图形 TreeRoot 也就是一般GEF应用程序里的画布,这个地方要想想清楚。 同时,虽然看起来节点间有线连接,但这里我们并不需要 Connection 对象,这些线是由布局 管理器绘制的,毕竟我们并不需要手动改变线的走向。所以,模型部分就是这么简单,当然别忘 了要实现通知机制,下面看看都有哪些 EditPart。 与模型相对应,我们有 TreeNodePart 和 TreeRootPart,后者和前者之间也是继承关系。在 getContentPane()方法里,要返回 TreeBranch 图形所包含的 contentsPane 部分;在 getModelChildren()方法里,要返回 TreeNode 的 nodes 属性;在 createFigure()方法里, TreeNodePart 应返回 TreeBranch 实例,而 TreeRootPart 要覆盖这个方法,返回 TreeRoot 实例;另外要注意在 refreshVisuals()方法里,要把模型的当前属性正确反映到图形中,例如 TreeNode 里有反映节点当前是否展开的布尔变量 expanded,则 refreshVisuals()方法里一 定要把这个属性的当前值赋给图形才可以。以下是 TreeNodePart 的部分代码: public IFigure getContentPane() { return ((TreeBranch) getFigure()).getContentsPane(); } protected List getModelChildren() { return ((TreeNode) getModel()).getNodes(); } protected IFigure createFigure() { return new TreeBranch(); } protected void createEditPolicies() { installEditPolicy(EditPolicy.COMPONENT_ROLE, new TreeNodeEditPolicy()); installEditPolicy(EditPolicy.LAYOUT_ROLE, new TreeNodeLayoutEditPolicy()); installEditPolicy(EditPolicy.SELECTION_FEEDBACK_ROLE, new ContainerHighli ghtEditPolicy()); } 上面代码中用到了几个 EditPolicy,这里说一下它们各自的用途。实际上,从 Role 中已经可以 看出来,TreeNodeEditPolicy 是用来负责节点的删除,没有什么特别; TreeNodeLayoutEditPolicy 则复杂一些,我把它实现为 ConstrainedLayoutEditPolicy 的一 个子类,并实现 createAddCommand()和 getCreateCommand()方法,分别返回改变节点 的父节点和创建新节点的命令,另外我让 createChildEditPolicy()方法返回 NonResizableEditPolicy 的实例,并覆盖其 createSelectionHandles()方法如下,以便在用 户选中一个节点时用一个控制点表示选中状态,不用缺省边框的原因是,边框会将整个子树包住, 不够美观,并且在多选的时候界面比较混乱。 protected List createSelectionHandles() { List list=new ArrayList(); list.add(new ResizeHandle((GraphicalEditPart)getHost(), PositionConstants.N ORTH)); return list; } 选中节点的效果如下图,我根据需要改变了树节点的显示(修改 PageNode 类): 图 3 同时选中三个节点(Node2、Node3 和 Node8) 最后一个 ContainerHighlightEditPolicy 的唯一作用是当用户拖动节点到另一个节点区域中时, 加亮显示后者,方便用户做出是否应该放开鼠标的选择。它是 GraphicalEditPolicy 的子类,部 分代码如下,如果你看过 Logic 例子的话,应该不难发现这个类就是我从那里拿过来然后修改 一下得到的。 protected void showHighlight() { ((TreeBranch) getContainerFigure()).setSelected(true); } public void eraseTargetFeedback(Request request) { ((TreeBranch) getContainerFigure()).setSelected(false); } 好了,现在树编辑器应该已经能够工作了。为了让用户使用更方便,你可以实现展开/收起子节 点、横向/纵向排列子节点等等功能,在视图部分 Draw2D 的例子代码已经内置了这些功能,你 要做的就是给模型增加适当的属性。我这里的一个截图如下所示,其中 Node1 是收起状态, Node6 纵向排列子节点(以节省横向空间)。 图 4 树编辑器的运行界面 这个编辑器我花一天时间就完成了,但如果不是利用 Draw2D 的例子,相信至少要四至六天, 而且缺陷会比较多,功能上也不会这么完善。我感觉在 GEF 中遇到没有实现过的功能前最好先 找一找有没有可以利用的资源,比如 GEF 提供的几个例子就很好,当然首先要理解它们才谈得 上利用。 GEF 入门系列(十二、自定义 Request) 先简单回顾一下 Request 在 GEF 里的作用。Request 是 GEF 里一个比较重要的角色,Tool 将原始的鼠标事件转换为 EditPart 可以识别的请求,Request 则承载了这些请求信息。举例来 说,用户在调色板(Palette)里选择了创建节点工具(CreationTool),然后在画布区域按下 鼠标左键,这时产生在画布上的鼠标单击事件将被 CreationTool 转换为一个 CreateRequest, 它里面包含了要创建的对象,坐标位置等信息。 EditPart 上如果安装了能够处理 CreateRequest 的 EditPolicy,则相应的 EditPolicy 会根据这个 CreateRequest 创建一个 Command,由后者实际执行创建新对象的必要操作。 GEF 已经为我们提供了很多种类的 Request,其中最常用的是 CreateRequest 及其子类 CreateConnectionRequest,其他比较常见的还有 SelectionRequest, ChangeBoundsRequest 和 ReconnectRequest 等等。要实现一个典型的图形化应用程序, 例如 UML 类图编辑器,这些预定义的 Request 基本够用了。然而各种稀奇古怪的需求我相信 大家也见过不少,很多需求不太符合约定俗成的使用习惯,因此实现起来更多依赖开发人员的编 码,而不是开发框架带来的便利。在这种时候,我们唯一的期望就是开发框架提供足够的扩展机 制,以便让我们额外编写的代码能和其他代码和平共处,幸好 GEF 是具有足够的扩展性的。有 点跑题了,再回到 Request 的问题上,为了说明什么情况下需要自定义 Request,我在前文“应 用实例”里的示例应用基础上假设一个新的需求: 在 Palette 里增加三个工具,作用分别是把选中节点的背景颜色改变为红色、绿色和蓝色。 假如你用过 Photoshop 或类似软件,这个需求很像给节点上色的“油漆桶”或“上色工具”,当然 在用户界面的背后,实际应用里这些颜色可能代表一个节点的重要程度,优先级或是异常信息等 等。现在,让我们通过创建一个自定义的 Request 来实现这个需求,还是以前文中的示例项目 为基础。 一、首先,原来的模型里节点(Node)类里没有反映颜色的成员变量,所以先要在 Node 类里 添加一个 color 属性,以及相应的 getter/setter 方法,注意这个 setter 方法里要和其他成员 变量的 setter 方法一样传递模型改变的消息。仿照其他成员变量,还应该有一个静态字符串变 量,用来区分消息对应哪个属性。 final public static String PROP_COLOR = "COLOR"; protected RGB color = new RGB(255, 255, 255); public RGB getColor() { return color; } public void setColor(RGB color) { if (this.color.equals(color)) { return; } this.color = color; firePropertyChange(PROP_COLOR, null, color); } 二、然后,要让 Node 的 color 属性变化能够反映到图形上,因此要修改 NodePart 里的 propertyChanged()和 refreshVisuals()方法,在前者里增加对 color 属性的响应,在后者里 将 NodeFigure 的背景颜色设置为 Node 的 color 属性对应的颜色。(注意,Color 对象是系 统资源对象,实际使用里需要缓存以避免系统资源耗尽,为节约篇幅起见,示例代码直接 new Color()了) public void propertyChange(PropertyChangeEvent evt) { if (evt.getPropertyName().equals(Node.PROP_COLOR))//Response to color cha nge refreshVisuals(); } protected void refreshVisuals() { ((NodeFigure) this.getFigure()).setBackgroundColor(new Color(null, node.getC olor()));//TODO cache color instances } 三、现在来创建我们自己的 Request,因为目的是改变颜色,所以不妨叫做 ChangeColorRequest。它应当继承自 org.eclipse.gef.Request,我们需要 ChangeColorRequest 上带有两样信息:1.需要改变颜色的节点;2.目标颜色。因此它应该有 这两个成员变量。 import org.eclipse.gef.Request; import org.eclipse.swt.graphics.RGB; import com.example.model.Node; public class ChangeColorRequest extends Request{ final static public String REQ_CHANGE_COLOR="REQ_CHANGE_COLOR"; private Node node; private RGB color; public ChangeColorRequest(Node node, RGB color) { super(); this.color = color; this.node = node; setType(REQ_CHANGE_COLOR); } public RGB getColor() { return color; } public Node getNode() { return node; } public void setNode(Node node) { this.node = node; } public void setColor(RGB color) { this.color = color; } } ChangeColorRequest 看起来和一个 JavaBean 差不多,的确如此,因为 Request 的作用就 是传递翻译后的鼠标事件。如果你看一下 org.eclipse.gef.Request 的代码,你会发现 Request 还有一个 type 属性,这个属性一般是一个字符串(在 gef 的 RequestConstants 里预定义了 一些,如 RequestConstants.REQ_SELECTION_HOVER), EditPolicy 可以根据它决定是 否处理这个 Request。在我们的例子里,顺便定义了这样一个常量字符串 REQ_CHANGE_COLOR,在后面的 ChangeColorEditPolicy 里会用到它。 四、现在有一个问题,这个 Request 的实例应该在哪里生成?答案是在 Tool 里,用户在画布 区域按下鼠标左键时,当前 Palette 里被选中的 Tool 负责创建一个 Request。我们现在面对 的这个需求需要我们创建一种新的 Tool:ChangeColorTool。我们让 ChangeColorTool 继承 org.eclipse.gef.tools.SelectionTool,因为“上色工具”的用法和“选择工具”基本上差不多。显 然,我们需要覆盖的是 handleButtonDown()方法,用来告诉 gef 如果用户当前选择了这个工 具,在画布区域按下鼠标会发生什么事情。代码如下: import org.eclipse.gef.EditPart; import org.eclipse.gef.commands.Command; import org.eclipse.gef.tools.SelectionTool; import org.eclipse.swt.graphics.RGB; import com.example.model.Node; import com.example.parts.NodePart; public class ChangeColorTool extends SelectionTool { private RGB color; public ChangeColorTool(RGB color) { super(); this.color = color; } /** * If target editpart is an {@link NodePart}, create a {@link ChangeColorRequ est} instance, * get command from target editpart with this request and execute. */ @Override protected boolean handleButtonDown(int button) { //Get selected editpart EditPart editPart = this.getTargetEditPart(); if (editPart instanceof NodePart) { NodePart nodePart = (NodePart) editPart; Node node = (Node) nodePart.getModel(); //Create an instance of ChangeColorRequest ChangeColorRequest request = new ChangeColorRequest(node, color); //Get command from the editpart Command command = editPart.getCommand(request); //Execute the command this.getDomain().getCommandStack().execute(command); return true; } return false; } } 五、有了 Tool,还需要用 ToolEntry 把它包装起来添加到 Palette 里。所以我们创建一个名为 ChangeColorToolEntry 并继承 org.eclipse.gef.palette.ToolEntry 的类,覆盖 createTool ()方法,让它返回我们的 ChangeColorTool 实例。这个 ChangeColorToolEntry 代码应该很 容易理解: import org.eclipse.gef.SharedCursors; import org.eclipse.gef.Tool; import org.eclipse.gef.palette.ToolEntry; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.swt.graphics.RGB; public class ChangeColorToolEntry extends ToolEntry { private RGB color; public ChangeColorToolEntry(RGB color, String label, String shortDesc, Image Descriptor iconSmall, ImageDescriptor iconLarge) { super(label, shortDesc, iconSmall, iconLarge); this.color = color; } @Override public Tool createTool() { ChangeColorTool tool = new ChangeColorTool(color); tool.setUnloadWhenFinished(false);//Switch to selection tool after performe d? tool.setDefaultCursor(SharedCursors.CROSS);//Any cursor you like return tool; } } 六、要把三个这样的 ToolEntry 添加到Palette 里,当然是通过修改原来的 PaletteFactory 类。 为节约篇幅,这里就不帖它的代码了,可以下载并参考示例代码 PaletteFactory.java 里的 createCategories()和 createColorDrawer()方法。 到目前为止,ChangeColorRequest 已经可以发出了,接下来要解决的问题是如何让 EditPart 处理这个请求。 七、我们知道,gef 里任何对模型的修改都是通过 command 完成的,因此一个 ChangeColorCommand 肯定是需要的。它的 execute()方法和 undo()方法如下所示: public class ChangeColorCommand extends Command{ private RGB oldColor; @Override public void execute() { oldColor = node.getColor(); node.setColor(color); } @Override public void undo() { node.setColor(oldColor); } } 八、EditPolicy 负责接收所有的 Request,所以还要创建一个 ChangeColorEditPolicy。在下 面列出的代码里,你会看到我们定义了一个新的“Role”字符串,过一会儿我们在 EditPart 上安 装这个 EditPolicy 的时候要以这个字符串作为 Key,以避免覆盖 EditPart 上已有的其他 EditPolicy。 import org.eclipse.gef.Request; import org.eclipse.gef.commands.Command; import org.eclipse.gef.editpolicies.AbstractEditPolicy; import org.eclipse.swt.graphics.RGB; import com.example.model.Node; public class ChangeColorEditPolicy extends AbstractEditPolicy { final static public String CHANGE_COLOR_ROLE = "CHANGE_COLOR_ROLE"; @Override public Command getCommand(Request request) { //Judge whether this request is intersting by its type if (request.getType() == ChangeColorRequest.REQ_CHANGE_COLOR) { ChangeColorRequest theRequest = (ChangeColorRequest) request; //Get information from request Node node = theRequest.getNode(); RGB color = theRequest.getColor(); //Create corresponding command and return it ChangeColorCommand command = new ChangeColorCommand(node, col or); return command; } return null; } } 九、最后还是回到 EditPart,前面在第二个步骤里我们曾经修改过的 NodePart 里还有最后一 处需要添加,那就是在 installEditPolicies()方法里添加刚刚创建的 ChangeColorEditPolicy: protected void createEditPolicies() { //Add change color editpolicy installEditPolicy(ChangeColorEditPolicy.CHANGE_COLOR_ROLE, new ChangeCol orEditPolicy()); } 现在我们已经完成了所有必要的修改,来看一下运行结果。 总结一下,需要创建的类有:ChangeColorRequest, ChangeColorTool, ChangeColorToolEntry, ChangeColorCommand, ChangeColorEditPolicy;需要修改的 类有:Node, NodePart, PaletteFactory。在实例项目里,为了方便大家浏览,所有新创建的 类都放在 com.example.request 包里,实际项目里还是建议分别放在对应的包里。 下载示例代码(在 eclipse3.2.1 和 gef3.2 下编译通过) GEF 常见问题及技巧- 把 GEF 放在 ViewPart 里 其实可以放在任何 Composite 上,当然也就可以放在视图里了。关键任务是创建 GraphicalViewer、RootEditPart、EditDomain 和 EditPartFactory 这些对象,下面的代码 是我从别处拷来的,稍微修改了一下。 public class TestView extends ViewPart { ScrollingGraphicalViewer graphicalViewer; FigureCanvas canvas; Diagram diagram; public void createPartControl(Composite parent) { graphicalViewer = new ScrollingGraphicalViewer(); canvas = (FigureCanvas) graphicalViewer.createControl(parent); ScalableFreeformRootEditPart root = new ScalableFreeformRootEditPart(); graphicalViewer.setRootEditPart(root); graphicalViewer.setEditDomain(new EditDomain()); graphicalViewer.setEditPartFactory(new PartFactory()); graphicalViewer.setContents(diagram); } } 运行结果如下,这个基本上只有视图的功能,也可以增加编辑功能,例如对 GraphicalViewer 加一个 DropTargetListener 就可以从调色板里拉对象上来了,等等。这个代码有点问题,就 是打开 View 后要调整一下大小才能显示出图形,该怎么解决呢…… EMF+GEF 的属性页问题 最近有朋友问使用 EMF 作为 GEF 模型时,如何在选中 editpart 时在属性页里显示属性的问题。 是的,因为 GEF 是这样判断是否填充属性 页的: public Object getAdapter(Class key) { if (IPropertySource.class == key) { if (getModel() instanceof IPropertySource) return getModel(); if (getModel() instanceof IAdaptable) return ((IAdaptable)getModel()).getAdapter(key); } if (AccessibleEditPart.class == key) return getAccessibleEditPart(); return null; } 所以,一般(不使用 EMF)我们让模型类实现 IPropertySource 接口即可看到属性。而用 EMF 生成的模型类是不实现这个接口的,因此用户在界面上选中 editpart 时属性页里只能是空白。 要解决这个问题,一种方式是覆盖 editpart 的 getAdapter()方法,返回一个自定义的 PropertySource, 这个办法比较直接,但那么多属性写起来很麻烦,更重要的是当 ecore 模 型改变后这些属性是不会跟着变的;另一种方式是在 editor 类里作文章,工作量 比较小,具体 办法如下: ModelItemProviderAdapterFactory adapterFactory; AdapterFactoryContentProvider adapterFactoryConentProvider; //Constructor of the editor public TobeEditor() { setEditDomain(new DefaultEditDomain(this)); //For getting propertysource from emf.edit adapterFactory = new ModelItemProviderAdapterFactory(); adapterFactoryConentProvider = new AdapterFactoryContentProvider(adapterF actory); } public Object getAdapter(Class type) { if (type == IContentOutlinePage.class) return new OutlinePage(); if (type == org.eclipse.ui.views.properties.IPropertySheetPage.class) { PropertySheetPage page = new PropertySheetPage(); UndoablePropertySheetEntry root = new UndoablePropertySheetEntry(getC ommandStack()); root.setPropertySourceProvider(new IPropertySourceProvider() { public IPropertySource getPropertySource(Object object) { if (object instanceof EditPart) { Object model = ((EditPart) object).getModel(); return new PropertySource(model, (IItemPropertySource) adapterF actory.adapt(model, IItemPropertySource.class)); } else { return adapterFactoryConentProvider.getPropertySource(object); } } }); page.setRootEntry(root); return page; } return super.getAdapter(type); } 也就是对 UndoablePropertySheetEntry 做一些处理,让它能够适应 editpart 的选择(GEF 里选中元素的都是 editpart 而非 model 本身)。这个方法在显示属性方面没有什么问题,但 在属性页里修改属性值后,是不能 undo 的,而且不会显示表示 dirty 的*号,所以还有待改进。 EMF+GEF 里像这种别扭的地方还远不只这一处,不过我相信大部分都是可以适当修改一些代 码解决的,希望它们之间增加一些合作,同时继续期待 GMF。 自动换行的 draw2d 标签 Draw2D 里的 Label 不支持自动换行,虽然可以插入换行符手动换行。用 TextFlow 和适当的 Layout 可以实现文字的自动换行。以下代码由 sean 朋友贡献,原文链接。 class LabelEx extends FlowPage { private TextFlow contents; public LabelEx() { this(""); } public LabelEx(String text) { contents = new TextFlow(); contents.setLayoutManager(new ParagraphTextLayout(contents, ParagraphT extLayout.WORD_WRAP_SOFT)); contents.setText(text); add(contents); } public void setText(String text) { contents.setText(text); } public String getText() { return contents.getText(); } } GEF 常见问题 1:为图形编辑器设置背景 图片 GEF 的 RootEditPart 对应的 Figure 是一个由多个 Layer 组成的 LayeredPane,每个 Layer 负责包含不同类型的图形元素,如节点、连接、网格线等等。所以要让图形编辑器显示一个图片 作为背景,可以在它们其中一个层里绘制这个图片,也可以添加一个层专门放置背景图片。我推 荐使用后者,以下代码是在前面的 GefPractice 项目基础上做了简单修改得到的: static Image BG_IMAGE=new Image(null,"c:\\bg.jpg"); protected void configureGraphicalViewer() { super.configureGraphicalViewer(); getGraphicalViewer().setRootEditPart(new ScalableFreeformRootEditPart() { //覆盖 ScalableFreeformRootEditPart 的 createlayers 方法以便增加自己的层 protected void createLayers(LayeredPane layeredPane) { Layer layer = new FreeformLayer() { protected void paintFigure(Graphics graphics) { super.paintFigure(graphics); //在层上绘制图片,也可以绘制其他图形作为背景,GEF 的网格线就是一例 graphics.drawImage(BG_IMAGE,0,0); } }; layeredPane.add(layer); super.createLayers(layeredPane); } }); getGraphicalViewer().setEditPartFactory(new PartFactory()); } 这样得到的背景图片只显示编辑器可见区域的部分,也就是会随滚动条滚动,见下图。 具有背景图片的图形编辑器 GEF 常见问题 2:具有转折点的连接线 从直线连接转换到可以任意增减转折点的折线连接,因为模型里要增加新的元素,所以模型、 editpart 和图形部分都要有所修改,显得稍微有些烦琐,但其实很多代码是通用的。这个过程 主要分为以下几个部分: 1、在模型里增加转折点对应的类(这些转折点在 GEF 里称作 Bendpoint),在类里要具有两 个 Dimension 类型用来记录 Bendpoint 相对连接线起止点的位置。在连接类里要维护一个 Bendpoint 列表,并提供访问方法,由于篇幅关系这里只列出连接类中的这几个方法。 public void addBendpoint(int index, ConnectionBendpoint point) { getBendpoints().add(index, point); firePropertyChange(PROP_BENDPOINT, null, null); } /** * zhanghao: 为了在更新两个 dimension 后能发送事件,在 MoveBendpointCommand 要 在用这个方法设置新坐标, * 而不是直接用 BendPoint 里的方法。 */ public void setBendpointRelativeDimensions(int index, Dimension d1, Dimension d2){ ConnectionBendpoint cbp=(ConnectionBendpoint)getBendpoints().get(index); cbp.setRelativeDimensions(d1,d2); firePropertyChange(PROP_BENDPOINT, null, null); } public void removeBendpoint(int index) { getBendpoints().remove(index); firePropertyChange(PROP_BENDPOINT, null, null); } 2、在原来的连接方式里,由于连接线本身不需要刷新,所以现在要确保这个 editpart 实现了 PropertyChangeListener 接口,并像其他 editpart 一样覆盖了 activate()和 deactivate() 这两个方法,以便接收 Bendpoint 发生改变的事件。 public void activate() { super.activate(); ((Connection)getModel()).addPropertyChangeListener(this); } public void deactivate() { super.deactivate(); ((Connection)getModel()).removePropertyChangeListener(this); } public void propertyChange(PropertyChangeEvent event) { String property = event.getPropertyName(); if(Connection.PROP_BENDPOINT.equals(property)){ refreshBendpoints(); } } 为模型连接类对应的 editpart 里增加一个继承自 BendpointEditPolicy 的子类 ConnectionBendPointEditPolicy,这个类的内容后面会说到。 protected void createEditPolicies() { installEditPolicy(EditPolicy.CONNECTION_BENDPOINTS_ROLE, new Connection BendPointEditPolicy()); } 直线连接的情况下,连接的刷新不需要我们负责,但增加了 Bendpoint 以后,必须在 Bendpoint 发生改变时刷新连接线的显示。所以在上面这个 editpart 的 refreshVisuals()方法里需要增加 一些代码,以便把模型里的 Bendpoint 转换为图形上的 relativeBendpoint。 protected void refreshVisuals() { Connection conn = (Connection) getModel(); List modelConstraint = conn.getBendpoints(); List figureConstraint = new ArrayList(); for (int i = 0; i < modelConstraint.size(); i++) { ConnectionBendpoint cbp = (ConnectionBendpoint) modelConstraint .get(i); RelativeBendpoint rbp = new RelativeBendpoint(getConnectionFigure()); rbp.setRelativeDimensions(cbp.getFirstRelativeDimension(), cbp .getSecondRelativeDimension()); rbp.setWeight((i + 1) / ((float) modelConstraint.size() + 1)); figureConstraint.add(rbp); } getConnectionFigure().setRoutingConstraint(figureConstraint); } 3、创建 CreateBendpointCommand、MoveBendpointCommand 和 DeleteBendpointCommand 这三个类,可以像 Logic 例子那样创建一个基类 BendPointCommand 让它们来继承。作为例子,BendpointCommand 的内容如下。 public class BendpointCommand extends Command { protected int index; protected Connection connection; protected Dimension d1, d2; public void setConnection(Connection connection) { this.connection = connection; } public void redo() { execute(); } public void setRelativeDimensions(Dimension dim1, Dimension dim2) { d1 = dim1; d2 = dim2; } public void setIndex(int i) { index = i; } } 4、在 ConnectionBendPointEditPolicy 里实现 BendpointEditPolicy 定义的创建、移动和删 除 Bendpoint 的三个方法。 public class ConnectionBendPointEditPolicy extends BendpointEditPolicy { protected Command getCreateBendpointCommand(BendpointRequest request) { CreateBendpointCommand cmd = new CreateBendpointCommand(); Point p = request.getLocation(); Connection conn = getConnection(); conn.translateToRelative(p); Point ref1 = getConnection().getSourceAnchor().getReferencePoint(); Point ref2 = getConnection().getTargetAnchor().getReferencePoint(); conn.translateToRelative(ref1); conn.translateToRelative(ref2); cmd.setRelativeDimensions(p.getDifference(ref1), p.getDifference(ref2)); cmd.setConnection((com.example.model.Connection) request.getSource() .getModel()); cmd.setIndex(request.getIndex()); return cmd; } protected Command getDeleteBendpointCommand(BendpointRequest request) { BendpointCommand cmd = new DeleteBendpointCommand(); Point p = request.getLocation(); cmd.setConnection((com.example.model.Connection) request.getSource().g etModel()); cmd.setIndex(request.getIndex()); return cmd; } protected Command getMoveBendpointCommand(BendpointRequest request) { MoveBendpointCommand cmd = new MoveBendpointCommand(); Point p = request.getLocation(); Connection conn = getConnection(); conn.translateToRelative(p); Point ref1 = getConnection().getSourceAnchor().getReferencePoint(); Point ref2 = getConnection().getTargetAnchor().getReferencePoint(); conn.translateToRelative(ref1); conn.translateToRelative(ref2); cmd.setRelativeDimensions(p.getDifference(ref1), p.getDifference(ref2)); cmd.setConnection((com.example.model.Connection) request.getSource() .getModel()); cmd.setIndex(request.getIndex()); return cmd; } } 修改完成后的编辑器如下图所示。 编辑器中的转折连接线 点此下载工程,此工程修改自 GEF 应用实例中的 GefPractice,目标文件的扩展名改 为.gefpracticebp。 GEF 常见问题 3:自身连接 在类图里能看到一些对象具有对自己的引用,通常这些引用用于表达树状结构,即父子节点都是 同一类对象。用 GEF 绘制这样的连接线一般是通过转折点(Bendpoint)实现的,如果你的 GEF 应用程序里还不能使用 Bendpoint,请按照上一篇介绍的步骤添加对 Bendpoint 的支持。 原先我们的 GefPractice 应用程序是不允许一条连接线的起点和终点都是同一个图形的,因为 这样会导致连接线缩成一个点隐藏在图形下方,用户并不知道它的存在。当时我们在 CreateConnectionCommand 类的 canExecute()方法里进行了如下判断: public boolean canExecute() { if (source.equals(target)) return false; } 因此现在首先要把这两句删除。然后在 execute()方法里对自身连接的这种情况稍做处理,处 理的方法是给这条连接线在适当位置增加三个 Bendpoint,你也可以根据想要的连接线形状修 改 Bendpoint 的数目和位置。 public void execute() { connection = new Connection(source, target); if (source == target) { //The start and end points of our connection are both at the center of the re ctangle, //so the two relative dimensions are equal. ConnectionBendpoint cbp = new ConnectionBendpoint(); cbp.setRelativeDimensions(new Dimension(0, -60), new Dimension(0, -60)); connection.addBendpoint(0, cbp); ConnectionBendpoint cbp2 = new ConnectionBendpoint(); cbp2.setRelativeDimensions(new Dimension(100, -60), new Dimension(100, -60)); connection.addBendpoint(1, cbp2); ConnectionBendpoint cbp3 = new ConnectionBendpoint(); cbp3.setRelativeDimensions(new Dimension(100, 0), new Dimension(100, 0 )); connection.addBendpoint(2, cbp3); } } 现在用户只要选择连接工具,然后在一个节点上连续点两下就可以创建自身连接了,如下图所示。 自身连接 点此下载工程,此工程修改自 GEF 常见问题 2 中的 GefPractice-bp,目标文件扩展名 为.gefpracticesc。 GEF 常见问题 4:非矩形图元 现在假设要把原来 GefPractice 例子里的矩形图元节点换成用椭圆形表示,都需要做哪些改动 呢?很显然,首先要把原来继承 RectangleFigure 的 NodeFigure 类改为继承 Ellipse: public class NodeFigure extends Ellipse /*RectangleFigure*/{ } 这样修改后可以看到编辑器中的图元已经变成椭圆形了。但如果用户点选一个图元,表示选中的 边框(选择框)仍然是矩形的,如图 1 所示: 图 1 椭圆形的节点和矩形选择框 如果觉得矩形的选择框不太协调,可以通过覆盖 DiagramLayoutEditPolicy 的 createChildEditPolicy()方法修改。缺省情况下这个方法返回一个 ResizableEditPolicy,我 们要定义自己的子类(EllipseResizableEditPolicy)来替代它作为返回值。 EllipseResizableEditPolicy 里需要覆盖 ResizableEditPolicy 的两个方法, createSelectionHandles()方法决定“控制柄”(ResizeHandle)和 “选择框”(MoveHandle) 的相关情况,我们的实现如下: protected List createSelectionHandles() { List list = new ArrayList(); //添加选择框 //ResizableHandleKit.addMoveHandle((GraphicalEditPart) getHost(), list); list.add(new MoveHandle((GraphicalEditPart) getHost()) { protected void initialize() { super.initialize(); setBorder(new LineBorder(1) { public void paint(IFigure figure, Graphics graphics, Insets insets) { tempRect.setBounds(getPaintRectangle(figure, insets)); if (getWidth() % 2 == 1) { tempRect.width--; tempRect.height--; } tempRect.shrink(getWidth() / 2, getWidth() / 2); graphics.setLineWidth(getWidth()); if (getColor() != null) graphics.setForegroundColor(getColor()); //用椭圆形替代矩形 //graphics.drawRectangle(tempRect); graphics.drawOval(tempRect); } }); } }); //添加控制柄 ResizableHandleKit.addHandle((GraphicalEditPart) getHost(), list, PositionConst ants.EAST); ResizableHandleKit.addHandle((GraphicalEditPart) getHost(), list, PositionConst ants.SOUTH); ResizableHandleKit.addHandle((GraphicalEditPart) getHost(), list, PositionConst ants.WEST); ResizableHandleKit.addHandle((GraphicalEditPart) getHost(), list, PositionConst ants.NORTH); return list; } createDragSourceFeedbackFigure()方法决定用户拖动图形时,随鼠标移动的半透明图形 (即“鬼影”)的形状和颜色,因此我们覆盖这个方法以显示椭圆形的鬼影。 protected IFigure createDragSourceFeedbackFigure() { //用椭圆替代矩形 //RectangleFigure r = new RectangleFigure(); Ellipse r = new Ellipse(); FigureUtilities.makeGhostShape(r); r.setLineStyle(Graphics.LINE_DOT); r.setForegroundColor(ColorConstants.white); r.setBounds(getInitialFeedbackBounds()); addFeedback(r); return r; } 经过以上这些修改,可以看到选择框和鬼影都是椭圆的了,如图 2 所示。 图 2 与节点形状相同的选择框和鬼影 点此下载工程,此工程修改自 GEF 应用实例中的 GefPractice,目标文件的扩展名改 为.gefpracticeel。 GEF 常见问题 5:自动布局 利用自动布局功能,我们可以把本来不包含图形信息的文件以图形化的方式展示出来,典型的例 子比如将一组 Java 接口反向工程为类图,那么图中每个图元的坐标应该必须都是自动生成的。 GEF 里提供了 DirectedGraphLayout 类用来实现自动布局功能,下面介绍一下怎样在程序里 使用它。 DirectedGraphLayout 提供的 visit()方法接受一个 org.eclipse.draw2d.graph.DirectedGraph 实例,它遍历这个有向图的所有节点和边,并按 照它自己的算法计算出每个节点布局后的新位置。所以在使用它布局画布上的图元分为两个步骤: 1、构造有向图,2、将布局信息应用到图元。 还是以 gefpractice 为基础,我们在主工具条上增加了一个自动布局按钮,当用户按下它时自动 布局编辑器里的图形,再次按下时恢复以前的布局。为了完成步骤 1,我们要在 DiagramPart 里添加以下两个方法: /** * 将图元(NodePart)转换为节点(Node)到有向图 * @param graph * @param map */ public void contributeNodesToGraph(DirectedGraph graph, Map map) { for (int i = 0; i < getChildren().size(); i++) { NodePart node = (NodePart)getChildren().get(i); org.eclipse.draw2d.graph.Node n = new org.eclipse.draw2d.graph.Node(nod e); n.width = node.getFigure().getPreferredSize().width; n.height = node.getFigure().getPreferredSize().height; n.setPadding(new Insets(10,8,10,12)); map.put(node, n); graph.nodes.add(n); } } /** * 将连接(ConnectionPart)转换为边(Edge)添加到有向图 * @param graph * @param map */ public void contributeEdgesToGraph(DirectedGraph graph, Map map) { for (int i = 0; i < getChildren().size(); i++) { NodePart node = (NodePart)children.get(i); List outgoing = node.getSourceConnections(); for (int j = 0; j < outgoing.size(); j++) { ConnectionPart conn = (ConnectionPart)outgoing.get(j); Node source = (Node)map.get(conn.getSource()); Node target = (Node)map.get(conn.getTarget()); Edge e = new Edge(conn, source, target); e.weight = 2; graph.edges.add(e); map.put(conn, e); } } } 要实现步骤 2,在 DiagramPart 里添加下面这个方法: /** * 利用布局后的有向图里节点的位置信息重新定位画布上的图元 * @param graph * @param map */ protected void applyGraphResults(DirectedGraph graph, Map map) { for (int i = 0; i < getChildren().size(); i++) { NodePart node = (NodePart)getChildren().get(i); Node n = (Node)map.get(node); node.getFigure().setBounds(new Rectangle(n.x, n.y, n.width, n.height)); } } 为了以最少的代码说明问题,上面的方法里只是简单的移动了图形,而没有改变模型里 Node 的属性值,在大多情况下这里使用一个 CompoundCommand 对模型进行修改更为合适,这样 用户不仅可以撤消(Undo)这个自动布局操作,还可以在重新打开文件时看到关闭前的样子。 注意,DirectedGraphLayout 是不保证每次布局都得到完全相同的结果的。 因为 Draw2D 里是用 LayoutManager 管理布局的,而 DirectedGraphLayout 只是对布局算 法的一个包装,所以我们还要创建一个布局类。GraphLayoutManager 调用我们在上面已经添 加的那几个方法生成有向图(partsToNodes 变量维护了编辑器图元到有向图图元的映射), 利用 DirectedGraphLayout 对这个有向图布局,再把结果应用到编辑器里图元。如下所示: class GraphLayoutManager extends AbstractLayout { private DiagramPart diagram; GraphLayoutManager(DiagramPart diagram) { this.diagram = diagram; } protected Dimension calculatePreferredSize(IFigure container, int wHint, int hH int) { container.validate(); List children = container.getChildren(); Rectangle result = new Rectangle().setLocation(container.getClientArea().g etLocation()); for (int i = 0; i < children.size(); i++) result.union(((IFigure) children.get(i)).getBounds()); result.resize(container.getInsets().getWidth(), container.getInsets().getHeig ht()); return result.getSize(); } public void layout(IFigure container) { DirectedGraph graph = new DirectedGraph(); Map partsToNodes = new HashMap(); diagram.contributeNodesToGraph(graph, partsToNodes); diagram.contributeEdgesToGraph(graph, partsToNodes); new DirectedGraphLayout().visit(graph); diagram.applyGraphResults(graph, partsToNodes); } } 当用户按下自动布局按钮时,只要设置 DiagramPart 对应的图形的布局管理器为 GraphLayoutManager 就可以实现自动布局了,布局的结果如图所示。 自动布局的结果 点此下载工程,此工程修改自 GEF 应用实例中的 GefPractice,目标文件的扩展名改 为.gefpracticeal。最后有几点需要说明: 1、DirectedGraphLayout 只能对连通的有向图进行布局,否则会产生异常“graph is not fully connected”,参考 Eclipse.org 文章 Building a Database Schema Diagram Editor 中使 用的 NodeJoiningDirectedGraphLayout 可以解决这个问题; 2、如果要对具有嵌套关系的有向图自动布局,应使用 SubGraph 和 CompoundDirectedGraphLayout; 3、这个版本的 gefpractice 在自动布局后,没有对连接线进行处理,所以可能会出现连接线穿 过图元的情况,这个问题和上一个问题都可以参考 GEF 提供的 flow 例子解决。 Update(2007/4/9):如果 diagram 是放在 ScrollPane 里的,则要修改一下 applyGraphResults()方法,增加 container 作为参数以使整个 diagram 能正确滚动。 protected void applyGraphResults(DirectedGraph graph, Map map, IFigure contai ner) { for (Iterator iterator = this.nodeParts.iterator(); iterator.hasNext();) { NodePart element = (NodePart) iterator.next(); Node n = (Node) map.get(element); Rectangle containerBounds=container.getBounds(); Rectangle elementBounds=new Rectangle(n.x, n.y, n.width, n.height); element.getFigure().setBounds(elementBounds.translate(containerBounds.g etLocation())); } } GEF 常见问题 6:使用对话框 除了利用 Eclipse 提供的属性视图以外,GEF 应用程序里当然也可以通过弹出对话框修改模型 信息。 要实现双击一个节点打开对话框,在 NodePart 里要增加的代码如下: public void performRequest(Request req) { if(req.getType().equals(RequestConstants.REQ_OPEN)){ MessageDialog.openInformation(getViewer().getControl().getShell(),"Gef Pr actice","A Dialog"); } } 作为例子,上面这段代码只打开一个显示信息的对话框,你可以替换成自己实现的对话框显示/ 修改节点信息。 在 CreateNodeCommand 里增加下面的代码,可以在每次创建一个节点时通过对话框指定节 点的名称: public void execute() { InputDialog dlg = new InputDialog(shell, "Gef Practice", "New node's name:", " Node", null); if (Window.OK == dlg.open()) { this.node.setName(dlg.getValue()); } this.diagram.addNode(this.node); } 因为打开对话框时需要用到 Shell,所以要在 CreateNodeCommand 里增加一个 Shell 类型 的成员变量,并在 DiagramLayoutEditPolicy 里创建 CreateNodeCommand 时把一个 shell 实例传递给它。 创建节点时先弹出对话框 点此下载工程,此工程修改自 GEF 应用实例中的 GefPractice,目标文件的扩展名改 为.gefpracticedlg。 关于 Draw2D 里的 Layout 就像在 swt 里我们使用 layout 来控制各个控件的摆放位置一样,在 Draw2D 里最好也把这个 工作交给 LayoutManager 来做。除非是在自己实现的 Layout 里,一般程序里自己不要轻易 使用 setBounds()、setLocation()和 setSize()这些方法控制图形的位置和大小,而应该在为 每个图形设置了适当的 LayoutManager 后,通过 setConstraint()和 setPreferredSize()等 方法告诉 layoutmanager 如何布局。 在需要的时候,父图形的布局管理器负责修改每个子图形的位置和大小,但计算每个子图形大小 的工作可能是交给子图形自己的 LayoutManager 来做的,计算的方法一般是在这个 LayoutManager 的 getPreferredSize()方法里体现。 例如当父图形使用 XYLayout,子图形使用 ToolbarLayout 时,假设在子图形里又增加了子子 图形(子图形里的子图形),add()方法会导致 revalidate()的调用,这时父图形的 xylayout 将检查子图形是否具有 constraint,如果有并且有至少一个方向为-1,则利用子图形上的 ToolbarLayout 计算出子图形新的尺寸,这个尺寸是和子图形里包含的子子图形的数目有关的 (ToolbarLayout 会把每个子图形的宽/高度加起来,加上其中间隔的空间,再考虑图形的边框, 返回得到的尺寸)。 XYLayout 对 layout(IFigure)方法的实现: public void layout(IFigure parent) { Iterator children = parent.getChildren().iterator(); Point offset = getOrigin(parent); IFigure f; while (children.hasNext()) { f = (IFigure)children.next(); Rectangle bounds = (Rectangle)getConstraint(f);//因此必须为子图形指定 constraint if (bounds == null) continue; if (bounds.width == -1 || bounds.height == -1) { Dimension preferredSize = f.getPreferredSize(bounds.width, bounds.heig ht); bounds = bounds.getCopy(); if (bounds.width == -1) bounds.width = preferredSize.width; if (bounds.height == -1) bounds.height = preferredSize.height; } bounds = bounds.getTranslated(offset); f.setBounds(bounds); } } Draw2D 里 Figure 类的 setPreferredSize(Dimension)和 setSize(Dimension)的区别是, setSize()方法不会调用 revalidate()方法导致重新 layout,而只是调用 repaint()对所涉及到 的“脏”区域进行重绘(repaint)。setPreferredSize()方法可以约等于 setSize()方法 +revalidate()方法,因为在 Figure 对 getPreferredSize(int,int)的实现里,若该 figure 没 有任何 layoutmanager,则返回当前 size: public Dimension getPreferredSize(int wHint, int hHint) { if (prefSize != null) return prefSize; if (getLayoutManager() != null) { Dimension d = getLayoutManager().getPreferredSize(this, wHint, hHint); if (d != null) return d; } return getSize(); } 只要看一下 ToolbarLayout.java 就会知道,ToolbarLayout 对 constraint 是不感兴趣的, 调用它的 getConstraint()永远返回 null 值,所以我们不必对放在使用 ToolbarLayout 的图形 的子图形设置 constraint。因此,假如我们的问题是,有图形 A 包含 B,B 包含 C,要实现 B (使用 ToolbarLayout)尺寸随 C 数目多少而自动改变该如何做呢?这要看 A 使用何种 LayoutManager,如果是 ToolbarLayout 则不用做特殊的设置,如果是 XYLayout 则要用 A.getLayoutManager().setConstraint(B,new Rectangle(x,y,-1,-1))这样的语句为 A 设 置 constraint,对图形 C 则用 setPreferredSize()指定实际大小。 一个 Layout 的例子,点此下载,截图如下。 GEF 常见问题 7:计算字符串在画布上占 据的空间 要准确的计算文字在画布上占据的空间,可以利用 org.eclipse.swt.graphics.GC 的 stringExtent()方法实现,见下面的代码 GC gc = new GC(Display.getDefault()); gc.setFont(yourFont);//这一步不可缺少,因为有些字体里各字符的宽度是不同的 Point size = gc.stringExtent(text);//得到文字占据的尺寸 label.setPreferredSize(size.x + 16, size.y + 10);//让标签的尺寸比文字稍大 gc.dispose(); 运行时的效果: GEF 常见问题 8:导出到图片 利用 org.eclipse.draw2d.SWTGraphics 类和 org.eclipse.swt.graphics.ImageLoader 类 可以实现把画布导出到图片文件的功能,原理是在内存里创建一个空白的 Image,然后把 Diagram 画到它上面,最后保存到指定文件和格式。 我们可以把导出工作分为两部分,第一部分负责提供要导出的 IFigure 实例(若要导出整个画 布,应从 GraphicalViewer 获得 PRINTABLE_LAYERS,否则会丢失画布上的连线),并负责 将得到的图片数据写入文件;第二部分负责 IFigure 实例到图片数据的转换过程。以下是前者 的示例代码: String filename = ...; PracticeEditor editor = ...; ScalableFreeformRootEditPart rootPart = ...; IFigure figure = rootPart.getLayer(ScalableFreeformRootEditPart.PRINTABLE_LAY ERS);//To ensure every graphical element is included byte[] data = createImage(figure, SWT.IMAGE_PNG); try { FileOutputStream fos = new FileOutputStream(filename); fos.write(data); fos.close(); } catch (IOException e) { e.printStackTrace(); } 上面代码里调用的 createImage()方法是实际在内存里作画并转换为可写入为文件的二进制流 的地方,代码如下所示: private byte[] createImage(IFigure figure, int format) { Rectangle r = figure.getBounds(); ByteArrayOutputStream result = new ByteArrayOutputStream(); Image image = null; GC gc = null; Graphics g = null; try { image = new Image(null, r.width, r.height); gc = new GC(image); g = new SWTGraphics(gc); g.translate(r.x * -1, r.y * -1); figure.paint(g); ImageLoader imageLoader = new ImageLoader(); imageLoader.data = new ImageData[] { image.getImageData() }; imageLoader.save(result, format); } finally { if (g != null) { g.dispose(); } if (gc != null) { gc.dispose(); } if (image != null) { image.dispose(); } } return result.toByteArray(); } 点此下载工程,此工程修改自 GEF 应用实例中的 GefPractice,目标文件的扩展名 为.gefpractice 不变。 图 1 运行后增加了导出功能按钮 GEF 新闻组里相关链接: http://dev.eclipse.org/newslists/news.eclipse.tools.gef/msg05012.html http://dev.eclipse.org/newslists/news.eclipse.tools.gef/msg06329.html
还剩139页未读

继续阅读

下载pdf到电脑,查找使用更方便

pdf的实际排版效果,会与网站的显示效果略有不同!!

需要 6 金币 [ 分享pdf获得金币 ] 9 人已下载

下载pdf

pdf贡献者

love_aimyself

贡献于2013-05-16

下载需要 6 金币 [金币充值 ]
亲,您也可以通过 分享原创pdf 来获得金币奖励!
下载pdf