使用 GMF 快速开发支持嵌套图元的编辑器

来源:developerWorks 中国 作者:闫 哲
  
GMF 是 Eclipse 项目中支持快速开发图形界面的工具包,本文介绍如何使用 GMF 建立嵌套图元来表达层次模型。结合应用示例,从建立领域模型、开发图形定义、工具定义、映射信息直到生成插件代码,详细描述了整个开发过程和其中容易疏忽的问题,并给出对应大纲视图的开发方法。

摘要

GMF(Graphical Modeling Framework)是 Eclipse 项目中支持快速开发图形界面的工具包,但是目前相关资料并不丰富,这给开发人员学习和实践带来了一定困难。实际应用中往往需要绘制带有层次关系的模型,由于层次的出现,在表达领域模型、图形绘制信息等方面就要额外处理。本文对比了建立层次模型的两种方法,重点介绍其中的嵌套图元方法。结合一个简单应用,从建立领域模型、开发图形定义、工具定义、映射信息直到生成插件代码,详细描述了整个开发过程和其中容易疏忽的问题,并给出图示。为了辅助显示模型层次,还进一步介绍了对应大纲视图的开发方法。





前言

Graphical Modeling Framework 是 eclipse 项目中支持快速开发图形界面的工具包,它有效地将 EMF 和 GEF 连接在一起,使得开发人员通过定制图形、工具、映射等抽象信息就可以快速创建出功能强大的 Eclipse 图形插件。

实际中往往需要绘制带有层次关系的模型,这相比开发只有一层的模型更为复杂。对于软件开发人员而言,最熟悉的场景莫过于在设计时为系统划分功能模块,继而设计代码包结构。“包”中包含代码“文件”,也可能包含其它“包”,这就构成了项目的层次结构。在同一个“包”中,“文件”之间的关系比较紧密,但由于系统复杂性,不同“包”的“文件”之间往往也存在依赖关系。怎样开发可以表达这种层次关系的编辑器呢?本文重点介绍使用 GMF 建立嵌套图元的方法。

嵌套图元是指通过相互嵌套的图元来表达层次关系,即外层图元作为容器,包含内层图元,从而直观的表达模型的层次结构。这种方法的特点是将不同层次的图元绘制在一个平面上,可以方便的建立各层次图元之间的关联,使得模型的表达方式更加灵活。对于前面提到的应用场景,嵌套图元方法比较适用。可以将“包”作为容器,“文件”作为最小单位的图元;容器可以相互嵌套,也可以包含最小图元,不同容器中的图元可以根据需要建立关联。不过,由于嵌套图元模型中既有全局信息,又有内部细节,复杂度不易控制,适用于规模较小的分布式系统。

与嵌套图元相比,还有另外一种方法可以表达层次关系,即将主模型和子模型相分离,主模型关注高层次的抽象图元,子模型则描述细节。由于主/子模型截然分开,难以表达模块间的复杂关系,适用于松散耦合的系统。两种方法如下图所示。后者在[3]中已有详细介绍,本文主要关注嵌套图元方法。

(A)通过嵌套图元表达层次关系 (B)主模型与子模型相分离

使用 GMF 技术开发编辑器的过程如下图所示。


图 1. GMF 项目开发过程
GMF 项目开发过程

首先创建 GMF 项目,分别然后建立领域模型、图形定义和工具定义,其中领域模型相当于业务的元模型,定义主要建模元素及其关系;图形定义用于表达各元素的图形绘制信息;工具定义则给出建立模型时需要的工具。在此基础上开发映射模型,将图形定义、工具定义与领域模型连接起来。继而得到生成模型,最后自动产生插件代码。以插件方式运行,即可以完成图形编辑器的基本功能了。

下面本文按照以上的开发顺序,介绍如何通过容器嵌套来表达图元的层次关系。





定义领域模型

