世界地球日

今天是第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();