图形程序设计

到目前为止,我们编写的程序都是通过键盘接收输入,在控制台屏幕上显示结果。绝大多数用户并不喜欢这种交互方式。现代的程序早已不采用这种操作方法了, Web 页面更是如 此。从本章开始,将介绍如何编写使用图形用户界面(GUI) 的 Java 程序。本章主要讲述如何编写定义屏幕上的窗口大小和位置的程序;如何在窗口中采用多种字体显示文本;如何显示图像等。这些都是需要掌握的编程技能, 在后续各章中,将会使用这些技术编写一些很有趣味的程序。

随后的两章, 将介绍如何处理按键, 点击鼠标这样的事件, 以及如何在应用程序中添加菜单和按钮这样的界面元素。学习完这三章之后, 读者就应该能够掌握编写独立运行的图形 应用程序的要素了。有关更加复杂的图形程序设计技巧请参看卷 n。 另外, 如果只希望用 Java 编写服务器端的应用程序,并且对 GUI 编程又不感兴趣,那 么就可以跳过这几章。

10.1 Swing 概述

在 Java 1.0 刚刚出现的时候,包含了一个用于基本 GUI 程序设计的类库,Sun将它称为 抽象窗口工具箱 ( Abstract Window Toolkit, AWT)。基本 AWT 库采用将处理用户界面元素 的任务委派给每个目标平台( Windows、Solaris、 Macintosh 等)的本地 GUI 工具箱的方式, 由本地 GUI 工具箱负责用户界面元素的创建和动作。例如,如果使用最初的 AWT 在 Java 窗 口中放置一个文本框, 就会有一个低层的“ 对等体” 文本框, 用来真正地处理文本输入。从 理论上说,结果程序可以运行在任何平台上, 但观感( look and feel ) 的效果却依赖于目标平台,因此,Sun 公司的口号是 “ 一次编写,随处使用”

对于简单的应用程序来说,基于对等体方法的效果还是不错的。但是,要想编写依赖于本地用户界面元素的高质量、 可移植的图形库就会暴露出一些缺陷。例如,菜单、 滚动条和 文本域这些用户界面元素, 在不同的平台上,操作行为存在着一些微妙的差别。因此, 要想给予用户一致的、 可预见性的界面操作方式是相当困难的。而且,有些图形环境(如 X11/ Motif) 并没有像 Windows 或 Macintosh 这样丰富的用户界面组件集合。这也就将基于对等体的可移植库限制在了“ 最小公分母” 的范围内。其结果使 AWT 构建的 GUI 应用程序看起来没有 Windows 或 Macintosh 应用程序显示的那么漂亮, 也没有提供那些平台用户所认知的功能。更加糟糕的是, 在不同平台上的 AWT 用户界面库中存在着不同的 bug。研发人员必须在每一个平号上测试应用程序, 因此人们嘲弄地将 AWT 称为 “ 一次编写, 随处调试”

在 1996 年,Netscape 创建了一种称为 IFC ( Internet Foundation Class) 的 GUI 库, 它采 用了与 AWT 完全不同的工作方式。它将按钮、菜单这样的用户界面元素绘制在空白窗口上, 而对等体只需要创建和绘制窗口。因此,Netscape 的 IFC 组件在程序运行的所有平台上的外 观和动作都一样。Sun 与 Netscape 合作完善了这种方式, 创建了一个名为 Swing 的用户界面库。Swing 可作为 Java 1.1 的扩展部分使用, 现已成为 Java SE 1.2 标准库的一部分。 就像 Duke Ellington 所说的那样:“ 如果没有 Swing, Java 图形界面就没有任何意义”。 现在, Swing 是不对等基于 GUI 工具箱的正式名字。它已是 Java 基础类库(Java Foundation Class, JFC ) 的一部分。完整的 JFC 十分庞大, 其中包含的内容远远大于 Swing GUI 工具箱。 JFC 特性不仅仅包含了 Swing 组件,而且还包含了一个可访问性 API、 一个2D API 和一个可 拖放 API。

注释: Swing 没有完全替代 AWT, 而是基于 AWT 架构之上。Swing 仅仅提供了能力更加强大的用户界面组件。 尤其在采用 Swing 编写的程序中,还需要使用基本的 AWT 处理事件。从现在开始, Swing 是指 “ 被绘制的” 用户界面类 AWT 是指像事件处理这样的窗口工具箱的底层机制

当然, 在用户屏幕上显示 基于 Swing 用户界面的元素要比显示 AWT 的基于对等体组件 的速度慢一些 。鉴于以往的经验, 对于任何一台现代的计算机来说, 微小的速度差别无妨大 碍。另外,由于下列几点无法抗拒的原因,人们选择 Swing:

• Swing 拥有一个丰富、 便捷的用户界面元素集合。

• Swing 对底层平台依赖的很少,因此与平台相关的 bug 很少。

• Swing 给予不同平台的用户一致的感觉。

不过, 上面第三点存在着一个潜在的问题: 如果在所有平台上用户界面元素看起来都一 样, 那么它们就有可能与本地控件不一样,而这些平台的用户对此可能并不熟悉。

Swing 采用了一种很巧妙的方式来解决这个问题。在程序员编写 Swing 程序时,可以为 程序 指定专门的“ 观感” 。例如, 图 10-1 和图 10-2 展示了同一个程序在 Windows 和 GTK 平 台下运行的观感。

此外, Sun 开发了一种称为 Metal 的独立于平台的观感。现在,市场上人们将它称为 “ Java 观感”。不过,绝大多数程序员还继续沿用 Metal 这个术语,在本书中也将这样称呼。

有些人批评 Metal 有点笨重, 而在 Java SE 5.0 中看起来却焕然一新(参见图 10-3 )。现 在, Metal 外观支持多种主题, 每一种主题的颜色和字体都有微小的变化。默认的主题叫做 Ocean。

在 Java SE 6 中, Sun 改进了对 Windows 和 GTK 本地观感的支持。Swing 应用程序将会支持色彩主题的个性化设置, 逼真地表现着动态按钮和变得十分时尚的滚动条。

Java 7 提供了一个新观感, 称为 Nimbus (见图 10-4 ), 不过默认情况下不可用。Nimbus 使用了矢量绘图而不是位图绘图, 所以它不依赖于屏幕分辨率。

有些用户希望 Java 应用使用其平台的本地观感, 但另外一些用户可能更喜欢 Metal 或某 种第三方观感。在第 11 章中你会了解到, 让用户选择他们喜欢的观感非常容易。

注释: Java 程序员可以扩展已有的观感, 或者甚至还可以设计一个全新的观感, 不过我们无法在本书中留出篇幅来介绍具体如何做到。这是一个很麻烦的过程, 需要指定各个 Swing 组件如何绘制。有些开发人员已经做过这个工作, 特别是将 Java 移植到某些非传统平台上时(如 kiosk 终端或手持设备)。 可以访问 www.javootoo.com, 其中给出了一组很有意思的观感实现。

