GEF应用程序快速入门

 
一. 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), nullnull);
在新的 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;
}
也就是根据要处理的属性名做不同操作。要注意的是,下拉框类型的编辑器是以 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());
}
由于我们在构造方法里指定了使用树结构显示大纲,所以 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);
}
这样,树型大纲视图就完成了,见下图。很多 GEF 应用程序同时具有树型和缩略图两种大纲,实现的基本思路是一样的,但代码会稍微复杂一些,因为这两种大纲一般要通过一个 PageBook 进行切换,缩略图一般由 org.eclipse.draw2d.parts.ScrollableThumbnail 负责实现,这里暂时不讲了(也许以后会详细说),你也可以通过看 logic 例子的 LogicEditor 这个类的代码来了解。

 
 
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值