导出Plugin的一点心得

Eclipse升级到了3.1以后,在Plugin manifest editor上做了相当大的改动,可能是刚刚完全支持OSGi的缘故吧,感觉这个editor的小bug挺多的。比如很多情况下表示dirty的星号不会消失,有时会弹出带红叉的对话框等等,不过我相信在eclipse 3.2版本里会得到解决,因为关于它的bug report已经不少了。

除了一些小bug以外,我发现在3.1里导出一个Plugin也变得有些“莫名其妙”,最近经过实验摸索,得到以下一些心得:

1、开发plugin的时候,不要在project properties的java build path里做任何修改,应该在plugin配置文件的dependencies、runtime和build这几个部分添加。

2、在build页上面部分,Library的缺省情况是".",我发现有时候指定为"."会造成class不被输出。比较保险的方法是指定一个.jar名字,对应的folder还是src/,勾中下面的Include selected library选项。

3、在build页中间部分指定要输出的文件,一般除了缺省的那些以外,还要包含如lib、icons、log4j.properties等目录或文件。

4、在build页下面部分,也就是“Extra Classpath Entries”,指定你的应用程序用到的外部包,例如log4j.jar等等。

5、需要注意的是,如果Library里指定了abc.jar,则输出时会生成abc.jar这个文件,但Plugin在运行时会报ClassNotFound异常,原因是你没有把abc.jar指定到这个Plugin的classpath里。解决的办法是在Manifest.mf文件的Bundle-ClassPath项里手动加上abc.jar,例如“Bundle-ClassPath: abc.jar,lib/log4j-1.2.11.jar”这样。

6、若你有两个Plugin,多个的情况也一样,假设B依赖A,那么不仅在B的dependencies里要加上A,还要保证A的Runtime页里export了B所需的所有包,也就是在A的Manifest.mf文件里指定正确的“Export-Package”项。

7、输出为目录或输出为单个jar包在运行时没有什么区别,感觉jar包方便一些,输出的jar包名就是项目名。

希望对你有帮助。

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

写代码的代码:JET

用过EMF的人想必都对它的代码生成功能印象深刻吧,有没有想过这是怎样实现的呢?

代码生成一般是通过写好的模板,在用户输入一些限制条件后,由程序把这两者结合起来得到需要的代码。EMF也是这样,它内置了一些模板(放在 org.eclipse.emf.codegen.ecore里),我们通过Java Interface或XML Schema文件建立ecore模型后(这一步由org.eclipse.emf.codegen.ui支持),插件 org.eclipse.emf.codegen帮我们生成model、edit、editor和test这几个新的插件,这些生成的插件就可以被用来构 造应用程序了。

EMF使用JET(Java Emitter Templates)进行代码生成,JET使用的模板文件与JSP格式几乎完全一致,像JSP里有application这样的隐含变量一样,在JET模 板文件里可以使用argument这个隐含变量,它代表用户的输入参数(对EMF来说就是ecore模型)。下面是一个最简单的带有参数的模板文件 hello.txtjet,JET模板文件的扩展名一般是要生成文件类型再加上jet。

<%@ jet package="hello" class="GreetingTemplate" %>
Hello, <%=argument%>!

只要给这个模板文件一个参数,JET就能帮我们生成一个字符串,其内容是“Hello,参数!”。如果我们定义一个.javajet的模板,配合适当的参数,再把生成的字符串保存为文件,就得到了java代码,EMF的代码生成过程就是如此。

自己实现代码生成器的过程如下:创建一个Plugin项目,编写模板文件.javajet,一般放在templates目录下;定义元模型,这个元 模型要与模板相匹配,模板的argument一般就是元模型最外层的container;构造一个用户界面,让用户可以构造元模型的实例;提供一个 Action比如按钮,用户按下即开始生成代码。

代码的生成是由JETEmitter完成的,它会在workbench里先生成一个.JETEmitter项目,把模板转换为java类(这个过程类似JSP转换为Servlet),然后调用这些类的generate()方法得到结果。

我做了一个生成.txt文件的生成器插件,点此下载。安装后在Eclipse主菜单里选File->New->Others,在New对话框里选择Sample Wizards下的Echo filename text file,新建的文本文件内容里会包含文件名。

因为用途有限,JET的资料不是很多,这里有两篇:链接1链接2,其中后者在EMF的帮助里也找得到。

最后补充一个Tip,在plugin里访问一个相对路径文件的方法如下:

