John Carr(carr.john@heb.com)
首席程序员/分析员,H.E.B. Grocery 公司
2001 年 10 月
Java 2D 可能是在 Java 程序中编写 2D 图形程序的最显著的解决方案,但它不是唯一的一个。在本文中,Java 开发者 John Carr 提出了一种优秀的备用方案 — “Java 科学对象”(Java Objects for Science(JSci)),一个开放源代码的包,它使您能够在 Swing 中创建 2D 条形图、饼形图和折线图。对于大多数 Java 开发者,任何类型的图形开发在本质上都与 Java 2D 和 3D API 以及
java.awt.Graphics
有紧密联系。虽然 Java 2D 和 3D API 为在 Swing 中创建图形提供优秀的工具,但您并非只可以任意使用它们,当然它们也不是最容易学的。对于那些没有时间、需要或有兴趣熬夜深刻了解java.awt.Graphics
的人,我向您推荐一个开放源代码的备用方案:JSci。Java 科学对象(JSci)开放源代码项目是 Durham(英国 Durham)大学粒子理论中心的三年级研究生 Mark Hale 创立的。JSci 是一个包集合,包含数学和科学类。 在撰写本文时,JSci 的版本是 .87,运行在 Java 1.1.8、1.2.x 或 1.3.x 上,但将来可能为 Java 1.4 写更新版本的 JSci。这个项目的目的是以可能有助于基于科学的软件开发的最自然方式封装科学方法和原则。支持 JSci 的设计哲学是基于这样一种思想 — “直接从黑板到代码”。也就是,数学概念和构造应该以某种方式封装在代码中。在某种程度上,JSci 作为对象设计实验与作为数学库差不多。
使用 JSci,您既可以在 AWT 中也可以在 Swing 中创建简单的条形图、折线图和饼形图。
JSci.swing.JBarGraph
、JSci.swing.JPieChart
和JSci.swing.JLineGraph
API 组件设计得也很好,这些组件和 AWT 绘图类都遵守 MVC 体系结构。在本文中,我将介绍
JSci.swing
包并向您展示如何使用它的类和方法创建条形图、饼形图和折线图。我们将首先看一下组成包的核心的类。JSci.swing 包
用于在 Swing 中创建图形的类位于JSci.swing
包中。图 1 显示了JSci.swing
的类图表。JSci.swing
中的类,除JImageCanvas
之外,都从JDoubleBufferedComponent
继承。注意:JDoubleBufferedComponent
和JImageCanvas
都是从javax.swing.JComponent
继承的。我们将在下面部分详细讨论每个类的功能。
JDoubleBufferedComponent
JSci.swing
的超类是一个抽象类,被称为JDoubleBufferedComponent
。这个类相对来说比较简单,它为自己将要建立于其上的图形提供双缓冲功能。 双缓冲指出接收组件是否应该使用缓冲区来绘画。如果双缓冲被设置为 true,那么来自这个组件的所有图画都将在屏外(offscreen)绘画缓冲区完成。屏外绘画缓冲区稍后将被复制到屏幕上。根据 javadocs,Swing 绘画系统总是使用最大的双缓冲区。如果一个组件有缓冲,而且它的其中一个父组件也有缓冲,那么就使用它的父组件的缓冲区。
JDoubleBufferedComponent
依靠自己而不是 Swing 的双缓冲实现JComponent
处理双缓冲。这为使用JSci.swing
包的开发者提供了比只使用 Swing 更细粒度的对双缓冲的控制。JImageCanvas
JImageCanvas
是另一个简单的(straightforward)类。它的目的是允许图像被直接添加到容器。JImageCanvas
创建一个java.awt.MediaTracker
实例来装入和跟踪图像。JContourPlot
contour plot 是 3 个数字变量之间关系的二维图解表示。两个变量用于 x 轴和 y 轴,第 3 个变量 z 用于等高位。等高位被绘制成曲线;曲线之间区域的颜色编码表示内插值。JGraph2D
JGraph2D
超类提供 2D 图形的抽象封装。JScatterGraph
和JLineGraph
都继承了JGraph2D
。散点图是一种统计图,被绘制用来比较两个数据集。可用它来发现两个数据集之间的相关性。折线图显示两段信息之间的关系,以及它们是如何依赖另一方发生变化的。 沿着折线图一条边的数字被称为刻度。在后面我将更详细地讨论折线图以及如何构造一个折线图。JCategoryGraph2D
JCategoryGraph2D
超类提供 2D 图形分类的抽象封装。JBarGraph
和JPieChart
都继承了JCategoryGraph2D
。一个条形图由一个坐标轴和一系列带标签的水平或垂直条组成,它们显示每个条的不同值。饼形图是一个被分成若干部分的圆形图,每部分显示一些相关信息片段的大小。饼形图被用于显示组成整体的各部分的大小。我们将在后面讨论关于构造饼形图和条形图的更多内容。JLineGraph3D 和 JLineTrace
如果您觉得自己需要挑战,那么请尝试JLineGraph3D
。如名称所暗示的那样,JLineGraph3D
允许您向折线图中添加第三维。JLineGraph3D
不单是让您按照 x(水平)和 y(垂直)来考虑,而是让您把 z(深度)也考虑在内。JLineTrace
允许您使用鼠标侦听器跟踪 2D 折线图。构造一个 JPieChart
要构造任何形状的图形,您都必须理解如何用一种有意义的方式来组织数据。不管是构造条形图还是构造饼形图,当涉及到数据建模时,其过程都是相同的。图 2 提供一个数据示例,它可用于创建条形图或饼形图。一个 series 表示一个数字数组。对于
JPieChart
,series 可以是float
类型或double
类型。一个 category 表示一个 series 内的数据列标识符。对于JPieChart
,category 用String
形式表示。当然,您希望有一个对您的观众来说有些意义的 category,这就是为什么一个城市名看起来好象适合PieGraph.java
中饼形图示例中的每个 category 的原因所在。为简单起见,饼形图将只用一个数据 series。清单 1 分别包含 category 和 series 类变量。
// ... public class PieGraph extends JFrame implements ItemListener { // ... private static final String CATEGORY_NAMES[] = { "London", "Paris", "New York" }; private static final float CONSUMERS_SERIES[] = { 45.3f, 27.1f, 55.5f }; // ... }
现在,已经定义了
CONSUMERS_NAMES
和SALES_SERIES
数组,我们就可以开始构造饼形图。这个示例中使用的模型是DefaultCategoryGraph2DModel
。在下一部分,我将更深入地讨论该模型的工作机制。清单 2 显示如何创建一个DefaultCategoryGraph2DModel
实例。清单 2. 创建一个 DefaultCategoryGraph2DModel 实例
// ... private JPieChart getPieGraph() { // ... DefaultCategoryGraph2DModel model = new DefaultCategoryGraph2DModel(); model.setCategories(CATEGORY_NAMES); model.addSeries(CONSUMERS_SERIES); // ... } // ...
在实例化
DefaultCategoryGraph2DModel
之后,category 和 series 一定包含在这个模型中。通过setCategories(String categoryNames[])
方法设置一个 category。因为这个模型允许多个 series,所以方法addSeries(float series[])
或addSeries(double series[])
可用于这个目的。DefaultCategoryGraph2DModel
将每个增加的 series 存储在Vector
中。这个 model 实例现在可用作
JPieChart
的构造器的一个输入参数。 在创建了JPieChart
实例后,可用setColor(int category, java.awt.Color c)
方法定义每个 category 的颜色。 清单 3 显示了如何创建JPieChart
实例和设置 category 颜色。
// ... public class PieGraph extends JFrame implements ItemListener { // ... private static final int LONDON = 0; private static final int PARIS = 1; private static final int NEW_YORK = 2; // ... private JPieChart getPieGraph() { // ... JPieChart pieChart = new JPieChart(model); pieChart.setColor(LONDON,Color.blue); pieChart.setColor(PARIS,Color.yellow); pieChart.setColor(NEW_YORK,Color.orange); // ... } // ...
图 3 用图解形式说明了使用
PieGraph.java
创建的饼形图。当用户请求更改 category 的颜色(通过应用程序右边的列表框)时,方法JPieChart.setColor(int category, java.awt.Color c)
被调用来实现这种更改。要使这种更改可见,在设置了新的扇形块颜色后调用JPieChart.redraw()
方法。图 3. PieGraph.java 生成的饼形图的输出示例
![]()
我们只讨论了
DefaultCategoryGraph2DModel
用途的一些皮毛。其它还有许多有用的方法,包括允许您隐藏 series 的方法(setSeriesVisible(int series, boolean visible)
)或更改 series 数据的方法(changeSeries(int series, float newSeries[])
)。 我鼓励您在有空的时候更深入地研究这些类(请参阅参考资料)。构造一个 JBarGraph
构造一个条形图首先必须象在构造饼形图中那样为数据建模。条形图,和饼形图一样,有一个数据 series 数组(或多个)和一个 category 数组。我不使用DefaultCategoryGraph2DModel
作为条形图的模型,而是将说明如何为您的数据创建一个定制模型,以及JBarGraph
怎样在运行时期间使用这个模型生成条形图。为使这个练习更加有趣,我将向您展示如何动态地更改 series 元素的值和重画条形图。如图 4 所示,一个定制 category 图模型必须继承
AbstractGraphModel
并实现CategoryGraph2DModel
(参见SalesGraphModel.java
)。注意:这部分涉及的方法继承自CategoryGraph2DModel
接口。表 1 展示了
CategoryGraph2DModel
接口的方法以及JBarGraph
如何在运行时利用模型。表 1. CategoryGraph2DModel 接口
方法 描述 public abstract void addGraphDataListener(GraphDataListener)
添加一个侦听器 public abstract void firstSeries()
选择第 1 个数据 series public abstract String getCategory(int i)
返回第 i 个 category public abstract float getValue(int i)
返回第 i 个 category 的值 public abstract boolean nextSeries()
选择下一个数据 series public abstract void removeGraphDataListener(GraphDataListener)
除去一个侦听器 public abstract int seriesLength()
返回当前 series 的长度 让我们看一下这些方法的较为详细一点的信息。
在运行时,
JBarGraph
调用public void firstSeries()
,它选择第 1 个数据 series。如果您正在处理多个数据 series,将它们存储在Vector
中是有意义的。调用的下一个方法是public int seriesLength()
;这个方法返回当前 series 的长度。seriesLength()
方法返回的整型值被用于形成一个循环构造来获取每个 category 以及那个 series 的相应整型值。在这个 series 循环中,调用public String getCategory(int i)
获取第 i 个 category 的名称,并调用public float getValue(int i)
获取第 i 个 category 的值。(术语第 i 个是指 category 的顺序,比如第 1、第 2、第 3 等等。)为当前 series 收集了全部数据元素后,调用public boolean nextSeries()
方法选择下一个数据 series。如果nextSeries()
方法返回 true,那么为新数据 series 收集数据元素的过程将重新开始 — 从seriesLength()
方法开始。当nextSeries()
返回 false 时,模型中不再有数据 series。在
清单 4. 创建一个 JBarGraphBarGraph.java
中SalesGraphModel
继承AbstractGraphModel
并实现CategoryGraph2DModel
。为简单起见,将SalesGraphModel
配置为只处理一个数据 series。如果您查看SalesGraphModel
中的nextSeries()
方法,您会注意到它只返回 false;这表明这个模型内只包含一个数据 series。传给SalesGraphModel
构造器的输入参数包括 category 的名称和一个数据 series。创建SalesGraphModel
和JBarGraph
实例的代码可在清单 4 的getBarGraph()
方法中找到。
// ... public class BarGraph extends JFrame implements ActionListener { // ... private static final String CATEGORY_NAMES[] = { "London", "Paris", "New York" }; // ... private static final float SALES_SERIES[] = { 24.1f, 14.4f, 36.8f }; // ... private JBarGraph getBarGraph() { SalesGraphModel model = new SalesGraphModel(CATEGORY_NAMES, SALES_SERIES); barGraph = new JBarGraph(model); return barGraph; } // ...
图 5 显示
BarGraph.java
和SalesGraphModel.java
产生的输出。您或许已注意到,所有的 category 条颜色都一样,这与饼形图示例相反,在饼形图示例中,每个 category 的颜色都不同。这是因为JBarGraph
只允许您改变数据 series 的颜色表示,而不是 series 内的 category。图 5. BarGraph.java 生成的条形图的输出示例
![]()
SalesGraphModel
具有允许在数据 series 内增加 category 值的功能。当用户从BarGraph
示例选择了一个 category(通过单选按钮)并按下“Add 1 to total”按钮时,public void incrementCategoryTotal(int i)
方法被调用。这个方法把选中要增加的 category 作为方法参数传递,传递后会有一个通知被发送到JBarGraph
实例(通过public void dataChanged(GraphDataEvent)
方法):模型中的数据已经发生改变,必须重画此条形图。清单 5 显示了增加 category 总数的过程。
// ... public class BarGraph extends JFrame implements ActionListener { private void addToCategoryTotal(int category) { SalesGraphModel model = (SalesGraphModel)barGraph.getModel(); model.incrementCategoryTotal(category); } // ... } // ... public class SalesGraphModel extends AbstractGraphModel implements CategoryGraph2DModel { // ... private float seriesTotals[]; // ... public void incrementCategoryTotal(int i) { seriesTotals[i] ; } // ... }
构造一个 JLineGraph
JLineGraph
的模型与JPieChart
或JBarGraph
的模型相似;这两个模型都有多个数据 series 和一个标识 series 内数据的 category。但JLineGraph
的模型引入了 x 轴和 y 轴坐标系的复杂性,在这一部分,我将研究如何创建一个定制的模型,并展示JLineGraph
是如何在运行时期间使用这个模型生成一个折线图的。注意:这一部分涉及的方法继承自Graph2DModel
接口。一个定制的折线图模型必须要从
AbstractGraphModel
继承并实现Graph2DModel
接口,如表 2 中所示。表 2. Graph2DModel 接口
方法 描述 public abstract void addGraphDataListener(GraphDataListener)
添加一个侦听器 public abstract void firstSeries()
选择第 1 个数据 series public abstract float getXCoord(int i)
返回第 i 个 category public abstract float getYCoord(int i)
返回第 i 个 category 的值 public abstract boolean nextSeries()
选择下一个数据 series public abstract void removeGraphDataListener(GraphDataListener)
除去一个侦听器 public abstract int seriesLength()
返回当前 series 的长度 这些方法的执行与表 1 中所示的相似。
在运行时期间,
JLineGraph
调用public void firstSeries()
方法;这选择第 1 个数据 series。调用的下一个方法是public int seriesLength()
;它返回当前 series 的长度。seriesLength()
方法返回的整型值被用于形成一个循环构造以获取那个 series 的 x 轴和 y 轴坐标值。在这个 series 循环中,调用public float getXCoord(int i)
获取第 i 个 category,并调用public float getYCoord(int i)
获取第 i 个 category 的值。为当前 series 收集了全部数据元素后,调用public boolean nextSeries()
方法选择下一个数据 series。如果nextSeries()
方法返回 true,那么为新数据 series 收集数据元素的过程将重新开始 — 从seriesLength()
方法开始。如果nextSeries()
返回 false,那是因为模型中不再有数据 series。缺省情况下,
JLineGraph
只接受 x 轴和 y 轴的浮点值。在我的折线图中,x 轴应该包含代表一年中前六月(一月到六月)的String
。要完成这个任务,必须发生两件事:第一,必须继承Graph2DModel
接口以包含public String getXLabel(float i)
方法。这样您就能够获得 x 轴的String
表示。第二,必须继承JLineGraph
,并覆盖drawLabeledAxes(Graphics g)
方法。这一步将允许您使用新的接口访问getXLabel(float f)
方法。图 6 是一个类图表, 显示创建一个带标签的折线图涉及的类。创建
DemandGraphModel
和JLineGraph
类的实例代码可在getLineGraph()
方法中找到,清单 6 中显示了这些代码。
// ... public class LineGraph extends JFrame { private JLineGraph lineGraph; private static final String MONTH_NAMES[] = { "Jan", "Feb", //... }; private static final int MONTH_NUMBERING[] = {0, 1, 2, 3, 4, 5 }; private static final int LONDON_SERIES = 0; private static final int PARIS_SERIES = 1; private static final int NEW_YORK_SERIES= 2; private static final float LONDON_DEMAND[] = { 1.2f, 3.7f, 6.7f, // ... }; private static final float PARIS_DEMAND[] = { 12.6f, 15.0f, 13.7f, // ... }; private static final float NEW_YORK_DEMAND[] = { 15.0f, 13.9f, 10.1f, // ... }; // ... private JLineGraph getLineGraph() { DemandGraphModel model = new DemandGraphModel(); model.addSeries(LONDON_DEMAND); model.addSeries(PARIS_DEMAND); model.addSeries(NEW_YORK_DEMAND); model.setXAxisLabel(MONTH_NAMES); model.setXAxis(MONTH_NUMBERING); lineGraph = new LabeledLineGraph(model); lineGraph.setColor(LONDON_SERIES, Color.red); lineGraph.setColor(PARIS_SERIES, Color.yellow); lineGraph.setColor(NEW_YORK_SERIES, Color.blue); lineGraph.setXIncrement(1); return lineGraph; } // ... }
清单 6 的最后一行显示了对
LabeledLineGraph setXIncrement(int i)
方法的一次调用。完成这次调用是为了确保 x 轴是按照严格的增量画的。换句话说,JLabeledLineGraph
将用最大化应用程序窗口或浮动窗口的相同坐标来标记 x 轴上的每一“格”。图 7 显示了LineGraph.java
产生的输出示例。调整应用程序窗口的大小,看看折线图是如何被重画以适合窗口新尺寸的。图 7. LineGraph.java 生成的折线图的输出示例
![]()
结论
JSci 是一个开放源代码成果,主要用于科学应用。在本文中,我已经介绍了作为创建 2D 图形工具的 JSci 的一些可能用途。JSci 是专业绘图包的备用的免费软件。虽然 JSci 并不提供专业画图包所带的内建类型支持,您却的确可以访问源代码,并且可以自由修改代码,然后将它们提交回 JSci 社区。JSci 是一个不断发展的成果,我认为它是一个很好的备用方案,您在用 Java 创建 2D 图形时可以考虑使用它。
- 下载“Java 科学 API 对象”的最新版本并在 JSci 主页上学习关于项目的更多知识。
- 下载本文中使用的源代码。
- “Swing 模型过滤器”(developerWorks,2001 年 2 月)是一篇介绍 Swing 功能的优秀文章。
- John Zukowski 在“Merlin 的魔力:Porter-Duff 决定一切!”(developerWorks,2001 年 9 月)中讨论了 Java 2D 最近增加的功能。
- IBM alphaWorks 提供了 Java 图形基础类(Graph Foundation Classes(GFC)),一个支持用 Java 语言进行图形编程的框架。
- IBM 研究组支持众多与图形和可视化编程相关的项目。
关于作者
John Carr 是一个“经过 Sun 认证的 Java 程序员”,目前在 H.E.B Grocery 公司(位于 San Antonio,Texas)工作,是一位客户端和服务器端 Java 应用开发者。John 还是 San Antonio 学院的一位兼职教员,目前教“高级面向对象编程”。可通过 carr.john@heb.com 与 John 联系。