一.
GEF
应用程序是什么样子
初步了解了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
应用程序的实现
模型是根据应用需求来设计的,所以我们的模型包括代表整个图的Diagram、代表节点的Node和代表连接的Connection这些对象。我们知道,模型是要负责把自己的改变通知给EditPart的,为了把这个功能分离出来,我们使用名为Element的抽象类专门来实现通知机制,然后让其他模型类继承它。Element类里包括一个PropertyChangeSupport类型的成员变量,并提供了addPropertyChangeListener()、removePropertyChangeListener()和fireXXX()方法分别用来注册监听器和通知监听器模型改变事件。在GEF里,模型的监听器就是EditPart,在EditPart的active()方法里我们会把它作为监听器注册到模型中。所以,总共有四个类组成了我们的模型部分。
构造一个
GEF
应用程序通常分为这么几个步骤:设计模型、设计
EditPart
和
Figure
、设计
EditPolicy
和
Command
,其中
EditPart
是最主要的一部分,因为在实现它的时候不可避免的要使用到
EditPolicy
,而后者又涉及到
Command
。
现在我们来看个例子,它的功能非常简单,用户可以在画布上增加节点(
Node
)和节点间的连接,可以直接编辑节点的名称以及改变节点的位置,用户可以撤消
/
重做任何操作,有一个树状的大纲视图和一个属性页。
点此下载
,这是一个
Eclipse
的项目打包文件,在
Eclipse
里导入后运行
Run-time Workbench
,新建一个扩展名为
"gefpractice"
的文件就会打开这个编辑器。
你可以参考着代码来看接下来的内容了,让我们从模型开始说起。模型是根据应用需求来设计的,所以我们的模型包括代表整个图的
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
,后面会看到拖动源就是调色板。
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
。
三.选择面板(调色板)的实现
这个
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
或其子类,等等。
四.
DirectEdit
、属性页和大纲视图的实现
主要讨论如何实现
DirectEdit
、属性页和大纲视图,这些都是一个完整
GEF
应用程序需要提供的基本功能。
实现
DirectEdit
所谓
DirectEdit
(也称
In-Place-Edit
),就是允许用户在原本显示内容的地方直接对内容进行修改,例如在
Windows
资源管理器里选中一个文件,然后按
F2
键就可以开始修改文件名。实现
DirectEdit
的原理很直接:当用户发出修改请求(
REQ_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;
}
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
。
实现大纲视图
在
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());
}
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);
}
if (type == IContentOutlinePage.class)
return new OutlinePage();
return super.getAdapter(type);
}
这样,树型大纲视图就完成了,见下图。很多
GEF
应用程序同时具有树型和缩略图两种大纲,实现的基本思路是一样的,但代码会稍微复杂一些,因为这两种大纲一般要通过一个
PageBook
进行切换,缩略图一般由
org.eclipse.draw2d.parts.ScrollableThumbnail
负责实现,这里暂时不讲了(也许以后会详细说),你也可以通过看
logic
例子的
LogicEditor
这个类的代码来了解。