[zt]GEF 进阶,第一部分: Anchor

阅读更多

FROM:http://www.ibm.com/developerworks/cn/opensource/os-ecl-gef/part1/index.html

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. 源锚点和目标锚点
 [zt]GEF 进阶,第一部分: Anchor_第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的表示方式

[zt]GEF 进阶,第一部分: Anchor_第2张图片 
 

我们可以用一个变量表示角度,从而计算出中心射线与边界的交点,把这个交点作为图形的锚点。通过这样的方式,边界上的任一点都可以成为锚点,可以通过手工调整锚点,避免连线重叠。


第二步,修改Model

为了表示锚点,我们需要一个表示角度的变量,这个变量应该放到模型中以便能够把锚点信息记录到文件中。对于一条来说,它有两个锚点,所以应该在连线对象中添加两个成员,在Shapes例子中,连线对象是org.eclipse.gef.examples.shapes.model.Connection, 我们修改它添加两个成员和相应的Getter和Setter方法:

java 代码
  1. private double sourceAngle;   
  2. private double targetAngle;   
  3.   
  4. public double getSourceAngle() {   
  5.  return sourceAngle;   
  6. }   
  7.   
  8. public void setSourceAngle(double sourceAngle) {   
  9.  this.sourceAngle = sourceAngle;   
  10. }   
  11.   
  12. public double getTargetAngle() {   
  13.  return targetAngle;   
  14. }   
  15.   
  16. public void setTargetAngle(double targetAngle) {   
  17.  this.targetAngle = targetAngle;   
  18. }   


sourceAngle保存了源锚点的角度,targetAngle保存了目标锚点的角度,使用弧度表示。

 

第三步,实现ConnectionAnchor接口

锚点的接口是由org.eclipse.draw2d.ConnectionAnchor定义的,我们需要实现这个接口,但是一般来说我们不用从头开始,可以通过继承其它类来减少我们的工作。由于存在椭圆和长方形两种图形,所以我们还需要实现两个子类。最终我们定义了基础类BorderAnchor和RectangleBorderAnchor,EllipseBorderAnchor两个子类。BorderAnchor的代码如下:

java 代码
  1. package org.eclipse.gef.examples.shapes.anchor;   
  2.   
  3. import org.eclipse.draw2d.ChopboxAnchor;   
  4. import org.eclipse.draw2d.IFigure;   
  5. import org.eclipse.draw2d.geometry.Point;   
  6.   
  7. public abstract class BorderAnchor extends ChopboxAnchor {   
  8.  protected double angle;   
  9.     
  10.  public BorderAnchor(IFigure figure) {   
  11.   super(figure);   
  12.   angle = Double.MAX_VALUE;   
  13.  }   
  14.     
  15.  public abstract Point getBorderPoint(Point reference);   
  16.     
  17.  public Point getLocation(Point reference) {   
  18.   // 如果angle没有被初始化,使用缺省的ChopboxAnchor,否则计算一个边界锚点   
  19.   if(angle == Double.MAX_VALUE)   
  20.    return super.getLocation(reference);   
  21.   else  
  22.    return getBorderPoint(reference);   
  23.  }   
  24.     
  25.  public double getAngle() {   
  26.   return angle;   
  27.  }   
  28.   
  29.  public void setAngle(double angle) {   
  30.   this.angle = angle;   
  31.  }   
  32. }   
  33.     
  34.   


重要的是getLocation()方法,它有一个参数"Point reference",即一个参考点,在计算锚点时,我们可以根据参考点来决定锚点的位置,对于ChopboxAnchor来说,参考点就是另外一个图形的中心点。BorderAnchor类有一个angle成员,保存了锚点的角度,它会被初始化为Double.MAX_VALUE,所以我们判断angle是否等于Double.MAX_VALUE,如果是则BorderAnchor相当于一个ChopboxAnchor,如果否则调用一个抽象方法getBorderPoint()来计算我们的锚点。BorderAnchor的两个子类分别实现了计算椭圆和长方形锚点的算法,EllipseBorderAnchor的代码如下所示: 