领域模型是建模的基础,要想表达图元的嵌套关系,首先需要在该模型中建立包含(composition)关系。有了包含关系,在编辑器中包含的元素就体现为容器,被包含的元素体现为容器中的图元,而容器也可以包含另一个容器。领域模型有多种表达方式,GMF 开发中要求EMF模型,而大多数开发人员更熟悉UML模型。为了便于建立需要的领域模型,可以先使用 UML 工具建立类图,而后转化得到 EMF 模型。对于包含而言,UML 区分了组合 composition 和聚合 aggregation,在类图上体现为实心的菱形和空心的菱形,它们之间的区别在讲解面向对象技术的资料上都可以找到。而 EMF 模型没有做这种区分,它通过包含 contain 来表达,对应于 UML 中的组合关系。也就是说,如果 UML 模型用来转化生成 EMF 模型,就需要使用组合关系而不是聚合关系。

我们对前面的应用场景进行抽象,以便应用到更多的场景中。在目标模型中,“容器 Container”对应“包”,“实体 Entity”对应“文件”,“连接 Link”对应它们之间的依赖关系。于是我们定义如下的类图。


图 2. 类图-领域模型
类图-领域模型

在该模型中,类 Diagram 表示整个模型图,类 Node 和 Link 分别表示节点和它们之间的有向连线。从类 Node 派生得到类 Container 和 Entity,分别表示可以容纳其他节点的容器和不可再分的最小图元。而且,Link 通过 source 和 target 记录了起始节点和终止节点;相应的,Node 通过 outgoing 和 incoming 记录了发出和进入的连线。

[5]中已经介绍了如何借助 EclipseUML 插件建立 UML 模型,继而转换为 EMF 模型。本文不再赘述,生成的 EMF 模型如下图所示。

基于 EMF 模型,可以自动产生模型代码和编辑代码,前者定义了该模型中声明的类及其关联,后者定义了与这些类相关的绘图编辑信息。





定义图形信息

容器节点可以包含若干节点,在图形定义中我们需要使用适当的元素来表达,这里介绍两种方法。一种是使用 GMF2.0 新引入的元素——带标签的容器(Labeled Container),另一种是使用套件(Compartment)。前者本身就是表示容器的图元,它可以容纳其他图元,并且带有标签以显示标题。后者也作为容器,但是它不能单独使用,需要依附在某个图元上,并作为该图元的子元素存在。两者的开发和使用方法各不相同,这体现在本节的图形定义以及后面的映射定义等,本文将针对这两种方法分别进行介绍。

建立图形定义可以使用创建向导,依次设定文件的位置和名称、相关联的领域模型和对应模型图的类元素,最后通过选择框指明领域模型中的哪些类作为绘图节点,哪些类或引用作为连线,哪些属性作为标签。点击完成按钮就可以建立基本的图形框架,而后再根据需要调整图形信息。

图形定义——带标签的容器

图形定义中需要定义图形库和图元对象,图形库只关注绘图效果,通过图形描述符定义各种图形的形状、颜色等信息;而图元对象表示可以被添加到画布上的元素,它们使用图形库中的图形来绘制。


图 3. 使用带标签容器建立的图形定义
使用带标签容器建立的图形定义

在上图中,被选中的元素 ContainerFigure 就是带标签的容器,其中还包含用来显示标题的标签。图形库中还有描述实体图形的 EntityFigure 和描述连接图形的 LinkFigure,折线修饰符 Arrow 表示箭头,在 LinkFigure 的属性中将其指定为连线目标端的修饰符号。另外,画布中定义了图元对象 Container、Entity 和 Link 等,分别使用图形库中的描述符 ContainerFigureDes、EntityFigureDes 和 LinkFigureDes。

图形定义——套件

相比上面的方法,这里还要在图元对象部分增加套件对象的定义,以表明容器图元又包含了套件图元,其中再容纳其他图元。


图 4. 使用套件建立的图形定义
使用套件建立的图形定义

在上图中,容器图元使用圆角矩形,非容器图元使用直角矩形。被选中的就是套件图元,可以为其定制属性:Collapsible 表明是否可以折叠,Figure 指定所依赖的图形。它放置在容器图元上,因此指定图形为 ContainerFigureDes。





定义工具信息