Java SE 5.0 引入了一种称为 Synth 的观感, 可以更容易地实现这一过程。在 Synth 中, 可以提供图像文件和 XML 描述文件来定义新的观感, 而无须进行任何编程。

提示: Napkin 观感(http://napkinlaf.sourceforge.net) 为所有用户界面元素提供了一种手 绘外观。在向客户展示原型时非常有用, 它会向客户发出一个清楚的信号,指示你提供的并非最终的产品。

注释: 绝大多数 Java 用户界面程序设计都采用 Swing, 但有一个特别的例外。Eclipse 集成开发环境使用了一种与 AWT 类似称为 SWT 的图形工具箱, 它可以映射到不同平台的本地组件上。有关 SWT 的描述可以在网站 www.eclipse.org/ articles/ 找到。

Oracle 正在开发一种替代技术, 称为 JavaFX, 将来某个时间可能会取代 Swing。本书中不会讨论 JavaFX。有关的更多信息请访问 http://docs.oracle.com/javase/8/javafic/getstarted-tutorial/jfk-overview.htm。

如果使用过 Visual Baisc 或 C# 编写 Microsoft Windows 应用程序, 就应该了解这些产品提供的图形布局工具和资源编辑器带来的便利。这些工具可以用来设计应用程序的外观, 然 后生成大部分(通常是全部)GUI 代码。尽管也有一些 Java 程序设计的 GUI 构造器,但要想 有效地使用这些工具,需要知道如何手工地创建用户界面。本章剩余的部分将介绍关于显示 一个窗口及绘制内容的基本知识。

10.2 创建框架

在 Java 中, 顶层窗口 (就是没有包含在其他窗口中的窗口)被称为 框架(frame ) 。在 AWT 库中有一个称为 Frame 的类, 用于描述顶层窗口。这个类的 Swing 版本名为 JFrame, 它扩展于 Frame 类。JFrame 是极少数几个不绘制在画布上的 Swing 组件之一。因此,它的修饰部件(按钮、标题栏、图标等)由用户的窗口系统绘制, 而不是由 Swing 绘制。

警告: 绝大多数 Swing 组件类都以“ J” 开头, 例如,JButton、JFrame 等 。在 Java 中有 Button 和 Frame 这样的类, 但它们属于 AWT 组件 。如果偶然地忘记书写“ J”,程序仍然可以进行编译和运行,但是将 Swing 和 AWT 组件混合在一起使用将会导致视觉和行 为的不一致。

在本节中,将介绍有关 Swing 的 JFrame 的常用方法。程序清单 10-1 给出了一个在屏幕 中显示一个空框架的简单程序, 如图 10-5 所示。(略过)

下面逐行地讲解一下这个程序。 Swing 类位于 javax.swing 包中。包名 javax 表示这是 一个 Java 扩展包, 而不是核心包。出于历史原因 Swing 类被认为是一个扩展。不过从 1.2 版本开始,在每个 Java SE 实现中都包含它。

在默认情况下, 框架的大小为 0 x 0 像素, 这种框架 没有什么实际意义。这里定义了一个子类 SimpleFrame, 它的构造器将框架大小设置为 300 x 200 像素。 这是 SimpleFrame 和 JFrame 之间唯一的差别。

在 SimpleFrameTest 类的 main 方法中, 我们构造了一个 SimpleFrame 对象使它可见。

在每个 Swing 程序中,有两个技术问题需要强调。

首先,所有的 Swing 组件必须由 事件分派线程 (event dispatch thread ) 进行配置, 线程将鼠标点击和按键控制转移到用户接口组件 。下面的代码片断是事件分派线程中的执行代码:

EventQueue.invokeLater(() ->
  statements

  这一内容将在 14 章中详细讨论。现在, 只需要简单地将其看作启动一个 Swing 程序的神奇代码即可。

  注释:许多 Swing 程序并没有在事件分派线程中初始化用户界面。在主线程中完成初始化是通常采用的方式。遗憾的是, 由于 Swing 组件十分复杂, JDK 的程序员无法保证这 种方式的安全性。 虽然发生错误的概率非常小, 但任何人不愿意成为遭遇这个问题的不幸者之一。即使代码看起来有些神秘,也最好能够保证其正确性。

  接下来,定义一个用户关闭这个框架时的响应动作。对于这个程序而言,只让程序简单 地退出即可。选择这个响应动作的语句是

frame.setDefaultCloseOperation(JFrame.EXIT_0N_CL0SE);

  在包含多个框架的程序中, 不能在用户关闭其中的一个框架时就让程序退出。在默认情况下,用户关闭窗口时只是将框架隐藏起来, 而程序并没有终止(在最后一个框架不可见之后,程序再终止,这样处理比较合适,而 Swing却不是这样工作的)。

  简单地构造框架是不会自动地显示出来的, 框架起初是不可见的。这就给程序员了一个机会, 可以在框架第一次显示之前往其中添加组件。为了显示框架,main方法需要调用框架 的 setVisible 方法。

  注释:在 Java SE 5.0 以前的版本中, 可以使用 JFrame 类从超类 Window 继承 show 方法。 Window 类的超类是 Component, 其中也有一个 show 方法。在 Java SE 1.2 中不提倡使用 Component.show。如果想要显示一个组件, 建议调用 setVisible(true) 。然而,Java SE 1.4 以前的版本,并没有反对使用 Window.show 方法。事实上, 这个方法很实用, 它可以让窗口可见, 且置于其他窗口的前面。 遗憾的是,由于不提倡使用它, 随之也失去了这一 好处, Java SE 5.0 也不赞成使用 show 显示窗口。

  在初始化语句结束后,main方法退出。需要注意,退出 main 并没有终止程序,终止的只是主线程。事件分派线程保持程序处于激活状态, 直到关闭框架或调用 SyStem.exit 方法终 止程序。

  图 10-5中显示的是运行程序清单 10-1 的结果, 它只是一个很枯燥的顶层窗口。在这个图中看到的标题栏和外框装饰(比如, 重置窗口大小的拐角)都是由操作系统绘制的,而不是 Swing 库。在 Windows、 GTK 或 Mac 下运行同样的程序,将会得到不同的框架装饰。 Swing库负责绘制框架内的所有内容。在这个程序中, 只用默认的背景色填充了框架。

  注释: 可以调用 frame.setUndecorated(true) 关闭所有框架装饰。

10.3 框架定位

  JFrame 类本身只包含若干个改变框架外观的方法。当然,通过继承, 从 JFrame 的各个 超类中继承了许多用于处理框架大小和位置的方法 其中最重要的有下面几个:

  • setLocation 和 setBounds 方法用于设置框架的位置。

  • setlconlmage 用于告诉窗口系统在标题栏、任务切换窗口等位置显示哪个图标。

  • setTitle 用于改变标题栏的文字。

  • setResizable 利用一个 boolean 值确定框 架的大小是否允许用户改变。

   提示:本节 API 注解给出了一些最为重要 的用于设置框架适当观感的方法。其中一些定义在 JFrame 类中, 而另一些来自 JFrame 的各个超类。因此, 可能需要查阅 API 文 档, 以便确定是否存在能够实现某个特定目的的方法。遗憾的是, 在文档中查阅一个类 继承的方法是一件比较令人烦恼的事情。对于子类来说,API 文档只解释了覆盖的方法。 例如, 可以应用于 JFrame 类对象的 toFront 方法, 由于它是从 Window 类继承而来的, 所以 JFrame 文档没有对它进行解释。如果 认为应该有一个能够完成某项操作的方法, 而在处理的类文档中又没有解释, 就应该查看这个类的超类 API 文档。 每页 API 上面 都有一个对超类的超链接,继承方法被列在新方法和覆盖方法的汇总下面。

  正像 API 注解中所说的, 对 Component 类(是所有 GUI 对象的祖先) 和 Window 类 ( 是 Frame类的超类) 需要仔细地研究一下, 从中找到缩放和改变框架的方法。 例如, 在 Component 类中的 setLocation 方法是重定位组件的一个方法。如果调用

setLocation(x, y)

  则窗口将放置在左上角水平 x 像素,垂直 y 像素的位置, 坐标( 0, 0 ) 位于屏幕的左上角。 同样地, Component 中的 setBounds 方法可以实现一步重定位组件(特别是 JFrame) 大小和 位置的操作, 例如:

setBounds(x, y, width, height)

  可以让窗口系统控制窗口的位置,如果在显示窗口之前调用

setLocationByPlatform(true):

  窗口系统会选用窗口的位置(而不是大小) ,通常是距最后一个显示窗口很少偏移量的位置。

  注释: 对于框架来说, setLocation 和 setBounds 中的坐标均相对于整个屏幕。在第 12 章 中将会看到, 在容器中包含的组件所指的坐标均相对于容器。

  10.3.1 框架属性

    组件类的很多方法是以获取 / 设置方法对形式出现的, 例如,Frame 类的下列方法:

public String getTitle()
public void setTitle(String title)

    这样的一个获取 / 设置方法对被称为一种属性。属性包含属性名和类型。将 get 或 set 之后的 第一个字母改为小写字母就可以得到相应的属性名。例如, Frame 类有一个名为 title且类型 为 String 的属性。

    从概念上讲,title 是框架的一个属性。当设置这个属性时,希望这个标题能够改变用户 屏幕上的显示。当获取这个属性时, 希望能够返回已经设置的属性值。

    我们并不清楚(也不关心)Frame 类是如何实现这个属性的。或许只是简单的利用对等 框架存储标题。或许有一个实例域:

private String title; // not required for property

    如果类没有匹配的实例域,我们将不清楚(也不关心)如何实现获取和设置方法。或许 只是读、写实例域, 或许还执行了很多其他的操作。例如, 当标题发生变化时,通知给窗口系统。

    针对 get/set 约定有一个例外: 对于类型为 boolean 的属性, 获取方法由 is 开头。例如, 下面两个方法定义了 locationByPlatform 属性:

public boolean islocationByPlatform()
public void setLocationByPIatforra(boolean b)

  10.3.2 确定合适的框架大小

    要记住: 如果没有明确地指定框架的大小,所有框架的默认值为 0 x 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);
setLocationByPlatfori(true);

    另外, 还提供一个图标。由于图像的描述与系统有关,所以需要再次使用工具箱加载图像。然后, 将这个图像设置为框架的图标。

Image img = new Inagelcon("icon.gif").getlmage();
setlconlnage(img);

    对于不同的操作系统,所看到的图标显示位置有可能不同。例如,在 Windows 中,图标 显示在窗口的左上角,按下 ALT+TAB,可以在活动任务的列表中看到相应程序的图标。

    程序清单 10-2 是完整的程序。当运行程序时,请注意看 “ Core Java” 图标。

    下面是为了处理框架给予的一些提示:

    • 如果框架中只包含标准的组件, 如按钮和文本框,那么可以通过调用 pack 方法设置 框架大小。框架将被设置为刚好能够放置所有组件的大小。在通常情况下, 将程序的主框架尺寸设置为最大。可以通过调用下列方法将框架设置为最大。 frame.setExtendedState(Frame.MAXIMIZED_BOTH);

    • 牢记用户定位应用程序的框架位置、 重置框架大小,并且在应用程序再次启动时恢复 这些内容是一个不错的想法。在第 13 章中将会介绍如何运用 API 的参数选择达到这 个目的。

    • GraphicsDevice 类还允许以全屏模式执行应用。

    API java.awt.Component 1.0

    • boolean isVisible()
    • void setVisible(boolean b)

      获取或设置visible属性。组件最初是可见的,但JFrame这样的顶层组件例外。
    • void setSize(int width,int height)1.1
      使用给定的宽度和高度,重新设置组件的大小。
    • void setLocation(int x,int y)1.1
      将组件移到一个新的位置上。如果这个组件不是顶层组件,x和y坐标(或者p.x和p.y)是容器坐标;否则是屏幕坐标(例如:JFrame)。      

    •void setBounds(int x,int y,int width,int height)1.1

      移动并重新设置组件的大小。

    • Dimension getSize()1.1

    • void setSize(Dimension d)1.1

      获取或设置当前组件的size属性。

    API java.awt.Window 1.0

    • void toFront()

      将这个窗口显示在其他窗口前面。

    • void toBack()

      将这个窗口移到桌面窗口栈的后面, 并相应地重新排列所有的可见窗口。

    • boolean isLocationByPlatform() 5.0

    • void setLocationByPlatform(boolean b) 5.0

      获取或设置 locationByPlatform 属性。这个属性在窗口显示之前被设置,由平台选择 一个合适的位置。

    API java.awt.Window 1.0

    • boolean isResizable( )

    • void setResizable(boolean b )

      获取或设置 resizable 属性。这个属性设置后, 用户可以重新设置框架的大小。

    • String getTitle( )

    • void setTitle(String s )

      获取或设置 title 属性, 这个属性确定框架标题栏中的文字。

    • Image getIconImage( )

    • void setIconImage( Image image )

      获取或设置 iconlmage 属性, 这个属性确定框架的图标。窗口系统可能会将图标作为 框架装饰或其他部位的一部分显示。

    • boolean isUndecoratecK ) 1.4

    • void setUndecorated(boolean b ) 1.4

      获取或设置 undecorated 属性。这个属性设置后, 框架显示中将没有标题栏或关闭按 钮这样的装饰。在框架显示之前,必须调用这个方法。     • int getExtendedState() 1.4

    • void setExtendedState(int state) 1.4

      获取或设置窗口状态。状态是下列值之一。

