内容


Graphical Modeling Framework 进阶

Comments

前言

在 GMF 出现之前我们在做一个基于模型的图形化工具时,通常用 EMF 来创建后台模型, 用 GEF 来做用户界面和后台模型的同步,这需要花费相当的时间去了解 EMF 的模型设计方法和 GEF 的 MVC 框架。

为了简化开发过程,GMF 对 EMF 和 GEF 做了进一步的封装,提供了一套图形化的开发环境和运行时框架,开发人员可在图形环境中轻松定制自己的领域模型,并基于该模型一步一步地进行设计开发。最后 GMF 根据用户的定制情况,自动生成代码框架。

本文以 zachman view 为例子,主要介绍在用 GMF 生成一个 zachman 的 diagram 后,如何对代码作进一步的高级扩展以满足具体的要求,下面将在 5 个方面进行扩展:

1. 自定义布局管理器和背景色

2. 定制上下文菜单

3. 动态加载 Palette Entry

4. 定制 EditPolicy 和 Command

5. 模型转换

GMF 简介

GMF 框架主要包括两个部分:运行时框架,图形化开发环境。图形开发环境依赖底层的运行时框架。

图 1 GMF 组件
图 1 GMF 组件
图 1 GMF 组件

如图 1 所示,GMF Editor 主要基于 GMF Runtime 来构造应用框架,但同时允许开发人员直接使用 EMF、GEF 和 Eclipse 系统接口来完成一些特定功能(GMF 不能很好支持的功能)。

GMF 的图形化开发环境

GMF 编辑器的主要功能是用来定义一组用来生成代码框架的模型,主要包括:graphical model, tool model 和 map model。

图 2 GMF 模型
图 2 GMF 模型
图 2 GMF 模型

如图 2,GMF 编辑器首先需要一个 domain model,通常是一个 *.ecore 文件,GMF 根据这个 domain model 做进一步的操作,这个 model 是实施 GMF 后续工作的基础。

  • graphical model:定义了 ecore model 元素的图形信息,如形状,颜色,边框等等。严格的说这个图形信息是和 ecore model 无关的(以便于领域模型和用户界面模型的映射),但是在具体的操作过程中我们都是根据 ecore model 来生成其所对应的 graphical model.
  • tool model:定义了在未来生成的图形编辑器中 palette view 里面的 palette entry 信息。 如名字,图标。
  • map model:定义了 ecore model, graphical model 和 tool model 三者间的映射关系,通过 map model 来调整其他 3 个 model 的映射关系可以增强 model的可重用性和灵活性。
  • generator model:由 map model 结合其他三个 model 生成,最终 GMF Runtime 根据 generator model 来生成 diagram editor 的代码框架。可以通过调整 generator model 的一些属性来做一些个性化的工作。比如:默认 GMF 框架会把 ecore model 和 notation model(diagram model) 分开来保存,可以通过修改其属性"Same File For Diagram And Model" 来使 model 和 view 保存在一个文件中,等等。

GMF 进阶

环境:

GMF 可以方便的为开发人员生成模型的 diagram 框架,和一些基本的功能。本文以 zachman view 为例子具体讲述在用 GMF 生成一个 diagram 框架后,如何进一步对代码进行扩展以满足具体的要求。

图 3 是 zachman view 的 ecore model, zachman 由行和列组成,每个列包含一些 CItem 对象,每个行引用一个或多个 CItem 对象,每个 CItem 关联一个特定的行。每个 CItem 包含 0 个或多个属性。

图 3 zachman ecore model
图 3 zachman ecore model
图 3 zachman ecore model

根据图3的 ecore model,我们可以借助 GMF 的图形化开发环境快速地生成与之对应的 graphical model, tool model 和 map model,进而由 generator model 生成 diagram 的基本框架(详细步骤可参考 15 分钟学会 Eclipse GMFGMF Tutorial)。生成的 diagram 如下图 4,

图 4 生成的 Zachman view diagram
图 4 生成的 Zachman view diagram
图 4 生成的 Zachman view diagram

图4 中,用户必须自己调整每个元素的位置,其中的行和列对象不能像表格一样自动调整布局,Palette Viewer 上的 entry 项也没有办法进行定制,此外,弹出菜单上也有好多的无关项容易引起误会,等等。