工具信息用于配置调色板上的工具按钮。工具可分为标准工具、创建工具和通用工具,标准工具提供绘图中常用的选中、放大/缩小等功能,创建工具就是用来创建图元的,而通用工具没有功能限制,通过指定自定义的工具类来实现。GMF 框架已经内置了标准工具,一般情况下无需再添加,这里主要提供容器 Container、实体图元 Entity 和连接 Link 的创建工具。


图 5. 工具定义
工具定义

在上图中,将节点和连接分成两个工具组,中间用间隔符隔开。然后对应定义了三个创建工具,并使用了缺省的图标,如果需要替换为自定义图标,则需要使用 Bundle Image 并指明图标在项目中的相对路径。





定义映射信息

映射信息是 GMF 开发中至关重要的步骤,它将前期开发的图形定义、工具定义与 EMF 模型关联在一起,使得通过图形界面上的操作可以建立起所需的模型。根据内容不同,需要定义三类映射关系,即画布映射、节点映射和连接映射。画布映射在最外层定义了所建立的整个模型对应于领域模型中的哪个元素、图形定义中的画布以及工具定义中的调色板。类似的,节点映射/连接映射定义了对应的领域模型中的元素、图形定义中的节点和工具定义中的工具。

定义映射信息也可以使用创建向导,依次设定文件的位置和名称、相关联的领域模型和最外层类、工具定义以及图形定义文件,最后选择在映射中哪些元素作为节点,哪些作为连接。点击完成按钮就可以得到基本的映射框架,而后再根据需要进行调整。

映射定义——带标签的容器

本例需要分别为画布和两个节点——Container 和 Entity,以及一个连接——Link 进行映射。对于节点映射,GMF 规定首先建立顶层节点引用,它表示可以直接在画布中绘制的图元。本例依次建立两个这样的顶层节点引用,之下各自建立节点映射。进一步的,由于容器节点可以容纳其他节点,因此,我们在该节点映射之下再增加两个子节点引用,分别对应容器和实体节点。值得注意的是,这种引用关系并不像前面的操作,再次建立新的节点映射,而是直接借用已经在上面建立起来的节点映射,只需在属性“被引用子节点(Referenced Child)”中进行选择。


图 6. 使用带标签容器的映射定义
使用带标签容器的映射定义

以容器节点为例,在顶层节点引用中,指定包含特性(Containment Feature)为 nodes:Node,这表示该顶层节点与根元素 Diagram 具有 nodes 关系。其中进一步定义节点映射,指定对应的 EMF 模型元素为 Container 类,使用图形定义中的 Container 图元来显示,借助工具定义中的 Container 工具来创建。该节点映射中进而定义两个子节点引用,一个表示 Entity 节点,其包含特性为 elements:Node,被引用子节点为之前已经定义的 Entity 节点映射;另一个表示 Container 节点,其包含特性也是 elements:Node,被引用子节点是已定义的 Container 节点映射。下面简要给出该编辑器中其他项目的属性信息。

Canvas Mapping
	Domain Model: HierarchyDemo
	Element: Diagram
	Diagram Canvas: Canvas HierarchyDemo
Top Node Reference <nodes:Entity/Entity>
Containment Feature: nodes:Node
Node Mapping<Entity/Entity>
Element: Entity->Node
Diagram Node: Node Entity(EntityFigureDes)
Tool: Creation Tool Entity
Link Mapping
	Containment Feature: links:Link
	Element: Link
	Source Feature: source:Node
	Target Feature: target:Node
	Diagram Link: Connection Link
	Tool: Creation Tool Link

映射定义——套件

相比上面的方法,映射定义主要的还是为容器节点、实体节点以及连接建立映射,但由于该方法的图形定义中增加了套件图元用于容纳子图元,所以还需要为其定义映射关系。


图 7. 使用套件的映射定义
使用套件的映射定义

在上图中,顶层节点引用和子节点引用的设置与第一种方法基本相同,区别在于套件图元的映射。根据前面的介绍,套件实际上是容器节点的子元素,所以在映射定义中,套件的映射也应该定义在容器映射之中。图中选中部分就是该套件映射,它对应图形定义中的套件图元,可以容纳 Container 和 Entity 两个子节点引用。实际上,这种包含关系也可以在子节点引用的属性“套件 Compartment”中进行指定。