Frame.NORMAL
Frame.ICONIFIED
Frame.MAXIMIZED_HORIZ
Frame.MAXIMIZED_VERT
Frame.MAXIMIZED_BOTH

    API java.awt.Toolkit 1.0

    • static Toolkit getDefaultToolkit( )

      返回默认的工具箱。

    • Dimension getScreenSize()

      返回用户屏幕的尺寸。

    API java.swing.ImageIcon 1.2

    • ImageIcon(String filename)

      构造一个图标, 其图像存储在一个文件中。

    • Image getlmage( )

      获得该图标的图像。

10.4 在组件中显示信息

  本节将论述如何在框架内显示信息。 例如, 我们不再像第 3 章那样, 采用文本方式将 “ Not a Hello, World program” 显示在控制台窗口中, 而是在框架中显示这个消息, 如图 10-7 所示。

  可以将消息字符串直接绘制在框架中, 但这并不是一种好的编程习惯。在 Java 中, 框架被设计为放置组件的容器, 可以将菜单栏和其他的用户界面元素放置在其中。在通常情 况下,应该在另一组件上绘制信息,并将这个组件添加到框 架中。

  JFrame 的结构相当复杂。在图 10-8中给出了 JFrame 的 结构。可以看到,在 JFrame 中有四层面板。其中的根面板、 层级面板和玻璃面板人们并不太关心;它们是用来组织菜单栏和内容窗格以及实现观感的。 Swing 程序员最关心的是内容窗格(content pane) 。在设计框架的时候, 要使用下列代码将所 有的组件添加到内容窗格中:

