级别: 初级 John Carr (carr.john@heb.com), 首席程序员/分析员, H.E.B. Grocery 公司
2001 年 10 月 09 日 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 继承的。
我们将在下面部分详细讨论每个类的功能。
JSci.swing 的超类是一个抽象类,被称为
JDoubleBufferedComponent 。这个类相对来说比较简单,它为自己将要建立于其上的图形提供双缓冲功能。 双缓冲指出接收组件是否应该使用缓冲区来绘画。如果双缓冲被设置为 true,那么来自这个组件的所有图画都将在屏外(offscreen)绘画缓冲区完成。屏外绘画缓冲区稍后将被复制到屏幕上。根据 javadocs,Swing 绘画系统总是使用最大的双缓冲区。如果一个组件有缓冲,而且它的其中一个父组件也有缓冲,那么就使用它的父组件的缓冲区。
JDoubleBufferedComponent 依靠自己而不是 Swing 的双缓冲实现
JComponent 处理双缓冲。这为使用
JSci.swing 包的开发者提供了比只使用 Swing 更细粒度的对双缓冲的控制。
JImageCanvas 是另一个简单的(straightforward)类。它的目的是允许图像被直接添加到容器。
JImageCanvas 创建一个
java.awt.MediaTracker 实例来装入和跟踪图像。
contour plot是 3 个数字变量之间关系的二维图解表示。两个变量用于 x 轴和 y 轴,第 3 个变量 z 用于等高位。等高位被绘制成曲线;曲线之间区域的颜色编码表示内插值。
JGraph2D 超类提供 2D 图形的抽象封装。
JScatterGraph 和
JLineGraph 都继承了
JGraph2D 。
散点图是一种统计图,被绘制用来比较两个数据集。可用它来发现两个数据集之间的相关性。
折线图显示两段信息之间的关系,以及它们是如何依赖另一方发生变化的。 沿着折线图一条边的数字被称为
刻度。在后面我将更详细地讨论折线图以及如何构造一个折线图。
JCategoryGraph2D 超类提供 2D 图形分类的抽象封装。
JBarGraph 和
JPieChart 都继承了
JCategoryGraph2D 。一个
条形图由一个坐标轴和一系列带标签的水平或垂直条组成,它们显示每个条的不同值。
饼形图是一个被分成若干部分的圆形图,每部分显示一些相关信息片段的大小。饼形图被用于显示组成整体的各部分的大小。我们将在后面讨论关于构造饼形图和条形图的更多内容。
如果您觉得自己需要挑战,那么请尝试
JLineGraph3D 。如名称所暗示的那样,
JLineGraph3D 允许您向折线图中添加第三维。
JLineGraph3D 不单是让您按照 x(水平)和 y(垂直)来考虑,而是让您把 z(深度)也考虑在内。
JLineTrace 允许您使用鼠标侦听器跟踪 2D 折线图。
构造一个 JPieChart
要构造任何形状的图形,您都必须理解如何用一种有意义的方式来组织数据。不管是构造条形图还是构造饼形图,当涉及到数据建模时,其过程都是相同的。图 2 提供一个数据示例,它可用于创建条形图或饼形图。
图 2. 用于创建条形图或饼形图的示例数据
一个
series 表示一个数字数组。对于
JPieChart ,series 可以是
float 类型或
double 类型。一个
category 表示一个 series 内的数据列标识符。对于
JPieChart ,category 用
String 形式表示。当然,您希望有一个对您的观众来说有些意义的 category,这就是为什么一个城市名看起来好象适合
PieGraph.java 中饼形图示例中的每个 category 的原因所在。为简单起见,饼形图将只用一个数据 series。清单 1 分别包含 category 和 series 类变量。
清单 1. 为饼形图定义 x 轴和数据 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 颜色。
清单 3. 设置饼形图扇形块颜色
// ...
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 接口。
图 4. 创建一个定制 category 图模型
表 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。
在
BarGraph.java 中
SalesGraphModel 继承
AbstractGraphModel 并实现
CategoryGraph2DModel 。为简单起见,将
SalesGraphModel 配置为只处理一个数据 series。如果您查看
SalesGraphModel 中的
nextSeries() 方法,您会注意到它只返回 false;这表明这个模型内只包含一个数据 series。传给
SalesGraphModel 构造器的输入参数包括 category 的名称和一个数据 series。创建
SalesGraphModel 和
JBarGraph 实例的代码可在清单 4 的
getBarGraph() 方法中找到。
清单 4. 创建一个 JBarGraph
// ...
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 总数的过程。
清单 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 的模型与
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 是一个类图表, 显示创建一个带标签的折线图涉及的类。
图 6. 带标签折线图的类图表
创建
DemandGraphModel 和
JLineGraph 类的实例代码可在
getLineGraph() 方法中找到,清单 6 中显示了这些代码。
清单 6. 创建一个 JLineGraph 实例
// ...
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 图形时可以考虑使用它。
参考资料
关于作者  | 
|  |
John Carr 是一个“经过 Sun 认证的 Java 程序员”,目前在 H.E.B Grocery 公司(位于 San Antonio,Texas)工作,是一位客户端和服务器端 Java 应用开发者。John 还是 San Antonio 学院的一位兼职教员,目前教“高级面向对象编程”。可通过
carr.john@heb.com 与 John 联系。
|
对本文的评价
|