产生“代码生成信息”

前面建立的映射信息可以自动产生一个特定文件,该文件称为代码生成文件,较详细的描述了如何生成图形编辑器的代码。该文件中直接对应 EditPart,它是 GMF 最终代码中的元素,代表显示在编辑器中的图元。从映射文件到代码生成文件,再到最终代码,是抽象层次不断降低的结果,这与 Model Driven Architecture 具有相似的原则。

生成文件——带标签的容器


图 8. 使用带标签容器的代码生成文件
使用带标签容器的代码生成文件

代码生成文件描述了如何生成模型图,包括生成顶层节点、子节点和连接。模型图 DiagramEditPart 的 Viewmap 和容器 EditPart(包括 ContainerEditPart 和 Container2EditPart)的 Viewmap,描述如何在图形上绘制被容纳的内容,其布局属性定义内容图元的布局,缺省的 UNKNOWN 以及 XY_LAYOUT 类型表示根据鼠标位置来布局,并支持随意拖动容器内的图元位置。

代码生成文件可以自动产生 GMF 代码,缺省情况下生成一个插件项目,并依赖于前面建立的 EMF 模型和编辑项目。至此,已经开发出一个满足基本要求的图形编辑器,以 Eclipse 插件方式运行,就可以进行新建、编辑、保存模型等常规操作。如下图所示。


图 9. 使用带标签容器实现的编辑器界面
使用带标签容器实现的编辑器界面

生成文件——套件


图 10. 使用套件的代码生成文件
使用套件的代码生成文件

与上面的方法相比,代码生成文件中增加了两个套件对象,即 ContainerContainerComparmentEditpart 和 ContainerContainerCompartment2EditPart,分别对应顶层容器节点和子节点容器的套件 Editpart。

基于该生成文件,自动生成项目代码,运行界面如下图所示。


图 11. 使用套件建立的编辑器界面
使用套件建立的编辑器界面

以上过程开发的 GMF 代码只是建立了图形编辑器的基本框架,而其他与应用有关的特定需求还要进行定制或开发。本文所关注的模型层次是在图中通过嵌套图元来表达,而树形结构能够更清晰的表达层次,往往作为模型图的一种补充方式。这里可以借用 Eclipse 常用的大纲视图。GMF 自动生成的代码对于大纲视图的支持比较弱,在图元嵌套的情况下,不能完整显示模型的结构,因此我们需要扩展开发支持多层模型的大纲视图。





开发大纲视图

在自动生成的插件项目中,有代表编辑器的 XXXEditor 类,它在 plugin.xml 中注册为 org.eclipse.ui.editors 扩展点元素。Editor 类中有继承得到的 getOutlineViewEditPartFactory 方法,它返回 EditPartFactory 对象,用于创建作为 Outline Tree 节点元素的 EditPart 对象。根据前面的介绍,模型图上的元素也是 EditPart 对象,它和 Outline 中的 EditPart 对象相对应。GMF 中的 EditPart 在 MVC 架构中承担 Controller 的作用,它将 Model——EMF 业务模型和 View——Draw2D 图形连接在一起。而编辑器中的 EditPart 和 Outline 中的 EditPart 正是同一个 Model 的不同 Controller,从而呈现出不同的 View。因此,为了正确显示模型层次,需要重定义 getOutlineViewEditPartFactory 方法。

分析元模型可知,从模型层次角度来分,有三类元素,即顶层的图元素、中间层的容器元素以及底层的叶节点元素,它们在处理层次关系时体现不同特点。由此,分别定义 DiagramTreeEditPart、ContainerTreeEditPart 和 LeafTreeEditPart,有如下的方法实现。

public EditPartFactory getOutlineViewEditPartFactory() {
     return new EditPartFactory() {
          public EditPart createEditPart(EditPart context, Object model) {
               if (model instanceof Diagram) {
                    return new DiagramTreeEditPart(model);
               } else if (model instanceof Node) {
                    EObject objInModel = ((Node) model).getElement();
                    if (objInModel instanceof HierarchyDemo.Container)
                         return new ContainerTreeEditPart(model);
                    else
                         return new LeafTreeEditPart(model);
               } else
                    return null;
         }
     };
}

