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

简介: 前面的帖子里曾说过如何使用布局,当时主要集中在ToolbarLayout和FlowLayout(统称OrderedLayout),还有很多应用程序使用的是可以自由拖动子图形的布局,在GEF里称为XYLayout,而且这样的应用多半会需要在图形之间建立一些连接线,比如下图所示的情景。

前面的帖子里曾说过如何使用布局,当时主要集中在ToolbarLayout和FlowLayout(统称OrderedLayout),还有很多应用程序使用的是可以自由拖动子图形的布局,在GEF里称为XYLayout,而且这样的应用多半会需要在图形之间建立一些连接线,比如下图所示的情景。连接的出现在一定程度上增加了模型的复杂度,连接线的刷新也是GEF关注的一个问题,这里就主要讨论这类应用的实现,并将特别讨论一下展开/折叠(expand/collapse)功能的实现。请点这里下载本篇示例代码。

xylayout.gif
图1 使用XYLayout的应用程序

还是从模型开始说起,使用XYLayout时,每个子图形对应的模型要维护自身的坐标和尺寸信息,这就在模型里引入了一些与实际业务无关的成员变量。为了解决这个问题,一般我们是让所有需要具有这些界面信息的模型元素继承自一个抽象类(如Node),而这个类里提供如point、dimension等变量和getter/setter方法:

None.gif public class Node extends Element implements IPropertySource {
None.gif    protected Point location 
=   new  Point( 0 0 ); // 位置
None.gif
    protected Dimension size  =   new  Dimension( 100 150 ); // 尺寸
None.gif
    protected String name  =   " Node " ; // 标签
None.gif
    protected List outputs  =   new  ArrayList( 5 ); // 节点作为起点的连接
None.gif    
protected List inputs  =   new  ArrayList( 5 ); // 节点作为终点的连接
None.gif

None.gif}

EditPart方面也是一样的,如果你的应用程序里有多个需要自由拖动和改变大小的EditPart,那么最好提供一个抽象的EditPart(如NodePart),在这个类里实现propertyChange()、createEditPolicy()、active()、deactive()和refreshVisuals()等常用方法的缺省实现,如果子类需要扩展某个方法,只要先调用super()再写自己的扩展代码即可,典型的NodePart代码如下所示,注意它是NodeEditPart的子类,后者是GEF专为具有连接功能的节点提供的EditPart:

None.gif public abstract class NodePart extends AbstractGraphicalEditPart implements PropertyChangeListener, NodeEditPart {
None.gif    public 
void  propertyChange(PropertyChangeEvent evt) {
None.gif        
if  (evt.getPropertyName().equals(Node.PROP_LOCATION))
None.gif            refreshVisuals();
None.gif        
else   if  (evt.getPropertyName().equals(Node.PROP_SIZE))
None.gif            refreshVisuals();
None.gif        
else   if  (evt.getPropertyName().equals(Node.PROP_INPUTS))
None.gif            refreshTargetConnections();
None.gif        
else   if  (evt.getPropertyName().equals(Node.PROP_OUTPUTS))
None.gif            refreshSourceConnections();
None.gif    }

None.gif    protected 
void  createEditPolicies() {
None.gif        installEditPolicy(EditPolicy.COMPONENT_ROLE, 
new  NodeEditPolicy());
None.gif        installEditPolicy(EditPolicy.GRAPHICAL_NODE_ROLE, 
new  NodeGraphicalNodeEditPolicy());
None.gif    }

None.gif    public 
void  activate() {…}
None.gif    public 
void  deactivate() {…}

None.gif    protected 
void  refreshVisuals() {
None.gif        Node node 
=  (Node) getModel();
None.gif        Point loc 
=  node.getLocation();
None.gif        Dimension size 
=   new  Dimension(node.getSize());
None.gif        Rectangle rectangle 
=   new  Rectangle(loc, size);
None.gif        ((GraphicalEditPart) getParent()).setLayoutConstraint(
this , getFigure(), rectangle);
None.gif    }

None.gif    
// 以下是NodeEditPart中抽象方法的实现
None.gif
    public ConnectionAnchor getSourceConnectionAnchor(ConnectionEditPart connection) {
None.gif        
return   new  ChopBoxAnchor (getFigure());
None.gif    }
None.gif    public ConnectionAnchor getSourceConnectionAnchor(Request request) {
None.gif        
return   new  ChopBoxAnchor (getFigure());
None.gif    }
None.gif    public ConnectionAnchor getTargetConnectionAnchor(ConnectionEditPart connection) {
None.gif        
return   new  ChopBoxAnchor (getFigure());
None.gif    }
None.gif    public ConnectionAnchor getTargetConnectionAnchor(Request request) {
None.gif        
return   new  ChopBoxAnchor(getFigure());
None.gif    }
None.gif    protected List getModelSourceConnections() {
None.gif        
return  ((Node)  this .getModel()).getOutgoingConnections();
None.gif    }
None.gif    protected List getModelTargetConnections() {
None.gif        
return  ((Node)  this .getModel()).getIncomingConnections();
None.gif    }
None.gif}

