本文在 GMF2.0 的基础上,用一个自上而下的流程分析建模工具为例,完整的描述了从如何建模,如何修改模型,以及如何客户化生成的代码框架的整个过程,主要涉及布局,UI 外观,模型操作以及对多个 Editor 的支持等等。
GMF(Graphical Modeling Framework)是Eclipse的一个开源项目,它在结合了EMF和GEF的基础上,为基于模型的图形化编辑器的开发提供了一个功能强大的框架,开发人员可以采用建模的方式很容易的生成高质量的代码框架。
GMF主要由开发工具和运行时两部分组成。开发工具负责基于GMF核心模型的设计和建模工作,包括:描述具体领域模型的 Graph 模型的表示,Tool 模型的表示,以及如何用 Mapping 模型对前面三个模型进行映射组合,从而根据定义好的 Mapping 模型生成 Gen 模型,并最终完成代码框架的生成。三个独立的模型设计大大提高了模型的可重用性,开发人员可以在各模型所关注的领域对其进行进一步的定制,这个将在深入了解GMF建模工具中有详细介绍。GMF的运行时环境提供了丰富的组件(如Command框架,Services, Global Action, Layout等等)和强大的扩展机制,如 org.eclipse.gmf.runtime.emf.ui.modelingAssistantProviders, org.eclipse.gmf.runtime.common.ui.services.parserProviders,org.eclipse.gmf.runtime.diagram.core.viewProviders 等等,这些将在深入了解GMF运行时框架中详细地介绍。
刚发布的GMF2.0 在 GMF1.0 的基础上进一步提高了其易用性,同时也加入了相当的新功能。比如:Diagram 内容导航的支持,对 RCP 应用的支持,对 Preference Pages 的支持,支持模型的合并等等。
本文主要基于 GMF2.0 的运行环境,介绍如何实现一个自上而下的 Process 分析工具。其主要功能是:自上而下的分解一个大的 Process 为一批子 Process,每个 Process 可以由一个具体的 Flow 来描述,对于同一个模型,可以由多个Editor去编辑,如Process Editor, Flow Editor。期望的结果如下图。
图 1.Process Editor
图 2.Flow Editor
本文将从定义领域模型入手,一步一步地介绍实现的细节。
![]() ![]() |
![]()
|
GMF框架提供了对一个具体的Ecore模型进行图形化建模的支持,以模型驱动的方式,一步一步完成功能描述,直至代码的生成。参见(Graphical Model Framework进阶)。本文的环境如下:
- Eclipse3.3.0
- EMF2.3.0
- GEF3.3.0
- GMF2.0
ECore模型是GMF建模的起点,通常ECore模型是一个领域模型,描述了要建模的领域特征。其后的GMF借鉴了MVC的思想,Ecore模型就是MVC中的Model,详情可参见(深入了解GMF建模工具)。
用EMF向导新建一个空的process.ecore文件,右键单击该Ecore文件,选择Initiallize ecore_diagram diagram file,将会打开一个Ecore Diagram编辑器,创建Process的ECore模型如下:
当然,开发人员也可以直接用传统的树形EMF编辑器构上面的Ecore模型,或用RSA(Rational Software Architect)对模型进行UML图的描述,再转换成Ecore模型。
Graph模型是用来描述领域模型的显示信息,在GMF的定义中,Graph模型原则上是一个独立的模型,它描述了一个用于信息显示的模型。从它的定义里面就可以看出,它描述了Diagram, Node, Connection和Figure等图形化信息。将其关联到具体的领域模型上,就表示了该领域模型前端的显示图元。
选择已经定义的领域模型,打开向导对话框,
在图4中,GMF提供了一系列向导来辅助创建不同的GMF模型。值得一提的是,GMF2.0支持对已有的Graph模型和Tool模型的提供更新操作。对于一个已存在的模型,新的更新会被自动添加到模型中,而原有的部分不会影响,参见(GMF2.0新特性介绍)。在具体的实现上,GMF2.0添加了Reconcile Graphical Definition Model和Reconcile Tool Definition Model两个向导来完成此功能,和GMF1.0相比,这个设计更人性化。
在具体的操作中,以上面定义的 ECore 模型为基础,利用 GMF 向导一步一步的生成对应的Graph模型如下图5,操作细节不再赘述。当然,也可以不用ECore模型,直接编辑一个空的Graph模型来进行定制,因为原则上Graph模型是独立的,可以被多个 Ecore 模型和Mapping 模型公用的。GMF向导以 ECore 模型为基础,方便了开发人员进行更有效的开发。
在Graph模型中,选择Map元素为根元素,这里只是简单的为每个元素定义了图元信息。关于图元外观的定制会在下面介绍。
图形化建模工具里有一个很重要的部分,就是Palette部分,它定义一系列的Tool Entry项,每个Tool Entry项对应工具所支持的某一类型对象的创建,如RSA Class Diagram的Palette里的Package, Class, Interface 等Tool Entry项。
GMF提供了对Palette部分的功能建模。Tool模型提供了对将要在Diagram里面出现的Tool Entry项的描述,包括名称,描述等。同时,GMF Tool模型提供了对常用Tool Entry的支持,此外,还有一些标准的Tool Entry项,如放大/缩小,选择,注释等等。Tool模型也是一个独立的模型,可以被多个ECore模型和Mapping模型共享。
这里使用GMF Tool模型创建向导为ECore模型生成了一个Tool模型。可以在下面的属性也里面对Tool Entry做一些基本的配置,不过,上面的Default Image是不允许配置的,GMF自动为每个Tool Entry生成了默认的图标,如果要自己定义图标,可以使用Bundle Image来指定特定的图标。
Mapping模型是一个组织者,针对前面提到的三个独立的模型,由Mapping模型把这三个模型合理的组织起来。在Mapping模型里,孤立的模型间有了相互的联系,领域模型的对象将会由特定的Graph模型里的图元来进行展示,同时,还在Tool模型里为其设定一个Tool Entry作为Palette上的元素创建选项。
此外,GMF Mapping模型还提供了丰富灵活的配置选项,详情参见(深入了解GMF建模工具)。
上图中,只提供了在一个Diagram Editor里面创建一个Process节点的功能,实际需要每个Process节点下可以创建多个子节点。通常这种情况下,需要在Node Mapping下创建一个Child Reference和Compartment Mapping来定义节点所包含的子节点的信息,但是这样一来,在Diagram的显示上将会是一个Process节点图形内部包含了其所有的子 Process节点,它们在前端图形显示上是一个包含关系,不能满足我们所期望的层次关系的表示。所以这里只是在Diagram定义了Process的节点的信息,其父子节点的层次关系将在代码部分解决。
在这个例子中,我们需要在双击一个Process节点时,打开一个Flow Editor来对应Process的内部流程的编辑,因此,需要再定义一个Flow Editor,对应的Mapping模型如下图8
在这个Flow里面关注3个元素,Task、Decision和表示它们间流程关系的Relationship。Flow Mapping模型共享了Process的Ecore模型和Graph模型。
Gen模型是用来生成Diagram框架的模型,Gen 原模型描述了生成Diagram代码框架所需要的一些元素。在GMF中,Gen模型由Mapping模型生成,一个Mapping模型对应一个Gen模型,当然也可以由一个Mapping模型生成多个自定义的Gen模型,这样就可以生成多个Diagram Editor了。
图 9.Process Gen模型
图 10.Flow Gen模型
Gen模型和Mapping模型相比,有更多关于代码生成的属性信息,开发人员可以自己定义生成代码的一些细节信息。在GMF2.0里,加入了内容导航元模型,在Gen模型里会生成一个Gen Navigator项,其下的一系列子项详细定义了导航栏和编辑器间作内容同步时的配置信息。其次,在Gen Diagram下面,还增加了对Preference Page的支持,开发人员可以自己配制和定制Diagram的Preference page页面,GMF提供了预定义的一组标准的Preference page,如Appearance, Printing, Connections等,当然,开发人员也可以预定义自己的Preference Page的配置情况。GMF将会生成相应的代码框架,开发人员只需把注意力集中在具体的应用逻辑上。值得一提的是,GMF Gen模型提供了对RCP应用在Gen模型的支持,通过对Gen Application项的定义和配置,可以很容易实现对RCP应用的设置,比如菜单项,工具条等等。
在一个GMF工程下面可以有多个Tool/Graph模型,同时也可以有多个Mapping模型和对应的Gen模型。事实上,本实例中就是创建了两个Mapping模型和Gen模型,分别用于Process Diagram和Flow Diagram的建模。
![]() ![]() |
![]()
|
在生成的代码框架中,GMF Diagram的默认布局管理器是FreeFormLayoutEx,在此布局管理器中,图元采用XY布局管理。这个和Process预期的自上而下的树形布局不相符,因此需要对Process Diagram的图元布局管理器进行一定的修改。
Node的布局是由它的父元素对应的图元的布局管理器来控制的,在Process Diagram里Node就是Process图元,它的父容器就是Diagram,这样,通过修改Diagram的布局管理器就规范了其子节点(Process节点)的布局了。可参见(GMF进阶-自定义布局管理器和背景色)
所不同的只是Layout()方法的实现,本示例中用树形的布局来组织Process节点的显示。
Connection图元是一个由至少两个点组成的线/折线,并且有可能带有一个箭头指向目标节点。折线上点的位置和数量决定了折线的形状和布局。默认的两个图元之间的一个连接是一条带箭头的直线,要实现图所示的Connection的折线型布局,这里需要修改Connection的布局管理器。
首先自定一个布局管理器,在这个布局管理器里会自动计算出折线的形状。
public class ConnectionLayout extends DelegatingLayout {
public void layout(IFigure parent) {
...
Connection conn = (Connection)parent;
points = new PointList();
while(条件成立){
Point p[i] = //计算第i个节点坐标
Points.add(p[i]);
}
...
conn.setPoints(points);
}
}
|
其次,和Node的布局类似,把此布局管理器安装到Connection的图元上,如下所示。
public class RelationshipConnection extends PolylineConnectionEx {
public RelationshipConnection() {
setLayoutManager(new ConnectionLayout());
}
}
|
这样,在Connection进行布局的时候就会把原来的直线通过计算变成折线形状,照此方法,可以很容易的构造出水平或垂直方向的树形布局。
在GMF生成的Diagram Editor里,在编辑模型的时候,可以选中一些或全部模型,在选择工具条上的按钮或右键菜单里的Format->Arrange->All,就可以对选中的模型元素进行布局。
事实上,GMF对此功能提供了一个扩展点 org.eclipse.gmf.runtime.diagram.ui.layoutProviders进行支持,由这个扩展点可以轻松实现上述功能。下面实现一个CustomerLayoutProvider类,
这里需要注意两个方法
provides()
方法,该方法检查当前的Provider是否提供具体的方法,检查选中的节点是否可以被布局,并且验证被传递的布局类型是否是有效的。
layoutLayoutNodes()
方法,该方法是真正计算所要布局的节点的坐标从而进行布局的更新,和上面提到的做Node的布局是类似的。所不同的是此方法返回一个Runnable对象,由这个Runnable对象的执行,完成真正的布局功能的执行。具体的功能实现可参见GMF默认的LayoutProvider: org.eclipse.gmf.runtime.diagram.ui.providers.internal.DefaultProvider。
![]() ![]() |
![]()
|
GMF和GEF一样,都是用Draw2D来完成模型前端的显示工作,这里提到的对UI外观的修改主要是对Draw2D功能的一些应用。
在GMF中,每一个EditPart都会有一个Figure或PolylineConnection对象来做其前端的显示,因此对UI的外观的修改也就是对这些图形属性的修改。
在图2中,要实现图形背景的渐变色,需要对TaskEditPart
对应的TaskFigure
进行修改,在图形进行重新绘制时设置其背景色。这里Draw2D提供了fillGradient()
方法来绘制渐变色。如下:
public void paintFigure(Graphics g) {
super.paintFigure(g);
Color oldForeground = g.getForegroundColor();
Color oldBackground = g.getBackgroundColor();
g.setForegroundColor(FlowUtil.FORE_COLOR);
g.setBackgroundColor(FlowUtil.TASK_BG_COLOR);
g.fillGradient(bounds, true);
g.setForegroundColor(oldForeground);
g.setBackgroundColor(oldBackground);
}
|
默认情况下,Process和Task节点的图标和文本是放在同一行的,如何设置其为上下布局呢?修改它们对应的XXXNameEditPart的两个方法如下,设置文本的在下,图标在上:
protected void setLabelTextHelper(IFigure figure, String text) {
if (figure instanceof WrapLabel) {
WrapLabel l = (WrapLabel)figure;
l.setText(text);
l.setTextPlacement(PositionConstants.SOUTH);
l.setTextWrap(true);
} else {
((Label) figure).setText(text);
}
}
protected void setLabelIconHelper(IFigure figure, Image icon) {
if (figure instanceof WrapLabel) {
WrapLabel l = (WrapLabel)figure;
l.setIcon(icon);
l.setIconAlignment(PositionConstants.TOP);
} else {
((Label) figure).setIcon(icon);
}
}
|
![]() ![]() |
![]()
|
GMF工具生成了一个很好的代码框架,但是为了使其更加符合具体的应用,需要对生成的代码进行一定的修改,尤其是对UI上的一些操作常常伴随着模型的修改。由于GMF是基于事务的机制对模型进行管理,所以,对模型的修改或更新操作需要放在一个具体的事务中进行。GMF丰富的EditPolicy和 Command机制提供了对上述的支持。
模型的创建是由CreationEditPolicy
来处理的,如果一个EditPart需要创建其子元素,则需为其安装一个CreationEditPolicy
来控制子元素的生成。通常为
清单 5. 安装自定义的CreationEditPolicy
installEditPolicy(EditPolicyRoles.CREATION_ROLE, new XXXCreationEditPolicy());
|
其中,XXXCreationEditPolicy
是对CreationEditPolicy
的一个扩展,本实例中有个限制,需要在创建一个Process节点的同时,创建一条和它的父节点的连线,也就是一个带箭头Connection连接,实现getCreateCommand()
如下。
protected Command getCreateCommand(CreateViewRequest request) {
TransactionalEditingDomain editingDomain = ((IGraphicalEditPart) getHost())
.getEditingDomain();
CompositeTransactionalCommand cc = //Create Process command
...
Command cmd = new ICommandProxy(cc.reduce());
CompoundCommand cm = new CompoundCommand();
cm.add(cmd);
// add Create Connection command
if(request instanceof CreateViewAndElementRequest){
CreateElementRequestAdapter requestAdapter =
((CreateViewAndElementRequest)request)
.getViewAndElementDescriptor().getCreateElementRequestAdapter();
if(CBSDiagramUtil.getInstance().getSourceEP() instanceof DANode2EditPart){
CreateChildrenRelation4ProcessCommand cmd1 = createRalationCommand(
editingDomain,
(CreateViewAndElementRequest)request,
requestAdapter);
Command cmd2 = new ICommandProxy(cmd1);
cm.add(cmd2);
}
}
return new ICommandProxy(new CommandProxy(cm.unwrap()));
}
private CreateChildrenRelation4ProcessCommand createRelationCommand(
TransactionalEditingDomain editingDomain, CreateViewAndElementRequest request,
CreateElementRequestAdapter requestAdapter) {
Diagram diagramView = (View)getHost().getModel()).getDiagram();
IElementType type = ProcessElementTypes.Relation_3001;
CreateConnectionViewAndElementRequest req =
New CreateConnectionViewAndElementRequest(type,
((IHintedType) type).getSemanticHint(),
request.getViewAndElementDescriptor().getPreferencesHint());
CreateChildrenRelation4ProcessCommand cmd =
new CreateChildrenRelation4ProcessCommand(editingDomain,requestAdapter,req,
diagramView.getDiagram());
return cmd ;
}
|
在创建一个Create Process命令后,紧接着在生成一个创建Connection的命令,这样就会在创建子Process后自动创建一条与其父节点的连接。
这里需要说明的一点是,在创建一个Connection对应的模型信息是在对应EditPart的XXXItemSemanticEditPolicy
生成的Command里完成的,GMF2.0里面这个Command是作为一个独立的Command放在command package下面的,和其他的CreateElementCommand
类似。如果使用之前的GMF版本的话,这个Command是作为XXXItemSemanticEditPolicy
的一个内部类存在的。
生成一个Connection对应的模型后,需要在Diagram里面创建一个其对应的Notation View的信息,也就是一个Edge对象,由这个Edge对象维护Connection的显示信息,包括连线形状,起始点和目标节点等。这和创建一个 Node的Notation View类似,需要指定CreateElementRequestAdapter, SemanticHint
,然后由RelationshipViewFactory
的CreateView()
方法来生成一个Edge对象。所有的这些操作都是在CreateChildrenRelation4PorcessCommand
类的doExecuteWithResult()
方法里面进行,具体操作由CreateRelationView()
方法完成,其中containerView为Diagram View,因为所有的Process View都是Diagram View的子节点。
private void createRelationView() {
DARelationViewFactory drvf = new DARelationViewFactory();
final int index = -1 ;
boolean persisted = true ;
final String semanticHint = connReq.getConnectionViewAndElementDescriptor()
.getSemanticHint();
ConnectionViewAndElementDescriptor ccd =
connReq.getConnectionViewAndElementDescriptor();
final CreateElementRequestAdapter rAdapter = ccd.getCreateElementRequestAdapter();
PreferencesHint preferencesHint = ccd.getPreferencesHint();
Node srcNode = getSrcView();
Node tgtNode = getTgtView();
if(srcNode == null || tgtNode == null){
return ;
}
View view = drvf.createView(rAdapter, containerView, semanticHint,
index, persisted, preferencesHint);
Edge edge = (Edge)view;
edge.setSource(srcNode);
edge.setTarget(tgtNode);
}
|
在EditPolicy中,有一个SemanticEditPolicy,GMF生成的代码框架中常会以此EditPolicy为基础,派生出XXXBaseItemSemanticEditPolicy
,进而派生出其他EditPart所需的具体的XXXItemSemanticEditPolicy
。正是这个EditPolicy控制着元素的删除操作。安装在EditPart上的SemanticEditPolicy控制此EditPart被删除时所进行的操作。
在本实例中,要求在删除一个Process节点时删除它所关联的子节点,如果此Process节点有Flow的信息,删除其对应的Flow的信息,并关闭对应的Flow Editor(如果打开的话)。基于上述要求,首先需要自定义一个CustomizeDestroyElementCommand
,由它来完成具体的删除操作。
清单 8. 自定义DestroyElementCommand
public class CustomizeDestroyElementCommand extends DestroyElementCommand {
…
@Override
protected CommandResult doExecuteWithResult(IProgressMonitor monitor, IAdaptable info)
throws ExecutionException {
ProcessEditPart editpart = (ProcessEditPart)getHost();
if(root == null) {
root = (ProcessEditPart)editpart.getParent();
}
// delete all descents and their relative process flow
List children = editpart.getSourceConnections();
int size = children.size();
for(int i=0; i<size; i++) {
delete((ProcessEditPart)((RelationshipsEditPart)children.get(i)).getTarget());
}
// update parent's process flow
List targetConns = editpart.getTargetConnections();
if(targetConns.size() != 0) {
ProcessEditPart parent =
(ProcessEditPart)((RelationshipsEditPart)targetConns.get(0)).getSource();
Diagram flowDiagram = getOrDeleteProcessFlowDiagram(parent, false);
if(flowDiagram != null) {
deleteFlow(flowDiagram, ((Node)editpart.getModel()).getElement());
}
}
// delete self process flow and close flow editor if acitved
Diagram flowDiagram = getOrDeleteProcessFlowDiagram(editpart, true);
if(flowDiagram != null) {
closeEditor(flowDiagram);
}
return super.doExecuteWithResult(monitor, info);
}
}
|
其次需要把上面的Command安装在正确的地方,如下,修改ProcessEditPart的XXXItemSemanticEditPolicy
的方法如下,用CustomizeDestroryElementCommand
替换掉原有的Command。
清单 9. 应用自定义的DestroyElementCommand
protected Command getDestroyElementCommand(DestroyElementRequest req) {
CompoundCommand cc = getDestroyEdgesCommand();
addDestroyShortcutsCommand(cc);
View view = (View) getHost().getModel();
if (view.getEAnnotation("Shortcut") != null) { //$NON-NLS-1$
req.setElementToDestroy(view);
}
cc.add(getGEFWrapper(new CustomizeDestroyElementCommand(req)));
return cc.unwrap();
}
|
![]() ![]() |
![]()
|
很明显,这里需要处理多个编辑器同时打开一个模型的情况,我们希望可以在编辑Process树的时候,同时可以打开它的Flow Editor来编辑其具体的流程细节。但是如果用默认的方法给一个编辑器设置一个文件作为输入的话,这一点是绝对做不到的。因为Eclipse本身在第二次打开同样的文件时,如果发现它已经被一个Editor打开就不会再次打开它。
针对这种情况,一种比较直接的办法就是把一个模型拆分成一些小的部分,分别用文件保存,每个Editor之打开其中的一个文件,像WBM就采用了类似的做法。
还有一种就是整个模型就是一个文件,而用非文件的方式去打开和编辑模型,像RSA就是这种作法,整个模型就是一个大的.emx文件。本文计划采用一个模型文件的做法。
首先,需要一个EditingDomainManager
来管理EditingDomain和文件所对应Resource。由这个Manager维护当前处于活跃状态的EditingDomain和Resource,Flow Editor共享其对应的Process Editor的EditingDomain。
public class EditingDomainManager {
public TransactionalEditingDomain getCurrentEditingDomain(final String id){
...
return editingDomain ;
}
public Diagram getDiagramOfElement(Resource gmfRes, String editorID, Process element) {
...
return getDiagram(gmfRes, kind,element);
}
}
|
其次,需要用DiagramEditorInput
来替换默认的以文件为输入的FileEditorInput
。双击一个Process图标的时,如果当前节点没有对应的Diagram,则由initialFlowDiagram()
方法生成一个空的Diagram对象,然后构造DiagramEditorInput来打开Flow Editor。
private void openFlowEditor() {
...
IWorkbenchPage page = PlatformUI.getWorkbench()
.getActiveWorkbenchWindow().getActivePage();
if(!isFlowDiagramExisted(node)){
initialFlowDiagram(node);
}
Diagram diagram = getProcessNodeDiagram(node);
DiagramEditorInput input = new DiagramEditorInput(diagram);
try {
IEditorPart ep = page.openEditor(input, editorId, true);
} catch (PartInitException e) {
e.printStackTrace();
}
}
|
为了从DiagramEditorInput打开一个Editor,Flow Diagram Editor要修改它的getDiagram()
方法,默认的getDiagram()
方法没有提供对DiagramEditorInput的处理。
@Override
public Diagram getDiagram() {
IEditorInput input = getEditorInput();
if(input instanceof DiagramEditorInput){
DiagramEditorInput dinput = (DiagramEditorInput)input;
return dinput.getDiagram();
}
return super.getDiagram();
}
|
此外还需要对FlowDocumentProvider
进行相应的修改,重载三个函数如下
@Override
public IEditorInput createInputWithEditingDomain(IEditorInput editorInput,
TransactionalEditingDomain domain) {
return editorInput;
}
@Override
public IStatus getStatus(Object element) {
return STATUS_OK;
}
@Override
public boolean isModifiable(Object element) {
if(element instanceof DiagramEditorInput){
return true;
}
return super.isModifiable(element);
}
|
这样,就可以同时打开多个Process文件,并且编辑多个Flow Editor了,具体如图1和图2所示。
![]() ![]() |
![]()
|
GMF2.0在前一个版本的基础上,在可用性和功能上都有了很大的改进,本文用一个完整的例子介绍了GMF在从建模、模型修改到生成代码框架以及对代码定制的过程,对GMF的应用和开发做了一个系统的介绍。当然,GMF本身还在不断的完善中,新的功能也会不断的包含近来,希望能有越来越多的人学习和使用 GMF。