期望的 diagram 如下图5

图 5 扩展后的 Zachman view diagram
图 5 扩展后的 Zachman view diagram
图 5 扩展后的 Zachman view diagram

在图 5 中,扩展了 Zachman 的布局管理器,行和列将自动调整它们的布局,修改了上下文菜单,去掉了无关的菜单项,同时增加了用户自定义的 palette entry 项等等。下面的章节将对这些实现的细节进行详细说明。

自定义布局管理和背景色

在图 4 中,每个行(列)的位置是由创建对象时鼠标的位置决定的,调整一个列的高度不会引起其他列的高度的同步改变,因此,为了实现行和列的自动按表格布局,行列不重叠,列同高,行同宽,需要对 Zachman view 的布局管理器进行定制。

Zachman ecore model 中根元素 Zachman 和其中的每个元素生成对应的 EditPart,每个 EditPart 中定义了一个 Figure 对象用来表示元素的图形化信息,这个 Figure 的属性和外观特点就是通过在 graphical model 里的描述生成的。每个 Figure 有一个布局管理器,由它来管理其中的子元素的布局位置。通常根元素的所对应的布局管理器是 FreeformLayout,该布局管理器中,用户可随意指定子对象位置大小。下面将解决如何扩展 FreeformLayout 来定制根的子元素的布局。

首先需要定一个布局管理 ZachmanDiagramLayout,

清单 1 自定义布局管理器
public class ZachmanDiagramLayout extends FreeformLayout {
  ... 
  public void layout(IFigure parent) {
    Iterator cit = parent.getChildren().iterator();
    IFigure f;
    ...
    while(cit.hasNext()){
      f = (IFigure)cit.next();
      Rectangle bounds = (Rectangle) getConstraint(f);
      
      //计算出f的bounds
      
      f.setBounds(bounds);
      setConstraint(f, bounds);
      ...
    }
  }
  ...
}

其次,安装 ZachmanDiagramLayout。根 ZachmanEditPart 元素扩展的是 DiagramEditPart,其定义了一个方法 protected IFigure createFigure() {…},由这个方法返回根元素 ZachmanEditPart 的图形对象,只要给它设置 ZachmanDiagramLayout 就可以了。如下

清单 2 安装布局管理器
protected IFigure createFigure() {
	IFigure f = super.createFigure();
	ZachmanDiagramLayout layout = new ZachmanDiagramLayout();
	f.setLayoutManager(layout);
	
	return f;
}

如果要定制某些对象的布局,都可以按照上面两个步骤通过给其父对象安装布局管理器来实现。

如果想要修改 ZachmanDiagramEditor 的背景色,可以通过 GMF editor 的 getGraphicalViewer().getControl() 获取 editor 的画布,调用其 setBackgraoundColor(…) 方法就可以给 editor 设置新的背景色了,如果加上调用画布的 redraw() 方法,就可以随需变换 editor 的背景色了。

定制上下文菜单

ZachmanDiagramEditor 自带了很多系统的菜单项,如图6

图 6 上下文菜单
图 6 上下文菜单

在实际的应用中,很多菜单项都是多余的,如何把多余的菜单项去掉并且添加自定义菜单呢?

GMF 提供了一个扩展点 org.eclipse.gmf.runtime.common.ui.services.action.contributionItemProviders。这个扩展点有一个子项 contributionItemProvider,它对应的 class 为 com.ibm.eis.soaif.smm.model.diagram.providers.DiagramContributionItemProvider,由这个 DiagramContributionItemProvider 决定了 GMF Editor 上 contextMenu 的内容,所以要需要重写 ContextMenu 的这个 provider 来定制菜单内容。

清单 3 定制上下文菜单
public class ZachmanContextMenuProvider extends DiagramContextMenuProvider {
   public void buildContextMenu(IMenuManager menu) {
     ...
     // 添加用户自定义菜单项
     // ...
     super.buildContextMenu(menu);
   }
// 过滤不需要的上下文菜单
   public IContributionItem[] getItems() {
     IContributionItem[] ic = super.getItems();
     filterSystemPopMenu(ic);
     return ic; 
   }
}

通过 filterSystemPopMenu(),过滤掉不需要的系统菜单信息。一个 IContributionItem 对应一个菜单信息,通过设置其为不可见,可以过滤掉相应的菜单项。

