构造一个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 Node", 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、属性表和大纲视图等一些功能没有讲解,下面的帖子里将介绍这些常用功能的实现。