几何图形编辑器
概要图形编辑框架(Graphical Editing Framework - GEF)为创建用于可视化编辑任意模型的编辑器提供了强大的基础。它的功能依赖于模块化的结构,合理选用的设计模式,和相对独立的组件,这些组件构成了一个完整的编辑器。对于一个新手来说,GEF中所涉及的大量概念和技术可能是令人难以承受的。然而,一旦这些技术被掌握并正确使用,它们就可以帮助开发出具有高扩展性和维护性的软件。本文将对GEF作相对全面的介绍。文中将描述一个几何图形编辑器作为例子-它虽然简单,但是覆盖了GEF中的核心概念。
作者 Bo Majewski, Cisco Systems, Inc.
2004年12月8日译者 Qi Liang
2005年9月7日
简介
绘图编辑框架(GEF)被设计用来以图形而不是文本的方式来编辑用户数据,一般被称为模型(model)。当处理包含多对多,一对多以及其他复杂关系的实体时,GEF是一种很有价值的工具。随着Eclipse Rich Client Platform 的流行,使得编辑器的开发不仅仅局限于编程,GEF的重要性也与日俱增。比如说,数据库schema编辑器 [7],逻辑电路编辑器和任务流管理器,这些例子都很好地展示了GEF是一种可以用于各个不同领域的,具有强大功能和灵活性的框架。
然而,任何通用框架都设计复杂,难于学习,GEF也不例外。到现在为止,最小的例子也将涉及75个类。即使对于最勤勉的开发者来说,要从GEF用户定义类型和GEF提供的上百种类型之间相互作用来理解GEF的独特之处,对耐心和智力的都是一种考验。为了改变这种状况,一个全新的,规模更小的编辑器例子被添加进即将到来的Eclipse 3.1(译:翻译此文时,Eclipse 3.1已经发布)。这个几何图形编辑器(看图1)允许你创建,编辑简单的图。它处理两种对象,矩形和椭圆。你可以在实线和虚线这两种连接类型中选择一种来连接两个对象。每一个连接都是有方向的,也就是说从一个源对象开始,在目标对象处终止。箭头用来表示连接方向。连接可以转移,也就是通过拖动它的源点或目标点到一个新的对象上。编辑器中的对象可以点击选中,也可以通过拖拉一个区域来选择。选中的对象可以被删除。所有的模型操作,比如添加,删除对象,移动对象,改变大小等等,都可以undo或redo。最后,编辑器集成了两个Eclipse标准视图Properties和Outline。这个编辑器的价值不是在于它的可用性,而是作为例子,通过有限的两种用户定义类型来演示在一个成熟GEF编辑器中会碰到的大多数概念和技术。
将最新的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); } } ... }
椭圆和矩形这两类对象,在许多方面是相同的,它们的公共功能可以被提取出来放在公共类中。尤其是两者都代表着占据某个位置,具有一定大小的对象。它们可以彼此连接。这些属性的任何修改都需要通知监听者。更进一步地说,它们的位置和大小属性都可以通过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); } ... }
顶层模型类
通过上面的准备,我们可以开始编写顶层模型类。Connection
类表示两个图形间的连接。它存储连接的源和目标。通过调用disconnect
或reconnect
方法可以修改连接。连接含有一个boolean值来表示连接是否存在。命令会使用这个值来验证某种操作的合法性。源连接和目标连接都保持一个到源图形的引用,这样使得被断开的连接可以很容易地被重新连接。连接包含一个属性,就是线的类型。EllipticalShape
和RectangularShape
类都扩展了Shape
类,添加了很少的功能。
ShapeDiagram
类是ModelElement
类的子类,它可以作为一种容器。它维护一组图形,并通知监听器这组图形的变化。命令可以调用addChild
和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; } }
实现上需要注意的地方
细心的读者一定意识到这个模型创建了一个有向图的实现,图形作为顶点,连接作为边,所有图形,连接构成的图就是图。这里所形成的表示方式称为邻接点列表表示法,它很适合稀疏图。只要略作修改,这个模型的代码就可以转变为一般的图表示。这里对算法书中的图实现所需要做的就是添加代码使得图,节点,和边在发生改变的时候发送事件。不象数学上的图,节点不是零维的点,而是有矩形边框。最后,图存储了所有的边,而图形并没有存储连接,因为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(); ... } }
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); }
图编辑部件监视子编辑部件的添加,移除事件。当任何新的图形添加或移除时,ShapesDiagam
类将发送这些事件。当图编辑部件检测到这两种属性修改事件时,图编辑部件都会调用AbstractEditPart
类中定义的refreshChildren
方法。这个方法会遍历所有子模型对象,并相应地添加,移除,或重新排序子编辑部件。
ShapeEditPart 类
ShapeEditPart
类管理所有的图形。当DiagramEditPart
会返回子模型列表时,ShapeEditPart
由ShapesEditPartFactory
类根据每个模型对象的类型创建。工厂类创建的每个部件都拥有一个它们所控制的子模型。一旦模型对象被设置,编辑部件被要求创建可视图形来表示模型对象。根据模型对象的类型,返回椭圆或矩形的编辑部件。
这个编辑部件关注四类属性修改事件:大小,位置,源连接,和目标连接。如果图形改变了大小或位置,refreshVisual
方法会被调用。这个方法在可视图形被创建的时候就会由GEF自动调用。在这个方法中,可视图形的可视属性应该根据模型的状态做相应调整。重用模型更新方法是GEF编辑器中经常碰到的又一种模式。在我们这个编辑部件类中,新的位置和大小被获取并储存在表示图形的可视图形中。此外,新的边界会传给父控制器的布局管理器。当源连接或目标连接改变时,源连接或目标连接改编辑部件会调用AbstractGraphicalEditPart
类中的方法刷新。和refreshChildren
方法相似,这些方法会遍历所有的连接,并相应添加,删除,或重新定位它们的编辑部件。
class ShapeEditPart extends AbstractGraphicalEditPart
implements PropertyChangeListener, NodeEditPart {
protected List getModelSourceConnections() { return getCastedModel().getSourceConnections(); }
protected List getModelTargetConnections() { return getCastedModel().getTargetConnections(); }
public ConnectionAnchor getSourceConnectionAnchor(ConnectionEditPart connection) { return new ChopboxAnchor(getFigure()); }
public ConnectionAnchor getSourceConnectionAnchor(Request request) { return new ChopboxAnchor(getFigure()); } public void propertyChange(PropertyChangeEvent evt) { String prop = evt.getPropertyName(); if (Shape.SIZE_PROP.equals(prop) || Shape.LOCATION_PROP.equals(prop)) { refreshVisuals(); } if (Shape.SOURCE_CONNECTIONS_PROP.equals(prop)) { refreshSourceConnections(); } if (Shape.TARGET_CONNECTIONS_PROP.equals(prop)) { refreshTargetConnections(); } }
protected void refreshVisuals() { Rectangle bounds = new Rectangle(getCastedModel().getLocation(), getCastedModel().getSize()); figure.setBounds(bounds); ((GraphicalEditPart) getParent()).setLayoutConstraint(this, figure, bounds); } }
由于图形可以连接到其他图形,图形编辑部件重写了getModelSourceConnections
方法和getModelTargetConnections
方法。这两个方法的任务就是要通知GEF有关该图形的源连接和目标连接。此外,ShapeEditPart
实现了NodeEditPart接口。通过实现这个接口,编辑部件可以定义源锚点和目标锚点,锚点就是图形和连接接触的连接点。逻辑电路编辑器的例子使用这个功能来指定线如何连接到一个逻辑门元件。既然图形并没有特定的连接点,我们就使用包围矩形锚点,它将连接设置在可视图形的包围矩形上。如果你愿意,你可以为椭圆返回EllipseAnchor,它将返回一个椭圆边界上的点。对于更加复杂的图形,你应该继承AbstractConnectionAnchor类,并实现getLocation方法。注意,有两种方法需要实现:一个使用ConnectionEditPart对象作为参数,另一个使用Request对象。当一个新的连接被创建时,第二个方法会被调用以便用户得到反馈,而第一个方法用于已建立的连接。
图形编辑部件安装了两个策略。ShapeComponentEditPolicy
提供命令将一个图形从图删除。第二个策略处理图形间连接的创建和转移,它的索引字是GRAPHICAL_NODE_ROLE
。连接创建工具创建新的连接需要两个步骤。当用户点击模型元素的可视图形时,该策略被要求创建一个连接命令。如果这个方法返回null
,表示这个连接不能从所给的模型元素开始。如果允许连接的话,将创建新的命令,并作为起始命令存储在请求中。当用户点击另一个可视图形时,会要求策略提供一个连接完成命令。这是一个根据起始命令创建的新命令,而起始命令中包含了连接结束点的信息。
new GraphicalNodeEditPolicy() {
protected Command getConnectionCreateCommand(CreateConnectionRequest request) { Shape source = (Shape) getHost().getModel(); int style = ((Integer) request.getNewObjectType()).intValue(); ConnectionCreateCommand cmd = new ConnectionCreateCommand(source, style); request.setStartCommand(cmd); return cmd; }
protected Command getConnectionCompleteCommand(CreateConnectionRequest request) { ConnectionCreateCommand cmd = (ConnectionCreateCommand) request.getStartCommand(); cmd.setTarget((Shape) getHost().getModel()); return cmd; } ... }
图形节点编辑策略的另一个任务是提供连接的转移命令。连接可以修改连接的源或目标实现转移。连接转移命令和连接创建命令有同样的规则。尤其是当一个连接不能转移时,策略返回null。策略也可能通过canExecute
方法返回false来得到一个拒绝执行的命令。由于篇幅限制,这些命令的细节就不多说了,读者可以参考代码。
ConnectionEditPart 类
由于连接也是用户可编辑的模型对象,它们必须有自己的控制器。连接的控制器是由ConnectionEditPart
类实现,它继承自AbstractConnectionEditPart
类。和其他控制器类似,它也实现了PropertyChangeListener
接口,并注册自己为模型的监听器。连接部件返回一个带有箭头的线作为可视图形。它安装了两个编辑策略。第一个是ConnectionComponentPolicy,它提供删除命令给Delete菜单项所需要的action。第二个比较有意思。它含有一个被选择的连接,这个连接包括起始端和结束端的标识。没有这个策略,就不可能转移连接,因为当一个连接被拖动时,GEF没有办法获取连接两端的标识。GEF的设计者建议所有的ConnectionEditParts都应该有这个策略,即使连接的两端都不能拖动。至少这个策略提供了一种视觉上的选择反馈。propertyChange方法可以收到线风格属性的变化,并对线figure作相应的调整。
class ConnectionEditPart extends AbstractConnectionEditPart
implements PropertyChangeListener { protected IFigure createFigure() {
PolylineConnection connection = (PolylineConnection) super.createFigure(); connection.setTargetDecoration(new PolygonDecoration()); connection.setLineStyle(getCastedModel().getLineStyle()); return connection; } protected void createEditPolicies() {
installEditPolicy(EditPolicy.CONNECTION_ROLE, new ConnectionEditPolicy() { protected Command getDeleteCommand(GroupRequest request) { return new ConnectionDeleteCommand(getCastedModel()); } });
installEditPolicy(EditPolicy.CONNECTION_ENDPOINTS_ROLE, new ConnectionEndpointEditPolicy()); } public void propertyChange(PropertyChangeEvent event) { String property = event.getPropertyName();
if (Connection.LINESTYLE_PROP.equals(property)) { ((PolylineConnection) getFigure()). setLineStyle(getCastedModel().getLineStyle()); } } ... }
几何图形编辑器
几何图形编辑器继承了GraphicalEditorWithFlyoutPalette
类。这个类是图形编辑器的一种特殊形式,它本身也是一种编辑部件,并可以拥有一个提供工具的面板。使用这个类必须实现两个方法,getPaletteRoot
和getPalettePreferences
。第一个方法必须返回包含所有工具选项的面板的根节点。工具选项是一种特殊的面板选项,它将工具安装在编辑器的编辑域上。它们必须位于面板抽屉中,面板抽屉将工具选项很方便地组合起来。一般推荐有一个工具选项作为整个工具面板的缺省选项。一个典型的解决方法就是直接使用SelectionToolEntry
类的实例。第二个方法返回的面板首选项中包含的内容有,报告面板是可见还是被折叠起来了,面板停靠的位置,以及面板的宽度。通常的解决方法是将它们存在plug-in的首选项存储区中。
我们上面提到的编辑域起了一个中心控制器的作用。它负责保存工具,载入缺省工具,维护当前激活的工具,并将鼠标和键盘事件转发给当前激活的工具,以及处理命令栈。GEF提供了缺省实现,DefaultEditDomain
,你应该在编辑器的构造函数中设置它的实例。
图形编辑器的另一部分工作是创建并初始化图形viewer。图形viewer是一种特殊的EditPartViewer
,它能够做点击测试。我们可以使用GraphicalEditor
类提供的缺省viewer。然而,还是需要做一些事。在configureGraphicalViewer
方法中设置编辑部件的工厂类。这个工厂类必须实现一个接口EditPartFactory,这个接口只有一个方法,createEditPart(EditPart, Object)。它的第一个参数是编辑部件,它一般是所创建的编辑部件的父部件,第二个参数是新创建的编辑部件所对应的模型部件。其他要做的包括设置键处理器,上下文菜单等。
protected void configureGraphicalViewer() { super.configureGraphicalViewer(); GraphicalViewer viewer = getGraphicalViewer(); viewer.setRootEditPart(new ScalableRootEditPart());
viewer.setEditPartFactory(new ShapesEditPartFactory()); viewer.setKeyHandler( new GraphicalViewerKeyHandler(viewer).setParent(getCommonKeyHandler())); ContextMenuProvider cmProvider = new ShapesEditorContextMenuProvider(viewer, getActionRegistry()); viewer.setContextMenu(cmProvider); getSite().registerContextMenu(cmProvider, viewer); } protected void initializeGraphicalViewer() { super.initializeGraphicalViewer(); GraphicalViewer graphicalViewer = getGraphicalViewer();
graphicalViewer.setContents(getModel());
graphicalViewer.addDropTargetListener(createTransferDropTargetListener()); }
一旦工厂类被设置,你应该在图形viewer中设置内容。内容自然就是从IEditorInput
实例恢复得到的对象,IEditorInput
实例通过setInput
方法传递给编辑器。这个例子在图形viewer上添加一个目标放置监听器。它允许用户使用拖放的方式添加新图形,而不是选择加点击的方式。这个目标放置监听器使用TemplateTransferDropTargetListener
的子类,它使用CreateRequest
来获得添加对象到模型的命令,这个模型当然就是拖放动作结束时所在的编辑部件所表示的模型。
除了上面谈到的任务,编辑器还负责监视命令栈来报告当前编辑的内容是否被修改。这是一个比较好的解决方法,因为它可以使这个标记和用户所做的undo和redo同步起来。注意,命令栈含有上次存储的位置信息,这个信息在doSave
和doSaveAs
这两个方法中被标记。编辑器的其他细节,比如模型的实际存储和恢复,这里就不讨论了,因为它们和具体的应用相关。接下来,我们讨论编辑器的如何将编辑器内容暴露给其视图,如何将菜单选项和编辑器的action联系起来,以及其他workbench协作的技术。
和workbench集成在一起
到目前为止,我们谈的都是这个几何图形编辑器如何工作。然而,它没有和workbench很好地集成。例如,Edit菜单动作,比如Delete,Undo和Redo,就不能工作。其他视图不能用其他方式显示编辑器内容。换句话说,目前所完成的编辑器没有很好地利用Eclipse workbench的优势。在下面的三小节,将解释如何将这个孤立的编辑器变成workbench的一部分。
编辑器Action
ShapesEditor
类创建了大量缺省 ,它们在编辑器初始化过程中被createActions
方法中创建。这些 是undo,redo,select all,delete,save和print。为了将标准菜单选项连接到这些 ,你应该在plugin.xml
文件中定义一个action bar contributor。在这个action bar contributor中,你需要实现两个方法。第一个是buildActions
,它可以为undo,redo和delete创建可重定位的 。如果你需要使用键盘选择所有的widget,你需要在第二个方法declareGlobalActionKeys
中为所选择的 添加一个全局 关键字。
public class ShapesEditorActionBarContributor extends ActionBarContributor {
protected void buildActions() { this.addRetargetAction(new UndoRetargetAction()); this.addRetargetAction(new RedoRetargetAction()); this.addRetargetAction(new DeleteRetargetAction()); } public void contributeToToolBar(IToolBarManager toolBarManager) { super.contributeToToolBar(toolBarManager); toolBarManager.add(getAction(ActionFactory.UNDO.getId())); toolBarManager.add(getAction(ActionFactory.REDO.getId())); } protected void declareGlobalActionKeys() {
this.addGlobalActionKey(ActionFactory.SELECT_ALL.getId()); } }
我们来仔细看一下当用户在Edit菜单中选择Delete时发生了些什么(看图12)。ShapesEditor
类的父类将删除动作添加到动作注册表中。当删除动作被执行时,它检查当前的所选择的对象是否是EditPart
类的实例。对每个对象,它都从编辑部件中请求一个命令。接下来,每个编辑部件检查是否有编辑策略可以处理删除请求。对几何图形,ShapeComponentEditPolicy
可以处理删除请求,并且返回ShapeDeleteCommand
实例。动作执行该命令,从而将图形从图中删除。图发送一个属性修改事件,DiagramEditPart
收到该事件,最终使得代表被删除图形的矩形或椭圆从显示中被删除。
提供属性
每个图形编辑器都是可以发送选择事件。你可以建立一个视图,并将它作为选择监听器注册在workbench site的页面上。每次你在图形编辑器中选择一个对象,你的视图都会在selectionChanged
方法中收到一个通知。Eclipse的一个标准视图,Properties视图,会监听选择事件,并且每次都检查这个对象是否实现了IPropertySource
接口。如果是的话,它使用这个接口的方法来查询所选择的对象属性,并以表格的方式显示出来。
通过上面所描述的,在图形编辑器中提供对象的属性只要实现IPropertySource
接口就可以了。通过查看Shape
类,你可以看到对象的位置和大小是如何在Properties视图中显示的。
提供Outline
Outline视图是另一种,常常也是更简洁的查看模型对象的方式。在Java编辑器中,它可以用来显示一个类所import的类,所包含的变量,和方法,却不需要用户深入代码。图形编辑器也可以使用这个视图。图形编辑器和逻辑电路编辑器类似,可以以树的方式显示所编辑的内容(看图1)。数据库schema编辑器[7]也提供了类似的视图。
为了将所编辑的内容提供给Outline视图,你需要重写getAdapter
方法,并当adapter类为IContentOutlinePage
接口时,返回一个outline实现。实现outline的最简单的方法是扩展ContentOutlinePage
类,并提供适当的EditPartViewer
。
public Object getAdapter(Class type) { // returns the content outline page for this editor
if (type == IContentOutlinePage.class) { if (outlinePage == null) { outlinePage = new ShapesEditorOutlinePage(this, new TreeViewer()); } return outlinePage; } return super.getAdapter(type); }
在我们这个例子中,编辑部件视图是有一个TreeViewer实现的。你应该和主编辑器一样提供给它同样的编辑域。TreeViewer,就象其他EditPartViewer
,需要一个创建子编辑部件的方法。编辑器和DiagramEditPart
一样,都是设置一个编辑部件工厂。此外,outline中的选择和主编辑窗口的选择需要通过选择同步器同步起来,选择同步器是一个GEF工具类,它协调两个编辑部件的选择状态。ShapesTreeEditPartFactory
根据模型类型,返回ShapeTreeEditPart
或DiagramTreeEditPart
的实例。通过这些类,读者应该可以轻易地发现这些模式很熟悉。两个编辑部件都实现了PropertyChangeListener
接口,并通过调整模型的可视化表示来对属性变化做出响应。它们都安装编辑策略来控制通过它们所暴露的交互类型。
GEF用到的设计模式
GEF通过大量使用设计模式来得到它的灵活性。下面是一下经常碰到的模式的小结。详细内容,请参考 [2]。
IFigure
接口。控制的类型必须是EditPart
或它的子类。Commands
,这些Commands
以链的方式组织在一起。createChild
方法允许你不使用工厂就创建子编辑部件。总结
我希望能够对这个简单图形编辑器的大多数方面作详细的描述。提供足够的信息使得人们能够读完这篇文章,去看更大的例子,比如逻辑电路编辑器。通过理解象CircuitEditPart
,AndGateFigure和其他类的角色,你可以关注其他例子的更复杂的方面。在GEF的众多领域和技术中,有很多我甚至都没有涉及过。然而,这些技术只有在很好地理解基础内容的情况下,才可能去学习。毕竟,如果你为了使Select All菜单项工作都要花数小时,那么设计一个拖反馈的目的又是什么呢?
感谢
我想感谢Randy Hudson,他的意见帮助提高了本文结构和准确性。我也感谢Jill Sueoka仔细检查我所写一个又一个版本。
参考书目
[1] | Eric Bordeau, Using Native Drag and Drop with GEF, Eclipse Corner Article, August 2003 |
[2] | Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides, Design Patterns: Elements of Reusable Object-Oriented Software, Addison Wesley, 1995, ISBN 0-201-63361-2 |
[3] | Randy Hudson, Create an Eclipse-based application using the Graphical Editing Framework, IBM developerWorks, July 2003 |
[4] | Daniel Lee, Display a UML Diagram using Draw2D Diagram, Eclipse Corner Article, August 2003 |
[5] | Xavier Mehaut et al., Synthetic GEF description, June 2004 |
[6] | William Moore, David Dean, Anna Gerber, Gunnar Wagenknecht and Philippe Vanderheyden, Eclipse Development using the Graphical Editing Framework and the Eclipse Modeling Framework, IBM RedBooks, 2004, ISBN 0738453161 |
[7] | Phil Zoio, Building a Database Schema Diagram Editor with GEF, Eclipse Corner Article, September 2004 |
相关推荐
在这个场景下,我们关注的是一个名为“计算机图形学图形编辑器”的工具,它允许用户绘制并操作基本的几何图形,如圆、多边形和椭圆。 首先,我们从标题中了解到这个编辑器专注于图形的创建。在计算机图形学中,基本...
《易语言几何图形模块1.0及使用例程》是一个专为易语言用户设计的图形绘制工具包,它提供了一系列接口和示例程序,帮助开发者在易语言环境中创建丰富的图形界面和动态图形效果。易语言作为一款中国本土的编程语言,...
在IT领域,几何图形面积计算工具是一款非常实用的软件,专为解决数学问题中的几何图形面积计算而设计。它能够帮助用户快速、准确地计算出各种几何形状的面积,包括但不限于三角形、矩形、圆形、平行四边形、梯形、...
易语言源码易语言平面几何图形模块源码.rar 易语言源码易语言平面几何图形模块源码.rar 易语言源码易语言平面几何图形模块源码.rar 易语言源码易语言平面几何图形模块源码.rar 易语言源码易语言平面几何图形模块...
《几何制图与几何图形制作详解》 在信息化时代,几何制图软件已经成为了学习、工作及生活中不可或缺的工具。本软件专注于几何图形的创建,无论是日常生活中的简单描绘,还是教学过程中的复杂示例,都能轻松应对。...
"如何画好数学试卷中的几何图形PPT教案.pptx" 本资源是一个详细的PPT教案,旨在帮助教师和学生画出数学试卷中的几何图形。该教案共13页,涵盖了画图形、标注字母、标注角和弧线、画坐标系、画抛物线和双曲线等多个...
【图形编辑器】是一种软件工具,它允许用户创建、修改和操作各种图形对象,如线条、形状、图像等。在开发这样一个系统时,设计模式起着至关重要的作用,因为它们提供了解决常见问题的结构化方法,使得代码更加可维护...
此外,为了提升用户体验,可能还需要实现一些附加功能,比如图形的编辑(添加、删除、移动顶点)、选择与高亮显示,以及图形属性(如颜色、填充)的设置。这通常涉及更多的API调用和事件监听。 总的来说,"百度地图...
“在Word中画几何图形的技巧” 本文将教您如何在Word中画出简单的几何图形,例如坐标、矩形、...通过掌握这些知识点,您可以在Word中画出简单的几何图形,并且可以对图形进行编辑、组合、取消组合、标注和排版等操作。
【超实用】各种几何图形计算涉及的是数学领域中的几何学,尤其在实际应用中,如建筑、工程、设计等行业的日常工作中,这类计算是必不可少的工具。几何图形计算包括平面几何和立体几何两大部分,主要关注图形的尺寸、...
在IT领域,几何图形计算是一种涉及数学、计算机图形学和编程技术的重要概念。它主要研究如何用计算机处理和分析二维和三维几何形状,这在游戏开发、计算机辅助设计(CAD)、虚拟现实、图像处理和科学可视化等多个...
写有全面的几何图形计算公式;在表中写入几何图形的相关尺寸数据,表中即可自动计算出各种几何图形的面积、体积或长度。还有能自动计算各种贮藏池的容积与施工制作的用料量。展示了电子表格的多面应用功能,是一款...
在本压缩包中,我们拥有一个关于几何图形识别的源代码项目。这个项目分为两个主要部分:一个是生成动态链接库(DLL),另一个是基于MFC(Microsoft Foundation Classes)的简单对话框应用程序。这两个部分共同作用,...
3. 几何图形(满分50分) 版本1:满分 10 分 设计抽象类 GeometricObject 及其子类 Triangle 和Circle。 GeometricObject 类设计要求如下: ■ 一个名为 color 的Color类型的私有数据域,表示对象的颜色 ■ 一个名...
本资源摘要信息涉及到Word实训项目7的多个知识点,包括数学公式录入、数学公式编辑器、平面几何图形绘制、段落格式设置、公式制作和平面几何图形制作等。用户需要了解这些知识点,以便正确地制作数学试卷。
"几何图形抽象艺术创意向日葵色扁平化ppt模板"是一款专为需要展示专业且有设计感内容的人群设计的资源。这款模板的独特之处在于它结合了多种几何图形——圆形、矩形、三角形,以及实线和虚线,这些元素在视觉传达上...
这篇内容将深入解析标题“半透明几何图形创意朦胧紫背景iOS风格ppt模板”以及相关标签所涉及的IT知识点,特别是如何在设计中运用这些元素来创建高质量的演示文稿。 首先,我们要理解“iOS风格”。iOS是苹果公司开发...
简单几何图形计算.exe
标题中的“红色几何图形通用PPT模板”表明这是一个设计风格以红色几何图形为主的PowerPoint(PPT)模板,适用于各种用途。这样的模板通常包含了一系列预设的幻灯片布局,帮助用户快速创建具有专业外观的演示文稿。...