Container contentPane = frame.getContentPane();
Component c = . . .;
contentPane.add (c);

    在 Java SE 1.4 及以前的版本中, JFrame 类中的 add 方法抛出了一个异常信息“ Do not use JFrame.add().Use JFrame.getContentPane().add instead”。 如今, JFrame.add 方法不再显示这些提示信息,只是简单地调用内容窗格的 add,

    因此,可以直接调用

frame.add(c);

    在这里,打算将一个绘制消息的组件添加到框架中。绘制一个组件,需要定义一个扩展JComponent 的类,并覆盖其中的 paintComponent 方法。     paintComponent 方法有一个 Graphics 类型的参数, 这个参数保存着用于绘制图像和文本的设置,例如,设置的字体或当前的颜色。在 Java 中,所有的绘制都必须使用 Graphics 对 其中包含了绘制图案、 图像和文本的方法。

    注释: Graphics 参数与 Windows 中的设备环境或 X11程序设计中的图形环境基本类似。

    下列代码给出了如何创建一个能够进行绘制的组件:

class MyComponent extends JComponent
  public void paintComponent(Graphics g)
    code for drawing

    无论何种原因, 只要窗口需要重新绘图, 事件处理器就会通告组件,从而引发执行所有组件的 paintComponent 方法。

    一定不要自己调用 paintComponent 方法。在应用程序需要重新绘图的时候, 这个方法将被自动地调用,不要人为地干预这个自动的处理过程。     何种类别的动作会触发这个自动响应过程呢? 例如,在用户扩大窗口或极小化窗口,然 后又恢复窗口的大小时会引发重新绘图。如果用户弹出了另外一个窗口,并且这个窗口覆盖 了一个已经存在的窗口,使得覆盖的窗口不可见,则此时被覆盖的应用程序窗口被破坏,需要重新绘制(图形系统不保存下面的像素。) 当然,窗口第一次显示时,需要处理一些代码, 主要包含确定绘制最初元素的方式以及位置。

    提示:如果需要强制刷新屏幕, 就需要调用 repaint 方法, 而不是 paintComponent 方法。 它将引发采用相应配置的 Graphics 对象调用所有组件的 paintComponent 方法 。

    从上述代码片段中可以看到, paintComponent 方法只有一个 Graphics 类型的参数。对于 屏幕显示来说,Graphics 对象的度量单位是像素。 坐标( 0,0 ) 指出所绘制组件表面的左上角。

    显示文本是一种特殊的绘图。在 Graphics 类中有一个 drawstring方法, 调用的语法格式为:

g.drawString(text, x, y)

    在这里, 打算在原始窗口大约水平 1/4, 垂直 1/2 的位置显示字符串“ Not a Hello, World program" 。 现在,尽管不知道应该如何度量这个字符串的大小, 但可以将字符串的开始位置定义在坐标( 75, 100 )。这就意味着字符串中的第一个字符位于从左向右 75 个像素, 从上向下 100 个像素的位置(实际上, 文本的基线位于像素 100 的位置, 有关文本的度量方式将 在稍后阐述)。因此, paintComponent 方法的书写内容如下所示:

public class NotHel1oWorldComponent 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);

    最后, 组件要告诉用户它应该有多大。覆盖 getPreferredSize 方法, 返回一个有首选宽度 和高度的 Dimension 类对象:

public class NotHel1oWorldComponent extends JComponent
    private static final int DEFAULT.WIDTH = 300;
    private static final int DEFAULT.HEIGHT = 200;
    public Dimension getPreferredSize() { return new Dimension(DEFAULT_WIDTH, DEFAULT_HEIGHT); }

    在框架中填入一个或多个组件时, 如果你只想使用它们的首选大小, 可以调用 pack 方法 而不是 setSize 方法:

class NotHelloWorldFrame extends JFrame
    public NotHelloWorldFrame ()
        add(new NotHel1oWorldComponent());
        pack();

    程序清单 10-3 给出了完整的代码。

    注释: 有些程序员更喜欢扩展 JPanel, 而不是 JComponent。JPanel 是一个可以包含其他组件的容器( container), 但同样也可以在其上面进行绘制。有一点不同之处是, 面板不透明, 这意味着需要在面板的边界内绘制所有的像素。 最容易实现的方法是, 在每个面板子类的 paintComponent 方法中调用 super.paintComponent 来用背景色绘制面板:

class NotHel1oWorldPanel extends JPanel
    public void paintComponent(Craphics g)
        super.paintComponent (g);
        code for drawing

  API java.swing.JFrame 1.2

  • Container getContentPane( )

    返回这个 JFrame 的内容窗格对象。

  • Component add(Component c )

    将一个给定的组件添加到该框架的内容窗格中(在 Java SE 5.0 以前的版本中,这个方 法将抛出一个异常)。

  API java.awt.Component 1.0

  • void repaint( )

    “ 尽可能快地” 重新绘制组件。

  • Dimension getPreferredSize( )

    要覆盖这个方法,返回这个组件的首选大小。

  API java.swing.JComponent 1.2

  • void paintComponent(Grphics g)

    覆盖这个方法来描述应该如何绘制自己的组件。

  API java.awt.Window 1.0

  • void pack( )

    调整窗口大小,要考虑到其组件的首选大小。

10.5 处理2D图形

  自从 Java 版本 1.0 以来, Graphics 类就包含绘制直线、矩形和椭圆等方法。但是,这些 绘制图形的操作能力非常有限。例如, 不能改变线的粗细,不能旋转这些图形。

  Java SE 1.2引入了 Java 2D 库,这个库实现了一组功能强大的图形操作。在本章中,只 介绍 Java 2D 库的基础部分,有关高级功能的详细内容请参看卷 II 的第 7 章。

  要想使用 Java 2D 库绘制图形, 需要获得一个 Graphics2D 类对象。这个类是 Graphics 类 的子类。自从 Java SE 2 版本以来,paintComponent 方法就会自动地获得一个 Graphics2D 类 对象,我们只需要进行一次类型转换就可以了。如下所示:

public void paintComponent(Craphics g)
    Craphics2D g2 = (Graphics2D) g;

  Java 2D 库采用面向对象的方式将几何图形组织起来。包含描述直线、 矩形的椭圆的类:

Line2D
Rectangle2D
Ellipse2D

  这些类全部实现了 Shape 接口。

  注释:Java 2D 库支持更加复杂的图形, 例如圆弧、二次曲线、三次曲线和通用路径。有关更详细的内容请参看卷 2 第 7 章。

  要想绘制图形,首先要创建一个实现了 Shape 接口的类的对象, 然后调用 GraphicS2D 类 中的 draw 方法。例如,

Rectangle2D rect = . . .;
g2.draw(rect);

  注释: 在 Java 2D 库出现之前, 程序员使用 Grpahics 类中的 drawRectangle 方法绘制图形。从表面上看, 老式风格的方法调用起来好像更加简单一点。然而,使用 Java 2D 库, 可以选择 Java 2D 库中提供的一些工具提高绘制能力。

  使用 Java 2D 图形类或许会增加一些复杂度。在 1.0 的绘制方法中, 采用的是整型像素坐标, 而 Java 2D 图形采用的是浮点坐标。在很多情况下,用户可以使用更有意义的形式 (例如,微米或英寸)指定图形的坐标,然后再将其转换成像素,这样做很方便。在 Java 2D 库中, 内部的很多浮点计算都采用单精度 float。毕竟, 几何计算的最终目的是要设置屏幕或 打印机的像素, 所以单精度完全可以满足要求了。只要舍入误差限制在一个像素的范围内, 视觉效果就不会受到任何影响。

  然而,有时候程序员处理 float 并不太方便, 这是因为 Java 程序设计语言在将 double 值 转换成 float 值时必须进行类型转换。例如,考虑下列的语句:

float f = 1.2; // Error

  这条语句无法通过编译,因为常量 1.2 属于 double 类型,而编译器不允许丢失精度。解 决的方法是给浮点常量添加一个后缀 F:

float f = 1.2F; // Ok

  现在,看一下这条语句:

Rectang1e2D r = . . .
float f = r.getWidth(); // Error

  这条语句也无法通过编译, 其原因与前面一样。由于 getWidth 方法的返回类型是 double, 所以需要进行类型强制转换:

float f = (float) r.getWidth(): // OK

  由于后缀和类型转换都有点麻烦, 所以 2D 库的设计者决定为每个图形类提供两个版本: 一个是为那些节省空间的程序员提供的 float 类型的坐标;另一个是为那些懒惰的程序员提供 的 double 类型的坐标(本书主要采用的是第二个版本, 即 double 类型的坐标)。

  这个库的设计者选择了一种古怪且在最初看起来还有些混乱的方式进行了打包。看一下 Rectangle2D 类, 这是一个拥有两个具体子类的抽象类,这两个具体子类也是静态内部类:

Rectangle2D.Float
Rectang1e2D.Double

  图 10-9显示了它们之间的继承示意图。

  最好淡化这两个具体类是静态内部类, 这样做只是为了避免使用 FloatRectangle2D 和 DoubleRectangle2D 这样的名字(有关静态内部类更详细的信息请参看第 6 章)。

  当 创 建 一 个 Rectangle2D.Float 对 象 时, 应 该 提 供 float 型 数 值 的 坐 标。 而 创 建 Rectangle2D. Double 对象时,应该提供 double 型数值的坐标。

Rectangle2D.FIoat floatRect = new Rectangle2D.Float(10.OF, 25.OF, 22.SF, 20.OF);
Rectangle2D.Double doubleRect = new Rectangle2D.Double(10.0, 25.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, 25.0, 22.5, 20.0);

  也就是说, 只有在构造图形对象时,才需要使用烦人的内部类。

  构造参数表示矩形的左上角位置、宽和髙。

  注释: 实际上, Rectangle2D.Float 类包含了一个不是由 Rectangle2D 继承而来的附加方法 setRect(float x, float y, float h, float w) 。如果将 Rectangle2D.Float 的引用存储在 Rectangle2D 变量中, 那就会失去这个方法。但是,这也没有太大关系, 因为在 Rectangle2D 中有一个 参教为 double 类型的 setRect 方法。

  Rectangle2D方法的参数和返回值均为 double 类型。例如, 即使 Rectangle2D.Float 对象 存储 float 类型的宽度,getWidth 方法也返回一个 double 值。

  提示:直接使用 Double 图形类可以避免处理 float 类型的值, 然而如果需要创建上千个 图形对象, 还是应该考虑使用 Float 类,这样可以节省存储空间

  前面对 Rectangle2D 类的论述也适用于其他图形类。另外,Point2D 类也有两个子类Point2D.Float 和 Point2D.Double。下面是构造一个点对象的方法:

Point2D p = new Point2D.Double (10 , 20);

  提示:Point2D 类是很有用的。使用 Point2D 对象比使用单独的 x 和 y 更加具有面向对象的风格。许多构造器和方法都接收 Point2D 型参数, 我们建议在可能的情况下使用 Point2D 对象。这样会使几何计算更容易理解。

  Rectangle2D 和 Ellipse2D 类都是由公共超类 RectangularShape 继承来的。无可非议, 楠 圆不是矩形, 但它们都有着矩形边界, 如图 10-10 所示。

  RectangularShape 类定义了 20 多个有关图形操作的通用方法, 其中比较常用的方法有 getWidth、getHeight、 getCenterX、getCenterY 等(但在写本书时,getCenter 方法 还不能以 Point2D 对象的形式返回中心位置)。

  最后, 从 Java 1.0 遗留下来的两个类也被放置在图形类的继承层次中。它们是 Rectangle 和 Point 类, 分别扩展于 Rectangle2D 和 Point2D 类,并用整型坐标存储矩形和点。

  图 10-11 给出了图形类之间的关系。 不过, 省略了Double 和 Float 子类。图中的遗留类采用填充灰色的方式标记。

  Rectangle2D 和 Ellipse2D 对象很容易构造。需要给出

  • 左上角的 x 和 y 坐标;

  • 宽和高。

  对于椭圆,这些内容代表外接矩形。例如,

