首页 Eclipse插件开发:GEF入门系列宝典

Eclipse插件开发:GEF入门系列宝典

举报
开通vip

Eclipse插件开发:GEF入门系列宝典Eclipse插件开发:GEF入门系列宝典 GEF入门系列(一、Draw2D) 由于工作的需要,最近开始研究GEF(Graphical Editor Framework)这个框架,它可以用来给用户提供图形化编辑模型的功能,从而提升用户体验,典型的应用如图形化的流程设计器、UML类图编辑器等等。其实一年多来我们做的项目都是和它有关的,只是之前我具体负责的事情和它没什么关系。那时也看过黄老大写的代码,EMF和GEF混在一起特别晕,没能坚持看下去。这次自己要动手做了,正好趁此机会把它搞明白,感觉GEF做出来的东西给人...

Eclipse插件开发:GEF入门系列宝典
Eclipse插件开发:GEF入门系列宝典 GEF入门系列(一、Draw2D) 由于工作的需要,最近开始研究GEF(Graphical Editor Framework)这个框架,它可以用来给用户提供图形化编辑模型的功能,从而提升用户体验,典型的应用如图形化的流程 设计 领导形象设计圆作业设计ao工艺污水处理厂设计附属工程施工组织设计清扫机器人结构设计 器、UML类图编辑器等等。其实一年多来我们做的项目都是和它有关的,只是之前我具体负责的事情和它没什么关系。那时也看过黄老大写的代码,EMF和GEF混在一起特别晕,没能坚持看下去。这次自己要动手做了,正好趁此机会把它搞明白,感觉GEF做出来的东西给人很专业的感觉,功能也很强大,应该挺有前途的。此外,GEF里用到了很多经典模式,最突出的如大量应用Command模式,方便的实现Undo/Redo功能等等,通过学习GEF,等于演练了这些模式,比只是看看书写几个类那种学习方式的效果好很多。 现在网上关于GEF的文章和教程还不是很多(比起一年前还是增加了几篇),基本上都是eclipse.org上的那些,其中少数几篇有中文版,中文的原创就属于凤毛麟角了,市场上似乎也没有这方面的成书。GEF SDK里自带的文档则比较抽象,不适合入门。我觉得最好的入门方法是结合具体的例子,一边看代码,一边对照文档,然后自己再动手做一做。当然这个例子要简单点才好,像GEF的那个logic的例子就太复杂了,即使是flow(运行界面见下图)我觉得也有点大;另外例子要比较 规范 编程规范下载gsp规范下载钢格栅规范下载警徽规范下载建设厅规范下载 的,否则学成错误的路子以后还要花时间改就不值得了。 用GEF编写的流程编辑器 GEF的结构决定了GEF应用程序的复杂性,即使最最简单的GEF程序也包含五六个包和十几个类,刚开始接触时有点晕是很正常的。我找到一个还不错的例子,当然它很简单了,如果你现在就想自己试试GEF,可以点这里下载一个zip包(若已无法下载请用这个链接),展开后是六个项目(pt1,pt2,„,pt6),每一个是在前面一个的基础上增加一些功能得到的, pt1是最简单的一个,这样你就可以看到那些典型的功能(例如DirectEdit、Palette等等) 在GEF里应该怎样实现了。关于这个例子的更多信息请看作者blog上的说明: “Back in March, I talked a little about my initial attempts writing an Eclipse Graphical Editor Framework (GEF) application. I wanted, then, to write a tutorial that essentially walked the reader through the various stages of the development of my first application. I even suggested some kind of versioned literate programming approach to writing the tutorial and the code at the same time. I haven't had time since then to make any progress, but I did get the GEF application to the stage where I had put together a snapshot at each of six milestones. A few people have written to me over the last six months asking the status of my tutorial and I've sent them my six snapshots as a starting point. It makes sense for me to just to offer them here. You can download a ZIP file with the six snapshots at Hopefully they are still useful, even without a surrounding tutorial.” 需要注意一点,这个例子应该是在Eclipse 2.1里写的,所以如果你想在Eclipse 3里运行 这个例子,要修改plugin.xml里的dependencies为: 再修改一下DiagramCreationWizard这个类finish()方法里page.openEditor(newFile); 这句改为page.openEditor(new FileEditorInput(newFile),"com.jtauber.river.editor");,还有一些warning不太影响, 可以不用管。 或者如果你不是特别着急的话,留意我这个半新手写的GEF入门系列帖子,说不定能引起你 更多的共鸣,也是一个办法吧。 GEF的学习周期是比较长的,学之前应该有这个心理准备。特别是如果你没有开发过Eclipse 插件,那么最好先花时间熟悉一下Eclipse的插件体系结构,这方面的文章还是很多的,也 不是很难,基本上会开发简单的Editor就可以了,因为GEF应用程序一般都是在Editor 里进行图形编辑的。另外,绝大多数GEF应用程序都是基于Draw2D的,可以说GEF离不开 Draw2D,而后者有些概念很难搞明白,加上其文档比GEF更少,所以我会从Draw2D开始说 起,当然不能讲得很深入,因为我自己也是略知皮毛而已。 说实话,我对写这个系列不太有信心,因为自己也是刚入门而已。但要是等到几个月后再写, 很多心得怕是讲不出来了。所以还是那句话,有什么写错的请指正,并且欢迎交流。 关于Java2D相信大家都不会陌生,它是基于AWT/Swing的二维图形处理包, JDK附带的示例程序向我们展示了Java2D十分强大的图形处理能力。在Draw2D出现以前,SWT应用程序在这方面一直处于下风,而Draw2D这个SWT世界里的Java2D改变了这种形势。 可能很多人还不十分了解GEF和Draw2D的关系:一些应用程序是只使用Draw2D,看起来却和GEF应用程序具有相似的外观。原因是什么,下面先简单解释一下: GEF是具有 标准 excel标准偏差excel标准偏差函数exl标准差函数国标检验抽样标准表免费下载红头文件格式标准下载 MVC(Model-View-Control)结构的图形编辑框架,其中Model由我们自己根据业务来设计,它要能够提供某种模型改变 通知 关于发布提成方案的通知关于xx通知关于成立公司筹建组的通知关于红头文件的使用公开通知关于计发全勤奖的通知 的机制,用来把Model的变化告诉Control层;Control层由一些EditPart实现,EditPart是整个GEF的核心部件,关于EditPart的机制和功能将在以后的帖子里介绍;而View层(大多数情况下)就是我们这里要说的Draw2D了,其作用是把Model以图形化的方式表现给使用者。 虽然GEF可以使用任何图形包作为View层,但实际上GEF对Draw2D的依赖是很强的。举例来说:虽然EditPart(org.eclipse.gef.EditPart)接口并不要求引入任何Draw2D的类,但我们最常使用的AbstractGraphicalEditPart类的createFigure()方法就需要返回IFigure类型。由于这个原因,在GEF的SDK中索性包含了Draw2D包就不奇怪了,同样道理,只有先了解Draw2D才可能掌握GEF。 这样,对于一开始提出的问题可以总结如下:Draw2D是基于SWT的图形处理包,它适合用作GEF的View层。如果一个应用仅需要显示图形,只用Draw2D就够了;若该应用的模型要求以图形化的方式被编辑,那么最好使用GEF框架。 现在让我们来看看Draw2D里都有些什么,请看下图。 图1 Draw2D的结构 Draw2D通过被称为LightweightSystem(以下简称LWS)的部件与SWT中的某一个Canvas实例相连,这个Canvas在Draw2D应用程序里一般是应用程序的Shell,在GEF应用程序里更多是某个Editor的Control(createPartControl()方法中的参数),在界面上我们虽然看不到LWS的存在,但其他所有能看到的图形都是放在它里面的,这些图形按父子包含关系形成一个树状的层次结构。 LWS是Draw2D的核心部件,它包含三个主要组成部分:RootFigure是LWS中所有图形的根,也就是说其他图形都是直接或间接放在RootFigure里的;EventDispatcher把Canvas上的各种事件分派给RootFigure,这些事件最终会被分派给适当的图形,请注意这个RootFigure和你应用程序中最顶层的IFigure不是同一个对象,前者是看不见的被LWS内部使用的,而后者通常会是一个可见的画布,它是直接放在前者中的;UpdateManager用来重绘图形,当Canvas被要求重绘时,LWS会调用它的performUpdate()方法。 LWS是连接SWT和Draw2D的桥梁,利用它,我们不仅可以轻松创建任意形状的图形(不仅仅限于矩形),同时能够节省系统资源(因为是轻量级组件)。一个典型的纯Draw2D应用程序代码具有类似下面的结构: //创建SWT的Canvas(Shell是Canvas的子类) Shell shell = new Shell(); shell.open(); shell.setText("A Draw2d application"); //创建LightweightSystem,放在shell上 LightweightSystem lws = new LightweightSystem(shell); //创建应用程序中的最顶层图形 IFigure panel = new Figure(); panel.setLayoutManager(new FlowLayout()); //把这个图形放置于LightweightSystem的RootFigure里 lws.setContents(panel); //创建应用程序中的其他图形,并放置于应用程序的顶层图形中 panel.add(); while (!shell.isDisposed ()) { if (!display.readAndDispatch ()) display.sleep (); } 接下来说说图形,Draw2D中的图形全部都实现IFigure(org.eclipse.draw2d.IFigure)接口,这些图形不仅仅是你看到的屏幕上的一块形状而已,除了控制图形的尺寸位置以外,你还可以监听图形上的事件(鼠标事件、图形结构改变等等,来自LWS的EventDispatcher)、设置鼠标指针形状、让图形变透明、聚焦等等,每个图形甚至还拥有自己的Tooltip,十分的灵活。 Draw2D提供了很多缺省图形,最常见的有三类:1、形状(Shape),如矩形、三角形、椭圆形等等;2、控件(Widget),如标签、按钮、滚动条等等;3、层(Layer),它们用来为放置于其中的图形提供缩放、滚动等功能,在3.0版本的GEF中,还新增了GridLayer和GuideLayer用来实现"吸附到网格"功能。在以IFigure为根节点的类树下有相当多的类,不过我个人感觉组织得有些混乱,幸好大部分情况下我们只用到其中常用的那一部分。 图2 一个Draw2D应用程序 每个图形都可以拥有一个边框(Border),Draw2D所提供的边框类型有GroupBoxBorder、TitleBarBorder、ImageBorder、ButtonBorder,以及可以组合两种边框的CompoundBorder等等,在Draw2D里还专门有一个Insets类用来表示边框在图形中所占的位置,它包含上下左右四个整型数值。 我们知道,一个图形可以包含很多个子图形,这些被包含的图形在显示的时候必须以某种方式被排列起来,负责这个任务的就是父图形的LayoutManager。同样的,Draw2D已经为我们提供了一系列可以直接使用的LayoutManager,如FlowLayout适合用于表格式的排列,XYLayout适合让用户在画布上用鼠标随意改变图形的位置,等等。如果没有适合我们应用的LayoutManager,可以自己定制。每个LayoutManager都包含某种算法,该算法将考虑与每个子图形关联的Constraint对象,计算得出子图形最终的位置和大小。 图形化应用程序的一个常见任务就是在两个图形之间做连接,想象一下UML类图中的各种连接线,或者程序流程图中表示数据流的线条,它们有着不同的外观,有些连接线还要显示名称,而且最好能不交叉。利用Draw2D中的Router、Anchor和Locator,可以实现多种连接样式,其中Router负责连接线的外观和操作方式,最简单的是设置Router为null(无Router),这样会使用直线连接,其他连接方式包括折线、具有控制点的折线等等(见图3),若想控制连接线不互相交叉也需要在Router中作文章。Anchor控制连接线端点在图形上的位置,即"锚点"的位置,最易于使用的是ChopBoxAnchor,它先假设图形中心为连接点,然后计算这条假 想连线与图形边缘的交汇点作为实际的锚点,其他Anchor还有EllipseAnchor、LabelAnchor和XYAnchor等等;最后,Locator的作用是定位图形,例如希望在连接线中点处以一个标签显示此连线的名称/作用,就可以使用MidpointLocator来帮助定位这个标签,其他Locator还有ArrowLocator用于定位可旋转的修饰(Decoration,例如PolygonDecoration)、BendpointerLocator用于定位连接控制点、ConnectionEndpointLocator用于定位连接端点(通过指定uDistance和vDistance属性的值可以设置以端点为原点的坐标)。 图3 三种Router的外观 此外,Draw2D在org.eclipse.draw2d.geometry包里提供了几个很方便的类型,如Dimension、Rectangle、Insets、Point和PointList等等,这些类型既在Draw2D内部广泛使用,也可以被开发人员用来简化计算。例如Rectangle表示的是一个矩形区域,它提供getIntersection()方法能够方便的计算该区域与另一矩形区域的重叠区域、getTransposed()方法可以得到长宽值交换后的矩形区域、scale()方法进行矩形的拉伸等等。在自己实现LayoutManager的时候,由于会涉及到比较复杂的几何计算,所以更推荐使用这些类。 以上介绍了Draw2D提供的大部分功能,利用这些我们已经能够画出十分漂亮的图形了。但对大多数实际应用来说这样还远远不够,我们还要能编辑它,并把对图形的修改反映到模型里去。为了漂亮的完成这个艰巨任务,GEF绝对是不二之选。从下一次开始,我们将正式进入GEF的世界。 参考资料: 1. GEF Developer Guide 2. Eclipse Development - Using the Graphical Editing Framework and the Eclipse Modeling Framework 3. Displaying a UML Diagram with Draw2D 二、GEF概述 在前面的帖子已经提到,GEF(Graphical Editor Framework)是一个图形化编辑框架,它允许开发人员以图形化的方式展示和编辑模型,从而提升用户体验。这样的应用程序有很多,例如:UML类图编辑器、图形化XML编辑器、界面设计工具以及图形化数据库结构设计工具等等。归结一下,可以发现它们在图形化编辑方面具有以下共同之处: 1. 提供一个编辑区域和一个工具条,用户在工具条里选择需要的工具,以拖动或单击的方 式将节点或连接放置在编辑区域; 2. 节点可以包含子节点; 3. 用户能够查看和修改某个节点或连接的大部分属性; 4. 连接端点锚定在节点上; 5. 提供上下文菜单和键盘命令; 6. 提供图形的缩放功能; 7. 提供一个大纲视图,显示编辑区域的缩略图,或是树状模型结构; 8. 支持撤消/重做功能; 9. 等等。 图1 基于GEF的界面设计工具(Visual Editor,VE)的工作界面 GEF最早是Eclipse的一个内部项目,后来逐渐转变为Eclipse的一个开源工具项目,Eclipse的不少其他子项目都需要它的支持。Eclipse 3.0版本花了很大功夫在从Platform中剥离各种功能部件上,包括GEF和IDE在内的很多曾经只能在Eclipse内部使用的工具成为可以独立使用的软件/插件包了。理论上我们是可以脱离Eclipse用GEF包构造自己的应用程序的,但由于它们之间天然的联系,而且Eclipse确实是一个很值得支持的开发平台,所以我还是推荐你在Eclipse中使用它。 GEF的优势是提供了标准的MVC(Model-View-Control)结构,开发人员可以利用GEF来完成以上这些功能,而不需要自己重新设计。与其他一些MVC编辑框架相比,GEF的一个主要设计目标是尽量减少模型和视图之间的依赖,好处是可以根据需要选择任意模型和视图的组合,而不必受开发框架的局限(不过实际上还是很少有脱离Draw2D的实现)。 现在来看看GEF是如何实现MVC框架的吧,在这个帖子里我们先概括介绍一下它的各个组成部分,以后将结合例子进行更详细的说明。 图2 GEF结构图 模型:GEF的模型只与控制器打交道,而不知道任何与视图有关的东西。为了能让控制器知道模型的变化,应该把控制器作为事件监听者注册在模型中,当模型发生变化时,就触发相应的事件给控制器,后者负责通知各个视图进行更新。 典型的模型对象会包含PropertyChangeSupport类型的成员变量,用来维护监听器成员即控制器;对于与其他对象具有连接关系的模型,要维护连入/连出的连接列表;如果模型对应的节点具有大小和位置信息,还要维护它们。这些变量并不是模型本身必须的信息,维护它们使模型变得不够清晰,但你可以通过构造一些抽象模型类(例如让所有具有连接的模型对象继承Node类)来维持它们的可读性。 相对来讲GEF中模型是MVC中最简单的一部分。 控制器:我们知道,在MVC结构里控制器是模型与视图之间的桥梁,也是整个GEF的核心。它不仅要监听模型的变化,当用户编辑视图时,还要把编辑结果反映到模型上。举个例子来说,用户在数据库结构图上删除一个表时,控制器应该从模型中删除这个表对象、表中的字段对象、以及与这些对象有关的所有连接。当然在GEF中这些操作不是由直接控制器完成的,这个稍后就会说到。 GEF中的控制器是所谓的EditPart对象,更确切的说应该是一组EditPart对象共同组成了GEF的控制器这部分,每一个模型对象都对应一个EditPart对象。你的应用程序中需要有一个EditPartFactory对象负责根据给定模型对象创建对应的EditPart对象,这个工厂类将被视图利用。 RootEditPart是一种特殊的EditPart,它和你的模型没有任何关系,它的作用是把EditPartViewer和contents(应用程序的最上层EditPart,一般代表一块画布)联系起来,可以把它想成是contents的容器。EditPartViewer有一个方法setRootEditPart()专门用来指定视图对应的RooEditPart。 图3 EditPart对象 用户的编辑操作被转换为一系列请求(Request),有很多种类的请求,这些种类在GEF里被称为角色(Role),GEF里有图形化和非图形化这两大类角色,前者比如Layout Role对应和布局有关的的操作,后者比如Connection Role对应和连接有关的操作等等。角色这个概念是通过编辑策略(EditPolicy)来实现的,EditPolicy的主要功能是根据请求创建相应的命令(Command),而后者会直接操作模型对象。对每一个EditPart,你都可以"安装"一些EditPolicy,用户对这个EditPart的特定操作会被交给已安装的对应EditPolicy处理。这样做的直接好处是可以在不同EditPart之间共享一些重复操作。 在GEF SDK提供的帮助文档(GEF开发指南)里有一份详细的EditPolicy、Role和Request类型列表,这里就不赘述了。 视图:前面说过,GEF的视图可以有很多种,GEF目前提供了图形(GraphicalViewer)和树状(TreeViewer)这两种,前者利用Draw2D图形(IFigure)作为表现方式,多用于编辑区域,后者则多用于实现大纲展示。视图的任务同样繁重,除了模型的显示功能以外,还要提供编辑功能、回显(Feedback)、工具提示(ToolTip)等等。 GEF使用EditPartViewer作为视图,它的作用和JFace中的Viewer十分类似,而EditPart就相当于是它的ContentProvider和LabelProvider,通过setContents()方法来指定。我们经常使用的Editor是一个GraphicalEditorWithPalette(GEF提供的Editor,是EditorPart的子类,具有图形化编辑区域和一个工具条),这个Editor使用GraphicalEditViewer和PaletteViewer这两个视图类,PaletteViewer也是GraphicalEditViewer的子类。开发人员要在configureGraphicalViewer()和initializeGraphicalViewer()这两个方法里对EditPartViewer进行定制,包括指定它的contents和EditPartFactory等等。 EditPartViewer同时也是ISelectionProvider,这样当用户在编辑区域做选择操作时,注册的SelectionChangeListener就可以收到选择事件。EditPartViewer会维护各个EditPart的选中状态,如果没有被选中的EditPart,则缺省选中的是作为contents的EditPart。 初步了解了GEF的MVC实现方式,让我们看看典型的GEF应用程序是什么样子的。大部分GEF应用程序都实现为Eclipse的Editor,也就是说整个编辑区域是放置在一个Editor里的。所以典型的GEF应用程序具有一个图形编辑区域包含在一个Editor(例如 GraphicalEditorWithPalette)里,可能有一个大纲视图和一个属性页,一个用于创建EditPart实例的EditPartFactory,一些表示业务的模型对象,与模型对象对应的一些EditPart,每个EditPart对应一个IFigure的子类对象显示给用户,一些EditPolicy对象,以及一些Command对象。 GEF应用程序的工作方式如下: EditPartViewer接受用户的操作,例如节点的选择、新增或删除等等,每个节点都对应一个EditPart对象,这个对象有一组按操作Role分开的EditPolicy,每个EditPolicy会对应一些Command对象,Command最终对模型进行直接修改。用户的操作转换为Request分配给适当的EditPolicy,由后者创建适当的Command来修改模型,这些Command会保留在EditDomain(专门用于维护EditPartViewer、Command等信息的对象,一般每个Editor对应唯一一个该对象)的命令堆栈里,用于实现撤消/重做功能。 以上介绍了GEF中一些比较重要的概念,不知道看过之后你是否对它有了一个大概的印象。如果没有也没关系,因为在后面的帖子里将会有结合例子的讲解,我们使用的实例就是序言里提到的第六个项目。 参考资料: 1. GEF开发指南 2. Eclipse Development - Using the Graphical Editing Framework and the Eclipse Modeling Framework 三、应用实例 构造一个GEF应用程序通常分为这么几个步骤:设计模型、设计EditPart和Figure、设计EditPolicy和Command,其中EditPart是最主要的一部分,因为在实现它的时候不可避免的要使用到EditPolicy,而后者又涉及到Command。 现在我们来看个例子,它的功能非常简单,用户可以在画布上增加节点(Node)和节点间的连接,可以直接编辑节点的名称以及改变节点的位置,用户可以撤消/重做任何操作,有一个树状的大纲视图和一个属性页。点此下载,这是一个Eclipse的项目打包文件,在Eclipse里导入后运行Run-time Workbench,新建一个扩展名为"gefpractice"的文件就会打开这个编辑器。 图1 Practice Editor的使用界面 你可以参考着代码来看接下来的内容了,让我们从模型开始说起。模型是根据应用需求来设计的,所以我们的模型包括代表整个图的Diagram、代表节点的Node和代表连接的Connection这些对象。我们知道,模型是要负责把自己的改变通知给EditPart的,为了把这个功能分离出来,我们使用名为Element的抽象类专门来实现通知机制,然后让其他模型类继承它。Element类里包括一个PropertyChangeSupport类型的成员变量,并提供了 addPropertyChangeListener()、removePropertyChangeListener()和fireXXX()方法分别用来注册监听器和通知监听器模型改变事件。在GEF里,模型的监听器就是EditPart,在EditPart的active()方法里我们会把它作为监听器注册到模型中。所以,总共有四个类组成了我们的模型部分。 在前面的贴子里说过,大部分GEF应用程序都是实现为Editor的,这个例子也不例外,对应的Editor名为PracticeEditor。这个Editor继承了GraphicalEditorWithPalette类,表示它是一个具有调色板的图形编辑器。最重要的两个方法是configureGraphicalViewer()和initializeGraphicalViewer(),分别用来定制和初始化EditPartViewer(关于EditPartViewer的作用请查看前面的帖子),简单查看一下GEF的代码你会发现,在GraphicalEditor类里会先后调用这两个方法,只是中间插了一个hookGraphicalViewer()方法,其作用是同步选择和把EditPartViewer作为SelectionProvider注册到所在的site(Site是Workbench的概念,请查Eclipse帮助)。所以,与选择无关的初始化操作应该在前者中完成,否则放在后者完成。例子中,在这两个方法里我们配置了RootEditPart、用于创建EditPart的EditPartFactory、Contents即Diagram对象和增加了拖放支持,拖动目标是当前EditPartViewer,后面会看到拖动源就是调色板。 这个Editor是带有调色板的,所以要告诉GEF我们的调色板里都有哪些工具,这是通过覆盖getPaletteRoot()方法来实现的。在这个方法里,我们利用自己写的一个工具类PaletteFactory构造一个PaletteRoot对象并返回,我们的调色板里需要有三种工具:选择工具、节点工具和连接工具。在GEF里,调色板里可以有抽屉(PaletteDrawer)把各种工具归类放置,每个工具都是一个ToolEntry,选择工具(SelectionToolEntry)和连接工具(ConnectionCreationToolEntry)是预先定义好的几种工具中的两个,所以可以直接使用。对于节点工具,要使用CombinedTemplateCreationEntry,并把节点类型作为参数之一传给它,创建节点工具的代码如下所示。 ToolEntry tool = new CombinedTemplateCreationEntry("Node", "Create a new N ode", Node.class, new SimpleFactory(Node.class), null, null); 在新的3.0版本GEF里还提供了一种可以自动隐藏调色板的编辑器 GraphicalEditorWithFlyoutPalette,对调色板的外观有更多选项可以选择,以后的帖子里可能会提到如何使用。 调色板的初始化操作应该放在initializePaletteViewer()里完成,最主要的任务是为调色板所在的EditPartViewer添加拖动源事件支持,前面我们已经为画布所在EditPartViewer添加了拖动目标事件,所以现在就可以实现完整的拖放操作了。这里稍微讲解一下拖放的实现原理,以用来创建节点对象的节点工具为例,它在调色板里是一个 CombinedTemplateCreationEntry,在创建这个PaletteEntry时(见上面的代码)我们指定该对象对应一个Node.class,所以在用户从调色板里拖动这个工具时,内存里有一个TemplateTransfer单例对象会 记录 混凝土 养护记录下载土方回填监理旁站记录免费下载集备记录下载集备记录下载集备记录下载 下Node.class(称作template),当用户在画布上松开鼠标时,拖放结束的事件被触发,将由画布注册的 DiagramTemplateTransferDropTargetListener对象来处理template对象(现在是Node.class),在例子中我们的处理方法是用一个名为ElementFactory的对象负责根据这个template创建一个对应类型的实例。 以上我们建立了模型和用于实现视图的Editor,因为模型的改变都是由Command对象直接修改的,所以下面我们先来看都有哪些Command。由需求可知,我们对模型的操作有增加/删除节点、修改节点名称、改变节点位置和增加/删除连接等,所以对应就有 CreateNodeCommand、DeleteNodeCommand、RenameNodeCommand、MoveNodeCommand、CreateConnectionCommand和DeleteConnectionCommand这些对象,它们都放归类在commands包里。一个Command对象里最重要的当然是execute()方法了,也就是执行命令的方法。除此以外,因为要实现撤消/重做功能,所以在Command对象里都有Undo()和Redo()方法,同时在Command对象里要有成员变量负责保留执行该命令时的相关状态,例如RenameNodeCommand里要有oldName和newName两个变量,这样才能正确的执行Undo()和Redo()方法,要记住,每个被执行过的Command对象实例都是被保存在EditDomain的CommandStack中的。 例子里的EditPolicy都放在policies包里,与图形有关的(GraphicalEditPart的子类)有DiagramLayoutEditPolicy、NodeDirectEditPolicy和NodeGraphicalNodeEditPolicy,另外两个则是与图形无关的编辑策略。可以看到,在后一种类型的两个类(ConnectionEditPolicy和NodeEditPolicy)中我们只覆盖了createDeleteCommand()方法,该方法用于创建一个负责"删除"操作的Command对象并返回,要搞清这个方法看似矛盾的名字里create和delete是对不同对象而言的。 有了Command和EditPolicy,现在可以来看看EditPart部分了。每一个模型对象都对应一个EditPart,所以我们的三个模型对象(Element不算)分别对应DiagramPart、ConnectionPart和NodePart。对于含有子元素的EditPart,必须覆盖getModelChildren()方法返回子对象列表,例如DiagramPart里这个方法返回的是Diagram对象包含的Node对象列表。 每个EditPart都有active()和deactive()两个方法,一般我们在前者里注册监听器(因为实现了PropertyChangeListener接口,所以EditPart本身就是监听器)到模型对象,在后者里将监听器从列表里移除。在触发监听器事件的propertyChange()方法里,一般是根据"事件名"称决定使用何种方式刷新视图,例如对于NodePart,如果是节点本身的属性发生变化,则调用refreshVisuals()方法,若是与它相关的连接发生变化,则调用refreshTargetConnections() 或refreshSourceConnections()。这里用到的事件名称都是我们自己来规定的,在例子中比如Node.PROP_NAME表示节点的名称属性,Node.PROP_LOCATION表示节点的位置属性,等等。 EditPart(确切的说是AbstractGraphicalEditpart)另外一个需要实现的重要方法是createFigure(),这个方法应该返回模型在视图中的图形表示,是一个IFigure类型对象。一般都把这些图形放在figures包里,例子里只有NodeFigure一个自定义图形,Diagram对象对应的是GEF自带的名为FreeformLayer的图形,它是一个可以在东南西北四个方向任意扩展的层图形;而Connection对应的也是GEF自带的图形,名为PolylineConnection,这个图形缺省是一条用来连接另外两个图形的直线,在例子里我们通过setTargetDecoration()方法让连接的目标端显示一个箭头。 最后,要为EditPart增加适当的EditPolicy,这是通过覆盖EditPart的createEditPolicies() 方法来实现的,每一个被"安装"到EditPart中的EditPolicy都对应一个用来表示角色(Role)的字符串。对于在模型中有子元素的EditPart,一般都会安装一个EditPolicy.LAYOUT_ROLE 角色的EditPolicy(见下面的代码),后者多为LayoutEditPolicy的子类;对于连接类型的EditPart,一般要安装EditPolicy.CONNECTION_ENDPOINTS_ROLE角色的EditPolicy,后者则多为ConnectionEndpointEditPolicy或其子类,等等。 installEditPolicy(EditPolicy.LAYOUT_ROLE, new DiagramLayoutEditPolicy()); 用户的操作会被当前工具(缺省为选择工具SelectionTool)转换为请求(Request),请求根据类型被分发到目标EditPart所安装的EditPolicy,后者根据请求对应的角色来判断是否应该创建命令并执行。 在以前的帖子里说过,Role-EditPolicy-Command这样的设计主要是为了尽量重用代码,例如同一个EditPolicy可以被安装在不同EditPart中,而同一个Command可以被不同的EditPolicy所使用,等等。当然,凡事有利必有弊,我认为这种的设计也有缺点,首先在代码上看来不够直观,你必须对众多Role、EditPolicy有所了解,增加了学习周期;另外大部分不需要重用的代码也要按照这个相对复杂的方式来写,带来了额外工作量。 以上就是一个GEF应用程序里最基本的几个组成部分,例子中还有如Direct Edit、属性表和大纲视图等一些功能没有讲解,下面的帖子里将介绍这些常用功能的实现。 四、其他功能 最近由于实验室任务繁重,一直没有继续研究GEF,本来已经掌握的一些东西好象又丢掉了不少,真是无奈啊,看来还是要经常碰碰。刚刚接触GEF的朋友大都会有这样的印象:GEF里概念太多,比较绕,一些能直接实现的功能非要拐几个弯到另一个类里做,而且很多类的名字十分相似,加上不知道他们的作用,感觉就好象一团乱麻。我觉得这种情况是由图形用户界面(GUI)的复杂性所决定的,GUI看似简单,实际上包含了相当多的逻辑,特别是GEF处理的这种图形编辑方式,可以说是最复杂的一种。GEF里每一个类,应该说都有它存在的理由,我们要尽可能了解作者的意图,这就需要多看文档和好的例子。 在Eclipse里查看文档和代码相当便利,比如我们对某个类的用法不清楚,一般首先找它的注释(选中类或方法按F2),其次可以查看它在其他地方用法(选中类或方法按Ctrl+Shift+G),还可以找它的源代码(Ctrl+鼠标左键或F3)来看,另外Ctrl+Shift+T可以按名称查找一个类等等。学GEF是少不了看代码的,当然还需要时间和耐心。 好,闲话少说,下面进入正题。这篇帖子将继续上一篇内容,主要讨论如何实现DirectEdit、属性页和大纲视图,这些都是一个完整GEF应用程序需要提供的基本功能。 实现DirectEdit 所谓DirectEdit(也称In-Place-Edit),就是允许用户在原本显示内容的地方直接对内容进行修改,例如在Windows资源管理器里选中一个文件,然后按F2键就可以开始修改文件名。实现DirectEdit的原理很直接:当用户发出修改请求(REQ_DIRECT_EDIT)时,就在文字内容所在位置覆盖一个文本框(也可以是下拉框,这里我们只讨论文本的情况)作为编辑器,编辑结束后,再将编辑器中的内容应用到模型里即可。(作为类似的功能请参考:给表格的单元格增加编辑功能) 图1 Direct Edit 在GEF里,这个弹出的编辑器由DirectEditManager类负责管理,在我们的NodePart类里,通过覆盖performRequest()方法响应用户的DirectEdit请求,在这个方法里一般要构造一个DirectEditManager类的实例(例子中的NodeDirectEditManager),并传入必要的参数,包括接受请求的EditPart(就是自己,this)、编辑器类型(使用TextCellEditor)以及用来 定位编辑器的CellEditorLocator(NodeCellEditorLocator),然后用show()方法使编辑器显示出来,而编辑器中显示的内容已经在构造方法里得到。简单看一下NodeCellEditorLocator 类,它的关键方法在relocate()里,当编辑器里的内容改变时,这个方法被调用从而让编辑器始终处于正确的坐标位置。DirectEditManager有一个重要的initCellEditor()方法,它的主要作用是设置编辑器的初始值。在我们的例子里,初始值设置为被编辑NodePart对应模型 (Node)的name属性值;这里还另外完成了设置编辑器字体和选中全部文字(selectAll)的功能,因为这样更符合一般使用习惯。 在NodePart里还要增加一个角色为DIRECT_EDIT_ROLE的EditPolicy,它应该继承自DirectEditPolicy,有两个方法需要实现:getDirectEditCommand()和 showCurrentEditValue(),虽然还未遇到过,但前者的作用你不应该感到陌生--在编辑结束时生成一个Command对象将修改结果作用到模型;后者的目的是更新Figure中的显示,虽然我们的编辑器覆盖了Figure中的文本,似乎并不需要管Figure的显示,但在编辑中时刻保持这两个文本的一致才不会出现"盖不住"的情况,例如当编辑器里的文本较短时。 实现属性页 在GEF里实现属性页和普通应用程序基本一样,例如我们希望可以通过属性视图(PropertyView)显示和编辑每个节点的属性,则可以让Node类实现IPropertySource接口,并通过一个IPropertyDescriptor[]类型的成员变量描述要在属性视图里显示的那些属性。有朋友问,要在属性页里增加一个属性都该改哪些地方,主要是三个地方:首先要在你的IPropertyDescriptor[]变量里增加对应的描述,包括属性名和属性编辑方式(比如文本或是下拉框,如果是后者还要指定选项列表),其次是getPropertyValue()和setPropertyValue()里增加读取属性值和将结果写入的代码,这两个方法里一般都是像下面的结构(以前者为例): public Object getPropertyValue(Object id) { if (PROP_NAME.equals(id)) return getName(); if (PROP_VISIBLE.equals(id)) return isVisible() ? new Integer(0) : new Integer(1); return null; } 也就是根据要处理的属性名做不同操作。要注意的是,下拉框类型的编辑器是以Integer类型数据代表选中项序号的,而不是int或String,例如上面的代码根据visible属性返回第零项或第一项,否则会出现ClassCastException。 图2 属性页 实现大纲视图 在Eclipse里,当编辑器(Editor)被激活时,大纲视图自动通过这个编辑器的getAdapter()方法寻找它提供的大纲(大纲实现IcontentOutlinePage接口)。GEF提供了 ContentOutlinePage类用来实现大纲视图,我们要做的就是实现一个它的子类,并重点实现createControl()方法。ContentOutlinePage是org.eclipse.ui.part.Page的一个子类,大纲视图则是PageBookView的子类,在大纲视图中有一个PageBook,包含了很多Page并可以在它们之间切换,切换的依据就是当前活动的Editor。因此,我们在createControl()方法里要做的就是构造这个Page,简化后的代码如下所示: private Control outline; public OutlinePage() { super(new TreeViewer()); } public void createControl(Composite parent) { outline = getViewer().createControl(parent); getSelectionSynchronizer().addViewer(getViewer()); getViewer().setEditDomain(getEditDomain()); getViewer().setEditPartFactory(new TreePartFactory()); getViewer().setContents(getDiagram()); } 由于我们在构造方法里指定了使用树结构显示大纲,所以createControl()里的第一句就会使outline变量得到一个Tree(见org.eclipse.gef.ui.parts.TreeViewer的代码),第二句把TreeViewer加到选择同步器中,从而让用户不论在大纲或编辑区域里选择EditPart时,另一方都能自动做出同样的选择;最后三行的作用在以前的帖子里都有介绍,总体目的是把大纲视图的模型与编辑区域的模型联系在一起,这样,对于同一个模型我们就有了两个视图,体会到MVC的好处了吧。 实现大纲视图最重要的工作基本就是这些,但还没有完,我们要在init()方法里绑定UNDO/REDO/DELETE等命令到Eclipse主窗口,否则当大纲视图处于活动状态时,主工具条上的这些命令就会变为不可用状态;在 getControl()方法里要返回我们的outline成员变量,也就是指定让这个控件出现在大纲视图中;在dispose()方法里应该把这个TreeViewer从选择同步器中移除;最后,必须在PracticeEditor里覆盖getAdapter()方法,前面说过,这个方法是在Editor激活时被大纲视图调用的,所以在这里必须把我们实现好的OutlinePage返回给大纲视图使用,代码如下: public Object getAdapter(Class type) { if (type == IContentOutlinePage.class) return new OutlinePage(); return super.getAdapter(type); } 这样,树型大纲视图就完成了,见下图。很多GEF应用程序同时具有树型和缩略图两种大纲,实现的基本思路是一样的,但代码会稍微复杂一些,因为这两种大纲一般要通过一个PageBook进行切换,缩略图一般由org.eclipse.draw2d.parts.ScrollableThumbnail负责实现,这里暂时不讲了(也许以后会详细说),你也可以通过看logic例子的LogicEditor这个类的代码来了解。 图3 大纲视图 P.S.写这篇帖子的时候,我对例子又做了一些修改,都是和这篇帖子所说的内容相关的,所以如果你以前下载过,会发现那时的代码与现在稍有不同(功能还是完全一样的,下载)。另外要说一下,这个例子并不完善,比如删除一个节点的时候,它的连接就没同时删除,一些键盘快捷键不起作用,还存在很多被注释掉的代码等等。如果有兴趣你可以来修改它们,也是不错的学习途径。 五、浅谈布局 虽然很多GEF应用程序里都会用到连接(Connection),但也有一些应用是不需要用连接来表达关系的,我们目前正在做的这个项目就是这样一个例子。在这类应用中,模型对象间的关系主要通过图形的包含来表达,所以大多是一对多关系。 图1 不使用连接的GEF应用 先简单描述一下我们这个项目,该项目需要一个图形化的模型编辑器,主要功能是在一个具有三行N列的表格中自由增加/删除节点,节点可在不同单元格间拖动,可以合并相邻节点,表格列可增减、拖动等等。由于SWT/Jface提供的表格很难实现这些功能,所以我们选择了使用GEF开发,目前看来效果还是很不错的(见下图),这里就简单介绍一下实现过程中与图形和布局有关的一些问题。 在动手之前首先还是要考虑模型的构造。由于Draw2D只提供了很有限的Layout,如ToolbarLayout、FlowLayout和XYLayout,并没有一个GridLayout,所以不能把整个表格作为一个EditPart,而应该把每一列看作一个EditPart(因为对列的操作比对行的操作多,所以不把行作为EditPart),这样才能实现列的拖动。另外,从需求中可以看出,每个节点都包含在一个列中,但仔细再研究一下会发现,实际上节点并非直接包含在列中,而是有一个单元格对象作为中间的桥梁,即每个列包含固定的三个单元格,每个单元格可以包含任意个节点。经过以上分析,我们的模型、EditPart和Figure应该已经初步成形了,见下表: 模型 EditPart Figure 画布 Diagram DiagramPart FreeformLayer 列 Column ColumnPart ColumnFigure 单元格 Cell CellPart CellFigure 节点 Node NodePart NodeFigure 表中从上到下是包含关系,也就是一对多关系,下图简单显示了这些关系: 图2 图形包含关系图 让我们从画布开始考虑。在画布上,列显示为一个纵向(高大于宽)的矩形,每个列有一个头(Header)用来显示列名,所有列在画布上是横向排列的。因此,画布应该使用ToolbarLayout 或FlowLayout中的一种。这两种Layout有很多相似之处,尤其它们都是按指定的方向排列显示图形,不同之处主要在于:当图形太多容纳不下的时候,ToolbarLayout会牺牲一些图形来保持一行(列),而FlowLayout则允许换行(列)显示。 对于我们的画布来说,显然应该使用ToolbarLayout作为布局管理器,因为它的子图形ColumnFigure是不应该出现换行的。以下是定义画布图形的代码: Figure f = new FreeformLayer(); ToolbarLayout layout=new ToolbarLayout(); layout.setVertical(false); layout.setSpacing(5); layout.setStretchMinorAxis(true); f.setLayoutManager(layout); f.setBorder(new MarginBorder(5)); 其中setVertical(false)指定横向排列子图形,setSpacing(5)指定子图形之间保留5象素的距离,setStretchMinorAxis(true) 指定每个子图形的高度都保持一致。 ColumnFigure的情况要稍微复杂一些,因为它要有一个头部区域,而且它的三个子图形(CellFigure)合在一起要能够充满下部区域,并且适应其高度的变化。一开始我用Draw2D提供的Label来实现列头,但有一个不足,那就是你无法设置它的高度,因为Label类覆盖了Figure的getPreferedSize()方法,使得它的高度只与里面的文本有关。解决方法是构造一个HeaderFigure,让它维护一个Label,设置列头高度时实际设置的是HeaderFigure的高度;或者直接让HeaderFiguer继承Label并重新覆盖getPreferedSize()也可以。我在项目里使用的是前者。 第二个问题花了我一些时间才搞定,一开始我是在CellPart的refreshVisuals()方法里手动设置CellFigure的高度为ColumnFigure下部区域高度的三分之一,但这样很勉强,而且还需要额外考虑spacing带来的影响。后来通过自定义Layout的方式比较圆满的解决了这个问题,我让ColumnFigure使用自定义的ColumnLayout,这个Layout继承自ToolbarLayout,但覆盖了layout()方法,内容如下: class ColumnLayout extends ToolbarLayout { public void layout(IFigure parent) { IFigure nameFigure=(IFigure)parent.getChildren().get(0); IFigure childrenFigure=(IFigure)parent.getChildren().get(1); Rectangle clientArea=parent.getClientArea(); nameFigure.setBounds(new Rectangle(clientArea.x,clientArea.y,clientArea.width,30)); childrenFigure.setBounds(new Rectangle(clientArea.x,nameFigure.getBounds().height+clientArea.y,clientArea.width,clientArea.height-nameFigure.getBound s().height)); } } 也就是说,在layout里控制列头和下部的高度分别为30和剩下的高度。但这还没有完,为了 让单元格正确的定位在表格列中,我们还要指定列下部图形(childrenFigure)的布局管理器, 因为实际上单元格都是放在这个图形里的。前面说过,Draw2D并没有提供一个像SWT中 FillLayout那样的布局管理器,所以我们要再自定义另一个layout,我暂时给它起名为 FillLayout(与SWT的FillLayout同名),还是要覆盖layout方法,如下所示(因为用了 transposer所以horizontal和vertical两种情况可以统一处理,这个transposer只在 horizontal时才起作用): public void layout(IFigure parent) { List children = parent.getChildren(); int numChildren = children.size(); Rectangle clientArea = transposer.t(parent.getClientArea()); int x = clientArea.x; int y = clientArea.y; for (int i = 0; i < numChildren; i++) { IFigure child = (IFigure) children.get(i); Rectangle newBounds = new Rectangle(x, y, clientArea.width, -1); int divided = (clientArea.height - ((numChildren - 1) * spacing)) / numChil dren; if (i == numChildren - 1) divided = clientArea.height - ((divided + spacing) * (numChildren - 1)); newBounds.height = divided; child.setBounds(transposer.t(newBounds)); y += newBounds.height + spacing; } } 上面这些语句的作用是将父图形的高(宽)度平均分配给每个子图形,如果是处于最后的一位的子图形,让它占据所有剩下的空间(防止除不尽的情况留下空白)。完成了这个FillLayout,只要让childrenFigure使用它作为布局管理器即可,下面是ColumnFigure的大部分代码,列头图形(HeaderFigure)和列下部图形(ChildrenFigure)作为内部类存在: private HeaderFigure name = new HeaderFigure(); private ChildrenFigure childrenFigure = new ChildrenFigure(); public ColumnFigure() { ToolbarLayout layout = new ColumnLayout(); layout.setVertical(true); layout.setStretchMinorAxis(true); setLayoutManager(layout); setBorder(new LineBorder()); setBackgroundColor(color); setOpaque(true); add(name); add(childrenFigure); setPreferredSize(100, -1); } class ChildrenFigure extends Figure { public ChildrenFigure() { ToolbarLayout layout = new FillLayout(); layout.setMinorAlignment(ToolbarLayout.ALIGN_CENTER); layout.setStretchMinorAxis(true); layout.setVertical(true); layout.setSpacing(5); setLayoutManager(layout); } } class HeaderFigure extends Figure { private String text; private Label label; public HeaderFigure() { this.label = new Label(); this.add(label); setOpaque(true); } public String getText() { return this.label.getText(); } public Rectangle getTextBounds() { return this.label.getTextBounds(); } public void setText(String text) { this.text = text; this.label.setText(text); this.repaint(); } public void setBounds(Rectangle rect) { super.setBounds(rect); this.label.setBounds(rect); } } 单元格的布局管理器同样使用FillLayout,因为在需求中,用户向单元格里添加第一个节点时,该节点要充满单元格;当单元格里有两个节点时,每个节点占二分之一的高度;依次类推。下面的表格总结了各个图形使用的布局管理。由表可见,只有包含子图形的那些图形才需要布局管理器,原因很明显:布局管理器关心和管理的是"子"图形,请时刻牢记这一点。 布局管理器 直接子图形 画布 列 ToolbarLayout 列 列头部、列下部 ColumnLayout -列头部 无 无 -列下部 单元格 FillLayout 单元格 节点 FillLayout 节点 无 无 这里需要特别提醒一点:在一个图形使用ToolbarLayout或子类作为布局管理器时,图形对应的EditPart上如果安装了FlowLayoutEditPolicy或子类,你可能会得到一个ClassCastException异常。例如例子中的CellFigure,它对应的EditPart是CellPart,其上安装了CellLayoutEditPolicy是FlowLayoutEditPolicy的一个子类。出现这个异常的原因是在FlowLayoutEditPolicy的isHorizontal()方法中会将图形的layout强制转换为FlowLayout,而我们使用的是ToolbarLayout。我认为这是GEF的一个疏忽,因为作者曾说过FlowLayout可应用于ToolbarLayout。幸好解决方法也不复杂:在你的那个EditPolicy中覆盖isHorizontal()方法,在这个方法里先判断layout是ToolbarLayout还是FlowLayout,再根据结果返回合适的boolean值即可。 最后,关于我们的画布还有一个问题没有解决,我们希望表格列增多到一定程度后,画布可以向右边扩展尺寸,前面说过画布使用的是FreeformLayer作为图形。为了达到目的,还必须在editor里设置rootEditPart为ScalableRootEditPart,要注意不是 ScalableFreeformRootEditPart,后者在需要各个方向都能扩展的画布的应用程序中经常被使用。关于各种RootEditPart的用法,在后续帖子里将会介绍到。 以上结合具体实例讲解了如何在GEF中使用ToolbarLayout以及自定义简单的布局管理器。我们构造图形应该遵守一个原则,那就是尽量让布局管理器决定每个子图形的位置和尺寸,这样可以避免很多麻烦。当然也有例外,比如在XYLayout这种只关心子图形位置的布局管理器中,就必须为每个子图形指定尺寸,否则图形将因为尺寸过小而不可见,这也是一个开发人员十分容易疏忽的地方。 六、添加菜单和工具条 我发现一旦稍稍体会到GEF的妙处,就会很自然的被它吸引住。不仅是因为用它做出的图形界面好看,更重要的是,UI中最复杂和细微的问题,在GEF的设计中无不被周到的考虑并以适当的模式解决,当你了解了这些,完全可以把这些解决方法加以转换,用来解决其他领域的设计问题。去年黄老大在一个GEF项目结束后,仍然没有放弃对它的继续研究,现在甚至利用业余时 间开发了基于GEF的SWT/JFace增强软件包,Eclipse和GEF的魅力可见一斑。我相信在未来的两年里,由于RCP/GEF等技术的成熟,Java Standalone应用程序必将有所发展,在B/S模式难以实现的那部分领域里扮演重要的角色。 本篇的主题是实现菜单功能,由于Eclipse的可扩展设计,在GEF应用程序中添加菜单要多几处考虑,所以我首先介绍Eclipse里关于菜单的一些概念,然后再通过实例描述如何在GEF里添加菜单、工具条和上下文菜单。 我们知道,Eclipse本身只是一个平台(Platform),用户并不能直接用它来工作,它的作用是为那些提供实际功能的部件提供一个基础环境,所有部件都通过平台指定的方式构造界面和使用资源。在Eclipse里,这些部件被称为插件(Plugins),例如Java开发环境(JDT)、Ant支持、CVS客户端和帮助系统等等都是插件,由于我们从eclipse.org下载的Eclipse本身已经包含了这些常用插件,所以不需要额外的安装,就好象Windows本身已经包含了记事本、画图等等工具一样。如果我们需要新功能,就要通过下载安装或在线更新的方式把它们安装到Eclipse平台上,常见的如XML编辑器、Properties文件编辑器,J2EE开发支持等等,包括GEF开发包也是这类插件。插件一般都安装在Eclipse安装目录的plugins子目录下,也可以使用link方式安装在其他位置。 Eclipse平台的一个优秀之处在于,如此众多的插件能够完美的集成在同一个环境中,要知道,每个插件都可能具有编辑器、视图、菜单、工具条、文件关联等等复杂元素,要让它们能够和平共处可不是件容易事。为此,Eclipse提供了一系列机制来解决由此带来的各种问题。由于篇幅限制,这里只能简单讲一下菜单和工具条的部分,更多内容请参考Eclipse随机提供的插件开发帮助文档。 大多数情况下,我们说开发一个基于Eclipse的应用程序就是指开发一个Eclipse插件(plugin),Eclipse里的每个插件都有一个名为plugin.xml的文件用来定义插件里的各种元素,例如这个插件都有哪些编辑器,哪些视图等等。在视图中使用菜单和工具条请参考以前的贴子,本篇只介绍编辑器的情况,因为GEF应用程序大多数是基于编辑器的。 图1 Eclipse平台的几个组成部分 首先要介绍Retarget Action的概念,这是一种具有一定语义但没有实际功能的Action,它唯一的作用就是在主菜单条或主工具条上占据一个项位置,编辑器可以将具有实际功能的Action映射到某个Retarget Action,当这个编辑器被激活时,主菜单/工具条上的那个Retarget Action就会具有那个Action的功能。举例来说,Eclipse提供了 IWorkbenchActionConstants.COPY这个Retarget Action,它的文字和图标都是预先定义好的,假设我们的编辑器需要一个"复制节点到剪贴板"功能,因为"复制节点"和"复制"这两个词的语义十分相近,所以可以新建一个具有实际功能的CopyNodeAction(extends Action),然后在适当的位置调用下面代码实现二者的映射: IActionBars.setGlobalActionHandler(IWorkbenchActionConstants.COPY,copyNodeAction) 当这个编辑器被激活时,Eclipse会检查到这个映射,让COPY项变为可用状态,并且当用户按下它时去执行CopyNodeAction里定义的操作,即run()方法里的代码。Eclipse引入Retarget Action的目的是为了尽量减少主菜单/工具条的重建消耗,并且有利于用户使用上的一致性。在GEF应用程序里,因为很可能存在多个视图(例如编辑视图和大纲视图,即使暂时只有一个视图,也要考虑到以后扩展为多个的可能),而每个视图都应该能够完成相类似的操作,例如在树结构的大纲视图里也应该像编辑视图一样可以删除选中节点,所以一般的操作都应以映射到Retarget Action的方式建立。 主菜单/主工具条 与视图窗口不同,编辑器没有自己的菜单栏和工具条,它的菜单只能加在主菜单里。由于一个编 辑器可以有多个实例,而它们应当具有相同的菜单和工具条,所以在plugin.xml里定义一个编 辑器的时候,元素有一个contributorClass属性,它的值是一个实现 IEditorActionBarContributor接口的类的全名,该类可以称为"菜单工具条添加器"。在添加 器里可以向Eclipse的主菜单/主工具条里添加自己需要的项。还是以我们这个项目为例,它要 求对每个操作可以撤消/重做,对画布上的每个元素可以删除,对每个节点元素可以设置它的优 先级为高、中、低三个等级。所以我们要添加这六个Retarget Action,以下就是 DiagramActionBarContributor类的部分代码: public class DiagramActionBarContributor extends ActionBarContributor { protected void buildActions() { addRetargetAction(new UndoRetargetAction()); addRetargetAction(new RedoRetargetAction()); addRetargetAction(new DeleteRetargetAction()); addRetargetAction(new PriorityRetargetAction(IConstants.PRIORITY_HIGH)); addRetargetAction(new PriorityRetargetAction(IConstants.PRIORITY_MEDIUM)); addRetargetAction(new PriorityRetargetAction(IConstants.PRIORITY_LOW)); } protected void declareGlobalActionKeys() { } public void contributeToToolBar(IToolBarManager toolBarManager) { …… } public void contributeToMenu(IMenuManager menuManager) { IMenuManager mgr=new MenuManager("&Node","Node"); menuManager.insertAfter(IWorkbenchActionConstants.M_EDIT,mgr); mgr.add(getAction(IConstants.ACTION_MARK_PRIORITY_HIGH)); mgr.add(getAction(IConstants.ACTION_MARK_PRIORITY_MEDIUM)); mgr.add(getAction(IConstants.ACTION_MARK_PRIORITY_LOW)); } } 可以看到,DiagramActionBarContributor类继承自GEF提供的类ActionBarContributor,后者是实现了IEditorActionBarContributor接口的一个抽象类。buildActions()方法用于创建那些要添加到主菜单/工具条的Retarget Actions,并把它们注册到一个专门的注册表里;而contributeToMenu()方法里的代码把这些Retarget Actions实际添加到主菜单栏,使用IMenuManager.insertAfter()是为了让新加的菜单出现在指定的系统菜单后面,contributeToToolBar()里则是添加到主工具条的代码。 图2 添加到主菜单条和主工具条上的Action GEF 在ActionBarContributor里维护了retargetActions和globalActionKeys两个列表,其中后者是一个Retarget Actions的ID列表,addRetargetAction()方法会把一个Retarget Action同时加到二者中,对于已有的Retarget Actions,我们应该在 declareGlobalActionKeys()方法里调用addGlobalActionKey()方法来声明,在一个编辑器被激活的时候,与globalActionKeys里的那些ID具有相同ID值的(具有实际功能的)Action将被联系到该ID对应的Retarget Action,因此就不需要显式的去调用 setGlobalActionHandler()方法了,只要保证二者的ID相同即可实现映射。 GEF已经内置了撤消/重做和删除这三个操作的Retarget Action(因为太常用了),它们的ID分别是IWorkbenchActionConstants.UNDO、REDO和DELETE,所以没有什么问题。而设置优先级这个Action没有语义相近的现成Retarget Action可用,所以我们自己要先定义一个PriorityRetargetAction,内容如下(没有经过国际化处理): public class PriorityRetargetAction extends LabelRetargetAction{ public PriorityRetargetAction(int priority) { super(null,null); switch (priority) { case IConstants.PRIORITY_HIGH: setId(IConstants.ACTION_MARK_PRIORITY_HIGH); setText("High Priority"); break; case IConstants.PRIORITY_MEDIUM: setId(IConstants.ACTION_MARK_PRIORITY_MEDIUM); setText("Medium Priority"); break; case IConstants.PRIORITY_LOW: setId(IConstants.ACTION_MARK_PRIORITY_LOW); setText("Low Priority"); break; default: break; } } } 接下来要在编辑器(CbmEditor)的createActions()里建立具有实际功能的Actions,它们应该是SelectionAction(GEF提供)的子类,因为我们需要得到当前选中的节点。稍后将给出PriorityAction的代码,编辑器的createActions()方法的代码如下所示: protected void createActions() { super.createActions(); //高优先级 IAction action=new PriorityAction(this, IConstants.PRIORITY_HIGH); action.setId(IConstants.ACTION_MARK_PRIORITY_HIGH); getActionRegistry().registerAction(action); getSelectionActions().add(action.getId()); //中等优先级 action=new PriorityAction(this, IConstants.PRIORITY_MEDIUM); action.setId(IConstants.ACTION_MARK_PRIORITY_MEDIUM); getActionRegistry().registerAction(action); getSelectionActions().add(action.getId()); //低优先级 action=new PriorityAction(this, IConstants.PRIORITY_LOW); action.setId(IConstants.ACTION_MARK_PRIORITY_LOW); getActionRegistry().registerAction(action); getSelectionActions().add(action.getId()); } 请再次注意在这个方法里每个Action的id都与前面创建的Retarget Action的ID对应,否则将无法对应到主菜单条和主工具条中的Retarget Actions。你可能已经发现了,这里我们只创建了设置优先级的三个Action,而没有建立负责撤消/重做和删除的Action。其实GEF在这个类的父类(GraphicalEditor)里已经创建了这些常用Action,包括撤消/重做、全选、保存、打印等,所以只要别忘记调用super.createActions()就可以了。 GEF提供的UNDO/REDO/DELETE等Action会根据当前选择的editpart(s)自动判断自己是否可用,我们定义的Action则要自己在Action的calculateEnabled()方法里计算。另外,为了实现撤消/重做的功能,一般Action执行的时候要建立一个Command,将后者加入CommandStack里,然后执行这个Command对象,而不是直接把执行代码写在Action的run()方法里。下面是我们的设置优先级PriorityAction的部分代码,该类继承自 SelectionAction: public void run() { execute(createCommand()); } private Command createCommand() { List objects = getSelectedObjects(); if (objects.isEmpty()) return null; for (Iterator iter = objects.iterator(); iter.hasNext();) { Object obj = iter.next(); if ((!(obj instanceof NodePart)) && (!(obj instanceof NodeTreeEditPart))) return null; } CompoundCommand compoundCmd = new CompoundCommand(GEFMessages.DeleteAction_ActionDeleteCommandName); for (int i = 0; i < objects.size(); i++) { EditPart object = (EditPart) objects.get(i); ChangePriorityCommand cmd = new ChangePriorityCommand(); cmd.setNode((Node) object.getModel()); cmd.setNewPriority(priority); compoundCmd.add(cmd); } return compoundCmd; } protected boolean calculateEnabled() { Command cmd = createCommand(); if (cmd == null) return false; return cmd.canExecute(); } 因为允许用户一次对多个选中的节点设置优先级,所以在这个Action里我们创建了多个Command对象,并把它们加到一个CompoundCommand对象里,好处是在撤消/重做的时候也可以一次性完成,而不是一个节点一个节点的来。 上下文菜单 在GEF里实现右键弹出的上下文菜单是很方便的,只要写一个继承org.eclipse.gef. ContextMenuProvider的自定义类,在它的buildContextMenu()方法里编写添加菜单项的代码,然后在编辑器里调用GraphicalViewer. SetContextMenu()即可。GEF为我们预先定义了一些菜单组(Group)用来区分不同用途的菜单项,每个组在外观上表现为一条分隔线,例如有UNDO组、COPY组和PRINT组等等。如果你的菜单项不适合放在任何一个组中,可以放在OTHERS组里,当然如果你的菜单项很多,也可以定义新的组用来分类。 图3 上下文菜单 假设我们要实现如上图所示的上下文菜单,并且已经创建并在ActionRegistry里了这些Action (在Editor的createActions()方法里完成),ContextMenuProvider应该像下面这样写: public class CbmEditorContextMenuProvider extends ContextMenuProvider { private ActionRegistry actionRegistry; public CbmEditorContextMenuProvider(EditPartViewer viewer, ActionRegistry registry) { super(viewer); actionRegistry = registry; } public void buildContextMenu(IMenuManager menu) { // Add standard action groups to the menu GEFActionConstants.addStandardActionGroups(menu); // Add actions to the menu menu.appendToGroup(GEFActionConstants.GROUP_UNDO,getAction(ActionFactory.UNDO.getId())); menu.appendToGroup(GEFActionConstants.GROUP_UNDO, getAction(ActionFactory.REDO.getId())); menu.appendToGroup(GEFActionConstants.GROUP_EDIT, getAction(Action Factory.DELETE.getId())); menu.appendToGroup(GEFActionConstants.GROUP_REST,getAction(IConst ants.ACTION_MARK_PRIORITY_HIGH)); menu.appendToGroup(GEFActionConstants.GROUP_REST,getAction(IConst ants.ACTION_MARK_PRIORITY_MEDIUM)); menu.appendToGroup(GEFActionConstants.GROUP_REST,getAction(IConst ants.ACTION_MARK_PRIORITY_LOW)); } private IAction getAction(String actionId) { return actionRegistry.getAction(actionId); } } 注意buildContextMenu()方法里的第一句是创建缺省的那些组,如果没有忽略了这一步后面的语句会提示组不存在的错误,你也可以通过这个方法看到GEF是怎样建组的以及都有哪些组。让编辑器使用这个类的代码一般写在configureGraphicalViewer()方法里。 因为顺便介绍了Eclipse的一些基本概念,加上代码比较多,所以这篇贴子看起来比较长,其实通过查看GEF对内置的UNDO/REDO等的实现很容易就会明白菜单的使用方法。 七、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 Pr opertyChangeListener, 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 con nection) { return new ChopBoxAnchor (getFigure()); } public ConnectionAnchor getSourceConnectionAnchor(Request request) { return new ChopBoxAnchor (getFigure()); } public ConnectionAnchor getTargetConnectionAnchor(ConnectionEditPart con nection) { 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以及多重选择这些情况下;另外,这种方法只适用于容器里的子元素不是容器的情况,如果有多层的容器关系,则每一层都要做类似的处理才可以。 知道原因了,PolygonDecoration是RotatableDecoration,而MidepointLocator只能处理不可旋转的图形。用下面这个类代替MidpointLocator即可: class MyMidpointLocator extends MidpointLocator { public MyMidpointLocator(Connection c, int i) { super(c, i); } public void relocate(IFigure target) { PointList points = getConnection().getPoints(); RotatableDecoration arrow = (RotatableDecoration) target; arrow.setLocation(getLocation(points)); arrow.setReferencePoint(points.getPoint(getIndex())); } } 八、使用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文件,该插件的免费版本可以从这里下载。 图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 instanceValu e) { 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 XMLResourceImpl(fileURI); 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。 九、增加易用性 当一个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_DO CK_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.cl ass.toString()); if (manager != null) manager.setZoom(diagram.getZoom()); 在保存模型前得到当前的缩放比例放在画布模型里一起保存: ZoomManager manager = (ZoomManager) getGraphicalViewer().getProperty(ZoomManager.cl ass.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。 标尺和辅助线 标尺位于画布的上部和左侧,在每个标尺上可以建立很多与标尺垂直的辅助线,这些显示在画布上的虚线具有吸附功能。 图5 标尺和辅助线 标尺和辅助线的实现要稍微复杂一些。首先要修改原有的模型,新增加标尺和辅助线这两个类,它们之间的关系请看下图: 图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,所以要给它们增加监听器 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]处理颜色类型的偏好项 在Eclipse里实现偏好页(PreferencePages)时,我们一般要在Plugin类的start()方法里预先设置好每一项的缺省值,但IPreferenceStore接口只提供了参数为整型、布尔型、字符串等基本类型的setDefault()方法,如果某个偏好项是颜色类型怎么办呢, 这时要使用PreferenceConverter这个类,也是由jface提供的,该类提供了额外的一些setDefault()方法可以接收RGB、Font、Point等类型的参数,所以可以使用下面的方法设置缺省值: PreferenceConverter.setDefault(getPreferenceStore(), IConstants.PRE F_COLOR_PRIORITY_HIGH, new RGB(255, 128, 64)); 要取得某个颜色类型的偏好项值,就用下面的语句: PreferenceConverter.getColor(CbmPlugin.getDefault().getPreferenceSt ore(), IConstants.PREF_COLOR_PRIORITY_HIGH) JFace在颜色、图象和字体等资源的管理方面为我们做了很多工作,我们应该尽量利用已有的这些功能。 图1 颜色类型的偏好项 [GEF]在非XYLayout布局的container里调整children尺寸 以前只做过两种类型的GEF程序,一种是画布使用XYLayout,子图形可以在上面随意改变大小和位置;另一种是画布使用非XYLayout的布局,子图形的大小和位置由布局决定,用户不能用鼠标拖动的方式改变。现在这个项目有点特殊,因为要实现类似“表格”的功能,所以要求画布使用ToolbarLayout排列表格列,但列的高度要能够改变。我查看了FlowLayoutEditPolicy类,里面没有一个类似“createChangeConstraintCommand”这样的方法,那么该怎样实现这个 功能呢, 图1 画布使用ToolbarLayout同时可以调整列的高度 经过黄老大的指点和查看代码,原来要在画布的EditPolicy里覆盖 createChildEditPolicy()方法,FlowLayoutEditPolicy缺省是返回一个 NonResizableEditPolicy,我们要改为返回一个ResizableEditPolicy,为了只 让用户能拖动列的底部,还要稍微设置一下这个EditPolicy,如下所示: protected EditPolicy createChildEditPolicy(EditPart child) { ResizableEditPolicy policy = new ResizableEditPolicy(); policy.setResizeDirections(PositionConstants.SOUTH); return policy; } 在运行时,这个被返回的EditPolicy会被安装在child的EditPart上,把我们 的画布看作parent,列就是child。当用户拖动列图形的handler时,产生一个 类型为REQ_RESIZE的请求,这个请求被转发给parent的EditPolicy,所以我 们要在画布的EditPolicy里覆盖getCommand()方法对这个请求进行处理,如下 所示: public Command getCommand(Request request) { if (REQ_RESIZE_CHILDREN.equals(request.getType())) { ChangeColumnHeightCommand cmd = new ChangeColumnHeightComma nd(); Column column = (Column) ((ColumnPart) ((ChangeBoundsReques t) request).getEditParts().get(0)).getModel(); cmd.setColumn(column); cmd.setNewHeight(column.getHeight() + ((ChangeBoundsReques t) request).getSizeDelta().height); return cmd; } return super.getCommand(request); } 这就会返回一个用来调整列高度的Command,这个命令的具体内容这里不赘述 了。createChildEditPolicy是在LayoutEditPolicy里定义的一个抽象方法, GEF提供的与Layout有关的EditPolicy里都会提供一个缺省的实现,但在需求 比较特殊的情况下我们要提供自己的实现。这里是OrderedLayoutEditPolicy(FlowLayoutEditPolicy的父类)关于该方法的注释:“Since Ordered layouts generally don't use constraints, a NonResizableEditPolicy is used by default for children. Subclasses may override this method to supply a different EditPolicy.”很明显GEF已经考虑到了这种情况。 十、表格的一个实现 在目前的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的操 作。例如,创建一个新行转换为在第一列中增加一个新的单元格,当然在这同时我们要让程序给 其余每一列同样增加一个单元格。 图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() + ICo nstants.COLUMN_SPACING); getLineFeedback().setPoint(p2, 1); } 其中p2代表插入线中右边的那个点,我们将它的横坐标加上一个量即可增加这条线的长度,这 个量和表格当前列的数目有关,和列间距也有关,计算的方法看上面的代码很清楚。这样修改后 的效果如下图所示,拖动行到新的位置时也会使用同样的插入线。 图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.g etColumns().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类是控制点的自定义实现,在下面很快会讲到。现在,用户可以看到整个行被选中的效果了。 图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); } 0); } public String getText() { return "&Open in browser"; } } 上面是一个例子IAction,它的作用是在触发时将用户选中的条目在浏览器里打开。这个类同时 还实现了ISelectionChangeAction,这样就可以在用户没有选中任何条目的时候将自己变为 不可用。当然,你要把它作为监听器加入某个列表对象的监听器列表,像下面代码里这样: private void createActions() { openAction = new OpenAction(); removeAction = new RemoveAction(tvResult); reloadAction = new ReloadAction(tvResult); tvResult.addSelectionChangedListener(openAction); tvResult.addSelectionChangedListener(removeAction); tvResult.addSelectionChangedListener(reloadAction); } 注意,最后的三句就是加入监听列表的功能。有些IAction需要改变所监听对象(比如一个 TableViewer)的行为,所以要把那个对象作为参数传递给它才行。下面是把IAction对象加 入菜单的代码: private void createMenu() { IMenuManager mgr = getViewSite().getActionBars().getMenuManager(); mgr.add(openAction); mgr.add(removeAction); mgr.add(reloadAction); } 把IAction对象加到工具条的代码几乎完全一样,只是第一句有所不同: private void createMenu() { IToolBarManager mgr = getViewSite().getActionBars().getToolBarManager(); mgr.add(openAction); mgr.add(removeAction); mgr.add(reloadAction); } 上下文菜单就显得有些麻烦了,因为View并没有一个“PopupMenuManager”这样的东西,所以我们只能半手动的来建立: private void createContextMenu() { MenuManager mgr = new MenuManager(); mgr.setRemoveAllWhenShown(true); mgr.addMenuListener(new IMenuListener() { public void menuAboutToShow(IMenuManager manager) { fillContextMenu(manager); } }); Menu menu = mgr.createContextMenu(tvResult.getControl()); tvResult.getControl().setMenu(menu); getSite().registerContextMenu(mgr, tvResult); } 这个MenuManager和我们在createMenu()里通过getMenuManager()得到的是同一个类(但不是同一个实例哦),setRemoveAllWhenShown(true)的作用是清空以前显示的菜单项,当触发了menu事件时,重新填充(fillContextMenu),所以如果你不把 removeAllWhenShow置为true的话,每点一下右键你就会看到菜单项多出一倍来。Menu是swt的控件(刚才说的MenuManager、ToolbarManager都是jface里的东西,jface给swt包了一层),用MenuManager可以创建出一个Menu对象,然后我们用表格的setMenu方法将表格控件与Menu控件联系在一起就好了。 最后还有一句,它是为扩展这个上下文菜单用的,例如你可以在plugin.xml里统一指定给某种类型的元素都加上某个菜单项(例如,如果用户选中了一个.zip文件,会多出一个“解压缩”选项)。那么新加的菜单项会出现在上下文菜单的哪里呢,最上方还是最下方,还是……,所以呢,要在fillContextMenu的时候指定一下: protected void fillContextMenu(IMenuManager manager) { manager.add(openAction); manager.add(removeAction); manager.add(reloadAction); manager.add(new GroupMarker(IWorkbenchActionConstants.MB_ADDITION S)); } 前三句都没什么特别,最后一句就是指定了上面我们说的这个“增加点”,这样,你想让后来的菜单放在哪里都行了。 最后,Eclipse的Workbench提供了一些比较通用的系统菜单项,如下: public static final String [] GLOBAL_ACTIONS = { UNDO, REDO, CUT, COPY, PASTE, PRINT, DELETE, FIND, SELECT_ALL, BOOKMARK }; 当你的焦点在不同的View或Editor里时,同一个系统菜单项会有不同的作用产生。例如在文本编辑器中delete项是删除当前选中的文字,而在你的视图里,你希望delete的作用是删除用户选中的表格条目,刚好是removeAction的功能。所以你要把你的IAction对象和系统菜单挂在一起: private void hookGlobalActions() { IActionBars bars = getViewSite().getActionBars(); bars.setGlobalActionHandler(IWorkbenchActionConstants.DELETE, removeA ction); } 注意,要选择语义上比较相近的系统菜单项来挂接,否则会造成用户的困扰。比如你非要把COPY实现为openAction,当用户在系统菜单里选了copy命令,本以为会把当前选中的条目复制到剪贴板,你却给人家打开了这些条目,多滑稽。 好了,菜单方面基本上就这些内容。可以看出,Eclipse的Workbench的确为我们提供了很多方便,特别是如果能够灵活利用plugin来进行定义,不仅可以节约大量的代码,还能让我们始终保持对系统的掌握。所以说,RCP的风行可不是没有道理哦 十二、 前两天GEF发布了3.1M7版本,但使用下来发现和M6没有什么区别,是不是主要为了和Eclipse版本相配套,希望3.1正式版早日发布,应该会新增不少内容。上一篇帖子介绍了如何实现表格功能,在开发过程中,另一个经常用到的功能就是树,虽然SWT提供了标准的树控件,但使用它完成如组织结构图这样的应用还是不够直观和方便。在目前版本(3.1M7)的GEF中虽然没有直接支持树的实现,但Draw2D提供的例子程序里却有我们可以利用的代码(org.eclipse.draw2d.examples.tree.TreeExample,运行界面见下图),通过它可以节约不少工作量。 图1 Draw2D例子中的TreeExample 记得数年前曾用Swing做过一个组织结构图的编辑工具,当时的实现方式是让画布使用XYLayout,在适当的时候计算和刷新每个树节点的位置,算法的思想则是深度优先搜索,非树 叶节点的位置由其子节点的数目和位置决定。我想这应该是比较直观的方法吧,但是这次看了Draw2D例子里的实现觉得也很有道理,以前没想到过。在这个例子里树节点图形称为TreeBranch,它包含一个PageNode(表现为带有折角的矩形)和一个透明容器contentsPane,(一个Layer,用来放置子节点)。在一般情况下,TreeBranch本身使用名为NormalLayout的布局管理器将PageNode放在子节点的正上方,而contentsPane则使用名为TreeLayout的布局管理器计算每个子节点应在的位置。所以我们看到的整个树实际上是由很多层子树叠加而成的,任何一个非叶节点对应的图形的尺寸都等于以它为根节点的子树所占区域的大小。 从这个例子里我们还看到,用户可以选择使用横向或纵向组织树(见图2),可以压缩各节点之间的空隙,每个节点可以横向或纵向排列子节点,还可以展开或收起子节点,等等,这为我们实现一个方便好用的树编辑器提供了良好的基础(视图部分的工作大大简化了)。 图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.N ORTH)); return list; } 选中节点的效果如下图,我根据需要改变了树节点的显示(修改PageNode类): 图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纵向排列子节点(以节省横向空间)。 图4 树编辑器的运行界面 这个编辑器我花一天时间就完成了,但如果不是利用Draw2D的例子,相信至少要四至六天,而且缺陷会比较多,功能上也不会这么完善。我感觉在GEF中遇到没有实现过的功能前最好先找一找有没有可以利用的资源,比如GEF提供的几个例子就很好,当然首先要理解它们才谈得上利用。 Eclipse基础,,使用links方式安装Eclipse插件 eclispe想必大家都很熟悉了,一般来说,eclipse插件都是安装在plugins目录下。不过这样一来,当安装了许多插件之后,eclipse变的很大,最主要的是不便于更新和管理众多插件。用links方式安装eclipse插件,可以解决这个问题。 当前配置XP SP1,eclipse3.0.1 现在假设我的eclipse安装目录是D:\eclipse,待安装插件目录是D:\plug-in ,我将要安装LanguagePackFeature(语言包)、emf-sdo-xsd-SDK、GEF-SDK、Lomboz这四个插件。 先把这四个插件程序全部放在D:\plug-in目录里,分别解压。如Lomboz3.0.1.zip解压成Lomboz3.0.1目录,这个目录包含一个plugins目录,要先在Lomboz3.0.1目录中新建一个子目录eclipse,然后把plugins目录移动到刚建立的eclipse目录中,即目录结构要是这样的: D:\plug-in\Lomboz3.0.1\eclipse\plugins 将会到指定的目录下去查找 eclipse\features 目录和 Eclipse eclipse\plugins 目录,看是否有合法的功能部件和(或)插件。也就是说,目 \eclipse 目录。如果找到,附加的功能部件和插件在运行标目录必须包含一个 期配置是将是可用的,如果链接文件是在工作区创建之后添加的,附加的功能部件和插件会作为新的配置变更来处理。 其它压缩文件解压后若已经包含eclipse\plugins目录,则不需要建立eclipse目录。 然后在 eclipse安装目录D:\eclipse目录中建立一个子目录links,在links目录中建立一个link文件,比如 LanguagePackFeature.link,改文件内容为 path=D:/plug-in/LanguagePackFeature 即这个link文件要对应一个刚解压后的插件目录。 说明: 1. 插件可以分别安装在多个自定义的目录中。 2. 一个自定义目录可以安装多个插件。 3. link文件的文件名及扩展名可以取任意名称,比如ddd.txt,myplugin都可以。 4. link文件中path=插件目录的path路径分隔要用\\或是/ 5. 在links目录也可以有多个link文件,每个link文件中的path参数都将生效。 6. 插件目录可以使用相对路径。 7. 可以在links目录中建立一个子目录,转移暂时不用的插件到此子目录中,加快eclipse启动。 8. 如果安装后看不到插件,把eclipse 目录下的configuration目录删除,重启即可。 Supporting Dynamic Bundle Loading In Your Plug-In With Eclipse 3.0 there is the ability to insert and remove plug-ins into a running instance of Eclipse. This is fantastic support, and an absolute godsend to certain RCP applications, but the leveraging the support doesn't come for free. As a plug-in developer you need to ensure that any extensions you may utilize are allowed to appear and disappear at any given point. When they disappear you should acknowledge their disappearance by cleaning up any internal structures that may represent the extensions and remove any UI artifacts that they may drive. When they appear you should augment your internal structures and (possibly) create new UI artifacts. For the 3.0 release the Platform UI team began investigating plug-in addition. While we did support UI extensions (belonging to the org.eclipse.ui namespace) being added, we did it without providing any viable strategy as to how others might follow our lead. With 3.1 we attempted to support plug-in removal and to provide utilities that other plug-in developers might use to make their own extension points dynamically aware. The result of our work is the org.eclipse.core.runtime.dynamicHelpers.IExtensionTracker and associated interfaces. This interface provides a mechanism by which plug-in developers can easily track the coming and going of extensions and manage the resources generated by such actions. Imagine for a moment that you have an extension point in your plug-in called widgets. Internally you have WidgetDescriptors that encapsulate widget extensions and a WidgetRegistry to manage them. Using IExtensionTracker your code might look something like this: public class WidgetRegistry implements IExtensionChangeHandler { public WidgetRegistry() { IExtensionTracker tracker = PlatformUI.getWorkbench() .getExtensionTracker(); IExtensionPoint point = Platform.getExtensionRegistry() .getExtensionPoint("my.plugin.namespace", "widget"); IExtension[] extensions = point.getExtensions(); // initial population for (int i = 0; i < extensions.length; i++) { addExtension(tracker, extensions[i]); } tracker .registerHandler(this, tracker .createExtensionPointFilter(point)); } public void addExtension(IExtensionTracker tracker, IExtension extension) { WidgetDescriptor descriptor = new WidgetDescriptor(extension); tracker.registerObject(extension, descriptor, IExtensionTracker.REF_STRONG); addToInternalStructure(descriptor); } private void addToInternalStructure(WidgetDescriptor descriptor) { // registry specific logic } public void removeExtension(IExtension extension, Object[] objects) { for (int i = 0; i < objects.length; i++) { WidgetDescriptor descriptor = (WidgetDescriptor) objects[i]; removeFromInternalStructure(descriptor); } } private void removeFromInternalStructure(WidgetDescriptor descriptor) { // registry specific logic } } public void dispose() { PlatformUI.getWorkbench() .getExtensionTracker().unregisterHandler(this) } } Platform UI provides IExtensionTracker instances at various levels of the Workbench. If you're tracking objects that live for the life of the workbench you should use the tracker provided by IWorkbench.getExtensionTracker(). If your objects are relevant to particular workbench windows or pages, you should use the trackers provided by IWorkbenchWindow.getExtensionTracker() or IWorkbenchPage.getExtensionTracker(). For instance, Workbench tracks view descriptors at the Workbench level but tracks actual view instances at the workbench page level. Your handlers may be registered against particular extension points via providing an IFilter instance to IExtensionTracker.registerHandler(). Your handler will only be called when extensions matching the IFilter object are added or removed. When an extension enters the runtime this method will be called. Your handler then has the opportunity to incorporate the new extension into its associated model. Any objects created based on the IExtension provided to this interface should be registered against the tracker via IExtensionTracker.registerObject(). When an extension leaves the runtime this method will be called. Any objects previously registered against the extension will be passed as arguments. Your handler may then clean up and dispose of the objects as necessary. It's good practice to unregister your handlers so that your registries are not leaked. Supporting Dynamic Bundle Loading In Your Plug-In (Part 2) Previously I discussed methods that plug-in developers could use to ensure that their model of the Eclipse extension registry stayed up to date. While this was the main thrust of the work we did in platform-ui towards utilizing the improvements in the core plug-in registry it was not the only motive. Memory footprint was also a concern for 3.1 and while we were refactoring the registries for dynamic support we kept this in mind. Previous to 3.1 the pattern in the UI was to read our extension points and fully realize any descriptors that they may represent. In 3.1 we tried to take the lazy approach to this. For instance, in 3.0 we would read the view extension point and create heavyweight descriptors for each view defined. We would pull out all strings from configuration elements and hold onto them for later use . In 3.1, the core registry has the ability to punt unused configuration elements to disk instead of holding onto them in memory. For this to be useful we had to ensure that we (UI) weren't holding onto the String objects that they (Core) were smart enough to discard. Now instead of ripping out and holding onto all Strings we instead only extract those that are used often enough (or in particular patterns) such that it's beneficial to do so. In the view descriptor example, we no longer read out the Strings for label, description, path, etc - only the identifier String is read. When any of the other Strings are required we delegate back to the originating IConfigurationElement (which itself is a lightweight handle) for the content. While the above pattern doesn't particularly help your plug-in act in a dynamic way, it does help to make it a good Eclipse citizen. 利用Digester把XML转换为Java对象 在一个比较完整的应用系统里,经常需要有一些配置文件。简单的属性使用.properties文件 即可,但要配置一些复杂对象,则应该考虑使用xml文件。一般用来读取xml文件的工具 包有DOM、SAX和JDOM等,但用过的人都知道,它们属于比较底层的API,写起来代码 量很大,而且如果修改了xml文件的格式,代码也要做大幅度的改动。Jakarta Commons项 目里的Digester包,可以轻松实现xml文件到Java对象的转换,看下面这个例子。 在一个项目里,需要提供一些统计图,但图的内容暂时未能确定。所以我决定让图可以配置, 所有定义保存在一个名为charts.xml(或国际化后的文件名如charts_zh_CN.xml,这里只考 虑缺省语言)的文件内,下面是该文件的部分内容: 统计图一 false true Bar 时间 数据 500 360 select count(c),c.department.name from edu.pku.pub.aims.model.business.Client c gr oup by c.department 可以看出,我为每个图定义了id、title、legendVisible等等属性,这些属性的意义都很明显, 它们将影响统计图的数据和在页面中的表现。在程序里,我需要把这个文件里的定义读到一 个注册表类ChartRegistry里,该注册表维护一个java.util.List类型的registry变量,其中每 个元素是一个ChartConfig类。现在Digester该显示它的价值了。 为了方便使用Digester,我们让ChartConfig也具有统计图的每个属性(id、title、legendVisible 等等),名称与charts.xml里的元素的属性(子元素)一一对应,并且都具有getter和setter 方法,也就是说,ChartConfig是一个bean类。在ChartRegistry类里定义一个deregister()方 法,它的作用是用Digester读入并解析指定的xml文件,代码如下;还有一个register()方法 用来把ChartConfig对象加到registry里。 public void deregister(URL url) throws IOException,SAXException{ InputStream is = new FileInputStream(url.getFile()); Digester digester = new Digester(); digester.push(this); digester.setValidating(false); digester.addObjectCreate("charts/chart", ChartConfig.class); digester.addSetProperties("charts/chart"); digester.addBeanPropertySetter("charts/chart/legendVisible"); digester.addBeanPropertySetter("charts/chart/toolTipsVisible"); digester.addBeanPropertySetter("charts/chart/title"); digester.addBeanPropertySetter("charts/chart/type"); digester.addBeanPropertySetter("charts/chart/labelx"); digester.addBeanPropertySetter("charts/chart/labely"); digester.addBeanPropertySetter("charts/chart/width"); digester.addBeanPropertySetter("charts/chart/height"); digester.addBeanPropertySetter("charts/chart/hql"); digester.addBeanPropertySetter("charts/chart/description"); digester.addSetNext("charts/chart","register"); digester.parse(is); Collections.sort(registry); } 基本上来说,Digester和SAX解析xml的过程很像,它的原理就是制定一些规则,在遍历 每个节点时检查是否有匹配的规则,如果有就执行对应的操作。例如,上面的代码中, “digester.addObjectCreate("charts/chart", ChartConfig.class);”这一句的作用是告诉Digester:如 果遇到匹配“charts/chart”形式的节点,就执行一个“对象创建”操作,创建什么对象呢,应该 创建Class为“ChartConfig.class”的对象;类似的,addSetProperties()是告诉Digester将指定 节点的属性全部映射到对象的属性,在这个例子里指的就是id属性;addBeanPropertySetter() 是将子节点转换为对象的属性,这个方法还可以有第二个参数,当对象的属性名和子节点的名字不一样时用来指定对象的属性名;addSetNext()是说在遇到匹配节点后,对当前对象的父对象执行一个方法,参数是当前参数,对这个例子来说就是执行 ChartConfig.register(ChartConfig)方法。因此这样构造得到的Digester会把charts.xml里的每个元素转换为一个ChartConfig对象,并register到ChartRegistry里。 顺利得到了ChartRegister对象,我就可以在程序里根据它的内容构造统计图了(统计图一般使用jfreechart来生成,这里就不赘述了)。与Digester具有类似功能的工具包其实还有不少,例如Caster、Jato等等,我没有实际使用过它们,但因为我对用过的Jakarta其他项目都很满意(例如BeanUtils、HttpClient,品牌效应,),所以一开始就选择了Digester:简单方便。 ChartConfig.register(ChartConfig)应该是ChartRegistry.register(ChartConfig) 又向楼主多学了一招,顺便帮楼主把代码帖全。注意用Digester时还要一起下载commons的BeanUtil和sax的实现包(我用xerces),否则会出错 package chengang.digester; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.Collections; import org.apache.commons.digester.Digester; import org.xml.sax.SAXException; /** * * @author ChenGang */ public class ChartRegistry { public static void main(String[] args) throws MalformedURLException, IOException, SAXException { new ChartRegistry().deregister(new URL("file:\\E:\\eclipse\\workspace\\wxxr_boss_connector\\charts.xml")); } java.util.List registry=new ArrayList(); public void deregister(URL url) throws IOException,SAXException{ InputStream is = new FileInputStream(url.getFile()); Digester digester = new Digester(); digester.push(this); digester.setValidating(false); digester.addObjectCreate("charts/chart", ChartConfig.class); digester.addSetProperties("charts/chart"); digester.addBeanPropertySetter("charts/chart/legendVisible"); digester.addBeanPropertySetter("charts/chart/type"); digester.addBeanPropertySetter("charts/chart/width"); digester.addBeanPropertySetter("charts/chart/description"); digester.addSetNext("charts/chart","register"); digester.parse(is); Collections.sort(registry); } public void register(ChartConfig obj){ registry.add(obj); } } package chengang.digester; /** * * @author ChenGang */ public class ChartConfig { private String id; private boolean legendVisible; private String type; private int width; private String description; public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public String getId() { return id; } public void setId(String id) { this.id = id; } public boolean isLegendVisible() { return legendVisible; } public void setLegendVisible(boolean legendVisible) { this.legendVisible = legendVisible; } public String getType() { return type; } public void setType(String type) { this.type = type; } public int getWidth() { return width; } public void setWidth(int width) { this.width = width; } } charts.xml文件,我简化了一下。 false 中国 500 设置Eclipse RCP程序的外观和首选项 RCP应用程序的缺省外观是一个空白窗口,一般我们要通过一个WorkbenchAdvisor类对界面进行定制。WorkbenchAdvisor有很多回调方法,可以在preWindowOpen()方法里设置菜单、工具条、状态栏、进度栏、透视图切换工具是否可见,在fillActionBars()方法里添加菜单和工具条项,在getInitialWindowPerspectiveId()方法里指定首选的透视图。 缺省情况下,透视图切换工具位于窗口左上角,在Eclipse里可以通过Window->Preferences->Workbench->Appearance改变它的位置,那么怎样用程序控制它呢,有两个方法,第一个是使用如下代码设置IPreferenceStore中的变量: IPreferenceStore apiStore = PrefUtil.getAPIPreferenceStore(); apiStore.setValue(IWorkbenchPreferenceConstants.DOCK_PERSPECTIVE_BAR, IWorkbenchPr eferenceConstants.TOP_RIGHT); 另一个方法是在plugin所在目录建一个名为plugin_customization.ini的文件,里面写如下内容: your.plugin.id/DOCK_PERSPECTIVE_BAR=topRight 其他与plugin相关的Preference值可以用同样方法设置。 Update:在最新的Eclipse 3.1M5a版本中,对RCP应用程序菜单和工具条的定制方法有所改变,应该使用新加入的ActionBarAdvisor类来完成此项工作。 利用BeanUtils在对象间复制属性 commons-beanutils是jakarta commons子项目中的一个软件包,其主要目的是利用反射机制对JavaBean的属性进行处理。我们知道,一个JavaBean通常包含了大量的属性,很多情况下,对JavaBean的处理导致大量get/set代码堆积,增加了代码长度和阅读代码的难度 BeanUtils是这个包里比较常用的一个工具类,这里只介绍它的 copyProperties()方法。该方法定义如下: public static void copyProperties(java.lang.Object dest,java.lang.Object orig) throws java.lang.IllegalAccessException, java.lang.reflect.InvocationTargetException 如果你有两个具有很多相同属性的JavaBean,一个很常见的情况就是Struts里的PO对象(持久对象)和对应的ActionForm,例如Teacher和TeacherForm。我们一般会在Action里从ActionForm构造一个PO对象,传统的方式是使用类似下面的语句对属性逐个赋值: //得到TeacherForm TeacherForm teacherForm=(TeacherForm)form; //构造Teacher对象 Teacher teacher=new Teacher(); //赋值 teacher.setName(teacherForm.getName()); teacher.setAge(teacherForm.getAge()); teacher.setGender(teacherForm.getGender()); teacher.setMajor(teacherForm.getMajor()); teacher.setDepartment(teacherForm.getDepartment()); //持久化Teacher对象到数据库 HibernateDAO=; HibernateDAO.save(teacher); 而使用BeanUtils后,代码就大大改观了,如下所示: //得到TeacherForm TeacherForm teacherForm=(TeacherForm)form; //构造Teacher对象 Teacher teacher=new Teacher(); //赋值 BeanUtils.copyProperties(teacher,teacherForm); //持久化Teacher对象到数据库 HibernateDAO=; HibernateDAO.save(teacher); 如果Teacher和TeacherForm间存在名称不相同的属性,则BeanUtils不对这些属性进行处理,需要程序员手动处理。例如Teacher包含modifyDate(该属性记录最后修改日期,不需要用户在界面中输入)属性而TeacherForm无此属性,那么在上面代码的copyProperties()后还要加上一句: teacher.setModifyDate(new Date()); 怎么样,很方便吧~除BeanUtils外还有一个名为PropertyUtils的工具类,它也提供copyProperties()方法,作用与BeanUtils的同名方法十分相似,主要的区别在于后者提供类型转换功能,即发现两个JavaBean的同名属性为不同类型时,在支持的数据类型范围内进行转换,而前者不支持这个功能,但是速度会更快一些。BeanUtils支持的转换类型如下: , java.lang.BigDecimal , java.lang.BigInteger , boolean and java.lang.Boolean , byte and java.lang.Byte , char and java.lang.Character , java.lang.Class , double and java.lang.Double , float and java.lang.Float , int and java.lang.Integer , long and java.lang.Long , short and java.lang.Short , java.lang.String , java.sql.Date , java.sql.Time , java.sql.Timestamp 这里要注意一点,java.util.Date是不被支持的,而它的子类java.sql.Date 是被支持的。因此如果对象包含时间类型的属性,且希望被转换的时候,一定要 使用java.sql.Date类型。否则在转换时会提示argument mistype异常 Building a Database Schema Diagram Editor with GEF Summary GEF is a very powerful framework for visually creating and editing models. With a small initial investment, even the relative Eclipse novice can be quickly up and running, building applications with graphical editing capabilities. To illustrate, this article uses a relational database schema diagram editor with a deliberately simplified underlying model, but with enough bells and whistles to show some of the interesting features of GEF at work. Phil Zoio, Realsolve Solutions Ltd. September 27, 2004 Introduction Having graphical editing capabilities can be a real asset, if not an essential feature, for many tools and applications. Examples are not hard to think of: UML tools, GUI builders, in fact, any application which comprises a dynamic model which can be visualized. With GEF, Eclipse developers have at their disposal a framework which can really simplify development of graphical editors. This article uses a simple but non-trivial example to show how a GEF application works - and what you need to do to get it to perform its little miracles. The screenshot below shows what our example editor looks like. The edit area uses a "flyout" palette which contains some very basic entries and can be minimized to increase the editable screen area. On the right side is a scrollable graphical viewer containing the tables and their relationships. Download and unzip the example plug-in schemaeditor.zip into your eclipse/ directory, then create a new diagram by launching the wizard from the File menu: File -> New -> Example ... -> GEF (Graphical Editing Framework) -> Schema Diagram Editor. At the heart of GEF is the Model-View-Controller pattern, discussed in Randy Hudson's introductory tutorial How to Get Started with the GEF, and also providing a focus for much of this article. The Model The starting point for any GEF application is the model. This is what needs to be displayed, edited and persisted. Our somewhat oversimplified model contains the following classes: , Table: represents a relational database table. The only attribute that the table holds itself is the name , Column: represents a table column. Here we are interested in the column name and the type of data, which itself can either be VARCHAR or INTEGER , Relationship: represents a primary key/foreign key relationship between two tables. The foreign key table we denote the source of the relationship, while the primary key table is the target. Note that our model applies a relationship directly between two tables, rather than between foreign and primary key fields in the respective tables, as we would in the real world , Schema: simply represents all the tables we plan to group together (and ultimately show on the same diagram) Our model is extremely simple, but does at least include the two key forms of relationship in a typical GEF model: , The parent-child relationship that exists between schemas and tables, and between tables and columns , Connections between different nodes. In our example application, the connections are in the form of primary key/foreign key relationships, with the nodes being the tables Of course, we need to decide what we want our editor to be able to do with the model. Here, we want to be able to: , lay our tables nicely on the diagram. The diagram must scale to accommodate any growth in our model, and should be readable , add new tables to our schema, and new columns to our table, using either drag and drop or point and click , directly edit the names of the tables as well as both the names and types of our columns. In both cases, we want validation to tell us when we are typing in nonsense , use drag and drop to set up relationships between tables, as well as to change these relationships , use drag and drop to move column definitions from one table to another, or to reorder columns within tables , be able to delete tables, columns and relationships by hitting the delete key , have a choice between manually laying out our diagram, and having this done automatically. When using manual layout, we want to be able to shift our tables around using drag and drop There are of course many more things we would like to be able to do with our editor, but we have enough here to be able to test out many of the most commonly used GEF features. The View Figures The display component in both GEF and draw2d is built around the draw2d IFigure interface. Figures in draw2d are lightweight objects which can be nested to create a complex graphical representation. The view is created by rendering and laying out the set of figures which reflect the model. In a typical GEF application, you would normally create a set of customized IFigure implementations, each subclassing Figure. If you're unfamiliar with draw2d and Figures, take a look at Daniel Lee's article on Display a UML Diagram using Draw2D, In our application we have the following figures: , EditableLabel: a subclass of the draw2d Label class which itself subclasses Figure. We need this for the column and table names , ColumnsFigure: a container for all the column labels , TableFigure: contains an EditableLabel for the table name, as well as a ColumnsFigure for the column names , SchemaFigure: a container for all the TableFigures in the schema We haven't provided any custom figures to represent connections - we simply use the draw2d PolylineConnection class, which is a just a line with zero or more kinks or bend points. Because the table names as well as the number of columns and their names are likely to change during the lifetime of a TableFigure instance, we want our ColumnsFigure and TableFigure to be resizable. A key role in allowing this to happen is played by layout managers, another important part of the draw2d framework. Layout Management GEF provides a layout management framework which is distinct from the Swing and Eclipse SWT layout managers: its job is specifically to handle layout of the child figures of draw2d IFigure instances. Your job as an application developer is to decide which layout manager to use for each figure containing child figures. Broadly speaking, there are three types of layout managers: , Structured layout managers, such as FlowLayout and ToolbarLayout, which lay out child figures according to their order by arranging them vertically or horizontally , Constraint-based layout managers, such as the XYLayout and the DelegatingLayout. Here the application itself participates directly in the placement of figures by setting a constraint Object for each child figure. In the case of the XYLayout, this object is a Rectangle with specified location and size , Layout using geometry computation algorithms. Here layout is determined by applying a series of rather complex algorithms to calculate the "best" layout for child figures. The algorithms take a specially constructed data structure as input and deliver as their output a solution to geometrical problems such as node placement and routing of paths. The algorithms provided by GEF are in the classes DirectedGraphLayout and CompoundDirectedGraphLayout The GEF developer needs to understand which layout managers can be best applied in which situation. Structured layout managers are suitable when there is a well defined parent-child relationship between the containing figure and its children and the children are not related to each other in arbitrary ways. In our example application, TableFigure uses a ToolbarLayout to place its children (simply stacking them vertically). The ColumnsFigure does the same with its child Label objects, but uses the FlowLayout for this purpose. This kind of arrangement of course does not work with SchemaFigure - any of its child TableFigures may be related to any other via a primary key/foreign key relationship, so we cannot simply stack the table figures next to each other or side by side. For SchemaFigure we need to choose between either a constraint-based layout manager or a graph layout manager. In our example application we use both. Users can switch between manual layout, which involves dragging table figures to their desired locations, and automatic placement of figures using geometry computation algorithms. How this is done is beyond the scope of this article, although interested readers can examine the DelegatingLayoutManager class in the example application source code. Open a schema diagram editor and make some changes, switching between manual and automatic layout using the icon. The Controller We only really move into GEF territory proper when we start talking about the controller in the MVC trilogy. GEF provides an abstraction that prevents the model from having to know about the figures, and vice versa. At the centre of this architecture is the EditPart interface. EditParts The first thing to know is that typically every separately editable part of the model will need to be associated with an EditPart instance. This means that there will usually be a close to one-for-one mapping between classes in the model hierarchy and classes in the EditPart hierarchy. In most cases, an EditPart is also a GraphicalEditPart, which means that as well as managing a model component, it also has an associated view component. Because the model and view are completely decoupled, all coordination between the model and the view must be managed by the EditPart. This coordination can be divided into two separate areas of activity: 1. Acting as a listener to changes in the model so that these can be propagated to the view, by calling layout related methods. We discuss this in detail in the section Updating an Repainting the Display 2. Providing a means by which user interaction can be interpreted and propagated to changes in the model. Central to this are EditPolicies, discussed in the section EditPolicies and Roles 3. Managing what are known as direct edits, where the user types text directly into an editable control In our example application we have the following EditPart implementations , SchemaDiagramPart: represents a Schema instance and associated SchemaFigure , TablePart: represents a Table and manages the TableFigure and child view components , ColumnPart: enables editing functionality for the column label , RelationshipPart: represents a primary key/foreign key relationship. In the same way that Relationship in the model is associated with two Table instances, a RelationshipPart is associated with two TableParts When an instance of any of these classes is created, it is automatically associated with a part of the model. This is a build-in feature of the framework. As part of our editor, we have to provide an EditPartFactory implementation. Ours looks like this: public class SchemaEditPartFactory implements EditPartFactory { public EditPart createEditPart(EditPart context, Object model) { EditPart part = null; if (model instanceof Schema) part = new SchemaDiagramPart(); else if (model instanceof Table) part = new TablePart(); else if (model instanceof Relationship) part = new RelationshipPart(); else if (model instanceof Column) part = new ColumnPart(); part.setModel(model); return part; } } SchemaDiagramPart, TablePart and ColumnPart all extend AbstractGraphicalEditPart and implement GraphicalEditPart. In addition, TablePart can be a node in a primary/foreign key relationship, so it has to implement NodeEditPart. Finally, RelationshipPart represents the connection part of the relationship, so it extends AbstractConnectionEditPart. SchemaDiagramPart's job is primarily managing the layout of the tables. ColumnPart's role is relatively limited - it just needs to handle editing of the label displaying name and type information. Of the four of these, TablePart has the most to do. In GEF, most of the work that is done to manage relationships is done by NodeEditPart, and not ConnectionEditPart. Because we sometimes need to rename tables, TablePart also has to manage editing of the label that displays its name. We will spend more of our time focusing on TablePart. In a GEF application, there are a number of tasks EditPart subclasses must fulfill: 1. Provide a figure instance to be associated with the EditPart. In the case of TablePart, we simply return a new TableFigure instance with a name label: 2. protected IFigure createFigure() 3. { 4. Table table = getTable(); 5. EditableLabel label = new EditableLabel(table.getName()); 6. TableFigure tableFigure = new TableFigure(label); 7. return tableFigure; } 8. EditParts which represent parent objects in parent-child relationships need to override getModelChildren(). In the case of TablePart, our implementation of this method simply returns the Column objects it contains: 9. protected List getModelChildren() 10. { 11. return getTable().getColumns(); } Note that the AbstractEditPart implements a parallel method getChildren(), which returns the EditPart collection representing the model children. In the case of TablePart, getChildren() returns a list of ColumnPart objects. We know this because our implementation of EditPartFactory associates Column model instances with instances of ColumnPart. The EditPart List returned by getChildren() always needs to be kept in sync with the getModelChildren(). In the Section Synchronizing EditPart Relationships with Model Changes we describe how this happens 12. If the parent EditPart's figure is not the direct parent of the child EditPart's figure, you will need to override AbstractGraphicalEditPart.getContentPane(). The content pane is the containing figure into which GEF adds figures created by child EditParts, which is by default the figure returned by the EditPart's createFigure() method. In our example application the column labels are not contained within a TableFigure but within its ColumnsFigure child. Our implementation of getContentPane() in TablePart reflects this: public IFigure getContentPane() { TableFigure figure = (TableFigure) getFigure(); return figure.getColumnsFigure(); } Do not add and remove child figures by overriding AbstractGraphicalEditPart.addChildVisual() and AbstractGraphicalEditPart.removeChildVisual(). Override getContentPane() instead. 13. EditParts which represent nodes (model objects which may participate in connections) must also implement a number of additional methods defined in the interface NodeEditPart o protected List getModelSourceConnections(): this returns all the connection model objects for which the node model object is the source. In our example application, we have identified foreign keys as the source of a primary key/foreign key relationship. TablePart's implementation contains just a single line of code: return getTable().getForeignKeyRelationships(); This method simply returns the Relationship objects for which the current TablePart's Table is the foreign key. Once again, there is a parallel method getSourceConnections(), which returns the List of RelationshipParts associated with these relationships. We also consider in the Section Synchronizing EditPart Relationships with Model Changes how the ConnectionEditPart list returned by getSourceConnections() stays in sync with the Relationship list returned by getModelSourceConnections() o protected List getModelTargetConnections(): this works identically to getModelSourceConnections(), except that it returns the Relationship objects for which the current TablePart's Table is the primary key o the node GraphicalEditPart must also provide implementations of the NodeEditPart getSourceConnectionAnchor() and getTargetConnectionAnchor() methods. In each case, these methods return objects which represent the points to which connections between nodes can be attached 14. Provide an implementation for createEditPolicies(), during which EditPolicy implementations are associated with specific editing roles. The EditPolicy and its associated roles, Request and Command objects are a fundamental part of GEF which we discuss in the next sections Requests We begin with requests because these are really the starting point of the editing process that GEF application developer works with. In fact, the real magic in GEF is being able to interpret user interactions and transform these into requests, which the application can work with in an object-oriented fashion. For example, when we drag from the "New Column" palette button onto an existing table on the diagram, we are of course trying to add a new column to the table. As users interact with the application, GEF's behind-the-scenes work produces Request objects. In the create column example, GEF produces a CreateRequest, which contains the following important information: , the instance of the new model object that has been created (but probably not yet configured and definitely not added to the rest of the model) , The EditPart object which is hosting this request. In our case this will be an instance of TablePart Different types of user interactions will produce different Request types - these are well covered in the GEF API and platform documentation. These request objects neatly encapsulate the information the application needs to transform user interaction into changes to the model. We can take a look at how this is done once we have looked at Commands and EditPolicies, which we cover in the next section. EditPolicies and Roles An EditPolicy is really just an extension of an EditPart, in the sense that certain editing related tasks are passed on from the EditPart to its EditPolicyimplementations would rapidly become delegates. EditPart bloated if they had to take on everything that EditPolicies do. To understand what an EditPolicy is and what it does, lets start by looking at the createEditPolicies() method in TablePart: protected void createEditPolicies() { installEditPolicy(EditPolicy.GRAPHICAL_NODE_ROLE, new TableNodeEditPolicy()); installEditPolicy(EditPolicy.LAYOUT_ROLE, new TableLayoutEditPolicy()); installEditPolicy(EditPolicy.CONTAINER_ROLE, new TableContainerEditPolicy()); installEditPolicy(EditPolicy.COMPONENT_ROLE, new TableEditPolicy()); installEditPolicy(EditPolicy.DIRECT_EDIT_ROLE, new TableDirectEditPolicy()); } The purpose of this method is simply to decorate the TablePart with editing functionality. Each call to installEditPolicy() in the above method registers an EditPolicy with the EditPart. The key constant used in each of these calls is the name of the role used. For example, EditPolicy.CONTAINER_ROLE is simply the string "ContainerEditPolicy". The container role is relevant for TablePart because we know that tables contain columns, and one of our application's requirements is to create new columns and add these to existing tables. The use of a particular role name in the installEditPolicy() call is really just a convention - the framework does not attach any behavior to a particular choice of role name. What distinguishes an EditPolicy implementation (and its corresponding role) is the type of requests it understands. Most of the abstract EditPolicy classes provide an implementation of the getCommand(Request request) method. In ContainerEditPolicy we find the following: public Command getCommand(Request request) { if (REQ_CREATE.equals(request.getType())) return getCreateCommand((CreateRequest)request); if (REQ_ADD.equals(request.getType())) return getAddCommand((GroupRequest)request); if (REQ_CLONE.equals(request.getType())) return getCloneCommand((ChangeBoundsRequest)request); if (REQ_ORPHAN_CHILDREN.equals(request.getType())) return getOrphanChildrenCommand((GroupRequest)request); return null; } Here getCommand() simply uses the request type to determine which getXXXCommand() method to call. In ContainerEditPolicy, getCreateCommand() is abstract - we must provide an implementation in order to use the base ContainerEditPolicy functionality. Here is our implementation of TableContainerEditPolicy: public class TableContainerEditPolicy extends ContainerEditPolicy { protected Command getCreateCommand(CreateRequest request) { Object newObject = request.getNewObject(); if (!(newObject instanceof Column)) { return null; } Column column = (Column) newObject; TablePart tablePart = (TablePart) getHost(); Table table = tablePart.getTable(); ColumnCreateCommand command = new ColumnCreateCommand(); command.setTable(table); command.setColumn(column); return command; } } In most cases, our EditPolicy implementations simply amount to using a Request object to generate a Command. Our getCreateCommand() method , gets the new object embodied in the CreateRequest and makes sure that it is an instance of Column, , gets the Table object associated with the host EditPart, and , creates an instance of the relevant Command class, configures it with the Table and Column information, and returns it Our TablePart createEditPolicies() implementation uses one of our customized EditPolicy implementations for each invocation of installEditPolicy(). Each of our EditPolicy implementations subclasses a GEF-provided abstract EditPolicy for a different role. For example, TableEditPolicy extends ComponentEditPolicy to fulfill the EditPolicy.COMPONENT_ROLE. It does so by implementing the createDeleteCommand(GroupRequest request) to handle requests of type REQ_DELETE. The GEF platform documentation provides a lot more detail on the types of roles and requests and how and when they can be used, so we won't cover them in any more detail here. Commands Command is GEF's abstract base class whose function is simply to encapsulate our application's response to a request. Key methods included in the Command class are the following: , execute(): Command provides a no-op implementation. As the name suggests, this contains the code to apply any change to the model that the Command object encapsulates , undo(): used to reverse the effect of execute(). Here Command also provides a no-op implementation , redo(): used redo a command execution. The Command implementation simply calls execute(), which should usually be adequate , canExecute(): whether execute() can be executed. The subclass can implement this to specify the conditions under which the command can be executed , canUndo(): whether undo() can be executed. The Command implementation simply returns true, which subclasses can override , canRedo(): whether redo() can be executed. The Command implementation here also simply returns true Any non-trivial Command subclass would need to implement execute(). Implementation of undo() would be recommended in most cases. The other methods are optional and would only be overridden as required. Lets take a look at our rather straightforward ColumnCreateCommand implementation: public class ColumnCreateCommand extends Command { private Column column; private Table table; public void setColumn(Column column) { this.column = column; this.column.setName("COLUMN " + (table.getColumns().size() + 1)); this.column.setType(Column.VARCHAR); } public void setTable(Table table) { this.table = table; } public void execute() { table.addColumn(column); } public void undo() { table.removeColumn(column); } } Much of the class is self-explanatory. We have setter methods to populate the Command object with the newly-created Column as well as the target container Table. We arbitrarily provide a name and type for the Column , which the user can later change. We can also see that execute() simply adds the Column object to the Table, and undo() simply reverses that change. The use of Commands has two key advantages over using EditPolicies directly to effect model changes , Commands are more elegant and in line with OO best practice , The Command framework has built-in support for undo and redo functionality The Command implementation is closely tied to the model, and should be cleanly separated from GEF-specific components. It should not contain any references to EditParts or EditPolicies. Observing this rule preserves the clean separation between commands and the UI logic, helping to keep code more maintainable and bug-free. Propagating Model Changes Once we've changed the model, our GEF editor needs to propagate these changes to the UI. Our model, view and controller need to work together to achieve this. So far, we have discussed the GraphicalEditPart's responsibility to provide a figure to represent the part of the model it is managing. To participate in a fully functional graphical editor, it needs to do more: , It needs to act as a listener for changes in the model. The model itself needs to fire event notifications which the EditPart can receive , It needs to maintain its own child and connection relationships with other EditParts, keeping these in sync with changes to the model , It needs to update the figures that it is managing, and their layouts, in line with model changes We discuss each of these in turn. Sending and Receiving Event Notifications The requirements imposed on our model implementation are that 1. it exposes a mechanism by which listeners can register interest in event notifications, and 2. it actually fires these event notifications at the appropriate times! In our example application we want all our model objects to use a common framework, so we satisfy the first requirement by allowing all our model classes to extend PropertyAwareObject, which looks like this: public abstract class PropertyAwareObject implements Serializable { public static final String CHILD = "CHILD"; ... other String constants representing the other types of model changes protected transient PropertyChangeSupport listeners = new PropertyChangeSupport(this); protected PropertyAwareObject() { } public void addPropertyChangeListener(PropertyChangeListener l) { listeners.addPropertyChangeListener(l); } public void removePropertyChangeListener(PropertyChangeListener l) { listeners.removePropertyChangeListener(l); } protected void firePropertyChange(String prop, Object old, Object newValue) { listeners.firePropertyChange(prop, old, newValue); } ... } Our abstract model base class contains a few String constants representing the types of model changes it knows about. It uses the java.beans.PropertyChangeSupport to provide the "plumbing" for the event handling. It also exposes methods and which observers can use to register and deregister their interest in model changes. Finally, it includes a firePropertyChange() method which subclasses can use to trigger property events. In our example of adding a column to a table, we see a good example in Table: public void addColumn(Column column) { columns.add(column); firePropertyChange(CHILD, null, column); } With this mechanism available, we now need to take advantage in our EditPart listeners. Once again, we address the issue by providing a common base class for our GraphicalEditParts to extend, shown below: public abstract class PropertyAwarePart extends AbstractGraphicalEditPart implements PropertyChangeListener { public void activate() { super.activate(); PropertyAwareObject propertyAwareObject = (PropertyAwareObject) getModel(); propertyAwareObject.addPropertyChangeListener(this); } public void deactivate() { super.deactivate(); PropertyAwareObject propertyAwareObject = (PropertyAwareObject) getModel(); propertyAwareObject.removePropertyChangeListener(this); } public void propertyChange(PropertyChangeEvent evt) { //handle property change event ... } } The GEF API documentation recommends the use of activate() and deactivate() to register or deregister model listeners. This is what we do here. After casting our model object to PropertyAwareObject, we add our EditPart as a listener in , and allow it to be . Once the EditPart is activated, any event notifications fired removed on deactivation in from our model will result in an invocation of propertyChange() . Our propertyChange() implementation in PropertyAwarePart in turn delegates its response to other methods, which can be overridden by EditPart subclasses to customize reactions to specific changes in the model. Synchronizing EditPart Relationships with Model Changes As we mentioned previously, the first thing the EditPart implementation needs to do in response to a model change is to ensure that its relationship hierarchy is in sync with that of the model. GEF provides a quick and easy solution in the form of three methods in the EditPart hierarchy. Before discussing a more performant approach that many applications will demand, we'll take a look at these methods. , refreshChildren(): when an EditPart represents a model object with children, this method may need to be called. Our example of adding a column to a table is a good one. The same applies for removing a column from a table. If we moved a column from one table to another, refreshChildren() would need to be called for both corresponding TableParts. The base implementation of this method not only synchronizes your model and EditPart hierarchies - it also adds or removes visual components as required by calling the AbstractGraphicalEditPart addChildVisual() and removeChildVisual() methods , refreshSourceConnections(): this applies to any model change where the source of a connection is added, removed or reassigned. For example, if we added or deleted a primary/foreign key relationship, this method would need to be called , refreshTargetConnections(): this only applies to a model change where the target of a connection is added, removed or reassigned. It would be needed for any change affecting the primary key of a relationship between tables Returning to our example of adding a column to a table, our implementation of PropertyAwarePart.propertyChange() can be reduced to the following: public void propertyChange(PropertyChangeEvent evt) { String property = evt.getPropertyName(); if (PropertyAwareObject.CHILD.equals(property)) { refreshChildren(); refreshVisuals(); } ... handle other types of property changes here } To resynchronize the EditPart hierarchy, we simply call refreshChildren() . To update the display, we then call refreshVisuals() . We discuss the mechanics and rationale for in the next section. Using the methods refreshChildren(), refreshSourceConnections() and refreshSourceConnections() can help you get your application working quickly, but if we want our application to run efficiently, we need to be more selective in the methods we use. For example, to add or remove a child, we can use the EditPart methods addChild(EditPart, int) and removeChild(EditPart). Our revised handleChildChange(PropertyChangeEvent)below is a better performing replacement for refreshChildren() which uses these methods: protected void handleChildChange(PropertyChangeEvent evt) { Object newValue = evt.getNewValue(); Object oldValue = evt.getOldValue(); if (newValue != null) { //add new child EditPart editPart = createChild(newValue); int modelIndex = getModelChildren().indexOf(newValue); addChild(editPart, modelIndex); } else { //remove an existing child List children = getChildren(); EditPart partToRemove = null; for (Iterator iter = children.iterator(); iter.hasNext();) { EditPart part = (EditPart) iter.next(); if (part.getModel() == oldValue) { partToRemove = part; break; } } if (partToRemove != null) removeChild(partToRemove); } } When adding our child, we need to call createChild() to get a new EditPart for the model child. We then find the index of the model child in the containing List, and add our new child EditPart using this index . When removing a child, we iterate through the existing children EditParts until we find the one representing the removed model child . We then remove this EditPart . Clearly, there is more work here than in simply calling refreshChildren(): but for large models where performance is critical, this effort will be worth it. Interested readers can examine handleInputChange(PropertyChangeEvent) and handleOuputChange(PropertyChangeEvent) in PropertyAwarePart for similar alternatives to refreshSourceConnections() and refreshTargetConnections() when updating relationships. Updating and Repainting the Display Consider our example of adding a column to a table. In draw2d terms, this is represented by adding an EditableLabel into a ColumnsFigure instance, which is itself contained within a TableFigure. Both the ColumnsFigure and the TableFigure both need to enlarge - the result otherwise is ugly (take my word for it!). A few things need to happen: 1. The cached information held by the layout managers for the TableFigure and ColumnsFigure, which includes minimum size and preferred size for the child figures, needs to be thrown away 2. The SchemaFigure's layout manager needs to update any cached constraint information it is holding for the TableFigure 3. The bounds of both the TableFigure and the ColumnsFigure need to change to reflect addition of the column 4. Any area affected by the change needs to be repainted In fact, all we need to achieve this is in our implementation of refreshVisuals() in TablePart: protected void refreshVisuals() { TableFigure tableFigure = (TableFigure) getFigure(); Point location = tableFigure.getLocation(); SchemaDiagramPart parent = (SchemaDiagramPart) getParent(); Rectangle constraint = new Rectangle(location.x, location.y, -1, -1); parent.setLayoutConstraint(this, tableFigure, constraint); } We get the location of our figure , use this to provide a new Rectangle constraint object . By setting the width and height to -1, we ensure that the preferred width and height are calculated automatically. We then pass on the constraint to the parent EditPart's layout manager . That's all there is to it. But how do we know that the preferred size calculation for the TableFigure or ColumnFigure won't be using some stale cached value? If you're interested in the answers to questions like this, read the sidebar below. Sidebar: Invalidating and Updating Figures How does GEF know when to rearrange and resize figures, and which parts of the screen to repaint? The key is in the methods in the IFigure interface and in the behavior of the update manager: , invalidate(): each Figure instance has a valid flag which can be set to true or false. When invalidate() is called: 1. the valid flag for the figure is set to false (the figure becomes invalid) 2. invalidate() is called on the Figure's layout manager. Here, any cached preferred size information that the layout manager holds for the figure is thrown away The importance of the invalid state will become more clear when we discuss validate(). , revalidate(): this method is used to invalidate() a figure and its parent chain. When called on a figure, this figure invalidates itself and then calls its parent's revalidate() method. The hierarchy's root figure (one with no parent) is finally placed on the update manager's queue of invalid root figures, that is, figures that need to be validated. revalidate() is called automatically when changes to figures occur which are likely to affect the bounds of parent figures. Its role can be clearly seen in its usages in the Figure and Label classes draw2d package, shown below: In our example of adding a column label to a table, revalidate() is automatically called when the new column label is added to the ColumnsFigure instance using the IFigure.addFigure(IFigure, Object, int) method. This is why correct resizing of the table occurs without having to invalidate any figures in our example's code. If no method is called which itself automatically invokes revalidate(), you may need to invoke this method yourself in your application code to correctly update the display. , repaint(): in the same way that revalidate() queues a figure with the update manager for validating, this method queues the figure's bounds as a dirty region for repainting. Like revalidate(), this method is automatically called in many places in draw2d, such as when the size of a figure changes. You are most likely to need to call this method in your application code if implementing custom subclasses of Figure. , validate(): this finishes the job that revalidate() started. The update manager calls validate() on each of the invalid root figures on the queue. During the validate() execution the following happens: 1. the figure's valid flag is set back to true 2. layout takes place - if the figure has a layout manager then its layout() method is called 3. the figure then validates each of its invalid children The value of revalidate() is in helping to ensure that only figures that need to participate in the validate and layout process can be correctly flagged as invalid before this process begins. After validating its invalid root figures, the update manager will repaint the enclosing rectangle for regions marked as dirty via the repaint() method. Conclusion We've covered quite a lot of ground in this article. Most significantly, we've talked about how you can use the basic building blocks of a GEF application to easily build an application which adheres to a clean MVC design. With the exception of the direct editing functionality, most of the other types of editing operations work in a very similar way to the simple column adding example presented. Of course, all of the building blocks need to be put together in the context of an Eclipse editor. Space limitations preclude any discussion of these topics, but interested readers can peruse the source code, as well as that of the official GEF examples, to see how this can be done. For more information on GEF, look at the Eclipse platform documentation, available via Eclipse online help if you download and install the GEF SDK. How to Get Started with the GEF gives a good introduction to GEF basics. Display a UML Diagram using Draw2D is a good starting point for those unfamiliar with Eclipse draw2d. Eclipse Development using the Graphical Editing Framework and the Eclipse Modeling Framework is an IBM Redbook providing more detailed coverage of GEF and EMF. You will also need to install EMF to get the Redbook examples to work. Acknowledgements Thanks to Randy Hudson and Jim des Rivières for their thorough and careful reviews, which have been very helpful in improving both the technical accuracy and readability of this article. Source Code To run the example or view the source code for this article, download and into your subdirectory. Note that you unzip schemaeditor.zip eclipse/ will need Eclipse 3.0 or later to run the examples. 3.4 Building an editor We now know the base classes and concepts of the Graphical Editing Framework, and we are ready to build our first graphical editor skeleton. In this section we explain how to get started and then go forward step-by-step. 3.4.1 The editor class First, we have to create the plug-in and then define the editor extension. We do not describe this here because it is a common process of the Eclipse plug-in programming model. The Eclipse documentation provides detailed information about this. By default, the editor class is created by extending org.eclipse.ui.part.EditorPart. This is the main class of the editor and is responsible for receiving the input, creating and configuring the viewer, handling the input, and saving the input. Typically you will already have a save option in your model, so we do not discuss the implementation of the methods save, isSaveAsAllowed, and saveAs here. As a result, we will have the class skeleton shown in Example 3-2. Example 3-2: ExampleGEFEditor.java (initial stage) /** * This is the example editor skeleton that is build * in Building an editor in chapter Introduction to GEF. * * @see org.eclipse.ui.part.EditorPart */ public class ExampleGEFEditor extends EditorPart { public ExampleGEFEditor() {} public void createPartControl(Composite parent) {} public void setFocus() { // what should be done if the editor gains focus? // it's your part } public void doSave(IProgressMonitor monitor) { // your save implementation here } public void doSaveAs() { // your save as implementation here } public boolean isDirty() { return false; } public boolean isSaveAsAllowed() { // your implementation here return false; } public void gotoMarker(IMarker marker) {} public void init(IEditorSite site, IEditorInput input) throws PartInitException {} } Note JavaDoc comments have been removed for readability reasons. 3.4.2 EditDomain Next we need an EditDomain. An EditDomain is an interface that logically bundles an editor, viewers, and tools. Therefore, it defines the real editor application. An EditDomain provides a CommandStack, which keeps track of all executed commands. This is necessary for undo and redo operations and useful to determine if the model was modified (is dirty) or not. Usually you will have one EditDomain per editor, but it is also possible to share an EditDomain across several editors in a multi-page editor. It is up to you when to create the EditDomain. It is possible to create it lazily. You can use the class EditDomain directly, however, the Graphical Editing Framework provides an implementation, which additionally knows about the editor that created it. This implementation is called DefaultEditDomain and used in our example shown in Example 3-3. Example 3-3: Adding EditDomain to the editor /** the EditDomain, will be initialized lazily */ private EditDomain editDomain; /** * Returns the EditDomain used by this editor. * @return the EditDomain used by this editor */ public EditDomain getEditDomain() { if (editDomain == null) editDomain = new DefaultEditDomain(this); return editDomain; } 3.4.3 CommandStack After adding the EditDomain, we have access to the CommandStack. We will use the CommandStack to indicate when an editor is dirty. Note If you ever execute a command yourself, please ensure that you execute it through the CommandStack. The CommandStack contains the method isDirty, which indicates if a CommandStack has executed commands after the last save. How does the CommandStack know about the last save? A CommandStack knows about this because we have to tell it whenever the editor is saved. This is not done by simply delegating the editors isDirty method to the CommandStack; instead, we need a listener that listens to CommandStack changes and updates the dirty state of our editor. Whenever this state changes, we need to inform the Eclipse workbench. But you need not be concerned about this. The superclass EditorPart provides methods for the last part. We start with the last part, as it is the easiest task. We simply add a flag for the dirty state and a setter that automatically fires an event, as shown in Example 3-4. Example 3-4: Indicating the dirty state of our editor (part 1) /** the dirty state */ private boolean isDirty; /** * Indicates if the editor has unsaved changes. * @see EditorPart#isDirty */ public boolean isDirty() { return isDirty; } /** * Sets the dirty state of this editor. * *

