几何图形编辑器
图形编辑框架(Graphical Editing Framework - GEF)为创建用于可视化编辑任意模型的编辑器提供了强大的基础。它的功能依赖于模块化的结构,合理选用的设计模式,和相对独立的组件,这些组件构成了一个完整的编辑器。对于一个新手来说,GEF中所涉及的大量概念和技术可能是令人难以承受的。然而,一旦这些技术被掌握并正确使用,它们就可以帮助开发出具有高扩展性和维护性的软件。本文将对GEF作相对全面的介绍。文中将描述一个几何图形编辑器作为例子-它虽然简单,但是覆盖了GEF中的核心概念。
作者 Bo Majewski, Cisco Systems, Inc.
2004年12月8日
译者 Qi Liang
2005年9月7日
绘图编辑框架
(GEF)被设计用来以图形而不是文本的方式来编辑用户数据,一般被称为
模型
(
model
)。当处理包含多对多,一对多以及其他复杂关系的实体时,GEF是一种很有价值的工具。随着Eclipse
Client Platform 的流行,使得编辑器的开发不仅仅局限于编程,GEF的重要性也与日俱增。比如说,数据库schema编辑器
[7]
,逻辑电路编辑器和任务流管理器,这些例子都很好地展示了GEF是一种可以用于各个不同领域的,具有强大功能和灵活性的框架。
然而,任何通用框架都设计复杂,难于学习,GEF也不例外。到现在为止,最小的例子也将涉及75个类。即使对于最勤勉的开发者来说,要从GEF用户定义类型和GEF提供的上百种类型之间相互作用来理解GEF的独特之处,对耐心和智力的都是一种考验。为了改变这种状况,一个全新的,规模更小的编辑器例子被添加进即将到来的Eclipse 3.1(译:翻译此文时,Eclipse 3.1已经发布)。这个几何图形编辑器(看
图1
)允许你创建,编辑简单的图。它处理两种对象,矩形和椭圆。你可以在实线和虚线这两种连接类型中选择一种来连接两个对象。每一个连接都是有方向的,也就是说从一个源对象开始,在目标对象处终止。箭头用来表示连接方向。连接可以转移,也就是通过拖动它的源点或目标点到一个新的对象上。编辑器中的对象可以点击选中,也可以通过拖拉一个区域来选择。选中的对象可以被删除。所有的模型操作,比如添加,删除对象,移动对象,改变大小等等,都可以undo或redo。最后,编辑器集成了两个Eclipse标准视图
Properties
和
Outline
。这个编辑器的价值不是在于它的可用性,而是作为例子,通过有限的两种用户定义类型来演示在一个成熟GEF编辑器中会碰到的大多数概念和技术。
图 1
. 运行在Linux下的几何图形编辑器
将最新的Eclipse 3.1 GEF例子从
GEF项目下载页面
下载下来,并解压缩至你的Eclipse目录中。按
Ctrl-N
,会弹出创建向导,将
Examples
目录展开,选择
Shapes Diagram
。下面给出几何图内部工作的详细的全面介绍。在我们接触代码前,我们先来看看GEF主要思想。
GEF核心概念
GEF帮助你为数据构造一个可视化的编辑器。数据可以是带有简单温度旋钮的温度调节器,也可以是一个包含几百个路由器,连接和服务质量策略的虚拟局域网。幸亏GEF设计者,他们设法建立一种框架,使得它能够和任何数据一起工作,用GEF的术语来说,就是任何
模型(model)
。这是通过严格遵循了模型-视图-控制器模式(MVC)来做到的。模型就是你的数据。对于GEF,模式可以是任何普通的Java对象(POJO)。模型不应该知道任何有关于控制器或视图的信息。
视图(view
)是模型或其某一部分在屏幕上的可视化表示。它可以是矩形,线或椭圆这样的简单图形,也可以是彼此嵌套的逻辑电路。同时,视图也应该对模型和控制器一无所知。虽然任何实现
IFigure
接口的类都可以作为视图,但是GEF使用Draw2D
可视图形(figure)
。
控制器
,可称为
编辑部件(edit
part)
,是模型和视图之间的桥梁。当你开始编辑你的模型时,一个顶层的控制器被创建出来。如果模型由若干个片段组成,顶层控制器就会将这个信息通知GEF。接下来,每个片段的子控制器被创建出来。如果它们又包含子片段,这个过程就会一直继续下去,直到所有组成模型的对象都有它们的控制器。控制器的另一个任务是创建可视图形来表示模型。一旦模型被设置到某个控制器,GEF就向控制器要合适的
IFigure
对象。既然模型和视图彼此都不知道对方,控制器负责监听模型的修改,并更新模型的可视化表示。结果,在许多GEF编辑器中,一个常见的模式就是模型发
PropertyChangeEvent
通知。当一个编辑部件收到事件通知时,它通过调整模型的外观或结构上的表示来作相应的改变。
可视编辑的另一个方面就是对用户动作和鼠标,键盘事件作出响应。这里的挑战在于提供一种机制,提供合理的缺省行为,并且允许重新定义行为来覆盖缺省行为,以适应所编辑模型。比如鼠标拖动事件,如果我们假设每次检测到鼠标拖动事件,所选中对象都被移动的话,我们就限制编辑器开发者的自由。很有可能有人希望在鼠标拖动的时候,提供放大,缩小的行为。GEF通过使用工具(tool),请求(request)和策略(policy)解决了这个问题。
工具
是一种有状态的对象,它将象鼠标按钮被按下,被拖动等低层事件翻译成高层的由
Request
对象表示的
请求
。发送哪个请求取决于所激活的工具。例如,连接工具在收到鼠标按钮被按下这样的事件时,会发送一个连接开始或结束的请求。如果是一个创建工具,我们就会收到一个创建请求。GEF包含了大量预定义的工具以及创建应用特定工具的方法。工具可以由程序控制激活,也可以在用户实施一个动作后激活。在大多数情况下,工具将请求发送给鼠标位置下面的图形的
EditPart
。例如,如果你点击一个代表widget的矩形,与此相关的编辑部件就会收到一个选中请求或者直接编辑的请求。有时候,请求会发送给区域中的所有可视图形的编辑部件,比如
MarqueeSelectionTool
就是这样。无论一个或多个编辑部件怎样被选择为请求目标,它们自己并不处理请求。而是将这个任务交给所注册的
编辑策略
(
edit policies
)。每个编辑策略都会为该请求提供一个命令。不希望处理请求的策略将返回一个
null
。使用策略而不是编辑部件来响应请求的机制使得策略和编辑部件都尽可能短小,功能集中。同时,也意味着调试和维护代码变得更容易。GEF的最后一个部分就是
命令
(
command
)。GEF并没有直接修改模型,它要求你使用命令来做实际的修改。每个命令应该实现执行对模型或模型一部分的修改和撤销修改。这样,GEF编辑器自动支持模型修改的undo/redo。
除了能够提升你的技能以及设计模式方面的知识外,使用GEF的一个重要的优点在于它能够和Eclipse平台完全集成在一起。在编辑器中选中的对象可以为标准
Properties
视图提供属性。Eclipse向导可以用来创建,初始化GEF编辑器编辑的模型。
Edit
菜单中的
Undo
和
Redo
可以触发GEF编辑修改的撤销和重做。简单地说,GEF编辑器实现
IEditorPart
接口,是Eclipse平台中的一员,它和文本编辑器或其他workbench编辑器处于同样的集成层次。
创建GEF编辑器的第一步是创建模型。在我们的例子里,模型由四类对象组成:几何图(包含所有的图形),两种类型的图形,和图形间的连接。在我们为这些类编写代码前,我们准备了一些基础结构。
核心模型类
当你创建模型时,你可以参考下面的内容:
模型存储了所有用户可以编辑或浏览的数据。
这同时也包括和可视化表示相关的数据,比如边界。你不能依赖编辑部件或可视图形来保存这些数据,因为这些对象可能根据需要创建或丢弃。如果你不喜欢将你的可视数据和你的业务数据绑定在一起,可以参考
[3]
中的建议。
提供持久化模型的方法。
确信当编辑器在关闭时,你的模型被持久化。当同样的编辑器被打开时,实现方法使得模型状态可以从持久存储器中恢复。
模型必须保持与视图或控制器无关。
不要存储任何对视图或控制器的引用。GEF在某种条件下会丢弃视图或控制器。如果你保持了这些引用,你可能会碰到一个失效的可视图形或编辑部件。
提供方法允许别人监听模型的变化。
这使得控制器可以及时响应修改,并对视图作适当调整。既然你不能保持对控制器的引用,唯一的方法就是为控制器提供一种途径,使得它能够作为一个事件接受者注册(和撤销注册)在模型上。一个好的办法就是使用
java.beans
包中的属性修改事件通知。
上面所列的规则对于所有模型都是相同的,为基本类建立类层次来强化这些规则是很有好处的。
ModelElement
类继承了Java的
Object
类,并提供了三个功能:持久化,属性改变和属性源支持。简单的模型持久化可以通过实现
java.io.Serializable
接口以及
readObject方法来完成。这使得你可以将编辑器的模型以二进制格式存储。当需要和某种应用一起工作时,这并不能提供的格式的可移植性。在复杂的情况下,你需要实现将模型以XML或类似的格式存储。模型的改变通过属性改变事件来通知。这个基本类允许编辑部件
注册和
撤销注册为属性改变通知的接受者。属性改变通知是通过调用
firePropertyChange方法触发的。最后,为了帮助和workbench的
Properties
视图集成,需要实现IPropertySource接口(细节在图2中忽略)。
public abstract class ModelElement implements
IPropertySource,
Serializable {
private transient PropertyChangeSupport pcsDelegate =
new PropertyChangeSupport(this);
public synchronized void addPropertyChangeListener(PropertyChangeListener l) {
if (l == null) {
throw new IllegalArgumentException();
pcsDelegate.addPropertyChangeListener(l);
protected void firePropertyChange(String property,
Object oldValue,
Object newValue) {
if (pcsDelegate.hasListeners(property)) {
pcsDelegate.firePropertyChange(property, oldValue, newValue);
private void readObject(ObjectInputStream in) throws IOException,
ClassNotFoundException {
in.defaultReadObject();
pcsDelegate = new PropertyChangeSupport(this);
public synchronized void removePropertyChangeListener(PropertyChangeListener l) {
if (l != null) {
pcsDelegate.removePropertyChangeListener(l);
图 2. 所有模型对象的基类
椭圆和矩形这两类对象,在许多方面是相同的,它们的公共功能可以被提取出来放在公共类中。尤其是两者都代表着占据某个位置,具有一定大小的对象。它们可以彼此连接。这些属性的任何修改都需要通知监听者。更进一步地说,它们的位置和大小属性都可以通过IPropertySource
接口暴露,这允许用户通过Properties视图来查看,和修改它们。
对象间连接的管理很值得仔细看一下。这里并没有一个全局的用于存储所有连接的地方。GEF要求模型部件报告它们之间的连接的情况,是源还是目标。这些信息都以List对象的形式
提供。Shape
类维护了两个数组列表,分别存储
源连接和
目标连接。源连接是指那些以当前图形作为源的连接,目标连接是指以当前图形作为目标的连接。两个包可见方法(
和
)使得图形和连接可以彼此知道相互之间的关系。此外,两个公有方法(
和
)使得model
包外面的类知道图形的连接情况。这些方法都会被相关的图形(形状)控制器所使用,具体内容将在接下来的部分中加以介绍。
public abstract class Shape extends ModelElement {
private Point location = new Point(0, 0);
private Dimension size = new Dimension(50, 50);
private List sourceConnections = new ArrayList();
private List targetConnections = new ArrayList();
public Point getLocation() {
return location.getCopy();
public void setLocation(Point newLocation) {
if (newLocation == null) {
throw new IllegalArgumentException();
location.setLocation(newLocation);
firePropertyChange(LOCATION_PROP, null, location);
void addConnection(Connection conn) {
if (conn == null || conn.getSource() == conn.getTarget()) {
throw new IllegalArgumentException();
if (conn.getSource() == this) {
sourceConnections.add(conn);
firePropertyChange(SOURCE_CONNECTIONS_PROP, null, conn);
} else if (conn.getTarget() == this) {
targetConnections.add(conn);
firePropertyChange(TARGET_CONNECTIONS_PROP, null, conn);
void removeConnection(Connection conn) {
if (conn == null) {
throw new IllegalArgumentException();
if (conn.getSource() == this) {
sourceConnections.remove(conn);
firePropertyChange(SOURCE_CONNECTIONS_PROP, null, conn);
} else if (conn.getTarget() == this) {
targetConnections.remove(conn);
firePropertyChange(TARGET_CONNECTIONS_PROP, null, conn);
public List getSourceConnections() {
return new ArrayList(sourceConnections);
public List getTargetConnections() {
return new ArrayList(targetConnections);
图 3 图形功能
顶层模型类
通过上面的准备,我们可以开始编写顶层模型类。Connection
类表示两个图形间的连接。它存储连接的源和目标。通过调用disconnect
或reconnect
方法可以修改连接。连接含有一个boolean值来表示连接是否存在。命令会使用这个值来验证某种操作的合法性。源连接和目标连接都保持一个到源图形的引用,这样使得被断开的连接可以很容易地被重新连接。连接包含一个属性,就是线的类型。EllipticalShape
和RectangularShape
类都扩展了Shape
类,添加了很少的功能。
ShapeDiagram
类是ModelElement
类的子类,它可以作为一种容器。它维护一组图形,并通知监听器这组图形的变化。命令可以调用![tag](images/tag_1.gif)
addChild
和![tag](images/tag_2.gif)
removeChild
方法,并检查返回的boolean值来验证它们的操作。这个类也提供了
公共方法给控制器类。
public class ShapesDiagram extends ModelElement {
private Collection shapes = new Vector();
public boolean addChild(Shape s) {
if (s != null && shapes.add(s)) {
firePropertyChange(CHILD_ADDED_PROP, null, s);
return true;
return false;
public List getChildren() {
return new Vector(shapes);
public boolean removeChild(Shape s) {
if (s != null && shapes.remove(s)) {
firePropertyChange(CHILD_REMOVED_PROP, null, s);
return true;
return false;
图 4.
ShapeDiagram
- 图形的容器
实现上需要注意的地方
细心的读者一定意识到这个模型创建了一个有向图的实现,图形作为顶点,连接作为边,所有图形,连接构成的图就是图。这里所形成的表示方式称为邻接点列表表示法,它很适合稀疏图。只要略作修改,这个模型的代码就可以转变为一般的图表示。这里对算法书中的图实现所需要做的就是添加代码使得图,节点,和边在发生改变的时候发送事件。不象数学上的图,节点不是零维的点,而是有矩形边框。最后,图存储了所有的边,而图形并没有存储连接,因为GEF并没有要求这么做。
值得注意的是,由上面的类所提供的解决方案并不是唯一的方法。那些开发计算机图形的人更愿意用另一种方法来存储连接,安排节点和边之间的通信。然而,这些细节并不是那么重要。设计者可以自由地选择他们认为更具普遍性,更快,或者功能更强的模型表示。关键的地方在模型改变的消息通知,模型修改的维护,包括对可视属性和模型持久化的支持。其余的都取决于你的经验和需要,你可以自由地进行选择。
由于这个图形编辑器非常的简单,我们不必创建可视图形来表示我们的模型,而是使用预定义的可视图形。Figure
类加上FreeformLayout
布局管理器用来表示图。这允许我们将对象拖放到任何位置。RectangleFigure
和Ellipse
都可以表示对象。使用预定义的可视图形来表示部分模型并不是通常的做法。即使你的视图没有引用模型或控制器,它都必须为每个用户可能需要查看或修改的模型重要方面都定义可视化属性。因此常常会定义拥有大量可视化属性,比如颜色,文本,嵌套可视图形等,的复杂可视图形,每个属性都对应于它们所表示的模型属性。有关创建复杂可视图形的详细处理,请参考 [4]。
部件(part)
对于模型的每个独立部分,我们都必须定义控制器。所谓“独立”,指的是这个实体都可以作为用户操作的对象。一个比较好的原则就是任何可以被选择,或删除的对象都应该有它自己的编辑部件。
编辑部件知道模型,监听模型改变所产生的事件,然后更新视图。由于在模型层所做的设计选择,所有的编辑部件都必需遵循图5所示的模式。每个部件
都实现PropertyChangeListener
接口。当它被激活时
,它将自己注册为模型的属性修改事件的接收者。当失活时
,它将自己从监听器的列表中移除。最后,当它收到属性修改事件时
,它会根据属性名和新旧值来刷新表示模型的可视图形。事实上,这个模式使用非常普遍,在大的应用中,它会建立一个基类来提供这样的行为。
public abstract class SpecificPart extends AbstractGraphicalEditPart
implements PropertyChangeListener {
public void activate() {
if (!isActive()) {
super.activate();
((PropertyAwareModel) this.getModel()).addPropertyChangeListener(this);
public void deactivate() {
if (isActive()) {
((PropertyAwareModel) this.getModel()).removePropertyChangeListener(this);
super.deactivate();
public void propertyChage(PropertyChangeEvent evt) {
String prop = evt.getPropertyName();
图 5. 知道属性变化的编辑部件
DiagramEditPart 类
当编辑器成功载入一个几何图,并将它设置在一个图形viewer上,就要求ShapesEditPartFactory创建一个编辑部件来控制图。它创建一个新的DiagramEditPart实例,并将图设置为它的模型。当新创建的编辑部件被激活时,它将自己注册为模型的监听器,并创建一个使用free form布局管理器的可视图形,这种布局管理器允许通过它们的边界来定位图的可视图形。DiagramEditPart通过getModelChildren方法来获取图中包含的所有图形。就象前面提到的,GEF为返回的所有子模型对象都会创建编辑部件和可视图形。
DiagramEditPart
类安装了三个策略。所有的策略都在AbstractEditPart
类的createEditPolicies
方法中定义,同时所有继承自AbstractGraphicalEditPart的实类都必需实现这个方法。编辑部件使用这些策略来处理工具发出的请求。在最简单的情况下,策略负责生成许多命令。策略使用String类型的索引字注册在编辑部件上,这个索引字被称为策略角色。这些索引字对编辑部件本身来说没有什么意义。然而,对软件开放人员,就有意义了,它使得其他人,尤其是扩展你的控制器的人,可以通过这些索引字来关闭或移除策略。就GEF而言,你的索引字可以是“foobar”。然而,你最好告诉你程序员同伴,当布局管理器改变的时候,为了设置新的布局策略,需要安装新的“foobar”策略。由于这可能很有趣,且不是那么显而易见,所以推荐你使用EditPolicy接口定义索引字,这些名字需要很好的表达该策略在编辑部件中的角色。
安装的第一个策略
的索引字是EditPolicy.COMPONENT_ROLE
,它负责阻止模型的根被删除。它重写了createDeleteCommand
方法,并返回一个不能被执行的命令。第二个策略
的索引字是LAYOUT_ROLE
,它处理创建请求和边界修改请求。当新的图形被放置到图中,第一个请求被发送出来。布局策略返回一个命令,这个命令添加新的图形到图编辑器中,并把它放置在适当的位置。用户修改图中已存在的图形大小或移动它时,都会发出边界修改请求。第三个installEditPolicy
调用
删除一个策略。它在用户点击模型根所在区域时,阻止根部件提供选择反馈。这里也可以看出一个有意义的策略索引字的重要性。
protected void createEditPolicies() {
installEditPolicy(EditPolicy.COMPONENT_ROLE, new RootComponentEditPolicy());
XYLayout layout = (XYLayout) getContentPane().getLayoutManager();
installEditPolicy(EditPolicy.LAYOUT_ROLE, new ShapesXYLayoutEditPolicy(layout));
installEditPolicy(EditPolicy.SELECTION_FEEDBACK_ROLE, null);
图 6. 图编辑部件中安装的策略 (译:这里和Eclipse 3.1 GEF提供的例子代码略有出入)
图编辑部件监视子编辑部件的添加,移除事件。当任何新的图形添加或移除时,ShapesDiagam
类将发送这些事件。当图编辑部件检测到这两种属性修改事件时,图编辑部件都会调用AbstractEditPart
类中定义的refreshChildren
方法。这个方法会遍历所有子模型对象,并相应地添加,移除,或重新排序子编辑部件。
ShapeEditPart 类
ShapeEditPart
类管理所有的图形。当DiagramEditPart
会返回子模型列表时,ShapeEditPart
由ShapesEditPartFactory
类根据每个模型对象的类型创建。工厂类创建的每个部件都拥有一个它们所控制的子模型。一旦模型对象被设置,编辑部件被要求创建可视图形来表示模型对象。根据模型对象的类型,返回椭圆或矩形的编辑部件。
这个编辑部件关注四类属性修改事件:大小,位置,源连接,和目标连接。如果图形改变了大小或位置,![tag](images/tag_6.gif)
refreshVisual
方法会被调用。这个方法在可视图形被创建的时候就会由GEF自动调用。在这个方法中,可视图形的可视属性应该根据模型的状态做相应调整。重用模型更新方法是GEF编辑器中经常碰到的又一种模式。在我们这个编辑部件类中,新的位置和大小被获取并储存在表示图形的可视图形中。此外,新的边界会传给父控制器的布局管理器。当源连接或目标连接改变时,源连接或目标连接改编辑部件会调用AbstractGraphicalEditPart
类中的方法刷新。和refreshChildren
方法相似,这些方法会遍历所有的连接,并相应添加,删除,或重新定位它们的编辑部件。