从代码里可以看到,NodePart已经通过安装两个EditPolicy实现关于图形删除、移动和改变尺寸的功能,所以具体的NodePart只要继承这个类就自动拥有了这些功能,当然模型得是Node的子类才可以。在GEF应用程序里我们应该善于利用继承的方式来简化开发工作。代码后半部分中的几个getXXXAnchor()方法是用来规定连接线锚点(Anchor)的,这里我们使用了在Draw2D那篇帖子里介绍过的ChopBoxAnchor作为锚点,它是Draw2D自带的。而代码最后两个方法的返回值则规定了以这个EditPart为起点和终点的连接列表,列表中每一个元素都应该是Connection类型,这个类是模型的一部分,接下来就要说到。

在GEF里,节点间的连接线也需要有自己的模型和对应的EditPart,所以这里我们需要定义Connection和ConnectionPart这两个类,前者和其他模型元素没有什么区别,它维护source和target两个节点变量,代表连接的起点和终点;ConnectionPart继承于GEF的AbstractConnectionPart类,请看下面的代码:

None.gif public class ConnectionPart extends AbstractConnectionEditPart {
None.gif    protected IFigure createFigure() {
None.gif        PolylineConnection conn 
=   new  PolylineConnection();
None.gif        conn.setTargetDecoration(
new  PolygonDecoration());
None.gif        conn.setConnectionRouter(
new  BendpointConnectionRouter());
None.gif        
return  conn;
None.gif    }

None.gif    protected 
void  createEditPolicies() {
None.gif        installEditPolicy(EditPolicy.COMPONENT_ROLE, 
new  ConnectionEditPolicy());
None.gif        installEditPolicy(EditPolicy.CONNECTION_ENDPOINTS_ROLE, 
new  ConnectionEndpointEditPolicy());
None.gif    }

None.gif    protected 
void  refreshVisuals() {
None.gif    }

None.gif    public 
void  setSelected( int  value) {
None.gif        super.setSelected(value);
None.gif        
if  (value  !=  EditPart.SELECTED_NONE)
None.gif            ((PolylineConnection) getFigure()).setLineWidth(
2 );
None.gif        
else
None.gif            ((PolylineConnection) getFigure()).setLineWidth(
1 );
None.gif    }
None.gif}

在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()方法,提供了用于实现各种编辑目的的命令:

None.gif public class NodeGraphicalNodeEditPolicy extends GraphicalNodeEditPolicy {
None.gif    protected Command getConnectionCompleteCommand(CreateConnectionRequest request) {
None.gif        ConnectionCreateCommand command 
=  (ConnectionCreateCommand) request.getStartCommand();
None.gif        command.setTarget((Node) getHost().getModel());
None.gif        
return  command;
None.gif    }

None.gif    protected Command getConnectionCreateCommand(CreateConnectionRequest request) {
None.gif        ConnectionCreateCommand command 
=   new  ConnectionCreateCommand();
None.gif        command.setSource((Node) getHost().getModel());
None.gif        request.setStartCommand(command);
None.gif        
return  command;
None.gif    }

None.gif    protected Command getReconnectSourceCommand(ReconnectRequest request) {
None.gif        
return   null ;
None.gif    }

None.gif    protected Command getReconnectTargetCommand(ReconnectRequest request) {
None.gif        
return   null ;
None.gif    }
None.gif}