java 代码
  1. package org.eclipse.gef.examples.shapes.anchor;   
  2.   
  3. import org.eclipse.draw2d.IFigure;   
  4. import org.eclipse.draw2d.geometry.Point;   
  5. import org.eclipse.draw2d.geometry.PrecisionPoint;   
  6. import org.eclipse.draw2d.geometry.Rectangle;   
  7.   
  8. public class EllipseBorderAnchor extends BorderAnchor {   
  9.  public EllipseBorderAnchor(IFigure figure) {   
  10.   super(figure);   
  11.  }   
  12.   
  13.  @Override  
  14.  public Point getBorderPoint(Point reference) {   
  15.   //得到owner矩形,转换为绝对坐标   
  16.   Rectangle r = Rectangle.SINGLETON;   
  17.   r.setBounds(getOwner().getBounds());   
  18.   getOwner().translateToAbsolute(r);   
  19.      
  20.   // 椭圆方程和直线方程,解2元2次方程   
  21.   double a = r.width >> 1;   
  22.   double b = r.height >> 1;   
  23.   double k = Math.tan(angle);     
  24.   double dx = 0.0, dy = 0.0;   
  25.      
  26.   dx = Math.sqrt(1.0 / (1.0 / (a * a) + k * k / (b * b)));   
  27.   if(angle > Math.PI / 2 || angle < -Math.PI / 2)   
  28.    dx = -dx;   
  29.   dy = k * dx;   
  30.      
  31.   // 得到椭圆中心点,加上锚点偏移,得到最终锚点坐标   
  32.   PrecisionPoint pp = new PrecisionPoint(r.getCenter());    
  33.   pp.translate((int)dx, (int)dy);   
  34.   return new Point(pp);    
  35.  }   
  36. }   
  37.     


值的注意的地方是我们可以通过getOwner().getBounds()来得到Owner的边界矩形,这是我们能够计算出锚点的重要前提。此外我们要注意的是必须把坐标转换为绝对坐标,这是通过getOwner().translateToAbsolute(r)来实现的。最后,我们返回了锚点的绝对坐标,中间的具体计算过程只不过是根据椭圆方程和射线方程求值而已。在我们的实现中,并没有用到参考点,如果你想有更多的变数,可以把参考点考虑进去。

同样,RectangleBorderAnchor也是如此,只不过求长方形边界点的方法稍微不一样而已,我们就不一一解释了,代码如下:

 

java 代码
  1. package org.eclipse.gef.examples.shapes.anchor;   
  2.   
  3. import org.eclipse.draw2d.IFigure;   
  4. import org.eclipse.draw2d.geometry.Point;   
  5. import org.eclipse.draw2d.geometry.PrecisionPoint;   
  6. import org.eclipse.draw2d.geometry.Rectangle;   
  7.   
  8. public class RectangleBorderAnchor extends BorderAnchor {   
  9.   
  10.  public RectangleBorderAnchor(IFigure figure) {   
  11.   super(figure);   
  12.  }   
  13.   
  14.  @Override  
  15.  public Point getBorderPoint(Point reference) {   
  16.   // 得到owner矩形,转换为绝对坐标   
  17.   Rectangle r = Rectangle.SINGLETON;   
  18.   r.setBounds(getOwner().getBounds());   
  19.   getOwner().translateToAbsolute(r);   
  20.      
  21.   // 根据角度,计算锚点相对于owner中心点的偏移   
  22.   double dx = 0.0, dy = 0.0;   
  23.   double tan = Math.atan2(r.height, r.width);   
  24.   if(angle >= -tan && angle <= tan) {   
  25.    dx = r.width >> 1;   
  26.    dy = dx * Math.tan(angle);   
  27.   } else if(angle >= tan && angle <= Math.PI - tan) {   
  28.    dy = r.height >> 1;   
  29.    dx = dy / Math.tan(angle);   
  30.   } else if(angle <= -tan && angle >= tan - Math.PI) {   
  31.    dy = -(r.height >> 1);   
  32.    dx = dy / Math.tan(angle);   
  33.   } else {   
  34.    dx = -(r.width >> 1);   
  35.    dy = dx * Math.tan(angle);   
  36.   }   
  37.   
  38.   // 得到长方形中心点,加上偏移,得到最终锚点坐标   
  39.   PrecisionPoint pp = new PrecisionPoint(r.getCenter());    
  40.   pp.translate((int)dx, (int)dy);   
  41.   return new Point(pp);    
  42.  }   
  43. }   


