[Eclipse]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网站)

file
图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。

搬家前链接:https://www.cnblogs.com/bjzhanghao/archive/2005/04/15/138447.html

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

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

file
图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 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。

file
图2 容器Subject3处于展开状态

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

file
图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以及多重选择这些情况下;另外,这种方法只适用于容器里的子元素不是容器的情况,如果有多层的容器关系,则每一层都要做类似的处理才可以。

搬家前链接:https://www.cnblogs.com/bjzhanghao/archive/2005/04/11/135546.html

[Eclipse]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应用程序大多数是基于编辑器的。

file
图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_HIGH));
        addRetargetAction(new PriorityRetargetAction(IConstants.PRIORITY_MEDIUM));
        addRetargetAction(new PriorityRetargetAction(IConstants.PRIORITY_LOW));
    }
    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()里则是添加到主工具条的代码。

file
图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(GEFMessages.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组里,当然如果你的菜单项很多,也可以定义新的组用来分类。

file
图3 上下文菜单

假设我们要实现如上图所示的上下文菜单,并且已经创建并在ActionRegistry里了这些Action(在Editor的createActions()方法里完成),ContextMenuProvider应该像下面这样写:

public class CbmEditorContextMenuProvider extends ContextMenuProvider {
    private ActionRegistry actionRegistry;
    public CbmEditorContextMenuProvider(EditPartViewer viewer, ActionRegistry 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(ActionFactory.UNDO.getId()));
        menu.appendToGroup(GEFActionConstants.GROUP_UNDO, getAction(ActionFactory.REDO.getId()));
        menu.appendToGroup(GEFActionConstants.GROUP_EDIT, getAction(ActionFactory.DELETE.getId()));
        menu.appendToGroup(GEFActionConstants.GROUP_REST,getAction(IConstants.ACTION_MARK_PRIORITY_HIGH)); 
        menu.appendToGroup(GEFActionConstants.GROUP_REST,getAction(IConstants.ACTION_MARK_PRIORITY_MEDIUM)); 
        menu.appendToGroup(GEFActionConstants.GROUP_REST,getAction(IConstants.ACTION_MARK_PRIORITY_LOW));
    }
    private IAction getAction(String actionId) {
        return actionRegistry.getAction(actionId);
    }
}

注意buildContextMenu()方法里的第一句是创建缺省的那些组,如果没有忽略了这一步后面的语句会提示组不存在的错误,你也可以通过这个方法看到GEF是怎样建组的以及都有哪些组。让编辑器使用这个类的代码一般写在configureGraphicalViewer()方法里。

因为顺便介绍了Eclipse的一些基本概念,加上代码比较多,所以这篇贴子看起来比较长,其实通过查看GEF对内置的UNDO/REDO等的实现很容易就会明白菜单的使用方法。

搬家前链接:https://www.cnblogs.com/bjzhanghao/archive/2005/03/30/128704.html

[GEF]在非XYLayout布局的container里调整children尺寸

以前只做过两种类型的GEF程序,一种是画布使用XYLayout,子图形可以在上面随意改变大小和位置;另一种是画布使用非XYLayout的布局,子图形的大小和位置由布局决定,用户不能用鼠标拖动的方式改变。现在这个项目有点特殊,因为要实现类似“表格”的功能,所以要求画布使用ToolbarLayout排列表格列,但列的高度要能够改变。我查看了FlowLayoutEditPolicy类,里面没有一个类似createChangeConstraintCommand这样的方法,那么该怎样实现这个功能呢?

file
图1 画布使用ToolbarLayout同时可以调整列的高度

经过黄老大的指点和查看代码,原来要在画布的EditPolicy里覆盖createChildEditPolicy()方法,FlowLayoutEditPolicy缺省是返回一个NonResizableEditPolicy,我们要改为返回一个ResizableEditPolicy,为了只让用户能拖动列的底部,还要稍微设置一下这个EditPolicy,如下所示:

protected EditPolicy createChildEditPolicy(EditPart child) {
    ResizableEditPolicy policy = new ResizableEditPolicy();
    policy.setResizeDirections(PositionConstants.SOUTH);
    return policy;
}