因为是针对节点的,所以这里面都是和连接线有关的方法,因为只有节点才需要连接线。这些方法名称的意义都很明显:getConnectionCreateCommand()是当用户选择了连接线工具并点中一个节点时调用,getConnectionCompleteCommand()是在用户选择了连接终点时调用,getReconnectSourceCommand()和getReconnectTargetCommand()则分别是在用户拖动一个连接线的起点/终点到其他节点上时调用,这里我们返回null表示不提供改变连接端点的功能。关于命令(Command)本身,我想没有必要做详细说明了,基本上只要搞清了模型之间的关系,命令就很容易写出来,请下载例子后自己查看。

下面应郭奕朋友的要求说一说如何实现容器(Container)的折叠/展开功能。在有些应用里,画布中的图形还能够包含子图形,这种图形称为容器(画布本身当然也是容器),为了让画布看起来更简洁,可以让容器具有"折叠"和"展开"两种状态,当折叠时只显示部分信息,不显示子图形,展开时则显示完整的容器和子图形,见图2和图3,本例中各模型元素的包含关系是Diagram->Subject->Attribute。

expand.gif
图2 容器Subject3处于展开状态

要为Subject增加展开/折叠功能主要存在两个问题需要考虑:一是如何隐藏容器里的子图形,并改变容器的外观,我采取的方法是在需要折叠/展开的时候改变容器图形,将contentPane也就是包含子图形的那个图形隐藏起来,从而达到隐藏子图形的目的;二是与容器包含的子图形相连的连接线的处理,因为子图形有可能与其他容器或容器中的子图形之间存在连接线,例如图2中Attribute4与Attribute6之间的连接线,这些连接线在折叠状态下应该连接到子图形所在容器上才符合逻辑(例如在Subject3折叠后,原来从Attribute4到Attribute6的连接应该变成从Subject3到Atribute6的连接,见图3)。

collapse.gif
图3 容器Subject3处于折叠状态

现在一个一个来解决。首先,不论容器处于什么状态,都应该只是视图上的变化,而不是模型中的变化(例如折叠后的容器中没有显示子图形不代表模型中的容器不包含子图形),但在容器模型中要有一个表示状态的布尔型变量collapsed(初始值为false),用来指示EditPart刷新视图。假设我们希望用户双击一个容器可以改变它的展开/折叠状态,那么在容器的EditPart(例子里的SubjectPart)里要覆盖performRequest()方法改变容器的状态值:

None.gif public  void  performRequest(Request req) {
None.gif    
if  (req.getType()  ==  RequestConstants.REQ_OPEN)
None.gif        getSubject().setCollapsed(
! getSubject().isCollapsed());
None.gif}

注意这个状态值的改变是会触发所有监听器的propertyChange()方法的,而SubjectPart正是这样一个监听器,所以在它的propertyChange()方法里要增加对这个新属性变化事件的处理代码,判断当前状态隐藏或显示contantPane:

None.gif public  void  propertyChange(PropertyChangeEvent evt) {
None.gif    
if  (Subject.PROP_COLLAPSED.equals(evt.getPropertyName())) {
None.gif        SubjectFigure figure 
=  ((SubjectFigure) getFigure());
None.gif        
if  ( ! getSubject().isCollapsed()) {
None.gif            figure.add(getContentPane());
None.gif        } 
else  {
None.gif            figure.remove(getContentPane());
None.gif        }
None.gif        refreshVisuals();
None.gif        refreshSourceConnections();
None.gif        refreshTargetConnections();
None.gif    }
None.gif    
if  (Subject.PROP_STRUCTURE.equals(evt.getPropertyName()))
None.gif        refreshChildren();
None.gif    super.propertyChange(evt);
None.gif}