String base = Platform.getBundle(pluginId).getEntry("/").toString();
String relativeUri = "templates/echo.txtjet";
JETEmitter emitter = new JETEmitter(base + relativeUri, getClass().getClassLoader());

更正(08/23),上面说的方法只对JETEmitter有效,得到的路径是Platform内部路径而非本地路径,这里提供另一个方式可以得到本地路径:

Platform.asLocalURL(Platform.getBundle(pluginId).getEntry("/log4j.properties"))

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

在SWT里显示AWT对象

今天遇到一个问题,就是要在一个Eclipse插件里显示JFreeChart的图形,因为后者是基于Java2D的,要把图形显示在SWT应用程序里需要利用SWT-AWT桥接器来实现,虽说桥接的方式多半会伴随着性能下降,但总归是一个解决方法。

代码并不复杂,以下是一个片断:

public void createPartControl(Composite parent) {
    parent.setLayout(new FillLayout(SWT.VERTICAL));
    Composite drawarea = new Composite(parent, SWT.EMBEDDED);
    drawarea.setLayout(new FillLayout());
    Frame canvasFrame = SWT_AWT.new_Frame(drawarea);
    canvas = new java.awt.Canvas() {
        public void paint(Graphics g) {
            super.paint(g);
            if (chart != null)
                chart.draw((Graphics2D) g, getBounds());
        }
    };
    canvasFrame.add(canvas);
}

关键之处在于SWT_AWT.new_Frame()方法,得到的是一个java.awt.Frame对象,要显示的AWT内容都放在它上面就好。

BTW, SWT下免费的图表工具好象很少,只能暂时先这样使用JFreeChart了。

Update: 如果要在SWT里显示带有动画效果的AWT图形,最好在Frame上先放一个JPanel这样的带有双缓冲的控件,否则图象在运动时会产生明显的闪烁。

搬家前链接:https://www.cnblogs.com/bjzhanghao/archive/2005/07/17/194710.html

由模型生成GEF应用程序:Merlin

接触和使用过EMF的朋友都知道,只要定义好ecore模型,就能够利用EMF的代码生成工具得到一个可用编辑器,而ecore模型可以从rose模型、java接口或xml schema很方便的转换生成。不过,虽然得到的编辑器从功能上来说是足够了,但针对不同的需求还须要进行定制,这里的工作并不少,如果想定制为图形化编辑器要改的就更多了,基本上相当于重写的工作量。

Merlin开源项目可以为我们节约不少的时间,它是一个基于EMF的模型转换和代码生成工具。EMF本身具有生成代码的功能(例如生成模型java代码、编辑框架和编辑器),Merlin是对它的增强,支持生成GEF实现的图形化编辑器。在介绍EMF的各种资料里经常可以看到图书馆的例子,它的rose模型可以通过这个链接下载到,这里就简单介绍一下利用这个模型生成GEF应用程序的过程。我用的环境是Eclipse 3.1M7、GEF 3.1M7和EMF 2.1.0(注意:EMF用3月份版本会报错,升级为5月26的版本正常)。

首先新建一个EMF项目,名称叫library就可以,选择rose模型导入方式,然后输入URL或本地文件名(library.mdl)就可以得到library.ecore和library.genmodel文件,其中前者是ecore模型(元模型为ecore),后者是与其同步的供EMF代码生成器使用的一个模型。需要的话,可以利用缺省编辑器对.genmodel文件里的各个节点进行一些设置,如包名、属性的性质等等。

然后利用genmodel编辑器从library.genmodel生成模型代码、编辑框架和编辑器,这样我们就得到了三个完整的项目(library、library.edit和library.editor),后面将要生成的GEF编辑器会依赖前两个。

现在利用新建向导创建一个GEF Generation Model(在Merlin Tools->GEF类别下),名字取为library.gefgenmodel,输入文件就是上一步得到的library.genmodel文件,向导结束后即可得到用来生成GEF编辑器的模型文件。在这个文件里可以对将要生成的程序进行初步设置,例如各种类名、包名、Palette里的工具项、EditPart上都安装哪些EditPolicy以及对应的图形等等。

