由于Eclipse版本不符造成的异常

前几天把GEF版本从3.0.1升级到了3.1M6,发现以前运行正常的GEF程序现在总出现下面这个异常,例如在我移动一个节点时,或者创建一个新节点时。

!ENTRY org.eclipse.ui 4 0 2005-04-19 23:12:55.974
!MESSAGE tried to access method org.eclipse.ui.views.properties.PropertySheetEntry.refreshFromRoot()V from class org.eclipse.gef.ui.properties.UndoablePropertySheetEntry
!STACK 0
java.lang.IllegalAccessError: tried to access method org.eclipse.ui.views.properties.PropertySheetEntry.refreshFromRoot()V from class org.eclipse.gef.ui.properties.UndoablePropertySheetEntry
    at org.eclipse.gef.ui.properties.UndoablePropertySheetEntry.access$0(UndoablePropertySheetEntry.java:1)
    at org.eclipse.gef.ui.properties.UndoablePropertySheetEntry$1.commandStackChanged(UndoablePropertySheetEntry.java:103)
    at org.eclipse.gef.commands.CommandStack.notifyListeners(CommandStack.java:253)
    at org.eclipse.gef.commands.CommandStack.execute(CommandStack.java:141)
    at org.eclipse.gef.tools.AbstractTool.executeCommand(AbstractTool.java:374)
    at org.eclipse.gef.tools.AbstractTool.executeCurrentCommand(AbstractTool.java:386)
    at org.eclipse.gef.tools.DragEditPartsTracker.performDrag(DragEditPartsTracker.java:450)
    at org.eclipse.gef.tools.DragEditPartsTracker.handleButtonUp(DragEditPartsTracker.java:320)
    at org.eclipse.gef.tools.AbstractTool.mouseUp(AbstractTool.java:1035)
    at org.eclipse.gef.tools.SelectionTool.mouseUp(SelectionTool.java:545)
    at org.eclipse.gef.EditDomain.mouseUp(EditDomain.java:259)
    at org.eclipse.gef.ui.parts.DomainEventDispatcher.dispatchMouseReleased(DomainEventDispatcher.java:374)
    at org.eclipse.draw2d.LightweightSystem$EventHandler.mouseUp(LightweightSystem.java:548)
    at org.eclipse.swt.widgets.TypedListener.handleEvent(TypedListener.java:136)
    at org.eclipse.swt.widgets.EventTable.sendEvent(EventTable.java:82)
    at org.eclipse.swt.widgets.Widget.sendEvent(Widget.java:842)
    at org.eclipse.swt.widgets.Display.runDeferredEvents(Display.java:2908)
    at org.eclipse.swt.widgets.Display.readAndDispatch(Display.java:2541)
    at org.eclipse.ui.internal.Workbench.runEventLoop(Workbench.java:1612)
    at org.eclipse.ui.internal.Workbench.runUI(Workbench.java:1578)
    at org.eclipse.ui.internal.Workbench.createAndRunWorkbench(Workbench.java:293)
    at org.eclipse.ui.PlatformUI.createAndRunWorkbench(PlatformUI.java:144)
    at org.eclipse.ui.internal.ide.IDEApplication.run(IDEApplication.java:102)
    at org.eclipse.core.internal.runtime.PlatformActivator$1.run(PlatformActivator.java:228)
    at org.eclipse.core.runtime.adaptor.EclipseStarter.run(EclipseStarter.java:333)
    at org.eclipse.core.runtime.adaptor.EclipseStarter.run(EclipseStarter.java:150)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
    at java.lang.reflect.Method.invoke(Unknown Source)
    at org.eclipse.core.launcher.Main.invokeFramework(Main.java:268)
    at org.eclipse.core.launcher.Main.basicRun(Main.java:260)
    at org.eclipse.core.launcher.Main.run(Main.java:887)
    at org.eclipse.core.launcher.Main.main(Main.java:871)

调试了很久也没找到原因,最后在GEF新闻组里得到了答案,原来GEF3.1M6要求Eclipse的版本在3.1M6或以上,而我正在使用的是Eclipse 3.1M5a,换到3.1M6一试果然OK。

如果你遇到类似的奇怪异常,不妨也先检查一下Eclipse和插件的版本。

[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]处理颜色类型的偏好项

在Eclipse里实现偏好页(PreferencePages)时,我们一般要在Plugin类的start()方法里预先设置好每一项的缺省值,但IPreferenceStore接口只提供了参数为整型、布尔型、字符串等基本类型的setDefault()方法,如果某个偏好项是颜色类型怎么办呢?