为了让容器显示不同的图标以反应折叠状态,在SubjectPart的refreshVisuals()方法里要做额外的工作,如下所示:

None.gif protected  void  refreshVisuals() {
None.gif    super.refreshVisuals();
None.gif    SubjectFigure figure 
=  (SubjectFigure) getFigure();
None.gif    figure.setName(((Node) 
this .getModel()).getName());
None.gif    
if  ( ! getSubject().isCollapsed()) {
None.gif        figure.setIcon(SubjectPlugin.getImage(IConstants.IMG_FILE));
None.gif    } 
else  {
None.gif        figure.setIcon(SubjectPlugin.getImage(IConstants.IMG_FOLDER));
None.gif    }
None.gif}

因为折叠后的容器图形应该变小,所以我让Subject对象覆盖了Node对象的getSize()方法,在折叠状态时返回一个固定的Dimension对象,该值就决定了Subject折叠状态的图形尺寸,如下所示:

None.gif protected Dimension collapsedDimension  =   new  Dimension( 80 50 );
None.gifpublic Dimension getSize() {
None.gif    
if  ( ! isCollapsed())
None.gif        
return  super.getSize();
None.gif    
else
None.gif        
return  collapsedDimension;
None.gif}

上面的几段代码更改解决了第一个问题,第二个问题要稍微麻烦一些。为了在不同状态下返回正确的连接,我们要修改getModelSourceConnections()方法和getModelTargetConnections()方法,前面已经说过,这两个方法的作用是返回与节点相关的连接对象列表,我们要做的就是让它们根据节点的当前状态返回正确的连接,所以作为容器的SubjectPart要做这样的修改:

None.gif protected List getModelSourceConnections() {
None.gif    
if  ( ! getSubject().isCollapsed()) {
None.gif        
return  getSubject().getOutgoingConnections();
None.gif    } 
else  {
None.gif        List l 
=   new  ArrayList();
None.gif        l.addAll(getSubject().getOutgoingConnections());
None.gif        
for  (Iterator iter  =  getSubject().getAttributes().iterator(); iter.hasNext();) {
None.gif            Attribute attribute 
=  (Attribute) iter.next();
None.gif            l.addAll(attribute.getOutgoingConnections());
None.gif        }
None.gif        
return  l;
None.gif    }
None.gif}

也就是说,当处于展开状态时,正常返回自己作为起点的那些连接;否则除了这些连接以外,还要包括子图形对应的那些连接。作为子图形的AttributePart也要修改,因为当所在容器折叠后,它们对应的连接也要隐藏,修改后的代码如下所示:

None.gif protected List getModelSourceConnections() {
None.gif    Attribute attribute 
=  (Attribute) getModel();
None.gif    Subject subject 
=  (Subject) ((SubjectPart) getParent()).getModel();
None.gif    
if  ( ! subject.isCollapsed()) {
None.gif        
return  attribute.getOutgoingConnections();
None.gif    } 
else  {
None.gif        
return  Collections.EMPTY_LIST;
None.gif    }
None.gif}

由于getModelTargetConnections()的代码和getModelSourceConnections()非常类似,这里就不列出其内容了。在一般情况下,我们只让一个EditPart监听一个模型的变化,但是请记住,GEF框架并没有规定EditPart与被监听的模型一一对应(实际上GEF中的很多设计就是为了减少对开发人员的限制),因此在必要时我们大可以根据自己的需要灵活运用。在实现展开/折叠功能时,子元素的EditPart应该能够监听所在容器的状态变化,当collapsed值改变时更新与子图形相关的连接线(若不进行更新则这些连接线会变成"无头线")。让子元素EditPart监听容器模型的变化很简单,只要在AttributePart的activate()里把自己作为监听器加到容器模型的监听器列表即可,注意别忘记在deactivate()里注销掉,而propertyChange()方法里是事件发生时的处理,代码如下:

