[Eclipse]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 utputs = 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 PropertyChangeListener, 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 NodeGraphicalNodeEditPolicy());   }   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(), rectangle);   }   //以下是NodeEditPart中抽象方法的实现   public ConnectionAnchor getSourceConnectionAnchor(ConnectionEditPart connection) {     return new ChopBoxAnchor (getFigure());   }   public ConnectionAnchor getSourceConnectionAnchor(Request request) {     return new ChopBoxAnchor (getFigure());   }   public ConnectionAnchor getTargetConnectionAnchor(ConnectionEditPart connection) {     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 ConnectionEndpointEditPolicy());   }   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(CreateConnectionRequest request) {     ConnectionCreateCommand command = (ConnectionCreateCommand) request.getStartCommand();     command.setTarget((Node) getHost().getModel());     return command;   }   protected Command getConnectionCreateCommand(CreateConnectionRequest request) {     ConnectionCreateCommand command = new ConnectionCreateCommand();     command.setSource((Node) getHost().getModel());     request.setStartCommand(command);     return command;   }   protected Command getReconnectSourceCommand(ReconnectRequest request) {     return null;   }   protected Command getReconnectTargetCommand(ReconnectRequest request) {     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以及多重选择这些 情况下;另外,这种方法只适用于容器里的子元素不是容器的情况,如果有多层的容器关系 ,则每一层都要做类似的处理才可以。

本文配套源码

享受每一刻的感觉,欣赏每一处的风景,这就是人生。

[Eclipse]GEF入门系列(七、XYLayout和展开/折叠功能)

相关文章:

你感兴趣的文章:

标签云: