鸡年第一天,首先向大家拜个年——恭祝新春快乐,万事如意。一年之计在于春,你对新的一年有什么安排呢?好的,下面还是进入正题吧。
关于Java2D相信大家都不会陌生,它是基于AWT/Swing的二维图形处理包, JDK附带的示例程序向我们展示了Java2D十分强大的图形处理能力。在Draw2D出现以前,SWT应用程序在这方面一直处于下风,而Draw2D这个SWT世界里的Java2D改变了这种形势。
可能很多人还不十分了解GEF和Draw2D的关系:一些应用程序是只使用Draw2D,看起来却和GEF应用程序具有相似的外观。原因是什么,下面先简单解释一下:
GEF是具有标准MVC(Model-View-Control)结构的图形编辑框架,其中Model由我们自己根据业务来设计,它要能够提供某种模型改变通知的机制,用来把Model的变化告诉Control层;Control层由一些EditPart实现,EditPart是整个GEF的核心部件,关于EditPart的机制和功能将在以后的帖子里介绍;而View层(大多数情况下)就是我们这里要说的Draw2D了,其作用是把Model以图形化的方式表现给使用者。
虽然GEF可以使用任何图形包作为View层,但实际上GEF对Draw2D的依赖是很强的。举例来说:虽然EditPart(org.eclipse.gef.EditPart)接口并不要求引入任何Draw2D的类,但我们最常使用的AbstractGraphicalEditPart类的createFigure()方法就需要返回IFigure类型。由于这个原因,在GEF的SDK中索性包含了Draw2D包就不奇怪了,同样道理,只有先了解Draw2D才可能掌握GEF。
这样,对于一开始提出的问题可以总结如下:Draw2D是基于SWT的图形处理包,它适合用作GEF的View层。如果一个应用仅需要显示图形,只用Draw2D就够了;若该应用的模型要求以图形化的方式被编辑,那么最好使用GEF框架。
现在让我们来看看Draw2D里都有些什么,请看下图。
图1 Draw2D的结构
Draw2D通过被称为LightweightSystem(以下简称LWS)的部件与SWT中的某一个Canvas实例相连,这个Canvas在Draw2D应用程序里一般是应用程序的Shell,在GEF应用程序里更多是某个Editor的Control(createPartControl()方法中的参数),在界面上我们虽然看不到LWS的存在,但其他所有能看到的图形都是放在它里面的,这些图形按父子包含关系形成一个树状的层次结构。
LWS是Draw2D的核心部件,它包含三个主要组成部分:RootFigure是LWS中所有图形的根,也就是说其他图形都是直接或间接放在RootFigure里的;EventDispatcher把Canvas上的各种事件分派给RootFigure,这些事件最终会被分派给适当的图形,请注意这个RootFigure和你应用程序中最顶层的IFigure不是同一个对象,前者是看不见的被LWS内部使用的,而后者通常会是一个可见的画布,它是直接放在前者中的;UpdateManager用来重绘图形,当Canvas被要求重绘时,LWS会调用它的performUpdate()方法。
LWS是连接SWT和Draw2D的桥梁,利用它,我们不仅可以轻松创建任意形状的图形(不仅仅限于矩形),同时能够节省系统资源(因为是轻量级组件)。一个典型的纯Draw2D应用程序代码具有类似下面的结构:
//创建SWT的Canvas(Shell是Canvas的子类) Shell shell = new Shell(); shell.open(); shell.setText("A Draw2d application"); //创建LightweightSystem,放在shell上 LightweightSystem lws = new LightweightSystem(shell); //创建应用程序中的最顶层图形 IFigure panel = new Figure(); panel.setLayoutManager(new FlowLayout()); //把这个图形放置于LightweightSystem的RootFigure里 lws.setContents(panel); //创建应用程序中的其他图形,并放置于应用程序的顶层图形中 panel.add(); while (!shell.isDisposed ()) { if (!display.readAndDispatch ()) display.sleep (); }
接下来说说图形,Draw2D中的图形全部都实现IFigure(org.eclipse.draw2d.IFigure)接口,这些图形不仅仅是你看到的屏幕上的一块形状而已,除了控制图形的尺寸位置以外,你还可以监听图形上的事件(鼠标事件、图形结构改变等等,来自LWS的EventDispatcher)、设置鼠标指针形状、让图形变透明、聚焦等等,每个图形甚至还拥有自己的Tooltip,十分的灵活。
Draw2D提供了很多缺省图形,最常见的有三类:1、形状(Shape),如矩形、三角形、椭圆形等等;2、控件(Widget),如标签、按钮、滚动条等等;3、层(Layer),它们用来为放置于其中的图形提供缩放、滚动等功能,在3.0版本的GEF中,还新增了GridLayer和GuideLayer用来实现"吸附到网格"功能。在以IFigure为根节点的类树下有相当多的类,不过我个人感觉组织得有些混乱,幸好大部分情况下我们只用到其中常用的那一部分。
图2 一个Draw2D应用程序
每个图形都可以拥有一个边框(Border),Draw2D所提供的边框类型有GroupBoxBorder、TitleBarBorder、ImageBorder、ButtonBorder,以及可以组合两种边框的CompoundBorder等等,在Draw2D里还专门有一个Insets类用来表示边框在图形中所占的位置,它包含上下左右四个整型数值。
我们知道,一个图形可以包含很多个子图形,这些被包含的图形在显示的时候必须以某种方式被排列起来,负责这个任务的就是父图形的LayoutManager。同样的,Draw2D已经为我们提供了一系列可以直接使用的LayoutManager,如FlowLayout适合用于表格式的排列,XYLayout适合让用户在画布上用鼠标随意改变图形的位置,等等。如果没有适合我们应用的LayoutManager,可以自己定制。每个LayoutManager都包含某种算法,该算法将考虑与每个子图形关联的Constraint对象,计算得出子图形最终的位置和大小。
图形化应用程序的一个常见任务就是在两个图形之间做连接,想象一下UML类图中的各种连接线,或者程序流程图中表示数据流的线条,它们有着不同的外观,有些连接线还要显示名称,而且最好能不交叉。利用Draw2D中的Router、Anchor和Locator,可以实现多种连接样式,其中Router负责连接线的外观和操作方式,最简单的是设置Router为null(无Router),这样会使用直线连接,其他连接方式包括折线、具有控制点的折线等等(见图3),若想控制连接线不互相交叉也需要在Router中作文章。Anchor控制连接线端点在图形上的位置,即"锚点"的位置,最易于使用的是ChopBoxAnchor,它先假设图形中心为连接点,然后计算这条假想连线与图形边缘的交汇点作为实际的锚点,其他Anchor还有EllipseAnchor、LabelAnchor和XYAnchor等等;最后,Locator的作用是定位图形,例如希望在连接线中点处以一个标签显示此连线的名称/作用,就可以使用MidpointLocator来帮助定位这个标签,其他Locator还有ArrowLocator用于定位可旋转的修饰(Decoration,例如PolygonDecoration)、BendpointerLocator用于定位连接控制点、ConnectionEndpointLocator用于定位连接端点(通过指定uDistance和vDistance属性的值可以设置以端点为原点的坐标)。
图3 三种Router的外观
此外,Draw2D在org.eclipse.draw2d.geometry包里提供了几个很方便的类型,如Dimension、Rectangle、Insets、Point和PointList等等,这些类型既在Draw2D内部广泛使用,也可以被开发人员用来简化计算。例如Rectangle表示的是一个矩形区域,它提供getIntersection()方法能够方便的计算该区域与另一矩形区域的重叠区域、getTransposed()方法可以得到长宽值交换后的矩形区域、scale()方法进行矩形的拉伸等等。在自己实现LayoutManager的时候,由于会涉及到比较复杂的几何计算,所以更推荐使用这些类。
以上介绍了Draw2D提供的大部分功能,利用这些我们已经能够画出十分漂亮的图形了。但对大多数实际应用来说这样还远远不够,我们还要能编辑它,并把对图形的修改反映到模型里去。为了漂亮的完成这个艰巨任务,GEF绝对是不二之选。从下一次开始,我们将正式进入GEF的世界。
参考资料:
- GEF Developer Guide
- Eclipse Development - Using the Graphical Editing Framework and the Eclipse Modeling Framework
- Displaying a UML Diagram with Draw2D