An event will be fired immediately if the new * state is different than the current one. * * @param dirty the new dirty state to set */ protected void setDirty(boolean dirty) { if(isDirty != dirty) { isDirty = dirty; firePropertyChange(IEditorPart.PROP_DIRTY); } } Now we implement the listener and attach it to the CommandStack as shown in Example 3-5. Example 3-5: The CommandStackListener /** * The CommandStackListener that listens for * CommandStack changes. */ private CommandStackListener commandStackListener = new CommandStackListener() { public void commandStackChanged(EventObject event) { setDirty(getCommandStack().isDirty()); } }; /** * Returns the CommandStack of this editor's * EditDomain. * * @return the CommandStack */ public CommandStack getCommandStack() { return getEditDomain().getCommandStack(); } /** * Returns the CommandStackListener. * @return the CommandStackListener */ protected CommandStackListener getCommandStackListener() { return commandStackListener; } Attaching the listener should be done when the editor gets it input, and removing it should be done in the editor's dispose method. See Example 3-6. Example 3-6: Attaching and removing the CommandStackListener /** * Initializes the editor. * @see EditorPart#init */ public void init(IEditorSite site, IEditorInput input) throws PartInitException { // store site and input setSite(site); setInput(input); // add CommandStackListener getCommandStack().addCommandStackListener(getCommandStackListener()); } /* (non-Javadoc) * @see org.eclipse.ui.IWorkbenchPart#dispose() */ public void dispose() { // remove CommandStackListener getCommandStack().removeCommandStackListener(getCommandStackListener( )); // important: always call super implementation of dispose super.dispose(); } Do not forget to update the CommandStack when the editor content is saved. See Example 3-7. Example 3-7: Update CommandStack on editor save /** * TODO: Implement "doSave". * @see EditorPart#doSave */ public void doSave(IProgressMonitor monitor) { // your implementation here // update CommandStack getCommandStack().markSaveLocation(); } /** * TODO: Implement "doSaveAs". * @see EditorPart#doSaveAs */ public void doSaveAs() { // your implementation here // update CommandStack getCommandStack().markSaveLocation(); } 3.4.4 Attaching the viewer The GraphicalViewer is the next element that must be integrated into our editor. The method createPartControl is the best location to do this. First we create a GraphicalViewer, then we configure this instance, and add it to the EditDomain. See Example 3-8. Example 3-8: Attaching a GraphicalViewer to our editor /** the graphical viewer */ private GraphicalViewer graphicalViewer; /** * Creates the controls of the editor. * @see EditorPart#createPartControl */ public void createPartControl(Composite parent) { graphicalViewer = createGraphicalViewer(parent); } /** * Creates a new GraphicalViewer, configures, registers * and initializes it. * * @param parent the parent composite * @return a new GraphicalViewer */ private GraphicalViewer createGraphicalViewer(Composite parent) { // create graphical viewer GraphicalViewer viewer = new ScrollingGraphicalViewer(); viewer.createControl(parent); // configure the viewer viewer.getControl().setBackground(parent.getBackground()); viewer.setRootEditPart(new ScalableFreeformRootEditPart()); // hook the viewer into the EditDomain getEditDomain().addViewer(viewer); // acticate the viewer as selection provider for Eclipse getSite().setSelectionProvider(viewer); // initialize the viewer with input viewer.setEditPartFactory(getEditPartFactory()); viewer.setContents(getContent()); return viewer; } /** * Returns the GraphicalViewer of this editor. * @return the GraphicalViewer */ public GraphicalViewer getGraphicalViewer() { return graphicalViewer; } /** * Returns the content of this editor * @return the model object */ protected Object getContent() { // todo return your model here return null; } /** * Returns the EditPartFactory that the * GraphicalViewer will use. * @return the EditPartFactory */ protected EditPartFactory getEditPartFactory() { // todo return your EditPartFactory here return null; } Note Although we have chosen to use the ScalableFreeformRootEditPart here, you are free to use whatever RootEditPart you like. 3.4.5 Being adaptable One of the key concepts inside Eclipse is the IAdaptable technology. It is also used within the Graphical Editing Framework. That is why we have to ensure that our editor implements this interface so that the GEF elements we have created provide adaptable behavior, which may be of interest to the Graphical Editing Framework itself or to other Eclipse code. So far, we have created the following important GEF elements: , EditDomain , GraphicalViewer EditDomain also provides access to a third important element, CommandStack. Example 3-9 shows how to provide adapter access to the elements in our sample editor. Example 3-9: Overwriting the getAdapter method /* (non-Javadoc) * @see org.eclipse.core.runtime.IAdaptable#getAdapter(java.lang.Class) */ public Object getAdapter(Class adapter) { // we need to handle common GEF elements we created if (adapter == GraphicalViewer.class || adapter == EditPartViewer.class) return getGraphicalViewer(); else if (adapter == CommandStack.class) return getCommandStack(); else if (adapter == EditDomain.class) return getEditDomain(); // the super implementation handles the rest return super.getAdapter(adapter); } 3.4.6 Introducing the palette The palette in GEF editors is the home for tools. But before we discuss tools, we need to create a palette inside our editor. The GEF palette is implemented reusing GEF technology; thus it has a model presented in a viewer (the PaletteViewer). The palette model The palette model is a simple model starting with a PaletteRoot. Each PaletteViewer needs a PaletteRoot. The PaletteRoot is a palette container (PaletteContainer). Palette containers are used to organize palette entries (PaletteEntry). Besides the PaletteRoot, there are two additional palette containers - PaletteGroup and PaletteDrawer. We suggest that you use both of them to organize your palette. Each provides a container for palette entries, but the PaletteGroup cannot be collapsed, while the PaletteDrawer can be collapsed. Additional information can be found in the GEF JavaDoc. Attaching the palette Attaching a palette is similar to attaching a viewer. First, we need to create a new PaletteViewer, as shown in Example 3-10. Example 3-10: Creating a PaletteViewer /** * Creates a new PaletteViewer, configures, registers * and initializes it. * @param parent the parent composite * @return a new PaletteViewer */ private PaletteViewer createPaletteViewer(Composite parent) { // create graphical viewer PaletteViewer viewer = new PaletteViewer(); viewer.createControl(parent); // hook the viewer into the EditDomain (only one palette per EditDomain) getEditDomain().setPaletteViewer(viewer); // important: the palette is initialized via EditDomain getEditDomain().setPaletteRoot(getPaletteRoot()); return viewer; } /** * Returns the PaletteRoot this editor's palette uses. * @return the PaletteRoot */ protected PaletteRoot getPaletteRoot() { // todo add your palette entries here return null; } Next, we need to add this viewer to the editor's composite. The SWT SashForm is used to have the palette's width modifiable, as shown in Example 3-11. Example 3-11: Adding the PaletteViewer to the editor's composite /** * Creates the controls of the editor. * @see EditorPart#createPartControl */ public void createPartControl(Composite parent) { SashForm sashForm = new SashForm(parent, SWT.HORIZONTAL); sashForm.setWeights(new int[] {30,70}); paletteViewer = createPaletteViewer(sashForm); graphicalViewer = createGraphicalViewer(sashForm); } /** the palette viewer */ private PaletteViewer paletteViewer; /** * Returns the PaletteViewer of this editor. * @return the PaletteViewer */ public PaletteViewer getPaletteViewer() { return paletteViewer; } There are several default tools available, and we need to add them so that we have an initial PaletteRoot. See Example 3-12. Example 3-12: Initial PaletteRoot with default tools /** the palette root */ private PaletteRoot paletteRoot; /** * Returns the PaletteRoot this editor's palette uses. * @return the PaletteRoot */ protected PaletteRoot getPaletteRoot() { if (null == paletteRoot) { // create root paletteRoot = new PaletteRoot(); List categories = new ArrayList(); // a group of default control tools PaletteGroup controls = new PaletteGroup("Controls"); // the selection tool ToolEntry tool = new SelectionToolEntry(); controls.add(tool); // use selection tool as default entry paletteRoot.setDefaultEntry(tool); // the marquee selection tool controls.add(new MarqueeToolEntry()); // a separator PaletteSeparator separator = new PaletteSeparator( EditorExamplePlugin.PLUGIN_ID + ".palette.seperator"); separator.setUserModificationPermission( PaletteEntry.PERMISSION_NO_MODIFICATION); controls.add(separator); // a tool for creating connection controls.add( new ConnectionCreationToolEntry( "Connections", "Create Connections", null, ImageDescriptor.createFromFile( getClass(), "icons/connection16.gif"), ImageDescriptor.createFromFile( getClass(), "icons/connection24.gif"))); // todo add your palette drawers and entries here // add all categroies to root paletteRoot.addAll(categories); } return paletteRoot; } Palette customizer It is possible to attach a palette customizer to the palette. This will enable the users of your editor to modify the palette to work in the way they prefer. For implementation details, please see our completed redbook sample application as described in Chapter 7, "Implementing the sample" on page 203, or the Logic example application provided from the GEF development team. 3.4.7 Actions Actions are common objects in the Eclipse workbench to do something when user requests are initiated through menu items, toolbar buttons or context menu items. The Graphical Editing Framework provides a set of standard actions and an infrastructure for using these actions within the Graphical Editing Framework. ActionRegistry The class org.eclipse.gef.actions.ActionRegistry serves as a container for editor actions. The editor is responsible for providing and maintaining an ActionRegistry. See Example 3-13. Example 3-13: Adding an ActionRegistry to the editor /** the editor's action registry */ private ActionRegistry actionRegistry; /** * Returns the action registry of this editor. * @return the action registry */ public ActionRegistry getActionRegistry() { if (actionRegistry == null) actionRegistry = new ActionRegistry(); return actionRegistry; } /* (non-Javadoc) * @see org.eclipse.core.runtime.IAdaptable#getAdapter(java.lang.Class) */ public Object getAdapter(Class adapter) { // we need to handle common GEF elements we created if (adapter == GraphicalViewer.class || adapter == EditPartViewer.class) return getGraphicalViewer(); else if (adapter == CommandStack.class) return getCommandStack(); else if (adapter == EditDomain.class) return getEditDomain(); else if (adapter == ActionRegistry.class) return getActionRegistry(); // the super implementation handles the rest return super.getAdapter(adapter); } /* (non-Javadoc) * @see org.eclipse.ui.IWorkbenchPart#dispose() */ public void dispose() { // remove CommandStackListener getCommandStack().removeCommandStackListener(getCommandStackListener()); // disposy the ActionRegistry (will dispose all actions) getActionRegistry().dispose(); // important: always call super implementation of dispose super.dispose(); } Managing actions As soon as we have the ActionRegistry, we are able to create actions and to register them. The Graphical Editing Framework provides a set of default actions (redo, undo, delete, print, and save). These actions need some special handling to stay up-to-date with the editor, the CommandStack or the EditParts. The GEF default actions are not implemented as listeners to some events. Instead, they have to be updated manually. This can be done from within the editor as shown in Example 3-14. Example 3-14: Added infrastructure for supporting different actions /** the list of action ids that are to EditPart actions */ private List editPartActionIDs = new ArrayList(); /** the list of action ids that are to CommandStack actions */ private List stackActionIDs = new ArrayList(); /** the list of action ids that are editor actions */ private List editorActionIDs = new ArrayList(); /** * Adds an EditPart action to this editor. * *

EditPart actions are actions that depend * and work on the selected EditParts. * * @param action the EditPart action */ protected void addEditPartAction(SelectionAction action) { getActionRegistry().registerAction(action); editPartActionIDs.add(action.getId()); } /** * Adds an CommandStack action to this editor. * *

CommandStack actions are actions that depend * and work on the CommandStack. * * @param action the CommandStack action */ protected void addStackAction(StackAction action) { getActionRegistry().registerAction(action); stackActionIDs.add(action.getId()); } /** * Adds an editor action to this editor. * *

ActionRegistry. * (This is a helper method.) * * @param action the action to add. */ protected void addAction(IAction action) { getActionRegistry().registerAction(action); } Now that we have the action infrastructure, we must implement the automatic updating of the different actions. Editor actions must be updated when the editor changes, CommandStack actions when the CommandStack changes, and EditPart actions when the selection changes. Example 3-15 shows how to add update support for actions to our sample editor. Example 3-15: Adding update support for the actions /** * Updates the specified actions. * * @param actionIds the list of ids of actions to update */ private void updateActions(List actionIds) { for (Iterator ids = actionIds.iterator(); ids.hasNext();) { IAction action = getActionRegistry().getAction(ids.next()); if (null != action && action instanceof UpdateAction) ((UpdateAction) action).update(); } } /** * The CommandStackListener that listens for * CommandStack changes. */ private CommandStackListener commandStackListener = new CommandStackListener() { public void commandStackChanged(EventObject event) { updateActions(stackActionIDs); setDirty(getCommandStack().isDirty()); } }; /** the selection listener */ private ISelectionListener selectionListener = new ISelectionListener() { public void selectionChanged(IWorkbenchPart part, ISelection selection) { updateActions(editPartActionIDs); } }; /** * Returns the selection listener. * * @return the ISelectionListener */ protected ISelectionListener getSelectionListener() { return selectionListener; } /** * Initializes the editor. * @see EditorPart#init */ public void init(IEditorSite site, IEditorInput input) throws PartInitException { // store site and input setSite(site); setInput(input); // add CommandStackListener getCommandStack().addCommandStackListener(getCommandStackListener()); // add selection change listener getSite() .getWorkbenchWindow() .getSelectionService() .addSelectionListener( getSelectionListener()); } /* (non-Javadoc) * @see org.eclipse.ui.IWorkbenchPart#dispose() */ public void dispose() { // remove CommandStackListener getCommandStack().removeCommandStackListener(getCommandStackListener( )); // remove selection listener getSite() .getWorkbenchWindow() .getSelectionService() .removeSelectionListener( getSelectionListener()); / disposy the ActionRegistry (will dispose all actions) getActionRegistry().dispose(); // important: always call super implementation of dispose super.dispose(); } /* (non-Javadoc) * @see org.eclipse.ui.part.WorkbenchPart#firePropertyChange(int) */ protected void firePropertyChange(int propertyId) { super.firePropertyChange(propertyId); updateActions(editorActionIDs); } Now, when all the infrastructure is ready, we are able to create and add our actions as shown in Example 3-16. Example 3-16: Adding actions to the editor /** * Initializes the editor. * @see EditorPart#init */ public void init(IEditorSite site, IEditorInput input) throws PartInitException { // store site and input setSite(site); setInput(input); // add CommandStackListener getCommandStack().addCommandStackListener(getCommandStackListener()); // add selection change listener getSite() .getWorkbenchWindow() .getSelectionService() .addSelectionListener( getSelectionListener()); // initialize actions createActions(); } /** * Creates actions and registers them to the ActionRegistry. */ protected void createActions() { addStackAction(new UndoAction(this)); addStackAction(new RedoAction(this)); addEditPartAction(new DeleteAction((IWorkbenchPart) this)); addEditorAction(new SaveAction(this)); addEditorAction(new PrintAction(this)); } 3.4.8 Adapting to the properties view EditParts are responsible for delivering IPropertySource adapters for the properties view, but this is not discussed here. The Graphical Editing Framework provides a solution to cover modifications occurred in the properties view into the CommandStack. This enables the possibility to undo and redo changes. To enable this, the editor must deliver its own IPropertySheetPage. This IPropertySheetPage is a default PropertySheetPage customized with an undoable root entry provided by GEF. See Example 3-17. Example 3-17: Making the properties view undoable /* (non-Javadoc) * @see org.eclipse.core.runtime.IAdaptable#getAdapter(java.lang.Class) */ public Object getAdapter(Class adapter) { // we need to handle common GEF elements we created if (adapter == GraphicalViewer.class || adapter == EditPartViewer.class) return getGraphicalViewer(); else if (adapter == CommandStack.class) return getCommandStack(); else if (adapter == EditDomain.class) return getEditDomain(); else if (adapter == ActionRegistry.class) return getActionRegistry(); else if (adapter == IPropertySheetPage.class) return getPropertySheetPage(); // the super implementation handles the rest return super.getAdapter(adapter); } /** the undoable IPropertySheetPage */ private PropertySheetPage undoablePropertySheetPage; /** * Returns the undoable PropertySheetPage for * this editor. * * @return the undoable PropertySheetPage */ protected PropertySheetPage getPropertySheetPage() { if (null == undoablePropertySheetPage) { undoablePropertySheetPage = new PropertySheetPage(); undoablePropertySheetPage.setRootEntry( GEFPlugin.createUndoablePropertySheetEntry(getCommandStack())); } return undoablePropertySheetPage; } 3.4.9 Providing an outline view Providing an outline view is handled in the typical Eclipse way. We need to provide an adapter of type IContentOutlinePage. We can do this in several ways. One way is to create a complete SWT based outline view without using the Graphical Editing Framework. In many cases this is suitable and can be easily done, because many components, such as content and label providers or even tree viewers, can be reused to show a tree of your model objects. If you do not have these reusable components available, then a second way is to build a tree with GEF components. The Graphical Editing Framework provides a TreeViewer and TreeEditParts for this case. You can also reuse actions created for your graphical editor. For details about implementing a model tree with the GEF TreeViewer and TreeEditParts, please see our redbook sample application. A third way is to provide an overview of your graphical editor. Example 3-18 and Example 3-19 show a possible implementation of this. Example 3-18: An overview outline page /** * This is a sample implementation of an outline page showing an * overview of a graphical editor. * * @author Gunnar Wagenknecht */ public class OverviewOutlinePage extends Page implements IContentOutlinePage { /** the control of the overview */ private Canvas overview; /** the root edit part */ private ScalableFreeformRootEditPart rootEditPart; /** the thumbnail */ private Thumbnail thumbnail; / ** * Creates a new OverviewOutlinePage instance. * @param rootEditPart the root edit part to show the overview from */ public OverviewOutlinePage(ScalableFreeformRootEditPart rootEditPart) { super(); this.rootEditPart = rootEditPart; } /* (non-Javadoc) * @see ISelectionProvider#addSelectionChangedListener * (ISelectionChangedListener) */ public void addSelectionChangedListener(ISelectionChangedListener listener) {} /* (non-Javadoc) * @see IPage#createControl(Composite) */ public void createControl(Composite parent) { // create canvas and lws overview = new Canvas(parent, SWT.NONE); LightweightSystem lws = new LightweightSystem(overview); // create thumbnail thumbnail = new ScrollableThumbnail((Viewport) rootEditPart.getFigure()); thumbnail.setBorder(new MarginBorder(3)); thumbnail.setSource( rootEditPart.getLayer(LayerConstants.PRINTABLE_LAYERS)); lws.setContents(thumbnail); } /* (non-Javadoc) * @see org.eclipse.ui.part.IPage#dispose() */ public void dispose() { if (null != thumbnail) thumbnail.deactivate(); super.dispose(); } /* (non-Javadoc) * @see org.eclipse.ui.part.IPage#getControl() */ public Control getControl() { return overview; } /* (non-Javadoc) * @see org.eclipse.jface.viewers.ISelectionProvider#getSelection() */ public ISelection getSelection() { return StructuredSelection.EMPTY; } /* (non-Javadoc) * @see ISelectionProvider#removeSelectionChangedListener * (ISelectionChangedListener) */ public void removeSelectionChangedListener( ISelectionChangedListener listener) {} /* (non-Javadoc) * @see org.eclipse.ui.part.IPage#setFocus() */ public void setFocus() { if (getControl() != null) getControl().setFocus(); } /* (non-Javadoc) * @see ISelectionProvider#setSelection(ISelection) */ public void setSelection(ISelection selection) {} } Example 3-19: Attaching the overview to the editor /* (non-Javadoc) * @see org.eclipse.core.runtime.IAdaptable#getAdapter(java.lang.Class) */ public Object getAdapter(Class adapter) { // we need to handle common GEF elements we created if (adapter == GraphicalViewer.class || adapter == EditPartViewer.class) return getGraphicalViewer(); else if (adapter == CommandStack.class) return getCommandStack(); else if (adapter == EditDomain.class) return getEditDomain(); else if (adapter == ActionRegistry.class) return getActionRegistry(); else if (adapter == IPropertySheetPage.class) return getPropertySheetPage(); else if (adapter == IContentOutlinePage.class) return getOverviewOutlinePage(); // the super implementation handles the rest return super.getAdapter(adapter); } /** the overview outline page */ private OverviewOutlinePage overviewOutlinePage; /** * Returns the overview for the outline view. * * @return the overview */ protected OverviewOutlinePage getOverviewOutlinePage() { if (null == overviewOutlinePage && null != getGraphicalViewer()) { RootEditPart rootEditPart = getGraphicalViewer().getRootEditPart(); if (rootEditPart instanceof ScalableFreeformRootEditPart) { overviewOutlinePage = new OverviewOutlinePage( (ScalableFreeformRootEditPart) rootEditPart); } } return overviewOutlinePage; } Now this page can be used in the editor, as shown in Example 3-19. 3.4.10 Controlling your editor with the keyboard The Graphical Editing Framework uses the concept of KeyHandlers to answer key strokes. By default, the anGEF GraphicalViewer does not answer key strokes. We have to enable this. This is not a difficult task, because as with all other GEF concepts, there is a default implementation available, which provides a very feature-rich set of keys for interacting with a GraphicalViewer. The default implementation is the class org.eclipse.gef.ui.parts.GraphicalViewerKeyHandler. Example 3-20 shows how to use this key handler with our editor sample. Example 3-20: Enabling our editor for keyboard interaction /** * Creates a new GraphicalViewer, configures, registers * and initializes it. * @param parent the parent composite * @return a new GraphicalViewer */ private GraphicalViewer createGraphicalViewer(Composite parent) { // create graphical viewer GraphicalViewer viewer = new ScrollingGraphicalViewer(); viewer.createControl(parent); // configure the viewer viewer.getControl().setBackground(parent.getBackground()); viewer.setRootEditPart(new ScalableFreeformRootEditPart()); viewer.setKeyHandler(new GraphicalViewerKeyHandler(viewer)); // hook the viewer into the EditDomain getEditDomain().addViewer(viewer); // acticate the viewer as selection provider for Eclipse getSite().setSelectionProvider(viewer); // initialize the viewer with input viewer.setEditPartFactory(getEditPartFactory()); viewer.setContents(getContent()); return viewer; } Tip If you like to attach actions to your own key strokes, you do not need to overwrite the GraphicalViewerKeyHandler. It is simply possible to attach a parent to KeyHandlers. Thus, you simply create your own KeyHandler instance (not GraphicalViewerKeyHandler), configure this KeyHandler instance, and set it as the parent of the GraphicalViewerKeyHandler you created for the GraphicalViewer.

本文档为【Eclipse插件开发:GEF入门系列宝典】,请使用软件OFFICE或WPS软件打开。作品中的文字与图均可以修改和编辑, 图片更改请在作品中右键图片并更换,文字修改请直接点击文字进行修改,也可以新增和删除文档中的内容。
该文档来自用户分享,如有侵权行为请发邮件ishare@vip.sina.com联系网站客服,我们会及时删除。
[版权声明] 本站所有资料为用户分享产生,若发现您的权利被侵害,请联系客服邮件isharekefu@iask.cn,我们尽快处理。
本作品所展示的图片、画像、字体、音乐的版权可能需版权方额外授权,请谨慎使用。
网站提供的党政主题相关内容(国旗、国徽、党徽..)目的在于配合国家政策宣传,仅限个人学习分享使用,禁止用于任何广告和商用目的。
下载需要: 免费 已有0 人下载
最新资料
资料动态
专题动态
is_511210
暂无简介~
格式:doc
大小:1MB
软件:Word
页数:217
分类:生活休闲
上传时间:2018-04-26
浏览量:83