E11ipse2D e = new Ellipse2D.Double(150, 200, 100, 50);

  用左上角位于(150, 200) 、宽 100、高 50 的外接矩形构造一个椭圆。

  然而,有时候并不知道左上角的位置。经常得到的是矩形的两个对角点, 而这两个对角不一定是左上角和右下角。不能直接这样构造一个矩形:

Rectangle2D rect = new Rectangle2D.0ouble(px, py, qx - px, qy - py); // Error

  如果 p 不是左上角,两个坐标之差就为负,矩形就为空。在这种情况下,首先创建一个 空矩形,然后调用 setFrameFromDiagonal 方法,如下所示:

Rectangle2D rect = new Rectangle2D.Doub1e();
rect.setFrameFromDiagonal (px, py, qx, qy);

  或者,如果已知的顶点分别用 Point2D 类型的两个对象 p 和 q 表示,就应该这样调用:

rect.setFrameFroeiiiDiagonal (p, q);

  在构造楠圆(这种情况还出现在构造其他图形时)时,通常可以知道椭圆的中心、宽和 高,而不是外接矩形的顶点。setFrameFromCenter 方法使用中心点,但仍然要给出四个顶点中的一个。因此,通常采用下列方式构造橢圆:

E11ipse2D ellipse = new E111pse2D.Doub1e(centerX - width / 2, centerY - height / 2, width, height);

  要想构造一条直线,需要提供起点和终点。这两个点既可以使用 Point2D 对象表示,也可以使用一对数值表示:

Line2D line = new Line2D.Double(start, end);
Line2D line = new Line2D.0ouble(startX, startY, endX, endY);

  程序清单 10-4 中的程序绘制了一个矩形;这个矩形的内接椭圆;矩形的对角线以及以矩形中心为圆点的圆。图 10-12 显示了结果。(略过)

   API java.awt.geom.RectangularShape 1.2

  • double getCenterX( )

  • double getCenterY( )

  • double getMinX( )

  • double getMinY( )

  • double getMaxX( )

  • double getMaxY( )

    返回闭合矩形的中心, 以及最小、 最大 x 和 y 坐标值。

  • double getWidth( )

  • double getHeight( )

    返回闭合矩形的宽和高。

  • double getX( )

  • double getY( )

    返回闭合矩形左上角的 x 和 y 坐标。

  API java.awt.geom.Rectangular2D.Double 1.2

  • Rectangle2D . Double(double x ,double y , double w , double h )

    利用给定的左上角、宽和高, 构造一个矩形。

  API java.awt.geom.Rectangular2D.Float 1.2

  • Rectangle2D . FIoat(f1oat x ,float y,float w ,float h )

    利用给定的左上角、 宽和高, 构造一个矩形。

  API java.awt.geom.Ellipse2D.Double 1.2

  • Ellipse2D . Double( double x , double y ,double w , double h )

    利用给定的左上角、 宽和高的外接矩形, 构造一个椭圆。

  API java.awt.geom.Point2D.Double 1.2

  • Point2D . Double(double x , double y )

    利用给定坐标构造一个点。

  API java.awt.geom.Line2D.Double 1.2

  • Line2D .Double(Point2D start , Point2D end )

  • Line2D . Double(double startX , double startY, double endX , double endY )

    使用给定的起点和终点, 构造一条直线。

10.6 使用颜色

  使用 Graphics2D 类的 setPaint 方法可以为图形环境上的所有后续的绘制操作选择颜色。 例如:

g2.setPaint(Color.RED);
g2.drawString("Warning!", 100, 100);

  只需要将调用 draw 替换为调用 fill 就可以用一种颜色填充一个封闭图形(例如: 矩形或 椭圆)的内部:

Rectangle2D rect = . . .;
g2 .setPaint(Color.RED);
g2 .fill(rect); // fills rect with red

  要想绘制多种颜色, 就需要按照选择颜色、 绘制图形、 再选择另外一种颜色、 再绘制图形的过程实施。

  注释: fill 方法会在右侧和下方少绘制一个像素。 例如, 如果绘制一个 new Rectangle2D. Double(0, 0, 10, 20), 绘制的矩形将包括 x = 10 和 y = 20 的像素。如果填充这个矩形, 则不会绘制 x = 10 和 y = 20 的像素。

  Color 类用于定义颜色。在 java.awt.Color 类中提供了 13 个预定义的常量, 它们分別表 示 13 种标准颜色。

BLACK , BLUE, CYAN, DARK.GRAY, CRAY, GREEN, LIGHT_CRAY , MACENTA, ORANGE, PINK, RED, WHITE, YELLOW

  注释: 在 Java SE 1.4 之前的版本中, 颜色常量的名字为小写形式, 例如, Color.red。 这似乎有些超出寻常, 因为标准编码的惯例是采用大写形式书写常量。现在可以采用大写的形式书写标准颜色的名字,不过,为了向后兼容, 也可以用小写形式书写。

  可以通过提供红、 绿和蓝三色成分来创建一个 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);

  注释: 除了纯色以外,还可以选择更复杂的“ 颜料” 设置,例如, 改变色调( hue) 或者图像。有关这方面更加详细的内容请参看卷 2 中的高级 AWT 章节。如果使用 Graphics 对象, 而不是 Graphics2D 对象, 就需要使用 setColor 方法设置颜色。

  要想设置背景颜色, 就需要使用 Component 类中的 setBackground 方法。Component 类 是 JComponent 类的祖先。

MyComponent p = new HyComponent():
p.setBackground(Color.PINK);

  另外,还有一个 setForeground方法,它是用来设定在组件上进行绘制时使用的默认颜色。

  提示:从名字就可以看出,Color 类中的 brighter( ) 方法和 darker( ) 方法的功能, 它们分别加亮或变暗当前的颜色。使用 brighter 方法也是加亮条目的好办法。实际上, brighter() 只微微地加亮一点。要达到耀眼的效果, 需要调用三次这个方法:c.brighter( ). brighter( ). brighter( )。

  Java 在 SystemColor 类中预定义了很多颜色的名字。在这个类中的常量, 封装了用户系统的各个元素的颜色。例如,

p.setBackground(SystemColor.window)

  它将把面板的背景颜色设定为用户桌面上所有窗口使用的默认颜色。(无论何时重新绘制窗口, 都会填充背景颜色。)当希望让绘制的用户界面元素与用户桌面上已经存在的其他元素的颜色匹配时, 使用 SystemColor 类中的颜色非常有用。表 10-1 列出了系统颜色的名字和它们的含义。

  • Paint getPaint( )

  • void setPaint( Paint p )

    获取或设置这个图形环境的绘制属性。Color 类实现了 Paint 接口。因此, 可以使用这个方法将绘制属性设置为纯色。

  • void fill ( Shape s)

    用当前的颜料填充该图形。

  API java.awt.Componet 1.0

  • Color getBackground( )   

  • void set Background(Color c )

    获取或设置背景颜色。

    参数:c 新背景颜色

  • Color getForeground( )

  • void setForeground(Color c )

    获取或设置前景颜色。

    参数:c 新前景颜色  