添加用户自定义 Action 到上下文菜单,首先定义一个 Action

清单 4 自定义 Action
public class SampleAction extends Action { 
public SampleAction(String text) {
     super(text);
     setId("Zachman.sample");
     setText("Sample");
 }
 @Override
public void run() {
    super.run();
}
}

添加 SampleAction 到上下文菜单中去

清单 5 添加自定义 Action 到上下文菜单
public void buildContextMenu(IMenuManager menu) {
   SampleAction action = new SampleAction("Sample");
   menu.add(action);
   super.buildContextMenu(menu);
 }

最后,需要用 ZachmanContextMenuProvider 去替换 ZachmanDiagramEditor 原有的 DiagramContextMenuProvider,修改 ZachmanDiagramEditor 的 configureGraphicalViewer() 方法如下:

清单 6 为 Editor 安装 ContextMenuProvider
protected void configureGraphicalViewer() {
  super.configureGraphicalViewer();
  IDiagramGraphicalViewer viewer = getDiagramGraphicalViewer();
  /* customize popup menu */
  ContextMenuProvider provider = new ZachmanContextMenuProvider(this,viewer);
  viewer.setContextMenu(provider);
  getSite().registerContextMenu(ActionIds.DIAGRAM_EDITOR_CONTEXT_MENU
	  ,provider
	  ,viewer);    
}

新的上下文菜单如下图7

图 7 用户自定义菜单
图 7 用户自定义菜单

动态加载 Palette Entry

在图 3 中,CItem 包含多个属性项,根据属性项的变化,我们希望能够在 palette viewer上同步更新 palette viewer 上的 entry 项。

GMF 生成的 Palette View 上提供了用户在 tool model 里面定义的 palette entry, 但是它们是静态的,如何像 palette view 上根据需要动态添加 palette entry 呢?

首先,每个 palette Viewer 如下

图 8 palette Viewer
图 8 palette Viewer

红色区域是一个 PaletteContainer,它里面有 Column, Row 和 CItem 三个 ToolEntry。每个 PaletteViewer 对应一个 PaletteRoot, PaletteRoot 是一个 PaletteContainer,它是 PaletteViewer 的根容器。下面介绍如何向 palette viewer 添加一个 Sample 属性项。

1. 定义 Tool, 由 Tool 完成 ToolEntry 在接收到鼠标事件的具体动作。

清单 7 SampleCreationTool
public class SampleCreationTool extends SelectionTool {
protected boolean handleButtonDown(int button)
    {
        if(button == 3){
    	        //TODO right click event
    	    }
    	    if(button == 1){
            //TODO left click event 
		    
/* set the default palette selection */
getDomain().loadDefaultTool();
}
}

2. 定义 ToolEntry, ToolEntry 是 PaletteViewer 里面可以显示的一个 entry,如图8中的 "Zoom"、"Column" 等。

清单 8 定义 ToolEntry
public class AttibuteCreationToolEntry extends ToolEntry{