在运行时,这个被返回的EditPolicy会被安装在child的EditPart上,把我们的画布看作parent,列就是child。当用户拖动列图形的handler时,产生一个类型为REQ_RESIZE的请求,这个请求被转发给parent的EditPolicy,所以我们要在画布的EditPolicy里覆盖getCommand()方法对这个请求进行处理,如下所示:

public Command getCommand(Request request) {
    if (REQ_RESIZE_CHILDREN.equals(request.getType())) {
        ChangeColumnHeightCommand cmd = new ChangeColumnHeightCommand();
        Column column = (Column) ((ColumnPart) ((ChangeBoundsRequest) request).getEditParts().get(0)).getModel();
        cmd.setColumn(column);
        cmd.setNewHeight(column.getHeight() + ((ChangeBoundsRequest) request).getSizeDelta().height);
        return cmd;
    }
    return super.getCommand(request);
}

这就会返回一个用来调整列高度的Command,这个命令的具体内容这里不赘述了。createChildEditPolicy是在LayoutEditPolicy里定义的一个抽象方法,GEF提供的与Layout有关的EditPolicy里都会提供一个缺省的实现,但在需求比较特殊的情况下我们要提供自己的实现。这里是OrderedLayoutEditPolicy(FlowLayoutEditPolicy的父类)关于该方法的注释:

Since Ordered layouts generally don't use constraints, a NonResizableEditPolicy is used by default for children. Subclasses may override this method to supply a different EditPolicy.

很明显GEF已经考虑到了这种情况。

搬家前链接:https://www.cnblogs.com/bjzhanghao/archive/2005/03/29/128091.html

[Eclipse]GEF入门系列(五、浅谈布局)

虽然很多GEF应用程序里都会用到连接(Connection),但也有一些应用是不需要用连接来表达关系的,我们目前正在做的这个项目就是这样一个例子。在这类应用中,模型对象间的关系主要通过图形的包含来表达,所以大多是一对多关系。

file
图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

表中从上到下是包含关系,也就是一对多关系,下图简单显示了这些关系:

file
图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.getBounds().height+clientArea.y,clientArea.width,clientArea.height-nameFigure.getBounds().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)) / numChildren;
        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这种只关心子图形位置的布局管理器中,就必须为每个子图形指定尺寸,否则图形将因为尺寸过小而不可见,这也是一个开发人员十分容易疏忽的地方。

搬家前链接:https://www.cnblogs.com/bjzhanghao/archive/2005/03/24/124497.html