10.7 文本使用特殊字体

  在本章开始的“ Not a Hello, World” 程序中用默认字体显示了一个字符串。实际上,经常希望选用不同的字体显示文本。人们可以通过字体名( font face name) 指定一种字体。字体名由“ Helvetica” 这样的字体家族名(font family name) 和一个可选的“ Bold” 后缀组成。 例如,“ Helvetica” 和“ HelveticaBold” 属于“ Helvetica ” 家族的字体。

  要想知道某台特定计算机上允许使用的字体, 就需要调用 GraphicsEnvironment 类中的 getAvailableFontFamilyNames 方法。这个方法将返回一个字符型数组, 其中包含了所有可用 的字体名。GraphicsEnvironment 类描述了用户系统的图形环境, 为了得到这个类的对象,需要 调用静态的 getLocalGraphicsEnvironment 方法。下面这个程序将打印出系统上的所有字体名:

import java.awt.*;
public class ListFonts
    public static void main(String[] args)
        String[] fontNames = GraphicsEnvironment
            .getLocalGraphicsEnvironment()
            .getAvailableFontFamilyNames();
        for (String fontName : fontNames)
            System.out.println(fontName);

  在某个系统上,输出的结果为:

Abadi MT Condensed Light
Arial
Arial Black
Arial Narrow
Arioso
Baskerville
Binner Gothic

  后面还有 70 种左右的字体。

  字体名可以商标化, 字体设计在一些权限内可以版权化。因此,字体的分发需要向字体的创始者付版税。当然,像名牌香水有廉价仿制品一样, 字体也有外观相似的。例如, Helvetica 的仿制品就是 Windows 中称为 Arial 的字体。 为了创建一个公共基准, AWT 定义了五个逻辑(logical) 字体名:

SansSerif
Serif
Monospaced
Dialog
Dialoglnput

  这些字体将被映射到客户机上的实际字体。例如,在 Windows 系统中, SansSerif将被映 射到 Arial。

  另外,Oracle JDK 包含 3 种字体,它们是“ Lucida Sans”,“ Lucida Bright” 和“ Lucida Sans Typewriter" 。

  要想使用某种字体绘制字符, 必须首先利用指定的字体名、字体风格和字体大小来创建 —个 Font 类对象。下面是构造一个 Font 对象的例子:

Font sansboldl4 = new Font(°SansSerif", Font.BOLD, 14);

  第三个参数是以点数目计算的字体大小。点数目是排版中普遍使用的表示字体大小的单位,每英寸包含 72 个点。

  在 Font 构造器中,提供字体名的位置也可以给出逻辑字体名称。另外, 利用 Font 构造器的 第二个参数可以指定字体的风格(常规、加粗、斜体或加粗斜体,) 下面是几个字体凤格的值:

Font.PLAIN
Font.BOLD
Font.ITALIC
Font.BOLD + Font.ITALIC

  注释: 字体映射定义在 Java 安装的 jre/lib 子目录中的 fontconfig.properties 文件中。 有 关 这 个 文 件 的 详 细 内 容 请 参 看 http://docs.oracle.eom/javase/8/docs/tedmotes/guides/intl/ fontconfig.html o

  可以读取 TrueType 或 PostScriot Type 1 格式的字体文件。这需要一个字体输入流 通常从磁盘文件或者 URL 读取(有关流的更详细信息请参看卷 2 第 1 章。) 然后调用静态方法 Font.createFont:

URL url = new URL("http://www.fonts.com/Wingbats.ttf");
InputStream in = url.openStream();
Font fl = Font.createFont(Font.TRUETYPE_FONT, in);

  上面定义的字体为常规字体,大小为 1 。 可以使用 deriveFont 方法得到希望大小的字体:

Font f = fl.deriveFont(14.0F);

  警告: deriveFont 方法有两个重载版本。一个(有一个 float 参数)设置字体的大小;另一 个(有一个 int 参数)设置字体风格。所以 f.deriveFont(14) 设置的是字体风格, 而不是大小(其结果为斜体, 因为 14 的二进制表示的是 ITALIC, 而 不 是 BOLD)。

  Java 字体 包 含 了 通 用 的 ASCII 字 符 和 符 号。 例 如, 如 果 用 Dialog 字 体 打 印 字 符 ‘ \u2297’,那么就会看到 × 字符。只有在 Unicode 字符集中定义的符号才能够使用。

  下面这段代码将使用系统中丨4 号加粗的标准 sans serif字体显示字符串“ Hello,World”:

Font sansbo1dl4 = new Font("SansSerif", Font.BOLD, 14);
g2.setFont(sansbo1dl4);
String message = "Hello, World!";
g2.drawString(message, 75, 100);

  接下来, 将字符串绘制在面板的中央,而不是任意位置。因此, 需要知道字符串占据的 宽和高的像素数量。这两个值取决于下面三个因素:

  • 使用的字体(在前面列举的例子中为 sans serif,加粗,14 号;

  • 字符串(在前面列举的例子中为“ Hello,World”;)

  • 绘制字体的设备(在前面列举的例子中为用户屏幕)。

  要想得到屏幕设备字体属性的描述对象, 需要调用 GraphicS2D 类中的 getFontRenderContext 方法。它将返回一个 FontRenderContext 类对象。可以直接将这个对象传递给 Font 类 的 getStringBounds 方法:

FontRenderContext context = g2.getFontRenderContext();
Rectangle2D bounds = sansboldl4..getStringBounds(message, context);

  getStringBounds 方法将返回包围字符串的矩形。

  为了解释这个矩形的大小, 需要清楚几个排版的相关术语。如图 10-13 所示。基线 (baseline) 是一条虚构的线, 例如,字母“ e” 所在的底线。上坡度(ascent) 是从基线到坡顶 ( ascenter) 的距离。例如,“ b” 和“ k” 以及大写字母的上面部分。下坡度(descent) 是从基 线到坡底(descenter) 的距离, 坡底是“ p” 和“ g” 这种字母的底线。

  行间距( leading) 是某一行的坡底与其下一行的坡顶之间的空隙(这个术语源于排字机分隔行的间隔带。) 字体的高度是连续两个基线之间的距离, 它等于下坡度 + 行间距 + 上坡度。

  getStringBounds 方法返回的矩形宽度是字符串水平方向的宽度。矩形的高度是上坡度、 下坡度、 行间距的总和。这个矩形始于字符串的基线,矩形顶部的 y 坐标为负值。因此,可 以采用下面的方法获得字符串的宽度、 髙度和上坡度:

double stringWidth = bounds.getWidth();
double stringHeight = bounds.getHeight();
double ascent = -bounds.getY();

  如果需要知道下坡度或行间距,可以使用 Font 类的 getLineMetrics 方法。这个方法将返 回一个 LineMetrics 类对象,获得下坡度和行间距的方法是:

LineMetrics metrics = f.getLineMetrics(message, context);
float descent = raetrics.getDescent();
float leading = metrics.getLeading();

  下面这段代码使用了所有这些信息, 将字符串显示在包围它的组件中央:

FontRenderContext context = g2.getFontRenderContext();
Rectang1e2D bounds = f.getStringBounds(message, context);
// (x,y) = top left comer 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; g2.drawString(message, (int) x, (int) baseY);

  为了能够获得中央的位置, 可以使用 getWidth() 得到组件的宽度。 使用 bounds. getWidth( ) 得到字符串的宽度。前者减去后者就是两侧应该剩余的空间。 因此, 每侧剩余的空间应该是这个差值的一半。高度也是一样。

  注释: 如果需要在 paintComponent 方法外部计算布局图 的尺度, 不能从 Graphics2D 对象得到字体绘制环境。换 作调用 JComponent 类的 getFontMetrics 方法, 而后紧接 着调用 getFontRenderContext:

FontRenderContext context = getFontMetncs(f) .getFontRenderContext ();

   为了说明位置是正确的, 示例程序绘制了基线和包围这个字符串的矩形。 程序清单 10-5 中是相应的代码。(略过)

  API java.awt.Font 1.0

  • Font(String name, int style, int size)

    创建一个新字体对象。

    参数:   name 字体名。 不是字体名(例如,“ Helvetica Bold”), 就是逻辑字体名 ( 例如,“ Serif”、“ SansSerif”)

        style 字体风格(Font.PLAIN、 Font.BOLD、 Font.ITALIC 或 Font.BOLD+Font. ITALIC)

        size 字体大小(例如, 12 )

  • String getFontName( ) 返回字体名, 例如,“ Helvetica Bold”。

  • String getFamily( ) 返回字体家族名,例如,“ Helvetica”。

  • String getName( ) 如果采用逻辑字体名创建字体, 将返回逻辑字体, 例如,“ SansSerif”;否则, 返回字 体名。

  • Rectangle2D getStringBounds(String s , FontRenderContext context) 1.2

    返回包围这个字符串的矩形。矩形的起点为基线。矩形顶端的 y 坐标等于上坡度的负 值。矩形的高度等于上坡度、下坡度和行间距之和。宽度等于字符串的宽度。

  •  LineMetrics getLineMetrics(String s , FontRenderContext context) 1.2

    返回测定字符串宽度的一个线性 metrics 对象。 • Font deriveFont(int style) 1.2

  • Font deriveFont(f1oat size) 1.2

  • Font deriveFont(int style, float size) 1.2

    返回一个新字体, 除给定大小和字体风格外, 其余与原字体一样。

  API java.awt.font.LineMetncs 1.2

  • float getAscent( ) 返回字体的上坡度—从基线到大写字母顶端的距离。

  • float getDescent( ) 返回字体的下坡度—从基线到坡底的距离。

  • float getLeading( ) 返回字体的行间距—从一行文本底端到下一行文本顶端之间的空隙。

  • float getHeight( ) 返回字体的总高度—两条文本基线之间的距离(下坡度 + 行间距 + 上坡度)。

  API java.awt.Graphics 1.0

  • Font getFont( )

  • void setFont(Font font)

    获取或设置当前的字体。这种字体将被应用于后续的文本绘制操作中。

    参数:font —种字体

  • void drawString(String str, int x, int y)

    采用当前字体和颜色绘制一个字符串。

    参数:str 将要绘制的字符串

      x 字符串开始的 x 坐标

      y 字符串基线的 y 坐标

  API java.awt.Graphics2D 1.2

  • FontRenderContext getFontRenderContext()

    返回这个图形文本中, 指定字体特征的字体绘制环境。

  • void drawString(String str, float x, float y)

    采用当前的字体和颜色绘制一个字符串。

    参数: str 将要绘制的字符串

      x 字符串开始的 x 坐标

      y 字符串基线的 y 坐标

  API javax.swing.JComponent 1.2

  • FontMetrics getFontMetrics(Font f) 5.0 获取给定字体的度量。FontMetrics 类是 LineMetrics 类的早先版。

  API java.awt.FontMetrics 1.0

  • FontRenderContext getFontRenderContext() 1.2 返回字体的字体绘制环境。

10.8 显示图像

  到目前为止,我们已经看到了如何通过绘制直线和图形创建一个简单的图像。而对于照 片这样的复杂图像来说,通常都是由扫描仪或特殊的图像处理软件生成的(正像在卷2中将 看到的,逐像素地生成图像。

   一旦图像保存在本地文件或因特网的某个位置上,就可以将它们读到 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 又前进了一步, 它在一个窗口中平铺显 示了一幅图像。 屏幕显示的结果如图 10-15 所示。这里采用 paintComponent 方法来实现平铺显示。它的基本过程为: 先在左上角显示图像的一个拷贝, 然后使用 copyArea 将其 拷贝到整个窗口:

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);

  程序清单 10-6 列出了图像显示程序的完整代码。

  API java.awt.Graphics 1.0

  • boolean drawlmage( Image img, int x , int y, ImageObserver observer )

    绘制一幅非比例图像。

    注意: 这个调用可能会在图像还没有绘制完毕就返回。

    参数: img 将要绘制的图像

        x 左上角的 x 坐标

        y 左上角的 y 坐标

        observer 绘制进程中以通告为目的的对象(可能为 null)。

  • boolean drawlmage(Image img, int x,int y,int width,int height , ImageObserver observer )

    绘制一幅比例图像。系统按照比例将图像放人给定宽和高的区域。注意:这个调用可能会在图像还没有绘制完毕就返回。

    参数: img 将要绘制的图像

      x 左上角的x坐标

      y 左上角的 y 坐标

      width 描述图像的宽度

      height 描述图像的髙度

      observer 绘制进程中以通告为目的的对象(可能为 null)

  • void copyArea(int x,int y,int width,int height,int dx,int dy)

    拷贝屏幕的一块区域。

      x 原始区域左上角的 x 坐标

      y 原始区域左上角的 y 坐标

      width 原始区域的宽度

      height 原始区域的高度

      dx 原始区域到目标区域的水平距离

      dy 原始区域到目标区域的垂直距离

  以上完成了有关 Java 图形编程的内容介绍。关于更高级的技术,可以参看卷 II 中有关 2D 图形和图像管理的讨论。在下一章,读者将学习如何让程序对用户的输人进行响应。