下面该定义这三个 TreeEditPart 了。一般的 TreeEditPart 中有两个主要方法,getText() 返回显示在树中的节点名称,本例直接使用模型中的 name 属性;getImage() 则返回节点图标。作为容器的 DiagramTreeEditPart 和 ContainerTreeEditPart 还需要实现 getModelChildren() 方法,它用于返回子节点列表,是建立层次关系的关键方法。由于前面的开发分别采用了带标签的容器和套件两种方法,它们在表达层次关系时略有不同,因此 getModelChildren 也有不同的实现方式。

大纲视图——带标签的容器

在元模型中,Diagram 包含 Node,所以 DiagramTreeEditPart 的 getModelChildren() 可以返回模型的子元素,即 model.getChildren()。带标签的容器中,第一个子元素就是标签,因此实际包含的图元是除了标签之外的其它子元素。

//ContainerTreeEditPart
protected List getModelChildren() {
     if(getModel() instanceof View){
          //remove the first label
          EList children = ((View)getModel()).getChildren();
          List realChildren = new ArrayList(children.size()-1);
          for(int i=0;i<children.size()-1;i++){
               realChildren.add(children.get(i+1));
          }
          return realChildren;
     }
     return null;
}

大纲视图——套件

由于套件作为容器 Container 的子元素,套件中容纳的图元与 Container 本身并不是直接包含的关系。而在显示层次关系的 Outline 树结构中,Container 之下应该是逻辑上包含的图元,这样 ContainerTreeEditPart 就返回对应 compartment 的子元素列表。但是,这并不正确,因为缺少了 Compartment 这一层,没有接收对应事件的主体,在其中进行的编辑操作(如添加子节点)无法及时体现在树中。可以换一个角度,由于 Compartment 与所依赖的 Container 对应同样的模型元素,outline 树结构中的容器节点就可以对应于 Compartment,这样既可以显示模型信息,又能及时反映层次的变化。DiagramTreeEditPart 和 ContainerTreeEditPart 具有相同的 getModelChildren 方法。

//DiagramTreeEditPart & ContainerTreeEditPart
protected List getModelChildren() {
     ArrayList resultList =new ArrayList();
     if(getModel() instanceof View){
          List initList=((View)getModel()).getChildren();
          for(int i=0;i<initList.size();i++){
               View item=(View)initList.get(i);
               if(item.getType().equals(
                 new Integer(ContainerEditPart.VISUAL_ID).toString())
                 || item.getType().equals(
                   new Integer(Container2EditPart.VISUAL_ID).toString())){
                    resultList.add(item.getChildren().get(1));
               }else
                    resultList.add(item);
          }
     }
     return resultList;
}





小结

GMF 技术将 EMF 与 GEF 连接起来,为快速开发 Eclipse 图形插件提供了有力支持。本文针对实际情况中带有层次关系的模型,介绍了如何使用 GMF 开发支持嵌套图元的编辑器。其中分为两个方法,一个是带标签的容器,另一个是套件。它们都具有容器功能,可以容纳其他图元,但在开发和使用上存在一些差异,现比较如下。

  1. 套件是 GMF1.0 版本就支持的概念,而带标签的容器是 GMF2.0 版本新增加的。如果开发环境仅要求 GMF1.0,那么只能选择套件了。
  2. 从开发过程来看,套件作为容器图元的子元素,在图形定义和映射定义中都需要考虑这一特殊元素的处理;而带标签的容器本身就是容器图元,可以更直接的表达层次关系。
  3. 套件可以折叠,以隐藏内部包含的图元及其与外部图元的连接,而带标签的容器无此功能。

GMF 技术也在不断发展中,希望经过我们的共同努力,它成为我们开发图形界面的有效工具。(责任编辑:A6)


时间:2008-10-10 09:40 来源:developerWorks 中国 作者:闫 哲 原文链接

好文,顶一下
(0)
0%
文章真差,踩一下
(4)
100%
------分隔线----------------------------


把开源带在你的身边-精美linux小纪念品
无觅相关文章插件,快速提升流量