这样我们就完成了自定义的锚点实现。在ConnectionAnchor接口中,还有其他4个方法,虽然我们没有用到,但是有必要了解一下它们:

java 代码
  1. void addAnchorListener(AnchorListener listener);   
  2. void removeAnchorListener(AnchorListener listener);   
  3. Point getReferencePoint();   
  4. IFigure getOwner();  


addAnchorListener()和removeAnchorListener()可以添加或删除一个锚点监听器,这样我们可以知道锚点何时发生了移动。getOwner()则是返回锚点的Onwer图形,显然我们可以指定另外一个图形为锚点的Owner,虽然这种需求可能不太多。而getReferencePoint()则是返回一个参考点,要注意的是,这个参考点不是给自己用的,而是给另外一个锚点用的。比如对于源锚点来说,它会调用目标锚点的getReferencePoint()方法,而对于目标锚点来说,它会调用源锚点的getReferencePoint()方法。我们可以看看ChopboxAnchor的getReferencePoint()实现,它返回的就是它的Owner的中心。

 
第四步,修改EditPart

锚点实现完成后,我们需要修改ShapeEditPart使它能够使用我们定义的锚点。EditPart中的getSourceConnectionAnchor(ConnectionEditPart connection)和getTargetConnectionAnchor(ConnectionEditPart connection)是决定使用哪种锚点的关键方法。它们还有一个重载版本,用来处理Reconnect时的锚点更新。这四个方法我们都需要修改,同时为了减少对象创建的次数,我们可以在ConnectionEditPart里面添加两个成员用来保存源锚点对象和目标锚点对象,如下:

java 代码
  1. /* In ConnectionEditPart.java */  
  2.   
  3. private BorderAnchor sourceAnchor;   
  4. private BorderAnchor targetAnchor;   
  5.   
  6. public BorderAnchor getSourceAnchor() {   
  7.  return sourceAnchor;   
  8. }   
  9.   
  10. public void setSourceAnchor(BorderAnchor sourceAnchor) {   
  11.  this.sourceAnchor = sourceAnchor;   
  12. }   
  13.   
  14. public BorderAnchor getTargetAnchor() {   
  15.  return targetAnchor;   
  16. }   
  17.   
  18. public void setTargetAnchor(BorderAnchor targetAnchor) {   
  19.  this.targetAnchor = targetAnchor;   
  20. }   

这样的话,在ShapeEditPart中应该检查一下ConnectionEditPart中的成员是否有效,如果有效则直接返回,无效则创建一个新的锚点对象。而Reconnect时的代码稍微复杂一些,我们需要根据鼠标的当前位置,重新计算angle的值,鼠标的当前位置是包含在ReconnectRequest里面的。我们给出getSourceConnectionAnchor()的代码,对于getTargetConnectionAnchor(),只要将Source换成Target即可。

 