	public Tool createTool() {
		SampleCreationTool tool = new SampleCreationTool();
    tool.setUnloadWhenFinished(false);
        
    return tool;
	}       
}

3. 归类 ToolEntry 项。PaletteDrawer 是对 PaletteContainer 的一个扩展,这里为 CItem 创建一个 Sample 属性来归类属于 Sample 的 ToolEntry

a. 创建一个 PaletteDrawer,名字为 Sample。

b. 添加 ToolEntry 为 Sample 的子项。如图 9 的 new, quarter, half 项等等

c. 然后把这个 PaletteDrawer 添加到 PaletteRoot 中去,刷新 PaletteViewer 就可以看到新添加的 PaletteEntry 项如图 9

图 9 用户自定义 PaletteViewer
图 9 用户自定义 PaletteViewer

Sample 是为 CItem 新添加的属性,通过单击它可以显示/隐藏它所包含的 ToolEntry("new","quarter","half","three-quarters","full")。

定制 EditPolicy 和 Command

GMF 沿用了 GEF 中定义的 EditPolicy 和 Command 机制来完成对应用模型的操作和用户界面交互。这种设计提高了代码的复用和灵活性,同一个 EditPolicy 可以被安装到多个 EditPart 上,而同一个 Command 也可以被多个 EditPolicy 重用。

通常,每个 EditPart 都会安装一些类型的 EditPolicy,每个 EditPolicy 对应一个 Role,用一个字符串常量来标示,发送到 EditPart 的请求都会被根据类型委托给合适的 EditPolicy,由 EditPolicy 根据请求的特点决定操作细节和否创建 Command 并执行。

GMF 除了支持 GEF 的 EditPolicy 外,还对增加了种类丰富的 EditPolicy,常用的如:

CreationEditPolicy: 用于元素的创建操作,模型的同步,显示方式等。

DragDropEditPolicy: 对象拖放时的行为控制,模型的同步,用户界面交互等等。

SemanticEditPolicy: 处理语意相关的操作,如对模型元素的删除操作。

在创建一个 CItem 对象时,要求出现一个插入线提示即将创建新对象的位置如图 10,同时要求创建一个新 CItem 的同时,建立起它和 Row 对象的关联。而这些都是自动生成的代码所无法提供的。下面介绍如何通过 EditPolicy 和 Command 实现上面的两个要求。

图 10 自定义 Editpolicy 和 Command
图 10 自定义 Editpolicy 和 Command
图 10 自定义 Editpolicy 和 Command

1. 添加插入线。当选中 palette viewer 上的 CItem 在编辑器里创建一个对象时,由安装在 CItem 上的 CreationEditPolicy 来处理这个请求,这里扩展一个新的 CItemCreationEditPolicy 来覆盖掉 CItem 原来的 CreationEditPolicy,提供对插入线的控制,见清单 9。由 updateInsertLine(…) 来控制插入线的位置和长度。

清单 9 CItemCreationEditPolicy
public class CItemCreationEditPolicy extends CreationEditPolicy {

	protected Command getCreateCommand(CreateViewRequest request) {
		Command cc = super.getCreateCommand(request);
		
		// update insert line 
		updateInsertLine(request);

		TransactionalEditingDomain editingDomain = ((IGraphicalEditPart) getHost())
				.getEditingDomain();		
		CompoundCommand cm = new CompoundCommand();
		cm.add(cc);
		
		UpdateRefsCommand cmd1 = new UpdateRefsCommand(editingDomain,
"Create Item",getDATable());
		Command cmd2 = new ICommandProxy(cmd1);
		cm.add(cmd2);
		
		return new ICommandProxy(new CommandProxy(cm.unwrap())); 
	}

	private void updateInsertLine(CreateViewRequest creq) {		
		IGraphicalEditPart igp = (IGraphicalEditPart)getHost();
		Point location = getPreferredLocation(creq);
		int width = igp.getFigure().getBounds().width;
		
		if(location == null){
			ZachmanUtil.getCurrentDiagramEditor().bgLayer.eraseInsertedLine();
			return ;
		}
		
		Point startP = location;
		Point endP = new Point (startP.x+width,startP.y);
		
		ZachmanUtil.getCurrentDiagramEditor().bgLayer.setLineToShow(startP, endP);		
		ZachmanUtil.getCurrentDiagramEditor().bgLayer.repaint();
		
	}
	
}

2. 同步 CItem 和 Row 的关联。清单 9 中,在创建完一个 CItem 对象后,在 CommandStack 里面添加 UpdateRefsCommand 来建立 CItem 对象和 Row 的关联,由 UpdateRefsCommand 来完成关联的创建。

清单 10 UpdateRefsCommand
public class UpdateRefsCommand extends AbstractTransactionalCommand {
	
	...
	
	@Override
	protected CommandResult doExecuteWithResult(IProgressMonitor monitor,
			IAdaptable info) throws ExecutionException {
		updateRefs(getCurrentItem());
		return CommandResult.newOKCommandResult();
	}
	
