世界地球日

今天是第36个世界地球日,也是我26岁生日,今年的生日有个惊喜。

早上刷牙时发现,有只大白猫在窗台上生下两只小猫,小猫身体小小的眼睛紧闭,简直就像两只小白老鼠。九点钟左右母猫离开不知去哪了,等了两个小时还没回来,可能是发现我在看她觉得不安全吧,所以我决定自己先给小猫喂点奶。谁知小猫用的东西很难买,连跑了三个地方,才在宠物医院买到猫牛奶(要医生开才行)和专用奶瓶。等我兑好温牛奶,都下午两点钟了,谁知小猫虽然五个小时没吃东西但就是不吃我拿的奶,把它拿在手里它就拼命挣扎和叫唤,试了一个多钟头还是没用,还是把它们放回原处让它们睡觉吧。

过了一会儿听到窗台上有动静,悄悄过去一看,原来是那只母猫回来了,她先是对两只小猫叫了几声,然后一次一个把小猫叼在嘴里带走了。

虽然一天没能去上班,但小猫终于和妈妈团聚了,我也放心了,祝你们一生平安。

Update04/23: 没想到今天(也许是昨天晚上)母猫带着小猫们又回来了,一共有四只小猫呢,它们好象把窗台当成自己的窝了。GF盛了一些牛奶和猫粮放在窗台上,刚才去看母猫已经吃掉了:)

Update: 更多照片见http://whitecats.mblogger.cn/

Update: 上面的链接已失效。

[Eclipse]GEF入门系列(九、增加易用性)

