第十章 图形程序设计
Swing 概述
AWT是“ 一次编写,随处使用”。
人们嘲弄地将 AWT 称为 “ 一次编写, 随处调试”。
在 1996 年,Netscape 创建了一种称为 IFC ( Internet Foundation Class) 的 GUI 库, 它采 用了与 AWT 完全不同的工作方式。它将按钮、菜单这样的用户界面元素绘制在空白窗口上, 而对等体只需要创建和绘制窗口。因此,Netscape 的 IFC组件在程序运行的所有平台上的外 观和动作都一样。Sun 与 Netscape 合作完善了这种方式, 创建了一个名为Swing 的用户界面 库。Swing可作为Java 1.1 的扩展部分使用,现已成为 Java SE 1.2标准库的一部分。
在用户屏幕上显示基于 Swing 用户界面的元素要比显示 AWT 的基于对等体组件 的速度慢一些。鉴于以往的经验, 对于任何一台现代的计算机来说, 微小的速度差别无妨大 碍。另外,由于下列几点无法抗拒的原因,人们选择 Swing:
•Swing 拥有一个丰富、 便捷的用户界面元素集合。
•Swing 对底层平台依赖的很少,因此与平台相关的 bug 很少。
•Swing 给予不同平台的用户一致的感觉。
不过,上面第三点存在着一个潜在的问题: 如果在所有平台上用户界面元素看起来都一 样,那么它们就有可能与本地控件不一样,而这些平台的用户对此可能并不熟悉。
创建框架
在 Java 中,顶层窗口(就是没有包含在其他窗口中的窗口)被称为框架(frame)。在 AWT 库中有一个称为 Frame 的类, 用于描述顶层窗口。这个类的 Swing 版本名为JFrame, 它扩展于 Frame 类。JFrame 是极少数几个不绘制在画布上的 Swing 组件之一。因此,它的修 饰部件(按钮、标题栏、图标等)由用户的窗口系统绘制, 而不是由 Swing绘制。
在本节中,将介绍有关 Swing 的 JFrame 的常用方法。程序清单 10-1 给出了一个在屏幕 中显示一个空框架的简单程序, 如图 10-5 所示。
//程序清单 10-1 simpleframe/SimpleFrameTest.java package simpleFrame; import java.awt.*; import javax.swing.*; /** * @version 1.32 2007-06-12 * @author Cay Horstmann */ public class SimpleFrameTest { public static void main(String[] args) { EventQueue.invokeLater(new Runnable() { public void run() { SimpleFrame frame = new SimpleFrame(); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setVisible(true); } }); } } class SimpleFrame extends JFrame { private static final int DEFAULT_WIDTH = 300; private static final int DEFAULT_HEIGHT = 200; public SimpleFrame() { setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT); } }
Swing 类位于javax.swing 包中。包名 javax 表示这是 一个 Java 扩展包, 而不是核心包。出于历史原因 Swing 类被认为是一个扩展。不过从 1.2 版本开始,在每个 Java SE实现中都包含它。
在每个 Swing 程序中,有两个技术问题需要强调。 首先,所有的 Swing 组件必须由事件分派线程(event dispatch thread) 进行配置,线程将 鼠标点击和按键控制转移到用户接口组件。下面的代码片断是事件分派线程中的执行代码:
EventQueue.invokeLater(0 ->
{
statements
});
可以调用 frame.setUndecorated(true) 关闭所有框架装饰。
框架定位
JFrame 类本身只包含若干个改变框架外观的方法。当然,通过继承, 从 JFrame 的各个 超类中继承了许多用于处理框架大小和位置的方法 其中最重要的有下面几个:
•setLocation 和 setBounds 方法用于设置框架的位置。
•setlconlmage 用于告诉窗口系统在标题栏、任务切换窗口等位置显示哪个图标。
•setTitle 用于改变标题栏的文字。
•setResizable 利用一个 boolean 值确定框 架的大小是否允许用户改变。
图 10-6 给出了 JFrame 类的继承层次。

框架属性
组件类的很多方法是以获取 / 设置方法对形式出现的, 例如,Frame 类的下列方法:
public String getTitle() public void setTitle(String title)
这样的一个获取 / 设置方法对被称为一种属性。属性包含属性名和类型。将 get 或 set 之后的 第一个字母改为小写字母就可以得到相应的属性名。例如, Frame 类有一个名为 title且类型 为 String 的属性。
从概念上讲,title 是框架的一个属性。当设置这个属性时,希望这个标题能够改变用户 屏幕上的显示。当获取这个属性时, 希望能够返回已经设置的属性值。
针对 get/set 约定有一个例外: 对于类型为 boolean 的属性, 获取方法由 is开头。例如, 下面两个方法定义了 locationByPlatform 属性:
public boolean islocationByPIatforn() public void setLocationByPIatforra(boolean b)
确定合适的框架大小
要记住: 如果没有明确地指定框架的大小,所有框架的默认值为 0x 0 像素。
为了得到屏幕的大小, 需要按照下列步骤操作。调用 Toolkit 类的静态方法 getDefaultToolkit 得到一个 Toolkit 对象(Toolkit 类包含很多与本地窗口系统打交道的方法)。然后,调用 getScreenSize方法,这个方法以 Dimension对象的形式返回屏幕的大小。Dimension 对象同时 用公有实例变量 width 和 height 保存着屏幕的宽度和高度。下面是相关的代码:
Toolkit kit = Toolkit.getDefaultToolkit(); Dimension screenSize= kit.getScreenSize(); int screenWidth = screenSize.width; int screenHeight = screenSize.height;
下面,将框架大小设定为上面取值的 50%,然后,告知窗口系统定位框架:
setSize(screenWidth / 2, screenHeight / 2); setLocationByPlatform(true);
另外,还提供一个图标。由于图像的描述与系统有关,所以需要再次使用工具箱加载图 像。然后,将这个图像设置为框架的图标。
Image img = new InageIcon("icon.gif").getImage(); setIconImage(img);
程序清单 10-2 是完整的程序。当运行程序时,请注意看 “ Core Java” 图标。 下面是为了处理框架给予的一些提示:
•如果框架中只包含标准的组件, 如按钮和文本框,那么可以通过调用pack方法设置 框架大小。框架将被设置为刚好能够放置所有组件的大小。在通常情况下, 将程序的 主框架尺寸设置为最大。可以通过调用下列方法将框架设置为最大。
frame.setExtendedState(Frame.MAXIMIZEDJOTH):
•牢记用户定位应用程序的框架位置、 重置框架大小,并且在应用程序再次启动时恢复 这些内容是一个不错的想法。在第 13 章中将会介绍如何运用 API 的参数选择达到这 个目的。
•GraphicsDevice 类还允许以全屏模式执行应用。
//程序清单 10-2 sizedFrame/SizedFrameTest.java package sizedFrame; import java.awt.*; import javax.swing.*; /** * @version 1.32 2007-04-14 * @author Cay Horstmann */ public class SizedFrameTest { public static void main(String[] args) { EventQueue.invokeLater(new Runnable() { public void run() { JFrame frame = new SizedFrame(); frame.setTitle("SizedFrame"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setVisible(true); } }); } } class SizedFrame extends JFrame { public SizedFrame() { // get screen dimensions Toolkit kit = Toolkit.getDefaultToolkit(); Dimension screenSize = kit.getScreenSize(); int screenHeight = screenSize.height; int screenWidth = screenSize.width; // set frame width, height and let platform pick screen location setSize(screenWidth / 2, screenHeight / 2); setLocationByPlatform(true); // set frame icon Image img = new ImageIcon("icon.gif").getImage(); setIconImage(img); } }
在组件中显示信息
JFrame 的结构相当复杂。在图 10-8中给出了 JFrame 的 结构。可以看到,在 JFrame 中有四层面板。其中的根面板、层级面板和玻璃面板人们并不太关心;它们是用来组织菜单栏和内容窗格以及实现观感的。 Swing 程序员最关心的是内容窗格(contentpane)。在设计框架的时候, 要使用下列代码将所 有的组件添加到内容窗格中:
Container contentPane = frame.getContentPane(); Component c = . . .; contentPane.add(c);

在 Java SE 1.4 及以前的版本中,JFrame 类中的 add方法抛出了一个异常信息“ Do not use JFrame.add().Use JFrame.getContentPaneQ.add instead”。如今, JFrame.add方法不再显示这些提示信息,只是简单地调用内容窗格的 add,
因此,可以直接调用
frame.add(c);
在这里,打算将一个绘制消息的组件添加到框架中。绘制一个组件,需要定义一个扩展 JComponent 的类,并覆盖其中的 paintComponent 方法。
paintComponent 方法有一个 Graphics 类型的参数, 这个参数保存着用于绘制图像和文本 的设置, 例如,设置的字体或当前的颜色。在 Java中,所有的绘制都必须使用 Graphics对 象, 其中包含了绘制图案、 图像和文本的方法。
下列代码给出了如何创建一个能够进行绘制的组件:
class MyComponent extends JComponent { public void paintComponent(Graphics g) { codefordrawing } }
无论何种原因, 只要窗口需要重新绘图, 事件处理器就会通告组件,从而引发执行所有 组件的 paintComponent 方法。
一定不要自己调用 paintComponent 方法。在应用程序需要重新绘图的时候, 这个方法将 被自动地调用,不要人为地干预这个自动的处理过程。
显示文本是一种特殊的绘图。在 Graphics 类中有一个 drawstring方法, 调用的语法格 式为:
g.drawString(text, x, y)
在这里, 打算在原始窗口大约水平 1/4, 垂直 1/2 的位置显示字符串“ Not a Hello, World program" o 现在,尽管不知道应该如何度量这个字符串的大小, 但可以将字符串的开始位置 定义在坐标(75, 100 )。这就意味着字符串中的第一个字符位于从左向右 75 个像素, 从上向下 100 个像素的位置(实际上, 文本的基线位于像素 100 的位置, 有关文本的度量方式将 在稍后阐述)。因此, paintComponent 方法的书写内容如下所示:
public class NotHelloWorldComponent extends JComponent { public static final int MESSACE_X = 75; public static final int MESSACE. Y = 100; public void paintComponent(Graphics g) { g.drawString("Not a Hello World program", MESSACE_X, MESSACE_Y); } ... }
程序清单 10-3 给出了完整的代码。
//程序清单 10-3 notHelloWorid/NotHelloWorld.java package notHelloWorld; import javax.swing.*; import java.awt.*; /** * @version 1.32 2007-06-12 * @author Cay Horstmann */ public class NotHelloWorld { public static void main(String[] args) { EventQueue.invokeLater(new Runnable() { public void run() { JFrame frame = new NotHelloWorldFrame(); frame.setTitle("NotHelloWorld"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setVisible(true); } }); } } /** * A frame that contains a message panel */ class NotHelloWorldFrame extends JFrame { public NotHelloWorldFrame() { add(new NotHelloWorldComponent()); pack(); } } /** * A component that displays a message. */ class NotHelloWorldComponent extends JComponent { public static final int MESSAGE_X = 75; public static final int MESSAGE_Y = 100; private static final int DEFAULT_WIDTH = 300; private static final int DEFAULT_HEIGHT = 200; public void paintComponent(Graphics g) { g.drawString("Not a Hello, World program", MESSAGE_X, MESSAGE_Y); } public Dimension getPreferredSize() { return new Dimension(DEFAULT_WIDTH, DEFAULT_HEIGHT); } }
处 理 2D 图形
自从 Java 版本 1.0 以来, Graphics类就包含绘制直线、矩形和楠圆等方法。但是,这些 绘制图形的操作能力非常有限。例如, 不能改变线的粗细,不能旋转这些图形。
要想使用 Java 2D 库绘制图形,需要获得一个 Graphics2D 类对象。这个类是 Graphics 类 的子类。自从 Java SE 2 版本以来,paintComponent 方法就会自动地获得一个 Graphics2D 类 对象,我们只需要进行一次类型转换就可以了。如下所示:
public void paintComponent(Craphics g) { Craphics2D g2 = (Graphics2D) g; ... }
Java 2D 库采用面向对象的方式将几何图形组织起来。包含描述直线、矩形的椭圆的类:
Line2D
Rectang1e2D
Ellipse2D
这些类全部实现了 Shape 接口。
要想绘制图形,首先要创建一个实现了 Shape 接口的类的对象,然后调用 GraphicS2D类 中的 draw方法。例如,
Rectangle2D rect = . . .;
g2.draw(rect);
当 创 建 一 个 Rectangle2D.Float 对 象 时, 应 该 提 供 float 型 数 值 的 坐 标。 而 创 建 Rectangle2D. Double 对象时,应该提供 double 型数值的坐标。
Rectangle2D.FIoat floatRect -new Rectangle2D.FIoat(10.OF, 25.OF, 22.SF, 20.OF); Rectangle2D.Double doubleRect = new Rectangle2D.Oouble(10.0, 2S.0, 22.5, 20.0);
实 际 上, 由 于 Rectangle2D.Float 和 Rectangle2D.Double 都 扩 展 于 Rectangle2D 类, 并 且子类只覆盖了 RectangldD 超类中的方法, 所以没有必要记住图形类型。可以直接使用 Rectangle2D 变量保存矩形的引用。
Rectangle2D floatRect = new Rectangle2D.FIoat(10.OF, 25.OF, 22.5F, 20.OF); Rectangle2D doubleRect = new Rectangle2D.Double(10.0, 2S.0, 22.5, 20.0);
也就是说, 只有在构造图形对象时,才需要使用烦人的内部类。
构造参数表示矩形的左上角位置、宽和髙。
实际上, Rectangle2D.Float 类包含了一个不是由 Rectangle2D 继承而来的附加方法 setRect(float x, float y , float h, float w)。如果将 Rectangle2D.Float 的引用存储在 Rectangle2D 变量中, 那就会失去这个方法。但是,这也没有太大关系, 因为在 Rectangle2D 中有一个 参教为 double 类型的 setRect 方法。
程序清单 10-4中的程序绘制了一个矩形;这个矩形的内接椭圆;矩形的对角线以及以矩 形中心为圆点的圆。图 10-12显示了结果。

//程序清单 10-4 draw/DrawTest.java package draw; import java.awt.*; import java.awt.geom.*; import javax.swing.*; /** * @version 1.32 2007-04-14 * @author Cay Horstmann */ public class DrawTest { public static void main(String[] args) { EventQueue.invokeLater(new Runnable() { public void run() { JFrame frame = new DrawFrame(); frame.setTitle("DrawTest"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setVisible(true); } }); } } /** * A frame that contains a panel with drawings */ class DrawFrame extends JFrame { public DrawFrame() { add(new DrawComponent()); pack(); } } /** * A component that displays rectangles and ellipses. */ class DrawComponent extends JComponent { private static final int DEFAULT_WIDTH = 400; private static final int DEFAULT_HEIGHT = 400; public void paintComponent(Graphics g) { Graphics2D g2 = (Graphics2D) g; // draw a rectangle double leftX = 100; double topY = 100; double width = 200; double height = 150; Rectangle2D rect = new Rectangle2D.Double(leftX, topY, width, height); g2.draw(rect); // draw the enclosed ellipse Ellipse2D ellipse = new Ellipse2D.Double(); ellipse.setFrame(rect); g2.draw(ellipse); // draw a diagonal line g2.draw(new Line2D.Double(leftX, topY, leftX + width, topY + height)); // draw a circle with the same center double centerX = rect.getCenterX(); double centerY = rect.getCenterY(); double radius = 150; Ellipse2D circle = new Ellipse2D.Double(); circle.setFrameFromCenter(centerX, centerY, centerX + radius, centerY + radius); g2.draw(circle); } public Dimension getPreferredSize() { return new Dimension(DEFAULT_WIDTH, DEFAULT_HEIGHT); } }
使用颜色
使用 Gr叩hics2D 类的 setPaint 方法可以为图形环境上的所有后续的绘制操作选择颜色。 例如:
g2.setPaint(Color.RED);
g2.drawString("Warning!", 100,100);
只需要将调用 draw 替换为调用 fill 就可以用一种颜色填充一个封闭图形(例如: 矩形或 椭圆)的内部:
Rectangle2D rect = . . .;
g2.setPaint(Color.RED);
g2.fi11(rect); // fills rect with red
要想绘制多种颜色, 就需要按照选择颜色、 绘制图形、 再选择另外一种颜色、再绘制图 形的过程实施。
Color 类用于定义颜色。在java.awt.Color 类中提供了 13 个预定义的常量, 它们分別表 示 13 种标准颜色。
BLACK, BLUE, CYAN, DARK.GRAY, CRAY, GREEN, LIGHT_CRAY, MACENTA, ORANGE, PINK, RED, WHITE, YELLOW
可以通过提供红、绿和蓝三色成分来创建一个 Color 对象,以达到定制颜色的目的。三 种颜色都是用 0 ~ 255(也就是一个字节)之间的整型数值表示,调用 Color 的构造器格式为:
Color(int redness, int greenness, int blueness)
下面是一个定制颜色的例子:
g2.setPaint(new Color(0, 128, 128)); // a dull blue-green g2.drawString("Welcome!",75, 125);
要想设置背景颜色, 就需要使用 Component 类中的 setBackground方法。Component 类 是 JComponent 类的祖先。
MyComponent p = new HyComponent(); p.setBackground(Color.PINK);
另外,还有一个 setForeground方法,它是用来设定在组件上进行绘制时使用的默认颜色。
文本使用特殊字体
要想知道某台特定计算机上允许使用的字体, 就需要调用 GraphicsEnvironment 类中的 getAvailableFontFamilyNames方法。这个方法将返回一个字符型数组, 其中包含了所有可用 的字体名。GraphicsEnvironment 类描述了用户系统的图形环境, 为了得到这个类的对象,需要 调用静态的 getLocalGraphicsEnvironment 方法。下面这个程序将打印出系统上的所有字体名:
import java.awt.*; public class ListFonts { public static void main(String[] args) { String[] fontNames = GraphicsEnvironment .getLocalGraphicsEnvironmentp .getAvailableFontFamilyNames(); for (String fontName : fontNames) System.out.println(fontName); } }
为了创建一个公共基准, AWT 定义了五个逻辑(logical) 字体名:
SansSerif
Serif
Monospaced
Dialog
Dialoglnput
些字体将被映射到客户机上的实际字体。例如,在 Windows 系统中, SansSerif将被映 射到 Arial。
要想使用某种字体绘制字符, 必须首先利用指定的字体名、字体风格和字体大小来创建— 个 Font 类对象。下面是构造一个 Font 对象的例子:
Font sansbold14 = new Font(°SansSerif", Font.BOLD, 14);
第三个参数是以点数目计算的字体大小。点数目是排版中普遍使用的表示字体大小的单 位,每英寸包含 72 个点。
为了说明位置是正确的, 示例程序绘制了基线和包围 这个字符串的矩形。图 10-14 给出了屏幕显示结果。程序图 10-M 绘制基线和字符串边框 清单 10-5 中是相应的代码。
//程序清单 font/FontTest.java package font; import java.awt.*; import java.awt.font.*; import java.awt.geom.*; import javax.swing.*; /** * @version 1.33 2007-04-14 * @author Cay Horstmann */ public class FontTest { public static void main(String[] args) { EventQueue.invokeLater(new Runnable() { public void run() { JFrame frame = new FontFrame(); frame.setTitle("FontTest"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setVisible(true); } }); } } /** * A frame with a text message component */ class FontFrame extends JFrame { public FontFrame() { add(new FontComponent()); pack(); } } /** * A component that shows a centered message in a box. */ class FontComponent extends JComponent { private static final int DEFAULT_WIDTH = 300; private static final int DEFAULT_HEIGHT = 200; public void paintComponent(Graphics g) { Graphics2D g2 = (Graphics2D) g; String message = "Hello, World!"; Font f = new Font("Serif", Font.BOLD, 36); g2.setFont(f); // measure the size of the message FontRenderContext context = g2.getFontRenderContext(); Rectangle2D bounds = f.getStringBounds(message, context); // set (x,y) = top left corner of text double x = (getWidth() - bounds.getWidth()) / 2; double y = (getHeight() - bounds.getHeight()) / 2; // add ascent to y to reach the baseline double ascent = -bounds.getY(); double baseY = y + ascent; // draw the message g2.drawString(message, (int) x, (int) baseY); g2.setPaint(Color.LIGHT_GRAY); // draw the baseline g2.draw(new Line2D.Double(x, baseY, x + bounds.getWidth(), baseY)); // draw the enclosing rectangle Rectangle2D rect = new Rectangle2D.Double(x, y, bounds.getWidth(), bounds.getHeight()); g2.draw(rect); } public Dimension getPreferredSize() { return new Dimension(DEFAULT_WIDTH, DEFAULT_HEIGHT); } }
显示图像
一旦图像保存在本地文件或因特网的某个位置上,就可以将它们读到 Java 应用程序中, 并在 Graphics 对象上进行显示。读取图像有很多方法。在这里我们使用你之前已经见过的 Imagelcon类:
Image image = new Imagelcon(filename) .getlmage();
这里的变量 image 包含了一个封装图像数据的对象引用。可以使用 Graphics类的 drawlmage 方法将图像显示出来。
public void paintComponent(Graphics g) { ... g.drawlmage(image, x, y, null); }
程序清单 10-6 又前进了一步, 它在一个窗口中平铺显 示了一幅图像。 这里采 用 paintComponent 方法来实现平铺显示。它的基本过程为: 先在左上角显示图像的一个拷贝, 然后使用 copyArea 将其 拷贝到整个窗口:
for (int i = 0; i * imageWidth <= getWidthO; i++)
for (int j = 0; j * imageHeight <= getHeightO; j++)
if (i + j > 0)
g.copyArea(0, 0, imageWidth, imageHeight,i * imageWidth, j * imageHeight);
程序清单 10-6 列出了图像显示程序的完整代码。
//程序清单 10-6 image/lmageTest.java package image; import java.awt.*; import javax.swing.*; /** * @version 1.33 2007-04-14 * @author Cay Horstmann */ public class ImageTest { public static void main(String[] args) { EventQueue.invokeLater(new Runnable() { public void run() { JFrame frame = new ImageFrame(); frame.setTitle("ImageTest"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setVisible(true); } }); } } /** * A frame with an image component */ class ImageFrame extends JFrame { public ImageFrame() { add(new ImageComponent()); pack(); } } /** * A component that displays a tiled image */ class ImageComponent extends JComponent { private static final int DEFAULT_WIDTH = 300; private static final int DEFAULT_HEIGHT = 200; private Image image; public ImageComponent() { image = new ImageIcon("blue-ball.gif").getImage(); } public void paintComponent(Graphics g) { if (image == null) return; int imageWidth = image.getWidth(this); int imageHeight = image.getHeight(this); // draw the image in the upper-left corner g.drawImage(image, 0, 0, null); // tile the image across the component for (int i = 0; i * imageWidth <= getWidth(); i++) for (int j = 0; j * imageHeight <= getHeight(); j++) if (i + j > 0) g.copyArea(0, 0, imageWidth, imageHeight, i * imageWidth, j * imageHeight); } public Dimension getPreferredSize() { return new Dimension(DEFAULT_WIDTH, DEFAULT_HEIGHT); } }