最后,在.gefgenmodel文件编辑器里选中Library:Gen GEF Model节点,点鼠标右键激活上下文菜单,执行里面的“Generate”命令即可生成新的library.gef项目,这个项目包含一个完整GEF应用程序应该具有的各部分代码,但其中一些类是与merlin相关的,例如生成的LibraryEditor继承自com.metys.merlin.generation.gef.editor.GEFEditor,LibraryEditPart也是如此,我觉得如果是完全独立的就更好了,否则还要对Merlin有所了解……

一般的步骤就是上面这样,如果有更详细的修改就要在得到的代码里进行了,和修改EMF生成的编辑器是一样的,修改后不要忘记去掉前面的“generated”标记。其实如果不对.genmodel和.gefgenmodel做任何修改,全部使用缺省设置也能够得到可以工作的代码,只是没有“个性”而已,对图书馆模型的这种“标准”实现如下图。

file
图1 用Merlin生成的图书馆编辑器,基本功能都有了

生成GEF编辑器只是Merlin的一个小功能而已,Merlin的作者一定是Jet高手,并且对模型映射有所研究,而后者在MDA中具有重要作用。从Merlin的包名来看,它以前大概是某公司的产品,后来转为开源项目继续开发的,虽然目前开发者名单里只有两个成员,但版本的发布却十分频繁,在我看来质量也不错,对于使用EMF的开发者来说应该是个有用的工具。

BTW,Eclipse.org上新增了一篇教你如何结合EMF与GEF的文章:Using GEF With EMF,我就是从这里面发现Merlin的。

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

[Eclipse]Plugin.xml转换为Manifest.mf的规则

为了更好的实现动态加载/卸载插件,Eclipse从3.0开始实现OSGI规范,原先在plugin.xml文件里定义的很多内容都被推荐放在manifest.mf文件里,只有是例外。下面是两种文件格式中各元素的对照表:

plugin.xml tag/attribute
manifest.mf header
<plugin id=> Bundle-SymbolicName
<plugin version=> Bundle-Version
<plugin name=> Bundle-Name
<plugin provider=> Bundle-Vendor
<plugin class=> Bundle-Activator
<fragment plugin-id=> Fragment-Host
<fragment plugin-version=> Fragment-Host: ; bundle-version=
, Require-Bundle
, Bundle-ClassPath

具体的转换方法在这里可以看到,也就是说,我们在Eclipse 3.0以上版本中开发插件的时候,最好使用manifest.mf文件的方式(实际上Eclipse自带的Plugin Manifest Editor在这方面已经帮我们做了不少工作,新版本的Eclipse在创建新项目时会自动把可以放在manifest.mf文件中的内容转移过去)。

[Eclipse]GEF入门系列(十一、树的一个实现)

两天前GEF发布了3.1M7版本,但使用下来发现和M6没有什么区别,是不是主要为了和Eclipse版本相配套?希望3.1正式版早日发布,应该会新增不少内容。上一篇帖子介绍了如何实现表格功能,在开发过程中,另一个经常用到的功能就是树,虽然SWT提供了标准的树控件,但使用它完成如组织结构图这样的应用还是不够直观和方便。在目前版本(3.1M7)的GEF中虽然没有直接支持树的实现,但Draw2D提供的例子程序里却有我们可以利用的代码(org.eclipse.draw2d.examples.tree.TreeExample,运行界面见下图),通过它可以节约不少工作量。

file
图1 Draw2D例子中的TreeExample

记得数年前曾用Swing做过一个组织结构图的编辑工具,当时的实现方式是让画布使用XYLayout,在适当的时候计算和刷新每个树节点的位置,算法的思想则是深度优先搜索,非树叶节点的位置由其子节点的数目和位置决定。我想这应该是比较直观的方法吧,但是这次看了Draw2D例子里的实现觉得也很有道理(递归),以前没想到过。在这个例子里树节点图形称为TreeBranch,它包含一个PageNode(表现为带有折角的矩形)和一个透明容器contentsPane,(一个Layer,用来放置子节点)。在一般情况下,TreeBranch本身使用名为NormalLayout的布局管理器将PageNode放在子节点的正上方,而contentsPane则使用名为TreeLayout的布局管理器计算每个子节点应在的位置。所以我们看到的整个树实际上是由很多层子树叠加而成的,任何一个非叶节点对应的图形的尺寸都等于以它为根节点的子树所占区域的大小。

从这个例子里我们还看到,用户可以选择使用横向或纵向组织树(见图2),可以压缩各节点之间的空隙,每个节点可以横向或纵向排列子节点,还可以展开或收起子节点,等等,这为我们实现一个方便好用的树编辑器提供了良好的基础(视图部分的工作大大简化了)。