	private updateRefs(CItem item){
		Row row = getRow(item);
		item.setRRow(row);
		row.getRItem().add(item);
	}
	...
	
}

模型转换

Zachman View 希望能把第三方的 zachman model 导入到图 3 的 zachman view ecore model 来使用和管理,因此,这里需要做模型的转换与显示。

GMF 的模型分两部分,一部分是 ecore model,一个是用于显示信息的 notation(diagram) model,也就是 diagram 的信息。在把一个模型转换成 zachman view ecore model 并在 diagram 中显示和管理,这里需要处理两件事情:

1. 把第三方 model 转换成 zachman view 的 ecore model;

2. 需要根据生成的 zachman view 的 ecore model 生成对应的 notation model。

ecore model 间的转换有很多方法,IBM 提供了一个很好的工具来做这种转换:MTF(Model Transformation Framework),可参见Model Transformation with the IBM Model Transformation Framework来做 ecore model 的转换。下面将重点介绍如何利用 GMF API 生成 notation model 的方法。

GMF 生成的 ZachmanDiagramEditor 中有一个 setDocumentProvider(IEditorInput input) 方法,这个方法为 editor 设置了一个 DocumentProvider,由这个 provider 负责 model 的保存和读取的操作。所以,这里计划在读取 zachman model 时,动态生成其对应的 notation model。

1. 当双击 zachman model 文件时,DocumentProvider 使用 setDocumentContent(IDocument document,IEditorInput editorInput) 方法来载入 model 文件,其调用 DiagramIOUtil 的 load(…) 方法来完成文件到 Resource 的载入。在载入过程中,load 方法会查找 model 中的 notation model,如果没有查找到,会抛出一个 NO_DIAGRAM_IN_RESOUCE 异常。在由 zachman ecore model 生成 notation model 的过程中,我们捕获这个异常,在其处理方法中完成对 notatoin model 的创建。

2. 为 zachman ecore model 创建一个 diagram,如下,首先根据 ecore model 创建一个 diagram,然后根据 ecore model 为 diagram 添加具体的 notation model 的信息。

清单 11 创建 diagram
private Diagram getDiagramForECoreModel(Zachman model) {
		Diagram diagram = null;

		diagram = ViewService.createDiagram(
								(EObject) model,
								"zachman",
				ZachmanDiagramEditorPlugin.DIAGRAM_PREFERENCES_HINT);

		// genarate diagram notation model
		generateDiagramModel(model, diagram);

		return diagram;
}

3. 由 ecore model 生成 notation model,下面的代码是创建 model 中 Column 元素对应的 View model 的代码,这里需要注意的是要根据不同的元素类型,创建相应的 CreateElementRequestAdapter,它包含了创建 View 的一些必须信息。

清单 12 为每个 model 元素创建对应的 View
private void generateDiagramModel(Zachman model, Diagram diagram) {
	boolean persisted = true;
	
	// create column view model
	Iterator dit = model.getColumns().iterator();
	ColumnViewFactory dvf = new ColumnViewFactory();
	CreateElementRequestAdapter dSemanticAdapter = dvf
			.getCreateSemanticAdapter();

	IElementType dElementType = ZachmanElementTypes.Column_1001;
	String dSemanticHint = ((IHintedType) dElementType).getSemanticHint();

	while (dit.hasNext()) {
		Column column = (Column) dit.next();
		dSemanticAdapter.setNewElement(column);
		View view = dvf.createView(dSemanticAdapter, diagram, dSemanticHint, -1,
				persisted, dvf.getColumnPreferencesHint());
	}
}

4. 设置 Document 的内容,在步骤1中的 setDocumentContent(…) 方法中的 document 参数有 setContect() 方法,对步骤3返回的 diagram,调用 document.setContent(diagram)。至此,view model 已经加入到 document 中,保存 model,再次打开 model 可见已经可以在 Zachman Diagram 中显示 model 的图形信息,并可进行相应的管理和操作。

这里需要注意的是,因为创建 notation model 是对 Resource 的修改,需要在一个事务中完成以上操作。

总结

GMF 为开发基于模型的图形化编辑器提供的便利,它为 EMF 和 GEF 间架起了一座沟通的桥梁,方便开发人员更好的集中精神在关键问题上,而不用考虑框架结构,界面一致性等情况。同时 GMF 允许开发人员直接使用 GEF 和 EMF 的接口去完成一些底层的复杂的功能。GMF 功能也在不断的完善和增强,希望我们的在本文的尝试会帮助大家解决在 GMF 开发过程可能会遇到的问题。


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Open source
ArticleID=197615
ArticleTitle=Graphical Modeling Framework 进阶
publish-date=02282007