这时要使用PreferenceConverter这个类,也是由jface提供的,该类提供了额外的一些setDefault()方法可以接收RGB、Font、Point等类型的参数,所以可以使用下面的方法设置缺省值:

PreferenceConverter.setDefault(getPreferenceStore(), IConstants.PREF_COLOR_PRIORITY_HIGH, new RGB(255, 128, 64));

要取得某个颜色类型的偏好项值,就用下面的语句:

PreferenceConverter.getColor(CbmPlugin.getDefault().getPreferenceStore(), IConstants.PREF_COLOR_PRIORITY_HIGH)

JFace在颜色、图象和字体等资源的管理方面为我们做了很多工作,我们应该尽量利用已有的这些功能。

file
图1 颜色类型的偏好项

顺便说一下,要让editor里的图形在用户更改preference设置后立即反应变化,要在preferenceStore上增加监听器,例如在editor的initializeGraphicalViewer()方法里这样写:

protected void initializeGraphicalViewer() {

    Activator.getDefault().getPreferenceStore().addPropertyChangeListener(new IPropertyChangeListener() {
        public void propertyChange(PropertyChangeEvent event) {
            // You may refresh some editparts only for better performance
            //    if(IConstants.PREF_SHOW_ACTIVITIES.equals(event.getProperty()))
                refreshAllEditParts(getGraphicalViewer().getRootEditPart());
        }

        private void refreshAllEditParts(EditPart part) {
            part.refresh();
            List children = part.getChildren();
            for (Iterator iter = children.iterator(); iter.hasNext();) {
                EditPart child = (EditPart) iter.next();
                refreshAllEditParts(child);
            }
        }

    });
}

最后,org.eclipse.ui.preferencePages扩展点的page元素的category属性的值应该是另一个page的id值。

搬家前链接:https://www.cnblogs.com/bjzhanghao/archive/2005/04/12/136105.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

利用Digester把XML转换为Java对象

在一个比较现实的应用系统里,经常需要有一些配置文件。简单的属性使用.properties文件即可,但要配置一些复杂对象,则应该考虑使用xml文件。一般用来读取xml文件的工具包有DOM、SAX和JDOM等,但用过的人都知道,它们属于比较底层的API,写起来代码量很大,而且如果修改了xml文件的格式,代码也要做大幅度的改动。Jakarta Commons项目里的Digester包,可以轻松实现xml文件到Java对象的转换,请看下面这个例子。

file

在一个项目里,需要提供一些统计图,但图的内容暂时未能确定。所以我决定让图可以配置,所有定义保存在一个名为charts.xml(或国际化后的文件名如charts_zh_CN.xml,这里只考虑缺省语言)的文件内,下面是该文件的部分内容:

<?xml version="1.0" encoding="UTF-8" ?>
<charts>
    <chart id="BarChart1" >
        <title>统计图一</title>
        <legendVisible>false</legendVisible>
        <toolTipsVisible>true</toolTipsVisible>
        <type>Bar</type>
        <labelx>时间</labelx>
        <labely>数据</labely>
        <width>500</width>
        <height>360</height>
        <hql>select count(c),c.department.name from edu.pku.pub.aims.model.business.Client c group by c.department</hql>
        <description></description>
    </chart>
</charts>

可以看出,我为每个图定义了id、title、legendVisible等等属性,这些属性的意义都很明显,它们将影响统计图的数据和在页面中的表现。在程序里,我需要把这个文件里的定义读到一个注册表类ChartRegistry里,该注册表维护一个java.util.List类型的registry变量,其中每个元素是一个ChartConfig类。现在Digester该显示它的价值了。

为了方便使用Digester,我们让ChartConfig也具有统计图的每个属性(id、title、legendVisible等等),名称与charts.xml里的元素的属性(子元素)一一对应,并且都具有getter和setter方法,也就是说,ChartConfig是一个bean类。在ChartRegistry类里定义一个deregister()方法,它的作用是用Digester读入并解析指定的xml文件,代码如下;还有一个register()方法用来把ChartConfig对象加到registry里。

public void deregister(URL url) throws IOException,SAXException{
    InputStream is = new FileInputStream(url.getFile());
    Digester digester = new Digester();
    digester.push(this);
    digester.setValidating(false);
    digester.addObjectCreate("charts/chart", ChartConfig.class);
    digester.addSetProperties("charts/chart");
    digester.addBeanPropertySetter("charts/chart/legendVisible");
    digester.addBeanPropertySetter("charts/chart/toolTipsVisible");
    digester.addBeanPropertySetter("charts/chart/title");
    digester.addBeanPropertySetter("charts/chart/type");
    digester.addBeanPropertySetter("charts/chart/labelx");
    digester.addBeanPropertySetter("charts/chart/labely");
    digester.addBeanPropertySetter("charts/chart/width");
    digester.addBeanPropertySetter("charts/chart/height");
    digester.addBeanPropertySetter("charts/chart/hql");
    digester.addBeanPropertySetter("charts/chart/description");
    digester.addSetNext("charts/chart","register");
    digester.parse(is);
    Collections.sort(registry);
}