None.gif public  void  activate() {
None.gif    super.activate();
None.gif    ((Attribute) getModel()).addPropertyChangeListener(
this );
None.gif    ((Subject) getParent().getModel()).addPropertyChangeListener(
this );
None.gif}
None.gifpublic 
void  deactivate() {
None.gif    super.deactivate();
None.gif    ((Attribute) getModel()).removePropertyChangeListener(
this );
None.gif    ((Subject) getParent().getModel()).removePropertyChangeListener(
this );
None.gif}
None.gifpublic 
void  propertyChange(PropertyChangeEvent evt) {
None.gif    
if  (evt.getPropertyName().equals(Subject.PROP_COLLAPSED)) {
None.gif        refreshSourceConnections();
None.gif        refreshTargetConnections();
None.gif    }
None.gif    super.propertyChange(evt);
None.gif}

这样,基本上就实现了容器的展开/折叠功能,之所以说"基本上",是因为我没有做仔细的测试(时间关系),目前的代码有可能会存在问题,特别是在Undo/Redo以及多重选择这些情况下;另外,这种方法只适用于容器里的子元素不是容器的情况,如果有多层的容器关系,则每一层都要做类似的处理才可以。

本文转自博客园八进制的博客,原文链接:[Eclipse]GEF入门系列(七、XYLayout和展开/折叠功能),如需转载请自行联系原博主。

相关文章
|
2月前
|
前端开发 Java Maven
Eclipse里使用Servlet实现简单的登录功能
Maven是一款非常方便的Java开发插件,它可以自动管理好开发过程中需要的jar包,提升开发者们的开发效率。在这里,我手把手教给大家如何新建一个Maven项目,并实现简单的用户登录功能。
112 0
|
9月前
|
Android开发
10activiti - 入门demo(Eclipse)
10activiti - 入门demo(Eclipse)
32 0
10activiti - 入门demo(Eclipse)
|
10月前
|
Java Android开发 Windows
Eclipse代码自动补全功能
Eclipse代码自动补全功能
|
11月前
|
数据可视化 Java API
【Eclipse设计】Excel表格的读写功能
【Eclipse设计】Excel表格的读写功能
127 0
|
Go Android开发
开心档-软件开发入门之Eclipse 添加书签
Eclipse 中可以在编辑器的任意一行添加书签。 您可以使用书签作为提示信息,或者使用书签快速定位到文件中的指定的行。
|
Java 应用服务中间件 Apache
Java Web入门之开发环境的搭建(包括下载Tomcat和 Eclipse for Java EE)
Java Web入门之开发环境的搭建(包括下载Tomcat和 Eclipse for Java EE)
208 0
Java Web入门之开发环境的搭建(包括下载Tomcat和 Eclipse for Java EE)
|
Java Android开发
STS|Eclipse修改代码补全功能
STS|Eclipse修改代码补全功能
189 0
STS|Eclipse修改代码补全功能
|
Java Android开发 C++
【玩转Eclipse】——eclipse实现代码块折叠-类似于VS中的#region……#endregion
 刚才在写代码的时候,写了十几行可以说是重复的代码:
【玩转Eclipse】——eclipse实现代码块折叠-类似于VS中的#region……#endregion
|
XML Java API
Spring框架入门以及Eclipse创建 Spring项目
Spring框架入门以及Eclipse创建 Spring项目
Spring框架入门以及Eclipse创建 Spring项目
|
XML Java Android开发
SpringBoot入门:使用IDEA和Eclipse构建第一个SpringBoot项目
Spring Boot是一个简化Spring开发的框架,用来监护spring应用开发,约定大于配置,去繁就简,just run 就能创建一个独立的,产品级的应用。我们在使用Spring Boot时只需要配置相应的Spring Boot就可以用所有的Spring组件,简单的说,spring boot就是整合了很多优秀的框架,不用我们自己手动的去写一堆xml配置然后进行配置。所以springboot在java开发中也变得越来越重要,这里总结一下如何使用IDEA和Eclipse创建一个简单的springboot项目。
408 0
SpringBoot入门:使用IDEA和Eclipse构建第一个SpringBoot项目