当一个GEF应用程序实现了大部分必需的业务功能后,为了能让用户使用得更方便,我们应该在易用性方面做些考虑。从3.0版本开始, GEF增加了更多这方面的新特性,开发人员很容易利用它们来改善自己的应用程序界面。这篇帖子将介绍主要的几个功能,它们有些在GEF 2.1中就出现了,但因为都是关于易用性的而且以前没有提到,所以放在这里一起来说。( 下载示例代码

可折叠调色板

在以前的例子里,我们的编辑器都继承自GraphicalEditorWithPalette。GEF 3.0提供了一个功能更加丰富的编辑器父类:GraphicalEditorWithFlyoutPalette,继承它的编辑器具有一个可以折叠的工具条,并且能够利用Eclipse自带的调色板视图,当调色板视图显示时,工具条会自动转移到这个视图中。

图1 可折叠和配置的调色板

与以前的GraphicalEditorWithPalette相比,继承 GraphicalEditorWithFlyoutPalette的编辑器要多做一些工作。首先要实现getPalettePreferences() 方法,它返回一个FlyoutPreferences实例,作用是把调色板的几个状态信息(位置、大小和是否展开)保存起来,这样下次打开编辑器的时候就可以自动套用这些设置。下面使用偏好设置的方式保存和载入这些状态,你也可以使用其他方法,比如保存为.properties文件:

protected FlyoutPreferences getPalettePreferences() {
    return new FlyoutPreferences() {
        public int getDockLocation() {
            return SubjectEditorPlugin.getDefault().getPreferenceStore().getInt(IConstants.PREF_PALETTE_DOCK_LOCATION);
        }
        public void setDockLocation(int location) { 
            SubjectEditorPlugin.getDefault().getPreferenceStore().setValue(IConstants.PREF_PALETTE_DOCK_LOCATION,location);
        }
        …
    };
}

然后要覆盖缺省的createPaletteViewerProvider()实现,在这里为调色板增加拖放支持,即指定调色板为拖放源(之所以用这样的方式,原因是在编辑器里没有办法得到它对应的调色板实例),在以前这个工作通常是在initializePaletteViewer ()方法里完成的,而现在这个方法已经不需要了:

protected PaletteViewerProvider createPaletteViewerProvider() {
    return new PaletteViewerProvider(getEditDomain()) {
        protected void configurePaletteViewer(PaletteViewer viewer) {
            super.configurePaletteViewer(viewer);
            viewer.addDragSourceListener(new TemplateTransferDragSourceListener(viewer));
        }
    };
}

GEF 3.0还允许用户对调色板里的各种工具进行定制,例如隐藏某个工具,或是修改工具的描述等等,这是通过给PaletteViewer定义一个 PaletteCustomizer实例实现的,但由于时间关系,这里暂时不详细介绍了,如果需要这项功能你可以参考Logic例子中的实现方法。

缩放

由于Draw2D中的图形都具有天然的缩放功能,因此在GEF里实现缩放功能是很容易的,而且缩放的效果不错。GEF为我们提供了 ZoomInAction和ZoomOutAction以及对应的RetargetAction(ZoomInRetargetAction和 ZoomOutRetargetAction),只要在编辑器里构造它们的实例,然后在编辑器的ActionBarContributer类里将它们添加到想要的菜单或工具条位置即可。因为ZoomInAction和ZoomOutAction的构造方法要求一个ZoomManager类型的参数,而后者需要从GEF的RootEditPart中获得(ScalableRootEditPart或 ScalableFreeformRootEditPart),所以最好在编辑器的 configureGraphicalViewer()里构造这两个Action比较方便,请看下面的代码:

protected void configureGraphicalViewer() {
    super.configureGraphicalViewer();
    ScalableFreeformRootEditPart root = new ScalableFreeformRootEditPart();
    getGraphicalViewer().setRootEditPart(root);
    getGraphicalViewer().setEditPartFactory(new PartFactory());
    action = new ZoomInAction(root.getZoomManager());
    getActionRegistry().registerAction(action);
    getSite().getKeyBindingService().registerAction(action);
    action = new ZoomOutAction(root.getZoomManager());
    getActionRegistry().registerAction(action);
    getSite().getKeyBindingService().registerAction(action);
}

假设我们想把这两个命令添加到主工具条上,在DiagramActionBarContributor里应该做两件事:在 buildActions()里构造对应的RetargetAction,然后在contributeToToolBar()里添加它们到工具条(原理请参考前面关于菜单和工具条的 帖子):

protected void buildActions() {
//其他命令
…
//缩放命令
addRetargetAction(new ZoomInRetargetAction());
addRetargetAction(new ZoomOutRetargetAction());
}
public void contributeToToolBar(IToolBarManager toolBarManager) {
    //工具条中的其他按钮
    …
    //缩放按钮
    toolBarManager.add(getAction(GEFActionConstants.ZOOM_IN));
    toolBarManager.add(getAction(GEFActionConstants.ZOOM_OUT));
    toolBarManager.add(new ZoomComboContributionItem(getPage()));
}

请注意,在contributeToToolBar()方法里我们额外添加了一个ZoomComboContributionItem 的实例,这个类也是GEF提供的,它的作用是显示一个缩放百分比的下拉框,用户可以选择或输入想要的数值。为了让这个下拉框能与编辑器联系在一起,我们要修改一下编辑器的getAdapter()方法,增加对它的支持:

public Object getAdapter(Class type) {
    …
    if (type == ZoomManager.class)
        return getGraphicalViewer().getProperty(ZoomManager.class.toString());
    return super.getAdapter(type);
}

现在,打开编辑器后主工具条中将出现下图所示的两个按钮和一个下拉框:

图2 缩放工具条

有时候我们想让程序把用户当前的缩放值记录下来,以便下次打开时显示同样的比例。这就须要在画布模型里增加一个zoom变量,在编辑器的初始化过程中增加下面的语句,其中diagram是我们的画布实例:

ZoomManager manager = (ZoomManager) getGraphicalViewer().getProperty(ZoomManager.class.toString());
if (manager != null)
manager.setZoom(diagram.getZoom());

在保存模型前得到当前的缩放比例放在画布模型里一起保存:

ZoomManager manager = (ZoomManager) getGraphicalViewer().getProperty(ZoomManager.class.toString());
if (manager != null)
    diagram.setZoom(manager.getZoom());

辅助网格

你可能用过一些这样的应用程序,画布里可以显示一个灰色的网格帮助定位你的图形元素,当被拖动的节点接近网格线条时会被”吸附”到网格上,这样可以很容易的把画布上的图形元素排列整齐,GEF 3.0里就提供了显示这种辅助网格的功能。

图3 辅助编辑网格

是否显示网格以及是否打开吸附功能是由GraphicalViewer的两个布尔类型的属性(property)值决定的,它们分别是 SnapToGrid.PROPERTY_GRID_VISIBLE和SnapToGrid.PROPERTY_GRID_ENABLED,这些属性是通过GriaphicalViewer.getProperty()和setProperty()方法来操作的。GEF为我们提供了一个 ToggleGridAction用来同时切换它们的值(保持这两个值同步确实符合一般使用习惯),但没有像缩放功能那样提供对应的 RetargetAction,不知道GEF是出于什么考虑。另外因为这个Action没有预先设置的图标,所以把它直接添加到工具条上会很不好看,所以要么把它只放在菜单中,要么为它设置一个图标,至于添加到菜单的方法这里不赘述了。

要想在保存模型时同时记录当前网格线是否显示,必须在画布模型里增加一个布尔类型变量,并在打开模型和保存模型的方法中增加处理它的代码。

几何对齐

这个功能也是为了方便用户排列图形元素的,如果打开了此功能,当用户拖动的图形有某个边靠近另一图形的某个平行边延长线时,会自动吸附到这条延长线上;若两个图形的中心线(通过图形中心点的水平或垂直线)平行靠近时也会产生吸附效果。例如下图中,Subject1的左边与 Subject2的右边是吸附在一起的,Subject3原本是与Subject2水平中心线吸附的,而用户在拖动的过程中它的上边吸附到 Subject1的底边。

图4 几何对齐

几何对齐也是通过GraphicalViewer的属性来控制是否打开的,属性的名称是 SnapToGeometry.PROPERTY_SNAP_ENABLED,值为布尔类型。在程序里增加吸附对齐切换的功能和前面说的增加网格切换功能基本是一样的,记住GEF为它提供的Action是ToggleSnapToGeometryAction。

要实现对齐功能,还有一个重要的步骤,那就是在画布所对应的EditPart的getAdapter()方法里增加对 SnapToHelper类的回应,像下面这样:

public Object getAdapter(Class adapter) {
    if (adapter == SnapToHelper.class) {
        List snapStrategies = new ArrayList();
        Boolean val = (Boolean)getViewer().getProperty(RulerProvider.PROPERTY_RULER_VISIBILITY);
        if (val != null && val.booleanValue())
            snapStrategies.add(new SnapToGuides(this));
        val = (Boolean)getViewer().getProperty(SnapToGeometry.PROPERTY_SNAP_ENABLED);
        if (val != null && val.booleanValue())
            snapStrategies.add(new SnapToGeometry(this));
        val = (Boolean)getViewer().getProperty(SnapToGrid.PROPERTY_GRID_ENABLED);
        if (val != null && val.booleanValue())
            snapStrategies.add(new SnapToGrid(this));
        
        if (snapStrategies.size() == 0)
            return null;
        if (snapStrategies.size() == 1)
            return (SnapToHelper)snapStrategies.get(0);

        SnapToHelper ss[] = new SnapToHelper[snapStrategies.size()];
        for (int i = 0; i < snapStrategies.size(); i++)
            ss[i] = (SnapToHelper)snapStrategies.get(i);
        return new CompoundSnapToHelper(ss);
    }
    return super.getAdapter(adapter);
}

 

标尺和辅助线

标尺位于画布的上部和左侧,在每个标尺上可以建立很多与标尺垂直的辅助线,这些显示在画布上的虚线具有吸附功能。

图5 标尺和辅助线

标尺和辅助线的实现要稍微复杂一些。首先要修改原有的模型,新增加标尺和辅助线这两个类,它们之间的关系请看下图:< /p>

图6 增加标尺和辅助线后的模型

与上篇帖子里的 模型图比较后可以发现,在Diagram类里增加了四个变量,其中除rulerVisibility以外三个的作用都在前面部分做过介绍,而rulerVisibility和它们类似,作用记录标尺的可见性,当然只有在标尺可见的时候辅助线才是可见的。我们新增了Ruler和 Guide两个类,前者表示标尺,后者表示辅助线。因为辅助线是建立在标尺上的,所以Ruler到Guide有一个包含关系(黑色菱形);画布上有两个标尺,分别用topRuler和leftRuler这两个变量引用,也是包含关系,也就是说,画布上只能同时具有这两个标尺;Node到Guide有两个引用,表示Node吸附到的两条辅助线(为了简单起见,在本文附的例子中并没有实际使用到它们,Guide类中定义的几个方法也没有用到)。Guide类里的map变量用来记录吸附在自己上的节点和对应的吸附边。要让画布上能够显示标尺,首先要将原先的GraphicalViewer改放在一个 RulerComposite实例上(而不是直接放在编辑器上),后者是GEF提供的专门用于显示标尺的组件,具体的改变方法如下:

//定义一个RulerComposite类型的变量
private RulerComposite rulerComp;
//创建RulerComposite,并把GraphicalViewer创建在其上< span style="color: #008000;">
protected void createGraphicalViewer(Composite parent) {
    rulerComp = new RulerComposite(parent, SWT.NONE);
    super.createGraphicalViewer(rulerComp);
    rulerComp.setGraphicalViewer((ScrollingGraphicalViewer) getGraphicalViewer());
}
//覆盖getGraphicalControl返回RulerComposite实例< span style="color: #008000;">
protected Control getGraphicalControl() {
return rulerComp;
}

然后,要设置GraphicalViewer的几个有关属性,如下所示,其中前两个分别表示左侧和上方的标尺,而最后一个表示标尺的可见性:

getGraphicalViewer().setProperty(RulerProvider.PROPERTY_VERTICAL_RULER,new SubjectRulerProvider(diagram.getLeftRuler()));
 getGraphicalViewer().setProperty(RulerProvider.PROPERTY_HORIZONTAL_RULER,newSubjectRulerProvider(diagram.getTopRuler()));
 getGraphicalViewer().setProperty(RulerProvider.PROPERTY_RULER_VISIBILITY,new Boolean(diagram.isRulerVisibility()));

在前两个方法里用到了SubjectRulerProvider这个类,它是我们从RulerProvider类继承过来的, RulerProvider是一个比较特殊的类,其作用有点像EditPolicy,不过除了一些getXXXCommand()方法以外,还有其他几个方法要实现。需要返回Command的方法包括:getCreateGuideCommand()、getDeleteGuideCommand()和 getMoveGuideCommand(),分别返回创建辅助线、删除辅助线和移动辅助线的命令,下面列出创建辅助线的命令,其他两个的实现方式是类似的,你可以在本文所附例子中找到它们的代码:

public class CreateGuideCommand extends Command {
private Guide guide;
private Ruler ruler;
private int position;
public CreateGuideCommand(Ruler parent, int position) {
    setLabel("Create Guide");
    this.ruler = parent;
    this.position = position;
}
public void execute() {
    guide = ModelFactory.eINSTANCE.createGuide();//创建一条新的辅助线
guide.setHorizontal(!ruler.isHorizontal());
    guide.setPosition(position);
    ruler.getGuides().add(guide);
}
public void undo() {
    ruler.getGuides().remove(guide);
}
}

接下来再看看RulerProvider的其他方法,SubjectRulerProvider维护一个Ruler对象,在构造方法里要把它的值传入。此外,在构造方法里还应该给Ruler和Guide模型对象增加监听器用来响应标尺和辅助线的变化,下面是Ruler监听器的主要代码(因为使用了EMF作为模型,所以监听器实现为Adapter。如果你不用EMF,可以使用PropertyChangeListener实现):

public void notifyChanged(Notification notification) {
switch (notification.getFeatureID(ModelPackage.class)) {
    case ModelPackage.RULER__UNIT:
        for (int i = 0; i < listeners.size(); i++)
            ((RulerChangeListener) listeners.get(i)).notifyUnitsChanged(ruler.getUnit());
            break;
    case ModelPackage.RULER__GUIDES:
        Guide guide = (Guide) notification.getNewValue();
        if (getGuides().contains(guide))
            guide.eAdapters().add(guideAdapter);
        else
            guide.eAdapters().remove(guideAdapter);
        for (int i = 0; i < listeners.size(); i++)
            ((RulerChangeListener) listeners.get(i)).notifyGuideReparented(guide);
        break;
}
}

可以看到监听器在被触发时所做的工作实际上是触发这个RulerProvider的监听器列表(listeners)里的所有监听器,而这些监听器就是RulerEditPart或GuideEditPart,而我们不需要去关心这两个类。Ruler的事件有两种,一是单位(象素、厘米、英寸)改变,二是创建辅助线,在创建辅助线的情况要给这个辅助线增加监听器。下面是Guide监听器的主要代码:

public void notifyChanged(Notification notification) {
Guide guide = (Guide) notification.getNotifier();
switch (notification.getFeatureID(ModelPackage.class)) {
    case ModelPackage.GUIDE__POSITION:
        for (int i = 0; i < listeners.size(); i++)
            ((RulerChangeListener) listeners.get(i)).notifyGuideMoved(guide);
        break;
    case ModelPackage.GUIDE__MAP:
        for (int i = 0; i < listeners.size(); i++)
        ((RulerChangeListener) listeners.get(i)).notifyPartAttachmentChanged(notification.getNewValue(),guide);
        break;
}
}

Guide监听器也有两种事件,一是辅助线位置改变,二是辅助线上吸附的图形的增减变化。请注意,这里的循环一定不要用 iterator的方式,而应该用上面列出的下标方式,否则会出现ConcurrentModificationException异常,原因和 RulerProvider的notifyXXX()实现有关。我们的SubjectRulerProvider构造方法如下所示,它的主要工作就是增加监听器:

public SubjectRulerProvider(Ruler ruler) {
this.ruler = ruler;
ruler.eAdapters().add(rulerAdapter);
//载入模型的情况下,ruler可能已经包含一些guides,所以要给它们增加监听器< span style="color: #008000;">
for (Iterator iter = ruler.getGuides().iterator(); iter.hasNext();) {
    Guide guide = (Guide) iter.next();
    guide.eAdapters().add(guideAdapter);
}
}

在RulerProvider里还有几个方法要实现才能正确使用标尺:getRuler()返回RulerProvider维护的 Ruler实例,getGuides()返回辅助线列表,getGuidePosition(Object)返回某条辅助线在标尺上的位置(以pixel 为单位),getPositions()返回标尺上所有辅助线位置构成的整数数组。以下是本例中的实现方式:

public Object getRuler() {
    return ruler;
}
public List getGuides() {
    return ruler.getGuides();
}
public int[] getGuidePositions() {
    List guides = getGuides();
    int[] result = new int[guides.size()];
    for (int i = 0; i < guides.size(); i++) {
        result[i] = ((Guide) guides.get(i)).getPosition();
    }
    return result;
}
public int getGuidePosition(Object arg0) {
    return ((Guide) arg0).getPosition();
}

有了这个自定义的RulerProvider类,再通过把该类的两个实例被放在GraphicalViewer的两个属性(PROPERTY_VERTICAL_RULER和PROPERTY_HORIZONTAL_RULER)中,画布就具有标尺的功能了。GEF提供了用于切换标尺可见性的命令:ToggleRulerVisibilityAction,我们使用和前面同样的方法把它加到主菜单即可控制显示或隐藏标尺和辅助线。

位置和尺寸对齐

图形编辑工具大多具有这样的功能:选中两个以上图形,再按一下按钮就可以让它们以某一个边或中心线对齐,或是调整它们为同样的宽度高度。GEF提供AlignmentAction和MatchSizeAction分别用来实现位置对齐和尺寸对齐,使用方法很简单,在编辑器的 createActions()方法里构造需要的对齐方式Action(例如对齐到上边、下边等等),然后在编辑器的 ActionBarContributor里通过这些Action对应的RetargetAction将它们添加到菜单或工具条即可。编辑器里的代码如下,注意最后一句的作用是把它们加到selectionAction列表里以响应选择事件:

IAction action=new AlignmentAction((IWorkbenchPart)this,PositionConstants.LEFT);
getActionRegistry().registerAction(action);
getSelectionActions().add(action.getId());
…

AlignmentAction的构造方法的参数是编辑器本身和一个代表对齐方式的整数,后者可以是 PositionConstants.LEFT、CENTER、RIGHT、TOP、MIDDLE、BOTTOM中的一个; MatchSizeAction有两个子类,MatchWidthAction和MatchHeightAction,你可以使用它们达到只调整宽度或高度的目的。下图是添加在工具条中的按钮,左边六个为位置对齐,最后两个为尺寸对齐,请注意,当选择多个图形时,被六个黑点包围的那个称为”主选择”,对齐时以该图形所在位置和大小为准做调整。

图7 位置对齐和尺寸对齐

由于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网站)

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

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

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

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

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

图1 使用XYLayout的应用程序

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

public class Node extends Element implements IPropertySource {
    protected Point location = new Point(0, 0);//位置
    protected Dimension size = new Dimension(100, 150);//尺寸
    protected String name = "Node";//标签
    protected List 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。

图2 容器Subject3处于展开状态

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

图3 容器Subject3处于折叠状态

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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