[Eclipse]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)时,就在文字内容所在位置覆盖一个文本框(也可以是下拉框,这里我们只讨论文本的情况)作为编辑器,编辑结束后,再将编辑器中的内容应用到模型里即可。(作为类似的功能请参考:给表格的单元格增加编辑功能

file
图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。

file
图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这个类的代码来了解。

file
图3 大纲视图

P.S.写这篇帖子的时候,我对例子又做了一些修改,都是和这篇帖子所说的内容相关的,所以如果你以前下载过,会发现那时的代码与现在稍有不同(功能还是完全一样的,下载)。另外要说一下,这个例子并不完善,比如删除一个节点的时候,它的连接就没同时删除,一些键盘快捷键不起作用,还存在很多被注释掉的代码等等。如果有兴趣你可以来修改它们,也是不错的学习途径。

搬家前链接:https://www.cnblogs.com/bjzhanghao/archive/2005/03/15/119010.html

[Eclipse]GEF入门系列(三、应用实例)

构造一个GEF应用程序通常分为这么几个步骤:设计模型、设计EditPart和Figure、设计EditPolicy和Command,其中 EditPart是最主要的一部分,因为在实现它的时候不可避免的要使用到EditPolicy,而后者又涉及到Command。

现在我们来看个例子,它的功能非常简单,用户可以在画布上增加节点(Node)和节点间的连接,可以直接编辑节点的名称以及改变节点的位置,用户可以撤消/重做任何操作,有一个树状的大纲视图和一个属性页。点此下载(Update: For Eclipse 3.1的版本),这是一个Eclipse的项目打包文件,在Eclipse里导入后运行Run-time Workbench,新建一个扩展名为"gefpractice"的文件就会打开这个编辑器。

file

图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 Node", 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、属性表和大纲视图等一些功能没有讲解,下面的帖子里将介绍这些常用功能的实现。

搬家前链接:https://www.cnblogs.com/bjzhanghao/archive/2005/02/19/106000.html

[Eclipse]GEF入门系列(二、GEF概述)

在前面的帖子已经提到,GEF(Graphical Editor Framework)是一个图形化编辑框架,它允许开发人员以图形化的方式展示和编辑模型,从而提升用户体验。这样的应用程序有很多,例如:UML类图编辑器、图形化XML编辑器、界面设计工具以及图形化数据库结构设计工具等等。归结一下,可以发现它们在图形化编辑方面具有以下共同之处:

  • 提供一个编辑区域和一个工具条,用户在工具条里选择需要的工具,以拖动或单击的方式将节点或连接放置在编辑区域;
  • 节点可以包含子节点;
  • 用户能够查看和修改某个节点或连接的大部分属性;
  • 连接端点锚定在节点上;
  • 提供上下文菜单和键盘命令;
  • 提供图形的缩放功能;
  • 提供一个大纲视图,显示编辑区域的缩略图,或是树状模型结构;
  • 支持撤消/重做功能;
  • 等等。

 file
图1 基于GEF的界面设计工具(Visual Editor,VE)的工作界面

GEF最早是Eclipse的一个内部项目,后来逐渐转变为Eclipse的一个开源工具项目,Eclipse的不少其他子项目都需要它的支持。Eclipse 3.0版本花了很大功夫在从Platform中剥离各种功能部件上,包括GEF和IDE在内的很多曾经只能在Eclipse内部使用的工具成为可以独立使用的软件/插件包了。理论上我们是可以脱离Eclipse用GEF包构造自己的应用程序的,但由于它们之间天然的联系,而且Eclipse确实是一个很值得支持的开发平台,所以我还是推荐你在Eclipse中使用它。

GEF的优势是提供了标准的MVC(Model-View-Control)结构,开发人员可以利用GEF来完成以上这些功能,而不需要自己重新设计。与其他一些MVC编辑框架相比,GEF的一个主要设计目标是尽量减少模型和视图之间的依赖,好处是可以根据需要选择任意模型和视图的组合,而不必受开发框架的局限(不过实际上还是很少有脱离Draw2D的实现)。

现在来看看GEF是如何实现MVC框架的吧,在这个帖子里我们先概括介绍一下它的各个组成部分,以后将结合例子进行更详细的说明。

file
图2 GEF结构图

模型:GEF的模型只与控制器打交道,而不知道任何与视图有关的东西。为了能让控制器知道模型的变化,应该把控制器作为事件监听者注册在模型中,当模型发生变化时,就触发相应的事件给控制器,后者负责通知各个视图进行更新。

典型的模型对象会包含PropertyChangeSupport类型的成员变量,用来维护监听器成员即控制器;对于与其他对象具有连接关系的模型,要维护连入/连出的连接列表;如果模型对应的节点具有大小和位置信息,还要维护它们。这些变量并不是模型本身必须的信息,维护它们使模型变得不够清晰,但你可以通过构造一些抽象模型类(例如让所有具有连接的模型对象继承Node类)来维持它们的可读性。

相对来讲GEF中模型是MVC中最简单的一部分。

控制器:我们知道,在MVC结构里控制器是模型与视图之间的桥梁,也是整个GEF的核心。它不仅要监听模型的变化,当用户编辑视图时,还要把编辑结果反映到模型上。举个例子来说,用户在数据库结构图上删除一个表时,控制器应该从模型中删除这个表对象、表中的字段对象、以及与这些对象有关的所有连接。当然在GEF中这些操作不是由直接控制器完成的,这个稍后就会说到。

GEF中的控制器是所谓的EditPart对象,更确切的说应该是一组EditPart对象共同组成了GEF的控制器这部分,每一个模型对象都对应一个EditPart对象。你的应用程序中需要有一个EditPartFactory对象负责根据给定模型对象创建对应的EditPart对象,这个工厂类将被视图利用。

RootEditPart是一种特殊的EditPart,它和你的模型没有任何关系,它的作用是把EditPartViewer和contents(应用程序的最上层EditPart,一般代表一块画布)联系起来,可以把它想成是contents的容器。EditPartViewer有一个方法setRootEditPart()专门用来指定视图对应的RooEditPart。

file
图3 EditPart对象

用户的编辑操作被转换为一系列请求(Request),有很多种类的请求,这些种类在GEF里被称为角色(Role),GEF里有图形化和非图形化这两大类角色,前者比如Layout Role对应和布局有关的的操作,后者比如Connection Role对应和连接有关的操作等等。角色这个概念是通过编辑策略(EditPolicy)来实现的,EditPolicy的主要功能是根据请求创建相应的命令(Command),而后者会直接操作模型对象。对每一个EditPart,你都可以"安装"一些EditPolicy,用户对这个EditPart的特定操作会被交给已安装的对应EditPolicy处理。这样做的直接好处是可以在不同EditPart之间共享一些重复操作。

在GEF SDK提供的帮助文档(GEF开发指南)里有一份详细的EditPolicy、Role和Request类型列表,这里就不赘述了。

视图:前面说过,GEF的视图可以有很多种,GEF目前提供了图形(GraphicalViewer)和树状(TreeViewer)这两种,前者利用Draw2D图形(IFigure)作为表现方式,多用于编辑区域,后者则多用于实现大纲展示。视图的任务同样繁重,除了模型的显示功能以外,还要提供编辑功能、回显(Feedback)、工具提示(ToolTip)等等。

GEF使用EditPartViewer作为视图,它的作用和JFace中的Viewer十分类似,而EditPart就相当于是它的ContentProvider和LabelProvider,通过setContents()方法来指定。我们经常使用的Editor是一个GraphicalEditorWithPalette(GEF提供的Editor,是EditorPart的子类,具有图形化编辑区域和一个工具条),这个Editor使用GraphicalEditViewer和PaletteViewer这两个视图类,PaletteViewer也是GraphicalEditViewer的子类。开发人员要在configureGraphicalViewer()和initializeGraphicalViewer()这两个方法里对EditPartViewer进行定制,包括指定它的contents和EditPartFactory等等。

EditPartViewer同时也是ISelectionProvider,这样当用户在编辑区域做选择操作时,注册的SelectionChangeListener就可以收到选择事件。EditPartViewer会维护各个EditPart的选中状态,如果没有被选中的EditPart,则缺省选中的是作为contents的EditPart。

初步了解了GEF的MVC实现方式,让我们看看典型的GEF应用程序是什么样子的。大部分GEF应用程序都实现为Eclipse的Editor,也就是说整个编辑区域是放置在一个Editor里的。所以典型的GEF应用程序具有一个图形编辑区域包含在一个Editor(例如GraphicalEditorWithPalette)里,可能有一个大纲视图和一个属性页,一个用于创建EditPart实例的EditPartFactory,一些表示业务的模型对象,与模型对象对应的一些EditPart,每个EditPart对应一个IFigure的子类对象显示给用户,一些EditPolicy对象,以及一些Command对象。

GEF应用程序的工作方式如下: EditPartViewer接受用户的操作,例如节点的选择、新增或删除等等,每个节点都对应一个EditPart对象,这个对象有一组按操作Role分开的EditPolicy,每个EditPolicy会对应一些Command对象,Command最终对模型进行直接修改。用户的操作转换为Request分配给适当的EditPolicy,由后者创建适当的Command来修改模型,这些Command会保留在EditDomain(专门用于维护EditPartViewer、Command等信息的对象,一般每个Editor对应唯一一个该对象)的命令堆栈里,用于实现撤消/重做功能。

以上介绍了GEF中一些比较重要的概念,不知道看过之后你是否对它有了一个大概的印象。如果没有也没关系,因为在后面的帖子里将会有结合例子的讲解,我们使用的实例就是序言里提到的第六个项目。

参考资料

  • GEF开发指南
  • Eclipse Development - Using the Graphical Editing Framework and the Eclipse Modeling Framework

搬家前链接:https://www.cnblogs.com/bjzhanghao/archive/2005/02/13/104045.html

[Eclipse]GEF入门系列(一、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里都有些什么,请看下图。

file
图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为根节点的类树下有相当多的类,不过我个人感觉组织得有些混乱,幸好大部分情况下我们只用到其中常用的那一部分。

file
图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属性的值可以设置以端点为原点的坐标)。

file
图3 三种Router的外观

此外,Draw2D在org.eclipse.draw2d.geometry包里提供了几个很方便的类型,如Dimension、Rectangle、Insets、Point和PointList等等,这些类型既在Draw2D内部广泛使用,也可以被开发人员用来简化计算。例如Rectangle表示的是一个矩形区域,它提供getIntersection()方法能够方便的计算该区域与另一矩形区域的重叠区域、getTransposed()方法可以得到长宽值交换后的矩形区域、scale()方法进行矩形的拉伸等等。在自己实现LayoutManager的时候,由于会涉及到比较复杂的几何计算,所以更推荐使用这些类。

以上介绍了Draw2D提供的大部分功能,利用这些我们已经能够画出十分漂亮的图形了。但对大多数实际应用来说这样还远远不够,我们还要能编辑它,并把对图形的修改反映到模型里去。为了漂亮的完成这个艰巨任务,GEF绝对是不二之选。从下一次开始,我们将正式进入GEF的世界。

参考资料

搬家前链接:https://www.cnblogs.com/bjzhanghao/archive/2005/02/09/103595.html

[Eclipse]GEF入门系列(序)

file

前些天换了新电脑,本人一直处于兴奋中,基本是"不务正业"的状态。快过年了,虽然没什么动力干活,但我玩游戏技术比较差,魔兽3打电脑一家还很费劲,干脆写写帖子就当是休息吧!

由于工作的需要,最近开始研究GEF(Graphical Editor Framework)这个框架,它可以用来给用户提供图形化编辑模型的功能,从而提升用户体验,典型的应用如图形化的流程设计器、UML类图编辑器等等。其实一年多来我们做的项目都是和它有关的,只是之前我具体负责的事情和它没什么关系。那时也看过黄老大写的代码,EMF和GEF混在一起特别晕,没能坚持看下去。这次自己要动手做了,正好趁此机会把它搞明白,感觉GEF做出来的东西给人很专业的感觉,功能也很强大,应该挺有前途的。此外,GEF里用到了很多经典模式,最突出的如大量应用Command模式,方便的实现Undo/Redo功能等等,通过学习GEF,等于演练了这些模式,比只是看看书写几个类那种学习方式的效果好很多。

现在网上关于GEF的文章和教程还不是很多(比起一年前还是增加了几篇),基本上都是eclipse.org上的那些,其中少数几篇有中文版,中文的原创就属于凤毛麟角了,市场上似乎也没有这方面的成书。GEF SDK里自带的文档则比较抽象,不适合入门。我觉得最好的入门方法是结合具体的例子,一边看代码,一边对照文档,然后自己再动手做一做。当然这个例子要简单点才好,像GEF的那个logic的例子就太复杂了,即使是flow(运行界面见下图)我觉得也有点大;另外例子要比较规范的,否则学成错误的路子以后还要花时间改就不值得了。

file

图 用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 Graphical Editor Framework (GEF) application. I wanted, then, to write a tutorial that essentially walked the reader through the various stages of the development of my first application. I even suggested some kind of versioned literate programming 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 application to the stage where I had put together a snapshot at each of six milestones. A few people have written to me over the last six months asking the status of 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为:

<import plugin="org.eclipse.core.resources"/> 
<import plugin="org.eclipse.gef"/>
<import plugin="org.eclipse.ui"/> 
<import plugin="org.eclipse.core.runtime"/> 
<import plugin="org.eclipse.core.runtime.compatibility"/> 
<import plugin="org.eclipse.ui.views"/> 

再修改一下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开始说起,当然不能讲得很深入,因为我自己也是略知皮毛而已。

说实话,我对写这个系列不太有信心,因为自己也是刚入门而已。但要是等到几个月后再写,很多心得怕是讲不出来了。所以还是那句话,有什么写错的请指正,并且欢迎交流。

搬家前链接:https://www.cnblogs.com/bjzhanghao/archive/2005/02/05/102513.html