【IT168 技术文章】Java 专家 Barry Feigenbaum 创建了保留的图形对象(Retained Graphic Objects (RGO))API 来启用 Java 平台的可重用图形对象的创建。本文中,Barry 描述了组成 RGO API 的类和方法,并在 Java 编程环境中演示了它们的使用。遵照本文中的上机实践示例,您将学会如何创建并保留诸如直线、椭圆、矩形、文本、子图形等对象类型。
创建持久(或者保留的)图形的能力还不是“抽象窗口工具箱(Abstract Window Toolkit (AWT))”的标准部分。相反,绘制的每个图形在每次显示其包含的容器时由应用程序代码重新绘制。本文中,我将向您介绍,如何通过向 Java 平台添加保留的图形对象(RGO)来解决 AWT 这个局限性。我的开发的方法 ― RGO API ― 需要的代码最少,并且可以在需要的时候允许您添加并实现多种对象类型。
保留的图形对象
RGO 提供了基于创建可绘制对象(类 Drawable 的实例)的强大和便利的绘图性能,这些对象收集在一个有序集(类 DrawableSequencer 的实例)中,然后每当需要呈现一个给定的对象时,它就重新绘制这个集合。可绘制对象和对象集都有描绘服务并且可以持久地储存它们自己。第三个相关类称为 DrawingContext ,它抽取类 java.awt.Graphics 中定义的 Java 对象表示服务。(请注意,出于本文的目的,我们将使用 Graphics 类,而不是更强大更复杂的 Graphics2D 。将 Graphics2D 用于更丰富的图形是一个简单的扩展问题。)
通过为每个保留的对象形式创建类的类型来定义 RGO。这些类都是类 Drawable 的子类,它们定义了可以保留的图形对象的类型。直线、椭圆、矩形、文本和子图形是其中一些类型示例。我们在本文中定义的相应样本对象是:
DrawableLine
DrawableOval
DrawableBox
DrawableText
DrawableSprite
许多其它图形对象类型,比如位图、三角形和常规的多边形可以根据需要添加。
使用 drawables
图 1 是一个说明 drawables 包中各类之间关系的 UML 模型。请注意 Drawables 、 DrawableSequencer 和 DrawingContext 之间的关系。
图 1. drawables 包中各类的关系
在下面的图 2 中,我向 DrawableSequencer 实例添加了 Drawable 子类的三个实例。一组 DrawableSequencerObject 实例记录了可绘制对象的显示优先级和对它的一个引用(实线箭头所示)。 DrawableSequencerObject 实例记录在 DrawableSequencer 实例内部的集合(实际上是 java.util.Vector )中。
在任何 DrawableSequencer.draw(DrawingContext) 方法调用期间,使用 DrawableSequencerEnumerator 实例来枚举(通过虚线箭头) DrawableSequencerObject 对象。
图 2. 向 DrawableSequencer 添加 Drawable 的三个实例
DrawingContext 实例实际上将在应用程序客户机窗口中描绘对象。客户机窗口是由 java.awt 和 javax.swing 类库中的类以及应用程序维护的。枚举器调用 Drawable.draw(DrawingContext) 方法。那个方法转而调用 Drawable.drawWorker(DrawingContext) 方法,通过调用 DrawingContext.draw...() 方法来处理请求,其中的省略号(...)表示图形类型。然后, DrawingContext 实例将绘图请求转发到与 DrawingContext 关联的 java.awt.Graphics 实例。
图 2 中显示的描绘对象步骤如下所示:
使用在其它地方创建的 java.awt.Graphics 对象创建 DrawingContext 实例。(这通常在应用程序的 paint(Graphics) 方法中完成,其中 Graphics 对象作为来自 Java 运行时支持的参数传入。)
调用传递 DrawingContext 实例的 DrawableSequencer.draw() 方法。
DrawableSequencer.draw() 创建 DrawableSequencerEnumerator 实例来遍历 DrawableSequencer 的集合中的所有对象。
下一步,对于集合中的每个 DrawableSequencerObject :
为 Drawable 实例调用 Drawable.draw() 方法,该实例与向它传递 DrawingContext 实例的 DrawableSequencerObject 实例相关联。
如果这个实例可见,那么 Drawable.draw() 立即为那个实例调用 Drawable.drawWorker() 。
Drawable.drawWorker() 通过调用形成对象的对象所需的 DrawingContext.draw... 例程来处理描绘请求。例如, DrawableBox 对象将使用 DrawingContext.drawBox() 方法。 Drawable.drawWorker() 方法可能在调用 DrawingContext.draw...() 方法之前执行对象转换。
然后 DrawingContext.draw...() 通过调用一个或多个 java.awt.Graphics() 方法来处理描绘请求。
为绘制一个实心(相对于空心)矩形, DrawingContext.drawBox() 方法将使用 Graphics.setColor() 方法和 Graphics.fillRect() 方法。然后, DrawingContext.draw...() 方法可能在调用 Graphics 方法完成操作之前执行对象转换。
在以下各节中,我将描述用来定义 RGO 编程框架(也称为 API)的类和方法。本文中讨论的所有类都在缺省 Java 包中。
有两种不同的方法来持久地存储 Drawable 对象。第一种方法使用 Java 平台的 Serializable 接口和 ObjectStreams ,该方法需要的代码量最少。这种方法中,每个 Drawable 对象和所有可从该方法获得的对象都必须实现 Serializable 接口。第二种方法(称为“自存储”,或者显式地为持久字段的存储编码)需要更多编码,但产生的持久文件要小得多(通常不到那些使用 Serializable 生成的文件大小的一半)。在保存复杂图形时,这个大小差别是很重要的。例如,比较存储包含 25 个测试 DrawableText 对象的图形所需的空间 ― 使用 Serializable 接口创建的文件大小为 2,636 字节,而使用“自存储”方法创建的文件大小为 1,284 字节。
在“自存储”方法中,持久保存的 drawables 继承了 PersistentDrawable 类。每个带新数据字段的 PersistentDrawable 子类必须定义 storeOn() 和 loadFrom() 方法。
drawables 包
在示例 drawables 包中找到的类是以表 1 中显示的层次结构组织起来的。
表 1. drawables 包的类层次结构
| 类名 | 属性 | 实现方法 |
Drawable | abstract, public | draw 、 storeOn 、 loadFrom |
DrawableBox | public | drawWorker 、 storeOn 、 loadFrom |
DrawableLine | public | drawWorker 、 storeOn 、 loadFrom |
DrawableOval | public | drawWorker 、 storeOn 、 loadFrom |
DrawableRectForm | abstract, public | storeOn 、 loadFrom |
DrawableBox | public | drawWorker |
DrawableOval | public | drawWorker |
DrawableSprite | public | drawWorker 、 storeOn 、 loadFrom |
DrawableText | public | drawWorker 、 storeOn 、 loadFrom |
我已经在 Drawable 或者 DrawableRectForm 超类中放置了尽可能多的公共行为。
方法和构造器
下列方法和构造器对于组成 drawables 包的类是公共的。
clone() 方法
标准 Java 类库的一个弱点是很少使用 java.lang.Cloneable 接口。很明显 Java 语言的设计者考虑到了克隆,因为 clone() 方法已在 java.lang.Object 类中定义。但他们还是决定在没有实现 Cloneable 接口的类中禁止该方法。让大多数类(如果不是所有的类)实现标准的克隆方法后,类(特别是象 Point 和 Rectangle 这样的简单数据结构类)产生一个一致的办法来复制这些对象。没有这个办法,您有时必须通过使用复制构造器,或者通过带有待复制对象字段的引用(作为参数)的普通构造器,或者通过目标类的其它一些工厂(factory)方法来创建对象的副本。这样的选择取决于目标类。它迫使复制代码要知道要进行复制的目标类型。通用克隆能力则无须这么做。 clone() 方法提供给了本文(在它实际可以使用的地方)中定义的所有类。
toString() 方法
每个类实现 toString() 方法。这样允许类的实例用在 String 实例所需的任何地方(比如用于打印实例)。在类不是 final(也就是说,类可以生成子类)的情况下, toString 行为在 toString() 方法(它生成类名称和括起的括号)和 bodyToString() 方法(它生成对象的内容)之间被分割。(这类似于通常用在 java.awt 和 javax.swing 包中的 toString() 方法和 paramString() 方法。)
这个方法允许所有的子类使用公共 toString() 方法,而允许一个子类一个子类地进行特定格式编排。
构造器类型 drawables 包中的每个类可以支持几种构造器类型:
缺省构造器:这是一个不带参数的构造器。缺省构造器用于类 Drawable 的所有非抽象子类来支持持久对象的重新装入。如果在参数没有提供信息的情况下无法使对象处于可用状态,特殊的类就可能不提供缺省构造器。
普通构造器:这个构造器提供在实例创建时指定的所有参数。
备用构造器:这个构造器提供在实例创建时指定的参数子集(通常是常用的那些)。请注意,一些类不能提供备用构造器。
复制构造器:这个构造器通过复制相同类的另一个实例来初始化新创建的对象。 clone() 方法使用它。这样,它就可以被提供 clone() 方法的任意类所使用。
访问方法
访问方法提供了对重要的私有(private)或者受保护的(protected)实例变量的公开(public)访问。要获取变量值,可以使用 get
类 Drawable 是可绘制对象的一个抽象模型,用于提供这些对象的公共定义和行为。每个可绘制对象的类型由 Drawable 的非抽象子类定义。这些子类定义了受支持的 RGO。我准备了一个可绘制对象类型的具有代表性的样本,这些对象可能使用 java.awt.Graphics 服务。通过建立 Drawable 或者 DrawableRectForm 的子类可以添加其它保留对象类型。
所有 Drawable 实例共享一些公共特征:
可见性:每个 Drawable 实例可以标记为 visible(可见)或 invisible(不可见)。不绘制不可见对象。实例的可见性可以在任何时候更改。
缩放:每个 Drawable 实例都有一个缩放因子(即,缩放比:M:N)。缺省值为 1:1 的比例。当 M 值比 N 值大时放大对象。相应地,M 值小于 N 值时缩小对象。这个比例通过类 Scale 实现。请注意,使用整数比例而不是浮点进行缩放,是因为整数比例需要较少的运行时计算(即,浮点和/或双精度与整数格式之间的转换)。
颜色:每个 Drawable 实例都有一个 java.awt.Color 值。通常以这种颜色绘制实例。子类可能支持多颜色对象。在这种情况下,颜色值可以被忽略或者作缺省值或者底色。
请参阅下面的清单 1,查看类 Drawable 的定义。 Drawable 是抽象的,因为它本身没有绘图功能。它是可克隆的,因为这样很容易在不知道特殊对象子类名称的情况下复制 Drawable 对象。 draw() 和 DrawWorker() 方法实际上导致显示 Drawable 对象。 draw() 方法用来处理可见与不可见对象的表示。方法 drawWorker() 是一个抽象方法,必须在 Drawable 的所有非抽象子类中定义。它负责描绘对象。通常,它使用 DrawingContext 类的服务方法来完成。
当使用自存储持久性时, loadFrom() 和 storeOn() 方法为 PersistentDrawable 对象提供持久性支持。每个添加实例变量的 PersistentDrawable 子类必须覆盖这些方法。由于文章篇幅有限,我不在所有的子类定义中显示这些方法,但所有这些方法都与下面的类 DrawableSprite 显示和描述的方法相似。
清单 1. 类 Drawable 的定义
2 {
3 protected Color _color; // object color
4 protected boolean _visible; // object is shown
5 protected Scale _scaler; // scale by this
6 protected Drawable(Color color, boolean visible, Scale scale) {
7 _color = new Color(color.getRGB());
8 _visible = visible;
9 _scaler = (Scale)scale.clone();
10 }
11 // draw myself
12 public final void draw(DrawingContext context) {
13 if(_visible) drawWorker(context);
14 }
15 abstract protected void drawWorker(DrawingContext context);
16 // save myself persistently
17 public void storeOn(DataOutputStream dos) throws IOException {
18 dos.writeByte(_color.getRed());
19 dos.writeByte(_color.getGreen());
20 dos.writeByte(_color.getBlue());
21 dos.writeInt(_scaler.multBy); dos.writeInt(_scaler.divBy);
22 dos.writeBoolean(_visible);
23 }
24 // reload myself from persistent storage
25 public void loadFrom(DataInputStream dis) throws IOException {
26 _color = new Color(s.readByte(), is.readByte(), dis.readByte());
27 _scaler = new Scale(is.readInt(), dis.readInt());
28 _visible = dis.readBoolean();
29 }
30 }
类 Drawable 的子类
本节中总结的 Drawable 的可描绘子类将要提供如何编码可绘制对象的示例。它们不一定是绘图应用程序需要的所有类型对象的代表。可以在包含的源文件中找到下面描述的所有子类。
类 DrawableText
类 DrawableText 定义一行水平文本。 DrawableText 对象是 java.awt.Graphics.drawString 函数的直接表示。可以设置文本的开始位置、前景色和字体大小。实际所用的字体大小是为活动 Java 字体定义的最接近字体大小。如果所期望的字体太小(2 点或者更小),那么将无法绘制文本。
DrawableText 从 Drawable 继承而来。它定义 drawWorker(...) 方法。这个方法,象所有其它 Drawable 子类 drawWorker() 方法一样,只是调用类 DrawingContext 参数的服务方法。
清单 2. 类 DrawableText
2 protected Point _start; // coordinates
3 protected String _text;
4 protected int _fontSize; // display size
5 // draw myself
6 public void drawWorker(DrawingContext context) {
7 context.drawText(_scaler.scale(_start), _text, _scaler.scale(_fontSize), _color);
8 }
9 }
10
类 DrawableLine
类 DrawableLine 定义了一条直线。 DrawableLine 对象是 java.awt.Graphics.drawLine 函数的直接表示。可以设置直线的开始和结束位置以及颜色。如上所述, DrawableLine 的 drawWorker() 方法调用类 DrawingContext 参数的服务方法。这适用于 Drawable 的所有子类的 drawWorker() 方法。清单 3 是类 DrawableLine 的定义。
清单 3. 类 DrawableLine
2 {
3 protected Point _start, _end; // coordinates
4 // draw myself
5 public void drawWorker(DrawingContext context) {
6 context.drawLine(_scaler.scale(_start), _scaler.scale(_end), _color);
7 }
8 }
9
类 DrawableSprite
类 DrawableSprite 定义了一个 36 条直线的集合,这 36 条直线从一个公共中心点向外延伸,相邻两线之间的夹角为 10 度。每条直线有随意选择的不同颜色。因此忽略了继承的颜色值。可以设置中心点和直线的长度。 DrawableSprite 对象是由一些 java.awt.Graphics.drawLine 图元函数构成的。它显示 Drawable 子类不受 java.awt.Graphics 服务直接表示的限制。清单 4 是类 DrawableSprite 的定义。
清单 4. 类 DrawableSprite
2 {
3 protected Point _center; // coordinates
4 protected int _length; // line length
5 // draw myself
6 public void drawWorker(DrawingContext context) {
7 context.drawSprite(_scaler.scale(_center), _scaler.scale(_length));
8 }
9 }
10
DrawableSprite 的另一个实现是使用 DrawableLine 实例的集合。在这种情况下,不需要 DrawingContext.drawSprite() 方法。清单 5 显示了修改过的类 DrawableSprite 定义。这里,直线包含在实例中而不是由 DisplayContext 服务方法绘制。因为在对象中有多条直线,每条直线都必须由函数性方法处理。
清单 5. 类 DrawableSprite的数组实现
2 {
3 private int numlines = 36;
4 protected DrawableLine[] _lines = new DrawableLine[numlines];
5 public DrawableSprite(Point center, int length, boolean visible, Scale scale) {
6 super(Color.white, visible, scale);
7 double step;
8 int i;
9 PositiveRandom rgb = new PositiveRandom(256);
10 // make radial lines every 2Pi/numlines radians;
11 // each line is a different color
12 for(i = 0, step = 0 * Math.PI;
13 i < numlines;
14 i++, step += Math.PI / numlines) {
15 double sin = Math.sin(step), cos = Math.cos(step);
16 DrawableLine line = new DrawableLine(
17 Point(center.x + (int)((length / 10) * cos),
18 center.y - (int)((length / 10) * sin)),
19 Point(center.x + (int)( length * cos),
20 center.y - (int)( length * sin)),
21 new Color(rgb.next(), rgb.next(), rgb.next()), true, _scaler);
22 _lines[i] = line;
23 }
24 }
25 public void setScale(Scale scale) {
26 super.setScale(scale);
27 // scale each line
28 for(int i = 0; i < numlines; i++) {
29 _lines[i].setScale(scale);
30 }
31 }
32 // draw myself
33 public void drawWorker(DrawingContext context) {
34 // draw each line
35 for(i = 0; i < numlines; i++) {
36 _lines[i].draw(context);
37 }
38 }
39 // save myself
40 public void storeOn(DataOutputStream dos) throws IOException {
41 super.storeOn(dos);
42 // save each line
43 dos.writeInt(numlines);
44 for(int i = 0; i < numlines; i++) {
45 _lines[i].storeOn(dos);
46 }
47 }
48 // reload myself
49 public void loadFrom(DataInputStream dis) throws IOException {
50 super.loadFrom(dis);
51 // load each line
52 numlines = dis.readInt(numlines);
53 _lines = new DrawableLine[numlines];
54 for(int i = 0, i < numlines; i++) {
55 _lines[i] = new DrawableLine();
56 _line[i].loadFrom(dis);
57 }
58 }
59 }
60
loadFrom(...) 和 storeTo(...) 方法将从 Drawable 继承来的方法扩展为分别装入和保存该类引入的附加字段。
另一种实现是使用 DrawableSequencer 而不是数组来保留这些直线。这将不需要使用 for 循环来迭代每条直线。这个解决方案将需要 DrawableSequencer 从 Drawable 继承。
类 DrawableRectForm
类 DrawableRectForm 为可以由边界矩形框描述的可绘制对象定义了抽象模型。示例有实心和空心圆、椭圆以及矩形。可以设置对象的边角位置、宽度、高度和颜色。
清单 6 显示了类 DrawableRectForm 的定义。由于 DrawableRectForm 是抽象的,所以不需要为它实现 drawWorker(...) 方法。
清单 6. 类 DrawableRectForm
2 {
3 protected Rectangle _area; // coordinates & size
4 }
5
类 DrawableBox 和类 DrawableOval
类 DrawableBox 和类 DrawableOval 分别定义了实心矩形和圆形(或者椭圆形)对象。 DrawableBox 对象是 java.awt.Graphics.fillRect 函数的直接表示; DrawableOval 对象是 java.awt.Graphics.fillOval 函数的直接表示。可以设置对象的边角位置、宽度、高度和颜色。
清单 7 显示了类 DrawableBox 和类 DrawableOval 的定义。由于这两个类都从 DrawableRectForm 继承而来并且没有添加新的实例变量,因此它们不需要定义 loadFrom(...) 和 storeTo(...) 方法。
清单 7. 类 DrawableBox 和类DrawableOval
2 {
3 // draw myself
4 public void drawWorker(DrawingContext context) {
5 context.drawBox(_scaler.scale(_area), _color);
6 }
7 }
8 public class DrawableOval extends DrawableRectForm
9 {
10 // draw myself
11 public void drawWorker(DrawingContext context) {
12 context.drawOval(_scaler.scale(_area), _color);
13 }
14 }
15
其名称以 DrawableSequencer 开始的类负责创建并维护 Drawable 对象的有序集。这个集合可以编辑、可以在 DrawingContext 上描绘、可以存储到文件中,或者从文件装入。
每个对象根据绘制优先级排序。优先级较低(在数学意义上)的对象排列在优先级较高的对象后面。如果优先级较高的对象不透明,那么它将隐藏紧跟在它后面的优先级较低的对象。多个对象可能有相同的优先级。不指定相同优先级对象的相对绘图顺序。
DrawableSequencer 的编辑能力包括添加对象、除去对象或者除去所有的对象。通过除去对象,然后根据不同的优先级进行添加来重新排列对象。
DrawableSequencer 支持符合 java.util.Enumeration 的对象的创建。可以使用它们访问包含在序列中的对象。提供了两个枚举类型:
无限制:集合中的所有 Drawable 对象将按照优先级顺序被查看。
限制范围:其优先级值在指定的值范围之间(包括边界值)的集合中的所有 Drawable 对象按照优先级顺序被查看。如果高值小于低值,那么将看不到对象。
存储方法
当使用自存储持久性时, DrawableSequencer 提供服务来将它的对象持久地存储到文件中,并在稍后重新装入它们。重新装入一组已保存对象的集合意味着根据优先级将它们添加到现有的序列中。要用文件中的对象替换序列,必须首先从序列中除去所有对象。
当使用 Java 的 ObjectStream 持久性时, DrawableSequencer 提供了静态 loadFrom() 和 storeIn() 方法,它们使用 ObjectStreams 来装入和存储 Drawables 。这个机制总是替代现有的可绘制对象的内容。
在“自存储”方法中,每个对象通过它实例上的方法装入或者存储其本身。在 ObjectStream 持久性方法中,每个对象由外部应用的逻辑来保存或装入。因此,我更喜欢“自存储”方法。
在下面各节中,我们将讨论与类 DrawableSequencer 有关的每个类的定义和用法。
类 DrawableSequencer
类 DrawableSequencer 提供了上述支持。清单 8 是类 DrawableSequencer 的定义。
清单 8. 类 DrawableSequencer
2 {
3 private Vector _items; // ordered items in list
4 // add new drawable
5 public void addAt(Drawable drawable, int priority) {
6 int i;
7 // insert element at proper priority; higher priorities come last
8 for(i = 0; i < _items.size(); i++) {
9 if(priority <= dso.priority) // past all lower priorities elements
10 break;
11 }
12 _items.insertElementAt(new DrawableSequencerObject(priority, drawable), i);
13 }
14 // remove all drawables
15 public void removeAll() {
16 _items.removeAllElements();
17 }
18 // remove a drawable
19 public void remove(Drawable drawable) throws NoSuchElementException {
20 int i, size = _items.size();
21 // find element to remove
22 for(i = 0; i < size; i++) {
23 DrawableSequencerObject dso =
24 (DrawableSequencerObject)_items.elementAt(i);
25 if(drawable == dso.drawable) // must be the same (vs equal)
26 break;
27 }
28 if(i < size) // found one
29 _items.removeElementAt(i);
30 else
31 throw new NoSuchElementException(
32 "DrawableSequencer.remove() - drawable not found");
33 }
34 // get enumerator for all
35 public Enumeration elements() {
36 return elements(Integer.MIN_VALUE, Integer.MAX_VALUE);
37 }
38 // get enumerator for range
39 public Enumeration elements(int low, int high) {
40 return new DrawableSequencerEnumerator(_items, low, high);
41 }
42 // draw all matching drawables
43 protected void drawSelected(DrawingContext context, Enumeration e) {
44 // walk all drawables, and draw them
45 while(e.hasMoreElements()) {
46 Drawable d = (Drawable)e.nextElement();
47 d.draw(context); // draw current drawable
48 }
49 }
50 // draw all drawables
51 public void draw(DrawingContext context) {
52 drawSelected(context, elements());
53 }
54 // draw selected drawables
55 public void draw(DrawingContext context, int min, int max) {
56 drawSelected(context, elements(min, max));
57 }
58 }
59
loadFrom() 和 storeOn() 方法为包含在 DrawableSequencer 中的 Drawable 对象提供持久性支持。当需要每个文件的单个序列时,文件形式是很方便的。流形式使用一个现有的流。这样多个序列可以存储在一个流上。
清单 9 定义类 DrawableSequencer 的持久性服务。
清单 9. 类 DrawableSequencer的持久性服务
2 public void storeOn(DataOutputStream dos) throws IOException {
3 // walk all drawables, and store them
4 Enumeration e = _items.elements();
5 dos.writeInt(_items.size()); // write total count
6 while(e.hasMoreElements()) {
7 DrawableSequencerObject dso =
8 (DrawableSequencerObject)e.nextElement();
9 Drawable drawable = dso.drawable;
10 int priority = dso.priority;
11 dos.writeInt(priority); // write priority
12 dos.writeUTF(drawable.getClass().getName()); // then class name
13 drawable.storeOn(dos); // then class instance variables
14 dos.flush();
15 }
16 }
17 // store all on a stream
18 public void storeOn(File f) throws IOException {
19 FileOutputStream fos = new FileOutputStream(f);
20 BufferedOutputStream bos = new BufferedOutputStream(fos);
21 DataOutputStream dos = new DataOutputStream(bos);
22 storeOn(dos); // store myself
23 dos.close();
24 }
25 // load all from a stream
26 public void loadFrom(DataInputStream dis) throws IOException,
27 ClassNotFoundException, InstantiationException, IllegalAccessException {
28 int drawableCount; // number of objects in stream
29 // load all names
30 try {
31 // walk all stored drawables, and load them
32 drawableCount = dis.readInt();
33 for(int count = 0; count < drawableCount; count++) {
34 int priority = dis.readInt();
35 String className = dis.readUTF();
36 Class classObject = Class.forName(className);
37 Object object = classObject.newInstance();
38 if(!(object instanceof Drawable))
39 throw new ClassNotFoundException(className + " not a subclass of Drawable");
40 Drawable drawable = (Drawable)object;
41 drawable.loadFrom(dis);
42 addAt(drawable, priority);
43 }
44 }
45 catch(EOFException ex) { // map exception
46 throw new ClassNotFoundException(ex.getMessage());
47 }
48 }
49 // load all drawables from a file
50 public void loadFrom(File f) throws IOException,
51 ClassNotFoundException, InstantiationException, IllegalAccessException {
52 FileInputStream fis = new FileInputStream(f); // get a data stream to a file
53 BufferedInputStream bis = new BufferedInputStream(fis);
54 DataInputStream dis = new DataInputStream(bis);
55 loadFrom(dis); // load myself
56 dis.close();
57 }
58
类 DrawableSequencerObject
类 DrawableSequencerObject 表示存储在 DrawableSequencer 中的信息。 它记录 Drawable 对象和它的优先级。这个类不是公用的,因为只能由 DrawableSequencer 使用它。清单 10 是类 DrawableSequencerObject 的定义。
清单 10. 类DrawableSequencerObject
2 {
3 public int priority; // relative position selector
4 public Drawable drawable; // actual item to draw
5 }
6
类 DrawableSequencerEnumerator
类 DrawableSequencerEnumerator 提供了服务来迭代 DrawableSequencer 中的对象。本质上,它是 C 结构,并且记录鼠标的当前位置和选择范围的高低值。这个类不是公共的,因为它是由 DrawableSequencer 的方法创建的,并且符合公共接口 java.util.Enumeration 。
由于 DrawableSequencer 使用 java.util.Vector 来存放 Drawable 对象,所以枚举器只增加索引值来在序列中前进。不在期望范围内的值被跳过。索引总是在待访问序列的下一个对象上设置,或者越过最后一个对象设置。
清单 11 是类 DrawableSequencerEnumerator 的定义。
清单 11. 类DrawableSequencerEnumerator
2 {
3 private int _index; // relative position selector
4 private Vector _vector; // enumerate over this
5 private int _low, _high; // range selectors
6 public DrawableSequencerEnumerator(Vector vector, int low, int high) {
7 _vector = vector;
8 _low = low; _high= high;
9 // advance while outside of range to set to first
10 for(_index = 0; _index < _vector.size(); _index++) {
11 int priority =
12 ((DrawableSequencerObject)_vector.elementAt(_index)).priority;
13 if(priority >= _low && priority <= _high) // in range
14 break;
15 }
16 }
17 // test for done
18 public boolean hasMoreElements() {
19 return _index < _vector.size();
20 }
21 // get current and advance
22 public Object nextElement() {
23 Object result =
24 ((DrawableSequencerObject)_vector.elementAt(_index)).drawable;
25 // advance while inside of range to set to next
26 for(_index++; _index < _vector.size(); _index++) {
27 int priority =
28 ((DrawableSequencerObject)_vector.elementAt(_index)).priority;
29 if(priority >= _low && priority <= _high) // in range
30 break;
31 }
32 return result;
33 }
34 }
35
类 DrawingContext 提供对 java.awt.Graphics 绘图服务的访问,还提供下列附加服务:
缩放:每个 DrawingContext 提供逻辑窗口区域和逻辑框架区域。这些区域目前只限于一个正方形,但它可以简单地扩展为任意矩形。应用程序可以将框架区域与显示的框架大小相关联。 DrawingContext 实例将自动缩放对象,使窗口范围显示在一个框架大小的区域内。
平移:每个 DrawingContext 提供窗口原点。原点弥补了 java.awt.Graphics 的 x 和 y 方向与普通数学方向之间的差别。在 Java 语言中,x 从左到右递增,y 从上到下递增。在数学中,x 从左到右递增,y 从下到上递增。而且,移动原点产生扫视显示的对象的效果。
清单 12 是类 DrawingContext 的定义。
清单 12. 类 DrawingContext
2 {
3 protected Graphics _graphics; // associated graphics context
4 protected Point _origin; // origin in space
5 protected Scale _scaler; // scale by this
6 public DrawingContext(Graphics graphics, Point origin, Scale scale) {
7 _graphics = graphics.create(); // local copy that can change
8 _origin = new Point(origin.x, origin.y);
9 _scaler = (Scale)scale.clone();
10 }
11 // do rectangle adjustments
12 protected Rectangle map(Rectangle r) {
13 Point xy = map(r.x, r.y), wh = _scaler.scale(r.width, r.height);
14 return new Rectangle(xy.x, xy.y, wh.x, wh.y);
15 }
16 // do translate and scale
17 protected Point map(int x, int y) {
18 Point mapped;
19 mapped = _scaler.scale(x, y);
20 mapped.x = _origin.x + mapped.x; // x increases towards screen right
21 mapped.y = _origin.y - mapped.y; // y increases towards screen bottom
22 return new Point(mapped.x, mapped.y);
23 }
24 public void drawLine(Point start, Point end, Color color) {
25 _graphics.setColor(color);
26 Point s = map(start), e = map(end);
27 _graphics.drawLine(s.x, s.y, e.x, e.y);
28 }
29 public void drawBox(Rectangle area, Color color) {
30 // similar to drawLine above but uses _graphics.fillRect
31 }
32 public void drawOval(Rectangle area, Color color) {
33 // similar to drawLine above but uses _graphics.fillOval
34 }
35 public void drawSprite(Point center, int length) {
36 double Pi = Math.PI;
37 PositiveRandom rgb = new PositiveRandom(256);
38 Point c = map(center);
39 int l = _scaler.scale(length);
40 // make radial lines every 2Pi/36 radians
41 for(double step = 0 * Pi; step < 2 * Pi; step += Pi / 36) {
42 _graphics.setColor(new Color(rgb.next(), rgb.next(), rgb.next()));
43 double sin = Math.sin(step), cos = Math.cos(step);
44 _graphics.drawLine(c.x + (int)((l / 10) * cos), c.y - (int)((l / 10) * sin),
45 c.x + (int)( l * cos), c.y - (int)( l * sin));
46 }
47 }
48 public void drawText(Point start, String text, int fontSize, Color color) {
49 int newFontSize = _scaler.scale(fontSize);
50 if(newFontSize > 2) { // font can be seen
51 _graphics.setColor(color);
52 Font of = _graphics.getFont(); // scale the font
53 Font nf = new Font(of.getName(), of.getStyle(), newFontSize);
54 _graphics.setFont(nf);
55 Point p = map(start);
56 _graphics.drawString(text, p.x, p.y);
57 }
58 }
59 :
60 // other drawing services added here
61 :
62 }
63
原点和缩放访问方法(没有在清单中显示)允许动态更改通过 DrawingContext 显示的 DrawableSequencer 。每种绘图服务( draw(...) )调用一个或多个 java.awt.Graphics 服务从而在实际上描绘对象。特别的, drawSprite(...) 多次调用 Graphics.drawLine() 。 drawText(...) 服务必须进行特殊处理来弥补 Java 语言文本字体有限的缩放能力。
服务类
drawables 包提供并使用两个简单服务类,类 Scale 和类 PositiveRandom 。
类 Scale 提供服务来缩放各种数据类型。清单 13 显示了类 Scale 的接口。
清单 13. 类 Scale
2 {
3 public int multBy;
4 public int divBy;
5 public Scale(int m, int d);
6 public Scale();
7 public Scale(Scale other);
8 public String toString();
9 public Object clone();
10 public int scale(int v); // scale a value
11 public Point scale(int x, int y); // scale x, y pair
12 public Point scale(Point other); // scale a point
13 public Rectangle scale(Rectangle other); // scale a rectangle
14 public Dimension scale(Dimension other); // scale an extent
15 }
16
类 PositiveRandom 产生限制在 0:limit-1 (包含边界)范围的随机数字。清单 14 显示了类 PositiveRandom 的接口。
清单 14. 类 PositiveRandom
2 {
3 public PositiveRandom(int limit);
4 public PositiveRandom();
5 public int next(); // get a random number (0:limit-1)
6 }
7
备用方法
显示在 清单 8 中的 DrawableSequencer 仅仅是一个绘图引擎示例。可以构建其它序列器,它们可能基于 java.util.Map 接口。例如,如果不需要文件的持久性和其它 DrawableSequencer 服务(诸如缩放和平移),那么可以用简单的 Java 数组来代替序列器,如清单 15 所示。
清单 15. 简单的绘图应用程序
2 {
3 private static final int MAX_DRAWABLES = 100; // arbitrary
4 private Drawable[] drawables = new Drawable[MAX_DRAWABLES];
5 private int nextDrawable = 0;
6 // code to create a Drawable subclass
7 public void createDrawable(...) throws ... {
8 if(nextDrawable < length) {
9 Drawable d;
10 :
11 // code to make the drawable
12 :
13 drawables[nextDrawable++] = d; // record drawable
14 }
15 else
16 throw ...; // recover from full condition
17 }
18 // paint the collection
19 public void paint(Graphics g) {
20 DrawingContext dc = new DrawingContext(g);
21 // walk list of drawables
22 for(int i = 0; i < length; i++) {
23 if(drawables[i] != null)
24 drawables[i].draw(dc);
25 }
26 }
27 }
28
在一个完全不同的实现中,RGO 可以定义为 Java 接口而不是 Java 类。这样允许 RGO 充当在应用程序中使用的其它类型。然后,通常每个 RGO 类型都会有一个缺省实现(例如, DrawableLine 接口可以有 DefaultDrawableLine 或者 DrawableLineImpl 类)。
也可能设计备用类层次结构。例如,考虑如表 2 中所示的层次结构。
表 2. 备用类层次结构类名 属性 Drawableabstract, public DrawableCommentpublic PersistentDrawableabstract, public DrawablePointpublic ScaleableDrawableabstract, public DrawableSequencerpublic DrawableSpritepublic ColoredDrawableabstract, public DrawableBoxpublic DrawableLinepublic DrawableOvalpublic DrawableRectFormabstract, public DrawableBoxpublic DrawableOvalpublic DrawableTextpublic
这种组织允许创建非持久性的可绘制对象(诸如 DrawableComment )并且消除 DrawableSprite 中不使用的颜色值。现在有可能使用不可缩放的对象(诸如 DrawablePoint )。它还允许创建、绘制和持久保存 Drawable 对象的树结构。取决于所有包含对象的缩放需要, DrawableSequencer 可以从 ScaleableDrawable (如下所示)或者从 PersistentDrawable 继承。
尽管本文中包含的示例没有演示,但还是能很容易地想象使用上面的层次结构如何开发复杂的对象编辑器。这样的编辑器在任何时候都可以从 DrawableSequencer 添加和删除对象。无需从序列器中除去可绘制对象就可编辑它们本身。例如,可以使对象不可见或者更改它的缩放比。
备用设计可能性的讨论当然是很简要的,主要是用来激发您的想象。重要的是掌握我在本文中描述的编程模块的灵活性。可以构造 DrawableSequencer 集合来创建比这里定义的那些可绘制对象复杂得多的对象。 Drawable 对象还可以和多个 DrawableSequencer 以及诸如 java.util.Vector 的其它集合关联。如果序列器保存到文件中并被重新装入,那么每个序列器将获取以前共享对象的独立副本。
演示程序
我已经开发了一个简单的驱动程序来演示 drawable 包的能力。该演示有三个实现,如下面的表 3 所描述,每个实现所满足的功能性要求略有不同。
表 3. 三个演示
| 实现 | AWT GUI 对 Swing GUI* | 自存储对 ObjectStream 持久性 |
| DrawableTester | AWT | 自存储 |
| DrawableTester2 | Swing | 自存储 |
| DrawableTesterOS | Swing | ObjectStream |
基于 AWT 和基于 Swing 的 GUI 之间的一个显著差别是 AWT 的 Panel 调用方法 update() 作为 redraw() 方法的结果,这样应用程序就不需要绘制背景。Swing 的 JPanel,因为使用轻量级组件,没有这样做,所以 paint() 方法必须首先绘制背景。
通常来说, DrawableTester 演示提供接口来允许创建 Drawable 对象的多种类型,并将它们添加到单个 DrawableSequencer 中。然后它驱动序列器以便在它的客户机区域中显示对象。它提供了对对话框的访问来将当前的 Drawable 集合保存到用户可选的文件中,从用户可选的文件中重新装入它,然后改变已绘制对象的缩放比和原点。
下面几张图显示了保留的图形对象的实际外观和工作原理。同时鼓励您尝试一下包含在本文 参考资料一节中的源码清单。
图 3. 文本演示

图 4. 椭圆和方形演示,以及更改缩放比
请注意表 4 中对象的相对位置。许多对象被优先级较高的对象所遮盖。
图 5. 子图形演示
图 6. 缩放并移动对象后的演示
图 7. 样本演示界面

结束语
使用本文提供的示例以及所包含的源码,您应该发现在需要图形对象时,创建和保留它们相当简单。另外,可以以本文中的示例为起点,在这里描述的 API 上创建您自己的变体。因为类 Drawable 和类 DrawingContext 都可以生成子类,因此添加新的可绘图对象和 java.awt.Graphics 绘图服务接口非常容易。新的对象作为 Drawable 的新子类添加。新的绘图服务和数据转换作为 DrawingContext 的子类添加。