file
图2 纵向组织的树

这里要插一句,Draw2D例子中提供的这些类的具体内容我没有仔细研究,相当于把它们当作Draw2D API的一部分来用了(包括TreeRoot、TreeBranch、TreeLayout、BranchLayout、NormalLayout、HangingLayout、PageNode等几个类,把代码拷到你的项目中即可使用),因为按照GEF 3.1的计划表,它们很有可能以某种形式出现在正式版的GEF 3.1里。下面介绍一下我是如何把它们转换为GEF应用程序的视图部分从而实现树编辑器的。

首先从模型部分开始。因为树是由一个个节点构成的,所以模型中最主要的就是节点类(我称为TreeNode),它和例子里的TreeBranch图形相对应,它应该至少包含nodes(子节点列表)和text(显示文本)这两个属性;例子里有一个TreeRoot是TreeBranch的子类,用来表示根节点,在TreeRoot里多了一些属性,如horizontal、majorSpacing等等用来控制整个树的外观,所以模型里也应该有一个继承TreeNode的子类,而实际上这个类就应该是编辑器的contents,它对应的图形TreeRoot也就是一般GEF应用程序里的画布,这个地方要想想清楚。同时,虽然看起来节点间有线连接,但这里我们并不需要Connection对象,这些线是由布局管理器绘制的,毕竟我们并不需要手动改变线的走向。所以,模型部分就是这么简单,当然别忘了要实现通知机制,下面看看都有哪些EditPart。

与模型相对应,我们有TreeNodePart和TreeRootPart,后者和前者之间也是继承关系。在getContentPane()方法里,要返回TreeBranch图形所包含的contentsPane部分;在getModelChildren()方法里,要返回TreeNode的nodes属性;在createFigure()方法里,TreeNodePart应返回TreeBranch实例,而TreeRootPart要覆盖这个方法,返回TreeRoot实例;另外要注意在refreshVisuals()方法里,要把模型的当前属性正确反映到图形中,例如TreeNode里有反映节点当前是否展开的布尔变量expanded,则refreshVisuals()方法里一定要把这个属性的当前值赋给图形才可以。以下是TreeNodePart的部分代码:

public IFigure getContentPane() {
    return ((TreeBranch) getFigure()).getContentsPane();
}

protected List getModelChildren() {
    return ((TreeNode) getModel()).getNodes();
}

protected IFigure createFigure() {
    return new TreeBranch();
}

protected void createEditPolicies() {
    installEditPolicy(EditPolicy.COMPONENT_ROLE, new TreeNodeEditPolicy());
    installEditPolicy(EditPolicy.LAYOUT_ROLE, new TreeNodeLayoutEditPolicy());
    installEditPolicy(EditPolicy.SELECTION_FEEDBACK_ROLE, new ContainerHighlightEditPolicy());
}

上面代码中用到了几个EditPolicy,这里说一下它们各自的用途。实际上,从Role中已经可以看出来,TreeNodeEditPolicy是用来负责节点的删除,没有什么特别;TreeNodeLayoutEditPolicy则复杂一些,我把它实现为ConstrainedLayoutEditPolicy的一个子类,并实现createAddCommand()和getCreateCommand()方法,分别返回改变节点的父节点和创建新节点的命令,另外我让createChildEditPolicy()方法返回NonResizableEditPolicy的实例,并覆盖其createSelectionHandles()方法如下,以便在用户选中一个节点时用一个控制点表示选中状态,不用缺省边框的原因是,边框会将整个子树包住,不够美观,并且在多选的时候界面比较混乱。

protected List createSelectionHandles() {
    List list=new ArrayList();
    list.add(new ResizeHandle((GraphicalEditPart)getHost(), PositionConstants.NORTH));
    return list;
}

选中节点的效果如下图,我根据需要改变了树节点的显示(修改PageNode类):

file
图3 同时选中三个节点(Node2、Node3和Node8)

最后一个ContainerHighlightEditPolicy的唯一作用是当用户拖动节点到另一个节点区域中时,加亮显示后者,方便用户做出是否应该放开鼠标的选择。它是GraphicalEditPolicy的子类,部分代码如下,如果你看过Logic例子的话,应该不难发现这个类就是我从那里拿过来然后修改一下得到的。

protected void showHighlight() {
    ((TreeBranch) getContainerFigure()).setSelected(true);
}

