java web动态图表编程

446
本书向读者展示如何使用 Java Applet、Java Servlet、Java Server Pages(JSP)、JavaBean 以及开放源代码的 Web 图表生成引擎——JFreeChart 和 Cewolf 来开发奇妙的 Web 动态图表 应用程序——以一种跨平台、小巧、结构清晰的模式在 Web 上生成动态图表。 随着计算机网络及编程技术的发展,使得越来越多的应用程序被移植成以 Web 应用程序 (浏览器/服务器)的方式,工作在因特网/局域网环境之中。网络开发人员发现,某些传统 IDE 开发环境,例如:在 Dephi 和 VB 中可以轻而易举地实现的图形界面,而在 Web 程序中却 很难实现,其中一个典型的例子就是 Web 图表(Chart)的生成与处理。即使 Web 程序(ASP、 ASP.NET 等)能够处理一些图表,也基本是采用 ActiveX 或者 COM 组件的结构。这种方法有 三大弊端: ? 开发 ASP 比较简单,但开发 ActiveX 和 COM 却很难; ? 基于 ActiveX 和 COM 构架的 Web 应用程序只能用于微软 Windows 的 Web 服务器 IIS 环境下, 移植性差; ? 由于 ActiveX 和 COM 与 Windows 深度紧密结合,在带来强大功能的同时,也一并带来了 Windows 固有的缺陷——安全性和稳定性差。 PHP 可以用于 Web 图表处理,它具有跨平台、安全性能高、兼容性及扩展性强的优点,但也 有其固有的缺陷,具体表现为以下几点: ? 效率不高,基于解释型,而非编译型,这一点与 ASP 类似。ASP.NET 吸取了 JSP 的优点, 属于编译型,大大提高了运行速度和效率; ? 安装复杂,每一种扩展模块不完全是由 PHP 自身来完成。当图形需要 GD 库,使用类似的扩 展库时,安装和调试是 PHP 的一大问题; ? 因为是开发源代码产品,所以缺乏企业级的商业支持。 JSP 的出现,使得上述弊端不复存在。随着 Java 对二维图形及三维图像的处理能力越来 越强大,利用 JSP 来简单、高效地开发一个 Web 图表应用程序已经不是一件难事了。为了展 示如何编写 Web 图表应用程序,本书中,我们不仅提供了多个 JSP 图表生成的实例,而且还 将从以下两个方面加以说明 Web 图表编程是如何实现的。 1.利用 Java 自身对图形的处理能力,由开发者编写代码来生成 Web 动态图表。将以大量的 例程,不同的方式(例如:Java Applet、Java Servlet、JSP、JavaBean),从不同的角度来 展示如何编写 Web 图表的 Java 程序。 2. 借助第三方的图表生成引擎来完成图表, 主要利用一些 Java 开放源代码组织开发的作品, 例如: ? 由 www.jfree.org 推出的 JFreeChart; ? 由 cewolf.sourceforge.net 推出的 Cewolf。 Cewolf 是基于 JFreeChart 的二次开发。 实质上, 是基于 JFreeChart 的应用。 JFreeChart 没有免费提供开发文档,只有英文版的相关资料,因此 JFreeChart 和 Cewolf 在国内的应用 受到了一定的限制。 根据我们使用及研究 JFreeChart 和 Cewolf 的经验,将在本书中提供大量的例程和详尽 的讲解,帮助读者轻松地掌握上述两种非常优秀的、基于 Java 的 Web 图表生成引擎。相信阅 读本书,并加以必要的练习,读者将会发现,基于 JSP 的 Web 图表编程是可以轻松完成的。 本书结构及内容 本书例程丰富、代码简洁、结构清晰、讲解准确、图文并茂。共分九章,各章具体内容 如下: 第 1 章 Java 概述 本章主要概述 Java 历史及其发展、Java 开发环境的建立,以及编写一个简单的 Java 应 用程序。

Upload: yiditushe

Post on 15-May-2015

3.799 views

Category:

Documents


5 download

TRANSCRIPT

Page 1: Java Web动态图表编程

本书向读者展示如何使用 Java Applet、Java Servlet、Java Server Pages(JSP)、JavaBean

以及开放源代码的 Web 图表生成引擎——JFreeChart 和 Cewolf 来开发奇妙的 Web 动态图表

应用程序——以一种跨平台、小巧、结构清晰的模式在 Web 上生成动态图表。

随着计算机网络及编程技术的发展,使得越来越多的应用程序被移植成以 Web应用程序

(浏览器/服务器)的方式,工作在因特网/局域网环境之中。网络开发人员发现,某些传统

IDE 开发环境,例如:在 Dephi 和 VB中可以轻而易举地实现的图形界面,而在 Web 程序中却

很难实现,其中一个典型的例子就是 Web 图表(Chart)的生成与处理。即使 Web 程序(ASP、

ASP.NET 等)能够处理一些图表,也基本是采用 ActiveX 或者 COM 组件的结构。这种方法有

三大弊端:

? 开发 ASP比较简单,但开发 ActiveX 和 COM 却很难;

? 基于ActiveX和 COM构架的Web应用程序只能用于微软Windows的 Web服务器IIS环境下,

移植性差;

? 由于 ActiveX 和 COM 与 Windows 深度紧密结合,在带来强大功能的同时,也一并带来了

Windows 固有的缺陷——安全性和稳定性差。

PHP 可以用于 Web 图表处理,它具有跨平台、安全性能高、兼容性及扩展性强的优点,但也

有其固有的缺陷,具体表现为以下几点:

? 效率不高,基于解释型,而非编译型,这一点与 ASP 类似。ASP.NET 吸取了 JSP 的优点,

属于编译型,大大提高了运行速度和效率;

? 安装复杂,每一种扩展模块不完全是由 PHP自身来完成。当图形需要 GD 库,使用类似的扩

展库时,安装和调试是 PHP 的一大问题;

? 因为是开发源代码产品,所以缺乏企业级的商业支持。

JSP 的出现,使得上述弊端不复存在。随着 Java对二维图形及三维图像的处理能力越来

越强大,利用 JSP 来简单、高效地开发一个 Web图表应用程序已经不是一件难事了。为了展

示如何编写 Web 图表应用程序,本书中,我们不仅提供了多个 JSP 图表生成的实例,而且还

将从以下两个方面加以说明 Web 图表编程是如何实现的。

1.利用 Java 自身对图形的处理能力,由开发者编写代码来生成 Web 动态图表。将以大量的

例程,不同的方式(例如:Java Applet、Java Servlet、JSP、JavaBean),从不同的角度来

展示如何编写 Web 图表的 Java 程序。

2. 借助第三方的图表生成引擎来完成图表, 主要利用一些 Java 开放源代码组织开发的作品,

例如:

? 由 www.jfree.org 推出的 JFreeChart;

? 由 cewolf.sourceforge.net 推出的 Cewolf。

Cewolf 是基于 JFreeChart 的二次开发。 实质上, 是基于 JFreeChart 的应用。 JFreeChart

没有免费提供开发文档,只有英文版的相关资料,因此 JFreeChart 和 Cewolf 在国内的应用

受到了一定的限制。

根据我们使用及研究 JFreeChart 和 Cewolf 的经验,将在本书中提供大量的例程和详尽

的讲解,帮助读者轻松地掌握上述两种非常优秀的、基于 Java 的 Web 图表生成引擎。相信阅

读本书,并加以必要的练习,读者将会发现,基于 JSP 的 Web 图表编程是可以轻松完成的。

本书结构及内容

本书例程丰富、代码简洁、结构清晰、讲解准确、图文并茂。共分九章,各章具体内容

如下:

第 1 章 Java 概述

本章主要概述 Java 历史及其发展、Java 开发环境的建立,以及编写一个简单的 Java 应

用程序。

Page 2: Java Web动态图表编程

第 2 章 Java Applet 与绘图基础

本章简要介绍 Applet 绘图基础,例如:图形环境、图形对象、基本的字体及颜色控制,

以及如何使用 Applet 绘制一些基本的几何图形。通过本章的学习,读者将掌握基本的 Java

图像编程方法。

第 3 章 Java Applet 图表绘制实例

本章综合利用绘制基本几何图形的方法来开发以下常见的图表:

? ·垂直柱状图

? ·饼图

探讨如何以图表的方式生成以下内容:

? ·在 Web动态图表中加载外部图像文件

? ·支票

? ·如何从 HTML 文档中获取参数生成动态图表

通过本章的学习,读者将会理解任何复杂的图表都可以分解成一些基本的几何图形;通

过排列组合不同的外部图像文件,以及基本的几何图形,就可以生成复杂的图表。

第 4 章 JSP/Servlet 运行环境的搭建

本章主要介绍 JSP Web 服务器的安装。在运行 JSP/Servlet 之前,首先需要安装 JSP/

Servlet 的运行环境,它就是我们平常所说的,支持 JSP/Servlet 的 Web服务器(容器)。

目前有很多支持 JSP/Servlet 的 Web 服务器,我们先介绍两种免费的、高性能的、适合

中小企业 JSP/Servlet 的 Web 服务器——Tomcat 和 Resin,并将详细阐述 Tomcat 实现

JSP/Servlet 的运行机制。此外,介绍一种企业级的 J2EE 服务器——Weblogic,学习如何在

这些服务器中部署及发布基于 JSP/Servlet 的 Web 图表应用程序。

第 5 章 基于 Servlet 的 Web图表编程

本章讲述 Web 请求/响应机制(get 和 post)、如何部署 Servlet、如何利用 Servlet 生

成 Web 动态图表, 并且提供了模拟网站投票统计、 生成登录验证码、 普通/3D甘特图等 Servlet

实例。

第 6 章 JSP Web 图表编程基础

本章讲述 JSP 绘图基础。 在 JSP 的环境下, 如何使用 Java.awt.Graphics 类的各种方法,

包括绘制直线、文本(字符串)、矩形、椭圆和圆、圆弧、多边形、折线,以及如何加载外部

图像文件,等等。

在绘制基本几何图形的基础上,我们将以圆柱体和立方体为例,展示如何通过绘制多个多边

形并将其组合成复杂几何图形的方法;利用 Java.imageio 包中 ImageIO 类的支持,调用

ImageIO 来执行诸如加载图像,以及对图像进行编码输出等工作。

第 7 章 JSP与 Java2D Web 图表编程

本章阐述如何利用 Java2D API 对于高级二维图形的处理能力,例如:笔划属性的设 定、

图形填充、渐变色、图像的透明度、字体处理、图形边缘的反锯齿、图形对象的转换以及变

形,等等,利用 B/S应用程序中生成 Web 图表的方法,并以二维及三维图表的形式,包括折

线图、水平直方图、垂直柱状图、堆栈图、饼图,以及二次曲线来进行讲解和说明。

提供了一个绘制复杂图表的例程——股市大盘指数走势图。通过本章的学习,读者可以

不借助任何第三方的程序,编写生成各种风格、复杂的 Web 图表应用程序。

第 8 章 开放源代码与 Web 图表编程

本章详细讲述了 JFreeChart 和 Cewolf 这两个 Web 图表生成引擎的安装、配置及使用方

法。提供了在 JFreeChart 和 Cewolf 的环境下生成以下类型图表的完整例程及讲解:

? 普通/3D 水平及垂直直方图、普通/3D饼图、普通/3D 堆栈图

? 线段(曲线)图、区域图、时序图、蜡烛图、移动平均线图

Page 3: Java Web动态图表编程

? 罗盘图、温度计图、速度表图、信号图

? 甘特图、多轴图表、组合图表

此外,还包括如何在 Cewolf 的环境中,创建自定义绘制属性的图表。

第 9 章 Web图表生成引擎的设计思路与实现

本章讨论如何开发一个图表生成引擎; 如何设计图表生成引擎 (封装成 JavaBean 的形式)

以达到高效、重复使用的目的。图表生成引擎的设计难点是什么?该如何处理?如何优化引

擎?我们将提供具体的实例加以讲解。通过本章的学习,读者将会感受到,图表引擎的设计

原来可以这么轻松地实现!

本书的适用对象

本书适合从事 Java 及 Web编程的开发人员, 在 JSP/Servlet 应用程序中需要提供图表显

示及处理功能的 JSP/Servlet 程序开发者,以及编程爱好者阅读。对于初学入门的应用开发

人员,可以作为自学的实践指导书;对于已经从事 Web 编程工作的人员,可以作为一本供随

时翻阅查询的案头参考书。

建议

我们建议读者在学习期间避免使用可视化的开发工具, 例如: JBuilder、 JCreator、 KAWA、

Visual Cafe、Eclipse、IntelliJ IDEA、BEA WebLogic Workshop、Oracle JDeveloper 等,

所有工作都可以使用 JSP 服务器(Tomcat/Resin)+ WWW 浏览器(IE/Mozilla/Firefox)+文

本编辑器(EditPlus)来完成。附录中,我们向读者介绍了一种优秀的 Java IDE 开发工具—

—Gel。利用 Gel,可以方便地创建、编辑、编译及运行 Java/JSP/Servlet 应用程序。

为了达到最佳的学习效果,我们建议读者在阅读本书的同时,亲自动手按照本书的示例

去操作。 所有的源程序都可以在 www.broadview.com.cn 网站上下载, 但建议读者最好对照源

程序自己动手录入一次,这样可以加深理解和记忆。

因本书的例程非常丰富,为节省篇幅,故基本上不列出每个例程的完整清单。本书详尽

地讲解了每个源程序的设计思路、 运行机制及相对应的核心代码段。 建议读者在阅读本书时,

首先用 EditPlus 之类的文本编辑器来打开相应例程的源程序, 其次阅读其完整的源程序, 最

后再阅读本书的讲解,相信这样会让读者取得事半功倍的效果。

致谢

本书由钟京馗(City University of New York)执笔主编和最后定稿。唐桓(University

of Bridgeport)参与本书例程的编写与调试工作。

本书出版之际,特别鸣谢钟志祥、李美行、朱晓蕾、朱正才、胡元妹、黄桂玉、武冠军、唐

光富、彭燕给予的大力支持与协助。

在编写本书的过程中,我们尽力保持内容的科学性、先进性和实用性,同时力求做到讲

解原理,深入浅出,语言通俗易懂。但鉴于作者学识有限,不足之处在所难免,见仁见智,

恳请广大读者指正。我们将在本书的再版中使其更臻完善。

 钟京馗

2005 年 9 月于纽约

Page 4: Java Web动态图表编程

第 1章 Java概述 1

1.1 Java简介 1

1.1.1 Java发展简史 1

1.1.2 Java的体系 2

1.1.3 Java的优点 3

1.2 Java开发环境的搭建 4

1.2.1 Java运行环境的要求 4

1.2.2 Java的安装和配置 6

1.3 Java/JSP开发工具 8

1.3.1 EditPlus简介 9

1.3.2 UltraEdit简介 11

1.3.3 其他 Java/JSP开发工具 13

1.4 第一个 Java程序 15

1.5 本章小结 17

第 2章 Java Applet与绘图基础 18

2.1 Java Applet概述 18

2.2 Java Applet工作流程 20

2.2.1 组件与容器 20

2.2.2 Applet的编译与执行 22

2.2.3 包含 Applet的 HTML 23

2.3 绘制文本(字符串) 23

2.4 绘制线段 29

2.5 色彩管理 31

2.5.1 色彩基础 31

2.5.2 Java的色彩管理 34

2.6 字体控制 36

2.7 绘制矩形 40

2.7.1 绘制普通矩形 40

2.7.2 绘制 3D矩形 43

2.8 绘制椭圆 46

2.8.1 绘制普通(空心)椭圆 46

2.8.2 绘制实心椭圆 48

2.8.3 绘制 3D椭圆 49

2.8.4 绘制圆柱体 50

2.9 绘制圆弧 54

2.9.1 绘制普通(空心)圆弧 55

2.9.2 绘制实心圆弧 56

2.9.3 绘制 3D圆弧 57

2.10 绘制多边形 59

2.10.1 绘制空心多边形 59

2.10.2 绘制实心多边形 61

2.10.3 绘制折线 61

2.10.4 绘制三角形(箭头) 62

2.10.5 绘制平行四边形及立方体 69

Page 5: Java Web动态图表编程

2.11 图像的载入与显示 78

2.11.1 在 Applet中加载和

2.11.1 显示图像 78

2.11.2 使用 MediaTracker加载并

2.11.2 显示图像 84

2.11.3 使用双缓冲技术绘制图像 89

2.12 本章小结 93

第 3章 Java Applet图表绘制实例 94

3.1 Java Applet生成Web

3.1 动态图表 94

3.1.1 垂直柱状图 95

3.1.2 饼图 102

3.2 Java Applet生成单据 109

3.2.1 带徽标的Web动态图表 110

3.2.2 支票的生成 111

3.3 从 HTML文档获取参数

3.3 生成动态图表 123

3.3.1 传递参数的 HTML文档 123

3.3.2 获取参数并生成图表 124

3.4 本章小结 126

第 4章 JSP/Servlet运行环境的搭建 128

4.1 Tomcat的安装和配置 128

4.1.1 Tomcat的安装 129

4.1.2 测试第一个 JSP程序 132

4.1.3 配置 Tomcat 132

4.1.4 转换后的 JSP页面文件 145

4.2 Resin的安装和配置 147

4.2.1 Resin的安装 147

4.2.2 Resin的配置 149

4.3 BEA Weblogic的安装和配置 150

4.3.1 BEA Weblogic的安装 150

4.3.2 BEA Weblogic的配置 152

4.3.3 测试 BEA Weblogic

4.3.3 的配置 155

4.3.4 部署第一个Web

4.3.4 应用程序 156

4.4 本章小结 157

第 5章 基于 Servlet的Web图表编程 158

5.1 Servlet简介及其构架 158

5.1.1 Servlet的特点 158

5.1.2 Servlet的接口 159

5.1.3 HttpServlet类简介 160

5.1.4 HttpServletRequest接口 161

5.1.5 HttpServletResponse接口 162

Page 6: Java Web动态图表编程

5.2 Servlet处理 HTTP get请求 163

5.3 Servlet处理包含数据的 HTTP

5.3 get请求及解决中文乱码 177

5.4 Servlet处理 HTTP post及包含

5.4 数据的 post请求 187

5.5 Servlet生成Web投票统计图 191

5.6 Servlet生成登录验证码 198

5.6.1 Servlet生成登录验证码

5.6.1 实例 1 199

5.6.2 Servlet生成登录验证码

5.6.2 实例 2 209

5.7 Servlet高级设置 214

5.7.1 Servlet初始化参数 214

5.7.2 Servlet加载优先级 216

5.7.3 Servlet映射 217

5.8 Servlet绘制甘特图 218

5.9 Servlet绘制 3D甘特图 222

5.10 本章小结 228

第 6章 JSP Web图表编程基础 229

6.1 JSP概述 230

6.1.1 JSP运行机制 231

6.1.2 JSP的优点 233

6.2 JSP语法简介 234

6.2.1 JSP文件结构 234

6.2.2 JSP文件中的元素简介 240

6.3 JSP调用 Servlet生成

6.3 动态图表 257

6.3.1 JSP生成验证码 257

6.3.2 JSP生成甘特图 258

6.3.3 JSP其他相关知识 258

6.4 JSP生成基本动态图表 260

6.4.1 JSP绘制文本和线段 260

6.4.2 JSP与字体控制 266

6.4.3 JSP绘制矩形 273

6.4.4 JSP绘制椭圆 275

6.4.5 JSP绘制圆弧 276

6.4.6 JSP绘制多边形和折线 277

6.4.7 JSP绘制三角形 277

6.4.8 JSP绘制平行四边形和

6.4.8 立方体 280

6.4.9 JSP加载并显示图像 281

6.5 本章小结 282

第 7章 JSP与 Java2D Web图表编程 283

7.1 Java2D概述 283

Page 7: Java Web动态图表编程

7.2 Java AWT与 Java2D 285

7.3 Java2D与填充属性 287

7.3.1 设置填充属性 287

7.3.2 填充属性的用法 287

7.4 Java2D与笔划属性 291

7.4.1 线段端点的形状风格 292

7.4.2 线段转折处的形状风格 292

7.4.3 虚线风格 293

7.4.4 BasicStroke构造器 294

7.4.5 Java2D Web图表实例

7.4.5 之折线图 294

7.5 创建基本 Java2D图形 309

7.5.1 Java2D图形(Shape)

7.5.1 接口概述 309

7.5.2 Point2D 310

7.5.3 Line2D 311

7.5.4 Rectangle2D 312

7.5.5 RoundRectangle2D 314

7.5.6 Java2D Web图表实例之

7.5.6 柱状图 315

7.5.7 Ellipse2D 333

7.5.8 Arc2D 334

7.6 Java2D实例饼图类图表 337

7.6.1 普通饼图 338

7.6.2 圆圈图 342

7.6.3 3D饼图 344

7.7 图形重叠 346

7.8 alpha复合 348

7.8.1 alpha复合概述 348

7.8.2 AlphaComposite类的使用 350

7.8.3 AlphaComposite应用实例 352

7.9 图形对象的转换 357

7.9.1 图形对象转换(transformation)

7.9.2 概述 357

7.9.2 平移(translation) 359

7.9.3 旋转(rotation) 360

7.9.4 缩放(scale) 362

7.9.5 扭曲(shear) 364

7.10 图形渲染线索 366

7.11 Java2D与高级文本处理 368

7.11.1 空心文本 368

7.11.2 弯曲文本 369

7.11.3 单行长文本自动分行 371

7.12 Java2D创建复杂图形 373

Page 8: Java Web动态图表编程

7.12.1 Area 374

7.12.2 曲线 375

7.12.3 通用路径 377

7.13 Web图表实例解析 379

7.13.1 透明 3D饼图 379

7.13.2 股市指数走势图 381

7.14 本章小结 391

第 8章 开放源代码作品与Web图表编程 392

8.1 开放源代码作品简介 392

8.2 JFreeChart与 JSP图表编程 394

8.2.1 JFreeChart简介 394

8.2.2 JFreeChart的安装及其

8.2.2 核心类 395

8.2.3 JFreeChart生成直方图表 398

8.2.4 JFreeChart生成饼型图表 411

8.2.5 JFreeChart生成线段图表 416

8.2.6 JFreeChart生成区域图表 420

8.2.7 JFreeChart生成时序

8.2.7 (Time Series)图表 424

8.2.8 JFreeChart生成甘特图表 430

8.2.9 JFreeChart生成多轴

8.2.9 (Multiple Axis)图表 432

8.2.10 JFreeChart生成组合

8.2.10 (Combined Axis)图表 435

8.2.11 JFreeChart生成其他类型

8.2.11 的图表 441

8.3 JFreeChart与 Servlet

8.3 图表编程 444

8.3.1 简单的 Servlet图表编程 444

8.3.2 交互式 Servlet图表编程 446

8.4 Cewolf与 JSP图表编程 448

8.4.1 Cewolf简介 448

8.4.2 Cewolf的下载与安装 448

8.4.3 Cewolf生成直方图表 450

8.4.4 Cewolf生成基于 DefaultCategory

8.4.4 Dataset数据集的图表 454

8.4.5 Cewolf生成饼图 460

8.4.6 Cewolf生成基于 XYDataset

8.4.6 数据集的图表 462

8.4.7 Cewolf生成基于 OHLCDataset

8.4.7 数据集的图表 465

8.4.8 Cewolf生成甘特图表 466

8.4.9 Cewolf生成信号图表 467

8.4.10 Cewolf生成速度图表 468

Page 9: Java Web动态图表编程

8.4.11 Cewolf生成OverLay类型

8.4.11 的图表 468

8.4.12 Cewolf生成组合图表 470

8.4.13 生成自定义风格的

8.4.13 Cewolf图表 472

8.5 本章小结 473

第 9章 Web图表生成引擎的设计思路与实现 475

9.1 Web动态图表生成引擎的

9.1 设计思路 475

9.2 Web动态图表生成引擎的

9.2 设计模型 480

9.2.1 生成普通线段图的

9.3.2 JavaBean 480

9.2.2 生成 3D线段图的

9.3.2 JavaBean 483

9.2.3 生成普通直方图的

9.3.2 JavaBean 484

9.2.4 生成 3D直方图的

9.3.2 JavaBean 487

9.2.5 生成普通饼图的

9.3.2 JavaBean 488

9.2.6 生成 3D饼图的

9.3.2 JavaBean 490

9.3 数据分离 491

9.3.1 创建及调用 CategoryDataset

9.3.2 类数据集对象 491

9.3.2 创建及调用 PieDataset类

9.3.2 数据集对象 495

9.4 引擎的优化概述 496

9.5 本章小结 498

附录 Gel使用指南 499

Page 10: Java Web动态图表编程

第 1 章 Java 概述

Java 是美国 Sun微系统公司(Sun Microsystems, Inc.)开发,近年来飞速发展的一项崭

新的计算机技术。

1.1 Java简介

Java 既是一种程序设计语言,也是一个完整的平台。作为一种程序语言,它简洁、面向

对象、 安全、 健壮, 以及适用于 Internet 技术; 而作为一个平台 (JRE, Java Runtime Environment, Java运行环境或者说是 Java虚拟机) , 对于符合 Sun公司 Java标准的应用程序, 都可以在 Java 平台上正确运行,与程序运行的操作系统无关。

Java 为什么能够成为目前编写 Web 应用程序的首选设计语言呢?并且具备跨平台的能

力呢?我们先简单地了解一下 Java 的发展历史和体系结构。

1.1.1 Java 发展简史

Java 起初并非叫做 Java,而是叫做 Oak。早期是为了嵌入式系统而设计的一项产品。20 世纪 90 年代初期, Sun公司预测微处理器的发展将会对家用电器以及智能化电子消费设备起

到决定性的作用,于是,在 1990 年 12 月,Sun公司就由 Patrick Naughton、Mike Sheridan和 James Gosling成立了一个叫做 Green Team的小组。其主要目标就是开发一种分布式系统架

构,使其能够在智能化电子消费设备作业平台上执行。例如,PDA、手机、信息家电(IA, Internet/Information Appliance)等。一开始,准备采用 C++,但 C++太过复杂,安全性差。

最后,基于 C++开发出一种新的语言 Oak,Oak是一种用于网络、精巧而安全的语言。 1992 年,Green Team发表了一款名叫 Star Seven(*7)的机器,它有点像现在我们熟悉

的 PDA,但市面上的 PDA几乎都不是它的对手,更不要说是早在 10 年前那个计算机还不普

及的时代了。Java 语言的前身 Oak就这样诞生了,主要目的是用来编写在 Star Seven上的应

用程序。为什么叫 Oak 呢?原因是 James Gosling 办公室的窗外,正好有一棵橡树(Oak) ,

就取了这个名字。至于为什么 Oak 又会改名为 Java 呢?这是因为在 Oak 注册商标时,发现

已经被另外一家公司使用了。那要取什么新名字呢?工程师们边喝着咖啡边讨论着,看着手

中的咖啡,突然灵机一动,就叫 Java 好了。就这样它就变成了大名鼎鼎、如日中天的 Java 了。

但是,Green Team的项目发展却不尽如人意。智能化电子消费设备的市场发展远远低于 Sun的预期设想。Sun公司曾依此投标一个交互式电视项目,结果被 SGI打败了。可怜的 Oak 几乎无家可归,就在举步维艰,随时会被取消的时刻,情况却发生了巨大变化。1993 年,因

特网在美国开始大规模的发展,基于因特网的WWW也爆炸性地流行起来。Sun公司发现, Green Team 的项目研究成果——Java 似乎天生就是为因特网而诞生的,因为恰巧就是 Mark Andreessen开发的 Mosaic和 Netscape启发了 Oak, 他们利用 Java 开发了 HotJava 浏览器, 得

到了 Sun公司首席执行官 ScottMcNealy的支持,触发了 Java 进军 Internet。

于是,Java 第一次以 Applet 的形式在因特网上出现了。Applet 不但使 WWW 页面显示

静态的内容,而且可以显示动态的内容和动画。同时实行了本地计算机,从远程联网的服务

Page 11: Java Web动态图表编程

器上下载资料并正确地显示出来。这些技术在当时引起了巨大的震撼,促使 Java 在因特网上

得以飞速地发展。 1995 年 5月 23 日, Sun在 SunWorld’95 上正式发布 Java 和 HotJava 浏览器。

1.1.2 Java 的体系

Java 发展到今天,已从编程语言发展成为全球第一大通用开发平台。Java 技术已被计算

机行业主要公司所采纳。 1999 年, Sun公司推出了以 Java2 平台为核心的 J2EE、 J2SE 和 J2ME 三大平台。随着三大平台的迅速推进,全球形成了一股巨大的 Java 应用浪潮。

1.Java 2 Platform, Micro Edition(J2ME)

Java 2 平台微型版。Sun 公司将 J2ME 定义为“一种以广泛的消费性产品为目标、高度

优化的 Java运行环境” 。 自 1999年 6月在 JavaOne Developer Conference上声明之后, J2ME 进入了小型设备开发的行列。通过 Java 的特性,遵循 J2ME 规范开发的 Java 程序可以运行在

各种不同的小型设备上。

2.Java 2 Platform, Standard Edition(J2SE)

Java 2 平台标准版,适用于桌面系统应用程序的开发。本书例程就是利用 J2SE 5.0版的

相关图形 API包来开发的。

3.Java 2 Platform, Enterprise Edition(J2EE)

J2EE 是一种利用 Java 2 平台来简化企业解决方案的开发、 部署和管理等相关复杂问题的

体系结构。J2EE 技术的核心就是 Java 平台或 Java 2平台的标准版,J2EE 不仅巩固了标准版

的许多优点,例如: “一次编写、随处运行”的特性、方便存取数据库的 JDBC API、CORBA 技术,以及能够在 Internet 应用中保护数据的安全模式等,同时还提供了对 EJB(Enterprise JavaBeans)、Java Servlets API、JSP(Java Server Pages) ,以及 XML技术的全面支持。

本书第 4 章,阐述了如何将我们开发的 JSP 图表应用程序,在 J2EE 平台上进行部署和

管理。

1.1.3 Java 的优点

Java 是一种面向对象、分布式、解释、健壮、安全、可移植、性能优异,以及多线程的

语言。下面简单介绍其中的几个优点。

1.Write Once, Run Anywhere

“一次编写,随处运行” 。这是程序设计师们喜爱 Java的原因之一,核心就是 JVM(Java 虚拟机)技术。

编写好一个 Java 程序, 首先, 要通过一段翻译程序, 编译成一种叫做字节码的中间代码。

然后经 Java 平台的解释器,翻译成机器语言来执行——平台的核心叫做 JVM。Java 的编译

过程与其他语言不同。例如,C++在编译的时候,是与机器的硬件平台信息密不可分的。

编译程序通过查表将所有指令操作数和操作码等,转换成内存的偏移量,即程序运行时的

内存分配方式,以保证程序运行。而 Java 却是将指令转换成为一种扩展名为 class 的文件,

这种文件不包含硬件的信息。只要安装了 JVM,创立内存布局后,通过查表来确定一条指

令所在的地址,这就保证了 Java 的可移植性和安全性。

上述 Java 程序的编译和运行流程,如图 1.1 所示。

Page 12: Java Web动态图表编程

图 1.1 Java的编译和运行流程

MyApp.java 源文件

Interpreter 编译器

Ø 装载类

Ø 生成字节码

Ø 及时编译

Ø 解释执行

Java 应用

程序 JVM (J2SE)

Unix/Linux

Java 应用

程序 JVM (J2SE)

MS Windows

Java 应用

程序 JVM (J2ME)

Palm OS

①:编译

②:运行

Page 13: Java Web动态图表编程

2.简单

纯粹的面向对象,加上数量巨大的类所提供的方法(函数)库的支持,使得利用 Java 开

发各种应用程序,可以说是易如反掌。此外,在程序除错、修改、升级和增加新功能等方面,

因其面向对象的特性,使得这些维护也变得非常容易。

3.网络功能

Java 可以说是借助因特网而重获新生的,自然具备编写网络功能的程序。不论是一般因

特网/局域网的程序,如 Socket、Email、基于 Web 服务器的 Servlet、JSP 程序,甚至连分

布式网络程序,如 CORBA、RMI等的支持也是非常丰富的,使用起来也很方便。

4.资源回收处理(Garbage Collection)

Garbage Collection 是由 JVM 对内存实行动态管理的。程序需要多少内存、哪些程序的

内存已经不使用了,需要释放归还给系统,这些烦琐且危险的操作全部交由 JVM 去管理。

让我们能够更专心地编写程序,而不需要担心内存的问题。内存的统一管理,对于跨平台也

有相当大的帮助。

5.异常处理(Exception)

为了使 Java 程式更稳定、更安全,Java 引入了异常处理机制。能够在程序中产生异常情

况的地方,执行相对应的处理,不至于因突发或意外的错误造成执行中断或是死机。通过这

种异常处理,不仅能够清晰地掌握整个程序执行的流程,也使得程序的设计更为严谨。

1.2 Java开发环境的搭建

现在我们开始搭建 Java 开发环境。本章仅涉及 J2SDK 开发环境的安装与调试,并不涉

及 JSP 及 J2EE 服务器的安装与调试,这部分内容将在第 4 章进行说明。

1.2.1 Java 运行环境的要求

首先,需要了解 Java 对计算机主要硬件要求的最低配置,如表 1.1 所示。该表右边部分

为笔者写作本书时所使用的计算机配置。因为涉及大量在计算机内存中进行绘制各种图表的

运算与操作,建议使用较高配置的计算机以及尽可能多的内存,以便流畅地运行本书中的程

序。

表 1.1 Java的计算机硬件配置

硬 件 最低要求 笔者的配置

CPU Intel 或者兼容微处理器,奔腾 166MHz 及其以上 AMD Athlon 3200+

内存

最低 32MB,可以运行图形界面的 Java Application;

最低 48MB,可以运行 Java Applet;内存不足,将会导

致系统(尤其是服务器)的性能大幅下降

512 MB DDR,Kingston内存

(如果读者希望把本书中的例程运行在 J2EE的

服务器,如 WebLogic,最好不低于 256 MB)

硬盘 242 MB 80 GB

图形显示卡 无要求 Nvidia Ti 4200 64 MB DDR

网卡 普通网卡 3Com OfficeConnect 10/100 兆位网卡

其他部件,如图形显示卡、光驱及显示器等,Java 并没有特别的要求。因为本书涉及图

Page 14: Java Web动态图表编程

形的实时生成, 而 Java 的图形生成是通过图形显示卡调用 OpenGL功能来加速图像的渲染和

处理的。因此,如果系统拥有一块高质量的图形显示卡,将会大大地提高系统的性能。目前

市面上的图形显示卡,无论是在计算机主板上集成的显卡,还是普通的独立图形显示卡,都

具有强大的图形处理功能,足以满足流畅地运行本书中的所有例程。 Java 具有跨平台的特点,支持所有的主流操作系统,如下所示。

1.微软公司 Windows 系列

Ø Windows 98 / Me Ø Windows NT Ø Windows 2000 / Windows 2000 Server Ø Windows XP Ø Windows 2003 Server

2.UNIX 系列

Ø AIX, IBM 的 UNIX, 是根据 SVR2 以及一部分 BSD 延伸而来, 加上各种硬件的支持。

具备特有的系统管理功能(SMIT,系统管理接口工具)。

Ø SunOS(680x0,Sparc,i386):基于 4.3BSD,包含许多来自 System V 的东西。Sun 公司开发的 UNIX 操作系统,该系统对 UNIX 的贡献是:NFS,OpenLook GUI标准,

现演变为 Solaris。

Ø HP­UX(HP):惠普公司的 UNIX。

Ø SCO UNIX。

3.类 UNIX 系列

Ø Linux:包括各种版本的 Linux,如 RedHat、Turbo 和 Mandrake Linux。

Ø FreeBSD:由美国加州大学伯克利分校计算机系统研究小组设计和维护。

Page 15: Java Web动态图表编程

4.其他操作系统

Ø Mac OS:苹果电脑公司推出的操作系统,主要用于 Power PC。

上述操作系统都可以安装 Java。笔者的操作系统是微软 Windows XP SP2。本书的所有例

程都是在Windows XP 环境下进行编写和调试的。

1.2.2 Java 的安装和配置

本书采用 Sun公司发布的最新版本,也是近年来 Java 最重要的一个升级版本——Java 2 Platform Standard Edition 5.0,即 J2SE 5.0 作为我们的 Java 开发平台。至于 J2SE 5.0 具备哪些

新的功能和特性,请读者自行查阅相关资料。本书将尽量使用 J2SE 5.0 中的新特性来增强代

码的性能,如 Image I/O、Java 2D等,具体将在其他章节中说明。 J2SE 5.0 的下载地址:http://java.sun.com/j2se/1.5.0/download.jsp。双击下载文件:jdk­1_

5_0­windows­i586.exe就开始了 J2SE 5.0 开发环境的安装过程,如图 1.2所示。

图 1.2 Java的安装过程者一:改变默认的安装路径

安装过程如下:

(1) 安装程序经过自解压后, 就会出现安装协议的对话框, 选择 【I accept the terms in the license agreement】并单击【Next】按钮;

(2)在出现选择安装路径的对话框时,我们改变 J2SE 5.0 的默认安装路径。单击右边

的【Change…】按钮,在出现的路径修改对话框中,输入“c:\jdk1.5.0”后退回到先前的对话

框;

(3)单击【Next】按钮,继续剩余部分的安装。接着 J2SE 5.0 会提示 Java 运行环境(即 JRE 5.0)的安装路径,这里不做任何的改变,采用其默认设置。

然后,连续单击【Next】按钮直到完成安装。最后系统会提示重新启动计算机,再重新

启动计算机,需要对运行 Java的环境变量进行设置。这是非常重要的一个步骤,如果没有设

置成功,在运行 Java 时会出现错误。

环境变量的设置,如图 1.3、图 1.4 所示。

默认

Page 16: Java Web动态图表编程

图 1.3 Java的安装过程之二:环境参数的设置

(1)进入【控制面板】 ,单击【系统】 ,在出现的【系统属性】对话框中,单击【高级】

选项。

(2)单击【环境变量】按钮。

(3)在环境变量对话框中,单击位于【系统变量】组中的【新建】按钮。

(4)新建一个系统变量, “变量名”为 JAVA_HOME, “变量值”为 C:\JDK1.5.0,然后

单击【确定】按钮,如图 1.4 所示。这个 JAVA_HOME 的值就是 J2SE 5.0 的安装路径。

图 1.4 Java的安装过程之三:环境参数的设置

(5) 重复第 4 步, 在 “变量名” 中输入 “CLASSPATH” , 在 “变量值” 中输入 “. %JAVA _HOME%\lib” 。注意,这里的“.”是要求 JAVA编译器在查找 JAVA class 文件时,首

先从当前目录开始。

(6)最后是修改系统变量“PATH”的值, “PATH”的新值是在原有值前面加上

“%JAVA_HOME%\bin;” 。

提示:在 Java 1.2 版以后,不再需要 CLASSPATH 来设置系统类的路径。CLASSPATH 是为了设置用户编写的类或者第三方开发的类库而设置的。

现在,为确保系统环境变量生效,重新启动计算机。重启之后,测试 J2SE 5.0 的安装与

环境变量设置是否正确。单击【开始】→【所有程序】→【附件】→【命令提示符】 ,启动命

令提示符后,在该窗口中输入如下命令:

echo %java_home% echo %classpath% echo %path%

Page 17: Java Web动态图表编程

java –version

在笔者本机上,得到如图 1.5所示的结果。

图 1.5 测试 Java运行环境系统参数的设置

如果读者得到如图 1.5 一样的结果,就说明最新版的 Java 开发环境已经安装成功。

提示:读者计算机中“Path”值可能同笔者计算机的值不一样,但其中一定要包含

“C:\jdk1.5.0\bin”的内容。

1.3 Java/JSP开发工具

为了获得 Java 图表编程学习的最佳效果, 本书所有源程序都没有使用任何一种可视化的 Java 开发环境来进行开发。建议读者在学习期间不要使用可视化的开发工具,如 JBuilder、 JCreator、KAWA、Visual Cafe、Eclipse、IntelliJ IDEA、BEA WebLogic Workshop、Oracle JDeveloper 等。本书所有的操作,都可以用 JSP 服务器(Tomcat/Resin)+WWW 浏览器

(IE/Mozilla/Firefox)+文本编辑器(EditPlus)来轻松完成。这样,我们才可以深刻地理解

系统的运行机制。等到读者对此非常熟悉的时候,再使用可视化开发工具进行开发,定会达

到事半功倍的效果。此外,在本书的附录中,我们将向读者介绍一款优秀的 Java IDE 开发工

具——Gel。利用 Gel,可以方便地创建、编辑、编译及运行 Java/JSP/Servlet 应用程序。建议

读者循序渐进地学习之后,再来学习 Gel,相信一定会喜欢上这款精巧而强大的 Java 开发环

境。

基于文本编辑器的 Java/JSP 开发工具有很多种,如记事本、UltraEdit、EditPlus 等。但最

简单及成本最低廉的非 Windows 自带的记事本(NotePad)莫属了。可以直接使用记事本进

行 Java/JSP 的程序设计,它具有简单、方便的特点。但其毕竟不是为程序员准备的开发工具,

因此,并不适用于程序设计。

现在,向读者推荐一款韩国的文本编辑器——EditPlus作为我们的 Java及 JSP编辑器。

1.3.1 EditPlus 简介

EditPlus 是由韩国人编写的一款共享软件,官方网址是 www.editplus.com。最新版本是 EditPlus 2.12。EditPlus 是功能全面的文本、HTML、程序源代码编辑器。主要特点如下:

(1)默认支持 HTML、CSS、PHP、ASP、Perl、C/C++、Java、JavaScript 和 VBScript 的语法高亮显示,通过定制语法文件,可以扩展到其他程序语言。

(2)EditPlus 提供了与 Internet 的无缝连接,可以在 EditPlus 的工作区域中打开 Intelnet 浏览窗口。

Page 18: Java Web动态图表编程

(3)提供了多工作窗口。不用切换到桌面,便可在工作区域中打开多个文档。

(4)正确地配置 Java 的编译器“Javac”以及解释器“Java”后,使用 EditPlus 的菜单

可以直接编译执行 Java 程序。

所谓的文本编辑器,最终还是要归结在文本编辑上。EditPlus 提供了比普通编辑器更加

强大的编辑功能。首先可以无限制地 Undo,其次具有更强大的删除功能,EditPlus 不仅可以

删除一个字,而且还可以删除选择部分、删除整个行等。提供了更强大的复制功能,可以复

制整个行,如单击【Edit】菜单,选择【Duplicate line】命令,即可实现此功能。这些大大地

超越了记事本的功能。 EditPlus 查找/替换功能也是一流的。不但可以进行一般的查找和替换,而且支持以正则

表达式的方式进行查找与替换, 甚至可以在磁盘某一目录的所有文件中, 进行字符串的查找。

比如说,笔者要查找 D:\webchart 目录(包括其子目录)下,所有的 jsp文件中,哪些文件包

含了字符串: “charset=GB2312” ,我们就可以单击菜单栏上的【Search】,选择【Find in Files】

命令。如图 1.6 所示,在出现的查找对话框中,分别填入以下内容:

Ø【Find what】填入“charset=GB2312” ,表示要查找的内容。

Ø【File type】填入“*.jsp” ,表示要查找所有扩展名为 jsp 的文件,如果这一栏不输入

任何条件,则表示在所有的文件中查找,而忽略文件的格式。

Ø【Folder】填入“d:\webchart” ,表示希望查找文件位于 d:\webchart 这个目录下。

Ø 如果要查找 d:\webchart 及其子目录下的所有 jsp 文件,就需要选中该对话框中的

“include subfolders”选项。

填好上述栏目后,单击该对话框中的【Find】按钮,就可以在 EditPlus 下方的输入输出

窗口中,得到查找的结果了。在输出窗口中,会提供一个被查找到的匹配文件的清单。在此

窗口的左侧,指出与被查找的字符串“charset=GB2312”相匹配文件的所在目录以及文件名,

然后给出字符串在该文件中出现的位置,该位置用行列坐标表示。在输出窗口的最底部, EditPlus 会提供一个统计结果——查找到的匹配的文件总数。

图 1.6 EditPlus的 Find in Files功能

双击查找结果清单中出现的任一文件名,EditPlus 会在主窗口中显示出该文件的内容,

并将鼠标指针定位到被查找的字符串第一次出现的位置。

该功能在大量文件中进行查找时,提供了强大的支持。

前面谈到,设置好 Java 的编译器“Javac”和解释器“Java”后,通过 EditPlus 的菜单可

Page 19: Java Web动态图表编程

以直接编译执行 Java 程序。现在就来看看如何在 EditPlus 中设置。单击菜单栏中的【Tools】

→【Configure User Tools】→【Preferences】→【User tools】,如图 1.7 所示。

Page 20: Java Web动态图表编程

图 1.7 EditPlus设置编译与运行 Java程序的功能

Ø【Add Tool】:表示新增一个用户自定义工具。

Ø【Program】:表示该工具与程序有关。

Ø【Menu text】:表示要求为添加的工具命名。这里填入:编译 JAVA。

Ø【Command】:表示该工具运行时,所调用的可执行程序。Java 中的编译命令是 javac,

所以这里选择 Java 安装目录中 bin子目录中的 javac.exe程序。

Ø【Argument】:表示执行 javac.exe 程序时,所需要的参数。单击旁边的带有向下三角

形的按钮来选择“$(FilePath) ”即可。

Ø【Initial directory】:选择初始化目录。单击旁边的带有向下三角形的按钮来选择“$ (FileDir) ”即可。

Ø【Capture output】:选择捕获屏幕输出。

通过 EditPlus 来编译 Java 程序完成。现在继续增加一个新的工具,设置通过 EditPlus 来

运行编译成功的 Java 程序。方法同前面讲述的设置编译 Java 程序的过程基本相同。按照图 1.7 右下方所示的“运行 Java 的配置”内容,进行设定即可。

正确设置后,无论在 EditPlus 编译还是运行 Java 程序的时候,EditPlus 的输出窗口中都

会显示出相应的编译和运行结果。

关于 EditPlus 就简要介绍到这里。

1.3.2 UltraEdit 简介

UltraEdit 是共享软件,官方网址是 www.ultraedit.com,最新版本是 v11.00a。与 EditPlus 类似,UltraEdit 是一款功能强大的文本、HTML、程序源代码编辑器。作为源代码编辑器,

其默认配置可以对 C/C++、VB、HTML、Java 和 Perl 进行语法高亮显示。设计 Java 程序也

很方便,具有复制、粘贴、剪切、查找、替换、格式控制等编辑功能。 UltraEdit 同样可以在【Advanced】菜单的【Tool Configuration】菜单项配置 Java 编译器

Javac和解释器 Java,直接编译运行 Java 程序。 UltraEdit 设置 Java 编译器“Javac”的方法如图 1.8 所示。

运行 Java 的配置

Page 21: Java Web动态图表编程

图 1.8 UltraEdit设置编译 Java程序的功能

Ø【Command Line】:表示命名行的执行方式。n%表示 Java 源程序的文件名,%e表示 Java 源程序的扩展名。因为已经正确设置了 Java 的运行路径,所以使用 javac就可以

了,否则就要用 C:\jdk1.5.0\bin\javac.exe。

Ø【Working Directory】:表示工作目录。%p表示工作目录就是当前目录。

Ø【Menu Item Name】:要求给添加的工具命名。填入:编译 JAVA。

Ø【Save Active File】:表示该工具运行之前,首先保存当前 Java 源程序。

Ø【Output to List Box】:表示将结果输出到列表框中。

Ø【Capture Output】:选择捕获屏幕输出。

Ø【Insert】:Java 编译的工具菜单就做好了。

同理,设置 Java 解释器“Java”的方法如图 1.9 所示。与前面的步骤类似,只需要更改

第一和第三个步骤即可。【Command Line】输入“java %n” ,【Menu Item Name】输入“运行 Java” 。

关于 UltraEdit 就简单介绍到这里。

Page 22: Java Web动态图表编程

图 1.9 UltraEdit设置运行 Java程序的功能

1.3.3 其他 Java/JSP 开发工具

下面向读者简要介绍两个开放源代码的作品 Eclipse 和 Gel,虽然本书并不使用这两种开

发工具,但笔者觉得有必要向读者介绍一下,目前国外非常流行的 Java IDE 开发工具。

1.Eclipse 简介

Eclipse是一个开放源代码的项目, 可以在 www.eclipse.org免费下载 Eclipse的最新版本。

一般 Eclipse提供几种下载版本:Release、Stable Build、Integration Build和 Nightly Build。建

议下载 Release或 Stable版本。Eclipse本身是用 Java语言编写的,但下载的压缩包中并不包

含 Java 运行环境,需要用户自行安装 JRE,并且需要在操作系统的环境变量中,指明 JRE 中 bin的路径。如果已经安装了 JDK,就不用安装 JRE了。

安装 Eclipse的步骤非常简单,只需将下载的压缩包按原路径直接解压即可。注意,如果

要更新版本,请先删除旧版本重新安装,不能直接解压到原来的路径覆盖旧版本。解压缩之

后,可以到相应的安装路径去找 Eclipse.exe 运行。如果下载的是 Release 或 Stable 版本,并

且 JRE环境安装正确无误, 一般来说不会有什么问题, 在闪现一个很酷的月蚀图片后, Eclipse 会显示它的默认界面。

Eclipse 的界面有点像 JBilder,实际操作进程中会发现它更像 IVJ。毕竟开发 Eclipse 的主导

是开发 IVJ 的原班人马(可参考 www.oti.com)。值得一提的是,Eclipse 项目的参与者除了 IBM 以外,还有 Borland、Rational Software、RedHat、Merant,以及 BEA等一大批业界的佼佼者,为 Eclipse的未来奠定了良好的基础。

Eclipse 不仅可以开发 Java 应用程序,还可以安装 Lomboz 插件,并整合 Tomcat 以实现

对 JSP 以及 Java Servlets 的开发。

本书不在这里讨论 Eclipse的下载、安装、使用、开发、各种插件的安装和使用,以及与 JBoss、Tomcat、WebLogic等 JSP 或者 J2EE 服务器的整合。有兴趣的读者,请自行查阅相关

资料。

2.Gel 简介

Gel 是由 Gerald Nunn 发布的一款 Java IDE 开发工具。Gel 的官方主页是 http:// memescape.co.uk/gexperts/index.html。Gel 是完全免费的,读者可到该网站下载。在本书写作

时,Gel 的最新版本是 RC39。目前该 IDE 工具,在国外众多的 Java、JSP 及 J2EE 程序员中

Page 23: Java Web动态图表编程

引起了极大的反响,但国内很少有人使用这款优秀的 Java IDE。 Gel 的运行环境如下:

Ø Windows 95/98/ME/2000/XP Ø Internet Explorer 4+ Ø JDK 1.1+ Gel 的特点如下:

(1)编辑器(Editor)

Ø 支持对 Java、JSP、HTML、XML、C、C++、Perl、Python,以及更多编程语言的语

法高亮显示。

Ø 无限制地取消及恢复功能。

Ø 整段代码的减少缩进量与增加缩进量。

Ø 高亮显示配对的大括号。

Ø 实时地拼写检查。

Ø 可以显示控制字符。

Ø 整合的源代码管理及控制,支持 CVS、VSS、Clearcase、Perforce、QVCS、CS­RCS 以

及更多的格式。

Ø 正则表达式搜索。

Ø 文件查找。

Ø 将被选择代码完整地输出为 HTML及 RTF 文件。

Ø 保持本地程序的更新历史,易于在需要的时候恢复原状。

(2)代码援助(Code Assistants)

Ø 自动完成 Java 和 JSP 的编码。

Ø Java 和 JSP 的参数提示。

Ø Java 和 JSP 的标识符提示。

Ø 查找声明,并快速跳转到声明所属的变量、方法或类。

Ø 类浏览器。

(3)项目管理(Project Management)

Ø 智能文件夹,当系统文件发生改变的时候,自动同步更新。

Ø 可以运行 Applications、Applets 以及应用服务器。

(4)J2EE Ø 直接预览 Servlets 及 JSP。

Ø 自动完成 JSP 标签,支持 HTML及自定义 JSP 标签。

(5)其他

Ø 支持集成 ANT。

Ø 支持集成 JUnit。

Ø 定制工具栏及快捷方式。

Ø 支持创建自定义工具。

Ø Javadoc浏览器。

总的来说,Gel 虽然小巧,但速度奇快,更重要的是它拥有许多大型 Java IDE 的完整功

能。本书附录,将详细介绍 Gel 的设置及如何利用 Gel 来开发 JSP 程序。

1.4 第一个 Java 程序

前面我们讲解了 J2SE 5.0的安装及环境变量的设置,现在我们就编写一个很简单的 Java

Page 24: Java Web动态图表编程

应用程序 Application来结束本章的学习。

在 EditPlus 或 UltraEdit 中输入如下代码(\chap01\Fig1.4\WelcomeJavaChart.java) :

1: // Fig. 1.4: WelcomeJavaChart.java 2: // 第一个 Java程序 3: 4: public class WelcomeJavaChart 5: // Java主程序入口 6: public static void main(String args[]) 7: System.out.println("**************************************"); 8: System.out.println("欢迎来到<<精通 Java Web动态图表编程>>的世界! "); 9: System.out.println("**************************************"); 10: 11: //主程序结束 12: 13: // end class WelcomeJavaChart

该源程序仅仅在命令窗口显示三条文本。可以用传统的方法,在 DOS 下对该文件进行

编译及运行,也可以在 EditPlus 及 UltraEdit 中进行。

将WelcomeJavaChart.java拷贝到D:\webchart\chap01\Fig1.4子目录下,运行如下命令:

D:\webchart\chap01\Fig1.4>javac WelcomeJavaChart.java D:\webchart\chap01\Fig1.4>java WelcomeJavaChart

编译及运行结果如图 1.10 所示。

在 UltraEdit 中,编译和运行的方法是运行【Advanced】→【编译 JAVA】→【运行 JAVA】

工具,如果全部设置正确,就可以看到如图 1.11 所示的结果。

图 1.10 WelcomeJavaChart.java的运行结果

Page 25: Java Web动态图表编程

图 1.11 UltraEdit下WelcomeJavaChart.java的运行效果

在 EditPlus 中,编译和运行的方法是运行【Tools】→【编译 JAVA】→【运行 JAVA】工

具,如果全部设置正确,就可以看到如图 1.12 所示的结果。

Page 26: Java Web动态图表编程

图 1.12 EditPlus下的WelcomeJavaChart.java的运行结果

1.5 本章小结

本章主要介绍 Java 的历史及其运行机制、技术特点,讲解了各种 Java 开发环境的搭建,

包括 JDK的下载、安装和环境变量的设置,在 EditPlus 和 UltraEdit 中如何设置编译及运行。

此外,简要介绍了两款目前国外流行的 Java IDE 开发环境:Eclipse和 Gel。最后用一个简单

的 Java Application来测试搭建的 Java 开发环境是否成功。

只有在正确搭建 Java 开发环境的基础上,才能顺利地搭建 JSP 服务器。下面学习 Java Applet 的编写和调试,让我们一起进入 Web 图表编程的精彩世界!

第 2 章 Java Applet 与绘图基础

Java 最初在因特网上流行,很大程度上归功于 Java Applet。Java 对图形处理的应用最早

就是从 Applet 开始的,对于希望掌握 Web 动态图表编程的读者来说,从 Applet 着手学习是

一个很好的途径。理解 Applet 的绘图基础,就可以顺利地过渡到如何运用 JSP 来进行图表的

绘制。

2.1 Java Applet概述

Java 共有两种类型的程序。一种称为 Java Application,如第 1 章 1.4 节所示的 Welcome

Page 27: Java Web动态图表编程

JSPChart.java。本章介绍另外一种 Java 程序,这类程序称为 Java Applet。Java Applet 是一种

可以嵌入 HTML文档(即 Web 页)中并可以被执行的 Java 程序。支持 Java 的浏览器,在浏

览一个包含有 Java Applet 的 Web 页面时,该 Applet 就被下载到该浏览器中,并被 JVM 解释

执行。

目前,支持 Java2 的浏览器有:Mozilla、Netscape7 及其以上版本、Opera 和 FireFox。微

软的 Internet Explorer 或者早期的 Netscape版本,需要安装 Java 的插件(Plug­in)才可以正

常浏览 Java Applet 的 Web 页面。

提示:如果读者的计算机中已经安装了 J2SE 5.0 开发环境,那么 IE 就不需要再安装插

件。该插件是作为 J2SE 5.0 的一部分(JRE 5.0)安装在浏览器上的。

我们先来看 J2SE 5.0 中提供的一些 Applet 范例,展示了 Applet 的强大功能。其中每个 Applet 还提供了源代码。通过阅读这些源代码,可以了解 Java 最新的、令人激动的新特性及

其用法。 J2SE 5.0 提供的范例位于 J2SE 5.0 安装目录中的 demo子目录下。在 MS Windows 中,

该范例的默认安装位置是:C:\jdk1.5.0\demo\applets;在 Unix/Linux/Mac OS 中,该范例的默

认安装位置是:/usr/local/jdk1.5.0/demo/applets。

在 applets 子目录下又包含了一些子目录,每一个子目录对应着一个相应的 Java Applet,

每个 Applet 展示了不同的功能。表 2.1 列出了这些范例及其功能。

表 2.1 applets目录中的范例

范 例 说 明

Animator 演示了四种不同类型的动画

ArcTest 演示了圆弧的绘制,可以与该 Applet 交互并改变圆弧的显示属性

BarChart 演示了一个简单的条形图

Blink 演示了以不同颜色显示并闪烁的文本

CardTest 演示了几个 GUI 的部件及布局方式

Clock 演示了一个可以显示当前日期和时间的时钟

DitherTest 演示了用图形抖动(dithering)技术进行图形绘制,该技术实现的是从一种颜色逐渐转

变成另一种颜色(渐变色)

DrawTest 演示了使用鼠标在 applet 上,用不同的颜色绘制线段和点

Fractal 演示了绘制一个不规则图形

GraphicsTest 绘制不同的图形,以演示 Java 对图形的处理能力

GraphLayout 绘制一个通过直线连接,由许多个节点(用矩形表示)组成的图形。可以拖动其中一个

节点,观察其他节点是如何重新调整它们位置的情形,以演示复杂的图形交互

ImageMap

演示了一个有热点的图像。当鼠标移动到热点区域,该区域将会高亮显示,并在

appletviewer(或者浏览器)的左下角(状态栏)显示一条消息。当鼠标定位在图像中人像

嘴上的时候,会听见 applet 说“hi”

JumpingBox 在屏幕上随机移动一个矩形,可以试一试用鼠标去捕获它

MoleculeViewer 演示了几个不同化学分子结构的三维视图,可以拖动鼠标从不同的角度来观察这些化学

分子的结构

NervousText 绘制在屏幕上跳动的文本

SimpleGraph 绘制一条复杂的曲线

Page 28: Java Web动态图表编程

SortDemo 比较三种不同的排序技术。执行该 Applet 时,会出现三个 appletviewer 窗口。单击其中

任意一个窗口,都会开始排序,不同的排序技术所花费的时间是不同的

SpreadSheet 演示了一个简单的,由行和列组成的电子表格

TicTacToe 演示了一个叫做 TicTacToe 的游戏,由计算机和用户互弈

WireFrame 演示了四个由许多线段组成的三维图形,分别是立方体、恐龙、直升飞机和军舰

运行上述例程的方法有两种。我们以运行表 2.1中所讲述的 ArcTest Applet 程序为例:

(1)用 Java 自带的 Applet 容器 appletview.exe来运行上述例程。

在【命令提示符】下,输入如下命令:

C:\Documents and Settings\JKZhong>cd C:\jdk1.5.0\demo\applets\ ArcTest C:\jdk1.5.0\demo\applets\ArcTest>appletviewer example1.html

(2)会看到 ArcTest 的运行结果,再进行如下操作,会看到如图 2.1 所示的结果。

Ø 在第一个文本框中输入 10,表示圆弧的起始角度。

Ø 在第二个文本框中输入 145,表示圆弧的终止角度。

Ø 按钮【Fill】表示绘制并填充这个圆弧,按钮【Draw】表示仅仅绘制圆弧。单击【Fill】

按钮可以看到,绘制的结果是一个从 10开始到 145结束的实心圆弧。

(3)直接用浏览器打开 C:\jdk1.5.0\demo\applets\ArcTest 子目录下的 example1.html 文

档,就可以运行上述 Java Applet 程序,这里以微软的 IE 为例。

Ø 如果与笔者相同,使用微软 Windows XP SP2 下的 IE,可能会看到如图 2.2 所示的结

果。IE 可能会有这样一个提示: “为帮助保护您的安全,Internet Explorer 已经限制此

文件显示可能访问您的计算机的活动内容。单击此处查看选项…” 。

Ø 这时无法看到 Java Applet 的运行结果,因为被 IE 屏蔽了。解决的方法是,在 IE 的提

示下单击鼠标右键,在弹出的菜单中选择【允许阻止的内容】。

Ø 然后会弹出一个【安全警告】对话框,单击【是】按钮,即可看到运行如图 2.2 所示

的 Java Applet 结果。

图 2.1 appltviewer的运行结果 图 2.2 ArcTest Applet的运行结果

提示:如果 appletviewer 命令不能工作,或者收到一条消息,表示没有找到 appletviewer 命令,则计算机可能没有正确地设置 PATH 的环境变量。读者需要按照 1.2.2 节讲述的方法重

新设置 PATH 变量,修改 PATH 变量后,需要重新启动计算机。

Page 29: Java Web动态图表编程

2.2 Java Applet工作流程

2.2.1 组件与容器

一个组件(Component)代表可以在屏幕上显示出来的一个图形实例,也就是 Component 类的任意一个子类的对象。图 2.3 是以 Component 类为基础类的继承关系结构图,图中箭头

所指向的类为超类。

从图 2.3 可以看到,JApplet、JFrame、Dialog 等就是一个组件,它们都属于 Component 类的子类。所有从 Container 派生出来的类都可以包含由 Component 类派生出来的任意一个

类的对象,所以我们称之为容器(Container)。又因为 Container 类是 Component 类的一个子

类, 所以任意一个容器对象同时又是一个 Component 对象, 因此一个容器可以包含其他容器。

下面以 Panel 类对象来说明这个关系。我们把一些图形实例如矩形、椭圆等放置在一个 Panel 类的对象——panelA中, 再把另外一些 Component 对象如菜单、 按钮等放置在另外一个 Panel 类的对象——panelB 中,最后把 panelA和 panelB 放置在另外一个 Panel 类的对象——panelC 中。这里的例外是,Windows 类及其子类的对象,不能包含在其他容器中,否则,系统会抛

出一个异常。

图 2.3 以 Component类为基础类的继承关系结构图

JComponent 类是所有在一个窗口中作为 GUI 一部分使用的 Swing 组件的基类。因为 JComponent 也是从 Container 类继承下来的,故此所有的 Swing组件也是容器。

JApplet 类是所有 Swing Applet 类对象的基类,它由 Component 派生的 Container 类派生

出来。因此,一个 Applet 程序就从 Container 和 Component 类继承所有的方法。此外,它也

继承了旧的 Applet 类的方法。

这里需要注意以下几点:

Ø JApplet 类、JFrame类、JDialog类以及 JComponent 类,包含在 javax.swing包中。

Ø Applet 类包含在 java.applet 包中。

Ø 除此之外的类,包含在 java.awt 包中。 java.applet 包很小,它只包含这一个类和三个相关的接口,我们很少直接使用该包中的

方法。在 JApplet 类中,对旧的 Applet 类的方法进行了扩充和改进(旧类与 javax.swing 包中的 GUI组件不完全兼容)。所以本书所有相关的 Applet 类都扩展于 JApplet 类。

Component (抽象类)

Container (抽象类) JComponent

(Swing组件)

Window (无边框或标题栏)

Panel (旧式容器)

Applet (旧式 Applet 类)

Frame (旧式框架类)

Dialog (旧式对话框类)

JApplets JFrame JDialog JWindow

(无边框或标题栏) Applet 的基类 应用程序

窗口的基类

对话框的基类

Page 30: Java Web动态图表编程

2.2.2 Applet 的编译与执行

如同 Java Application程序一样,在执行前必须对 Applet 类的源程序进行编译,编译的方

法与 1.4 节讲述的方法相同。 Applet 被嵌入到 HTML页面中, 并由 Applet 容器 (appeltviewer 或者 Web 浏览器) 执行。

Applet 的运行由浏览器控制,不由 Applet 中的代码控制。当浏览器载入包含有 Applet 的 Web 页面时,它将生成一个 Applet 类的对象,然后利用 Applet 类对象的五个方法控制 Applet 的

执行。这五个方法都是 public类型,不返回任何结果,以及不需要任何参数的方法。

表 2.2 列出了 5 个方法及其执行的相应功能。

表 2.2 Applet默认的 5个方法

方 法 说 明

void init() 启动 Applet。浏览器总是调用 Applet 类的默认构造器生成对象,然后调用 init()方法进行初始

化。一般在这个方法中生成 Applet 运行所需的对象并初始化 Applet 类的所有数据成员

void start()

方法由浏览器调用,启动或者重新启动 Applet。当 Applet 第一次启动时,start()方法将紧跟

在 init()方法后被浏览器调用。如果用户离开当前的 HTML 页面后,在重新返回到当前 HTML

页面时,Start()方法也将被调用。start()方法一般用来启动 applet 需要的任何附加线程

void paint( Graphics g )

在 init()方法执行结束, start()方法启动后, 就调用此画图方法。 另外, 每次需要重新绘制 Applet

时,也将调用此方法。本方法的典型应用,包括使用 Applet 容器传递给 paint 方法的 Graphics

对象 g画图

void stop() 当用户离开包含有该 Applet 的 HTML 页面时,浏览器调用此方法。stop()方法被调用后,将

立即停止所有在 start()方法中启动的操作

void destroy() 在终止 Applet 运行时,调用 destroy()方法,以便释放 Applet 占用的,由本地操作系统管理的

任何系统资源。此方法执行之前,总是先调用 stop()方法

我们编写的 Applet 程序都将继承上述 5 种默认的方法。对于每个 Applet,方法调用的顺

序总是 init、start 和 paint,这就保证了每个 Applet 开始执行时都将调用这些方法。但是,并

非每个 Applet 都需要调用 5 种方法。我们可以重载上述的 5 种方法,也可以重载其中的某种

方法,以便 Applet 实现我们所设计的方法。

浏览器通过包含有 Applet 的 HTML页面来运行 Applet 程序。 那么, 在执行 Applet 之前,

首先要创建 HTML文档。

Page 31: Java Web动态图表编程

2.2.3 包含 Applet 的 HTML

通常,HTML文档以“.html”或者“htm”的扩展名结尾。一个在 HTML页面中包含有 Applet 的 HTML文档,必须指明 Applet 容器应该装入和执行哪一个 Applet。

下面是一个完整的包含 Applet 的 HTML源代码:

1: <!DOCTYPE HTML PUBLIC "­//W3C//DTD HTML 4.0 Transitional//EN"> 2: <HTML> 3: <HEAD> 4: <TITLE>包含有 Applet 的 HTML页面</TITLE> 5: </HEAD> 6: 7: <BODY> 8: <APPLET CODE = "MyAppletName.class" 9: WIDTH = "Applet_width_in_pixels" 10: HEIGHT = "Applet_height_in_pixels" > 11: </APPLET> 12: </BODY> 13: </HTML>

我们可以看到,代码第 8 行~第 11 行是隶属于<applet>和</applet>标记之间的内容,也

是一个包含 Applet 的 HTML页面所需的最少内容,其余部分都是标准的 HTML标记。 Applet 标记内的“CODE”属性,表示该 Applet 的.class 文件,它假设 Applet 位于包含

该 Applet 的网页所在的相同目录下。本例指明 Applet 容器,载入一个名为 “MyApplet Name”

的 Applet 字节码文件。如果.class 文件和 HTML 文件不在同一目录下,则需要使用一个 CODEBASE属性, 它拥有一个URL值, 指明 applet源代码的位置, URL可以是绝对值 (以 http:// 开头),也可以是相对值。属性“WIDTH”表示该Applet在网页上显示的宽度,用像素表示。属

性“HEIGHT”表示该 Applet在网页上显示的高度,也用像素表示。

了解前面我们介绍的 Applet 的相关知识,就可以编写一些 Applet 程序了。

2.3 绘制文本(字符串)

现在,让我们来开发一些 Applet。首先介绍一个简单的 Applet 程序——DrawString Applet.java,就是在 Applet 上绘制一段文本(字符串) “欢迎来到《精通 Java Web 动态图表

编程》的世界! ” 。 DrawStringApplet.java 的源程序清单如下(\chap02\Fig2.3\Fig2.3_01\):

1: // Fig. 2.3_01: DrawStringApplet.java 2: // 第一个 Java Applet程序, 绘制文本(字符串) 3: 4: // 引入相关的包

5: import java.awt.Graphics; // 引入 Graphics 类

6: import javax.swing.JApplet; // 引入 JApplet 类 7: 8: public class DrawStringApplet extends JApplet 9: 10: 11: // 在 applet上绘制文本 12: public void paint(Graphics g) 13: 14: // 调用父类的 paint 方法 15: super.paint(g); 16: 17: g.drawString("欢迎来到《精通 Java Web动态图表编程》的世界!", 25, 25);

Page 32: Java Web动态图表编程

18: 19: // paint 方法结束 20: 21: // DrawStringApplet 类结束

提示:本书后面的例程将不再列出完整的源代码清单,请读者从 www.broadview.com.cn网

站下载本书配套代码,查看相关目录下完整的源代码清单。

包含这个 Applet 的 web 页面文件 DrawStringApplet.html 的源程序清单如下:

1: <!DOCTYPE HTML PUBLIC "­//W3C//DTD HTML 4.0 Transitional//EN"> 2: <HTML> 3: <BODY> 4: <APPLET CODE = "DrawStringApplet.class" WIDTH = "320 " HEIGHT = "45"

> 5: </APPLET> 6: </BODY> 7: </HTML>

提示:本书所有的 Java Applet 都将使用与本例相类似的 HTML 文档。对于不同的 Java Applet, 读者只需要修改上述代码第四行的内容, 重新设置 “CODE” 、 “WIDTH” 和 “HEIGHT”

的属性值即可。

图 2.4 展示了名为 DrawStringApplet 的 Applet 在四个不同的 Applet 容器中的运行结果。

分别是 Appletviewer、 Netscape Web 浏览器 (现在更名为 Mozilla)、 Opera, 以及微软的 Internet Explorer Web 浏览器。

该程序说明了 Java的几个重要特性。程序第 17行完成的实际工作,即在屏幕上绘制一行文

本“欢迎来到《精通 Java Web动态图表编程》的世界! ” 。下面我们逐行来分析本程序。

程序第 1 行~第 2 行:

// Fig. 2.3: DrawStringApplet.java // 第一个 Java Applet程序, 绘制文本(字符串)

是注释。第 1 行说明例程的编号,以及源程序的文件名。第 2 行描述程序所实现的功能。

Page 33: Java Web动态图表编程

图 2.4 DrawStringApplet在四个不同的 Applet容器中的运行结果

程序第 5 行:

import java.awt.Graphics; // 引入 Graphics 类

是一个 import 声明,表明 Applet 使用 java.awt 包中的 Graphics 类。Java 包含了很多预定

义的类,并按包(类的集合)的形式进行分组。Graphics 类是一个具备图形处理能力的类,

可以让 Java Applet 能够绘制直线、文本、矩形及椭圆形等图形对象。

程序第 6 行:

import javax.swing.JApplet; // 引入 JApplet 类

也是一个 import 声明,表明 applet 使用 javax.swing包中的 JApplet 类。在 Java 2 中创建

一个 Applet 时,必须引进 JApplet 类。

程序第 8 行:

public class DrawStringApplet extends Japplet

声明了一个名为DrawStringApplet的公有 (public) 类, 并继承了 javax.swing包中的 JApplet 类。该类的主体从程序第 9 行的左花括号“”开始,到程序第 21 行的右花括号“”结束。

在继承关系中,JApplet 类称为超类(也称为基类或父类),DrawStringApplet 被称为子类或派

生类。因为,DrawStringApplet 继承了 JApplet 类,所以,它不仅具有 JApplet 类的所有属性

x轴 y轴

applet 菜单

绘制区左上角是(0,0),绘

制区边缘在状态栏上 x坐标从

左向右递增,y 坐标从上向下

递增 文本在该处的坐

标是(25,25)

绘制区域在左上角

坐标是(25,25)

绘制区域在左上角

坐标是(25,25)

状态栏

绘制区域在左上角

坐标是(25,25)

appletviewer 窗口

状态栏显示 applet 当前的运行状态

状态栏

Page 34: Java Web动态图表编程

和方法,而且还可以增加新的方法和属性,以及扩展被继承的方法。

程序第 12 行:

public void paint(Graphics g)

声明 Java Applet的 paint方法。 前面我们提到的 paint方法是 Java Applet容器在载入Applet 时,自动调用三个方法中的一个(其他两个是 init 和 start 方法)。如果我们不在 Applet 中重

新定义该方法,那么 Applet 容器将调用继承下来的默认超类的 paint 方法。因为,默认超类

(JApplet)的 init、start 和 paint 方法为空(指明该方法的方法体中没有任何语句),也就是

说,超类的 paint 方法不会在 Applet 上绘制任何东西,所以在屏幕上也就不会有任何显示。 void表示 paint 方法执行完后,不返回任何结果。paint 方法需要一个 Graphics 类的对象

g(对象也称为“类实例”或者“实例” )。paint 方法使用 Graphics 类的对象 g 在 applet 上绘

制各种图像对象, 如文本及各种几何图形等。 Graphics 类的对象 g的创建由 Applet 容器负责。

为了实现在屏幕上绘制出一段文本,程序第 13 行~19 行,重载(重新定义)了超类的 paint 方法。

程序第 15 行:

super.paint(g);

调用超类 JApplet 的 paint 方法。 该语句是 Java Applet 程序中 paint 方法体的第一个语句。

早期的 Java Applet 在没有该语句的情况下仍然可以正常运行,但在一个拥有众多绘图组件和 GUI组件的复杂 Java Applet 中, 忽略该语句可能会导致严重的错误。 所以, 在编写 Java Applet 程序的时候,一定在 paint 方法的第一行设置这条语句,这是一个很好的编程习惯。

程序第 17 行:

g.drawString(“欢迎来到《精通 Java Web动态图表编程》的世界!”, 25, 25);

通过 Graphics 类的对象 g来调用该类的 drawString方法, 以实现在 Applet 上绘制文本的

功能。drawString方法的具体使用如表 2.3 所示。

表 2.3 drawString方法

方法 public abstract void drawString(String str, int x, int y)

说明 用当前的字体和颜色,在指定的坐标(x,y)处,绘制字符串 str 的内容

调用的具体方法是 Graphics 类的对象名称(这里是 g)加上一个点操作符“.” ,再加入

所调用的方法名称(这里是 drawString)。方法名后面跟着一对括号,括号内包含有执行该方

法所需的参数。 drawString 方法需要三个参数,第一个是 String 类的字符串参数,表示要绘制的文本。

最后两个是 int 类型的整数型参数,表示要绘制文本的第一个字符左下角所在的位置,该位

置用 Java 的平面坐标系来表示。

图 2.5 说明了 Java 坐标系。坐标原点位于左上角,以像素为单位。像素是计算机屏幕上

最小的显示单位。在 Java 的坐标系中,第一个是 x 坐标,表示当前位置为水平方向,距离坐

标原点 x 个像素;第二个是 y坐标,表示当前位置为垂直方向,距离坐标原点 y个像素。在 Appletviewer 中,坐标原点(0,0)正好位于 Applet 菜单的下方。在 Web 浏览器中,原点(0, 0)位于 Applet 执行的矩形区域的左上角(如图 2.5所示)。

Page 35: Java Web动态图表编程

图 2.5 Java坐标系

计算机在屏幕上显示的内容都是由屏幕上的每一个像素组成的。例如,计算机显示器的

分辨率是 800×600,表示计算机屏幕上的每一行由 800 个点组成,共有 600 行,整个计算机

屏幕共有 480 000 个像素。现在的计算机可以支持更高的分辨率,也就是说,屏幕上可以显

示更多的像素。Java Applet 在屏幕上的大小,依赖于屏幕的大小与分辨率。对于大小相同的

屏幕来说,分辨率越高,Java Applet 在屏幕上的显示就越小,这是因为屏幕上的像素更多了。

程序第 17 行,将在坐标(25,25)上绘制“欢迎来到《精通 Java Web动态图表编程》

的世界!”如图 2.4、图 2.5 所示。 Java Applet 可以在 paint 方法中多次调用 drawString 方法,在屏幕的任意位置绘制文本。

我们就可以利用这种方法来显示多行文本。 DrawMultiStringApplet.java 演示了如何绘制多行文本(\chap02\Fig2.3\Fig2.3_02)。通过

调用该目录下的 DrawMultiStringApplet.html 的运行结果如图 2.6 所示。

图 2.6 在 Java Applet上绘制多行文本

本例同第一个 Java Applet 程序相比,仅仅增加了第 18 行。图 2.7 所示的两行文本之所

以能够左对齐, 就是因为第 17行和第 18行的 drawString方法, 都使用了相同的 x 坐标 (25)。

此外,两者使用不同的 y 坐标(第 17 行是 25,第 18 行是 40)。因此在 Applet 相同的水平

位置,不同的垂直位置上显示文本,也就实现了左对齐的功能了。如果把第 17 行和第 18 行

的内容相互交换,则运行的结果也是相同的,这说明文本的位置由其坐标决定。

绘制文本时,不能通过换行符(\n)来实现文本换行的功能。如 DrawMultiStringApplet2. java(\chap02\Fig2.3\Fig2.3_03)和 DrawStringApplet.java 相比,主要是第 17 行语句不同,如下

所示:

g.drawString(“欢迎来到《精通 Java Web动态图表编程》的世界!\n不用不知道,Java 真奇妙!”, 25, 25);

即使绘制的文本中包含有换行符(\n),运行结果如图 2.7 所示,字符串并没有分行显示。

坐标原点(0,0)

25 +X X 轴

25

+Y

Y 轴

(x,y)

欢迎来到《精

坐标(25,25)

坐标(25,40)

Page 36: Java Web动态图表编程

图 2.7 用\n绘制多行文本

提示: 在某些 Java 版本中, 运行结果会在字符串中的换行符位置处显示一个黑色小方框,

表示 drawString方法不能识别该字符。

2.4 绘制线段

如果要在 Java Applet 中绘制一段直线,就需要调用 Graphics 类的 drawLine方法。 先来

看 drawLine的用法,如表 2.4 所示。

表 2.4 drawLine方法

方法 public abstract void drawLine(int x1, int y1, int x2, int y2)

说明 用当前颜色在端点 1(x1,y1)和端点 2(x2,y2)之间绘制一条直线

drawLine方法需要四个参数,表示该线段的两个端点。第一个端点的 x 坐标和 y坐标分

别由第 x1、y1 个参数决定;第二个端点的 x 坐标和 y坐标分别由第 x2、y2 个参数决定。所

有的坐标值都相对于该 Applet 的左上角坐标(0,0)。drawLine方法就是在这两个端点之间

绘制一条直线。

程序 drawLineApplet.java(\chap02\Fig2.4)绘制了 11 条直线、1 个三角形和 1 个矩形。源

程序的运行结果如图 2.8 所示。

图 2.8 drawLineApplet.java 的运行结果

程序第 5 行:

import java.awt.Graphics; // 引入 Graphics 类

导入 Applet所需的 Graphics类 (来自 java.awt包)。 实际上, 如果总是使用完整的 Graphics 类名(java.awt.Graphics),即包含完整的包名和类名,就不需要第 5 行的 import 语句了。也

就是说,如果没有第 5 条语句,那么第 12 行就可以改写成如下形式:

坐标(90,10)

坐标(10,10)

坐标(90,110)

坐标(110,110)

坐标(210,110) 坐标(230,110) 坐标(330,110)

坐标(330,10)

坐标(230,10)

坐标(160,10)

Page 37: Java Web动态图表编程

public void paint(java.awt.Graphics g)

程序第 6 行:

import javax.swing.*; // 引入 javax.swing包中所有的类

这里告诉编译器,Applet 可以使用 javax.swing 包中所有的类。星号(*)表示 javax.swing 包中所有的类都可以被编译器所使用。不过,在程序开始运行时,它并不会将该包中所有的

类都载入内存中,而只是装载程序使用的那些类。本例仅加载了 JApplet 类。

程序第 15 行:

super.paint(g);

调用超类 JApplet 的 paint 方法。编写 Java Applet 程序时,在 paint 方法的第一行设置这

条语句。

程序第 18 行~第 21 行:

for (int i = 1; i <= 11 ; i++ ) g.drawLine(10, 10, 90, i * 10 );

运行了一个循环语句,调用了 11 次 drawLine 方法,在 Applet 上绘制了 11 条线段。这 11 条线段第 1 个端点的坐标都是相同的,即(10,10) ,第 2 个端点坐标的 x 坐标也是完全

相同的。只是 y坐标不同,每循环一次,y坐标依次增加 10 像素。该循环的运行结果如图 2.8 中最左边的图形。

程序第 24 行~第 26 行:

g.drawLine(160, 10, 110, 110); g.drawLine(160, 10, 210, 110); g.drawLine(110, 110, 210, 110);

直接调用 3 次 drawLine 方法,在 Applet 上绘制了由 3 条线段而组成的一个三角形。这

个三角形的三个端点坐标分别是(160,10) 、(110,110)和(210,110)。用 drawLine方法

在两个端点之间绘制一条直线,运行结果如图 2.8 中的三角形。

程序第 29 行~第 32 行:

g.drawLine(230, 10, 230, 110); g.drawLine(230, 10, 330, 10); g.drawLine(330, 10, 330, 110); g.drawLine(230, 110, 330, 110);

直接调用 4 次 drawLine 方法,在 Applet 上绘制了由 4 条线段组成的一个矩形。这个矩

形的 4 个端点坐标分别是:(230,10) 、(330,10) 、(330,110)和(230,110)。用 drawLine 方法绘制该矩形的四条边,运行结果如图 2.8 中右边的矩形。

Page 38: Java Web动态图表编程

2.5 色彩管理

早期的电脑系统是没有色彩管理这个概念的, 直到 90年代苹果电脑推出了 ColorSync 1.0 色彩管理,它局限于苹果电脑之间的色彩控制。1993 年国际色彩联盟(Interational Color Consortium)成立,制定了开放式色彩管理规则ICC,使色彩在不同品牌的电脑输出/输

入设备之间能够互相沟通,产生一致的色彩效果。随后,苹果电脑系统、微软的 Windows 系统、Sun微电脑系统等相继支持 ICC 规则。

2.5.1 色彩基础

当我们从事设计的时候,最想了解的是如何有效地使用色彩。例如,使用什么色彩会产

生什么效果,用什么色彩代表什么性格,色彩如何搭配才美观,等等。要了解这些内容,首

先要认识色彩,知道色彩具有什么物理特性,以及色彩是如何形成的。

1.什么是色彩

我们生活在一个多彩的世界里。白天,在阳光的照耀下,各种色彩争奇斗艳,并随着照

射光的改变而变化无穷。但是,每当黄昏,大地上的景物无论多么鲜艳,都将被夜幕吞没。

在漆黑的夜晚,我们不但看不到物体的颜色,甚至连物体的外形也分辨不清。同样,在暗室

里,我们什么色彩也感觉不到。这些事实告诉我们,没有光就没有色,光是人们感知色彩的

必要条件。色彩来源于光,所以,光是色彩的源泉,色彩是光的表现。要了解色彩产生的原

理,必须对光做进一步的了解。

光的物理性质由它的波长和能量来决定。波长决定了光的颜色,能量决定了光的强度。

光映像到我们的眼睛时,波长不同决定了光的色相不同。波长相同能量不同,则决定了色彩

明暗的不同。

只有波长 380nm~780nm(1nm=10 −6 mm)光的辐射才能引起人们的视觉感知,这段光波

叫做可见光。我们看到的色彩,事实上是以光为媒体的一种感觉。色彩是人在接受光的刺激

后,视网膜的兴奋传送到大脑中枢而产生的印象。

英国科学家牛顿在 1666 年发现,把太阳光经过三棱镜折射,然后投射到白色屏幕上,会

显出一条像彩虹一样美丽的色光带谱,从红色开始,依次是橙、黄、绿、青、蓝、紫七色,

如图 2.9 所示。

这条依次排列的彩色光带称为光谱。这种被分解过的色

光,即使再通过三棱镜也不会分解为其他的色光,我们把光

谱中不能再分解的色光叫做单色光。由单色光混合而成的光

叫做复色光。自然界的太阳光,白炽灯和日光灯发出的光都

是复色光。

2.色彩的三属性

我们不仅观察物体的色彩,同时还会注意到形状、面积、体积,以致该物体的功能和所

处的环境。这些对色彩的感觉都会产生影响。为了寻找规律,人们抽出纯粹色知觉的要素,

认为构成色彩的基本要素是色相、明度、纯度,这就是色彩的三属性。

Ø 色相(Hue)

色相指色的相貌与名称。色轮是表示最基本色相关系的色表。色轮上 90 度角内的几种色彩称作同类色,也叫做近邻色或姐妹色;90 角以外的色

图 2.10 色相

图 2.9 光谱

Page 39: Java Web动态图表编程

彩称为对比色。色轮上相对位置的色叫做补色,也称为相反色,如图 2.10所示。

Ø 明度(Value或 Brightness)

人眼之所以能够看到物体的明暗,是因为物体所反射色光的光量(热量)存在差异。光

量越多,明度越高,反之明度越低。色料的色彩明度,取决于混色中白色和黑色含量的多少,

如图 2.11 所示。

Ø 纯度(Chroma)

所谓色彩的纯度,是指色彩鲜艳与混浊的程度,也称为彩度。色轮上各颜色都是纯色,

纯度最高。色料的混合中,越混合,色彩的纯度越低,如图 2.12所示。

图 2.11 明度变化 图 2.12 纯度变化

3.混色理论

Ø 原色

无法用其他色彩混合得到的基本色彩称为原色。原色按照一定比例混合可以得到各种色

彩。究竟原色有多少种,又是哪几种颜色?历史上不同的学者有不同的说法,但基本上分为

三原色说和四原色说。

色彩的混色有两种类型:加法混色和减法混色。

Ø 加法混色

色光三原色(红、绿、蓝)按照一定的比例混合可以得到各种色光。三原色光等量混合

可以得到白色。色光混色和色料混色刚好相反,色光混色是越混合越明亮,所以称为加法混

色。我们熟悉的电视机和电脑的 CRT显示器产生的色彩方式就属于加法混色,如图 2.13A所

示。

Ø 减法混色

色料三原色(品红、黄、青蓝)按照一定的比例混合可以得到各种色彩。理论上三原色

等量混合可以得到黑色。因为色料越混合越灰暗,所以称为减法混色。水彩、油画、印刷等,

它们产生各种颜色的方法都是减法混色,如图 2.13B所示。

A:加法混色 B:减法混色

图 2.13 色彩的混合

这两种混色方法,通过对相对应的色彩取反操作,就可以相互转换了。

Page 40: Java Web动态图表编程

4.色彩空间及色彩模型

“色彩空间”一词源于英语“Color Space” ,也称做“色域” 。实际上就是各种色彩的集

合,色彩的种类越多,色彩空间越大,能够表现的色彩范围(即色域)越广。对于具体的图

像设备而言,其色彩空间就是它所能表现色彩的总和。

自然界中的色彩千变万化,我们要准确地表示色彩空间就要使用到色彩模型。人们建立

了多种色彩模型,以一维、二维、三维甚至四维空间坐标来规范这些色彩。

最常用的色彩模型有: HSB、 RGB、 CMYK 以及 CIE Lab 等色彩模型。 其中 RGB、 CMYK 是惟一两种专门面向数码设计和出版印刷的色彩模型。这里我们介绍 RGB、CMYK 色彩模

型,认识这两个色彩模型,有助于我们在设计中准确地把握色彩。

所谓 RGB 就是:红(Red)、绿(Green)、蓝(Blue)三种色光原色。RGB 色彩模型的

混色属于加法混色。每种原色的数值越高,色彩越明亮。R、G、B 都为 0 时是黑色,都为 255 时是白色。

RGB 是电脑设计中最直接的色彩表示方法。电脑中的 24 位真彩图像,就是采用 RGB 模型来精确记录色彩的。所以,在电脑中利用 RGB数值可以精确取得某种颜色。

R、G、B 数值和色彩的三属性没有直接的联系,不能揭示色彩之间的关系。所以在进行

配色设计时,RGB 模型就不是那么适用了。 RGB 色彩模型又有 AdobeRGB、AppleRGB、sRGB 等几种,这些 RGB 色彩模型大多与

显示设备、输入设备(数码相机、扫描仪)相关联。

Ø“sRGB”是所谓的“标准 RGB 色彩模型” 。这种色彩空间由微软公司与惠普公司于 1997 年联合确立,被许多软件、硬件厂商所采用,逐步成为许多扫描仪、低档打印机

和软件的默认色彩空间。同样采用 sRGB 色彩空间的设备之间,可以实现色彩相互模

拟,但却是通过牺牲色彩范围来实现各种设备之间色彩的一致性的,这是所有 RGB 色彩空间中最狭窄的一个。

Ø “Apple RGB” 是苹果公司早期为苹果显示器制定的色彩模式。 其色彩范围并不比 sRGB 大多少。这种显示器已经很少使用,这一标准已逐步被淘汰。

Ø“Adobe RGB(1998) ”由 Adobe 公司制定。其最早使用在 Photoshop 5.x 中,被称为 SMPTE­240M。它具备非常大的色彩范围,绝大部分色彩又是设备可呈现的,这一色

彩模型全部包含了 CMYK 的色彩范围,为印刷输出提供了便利,可以更好地还原原

稿的色彩,在出版印刷领域得到了广泛应用。

Ø“ColorMatch RGB”是由 Radius 公司定义的色彩模型,与该公司的 Pressview 显示器

的本机色彩模型相符合。 “Wide Gamut RGB”是用纯谱色原色定义的色彩范围的 RGB 色彩空间。这种空间的色域包括了几乎所有的可见色,比典型的显示器所显示的色域

还要宽。但是,由于这一色彩范围中的很多色彩不能在 RGB 显示器或印刷上准确重

现,所以这一色彩模型并没有太大的实用价值。 RGB 只是光色的表示模型,在印刷行业中,使用另外一种色彩模型 CMYK。CMYK 分

别是青色(Cyan) 、品红(Magenta) 、黄色(Yellow) 、黑色(Black)三种油墨色。每一种颜

色都用百分比来表示,而不是 RGB 那样的 256 级度。

理论上将印刷三原色,即青色(Cyan)、品红(Magenta)、黄色(Yellow)混合之后,

应该可以将红绿蓝光全部吸收而得到纯正的黑色,只是现实生活中找不到这种光线吸收,反

射特性都十分完美的颜料,将三种颜色混合后还是会有些许光线反射出来,呈现暗灰色或深

褐色。事实上除了黑色外,用颜料三原色也无法混合出许多暗色系的颜色,为了弥补这个缺

点,在实际印刷的时候,额外加入黑色的颜料,以解决无法产生黑色的问题。因此就有所谓 CMYK 的色彩模式,K 表示黑色(Black)。

Page 41: Java Web动态图表编程

2.5.2 Java 的色彩管理

Java 以跨平台,与硬件无关的方式支持色彩管理。Java 的色彩管理功能来自于 java.awt 包中的 Color 类。Color 类允许在应用程序中指定自己需要的任意色彩,不用担心因计算机的

硬件设备所支持的绘制方式不同而引起的颜色差别。Java 支持 sRGB 色彩模型,将自动找到

最接近的颜色。 Java 对色彩的管理都被封装在 java.awt.Color 类中。 Color 类提供了对颜色控制的常量和方法。表 2.5 列出了 Color 类中部分预定义的颜色常

量及其 RGB 值。

表 2.5 部分 Color颜色常量及其 RGB值

颜色常量 颜 色 RGB 值

public final static Color ORANGE/orange 橙色 255, 200, 0

public final static Color PINK/pink 粉红色 255, 175, 175

public final static Color CYAN/cyan 青色 0, 255, 255

public final static ColorMAGENTA/magenta 品红色 255, 0, 255

public final static Color YELLOW/yellow 黄色 255, 255, 0

public final static Color RED/red 红色 255, 0, 0

public final static Color BLACK/black 黑色 0, 0, 0

public final static ColorWHITE/white 白色 255, 255, 255

public final static Color LIGHT_GRAY/lightGray 淡灰色 192, 192, 192

颜色常量的名称有两种写法,一种是小写,另一种是大写。我们建议采用大写的形式表

达 Java 的颜色常量,因为它们更符合常量的命名约定。 Color 类通过设置不同的三个原色:红、绿、蓝(RGB)的值而创建各种颜色。这三个

原色的合并值称为 RGB 值。RGB 中的每个值又可以表示为 0~255 范围内的某个整数,或者

表示为 0.0~1.0 之间的浮点数。RGB 中的 R 表示颜色中红色的数值,G 表示绿色的数值,而 B 表示蓝色的数值。数值越大,表示相应的色彩含量就越多。RBG总共可以产生的颜色总数

为 256×256×256,即 16777216 种颜色。表 2.6 列出了 Color 的方法以及 Graphics 类中与之相

关的方法。

表 2.6 Color的方法以及Graphics类中与之相关的方法

Color的构造器及部分方法

public Color(int r, int g, int b) // Color class

创建一种基于红色、绿色和蓝色的颜色,每种颜色的表示方法为 0~255 之间的一个整数

public Color(float r, float g, float b) // Color class

创建一种基于红色、绿色和蓝色的颜色,每种颜色的表示方法为 0~1 之间的一个浮点数

public int getRed() // Color class

返回一个 0~255 之间的,表示 RGB 中红色的整数值

public int getGreen() // Color class

返回一个 0~255 之间的,表示 RGB 中绿色的整数值

public int getBlue() // Color class

返回一个 0~255 之间的,表示 RGB 中蓝色的整数值

Page 42: Java Web动态图表编程

续表 Graphics 类中操作 Color对象的方法

public abstract Color getColor() // Graphics class

返回一个表示在图形环境中当前绘图颜色的对象

public abstract void setColor(Color c) // Graphics class

将当前的绘图颜色设置为 Color 对象 c 所定义的颜色

Color 类提供了两个构造器,一个接收三个 int 参数,另一个接收三个 float 参数。每个参

数分别表示 RGB 中红色、绿色和蓝色的数值。int 的值介于 0~255 之间,float 的值介于 0 和 1 之间。

Color 类的 getRed、getGreen、getBlue方法都返回一个 0~255 之间的一个整数,该整数

分别代表图像环境中,当前颜色对象的红色、绿色和蓝色的数值。 Graphics 类的 getColor 方法,返回一个代表当前绘制颜色的 Color 对象。setColor 方法设

置当前的绘制颜色。

2.6 字体控制

大多数用于字体控制的方法和常量都属于 Font 类。 表 2.7 列出了 Font 类中部分预定义的

字体常量、方法,以及 Graphics 类中与 Font 类相关的一些方法。

表 2.7 Font的方法以及Graphics类中与之相关的方法

Font 的构造器及部分方法

public final static int PLAIN // Font class

表示普通字体风格的常量

public final static int HOLD // Font class

表示粗体字体风格的常量

public final static int ITALIC // Font class

表示斜体字体风格的常量

public Font(String name, int style, int size) // Font class

用指定的字体,风格和大小创建一个新的 Font 对象

public int getStyle() // Font class

返回一个表示当前字体风格的整数值

public int getSize() // Font class

返回一个表示当前字体大小的整数值

public String getName() // Font class

续表 Font 的构造器及部分方法

返回一个表示当前字体的字体名称的字符串

public String getFamily() // Font class

返回一个表示当前字体的字体家族名称的字符串

public boolean isPlain() // Font class

如果字体的风格为普通字体,则返回真,否则返回假

public boolean isBold() // Font class

Page 43: Java Web动态图表编程

如果字体的风格为粗体字体,则返回真,否则返回假

public boolean isItalic() // Font class

如果字体的风格为斜体字体,则返回真,否则返回假

Graphics 类中操作 Font 对象的方法

public Font getFont() // Graphics class

返回一个表示当前字体的 Font 对象引用

public void setFont(Font font) // Graphics class

将当前字体设置为 Font 对象 font 所定义的字体

Font 构造器接收三个参数:字体名称、字体风格和字体大小。字体名称是运行程序的系

统当前所支持的任意字体,如 Courier、Helvetica 和 TimesRoman(注:字体随操作系统的不

同而相差较大,Java 确保了 Courier、Helvetica、TimesRoman、Dialog 和 DialogInput 在不同

的操作系统中有相同的显示)。如果应用程序指定的字体不可用,则 Java 将使用系统的默认

字体来取代它。 Font 类的 static常量 PLAIN、BOLD、ITALIC 用于说明字体风格,分别代表普通、粗体

及斜体字体。字体风格可以组合使用,如 Font.PLAIN+Font.BOLD 等。字体大小用磅(point)

来表示,1磅是 1/72英寸(注:这里说的磅,就是计算机显示器上所显示的一个像素。该像

素的大小,与计算机当前设置的分辨率相关)。 Font 类的 getStyle、getSize、getName和 getFamily四个方法,分别返回一个当前字体对

象的风格、字体大小、字体名称及字体隶属的字体家族的结果。字体对象的风格及字体的大

小,返回的结果都是整数值,而字体名称及字体隶属的字体家族返回的结果为字符串。 Font 类的 isPlain、isBold及 isItalic方法返回一个布尔值。这三个方法分别测试当前字体

是否是普通、粗体或者斜体风格,并返回相应的结果。 Graphics 类的 getFont 方法用于获得当前字体, 即获得在当前图形环境下绘制文本所用的

字体。Graphics 类的 setFont 方法用于设置当前字体,即指定当前图形环境下绘制文本所用的

字体。setFont 方法以一个 Font 对象为参数。 DrawFontWithColor.java(\chap02\Fig2.6\Fig2.6_01)演示了如何使用不同字体和颜色来绘

制文本,运行结果如图 2.14 所示。

图 2.14 用不同颜色及字体绘制文本

程序第 5 行~第 6 行:

import java.awt.*; // 引入 java.awt包中所有的类

import javax.swing.*; // 引入 javax.swing包中所有的类

分别引入了 java.awt 包和 javax.swing包中所有的类。

程序第 16行, 声明了一个名为 text的字符串变量, 并初始化它的值为 “欢迎来到 Java Web

Page 44: Java Web动态图表编程

动态图表编程的世界!”。

程序第 18 行~第 20 行:

g.setColor(Color.BLUE); g.setFont(new Font("宋体", Font.PLAIN, 30)); g.drawString( text, 10, 25 );

程序第 18 行,使用 Graphics 类的 setColor 方法,设置当前图形环境下,绘制图形对象

或文本对象的颜色为蓝色。颜色的设定方法是直接调用 Color 类里面的静态变量。

程序第 19 行,使用 Graphics 类的 setFont 方法,设置当前图形环境下,绘制文本的字体

为宋体,字体风格为普通字体,字体大小为 30磅。

程序第 20 行,在 Applet 坐标(10,25)处,绘制文本“欢迎来到 Java Web 动态图表编

程的世界! ” 。该文本的字体为宋体,字体风格为普通字体,大小为 30 磅,颜色为蓝色。

程序第 22 行~第 24 行、第 26 行~第 28 行、第 30行~第 32 行,使用类似的方法,绘

制了不同颜色及字体的文本。

注意,程序第 22 行:

g.setColor(new Color(0.0f, 0.0f, 0.0f));

设置颜色的方法与第 18 行不同, 采用的是由一个 Color 类接收三个浮点数的构造器所创

建的 Color 类对象。程序第 30 行,采用另外一个接收三个整数的构造器来创建 Color 对象。

每次调用 Color 的构造器后,都将 Color 类的对象传递给 Graphics 类的 setColor 方法,以改

变绘制对象的颜色。程序第 23 行、第 27 行和第 31 行,分别重新设定字体对象,都是调用 Font 类的构造器来创建一个新的 Font 对象,并将该对象传递给 Graphics 类的 setFont 方法。

在调用 Font 类的构造器创建 Font 对象时,都提供了三个参数,分别是字体名称、字体风格

和字体大小。

在设置好绘制图形对象或文本颜色及字体对象后,应用程序就将一直使用定义好的颜色

和字体进行绘制,除非重新定义新的颜色和字体。我们在 DrawFontWithColor.java 程序中一

共设置了四种不同的颜色和字体,运行结果如图 2.15所示,共绘制了四种不同颜色及字体的

文本。

在创建一个 Font 对象之前,我们需要知道本地操作系统中究竟有哪些是可供使用的字

体。ShowFonts.java (\chap02\Fig2.6\Fig2.6_02)将列出本地操作系统中所有可供使用的字体清

单,运行结果如图 2.15 所示。

图 2.15 获取本地操作系统的可用字体

我们调用 java.awt 包中 GraphicsEnvironment 类中的 getAllFonts 方法, 来获得本地操作系

统中所有可供使用的字体清单。getAllFonts 方法,返回一个 Font 对象数组,其中包含了当前

操作系统中所有可用的字体对象。数组中每个 Font 实例,默认的字体大小为 1 磅,一般需要

Page 45: Java Web动态图表编程

重新设置字体大小,否则会因字体太小而无法看清楚。

程序第 10 行~第 12 行: 声明了三个变量。 一个用于绘制字符串对象 text, 一个 Graphics Environment 实例 e,以及一个 Font 数组对象 fonts。

程序第 14 行~第 18 行:

public void init()

e = GraphicsEnvironment.getLocalGraphicsEnvironment(); fonts= e.getAllFonts();

在 Java Applet 默认的第一个调用的 init 方法中,对 GraphicsEnvironment 实例 e,使用 GraphicsEnvironment 的静态方法 getLocalGraphicsEnvironment 进行初始化。然后在 e 中调用 GraphicsEnvironment类的 getAllFonts方法, 对 Font数组对象 fonts进行初始化。 在 Java Applet 类的 paint 方法中,将系统的可用字体全部绘制出来。

程序第 26 行~第 31 行:

for (int i = 0; i< fonts.length ­ 1 ; i++)

g.setFont(new Font(fonts[i].getName(), Font.PLAIN ,15)); g.drawString("Font name:" + fonts[i].getName() +

", 效果:" + text, 40, 30 + 20 * i);

使用一个循环,将 fonts 字体数组中的 Font 对象一一取出,并调用 Font 的构造器来创建

新的字体对象。注意,我们设置的字体风格都是普通字体,字体大小为 15 磅。然后用我们

设定的字体,绘制出一段相同的文本,如图 2.15 所示。某些字体并不能正确地显示中文,

在这种情况下,Java 用一个方框来表示无法用当前设定的字体来绘制该字符。

2.7 绘制矩形

在 Web 图表的绘制中,矩形是最基本的几何图形之一。在第 2.4 节,我们阐述了如何利

用 Graphics 类的 drawLine 方法,通过绘制四条直线来组成一个矩形的例子。其实 Graphics 类提供了直接绘制矩形的各种方法,我们直接调用这些方法,就可以方便地绘制矩形。

2.7.1 绘制普通矩形

1.绘制空心矩形

本节提供一些绘制矩形的Graphics方法。表 2.8列出了绘制普通(空心)矩形的方法。

表 2.8 绘制普通矩形的Graphics方法

Public void drawRect(int x, int y, int width, int height)

用当前颜色绘制一个矩形,其左上角坐标为(x,y),宽度为 width, 高度为 height(如图 2.16A 所示)

public void drawRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight)

用当前颜色绘制一个圆角矩形,其左上角坐标为(x, y),宽度为 width, 高度为 height,arcWidth以及 arcHeight 指定圆角

的弧宽和弧高(如图 2.16B 所示)

Page 46: Java Web动态图表编程

图 2.16 普通(空心)矩形的绘制方法

程序 DrawRectApplet.java(\chap02\Fig2.7\Fig2.7_01)绘制了三个矩形:一个方角矩形,

两个圆角矩形。

程序中使用了一种名为“汉真广标”的字体,程序第 17 行,如果读者的计算机中没有

这种字体,Java 将自动调用默认的系统字体(宋体)来代替“汉真广标” ,源程序的运行结

果如图 2.17所示。

图 2.17 DrawRectApplet.java的运行结果

程序第 17 行:

g.setFont(new Font("汉真广标", Font.PLAIN, 35));

设置字体为“汉真广标” ,字体风格为普通,字体大小为 35 磅。程序第 18 行,用设置

好的“汉真广标”字体在坐标(10,40)处,绘制文本“普通矩形的画法” 。如图 2.17①所示。

程序第 21 行~第 22 行:

g.setColor(Color.RED); g.drawRect(10, 60, 150, 120);

绘制一个左上角顶点坐标为(10,60),宽度为 150 磅,高度为 120 磅的矩形,注意,

程序第 21 行,设置当前的绘制颜色为 Color.RED,所以该矩形的边框是红色(如图 2.17②)。

程序第 25 行~第 28 行:

g.setColor( Color.LIGHT_GRAY ); g.drawRect( 210, 60, 150, 120 ); g.setColor( Color.BLUE ); g.drawRoundRect( 210, 60, 150, 120, 60, 40);

这里我们绘制了一个圆角矩形。为了说明圆角矩形和方角矩形,以及圆角矩形和圆角的

关系,程序第 25 行~第 26 行,绘制了一个边框为浅灰色,宽度为 150磅,高度为 120磅的

普通矩形。程序第 27行,重新设置当前的绘制颜色为蓝色。程序第 28 行,绘制了一个圆角

矩形。注意,圆角矩形的构造器接收 6个参数,前面四个参数和程序第 26行,绘制的矩形语

句完全一样。它定义了圆角矩形左上角坐标,以及宽度和高度,也就是说,圆角矩形的绘制

弧宽(arcWidth)

(x,y) (height) (x,y)

宽度(width) 宽度(width)

弧高

(arcHeight)

(height)

高度

高度

A: drawRect B: drawRoundRect

Page 47: Java Web动态图表编程

区域就在这四个参数决定的矩形之中。圆角矩形四个边角的弧宽和弧高,是由最后两个参数

决定的。本例中,圆弧的宽度为 60 磅,高度为 40 磅。从图 2.17③中,可以看到,最后两个

参数实际是上绘制了一个长边长为 60 磅,短边长为 40 磅的一个椭圆。圆角矩形的弧线的形

状正好和这个椭圆的 1/4 相吻合。如果弧宽和弧高的数值相同,则椭圆就变成了一个正圆形。

程序第 25 行~第 28行:

g.setColor( Color.LIGHT_GRAY ); g.drawRect( 410, 60, 150, 120 ); g.setColor( Color.BLUE ); g.drawRoundRect( 410, 60, 150, 120, 80, 80);

这里我们又画了一个圆角矩形,除了圆角矩形左上角顶点坐标不同外,其他部分基本和

前例相同。另外,程序第 28 行的语句中,drawRoundRect 方法最后两个参数,弧宽和弧高是

相同的,所以,本圆角矩形的四个边角弧线就变成正圆形了,如图 2.17④所示。如果 width、 height、arcWidth 和 arcHeight 的取值完全相同,则最终生成一个正圆;如果最后两个参数的

取值同时为 0,则最终生成一个正方形。

2.绘制实心矩形

绘制实心矩形就是填充矩形。表 2.9 列出了 Graphics 类中绘制实心矩形的方法。

表 2.9 绘制实心矩形的Graphics方法

Public void fillRect(int x, int y, int width, int height)

用当前颜色绘制一个实心矩形,其左上角坐标为(x, y),宽度为 width, 高度为 height

public void fillRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight)

用当前颜色绘制一个实心圆角矩形,其左上角坐标为(x, y),宽度为 width, 高度为 height,arcWidth以及 arcHeight 指定

圆角的弧宽和弧高

程序 FillRectApplet.java (\chap02\Fig2.7\Fig2.7_02)演示了如何绘制实心矩形, 运行结果如

图 2.18 所示。

图 2.18 FillRectApplet.java的运行结果

本程序和前例几乎相同,仅仅是在程序第 22 行将原来的 drawRect 方法替换成了 fillRect 方法:

g.fillRect(10, 60, 150, 120);

将程序第 28行,drawRoundRect 方法替换成 fillRoundRect 方法:

Page 48: Java Web动态图表编程

g.fillRoundRect( 210, 60, 150, 120, 60, 40);

程序第 34 行,进行了同样的替换,这样就实现了填充矩形的功能。

2.7.2 绘制 3D 矩形

1.利用 draw3DRect 来绘制空心 3D矩形

表 2.10 列出了 Graphics 类中绘制空心 3D 矩形的方法。

表 2.10 绘制空心 3D矩形的Graphics方法

Public void draw3DRect(int x, int y, int width, int height, Boolean b)

用当前颜色绘制一个空心 3D 矩形,其左上角坐标为(x, y),宽度为 width,高度为 height,如果 b 为真,则该矩形向外

凸出;如果 b 为假,则该矩形向内凹陷

程序 Draw3DRectApplet.java (\chap02\Fig2.7\Fig2.7_03) 演示了如何绘制空心 3D 矩形,

运行结果如图 2.19 所示。 draw3DRect 方法接收 5 个参数。其中前两个表示矩形左上角顶点坐标,接下来的两个参

数表示矩形的宽度和高度。 最后一个参数是个布尔值, 它表示要绘制的矩形是向外凸出 (true)

的,还是向内凹陷(false)的。 draw3DRect 方法,通过用原色绘制并填充两条矩形边,再用稍深的颜色绘制另外两条矩

形边。通过这种方式来表现三维效果。向外凸出的矩形用原色绘制上面和左边两条矩形边,

而向内凹陷(false)的矩形是用原色绘制下面和右边的两条矩形边。注意,某些颜色并不能

体现三维效果。 Draw3DRectApplet.java 演示了上述方法的用法。为了取得比较明显的三维效果,我们在

程序第 20 行,设置当前的绘制颜色为浅灰色,从程序的实际运行结果来看,获得了明显的三

维效果,如图 2.19 所示。

2.利用 fill3DRect 来绘制实心 3D 矩形

绘制实心 3D 矩形与 draw3DRect 方法所接收的参数一样,只是将 draw3DRect 方法名替

换成了 fill3DRect 方法名。表 2.11 列出了 Graphics 类中绘制实心 3D 矩形的方法。

表 2.11 绘制实心 3D矩形的Graphics方法

Public void fill3DRect(int x, int y, int width, int height, Boolean b)

用当前颜色绘制一个实心 3D 矩形,其左上角坐标为(x, y),宽度为 width,高度为 height,如果 b 为真,则该矩形向外

凸出;如果 b 为假,则该矩形向内凹陷

程序 Fill3DRectApplet.java (\chap02\Fig2.7\Fig2.7_04) 演示了如何绘制实心 3D 矩形,运

行结果如图 2.20 所示。

Page 49: Java Web动态图表编程

图 2.19 FillRectApplet.java的运行结果 图 2.20 Fill3DRectApplet.java的运行结果

3.利用绘制多个实心矩形来生成 3D矩形

上节所讲述的 3D 矩形,只是具有一些凸出和凹陷感觉的平面图形。本节提供一个真正

绘制实心 3D 矩形的方法。我们通过绘制由一组红色、重叠在一起的、有凸出感的 3D 矩形所

组成的图形,来实现绘制 3D矩形的功能。 New3DRectApplet.java(\chap02\Fig2.7\Fig2.7_05)演示了如何绘制不同风格 3D 矩形的方

法,运行结果如图 2.21 所示。

图 2.21 New3DRectApplet.java的运行结果

程序第 20行, 调用 Color的构造器, 创建了一个 Color对象 red。 RGB取值设定为 255、 255、0,表示红色。

程序第 21 行:

g.setColor(red.darker()); //重新设置当前颜色为深红色

调用 Color 类的 darker 方法,重新创建一个比当前颜色更深一些的颜色作为当前颜色。

因为程序第 20 行,创建了一个红色的颜色,所以在程序执行完第 21 行语句后,当前颜色就

重新设置为深红色了。

程序第 23 行:

int thickness = 20; //设置矩形的厚度

程序第 25 行~第 31行:

for (int i = thickness ­ 1; i >= 0; i­­)

g.fill3DRect(10 + i, 80 ­ i, 150, 120, true); g.fill3DRect(210 + i, 60 + i, 150, 120, true);

① ②

③ ④

Page 50: Java Web动态图表编程

g.fill3DRect(30 ­ i, 230 ­ i, 150, 120, true); g.fill3DRect(230 ­ i, 210 + i, 150, 120, true);

调用一个循环方法来绘制 3D矩形。 循环的次数由程序第 23行, 声明的整型变量 thickness 决定。 该数值越大, 3D矩形的厚度就越厚。 在循环的方法体内, 共有四条 fill3DRect绘图语句,

它们分别绘制了四种不同风格的 3D 矩形。这里我们用程序第 27 行,以及如图 2.22 所示的 3D 矩形示意图为例来说明。

Page 51: Java Web动态图表编程

图 2.22 3D矩形的画法示意图

第一次循环,i 的值是 20,所以程序第 27行,执行的是:

g.fill3DRect(30, 60, 150, 120, true);

这样就绘制了一个有凸出感的,左上角坐标为(30,60),宽度为 150,高度为 120,并

且颜色为深红色的 3D 矩形。之后,每循环一次,就绘制一个这样的 3D矩形,只是矩形左上

角的坐标不同而已。最后一次循环,i的值为 0,这时程序第 27行,实际执行的是:

g.fill3DRect(10, 80, 150, 120, true);

这样就绘制了一个有凸出感的,左上角坐标为(10,80),宽度为 150,高度为 120,并

且颜色为深红色的 3D 矩形。这二十个重叠在一起的 3D 矩形,最终就形成了如图 2.21①所示

的图形了。

同理,程序第 28行、第 29行和第 30行分别绘制如图 2.21中②、③、④所示的图形。

读者可以看到,用这种方法绘制的 3D 矩形,既简单又美观。此方法除了可以绘制 3D 矩形以外,还适用于其他 3D图形的绘制,如椭圆、圆弧、粗线等。这些 3D 图形的绘制方法

将在以后的代码中向读者阐述。

注意,程序第 27 行~第 30行,生成不同风格的 3D矩形,同样也可以生成 3D 的直方图

和条形图。

2.8 绘制椭圆

在 Web 图表的绘制中,椭圆也是最基本的几何图形之一。

2.8.1 绘制普通(空心)椭圆

我们直接调用 Graphics 类中提供的绘制椭圆的方法,就可以方便地绘制椭圆。表 2.12 列出了 Graphics 类中绘制椭圆的方法。

表 2.12 绘制椭圆的Graphics方法

Public void drawOval(int x, int y, int width, int height)

用当前颜色绘制一个空心椭圆,其所在的边界矩形的左上角顶点坐标为(x, y),宽度为 width,高度为 height,椭圆与边

界矩形的各边中心内切,如图 2.23 所示

从图 2.24 可以看出,如果椭圆边界矩形的 width和 height 的取值大小相同,该边界矩形

就会变成正方形,而该边界矩形内的椭圆也将变成正圆形。这样,就实现了绘制正圆图形的

功能。

第 1 个矩形(30,60) 最后 1 个矩形(10,80)

weight 150

height 120

thickness 20

Page 52: Java Web动态图表编程

图 2.23 椭圆及其边界矩形 图 2.24 DrawOvalApplet.java的运行结果

程序 DrawOvalApplet.java(\chap02\Fig2.8\Fig2.8_01)演示了如何绘制椭圆及正圆图形,运

行结果如 2.24 所示。

程序第 21 行~第 22 行,绘制了一个浅灰色的矩形,左上角顶点坐标为(10,60),宽

度为 150,高度为 120。

程序第 23 行~第 24 行,绘制了一个边框为红色的椭圆。我们可以看到,程序第 24 行 drawOval 方法和第 22 行 drawRect 方法,接收的参数相同。该椭圆的外接矩形边界和程序第 22 行语句,所绘制的矩形图形完全重叠。图 2.24 展示了椭圆及其边界矩形的关系。

程序第 27 行~第 30行:

g.setColor(Color.LIGHT_GRAY); g.drawRect(210, 60, 120, 120); g.setColor(Color.RED); g.drawOval(210, 60, 120, 120);

这段代码演示了如何绘制正圆形,与程序第 21行~第 24 行的代码内容基本相同。同样

是先绘制了一个和欲绘制椭圆的外接边界矩形完全重叠的浅灰色矩形。注意,程序第 28行, drawRect方法的最后两个参数的取值相同,这样就改变了矩形的形状为正方形。同理,程序第 24行的 drawOval方法的最后两个参数也相同,所以椭圆的形状也就改变成了正圆形。

(x,y)

width

height

Page 53: Java Web动态图表编程

2.8.2 绘制实心椭圆

绘制实心椭圆就是用某种颜色向一个空心椭圆中进行填充操作。我们直接调用 Graphics 类中提供的绘制实心椭圆的方法绘制实心椭圆。 表 2.13 列出了 Graphics 类中绘制实心椭圆的

方法。

表 2.13 绘制实心椭圆的Graphics方法

Public void fillOval(int x, int y, int width, int height)

用当前颜色绘制一个实心椭圆,其所在的边界矩形的左上角顶点坐标为(x, y),宽度为 width,高度为 height,椭圆与边

界矩形的各边中心内切,如图 2.23 所示

程序 FillOvalApplet.java(\chap02\Fig2.8\Fig2.8_02)演示了如何绘制椭圆及正圆图形运行

结果如图 2.25 所示。

图 2.25 FillOvalApplet.java的运行结果

程序第 21 行~第 22 行:绘制了一个浅灰色的矩形,左上角顶点坐标为(10,60),宽

度为 150 像素,高度为 120 像素。

程序第 23 行~第 24行, 绘制了一个红色的实心椭圆。 我们可以看到程序第 24 行 fillOval 方法和第 22行 drawRect 方法,接收的参数相同。该椭圆的外接矩形边界和程序第 22 行语句

所绘制的矩形的图形完全重叠。

程序第 27 行~第 30行:

g.setColor(Color.LIGHT_GRAY); g.drawRect(210, 60, 120, 120); g.setColor(Color.RED); g.drawOval(210, 60, 120, 120);

这段代码演示了如何绘制实心正圆形,与程序第 21行~第 24 行的代码内容基本相同。

也是先绘制了一个与将绘制的椭圆的外接边界矩形完全重叠的浅灰色矩形。注意,程序第 28 行,drawRect 方法的最后两个参数的取值相同,这样就改变了矩形的形状为正方形。同理,

程序第 24 行的 drawOval 方法的最后两个参数也相同, 所以椭圆的形状也就改变成了正圆形。

2.8.3 绘制 3D 椭圆

我们通过绘制一组由不同颜色,并重叠在一起的椭圆所组成的图形,来实现绘制 3D 椭

圆的功能。

程序 Draw3DOvalApplet.java (\chap02\Fig2.8\Fig2.8_03)演示了如何绘制 3D 椭圆, 运行结

Page 54: Java Web动态图表编程

果如图 2.26所示。

图 2.26 Draw3DOvalApplet.java的运行结果

程序第 22 行~第 29行:

for (int i = thickness ­ 1; i >= 0; i­­)

g.setColor(new Color(255, i * 10, i)); g.fillOval(10 + i, 80 ­ i, 150, 120); g.fillOval(210 + i, 60 + i, 150, 120); g.fillOval(30 ­ i, 230 ­ i, 150, 120); g.fillOval(230 ­ i, 210 + i, 150, 120);

循环次数由程序第 20行定义的整型变量 thickness 决定。该数值越大,3D 椭圆的厚度就

越厚。在该循环体内,程序第 24 行:

g.setColor(new Color(255, i * 10, i));

每循环一次, 当前的绘图颜色会根据 RGB的值而重新设定一次。 接着一共是 4 条 fillOval 绘图语句,它们分别绘制了 4 种不同风格的 3D 椭圆。这里我们用程序第 24 行和第 25 行为

例,来说明其绘制过程。

第 1 次循环,i 的值是 20,所以,第 24 行和第 25行执行以下两条语句:

g.setColor(new Color(255, 200, 20)); g.fillOval(30, 60, 150, 120);

第 24 行执行完后,当前颜色就被设置成一种近似于土黄色(RGB 值:255,200,20)

的颜色。接着用当前颜色绘制了一个实心椭圆,该椭圆边界矩形的左上角的顶点坐标在(30, 60),宽度为 150,高度为 120。之后,每循环一次,就会重新设置当前的颜色,再绘制一个

这样的椭圆, 只是椭圆的边界矩形的左上角顶点的坐标不同而已。 最后一次循环, i 的值为 0,

这时,第 24行和第 25 行语句实际执行的是:

g.setColor(new Color(255, 0, 0)); g.fillOval(10, 80, 150, 120);

new Color(255, 0, 0)定义了红色为当前的绘图颜色, 这样就绘制了边界矩形左上角的坐标

在(10,80) ,宽度为 150,高度为 120,并且颜色为红色的椭圆。20 个重叠在一起的椭圆,

最终形成了如图 2.26①所示的图形。该 3D 椭圆的底部是土黄色,从椭圆的底部到顶部,颜

色逐步过渡到红色。

Page 55: Java Web动态图表编程

同理,程序第 26 行、第 27 行和第 28 行分别绘制如图 2.26 中②、③、④所示的图形。

2.8.4 绘制圆柱体

圆柱体可以看成是由上下两个椭圆,再加上一个矩形所组成的图形,如图 2.27 所示。

图 2.27 圆柱体的绘制方法

矩形左上角的顶点坐标是(rectX, rectY)。上下两个椭圆左上角的顶点坐标分别是(ovalX1, ovalY1)及(ovalX2, ovalY2)。

矩形的宽度和椭圆边界矩形的宽度相同,都是 width,矩形的高度为 rectHeight,椭圆的

边界矩形高度为 ovalHeight。

矩形及上下椭圆边界矩形的左上角坐标之间存在如下关系:

Ø 上面一个椭圆边界矩形的左上角坐标

ovalX1 = rectX,

ovalY1 = rectY – ovalHeight / 2 ;

Ø 下面一个椭圆边界矩形的左上角坐标

ovalX2 = rectX,

ovalY2 = ovalY1 + rectHeight ;

所以,只要知道矩形左上角顶点的坐标、矩形的宽度、矩形的高度,以及椭圆的高度就

可以绘制出一个圆柱体了。 DrawCylinderApplet.java(\chap02\Fig2.8\Fig2.8_04)演示了如何利用上面所讲的方法来绘

制一个圆柱体,本程序中使用了下列几个方法:

Ø calculateOvalCoordinate——利用上面的公式计算上、下椭圆的坐标。

Ø resetRectCoordinate——重新设置矩形左上角顶点的坐标。

Ø drawCylinder——绘制圆柱体。

Ø paint——Java Applet默认的自动调用的方法之一, 在此方法体内, 再调用 draw Cylinder 方法,进行实际圆柱体的绘制工作。

DrawCylinderApplet.java 的运行结果如图 2.28所示。

上方椭圆外切矩形顶点 (ovalX1, ovalY1)

矩形左上角顶点 (rectX, rectY)

下方椭圆外切矩形顶点 (ovalX2, ovalY2)

椭圆的高 ovalHeight

矩形的高 rectHeight

椭圆的高 ovalHeight width

Page 56: Java Web动态图表编程

图 2.28 DrawCylinderApplet.java的运行结果

程序第 10 行~第 15 行,定义并初始化了 rectX、rectY、rectWidth、rectHeight,以及 ovalHeight 这 5 个整型变量。分别定义了矩形的左上角坐标,宽度和高度,以及椭圆的高度。

然后声明了 5个整型变量,ovalX1、ovalY1、ovalX2、ovalY2、ovalWidth。分别代表上面一

个椭圆的边界矩形左上角坐标(ovalX1,ovalY1),下面一个椭圆的边界矩形左上角坐标

(ovalX2,ovalY2)以及椭圆的宽度。然后声明并初始化了两个颜色变量,outlineColor 和 fillColor,outlineColor 代表上面一个椭圆的边框颜色,我们设定其默认值为淡灰色。fillColor 代表矩形和下面一个椭圆的填充颜色,我们设定其默认值为红色。

然后,程序执行第 57 行的 paint 方法,程序第 60 行,调用父类的 paint 方法。运行完程

序第 63 行和第 64 行后,在 Applet 上,用 15 磅的“汉真广标”字体绘制了一条文本“圆柱

体画法” 。

之后,程序执行第 67 行:

drawCylinder(g);

然后, 程序转向执行第 35 行的 drawCylinder 方法, 该方法接收一个 Graphics 类的对象 g。

程序第 38 行:

calculateOvalCoordinate();

程序立即转向执行第 18行的 calculateOvalCoordinate 方法,该方法只有一个目的,就是

根据程序第10行~第12行, 定义并初始化的 rectX、 rectY、 rectWidth、 rectHeight以及ovalHeight 这 5 个整型变量值, 来计算并将计算结果赋值给程序第 13 行声明的 ovalX1、 ovalY1、 ovalX2、 ovalY2、ovalWidth这 5 个整型变量。

计算过程在程序第 20行~第 24行:

ovalWidth = this.rectWidth; ovalX1 = this.rectX; ovalY1 = this.rectY ­ ovalHeight / 2; ovalX2 = this.rectX; ovalY2 = ovalY1 + rectHeight;

程序返回到 drawCylinder 方法中,接着继续执行。

程序第 41 行~第 44行:

g.setColor(fillColor); g.fillOval(ovalX2, ovalY2, ovalWidth, ovalHeight);

Page 57: Java Web动态图表编程

g.setColor(outlineColor); g.drawOval(ovalX2, ovalY2, ovalWidth, ovalHeight);

先将红色(fillColor)设置为当前的绘图颜色,然后用当前颜色填充圆柱体中下面的一个

椭圆。首先绘制最下方的一个椭圆,因为 Java 默认的绘图模式是覆盖模式,也就是说,如果

绘制的图形相互重叠,那么默认图形重叠部分就仅仅显示为最后绘制的图形。这里,椭圆在

调用 calculateOvalCoordinate方法后,得到的绘制参数为:

ovalWidth = 100; ovalX1 = 10; ovalY1 = 120 – 40 / 2 = 100; ovalX2 = 10; ovalY2 = 100 + 250 = 350;

填充完椭圆后,我们又绘制了一个淡灰色的空心椭圆。注意,这里绘制空心椭圆的参数

和前面绘制实心椭圆的参数完全相同,程序执行完这 4 条语句,就绘制了一个边缘颜色为淡

灰色,其余颜色为红色的实心椭圆。

程序第 47 行~第 48行:

g.setColor(fillColor); g.fillRect(rectX, rectY, rectWidth, rectHeight);

用红色绘制了一个实心矩形。

程序第 51 行~第 54行,绘制矩形上方的一个椭圆:

g.setColor(g.getColor().darker()); g.fillOval(ovalX1, ovalY1, ovalWidth, ovalHeight); g.setColor(outlineColor); g.drawOval(ovalX1, ovalY1, ovalWidth, ovalHeight);

与程序第 41行~第 44 行,绘制下方椭圆的过程几乎一样,不同的是,为了更好地显示

圆柱体的外观,程序第 51 行,调用了 Graphics 类的 getColor 方法,得到当前的绘图颜色的

对象——红色。然后,调用 Color 类的 darker 方法,生成一个比当前颜色更深(暗)一些的

颜色对象。程序执行完第 51 行后,当前的绘制颜色就转变成了深红色。程序执行完这 4 条语

句,就绘制了一个边缘颜色为淡灰色的深红色实心椭圆。

之后,程序返回到 paint 方法中。这时,一个由上、下两个椭圆及一个矩形组成的圆柱

体就绘制完成了,如图 2.28①所示。

程序继续运行到第 69行:

rectHeight = 350;

对变量 rectHeight 进行直接赋值,以达到重新设置矩形高度的目的。现在,矩形的高度

就被设置为 350 了。

程序第 70 行:

resetRectCoordinate(250, 50);

调用 resetRectCoordinate 方法,重新设定矩形左上角顶点坐标。这里,程序转向

到第 28 行的 resetRectCoordinate方法。

程序第 30 行~第 31行:

this.rectX = rectX; this.rectY = rectY;

将接收到的两个整型参数分别赋值给程序第 10行所声明的变量 rectX和 rectY。这时,左上

角顶点坐标就更新为 250和 50了。之后,程序返回到 paint方法中,继续执行第 72行:

fillColor = Color.BLUE;

Page 58: Java Web动态图表编程

重新设定绘图时的填充颜色为蓝色。

程序第 70 行:

drawCylinder(g);

再次调用 drawCylinder 方法,程序再次转到第 35 行的 drawCylinder 方法。同理,程序

立即转向执行第 18 行的 calculateOvalCoordinate 方法,根据新的矩形参数来重新计算并将计

算结果再次赋值给程序第 13 行,声明的 ovalX1、ovalY1、ovalX2、ovalY2 和 ovalWidth这 5 个整型变量。这次,椭圆在调用 calculateOvalCoordinate方法后,得到的绘制参数为:

ovalWidth = 100(没有改变); ovalX1 = 250; ovalY1 = 50 – 40 / 2 = 30; ovalX2 = 250; ovalY2 = 30 + 350 = 380;

然后,根据新的绘制参数,再次以如下的顺序绘制图形:

(1)下方实心椭圆,颜色为蓝色;

(2)中间的实心矩形,颜色为蓝色;

(3)上方的实心椭圆,颜色为深蓝色,并且该椭圆的边缘为浅灰色。

圆柱体绘制完成后的最终效果如图 2.28②所示。之后,程序再次返回到 paint 方法,并

结束整个程序的运行。

2.9 绘制圆弧

圆弧就是一个空心椭圆的一部分,如图 2.29 所示。

图 2.29 圆弧的绘制方法

左上角顶点(x,y)

arcAngle

外切

矩形

高度 height

外切矩形宽度 width

StartAngle 圆弧起始角度

Page 59: Java Web动态图表编程

2.9.1 绘制普通(空心)圆弧

我们直接调用 Graphics 类中提供的绘制圆弧的方法,来绘制空心圆弧。表 2.14 列出了 Graphics 类中绘制空心圆弧的方法。

表 2.14 绘制圆弧的Graphics方法

Public void drawArc(int x, int y, int width, int height, int startAngle, int arcAngle)

用当前颜色绘制一段空心圆弧,该圆弧所在椭圆的外切矩形的左上角坐标为(x, y),宽度为 width,高度为 heigh,以角

度 startAngle 所在的地方为起点,绘制一段角度为 arcAngle 的空心圆弧,如图 2.29 所示

一段圆弧是椭圆或圆的一部分,圆弧的角度通过度数来衡量。一段圆弧在两个角度之间

绘制,一个为起始角度,另一个为圆弧角度,起始角度为圆弧开始的角度,圆弧角度为圆弧

最后的度数。图 2.30 展示了两段圆弧,左边的坐标系显示圆弧从 0°画到大约 110°,以逆

时针方向画过的弧用正角度衡量。右边的坐标系显示圆弧从 0°画到大约−110°,以顺时针

方向画过的圆弧用负角度衡量。如图 2.30 所示的正、负角度圆弧的绘制方向。 drawArc 方法接收 6 个参数。前两个参数指明了包含圆弧的外切矩形的左上角坐标,第

3 个和第 4 个参数定义了外切矩形的宽和高,第 5 个参数定义了圆弧的开始角度,第 6 个参

数定义了圆弧的角度,并用当前颜色绘制圆弧。 DrawArcsApplet.java (\chap02\Fig2.9\Fig2.9_01)演示了圆弧的画法, 运行结果如图 2.31 所

示。

图 2.30 正、负角度圆弧的绘制方向 图 2.31 几种不同圆弧的绘制方法

程序第 20 行~第 24行,绘制了两个浅灰色的矩形和两个浅灰色的正方形。

程序第 26 行,重新设置当前的绘图颜色为蓝色。

程序第 29 行:

g.drawArc(10, 60, 150, 120, 0, 360);

绘制了一个圆弧。该语句接收 6 个参数中,前 4 个参数和程序第 21 行的 drawRect 方法

的参数完全相同,后两个参数指定圆弧的起始角度在 0°,圆弧的角度为 360°,正好形成

一个首尾封闭的圆弧,最后就形成了一个椭圆,如图 2.31①所示。

程序第 32 行:

正角度 90°

负角度 90°

180° 0°

270°

180° 0°

270°

Page 60: Java Web动态图表编程

g.drawArc(210, 60, 120, 120, 90, 270);

这里圆弧所在椭圆的外切矩形的宽度和高度相同,所以变成了一个正方形。最后两个参

数指定圆弧的起始角度在 90°,并按逆时针方向绘制一个角度为 270°的圆弧,所以圆弧的

起始角度在 90°,终点在 90°+270°=360°,如图 2.31②所示。

程序第 34 行:

g.setColor(Color.RED);

重新设置当前的绘图颜色为红色。

程序第 37 行:

g.drawArc(10, 210, 150, 120, 30, ­ 150);

绘制了一个圆弧。该语句接收 6 个参数中,前 4 个参数和程序第 23 行的 drawRect 方法

的参数完全相同,后两个参数指定圆弧的起始角度在 30°,并按顺时针方向绘制一个角度为 150°的圆弧,如图 2.31③所示。

程序第 40 行:

g.drawArc(210, 210, 120, 120, ­ 90, ­ 270);

同样,这里圆弧所在椭圆的外切矩形的宽度和高度相同,所以变成了一个正方形。最后

两个参数指定圆弧的起始角度在−90° (也就是 270°), 并按顺时针方向绘制一个角度为 270 °的圆弧,所以圆弧的起始角度在 270°,终点在 270°−270°=0°,如图 2.31④所示。

2.9.2 绘制实心圆弧

Graphics类中提供了绘制圆弧的 fillArc方法,可以非常方便地绘制实心圆弧。表 2.15 列出

了 Graphics 类中绘制实心圆弧的方法。

表 2.15 绘制圆弧的Graphics方法

Public void fillArc(int x, int y, int width, int height, int startAngle, int arcAngle)

用当前颜色填充一段圆弧,该实心圆弧所在椭圆的外切矩形的左上角坐标为(x, y),宽度为 width和高度为 heigh,以角

度 startAngle 所在的地方为起点,绘制一段角度为 arcAngle 的实心圆弧

fillArc方法同样接收 6 个参数。前两个参数指明了包含圆弧的外切矩形的左上角坐标,

第 3 个和第 4 个参数定义了外切矩形的宽和高,第 5个参数定义了圆弧的开始角度,第 6 个

参数定义了圆弧的角度,并用当前颜色填充圆弧。 FillArcsApplet.java(\chap02\Fig2.9\Fig2.9_02)演示了实心圆弧的画法,程序运行结果如图

2.32 所示。 FillArcsApplet.java 和前面介绍的 FillArcsApplet.java 程序几乎完全相同, 只是将 drawArc

方法替换成 fillArc方法。程序第 29 行、第 32 行、第 37 行和第 40 行分别调用 fillArc方法,

绘制了不同形式的实心圆弧。程序的运行结果如图 2.32 所示。

Page 61: Java Web动态图表编程

图 2.32 几种实心圆弧的绘制方法

2.9.3 绘制 3D 圆弧

Java.awt 包中的 Graphics 类中,并没有提供绘制 3D 圆弧的方法,我们可以利用绘制多

个重叠在一起的实心圆弧来实现这一功能。Draw3DArcApplet.java(chap02\Fig2.9\ Fig2.9 _03) 演示了如何绘制 3D圆弧,运行结果如图 2.33所示。

Page 62: Java Web动态图表编程

图 2.33 3D圆弧的绘制方法

程序第 19 行:

int thickness = 20; //设置 3D圆弧的厚度

声明了一个整型变量 thickness,并初始化其值为 20。thickness 代表 3D 圆弧的厚度。

程序第 21 行~第 36行,是一个循环,该循环的次数由前面设定的变量 thickness 决定,

本例共循环 20 次。

程序第 23 行~第 30行:

if (i == 0)

g.setColor(new Color(255, 200, 20)); else

g.setColor(new Color(255, i * 10, i));

每一次循环,就重新设定当前实心圆弧的填充颜色。注意,本例设定最后绘制的实心圆

弧的绘制颜色为 new Color (255, 200, 20) , 其余圆弧的绘制颜色则根据 i 值的变化而变化 (程

序第 29 行)。

程序第 33 行~第 35行,绘制 3D 圆弧的核心语句。

程序第 33 行:

g.fillArc(40, 60 + i, 250, 160, 10, 70);

按从下到上的顺序(因为 i的值是由大到小递减的)填充圆弧。圆弧的起始角度为 10°,

圆弧的弧度为 70°。绘制结果如图 2.33①所示。同理,程序第 34行和第 35行,分别绘制了

两个如图 2.33②及 2.33③所示的不同的 3D 圆弧。

2.10 绘制多边形

我们绘制的很多图形实际上不全都是规则的图形,在这种情况下,Java 绘制多边形和折

线的功能就可以大显身手了。此外,绘制某些 3D 图形时,我们也可以借助多边形来实现。

Page 63: Java Web动态图表编程

Graphics 类中提供了绘制多边形的 drawPolygon方法,可以方便地绘制多边形。

2.10.1 绘制空心多边形

表 2.16 列出了 Graphics 类中绘制空心多边形的方法。

表 2.16 绘制空心多边形的Graphics方法

public abstract void drawPolygon(int[] xPoints, int[] yPoints, int nPoints)

使用当前颜色绘制 xPoints、yPoints 数组指定的多边形,每个顶点的 x 坐标由 xPoints 数组指定,每个顶点的 y 坐标由

yPoints数组指定

drawPolygon 方法用来绘制一个多边形,多边形(polygon)是多条边相互连接的封闭图

形。drawPolygon 方法接收 3 个参数:一个包含 x 坐标的整型数组,一个包含 y 坐标的整型

数组,最后是多边形顶点的数目。注意,在多边形中,最后一个顶点会自动连接第一个顶点,

也就是说,最后一个顶点的坐标和第一个顶点的坐标不必相同。 DrawPolygon.java(\chap02\Fig2.10\Fig2.10_01)演示了多边形的画法,运行结果如图 2.34所

示。

图 2.34 DrawPolygon.java的运行结果

程序绘制两个多边形,第 1 个多边形是五角星。程序第 21 行~第 36 行:

int[] xValues = 55, 67, 109, 73, 83, 55, 27, 37, 1, 43 ; int[] yValues = 70, 106, 106, 124, 166, 142, 166, 124, 106, 106 ;

声明了两个数组 xValues 和 yValues,分别表示将绘制的五角星每一个顶点的横坐标及纵

坐标。

程序第 24 行:

Polygon fiveStar = new Polygon(xValues, yValues, xValues.length);

调用了 java.lang包中的 Polygon的构造器,创建了一个 Polygon类的实例 fiveStar。该构

造器接收 3个参数,前面两个参数是程序第 22行和第 23 行定义好的两个整型数组,代表多

边形上所有顶点的 x 坐标和 y 坐标的集合,最后一个参数是 1 个整型变量,表示该多边形有

多少个顶点。因为五角星共有 10个顶点,所以程序第 24行也可写成如下形成:

Polygon fiveStar = new Polygon(xValues, yValues, 10);

而数组 xValues 和 yValues 正好包含了 10 个元素,所以我们直接调用数组的静态变量 length,用 xValues.length的方式,来获得数组中所有元素的个数。

程序第 25 行:

Page 64: Java Web动态图表编程

g.drawPolygon(fiveStar);

将刚刚创建的 Polygon对象 fiveStar,传递给 Graphics 类的 drawPolygon方法。然后,程

序就在这里执行绘制多边形的工作。

程序第 28 行~第 36行,用另外一种方式绘制了一个多边形。

程序第 28 行:

Polygon sharp = new Polygon();

调用 Polygon类默认的构造器,创建了一个 Polygon 类的实例 sharp,默认的构造器不接

收任何参数。由 Polygon 类默认的构造器,创建的 Polygon 类的实例是不包括任何顶点的,

需要手工添加该多边形的各个顶点的坐标。

程序第 29 行~第 33行:

sharp.addPoint(185, 45); sharp.addPoint(195, 110); sharp.addPoint(290, 160); sharp.addPoint(220, 180); sharp.addPoint(150, 140);

调用 Polygon 类的 addPoint 方法,向对象 sharp 中添加顶点,这里共添加了 5 个顶点。

程序第 35 行,重新设定当前的绘图颜色为红色。

程序第 36 行:

g.drawPolygon(sharp);

将创建好的 Polygon实例 sharp再次传递给Graphics类的 drawPolygon方法, 绘制多边形。

2.10.2 绘制实心多边形

表 2.17 列出了 Graphics 类中绘制实心多边形的方法。

表 2.17 绘制实心多边形的Graphics方法

public abstract void fillPolygon(int[] xPoints, int[] yPoints, int nPoints)

使用当前颜色绘制并填充由 xPoints、yPoints数组指定的多边形,每个顶点的 x坐标由 xPoints数组指定,每个顶点的 y

坐标由 yPoints数组指定

fillPolygon 方法,用当前的绘图颜色来填充一个实心多边形。 fillPolygon 方法和 drawPolygon方法一样,都接收 3个参数——一个包含 x 坐标的整型数组,一个包含 y坐标的

整型数组,最后是表示该多边形顶点的数目。 FillPolygon.java (\chap02\Fig2.10\Fig2.10_02)演示了实心多边形的画法,运行结果如图

2.35 所示。

Page 65: Java Web动态图表编程

图 2.35 FillPolygon.java的运行结果

FillPolygon.java 和 DrawPolygon.java 的源程序几乎完全相同。只是在 FillPolygon.java 程

序中, 将原来在DrawPolygon.java源程序中的 drawPolygon方法全部替换成了 fillPolygon方法。

程序其余部分代码没有变化。

2.10.3 绘制折线

使用 drawPolyline 的方法绘制一条折线,折线是一条由多条线段连接起来的首尾不相连

的开放图形。如果折线的第一个顶点和最后一个顶点的坐标相同,那么该折线也就变成了一

个封闭的图形——多边形。

表 2.18 列出了 Graphics 类中绘制折线的方法。

表 2.18 绘制折线的Graphics方法

public abstract void drawPolyline (int[] xPoints, int[] yPoints, int nPoints)

使用当前颜色绘制一系列相互连接的线段,线段端点的坐标相应由数组 xPoints 和 yPoints 指定,线段的端点个数由

nPoints指定

drawPolyline方法和 drawPolygon方法一样, 都接收 3个参数——第 1 个参数包含 x坐标

的整型数组,第 2 个参数包含 y坐标的整型数组,最后 1 个参数指明该折线总共有多少个端

点。 DrawPolyline.java(\chap02\Fig2.10\Fig2.10_03)演示了实心多边形的画法,运行结果如图

2.36 所示。

图 2.36 DrawPolylin.java的运行结果

2.10.4 绘制三角形(箭头)

本节将以绘制 4 种不同风格的等腰三角形(箭头)为例。

假设等腰三角形 ABC 的底边 AB 的边长为 L,底角的度数为α,则等腰三角形 ABC 的

三个顶点的坐标 A、B 和 C 分别可以用以下公式计算,如图 2.37所示。 1.等腰三角形①:

Ax =x−L/2,Ay =y Bx =x+L/2,By =y Cx =x,Cy =y−tgα*L/2

Page 66: Java Web动态图表编程

图 2.37 DrawPolylin.java的运行结果

2.等腰三角形②:

Ax =x,Ay =y−L/2 Bx =x,By =y +L/2 Cx =x+tgα*L/2,Cy =y

3.等腰三角形③:

Ax =x−L/2,Ay =y Bx =x+L/2,By =y Cx =x,Cy =y+tgα*L/2

4.等腰三角形④:

Ax =x,Ay =y−L/2 Bx =x,By =y+L/2 Cx =x−tgα*L/2,Cy =y

DrawTriangleApplet.java(\chap02\Fig2.10\Fig2.10_04)演示了如何利用上述计算来绘制三

角形,运行结果如图 2.38 所示。

源程序共包含了 5 种方法,如下所示:

Ø drawTrigangle Ø setBaseLine Ø setFillColor Ø setOutlineColor Ø paint

C

A B

A

B

C

A B

C

C

A

B

α α

α

α

α α α

α

底边 L 底边 L

中点坐标

(x,y)

底边 L

底边 L

中点坐标

(x,y)

中点坐标

(x,y)

中点坐标

(x,y)

Page 67: Java Web动态图表编程

图 2.38 DrawPolylin.java的运行结果

5 种方法分别执行了不同的任务。paint 方法是 Java Applet 自动执行的方法之一,我们在 paint 方法内再调用其他的方法以完成绘制不同大小、不同方向、不同风格的三角形。 drawTrigangle方法,负责进行计算三角形 3 个顶点坐标,并根据用户提供的参数来决定三角

形的填充颜色、三角形轮廓的绘制颜色,以及三角形的方向等信息来进行图像绘制。 setBaseLine负责设置等腰三角形底边的长度。 setFillColor负责设置等腰三角形的填充颜色 (即

绘制实心三角形的颜色)。setOutlineColor 负责设置绘制等腰三角形轮廓的颜色(即绘制空心

三角形的颜色)。

程序第 9 行~第 10 行:

int[] TriangleXValues = new int[3]; int[] TriangleYValues = new int[3];

声明并初始化两个整型数组。TriangleXValues 代表三角形 3 个顶点的横坐标,TriangleY Values 代表三角形 3 个顶点的纵坐标。

程序第 11 行:

int alpha = 60;

声明了一个值为 60 的整型变量 alpha。alpha 表示等腰三角形底角。这里设置默认底角 alpha 的度数为 60°。

程序第 12 行:

int baseLine = 80;

声明了一个值为 80 的整型变量 baseLine,baseLine表示等腰三角形底边。这里设置默认

底边 baseLine的长度为 80。

程序第 15 行:

final int UP = 1, RIGHT = 2, DOWN = 3, LEFT = 4;

声明了 4 个整型变量 UP、RIGHT、DOWN、LEFT,代表三角形的方向。分别如图 2.38 所示的向上、向右、向下和向左的三角形。这里我们使用了 final 关键字,表示这些变量的值

在声明后,不能再被修改(相当于常量)。

程序第 18 行:

final int OUTLINE = 1, FILL = 2, MIXED = 3;

这里声明 3 个整型变量 OUTLINE、FILL 和 MIXED,分别代表三角形的绘制风格,分

别表示为:

Ø OUTLINE:绘制三角形轮廓(即绘制空心三角形);

Ø FILL:绘制实心三角形;

Page 68: Java Web动态图表编程

Ø MIXED:绘制一个带轮廓的实心三角形(即先绘制一个实心三角形,然后在其上再

绘制一个空心三角形)。

程序第 20 行~第 21行:

Color fillColor = Color.BLACK; // 默认的填充颜色

Color outlineColor = Color.BLACK; //默认的轮廓颜色

声明了两个 Color 类的对象实例,fillColor 和 outlineColor 分别表示绘制实心三角形和三

角形轮廓的颜色。两者的默认绘制颜色被设置为黑色。

当程序运行时,完成上述变量的创建工作后,首先执行程序第 108 行默认的 paint 方法。

在该方法体内,首先进行图形标题的绘制工作,即程序第 115 行,绘制“三角形的画法”这

个文本。

程序第 117 行:

setFillColor(Color.RED);

设置填充实心三角形的颜色为红色,注意,这里没有设置绘制三角形轮廓的颜色,所有 outlineColor 对象仍然保持黑色不变。

程序第 120行:

drawTrigangle(50, 120, UP, OUTLINE, g);

调用 drawTrigangle方法,并传递 5个参数。前面 4个参数是整型参数,最后 1 个参数是

由 applet 容器创建的 Graphics 对象 g。前面 4 个参数的具体含义是:

Ø 50,100:表示将要绘制的三角形底边中点的坐标为(50,100) ;

Ø UP:表示该三角形的形状是一个向上的三角形;

Ø OUTLINE:表示仅绘制该三角形的轮廓(即空心三角形)。

然后,程序转向到第 23行,并执行 drawTrigangle方法。drawTrigangle方法,接收刚刚

由 paint 方法调用并传递过来的 5 个参数。

在 drawTrigangle方法体内,首先运行程序第 27行:

int offset = (int)(baseLine / 2 * Math.tan(alpha * Math.PI / 180));

定义了一个局部的整型变量 offset。offset 是一个偏移量,它表示三角形顶点 C 到底边中

点的距离。套用图 2.37 所示的计算方法,可知该偏移量等于底角α乘以底边长的一半,即:

offset = tgα* L/2

因为,Java.lang.Math包中的 tan方法接收 double类型的弧度值,所以这里使用:

alpha * Math.PI / 180

将角度值转化成弧度值。执行:

Math.tan(alpha * 180 / Math.PI))

得到的结果是一个 double类型的值,再用(int)的方法,将其从双精度型数值强制转换

为整型数值,并将转换后的结果赋值给局部整型变量 offset。

程序第 30行,根据用户传递过来的第 3个整型参数,进入一个 switch多重选择语句:

switch (triangleDirection)

根据用户传递过来的 triangleDirection值,进行下一步操作,即计算三角形 3 个顶点的坐

标。这里程序传入 triangleDirection的值为 UP。

根据程序第 15 行的定义,UP 的值为 1,所以,程序会执行第 32行~第 39 行:

Page 69: Java Web动态图表编程

case 1: TriangleXValues[0] = x ­ baseLine / 2; TriangleYValues[0] = y; TriangleXValues[1] = x + baseLine / 2; TriangleYValues[1] = y; TriangleXValues[2] = x; TriangleYValues[2] = y ­ offset; break;

根据前面介绍的计算公式,计算出形状向上的三角形的 3 个顶点的坐标,并将这 3个坐

标分别赋值给数组 TriangleXValues 和 TriangleYValues。计算完 3 个顶点的坐标之后,执行

程序第 39 行的 break 语句。退出当前的 switch 语句后,程序进入第 70 行,另外一个 switch 语句:

switch (triangleStryle)

根据用户传递的第 4 个参数 triangleStyle 进行三角形绘制风格的选择。当前用户传递的

是“OUTLINE” ,根据程序第 18行的定义,OUTLINE 的值为 1。所以,程序执行第 72~第 75 行:

case 1: g.setColor(outlineColor); g.drawPolygon(TriangleXValues, TriangleYValues, 3); break;

程序第 73 行,先设置当前的绘制颜色为 Color 类的对象 outlineColor 所指定的颜色。 outlineColor 在程序第 21行声明后并没有改变,仍然是黑色。

程序第 74 行:

g.drawPolygon(TriangleXValues, TriangleYValues, 3);

调用 Graphics 类的 drawPloyon方法, 绘制一个空心多边形。 执行完程序第 74行语句后,

程序在 Applet 上绘制出一个形状为向上的空心三角形,颜色为黑色,并且该三角形底边的长

度为默认的 80 磅。实际运行结果如图 2.38①所示。

程序第 75 行是一个 break语句。在这里,程序跳出 switch语句,然后,再返回到先前的 paint 方法里。程序运行第 123行:

drawTrigangle(100, 120, RIGHT, FILL, g);

再次调用并转向至程序第 23 行,执行 drawTrigangle 方法。同理,drawTrigangle方法先

计算三角形顶点 C 距离底边中心的偏离量 offset。

因为 RIGHT的值为 2(见程序第 15 行),所以。程序执行第 30 行的 switch语句中的第 41 行~第 48行:

case 2: TriangleXValues[0] = x; TriangleYValues[0] = y ­ baseLine / 2; TriangleXValues[1] = x; TriangleYValues[1] = y + baseLine / 2; TriangleXValues[2] = x + offset; TriangleYValues[2] = y; break;

对数组 TriangleXValues 和 TriangleYValues 再次进行赋值后,又进入程序第 70 行,另外

一个 switch语句。

因为整型参数 Fill 的值为 2,所以,执行该 switch语句中的第 77行~第 80 行:

case 2:

Page 70: Java Web动态图表编程

g.setColor(fillColor); g.fillPolygon(TriangleXValues, TriangleYValues, 3); break;

程序第 78 行,重新设置当前的绘图颜色。因为 fillColor 已经在程序第 117 行,被重新设

置成了红色, 当前的绘制颜色就已经变成了红色。 程序第 79 行, 调用 Graphics 类的 fillPolygon 方法,再次绘制三角形,这次绘制的三角形是一个形状向右、颜色为红色并且底边长度为 80 磅的实心三角形,实际运行效果如图 2.38②所示。

程序再次返回到 paint 方法,继续执行下面的语句,程序第 125行:

setBaseLine(40);

调用 setBaseLine方法,程序转向到第 92 行~第 95 行:

public void setBaseLine(int baseLine)

this.baseLine = baseLine;

setBaseLine 方法很简单,只是将用户在 setBaseLine 方法中传递整型参数 baseLine 的值

重新赋值给类的数据成员 baseLine。注意,这里用 this 这个关键字来区分用户提交的参数 baseLine和类的数据成员 baseLine。这时,类的数据成员变量 baseLine的值就改变为 40了。

接着程序返回到 paint 方法中,继续执行下面的语句。

程序第 126行:

setOutlineColor(Color.BLUE);

调用 setOutlineColor 方法,程序会转向到第 102行~105 行:

public void setOutlineColor(Color outlineColor)

this.outlineColor = outlineColor;

同样地,用 this 关键字将用户传递的 outlineColor 对象赋值给系统变量 outlineColor。此

时,系统 outlineColor 变量就被设置为 Color.BLUE。然后,程序再返回到 paint 方法,执行第 127 行:

setFillColor(Color.GREEN);

调用 setOutlineColor 方法,程序转向到第 97 行~第 100 行:

public void setFillColor(Color fillColor)

this.fillColor = fillColor;

同样地,用 this 关键字将用户传递的 fillColor 对象赋值给系统变量 fillColor。这时,系

统变量 fillColor 就被设置成为 Color.GREEN。

然后,程序再返回到 paint 方法,执行第 130 行:

drawTrigangle(200, 120, DOWN, MIXED, g);

再次调用 drawTrigangle方法,重复前面我们所讲的步骤,运行完程序第 130 行,程序会

在 Applet 上绘制一个颜色为绿色、向下的、三角形的轮廓颜色为蓝色、底边长度为 40 磅的

实心三角形。实际运行效果如图 2.38③所示。

同理,程序第 132 行~第 136行:

setBaseLine(60);

Page 71: Java Web动态图表编程

setFillColor(Color.ORANGE);

// 绘制三角形 4 drawTrigangle(300, 120, LEFT, FILL, g);

根据前面所介绍的绘制过程,我们可以得到一个底边长度为 60 磅、颜色为橙色、方向

向左的实心三角形,如图 2.38④所示。

2.10.5 绘制平行四边形及立方体

平行四边形是创建图形三维效果的一个有效途径, 在Web 图表的设计中起着非常重要的

作用。比如说,一个立方体(如 2.7.2.节“利用绘制多个实心矩形来生成 3D 矩形”所绘制的 3D 矩形)由一个正面的矩形,顶部一个平行四边形以及右边侧面的一个平行四边形组成,如

图 2.39 所示。

图 2.40 是立方体对象 Cube 的结构视图。我们把立方体对象 Cube 分解成 3 部分,如图 2.40 所示。

图 2.39 立方体与平行四边形之间的关系 图 2.40 立方体对象 Cube的结构视图

我们假设以立方体左下角的顶点为基准点 P0,P0 的坐标为(baseXPts,baseYPts),立方

体高度为 height,宽度为 weight,厚度为 thickness,顶部平行四边形 pTop斜边与水平边的夹

角为α。该立方体各个顶点与基准点 P0 的关系就可以用下面的数学公式来表示。 1.正面矩形 rFront 的 4 个端点分别是:

P1x=baseXPts+width P1y=baseYPts P2x=P1x P2y=baseYPts−height P6x=baseXPts P6y=P2y

正面矩形 rFront 可以使用前面所学的 Graphics 类的相关方法完成:

g.drawRect(P6x,P6y,width,height)或者 g.fillRect(P6x,P6y,width,height)或者 g.fill3DRect(P6x,P6y,width,height,true)或者

g.fill3DRect(P6x,P6y,width,height,false)

2.顶部平行四边形 pTop 4 个端点由 P6、P2、P3 和 P5 构成,P2 和 P6 已经计算过了,只

需要计算 P3 和 P5,分别是:

顶部平行四边形

侧面

平行

四边

正面矩形

P5 P3

P6 P2

P1 P0

α

pTop

pRight

高(height) rFront

基准点

(baseXPts, baseYPts)

厚度 thickness

宽(width)

P4

Page 72: Java Web动态图表编程

P3x=P2x+cosα*thickness P3y=P2y−sinα*thickness

P5x=P6x+cosα*thickness P5y=P3y

通过前面绘制多边形的方法来绘制这个平行四边形。将 4 个端点 P2、P3、P5 和 P6 的坐标

值分别放入两个整型数组 pTopX 和 pTopY,然后,使用以下方法来绘制:

g. drawPolygon(pTopX,pTopY,4)或者 g. filldrawPolygon(pTopX,pTopY,4)

3.右侧面平行四边形 pRight 的 4 个端点由 P1、P2、P3 和 P4 构成,P1、P2 和 P3 已经计算

过了,这里只需计算 P4 :

P4x=P3x P4y=P3y+height

同理,将 pRight 的 4 个端点 P1、P2、P3 和 P4 的坐标值分别放入两个整型数组 pRightX 和 pRightY,然后,使用以下方法来绘制:

g. drawPolygon(pRightX,pRightY,4)或者 g. filldrawPolygon(pRightX,pRightY,4)

我们可以按照下面的步骤来绘制这个立方体:

(1)先计算出各个端点的坐标;

(2)绘制顶部的平行四边形 pTop;

(3)绘制右侧面的平行四边形 pRight;

(4)绘制正面的矩形 rFront。

源程序 DrawCubeApplet.java(\chap02\Fig2.10\Fig2.10_05)演示了如何绘制一个立方体, 运

行结果如图 2.41 所示。该程序采用了同上一节讲述的 DrawTriangleApplet.java 相类似的结构

来完成绘制工作。其中主要的方法、方法名及完成的相应功能如下:

图 2.41 DrawCubeApplet.java的运行结果

Ø setOutlineColor:设置整个立方体的轮廓颜色。

Ø setFrontFillColor:设置正面实心矩形的填充颜色。

Ø setTopFillColor:设置顶部实心平行四边形的填充颜色。

Ø setRightFillColor:设置右侧部实心平行四边形的填充颜色。

Ø setBasePoint:设置基准点的坐标。

Page 73: Java Web动态图表编程

Ø setWidth:设置立方体的宽度。

Ø setHeight:设置立方体的高度。

Ø setThickness:设置立方体的厚度。

Ø setAngle:设置平行四边形的夹角α;

Ø calculateOffset:计算 cosα* thickness 和 sinα*thickness 的值。

Ø buildRFront:计算绘制正面矩形所需的数据。

Ø buildPTop:计算绘制顶部平行四边形所需的数据。

Ø buildPRight:计算绘制右侧部平行四边性所需的数据。

Ø drawFront:绘制正面矩形。

Ø drawTop:绘制顶部平行四边形。

Ø drawRight:绘制右侧部平行四边形。

Ø DrawCube:通过调用上面的方法来绘制完整的平行四边形。

程序第 9 行:

int baseXPts = 10, baseYPts = 200; // 立方体基准点坐标

声明了两个整型变量 baseXPts 和 baseYPts,表示立方体左下角那一点的坐标值,就是图 2.40 中所示的点 P0。我们默认设置该点的坐标为(10, 200)。

程序第 10 行~第 12行:

int width = 100; // 立方体的宽度

int height = 120; // 立方体的高度

int thickness = 30; // 立方体的厚度

声明 3 个整型变量 width、height 和 thickness,分别表示立方体的宽度、高度和厚度。

程序第 14 行~第 17行:

int alpha = 45; // 平行四边形的夹角

int offsetX = 0; // 偏移量 X int offsetY = 0; // 偏移量 Y final int PARALLELOGRAM_POINT_NUMBER = 4; // 平行四边形的端点数目

程序第 14 行,声明了整型变量 alpha,将图 2.40 中立方体顶部平行四边形斜边和水平边

的夹角α,设置为 45°。程序第 15 行和第 16 行,声明变量 offsetX,表示图 2.40 中点 P5 和

点 P6 之间水平方向上的距离,offsetY 表示点 P5 和 P6 之间在垂直方向上的距离。同理,点 P3 和 P2 之间也存在着这种关系。程序第 17 行,使用了一个 final 修饰的整型变量 PARALLELOGRAM_POINT_NUMBER,表示要绘制的多边形的端点数目。因为平行四边形

总共有 4 个端点,所以,这里我们将其值设置为 4。

程序第 19 行~第 27行:

int[] pTopX = new int[PARALLELOGRAM_POINT_NUMBER]; // 顶部平行四边形端点的 Y坐标数组 int[] pTopY = new int[PARALLELOGRAM_POINT_NUMBER];

// 右侧部平行四边形端点的 X坐标数组 int[] pRightX = new int[PARALLELOGRAM_POINT_NUMBER]; // 右侧部平行四边形端点的 Y坐标数组 int[] pRightY = new int[PARALLELOGRAM_POINT_NUMBER];

程序第 19 行~第 21 行,用整型数组 pTopX 和 pTopY 分别保存顶部平行四边形 4 个端

点的横坐标和纵坐标。程序第 25 行~第 27 行,用整型数组 pRightX 和 pRightY 分别保存右

侧部平行四边形 4 个端点的横坐标和纵坐标。

程序第 33 行:

Page 74: Java Web动态图表编程

final int OUTLINE = 1, FILL = 2, MIXED = 3;

声明 3 个整型变量 OUTLINE、FILL和 MIXED,分别代表立方体的绘制风格:

Ø OUTLINE:绘制立方体轮廓(即绘制空心立方体);

Ø FILL:绘制实心立方体;

Ø MIXED:绘制一个带轮廓的实心立方体(即先绘制一个实心立方体,然后在其上再

绘制一个空心的立方体)。

程序第 35 行~第 37行:

Color rFrontFillColor = new Color(178, 0, 0); // 默认的填充颜色

Color pTopFillColor = Color.RED; // 默认的填充颜色

Color pRightFillColor = new Color(124, 0, 0); // 默认的填充颜色

声明并初始化了 3 个 Color 类的对象。分别表示立方体正面的矩形、顶部平行四边形及

右侧部平行四边形默认的填充颜色。RGB 为(178,0,0)和(124,0,0)表示两种红色。

它们都比 Color.RED 所表示的红色要深一些,红色立方体的正面和右侧面如图 2.41①所示。

提示:本书提到的“填充” ,表示用当前颜色绘制一个实心的图形对象,而“描绘”表

示用当前颜色绘制一个空心的图形对象(即绘制图形的轮廓)。

程序第 38 行:

Color outlineColor = Color.LIGHT_GRAY; //默认的立方体轮廓颜色

设置默认描绘立方体的颜色为淡灰色。

程序首先对上述变量进行初始化,然后,直接调用程序第 190 行的 paint 方法,再回到 paint 的方法体中。

绘制完标题后,程序第 200 行:

DrawCube(FILL, g);

调用 DrawCube方法来绘制一个立方体。之后,程序立即转到第 140 行,执行 DrawCube 方法。DrawCube方法需要两个参数,第 1 个是整型参数,表示要绘制的平行四边形的风格,

该风格在程序第 33 行已经定义了,FILL 在这里表示要绘制一个实心立方体;第 2 个参数是 Graphics 类的对象 g,它是由 Applet 容器创建的。

程序运行到 DrawCube的方法,首先执行程序第 142行:

calculateOffset();

调用程序第 97 行 calculateOffset 方法,利用下列公式:

offsetX = (int)(Math.cos(alpha * Math.PI / 180) * thickness + 0.5); offsetY = (int)(Math.sin(alpha * Math.PI / 180) * thickness + 0.5);

计算偏移量的值。注意,我们在 Math.cos(alpha * Math.PI / 180) * thickness 后面加上了一

个 0.5 的浮点数。原因是 Math.cos 的结果是一个 double 值的小数,而变量 offsetX 是一个整

型变量, 所以我们使用 (int) 的方法, 强制将 double值转换成整数, 转换的结果是将 Math.cos 值的小数部分全部丢弃。如:double类型的 10.999 和 10.0001,都被转换成同样的整数 10,

将其结果加上 0.5 后,凡是小数部分大于 0.5 的,其个位将会递增 1。如 10.999 +0.5=11.499,

转换成整数后,将得到 11 的结果;而 10.0001+0.5=10.5001,转换成整数后,该值仍然是 10。

上面的例子说明,一个浮点数取整的时候,加上 0.5后,就实现了 4 舍 5入的功能。 calculateOffset 方法结束后,程序返回到 DrawCube方法中,继续执行第 143 行:

buildRFront();

Page 75: Java Web动态图表编程

调用 buildRFront 方法。程序转到第 104行 buildRFront 方法,在 buildRFront 方法体内,

用以下方法计算出图 2.40 所示的 P1、P2 和 P6 点:

pRightX[0] = baseXPts + width; pRightY[0] = baseYPts;

pRightX[1] = pRightX[0]; pRightY[1] = baseYPts ­ height;

pTopX[0] = baseXPts; pTopY[0] = pRightY[1];

P0、P1、P2 和 P6 这 4 个点,正好组成正面的矩形。其中 P1 和 P2 两个点分别与右侧面平

行四边形 (P1P2P3P4) 左边两个端点重合, P6 和 P2 这两个点分别同顶部平行四边形 (P6P2P3 P5)

下方的两个端点相重合。因此,在计算出 P1、P2 和 P6 点后,将点 P1 和 P2 的结果赋值给代表

右侧平行四边形坐标的数组 pRightX,以及 pRightY 第 1 个和第 2 个元素。将点 P6 的结果赋

值给代表顶部平行四边形坐标的数组 pTopX,以及 pTopY 第 1 个元素。

之后,程序回到 DrawCube方法中,并执行程序第 144 行:

buildPTop();

程序转到第 117行的 buildPTop方法。

在 buildPTop方法体内,用以下方法计算出图 2.40 所示的 P2、P3 和 P5 点:

pTopX[1] = pRightX[1]; pTopY[1] = pRightY[1];

pTopX[2] = pTopX[1] + offsetX; pTopY[2] = pTopY[1] ­ offsetY;

pTopX[3] = pTopX[0] + offsetX; pTopY[3] = pTopY[2];

经过计算以及赋值操作后,整型数组 pTopX 和 pTopY 就完成了初始化,保存了点 P6、 P2、P3、P5 坐标的相关信息。

程序返回到 DrawCube方法中,继续执行第 145 行:

buildPRight();

程序转到并执行第 130 行 buildPRight 方法。

在 buildPRight 方法体内,用以下方法计算出图 2.40所示的 P3 和 P4 点:

pRightX[2] = pTopX[2]; pRightY[2] = pTopY[2];

pRightX[3] = pRightX[2]; pRightY[3] = pRightY[2] + height;

经过计算以及赋值操作后, 整型数组 pRightX 和 pRightY 就完成了初始化, 保存了点 P1、 P2、P3、P4 坐标的相关信息。

之后, 程序返回到 DrawCube方法中, 继续执行程序第 147 行的 switch多重选择语句块:

switch (cubeStyle)

程序根据用户传递的整型参数进行选择。

现在用户传递的参数是“FILL” ,根据程序第 33 行的声明,变量 FILL 的值为 2。所以

程序执行标号为 2 的语句,即程序第 158 行~第 168行。这段代码进行了 3 次填充操作,首

先是程序第 159 行~第 160 行:

Page 76: Java Web动态图表编程

g.setColor(pTopFillColor); g.fillPolygon(pTopX, pTopY, PARALLELOGRAM_POINT_NUMBER);

用 Color 类的实例 pTopFillColor 表示的颜色,即红色(Color.RED)对顶部的平行四边

形进行填充的绘制工作。

程序第 162行~第 163 行:

g.setColor(rFrontFillColor); g.fillRect(pTopX[0], pTopY[0], width, height);

重新设置当前的绘图颜色, 用 Color类的实例 rFrontFillColor表示的颜色, 即暗红色 (RBG 值:178,0,0)对正面的矩形进行填充的绘制工作。注意,程序第 163 行,我们在这里没有

采用 Graphics 类的 fillPolygon方法,而是直接调用了 Graphics 类的 fillRect 方法,因为:

Ø 可以减少定义两个用于保存正面矩形坐标的整型数组,节省内存;

Ø 可以使程序更精简;

Ø 得到了绘制矩形所需要的参数,我们可以直接调用 fillRect 方法。

因为矩形的左上角顶点(P6)和顶部平行四边形(P6P2P3P5)的第一个端点相重合,所以

直接使用 pTopX[0]和 pTopY[0]来表示该点。

此外,还可以使用点 P0 和 P6 之间的关系,即两者的横坐标相同,纵坐标相差 height,使

用下列的方法来绘制该矩形:

g.fillRect(baseXPts, baseYPts ­ height, width, height);

填充完正面的矩形后,程序继续运行第 165 行~第 166 行:

g.setColor(pRightFillColor); g.fillPolygon(pRightX, pRightY, PARALLELOGRAM_POINT_NUMBER);

与程序第 159 行~第 160 行的代码基本相同。重新设置当前的绘图颜色,即用 Color 类

的实例 rFrontFillColor 表示的颜色,即深红色(RBG值:124, 0, 0)来填充右侧面这个平行四

边形。

程序执行完第 166 行语句后,通过绘制两个平行四边形和一个矩形就得到了一个默认的

红色的实心立方体,如图 2.41①所示。这里总结一下该立方体的基本参数:

Ø 立方体基准点的坐标是:10,200。

Ø 顶部平行四边形的夹角为:45°。

Ø 立方体的长度、宽度和厚度分别为:100 磅、120磅和 30 磅。

Ø 立方体的风格为实心。

之后,程序执行第 168 行的 break语句,中断 DrawCube方法的运行再返回到 paint 方法

中。

程序第 203行~第 211 行,用蓝色填充了一个如图 2.41②所示的立方体。下面我们来具

体分析。

程序第 203行~第 207 行:

setBasePoint(150, 300); setWidth(80); setHeight(220); setThickness(40); setAngle(60);

这段代码调用了 5 个 set 方法,重新设置了立方体的基准点坐标为(150,300)、宽度为 80 磅、高度为 220 磅、厚度为 40 磅、顶部平行四边形的夹角为 60°。上述 set 方法与上一

节所讲述的 DrawTriangleApplet.java 程序中的相关 set方法的结构和用法完全一致, 我们就此

略过。

Page 77: Java Web动态图表编程

程序第 208行~第 210 行:

setPTopFillColor(new Color(0, 0, 255)); setFrontFillColor(new Color(0, 0, 178)); setPRightFillColor(new Color(0, 0, 124));

这段代码调用了 3 个 set 方法,重新设置了立方体的 3 个面,即顶部平行四边形的填充

颜色、正面矩形的填充颜色及右侧面平行四边形的填充颜色。

程序第 211 行:

DrawCube(FILL, g);

与程序第 200 行的代码完全相同,再次调用 DrawCube 方法,并提供与程序第 200 行代

码相同的参数。之后,程序再次进入第 140 行的 DrawCube方法体中,重复前面讲述的计算、

赋值及填充绘制等功能。根据立方体的绘制参数,得到的绘制结果如图 2.41②所示,与默认

的立方体绘制结果(图 2.41①)相比,我们可以清楚地看到两者之间的差别。

程序第 214行~第 215 行:

setBasePoint(280, 300); setOutlineColor(Color.ORANGE);

重新设置该立方体的基准点坐标为:(280,300),然后,重新设置立方体轮廓的描绘颜

色为橙色(默认为浅灰色,见程序第 38行)。

程序第 216行:

DrawCube(OUTLINE, g);

再次调用 DrawCube方法。

不过提供参数中的第 1 个整型参数的值由先前的“FILL”改变为“OUTLINE” 。同理,

根据程序第 33 行的定义,OUTLINE 的值为 1,所以这次调用 DrawCube 方法,进入 switch 语句块时,将执行第 149 行~第 156 行:

case 1: g.setColor(outlineColor); g.drawPolygon(pTopX, pTopY, PARALLELOGRAM_POINT_NUMBER);

g.drawRect(pTopX[0], pTopY[0], width, height);

g.drawPolygon(pRightX, pRightY, PARALLELOGRAM_POINT_NUMBER); break;

先设置立方体描绘的颜色为 Color 对象 outlineColor 所指定的颜色——橙色,然后,程序

分别用橙色描绘了立方体的 3个组成部分,即顶部平行四边形、正面的矩形及右侧面的平行

四边形。绘制结果如图 2.41③所示。

该立方体绘制完成后,程序继续执行第 219 行~第 228 行。

我们先看程序第 219行~第 227 行所进行的操作:

setBasePoint(400, 280); setWidth(120); setHeight(180); setThickness(20); setAngle(30); setPTopFillColor(Color.GREEN); setFrontFillColor(Color.MAGENTA); setPRightFillColor(Color.PINK); setOutlineColor(Color.BLACK);

这段代码也是调用一些简单的 set 方法,重新设置立方体的基准点、宽度、高度、厚度、

夹角、立方体三个面的填充颜色及轮廓的描绘颜色。这次我们设置其顶部的平行四边形的填

Page 78: Java Web动态图表编程

充色为绿色、正面矩形的填充色为品红色、右侧面平行四边形为粉红色、立方体的轮廓描绘

颜色为黑色。

程序第 228行:

DrawCube(MIXED, g);

再次调用 DrawCube方法,这次提供的参数中的第 1个整型参数的值改变为“MIXED” 。

同理,根据程序第 33 行的定义,整型变量 MIXED 的值为 3,所以这次调用 DrawCube方法,

进入 switch语句块时,将执行第 170行~第 186 行:

case 3: g.setColor(pTopFillColor); g.fillPolygon(pTopX, pTopY, PARALLELOGRAM_POINT_NUMBER);

g.setColor(rFrontFillColor); g.fillRect(pTopX[0], pTopY[0], width, height);

g.setColor(pRightFillColor); g.fillPolygon(pRightX, pRightY, PARALLELOGRAM_POINT_NUMBER);

g.setColor(outlineColor); g.drawPolygon(pTopX, pTopY, PARALLELOGRAM_POINT_NUMBER);

g.drawRect(pTopX[0], pTopY[0], width, height);

g.drawPolygon(pRightX, pRightY, PARALLELOGRAM_POINT_NUMBER); break;

这段代码进行了 3 次图形的填充和 1 次的轮廓描绘操作。用我们设定的立方体 3 个面的

填充颜色,分别对立方体的顶部、正面和右侧面进行填充操作,然后,用设定的轮廓颜色对

立方体的 3个面依次进行轮廓的描绘操作。实际运行结果如图 2.41④所示。

通过前面的学习,读者对 Java 如何控制颜色、操作字体、绘制基本的几何图形,以及扩

展到三维图形的绘制, 已经有了一个初步的认识。 下一节, 我们将学习如何在 Java Applet 中,

加载并显示计算机上的图形文件。

2.11 图像的载入与显示

在现实工作中,我们希望在 Web 图表最终输出结果中包含一些图标或者图像。这些图标

和图像可以是公司的徽标,也可以是软件的徽标(如微软的 Windows 操作系统的徽标、JSP Web 服务器 Tomcat 的徽标等),也可能是任意一幅图片。如果这些徽标或者图像都需要我们

自己使用 Java 相关的绘制方法来完成,那么效率实在是太低了。对于一些已经绘制完成的图

像,我们可以直接使用 Java 从文件中读取并显示图像。

2.11.1 在 Applet 中加载和显示图像

Java 中有多种方法都可以加载并显示图像文件,如下所述:

Ø 调用 Image类的相关方法。

Ø 调用 ImageIcon类的相关方法。 Java J2SE 5.0 版之前的旧版 Java 处理格式基本上分为以下三类:

Ø 联合图像专家组(Joint Photographic Experts Group)格式,即 JPG/JPEG 格式。

Ø 图形交错格式(Graphics Interchange Format 格式),即 GIF 格式。

Ø 可移植的网络图像(Portable Network Graphics)格式,即 PNG格式。

Page 79: Java Web动态图表编程

J2SE 5.0 后的版本就支持了微软的 BMP 格式图像。BMP 图像作为 Windows 环境下主要

的图像格式之一,以其格式简单、适应性强而备受欢迎。因此在 J2SE 5.0 中,Sun公司也将

其纳入 Java 的支持范围。但 BMP 文件格式,在处理单色图像和真彩色图像的时候,无论图

像数据多么庞大,都不对图像数据进行任何压缩处理,这就使得 BMP 在存储单色图像或者

真彩色图像时,文件体积过于庞大。所以在网络上,一般不采用 BMP 的文件格式来进行传

送。

表 2.19 列出了 java.swing包中的 ImageIcon类中的部分构造器。

表 2.19 ImageIcon类中的部分构造器

ImageIcon()

生成尚未初始化的 ImageIcon对象,在使用之前必须使用 Image 对象初始化

ImageIcon(String filename)

以 filename 指定的文件生成一个 ImageIcon对象,filename 既可以表示文件的绝对路径,也可以是当前目录的相对路径

ImageIcon(String filename, String description)

同上面构造器一样,从 filename 指定的文件生成一个 ImageIcon 对象,第二个参数指定图像的说明。该图像将被

MediaTracker 类的对象预先装载用来监视图像的加载状况

ImageIcon(URL location)

以 URL 参数指定的源文件对象创建一个 ImageIcon对象。URL 类表示统一资源定位器,它指出 World Wide Web上的

资源。这里的 URL 可以是因特网上的地址,也可以是本地计算机

ImageIcon(URL location, String description)

URL 指定图像文件的位置,description 指定文件的名称。这里的 URL 可以是因特网上的地址,也可以是本地计算机

ImageIcon(Image image)

以 Image 的对象来创建一个 ImageIcon对象

ImageIcon(Image image, String description)

以 Image 的对象来创建一个 ImageIcon对象,第二个参数提供了图像的说明

ImageIcon类不是 abstarct 类,所以,程序可以直接创建 ImageIcon对象。ImageIcon类提

供了多个构造器,允许程序使用本地计算机上的图像,或者使用存储在 Internet 上的图像来

初始化 ImageIcon对象。

在这里我们已经涉及到了 URL,所以让我们先简要介绍 URL和 URL类。

1.URL

Internet 提供了许多协议,HTTP 是超文本传输协议(Hyper Text Transfer Protocol),用

在WorldWide Web上,它构成了因特网的基础。它用 URI(Uniform Resource Identifier)统一

资源标识符来标识 Internet 上的数据,用于指定文档位置的 URI(Uniform Resource Locator)

统一资源定位符。一般的 URL引用文件或目录,也可以引用执行复杂任务的对象,诸如执行

数据库查询和 Internet 搜索。URL 是相对于使用路径来标识数据资源文件标准方法的扩展。 URL由 4 部分组成,它描述了要访问的资源文件的位置、名称,以及如何与保存该文件的主

机通信而获取文件。URL中的某些部分可以完全忽略或者是使用其默认值。

表 2.20 URL组成

协 议 域 名 端 口 路径和文件名

http:// java.sun.com :80 /index.html

ftp:// ftp.freebsd.org/ :21

Page 80: Java Web动态图表编程

协议指定了用来获得文件的通信方法。FTP 是文件传输协议(File Transfer Protocol)、远

程收取信件的 POP3 或者 IMAP 协议,以及用于发送信件的 SMTP 协议,远程登录的 Telnet 和 SSH 协议等。

端口在计算机上只是标识特定服务的数字,一个计算机可以有多个端口,每个端口提供

不同的服务。通常不需要指定 URL中的端口,因为标准端口与特定的协议相关,系统将指定

默认的端口。例如,HTTP 协议使用 80 端口、FTP 协议使用 21 端口、POP3 使用 110 端口、 SMTP 协议使用 25 端口、TELNET协议使用 23端口、SSH 协议使用 22端口,等等。

因为域名是数据资源引用的一部分,网络上每个数据源都有惟一的 URL。只要知道数据

资源在因特网上的 URL、储存该数据的计算机已经连接在因特网中并对外提供服务、具有相

应的读取权限,我们就可以获得该数据资源。

2.URL 类

ImageIcon的构造器中接收了 URL类的实例, URL类封装了 URL, 它隶属于 java.net 包。 Java 的网络功能集成在几个包中。java.net 包中的类和接口声明了基本的网络功能,使开发基

于 Internet 和 Web 的应用更为简单。本书讲述的内容(第 3 章以后的内容),也是一种基于 Internet/intranet 的 Web 服务器端编程技术。Java 通过该包提供了基于流的通信(stream­based communication),使得应用程序能够把网络视为数据流。此外,java.net 包中的类和接口还提

供了基于数据包的通信(packet­based communication),用于传输单独的信息包,通常是在 Internet 上用来传输音频和视频。

本书对网络的讨论,主要集中在浏览器/服务器(Browser/Server)的两端。浏览器请求

执行某些操作,服务器响应其请求并执行相应的操作,将执行结果返回给浏览器。

程序 LoadImageApplet.java(\chap02\Fig2.11\Fig2.11_01)演示了如何加载并显示几种格式

图像的方法, 除了旧版 Java 支持的 PNG、 GIF、 JPG 外, 我们在程序中还提供了如何加载 BMP 图像文件的方法,运行结果如图 2.42 所示。讲解本程序源代码之前,读者可以在相应的目录

下先浏览本程序将要加载并显示的 4 个不同格式的图像文件。

图 2.42 Applet加载并显示不同格式的外部图像文件

程序第 6 行:

import java.net.*;

ImageIcon类接收 URL实例的构造器,URL类是在 java.net 包中被定义的,所以,这里

必须引入 java.net 包。

程序第 7 行:

Page 81: Java Web动态图表编程

import javax.imageio.*;

旧版 Java 并不支持 BMP 格式的图像文件,Java 在新版的 javax.imageio包中的 ImageIO 类中提供对读取BMP格式图像文件的支持。 因为例程中涉及到了BMP格式图像文件的读取,

所以,这里也必须引入 javax.imageio这个包。

程序第 11 行:

private Image logoJPG, logoBMP, logoPNG;

声明了 3 个 Image类的变量 logoJPG、logoBMP 和 logoPNG,分别表示加载 JPG、BMP 和 PNG 格式图像文件的 Image对象。Image类属于 java.awt 包中 Image类,可以读取图像文

件。

程序第 12 行:

private ImageIcon logoGIF;

定义了一个 ImageIcon类的对象 logoGIF,表示该对象用于处理GIF格式的图像文件。

程序第 15 行~第 36 行,是 Applet 的 init 方法,加载图像、初始化和预处理工作就在 Applet 的 init 方法中完成。

程序第 18 行:

logoJPG = getImage(getDocumentBase(), "logo.jpg");

Image类是一个抽象类 (abstract class), 所以 Applet 不能直接创建 Image类的对象。 Applet 必须调用一个方法,让 Applet 容器加载并返回程序要使用的 Image 类的对象。JApplet 的超

类 Applet 提供了一个名为 getImage 的方法,该方法将 Image 加载到 Applet 中,方法接收两

个参数——图像文件的位置和图像文件名。在第 1 个参数中,Applet 方法 getDocumentBase 返回一个 URL,表示图像资源在 Internet 中的位置(如果 Applet 是从本地计算机上加载的,

则返回该图像资源在本地计算机中的位置)。本例中,Java Applet 编译后的.class 文件和调用

该.class 文件的 HTML 文件,以及 4 个不同格式的图像文件都保存在同一个目录下。方法 getDocumentBase 以 URL 类的对象的形式返回图像文件 logo.jpg 在本地计算机中的位置,第 2 个参数指定图像文件的文件名。

从第 18 行开始,程序就从本地计算机加载图像(或者从 Internet 下载图像,视 URL 对

象的内容而定)。程序使用一个独立的线程去执行加载图像,这样,程序在图像开始加载后,

继续执行下面的语句。

提示:如果 URL对象指定被请求的文件资源不可用,getDocumentBase方法并不提示出

错或者抛出异常。

程序第 20 行:

logoPNG = getImage(getDocumentBase(), "logo.png");

该行与程序第 18 行语句,除了加载的图像文件的文件名不同外,其余完全相同。程序在本

地计算机中加载一个名为 logo.png的图像文件时,也使用一个独立的线程去执行它。

程序第 26 行:

logoGIF = new ImageIcon(new URL(getCodeBase(), "logo.gif"));

ImageIcon类不是 abstract 类, 所以可以直接创建 ImageIcon的实例。本条语句在执行时,

就创建了 ImageIcon类的对象 logoGIF,用于加载本地计算机上的文件名为 logo.gif的图像文

件。从表 2.19 中,我们可以看到 ImageIcon类提供了多个构造器,允许程序使用本地计算机

上的图像,或者是存储在 Internet 中的图像来初始化 ImageIcon 对象。注意,如果以程序第

Page 82: Java Web动态图表编程

26 行的方式, 创建并初始化 ImageIcon对象时, 系统可能会抛出异常。 例如, 输入错误的 URL 字符串等,所以我们把该语句放入程序第 23 行~第 35 行之间的 try…catch 语句块中。在这

里发生的异常,可能属于以下两类或者其派生的子类:

Ø IOException Ø java.net.UnknowHostException 程序第 29 行:

logoBMP = ImageIO.read(new URL(getCodeBase(), "logo.bmp"));

同理, URL对象可能产生异常, 所以我们将其也放入程序第 23 行~第 35行之间的 try… catch语句块中。不同的是,这里我们处理的是 BMP格式的图像文件。传统的 getImage方法

是不支持 BMP 格式的,我们在此引入了 javax.imageio 包中的 ImageIO 类来处理。logoBMP 是一个 Image类的对象,Image是抽象类,不能直接实例化,必须由 Applet 调用另外一个方

法,让 Applet 容器加载并返回程序要使用的 Image类的对象。ImageIO 的 read方法,返回一

个 BufferedImage的对象引用,BufferedImage是 java.awt.Image类的子类,而子类的实例对象

是可以被其父类直接使用的,故此,实现了获得 Image类的实例的目的。 init 的方法执行后,程序执行第 39 行的 paint 方法。paint 方法中的第 42 行~第 48 行,

在 Applet 上绘制了一条橙色的文本后, 继续执行其余语句, 将需要显示的图像一一绘制出来。

程序第 51 行:

g.drawImage(logoJPG, 5, 50, this);

调用 Graphics 类的 drawImage方法,显示 logoJPG 对象。drawImage方法需要 4 个参数:

第 1 个参数是将要显示的 Image类的对象引用(这里是 logoJPG);第 2 个和第 3 个参数是个

整形参数,表示该 Image类的对象的引用 logoJPG 在 applet 中绘制所需的 x 和 y坐标,该坐

标表示图像左上角顶点的坐标位置;最后一个参数是 ImageObserver 对象的引用。通常, ImageObserver 是程序用来显示图像的对象, ImageObserver 可以是任何一个实现 ImageObserver接口的对象。 Component类实现了 ImageObserver接口, 因此, 所有的Component 都是 ImageObserver。因为 JApplet 也是 Component 类的子类(见第 2.2.1 节组件与容器,图 2.3 所示),所以 JApplet 也是 ImageObserver。最后一个参数使用了关键字“this” ,说明当前

这个 ImageObserver 对象就是 JApplet 本身。 ImageObserver 负责处理从 URL 制定的资源上加载图像数据的过程。当这个图像数据在

因特网上,而且图像文件比较大的时候,图像的加载过程就可能有比较长的延迟。为解决此

问题,ImageObserver 接口还声明了 imageUpdate的方法,该方法会在前面请求有关图像的信

息可用时自动调用,并在组件上绘制该图像。

本语句执行后,将在制定位置显示 Image类的对象 logoJPG,运行结果如图 2.42①所示。

程序第 52 行和第 54行:

g.drawImage(logoPNG, 160, 50, this); g.drawImage(logoBMP, 5, 160, this);

同样是调用 Graphics 类的 drawImage方法来显示图像,除了需要显示的 Image对象不同

外,其余全部相同。这里是分别绘制一个 PNG 和 BMP 格式的图像文件。运行结果如图 2.42 ②和图 2.42③所示。

程序第 55 行:

logoGIF.paintIcon(this, g, 160, 160);

直接调用 ImageIcon 类的 paintIcon 方法来显示图像,该方法需要 4 个参数:Component 对象引用、Graphics 对象引用及图像的绘制坐标(以图像的左上角为基准点)。本语句的运行

Page 83: Java Web动态图表编程

结果如图 2.42④所示。

对比这两种加载及显示图像的方法,我们推荐使用 ImageIcon 方法。因为 ImageIcon 方

法更简单,更直接地创建 ImageIcon 类的实例,而且在显示图像的时候也不需要使用 ImageObserver 的引用。

2.11.2 使用 MediaTracker 加载并显示图像

上节我们阐述了两种加载并显示图像的方法,但是采用异步的方式,即程序在后台完成

图像的加载和显示,而主进程继续执行。这种情况可能在图像还未被完全加载完毕时造成不

完整的显示。在某些情况下,我们需要在图像完全加载完毕后再进行显示,要得到这种功能

就需要了解本节介绍的另外一种加载图像的方法,即调用媒体跟踪器(Media Tracker)来加

载图像。

媒体跟踪器是 MediaTracker 类型的对象,专门用来跟踪图像的加载。这个类在 java.awt 包中定义,目前只管理图像的载入,以后可能会扩展其功能用来跟踪其他类型的媒体资源的

加载。MediaTracker 只有一个构造器,需要一个组件引用的参数,这个组件对象就是装入图

像的组件。表 2.21 列出了媒体跟踪器的构造器及其相关方法。

表 2.21 媒体跟踪器的构造器及其相关方法

public MediaTracker(Component comp)

构造器:接收一个用于加载图像的 Component 对象

public void addImage(Image image, int id)

续表

把 Image 类的对象 image 添加到当前媒体跟踪器要追踪的图像列表中,整型对象 id 表示该 image 的标识

public void waitForAll()

初始化加载过程并等待所有被跟踪的图像加载

public boolean waitForAll(long ms)

指定初始化加载过程并等待图像加载的时间,时间用毫秒表示

Public boolean checkID(int id)

检查指定的标识为 id 的 Image 对象 image 是否加载完毕,如果已经加载完毕,则返回真

Public boolean checkAll()

检查所有被跟踪的 Image 对象 image 是否加载完毕,如果已经加载完毕,则返回真

Public boolean isErrorAny()

检查所有被跟踪的 Image 对象 image 的错误状况,没有错误则返回假

Public boolean isErrorID(int id)

检查指定的标识为 id 的 Image 对象 image 在加载过程中是否发生错误,没有错误则返回假

媒体跟踪器的使用是在调用 getImage()方法,获得对 Image 的对象引用后,调用跟踪器

的 addImage()方法就将 Image对象传递给媒体跟踪器。addImage方法有两个参数:要跟踪的

图像的引用,以及用来跟踪图像的 int 类型的标识。媒体跟踪器可以同时跟踪多个图像,该

标识决定被加载图像的优先级。

程序 MediaTrackerApplet.java(\chap02\Fig2.11\Fig2.11_02)演示了如何在 Applet 中调用 MediaTracker 类以及跟踪图像,运行结果如图 2.43 所示。

Page 84: Java Web动态图表编程

图 2.43 用媒体跟踪器(MediaTracker)加载外部图像文件

程序第 9 行~第 11 行:

private Image[] images = new Image[3]; private Image bg; // 背景图像 private MediaTracker mt = null;

声明了 1 个 Image 类的数组对象 images,其中包含 3 个 image 对象,1 个 Image 类的对

象 bg,表示本 Applet 将要加载的背景图像,还声明了一个 MediaTracker 类的对象 mt。

程序第 14 行~第 27行,在 init 方法内,对上述变量进行初始化。

程序第 16 行:

bg = getImage(getCodeBase(), "images/background.gif");

用 Applet 类 getImage 方法得到一个 Image 类的对象引用,并传递给 Applet。注意,在 getImage的方法内,第 2 个参数的使用方法:

"images/background.gif"

表示图像文件的位置在 Java Applet 当前目录下的 images 子目录中,因此这里表示加载

当前目录中的 images 子目录下的 background.gif图像文件。

程序第 19 行:

mt = new MediaTracker(this);

初始化 MediaTracker 的实例 mt。MediaTracker 只有一个构造器,需要一个组件引用的参

数,这个组件对象就是装入图像的组件。这里的 this 表示本容器,也就是在本 Applet 中创建

图像。

程序第 20 行:

mt.addImage(bg, 0);

使用 MediaTracker 类的 addImage 方法,指定要跟踪的图像对象。媒体跟踪器可以同时

跟踪多个图像,多个图像可以拥有相同的标识,标识的值决定加载图像的优先级,具有相对

较低标识值的图像优先加载。这里,图像 bg的标识指定为 0。

程序第 22 行~第 25行:

for (int i = 0; i < images.length; i++)

Page 85: Java Web动态图表编程

images[i] = getImage(getCodeBase(), "images/logo" + (i + 1) + ".jpg"); mt.addImage(images[i], i + 1);

使用了一个循环,在用 getImage 方法获得图像的引用后,将当前目录下的 images 子目

录中 logo1.jpg、logo2 以及 logo3.jpg图像文件,依次添加到当前媒体跟踪器 mt 要跟踪的图像

列表中。标识在这里分别设定为 1、2、3。 init 方法结束,进入 paint 方法中。

程序第 32 行~第 40行:

try

mt.waitForAll(); catch (InterruptedException e)

System.out.println(e); return ;

程序在第 34行,调用 waitForAll 方法初始化加载过程,并等待所有被跟踪的图像加载完

毕后返回。如果要加载某个特定标识的图像对象,可以调用跟踪器的 waitForID 方法,并传

递一个标识给该方法,开始加载与某个特定标识相关的所有图像对象,并等待所有与该标识

相关的图像加载完成后才返回。例如,希望跟踪标识为 1 的图像,在上面的语句块中,只需

要更改程序第 34 行:

mt.waitForID(1);

如果我们只希望跟踪某一个特定的图像对象,就赋予该媒体跟踪器中所跟踪的每个图像

对象都具有一个惟一的标识。这样,一旦出现异常,我们可以通过相应的方法获得出错图像

的标识,从而找到发生错误的地方。

这里介绍的 waitForID 和 waitForAll 方法将无限期进行等待。因为这些方法可能会抛出

异常,所以将其放入一个 try…catch 语句块中。还有另外一个版本的 waitForAll 方法,该方

法需要一个长整型变量,指定初始化加载过程以及等待图像加载的时间,时间用毫秒表示,

由这个长整型的参数决定。但是使用这种方法,我们并不知道在该方法返回时,图像是否真

的装载完毕了,还是等待的时间到了;也许图像早就加载完毕了,但因为设置的等待时间还

没有结束,因此媒体跟踪器仍旧在等待,这样会严重浪费系统资源。所以我们推荐使用前面

介绍的第 1和第 2 种方法,而不要随意使用需要接收一个等待时间参数的 waitForAll 方法。

如果 waitForID 和 waitForAll 方法在无限期的等待中抛出异常, 将被程序第 36行~第 40 行的 catch语句块所捕获,然后在命令控制台输出一条有关的异常信息,之后,主线程返回, Applet 上没有任何显示。

waitForAll 方法执行完毕后,虽然所有被跟踪的图像都已经加载完毕,但并不说明这些

图像的加载过程中没有任何错误(例如,图像的路径有错、或者图像的文件名有误、或者文

件名和路径都正确,但却是 Java 不支持的图像格式),所以,我们必须明确地检查错误,因

此程序第 42行~第 53 行的代码就负责此项检查工作:

if (mt.isErrorAny())

for (int i = 0; i <= 4; i++)

if (mt.isErrorID(i)) g.drawString("图像产生错误状况!文件标志为:" + i, 10, 40 + 20 * i);

Page 86: Java Web动态图表编程

return ;

程序第 42 行,调用了 isErrorAny 方法,用来检查并确定任何被追踪的图像是否产生了

错误。如果方法返回 false,则说明没有错误发生;如果图像产生了错误,isErrorAny 方法返

回真。我们在程序第 44 行~第 49 行的代码中,对其进行进一步的检查。

程序第 44行~第 50行, 编写了一段循环代码来检查并确定到底是哪一个被追踪的图像产

生了错误。程序第 46 行,我们将循环语句中的局部变量 i 作为参数并调用 isErrorID 方法。

如果 isErrorID 方法返回 true,则说明至少在加载一个图像标志与变量 i 相关的图像发生了错

误。因为每个图像都指定了惟一的标识,所以一旦发生错误,可以轻松地找到错误的图像。

找到发生错误的图像后,程序将在 Applet 上绘制出一条与该错误相关的信息。

当确定图像的加载没有错误时,就可以显示这些图像了。

程序第 55 行:

g.drawImage(bg, 0, 0, this);

我们前面介绍过的Graphics类的 dwawImage方法, 在 Applet上绘制图像 bg, 如图 2.43 ①所示。

图像 bg (background.gif) 的宽度和高度分别是 565 磅和 470 磅。 为达到将该图作为 Applet 背景的目的,在调用 MediaTrackerApplet.class 的 HTML 文档中,我们指定该 Media TrackerApplet.class 的宽度和高度正好与图像 bg(background.gif)的宽度和高度相同。例如, MediaTrackerApplet.html 文档的第 4 行~第 5行所示:

<APPLET CODE = "MediaTrackerApplet.class" WIDTH = "565" HEIGHT = "470"> </APPLET>

程序第 57 行以及第 59 行:

g.setFont(new Font("汉真广标", Font.PLAIN, 30)); g.drawString("MediaTracker 的使用", 15, 30);

用“汉真广标” (30磅,普通风格)这种字体在 Applet 上的坐标(15,30)处绘制文本

“MediaTracker 的使用” 。Java 的绘图模式默认为覆盖模式,所以, “MediaTracker 的使用”

这段文本将显示在已经绘制完成的图像 bg 上,见图 2.43②所示。反之,如果程序第 57、59 行的位置互换,将无法看到文本“MediaTracker 的使用” ,因为图像对象 bg将它覆盖了。

程序第 61 行~第 64行:

for (int i = 0; i < 3; i++)

g.drawImage(images[i], 410, 40+i * 100, this);

循环绘制出 Image的数组对象 images 中的每一个图像对象。因为,每个图像对象的绘制

横坐标都为 410 像素(磅),而纵坐标依次递增 100像素(磅),所以,显示效果呈现纵向排

列。如图 2.43③、2.43④、2.43⑤所示。

为了测试 MediaTrackerApplet.java 程序中的异常捕获功能是否正常工作,我们将 images 子目录下的 background.gif,以及 logo2.jpg 分别重新改名为 bf.gif 和 logo2bak.jpg。重新启动 IE,再调用 MediaTrackerApplet.html 文档,我们可以看到,程序中的异常捕获功能已经能够

正常工作了。因为,每个图像对象都定义了惟一的标识,所以在输出结果上,可以看到标志

为 “0” 和 “2” 的图像对象出现了错误, 而这两个图像文件正是我们刚刚更名前的 background.gif 以及 logo2.jpg(标志分别为 0 和 2,见源程序第 20、25 行) 。出现异常后的程序输出结果,

Page 87: Java Web动态图表编程

如图 2.44 所示。

图 2.44 异常的捕获

2.11.3 使用双缓冲技术绘制图像

读者可能已经发现, 在运行前面第 2.8.4节所讲述的绘制圆柱体的源程序, 以及运行 2.10.5 节所介绍的绘制平行四边形及立方体源程序的时候, 当我们将 IE 从当前窗口转变成非当前窗

口状态,再从非当前窗口恢复到当前窗口状态,有时,某些绘制好的图像会消失,除非我们

重新刷新 IE 窗口,显示才会恢复正常。此外,当我们移动 IE 窗口或者其他的窗口在 IE 上移

动的时候,图像会有些闪烁。但运行 2.11.1 节的加载并绘制图像文件的源程序,却没有这种

现象。这是怎么一回事呢?这就要涉及到 Java Applet 中的 paint 方法的绘图机制了。产生这

种现象的主要原因是:

Ø 由于在显示所绘制的图像时,调用了 repaint 方法。repaint 方法被调用时,需要清除

整个背景,然后才调用 paint 方法显示画面。这样,在清除背景和绘制图像的短暂时

间间隔内被用户看见的就是闪烁。

Ø 由于 paint()方法需要进行复杂的计算,图像中包含着多个图形,不同图形的复杂程度

及其所需要的绘制时间不同,因此,图像中的各个像素值不能同时产生,使得图形的

生成频率低于显示器的刷新频率,从而造成闪烁。

提示:运行 Java 编写的动画程序时,发生不连贯或闪烁现象时,可参考下文所介绍的方

法加以改善。

下面两种方法可以明显地消除或减弱闪烁:

Ø 重载 update方法

当AWT接收到Applet重新绘制的请求时, 调用Applet的 update方法。 默认情况下, update 方法清除 Applet 的背景,然后调用 paint 方法。重载 update方法,就可以将以前在 paint 方法

中的绘图代码包含在 update方法中,从而避免每次重新绘制时将整个区域清除。

Ø 双缓冲技术

双缓冲技术在很多动画 Applet 中被采用。主要原理是创建一幅后台图像,将每一帧画入

图像,然后调用 drawImage方法,将整个后台图像一次画到屏幕上去。这种方法的优点在于

大部分绘制是在后台进行的。将后台绘制的图像一次绘制到屏幕上。在创建后台图像前,首

先通过调用 createImage 方法生成合适的后台缓冲区,然后获得在缓冲区的绘图环境(即 Graphics 类对象)。

综上所述,改善前面我们写的一些 Java Applet 源程序的思路是:不直接在 paint 方法中

调用各种绘制方法,而是采用重载 update方法及双缓冲技术,生成一个图像的缓冲区,获得

Page 88: Java Web动态图表编程

该缓冲区中的绘图环境后,将该绘图环境读入内存。paint 方法不再负责图像的绘制工作,即 paint 方法不再装入任何的图像绘制代码。我们在 paint 方法中,直接调用 update 方法,在内

存缓冲区的绘图环境下进行图像的绘制工作,当所有的图像绘制工作完成后,最后将缓冲区

的内容一次性地写入 Applet 并在 Applet 窗口中直接显示出来。 这种方法很巧妙地解决了图像

丢失和闪烁的问题。

现在我们就遵循上面的思路,重新改写第 2.8.4和第 2.10.5 节所介绍的源程序。

首先看改写的 2.8.4 节介绍的绘制圆柱体的源程序 DrawBufferedCylinder.java (\ chap02\Fig2.11\Fig2.11_03)。

DrawBufferedCylinder.java 与第 2.8.4 节所介绍的源程序 DrawCylinderApplet.java 在绘制

圆柱体时的不同之处在于,前者是先绘制在内存缓冲区中的,然后显示在 Applet 窗口中,而

后者是直接在 Applet 上进行绘制。

程序第 16 行~第 17行:

Image offImage; Graphics offGraphics;

声明了一个名为 offImage的 Image对象,表示缓冲区的 Image对象。然后声明了一个名

为 offGraphics 的 Graphics 对象,offGraphics 是标识缓冲区的图像绘制环境。这两个系统成

员变量在这里仅仅是声明,并没有初始化,我们将在 init 方法中对其进行初始化。

程序第 19 行:

int appltWidth = 370, appletHeight = 420;

增加了两个类的数据成员变量 appletWidth和 appletHeight, 分别表示本 Applet的宽度和高

度。appletWidth 和 appletHeight 的值,与调用本 Applet 的 HTML 文档 DrawBuffered Cylinder.html 中的 Applet 属性中的 Width和 Height 相同。

程序首先运行第 22 行~第 26行的 init 方法:

public void init()

offImage = createImage(appltWidth, appletHeight); offGraphics = offImage.getGraphics();

使用 Component 类的 createImage 方法,创建了一个 Image 类的实例,并将该实例对象

赋给 offImage。Component 类的 createImage方法,返回的是一个后台的(off­screen),用于

双缓冲的图像对象。它接收两个整型参数,分别指定该图像对象的宽度和高度,如果该 Component 是不可显示的部件,则返回的结果为 null。

接着,用 Image 类的 getGraphics 方法,创建了一个名为 offGraphics 的 Graphics 对象,

并获得图形环境。后台的(off­screen)缓冲图像将由它来产生,因为这里画的是内存缓冲区

图像,所以 Applet 窗口上不会有显示。

然后, 程序执行第 68行的 paint方法, 我们强制其执行程序第 73行~第 91行的 update 方法。

程序第 75 行~第 77行:

offGraphics.setColor(Color.BLACK); offGraphics.setFont(new Font("汉真广标", Font.PLAIN, 35)); offGraphics.drawString("圆柱体画法", 10, 40);

前面例程都是调用 paint 方法中的参数 Graphics 对象 g 的相关绘制方法,对文本和其他

图形进行绘制操作。上面的代码是在缓冲区图像的图形环境中绘制,所以,我们不再调用 g 的相关方法,而是调用 offGraphics 的相关方法进行绘图作业。因为 offGraphics 是缓冲区图

Page 89: Java Web动态图表编程

像的图形环境对象,所以每当 Applet 调用 offGraphics 方法时,绘制工作都将在内存中的缓冲

区中进行。这几行代码设置了当前的绘图颜色、字体并绘制一条文本“圆柱体画法” 。

程序第 80 行:

drawCylinder(offGraphics);

调用 drawCylinder 方法, 并将 offGraphics 作为参数传递给该方法。 因此, 程序第 46 行~

第 66 行:

public void drawCylinder(Graphics g)

// 计算椭圆的坐标 calculateOvalCoordinate();

// 绘制下方椭圆 g.setColor(fillColor); g.fillOval(ovalX2, ovalY2, ovalWidth, ovalHeight); g.setColor(outlineColor); g.drawOval(ovalX2, ovalY2, ovalWidth, ovalHeight);

// 绘制中间的矩形 g.setColor(fillColor); g.fillRect(rectX, rectY, rectWidth, rectHeight);

// 绘制上方的椭圆 g.setColor(g.getColor().darker()); g.fillOval(ovalX1, ovalY1, ovalWidth, ovalHeight); g.setColor(outlineColor); g.drawOval(ovalX1, ovalY1, ovalWidth, ovalHeight);

因为,传递的参数是缓冲区的图形绘制环境对象 offGraphics,所以,drawCylinder 方法

同样是在 offGraphics 上进行绘制。这里需要注意的是程序第 90 行,update 方法中的最后一

条语句:

g.drawImage(offImage, 0, 0, null);

在调用 drawImage方法时把 null 作为第 4 个参数,这样可以防止 drawImage调用 update 方法。因为图像的所有内容都已装入内存,所以,图像在 Applet 窗口的显示就一气呵成了。

现在我们检测程序的运行结果。 将 IE 从当前窗口转变成非当前窗口状态, 再从非当前窗

口恢复到当前窗口状态,我们会发现,绘制好的图像不再消失了,也不需要重新刷新 IE 窗口

了。

重新改写的第 2.10.5 节所介绍的绘制立方体的 DrawBufferedParallelogram.java (chap02\ Fig2.11\Fig2.11_04)的源程序我们也不在赘述,请读者自行查阅并运行该程序。

Page 90: Java Web动态图表编程

2.12 本章小结

本章主要介绍了色彩的基本知识,Java Applet 运行机制及如何编写、运行 Java Applet 程

序,详细论述了各种基本几何图形在 Java Applet 下的编程工作。

详尽阐述了 Java.awt.Graphics 类的各种方法,包括绘制直线、文本(字符串)、矩形、椭

圆、圆、圆弧、多边形和折线。在绘制基本的几何图形的基础上,我们以绘制圆柱体和立方

体为例,向读者展示了如何通过绘制多个多边形并将其组合成一个复杂的几何图形的方法。

用多个多边形实现 3D 效果,以及组合各种不同基本几何图形和多边形就能够生成复杂的图

形。

讲述了 Java Applet 的图形加载及显示的几种方法,并分析了各自的优点。因为 Applet 的局限性,在图像绘制完成的情况下,可能会出现图像丢失或者闪烁的情况,为解决这个问

题,我们提出了如何利用双缓冲技术来改善绘制性能,并采用双缓冲技术重新改写了几个以

前的绘制程序。

在掌握了 Java Applet 绘制各种基本的几何图形及多边形的方法后, 接下来我们将学习使

用 Java Applet 生成 Web 动态图表的几个简单实例。

第 3 章 Java Applet 图表绘制实例

本章将阐述利用 Java Applet 来绘制 Web 动态图表的一些简单实例。目前,企业中基于 B/S 体系结构的应用程序越来越多,对 B/S 应用程序的要求也越来越高。WWW 的发展使得

基于 B/S 的应用程序不再局限于静态或者简单的动态内容。在工作中,我们经常将所需要的

数据用图表的方法在Web 页面上表示出来,例如:柱形(直方)图、饼形图、线段图、趋势

图、甘特图等,并且这些图表能够根据数据的改变而自动更新。 Java Applet 是可用于开发的一种轻量级、较简单、基于客户端的Web 动态图表。

3.1 Java Applet生成Web 动态图表

本节介绍几个利用 Java Applet 来绘制 Web 动态图表的简单实例。参与图表运算的数据

在实际工作中的来源是多种多样的。有通过 HTML 表单提交的,有通过 Applet 传递参数的,

但绝大多数是通过查询或访问数据库而得到的。因为本书不涉及 Java 和数据库编程的相关知

识,所以本书中凡是涉及到Web图表运算的实时数据都通过随机数产生。

为让读者能尽快地掌握 Web 动态图表的编程思路和方法,我们在讲述本章的内容时,将

每节的例程都用一个单独的 Java 源程序来编写(Fig.3.02_02:CheckMaker.java 除外)。我们将

在学习过程中,逐步过渡到用面向对象的编程思路来设计 Web 动态图表。

Page 91: Java Web动态图表编程

首先让我们看看 Web 图表的外观结构,如图 3.1 所示。 Web 图表结构可以简单地分成两大区域:

Ø 标题区:主要负责绘制图表的标题,表示本图表的名称,一般位于整个图表的上方。

除此之外,还可以将其设置在整个图表的下方、左方及右方。

Ø 图表区:负责绘制具体的图形。如图 3.1 中的折线图及三维饼图,图表区中有时还包

括图例。

图例就是图中所示的三维饼图的右边部分。 图例用来说明图表中各个部分所表示的意义。

在图表区中,某些图表需要坐标系作为参考。如图 3.1 中的折线图,而某些形状的图表

则不需要坐标系,如图 3.1 中所示的三维饼图。同样地,我们在图表区域中,可以设置图例

来说明图表中各部分所代表的含义,如三维饼图所示;也可以不用图表,直接在坐标系中说

明图表中各个部分的含义,如折线图中的坐标系所示。

Page 92: Java Web动态图表编程

图 3.1 图表的布局结构

提示:为使 Web 动态图表的标题更具美观和多样性,读者可以在本地计算机中安装几套

字库。本书中所使用的字库除 Windows 系统自带的以外,还使用了额外安装的方正字库、经

典字库等。

3.1.1 垂直柱状图

垂直柱状图是使用最频繁的一种 Web 图表。我们就用它作为第一个 Web 动态图表的例

程。现在我们希望用垂直柱状图来表示某书店 5种计算机编程类书籍在某天销售量的统计情

况。各书籍的销售量从一个 1~100 之间的随机数中产生。如图 3.2 所示,程序 VBar.java (\chap03\Fig3.1\Fig.3.1_01)的运行结果。

图 3.2 实际上是由三部分组成:

(1)图表标题:也就是文本“Java Web 图表设计 Applet 版——柱状图” 。

(2)图表区又由两部分组成:

Ø 坐标系:横坐标表示编程书籍,纵坐标表示销售数量。

Ø 表示不同书籍销售量的柱状图。

每次我们重新刷新页面时,VBar.java 程序的运行都会重新生成新的、随机的书籍销售数

据。当销售数据发生变化后,图 3.2 中所示了表示各书籍销售量的柱状图也相应地发生变化。

标题区

图表绘制区

标题区

图表绘制区

Page 93: Java Web动态图表编程

图 3.2 VBar.java的运行结果(垂直柱状图)

垂直柱状图的设计思路:

(1)首先获得每种书籍的销售量。

(2)确定标题区域的范围。

(3)确定图表绘制区域的范围。

(4)绘制标题。

(5)在图表绘制区域内,绘制以下图形对象:

Ø 表示书籍销售量的纵坐标。

Ø 表示书籍名称的横坐标。

Ø 用绘制实心矩形来代表垂直的条形,确定相邻矩形的间隔宽度。

Ø 根据每本书的销售量依照一定的比例绘制不同高度的实心矩形。

本章所有代码采用前一章所介绍过的双缓冲技术。程序 VBar.java第 8 行~第 9 行:

Image offImage; Graphics offGraphics;

声明用于绘制缓冲区图形 Image对象 offImage及其在缓冲区的绘图环境 offGraphics。

程序第 12 行:

int appltWidth = 600, appletHeight = 500;

声明了两个整型变量 appletWidth及 appletHeight 并对其初始化,这两个变量分别表示本 Applet 的宽度和高度,它们的值和调用该 Applet 的 HTML 文档中 Applet 的参数相同。注:

如果没有特别说明, 本章中所有的 Java 源程序中与 Applet 相关的宽度和高度的取值与调用该 Applet 的 HTML文档相同。

程序第 14 行:

int bookSales[] = new int[5];

声明了一个整型数组 bookSales,表示书籍的销售量, 我们将在 Applet 中的 init 方法里对

其进行初始化。

程序第 16 行~第 19 行:

String bookTitle[] =

"Python", "JAVA", "C#", "Perl", "PHP"

Page 94: Java Web动态图表编程

;

声明并初始化了一个字符串数组 bookTitle,表示被统计的计算机书籍的书名。这里我们

用了 Python、JAVA、C#、Perl,以及 PHP 这五个字符串来表示这些书名。

程序第 22 行~第 25 行:

Color color[] =

new Color(99, 99, 0), Color.GREEN, Color.YELLOW, Color.RED, Color.BLUE ;

声明了包含有五个 Color 对象的 Color 数组,数组中的每种颜色用于表示不同的书籍。

其中 new Color(99, 99, 0)的颜色效果为褐色。

到这里,VBar 类的成员变量就全部声明完毕。那么如何在宽为 600 像素,高为 500 像素

的绘图环境中分配标题区域和图表绘制区域?如何在图表绘制区域中再划分柱状图的绘制区

域?如图 3.3所示。

图 3.3 垂直柱状图表的设计思路

我们的绘制思路如下:

(1)标题区域占据整个 Applet 最上面高度为 30 像素,宽度为 600像素的矩形区域。也

就是图 3.3 中最上面的绿色区域。

(2)剩下的区域都是图表绘制区域。除标题外,其余所有图表对象都绘制在此区域内。

除去标题区域,图表绘制区域实际上的宽度为 600 像素,高度为 470 像素。本例中,此区域

内的图形绘制对象又由两部分组成:

Ø 坐标系。纵坐标表示书籍的销售量,因为每本书的销售量是从一个 1~100 之间的随

机数中产生的,也就是说,每种书的销售量最大值为 100,如果直接用像素来表示,

仅仅需要 100 像素就够了。现在我们还剩下 470 像素,如果仅用 100 像素,不仅不美

观,而且也浪费了大量的空间。所以我们在垂直方向上用 380 像素来表示销售量为 100 的这个数据,简而言之,也就是每一个单位的销售量就表示纵坐标上 38 像素。

横坐标表示书籍的名称,本例中共有五种不同的书籍。这里我们用 400像素的宽度来

表示这些书籍,也就是说,实心矩形的绘制区域在宽度为 400 像素,高度为 380像素

的矩形区域内,而我们最终确定的坐标系原点位于点(80,445)。两条坐标轴,从美

观和实际的运行效果来看,应该分别比实心矩形绘制区域的宽度和高度的值更大一

些。从图 3.3 我们可以看到,纵轴的最高点比矩形绘制区域的高度大 20 像素,坐标

为 (80, 40); 横轴的端点比实心矩形绘制区域的宽度更多出 70 像素, 坐标位于 (550, 445)。

标题区域

图表绘制区域

(480,65)

(80,40)

(80,65)

实心矩形图绘制区

(80,445)

坐标系原点 (480,445) (550,445)

Page 95: Java Web动态图表编程

Ø 上述坐标系确定后,就可以确定实心矩形的绘制坐标了。我们先确定所有代表书籍销

售量的实心矩形的底端和横轴相重合,然后就可以推算出除实心矩形的高度后,就可

以推算出实心矩形的左上角顶点的坐标。另外,我们设定所有的实心矩形的宽度为 50 像素。

Ø 绘制的顺序是,先绘制标题区域,然后是绘制代表书籍销售量的 10 条直线,以及代

表各书籍实际销售量的实心矩形,最后是绘制坐标系及其说明文字。

在理清上述Web 图表的绘制思路后,我们接着讨论源程序的 init 方法:

程序第 28 行~第 38 行:

public void init()

offImage = createImage(appltWidth, appletHeight); offGraphics = offImage.getGraphics();

for (int i = 0; i < bookSales.length; i++)

bookSales[i] = 1+(int)(Math.random() * 100);

首先还是创建缓冲区 Image对象 offImage及其绘图环境对象 offGraphics,然后初始化整

型数组 bookSales。注意语句:

1+(int)(Math.random() * 100);

正好生成一个 1~100之间的整数。通过一个循环,将 bookSales 中的每个元素都进行赋

值。 接着会运行 paint方法, 然后在 paint方法中调用第 48行~第 96行的 update方法。 在 update 方法里面的第 51 行~第 53 行:

offGraphics.setColor(Color.black); offGraphics.setFont(new Font("方正粗宋简体", Font.BOLD, 30)); offGraphics.drawstring("Java Web图表设计 Applet版——柱状图", 15, 30);

就实现了绘制标题区域的功能。用黑色,粗体,30 磅的“方正粗宋简体”这种字体在 Applet 坐标的点(15,30)处绘制了文本“Java Web 图表设计 Applet 版——柱状图” 。

完成标题区域的绘制后,现在进入图表绘制区域。

程序第 56 行:

offGraphics.setFont(new Font("SansSerif", Font.PLAIN, 12));

重新设置当前的绘制字体。

前面说过,实心矩形绘制区域的高度为 380 像素表示 100 个单位的销售量,也就是说,

每 38 像素就相当于 10 个单位的销售量。为了更清晰地表达销售量的显示效果,我们在实心

矩形的绘制区域内再绘制 10 条直线,分别表示 10、20、30、…、90、100个单位的销售量,

并且在每条直线旁标明该条直线所代表的销售量的数据。

程序第 57 行~第 67行,实现了上述功能:

int salesValue = 0; for (int i = 418; i > 0; i ­= 38)

offGraphics.setColor(Color.black); offGraphics.drawstring("" + salesValue, 36, (i + 27));

offGraphics.setColor(Color.lightGray); offGraphics.drawLine(80, (i + 27), 520, (i + 27));

salesValue += 10;

Page 96: Java Web动态图表编程

先声明了一个局部的整型变量 salesValue,并将其值初始化为 0。因为坐标系的原点位置

在点(80,445)处,也就是说,销售量为 0 的直线的纵坐标值都为 445,而销售量为 10 个

单位的直线纵坐标值都为 445−38=407。先从最下面一条直线开始绘制,第 1 次循环,循环变

量 i 的值为 418,注意第 61 行,我们的用法:

offGraphics.drawstring("" + salesValue, 36, (i + 27));

drawString方法接收的第 1个参数是字符串型,所以我们用:

"" + salesValue

这种方式, 将一个内容为空的字符串 (注: 不是空字符串 null) 和其后面的整数 salesValues 连接起来后,整个结果就被 Java 的编译器自动转变成一个字符串。这种用法非常简单,推荐

大家使用。

也就是说,第 1 次循环,程序第 61 行和第 64 行,实际执行的内容是:

offGraphics.drawstring("0", 36, 445); offGraphics.drawLine(80, 445, 520, 445);

之后,salesValue的值递增 10。

同理,第 2次绘制,程序第 61行和第 64 行,实际执行的内容是:

offGraphics.drawstring("10", 36, 407); offGraphics.drawLine(80, 407, 520, 407);

其余步骤,此处不再赘述。程序执行完第 67 行后,绘制完成的图像如图 3.4①所示。

图 3.4 垂直柱状图表的绘制过程

接着绘制表示各种计算机书籍销售量的实心矩形。

程序第 70 行~第 81 行:

int drawHigh = 0;

for (int i = 0; i < bookTitle.length; i++) offGraphics.setColor(color[i]); drawHigh = (int)(Math.ceil(bookSales[i] * 3.8));

Page 97: Java Web动态图表编程

offGraphics.fill3DRect(110+i * 80, 445­drawHigh, 50, drawHigh, true);

offGraphics.setColor(Color.BLACK); offGraphics.drawstring(bookTitle[i], 110+i * 80, 465);

首先声明了一个局部变量 drawHigh,该变量表示销售量所对应的矩形的绘制高度。接着

我们用一个循环,计算出每种计算机书籍的销售量所对应的绘制高度。然后程序在第 77 行,

绘制实心矩形:

offGraphics.fill3Drect(110+i * 80, 445­drawHigh, 50, drawHigh, true);

实心矩形的左上角顶点的绘制参数由下列方法计算:

横坐标:110+i * 80 纵坐标:445–drawHigh 也就是说,第一次循环绘制第 1 个实心矩形左上角的横坐标为 110。而每一个循环后,

实心矩形的横坐标依次增大 80像素。矩形的纵坐标由其绘制高度通过“445– drawHigh”得

到,矩形的宽度为 50 像素(即两个相邻之间的实心矩形的间隔距离是 30 像素),高度为 drawHigh像素。这样就保证了所有的实心矩形的下端都和纵坐标为 445 的直线相重合,而矩

形的填充颜色由 Color 对象的数组 Color 决定。

然后重新设置当前绘图颜色为黑色,再通过调用 drawString 方法绘制出该实心矩形所代

表计算机书籍的名称,程序第 80 行:

offGraphics.drawString(bookTitle[i], 110+i * 80, 465);

注意,这里的纵坐标都是 465,就保证了所有的说明文字都在同一水平线上(纵坐标为 465 的直线),而横坐标正好和矩形左上角顶点的横坐标相同,这样实心矩形和其说明文字也

就排列整齐了。

程序循环的次数由表示计算机书籍名称的字符串数组 bookTitle决定。

本段循环结束后,程序绘制的结果如图 3.4②所示。

接着进入坐标系的横轴和纵轴的绘制过程。程序第 84 行~第 86行:

offGraphics.setColor(Color.BLACK); offGraphics.drawLine(80, 40, 80, 445); offGraphics.drawLine(80, 445, 550, 445);

首先设置当前的绘制颜色为黑色。横轴和纵轴通过绘制两条直线得到。我们从横轴和纵

轴的绘制参数可以得知:两条直线交点的坐标为(80,445)。

程序运行完第 86 行后的结果如 3.4③所示。

然后,绘制坐标系中横轴和纵轴的文字说明,程序第 89 行~第 91行完成该功能:

offGraphics.setFont(new Font("黑体", Font.BOLD, 16)); offGraphics.drawString("销售量", 20, 50); offGraphics.drawString("编程书籍", 500, 465);

用黑体,大小为 16 磅的字体在点(20,50)处绘制文本“销售量” (纵轴的文字说明),

然后在点(500, 465)处绘制文本“编程书籍” (横轴的文字说明)。

程序第 94 行,完成最后的工作:

g.drawImage(offImage, 0, 0, null);

将缓冲区的图像内容全部输出到 Applet 中,并结束 updatefh方法。至此,垂直柱形图的

所有绘制工作就完成了。程序的实际运行效果如图 3.2 所示。

Page 98: Java Web动态图表编程

3.1.2 饼图

饼图也是一种应用非常广泛的图表类型。常用于表示数据之间的对比关系。我们现在用

饼图来表示某书店五种计算机编程类书籍某天的销售量和当天所有书籍销售总量的对比关

系。各书籍的销售量仍旧从一个 1~100之间的随机数中产生。

程序 Pie.java 演示了如何绘制饼图。同样地,我们先看图 3.5 所示的 Pie.java(\chap03\ Fig3.1\Fig.3.1_02)的运行结果,然后分析该程序的运行流程。

图 3.5 Pie.java的运行结果

图 3.5 实际上也是由三部分组成的:

(1)图表标题:也就是文本“Java Web 图表设计 Applet 版——饼图” 。

(2)图表区又由两部分组成:

Ø 图 3.5 右边的图例。

Ø 图 3.5 左边部分表示各种计算机书籍销售量及其与销售总量之间比例关系的饼图。饼

图的效果实际上是通过绘制一组颜色各异的实心圆弧而得到的。

每次我们重新刷新页面的时候, Pie.java 程序的运行都会重新生成新的、 随机的书籍销售

数据。当销售数据发生变化后,图 3.5中的饼图也会相应地发生变化。

饼图的设计思路:

(1)首先获得所有书籍的销售量。

(2)计算所有书籍的销售总量及每本书的销售量占所有书籍销售总量的比例。

(3)确定标题区域的范围。

(4)确定图表绘制区域的范围。

(5)绘制标题。

(6)在图表绘制区域内,再绘制以下图形对象:

Ø 图例:图例中又包含 4 个元素,即表示某种图书的实心矩形小方块,书籍名称、书籍

销售量、该销售量占销售总量的比例。

Ø 绘制表示销售量的实心圆弧。实心圆弧的大小(弧度)由其销售量占销售总量的比例

决定。

程序第 6 行,引进了一个新的包:

import java.text.*; // 引入 java.text包中所有的类

java.text 包允许通过与特定语言无关的方式格式化文本消息、日期和数值。本例程将引

Page 99: Java Web动态图表编程

用该包中的 DecimalFormat 类,用于格式化浮点数的格式化输出。

与前例相同,程序第 10 行~第 11 行:

Image offImage; Graphics offGraphics;

声明了用于绘制缓冲区图形对象的 Image 对象 offImage 及其在缓冲区的绘图环境 offGraphics。

程序第 13 行:

int appltWidth = 600, appletHeight = 500;

声明了两个整型变量 appletWidth和 appletHeight 并对其初始化,这两个变量分别表示本 applet 的宽度和高度。

程序第 15 行:

int bookSales[] = new int[5];

声明了一个整型数组 bookSales,表示书籍的销售量, 我们将在 Applet 中的 init 方法中对

其进行初始化。

程序第 17 行:

int totalSales = 0;

声明了一个整型变量 totalSales,表示书籍的销售总量,即各种书籍销售量之合计。默认 totalSales 的值为 0。我们将在 Applet 中的 init 方法中对其进行再次赋值的操作。

程序第 19 行~第 22 行,声明并初始化了一个字符串数组 bookTitle,表示被统计的计算

机书籍的书名。这里我们同样用了 Python、JAVA、C#、Perl,以及 PHP 这 5 个字符串来表示

这些书名。

程序第 25 行~第 28 行,声明了包含有 5 个 Color 对象的 Color 数组,数组中的每种颜

色用于表示不同的书籍。其中 new Color(99, 99, 0)的颜色效果为黄褐色。

到这里,Pie 类的成员变量就全部声明完毕。那么如何在宽为 600 像素,高为 500 像素

的绘图环境中分配标题区域和图表绘制区域?如何在图表绘制区域中划分饼图的绘制区域以

及图例的绘制区域?如图 3.6所示。

图 3.6 饼图的设计思路

本例中饼图的绘制思路如下:

(1)标题区域占据整个 Applet 最上面高度为 30 像素,宽度为 600像素的矩形区域。也

就是图 3.3 中最上面的绿色区域。

350

标题区域

图表绘制区域 190

(395, 95) (15, 95)

300

实心圆弧图绘制区 图例绘制区

编程类图标 销售数量 所占比例

Python 50 22.94%

JAVA 38 17.43%

C# 52 23.85%

Perl 33 15.14%

PHP 45 20.64%

Page 100: Java Web动态图表编程

(2)剩下的区域都是图表绘制区域。除标题外,其余的所有图表对象都绘制在此区域

内。除去标题区域,图表绘制区域的实际宽度为 600像素,高度为 470 像素。本例中,此区

域内的图形绘制对象又由两部分组成:

Ø 左侧的实心圆弧绘制区。一组绘制在一起的实心圆弧就组成了一个饼图的样式。该区

域中, 除去蓝色的实心圆弧外, 所有圆弧的外切矩形的左上角的顶点坐标为 (15, 95)。

为了美观地显示效果,我们设定其中一个实心圆弧(即蓝色圆弧)的外切矩形的左上

角的顶点坐标为(30, 93),也就是向右水平移动了 15 像素,向上垂直移动了两个像

素, 这样的显示效果比较美观。 另外, 所有实心圆弧的外切矩形的宽度都是 350 像素,

高度为 300像素。

Ø 确定了圆弧的外切矩形的宽度、高度,以及左上角的顶点坐标后,就可以绘制实心圆

弧了。我们将绘制的第 1 个圆弧的起点角度在 30 度,而圆弧的圆心角的大小由其销

售量决定。

Ø 右侧的图例绘制区。图例被确定在一个蓝色的空心矩形内,该矩形左上角顶点

坐标为(395,95),和左侧的实心圆弧的外切矩形的左上角的顶点坐标相同,处于

同一高度。其宽度为 190 像素,高度为 300 像素。在图例绘制区内,再依次采用相对

应的颜色绘制 1 个实心的小矩形,以及与其对应的书籍名称、销售数量及其所占销售

总量的比例。

Ø 绘制的顺序是,先绘制标题区域,然后绘制仅有一个空心矩形的不含任何文字说明的

图例,之后是绘制实心圆弧及其相应的图例。

明白饼图的绘制思路后,我们接着讨论 Pie.java 源程序中的 init 方法:

程序第 31 行~第 42 行:

public void init()

offImage = createImage(appletWidth, appletHeight); offGraphics = offImage.getGraphics();

for (int i = 0; i < bookSales.length; i++)

bookSales[i] = 1+(int)(Math.random() * 100); totalSales += bookSales[i];

首先还是创建缓冲区 Image对象 offImage及其绘图环境对象 offGraphics。然后初始化整

型数组 bookSales。与前例不同的是,这里我们在对数组 bookSales 赋值的时候,同时也进行

销售总量的计算:

totalSales += bookSales[i];

每循环一次,就将新的、随机生成的数据累加到变量 totalSales 中。循环完毕,则得到了

销售总量的数值。

接着运行 paint 方法,然后在 paint 方法中调用程序第 52 行~第 106 行的 update 方法。

在 update方法里面,我们首先绘制 Applet 的背景,程序第 55 行~第 56 行:

offGraphics.setColor(new Color(144, 255, 255)); offGraphics.fillRect(0, 0, appletWidth, appletHeight);

用蓝色(new Color(144, 255, 255))填充了整个 Applet 背景。填充背景的功能实际上是

通过填充一个同我们设定的 Applet 宽度和高度都相同的实心矩形而实现的。

程序第 59 行~第 61 行:

Page 101: Java Web动态图表编程

offGraphics.setColor(Color.BLACK); offGraphics.setFont(new Font("方正隶变简体", Font.BOLD, 30)); offGraphics.drawString("Java Web图表设计 Applet版——饼图", 15, 30);

这里就实现了绘制标题区域的功能。用黑色、粗体、30 磅的“方正隶变简体”这种字体

在 Applet 坐标点(15,30)处绘制了文本“Java Web 图表设计 Applet 版——饼图” 。

完成标题区域的绘制后,现在进入图表绘制区域。

程序第 64 行:

offGraphics.drawString("计算机编程类图书销售统计表", 70, 465);

在 Applet 的底部绘制一条文本“计算机编程类图书销售统计表” 。采用的字体和前面的

相同。

程序第 65 行~第 71行:

offGraphics.setFont(new Font("SansSerif", Font.PLAIN, 12)); offGraphics.drawString("编程类图书", 400, 125); offGraphics.drawString("销售数量", 475, 125); offGraphics.drawString("所占比例", 535, 125);

offGraphics.setColor(Color.blue); offGraphics.drawRect(395, 95, 190, 300);

重新设置字体后,绘制了 3条文本: “编程类图书” 、 “销售数量”及“所占比例” 。它们

的纵坐标都为 125,同在一条水平线上。然后绘制一个空心的蓝色矩形,其绘制参数如图 3.6 所示,左上角顶点(395,95),宽度和高度分别为 190 像素和 300像素。

程序运行完第 71 行后,绘制结果如图 3.7 所示。

图 3.7 Pie.java的运行流程

程序第 74 行:

int arcStartAngle = 30, arcAngle = 0;

声明了两个整型变量 arcStartAngle和 arcAngle。 arcStartAngle表示绘制圆弧的起始角度,

默认的起始角度是 30度。arcAngle表示圆弧两条边线之间的夹角(圆心角),默认值为 0。

程序第 75 行:

Page 102: Java Web动态图表编程

float proportion;

声明了 1 个浮点变量:proportion,该变量表示某种图书销售量与所有图书销售总量之

间的比例关系。

程序第 76 行:

DecimalFormat twoDigits = new DecimalFormat ("0.00");

声明了 1 个 DecimalFormat 类的对象 twoDigits。DecimalFormat 类隶属于 java.text 包。我

们前面讲过,引用该包中的 DecimalFormat 类,用于格式化浮点数的格式化输出。在本例中,

我们决定输出的变量 proportion 值的小数点右边带两位小数(即精确到 0.01)。该语句创建

一个格式为“0.00”进行初始化的 DecimalFormat 对象,其中每个“0”分别指定格式化浮点

数中必不可少的数位。这种特定的格式表明每个用 twoDigits 格式化后的数字,其小数点坐

标至少有 1位, 而小数点右边至少保留两位。 如果被格式化的数字与上面的格式要求不匹配,

则“0”将被自动插入到该数相应的数位上。

然后程序就进入了实心圆弧及其图例说明文字的绘制过程,程序第 78 行~第 101 行实

现了这个过程:

for (int i = 0; i < bookTitle.length; i++)

arcAngle = (int)(bookSales[i] * 360 / (float)totalSales + 0.5); proportion = ((float)bookSales[i]) / totalSales * 100;

offGraphics.setColor(color[i]);

if (i < bookTitle.length ­ 1)

offGraphics.fillArc(15, 95, 350, 300, arcStartAngle, arcAngle); else

offGraphics.fillArc(30, 93, 350, 300, arcStartAngle, arcAngle);

arcStartAngle += arcAngle;

offGraphics.fillRect(400, 155+i * 50, 12, 12); offGraphics.setColor(Color.black); offGraphics.drawString(bookTitle[i], 420, 167+i * 50); offGraphics.drawString("" + bookSales[i], 490, 167+i * 50); offGraphics.drawString("" + twoDigits.format(proportion) + "%", 540,

167+i * 50);

在循环体中,程序第 80 行首先计算圆弧的圆心角的度数。注意,我们又用了在浮点数

后面再加上 0.5,然后再取整的操作,达到了小数点后一位数字四舍五入的功能。圆心角的值

等于该种图书的销售量除以销售总量再乘以 360。因为 Java 的除法操作符“/”默认是整除。

因为我们声明的整型数组变量 bookSales 中的元素,以及变量 totalSales 都是整数。所以,在

整型变量 totalSales 前面用了一个关键字“float”就将整型变量 totalSales 的值转变为浮点类

型的数值了。当执行除法操作时,结果就不再是整数,而自动转变为 folat 类型的浮点数了。

然后把得到的浮点类型的结果加上 0.5再取整后, 实现了小数点后一位数字四舍五入的功能,

最后再把取整后的结果赋值给变量 arcAngle,这样我们就得到了当前圆弧的圆心角的度数。

程序第 81 行,计算当前销售量占销售总量的百分比。同样的,我们强制转换 bookSales [i]为浮点型。所以当所得的商再乘以 100 后,结果仍是浮点型,然后结果被赋值给 float 变量 proportion。

程序第 83 行,设定当前的绘制颜色。接下来,绘制实心圆弧:

Page 103: Java Web动态图表编程

if (i < bookTitle.length ­ 1)

offGraphics.fillArc(15, 95, 350, 300, arcStartAngle, arcAngle); else

offGraphics.fillArc(30, 93, 350, 300, arcStartAngle, arcAngle);

上述代码说明,如果绘制的不是最后一个实心圆弧(图 3.5 和图 3.6 中所示的蓝色实心

圆弧),则一直执行程序第 87 行。该圆弧的外切矩形的参数都是一样的,即图 3.6 中所示的

左上角顶点坐标为(15,95),宽度为 350像素,高度为 300 像素。第 1 次循环,arcStartAngle 的值为 30,也就是说,第 1 个圆弧的起始位置在 30 度,圆弧的圆心角的大小已经在程序第 80 行计算出了。这时,根据这些参数,就可以绘制出第 1 个实心圆弧。

如果这时局部变量 i 的值等于 bookTitle.length−1,则说明绘制的是最后一个实心圆弧,

执行程序第 91 行。就是圆弧的外切矩形的左上角顶点坐标变更为(30,93),宽度和高度保

持不变,分别为 350像素和 300 像素。

程序第 94 行:

arcStartAngle += arcAngle;

重新计算绘制下一个圆弧的起始角度。 本条语句实现的功能是, 绘制下一个圆弧的时候,

其起始角度正好和当前圆弧的结束位置相同。

绘制完实心圆弧后,接着绘制图例部分。程序第 96 行,先用当前颜色绘制一个宽度和

高度都为 12像素的实心矩形,然后程序第 97 行,改变当前的绘制颜色为黑色。

程序第 98 行,绘制当前书籍的名称。程序第 99行,绘制当前书籍的销售量。

程序第 100行:

offGraphics.drawString(twoDigits.format(proportion) + "%", 540, 167+i * 50);

先格式化浮点变量 proportion的输出格式。注意,这里格式化字符串所使用的方法:

twoDigits.format(proportion)

调用 DecimalFormat 类的 format 方法,来格式化所传递参数的输入格式,然后在返回的

结果后再加上一个“%”符号,然后再绘制其百分比。

在绘制书籍名称、 销售数量及销售比例这 3 个字符串的时候, 它们的纵坐标都是相同的,

都是 167+i * 50。这样就保证了这三个字符串的绘制高度相同。而每循环一次,绘制的新的

这三条字符串的高度会比当前字符串的绘制位置低 50 像素。

程序运行到这里,就结束了一次循环,然后会重复上述过程。

循环语句执行结束后,程序运行第 104行:

g.drawImage(offImage, 0, 0, null);

将缓冲区的图像内容发送到 Applet 上显示,之后便结束了 update方法,饼图的所有绘制

工作就完成了。程序的实际运行效果如图 3.5 所示。

3.2 Java Applet生成单据

从前面的例程我们可以看到,用 Applet 来生成 Web 动态图表是非常方便的。有时候,我

们需要生成一些复杂的图表:

(1)带有某个公司徽标的图表。

图 3.8 Java的徽标

Page 104: Java Web动态图表编程

(2)各种单据:

Ø 账本及相关凭证;

Ø 支票;

Ø 销售发票;

Ø 其他单据:如发货单、提货单、入库单、检验单、海关报关单,等等。

下面简单谈谈解决上述问题的思路,先说说第一个问题:比如说要绘制一个附带有如图 3.8 所示的 Java 徽标。

解决方法很简单,上一章我们讨论过在 Java Applet 如何加载并显示图像。那么,我们就

利用这一功能来在 Applet 中载入某个公司的徽标即可。 对于第二个问题, 以支票的生成为例。

我们认为最高效及简单的解决办法就是:通过扫描一张完整的、清晰的空白支票而得到一张

图像,然后以该图像的宽度和高度生成一个大小相同的 Java Applet,仔细调整相关的绘制参

数,在需要填写的位置上绘制出相关的内容即可。这种方法同样也适用于生成其他各种形式

的单据。

这里,我们提供两个简单的实例,分别展示如何用上述思路来解决这些问题。

3.2.1 带徽标的 Web 动态图表

以本章 3.1节的VBar.java为例, 在该程序的基础上再显示一个如图 3.8所示的 Java徽标,

那么该如何修改 VBar.java 源程序呢?

源程序 LogoVBar.java(\chap03\Fig3.2\Fig3.2_01)是 VBar.java 的升级版本,实现了上述

功能。本程序和第 3.1 节的 VBar.java 相比,仅仅改动了几个地方。

在程序第 9行,新增加了一个 Image类的对象:

Image logoJPG;

在程序第 38行,init 方法中用 Applet 类的 getImage 方法对其进行初始化:

logoJPG = getImage(getDocumentBase(), "javaLogo.jpg");

在 update方法中,程序第 52 行:

offGraphics.drawImage(logoJPG, 100, 0, this);

在点(100,0)处绘制 Image对象 logoJPG。

程序第 55 行:

offGraphics.drawString("带 Java徽标的 Web图表", 135, 40);

重新绘制了一条文本,我们改变了文本的内容和绘制位置。程序的其他部分没有任何改

变,读者可以参考第 3.1 节的解释。

程序的运行结果如图 3.9 所示。

Page 105: Java Web动态图表编程

图 3.9 带徽标的 LogoVBar.java的运行结果

Page 106: Java Web动态图表编程

3.2.2 支票的生成

前面提到,生成类似支票这样的单据的最高效及简单的办法就是:通过扫描一张完整、

清晰的空白支票而得到一张图像,然后以该图像的宽度和高度生成一个大小相同的 Java Applet,在该 Applet 中加载并显示该图像,然后仔细调整相关的绘制参数,在需要填写的位

置上绘制出相关的内容即可。

上述方法的前提是仅仅需要实现扫描一张完整、清晰的空白支票。下面以一张我们自

己制作的转账支票为例,讲述如何生成单据,如图 3.10 所示。

图 3.10 转账支票的外观

简单地说,支票分为左右两部分。左边的部分是支票存根,右边部分是交付给收款人或

银行。需要填写的内容主要有“出票日期” 、 “付款金额” 、 “收款人” 、 “出票人账号” 、 “用途” 、

“付款行名称” 、 “科目” ,以及“对方科目”等栏目。似乎只需要调整好上述各栏目绘制内容

的坐标就可以很轻松地完成支票的生成工作了。其实不然,支票的绘制看似简单,实则有几

个不大不小的麻烦需要处理。

1.出票日期的处理

Ø 需要将年、月、日提取出来,并将其分别输入到相应栏目。另外,在支票右边部分“出

票日期”一栏,需要输入大写的日期。也就是说,这里涉及到如何将“2004­12­13”

这种日期格式中的年、月、日转换成 “贰零零肆”年“拾贰”月及“拾叁”中文格

式。

Ø 对于“年”的转换,我们可以将阿拉伯数字和中文大写数字进行简单的一对一的转换

操作,如“1999” 、 “2005”可以直接转换成“壹玖玖玖” 、 “贰零零伍” ;

Ø 对于“月”的转换,却不能直接将阿拉伯数字和中文大写数字进行简单的一一对应的

转换操作,如“10” 、 “11” 、 “12”这三个月份,按简单的一一对应的方法转换,我们

得到的是“壹零” 、 “壹壹”和“壹贰” 的结果,这不符合我们的习惯称呼。 “10” 、 “11” 、

“12”这三个月份的转换结果应该是“拾” 、 “拾壹”和“拾贰” 。但对于 1~9 月份,

则可以直接采用按简单的一一对应的方法进行转换。

Ø 对于“日”的转换,同“月”的转换类似。1~9 之间的“日” ,可以直接转换。 “10” 、

“20” 和 “30” 这几个日子的转换结果, 应该分别是 “拾” 、 “贰拾” 和 “叁拾” 。 而 “11” ~

“19”之间的日子转换结果应该是“拾壹”~“拾玖” ; “21”~“29”之间的日子转

换结果应该是“贰拾壹”~“贰拾玖” ; “31”转换成“叁拾壹” 。

2.转账金额的处理

这里涉及到以下几个方面:

Page 107: Java Web动态图表编程

Ø 如何将小写的转账金额转换成中文的大写金额,即如何将“6 437192.08”这类的小写

金额转换为“陆佰肆拾叁万柒仟壹佰玖拾贰圆(元)零角捌分”这种中文货币的大写

金额。

Ø 转账金额在支票左边部分的绘制还需要将其格式化成货币的表现形式, 即用小数点将

元和角、分隔开,并在千位,百万,十亿(以千位单位)的数位上用逗号隔开。如“6 437192.08”应该表示为: “¥6 437 192.08”的格式。

Ø 转账金额在支票右边的绘制内容有两部分,一部分需要在“人民币大写”一栏内,以

中文大写格式填入转账金额, 另一部分是将阿拉伯数字格式的转账金额中的每一个数

字都相应地填写到“人民币大写”一栏右边的小写栏内。注意,在小写栏内转账金额

的最高位数字前还需要绘制一个人民币(¥)的符号。

源程序 CheckMaker.java(\chap03\Fig3.2\Fig3.2_02)演示了如何实现支票的绘制功能。

上面谈到了程序应该对日期及金额进行中文转换的操作,在本例程中,除了没有提供“将小

写的转账金额转换成中文的大写金额”这一功能外,其余所有功能都可以在源程序中通过一

些简单而巧妙的方法实现。我们在 CheckMaker.java 引用了一个自定义的 DateToChinese类来

实现阿拉伯数字的年、月、日到中文大写日期的转换功能。

我们先看看源程序 CheckMaker.java 的运行结果,如图 3.11 所示。

图 3.11 CheckMaker.java的运行结果

我们先讨论实现阿拉伯数字的年、 月、 日到中文大写日期转换功能的 DateToChinese. java (\chap03\Fig3.2\Fig3.2_02)的源程序的运行机制。

程序第 7 行~第 10 行:

String[] chineseNumber =

"零", "壹", "贰", "叁", "肆", "伍", "陆", "柒", "捌", "玖", "拾" ;

定义了一个字符串数组 chineseNumber,该数组中每一个元素分别对应着用中文表示的

阿拉伯数字中的 0~9。

程序第 12 行~第 15 行:

public DateToChinese()

Super();

声明默认的构造器,在其方法体内,用 super 方法来调用其父类默认的构造器方法。程

序第 4 行在声明类的名字为 DateToChinese 的时候,并没有指明本类所继承的父类。在这种

情况下,Java 默认所有未指明父类的类都将继承 Object 类,Object 类位于 java.lang包中。实

Page 108: Java Web动态图表编程

际上,Object 类是所有 Java 类的超类,也就是说,每个 Java 类都是 Object 类的子类或间接

子类,都继承 Object 类所声明的 11个方法。根据 Java 机制,每个 Java 类的默认构造器中,

不论程序是否显示 super 方法来调用其父类默认的构造器方法, 编译器都将用 super 方法来调

用其父类的默认构造器方法。

类的继承、封装及多态的内容不属于本书讨论的范畴,但因本书中的源程序涉及到这三

个面向对象设计的最重要的概念,所以在这里我们仅做一个很简单的介绍。

除默认的构造器外,DateToChinese类提供了 5 个方法以实现不同的功能:

Ø 程序第 17 行,getDayNumber 方法返回日期中“日”的中文大写转换结果。

Ø 程序第 48 行,getMonthNumber 方法返回日期中“月”的中文大写转换结果。

Ø 程序第 61 行,getCapitalNumber 方法返回日期中“年”的中文大写转换结果。

Ø 程序第 85 行,quotient 方法返回两个整数之间的整除运算结果。

Ø 程序第 90 行,remainder 方法返回两个整数之间的求模(余数)运算结果。

我们先介绍程序第 61行~第 83行的 getCapitalNumber 方法。该方法代码如下:

public String getCapitalNumber(int number) int divisor = 1, digit; String output = "";

// 找到“最大的基数” for (int i = 1; i < number; i *= 10) divisor = i;

while (divisor >= 1) digit = quotient(number, divisor);

output += chineseNumber[digit];

number = remainder(number, divisor); divisor = quotient(divisor, 10);

return output; //分离数字结束

getCapitalNumber 方法接收一个整型参数,以该方法接收到的整型参数“2004”为例,

讨论该方法的实现过程。

程序第 63 行,声明了两个整型变量:divisor 和 digit,其中 divisor 的初始值为 1。

程序第 64 行,声明了 1 个字符串变量:output,初始化为一个内容为空的字符串,表示

要返回的字符串结果。

程序第 67行~第 70行,用一个循环找到整数 2004的“最大基数” 。 “最大基数”在这里的

意思是指某个整数最高位数(也就是,从左往右数第 1个非零数字)的基数,比如说:

2004 = 2 * 1000 + 0 * 100 + 0 * 10 + 4 * 1

所以 2004 的最大基数是 1000。又如:

642589 = 6 * 100000 + 4 * 10000 + 2 * 1000 + 5 * 100 + 8 * 10 + 9 * 1

所以 642589 的最大基数是 100000。

计算最大基数后,并将最大基数赋值给变量 divisor,此时 divisor 的值为 1000。

接下来,程序第 72 行~第 80 行的另一个 while循环:

while (divisor >= 1)

Page 109: Java Web动态图表编程

digit = quotient(number, divisor);

output += chineseNumber[digit];

number = remainder(number, divisor); divisor = quotient(divisor, 10);

divisor 的值为 1000,大于 1,所以进入 while循环,在其循环体内:

程序第 74 行,调用 quotient 方法,并传递两个参数:2004 和 1000。然后程序会跳转至

第 85 行的 quotient 方法:

public int quotient(int a, int b) return a / b;

quotient 方法接收两个整型参数,并返回这两个整数执行整除运算的结果。这里接收的

是 2004 和 1000,因为 2004/1000=2,所以 digit 的值为 2。然后程序返回 getCapital Number 方法中的 while循环中第 76 行,继续执行。

此时,相当于执行:

output += chineseNumber[2];

而 chineseNumber[2]的结果为“贰” ,因此,字符串变量 output 的值现在就变为“贰” 。

接下来,程序执行第 78 行,调用 remainder 方法,并提供两个整型参数:2004 和 1000,

然后程序会跳转至 90行的 remainder 方法:

public int remainder(int a, int b) return a % b;

remainder 方法也接收两个整型参数,并返回这两个整数执行求模运算的结果。这里接收的

是 2004和 1000,因为 2004%1000=4,所以返回的结果是 4。然后程序返回 getCapital Number 方法中的 while循环中第 78 行,将结果 4 赋值给变量 number 后,继续执行程序第 79行。

此时,相当于执行:

divisor = quotient(1000, 10);

再次调用 quotient 方法,并将结果 1000/10=100 赋值给变量 divisor。此时 divisor 的值等

于 100,仍然不小于 1,所以程序继续进行第 2 次循环,同样重复上面的步骤,这次接收的参

数是 4 和 100, 因为 4/100=0, 所以 digit 的值为 0,之后,因为 chineseNumber[0]的值为“零” ,

所以 output 的值就变成了“贰零” ,同理,变量 nubmer=4 % 100,所以 number 的值仍然为 4,

而 divisor=100/10,所以 divisor 的值就改变为 10。

现在进入第 3 次循环,同样重复上面的步骤,这次接收的参数是 4 和 10,因为 4/10=0,

所以 digit 的值还是为 0,之后,因为 chineseNumber[0]的值为“零” ,所以 output 的值就变成

了“贰零零” ,同理,变量 nubmer=4%10,所以 number 的值仍然为 4,而 divisor=10/10,所

以现在 divisor 的值就改变为 1 了。

现在进入第 4 次循环,同样重复上面的步骤,这次接收的参数是 4 和 1,因为 4/1=4,所

以 digit 的值改变为 4,之后,因为 chineseNumber[4]的值为“肆” ,所以 output 的值就变成了

“贰零零肆” ,同理,变量 nubmer=4%1,所以 number 的值仍然为 4,而 divisor=1/10,所以

现在 divisor 的值就改变为 0 了,因为 0 小于 1,所以程序结束循环。

最后将字符串变量 output 内容作为 getCapitalNumber 方法的返回值。至此,getCapital Number 方法结束。

Page 110: Java Web动态图表编程

综上所述,getCapitalNumber 方法实现的功能实际上是将给定的一个任意长的整数(当

然该整数被限定在 Java 对整数的处理范围之内)按一一对应的方式把数字转换成中文,并返

回转换后的结果。

现在介绍程序第 48 行~第 58 行的 getMonthNumber 方法。该方法代码如下:

public String getMonthNumber(int month) String output = ""; if (month > 10) output += "拾"; month ­= 10;

output += getCapitalNumber(month); return output;

顾名思义,该方法返回日期“月”的中文大写转换结果。getMonthNumber 方法接收一个

整型参数 month,程序第 50 行,声明了一个字符串对象:output,并初始化其内容为空。如

果传递的参数在 1~10 之间,则程序跳过第 51 行~第 55 行之间的 if 语句块,执行第 56 行

的语句:

output += getCapitalNumber(month);

将变量 month作为参数再次传递并调用 getCpitalNumber 方法,把 getCpitalNumber 方法

返回的字符串在串接在变量 output 后,最后返回 output。

如果传递的参数大于 10,这里以传入的 month参数: “12”为例,则程序首先进入第 51 行~第 55 行之间的 if语句块,执行完第 53 行的语句后,output 值更新为“拾” 。第 54 行,

将 month参数减去 10 后,再将相减后的结果重新赋值给 month。所以这时,month的值更改

为 2(12­10) 。然后再执行程序第 56行的语句,这时实际上执行的是:

output += getCapitalNumber(2);

getCpitalNumber(2)方法返回的结果是“贰” ,现在字符串变量 output 的值就等于“拾贰”

了,最后再返回 output 的内容。

综上所述,对于“月”的转换,getMonthNumber 方法对“10” 、 “11” 、 “12”这三个月份

返回的结果是: “拾” 、 “拾壹”和“拾贰” ;对于 1~9月份,则可以直接采用按简单的一一对

应的方法返回相应的结果。注意,这里我们并没有对大于 12 以上的数进行额外的处理,因为

在程序CheckMaker.java中, 我们调用 getMonthNumber方法时所传递的参数范围就限定在 1~ 12 之间了。

现在介绍程序第 17 行~第 46 行的 getDayNumber 方法。该方法代码如下:

public String getDayNumber(int day) String output = ""; if (day < 20) output += getMonthNumber(day);

else if (day >= 20 && day < 30) output += "贰拾"; day ­= 20; if (day == 0) return "贰拾";

Page 111: Java Web动态图表编程

output += getCapitalNumber(day); else output += "叁拾"; day ­= 30; if (day == 0) return "叁拾";

output += getCapitalNumber(day);

return output;

getDayNumber 方法首先对传入的参数 day进行判断:

(1)day 小于 20,则执行程序第 22 行,直接将 day 传递给 getMonthNumber 方法,会

重复执行刚刚我们所讲述的 getMonthNumber 方法的执行过程,然后执行程序第 45 行,并返

回所得到的结果。

(2)day如果大于或等于 20 且小于 30,则执行程序第 24 行~第 33 行之间的语句块。

这里以 day 等于“27”为例,首先 output 的内容就改变为“贰拾” ,然后 day 减去 20,如果

差为“0” ,说明 day 正好等于 20,则立刻返回字符串“贰拾” ;否则将新的 day 的值传递给 getCpitalNumber 方法,即执行 getCpitalNumber(7)方法,返回的结果“柒”附加在字符串

对象 output 的内容后,这时字符串 output 的内容就改变为“贰拾柒”了。最后执行程序第 45 行,返回 output 的内容。

(3)在其他情况下,则执行程序第 35 行~第 43 行之间的语句块。与前面介绍的第 2 点运行结构过程完全相同,此处不再赘述。同理,注意这里我们并没有对大于 31 以上的数进

行额外的处理,因为在程序 CheckMaker.java 中,调用 getDayNumber 方法时所传递的参数范

围就限定在 1~31 之间了。

现在讨论 CheckMaker.java 的运行过程。

程序第 6 行~第 7 行:

import java.util.*; //引入 java.util包中所有的类

import java.text.*; //引入 java.text包中的所有的类

分别引进 java.util 包和 java.text 包。java.text 包我们已经介绍过了。java.util 包是一些工

具类的集合,里面包含了一些常用的工具类,如用于日期及时间处理的 Date类等。

程序第 11 行:

Image checkPNG;

声明了一个 Image 类的对象 checkPNG,用于加载和显示背景图像,如图 3.10 所示的空

白支票。

程序第 12 行~第 13行:

Image offImage; Graphics offGraphics;

声明用于绘制缓冲区图形对象的 Image 对象 offImage 及其在缓冲区的绘图环境 offGraphics。

程序第 15 行:

int appltWidth = 740, appletHeight = 270;

定义本 Java Applet 的宽度和高度。这里的宽度和高度与背景图像,即空白支票的宽度和

Page 112: Java Web动态图表编程

高度相同。

程序第 16 行~第 22行,创建了一些字符串对象用于空白支票某些栏目的绘制。上述字

符串对象分别表示: “科目” 、 “对方科目” 、 “收款人” 、 “付出行名称” 、 “用途” 、 “会计”以及

“单位主管”等内容。

程序第 24 行:

Date issuedDate = new Date();

创建了一个 Date类的实例 issuedDate,表示支票的出票日期。Date类隶属于 java.util 包,

主要用于 Java 对日期和时间的处理。 用 new Date()方法创建的 Date对象表示的是当前的系统

时间。默认的 Date对象的输出格式如下所示(以 2004 年 12 月 13日为例) :

Mon Dec 13 20:12:50 CST 2004­12­13

程序第 25 行~第 27行,声明了 3 个整型变量:year、month,以及 day,分别表示支票

出票日期的年、月、日。

程序第 28 行:

double amount = 6437192.08d;

创建了 1 个值为 6437192.08d双精度型的变量:amount,表示转账金额。最后一位数字

后面的 8 表示该浮点数为双精度型(这里的 d可以省略不写,我们的编程习惯是写上“d”表

示 double型, “f”表示 float 型,以示区别)。

程序第 30 行:

String accountNumber = "渝工行 2004010178930";

创建字符串对象 accountNumber,表示出票人账号。

程序第 31 行:

String chineseAmount = "陆佰肆拾叁万柒仟壹佰玖拾贰圆零角捌分";

创建了一个字符串变量 chineseAmount,并用“陆佰肆拾叁万柒仟壹佰玖拾贰圆零角捌

分”对其进行初始化。chineseAmount 实际上是程序第 28 行,转账金额 6 437 192.08 的人民

币大写的货币表现格式。 因为本程序并没有涉及到如何将类似于程序第 28行的数字值自动转

换成相应的人民币大写的货币表现格式的方法,所以这里我们直接提供了一个与 6437192.08 相对应的人民币大写的货币表现格式。对此有兴趣的读者,可以借鉴我们提供的年、月、日

的转换方法来完成这一功能。

程序第 33 行:

NumberFormat moneyFormat = NumberFormat.getCurrencyInstance(Locale. PRC);

创建了 1 个 NumberFormat 的实例对象 accountNumber,并调用 NumberFormat 的静态方

法 getCurrencyInstance 对其进行初始化。该方法返回一个能将数字值格式化为指定的货币值

形式(如表示美元的数字值前,通常加一个“$”符号;表示人民币的数字值前,通常加一个

“¥”符号)的 NumberFormat 对象。方法中的参数 Locale.PRC 表示货币值应该按中国的人

民币格式显示,即以“¥”符号开头,并用小数点将元和角、分隔开,并在千位、百万、十

亿(以千位单位)的数位上用逗号隔开。Locale 类提供了一些静态的常量,用于表示特定国

家的货币值,以便能正确地显示某个国家的货币格式。其他的一些表示国家和地区的静态常

量还有:

Ø Locale. CHINA:中国

Ø Locale. US:美国

Page 113: Java Web动态图表编程

Ø Locale. CANADA:加拿大

Ø Locale. FRANCE:法国

Ø Locale. GERMAN:德国

Ø Locale. UK:英国

Ø Locale. KOREA:韩国

Ø Locale. JAPAN:日本

Ø Locale. ITALY:意大利

Ø Locale. TAIWAN:中国台湾 NumberFormat 隶属于 java.text 包, 而 Locale类隶属于 java.util 包。 两者分别在程序第 6、

7 行引进。

程序第 35 行:

DateToChinese dtc = new DateToChinese();

用 DateToChinese类的默认构造器创建了 1 个 DateToChinese类的实例——dtc。注意,我

们并没有引入 DateToChinese 类。如果一个类与使用它的类位于同一个包(目录)中,则不

需要 import。如果程序员没有为类指定一个包,则该类将置于一个没有名称的默认包中,它包含

当前目录中所有已编译、但未显示放置在某个包中的类。这就是我们为什么必须在 Java程序中,

为了使用 Java API中的类,就一定要用 import语句将其引入后才可以使用。

提示:如果程序通过在类名前面添加完全包名来限制类的名称,则可以不需要 import 语

句。例如,程序第 24行,可改写成以下形式:

java.util.Date issuedDate = new java.util.Date();

至此,本例的变量声明完毕。接下来程序运行第 38行~第 46 行的 init 方法:

public void init() checkPNG = getImage(getDocumentBase(), "check.jpg"); offImage = createImage(appletWidth, appletHeight); offGraphics = offImage.getGraphics(); year = issuedDate.getYear() + 1900; month = issuedDate.getMonth() + 1; day = issuedDate.getDate();

关于 Init 方法现在读者都已经很熟悉了。首先还是加载背景图像——空白的支票,然后

创建缓冲图像对象及其绘图环境。之后用 Date类的相关方法获得程序第 24行,用 new Date ()方法创建的 Date对象:issuedDate所表示的当前系统时间中的年、月、日等信息。

Date 类的 getYear 方法,返回 Date 对象的年值,该年值是以 1900 为基准点的,也就是

说,公元 2000 年则 getYear 返回 100(2000–1900)。所以,程序第 43 行,在 getYear 方法后

加上了 1900,就得到了 Date对象 issuedDate的实际年值。 Date类的 getMonth方法返回 Date对象的月值,该月值是以 0~11 分别代表 1~12 月。

所以,程序第 44 行,在 getMonth方法后加上了 1,就得到了 Date对象 issuedDate的实际月

值。

程序第 45 行,issuedDate.getDate返回了 issuedDate的日值。

之后,我们又会看见熟悉的运行流程,程序先执行 paint 方法,再执行 update 方法,最

后再将缓冲区内的内容显示在 Applet 上。

在 update方法中,我们首先显示被加载的背景图像,空白的支票。然后,程序在第 142 行:

drawLeft();

Page 114: Java Web动态图表编程

调用 drawLeft 方法,绘制支票左边部分的内容。程序转到第 56 行~第 85行的 drawLeft 方法中继续运行:

public void drawLeft() offGraphics.setFont(new Font("宋体", Font.PLAIN, 12));

// 绘制科目 offGraphics.drawString(subjectName, 85, 80);

// 绘制对方科目 offGraphics.drawString(partnerSubjectName, 85, 108);

// 绘制出票日期 offGraphics.drawString("" + year, 77, 133); offGraphics.drawString("" + month, 115, 133); offGraphics.drawString("" + day, 140, 133);

// 绘制收款人 offGraphics.drawString(receiver, 70, 165);

// 绘制付款金额 offGraphics.drawString(moneyFormat.format(amount), 70, 188);

// 绘制付款用途 offGraphics.drawString(payFor, 70, 208);

// 绘制会计人员 offGraphics.drawString(accountant, 60, 255);

// 绘制主管 offGraphics.drawString(supervisor, 125, 255);

这段代码仅仅重复使用了 drawString 的方法,将相关内容绘制到各自对应的栏目中。这

里惟一需要介绍的地方就是程序第 75 行,用 moneyFormat 类的 format 方法,并接收 1 个参

数:即程序第 28行所声明的双精度型变量 amount,就获得了 amount 的中国货币表现格式的

输出。如果所提供的参数是整数或者只有 1 位小数,则 moneyFormat 类的 format 方法会在相

应的数位上自动添加“0” 。 drawLeft 方法运行完毕后,程序的绘制结果如图 3.12所示。

我们可以看到“金额”一栏中 amount 的中国货币表现格式的输出效果。

然后,程序返回 update方法,调用 drawRight 方法,绘制支票右边部分的相关内容。

这时,程序进入第 87 行~第 134 行的 drawRight 方法。实际上,drawRight 方法也是由

一系列的 drawString 方法组成。drawRight 方法运行完成后的绘制结果如图 3.11 右边部分所

示。

在该方法中,要注意图 3.13 圆圈部分是如何绘制的。

Page 115: Java Web动态图表编程

图 3.12 drawLeft方法的运行结果 图 3.13 drawRight方法的运行结果

drawRight 方法中的第 117 行~第 133 行的代码就进行了圆圈部分的绘制工作:

offGraphics.setFont(new Font("宋体", Font.PLAIN, 14));

String amountOutput = moneyFormat.format(amount);

int j = 0; for (int i = amountOutput.length() ­ 1; i >= 0; i­­) String s = "" + amountOutput.charAt(i);

// 跳过小数点 if (s.equals(".")||s.equals(","))

continue;

offGraphics.drawString(s, 710­j * 15, 127); j++;

我们首先重新设定当前的绘图字体。

程序第 119 行,创建一个 String 对象 amountOutput,并调用 NumberFormat 类的 format 方法对其进行初始化,amountOutput 的内容如下:

¥6,437,192.08

然后需要将字符串“¥6,437,192.08”中的每一个数字,以及中国货币符号“¥” ,绘制

到圆圈区域内所示的相应位置上。注意,amountOutput 中的逗号“, ”及小数点“.”是不需

要绘制的。

我们的绘制思路是从字符串 amountOutput 的最后 1 个(即倒数第 1 个)字符开始读取,

将其绘制到图 3.13 圆圈区域中“分”的位置;然后读取倒数第 2个字符,将其绘制在“角”

的位置上;如果读取字符串中当前字符的内容为逗号或小数点,则程序忽略该字符。依此类

推,直到绘制完中国货币符号“¥”为止。

drawRight 方法中的第 121 行~第 133 行,就利用上述思路实现了绘制字符串 amountOutput 的目的。

因为 String类的 charAt 方法返回的是一个字符,所以在程序第 124 行,用了字符串的串

接方法,创建了一个 String对象 s,而 s 的内容就是 charAt 方法返回的一个字符。

然后,程序在第 127 行,判断 s 的内容是否是逗号或小数点,如果是,则程序不对 s 做

任何处理,调用 continue 关键字,跳过剩余的部分而直接进入下一次循环。如果 s 的内容不

是逗号或小数点,则运行程序第 131 行:

Page 116: Java Web动态图表编程

offGraphics.drawString(s, 710­j * 15, 127);

这里的 710正好是圆圈区域中“分”的横坐标,每循环一次,j 的值会递增 1,也就是说,

下一个对象的绘制位置向左移动 15 像素。

当循环结束的时候,所有的数字以及中国货币符号“¥”就被绘制到相应的位置了。

当 drawRight 方法结束,程序返回到 update 方法中,缓冲区的图像绘制工作也就随之结

束,最后程序在第 147 行,将缓冲区的图像全部显示在 Applet 上,程序就此结束。最终的绘

制结果如图 3.11 所示。

关于如何在 Java Applet 中加载并显示图像,并利用该功能来生成支票或者其他单据类的

图表,我们就介绍到这里,利用这种思路可以实现其他复杂的票据及单据的绘制工作。

3.3 从 HTML 文档获取参数生成动态图表

前面我们在介绍 Java Applet 中生成动态图表的时候, 所需要的数据都是在该 Applet 源程

序中直接指定或者是在该程序中由实时生成的随机数而得来。此前,我们所用到的 HTML文

档仅仅是加载 Java Applet 的.class 文件,以及设定该 Applet 在浏览器中所占的宽度和高度这

两个最基本的参数。其实除了在 Java 源程序自身可以生成所有需要的数据外,Applet 还可以

从 HTML文档中读取数据。

当我们希望改变 Web 图表的一些相关信息的时候,以第 3.12 节的 Pie.java 为例,假如我

们更改 Web 图表的标题: “Java Web 图表设计 Applet 版——饼图”为“Java Web 图表设计 Applet & HTML版” ,就必须修改 Pie.java 源程序第 61 行,用新的内容替换掉旧的内容,然

后再次编译,重新启动浏览器,才可以看到修改后的结果。如果要求在不修改 Java源程序的

基础上,也同样达到修改 Java Applet 中某些内容的目的,则最简单而有效的办法是从 HTML 文档中读入所需的数据。

下面我们来学习如何通过 HTML文档向 Java Applet 传递数据。

3.3.1 传递参数的 HTML 文档

利用 Applet 来接收从 HTML 中传递过来的参数,需要在 HTML 文档中的<APPLET>和 </APPLET>标记中,再插入用于向 Java Applet 传递参数的标记。下面就 HTML 文档中用于 Java Applet 的标记做一个简单说明,如表 3.1 所示。

表 3.1 用于 HTML文档的 Applet的标记

标记名称 说 明

CODE 指定 Applet 的类名

WIDTH 指定 Applet 窗口宽度的像素尺寸

HEIGHT 指定 Applet 窗口高度的像素尺寸

CODEBASE

指定 Applet 的 URL 地址。它可以是绝对地址 ,如 java.sun.com;也可以是相对于当前 HTML 所在

目录的相对地址,如/AppletPath/Name。如果 HTML 文件不指定 CODEBASE 标志,浏览器将使用和

HTML 文件相同的 URL

ALT 虽然 Java 在 WWW 上很受欢迎,但并非所有浏览器都支持 Java。如果某种浏览器无法运行 Java

Applet,那么它在遇到 APPLET 语句时将显示 ALT 标志指定的文本信息

ALIGN 用来控制把 Applet 窗口显示在 HTML 文档窗口位置。与 HTML 语法中的<IMG>语句相同,ALIGN

Page 117: Java Web动态图表编程

标志指定的值可以是 TOP、MIDDLE 或 BOTTOM

NAME 把指定的名字赋予 Applet 的当前实例。当浏览器同时运行两个或多个 Applet 时,各 Applet 可通过

名字相互引用或交换信息。如果忽略 NAME 标志,Applet 的名字将对应于其类名

PARAM 指定由 Applet 读取的参数值,每个标记指定一个参数

我们重新编写了一个 PieNew.java(\chap03\Fig3.3)的源程序,这次绘制饼图所需要的数

据全部从 PieNew.html 文档中获得。

我们从 PieNew.html(\chap03\Fig3.3)文档中可以观察到,同以前的 HTML 文档相比,

增加了一些如下所示的 PARAM 标记:

<PARAM NAME = "bookTitle1" VALUE = "Python">

PARAM 指定了由 Java Applet 读取的参数。每个 PARAM 标记(即参数)又包含两个属

性:NAME 和 VALUE。NAME 表示参数的名称,VALUE 表示参数的值。

一般来说,HTML向 Java Applet 传递的参数,可以在 Java Applet 中通过编程的方式获

得这些参数值。通常会在 Java Applet 的 init 方法中,调用 getParameter 方法来获得某个参数

的值。如果 getParameter 方法中指定某个参数可用,则以字符串的形式返回某个参数的值。

下面阐述如何在 Java Applet 中,获取这些参数的详细用法。

3.3.2 获取参数并生成图表

源程序 PieNew.java 和本章第 3.1.2 节介绍的 Pie.java 的结构完全相同。不同之处在于, PieNew.java 中生成饼图的数据,没有采用 Pie.java 中的由随机数的方式产生,所有参与生成

饼图的数据全部都是从 PieNew.html 文档中读取相关参数而获得。

我们以 init 方法中,获取书籍的名称及其销售量的参数为例。

程序第 52 行~第 65行:

for (int i = 0; i < 5; i++)

bookTitle[i] = getParameter("bookTitle" + (i + 1)); tempBookSales[i] = getParameter("bookSales" + (i + 1)); if (tempBookSales[i] != null)

bookSales[i] = Integer.parseInt(tempBookSales[i]); else

bookSales[i] = 0; totalSales += bookSales[i];

一般来说,HTML向 Java Applet 传递的参数,可以在 Java Applet 中,通过编程的方式

获得这些参数值。通常会在 Java Applet 的 init 方法中,调用 getParameter 方法(程序第 54、 55 行)来获得某个参数值。如果 getParameter 方法中指定的某个参数可用,则以字符串的形

式返回某个参数值。本段程序块总共循环 5 次,调用 getParameter 方法,试图获得参数

“bookTitle1” 、 “bookSales1” 、 “bookTitle2” 、 “bookSales2” 、 “bookTitle3” 、 “bookSales3” 、

“bookTitle4” 、 “bookSales4” 、 “bookTitle5”和“bookSales5”等参数值。如果在相应的 HTML 文档中存在同名的参数,则返回该参数值。我们可以看到 NewPie.html 文档中,的确提供了

这些同名的参数(这些参数在 html 中是不区分大小写的)。因此,会分别获得如“Python” 、

“45” 、 “Java” 、 “115” , 依此类推。 记住返回的这些参数值都是字符串类型, 因此参数值 “45” 、

“115”等,虽然它们表示的值都是书籍的销售量,但实际上都是字符串对象,并不能直接用

Page 118: Java Web动态图表编程

于数学运算。因为我们需要计算所有书籍的销售总量,所以必须将这些字符串对象转换成可

用于数学计算的数字型的数据类型,如字节型、短整型、整数型、长整型、浮点型和双精度

型等。

在 java.lang包中,对于每一个基本的数字型数据类型,均设置了一个相应的类,例如: Integer 类对应于 int,Double 类对应于 double 等。这些类中均提供了一个转换方法,用于实

现字符串向相应数字类型的转换。表 3.2 列出了详细的转换方法。

表 3.2 字符串向数字的转换

目标类型 类 方 法 示 例

byte Byte parseByte byte b = Byte.parseByte(“100”);

short Short parseShort short s = Short.parseShort(“100”);

int Integer parseInt int i = Integer.parseInteger(“100”);

续表

目标类型 类 方 法 示 例

long Long ParseLong long l = Long.parseLong(“100”);

float Float parseFloat float f = Float.parseFloat(“100.173”);

double Double parseDouble double d = Double.parseDouble(“100.173”);

需要注意的是,在使用上述方法将字符串向数字转换的过程中,如果被转换的字符串与

目标类型不匹配,则在转换过程中抛出异常。所以,如果无法肯定被转换的字符串和目标数

据类型是否匹配,我们就必须将转换方法放置在 try…catch 语句块中,捕获这种异常,并做

相应的处理。再调用 getParameter 方法,如果试图获得的参数值在相应的 HTML文档中并不

存在,则该方法返回一个 null 对象。所以在上面的循环中,程序第 54 行~第 55 行,先分别

用了两个字符串数组:bookTitle、tempBookSales 来存储书籍变量及书籍销售量,然后程序第 56 行,首先判断当前的 tempBookSales 字符串数组中的元素是否为 null,如果不为 null,则执

行 parseInteger方法,进行字符串对象到整型对象的转换工作。

整个程序的流程和第 3.1.2 节介绍的 Pie.java 完全相同。读者可以参考该节的相关内容,

此处不赘述。 NewPie.java 的运行结果如图 3.14 所示。如果改变 NewPie.java 中的 PARAM 相关参数的

值,则只需要刷新浏览器,就可以看到新的结果了。

图 3.14 NewPie.java的运行结果

Page 119: Java Web动态图表编程

3.4 本章小结

本章通过 5个不同的实例,讲解了 Java Applet 动态图表的编写方法。

用 Java Applet 来生成动态图表,有如下优点:

Ø 简单而高效的开发速度。Java Applet 是一种简单、快速的开发 Web 动态图表的有效

方法。

Ø 结合图像的加载及显示功能,Java Applet 使得 Web 动态图表的应用处理能力变得非

常强大,可以完成非常复杂的业务单据。

Ø Java 设计上的平台无关性,使得 Java Applet 可以在装有 Java 虚拟机的计算机上很好

地工作。这就保证了代码在 IE 和 Netscape,以及其他多种浏览器之间保持良好的兼

容性。

Ø Java 良好的安全性,使得 Java Applet 比 ActiveX 或任何其他的插件都更具安全性。例

如,ActiveX 可以重写文件系统,但在 Java 中是被绝对禁止的。 Java Applet 也有其无法避免的缺点:

Ø Java Applet 是为客户端执行的,也就是说,必须安装 Java 虚拟机(JVM)。如果没有

安装 Java 虚拟机,则程序无法运行。

Ø Java 是被设计为与平台无关的,因此,当编译 Java 时,并不像别的语言那样被编译

成针对某种特定平台的机器码,而是被编译成与平台无关的通用二进制代码。当客户

端下载一个含有 Java Applet 的页面时, Applet 中的这种二进制代码就通过网络传送到

客户端的浏览器,在客户端的浏览器内部装有一个 Java 虚拟机,为了使客户端的机

器能读懂这个 Applet 的内容,浏览器首先要启动这个 Java 虚拟机,然后将 Applet 中

的二进制代码编译成机器语言。这种一次编译,到处运行的特性节省下来的时间就花

费在每个访问者的身上了。

Ø 含有 Applet 的页面在每次请求 class 文件时都需要重新下载,如果网络的通信状况不

好,将会导致程序运行变得异常缓慢。

解决的方法是,Web 动态图表的生成不再由客户端的浏览器生成,当客户端的浏览器在

访问含有 Web 动态图表页面的时候,直接由 Web 服务器生成实时的动态图表(图像),然后

将结果返回给客户端。这就是所谓的“服务器端编程技术” 。

基于 Java 的服务器端编程技术,目前主要有三种方法:

Ø Java Servlet 技术。

Ø Java Server Pages,即 JSP 技术。

Ø JSP + JavaBean技术。

本书的重点就是阐述如何通过服务器端编程技术来实现 Web 动态图表,其中重点介绍 JSP 技术和 JSP + JavaBean技术是如何应用于 Web动态图表编程的。具备了 Java Applet 的 Web 图表编程基础,就可以进行这方面的学习了。首先,我们必须掌握如何搭建 JSP/Servlet 的运行环境。

Page 120: Java Web动态图表编程

第 4 章 JSP/Servlet 运行环境的搭建

Java Applet 作为 Java 在 Web 上主要的交互式应用模式一直持续了很长的时间。我们概

述了 Java Applet在应用方面的种种局限性。 对这些局限性及不足之处, 解决方案一直到Servlet 的出现才比较完美。其后,Sun公司在 1999 年下半年正式推出 Servlet 的升级产品 JSP(Java Server Pages),以其简单而高效的开发方式、跨平台的能力及卓越的执行性能,使得基于 JSP 的开发及应用在美国得到各大公司的热烈响应和支持。由于 JSP/Servlet 技术也是 J2EE 技术

最重要的组成部分之一,在企业级也得到了广泛的应用。短短几年时间,JSP 已经成为美国

最主要的 Web 开发技术。选择 JSP/Servlet 技术作为基于服务器端的 Web 动态图表的开发是

非常明智的。

在运行及调试 JSP/Servlet 之前,必须安装 JSP/Servlet 的运行环境。JSP/Servlet 的运行

环境也就是我们平常所说的支持 JSP/Servlet 的 Web 服务器。

目前,已经有很多支持 JSP/Servlet 的 Web 服务器,这里我们介绍两种免费、高性能的适

合中小企业的 JSP/Servlet 的 Web 服务器——Tomcat 和 Resin, 然后再介绍一种企业级的 J2EE 服务器:Weblogic,学习如何在这些服务器中部署及发布我们编写的基于 JSP/Servlet 的 Web 图表应用程序。

在安装 JSP/Servlet 的 Web 服务器之前,要先安装 JDK 的开发及运行环境。JDK 的安装

请读者参考本书第 1章。

4.1 Tomcat的安装和配置

Tomcat 服务器是当今使用最为广泛的 JSP/Servlet 的 Web 服务器,它是 Apache 基金会 Jakarta 项目中的一个核心项目,由 Apache,Sun和其他一些公司及个人共同开发而成。由于

有了 Sun的参与和支持,最新的 Servlet 和 JSP 规范总能在 Tomcat 中得到体现。 Tomcat 具有运行稳定,性能优异等特点,而且 Tomcat 是完全免费的开放源代码产品。

是中小企业首选的 JSP/Servlet 的 Web 服务器之一。笔者写作的时候,使用的是 Tomcat 最

新版本 5.5.7,读者可以在 Apache 网站上下载, URL 如下: http://jakarta.apache. org/site/binindex.cgi

Apache提供了两种用于Windows 的 Tomcat 安装程序,一种是 akarta­tomcat­5.5.7.exe的

可执行文件,另一种是 jakarta­tomcat­5.5.7.zip 的格式,我们建议读者下载 zip 版的服务器程

序。

4.1.1 Tomcat 的安装

我们分别介绍 jakarta­tomcat­5.5.7.exe 及 jakarta­tomcat­5.5.7.zip 版服务器程序的安装过

程。

Page 121: Java Web动态图表编程

1.jakarta­tomcat­5.5.7.exe 的安装

(1) 双击下载的 jakarta­tomcat­5.5.7.exe可执行文件, 就会出现如图 4.1 所示的 Apache Tomcat 安装向导,单击【Next】按钮。

(2)在出现如图 4.2所示的 Tomcat 授权协议界面后,单击右边的【I Agree】按钮。

图 4.1 Tomcat的安装向导 图 4.2 授权协议

(3)在出现如图 4.3 所示的 Tomcat 安装组件选择对话框时,我们改变 Tomcat 的默认安

装类型“Normal”为“Full” ,并单击【Next】按钮。

(4)在出现如图 4.4 所示的 Tomcat 的安装路径对话框时,我们同样也改变 Tomcat 的默

认安装路径“C:\Program Files\Apache Software Foundation\Tomcat 5.5”为“C:\Tomcat 5.5” ,

并单击【Next】按钮。

图 4.3 选择安装组件 图 4.4 安装路径

(5)配置 Tomcat,创建一个管理员密码。请记住管理员密码,以后管理 Tomcat 的时候,

需要使用该密码,单击【Next】按钮,如图 4.5 所示。

(6)选择 Java 虚拟机的路径,如果已经安装了 J2SE 5.0,Tomcat 会自动找到其安装路

径,单击【Next】按钮,继续剩余部分的安装,如图 4.6 所示。

Page 122: Java Web动态图表编程

图 4.5 创建管理员密码 图 4.6 选择 JRE

(7)系统会复制相关文件到 Tomcat 的安装目录,最后系统会提示是否运行 Tomcat 服

务器,以及说明文件,单击【Finish】按钮,启动 Tomcat 服务器,如图 4.7 所示。

图 4.7 结束安装

(8)然后我们可以在右下方系统工具栏中,看到 这个图标。右键单击 图标,会

弹出一个菜单。【Configure…】选项是用于设置 Tomcat 的相关运行参数的。包括启动与关闭 Tomcat 服务器。Tomcat 启动类型:如自动、手动、禁止等,如图 4.9所示。这里我们保持这

些参数不变。【Start Service】是启动 Tomcat 服务器,【Stop Service】是关闭 Tomcat 服务器。

如果【Start Service】显示为灰色,表示 Tomcat 服务器已经启动。

下面测试 Tomcat 服务器是否已经正常运行。启动 Tomcat 有几种方法:其一,可以直接

在 Tomcat 安装目录下(本例:C:\tomcat5.5)的 bin 子目录中运行 tomcat5.exe 程序,然后会

弹出一个DOS窗口, Tomcat以命令行方式并加载相关参数启动服务程序, 如果看到 “Server startup in xxxx ms”的信息,则表示 Tomcat服务器已经正常启动了,如图 4.8所示。

图 4.8 Tomcat的启动方式 1

其二,在 bin目录下,我们也可以执行 tomcat5w.exe 文件,单击运行界面下方的【Start】

按钮,就可以启动 Tomcat;单击【Stop】按钮,则可以停止 Tomcat服务器,如图 4.9所示。

现在启动浏览器,在地址栏内输入:http://localhost:8080 或者 http://127.0.0.1:8080,如果

Page 123: Java Web动态图表编程

看到如图 4.10 所示的结果,表示 Tomcat 安装和配置是正确的。

图 4.9 Tomcat的启动方式 2 图 4.10 默认的 Tomcat的启动画面

2.jakarta­tomcat­5.5.7.zip 的安装

首先将 jakarta­tomcat­5.5.7.zip版服务器程序进行解压,如解压到 C:\jakarta­tomcat­ 5.5.7 目录下。然后直接运行该目录下 bin 子目录中 startup.bat 批处理文件或者前文介绍的两种启

动方法就可以运行 Tomcat 服务器了。

同样, 启动浏览器在地址栏内输入 http://localhost:8080 或者 http://127.0.0.1:8080, 如果同

样看到如图 4.10 所示的运行结果,则表示 zip版 Tomcat 安装和配置也是正确的。

提示:如果不能启动 Tomcat 服务器,或者无法在浏览器中获得相应的结果,几乎都是

因为 J2SE5.0 的安装出现了问题,请参考本书第 1 章的相关内容。

Page 124: Java Web动态图表编程

4.1.2 测试第一个 JSP 程序

现在编写一个很简单的 JSP 文件——test.jsp(\chap04\Fig4.1\Fig.4.1_01),将 test.jsp文件

拷贝到 Tomcat安装目录 (本例:C:\tomcat5.5)\webapps\ROOT子目录下,启动 Tomcat。在浏

览器中地址栏输入 http://localhost:8080/test.jsp 或者 http://127.0.0.1:8080/test.jsp,如果看到如

图 4.11 所示的运行结果,则表示读者第一个 JSP 页面编写及部署成功了。

图 4.11 test.jsp的运行结果

4.1.3 配置 Tomcat

虽然 Tomcat 现在可以正确运行了,但是读者最好还是参考本书第 1.2.2 节介绍的内容,

新建一个系统变量——CATALINA_HOME,其值为 Tomcat 安装目录,以本书为例,其值为: C:\Tomcat5.5。然后更新系统变量——CLASS_PATH,将原值:

.;%JAVA_HOME%\lib

更新为,

.;%JAVA_HOME%\lib;%CATALINA_HOME%\lib

同样,请读者不要忘记最前面那个“.” 。 Web 应用程序的安装和部署一直是个令初学者感到非常头痛的难题,但却是必须要掌握的

一个关键内容。很多读者会遇到这样的问题,在本机运行很好的 Web 应用程序,如何将其打包

并在 Web 服务器上进行安装和部署?部署成功,为什么没有得到正确的结果?要想解决这些问

题,必须对 JSP/Servlet 的Web 服务器的运行机制有一定的了解,这需要专门的著作来对其详细

的讲解。因为本书内容涉及如何将程序发布到不同的 JSP/Servlet 的 Web 服务器上,所以在这里

我们以Tomcat为例,简要地探讨其启动过程,以及如何对其进行配置。

让我们来研究一下 Tomcat 目录结构中最重要的几个子目录,如表 4.1 所示。

表 4.1 Tomcat目录结构

目 录 名 简 介

bin 存放启动和关闭 tomcat 脚本

续表

目 录 名 简 介

conf 包含不同的配置文件,server.xml(Tomcat 的主要配置文件)和 web.xml

work 存放 jsp编译后产生的 class 文件

webapps 存放应用程序示例,将要部署的应用程序会放到此目录

logs 存放日志文件

其中最重要的是位于 conf子目录下的两个配置文件——server.xml 和 web.xml。 server.xml 是一个 xml 格式的配置文件,它决定了 Tomcat 服务器运行的各项参数。xml

文档由一系列的标记及元素组成。

Page 125: Java Web动态图表编程

server.xml 的组织结构如下:

(1)顶级元素——<Server>是整个配置文件的根元素。Server 元素代表整个 Catalina servlet 容器。因此,它必须是 conf/server.xml 配置文件中的最外层元素。其属性代表了整个 servlet 容器的特性, 而<Service>代表与一个引擎 (Engine) 相关联的一组连接器 (Connectors) ;

也就是说,Server 包含了一个或者多个连接器(Connectors),以及一个引擎(Engine),它负

责处理所有 Connector 所获得的客户请求。

(2)连接器(Connectors)——代表外部客户之间的接口。外部客户向特定的 Service 发送请求,并接收响应。一个 Connector 将在某个指定端口上侦听客户请求,并将获得的请

求交给 Engine来处理,从 Engine处获得回应并返回客户。Tomcat 有两个典型的 Connector,

一个直接监听来自 browser 的 http请求,一个监听来自其他 Web Server 的请求:

Ø Coyote Http/1.1 Connector 在端口 8080 处侦听来自客户 browser 的 http请求。

Ø Coyote JK2 Connector 在端口 8009 处侦听来自其他 WebServer(Apache)的 servlet/jsp 代理请求。

(3)容器——代表一些组件,包括:引擎(Engine)、Host 和 Context。这些组件的功能

是处理进来的请求,生成对应的响应。

Ø 引擎 (Engine) 处理一个 Service的所有请求。 Engine下可以配置多个虚拟主机 (Virtual Host),每个虚拟主机都有一个域名。当 Engine获得一个请求时,它把该请求匹配到

某个 Host 上,然后把该请求交给该 Host 来处理。Engine有一个默认虚拟主机,当请

求无法匹配到任何一个 Host 上的时候,将交给该默认 Host 来处理。

Ø Host 处理一个特定虚拟主机的所有请求。它代表一个虚拟主机, 每个虚拟主机和某个

网络域(Domain Name)相匹配。每个虚拟主机下都可以部署(deploy)一个或者多

个 Web 应用程序(Web App),每个应用程序对应于一个 Context 和一个 Context path。

当 Host 获得一个请求时,将把该请求匹配到某个 Context 上,然后把该请求交给该 Context 来处理。匹配的方法是“最长匹配” ,所以一个 path 值为""的 Context 将成为

该 Host 默认的 Context,所有无法和其他 Context 的路径名匹配的请求都将最终和该

默认 Context 匹配。

Ø Context 处理某个特定 Web 应用的所有请求。 一个 Context 对应于一个 Web 应用程序,

一个 Web 应用程序由一个或者多个 Servlet 组成。 Context 在创建的时候将根据配置文

件 CATALINA_HOME/conf/web.xml 和 WEBAPP_HOME/WEB­INF/ web.xml 载入 Servlet 类。 当 Context 获得请求时, 将在映射表 (mapping table) 中寻找相匹配的 Servlet 类,如果找到,则执行该类,并获得请求的回应,返回相应的结果。

对于 server.xml 文档中的每个元素,对应的文档按照如下方式组织:

(1)概述:对这个特定组件的整体描述。每个组件在 org.apache.catalina 包中存在一个

对应的 Java 接口,可能有一个或多个标准实现了这个接口。

(2)属性:该元素的合法属性。一般来说,这又分成公共属性和标准实现属性。公共

属性是所有实现了该 Java 接口的实现都具有的属性。标准实现属性是实现了该 Java 接口的

某个特定 Java 类具有的属性。

(3)嵌套组件:列举了可以合法地嵌在这个元素下的组件。有些元素可以嵌入任何容

器,而另一些元素只能嵌入在 Context 中。

(4)专有特性:描述了该接口的标准实现支持的专有特性的配置,与每个元素类型有

关。

接着简要介绍 Context 的部署配置文件 web.xml,前面我们说过,Context 处理某个特定 web 应用的所有请求。一个 Context 对应于一个 Web 应用程序,一个 Web 应用程序由一个或

者多个 Servlet 组成。当一个 Web App被初始化的时候,它将用自己的 ClassLoader 对象载入

Page 126: Java Web动态图表编程

部署配置文件“web.xml”中定义的每个 servlet 类。它首先载入在 CATALINA_HOME/ conf/web.xml中部署的 servlet类, 然后载入在自己的Web App根目录下的WEB­INF/web. xml 中部署的 servlet 类。

web.xml 文件由两部分组成:

Ø servlet 类定义

Ø servlet 映射定义

每个被载入的 servlet 类都有一个名字,且被注册在该 Context 的映射表(mapping table)

中,和某种 URL PATTERN 对应,当该 Context 获得请求时,将查询映射表,找到被请求的 servlet,并执行以获得请求回应。

了解了 server.xml 和 web.xml 的结构后,我们就可以来研究一下 Tomcat 的启动过程了。

因为 server.xml 决定了 Tomcat 服务器是如何启动的, 所以我们还是继续关注 server.xml 文档,

通过它来研究 Tomcat 的启动过程。这里我们介绍其中最重要的配置语句:

<Server port="8005" shutdown="SHUTDOWN">

启动 Server,在端口 8005处监听关闭命令,如果接收到“SHUTDOWN”字符串则关闭

服务器。

<Listener className="org.apache.catalina.mbeans.ServerLifecycleList ener" />

<Listener className="org.apache.catalina.mbeans.GlobalResourcesLife­ cycleListener" />

如果一个 Java 对象需要知道 Context 什么时候启动,什么时候停止,可以在这个对象中

嵌套一个 Listener 元素。该 Listener 元素必须已实现了 org.apache.catalina. Lifecycle Listener 接口,在发生对应的生命期事件的时候,通知该 Listener。注意,一个 listener 可以具有任意

多的附加属性。属性名与 JavaBean的属性名相对应,使用标准的属性命名方法。

<!­­ Global JNDI resources ­­> <GlobalNamingResources>

<!­­ Test entry for demonstration purposes ­­> <Environment name="simpleValue" type="java.lang.Integer" value="30"/>

<!­­ Editable user database that can also be used by UserDatabaseRealm to authenticate users ­­>

<Resource name="UserDatabase" auth="Container" type="org.apache.catalina.UserDatabase"

description="User database that can be updated and saved" factory="org.apache.catalina.users.MemoryUserDatabase Factory"

pathname="conf/tomcat­users.xml" />

</GlobalNamingResources>

GlobalNamingResources 定义了服务器的全局 JNDI资源。 JNDI(The Java Naming and Directory Interface,Java命名和目录接口)是一组在 Java 应

用中访问命名和目录服务的 API。命名服务将名称和对象联系起来,使得我们可以用名称访

问对象。目录服务是一种命名服务,在这种服务里,对象不但有名称,还有属性。

命名或目录服务可以集中存储共有信息,这一点在网络应用中很重要,使应用更协调、

更容易管理。例如,数据库服务设置在目录服务中,以便被有关的数据库应用程序所使用。 JNDI允许访问文件系统中的文件,定位远程 RMI注册的对象,访问像 LDAP 这样的目

录服务,定位网络上的 EJB组件等。JNDI架构提供了一组标准的独立于命名系统的 API,这

些 API构建在与命名系统有关的驱动之上。这一层有助于将应用与实际数据源分离,因此不

管应用访问的是 LDAP、RMI、DNS,还是其他的目录服务。换句话说,JNDI独立于目录服

Page 127: Java Web动态图表编程

务的具体实现,只要有目录的服务提供接口(或驱动)就可以使用目录。

我们可以在 GlobalNamingResources 中嵌套<Environment>元素,配置命名的值,这些值

作为环境条目资源(Environment Entry Resource) ,对整个 Web 应用可见。例如,可以按照如

下方法创建一个环境条目:

<GlobalNamingResources ...> ... <Environment name="maxExemptions" value="10"

type="java.lang.Integer" override="false"/> ...

</GlobalNamingResources>

这与在/WEB­INF/web.xml 中包含如下元素是等价的:

<env­entry> <env­entry­name>maxExemptions</param­name> <env­entry­value>10</env­entry­value> <env­entry­type>java.lang.Integer</env­entry­type>

</env­entry>

<Environment>元素的有效属性如表 4.2 所示。

表 4.2 <Environment>元素的有效属性

属 性 环境条目的文字描述(可选)

name 环境条目的名称,相对于 java:comp/env context

override 如果不希望/WEB­INF/web.xml 中具有相同名称<env­entry>覆盖指定的值,设为 false。默认值为 true

type 环境条目 Java 类名的全称。 在/WEB­INF/web.xml 中, <env­entry­type>必须是如下的值: java.lang. Boolean,

java.lang.Byte, java.lang.Character, java.lang.Double, java.lang.Float, java.lang.Integer, java.lang. Long, java.lang.

Short, or java.lang.String

value 通过 JNDI context 请求,返回给应用的参数值。该值必须转换成 type 属性定义的 Java 类型

资源参数用来配置资源管理器(resource manager 或对象工厂 object factory)。在 JNDI 查找时,资源管理器返回查找的对象。在资源可以被访问之前,对<Context>或<Default Context>元素的每个<Resource>元素,或者/WEB­INF/web.xml 中定义的每个<resource­ ref>或 <resource­env­ref>元素,都必须定义资源参数。

资源参数是用名称定义的,使用的资源管理器(或者 object factory)不同,参数名称的

集合也不一样。这些参数名与工厂类的 JavaBeans 属性相对应。JNDI 实现通过调用对应的 JavaBeans 属性设置函数来配置特定的工厂类,然后通过 lookup()调用使得该实例可见。

一个 JDBC 数据源的资源参数可以按照如下方式定义:

<GlobalNamingResources ...> ... <ResourceParams name="jdbc/EmployeeDB"> <parameter> <name>driverClassName</name> <value>org.hsql.jdbcDriver</value>

</parameter> <parameter> <name>driverName</name> </value>jdbc:HypersonicSQL:database</value>

</parameter> <parameter> <name>user</name> <value>dbusername</value>

</parameter> <parameter> <name>password</name>

Page 128: Java Web动态图表编程

<value>dbpassword</value> </parameter>

</ResourceParams> ...

</GlobalNamingResources>

如果需要为某个特定的资源类型指定工厂内的 Java 类名,在<ResourceParams>元素中嵌

套一个叫做 factory的<parameter>条目,如表 4.3所示。

表 4.3 在<ResourceParams>元素中嵌套

属 性 环境条目的文字描述(可选)

name

配置的资源名称,相对于 java:comp/env context。这个名称必须与 CATALINA_HOME/conf/server.xml 中

某个<Resource>元素定义的资源名称匹配,或者在/WEB­INF/web.xml 中通过<resource­ref>或者<resource

­env­ref>元素应用

这段配置保持默认即可。

<Service name="Catalina">

Tomcat 的 Standalone Service。Service 是一组 Connector 的集合,它们共用一个 Engine 来处理所有 Connector 收到的请求。这里定义 Service 的名称为“Catalina” 。

<Connector port="8080" maxThreads="150" minSpareThreads="25" maxSpareThreads="75" enableLookups="false" redirectPort="8443" acceptCount="100" connectionTimeout="20000" disableUploadTimeout="true" />

分别说明下列参数:

Ø port:在端口号 8080 处侦听来自客户 browser 的非 SSL的 HTTP1.1 请求。

Ø minSpareThreads:该 Connector 先创建 25 个线程等待客户请求,每个请求由一个线

程负责。

Ø maxSpareThreads:当现有线程不够服务客户请求时,若线程总数不足 75 个,则创建

新线程来处理请求。

Ø acceptCount:当现有线程已经达到最大数 100 时,为客户请求排队。当队列中请求数

超过 100 时,后来的请求返回 Connection refused错误。

Ø redirectport:当客户请求是 https 时,把该请求转发到端口 8443 去,基于 SSL的 http 请求将会被监听 8443端口的 Connector 所处理。

Ø enableLookups:默认为 true。当 Web 应用程序调用 request.getRemoteHost()进行 DNS 查询时,可以获得远程客户端的实际主机名,若为 false则不进行 DNS 查询,而是返

回其 Ip 地址。因为获取远程客户端的实际主机名对 Web 服务器的性能有很大影响,

所以一般设置为 false。

Ø connectionTimeout:指定超时的时间数(以毫秒为单位) ,如果不想设定超时的时间

数,则将其值设定为0即可。

<Engine name="Catalina" defaultHost="localhost">

Engine元素代表与特定的 Catalina Service相关的所有请求处理机制。它从一个或者多个 Connector 接受请求并处理, 将完整的响应返回给 Connector, 由 Connector 最终传回给客户端。 defaultHost 指定默认的处理请求的主机名,默认虚拟主机是本地机(localhost)。

每个 Service 元素必须有且仅有一个 Engine 元素,Engine 元素跟在对应的 Connector 元

素的后面,所有的 Engine支持如表 4.4 所示的属性。

表 4.4 Engine支持的属性

Page 129: Java Web动态图表编程

属 性 描 述

backgroundProcessorDelay 这个值代表在该 engine 及其子容器(包括所有的 wrappers)上调用 backgroundProcess 方

法的延时,以秒为单位。如果延时值非负,子容器不会被调用,这意味着子容器使用自己的

处理线程。如果该值为正,会创建一个新的线程。在等待指定的时间以后,该线程在 Engine

及其子容器上调用 backgroundProcess方法。如果没有指定,默认值为 10,代表 10 秒的延时

className 实现的 Java 类名。该类必须实现 org.pache.catalina.Engine 接口。如果没有指定,使用标准

实现

defaultHost 默认主机名。默认主机用来处理不能匹配的任何 Context 路径请求。嵌套在 Engine 中的

Host 元素中,必须有一个 Host 的 name 属性与该名称匹配

jvmRoute 实现了动态负载均衡,并且实现了会话绑定机制,即用户第一次访问的会话将绑定在首次

访问的 tomcat 上,防止会话信息丢失

name Engine 的逻辑名,用在日志和错误消息中

debug 与这个 Engine 相关联的 Logger 记录的详细程度。数字越大,输出越详细。如果没有指定,

该值为 0

可以在 Engine元素中嵌入一个或者多个 Host 元素,每个 Host 元素代表一个不同的虚拟

主机。至少需要定义一个 Host,并且嵌套的 Host 元素中,必须有一个名称与上面指定的 defaultHost 属性相匹配。

也可以在 Engine元素中嵌入一个 DefaultContext 元素 (可选), 用来定义自动发布的 Web 应用的默认属性。

下列元素可以嵌套在 Engine元素中,但每种元素至多只能嵌套一次:

Ø Logger:配置一个 logger,用来接收和处理 Engine 的所有日志消息,还包括与这个 Engine 相关的所有 Connector 的消息。另外,Engine 还负责记录嵌套的所有 Host 和 Context 的消息,除非被低层的 Logger 配置覆盖。

Ø Realm: 配置一个Realm, 允许用户数据库, 以及用户相关角色在所有的Host和Context 之间共享,除非被低层的 Realm配置覆盖。

<Realm className="org.apache.catalina.realm.UserDatabaseRealm" resourceName="UserDatabase"/>

Realm元素是一个包含用户名、密码和用户角色的数据库。角色与 Unix 的 group类似。 Realm 的不同实现允许将 Catalina 集成认证信息加载到已经被创建和维护的环境中,然后利

用这些信息来实现容器管理的安全性(Container Managed Security)。

所有 Realm的实现都支持如表 4.5 所示的属性。

表 4.5 所有 Realm的实现支持的属性

属 性 描 述

className 实现的 Java 类名。这个类必须实现 org.apache.catalina.Realm接口

和大多数 Catalina 组件不一样的是, Realm有几个标准的实现。 所以, 必须使用 className 属性来选择读者希望使用的实现。

JDBC Database Realm (org.apache.catalina.realm. UserDatabaseRealm)

JDBC Database Realm将 Catalina 连接到一个在 JNDI中注册名字为 UserDatabase的关系

数据库,通过正确的 JDBC 驱动、访问、用来查询用户名、密码和它们相关的角色。由于查

询是在每次必要的时候完成的,因此数据库的改变会马上反映到用来认证新登录的信息中。

<Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true"

Page 130: Java Web动态图表编程

xmlValidation="false" xmlNamespaceAware="false">

Host 元素代表一个虚拟主机,虚拟主机将服务器的域名(比如 www.mycompany.com)

和运行 Catalina 的某个特定服务器联系起来。为了生效,网络名称必须在管理所在 Internet 域的 DNS 服务器进行注册。

在许多情况下,系统管理员可能希望为同一个虚拟主机或应用关联多个域名(比如 www. mycompany.com和 company.com)。这可以利用下面讨论的Host Name Alias特征来完成。

在 Engine元素中可以嵌套一个或多个 Host 元素。在 Host 元素中可以嵌套 context 元素。

在与每个 Engine 相关联的所有 Host 中,必须有一个 Host 的名称与 Engine 的 defaultHost 属

性相匹配。Host 的相关属性如表 4.6 所示。

表 4.6 Host的相关属性

属 性 描 述

appBase 虚拟主机的 Application Base 目录。这是在该虚拟主机上发布 Web应用的目录路径。可以

指定绝对路径,或者使用相对于 CATALINA_HOME 的路径

autoDeploy 这个标志表示,在 Tomcat 运行的时候,放到 appBase 目录下的新的 Web 应用程序。默认

为 true。这方面的更多信息请参考应用自动发布

backgroundProcessorDelay 这个值代表在该 host 及其子容器(包括所有的 wrappers)上调用 backgroundProcess 方法

的延时,以秒为单位。如果延时值非负,子容器不会被调用,这意味着子容器使用自己的处

理线程。如果该值为正,会创建一个新的线程。在等待指定的时间以后,该线程在 Host 及

其子容器上调用 backgroundProcess 方法。Host 使用后台处理与 Web应用实时发布有关的操

作。如果没有指定,默认值是−1,说明 host 依赖其所属的 Engine 的后台处理

className 实现的 Java 类名。该类必须实现 org.apache.catalina.Host 接口。如果没有指定,使用标准实

deployOnStartup 此标志表明该 host 的 Web应用是否由 host configurator 自动发布。默认为 true。这方面的

更多信息请参考自动应用发布

name 虚拟主机的域名,也就是在 DNS 服务器上注册的名称。嵌套在 Engine 的所有 Host 中,必

须有一个 Host 的名字与 Engine 的 defaultHost 属性相同。如果要为同一个虚拟主机指定多个

域名,可以使用主机别名

debug 与 Engine 相关联的 Logger 的调试信息的详细程度。数字越大,输出越详细。如果没有指

定,默认值为 0

deployXML 如果不想使用 Context XML 配置文件来发布 Web 应用,设为 false。同时也失去了利用

manager 应用程序安装 Web 应用或者“.war”文件的能力(这些 Web 应用或.war 文件不在

Host 的配置基准目录

$CATALINA_HOME/conf/[engine_name]/[host_name]下面)Web 应用使用 catalina 的安全

许可发布,如果需要让不可信的用户管理 Web应用,这个值可以设为 false。默认为 true

errorReportValveClass Host 使用的错误报告 valve 的 Java 类名。这个 valve 的责任是输出错误报告。设置这个值

可以定制 Tomcat 产生错误页面的格式。这个类必须实现 org.apache.catalina.Valve 接口。如

果没有指定,则使用默认值

org.apache.catalina.valvees.ErrorReportValve.

unpackWARs 如果希望将位于 appBase 目录下的 WAR 文件解压缩成对应的目录结构,设为 true。如果

希望直接从 WAR 文件运行 Web 应用,设为 false。更多信息请参考应用自动发布

续表

属 性 描 述

workDir Host 的 Web应用使用的临时目录的路径。每个应用都有自己的子目录,用于临时的读写。

Page 131: Java Web动态图表编程

如果在 Context 中设置了 workDir 属性,它将会覆盖 Host 的 workDir 属性。如 Servlet

Specification中所述,通过 servlet context 的属性 javax.servlet.context.tempdir,这个目录可以

被 servlet 使用。如果没有指定,使用$CATALINA_HOME/work 下面合适的目录

我们可以在 Host 元素中嵌套一个或者多个 Context 元素,每个 Context 元素代表这个虚

拟主机下的一个不同的Web 应用。同时,可以嵌套一个 DefaultContext 元素,用来定义后续

发布的 Web 应用的默认值。也可以在 Host 元素中选择嵌套一个 DefaultContext 元素,用来定

义自动发布的 Web 应用的默认特性。

下列元素可以嵌套在 Host 元素中,但至多只能嵌套一个实例:

Ø Logger——配置一个 logger,用来接收和处理 Host 的所有日志消息,以及这个 Host 的所有 Context 的日志消息(除非被低一级的 Logger 配置覆盖) 。

Ø Realm——配置一个 realm,Realm 的用户数据库,以及用户角色被这个 Host 的所有 Context 共享(除非被低一级的 Realm配置覆盖) 。

2.应用自动发布

如果没有改变 Tomcat 默认的 deployOnStartup属性(默认为 true),当 Catalina 第一次启

动时,会自动采取如下步骤:

(1) 假定 CATALINA_HOME/conf/[engine_name]/[host_name]目录中的任何 XML文件都

包含一个 Context 元素(以及它相关的子元素),通常情况下,这个<Context>的 docBase属性

指向一个 Web 应用目录的绝对路径,或者是WAR 文件的绝对路径。

(2)如果 WAR 文件对应的目录不存在,则这个 WAR 文件会被自动展开,除非 unpackWARs 属性设为 false。在重新发布更新后的 WAR 文件时,重新启动 Tomcat 之前一定

要删除展开后的目录,这样更新后的 WAR 文件才会被重新展开(如果使用 auto deployer,它

会自动完成这项工作) 。

(3)application base目录下的任何子目录,如果包含/WEB­INF/web.xml 文件,Tomcat 认为是一个展开后的 Web 应用,会为这个目录自动产生一个 Context 元素,即使该目录没有

在 conf/server.xml 文件中出现。产生的 Context 会使用 DefaultContext 中的属性来配置。自动

产生的 Context 的 context 路径是“/”后面跟上目录名,除非目录名是 ROOT,这种情况下 context 路径是空字符串( “” )。

除了启动时的自动发布以外,在 Tomcat 运行时,当新的 XML 配置文件,WAR 文件或

者子目录(包含新的 Web 应用)放到 appBase 目录下,或者当 XML 配置文件放到 CATALINA_HOME/conf/[engine_name]/[host_name]目录的时候, 该Web 应用被自动发布。 auto deployer 也负责跟踪 Web 应用的如下变化:

(1)如果更新了WEB­INF/web.xml 文件,会触发 Web 应用的重载。

(2)如果WAR 文件被更新,并且 WAR 文件已经展开,首先删除展开的 Web 应用,然

后发布更新的 WAR 文件。

(3)如果 XML配置文件被更新,首先删除该应用(但是不删除任何展开以后的目录),

然后发布相关的 Web 应用。

其实,对于普通的应用来说,上述配置基本可以保持不变。Tomcat 默认的 Web 应用程

序的根目录文档是位于其安装目录\webapps\ROOT下。如果希望指定另外一个目录作为我们

的 Web 应用程序的根目录, 该如何修改 server.xml 呢?假设希望将 D:\webchart 作为我们 Web 应用程序的根目录,就只需要在 server.xml 的<Host></Host>标记之间,加上以下内容:

<Context docBase="d:\webchart" path="" debug="0" reloadable="true" crossContext="true" />

Ø docBase:应用程序的路径或者是 WAR 文件存放的路径,这里我们指定为 D 盘下的

Page 132: Java Web动态图表编程

webchart 子目录。

Ø path:表示此 Web 应用程序的 url 的前缀,这样请求的 url 为 http://localhost: 8080/path/****,注意我们这里的 path的值为空字符串 (“”) , 表示是 ROOT文档目录。

Ø reloadable:该属性非常重要,如果为 true,则 Tomcat 会自动检测应用程序的 /WEB­INF/lib 和/WEB­INF/classes 目录的变化,Tomcat 会自动装载新的应用程序。当

然,如果 jsp 文件也发生变化,Tomcat 也会重新编译该 jsp 文件。这样,我们就可以

在不重新启动 Tomcat 的情况下,让 Tomcat 再次装载应用程序。该特征在开发阶段很

有用,但也大大增加了服务器的开销。因此,在 Web 应用正式发布以后,不推荐使

用。

Ø crossContext:如果要在应用程序内调用 ServletContext.getContext()来返回在该虚拟主

机上运行的其他 web application的 request dispatcher,就需要将其值设为 true。在安全

性很重要的环境中, 还是设置为 false, 使得 getContext()总是返回 null。 默认值为 false。

到这里,Tomcat 的一些设置及工作机制就介绍完了。更多的内容,请读者查阅相关的 Tomcat 文档。

现在来测试我们修改后的 server.xml 是否可以正常工作。首先在 D 盘上新建一个目录: webchart。然后,将位于\chap04\Fig4.1\Fig.4.1_02\目录下的 index.jsp 文件拷贝到 D:\webchart 下,重新启动 Tomcat,打开浏览器,并在地址栏输入 http://localhost:8080。如果看到如图 4.12 所示的结果,就说明Web 应用程序的根目录设置好了。

现在修改 D:\webchart\index.jsp文件的第 25 行~第 27行的代码,如下所示:

10*6的积是:&nbsp;<%=getProduct(10,6)%> <BR><BR> 10/6的商是:&nbsp;<%=getQuotient(10,6)%>

保存 index.jsp文件,直接刷新浏览器,可以看到如图 4.13 所示的结果。

可见,在没有重新启动 Tomcat 的情况下,当浏览 index.jsp 文件的时候,Tomcat 检测到 index.jsp 自上次编译以来,源文件发生了变化,于是就自动编译。这样,我们就得到了正确

的结果。

图 4.12 定制 Tomcat 图 4.13 更改后的 index.jsp

讨论完了 server.xml,我们再继续阐述 web.xml 的核心配置。

<servlet> <servlet­name>default</servlet­name> <servlet­class>org.apache.catalina.servlets.DefaultServlet

</servlet­class> <init­param>

<param­name>debug</param­name> <param­value>0</param­value>

</init­param> <init­param>

<param­name>listings</param­name> <param­value>true</param­value>

Page 133: Java Web动态图表编程

</init­param> <load­on­startup>1</load­on­startup>

</servlet>

当用户的 HTTP 请求无法匹配任何一个 servlet 的时候,该 servlet 被执行。

<servlet> <servlet­name>invoker</servlet­name> <servlet­class> org.apache.catalina.servlets.InvokerServlet

</servlet­class> <init­param>

<param­name>debug</param­name> <param­value>0</param­value>

</init­param> <load­on­startup>2</load­on­startup>

</servlet>

处理一个 Web 应用程序中的匿名 servlet。当一个 servlet 被编写并编译放入/WEB­ INF/classes/中,却没有在/WEB­INF/web.xml 中定义的时候,该 servlet 被调用,把匿名 servlet 映射成/servlet/ClassName的形式。传统上这个 servlet 的 URL被映射为“/servlet/*” 。

<servlet> <servlet­name>jsp</servlet­name> <servlet­class>org.apache.jasper.servlet.JspServlet</servlet­

class> <init­param>

<param­name>fork</param­name> <param­value>false</param­value>

</init­param> <init­param>

<param­name>xpoweredBy</param­name> <param­value>false</param­value>

</init­param> <load­on­startup>3</load­on­startup>

</servlet>

当请求的是一个 JSP 页面的时候, (*.jsp)将调用该 JSP 页面的 Servlet。它说明将客户

端请求的 JSP 页面首先编译成为 Servlet,然后再执行。这种方式也就是 Tomcat 对 JSP 页面

提供支持的机制。其实所有的 JSP/Servlet 服务器对 JSP 的工作机制都是一样的,即 JSP 会被 JSP/Servlet 服务器首先自动编译成 Serlvet,然后以 Serlvet 的方式工作。

<welcome­file­list> <welcome­file>index.html</welcome­file> <welcome­file>index.htm</welcome­file> <welcome­file>index.jsp</welcome­file>

</welcome­file­list>

设定网站索引文件的优先级。当客户端的请求没有指明是访问哪个页面文件的时候,默

认首先返回 index.html 文件,其次是 index.htm文件,最后才是 index.jsp文件。

如果希望默认最先返回的页面索引文件是 index.jsp,则只需要将 index.jsp的位置放在第

一个 welcome­file的位置上即可。

最后谈谈 Tomcat 服务器处理一个 http请求的全过程:

假设来自客户的请求为:

http://localhost:8080/chart/chart_index.jsp

(1)http请求被发送到本机端口 8080,被在那里监听的 Coyote HTTP/1.1 Connector 获

得。

(2)Connector 把该请求交给它所在的 Service的 Engine来处理,并等待来自 Engine的

回应。

Page 134: Java Web动态图表编程

(3)Engine获得请求 localhost/chart/chart_index.jsp,匹配它所拥有的所有虚拟主机 Host。

(4)Engine匹配到名为 localhost 的 Host(即使匹配不到也把请求交给该 Host 处理,因

为该 Host 被定义为该 Engine的默认主机) 。

(5)localhost Host 获得请求/chart/chart_index.jsp,匹配它拥有的所有 Context。

(6)Host 匹配到路径为/chart 的 Context(如果匹配不到就把该请求交给路径名为""的 Context 去处理) 。

(7)path="/chart"的 Context 获得请求/chart_index.jsp,在它的 mapping table中寻找对应

的 servlet。

(8)Context 匹配到 URL PATTERN 为*.jsp的 servlet,对应于 JspServlet 类。

(9)构造 HttpServletRequest 对象和 HttpServletResponse对象,作为参数调用 JspServlet 的 doGet 或 doPost 方法。

(10)Context 把执行后的 HttpServletResponse对象返回给 Host。

(11)Host 把 HttpServletResponse对象返回给 Engine。

(12)Engine把 HttpServletResponse对象返回给 Connector。

(13)Connector 把 HttpServletResponse对象返回给客户 browser。

为进一步了解 JSP 的工作原理,我们以 index.jsp 为例,来阐述 Tomcat 对其编译后的结

果。

4.1.4 转换后的 JSP 页面文件

在 Tomcat 的安装目录下(本例:C:\tomcat5.5)的 work\Catalina\localhost\_\org\apache\ jsp 子目录中,可以找到名为 index_jsp.java 的 java 文件及其编译后的 index_jsp.class 的字节码文

件。 上节所讲述的 index.jsp文件首先被改写为: index_jsp.java, 然后再被编译成字节码的 class 文件。

所以, 当客户端第一次请求 JSP 页面时, 需要一点时间等待, 当该 JSP 页面被编译成 class 文件后,只要 JSP 源文件没有改变,当再次请求该 JSP 页面时,就不再有任何延时了。

1: package org.apache.jsp; 2: 3: import javax.servlet.*; 4: import javax.servlet.http.*; 5: import javax.servlet.jsp.*; 6: 7: public final class index_jsp extends org.apache.jasper.runtime.

HttpJspBase 8: implements org.apache.jasper.runtime.JspSourceDependent 9: 10: 11: int getProduct(int a, int b) 12: 13: 14: return a * b; 15: 16: 17: float getQuotient(int a, int b) 18: 19: return (float)(a) / b; 20: 21: 22: private static java.util.Vector _jspx_dependants; 23: 24: public java.util.List getDependants() 25: return _jspx_dependants; 26: 27:

Page 135: Java Web动态图表编程

28: public void _jspService(HttpServletRequest request, HttpServlet Response response)

29: throws java.io.IOException, ServletException 30: 31: JspFactory _jspxFactory = null; 32: PageContext pageContext = null; 33: HttpSession session = null; 34: ServletContext application = null; 35: ServletConfig config = null; 36: JspWriter out = null; 37: Object page = this; 38: JspWriter _jspx_out = null; 39: PageContext _jspx_page_context = null; 40: 41: 42: try 43: _jspxFactory = JspFactory.getDefaultFactory(); 44: response.setContentType("text/html;charset=GB2312"); 45: pageContext = _ jspxFactory. getPageContext (this, request,

response, 46: null, true, 8192, true); 47: _jspx_page_context = pageContext; 48: application = pageContext.getServletContext(); 49: config = pageContext.getServletConfig(); 50: session = pageContext.getSession(); 51: out = pageContext.getOut(); 52: _jspx_out = out; 53: 54: out.write("<!­­\r\n"); 55: out.write("\tFig. 4.01_02: index.jsp\r\n"); 56: out.write("\t功能: 测试修改后的 server.xml及 Tomcat是否正常工作

\r\n"); 57: out.write("­­>\r\n"); 58: out.write("\r\n"); 59: out.write("\r\n"); 60: out.write("<HTML>\r\n"); 61: out.write("<HEAD>\r\n"); 62: out.write("<TITLE> 测试修改后的 server.xml及 Tomcat是否正常工作

</TITLE>\r\n"); 63: out.write("</HEAD>\r\n"); 64: out.write("\r\n"); 65: out.write("<BODY>\r\n"); 66: out.write("\r\n"); 67: out.write("20*6的积是:&nbsp;"); 68: out.print(getProduct(20,6)); 69: out.write("\r\n"); 70: out.write("<BR><BR>\r\n"); 71: out.write("20/6的商是:&nbsp;"); 72: out.print(getQuotient(20,6)); 73: out.write("\r\n"); 74: out.write(" \r\n"); 75: out.write("</BODY>\r\n"); 76: out.write("</HTML>\r\n"); 77: catch (Throwable t) 78: if (!(t instanceof SkipPageException)) 79: out = _jspx_out; 80: if (out != null && out.getBufferSize() != 0) 81: out.clearBuffer(); 82: if (_jspx_page_context != null) _jspx_page_context. handle

PageException(t); 83: 84: finally 85: if (_jspxFactory != null)_jspxFactory.releasePageContext (_

jspx_page_context); 86: 87: 88:

Page 136: Java Web动态图表编程

index_jsp.java 的第 11 行~第 20 行、第 44 行,以及第 54 行~第 76 行就是被 Tomcat 改

写的 index.jsp文件。

该转换过程是由 Tomcat 自动进行的,我们只需要注意 JSP 源文件的编写就可以了。

至此,Tomcat 运行机制和配置就介绍完了。本书以后的例程,将以本节介绍的 Tomcat 5.5.7 为 JSP/Servlet 服务器。

4.2 Resin 的安装和配置

Resin是由 Caucho公司开发的目前运行速度最快的 JSP/Servlets 运行平台。Resin是一款

十分出色的、商业级的、支持 JSP/Servlets/J2EE 的 Web 服务器。笔者一直非常喜欢 Resin,

个人认为它比 Tomcat 更为优秀,尤其对中文的支持,Resin比 Tomcat 要完善。在我们开发过

的基于 JSP 的 Browser/Server 的 Web 应用程序中,几乎全部都采用了 Resin作为 JSP/ Servlets 运行平台。Resin的下载地址是 http://www.caucho.com/download/index. xtp。

利用 Resin 开发或者学习都是免费的,但是如果把 Resin 作为收费产品发布是需要付费

的。以实际的使用效果来看,我们推荐使用 Resin 3.0.11 版。

4.2.1 Resin 的安装

Resin 的安装很简单,把下载后得到的 Resin­3.0.11.zip 文件解压到任何目录下面即可,

如 c:\resin­3.0.11。以下的介绍都假设 Resin安装在 c:\resin­3.0.11 目录下。我们可以看到如图 4.14 所示的目录结构。

这里我们讲述最重要的三个目录:

(1)bin子目录:存放启动与关闭 Resin服务器的可执行文件,3.0.11 版在 bin子目录只

存放了 Unix 下的 Resin 的启动脚本,此前的版本,把 Windows 下的启动程序也都放在这个

目录中。

(2)conf子目录:与 Tomcat的 conf子目录类似,存放 Resin服务器启动的配置文件。

(3)doc子目录:默认的 Web 应用程序根目录。

现在先试着运行 Resin服务器。因为 Resin默认的监听端口和 Tomcat 一样,都是 8080,

所以如果 Tomcat 正在运行中,并且没有修改默认的监听端口 8080,那么会和 Resin 发生冲

突,所以,我们先暂时停止 Tomcat 服务器的运行。

首先进入命令行控制台,然后进入 c:\resin­3.0.11\目录,在该目录执行 httpd,就可以运

行 Resin服务器了。Resin服务器的启动过程如下:

(1)弹出一个有 start 和 stop两个单选按钮和一个 Quit 按钮的对话框。

(2)同时出现一个显示 Resin的运行相关信息的窗口,如图 4.15所示。

Page 137: Java Web动态图表编程

图 4.14 Resin的目录结构 图 4.15 Resin的启动过程

我们从图 4.15 中可以看到,默认的 Resin 服务器监听 8080 端口。点选 Stop,可以停止

当前的 Resin服务器进程;再点选 Start,又可以开启新的 Resin服务器进程。关闭该对话框,

则回到 Command控制台命令符的提示状态下。

启动浏览器,输入 http://localhost:8080,我们可以看到如图 4.16所示的画面。

图 4.16 Resin的默认启动页面

Page 138: Java Web动态图表编程

4.2.2 Resin 的配置

本书是以 Tomcat 作为 JSP/Servlet 的服务器。 其实 Resin 可以说是比 Tomcat 更优秀的 JSP/Servlet 服务器,为了让读者同时体验这两款优秀的 JSP/Servlet 的服务器的运行性能和特

点,我们可以同时安装并运行 Tomcat 与 Resin 服务器,并将它们的 Web 应用程序的根目录

都设置为 D:\webchart。这样,读者可以在实际的工作中体会这两者之间的不同。 (注意:为

让读者可以轻松地配置本书的运行环境,故本书不涉及如何将 Resin和 IIS/Apache的集成)。

下面简要介绍一下 Resin服务器的配置。Resin服务器和大多数 Java Web 服务器一样,

通过一个 xml 格式的 conf文件进行配置。

进入 c:\resin­3.0.11\conf目录,打开 resin.conf。先查找到第 70 行:

<http server­id="" host="*" port="8080"/>

用以下的方式修改默认的监听端口 8080:

<http server­id="" host="*" port="8888"/>

然后修改默认的应用程序的根目录,找到第 223行:

<web­app id='/' document­directory="webapps/ROOT"/>

修改为:

<web­app id='/' document­directory=" d:\webchart "/>

其他设置保持不变就可以了,现在保存修改后的 resin.conf 文件。我们甚至不用重新启

动 Resin 服务器,Resin 服务器在监测到其配置文件 resin.conf 发生改变后,会立即重新加载

配置文件 resin.conf,并自动重新启动 resin.conf。稍等一会,就可以用以下的方法: http://localhost:8888

来测试 Resin服务器运行是否成功。如果没有再次修改 D:\webchart 下的 index.jsp文件,

那么就应该看到与图 4.13 所示完全一样的结果,这说明我们的修改是成功的。

如果在 NT4 或者Win2k/XP/2003 环境下,需要把 Resin设置成一个服务进程,则按照以

下步骤进行:

(1)先按本书第 1 章所示的方法再创建一个系统变量,在“变量名”中输入“RESIN_ HOME” ,在“变量值”中输入“c:\resin­3.0.11” 。

(2)在 Command控制台进入 c:\resin­3.0.11\bin目录下,键入如下命令:

httpd –install ­conf conf/resin.conf

就可以在【控制面板】的【管理工具】的【服务】下面看到新增的一条“Resin Web Server”

的自动服务。以后只要进入NT4或者Win2k/XP/2003,就可以自动启动 Resin服务。

(3)如果要卸载该服务,只需要在 Command控制台进入 c:\resin­3.0.11\bin目录下,键

入如下命令即可:

httpd –remove

Resin自带的帮助文档已经非常详尽,读者可以自行阅读。

4.3 BEA Weblogic 的安装和配置

应用服务器(Application Server)是运行 Java 企业组件的平台,在开发企业级的 Web应

用程序方面能提供更灵活强大的支持,构成应用软件的主要运行环境。当前主流的应用服务

Page 139: Java Web动态图表编程

器是 BEA公司的 Weblogic Server 和 IBM 公司的 Websphere,以及免费的 Jboss。我们推荐使

用 Weblogic,因为它的体系结构更加简洁,开发和部署更加方便,是 Java 企业软件开发人员

首选的开发平台。 Weblogic是由 BEA公司开发的目前应用最为广泛的应用服务器。尤其在 BEA网站上注

册后,BEA公司提供长达一年免费的 Weblogic使用期限。对于希望学习 J2EE 的读者来说,

这实在是一个好消息。 Weblogic的下载地址:http://commerce.bea.com/index.jsp。 Weblogic的在线文档链接地址:http://edocs.bea.com/。

4.3.1 BEA Weblogic 的安装

安装步骤如下所示:

(1)双击 platform813_win32_free.exe 安装程序,安装程序将会自解压并开始安装前的

准备工作,如图 4.17所示。

图 4.17 BEA Weblogic的安装过程 1

(2)安装前的准备工作结束后,单击【Next】按钮,如图 4.18所示。

(3)创建一个新的 BEA Home,这里我们选择默认的 C:\bea,然后单击【Next】按钮,

如图 4.19 所示。

Page 140: Java Web动态图表编程

图 4.18 BEA Weblogic的安装过程 2 图 4.19 确定 BEA 的 Home的目录

(4)选择安装类型,这里我们选择完全安装【Complete】,然后单击【Next】按钮,如

图 4.20 所示。

(5)选择 Weblogic的安装目录,这里仍然选择其默认的安装目录,如图 4.21 所示。

图 4.20 选择 BEA Weblogic的安装组件 图 4.21 选择Weblogic的安装目录

(6)然后是相关文件的拷贝画面,如图 4.22 所示。

图 4.22 拷贝相关文件

(7)没有必要安装 XML Spy这个 XML编辑程序和运行快速启动(Run QuickStart),单

击【Done】结束安装过程,如图 4.23 所示。

Page 141: Java Web动态图表编程

图 4.23 结束 BEA Weblogic的安装

4.3.2 BEA Weblogic 的配置

在 weblogic7.x 之前,安装完成 weblogic会自动创建默认的应用目录 DefaultWebApp。如

果没有特别的需要,就可以利用这个默认的应用目录部署 Web 应用程序或者 J2EE 系统了。

而在 weblogic8.x 之后版本中,它不会自动创建默认的应用目录。所以我们需要使用 Configuration Wizard或者 QuickStart 来创建自己的应用目录。

(1)依次单击【开始】→【所有程序】→【BEA WebLogic Platform 8.1(BEA Home 1)】

→【Configuration Wizard】,启动【BEA WebLogic Configuration Wizard】,会出现如图 4.24 所

示的配置界面,选择【create a new weblogic configuration】,单击【Next】按钮。

图 4.24 创建一个新的配置

(2)在【template】选项中选择【base weblogic server domain】,然后单击【Next】按钮,

如图 4.25 所示。

Page 142: Java Web动态图表编程

图 4.25 选择【base weblogic server domain】

(3)选择快速配置【Express】,然后单击【Next】按钮。

(4)在创建管理员用户名和密码的界面上输入用户名、密码和创建该服务的描述(请

记住这个用户密码,它是启动这个服务和进入服务控制台的账号),然后单击【Next】按钮,

如图 4.26 所示。

图 4.26 创建管理员的用户名和密码

(5)选择 JDK的版本。目前 Weblogic自带的 JDK为 1.4.2 版。其又分为两个版本:一

个是由 Weblogic 公司开发的 JRockit 版,另一个是 Sun 公司的 JDK。Weblogic 的 JRockit 版 JDK 对 Sun公司的 JDK 在内存的垃圾回收上做了优化,性能更加优良。所以,这里我们选择 JRockit。单击【Next】按钮,如图 4.27所示。

Page 143: Java Web动态图表编程

图 4.27 选择 JDK的版本

提示: 根据我们的测试, 目前版本Weblogic 8.13对 J2SE5.0的支持并不好。 即使选择 【Other Java SDK】,并单击旁边的【Browse】按钮,在弹出的对话框中,选择 C 盘下的【jdk1.5.0】。

但在其后的启动中,却会出现错误而无法启动成功,所以这里我们还是选择 JDK1.4.2 版。

(6)该界面可以修改创建服务的目录和名称,这里我们保持默认值,然后单击【Create】

按钮开始创建。

图 4.28 结束创建服务

Page 144: Java Web动态图表编程

4.3.3 测试 BEA Weblogic 的配置

下面启动Weblogic来检查配置是否成功。 依次单击 【开始】 →所有程序】 → 【BEA WebLogic Platform 8.1(BEA Home 1)】→【User Projects】→【My Domain】→【Start Server】, 如果

出现如图 4.29 所示的内容,则表示配置成功。

图 4.29 启动Weblogic

注意,最后两行要包含如下所示的内容:

<Server started in RUNNING mode> <Thread "ListenThread.Default" listening on port 7001, ip address *.*>

现在打开浏览器,输入地址:

http://localhost:7001/console

会出现如图 4.30 所示的登录界面, 这里需要我们输入刚刚在创建应用目录时输入的用户

名和密码。

图 4.30 登录Weblogic服务器的管理控制台

现在将进入Weblogic的控制台。接下来部署我们第一个 Web 应用程序。

Page 145: Java Web动态图表编程

4.3.4 部署第一个 Web 应用程序

部署步骤如下所示:

(1) 将Web程序打包。 在硬盘上新建一个目录, 如Weblogic_Builder, 然后将D:\web chart\ 目录下的 index.jsp 文件及 chap04 子目录中的内容全部拷贝到 Weblogic_Builder 下。在 Weblogic_Builder 目录下新建一个子目录 WEB­INF,再在 WEB­INF 子目录下新建一个名为 web.xml 的文档,内容如下:

<?xml version="1.0" encoding="ISO­8859­1"?>

<web­app xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema­instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee web­app_2_4.

xsd" version="2.4">

<web­app> <welcome­file­list> <welcome­file>index.jsp</welcome­file> </web­app>

进入命令行控制台,在 Weblogic_Builder 目录下执行以下命令:

jar cvf test.war *.*

将 Weblogic_Builder 目录下的所有文件打包成 test.war 文件;

(2)登录 Weblogic的控制台。

(3)依次单击【mydomain】→【Deployments】→【Web Application Modules】→【Deploy a new Web Application Module…】如图 4.31 所示。

图 4.31 在Weblogic下部署Web应用程序

(4)然后单击【Upload Your File(s)】→【浏览】,选择打包文件 test.war,单击【Upload】

按钮,返回先前的页面,再单击【myserver】,如图 4.32 所示。

Page 146: Java Web动态图表编程

图 4.32 上传打包好的 test.war文件

(5)单击【upload】,在出现的页面中选择【test.war】,单击【Target Module】,如图 4.33 所示。

(6)在出现的页面中单击【Deploy】选项,进行部署。

(7)当看到如图 4.34 的结果,则说明Web 应用程序部署成功了。

现在,我们启动浏览器,在地址栏中输入 http://localhost:7001/test

图 4.33 选择已经上传到服务器的 test.war文件 图 4.34 Web应用程序部署成功的显示页面

或者输入 http://localhost:7001/test/index.jsp,应该会看到类似图 4.13 所示的结果,如果在

地址栏输入 http://localhost:7001/test/chap04/Fig4.1/Fig.4.1_01/test.jsp则会看到类似图 4.11所示

的结果。Weblogic下的 Web 应用程序的部署我们就介绍到这里。

4.4 本章小结

本章先阐述了两个轻量级的 JSP/Servlet 的服务器 Tomcat、Resin 的安装和配置过程,然

后讲述了企业级的 J2EE 应用服务器 Weblogic的安装、配置和 Web 应用程序的部署。

正确地理解和配置 JSP/Servlet 服务器会对我们今后 Web 应用程序的发布带来很大方便。

本书将在后面的 Web 图表例程中,继续介绍相关的配置工作。

在理解本章的基础上,我们就可以开发一些基于 JSP/Servlet 的 Web 图表应用程序了。下

一章我们将阐述基于 Servlet 的 Web 图表编程技术。

Page 147: Java Web动态图表编程

第 5 章 基于 Servlet 的 Web 图表编程

我们在阐述 Tomcat 安装及配置过程的时候,谈论过 JSP 最终都被 JSP/Servlet 服务器转

换成 Java Servlet 而被执行。JSP 为实现 Web 的请求/响应机制提供了一种方便而强大的方法,

并且开发人员无须了解 Servelt的底层细节。 Servlet和 JSP技术一起构成了 Java 2企业版 (Java 2 Enterprise Edition, J2EE)的 Web 层技术。

本章讲述 Web 的请求/响应机制(主要是 get 和 post 请求)、如何部署 Servlet、如何利用 Servlet 生成 Web 动态图表。

5.1 Servlet 简介及其构架

Servlet 是利用 Java 技术编写的,在 Web 服务器端执行的程序。其功能有些类似于以前

的 CGI程序,但是两者的性能和功能却不可同日而语。 Servlet 发展到今天,已经是一种非常成熟的技术了。 本章的 Servlet 将说明客户与服务器

之间如何通过 HTTP 协议进行通信。客户端向服务器发送 HTTP 请求,Servlet 服务器(引擎 /容器)收到请求后,将该请求传递给相应的 Servlet 进行处理。Servlet 在进行处理的过程中,

可能包括与数据库或其他服务器端组件进行交互, 如其他的 Servlet、 JSP 或者 EJB (Enterprise JavaBean)。然后,Servlet 将结果返回给客户,该结果通常是以 HTML、XHTML 或 XML 等文档形式返回给客户端并在浏览器中显示结果。也可以返回其他格式的数据,如图像(图

表)及二进制数据等。本章例程的原理就是利用 Java对图像的处理能力,在服务器端根据相

应的数据而实时生成图像(图表),并将结果以图像的方式返回给客户端,达到生成动态的 Web 图表的目的。

5.1.1 Servlet 的特点

因为 Servlet 是 Java 的一个类,所以它可以借助 Java 在网络、图像、文件、数据库、多

线程、RMI等领域的强大处理能力来实现多种功能,如本章就是借助 Java 对图像的处理能力

而在 Web 服务器端生成实时图像,并返回给客户端。 Servlet 的特点如下所示:

(1)高性能:Servlet 一旦被 JSP/Servlet 容器加载,只要 JSP/Servlet 容器没有重新启动,

它就驻留在内存中,这样,大大加快了反应速度。在传统的 CGI中,每个请求都要启动一个

新的进程。虽然 CGI程序本身执行时间较短,但启动进程所需要的开销很可能反而超过实际

执行时间。而使用 Servlet 时,服务器上仅有一个 Java 虚拟机在运行,只有当 Servlet 被调用

时,它才被加载,而且直到 Servlet 更改时,它才会被要求再次加载。在传统 CGI 中,如果

有 N 个并发的对同一 CGI 程序的请求,则该 CGI 程序的代码在内存中重复装载了 N 次;而

对于 Servlet,处理请求的是 N 个线程,只需要一份 Servlet 类代码。在性能优化方面,Servlet 也比 CGI有着更多的选择,比如缓冲以前的计算结果,保持数据库连接的活动等等。

(2)通过使用 Servlet API,开发人员不必担心服务器的内部运作方式。表格资料、服务

器头、cookies 等都可以通过 Servlet 处理。

Page 148: Java Web动态图表编程

(3)可移植性:Servlet 是用 Java 编写的,当然就不必担心操作系统或服务器的类型,

能将其从一个服务器移到另一个服务器以供发布,这一优点充分体现了 Java“一次编写,随

处运行”的优越特性。

(4)安全性:Servlet 共有几种不同层次的安全保障:

Ø 首先是基于 Java 的安全框架,因为 Servlet 是利用 Java 编写的。

Ø 因 Servlet 是运行于 JSP/Servlet 容器内的,所以,Servlet 也被 JSP/Servlet 容器的安全

机制所保护。

(5)高度可扩展性与模块化:单个的 Servlet 可以完成一个特定的任务,同时,不同的 Servlet 之间可以相互通信、协作以完成复杂的业务逻辑。

5.1.2 Servlet 的接口

从架构上看,所有基于 Servlet 的类都必须实现 Servlet 接口。与前面我们所讲的 Applet 方法类似,Servlet 接口方法是由 Servlet 容器调用的。该接口声明了 5 个方法,如表 5.1所示。

表 5.1 Servlet的接口方法

方 法 说 明

void init (ServletConfig config) 在 Servlet 的执行周期中,Servelt 容器调用该方法一次,以初始化 Servlet。

ServletConfig参数由执行该 Servlet 的 Servlet 容器提供

ServletConfig getServletConfig() 该方法返回实现 ServletConfig 接口的对象引用。该对象提供了对 Servlet 配置

信息的访问, 如 Servlet的初始化参数和 ServeltContext, 而ServeltContext为 Servlet

提供了对自身环境的访问(即执行该 Servlet 的 Servlet 容器)

String getServletInfo() 该方法由编写 Servlet 的程序员定义,用于返回包含了诸如 Servlet 的作者、版

本号等 Servlet 信息的字符串

void service(ServletRequest request,

ServletResponse response)

Servlet 容器调用该方法,以响应客户端对 Servlet 的请求

续表

方 法 说 明

void destroy() 当 Servlet 容器终止 Servlet 时,将调用该“注销”方法。该方法将释放 Servlet

所使用的资源,诸如打开的文件及数据库连接等

Servlet 部署在 JSP/Servlet 容器内,其生命周期由 JSP/Servlet 容器管理,这一点与我们以

前讨论过的 Applet 的生命周期由 Applet 的容器——浏览器管理一样。

Ø 加载 Servlet:该操作一般是用容器动态执行。有些服务器提供了相应的管理功能,可

以在启动的时候就加载 Servlet 并能够初始化特定的 Servlet。

Ø 创建一个 Servlet 实例。

Ø 调用 Servlet 的 init 方法。

Ø 根据客户端对 Servlet 的请求,调用 service方法。

Ø 注销 Servlet:容器通过 destroy方法来注销 Servlet 以释放系统资源。 Servlet 包定义了两个实现 Servlet 接口的抽象类:

Ø GenericServlet 类,该包隶属于 javax.servlet 包。

Ø HttpServlet 类,位于 javax.servlet.http包。

这两个类为 Servlet 接口的所有方法提供了默认的实现。对于开发服务器端的程序员来

说,主要是继承并扩展 GenericServlet 类和 HttpServlet 类,并重写其中的一些或全部方法。 GenericServlet 类也是抽象类,它的 service 方法也是一个抽象方法,所有继承 GenericServlet

Page 149: Java Web动态图表编程

的子类必须直接或间接地实现该方法。HttpServlet 类通过运行 Servlet 接口,实现了 Http协议

的功能。

每个 Servlet 的核心方法是 service 方法,它接收一个 ServletRequest 对象和一个 Servlet Response对象。这些对象提供了对输入和输出流的访问,使 Servlet 可以读取客户的数据,并

向客户发送数据。这些流既可以基于字节,也可以基于字符。如果在 Servlet 执行过程中发生

错误,则会抛出 ServletException或者 IOException异常。

5.1.3 HttpServlet 类简介

基于 Web 的 Servlet 通常扩展 HttpServlet 类。HttpServlet 类重写了 service方法,以区分

从客户端 Web 浏览器中收到的请求。客户端有两种典型的请求类型(方法)——get 方法和 post 方法。

Ø get 请求从服务器获取(读取)数据,一般用于从服务器获取 HTML文档或图像。

Ø post 请求向服务器发送(传送)数据,一般用于向服务器发送信息,如确认信息和用

户提交的表单中的数据。 HttpServlet类定义了 doGet和 doPost方法, 两者分别响应客户端相应的请求类型。 doGet

和 doPost 方法都由 service方法调用。 当客户端的请求到达服务器端时, 首先调用的是 service 方法。service方法会判断客户端的请求类型,然后再调用相应的方法来处理这种请求。

doGet和 doPost方法是以一个HttpServletRequest对象和一个HttpServletResponse对象作

为参数,使得客户端和服务器之间能够进行通信和交互。HttpServletRequest 接口方法使访问

请求所提供的数据更容易; HttpServletResponse接口的方法则便于将 servlet的结果返回给Web 客户。

除此之外,HttpServlet 类还有其他一些方法,如下所示:

Ø doPut 方法:在响应 HTTP 的 put 请求时调用。这种请求通常用来把文件存储在服务

器上。在一些服务器上,由于固有的安全风险,一般不会提供该方法。客户可能在服

务器上放置可执行的应用程序,如果执行该程序,可能会删除某些重要的文件或者占

用系统资源,可能导致服务器受到破坏。

Ø doHead方法:在响应 HTTP 的 head请求时调用。如果客户只希望得到响应的头部,

诸如响应的内容类型和内容长度,则通常使用这种请求。

Ø doOptions 方法:在响应 HTTP 的 options 请求时调用。它将服务器支持的 HTTP 选项

信息返回给客户,诸如 HTTP 的版本号(1.0 版或 1.1版)和服务器支持的请求方法。

Ø doDelete方法:是响应 HTTP 的 delete请求调用。这种请求通常用于从服务器中删除

文件。在有些服务器上,由于固有的安全风险,一般也不提供该方法,因为客户可能

会删除服务器或者应用程序运行所必需的关键文件。

上述 doPut、doHead、doOptions,以及 doDelete方法在 Web 应用程序中很少提供具体的

实现方法。这些方法都接收类型为 HttpServletRequest 和 HttpServletResponse的参数,并且都

返回 void。这部分内容也不在本书的讨论范围之内。一般来说,只需要关注 doGet 及 doPost 方法即可。

下面,我们来讨论 HttpServletRequest 接口和 HttpServletReponse接口。

5.1.4 HttpServletRequest 接口

该接口代表了 HTTP 请求,继承了 ServletRequest 接口。每次调用 HttpServlet 的 doGet 或 doPost方法时, 都会接收到一个实现 HttpServletRequest接口的对象。 执行 Servlet的 Web 服务器创建了一个 HttpServletReques 对象, 并将其传递给 Servlet 的 service方法, 再由 service

Page 150: Java Web动态图表编程

方法传递给 doGet 和 doPost 方法。该对象包含客户的请求信息。

在 HttpServletRequest 接口中提供了许多方法,使 Servlet 能够处理客户请求。最常用的 getParameter 方法就是获得客户请求中的参数,这个参数是客户端表单中的数据。该接口中

的某些方法继承于 ServletRequest 接口。表 5.2 列出了几个常用的方法。

表 5.2 HttpServletRequest接口的部分方法

方 法 说 明

String getParameter(String name) 获取作为 get 或 post 请求的一部分而发送给 Servlet 的参数的值。参数 name 代表

参数的名称

Enumeration

getParameterNames() 该方法返回作为 post 请求的一部分而发送给 Servlet 的所有参数的名称

String[] getParameter(String

name)

对于一个具有多个值的参数,该方法返回一个字符串数组,字符串数组包含了指

定的Servlet参数的值。 该参数的值一般是由客户浏览器表单中的checkbox或者 select

对象提交的

Cookie[] getCookies() 返回一个由服务器存储在客户端上的 Cookie 对象数组。Servlet 可以使用 Cookie

对象惟一性来识别客户

HttpSession getSession(Boolean

create)

HttpSession对象的用途类似于 Cookie 对象,也利用其惟一性来识别客户端。返回

一个与客户端当前浏览器会话相关联的 HttpSession对象, 如果没有给客户端分配一

个 session,则该方法会创建一个 HttpSession 对象(参数为 true 时)

HttpSession getSession() 返回同客户端相关联的 session,如果没有与客户端相关联的 session,返回 null

读者可以在 http://java.sun.com/j2ee/1.4/docs/api/javax/servlet/http/HttpServletRequest. html中

查阅到 HttpServletRequest 接口的所有方法。

5.1.5 HttpServletResponse 接口

该接口代表了对客户端的HTTP响应, 继承了 ServletResponse接口。 每次调用Http Servlet 的 doGet或 doPost方法时, 都会接收到一个实现 HttpServletResponse接口的对象。 执行 Servlet 的 Web 服务器创建了一个 HttpServletResponse对象,并将其传递给 Servlet 的 service方法,

再由 service 方法传递给 doGet 和 doPost 方法。该对象提供了一些方法,使 Servlet 能够显示

地响应客户。其中一些方法继承于 ServletResponse接口。表 5.3 列出了几个常用的方法。

表 5.3 HttpServletResponse接口的部分方法

方 法 说 明

void addCookie(Cookie cookie) 用于将 Cookie 添加到对客户的响应的头部。 Cookie 是否能存储在客户端上,

取决于 Cookie 的生存期和客户端是否接受 Cookie

String encodeURL(String url) 使用 URL 和一个 SessionID 重写这个 URL

ServletOutputStream getOutputStream() 获取一个基于字节的输出流,用于将二进制数据发送给客户端

PrintWriter getWriter() 获取一个基于字符的输出流,用于将文本数据发送给客户端

void setCharacterEncoding

(String charset)

设置响应客户端浏览器的字符编码类型。中国一般使用的字符编码类型为

“GB2312”或者“GBK”

续表

方 法 说 明

void setContentType(String type) 设置响应客户端浏览器的 MIME 类型。 MIME 类型帮助浏览器确定如何显示

数据(或者运行其他应用程序来处理数据)。例如,MIME 类型为“text/html” ,

表示 HTML 文档;MIME 类型为“image/jpeg” ,则表示格式为 jpeg的图像

Page 151: Java Web动态图表编程

读者可以在 http://java.sun.com/j2ee/1.4/docs/api/javax/servlet/http/HttpServletResponse. html 中查阅到 HttpServletResponse接口的所有方法。

5.2 Servlet 处理 HTTP get 请求

HTTP get 请求的主要目的是获取指定 URL服务器上的内容——该内容通常是 HTML或

者 XHTML文档。图 5.1 演示了在 Tomcat 服务器下处理一个 HTTP get 请求的 Servlet 运行过

程。

图 5.1 Tomcat服务器下运行WelcomeServlet的结果

在用户单击 “获得 HTML文档” 按钮 (图 5.1①所示), 将向 URL为 “/welcome” 的 Servlet 发送一个 get 请求。weclome Servlet 响应该请求,为客户端返回一个动态生成的,显示如图 5.1②“您好,欢迎进入 Servlet 图表编程世界”的 XHTML文档。

如果要使用 get 请求,在客户端 HTML 文档中的 Form 表单中必须指定请求的类型为

“get” ,如:

<form action = "/welcome" method = "get"> ….

</form>

调用 Servlet 的 WelcomeServlet.html 文档(chap05\Fig5.2\Fig.5.2_01)在第 12 行~第 19 行:

<form action = "/welcome" method = "get">

<p> 点击按钮调用 Servlet <input type = "submit" value = "获得 HTML 文档" />

</p>

</form>

提供了一个表单(form)。表单的 action( “\welcome” )指定调用的 Servlet 的 URL路径,

所以,它向 URL路径为“\welcome”的 Servlet 发送请求信息,而表单的 method指定发送的

请求类型为“get” ,这就会触发服务器上该 Servlet 的 doGet 方法。 WelcomeServlet.html 文档生成如图 5.1①所示的运行结果。

在运行被调用的 Servlet——WelcomeServlet.java(chap05\Fig5.2\Fig.5.2_01)之前,请先在 Tomcat/Resin 的 Web 应用程序的根目录(本书的 Web 应用程序根目录为 d:\webchart)下的 WEB­INF中新建一个子目录 classes。然后在 classes子目录下新建一个 com子目录,接着在 com 子目录下新建一个 fatcat的子目录,最后在 fatcat的子目录下新建一个webchart的子目录。

Page 152: Java Web动态图表编程

WelcomeServlet.java 程序第 4 行:

package com.fatcat.webchart;

使用了 package语句,说明本源程序要严格地按照 Java Servlet 的规定,将源程序部署在

相应的目录中。将本源程序 WelcomeServlet.java 拷贝到刚创建的 Tomcat 或者 Resin 的 Web 应用程序根目录下的\WEB­INF\classes\com\fatcat\webchart 子目录下,正确拷贝后的目录结构

如图 5.2 所示。

图 5.2 WelcomeServlet的目录结构

程序第 6 行~第 7 行:

import javax.servlet.*; import javax.servlet.http.*;

引入了 javax.servlet 和 javax.servlet.http包。 一般来说, Servlet 都需要使用这些包中的类。 javax.servlet.http包为 Servlet 提供了超类 HttpServlet,用来处理 HTTP get 请求和 post 请求。

该类实现 javax.servlet.Servlet 接口, 并添加了支持 HTTP 协议请求的方法。 所以程序第 10行:

public class WelcomeServlet extends HttpServlet

定义了类 WelcomeServlet 并继承了 HttpServlet 类,用来实现 Servlet 的功能。

超类 HttpServlet 提供了 doGet 方法来响应 get 请求。其默认功能是指出“Method not allowed”错误。在 Internet Explorer 中,这种错误一般表示为一个显示“This page can not be displayed”的Web页面,而在 Netscape Navigator中,则显示一个“Error:405”的Web页面。

程序第 13 行~第 45行,重载了 doGet 方法,提供了对 get 请求的处理。doGet 方法有两

个参数——1 个是 HttpServletRequest 对象和 1 个 HttpServletResponse对象。前面讨论过两者

都隶属于 javax.servlet.http包。 HttpServletRequest对象代表客户端的请求, Http ServletResponse 对象代表服务器对客户端的响应。如果 doGet 方法不能处理客户的请求,则抛出 javax.servlet.ServletException类型的异常。如果 doGet 方法在流处理过程中,即从客户端中读

取数据或向客户写数据的处理过程中发生错误,则会抛出 java.io.IO Exception异常。

为了演示对 get 请求的响应,本例所示的 Servlet 在接收客户端 get 请求后,就创建了一

个 XHTML文档,程序第 38 行:

out.println( "<h1>您好,<br>欢迎进入 Servlet图表编程的世界</h1>" );

该文档包含“您好,<br>欢迎进入 Servlet 图表编程的世界”这样一段文本,中间的这个

“<br>”在 HTML 文档中表示换行符。图 5.1②所示的结果可以看到,这段文本分成两行显

示了。该文本就是服务器对客户端的响应。该响应通过 PrintWriter 对象发送给客户,而 PrintWriter 对象是从 HttpServletResponse对象中获取的。

程序第 17 行:

response.setContentType( "text/html;charset=gb2312" );

webchart

WEB­INF classes

com fatcat

webchart WelcomeServelt.java

D:\

Web.xml

Page 153: Java Web动态图表编程

使用 response 对象的 setContenType 方法,设置返回给客户端响应数据内容的类型。客

户端浏览器则根据服务器返回文档类型来处理文档的内容。内容的类型也被称为数据的 MIME(Multipurpose Internet Mail Extension)类型。本例中,返回数据内容的类型为 text/html,

它是通知浏览器响应的一个 XHTML文档。客户端的浏览器就必须读取文档中的 XHTML标

记,并根据标记对文档进行格式化处理,然后将文档显示在浏览器中。

程序第 18 行:

PrintWriter out = response.getWriter();

调用 response 的 getWriter 方法,创建一个 PrintWriter 类的实例 out,通过 out 对象,就

可以向客户发送内容。注意前面我们说过,response 的 getWriter 方法获取的是一个基于字符

的输出流,用于将文本数据发送给客户端。所以,如果我们要发送二进制数据,如我们生成

的图表等,则应该使用 getOutputStream方法,获取 ServletOutputStream的对象引用。

程序第 23 行~第 42 行,通过调用 out 对象的 println 方法,将字符串写入该对象中,以

创建 XHTML文档。该方法在其 String参数后面输出一个换行符。注意,该换行符仅仅出现

在 XHTML的源文件中。 如果要在 XHTML的输出结果中输出一个换行符, 则需要在 XHTML 源文件相应的位置有一个“<br>”内容,如程序第 38 行所示。

程序第 43 行,关闭输出流,更新输出缓冲区并将信息发送给客户。整个请求/响应过程

就结束了。

最后,在运行本程序之前,还应该对本 Servlet 进行部署。 JSP/Servlet 容器中的配置文件(也就是所谓的部署描述符)web.xml 中部署这个 Servlet。

配置文件指定 Servlet 的各种配置参数,如:

Ø 调用 Servlet 的名称(即 Servlet 的别名)。

Ø Servlet 的描述。

Ø Servlet 完全合法的路径名及类名。

Ø Servlet 映射(即 JSP/Servlet 容器调用 Servlet 的 URL路径)。

将 web.xml(\chap05\Fig5.2\Fig.5.2_01)文档拷贝到 Tomcat 或 Resin的 Web 应用程序根

目录下的\WEB­INF\子目录下,现在打开 web.xml,我们可以看见第 1 行~第 5 行:

<?xml version="1.0" encoding="GB2312"?>

<!DOCTYPE web­app PUBLIC "­//Sun Microsystems, Inc.//DTD Web Application 2.2//EN" "http://java.sun.com/j2ee/dtds/web­app_2_2.dtd">

指定 Web 应用程序部署描述符的文档类型,以及 XML 文件 DTD 位置。表明这是一个

符合 XML标准的,编码方式为 GB2312 的 xml 文档。 XML的精髓是允许文档编写者制定基于信息描述、体现数据之间逻辑关系的自定义标记,

为确保文档具有较强的易读性、清晰的语义和易检索性。一个完全意义上的 xml 文档是必须遵

从XML语法的,也就是说,它必须遵守文档类型定义 DTD中已声明的规定。 DTD 定义了 XML文档的整体结构以及文档的语法。简而言之,DTD 规定了一个语法分

析器以解释“有效的”XML文档所需所有规则的细节。 DTD 实际上可以看做是一个或多个 XML 文件的模板,这些 XML 文件中的元素、元素

的属性、元素的排列方式/顺序、元素包含的内容等,都必须符合 DTD 中的定义。 DTD 可以是一个完全独立的文件,也可以在 XML文件中直接设定。所以,DTD 分为外

部 DTD(在 XML文件中调用另外已经编辑好的 DTD)和内部 DTD(在 XML文件中直接设

定 DTD)两种。

我们这里选择的是外部的 DTD 文档。外部 DTD 是一个独立于 XML 的文件,实际上也

Page 154: Java Web动态图表编程

是一个文本文件,只是使用.dtd为文件扩展名。外部 DTD 独立于 XML文件,它可以供多个 XML文件使用,就像用同一个模板可以写出多个不同内容的文件一样。

采用外部 DTD 的好处是,该 DTD 是经过众多公司或组织充分讨论后而形成的,语法严

谨,适用性和通用性强。所以我们采用的外部 DTD是由 Sun公司制定的,该 DTD 的存放位

置在:http://java.sun.com/j2ee/dtds/web­app_2_2.dtd DTD 的语法相当复杂,这里我们仅仅做一个简单的介绍。 web.xml 源文件中的第 7 行~第 22 行的元素“web­app”定义了 Web 应用程序中每个

Servlet 的配置信息,以及每个 Servlet 的 Servlet 映射。

第 9 行~第 13 行:

<servlet> <servlet­name>WelcomeServlet</servlet­name> <description>简单地处理 get请求的 Servlet</description> <servlet­class>com.fatcat.webchart.WelcomeServlet</servlet­class>

</servlet>

元素“servlet”描述 Servlet。

第 10 行的元素“servlet­name”为当前 Servlet 定义的一个名称。该 Servlet 的名称被定义

为“WelcomeServlet” 。

第 11 行的元素“description” ,用来描述当前 Servlet 的一个说明。

第 12 行的元素“servlet­class” ,用来指定当前被加载的 Servlet 完整的、合法的路径及类

的名称。Servlet 类的源程序或其编译后的.class 文件,默认地存放在 JSP/Servlet 容器的 WEB­INF/classes 子目录下。前面讲过,源程序 WelcomeServlet.java 第 4 行:

package com.fatcat.webchart;

使用了 package语句, 就表示 WelcomeServlet.java 编译后的.class 文件, 应该放在 Tomcat 或者 Resin的 Web 应用程序根目录下\WEB­INF\classes\com\fatcat\webchart 的子目录下。 这就

是为什么必须先在\WEB­INF\classes 下创建 com、fatcat 和 webchart 这几个子目录的原因,它

们正好一一对应于 package中 com.fatcat.webchart 的声明。 web.xml 源文件的第 15 行~第 18 行:

<servlet­mapping> <servlet­name>WelcomeServlet</servlet­name> <url­pattern>/welcome</url­pattern>

</servlet­mapping>

元素“servlet­mapping”指定 Servlet 的映射。第 16 行中的元素“servlet­name”指定被映

射的 Servlet名称。 该名称要与第 10行, 为当前 Servlet定义的名称相同, 所以这里 “servlet­ name”

的值也是 WelcomeServlet。第 17 行中的元素“url­pattern”指定 JSP/Servlet 容器调用 Servlet 的 URL路径。我们在这里定义 JSP/Servlet 容器调用 Servlet 的 URL路径映射为“/welcome” 。

当浏览器向 JSP/Servlet 服务器 URL路径为“/welcome”的 Servlet 发出“get”请求时, JSP/Servlet 服务器会将该请求转交给名字为“WelcomeServlet”的 Servlet 处理,因为

“WelcomeServlet”是别名,实际上最后会由\WEB­INF\classes\com\fatcat\webchart 子目录下

的 WelcomeServlet.class 所处理。

注意:这里我们在映射“URL”的时候,并没有指定 Servlet 所在服务器的地址及端口,

在这种情况下,JSP/Servlet 服务器会默认该 Servlet 位于本地机。

综上所述,在 web.xml 中部署某个 Servlet 的时候,首先声明 Servlet,指定这个 Servelt 的名字和完整的路径及类,然后映射这个 Servlet,最后通过这个映射来访问 Servlet。

经过一系列比较烦琐和复杂的配置后,我们将所有的文件都放置到正确的目录中,现在

Page 155: Java Web动态图表编程

就可以启动浏览器,在地址栏中输入: http://localhost:8080/chap05/Fig5.2/Fig.5.2_01/ WelcomeServlet.html。

就可以看到如图 5.1 所示的画面,单击“获得 HTML 文档”按钮,就可以得到如图 5.1 ②所示的“您好,欢迎进入 Servlet 图表编程世界”XHTML文档的结果。

提示: 如果读者在 Tomcat 的环境下运行本例程, 而并没有得到如图 5.1 所示的运行结果,

最简单的解决方法是:请先退出 Tomcat,这时我们在\WEB­INF\classes\com\fatcat\ webchart 中看见只有 WelcomeServlet.java 的源程序,说明这时 Tomcat 并没有生成相对应的 WelcomeServlet.class 文件。 现在再启动 Resin来运行本例, 可立即获得正确的结果。 退出 Resin 后,我们可以看到在 \WEB­INF\classes\com\fatcat\webchart 子目录下生成了所对应的 WelcomeServlet.class 文件。因此,这时再启动 Tomcat,因为已经创建了正确的 class 文件,

所以本例现在也能够正确运行了。另一个解决方法是:不借助 Resin 自动编译功能,而直接

在命令提示符下手动将 Servlet 源程序编译成 class 文件。如果希望手动编译 Servlet 源程序,

则需要将 Tomcat 安装目录下的\common\lib\servlet­api.jar 文件复制到 JDK 安装目录下的 \jre\lib\ext 子目录中。然后进入 Servlet 源程序所在的目录,在命令提示符下输入:javac WelcomeServlet.java 即可。在本书后续讲解的例程中,如果大家在 Tomcat 的环境下无法获得

正确的运行结果,则可以借鉴本例的解决方法。 ”

单击 IE 的【查看】→【源文件】,我们可以看到文档的源代码如图 5.3 所示。

图 5.3 WelcomeServlet返回的 HTML源代码

该源代码正是WelcomeServlet.java源程序第 23行~第 42行, 通过调用 out对象的 println 方法,响应客户端的“get”请求而返回的文本内容。

提示:如果读者运行本例程的时候,没有看到相同的结果,请首先检查文件是否按本例

程的介绍拷贝到正确的目录。 当确定文件都已经拷贝到正确的位置后, 仍无法得到正确结果,

请读者重新启动 Tomcat 服务器。因为在修改 web. xml 部署描述符后,必须重新启动 Tomcat 服务器,否则 Tomcat 将不能识别新的 Web 应用程序,而 Resin服务器则不需要重新启动。

实际上,我们不必通过图 5.1①中所示的 HTML文档来调用该 Servlet。只要启动 IE,在

浏览器中直接输入如下的 URL:http://localhost:8080/welcome,就可以调用该 Servlet 了,这

种方式同样向服务器发送了一个“get”请求——与通过图 5.1①中所示 HTML 文档调用该 Servlet 的方法完全一致。

通过本例介绍的配置 Servlet 方法,也同样适用于 Resin 服务器。我们在浏览器的地址栏

中输入 http://localhost:8888/chap05/Fig5.2/Fig.5.2_01/WelcomeServlet.html, 就可以看到如图 5.4 所示的运行结果。

Page 156: Java Web动态图表编程

图 5.4 Resin服务器下运行WelcomeServlet

我们可以在浏览器中直接输入如下的 URL 来访问该 Servelt:http://localhost:8888/ welcome,从而得到如图 5.4②所示的结果。

通过我们在 Tomcat 和 Resin 上的运行结果,读者可以看到本例所介绍的 Servlet 配置方

法其实并不麻烦。相反,本配置方法结构清晰,配置文件及 Servlet 源程序正确的存放位置也

非常容易掌握,不容易出错;而且在更换服务器后,任何配置文件都不需要修改,即可成功

运行。所以,我们建议读者在实际的工作中,可以采用本例程所讲述的方法来配置 Servlet,

这样会给我们的工作带来很大方便。

本书讨论的所有 Servlet 的配置和部署都将在现有的这个 web.xml 上进行。 我们将通过不

断地修改或更新这个 web.xml 来部署新的 Servlet。

掌握了第一个 Servlet 的编写及配置的方法后,让我们继续讨论如何利用 get 请求从一个 Servlet 获得图像。

利用 Servlet 来生成动态图表,基本思路如下:

(1)获得相关的图形或图像的绘制参数。

(2)调用 java.awt.image、java.awt 等 Java 图像类库进行绘制。

(3)将绘制完成的结果,以图像的格式(jpg/jpeg/gif/png)返回给发出请求的客户端。

这里我们先以本书第 2.10.4 节所示的绘制三角形(箭头)程序(Fig.2.10_04: Draw TriangleApplet.java)为蓝本,编写一个动态的、能随机生成方向的三角形 Servlet 实例。

当然, 在运行本例程之前, 需要将例程 Servlet的源程序 TriangleServlet.java拷贝到 Tomcat 或者 Resin的 Web 应用程序根目录下\WEB­INF\classes\com\fatcat\webchart 的子目录下面。

打开子目录中 web.xml 文档,我们可以看到如下的内容:

1: <?xml version="1.0" encoding="GB2312"?> 2: 3: <!DOCTYPE web­app PUBLIC 4: "­//Sun Microsystems, Inc.//DTD Web Application 2.2//EN" 5: "http://java.sun.com/j2ee/dtds/web­app_2_2.dtd"> 6: 7: <web­app> 8: 9: <servlet> 10: <servlet­name>TriangleServlet</servlet­name> 11: <description>绘制三角形的 Servlet</description> 12: <servlet­class>com.fatcat.webchart.TriangleServlet</servlet­class> 13: </servlet> 14: 15: <servlet­mapping> 16: <servlet­name>TriangleServlet</servlet­name> 17: <url­pattern>/drawTrigangle</url­pattern>

Page 157: Java Web动态图表编程

18: </servlet­mapping> 19: 20: </web­app>

复制本文档中第 9 行~第 18 行内容,然后再打开 Tomcat 或者 Resin的 Web 应用程序根

目录下的\WEB­INF\web.xml 文档,将刚刚复制的内容粘贴到\WEB­INF\web.xml 文档中的 <web­app></web­app>标记中。

现在我们只讨论调用生成三角形 Servlet的 TriangleSevlet.html文档的源程序的第 12行,

<form action = "/drawTrigangle" method = "get">

表单标记 form 的属性 action 值为“/drawTrigangle”,method 的值为“get”,说明向 URL为“/drawTrigangle”的 Servlet 发送 get 请求。

现在重新启动 Tomcat 服务器,运行 IE,在地址栏中输入:http://localhost:8080/chap05/ Fig5.2/Fig.5.2_02/TriangleServlet.html。

TriangleServlet.html 文档的运行结果如图 5.5 所示。

Page 158: Java Web动态图表编程

图 5.5 TriangleServlet.html的运行结果

单击“获得三角形(箭头) ”按钮后,我们可以得到如图 5.6 所示的运行结果。

图 5.6 TriangleServlet生成的随机三角形

刷新该页面,或者退回到 TriangleSevlet.html 再次调用该 Servlet,我们会得到一个新的、

随机产生的三角形,图 5.6 中的②、③、④是我们在刷新该页面后得到的结果。

现在来看看 TriangleServlet.java(chap05\Fig5.2\Fig.5.2_02)是如何实现动态绘制三角形

(箭头)功能的。

程序第 4 行:

package com.fatcat.webchart;

仍然使用了 package语句,说明本源程序严格地按照 Java Servlet 的规定,将源程序部署

在相应的目录中。我们把源程序 TriangleServlet.java 拷贝到 Tomcat 或者 Resin的 Web 应用程

序根目录下\WEB­INF\classes\com\fatcat\webchart 的子目录下面,正确拷贝后的目录结构如图 5.7 所示。

图 5.7 TriangleServlet Web应用程序的目录结构

程序第 6 行~第 7 行:

import javax.servlet.*; import javax.servlet.http.*;

引入了 javax.servlet 和 javax.servlet.http包。我们再次强调 Servle需要使用这些包中的某

些类。javax.servlet.http包为 Servlet 提供了超类 HttpServlet,用来处理 HTTP get 和 post 请求。

webchart

WEB­INF classes

com fatcat

webchart TriangleServlet.java

D:\

Web.xml

Page 159: Java Web动态图表编程

程序第 8 行~第 12 行:

import java.io.*; import java.util.*; import java.awt.image.*; import java.awt.*; import com.sun.image.codec.jpeg.*;

是我们熟悉的 Java 相关的输入输出类、工具类、图像类等,注意这里的第 12 行,我们

引进了一个新的 Java 包——com.sun.imgae.codec.jpeg.*。该包是 Java 用来处理 JPEG 的特殊

类,我们在这里使用该包用来处理内存图像的编码格式。

源程序共包含了 7 个方法:

Ø drawTrigangle Ø setBaseLine Ø setAlpha Ø setFillColor Ø setOutlineColor Ø doGet Ø doPost 其中增加 setAlpha、doGet、doPost 方法,但删除了 init 方法,分别执行了不同的任务。

原先 init 方法中实现的功能被移植到 doGet 方法中。我们重载了 doGet 方法,提供了对 get 请求的处理。当客户端发生 get 请求时,就会调用程序第 120 行~第 183 行的 doGet 方法。

再次强调, doGet方法有两个参数: 一个是HttpServletRequest对象, 另一个HttpServletResponse 对象,两者都隶属于 javax.servlet.http 包。HttpServletRequest 对象代表客户端的请求, HttpServletResponse对象代表服务器对客户端的响应。如果 doGet 方法不能处理客户的请求,

则抛出 javax.servlet.ServletException 类型的异常。如果 doGet 方法在流处理过程中,即从客

户端中读取数据或向客户写数据的处理过程中发生错误, 则会抛出 java.io. IOException异常。

重载后的 doGet 方法响应了对客户端的请求,在其方法体中分别按顺序又调用了:

Ø setBaseLine Ø setAlpha Ø setOutlineColor Ø setFillColor Ø drawTrigangle 这 5 个方法以完成绘制不同大小,不同方向,不同风格的三角形。drawTrigangle方法负

责进行计算三角形的 3 个顶点坐标,并根据随机生成的参数来决定三角形的填充颜色、三角

形轮廓的绘制颜色以及三角形的方向等信息来进行图像的绘制。setBaseLine负责设置等腰三

角形底边的长度。setAlpha 负责设置等腰三角形底角的度数。setFillColor 负责设置填充等腰

三角形颜色。setOutlineColor 负责设置描绘等腰三角形轮廓的颜色。

程序第 125行:

response.reset();

请读者注意, 我们在 doGet方法中的第 1条语句, 通过 response调用了 Response类的 reset 方法,该方法继承于 ServletResponse,用于清空 response 里缓冲区的内容。另外,在 doGet 或者是 doPost 的方法体中,第 1 条语句一定要调用 reset 方法。

程序第 128行:

response.setContentType("image/jpeg");

Page 160: Java Web动态图表编程

使用 response 对象的 setContenType 方法,设置返回给客户端响应数据内容的类型为图

像类,图像的格式为 jpeg 格式。我们讨论过,客户端的浏览器是根据服务器返回文档类型

(MIME)来处理文档内容的。本例中,返回数据内容的类型为 image/jpeg,它通知浏览器响

应一个格式为 jpeg的图像。于是,客户端浏览器就知道它必须读取该图像,然后将图像显示

在浏览器中。

如果没有程序第 125行 response的 reset 方法,而仅仅只有程序第 128 行 setConten Type 方法,即使我们在这里指明了返回的数据类型为 image/jpeg,可实际上,JSP/Servlet 服务器

默认的还是返回 text/html。 只有当执行到第 128 行 setContentType ( “image/jpeg” ) 时, response 缓冲区为空时,JSP/Servlet 服务器才返回 image/jpeg 的图像格式。

但是,无法确定 JSP/Servlet 服务器在执行到 response 的 setContentType( “image/ jpeg” )

方法时,response 缓冲区的内容为空。这样,即使生成的图片内容和格式都没有问题,但因

为返回的数据类型和数据的实际内容不一致,所以,在客户端上也就不会有正确地显示。

但某些 JSP/Servlet 服务器比较“聪明” ,如 Tomcat,如果碰见以下语句:

<%@ page contentType="image/jpeg" %> 或 response.setContentType("image/jpeg");

将会自动调用 response 的 reset 方法。即使没有在程序中显示的声明 response 的 reset 方

法,还是可以得到正确的输出结果。 Resin服务器在该问题上,不同的版本有着不同的输出结果。Resin的某些版本与 Tomcat

类似,也会自动调用 response的 reset 方法,而 Resin 的某些版本却非常忠于源程序,不会自

动调用 response的 reset 方法,所有输出结果就不正确。

同理, 经我们测试, BEA公司的 Weblogic服务器也不会自动调用 response的 reset 方法,

所以如果缺少了 response的 reset 方法,也不能得到正确的结果。

综上所述,我们强烈建议读者在编写 JSP/Servlet 的 Web 图表程序时,要在源程序中,首

先调用 response 的 reset 方法,强制清空 response 缓冲区的内容。这样,可以确保在所有的 JSP/Servlet 服务器上运行 Web 图表编程的源程序时,都可以获得正确的结果。

程序第 131行~第 136 行:

int width = 400, height = 300; BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);

// 得到图形环境对象 g Graphics g = image.getGraphics();

创建了一个宽度和高度分别为 400 像素和 300 像素的缓冲图像 BufferedImage 的对象 image。

程序第 138行~第 150 行,创建了两个 Color 数组。其中 oColor 数组代表生成的三角形

轮廓的描绘颜色,fColor 数组代表生成的三角形填充的颜色。它们将用于绘制三角形时随机

选取的轮廓描绘颜色和填充颜色。

程序第 153行~第 154 行:

g.setColor(Color.WHITE); g.fillRect(0, 0, width, height);

填充一个和缓冲图像宽度、高度都相同的白色矩形作为缓冲图像的背景图案。注意,在 JSP/Servlet 中,缓冲图像的默认背景及绘图颜色为黑色。

程序第 156行~第 158 行:绘制图像的标题。

g.setColor(Color.BLACK);

Page 161: Java Web动态图表编程

g.setFont(new Font("方正粗宋简体", Font.PLAIN, 30)); g.drawString("随机生成三角形­Servlet版", 10, 35);

程序第 160行~第 161 行:

int tempBaseLine = 100+(int)(Math.random() * 50); setBaseLine(tempBaseLine);

定义一个整型变量 tempBaseLine,并用一个 100~149 之间的随机数赋值,然后将其作

为参数传递并调用 setBaseLine方法。 程序会转到第 99 行~第 102行的 setBaseLine方法体中

运行:

public void setBaseLine(int baseLine)

this.baseLine = baseLine;

setBaseLine 方法很简单,仅仅是将用户在 setBaseLine 方法中传递整型参数 baseLine 的

值重新赋值给类的数据成员 baseLine。我们假设这时得到的随机数 baseline为 120,那么,类

的数据成员变量 baseLine 的值,即三角形底边长度就被设置为 120 像素了。接着程序返回 doGet 方法中,继续执行下面的语句。

程序第 163行~第 164 行:

int tempAlpha = 30+(int)(Math.random() * 30); setAlpha(tempAlpha);

定义一个整型变量 tempAlpha,并用一个 30~59 之间的随机数赋值,然后将其作为参数

传递并调用 setAlpha 方法。程序会转到第 104 行~第 107 行的 setAlpha 方法体中运行:

public void setAlpha(int alpha) this.alpha = alpha;

setAlpha 方法很简单,是将用户在 setAlpha 方法中传递的整型参数 alpha 值重新赋值给

类的数据成员 alpha。我们假设这时得到的随机数 alpha 为 45,类的数据成员变量 base Line 的值就改变为 45,即三角形底角就被设置为 45 度了。接着程序返回 doGet 方法中,继续执

行。

程序第 166行~第 170 行:

int tempOutlineColorIndex = (int)(Math.random() * 5); setOutlineColor(oColor[tempOutlineColorIndex]);

int tempfillColorIndex = (int)(Math.random() * 5); setFillColor(fColor[tempfillColorIndex]);

分别生成了两个 0~4 之间的随机数,并将其作为参数,分别调用了 setOutlineColor 和 setFillColor 方法,用于设定绘制三角形时所需的填充颜色和三角形轮廓的描绘颜色。

程序第 172行:

int tempDirection = 1+(int)(Math.random() * 4);

生成了 1 个 1~4 之间的随机数,该随机数用来表示程序第 22 行:

final int UP = 1, RIGHT = 2, DOWN = 3, LEFT = 4;

定义表示三角形方向的 4 个 final 变量之一。UP、RIGHT、DOWN 和 LEFT,代表三角

形的方向,分别表示如本书第 2 章中的图 2.38 所示的向上、向右、向下和向左的三角形。

程序第 173行:

Page 162: Java Web动态图表编程

int tempStyle = 1+(int)(Math.random() * 3);

生成了 1 个 1~3 之间的随机数,该随机数用来表示程序第 25 行:

final int OUTLINE = 1, FILL = 2, MIXED = 3;

定义表示三角形方向的 3 个 final 变量之一。整型变量 OUTLINE、FILL 和 MIXED,分

别代表三角形的绘制风格。分别表示为:

Ø OUTLINE:描绘三角形轮廓。

Ø FILL:填充实心三角形。

Ø MIXED:绘制一个带轮廓的实心三角形(即先填充一个实心三角形,然后再描绘其

轮廓)。

程序第 176行:

drawTrigangle(180, 150, tempDirection, tempStyle, g);

调用了 drawTrigangle 方法,完成绘制工作。在调用 drawTrigangle 方法的时候,传递 5 个参数。前面 4 个参数是整型参数,最后 1 个参数是由缓冲图像 image 的绘图环境 Graphics 对象 g。

前面 4 个参数的具体含义如下:

Ø 180,150:表示欲绘制三角形底边中点的坐标是(180,150)。

Ø tempDirection:表示该三角形的方向。

Ø tempStyle:表示该三角形的绘制风格。

在 drawTrigangle方法体内,首先运行的是第 34行:

int offset = (int)(baseLine / 2 * Math.tan(alpha * Math.PI / 180));

定义了一个局部的整型变量 offset。offset 是一个偏移量,它表示三角形顶点 C 到底边中

点的距离。该距离的计算,读者请参考图 2.38 所示的计算方法。

之后,程序接着执行第 37 行,根据传递的随机生成的第 3 个整型参数:tempDirection,

进入一个 switch多重选择语句:

switch (triangleDirection)

这里根据 tempDirection的值进行下一步的操作,即计算三角形 3个顶点的坐标。之后,

就会退出 switch语句。

程序又进入第 77 行,另外一个 switch语句:

switch (triangleStryle)

根据传递的随机生成的第 4 个整型参数 tempStyle进行三角形绘制风格的选择。

这段代码的处理,同我们在本书第 2.10.4 节所介绍程序的相关方法完全一致,请读者参

阅该部分的内容。

至此,缓冲图像的内容就全部绘制完成了,接下来,需要将该图像的内容输出到客户端

的浏览器中。

程序第 179行~第 182 行的代码完成了图像的输出工作:

ServletOutputStream out = response.getOutputStream(); JPEGImageEncoder encoder = JPEGCodec.createJPEGEncoder(out); encoder.encode(image); out.close();

先用 response的 getOutputStream 方法创建一个 ServletOutputStream对象 out,然后利用 com.sun.image.codec.jpeg包中的 JPEGImageEncoder 类来编码图像, 即生成一个 JPEG 格式的

Page 163: Java Web动态图表编程

图像。最后调用 ServletOutputStream对象 out 的 close 方法,关闭输出流并结束此 Servlet。程

序的运行结果如图 5.6 所示。

本例程中使用了 Sun的 JPEG 特殊包——com.sun.image.codec.jpeg.*。我们认为采用该包

编写的程序的通用性不够好,因为这些代码在 com.sun包中,不是核心 API的一部分,也不

是标准的扩展包,因此会影响代码的可移植性。虽然利用该包可以生成正确无误的图表,但

我们完全可以利用 J2SE 5.0 的新特性来对缓冲图像的内容进行解码及编码工作。这样,代码

会更简洁、性能更强大、移植性更好。

我们将在后面的例程中向读者介绍如何利用 J2SE 5.0 新特性来编写用于图表处理的 Servlet。

5.3 Servlet处理包含数据的HTTP get请求及解决中文乱码

在向 Web 服务器发送 get 请求、获取文档或资源时,经常需要同时提交一些数据。比如,

登录 Web 邮箱时,需要提交登录名和密码;参与一个网络调查/投票的时候,需要向服务器

提交所选择的内容。

在 get 请求中,参数以参数名/参数值成对的形式提交给 JSP/Servlet 服务器。这里我们先

探讨在 HTML 文档中如何向 JSP/Servlet 服务器提交参数,以及如何在 Servlet 中获取并处理

该参数。 GetDataServlet.html 文档(chap05\Fig5.3\Fig.5.3_01)的源程序的第 14 行~第 48 行,是

一个包含了两个表单对象的表单。

程序第 14 行,表单标记 form 的属性 action的值为“/getData”,method的值为“get”,

说明向 URL为“/getData”的 Servlet 发送 get 请求。

程序第 19 行:

<input name="name" type="text" size="40" />

生成了一个单行文本框的表单对象,对象的名字为“name” ,值为用户在该文本字段中

输入的内容,如图 5.8①所示。

程序第 25 行~第 38 行:

<input name="level" type="radio" value="初学者" checked="checked" /> 初级学习者,没有参加认证考试 <br /> <input name="level" type="radio" value="Sun认证移动应用程序开发员" /> Sun Certified Mobile Application Developer <br /> <input name="level" type="radio" value="Sun认证 Java程序员" /> Sun Certified Java Programmer <br /> <input name="level" type="radio" value="Sun认证 Java开发员" /> Sun Certified Java Developer <br /> <input name="level" type="radio" value="Sun认证 Java Web组件开发员" /> Sun Certified Java Web Component Developer <br /> <input name="level" type="radio" value="Sun认证 J2EE企业组件开发员" /> Sun Certified J2EE Business Component Developer<br /> <input name="level" type="radio" value="Sun认证企业构架师" /> Sun Certified Enterprise Architect<br />

Page 164: Java Web动态图表编程

图 5.8 GetDataServlet.html的运行结果

这段代码生成了一组共 7 个单选按钮,如图 5.8②所示。注意,这 7 个单选按钮名字都

是相同的,都叫“level” ,只是每个单选按钮的值不一样,但一组单选按钮一次只能选中一个

选项。注意,第 25 行中包含 checked=“checked”代码,将单选按钮 checked 的属性设置为

“checked” ,则该单选按钮在默认状况下,被设置为选中状态。

运行本例程之前,需要将本例程 Servlet 源程序 GetDataServlet.java 拷贝到 Tomcat 或者 Resin的 Web 应用程序根目录下\WEB­INF\classes\com\fatcat\webchart 的子目录下面。

复制本文档中第 9 行~第 18 行的内容:

<servlet> <servlet­name> GetDataServlet</servlet­name>

<description>提交数据的 HTTP get请求</description> <servlet­class>com.fatcat.webchart.GetDataServlet</servlet­class>

</servlet>

<servlet­mapping> <servlet­name> GetDataServlet</servlet­name> <url­pattern> /getData</url­pattern>

</servlet­mapping>

然后再打开 Tomcat 或者 Resin的 Web 应用程序根目录下的\WEB­INF\web.xml 文档,将

刚刚复制的内容粘贴到\WEB­INF\web.xml 文档中的<web­app></web­app>标记中。

重新启动 Tomcat,运行 IE,在地址栏内输入:

http://localhost:8080/chap05/Fig5.3/Fig.5.3_01/GetDataServlet.html

如图 5.8 所示的运行结果,在输入姓名的文本框中输入“唐桓”两个字,然后选中单选

按钮“Sun Certificated Java Programmer” ,单击【提交】按钮,以调用 GetDataServlet。当用

户按下【提交】按钮后,会将 input 元素(本例中的“name”和“level”两个表单元素)的

值作为请求的一部分放置在参数名/参数值对中,并提交给 JSP/Servlet 服务器。我们可以得

到如图 5.9 所示的结果。

图 5.9 GetData Servlet的运行结果

Page 165: Java Web动态图表编程

注意:图 5.9 所示 GetData 的 Servlet 运行结果中,浏览器地址栏以如下的方式:http:// localhost:8080/getData?name=%CC%C6%BB%B8&level=Sun%C8%CF%D6%A4Java%B3%CC %D0%F2%D4%B1&Submit=%CC%E1%BD%BB

将参数“name”和“level”及值传递给 JSP/Servlet 服务器。在浏览器地址栏的 URL 参

数中有一个“?”号。这里的“?”将提交的字符串参数,作为 get 请求的组成部分进行传递

数据与 get 请求中 URL 非数据部分区分开。参数名/参数值在 URL 中以“参数名=参数值”

的形式进行传递。如本例 URL中:

name=%CC%C6%BB%B8 level=Sun%C8%CF%D6%A4Java%B3%CC%D0%F2%D4%B1&Submit=%CC%E1%BD%BB

如果同时提交的参数不止一个,则参数之间用“&”符号相互分开。而且参数中内容都

被自动转换成 gb2312 的编码格式。

下面我们来看看 GetDataServlet.java(\chap05\Fig5.3\Fig.5.3_01)这个 Servlet 的源程序,并

讨论它是如何处理这些参数,并得到如图 5.9 所示的运行结果。

程序第 21 行~第 22行:

String tempName = request.getParameter("name"); String tempLevel = request.getParameter("level");

调用 request 的 getParameter 方法来获取参数的值。该方法需要传递一个参数的名称, getParameter 方法以字符串的形式返回该参数的值。如果请求的参数不存在,不同的 JSP/ Servlet 服务器会返回 null 或者一个内容为空的字符串。

如果参数具有多个值,则应调用 request 的 getParameterValues 方法,返回一个字符串数

组。另外,调用 request 的 getParameterNames 方法,将返回一个 Enumeration 对象,然后遍

历该 Enumeration中的所有元素,并取出各元素的值即可。 GetDataServlet与本章第 5.2节所讲的WelcomeServlet 两个 Servlet的运行流程基本相同。

除了通过调用 request 的 getParameter 方法来获取参数的值外,GetDataServlet.java 中还

提供了一个 convertToChinese的方法,该方法是用来转换字符串编码格式的。前面我们提到,

运行 GetDataServlet.html 页面的时候,在用户按下【提交】按钮后,会将 input 元素(本例中

的“name”和“level”两个表单元素)的值作为请求的一部分放置在参数名/参数值对中,并

附加在 URL后提交给 JSP/Servlet 服务器,参数中的内容被自动转换成 gb2312 的编码格式。

既然已经被浏览器自动将参数的内容转换成 gb2312的格式, 而且在 GetData Servlet.java 的第 19 行:

response.setContentType("text/html;charset=gb2312");

也设置了返回的数据编码方式为 gb2312。 通过 request的 getParameter方法获得参数值后,

那么在 Servlet 中,是否不用任何处理就可以正常显示中文了呢?实际情况却不是这样。为了

说明,这里我们必须进行一次字符串编码转换的工作,暂时修改 GetData Servlet.java 中的第 49 行:

out.println("<h1>您好," + name + "<br>您提交的信息如下:<br>");

更改为:

out.println("<h1>您好," + tempName + "<br>您提交的信息如下:<br>");

另外,将程序第 52 行:

out.println("您的 Java编程水平为:<br>" + level + "</h1>");

更改为:

Page 166: Java Web动态图表编程

out.println("您的 Java编程水平为:<br>" + tempLevel + "</h1>");

重新启动 Tomcat 及 IE 并运行 GetDataServlet.html 文档,同样输入如图 5.8所示的数据,

单击【提交】按钮,我们就得到如图 5.10 所示的运行结果。

可以看到, 在 GetDataServlet.html 输入并提交的参数 name的值 “唐桓” , 以及参数 level 的值“Sun认证 Java 程序员”并没有得到正确的输出。这是什么原因呢?

首先简单回顾一下字符集的编码历史。

任何数据最终都是以二进制的形式在计算机上表示,包括操作系统、应用软件及其开发

工具等,连屏幕上显示的一个字符也是如此。计算机发明的时候,所设计的键盘是以当时广

泛使用的机械式打字机键盘为蓝本。除去 0~9 这 10个数字键,以及 26个大小写字母,还有

另外一些特殊的按键,如空格键、回车键、换档键等,这些按键都用一个数字来表示。因为

这些键(符号)的数目远远低于 200,所以,一个字节所能表示的数字范围(2 8 =256)就足

以容纳所有的这些字符。实际上表示这些字符的数字字节最高位(bit)都为 0,也就是说,

这些数字都在 0~127之间。例如,字符 a 对应的数字为 97,字符 A对应的数字为 65,字符 z 对应的数字为 122,字符 Z 对应的数字为 90。这种字符与数字对应的编码固定下来后,这

套编码规则被称之为 ASCII码(美国国家标准信息交换码)。

图 5.10 取消字符串编码转换后的 GetData Servlet的运行结果

随着计算机逐渐在其他国家的应用和普及,许多国家都把本地的字符集引入计算机,大

大地扩展了计算机字符的范围。 一个字节所能表示的数字范围是不能容纳所有的中文汉字的,

我们国家将每一个中文字符都用两个字节的数字来表示, 原有的ASCII字符的编码保持不变,

仍用一个字节表示。为了将一个中文字符与两个 ASCII码字符相区别,中文字符的每个字节

最高位(bit)都为 1,中国大陆为每一个中文字符都指定了一个对应的数字,并作为标准的

编码固定下来,这套编码规则称为 GBK(国际码) 。后来又在 GBK 的基础上对更多的中文字

符(包括繁体)进行了编码,新的编码系统就是 GB2312,可见 GBK 是 GB2312的子集。使

用中文的国家和地区很多,同样的一个字符,如“中国”的“中”字,在中国大陆的编码是

十六进制的 D6D0,而在中国台湾的编码是十六进制的 A4A4,台湾地区对中文字符集的编码

规则称为 BIG5(大五码)。

这样就出现了一种现象,即同一个字符在不同的国家和地区很可能有着不同的编码

(ASCII码除外) 。随着世界各国的交往越来越密切,全球一体化的趋势越来越明显,不同的

国家和地区间交换越来越多的电子文档和数据,这种不统一的编码方式已经严重制约了国家

和地区间在计算机使用和技术方面的交流。

为了解决各个国家和地区使用本地化字符编码带来的不利影响,人们将全世界所有的符

号进行了统一编码,称之为 Unicode 编码,所有字符不再区分国家和地区,都是人类共有的

符号,如“中国”的“中”这个符号,在全世界的任何角落始终对应的都是一个十六进制的

数字 4e2d。如果所有的计算机系统都使用这种编码方式,在中国大陆的本地化系统中显示的

“中”这个符号,发送到其他国家的本地化系统中,显示的仍然是“中”这个符号。Unicode

Page 167: Java Web动态图表编程

编码的字符都占用两个字节, 也就是说全世界所有的字符个数不会超过 2的 16次方 (65 536)。

然而,目前 Unicode 一统天下的局面暂时还难以形成,在相当长的一段时期内,人们看

到的都是本地化字符编码与 Unicode 编码共存的景象。既然本地化字符编码与 Unicode 编码

共存,那就少不了涉及两者之间的转化问题,在 Java 中字符使用的都是 Unicode 编码,Java 技术在通过 Unicode 保证跨平台特性的前提下也支持了全扩展的本地平台字符集,我们的键

盘输入和显示输出都是采用本地编码。 Java中的字符采用的是Unicode编码, 每个字符都占用两个字节。 String类中的 get Bytes

方法,并不是简单地将字符串中的字节数据存放到一个字节数组中去,而是将 Unicode 码的

字符串中每个字符数字转换成该字符在指定的字符集下的数字,最后这些数字存放到一个字

节数组中返回。在 JDK 包中必须含有对应的字符集编码器的类才可以将 Unicode成功地转换

到本地字符集码。

为更好理解导致上述现象的原因, 我们重新编写了一个 Servlet 来测试接收参数的编码格

式。

我们在下载源码中向读者提供了一个输出字符 Unicode编码的 Servlet。 该 Servlet 的程序

名为——GetCharCodeServlet.java,位于下载源码的 chap05\Fig5.3\Fig.5.3_02 子目录中,将其

拷贝到 Web 应用程序根目录下的\WEB­INF\classes\com\fatcat\webchart 子目录下面。

打开位于下载源码根目录下的 chap05\Fig5.3\Fig.5.3_02 子目录中 web.xml 文档,并复制

本文档中的第 9 行~第 18 行的内容:

<servlet> <servlet­name> GetCharCodeServlet</servlet­name>

<description>获取提交数据字符的 Unicode编码</description> <servlet­class>com.fatcat.webchart. GetCharCodeServlet</servlet­

class> </servlet>

<servlet­mapping> <servlet­name> GetCharCodeServlet</servlet­name> <url­pattern> /getCharCode</url­pattern>

</servlet­mapping>

打开 Tomcat 或 Resin的 Web 应用程序根目录下的\WEB­INF\web.xml 文档,将刚刚复制

的内容粘贴到 \WEB­INF\web.xml 文档中的<web­app></web­app>标记中。因为 GetChar CodeServlet.html 文档(\chap05\Fig5.3\Fig.5.3_02)和刚刚介绍的 GetDataServlet. html 文档除

调用的 Servlet 的 URL不同外,其余完全相同,故在此略过。

重新启动 Tomcat,运行 IE,在地址栏内输入:http://localhost:8080/chap05/Fig5.3/Fig. 5.3_02/GetCharCodeServlet.html。

在图 5.8 所示的运行结果中,还是在输入姓名的文本框中输入“唐桓”两个字,然后在

选中单选按钮“Sun Certificated Java Programmer” ,单击【提交】按钮,以调用 GetChar CodeServlet,我们可以看见如图 5.11所示的结果。

运行 GetDataServlet.html 文档,输入如图 5.8 所示的数据,单击【提交】按钮,我们就

得到如图 5.11 所示的运行结果。

Page 168: Java Web动态图表编程

图 5.11 获取字符在编码转换前后各自的 Unicode代码

可以看到, 在程序输出的结果中, 字符串被重新编码前, 参数 name的值 “唐桓” 的 Unicode 编码为:

CC C6 BB B8

该码值其实是“唐桓”这个字符串的 gb2312 的编码值,并不是 Unicode编码值。

我们还可以在 GetDataServlet.html 单击 【提交】 按钮后, 发送到 JSP/Servlet 服务器的 URL 的 内 容 中 得 到 验 证 : http://localhost:8080/getData?name=%CC%C6%BB%B8& level= Sun%C8%CF%D6%A4Java%B3%CC%D0%F2%D4%B1&Submit=%CC%E1%BD% BB。

可以清楚地观察到,参数 name=%CC%C6%BB%B8,这里的 CC C6 BB B8 就是 Servlet 中,在字符串被重新编码前,输出的参数 name的值“唐桓”Unicode的编码,实际上,字符

串“唐桓”Unicode的编码是 5510 和 6853。显然,在 JSP/Servlet 服务器接收到该字符串后,

重新编码过程中发生了错误。也就是,JSP/Servlet 服务器(经我们测试 Tomcat 和 Resin,在

这里都出现相同的错误)接收到客户端 HTTP 请求及相关参数的信息后,“唐桓”这个字符

串并没有被正确地转换成 Unicode的编码,所以,最终出现了错误。

同理,参数 level 的值为“Sun 认证 Java 程序员” 。在 GetDataServlet.html 单击【提交】

按钮后,浏览器也自动将其编码成 gb2312 的格式,我们在 URL 的内容中可以看到: level=Sun%C8%CF%D6%A4Java%B3%CC%D0%F2%D4%B1

在同时包含有英文字符和中文字符的字符串中,英文字符在被转换成 gb2312 的编码前

后,字符的编码都一样,所以内容保持不变,而非英文字符(如:中文、日文、韩文等)在

转换后,URL中就显示该字符按照某种格式编码后所得到的码值。

实际上,在 JSP/Servlet 服务器中,响应客户端的 HTTP 请求并返回请求内容的过程中,

进行了两次编码的工作,有关字符编码的默认过程如下:

(1)获得当前按默认编码格式,即按照标准的西方英语国家字符集(ISO8859­1)编码

的响应内容。

(2)将当前所有的内容转换成按 Unicode编码的内容。

(3)将按 Unicode编码的内容转换成按照指定的字符集(编码格式)编码的内容。

了解这个过程后,我们来看本例中,客户端发送 get 请求后,JSP/Servlet 服务器是如何

工作的:

(1)获得参数 name,然后通过调用 request 的 getParameter 方法,获取参数 name 的值

——“唐桓” 。

(2)将字符串“唐桓”以 Unicode的方式,进行编码转换。

(3)最后重新编码成 gb2312 输出。

这里在第 2 步,也就是将获得的参数值“唐桓”转换成 Unicode 时,发生了错误。原因

是 JSP/Servlet 服务器认为接收的参数值“唐桓”是按照默认值进行编码的,也就是说,Java 虚拟机认为这时字符串“唐桓”是按 ISO8859­1 的方式进行编码。而实际情况下,在我们单

Page 169: Java Web动态图表编程

击【提交】按钮后,浏览器就自动将提交的参数值转换成 gb2312 格式编码的字符了,即在 JSP/Servlet 接收到这些参数之前,这些参数就已经被重新编码过一次了,已经不是默认的 ISO8859­1 编码的字符串了。当 JSP/Servlet 服务器接收到这些参数后,仍然是按照默认的步

骤和方法对这些参数进行再次编码操作,所以出错。

因为英文字符在不同的字符集转换中编码保持不变,所以,转换结果对包含有英文字符

的字符串来说,英语部分的字符是正确的,但其他字符就会出错。

为解决这个问题,我们只需要把接收到的参数再将其反编码成 ISO8859­1的格式就可以

了。这样,当 JSP/Servlet 服务器执行字符串编码转换的过程就不会出错了。源程序 GetDataServelt 和 GetCharCodeServlet 中的 convertTo8859 方法,就完成了这个功能,将以 gb2312 编码的字符串恢复成以 ISO8859­1 编码的字符串。

现在来看 GetCharCodeServlet.java 的源程序,我们就很清楚了。

程序第 21 行~第 22行:

String tempName = request.getParameter("name"); String tempLevel = request.getParameter("level");

获得客户端提交的参数。程序第 45 行~第 48 行,直接输出这些参数值及其每个字符的 Unicode码值:

out.println("字符串\"" + tempName + "\"的 Unicode编码为:&nbsp;"); out.println(getCharCode(tempName) + "<br>"); out.println("字符串\"" + tempLevel + "\"的 Unicode编码为:&nbsp;<br>"); out.println(getCharCode(tempLevel));

程序中通过调用 getCharCode 方法(第 92 行~第 100 行),获得某个字符的 Unicode 码

值:

public String getCharCode(String str)

String temp = ""; for (int i = 0; i < str.length(); i++) temp += Integer.toHexString((int)str.charAt(i)) + "&nbsp;"; return temp;

在该方法中,首先调用字符串的 charAt 方法,将字符串中的每个字符分离出来。注意,

在 charAt 方法前面加上了(int),表示将字符转换成其对应的整数值。然后,调用 Integer。

类的静态方法 toHexString,此方法接收一个整数,返回用字符串形式表示的该整数对应的十

六进制的值,这个值就是这个字符的 Unicode码值。

程序执行完第 48 行后,可以看到,接收到的字符串在没有被重新编码前,英文字符的

输出是正确的,而中文字符则显示为乱码。

程序第 53 行~第 54行,将获得的这些参数值重新按照 ISO8859­1 的标准进行编码:

String name = convertTo8859(tempName); String level = convertTo8859(tempLevel);

这里通过调用 convertTo8859 的方法(第 76 行~第 89 行):

public String convertTo8859(String strInput) try String tempStr = strInput; byte[] tempStrBytes = tempStr.getBytes("ISO8859­1"); String strOutput = new String(tempStrBytes);

Page 170: Java Web动态图表编程

return strOutput; catch (Exception e) return "";

在这段代码中,首先声明了一个 tempStr 的字符串。然后,声明了一个 tempStrBytes 的

字节数组。String 类的 getBytes 方法,接收一个指定的编码格式的参数,返回该字符串按指

定编码格式转换而得到的字节数组。本例,我们指定编码格式为“ISO­8859­1” ,表示返回字

符串 tempStr 以“ISO­8859­1”编码方式转换而得到的字节数组。注意,因为在字符的编码转

换过程中,可能会出现异常,所以必须将转换过程放置在 try…catch…方法块中。

将字符串的编码转换成 ISO­8859­1 格式后,程序在第 60 行~第 64 行:

out.println("<h1>编码后:<br>"); out.println("字符串\"" + name + "\"的 Unicode编码为:&nbsp;"); out.println(getCharCode(name) + "<br>"); out.println("字符串\"" + level + "\"的 Unicode编码为:&nbsp;<br>"); out.println(getCharCode(level));

再次输出这些转换编码后的参数值及其每个字符的 Unicode 码值,这次我们就可以得到

正确的输出结果。同理,level 字符串“Sun认证 Java程序员”中,英文字符“Sun” 、 “Java”

在编码转换前后,其 Unicode是没有变化的,分别是“53 75 6e”和“4a 61 76 61” 。

综上所述,在 JSP/Servlet 中如果出现中文显示错误的情况,基本上可采用本节介绍的方

法加以解决。但 Java对字符进行的编码转换操作是一个很底层的问题,所涉及的内容也很复

杂。这里我们仅仅给出了在 JSP/Servlet 中的解决方法,在其他一些 Java 应用程序中,如果出

现中文字符显示乱码的情况,需要读者具体情况具体分析。读者可以借鉴我们的思路来分析

问题,并找出问题的根源及其解决办法。

读者可以直接在浏览器的地址栏中输入如下内容,来发送 get 请求以直接调用这两个 Servlet:http://localhost:8080/getData?name=钟京馗&level=J2EE 企业级开发及应用,运行结

果如图 5.12 所示。

图 5.12 Tomcat服务器下直接在浏览器的地址栏中输入中文并提交数据的运行结果

或者输入:http://localhost:8888/getCharCode?name=钟京馗&level=J2EE 企业级开发及应

用,运行结果如图 5.13 所示。

Page 171: Java Web动态图表编程

图 5.13 Resin服务器下直接在浏览器的地址栏中输入中文并提交数据的运行结果

Page 172: Java Web动态图表编程

5.4 Servlet 处理 HTTP post 及包含数据的 post 请求

当我们采用 HTTP get 请求并提交一些数据到 JSP/Servlet 服务器的时候,数据的安全性

比较差,提交的信息被转换成 gb2312 的字符后附加在 URL里,一起传递给 JSP/Servlet 服务

器。任何人都可以在浏览器的地址栏中截取该信息,只要将该信息进行正确的解码转换,就

可以得到原始的文本。所以,get 请求适合在对数据的安全性要求较低的网络环境中运行。当

我们在网上购物、登录Web 电子邮件等需要提交用户名和密码等机密信息的时候,一般就采

用 post 请求。post 请求的作用与 get 几乎一样,但在发送请求并提交数据的同时,数据也一

并显示在 URL后,这样,安全性就比 get 请求好得多。 PostDataServlet.html 文档(chap05\Fig5.4\)源程序的第 19 行:

<form action = "/postData" method = "post">

表单标记 form的属性 action的值为“/postData”,method的值为“post”,说明向 URL 为“/postData”的 Servlet 发送 post 请求。PostDataServlet.html 文档的其余部分和我们介绍过

的 GetDataServlet.html 文档,以及 GetCharCodeServlet.html 文档完全一样,此处不再赘述。

运行结果如图 5.14 所示。

图 5.14 PostDataServlet.html的运行结果

然后,打开位于下载源码根目录下的 chap05\Fig5.4 子目录中 web.xml 文档,同样复制该

文档中的第 9 行~第 18 行的内容:

<servlet> <servlet­name>PostDataServlet</servlet­name>

<description>获取提交数据的 HTTP post 请求</description> <servlet­class>com.fatcat.webchart.PostDataServlet</servlet­class>

</servlet>

<servlet­mapping> <servlet­name>PostDataServlet</servlet­name> <url­pattern>/postData</url­pattern>

</servlet­mapping>

将复制的内容粘贴到\WEB­INF\web.xml 文档中的<web­app></web­app>标记中。 PostDataServlet.java 的源代码中第 13行~第 56 行的 doPost 方法:

protected void doPost(HttpServletRequest request, HttpServletResponse response)throws ServletException, IOException

// 发送 XHTML 格式的页面给客户端 response.setContentType("text/html;charset=gb2312");

Page 173: Java Web动态图表编程

String tempName = request.getParameter("name"); String tempLevel = request.getParameter("level");

PrintWriter out = response.getWriter();

// 开始生成 XHTML 文档 out.println("<?xml version = \"1.0\"?>");

out.println("<!DOCTYPE html PUBLIC \"­//W3C//DTD " + "XHTML 1.0 Strict//EN\"\"http://www.w3.org"+ "/TR/xhtml1/DTD/xhtml1­strict.dtd\">");

out.println("<html xmlns = \"http://www.w3.org/1999/xhtml\">");

// 生成文档的 head部分 out.println("<head>"); out.println("<title>处理客户端提交数据的 servlet</title>"); out.println("</head>");

// 生成文档的 body部分 out.println("<body>");

// 获得正确编码的中文字符串 String name = convertTo8859(tempName); if (name == null || name.equals("")) name = "Java爱好者"; out.println("<h1>您好," + name + "<br>您提交的信息如下:<br>");

String level = convertTo8859(tempLevel); out.println("您的 Java编程水平为:<br>" + level + "</h1>"); out.println("</body>");

// 结束 XHTML 文档 out.println("</html>"); out.close(); // 关闭输出流

该 doPost 方法所接收的参数与 doGet 方法相同。一个是表示客户请求的,实现了 Http ServletRequest 接口的对象;另一个是表示服务器响应的,实现了 HttpServletRequest 接口的

对象。与 doGet 方法一样,如果不能处理客户的请求,doPost 方法,抛出 Servlet Exception 异常;如果在流过程中出现问题,则抛出 IOException异常。

将 PostDataServlet.java 复制到 Web 应用程序根目录下的 \WEB­INF\classes\com\ fatcat\webchart 子目录下面。重新启动 Tomcat 服务器,启动 IE,在地址栏中输入如下 URL:

http://localhost:8080/chap05/Fig5.4/PostDataServlet.html

输入“肥猫”及选择“Sun Certified Java Web Component Developer” ,可以看到图 5.14 所示的运行结果,单击【提交】按钮,得到如图 5.15所示的结果。

图 5.15 PostData Servlet的运行结果

Page 174: Java Web动态图表编程

我们看到浏览器的地址栏里,提交的数据没有附在 URL 上,这在一定程度上保证了数

据的安全。

现在分析一下该源程序的工作流程:

单击【提交】按钮后,PostDataServlet.html 向 JSP/Servlet 发出了一个 post 请求。该请求

传递给 Servlet 后,因是一个 post 请求,所以,Servlet 会调用程序第 13 行~第 56 行的 doPost 方法来处理。

该方法与上一节例程所介绍的 doGet 方法完全一致,读者可以参考前例。

注意,程序第 59 行~第 63 行,同样也声明了一个 doGet 方法:

protected void doGet(HttpServletRequest request, HttpServletResponse response)throws ServletException, IOException

doPost(request, response);

该方法是调用了程序第 13 行~第 56 行的 doPost 方法,来响应客户端的 get 请求。这样 Servlet 就可以同时处理 get 和 post 请求。

本例之前所介绍的例程都没有提供 doPost 方法,所以无法响应 post 请求,可以按照上述

方法, 添加一个 doPost 的方法, 并在 doPost 方法中调用 doGet 方法就可以了, 添加方法如下:

protected void doPost(HttpServletRequest request, HttpServletResponse response)throws ServletException, IOException

doGet(request, response);

当客户端发出请求时,Servlet 并没有提供与之相应的响应时,Servlet 会返回给客户端一

个代码为“405”的错误,表示被请求的 Servlet 无法响应该请求。

以本例来说明,将 PostDataServlet.java 第 59 行~第 63 行的 doGet 方法,删掉或注释掉,

再将 PostDataServlet.html 中的第 19 行:

<form action = "/postData" method = "post">

将 method的属性值由“post”更改为“get” ,如下所示:

<form action = "/postData" method = "get">

重新启动 Tomcat,运行 PostDataServlet.html,我们可以看到,在 Tomcat 中提示的内容

是: HTTP Status 405 – HTTP method GET is not supported by this URL,如图 5.16 所示。

图 5.16 Tomcat下的代码为 405的错误提示

而在 Resin中的提示是:405 GET not supported,如图 5.17 所示。

Page 175: Java Web动态图表编程

图 5.17 Resin下的代码为 405的错误提示

下面几节,我们将通过下面的内容阐述如何利用 Servlet 来生成 Web 动态图表的一些实

例,如投票统计图、登录验证码、加载图像文件并生成新的图像等。

Page 176: Java Web动态图表编程

5.5 Servlet 生成Web 投票统计图

Web 投票是网站从其访问者那里获得反馈信息的一种方式。当网站的访问者针对某个问

题进行了投票后,可以看到整个投票结果,以及相关的统计信息。

这里,我们编写了一个模拟网站投票并生成投票结果的 Web 图表程序。本程序向访问者

调查在 Python、Java、C#、Perl 和 PHP 这五种编程语言中,哪种编程语言是其最熟悉的。在

实际的运行环境中, 网站投票的相关信息一般保存在数据库中。 因本书不涉及数据库的编程,

所以,我们考虑将投票的相关信息保存在一个静态类型的数组中。当用户单击【投票】按钮

后,就可以看到投票的结果。平常在页面上按“刷新”按钮时,将会再次提交相关请求及数

据。而本程序则会判断用户提交的数据是否是单击“刷新”按钮而产生的。如果是,将不会

更新数据。投票结果显示为一水平直方图,每种编程语言都将根据用户单击的数量而呈现出

相应的长度。同时,我们可以看到访问者提交数据的详情,如每种编程语言的投票数及其占

总投票数的比率。这样,我们可以直观地观察投票结果,如图 5.19 所示。

先复制 chap05\Fig5.5 子目录,web.xml 文档中的第 9 行~第 18 行的内容,将复制的内

容粘贴到\WEB­INF\web.xml 文档的<web­app></web­app>标记中。

<servlet> <servlet­name>Vote</servlet­name>

<description>模拟网站投票并生成结果的 Servlet</description> <servlet­class>com.fatcat.webchart.VoteServlet</servlet­class>

</servlet>

<servlet­mapping> <servlet­name>Vote</servlet­name> <url­pattern>/vote</url­pattern>

</servlet­mapping>

现在来看发送数据及请求的 VoteServlet.html 的源代码的第 12 行:

<form action = "/vote" method = "get">

发送 get 请求给 URL 为 vote 的 Servlet。程序第 16 行~第 20 行,定义了一组单选按钮

名字为“program” 。编程语言 Python、Java、C#、Perl、PHP 的参数值分别为 0~4。

将 VoteServlet.java(\chap05\Fig5.5) 复制到 Web 应用程序根目录下的 \WEB­INF\ classes\com\fatcat\webchart 子目录下面。现在我们看看其运行结果,如图 5.18 所示。

图 5.19 是多次单击【投票】按钮后的运行结果。

这里假设用户选择的是“Java”编程语言这个选项,并单击【投票】按钮,下面我们来

探讨 VoteServlet.java 程序的流程。

Page 177: Java Web动态图表编程

图 5.18 VoteServlet.html的运行结果 图 5.19 Vote Servlet的运行结果

程序第 17 行~第 20行:

static int[] voteCounter = 0, 0, 0, 0, 0

;

声明并初始化了一个静态的、全局的整型数组对象 voteCounter,并将其中每个元素的值

初始化为 0。 voteCounter 表示客户选择的相关编程语言的投票数。 注意, 这里变量 voteCounter 使用了关键字 static。这样,在并发的 HTTP 客户端访问中,当任何一个客户端向 Serlvet 提

交数据,并更新 voteCounter 中任意一个元素的值后,所有访问该 Servlet 的客户端均可获得 voteCounter 的最新数据。因为 voteCounter 是一个全局变量,所以,我们把它放在 doGet,以

及 doPost 的方法外面。该数组中的每个元素分别代表客户端点击的编程语言:Python、JAVA、 C#、Per,以及 PHP的次数,下标(索引值)分别为 0~4。

VoteServlet.java 里面共有两个方法: doGet 和 doPost。程序第 129行~第 133 行的 doPost 方法仅仅是调用 doGet 方法。

程序第 23 行~第 126 行, 定义了 doGet 方法。 doGet 方法实现了统计客户端提交的数据,

并根据这些数据绘制出相应的水平直方图,最后将绘图结果返回给客户端。前面介绍的生成

三角形的例程中使用了 Sun的 JPEG 特殊类 com.sun.image.codec.jpeg.*。 但我们认为采用该包

编写程序的通用性不够好,因为,这些代码在 com.sun 包中,不是核心 API的一部分,也不

是标准的扩展包。JPEGImageEncoder 是 Sun 用来实现 JRE 某些功能所使用的特殊的 class。

因此,如果对图像的编码采用 JPEGImageEncoder 类,在其他一些 JRE 上运行会发生异常,

影响代码的可移植性。

在本例中,我们利用 J2SE 5.0的新特性,来对缓冲图像的内容进行解码及编码工作。这

样代码会更简洁,性能更强大,移植性更好。程序在第 12 行:

import javax.imageio.*;

引入了 Java 对图像处理的新包。javax.imageio 包默认可以读入一个 GIF、PNG、JPEG 和 BMP 格式的图像(注:J2SE1.4 版中也提供了该包,但不支持对 BMP 格式图像的读写操

作),以及输出一个 PNG、JPEG和 BMP 格式的图像,并不支持输出 GIF 格式的图像。原因

是 GIF 格式的图像是有版权的,所以 Sun公司在其有关图像处理的包中,就只提供了对 GIF 格式图像的读入功能,也就是说,只提供了对 GIF 图像的解码功能,而没有提供对 GIF 格式

图像的输出(即 GIF 图像的编码)功能。本程序将使用该包中的 ImageIO 类,来将缓冲图像

按指定的图像格式进行编码操作。

程序第 27 行:

Page 178: Java Web动态图表编程

response.reset();

注意, 在输出图像时, 程序的第一个语句要调用 response的 reset 方法, 强制清空 response 缓冲区的内容。 这样, 可以保证在所有的 JSP/Servlet 服务器上运行Web 图表编程的源程序时,

都可以获得正确的结果。

程序第 30 行:

response.setContentType("image/png");

使用 response 对象的 setContenType 方法,设置返回给客户端响应数据内容的类型为图

像,图像的格式为 png格式。

程序第 33 行:

float totalCounter = 0;

声明了一个浮点变量 totalCounter,并将其值初始化为 0。变量 totalCounter 表示客户端

的用户单击【投票】按钮的总次数。

程序第 36 行:

String program = request.getParameter("program");

获得用户 HTTP get 请求中的参数 program。因为,我们选择的单选按钮是 Java,而其对

应的参数值为“1” ,所以,这时字符串 program的值就为“1”了。

程序第 39 行:

String accept = request.getHeader("Accept");

调用 request 的 getHeader 方法,获取页面头部信息。当正常浏览时,request.getHeader ( “Accept” )返回的内容为“ image/gif、 image/x­xbit­map、 image/jpeg、 image/pjpeg、 application/msword、*/*”的字符串。而刷新页面时,该方法返回字符串的值为“*/*” ,将该

值赋值给字符串变量 accept。

程序第 40 行~第 44行:

if (program != null && !accept.equals("*/*")) int programID = Integer.parseInt(program); voteCounter[programID]++;

本段代码的功能是, 判断字符串变量 proram是否为空, 以及客户端是否在刷新当前页面。

如果 proram不为空,而且客户端也没有刷新页面,则更新静态变量整形数组 voteCounter 中,

相对应的元素值。注意,如果客户端是通过按浏览器工具栏上的“刷新”按钮或者是按键盘

的“F5”键来刷新当前页面的时候,request.getHeader( “Accept” )返回的内容为“*/*” ,所

以,我们在 accept.equals( “*/*” )方法前,加了一个取反符“!” 。

程序第 42 行,将字符串 program的值转换成整数后,并赋值给整型变量 programID。因

此,现在 programID 的值为 1。因为,表示投票数的元素下标值和变量 programID 正好相等,

所以,在程序第 43 行,直接用 voteCounter[programID]++ 的方式更新数组 voteCounter 中相

应元素的值。现在元素 voteCounter[1]的值为 1,也就是说,代表 Java 投票数的那个元素的值

为 1,而其他元素的值为 0。

程序第 47 行~第 50行:

for (int i = 0; i < voteCounter.length; i++) totalCounter += voteCounter[i];

Page 179: Java Web动态图表编程

通过一个简单的循环,计算出所有编程语言的总投票数。

程序第 54 行~第 56行,创建了一个长度和宽度分别为 600 像素和 330 像素的缓冲图像

对象 image。程序第 59 行,创建了 image对象的绘图环境 g。

程序第 62 行~第 68行:

g.setColor(Color.YELLOW); g.fillRect(0, 0, width, height); g.setColor(Color.white);

g.fillRect(11, 35, 580, 280); g.setColor(Color.black); g.drawRect(10, 35, 580, 280);

这里先用黄色填充了整个图形。然后,在 Java 坐标系中的点(10,35)处,黄色的背景

上绘制了一个有黑色边框的白色矩形。该矩形的长度和宽度分别为 580 像素及 280像素。

程序第 70 行~第 72行绘制标题。程序执行完第 72行后的绘制结果如图 5.20①所示。

程序第 74 行~第 77行,创建了一个字符串数组对象 programName,表示 5 种不同编程

语言的名称。

Page 180: Java Web动态图表编程

图 5.20 Vote Servlet的运行过程

程序第 79 行~第 84行,创建了一个 Color 类的数组对象 color,分别表示 5 种编程语言

投票数的水平直方图的颜色。

程序第 86 行~第 88行:

g.setFont(new Font("黑体", Font.BOLD, 15)); g.drawString("编程语言", 25, 50); g.drawString("所占比例", 520, 300);

在坐标系中相应位置绘制了两条字符串“编程语言”和“所占比例” 。程序执行完第 72 行后的绘制结果如图 5.20②所示。

程序第 92 行~第 107行:

int scaleLine = 0;

for (int i = 0; i <= 400; i += 40) g.setColor(Color.black); g.drawString("" + scaleLine + "%", (i + 80), 300);

g.setColor(Color.lightGray); g.drawLine((i + 80), 65, (i + 80), 280);

scaleLine += 10;

g.setColor(Color.black); g.drawLine(80, 55, 80, 280); g.drawLine(80, 280, 550, 280);

为了更清晰地表现各种编程语言投票数占总投票数的比例,我们在坐标系相应位置用淡

灰色绘制了 10 条垂直的直线,每条直线依次表示到达该位置的投票数(用水平直方图表示)

正好占总投票数的 0%、10%、20%……90%、100%。然后,程序第 106 行和第 107行,在相

关位置绘制了两条黑色的直线。执行完第 107 行后的绘制结果如图 5.20③所示。

程序第 109行~第 120 行:

int drawWidth = 0;

for (int i = 0; i < programName.length; i++) g.setColor(color[i]); drawWidth = (int)(voteCounter[i] * 400 / totalCounter + 0.5); g.fill3DRect(80, 78+i * 40, drawWidth, 25, true); g.setColor(Color.black); g.drawString("" + voteCounter[i] + " 票数 (" + (int)(voteCounter[i] * 100 / totalCounter + 0.5) + "%)", 100+drawWidth, 95+i * 40);

Page 181: Java Web动态图表编程

g.drawString(programName[i], 40, 95+i * 40);

根据客户端的投票情况, 计算每种编程语言的投票数, 进而计算出其占总投票数的比例。

然后,根据比例绘制相应长度的一个实心矩形,并绘制出相关的投票信息。例如,客户端对

该种编程语言的投票数及其占总投票数的比例。在本例中,我们对数据做出了如下的处理:

Ø 水平直方图最长绘制长度为 400 像素;

Ø 客户端对某种编程语言的投票数占总投票数的百分比只计算到个位, 后面数值部分采

取四舍五入的方法。

程序第 109 行,定义了一个整型变量 drawWidth,它表示水平直方图(实心矩形)的绘

制宽度。程序第 114 行,利用如下算法:

(int)(voteCounter[i] * 400 / totalCounter + 0.5)

该算法实现了水平直方图(实心矩形)绘制宽度的计算。根据前面的假设,在程序第 111 行~第 120行的循环中,当循环变量 i的值不为 1 时,绘制实心矩形的长度为 0;当循环变量 i的值为 1时,计算得到实心矩形的绘制长度为 400像素,因为在 voteCounter 数组中,只有下

标为 1 的元素,即 voteCounter[1]的值为 1,其余元素的值都为 0,而总投票数也为 1,所以

最后的绘制宽度就是 400 像素。

程序第 117 行~第 118 行:

g.drawString("" + voteCounter[i] + " 票数 (" + (int)(voteCounter[i] * 100/ totalCounter + 0.5) + "%)", 100+drawWidth, 95+i * 40);

实现了绘制该编程语言的投票数及其占总票数的百分比的功能。

重复该循环,就绘制出了所有的图形,然后使用新的 ImageIO 类将其输出到客户端。

程序第 123行~第 125 行:

ServletOutputStream sos = response.getOutputStream(); ImageIO.write(image, "PNG", sos); sos.close();

先用 response的 getOutputStream方法,创建了一个 ServletOutputStream对象 sos,然后,

利用 javax.imageio包中的 ImageIO 类的静态 write方法来编码图像, 即生成一个 PNG格式的

图像。ImageIO 类的多种不同版本的 write方法,都用于对图像进行指定格式的编码或格式转

换操作。本例中,调用的 write方法,需要接收 3 个参数:

Ø 一个实现了 RenderedImage接口图像的绘图环境对象。该绘图环境可以从一个文件、

输入流或 URL 中获取图像的绘图环境得到。本例中,图像的绘图环境对象就是缓冲

图像 image。

Ø 一个指定了输出图像格式的字符串对象。本例中,指明图像的输出格式为“PNG”格

式。

Ø 一个实现了 ImageOutputStream 接口的输出流对象。本例中,实现该接口的对象就是 ServletOutputStream类的对象 sos。

最后,调用 ServletOutputStream对象 sos 的 close方法,关闭输出流并结束本 Servlet。

如果要输出其他格式的图像,以输出“JPG”格式图像为例。需要改动两个地方。一个

是程序第 30行:

response.setContentType("image/png");

修改为:

response.setContentType("image/jpeg");

Page 182: Java Web动态图表编程

另一个需要修改程序第 124 行:

ImageIO.write(image, "PNG", sos);

修改为:

ImageIO.write(image, "JPG", sos);

如果希望编码成“BMP”格式的图像,修改方法相同,此处不再赘述。

如果在生成图像/图表的同时,还需要处理客户端提交的中文参数,则需要修改程序第 30 行:

response.setContentType("image/png; charset=gb2312");

然后,使用前面例程介绍的编码转换方法进行处理就可以了。

按照我们假设的情况,当用户选择的是“Java”编程语言这个选项,并第一次单击【投

票】按钮后,本 Servlet 绘制出来的结果应该如图 5.21 所示。

单击工具栏上的“刷新”按钮或按键盘上的“F5”键,程序第 40 行~第 44行,将判断

出事件的发生,不会发生更新数据的情况。读者可以修改程序的判断语句,并观察其运行结

果。

Page 183: Java Web动态图表编程

图 5.21 第一次提交数据后的运行结果

当返回投票页面,再次单击【投票】按钮时,就可以看到数据更新了。图 5.19显示的结

果就是多次单击【投票】按钮后的运行结果。只要不重新启动计算机或 JSP/Servlet,服务器

就会保存所有的投票信息。如果重新启动计算机或 JSP/Servlet 服务器,投票信息将全部被重

置,并重新开始更新。

5.6 Servlet 生成登录验证码

登录验证码技术是一种有效防范利用穷举法或脚本, 通过在 Web 登录页面自动输入登录

名及登录密码来猜测及破解用户登录名及密码的技术。它通常是在用户进入 Web 登录页面

后,随机生成验证码,验证码是一幅包含一些数字或字母的图像。用户在登录的时候,必须

同时输入正确的登录名、登录密码以及验证码上所显示的数字或字母组合才可以通过身份校验。

对于一些具有自动填写登录信息的Web破解类程序来说, 验证码技术对它的防范是相当成功的。

在大多数情况下,这类自动注册程序并不能识别验证码中的字符。

登录验证码技术也是一种有效防范恶意抢注免费邮箱等行为的技术。利用与上述介绍的

类似 Web 自动注册机,如果没有登录验证码技术,可以申请到许多免费的资源,如免费的 Web 电子邮箱、一些实时通讯/聊天软件,如,ICQ、UC、QQ 等。对于这类恶意抢占资源的

行为,在没有使用验证码技术之前,是很难防范的。而采用该技术后,这类恶意抢注行为大

幅减少。

此外,如果不使用登录验证码技术,攻击者除了会使用自动注册程序,注册大量的 Web 服务账户外,还可以使用这些账户影响其他普通用户的正常使用,如发送垃圾邮件或通过反

复登录多个账户来降低服务器的响应速度等。

某些新的 Web 破解类程序,还具有智能识别验证码上所显示的数字或字母的功能,为了

降低验证码被程序自动识别的风险,在生成验证码时,一般采用随机生成一些干扰图像,以

增大 Web 破解类程序自动识别验证码的难度。

可见,登录验证码技术有助于确保普通注册用户而不是自动化程序在填写登录表单,这

是 Web 应用中非常重要的一项技术。

现在大量网站都采取了验证码这类技术。如:

Ø 新浪免费邮箱的注册页面(http://mail.sina.com.cn/cgi­bin/register/regMember1.cgi)。

Ø 腾讯 QQ 首页上的邮箱登录页面(http://www.qq.com/)。

Page 184: Java Web动态图表编程

Ø Hotmail 的 Web 邮箱的申请页面。

设计思路如下:

Ø 当客户端每次访问登录页面时,都会由一个 Servlet 生成一个新的验证码,并提供一

个供客户输入验证码的表单。 在生成验证码的同时, 将该验证码的内容保存在 Session 中。

Ø 用户输入验证码的内容后,单击“校验验证码”按钮后,会将提交的信息传递给另一

个 Servlet 处理。

Ø 该 Servlet 接收到用户提交的信息后,会将用户输入的验证码内容与保存在 Session中

验证码的内容相对比,根据校验结果,输出不同的内容。

我们将介绍两种应用上述设计思路,编写的生成登录验证码的方法。

5.6.1 Servlet 生成登录验证码实例 1

本节介绍的实例包含三个主要文件。第一个文件是登录页面(validate.html),第二个文

件是 Servlet,其中一个 Servlet(CodeMakerServlet.java)用于生成随机登录验证码,最后一

个文件是 Servlet(ValidateServlet.java)用于校验用户输入的验证码和随机生成的验证码是否

相同。本例演示了如何生成一个包含 4个数字的验证码,以及如何校验用户输入的数据是否

和验证码相同。

复制\chap05\Fig5.6\ Fig.5.6_01 子目录中 web.xml 文档中的第 9行~第 29行内容,然后

将复制的内容粘贴到\WEB­INF\web.xml 文档中<web­app></web­app>的标记中。

<servlet> <servlet­name>CodeMakerServlet</servlet­name>

<description>生成验证码的 Servlet</description> <servlet­class>com.fatcat.webchart.CodeMakerServlet</servlet­

class> </servlet>

<servlet­mapping> <servlet­name>CodeMakerServlet</servlet­name> <url­pattern>/codeMaker</url­pattern>

</servlet­mapping>

<servlet> <servlet­name>ValidateServlet</servlet­name>

<description>校验验证码的 Servlet</description> <servlet­class>com.fatcat.webchart.ValidateServlet</servlet­ class>

</servlet>

<servlet­mapping> <servlet­name>ValidateServlet</servlet­name> <url­pattern>/validate</url­pattern>

</servlet­mapping>

然后, 将下载源码根目录下的 chap05\Fig5.6\Fig.5.6_01 子目录中的两个 Servlet 源程 序, CodeMakerServlet.java 和 ValidateServlet.java 拷贝到 Web 应用程序根目录下的 WEB­ INF\com\fatcat\webchart 子目录下面。重新启动 Tomcat 服务器,运行 IE,在地址栏中输入: http:// localhost:8080/chap05/Fig5.6/Fig.5.6_01/validate.html。

我们可以看到图 5.22①所示的结果。

在 validate.html 页面中,验证码图片显示的字符为“6943” ,当客户在验证码下面的文本

框中也输入“6943” ,并单击【校验验证码】按钮,validate.html 会向 URL为/validate的名为 ValidateServlet 的 Servlet 发出 post 请求。

ValidateServlet 接收到该请求后, 程序会判断客户在文本框中输入内容和验证码图片中显

Page 185: Java Web动态图表编程

示字符的内容是否相同, 如果相同, 则返回如图 5.22②所示的页面。 反之, 则会返回如图 5.23 ②所示的页面,并提示用户重新输入。

图 5.22 验证成功后的显示页面 图 5.23 验证失败后的显示页面

关于 validate.html(\chap05\Fig5.6\Fig.5.6_01)代码,有三点需要说明:

(1)为了确保系统安全,我们设定当客户端每次访问 validate.html 页面时,都会从服务

器重新加载最新的验证码图像。为实现此功能,我们在服务器端和客户端分别采用了一段代

码,强迫浏览器不再缓存 Web 页面,该功能由程序第 7 行~第 10 行的代码实现:

<META HTTP­EQUIV="CONTENT­TYPE" CONTENT="text/html; charset=gb2312"> <META HTTP­EQUIV="Pragma" CONTENT="no­cache"> <META HTTP­EQUIV="Cache­Control" CONTENT="no­cache"> <META HTTP­EQUIV="Expires" CONTENT="0">

(2)同样地,为了保证用户提交数据的安全性,我们采用了 post 的请求方法,见程序

第 16 行。

(3)我们在程序的第 19 行,采用了 HTML加载图像文件的标准语法,如,<img src=” /codeMaker”/>来加载 Servelt 生成的验证码图像。因为,URL 为/codeMaker 的 Servlet,无论

是对 HTTP 的 get 请求还是 post 请求, 都返回一个图像文件, 因此, 我们在 HTML的元素 img 的属性 src后,直接给出 Servlet 的 URL即可。

单击【校验验证码】按钮后,由 ValidateServlet 进行数据校验。生成验证码图片的 Servlet 的源程序——CodeMakerServlet.java 中共有 4 个方法,如下所示:

(1)doGet 方法,负责响应客户端的 HTTP get 请求。方法体中,分别调用 drawCode和 drawNoise方法。

(2)doPost 方法,负责响应客户端的 HTTP post 请求,本例中,doPost 仅仅调用 doGet 方法来响应客户端的 post 请求。

(3)drawCode方法,负责绘制验证码。

(4)drawNoise方法,负责绘制干扰线。

程序第 16 行~第 22行:

private Font[] codeFont =

new Font("Algerian", Font.BOLD, 65), new Font("Vivaldi", Font.BOLD, 85), new Font("Broadway", Font.BOLD, 60), new Font("Forte", Font.BOLD, 75)

;

创建了包含 4 个 Font 对象的 Font 数组 codeFont,用于设置绘制验证码所需要的字体对

象。

Page 186: Java Web动态图表编程

程序第 24 行~第 27行:

private Color[] color =

Color.BLACK, Color.RED, Color.DARK_GRAY, Color.BLUE ;

创建了包含有 4 个 Color 对象的 Color 数组 color,用于设置绘制验证码所需要的颜色对

象。

程序第 29 行:

String codeNumbers = “”;

声明了字符串变量 codeNumbers,表示绘制完成的验证码内容。

程序第 31 行:声明了两个整型变量,width 和 height,表示生成验证码图像的宽度和高

度。

程序第 34 行~第 81行,重新定义了 doGet 方法,以响应客户端 get 请求。

程序第 38 行:调用 response的 reset 方法,强制清空 response缓冲区的内容。

程序第 41 行:

response.setContentType("image/png");

使用 response 对象的 setContenType 方法,设置返回给客户端响应数据内容的类型为图

像,图像的格式为 png。

前面讨论过,为了保证系统安全,我们设定当客户端每次访问 validate.html 页面时,都

应从服务器重新加载最新的验证码图像。为实现此功能,我们在服务器端和客户端分别采用

了一段代码,强迫浏览器不再缓存Web 页面。程序第 44 行~第 46行的代码,在服务器端实

现了该功能:

response.setHeader("Pragma", "No­cache"); response.setHeader("Cache­Control", "no­cache"); response.setDateHeader("Expires", 0);

程序第 49 行~第 50 行,创建一个长度和宽度分别为 250 像素和 70 像素的缓冲图像对

象 image。

程序第 53 行,创建了 image对象的绘图环境 g。

程序第 56 行~第 57行:

g.setColor(Color.YELLOW); g.fillRect(0, 0, width, height);

用黄色填充整个图形,使其作为图像的背景色。

程序第 59 行~第 62行:

for (int i = 0; i < 4; i++)

drawCode(g, i);

在 for 循环体中,4 次调用了 drawCode 方法,用来绘制验证码,每调用一次 drawCode,

就会跳转执行程序第 84 行~第 92 行的 drawCode 方法,并在缓冲图像的相关位置绘制一个

数字字符。

public void drawCode(Graphics graphics, int i)

int number = (int)(Math.random() * 10); graphics.setFont(codeFont[i]); graphics.setColor(color[i]);

Page 187: Java Web动态图表编程

graphics.drawString("" + number, 10 + i * 60, 60);

codeNumbers += number;

drawCode 方法接收两个参数,一个是 Graphics 对象 graphics,另一个是整型变量 i。程

序第 86 行,定义了一个局部整型变量 number,number 的值是从一个 0~9 之间的随机数中

产生。然后,程序设置当前绘制图像的字体对象(程序第 87 行),以及颜色对象(程序第 88 行)。这里的字体对象和颜色对象都是从我们前面定义的字体对象数组 codeFont,以及颜色对

象数组 color 中获取的。程序第 89行,调用 drawString方法,在当前缓冲图像的绘图环境中,

绘制刚计算出的随机数。注意,绘制随机数时,绘制参数中横坐标的值与 drawCode 方法传

递的整型参数 i 有关,纵坐标的值总是保持 60 不变。

程序第 91 行,更新字符串 codeNumbers 内容。一旦调用了 drawCode方法,code Number 就会将本次 drawCode方法, 产生随机数的内容附加在其内容中。 程序第 92 行, 结束 drawCode 方法。

程序第 59 行~第 62行,循环结束后,在当前缓冲图像的绘图环境中,总共绘制了 4个

数字字符,同时,字符串 codeNumber 内容就是 4 个数字字符的内容。

验证码绘制完成后,继续绘制干扰线。前面我们讨论过,一些 Web破解类程序具有智能

识别验证码上所显示数字或字母的功能,为了降低验证码被程序自动识别的风险,在生成验

证码时,通常还要随机生成一些干扰图像,以增大 Web 破解类程序自动识别验证码的难度。

程序第 64 行:

drawNoise(g, 30);

调用 drawNoise方法绘制干扰线, 就是为了达到这个目的。 drawNoise方法接收两个参数,

一个是 Graphics 对象 graphics,另一个是整型变量 lineNumber。lineNumber 表示要绘制干扰

线的数目,该值越大,则绘制在验证码上的干扰线就越多,干扰效果就越好,但过多的干扰

线会影响普通用户的识别。

这里我们设置 lineNumber 值为 30,即绘制 30 条干扰线。干扰线的实际效果如图 5.22①

以及图 5.22②所示。程序第 95行~第 106行 drawNoise方法,完成该干扰线的绘制工作:

public void drawNoise(Graphics graphics, int lineNumber)

graphics.setColor(Color.YELLOW); for (int i = 0; i < lineNumber; i++)

int pointX1 = 1 + (int)(Math.random() * width); int pointY1 = 1 + (int)(Math.random() * height); int pointX2 = 1 + (int)(Math.random() * width); int pointY2 = 1 + (int)(Math.random() * height); graphics.drawLine(pointX1, pointY1, pointX2, pointY2);

drawNoise 方法定义了 4 个局部的整型变量,并分别用随机数对其赋值。这 4 个变量分

别代表干扰线两个端点的横坐标和纵坐标。 利用这 4个变量, 调用 drawLine方法, 用黄色 (程

序第 97 行)在当前绘图环境中绘制 30条直线。

程序第 67 行~第 68行:

g.setColor(Color.black); g.drawRect(0, 0, width ­ 1, height ­ 1);

在当前绘图环境中绘制一个黑色的空心矩形,实现了验证码的边框效果。

前面的设计思路中提到,在生成验证码的同时,将验证码的内容保存在 Session 中。那

Page 188: Java Web动态图表编程

么什么是 Session?为什么要使用 Session?我们这里做一个简单的回顾。

众所周知,Web 应用程序协议被分成两大类别,无状态(stateless)和有状态(stateful);

协议状态指的是它 “记忆” 从一个传输到下一个传输信息的能力。 Web 应用程序依赖于 HTTP (属于无状态协议),它不能在各个网页之间存储用户的数据。这导致了在各个 Web 应用程

序中间也无法保存及共享数据。本例中,验证码的生成是由 CodeMakerServlet 生成的,但判

断用户输入和由 CodeMakerServlet 生成的验证码内容是否相同却是由 Validate Servlet 所负

责。ValidateServlet 是如何获得 CodeMakerServlet 所生成验证码的内容呢?也就是说,我们需

要一种可以在 Web 应用程序(JSP/Servlet)以及网页中“维持”状态、 “记忆”及“传递”数

据的机制。

现在有许多方法可以在 HTTP 上模拟有状态连接。一般有以下三种解决方法:

(1)Cookie。利用 HTTP Cookie 来存储有关客户端和服务器之间的会话信息,后继的

各个连接可以查看当前会话,从服务器提取有关该会话的完整信息。这是一种应用最为广泛

的方法。

(2)改写 URL。可以把一些标识会话的数据附加到每个 URL后面,服务器能够把该会

话标识和它所保存的会话数据关联起来。在浏览器不支持 Cookie 或用户禁用 Cookie 的情况

下也同样有效。然而,大部分使用 Cookie时所面临的问题同样存在,即服务器端的程序要进

行许多简单且单调冗长的处理。另外,还必须保证每个 URL后面都附加了必要的信息(包括

非直接的,如通过 Location 给出的重定向 URL)。如果用户结束会话之后又通过书签返回,

则会话信息将会丢失。

(3)隐藏表单域。HTML 表单中可以包含下面的输入域:<INPUT TYPE="HIDDEN" NAME="session" VALUE="...">。 当表单被提交时, 隐藏域的名字和数据也被包含到GET或 POST 数据里,我们可以利用这一机制来维持会话信息。然而,这种方法有一个很大的缺点,它要求所

有页面都是动态生成的,因为整个问题的核心就是每个会话都要有惟一的标识符。 Java Servlet 提供了一种与众不同的方案——HttpSession。HttpSession是一个基于 Cookie

或者 URL 改写机制的高级会话状态跟踪接口。如果浏览器支持 Cookie,则使用 Cookie;如

果浏览器不支持 Cookie或者 Cookie功能被关闭,则自动使用 URL改写方法。Servlet 开发者

无须关心细节问题,也无须直接处理 Cookie或附加到 URL后面的信息,HttpSession自动为 Servlet 开发者提供一个可以方便存储会话信息的机制。

HttpSession 类隶属于 javax.servlet.http 包。每个客户端和服务器联机,都有一个对应的 Session 对象。也就是说,如果有 N 个用户同时请求该 Servlet,则 JSP/Servelt 服务器会有 N 个分别对应于每个客户端的 Session对象。因为,Session对象和客户端对象是绑定在一起的。

所以,如果将数据保存在 Session 对象中,客户端在联机过程中,在该客户端所请求的 Web 页面(JSP/HTML)以及 Servlet 应用程序中,就可以存取这些数据,从而达到跨网页分享数

据的目的。这就解决了我们刚才提到的 ValidateServlet 如何获得 CodeMaker Servlet 所生成验

证码内容的问题。现在我们来看本例中,JSP/Servlet 服务器是如何实现 Session与联机客户端

一一对应的。

当有多个客户端联机到 JSP/Servlet 服务器(调用 validate.html 页面),并发出 get/post 请

求给 CodeMakerServlet 以获取验证码图像时(validate.html 程序第 19 行),CodeMaker Servlet 为联机的客户端通过调用 HttpServletRequest 的 getSession方法,自动创建一个会话对象,如

程序第 71 行所示:

HttpSession session = request.getSession(true);

如果该客户端并没有被绑定 Session 对象,getSession 方法有可能返回 null。我们给 getSession 方法指定了一个布尔值为真“true”的参数,指定如果 getSession 方法返回结果为 null,则 CodeMakerServlet 为该客户端创建一个 Session 对象,将一个用于识别该 Session 对

Page 189: Java Web动态图表编程

象的 sessionID 写入客户端的 Cookie中,以便 Web 应用程序识别该对象。也就是说,当我们

使用 Session对象的时候,客户端浏览器不能关闭 Cookie功能。

程序第 72 行:

session. setAttribute("codeNumbers", codeNumbers);

在 Java Servlet API 2.1 版或者更早版本的 Servlet API中,保存数据对象的使用方法是 putValue。而查看以前保存数据对象则使用 getValue 方法。getValue返回 Object,因此,必须

把它转换成具体的数据类型。如果 Session中指定的数据对象不存在,getValue返回 null。

在 Java Servlet API API 2.2 版及其以后的版本中,Sun推荐使用 setAttribute方法来代替 putValue方法保存数据对象。同样,推荐使用 getAttribute方法来代替 getValue方法,获得以

前保存数据对象。 getAttribute返回Object, 因此, 必须把它转换成具体的数据类型。 如果Session 中指定的数据对象不存在,getAttribute返回 null。

这里 setAttribute方法接收两个参数。第一个参数是字符串变量“codeNumbers” ,表示和

该 Session属性名字为“codeNumbers” ;第二个参数是指定和属性名字为“code Numbers”绑

定的数据对象。这里我们将字符串对象 codeNumbers 和该 Session 相绑定。注意,Session 可

以绑定 Java 中任何合法的数据对象,不管是基本的数据类型对象,还是某个类(包括 Java 自带的类,以及开发人员自定义的类)的实例,都可以保存在该 Session对象中。

当客户端每次发出 get 或 post 请求时,该客户端 sessionID 也会同时传递给 JSP/Servlet 服务器。JSP/Servelt 服务器以此 sessionID 来查找保存于服务器上的 Session 对象,如果找到

与该 sessionID 值相同的 Session 对象,则将客户端和该 Session 对象联系起来。如此,就可

以读取、更新、删除,以及保存在该 Session中的数据对象了。

这就是 JSP/Servlet 服务器通过 Session实现与联机客户端一一对应的这种机制。

程序第 75 行:

codeNumbers = "";

重新设置字符串变量 codeNumbers 内容,将其内容设置为空。重复该循环,绘制所有的

图形。

现在使用新的 ImageIO 类将其输出到客户端。

程序第 78 行~第 80行:

ServletOutputStream sos = response.getOutputStream(); ImageIO.write(image, "PNG", sos); sos.close();

这段代码同前面一个例程完全相同。 也是先用 response的 getOutputStream方法创建一个 ServletOutputStream对象 sos,然后,利用 javax.imageio包中的 ImageIO 类的静态 write方法

来编码图像,即生成一个 PNG 格式的图像。最后,调用 ServletOutputStream对象 sos 的 close 方法,关闭输出流并结束本 Servlet。

这里有两点需要向读者说明:

(1)Session 对象不可能无限期地,无限制地保存在服务器上。如果没有这个限制,任

何高性能的服务器都可能会在很短的时间内崩溃,因为 Session 对象的创建、修改、更新、

查找、跟踪和删除都需要消耗服务器资源。由于WWW 无状态联机的特性,所以无法确定客

户端什么时候完成诸如网页浏览、登录、发出 get/post 请求,也无法确定客户端什么时候脱

机。因此,也就无法确定 Session对象何时应该被注销,只有当某个 Session对象被注销后,

其所占用的系统资源才会被释放,才能被服务器所使用。为解决此问题,一般在 JSP/Servlet 服务器中,对每个 Session 对象都设置了一个默认的、有效的生命周期。Resin 服务器默认 Session的生命周期是 30分钟。当某个联机客户端在创建 Session对象后,在 Session默认的、

Page 190: Java Web动态图表编程

有效的生命周期内没有向 JSP/Servlet 服务器发出任何 get 或 post 请求,则该客户端就被认为

是处于脱机状态,与该客户端相对应的 Session对象也就自动被 JSP/Servlet 服务器所注销。

(2) 前面介绍过, 使用 Cookie也可以实现跨网页的数据分享, 那么为什么要使用 Session 对象呢?我们只要看看 Cookie的缺点就清楚了:

Ø Cookie只能保存文本数据(即字符串对象),无法保存其他类型的数据对象。

Ø 要使用 Cookie,必须要有客户端浏览器的支持。

Ø Cookie保存在客户端,数据的大小受限制。

Ø Cookie在数据的存取上不方便。

Ø Cookie是以明文的方式保存在客户端的, 因此, 在一些对安全性要求比较高的环境下, Cookie就无法满足安全性的要求。

对于Session对象, 除了要求客户端浏览器必须支持Cookie外, 其他Cookie的缺点Session 都没有。

那么如何设置 Session的生命周期呢?先来看 Tomcat 中的设置方法。

打开 Tomcat 安装目录下 conf/server.xml 文件,在<host></host>标记中增加以下内容:

<session­config> <session­timeout>60</session­timeout>

</session­config>

这里<session­timout>标记就是设置 Session的生命周期, 60 表示 Session对象的生命周期

为 60 分钟,该数字是以分钟为计时单位。

同理, 在 Resin中设置 Session的生命周期的方法是: 修改 Resin安装目录 conf/resin. conf 文件,在 resin.conf文件<web­app></web­app>标记中增加以下内容:

<session­config> <!­­ 2 hour timeout ­­> <session­timeout>120</session­timeout>

</session­config>

<session­timout>标记中的数字,同样也是以分钟为计时单位。这里设置 Session 的生命

周期为 120分钟。

生成验证码的 Servlet(CodeMakerServlet.java)我们就介绍完了,下面讨论用于校验用

户输入的验证码与随机生成的验证码内容是否相同。该功能由 ValidateServlet.java 程序实现。 ValidateServlet.java 的源程序中共有两个方法,如下所示:

Ø doGet 方法:负责响应客户端 HTTP get 请求。本例中,doGet 仅仅调用 doPost 方法来

响应客户端 get 请求。

Ø doPost 方法:负责响应客户端 HTTP post 请求,用来校验保存在 Session中的验证码

内容是否与用户输入的验证码内容相同,并根据校验结果输出不同的内容。

同样,为了保证系统安全,我们在服务器端使用程序第 20 行~第 22 行的代码,强迫浏

览器不再缓存 Web 页面。

程序第 25 行:

String validateCode = request.getParameter("validateCode");

获得客户端提交的 vaildateCode参数值。

程序第 28 行~第 29行:

HttpSession session = request.getSession(); String codeNumbers = (String)session.getAttribute("codeNumbers");

获得先前保存的 Session 对象“codeNumbers” 。这里使用的是 Session 类的 getAttribute 方法,来获得先前保存的属性名字为“codeNumbers”的 Session对象绑定在一起的数据对象。

Page 191: Java Web动态图表编程

getAttribute 方法需要接收一个字符串参数,该字符串表示 Session 的属性名字。如果指定属

性名字的 Session对象不存在,则返回 null 对象。如果指定属性名字的 Session对象存在,则

返回一个 Object 对象。我们必须手动转换或指定该 Object 对象的类型。

因为,在生成验证码图片的 Servlet 中,我们已经将一个字符串对象( “codeNumbers” )

和该 Session绑定(见 CodeMakerServlet.java 源程序第 72 行)。所以,程序第 29 行,使用了

“ (String) ”关键字,强制将返回的 Object 对象转换成 String 对象,然后,使用转换后得到

的 String对象来初始化字符串对象 codeNumbers。

程序第 33 行~第 36行:

if (codeNumbers == null)

response.sendRedirect(url); return ;

这段代码实现的功能是:如果客户端没有通过 validate.html 验证页面而直接访问本 Servlet(ValidateServlet.java),则将浏览器重新导向到指定的页面去处理。如果客户端没有通

过 validate.html 验证页面而直接访问本 Servlet,则此 Servlet 在执行到程序第 28行~第 29行

语句时,getAttribute方法返回对象为 null,此时没有数据对象和该 Session相绑定。

如果 codeNumbers 对象为 null,则会调用 response的 sendRedirect 方法,将浏览器重新导

向到指定的页面去处理。这里指定处理页面的 URL 就是“ /chap05/Fig5.6/Fig.5.6_01 /validate.html” (见程序第 31行)。因此,如果客户端的浏览器第一次直接访问 Servlet,就会

被 Servlet 重新导向到 JSP/Servlet 服务器根目录下\chap05\Fig5.6\Fig.5.6_01\ validate. html 页

面。程序的其他部分,读者都很熟悉了。现在介绍这个 Servlet 第 59 行~第 69行的核心代码

部分:

if (codeNumbers.equals(validateCode))

out.println( "<h1><font color=\"green\">输入相同,校验成功</font></h1> ");

else

out.println("<h1><font color=\"red\">输入错误,校验失败</font> <br>");

out.println("<p>请重新<a href=\"" + url + "\">输入验证码</a><h1> </p>");

这里调用字符串 codeNumbers 的 equals 方法,比较它和字符串 validateCode的内容是否

相同。如果相同,则返回真值,并输出如图 5.22②所示的结果;否则,返回假值,并输出如

图 5.23②所示的结果。

5.6.2 Servlet 生成登录验证码实例 2

前例所讲述的生成验证码图像 Servlet 有个缺陷。 如果我们希望更改验证码图像中数字字

符的字体、色彩、大小等属性,就必须修改源程序,然后,重新编译这个 Servlet 才可以看到

新的执行效果。如果我们对执行效果不满意,就要重复上述过程,这是比较麻烦的。

怎样才可以在不修改源程序的情况下,任意设定验证码的显示效果呢?我们的思路是:

生成随机数后,加载相应的数字字符图像文件,最后,将所有加载的数字字符图像文件合成

为一个验证码图像后再返回给客户端。如果要更改验证码图像中数字字符的显示效果,则只

需要替换数字字符的图像文件即可。这样就达到了在不修改源程序的情况下生成个性化的、

Page 192: Java Web动态图表编程

丰富多彩的验证码图像的目的。

复制 chap05\Fig5.6\ Fig.5.6_02 子目录中 web.xml 文档第 9 行~第 18 行的内容:

<servlet> <servlet­name>ImageCodeMakerServlet</servlet­name>

<description>加载图像文件并生成验证码的 Servlet</description> <servlet­class>com.fatcat.webchart.ImageCodeMakerServlet</servlet

­class> </servlet>

<servlet­mapping> <servlet­name>ImageCodeMakerServlet</servlet­name> <url­pattern>/imageCodeMaker</url­pattern>

</servlet­mapping>

并粘贴到\WEB­INF\web.xml 文档中<web­app></web­app>标记中。

在读者下载的源码根目录下 chap05\Fig5.6\Fig.5.6_02 的子目录中还可以看到一个 images 子目录,其下面是 10 个 gif 图像文件,分别表示数字字符 0~9。两个 Servlet 源程序: ImageCodeMakerServlet.java 和 ValidateServlet.java。同样,也有一个 validate.html 文档,该 HTML文档作用和前例相同。

这里我们暂时使用上节介绍的校验 Servlet。现在只拷贝 ImageCodeMakerServlet.java 到 Web 应用程序根目录下的\WEB­INF\com\fatcat\webchart 子目录下。然后,将 images 整个目

录拷贝到 Web 应用程序根目录下面(本书的路径为:d:\webchart\)。

这里的 vaildate.html 的源程序与上节介绍的 validate.html 相比, 只是程序的第 19行不同:

<img src="/imageCodeMaker" /><br />

获取实时生成的验证码图像的 Servlet 不同而已。

现在观察程序的运行结果,重新启动 Tomcat 服务器运行 IE,在其地址栏中输入:http:// localhost:8080/chap05/Fig5.6/Fig.5.6_02/validate.html。

可以看到如图 5.24①所示的结果。

图 5.24 验证成功后的显示页面

客户端输入验证码的校验过程同上节讲述的完全一样,我们在此略过。该验证码图片实

际上由 Web 应用程序根目录下 images 子目录下的 3.gif、9.gif、7.gif和 6.gif图像文件合成而

成。下面讨论本例中的图片效果是如何实现的。

生成该验证码的 Servlet(chap05\Fig5.6\Fig.5.6_02\ImageCodeMakerServlet.java)的源程

序与上节介绍的 Servlet 结构几乎相同,不同的只是在程序第 71 行~第 88 行的 drawCode方

法:

public void drawCode(Graphics graphics, int i, HttpServletRequest request)

Page 193: Java Web动态图表编程

int number = (int)(Math.random() * 10); String imageFilePath = request.getRealPath("\\images\\" + number +

".gif");File imageFile = new File(imageFilePath); Image gifFile = null; try

gifFile = ImageIO.read(imageFile); catch (Exception e)

System.out.println(e); graphics.drawImage(gifFile, i* 60, 0, null);

codeNumbers += number;

首先,方法接收的参数不同。本例中 drawCode接收 3 个参数,第 1、2 个参数与前例相

同,但第 3 个参数接收一个 HttpServletRequest 的对象。该对象就是代表客户端请求的 request 对象。

程序第 73 行,定义一个局部的整型变量 number,number 值是从一个 0~9 之间的随机

数中产生。这里我们以图 5.24①所示的验证码为例,来探讨本方法的运行过程。第一次调用 drawCode方法时,计算出来的随机数的值为 3,然后,将该值赋值给变量 number。

所以,程序第 74 行:

String imageFilePath = request.getRealPath("\\images\\" + number + ".gif");

实际执行的是:

String imageFilePath = request.getRealPath("\\images\\3.gif");

先调用 response的 getRealPath方法,该方法返回一个字符串对象,表示其参数中指定文

件的实际路径。也就是说,返回的是 Web 应用程序根目录下“\images\3.gif”文件的实际路

径。 这里之所以要使用 “\\” , 是因为 “\” 在 Java 中是作为转义字符的。 所以, 这里 “\\images\\3.gif”

经过编译器解释后就表示字符串“\image\3.gif” 。

在本机上,该实际路径返回的字符串内容是“d:\webchart\images\3.gif”这样一个完整的

路径。 注意, 如果是在 UNIX/Linux 下, 返回结果类似于 “Web 应用程序根目录/images/3.gif” ,

斜杠的方向和 Windows 正好相反。Java 可以做出正确的处理,我们只需要给出正确的参数就

可以了。之后,将返回内容赋值给字符串变量 imageFilePath。

程序第 75 行:

File imageFile = new File(imageFilePath);

创建了一个 File对象 imageFile,并用 File的构造器对其进行初始化。这里 File构造器接

收的参数就是刚刚获得的 imageFilePath,也就是 3.gif 这个图像文件的完整路径。

程序第 76 行:

Image gifFile = null;

声明了一个 Image 类的对象 gifFile,gifFile 用来表示我们将要加载的文件。Image 类隶

属于 java.awt 包。

程序第 79 行:

gifFile = ImageIO.read(imageFile);

Page 194: Java Web动态图表编程

这里再次利用 javax.imageio包中的 ImageIO 类的静态 read方法,获得一个 Image对象。 ImageIO 类的静态 read方法,返回一个 BufferedImaged类的对象。因为,Buffered Imaged类

是 Image 类的子类,所以,可以将 ImageIO 类的静态 read 方法返回的 Buffered Imaged 类的

对象,直接赋值给 Image对象 gifFile。因为,在加载图像的过程中,可能会发生异常,所以,

将该方法放置在 try…catch…中。

程序第 85 行:

graphics.drawImage(gifFile, i* 60, 0, null);

直接调用drawImage方法, 在当前缓冲图像的绘图环境中绘制刚刚加载的3.gif图像文件。

注意,绘制该图像时,绘制参数中横坐标的值与 drawCode方法传递的整型参数 i 有关,而纵

坐标总是保持0不变。这里的“i* 60” ,表示下一次绘制图像时的横坐标向右再移动 60 像素。

为什么要再移动 60 像素呢?读者可以使用 ACDSee 之类的软件,查看/images 目录下 gif 图

像文件,其中 2.gif和 4.gif两个图像的宽度最大,都为 60 像素。所以,我们这里定义两次绘

图之间的横坐标相差 60 像素。而 gif 图像的高度都相同,都为 60 像素。这也是我们为什么

将验证码图像的宽度设置为 240 像素, 高度为 60像素的原因(程序第 18 行)。

注意,在 JSP/Servlet中调用Graphics类的 drawImage方法时,最后一个参数定义为 null。

程序第 87 行,更新字符串 codeNumbers 的内容。一旦调用 drawCode方法,code Number 就会将本次 drawCode 方法所产生的随机数内容,附加在其内容之中。程序第 88 行,结束 drawCode方法。

这样,程序第一次循环,调用 drawCode方法时,就在当前绘图环境中坐标为(0,0)处,

绘制了图像 3.gif。同理:

Ø 第二次循环,就在当前的绘图环境中坐标为(60,0)处,绘制图像 9.gif;

Ø 第三次循环,就在当前的绘图环境中坐标为(120,0)处,绘制图像 7.gif;

Ø 第四次循环,就在当前的绘图环境中坐标为(180,0)处,绘制图像 6.gif。

与前例不同的地方是在程序第 51 行:

// drawNoise(g, 30);

将 drawNoise方法,也就是绘制干扰线的方法注释掉,最终生成的验证码图像上就没有干扰

线(如图 5.24①所示)。最后,将图像输出到客户端,就完成了生成验证码的全过程。

如果希望生成其他风格的验证码, 只需要将另外风格的数字字符文件覆盖到 Web 应用程

序根目录下 images\子目录中就可以了。需要注意以下两点:

(1)图像文件的格式为 gif;

(2)图像文件的宽度及高度都不超过 60 像素。

如此,我们就不必改变源代码而得到不同风格的验证码。

当前,我们调用校验验证码的 Servlet 与前例完全一样。如果用户输入的数据与验证码内

容相同,则可以看到如图 5.24②所示的结果。如果两者内容不相同,当点击如图 5.23②所示

“输入验证码”的超链接的时候, validate Servlet 会将页面重新链接到: http:// localhost:8080/chap05/Fig5.6/Fig.5.6_01/validate.html , 而 没 有 导 向 如 下 地 址 : http:// localhost:8080/ chap05/Fig5.6/Fig.5.6_02/ validate.html。

如何让 Servlet 在用户输入错误的情况下,重新导向到各自不同的 validate.htm 页面呢?

其实也很简单,我们只需要在两个文档中做些小小的修改即可。

(1)修改 chap05\Fig5.6\Fig.5.6_02\下的 validate.html 文档。

在该文档第 25 行后,增加一行代码即可:

<input type="hidden" name="digits" value="digits"/>

Page 195: Java Web动态图表编程

在 validate.html 文档中的表单区域增加一个隐藏的表单对象,该表单对象的名称和值均

为“digits” 。在运行本文档的时候。如果客户端单击【校验验证码】按钮时,该隐藏的表单

对象同时提交给相应的 Servlet。

(2)修改\WEB­INF\com\fatcat\webchart\ValidateServlet.java,将程序第 67 行~第 68 行

的代码:

out.println("<p>请重新<a href=\"" + url +"\">输入验证码</a><h1></p>");

更改为以下内容即可:

String url2 = "/chap05/Fig5.6/Fig.5.6_02/validate.html"; String flag = request.getParameter("digits"); if ( flag == null)

out.println("<p>请重新<a href=\"" + url + "\">输入验证码</a><h1> </p>"); else

out.println("<p>请重新<a href=\"" + url2 + "\">输入验证码</a><h1> </p>");

这里新增了两个字符串变量 url2 和 flag。 flag从客户端 request 请求中获取 “digits” 参数。

只有 chap05\Fig5.6\Fig.5.6_02\下 validate.html 文档才会提交这个参数。因此,如果客户端输

入的数据和验证码不符,我们就在这里做简单的判断,如果 flag为 null,则说明当前 request 请求来自:http://localhost:8080/chap05/Fig5.6/Fig.5.6_01/validate.html,则将页面重新链接到

上 述 地 址 。 反 之 , 如 果 flag 不 为 null , 则 说 明 当 前 request 请 求 来 自 : http://localhost:8080/chap05/Fig5.6/Fig.5.6_02/validate.html,则将页面重新链接到该地址即可。

重新启动 JSP/Servlet 服务器,现在 vaildate Servlet 就可以正确处理来自不同页面的校验

请求了。如果用户输入与验证码内容不符,则会重新链接到发出该 request 请求填写验证信息

的页面。

Page 196: Java Web动态图表编程

5.7 Servlet 高级设置

通过前面的例程我们介绍了关于 Servlet 的设置。本节将介绍一些新的设置方法。 Servlet 的设置包含除前面介绍过的 Servlet 完整路径及类名、别名、描述、映射外,还包

括初始化参数、启动加载优先级等。

5.7.1 Servlet 初始化参数

Servlet 可以配置一些初始化参数,并在 Servlet 中通过 javax.servlet.ServletConfig 接口中

的方法来获取这些参数。表 5.4 列出了该接口中的几个常用方法。

表 5.4 ServletConfig接口的部分方法

方 法 说 明

public String getInitParameter( String name) 返回指定名字的 Servlet 初始化参数

public Enumeration

getInitParameterNames() 返回一个 Enumeration对象,包含所有的 Servlet 初始化参数的名字

public String getServletName()

返回当前 Servlet 实例的名字。该名字可能由服务器管理员命名,也可由

Web应用程序的部署描述符提供,如果当前 Servlet 实例没有被命名,则返

回该 Servlet 的类名

public ServletContext

getServletContext()

返回 Servlet 上下文对象的引用。它用来定义一个 Servlet 的环境对象。

也可以认为是多个客户端共享的信息。 它与 session的区别在于应用范围不

同,session只对应于一个用户

我们以本章第 5.5 节介绍的投票统计的 Servlet 来说明如何配置及获取初始化参数。在前

面介绍的投票统计的 Servlet 中,编程语言 Python、Java、C#、Perl、PHP 的点击数的初始值

被设置为 0,现在我们在 Servlet 中,设定 Python、Java、CSharp(即 C#)、Perl、PHP 的点

击数的初始值为 10。用户的点击数在设定初始值的基础上进行增加。 Servlet 的初始化参数是保存在 web.xml 中, 通过 ServletConfig接口中的 getInit Parameter

方法,获得这些初始化参数。下面来看看具体的例子。

复制 chap05\Fig5.7\下的 web.html 文档中的第 9 行~第 38 行的内容,然后,将复制的内

容粘贴到\WEB­INF\web.xml 文档的<web­app></web­app>标记中。

<servlet> <servlet­name>InitVote</servlet­name>

<description>调用初始化参数的网站投票 Servlet</description> <servlet­class>com.fatcat.webchart.InitVoteServlet</servlet­ class>

<init­param> <param­name>Python</param­name> <param­value>10</param­value>

</init­param> <init­param>

<param­name>Java</param­name> <param­value>10</param­value>

</init­param> <init­param>

<param­name>CSharp</param­name> <param­value>10</param­value>

</init­param> <init­param>

<param­name>Perl</param­name> <param­value>10</param­value>

Page 197: Java Web动态图表编程

</init­param> <init­param>

<param­name>PHP</param­name> <param­value>10</param­value>

</init­param> </servlet>

<servlet­mapping> <servlet­name>InitVote</servlet­name> <url­pattern>/initVote</url­pattern>

</servlet­mapping>

我们可以看到,每一个初始化的参数都是放置在<servlet></servlet>的标记中。

<servlet> ... <init­param>

<param­name>Python</param­name> <param­value>10</param­value>

</init­param> ...

<\servlet>

而每一个初始化参数的标记是包含在<init­param></init­param>标记及其之间的两个元

素。<param­name>以及<param­value>标记,分别表示参数的名称和值。在上述 web.xml 文件

中,我们指定 Python、Java、CSharp、Perl 和 PHP 参数的参数值为 10。

获取初始化参数的 Servlet 源程序(chap05\Fig5.7\InitVoteServlet.java)增加了一个 init 方

法(程序第 23 行~第 30 行),以获取保存在 web.xml 中的初始化参数。我们在第 5.1 节中介

绍过 Servlet 的运行机制,Servlet 部署在 JSP/Servlet 容器内,其生命周期由 JSP/Servlet 容器

管理,同 Applet 的生命周期由 Applet 的容器——浏览器管理相同:在创建一个 Servlet 实例

后,就调用 Servlet 的 init 方法。

public void init()throws ServletException

voteCounter[0] = Integer.parseInt(getInitParameter("Python")); voteCounter[1] = Integer.parseInt(getInitParameter("Java")); voteCounter[2] = Integer.parseInt(getInitParameter("CSharp")); voteCounter[3] = Integer.parseInt(getInitParameter("Perl")); voteCounter[4] = Integer.parseInt(getInitParameter("PHP"));

在上面 init 方法中,获得 web.xml 中的初始化参数的方法是 getInitParameter 方法。该方

法接收一个字符串变量,表示初始化参数的名字,返回结果是一个字符串,表示该初始化参

数的值。程序的其余部分和第 5.5 节的 Servlet 完全相同。

同样,只要不重新启动计算机或 JSP/Servlet 服务器,服务器就会保存所有的投票信息。

如果重新启动计算机或 JSP/Servlet 服务器, 投票信息将全部被重置, 并重新获取初始化参数,

然后,再更新点击数。

调用本 Servlet 的 HTML 文档 InitVoteServlet.html(chap05\Fig5.7\)和第 5.5 节的 VoteServlet.html 文档,除在程序第 12 行被更改为以下内容:

<form action = "/initVote" method = "get">

其余部分也完全相同。

图 5.25 是启动 IE,运行 http:// chap05/Fig5.7/InitVoteServlet.html,不选择任何单选项,

直接单击【投票】按钮后的运行结果。我们可以看到,5 种语言的投票点击数的初始值都被

设置为 10。说明本 Servlet 已经正确加载了所有的初始化参数。

Page 198: Java Web动态图表编程

图 5.25 获取初始化参数的 Servlet

5.7.2 Servlet 加载优先级

Servlet 加载优先级是通过 web.xml 文件来配置的。我们可以在 web.xml 文件中的 <servlet></server>标记中增加一个<load­on­startup></load­on­startup>属性。如下所示:

<servlet> <servlet­name>CodeMakerServlet</servlet­name> <description>生成认证码的 Servlet</description> <servlet­class>com.fatcat.webchart.CodeMakerServlet</servlet­ class> <load­on­startup>20</load­on­startup>

</servlet>

<servlet­mapping> <servlet­name>CodeMakerServlet</servlet­name> <url­pattern>/codeMaker</url­pattern>

</servlet­mapping>

<servlet> <servlet­name>ValidateServlet</servlet­name> <description>校验认证码的 Servlet</description> <servlet­class>com.fatcat.webchart.ValidateServlet</servlet­ class> <load­on­startup>10</load­on­startup>

</servlet>

<servlet­mapping> <servlet­name>ValidateServlet</servlet­name> <url­pattern>/validate</url­pattern>

</servlet­mapping>

<servlet> <servlet­name>ImageCodeMakerServlet</servlet­name> <description>加载图像文件并生成验证码的 Servlet</description> <servlet­class>com.fatcat.webchart.ImageCodeMakerServlet</

servlet­class> <load­on­startup>AnyTime</load­on­startup>

<servlet­mapping> <servlet­name>ImageCodeMakerServlet</servlet­name> <url­pattern>/imageCodeMaker</url­pattern>

</servlet­mapping>

属性<load­on­startup></load­on­startup>中,我们分别提供了 3 个不同的值:10、20 和 AnyTime。 这里设定, ValidateServlet在CodeMakerServlet之前先被加载。 属性<load­on­ startup> 的值越小,则说明该 Servlet 的加载优先级越高,也就是说,被 JSP/Servlet 服务器加载的时间

Page 199: Java Web动态图表编程

就越早。 如果说该属性值为 AnyTime, 则说明该 Servlet 可以在服务器启动后的任意时间加载。

5.7.3 Servlet 映射

前面所介绍的每个例程中,我们都对 Servlet 进行了设置。每一个 Servlet 都对应了一个

映射。一个 Servlet 可以设置多个映射。这样就可以通过不同的 URL来访问同一个 Servlet。

我们以本章第 5.1 节所介绍的WelcomeServlet 为例,讨论如何为其设定多个映射。

<servlet> <servlet­name>WelcomeServlet</servlet­name> <description>简单的、处理 get请求的 Servlet</description> <servlet­class>com.fatcat.webchart.WelcomeServlet</servlet­

class> </servlet>

<servlet­mapping> <servlet­name>WelcomeServlet</servlet­name> <url­pattern>/welcome</url­pattern>

</servlet­mapping> <servlet­mapping>

<servlet­name>WelcomeServlet</servlet­name> <url­pattern>/welcome/*</url­pattern>

</servlet­mapping> <servlet­mapping>

<servlet­name>WelcomeServlet</servlet­name> <url­pattern>/fatcat/welcome</url­pattern>

</servlet­mapping> <servlet­mapping>

<servlet­name>WelcomeServlet</servlet­name> <url­pattern>/welcome.jsp</url­pattern>

</servlet­mapping>

通过上述的配置,我们就可以按照<url­pattern>属性中指定的 URL 来访问 Welcome Servlet。下面的访问方式都是正确的:

Ø http://localhost:8080/welcome/msadfaasdfasd Ø http://localhost:8080/welcome/welcome&words=fatcat Ø http://localhost:8080/welcome/test.html Ø http://localhost:8080/welcome/weclome.jsp?name=fatcat 也就是说,只要访问的 URL是以“/welcome/”开头,都可以正确地访问到这个 Servlet。

5.8 Servlet 绘制甘特图

甘特图又称为“Gantt Chart” ,是美国人甘特在 20 世纪 20 年代率先提出的。如图 5.26 所示。它与我们前面介绍的水平直方图相类似,是一种安排工作进度的有力工具。在甘特图

中,用横轴表示时间刻度,纵轴表示计划,用一条“横条”表示该计划的起始和结束时间。

甘特图的优点是简单、明了、直观、易于绘制,因此,它是 Web 图表编程中经常绘制的图表

之一。

本节介绍如何利用一些基本图形的绘制(直线、实心矩形、文本)来实现甘特图。

本节的例程(chap05\Fig5.8\GanterServlet.java)中的甘特图,用来表示一个总天数为 10 天的项目进度安排。 该项目从 2月 1 日开始到 2月 10日结束, 该项目又可分解为 8 个子计划,

每个计划的开始时间及完成该计划所需的时间从随机数中产生。 程序的运行结果如图 5.26 所

示。我们可以观察到,该项目中各个计划的进度、开始时间、完成各计划所需的时间、结束

时间等信息。

Page 200: Java Web动态图表编程

图 5.26 生成甘特图的 Servlet

首先下载源码根目录下的 chap05\Fig5.8 子目录中的 web.xml 文档,复制该文档中的第 9 行~第 18 行的内容,然后,将复制的内容粘贴到 \WEB­INF\web.xml 文档中的<web­ app></web­app>标记中。

<servlet> <servlet­name>GanterServlet</servlet­name>

<description>绘制甘特图的 Servlet</description> <servlet­class>com.fatcat.webchart.GanterServlet</servlet­class>

</servlet>

<servlet­mapping> <servlet­name>GanterServlet</servlet­name> <url­pattern>/ganter</url­pattern>

</servlet­mapping>

调用本 Servlet 的 Ganter.html 的源程序很简单,只需要了解程序第 12 行,调用 Servlet 生成甘特图的方法就可以了。然后,将 GanterServlet.java 拷贝到\WEB­INF\com\fatcat\web chart\下面。

GanterServlet.java 的源程序第 26 行~第 33 行:

int totalProcess = 8; int[] startDay = new int[totalProcess]; int[] processDay = new int[totalProcess]; for (int i = 0; i < totalProcess; i++)

startDay[i] = 1+(int)(Math.random() * 9); processDay[i] = 1+(int)(Math.random() * (11­startDay[i]));

整型变量 totalProcess 表示组成该项目子计划的数目, 整型数组 startDay表示计划的开始

时间, 整型数组 processDay表示完成计划所需要的时间。 然后, 通过一个循环, 分别为 startDay 和 processDay数组赋值。注意,这里 startDay中元素的值是从一个 1~9 中的随机数中产生,

而 processDay中元素的值是从 1到 11­startDay之间的数字中产生。

程序第 36 行~第 70行的代码,绘制的结果如图 5.27①所示。

程序第 72 行~第 78行:

for (int i = 0; i <= 400; i += 40)

Page 201: Java Web动态图表编程

g.setColor(Color.BLACK); g.drawString("2­" + (1+i / 40), (i + 70), 420); g.setColor(Color.LIGHT_GRAY); g.drawLine((i + 80), 65, (i + 80), 400);

这段代码绘制了 11 条垂直方向的浅灰色的直线,每条直线表示 2月 1 日到 2 月 11 日中

的每一天,以及横坐标上表示时间的字段。绘制的结果如图 5.27②所示。

图 5.27 甘特图的绘制过程 1

程序第 81 行~第 83行:

g.setColor(Color.BLACK); g.fillRect(77, 55, 3, 345); g.fillRect(80, 397, 420, 3);

绘制坐标轴,与以前我们绘制坐标轴不同之处是:以前绘制的坐标轴是用两条直线来表示。

本例中,是用两个实心矩形来表示。程序执行完此段代码后的运行结果如图 5.28①所示。

Page 202: Java Web动态图表编程

图 5.28 甘特图的绘制过程 2

程序第 85 行~第 101行,是绘制甘特图的核心代码。主要执行了以下操作:

Ø 绘制纵坐标上的字段,表示组成本项目的子计划。

Ø 绘制浅灰色的水平直线。

Ø 计算代表每个子计划的实心矩形的绘制参数, 如左上角顶点坐标以及绘制实心矩形的

高度,参与运算的这些数据都从前面的整型数组中获得。计算绘制参数后,用不同的

颜色填充实心矩形。绘制完实心矩形后,用字体为黑体、大小为 15 磅的字体,在实

心矩形上绘制完成该计划所需要的时间。当循环结束后,完成绘制工作。

现在来看程序是如何实现上述操作的。在该循环中,程序第 89 行~第 90行:

g.drawString("计划" + (i + 1), 40, 95+i * 40);

完成了上述第 1 点的绘制工作。程序第 91 行~第 92行:

g.setColor(Color.LIGHT_GRAY); g.drawLine(80, 90+i * 40, 480, 90+i * 40);

完成了上述第 2 点的绘制工作。程序执行完第 92 行后的绘制效果如图 5.28②所示。

程序第 93 行~第 100行:

g.setColor(color[i]); drawWidth = processDay[i] * 40; g.fill3DRect(80+(startDay[i] ­ 1) * 40, 78+i * 40, drawWidth, 25, true); g.setColor(Color.WHITE); g.setFont(new Font("黑体", Font.BOLD, 15)); g.drawString("" + processDay[i] + "天", 70+(startDay[i] ­ 1) * 40 +

drawWidth / 2, 95+i * 40); g.setFont(new Font("SansSerif", Font.PLAIN, 12));

完成了上述第 3 点的绘制工作。注意,我们这里设定每一天在横坐标上的间隔是 40 像

素。

程序最后部分将缓冲图像输出到客户端,并结束此程序。本 Servlet 最后绘制效果如图 5.26 所示。

到目前为止,我们所介绍的绘制 Web 动态图表例程,都是在单一的 Servlet 中完成。如

何利用多个不同的 Servlet 来完成一幅图表的绘制工作呢?目前我们绘制的坐标轴都是没有

箭头的,如何调用本章 5.2 节所介绍的绘制三角形的 Serlvet 来帮助完善坐标轴的绘制呢?如

何利用绘制平行四边形的方法来绘制 3D 图表呢?

Page 203: Java Web动态图表编程

下一节我们将介绍如何让多个 Servlet 协同工作,来绘制比较复杂的 3D 甘特图。

5.9 Servlet 绘制 3D 甘特图

本节将主要介绍,如何让多个 Servlet 协同工作,来完成绘制比较复杂的 3D 图表。我们

以上一节所介绍的绘制甘特图的 Servlet 为蓝本,重新改写这个 Servlet,使其能够绘制 3D 甘

特图。

本例的运行效果,如图 5.29 所示。

图 5.29 生成 3D甘特图的 Servlet

为完成本例所介绍的 3D 甘特图的绘制工作,我们需要引出本章 5.2 节所介绍的绘制三

角形的 Serlvet(WEB­INF\com\fatcat\webchart\TriangleServlet.java),另外,我们再编写一个

类,用于绘制各种风格 3D 矩形(\chap05\Fig5.9\DrawParallelogram.java)。这两个类,将被本

例所讲述的绘制 3D甘特图 Ganter3DServlet.java 所调用。图 5.29中横轴最右边的箭头就是调

用 TriangleServlet.java,并由该 Servlet 绘制。图 5.29 中,各种颜色实心矩形的绘制工作是调

用 DrawParallelogram.java 类完成的。

打开下载源码根目录下 chap05\Fig5.9 子目录中的 web.xml 文档, 复制该文档中第 9 行~

第 18 行的内容,然后,将复制的内容粘贴到\WEB­INF\web.xml 文档中的<web­app> </web­ app>标记中。

<servlet> <servlet­name> Ganter3DServlet</servlet­name>

<description>绘制甘特图的 Servlet</description> <servlet­class>com.fatcat.webchart.Ganter3DServlet</servlet­ class>

</servlet>

<servlet­mapping> <servlet­name>Ganter3DServlet</servlet­name> <url­pattern>/ganter3D</url­pattern>

</servlet­mapping>

调用此 Servlet 的 Ganter3D.html 的源程序也很简单, 只需要了解程序第 12 行调用 Servlet 生成 3D 甘特图的方法就可以了。然后将本节所讨论的两个 Servlet 源程序—— DrawParallelogram.java 以及 Ganter3DServlet.java 拷贝到目录\WEB­INF\com\fatcat\ webchart\ 下。因调用绘制箭头的 TriangleServlet.java 已经保存在该目录中,因此,我们在 Ganter3D Servlet.java 程序中就可以直接调用 TriangleServlet.java。

Page 204: Java Web动态图表编程

有关 TriangleServlet.java 的详细介绍,请看本章第二节的内容。

相信读者在阅读 DrawParallelogram.java 代码的时候,会有一种很熟悉的感觉。不错, DrawParallelogram.java 的源程序是以本书第 2 章第 2.10.5 节所讨论的绘制平行四边形及立方

体 Applet(位于\chap02\Fig2.10\Fig2.10_05 \DrawParallelogramApplet.java)为蓝本。本例程使

用的绘制原理及方法和DrawParallelogramApplet.java完全一致。因此,本节不再对如何绘制平行

四边形及立方体做出阐述,请读者参考本书第 2章第 2.10.5节的相关内容。

本例程中, 我们新增加了一个 setFillColor 的方法, 用来设置立方体三个表面的填充颜色。

该方法见 DrawParallelogram.java 的源程序第 70 行~第 75 行:

public void setFillColor(Color pTopFillColor)

setPTopFillColor(pTopFillColor); setFrontFillColor(pTopFillColor.darker()); setPRightFillColor(pTopFillColor.darker().darker());

setFillColor 的方法,接收一个 Color 类的实例作为参数,该段代码实现的功能如下:

Ø 调用程序第 58 行~第 61 行 setPTopFillColor 方法,将参数指定的颜色对象 pTopFill Color 设置为填充立方体顶部平行四边形的填充颜色。

Ø 调用程序第 52 行~第 55行 setFrontFillColor 方法, 用指定的颜色对象 pTopFill Color,

更深一些的颜色对象(调用 Color 类的 darker 方法),设置为填充立方体正面平行四

边形的填充颜色。

Ø 调用程序第 64 行~第 67 行 setPRightFillColor 方法,再连续两次调用 Color 类的 darker 方法,将获得的 Color对象,设置为填充立方体右侧面平行四边形的填充颜色。

程序的其他部分与 DrawParallelogramApplet.java 完全相同。

现在讲解调用这两个类的 Ganter3DServlet.java 源程序,我们要调用的类 Draw Parallelo gram.java,以及 TriangleServlet.java 都位于本包(\WEB­INF\com\fatcat\webchart\)中,所以,

不需要 import 语句将其引入本程序中。

程序第 1 行~第 69 行代码,就是绘制标题、背景色、图表区域边框等。绘制的结果如

图 5.30①所示。

程序第 70 行~第 80行:

g.setFont(new Font("SansSerif", Font.PLAIN, 12));

// 绘制横坐标上的说明文字,垂直直线和斜线

for (int i = 0; i <= 400; i += 40)

g.setColor(Color.black); g.drawString("2­" + (1+i / 40), (i + 70), 440); g.setColor(Color.lightGray); g.drawLine((i + 91), 65, (i + 91), 410); g.drawLine((i + 81), 420, (i + 91), 410);

这段代码首先绘制横坐标上表示时间的字段, “2­1”~“2­11” ,表示 2 月 1 日到 2 月 11 日中的每一天。然后,程序第 78 行,绘制 11 条垂直方向的浅灰色直线。程序第 79 行,绘制

如图 5.30②红圈部分所示的斜线。

Page 205: Java Web动态图表编程

图 5.30 3D甘特图的绘制过程 1

程序第 83行~第 85行,绘制坐标轴。同样地,用两个黑色的实心矩形来代表坐标轴:

g.setColor(Color.black); g.fillRect(77, 60, 3, 363); g.fillRect(80, 420, 420, 3);

绘制效果如图 5.31①所示。

然后,在横轴的最右边绘制了一个方向向右的三角形。绘制该三角形的工作是通过调用 TriangleServlet.java 类来完成的,见源程序代码第 88行~第 91 行:

TriangleServlet ts = new TriangleServlet(); ts.setBaseLine(10); ts.setAlpha(60); ts.drawTrigangle(500, 421, 2, 2, g);

程序第 88 行,调用 TriangleServlet 类的默认构造器方法,创建了一个 TriangleServlet 类

的对象 ts。注意,在 TriangleServlet.java 源程序中,我们并没有提供任何构造器方法。既然

没有提供该构造器方法, 为什么可以调用这个构造器的方法呢?这里就需要了解一下 Java 类

的继承以及编译机制了。如果一个类没有使用“extends”关键字指明其父类,则默认继承其

“Object”类(隶属于 java.lang包)。无论是否在该类中提供了默认的构造器。Java 编译器在

编译该类的时候,都将自动调用其父类的默认构造器。因此,虽然 TriangleServlet.java 源程

序中并没有提供任何构造器的方法,这里调用其默认的构造器的方法,实际上,就是调用了 java.lang.Object 的默认构造器的方法,所以程序不会出错。

程序第 89行, TriangleServlet类的对象 ts调用其 setBaseLine方法 (见 Triangle Servlet.java 源程序第 99行~第 102 行),设置三角形底边的长度为 10 像素。

程序第 90行, ts调用 setAlpha方法 (见 TriangleServlet.java源程序第 104行~第 107行),

设置三角形底角的角度为 60 度。

程序第 91 行,ts 调用 drawTrigangle方法(见 TriangleServlet.java 源程序第 30 行~第 97 行),绘制三角形。这里为 drawTrigangle方法提供了 5 个参数。第 1、2 个整形参数表示该三

角形底边中点的坐标,第 3 个参数“2” ,表示三角形的方向向右(见 TriangleServlet.java 源

程序第 22 行)。第 4 个参数“2” ,表示三角形绘制风格为实心三角形(见 TriangleServlet.java 源程序第 25行)。第 5 个参数“g” ,表示把当前绘图环境对象传递给 drawTrigangle方法,并

在当前的绘图环境中进行图形绘制工作。

执行完程序第 91 行后,绘制结果如图 5.31②所示。

程序第 94 行,调用 DrawParallelogram类的默认构造器方法,创建了一个 Draw Parallelo gram 类的对象 dp。同样,在 DrawParallelogram.java 中,也没有提供默认构造器的方法,这

Page 206: Java Web动态图表编程

里实际上是调用 java.lang.Object 的默认构造器。

程序第 96 行:

int drawWidth = 0;

初始化 3D矩形(立方体)的绘制长度为 0。

程序第 97 行~第 128行,通过一个循环,计算所有 3D 矩形的绘制参数,并根据这些参

数绘制 3D矩形。程序第 100行~第 106 行:

g.setColor(Color.LIGHT_GRAY); g.drawLine(90, 90+i * 40, 490, 90+i * 40); if (i == processDay.length ­ 1)

g.drawLine(90, 90+(i + 1) * 40, 490, 90+(i + 1) * 40); g.drawLine(80, 100+i * 40, 90, 90+i * 40);

图 5.31 3D甘特图的绘制过程 2

绘制水平直线、连接水平直线和纵轴之间的斜线。绘制结果如图 5.32 所示。

程序第 109行~第 111 行:

g.setColor(Color.black); g.setFont(new Font("SansSerif", Font.PLAIN, 12)); g.drawString("计划" + (i + 1), 45, 123+i * 40);

这段代码绘制了纵坐标的说明字段。绘制结果如图 5.32 所示。

图 5.32 3D甘特图的绘制过程 3

程序第 114 行:

Page 207: Java Web动态图表编程

drawWidth = processDay[i] * 40;

计算 3D 矩形的绘制长度。3D 矩形表示完成子计划所需要的天数,3D 矩形的长度越长,

说明完成该计划所需要的时间也就越多。同样,在坐标系中,每 40 像素表示一天,所以,这

里用 processDay数组中的元素再乘以 40就得到了 3D矩形的实际绘制长度。

程序第 115 行~第 121 行,调用 DrawParallelogram 类的对象 dp 相关方法,绘制 3D 矩

形。

程序第 115 行:

dp.setFillColor(color[i]);

调用前面介绍的 setFillColor 方法,设置 3D 矩形顶面、正面和右侧面的填充颜色。

程序第 116 行:

dp.setBasePoint(80+(startDay[i] ­ 1) * 40, 125+i * 40);

调用 setBasePoint 方法,设置 3D 矩形基准点(左下方顶点)的坐标。

程序第 117 行:

dp.setWidth(drawWidth);

调用 setWidth 方法,设置 3D 矩形的绘制宽度。这个宽度就是程序第 114 行,计算出来

的 3D 矩形的绘制长度。

程序第 118 行:

dp.setHeight(20);

调用 setHeight 方法, 设置 3D 矩形的绘制高度。 这里设定 3D 矩形的绘制高度为 20像素。

程序第 119 行:

dp.setThickness(15);

调用 setThickness 方法,设置 3D 矩形的绘制厚度。这里设定 3D 矩形的绘制厚度为 15 像素。

程序第 120行:

dp.setAngle(45);

调用 setAngle 方法,设置 3D 矩形的顶部平行四边形的斜边与其水平边的夹角度数。这

里设置该夹角度数为 45 度。

程序第 121行:

dp.drawParallelogram(2, g);

调用 drawParallelogram方法(见 DrawParallelogram.java 源程序第 152 行~第 200 行),

绘制 3D 实心矩形。第1个参数“2” ,表示该 3D 矩形的绘制风格为实心矩形(见 DrawParallelogram.java 源程序第 37 行);第 2 个参数“g” ,将当前缓冲图像的绘图环境对象 g传递给 drawParallelogram方法,drawParallelogram方法,将在 g上绘制 3D 实心矩形。

程序第 124行~第 127 行:

g.setColor(Color.WHITE); g.setFont(new Font("黑体", Font.BOLD, 15)); g.drawString("" + processDay[i] + "天", 70+(startDay[i] ­ 1) * 40

+ drawWidth / 2, 120+i * 40);

在 3D 实心矩形的正面矩形上绘制说明文字,用于说明完成本计划所需要的天数。

最后,程序第 131行~第 133 行,将缓冲图像输出到客户端,并结束本程序。本 Servlet

Page 208: Java Web动态图表编程

最后的绘制效果如图 5.29 所示。

至此, 我们阐述了 Ganter3DServlet.java 与其他 Servlet 协同工作来绘制 3D甘特图表的方

法。可以将复杂的图表分解成一些基本的几何图像。然后,分别编写一些绘制基本几何图像

的类,封装其中的数据对象和具体的计算以及绘制方法,通过调用这些类提供的相关的公共

方法,就可以完成复杂的 Web 图表绘制工作。

5.10 本章小结

本章详细介绍了 Java Servlet 安装和部署的相关知识。通过几个实例的讲解,了解了 Servlet 生成动态图表的机制及其运行过程。多个 Servlet 可以协同工作完成复杂的 Web 图表

绘制工作。Servlet 异常强大而灵活的功能,非常适合于 Web 应用程序服务器端的编程。它可

以完成复杂的业务逻辑。但 Servlet 在带来强大功能的同时,随之而来的是其固有的弊端,那

就是 Servlet 的安装、编译和部署是非常复杂的。

有鉴于此, Sun公司在1999年的下半年, 推出了基于Servlet技术的 JSP (Java Server Pages)

技术。它在保留 Servlet 高性能的同时,以一种更方便、更快捷的方式开发Web 应用程序。

下章,我们将开始 JSP 的学习之旅。

第 6 章 JSP Web 图表编程基础

本章将介绍如何利用 JSP 技术生成动态的 Web 图表。JSP 是 Java Server Pages(Java 服

务器页面)的缩写。它是由 Sun 公司开发并加以推广,建构在 Servlet 技术之上的 Web 服务

器端编程技术。

为什么 Sun公司会推出基于 Servlet 技术的 JSP 技术?通过上一章的学习, 读者已经非常

清楚 Servlet 在服务器端的强大编程能力。但是 Servlet 在带来强大功能的同时,随之而来的

是其固有的弊端,那就是 Servlet 的安装、编译和部署是非常麻烦的。要成功开发并部署一个 Servlet,需要的步骤如下:

(1)编写正确无误的 Servlet 源代码。

(2)更新\WEB­INF\web.xml 文档,在 web.xml 文档中指明 JSP/Servlet 容器要加载的 Servlet 的别名、Servlet 完整的路径名及类名、URL 映射、Servlet 描述等。

(3)将 Servlet 源代码放置在相对应的\WEB­INF\classes 路径下。

(4)对于某些 JSP/Servlet 容器,还必须重新启动一次才能加载新部署的 Servlet。

如果在此过程中出现错误,将无法调用该 Servlet。尽管 Servlet 具有强大的性能,而且是

一种 Web 应用程序服务器端的首选平台。但 Servlet 的配置和部署,一直是很多开发者望而

却步的一个难关,影响了 Servlet 在全球的推广使用。

除此之外,Servlet 的编写也是一件烦琐的工作。从上一章介绍的例程可以看出,Servlet

Page 209: Java Web动态图表编程

有一个最大的缺点:尽管 Servlet 是百分之百的 Java 程序,但为了将最终的输出结果以 HTML/XML的格式返回给 HTTP 客户端,在 Java 的代码中,我们不得不嵌入大量的 HTML 代码。必须通过一句一句地调用 PrintWriter 类对象 out 的 println方法,来输出每一句 HTML 代码。下面以第 5 章介绍的 WelcomeServlet.java(chap05\Fig5.2\ Fig.5.2_01)为例来说明,其

源程序清单如下:

1: // Fig. 5.02_01: WelcomeServlet.java 2: // 简单处理客户端"get"请求的 servlet 3:4: package com.fatcat.webchart; 5: 6: import javax.servlet.*; 7: import javax.servlet.http.*; 8: import java.io.*; 9: 10: public class WelcomeServlet extends HttpServlet 11: 12: // 处理客户端的 "get" 请求 13: protected void doGet( HttpServletRequest request, 14: HttpServletResponse response ) 15: throws ServletException, IOException 16: 17: response.setContentType( "text/html;charset=gb2312" ); 18: PrintWriter out = response.getWriter(); 19: 20: // 发送 XHTML 格式的页面给客户端 21: 22: // 开始生成 XHTML 文档 23: out.println( "<?xml version = \"1.0\"?>" ); 24: 25: out.println( "<!DOCTYPE html PUBLIC \"­//W3C//DTD " + 26: "XHTML 1.0 Strict//EN\" \"http://www.w3.org" + 27: "/TR/xhtml1/DTD/xhtml1­strict.dtd\">" ); 28: 29: out.println( "<html xmlns = \"http://www.w3.org/1999/ xhtml\

">" ); 30: 31: // 生成文档的 head部分 32: out.println( "<head>" ); 33: out.println( "<title>简单的处理客户端\"get\"请求的 servlet</

title>" ); 34: out.println( "</head>" ); 35: 36: // 生成文档的 body部分 37: out.println( "<body>" ); 38: out.println( "<h1>您好,<br>欢迎进入 Servlet图表编程的世界</h1>" ); 39: out.println( "</body>" ); 40: 41: // 结束 XHTML 文档 42: out.println( "</html>" ); 43: out.close(); // 关闭输出流 44: 45:

在上面的例程中, 使用了大量的 out.println语句来输出 HTML内容。 实际上, 这些 HTML 代码并不是我们需要处理的对象。在 Servlet 中,通过简单方式便可实现的 HTML 代码,开

发者却要通过 out.println来生成每一行的 HTML内容。HTML内容不得不通过代码来实现,

对于一个包含有大量HTML代码的Servlet来说, 实在是一项繁重而费时的工作, 而且在 servlet 实际应用中也是一个严重问题。

为此,人们开始寻求一种更好的解决方式。在 Sun正式发布 JSP(Java Server Pages)之

Page 210: Java Web动态图表编程

后, 这种新的 Web 应用开发技术很快引起了人们的关注。 JSP 为创建高性能的动态 Web 应用,

提供了一个独特的开发环境。

6.1 JSP 概述

JSP 技术是 J2EE 体系结构中的一个重要组成技术。它为开发人员提供了一个 Server 端

框架,基于这个框架,开发人员可以综合使用 HTML、XML、Java 语言,以及其他脚本语言,

灵活、快速地创建动态网页。

JSP 技术基于 Servlet 技术,但它在更高一级的层次上抽象 Servlet。可以让常规、静态的 HTML 与动态产生的内容相结合,看起来像一个 HTML 网页,但却作为 Servlet 来运行。使

用 JSP 要比 Servlet 更简单,是一段标准的 Java 代码。可以放置在任何静态 HTML文件可以

放置的位置,不用编译,不用打包,也不用进行 ClassPath的设置,如同访问普通网页那样访

问。 JSP服务器将客户端访问的 JSP文件, 自动转换成相应的Servlet, 并由服务器负责该Servlet 的配置和部署工作。

JSP 技术源自于 Servlet。理论上,凡是 Servlet 可以完成的工作,JSP 同样可以完成,并

且最终以 Servlet 的形式在 Web 服务器端运行。因此,它又继承了 Servlet 的所有优点,例如

高性能、分布式等。

在 JSP 中,编写静态 HTML更加方便,不必使用 println语句来输出每一行 HTML代码。

更重要的是,借助内容和外观的分离,在页面制作中不同性质的任务可以方便地分开。比如,

由页面设计专家进行 HTML设计,同时保留出供 Servlet 程序员插入动态内容的空间。

按照 Sun的说法,JSP 将 Servlet 中的 HTML 代码脱离出来,从而加速了 Web 应用开

发和页面维护。 “JSP 技术应该被视为标准, 而 Servlets 在多数情况下可视为一种补充……”

(详见 Sun 发布的“应用开发模型”文档 Section 1.9,1999/12/15 听取意见版)。

提示: Servlet是在 Java源程序中插入HTML代码, 而 JSP是在 HTML源程序中插入 Java 代码。

6.1.1 JSP 运行机制

JSP 的运行机制,也就是 JSP 如何实现客户端和服务器端交互的基本流程,如图 6.1 所

示。

(1)用户在 HTTP 客户端(浏览器)发出的请求信息,被存储在 Request 对象中并发传

送给 JSP/Servlet 服务器,如图 6.1①所示。

(2) JSP/Servlet 服务器根据请求信息中指定的 JSP 文件来处理该 Request 对象, 如图 6.1 ②所示。

(3)JSP 文件处理该 Request 对象时,一般有以下两种处理途径: A.在当前的 JSP 文件中处理该请求。

Ø 当前的 JSP 生成响应该请求的 Response响应对象,将生成 Response响应对象返回给 JSP/Servlet 服务器,如图 6.1⑤所示。

Ø JSP/Servlet 服务器在接收到请求指定的 JSP 文件,返回结果后,将接收到的 Response 响应对象生成标准的 HTML/XML 格式内容,再返回给 HTTP 客户端,如图 6.1⑥所

示。

Page 211: Java Web动态图表编程

图 6.1 JSP与客户端和服务器端交互的基本流程

B.根据实际需要,将 Request 对象转发给其他的服务器端组件处理。

Ø 将客户端请求转发给当前 JSP 文件所指定的其他服务器端组件(如 Servlet 组件、 JavaBean组件或 EJB 组件等)加以处理,如图 6.1③所示。

Ø 接收到 JSP 页面转发请求的 Request 对象后,服务器端组件生成 Response响应对象,

并返回给调用它的 JSP 页面,如图 6.1④所示。

Ø JSP 页面接收到服务器端组件生成 Response 响应对象,并将其返回给 JSP/Servlet 服

务器,如图 6.1⑤所示。

Ø JSP/Servlet 服务器在接收到请求指定的 JSP 文件, 返回结果后将接收到的 Response响

应对象生成标准的 HTML/XML格式内容,再返回给 HTTP 客户端,如图 6.1⑥所示。

JSP 文件如同一个普通静态 HTML 文件,只不过里面包含了一些 Java 代码。它使用.jsp 的后缀,用来通知服务器该文件需要特殊的处理。当我们访问一个 JSP 页面的时候,该文件

首先会被 JSP引擎翻译为一个 Java源文件, 其实就是一个Servlet并进行编译, 如同其他Servlet 一样,由 Servlet 引擎来处理。Servlet 引擎装载这个类,处理来自客户的请求,并把结果返回

给客户。在技术实现细节上,JSP 的实现借助了 Servlet 技术,系统在首次载入 JSP 时,自动

将其编译成内部的 Servlet,JSP 对 Request 对象和 Response 对象(以及其他隐含对象)的处

理,最终都是由其对应的 Servlet 来完成的。

随后的客户在访问 JSP 页面时,只要该文件没有更改,JSP 引擎就直接调用已经编译并

加载的 Servlet。如果已经修改,就会再次执行以上的过程,翻译、编译并加载。这就是所谓

的“第一人惩罚” 。

客户端 response 响应 ⑥

request 请求 JSP/Servlet 服务器 response 响应

② request 请求 JSP 文件 response 响应

③ forward 转发 其他 Web

组件

Page 212: Java Web动态图表编程

6.1.2 JSP 的优点

JSP 技术是从 Servlet 技术发展起来的。在 Java 服务器端编程中普遍采用的就是 JSP,而

不是 Servlet。因为 JSP 在编写表示页面时远比 Servlet 简单,并且不需要手工编译(由 Servlet 容器自动编译), 目前 Servlet 主要用于视图控制器、 处理后台应用等。 由于 JSP 构建在 Servlet 上,所以它具有 Servlet 所有的强大功能。

在开发 JSP 规范的过程中,Sun公司与许多主要的 Web 服务器、应用服务器和开发工具

供应商积极合作,不断完善技术。现在有很多支持 JSP/Servlet 的服务器,除了我们介绍过的 Sun 公司的 Java Web Services Developer Pack、轻量级的 Apache 软件基金组织的 Tomcat、 Caucho公司的 Resin和企业级 BEA公司的 Weblogic,还有 Macromedia 公司的 JRun 4、开放

源代码组织的 JBoss、IBM 公司的 WebSphere、Borland公司的 Borland Enterprise Server 5.0.2、 SilverStream的 Application Server 以及 Fujitsu公司的 Interstage。

在传统网页 HTML文件(*.htm,*.html)中,加入 Java程序片段(Scriptlet)和 JSP 标记,

就构成了 JSP 网页(*.jsp)。 JSP 基于强大的 Java 语言,具有良好的伸缩性,与 Java Enterprise API集成在一起,在

网络应用开发领域中具有得天独厚的优势。基于 Java平台构建网络程序,已经被越来越多的

人们认为是最有发展前途的技术。

从 JSP 近几年的发展来看,已经获得了巨大成功。它通过和 EJB 等 J2EE 组件进行集成,

可以编写处理高负载的企业级应用,并且加速了动态Web 页面的开发。

下面我们来总结一下 JSP 的优点:

Ø 将内容的生成和显示进行分离

使用 JSP 技术,Web 页面开发人员可以使用 HTML或者 XML标识来设计和格式化最终

页面。使用 JSP 标识或者小脚本来生成页面上的动态内容。生成内容的逻辑被封装在标识和 JavaBeans 的组件中,并且捆绑在小脚本中,所有的脚本在服务器端运行。如果核心逻辑被封

装在标识和 Bean之中,那么其他人,如 Web 管理人员和页面设计者,都能够编辑和使用 JSP 页面,而不会影响内容的生成。

在服务器端, JSP 引擎解释标识和小脚本, 生成所请求的内容 (例如, 通过访问 JavaBeans 组件,使用 JDBC 技术访问数据库),并且将结果以 HTML 或者 XML 页面形式发送回浏览

器。这样有助于开发者保护自己代码的同时,又保证了任何基于 HTML 的 Web 浏览器的完

全可用性。

Ø 生成可重用的组件

绝大多数 JSP 页面依赖于可重用、跨平台的组件(JavaBeans 或者 Enterprise JavaBeans 组件)来执行应用程序所要求的更为复杂的处理。开发人员能够共享和交换执行普通操作的

组件或使这些组件为更多的使用者、客户团体所使用。基于组件的方法加速了总体开发,并

使得各种组织在他们现有技能和优化结果上的开发得到平衡。

Ø 采用标识简化页面开发 Web 页面开发人员不一定都是熟悉脚本语言的编程人员。Java Server Pages 技术封装了

许多功能,这些功能是易用的、与 JSP 相关的 XML 标识中进行动态内容生成时所需要的。

标准的 JSP 标识能够访问和实例化 JavaBeans 组件,设置或者检索组件属性,下载 Applet 以

及执行其他方法。

通过开发定制标识库,JSP 技术非常容易扩展。第三方开发人员和其他人员可以为常用

功能创建自己的标识库。 这使得 Web 页面开发人员能够使用熟悉的工具和如同标识一样执行

特定功能的构件来工作。

Page 213: Java Web动态图表编程

Ø JSP 能提供所有的 Servlets 功能

与 Servlets 相比,JSP 能提供所有的 Servlets 功能,它比使用 Prineln 书写和个性 HTML 更方便。此外,可以更加明确地进行分工。Web 页面设计人员编写 HTML,只需要留出空间

让 Servlets 程序员插入动态部分即可。

Ø 健壮的存储管理和安全性

由于 JSP 页面的内置脚本语言是基于 Java 的编程语言, 并且所有的 JSP 页面都被编译成 Java Servlet。因此,JSP 页面就具有 Java 技术的所有优点,包括健壮的存储管理和安全性。

Ø 一次编写,随处运行

作为 Java 平台的一部分,JSP 拥有 Java 编程语言“一次编写,随处运行”的特点。

Ø JSP 平台适应性非常广泛

几乎所有的平台都支持 Java。JSP+JavaBeans 的形式令它们可以在任何平台下通行无阻。

从一个平台移植到另一个平台,JSP 和 JavaBeans 甚至不用重新编译,因为 Java 字节码都是

标准的字节码,与平台无关。

其他的优点,如 Java 连接数据库的技术——JDBC(Java Database Connectivity)提供了

一种与几乎所有数据库高效、简洁的操作方式,包括数据库连接池、事务处理等。因为这些

内容超出了本书的讨论范围,有兴趣的读者请自行查阅相关资料。

我们将提供更多的实例及详细解释,介绍如何运用 JSP 技术来绘制 Web 动态图表。

在进行 JSP 编程之前,我们还必须对 JSP 的相关知识做一个简要的了解。

6.2 JSP 语法简介

6.2.1 JSP 文件结构

我们在第 4章 JSP/Servlet 运行环境的搭建中,向读者提供了几个 JSP 源程序,用来测试 JSP/Servlet 运行环境是否搭建成功。但我们并没有对 JSP 文件结构做出解释和分析。现在,

我们以 JSPStructure.jsp文件(chap06\Fig6.1\Fig.6.1_01\)为例,讨论 JSP 文件的结构。 JSPStructure.jsp的源程序清单如下:

1: <!­­ 2: Fig. 6.01_01: JSPStructure.jsp 3: 功能: JSP文件结构

4: ­­> 5: <%@ page language="java" contentType="text/html;charset=GB2312"%> 6: <HTML> 7: <HEAD> 8: <TITLE> JSP文件结构 </TITLE> 9: </HEAD> 10: 11: <BODY> 12: <% 13: int a = 55, b = 45; 14: %> 15: 整数: 55 + 45 的结果是: <%= a + b%> 16: <BR><BR> 17: 整数: 55 ­ 45 的结果是: <%= a ­ b%> 18: </BODY>

Page 214: Java Web动态图表编程

19: </HTML>

从上面的代码清单中可以看到,JSP 页面除了比普通 HTML页面多一些 Java 代码外,两

者具有基本相同的结构。Java 代码是通过“<%”和“%>”符号加入到 HTML代码中间的,

其主要功能就是执行“<%”和“%>”之间的 Java 代码。

打开浏览器,并在地址栏输入:http://localhost:8080/chap06/Fig6.1/Fig.6.1_01/ JSP Structure.jsp。如果 JSP 源程序没有错误,则会看到如图 6.2 所示的结果。

图 6.2 JSPStructure.jsp的运行结果

单击 IE 工具栏【查看】→【源文件】,可以看到如图 6.3 所示的结果。

Page 215: Java Web动态图表编程

图 6.3 JSPStructure.jsp返回的 HTML源代码

不难看出,JSPStructure.jsp 的源程序中“<%”和“%>”之间的 Java 代码块,已经被 JSP/Servlet 服务器替换成相应的运行结果。

现在我们将源程序第 13 行中的“;”去掉,变为:

int a = 55, b = 45 //遗漏了分号

源程序就会出现错误,如果采用 Tomcat 作为 JSP/Servlet 服务器,就会看到如图 6.4 所示

的错误页面。

图 6.4 Tomcat服务器下的错误提示

如果采用 Resin作为 JSP/Servlet 服务器,则会看到如图 6.5 所示的错误页面。

提示:Tomcat 或 Resin会因不同的版本而显示出不同的错误提示信息。旧版本的 Tomcat 不会直接给出 JSP 源文件出现错误的地方,而是给出该 JSP 源文件被转换成 Servlet,即 java 源程序后,代码出现错误的地方。

Page 216: Java Web动态图表编程

图 6.5 Resin服务器下的错误提示

我们使用的当前版本的 Tomcat 和 Resin服务器提供的错误信息非常简洁, 直接给出源程

序错误的类型,以及出错代码在 JSPStructure.jsp 源程序的具体位置。注意,Tomcat 和 Resin 指出的错误位置有轻微的差别。如果采用旧版本的 Tomcat 服务器,如 5.0.28 版,则该版本的 Tomcat 就不能正确地指出该出错代码在 JSPStructure.jsp 源程序中的具体位置。Tomcat 指明

出错的位置在第 54 行。这里的第 54 行,并不是指该代码在 JSPStructure.jsp源程序的位置,

而是指该代码在 JSPStructure.jsp编译成 Servlet 后,在该 Servlet 的源程序中位置。从 Tomcat 提供的出错信息中可以知道 JSPStructure.jsp 被编译成 Servlet 后,该 Servlet 的源程序名字为 JSPStructure_jsp.java,且保存位置在:

C:\Tomcat\work\Catalina\localhost\_\org\apache\jsp\chap06\Fig6_1\Fig_ 6_1_005f01\JSPStructure_jsp.java

这里的 C:\Tomcat 是本书中 Tomcat 的安装目录。 我们来看看 JSPStructure_jsp.java 的源程

序清单如下:

1: package org.apache.jsp.chap06.Fig6_1.Fig_6_1_005f01; 2: 3: import javax.servlet.*; 4: import javax.servlet.http.*; 5: import javax.servlet.jsp.*; 6: 7: public final class JSPStructure_jsp extends org.apache.jasper.

runtime.HttpJspBase 8: implements org.apache.jasper.runtime.JspSourceDependent 9: 10: private static java.util.Vector _jspx_dependants; 11: 12: public java.util.List getDependants() 13: return _jspx_dependants; 14: 15: 16: public void _jspService(HttpServletRequest request, HttpServlet­

Response response) 17: throws java.io.IOException, ServletException 18: 19: JspFactory _jspxFactory = null; 20: PageContext pageContext = null; 21: HttpSession session = null; 22: ServletContext application = null; 23: ServletConfig config = null; 24: JspWriter out = null; 25: Object page = this; 26: JspWriter _jspx_out = null; 27: PageContext _jspx_page_context = null; 28: 29:

Page 217: Java Web动态图表编程

30: try 31: _jspxFactory = JspFactory.getDefaultFactory(); 32: response.setContentType("text/html;charset=GB2312"); 33: pageContext = _jspxFactory.getPageContext(this, request,

response, 34: null, true, 8192, true); 35: _jspx_page_context = pageContext; 36: application = pageContext.getServletContext(); 37: config = pageContext.getServletConfig(); 38: session = pageContext.getSession(); 39: out = pageContext.getOut(); 40: _jspx_out = out; 41: 42: out.write("<!­­\r\n"); 43: out.write("\tFig. 6.01_01: JSPStructure.jsp\r\n"); 44: out.write("\t功能: JSP文件结构\r\n"); 45: out.write("­­>\r\n"); 46: out.write("\r\n"); 47: out.write("<HTML>\r\n"); 48: out.write("<HEAD>\r\n"); 49: out.write("<TITLE> JSP文件结构 </TITLE>\r\n"); 50: out.write("</HEAD>\r\n"); 51: out.write("\r\n"); 52: out.write("<BODY>\r\n"); 53: 54: int a = 55, b = 45 55: 56: out.write("\r\n"); 57: out.write("整数: 55 + 45 的结果是: "); 58: out.print( a + b); 59: out.write("\r\n"); 60: out.write("<BR><BR>\r\n"); 61: out.write("整数: 55 ­ 45 的结果是: "); 62: out.print( a ­ b); 63: out.write("\r\n"); 64: out.write("</BODY>\r\n"); 65: out.write("</HTML>\r\n"); 66: catch (Throwable t) 67: if (!(t instanceof SkipPageException)) 68: out = _jspx_out; 69: if (out != null && out.getBufferSize() != 0) 70: out.clearBuffer(); 71: if (_jspx_page_context != null) _jspx_page_context.Handle­

PageException(t); 72: 73: finally 74: if (_jspxFactory != null) _jspxFactory.releasePageContext

(_jspx_page_context); 75: 76: 77:

程序第 54 行:

int a = 55, b = 45

正是代码发生错误的地方。因此,在程序的开发、调试已经正式运行阶段,采用最新版

本的 Tomcat 或 Resin服务器,实在是一个明智的选择。

下面分析 JSP 文档的结构。JSP 文档都以“jsp”为扩展名,文档正文又可分为几个部分。

下面以 JSPStructure.jsp的文档为例,加以说明。

首先是 JSP 指令。它描述页面的基本信息。如所使用的语言、是否维持会话状态、是否

使用缓冲等。JSP 指令由“<%@”开始,到“%>”结束,见程序第 5 行:

<%@ page language="java" contentType="text/html;charset=GB2312" %>

Page 218: Java Web动态图表编程

指令中 page language="java",简单地定义了脚本使用语言为 Java 语言。在目前的 JSP 规

范中,language属性默认值为“java” 。ContentType属性确定返回客户端响应数据内容的类型

以及字符的编码方式。 “text/html;charset=GB2312” 说明返回给客户端的数据类型为 text/html,

它通知客户端浏览器响应一个 XHTML文档。该 HTML文档,字符集编码方式为 GB2312。

于是,客户端浏览器就知道它必须读取文档中的 XHTML标记,并根据标记以及相应的字符

集,对文档进行格式化处理,然后将文档显示在浏览器中。

位于“<%”和“%>”之间的代码块,是描述 JSP 文档处理逻辑的 Java 代码,它被称之

为 Scriptlets。如程序第 12 行~第 14 行所示:

<% int a = 55, b = 45; %>

最后,位于“<%=”和“%>”之间的代码称为 JSP 表达式,如程序第 15 行的“<%= a + b%>”以及程序第 17 行的“<%= a−b%>”所示。JSP 表达式提供了一种将 JSP 生成的数值嵌

入 HTML 页面的简单方法。表达式将所得结果转换成字符串形式,然后连接在 HTML 代码

中。

此外,还有一种位于“<%! ”和“%>”之间的代码,称为 JSP 声明。它主要用于在 JSP 页面中声明变量、方法及类。如本书第 4 章所介绍的 test.jsp 文档( chap04\Fig4.1\ Fig.4.1_01\test.jsp)中第 8 行~第 21 行所示:

8: <%! 9: SimpleDateFormat shortSDF = new SimpleDateFormat("yyyy­MM­dd"); 10: SimpleDateFormat fullSDF = new SimpleDateFormat("yyyy­MM­dd

HH:mm:ss"); 11: 12: String gotShortDate(Date date) 13: 14: return shortSDF.format(date); 15: 16: 17: String gotFullDate(Date date) 18: 19: return fullSDF.format(date); 20: 21: %>

定义了变量、方法及类后,就可以在 JSP 文档的任意地方调用。如 test.jsp文档中第 35 行、第 37 行所示:

35: (yyyy­MM­dd)格式的转换结果为:&nbsp;<%=gotShortDate(now)%> 36: <BR><BR> 37: (yyyy­MM­dd HH:mm:ss)格式的转换结果为:&nbsp;<%=gotFullDate(now)%>

提示:JSP 代码和 Java 代码都是大小写敏感的。如上述程序第 35 行的调用方法如果写

成:GotShortDate(now),服务器就会给出错误提示。同样,类名、包名、路径名及一些 JSP 标签都不能写错。

6.2.2 JSP 文件中的元素简介

JSP 页面中的元素一般分为以下五类:注释、模板元素、脚本元素、指令元素和动作元

素。下面分别介绍每一类元素。

1.注释

程序 comment.jsp(chap06\Fig6.2\Fig.6.2_01\)演示了在 JSP 中注释的语法。其源程序清

Page 219: Java Web动态图表编程

单如下:

1: <!­­ 2: Fig. 6.02_01: comments.jsp 3: 功能: JSP注释语法示例 4: ­­> 5: <%@ page language="java" contentType="text/html;charset= GB2312" %> 6: <HTML> 7: <HEAD> 8: <TITLE> JSP注释语法示例 </TITLE> 9: </HEAD> 10: 11: <BODY> 12: <%­­ 本注释将不会在客户端出现 ­­%> 13: 14: <!­­ 本注释的生成时间: <%=(new java.util.Date()).toLocaleString() %>

­­> 15: <h1> 16: <% 17: // 获得系统的当前时间 18: int time = (new java.util.Date()).getHours(); 19: 20: /* 21: 根据时间来输出不同的问候语. 22: 12点前,输出: 上午好; 23: 12点到 18点,输出: 下午好; 24: 18点后,输出: 晚上好. 25: */ 26: 27: if (time <= 12) 28: out.print("上午好!"); 29: else if (time >= 13 && time <= 18) 30: out.print("下午好!"); 31: else 32: out.print("晚上好!"); 33: %> 34: </h1> 35: </BODY> 36: </HTML>

运行该程序后,在 IE 的【查看源文件】中,我们可以看到如图 6.6 所示的返回结果。

图 6.6 客户端显示的 comment.jsp的返回结果

JSP 文档中的注释共分三种:

Ø HTML/XML风格的注释

Page 220: Java Web动态图表编程

该注释返回给客户端(也就是它可以在【查看源文件】中看到),它又分为两种写法:

1)语法 1:

<!­­ comment ­­>

如程序 comment.jsp中第 1 行~第 4 行:

<!­­ Fig. 6.02_01: comments.jsp 功能: JSP注释语法示例

­­>

如图 6.6①所示,我们可以看到这段注释已经返回给了客户端。

2)语法 2:

<!­­ comment [ <%= expression %> ] ­­>

我们可以在注释中加上表达式,如程序 comment.jsp 中第 14 行:

<!­­ 本注释生成的时间<%=new java.util.Date()).toLocaleString() %> ­­>

如图 6.6②所示,我们可以看到这段注释已经返回给了客户端。

Ø 隐藏注释

语法:

<%­­ comment ­­%>

如程序 comment.jsp中第 12 行:

<%­­ 本注释将不会在客户端出现 ­­%>

JSP 文档在被编译的时候,如果文档中出现隐藏注释的标记,则编译器忽略该标记中所

有的文本。隐藏注释标记中的所有文本也不会显示在客户端的浏览器中。

Ø Java 注释风格

前面说过,JSP 中的 Java 代码段是标准的 Java 程序。因此,Java 的注释方法也可以在 JSP 中使用,它也分为两种写法:

1)单行注释,语法:

// comment

如例程 comment.jsp中第 17 行:

// 获得系统的当前时间

2)多行注释,在多行语句的首尾处加上以下“/*”和“*/” ,语法:

/* Any comments */

如例程 comment.jsp中第 20 行~第 25 行:

/* 根据时间来输出不同的问候语

12点前,输出: 上午好; 12点到 18点,输出: 下午好; 18点后,输出: 晚上好. */

Page 221: Java Web动态图表编程

当然,对于 Java 风格的注释不会返回给客户端。

2.模板元素

模板元素是指 JSP 文档中的静态内容,也就是 HTML/XML 代码。如上例中第 6 行~第 11 行、第 15 行、第 34行~第 36行,这些代码都遵循 HTML或者 XML语法。

模板元素的功能有些类似于网页的框架, 决定了网页结构和最终的外观。 模板元素在 JSP 编译的时候,也被编译到 Servlet 里。当客户端请求这个 JSP 文档时,JSP/Servlet 服务器将这

些模板元素原封不动地发送到客户端。

以 JSPStructure.jsp的源文件为例,程序第 6 行~第 9行:

<HTML> <HEAD> <TITLE> JSP文件结构 </TITLE> </HEAD>

是标准的 HTML 代码,也就是模板元素。当 JSPStructure.jsp 被编译成 Servlet 时,上面

的代码会在 Servlet 中,被以下代码所替代(JSPStructure_jsp.java 第 47 行~第 50行):

out.write("<HTML>\r\n"); out.write("<HEAD>\r\n"); out.write("<TITLE> JSP文件结构 </TITLE>\r\n"); out.write("</HEAD>\r\n");

3.脚本元素

脚本元素包括前面介绍的声明(Declaration)、表达式(Expression)和 Scriptlets。

Ø 声明(Declaration)

在 JSP 程序中声明合法的类、方法及变量。 JSP 语法:

<%! declaration; [ declaration; ]+ ... %>

可以一次性声明多个变量和方法,以";"结尾。这些声明要在 Java 中是合法的。如本书

第 4 章中所介绍的 test.jsp文档(chap04\Fig4.1\ Fig.4.1_01\test.jsp)中第 8 行~第 21 行所示。

另外,也可以写成以下形式:

<%! int i = 0; %> <%! int a, b, c; %> <%! TriangleServlet ts = new TriangleServlet(); %>

直接使用在<% @ page %>中,被包含进来的已经声明的变量和方法,不需要对它们进行

重新声明。一个声明仅在一个页面中有效。如果每个页面都需要用到一些声明,最好把它们

写成一个单独的文件,然后使用<%@ include %>或<jsp:include >元素包含进来。

Ø 表达式(Expression)

表示一个符合 JSP 语法的表达式。 JSP 语法:

<% = expression %>

JSPStructure.jsp中第 15 行以及第 17 行所示:

<%= a + b%>

<%= a ­ b%>

表达式元素是表示一个在脚本语言中被定义的表达式。在运行后被自动转化为字符串,

Page 222: Java Web动态图表编程

然后插入到该表达式在 JSP 文件的位置显示。因为表达式的值已经被转化为字符串,所以可

以在一行文本中插入该表达式。

读者在 JSP 中使用表达式时,请注意以下几点:

不能用一个分号(";")来作为表达式的结束符。但同样的表达式用在 Scriptlet 中就需

要以分号来结尾。一个表达式能够变得很复杂,可以由一个或多个表达式组成,这些表达式

的顺序是从左到右。

Ø Scriptlets 包含一个有效的 Java 程序段。 JSP 语法:

<% code fragment %>

JSPStructure.jsp中第 12 行~第 14 行所示:

<% int a = 55, b = 45; %>

以及程序 comment.jsp(chap06\Fig6.2\Fig.6.2_01\)第 27 行~第 32行:

<%if (time <= 12) out.print("上午好!");

else if (time >= 13 && time <= 18) out.print("下午好!");

else out.print("晚上好!");

%>

一个 Scriptlet 能够包含多个 JSP 语句、方法、变量以及表达式。因此,在 Scriptlet 中,

可以完成以下工作:

Ø 声明将要用到的变量或方法。

Ø 编写 JSP 表达式。

Ø 使用任何隐含的对象和任何用<jsp:useBean>声明过的对象。

Ø 编写 Java 代码。

任何文本、HTML 标记、JSP 元素都必须在 Scriptlet 之外。当 JSP 收到客户的请求时, Scriptlet 就会被执行。如果 Scriptlet 有显示的内容,这些显示内容就被存在 out 对象中。

4.指令元素

JSP 指令是 JSP 向 JSP 容器发送的消息。用来设置全局值。如类声明、要实现的方法、

输出内容类型等。不向客户端产生任何输出,只影响当前的 JSP 文件。 JSP 语法:

<%@ page [ language="java" ] [ extends="package.class" ] [ import="package.class | package.*, ..." ] [ session="true | false" ] [ buffer="none | 8kb | sizekb" ] [ autoFlush="true | false" ] [ isThreadSafe="true | false" ] [ info="text" ] [ errorPage="relativeURL" ] [ contentType="mimeType [ ;charset=characterSet ]" | "text/html ;

charset=ISO­8859­1" ] [ isErrorPage="true | false" ] %>

Page 223: Java Web动态图表编程

如:comment.jsp中第 5 行所示:

<%@ page language="java" contentType="text/html;charset=GB2312"%>

此外,还可以写成:

<%@ page import="java.util.*, java.lang.*" %> <%@ page buffer="5kb" autoFlush="false" %> <%@ page errorPage="error.jsp" %>

<%@ page %>指令作用于当前的整个 JSP 页面,同样包括静态的包含文件。但是<% @ page %>指令不能作用于动态的包含文件,比如<jsp:include> 。

我们可以在一个页面中用多个<% @ page %>指令,但是其中的属性只能使用一次。不过

有个例外,那就是 import 属性。因为 import 属性和 Java 中的 import 语句差不多(参照 Java Language),可以在一个 JSP 文档中多次使用 import 属性。就像在一个 Java 文档中,可以用 import 关键字引入多个包是一样的道理。

无论把<% @ page %>指令放在 JSP 文件的任何地方,其作用范围都是整个 JSP 页面。不

过,为了 JSP 程序的可读性,以及培养良好的编程习惯,我们建议读者把它放在 JSP 文件的

顶部。

下面简要介绍上述某些指令元素的功能:

Ø <% @ page import =”java.io.*,java.util.*”%> // 导入包

Ø <% @ buffer=” ”%> // 定义对客户输出流的缓冲模型

Ø <% @ info=” ”%> // 可以使用 servlet.getServletInfo()得到该字符

Ø <% @ isErrorPage=” ”%> // 当前页面是否处理异常或错误

Ø <% @ errorPage=” ”%> // 如果发生错误或异常,应该调用哪一个 JSP页面来处理

Ø <% @ isThreadSafe=” ” %> // JSP文件是否能多线程使用

除此之外,指令元素中还有一个重要的 Taglib 指令。 Taglib 指令用来定义一个标签库及其自定义标签的前缀。 JSP 语法:

<%@ taglib uri="URIToTagLibrary" prefix="tagPrefix" %>

例如:

<%@taglib uri='/WEB­INF/cewolf.tld' prefix='cewolf' %>

<% @ taglib %>指令声明此 JSP 文件使用了自定义的标签,同时引用标签库,指定了它

们标签的前缀。

Ø uri="URIToTagLibrary" Uniform Resource Identifier (URI)根据标签前缀,对自定义的标签进行惟一的命名,URI

可以是以下内容: 1)Uniform Resource Locator(URL)。由 RFC 2396 定义。 2)Uniform Resource Name(URN)。由 RFC 2396 定义。 3)一个相对或绝对的路径。

因此,uri='/WEB­INF/cewolf.tld' 说明该标签库文件位于 Web 应用程序根目录下/WEB ­INF/,标签库文件名为 cewolf.tld。

Ø prefix="tagPrefix" 自定义标签之前的前缀。请不要使用 jsp、jspx、java、javax、servlet、sun 和 sunw 作为

前缀。prefix='cewolf ' 说明该标签前缀为 cewolf。

我们必须在使用自定义标签之前使用<% @ taglib %>指令,而且可以在一个页面中多次

使用,但是前缀只能使用一次。

Page 224: Java Web动态图表编程

提示:关于标签库的使用,我们将在第 8 章开放源代码作品与 Web 图表编程中向读者介

绍。

Page 225: Java Web动态图表编程

5.动作元素

JSP 动作元素是由 XML 语法构成的。它是在请求的处理阶段起作用,影响 JSP 运行的

行为和发送给客户的应答。JSP 规范定义了一系列的标准动作,用 jsp作为前缀。这些标准动

作可以由任意一个符合 JSP/Servlet 规范的容器所提供,而不管容器是如何实现它们的。 JSP 动作元素的语法是参照 XML语法的,所以有两种书写方式:

<prefix:tag attribute=value attribute­list … />

以及:

<prefix:tag attribute=value attribute­list …> … </prefix:tag>

从效果上来说,一个标准动作是能够嵌入到 JSP 页面之中的一个标记。在页面被编译为 Servlet 期间,当 JSP/Servlet 容器遇到该标记时,就用相应于请求的预定义任务的 Java 代码来

代替它。这里我们讨论几个最重要的 JSP 标准动作。

Ø <jsp:param> 为其他标签提供附加信息。 JSP 语法:

<jsp:param name=”paramName” value=”paramValue”/>

附加信息是以“参数名—参数值”成对的方式提供给其他标签的。它与<jsp:include>、 <jsp:forward>以及<jsp:plugin>一起使用。

Ø <jsp:include> <jsp:include>元素允许包含动态文件和静态文件,这两种包含文件的结果是不同的。如

果文件是静态的,那么这种包含只是把包含文件的内容加到 jsp文件中去,该文件不会被 JSP 编译器执行;而如果该文件是动态的,那么这个被包含的文件也会被 JSP 编译器执行。

理论上<% @ include="" %>与<jsp:include>有所不同。我们把<jsp:include>叫做自动刷

新,<jsp:include>包含的内容是动态改变的,它在被执行时才确定。而<% @ include="" %> 与包含的内容是固定不变的,一旦经过编译就保持不变。在使用较高版本的 Tomcat 时,这些

功能是一样的。 JSP 语法:

<jsp:include page="relativeURL | <%= expression%>" flush="true" />

或者:

<jsp:include page="relativeURL | <%= expression %>" flush="true" > <jsp:param name="parameterName"

value="parameterValue | <%= expression %>" />+ </jsp:include>

下面简要介绍上述某些指令元素的功能:

Ø page="relativeURL | <%= expression %>" 参数为一相对路径,或者代表相对路径的表达式。

Ø flush="true" 这里必须使用 flush="true",不能使用 false值。默认值为 false。

Ø <jsp:param name="parameterName" value="parameterValue | <%= expression %>" />+

注意,这里有个“+”号,这是正则表达式的语法,表示该语句在“<jsp:include>” 、

Page 226: Java Web动态图表编程

“</jsp:include>”标记中可以出现 1 次或多次,也就是至少要出现 1 次。<jsp:param>可以传

递一个或多个参数给动态文件。name指定参数名,value指定参数值。

根据上述语法,以下用法都是合法的:

<jsp:include page="drawLine.jsp" /> <jsp:include page="/info/readme.html" /> <jsp:include page="/index.html" />

<jsp:include page="pie.jsp"> <jsp:param name="Python" value="45" /> <jsp:param name="JAVA" value="115" /> <jsp:param name="C#" value="70" /> <jsp:param name="Perl" value="35" /> <jsp:param name="PHP" value="65" />

</jsp:include>

实际上,我们并不能从文件名上判断一个文件是动态的还是静态的,比如 drawData. jsp。

它可能是绘制某些图表的 JSP 页面,也可能只是包含一些绘制图表所需的数据或信息而已,

而并不需要执行。<jsp:include能够同时处理这两种文件,不需要判断此文件是动态的还是静

态的。

Ø <jsp:forward> 允许将请求转发到另一个 JSP,Servlet 或者静态资源文件。根据不同的请求,转发到不

同的程序去处理。 JSP 语法:

<jsp:forward page="relativeURL" | "<%= expression %>" />

或者:

<jsp:forward page="relativeURL" | "<%= expression %>" > <jsp:param name="parameterName"

value="parameterValue | <%= expression %>" />+ </jsp:forward>

下面简要介绍上述某些指令元素的功能:

Ø page="relativeURL | <%= expression %>" <jsp:forward>标签从一个 JSP文件向另一个文件, 传递一个包含用户请求的 request对象。

而<jsp:forward>标签以下的代码,将不能执行。

Ø <jsp:param name="parameterName" value="parameterValue | <%= expression %>" />+ 注意,这里有个“+”号,这也是正则表达式的语法,表示该语句在“<jsp:forward>” 、

“</jsp:forward>”标记中可以出现 1 次或多次,也就是至少要出现 1 次。如果传递多个参数,

则可以在一个 JSP 文件中使用多个<jsp:param>。name指定参数名,value指定参数值。

根据上述语法,以下用法都是合法的:

<jsp:forward page="drawLine.jsp" />

<jsp:forward page="pie.jsp"> <jsp:param name="Python" value="45" /> <jsp:param name="JAVA" value="115" /> <jsp:param name="C#" value="70" /> <jsp:param name="Perl" value="35" /> <jsp:param name="PHP" value="65" />

</jsp:forward>

注意,请求被转到资源文件必须位于同 JSP 发送请求相同的上下文环境之中。每当 JSP/Servlet 服务器碰到该请求时,就会停止执行当前的 JSP 文档,转而执行被转发的资源文

件。 如果使用了非缓冲输出, 在使用<jsp:forward>时就要小心。 如果在使用<jsp:forward>之前,

Page 227: Java Web动态图表编程

jsp文件已经有了数据,那么文件执行就会出错。

Ø <jsp:useBean> 用来实例化 JavaBean,或者定位一个已经存在的 Bean 实例,并且把它赋给一个变量名

(或者 id),并给定一个具体的范围来确定对象的生命周期。 JSP 语法:

<jsp:useBean id="beanInstanceName" scope="page | request | session | application"

class="package.class" | type="typeName" | beanName="package.class | <%= expression %>" | class="package.class" type="typeName"

/>

或者:

<jsp:useBean id="beanInstanceName" scope="page | request | session | application"

class="package.class" | type="typeName" | beanName="beanName | <%= expression %>" | class="package.class" type="typeName"

> other elements

</jsp:useBean>

下面简要介绍上述某些指令元素的功能:

Ø id="beanInstanceName" id是一个大小写敏感的名字, 表示某个 bean的实例。 如果要使用一个已经创建好的 Bean

对象,那么这个 ID 的值必须与原来定义的 ID值一致。

Ø scope="page | request | session | application" scope决定 Bean对象存在的有效范围。Scope有 4 个可选值,默认值是 page。简单说明

如下:

(1)page——表示 Bean对象与到该页面的特定请求相关联。

(2)Request——表示对象与到该页面的特定客户请求相联系。如果请求被使用 <jsp:forward>标准动作发送到其他 JSP,或者使用<jsp:include>动作包含了另外的 JSP,则在

所涉及的 JSP 中,该对象是有效的。

(3)Session——当前会话中,由同一个客户发送的任何请求中,该对象都是有效的。

(4)Application——在同一个 Web 应用程序中,任何的 JSP 页面,该对象都是有效的。

Ø class="package.class " 指定 Bean类路径和类名。该 package.class 的名字也是大小写敏感的。

Ø beanName="beanName" type="typeName" 实例化一个 Bean对象,同时指定该 Bean对象的类型。

Ø type="typeName" 指定 Bean对象的类型。Type可以是一个类本身(class),也可以是该类的父类,或者是

该类的一个接口。

根据上述语法,以下用法都是合法的:

<jsp:useBean id="dP" scope="session" class="com.fatcat.webchart. Draw Parallelogram" />

Page 228: Java Web动态图表编程

<jsp:setProperty name="dP" property="width" value="25"/> <jsp:setProperty name="dP" property="height" value="125"/> <jsp:setProperty name="dP" property="thickness" value="15"/> <jsp:setProperty name="dP" property="alpha" value="45"/>

</jsp:useBean>

Ø <jsp: setProperty>与<jsp: getProperty> <jsp:setProperty>和 useBean 一起工作,主要用来设置 Bean 的简单属性和索引属性。

<jsp:setProperty>标签使用 Bean给定的 setXXXX()方法,在 Bean中设置一个或多个属性值。 JSP 语法:

<jsp:setProperty name="beanName" property="*" | property="propertyName" param="parameterName" | property="propertyName" | property="propertyName" value="propertyValue|<%=expression %>"

/>

或者:

<jsp:setProperty name="beanName" property="*" | property="propertyName" param="parameterName" | property="propertyName" | property="propertyName" value="propertyValue|<%=expression %>"

> </jsp:setProperty >

下面简要介绍上述某些指令元素的功能:

Ø name="beanName" 表示某个已经在<jsp:useBean>中,创建 Bean的实例名字。

Ø property="*" 储存用户在 JSP 中输入的所有值,用于匹配 Bean 中的属性。Bean 中的属性名字必须和

Request 对象中参数名一致。property="*"是一种设置 Bean属性的快捷方式。从客户端传到服

务器上的参数值一般都是字符类型(即 String类),这些字符串为了能够在 Bean中匹配就必

须转换成其他的类型,而转换过程是由 JSP 内在机制自动实现的。

如果 Request 对象的参数值中有空值,那么对应的 Bean属性将不会设定任何值。同样,

如果 Bean中有一个属性没有与之相对应的 Request 参数值,那么该属性也同样不会被设置。

Ø property="propertyName" 使用 Request 中的一个参数值来指定 Bean中的一个属性值。在这个语法中,property指

定 Bean的属性名,而且 Bean属性和 Request 参数的名字应该相同,否则需要用另一种语法,

即指定 param。propertyName名字和 Bean中的 setXXXX 方法中的 XXXX 要完全相同。也就

是说,Bean中如果有 setWidth(int width)方法,那么 propertyName的值就应该是“width” 。

Ø property="propertyName" value="propertyValue|<%=expression %>" 使用指定的值来设定 Bean 的相关属性。这个值可以是字符串,也可以是表达式。如果

该值是一个字符串,它会被自动转换成 Bean 属性的类型。如果它是一个表达式,那么它的

类型就必须和设定的属性值类型一致。

如果参数值为空,那么对应的属性值也不会被设定。此外,不能在一个<jsp:setProperty> 中同时使用 param和 value。

根据上述语法,以下用法都是合法的:

<jsp:useBean id="dP" scope="session" class="com.fatcat.webchart. Draw Parallelogram" />

<jsp:setProperty name="dP" property="baseXPts" value="80"/> <jsp:setProperty name="dP" property="baseYPts" value="125"/>

Page 229: Java Web动态图表编程

<jsp:setProperty name="dP" property="thickness" /> <jsp:setProperty name="dP" property="*">

</jsp:useBean>

注意:如果使用了 property="*",那么 Bean的属性就没有必要按 HTML表单中的顺序排

序。 <jsp:getProperty>和<jsp:setProperty>正好相反,用来获取 Bean 的属性。getProperty 获得

Bean 的属性值都将自动转换为一个字符串对象。如果属性是一个对象,则将调用该对象的 toString方法。其语法与<jsp:setProperty>相似,我们在此略过。

上述内容对于学习 JSP 文档的编写是非常重要的,我们将在以后的例程中展示其具体的

应用。

Ø <jsp: plugin> 执行一个 Applet 或 Bean,有时还必须下载一个 Java插件用于执行它。 JSP 语法:

<jsp:plugin type="bean | applet" code="classFileName" codebase="classFileDirectoryName" [ name="instanceName" ] [ archive="URIToArchive, ..." ] [ align="bottom | top | middle | left | right" ] [ height="displayPixels" ] [ width="displayPixels" ] [ hspace="leftRightPixels" ] [ vspace="topBottomPixels" ] [ jreversion="JREVersionNumber | 1.1" ] [ nspluginurl="URLToPlugin" ] [ iepluginurl="URLToPlugin" ] >

[ <jsp:params> [ <jsp:param name="parameterName"

value="parameterValue | <%= expression %>" /> ]+ </jsp:params> ]

[ <jsp:fallback> text message for user </jsp:fallback> ] </jsp:plugin>

<jsp:plugin>元素用于在浏览器中播放或显示一个对象,典型的对象就是 Applet 和 Bean,

而这种显示是需要浏览器的 Java 插件。

当 JSP 文档被编译,送往浏览器时,<jsp:plugin>元素会根据浏览器的版本被替换成 <object>或者<embed>元素。注意,<object>用于 HTML4.0 规范,<embed>用于 HTML3.2 规

范。

通常<jsp:plugin>元素会指定对象是 Applet 还是 Bean,也会指定 class 的名字、位置。此

外,还会指定从哪里下载这个 Java 插件。

下面简要介绍上述某些指令元素的功能:

Ø type ="bean | applet" 将被执行的插件对象类型,必须指定是 Bean还是 Applet,因为该属性没有默认值。

Ø code="classFileName" 将被 Java插件执行的 Java Class的名字, 必须以.class结尾。 这个文件必须存在于 codebase

属性指定的目录中。

Ø codebase="classFileDirectoryName" 将被执行的 Java Class 文件的目录 (或者是路径), 如果没有提供此属性, 那么<jsp:plugin>

的 JSP 文件的当前目录将被使用。

Ø name="instanceName"

Page 230: Java Web动态图表编程

这个 Bean或 Applet 实例的名字,将在 JSP 其他的地方调用。

Ø archive="URIToArchive, ..." 一些由逗号分开的路径名,这些路径名用于预加载一些将要使用的 class,这可以提高

applet 的性能。

Ø align="bottom | top | middle | left | right" 图形、对象、Applet 的位置,有以下的可选值:

(1)bottom (2)top (3)middle (4)left (5)right Ø height="displayPixels" width="displayPixels" Applet 或 Bean将要显示长宽的值,此值为数字,单位为像素。

Ø hspace="leftRightPixels" vspace="topBottomPixels" Applet 或 Bean显示时在屏幕左右、上下所需留下的空间,单位为像素。

Ø jreversion="JREVersionNumber | 1.1" Applet 或 Bean运行所需的 Java Runtime Environment(JRE)的版本,默认值为 1.1。

Ø nspluginurl="URLToPlugin" Netscape Navigator 用户能够使用的 JRE 的下载地址,此值为一个标准的 URL,如:

http://java.sun.com。

Ø iepluginurl="URLToPlugin" IE 用户能够使用的 JRE 的下载地址,此值为一个标准的 URL,如 http://java.sun.com/

products/plugin/index.html#download。

Ø <jsp:params> [ <jsp:param name="parameterName" value="parameterValue | <%= expression %>" /> ]+ </jsp:params>

向 Applet 或 Bean传送的参数或参数值。可以使用表达式。

Ø <jsp:fallback> text message for user </jsp:fallback> Java 插件不能启动时显示给用户的一段文字, 如果插件能够启动而 Applet 或 Bean不能,

那么浏览器会有一个出错信息弹出。

我们先讨论一个加载 Applet,并传递参数及根据参数绘制饼图的 JSP 源程序 loadApplet.jsp(chap06\Fig6.2\Fig.6.2_02\)。

源程序第 12行~第 18 行:

<% int python = 1 + (int)(Math.random()* 50); int java = 1 + (int)(Math.random()* 50); int cSharp = 1 + (int)(Math.random()* 50); int perl = 1 + (int)(Math.random()* 50); int php = 1 + (int)(Math.random()* 50);

%>

这段 Scriptlets 代码中,定义了 5个整型变量,在 1~50 之间的随机数中产生。

程序第 19 行:

<jsp:plugin type="applet" code="Pie.class" width = "600" height = "500" >

指定当前加载的类型是 Applet,类的名字为 Pie.class,该类位于当前目录下,显示 Applet 的宽度为 600 像素,高度为 500 像素。

Page 231: Java Web动态图表编程

程序第 20 行~第 43行:

<jsp:params> <jsp:param name = "titleFontName" value = "经典行书简"/> <jsp:param name = "titleFontSize" value = "32"/>

<jsp:param name = "chartTitle" value = "Java Web图表设计 JSP版—加载 Applet"/>

<jsp:param name = "bookTitle1" value = "Python"/> <jsp:param name = "bookSales1" value = "<%=python%>"/>

<jsp:param name = "bookTitle2" value = "JAVA"/> <jsp:param name = "bookSales2" value = "<%=java%>"/>

<jsp:param name = "bookTitle3" value = "C#"/> <jsp:param name = "bookSales3" value = "<%=cSharp%>"/>

<jsp:param name = "bookTitle4" value = "Perl"/> <jsp:param name = "bookSales4" value = "<%=perl%>"/>

<jsp:param name = "bookTitle5" value = "PHP"/> <jsp:param name = "bookSales5" value = "<%=php%>"/>

<jsp:param name = "subTitle" value = "计算机编程类图书销售统计图"/> </jsp:params>

向所加载的 Pie.class 传递多个参数。注意,语法必须符合 XML 规范,所有标记必须全

部匹配。

例如,程序第 21 行:

<jsp:param name = "titleFontName" value = "经典行书简"/>

也可写成以下的格式:

<jsp:param name = "titleFontName" value = "经典行书简" > </jsp:param>

我们前面讲过,参数的传递有两种方式:一种是直接在 value=“xxx”中指定,另一种是利

用表达式。程序第 28、31、34、37 及第 40 行采用了表达式,利用前面定义的 5 个整型变量

对 value赋值。

程序第 44 行~第 46行:

<jsp:fallback> <p><h1>无法加载 Applet</h1></p>

</jsp:fallback>

如果在加载 Applet 的过程中出现错误,则会显示一个出错信息: “无法加载 Applet” 。它

是<jsp:plugin>动作的一部分,并且只能在<jsp:plugin>动作中使用。

下面来看看 Pie.java(chap06\Fig6.2\Fig.6.2_02\)的源程序。该程序同本书第 3.3 节所介绍

的 PieNew.java 的结构和用法完全相同,请读者参考该节的相关内容。运行 IE,在地址栏中

输入 http://localhost:8080/chap06/Fig6.2/Fig.6.2_02/loadApplet.jsp,运行结果如图 6.7 所示。

现在再看看执行后的 loadApplet.jsp返回给客户端的代码:

1: <!­­ 2: Fig. 6.02_02_01: loadApplet.jsp 3: 功能: JSP加载 Applet 4: ­­> 5: 6: <HTML> 7: <HEAD>

Page 232: Java Web动态图表编程

8: <TITLE> JSP加载 Applet </TITLE> 9: </HEAD> 10: 11: <BODY>

Page 233: Java Web动态图表编程

图 6.7 JSP加载 Applet

12: 13: <OBJECT classid="clsid:8AD9C840­044E­11D1­B3E9­00805F499D93"

width= "600" height="500"codebase="http://java.sun.com/ products/plugin/1.2.2/jinstall­1_2_2­win.cab#Version=1,2,2,0" >

14: <PARAM name="java_code" value="Pie.class"> 15: <PARAM name="type" value="application/x­java­applet;"> 16: <PARAM name="titleFontName" value="经典行书简"> 17: <PARAM name="titleFontSize" value="32"> 18: <PARAM name="chartTitle" value="Java Web图表设计 JSP版—加载 Applet"> 19: <PARAM name="bookTitle1" value="Python"> 20: <PARAM name="bookSales1" value="36"> 21: <PARAM name="bookTitle2" value="JAVA"> 22: <PARAM name="bookSales2" value="42"> 23: <PARAM name="bookTitle3" value="C#"> 24: <PARAM name="bookSales3" value="11"> 25: <PARAM name="bookTitle4" value="Perl"> 26: <PARAM name="bookSales4" value="24"> 27: <PARAM name="bookTitle5" value="PHP"> 28: <PARAM name="bookSales5" value="27"> 29: <PARAM name="subTitle" value="计算机编程类图书销售统计图"> 30: <COMMENT> 31: <EMBED type="application/x­java­applet;" width="600" height= "500"

pluginspage="http://java.sun.com/products/plugin/"java_ code="Pie.class" titleFontName="经典行书简" titleFontSize= "32"chartTitle="Java Web图表设计 JSP版—加载 Applet" bookTitle1= "Python" bookSales1="36" bookTitle2="JAVA" bookSales2="42" bookTitle3="C#" bookSales3="11" bookTitle4="Perl" bookSales4= "24" bookTitle5="PHP" bookSales5="27" subTitle="计算机编程类图书销售统计图 "/>

32: <NOEMBED> 33: 34: <p><h1>无法加载 Applet</h1></p> 35: 36: </NOEMBED> 37: </COMMENT> 38: </OBJECT> 39: 40:

Page 234: Java Web动态图表编程

41: </BODY> 42: </HTML>

可以看出,JSP/Servlet 服务器把<jsp:plugin>指令相关的内容,翻译成了浏览器可以识别

的插入代码,通过<jsp:plugin>,使得插入的组件具有更好的动态特征。

6.3 JSP 调用 Servlet 生成动态图表

本节将通过几个例程来演示 JSP 如何调用 Servlet 来生成 Web 动态图表。本节涉及的 Servlet,将采用第 5 章所介绍的 Servlet。

6.3.1 JSP 生成验证码

程序 invokeServlet.jsp(chap06\Fig6.3\Fig.6.3_01)演示了如何在 JSP 中调用 Servlet 生成 Web 图表,运行结果如图 6.8 所示。

图 6.8 JSP调用 Servlet

调用方法同我们以前介绍的在 HTML 文档中,调用 Servlet 生成 Web 图表是一样的。

invokeServlet.jsp 里面调用了两个不同的 Servlet,分别

在程序第 14行:

<img src="/codeMaker" /><br />

以及程序第 19 行:

<img src="/imageCodeMaker" /><br />

我们看到,调用方法实际上是利用 HTML 显示图像的

语法。invokeServlet.jsp的运行结果如图 6.8 所示。

6.3.2 JSP 生成甘特图

程序 invokeGanterServlet.jsp (chap06\Fig6.3\Fig.6.3_ 02)

也是调用了两个 Servlet 来生成甘特图,运行结果如图 6.9 所示。

图 6.9 JSP调用 Servlet

Page 235: Java Web动态图表编程

本例和前例不同的是,在本程序第 14 行、第 19行。我们将由 Servlet 生成的甘特图默认

的宽度和高度修改为 265 像素和 240 像素。也就是说,在 HTML中所有<img>图像标记中可

用的属性都可以应用在 Servlet 生成的图像中。

6.3.3 JSP 其他相关知识

这里我们向读者简要介绍一些有关 JSP 的编程要点。

1.JSP 中文乱码问题

在 JSP 的应用开发中,JSP 的中文乱码一直是一个困扰众多开发者的问题。该问题的产

生和解决方法,同本书第 5.3 节所介绍的 Servlet 中文乱码问题的相关知识完全相同。这里就

不再对此问题进行讨论。

对于不同的 JSP/Servlet Web 服务器、同一个 JSP/Servlet Web 服务器的不同版本,以及不

同的 JDK版本对中文的处理可能不同。在执行相同的 JSP 页面文档时,可能会出现乱码的情

况。解决方法前面已经阐述,此处不再赘述。

2.JSP 内部对象概述

JSP 为简化 Web 应用程序的开发提供了一些内部对象。 内部对象包括: request、 response、 out、session、pageContext、application、config、page、exception等。这些内部对象不需要由

开发者实例化,它们是由 JSP/Servlet 容器管理来实现的,在所有的 JSP 页面中都能够使用内

部对象。

这些对象同我们前面所讲述的 Servlet 相关对象的作用及用法是一致的。 我们简要讨论其

中几个内部对象。

Ø out 对象 out 对象被封装成 javax.servlet.jsp.JspWriter 接口。它主要用于向客户端发送输出流,也

就是说,由 out 对象向客户端输出数据。 out 是 JSP 中使用最频繁的对象。主要是调用 out 对象的 print 和 println方法。两者不同

之处在于,println 方法在输出内容后,将在内容的结尾处自动添加一个换行符,即: “\n” 。

但换行符“\n”在 HTML语法中是被忽略的。如果要在 HTML页面上体现换行的效果,就必

须明确使用 out.println(“<br \>”)或者 out.print(“<br>”)来实现。换行符“\n”被 HTML 语法所忽略,但在 HTML的源代码中,我们可以看到,只要是调用了 out.println方法,下一

条源代码将会出现新的一行被输出。如果调用的是 out.print 方法,即下一条源代码将会紧接

着当前源代码的最后位置被输出。

Ø request 对象 request 对象代表请求对象,它继承于 HttpServletRequest 接口。来自客户端的请求经

JSP/Servlet 容器处理后,再由 request 对象进行封装。它作为 jspService 方法的一个参数由容

器传递给 JSP 页面。

request 对象的相关方法,读者可以参考我们第 5章讲述的 Servlet 的相关内容。

Ø response对象 response 对象代表响应对象,它继承于 HttpServletResponse 接口。它封装了 JSP 产生的

响应,然后将 response对象发送到客户端以响应客户的请求。因为输出是缓冲的,所以可以

Page 236: Java Web动态图表编程

设置 HTTP 状态码和 response头信息。 response对象的相关方法,读者可以参考我们第 5 章讲述的 Servlet 相关内容。

Ø session对象 session对象用在不同页面之间保存共享的信息。一般用于保存每个客户端的信息,以便

跟踪每个用户的操作状态。session的 ID 保存在客户端的 Cookie中;session的信息保存在服

务器中。

session对象和客户端的会话紧密联系在一起,它由 JSP/Servlet 容器自动创建。 session对象的相关方法,读者可以参考我们第 5章所讲述的 Servlet 相关内容。

Ø application对象 application对象为多个 Web 应用程序保存共享信息。对于某一个 JSP/Servlet 容器来说,

每个用户都共同使用一个 application对象,也就是对每个客户端而言,操作的是同一个对象。

而 session对象,相对于不同的客户端是各自独立的。application对象有些类似于 Java 语法中

的 static类型的数据对象,而 session有些类似 Java 语法中 private类型的数据对象。

如果创建了 application 对象,只要服务器没有关闭或者重新启动,该 application 对象就

会一直保持。而 session对象可以在强制其失效或注销,也可以在超过其默认的失效时间后,

自动注销。这也是两者的主要区别之一。

此外,其他 JSP 的内部对象如:pageContext 被封装成 java.servlet.jsp.PageContext 接口,

它为 JSP 页面包装页面的上下文;config对象被封装成 javax.servlet.ServletConfig接口,它表

示 Servlet 的配置、exception对象是 java.lang.Throwable类的一个实例,它指的是运行时的异

常,在处理错误页面时使用 exception对象。

6.4 JSP 生成基本动态图表

JSP 的绘图功能是由 Java 抽象化窗口工具包 (AWT, Abstract Windows Toolkit) 所支持的。

该包中包含一些子类,如 Font 类,其包含了用于操作字体的方法和一些常量;Color 类,包

含用于操作颜色的方法和常量;Graphics 类,包含了绘制字符串、直线、矩形,以及其他形

状图形的方法。Toolkit 类,提供了从系统中获取可显示字体集等信息的方法。这些类及其一

些方法我们在讲述 Applet 编程的时候已经讨论过了。

在 JSP 程序中,通过引用 Java 抽象化窗口工具包中的 Graphics 类及其方法并结合 Font、 Color 类,就可以轻松地在程序中创建图文并茂、丰富多彩的图表了。

在了解了上述 JSP 的优点、页面结构、运行机制及其相关知识后,就可以用 JSP 来开发

基于 Web 的动态图表应用程序了。同样,任何复杂的图表都可以分解成基本的图表。因此,

掌握 JSP 绘制基本图表是非常重要的。我们在第 2章以及第 3 章,通过 Applet 向读者介绍了

基本的 Web 图表绘制方法。 本节将把第 2 章使用 Applet 实现的 Web 图表改写成用 JSP 实现,

此外,再扩充一些基于基本图形的应用。

6.4.1 JSP 绘制文本和线段

我们还是从一个简单的 basic.jsp(chap06\Fig6.4\Fig.6.4_01)图表生成程序开始讲述,在 JSP 中如何引用 AWT包中的类及方法来生成一些简单的基本图形, 以及如何将生成的图形发

布到 Web 页面上。

类似于我们介绍过的 Servlet,当一个返回给 Web 页面,带有 image/jpeg(或者其他的图

Page 237: Java Web动态图表编程

像格式)的 MIME 类型被发送时,客户端浏览器将返回结果当做一个图像,然后浏览器显示

该图像作为页面的一部分或者完全作为图像自身。为设置这个 JSP 页面 MIME 的类型,需要

设置 contentType属性,如 basic.jsp源程序第 5 行所示:

<%@ page language="java" contentType="image/png;charset=GB2312"

为正确地显示中文,我们制定编码方式为 GB2312。

程序第 6 行~第 8 行:

import="java.awt.image.*" import="java.awt.*" import="javax.imageio.*"

引入我们熟悉的 Java 图像处理的相关包。请读者注意程序第 8 行,我们仍然没有引进 Sun 的 JPEG 特殊类——com.sun.image.codec.jpeg.*。因为我们认为采用该包编写的程序的通

用性不够好。这些代码在 com.sun 包中,不是 API 核心的一部分,也不是标准的扩展包。 JPEGImageEncoder 是 Sun 用来实现 JRE 某些功能所使用的特殊的 class。因此,如果对图像

的编码采用 JPEGImageEncoder 类,在其他的一些 JRE 上运行会发生异常,影响代码的移植

性。与前一章 Servlet 相同,我们仍然利用 J2SE 5.0 的新特性,来对缓冲图像的内容进行解码

及编码的工作。这样,代码会更简洁,性能更强大,移植性更好。程序第 8 行,引入了 Java 对图像处理的新包。javax.imageio 包默认可以读入一个 GIF、PNG、JPEG 和 BMP 格式的图

像(注意:J2SE1.4 版中也提供了该包,但是不支持对 BMP 格式图像的读写操作),以及输

出一个 PNG、JPEG 和 BMP 格式的图像,并不支持输出 GIF 格式的图像。原因是 GIF 格式

的图像是有版权的,所以 Sun公司在其有关图像处理的包中,只提供了对 GIF 格式图像的读

入功能。 也就是说, 只提供了对 GIF 图像的解码功能, 而没有提供对 GIF 格式图像的输出 (即 GIF 图像的编码)功能。本程序将使用该包中的 ImageIO 类,来对缓冲图像按指定的图像格

式进行编码操作。

程序第 13 行:

response.reset();

注意,JSP 的方法体中第 1 条语句,通过 response 调用了 Response 类的 reset 方法,该

方法继承于 ServletResponse。用于清空 response里的缓冲区的内容。在 JSP 代码中,第 1 条

语句,需要调用 reset 方法,原因和在 Servlet 中介绍的完全相同。

程序第 16 行:

response.setContentType("image/png");

使用 response 对象的 setContenType 方法,设置返回给客户端的响应数据内容的类型为

图像类,图像的格式为 png 格式。我们讨论过,客户端浏览器是根据服务器返回文档类型

(MIME)来处理文档内容的。本例中,返回数据内容的类型为 image/png,它通知浏览器响

应一个格式为 PNG 的图像。于是,客户端浏览器就知道它必须读取该图像,然后将图像显

示在浏览器中。

如果没有程序第 16行 response的 reset 方法,而只有程序第 5 行的 setContenType方法,

即使我们指明了返回的数据类型为 image/png,实际上,JSP/Servlet 服务器默认还是返回的 text/html。只有当执行到程序第 16 行的 setContentType("image/png")时,且 response 的缓

冲区为空时,JSP/Servlet 服务器才会返回 image/jpeg 的图像格式。

但是,无法保证 JSP/Servlet 服务器在执行到 response 的 setContentType("image/png")方

法时,response 缓冲区的内容为空。即使生成的图片内容和格式都没有问题,因为返回的数

据类型和数据的实际内容不一致,在客户端上也不会有正确的显示。

Page 238: Java Web动态图表编程

前面介绍过,某些 JSP/Servlet 服务器比较“聪明” ,例如 Tomcat,当遇到以下的语句:

<%@ page contentType="image/png" %> 或 response.setContentType("image/png");

将会自动调用 response 的 reset 方法。这样,即使没有在程序中显示的声明 response 的 reset 方法,还是可以得到正确的输出结果。

Resin服务器在这个问题上, 不同的版本有着不同的输出结果。 Resin的某些版本与Tomcat 类似,也会自动调用 response的 reset 方法。而 Resin 的某些版本却非常忠于源程序,不会自

动调用 response的 reset 方法,所有输出结果就不正确。

经我们测试,BEA公司的 Weblogic服务器也不会自动调用 response的 reset 方法,所以

如果缺少了 response的 reset 方法,就不能得到正确的结果。

综上所述,我们建议读者在编写 JSP/Servlet 的 Web 图表程序时,在源程序中,首先调用 response的 reset 方法, 强制清空 response缓冲区的内容。 这样, 可以保证在所有的 JSP/Servlet 服务器上,运行 Web 图表源程序时获得正确的结果。

程序第 19 行~第 23行:

int width=640, height=480;

BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);

Graphics g=image.getGraphics();

程序第 19 行,设定该图像的宽为 640 像素,高为 480 像素,程序第 21 行~第 22 行,

创建一个 BufferedImage的实例——image来绘制动态图像, 然后需要得到图形环境进行绘制。

程序第 16 行,使用 getGraphics()的方法创建了一个 Graphics 对象 g。现在我们就可以绘制图

像内容了。由于 JSP/Servlet 中的缓冲图像的默认背景及绘图颜色为黑色。因此,我们使用一

个与缓冲图像的宽度、 高度都相同的白色矩形作为缓冲图像的背景图案。 见程序第 26 行~第 27 行:

g.setColor(Color.WHITE); g.fillRect(0, 0, width, height);

程序第 30 行,重新设定当前的绘制颜色为红色,然后程序在第 39 行~第 47 行,调用 drawLine方法,分别绘制了 11 条直线、一个三角形和一个矩形。接着,程序在第 49 行~第 50 行,重新设定字体,并调用 drawString方法绘制字符串。

程序第 53 行:

g.dispose();

完成图像的绘制工作。调用 Graphics 对象 g 的 dispose 方法来部署(dispose)这个图形

环境。

程序第 56 行~第 58行:

ServletOutputStream sos = response.getOutputStream(); ImageIO.write(image, "PNG", sos); sos.close();

使用 response 的 getOutputStream方法,创建一个 ServletOutputStream对象 sos,然后利

用 javax.imageio包中的 ImageIO类的静态 write方法来编码图像, 生成一个 PNG格式的图像。 ImageIO类有多个不同版本的write方法, 都用于对图像进行指定格式的编码或格式转换操作。

本例中调用的 write方法,需要接收三个参数:

Ø 一个实现了 RenderedImage 接口图像的绘图环境对象。该绘图环境可以从一个文件、

Page 239: Java Web动态图表编程

输入流或 URL 中获取图像的绘图环境得到。本例中图像的绘图环境对象,就是缓冲

图像 image。

Ø 指定输出图像格式的字符串对象。本例指明图像的输出格式为“PNG”格式。

Ø 一个实现了 ImageOutputStream 接口的输出流对象。本例中实现该接口的对象就是 ServletOutputStream类的对象 sos。

最后调用 ServletOutputStream对象 sos 的 close方法,关闭输出流并结束本 JSP。

启动浏览器,通过 http://localhost:8080/chap06/Fig6.4/Fig.6.4_01/basic.jsp来访问本 JSP 文

件,运行结果如图 6.10 所示。

图 6.10 JSP Web图表的绘制

如果要输出其他格式的图像,以输出“JPG”格式图像为例。需要改动以下几个地方:

程序第 5 行:

<%@ page language="java" contentType="image/png;charset=GB2312"

重新设置为:

<%@ page language="java" contentType="image/jpeg;charset=GB2312"

程序第 16 行:

response.setContentType("image/png");

重新设置为:

response.setContentType("image/jpeg");

程序第 54 行:

ImageIO.write(image, "PNG", sos);

重新修改为:

ImageIO.write(image, "JPG", sos);

现在, 不需要重新启动 Tomcat 服务器, 直接在 IE 中刷新就可以看到程序修改后的结果。

如果希望编码成“BMP”格式的图像,修改方法相同,此处不再赘述。

读者可以进行测试,如果以“JPEG”的格式输出,图像质量与“PNG”相比,差距很大,

没有 PNG 格式的图像清晰。此外,我们对保存下来的 basic.jpg 同 basic.png 的文件容量进行

比较:

在本机上,由 basic.jsp生成的 basic.jpg的文件容量大小为 14.4K 字节;而 basic.png只有 3.51K 字节。PNG格式的显示效果远比 JPG 格式要好,而且生成的图像文件大小也远远低于

Page 240: Java Web动态图表编程

JPG 格式。

所以,我们也建议读者采用 PNG 格式,作为图表最终的编码格式。基于这个原因,本

书也一直采用 PNG 格式作为我们的编码格式。

下面总结一下 JSP 绘图的基本步骤:

(1)设置返回的 MIME 为 image类型,格式可为 JPEG、BMP、PNG之一。

(2)调用 Response类的 reset 方法,强制清空缓冲区。

(3)创建缓冲图像对象并获得其绘图环境。

(4)调用 java.awt 等 Java 图像类库进行绘制。

(5)调用 dispose方法,部署图像。

(6)将绘制完成的缓冲图像编码,最后以(jpg/jpeg/bmp/png)图像的格式,返回给发

出请求的客户端。

绘制 JSP 图表的代码, 类似前面介绍的 Servlet 绘制图表的 doGet、 doPost 方法中的代码。

使用 JSP 开发 Web 图表程序,其优点也是不言而喻的:

Ø 一个 JSP 文件就可以同时响应客户端的 Get 和 Post 请求。

Ø 不用重新启动 JSP/Servlet 服务器, 修改源文件后, 刷新浏览器即可获得修改后的结果。

Ø 不需要修改 web.xml 文档。

在 Tomcat、Resin 服务器中,basic.jsp 程序已经正常工作了。我们再测试一下 basic.jsp 在 Weblogic中是否运行正常。

如果要在 Weblogic中运行 basic.jsp程序,首先需要将其打包,然后部署在Weblogic中。

按照我们第 4 章介绍的步骤,为避免混乱,我们首先将 Weblogic_Builder 下所有文件删除掉,

然后将 D:\webchart\chap06\Fig6.4\Fig.6.4_01 目录下的 basic.jsp 文件拷贝到 Weblogic_Builder 下。在 Weblogic_Builder 目录下,新建一个子目录 WEB­INF,在 WEB­INF 子目录下,新建

一个名为 web.xml 的文档,内容如下:

<?xml version="1.0" encoding="GB2312"?>

<!DOCTYPE web­app PUBLIC "­//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/j2ee/dtds/web­app_2_2.dtd">

<web­app> <display­name>Basic JSP</display­name> <description>

Basic JSP </description>

</web­app>

进入命令行控制台,在 Weblogic_Builder 目录下执行以下命令:

jar cvf basic.war *.*

将 Weblogic_Builder 目录下所有的文件打包成 basic.war 文件。步骤如下:

(1)登录 Weblogic的控制台。

(2)依次单击【mydomain】→【Deployments】→【Web Application Modules】→【Deploy a new Web Application Module…】。

(3)单击【Upload Your File(s)】→【浏览】,选择打包文件 basic.war,单击【Upload】

按钮,返回先前的页面,然后再单击【myserver】。

(4)单击【upload】→【basic.war】→【Target Module】。

(5)在出现的页面中单击【Deploy】选项,完成部署。

现在启动浏览器,在地址栏中输入:

Page 241: Java Web动态图表编程

http://localhost:7001/basic/basic.jsp

将会看到如图 6.11 所示的结果。

在 Weblogic中部署其他类似的 JSP 文档时,读者也可以参照此方法。

图 6.11 Weblogic下 basic.jsp的运行结果

如果读者还是希望利用 com.sun.image.codec.jpeg包来对图像进行编码工作,则需要修改

本程序为:

Ø 引入 com.sun.image.codec.jpeg包,如:

import="com.sun.image.codec.jpeg.*"

Ø 程序第 57 行,修改为:

JPEGImageEncoder encoder = JPEGCodec.createJPEGEncoder(sos); encoder.encode(image);

6.4.2 JSP 与字体控制

我们在第 2 章介绍 Applet 的时候,讨论了一些 Java 关于字体控制的一些方法。此前,

我们使用的字体基本都是普通(plain)、斜体(italic),以及粗体(bold)三种风格。是否只

能使用这几种风格的字体呢?其实,只需要将字体的应用扩展一下,就可以设计出很多丰富

多彩的视觉效果。为达到此目的,我们需要了解关于某种字体的精确信息,如图 6.12 所示。

图 6.12 字体规格

字体的高度(height)、下降值(descent,即字符低于基线的部分)、上升值(ascent,即

字符高于基线的部分)、超出值(leading,即字符高度减去下降值和上升值的剩余部分)等。 Java 在绘制文本时,坐标是以字符的基线为标准的。

在 FontMetrics 类中, 定义了一些获取字体精确信息的方法。 FontMetrics 类继承于 Object 类,隶属于 java.awt 包。表 6.1 列出了该类,以及 Graphics 类中,一些获取字体精确信息的

方法。

表 6.1 FontMetrics类及Graphics类的相关方法

超出值

上升值

基线 下降值

Page 242: Java Web动态图表编程

public Font getFont() // FontMetrics class

返回当前字体对象

public int getAscent() // FontMetrics class

返回用像素表示字体基线的上升值

public int getDescent() // FontMetrics class

返回用像素表示字体基线的下降值

public int getLeading() // FontMetrics class

返回用像素表示字体的超出值

public int getHeight() // FontMetrics class

返回用像素表示字体的高度

public int stringWidth(String str) // FontMetrics class

返回一个表示当前字体下,给定的整个字符串长度的整数值

public FontMetrics getFontMetrics() // Graphics class

返回一个表示当前字体的 FontMetrics对象

public FontMetrics getFontMetrics(Font font) // Graphics class

返回一个表示指定字体的 FontMetrics对象

fontInfo.jsp(chap06\Fig6.4\Fig.6.4_02)演示了如何使用上述方法,运行结果如图 6.13 所

示。

程序第 30 行~第 32行:

Font[] font = new Font[2]; font[0] = new Font("方正粗宋简体", Font.PLAIN, 20); font[1] = new Font("华文隶书", Font.BOLD, 18);

定义了一个 Font 数组,并对其进行初始化。

程序第 33 行,声明了一个字符串包含“欢迎来到 Java Web 图表编程的世界”的一段文

本。程序第 36 行,定义了 5 个整型变量,分别表示字体的高度、下降值、上升值、超出值等,

其中变量 stringLength表示一个字符串中所有字符的宽度。

Page 243: Java Web动态图表编程

图 6.13 获得字符的精确信息

程序第 37 行~第 56行,通过一个循环,获得当字体不同时,相应字体的高度、下降值、

上升值、超出值以及字符串 title在应用于不同的字体时,整个字符串宽度的变化情况。

程序第 39 行~第 40行:

g.setFont(font[i]); ascent = g.getFontMetrics().getAscent(); descent = g.getFontMetrics().getDescent(); fontHeight = g.getFontMetrics().getHeight(); leading = g.getFontMetrics().getLeading(); stringLength = g.getFontMetrics().stringWidth(title);

通过 Graphics 对象 g 的 getFontMetrics 方法,获得一个 FontMetrics 对象,然后调用 FontMetrics 对象的 getAscent、getDescent、getHeight 和 getLeading方法,获得字符的高度、

下降值、上升值和超出值,这些方法都不需要任何参数。但是 stringWidth方法,需要一个字

符串参数,返回当前字体下该字符串的宽度。

程序第 47 行~第 55行,绘制获得的这些信息。

从图 6.13 中我们可以看到,当字体改变时,字符串对象 title的宽度也发生了变化。第一

次宽度是 312 像素,第二次是 282 像素。

了解如何获得字体精确信息后,现在我们把字体的应用加以扩展,讨论如何设计出丰富

多彩的视觉效果。

首先绘制一段文本,在紧靠该文本再次绘制相同内容的文本。每次绘制时,调用的色彩

是经过精心选择的。 对于绘图区域中任意位置的一个像素来说, 其周围共有 8个点, 如图 6.14 所示。

西北(Northwest)

坐标:X−1,Y−1

北(North)

坐标:X,Y−1

东北(Northeast)

坐标:X+1,Y−1

西(West)

坐标:X−1,Y

中央(Center)

坐标:X,Y

东(East)

坐标:X+1,Y

西南(Southwest)

坐标:X−1,Y+1

南(South)

坐标:X,Y+1

东南(Southeast)

坐标:X+1,Y+1

图 6.14 点的位置

假设绘图区域中,任意位置的一个像素坐标为 x,y。则其周围 8个点的坐标分别为:

Ø 左上角点(西北方)的坐标:x−1,y−1。

Page 244: Java Web动态图表编程

Ø 左边点(正西方)的坐标:x−1,y。

Ø 左下角点(西南方)的坐标:x−1,y+1。

Ø 上方点(正北方)的坐标:x,y−1。

Ø 下方点(正南方)的坐标:x,y+1。

Ø 右上方点(东北方)的坐标:x+1,y−1。

Ø 右边点(正东方)的坐标:x+1,y。

Ø 右下角点(东南方)的坐标:x+1,y+1。

下面的例程 fontEffect.jsp(\chap06\Fig6.4\Fig.6.4_02)演示了如何实现字体的多种视觉效

果,运行效果,如图 6.15 所示。

图 6.15 字体效果

从运行结果中可以看出,一共生成了 7种视觉效果的字体。

首先,fontEffect.jsp程序第 11 行~第 31 行:

<%! int ShiftNorth(int p, int distance)

return (p ­ distance);

int ShiftSouth(int p, int distance)

return (p + distance);

int ShiftEast(int p, int distance)

return (p + distance);

int ShiftWest(int p, int distance)

return (p ­ distance); %>

声明了 4 个方法,这些方法计算出了绘图区域中,任意一个点向其正东方、正西方、正

Page 245: Java Web动态图表编程

南方、正北方移动 n个像素后的位置。本程序中实现的各种视觉效果,即是反复调用这些方

法来实现的。

程序第 48 行~第 50行:

Color bg = new Color(150, 150, 150); g.setColor(bg); g.fillRect(0, 0, width, height);

调用 Color 的构造器,创建了一个 Color 类的实例 bg,bg的 RGB 值同为 150,实际的效

果是浅灰色。

程序第 53 行~第 58行:

int x = 10, y = 100; g.setFont(new Font("方正粗宋简体", Font.PLAIN, 35)); g.setColor(new Color(50, 50, 50)); g.drawString("欢迎来到 Java Web图表编程的世界", ShiftEast(x, 2),

ShiftSouth(y, 2)); g.setColor(new Color(220, 220, 220)); g.drawString("欢迎来到 Java Web图表编程的世界", x, y);

本段代码实现文本的阴影效果,如图 6.15①所示。首先重新设置当前字体对象为“方正

粗宋简体” 、风格为普通字体、大小为 35 像素,然后设置当前颜色(new Color(50,50,50)) 。

之后,在坐标(12,102)处绘制文本“欢迎来到 Java Web 图表编程的世界” 。这里的坐标

(12,102),是由程序第 56 行中分别调用 ShiftEast 和 ShiftSouth 得到。接着改变当前颜色

为(new Color(220,220,220))所代表的颜色。最后,在坐标(10,100)处,再次绘制相同

内容的文本“欢迎来到 Java Web 图表编程的世界” 。因为两次绘制的文本内容相同,颜色相

异且位置有轻微的错位(第一次绘制文本的位置,距离第二次绘制文本的位置为东南方两个

像素),最终两段文本重叠在一起就产生了阴影字体的效果。

程序第 61 行~第 66行:

x = 10; y = 150; g.setColor(new Color(220, 220, 220)); g.drawString("欢迎来到 Java Web图表编程的世界", ShiftEast(x, 1),

ShiftSouth(y, 1)); g.setColor(new Color(50, 50, 50)); g.drawString("欢迎来到 Java Web图表编程的世界", x, y);

本段代码实现文本的浮雕效果, 如图 6.15②所示。 方法与生成阴影效果的文本基本相同,

用不同的颜色,在不同的位置绘制相同内容的文本。

程序第 69 行~第 77行:

x = 10; y = 200; g.setColor(Color.RED); g.drawString("欢迎来到 Java Web图表编程的世界", ShiftWest(x, 1),

ShiftNorth(y, 1)); g.drawString("欢迎来到 Java Web图表编程的世界", ShiftWest(x, 1),

ShiftSouth(y, 1)); g.drawString("欢迎来到 Java Web图表编程的世界", ShiftEast(x, 1),

ShiftNorth(y, 1)); g.drawString("欢迎来到 Java Web图表编程的世界", ShiftEast(x, 1),

ShiftSouth(y, 1)); g.setColor(Color.YELLOW); g.drawString("欢迎来到 Java Web图表编程的世界", x, y);

本段代码实现文本的轮廓效果,如图 6.15③所示。

程序第 72 行~第 75行,实际执行的是:

Page 246: Java Web动态图表编程

g.drawString("欢迎来到 Java Web图表编程的世界", 9, 199); g.drawString("欢迎来到 Java Web图表编程的世界", 9, 201); g.drawString("欢迎来到 Java Web图表编程的世界", 11, 199); g.drawString("欢迎来到 Java Web图表编程的世界", 11, 201);

以点(10,200)为中心,分别在距其西北方、西南方、东北方、东南方 1 像素的位置,

用红色绘制同样内容的一段文本。然后程序用黄色在点(10,200)处,绘制相同内容的一段

文本。这五段文本重叠在一起,就绘制出了文本轮廓的视觉效果。

程序第 80 行~第 88行:

x = 10; y = 250; g.setColor(Color.BLACK); g.drawString("欢迎来到 Java Web图表编程的世界", ShiftWest(x, 1),

ShiftNorth(y, 1)); g.drawString("欢迎来到 Java Web图表编程的世界", ShiftWest(x, 1),

ShiftSouth(y, 1)); g.drawString("欢迎来到 Java Web图表编程的世界", ShiftEast(x, 1),

ShiftNorth(y, 1)); g.drawString("欢迎来到 Java Web图表编程的世界", ShiftEast(x, 1),

ShiftSouth(y, 1)); g.setColor(bg); g.drawString("欢迎来到 Java Web图表编程的世界", x, y);

本段代码实现文本的空心字体效果,如图 6.15④所示。实现原理和生成轮廓效果完全一

样,只是最后一次绘制文本时,将字体的颜色设定为当前背景色(程序第 87 行)。

程序第 91 行~第 102行:

x = 10; y = 300; int w = (g.getFontMetrics()).stringWidth("欢迎来到 Java Web图表编程的世界

"); int h = (g.getFontMetrics()).getHeight(); int d = (g.getFontMetrics()).getDescent(); g.setColor(new Color(220, 220, 220)); g.drawString("欢迎来到 Java Web图表编程的世界", x, y); g.setColor(bg); for (int i = 0; i < h; i += 3) g.drawLine(x, y + d ­ i, x + w, y + d ­ i);

本段代码实现文本的百叶窗效果,类似于 IBM 公司的 logo,如图 6.15⑤所示。首先绘制

一段文本,获得该文本的精确信息,如整个文本的宽度、高度以及下降值等等。从该文本的

底部开始到文本的高度结束,每隔一定的间隔,绘制一条和该文本宽度相同的直线。

程序第 93 行~第 95行,先调用 g的 getFontMetrics 方法,获得 FontMetrics 对象后,再

调用 FontMetrics 对象的 stringWidth、getHeight,以及 getDescent 方法来获取字符串“欢迎来

到 Java Web 图表编程的世界”的精确信息,如整个字符串的宽度、高度及下降值等。程序在

第 97 行,绘制了该文本。最后,程序在第 99 行~第 102 行的循环中,每隔 3 像素绘制一条

直线,该直线的宽度被设置成与字符串“欢迎来到 Java Web 图表编程的世界”的宽度相同。

程序第 105行~第 120 行:

x = 10; y = 350; Color topColor = new Color(200, 200, 0); Color sideColor = new Color(100, 100, 0); for (int i = 0; i < 5; i++) g.setColor(topColor);

Page 247: Java Web动态图表编程

g.drawString("欢迎来到 Java Web图表编程的世界", ShiftEast(x, i), ShiftNorth(ShiftSouth(y, i), 1));

g.setColor(sideColor); g.drawString("欢迎来到 Java Web图表编程的世界", ShiftWest(ShiftEast(x, i),

1),ShiftSouth(y, i)); g.setColor(Color.YELLOW); g.drawString("欢迎来到 Java Web图表编程的世界", ShiftEast(x, 5),

ShiftSouth(y, 5));

本段代码实现文本的三维立体效果,如图 6.15⑥所示。原理与前面介绍的一样,都是绘

制一系列重叠在一起的文本。需要注意的是,这段代码核心部分在循环体中,实现绘制字体

顶端颜色及其侧面颜色。最后,用黄色绘制相同的文本。

程序第 123行~第 136 行:

x = 300; y = 400; int fontSize = 0;

for (int i = 0; i < 20; i++)

fontSize = 12+i; g.setFont(new Font("方正粗宋简体", Font.PLAIN, fontSize)); w = g.getFontMetrics().stringWidth("欢迎来到 Java Web图表编程的世界"); g.setColor(new Color(0, 65+i * 10, 0)); g.drawString("欢迎来到 Java Web图表编程的世界", (width ­ w) / 2,

ShiftSouth(y,2 * i));

本段代码实现文本的运动字体效果,如图 6.15⑦所示。实现原理是,在一段循环中,每

循环一次,则改变字体的大小、颜色及绘制坐标。然后调用当前字体对象,颜色对象及坐标

绘制文本。注意,在这里设定字符串绘制坐标居中的方法是:(width−w)/2。即使用缓冲图像

的宽度减去整个字符串的宽度,将得到两者的差再除以 2,并以该值作为字符串绘制参数中

的横坐标,这样就实现了字符串居中绘制的功能。

关于基本几何图形的绘制,读者应该非常熟悉了。若没有新的内容,我们将不再对源代

码作出解释,仅罗列出源代码及其运行结果。关于源代码的解释请读者参考本书第二章的相

关内容。

6.4.3 JSP 绘制矩形

1.JSP 绘制普通矩形

drawRect.jsp (chap06\Fig6.4\Fig.6.4_03\) 演示了如何绘制普通矩形, 其运行结果如图 6.16 所示。源代码的讲解此处不再赘述。

Page 248: Java Web动态图表编程

图 6.16 drawRect.jsp的运行效果

2.JSP 绘制 3D 矩形

draw3DRect1.jsp(chap06\Fig6.4\Fig.6.4_03\)的源程序演示了如何绘制 3D矩形,运行结

果如图 6.17所示。

图 6.17 draw3DRect1.jsp的运行效果

draw3DRect2.jsp(chap06\Fig6.4\Fig.6.4_03\)的演示了 3D 矩形的另外一种绘制方法,如

图 6.18 所示。源代码的讲解此处不再赘述。

Page 249: Java Web动态图表编程

图 6.18 drawRect3D2.jsp的运行效果

6.4.4 JSP 绘制椭圆

1.JSP 绘制椭圆或正圆

drawOval.jsp (chap06\Fig6.4\Fig.6.4_04\) 演示了如何绘制椭圆或正圆, 运行结果如图 6.19 所示。源代码的讲解此处不再赘述。

2.JSP 绘制 3D 椭圆

Draw3DOval.jsp(chap06\Fig6.4\Fig.6.4_04\)的演示了如何绘制 3D 椭圆,运行结果如图 6.20 所示。源代码的讲解此处不再赘述。

图 6.19 drawOval.jsp的运行效果 图 6.20 draw3DOval.jsp的运行效果

Page 250: Java Web动态图表编程

3.JSP 绘制圆柱体

drawCylinder.jsp(chap06\Fig6.4\Fig.6.4_04\)的演示了如何绘制圆柱体,运行结果如图 6.21 所示。源代码的讲解此处不再赘述。

图 6.21 drawCylinder.jsp的运行效果

6.4.5 JSP 绘制圆弧

1.JSP 绘制普通圆弧

drawArcs.jsp(chap06\Fig6.4\Fig.6.4_05\)显示了如何绘制普通圆弧,运行结果如图 6.22 所示。

图 6.22 drawArcs.jsp的运行效果

Page 251: Java Web动态图表编程

2.JSP 绘制 3D 圆弧

draw3DArcs.jsp(chap06\Fig6.4\Fig.6.4_05\)显示

了如何绘制 3D 圆弧,运行结果如图 6.23 所示。

6.4.6 JSP 绘制多边形和折线

1.JSP 绘制多边形

drawPolygon.jsp( chap06\Fig6.4\Fig.6.4_06\)演

示了如何绘制多边形,运行结果如图 6.24 所示。

2.JSP 绘制折线

drawPolyline.jsp( chap06\Fig6.4\Fig.6.4_06\)显

示了如何绘制折线,运行效果如图 6.25所示。

图 6.24 drawPloygon.jsp的运行效果 图 6.25 drawPolyline.jsp的运行效果

6.4.7 JSP 绘制三角形

三角形的绘制与 Applet 以及 Servlet 中介绍的绘制方法完全相同。本例在此做了少许修

改,增加了几个数据变量及方法。程序 drawTriangle.jsp可以接收并处理客户端提交的绘制三

角形的相关参数,如果客户端没有提交相关参数,则调用默认的参数进行绘制。 drawTriangle.jsp(\chap06\Fig6.4\Fig.6.4_07)中关于三角形的绘制过程,请读者参考前面

的相关内容。

程序在第 16行和第 17 行,新增加了两个成员变量:

int basePointX = 80; // 默认的绘制横坐标

int basePointY = 100; // 默认的绘制纵坐标

表示绘制三角形基准点的坐标(即底边中点坐标) 。

程序在第 105 行~第 118行:

public void setAlpha(int alpha)

this.alpha = alpha;

图 6.23 draw3DArcs.jsp的运行效果

Page 252: Java Web动态图表编程

public void setBasePointX(int x)

this.basePointX = x;

public void setBasePointY(int y)

this.basePointY = y;

定义了 3 个简单的 set 方法,用于设置三角形的底角度数,以及绘制基准点。

程序在第 141 行~第 179 行:

// 获取客户端提交的等腰三角形的基准点参数 String basePointXString = request.getParameter("x"); String basePointYString = request.getParameter("y"); if (basePointXString != null && basePointYString != null) setBasePointX(Integer.parseInt(basePointXString)); setBasePointY(Integer.parseInt(basePointYString));

// 获取客户端提交的等腰三角形的底角参数 String alphaString = request.getParameter("alpha"); if (alphaString != null)

setAlpha(Integer.parseInt(alphaString));

// 获取客户端提交的等腰三角形的底边长度参数 String baseLineString = request.getParameter("baseLine"); if (baseLineString != null)

this.baseLine = Integer.parseInt(baseLineString);

// 获取客户端提交的等腰三角形的方向参数 String directionString = request.getParameter("direction"); int direction = 1; if (directionString != null)

direction = Integer.parseInt(directionString);

// 获取客户端提交的等腰三角形的绘制风格参数 String styleString = request.getParameter("style"); int style = 1; if (directionString != null)

style = Integer.parseInt(styleString);

这段代码用来获取客户端提交的参数。如果参数不为空,则调用相应的 set 方法,将客

户端提交的参数,转换成整型对象后赋值给相关的数据变量。这些参数包括:三角形的基准

点坐标、底边的长度、底角的度数、三角形的方向及绘制风格等。

程序第 182行,调用 drawTrigangle方法绘制三角形。

现 在 向 IE 的 地 址 栏 中 输 入 : http://localhost:8080/chap06/Fig6.4/Fig.6.4_07/drawTrian gle. Jsp, darwTriangle.jsp的运行结果如图 6.26所示。

前面讲述过,如果要在一个 HTML 或者 JSP 页

面中,调用 darwTriangle.jsp生成的图像,只需要调用

图 6.26 darwTriangle.jsp的运行结果

Page 253: Java Web动态图表编程

标准的 HTML语句中,显示图像的标记即可。

例如:

<img src="drawTriangle.jsp">

获得如图 6.26 所示的由 drawTriangle.jsp生成的默认图像。那么如何才能向其提交参数,

并生成自定义的图像呢?有个简单的解决办法,如下所示:

<img src="drawTriangle.jsp?baseLine=60&alpha=45&direction=2&style =2&x=20&y=60">

也就是说,直接在 URL 上添加需要提交的参数,包括参数名和参数值。当 HTML/JSP 文档在解析到<img src=”xxx.jsp?xxxx=xx&xxx=xxx”>这样的标记时,在向服务器请求并加载

图像的同时,这些参数也随请求一并发送给了 JSP/Servlet 服务器。JSP/Servlet 服务器在接收

到这些参数后,按照程序中设定的处理方法进行处理,最后将实时生成的图像再返回给客户

端,达到生成自定义图像的目的。 getTriangle.jsp (\chap06\Fig6.4\Fig.6.4_07)演示了如何通过上述方法,生成各种自定义的

三角形,源程序清单如下:

1: <!­­ 2: Fig. 6.04_07_02: getTriangle.jsp 3: 功能:获取自定义三角形 4: ­­> 5: <%@ page language="java" contentType="image/png;charset=GB2312"%> 6: 7: <img src="drawTriangle.jsp?baseLine=90&direction=1&style=1&x=100&

y=100"> 8: &nbsp;&nbsp; 9: <img src="drawTriangle.jsp?baseLine=60&alpha=45&direction=2&style=

2&x=20&y=60"> 10: <BR><BR> 11: <img src="drawTriangle.jsp?baseLine=80&alpha=60&direction=3&

style=3&x=160&y=10"> 12: &nbsp;&nbsp; 13: <img src="drawTriangle.jsp?baseLine=100&alpha=60&direction=4&style=

2&x=90&y=70">

getTriangle.jsp的运行结果如图 6.27所示。

图 6.27 getTriangle.jsp的运行结果

6.4.8 JSP 绘制平行四边形和立方体

drawCube.jsp(\chap06\Fig6.4\Fig.6.4_08)的结构及程序运行过程,请读者参考第 2章相

关内容。DrawCube的运行结果如图 6.28 所示。

Page 254: Java Web动态图表编程

图 6.28 DrawCube.jsp的运行结果

Page 255: Java Web动态图表编程

6.4.9 JSP 加载并显示图像

loadImage.jsp(\chap06\Fig6.4\Fig.6.4_09)演示了如何在 JSP 程序中加载外部图像。运行本

程序之前,请读者先将下载源码 chap06\Fig6.4\Fig.6.4_09 目录下的 4 个图像文件拷贝到Web 应用程序根目录下的 images 子目录中(d:\webchart\images\)。

程序第 38 行~第 45行:

File pngFile = new File(getServletContext().getRealPath("images\\logo.png"));

File jpgFile = new File(getServletContext().getRealPath("images\\logo.jpg"));

File gifFile = new File(getServletContext().getRealPath("images\\logo.gif"));

File bmpFile = new File(getServletContext().getRealPath("images\\logo.bmp"));

声明了 4 个 File对象,分别对应“PNG” 、 “JPG” 、 “GIF” ,以及“BMP”格式的图像。

注意, 这里我们并没有使用 request.getRealPath的方法, 来获得文件的完整路径。 在 Java Servlet 2.1 版本以后,Sun建议不再使用 Request(继承于 javax.servlet 包中的 ServletRequest 接口)

类的 getRealPath,而采用 javax.servlet 包中 ServletContext 接口的 getRealPath方法。因此,我

们采用 Java 内建对象之一的 config 对象的 getServletContext 方法,获得当前 ServletContext 对象,然后再调用该对象的 getRealPath方法。getRealPath方法,需要接收一个字符串参数,

指定文件名。getRealPath方法,最后返回一个字符串,如果制定的文件存在,则返回该文件

的完整路径。

程序在第 48行~第 58 行:

logoPNG = ImageIO.read(pngFile); g.drawImage(logoPNG, 10, 50, null);

logoJPG = ImageIO.read(jpgFile); g.drawImage(logoJPG, 10, 200, null);

logoGIF = ImageIO.read(gifFile); g.drawImage(logoGIF, 210, 50, null);

logoBMP = ImageIO.read(bmpFile); g.drawImage(logoBMP, 210, 200, null);

首先,调用 javax.imageio包中 ImageIO 类的 read方法,将创建的 Image对象赋值给程序

第 36 行,声明的 4 个 Image 对象。然后调用 Graphics 类的 drawImage 方法,将这些 Image 对象的内容显示出来。注意,在 JSP 编程中,drawImage 方法中最后一个参数是“null” ,而

在 Applet 中,drawImage方法的最后一个参数是“this” 。

使用 Java 的 javax.imageio 包中的 ImageIO 类,来加载及编码图像是非常高效的。所以

我们建议读者采用 ImageIO 类,来处理图像的输入和输出等操作。loadImage.jsp 的运行结果

如图 6.29 所示。

Page 256: Java Web动态图表编程

图 6.29 loadImage.jsp的运行结果

6.5 本章小结

本章主要介绍了 JSP 的优点、结构及运行机制,以及 JSP 绘图的相关知识。简单介绍了 JSP 的语法及其内部对象,并提供了一些简单的实例来说明 JSP 是如何通过读取 HTML/JSP 文档中的数据来绘制统计饼图、如何调用 Servlet 生成不同的验证码、如何加载 Applet,以及

如何绘制甘特图等。

以第 2 章的内容为基础,详尽地阐述了在 JSP 环境下,如何使用 Java.awt.Graphics 类的

各种方法,包括绘制直线、文本(字符串)、矩形、椭圆和圆、圆弧、多边形和折线。在绘制

基本几何图形的基础上,我们以绘制圆柱体和立方体为例,向读者演示了,如何通过绘制多

个多边形并将其组合成一个复杂几何图形的方法。得益于 Java.imageio 包中的 ImageIO 类的

支持,通过调用 ImageIO 类,来执行加载图像以及对图像进行编码输出等工作,就变得简单,

而且移植性更好了。

到目前为止,我们向读者介绍了基于 Java 对基本几何图形的处理。组合这些基本的几何

图形,我们可以绘制一些普通的、常见的Web 图表。要绘制复杂的图形,就涉及到我们即将

学习的 JSP 与 Java2D的相关编程知识了。

第 7 章 JSP 与 Java2D Web 图表编程

本章介绍如何利用 JSP 与 Java2D 技术来生成复杂的Web 图表。Java2D API提供了绘制

复杂图形的支持。在 java.awt.geom 包中,提供了很多独特的用以处理圆弧、文字和图像的 API。该包与 java.awt、java.awt.color、java.awt.image、java.awt.font、java.awt.print,以及 java.awt.image.renderable 包中的 API 相结合,就可以实现美观的、复杂的、丰富多彩的 Web 图表了。

Page 257: Java Web动态图表编程

7.1 Java2D 概述

顾名思义,Java 2D 就是 Java 对二维图形的支持。Java 的 AWT 中,已经提供了实现简

单的二维图形功能。读者在阅读本书前面部分已经感受到,虽然使用 java.awt 包,可以生成

基本的 Web 图表,但是有一定的局限性,如:

Ø 所有线条只能使用单一像素的宽度绘出;

Ø 如果要旋转、放大、缩小任何对象,必须自己动手进行数学运算才能达成;

Ø 如果要进行渐进色或纹理等特殊着色处理,必须自己动手编写源代码;

Ø 只提供最基本的图像显示功能。 Java2D 则提供复杂的、强大灵活的并且独立于图形设备和分辨率的二维图形功能。 Java 2D API是 JFC (Java Fundation Classes) 的一员, 加强了传统AWT (Abstract Windowing

Toolkit)图像绘制功能。在 JDK1.2 中,已经支持 Java 2D 的使用。通过 Java 2D API,我们

可以绘制出任意的几何图形、 运用不同的填色效果、 对图形做平移、 旋转 (rotate)、 缩放 (scale)、

扭曲(shear)等。当然,Java 2D API还有许多增强 AWT功能的部分,如处理图像文件可以

有不同的滤镜(filter)效果、对于任意的几何图形也能做到碰撞侦测(hit detection)、图形重

叠复合计算(composite)等功能。本章将对涉及本书内容的 Java2D 相关技术做简要的介绍。 Graphics2D 是 Java2D 的绘图环境。Graphics2D 对象的属性,决定了所绘制图形的所有

信息。绝大部分信息包含在 Graphics2D 对象的 6 个属性内,如下所示:

(1)填充属性(paint) paint 属性,决定图形描绘或填充的颜色,定义填充图形的模式。填充属性是通过调用绘

图环境的 setPaint 方法进行设置。默认的填充属性是当前绘图环境的填充属性。

(2)笔划属性(stroke) stroke属性,决定图形描绘所使用的笔划类型,如实线、虚线(注:点划线也属于虚线)

以及线条的粗细。它决定线段端点的形状。笔划属性是通过图形环境中 setStroke方法进行设

置。默认的笔划属性定义了一个正方形的、粗细为 1像素的实线,末端为正方形,接口为 45 度斜面。

(3)字体属性(font) font 属性, 决定绘制文本所使用的字体。 调用图形环境 setFont 方法, 即可设置字体属性。

默认字体是当前图形环境所使用的字体。

(4)转换属性(transform) transform属性,决定渲染过程中应用的转换方法。通过当前的转换方法,绘制图形可以平

移、旋转、缩放和扭曲。默认的转换方法是恒等转换,即保留原样不进行任何改动。

(5)剪切属性(clip) clip 属性,定义绘图组件上一个区域的边界。渲染会受到 clip 的限制,只能在 clip 定义

的区域内进行。调用绘图环境 setClip方法,可以设置剪切属性。默认的剪切属性是整个图形

环境。

(6)复合属性(composite) composite属性,决定如何在组件上绘制重叠放置的图形。我们可以修改图形填充颜色的

透明度,使底部被上面图形所覆盖的部分也能显示出来。还可以调用图形环境 setComposite方

法设置复合属性。默认复合属性可以在任何已有的图形上绘制一个新图形。

所有表示属性的对象都作为引用,存储在 Graphics2D 对象内。因此,我们必须调用相关 set 方法,来设置或修改图形环境中各组件的相关属性。

Graphics2D 渲染图形环境中各个组件时共有 4 种基本的方法:

(1)draw(Shape shape)

Page 258: Java Web动态图表编程

使用图形环境的当前属性渲染一个图形。这里的渲染,其实与我们学习 Applet 中所说的

描绘图形是相同的意思。两者都是描绘当前图形的外观轮廓。

(2)fill(Shape shape)

使用图形环境的当前属性填充一个图形。这里的填充,其实与我们学习 Applet 中所说的

填充图形是相同的意思。两者都是填充一个实心的当前图形。

(3)drawString(String string)

使用图形环境的当前属性绘制一段文本。

(4)drawImage() 使用图形环境的当前属性渲染(显示)一幅图像。 Graphics2D 类位于 java.awt 包中。因此,要调用 Graphics2D,就必须引入该包。而

Graphics2D 类所渲染的各种 Shape 对象却是位于 java.awt.geom 中。所以必须同时引入 java.awt.geom包。这里所指的 Shape对象实现了 Shape这个接口的任何类的实例。

7.2 Java AWT与 Java2D

我们先看看调用 Java AWT相关包来进行 Web 图表绘制的相关方法:

public void paint(Graphics g) // 调用父类的 paint 方法 super.paint(g);

// 设置笔划 g.setColor(someColor); g.setFont(someFont);

// 绘制图形

g.drawString(...); // 绘制文本

g.drawLine(...); // 绘制线段

g.drawRect(...); // 描绘矩形

g.fillRect(...); // 填充矩形

g.drawOval(...); // 描绘椭圆

g.fillOval(...); // 填充椭圆

g.drawPolygon(...); // 描绘多边形

g.drawPolygon(...); // 填充多边形

g.drawImage(...); // 显示图像 ...

这些方法读者都已经非常熟悉了。在 Java2D 中,这些代码有些什么不同呢?因为 Graphics2D 类是 Graphics 的子类,所以可以完成本书前面所讲述的所有方法而不需做任何变

动。只需声明如下所示的一个 Graphics2D 的对象引用即可。

Graphics2D g2d = (Graphics2D)g

也就是说, 如果要调用Java2D的方法, 就必须将Graphics的引用 g强制转换成Graphics2D 的引用 g2d,然后通过 g2d 来调用 Graphics 的引用 g 的相关方法。上述代码可以改写成如下

形式:

public void paint(Graphics g) // 调用父类的 paint 方法 super.paint(g);

Graphics2D g2d = (Graphics2D) g

Page 259: Java Web动态图表编程

// 设置笔划 g2d.setColor(someColor); g2d.setFont(someFont);

// 绘制图形

g2d.drawString (...); // 绘制文本

g2d.drawLine(...); // 绘制线段

g2d.drawRect(...); // 描绘矩形

g2d.fillRect(...); // 填充矩形

g2d.drawOval(...); // 描绘椭圆

g2d.fillOval(...); // 填充椭圆

g2d.drawPolygon(...); // 描绘多边形

g2d.drawPolygon(...); // 填充多边形

g2d.drawImage(...); // 显示图像 ...

我们着重于 Graphics2D 的新特性。除了示例中调用 Graphics 对象的方法外,Graphics2D 对象还拥有自己独特的新方法。如下所示:

public void paintComponent(Graphics g) // 调用父类的 paintComponent 方法 super.paintComponent(g);

Graphics2D g2d = (Graphics2D) g

// 设置绘图环境属性 g2d.setPaint(fillColorOrPattern); g2d.setStroke(penThicknessOrPattern); g2d.setComposite(someAlphaComposite); g2d.setFont(anyFont); g2d.translate(...); g2d.rotate(...); g2d.scale(...); g2d.shear(...); g2d.setTransform(someAffineTransform);

// 创建某种图形 SomeShape shape = new SomeShape(...);

// 绘制图形

g2d.draw(shape); // 描绘图形轮廓

g2d.fill(shape); // 填充实心图形 ...

我们可以看出,在 Graphics2D 中,图形的渲染(绘制)一般分成三步进行:

(1)创建所要绘制的图形。

(2)设置该图形绘图环境的相关属性,如笔划粗细、线条类型(实线、虚线、点划线)、

绘制的颜色、绘制模式、旋转、平移、缩放、扭曲、字体等属性。

(3)根据设置好的相关绘图环境的相关属性,描绘或填充该图形。

在理解上述步骤的基础上,我们就可以编写一些基于 Java2D API的 Web 图表程序了。

提示:Java2D 本身就是一个博大精深的内容。它需要一本专著来讲述,已经超出本书的

内容。但 Java2D可以给我们带来强大的图形、图像处理功能,令 Web 图表更加美观。因此,

我们将 Java2D 作为单独的一章进行详尽的讲解。

Page 260: Java Web动态图表编程

7.3 Java2D 与填充属性

7.3.1 设置填充属性

paint 属性,决定图形的描绘或填充的颜色,也用它来定义填充图形的模式。填充属性是

通过调用绘图环境的 setPaint方法进行设置。 默认的 paint属性的颜色是当前绘图环境的颜色,

如表 7.1 所示的 setPaint 方法。

表 7.1 Java2D的 setPaint方法

方 法 说 明

abstract void setPaint(Paint paint) 设置 Graphics2D 图形环境的填充属性

Graphics2D 的 setPaint 方法接收一个参数,该参数为 Paint 类的实例 paint。这里 Paint 类

的对象 paint 可以是任何实现 java.awt.Paint 接口的类的对象。Paint 对象可以是我们熟悉的 java.awt.Color 类的一个实例(Color 类实现了 Paint 接口),也可以是 java.awt.GradientPaint 类、java.awt.SystemColor 类、java.awt.TexturePaint 类的一个实例。

其中 SystemColor 类继承于 Color 类。下面分别给出几个实例,说明 setPaint 的用法。

7.3.2 填充属性的用法

1.纯色及渐进色填充

gradientPaint.jsp(chap07\Fig7.3\Fig.7.3_01\)演

示了背景填充的方法,运行结果如图 7.1 所示。

首先,程序在第 8 行:

import="java.awt.geom.*"

引进了 Java2D 包。我们在前面说过,geom包中

定义了 Java2D 所需要的复杂的二维图形。一般来说,

只要是调用 Java2D,就需要引入该包。

程序第 27 行:

Graphics2D g2d = (Graphics2D)g;

将 Graphics 的引用 g 强制转换成 Graphics2D 的引用 g2d。这是使用 Graphics2D 对象必

须要做的工作。

程序第 30 行~第 31行:

g2d.setColor(Color.YELLOW); g2d.fillRect(0, 0, width, height);

这里我们调用 Graphics 类的方法 setColor 设置当前绘制颜色为黄色。然后调用 Graphics 类的 fillRect 方法,填充一个黄色的实心矩形。这两句代码是读者经常见到的,前面的例程是

用来填充绘图环境的背景。 这两句代码演示了 Graphics2D 继承于 Graphics 类的子类和父类的

关系。因此,在 Graphics2D 类的实例中,可以直接调用 Graphics 类的方法。这些方法的用法

和我们以前介绍的 Graphics 类的用法完全相同。因为 Color 类实现了 java.awt.Paint 接口,所

以程序第 30行,可以改写成如下形式:

图 7.1 gradientPaint.jsp的运行结果

Page 261: Java Web动态图表编程

g2d.setPaint(Color.YELLOW);

程序运行结果也完全相同。

程序第 34 行~第 35行:

GradientPaint gp1 = new GradientPaint(15, 75, Color.RED, 50, 120, Color.GREEN, false);

创建了一个 GradientPaint 类的实例 gp1。GradientPaint 类实现了用渐进色(也称为颜色

梯度)描绘或填充图形组件的效果。这里所调用的 GradientPaint 的构造器共有 7 个参数。第 1、2 个参数指定渐进色的起始坐标,第 3 个参数指定渐进色的起始颜色为红色,第 4、5 个

参数指定渐进色的结束坐标,第 6 个参数指定渐进色的结束颜色为绿色,第 7 个参数是个布

尔值。该布尔值指定渐进色是循环的(true),还是非循环的(false)。

参数中两种坐标指定了渐进色中颜色的变化方向。因为渐进色的结束坐标(50,120)在

其起始坐标(15,75)的右下方,因此,渐进色中颜色的变换方向也是向右下方变化。因为这

里的布尔值为“false” ,所以渐进色是非循环的,也就是说,渐进色的颜色从红色渐变到绿色

后,颜色不再进行渐变。

程序第 34 行:

g2d.setPaint(gp1);

调用 Graphics2D 类的 setPaint 方法,设置图形环境的填充模式,setPaint 需要接收一个 Paint 类的实例对象。这里我们将 GradientPaint 对象 gp1 作为其参数。

程序第 35 行:

g2d.fill(new Rectangle(50, 10, width­100, height­220));

调用 Graphics2D 类的 fill 方法,填充一个 Shape对象。前文说过,这里所指的 Shape对

象,其实就是实现了 Shape接口的任何类的实例。查看 J2SE 5.0 API可知以下对象都实现了 Shape接口,如

Arc2D, Arc2D.Double, Arc2D.Float, Area, BasicTextUI.BasicCaret, CubicCurve2D, CubicCurve2D.Double, CubicCurve2D.Float, DefaultCaret, Ellipse2D, Ellipse2D.Double, Ellipse2D.Float, GeneralPath, Line2D, Line2D.Double, Line2D.Float, Polygon, QuadCurve2D, QuadCurve2D.Double, QuadCurve2D.Float, Rectangle, Rectangle2D, Rectangle2D.Double, Rectangle2D.Float, RectangularShape, RoundRectangle2D, RoundRectangle2D.Double, RoundRectangle2D.Float。

这些对象大部分都位于 java.awt.geom 包中。因此,我们在程序第 35 行,直接调用 java.awt.Rectangle类的构造器,创建一个 Shape对象。之后,调用 g2d的 fill 方法,填充创建

的这个 Shape对象:左上角坐标为(50,10),宽度为 220 像素,高度为 180 像素的矩形。

绘制结果如图 7.1①所示。我们在调用 GradientPaint 的构造器时,第 7 个参数布尔值为

“false” ,因此,图 7.1①中可以看到渐进色从红色渐变成绿色后,就一直保持绿色而没有发

生变化。

程序第 39 行~第 42行:

GradientPaint gp2 = new GradientPaint(15, 75, Color.RED, 50, 120, Color.GREEN, true);

g2d.setPaint(gp2); g2d.fill(new Rectangle(50, 210, width­100, height­220));

为了对比 GradientPaint 的构造器,第 7 个参数布尔值为“true”时,可以观察到渐进色

从红色渐变为绿色后,又从绿色渐变为红色,然后一直这样循环渐变。同时,渐变色的方向

Page 262: Java Web动态图表编程

是向右下方变化的。该段代码绘制结果如图 7.1②所示。

2.纹理填充

texturePaint.jsp(chap07\Fig7.3\Fig.7.3_02\)演示了纹理填充的方法。纹理填充类似于在 Windows 中设置壁纸图像平铺的效果。如果希望以纹理模式进行填充操作,首先必须指定用

于纹理填充操作的缓冲图像, 其次指定使用该缓冲图像中的哪一部分矩形区域作为填充纹理。

创建缓冲图像一般有两种方法:一种是由开发者自己编写,另一种是通过加载外部的图像文

件来创建。本例分别给出这两种创建缓冲图像的方法。

程序首先在第 31 行~第 32 行:

BufferedImage bg = new BufferedImage(10, 10, BufferedImage.TYPE_INT_RGB);

创建了一个宽度和高度均为 10像素的 BufferedImage 对象 bg。

然后程序在第 34 行:

Graphics2D bg2d = bg.createGraphics();

声明了一个 Graphics2D 的对象 bg2d,调用 bg 的 createGraphics 方法,并用返回的 Graphics2D 的实例对 bg2d进行初始化。

程序第 35 行~第 42行:

bg2d.setColor(Color.YELLOW); bg2d.fillRect(0, 0, 10, 10); bg2d.setColor(Color.RED); bg2d.fillRect(1, 1, 6, 6); bg2d.setColor(Color.BLUE); bg2d.fillRect(2, 2, 4, 4); bg2d.setColor(Color.BLACK); bg2d.fillRect(4, 4, 3, 3);

这段代码,绘制了一个 10x10 像素的黄色实心矩形。在该黄色实心矩形中,又覆盖着三

个尺寸大小、颜色不一的实心矩形。

程序第 45 行:

TexturePaint tp1 = new TexturePaint(bg, new Rectangle(10,10));

声明了一个 TexturPaint 对象 tp1。这里我们调用 TexturPaint 的构造器来生成 TexturPaint 的实例。该构造器接收两个参数。正如我们刚才所说,如果希望以纹理模式进行填充操作,

首先必须指定用于纹理填充操作的缓冲图像,其次指定使用该缓冲图像中的哪一部分矩形区

域作为填充纹理。因此,这里构造器接收的第 1 个参数就是我们创建的缓冲图形对象 bg,第 2 个参数是一个 Rectangle2D 的实例。Rectangle2D 位于 java.awt.geom 包中,因为 Rectangle 类(位于 java.awt 包中)是其直接的子类,所以我们可以用 Rectangle 类的实例来代替 Rectangle2D 的实例。注意,这里的 new Rectangle(10, 10)实际上等同于 new Rectangle (0,0,10,10)。本例中 Rectangle 和 bg 的大小正好相同,宽度和高度均为 10 像素。我们也可

以指定 bg图像中的一部分区域作为纹理。

程序第 46 行:

g2d.setPaint(tp1);

设置当前绘图环境的填充模式为纹理填充。

程序第 48 行:

g2d.fillRect(0, 0, width, height);

Page 263: Java Web动态图表编程

调用 fillRect 方法,利用当前纹理填充模式对整个绘图环境进行纹理填充。

接下来,程序利用加载外部图像文件的方式来创建缓冲图像。请读者将 javaLogo.jpg 文

件(chap07\Fig7.3\Fig.7.3_02\)复制到 Web 应用程序的根目录中。

程序第 52 行~第 57行:

File jpgFile = new File(getServletContext().getRealPath("javaLogo.jpg"));

BufferedImage im = ImageIO.read(jpgFile);

// 创建纹理填充对象 TexturePaint tp2 =

new TexturePaint(im, new Rectangle(im.getWidth(),im.getHeight ()));

其中程序第 52 行~第 54 行,都是我们熟悉的代码,就是利用 ImageIO 类的 read方法,

加载外部的图像文件。并用 ImageIO 类的 read 方法,返回的对象来初始化 BufferedImage 类

的对象 im。

程序第 56 行~第 57行,创建 TexturePaint 对象 tp2。我们指定这里构造器的缓冲图像参

数为 im,同样,用一个 Rectangle 实例来代替 Rectangle2D 实例。Rectangle 的宽度和高度通

过调用 im的 getWidth和 getHeight 方法获得,这两个方法分别返回当前图像的宽度和高度。

程序第 60 行~第 61行:

g2d.setPaint(tp2); g2d.fillOval(50, 50, width ­100, height­100);

设置绘图环境的填充模式为纹理填充对象 tp2 所

表示的模式。然后,按照 tp2 所表示的模式填充一个

外切矩形,左上角坐标为(50,50),宽度为 220 像素,

高度为 300像素的一个椭圆。 texturePaint.jsp的运行结果如图 7.2 所示。

7.4 Java2D 与笔划属性

我们在前面讨论过,java.awt 默认的笔划属性,定

义了一个正方形的、粗细为 1 像素的实线,末端为正

方形,接口为 45度斜面。而在 Java2D中,stroke属性

可以自定义实线、虚线、点划线,以及线条的粗细。

它也自定义线段端点的形状风格(end style)及两条线

段相交处的形状风格(join style)。

在图形环境中应用笔划属性的基本步骤如下:

(1)创建一个 BasicStroke 的实例。BasicStroke 类位于 java.awt 包中。它实现了 Stroke 接口。BasicStroke有几个不同的构造器,分别用来设置不同的笔划属性。

(2)将创建好的 BasicStroke 的实例作为参数传递给 Graphics2D 的 setStroke 方法, setStroke方法,需要接收一个任何实现 Stroke接口的类的实例。

(3)通过 Graphics2D 的 draw 方法就可以应用自定义的笔划属性了。Graphics2D 根据 Storke对象的属性来描绘图形的轮廓。

图 7.2 texturedPaint.jsp的运行结果

Page 264: Java Web动态图表编程

7.4.1 线段端点的形状风格

线段端点有许多种不同形状的风格。Basicstroke 支持三种端点风格,分别由三个常数来

表示,如图 7.3 所示。

public static final int CAP_BUTT

这种端点的形状风格不会在端点加上任何东西

作为修饰,采用这种端点直线段的外观类似于矩形。

如图 7.3①所示。

Public static final int CAP_RNOUD

这种端点会在线段的端点加上半圆作为修饰,半圆的直径和线段宽度相同。如图 7.3②

所示。

Pubic static final int CAP_SQUARE

这种端点风格会在线段的端点加上矩形作为修饰,矩形的宽为线段宽度的一半,高度和

线段宽度相同。该端点的风格适用于各种线段。如图 7.3③所示。

7.4.2 线段转折处的形状风格

BasicStroke提供了三种不同的风格,来表示线段转折处笔划的连接方式,如图 7.4 所示。

图 7.4 BasicStroke笔划线段转折处的三种风格

同样,它们也分别由三个常数表示,这些常数也被定义于 BasicStroke 类中,分别是 JOIN_BEVEL、JOIN_MITER 和 JOIN_ROUND。

public static final int JOIN_BEVEL

这种风格的线段会通过如图 7.4①所示的方式连接。

public static final int JOIN_MITER

这种风格的线段,会通过延长两边笔划(延伸它们的外缘)并相交于某一点而生成,不

过线段延伸的长度(miter)却是有限制的。试想一下,如果相交的笔划接近于平行,那么笔

划的延长线可能会在很远的位置才会相交, 这种情况下, 笔划的形状就变形了。 因此, Java2D 对 miter 的长度有一个限制。如果延伸的长度大于 miter 限制长度的话,系统便自动更改 JOIN_BEVE 风格。JOIN_MITER 的 miter 并未直接指定 miter 的长度,而是通过下面三个值

来指定:

Ø halfLength:线段宽度的一半。

Ø miterLength:外缘延伸的相交处和内缘相交处的距离。

Ø miterLimit:外缘延伸长度的上限。

如果 miterLength大于(miterLimit 乘以 halfLength),系统便自动改用 JOIN_BEVEL,而

不会采用 JOIN_MITER。(注:miterLimit 的值就是 BasicStroke 器中 miterLimit 参数的值。 HalfLength 及 miterLength 这两个变量的值则是由系统来计算的。)这样做的目的是要避免在

图 7.3 BasicStroke支持三种端点风格

CAP_BUTT ① CAP_RNOUD ② CAP_SQUARE ③

① ② ③

Page 265: Java Web动态图表编程

线段转折处生成夸张的笔划。当两条线段近乎平行时,采用 JOIN_MITER 来生成的线段笔划

会过度地延伸,远离线段的转折处。

public static final int JOIN_ROUND

这种风格的线段,会在结合处添加半圆形作为修饰,与 CAP_ROUND端点风格差不多。

7.4.3 虚线风格

BasicStroke 还可以设置任意风格的虚线。使用 BasicStroke 对象,可以定义复杂的短划

线图案。在设置虚线风格的时候,需要指定两个用于控制虚线风格的参数:

(1)dash:代表虚线图案的数组。数组中的元素交替代表虚线线段中“虚线线段的长度”

及“两条虚线之间空白部分的长度” 。索引值为偶数的数组元素代表着虚线中“虚线线段的长

度” ,而索引值为奇数的数组元素代表“两条虚线之间空白部分的长度” 。举例来说:

Ø 假设 dash 数组元素为15,12,表示的意义是虚线段中“虚线线段的长度”及“两条

虚线之间空白部分的长度”分别为 15像素和 12 像素(注:因为数组的第 1 个元素的

索引值为“0” )。

Ø 如果 dash 数组元素为8,4,12,5,7,10,则表示该虚线由一组长短不一的虚线组成(点

划线)。第一段虚线的长度为 8 像素,紧跟 4像素的空白,接着是 12 像素的虚线,随

后是 5 像素的空白,然后是 7像素的虚线,最后是 10 像素的空白。

Ø 如果 dash 数组中只有一个元素,如:10,则表示虚线的长度和它们之间的间距为

常数,即 10像素,等同于数组10,10。

(2)dash_phase:定义虚线图案开始位置的偏移量。以虚线数组为15,12的虚线图

案为例。当虚线的偏移量为 0 时,表示从虚线图案的开头部分开始绘制虚线,接着绘制空

白,以此类推,如图 7.5①所示。当虚线的偏移量为 5 时,虽然还是表示从虚线图案的开

头部分开始绘制虚线,但这里虚线的长度为 10 像素(15­5),然后是 12 像素的空白部分,

在此之后,绘制和虚线的偏移量为 0 的虚线图案相同的图案。如图 7.5②所示。

图 7.5 虚线风格

7.4.4 BasicStroke 构造器

在理解上述笔划的端点风格、转折点风格以及虚线风格的概念后,创建 BasicStroke笔划

对象便是比较轻松的事情了。 BasicStroke笔划对象的创建一般是通过 BasicStroke类的构造器

进行的。

表 7.2 所示 BasicStroke的构造器方法。

表 7.2 BasicStroke的构造器方法

方法:public BasicStroke()

说明:BasicStroke 的默认构造器。根据默认值来创建 BasicStroke 对象:宽度为 1.0、端点风格为 CAP_SQUARE、线段

转折处的风格为 JOIN_MITER、miter 为 10.0 的虚线段

方法:public BasicStroke(float width)

说明:该构造器根据参数指定的值来创建 BasicStroke 对象:宽度为 width 个像素、端点风格为 CAP_SQUARE、线段转

虚线

偏移量

虚线

偏移量

单位:像素

1

2

0 15 25 40 50

0

5

0 10 20 35 45 60

Page 266: Java Web动态图表编程

折处风格为 JOIN_MITER、miter 为 10.0 的虚线段

方法:public BasicStroke(float width, int cap, int join)

说明:该构造器根据参数指定的宽度、端点风格以及线段转折处的风格来创建 BasicStroke 对象。如果线段转折处风格

为 JOIN_MITER,则会自动设定 miter 的上限值为 10.0

方法:public BasicStroke(float width, int cap, int join, float miterLimit)

说明:该构造器根据参数指定的宽度、端点风格以及线段转折处的风格来创建 BasicStroke 对象。如果线段转折处风格

为 JOIN_MITER,则设定 miter 上限值为参数 miterLimit 所指定的值

方法:public BasicStroke(float width, int cap, int join, float miterLimit, float[] dash, float dash_phase)

说明:该构造器根据参数指定的宽度、端点风格以及线段转折处的风格、虚线数组以及虚线偏移量来创建 BasicStroke

对象。如果线段转折处风格为 JOIN_MITER,则设定 miter 上限值为参数 miterLimit 所指定的值

7.4.5 Java2D Web 图表实例之折线图

1.普通线段图表

现在我们来编写一个综合应用所学 Java2D API 内容的 Web 图表。 lineChart.jsp (\chap07\Fig7.4\Fig.7.4_01)演示了如何利用 Java2D API来绘制线段图。lineChart.jsp综合了目

前我们所讨论的 Java AWT、Servlet 和 Java2D 技术。 lineChart.jsp 的运行结果如图 7.6 所示。本程序绘制了两条折线,分别代表 Java 类以及

C#类计算机编程图书在 2004 年的月销售量。月销售量由随机数产生。如图 7.6 所示,书籍的

月销售量 Web 折线图表绘制效果比以前我们绘制的 Web 图表更美观,有了漂亮的背景、立

体感很强的边框、虚线组成的网格线、和谐的坐标轴等等,这些效果是如何实现的呢?我们

将详细讨论整个源程序,将其抽丝剥茧,将其绘制的步骤一一呈现在读者面前。 lineChart.jsp 调用了以前我们讨论过的生成三角形的 Servlet,来生成坐标轴上的箭头。

与以前讲过的 JSP/HTML 文档,调用 Servlet 的方法不同,本例采用了 JavaBean 的形式,来

调用该 Servlet (关于如何编写 JavaBean, 我们将在本书最后一章进行讲解)。 为调用该 Servlet,

程序首先在第 11 行:

图 7.6 lineChart.jsp的运行结果

import="com.fatcat.webchart.*"

引入该 Servlet:TriangleServlet。

程序第 32 行,创建了 Graphics2D 的对象 g2d:

Graphics2D g2d = image.createGraphics();

Page 267: Java Web动态图表编程

这里我们没有使用以前介绍的方法:

Graphics g = image.getGraphics(); Graphics2D g2d = (Graphics2D)g;

而是直接调用了 BufferedImage 对象 image 的 createGraphics 方法。因为 createGraphics 方法直接返回一个 Graphics2D 的对象,这样,就不需要必须将 Graphics 类的对象强制转换成 Graphics2D 的类型才可以使用。

程序第 35 行~第 36行:

g2d.setColor(Color.WHITE); g2d.fillRect(0, 0, width, height);

将整个绘图环境的背景设置为白色。

程序第 39 行~第 40行:

GradientPaint grayGP = new GradientPaint(0, 0, Color.GRAY, width, height, new Color(200, 200, 200), false);

创建了一个渐进色对象 grayGP。渐进色的第 1 个颜色为灰色,坐标为(0, 0),第 2 个颜

色为 RGB 值均为 200 的颜色对象表示浅灰色,坐标为绘制环境右下角的顶点坐标,说明本

渐进色的变化方向是从左上角到右下角变化的。接着设定该渐进色为非循环风格。

程序第 41 行~第 42行:

g2d.setPaint(grayGP); g2d.fillRoundRect(20, 20, width ­ 20, height ­ 20, 50, 50);

设置绘图环境当前填充模式为 grayGP 对象所表示的渐进色对象。之后,用该 grayGP 属

性来填充一个圆角矩形。这时,程序的运行结果如图 7.7①所示。我们可以看到,绘制了一个

渐进色为灰色的圆角矩形。

图 7.7 lineChart.jsp的运行过程 1 程序第 44 行~第 45行:

g2d.setPaint(Color.WHITE); g2d.fillRoundRect(12, 12, width ­ 20, height ­ 20, 50, 50);

Page 268: Java Web动态图表编程

填充一个白色的圆角矩形。该圆角矩形和刚才绘制的圆角矩形大小相同,只是绘制的位

置不同。这时,程序的运行结果如图 7.7②所示。

我们可以看到,现在实现了一个简单的阴影效果。接下来,我们描绘一个边框。

程序第 47 行~第 50行:

BasicStroke bs = new BasicStroke(4.0f); g2d.setStroke(bs); g2d.setPaint(new Color(53, 76, 112)); g2d.drawRoundRect(12, 12, width ­ 20, height ­ 20, 50, 50);

创建一个 BasicStroke 类的实例 bs。通过调用 BasicStroke 的构造器,设定该笔划的宽度

为 4 像素。然后将该笔划对象 bs 应用到当前绘图环境中,并设置绘图环境当前的绘制颜色为 RGB 值为 53、76、112 所代表的颜色,接着描绘了一个圆角矩形。这次绘制的圆角矩形的位

置和刚才填充的白色矩形的位置相同。所以,现在得到了如图 7.7③所示的运行结果。

现在我们需要在图表的绘制区域中填充一个渐进色的矩形,作为以后绘制销售折线的背

景。

程序第 52 行~第 55行:

GradientPaint blueGP = new GradientPaint(120, 60, new Color(215, 230, 252), 120, 300, Color.WHITE, false); g2d.setPaint(blueGP); g2d.fillRect(120, 60, 440, 300);

创建了一个渐进色对象 blueGP。渐进色的第 1 个颜色为淡蓝色(RGB 值:215,230,252),

坐标和图表绘图区域的左上角顶点坐标相同, 都是 (120, 60), 第 2 个颜色为白色, 坐标为 (120, 300),说明该渐进色的变化方向是垂直从上向下变化的,然后设定该渐进色为非循环风格。

程序第 54 行,设置当前绘图环境的填充属性为 blueGP 所代表的渐进色。

程序第 55 行,调用 fillRect 方法,填充一个矩形。该矩形所在区域就是以后绘制销售折

线的背景。矩形左上角顶点坐标为(120, 60),矩形的宽度为 440 像素,高度为 300 像素。

程序运行结果如图 7.7④所示。

程序第 58 行~第 61行:

String chartTitle = "计算机编程类图书 2004年月销售量统计图"; g2d.setFont(new Font("方正粗宋简体", Font.PLAIN, 22)); g2d.setColor(Color.BLACK); g2d.drawString(chartTitle, 140, 40);

很熟悉的代码,绘制图表标题。这时的运行结果如图 7.8①所示。

Page 269: Java Web动态图表编程

图 7.8 lineChart.jsp的运行过程 2

程序第 64 行~第 67行:

float[]dashes = 3.f ; bs = new BasicStroke(1.0f, BasicStroke.CAP_ROUND, BasicStroke.

JOIN_ROUND, 10, dashes, 0); g2d.setStroke(bs);

这段代码的作用是设置当前绘图环境的笔划属性。首先创建只包含一个值为 3.0 的单精

度浮点数的 float 数组。然后,调用 BasicStroke 的另一个版本的构造器,将重新生成的 BasicStroke对象再赋值给变量 bs。这次定义的笔划属性是:笔划的外观为虚线,宽度为 1 像

素,线段断点的风格为半圆,线段转折处的风格为圆角,虚线的线段长度为 3 像素,虚线和

虚线之间的空白也为 3 像素,从开头部分开始绘制。

程序第 68 行~第 70行:

g2d.setFont(new Font("Courier New", Font.PLAIN, 12)); String str = "2004­"; int stringLength = 0;

设置图像环境的当前绘制字体为“Courier New” ,字体大小为 12磅,风格为普通。接着

创建了两个变量: 一个字符串变量 str, 并赋其初始值为 “2004­” ;一个整型变量: stringLength,

表示整个字符串的长度。

程序第 72 行~第 89行:

for (int i = 1; i <= 12; i++)

// 绘制垂直方向虚线 g2d.drawLine(80+i * 40, 50, 80+i * 40, 360);

// 绘制横轴上月份的说明文字 str += i; stringLength = g2d.getFontMetrics().stringWidth(str); if (i % 2 == 0) g2d.drawString(str, 80+i * 40 ­ stringLength / 2, 387); else

Page 270: Java Web动态图表编程

g2d.drawString(str, 80+i * 40 ­ stringLength / 2, 375); str = "2004­";

本例中,绘制的是月销售量的曲线图,其中横轴表示月份,纵轴表示当月的销售量。因

此横轴就被等分成 12段,分别表示一年中的某月。在这个循环次数为 12次的循环体中,我

们以程序第 1 次循环运行过程为例,看看程序是如何绘制的。首先,程序在第 73 行,应用当

前的笔划属性,绘制了一条垂直方向的虚线。因为这是第 1 次循环,因此,i值为 1,绘制语

句实际上为:

g2d.drawLine(120, 50, 120, 360);

然后将循环变量 i 的值添加到前面创建的字符串对象 str 后, 此时 str 的内容为 “2004­1” 。

程序第 79 行, 通过 g2d先调用 Graphics2D 的 getFontMetrics 方法, 接着该方法返回一个 FontMetrics 对象,再调用该 FontMetrics 对象的 stringWidth 方法,获得当前字符串变量 str 的长度。

接下来,程序绘制当前字符串 str 的内容“2004­1” 。这里我们做了一个小小的处理,奇

数月份的绘制位置紧靠着曲线绘制区域的下方,偶数月份的绘制位置在奇数月份的下方。并

且字符串的中点位置正好和垂直虚线的位置相重合 (实现方法: 设定字符串 str 绘制的横坐标 =当前垂直虚线的横坐标−str 字符串长度的一半,见程序第 82 行、第 86 行),这样显示的效

果更准确,也更美观。

绘制完当前字符串 str的内容“2004­1”后,程序在第 88 行,重新清空 str 的内容。

至此,程序就绘制了一条垂直虚线以及一段表示月份的字符串。

下面程序返回到 72 行,继续进行循环,每次进入循环,便在不同的位置绘制一条垂直

虚线及一段表示月份的字符串。本段代码结束后的绘制效果如图 7.8②所示。

程序第 91 行~第 105行:

str = ""; int stringHeight = 0;

for (int i = 0; i < 300; i += 30)

// 绘制水平方向虚线 g2d.drawLine(120, 60+i, 570, 60+i);

// 绘制纵轴上销售量的说明文字 str += 100­i / 3; stringHeight = g2d.getFontMetrics().getAscent(); stringLength = g2d.getFontMetrics().stringWidth(str); g2d.drawString(str, 110­stringLength, 60+i + stringHeight / 2); str = "";

本段代码实现的是绘制水平方向的虚线,以及纵轴上关于销售量说明文字的功能。本例

中, 月销售量由随机数产生。 为便于绘制销售折线, 我们假设每种书籍的月销售量都低于 100 本。折线绘制区域的高度为 300 像素。因此,我们绘制了 10 条水平方向的虚线,分别表示销

售量为表示 10、20、30…90、100 单位在纵轴上的位置,在该绘制区域内,每 3 像素表示一

个销售量单位。

首先将字符串变量 str的内容清空,然后定义一个整型变量 stringHeight 表示字符串的高

度,并将其初始值赋值为 1。同样进入一个循环,在循环体中绘制 10 条水平方向的虚线,以

及纵轴上表示销售量的数字。我们仍然以第 1 次循环为例,讲述该段代码的运行过程:

还是应用前面定义的笔划属性,绘制水平虚线。因为是第 1 次循环,所以当前的循环变

Page 271: Java Web动态图表编程

量 i 的值为 0,则程序第 97 行,绘制第 1条水平方向的虚线执行的语句是:

g2d.drawLine(120, 60, 570, 60);

这里的(120, 60)正好与销售折线的背景矩形左上角顶点(程序第 55行)的坐标相同。

接着绘制纵轴上销售量的说明文字。此时,字符串 str 的内容为“100” (100­0/3),然后程序

在第 101 行, 通过 g2d先调用 Graphics2D 的 getFontMetrics 方法, 返回一个 FontMetrics 对象,

再调用 FontMetrics 对象的 getAscent 方法,获得当前字符串变量 str 的上升值,并将该值赋给

整型变量 stringHeight。

程序第 102行,获得该字符串的宽度。

程序第 103行的代码,实现了两个效果:

Ø 无论当前字符串 str 的内容是什么,绘制完该字符串的最后一个字符后的横坐标正好

为 110,这就实现了右对齐的效果;

Ø 字符串高度的中点位置正好与水平虚线的纵坐标相同。

此时,第 1 条水平虚线和销售量说明文字就绘制完毕了。紧接着程序再次清空字符串 str 的内容。然后返回到程序第 91行,进行第 2 次绘制,一直绘制到第 10 条水平虚线以及销售

量说明文字。注意,绘制的方向是从上向下的。

本段代码的运行结果如 7.8③所示。

程序第 108行~第 111 行:

g2d.setStroke(new BasicStroke(3.0f)); g2d.setColor(new Color(53, 76, 112)); g2d.drawLine(120, 50, 120, 360); g2d.dzrawLine(120, 360, 570, 360);

实现绘制坐标轴的功能。首先重新设置当前绘图环境笔划宽度的属性为 3像素,然后设

置当前的绘图颜色 RGB 值为(53,76,112)所代表的颜色。最后绘制宽度为 3 像素的两条直

线:第 1 条直线的两个端点坐标为(120,50)和(120,360),是条垂直的直线,表示纵轴;第 2 条直线的两个端点坐标为(120,360)和(570,360),是一条水平的直线,表示横轴。

程序第 114 行~第 115 行:

g2d.setFont(new Font("黑体", Font.PLAIN, 15)); g2d.drawString("月销售量", 40, 45);

绘制纵轴的标题。

程序执行第 115行后的结果如图 7.8④所示。

程序第 118 行~第 123 行:

TriangleServlet ts = new TriangleServlet(); ts.setFillColor(new Color(53, 76, 112)); ts.setBaseLine(10); ts.setAlpha(60); ts.drawTrigangle(570, 360, 2, 2, g2d); // 绘制横坐标轴上的箭头

ts.drawTrigangle(120, 50, 1, 2, g2d); // 绘制纵坐标轴上的箭头

上述代码在 JSP文档中的用法, 是在本书中第 1次出现。 这里演示了如何实现调用 Servlet 进行绘图的功能。TriangleServlet 是我们在第 5 章讲过的绘制三角形的 Servlet。回忆一下,

我们以前的调用方法, 都是在 JSP/HTML文档中,通过 HTML语法中显示一幅图像的标记来

实现。类似的用法,我们以前在绘制 3D 甘特图中也介绍过,但那是在 Servlet 中,调用另一

个 Servlet (详见第 5 章第 5.10 节的相关内容)。 本例是在 JSP 文档中, 该方法调用了位于 Web 应用程序根目录WEB­INF\classes\com\fatcat\webchart 下的 Triangle Servlet.java 文件。所以我

们在程序第 11行,引入了该子目录下所有的类。引入 TriangleServlet 类后,我们就可以按照

Page 272: Java Web动态图表编程

平常操作 Java 类的方式,来使用 TriangleServlet 类。

首先调用 TriangleServlet 类默认的构造器,创建一个 TriangleServlet 类的实例 ts。然后设

置 ts 的填充颜色(RGB值:53,76,112,和坐标轴颜色相同),再设置 ts 的底边长度(10像素)

以及底角的度数(60 度),接着调用 TriangleServlet 类 drawTrigangle 方法绘制三角形。 drawTrigangle方法接收 5 个参数,并在坐标(570,360)处填充了一个实心,方向向右的三角

形;在坐标(120,50)处,填充了一个实心的,方向向上的三角形。

程序执行第 123 行后的运行结果如图 7.9 所示。

图 7.9 lineChart.jsp的运行过程 3

此时,除销售折线以外的所有组件都已经绘制完毕。让我们进入最后的销售折线和图例

的绘制工作。我们采用了以前介绍过的绘制折线的方法来绘制折线。

我们来回忆一下,绘制折线所需要的参数有 3 个:两个整型数组,分别表示折线每个端

点的横坐标和纵坐标的集合,还有 1 个整型变量,表示需要连接在一起的折线端点的数目。

因此我们需要提供两个整型数组, 每个数组包含有 12个元素。 其中一个数组表示月份的绘制

数据,另一个数组表示该月销售量的绘制数据,还要提供一个整型参数,用来将这 12 个端点

连接起来。

程序第 125行~第 130 行:

String[] bookTitle = "JAVA", "C#" ; Color[] bookColor = Color.RED, Color.ORANGE ; int[] sales = new int[12]; int[] month = new int[12];

g2d.setFont(new Font("Courier New", Font.PLAIN, 12));

程序第 125行,定义了 1 个字符串数组 bookTitle,包含两个字符串变量“JAVA” 、 “C#” ,

表示要绘制的折线名称。 本例共绘制了两条折线, 一条模拟 Java 类编程书籍的月销售量折线;

另外一条模拟 C#类编程书籍的月销售量折线。

程序第 126 行,定义了 1 个 Color 数组,用于绘制折线时需要填充的颜色。接着创建了

两个整型数组,如前所述,每个数组包含 12 个整型变量。sales 数组表示每个月销售量的数

据,month表示月份的绘制数据。

程序第 131行~第 153 行,绘制销售折线及图例:

for (int i = 0; i < bookTitle.length; i++)

// 初始化绘制数据 int bookSales = 0;

Page 273: Java Web动态图表编程

for (int j = 0; j < sales.length; j++) bookSales = 45+(int)(Math.random() * 50); sales[j] = 360­bookSales * 3; month[j] = 120+j * 40;

g2d.setColor(Color.GRAY); g2d.setStroke(new BasicStroke(5.0f)); g2d.setColor(bookColor[i]);

// 绘制月销售量折线 g2d.drawPolyline(month, sales, sales.length);

// 绘制图例 g2d.fillRect(30, 140+i * 20, 10, 10); g2d.setColor(Color.BLACK); g2d.drawString(bookTitle[i], 45, 150+i * 20);

本段代码共循环了两次,我们仍然以第一次循环来说明本段代码的运行过程。

首先程序在第 134 行,定义了一个局部的整型变量 bookSales,用于临时储存随机生成的

销售量数据。程序第 135行~第 140行,利用一个循环来生成绘制折线所需要的所有数据。

程序第 137行, 生成了一个 45到 95之间的随机数, 该随机数被赋值给局部变量 bookSales。

前面我们说过,折线绘制区域的横轴所在的纵坐标为 360,因此,销售量 N的纵坐标的位置应

为 360−3N。如图 7.10 所示。假设第一次生成的随机数 bookSales 的值为 70,则在宽度和高度

为 440X300像素的折线绘制区域内。销售量为 80的在纵轴上的绘制坐标为:

360 – 3 X 70 = 150

从图 7.10 还可以看出,销售量 N 的横坐标的位置和表示该月垂直虚线的横坐标完全相

同。因此程序在第 139 行,设定横坐标的计算方法是:

month[j] = 120+j * 40;

循环 12 次后,销售量的绘制信息就全部计算完毕了。

程序第 143行~第 144 行:

g2d.setStroke(new BasicStroke(5.0f)); g2d.setColor(bookColor[i]);

Page 274: Java Web动态图表编程

图 7.10 销售量绘制信息的计算方法

重新设置笔划为 5 像素粗,其他笔划属性采用默认值。然后设置绘制颜色。

程序第 147行,调用 Graphics2D 的 drawPolyline方法绘制折线。这里我们提供了 3 个参

数,包括两个整型数组 month和 sales。最后提供了一个整型参数 sales.length,表示要将这 12 个点都连接起来。这里绘制折线的笔划为 5 像素。

程序第 150行~第 152 行:

在折线的绘制区域,纵轴的左侧绘制图例。图例由两部分组成:

Ø 与书籍销售量折线颜色相同的一个小方块;

Ø 书籍的名称。

然后程序返回到第 131 行,再次进入循环,绘制剩下的销售量折线。

接着就是对缓冲图像进行编码,并向客户端输出该图像。程序最终绘制结果如图 7.6 所

示。

从本例可以看出,Java2D 对图形处理的强大功能。

2.3D 线段图表

本节介绍两种绘制 3D 线段图的方法,首先让我们回忆一下,第 2章 2.7.2节所介绍的利

用绘制多个矩形来生成 3D 矩形的方法。运用其绘制原理同样可以生成 3D 线段。

我们以 lineChart.jsp 为蓝本,编写了生成 3D 线段图的 line3DChart1.jsp( \chap07\ Fig7.4\Fig.7.4_02)。先看看 line3DChart1.jsp的运行结果,如图 7.11 所示。

我们可以观察到,折线已经具有很强的立体感了。程序 line3DChart1.jsp除了绘制 3D 折

线的代码与前例不同外,其他的代码完全相同,因此我们仅介绍本程序中绘制 3D 折线的这

一部分代码。

程序第 177行:

int thickness = 15;

Page 275: Java Web动态图表编程

图 7.11 3D销售折线图表的绘制方法 1

设置折线的厚度为 15像素。

程序第 180行:

drawLineShadow(month, sales, thickness, bookColor[i], g2d);

调用方法 drawLineShadow,程序会立即转到第 15 行~第 42 行的 drawLineShadow 方法

体中去执行。drawLineShadow方法,实现了绘制折线阴影的目的。下面来看看如何实现该功

能: public void drawLineShadow(int xPoint[], int yPoint[], int thickness,

Color color, Graphics2D g2d)

int shadowX[] = new int[xPoint.length]; int shadowY[] = new int[yPoint.length];

for (int i = thickness; i >= 0; i­­)

if (i == thickness || i == 0)

g2d.setStroke(new BasicStroke(2.0f)); g2d.setColor(color.darker());

else

g2d.setStroke(new BasicStroke(4.0f)); g2d.setColor(color);

for (int j = 0; j < xPoint.length; j ++)

shadowX[j]=xPoint[j] + i; shadowY[j]=yPoint[j] ­ i;

g2d.drawPolyline(shadowX, shadowY, shadowX.length);

方法 drawLineShadow 需要 5 个参数。第 1 个和第 2 个整型数组,表示原始的销售折线

分别在横轴和纵轴上的绘制参数;第 3个整型参数表示销售折线需要绘制的厚度;第 4 个参

数是一个 Color 类的实例,用于表示绘制的颜色;最后一个参数是图形绘制环境 Graphics2D 的一个实例。

Page 276: Java Web动态图表编程

在 drawLineShadow 方法体(程序第 18 行~第 41 行)中,首先定义了两个整型数组 shadowX[] 和 shadowY[],分别表示折线阴影在横轴和纵轴上的绘制参数。其数组元素的数

目,分别由原始的折线数组元素的个数决定。然后程序进入一个循环语句中(程序第 21行~

第 41 行),循环次数由参数 thickness 决定。每循环一次,先设置绘图笔划的宽度以及绘制颜

色,接着计算折线阴影的绘制数据,最后根据计算结果和图形环境的绘制参数进行绘制。

为了绘制图表的效果更美观,在循环体中,我们首先对当前绘制的阴影做了一个判定,

以设置其笔划的宽度和颜色。如果是第一次或最后一次绘制阴影,则设置其绘制笔划的宽度

为 2 像素,绘制的颜色比参数 color 更深一些的颜色(程序第 25 行~第 26 行)。否则,设置

其绘制笔划的宽度为 4 像素,绘制的颜色就是参数 color(程序第 30 行~第 31 行)。然后计

算折线阴影在横轴和纵轴上各自的 12 个绘制参数。根据第 2 章第 2.7.2 节的例程,可采用以

下的方法绘制 4 种不同风格的阴影。

绘制风格 1: for (int j = 0; j < xPoint.length; j ++)

shadowX[j]=xPoint[j] + i; shadowY[j]=yPoint[j] ­ i;

绘制风格 2:

for (int j = 0; j < xPoint.length; j ++)

shadowX[j]=xPoint[j] + i; shadowY[j]=yPoint[j] + i;

绘制风格 3:

for (int j = 0; j < xPoint.length; j ++)

shadowX[j]=xPoint[j] ­ i; shadowY[j]=yPoint[j] ­ i;

绘制风格 4:

for (int j = 0; j < xPoint.length; j ++)

shadowX[j]=xPoint[j] ­ i; shadowY[j]=yPoint[j] + i;

读者可以将代码修改成这几种方式,看看实际的运行效果有什么不同。绘制完折线所有

的阴影部分,就结束了 drawLineShadow方法。程序返回到第 183 行:

g2d.drawPolyline(month, sales, sales.length);

绘制原始的折线。最后是绘制图例,以及图表编码和输出过程。

我们在讲述第 2 章第 2.10.5 节内容的时候,讨论了另外一种 3D 矩形,也就是立方体的

绘制方法。

这里我们借鉴其绘制思路,来绘制 3D 折线的阴影部分。 Line3DChart2.jsp(\chap07\Fig7.4\Fig.7.4_02)演示了如何利用该思路来绘制线段的阴影

部分,运行效果如图 7.12 所示。

Page 277: Java Web动态图表编程

图 7.12 3D销售折线图表的绘制方法 2

程序第 187行:

int thickness = 25;

创建整型变量 thickness 并赋值 thickness。本例中,折线的厚度为 25 像素。

程序第 190行:

int alpha = 45;

创建了一个新的整型变量 alpha,表示原始折线与其阴影之间的夹角大小。

程序第 193行:

drawLineShadow( month, sales, thickness, alpha, bookColor[i], g2d);

调用 drawLineShadow 方法,绘制折线的阴影。本例中的 drawLineShadow 方法,需要 6 个参数。同前例相比,多提供了一个表示原始折线与其阴影之间夹角大小的整型变量 alpha。

现在程序跳转到第 14行~第 40 行的 drawLineShadow方法中执行:

public void drawLineShadow(int xPoint[], int yPoint[], int thickness, int alpha, Color color, Graphics2D g2d)

int shadowX[] = new int[xPoint.length]; int shadowY[] = new int[yPoint.length];

for (int i = thickness; i >= 0; i­­)

if (i == thickness || i == 0)

g2d.setStroke(new BasicStroke(2.0f)); g2d.setColor(color.darker());

else

g2d.setStroke(new BasicStroke(4.0f)); g2d.setColor(color);

for (int j = 0; j < xPoint.length; j ++)

shadowX[j]=xPoint[j] + getOffsetX (alpha, i);

shadowY[j]=yPoint[j] – getOffsetY (alpha, i);

Page 278: Java Web动态图表编程

g2d.drawPolyline(shadowX,shadowY,

shadowX. length);

drawLineShadow 方法的运行过程和前例基本相同,只是在程序第 35 行,调用了一个 getOffsetX(程序第 43 行~第 46 行)的方法。getOffsetX 返回一个整数值,表示当前阴影距

离原始折线在水平方向上的偏移量:

public int getOffsetX(int alpha, int thickness)

return (int)(Math.cos(alpha * Math.PI / 180) * thickness + 0.5);

水平方向上的偏移量是通过计算折线阴影的厚度与夹角 alpha的余弦值的乘积而获得的。

注意,我们同样在 Math.cos(alpha * Math.PI / 180)* thickness 后面加上了一个 0.5 的浮点数,

实现对浮点数的小数部分,进行 4 舍 5入的处理。

程序第 36 行,调用 getOffsetY(程序第 49 行~第 52 行)的方法。getOffsetY 返回一个

整数值表示当前阴影和原始折线在垂直方向上的偏移量。垂直方向上的偏移量是通过计算折

线阴影的厚度与夹角 alpha 的正弦值的乘积而获得的,如下所示:

public int getOffsetY(int alpha, int thickness)

return (int)(Math.sin(alpha * Math.PI / 180) * thickness + 0.5);

获得所有绘制折线的数据后,程序在第 38 行,调用 drawPolyline方法,绘制折线阴影。

结束整个循环后,程序再返回并执行第 196 行:

g2d.drawPolyline(month, sales, sales.length);

绘制原始位置的折线。

剩下的绘制过程此处不再赘述。本例首先通过两个参数 thickness 和 alpha 绘制阴影,然

后再绘制原始位置的折线。我们可以利用这个思路来绘制其他的 3D 图形。

7.5 创建基本 Java2D 图形

7.5.1 Java2D 图形(Shape)接口概述

Shape(图形)是任何实现 Shape接口类的一个实例。Shape轮廓(轮廓线)是指它的路

径。

我们知道,在 Graphics2D 中,无论是描绘还是填充操作都需要一个实现 Shape接口的实

例对象(即任意形状的一个图形)。该对象可以用其父类 Graphics 的一些方法来创建一些标

准的几何图形。例如第 2 章讨论过的:线段(Line)、矩形(Rectangle)、圆角矩形(Round Rectangle)、圆弧(Arc)、椭圆(Oval)、折线(Polyline)以及多边形(Polygon)等,因为

它们都实现了 Shape接口。

除此之外,Java2D 还可以生成复杂的几何图形。这些复杂几何图形的创建是通过 java.awt.geom包中的一些类来实现的。例如:

Arc2D, Arc2D.Double, Arc2D.Float, Area, BasicTextUI.BasicCaret, CubicCurve2D, CubicCurve2D.Double, CubicCurve2D.Float, DefaultCaret, Ellipse2D, Ellipse2D.Double,

Page 279: Java Web动态图表编程

Ellipse2D.Float, GeneralPath, Line2D, Line2D.Double, Line2D.Float, QuadCurve2D, QuadCurve2D.Double, QuadCurve2D. Float, Rectangle2D, Rectangle2D.Double, Rectangle2D. Float, RectangularShape, RoundRectangle2D, RoundRectangle2D.Double, Round Rectangle2D.Float

这些类分别代表了一种图形,并用单精度浮点数,Arc2D.Float、Rectangle2D.Float 等,

或者双精度浮点数,Arc2D.Double、Rectangle2D.Double 等来指定图形的尺寸。双精度实现

的绘制精度较高,但在某些平台上会降低性能。

在上述这些类中,组成形式是“外部类名”+“.”+“静态嵌套类名” 。这里的“.”表

示点运算符。根据 Java 的语法规则,要调用这些静态嵌套类,只需要用外部类名限定该嵌套

类名就可以了。例如:

Shape ellipse = new Ellipse2D.Double( 15, 40, 150, 200); Shpae line = new Line2D.Double(10, 10, 80, 80);

我们所指描绘某个 Shape也就是对 Shape路径(轮廓)用当前在 Graphics2D 图形绘制环

境中定义的笔划、颜色、填充模式的绘制属性进行绘制。而所谓的填充某个 Shape,即在其

路径区,用当前在 Graphics2D 图形绘制环境中定义的笔划、颜色、填充模式的绘制属性来填

充一个实心 Shape。 GeneralPath是一种表示由直线、二次曲线、三次曲线构造的二维对象的复杂图形。Java

2D API还提供了支持构造几何区域形状的特殊类型,如 Area。

上述类中包含了丰富的方法。我们选择其中一些重要的内容,深入学习如何创建 Java2D 图形。

7.5.2 Point2D 不要

该类位于 java.awt.geom.Point2D 包中。代表在 Java 坐标系中,坐标为(x, y)的某个点,

包括浮点和双精度两种方式:Point2D.Float 和 Point2D.Double。java.awt 包中也有一个 Point 类,用于定义一个点,只是接收的参数为 int 类型。该类(java.awt.Point)和 Point2D.Double,

以及 Point2D.Float 的父类都是 Point2D 类。

表 7.3 列出了 Point2D 的构造器及部分方法。

表 7.3 Point2D的构造器及方法

方法:public Point2D.Double()

说明:创建一个坐标为(0, 0)点

方法:public Point2D.Double()(double x, double y)

说明:创建一个坐标为(x, y)点

方法:public double getX()

说明:返回一个双精度值,表示坐标上某点的横坐标

方法:public double getY()

说明:返回一个双精度值,表示坐标上某点的纵坐标

方法:public double distance(double PX, double PY)

说明:返回一个双精度值,表示当前点到指定的(PX,PY)点之间的距离

方法:public static double distance(double X1, double Y1, double X2, double Y2)

说明:静态方法,返回一个双精度值,表示点(X1, Y1)到点(X2,Y2)之间的距离

方法:public double distance(Point2D pt)

说明:返回一个双精度值,表示当前点到指定的 Point2D 对象 pt 之间的距离

Page 280: Java Web动态图表编程

续表

方法:public static double distanceSq(double X1, double Y1, double X2, double Y2)

说明:静态方法,返回一个双精度值,表示点(X1, Y1)到点(X2,Y2)之间距离的平方

方法:public abstract void setLocation(double x, double y)

说明:将当前点的坐标设置成新的坐标为(x, y)的位置

方法:public void setLocation(Point2D p)

说明:将当前点的坐标设置成指定的 Point2D 对象 p的位置

上述这些方法,我们仅以接收双精度型参数以及返回双精度型结果的构造器及其相关方

法为例。单精度型的构造器及其方法与其类似,只是接收的参数和返回的类型不同。

下面列出了上述构造器及其部分方法的用法:

Point2D.Double p1 = new Point2D.Double(1.5, 5.5); Point p2 = new Point(20, 30); double distance1 = p1.distance(p2); double distance2 = Point2D.distance(1.5, 5.5, 20, 30);

7.5.3 Line2D 不要

该类位于 java.awt.geom.Line2D 包中。表示在 Java 坐标系中任意两点之间的线段。用来

指定直线,包括浮点和双精度两种:Line2D.Float 和 Line2D.Double。

表 7.4 列出了 Line2D 的构造器及其部分方法。

表 7.4 Line2D的构造器及其方法

方法:public Line2D.Double()

说明:创建一个从坐标(0, 0)到(0, 0)的线段

方法:public Line2D.Double(double x1, double y1, double x2, double y2)

说明:创建一个从坐标(x1, y1)到(x2, y2)的线段

方法:public Line2D.Double(Point2D p1, Point2D p2)

说明:创建一个连接两个 Point2D 实例 p1 和 p2 之间的直线

方法:public boolean intersectsLine(Line2D l)

说明:比较当前 Line2D 对象,是否和指定的 Line2D 对象相交,如果相交,则返回真值,否则返回假值

下面列出了上述构造器及其部分方法的用法:

Point2D.Double p1 = new Point2D.Double(1.5, 5.5); Point p2 = new Point(20, 30); Line2D.Double line1 = new Line2D.Double(p1, p2); Line2D.Double line2 = new Line2D.Double(1.0, 2.0, 30, 50);

if (line1.intersectsLine(line2))

System.out.print("Line1 和 Line2 相交!"); else

System.out.println("Line1和 Line2不相交!");

7.5.4 Rectangle2D 不要

该类位于 java.awt.geom.Rectangle2D 包中。用来指定矩形,包括浮点和双精度两种方式:

Page 281: Java Web动态图表编程

Rectangle2D.Float 和 Rectangle2D.Double。

表 7.5 列出了 Rectangle2D 的构造器及其部分方法。

表 7.5 Rectangle2D的构造器及其方法

方法:public Rectangle2D.Double()

说明:创建一个矩形,其左上角顶点坐标为(0, 0),宽度和高度都为 0

方法:public Rectangle2D.Double(double x, double y, double w, double h)

说明:创建一个矩形,其左上角顶点坐标为(x, y),宽度为参数 w 指定的值,高度为参数 h指定的值

方法:public double getHeight()

说明:返回一个双精度值,表示矩形的高度

方法:public double getWidth()

说明:返回一个双精度值,表示矩形的宽度

方法:public double getX()

说明:返回一个双精度值,表示矩形左上角顶点的横坐标

方法:public double getY()

说明:返回一个双精度值,表示矩形左上角顶点的纵坐标

方法:public boolean isEmpty()

说明:返回一个布尔值,如果当前矩形的高度或宽度为 0,或者两者同时为 0,则返回真,反之,则返回假

方法:public boolean contains(double x, double y)

说明:返回一个布尔值,如果坐标为(x,y)的点位于当前矩形内,则返回真,否则,返回假

方法:public void setRect(double x, double y, double w, double h)

说明:将当前矩形重新设置成左上角顶点坐标为(x,y),宽度为 w,高度为 h的矩形

方法:public Rectangle2D createIntersection(Rectangle2D r)

说明:创建一个新的矩形。该矩形由当前矩形和指定矩形 r 之间重叠的部分创建,如图 7.13①所示

方法:public Rectangle2D createUnion(Rectangle2D r)

说明:创建一个新的矩形。该矩形由当前矩形和指定矩形 r 之间合并组成,如图 7.13②所示

Rectangle2DTest.jsp (\chap07\Fig7.5\Fig.7.5_04)演示了上述构造器及其部分方法的用法,

运行结果如图 7.13 所示。

图 7.13 Rectangle2D的使用方法

程序第 35 行~第 36行:

Rectangle2D.Float r1 = new Rectangle2D.Float(10.0f, 10.0f, 100.0f, 70.0f); Rectangle2D.Float r2 = new Rectangle2D.Float(60.0f, 50.0f, 100.0f, 70.0f);

Page 282: Java Web动态图表编程

调用 Rectangle2D.Float 的构造器,创建了两个 Rectangle2D.Float 的实例:r1 和 r2。

程序第 38 行~第 39行:

Rectangle2D.Double r3 = new Rectangle2D.Double(180.0, 10.0, 100.0, 70.0); Rectangle2D.Double r4 = new Rectangle2D.Double(230.0, 50.0, 100.0, 70.0);

调用 Rectangle2D.Double的构造器,创建了两个 Rectangle2D.Double的实例:r3 和 r4。

程序第 42 行~第 43行:

g2d.fill(r1.createIntersection(r2)); g2d.fill(r3.createUnion(r4));

调用 Graphics2D 的 fill 方法,用黄色(程序第 32 行设定) 首先填充由矩形 r1 和 r2 重

叠部分组成的新的矩形,如图 7.13①中所示的阴影矩形区域;然后再填充由 r3 和 r4 合并而

组成的矩形,如图 7.13②所示的阴影区域。

程序第 46 行~第 49行,定义了一个笔划对象 bs:

float[] dashes = 3.f ; BasicStroke bs = new BasicStroke(1.0f, BasicStroke.CAP_ROUND,

BasicStroke.JOIN_ROUND, 10, dashes, 0); g2d.setStroke(bs);

程序第 52 行~第 56行,用笔划对象 bs及黑色来描绘这 4 个矩形:

g2d.setPaint(Color.BLACK); g2d.draw(r1); g2d.draw(r2); g2d.draw(r3); g2d.draw(r4);

调用 Graphics2D 类的 draw 方法,描绘当前图形的轮廓,如图 7.13①中所示的两个黑色

虚线矩形及如图 7.13②所示的两个黑色虚线矩形。

可以清楚地看到,调用 Rectangle2D 类的 createIntersection方法,得到的新矩形是两个矩

形的交集,而 createUnion返回的是两个矩形的并集。

在 Graphics2D 中,不必再向 Graphics 类调用 drawXXXX 方法或 fillXXXX方法,来描绘

一个图形的轮廓或填充一个实心图形。如果要描绘图形的轮廓,则只需要将该图形的引用作

为参数传递给 draw方法就可以了; 如果要填充图形, 则只需要将该图形的引用作为参数传递

给 fill 方法就可以了。相比之下,Java2D 的方法要清晰很多。虽然生成双精度的图形效果最

好,但是大量的双精度运算可能会降低系统效率。因此,我们建议一般情况下,采用单精度

或整数型图形就可以了。

7.5.5 RoundRectangle2D 不要

该类位于 java.awt.geom.RoundRectangle2D 包中,用来指定圆角矩形。包括浮点和双精

度两种方式:RoundRectangle2D.Float 和 RoundRectangle2D.Double。

表 7.6 列出了 RoundRectangle2D 的构造器及其部分方法。

表 7.6 Arc2D的构造器及其方法

方法:public RoundRectangle2D.Double()

说明:创建一个圆角矩形,其左上角顶点坐标为(0, 0),宽度和高度都为 0,并且圆角的弧宽和弧高也都为 0

方法:public RoundRectangle2D.Double

(double x, double y, double w, double h, double arcw, double arch)

说明:创建一个圆角矩形,其左上角顶点坐标为(x, y),宽度为 w,高度为 h,圆角的弧宽为 arcw,弧高为 arch

Page 283: Java Web动态图表编程

方法:public double getHeight()

说明:返回一个双精度值,表示圆角矩形的高度

方法:public double getWidth()

说明:返回一个双精度值,表示圆角矩形的宽度

方法:public double getX()

说明:返回一个双精度值,表示圆角矩形左上角顶点的横坐标

方法:public double getY()

续表

说明:返回一个双精度值,表示圆角矩形左上角顶点的纵坐标

方法:public boolean isEmpty()

说明:返回一个布尔值,判断当前圆角矩形是否为空

方法:public double getArcWidth()

说明:返回一个双精度值,表示圆角矩形圆角的弧宽

方法:public double getArcHeight()

说明:返回一个双精度值,表示圆角矩形圆角的弧高

方法:public double getCenterX()

说明:返回一个双精度值,表示圆角矩形几何中心的横坐标

方法:public double getCenterY()

说明:返回一个双精度值,表示圆角矩形几何中心的纵坐标

Java2D 圆角矩形和 Java2D 普通矩形之间, 惟一区别就是圆角矩形的四个角不再是直角,

而变成了四段圆弧,圆弧的宽度和高度可以任意设定。

下一节,我们将综合利用目前学到的 Web 图表绘制知识,示例一个 Java2D 普通垂直柱

状图以及 3D 垂直柱状图。

7.5.6 Java2D Web 图表实例之柱状图

1.普通垂直柱状图表

我们已经学习了在 Applet、Servlet 中如何绘制垂直柱状图,但那些都是基于 Java.awt 范

畴的。现在我们来讨论基于 Java2D 的垂直柱状图。图表绘制的数据与 7.4 节相同。

图 7.14 vBarChart.jsp的运行结果

Page 284: Java Web动态图表编程

先看看 vBarChart.jsp(\chap07\Fig7.5\Fig.7.5_04)运行结果。 vBarChart.jsp 的绘制背景步骤和前面介绍的 lineChart.jsp 思路是一样的。下面我们逐步

深入地阐述其运行过程。

程序第 30 行~第 31行:

g2d.setPaint(Color.WHITE); g2d.fillRect(0, 0, width, height);

先将绘图环境的整个背景设置为白色。

程序第 34 行~第 39行:

GradientPaint grayGP = new GradientPaint(0, 0, Color.GRAY, width, height, new Color(218, 214, 212), false);

g2d.setPaint(grayGP); RoundRectangle2D.Float bgRR =

new RoundRectangle2D.Float(5, 5, width­5, height­5 , 50, 50); g2d.fill(bgRR);

创建了一个渐进色对象 grayGP。渐进色的第 1 个颜色为灰色,坐标为(0, 0),第 2 个颜

色 RGB 值分别为:218、214、212 的颜色对象表示浅灰色。坐标为绘制环境右下角顶点的坐

标,说明此渐进色变化方向是从左上角到右下角变化的,然后设定该渐进色为非循环风格。

设置绘图环境当前的填充模式为 grayGP 对象所表示的渐进色对象。使用该 grayGP 属性来填

充一个圆角矩形,该圆角矩形是一个由深灰色逐渐变化到浅灰色的圆角矩形。这里我们创建

了基于 Java2D 单精度的 RoundRectangle2D 实例 bgRR。

程序第 42 行~第 45行:

GradientPaint blueGP = new GradientPaint(0, 0, new Color(14, 97, 147), 0, height, new Color(240, 243, 247), false);

g2d.setPaint(blueGP); g2d.fillRoundRect(0, 0, width ­ 5, height ­ 5, 50, 50);

创建了一个渐进色对象 blueGP。渐进色第 1 个颜色 RGB 值分别为 14、97、147 表示的

深蓝色,坐标为(0, 0)。第 2 个颜色 RGB 值分别为:240、243、247的颜色对象表示淡蓝色,

坐标为绘制环境右下角顶点的坐标,说明此渐进色变化方向是从上到下垂直变化的,然后设

定该渐进色为非循环风格。 接着用 blueGP填充一个圆角矩形, 该圆角矩形和前面创建的 bgRR 大小一样,只是位置不同,此圆角矩形覆盖在 bgRR 之上,位于 bgRR 前方及上方 5像素处。

程序第 48 行~第 51行:

BasicStroke bs = new BasicStroke(1.2f); g2d.setStroke(bs); g2d.setPaint(new Color(55,71, 105)); g2d.drawRoundRect(0, 0, width ­5, height ­5, 50, 50);

创建了一个笔划宽度为 1.2 像素的 BasicStroke对象 bs。然后用 RGB 值分别为 55、71、 105 的颜色,绘制蓝色圆角矩形的轮廓。

这两个圆角矩形绘制完成后,就形成了一个具有浅灰色阴影的蓝色圆角矩形的绘制效

果。运行结果如图 7.15 所示。

Page 285: Java Web动态图表编程

图 7.15 vBarChart.jsp的运行过程 1

程序第 54 行~第 57行:

Rectangle2D.Float drawArea = new Rectangle2D.Float(63, 48, 400, 300);

g2d.setPaint(Color.GRAY); g2d.fill(drawArea);

创建了一个单精度 Rectangle2D 对象 drawArea,表示Web 直方图的绘制区域。这里我们

先填充一个灰色的矩形,用于实现该直方图绘制区域的阴影效果。

程序第 60 行~第 62行:

g2d.setPaint(Color.WHITE); drawArea = new Rectangle2D.Float(60,45, 400, 300); g2d.fill(drawArea);

重新定义 drawArea 坐标,然后在新的位置重新用白色填充 drawArea 矩形。

程序第 65 行~第 66行:

g2d.setPaint(Color.BLACK); g2d.draw(drawArea);

用黑色绘制矩形 drawArea 轮廓。程序运行效果如图 7.16 所示。

程序第 69 行~第 74行:

String chartTitle = "计算机编程类图书 2004年月销售量统计图"; g2d.setFont(new Font("华文隶书", Font.PLAIN, 25)); int stringLength = g2d.getFontMetrics().stringWidth(chartTitle);

g2d.setColor(Color.WHITE); g2d.drawString(chartTitle, (width ­ stringLength) / 2, 25 );

Page 286: Java Web动态图表编程

图 7.16 vBarChart.jsp的运行过程 2

绘制该图表的标题。这里先创建了一个整型变量 stringLenth,然后调用 FontMetrics 的 stringWidth方法,获取标题的绘制长度。然后用(width – stringLength)/ 2 的方法实现标题

居中。

程序第 77 行~第 85行:

float[] dashes = 3.f ; bs = new BasicStroke(1.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN

_ROUND, 10, dashes, 0);

g2d.setStroke(bs);

String str = ""; stringLength = 0; g2d.setPaint(Color.BLACK); g2d.setFont(new Font("宋体", Font.PLAIN, 12));

设置当前绘图环境的笔划属性。首先创建一个值为 3.0 的单精度浮点数 float 数组,然后

调用 BasicStroke 另一个版本的构造器,将重新生成的 BasicStroke 对象赋值给变量 bs。这次

定义的笔划属性是:笔划的外观为虚线,宽度为 1 像素,线段断点的风格为半圆,线段转折

处的风格为圆角,虚线的线段长度为 3像素,虚线和虚线之间的空白也为 3 像素,从开头部

分开始绘制。接着设置图像环境的当前绘制字体为“Courier New” ,字体大小为 12 磅,风格

为普通。创建了一个字符串变量 str,并将 str的内容设置为空。然后将整型变量 stringLength 的值重新设置为 0,最后设置当前字体为宋体、12 磅、普通风格。

程序第 86 行~第 98行:

for (int i = 1; i <= 12; i++)

// 绘制垂直方向虚线

g2d.drawLine(60+i * 400/13, 45, 60+i * 400/13, 345);

// 绘制横轴上月份的说明文字

str += i + "月"; stringLength = g2d.getFontMetrics().stringWidth(str); g2d.drawString(str, 60+i * 400/13 ­ stringLength / 2, 360);

Page 287: Java Web动态图表编程

// 重置月份说明文字

str = "";

本段代码绘制垂直方向的虚线, 然后绘制横轴上关于月份的说明文字, 绘制结果如图 7.17 所示。

程序第 100行~第 102 行:

g2d.setFont(new Font("Arial", Font.BOLD, 14)); str = ""; int stringHeight = 0;

重新设置绘图环境当前字体属性,以及清空字符串 str 的内容。定义一个整型变量 stringHeight 用于表示字符串的高度。

程序第 104行~第 115 行:

for (int i = 0; i <= 300; i += 30)

// 绘制水平方向虚线 g2d.drawLine(60, 45+i, 460, 45+i);

// 绘制纵轴上销售量的说明文字 str += 100­i / 3; stringHeight = g2d.getFontMetrics().getAscent(); stringLength = g2d.getFontMetrics().stringWidth(str); g2d.drawString(str, 55­stringLength, 45+i + stringHeight / 2); str = "";

这段代码的循环,就是绘制水平方向的虚线以及纵轴上销售量的说明文字。同样,这里

通过 g2d 先调用 Graphics2D 的 getFontMetrics 方法,该方法返回一个 FontMetrics 对象,再调

用该 FontMetrics 对象的 getAscent 方法,获得当前字符串变量 str的上升值(这里的上升值,

就是字符串的高度),并将该值赋给整型变量 stringHeight。最后实现字符串在水平方向上的右

对齐和在垂直方向上的居中对齐的目的。运行的结果如图 7.18所示。

Page 288: Java Web动态图表编程

图 7.17 vBarChart.jsp的运行过程 3 图 7.18 vBarChart.jsp的运行过程 4

程序第 117 行~第 124 行:

String[] bookTitle = "JAVA", "C#" ; Color[] bookColor = new Color(230,111, 71) , new Color(107,165,239); double[] sales = new double[12]; int[] month = new int[12];

g2d.setFont(new Font("Courier New", Font.PLAIN, 12)); g2d.setStroke(new BasicStroke()); Rectangle2D.Double bar ;

这段代码对我们将要绘制的柱状图所需要的绘制参数进行创建和初始化。 程序第 123行,

将笔划属性重新设置为系统的默认笔划。程序第 124行,声明了一个双精度型的 Rectangle2D 对象 bar。

程序第 126 行~第 163 行,进入一个循环绘制柱状图。本循环共循环 12 次,每次根据

实时生成的销售量数据绘制表示某个月的柱状图。在该循环体中程序第 129 行:

double bookSales = 0.0;

创建了一个局部的双精度浮点变量 bookSales,并将其值初始化为 0。之后,程序进入一

个循环,见程序第 131 行~第 156 行。在嵌套的循环代码块中:

程序第 133行,生成一个 1.0到 296.0之间的双精度浮点数。模拟当月的书籍销售量。

bookSales = 1 + Math.random() * 295;

程序第 134行,计算该销售量在纵轴上的绘制坐标:

sales[j] = 345 ­ bookSales ;

因为图表绘制区域的最下方(横轴)的纵坐标为 345,所以,销售量在纵轴上的实际绘

制坐标为:345 – bookSales。

程序第 135行,计算该销售量在横轴上的绘制坐标:

month[j] = 60+ ((j+1) * 400)/13;

程序第 136行~第 137 行,初始化双精度型的 Rectangle2D 对象 bar:

bar = new Rectangle2D.Double(month[j]­6 + i *10, sales[j]+2, 10, bookSales­2);

Page 289: Java Web动态图表编程

注意,这里我们设置 bar 的横坐标为 month[j]­6 + i *10,纵坐标为 sales[j]+2。为什么要设

置这样的坐标呢?请看图 7.19所示。

图 7.19 直方图绘制参数示意图

这两行代码定义的柱状图绘制对象 bar 是图 7.19 中所示的灰色矩形,它实现了图中彩色

矩形的阴影效果。我们可以观察到,所有矩形的宽度都为 10 像素。每个灰色矩形位于其对应

的彩色矩形下方 2 像素、右方 4 像素处。

图中橘红色的矩形,代表的是 Java 书籍某个月的销售量,而蓝色矩形,代表的是 C#书

籍的销售量。并且,橘红色矩形和蓝色矩形紧紧连在一起,各自正好分列于垂直的虚线两侧。

彩色矩形还采用了渐进色的填充模式,彩色矩形顶端采用 bookColor[i]所代表的颜色,

其底部是 bookColor[i]更明亮一些的颜色,及 bookColor[i].brighter()所代表的颜色。

理解了这些参数所表达的含义,我们就不难理解上述代码了。

程序第 140行~第 141 行:

g2d.setPaint(Color.GRAY); g2d.fill(bar);

填充图中所示的灰色矩形。

程序第 143行~第 144 行:

bar = new Rectangle2D.Double(month[j]­10 + i *10, sales[j], 10, bookSales);

重新设置双精度型的 Rectangle2D 对象 bar 的坐标。

程序第 145行~第 147 行:

GradientPaint drawGP = new GradientPaint(month[j]­10 + i *10, (int)sales[j], bookColor[i], month[j]­10 + i *10, 345, bookColor[i].brighter(), false);

实现了刚才提及的在彩色矩形上进行渐进着色的功能。因此,这里定义了一个 GradinetPaint 的对象 grawGP。因为 drawGP 中的横坐标都相同(都是 month[j]­10 + i *10),

只是纵坐标不同,因此,该渐进色的变化方向是由上至下垂直变化的。

程序第 149行~第 155 行:

// 填充直方图 g2d.setPaint(drawGP); g2d.fill(bar);

// 描绘直方图轮廓 g2d.setPaint(Color.BLACK); g2d.draw(bar);

先利用定义的渐进色对象 drawGP 填充矩形,然后用黑色再次描绘该矩形的轮廓。

bookColor[I].brighter()

40

30

20

10

0 1 月 2 月 3 月 4 月

4

灰色知形横坐标:month[j].6+I*10

50

彩色矩形横

坐标:month [j]­10+I*10

2

bookColor[i]

1010

Page 290: Java Web动态图表编程

本段代码结束后,核心内容就绘制完成了。

程序第 159行~第 162 行:

g2d.setColor(bookColor[i]); g2d.fillRect(418, 50+i * 12, 10, 10); g2d.setColor(Color.BLACK); g2d.drawString(bookTitle[i], 432, 60+i * 12);

最后绘制图例。 我们将图例放置在柱状图绘制区域的右上方。 然后程序再次返回到第 126 行,进行下一次的绘制工作直至完成所有的循环。

vBarChart.jsp的最终运行结果如图 7.14 所示。

2.3D 垂直柱状图表

3D 垂直柱状图是前例的扩展。这里我们先看看

本例(\chap07\Fig7.5\Fig.7.5_04\ vBar3DChart. jsp)的

运行结果,如图 7.20所示。

与 vBarChart.jsp相比,图表有两个变化:

Ø 坐标轴绘制成具有 3D 效果的坐标轴了;

Ø 原来的矩形改变成立方体的样式了。

图表的其他部分基本没有什么变化。 本例的关键

就是如何实现上述两点变化。vBar3DChart. jsp 定义

的变量和方法比 vBarChart.jsp要多一些。 为了便于理

解 vBar3DChart.jsp 的源程序中定义的变量、方法及

其运行过程,请读者看图 7.21 所示 vBar3DChart.jsp 的图表绘制参数示意图。

图 7.21 参数示意图

我们对比图 7.21 来看 vBar3DChart.jsp中定义的变量及其所表示的对象:

Ø int basePointX, basePointY 表示 3D 垂直直方图的图表绘制区域的基准点,即图表绘制区域右下角顶点的横坐标及

纵坐标。

Ø int areaWidth, areaHeight 表示图表绘制区域的宽度和高度。

Ø int thickness

图表绘图区域左上角顶点

(topLeftPointX, topLeftPointY) 图表绘图区域宽度 areaWidth

垂直虚线

JAVA C# 3D坐标轴

垂直平行四边形 水平虚线

100 908070605040302010 0

基准点

(basePointX, basePointY) 角度 alpha 3D 坐标轴;

水平平行四边形

坐标轴厚度 thickness

图表

绘图

区域

高度

draw

Height

1月 2月 9月 10月 11月 12月

斜线

图 7.20 vBar3DChart.jsp的运行结果

Page 291: Java Web动态图表编程

表示 3D 坐标轴的厚度。

Ø int alpha 图 7.21 所示 3D 坐标轴其实是由两个黄色的平行四边形组成。其中一个是位于基准点右

方的水平方向的平行四边形;另一个是位于基准点上方垂直方向的平行四边形。这里的 alpha 指的是水平方向上的平行四边形底边和斜边之间的夹角。我们可以看到,这两个平行四边形

正好是前面我们在绘制立方体顶部的平行四边形和右侧面的平行四边形。 可以这样设想一下,

位于基准点右方的水平方向的平行四边形其实就是高度为 0 的立方体;而位于基准点上方垂

直方向的平行四边形其实就是宽度为 0的立方体。在 vBar3DChart.jsp中,我们就利用这个特

征来绘制 3D 坐标轴。

Ø int topLeftPointX, topLeftPointY 表示 3D 垂直直方图的图表绘制区域的左上角顶点的横坐标和纵坐标。我们可以通过以

下方法计算出该顶点横坐标和纵坐标的值:

topLeftPointX = basePointX ; topLeftPointY = basePointY – areaHeight;

vBar3DChart.jsp定义的方法及其功能:

Ø public double getOffsetX(double thickness, double alpha) 计算任意一点沿着 alpha 角度,移动 thickness 个像素后,距离其移动前位置在水平方向

上的偏移量。这里的 alpha 角度指的是对象移动方向和横轴之间的夹角。计算方法如下:

thickness * Math.cos(alpha * Math.PI / 180) ;

该方法返回的结果是一个双精度浮点数。

Ø public double getOffsetY(double thickness, double alpha) 计算任意一点沿着 alpha 角度,移动 thickness 个像素后,距离其移动前位置在垂直方向

上的偏移量。计算方法如下:

thickness * Math.sin(alpha * Math.PI / 180) ;

该方法返回的结果同样也是一个双精度浮点数。

Ø public Point2D.Double getPoint2DPoint(double startPtX, double startPtY, double thickness, double alpha)

返回一个 Point2D.Double对象。getPoint2DPoint 方法,接收 4 个双精度的浮点参数。其

中 startPtX 表示任意一点在移动前的横坐标,startPtY表示该点在移动前的纵坐标,thickness 表示该点移动的长度,alpha 表示该点移动的角度。因此,采用下面的方法计算出该点移动后

的位置,就可以创建并返回 Point2D.Double的对象了。

double newStartPtX = startPtX + getOffsetX(thickness, alpha); double newStartPtY = startPtY ­ getOffsetY(thickness, alpha); Point2D.Double point = new Point2D.Double(newStartPtX, newStartPtY); return point;

Ø public void drawHorizontalLine(double startPtX, double startPtY, double thickness, double alpha, double length, Graphics2D g2d)

绘制水平直线。将起始坐标为 startPtX, startPtY 的点,沿着角度 alpha 并移动 thickness 个像素后,在该处绘制长度为 length的水平直线。本方法在程序中,用于绘制图 7.20及 7.21 中所示的水平方向的虚线。具体实现的方法如下:

1)调用 getPoint2DPoint 方法,创建表示直线的新的起始点 Point2D 对象 newStartPoint:

Point2D.Double newStartPoint = getPoint2DPoint(startPtX, startPtY, thickness, alpha);

Page 292: Java Web动态图表编程

2)再创建一个表示直线结束点的 Point2D.Double对象 newEndPoint:

Point2D.Double newEndPoint = new Point2D.Double(newStartPoint.getX() + length, newStartPoint.

getY());

结束点的纵坐标和起始点相同,横坐标等于起始点横坐标加上直线的长度。 3)然后创建一个 Line2D.Double对象,表示一条连接端点 newStartPoint 和 newEndPoint

的线段:

Line2D.Double horizontalLine = new Line2D.Double(newStartPoint, newEndPoint);

4)调用 Graphics2D 的 draw方法,绘制该直线:

g2d.draw(horizontalLine); public void drawVerticalLine (double startPtX, double startPtY, double

thickness, double alpha, double length, Graphics2D g2d)

绘制垂直直线。将起始坐标为 startPtX, startPtY 的点,沿着角度 alpha 并移动 thickness 个像素后,在该处绘制长度为 length的垂直直线。本方法在程序中,用于绘制图 7.20及 7.21 中所示的垂直方向的虚线。具体实现的方法如下:

1)调用 getPoint2DPoint 方法,创建表示直线的新的起始点 Point2D 对象 newStartPoint:

Point2D.Double newStartPoint = getPoint2DPoint(startPtX, startPtY, thickness, alpha);

2)再创建一个表示直线结束点的 Point2D.Double对象 newEndPoint:

Point2D.Double newEndPoint = new Point2D.Double(newStartPoint.getX(), newStartPoint.getY() ­

length);

结束点的横坐标和起始点相同,纵坐标等于起始点纵坐标减去直线的长度。 3)然后创建一个 Line2D.Double对象,表示一条连接端点 newStartPoint 和 newEndPoint

的线段:

Line2D.Double verticalLine = new Line2D.Double(newStartPoint, newEndPoint);

4)调用 Graphics2D 的 draw方法,绘制该直线:

g2d.draw(verticalLine);

Ø public void drawSlantLine(double startPtX, double startPtY, double thickness, double alpha, Graphics2D g2d)

绘制斜线。 将起始坐标为 startPtX, startPtY 的点, 沿着角度 alpha 并移动 thickness 个像素

后,在该处绘制连接该点到其起始点的直线。本方法在程序中,用于绘制图 7.20及 7.21中所

示的斜线。具体实现的方法如下: 1)调用 getPoint2DPoint 方法,创建表示斜线起始点的 Point2D 对象 newStartPoint:

Point2D.Double newStartPoint = getPoint2DPoint(startPtX, startPtY, thickness, alpha);

2)再创建一个表示斜线结束点的 Point2D.Double对象 newEndPoint:

Point2D.Double newEndPoint = new Point2D.Double(startPtX, startPtY);

结束点的位置就是该点在移动前的位置。 3) 然后创建一个 Line2D.Double对象, 表示一条连接端点 newStartPoint和 newEnd Point

Page 293: Java Web动态图表编程

的线段:

Line2D.Double verticalLine = new Line2D.Double(newStartPoint, newEndPoint);

4)调用 Graphics2D 的 draw方法,绘制该斜线:

g2d.draw(verticalLine);

此外,本例设定,立方体的厚度和 3D 坐标轴的厚度相同。vBar3DChart.jsp的其他方法,

我们在程序 vBarChart.jsp的讲解中都已经涉及到了,我们在此略过。 vBar3DChart.jsp利用 DrawParallelogram类,来实现 3D 坐标轴的效果,因此,程序第 11

行:

import="com.fatcat.webchart.*"

我们将该包引入到 vBar3DChart.jsp中。

在程序第 14 行~第 72 行的 JSP 声明中,定义了前面我们所介绍的计算偏移量,创建 Point2D.Double对象以及绘制水平直线、垂直直线和斜线等方法。此处不再赘述。

程序第 91 行~第 112 行,绘制圆角矩形背景及其阴影。绘制结果如图 7.15 所示。

程序第 115 行~第 138 行:

int thickness = 10; int alpha = 45;

// 创建图表绘图区域 Rectangle2D.Double drawArea = new Rectangle2D.Double();

// 定义图表绘制区域的大小 int areaWidth = 400, areaHeight = 300;

// 定义图表绘制区域的右下方基准点坐标 int basePointX = 60, basePointY = 345;

// 定义图表绘制区左上角坐标 int topLeftPointX = basePointX , topLeftPointY = basePointY ­ areaHeight;

// 填充图表绘图区域 g2d.setPaint(Color.WHITE);

drawArea = new Rectangle2D.Double(topLeftPointX, topLeftPointY, areaWidth,

areaHeight); drawArea.setRect(topLeftPointX + thickness * Math.cos(alpha * Math.PI /

180), topLeftPointY ­ thickness * Math.sin(alpha * Math.PI /

180), areaWidth, areaHeight );

g2d.fill(drawArea);

首先定义 3D 坐标轴的厚度(thickness)为 10 像素,以及夹角(alpha)值为 45 度,接

着定义 3D 直方图表绘制区域的宽度为 400像素和高度为 300 像素,然后定义如图 7.21所示

的 3D 直方图表基准点的坐标(60, 345)并计算左上角顶点的坐标。调用 Rectangle2D. Double 对象 drawArea 的 setRect 方法,设定 drawArea 位置。注意,此时 darwArea 的位置是沿 45 度

角向右上方移动 10 像素。程序的绘制结果如图 7.22所示。

Page 294: Java Web动态图表编程

图 7.22 vBar3DChart.jsp的运行过程 1

程序第 141 行~第 152 行,利用绘制高度为 0 的立方体,来实现绘制水平方向的 3D 坐

标轴。

DrawParallelogram parallelogram = new DrawParallelogram();

// 绘制基准点右边水平方向的平行四边形 parallelogram.setFillColor(new Color(252, 198, 112)); parallelogram.setBasePoint(basePointX, basePointY ); parallelogram.setWidth(areaWidth); parallelogram.setHeight(0); parallelogram.setThickness(thickness); parallelogram.setAngle(alpha); parallelogram.drawParallelogram(2, g2d); parallelogram.setOutlineColor(Color.BLACK); parallelogram.drawParallelogram(1, g2d);

调用了位于 com.fatcat.webchart 下面的 DrawParallelogram类。因为,立方体顶部的平行

四边形的形状正好就是基准点右方的黄色平行四边形的形状。因此,程序在第 147行,调用 setHeight 设置立方体的高度为 0,这样就得到了立方体顶部平行四边形的绘制结果。 parallelogram中的各个方法,读者都很熟悉了,我们在此略过。

程序第 150行,先用橙色(RGB:252,198,112)填充该平行四边形,然后再用黑色描绘

其轮廓。此时,程序运行结果如图 7.23①所示。

Page 295: Java Web动态图表编程

图 7.23 vBar3DChart.jsp的运行过程 2

程序第 155行~第 163 行:

parallelogram.setPRightFillColor(new Color(252, 198, 112)); parallelogram.setWidth(0); parallelogram.setHeight(areaHeight); parallelogram.setThickness(thickness); parallelogram.setAngle(alpha); parallelogram.drawParallelogram(2, g2d); parallelogram.setOutlineColor(Color.BLACK); parallelogram.drawParallelogram(1, g2d);

通过绘制宽度为 0 的立方体,以达到绘制垂直方向上的平行四边形的目的。因此,在程

序第 156 行,调用 setWidth 方法,设置立方体的宽度为 0。用橙色(RGB:252,198,112)填

充该平行四边形,然后再用黑色描绘其轮廓。此时,程序运行结果如图 7.23②所示。

程序第 165行~第 166 行:

g2d.setPaint(Color.BLACK); g2d.draw(drawArea);

用黑色描绘 drawArea 的轮廓。

程序第 168行~第 185 行:

// 绘制图表标题

String chartTitle = "计算机编程类图书 2004年月销售量 3D统计图"; g2d.setFont(new Font("华文隶书", Font.PLAIN, 22)); int stringLength = g2d.getFontMetrics().stringWidth(chartTitle);

g2d.setColor(Color.WHITE); g2d.drawString(chartTitle, (width ­ stringLength) / 2, 25 );

// 创建虚线笔划 float[] dashes = 3.f ; bs = new BasicStroke(1.0f, BasicStroke.CAP_ROUND, BasicStroke.

JOIN_ROUND, 10, dashes, 0);

g2d.setStroke(bs); g2d.setFont(new Font("Courier New", Font.PLAIN, 12)); String str = ""; stringLength = 0; g2d.setPaint(Color.BLACK);

Page 296: Java Web动态图表编程

g2d.setFont(new Font("宋体", Font.PLAIN, 12));

这段代码与 vBarChart.jsp完全一样,绘制标题,设置虚线笔划属性等。

程序第 187行~第 207 行:

for (int i = 0; i <= 12; i++) // 绘制垂直方向虚线 drawVerticalLine(basePointX+i * areaWidth/13, basePointY ,

thickness, alpha, areaHeight , g2d);

// 绘制横轴上的斜线 drawSlantLine(basePointX+i * areaWidth/13, basePointY ,

thickness, alpha, g2d);

// 绘制横轴上月份的说明文字 if (i != 0)

str += i + "月"; stringLength = g2d.getFontMetrics().stringWidth(str); g2d.drawString(str, topLeftPointX + i * areaWidth/13 ­

stringLength/2, 360);

// 重置月份说明文字 str = "";

调用前面介绍的绘制垂直直线(drawVerticalLine)及斜线(drawSlantLine)的方法,在 drawArea 的矩形上绘制了垂直虚线及横轴上的斜线,以及横轴上的说明文字。运行结果如图 7.24 所示。

图 7.24 vBar3DChart.jsp的运行过程 3

然后, 程序重新设置字体, 重置字符串对象 str的内容为空, 并在创建整型变量 stringHeight 后,继续绘制水平方向的虚线。

程序第 213行~第 229 行:

for (int i = 0; i <= areaHeight ; i += 30) // 绘制水平方向虚线 drawHorizontalLine(topLeftPointX, topLeftPointY+i, thickness, alpha,

Page 297: Java Web动态图表编程

areaWidth, g2d);

// 绘制纵轴上的斜线 drawSlantLine(topLeftPointX, topLeftPointY+i, thickness, alpha, g2d);

// 绘制纵轴上销售量的说明文字 str += 100­i / 3; stringHeight = g2d.getFontMetrics().getAscent(); stringLength = g2d.getFontMetrics().stringWidth(str); g2d.drawString(str, 55­stringLength, topLeftPointY + i + stringHeight

/ 2);

// 重置销售量说明文字 str = "";

在 drawArea 的矩形上绘制了水平虚线及纵轴上的斜线, 以及纵轴上的说明文字。 运行结

果如图 7.25所示。

Page 298: Java Web动态图表编程

图 7.25 vBar3DChart.jsp的运行过程 4

剩下的代码与 vBarChart.jsp 有些不同,除原来使用 Rectangle2D.Double 对象 bar(用渐

进色填充矩形)被立方体所代替外,内、外层循环的控制变量交换了位置,在图例的绘制部

分也增加了一个条件判断语句外,其他部份基本没有变化,立方体的绘制还是通过调用 DrawParallelogram类的对象 parallelogram来完成。

程序第 240行~第 272 行:

for (int i = 0; i < sales.length; i++)

for (int j = 0; j < bookTitle.length; j++) bookSales = 1 + Math.random() * (areaHeight ­ 5); sales[i] = basePointY ­ bookSales ; month[i] = basePointX + ((i+1) * areaWidth)/13;

// 初始化 3D矩形的绘制参数 parallelogram.setBasePoint(month[i]­10 + j*10, basePointY); parallelogram.setWidth(10); parallelogram.setHeight(basePointY ­ (int)sales[i]); parallelogram.setThickness(thickness); parallelogram.setAngle(alpha); // 填充 3D矩形 parallelogram.setFillColor(bookColor[j]); parallelogram.drawParallelogram(2, g2d);

// 描绘 3D矩形轮廓 parallelogram.setOutlineColor(Color.BLACK); parallelogram.drawParallelogram(1, g2d);

// 绘制图例 if ( i < bookColor.length) g2d.setColor(bookColor[i]); g2d.fillRect(425, 40+i * 12, 10, 10); g2d.setColor(Color.BLACK); g2d.drawString(bookTitle[i], 438, 50+i * 12);

这段代码完成了立方体的绘制工作。这里的销售量是从一个随机的 1.0~296.0 之间的双

Page 299: Java Web动态图表编程

精度浮点数中产生。然后,计算立方体基准点的坐标数据,接着,调用 parallelogram的相关 set 方法,设置立方体的基准点坐标、宽度(10 像素)、高度、厚度(10 像素)及角度(45 度)。注意,程序第 251 行,设置立方体的绘制高度时,不要忘记将双精度浮点值强制转换成

整型数值。因为,编写的 DrawParallelogram 类中,setHeight 方法,接收一个数据类型为 int 的参数。

程序第 257行、第 261 行,分别调用 DrawParallelogram类的 drawParallelogram方法,用 bookColor[i]的颜色填充立方体,再用黑色描绘立方体的轮廓。

立方体绘制完成后,在程序第 265 行~第 268 行,绘制图例。

当整个循环完成后,最后的代码就是对缓冲图像进行编码并输出。绘制结果如图 7.20 所

示。

我们把上述代码稍稍做些修改,同样按照 vBarChart.jsp的循环方式来绘制立方体,看看

运行结果有何不同。程序代码如下所示:

for (int i = 0; i < bookTitle.length; i++) for (int j = 0; j < sales.length; j++)

bookSales = 1 + Math.random() * (areaHeight ­ 5); sales[j] = basePointY ­ bookSales ; month[j] = topLeftPointX + ((j+1) * areaWidth)/13;

// 初始化 3D矩形的绘制参数 parallelogram.setBasePoint(month[j]­10 + i*10, basePointY); parallelogram.setWidth(10); parallelogram.setHeight(basePointY ­ (int)sales[j]); parallelogram.setThickness(thickness); parallelogram.setAngle(alpha);

// 填充 3D矩形 parallelogram.setFillColor(bookColor[i]); parallelogram.drawParallelogram(2, g2d);

// 描绘 3D矩形轮廓 parallelogram.setOutlineColor(Color.BLACK); parallelogram.drawParallelogram(1, g2d);

// 绘制图例 g2d.setColor(bookColor[i]); g2d.fillRect(425, 40+i * 12, 10, 10); g2d.setColor(Color.BLACK); g2d.drawString(bookTitle[i], 438, 50+i * 12);

为了对比,vBar3DChart_NoGood.jsp(\chap07\Fig7.5\Fig.7.5_04)即采用了上述代码。读

者可以试着运行,如果都采用本程序中的默认值,两者运行结果是完全一样的。如果同时改

变两个程序的第 115 行,thickness 值为 20,然后再运行程序,当看到各自的运行结果,就会

明白我们为什么在这里修改代码了。

通过对本节两个例程的详尽分析。相信读者对 Java2D 强大图形处理能力有了更进一步

的认识。下面,我们继续学习如何创建 Java2D图形。

7.5.7 Ellipse2D 不要

该类位于 java.awt.geom.Ellipse2D 包中。用来指定椭圆(圆),包括浮点和双精度两种方

式:Ellipse2D.Float 和 Ellipse2D.Double。我们可以说, Ellipse2D 就是某个给定的 Rectangle2D

Page 300: Java Web动态图表编程

(如左上角顶点坐标为 x, y,宽度为 w,高度为 h的 Rectangle2D 对象,如图 7.26 所示)的

内切椭圆。如果椭圆的高度和宽度都相同,则得到正圆的图形。

图 7.26 Ellipse2D.Double示意图

表 7.7 列出了 Ellipse2D.Double的构造器及其常见的一些方法。

表 7.7 Ellipse2D.Double的构造器及其方法

方法:public Ellipse2D.Double()

说明:创建一个椭圆对象,其外切矩形的左上角顶点坐标为(0, 0),宽度和高度都为 0

方法:public Ellipse2D.Double(double x, double y, double w, double h)

说明:创建一个椭圆对象,其外切矩形的左上角顶点坐标为(x, y),宽度为 w 和高度为 h

方法:public Rectangle2D getBounds2D()

说明:返回一个 Rectangle2D.Double 对象,表示当前椭圆的外切矩形

方法:public double getHeight()

续表

说明:返回一个双精度值,表示椭圆(或其外切矩形)的高度

方法:public double getWidth()

说明:返回一个双精度值,表示椭圆(或其外切矩形)的宽度

方法:public double getX()

说明:返回一个双精度值,表示椭圆的外切矩形左上角顶点的横坐标

方法:public double getY()

说明:返回一个双精度值,表示椭圆的外切矩形左上角顶点的纵坐标

要定义椭圆,需要提供 4个参数,即椭圆外切矩形的左上角顶点坐标、宽度和高度。也

可以使用以下的语句定义一个椭圆:

Point2D.Double position = new Point2D.Double(10, 10); double width = 300.0, height = 150.0; Ellipse2D.Double ellipse = new Ellipse2D.Double(position.x, position.y, width, height);

这里指定的椭圆外切矩形左上角顶点坐标是通过 Point2D.Double对象 position再加上 “.”

点操作符及其公共变量 x、y获得的。

7.5.8 Arc2D 不要

该类位于 java.awt.geom.Arc2D 包中。表示用外切矩形、起始角、角度和封闭类型定义的

圆弧,包括浮点和双精度两种方式:Arc2D.Float 和 Arc2D.Double。圆弧和椭圆密切相关,因

(x,y)

h

w

Page 301: Java Web动态图表编程

为 Arc2D 就是某个给定的 Ellipse2D(如外切矩形左上角顶点坐标为 x, y,宽度为 w,高度为 h的 Ellipse2D 对象,如图 7.27所示)的一段弧。

图 7.27 Arc2D.Double示意图

一段圆弧是椭圆或圆的一部分,圆弧的角度通过度数来衡量。一段圆弧在两个角度间绘

制,一个为开始角度,另一个为圆弧角度。开始角度为圆弧开始的角度,圆弧角度为圆弧的

大小。Arc2D 以逆时针方向画过的弧用正角度衡量,而以顺时针方向画过的圆弧用负角度衡

量。

表 7.8 列出了 Arc2D.Double的构造器及其常见的一些方法。

表 7.8 Arc2D.Double的构造器及其方法

方法:public Arc2D.Double()

说明:创建一段圆弧,其外切矩形左上角顶点坐标为(0, 0),宽度和高度都为 0,并且圆弧的起始角度和大小也都为 0

方法:public Arc2D.Double(double x, double y, double w, double h, double startAngle, double arcAngle, int type)

说明:创建一段圆弧,其外切矩形左上角顶点坐标为(x, y),宽度为 w,高度为 h,圆弧的起始角度为 startAngle,圆弧

的大小为 arcAngle,以及圆弧的封闭类型为参数 type 所定义的圆弧

方法:public Arc2D.Double(int type)

说明:创建一段圆弧,其外切矩形左上角顶点坐标为(0, 0),宽度和高度都为 0,并且圆弧的起始角度和大小也都为 0,

以及圆弧的封闭类型为参数 type 所定义的类型

方法:public Arc2D.Double(Rectangle2D ellipseBounds, double startAngle, double arcAngle, int type)

说明:创建一段圆弧,其外切矩形为参数 ellipseBounds 所代表的矩形,圆弧的起始角度为 startAngle,圆弧的大小为

arcAngle,以及圆弧的封闭类型为参数 type 所定义的类型

方法:public Point2D getStartPoint()

说明:返回一个 Point2D 对象,表示圆弧的起始点

方法:public Point2D getEndPoint()

说明:返回一个 Point2D 对象,表示圆弧的结束点

方法:public double getAngleStart()

说明:返回一个双精度值,表示圆弧的起始角度

方法:public double getAngleExtent()

说明:返回一个双精度值,表示圆弧角度的大小

方法:public double getHeight()

说明:返回一个双精度值,表示圆弧的外切矩形的高度

方法:public double getWidth()

说明:返回一个双精度值,表示圆弧的外切矩形的宽度

左上角顶点(x,y)

外切

矩形

高度 height

外切矩形宽度 width

arcAngle

startAngle 圆弧起始角度

Page 302: Java Web动态图表编程

方法:public double getX()

说明:返回一个双精度值,表示圆弧的外切矩形左上角顶点的横坐标

方法:public double getY()

说明:返回一个双精度值,表示圆弧的外切矩形左上角顶点的纵坐标

方法:Rectangle2D makeBounds(double x, double y, double w, double h)

说明:返回一个 Rectangle2D 对象,表示圆弧的外切矩形。左上角顶点坐标:x, y,宽度 w和高度 h

方法:public void setArc(double x,double y, double w, double h, double angSt, double angExt, int closure)

续表

说明:把当前 Arc2D 对象,按照 setArc 所提供的参数,进行重新设置

方法:public void setAngleStart(double angleStart)

说明:将当前圆弧的起始角度,设置成参数 angleStart 指定度数

方法:public void setAngleExtent(double arcAngle)

说明:将当前圆弧角度的大小,设置成参数 arcAngle 指定度数

方法:public void setArcType(int type)

说明:将当前圆弧的封闭类型,指定为参数 type 所定义的类型

要定义一段圆弧,需要提供 7个参数。前 4 个参数指定该圆弧外切矩形左上角的顶点坐

标、宽度和高度;第 5 个参数指定圆弧的起始角;第 6 个参数指定圆弧的大小;最后一个参

数指定如何封闭圆弧。这里共有 3 个常量,来表示圆弧的封闭类型:

ØArc2D.PIE:表示用两条直线来封闭圆弧。一条直线从圆弧的起始点连接到其外切矩形

的几何中心,另一条直线从圆弧外切矩形的几何中心连接到圆弧的结束点,如图 7.28 ①所示;

Ø Arc2D.CHORD:表示用圆弧的弦来封闭圆弧,如图 7.28②所示;

Ø Arc2D.OPEN:表示圆弧是开放型的,即不封闭圆弧,如图 7.28③所示。 arc2D.jsp (\chap07\Fig7.5\Fig.7.5_08) 演示了如何绘制不同风格的椭圆及圆弧,运行结果

如图 7.28 所示。

图 7.28 arc2D.jsp的运行结果

下面简要讲述 arc2D.jsp程序。

程序第 31 行~第 33行:

Rectangle2D.Double rect = new Rectangle2D.Double();

Page 303: Java Web动态图表编程

Ellipse2D.Double ellipse = new Ellipse2D.Double(); Arc2D.Double arc2d = new Arc2D.Double();

分别通过 Rectangle2D.Double、Ellipse2D.Double、Arc2D.Double的默认构造器,创建了

各自的实例:rect、ellipse、arc2d。

程序第 35 行~第 38行:

double startAngle = 45.0, arcAngle = 270.0; double rectWidth = 160.0, rectHeight = 120.0; Point2D.Double p2d = new Point2D.Double(5.0, 20.0); int arcType;

分别定义了以一些定义矩形、椭圆、圆弧所需要的数据变量:矩形的宽度为 160 像素,

高度为 120 像素,圆弧的开始角度为 45 度,圆弧的大小为 270 度。这里我们用一个 Point2D.Double对象 p2d 来表示矩形左上角顶点。整型变量 arcType 表示圆弧的封闭类型。

程序第 40 行~第 43行,定义了一个虚线的 BasicStroke对象 bs,我们将用该虚线,来绘

制圆弧的外切矩形及其所在椭圆的轮廓。

程序第 45 行~第 79行,在一个循环方法中,绘制了 3 个不同封闭类型的圆弧。这 3 个

圆弧除位置及封闭类型不同外,其余完全相同。

在循环中,我们首先对整型变量 arcType进行赋值。根据当前的循环次数,arcType被分

别赋予 Arc2D.PIE、Arc2D.CHORD以及 Arc2D.OPEN 等常量。

接着,用灰色及虚线笔划描绘外切矩形的轮廓(程序第 55 行~第 58 行),然后,用蓝色

及虚线笔划描绘圆弧所在椭圆的轮廓(程序第 61 行~第 64 行)。

程序第 67 行~第 70行, 用黄色填充当前圆弧对象 arc2d。 填充了一个开始角度为 45 度,

圆弧大小为 270 度的圆弧。如果是第一次循环,则该封闭类型为 Arc2D.PIE 的圆弧。

程序第 73 行~第 75 行,用红色,并用 2 像素宽的笔划,描绘当前圆弧对象 arc2d 的轮

廓。最后重新将笔划设置成 bs。

重复上述过程,绘制出封闭类型为 Arc2D.CHORD 以及 Arc2D.OPEN 的圆弧。

到目前为止,我们把基本的 Java2D 图形介绍完了。下节,我们将讲述几个基于 Arc2D.Double 图形生成的饼图及圆环图的实例。之后,我们将阐述基于 Java2D 的一些高级

应用,如图形的重叠、组合、平移、旋转、扭曲以及字体的一些应用技巧。最后,将继续讨

论如何利用 Java2D 的曲线、Area及通用路径来创建复杂的外观图形。

7.6 Java2D 实例饼图类图表

一般用圆弧和椭圆来绘制饼图或圆圈图类型的图表。

Page 304: Java Web动态图表编程

7.6.1 普通饼图

pieChart.jsp(\chap07\Fig7.6)演示了如何利用 Java2D API来绘制饼图。本例用饼图来显

示 5 种计算机编程书籍销售量之间的对比关系。pieChart.jsp的运行结果如图 7.29所示。

关于背景部分的绘制,从本例开始,我们将略过不提。图中的椭圆由 5 段不同颜色的圆

弧组成,每段圆弧代表了一种编程书籍。图中位于圆弧外部的白色文字,是书籍的名称及其

销售量占销售总量的比例。

程序第 14 行~第 35行,声明了一个名为 drawTips 的方法。用来绘制图中所示白色饼图

的说明文字。

程序第 46 行~第 83行,绘制背景及图表标题“编程类图书销售量统计饼图” 。

图 7.29 pieChart.jsp的运行结果

程序第 85 行~第 91行:

// 定义圆弧 Arc2D.Double arc2d = new Arc2D.Double();

double startAngle = 30.0, arcAngle = 0.0; double rectWidth = 320.0, rectHeight = 280.0; Point2D.Double p2d = new Point2D.Double((width ­ rectWidth)/2, 70.0); int arcType = Arc2D.PIE;

这段代码用来定义一个 Arc2D.Double 对象 arc2D,双精度变量 startAngle、arcAngle、 rectWidth、rectHeight 分别表示圆弧的开始角度、角度大小、外切矩形的宽度及高度。 Point2D.Double 对象 p2d,用来表示圆弧外切矩形左上角顶点。这里我们设置圆弧初始绘制

角度为 30 度,圆弧外切矩形宽度为 320 像素,高度为 280 像素。该外切矩形左上角顶点横坐

标为 90(500−320 再除以 2)、纵坐标为 70。本程序中,所有圆弧的外切矩形位置及大小都

是相同的。

程序第 94 行~第 100行,声明都包含了 5 个元素的字符串数组对象和 Color 数组对象,

字符串对象 bookTitle,用来表示参与绘图的书名,Color 对象 color,用来表示各书籍对应的

绘制颜色。

编程类图书销售量统计拼图

Page 305: Java Web动态图表编程

String bookTitle[] = "Python", "JAVA", "C#", "Perl", "PHP"; Color color[] = new Color[5]; color[0] = new Color(99,99,0); color[1] = new Color(255,169,66); color[2] = new Color(33,255, 66); color[3] = new Color(33,0,255); color[4] = new Color(255,0,66);

程序第 102行~第 104 行,绘制背景及图表标题“编程类图书销售量统计饼图” 。

double bookSales[] = new double[5]; double totalSales = 0.0 ; double proportion = 0.0;

这几个双精度变量表示的意义如下:

数组 bookSales,用来存储每种书籍的销售量。totalSales,表示所有书籍的销售量合计。 proportion,用来表示单种书籍的销售量与销售总量的比值。

程序第 106行~第 110 行,初始化销售数据,并计算销售总量。这里的销售量是从一个 1.0~100.0 之间的双精度浮点数中产生。

for (int i=0; i< bookSales.length; i++)

bookSales[i] = 1 + Math.random() * 99; totalSales += bookSales[i];

程序第 112 行~第 113 行:

for(int i = 0 ; i < bookTitle.length; i++)

arcAngle = bookSales[i]*360 / totalSales; proportion = bookSales[i]/totalSales * 100 ;

g2d.setColor( color[i] ); arc2d = new Arc2D.Double(p2d.x, p2d.y, rectWidth,

rectHeight, startAngle, arcAngle, arcType);

// 填充圆弧 g2d.fill( arc2d) ;

// 描绘圆弧轮廓 g2d.setStroke(new BasicStroke(1.2f)); g2d.setPaint(Color.GRAY); g2d.draw(arc2d);

// 更新圆弧的起始角度 startAngle += arcAngle;

// 格式化浮点数的输出格式 DecimalFormat twoDigits = new DecimalFormat("0.00"); g2d.setFont(new Font("Courier New", Font.PLAIN, 12));

// 绘制说明文字 drawTips(bookTitle[i] + " "+twoDigits.format(proportion) + "%",

Color.WHITE, arc2d, g2d);

本段代码中,首先根据销售量及销售总量,计算圆弧角度的大小,并计算两者的比值。

之后,设置当前圆弧的绘制颜色,程序第 118 行、第 119 行,调用 Arc2D.Double的构造器,

根据上述计算获得参数,创建一个圆弧对象。程序第 122 行,填充该圆弧,程序第 125 行~

第 127 行,定义了一个 1.2 像素粗的、灰色的笔划,来描绘当前圆弧的轮廓。

程序第 130行,更新下一个圆弧的绘制起始角度。

Page 306: Java Web动态图表编程

程序第 133行, 定义了 DecimalFormat 类的对象 towDigits, 表示格式化数字的输出方式。 DecimalFormat("0.00")表示将数值保留两位小数,如果不足两位小数,则自动在后面添“0”

补足。

程序第 137 行~第 138 行,调用 drawTips,绘制圆弧的说明文字。调用程序第 14 行~

第 35 行,声明的 drawTips 方法。注意,说明文字内容采用的方式是“bookTitle[i] + " "+twoDigits.format(ration) +"%"” , 也就是在书籍名后紧跟一个空格, 接着, 调用 DecimalFormat 类的对象 towDigits 的 format 方法,将当前书籍销售量和销售总量的比值格式化,输出为保

留两位小数的浮点数后,再紧跟一个百分比符号。

现在我们来看 drawTips 方法的源代码如下:

public void drawTips(String tips, Color color, Arc2D.Double arc2d, Graphics2D g2d)

Arc2D.Double position = arc2d;

position.setAngleExtent(arc2d.getAngleExtent()/2); position.x = arc2d.x ­ 15; position.y = arc2d.y ­ 15; position.width = arc2d.getWidth() + 30 ; position.height = arc2d.getHeight() + 30; Point2D.Double startPoint =

(Point2D.Double)position.getStartPoint(); Point2D.Double endPoint = (Point2D.Double)position.getEndPoint();

g2d.setPaint(color);

int stringLength = g2d.getFontMetrics().stringWidth(tips);

if (endPoint.x <= arc2d.getCenterX()) g2d.drawString(tips, (float)endPoint.x ­ stringLength, (float)

endPoint.y); else

g2d.drawString(tips, (float)endPoint.x , (float)endPoint.y );

drawTips 方法接收 4 个参数。第 1 个参数是字符串变量,表示要绘制圆弧说明文字的内

容。第 2 个参数是 Color 对象,表示绘制圆弧说明文字的颜色。第 3 个参数是 Arc2D.Double 对象,表示当前的圆弧对象,最后一个参数是当前 Graphics2D 的绘图环境对象。

那么如何实现在圆弧外围绘制说明文字这个功能的呢?请读者看图 7.30 所示的说明文

字的绘制方法示意图。

编程类图书销售量统计拼图

Page 307: Java Web动态图表编程

图 7.30 圆弧说明文字绘制方法示意图

可以看出,我们创建了这样一个新圆弧(图中白色的圆弧轮廓),即它和当前圆弧的圆心

(及外切矩形的几何中心)位置相同,但其外切矩形宽度和高度都大于当前圆弧,并且圆弧的

起始角度和当前圆弧相同,圆弧角度的大小正好等于当前圆弧的一半。而我们就在新圆弧的

终止点处,绘制当前圆弧的说明文字。 drawTips 方法,完成了以下两个工作:

Ø 根据用户传递的当前圆弧对象,新创建一个具有上述特征的新的圆弧对象;

Ø 在新的圆弧对象的终止点处,绘制用户指定的文本。

程序第 16 行~第 25行,实现了上述第 1 步工作。这里我们创建新圆弧的外切矩形的几

何中心和当前圆弧的几何中心重合, 宽度和高度比当前圆弧分别大 30 像素。 起始角度和当前

圆弧相同,圆弧的角度大小等于当前圆弧的一半。

最后,在绘制说明文字的时候,我们做了一个简单的处理(程序第 31 行~第 34 行)。

如果新的圆弧终止点位于圆弧的圆心(外切矩形的几何中心)左侧,即圆弧终止点的横坐标

小于或等于圆弧圆心的横坐标时,这段文本的最后一个字符绘制位置和圆弧终止点的横坐标

相同,这样看上去更美观。

如果要将说明文字放置在圆弧的内部,也可以采用本方法的思路,只需要将新圆弧的外

切矩形的宽度和高度都相继减少即可。

7.6.2 圆圈图

cirqueChart.jsp(\chap07\Fig7.6)演示了如何绘制圆圈图。先看其运行结果,如图 7.31 所示。

图 7.31 cirqueChart.jsp的运行效果

从图中可以观察出,圆圈图其实和饼图密切相关。实现圆圈图的绘制效果也很简单,只

需要在饼图的基础上再绘制一个与其背景图绘制属性(如颜色、渐进色、笔划)相同的椭圆

即可。该椭圆的中心与圆弧的中心位置相同。

了解上述思路后,我们看到 cirqueChart.jsp的源程序和 pieChart.jsp相比,主要是原来的

编程类图书销售量统计圆圈图

Page 308: Java Web动态图表编程

drawTips 方法,更换成了 drawTipsAndCirque 方法,而且两个方法接收的参数也完全相同,

除此之外,程序其余部分几乎完全相同。在 drawTipsAndCirque 方法中,除了完成 drawTips 方法所有的功能外,还要完成绘制一个具有一些特征的椭圆。

因此,我们仅讲述源程序第 18行~第 56 行的 drawTipsAndCirque方法。

public void drawTipsAndCirque(String tips, Color color,Arc2D.Double arc2d, Graphics2D g2d)

Arc2D.Double position = arc2d; position.setAngleExtent(arc2d.getAngleExtent()/2); position.x = arc2d.x ­ 15; position.y = arc2d.y ­ 15; position.width = arc2d.getWidth() + 30 ; position.height = arc2d.getHeight() + 30; Point2D.Double startPoint = (Point2D.Double)position.getStartPoint(); Point2D.Double endPoint = (Point2D.Double)position.getEndPoint();

g2d.setPaint(color);

int stringLength = g2d.getFontMetrics().stringWidth(tips);

if (endPoint.x <= arc2d.getCenterX()) g2d.drawString(tips, (float)endPoint.x­stringLength, (float)

endPoint.y); else

g2d.drawString(tips, (float)endPoint.x, (float)endPoint.y );

double centerX = arc2d.getCenterX() ; double centerY = arc2d.getCenterY() ; double scale = 0.35; double eWidth = arc2d.getWidth()* scale; double eHeight = arc2d.getHeight()* scale ;

Ellipse2D.Double ellipse = new Ellipse2D.Double(centerX­eWidth/2, centerY­eHeight/2, eWidth,

eHeight);

// 填充椭圆 g2d.setPaint(blueGP); g2d.fill(ellipse);

// 描绘椭圆轮廓 g2d.setStroke(new BasicStroke(1.2f)); g2d.setPaint(Color.LIGHT_GRAY); g2d.draw(ellipse);

程序第 18 行~第 37行与前例介绍的 drawTips 相同,完成圆弧说明文字的绘制工作。程

序第 39 行~第 43 行,定义了 5 个双精度浮点数。centerX 表示新建椭圆几何中心的横坐标, centerY 表示新建椭圆几何中心的纵坐标,scale 表示新建椭圆的外切矩形和圆弧外切矩形之

间的比值。如果 scale的值为 1,则表示新建椭圆的外切矩形和圆弧外切矩形大小相同;这里

我们设定 scale的值为 0.35, 表示新建椭圆的外切矩形的大小是圆弧外切矩形的 35%。 eWidth 表示新建椭圆的外切矩形的宽度,eHeight 表示新建椭圆的外切矩形的高度。

程序第 45 行,调用 Ellipse2D.Double的构造器,创建了一个名为 ellipse的椭圆。注意,

我们初始化该椭圆左上角顶点及宽度和高度的方法:

new Ellipse2D.Double(centerX­eWidth/2, centerY­eHeight/2, eWidth, eHeight);

程序第 49 行~第 50行, 先将当前绘图环境的填充属性设置为 blueGP, 再填充新建椭圆。

最后,用一个 1.2 像素宽的灰色笔划,描绘该椭圆的轮廓。

Page 309: Java Web动态图表编程

当这些图形组合在一起的时候,最终就形成了圆圈的图像。

7.6.3 3D 饼图

pie3DChart.jsp(\chap07\Fig7.6)演示了如何利用 Java2D API 来绘制 3D 饼图。我们以 pieChart.jsp的源程序为蓝本,在其基础上进行改写,以达到生成 3D 饼图的目的。

pie3DChart.jsp的实际运行效果,如图 7.32 所示。

图 7.32 pie3DChart.jsp的运行结果

pieChart.jsp 实现了绘制平面饼图的功能。如果需要绘制 3D 效果的饼图,我们可以在程

序中再增加一个名为 thickness,表示 3D 饼图的厚度。图 7.32 中所示,3D 饼图中 thickness 的值为 25。也就是说,该 3D 饼图由 25 个平面饼图重叠在一起组成。这些饼图(圆弧)的外

切矩形的大小相同,每段圆弧的起始角度和大小也相同,外切矩形左上角的横坐标也相同,

惟一不同的是其纵坐标不同。此外,我们将位于最上层饼图之下的饼图,绘制颜色稍深于最

上层的颜色。

我们现在看看 pie3DChart.jsp 的源程序。细心的读者已经发现,本程序使用了四个循环

来绘制这个 3D 饼图。程序首先在第 117行:

int thickness = 25;

定义 3D 饼图的厚度为 25 像素。接下来,程序在第 116 行~第 138 行:

startAngle = 30; for(int i = 0 ; i < bookTitle.length; i++)

arcAngle = bookSales[i]*360 / totalSales; if (startAngle >= 90 )

break;

for (int j= thickness; j> 0; j­­)

proportion = bookSales[i]/totalSales * 100 ; g2d.setColor( color[i].darker() ); arc2d = new Arc2D.Double(p2d.x, p2d.y + j, rectWidth,

rectHeight, startAngle ,arcAngle, arcType);

Page 310: Java Web动态图表编程

// 填充圆弧 g2d.fill( arc2d) ;

// 更新圆弧的起始角度 startAngle += arcAngle;

绘制起始角度不大于 90 度的圆弧。绘制的方向是逆时针方向。

同理,程序第 141 行~第 163行:

startAngle = 390; for(int i = bookTitle.length ­1 ; i >0 ; i­­)

arcAngle = bookSales[i]*360 / totalSales; if (startAngle <= 270 )

break;

for (int j= thickness; j> 0; j­­)

proportion = bookSales[i]/totalSales * 100 ; g2d.setColor( color[i].darker() ); arc2d = new Arc2D.Double(p2d.x, p2d.y + j, rectWidth,

rectHeight, startAngle, ­arcAngle, arcType);

// 填充圆弧 g2d.fill( arc2d) ;

// 更新圆弧的起始角度 startAngle ­= arcAngle;

绘制起始角度不小于 270度的圆弧。 注意, 程序第 154行~第 155行, 在创建Arc2d.Double 对象 arc2d时,设置圆弧的绘制角度为­arcAngle,这里的负号,表示绘制方向是顺时针方向。

程序第 166 行~第 187 行,绘制起始角度在 90 度到 270 度之间的圆弧。同样,程序第 177 行~第 178 行,在创建 Arc2d.Double 对象 arc2d 时,设置圆弧的起始角度和绘制角度的

方法,这段代码也是按顺时针方向绘制。最后程序第 190 行~第 217 行,绘制顶层的圆弧及

其说明文字。

这里我们为什么要将圆弧分开绘制?是否一定要在其中同时按照顺时针和逆时针的方

向绘制相关的圆弧?能否在一个循环(包括其子循环)内绘制出与本例运行结果完全一致的

饼图?

这个问题,我们先留给读者思索。解决方法将在本章最后一节阐述。

7.7 图形重叠

如果同时描绘图形的轮廓和填充图形,结果会根据描绘和填充的不同而有所差异。这里

我们以绘制一个椭圆为例,演示了同一个图形,因绘制属性及顺序不同而得到不同的绘制结

果。

严格来说,圆形实际轮廓的宽度为无限小,在图 7.33中用黑色线条来表示。图形轮廓描

绘的动作,实际上是沿着图形实际轮廓画出的另一个代表轮廓的图形,有一部分位于图形的

外部区域,另一部分位于图形的内部区域。

图 7.33 展示了一个椭圆的描绘轮廓、实际轮廓,以及内部区域。如果采用不透明色彩来

Page 311: Java Web动态图表编程

进行填充和描绘操作,我们得到的结果便会根据绘制顺序的不同而有所差异。如果采用半透

明色彩,那么我们得到的描绘轮廓和内部区域的结果,便会重叠在一起。

图 7.33 图形的实际轮廓、描绘轮廓和内部区域

程序 overlap.jsp(\chap07\Fig7.7)演示了上述情况,运行结果如图 7.34 所示。

图 7.34 overlap.jsp的运行结果

程序第 38 行~第 41行:

Color yellow = new Color(255, 255, 0, 128); g2d.setPaint(yellow); g2d.fill(ellipse); g2d.draw(ellipse);

程序第 38 行,调用了 Color 构造器,使用了 4 个 int 类型的变量。这里在本书中,首次

以这种的方式来调用 Color 的构造器。前 3个 int 变量,我们大家都很熟悉了,分别代表颜色

的 RGB 值, 最后 1 个 int 变量, 表示颜色的 alpha 值。 颜色的 alpha 值,指的是颜色的透明度。

该值取值范围在 0~255 之间。0表示该颜色为完全透明,255 表示该色彩完全不透明。RGB 值为 255、255、0 的颜色为黄色,这里定义其 alpha 值为 128,说明当前颜色为一个半透明的

黄色。定义当前图形环境的填充属性色彩为刚刚建立的 Color 对象 yellow。之后,先用半透

明的黄色描绘出椭圆的轮廓,然后,用半透明的黄色来填充椭圆。绘制结果如图 7.34①所示。

程序第 38 行~第 41行:

ellipse.setFrame(x + 160, y, w, h); g2d.setPaint(Color.RED); g2d.draw(ellipse); g2d.setPaint(Color.YELLOW); g2d.fill(ellipse);

这里调用 Ellipse2D.Double的 setFrame方法,重新定义 ellipse外切矩形的位置。先用红

色描绘出椭圆的轮廓,然后用不透明的黄色将 ellipse 填满。绘制结果如图 7.34②所示。

实际轮廓 描绘轮廓

内部区域

Page 312: Java Web动态图表编程

程序第 38 行~第 41行:

ellipse.setFrame(x +320, y, w, h); g2d.setPaint(Color.YELLOW); g2d.fill(ellipse); g2d.setPaint(Color.RED); g2d.draw(ellipse);

再次调用 Ellipse2D.Double的 setFrame方法,重新定义 ellipse外切矩形的位置。先用不

透明的黄色将 ellipse填满,然后再用红色描绘出椭圆的轮廓。绘制结果如图 7.34③所示。

我们可以清楚地看到,在不同的绘制属性(这里指色彩)以及绘制顺序的情况下,虽然

绘制的对象相同,也会得到不同的绘制结果。因此,读者在设计Web 图表中,图形对象的绘

制顺序要根据实际情况进行调整。

7.8 alpha 复合

7.8.1 alpha 复合概述

alpha 复合(alpha compositing)是关于图像透明度的内容。本书到目前为止,介绍的绘

制模式都是覆盖模式。也就是说,如果有两个或多个相互重叠在一起的图形对象,如字符串、

各种图形以及加载的外部文件,图形对象之间相互重叠部分的内容,只有位于最顶层的图

形对象才可见。如果希望被最顶层图形对象覆盖的下层图形对象的内容也可以显示出来,

就有必要确定如何使用处理重叠像素。如果红色矩形与蓝色矩形相重叠,其重叠区域应该

处理为红色、蓝色或二者的某种组合。重叠区域中像素的颜色,决定了哪一个矩形位于上

面及其透明程度。确定使用何种颜色处理重叠对象所共有像素的过程称为复合。两个接口

构成了 Java 2D 复合模型的基础:Composite 和 CompositeContext。 AlphaComposite(Composite接口的一种实现)支持大量不同的复合样式。该类的实例包

含了描述如何混合新颜色与已有颜色的复合规则。在 AlphaComposite类中,最常用的复合规

则是 SRC_OVER,表示混合时新颜色(源色)应覆盖在已有颜色(目标色)之上。要实现这

个目的,则必须调用图形对象每个像素的 alpha 组件(alpha component)来指定图像的透明

度(如上节,我们设置半透明的黄色)。

每个像素具有 alpha 组件的图像格式被称为具有 alpha 通道(alpha chanel)。实际上,并

非所有的图像格式都支持 alpha 通道(PNG 格式支持,这也是我们采用 PNG 格式,作为默认

的图像格式的原因之一)。图形对象中的像素的 alpha 值可以不同,取值范围从 0.0 到 1.0 之

间。0.0 表示完全透明,因为所有图形对象的颜色组件都为 0。像素的 alpha 值为 1.0 表示完

全不透明。在具有 alpha 通道的 RGB 图像中,每个像素由 4 个参数定义,即 3 个颜色组件参

数 RGB,以及一个 alpha 值。因每个像素 alpha 的值可以是不同的,因此,同一个图形对象

的透明度可以是均匀的(所有像素的 alpha 值均相同),也可以是不均匀的。只要像素的 alpha 值低于 1.0,则该像素在图形对象所处的位置至少具有一些透明度。如果图形对象没有设置 alpha 值,则 alpha 值默认为 1.0,即完全不透明。

Java 基本绘制系统仅提供复合颜色的基本布尔运算符。例如,布尔复合规则可能允许源

颜色值和目标颜色值为 AND、OR 或 XOR。这种方法存在一些问题。

Ø 它不具有“用户友好性”——当红色和蓝色进行 AND 操作而不是相加时,很难判断

将得到什么颜色。

Ø 布尔复合,不支持不同颜色空间的精确颜色复合。

Ø 直接布尔复合不考虑颜色的颜色模型。例如,在索引颜色模型中,图像中两个像素值

Page 313: Java Web动态图表编程

的布尔运算结果是两个索引的复合,而不是两种颜色的复合。

为此,Java2D API 通过使用 alpha­blending 规则(进行复合颜色时,考虑颜色模型信

息)从而避免了这些缺陷。当需要使用 alpha 复合时,Java2D 对此的处理机制如下:

Ø 顶层图形对象(即源图形对象)中每个像素的颜色组件和被其覆盖的图形对象(即目

标图形对象)中,相对应像素的颜色组件乘以它们的 alpha 组件。

Ø 顶层图形对象,根据当前的 alpha 复合规则在被覆盖的图形对象上进行渲染。

两者的复合规则有多种,但一般来说,源图形对象和被覆盖的图形对象的像素组件,按

照以下通用公式进行复合操作:

ColorR = ColorS*AlphaS*FractionS + ColorD*AlphaD*FractionD AlphaR = AlphaS*FractionS + AlphaD*FractionD

公式中的下标“R”表示复合结果的像素, “S”表示源图形对象像素, “D”表示目标图

形像素。因此,ColorR 表示源图形的颜色对象和目标图形的颜色对象,经过复合后产生的结

果颜色对象;AlphaR 表示源图形的 alpha 通道对象和目标图形的 alpha 通道对象,经过复合后

产生的 alpha 通道对象。FractionS 是指源图形对象的因子(分数),该因子由当前的复合规则

指定。

实际 Web 图表的开发应用中,alpha 的复合调用并不难,因为这些复合规则及操作被封

装到 java.awt 包中的 AlphaComposite类。AlphaComposite封装了复合规则,可以确定在对象

相互重叠时如何绘制颜色。 Graphics2D 类中定义了 setComposite方法,接收 AlphaComposite参数,来设定图形环境

中绘制时所使用的 alpha 复合规则。因此,使用 alpha 复合步骤如下:

Ø 首先创建一个 AlphaComposite对象。

Ø 将该 AlphaComposite 对象作为参数传递给绘图环境 Graphics2D 对象的 setComposite 方法。

Ø 调用 Graphics2D 的 draw或者 fill 方法绘制图形对象。

Page 314: Java Web动态图表编程

7.8.2 AlphaComposite 类的使用

因为AlphaComposite类没有构造器, 因此无法用new方法, 直接创建一个AlphaComposite 类的对象。但 AlphaComposite 类提供了静态的 getInstance 方法,该方法有两个版本,返回 AlphaComposite 对象的引用。其中一个版本 getInstance 方法,接收一个 int 类型的参数,表

示复合规则;另一个版本 getInstance 方法,除接收一个 int 类型的参数外,还接收一个 folat 类型的参数,表示 alpha 值。

这里我们仅介绍最常用的 AlphaComposite 类的复合规则 SRC_OVER。这也是图形环境

中默认的规则。 AlphaComposited的复合规则由AlphaComposite类的静态 int类型的常量决定。

除 SRC_OVER 复合规则外,还有其他的复合规则,如表 7.9 所示。

表 7.9 AlphaComposite的复合规则简要说明

AlphaComposite 复合规则 说 明 示 例

SRC_OVER

这是我们最常用的复合规则,也是图形环境默认的

复合规则。对结果起作用的源分数是 1,对结果起作用

的目标因子是 1­AlphaS。因此,从通用复合公式可以

导出,源像素与目标像素按以下方式复合在一起:

ColorR=ColorR + (1−AlphaS)*ColorD

AlphaR=AlphaS + (1−AlphaS)*AlphaD

计算复合后的结果颜色的公式应用于每个像素的红

色、绿色和蓝色组件。从上面的公式可以推导出:如

果源像素的 Alpha 值 AlphaS 为 1,那么目标像素的因

子为 0。 因此, 复合结果就是原始的源像素, 也就是说,

源像素是不透明的。如果源像素的 AlphaS 的值为 0,

那么结果就只是目标像素,因为这时源像素是透明的

(不可见)。右图为 alpha 值为 0.75 的源图像(红色椭

圆),所以下面的蓝色矩形就可见了

注:蓝色矩形为目标图形对象,

红色椭圆为源图形对象。

下同

SRC_IN

复合结果中源像素因子是目标像素 alpha 值及

AlphaD,复合结果中目标分数是 0。因此,只有位于目

标图像中的源像素渲染出来。这个复合规则的应用公

式如下:

ColorR=Colors * Alphad

AlphaR=AlphaS * AlphaD

续表 AlphaComposite 复合规则 说 明 示 例

SRC_OUT

这个复合规则把目标区域外的源像素渲染出来,也

就是说,把源图形对象和目标图形对象的重叠区域内

的图像内容给清除掉了。本规则的应用公式如下:

ColorR = ColorS * (1−AlphaD)

AlphaR = AlphaS * (1−AlphaD)

Page 315: Java Web动态图表编程

DST_OVER

用目标像素覆盖源像素。使用这个规则,每个源像

素在结果中的因子是 1­AlphaD,对应目标像素的因子

是 1。因为目标像素的 alpha 值是 1.0,源像素的因子

是 0,因此源图形对象就隐藏在目标图形对象之下

本规则的应用公式如下:

ColorR = ColorS * (1−AlphaD) + ColorD

AlphaR = AlphaS * (1−AlphaD) + AlphaD

DST_IN

使用这个规则,源像素的因子在复合结果中为 0,目

标像素的因子为 AlphaS。本规则的应用公式如下:

ColorR = ColorD * AlphaS

AlphaR = AlphaD * AlphaS

因此,目标图像对象和源图形对象重叠区域的像素

被渲染出来了,但它具有和源像素相同的 alpha 值,因

此,重叠区域内的目标像素看上去要比其他部分稍浅

DST_OUT

源像素因子在复合结果中是 0,目标分数是

1­AlphaS。本规则的应用公式如下:

ColorR = ColorD * (1−AlphaS)

AlphaR = AlphaD * (1−AlphaS)

SRC

用源像素替换目标像素,相当于 SRC_OVER 规则中

AlphaS 的值为 1,因此,本规则的应用公式就变成:

ColorR=ColorR

AlphaR=AlphaS

CLEAR

公式中源像素和目标像素的因子都是 0。复合结果

是,源图形对象中所有的像素都被清除了

此外,AlphaComposite类也定义了静态变量,这些静态变量是 AlphaComposite对象。每

个对象都对应于表 7.9 中所讲述的每一种复合规则, 这些静态变量都具有 1.0f的 alpha 值, 例

如 SrcOver、SrcIn、SrcOut、Src、DstOver、DstIn、DstOut、Clear。

因为 Clear 对象的 alpha 值为 1.0,用该对象进行复合会得到不透明的黑色。

7.8.3 AlphaComposite 应用实例

1.透明 3D 折线图表

透明 3D 折线图表,是本章前面讲述的 line3DChart.jsp的扩展。本例将原来两个 3D折线

之间的重叠部分图形内容都显示出来,体现了透明的效果。 transparnetLine3DChart.jsp (\chap07\Fig7.8)实现了上述效果,运行效果如图 7.35所示。

Page 316: Java Web动态图表编程

图 7.35 transparentLine3DChart.jsp的运行结果

本例我们只讲述透明效果是如何实现的,其余部分的绘制方法与 line3DChart.jsp 完全相

同。

程序第 146行:

AlphaComposite ac = AlphaComposite.getInstance(AlphaComposite. SRC_ OVER, .3f);

创建了一个 AlphaComposite的实例 ac。我们知道,AlphaComposite类没有构造器,因此

无法用 new 方法直接创建一个 AlphaComposite 类的对象。只能调用该类的静态 getInstance 方法,返回一个 AlphaComposite 对象的引用。这里提供了两个参数,第一个参数是 int 类,

表示创建的复合规则是 AlphaComposite.SRC_OVER;第二个参数是单精度浮点数,表示返回

的 AlphaCompsite对象的 alpha 值是 0.3f。注意,程序第 146 行,不能写成以下的形式:

AlphaComposite ac = AlphaComposite.getInstance(AlphaComposite. SrcOver, .3f);

这 里的 第一个 参数 AlphaComposite.SrcOver 不是 一个 整型参 数 , 它是一 个 AlphaComposite类的静态实例。因此,上述写法会发生编译错误。

因为 AlphaComposite实现了 Composite接口。因此,程序第 146行,可以写成以下形式:

Composite ac = AlphaComposite.getInstance(AlphaComposite. SrcOver, .3f);

在创建了 AlphaComposite对象 ac后,程序第 147行:

g2d.setComposite(ac);

将当前绘图环境的复合规则设置为 SRC_OVER, alpha 通道值为 0.3f (即 30%的透明度)。

程序进入循环,进行折线的阴影绘制。并按照设定的当前的复合规则,对图形对象重叠

部分进行渲染操作。如果是第一次绘制折线的阴影,我们重新设定了当前的复合规则,见程

序第 151 行~第 164行:

if (m == shadowThickness)

g2d.setStroke(new BasicStroke(2.0f)); g2d.setColor(new Color(123,123,123)); ac = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1.0f); g2d.setComposite(ac);

Page 317: Java Web动态图表编程

else

g2d.setStroke(new BasicStroke(4.0f)); g2d.setColor(bookColor[i]); ac = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.3f); g2d.setComposite(ac);

在绘制阴影部分的图形后,程序第 176 行~第 182行,再次设置复合规则,在原始位置

绘制折线。

ac = AlphaComposite.Src; g2d.setComposite(ac); g2d.setColor(new Color(123,123,123)); g2d.setStroke(new BasicStroke(2.0f));

// 绘制月销售量曲线 g2d.drawPolyline(month, sales, sales.length);

程序第 176 行,直接调用 AlphaComposite 的静态实例 Src 来重新设置 ac。Alpha Composite.Src相当于 AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1.0f)。

2.透明 3D 水平直方堆叠图表

堆叠图表也是一种比较常见的图表表现形式,因将同类的图形重叠放置在一起而得名。

现在我们来 看另 外一个 例程 , 绘制具有 透 明效果的 3D 水平直方堆叠图表 transparentStackedBar3DChart.jsp(\chap07\Fig7.8)。本例程模拟统计某个月中,计算机编程类

图书 Python、Java、C#周销售量对比图。Python、Java、C#每周的销售量由不同颜色的透明

水平直方图组成,水平直方图的长短由其销售量决定。销售量从一个 40~125 之间的随机数

中产生。然后,同一时期的水平直方图堆叠在一起。此外,图表还列出了每种书籍在每一周

的销售量及该周所有书籍的销售总量。

我们先看 transparentStackedBar3DChart.jsp的运行效果,如图 7.36所示。

图 7.36 transparentStackedBar3DChart.jsp

再来看 transparentStackedBar3DChart.jsp源程序的运行过程。关于背景及 3D 坐标轴的绘

制,请读者参阅以前的相关内容。本例中,除 3D坐标轴的厚度为 20 像素外,其余绘制对象,

如绘制区域大小, 坐标系基准点的位置, 与前面绘制 3D 垂直直方图的例程 (vBar3DChart.jsp)

Page 318: Java Web动态图表编程

完全相同。也就是说,3D 坐标轴的厚度(thickness)为 20 像素以及夹角(alpha)的值为 45 度, 3D 透明水平直方图表绘制区域的宽度为 400 像素, 高度为 300像素, 基准点的坐标为 (60, 345)。

程序第 115 行,设定 3D 坐标轴的厚度为 20 像素,程序第 116 行,夹角度数为 45 度。

程序第 184行:

int step = 50;

创建一个 int 类型的变量 step,在本例中用于控制循环的步长。在绘制垂直虚线的时候,

绘制区域的宽度为 400 像素。我们希望每 50像素就绘制一个垂直虚线,因此,将其值初始化

为 50。

在绘制完水平及垂直虚线、3D 坐标轴等的背景后,我们来看如何绘制堆叠图。

程序第 212行,又创建了一个 int整形变量 weeks。一个月有 4周,因此,将其设置为 4。

int weeks = 4;

程序第 246行,创建了 AlphaComposite的对象 ac,并将复合规则设定为 AlphaComposite. SRC_OVER,颜色的透明度为 0.6f。

程序第 249行,创建 int 变量 leftX,表示当前书籍销售量的水平直方图的横坐标。

int leftX = topLeftPointX; 程序第 251行~第 304 行,通过一个嵌套循环,绘制水平堆叠直方图。在本段循环中,

外层循环共 4 次,每次循环又进入一个嵌套的循环(程序第 255行~第 301 行)。此外,定义

了一个名为 totalSales 的 int 变量,用来计算当周的销售总量:

for (int i = 1; i <= weeks; i++)

// 书籍周销售总量 int totalSales = 0; for (int j = 0; j < bookTitle.length; j ++)

bookSales = 40 + (int)(Math.random() * 85); totalSales += bookSales;

// 初始化 3D矩形的绘制参数 parallelogram.setBasePoint(leftX , topLeftPointY + i *

step ); parallelogram.setWidth(bookSales); parallelogram.setHeight(cubeHeight); parallelogram.setThickness(thickness); parallelogram.setAngle(alpha);

// 填充 3D矩形 parallelogram.setFillColor(bookColor[j]); parallelogram.drawParallelogram(2, g2d);

// 描绘 3D矩形轮廓 parallelogram.setOutlineColor(Color.BLACK); parallelogram.drawParallelogram(1, g2d); leftX += bookSales;

// 调用 AlphaComposite的静态变量 Src,重新定义 AlphaCompoiste对

象 g2d.setComposite(AlphaComposite.Src);

// 绘制当前书籍的周销售量说明文字 g2d.setColor(bookColor[j].darker()); g2d.drawString("" + bookSales, leftX ­ bookSales/2,

topLeftPointY + i*step ­ 40);

Page 319: Java Web动态图表编程

// 绘制当前书籍的周总销售量说明文字 if (j == bookTitle.length ­ 1) g2d.setColor(Color.BLACK); g2d.drawString("" + totalSales, leftX + 20, topLeftPointY +

i * step ­ 20);

// 绘制图例 if (i == weeks ) g2d.setColor(bookColor[j]); g2d.fillRect(basePointX + 40 + j * 130, basePointY ­ 30,

10, 10); g2d.setColor(Color.BLACK); g2d.drawString(bookTitle[j], basePointX + 55 + j * 130,

basePointY ­ 20);

// 恢复图形环境的 AlphaCompoiste属性 g2d.setComposite(ac);

leftX = topLeftPointX;

程序第 257行,获得一个 40~125 之间的随机数 bookSales。程序第 258 行,将该值累加

到 totalSales 上。通过 DrawParallelogram类的实例 parallelogram,来绘制 3D 水平直方图。程

序第 261 行,设置 3D 水平直方图的基准点坐标。程序第 262 行,设置 3D 水平直方图的绘制

宽度,该宽度就是随机数 bookSalesd的值。之后,依次设置绘制高度、厚度、角度。

程序第 268行,填充水平直方图。程序第 272 行~第 273 行,用黑色绘制水平直方图的

轮廓。程序第 274 行,设定下一个水平直方图横坐标的绘制参数。这里我们看到,下一个水

平直方图的横坐标等于 leftX += bookSales。因此,下一个水平直方图的最左端和当前水平直

方图的最右端的绘制位置是相同的,这样就实现了堆叠的效果。又因程序第 246 行,创建了 AlphaComposite的对象 ac(复合规则设定为 AlphaComposite. SRC_OVER,颜色的透明度为 0.6f),同时也就实现了透明的效果。

然后,重新设置图形环境的复合规则为 AlphaComposite.Src,改变透明度为不透明。程

序第 281 行,绘制当前书籍的销售量。

程序第 284行~第 288 行,绘制完当月最后一种书籍的销售量之后,绘制当月所有书籍

的销售总量。

程序第 291行~第 297 行,当外层循环进行到最后一次的时候,绘制图例。

程序第 300行,重新设置图形环境的复合规则为 AlphaComposite对象 ac。

之后,结束内层循环,完成了第一周的透明 3D 水平直方堆叠图。然后,将下一次的水

平直方图的横坐标,重新设置成最初的绘制位置后,就结束了一次外层循环。重复上述过程,

就会完成第二、三、四周的透明 3D水平直方堆叠图的绘制过程。

程序的运行结果如图 7.36 所示。

7.9 图形对象的转换

7.9.1 图形对象转换(transformation)概述

本节继续讲述基于 Java2D 的图形转换(transformation)。这里所指的图形,是指 Java 可

Page 320: Java Web动态图表编程

以处理的图形对象,包括文本、外部图像文件及任何由 Java 生成的图形对象。这里所指的转

换,是指 Java2D 提供的对图形对象进行的 4 种操作功能:平移、旋转、缩放和扭曲。在 Graphics2D 的图形环境中, 可以通过 AffineTransform类 (位于 java.awt.geom. AffineTransform 包中)来实现上述转换操作。每个 Graphics2D 的图形环境都存在这个类。默认的 AffineTransform对象是保持用户坐标不变的恒等转换。在 Graphics2D 的图形环境中,可以通

过 getTransform方法,获得当前图形环境下的 AffineTransform对象的一个副本。例如:

AffineTransform at = g2d.getTransform();

可以通过 setTransform方法,设置当前图形环境的 AffineTransform属性,例如:

g2d.setTransform(at);

通过这两个方法,我们可以设置当前图形环境的 AffineTransform属性,然后在需要的时

候,恢复以前的 AffineTransform属性。

此外,AffineTransform类也提供构造器,如默认的恒等转换及其他转换的构造器。这里

介绍 AffineTransform类的生成转换对象的最简单方法,就是调用 AffineTransform类的 static 成员变量即可。调用方法与前面介绍的 AlphaComposite的 SrcOver 等静态成员变量相类似。

对应于这 4种转换,AffineTransform类提供了多种静态方法,其中最常用的是:

getTranslateInstance(double tx, double ty); getRotateInstance(double angle); getScaleInstance(double sx, double sy); getShearInstance(double shx, double shy);

每种方法都返回一个用户,通过参数指定的 AffineTransform对象。创建 AffineTransform 对象后,就可以将其作为 Graphics2D 对象 setTransform方法的参数应用于图形环境。另外一

个用途就是调用它来创建一个图形对象。 通过AffineTransform对象的 createTransformed Shape 方法,就可以创建一个 Shape对象了。假设我们使用如下语句,定义了一个 Rectangle对象:

Rectangle2D.Double rect2d = new Rectangle2D.Double(50, 50, 120, 80);

则该矩形的左上角顶点坐标为(50,50),宽度为 120像素,高度为 80 像素。我们使用下

面的语句,来创建一个转换对象:

AffineTransform at = getTranslateInstance(40, 50);

该转换对象将在 Java 坐标系中,将图形对象向右移动 40 像素,向下移动 50 像素。我们

可以使用下列语句,从 rect2d上再生成一个新的图形(Shape对象):

Shape transRect = at.createTransformedShape(rect2d);

新得到的图形外观和 rect2d 完全一样,只是左上角顶点的坐标更新为(90,100),如图 7.37所示。 这里需要注意的是, 虽然Shape对象 transRect的外观和 rect2d完全一样, 但 transRect 并不是一个 Rectangle2D.Double 的对象。在执行某种转换操作时(如扭曲、旋转等)可能会

发生变形的结果,转换结果可能不再是原始图形的外观。但转换操作可用于任何图形对象。

因此, createTransformedShape返回的图形对象结果也必须要能适用于任何的图形对象, Shape 类是任何图形对象的父类。 所以, createTransformedShape返回的图形对象的类型是 Shape类。

提示:实际上,createTransformedShape 返回的是 GeneralPath 对象。GeneralPath 对象几

乎可以实现任何形状的图形对象。我们将在后面的章节具体讲述 GeneralPath的用法。

基于我们所讨论的图形转换内容,createShape.jsp(\chap07\Fig7.9\Fig.7.9_01)演示了如

何通过 createTransformedShape方法,来创建一个 Shape类对象,运行结果如图 7.37所示。

Page 321: Java Web动态图表编程

图 7.37 createShape.jsp的运行结果

关于源代码,请读者参考前面的相关内容。

7.9.2 平移(translation)

默认情况下,所有的绘制工作都是基于 Java 坐标体系的原点来进行的。所谓的平移,是

指将原始图形对象从其原始位置移动到新的位置,并且移动后的新的图形对象的形状、大小

都保持不变。

有两种方法可以实现图形对象的平移:

(1)创建 AffineTransform对象后,利用 AffineTransform对象的 setToTranslation方法。

如下所示:

AffineTransform at = g2d.getTransform(); at.setToTranslation(10.0, 20.0);

setToTranslation方法,接收两个双精度型的参数。第 1 个参数,指定平移在图形环境 g2d 中绘制的任何图形对象,向右移动 10 像素;第 2 个参数,指定向下移动图形对象 20 像素。

(2)利用 Graphics2D 的 translate方法,如下所示:

g2d.translate(40.0, 50.0);

translation 方法,有两种不同的版本。其中一个版本接收两个双精度的参数,另一个版

本接收两个 int类型的参数。 这两种版本的参数使用方法与AffineTransform的 setToTranslation 方法,对参数的用法是一样的。以上例来说明第 1 个参数,指定平移在图形环境 g2d中绘制

的任何图形对象,向右移动 40像素;第 2 个参数,指定向下移动图形对象 50 像素。

这里我们提供一个简单的 translate.jsp程序, 它演示了如何调用上述两种平移图形对象的

方法。

translate.jsp(chap07\Fig7.9\Fig.7.9_02)的运行结果如 7.38 所示。

图 7.38 translate.jsp的运行结果

Page 322: Java Web动态图表编程

程序第 32 行~第 37 行,绘制了一个黄色的 Rectangle2D.Double 类的对象 rect2d。该矩

形的左上角坐标为(10,10),宽度为 120 像素,高度为 80 像素。

程序第 40 行:

AffineTransform at = g2d.getTransform();

通过 Graphics2D 对象 g2d 的 getTransform 方法,创建了表示当前图形环境的 AffineTransform对象的实例 at。

程序在第 42行~第 44 行:

g2d.translate(60, 40); g2d.setPaint(Color.RED); g2d.fill(rect2d);

首先调用 g2d的 translate方法,定义在图形环境上绘制的图形对象的坐标,向右移动 60 像素,向下移动 40 像素。然后用红色再次填充矩形 rect2d。因为发生了平移,因此这时的绘

制结果如图 7.38①所示。

程序第 47 行:

g2d.setTransform(at);

将图形对象的转换属性恢复到原始状态。如果不这样做,以后在相同的绘图环境中,进

行的绘制操作将累计越来越多的平移。

程序第 49 行~第 52 行,绘制了一个蓝色的 Rectangle2D.Double 类的对象 rect2d,该矩

形的左上角坐标为(200,10),宽度为 120 像素,高度为 80 像素。

程序第 55 行:

at.setToTranslation(60, 40);

调用 AffineTransform 的实例 at 的 setToTranslation 方法,设置图形对象的平移也向右移

动 60 像素,向下移动 40 像素。程序第 56 行,调用 g2d 的 setTransform 方法,将 at 应用到

当前图形环境中。程序第 58 行~第 59 行,用橙色来绘制平移后的 rect2d。这时的绘制结果

如图 7.38②所示。从本例可以看出,直接调用 Graphics2D 的相关图形平移方法,要比调用 AffineTransform的相关方法简单。因此,我们推荐直接调用 Graphics2D 的相关图形平移、旋

转、缩放及扭曲等方法来进行相关的处理。

7.9.3 旋转(rotation)

所谓的旋转,是指将原始图形对象围绕原点,按照给定的角度进行旋转。

有两种方法可以实现图形对象的旋转:

(1)创建 AffineTransform对象后,利用 AffineTransform对象的 setToRotation方法,如

下所示:

AffineTransform at = g2d.getTransform(); at.setToRotation(30*Math.PI/180);

setToRotation 方法有两种不同的版本。其中一个版本接收 1 个双精度的参数,表示图形

对象围绕原点旋转的弧度。at.setToRotation(30*Math.PI/180)表示顺时针旋转 30 度。另一

个版本接收 3 个双精度的参数。如下所示:

at.setToRotation(angle, x, y);

该方法表示围绕点 x,y旋转 angle弧度。它等同于三个连续的转换操作:

Ø 首先平移图形对象到点 x,y。

Page 323: Java Web动态图表编程

Ø 然后图形对象围绕新原点旋转 angle弧度。

Ø 最后再通过­x,­y平移回去,恢复原来的原点。

使用该方法,可以围绕图形参考点,绘制旋转的图形。例如,如果图形对象的参考点位

于 shapeX, shapeY,可以用如下的语句绘制旋转 60 弧度的图形对象:

at.setToRotation(30*Math.PI/180, shapeX, shapeY);

(2)利用 Graphics2D 的 rotate 方法。Rotate 也有两个版本,而且接收的参数与 Affine Transform对象的 setToRotation方法完全相同,实现的功能也相同,此处不再赘述。

这里我们提供一个简单的 rotation.jsp程序, 它演示了如何调用Graphics2D的 rotate方法,

来对一个字符串对象进行旋转。程序对字符串进行了 12 次旋转,每次旋转 30 度。 rotation.jsp (chap07\Fig7.9\Fig.7.9_03)的运行结果如图 7.39 所示。

图 7.39 rotation.jsp的运行结果

这里简要讲述一下源程序。

程序第 38 行:

g2d.translate(width/2, height/2);

将原点平移到绘制环境的中心。

程序第 41 行~第 46行:

for (int i=0; i<12; i++)

g2d.rotate(30*Math.PI/180); g2d.setPaint(colors[i%2]);

g2d.drawString(s, 0, 0);

本段代码共循环 12 次,每次循环将当前的循环对象旋转 30 度,然后绘制颜色为蓝色和

红色相互交替。最后绘制“精通 Java Web 图表编程”这段文本。注意,程序第 45行,调用

的 drawString方法,指定绘制位置在(0,0) 。因为当前的原点已经平移到图形环境的中心,

即(250,187)处。因此,这里的(0,0)点,也就是(250,187)点。第一次循环,绘制

的结果是围绕点(250,187)处,顺时针旋转 30 度,用蓝色绘制字符串“精通 Java Web 图

表编程” 。当 12 次循环完成后,绘制结果就如图 7.39所示。

Page 324: Java Web动态图表编程

7.9.4 缩放(scale)

所谓的缩放,是指将原始图形对象进行放大或缩小的操作。该操作是将图形对象的横坐

标和纵坐标,各乘以一个比例因子而产生的。因为横坐标和纵坐标的比例因子可以不同,因

此,缩放操作得到的结果可以是线性的(即横坐标和纵坐标的比例因子都相同),也可以非线

性的(即横坐标和纵坐标的比例因子相异)。

有两种方法可以实现图形对象的缩放:

(1)创建 AffineTransform对象后,利用 AffineTransform对象的 setToScale 方法,如下

所示:

AffineTransform at = g2d.getTransform(); at.setToScale(scaleX, scaleY);

缩放后得到的图形对象是通过对原始图形对象的每个像素的横坐标乘以比例因子 scaleX,纵坐标乘以比例因子 scaleY 而创建的。如果要得到原始图形一半大小的图形对象,

可以使用如下语句:

at.setToScale(0.5, 0.5);

(2)利用 Graphics2D 的 scale方法,该方法接收的参数和实现的功能和 AffineTransform 对象的 setToScale方法完全一致。

这里我们提供一个简单的 scale.jsp (chap07\Fig7.9\Fig.7.9_04)程序,它演示了如何调用 Graphics2D 的 scale方法,来对一幅图形进行放大和缩小的操作。

scale.jsp的运行结果如图 7.40 所示。

图 7.40 scale.jsp的运行结果

因 scale.jsp涉及调用 URL类的构造器来获取图像文件方法,因此程序第 11行:

import="java.net.*"

引进了 Java 用于网络处理的包。

程序第 44 行~第 45行:

Page 325: Java Web动态图表编程

URL imageURL = new URL("http://localhost:8080/chap07/Fig7.9/Fig.7.9_04/logo. png");

程序是在本机上运行的,所以这里使用了 localhost。此外,也可使用 127.0.0.1 来表示。

“8080”指 http协议的端口号。这里读入的一个图像资源文件位于 Web 服务器文档根目录下 chap07/Fig7.9/Fig.7.9_04/子目录中的文件 logo.png。

程序第 48 行~第 49行:

logoPNG = ImageIO.read(imageURL); g2d.drawImage(logoPNG, 10, 50, null);

调用 ImageIO 类的 read 方法,加载并显示 logoPNG 图像文件。以前我们加载图像文件

的时候,提供给 read方法的参数是一个 File类的实例。除此之外,ImageIO 类的 read方法还

支持 ImageInputStream、InputStream和 URL类的一个实例对象为参数。因此,这里我们提供

的是一个 URL 类的实例。然后,我们在坐标(10,50)处绘制 logo.png 文件的原始图像,如

图 7.40①所示。

程序第 51 行:

g2d.translate(width/5, height/2.5);

调用 g2d的 translate方法,将原点平移到点(width/5, height/2.5)处。

程序第 52 行:

g2d.scale(2.0, 2.0);

调用 g2d的 scale方法,对图像进行放大操作。因为比例因子同为 2.0,因此,放大后的

图像大小正好等于原来图像的二倍。程序第 53 行,在新的原点处绘制该图像,绘制结果如图 7.40②所示。

程序第 55 行~第 56行:

g2d.scale(0.25, 0.25); g2d.drawImage(logoPNG, 250, ­180, null);

对图形进行缩小操作。 这里的比例因子同为 0.25, 表示将当前图形对象缩小为原来的 1/4。

因为,当前图像的大小已经是原始图像的二倍,因此,缩小为原来的 1/4 后,正好等于原始

图像的 1/2。然后,新的绘制位置在当前原点上向右移动 250 像素,再向上移动 180 像素。

绘制结果如图 7.40③所示。

7.9.5 扭曲(shear)

所谓扭曲,是指按照一定角度绘制原始图形对象的平面投影图形,这是较少用到的一个

操作。它根据图形对象的纵坐标在每个像素的横坐标上加上一个值,根据图形对象的横坐标

在每个纵坐标上也加上一个值。由用户提供两个扭曲参数 shearX 和 shearY,并按以下的计算

公式来进行扭曲操作。

x = x + shearX * y y = y + shearY * x

有两种方法可以实现图形对象的平移:

(1)创建 AffineTransform对象后,利用 AffineTransform对象的 setToShear 方法,如下

所示:

AffineTransform at = g2d.getTransform(); at.setToShear(scaleX, scaleY);

(2)利用 Graphics2D 的 shear 方法

Page 326: Java Web动态图表编程

两种方法接收的参数都为两个双精度型的参数。扭曲后的图像横坐标和纵坐标的值都由

上述公式计算得到。 shear.jsp(chap07\Fig7.9\Fig.7.9_05)的源程序演示了如何调用 Graphics2D 的相关方法,来

获得扭曲图像的目的。运行结果如图 7.41 所示。

图 7.41 shear.jsp的运行结果

shear.jsp 与以前加载并显示图像文件的方法不同。本例采用了 ImageIO 类另一个版本的 read方法。read方法共有 4 个版本,分别接收四种不同类型的参数。除我们熟悉的 File类的

对象外,还可以接收 ImageInputStream、InputStream和 URL类的一个实例为参数。本例提供

给 ImageIO 类的 read方法的参数,是一个 URL类的实例 imageURL。 imageURL在程序第 44 行~第 45行被定义为:

URL imageURL = new URL("http://localhost:8080/chap07/Fig7.9/Fig.7.9_04/logo. png");

表示当前的协议为 http;端口号为 8080;Web 服务器的地址为 localhost(表示在本机,

也可以写成 127.0.0.1);资源文件位于Web 服务器文档根目录下 chap07/Fig7.9/Fig.7.9 _04/子

目录中,文件名是 logo.png。程序第 48 行,将 URL 类的实例 imageURL,作为参数传递给 ImageIO 类的 read方法,用于加载该图像文件。

程序第 50 行~第 55行:

for (int i = 1; i <= 3; i ++)

g2d.shear(i * 0.15f, i * 0.1f); g2d.drawImage(logoPNG, 0, 50, null); g2d.translate(135, 0);

通过循环,绘制了 3 个扭曲的图像。注意,每循环一次,当前原点的横坐标就向右移动 135 像素,而纵坐标保持不变。在加载 URL类资源的时候,可能会发生异常。因此,我们将

上述代码都放在一个 try…catch 代码块中。如果图像文件的加载过程中发生了异常,则执行

程序第 59 行。

Page 327: Java Web动态图表编程

7.10 图形渲染线索

本书生成的各种 Web 图表的最终效果中,凡是图形对象为非直线型的,在图形对象的弯

曲部分,我们可以观察到,图像的线条呈“锯齿”状,其中以饼图及各种图表的圆角矩形状

的绘制背景最为明显。实际上,Java 在进行一些图表生成的渲染操作中,存在一个质量与处

理时间之间的选择。总的来说,如果渲染的时间比较长,我们可以得到较高质量的渲染结果,

反之,可能得到比较差的渲染结果。Java 对于渲染操作,定义了一些默认的渲染设置。这些

默认设置是跨平台的。Graphics2D 也提供了一个名为 setRenderingHint 的方法,用于创建自

定义的渲染方法。 setRenderingHint 方法,需要程序员提供一个被称为“渲染线索

(RendingHints) ”的参数。

渲染线索指定 Java 按哪种方法进行渲染。 RenderingHints 类定义了各种类型的渲染线索,

常见的渲染线索如表 7.10 所示。

表 7.10 常见的渲染线索

键 相对应的键值及其说明

KEY_ANTIALIASING

设定渲染图形时,对图形弯曲部分进行平滑处理,也就是“反锯齿”处理。反锯齿处

理(antialiasing)是对图形弯曲部分,设置像素亮度的技术。它使得经过反锯齿处

理的线段或图形的轮廓看上去更加圆滑。当 Java 执行此渲染线索时,需要更多的时间

来 处 理 。 可 选 的 值 为 VALUE_ANTIALIAS_ON 、 VALUE_ANTIALIAS_OFF 、

VALUE_ANTIALIAS_DEFAULT

KEY_COLOR_RENDERING 该 线 索 影 响 颜 色 如 何 渲 染 。 可 能 值 为 VALUE_COLOR_RENDERSPEED 、

VALUE_COLOR_QUALITY、VALUE_COLOR_DEFAULT

KEY_DITHERING

“抖动(dithering)”是一个通过使用相近颜色的像素产生未设置颜色的效果,从而

可以用有限的颜色合成更多的颜色

可 选 值 为 VALUE_DITHER_ENABLE 、 VALUE_DITHER_DISABLE 、 VALUE_

DITHER_DEFAULT

KEY_FRACTIONALMETRICS 该渲染线索影响文本的显示质量。可选的值为 VALUE_FRACTIONALMETRICS_ON、

VALUE_FRACTIONALMETRICS_OFF、VALUE_FRACTIONALMETRICS_DEFALUT

KEY_INTERPOLATION

当转换源图形对象时,转换后的像素很少能够完全对应于目标图像中的像素位置。在

这种情况下,每个转换后的像素的颜色不得不以其周围的像素决定

插补(interpolation)是可以进行这项操作的几种技术之一。根据渲染的效果及其

所花费的时间顺序来排列。可选值为 VALUE_INTERPOLATION_BICUBIC、VALUE_

INTERPOLATION_BILINEAR、VALUE_INTERPOLATION_NEAREST_NEIGHBOR

续表

键 相对应的键值及其说明

KEY_RENDERING

该渲染线索决定渲染质量和渲染速度之间的优先值

可选值为 VALUE_RENDERING_SPEED、VALUE_RENDERING_QUALITY、VALUE_

RENDERING_DEFAULT

KEY_TEXT_ANTIALIASING

这个渲染线索决定渲染文本时,是否进行反锯齿处理。可选值为 VALUE_TEXT_

ANTIALIASING_ON 、 VALUE_TEXT_ANTIALIASING_OFF 、 VALUE_TEXT_

ANTIALIASING_DEFAULT

如果计算机不支持指定的相应渲染线索,则该渲染线索将不起任何作用。一般来说,我

Page 328: Java Web动态图表编程

们习惯仅对图形进行平滑处理,是否对文本也进行平滑处理,要视情况而定。如果文本应

用的字体比较小(例如,12 磅大小),进行平滑处理,则文本的显示效果将比没有被平滑处

理前清晰。如果文本应用的字体比较大,若进行平滑处理后,看上去会更美观。

这里我们以前面绘制的 3D 饼图为蓝本,新增对图形进行平滑处理的渲染线索功能。让

我们来看 3D 饼图经过平滑处理后的渲染效果。在 renderingHintsPie3D.jsp( \chap07\ Fig7.9\Fig.7.10)中,我们对图表的背景、标题,以及饼图都做了反锯齿处理。因为饼图的说

明文字比较小,我们就没有对其进行反锯齿处理。 renderingHintsPie3D.jsp的实际运行效果, 如图 7.42 所示。 对比图 7.32, 我们可以观察到,

反锯齿处理后,图表在背景、标题以及饼图表示出来的平滑性。因为,没有对饼图的说明文

字进行反锯齿处理,因此,饼图的说明文字也显示得非常清晰。

图 7.42 经过反锯齿处理的 3D饼图

renderingHintsPie3D.jsp程序第 54 行~第 55 行:

g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

定义图形环境的当前渲染线索。注意,RenderingHints 类,定义的各种渲染线索是保存

在一个 Map(一种一一映射类型的集合)中。这些渲染线索是以键/值成对的形式出现的。这

里提供给 setRenderingHint方法的第一个参数是键, 即RenderingHints.KEY_ ANTIALIASING。

第二个参数是对应于该键的键值,该键的键值为 RenderingHints. VALUE_ANTIALIAS_ON。

我们从表 7.10 可以看出, 其他的可选键值还有 RenderingHints. VALUE_ANTIALIAS_OFF (关

闭反锯齿功能)、RenderingHints.VALUE_ANTIALIAS_ DEFAULT(采用系统默认的设置)。

程序第 193行~第 228 行, 绘制最顶层饼图及其说明文字。 因说明文字字体较小 (12 磅,

见程序第 215 行),因此,程序第 218 行~第 219 行:

g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);

先关闭反锯齿处理,等绘制完饼图的说明文字后(程序第 222 行~第 223 行),程序第 226 行~第 227 行,重新打开反锯齿功能。

程序的其他部分我们都已经讲解过,此处不再赘述。建议读者可以将程序第 218 行~第 219 行,关闭反锯齿处理的两行代码注释掉,然后重新运行 IE,就可以看到小字体的文本,

Page 329: Java Web动态图表编程

经过反锯齿处理后的运行效果。

7.11 Java2D 与高级文本处理

7.11.1 空心文本

第 6 章 6.4.2节中, 我们曾经讲述过绘制空心文本的一些技巧 (详见该节 fontEffect. jsp)。

这里,我们再提供一个绘制空心字符的方法。 fontOutline.jsp(\chap07\Fig7.11\Fig.7.11_01)的运行结果,如图 7.43所示。

图 7.43 空心文本的绘制方法

fontOutline.jsp程序第 34 行:

Shape shape;

声明了一个 Shape的对象 shape。

程序第 36 行:

FontRenderContext frc =g2d.getFontRenderContext();

创建了一个 FontRenderContext 类的对象 frc。FontRenderContext 类位于 java.awt.font. FontRenderContext 包中。该类封装正确度量文本所需的信息。程序第 38 行,定义字符串“精

通 Java Web 图表编程” ,用于稍后绘制空心字符串。程序第 39 行,设置图形环境当前所应用

字体对象 font。这里我们设置的字体为“华文琥珀” 、风格为粗体加上斜体、大小为 45 像素。

程序第 41 行:

TextLayout tl = new TextLayout(outlineText, font, frc);

创建了一个 TextLayout 类的对象 tl。TextLayout 类位于 java.awt.font.TextLayout 包中。它

提供有一定样式的字符数据的不可变图形表示。本例采用该类的构造器需要三个参数,第一

个是字符串对象,第二个是 Font 对象,最后一个是 FontRenderContext 的对象。

程序第 43 行:

shape = tl.getOutline(AffineTransform.getTranslateInstance(10, 50));

将调用 tl的 getOutline方法, 返回的 Shape对象对变量 shape进行初始化。 注意, getOutline 方法,需要接收一个 AffineTransform类的实例作为参数。这里不仅获得了当前字体下指定字

符串的轮廓,而且还定义了图形对象的绘制位置于点(10, 50)处。实际上,除了程序第 43 行

的代码外,我们还可以使用如下方法调用 getOutline:

AffineTransform at = g2d.getTransform(); g2d.translate(10, 50); shape = tl.getOutline(null);

我们认为上述方法可读性好,使用方便。读者可以用上述代码来替换程序第 43 行,两

Page 330: Java Web动态图表编程

者实现的目的及其运行结果是完全相同的。

程序第 45 行,设置当前绘制颜色为红色。程序第 46 行,定义当前笔划属性的宽度为 2 像素。然后,描绘当前图形对象 shape 的轮廓。最后用白色填充 shape。绘制结果如图 7.43 所示。

7.11.2 弯曲文本

现在我们来看一段文本具有弯曲效果的代码。 rollingText.jsp(\chap07\Fig7.11\Fig.7.11_02) 的运行效果,如图 7.44 所示。

图 7.44 弯曲文本的绘制方法

程序第 38 行:

g2d.translate(10, 40);

首先将当前原点平移到点(10, 40)处。

程序第 40 行:

GlyphVector gv = font.createGlyphVector(frc, rollingText);

创建了一个GlyphVector类的实例 gv。 GlyphVector类位于 java.awt.font.GlyphVector包中,

该类提供了一种显示自定义布局机制效果的途径。 GlyphVector 对象可以看做是这样一种算法

的输出,首先取一个字符串,然后计算如何对其进行精确显示。系统中有一种内置算法,

而 Java2D API 允许高级用户定义自己的算法。GlyphVector 对象是一个字形及其位置的集

合,对布局特性(例如,字距调整等)进行控制的方法是使用字形而不是字符。 Font 类提供了 createGlyphVector 的方法,该方法返回一个 GlyphVector 对象。本例中的

createGlyphVector 方法接收两个参数。第一个参数是 FontRenderContext 类的实例,第二个参

数是字符串对象。

程序第 41 行:

int length = gv.getNumGlyphs();

调用 gv的 getNumGlyphs 方法,获取当前字体中字形的数目。然后,进入程序第 43 行~

第 53 行的循环。在循环体中,程序第 45 行,首先通过 gv的 getGlyphPosition方法,获得一

个 gv 对象中当前字形的位置,该位置是一个 Point2D 对象。程序第 46 行,定义了一个双精

度类型的变量 theta,表示字形将要旋转的角度。

Page 331: Java Web动态图表编程

第 47 行~第 48 行:

AffineTransform at = AffineTransform.getTranslateInstance(p.getX(), p.getY());

将当前原点再平移到当前字形的位置。程序第 49 行,将当前字形旋转 theta 度。

程序第 50 行:

Shape glyph = gv.getGlyphOutline(i);

创建了一个 Shape对象 glyph。

程序第 51 行:

Shape transformedGlyph = at.createTransformedShape(glyph);

调用 AffineTransform 对象 at 的 creatTransformedShap 方法,再次创建一个 Shape 对象 transformedGlyph。程序第 52 行,填充该 transformedGlyph图形。

整个循环结束后,得到如图 7.44 所示的运行结果。读者可以更改程序第 46行,theta 角

度值的计算方法,观察更改后的运行结果有什么不同。

7.11.3 单行长文本自动分行

Java AWT没有提供对单行长文本进行自动分行处理的功能。在以前介绍的 Web 图表编

程中,我们讲述过,在图形环境中无法利用“\n”符号对文本进行分行处理。如果要将文本

进行分行处理,我们以前是两次或多次调用图形环境的 drawString 方法,并通过指定不同的

绘制位置来实现。 multiLineLayout.jsp(\chap07\Fig7.11\Fig.7.11_03)演示了一种可以让文本按指定宽度进行

分行处理及绘制的技术。其运行结果如图 7.45 所示。

程序第 33 行, 设置当前绘制颜色为黑色。 程序第 34行, 设置图形环境应用的字体为 “华

文隶书” ,字体风格为粗体加斜体,字体大小为 35 磅。

程序第 36 行~第 40行:

String text = "欢迎来到《精通 Java Web动态图表编程》。这本书展示了如何使用" + "Java Applet、Servlet、Java Server Pages (JSP)及 JavaBean来" + "帮助你开发奇妙的 Web图表应用程序 ­ 以一种跨平台的、小巧的和" + "结构清晰的模式在 Web上生成动态图表。 " + "作者:钟京馗、唐桓" ;

借用了本书前言的一段话,定义了一个字符串对象 text。

程序第 42 行:

AttributedString as = new AttributedString(text);

Page 332: Java Web动态图表编程

图 7.45 文本自动分行

创建了一个 AttributedString对象 as。AttributedString 类位于 java.text.AttributedString包,

它用于保存文本的相关属性。这里 as 用于保存字符串对象 text 的相关属性。

程序第 43 行:

as.addAttribute(TextAttribute.FONT, font);

添加一些新的文本属性到 AttributedString 对象 as 中。

程序第 44 行:

AttributedCharacterIterator aci = as.getIterator();

调用 as的 getIterator 方法,创建一个 AttributedCharacterIterator 对象 aci。 AttributedCharacterIterator 对象可以使通过循环的方式,存取文本的相关属性。

程序第 46 行:

FontRenderContext frc = g2d.getFontRenderContext();

创建了 FontRenderContext 对象 frc。

程序第 47 行:

LineBreakMeasurer lbm = new LineBreakMeasurer(aci, frc);

创建了 LineBreakMeasurer 对象 lbm。lbm 用于将跨行的文本块断开以适应指定行长的 TextLayout 对象。

程序第 50 行~第 51行:

float x = 10.f; float y = 10.f;

定义了两个单精度的浮点变量 x 及 y。分别表示文本绘制位置的横坐标及纵坐标。

程序第 54 行:

float wrappingWidth = 450;

定义了单精度浮点变量 wrappingWidth,表示文本分行的宽度。也就是说,只要绘制的

文本宽度超过 450 像素,则文本的剩余部分就进行分行处理,一直到绘制完全部文本。

程序第 57 行~第 64行:

Page 333: Java Web动态图表编程

while (lbm.getPosition() < aci.getEndIndex())

TextLayout textLayout = lbm.nextLayout(wrappingWidth); y += textLayout.getAscent(); textLayout.draw(g2d, x, y);

y += textLayout.getDescent() + textLayout.getLeading();

分行处理的核心代码就是这一部分。LineBreakMeasurer 对象 lbm的 getPosition方法,返

回一个 int类型的变量, 表示当前 lbm的位置。 AttributedCharacterIterator对象 aci的getEndIndex 方法,也返回一个 int 类型的变量,表示文本最后一个字符的索引值。

在循环体中, 首先创建了一个TextLayout对象 textLayout, 该对象来源于 lbm的 nextLayout 方法。nextLayout 方法,接收一个 folat 参数 wrappingWidth。先更新当前文本的位置并返回

下一个 TextLayout 对象。

程序第 60 行,更新当前绘制文本的纵坐标。textLayout 的 getAscent 方法,返回一个浮

点值, 表示 textLayout的基线上升值。 程序第 61行, 调用 textLayout的 draw方法, 在Graphics2D 的绘图环境中的 x, y处绘制文本。

程序第 63 行,再次更新绘制文本的纵坐标,将 y值再累加上 textLayout 的基线下降值,

以及 textLayout 的超出值。

如果还有剩余的文本,就会继续重复上述步骤。最终的运行结果如图 7.45所示。

7.12 Java2D 创建复杂图形

除了前面介绍的利用 Java2D 创建一些基本的几何图形外,Java2D 还支持创建一些复

杂的图形。创建复杂图形的类有 Area、QuadCurve2D、CubicCurve2D 和 GeneralPath。

Page 334: Java Web动态图表编程

7.12.1 Area 不要

Area(区域)可用来从简单的形状(例如,圆和正方形)快速构造复杂的 Shape。通过

组合不同的Area来创建新的更复杂的 Shape。 Area支持任意 Shape图形对象之间的布尔操作。

如相加(add) 、相减(subtract) 、相交(ntersect) 、异或(exclusiveOr) 。以上几种布尔运算

如图 7.46 所示。

这里提供给读者一个绘制梨子的例程。pear.jsp(\chap07\Fig7.12\Fig.7.12_01)的运行结

果如下。借助几个 Area 对象(圆及椭圆)之间的布尔运算得到如图 7.47 所示的图形。

图 7.46 Area支持的布尔运算 图 7.47 pear.jsp

程序第 34 行~第 45 行,创建了 4 个 Ellipse2D.Double 对象,并以此为基础创建了 6 个 Area 对象。这 6 个 Area 对象中,circ 和 ov 用于创建梨子的主体部分,leaf1 和 leaf2 用于创

建梨子的叶子区域,st1和 st2 用于创建叶子的茎。

创建 Area 对象的方法是调用 Area 的构造器,并提交一个 Shape对象为参数。程序第 47 行,设置梨子的绘制基点。

程序第 50 行~第 56行:

leaf.setFrame(x ­ 16, y ­ 29, 15.0, 15.0); leaf1 = new Area(leaf); leaf.setFrame(x ­ 14, y ­ 47, 30.0, 30.0); leaf2 = new Area(leaf); leaf1.intersect(leaf2); g2d.setColor(Color.GREEN); g2d.fill(leaf1);

绘制梨子左上方的第一片叶子。Ellipse2D.Double 对象 leaf 的 setFrame 方法,用于设置 Ellipse2D 的外切矩形的大小和位置,然后重新定义了 Area 对象 leaf1。之后,再次设置 leaf 外切矩形的大小和位置,并重新定义 Area 对象 leaf 2。程序第 54 行,leaf1 和 leaf 2 做相交的

布尔运算,最后用绿色绘制 leaf1 和 leaf 2 做相交布尔运算后的图形区域。

同理,程序第 59 行~第 62 行,用相同方法绘制右上方第二片叶子。

程序第 65 行~第 71行:

stem.setFrame(x, y ­ 42, 40.0, 40.0); st1 = new Area(stem); stem.setFrame(x + 3, y ­ 47, 50.0, 50.0); st2 = new Area(stem); st1.subtract(st2); g2d.setColor(Color.BLACK);

①:相加

③:相交

②:相减

④:异或

Page 335: Java Web动态图表编程

g2d.fill(st1);

绘制梨子上方的茎。同样定义了两个椭圆型的 Area 对象 st1 和 st 2。但这次是通过 st1 和 st2 之间做相减的布尔运算来获得茎的图形(程序第 60 行),然后用黑色绘制梨子的茎。

程序第 74 行~第 80行:

circle.setFrame(x ­ 25, y , 50.0, 50.0); oval.setFrame(x ­ 19, y ­ 20, 40.0, 70.0); circ = new Area(circle); ov = new Area(oval); circ.add(ov); g2d.setColor(Color.YELLOW); g2d.fill(circ);

绘制梨子的主体部分。梨子的主体部分是通过两个相互重叠的圆及椭圆之间进行相加的

布尔运算得到的。最后用黄色绘制主体区域。

通过本例,我们掌握了如何利用 Area 对象的布尔运算来创建复杂图形对象的技巧。

7.12.2 曲线

Java 提供了两个用于绘制简单曲线的类。一个定义二次(二次方程式)曲线,另一个定

义三次(三次方程式)曲线。每种曲线都定义了一个位于两点之间的曲线。

1.二次曲线

二次曲线是一种最简单的曲线,它通过 3 个点来控制曲线的走向。一般只能表示向一个

方向弯曲的曲线。二次曲线由 QuadCurve2D 类表示。该类位于 java.awt.geom 包中,这个类

是 一 个 抽 象 类 , 也 是 QuadCurve2D.Double 和 QuadCurve2D.Float 类 的 基 础 类 。 QuadCurve2D.Double 表示双精度的二次曲线,QuadCurve2D.Float 表示单精度的二次曲线。

二次曲线由两个端点及一个定义每个端点处切线的控制点定义。切线是从端点到控制点且和

曲线相切的直线。 quadCurve.jsp(\chap07\Fig7.12\Fig.7.12_02)演示了如何绘制一条二次曲线,运行结果如图

7.48 所示。

图 7.48 二次曲线的绘制方法

程序第 34 行~第 36 行,定义了 3 个 Point2D.Double 的对象。start 和 end表示二次曲线

的两个端点,起始点和结束点。control 表示二次曲线的控制点。程序第 38行~第 41行,调

用 QuadCurve2D.Double 的构造器,创建了一个 QuadCurve2D.Double 类的对象 quadCurve。 QuadCurve2D.Double类的构造器需要接收 6 个双精度型的参数,分别对应于曲线的起始点,

Page 336: Java Web动态图表编程

控制点及结束点的坐标。这里定义了一个起始点为 start,结束点为 end,控制点为 control 的

二次曲线。

为了显示控制点对曲线方向是如何控制的,我们在程序第 43 行到第 48 行,用蓝色绘制

了控制点到曲线起始点和结束点的切线。最后用红色绘制了该二次曲线。

2.三次曲线

三次曲线能够更加准确地描述曲线的形状,它可以表示向多个不同方向弯曲的曲线。三

次曲线由 CubicCurve2D 类表示。该类位于 java.awt.geom 包中,是一个抽象类,同时也是 CubicCurve2D.Double 和 CubicCurve2D.Float 类的基础类。CubicCurve2D.Double 表示双精度

的三次曲线,CubicCurve2D.Float 表示单精度的三次曲线。三次曲线由两个端点及两个控制

点定义。每个控制点控制曲线的一个端点。 cubicCurve.jsp(\chap07\Fig7.12\Fig.7.12_02)演示了如何绘制一条三次曲线。 运行结果如图

7.49 所示。

程序第 34 行~第 37 行,定义了 4 个 Point2D.Double 的对象。start 和 end表示三次曲线

的两个端点,起始点和结束点。controlStart 表示三次曲线的第一个控制点,controlEnd 表示

三次曲线的第二个控制点。程序第 39 行~第 43行,调用 CubicCurve2D.Double 的构造器,

创建了一个 CubicCurve2D.Double类的对象 cubicCurve。CubicCurve2D.Double 类的构造器,

需要接收 8个双精度型的参数,分别对应于曲线起始点、第一个控制点、第二个控制点及结

束点的坐标。 这里定义了一个起始点为 start, 结束点为 end, 控制点为 controlStart和 controlEnd 的三次曲线。

图 7.49 三次曲线的绘制方法

为了显示控制点对曲线方向是如何控制的,我们在程序第 45 行到第 52 行,用蓝色绘制

了第一个控制点到曲线起始点的切线,第二个控制点到结束点的切线,以及连接两个控制点

之间的直线。最后用红色绘制了该三次曲线。

7.12.3 通用路径

通用路径(GeneralPath)用于创建复杂的图形,是由

直 线 和 复 杂 曲 线 构 成 的 图 形 。 该 类 位 于 java.awt.geom.GeneralPath 中。我们还是利用一个简单的

图 7.50 四角星

20 10 20

b

a c

d

Page 337: Java Web动态图表编程

例程来说明通用路径的用法。generalPath.jsp (\chap07\Fig7.12\Fig.7.12_03) 演示了如何利用通

用路径来绘制一组旋转的四角星。四角星绘制起始点和其他端点之间的关系图,如图 7.50 所

示。

从图 7.50 中可以看出, 四角星共有 8 个顶点。 如果以其最左边的一个顶点为绘制起始点,

假设起始点的坐标为 startX 和 startY,则四角星剩余 7 个顶点的坐标如下:

Ø 顶点 a:startX + 20,startY−5 Ø 顶点 b:a.x + 5,a.y−20 Ø 顶点 c:b.x + 5,b.y + 20 Ø 顶点 d:c.x + 20,c.y + 5 Ø 顶点 e:d.x−20,d.y + 5 Ø 顶点 f:e.x−5,e.y + 20 Ø 顶点 g:f.x−5,f.y−20 可以直接调用通用路径的默认构造器来生成一个 GeneralPath对象,如下所示:

GeneralPath star = new GeneralPath();

定义 star起始点的方法是调用 moveTo方法,moveTo 方法,接收两个单精度浮点数的参

数。用于表示起始点的坐标。因此,定义一个 Point2D.Float 的对象来表示起始点。如下所示:

Point2D.Float start = new Point.Float(startX, startY); start.moveTo(start.x, start.y);

定义起始点后,我们就可以从起始点开始添加新的路径了。以四角星中添加从起始点到 a 点的路径为例,需要调用 GeneralPath的 lineTo方法,如下所示:

star.lineTo(start.x + 20, start.y – 5);

lineTo表示起始点到 a 点之间的路径为直线。如果希望起始点到 a 点之间的路径是曲线,

则可调用 quadTo方法,表示两点之间的路径为二次曲线。如下所示:

Point2D.Float c1 = new Point2D.Float(x1, y1); start.quadTo(c1.x, c1.y, start.x + 20, start.y ­ 5);

其中 c1 表示该二次曲线的控制点。如果希望起始点到 a 点之间的路径是三次曲线,则可

调用 curveTo方法。如下所示:

Point2D.Float c1 = new Point2D.Float(x1, y1); Point2D.Float c2 = new Point2D.Float(x2, y2); start.curveTo(c1.x, c1.y, c2.x, c2.y, start.x + 20, start.y ­ 5);

这里的 c1、c2 分别表示起始点和 a 点的控制点。应用

了上述方法后,当前点,也就是 a 点,就被更新成路径的

终点,也成为后续路径的起始点。GeneralPath 类的 getCurrentPoint 方法,返回一个 Point2D 对象,表示当前

点的位置。closePath方法,表示把最后一个路径的终点和

路径最初的起始点连接起来。reset 方法,表示重新设置通

用路径。 generalPath.jsp的运行结果,如图 7.51 所示。

本程序中的注释已经向读者交代得很清楚了。这里我

们仅向读者说明四角星的绘制颜色是如何生成的。程序第 62 行~第 64行,利用 3个 0~255 之间的随机数,来分别

表示 Color 对象中的 RGB 值。 图 7.51 通用路径的应用

Page 338: Java Web动态图表编程

7.13 Web 图表实例解析

本节提供两个 Web 图表实例。首先,我们将前面学过的 3D 饼图修改成透明的 3D 饼图。

在绘制透明的 3D 饼图之前,我们只需要改动前面绘制 3D 饼图的方法,就可以用几行代码实

现前面许多行代码才能够完成的工作。最后,我们提供一个比较复杂的、综合的 Web 图表应

用实例——实时股市指数走势图,该走势图克隆了钱龙动态股市分析系统的大盘走势的运行

效果。

7.13.1 透明 3D 饼图 本章 7.6.3 节和 7.10 节中,我们向读者讲述了 3D 饼图的绘制方法。虽然得到了正确的

绘制结果,但程序却显得缺乏效率。程序是用几个完全独立的循环,来组成 3D 饼图的各个

圆弧的绘制工作。 我们也曾向读者建议如何改进前面所讨论的程序。这里我们提供一个将 3D 饼图的绘制工作在一个循环(包含一个子循环)完成的程序,并且增加了饼图的透视效果,

使其得到比较完美的运行效果。

图 7.52 为源程序 transparentPie3D.jsp(\chap07\Fig7.13\Fig.7.13_01)实际运行效果。

图 7.52 transparentPie3D.jsp的运行效果

transparentPie3D.jsp与以前介绍的 3D 饼图的源程序相比, 关键地方就是改变了循环的顺

序。整个饼图的绘制过程是由程序第 122 行~第 189行完成的。

先来看以前绘制 3D饼图的循环顺序:

for(int i = 0 ; i < bookTitle.length; i++)

... for (int j= thickness; j> 0; j­­)

...

首先绘制饼图中的一段圆弧, 然后再重复绘制 thickness 次, 就绘制了一个 thickness 像素

厚的 3D 圆弧。之后是绘制第二段圆弧,再重复绘制 thickness 次。当所有圆弧绘制完成后,

某种情况下,会出现饼图错色的错误。于是,在前面的例程中,再绘制一次圆弧,将发生错

色的饼图覆盖掉,才得到正确的结果。此外,这种饼图的绘制过程,不得不将 3D 饼图分解

编程类图书销售量统计 3D 饼圈

Page 339: Java Web动态图表编程

成几部分进行绘制,实在是麻烦。

本例中,我们仅将上述循环顺序进行了互换,如下所示:

for (int counter = thickness; counter > 0; counter­­)

... for(int i = 0 ; i < bookTitle.length; i++)

...

这里首先一次性将所有的圆弧绘制出来,组成一个完整的饼图,然后,再重复绘制 thickness 次。这样,通过一个简单的交换循环顺序的方法,就解决了以前例程的弊端。

下面讨论如何实现 3D 效果。

在子循环体中,首先程序第 127 行~第 128 行,打开反锯齿功能:

g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

程序第 131行~第 132 行:

AlphaComposite ac = AlphaComposite.getInstance(AlphaComposite.SRC_ OVER, .3f);

g2d.setComposite(ac);

将当前绘图环境的复合属性设置为 AlphaComposite 的实例 ac 所表示的复合对象。这里

设置图形对象的透明度为 0.3f。在随机生成圆弧角度及计算圆弧占饼图的比例后,程序在第 136 行进行了一个判断:

if (counter == 1 || counter == thickness)

如果程序是第一次或最后一次绘制饼图,则执行程序第 138 行~第 154 行代码。首先,

用当前复合属性填充圆弧(程序第 144 行)。程序第 147 行~第 148 行,重新设置复合属性的

颜色为不透明,用于绘制饼图中各段圆弧的轮廓。然后,用灰色及 1.2 像素宽的笔划绘制当

前圆弧的轮廓。

反之,如果程序不是第一次或最后一次绘制饼图,则执行程序第 158 行~第 164 行之间

的代码。这段代码是采用较深的颜色来描绘圆弧的轮廓的。

最后,当绘制最上面一层饼图的时候,同时在饼图周围绘制上说明文字。本功能由程序

第 174 行~第 187 行完成。

当结束内外两层循环后,程序的最终运行结果如图 7.52 所示。

这里需要注意的是,我们在程序第 120 行,设置第一段圆弧的起始角度为 30 度,所以

在图 7.52 中,表示 Python的褐色圆弧的起始角度总是在 30 度。如果不希望限定该圆弧的起

始角度,则只需要将程序第 120 行,改写成以下形式就可以了。

startAngle = (int)(Math.random() * 360);

这样表示 Python的褐色圆弧的起始角度就可以在任意角度开始了。

7.13.2 股市指数走势图

股市指数走势图是本章介绍的最后一个实例,也是一个比较复杂的 Web 图表应用实例。 stockIndex.jsp (\chap07\Fig7.13\Fig.7.13_02)是以上海乾隆高科技有限公司开发的钱龙动态股

市分析系统中的【大盘分析】模块中的【上证领先指标】子模块的运行结果为蓝本而编写的

一段代码。在进行讲解之前,我们先来看 stockIndex.jsp的运行结果,如图 7.53 所示。

Page 340: Java Web动态图表编程

图 7.53 stockIndex.jsp的运行结果

图表的背景用黑色绘制。整个图表划分三部分:标题区、顶部网格区和底部网格区。

Ø 标题区用于显示股市指标之一的“上证领先”的收盘指数。指数涨跌的具体点数及幅

度(用百分比表示)、当日最高指数及最低指数,当日及其昨日的收盘指数等。此外,

当指数上涨时,即当日的收盘指数大于昨日的收盘指数时,在指数的涨跌信息中,还

需要增加一个红色箭头向上的三角形,表示指数上涨;反之,则用绿色并且箭头向下

的三角形来表示指数下跌。昨日的收盘指数用白色表示。

Ø 顶部网格区用于绘制“上证领先”指数曲线。图表中所有的网格线为暗红色。网格左

边以昨日收盘指数为基准线(用白色数字表示),并标注有当日指数涨跌的变化情况,

位于昨日收盘指数上方的数字采用红色标注,表示当前指数高于昨日收盘指数。位于

昨日收盘指数下方的数字采用绿色标注,表示当前指数低于昨日收盘指数。同样,网

格右边用幅度,即百分比来表示指数的涨跌情况。在该网格区中,白色曲线表示当日

指数每分钟的变化情况。 因为中国股市的交易时间为上午的 9:30 到 11:30, 下午 13:00 到 15:00,指数曲线是以分钟为单位,因此白色的指数曲线实际上是由 240 点连接而

成的。

Ø 底部网格区用于绘制每分钟的股票交易量,这里采用的交易量是以“手”为单位。交

易量用黄色的直线表示。交易量越大,则黄色直线越长。在底部网格区的左边,用黄

色的数字注明交易量的数额。

虽然 stockIndex.jsp较长(超过 500 行),涉及的变量及方法较多,但结构却很清晰。

程序第 14 行~第 370 行,声明了很多变量及方法。程序第 372 行~第 518 行,依次绘

制了标题区、顶部网格区、指数走势曲线、底部网格区及交易量直线。在图表的绘制过程中,

多次调用了前面声明的变量及方法。 下面我们先看程序第14行~第370行声明的变量及方法。

程序第 18 行:

int width = 990, height = 600;

定义本图表的宽度和高度。这是目前我们绘制的最大尺寸的图表。

程序第 20 行:

int counter = 240;

Page 341: Java Web动态图表编程

counter 表示参与绘制指数走势曲线的各项数据的个数。 我们需要绘制的是每分钟股市大盘

指数的走势图,而股市交易的时间每天共 4个小时,因此,counter的值被设定为 240。

程序第 22 行~第 23行:

double xPoint[] = new double[counter]; double yPoint[] = new double[counter];

声明两个双精度值的数组 xPoint 和 yPoint。xPoint 表示指数走势曲线在横坐标上的绘制

位置,yPoint 表示在当日交易时间内每一分钟大盘指数的值。

程序第 25 行:

int volume[] = new int[counter]; // 交易手数

声明一个整型数组 volume。表示在当日交易时间内每一分钟大盘的交易手数。

程序第 26 行:

double preClose; // 昨日收盘指数

双精度变量 preClose表示昨日股市大盘的收盘指数。

程序第 28 行:

double open, close, max, min; // 今日开盘指数、收盘指数、最高指数、最低指数

声明 4 个双精度变量。open 表示当日股市大盘的开盘指数、close 表示当日股市大盘的

收盘指数、max 表示当日股市大盘的最高指数、min 表示当日股市大盘的最低指数。

程序第 31 行:

double scale, ratio;

双精度变量 scale 表示今日收盘指数与昨日收盘指数之差。ratio 表示当前计算出来的 scale值和昨日收盘指数之间的比值。

程序第 33 行~第 34行:

String time[] = "09:30", "10:00", "10:30", "11:00", "11:30", "13:30", "14:00", "14:30", "15:00";

字符串数组变量 time[]表示股市交易的时间。它用于绘制图表最下方的关于交易时间的

说明文字。

程序第 37 行~第 39行,定义了一个虚线笔划对象 bs。

程序第 40 行:

DecimalFormat twoDigits = new DecimalFormat("0.00");

声明了 DecimalFormat 对象 twoDigits,用来格式化浮点数的输出格式。

然后声明了一些方法,这些方法的名字及其所实现的功能如下。我们稍后具体介绍相关

方法的执行步骤。

Ø 程序第 45 行~第 117行,init 方法。随机生成并初始化每分钟股市指数及交易手数的

数据;

Ø 程序第 155行~第 182 行,drawDigits 方法。用于绘制标题区中出现的数字,如当日

的收盘指数、开盘指数、最高指数、最低指数及昨日收盘指数等;

Ø 程序第 185行~第 195 行,setDigitColor 方法。用于确定绘制标题区中数字的绘制颜

色,如果今日收盘指数大于昨日收盘指数,则使用红色,反之则使用绿色等;

Ø 程序第 198 行~第 257 行,drawTopGrid 方法。用于绘制顶部网格区,包括网格左边

和右边的说明文字;

Page 342: Java Web动态图表编程

Ø 程序第 273 行~第 327 行,drawBottomGrid方法。用于绘制底部网格区,包括网格左

边和下边的说明文字;

Ø 程序第 330 行~第 342 行,drawIndexCurve方法。用于绘制位于顶部网格区内的股市

大盘指数走势曲线。

此外,还声明了其他一些简单的方法,如获得股市最高指数等相关数据的方法。我们将

在后面做进一步的讲解。

程序首先从第 374 行开始运行。程序第 385 行,打开反锯齿渲染功能。注意:本例中,

凡是涉及绘制数字以及一些小字体的文本时,我们都暂时先关闭反锯齿渲染功能,等绘制工

作完成后,再重新打开反锯齿功能。本例中多次出现这种情况,此处不再赘述。

程序第 388行~第 389 行:

g2d.setPaint(Color.BLACK); g2d.fillRect(0, 0, width, height);

将图表的整个背景设置为黑色。

程序第 392行:

init();

调用 init 方法,初始化所有的数据。

public void init() preClose = 1000+Math.random() * 1500; // 初始化昨日收盘指数 max = preClose; min = preClose; for (int i = 0; i < counter; i++) if (i == 0) if (Math.random() > 0.5) yPoint[i] = preClose + Math.random() * 10;

else yPoint[i] = preClose ­ Math.random() * 10;

volume[i] = (int)(10000 * (1+Math.random() * 5)); else if (Math.random() > 0.5) if ((yPoint[i ­ 1] + Math.random() * 3) >= preClose * 1.018) yPoint[i] = yPoint[i ­ 1] ­ Math.random() * 3;

else yPoint[i] = yPoint[i ­ 1] + Math.random() * 3;

else if ((yPoint[i ­ 1] ­ Math.random() * 3) <= preClose * 0.982) yPoint[i] = yPoint[i ­ 1] + Math.random() * 3;

else yPoint[i] = yPoint[i ­ 1] ­ Math.random() * 3;

if (Math.random() > 0.5) volume[i] = volume[i ­ 1] + (int)(Math.random() * 10000);

else volume[i] = volume[i ­ 1] ­ (int)(Math.random() * 10000);

if (volume[i] >= 100000) volume[i] ­= 10000+(int)(Math.random() * 5000);

Page 343: Java Web动态图表编程

if (volume[i] <= 10000) volume[i] += 10000+(int)(Math.random() * 5000);

// 计算今日开盘指数及收盘指数 if (i == 0) open = yPoint[i];

else if (i == counter ­ 1) close = yPoint[i];

// 计算今日最高指数 if (yPoint[i] > max) max = yPoint[i];

// 计算今日最低指数 if (yPoint[i] < min) min = yPoint[i];

xPoint[i] = 80+i * 840 / 240; // 计算指数走势曲线横坐标

scale = close ­ preClose; // 计算今日收盘指数与昨日收盘指数之差; ratio = scale / preClose; // 计算 scale和昨日收盘指数之商;

程序第 147 行,生成了昨日收盘指数。昨日收盘指数是从一个 1000~2500 之间的随机

数中产生。 同时用该值初始化, 当日最高指数和最低指数。 程序第 50 行~第 113 行的循环中,

随机生成相关的数据。对于像股市指数这样的数据,我们随机生成这种类型数据的思路是:

首先生成第 1 个数据,第 2 个数据在第 1 个数据的基础上,进行微幅增加或减少的操作后得

到。同理,第 3 个数据在第 2个数据的基础上,通过微幅增加或减少的操作后得到,以此类

推。这样,既保证了数据的随机性,又保证了相邻数据间的值波动范围(在真实的股市交易

数据中,任意两个相邻时间段,例如,每分钟之间的数据之间的波动幅度是不大的)。

程序第 52 行~第 60 行,先判断程序是否是第 1 次循环。yPoint 数组中的第 1 个元素,

就是当日开盘指数,最后 1 个元素是当日的收盘指数。这里我们假设,当日开盘指数在昨日

收盘指数基础上, 在上下各 10 个点的区域内随机波动。 当方法 Math.random的值大于 0.5 时,

当日开盘指数在昨日收盘指数的基础上的上方 10 点的区域内进行取值;反之,则在下方 10 点的区域内进行取值。同理,程序第 59 行,在第 1次循环中,生成了表示交易量数组 volume 的第 1 个元素。

得到不同数组的第 1 个元素后,后面的循环(程序第 62 行~第 112 行)按照上述随机

数据的生成思路,对相关数组后续的 239 个元素进行赋值。注意,这里我们同样做了一些限

制,比如说,yPoint 数组中的任意一个元素的取值区间,实际上被限制在如下一个范围之内:

昨日收盘指数 * 0.982 <= 今日任一时刻的指数 <=昨日收盘指数 * 1.018 也就是说,今日股市任一时刻的指数以昨日收盘指数为基础,在其上下 1.8%的幅度内波

动。做此限制主要有两个因素,第一个因素是前面提到的,在真实的股市交易数据中,任意

两个相邻时间段,例如,每分钟之间的数据之间的波动幅度是不大的,第二个因素是为了绘

制指数曲线更平滑。

交易量的变化就比指数的变化大多了,因此,我们限定交易手数在 10000~100000之间

Page 344: Java Web动态图表编程

进行波动。

计算出当前随机指数数据和交易量数据后,程序第 91 行~第 98行,定义当日开盘指数

和收盘指数。开盘指数就是 yPoint 数组中的第一个元素,收盘指数就是 yPoint 数组中的最后

一个元素。

程序第 101 行~第 110 行,更新当前的指数最高值及最低值。当循环结束后,max 和 min就是当日指数中的最高值和最低值了。

程序第 112 行:

xPoint[i] = 80+i * 840 / 240; // 计算指数走势曲线横坐标

设定指数走势曲线绘制的横坐标,这个横坐标同样也是绘制交易量的横坐标。顶部网格

区域在本例中,实际上是一个矩形区域,其左上角顶点坐标是(80,40),宽度为 840 像素,

高度为 360像素。因此,我们用 80+i * 840 / 240 来表示绘制的横坐标。

当 init 方法中的循环过程结束后,我们就得到了相关的数据。在 init 方法中的最后两行,

即程序第 115 行~第 116 行:

scale = close ­ preClose; // 计算今日收盘指数与昨日收盘指数之差

ratio = scale / preClose; // 计算 scale和昨日收盘指数之商

计算双精度变量 scale 和 ratio 的值。关于 scale 和 ratio 变量的含义,请参考前面以及本

例的注释。

至此,init 方法就运行结束了。现在就可以绘制图表的各个区域了。首先绘制标题区域,

见程序第 395 行~第 401 行:

String chartTitle = "上证领先"; g2d.setColor(Color.YELLOW); g2d.setFont(new Font("楷体_GB2312", Font.BOLD, 25)); int stringLength = g2d.getFontMetrics().stringWidth(chartTitle); g2d.drawString(chartTitle, 0, 25); setDigitColor(); drawDigits(g2d, getClose(), stringLength + 5, 23);

这段代码,用黄色“楷体_GB2312”以及大小为 25 磅字体来绘制“上证领先”这几个字

符。程序第 400 行,调用 setDigitColor 方法,设置紧随其后的数字绘制颜色。如果今日收盘

指数大于昨日收盘指数则使用红色,反之,使用绿色。

程序第 401 行,调用 drawDigits 方法绘制数字。程序第 155 行~第 182 行的 drawDigits 方法,用于绘制标题区中出现的数字,这些数字格式是小数点后的数字字体比小数点前的数

字字体要小,两者的顶端持平,小数点后面保留两位小数。另外,小数部分的底端为一段横

线,该横线的长度和小数部分的数字长度相同。drawDigits 方法,实现了上述绘制风格。

同理,程序第 403行~第 409 行,绘制“涨跌”这两个字符,再绘制指数涨跌值。从图 7.53 中可以看出,文本“涨跌”与其后面的具体涨跌指数之间还有一个箭头,该箭头的绘制

过程见程序第 412 行~第 426行:

TriangleServlet ts = new TriangleServlet(); ts.setBaseLine(12); ts.setAlpha(60);

if (getScale() >= 0) ts.setFillColor(Color.RED); ts.drawTrigangle(stringLength + 202, 21, 1, 2, g2d); // 绘制红色箭头向

上的三角形

else

Page 345: Java Web动态图表编程

ts.setFillColor(Color.GREEN); ts.drawTrigangle(stringLength + 202, 12, 3, 2, g2d); // 绘制绿箭头向下

的三角形

这里还是调用前面的 Servlet:TriangleServlet 来绘制三角形,程序第 412 行,通过调用 TriangleServlet 的默认构造器创建了一个 TriangleServlet 的实例 ts。 程序第 413行、 第 414行,

分别设置 ts 的底边长度为 12 像素,底角度数为 60 度。然后,根据 scale 值是否大于零,来

决定三角形的填充颜色和方向。如果 scale 值大于零(通过 getScale 方法得到),则三角形的

绘制颜色为红色,方向向上;反之,三角形的绘制颜色为绿色,方向向下。

绘制“幅度” 、 “最高” 、 “最低” 、 “今开”和“昨开”的过程与上述过程完全相同,我们

在此略过。这部分图表绘制完成后,就结束了标题区域的所有绘制工作。

程序第 500行,调用 drawTopGrid方法,绘制顶部网格区域的内容。该方法位于程序 198 行~第 270行。

程序第 208行~第 223 行:

for (int i = 0; i < time.length; i++) g2d.setColor(new Color(168, 0, 0));

if (i % 2 != 0) g2d.setStroke(bs);

else g2d.setStroke(new BasicStroke());

g2d.drawLine(80+i * gridWidth, 40, 80+i * gridWidth, 360);

绘制了 8 条垂直方向直线,其中 4 条是虚线风格的直线。

前面我们在初始化指数的时候,设定今日股市任一时刻的指数以昨日收盘指数为基础,

在其上下 1.8%的幅度内波动。因此在程序第 225 行:

double max = getPreClose() * 1.02;

定义了一个局部的双精度变量 max,将其值设定为昨日收盘指数和 1.02 两者之间的积。

因此,这个变量 max 大于今日任意一个时刻的指数值。我们在这里又将顶部网格区域划分成

上、下两个部分。以昨日收盘价为基线,上、下部分又各自绘制了 8 条水平方向的直线。基

线坐标用白色绘制了昨日收盘价,在基线上方的直线所表示的指数值以昨日收盘价为基础,

依次递增 0.25%;在基线下方的直线所表示的指数值以昨日收盘价为基础,依次递减 0.25%。

也就是说,网格区域可以表示以昨日收盘价为基础,并且上下最大波动幅度为 2%的指数。

绘制完直线后,在其两端绘制相应的说明文字。见程序第 238 行~第 270行:

// 绘制水平方向的直线 for (int i = 0; i < 17; i++) g2d.setColor(new Color(168, 0, 0));

g2d.drawLine(80, 40+i * gridHeight, 920, 40+i * gridHeight);

g2d.setColor(Color.RED);

String leftStr = "", rightStr = "";

Page 346: Java Web动态图表编程

if (i < 17 / 2) leftStr = twoDigits.format(max * (1­i * 0.02 / 8)); rightStr = twoDigits.format(2­i * 0.25);

else if (i == 17 / 2) g2d.setColor(Color.WHITE); leftStr = twoDigits.format(getPreClose()); rightStr = twoDigits.format(0.00);

else g2d.setColor(Color.GREEN); leftStr = twoDigits.format(max * (1­i * 0.02 / 8)); rightStr = twoDigits.format(i * 0.25 ­ 2);

// 绘制水平方向上的左边关于指数以及右边关于指数百分比的说明文字 g2d.setFont(new Font("Arial", Font.BOLD, 12)); stringHeight = g2d.getFontMetrics().getAscent(); stringLength = g2d.getFontMetrics().stringWidth(leftStr); g2d.drawString(leftStr, 75­stringLength, 40+i * gridHeight +

stringHeight / 2); g2d.drawString(rightStr + "%", 925, 40+i * gridHeight + stringHeight /

2);

最后重新设置图形环境的反锯齿功能。 结束顶部网格区域的绘制工作后, 程序执行第 503 行的 drawIndexCurve方法。drawIndexCurve方法,用于绘制顶部网格区域内的白色指数走势

曲线。该方法详见程序第 330行~第 342 行: public void drawIndexCurve() Line2D.Double line = new Line2D.Double(); g2d.setColor(Color.WHITE); for (int i = 0; i < counter; i++) if (i < counter ­ 1) line.setLine(xPoint[i], countCurveY(yPoint[i]), xPoint[i + 1],

countCurveY(yPoint[i + 1])); g2d.draw(line);

指数走势曲线在本方法中,是通过绘制 239 条相连的 Line2D.Double 对象而实现。本段

代码中,首先设置绘制颜色为白色。然后在一个循环中,每次绘制一条连接当前指数及其后

续指数之间的Line2D.Double对象 line。 这里我们将指数值作为参数传递给 countCurveY方法,

用于将指数值转换成顶部网格区域内相对应的纵坐标。 程序第 345行~第 350行 countCurveY 方法,实现了这一转换。

public double countCurveY(double yPoint) double y; y = 360­(320* (yPoint ­ getPreClose() * 0.98) / (getPreClose() * 0.04)); return y;

Page 347: Java Web动态图表编程

该方法中,程序第 348行,实现了指数向绘制参数进行转换的核心语句。转换思路如图 7.54 所示。

图 7.54 转换方法

这里假设当前指数值为 yPoint。在网格区域中,走势曲线上的 a 点对应于该值。那么 a 点的纵坐标该如何确定呢?

我们知道,顶部网格区域的左上角顶点坐标为(80,40),左下角顶点坐标为(80,360)。

也就是说,顶部网格区域的高度为 320像素。而网格最下端的边线所代表的指数值为昨日收

盘指数的 98%,最上端的边线所代表的指数值为昨日收盘指数的 102%。因此,320 像素的高

度正好代表了昨日收盘指数的 4%所表示的高度。

假设点 a距网格最下端的边线的距离为 y,则我们可以得到以下的比例公式:

(preClose * 1.02 – preClose*0.98) : 320 = (yPoint – preClose*0.98):y

变形后得到:

y = (320 *(yPoint – preClose*0.98)) / (preClose * 0.04)

因此点 a 的纵坐标实际上等于 360−y。这就是 countCurveY 方法中,第 348 行语句的推

导过程。

程序第 506行,调用 drawBottomGrid方法,绘制底部的网格区域。程序第 509 行,调用 drawVolume绘制交易量直线。在 drawVolume方法中,又调用了 countVolumeY 方法,计算交

易量在底部网格区域的相对绘制参数。计算方法的思路和刚才我们讲述的 countCurveY 方法

完全一样。

到此为止,指数走势曲线的图表绘制工作就结束了。

7.14 本章小结

本章通过大量的实例,介绍了基于 Java2D 的 Web 图表绘制的各种技术。 Java2D 包含很多复杂的图形处理知识, 通过各种不同绘制方法及属性的组合可以生成许

多运行效果很好的 Web 图表。对于复杂的图表来说,通常的方法是将其分解成不同的部分,

并将图表每部分中的图形对象尽量分解成最基本的图形对象,或者通过不同的基本图形而组

成图形对象。此外,必须掌握色彩的复合、图形边缘的反锯齿、图形对象的转换、变形,以

Page 348: Java Web动态图表编程

及 GeneralPath对象的使用方法等。

我们建议读者在理解本章内容后,继续深入地了解 Java2D 更多的内容及其运行机制。

这样能够更好地掌握和理解 Java 对图形处理的机制。

第 8 章 开放源代码作品与 Web 图表编程

Java 能够在 90 年代末期至今高速发展,离不开一些开放源代码组织的贡献。如本书所

使用的 Jakarta(Tomcat)、Resin就是开放源代码作品。在 Web 图表开发领域也有两个主要的 Web 图表生成引擎——JFreeChart 和 Cewolf。它们都是开源作品,在 Web 图表引擎中占据了

非常重要的位置。因为 Cewolf的图表生成和渲染引擎实际是调用 JFreeChart 的图表生成及

渲染引擎, 因此本章将详尽讲解 JFreeChart 这个 Web图表生成引擎的安装、 配置及使用方法。

在掌握 JFreeChart 之后,我们再讲解 Cewolf的安装、配置及使用方法。

8.1 开放源代码作品简介

open­source(开放源码)作品被定义为描述其源码可以被公众使用的软件,并且此软件

的使用、修改和分发也不受许可证的限制。开放源码软件通常是有版权的,其许可证可能包

含这样一些限制,如有意保护它的开放源码状态、作者身份的公告或者开发的控制。 “开放源

码” 正在被公众利益软件组织注册为认证标记, 这也是创立正式的开放源码定义的一种手段。

开放源码软件主要散布于全世界的编程队伍,同时一些大学、政府机构承包商、协会和

商业公司也在开发它。开放源码软件在历史上曾经与 UNIX、Internet 联系得非常紧密。源码

分发也是实现交叉平台可移植性的惟一可行的实际办法。

在所有的开放源代码作品中,最大名鼎鼎并且在实际工作中得到广泛应用的非 Linux 莫

属。 这是由芬兰赫尔辛基大学学生 Linus Torvalds 在 1991 年开发出来的类 UNIX 的操作系统。 Linus Torvalds 把 Linux 的源程序在 Internet 上公开,世界各地的编程爱好者自发组织起来对 Linux 进行改进和编写各种应用程序。今天,Linux 已发展成为一个功能强大、运行非常稳定

及高效的、几乎支持目前所有硬件的操作系统,成为操作系统领域最耀眼的明星。 Linux 以其高效性和灵活性著称。 能够在 PC 计算机上实现全部 UNIX 特性, 具有多任务、

多用户的能力。Linux 之所以受到广大计算机爱好者的喜爱,主要原因有两个:一是它属于

自由软件,用户不用支付任何费用就可以获得它和它的源代码,并且可以根据自己的需要对

它进行必要的修改,无偿对它使用,无约束地传播;另一个原因是它具有 UNIX 的全部功能,

任何使用 UNIX 操作系统或想要学习 UNIX 操作系统的人都可以从 Linux 中获益。 Linux 开发以及它的源代码是在 GPL(General Public License一般公共许可)的保护下。

虽然它们对个人都是完全免费的,但是这并不意味着 Linux 和它的一些周边软件发行版本也

是免费的。对于开放源代码作品来说,它受到许可证的保护。许可证实际上是用户在使用开

放源代码产品时,同意遵从开放源代码产品及其作者所声明的权利及相关协议。

在 Linux 早期的版本,因为开发者担心自己的程序被其他厂商窃取,甚至喧宾夺主,因

Page 349: Java Web动态图表编程

此使用了相当严格的版权声明,禁止一切的商业行为。因为希望有人能够把 Linux 压成 CD­ROM,推广 Linux,造福那些不方便使用 Internet 及 FTP 的朋友,所以将版权声明更换成 GPL。从此之后,Linux 便是公认的 GNU操作系统。

GNU 起源于十余年前。Richard Stallman 在网络讨论区的一篇文章(http://www.cs.pdx. edu/~trent/gnu/begin, GNU announcement)中说明了他为什么要发起这一计划。这篇文章就

是后来的 GNU 宣言(GNU Manifesto) 。Richard Stallman 后来成立自由软件基金会(Free Software Foundation),全力投入 GNU 项目的工作,是自由软件基金会的终身义务工作者。 GNU是 GNU's Not Unix 的缩写。 在 GNU宣言内 Stallman提到 UNIX 虽然不是最好的操作系

统,但是至少不会太差,而他自信有能力改进 UNIX 的不足,使它成为一个优良的操作系统,

就是名为 GNU的操作系统。 GPL 许可证首先依照著作权法,获得软体的知识产权,然后通过 GPL 将此权力释放给

大众。只要遵守 GPL,不把原始代码以及对原始代码所做的修改据为己有,就可以拥有使用 GPL软件的权力。GPL的规定,有人认为过于严格。为了能够鼓励更多的人使用 GNU软体,

因此自由软件基金会另外制定了 LGPL—Library GPL 许可,限制比较宽松。

除 GPL许可证外,常见的一些 Open­source许可证类型如下:

Ø BSD(Berkeley Source Distribution)许可证:要求版权和著者身份申明;

Ø LGPL:库通用许可证。它不同于 GPL许可证,在该许可证下,库(函数库)可以自

由地联接到私有软件;

Ø Artistic License:使作者保持对进一步开发的控制;

Ø NPL(Netscape Public Lincense):基于 GPL精神的新的许可证,但是保持了对源码更

多的控制和所有权;

Ø Public Domain/Not Copyrighted/No Restrictions: 通常用于决定性的算法以鼓励广泛使

用,通常由美国政府使用。

综上所述,从操作系统到应用程序,从程序开发的编译器到开发工具,都有丰富的开放

源代码产品可供选择。前面我们所阐述的Web 图表例程,都是由我们自己编写并调试所有源

代码而得来的。除此之外,我们还可以借助第三方的图表生成引擎来生成图表,可以利用一

些 Java 开放源代码开发组织发布的作品,主要由以下两个开放源代码作品组成:

Ø 由 www.jfree.org推出的 JFreeChart;

Ø 由 cewolf.sourceforge.net 推出的 Cewolf。Cewolf是采用 JFreeChart 的图形绘制引擎而

另外开发的一套 Web 动态图表生成引擎。

利用 JFreeChart 或 Cewolf 提供的 API 接口,我们可以直接调用 JFreeChart 或 Cewolf 来

生成各种常见的 Web 图表。

提示:JFreeChart 官方网站,没有提供如何在 JSP 环境下调用 JFreeChart 的方法。对于

在 Servlet/JSP 环境下调用 JFreeChart 生成图表,其官方网站推荐使用 Cewolf。即便如此,我

们认为研究 JFreeChart 在 Servlet/JSP 环境下的应用方法,仍然是非常重要的。

8.2 JFreeChart 与 JSP图表编程

8.2.1 JFreeChart 简介

JFreeChart 是开放源代码组织 jfree.org 的一个 Java 开源项目(http://www.jfree.org/ freechart/index.html) 。它遵从 GNU下的 Lesser General Public License许可证协议。

JFreeChart被设计为在 Java Application、 Applet、 Servlets, 以及 JSP下生成各种图表 (PNG、

Page 350: Java Web动态图表编程

JPG 等图片格式),这些图表包括饼图、柱状图(普通柱状图及堆栈柱状图) 、线图、区域图、

分布图、混合图、甘特图,以及一些仪表盘等。如果结合 iText 引擎,可以生成 pdf格式的文

档;结合 Batik组件可以输出 SVG 格式(可伸缩矢量图 Scalable Vector Graphics SVG,是一

种基于 XML的语言,用于绘制二维图形)的矢量图形。 Text 引擎及 Batik组件也都是开放源代码作品。两者的下载地址及相关介绍如下:

Ø iText: http://www.lowagie.com/iText Ø Batik: http://xml.apache.org/batik 对此感兴趣的读者,在阅读本章后,可以继续研究相关领域的内容。到本书结稿时,

JFreeChart 的最新版是 0.9.21 版。 JFreeChart 是一款非常优秀的 Java 图表生成引擎,而且完全免费使用。但它有三个缺陷

直接影响了它的普及。

(1)JFreeChart 项目开发组织并不提供其 API的开发文档,没有开发文档,要靠自己摸

索来掌握一个开发工具实在是有点“蜀道难,难于上青天”的感觉。而且据我们的测试和研

究,JFreeChart 在不同版本之间的确存在着兼容性的问题(这点也是很多开放性源代码产品

的一个通病),此外,网络上能找到的 JFreeChart 使用方法等资料不仅数量稀少、版本陈旧,

而且绝大部分资料所提供的方法或源代码并不能在目前最新版的 JFreeChart 0.9.21 下正确运

行,这就导致学习 JFreeChart 的朋友感到非常困难。

(2)JFreeChart 的演示程序中,没有提供如何在 Servlet/JSP 环境下调用其类库来生成 Web 图表。对于在 Servlet/JSP 环境下,如何调用其类库来生成 Web 图表,需要开发者自行摸

索。在现今对 B/S 应用程序的要求越来越高的情况下,无疑是一个很大的缺陷。虽然其官方

网站对 JFreeChart 在 Servlet/JSP 提供了一个 jfreechart­sample.war 演示包(http://home Page.ntlworld.com/richard_c_atkinson/jfreechart/),但如何部署,也没有具体的解释。该演示包

并非运行在 JFreeChart 的最新版下,而是工作于 JfreeChart 0.9.20 版。

(3)JFreeChart 的类库和结构非常繁杂,要理清其结构和相互之间的关系,是一件具有

挑战性的工作。我们必须了解每个类型的图表对象应该对应哪些 Axis、Plot、Renderer 类,

并且必须非常熟悉这些类的构造函数中每个参数的具体含义。这些问题大大困扰着很多的初

学者。我们在研究 JFreeChart 应用的时候,也不得不花大量的时间来理解其类库中各个类之

间的关联。

迄今为止,我国还没有一本讲述如何使用 JFreeChart/Cewof 的专著。再加上其提供的文

档是英文的,对国内大部分读者来说,学习 JFreeChart 就具有一定的难度。经过我们的研究

和测试,这里向本书读者提供比较完整的最新版 JFreeChart 在 JSP 环境下的实例及其使用方

法。本章重点讲述 JFreeChart 及 Cewolf 的使用实例。试图将 JFreeChart 和 Cewolf 的学习难

度降低到最低,使读者可以轻轻松松地学习 JFreeChart。建议读者仔细阅读并理解我们编写

的实例,并在实际的开发工作中加以修改。如果读者对计算机图形学及算法有一定的研究,

请自行查阅 JFreeChart 的源代码,就可以深入理解 JFreeChart 的运行机制了。

虽然存在上述一些问题,但是 JFreeChart 本身仍不失为一个非常优秀的图表引擎,况且

该开源项目本身也在不断的发展中,某些缺陷相信会得到解决。

综上所述,我们将重点放在讲述 JSP 环境下 JFreeChart 的应用上,而不过多地涉及 JFreeChart 自身的运行机制。

8.2.2 JFreeChart 的安装及其核心类

1.JFreeChart 的下载与安装

首先需要到 JFreeChart 的官方网站去下载最新的版本:http://www.jfree.org/jfreechart

Page 351: Java Web动态图表编程

/index.html,这里有两种版本的 JFreeChart 可供下载:

Ø jfreechart­0.9.21.tar.gz,该版本适用于 UNIX/Linux。

Ø jfreechart­0.9.21.zip,该版本适用于Windows。

选择好适用的版本后,需要将下载的文件解包。

(1)在 UNIX/Linux 平台下的 SHELL中,输入如下命令:

jar –xvf jfreechart­0.9.21.tar.gz

会在当前目录下建立一个名字为“jfreechart­0.9.21”的目录,并将解包后的文件存放在

其中。

(2)jfreechart­0.9.21.zip,该版本适用于 Windows。有两种方法将其解包:

Ø 在【命令提示符】下,输入以下命令:

jar –xvf jfreechart­0.9.21.zip

Ø 直接利用 WinZIP 或者 WinRAR 之类的能够压缩/解压缩工具软件,直接将 jfreechart­ 0.9.21.zip释放到某个目录即可。

jfreechart­0.9.21.zip文件解包后,我们可以看到在名字为“jfreechart­0.9.21”目录下,包

含如下一些目录及文件,其具体含义如表 8.1 所示。

表 8.1 JFreeChart的目录结构

文件/目录名 说 明

ant 只包含一个 Ant 的 build.xml 脚本文件的目录。我们可以利用该脚本重新编译整个

JFreeChart 的源代码

CHANGLOG.TXT 一个文本文件,用于记录当前版本在原先版本的基础上,做了哪些改进

checkstyle 包含了几个 Checkstyle 属性文件(xml 格式)的目录。这些属性文件定义了 JFreeChart 源

代码的编码风格

jfreechart­0.9.21.jar

JAR 的全称是 Java Archive File,是 Java 存档文件。有些类似 zip文件,主要用于将一些

文件,主要是 class 文件,还有文本文件或图像文件等,重新组织、整理、压缩并存放在一

个包中,以方便调用相应的 class 文件。jfreechart­0.9.21.jar 包含着运行 JFreeChart 所需要的

库文件

jfreechart­0.9.21­demo.jar 一个可执行的,用于演示 JFreeChart 的 Java 应用程序

lib JFreeChart 需要的一些库文件

licence­LGPL.txt GNU 下关于 LGPL 许可证的说明文件

README.txt 文本文件,说明 JFreeChart 的一些重要信息

source 包含了 JFreeChart 所有源代码的目录

建议读者首先阅读 README.txt 文件,然后运行一个 JFreeChart 的演示程序。运行演示

程序的方法是在“jfreechart­0.9.21”的目录下面,输入如下命令:

java –jar jfreechart­0.9.21­demo.jar

如果希望重新编译 JFreeChart 中的类文件,可以利用 ant 目录中的 build.xml 文件。具体

用法是:转换到“jfreechart­0.9.21”目录中的“ant”子目录,输入以下命令:

ant compile

执行此命令后,将重新编译所有的源程序,并重新生成相应版本的 jar 文件。

提示:如果运行 ant compile命令,必须保证系统中安装有 Ant 1.5.1 或更高版本。Ant 的

Page 352: Java Web动态图表编程

内容不属于本书的讲述范畴,请读者自行浏览其官方网站以获得更多的信息: http://ant.apache.org/。

此外,JFreeChart 源程序还提供了一个扩展的 JavaDoc 命令,我们可以利用 JavaDoc 工

具,从源程序中直接生成一个 HTML格式文档,同样是利用 ant 目录中的 build.xml 文件。输

入以下命令:

ant javadoc

执行此命令后,将会在“jfreechart­0.9.21”目录下创建一个名为“javadoc”的子目录。

该子目录包含所有的 Javadoc HTML文档。

现在,我们需要把 jfreechart­0.9.21.jar及其 lib子目录下的 gnujaxp.jar、jcommon0.9.6.jar、 junit.jar 和 servlet.jar 这四个 JAR 文件拷贝到Web 应用程序根目录下的WEB­ INF\lib 子目录

中。如果读者的 Web 应用程序根目录下的 WEB­INF 目录中没有 lib 子目录,请自行创建 lib 子目录。

在/WEB­INF/web.xml 文件中增加以下内容:

<servlet> servlet­name>DisplayChart</servlet­name> <servlet­class>org.jfree.chart.servlet.DisplayChart</servlet­ lass>

</servlet> <servlet­mapping>

<servlet­name>DisplayChart</servlet­name> <url­pattern>/servlet/DisplayChart</url­pattern>

</servlet­mapping>

重新启动 Tomcat,就可以运行后面章节所介绍的源代码了。

2.JFreeChart 的核心类简介

在进入 JFreeChart 的世界之前,我们先大致了解一下 JFreeChart 中几个核心的对象类及

其功能,如表 8.2 所示。

表 8.2 JFreeChart的核心类简介

类 名 类的作用以及简单描述

JFreeChart 图表对象。 任何类型图表的最终表现形式, 都是在该对象的一些属性进行设置。 JFreeChart

引擎本身提供了一个工厂类(ChartFactory)用于创建不同类型的图表对象

续表

类 名 类的作用以及简单描述

XXXXXDataset 数据集对象。用于提供显示图表所用的数据。根据不同类型的图表对应着很多类型的数

据集对象

XXXXXPlot 图表绘制区域对象。基本上这个对象决定什么样式的图表,创建该对象时需要 Axis、

Renderer,以及相应的数据集对象的支持

XXXXXAxis 用于处理图表的两个轴:纵轴和横轴

XXXXXRenderer 负责如何显示或渲染一个图表对象

XXXXXURLGenerator 用于生成 Web图表中某个项目的鼠标单击链接

XXXXXToolTipGenerator 用于生成图象的帮助提示,不同类型图表对应不同类型的工具提示类

在了解上述基本类的功能后,现在就请读者和我们一起学习 JFreeChart 在 JSP 环境下的

编程方法。

Page 353: Java Web动态图表编程

8.2.3 JFreeChart 生成直方图表

1.普通垂直直方图表

首先,我们向读者提供一个最基本的,利用 JFreeChart 生成的普通垂直直方图实例—— JFreeChartBasicBar.jsp(\chap08\Fig8.2\Fig.8.2_01)。其运行效果如图 8.1所示。

图 8.1 JFreeChart实例 1:垂直直方图

利用 JFreeChart 生成动态图表的基本步骤简述如下:

(1)引入所有相应生成图表所需的 class 文件或包;

(2)创建绘制图表所需的数据集;

(3)创建 JFreeChart 的实例;

(4)设置图表的生成格式及文件名;

(5)设置图表的浏览路径;

(6)在 HTML文档中,调用<img>标记来显示该图表。

其中,第(4)、(5)、(6)步骤是固定的。如果我们把 JFreeChart 所有的包在第(1)个

步骤全部引入,那么第(1)个步骤,也是可以固定不变的(不同风格的图表,需要引入不同

的包)。因此,调用 JFreeChart 生成图表的核心步骤是第(2)步及第(3)步。 JFreeChartBasicBar.jsp向读者演示了上述动态图表生成的各步骤。

程序第 6 行~第 12 行:

import="org.jfree.chart.ChartFactory" import="org.jfree.chart.JFreeChart" import="org.jfree.chart.plot.PlotOrientation" import="org.jfree.data.category.DefaultCategoryDataset" import="org.jfree.chart.servlet.ServletUtilities" import="org.jfree.chart.ChartRenderingInfo" import="org.jfree.chart.entity.StandardEntityCollection"

引入了本例中调用 JFreeChart 所必需的包。 这里为了向读者展示 JFreeChart 的 class 文件

的结构,我们逐一引入了本例所使用的 JFreeChart 的 class 文件。对于这些 class 文件,本书

不会进行逐一讲解,请读者自行阅读 JFreeChart 目录中【source】子目录下的相关源程序。

程序第 24 行~第 36行:

// 创建数据集

Page 354: Java Web动态图表编程

DefaultCategoryDataset dataset = new DefaultCategoryDataset();

// 向数据集中添加绘制图表所需的数据 int bookSales; for (int i=0; i < bookTitle.length; i++)

for (int j=0; j < category.length; j++ )

bookSales = 1 + (int)(Math.random() * 100); dataset.addValue(bookSales, bookTitle[i], category[j]);

本段代码实现了步骤(2)。首先是创建数据集对象:dataset,dataset 是通过调用 Default CategoryDataset 类的默认构造器而创建的。DefaultCategoryDataset 类位于 org.jfree. data. category包中。然后,调用 dataset 对象的 addValue方法,向数据集 dataset 中添加绘制图表所

需的数据。注意,我们在 addValue 方法中,提供的数据类型是整型。实际上,JFreeChart 支

持双精度类型的数据。

程序第 39 行~第 49行:

JFreeChart chart = ChartFactory.createBarChart (

chartTitle, // 图表标题

"销售时间:2005年 2月", // 坐标标题

"销售量", // 坐标标题

dataset, // 定义绘制数据

PlotOrientation.VERTICAL,// 直方图的方向

true, // 定义图表是否包含图例

true, // 定义图表是否包含提示

false // 定义图表是否包含 URL );

本段代码实现了步骤(3)。首先是创建 JFreeChart 对象:chart,chart 是通过调用 Chart Factory 类的 createBarChart 方法而创建的。ChartFactory 类和 JFreeChart 类都位于 org. jfree.chart 包中。对于我们所需要的各种风格的图表,都是通过 ChartFactory 类的相关 creat XXXXXX 方法而创建的。这些相关的 creatXXXXXX方法将在后面的实例中向读者讲述。本

例是创建了一个垂直直方图的图表,因此调用的方法是 creatBarChart 方法。creatBarChart 方

法需要 8 个参数,第 1 个~第 3 个参数,是字符串对象,分别表示图表的标题,图表坐标轴

中横轴的标题及坐标轴中纵轴的标题;第 4 个参数是数据集的对象:dataset;第 5 个参数定

义了直方图的绘制方向,它是一个 PlotOrientation 类的对象。该类提供了两个静态的 PlotOrientation对象 PlotOrientation.VERTICAL,以及 PlotOrientation. HORIZONTAL,分别表

示 直 方 图 的 绘 制 方 向 , 是 垂 直 方 向 或 者 水 平 方 向 。 当 设 置 绘 制 方 向 是 PlotOrientation.HORIZONTAL,则前面设定的第 2 个及第 3 个字符串参数的绘制位置,也相

应地跟着调整;第 6个参数,定义在最终生成的图表中是否包含图例。本例中的图表位于图

表底部的中间位置,是一个细长的矩形,其中使用文字及不同颜色的小方块来表示直方图中

所对应的对象;第 7个参数,定义在最终生成的图表中是否包含提示,这里的提示,是指直

方图中的每个直方块的说明文字;第 8个参数,表示在最终生成的图表中,是否生成链接。

程序第 51 行~第 52行:

ChartRenderingInfo info =new ChartRenderingInfo(new StandardEntity Collection());

创建一个 ChartRenderingInfo类的对象 info,这个调用方法也是固定用法, 读者只需要记

住用法就可以了。

Page 355: Java Web动态图表编程

程序第 55 行~第 56行:

String fileName =ServletUtilities.saveChartAsPNG(chart, width, height, info, session);

本段代码实现了步骤 4。 设置图表的生成格式及文件名。 这里生成的是 PNG 格式的图形, PNG 图形是保存在 Web 服务器的临时目录中的,当相应的 session失效后,该图形也就自动

从临时目录中被删除。所有保存在 Web 服务器临时目录中的图形,都被 ChartDeleter 类的对

象所管理,当相应的 session失效后,就被 ChartDeleter对象自动删除。

程序第 59 行~第 60行:

String graphURL =request.getContextPath() + "/servlet/DisplayChart? filename=" + fileName;

本段代码实现了步骤 5。设置图表的浏览路径。这里的“/servlet/DisplayChart?filename =”

表示这些图表都由前面在/WEB­INF/web.xml 文件中设置的 DisplayChart 这个 Serlvet 处理。

程序第 64 行~第 73行:

<HTML> <HEAD>

<TITLE><%=chartTitle%></TITLE> </HEAD> <BODY>

<P ALIGN="CENTER"> <img src="<%=graphURL %>" border="1" > </P>

</BODY> </HTML>

本段代码实现了步骤 (6)。 是标准的 HTML标记。 注意程序第 70 行, 调用 HTML的<img> 标记来显示实时生成的图表。在<img>标记的“src”属性中,我们通过<%=graphURL %>,

指明图表的位置,以便客户端向相应的 Servlet 发出请求,获得相关的响应,并最终显示该图

表。

可以看出,调用默认的 JFreeChart 相关方法生成的直方图,并不是一件难事。下面,我

们用相同的方法为读者演示如何生成 3D 直方图。

2.3D 水平直方图表

下面为读者演示一个生成具有 3D 效果的水平直方图实例——JFreeChartHBar3D.jsp (\chap08\Fig8.2\Fig.8.2_02)。JFreeChartHBar3D.jsp 是在前例的基础上做了些改动。运行效果

如图 8.2 所示。

Page 356: Java Web动态图表编程

图 8.2 JFreeChart实例 2:3D水平直方图

与前例相比,仅有 3行代码不同,分别是程序第 19行:

String chartTitle = "JFreeChart实例 2: 编程类图书周销售量水平 3D直方图";

程序第 39 行:

JFreeChart chart = ChartFactory.createBarChart3D

如果要生成具有 3D效果的直方图,必须调用 ChartFactory类的 createBarChart3D 方法。

程序第 45 行:

PlotOrientation.HORIZONTAL, // 直方图的方向

制定直方图的绘制方向为水平方向。程序的其余部分完全相同,此处不再赘述。从本例

可以看出,对于同一个数据集来说,调用 ChartFactory 的方法不同,就可以生成不同风格的

图表。而通过 PlotOrientation 类的静态实例 PlotOrientation.VERTICAL,以及 Plot Orientation.HORIZONTAL,就可以设置图表的绘制方向。

这两个例子都是调用的 JFreeChart 的默认方法而生成的默认风格的图表。那么如何生成

自定义的图表呢?下面我们来一起讨论。

3.自定义直方图表

JFreeChartCustomBar.jsp (\chap08\Fig8.2\Fig.8.2_03)向读者演示了如何绘制自定义的直方

图。JFreeChartCustomBar.jsp运行结果如图 8.3 所示。

销售

时间

2005

年2月

第 1周

第 2周

第 3周

第 4周

Page 357: Java Web动态图表编程

图 8.3 JFreeChart实例 3:绘制自定义直方图

本例向读者介绍了部分绘制属性的自定义方法。更多的自定义绘制属性的方法,我们将

在后面的例程中向读者介绍。

本例将创建数据集对象的工作,从主程序中移植到第 28 行~第 40 行的 createDataset 方

法中完成; 将创建 JFreeChart 对象的工作, 从主程序中移植到第 42行~第 57 行的 createChart 方法中完成。

程序第 6 行~第 18 行:

import="java.awt.*" import="org.jfree.chart.ChartFactory" import="org.jfree.chart.JFreeChart" import="org.jfree.chart.axis.CategoryAxis" import="org.jfree.chart.axis.CategoryLabelPositions" import="org.jfree.chart.plot.CategoryPlot" import="org.jfree.chart.plot.PlotOrientation" import="org.jfree.chart.renderer.category.BarRenderer" import="org.jfree.data.category.CategoryDataset" import="org.jfree.data.category.DefaultCategoryDataset" import="org.jfree.chart.servlet.ServletUtilities" import="org.jfree.chart.ChartRenderingInfo" import="org.jfree.chart.entity.StandardEntityCollection"

引入了本例中调用 JFreeChart所必需的包。 为向读者展示 JFreeChart的 class文件的结构,

我们逐一引入了本例所使用的 JFreeChart 的 class 文件。

程序第 63 行~第 64行:

CategoryDataset dataset = createDataset(); JFreeChart chart = createChart(dataset);

分别调用 createDataset 方法和 createChart 方法, 创建数据集对象 dataset, 以及 JFreeChart 对象 chart。createChart 方法需要一个数据集对象 dataset 作为参数。

程序第 67 行~第 103行,演示了如何自定义绘制属性。

程序第 70 行:

chart.setBackgroundPaint(new Color(207, 225, 235));

设置整个图表的背景颜色。只要调用 chart 的 setBackgroundPaint 方法就可以了。 setBackgroundPaint 方法可以接收 Color 对象,以及 GradientPaint 对象。GradientPaint 对象可

Page 358: Java Web动态图表编程

以让整个图表的背景实现渐进色效果。

程序第 73 行~第 77行:

CategoryPlot plot = chart.getCategoryPlot(); plot.setBackgroundPaint(new Color(246, 208, 146)); plot.setDomainGridlinePaint(Color.BLACK); plot.setDomainGridlinesVisible(true); plot.setRangeGridlinePaint(Color.RED);

要对直方图对象(包括其他风格的图表对象)的绘制属性进行更多的设置,首先需要获

得该图表对象的引用。对于直方图对象来说,我们需要获得一个 CategoryPlot 的实例。因此,

程序第 73 行,调用 chart 的 getCategoryPlot 方法,创建了 CategoryPlot 的实例——plot。程序

第 74 行,调用 plot 的 setBackgroundPaint 方法,设置图表绘图区域的背景色。同理,这里的

参数也可以是 Color 对象或者 GradientPaint 对象。程序第 75 行,调用 plot 的 setDomainGridlinePaint 方法,设置图表绘制区域网格线(这里指垂直方向上的黑色平行线)

的绘制颜色。程序第 76 行,调用 plot 的 setDomainGridlinesVisible方法,设置图表绘制区域

网格线是否显示,默认是不显示的。setDomainGridlinesVisible 方法接收一个布尔对象,true 表示显示网格线,false表示不显示。程序第 77 行,调用 plot 的 setRangeGridlinePaint 方法,

设置图表绘制区域网格线(这里指水平方向上的红色平行线)的绘制颜色。

程序第 80 行~第 81行:

BarRenderer renderer = (BarRenderer) plot.getRenderer(); renderer.setDrawBarOutline(true);

设置是否绘制直方图的轮廓,这需要获得 BarRenderer 类的一个实例。通过调用 plot 的 getRenderer 方法,就可以获得一个 BarRenderer 类的引用。Plot 的 getRenderer 方法实际上返

回的是一个 CategoryItemRenderer 对象,所以需要用“ (BarRenderer) ”将其强制转换为 BarRenderer 类型。

程序第 84 行~第 95行:

Color color[] = new Color[bookTitle.length]; color[0] = new Color(99,99,0); color[1] = new Color(255,169,66); color[2] = new Color(33,255, 66); color[3] = new Color(33,0,255); color[4] = new Color(255,0,66); for (int i = 0; i < color.length; i++)

GradientPaint gp = new GradientPaint(0, 0, color[i].brighter(),0, height, color[i].darker());

renderer.setSeriesPaint(i, gp);

这段代码,用于自定义直方图中每个直方块的绘制颜色。通过程序第 90 行~第 95行循

环,设置每个直方块的绘制颜色。这里通过调用 renderer 的 setSeriesPaint 方法来实现这一目

的。setSeriesPaint 方法接收两个参数,第 1 个参数是整型参数,表示直方块的索引(从 0 开

始),第 2 个参数是一个 java.awt.Paint 对象。

程序第 98 行~第 101行:

CategoryAxis domainAxis = plot.getDomainAxis(); domainAxis.setCategoryLabelPositions( CategoryLabelPositions.createUpRotationLabelPositions(Math.PI /

6.0) );

这段代码设置横轴上说明文字的旋转方向。这里需要创建一个 CategoryAxis 对象 ——

Page 359: Java Web动态图表编程

domainAxis,通过调用 plot 的 getDomainAxis 方法得到。然后调用 domainAxis 的 set CategoryLabelPositions 方法来设置文字旋转的角度。setCategoryLabelPositions 方法需要接收

一个 CategoryLabelPositions 类的实例为参数。 该参数是通过调用 CategoryLabel Positions 的静

态 createUpRotationLabelPositions 方法来创建的。CreateUpRotationLabel Positions 方法接收一

个双精度的参数, 返回一个 CategoryLabelPositions 类的对象。 CreateUpRotation LabelPositions 方法接收一个双精度的参数表示文字是逆时针方向旋转和双精度的参数所代表的角度(用弧

度表示)。

除此之外,CategoryLabelPositions 还提供了一个 createDownRotationLabelPositions 的静

态方法, 接收的参数及其含义与 createUpRotationLabelPositions 方法相同, 区别只是文字按顺

时针方向进行旋转。程序的其余部分和前例相同,我们就此略过。

4.具有标签的直方图表

itemLableBar.jsp(\chap08\Fig8.2\Fig.8.2_04)演示了如何为直方图中的每个直方块加上标

签,以及如何放置自定义图例的位置,如图 8.4所示。

图 8.4 JFreeChart实例 4:具有标签的直方图

图 8.4 所示的程序运行结果中,我们可以看到,图表的背景和直方图绘制区域的背景都

实现了渐进色的效果。 图例也从默认地显示位置 (下方居中), 而在左边居中的位置进行显示。

图表中的每个直方块的上方也显示出了标签(Label)。

下面简单讨论,程序是如何实现标签及定义图例的显示位置。

程序第 105行~第 110 行:

renderer.setLabelGenerator(new StandardCategoryLabelGenerator()); renderer.setItemLabelsVisible(true); ItemLabelPosition p = new ItemLabelPosition(

ItemLabelAnchor.OUTSIDE12, TextAnchor.CENTER_LEFT, TextAnchor.CENTER_LEFT, ­Math.PI / 2.0

);

本段代码实现了直方图标签的显示功能。 首先必须调用 plot 的 getRenderer 方法, 获得一

个 BarRenderer 类的引用——renderer (程序第 86 行)。 然后调用 renderer 的 setLabel Generator 方法设置标签。setLabelGenerator 方法需要接收一个实现了 CategoryLabel Generator 接口(位

Page 360: Java Web动态图表编程

于 org.jfree.chart.labels 包)的对象。因为 StandardCategoryLabelGenerator 实现了该接口,因

此 我 们 调 用 StandardCategoryLabelGenerator 类 的 默 认 构 造 器 来 创 建 一 个 新 的 StandardCategoryLabelGenerator类的实例,并将其作为参数提供给 setLabelGenerator方法。

默认的是不显示标签的,因此,程序第 106 行,调用 renderer 的 setItemLabeIsVisible 方

法,并提供布尔变量 true为其参数,指明要显示的标签。

程序第 107行~第 110 行, 创建 ItemLabelPosition类的实例——p, 表示标签的显示位置。

这里通过调用 ItemLabelPosition 类的构造器来创建 p。该构造器需要 4 个参数。第 1 个参数 ItemLabelAnchor 类的实例,ItemLabelAnchor 类(位于 org.jfree.chart.labels)提供 26 个静态

的 ItemLabelAnchor 对 象 。 除 了 这 里 使 用 的 ItemLabelAnchor.CENTER 外 , 还 有 ItemLabelAnchor.INSIDE1~ItemLabelAnchor.INSIDE12,以及 ItemLabelAnchor.OUT SIDE1~ ItemLabelAnchor.OUTSIDE12;第 2 个、第 3 个参数都是 TextAnchor 类(位于 jcommon包)

的对象,同样是直接调用 TextAnchor 类的静态对象——TextAnchor.CENTER _LEFT 作为参

数。TextAnchor 类一共提供了 15 个静态对象,请读者查阅该类的源代码;第 4 个参数是一

个双精度型的变量,表示标签的绘制方向。

程序第 112 行:

renderer.setPositiveItemLabelPositionFallback(p);

将创建好的 ItemLabelPosition 类的实例 p 作为参数传递给 renderer 的 setPositive Item LablePositionFallback方法,设置直方图的标签绘制位置等相关属性。

程序第 115 行~第 116 行:

CategoryAxis domainAxis = plot.getDomainAxis(); domainAxis.setCategoryLabelPositions(CategoryLabelPositions.UP_45);

用另外一种方法来设置横轴标题文字的绘制角度。这里我们将其需要的参数 Category LabelPositions 类的实例,通过直接调用 CategoryLabelPositions 的静态对象 Category LabelPositions.UP_45 而得到。CategoryLabelPositions 类提供了 5个静态对象,其他 4个静态

对 象 分 别 是 : CategoryLabelPositions.UP_90 、 CategoryLabelPositions.STAN DARD 、 CategoryLabelPositions.DOWN_45 和 CategoryLabelPositions.DOWN_90。 读者可以逐一测试这

些静态对象在最终运行结果中的区别。

程序第 119 行~第 120 行:

Legend legend = chart.getLegend(); legend.setAnchor(Legend.WEST);

设置图表的显示位置。首先必须创建一个图例 Legend 类的对象——legend。chart 的 getLegend方法返回了一个表示当前图表图例对象的引用。 获得了 Legend类的对象 legend后,

就可以调用 legend的 setAnchor 方法设置图例的显示位置。setAnchor 方法接收一个表示方位

的整型参数。Legend 类同样也提供了一些静态整型变量:Legend.WEST、Legend.NORTH、 Legend.EAST和 Legend.SOURTH,分别表示图例的显示位置在图表的左边、上边、右边和下

边。此外,还有其他一些静态整型变量,请读者自行查阅相关源代码。如果不希望图表显示

图例,则只需要将上述两行的代码改写为如下的形式:

chart.setLegend(null);

调用 chart 的 setLegend方法,并提供 null 的参数即可。

5.另一种风格的 3D 直方图表

j3Dbar.jsp(\chap08\Fig8.2\Fig.8.2_05)演示了另一种风格的 3D 直方图。运行效果如图 8.5 所示。

Page 361: Java Web动态图表编程

图 8.5 JFreeChart实例 5:另一种风格的 3D直方图

图 8.5 模拟了几种计算机书籍当月各周和上月同期销售量之间的增减变化量的图表。我

们从直方图纵轴上的度量值可以知道,参与图表绘制的某些数据的值小于零。图表中,以直

方图纵轴上的度量值为 0 的位置为基准点,绘制一条具有 3D 效果的基准线。如果参与绘制

的数据大于零,则在基准线上方进行绘制;反之,则在基准线下方进行绘制。本图表中,在

基准线上方的直方块表示当周某种书籍的销售量大于上月同期;反之则表示销售量低于上月

同期。

实现图 8.5 所示图表效果的核心部分,是生成的数据集对象中包含正数及负数。本段代

码需要注意两个地方。首先是程序第 28行~第 45 行:

public CategoryDataset createDataset()

DefaultCategoryDataset dataset = new DefaultCategoryDataset(); for (int i=0; i < bookTitle.length; i++)

int bookSales; for (int j=0; j < category.length; j++ )

double temp = Math.random(); if(temp > 0.5)

bookSales = 5 + (int) (temp * 10); else

bookSales = ­(5 + (int) (temp * 10)); dataset.addValue(bookSales, bookTitle[i], category

[j]);

return dataset;

本段代码用于创建 CategoryDataset 数据集对象。程序第 36 行~第 40 行,生成了−14~ 14 之间的双精度随机数,这样就保证了生成如图 8.5所示相同风格的图表。

其次是程序第 88 行~第 89 行:

final NumberAxis rangeAxis = (NumberAxis) plot.getRangeAxis(); rangeAxis.setStandardTickUnits(NumberAxis.createIntegerTickUnits());

这两句代码实现的功能是:图表纵轴上的度量值被强制以整数的形式显示。这也是固定

Page 362: Java Web动态图表编程

用法,读者只需要记住其用法就可以了。

6.基于 XYDataset 数据集的直方图表

除了前面介绍的 CategoryDataset 类的对象可以生成绘制直方图所需的数据外,任何实现

了 XYDataset 接口的数据集对象都可以作为绘制直方图所需的数据对象。XYDataset 接口的

数据集对象主要用来绘制与日期相关的图表。 XYBar.jsp(\chap08\Fig8.2\Fig.8.2_06)借用国家统计局发布的一组“城市居民消费价格指

数”的数据,将这些数据输入到数据集 IntervalXYDataset 类的对象中,最后用直方图来显示

这些数据。运行结果如图 8.6所示。

Page 363: Java Web动态图表编程

图 8.6 JfreeChart实例 6:基于 XYDataset数据集的直方图

图中相关年份并没有绘制相应的直方块,如年份 1979、1981、1982…1986、1987及 1988 等。因为数据集中并没有上述年份“城市居民消费价格指数”的数据,因此这些年份为空白。

首先注意程序第 6 行~第 25 行,我们新引入的 JFreeChart 的相关包。

程序第 84 行:

IntervalXYDataset dataset = createDataset();

调用程序第 33 行~第 61 行的 creatDataset 方法, 创建了一个 IntervalXYDataset 对象—— dataset。前面说过,IntervalXYDataset 实现了 XYDataset 接口。

程序第 35 行:

TimeSeries t1 = new TimeSeries(chartTitle, xTitle, yTitle, Year.class);

创建了一个 TimeSeries 类(位于 org.jfree.data.time包)的实例——t1。TimeSeries 提供 3 个构造器,这里应用的是其最复杂的一个构造器。该版本的构造器接收 4个参数。前面 3 个

参数都是字符串变量,最后一个是 java.lang.Class 类型的对象,可以是 Day.class、Fixed Millisecond.class、Hour.class、Millisecond.class、Minute.class、Month.class、Quarter.class、 Second.class、Week.class,以及 Year.class,这些 class都是由 JFreeChart 提供的。注意,该构

造器中的第 2 个及第 3 个参数,可以为 null。

然后调用 t1 的 add方法, 将我们从中国国家统计局网站上查询到 1978—2002 年之间 “城

市居民消费价格指数”的数据(http://www.stats.gov.cn/tjsj/ndsj/yearbook2003_c.pdf)添加到 t1 中。t1 的 add 方法也有不同的版本,本例应用 add 方法接收两个参数。第 1 个参数是 RegularTimePeriod类的对象,而前面所介绍的 Day.class、FixedMillisecond.class、Hour.class、 Millisecond.class、Minute.class、Month.class、Quarter.class、Second.class、Week.class,以及 Year.class 都是 RegularTimePeriod类的子类。第 2 个参数是双精度类型的 double变量。因为 t1的 add方法在执行过程中可能发生异常, 因此必须将位于程序第 37行~第 53行之间的 add 方法,放置在 try…catch代码段中。

程序第 58 行:

TimeSeriesCollection tsc = new TimeSeriesCollection(t1);

创建了一个 TimeSeriesCollection 类(位于 org.jfree.data.time 包)的对象。TimeSeries

JFreeChart实例6:基于XYDataset数据集的直方图

Page 364: Java Web动态图表编程

Collection 类实现了 IntervalXYDataset 接口。这里调用的 TimeSeriesCollection 类的构造器需

要接收一个 TimeSeries 类的实例为参数。因此,我们把 t1 作为参数传递给该构造器,并创建

了 TimeSeriesCollection类的对象 tsc。

程序第 59 行:

tsc.setDomainIsPointsInTime(false);

调用 tsc 的 setDomainIsPointsInTime 方法,设置是否将横轴的说明文字按时间格式进行

处理。本方法接收一个布尔变量作为参数,true表示按时间格式或时期格式进行处理,反之,

则不进行上述处理。程序在第 60 行,返回了包含数据的 tsc。

程序第 85 行:

JFreeChart chart = createChart(dataset);

调用程序第 63 行~第 78 行的 creatChart 方法,创建了一个 JFreeChart 对象 chart。注意,

程序第 65 行:

JFreeChart chart = ChartFactory.createXYBarChart

调用 ChartFactory的是 createXYBarChart方法, 而不是前面我们讨论的 createBarChart 方

法或 createBarChart3D方法。createXYBarChart 方法与 createBarChart 方法大同小异,我们在

此略过。

程序第 95 行~第 96行:

chart.addSubtitle(new TextTitle("资料来源: 中国国家统计局 " +" http: //www.stats.gov.cn/tjsj/ndsj/yearbook2003_c.pdf"));

调用 chart 的 addSubtitle 方法,在图表上增加了一个子标题。注意,不能为该方法直接

提供一个字符串对象,而必须提供一个 Title 类的实例。TextTitle 是 Title 类的子类,因此可

以直接将其作为参数传递给 addSubtitle方法。

程序第 102行~第 105 行:

DateAxis axis = (DateAxis) plot.getDomainAxis(); axis.setTickMarkPosition(DateTickMarkPosition.MIDDLE); axis.setLowerMargin(0.01); axis.setUpperMargin(0.01);

创建 DateAxis 类(位于 org.jfree.chart.axis 包中)的对象 axis。通过 plot 对象(程序第 98 行创建)的 getDomainAxis 方法所创建。DataAxis 对象提供了一些方法用来定义直方图中直

方块的绘制属性。axis 的 setTickMarkPosition 方法用于设置横轴上的核对符号(tick mark)

的显示位置。该核对符号就是图 8.6 中横轴下方的指示年份位置的、一些非常短的垂直直线。

默认的核对符号位置是左对齐。参数 DateTickMarkPosition.MIDDLE 表示核对符号在居中显

示。关于 axis 的 setLowerMargin方法以及 setUpperMargin方法,用于设置坐标轴两端和图表

之间空白区域的大小,请读者修改参数的值,不妨多改变几次,然后再观察改变后的运行结

果。后续的讲解例程中,也多次使用了这两个方法。

至此,利用 JFreeChart 在 JSP 环境下生成直方图表就介绍完了。通过本节的内容,读者

应该对 JFreeChart 的整体运行机制有一个基本的认识。

8.2.4 JFreeChart 生成饼型图表

1.普通饼型图表

首先,我们为读者提供一个最基本的默认的饼图。jPie.jsp(\chap08\Fig8.2\Fig.8.2_07)演示

Page 365: Java Web动态图表编程

了如何绘制默认的饼图。运行效果如图 8.7 所示。

图 8.7 JFreeChart生成普通饼图

生成默认的饼图的主要步骤如下:

(1)生成绘制饼图所需的数据集对象,该数据集对象是 PieDataset 的一个实例;

(2)创建一个表示饼图的 JFreeChart 对象。这里的 JFreeChart 对象是由程序第 31 行,

调用 ChartFactory类的 createPieChart 方法所创建的。

这里需要提醒读者的是程序第 24 行:

dataset.setValue(bookTitle[i], (int)(10 + Math.random()* 40));

为饼图的数据集对象 dataset 添加数据的方法是 setValue,而不是 addValue。

2.自定义普通饼型图表

jCustomPie.jsp(\chap08\Fig8.2\Fig.8.2_08)向读者演示了如何绘制自定义的饼图。在本例

中,示例了如何设置图表标题的属性,如字体及颜色;何定义饼图中各个组成部分(圆弧)

的绘制颜色;何指定饼图中的某段圆弧(或者所有的圆弧)从其他圆弧中分离出来单独显示;

何设置反锯齿功能;何设置饼图的外形(椭圆或者正圆)等。其他设置饼图的相关绘制属性

的方法,我们将在后续的例程中向读者介绍。 jCustomPie.jsp运行结果如图 8.8 所示。

Page 366: Java Web动态图表编程

图 8.8 JFreeChart自定义的普通饼图效果

程序第 60 行~第 62行:

TextTitle title = chart.getTitle(); title.setFont(new Font("汉真广标", Font.BOLD, 25)); title.setPaint(Color.YELLOW);

本段代码用于设置整个图表的标题及字体,需要通过调用 chart 的 getTitle方法或者表示

图表标题的对象引用,然后再调用我们熟悉的 setFont 及 setPaint 方法就可以了。如果需要改

变图表的内容,则调用 setText 方法,并提供一个字符串对象作为该方法的参数即可。这段代

码适用于所有类型的图表。

程序第 65 行:

PiePlot plot = (PiePlot) chart.getPlot();

获得当前饼图对象的引用 plot。注意:调用 chart 的 getPlot 方法后,我们用“ (PiePlot) ”

来强制将当前图表对象的引用转换为 PiePlot 类型。

程序第 77 行~第 84行:

for (int i = 0; i < color.length; i++)

plot.setSectionPaint(i, color[i]);

// 分离显示饼图中表示"JAVA"的那部分圆弧 if (bookTitle[i].equals("JAVA"))

plot.setExplodePercent(i, 0.15);

在本段循环中,需要注意的是,给组成饼图的各段圆弧进行颜色设置的方法是 set SectionPaint。该方法接收两个参数,第 1个参数是整型参数,表示颜色的圆弧的索引值(也

是从 0 开始);第 2 个参数是 java.awt.Paint 对象。

在图 8.8 所示的运行结果中,我们可以看到,饼图中表示“JAVA”的那一段圆弧从其他

圆弧中分离出来,单独显示。这个功能由程序第 82行~第 83 行的代码所完成。首先判断当

前的圆弧是否代表“JAVA” ,如果结果为真,则调用 plot 的 setExplodePercent 方法,将当前

Page 367: Java Web动态图表编程

圆弧的绘制位置从其他圆弧中分离出来。setExplodePercent 方法需要接收两个参数。第 1 个

参数是整型变量,表示被分离圆弧的索引值;第 2 个参数是一个双精度变量,它表示的是一

个百分比值,如本例中的 0.15,即表示 15%。该值不仅影响被分离圆弧和其他圆弧之间的距

离,也影响了整个饼图最终外观的大小。请读者修改该值,进行几次测试就明白了。

在默认情况下,所有图表的绘制属性中的反锯齿功能都是打开的。它影响了整个图表的

绘制效果。如果打开反锯齿功能,图表中的标题或一些标签文字反而会失去清晰度。因此,

在一些强调标题或者标签文字的情况下,我们需要关闭反锯齿功能。如程序第 87 行所示:

//chart.setAntiAlias(false);

调用 chart 的 setAntiAlias 方法即可,参数为“true”表示打开反锯齿功能;参数为“false”

表示关闭反锯齿功能。该方法对所有类型的图表对象都适用。

程序第 90 行:

plot.setLabelFont(new Font("Courier New", Font.PLAIN, 12));

用来设置饼图中的各段圆弧说明文字的字体。

程序第 93 行:

plot.setCircular(false);

用来设置饼图外观形状,调用 plot 的 setCircular 方法即可。如果提供的参数为“true” ,

则表示饼图的外观为正圆形,反之,则为椭圆形。默认是正圆形。

3.实心 3D 饼型图表

jPie3D.jsp(\chap08\Fig8.2\Fig.8.2_09)向读者演示了如何绘制实心 3D 饼图。jPie3D.jsp 的

运行结果如图 8.9 所示。

图 8.9 JFreeChart 实心 3D饼图

创建 3D 饼图的关键是程序第 35 行:

JFreeChart chart = ChartFactory.createPieChart3D

调用 ChartFactory的 createPieChart3D 方法。

Page 368: Java Web动态图表编程

程序第 64 行:

PiePlot plot = (PiePlot3D) chart.getPlot();

注意,调用 chart 的 getPlot 方法后,我们使用“ (PiePlot3D) ”来强制将当前图表对象的

引用转换为 PiePlot3D 类型。

程序第 70 行:

plot.setStartAngle(60);

调用 plot 的 setStartAngle方法,设置饼图中,绘制第 1 段圆弧的起始角度。

程序第 73 行:

plot.setDirection(Rotation.ANTICLOCKWISE);

调用 plot 的 setDirection方法, 设置饼图中各段圆弧的绘制方向。 默认是按顺时针方向绘

制各段圆弧(Rotaion.CLOCKWISE),这里我们提供的参数是 Rotation. ANTICLOCKWISE,

表示按逆时针方向绘制饼图中的各段圆弧。

程序第 76 行~第 77行:

plot.setSectionOutlinePaint(new Color(26, 79, 169)); plot.setSectionOutlineStroke(new BasicStroke(1.5f));

设置绘制饼图轮廓线的相关属性。setSectionOutlinePaint 方法,用来设置饼图轮廓线的绘

制颜色。该方法接收一个 java.awt.Paint 类的实例为参数。setSectionOutlineStroke方法,用来

设置饼图轮廓线的笔划属性。该方法接收一个 java.awt.Stroke 类的实例为参数,我们在这里

设置饼图轮廓线的宽度为 1.5像素宽。

4.透明 3D 饼型图表

jTransparentPie3D.jsp (\chap08\Fig8.2\Fig.8.2_10)向读者演示了如何绘制具有透明效果的 3D 饼图。jPie3D.jsp的运行结果如图 8.10所示。

图 8.10 JFreeChart透明 3D饼图 jTransparentPie3D.jsp是在前例的基础上新增加了两个属性,实现了透明效果。

程序第 82 行~第 86行:

// 设置前景透明度

Page 369: Java Web动态图表编程

plot.setForegroundAlpha(0.4f);

// 设置背景透明度 plot.setBackgroundAlpha(0.7f);

plot的 setForegroundAlpha方法, 用于设置前景 (即饼图) 的透明度, 而 setBackgroundAlpha 方法,用于设置绘图区域背景部分的透明度。对于实现饼图的透明效果,只需要调用 setForegroundAlpha 方法即可。这两个方法都需要一个 float 类的变量为参数,表示图形对象

的 Alpha 复合值,取值范围在 0.0~1.0 之间。

8.2.5 JFreeChart 生成线段图表

本节讲述如何利用 JFreeChart 来生成线段图表。线段图表包括直线图表和曲线图表。用

于创建线段图表的数据集可以是实现了 CategoryDataset接口或者是XYDataset接口的数据集

的实例。

1.自定义线段图表

jLine.jsp(\chap08\Fig8.2\Fig.8.2_11)向读者演示了如何绘制自定义的线段图。运行效果如

图 8.11 所示。

图 8.11 JFreeChart自定义线段图

从图 8.11 中可以看出,我们在线段图上又进行了一些新的绘制属性的设置,如下所示:

Ø 改变默认的横轴和纵轴的标题字体的风格;

Ø 改变默认的图例显示风格;

Ø 自定义线段的绘制颜色及笔划的风格(虚线、实线、粗细等)。

我们来讨论上述绘制属性是如何设置的。我们在前面说过,绘制线段图所需要的数据集

可以是实现了 CategoryDataset 接口或者是 XYDataset 接口的数据集的实例。因此,线段图的

数据集对象的创建与直方图的数据集对象的创建是完全相同。因为绘制的是线段图,因此程

序第 46 行:

JFreeChart chart = ChartFactory.createLineChart

调用 ChartFactory类的 createLineChart 方法。

程序第 87 行~第 88行:

Page 370: Java Web动态图表编程

CategoryAxis domainAxis = plot.getDomainAxis(); domainAxis.setLabelFont(new Font("黑体", Font.BOLD, 15));

这两行代码首先获得图表中横轴对象的引用 domainAxis,表示横轴对象的是 Category Axis 类(位于 org.jfree.chart.axis 包)。之后调用 domainAxis 的 setLableFont 方法,对横轴的

标题字体属性进行设置。setLableFont 方法接收一个 java.awt.Font 类的对象。

程序第 91 行~第 93行:

ValueAxis rangeAxis = plot.getRangeAxis(); rangeAxis.setLabelFont(new Font("黑体", Font.BOLD, 15)); rangeAxis.setLabelAngle(Math.PI/2);

这三行代码用来设置纵轴标题的字体及其绘制方向。首先获得图表中纵轴对象的引用 rangeAxis。 表示纵轴对象的是 ValueAxis类 (位于 org.jfree.chart.axis包)。 之后调用 range Axis 的 setLableFont 方法,对纵轴的标题字体属性进行设置,该 setLableFont 方法也同样接收一个 java.awt.Font 类的对象。然后调用 rangeAxis 的 setLabelAngle 方法,设置纵轴标题的绘制角

度。默认的绘制角度是垂直绘制(即 90度),如果希望纵轴标题的绘制角度为水平方向(即 180 度),则需要将一个表示角度的双精度浮点数的变量提供给 setLabelAngle 方法即可,因

此这里我们提供的参数值为 Math.PI/2。

提示: 如果图形对象是XYPlot类型, 则其横轴对象不是CatagoryPlot类型, 而是ValueAxis 类型(纵轴方法不变)。在此情形下,获取横轴对象的方法是:

XYPlot plot = (XYPlot) chart.getPlot(); ValueAxis domainAxis = plot.getDomainAxis();

程序第 96 行~第 100行:

StandardLegend legend = (StandardLegend) chart.getLegend(); legend.setDisplaySeriesShapes(true); legend.setShapeScaleX(1.5); legend.setShapeScaleY(1.5); legend.setDisplaySeriesLines(true);

本段代码用来设置图例的显示属性。首先需要创建一个标准图例(StandardLegend)类

的对象 legend,该类隶属于 org.jfree.chart 包。调用 chart 的 getLegend方法获得当前图例对象

的引用,然后利用“ (StandardLegend) ”将其强制转换为 StandardLegend类型的对象。 legend的 setDisplaySeriesShapes 方法设定图例中,是否以一系列不同形状的小图标来表

示不同种类的数据(标准图例中,用小方块来表示不同种类的数据)。该方法接收一个布尔变

量,默认为不显示(false)。这里我们提供的参数为 true,因此最终的绘制结果中显示一系列

不同形状的小图标。 legend的 setShapeScalesX 及 setShapeScaleY 方法,用来设定一系列不同形状的小图标的

外观大小。两个方法都接收一个表示比例因子的双精度变量。该值如果等于 1,表示最终绘

制的小图标的外观大小和其原始尺寸相同;如果该值小于 1,则表示最终绘制的小图标按其

原始尺寸,根据给定的比例因子进行缩小;如果该值大于 1,则表示最终绘制的小图标按其

原始尺寸,根据给定的比例因子进行扩大。这里我们提供给两个方法的参数值都为 1.5,表示

小图标的外观大小是其默认大小 1.5 倍(即宽度和高度同时扩大 1.5 倍)。 legend的 setDisplaySeriesLines 方法,表示是否在图例中显示各种数据所对应的线型。该

方法同样接收一个布尔变量。默认为不显示(false),这里我们提供的参数为 true,因此最终

的绘制结果中在一系列不同形状的小图标的两边显示相对应的线型。

程序第 103行:

LineAndShapeRenderer renderer = (LineAndShapeRenderer) plot.get

Page 371: Java Web动态图表编程

Renderer();

获得表示线段图表的渲染对象(LineAndShapeRenderer类,位于 org.jfree.chart.renderer包)

的引用 renderer。

程序第 106 行~第 115 行,用于自定义线段的绘制颜色。注意,调用的方法是 renderer 的 setSeriesPaint 方法。

程序第 118 行~第 128 行:

BasicStroke bs ; for (int i = 0; i < bookTitle.length; i++)

float dashes[] = 10.0f; bs = new BasicStroke(2.0f, BasicStroke.CAP_ROUND,BasicStroke.JOIN_

ROUND, 10.f, dashes, 0.0f); if (i % 2 != 0)

renderer.setSeriesStroke(i, bs); else

renderer.setSeriesStroke(i, new BasicStroke(2.0f));

本段代码用来设置线段图表中,各线段的笔划属性。默认的绘制笔划属性是 1 像素宽的

实心直线。通过调用 renderer 的 setSeriesStroke方法,就可以设置任意一段直线的笔划属性。

该方法接收两个参数,第 1 个参数是整型变量,指明笔划所表示的数据对象的索引值;第 2 个参数是 java.awt.Stroke对象。

至此,本例所涉及的线段图表的绘制属性就介绍完了。

2.曲线图表

本例我们用 XPDataset 的数据集来绘制一个简单的曲线图表——正弦图表。

先看 jWave.jsp (\chap08\Fig8.2\Fig.8.2_12)的运行结果,如图 8.12所示。

需要提醒读者的是程序第 21 行~第 30行,所创建 XYDataset 的方法。

public XYDataset createDataset()

XYSeries series = new XYSeries(chartTitle); for (int i = 0; i <= 20; i++)

series.add(i, Math.sin((i / Math.PI))); XYSeriesCollection dataset = new XYSeriesCollection(series); return dataset;

Page 372: Java Web动态图表编程

图 8.12 JFreeChart曲线图

本方法实际返回的是一个 XYSeriesCollection 类(位于 org.jfree.data 包,该类实现了 XYDataset 接口)的对象。通过调用该类的构造器,并提供了一个 XYSeries 类的对象实例为

其参数,创建了 XYSeriesCollection 类的实例 dataset。XYSeries 类(位于 org.jfree.data.包)

主要用于为 XYDataset 类的对象提供数据服务。该类提供了许多 add 方法,用于添加数据。

本例使用的 add方法需要两个 double类型的参数。其他版本的 add方法,请读者自行阅读该

类的 Javadoc文档。

程序的其余部分没有新的内容,此处不再赘述。

8.2.6 JFreeChart 生成区域图表

1.普通区域图表

本例我们用 CategoryDataset 的数据集来绘制一个区域(Area)图表。

先看 jArea.jsp(\chap08\Fig8.2\Fig.8.2_13)的运行结果,如图 8.13 所示。

首先需要注意的是程序第 49 行,创建区域图表所调用的方法:

JFreeChart chart = ChartFactory.createAreaChart

创建区域图表是通过 ChartFactory 的 createAreaChart 方法。区域图所需要的数据集对象

与直方图和线段图是相同的,可以是 CategoryDataset对象,也可以是 XYDataset对象。

图 8.13 JFreeChart区域图

程序第 79 行:

plot.setForegroundAlpha(0.5f);

设置区域图中各个区域图形的透明度。默认区域图中各个区域图形的透明度为 1,即完

全不透明。

本例介绍两个新的绘制属性。一个是关于如何设置图表绘制边界,另一个是关于如何定

义坐标轴上的度量值(刻度值)数字的显示格式。

程序第 87 行~第 90行:

Page 373: Java Web动态图表编程

CategoryAxis domainAxis = plot.getDomainAxis(); domainAxis.setCategoryLabelPositions(CategoryLabelPositions.UP_45); domainAxis.setLowerMargin(0.0); domainAxis.setUpperMargin(0.0);

本段代码用于设置图表中的绘制边界。 图 8.13 所示的区域图边界和图表对象的绘制区域

边界是一样的(图表对象的绘制区域就是图 8.13 中的灰色矩形区域)。默认的区域图绘制边

界在图表对象的绘制区域的左右两边各留有一定的空白。程序第 89 行调用 domain Axis 的 setLowerMargin 方法,重新设置区域图的绘制左边界与图表对象的绘制区域左边界之间的空

白。该方法接收一个双精度浮点数变量为参数,表示空白与图表对象的绘制区域宽度之间的

比例因子。 如果该值为 0.3, 则表示区域图的绘制左边界与图表对象的绘制区域左边界之间的

空白长度为图表对象的绘制区域宽度的 30%。本例提供的参数值为 0.0,表示两者之间没有

空白。

同理,domainAxis 的 setUpperMargin方法用于重新设置区域图的绘制右边界与图表对象

的绘制区域右边界之间的空白。

程序第 93 行~第 96行:

NumberAxis rangeAxis = (NumberAxis)plot.getRangeAxis(); DecimalFormat df = new DecimalFormat("###0.00%"); NumberTickUnit ntu = new NumberTickUnit(0.1, df); rangeAxis.setTickUnit(ntu) ;

如何定义坐标轴上的度量值(刻度值)数字的显示格式。这里我们以纵轴为例。首先程

序第 93 行,获得用于表示图表纵轴对象的引用 rangeAxis。注意,前面讲述过 plot 的 getRangeAxis 方法返回的是 ValueAxis 类的对象。本例为了自定义纵轴上的度量值数字的显

示格式,因此将 getRangeAxis 方法返回的是 ValueAxis 类的对象强制转换成了 Number Axis 类的类型。NumberAxis 是 ValueAxis 的两个子类之一,另外一个是 DateAxis,两者都位于 org.jfree.chart.axis 包。NumberAxis 类主要用来自定义坐标轴上的度量值数字的显示格式,而 DateAxis 主要用于自定义坐标轴上的度量值,为日期或时间类的文字时的显示格式。下一节

的例程,我们将讲述如何自定义时间的显示格式。

程序第 94行, 创建了一个DecimalFormat类的对象 df, 提供的格式化范例为 “### 0.00%” 。

表示小数点前保留 3个数字,如果小数点前没有数字,则用一个 0 补上,小数点后保留两个 0,最后用百分号结尾。

程序第 95 行,创建了 NumberTickUnit 类的对象 ntu。NumberTickUnit 类位于 org. jfree.chart.axis 包,用来表示纵轴上两个相邻的刻度值(tick)之间的间隔值,以及刻度值的

显示格式。NumberTickUnit 类的构造器接收两个参数:第 1个参数是双精度浮点数,表示间

隔值;第 2 个参数是 java.text.NumberFormat 类的对象。本例提供的第 1 个参数值为 0.1,表

示刻度值以 0.1 的间隔依次递增(本例数据集中数据的取值范围是 0.3~1.3 之间,见程序第 39 行)。第 2 个参数就是 df。DecimalFormat 类是 NumberFormat 类的子类,因此我们可以直

接提供 DecimalFormat 类的对象给 NumberTickUnit 类的构造器。

程序第 96 行,调用 NumberAxis 类的 setTickUnit 方法,并将创建好的 NumberTickUnit 类的对象 ntu 作为参数传递给该方法,实现了按照我们定义的各种属性,来绘制纵轴上刻度

值的相关内容。

按照我们设置的属性,本例最后的绘制结果如图 8.13 所示。读者可以修改程序第 94 行

以及第 95 行的语句,再观察修改后的运行结果。

2.基于 XYDataset 数据集的区域图表

本例我们用 XYDataset 的数据集来绘制区域图表。先看 jXYArea.jsp(\chap08\Fig8.2\ Fig.8.2_14)的运行结果,如图 8.14所示。

Page 374: Java Web动态图表编程

图 8.14 JFreeChart基于 XYDataset的区域图

首先程序第 28 行~第 42 行,创建 XYDataset 数据集对象。

public XYDataset createDataset()

TimeSeries tsc = new TimeSeries(chartTitle); double value = 0.0; Day day = new Day(); for (int i = 0; i < 200; i++)

value += Math.random() ­ 0.5; tsc.add(day, value); day = (Day) day.next();

TimeSeriesCollection dataset = new TimeSeriesCollection(tsc); return dataset;

这里我们应用 JFreeChart 自带的 DAY 类(位于 org.jfree.date.free包),DAY 类非常类似 java.util.Calendar 类。程序第 32 行,创建一个 DAY 的实例 day。DAY 的默认构造器,创建了

一个表示当前日期的 DAY 类的实例。

程序第 37 行,调用 day的 next 方法,表示第 day+1 天。

程序第 47 行,创建基于 XYDataset 的区域图表对象 chart:

JFreeChart chart = ChartFactory.createXYAreaChart

注意:调用的方法是 createXYAreaChart。

程序第 76 行:

XYPlot plot = chart.getXYPlot();

通过 chart 的 getXYPlot 方法,获得当前区域图表对象的引用 plot。

程序第 79 行:

DateAxis domainAxis = new DateAxis("统计时段");

创建了一个 DataAxis 的实例 domainAxis。调用的是 DateAxis 的构造器,并提供了一个

内容为“统计时段”的字符串作为该构造器的参数。该字符串参数表示 domainAxis 的标题文

Page 375: Java Web动态图表编程

字。

程序第 80 行:

DateFormat df = new SimpleDateFormat("yyyy­MM");

创建了一个 java.text.DateFormat 类的实例 df,用于设置格式化日期的显示格式。参数中

的“yyyy­MM”表示日期的现实格式为:年份用 4 位数字表示,月份用两位数字表示,如果

月份为 1 位数,则为 0 补足位数。

程序第 81 行:

DateTickUnit unit = new DateTickUnit(DateTickUnit.DAY, 30, df);

创建了一个 DateTickUnit 类的实例 unit。该构造器接收 3 个参数:第 1 个参数是整型变

量,我们调用的是 DateTickUnit 的静态变量 DateTickUnit.DAY 表示日期;第 2 个参数也是整

型变量,表示坐标轴上两个相邻的日期刻度值(tick)之间的间隔,这里参数值为 30,表示

日期刻度值以月为单位,依次递增;第 3 个参数是 java.text.DateFormat 类的对象,这里我们

使用了刚创建的 df。df 实际上是 SimpleDateFormat 类的对象,而 SimpleDateFormat 类正是 DateFormat 类的子类,因此可以直接使用。

程序第 82 行:

domainAxis.setTickUnit(unit);

调用 domainAxis 的 setTickUnit 方法,把我们定义坐标轴的各种属性应用到当前的横轴

对象上。注意,此时横轴的标题文字已经从“统计时间” (程序第 50行)更改为“统计时段”

(程序第 79行)。

程序第 88 行:

plot.setDomainAxis(domainAxis);

调用 plot 的 setDomainAxis 方法,接收自定义的横轴对象 domainAxis。实现了在当前图

表中,按照我们定义的各种属性,来绘制横轴上刻度值的相关内容。

注意,程序第 85 行~第 86 行:

//domainAxis.setLowerMargin(0.0); //domainAxis.setUpperMargin(0.0);

我们注释了这两行代码,因此绘制结果显示区域图,在图表对象绘制区域的左右两边各

留出了默认宽度的空白。

程序的其余部分读者都很熟悉了,我们在此略过。

8.2.7 JFreeChart 生成时序(Time Series)图表

本节向读者介绍时序图表(Time Series)的绘制。时序图表主要用于绘制数据与日期、

时间联系非常紧密的图表。常用于金融和财经类的图表绘制,如股市的走势图、价格的走势

图、货币变动趋势图等等。时序图表和线段图表的绘制十分相似,不同之处有两点,一是时

序图的横坐标几乎只用于显示日期、时间的相关数据;二是时序图表只接收 XYDataset 类的

数据集对象,而不能使用 CategoryDataset 类的数据集对象。此外,时序图表还提供了一些独

特的功能,如绘制移动平均线等。

下面的例程中,演示了绘制时序图表所需的方法和技巧。

1.时序图表

jTimeSeries.jsp(\chap08\Fig8.2\Fig.8.2_15)演示了如何绘制时序图表。运行结果如图 8.15

Page 376: Java Web动态图表编程

所示。

图表的运行结果有以下两个特点:

(1)图例不仅使用了不同形状的小图标来表示不同种类的数据,而且小图标的外观有

了空心及实心的变化;

Page 377: Java Web动态图表编程

图 8.15 JFreeChart时序图

(2)在表示时间的横轴上,时间刻度值被垂直绘制。

本例中,数据集对象的生成虽然也是 XYDataset 对象,但是和以前的用法有所不同,请

看程序第 29行~第 46 行之间的 createDataset 方法。

public XYDataset createDataset()

TimeSeriesCollection dataset = new TimeSeriesCollection(); TimeSeries ts[] = new TimeSeries[bookTitle.length]; int bookSales = 0; for (int i =0; i< bookTitle.length; i++)

ts[i] = new TimeSeries(bookTitle[i], Month.class); for (int month = 1; month <= 12; month++)

bookSales = 70 + (int)(Math.random() * 100); ts[i].add(new Month(month, 2004), bookSales);

dataset.addSeries(ts[i]);

return dataset;

程序第 32 行,创建了一个 TimeSeries 对象的数组。

TimeSeries ts[] = new TimeSeries[bookTitle.length];

前面的例程中, 我们都只是将一个 TimeSeries 类的对象添加到 TimeSeriesCollection类的

对象中。那么如何把多个 TimeSeries 的对象添加到 TimeSeriesCollection类的对象中呢?首先

在程序第 31 行,调用 TimeSeriesCollection 类的默认构造器,创建一个数据为空的 TimeSeriesCollection 类对象 dataset。然后在循环中,程序第 42 行调用 addSeries 方法,并提

供一个 TimeSeriesCollection 类的对象为其参数,就可以添加多个 TimeSeriesCollection 类的对

象到 dataset 中了。

程序第 51 行:

JFreeChart chart = ChartFactory.createTimeSeriesChart

调用 ChartFactory 的 createTimeSeriesChart 方法,创建一个时序图表对象 chart。注意,

该方法接收的参数中,除了数据集对象必须为 XYDataset 外,并不接收设置图表绘制方向的

参数,如 PlotOrientation.VERTICAL或 PlotOrientation.HORIZONTAL之类的 参数。

Page 378: Java Web动态图表编程

这说明,不能像使用直方图和线段图那样,可以自定义图表的绘制方向。因此,对于时

序图表来说,横轴永远只能用来表示日期或者时间,它和纵轴的位置不能互相交换。

程序第 82 行~第 83行:

StandardLegend sl = (StandardLegend) chart.getLegend(); sl.setDisplaySeriesShapes(true);

设置图例的显示属性。 定义图例中显示一系列不同形状的小图标来表示不同种类的数据。

程序第 86 行~第 91行之间的代码,用来设置横轴上日期刻度的显示属性。需要注意的

是程序第 88行:

DateTickUnit unit = new DateTickUnit(DateTickUnit.MONTH, 1, df);

这里的参数表示横轴上的刻度为月份,月份依次递增。月份的显示格式由程序第 87 行,

创建的 DataFormat 对象 df所决定。

程序第 90 行:

domainAxis.setVerticalTickLabels(true);

调用 DateAxis 类的对象 domainAxis 的 setVerticalTickLables 方法,用来设置横轴上刻度

文字的绘制方向。该方向接收一个布尔变量,true 表示文字的绘制方向为垂直方向,false 表

示文字的绘制方向为水平方向。默认的横轴上刻度文字的绘制方向为水平方向。

程序第 93 行~第 94行:

StandardXYItemRenderer renderer = (StandardXYItem Renderer) plot.get Renderer();

如果要自定义时序图的绘制属性,则必须获得一个 StandardXYItemRenderer 类的对象引

用。通过 plot 的 getRenderer 方法,并将该方法返回结果强制转换为 StandardXYItem Renderer 类的类型。

程序第 95 行:

renderer.setPlotShapes(true);

这句代码表示,在时序图中数据的节点上,是否绘制图例中与之对应的小图标(图 8.15 中所示的线段与线段之间的小图标)。参数 true表示,绘制小图标,反之,则表示不绘制。

程序第 96 行~第 97行:

renderer.setSeriesShapesFilled(0, Boolean.TRUE); renderer.setSeriesShapesFilled(1, Boolean.FALSE);

设置小图标的外观。StandardXYItemRenderer 类的 setSeriesShapesFilled用来设置数据节

点上的小图标的外观形状是实心(true)的还是空心(false)的?它接收两个参数:第 1 个参

数是整型参数,表示小图标所代表的数据对象的索引值;第 2 个参数是 Boolean 类的静态变

量 TRUE 和 FALSE,用于设置小图标的外观。因此,我们在图 8.15 所示的最终结果中可以

看到,表示书籍“Python”销售量的红色线段中的小图标为实心矩形,而表示书籍“JAVA”

销售量的蓝色线段中的小图标为空心圆圈。

此外,我们也可以将程序第 96行~第 97 行改写成如下的内容:

renderer.setSeriesShapesFilled(0, true); renderer.setSeriesShapesFilled(1, false);

注意,在这种情况下,我们用 true和 false来代替 Boolean类的静态变量 TRUE和 FALSE,

在 Tomcat 的某些版本下,会提示出错,而 Resin则不会出现任何错误。

如果要设置所有图标的绘制外观,我们可以采用如下的方法:

Page 379: Java Web动态图表编程

XYItemRenderer renderer = plot.getRenderer(); if (renderer instanceof StandardXYItemRenderer)

StandardXYItemRenderer rr = (StandardXYItemRenderer) renderer; rr.setPlotShapes(true); rr.setShapesFilled(true);

2.走势图表及移动平均线的绘制

jMovingAverage.jsp(\chap08\Fig8.2\Fig.8.2_16)演示了如何绘制移动平均线。先看图 8.16 所示 jMovingAverage.jsp的运行效果。

图 8.16 中的红色曲线, 模拟了 2004年国际原油价格的走势曲线。 蓝色曲线, 是表示 2004 年国际原油 30 天价格的移动平均线。纵轴的刻度文字以美元符号($)开头,表示原油的价

格。横轴表示时间,刻度值从 2004 年 1月,并每间隔两个月显示一次。

移动平均线在金融、财经类的图表中使用非常广泛。JFreeChart 的时序图,提供了绘制

移动平均线的功能。它可以自动计算给定数据对象的任意时期的移动平均数据,并绘制相对

应的曲线。因此,我们首先必须了解移动平均数的计算方式。假设我们具有表 8.3 所示的一

些数据,让我们来看如何计算移动平均数。

Page 380: Java Web动态图表编程

图 8.16 JFreeChart走势图和移动平均线

表 8.3 国际原油价格表

价格(单位:美元/桶) 时 间

每日价格 3 天移动平均价格 5 天移动平均价格

2004 年 1 月 1 日 35.2957 - -

2004 年 1 月 2 日 34.8745 - -

2004 年 1 月 3 日 32.5929 34.2544 -

2004 年 1 月 4 日 33.6428 33.7034 -

2004 年 1 月 5 日 35.7017 33.9791 34.4215

2004 年 1 月 6 日 36.4524 35.2656 34.6529

2004 年 1 月 7 日 34.9561 35.7034 34.6692

如果要计算 3 天移动平均价格,以 2004年 1 月 3 日为例,计算方法如下:

[35.2957+34.8745+32.5929]/3=102.7631/3=34.2544

如果要计算 5 天移动平均价格,以 2004年 1 月 6 日为例,计算方法如下:

[34.8745+32.5929+33.6428+35.7017+36.4524]/3=173.2643/5=34.6529

从上述的计算方法可知, 要计算在某天的 N 天移动平均价格, 则在某天必须提供 N个数

据。因此 2004年 1 月 1日和 2004 年 1 月 2 日,无法计算 3 天移动平均价格。同理,2004 年 1 月 1 日~1月 4 日,也无法计算 5 天移动平均价格。

程序第 28 行~第 57行,创建绘制移动平均线所需要的数据集。

程序第 31 行,创建表示原油价格的 TimeSerires 对象 tsc。

程序第 32 行~第 33行:

double value = 35; Day day = new Day(1, 1, 2004);

设置原油在 2004年 1月 1 日的价格为 35 美元。 然后在程序第 33 行~第 43行的循环中,

Page 381: Java Web动态图表编程

生成全年的原油价格数据。每天的价格在其上一天原油价格的基础上浮动正负 1.5美元。 for (int i = 0; i < 360; i++)

// 生成原油价格 double flag = Math.random(); if (flag > 0.5)

value += Math.random() * 1.5; else

value ­= Math.random() * 1.5; tsc.add(day, value); day = (Day) day.next();

程序第 48 行~第 51行,创建表示移动平均线的数据集对象 mav:

TimeSeries mav = MovingAverage.createMovingAverage (

tsc, "30天油价移动平均价", 30, 30 );

实际上,表示移动平均线的数据集对象,也是从一个 TimeSeries 对象中派生出来的 TimeSeries 对象。创建方法是调用 MovingAverage 类的 createMovingAverage 方法。Create Moving Average方法需要接收 4 个参数,其作用如下:

Ø 第 1 个参数:TimeSeries 类实例。移动平均线的数据即通过该实例中的数据产生。

Ø 第 2 个参数:字符串类实例。用于设置移动平均线的标题。

Ø 第 3 个参数:整型变量 N。表示要计算 N天移动平均线所需的数据。

Ø 第 4 个参数:整型变量 M。表示从第M 天才开始计算 N 天移动平均线所需的数据,

从而跳过第 1 天~第 M−1 天期间的计算。实际上,MovingAverage 类的 create MovingAverage方法扩展了前面介绍的计算移动平均数的方法,M 的实际取值范围可

以是 0~N−1 之间的任意一个整数。

程序第 94 行~第 95行:

DecimalFormat priceFormat = new DecimalFormat("$###.00"); NumberTickUnit ntu = new NumberTickUnit(2.5, priceFormat);

这段代码用来设置纵轴刻度的显示格式。注意,我们在格式字符串中“$###.00” ,第 1 位使用了美元符号,因此最后的绘制结果,在纵轴刻度上的每个数字前都添加了一个美元符

号。程序第 95 行,参数中的 2.5,表示刻度值按 2.5的间隔依此递增。

程序第 101行:

DateTickUnit unit = new DateTickUnit(DateTickUnit.MONTH, 2, df);

参数中的“2” ,表示横轴上的刻度值每间隔两月显示一次。

8.2.8 JFreeChart 生成甘特图表

本节讲述如何利用 JFreeChart创建甘特图表。 先请看 jGantt.jsp(\chap08\Fig8.2\Fig. 8.2_17) 的运行结果,如图 8.17 所示。

Page 382: Java Web动态图表编程

图 8.17 JFreeChart甘特图

注:1为红色水平直方块,其他为蓝色水平直方块。

这里我们以一个软件的开发为例,来演示甘特图的创建。图 8.17 中所示的红色水平直方

块,用来模拟计划中软件开发的各个阶段所需的时间。图中蓝色水平直方块,用来模拟软件

的实际开发过程中,各个阶段实际花费的时间。

甘特图表的绘制同样需要一种特殊的数据集对象,实现了 IntervalCategoryDataset 接口

(位于 org.jfree.data.category包)的对象。TaskSeriesCollection类(位于 org.jfree.data.gant包)

实现了 IntervalCategoryDataset 接口,因此在程序中,我们利用该类对象 collection 来创建绘

制甘特图表所需要的数据集(程序第 84行)。

创建 TaskSeriesCollection类的对象 collection需要 TaskSeries类(位于 org.jfree.data.gant包)

的实例为参数。一个 TaskSeries 类的对象代表一个任务计划,因此,如果要在甘特图中绘制

多个任务计划,则必须创建多个 TaskSeries 类的对象。

程序在第 30 行~第 89 行的 createDataset()方法中,创建了两个 TaskSeries 类的对象 s1 和 s2。分别代表前面讲述的,软件开发项目各阶段的计划任务以及实际完成任务。

程序第 32 行:

TaskSeries s1 = new TaskSeries("计划");

调用 TaskSeries 类的构造器创建对象 s1。该构造器接收的参数“计划” ,表示该任务的

名称。

然后调用 s1的 add方法, 向该任务计划添加子任务。 add方法比较复杂, 它接收 1 个 Task 类的实例为参数。Task类(位于 org.jfree.data.gant 包)的对象表示一项子任务。我们以程序

第 33 行~第 34 行的 add方法为例,来说明其用法。

s1.add(new Task("软件开发可行性研究", new SimpleTimePeriod(getDate(2004, 2, 20), getDate(2004, 3, 5))));

本例使用 Task的构造器接收两个参数。第 1 个是字符串对象,表示该子任务的名称;第 2 个是 SimpleTimePeriod类(位于 org.java.data.time包)的对象,表示完成子任务所需要的时

段。它也需要两个 java.util.Date 类的参数,分别代表子任务的开始时间和结束时间。因此,

上面两句代码是向名为“计划”的任务中,添加一个名为“软件开发可行性研究”的子任务,

该子任务的开始时间为 2004年 2 月 20日,结束时间为 2004 年 3月 5 日。

因为 Sun 公司推荐使用 java.util.Calendar 类来代替原来的 java.util.Date 类。因此,我们

在程序第 108 行~第 114行,编写了一个 getDate的方法,通过调用 java.util.Calendar 类的方

1 1 1

1 1

1 1 1 1

1 1 1

Page 383: Java Web动态图表编程

法来初始化并获得一个 java.util.Date类的对象。

public Date getDate(int year, int month, int day)

Calendar calendar = Calendar.getInstance(); calendar.set(year, month, day); Date date = calendar.getTime(); return date;

Calendar 类的 getTime 方法,返回一个 java.util.Date 类的对象引用。因此,程序第 112 行,调用该方法就获得了一个 java.util.Date类的对象。

此外,程序第 33 行~第 34 行的 add方法,我们可以改写成另外一个版本:

s1.add(new Task("软件开发可行性研究", getDate(2004, 2, 20), getDate(2004, 3, 5)));

这个方法比刚才我们使用的版本要简洁一些,推荐读者在开发的时候使用此方法。

然后,多次调用 add方法,向 s1 中添加其他子任务。

同样,在程序第 58行~第 82行,又创建了 TaskSeries 类的对象 s2,表示任务实际的完

成情况。

程序第 85 行~第 86行:

collection.add(s1); collection.add(s2);

通过调用 collection的 add方法, 将两个任务对象添加到数据集中, 并结束 createDataset() 方法。甘特图表的其余部分代码,相信读者都已经很熟悉了,此处不再赘述。

8.2.9 JFreeChart 生成多轴(Multiple Axis)图表

多轴图表主要用于绘制在同一个绘制对象 (即前面我们讨论的 Plot 对象) 的绘制区域内,

根据两个不同的数据集合,将两个不同风格的图表对象(如直方图和线段图)绘制在同一个 Plot 对象中。比如说, 现在我们有两个数据集,其中一个表示的是 “Python” 、 “JAVA”和“C#”

这三类书籍某月每周的销售量,用直方图表示;另一个表示的是每类书籍每周的销售总量,

用线段图表示。两个数据集所对应的图表对象,都要绘制在同一个 Plot 内。这种图表被称为

多轴(Multiple Axis)图表。 jDualAxis.jsp(\chap08\Fig8.2\Fig.8.2_18)向读者演示了多轴图表的绘制方法。运行结果如

图 8.18 所示。

Page 384: Java Web动态图表编程

图 8.18 JFreeChart双轴图­1

图 8.18 所示直方图中的蓝、绿、橙色的直方块分别表示“Python” 、 “JAVA”和“C#”

这三类书籍每周的销售量,而红色线段图表示三类书籍每周的销售总量。我们可以看到,左、

右两边的坐标轴不仅标题不同(左边:销售量,右边:周销售总量),而且所表示的刻度值也

是不同的。

程序第 111 行之前的代码,绘制了表示三类书籍周销量的直方图,这部分代码读者已经

很熟悉了。只是在 createDataset1()方法中,程序第 41行~第 43 行:

bookSales = 50 + (int)(Math.random() * 200); dataset.addValue(bookSales, bookTitle[i], category[j]); totalSales[j] += bookSales;

使用一个 50~250 之间的随机数来表示某类书籍的周销售量。程序第 43 行,用于计算

所有书籍的销售总量。

程序第 114 行~第 132 行,根据另一个数据集对象绘制线段图。

程序第 114 行:

CategoryDataset dataset2 = createDataset2();

调用 createDataset2 方法,创建另外一个 CategoryDataset 类的数据集对象 dataset2。

程序第 117 行:

ValueAxis axis2 = new NumberAxis3D("周销售总量");

创建一个纵轴对象 axis2。

程序第 120行:

plot.setRangeAxis(1, axis2);

调用 plot 的 setRangeAxis 方法,设置右边纵轴所应用的纵轴对象为 axis2。setRangeAxis 方法接收两个参数。第 1 个参数表示纵轴的索引值,该参数的取值范围从 0 开始。0 表示左

边的纵轴,当前提供的参数值为 1,表示右边的纵轴;第 2 个参数是表示纵轴的 ValueAxis 对象。

Page 385: Java Web动态图表编程

程序第 123行:

plot.setDataset(1, dataset2);

调用 plot 的 setDataset 方法,设置当前绘图对象所应用的数据集对象为 dataset2。

程序第 126行:

plot.mapDatasetToRangeAxis(1, 1);

调用 plot 的 mapDatasetToRangeAxis 方法,将数据集对象 dataset2 中的数据映射到右边

的纵轴上(注意,我们认为实际是用于设置右边纵轴的刻度值)。该方法接收两个整型参数,

第 1 个参数表示数据集对象的索引值,第 2 个参数表示纵轴对象的索引值。在程序中,plot 所应用的第 1 个数据集对象 dataset1 的索引值为0,程序第 128 行应用数据集对象 dataset2 的索引值为 1,以此类推。第 2 个参数也是整型变量,表示纵轴的索引值。

之后,接着设置右边纵轴所应用的图表绘制(渲染)对象及其绘制属性。图表绘制对象,

这里指的是使用何种风格的图表(如线段图、直方图等)来表示相应的数据集对象。

程序第 129行:

CategoryItemRenderer renderer2 = new LineAndShapeRenderer();

用来设置图表绘制对象的风格为 LineAndShapeRenderer 类所表示的图表绘制对象。 LineAndShapeRenderer 类是 CategoryItemRenderer 类的子类,它表示直线或者其他形状的图

形。其默认构造器,所创建的图表绘制对象为线段图表。

程序第 130行:

renderer2.setSeriesPaint(0, Color.RED);

用来设置线段对象的绘制颜色为红色。

程序第 131行~第 132 行:

plot.setRenderer(1, renderer2); renderer2.setSeriesStroke(0, new BasicStroke(2.5f));

调用 plot 的 setRenderer 方法,设置右边纵轴图表所应用的渲染对象为 renderer2。最后设

置线段图表的笔划粗细为 2.5像素。

这样,通过上述代码就绘制出了如图 8.19 所示的多轴图表。如果我们将程序第 129 行的

代码替换成以下的内容:

BarRenderer3D renderer2 = new BarRenderer3D();

Page 386: Java Web动态图表编程

图 8.19 JFreeChart双轴图­2 则会使用两个直方图,分别表示数据集对象 dataset1 和 dataset2。运行结果如图 8.19 所

示。

我们在这里介绍的例程,实现了在一个图表中绘制双轴图表的功能。其实,JFreeChart 支持两个以上的多轴图表,因为多轴图表在实际开发工作中很少有机会使用到,并且其实现

方法和双轴图表基本相同,因此此处不再赘述。

8.2.10 JFreeChart 生成组合(Combined Axis)图表

组合图表(Combined Axis)也是比较复杂的一种 JFreeChart 图表。它和上一节介绍的多

轴图表相类似,也用于在同一个绘制对象(即前面我们讨论的 Plot 对象,这里我们称之为父 Plot 对象)的绘制区域内,根据两个不同数据集对象,将两个不同风格的图标对象(如直方

图和线段图)绘制在同一个 Plot 对象中。与多轴图表不同的是,绘制的两个不同风格的图表

对象(如直方图和线段图)各自是独立的。这两个独立的不同风格的图表对象(我们称之为

子 Plot 对象),两个子 Plot 对象可以具有独立的横轴或纵轴,当两者具有独立的横轴时,则

不拥有自身的纵轴,两者共享其父 Plot 对象的纵轴;反之,当两者具有独立的纵轴时,则不

拥有自身的横轴,两者共享其父 Plot 对象的横轴。

组合图表支持的数据集对象有 CategoryDataset 类,或者实现了 XYDataset 类或者 IntervalXYDataset 类接口的数据集对象。我们将依次向读者介绍,如何利用这些数据集对象

来绘制组合图表。

1.基于 CategoryDataset 数据集的组合图表

jCombinedCategoryPlot.jsp(\chap08\Fig8.2\Fig.8.2_19)演示了如何使用 CategoryDataset 类的

数据集来绘制组合图表。

请看图 8.20所示的 jCombinedCategoryPlot.jsp的运行结果。

图 8.20 JFreeChart组合图­1 jCombinedCategoryPlot.jsp程序模拟图表的含义, 以及所采用的数据集对象与前例基本相

同。在 8.20所示的运行结果中,我们可以看到,整个图表的绘制区域(父 Plot)被分成上下

两个部分, 上半部分的 3D 垂直直方图表 (子 Plot) 占据 2/3 的区域, 下半部分的线段图表 (子 Plot)占据剩下的 1/3 的区域。注意,3D 直方图表和线段图表用于各自独立的 plot 和纵轴,

但两者共享父 Plot 的横轴。

综上所述,它们之间的关系如图 8.21 所示。

Page 387: Java Web动态图表编程

图 8.21 CombinedDomainCategoryPlot子 Plot和父 Plot之间坐标轴的关系

程序第 82 行~第 95 行,用来创建上半部分的 3D 直方图,所需的数据集对象及其相关

绘制的参数。

程序第 82 行:

CategoryDataset dataset1 = createDataset1();

调用程序第37行~第50行的createDataset1方法, 创建CategoryDataset类的对象dataset1。

该方法的内容与前例介绍的方法完全相同,此处不再赘述。

程序第 84 行~第 85行:

NumberAxis rangeAxis1 = new NumberAxis("周销售量"); rangeAxis1.setStandardTickUnits(NumberAxis.createIntegerTickUnits(

));

创建 3D 直方图的纵轴对象 rangeAxis1,并设置其标题为“周销售量” 。

程序第 86 行~第 91行,用于设置 3D 直方图中各个直方块的绘制颜色。

程序第 93 行~第 94行:

CategoryPlot subplot1 = new CategoryPlot(dataset1, null, rangeAxis1, renderer1);

创建 CategoryPlot 对象 subplot1,用来绘制上半部分的直方图。注意,这里调用的 CategoryPlot 的构造器方法中,因为本例演示的是横轴共享,因此表示横轴的第 2 个参数,

必须设置为 null。如果希望纵轴共享,必须在构造器方法中,将表示纵轴的第 3 个参数设置

为 null。

程序第 95 行:

subplot1.setDomainGridlinesVisible(true);

调用 subplot1 的 setDomainGridlinesVisble,并提供参数 true,表示在 subplot1 中,显示

绘图区域的网格线。

同理,程序第 98 行~第 106 行,用来创建下半部分的线段图,所需的数据集对象及其

相关绘制的参数。

程序第 109行~第 110 行:

CategoryAxis domainAxis =new CategoryAxis("2005年 3月计算机编程类图表周销

售量统计图");

创建表示父 Plot 的横轴对象 domainAxis,并设置其标题为“2005 年 3 月计算机编程类

CombinedDomainXYPlot 父 Plot 的纵轴=null

Subplot 1

Subplot 2

Subplot 3

独立的纵轴

横轴=null

共享的父 Plot 的横轴

Page 388: Java Web动态图表编程

图表周销售量统计图” 。

程序第 113 行~第 116 行:

CombinedDomainCategoryPlot plot =new CombinedDomainCategoryPlot (domainAxis);

plot.add(subplot1, 2); plot.add(subplot2, 1);

创建基于 CategoryDataset 数据集的,横轴共享的组合图表对象(即父 Plot 对象)plot。

然后调用 plot 的 add 方法,将 subplot1 和 subplot2 添加到父 Plot 对象 plot 中。注意,add 方

法中接收两个参数:第 1 个参数表示子 Plot 对象,第 2 个整型参数表示该子 Plot 对象在父 Plot 对象的绘制区域中所占据的 2/3 的区域。这里的分母 3 等于所有子 Plot 对象的第 2 个参

数之和。同理,下半部分线段图的绘制区域为其父 Plot 对象绘制区域的 1/3。

2.基于 Time Series 数据集的组合图表

jCombinedTimeSeriePlot.jsp(\chap08\Fig8.2\Fig.8.2_20)演示了如何利用 IntervalXY Dataset 类的数据集来绘制组合图表。因为 TimeSeriesCollection 类实现了 IntervalXYDataset 接口,因

此本例采用 TimeSeriesCollection数据集对象。jCombinedTimeSeriePlot.jsp的运行结果,如图 8.22 所示。

从图 8.22 可以看出,父 Plot 对象也包含了两个子 Plot 对象。分别是左边的红色直方图

表和右边的原油价格走势曲线及其 30 天移动平均价曲线。 两者拥有各自独立的横轴, 并且共

享父 Plot 的纵轴。实现的方法和前例相类似。

Page 389: Java Web动态图表编程

图 8.22 JFreeChart组合图 2

本例需要向读者交代以下几点:

Ø 程序第 79 行~第 85 行以及程序第 127 行~第 134 行,在分别创建 XYPlot 类的对象 plot1 和 plot2 的时候,构造器的第 3 个参数,即纵轴都设置为 null。

Ø 程序第 153 行,创建 CombinedRangeXYPlot 类的对象。

Ø 程序第 154 行~第 155 行:

plot.add(plot1, 2); plot.add(plot2, 5);

与前例相类似,因此左边的直方图表占据了父 Plot 绘制区域的 2/7;右边的价格走势图

及其 30 天移动平均曲线图表占据父 Plot 绘制区域的 5/7。

程序的其他部分读者也很熟悉了,此处不再赘述。

3.基于 XYDataset 数据集的组合图表

本例 jCombinedXYPlotPlot.jsp(\chap08\Fig8.2\Fig.8.2_21)利用了XYDataset的数据集来绘

制一个复杂的组合图表。jCombinedXYPlotPlot.jsp运行结果如图 8.23 所示。

从图 8.23 中我们可以看到,本例的组合图表是由上、下两部分图表组成。上方图表是一

个双轴图表,由线段图和直方图组成。下方是一个线段图表,而且在该图表的左边部分,还

有一段提示文字( “钟京馗、唐桓设计” )。

本例的绘制思路是:首先绘制上方的双轴图表,然后绘制下方的线段图表,最后将两个

图表添加到父 Plot 对象中。

程序第 105行~第 122 行,实现绘制双轴图表的目的。

Page 390: Java Web动态图表编程

图 8.23 JFreeChart组合图 3

程序第 105行,通过调用程序第 54行~第 71 行的 createDataset1 方法,创建绘制双轴图

表所需的第 1 个 XYDataset 类的数据集对象 data1。 data1 实际是 XYSeriesCollection类的对象

(见程序第 36 行),因 XYSeriesCollection类实现了 XYData 类的接口,因此,我们可以直接

使用。

程序第 106 行~第 111 行的代码,用于绘制双轴图表左边纵轴所对应的图表对象(线段

图):

StandardXYItemRenderer renderer1 =new StandardXYItemRenderer (Standard XYItemRenderer.SHAPES_AND_LINES);

renderer1.setShapesFilled(true); renderer1.setSeriesStroke(0, new BasicStroke(1.5f)); NumberAxis rangeAxis1 = new NumberAxis("书籍销售量"); XYPlot subplot1 = new XYPlot(data1, null, rangeAxis1, renderer1);

注意,程序第 111 行,子 Plot 对象 subplot1 调用 XYPlot 构造器方法的时候,第 2 个参

数,即表示横轴对象的参数设置为 null。这里设置这些线段图的笔划粗细为 1.5 像素,线段

之间显示实心的系列小图标,左边纵轴的标题为“书籍销售量” 。

程序第 114 行~第 122 行:

XYBarRenderer renderer2 = new XYBarRenderer(); renderer2.setMargin(0.6); createDataset2(); subplot1.setDataset(1, data2); NumberAxis axis2 = new NumberAxis("书籍周销售总量"); axis2.setAutoRangeIncludesZero(false); subplot1.setRangeAxis(1, axis2); subplot1.setRenderer(1, renderer2); subplot1.mapDatasetToRangeAxis(1, 1);

将XYData数据集对象 data2映射到 subplot1的右边纵轴的绘制对象。 这部分代码的含义,

请读者参考本章第 8.2.9 节。这里介绍程序第 115 行所调用的 XYBarRenderer 类的 setMargin 方法,该方法接收一个双精度浮点数的参数,表示相邻两个直方块之间的间隔距离。

至此,完成了双轴图表的绘制工作。

Page 391: Java Web动态图表编程

程序第 125行~第 134 行,用于创建下方的线段图表对象 subplot2。

程序第 137行~第 141 行:

XYTextAnnotation annotation =new XYTextAnnotation("钟京馗、唐桓设计", 0.5, 150.0);

annotation.setFont(new Font("黑体", Font.PLAIN, 12)); annotation.setRotationAngle(Math.PI / 2.0); subplot2.addAnnotation(annotation);

这段代码用于向图表绘制区域中添加一条提示文本,XYTextAnnotation 类(位于 org. jfree.chart.annotations.包)即用于上述目的。程序第 137 行,创建 XYTextAnnotation类的实例

——annotation。这里调用该类的构造器方法需要接收 3 个参数,第一个参数是字符串对象,

表示提示文本的内容,后面两个参数是都是双精度型变量,表示提示文本的绘制坐标为(0.5, 150.0)。

程序第 138行,用于设置提示文本所用的字体。

程序第 140行,用于设置提示文本的绘制方向。

程序第 141 行,调用 subplot2 的 addAnnotation 方法,将 annotation 对象添加到 subplot2 中。

程序第 144行~第 145 行:

CombinedDomainXYPlot plot =new CombinedDomainXYPlot(new NumberAxis("周

数"));

创建 subplot1 和 subplot2 的父 Plot 对象 plot,并将其共享的横轴标题定义为“周数” 。

程序第 146行~第 149 行:

DecimalFormat df = new DecimalFormat("#0"); NumberTickUnit ntu = new NumberTickUnit(1, df); ValueAxis domainAxis = plot.getDomainAxis(); domainAxis.setStandardTickUnits(NumberAxis.createIntegerTickUnits());

设置横轴上的刻度值以整数的形式显示。

程序第 152行:

plot.setGap(10.0);

调用 plot 的 setGap方法, 设置子 Plot 对象之间的空白。 该方法接收一个双精度参数值为 10.0,表示子 Plot 对象之间的空白为 10.0 像素。

程序第 155行~第 157 行:

plot.add(subplot1, 1); plot.add(subplot2, 1); plot.setOrientation(PlotOrientation.VERTICAL);

我们给程序第 155 行和第 166 行的 add 方法,提供的第 2 个参数值相同都为 1。因此,

这两个图表各占父 Plot 对象 50%的绘图区域。 程序第 157 行, 调用 plot 的 setOrientation方法,

设置子图表对象 subplot1 和 subplot2 的排列方法,静态参数 PlotOrientation. VERTICAL,表

示按垂直方向排列子图表对象, 而静态参数 PlotOrientation.HORIZONTAL表示按水平方向排

列子图表对象。

8.2.11 JFreeChart 生成其他类型的图表

本节介绍几种利用 JFreeChart 生成其他类型图表的方法。

Page 392: Java Web动态图表编程

1.基于 OHLC 数据集的 K 线图表

K 线图也是常用的金融图表之一。K线图表上的一个数据点,通常需要 6 个数据,包括

日期(Date)、开盘价(Open)、最高价(High)、最低价(Low)、收盘价(Close),以及成

交量(volume)。JFreeChart 不仅专门提供了一个 OHLCDataset 类(位于 org.jfree.data.xy包)

来表示这种数据结构,而且在 ChartFactory类中还提供了一个 createCandlestickChart 的方法,

专门来绘制这类图表。

程序 jCandlestickPlot.jsp(\chap08\Fig8.2\Fig.8.2_22)演示了如何绘制 K线图,K 线图也

常称为蜡烛图。运行结果如图 8.24 所示。

本段代码的核心是如何创建 OHLCDataset 类的数据集对象。程序第 58 行~第 118 行的 createDataset 方法,演示了如何创建 OHLCDataset 类的数据集对象。前面说过,OHLCDataset 需要 6 个参数,分别是日期(Date)、开盘价(Open)、最高价(High)、最低价(Low)、收

盘价(Close)以及成交量(volume)。因此,我们提供了 6 个数组,即 date、high、low、open、 close 和 volume。其中 date 是 java.awt.Date 类的对象,其余的都是保存双精度型的数据的数

组。

程序第 60 行~第 115 行,用来初始化这 6 个数组。在生成的 4 个随机数中,我们分别

调用程序第 121 行~第 124 行的 getHigh 方法,来获取这 4 个随机数中的最大值。调用程序

第 127 行~第 130 行的 getLow方法,来获得这个随机数中的最小值。

程序第 117 行:

return new DefaultHighLowDataset("", date, high, low, open, close, volume);

Page 393: Java Web动态图表编程

图 8.24 JFreeChart K线图

是创建 OHLCDataset 类的数据集对象的核心。DefaultHighLowDataset 类(位于 org.jfree.data 包)实现了 OHLCDataset 类的接口。这里的构造器需要接收 7 个参数,第 1 个

参数是字符串参数,表示该数据集的名称。这个字符串内容将会出现在图例中,这里我们提

供了一个内容为空的字符串。其他参数就是刚才向读者讲述的 6个数组。

程序第 37 行~第 47行,演示了如何通过 ChartFactory类的 createCandlestickChart 方法,

来创建一个 K 线图。注意,程序第 43 行,设置图例显示的参数为 fasle,表示最终的图表绘

制结果中不包含图例。

2.罗盘图表

罗盘(指南针)图,是 JFreeChart 提供的一种有趣的图表。它利用罗盘指针的方位,来

表示用户提供的参数。利用 JFreeChart 可以非常轻松地创建罗盘图。

程序 jCompassPlot.jsp(\chap08\Fig8.2\Fig.8.2_23)演示了罗盘图表的绘制方法,如图 8.25 所示。

创建罗盘图需要的数据集对象是实现了 ValueDataset 类(位于 org.jfree.data 包)接口的

对象。DefaultValueDataset 类实现了 ValueDataset 类的接口,该类在这里使用的构造器接收一

个双精度的参数。该参数表示方位的角度,如 45.0 表示 45 度。

程序第 19 行~第 27行之间的代码创建了罗盘图表的对象:

public JFreeChart createChart(ValueDataset dataset)

CompassPlot plot = new CompassPlot(dataset); plot.setSeriesNeedle(9); plot.setSeriesPaint(0, Color.RED); plot.setSeriesOutlinePaint(0, Color.RED); JFreeChart chart = new JFreeChart(plot); return chart;

Page 394: Java Web动态图表编程

图 8.25 JFreeChart罗盘图

程序第 21 行,创建 CompassPlot 类(位于 org.jfree.chart.plot 包)的对象 plot。它接收了

一个 ValueDataset 类的数据集对象 dataset。

程序第 22 行,调用 CompassPlot 类的 setSeriesNeedle 方法,用于设置罗盘图中指针的外

观形状。该方法需要一个整形变量为参数,决定指针的外观形状。JFreeChart 提供了 1~9,

共计 9 种不同风格的指针(参数分别从 1~9),这里我们使用的是最后一种的外观形状。

程序第 23 行,调用 setSeriesPaint 方法,设置指针的绘制颜色。

程序第 24 行,调用 setSeriesOutlinePaint,设置指针轮廓的绘制颜色。

注意,一个罗盘上可以绘制多个指针,只需要通过 CompassPlot 的 add 方法,并提供一

个 ValueDataset 类的数据集对象即可。因此,在定义指针的绘制颜色时,必须指明该颜色应

用于哪一个指针。根据指针创建的先后顺序,系统自动为其创建了索引值。程序第 23 行和第 24 行中,第一个参数表示指针的索引值,0 表示第一个指针对象。

程序第 25 行,创建罗盘图表。

注意,罗盘指针的绘制方向是顺时针方向。0 度方位指向正北方(正对 N),图 8.23 显

示的角度为 90 度时罗盘指针指向的位置。

3.温度计图表

温度计图表,是 JFreeChart 提供的另一种有趣的图表。它使用温度的改变来显示用户提

交的参数。

程序 jThermometerPlot.jsp(\chap08\Fig8.2\Fig.8.2_24)演示了温度计图表的绘制方法。

运行结果如图 8.26 所示。

Page 395: Java Web动态图表编程

图 8.26 JFreeChart温度计图

温度计图表和罗盘图表的创建方法相类似,对比两个源程序,读者会发现它们之间的细

微差别。

8.3 JFreeChart 与 Servlet图表编程

8.3.1 简单的 Servlet 图表编程

JBarServlet.java 演示了如何利用 JFreeChart 来编写 Java Servlet 程序。它是利用本章上节

所介绍的第一个 JSP 程序(\chap08\Fig8.2\Fig.8.2_01\JFreeChartBasicBar.jsp)而改写成的 Serlvet。

打开 web.xml(\chap08\Fig8.3\Fig.8.3_01)文档,复制该文档中的第 9 行~第 18 行内容,

然后将复制的内容粘贴到\WEB­INF\web.xml 文档中的<web­app></web­app>标记中。

<servlet> <servlet­name>JBar</servlet­name>

<description>利用 JFreeChart生成直方图的 Servlet</description> <servlet­class> com.fatcat.webchart.JBarServlet</servlet­ class>

</servlet>

<servlet­mapping> <servlet­name>JBar</servlet­name> <url­pattern>/jBar</url­pattern>

</servlet­mapping>

将 Servlet 源程序 JBarServlet.java 拷贝到 Web 应用程序根目录下 WEB­INF\com\ fatcat\webchart 的子目录下。启动 Resin 服务器或者重新启动 Tomcat 服务器(建议读者使用 Resin),运行 IE,在其浏览器中输入: http://localhost:8080/chap08/Fig8.3/Fig.8.3_01/ JBarServlet.html 或者 http://localhost:8080/jBar 运行结果如图 8.27所示。

Page 396: Java Web动态图表编程

图 8.27 JFreeChart编写简单的 Servlet

我们可以看到,其运行结果与图 8.1 完全相同。

提示:如果读者在 Tomcat 的环境下运行本例程,而并没有得到如图 8.27 所示的运行结

果,解决方法可参考 5.2节介绍的提示。此外,如果不希望通过 Resin来自动编译此 Servlet,

而是希望在命令提示符下手动将 Servlet 源程序编译成 class 文件,则需要将 JFreeChart 安装

目录下的 jfreechart­0.9.21.jar 文件,以及 gnujaxp.jar、jcommon­0.9.6.jar、junit.jar 和 servlet.jar 文件复制到 JDK 的安装目录下的\jre\lib\ext 子目录中。即可在命令提示符下,正确地编译 Servlet 的源程序。在本书后续讲解的例程中,如果大家在 Tomcat 的环境下无法获得正确的

运行结果,则可以借鉴本例的解决方法,此处不再赘述。

Servlet 结构以及 JFreeChart 生成普通图表的编程方法读者都已经很熟悉了。这里需要提

醒的是程序第 74 行:

ChartUtilities.writeChartAsPNG(out, chart, width, height);

生成图像所调用的方法,需要使用 ChartUtilities 类(位于 org.jfree.chart 包)的 write 方

法。 这里write方法只需要 4个参数, 而不能调用前面介绍的 JSP文件中所使用的 ervletUtilities 类的 saveChartAsPNG 方法。

8.3.2 交互式 Servlet 图表编程

本节介绍如何利用 JFreeChart 来编写交互式的 Java Servlet 程序。这里所说的交互式,是

指 Servlet 程序可以根据用户选择,而生成不同风格的图表。我们以一个 JSP 程序和 Servlet 之间的交互为例。

打开 web.xml(\chap08\Fig8.3\Fig.8.3_02)文档,复制该文档中的第 9 行~第 18 行内容,

然后将其粘贴到\WEB­INF\web.xml 文档中的<web­app></web­app>标记中。

<servlet> <servlet­name>JInteractive</servlet­name>

<description>利用 JFreeChart生成的交互式 Servlet</description> <servlet­class>com.fatcat.webchart.JInteractiveServlet</servl­

etclass> </servlet>

<servlet­mapping> <servlet­name>JInteractive</servlet­name> <url­pattern>/jInteractive</url­pattern>

Page 397: Java Web动态图表编程

</servlet­mapping>

然后,将 Servlet 源程序 JInteractiveServlet.java(\chap08\Fig8.3\Fig.8.3_02)拷贝到 Web 应用程序根目录下\WEB­INF\com\fatcat\webchart 的子目录下。重新启动 Tomcat 服务器运行 IE,在其浏览器中输入:http://localhost:8888/chap08/Fig8.3/Fig.8.3_02/JInteractive. jsp,可以看

到如图 8.28所示的结果。

图 8.28 JInteractive.jsp的运行结果 1

如果用户没有选择 4种风格图表(饼图、3D 饼图、直方图、3D 直方图)中的任意一种,

则会显示一段红色的文本“请选择一种风格的图表! ” ,提示用户进行选择。

如果用户选择了“3D 饼图”这个单选项,并单击右边的“生成图表”按钮,则会看到

如图 8.29 所示的运行结果。

如果用户进行了其他的选择,则会生成相应的不同风格的图表。

我们先来看与 Servlet 交互的 JInteractive.jsp文档的源程序。 JInteractive.jsp的源程序主要

实现以下的功能:

(1)程序第 20 行~第 23 行,生成一个具备 4个单选按钮(饼图、3D饼图、直方图、 3D 直方图)的表单;

图 8.29 JInteractive.jsp的运行结果 2

(2)程序第 28 行~第 31 行,如果用户没有选择上述的任意一个单选项,则显示一段

红色的文本“请选择一种风格的图表!” ;

(3) 程序第 33 行~第 37 行, 利用 HTML中的 IMG标记, 通过请求 Servlet: /Interactive,

提供一个体现表格风格的“chartType”参数,并返回相应的图表。

Page 398: Java Web动态图表编程

现在讨论 JInteractiveServlet.java 的源程序:

程序第 49 行:

String chartType = request.getParameter("chartType");

获得客户端提交的参数“chartType” 。

程序第 57 行~第 72行:

if (chartType.equals("pie")) chart = createPieChart();

else if (chartType.equals("pie3D")) chart = createPieChart3D();

else if (chartType.equals("bar")) chart = createBarChart();

else if (chartType.equals("bar3D")) chart = createBarChart3D();

根据字符串变量 chartType 的值,调用不同的方法来创建相应的图表。这些方法我们都

已经很熟悉了,此处不再赘述。

8.4 Cewolf 与 JSP 图表编程

8.4.1 Cewolf 简介

Cewolf 是开放源代码组织 cewolf.sourceforge.net 的一个 Java 项目(主页 http://cewolf. sourceforge.net/) 。

Cewolf是采用 JFreeChart 的图形绘制引擎而开发的另外一套 Web 动态图表生成引擎。 可

以这样认为,Cewolf 实际上是 JFreeChart 的一个壳,核心仍然是 JFreeChart。因此它天生就

具有 JFreeChart 的优点,并且改进了 JFreeChart 难于使用的一些缺点,可以说它是目前优秀

的 Web 图表生成/绘制引擎之一。 Cewolf既不在服务器端生成任何文件,也不在 JSP文件中写入任何 Java代码,它采用一套

类似于XML格式的标签库,将这些标签嵌入 JSP文件中,就可以生成诸如饼图、柱状图、线图、

甘特图以及其他类型的图表。这些图表可以满足目前基本的商业级应用要求。

目前,Cewolf的最新版本是 cewolf­0.10.2 版,此版本于 2005 年 2月 22 日发布。注意,

不同版本的 Cewolf 并不兼容(与 JFreeChart 相同)。此外,Cewolf 还必须和与之相匹配的 JFreeChart 一同工作,才能完成相应的图表绘制工作(cewolf­0.10.2 版,必须同我们前面说过

的 jfreechart­0.9.21 版配合使用)。

与 JFreeChart 不同的是,Cewolf不仅是完全免费的开放源代码作品,而且提供了一些完

整的例程及标签库文档。这为我们学习使用 Cewolf 带来了很大的方便。

我们已经详尽讲解了在 JSP/Servlet 环境下, 直接调用 JFreeChart 生成 Web 图表的种种方

法,下面我们简要介绍 Cewolf的使用方法。

提示:经过我们的测试,cewolf­0.10.2 版在某些版本的 Resin 服务器下无法正常工作,

Page 399: Java Web动态图表编程

如 Resin 3.0.11 版,但本书发行时,在最新的 Resin 3.0.14 版服务器下,cewolf­0.10.2 版也可

以正常工作了。

8.4.2 Cewolf 的下载与安装

首先下载最新版的 cewolf10_2.zip 文件(2005 年 2 月 22 日发布),下载地址:http:// sourceforge.net/project/showfiles.php?group_id=57282。将其解压缩后,在 lib 下面,我们可以

看到如下几个 JAR 文件;

Ø jfreechart­0.9.21­demo.jar Ø jfreechart­0.9.21.jar Ø jcommon­0.9.6.jar Ø commons­logging.jar Ø cewolf.jar Ø batik­xml.jar Ø batik­util.jar Ø batik­svggen.jar Ø batik­dom.jar Ø batik­awt­util.jar 将上述几个 JAR 文件复制到应用程序根目录下\WEB­INF\lib 的子目录下。 如果前面我们

已经将 JFreeChart 的一些相关 JAR 文件拷贝到\WEB­INF\lib 子目录下, 则以下几个 JAR 文件

不需要拷贝到该目录下:

Ø jfreechart­0.9.21.jar Ø jcommon­0.9.6.jar 当然,采用将全部上述 JAR 文件拷贝到\WEB­INF\lib 子目录下,覆盖同名文件的方式也

可以。然后将/example/etc下的 overlib.js 复制到 WEB应用程序的根目录下面,该文件为浏览

器在浏览图像时提供了工具提示。将/etc 下的 cewolf.tld 和 cewolf­1.1.tld 复制到 Web 应用程

序的根目录下\WEB­INF的子目录下。最后在/WEB­INF/web.xml文件中增加以下内容:

<servlet> <servlet­name>CewolfServlet</servlet­name> <servlet­class>de.laures.cewolf.CewolfRenderer</servlet­class>

<init­param> <param­name>overliburl</param­name> <param­value>overlib.js</param­value>

</init­param>

<load­on­startup>1</load­on­startup> </servlet>

<servlet­mapping> <servlet­name>CewolfServlet</servlet­name> <url­pattern>/cewolf/*</url­pattern>

</servlet­mapping>

为了测试 Cewolf是否正确安装,重新启动 Tomcat 服务器后打开浏览器,在地址栏中输

入 http://localhost:8080/cewolf?state。

如果看到类似图 8.30所示的运行结果,即运行结果中有:Cewolf servlet up and running 这样一行输出,则证明 Cewolf已经安装成功。

Cewolf支持以下的图表类型。

Ø Line [XY]:对应 JFreeChart 的线段图。

Page 400: Java Web动态图表编程

Ø [3D] Pie:对应 JFreeChart 的饼图及 3D 饼图。

Ø [3D] Horizonal Bar:对应 JFreeChart 的水平直方图及 3D 水平直方图。

Ø [3D] Vertical [XY] Bar:对应 JFreeChart 的垂直直方图及 3D垂直直方图。

Ø [3D] Stacked Vertical Bar:对应 JFreeChart 的堆栈图及 3D 堆栈图。

Ø Area [XY]:对应 JFreeChart 的地形图。

Ø Scatter Plot:对应 JFreeChart 的散列图。

Ø Candlestick:对应 JFreeChart 的 K 线图(蜡烛图)。

Ø High Low:对应 JFreeChart 的 OHLC 图表。

Ø Gantt:对应 JFreeChart 的甘特图表。

Ø Overlaid:类似 JFreeChart 的多轴图表(不支持坐标映射)。

Ø Combined:对应 JFreeChart 的组合图表。

图 8.30 测试 Cewolf安装是否成功

8.4.3 Cewolf 生成直方图表

调用 Cewolf生成图表的步骤与 JFreeChart 相类似,分为以下几个步骤:

(1) 指定在什么地方加载 cewolf.tld文件来验证 JSP程序中, Cewolf标签使用的合法性;

(2)生成绘制图表所需的数据集对象;

(3)在 JSP 程序中,插入 Cewolf的标签,定义当前所绘制图表的类型、宽度、高度、

背景图案、背景色、所绑定的数据集对象等信息。

所有调用 Cewolf标签来生成 Web 图表的 JSP 程序,都必须符合上述几个步骤。

下面以生成普通垂直直方图的 CewolfVBar.jsp(\Fig8.4\Fig.8.4_01)为例,具体说明上述

步骤的编程方法。CewolfCBar.jsp的运行结果如图 8.31 所示。

程序第 6 行,使用/WEB­INF/目录下的 cewolf.tld文件,来验证 JSP 文件中标签的使用是

否正确。cewolf.tld是标签库描述文件(Tag Library Desriptors) ,简称 TLD 文件。TLD 文件

是一个 XML文档,它包含了关于整个标签库以及库中每个标签的信息。

Page 401: Java Web动态图表编程

图 8.31 Cewolf:垂直直方图

程序第 9 行~第 10 行:

<%@ page import="de.laures.cewolf.*, de.laures.cewolf.tooltips.*"%> <%@ page import="de.laures.cewolf.links.*"%>

引入 Cewolf 及其相关的包。利用 Cewolf 生成 Web 图表,注意,除必须引入 cewolf.tld 的标签库文件以及 de.laures.cewolf.*、de.laures.cewolf.tooltips.*和 de.laures.cewolf.links.*包以

外,还必须引入我们前面讲述 JFreeChart 时所使用的相关包(Cewolf 图形绘制引擎是基于 JFreeChart 的图形绘制引擎的二次开发) 。

程序第23行~第50行, 生成图表所需的数据集对象Object (实际上是DefaultCategory Dataset 类对象 dataset)及方法 getProducerId和 hasExpired。在 Cewolf中,生成图表所需数据的方法

基本相同,都是用 DatasetProducer 类的默认构造器中 produceDataset 方法,得到的一个包含

所需数据的Object对象。 produceDataset的方法体中, 根据要生成的图表类型而调用 JFreeChart 相对应的类,来生成所需的数据集对象 dataset。如程序第 27 行~第 38 行的代码所示:

DefaultCategoryDataset dataset = new DefaultCategoryDataset(); // 向数据集中添加绘制图表所需的数据 int bookSales; for (int i=0; i < bookTitle.length; i++)

for (int j=0; j < category.length; j++ )

bookSales = 50 + (int)(Math.random() * 50); dataset.addValue(bookSales, bookTitle[i], category

[j]);

return dataset;

程序第 27 行~第 37 行,在 JFreeChart 下创建数据集对象的方法。这段代码创建了一个 DefaultCategoryDataset 类的对象 dataset。程序第 38 行,将创建好的 dataset 返回给 Object 对

象。

程序第 41 行~第 44行:

public String getProducerId()

return "CategoryDataProducer";

Page 402: Java Web动态图表编程

声明了一个 getProducerID 的方法,该方法返回一个内容为“CategoryDataProducer”的

字符串,表示当前数据集生产者(DatasetProducer)的标识(ID)为“CategoryDataProducer” 。

该标识(ID)将会被后面的 Cewolf标签所使用。

程序第 46 行~第 49行:

public boolean hasExpired(Map params, Date since)

return false;

该方法被 Cewolf自身所调用,用于检查当前数据集生产者(DatasetProducer)上一次生

产的数据是否可以继续使用。布尔参数值“false” ,表示不检查以前的数据; “true” ,表示可

以使用以前数据集生产者所创建的数据。对于需要实时生成的 Web 图表来说,当然是提供

“false”参数,强制 Cewolf每次都根据最新的数据进行图表的绘制工作。

程序第 53 行:

pageContext.setAttribute("categoryData", categoryData);

为生成的数据集对象 categoryData 定义一个 ID,该 ID 值为“categoryData” ,将被 JSP 程序中的 Cewolf标签所引用。

至此,生成直方图表所需的数据及其相关方法就结束了。下面需要在 JSP 程序中,使用 Cewolf标签来对这个数据进行“加工” ,并最终生成所需的图表。

程序第 61 行~第 76行,就是 Cewolf标签的使用方法。我们把一段 XML风格的标签嵌

入 JSP 页面中,该段标签定义了该图表的类型、宽度、高度、背景图案、背景色、图表绑定

的数据等绘制属性。

<cewolf:chart id="verticalBar" title="Cewolf 编程实例 1: 垂直直方图" type="verticalBar" xaxislabel="销售时间:2005年 4月" yaxislabel="销售量"> <cewolf:data>

<cewolf:producer id="categoryData" /> </cewolf:data>

</cewolf:chart>

<cewolf:img chartid="verticalBar" renderer="/cewolf" width="500" height="375"/>

我们在程序第 6 行定义了 TLD 文件后,就可以在 JSP页面中使用定义标签了。注意:标

签的使用格式<cewolf:tag>,这里的 tag可以被替换成任何合法 Cewolf标签,如 chart、img、 plot、imgurl、legend、data 等。而且上述各个根标签还包含了一系列的属性,属性由属性名

称和属性内容组成。所有的属性都可以在 Cewolf的网站及下载文件中的\docs 中查到。

这段标签分成两部分,程序第 61 行~第 70行,描述<cewolf:chart>的内容,其中又包括

了一个<cewolf:data>的内容;程序第 72行~第 76 行,描述<cewolf:img>的内容。两者必须同

时使用才可以正确绘制图表。 标签中的属性有些是必选的, 有些是可选的。 本例中, <cewolf:img> 标签告诉 Cewolf绘图引擎,绘制一个宽度为 500像素,高度为 375像素、chartid为 verticalBar 的图表,该 chartid的值要与<cewolf:chart>标签中属性为 id的值完全相同才匹配成功。当前图

表的渲染(绘制)引擎由 cewolf 负责。然后,根据<cewolf:chart>标签中定义的其他属性值,

来确定该图表的类型(线段型、直方型、饼图型,甘特图等)、标题、X 轴和 Y 轴的标题、图

Page 403: Java Web动态图表编程

表背景、图表绑定的数据等。下面依次讲解:

Ø id=" verticalBar "和<cewolf:img>中属性 chartid=" verticalBar "的值相匹配, 两者必须完

全相同。该属性是必选项。

Ø type=" verticalBar ",定义图表的类型,它可以是以下类型中的任意一种:area, areaxy, horizontalbar, horizontalbar3d, line, pie, scatter, stackedhorizontalbar, stackedverticalbar, stackedverticalbar3d, timeseries, verticalbar, verticalbar3d, xy, candlestick, highlow, gantt, wind, signal, verticalxybar, pie3d。该属性是必选项。

Ø xaxislabel="销售时间:2005 年 4 月",图表 X 轴标题,可选项。该属性在图表 type 属性为“pie”或者“pie3d”时被忽略。

Ø yaxislabel="销售量",图表 Y轴标题,可选项。该属性在图表 type属性为“pie”或者

“pie3d”时被忽略;

Ø title="Cewolf编程实例 1:垂直直方图",定义图表的标题,可选项。

Ø <cewolf:producer id=" categoryData " />,该标签其实并不属于<cewolf:chart>标签,它

属于<cewolf:data>标签。但因为<cewolf:data>标签总是嵌套在<cewolf:chart> 标签中。

注意,数据生产者(producer)标签中属性 id 的值为 categoryData,该属性值与我们

前面所讲述的程序第 53 行:

pageContext.setAttribute("categoryData", categoryData);

定义数据集对象的 ID 相同。也就是说,我们当前绘制图表所需的数据就由这个 id 为

“categoryData”数据集对象所提供,通过这种方式,图表和数据就绑定在一起了。

综上所述,任何利用 Cewolf 创建 Web 动态图表的 JSP 程序,只要遵从上述例程所示的

结构和用法,就可以编写出生成其他图表的 JSP 应用程序了。

后续各节所讲述的例程和当前介绍的 CewolfVBar.jsp 源程序的结构、标签的使用方法大

同小异,我们就不再讲解源程序中数据集对象的生成过程,将简要介绍标签部分关键代码的

含义。

8.4.4 Cewolf 生成基于 DefaultCategoryDataset 数据集的图表

本节讲述如何利用 Cewolf 生成基于 DefaultCategoryDataset 数据集对象的图表。 DefaultCategoryDataset 数据集对象是使用频繁的数据对象,很多种类的图表都使用这种数据

集对象,如普通/堆栈区域图、普通/3D 直方堆栈图、普通/3D 直方图,以及线段图等。

1.Cewolf 生成水平直方图表

大部分种类的图表,都依赖于 DefaultCategoryDataset 数据集对象提供的数据来进行绘图

工作。因此我们将生成 DefaultCategoryDataset 数据集对象的这部分代码,单独写成一个 JSP 文件 CewolfDataset.jsp。然后,在其他依赖于 DefaultCategoryDataset 数据集对象进行图表绘

制工作的 JSP 程序,通过 include指令,用如下所示的方式,引入该 CewolfDataset.jsp文件即

可:

<%@ include file="CewolfDataset.jsp" %>

关于 CewolfDataset.jsp (\chap08\Fig8.4\Fig.8.4_02) 的源程序中的代码, 读者都是很熟悉,

此处不再赘述。 CewolfCategoryDatasetChart1.jsp (\chap08\Fig8.4\Fig.8.4_02)绘制了水平直方图和 3D水平

直方图,如图 8.32 所示。

程序第 6 行:

Page 404: Java Web动态图表编程

<%@ include file="CewolfDataset.jsp" %>

引入 CewolfDataset.jsp 文件。本 JSP 程序所需要引入的 JFreeChart、Cewolf 的各个类,

以及创建 DefaultCategoryDataset 类的数据集对象 categoryData 都由该文件负责。

程序第 14 行~第 31行,用于生成图 8.32①所示的背景为黄色、普通水平直方图表。核

心代码是程序第 17 行,<cewolf:chart>标签中的图表类型属性“type” :

type="horizontalBar"

Page 405: Java Web动态图表编程

图 8.32 Cewolf:水平直方图

属性值:horizontalBar 表示图表类型为水平直方图。

程序第 15行,设置<cewolf:chart>标签中的图表类型属性“id”值为“horizontalBarChart” ,

该属性值和程序第 27行,<cewolf:img>标签中的图表 id属性“chartId”值完全相同。

此外,程序第 20 行:

<cewolf:colorpaint color="#FFFF99"/>

用于设置整个图表背景的绘制颜色。该标签属于可选项,颜色使用方法同 HTML语法是

相同的,比如说,黑色的代码是“#000000” ,而红色的代码是“#FF0000” 。

程序第 35 行~第 53 行,用于生成图 8.32②所示背景为粉红色的、3D 水平直方图。程

序第 38 行,设定<cewolf:chart>标签中的图表类型属性“type”值为“horizontalBar3D” ,表

示图表的绘制类型为 3D 水平直方图。

另外,程序第 41 行:

antialias="false">

关闭图表的反锯齿功能。Antialias 属性用来设置图表的反锯齿功能。该属性的默认值为

“true” ,即打开反锯齿功能。对比图 8.32①和图 8.32②的运行结果,反锯齿功能对图表最终

绘制结果的影响一目了然。我们已经讲过,由于 JFreeChart 的反锯齿功能同样也作用于图表

中的文本,因此,如果图表中包含有中文字符,经过反锯齿处理后,显示效果会显得比较模

Page 406: Java Web动态图表编程

糊。

程序第 39 行~第 40行:

xaxislabel="<%=xaxislabel%>" yaxislabel="<%=yaxislabel%>"

这两句代码说明,在 Cewolf的标签中,设定任意一个属性的属性值时,可以插入 JSP代

码。 这里属性值中的<%=xaxislabel%>和<%=yaxislabel%>分别来自 CewolfDataset.jsp文件中的第 27 行和第 28行定义的两个字符串变量。

2.Cewolf 生成垂直堆栈图表

CewolfCategoryDatasetChart2.jsp (\chap08\Fig8.4\Fig.8.4_02)绘制了垂直堆栈图和 3D垂直

堆栈图,如图 8.33 所示。

程序第 10 行~第 22行:

ChartPostProcessor dataColor = new ChartPostProcessor() public void processChart(Object chart, Map params) CategoryPlot plot = (CategoryPlot)((JFreeChart)chart).getPlot(); for (int i = 0; i < params.size(); i++) String colorStr = (String)params.get(String.valueOf(i)); plot.getRenderer().setSeriesPaint(i, java.awt.Color.decode

(colorStr));

; pageContext.setAttribute("dataColor", dataColor);

创建了 ChartPostProcessor 类的对象 dataColor,它用来设定图表中各个图形对象的绘制

颜色。这实际上也隶属于 JFreeChart 的编程范畴。本段代码中所调用的 getPlot、getRenderer,

以及 setSeriesPaint 等方法都是 JFreeChart 的方法。因此,如果不清楚 Jfree Chart 的用法,

就只能调用 Cewolf来生成默认效果的动态图表, 从而无法发挥 JFreeChart 图表引擎的强大功

能。

Page 407: Java Web动态图表编程

图 8.33 Cewolf:垂直堆栈图

注意,程序第 40 行~第 45 行:

<cewolf:chartpostprocessor id="dataColor"> <cewolf:param name="0" value="<%= "#FFFFAA" %>"/> <cewolf:param name="1" value="<%= "#AAFFAA" %>"/> <cewolf:param name="2" value="<%= "#FFAAFF" %>"/> <cewolf:param name="3" value="<%= "#FFAAAA" %>"/>

</cewolf:chartpostprocessor>

通过<cewolf:param>标签,将包含参数名/参数值等信息的数据对象作为 Map 类的实例 params,以参数的形式传递给 dataColor 的 processChart 方法,然后,通过 params 的 get 方法

来获得参数值。再通过 Color 类的 decode 方法,接收并解析该参数值,返回一个 Color 类的

对象。 最后, 通过熟悉的 JFreeChart 中 CategoryPlot 类的 setSeriesPaint 方法, 利用返回的 Color 对象对图表中的各个绘制对象进行颜色的设定。

程序的其余部分,请读者自行阅读。

3.Cewolf 生成水平堆栈图表和 3D 垂直直方图表

CewolfCategoryDatasetChart3.jsp (\chap08\Fig8.4\Fig.8.4_02)绘制了水平堆栈图和 3D垂直

直方图,如图 8.34 所示。

Page 408: Java Web动态图表编程

图 8.34 Cewolf水平堆栈图及 3D垂直直方图

注意,图 8.34②所示 3D 垂直直方图中,各个直方块绘制颜色的自定义方法与前例垂直

堆栈图中使用的方法完全相同。只要是以 CategoryDataset 数据集对象为基础的Web 图表,都

可以调用这个方法来自定义图表中各个图表对象的绘制颜色。

此外,还要提醒读者,Cewolf不能生成 3D 水平堆栈图表。

Page 409: Java Web动态图表编程

4.Cewolf 生成区域图表

CewolfCategoryDatasetChart4.jsp(\chap08\Fig8.4\Fig.8.4_02)绘制了普通区域图和堆栈区

域图,如图 8.35 所示。

图 8.35 Cewolf:区域图

图 8.35①所示区域图的背景,实现了渐进色效果。图 8.35②所示堆栈区域图,用一幅图

像来作为整个图表的背景。在 Cewolf中,同样只需要添加几个属性就可以实现上述效果。

实现背景渐进色填充效果的代码,见程序第 20行~第 23 行:

<cewolf:gradientpaint> <cewolf:point x="0" y="0" color="#99CCCC" /> <cewolf:point x="300" y="0" color="#DDDDFF" />

</cewolf:gradientpaint>

程序第 44 行:

background="/bg.jpg" >

实现了用图像文件 bg.jpg 作为整个图表的背景图像的目的。这里“/bg.jpg”表示图像文

件 bg.jpg位于 Web 应用程序的根目录下。

Page 410: Java Web动态图表编程

5.Cewolf 生成线段图表

CewolfCategoryDatasetChart5.jsp(\chap08\Fig8.4\Fig.8.4_02)绘制了线段图并将该图表的

图例设置为左边显示,如图 8.36 所示。

图 8.36 Cewolf:线段图

CewolfCategoryDatasetChart5.jsp的源程序清单如下:

程序第 18 行:

legendanchor="west"

Legendanchor=“west” ,指定把图例显示在图表的西方(左面) 。该属性共有 4 个可选值: “north”、“south”、“west”以及“east”。分别代表图例的显示位置在北方(上方) 、南方(下方) 、西

方(左边)和东方(右边) 。默认是“south”,也就是把图例显示在图表的南方(下方) 。

8.4.5 Cewolf 生成饼图

CewolfPie.jsp(\chap08\Fig8.4\Fig.8.4_03)演示了如何利用 Cewolf 生成基于 DefaultPie Dataset 数据集对象的饼型图表。此外,还添加了一个新的功能,即如何在图表中生成工具提

示条(tooltip)。 CewolfPie.jsp的运行结果,以及工具提示条的运行效果如图 8.37所示。

Page 411: Java Web动态图表编程

图 8.37 Cewolf:饼图

我们可以看到,当鼠标停留在饼图中的某一区域(section)时,会出现一条非常醒目的

小提示条,显示的内容是鼠标所在饼图区域所代表的书籍销售量,起到了很好的提示作用。

首先,程序第 56 行~第 63 行:

PieToolTipGenerator pieTG = new PieToolTipGenerator() public String generateToolTip (PieDataset dataset, Comparable section,

int index) return "当前书籍销售量: " + dataset.getValue(section);

; pageContext.setAttribute("pieToolTipGenerator", pieTG);

创建了一个 PieToolTipGenerator 类的对象 pieTG,该对象表示饼图的提示工具条。其中

只有一个返回字符串对象的方法 generateToolTip。 PieDataset类的 getValue方法有多个版本 (详

见 JFreeChart Javadoc),这里提供给 getValue方法的参数是 java.lang.Comparable的一个对象,

表示“键/值”中的键,该方法返回该键的键值(java.lang.Number 类)。

然后,程序第 63 行以及第 87行:

<cewolf : map tooltipgeneratorid="pieToolTipGenerator"/>

通过增加一个<cewolf:map>标签,指定属性“tooltipgeneratorid”的值为“pieToolTip Generator”。该值必须和程序在第 63 行的 setAttribute方法中,设定的 pieTG 在 pageContext

Page 412: Java Web动态图表编程

中的属性名相同。这样,就生成了提示工具条。

程序第 96 行:

showlegend="false">

属性 showlegend 用于设定是否显示图例, “false”表示不显示图例;默认值为“true” ,

则显示图例。

8.4.6 Cewolf 生成基于 XYDataset 数据集的图表

同样,利用 XYDataset 数据集对象也可以创建几种类型的图表。因此,我们也把创建 XYDataset 类的数据集对象的代码,单独写成一个 CewolfXYDataset.jsp (\chap08\Fig8.4 \Fig.8.4_04)文件。

CewolfXYDataset.jsp的源程序很清晰,请读者自行阅读。

1.Cewolf 生成基于 XYDataset 数据集的线段图表

CewolfXY1.jsp (\chap08\Fig8.4\Fig.8.4_04)演示了如何利用 XYDataset 对象生成走势图的

方法。

运行结果如图 8.38 所示。 CewolfXY1.jsp的源程序没有难度,也请读者自行阅读。

2.Cewolf 生成基于 XYDataset 数据集的区域图表

CewolfXY2.jsp(\chap08\Fig8.4\Fig.8.4_04)演示了如何利用 XYDataset 对象生成区域图的

方法。

运行结果如图 8.39 所示。

Page 413: Java Web动态图表编程

图 8.38 Cewolf:XYDataset线段图

图 8.39 Cewolf:XYDataset区域图

这里需要注意程序第 23 行:

<cewolf:texturepaint image="/bg.jpg" width="60" height="60" />

利用 Cewolf标记 texturepaint 的“image”属性,将 bg.jpg图像以材质贴图的方式填充到

整个图表的背景区域中,属性“width”和“height”分别强制将 bg.jpg 图像的宽度和高度设

置为 60 像素。

3.Cewolf 生成散列图表

CewolfXY3.jsp (\chap08\Fig8.4\Fig.8.4_04)演示了如何利用 XYDataset 对象生成散列图的

方法。

运行结果如图 8.40 所示。 CewolfXY3.jsp的源程序也很清晰,请读者自行阅读!

Page 414: Java Web动态图表编程

4.Cewolf 生成时序图表

CewolfXY4.jsp (\chap08\Fig8.4\Fig.8.4_04)演示了如何利用 XYDataset 对象生成时序图的

方法。

运行结果如图 8.41 所示。

图 8.40 Cewolf:XYDataset散列图 图 8.41 Cewolf:时序图与移动平均线

本例在程序第 12 行~第 43 行,我们新增加了一个创建 TimeSeriesCollection 类的对象 dataset 方法。TimeSeriesCollection 类也是实现了 XYDataset 类接口的一个子类。该类对象的

创建,我们在本章例程 jMovingAverage.jsp(\chap08\Fig8.2\Fig.8.2_16)中已经介绍过,此处

不再赘述。

需要提醒读者的是,本例没有对坐标轴标题的属性进行设置,因此最后生成的图表中,

不包含横轴和纵轴的标题。

5.Cewolf 生成 XYDataset 的直方图表

CewolfXY5.jsp (\chap08\Fig8.4\Fig.8.4_04)演示了如何利用 XYDataset 对象生成直方图的

方法。

运行结果如图 8.42 所示。

Page 415: Java Web动态图表编程

图 8.42 Cewolf:XYDataset直方图

同样,在程序第 12行~第 26行,我们也新增加了一个创建 TimeSeriesCollection类的对

象 dataset 方法。本例也没有对坐标轴的标题属性进行设置,因此最后生成的图表中,不包含

横轴和纵轴的标题。

8.4.7 Cewolf 生成基于 OHLCDataset 数据集的图表

CewolfOHLC.jsp (\chap08\Fig8.4\Fig.8.4_05)演示了如何利用OHLCDataset对象生成K线

图(CandleStick),以及高低图(HighLow)的方法。

我们前面阐述过,OHLCDataset 主要用于财经类图表的绘制。 CewolfOHLC.jsp的运行结果如图 8.43 所示。

其中,图 8.43①所示的是 K线图,图 8.43②所示的是高低图。

程序第 32 行~第 65行,创建 OHLCDataset 对象的方法。注意,我们在该方法中创建了 dates 类的对象表示的是分钟。该 dates 数组中的对象又是通过调用程序第 54 行中 Calendar 类的对象 cal 的 getTime方法获得的。程序第 53 行,Calendar 对象 cal 的 roll 方法,表示每次

向后滚动 1 分钟,因为我们指定其第一个参数表示分钟的 Calendar 类的静态变量 Calendar.MINUTE。第二个参数表示第一个参数对象的滚动幅度(间隔)。该参数值为 1,表

示向后滚动 1 分钟,如果该值为负数,则表示向前滚动相应的间隔。

Page 416: Java Web动态图表编程

图 8.43 Cewolf:CandleStick/HighLow图

8.4.8 Cewolf 生成甘特图表

CewolfGantt.jsp(\chap08\Fig8.4\Fig.8.4_06)演示了如何生成甘特图的方法。 CewolfGantt.jsp的运行结果如图 8.44所示。

Page 417: Java Web动态图表编程

图 8.44 Cewolf:甘特图

注意,本例程中在创建 TaskSeriesCollection对象 ds 时,Date类的对象的生成方法。

8.4.9 Cewolf 生成信号图表

CewolfSignal.jsp(\chap08\Fig8.4\Fig.8.4_07)演示了如何生成信号图的方法。 CewolfSignal.jsp的运行结果如图 8.45 所示。

图 8.45 Cewolf:信号图

CewolfSignal.jsp的源程序清单如下:

注意,程序第 72 行~第 73 行:

<img src='<cewolf:imgurl chartid="signals" renderer="/cewolf" width="500" height="375" mime="image/png"/>'>

与以往调用 Cewolf的<cewolf:img>标签不同,本例首先是直接调用 HTML中 img标签,

Page 418: Java Web动态图表编程

然后在其 src属性中,再调用<cewolf:imgurl>标签来设置图像的相关信息。相比之下,我们更

喜欢直接使用<cewolf:img>标签。

8.4.10 Cewolf 生成速度图表

CewolfMeter.jsp(\chap08\Fig8.4\Fig.8.4_08)演示了如何生成速度图表的方法。 CewolfMeter.jsp的运行结果如图 8.46 所示。

图 8.46 Cewolf:速度图

CewolfMeter.jsp的源程序请读者自行阅读。

8.4.11 Cewolf 生成 OverLay 类型的图表

Cewolf生成的 OverLay类型的图表与 JFreeChart 中的多轴图表类似,即允许多个不同风

格的图表对象绘制在同一个 Plot 里。这种图表,在 Cewolf 里被称为 OverLay 图表。但是,

没有发现让 OverLay图表像 JFreeChart 那样支持坐标映射的功能。 此外, 在 OverLay图表中,

适用的数据集类型为 XYDataset 类的对象。 CewolfOverLay.jsp (\chap08\Fig8.4\Fig.8.4_09)演示了如何生成 OverLay类型图表的方法。

我们在这里用直方图来绘制 2005 年 1 月份国际原油每天的价格。 此外, 用线段图来绘制该月

原油价格 5天的移动平均价。 CewolfOverLay.jsp的运行结果如图 8.47 所示。

Page 419: Java Web动态图表编程

图 8.47 Cewolf:OverLay类型图表

本例中,关于 TimeSeriesCollection对象的创建方法,我们在本章前面的例程中已经讲述

过了,请读者参考前文。

注意, 如果要创建 OverLay类型的图表, 必须使用 Cewolf标签库中的 overlaidchart 标签,

如程序第 95行所示:

<cewolf:overlaidchart

此外,程序第 98 行:

type="overlaidxy"

指定 OverLay类型图表的绘制风格为 overlaidxy。目前最新版的 Cewolf对于该属性也只

提供了一个可选值,就是“overlaidxy” 。

程序第 99 行~第 100行:

xaxistype="date" yaxistype="number"

指定当前 OverLay类型图表的坐标轴中,横轴和纵轴所对应的数据类型。这里只有两种

可能,日期型或数字型。其中,xaxistype属性表示横轴所对应的数据类型,该属性为必选项; yaxistype属性表示纵轴所对应的数据类型, 该属性为可选项。 我们建议读者不要忽略 yaxistype 属性,最好视其也为必选项,并提供正确的数据类型。

程序第 105行~第 115 行:

<cewolf:plot type="xyverticalbar"> <cewolf:data>

<cewolf:producer id="barData" /> </cewolf:data>

</cewolf:plot>

<cewolf:plot type="xyline"> <cewolf:data>

<cewolf:producer id="movingLineData" /> </cewolf:data> </cewolf:plot>

Page 420: Java Web动态图表编程

这段标签用于绘制两个基于 XYDataset 的图表对象。 分别用 plot 标签的 type属性指定图

表的绘制风格,用 producer 标签的 id属性指定所绑定的数据集对象。这里首先绘制一个直方

图,其次绘制移动平均线图。

8.4.12 Cewolf 生成组合图表

CewolfCombine.jsp(\chap08\Fig8.4\Fig.8.4_10)演示了如何生成复合图表的方法。本例

使用的数据集对象和前例完全相同。 使用直方图来绘制 2005年 1月份国际原油每天的价格,

另外,用线段图来绘制该月原油价格 5天的移动平均价。 CewolfCombine.jsp的运行结果如图 8.48所示。

程序第 94 行~第 114 行的标签,绘制了图 8.48①所示的水平组合图表。

注意,如果要创建组合图表,必须使用 Cewolf 标签库中的 combinedchart 标签,如程序

第 94 行所示:

<cewolf:combinedchart

此外,程序第 96 行:

layout="horizontal"

属性 layout 指定组合图表的布局风格。它有两个可选项:horizontal 表示子 Plot 对象之间

按水平方向排列;vertical 表示子 Plot 对象之间按垂直方向排列。

程序第 98 行:

type="combinedxy"

Page 421: Java Web动态图表编程

图 8.48 Cewolf:水平和垂直组合图

指定组合类型图表的绘制风格为 combinedxy。目前最新版的 Cewolf 对于该属性也仅仅

提供了一个可选值,就是“combinedxy” 。

程序第 102行~第 112 行的标签,首先绘制了一个直方图,然后绘制了一个移动平均线

图。

程序第 118 行~第 138 行的标签,绘制了图 8.45②所示的垂直组合图表与水平组合图表

的过程类似,只是将 layout 属性值改变为 vertical。

Page 422: Java Web动态图表编程

8.4.13 生成自定义风格的 Cewolf 图表

前面说过,Cewolf和 JFreeChart 对图表中所有的绘制对象默认都进行了反锯齿的处理。

因此,绝大多数情况下,如果图表中包含有中文字符,则中文字符的最终显示效果并不理想。 Cewolf 所采用的默认字体的大小为 10 磅,因此,即使关闭了反锯齿功能,中文字体的轮廓

还是比较模糊。我们可以在 JFreeChart 中,通过重新设置字体的大小、风格、颜色等属性来

改善中文字符的绘制效果。此外,除了可以任意设置字体对象的属性外,在 JFreeChart 中,

我们还可以自定义图表中任意一个绘制对象所应用的颜色对象、绘制角度对象,以及渲染对

象等。综合调整及设置这些对象的各种属性,就可以比较完美地生成一幅图表。但是,如何

在 Cewolf中,也实现这些功能呢?

我们提供了一个在 Cewolf 下,比较完善地演示如何设置各种绘制属性的源程序 Custom Cewolf.jsp (\chap08\Fig8.4\Fig.8.4_11)。该程序以绘制甘特图的源代码为蓝本,新增加了一个

设置绘制属性的对象 chartParameter。通过该对象,我们可以控制所有的绘制属性。

首先请看,经过自定义绘制属性后,CustomCewolf.jsp的运行效果,如图 8.49 所示。

图 8.49 Cewolf:自定义图表的绘制属性

现在,我们获得了不错的绘制效果。这是如何实现的呢?看过 CustomCewolf.jsp的源程

序清单就明白了:

程序的核心有两点:

(1)程序第 166 行:

<cewolf:chartpostprocessor id="chartParameter"/>

利用 Cewolf标签库中的 chartpostprocessor 标签,设置当前图表的渲染属性。

(2)chartpostprocessor 标签属性 id指定由 id为“chartParameter”的 ChartPostProcessor 对象来负责整个图表的渲染工作。然后,被程序第 80 行~第 148 行,所设定的 ChartPost Processor 对象 chartParameter 所处理。

程序第 85 行:

Page 423: Java Web动态图表编程

JFreeChart jChart = (JFreeChart)chart; 首先,将程序第 80 行参数中的类型为 Object 的 chart 参数转换为一个 JFreeChart 对象

jChart。转换方法很简单,使用“(JFreeChart) chart”的方法即可。这是所有步骤中的第一步,

也是最重要的一步。后续的应用方法,都是我们在 JFreeChart 中反复多次使用过的一些方法。

如程序第 86行:

jChart.setAntiAlias(false);

关闭图表的反锯齿功能。 程序第 89 行~第 91 行, 对整个图表设置了一个渐进色的背景。

程序第 94 行~第 96行,设置图表标题的绘制字体及颜色等属性。程序第 99 行~第 100 行,

获得图表的绘制对象 plot 及渲染对象 renderer。

程序第 103行~第 111 行,设置甘特图中,各个小直方块的绘制颜色。

程序第 114 行~第 115 行,设置图表绘制区域的背景效果为另一个渐进色。

程序第 116 行~第 119 行,设置水平方向的网格线可见,并设置其颜色为蓝色,设置垂

直网格线的绘制颜色为红色。

程序第 122行~第 136行, 设置坐标轴上标题及刻度值文字所应用的字体及颜色等属性,

同时还设置了刻度值(日期)的显示格式。

程序第 139行~第 145 行,设置图例的显示属性,如小图标的大小、图例中文字的字体

等。

从本例中,读者可以看出,如果要在 Cewolf中设置各种绘制属性,则必须创建上述相关

对象及方法,而这些方法又都是 JFreeChart 的相关方法。

8.5 本章小结

本章通过丰富的例程,向读者详尽地讲解了两种优秀的 Web 图表生成引擎 Jfree Chart/Cewolf 的使用方法。通过对例程的学习,在实际的开发工作中,我们可以轻松地使用 JFreeChart 作为 Web 的图表生成引擎。要掌握好 JFreeChart 的应用,必须对其运行机制、类

库及相关方法熟练掌握才行。

尽管 JFreeChart/Cewolf 可以生成丰富的 Web 图表,但我们无法新建 JFreeChart/ Cewolf 中没有提供的 Web 图表类型。因此,在了解如何使用 JFreeChart/Cewolf的同时,读者还应掌

握本章以前所讲述的自己如何编写 Web 图表的方法。这样,当调用 JFreeChart/Cewolf,无法

完成或者不易完成图表的绘制要求时,我们就可以自己动手来编写相应的代码。

只要掌握了本书提供的例程,相信读者能够举一反三,绘制出自己需要的图表。

Page 424: Java Web动态图表编程

第 9 章 Web 图表生成引擎

的设计思路与实现

到目前为止,我们对 JFreeChart 图表生成引擎已经很熟悉了,感受到了 JFreeChart 在使

用上的复杂性以及性能上的强大性。那么,我们是否能够设计一个跨平台的、具有类似 JFreeChart 的基本功能,但使用简单的 Web 图表生成引擎呢?对于跨平台,Java 当然是最佳

的选择。 但是, 如何实现 Web 图表生成引擎呢?想必这是很多读者都非常感兴趣的一个领域。

本章提供了一个简单的、被封装成 JavaBean 的、实现了 Web 动态图表生成引擎的最基

本功能的设计思路及其实现方式。 它具有使用和开发都很简单、 易于发布/部署、 扩展性强 (虽

然仅仅演示了普通/3D 线段图、普通/3D 直方图、普通/3D 饼图的生成方法,但可以轻易地扩

展其他风格图表的生成方法) 、高性能(最终被转换成 Servlet 在 Web 应用服务器中驻留,不

在 Web 服务器中生成图像文件) 、模块化(数据与图表绘制部分的代码分离,以及可以通过

多个生成不同图表的引擎,相互合作而创建更复杂的图表) 、跨平台(本图表生成引擎可以运

行于 Linux/Unix/Windows/FreeBSD 等主流操作系统)等 特点。

9.1 Web动态图表生成引擎的设计思路

通过 JFreeChart 的学习,相信读者对 Web 动态图表生成引擎有了非常直观的认识。为了

更简单、直接、清晰地演示图表生成引擎最核心的设计思路,本章我们将以第 7 章讲

解的线段图、直方图和饼图等相关例程为蓝本,与此同时,我们约定此图表生成引擎正

常工作的前提是:

(1)图表绘制区域(plot)的大小和位置都固定不变,不因整个图表的大小改变而自动

调整图表绘制区域(plot)的大小和位置。

(2)绘制图表所需的数据集结构与第 7 章的相关例程相同。

我们关注以下几个方面:

Ø 图表生成引擎在上述两个前提下可以正常工作。

Ø 通过该引擎可以生成不同类型的图表(线段图、直方图、饼图并可任意扩展)。

Ø 如何实现模块化,即数据与图表绘制部分的代码分离。

这三点是图表生成引擎的核心问题。

其实,我们在第 5 章学习了一些如何编写及调用 Servlet 来生成 Web 图表的方法,例如:

编写了生成三角形(chap05/Fig5.2/Fig.5.2_02/)、水平直方图(chap05/Fig5.2/Fig.5.2 _04/)、

登录认证码 1(/chap05/Fig5.6/Fig.5.6_01/)以及登录认证码 2(/chap05/Fig5.6/Fig.5.6 _02/)

等 Servlet。实际上,这些 Servlet 可以被看做是一种特殊的图表生成引擎,只是每一个 Servlet 只能生成一种特定类型的图表而已。

我们在第 6 章、第 7 章,也多次调用绘制三角形的 TriangleServlet 来生成坐标轴上的箭

头,也多次调用绘制平行四边形的 Servlet(\chap05\Fig5.9\DrawParallelogram.java)来绘制具

有 3D 立方体及 3D 坐标的横轴与纵轴。可见,如果编写了一个可以生成某种类型图表的 Servlet,就可以在其他程序或图表生成引擎中进行调用,从而达到编写图表生成引擎的目的。

Page 425: Java Web动态图表编程

也就是说,可以通过编写 Servlet 的途径来达到编写图表生成引擎的目的。 Servlet 部署比较麻烦,如果要部署 Servlet,我们必须修改 WEB­INF\web.xml 文档,将

该 Servlet 注册在 Web 服务器中。如果改写的 web.xml 文档有错误,则可能导致整个 Web 服

务器瘫痪或无法启动。基于上述原因,最好将实现上述图表生成功能的 Servlet 改写成 JavaBean的形式来实现图表引擎设计。JavaBean的编写具有比 Servlet 简单、性能相同,而且

不需要改写 web.xml 等优点。

提示:本章建议读者使用 Resin 作为 JSP/Servlet 的容器。在修改了 Java Bean 的源代码

后,Resin 不需要重新启动,一旦源代码更新,将自动进行编译,便于我们调试程序。如果

读者使用 Tomcat,在测试或运行本章例程的过程中,无法获得正确的运行结果,也请换用 Resin服务器。

下面,我们编写一个绘制矩形的 JavaBean,了解基于 JavaBean 的 Web 图表引擎的实现

方法。先请看程序的运行结果。首先在 WEB­INF/classes 目录下,新创建一个目录,该新建

目录名为 chart。然后把源程序 WebChartBean1.java( \chap09\Fig9.1)复制到 WEB­ INF\classes\chart 下。启动 Resin 服务器运行 IE,在地址栏中输入:http://localhost:8888/ chap09/Fig9.1/beanChart1.htm,程序运行结果如图 9.1所示。

或者,在 IE 地址栏中输入:http://localhost:8888/chap09/Fig9.1/beanChart1.jsp,运行结果

如图 9.2 所示。

两者只是调用图表的方式不同。 WebChartBean1.java 的源程序与前面学过的 Servlet 很相似。

程序第 4 行:

package chart;

Page 426: Java Web动态图表编程

图 9.1 JavaBeans生成Web图表 1

图 9.2 JavaBeans生成Web图表 2

限定将本程序放置在WEB­INF/classes/chart 目录下。 这也是为什么要先创建一个 “chart”

子目录的原因。

程序第 15 行:

public class WebChartBean1 extends HttpServlet

注意,WebChartBean1 类继承了 HttpServlet 类,因此程序第 11 行~第 12 行:

import javax.servlet.*; import javax.servlet.http.*;

将相关的 Servlet 类引入。

程序第 17 行:

Page 427: Java Web动态图表编程

protected HttpServletResponse response;

定义了一个 HttpServletResponse类对象 response,表示向客户端返回的数据对象。

程序第 19 行~第 20行:

static int width = 500, height = 400; static int plotWidth = 400, plotHeight = 300;

整型变量 width 表示整个图表的宽度为 500 像素,height 表示整个图表的高度像素。 plotWidth 表示图表绘图区域的宽度,plotHeight 表示图表绘图区域的高度为 300 像素。本章

例程绘制图表的相关数据是固定不变的。

程序第 28 行:

public WebChartBean1()

声明了本 JavaBean的默认构造器,默认的构造器是不进行任何工作的。

程序第 30 行~第 34行:

public WebChartBean1(HttpServletResponse response) this.response = response; this.initialize();

声明了另一个版本的构造器,该构造器接收一个 Servlet 类的对象 response。首先将参数

提供的 response赋值给 JavaBean程序第 17行定义的系统变量 response, 然后, 调用 JavaBean 中程序第 37行~第 62 行的 initialize方法,对图表进行处理,包括获取 Graphics2D 对象、初

始化背景、绘制图表标题等。

程序第 64 行~第 83行:

public void getChart() // 部署图形环境 g2d.dispose();

// 输出图像到 WEB页面 try this.response.reset(); this.response.setContentType("image/png"); OutputStream sos = response.getOutputStream(); ImageIO.write(image, "PNG", sos);

sos.close(); catch (Exception e) System.out.println("图表输出错误!");

getChart 方法,用于将缓冲图表对象编码后发送给客户端。这些代码我们都很熟悉了,

只是需要注意两点:

Ø 必须在将缓冲图像返回给客户端之前,清空以前缓冲区的内容,以保证返回正确格式

的图形对象。

Ø 输出对象到客户端的时候,可能会发生异常,因此需要将代码放置在 try…catch代码

块中。以确保系统可以捕获异常。

请看调用该 JavaBean的 beanChart1.jsp(\chap09\Fig9.1)程序的用法:

程序第 6 行:

Page 428: Java Web动态图表编程

import="chart.WebChartBean1"

引入我们前面编写的WebChartBean1 这个 JavaBean。注意,JavaBean的正确路径。如果

需要创建某个 JavaBean的实例,则必须先将其引入到程序中。

程序第 9 行:

<jsp:useBean id="webChart" class="chart.WebChartBean1" scope="page" />

创建 WebChartBean1 的实例 webChart,并指明其作用范围为当前页面。

程序第 12 行~第 13行:

webChart = new WebChartBean1(response); webChart.getChart();

因为已经创建了 WebChartBean1 的实例 webChart,因此可以在程序第 12 行直接调用 JavaBean,WebChartBean1 的另一个版本的构造器重新对 webChart 进行赋值。我们提供的参

数 response 也是 Java Servelet 保留的对象名之一。程序第 13 行,通过调用 JavaBean 的对象 webChart 的 getChart 方法,获取服务器端返回的图表对象。

如果没有程序第 9 行,则程序第 12 行可改写成如下的形式,同样可以创建 JavaBean 的

实例。

WebChart webChart = new WebChartBean1(response);

因此,当我们直接在浏览器中通过:http://localhost:8888/chap09/Fig9.1/beanChart1.jsp 的

方式访问该 JavaBean 的时候,就得到了如图 9.3 所示的运行结果。实际上,beanChart1. jsp 返回一个 png 图像,因此,我们也可以使用传统的 HTML 中 IMG 标签的标准用法来获取显

示该图像,如 beanChart1.html(\chap09\Fig9.1)第 10 行代码所示:

<IMG SRC="beanChart1.jsp" ALT="无法显示图表">

综上所述,实现这个 JavaBean的关键有以下两点:

(1)需要继承 HttpServlet 类;

(2)将客户端的 response对象赋值给 JavaBean的 response对象。

下面介绍一些比较复杂的生成图表的 JavaBean。

9.2 Web动态图表生成引擎的设计模型

9.2.1 生成普通线段图的 JavaBean

现在我们来学习一些比较复杂的 JavaBean。这里指的复杂,是指程序的结构。我们重新

编写构造器的方法,在构造器中,通过调用一系列的方法来完成图表绘制的相关工作,在 JavaBean中实现生成图表的步骤和思路完全一样。 前例在构造器中仅仅调用了 initialize方法,

而在 initialize方法中完成了绘制图表所需的所有工作。 本例在构造器中包含了如下一些方法,

这些方法执行相关图表的绘制任务如下:

Ø initialize():用于创建缓冲图像对象以及获取缓冲图像的绘制环境;

Ø drawBackground():用于绘制整个图表的背景;

Ø drawTitle():用于绘制图表的标题;

Ø drawChart():用于绘制图表的主体部分,这部分也是我们常说的图表绘制区域(plot 绘制区域);

Ø drawAxisLabel():用于绘制图表坐标轴的标题。

Page 429: Java Web动态图表编程

此外,在 drawChart 方法中,我们通过调用 drawLegend方法来绘制图例。

本节例程 LineChartBean.java (\chap09\Fig9.2\Fig.9.2_01),是根据第 7 章的线段图 lineChart.jsp(\chap07\Fig7.4\Fig.7.4_01)而改写生成线段图的 JavaBean。

同样,把源程序 LineChartBean.java 复制到 WEB­INF\classes\chart 下。运行 IE,在地址

栏中输入:http://localhost:8888/chap09/Fig9.2/Fig.9.2_01/LineChartBean.jsp。

当然,也可以输入:http://localhost:8888/chap09/Fig9.2/Fig.9.2_01/LineChartBean.html。

本HTML和前例所介绍的完全相同, 也是在<img>中设置 “src” 属性为 “lineChart Bean.jsp”

来获取图表。此处不再赘述。

我们以浏览 LineChartBean.jsp (\chap09\Fig9.2\Fig.9.2_01)页面而获取 JavaBean 生成的图

表为例,如图 9.3 所示。

图 9.3 JavaBeans生成线段图表

在 LineChartBean.jsp中,并没有<jsp:useBean>标记。因此,通过程序第 10行:

LineChartBean lineChart = new LineChartBean(response);

创建 JavaBean的实例。这时,便会调用 LineChartBean.java 程序第 34 行~第 42 行之间

的构造器。当实例对象 lineChart 被创建好后,程序第 11 行,调用 getChart 方法,显示该图

表。

当调用 LineChartBean.java 程序第 34 行~第 42 行之间构造器方法的时候,又分别调用

了前面我们所讲述的 5 个方法来完成图表的绘制工作。

这里简要介绍这些方法所完成的图表绘制任务。

程序第 19 行和第 20行,4个整型变量:width表示图表的宽度,height 表示图表的高度, plotWidth表示图表绘制区域的宽度,plotHeigth表示图表绘制区域的高度。

程序第 22 行~第 24 行,字符串变量:xAxisLabel 表示横轴的标题,yAxisLabel 表示纵

轴的标题,chartTitle表示图表的标题。

在绘制图表背景 drawBackground的方法中,程序第 97 行:

drawArea = new Rectangle2D.Float(80, 30, plotWidth, plotHeight);

定义图表绘制区域的大小及位置。按照前面阐述的前提,图表绘制区域的大小和位置都

Page 430: Java Web动态图表编程

是固定的。因此,本例及后面的例程都具有相同的大小和位置,即图表绘制区域为矩形,其

左上角坐标为(80,30),宽度为 400 像素,高度为 300 像素。

图表背景绘制结束后,继续调用程序第 112 行~第 119 行的 drawTitle方法绘制标题。接

着调用程序第 122 行~第 261 行的 drawChart 方法绘制图表的主体部分。这部分的代码综合

运用了我们在第 7 章所学习的一些 Java2D 的绘制方法。

程序第 125行:

AffineTransform old = g2d.getTransform();

调用 graphics2D 的 getTransform 方法,获得当前绘图环境、在进行坐标平移前 AffineTransform对象的一个副本。程序第 126行,调用 graphics2D 的 translate方法,将坐标

轴原点平移至(80, 30)。

程序第 138 行的整型变量 xTickCounter,表示横轴上要绘制多个刻度(tick)。本例用于

显示 2004 年中每个月的计算机书籍销售量,因此这里设置该变量的值为 12。程序第 141 行

的整型变量 plotMargin表示图表绘制区域和纵轴之间的空白为 10像素。 我们从图 9.3 中可以

看到,图表绘制区域的两端各有一段空白,本来两端的空白应该相同,各为 10像素,但为什

么这里左右两端空白的宽度有所不同呢?请看程序第 144 行:

int hSpace = (plotWidth ­ 2 * plotMargin) / (xTickCounter ­ 1);

整型变量 hSpace,用来表示横轴上每一刻度的长度。因为是整形变量,所以这里使用的

计算方法是:(plotWidth ­ 2 * plotMargin)/(xTickCounter ­ 1)就会舍弃结果的小数部分而

得到一个整数结果。这样,累计到一定的程度,则会出现右端的空白长度大于左端空白长度

的现象。解决的办法是在这里采用 float 或者 double 类型的变量。

同理,程序第 169 行的整型变量 vTickCounter,表示纵轴上将要绘制的刻度个数。程序

第 172 行的整型变量 vSpace表示纵轴上刻度之间的空白。

程序第 213行:

g2d.setTransform(old);

绘图区域的线段图绘制结束后,再恢复到坐标系平移前的状态。之后,程序第 216行:

drawLegend(bookTitle, bookColor); 调用 drawLegend 方法绘制图例。该方法位于程序第 220 行~第 261 行,接收两个字符

串数组参数 bookTitle 和 bookColor,这两个参数分别对应于图例中每一项的说明文字及其绘

制颜色,用于绘制位于图表下方的图例。然后,在构造器中继续调用程序第 264 行~第 284 行的 drawAxisLabel 方法,来绘制坐标轴的标题。实现纵轴标题垂直绘制功能的是程序第 276 行:

g2d.rotate( ­ Math.PI / 2);

指定绘制的角度为负 90 度即可。本程序中的其余代码,读者可以参考第 7 章的相关例

程。 此外, 还可以修改程序中第 138行, 以及第 169行的整型变量 xTickCounter和 yTickCounter 的值,观察图表的绘制结果会发生什么变化。

9.2.2 生成 3D 线段图的 JavaBean

将 LineChart3DBean.java(\chap09\Fig9.2\Fig.9.2_02)复制到\WEB­INF\classes\chart 子目

录下。运行 IE,在地址栏中输入:http://localhost:8888/chap09/Fig9.2/Fig.9.2_02/Line Chart 3DBean.jsp,运行效果如图 9.4所示。

Page 431: Java Web动态图表编程

图 9.4 JavaBeans生成 3D线段图表

实现 3D 效果的线段图时,我们在 LineChart3DBean.java 的基础上增加了两个整型变量: thickness(程序第 27 行)和 angle(程序第 30 行),分别表示线段阴影的厚度及角度。另外,

还增加了相关绘制阴影所必需的方法:drawLineShadow(程序第 307 行~第 333 行)方法、 getOffsetX(程序第 336 行~第 339行)方法、getOffsetX(程序第 342行~第 345 行)方法。

这 两 个 整 型 变 量 及 其 相 关 方 法 , 都 是 直 接 从 第 7 章 的 lineChart3D2. jsp (\chap07\Fig7.4\Fig.7.4_02)程序中直接拷贝过来的,并没有对其进行任何修改。读者还可

以修改程序中的整型变量:xTickCounter、yTickCounter、thickness 和 angle 的值,观察图表

的绘制结果会发生什么变化。 LineChart3DBean.java 的源程序清单,因为没有新的内容,只是两个程序的组合。因此,

我们就不在这里列出其源代码,请读者自行下载源代码,阅读相应的源程序。

Page 432: Java Web动态图表编程

9.2.3 生成普通直方图的 JavaBean

这里我们以第 7 章所介绍的普通垂直柱状图的源程序 vBarChart.jsp (/chap07/Fig7.5/ Fig.7.5_04/)为蓝本,将其改写成 JavaBean。

同样,把源程序 VBarChartBean.java (\chap09\Fig9.2\Fig.9.2_03)复制到 WEB­INF\ classes\chart 子目录下。然后运行 IE,在地址栏中输入 http://localhost:8888/chap09/Fig9.2/ Fig.9.2_03/VBarChartBean.jsp。

或者 http://localhost:8888/chap09/Fig9.2/Fig.9.2_03/VBarChartBean.html。

这里以浏览 VBarChartBean.jsp 的页面而获得 JavaBean 生成的垂直直方图表为例,如图 9.5 所示。

图 9.5 JavaBeans生成垂直直方图表

本例不仅增加了一些新方法,而且还改写了原有的一些方法,实现了一些新的功能。如

可以自定义图表、横轴和纵轴标题的内容,以及根据数据而自动调整直方块的绘制参数等。

这里我们先来看调用 JavaBean 的 VBarChartBean.jsp(\chap09\Fig9.2\Fig.9.2_03)的源程序清单

如下:

1: <!­­ 2: Fig. 9.2_03_02: VBarChartBean.jsp 3: 功能: 调用 JavaBeans获取垂直图表 4: ­­> 5: <%@ page language="java" contentType="image/png;charset=GB2312" 6: import="chart.VBarChartBean" 7: %> 8: 9: <% 10: VBarChartBean vBarChart = new VBarChartBean(response); 11: vBarChart.setChartTitle("垂直直方图 Bean"); 12: vBarChart.setDomainAxisLabel("统计时间:2005年第 1季度"); 13: vBarChart.setRangeAxisLabel("月销售量"); 14: vBarChart.getChart(); 15: %>

在 VBarChartBean.jsp 调用 JavaBean 的方法中,调用了三个分别用于设置图表标题内容

Page 433: Java Web动态图表编程

的 setChartTitle 方法;设置横轴标题的 setDomainAxisLabel 方法,以及设置纵轴标题的 setRangeAxisLabel 方法。

VBarChartBean.java 中和以前我们在构造器中,调用以下方法: Initialize、 draw Background、drawTitle、drawChart、drawAxisLabel,现在被放置在程序第 338 行~第 363 行

的 getChart 方法中。此外,程序第 55 行~第 70 行,相关设置图表标题、横轴标题和纵轴标

题只是一个 setXXXXX 方法。

本例设置的参数较多,请看图 9.6 所示的各参数示意图。下面主要讲述程序第 151 行~

第 270 行的 drawChart 方法。

程序第 154行~第 155 行,保存当前坐标的属性后,将坐标原点平移到(80,30)。

程序第 173行~第 181 行:

int hSpace; if (xTickCounter == 1) hSpace = plotWidth;

else hSpace = (plotWidth ­ 2 * plotMargin) / (xTickCounter ­ 1);

这里我们使用新的方法来计算横轴上每一个刻度的长度。在以前绘制线段图的时候,我

们仅仅采用程序第 180 行所示的计算公式,这样存在一个隐患,如果 xTickCounter 的值为 1,

则会发生异常。因此,这里我们设定,当 xTickCounter 的值为 1 时,刻度的长度正好等于 plotWidth,即 400 像素。

程序第 185行、第 188行、第 191行~第 192 行,定义了两个整型变量:barOuterMargin 和 barInnerMargin,分别表示如图 9.6 所示的每组直方图之间的空白;每组直方图中,每个直

方块之间的空白。明白了相关参数的含义,程序就容易理解了。

程序的核心是如何计算直方图中, 每一个直方块的绘制位置和绘制宽度。 程序第 191行~

第 192 行:

int barLength =(hSpace ­ barOuterMargin ­ series.length * barInnerMargin) / series.length;

Page 434: Java Web动态图表编程

图 9.6 相关变量的含义示意图

定义了整型变量 barLength, 表示如图 9.6 所示的每组直方图中, 每个直方块的绘制宽度。

程序第 238行:

double totalBarLength = series.length * (barLength + barInnerMargin);

定义了双精度变量 totalBarLength,表示每组直方图中,所有直方块及其相邻直方块之间

的空白长度总和。

程序第 252行~第 253 行:

double xVBarPosition = plotMargin + i * hSpace – totalBarLength / 2 + j * (barLength + barInnerMargin);

双精度变量 xVBarPosition,表示每组直方图中,每个直方块的横坐标。计算公式中,直

方块的总长度 totalBarLength / 2,表示 xTickPosition 所在的位置。也就是说,所有的直方块

是以 xTickPosition所在位置的垂直线为中心均匀分布的。

程序第 255行:

g2d.setColor(paintColor[j % paintColor.length]);

这里设置直方块绘制颜色的方法和以前有所不同。调用了一个求模的运算来确定直方块

的绘制颜色。这样设置的原因很简单,如果需要绘制的直方块数量超过了我们所提供的颜色

数组变量:paintColor(程序第 35 行~第 39 行定义)的个数,则会循环使用颜色数组中所提

供的 Color 对象来绘制直方块。

程序第 258行~第 259 行:

Rectangle2D.Double bar = new Rectangle.Double(xVBarPosition, sales[j], barLength, plotHeight ­ sales[j]);

创建了一个 Rectangle2D.Double类的对象 bar, 表示每个直方块的绘制参数: 左上角坐标、

绘制宽度和高度等。程序第 261 行,调用 Graphics2D 的 fill 方法,填充该 bar。

程序的其他部分都是熟悉的内容,请读者自行阅读。读者可以修改程序第 26 行~第 33 行的两个字符串数组 series 和 category的内容,查看其运行结果的变化。注意,不能在 series

Page 435: Java Web动态图表编程

和 category数组中设置过多的内容, 否则在生成直方图中直方块的宽度非常细,无法看清楚。

解决这个问题的思路是,预定义一个直方块的最小绘制宽度。如果计算出来的直方块的绘制

宽度小于预定义的最小绘制宽度,则自动扩展图表绘制区域,及 plot 区域的宽度。

9.2.4 生成 3D 直方图的 JavaBean

首先, 将 VBarChart3DBean.java (\chap09\Fig9.2\Fig.9.2_04)复制到\WEB­INF\classes\ chart 子目录下。 本例可以生成普通的 3D 直方图表, 也可以生成具有透明效果的 3D 直方图。 其中,

透明度可以任意设定。运行 IE,在地址栏中输入: http://localhost:8888/chap09/ Fig 9.2/Fig.9.2_04/VBarChart3DBean.jsp,生成普通 3D 直方图表的效果如图 9.7 所示。

图 9.7 JavaBeans生成 3D直方图表

这里我们可以看到调用 JavaBean 的 VBarChart3DBean.jsp(\chap09\Fig9.2\Fig.9.2 _04)

的源程序与前例相比,减少了一个调用 setChartTitle 的方法。因此,图 9.7 的运行结果中,没

有看到图表的标题。 我们在 VBarChart3DBean.jsp的程序第 12 行代码后, 加上以下一条语句,

就可以生成具有透明效果的 3D 直方图。如图 9.8 所示。

Page 436: Java Web动态图表编程

图 9.8 JavaBeans生成透明 3D直方图表

vBarChart3D.setAlphaComposite(0.75f);

调用 JavaBean的 setAlphaComposite方法,并提供一个 0.0~1.0之间的一个 float 参数就

可以了。

为了简要说明问题,实现绘制 3D 直方图及透明 3D直方图,我们没有编写新的方法,而

是直接套用了第 7 章绘制 3D 垂直直方图(/chap07/Fig7.5/Fig.7.5_04/vBar3DChart.jsp)中所

使用的相关方法。包括,如何通过引入绘制立方体的 Servlet:DrawParallelogram 来绘制 3D 坐标轴,以及 3D 直方块的方法。仅仅修改了方法中的一些变量值,为实现透明效果,只需

要给 Graphics2D 设置一个 AlphaCompoiste对象,并提供相应的 alpha 值即可。在本例中,我

们提供了一个 setAlphaComposite的方法(程序第 67行~第 70 行), 用来设置 alpha 值。alpha 默认值是 1.0,即绘制不透明的 3D 直方图表。所有的这些方法,都是我们非常熟悉的。因此,

本例不再讲解。

9.2.5 生成普通饼图的 JavaBean

我们以第 7章介绍的绘制普通饼图的源程序 pieChart.jsp(/chap07/Fig7.6/)为蓝本,将其

改写成 JavaBean。

首先,将源程序 PieChartBean.java(\chap09\Fig9.2\Fig.9.2_05)复制到 WEB­INF\ classes\ chart 子目录下。然后运行 IE,在地址栏中输入: http://localhost:8888/chap09/Fig9.2/ Fig.9.2_05/PieChartBean.jsp。

或者,http://localhost:8888/chap09/Fig9.2/Fig.9.2_05/PieChartBean.html。

这里以浏览 PieChartBean.jsp的页面而获得 JavaBean生成的普通饼图结果为例, 如图 9.9 所示。

Page 437: Java Web动态图表编程

图 9.9 JavaBeans生成普通饼图

程序中, 绘制饼图及其提示文字的方法, 全部来自第 7章所介绍的普通饼图的 pieChart.jsp 源程序,并且在结构上没有做任何改动。对于饼图来说,不需要显示坐标轴,因此,我们将

饼图的绘制区域适当加以扩大。此外,绘制饼图所需的数据集也比前面介绍的直方图、线段

图要简单些。因此,饼图是所有图表中最容易绘制的一种。

以前我们所提供的数据集都包含两个字符串数组和一个 int、float 或 double类型的数组。

而饼图只需要一个字符串数组和一个 int、float 或 double 类型的数组就可以了。程序在第 32 行~第 33 行,注释了字符串数组对象 series 的内容。因此,在绘制图例时,所提供的字符串

对象参数,就从原来的 series 改变为数组对象 category(见程序第 221 行)。

如果读者对于绘制饼图的相关方法还有不清楚的地方,请直接查阅第 7 章的相关内容。

Page 438: Java Web动态图表编程

9.2.6 生成 3D 饼图的 JavaBean

我们以第 7 章所介绍的透明 3D 饼图的源程序 transparentPie3D.jsp(\chap07\ Fig7.13 \Fig.7.13_01)为蓝本,将其改写成 JavaBean。本 JavaBean 具有自定义 3D 饼图的厚度和透明

度 的 功 能 。 首 先 , 将 源 程 序 PieChart3DBean.java(\chap09\Fig9.2\Fig.9.2_06) 复 制 到 WEB­INF\classes\chart 子目录下。然后运行 IE,在地址栏中输入:http://localhost: 8888/ chap09/Fig9.2/Fig.9.2_06/PieChart3DBean.jsp。

或者,http://localhost:8888/chap09/Fig9.2/Fig.9.2_06/PieChart3DBean.html。

我们以浏览 PieChart3DBean.jsp的页面而获得 JavaBean生成默认 3D饼图的结果为例 (厚

度为 25 像素,透明度 alpha 值为 0.5),如图 9.10 所示。

图 9.10 JavaBeans生成 3D饼图

本例中,我们提供了一个 setAlphaComposite的方法(程序第 83 行~第 85 行),用来设

置 alpha 值。alpha 默认值是 0.5,即绘制结果为透明的 3D 饼图。然后,提供了一个用于设置 3D 饼图厚度的 setThickness 方法(程序第 88 行~第 91 行)。所有方法都是我们非常熟悉的,

因此,我们仅给出源程序的清单,此处不再赘述。

如果要生成自定义的 3D 饼图,只需要在调用本 JavaBean的 JSP 程序中,加入以下两句

代码即可:

pieChart3D.setAlphaComposite(1.0f); pieChart3D.setThickness(20);

生成不透明的,厚度为 20 像素的 3D饼图。运行结果如图 9.11 所示。

Page 439: Java Web动态图表编程

图 9.11 JavaBeans生成 3D饼图

9.3 数据分离

前面介绍的 JavaBean 在绘制图表时,所需的数据集都是在 JavaBean 中随机生成的。如

何才能做到在调用 JavaBean的 JSP 中自定义这些数据呢?这就必须要满足, 数据集的创建和

绘制图表的代码分离。由于不同类型的图表需要不同的数据集对象,因此,本节讲述创建绘

制线段图表和直方图表所需数据集的CategoryDataset类及适用于饼图的数据集PieDataset类,

并且讲解如何将其应用到图表绘制的代码中。

9.3.1 创建及调用 CategoryDataset 类数据集对象

首先,我们来看一个适用于绘制线段图表和直方图表的数据集对象 CategoryDataset 类。 CategoryDataset.java (\chap09\Fig9.3)的源程序清单如下:

1: // Fig. 9.3_01: CategoryData.java 2: // 功能: 生成直方图、线段图的所需的数据集对象 3: 4: package chart.data; 5: 6: public class CategoryData 7: 8: String category[]; 9: String series[]; 10: double value[][]; 11: 12: public CategoryData(String category[], String series[]) 13: 14: this.category = category; 15: this.series = series; 16: this.value = new double[category.length][series.length]; 17: 18: 19: public void addValue(double data, int categoryItemIndex, int

seriesItemIndex) 20: 21: value[categoryItemIndex][seriesItemIndex] = data;

Page 440: Java Web动态图表编程

22: 23: 24: public String[] getCategory() 25: 26: return this.category; 27: 28: 29: public String[] getSeries() 30: 31: return this.series; 32: 33: 34: public double getValue(int categoryItemIndex, int seriesItem Index) 35: 36: return value[categoryItemIndex][seriesItemIndex]; 37: 38:

CategoryDataset 类的成员变量包含两个字符串数组和一个二维 double数组。字符串数组 category[]表示需要绘制的数据种类。此对象的含义和我们前面各个例程中所介绍的变量 category[]完全一样。 字符串变量 series[]的含义也和我们在前面各个例程中的变量 series[]完全

一样。二维 double数组对象 value表示参与图表绘制的数据。

程序第 12 行~第 17行,CategoryDataset 类的构造器:

public CategoryData(String category[], String series[])

this.category = category; this.series = series; this.value = new double[category.length][series.length];

在这个构造器中,对类的成员变量 category、series 和 value 进行初始化。图表中的二维

双精度数组变量 value的元素个数是字符串变量 category和 series 中各自元素个数的乘积。

程序第 19 行~第 22行,addValue方法:

public void addValue(double data, int categoryItemIndex, int series ItemIndex)

value[categoryItemIndex][seriesItemIndex] = data;

对二维双精度数组变量 value 中,下标为 categoryItemIndex 和 seriesItemIndex 的元素进

行赋值。该元素值为参数 data 的值。

程序第 24 行~第 27 行,getCategory 方法,返回字符串数组变量 category;同理,程序

第 29 行~第 32 行的 getSeries 方法,返回字符串数组变量 series。

程序第 34 行~第 37行,getValue方法,返回二维双精度数组变量 value中,指定下标为 categoryItemIndex 和 seriesItemIndex 的元素值。

首先在WEB­INF\classes\chart 目录下新建一个 data 子目录, 然后将 CategoryDataset. java 复制到 data 子目录下。现在我们以前面所讲述的 VBarChartBean.java (chap09\Fig9.2\ Fig.9.2_03)为例,重新编写了一个 BarCDChartBean.java (\chap09\Fig9.3)程序。在 BarCDChart Bean.java 程序中,我们创建了 CategoryDataset 类,并通过该类的相关方法,获得绘制直方图

所需的数据。

首先,将 BarCDChartBean.java 复制到WEB­INF\classes\chart\data 子目录下。 BarCDChartBean.java 程序第 25行~第 27 行:

String series[], category[]; CategoryData cd; double value;

Page 441: Java Web动态图表编程

这是新增加的几个成员变量,因为 BarCDChartBean 类和 CategoryData 类,都在同一个

包中,因此,不需要 import 语句将 CategoryData 类引入。

程序第 66 行~第 69行:

public void setDataset( CategoryData cd)

this.cd = cd;

setDataset 方法,接收一个 CategoryData 类对象 cd为参数,并将参数赋值给 BarCDChart Bean类中的成员变量 cd。

在 initialize方法中,程序第 77 行:

series= cd.getSeries();

通过 CategoryData 类对象 cd 的 getSeries 方法,对成员变量中的字符串数组对象 series 进行初始化。

程序第 78 行:

category = cd.getCategory();

通过 CategoryData 类对象 cd 的 getCategory 方法,对成员变量中的字符串数组对象 category进行初始化。

程序第 240行~第 259 行:

for (int i = 0; i < xTickCounter; i++) // 获得绘制数据 for (int j = 0; j < series.length; j++) value = cd.getValue(i, j);

// 计算直方块的绘制横坐标 double xVBarPosition = plotMargin + i * hSpace ­ totalBarLength / 2 + j * (barLength + barInnerMargin);

g2d.setColor(paintColor[j % paintColor.length]);

// 设置每个直方块的绘制参数 Rectangle2D.Double bar = new Rectangle.Double(xVBarPosition, plotHeight

­value, barLength, value);

g2d.fill(bar);

这段代码根据所得到的数据来绘制图表。图表的绘制数据是存放在二维双精度数组变量 value中,因此,这里使用了两个嵌套的循环,获得 vlaue中各元素的值。程序第 254行~第 255 行,计算直方图的绘制坐标等相关参数。

我们用简单的方法,将创建数据集对象 CategoryData.类和用于图表绘制的 BarCD ChartBean 类整合在一起。调用 BarCDChartBean 类的 BarCDChartBean.jsp(\chap09\Fig9.3)

的源程序第 6 行:

import="chart.data.*"

引入 chart.data 包中所有的类。

程序第 21 行: CategoryData cd = new CategoryData(category, series);

Page 442: Java Web动态图表编程

创建 CategoryData 类的对象 cd。 提供的两个字符串数组参数, 就是我们在程序第 12 行~

第 19 行声明的两个字符串数组变量:category和 series。程序第 22 行~第 32 行,调用 cd的 addValue方法,向 CategoryData 类中的二维双精度数组变量 value进行赋值操作。

程序第 34 行:

barCD.setDataset(cd);

将创建的 CategoryData 类的对象 cd 作为参数,传递给 BarCDChartBean 类的 setDataset 方法,并用其来初始化 BarCDChartBean类中相关的成员变量。

其余部分的代码没有任何新的内容。BarCDChartBean.jsp 的运行结果如图 9.12 所示。

Page 443: Java Web动态图表编程

图 9.12 BarCDChartBean.jsp的运行结果

同理,只需要按照本例的步骤,简单修改一下我们以前所讲解的创建线段图,以及 3D 饼图的 JavaBean源代码,同样可以实现本例的功能。

9.3.2 创建及调用 PieDataset 类数据集对象

PieDataset 类用于创建绘制各种类型饼图的数据集。 编写 PieDataset 类的原理和前例完全

一样。本节仅给出各相关程序的源程序清单以及最后的运行结果。注意,首先要将 PieDataset.java 以及 PieCDChartBean.java 拷贝到 WEB\INF\classes\chart\data 子目录下。

PieDataset.java((\chap09\Fig9.3))的源程序清单如下:

1: Fig. 9.3_04: PieData.java 2: 功能: 生成饼图的所需的数据集对象 3: 4: package chart.data; 5: 6: public class PieData 7: 8: String category[]; 9: double value[]; 10: 11: public PieData(String category[]) 12: 13: this.category = category; 14: this.value = new double[category.length]; 15: 16: 17: public void addValue(double data, int categoryItemIndex) 18: 19: value[categoryItemIndex]= data; 20: 21: 22: public String[] getCategory() 23: 24: return this.category; 25:

Page 444: Java Web动态图表编程

26: 27: public double getValue(int categoryItemIndex) 28: 29: return value[categoryItemIndex]; 30: 31: 32: public double[] getValue() 33: 34: return this.value; 35: 36:

源程序是以讲解过的例程 PieChart3DBean.java (chap09\Fig9.2\Fig.9.2_06)为蓝本,调用 PieDataset 类的 PiePDChartBean.java。PiePDChartBean.jsp的运行结果如图 9.13所示。

图 9.13 PiePDChartBean.jsp的运行结果

9.4 引擎的优化概述

本章讲述的各个 JavaBean中,有相当多的代码实际上是相同和重复的,如初始化图表相

关参数、绘制图表标题、绘制坐标轴标题、返回图表对象等。其实,绘制不同类型的图表基

本上是通过各个 JavaBean 中的 drawPlot 方法实现的。因此,我们可以将 JavaBean 重新按以

下思路改写:

(1) 重新编写一个创建通用图表的 WebChart 抽象类。 该类负责初始化图表的相关参数、

绘制背景、绘制图表标题、绘制坐标轴标题、返回图表对象等工作。该类包含了一个 drawPlot 抽象方法,drawPlot 的具体实现方法则由 WebChart 抽象类的子类实现。

(2)重新编写绘制某类型图表的 WebChart 子类,如 BarChart(绘制直方图)、LineChart (线段图)、PieChart(饼图)、GanttChart(甘特图)等,这些类都继承了WebChart 类,但拥

有各自不同的 drawPlot 方法,用来绘制不同风格的图表。当然,这些子类也可以重载

(Overload)其父类,即 WebChart 抽象类的相关方法,以实现特定的绘制目的。例如,饼图

绘制区域的绘制就与线段图、 直方图等不同。 因此, 我们可以在 PieChart 类中, 重载WebChart 抽象类的绘制背景的同名方法,以实现绘制不同于其他图表背景的饼图背景的目的。另外,

我们还可以继承上述子类而创建新的子类,以实现 3D 效果。例如,可以创建 LineChart3D

Page 445: Java Web动态图表编程

类,该类继承 LineChart 类。这些类之间的关系,可以用图 9.14 所示的 WebChart 继承层次结

构图来表示。

(3)对于创建数据集对象的类,我们也可以按照上述思路。首先创建一个包含所有数

据集对象共性的 DataSet 抽象类,其中包含一个 addValue的抽象方法。然后,再创建 DataSet 类的各个子类,如 PieDataset(用于绘制饼图的数据集类)、CategoryDataset(用于绘制直方

图、线段图的数据集类)等。在各个类中,分别实现各自版本的 addValue 方法。

图 9.14 WebChart继承层次结构

按照上述思路来重新编写各个类,使其具有结构清晰、编写方便、扩展性强大等特点。

相信读者在掌握本书内容后,可以轻松地编写出上述各类的源代码。

WebChart

LineChart BarChart PieChart

LineChart3D BarChart3D PieChart3D

Page 446: Java Web动态图表编程

9.5 本章小结

本章介绍了一种生成Web 图表的,简单的 JavaBean方法。通过线段图、直方图、饼图,

以及与之相对应的数据集对象的分离与组合,我们不仅可以创建各种风格的图表,还能够对

其进行扩展。创建一个不同风格的图表,只需要创建一个子类,重载相应的 drawPlot 方法即

可。