基本上来说,Digester和SAX解析xml的过程很像,它的原理就是制定一些规则,在遍历每个节点时检查是否有匹配的规则,如果有就执行对应的操作。例如,上面的代码中,“digester.addObjectCreate("charts/chart", ChartConfig.class);”这一句的作用是告诉Digester:如果遇到匹配“charts/chart”形式的节点,就执行一个“对象创建”操作,创建什么对象呢,应该创建Class为“ChartConfig.class”的对象;类似的,addSetProperties()是告诉Digester将指定节点的属性全部映射到对象的属性,在这个例子里指的就是id属性;addBeanPropertySetter()是将子节点转换为对象的属性,这个方法还可以有第二个参数,当对象的属性名和子节点的名字不一样时用来指定对象的属性名;addSetNext()是说在遇到匹配节点后,对当前对象的父对象执行一个方法,参数是当前参数,对这个例子来说就是执行ChartConfig.register(ChartConfig)方法。因此这样构造得到的Digester会把charts.xml里的每个元素转换为一个ChartConfig对象,并register到ChartRegistry里。

顺利得到了ChartRegister对象,我就可以在程序里根据它的内容构造统计图了(统计图一般使用jfreechart来生成,这里就不赘述了)。与Digester具有类似功能的工具包其实还有不少,例如Caster、Jato等等,我没有实际使用过它们,但因为我对用过的Jakarta其他项目都很满意(例如BeanUtils、HttpClient,品牌效应?),所以一开始就选择了Digester:简单方便。

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

宣传一下黄老大的ET2项目

我曾经的supervisor黄老大开发了一个功能十分强大的Eclipse Toolkit,提供了一些Eclipse应用程序经常需要用到的组件,如高度可定制的表格控件等等,是对目前SWT/JFace/GEF的扩展和增强。

Here is the link: http://sourceforge.net/projects/eclipsetoolkit2/.
An old link is http://www.cs.umb.edu/~huangjun/.

I release all files under epl v1.0.

Sorry for bare documents and samples coz mid-term is coming! I will try my
best to make up.

I do believe there are many ways to impl such stuffs and I am not sure my
way is good enough. I am open to any comments. Thanks for your attention!”

file

[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

用Jena获得本体的缺省名称空间

这个标题其实有点问题,因为本体/RDF本身并没有名称空间的概念,它们只关心绝对的URI;在Jena里一旦模型读进内存,就都是使用绝对URI标识资源的,而当使用xml格式存储的时候,才会引出这些xml中的概念。

最近遇到一个问题,在一个程序里要读取多个xml格式的本体文件(*.owl),它们之间有import关系,在读一个文件之前,我需要先确认该文件需要import的那些名称空间所对应的本体(此本体的缺省名称空间是import的值)是否存在,所以我必须知道每个.owl文件的缺省名称空间和import值(后者在这里暂不讨论),这样做的一个好处是不需要让Jena在找不到本地文件时去访问网络,造成延迟。

虽然Jena API没有提供直接得到它的方法,从一个.owl文件得到缺省名称空间的方法其实很简单,但com.hp.hpl.jena.shared.PrefixMapping接口有一个getNsPrefixURI()方法,利用它可以得到xml文档中每个prefix对应的名称空间。

什么是前缀(prefix)呢?prefix主要是为了简化xml的书写,例如下面的语句中,rdf就是prefix,这样在xml文档的其他地方就可以用"rdf:xxx"表示"http://www.w3.org/1999/02/22-rdf-syntax-ns#xxx"这一长串了。

xmlns:rdf="[http://www.w3.org/1999/02/22-rdf-syntax-ns](http://www.w3.org/1999/02/22-rdf-syntax-ns)#"

有了这个方法,我们就可以得到rdf、owl、xsd等前缀对应的名称空间,而缺省名称空间对应的前缀是空字符串(""),因此使用model.getNsPrefixURI(""),而OntModel对象是实现PrefixMapping接口的。

当然,要得到一个xml文件的缺省名称空间有很多方法,只是在一个面向本体而不是xml的应用程序里,使用本体这个层次的API可能更合适一些。

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