public void eraseTargetFeedback(Request request) {
    ((TreeBranch) getContainerFigure()).setSelected(false);
}

好了,现在树编辑器应该已经能够工作了。为了让用户使用更方便,你可以实现展开/收起子节点、横向/纵向排列子节点等等功能,在视图部分Draw2D的例子代码已经内置了这些功能,你要做的就是给模型增加适当的属性。我这里的一个截图如下所示,其中Node1是收起状态,Node6纵向排列子节点(以节省横向空间)。

file
图4 树编辑器的运行界面

这个编辑器我花一天时间就完成了,但如果不是利用Draw2D的例子,相信至少要四至六天,而且缺陷会比较多,功能上也不会这么完善。我感觉在GEF中遇到没有实现过的功能前最好先找一找有没有可以利用的资源,比如GEF提供的几个例子就很好,当然首先要理解它们才谈得上利用。

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

[Eclipse]GEF入门系列(十、表格的一个实现)

在目前的GEF版本(3.1M6)里,可用的LayoutManager还不是很多,在新闻组里经常会看到要求增加更多布局的帖子,有人也提供了自己的实现,例如这个GridLayout,相当于SWT中GridLayout的Draw2D实现,等等。虽然可以肯定GEF的未来版本里会增加更多的布局供开发者使用(可能需要很长时间),然而目前要用GEF实现表格的操作还没有很直接的办法,这里说说我的做法,仅供参考。

实现表格的方法决定于模型的设计,初看来我们似乎应该有这些类:表格(Table)、行(Row)、列(Column)和单元格(Cell),每个模型对象对应一个EditPart,以及一个Figure,TablePart应该包含RowPart和ColumnPart,问题是RowFigure和ColumnFigure会产生交叉,想象一下你的表格该使用什么样的布局才能容纳它们?使用这样的模型并非不能实现(例如使用StackLayout),但我认为这样的模型需要做的额外工作会很多,所以我使用基于列的模型。

在我的表格模型里,只有三种对象:Table、Column和Cell,但Column有一个子类HeaderColumn表示第一列,同时Cell有一个子类HeaderCell表示位于第一列里的单元格,后面这两个类的作用主要是模拟实现对行的操作--把对行的操作都转换为对HeaderCell的操作。例如,创建一个新行转换为在第一列中增加一个新的单元格,当然在这同时我们要让程序给其余每一列同样增加一个单元格。

file
图1 表格编辑器

现在的问题就是怎样让用户察觉不到我们是在对单元格而不是对行操作。需要修改的地方有这么几处:一是创建新行或改变行位置时显示与行宽一致的插入提示线,二是在用户点击位于第一列中的单元格(HeaderCell)时显示为整个行被选中,三是允许用户通过鼠标拖动改变行高度,最后是在改变行所在位置或大小的时候显示正确的回显(Feedback)图形。下面依次介绍它们的实现方法。

调整插入线的宽度

在我们的调色板里有一个Row工具项,代表表格中的一个行,它的作用是创建新的行。注意这个工具项的名字虽然叫Row,实际上用它创建的是一个HeaderCell对象,创建它的代码如下:

tool = new CombinedTemplateCreationEntry("Row", "Create a new Row", HeaderCell.class, new SimpleFactory(HeaderCell.class), CbmPlugin.getImageDescriptor(IConstants.IMG_ROW), null);

创建新行的方式是从调色板里拖动它到想要的位置。在拖动过程中,随着鼠标所在位置的变化,编辑器应该能显示一条直线,用来表示如果此时放开鼠标新行将插入的位置。由于这个工具代表的是一个单元格,所以缺省情况下GEF会显示一条与单元格长度相同的插入线,为了让用户感觉到是在插入行,我们必须改变插入线的宽度。具体的方法是在HeaderColumnPart的负责Layout的那个EditPolicy(继承FlowLayoutEditPolicy)中覆盖showLayoutTargetFeedback()方法,修改后的代码如下:

protected void showLayoutTargetFeedback(Request request) {
    super.showLayoutTargetFeedback(request);
    // Expand feedback line's width
    Diagram diagram = (Diagram) getHost().getParent().getModel();
    Column column = (Column) getHost().getModel();
    Point p2 = getLineFeedback().getPoints().getPoint(1);
    p2.x = p2.x + (diagram.getColumns().size() - 1) * (column.getWidth() + IConstants.COLUMN_SPACING);
    getLineFeedback().setPoint(p2, 1);
}