java 代码
  1. /* In ShapeEditPart.java */  
  2.   
  3. /*  
  4.  * (non-Javadoc)  
  5.  * @see org.eclipse.gef.NodeEditPart#getSourceConnectionAnchor  
  6.  (org.eclipse.gef.ConnectionEditPart)  
  7.  */  
  8. public ConnectionAnchor getSourceConnectionAnchor   
  9. (ConnectionEditPart connection) {   
  10.  org.eclipse.gef.examples.shapes.parts.ConnectionEditPart con =   
  11.  (org.eclipse.gef.examples.shapes.parts.ConnectionEditPart)connection;   
  12.  BorderAnchor anchor = con.getSourceAnchor();   
  13.  if(anchor == null || anchor.getOwner() != getFigure()) {   
  14.   if(getModel() instanceof EllipticalShape)   
  15.    anchor = new EllipseBorderAnchor(getFigure());   
  16.   else if(getModel() instanceof RectangularShape)   
  17.    anchor = new RectangleBorderAnchor(getFigure());   
  18.   else  
  19.    throw new IllegalArgumentException("unexpected model");   
  20.      
  21.   Connection conModel = (Connection)con.getModel();   
  22.   anchor.setAngle(conModel.getSourceAngle());   
  23.   con.setSourceAnchor(anchor);   
  24.  }   
  25.  return anchor;   
  26. }   
  27.   
  28. /*  
  29.  * (non-Javadoc)  
  30.  * @see org.eclipse.gef.NodeEditPart#getSourceConnectionAnchor  
  31.  (org.eclipse.gef.Request)  
  32.  */  
  33. public ConnectionAnchor getSourceConnectionAnchor(Request request) {   
  34.  if(request instanceof ReconnectRequest) {   
  35.   ReconnectRequest r = (ReconnectRequest)request;   
  36.   org.eclipse.gef.examples.shapes.parts.ConnectionEditPart con =   
  37.   (org.eclipse.gef.examples.shapes.parts.ConnectionEditPart)r.   
  38.   getConnectionEditPart();   
  39.   Connection conModel = (Connection)con.getModel();   
  40.   BorderAnchor anchor = con.getSourceAnchor();   
  41.   GraphicalEditPart part = (GraphicalEditPart)r.getTarget();   
  42.   if(anchor == null || anchor.getOwner() != part.getFigure()) {   
  43.    if(getModel() instanceof EllipticalShape)   
  44.     anchor = new EllipseBorderAnchor(getFigure());   
  45.    else if(getModel() instanceof RectangleBorderAnchor)   
  46.     anchor = new RectangleBorderAnchor(getFigure());   
  47.    else  
  48.     throw new IllegalArgumentException("unexpected model");   
  49.       
  50.    anchor.setAngle(conModel.getSourceAngle());   
  51.    con.setSourceAnchor(anchor);   
  52.   }   
  53.      
  54.   Point loc = r.getLocation();   
  55.   Rectangle rect = Rectangle.SINGLETON;   
  56.   rect.setBounds(getFigure().getBounds());   
  57.   getFigure().translateToAbsolute(rect);   
  58.   Point ref = rect.getCenter();   
  59.   double dx = loc.x - ref.x;   
  60.   double dy = loc.y - ref.y;   
  61.   anchor.setAngle(Math.atan2(dy, dx));    
  62.   conModel.setSourceAngle(anchor.getAngle());   
  63.   return anchor;     
  64.  } else {   
  65.   if(getModel() instanceof EllipticalShape)   
  66.    return new EllipseBorderAnchor(getFigure());   
  67.   else if(getModel() instanceof RectangularShape)   
  68.    return new RectangleBorderAnchor(getFigure());   
  69.   else  
  70.    throw new IllegalArgumentException("unexpected model");   
  71.  }   
  72. }   


到这里我们的修改就完成了,但是由于Shapes示例不允许创建多条连线,所以我们还需要把ConnectionCreateCommand和ConnectionReconnectCommand中的一些代码注释掉,这个内容就不做更多介绍了,大家可以下载本文附带的代码查看具体的修改。最终,我们修改后的Shapes可以创建多条连线,并且可以手动调整它们的锚点以避免重叠,如图3所示:


图3. 新的Shapes示例
 [zt]GEF 进阶,第一部分: Anchor_第3张图片 


结束语

一个灵活的锚点实现对于复杂的图形编辑程序来说是必须的,我们所要做的仅仅只是实现ConnectionAnchor接口。本文实现的BorderAnchor是一个通用的锚点实现,你可以随意应用到自己的GEF程序中。或者在此基础上实现更为灵活的锚点功能。

下载

名字 大小 下载方法
org.eclipse.gef.examples.shapes_anchor.zip   HTTP

参考资料

  • 有关GEF的项目信息,请访问 GEF Project Home

  • 了解GEF的基础知识,请阅读developerWorks文章: 使用图形编辑框架创建基于 Eclipse 的应用程序



关于作者

马若劼,IBM 公司软件工程师,主要从事 Workplace Forms 的设计与开发。他在 Java,Eclipse 以及 Eclipse 插件技术方面拥有多年经验,同时也是开源项目 LumaQQ 的创立者。


你可能感兴趣的:(Eclipse,OpenSource,设计模式,IBM,框架)