其中p2代表插入线中右边的那个点,我们将它的横坐标加上一个量即可增加这条线的长度,这个量和表格当前列的数目有关,和列间距也有关,计算的方法看上面的代码很清楚。这样修改后的效果如下图所示,拖动行到新的位置时也会使用同样的插入线。

file
图2 与表格同宽的插入线

选中整个行

缺省情况下,鼠标点击一个单元格会在这个单元格四周产生一个黑色的边框,用来表示被选中的状态。为了让用户能选中整个行,要修改HeaderCell上的EditPolicy。在前面一篇帖子里已经专门讲过,单元格作为列的子元素,要修改它的EditPolicy就要在ColumnPart的EditPolicy的createChildEditPolicy()方法里返回自定义的EditPolicy,这里我返回的是自己实现的DragRowEditPolicy,它继承自GEF内置的ResizableEditPolicy类,它将被HeaderColumnPart加到子元素HeaderCellPart的EditPolicy列表。现在就来修改DragRowEditPolicy以实现整个行的选中。

首先要说明,在GEF里一个图形被选中时出现的黑边和控制点称为Handle,其中黑边称为MoveHandle,用于移动图形;而那些控制点称为ResizeHandle,用于改变图形的尺寸。要改变黑边的尺寸(由单元格的宽度扩展为整个表格的宽度),我们得继承MoveHandle并覆盖它的getLocator()方法,下面的代码是我的实现:

public class RowMoveHandle extends MoveHandle {
    public RowMoveHandle(GraphicalEditPart owner, Locator loc) {
        super(owner, loc);
    }
    public RowMoveHandle(GraphicalEditPart owner) {
        super(owner);
    }
    //计算得到选中行所占的位置,传给MoveHandleLocator作为参考
    public Locator getLocator() {
        IFigure refFigure = new Figure();
        Rectangle rect=((HeaderCellPart) getOwner()).getRowBound();
        translateToAbsolute(rect);
        refFigure.setBounds(rect);
        return new MoveHandleLocator(refFigure);
    }
}

在getLocator()方法里,我们调用了HeaderCellPart的getRowBound()方法用于得到选中行的位置和尺寸,这个方法的代码如下(放在HeaderCellPart里是因为在Handle里通过getOwner()可以很容易得到EditPart对象),行尺寸的计算方法与前面插入线的情况类似:

public Rectangle getRowBound(){
    Rectangle rect = getFigure().getBounds().getCopy();
    Diagram diagram = (Diagram) getParent().getParent().getModel();
    Column column = (Column) getParent().getModel();
    rect.setSize(diagram.getColumns().size() * column.getWidth() + (diagram.getColumns().size() - 1) * IConstants.COLUMN_SPACING, rect.getSize().height);
    return rect;
}

有了这个RowMoveHandle,只要把它代替原来缺省的MoveHandle加到HeaderColumnCell上即可,具体的方法就是覆盖DragRowEditPolicy的createSelectionHandles()方法,ResizableEditPolicy对这个方法的缺省实现是加一个黑框和八个控制点,而我们要改成下面这样:

protected List createSelectionHandles() {
    List l = new ArrayList();
    //四周的黑色边框
    l.add(new RowMoveHandle((GraphicalEditPart) getHost()));
    //下方的控制点
    l.add(new RowResizeHandle((GraphicalEditPart) getHost(), PositionConstants.SOUTH));
    return l;
}

代码里用到的RowResizeHandle类是控制点的自定义实现,在下面很快会讲到。现在,用户可以看到整个行被选中的效果了。

file
图3 选中整个行

改变行的高度

改变行高度比较自然的方式是让用户选中行后自由拖动下面的边。前面说过,GEF里的ResizeHandle具有调整图形尺寸的功能,美中不足的是ResizeHandle表现为黑色(或白色,非主选择时)的小方块,而我们希望它是一条线就好了,这样鼠标指针只要放在选中行的下边上就会变成改变尺寸的样子。这就需要我们实现刚才提到的RowResizeHandle类了,它是ResizeHandle的子类,代码如下:

public class RowResizeHandle extends ResizeHandle {
    public RowResizeHandle(GraphicalEditPart owner, int direction) {
        super(owner, direction);
        //改变控制点的尺寸,使之变成一条线
        setPreferredSize(new Dimension(((HeaderCellPart) owner).getRowBound().width, 2));
    }
    public RowResizeHandle(GraphicalEditPart owner, Locator loc, Cursor c) {
        super(owner, loc, c);
    }
    //缺省实现里控制点有描边,我们不需要,所以覆盖这个方法
    public void paintFigure(Graphics g) {
        Rectangle r = getBounds();
        g.setBackgroundColor(getFillColor());
        g.fillRectangle(r.x, r.y, r.width, r.height);
    }
    //与前面RowMoveHandle类似,但返回RelativeHandleLocator以使线显示在图形下方
    public Locator getLocator() {
        IFigure refFigure = new Figure();
        Rectangle rect=((HeaderCellPart) getOwner()).getRowBound();
        translateToAbsolute(rect);
        refFigure.setBounds(rect);
        return new RelativeHandleLocator(refFigure, PositionConstants.SOUTH);
    }
    //不论是否为主选择,都使用黑色填充
    protected Color getFillColor() {
        return ColorConstants.black;
    }
}

这样,我们就把控制点拉成了控制线,因为它的位置与选择框(RowMoveHandle)的一部分重合,所以在界面上感觉不到它的存在,但用户可以通过它控制行的高度,见下图。

file
图4 改变行高的提示

正确的回显图形

我们知道,在拖动图形和改变图形尺寸的时候,GEF会显示一个"影图"(Ghost Shape)作为回显,也就是显示图形的新位置和尺寸信息。因为操作行时目标对象实际是单元格,所以在缺省情况下回显也是单元格的样子(宽度与列宽相同)。为此,在DragRowEditPolicy里要覆盖getInitialFeedbackBounds()方法,这个方法返回的Rectangle决定了鼠标开始拖动时回显图形的初始状态,见以下代码:

protected Rectangle getInitialFeedbackBounds() {
    return ((HeaderCellPart) getHost()).getRowBound();
}

这时的回显见下图,在拖动行时也使用同样的回显。

file
图5 改变行高时的回显

经过上面的修改,对HeaderCell的操作在界面上已经完全表现为对表格行的操作了。这些操作的结果会转换为一些Command,包括CreateHeaderCellCommand(创建新行,你也可以命名为CreateRowCommand)、MoveHeaderCellCommand(移动行)、DeleteHeaderCellCommand(删除行)和ChangeHeaderCellHeightCommand(改变行高)等,在这些类里要对所有列执行同样的操作(例如改变HeaderCell的高度的同时改变同一行中其他单元格的高度),这样在界面上才能保持表格的外观,详细的代码没有必要贴在这里了。

P.S.曾经考虑过另一种实现表格的方法,就是模型里只有Table和Cell两种对象,然后自己写一个TableLayout负责单元格的布局。同样是因为修改的工作量相对比较大而没有采用,因为那样的话行和列都要使用自定义方式处理,而这篇贴子介绍的方法只关心行的处理就可以了。当然,这里说的也不是什么标准实现,不过效果还是不错的,而且确实可以实现,如果你有类似的需求可以作为参考。

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

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

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

可折叠调色板

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

file
图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);
}

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

file
图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里就提供了显示这种辅助网格的功能。

file
图3 辅助编辑网格

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

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

几何对齐

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

file
图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);
}

标尺和辅助线

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

file
图5 标尺和辅助线

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

file
图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创建在其上
protected void createGraphicalViewer(Composite parent) {
    rulerComp = new RulerComposite(parent, SWT.NONE);
    super.createGraphicalViewer(rulerComp);
    rulerComp.setGraphicalViewer((ScrollingGraphicalViewer) getGraphicalViewer());
}

// 覆盖getGraphicalControl返回RulerComposite实例
protected Control getGraphicalControl() {
    return rulerComp;
}

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

getGraphicalViewer().setProperty(RulerProvider.PROPERTY_VERTICAL_RULER,new SubjectRulerProvider(diagram.getLeftRuler()));
getGraphicalViewer().setProperty(RulerProvider.PROPERTY_HORIZONTAL_RULER,new SubjectRulerProvider(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,你可以使用它们达到只调整宽度或高度的目的。下图是添加在工具条中的按钮,左边六个为位置对齐,最后两个为尺寸对齐,请注意,当选择多个图形时,被六个黑点包围的那个称为"主选择",对齐时以该图形所在位置和大小为准做调整。

file
图7 位置对齐和尺寸对齐

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

[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