Java8-API-入门手册-全-
Java8 API 入门手册(全)
原文:Beginning Java 8 APIs
协议:CC BY-NC-SA 4.0
一、Swing 简介
在本章中,您将学习
- 什么是秋千
- 基于字符的界面和图形用户界面的区别
- 如何开发最简单的 Swing 程序
- 什么是 JFrame,它是如何由不同的组件组成的
- 如何向 JFrame 添加组件
- 什么是布局管理器以及 Swing 中不同类型的布局管理器
- 如何创建可重复使用的框架
- 如何处理事件
- 如何处理鼠标事件以及如何使用适配器类来处理鼠标事件
什么是 Swing?
Swing 提供了图形用户界面(GUI)组件来开发 Java 应用,其中包含一组丰富的图形,如窗口、按钮、复选框等。什么是图形用户界面?在我定义 GUI 之前,让我先定义一个用户界面(UI)。程序做三件事:
- 接受用户的输入
- 处理输入,以及
- 产生输出
根据输入和输出,用户界面提供了一种在用户和程序之间交换信息的手段。换句话说,用户界面定义了用户和程序之间交互的方式。使用键盘键入文本、使用鼠标选择菜单项或单击按钮都可以为程序提供输入。程序的输出可以以基于字符的文本、诸如条形图的图形、图片等形式显示在计算机显示器上。
你写过很多 Java 程序。您已经看到了这样的程序,用户必须在控制台上以文本输入的形式向程序提供输入,而程序会在控制台上打印输出。用户输入和程序输出都是文本形式的用户界面被称为基于字符的用户界面。GUI 允许用户使用称为控件或小部件的图形元素,使用键盘、鼠标和其他设备与程序进行交互。
图 1-1 显示了一个程序,让用户输入一个人的姓名和出生日期(DOB ),并使用键盘保存信息。这是一个基于字符的用户界面的例子。

图 1-1。
An example of a program with a character-based user interface
图 1-2 让用户执行相同的动作,但是使用图形用户界面。它在一个窗口中显示六个图形元素。它使用了两个标签(Name:和DOB:)、两个文本字段(用户将在其中输入Name和DOB值)以及两个按钮(Save和Close)。与基于字符的用户界面相比,图形用户界面使用户与程序的交互更加容易。你能猜出你打算在这一章开发什么样的应用吗?这将是所有关于图形用户界面。GUI 开发很有趣,比基于字符的程序开发稍微复杂一些。一旦你理解了 GUI 开发中涉及的元素,使用它会很有趣。

图 1-2。
An example of a program with a graphical user interface
本章试图介绍使用 Swing 组件和顶级容器进行 GUI 开发的基础知识。已经注意为那些以前可能没有使用任何编程语言/工具(例如,Visual C++、Visual Basic、VB.NET 或 PowerBuilder)来开发 GUI 的程序员解释 GUI 相关的细节。如果你已经使用了一种 GUI 开发语言/工具,你会更容易理解本章的内容。Swing 是一个庞大的主题,不可能涵盖它的每个细节。它本身就值得一本书。事实上,市场上有几本书专门介绍 Swing。
容器是一个可以容纳其他组件的组件。最高级别的容器称为顶级容器。一个JFrame、一个JDialog、一个JWindow和一个JApplet是顶级容器的例子。一个JPanel就是一个简单容器的例子。一个JButton,一个JTextField,等等。都是组件的例子。在 Swing 应用中,每个组件都必须包含在一个容器中。容器被称为组件的父组件,组件被称为容器的子组件。这种父子关系(或容器包含关系)被称为包含层次结构。要在屏幕上显示组件,顶级容器必须位于容器层次结构的根。每个 Swing 应用必须至少有一个顶级容器。图 1-3 显示了一个 Swing 应用的包容层次结构。顶级容器包含一个名为“容器 1”的容器,容器 1 又包含一个名为“组件 1”的组件和一个名为“容器 2”的容器,容器 2 又包含两个名为“组件 2”和“组件 3”的组件

图 1-3。
Containment hierarchy in a Swing application
最简单的 Swing 程序
让我们从最简单的 Swing 程序开始。您将显示一个JFrame,这是一个顶级容器,其中没有任何组件。要创建并显示一个JFrame,您需要执行以下操作:
- 创建一个
JFrame对象。 - 让它可见。
要创建一个JFrame对象,可以使用JFrame类的一个构造函数。其中一个构造函数接受一个字符串,该字符串将显示为JFrame的标题。代表 Swing 组件的类在javax.swing包中,同样的还有JFrame类。下面的代码片段创建了一个标题设置为“Simplest Swing”的JFrame对象:
// Create a JFrame object
JFrame frame = new JFrame("Simplest Swing");
当你创建一个JFrame对象时,默认情况下,它是不可见的。你需要调用它的setVisible (boolean visible)方法来使它可见。如果您将true传递给这个方法,JFrame将变得可见,如果您传递false,它将变得不可见。
// Make the JFrame visible on the screen
frame.setVisible(true);
这就是开发第一个 Swing 应用所要做的全部工作!事实上,您可以将创建和显示一个JFrame的两个语句包装成一个语句,如下所示:
new JFrame("Simplest Swing").setVisible(true);
Tip
创建一个JFrame并让它在main线程中可见并不是启动 Swing 应用的正确方式。但是,这对您将在这里使用的琐碎程序没有任何损害,所以我将继续使用这种方法来保持代码简单易学,这样您就可以专注于您正在学习的主题。还需要理解 Swing 中的事件处理和线程机制,才能理解为什么需要以另一种方式启动 Swing 应用。第三章详细解释了如何启动一个 Swing 应用。创建和显示JFrame的正确方法是包装 GUI 创建并使其在Runnable中可见,并将Runnable传递给javax.swing.SwingUtilities或java.awt.EventQueue类的invokeLater()方法,如下所示:
import javax.swing.JFrame;
import javax.swing.SwingUtilities;
...
SwingUtilities.invokeLater(() -> new JFrame("Test").setVisible(true));
清单 1-1 有创建和显示一个JFrame的完整代码。运行该程序时,在屏幕左上角显示一个JFrame,如图 1-4 所示。该图显示了程序在 Windows XP 上运行时的画面。在其他平台上,框架看起来可能有点不同。本章中大多数图形用户界面的截图都是在 Windows XP 上拍摄的。
清单 1-1。最简单的 Swing 程序
// SimplestSwing.java
package com.jdojo.swing;
import javax.swing.JFrame;
public class SimplestSwing {
public static void main(String[] args) {
// Create a frame
JFrame frame = new JFrame("Simplest Swing");
// Display the frame
frame.setVisible(true);
}
}
这不是很令人印象深刻,是吗?不要绝望。随着您对 Swing 的了解越来越多,您将会改进这个程序。这只是向您展示了 Swing 所提供功能的冰山一角。
您可以调整图中 1-4 所示的JFrame的大小,使其变大。将鼠标指针放在显示的JFrame的四个边(左、上、右或下)或四个角上。当您将鼠标指针放在JFrame的边缘时,它的形状会变成一个调整大小的指针(一条两端都有箭头的线)。然后只需拖动调整大小鼠标指针,向您想要的方向调整JFrame的大小。

图 1-4。
The Simplest Swing frame
图 1-5 显示了调整后的JFrame。注意,在创建JFrame时传递给构造函数的文本“Simplest Swing”显示在JFrame的标题栏中。

图 1-5。
The Simplest Swing frame after resizing
如何退出 Swing 应用?当运行清单 1-1 中列出的程序时,如何退出?当点击标题栏中的关闭按钮(标题栏上最右边带 X 的按钮)时,JFrame被关闭。但是,程序不会退出。如果您从命令提示符下运行该程序,当您关闭JFrame时,提示符不会返回。您必须强制退出该程序,例如,如果您在 Windows 上从命令提示符运行该程序,请按 Ctrl + C。那么,如何退出 Swing 应用呢?您可以定义一个JFrame的四种行为之一,以确定当JFrame关闭时会发生什么。它们在javax.swing.WindowsConstants接口中被定义为四个常量。JFrame类实现了WindowsConstants接口。您可以使用JFrame.CONSTANT_NAME语法引用所有这些常量(或者您可以使用WindowsConstants.CONSTANT_NAME语法)。这四个常数是
DO_NOTHING_ON_CLOSE:当用户关闭JFrame时,该选项不做任何事情。如果你为一个JFrame设置了这个选项,你必须提供一些其他的方法来退出应用,比如一个Exit按钮或者JFrame中的一个Exit菜单选项。HIDE_ON_CLOSE:该选项只是在用户关闭时隐藏一个JFrame。这是默认行为。这就是当你点击标题栏的关闭按钮来关闭清单 1-1 中列出的程序时发生的情况。JFrame只是变得不可见,程序仍在运行。DISPOSE_ON_CLOSE:该选项在用户关闭JFrame时隐藏并处理。处置一个JFrame会释放它所使用的任何操作系统级资源。注意HIDE_ON_CLOSE和DISPOSE_ON_CLOSE的区别。当您使用选项HIDE_ON_CLOSE时,一个JFrame只是被隐藏,但它仍然使用所有的操作系统资源。如果你的JFrame经常隐藏和显示,你可以使用这个选项。但是,如果您的JFrame消耗了许多资源,您可能希望使用DISPOSE_ON_CLOSE选项,这样资源可以在不显示时被释放和重用。EXIT_ON_CLOSE:该选项退出应用。当JFrame关闭时,设置该选项有效,如同System.exit()被调用。这个选项应该小心使用。此选项将退出应用。如果屏幕上显示不止一个JFrame或任何其他类型的窗口,对一个JFrame使用此选项将关闭所有其他窗口。请谨慎使用此选项,因为当应用退出时,您可能会丢失任何未保存的数据。
您可以通过将四个常量中的一个传递给setDefaultCloseOperation()方法来设置JFrame的默认关闭行为,如下所示:
// Exit the application when the JFrame is closed
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
你用第一个例子解决了一个问题。另一个问题是JFrame显示时没有可视区域。它只显示标题栏。您需要设置JFrame可见之前或之后的大小和位置。框架的大小由其宽度和高度(以像素为单位)来定义,您可以使用其setSize (int width, int height)方法来设置。该位置由相对于屏幕左上角的JFrame左上角的(x,y)坐标定义。默认情况下,它的位置被设置为(0,0),这就是JFrame显示在屏幕左上角的原因。您可以使用setLocation(int x, int y)方法设置JFrame的(x,y)坐标。如果你想一步设置它的大小和位置,使用它的setBounds(int x, int y, int width, int height)方法。清单 1-2 在最简单的 Swing 程序中解决了这两个问题。
清单 1-2。修订的最简单 Swing 程序
// RevisedSimplestSwing.java
package com.jdojo.swing;
import javax.swing.JFrame;
public class RevisedSimplestSwing {
public static void main(String[] args) {
// Create a frame
JFrame frame = new JFrame("Revised Simplest Swing");
// Set the default close behavior to exit the application
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// Set the x, y, width and height properties in one go
frame.setBounds(50, 50, 200, 200);
// Display the frame
frame.setVisible(true);
}
}
Tip
您可以通过用一个null参数调用它的setLocationRelativeTo()方法来将一个JFrame放在中间。
JFrame 的组件
您在前面的部分显示了一个JFrame。它看起来是空的;然而,它并不是真的空的。当您创建一个JFrame时,以下事情会自动为您完成:
- 一个称为根窗格的容器被添加为
JFrame的唯一子元素。根窗格是一个容器。它是JRootPane类的一个对象。你可以通过使用JFrame类的getRootPane()方法来获得根窗格的引用。 - 名为“玻璃窗格”和“分层窗格”的两个容器被添加到根窗格中。默认情况下,玻璃窗格是隐藏的,它放置在分层窗格的顶部。顾名思义,玻璃窗格是透明的,即使你让它看得见,你也能看穿它。分层窗格之所以这样命名,是因为它可以在不同的层中容纳其他容器或组件。可选地,分层窗格可以包含菜单栏。但是,当您创建
JFrame时,默认情况下不会添加菜单栏。你可以通过使用JFrame类的getGlassPane()和getLayeredPane()方法分别得到玻璃窗格和分层窗格的引用。 - 一个称为内容窗格的容器被添加到分层窗格中。默认情况下,内容窗格是空的。这是您应该添加所有 Swing 组件的容器,比如按钮、文本字段、标签等。大多数时候,您将使用
JFrame的内容窗格。您可以通过使用JFrame类的getContentPane()方法来获取内容窗格的引用。
图 1-6 显示了 a JFrame的组装。根窗格、分层窗格和玻璃窗格覆盖了一个JFrame的整个可视区域。一个JFrame的可视区域是它的大小减去所有四个边上的插图。容器的 Insets 由容器四周的边框所使用的空间组成:顶部、左侧、底部和右侧。对于一个JFrame,顶部的插图代表标题栏的高度。图 1-6 描绘了比根窗格更小的分层窗格,以便更好地可视化。

图 1-6。
The making of a JFrame
你糊涂了吗?如果你对一个JFrame的所有窗格感到困惑,这里有一个更简单的解释。把一个JFrame想象成一个相框。一个相框有一个玻璃盖,一个玻璃格形式的JFrame也是如此。在玻璃罩后面,你放置你的照片。这就是你的分层窗格。您可以在一个相框中放置多张图片。每张图片将构成玻璃盖后面的一层。只要一张图片没有与另一张完全重叠,您就可以查看它的全部或部分内容。在不同图层中拍摄的所有图片构成了相框的分层窗格。离玻璃盖最远的图片图层是您的内容窗格。通常你的相框里只有一张图片。分层窗格也是如此;默认情况下,它包含一个内容窗格。画框里的画是感兴趣的内容,画放在那里。内容窗格也是如此;所有组件都放在内容窗格中。
下面列出了JFrame的包容层级。一个JFrame在层次的顶端,菜单栏(默认不添加;此处显示是为了完整性),内容窗格位于容器层次结构的底部。
JFrame
root pane
glass pane
layered pane
menu bar
content pane
如果您仍然不能理解JFrame的所有“难点”(阅读窗格),您可以稍后再看这一部分。现在,您只需要理解JFrame的一个窗格,那就是内容窗格,它包含了JFrame的 Swing 组件。您应该将所有想要添加到JFrame的组件添加到它的内容窗格中。您可以按如下方式获取内容窗格的引用:
// Create a JFrame
JFrame frame = new JFrame("Test");
// Get the reference of the content pane
Container contentPane = frame.getContentPane();
向 JFrame 添加组件
本节解释如何将组件添加到JFrame的内容窗格中。使用容器的add()方法(注意内容窗格也是一个容器)将组件添加到容器中。
// Add aComponent to aContainer
aContainer.add(aComponent);
add()方法被重载。除了要添加的组件之外,该方法的参数还取决于其他因素,例如您希望组件在容器中如何布局。下一节将讨论所有版本的add()方法。
我将把当前的讨论限制在为JFrame添加一个按钮上,这是一个 Swing 组件。JButton类的对象代表一个按钮。如果您使用过 Windows,您一定使用过按钮,如消息框上的“确定”按钮、Internet 浏览器窗口上的“后退”和“前进”按钮。通常,JButton包含文本,也称为标签。这就是你如何创建一个JButton:
// Create a JButton with Close text
JButton closeButton = new JButton("Close");
要将closeButton添加到JFrame的内容窗格中,您必须做两件事:
- 获取
JFrame的内容窗格的引用。Container contentPane = frame.getContentPane(); - 调用内容窗格的
add()方法。contentPane.add(closeButton);
这就是将组件添加到内容窗格的全部工作。如果您想使用一行代码添加一个JButton,您可以通过将所有三个语句合并成一个来实现,如下所示:
frame.getContentPane().add(new JButton("Close"));
向JFrame添加组件的代码如清单 1-3 所示。当你运行程序时,你得到一个如图 1-7 所示的JFrame。当你点击Close按钮时没有任何反应,因为你还没有给它添加任何动作。
清单 1-3。向 JFrame 添加组件
// AddingComponentToJFrame.java
package com.jdojo.swing;
import javax.swing.JFrame;
import javax.swing.JButton;
import java.awt.Container;
public class AddingComponentToJFrame {
public static void main(String[] args) {
JFrame frame = new JFrame("Adding Component to JFrame");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
Container contentPane = frame.getContentPane();
// Add a close button
JButton closeButton = new JButton("Close");
contentPane.add(closeButton);
// set the size of the frame 300 x 200
frame.setBounds(50, 50, 300, 200);
frame.setVisible(true);
}
}

图 1-7。
A JFrame with a JButton with Close as its text
代码完成了向JFrame添加带有Close文本的JButton的工作。然而,JButton看起来非常大,它占据了JFrame的整个可视区域。请注意,您已经使用setBounds()方法将JFrame的大小设置为 300 像素宽和 200 像素高。因为JButton填满了整个JFrame,你能把JFrame的尺寸设置的小一点吗?或者,你能为JButton本身设置尺寸吗?这两种建议在这种情况下都行不通。如果你想让JFrame变小,你需要猜测它需要变小多少。如果你想为JButton设置大小,它会惨败;JButton将始终填充JFrame的整个可视区域。这是怎么回事?要完全理解正在发生的事情,您需要阅读下一节关于布局管理器的内容。
Swing 为计算JFrame和JButton的大小提供了一个神奇而快速的解决方案。JFrame类的pack()方法就是那个神奇的解决方案。该方法检查您添加到JFrame中的所有组件,决定它们的首选大小,并将JFrame的大小设置为刚好足以显示所有组件。当你调用这个方法时,你不需要设置JFrame的大小。pack()方法将计算JFrame的大小并为您设置。要修复大小调整问题,请移除对setBounds()方法的调用,并添加对pack()方法的调用。注意,setBounds()方法也为JFrame设置了(x,y)坐标。如果还想将JFrame的(x,y)坐标设置为(50,50),可以使用它的setLocation(50, 50)方法。清单 1-4 包含修改后的代码,图 1-8 显示了结果JFrame。
清单 1-4。打包 JFrame 的所有组件
// PackedJFrame.java
package com.jdojo.swing;
import javax.swing.JFrame;
import java.awt.Container;
import javax.swing.JButton;
public class PackedJFrame {
public static void main(String[] args) {
JFrame frame = new JFrame("Adding Component to JFrame");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// Add a close button
JButton closeButton = new JButton("Close");
Container contentPane = frame.getContentPane();
contentPane.add(closeButton);
// Calculates and sets appropriate size for the frame
frame.pack();
frame.setVisible(true);
}
}

图 1-8。
Packed JFrame with a JButton
到目前为止,您已经成功地将一个JButton添加到一个JFrame中。让我们在同一个JFrame上再加一个JButton。把这个新按钮叫做helpButton。代码将类似于清单 1-4,除了这次您将添加两个JButton类的实例。清单 1-5 包含了完整的程序。图 1-9 显示了运行程序时的结果。
清单 1-5。向 JFrame 添加两个按钮
// JFrameWithTwoJButtons.java
package com.jdojo.swing;
import javax.swing.JFrame;
import java.awt.Container;
import javax.swing.JButton;
public class JFrameWithTwoJButtons {
public static void main(String[] args) {
JFrame frame = new JFrame("Adding Component to JFrame");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// Add two buttons - Close and Help
JButton closeButton = new JButton("Close");
JButton helpButton = new JButton("Help");
Container contentPane = frame.getContentPane();
contentPane.add(closeButton);
contentPane.add(helpButton);
frame.pack();
frame.setVisible(true);
}
}

图 1-9。
A JFrame with two buttons: Close and Help. Only the Help button is visible
当您添加了Help按钮时,您丢失了Close按钮。这是否意味着你只能给一个JFrame添加一个按钮?答案是否定的。你可以给一个JFrame添加任意多的按钮。那么,你的Close按钮在哪里?在我回答这个问题之前,您需要理解内容窗格的布局机制。
内容窗格是一个容器。你给它添加组件。但是,它将布局所有组件的任务交给了一个称为布局管理器的对象。布局管理器只是一个 Java 对象,它唯一的工作就是确定容器中组件的位置和大小。清单 1-5 中的例子是精心挑选的,目的是向您介绍布局管理器的概念。存在许多类型的布局管理器。它们在容器内放置组件和确定组件大小的方式不同。
默认情况下,JFrame的内容窗格使用一个名为BorderLayout的布局管理器。由于BorderLayout布局组件的方式,在前面的例子中只显示了Help按钮。事实上,当您添加两个按钮时,内容窗格会收到这两个按钮。为了确认这两个按钮仍然在内容窗格中,在清单 1-5 中的main()方法的末尾添加下面的代码片段,它显示了内容窗格拥有的组件的数量。它将在标准输出上打印一条消息:"Content Pane has 2 components."每个容器都有一个getComponents()方法,该方法返回添加到其中的组件数组。
// Get the components added to the content pane
Component[] comps = contentPane.getComponents();
// Display how many components the content pane has
System.out.println("Content Pane has " + comps.length + " components.");
有了这个背景,是时候学习各种版面管理器了。当我在后面的部分讨论BorderLayout管理器时,您将解决丢失Close按钮的难题。但是在讨论各种布局管理器之前,我将向您介绍一些在使用 Swing 应用时经常使用的实用程序类。
Tip
一个组件一次只能添加到一个容器中。如果将同一个组件添加到另一个容器中,该组件将从第一个容器中移除并添加到第二个容器中。
一些实用程序类
在开始开发一些严肃的 Swing GUIs 之前,有必要提一下一些经常使用的实用程序类。它们是简单的类。它们中的大多数都有一些可以在构造函数中指定的属性,并且有这些属性的 getters 和 setters。
点类
顾名思义,Point类的对象表示二维空间中的一个位置。二维空间中的位置由两个值表示:x 坐标和 y 坐标。Point级在java.awt包里。下面的代码片段演示了它的用法:
// Create an object of the Point class with (x, y) coordinate of (20, 40)
Point p = new Point(20, 40);
// Get the x and y coordinate of p
int x = p.getX();
int y = p.getY();
// Set the x and y coordinate of p to (10, 60)
p.setLocation(10, 60);
Swing 中的Point类的主要用途是设置和获取组件的位置(x 和 y 坐标)。例如,您可以设置一个JButton的位置。
JButton closeButton = new JButton("Close");
// The following two statements do the same thing.
// You will use one of the following statements and not both.
closeButton.setLocation(10, 15);
closeButton.setLocation(new Point(10, 15));
// Get the location of the closeButton
Point p = closeButton.getLocation();
维度类
一个Dimension类的对象包装了一个组件的width和height。部件的width和height统称为其尺寸。换句话说,Dimension类的一个对象被用来表示一个组件的大小。您可以使用Dimension类的对象包装任意两个整数。然而,在这一章中,它将被用在组件大小的上下文中。这个类在java.awt包里。
// Create an object of the Dimension class with a width and height of 200 and 20
Dimension d = new Dimension(200, 20);
// Set the size of closeButton to 200 X 20\. Both of the statements have the same efecct.
// You will use one of the following two statements.
closeButton.setSize(200, 20);
closeButton.setsize(d);
// Get the size of closeButton
Dimension d2 = closeButton.getSize();
int width = d2.width;
int height = d2.height;
Insets 类
Insets类的对象表示容器周围的空间。它包装了四个名为top、left、bottom和right的属性。它们的值表示容器四边的剩余空间。班级在java.awt包里。
// Create an object of the Insets class
// using its constructor Insets(top, left, bottom, right)
Insets ins = new Insets(20, 5, 5, 5);
// Get the insets of a JFrame
Insets ins = frame.getInsets();
int top = ins.top;
int left = ins.left;
int bottom = ins.bottom;
int right = ins.right;
矩形类
顾名思义,Rectangle类的一个实例代表一个矩形。在java.awt包里。你可以用许多方法定义一个矩形。一个Rectangle由三个属性定义:
- 左上角的(x,y)坐标
- 宽度
- 高度
你可以把一个Rectangle对象想象成一个Point对象和一个Dimension对象的组合;Point对象保存左上角的(x,y)坐标,Dimension对象保存宽度和高度。您可以通过指定属性的不同组合来创建一个Rectangle类的对象。
// Create a Rectangle object whose upper-left corner is at (0, 0)
// with width and height as zero
Rectangle r1 = new Rectangle();
// Create a Rectangle object from a Point object with its width and height as zero
Rectangle r2 = new Rectangle(new Point(10, 10));
// Create a Rectangle object from a Point object and a Dimension object
Rectangle r3 = new Rectangle(new Point(10, 10), new Dimension(200, 100));
// Create a Rectangle object by specifying its upper-left corner's
// coordinate at (10, 10) and width as 200 and height as 100
Rectangle r4 = new Rectangle(10, 10, 200, 100);
Rectangle类定义了许多方法来操作Rectangle对象并查询其属性,例如左上角的(x,y)坐标、宽度和高度。
Rectangle类的对象定义了 Swing 应用中组件的位置和大小。组件的位置和大小被称为它的边界。两个方法,setBounds()和getBounds(),可以用来设置和获取任何组件或容器的边界。setBounds()方法是重载的,您可以指定组件或Rectangle对象的 x、y、宽度和高度属性。getBounds()方法返回一个Rectangle对象。在清单 1-2 中,您使用了setBounds()方法来设置框架的 x、y、宽度和高度。请注意,组件的“边界”是其位置和大小的组合。setLocation()和setSize()方法的组合将实现与setBounds()方法相同的功能。同样,你也可以用getLocation() (or,getX()和getY())和getSize() (or,getWidth()和getHeight())的组合来代替使用getBounds()的方法。
布局经理
容器使用布局管理器来计算其所有组件的位置和大小。换句话说,布局管理器的工作是计算容器中所有组件的四个属性(x、y、宽度和高度)。x 和 y 属性确定组件在容器中的位置。宽度和高度属性决定组件的大小。您可能会问,“为什么需要布局管理器来执行计算组件的四个属性的简单任务?难道不能在程序中指定这四个属性,让容器用它们来显示组件吗?”答案是肯定的。您可以在程序中指定这些属性。如果这样做,当调整容器大小时,组件将不会重新定位和调整大小。此外,您必须为您的应用将在其上运行的所有平台指定组件的大小,因为不同的平台呈现的组件略有不同。假设您的应用以多种语言显示文本。一个JButton,比如一个Close按钮的最佳大小,在不同的语言中是不同的,你必须计算每种语言中Close按钮的大小,并根据应用使用的语言进行设置。但是,如果您使用布局管理器,则不必考虑所有这些因素。布局管理器将为您做这些简单但耗时的事情。
使用布局管理器是可选的。如果不使用布局管理器,则需要负责计算和设置容器中所有组件的位置和大小。
从技术上讲,布局管理器是一个实现了LayoutManager接口的 Java 类的对象。从LayoutManager接口继承了另一个名为LayoutManager2的接口。一些布局管理器类实现了LayoutManager2接口。两个接口都在java.awt包里。
布局管理器有很多。一些布局管理器很简单,易于手工编码。有些手工编码非常复杂,应该由 NetBeans 之类的 GUI 构建工具来使用。如果没有可用的布局管理器满足您的需求,您可以创建自己的布局管理器。一些有用的布局管理器可以在互联网上免费获得。有时你需要嵌套它们来获得想要的效果。我将在本节讨论以下布局管理器:
FlowLayoutBorderLayoutCardLayoutBoxLayoutGridLayoutGridBagLayoutGroupLayoutSpringLayout
每个容器都有一个默认的布局管理器。一个JFrame的内容窗格的默认布局管理器是BorderLayout,对于一个JPanel,它是FlowLayout。它是在创建容器时设置的。您可以通过使用容器的setLayout()方法来更改容器的默认布局管理器。如果不希望容器使用布局管理器,可以将null传递给setLayout()方法。您可以使用容器的getLayout()方法来获取容器当前使用的布局管理器的引用。
// Set FlowLayout as the layout manager for the content pane of a JFrame
JFrame frame = new JFrame("Test Frame");
Container contentPane = frame.getContentPane();
contentPane.setLayout(new FlowLayout());
// Set BorderLayout as the layout manager for a JPanel
JPanel panel = new JPanel();
panel.setLayout(new BorderLayout());
// Get the layout manager for a container
LayoutManager layoutManager = container.getLayout()
从 Java 5 开始,对JFrame上的add()和setLayout()方法的调用被转发到它的内容窗格。在 Java 5 之前,在JFrame上调用这些方法会抛出运行时异常。也就是说,在 Java 5 中,这两个调用frame.setLayout()和frame.add()将与调用frame.getContentPane().setLayout()和frame.getContentPane().add()做同样的事情。注意到JFrame的getLayout()方法返回的是JFrame的布局管理器,而不是它的内容窗格,这一点非常重要。为了避免从JFrame到其内容窗格的不对称调用转移(一些调用被转移,一些不被转移)的麻烦,最好直接调用内容窗格的方法,而不是在JFrame上调用它们。
flow layout-流程配置
FlowLayout是 Swing 中最简单的布局管理器。它先水平布局组件,然后垂直布局。它按照组件被添加到容器的顺序放置组件。当水平放置组件时,可以从左到右或从右到左放置。水平布局方向取决于容器的方向。您可以通过调用容器的setComponentOrientation()方法来设置容器的方向。如果你想设置一个容器及其所有子容器的方向,你可以使用applyComponentOrientation()方法。下面是设置容器方向的一段代码:
// Method – 1
// Set the orientation of the content pane of a frame to "right to left"
JFrame frame = new JFrame("Test");
Container pane = frame.getContentPane();
pane.setComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT);
// Method – 2
// Set the orientation of the content pane and all its children to "right to left"
JFrame frame = new JFrame("Test");
Container pane = frame.getContentPane();
pane.applyComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT);
如果您的应用是多语言的,并且组件方向将在运行时决定,您可能希望以一种更通用的方式来设置组件的区域设置和方向,而不是在您的程序中对其进行硬编码。您可以为应用中的所有 Swing 组件全局设置默认语言环境,如下所示:
// "ar" is used for Arabic locale
JComponent.setDefaultLocale(new Locale("ar"));
当您创建一个JFrame时,您可以根据默认的区域设置获取组件的方向,并将其设置为框架及其子框架。这样,您不必为应用中的每个容器设置方向。
// Get the default locale
Locale defaultLocale = JComponent.getDefaultLocale();
// Get the component's orientation for the default locale
ComponentOrientation componentOrientation = ComponentOrientation.getOrientation(defaultLocale);
// Apply the component's default orientation for the whole frame
frame.applyComponentOrientation(componentOrientation);
A FlowLayout试图将所有组件放入一行,给它们自己喜欢的大小。如果一行中容纳不下所有组件,它将开始另一行。每个布局管理器都必须计算它需要布置所有组件的空间的高度和宽度。A FlowLayout要求宽度,这是所有组件的首选宽度之和。它要求高度,即容器中最高组件的高度。它为宽度和高度增加了额外的空间,以考虑组件之间的水平和垂直间隙。清单 1-6 展示了如何为JFrame的内容窗格使用FlowLayout。它向内容窗格添加了三个按钮。图 1-10 显示了使用FlowLayout的三按钮屏幕。
清单 1-6。使用流程布局管理器
// FlowLayoutTest.java
package com.jdojo.swing;
import java.awt.Container;
import java.awt.FlowLayout;
import javax.swing.JButton;
import javax.swing.JFrame;
public class FlowLayoutTest {
public static void main(String[] args) {
JFrame frame = new JFrame("Flow Layout Test");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
Container contentPane = frame.getContentPane();
contentPane.setLayout(new FlowLayout());
for(int i = 1; i <= 3; i++) {
contentPane.add(new JButton("Button " + i));
}
frame.pack();
frame.setVisible(true);
}
}

图 1-10。
Three buttons in a JFrame with a FlowLayout Manager
当水平展开框架时,按钮显示如图 1-11 所示。

图 1-11。
After the JFrame using a FlowLatout has been expanded horizontally
默认情况下,FlowLayout将所有组件在容器中心对齐。您可以通过调用其setAlignment()方法或在其构造函数中传递对齐来更改对齐,如下所示:
// Set the alignment when you create the layout manager object
FlowLayout flowLayout = new FlowLayout(FlowLayout.RIGHT);
// Set the alignment after you have created the flow layout manager
flowLayout.setAlignment(FlowLayout.RIGHT);
在FlowLayout类中定义了以下五个常数来表示五种不同的对齐:LEFT、RIGHT、CENTER、LEADING和TRAILING。前三个常数的定义是显而易见的。LEADING对准可以是左对准也可以是右对准;这取决于组件的方向。如果组件的方向是RIGHT_TO_LEFT,则LEADING对准表示RIGHT。如果组件的方向是LEFT_TO_RIGHT,则LEADING对准表示LEFT。类似地,TRAILING对齐可能意味着向左或向右。如果组件的方向是RIGHT_TO_LEFT,则TRAILING对准表示LEFT。如果组件的方向是LEFT_TO_RIGHT,则TRAILING对准表示RIGHT。使用LEADING和TRAILING而不是RIGHT和LEFT总是一个好主意,所以你不必担心你的组件的方向。
你可以在FlowLayout类的构造函数中或者使用它的setHgap()和setVgap()方法来设置两个组件之间的间隙。清单 1-7 给出了向一个JFrame添加三个按钮的完整代码。内容窗格使用带有LEADING对齐的FlowLayout,并且JFrame's方向设置为RIGHT_TO_LEFT。运行程序时,JFrame将如图 1-12 所示。
清单 1-7。自定义流程布局
// FlowLayoutTest2.java
package com.jdojo.swing;
import java.awt.ComponentOrientation;
import java.awt.Container;
import java.awt.FlowLayout;
import javax.swing.JButton;
import javax.swing.JFrame;
public class FlowLayoutTest2 {
public static void main(String[] args) {
int horizontalGap = 20;
int verticalGap = 10;
JFrame frame = new JFrame("Flow Layout Test");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
Container contentPane = frame.getContentPane();
FlowLayout flowLayout =
new FlowLayout(FlowLayout.LEADING, horizontalGap, verticalGap);
contentPane.setLayout(flowLayout);
frame.applyComponentOrientation(
ComponentOrientation.RIGHT_TO_LEFT);
for(int i = 1; i <= 3; i++) {
contentPane.add(new JButton("Button " + i));
}
frame.pack();
frame.setVisible(true);
}
}

图 1-12。
A JFrame having three buttons and a customized FlowLayout
你必须记住,一个FlowLayout试图将所有的组件仅排列在一行中。因此,它不要求适合所有组件的高度。相反,它要求容器中最高组件的高度。为了演示这一微妙之处,尝试在JFrame中添加 30 个按钮,这样它们就不在一行中。以下代码片段演示了这一点:
JFrame frame = new JFrame("Welcome to Swing");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.getContentPane().setLayout(new FlowLayout());
for(int i = 1; i <= 30; i++) {
frame.getContentPane().add(new JButton("Button " + i));
}
frame.pack();
frame.setVisible(true);
JFrame如图 1-13 所示。您可以看到 30 个按钮并没有全部显示出来。如果您调整JFrame的大小,使其高度变大,您将能够看到所有按钮,如图 1-14 所示。FlowLayout隐藏了不能在一行中显示的组件。

图 1-14。
A JFrame with 30 buttons after it is resized

图 1-13。
A JFrame with 30 buttons. Not all buttons are displayed
FlowLayout的特性有一个非常重要的含义,它试图在一行中布局所有组件。它要求高度刚好能够显示最高的组件。如果您将一个带有FlowLayout管理器的容器嵌套在另一个也使用FlowLayout管理器的容器中,您将永远不会在嵌套的容器中看到多于一行。为了演示这一点,向一个JPanel添加 30 个JButton实例。一个JPanel是一个空容器,默认布局管理器是一个FlowLayout。将JFrame的内容窗格的布局管理器设置为FlowLayout,并将JPanel添加到JFrame的内容窗格。通过这种方式,您可以将带有FlowLayout的容器JPanel嵌套在另一个带有FlowLayout的容器(内容窗格)中。清单 1-8 包含了演示这一点的完整程序。当运行程序时,产生的JFrame如图 1-15 所示。即使您调整JFrame的大小使其高度变大,您也只能看到一行按钮。
清单 1-8。嵌套流程布局管理器
// FlowLayoutNesting.java
package com.jdojo.swing;
import java.awt.FlowLayout;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
public class FlowLayoutNesting {
public static void main(String[] args) {
JFrame frame = new JFrame("FlowLayout Nesting");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// Set the content pane's layout to FlowLayout
frame.getContentPane().setLayout(new FlowLayout());
// JPanel is an empty container with a FlowLayout manager
JPanel panel = new JPanel();
// Add thirty JButtons to the JPanel
for(int i = 1; i <= 30; i++) {
panel.add(new JButton("Button " + i));
}
// Add JPanel to the content pane
frame.getContentPane().add(panel);
frame.pack();
frame.setVisible(true);
}
}

图 1-15。
A nested FlowLayout always display only one row
我想在结束关于FlowLayout的讨论时指出,由于本节讨论的限制,它在现实应用中的使用非常有限。它通常用于原型制作。
边界布局
将集装箱的空间分为五个区域:北、南、东、西、中。当您向带有BorderLayout的容器添加组件时,您需要指定您想要将组件添加到五个区域中的哪一个。BorderLayout类定义了五个常量来标识这五个区域中的每一个。常量有NORTH、SOUTH、EAST、WEST和CENTER。例如,要在北部区域添加一个按钮,您可以编写
// Add a button to the north area of the container
JButton northButton = new JButton("North");
container.add(northButton, BorderLayout.NORTH);
一个JFrame的内容窗格的默认布局是一个BorderLayout。清单 1-9 包含了向JFrame的内容窗格添加五个按钮的完整程序。得到的JFrame如图 1-16 所示。
清单 1-9。向 BorderLayout 添加组件
// BorderLayoutTest.java
package com.jdojo.swing;
import java.awt.BorderLayout;
import javax.swing.JFrame;
import java.awt.Container;
import javax.swing.JButton;
public class BorderLayoutTest {
public static void main(String[] args) {
JFrame frame = new JFrame("BorderLayout Test");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
Container container = frame.getContentPane();
// Add a button to each of the five areas of the BorderLayout
container.add(new JButton("North"), BorderLayout.NORTH);
container.add(new JButton("South"), BorderLayout.SOUTH);
container.add(new JButton("East"), BorderLayout.EAST);
container.add(new JButton("West"), BorderLayout.WEST);
container.add(new JButton("Center"), BorderLayout.CENTER);
frame.pack();
frame.setVisible(true);
}
}

图 1-16。
Five areas of the BorderLayout
您最多可以将一个组件添加到BorderLayout的一个区域。你可以留下一些空白区域。如果您想在一个BorderLayout的区域中添加多个组件,您可以通过将这些组件添加到一个容器中,然后将该容器添加到所需的区域中。
一个BorderLayout (中的五个区域(北、南、东、西、中)的方向是固定的,不依赖于组件的方向。还有四个常量用于指定BorderLayout中的区域。这些常量是PAGE_START、PAGE_END、LINE_START和LINE_END。PAGE_START和PAGE_END常量分别与NORTH和SOUTH常量相同。LINE_START和LINE_END常量根据容器的方向改变它们的位置。如果容器的方向是从左向右,LINE_START与WEST相同,LINE_END与EAST相同。如果容器的方向是从右向左,LINE_START与EAST相同,LINE_END与WEST相同。图 1-17 和图 1-18 描绘了BorderLayout不同组件方向区域的定位差异。

图 1-18。
A BorderLayout’s areas when the container’s orientation is right to left

图 1-17。
A BorderLayout’s areas when the container’s orientation is left to right
如果不指定组件的区域,它将被添加到中心。以下两个语句具有相同的效果:
// Assume that the container has a BorderLayout
// Add a button to the container without specifying the area
container.add(new JButton("Close"));
// The above statement is the same as the following
container.add(new JButton("Close"), BorderLayout.CENTER);
我已经说过,你最多可以给一个BorderLayout添加五个组件,五个区域各一个。如果在一个BorderLayout的相同区域添加多个组件会发生什么?也就是说,如果您编写以下代码,会发生什么情况?
// Assume that container has a BorderLayout
container.add(new JButton("Close"), BorderLayout.NORTH);
container.add(new JButton("Help"), BorderLayout.NORTH);
你会发现BorderLayout的北部区域只显示了一个按钮:最后添加的那个按钮。也就是说,北部区域只会显示Help按钮。这就是清单 1-5 中发生的事情。您在JFrame的内容窗格中添加了两个按钮Close和Help。由于您没有指定要添加它们的BorderLayout的区域,所以它们都被添加到中心区域。由于在BorderLayout的每个区域只能有一个组件,所以Help按钮取代了Close按钮。这就是当您运行清单 1-5 中的程序时没有看到Close按钮的原因。若要解决此问题,请在将两个按钮添加到容器时指定它们的区域。
Tip
如果您在一个BorderLayout托管容器中缺少一些组件,请确保您没有在同一区域添加多个组件。如果将组件添加到混合面积常数的BorderLayout中,PAGE_START、PAGE_END、LINE_START和LINE_END常数优先于NORTH、SOUTH、EAST和WEST常数。也就是说,如果使用add(c1, NORTH)和add(c2, PAGE_START)将两个组件添加到一个BorderLayout中,将使用c2,而不是c1。
a BorderLayout如何计算组件的大小?它根据组件放置的区域计算组件的大小。它考虑了组件在南北方向的首选高度。但是,它会根据南北方向的可用空间水平拉伸组件的宽度。也就是说,它不考虑南北向组件的首选宽度。它考虑了东西两侧组件的首选宽度,并赋予它们垂直填充整个空间所需的高度。中心区域的组件水平和垂直拉伸,以适应可用空间。也就是说,中心区域不考虑其组件的首选宽度和高度。
菜单布局
CardLayout将容器中的组件排列成一叠卡片。就像一叠卡片一样,在一个CardLayout中只有一张卡片(最上面的卡片)是可见的。它一次只能显示一个组件。你需要使用以下步骤来为一个容器使用一个CardLayout:
- 创建一个容器,比如一个
JPanel。JPanel cardPanel = new JPanel(); - 创建一个
CardLayout对象。CardLayout cardLayout = new CardLayout(); - 为容器设置布局管理器。
cardPanel.setLayout(cardLayout); - 向容器中添加组件。您需要为每个组件命名。要给
cardPanel添加一个JButton,使用下面的语句:cardPanel.add(new JButton("Card 1"), "myLuckyCard");你已经将你的卡命名为myLuckyCard。这个名字可以在CardLayout的show()方法中使用,使这张卡可见。 - 调用它的
next()方法来显示下一张卡片。cardLayout.next(cardPanel);
CardLayout类提供了几种翻转组件的方法。默认情况下,它显示添加到其中的第一个组件。所有翻转相关的方法都将它管理的容器作为其参数。first()和last()方法分别显示第一张和最后一张卡片。previous()和next()方法显示当前显示的卡片中的上一张和下一张卡片。如果显示最后一张牌,调用next()方法显示第一张牌。如果显示第一张牌,调用previous()方法显示最后一张牌。
清单 1-10 演示了如何使用一个CardLayout。图 1-19 显示了结果JFrame。当你点击Next按钮时,下一张牌被翻转。程序将两个 JPanels 添加到JFrame的内容窗格中。一个JPanel、buttonPanel有Next按钮,它被添加到内容窗格的南部区域。注意,默认情况下,JPanel使用FlowLayout。
清单 1-10。CardLayout 的实际应用
// CardLayoutTest.java
package com.jdojo.swing;
import java.awt.Container;
import javax.swing.JFrame;
import java.awt.CardLayout;
import javax.swing.JPanel;
import javax.swing.JButton;
import java.awt.Dimension;
import java.awt.BorderLayout;
public class CardLayoutTest {
public static void main(String[] args) {
JFrame frame = new JFrame("CardLayout Test");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
Container contentPane = frame.getContentPane();
// Add a Next JButton in a JPanel to the content pane
JPanel buttonPanel = new JPanel();
JButton nextButton = new JButton("Next");
buttonPanel.add(nextButton);
contentPane.add(buttonPanel, BorderLayout.SOUTH);
// Create a JPanel and set its layout to CardLayout
final JPanel cardPanel = new JPanel();
final CardLayout cardLayout = new CardLayout();
cardPanel.setLayout(cardLayout);
// Add five JButtons as cards to the cardPanel
for(int i = 1; i <= 5; i++) {
JButton card = new JButton("Card " + i);
card.setPreferredSize(new Dimension(200, 200));
String cardName = "card" + 1;
cardPanel.add(card, cardName);
}
// Add the cardPanel to the content pane
contentPane.add(cardPanel, BorderLayout.CENTER);
// Add an action listener to the Next button
nextButton.addActionListener(e -> cardLayout.next(cardPanel));
frame.pack();
frame.setVisible(true);
}
}

图 1-19。
A CardLayout in action. Click the Next JButton to flip through the cards
程序向Next按钮添加一个动作监听器。我还没有讨论如何给按钮添加一个动作监听器。有必要看看CardLayout的行动。我将在事件处理部分详细讨论如何向按钮添加动作。现在,只需要提到您需要调用JButton类的addActionListener()方法来添加一个动作监听器就足够了。这个方法接受一个类型为ActionListener接口的对象,并有一个名为actionPerformed()的方法。当你点击JButton时,执行actionPerformed()方法中的代码。翻转下一张牌的代码是对cardLayout.next(cardPanel)方法的调用。ActionListener接口是一个函数接口,您可以使用 lambda 表达式来创建它的实例,如下所示:
// Add an action listener to the Next JButton to flip the next card
nextButton.addActionListener(e -> cardLayout.next(cardPanel));
Tip
因为除了一个组件之外,其他组件对用户来说都是隐藏的,所以不经常使用。更容易使用的JTabbedPane提供了类似于CardLayout的功能。我将在第二章的中讨论JTabbedPane。一个JTabbedPane是一个容器,不是一个布局管理器。它将所有组件以选项卡的形式布局,并允许用户在这些选项卡之间切换。
box layout-方块配置
BoxLayout将组件水平排列成一行或垂直排列成一列。您需要使用以下步骤在您的程序中使用一个BoxLayout:
- 创建一个容器,例如一个
JPanel。JPanel hPanel = new JPanel(); - 创建一个
BoxLayout类的对象。与其他布局管理器不同,您需要将容器传递给类的构造函数。您还需要将正在创建的盒子的类型(水平或垂直)传递给它的构造函数。该类有四个常量:X_AXIS、Y_AXIS、LINE_AXIS和PAGE_AXIS。常量X_AXIS用于创建一个水平BoxLayout,从左到右排列所有组件。常量Y_AXIS用于创建一个从上到下布局所有组件的垂直BoxLayout。另外两个常量LINE_AXIS和PAGE_AXIS与X_AXIS和Y_AXIS类似。但是,他们在布局组件时使用容器的方向。// Create a BoxLayout for hPanel to lay out// components from left to rightBoxLayout boxLayout = new BoxLayout(hPanel, BoxLayout.X_AXIS); - 设置容器的布局。
hPanel.setLayout(boxLayout); - 将组件添加到容器中。
hPanel.add(new JButton("Button 1"));hPanel.add(new JButton("Button 2"));
清单 1-11 使用一个水平BoxLayout来显示三个按钮,如图 1-20 所示。
清单 1-11。使用水平方框布局
// BoxLayoutTest.java
package com.jdojo.swing;
import java.awt.Container;
import javax.swing.JFrame;
import javax.swing.JButton;
import javax.swing.JPanel;
import javax.swing.BoxLayout;
import java.awt.BorderLayout;
public class BoxLayoutTest {
public static void main(String[] args) {
JFrame frame = new JFrame("BoxLayout Test");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
Container contentPane = frame.getContentPane();
JPanel hPanel = new JPanel();
BoxLayout boxLayout = new BoxLayout(hPanel, BoxLayout.X_AXIS);
hPanel.setLayout(boxLayout);
for(int i = 1; i <= 3; i++) {
hPanel.add(new JButton("Button " + i));
}
contentPane.add(hPanel, BorderLayout.SOUTH);
frame.pack();
frame.setVisible(true);
}
}

图 1-20。
A JFrame with a horizontal BoxLayout with three buttons
A BoxLayout试图给水平布局中的所有组件提供优选的宽度,给垂直布局中的所有组件提供优选的高度。在水平布局中,最高组件的高度给定给所有其他组件。如果它无法调整组件的高度以匹配组中最高的组件,它会将组件沿中心水平对齐。您可以通过使用setAlignmentY()方法设置组件对齐或容器对齐来更改这个默认对齐。在垂直布局中,它试图为所有组件提供首选高度,并试图使所有组件的大小与最宽组件的宽度相同。如果它不能使所有组件具有相同的宽度,它会沿着它们的中心线垂直对齐它们。您可以通过使用setAlignmentX()方法改变组件的对齐或者容器的对齐来改变这个默认的对齐。
javax.swing包包含一个Box类,使得使用BoxLayout更加容易。一个Box容器使用一个BoxLayout作为它的布局管理器。Box类提供了static方法来创建水平或垂直布局的容器。方法createHorizontalBox()和createVerticalBox()分别创建一个水平和垂直的盒子。
// Create a horizontal box
Box hBox = Box.createHorizontalBox();
// Create a vertical box
Box vBox = Box.createVerticalBox();
要向Box添加组件,使用它的add()方法,如下所示:
// Add two buttons to the horizontal box
hBox.add(new JButton("Button 1");
hBox.add(new JButton("Button 2");
Box类还允许你创建不可见的组件并将它们添加到一个盒子中,这样你就可以调整两个组件之间的间距。它提供了四种类型的不可见组件:
- 胶
- 支柱
- 刚性区域
- 补白
胶水是一种不可见的、可扩展的成分。你可以使用Box类的createHorizontalGlue()和createVerticalGlue()静态方法创建水平和垂直粘合。以下代码片段在水平框布局中的两个按钮之间使用水平粘附。您还可以使用Box类的createGlue()静态方法创建一个 glue 组件,该组件可以水平和垂直扩展。
Box hBox = Box.createHorizontalBox();
hBox.add(new JButton("First"));
hBox.add(Box.createHorizontalGlue());
hBox.add(new JButton("Last"));
中间有胶水的按钮如图 1-21 所示。图 1-22 显示了容器水平展开后的情况。请注意两个按钮之间的水平空白区域,这是已经扩展的隐形胶水。

图 1-22。
A horizontal box with two buttons and a horizontal glue between them after resizing

图 1-21。
A horizontal box with two buttons and a horizontal glue between them
支柱是固定宽度或固定高度的不可见组件。您可以使用以像素为单位的宽度作为参数的createHorizontalStrut()方法创建一个水平支柱。您可以使用以像素为单位的高度作为参数的createVerticalStrut()方法创建一个垂直支柱。
// Add a 100px strut to a horizontal box
hBox.add(Box.createHorizontalStrut(100));
刚性区域是一种不可见的组件,其大小始终相同。您可以通过使用Box类的createRigidArea() static方法来创建一个刚性区域。你需要给它传递一个Dimension对象来指定它的宽度和高度。
// Add a 10x5 rigid area to a horizontal box
hBox.add(Box.createRigidArea(new Dimesnion(10, 5)));
填充器是一种不可见的自定义组件,可以通过指定自己的最小、最大和首选尺寸来创建。Box类的Filler静态嵌套类表示填充符。
// Create a filler, which acts like a glue. Note that the glue is
// just a filler with a minimum and preferred size set to zero and
// a maximum size set to Short.MAX_VALUE in both directions
Dimension minSize = new Dimension(0, 0);
Dimension prefSize = new Dimension(0, 0);
Dimension maxSize = new Dimension(Short.MAX_VALUE, Short.MAX_VALUE);
Box.Filler filler = new Box.Filler(minSize, prefSize, maxSize);
用一个水平和垂直的BoxLayout嵌套盒子可以得到一个非常强大的布局。Box类提供了创建粘合、支撑和刚性区域的便利方法。然而,它们都是Box.Filler类的对象。当最小和首选尺寸设置为零,最大尺寸设置为两个方向的Short.MAX_VALUE时,一个Box.Filler对象充当胶水。当粘附的最大高度设置为零时,它的行为类似于水平粘附。当粘附的最大宽度设置为零时,它的作用类似于垂直粘附。通过使用指定宽度和零高度的最小和首选尺寸,以及指定宽度和Short.MAX_VALUE高度的最大尺寸,您可以使用Box.Filler类创建水平支柱。你能想到用Box.Filler类创建一个刚性区域的方法吗?对于刚性区域,所有尺寸(最小、首选和最大)都是相同的。以下代码片段创建了一个 10x10 的刚性区域:
// Create a 10x10 rigid area
Dimension d = new Dimension(10, 10);
JComponent rigidArea = new Box.Filler(d, d, d);
清单 1-12 演示了如何使用Box类和 glue。图 1-23 显示了水平展开后的结果JFrame。当框架打开时,Previous和Next按钮之间没有间隙。
清单 1-12。使用 Box 类和 Glue 的 BoxLayout
// BoxLayoutGlueTest.java
package com.jdojo.swing;
import java.awt.Container;
import javax.swing.JFrame;
import javax.swing.JButton;
import javax.swing.Box;
import java.awt.BorderLayout;
public class BoxLayoutGlueTest {
public static void main(String[] args) {
JFrame frame = new JFrame("BoxLayout with Glue");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
Container contentPane = frame.getContentPane();
Box hBox = Box.createHorizontalBox();
hBox.add(new JButton("<<First"));
hBox.add(new JButton("<Previous"));
hBox.add(Box.createHorizontalGlue());
hBox.add(new JButton("Next>"));
hBox.add(new JButton("Last>>"));
contentPane.add(hBox, BorderLayout.SOUTH);
frame.pack();
frame.setVisible(true);
}
}

图 1-23。
A BoxLayout with glue
网格布局
一个GridLayout将组件排列在一个大小相等的矩形网格中。每个组件恰好被放置在一个单元中。它不考虑组件的首选大小。它将可用空间划分为大小相等的单元格,并根据单元格的大小调整每个组件的大小。
您可以指定网格中的行数或列数。如果两者都指定,则只使用行数,并计算列数。假设ncomponents是添加到容器中的组件数量,nrows和ncols是指定的行数和列数。如果nrows大于零,则使用以下公式计算网格中的列数:
ncols = (ncomponents + nrows - 1)/nrows
如果nrows为零,则使用以下公式计算网格中的行数:
nrows = (ncomponents + ncols - 1)/ncols
不能为nrows或ncols指定负数,并且它们中至少有一个必须大于零。否则,将引发运行时异常。
您可以使用GridLayout类的以下三个构造函数之一创建一个GridLayout:
GridLayout()GridLayout(int rows, int cols)GridLayout(int rows, int cols, int hgap, int vgap)
您可以指定行数、列数、水平间距以及网格中两个单元格之间的垂直间距。您也可以使用setRows()、setColumns()、setHgap()和setVgap()方法来设置这些属性。
无参数构造函数创建一行网格。列数与添加到容器中的组件数相同。
// Create a grid layout of one row
GridLayout gridLayout = new GridLayout();
第二个构造函数根据指定的行数或列数创建一个GridLayout。
// Create a grid layout of 5 rows. Specify 0 as the number of columns.
// The number of columns will be computed.
GridLayout gridLayout = new GridLayout(5, 0);
// Create a grid layout of 3 columns. Specify 0 as the number of rows.
// The number of rows will be computed.
GridLayout gridLayout = new GridLayout(0, 3);
// Create a grid layout with 2 rows and 3 columns. You have specified
// a non-zero value for rows, so the value for columns will be ignored.
// It will be computed based on the number of components.
GridLayout gridLayout = new GridLayout(2, 3);
第三个构造函数允许您指定行数或列数,以及两个单元格之间的水平和垂直间距。您可以创建一个三行的GridLayout,单元格之间的水平间距为 10 像素,垂直间距为 20 像素,如下所示:
GridLayout gridLayout = new GridLayout(3, 0, 10, 20);
清单 1-13 演示了如何使用一个GridLayout。请注意,您不需要指定组件将放置在哪个单元中。您只需将组件添加到容器中,布局管理器就会决定组件的位置。
清单 1-13。使用网格布局
// GridLayoutTest.java
package com.jdojo.swing;
import java.awt.GridLayout;
import javax.swing.JPanel;
import java.awt.BorderLayout;
import javax.swing.JFrame;
import java.awt.Container;
import javax.swing.JButton;
public class GridLayoutTest {
public static void main(String[] args) {
JFrame frame = new JFrame("GridLayout Test");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
Container contentPane = frame.getContentPane();
JPanel buttonPanel = new JPanel();
buttonPanel.setLayout(new GridLayout(3,0));
for(int i = 1; i <= 9 ; i++) {
buttonPanel.add(new JButton("Button " + i));
}
contentPane.add(buttonPanel, BorderLayout.CENTER);
frame.pack();
frame.setVisible(true);
}
}
图 1-24 显示了一个带有GridLayout的容器,有三排九个组件。图 1-25 显示了一个带有GridLayout的容器,该容器有三排七个组件。如果你用一个GridLayout调整容器的大小,所有的组件都将被调整大小,它们将是相同的大小。尝试通过运行清单 1-13 中的程序来调整JFrame的大小。

图 1-25。
A GridLayout with three rows and seven components

图 1-24。
A GridLayout with three rows and nine components
一个GridLayout是一个简单的手工编码的布局管理器。然而,它并不十分强大,原因有二。首先,它强制每个组件具有相同的大小,其次,您不能指定网格中组件的行号和列号(或确切位置)。也就是说,您只能向GridLayout添加一个组件。它们将按照您将它们添加到容器中的顺序水平排列,然后垂直排列。如果容器的方向是LEFT_TO_RIGHT,组件从左到右,然后从上到下排列。如果容器的方向是RIGHT_TO_LEFT,组件从右到左,然后从上到下排列。使用GridLayout的一个好方法是创建一组相同大小的按钮。例如,假设您将两个带有文本OK和Cancel的按钮添加到一个容器中,并希望它们具有相同的大小。您可以通过将按钮添加到由GridLayout布局管理器管理的容器中来实现这一点。
网格包布局
与GridLayout类似,GridBagLayout将组件布置在按行和列排列的矩形单元网格中。然而,它比GridLayout强大得多。它的强大带来了使用上的复杂性。不如GridLayout好用。在GridBagLayout中你可以定制的东西太多了,以至于很难快速学习和使用它的所有功能。
它可以让你定制组件的许多属性,如大小、对齐、可扩展性等。与GridLayout不同,网格中的所有单元不必大小相同。一个元件不需要精确地放置在一个单元中。一个组件可以水平和垂直跨越多个单元格。您可以指定其单元格内的组件应该如何对齐。
在使用GridBagLayout布局管理器时使用GridBagLayout和GridBagConstraints类。两个类都在java.awt包里。一个GridBagLayout类的对象定义了一个GridBagLayout布局管理器。GridBagConstraints类的对象为GridBagLayout中的组件定义约束。组件的约束用于布局组件。一些约束包括组件在网格中的位置、宽度、高度、单元格内的对齐方式等。
下面的代码片段创建了一个GridBagLayout类的对象,并将其设置为JPanel的布局管理器:
// Create a JPanel container
JPanel panel = new JPanel();
// Set GridBagLayout as the layout manager for the JPanel
GridBagLayout gridBagLayout = new GridBagLayout();
panel.setLayout(gridBagLayout);
让我们以最简单的形式使用GridBagLayout:创建一个框架,将其内容窗格的布局设置为GridBagLayout,并向内容窗格添加九个按钮。这是在清单 1-14 中完成的。图 1-26 显示运行程序时的画面。
清单 1-14。以最简单的形式使用的 GridBagLayout
// SimplestGridBagLayout.java
package com.jdojo.swing;
import javax.swing.JFrame;
import java.awt.Container;
import javax.swing.JButton;
import java.awt.GridBagLayout;
public class SimplestGridBagLayout {
public static void main(String[] args) {
String title = "GridBagLayout in its Simplest Form";
JFrame frame = new JFrame(title);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
Container contentPane = frame.getContentPane();
contentPane.setLayout(new GridBagLayout());
for(int i = 1; i <= 9; i++) {
contentPane.add(new JButton("Button " + i));
}
frame.pack();
frame.setVisible(true);
}
}

图 1-26。
Nine buttons in a GridBagLayout
起初,似乎 a GridBagLayout的行为像是 a FlowLayout。效果就像你用了一个FlowLayout一样。然而,GridBagLayout并不等同于FlowLayout,尽管它有能力像FlowLayout一样工作。它更强大(也更容易出错!)比一个FlowLayout。当您添加九个按钮时,您没有指定它们的单元格。您使用了contentPane.add(Component c)方法来添加按钮。结果是它在一行中放置了一个又一个按钮。
您可以指定GridBagLayout中组件应放置的单元格。要指定组件的单元格,需要调用add(Component c, Object constraints)方法,其中第二个参数是GridBagConstraints类的一个对象。如果您没有为一个GridBagLayout中的组件指定约束对象,它会将该组件放置在下一个单元中。下一个单元是用于放置前一个组件的单元之后的单元。如果没有对GridBagLayout中的任何组件使用约束,所有组件都被放置在一行中,如图 1-26 所示。当我讲述一个GridBagConstraints对象的gridx和gridy属性时,我会对此进行更多的讨论。
让我们通过展示它实际上是一个网格布局,并且它将组件放置在一个单元格网格中,来澄清一下GridBagLayout的记录。为了证明这一点,您将在前一个示例中在一个三行三列的单元格网格中显示九个按钮。这一次,只有一点不同:您将为按钮指定单元格在网格中的位置。行号和列号的组合表示单元格在网格中的位置。组件及其单元格的所有属性都是使用一个GridBagConstraints类的对象指定的。它有许多公共实例变量。它的gridx和gridy实例变量分别指定单元格的列号和行号。第一列用gridx = 0表示,第二列用gridx = 1表示,依此类推。第一行用gridy = 0表示,第二行用gridy = 1表示,依此类推。
哪个是网格中的第一个单元格—左上角、右上角、左下角还是右下角?这取决于容器的方向。如果容器使用LEFT_TO_RIGHT方向,网格左上角的单元格是第一个单元格。如果容器使用RIGHT_TO_LEFT方向,网格右上角的单元格是第一个单元格。表 1-1 和表 1-2 显示了具有不同容器方向的GridBagLayout中的单元格及其相应的gridx和gridy值。这些表格只显示了九个单元格。A GridBagLayout不限于只有九个单元。你想要多少细胞就有多少细胞。确切地说,您可以拥有最多Integer.MAX_VALUE个行和列,这肯定不会在任何应用中使用。
表 1-2。
Values of gridx and gridy for Cells in a Container with RIGHT_TO_LEFT Orientation
| `gridx=2, gridy=0` | `gridx=1, gridy=0` | `gridx=0, gridy=0` | | `gridx=2, gridy=1` | `gridx=1, gridy=1` | `gridx=0, gridy=1` | | `gridx=2, gridy=2` | `gridx=1, gridy=2` | `gridx=0, gridy=2` |表 1-1。
Values of gridx and gridy for Cells in a Container With LEFT_TO_RIGHT Orientation
| `gridx=0, gridy=0` | `gridx=1, gridy=0` | `gridx=2, gridy=0` | | `gridx=0, gridy=1` | `gridx=1, gridy=1` | `gridx=2, gridy=1` | | `gridx=0, gridy=2` | `gridx=1, gridy=2` | `gridx=2, gridy=2` |设置组件的gridx和gridy属性很容易。您为您的组件创建一个约束对象,这是一个GridBagConstraints类的对象;设置其gridx和gridy属性;并将约束对象传递给add()方法。下面的代码片段显示了如何在JButton的约束中设置gridx和gridy属性。当您调用container.add(component, constraint)方法时,约束对象被复制到正在添加的组件中,这样您就可以更改它的一些属性,并将其重新用于另一个组件。这样,您不必为添加到GridBagLayout的每个组件创建一个新的约束对象。然而,这种方法容易出错。您可能为某个组件设置了约束,但在为另一个组件重用约束对象时忘记了更改该约束。因此,在重用约束对象时要小心。
// Create a constraint object
GridBagConstraints gbc = new GridBagConstraints();
// Set gridx and gridy properties in the constraint object
gbc.gridx = 0;
gbc.gridy = 0;
// Add a JButton and pass the constraint object as the
// second argument to the add() method.
container.add(new JButton("B1"), gbc);
// Set the gridx property to 1\. The gridy property
// remains as 0 as set previously.
gbc.gridx = 1;
// Add another JButton to the container
container.add(new JButton("B2"), gbc);
清单 1-15 演示了如何为一个组件设置gridx和gridy值(或单元号)。图 1-27 显示了运行程序时得到的JFrame。
清单 1-15。为 GridBagLayout 中的组件设置 gridx 和 gridy 属性
// GridBagLayoutWithgridxAndgridy.java
package com.jdojo.swing;
import java.awt.GridBagLayout;
import java.awt.Container;
import javax.swing.JFrame;
import javax.swing.JButton;
import java.awt.GridBagConstraints;
public class GridBagLayoutWithgridxAndgridy {
public static void main(String[] args) {
String title = "GridBagLayout with gridx and gridy";
JFrame frame = new JFrame(title);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
Container contentPane = frame.getContentPane();
contentPane.setLayout(new GridBagLayout());
// Create an object for GridBagConstraints to set
// the constraints for each JButton
GridBagConstraints gbc = new GridBagConstraints();
for(int y = 0; y < 3; y++) {
for(int x = 0; x < 3; x++) {
gbc.gridx = x;
gbc.gridy = y;
String text = "Button (" + x + ", " + y + ")";
contentPane.add(new JButton(text), gbc);
}
}
frame.pack();
frame.setVisible(true);
}
}

图 1-27。
A GridBagLayout with nine buttons
您可以使用GridBagConstraints对象为组件指定其他约束。使用表 1-3 中列出的一个实例变量设置GridBagConstraints对象中的所有约束。该类还定义了许多常量,如RELATIVE、REMAINDER等。请注意,所有实例变量都是小写的。
表 1-3。
Instance Variables of the GridBagConstraints Class
| 实例变量 | 缺省值 | 可能的值 | 使用 | | --- | --- | --- | --- | | `gridx` `gridy` | `RELATIVE` | `RELATIVE` `An integer` | 组件所在网格中单元格的列号和行号。 | | `gridwidth` `gridheight` | `1` | `An integer``RELATIVE` | 用于显示组件的网格单元数。 | | `fill` | `NONE` | `BOTH``HORIZONTAL``VERTICAL` | 指定组件将如何填充网格中分配给它的单元格。 | | `ipadx` `ipady` | `0` | 整数 | 指定添加到其最小大小的组件的内部填充。允许负整数,这将减小组件的最小大小。 | | `insets` | `(0,0,0,0)` | Insets 对象 | 指定组件边缘与其在网格中的单元格之间的外部填充。允许负值。 | | `anchor` | `CENTER` | `CENTER`,`NORTH`,`NORTHEAST`,`EAST`,`SOUTHEAST`,`SOUTH`,`SOUTHWEST`,`WEST`,`NORTHWEST, PAGE_START`,`PAGE_END`,`LINE_START`,`LINE_END`,`FIRST_LINE_START`,`FIRST_LINE_END`,`LAST_LINE_START`,`LAST_LINE_END, BASELINE`,`BASELINE_LEADING`,`BASELINE_TRAILING`,`ABOVE_BASELINE`,`ABOVE_BASELINE_LEADING`,`ABOVE_BASELINE_TRAILING,BELOW_BASELINE`,`BELOW_BASELINE_LEADING`,`BELOW_BASELINE_TRAILING` | 组件在显示区域中的放置位置。 | | `weightx` `weighty` | `0.0` | 正的双精度值 | 调整容器大小时,额外空间(水平和垂直)如何在网格单元格中分布。 |以下部分详细讨论了每个约束的影响。
gridx 和 gridy 约束
gridx和gridy约束指定网格中放置组件的单元。一个组件可以在水平和垂直方向上占据多个单元格。一个组件占据的所有单元格合在一起称为该组件的显示区域。
让我们对gridx和gridy约束进行精确定义。它们指定组件显示区域的起始单元格。默认情况下,每个组件只占用一个单元格。我将在下一节讨论gridwidth和gridheight约束时讨论如何让一个组件占据多个单元格。关于设置组件的gridx和gridy约束值的更多细节,请参考清单 1-15。
您可以为gridx和gridy约束中的一个或两个指定一个RELATIVE值。如果你指定了gridx和gridy(一个大于或等于零的整数)的值,你就决定了组件将被放置在哪里。如果你指定一个或两个约束值为RELATVE,布局管理器将确定gridx和/或gridy的值。如果你阅读了关于GridBagLayout类的 API 文档,对于gridx和/或gridy的RELATIVE值的描述不是很清楚。它只是说,当您将gridx和/或gridy的值指定为RELATIVE时,该组件将被放置在该组件之前添加的组件的旁边。API 文档中的这个描述一清二楚!以下段落将通过示例详细描述gridx和gridy的设定值。
案例 1
您已经为gridx和gridy指定了值。这是网格中绝对定位的情况。您的组件根据您指定的gridx和gridy的值放置。你已经在清单 1-15 中看到了这种例子。
案例 2
您已经为gridx指定了一个值,并将gridy的值设置为RELATIVE。在这种情况下,布局管理器需要确定gridy的值。我们来看一个例子。假设您有三个按钮要放在网格中,并且您有一个container对象,它的布局管理器被设置为GridBagLayout。下面的代码片段将三个按钮添加到网格中。图 1-28 显示了带有三个按钮的屏幕。

图 1-28。
Specifying gridx and Setting gridy to RELATIVE
GridBagConstraints gbc = new GridBagConstraints();
JButton b1 = new JButton("Button 1");
JButton b2 = new JButton("Button 2");
JButton b3 = new JButton("Button 3");
gbc.gridx = 0;
gbc.gridy = 0;
container.add(b1, gbc);
gbc.gridx = 0;
gbc.gridy = GridBagConstraints.RELATIVE;
container.add(b2, gbc);
gbc.gridx = 1;
gbc.gridy = GridBagConstraints.RELATIVE ;
container.add(b3, gbc);
按钮b1的位置没有混淆,因为您已经指定了gridx和gridy的值。它被放置在第一行(gridy = 0)和第一列(gridx = 0)。
对于按钮b2,您已经指定了gridx = 0。您希望将它放在第一列,结果与您预期的一样。您已经将gridy指定为b2的RELATIVE。这意味着您在告诉GridBagLayout通过将b2放入第一列(gridx = 0)来为其找到一个合适的行。由于第一行已经被第一列中的b1占据,下一个可用于b2的行是第二行,它被放置在那里。
您已经为按钮b3设置了gridx = 1。这意味着它应该放在第二列。您将它的gridy指定为RELATIVE。这意味着布局管理器需要在第二列中为它找到一行。由于第一行没有任何组件放在第二列,布局管理器将其放在第一行。如果您指定gridx为 0,那么b3会被放置在哪里?再次应用相同的逻辑。由于第一列已经分别在第一行和第二行中有了b1和b2,对于b3唯一可用的下一行是第三行,并且布局管理器将把它放在b2的正下方。
案例 3
您已经为gridy指定了一个值,并将gridx的值设置为RELATIVE。在这种情况下,布局管理器需要确定gridx的值。也就是说,基于行号的指定值,布局管理器必须确定其列号。图 1-29 显示了使用以下代码片段时三个按钮的布局。以这种方式布局按钮的逻辑与前面的例子相同,只是这次布局管理器决定了b2和b3的列号,而不是它们的行号。

图 1-29。
Specifying gridy and setting gridx to RELATIVE in a GridBagLayout
gbc.gridx = 0;
gbc.gridy = 0;
container.add(b1, gbc);
gbc.gridx = GridBagConstraints.RELATIVE;
gbc.gridy = 0;
container.add(b2, gbc);
gbc.gridx = GridBagConstraints.RELATIVE;
gbc.gridy = 1;
container.add(b3, gbc);
案例 4
这是将gridx和gridy都指定为RELATIVE的四种可能性中的最后一种。布局管理器必须确定要添加的组件的行号和列号。它将首先确定行号。该组件的行将是当前行。当前行是哪一行?默认情况下,第一行(gridy = 0)是当前行。当你添加一个组件时,你也可以指定它的gridwidth约束。它的值之一是REMAINDER,这意味着这是该行中的最后一个组件。如果您将组件添加到第一行,且其gridwidth设置为REMAINDER,则第二行成为当前行。一旦布局管理器确定了组件的行号(即当前行),它会将组件放在该行中最后添加的组件旁边的列中。gridx和gridy的默认值为RELATIVE。现在你可以理解为什么清单 1-14 将所有按钮放在第一行,默认情况下,所有按钮都使用RELATIVE作为gridx和gridy。因为默认的gridwidth是 1,所以第一行总是当前行。每当添加一个按钮时,第一行(当前行)被指定为它的行,它的列是该行中添加的最后一个按钮的下一列。让我们来看一些例子,在这些例子中,您将把gridx和gridy都设置为RELATIVE。
例 1:
下面的代码片段展示了如图 1-30 所示的按钮:
gbc.gridx = 0;
gbc.gridy = 0;
container.add(b1, gbc);
gbc.gridx = GridBagConstraints.RELATIVE;
gbc.gridy = GridBagConstraints.RELATIVE;
container.add(b2, gbc);
gbc.gridx = GridBagConstraints.RELATIVE;
gbc.gridy = 1;
container.add(b3, gbc);

图 1-30。
Specifying Both gridx and gridy as RELATIVE
您通过指定gridx = 0和gridy = 0来为b1使用绝对定位。结果是将b1放在第一行第一列。您将b2的gridx和gridy都指定为RELATIVE。布局管理器必须确定b2的行号和列号。它查看当前行,默认情况下是第一行。因此,它将b2的行号设置为 0。它发现第一列中已经有一个组件(b1)。因此,它为b2设置下一列,即第二列。这里您可以看到b2位于第一行和第二列。理解b3的摆放很简单。因为您已经指定了它的gridy = 1,所以它被放在第二行。它的gridx是RELATIVE,因为第一列在第二行中可用,所以它被放在第一列中。
例 2:
下面的代码片段展示了如图 1-31 所示的按钮。请注意,b1按钮被放置在其可用空间的中央,这是默认行为。您可以使用我稍后将讨论的anchor属性定制组件在其分配空间内的放置。

图 1-31。
Specifying gridx and gridy as RELATIVE with gridwidth as REMAINDER
gbc.gridx = 0;
gbc.gridy = 0;
gbc.gridwidth = GridBagConstraints.REMAINDER;// Last component in the row
container.add(b1, gbc);
gbc.gridx = GridBagConstraints.RELATIVE;
gbc.gridy = GridBagConstraints.RELATIVE;
gbc.gridwidth = 1; // Reset to the default value
container.add(b2, gbc);
gbc.gridx = GridBagConstraints.RELATIVE; gbc.gridy = 1;
container.add(b3, gbc);
您为b1指定了gridx = 0和gridy = 0。这次,您将b1的gridwidth指定为REMAINDER。这意味着b1是第一行的最后一个组件。因为这是添加到第一行的唯一组件,所以它成为该行的第一个也是最后一个组件。添加b1后,其gridwidth为REMAINDER,第二行成为当前行。对于b2,将gridx和gridy设置为RELATIVE。布局管理器将第二行(gridy = 1)作为其行号。由于第二行b2之前没有放置元件,所以它将是该行的第一个。这导致将b2放置在第二行第一列。请注意,您将b2和b3的值设置为 1。确定b3的位置很简单。因为您将它的gridy指定为 1(第二行),所以它被放在第二行。它的gridx就是RELATIVE。由于b2已经在第一列,所以放在第二列。
gridwidth 和 gridheight 约束
gridwidth和gridheight约束分别指定组件显示区域的宽度和高度。两者的默认值都是 1。也就是说,默认情况下,组件放置在一个单元中。如果你为一个组件指定gridwidth = 2,它的显示区域将是两个单元格宽。如果您为一个组件指定gridheight = 2,它的显示区域将是两个单元格高。如果你曾经使用过 HTML 表格,你可以将gridwidth与colspan进行比较,将gridheight与 HTML 表格中单元格的rowspan属性进行比较。
您可以为gridwidth和gridheight指定两个预定义的常数。他们是REMAINDER和RELATIVE。gridwidth的REMAINDER值意味着组件将从其gridx单元格跨越到该行的其余部分。换句话说,它是行中的最后一个组件。gridheight的REMAINDER值表示它是该列中的最后一个组件。gridwidth的RELATIVE值表示组件显示区域的宽度将从其gridx到该行的倒数第二个单元格。gridheight的RELATIVE值表示组件显示区域的高度将从其gridy到倒数第二个单元格。让我们为gridwidth分别举一个例子。你可以为gridheight扩展这个概念。唯一的区别是gridwidth影响组件显示区域的宽度,而gridheight影响高度。
以下代码片段将九个按钮添加到一个容器中,第一行三个,第二行六个:
// Expand the component to fill the whole cell
gbc.fill = GridBagConstraints.BOTH;
gbc.gridx = 0;
gbc.gridy = 0;
container.add(new JButton("Button 1"), gbc);
gbc.gridx = 1;
gbc.gridy = 0;
gbc.gridwidth = GridBagConstraints.RELATIVE;
container.add(new JButton("Button 2"), gbc);
gbc.gridx = GridBagConstraints.RELATIVE; gbc.gridy = 0;
gbc.gridwidth = GridBagConstraints.REMAINDER;
container.add(new JButton("Button 3"), gbc);
// Reset gridwidth to its default value 1
gbc.gridwidth = 1;
// Place six JButtons in second row
gbc.gridy = 1;
for(int i = 0; i < 6; i++) {
gbc.gridx = i;
container.add(new JButton("Button " + (i + 4)), gbc);
}
第一句话对你来说是新的。它将GridBagConstraints的fill实例变量设置为BOTH,,这表示添加到单元格中的组件将在两个方向(水平和垂直)上扩展,以填充整个单元格区域。稍后我将更详细地讨论这一点。第一个按钮位于第一行第一列。
第二个按钮位于第一行第二列。它的gridwidth被设置为RELATIVE,这意味着它将从第二列(gridx = 1)跨越到该行的倒数第二列。第一行的最后一列是哪一列?你还不知道。您必须查看添加到GridBagLayout的所有组件,以找出网格中的最大行数和列数。现在,您知道第二个按钮从第二列开始,但是您不知道它将在哪一列结束(或者它将延伸到哪一列)。
让我们看看第三个按钮。您已经指定了它的gridy = 0,这意味着它应该放在第一行。您已经将其gridx设置为RELATIVE,这意味着它将被放置在第一行的第二个按钮之后。您已经将它的gridwidth值设置为REMAINDER,这意味着这是第一行中的最后一个组件。有个有趣的点要注意。第二个按钮将根据需要从第二列扩展到倒数第二列。你是说第三个按钮是第一行的最后一个组件,它应该占据其余的单元格。结果是,由于第二个按钮的gridwidth的RELATIVE的贪婪值,第三个按钮将始终只剩下一个单元格(最后一个单元格)。
在第二行中,您添加了六个按钮。每行中的单元格总数由一行中的最大列数决定。因此,每行(第一行和第二行)将有六个单元格。您已经将gridwidth设置为默认值 1,所以第二行中的每个按钮将只占据一个单元格。第一行第一个按钮占一个单元格,第三个按钮占一个单元格,第二个按钮占剩下的四个,如图 1-32 所示。

图 1-32。
Specifying gridwidth and gridheight
填充约束
A GridBagLayout给出每个组件的首选宽度和高度。列的宽度由列中最宽的部分决定。类似地,行的高度由行中最高的组件决定。fill约束值表示当组件的显示区域大于其尺寸时,组件如何水平和垂直扩展。注意fill约束仅在组件尺寸小于其显示区域时使用。
fill约束有四个可能的值:NONE、HORIZONTAL、VERTICAL和BOTH。它的默认值是NONE,意思是“不要展开组件”值HORIZONTAL表示“水平扩展组件以填充其显示区域。”值VERTICAL表示“垂直扩展组件以填充其显示区域。”值BOTH表示“水平和垂直扩展组件以填充其显示区域。”
以下代码片段向三行三列的网格添加了九个按钮,如图 1-33 所示。

图 1-33。
Specifying the fill constraint for a component in a GridBagLayout
gbc.gridx = 0; gbc.gridy = 0;
container.add(new JButton("Button 1"), gbc);
gbc.gridx = 1; gbc.gridy = 0;
container.add(new JButton("Button 2"), gbc);
gbc.gridx = 2; gbc.gridy = 0;
container.add(new JButton("Button 3"), gbc);
gbc.gridx = 0; gbc.gridy = 1;
container.add(new JButton("Button 4"), gbc);
gbc.gridx = 1; gbc.gridy = 1;
container.add(new JButton("This is a big Button 5"), gbc);
gbc.gridx = 2; gbc.gridy = 1;
container.add(new JButton("Button 6"), gbc);
gbc.gridx = 0; gbc.gridy = 2;
container.add(new JButton("Button 7"), gbc);
gbc.gridx = 1; gbc.gridy = 2;
gbc.fill = GridBagConstraints.HORIZONTAL;
container.add(new JButton("Button 8"), gbc);
gbc.gridx = 2; gbc.gridy = 2;
gbc.fill = GridBagConstraints.NONE;
container.add(new JButton("Button 9"), gbc);
第五个按钮决定第二列的宽度,因为它是该列中最宽的JButton。请注意第一行第二列中的空白。它有空白空间,因为对于第二个按钮来说,fill值是NONE,这是默认的,并且第二个按钮没有扩展到占据其显示区域的整个宽度。它被保留为自己喜欢的大小。看第八个按钮。您指定它应该水平扩展,它这样做是为了匹配其显示区域的宽度。
ipadx 和 ipady 约束
ipadx和ipady约束用于指定组件的内部填充。它们增加了元件的首选尺寸和最小尺寸。默认情况下,两个约束都设置为零。允许负值。这些约束的负值将减小组件的首选和最小尺寸。如果指定了ipadx的值,组件的首选和最小宽度将增加2*ipadx。同样,如果您指定了ipady的值,组件的首选和最小高度将增加2*ipady。这些选项很少使用。ipadx和ipady的值以像素为单位。
insets 约束
insets约束指定组件周围的外部填充。它在组件周围添加空间。您将insets值指定为java.awt.Insets类的对象。它有一个名为Insets(int top, int left, int bottom, int right)的构造函数。您可以为组件的所有四个边指定填充。默认情况下,insets的值被设置为四边都为零像素的Insets对象。下面的代码片段在一个 3X3 的网格中添加了九个按钮,所有按钮的四边都有五个像素。最终布局如图 1-34 所示。请注意,您已经为所有按钮指定了fill约束为BOTH,但是由于它们的insets约束,您仍然可以看到相邻按钮之间的间隙。insets约束告诉布局管理器在组件边缘和显示区域边缘之间留有空间。

图 1-34。
Specifying insets for components in a GridBagLayout
gbc.fill = GridBagConstraints.BOTH;
gbc.insets = new Insets(5, 5, 5, 5);
int count = 1;
for(int y = 0; y < 3; y++) {
gbc.gridy = y;
for(int x = 0; x < 3; x++) {
gbc.gridx = x;
container.add(new JButton("Button " + count++), gbc);
}
}
锚点约束
anchor约束指定当一个组件的尺寸小于其显示区域的尺寸时,该组件应放置在其显示区域内的何处。默认情况下,它的值被设置为CENTER,这意味着组件在其显示区域内居中。
在GridBagConstraints类中定义了许多常量,可以用作anchor约束的值。所有的常量可以分为三类:绝对的、基于方向的和基于基线的。
绝对值为NORTH、SOUTH、WEST、EAST、NORTHWEST、NORTHEAST、SOUTHWEST、SOUTHEAST、CENTER。图 1-35 显示了如何将一个组件放置在具有不同绝对锚值的单元内。请注意,图中的所有九个组件都将其fill约束设置为NONE。

图 1-35。
Absolute anchor values and their effects on component location in the display area
基于方向的值是基于容器的ComponentOrientation属性使用的。分别是PAGE_START、PAGE_END、LINE_START、LINE_END、FIRST_LINE_START、FIRST_LINE_END、LAST_LINE_START、LAST_LINE_END。图 1-36 和图 1-37 显示了当容器的方向设置为LEFT_TO_RIGHT和RIGHT_TO_LEFT时使用基于方向的锚值的效果。您可能会注意到,基于方向的值会根据容器使用的方向进行自我调整。

图 1-37。
Orientation-based anchor values and their effects when the container’s orientation is RIGHT_TO_LEFT

图 1-36。
Orientation-based anchor values and their effects when the container’s orientation is LEFT_TO_RIGHT
当您希望将一行中的组件沿其基线对齐时,将使用基线-基线锚点的值。一个组件的基线是什么?基线是相对于文本的。它是一条假想的线,文本中的字符就停留在这条线上。一个组件可能有一个基线。通常,组件的基线是组件的上边缘与其显示的文本的基线之间的距离(以像素为单位)。您可以通过使用组件的getBaseline(int width, int height)方法来获取组件的基线值。请注意,您需要传递组件的宽度和高度来获取其基线。不是每个组件都有基线。如果一个组件没有基线,这个方法返回–1。图 1-38 显示了三个组件,一个JLabel、一个JTextField和一个JButton,它们沿着基线排成一行GridBagLayout。

图 1-38。
A JLabel, a JTextField, and a JButton aligned along their baselines
GridBagLayout中的每一行都可以有一个基线。图 1-38 显示了包含三个组件的行的基线。图中的水平实线表示基线。请注意,这条水平实线是一条假想的线,它实际上并不存在。它仅用于演示基线概念。只有当至少一个组件具有有效基线并且其锚值为BASLINE、BASELINE_LEADING或BASELINE_TRAILING时,GridBagLayout中的一行才有基线。图 1-39 显示了一些基于基线的锚值。表 1-4 列出了所有可能的值及其描述。
表 1-4。
List of Baseline-Based Anchor’s Values and Descriptions
| 基于基线的锚值 | 竖向定线 | 水平线向 | | --- | --- | --- | | `BASELINE` | 行基线 | 中心 | | `BASELINE_LEADING` | 行基线 | 沿前缘对齐** | | `BASELINE_TRAILING` | 行基线 | 沿后缘对齐*** | | `ABOVE_BASELINE` | 底部边缘接触起始行的基线 | 中心 | | `ABOVE_BASELINE_LEADING` | 底边接触起始行的基线* | 沿前缘对齐** | | `ABOVE_BASELINE_TRAILING` | 底部边缘接触起始行的基线 | 沿后缘对齐*** | | `BELOW_BASELINE` | 上边缘接触起始行的基线* | 中心 | | `BELOW_BASELINE_LEADING` | 顶部边缘接触起始行的基线 | 沿前缘对齐** | | `BELOW_BASELINE_TRAILING` | 上边缘接触起始行的基线* | 沿后缘对齐*** |*starting row: The phrase “starting row” applies only when a component spans multiple rows. Otherwise, read it as the row in which the component is placed. If a row has no baseline, the component is vertically centered **Leading edge is left edge for LEFT_TO_RIGHT orientation and right edge for RIGHT_TO_LEFT orientation ***Trailing edge is right edge for LEFT_TO_RIGHT orientation and left edge for RIGHT_TO_LEFT orientation

图 1-39。
Some baseline-based anchor values in action
权重 x 和权重约束
weightx和weighty约束控制容器中的额外空间如何在行和列之间分配。weightx和weighty的默认值为零。它们可以有任何非负值。
图 1-40 显示一个JFrame使用九个按钮的GridBagLayout。图 1-41 为同一JFrame的水平和垂直展开图。

图 1-41。
A JFrame with a GridBagLayout having nine buttons after resizing

图 1-40。
A JFrame with a GridBagLayout having nine buttons with no extra spaces
请注意按钮组周围生成的额外空间。您已经将所有按钮的fill约束设置为BOTH,因此所有按钮都代表了GridBagLayout中的单元格网格。weightx和weighty约束保留默认值零。当所有组件的weightx和weighty约束设置为零时,容器中的任何额外空间都会出现在容器边缘和单元格网格边缘之间。
weightx值决定了额外水平空间在各列之间的分布,而weighty值决定了额外垂直空间在各行之间的分布。如果所有组件都有相同的weightx和weighty,额外的空间会在它们之间平均分配。图 1-42 显示了当weightx和weighty设置为 1.0 时的所有九个按钮。您可以为weightx和/或weighty设置任何正值。只要它们对于所有组件都是相同的,额外的空间将在它们之间平均分配。

图 1-42。
A JFrame with a GridBagLayout having nine buttons after resizing. All buttons have their weightx and weighty set to 1. Extra space is distributed among the display area of all buttons equally
下面是如何根据weightx值计算每一列的额外空间。假设一个带有GridBagLayout的容器被水平扩展以使ES像素的额外空间可用。假设网格中有三列三行。布局管理器将为每列中的组件找到weightx值的最大值。假设cwx1、cwx2和cwx3分别是第 1 列、第 2 列和第 3 列的weightx的最大值。列 1 将获得(cwx1 * ES)/(cwx1 + cwx2 + cwx3)数量的额外空间。列 2 将获得(cwx2 * ES)/(cwx1 + cwx2 + cwx3)数量的额外空间。第 3 列将获得(cwx3 * ES)/(cwx1 + cwx2 + cwx3)数量的额外空间。有必要通过使用该列中的最大值weightx来计算给予该列的额外空间,以维护单元格网格。使用weighty在单元格之间分配额外垂直空间的计算是类似的
Tip
weightx和weighty约束影响组件显示区域的大小和组件本身的大小。对于weightx和weighty,通常使用 0.0 到 1.0 之间的值。但是,您可以使用任何非负值。组件的大小受其他约束的影响,如fill、gridwidth、gridheight等。如果您希望您的组件在额外空间可用时扩展,您需要将其fill约束设置为HORIZONTAL、VERTICAL或BOTH。通过使用GridBagLayout类的setConstraints(Component c, GridBagConstraints cons)方法,在将组件添加到容器中之后,您还可以在GridBagLayout中为组件设置约束。
布局
在javax.swing包中的SpringLayout类的一个实例代表一个SpringLayout管理器。回想一下,布局管理器的工作是计算容器中组件的四个属性(x、y、宽度和高度)。换句话说,它负责定位容器内的组件并计算它们的大小。一个SpringLayout管理器用弹簧来表示组件的这四个属性。手工编码很麻烦。它是针对 GUI 生成器工具的。在本节中,我将通过手工编写一些简单的例子来介绍这种布局的基础。
什么是春天?在经理的背景下,你可以把弹簧想象成机械弹簧,它可以被拉伸、压缩或保持正常状态。一个Spring类的对象代表了一个SpringLayout中的弹簧。一个Spring对象有四个属性:最小值、首选值、最大值和当前值。你可以把这四个属性想象成它的四种长度。弹簧在最大程度压缩时有最小值。在正常状态下(既不压缩也不拉伸),它有自己的首选值。在最拉伸的状态下,它有最大值。它在任何给定时间点的值就是它的当前值。当弹簧的最小值、首选值和最大值相同时,称为支柱。
你如何创造一个春天?Spring类没有公共构造函数。它包含创建弹簧的工厂方法。要从头开始创建弹簧或支柱,可以使用其重载的constant()静态方法。您也可以使用元件的宽度或高度来建立弹簧。弹簧的最小值、首选值和最大值是根据组件的相应宽度或高度值设置的
// Create a strut of 10 pixels
Spring strutPadding = Spring.constant(10);
// Create a spring having 10, 25 and 50 as its minimum,
// preferred, and maximum value respectively.
Spring springPadding = Spring.constant(10, 25, 50);
// Create a spring from the width of a component named c1
Spring s1 = Spring.width(c1);
// Create a spring from the height of a component named c1
Spring s2 = Spring.height(c1);
Spring类有一些实用方法,可以让你操作 spring 属性。您可以通过使用sum()方法添加两个弹簧来创建一个新弹簧,如下所示:
// Assuming that s1 and s2 are two springs
Spring s3 = Spring.sum(s1, s2);
执行语句时不执行计算sum。相反,弹簧s3存储了s1和s2的引用。每当s1、s2或两者都改变时,计算s3的值。在这种情况下,s3的行为就像串联了弹簧s1和s2一样。
也可以通过从一个弹簧中减去另一个弹簧来创建弹簧。但是,您没有名为subtract()的方法。有一种叫做minus()的方法可以给出弹簧的负值。您可以使用sum()和minus()方法的组合来执行减法,如下所示:
// Perform s1 – s2, which is the same as s1 + (-s2)
Spring s4 = Spring.sum(s1, Spring.minus(s2));
要获得两个弹簧s1和s2的最大值,可以使用Spring.max(s1, s2)。注意没有对应的方法叫做min()。然而,您可以通过使用minus()和max()方法的组合来模拟它,就像这样:
// Minimum of 2 and 5 is the minus of the maximum of –2 and –5.
// To get the minimum of two spring s1 and s2, you can use minus
// of maximum of –s1 and –s2
Spring min = Spring.minus(Spring.max(Spring.minus(s1), Spring.minus(s2)));
你也可以使用scale()方法得到另一个弹簧的一部分。例如,如果您有一个弹簧s1,并且您想要创建一个值为其 40%的弹簧,您可以通过将 0.40f 作为第二个参数传递给scale()方法来实现,如下所示:
String fractionSpring = Spring.scale(s1, 0.40f);
Tip
创建弹簧后,不能更改弹簧的最小值、首选值和最大值。您可以通过使用它的setValue()方法来设置它的当前值。
你刚刚讨论了很多关于弹簧的问题。是时候看看他们的行动了。如何用SpringLayout将组件添加到容器中?在最简单的形式中,您使用容器的add()方法来添加组件。清单 1-16 将一个JFrame的内容窗格的布局设置为一个SpringLayout,并添加了两个按钮。图 1-43 显示运行程序时的JFrame。
清单 1-16。最简单的 spring 布局
// SimplestSpringLayout.java
package com.jdojo.swing;
import java.awt.Container;
import javax.swing.JFrame;
import javax.swing.SpringLayout;
import javax.swing.JButton;
public class SimplestSpringLayout {
public static void main(String[] args) {
JFrame frame = new JFrame("Simplest SpringLayout");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
Container contentPane = frame.getContentPane();
// Set the content pane's layout as SpringLayout
SpringLayout springLayout = new SpringLayout();
contentPane.setLayout(springLayout);
// Add two JButtons to the content pane
JButton b1 = new JButton("Button 1");
JButton b2 = new JButton("Little Bigger Button 2");
contentPane.add(b1);
contentPane.add(b2);
frame.pack();
frame.setVisible(true);
}
}

图 1-43。
The JFrame when you run the SimplestSpringLayout class
图 1-43 显示你只能看到JFrame的标题栏。当您展开 JFrame 时,您会看到如图 1-44 所示的屏幕。请注意,您的两个按钮都在JFrame中。然而,它们是重叠的。最简单的SpringLayout例子可能是最简单的编码;但是,看到结果就没那么简单了。

图 1-44。
After expanding the JFrame when you run the SimplestSpringLayout class
那么,你最简单的SpringLayout例子有什么问题呢?我提到过一个SpringLayout很难手工编码,你现在看到了!你在框架上使用了pack()方法来给它一个最佳的尺寸。但是您的框架没有显示区域。当您使用SpringLayout时,您必须指定所有组件和容器的 x、y、宽度和高度。这对开发人员来说是太多的工作,这就是为什么我说这个布局管理器是为 GUI 构建者设计的,而不是为手工编码设计的。
让我们再次检查图 1-43 和图 1-44 所示的屏幕。您会看到容器获得了位置(x 和 y ),按钮获得了大小(宽度和高度)。默认情况下,JFrame显示在(0,0)处,这就是您看到容器位置的方式(实际上,您的容器是一个内容窗格)。按钮获得它们默认的最小、首选和最大尺寸(都设置为相同的值),这是您展开屏幕后看到的按钮。默认情况下,SpringLayout将容器中的所有组件定位在(0,0)处。在这种情况下,两个按钮都位于(0,0)处。要解决此问题,请指定两个按钮和内容窗格的 x、y、宽度和高度。
A SpringLayout使用约束来排列组件。Constraints类的对象是SpringLayout类的静态内部类,代表组件和容器的约束。一个Constraints对象允许你使用它的方法指定一个组件的 x、y、宽度和高度。所有四个属性都必须根据一个Spring对象来指定。当您指定这些属性时,您需要使用SpringLayout类中定义的常量之一来指定它们,如表 1-5 中所列。
表 1-5。
List of Constants Defined in the SpringLayout Class
| 常数名称 | 描述 | | --- | --- | | `NORTH` | 它是`y`的同义词。它是组件的顶部边缘。 | | `WEST` | 它是`x`的同义词。它是组件的左边缘。 | | `SOUTH` | 它是组件的底部边缘。其值与`NORTH + HEIGHT`相同。 | | `EAST` | 它是组件的右边缘。和`WEST + WIDTH`一样。 | | `WIDTH` | 组件的宽度。 | | `HEIGHT` | 组件的高度。 | | `HORIZONTAL_CENTER` | 它是组件的水平中心。和`WEST + WIDTH/2`一样。 | | `VERTICAL_CENTER` | 它是组件的垂直中心。和`NORTH + HEIGHT/2`一样。 | | `BASELINE` | 它是组件的基线。 |可以相对于容器或另一个组件设置组件的 x 和 y 约束。Constraints类的一个对象指定了一个组件的约束。您需要创建一个SpringLayout.Constraints类的对象,并使用它的方法来设置约束的值。当你添加一个组件到一个容器中时,将这个约束对象传递给add()方法。清单 1-17 为两个按钮设置了 x 和 y 约束。注意,值(10,20)和(150,20)是根据Spring对象指定的,它们是从内容窗格的边缘开始测量的。图 1-45 显示运行程序时展开JFrame后的画面。
清单 1-17。为元件设置 x 和 y 约束
// SpringLayout2.java
package com.jdojo.swing;
import javax.swing.SpringLayout;
import java.awt.Container;
import javax.swing.JFrame;
import javax.swing.JButton;
import javax.swing.Spring;
public class SpringLayout2 {
public static void main(String[] args) {
JFrame frame = new JFrame("SpringLayout2");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
Container contentPane = frame.getContentPane();
// Set the content pane's layout to a SpringLayout
SpringLayout springLayout = new SpringLayout();
contentPane.setLayout(springLayout);
// Add two JButtons to the content pane
JButton b1 = new JButton("Button 1");
JButton b2 = new JButton("Little Bigger Button 2");
// Create Constraints objects for b1 and b2
SpringLayout.Constraints b1c = new SpringLayout.Constraints();
SpringLayout.Constraints b2c = new SpringLayout.Constraints();
// Create a Spring object for y value for b1 and b2
Spring yPadding = Spring.constant(20);
// Set (10, 20) for (x, y) for b1
b1c.setX(Spring.constant(10));
b1c.setY(yPadding);
// Set (150, 20) for (x, y) for b2
b2c.setX(Spring.constant(150));
b2c.setY(yPadding);
// Use the Constraints object while adding b1 and b2
contentPane.add(b1, b1c);
contentPane.add(b2, b2c);
frame.pack();
frame.setVisible(true);
}
}

图 1-45。
After expanding the JFrame when the (x, y) are set for two buttons
您尚未确定JFrame的大小。运行程序时,JFrame仍然显示,没有显示区域。至少这次两个按钮没有重叠。你选择了一个 150 像素的任意值作为b2的 x 值。也就是说,b2的左边缘距离内容窗格的左边缘 150 像素。有一种方法可以指定b2的左边缘应该与b1的右边缘相距指定的距离。为了实现这一点,您需要首先将b1添加到容器中。当您向容器中添加一个组件时,SpringLayout将一个Constraints对象关联到该组件,不管您是否将一个约束对象传递给容器的add()方法。您可以使用SpringLayout类的getConstraint(String edge, Component c)方法来获取组件任何边的约束。下面的代码片段做了同样的事情。它将b1的(x,y)设置为(10,20),将b2的(x,y)设置为(b1的右边缘+ 5,20)。如果用下面的代码片段替换清单 1-17 中添加两个按钮的代码,b2将出现在b1右侧 10 个像素处:
// Create a Spring object for y value for b1 and b2
Spring yPadding = Spring.constant(20);
// Set (10, 20) for (x, y) for b1
b1c.setX(Spring.constant(10));
b1c.setY(yPadding);
// Add b1 to the content pane first
contentPane.add(b1, b1c);
// Now query the layout manager for b1's EAST constraint,
// which is the right edge of b1
Spring b1Right = springLayout.getConstraint(SpringLayout.EAST, b1);
// Add a 5-pixel strut to the right edge of b1 to define the
// left edge of b2 and set it using setX() method on b2c
Spring b2Left = Spring.sum(b1Right, Spring.constant(5));
b2c.setX(b2Left);
b2c.setY(yPadding);
// Now add b2 to the content pane
contentPane.add(b2, b2c);
有一种更简单、更直观的方式来为SpringLayout中的组件设置约束。首先,将所有组件添加到容器中,不用担心它们的约束,然后使用SpringLayout类的putConstraint()方法定义约束。这里有两个版本的putConstraint()方法:
void putConstraint(String targetEdge, Component targetComponent, int padding, String sourceEdge,Component sourceComponent)void putConstraint(String targetEdge, Component targetComponent, Spring padding, String sourceEdge, Component sourceComponent)
第一个版本使用支柱。第三个参数(int padding)定义了一个固定弹簧,它将作为两个组件边缘之间的支柱(固定距离)。第二个版本使用弹簧代替。你可以将方法描述理解为,“targetComponent的targetEdge与sourceComponent的sourceEdge相距padding例如,如果您希望b2的左边缘距离b1的右边缘 5 个像素,您可以调用此方法:
// Set b2's left edge 5 pixels from b1's right edge
springLayout.putConstraint(SpringLayout.WEST, b2, 5,
SpringLayout.EAST, b1);
要将b1(左边缘定义 x 值)的左边缘设置为距离内容窗格的左边缘 10 个像素,可以使用
springLayout.putConstraint(SpringLayout.WEST, b1, 5,
SpringLayout.WEST, contentPane);
让我们回到你的JFrame在调用它的pack()方法时的大小调整问题。您需要设置内容窗格底部和右边的位置,以便pack()方法能够正确地调整它的大小。您将它的下边缘设置为比b1(或b2,离它的下边缘最近的那个)的下边缘低 10 个像素。在本例中,两者距离内容窗格的底部边缘的距离相同。您将它的右边缘设置为距离内容窗格中最右边的JButton的右边缘 10 个像素。以下代码片段实现了这一点:
// Set the bottom edge of the content pane
springLayout.putConstraint(SpringLayout.SOUTH, contentPane, 10,
SpringLayout.SOUTH, b1);
// Set the right edge of the content pane
springLayout.putConstraint(SpringLayout.EAST, contentPane, 10,
SpringLayout.EAST, b2);
清单 1-18 包含了完整的程序,图 1-46 显示了运行程序时的JFrame。
清单 1-18。使用 SpringLayout 类的 putConstraint()方法
// NiceSpringLayout.java
package com.jdojo.swing;
import javax.swing.JFrame;
import java.awt.Container;
import javax.swing.SpringLayout;
import javax.swing.JButton;
public class NiceSpringLayout {
public static void main(String[] args) {
JFrame frame = new JFrame("SpringLayout2");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
Container contentPane = frame.getContentPane();
// Set the content pane's layout to a SpringLayout
SpringLayout springLayout = new SpringLayout();
contentPane.setLayout(springLayout);
// Create two JButtons
JButton b1 = new JButton("Button 1");
JButton b2 = new JButton("Little Bigger Button 2");
// Add two JButtons without using any constraints
contentPane.add(b1);
contentPane.add(b2);
// Now add constraints to both JButtons
// Set x for b1 as 10
springLayout.putConstraint(SpringLayout.WEST, b1, 10,
SpringLayout.WEST, contentPane);
// Set y for b1 as 20
springLayout.putConstraint(SpringLayout.NORTH, b1, 20,
SpringLayout.NORTH, contentPane);
// Set x for b2 as 10 from the right edge of b1
springLayout.putConstraint(SpringLayout.WEST, b2, 10,
SpringLayout.EAST, b1);
// Set y for b1 as 20
springLayout.putConstraint(SpringLayout.NORTH, b2, 20,
SpringLayout.NORTH, contentPane);
/* Now set height and width for the content pane as the bottom
edge of b1 + 10 and right edge of b2 + 10\. Note that source
is b1 for content pane's height and b2 for its width
*/
// Set the bottom edge of the content pane
springLayout.putConstraint(SpringLayout.SOUTH, contentPane, 10,
SpringLayout.SOUTH, b1);
// Set the right edge of the content pane
springLayout.putConstraint(SpringLayout.EAST, contentPane, 10,
SpringLayout.EAST, b2);
frame.pack();
frame.setVisible(true);
}
}

图 1-46。
Nice SpringLayout with the JFrame sized automatically
是一个非常强大的布局,可以模仿许多复杂的布局。下面的代码片段有更多的例子。评论解释了它应该做什么。
// Place a JButton b1 horizontally centered at the top of the content pane, you // would set its constraints as below. Replace HORIZONTAL_CENTER with
// VERTICAL_CENTER to center the JButton vertically
springLayout.putConstraint(SpringLayout.HORIZONTAL_CENTER, north, 0,
SpringLayout.HORIZONTAL_CENTER,
contentPane);
// You can set the width of two JButtons, b1 and b2, to be the same by
// assigning the maximum width to the both of them. Assuming that you have
// already added b1 and b2 JButtons to the container
SpringLayout.Constraints b1c = springLayout.getConstraints(b1);
SpringLayout.Constraints b2c = springLayout.getConstraints(b2);
// Get a spring that represents the maximum of the width of b1 and b2,
// and set that spring as width for both b1 and b2
Spring maxWidth = Spring.max(b1c.getWidth(), b2c.getWidth());
b1c.setWidth(maxWidth);
b2c.setWidth(maxWidth);
群组布局
GroupLayout在javax.swing包里。它是供 GUI 构建者使用的。然而,手工编码也很容易。
A GroupLayout使用了组的概念。组由元素组成。组的元素可以是组件、间隙或另一个组。您可以将间隙视为两个组件之间的不可见区域。
在使用GroupLayout之前,你必须理解组的概念。有两种类型的组:
- 顺序组
- 平行链晶
当一个组中的元素一个接一个地连续排列时,称为顺序组。当一组中的元素平行放置时,称为平行组。平行组以四种方式之一对齐其元素:基线、居中、前导和尾随。在GroupLayout中,您需要为每个组件定义两次布局——一次沿着水平轴,一次沿着垂直轴。也就是说,您需要分别指定所有组件如何水平和垂直地组成一个组。让我们看一些组的例子。图 1-47 显示了一组两个组件。

图 1-47。
Two components, C1 and C2, form a sequential group along the horizontal axis and a parallel group along the vertical axis
在图 1-47 中,两个轴仅用于讨论目的,它们不是布局的一部分。组件一个接一个地放置(从左到右),沿水平轴形成一个连续的组。它们沿着垂直轴形成平行组。沿垂直轴,在平行组中,两个组件沿其顶边对齐。如果您对沿水平轴和垂直轴的顺序组和平行组的可视化有问题,您可以将图 1-47 重绘为图 1-48 。水平方向上的两个虚线箭头(从左到右)表示 C1 和 C2,当您在水平方向上可视化它们的分组时。您可以看到两个箭头是串联的,因此 C1 和 C2 沿着水平轴形成了一个连续的组。垂直方向上的两个虚线箭头(从上到下位于组件 C1 的左侧)表示 C1 和 C2,当您沿着垂直轴查看它们时。你可以看到这两个箭头不是串联的。相反,它们是并行的。因此,C1 和 C2 沿着纵轴形成平行组。您需要找出平行组的对齐方式。在这种情况下,C1 和 C2 沿着它们的上边缘对齐,这在GroupLayout术语中称为前导对齐。

图 1-48。
Groupings for components C1 and C2
C1 和 C2 还有其他可能的路线吗?平行组中有四种可能的对齐方式:基线对齐、居中对齐、前导对齐和尾随对齐。如果平行组沿垂直轴出现,则所有四种类型的对齐都是可能的。如果平行组沿水平轴出现,则只可能有三种对齐方式(居中、前导和尾随)。沿着垂直轴,前导与顶边相同,尾随与底边相同。沿水平轴,如果组件方向为LEFT_TO_RIGHT,前导为左边缘,如果组件方向为RIGHT_TO_LEFT,前导为右边缘。图 1-49 和图 1-50 显示了沿垂直轴和水平轴的可能对准。该对准由虚线示出。请注意,沿垂直轴,对齐线是水平的,沿水平轴,对齐线是垂直的。GroupLayout.Alignment枚举中的四个常量LEADING、TRAILING、CENTER和BASELINE用于表示四种对准类型。

图 1-50。
The three possible alignments in a parallel group along the horizontal axis in a group for component orientation of LEFT_TO_RIGHT. For RIGHT_TO_LEFT orientation, LEADING and TRAILING will swap edges

图 1-49。
The four possible alignments in a parallel group along the vertical axis in a group
如何为一个GroupLayout创建连续和并行的组?GroupLayout类包含三个内部类:Group、SequentialGroup和ParallelGroup。Group是一个抽象类,另外两个类继承自Group类。您不必直接创建这些类的对象。相反,您使用GroupLayout类的工厂方法来创建它们的对象。
GroupLayout类提供了两个单独的方法来创建组:createSequentialGroup()和createParallelGroup()。从这些方法的名称可以明显看出它们创建的组的种类。请注意,您需要为平行组指定对齐方式。createParallelGroup()方法被重载。不带参数的版本默认对齐为LEADING。另一个版本允许您指定对齐方式。一旦您有了一个组对象,您就可以分别使用它的addComponent()、addGap()和addGroup()方法向它添加组件、间隙和组。
你如何使用GroupLayout管理器?以下是使用GroupLayout需要遵循的步骤。假设你要在一个JFrame中放置两个按钮,如图 1-51 所示。

图 1-51。
The simplest GroupLayout in which two buttons are placed side by side
假设JFrame被命名为frame,两个JButtons被命名为b1和b2。首先,您需要创建一个GroupLayout类的对象。它只包含一个将容器引用作为参数的构造函数。这意味着在创建一个GroupLayout类的对象之前,您需要获得对您想要为其创建GroupLayout的容器的引用。
// Get the reference of the container
Container contentPane = frame.getContentPane();
// Create a GroupLayout object
GroupLayout groupLayout = new GroupLayout(contentPane);
// Set the layout manager for the container
contentPane.setLayout(groupLayout);
其次,您需要创建沿水平轴的组件组(称为水平组),并使用setHorizontalGroup()方法将该组设置为GroupLayout。请注意,一个组可以沿任何轴(水平轴和垂直轴)连续或平行。在你的例子中,两个按钮,b1和 b2,沿着水平轴形成一个连续的组。
// Create a sequential group
GroupLayout.SequentialGroup sGroup = groupLayout.createSequentialGroup();
// Add two buttons to the group
sGroup.addComponent(b1);
sGroup.addComponent(b2);
// Set the horizontal group for the GroupLayout
groupLayout.setHorizontalGroup(sGroup);
您可以将所有步骤合并为一步,如下所示:
groupLayout.setHorizontalGroup(groupLayout.createSequentialGroup()
.addComponent(b1)
.addComponent(b2));
最后,沿着垂直轴创建组件组(称为垂直组),并使用setVerticalGroup()方法将该组设置为GroupLayout。两个按钮沿垂直轴形成一个平行组。您可以按如下方式完成此操作:
groupLayout.setVerticalGroup(
groupLayout.createParallelGroup(GroupLayout.Alignment.BASELINE)
.addComponent(b1)
.addComponent(b2));
Tip
在一个GroupLayout中,你不能使用它的add()方法添加一个组件到容器中。相反,您可以沿着水平轴和垂直轴将组件添加到一个组中,并使用setHorizontalGroup()和setVerticalGroup()方法将该组添加到GroupLayout中。
清单 1-19 演示了如何使用一个GroupLayout在一个JFrame中并排显示两个按钮。运行程序时,JFrame显示如图 1-51 所示。我稍后将讨论更复杂的例子。
清单 1-19。最简单的组布局
// SimplestGroupLayout.java
package com.jdojo.swing;
import java.awt.Container;
import javax.swing.JFrame;
import javax.swing.JButton;
import javax.swing.GroupLayout;
public class SimplestGroupLayout {
public static void main(String[] args) {
JFrame frame = new JFrame("Simplest GroupLayout");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
Container contentPane = frame.getContentPane();
// Create an object of the GroupLayout class for contentPane
GroupLayout groupLayout = new GroupLayout(contentPane);
// Set the content pane's layout to a GroupLayout
contentPane.setLayout(groupLayout);
// Add two JButtons to the content pane
JButton b1 = new JButton("Button 1");
JButton b2 = new JButton("Little Bigger Button 2");
groupLayout.setHorizontalGroup(
groupLayout.createSequentialGroup()
.addComponent(b1)
.addComponent(b2));
groupLayout.setVerticalGroup(
groupLayout.createParallelGroup(GroupLayout.Alignment.BASELINE)
.addComponent(b1)
.addComponent(b2));
frame.pack();
frame.setVisible(true);
}
}
A GroupLayout还有两个特性值得讨论:
- 它允许您在两个组件之间添加间隙。
- 它允许您指定组件、间隙和组的调整大小行为。
你可以把间隙想象成一个看不见的组件。有两种类型的间隙:两个组件之间的间隙,以及组件和容器之间的间隙。您可以使用Group类的addGap()方法在两个组件之间添加一个间隙。您可以添加刚性间隙和柔性间隙(如弹簧)。刚性间隙的大小是固定的。灵活间隙有最小、首选和最大尺寸,当调整容器大小时,它就像弹簧一样。在前面的例子中,要在b1和b2之间添加一个 10 像素的刚性间隙,你可以这样设置你的水平组:
groupLayout.setHorizontalGroup(groupLayout.createSequentialGroup()
.addComponent(b1)
.addGap(10)
.addComponent(b2));
有三种方法可以在两个组件之间添加间隙。它们基于间隙大小及其调整大小的能力。
- 您可以使用
addGap(int gapSize)在两个组件之间添加一个刚性间隙。 - 您可以使用
addGap(int min, int pref, int max)方法在两个组件之间添加一个灵活的(类似弹簧的)间隙,它有一个最小、一个首选和一个最大尺寸。要添加一个最小、首选和最大尺寸分别为 5、10 和 50 的灵活间隙,可以这样设置水平组:groupLayout.setHorizontalGroup(groupLayout.createSequentialGroup().addComponent(b1).addGap(5, 10, 50).addComponent(b2)); - 您可以在两个组件之间添加首选间隙。在这种情况下,您可以选择指定间隙的大小,或者让布局管理器为您计算。但是,就此差距而言,您必须指定这两个组件的关联方式。这种间隙有三种:
RELATED、UNRELATED和INDENT。如果您要在标签和其对应的字段之间添加首选间隙,您需要在它们之间添加一个RELATED间隙。例如,如果您有一个登录表单,并且您想要在“用户 id:”和用于输入用户 ID 的文本字段之间添加一个首选间隙,那么您可以在它们之间添加一个RELATED间隙。当两个组件属于不同的组时,使用UNRELATED间隙。当您添加一个间隙只是为了缩进一个组件时,您添加了一个INDENT间隙。三种类型的间隙由在LayoutStyle.ComponentPlacement枚举中定义的三个常量RELATED、UNRELATED和INDENT表示。使用addPreferredGap()方法添加首选间隙。以下代码片段在b1和b2之间添加了一个RELATED首选间隙:groupLayout.setHorizontalGroup(groupLayout.createSequentialGroup().addComponent(b1).addPreferredGap(LayoutStyle.ComponentPlacement.RELATED).addComponent(b2));
您需要使用GroupLayout.SequentialGroup类的addContainerGap()方法在组件和容器的边缘之间添加一个间隙。该方法被重载。它还允许您指定间隙的首选大小和最大大小。
当您在不同的平台上运行应用时,设置硬编码的间隙可能会产生问题。这就是为什么GroupLayout有两个方法可以让你指定让GroupLayout根据你的应用运行的平台来计算首选的间隙。要让GroupLayout计算并设置两个组件之间的间隙,您需要调用它的setAutoCreateGaps(true)方法。要让它计算并设置组件和容器边缘之间的间隙,需要调用它的setAutoCreateContainerGaps(true)方法。默认情况下,间隙的自动计算是禁用的。替换该语句
// Create an object of the GroupLayout class
GroupLayout groupLayout = new GroupLayout(contentPane);
在清单 1-19 中使用了以下语句
// Create an object of the GroupLayout class and setup gaps
GroupLayout groupLayout = new GroupLayout(contentPane);
groupLayout.setAutoCreateGaps(true);
groupLayout.setAutoCreateContainerGaps(true);
现在,JFrame将如图 1-52 所示。您可以看到布局管理器为您添加了必要的间隙。

图 1-52。
The simplest GroupLayout with auto gaps enabled
A GroupLayout考虑组件的最小、首选和最大尺寸。当调整容器大小时,布局管理器询问组件的大小并相应地调整它们的大小。但是,您可以通过使用addComponent(Component c, int min, int pref, int max)方法来覆盖这种行为,该方法允许您指定组件的最小、首选和最大大小。您需要理解在GroupLayout类中定义的两个常量的含义。他们是DEFAULT_SIZE和PREFERRED_SIZE。它们可用于addComponent()方法中的min、pref和max参数。DEFAULT_SIZE表示布局管理器应该向组件请求该尺寸类型并使用它。PREFERRED_SIZE意味着管理器应该使用组件的首选尺寸。例如,如果您希望上一个示例中的JButton b2展开(默认情况下,一个JButton具有相同的min、pref和max大小),您可以将它添加到水平组,如下所示:
groupLayout.setHorizontalGroup(groupLayout.createSequentialGroup()
.addComponent(b1)
.addComponent(b2,
GroupLayout.PREFERRED_SIZE,
GroupLayout.PREFERRED_SIZE,
Integer.MAX_VALUE));
通过将PREFERRED_SIZE指定为最小尺寸和首选尺寸,您是在告诉布局管理器b2不应该缩短到其首选尺寸以下。Integer.MAX_VALUE因为它的最大尺寸告诉布局管理器它可以无限扩展。要使一个组件不可调整大小,您可以像使用GroupLayout.PREFERRED_SIZE一样使用它的所有三个大小。
您可以在GroupLayout中嵌套组。让我们来看一个名为b1、b2、b3、b4的四个按钮的布局,如图 1-53 所示。

图 1-53。
Nested groups in GroupLayout
让我们看看沿水平轴的组件布局。你可以看到两个平行的组(b1、b3)和(b2、b4),这两组是顺序放置的。让我们在伪代码中用PG和SG分别表示并行组和顺序组。注意在PG ( b1,b3)中,组件沿LEADING边缘(此处为左边缘)对齐,在PG ( b2,b4)中,组件沿TRAILING边缘(此处为右边缘)对齐。让我们将对齐插入到您的伪代码中,这些组将如下所示:PGLEADING和PGTRAILING。为了讨论这个例子,我编造了这个语法。您将很快看到 Java 代码。如果您对可视化排列有问题,您可以参考图 1-54 ,其中每个按钮都由沿水平轴的箭头表示。

图 1-54。
Four buttons represented by four arrows along horizontal axis
箭头的对齐方式与按钮相同。你可以观察到b1和b3的箭头是平行的,b2和b4的箭头也是平行的。如果您将两个平行的组可视化,您可以观察到这两个组沿着水平轴组成一个连续的组。为了帮助你形象化这个最终的排列,箭头排列已经在图 1-55 中进行了细化。

图 1-55。
Four buttons represented by four arrows along horizontal axis
每个平行组显示在虚线矩形内。从虚线矩形出来的箭头表示这些组沿着水平轴是连续的。理解这些组件沿轴的平行和顺序排列可能需要一段时间。一旦你掌握了它,在一个复杂的场景中使用一个GroupLayout将会非常容易。最有可能的是,您将使用 GUI 生成器工具来安排您的组件,并且您不会关心组的复杂性。但是,理解布局背后的概念总是有帮助的。
为了沿着水平轴结束这个讨论,伪代码看起来如下:
Horizontal Group = SG(PGLEADING, PGTRAILING)
类似地,您可以沿着垂直轴可视化分组排列。如果你在视觉上有问题,你可以把四个按钮都画成从上到下的箭头,看看它们是如何沿着纵轴分组的。以下是垂直分组排列:
Vertical Group = SG(PGBASELINE, PGBASELINE)
现在,很容易将伪代码翻译成 Java 代码,如清单 1-20 所示。
清单 1-20。GroupLayout 中的嵌套组
// NestedGroupLayout.java
package com.jdojo.swing;
import java.awt.Container;
import javax.swing.JFrame;
import javax.swing.JButton;
import javax.swing.GroupLayout;
import static javax.swing.GroupLayout.Alignment.*;
public class NestedGroupLayout {
public static void main(String[] args) {
JFrame frame = new JFrame("Nested Groups in GroupLayout");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
Container contentPane = frame.getContentPane();
// Set the content's pane layout to GroupLayout
GroupLayout groupLayout = new GroupLayout(contentPane);
groupLayout.setAutoCreateGaps(true);
groupLayout.setAutoCreateContainerGaps(true);
contentPane.setLayout(groupLayout);
// Add four JButtons to the content pane
JButton b1 = new JButton("Button 1");
JButton b2 = new JButton("Little Bigger Button 2");
JButton b3 = new JButton("3");
JButton b4 = new JButton("Button 4");
groupLayout.setHorizontalGroup(
groupLayout.createSequentialGroup()
.addGroup(groupLayout.createParallelGroup(LEADING)
.addComponent(b1)
.addComponent(b3))
.addGroup(groupLayout.createParallelGroup(TRAILING)
.addComponent(b2)
.addComponent(b4))
);
groupLayout.setVerticalGroup(
groupLayout.createSequentialGroup()
.addGroup(groupLayout.createParallelGroup(BASELINE)
.addComponent(b1)
.addComponent(b2))
.addGroup(groupLayout.createParallelGroup(BASELINE)
.addComponent(b3)
.addComponent(b4))
);
frame.pack();
frame.setVisible(true);
}
}
如何使两个组件的大小相同?让我们试着让b1和b3大小相同。当使组件可调整大小时,您需要考虑两件事情。首先,您需要考虑组的可调整行为。其次,您需要考虑组内组件的可调整行为。平行组的大小是最大元素的大小。如果你考虑PG{LEADING](b1, b3),这个组的宽度将是b1的大小,因为b1是这个组中最大的组件。默认情况下,JButton的大小是固定的。要使b3伸展到组的大小(这是b1的大小),您必须将它添加到组中,指定它可以扩展为addComponent(b3, GroupLayout.DEFAULT_SIZE, GroupLayout.DEFAULT_SIZE, Integer.MAX_VALUE)。这将迫使b3拉伸到与其组相同的大小,从而与b1的宽度相同。如果两个组件不在同一个平行组中,要使它们大小相同,可以使用GroupLayout类的linkSize()方法。当使用linkSize()方法使组件大小相同时,组件变得不可调整大小,不管它们的最小、首选和最大大小。
// Make b1, b2, b3 and b4 the same size
groupLayout.linkSize(b1, b2, b3, b4);
// Make b1 and b3 the same size horizontally
groupLayout.linkSize(SwingConstants.HORIZONTAL, new Component[]{b1, b3});
当您使用createParallelGroup(GroupLayout.Alignment a, boolean resizable)方法创建一个并行组时,您也可以调整组的大小。如果您将可调整大小的组件放在可调整大小的组中,则当您调整容器大小时,该组将调整大小,从而使组件调整大小。
空布局管理器
到目前为止,您可能已经意识到布局管理器处理容器内组件的定位和大小调整。如果调整了容器的大小,布局管理器将负责重新定位和调整其中组件的大小。如果您不想拥有布局管理器,您将失去这一优势,并且您需要负责容器内所有组件的定位和大小调整。告诉容器你不需要布局管理器是很简单的。只需将布局管理器设置为null,就像这样:
// Do not use a layout manager for myContainer
myContainer.setLayout(null);
您可以将JFrame的内容窗格的布局管理器设置为null,如下所示:
JFrame frame = new JFrame("No Layout Manager Frame");
Container contentPane = frame.getContentPane();
contentPane.setLayout(null);
短语“空布局管理器”仅仅意味着没有布局管理器。它也被称为绝对定位。请注意,您的程序可能运行在不同的平台上。当组件在不同的平台上显示时,它们的大小可能不同,而你的null布局管理器不能解释这种不一致。当你使用一个null布局管理器时,确保你的组件足够大,可以在所有平台上正常显示。
清单 1-21 为JFrame的内容窗格使用了一个null布局管理器。它添加了两个按钮。它还使用setBounds()方法设置按钮和JFrame的位置和大小。图 1-56 显示了最终的JFrame。
清单 1-21。使用空布局管理器
// NullLayout.java
package com.jdojo.swing;
import javax.swing.JFrame;
import java.awt.Container;
import javax.swing.JButton;
public class NullLayout {
public static void main(String[] args) {
JFrame frame = new JFrame("Null Layout Manager");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
Container contentPane = frame.getContentPane();
contentPane.setLayout(null);
JButton b1 = new JButton("Small Button 1");
JButton b2 = new JButton("Big Big Big Button 2...");
contentPane.add(b1);
contentPane.add(b2);
// Must set (x, y) and (width, height) of components
b1.setBounds(10, 10, 100, 20);
b2.setBounds(120, 10, 150, 20);
// Must set the size of JFrame, because it uses a null layout.
// Now, you cannot use the pack() method to compute its size.
frame.setBounds(0, 0, 350, 100);
frame.setVisible(true);
}
}

图 1-56。
A JFrame using a null layout manager
请注意,按钮的标签没有完全显示。这是你在使用null布局管理器时会遇到的问题之一。如果您试图在运行时调整JFrame的大小,您会注意到按钮不会自动调整大小,如果您使用了布局管理器,它们会自动调整大小。布局管理器根据平台、文本和字体来计算JButton的大小,而使用null布局管理器,你应该考虑所有这些因素来计算(大多数时候,你只是猜测)按钮的大小。在 Java 中使用null布局管理器不是一个好的实践,除非你正在原型开发或者学习null布局管理器。
创建可重用的 JFrame
在前面的章节中,您通过实例化JFrame类创建了一个JFrame,并使用该类的main()方法编写构建 GUI 的代码。您示例中的JFrame是不可重用的。到目前为止,您做得很好,因为 Swing 程序很简单,它们的唯一目的是在一个JFrame中显示一些组件。当您开始编写更复杂的 Swing 程序时,这种编程方式不会很好地工作。例如,假设您想在JFrame显示后使JFrame中的JButton不可见或被禁用。因为您已经将所有的JButton声明为main()方法中的局部变量,所以一旦main()方法执行完毕,您将无法访问它们的引用。为了使您的JFrame可重用,并保持添加到JFrame的组件的引用方便,以便您可以在以后引用它们,您需要改变创建JFrame的方法。
这是你创造JFrame的新方法。您创建自己的类,从JFrame类继承它,如下所示:
public class CustomFrame extends JFrame {
// Code for CustomFrame goes here
}
所有组件都在自定义类中声明为实例变量,如下所示:
public class CustomFrame extends JFrame {
// Declare all components in the JFrame as instance variables
JButton okButton = new JButton("OK");
JButton cancelButton = new JButton("Cancel");
}
您有一个向JFrame的内容窗格添加组件的initFrame()方法。您从自定义的构造函数JFrame中调用这个方法。Java 不需要方法initFrame()。这只是为 Swing 应用编写代码的惯例。为了显示您的JFrame,您实例化您的类并使其可见。这种方法有相似的代码,但排列方式不同,因此您可以编写一些更严肃的 Swing 程序。清单 1-22 完成了与清单 1-19 相同的事情。
清单 1-22。创建自定义 JFrame
// CustomFrame.java
package com.jdojo.swing;
import javax.swing.JFrame;
import javax.swing.GroupLayout.Alignment;
import javax.swing.JButton;
import java.awt.Container;
import javax.swing.GroupLayout;
public class CustomFrame extends JFrame {
// Declare all components as instance variables
JButton b1 = new JButton("Button 1");
JButton b2 = new JButton("Little Bigger Button 2");
public CustomFrame(String title) {
super(title);
initFrame();
}
// Initialize the frame and add components to it.
private void initFrame() {
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
Container contentPane = this.getContentPane();
GroupLayout groupLayout = new GroupLayout(contentPane);
contentPane.setLayout(groupLayout);
groupLayout.setHorizontalGroup(
groupLayout.createSequentialGroup()
.addComponent(b1)
.addComponent(b2)
);
groupLayout.setVerticalGroup(
groupLayout.createParallelGroup(Alignment.BASELINE)
.addComponent(b1)
.addComponent(b2)
);
}
// Display the CustomFrame
public static void main(String[] args) {
CustomFrame frame = new CustomFrame("Custom Frame");
frame.pack();
frame.setVisible(true);
}
}
事件处理
什么是事件?事件的字面意思是
"某事物在特定时间点的发生."
Swing 应用中事件的含义是相似的。Swing 中的事件是用户在特定时间点采取的动作。例如,在 Swing 应用中,按下按钮、按下键盘上的向下/向上键以及将鼠标移动到组件上方都是事件。有时,在 Swing(或任何基于 GUI 的应用)中发生的事件也被称为“触发事件”或“激发事件”当您说按钮上发生了单击事件时,您是指使用鼠标、空格键或应用允许您按下按钮的任何其他方式按下了按钮。有时你可以使用短语“点击事件已经在按钮上被触发或激发”来表示按钮已经被按下。
当事件发生时,您希望对事件做出响应。在程序中采取一个动作只不过是执行一段代码。为响应事件的发生而采取的行动称为事件处理。事件发生时执行的代码称为事件处理程序。有时,事件处理程序也称为事件侦听器。
如何编写事件处理程序取决于事件的类型和生成事件的组件。有时事件处理程序内置在 Swing 组件中,有时您需要自己编写事件处理程序。比如,当你按下一个JButton时,你需要自己编写事件处理程序。但是,当焦点在文本字段中时,如果按下键盘上的字母键,就会在文本字段中键入相应的字母,因为按键事件有一个由 Swing 提供的默认事件处理程序。
一个事件有三个参与者:
- 事件的来源
- 事件
- 事件处理程序(或事件监听器)
事件源是生成事件的组件。例如,当你按下一个JButton时,点击的事件发生在那个JButton上。在这种情况下,JButton是被点击事件的来源。
事件表示在源组件上发生的动作。Swing 中的事件由一个对象表示,该对象封装了事件的详细信息,例如事件的来源、事件发生的时间、事件发生的类型等。表示事件的对象的类是什么?这取决于所发生事件的类型。每种类型的事件都有一个类。例如,java.awt.event包中的ActionEvent类的一个对象代表一个JButton的点击事件。
我不会在本章讨论所有类型的事件。当我在第二章中讨论组件时,我将列出组件的重要事件。本节将解释如何在 Swing 应用中处理任何类型的事件。
事件处理程序是事件发生时执行的一段代码。像事件一样,事件处理程序也由对象表示,它封装了事件处理代码。哪个类的对象代表一个事件处理程序?这取决于事件处理程序应该处理的事件类型。事件处理程序也称为事件侦听器,因为它侦听源组件中发生的事件。在本章中,我将交替使用“事件处理程序”和“事件监听器”这两个短语。通常,事件侦听器是实现特定接口的对象。事件侦听器必须实现的特定接口取决于它将侦听的事件类型。例如,如果您对监听一个JButton的点击事件感兴趣(换句话说,如果您对处理一个JButton的点击事件感兴趣),您需要一个实现ActionListener接口的类的对象,它在java.awt.event包中。
查看事件处理的三个参与者的描述,似乎您需要编写大量代码来处理一个事件。不完全是。事件处理比看起来容易。我将列出处理一个事件的步骤,然后是一个如何处理一个JButton的点击事件的例子。以下是处理事件的步骤。这些步骤适用于处理任何 Swing 组件上的任何类型的事件。
- 标识要为其处理事件的组件。假设您已经将组件命名为
sourceComponent。所以你的事件源是sourceComponent。 - 标识要为源组件处理的事件。假设您对处理
Xxx事件感兴趣。这里的Xxx是一个事件名,您必须用源组件的事件名来替换它。回想一下,一个事件由一个对象表示。事件类的 Java 命名约定可以帮助您识别对象代表Xxx事件的类的名称。对象代表Xxx事件的类被命名为XxxEvent。通常事件类在java.awt.event和javax.swing.event包中。 - 是时候为
Xxx事件编写一个事件监听器了。回想一下,事件侦听器只不过是实现特定接口的类的对象。你怎么知道你需要在你的事件监听器类中实现什么特定的接口呢?在这里,Java 命名约定再次拯救了您。对于Xxx事件,您需要在事件监听器类中实现一个XxxListener接口。通常事件监听器接口在java.awt.event和javax.swing.event包中。XxxListener接口将有一个或多个方法。所有用于XxxListener的方法都接受一个类型为XxxEvent的参数,因为这些方法旨在处理一个XxxEvent。例如,假设您有一个XxxListener接口,它有一个名为aMethod()的方法,如public interface XxxListener {void aMethod(XxxEvent event);}所示,您的事件监听器类将如下所示。请注意,您将创建这个类。public class MyXxxEventListener implements XxxListener {public void aMethod(XxxEvent event) {// Your event handler code goes here}} - 你差不多完成了。您已经确定了事件源、感兴趣的事件和事件侦听器。只有一样东西不见了。您需要让事件源知道您的事件监听器有兴趣监听它的
Xxx事件。这也称为向事件源注册事件侦听器。向事件源注册事件侦听器类的对象。在您的例子中,您将创建一个MyXxxEventListener类的对象。MyXxxEventListener myXxxListener = new MyXxxEventListener();如何向事件源注册一个事件监听器?在这里,Java 命名约定再次派上了用场。如果一个组件(事件源)支持一个Xxx事件,它将有两个方法,addXxxListener(XxxListener l)和removeXxxListener(XxxListener l)。当您对组件的Xxx事件感兴趣时,您调用addXxxListener()方法,将事件侦听器作为参数传递。当你不想再监听组件的Xxx事件时,你调用它的removeXxxListener()方法。要添加您的myXxxListener对象作为sourceComponent的Xxx事件监听器,您需要编写sourceComponent.addXxxListener(myXxxListener);
这就是处理一个Xxx事件所需要做的一切。看起来你必须执行许多步骤来处理一个事件。然而,事实并非如此。你总是可以避免编写一个新的事件监听器类,它通过使用一个匿名内部类来实现XxxListener接口,这个匿名内部类实现了XxxListener接口。例如,您可以用两条语句编写上述代码,如下所示:
// Create an event listener object using an anonymous inner class
XxxListener myXxxListener = new XxxListener() {
public void aMethod(XxxEvent event) {
// Your event handler code goes here
}
};
// Add the event listener to the event source component
sourceComponent.addXxxListener(myXxxListener);
如果侦听器接口是一个函数接口,您可以使用 lambda 表达式来创建它的实例。您的XxxListener是一个函数接口,因为它只包含一个抽象方法。您可以避免创建庞大的匿名类,并将上面的代码重写如下:
// Add the event listener using a lambda expressions
sourceComponent.addXxxListener((XxxEvent event) -> {
// Your event handler code goes here
});
关于处理事件的理论,我已经讨论的够多了。是时候看一个例子了。向一个JButton添加一个事件监听器,然后向一个JFrame添加一个带有文本Close的JButton。当按下JButton时,JFrame关闭,应用退出。当按下JButton按钮时,会产生一个Action事件。一旦您知道了事件的名称,在本例中是Action,您只需要用单词Action替换前面通用示例中的Xxx。您将会知道您需要用来处理JButton的Action事件的类名和方法名。表 1-6 比较了用于处理JButton的Action事件的类/接口/方法的名称和我在讨论中使用的通用名称。
表 1-6。
A Comparison Between Generic Event Handlers With Action Event Handlers for a JButton
| 通用事件 Xxx | JButton 的操作事件 | 评论 | | --- | --- | --- | | `XxxEvent` | `ActionEvent` | `java.awt.event`包中`ActionEvent`类的一个对象代表`JButton`的`Action`事件。 | | `XxxListener` | `ActionListener` | 实现`ActionListener`接口的类的对象代表了`JButton`的`Action`事件处理程序。 | | `addXxxListener` `(XxxListener l)` | `addActionListener` `(ActionListener l)` | 一个`JButton`的`addActionListener()`方法用于为它的`Action`事件添加一个监听器。 | | `removeXxxListener` `(XxxListener l)` | `removeActionListener` `(ActionListener l)` | `JButton`的`removeActionListener()`方法用于移除其`Action`事件的监听器。 |ActionListener界面很简单。它有一个叫做actionPerformed()的方法。接口声明如下:
public interface ActionListener extends EventListener {
void actionPerformed(ActionEvent event);
}
所有事件监听器接口都继承自EventListener接口,该接口在java.util包中。EventListener接口是一个标记接口,它没有任何方法。它只是充当所有事件侦听器接口的祖先。当一个JButton被按下时,它所有注册的Action监听器的actionPerformed()方法被调用。
使用 lambda 表达式,下面是如何将一个Action侦听器添加到一个JButton中:
// Add an ActionListener to closeButton
closeButton.addActionListener(e -> System.exit(0));
清单 1-23 显示了一个包含一个JButton的JFrame。它向JButton添加了一个Action监听器。Action监听器简单地退出应用。点击JFrame中的Close按钮将关闭应用。
清单 1-23。带有带动作的关闭按钮的 JFrame
// SimplestEventHandlingFrame.java
package com.jdojo.swing;
import java.awt.FlowLayout;
import javax.swing.JFrame;
import javax.swing.JButton;
public class SimplestEventHandlingFrame extends JFrame {
JButton closeButton = new JButton("Close");
public SimplestEventHandlingFrame() {
super("Simplest Event Handling JFrame");
this.initFrame();
}
private void initFrame() {
this.setDefaultCloseOperation(EXIT_ON_CLOSE);
// Set a FlowLayout for the content pane
this.setLayout(new FlowLayout());
// Add the Close JButton to the content pane
this.getContentPane().add(closeButton);
// Add an ActionListener to closeButton
closeButton.addActionListener(e -> System.exit(0));
}
public static void main(String[] args) {
SimplestEventHandlingFrame frame =
new SimplestEventHandlingFrame();
frame.pack();
frame.setVisible(true);
}
}
让我们再举一个将Action监听器添加到JButton的例子。这次,给一个JFrame添加两个按钮:一个Close按钮和另一个显示点击次数的按钮。每次单击第二个按钮时,它的文本都会更新,以显示它被单击的次数。您需要使用一个实例变量来维护点击计数。清单 1-24 包含了完整的代码。图 1-57 显示计数器按钮被点击三次后的JFrame。
清单 1-24。带有两个带动作的按钮的 JFrame
// JButtonClickedCounter.java
package com.jdojo.swing;
import javax.swing.JFrame;
import java.awt.FlowLayout;
import java.awt.event.ActionEvent;
import javax.swing.JButton;
import java.awt.event.ActionListener;
public class JButtonClickedCounter extends JFrame {
int counter;
JButton counterButton = new JButton("Clicked #0");
JButton closeButton = new JButton("Close");
public JButtonClickedCounter() {
super("JButton Clicked Counter");
this.initFrame();
}
private void initFrame() {
this.setDefaultCloseOperation(EXIT_ON_CLOSE);
// Set a FlowLayout for the content pane
this.setLayout(new FlowLayout());
// Add two JButtons to the content pane
this.getContentPane().add(counterButton);
this.getContentPane().add(closeButton);
// Add an ActionListener to the counter JButton
counterButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
// Increment the counter and set the JButton text
counter++;
counterButton.setText("Clicked #" + counter);
}
});
// Add an ActionListener to closeButton
closeButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
// Exit the application, when this button is pressed
System.exit(0);
}
});
}
public static void main(String[] args) {
JButtonClickedCounter frame = new JButtonClickedCounter();
frame.pack();
frame.setVisible(true);
}
}

图 1-57。
A JFrame when it is displayed and after the counter JButton is clicked three times
图 1-58 显示了处理Action事件所涉及的类和接口的类图。

图 1-58。
A class diagram for classes and interfaces realted to Action Event
注意,您没有创建一个ActionEvent类的对象。当按下JButton时,创建一个ActionEvent类的对象,并将其传递给事件处理程序对象的actionPerformed()方法。默认情况下,ActionEvent的getActionCommand()方法返回JButton的文本。您可以使用setActionCommand()方法为JButton显式设置动作命令文本。getModifiers()返回动作事件期间按住的Shift、Ctrl、Alt等修改键的状态。修饰键是键盘上的一个键,只有在与其他键结合使用时才有意义。paramString()方法返回一个描述动作事件的字符串。它通常用于调试目的。
getActionCommand()方法的用途之一是根据JButton上显示的文本采取一些行动。例如,您可能有一个JButton,用于显示或隐藏屏幕上的一些细节。假设您想将一个JButton的文本显示为Show或Hide。您可以如下编写它的Action监听器:
JButton showHideButton = new JButton("Hide");
showHideButton.addActionListener(e -> {
if (e.getActionCommand().equals("Show")) {
// Show the details here...
showHideButton.setText("Hide");
}
else {
// Hide the details here...
showHideButton.setText("Show");
}});
在本节中,您学习了如何为组件添加事件处理程序。例子很简单。他们给JButton s 添加了动作事件处理程序。ActionListener接口是一个函数接口,你可以利用 lambda 表达式来编写动作事件监听器。Swing 是在 lambda 表达式出现之前很久开发的。所有事件侦听器接口都不是函数接口,因此您不能使用 lambda 表达式来创建它们的对象。在这些情况下,可以使用匿名类、成员内部类,或者在主类中实现侦听器接口。
处理鼠标事件
您可以处理组件上的鼠标活动(单击、进入、退出、按下和释放)。您将使用一个JButton来试验鼠标事件。一个MouseEvent类的对象代表一个组件上的Mouse事件。现在,您可以猜测,要处理Mouse事件,您将需要使用MouseListener接口。下面是该接口的声明方式:
public interface MouseListener extends EventListener {
public void mouseClicked(MouseEvent e);
public void mousePressed(MouseEvent e);
public void mouseReleased(MouseEvent e);
public void mouseEntered(MouseEvent e);
public void mouseExited(MouseEvent e);
}
MouseListener接口有五个方法。不能使用 lambda 表达式创建鼠标事件处理程序。当特定的鼠标事件发生时,调用MouseListener接口的方法之一。例如,当鼠标指针进入组件的边界时,组件上发生鼠标输入事件,并调用鼠标监听器对象的mouseEntered()方法。当鼠标指针离开组件边界时,会发生鼠标退出事件,并调用mouseExited()方法。其他方法的名称不言自明。
MouseEvent类有许多提供鼠标事件细节的方法:
- 方法返回鼠标点击的次数。
- 当事件发生时,
getX()和getY()方法返回鼠标相对于组件的 x 和 y 位置。 getXOnScreen()和getYOnScreen()方法返回事件发生时鼠标的绝对 x 和 y 位置。
假设您对处理JButton的两种鼠标事件感兴趣:鼠标进入和鼠标退出事件。JButton的文本发生变化以描述事件。鼠标事件处理程序代码如下:
mouseButton.addMouseListener(new MouseListener() {
@Override
public void mouseClicked(MouseEvent e) {
// Nothing to handle
}
@Override
public void mousePressed(MouseEvent e) {
// Nothing to handle
}
@Override
public void mouseReleased(MouseEvent e) {
// Nothing to handle
}
@Override
public void mouseEntered(MouseEvent e) {
mouseButton.setText("Mouse has entered!");
}
@Override
public void mouseExited(MouseEvent e) {
mouseButton.setText("Mouse has exited!");
}
});
在这段代码中,您为MouseListener接口的所有五个方法提供了一个实现,尽管您只对处理两种鼠标事件感兴趣。您将三个方法的主体留空。
清单 1-25 展示了一个JButton的鼠标进入和退出事件。当显示JFrame时,尝试将鼠标移进和移出JButton的边界,以改变其文本来指示适当的鼠标事件。
清单 1-25。处理鼠标事件
// HandlingMouseEvent.java
package com.jdojo.swing;
import java.awt.FlowLayout;
import javax.swing.JFrame;
import javax.swing.JButton;
import java.awt.event.MouseListener;
import java.awt.event.MouseEvent;
public class HandlingMouseEvent extends JFrame {
JButton mouseButton = new JButton("No Mouse Movement Yet!");
public HandlingMouseEvent() {
super("Handling Mouse Event");
this.initFrame();
}
private void initFrame() {
this.setDefaultCloseOperation(EXIT_ON_CLOSE);
this.setLayout(new FlowLayout());
this.getContentPane().add(mouseButton);
// Add a MouseListener to the JButton
mouseButton.addMouseListener(new MouseListener() {
@Override
public void mouseClicked(MouseEvent e) {
}
@Override
public void mousePressed(MouseEvent e) {
}
@Override
public void mouseReleased(MouseEvent e) {
}
@Override
public void mouseEntered(MouseEvent e) {
mouseButton.setText("Mouse has entered!");
}
@Override
public void mouseExited(MouseEvent e) {
mouseButton.setText("Mouse has exited!");
}
});
}
public static void main(String[] args) {
HandlingMouseEvent frame = new HandlingMouseEvent();
frame.pack();
frame.setVisible(true);
}
}
您是否总是必须为事件侦听器接口的所有事件处理方法提供实现,即使您对它们都不感兴趣?不,你没有。秋千的设计者考虑到了这种不便,并设计了一种方法来避免这种情况。Swing 为一些XxxListener接口提供了一个方便的类。这个类被命名为XxxAdapter。我将称它们为适配器类。一个XxxAdapter类被声明为抽象的,它实现了XxxListener接口。XxxAdapter类为XxxListener接口中的所有方法提供了空实现。下面的代码片段显示了具有两个方法m1()和m2()的XxxListener接口与其对应的XxxAdapter类之间的关系。
public interface XxxListener {
public void m1();
public void m2();
}
public abstract class XxxAdapter implements XxxListener {
@Override
public void m1() {
// No implementation provided here
}
@Override
public void m2() {
// No implementation provided here
}
}
并非所有事件侦听器接口都有相应的适配器类。声明多个方法的事件侦听器接口有一个对应的适配器类。例如,有一个名为MouseAdapter的用于MouseListener接口的适配器类。MouseAdapter对你有什么好处?它可以为您节省几行不必要的代码。如果您只想处理一些鼠标事件,您可以创建一个匿名内部类(或常规内部类),它继承自适配器类并覆盖您感兴趣的唯一方法。下面的代码片段使用MouseAdapter类重写了清单 1-28 中使用的事件处理程序:
mouseButton.addMouseListener(new MouseAdapter() {
@Override
public void mouseEntered(MouseEvent e) {
mouseButton.setText("Mouse has entered!");
}
@Override
public void mouseExited(MouseEvent e) {
mouseButton.setText("Mouse has exited!");
}
});
您可能会注意到,您不必担心MouseListener接口的其他三个方法,因为MouseAdapter类为您提供了空的实现。
对于ActionListener接口,没有名为ActionAdapter的适配器类。你能猜到为什么没有ActionAdapter课吗?由于ActionListener接口只有一个方法,提供一个适配器类不会为您节省任何击键。
请注意,使用适配器类来处理事件并没有什么特别的优势,除了节省一些击键。然而,它也有局限性。如果您希望通过使用主类本身来创建事件处理程序,则不能使用适配器类。通常,您的主类是从JFrame类继承的,Java 不允许您从多个类继承一个类。所以您不能从JFrame类继承您的主类以及适配器类。如果使用适配器类创建事件处理程序,则必须使用匿名内部类或常规内部类。
摘要
Swing 是一个使用 GUI 开发 Java 应用的小部件工具包。开发 Swing 应用中使用的大多数类都在javax.swing包中。GUI 由几个部分组成;每个部分代表一个图形,向用户显示信息,并让他们与应用进行交互。基于 Swing 的 GUI 应用中的每个部分都被称为一个组件,它是一个 Java 对象。可以包含其他组件的组件称为容器。容器和组件排列成父子层次结构。组件包含在一个容器中,该容器又可以包含在另一个容器中。存在两种类型的容器:顶级容器和非顶级容器。顶级容器不包含在另一个容器中,它可以直接显示在桌面上。例如,JFrame类的一个实例代表一个顶级容器,它是一个可以有标题栏、菜单栏、边框和其他组件的窗口。JButton类的一个实例代表一个组件。
顶级容器由许多层组成,如根窗格、分层窗格、玻璃窗格和内容窗格。组件被添加到内容窗格中。
Swing 提供了布局管理器,负责在容器中布局组件。布局管理器是一个负责确定要在容器中显示的组件的位置和大小的对象。每个容器都有一个默认的布局管理器。例如,BorderLayout是JFrame的默认布局管理器。你可以使用容器的setLayout()方法来设置不同的布局管理器。如果组件的布局管理器设置为null,则不使用布局管理器,您负责在容器中布局组件。
是所有布局管理器中最简单的,它先水平布局组件,然后垂直布局。BorderLayout将容器的空间分成五个区域(北、南、东、西、中),用于布局组件。CardLayout将容器中的组件排列成一叠卡片,一次只能看到一个组件。BoxLayout将组件水平排列成一行或垂直排列成一列。GridLayout将组件排列成大小相等的矩形网格,将每个组件放在一个单元格中。GridBagLayout在按行和列排列的矩形单元格网格中布置组件,其中每个组件占据一个或多个单元格。SpringLayout通过定义组件边缘之间的约束来布局组件;约束是根据弹簧定义的。GroupLayout通过形成连续和平行的组件组来布局组件。
事件表示用户操作,例如用户点击按钮。用户通过事件与 Swing 组件进行交互。在程序中采取行动来响应事件被称为事件处理。一个事件有三个参与者:事件源、事件和事件处理程序。事件源是生成事件的组件。事件由一个对象表示,该对象封装了导致事件发生的用户操作的详细信息。事件处理程序是响应事件发生而执行的特定接口的实例。允许您处理事件的组件包含添加和移除事件处理程序的方法。事件处理中使用的类、接口和方法遵循一种命名约定,这种约定使名称易于记忆。
二、Swing 组件
在本章中,您将学习
- 什么是秋千组件
- 不同类型的秋千组件
- 如何验证文本组件中的输入
- 如何使用菜单和工具栏
- 如何使用 JTable 和 JTree 组件编辑表格和分层数据
- 如何使用自定义和标准对话框
- 如何自定义组件的属性,如颜色、边框、字体等。
- 如何绘制组件以及如何绘制形状
- 立即绘画和双缓冲
什么是 Swing 组件?
Swing 提供了大量组件来构建 GUI。在 Java 程序中,Swing 组件是类的一个实例。JComponent类在javax.swing包中,它是所有 Swing 组件的基类。其类层次如图 2-1 所示。

图 2-1。
The class hierarchy for the JComponent class
该类继承自java.awt.Container类,而后者又继承自java.awt.Component类。JComponent是一个抽象类。您不能直接实例化它。你必须使用它的一个子类,比如JButton、JTextField等。
由于JComponent类继承自Container类,每个JComponent也可以作为一个容器。例如,一个JButton可以充当另一个JButton或其他JComponent的容器。除非 Swing 库已经提供了一个JComponent如JPanel作为容器使用,否则你不会使用(或需要)一个JComponent作为容器。但是,这种层次结构允许您编写如下代码:
JButton btn = new JButton("Container JButton");
btn.setLayout(new FlowLayout());
btn.add(new JButton("Container JButton. Do not use."));
作为所有 Swing 组件的基类,JComponent类提供了以下基本功能,这些功能由所有 Swing 组件继承。我将在本章后面详细讨论这些特性。
- 它为工具提示提供支持。工具提示是当鼠标指针在组件上停留一定时间时显示的简短文本。
- 它支持可插拔的外观。与组件外观(绘画和布局)和感觉(响应用户与组件的交互,如事件处理)相关的所有方面都由 UI delegate 对象处理。像
JComponent类一样,javax.swing.plaf包中的ComponentUI是用作 UI 委托对象的基类。JComponent的每个后代使用不同种类的 UI 委托对象,该对象是从ComponentUI类派生的。比如 aJButton使用ButtonUI,aJLabel使用LabelUI,aJToolTip使用ToolTipUI作为 UI 委托。 - 它支持在 Swing 组件周围添加边框。边界可以是任何一种预定义的类型(
Line、Bevel、Titled、Etched等)。)或自定义边框类型。 - 它为可访问性提供支持。应用的可访问性是指不同能力和残疾的人可以使用它的程度。例如,它可以为视力受损的用户以更大的字体显示文本。这本书不包括 Java 可访问性 API。
- 它支持双缓冲,有助于屏幕上的平滑绘画。当在屏幕上擦除和绘制组件时,可能会出现闪烁。为了避免任何闪烁,它提供了一个离屏缓冲区。擦除和重画(更新组件)在离屏缓冲区中完成,离屏缓冲区被复制到屏幕上。
- 它将键盘上的一个键绑定到一个 Swing 组件。您可以用一个
ActionListener对象将键盘上的任何键绑定到一个组件。当那个键被按下时,关联的ActionListener的actionPerformed()方法被调用。 - 当使用布局管理器时,它为布局组件提供支持。它包含获取和设置组件的最小、首选和最大大小的方法。一个
JComponent的三种不同的字体大小设置为布局管理器决定JComponent的大小提供了一个提示。
它允许将多个任意属性(key-value对)关联到一个 Swing 组件,并检索这些属性。JComponent 的putClientProperty()和getClientProperty()方法允许处理组件属性。
表 2-1 列出了JComponent类的一些常用方法,这些方法可用于所有 Swing 组件。
表 2-1。
Commonly Used Methods of the JComponent Class and Their Descriptions
| 方法名 | 描述 | | --- | --- | | `Border getBorder()` | 返回组件的边框,如果组件没有边框,则返回`null`。 | | `void setBorder(Border border)` | 设置组件的边框。 | | `Object getClientProperty(Object key)` | 返回与指定键关联的值。该值必须使用`putClientProperty (Object key, Object value)`方法设置。 | | `void putClientProperty(Object key, Object value)` | 向组件添加任意键-值对。 | | `Graphics getGraphics()` | 返回组件的图形上下文对象,该对象可用于在组件上绘图。 | | `Dimension getMaximumSize()``Dimension getMinimumSize()``Dimension getPreferredSize()``Dimension getSize(Dimension d)``void setMaximumSize(Dimension d)``void setMinimumSize(Dimension d)``void setPreferredSize(Dimension d)``void setSize(Dimension d)``void setSize(int width, int height)` | 获取/设置组件的最大、最小、首选和实际大小。当您调用`getSize()`方法时,您可以传递一个`Dimension`对象,大小将存储在其中,并返回相同的对象。这样,该方法可以避免创建新的`Dimension`对象。如果您传递了`null`,它将创建一个`Dimension`对象,在其中存储实际大小,并返回该对象。 | | `String getToolTipText()` | 返回此组件的工具提示文本。 | | `void setToolTipText(String text)` | 设置工具提示文本,当鼠标指针在组件上暂停一段指定的时间后,将显示该文本。 | | `boolean isDoubleBuffered()` | 如果组件使用双缓冲,则返回`true`。否则返回`false`。 | | `void setDoubleBuffered(boolean db)` | 设置组件是否应该使用双缓冲来绘制。 | | `boolean isFocusable()` | 如果组件可以获得焦点,则返回`true`。否则返回`false`。 | | `void setFocusable(boolean focusable)` | 设置组件是否可以获得焦点。 | | `boolean isVisible()` | 如果组件可见,则返回`true`。否则返回`false`。 | | `void setVisible(boolean v)` | 将组件设置为可见或不可见。 | | `boolean isEnabled()` | 如果组件被启用,则返回`true`。否则返回`false`。 | | `void setEnabled(boolean e)` | 启用或禁用组件。默认情况下,组件处于启用状态。启用的组件响应用户输入并生成事件。 | | `boolean requestFocus(boolean temporary)``boolean requestFocusInWindow()` | `requestFocus()`和`requestFocusInWindow()`方法都要求组件获得输入焦点。您应该使用`requestFocusInWindow()`方法而不是`requestFocus()`方法,因为它的行为在所有平台上都是一致的。布尔参数指示请求是否是临时的。如果请求肯定会失败,这些方法将返回`false`。如果请求成功,除非被否决,否则它们返回`true`。 | | `boolean isOpaque()` | 如果`JComponent`不透明,则返回`true`。否则,它返回`false`。 | | `void setOpaque(boolean opaque)` | 设置`JComponent`的不透明度。如果一个`JComponent`是不透明的,它将绘制其边界内的每个像素。如果它不是不透明的,它可能会在其边界内绘制一些像素或不绘制像素,从而允许其后面的像素显示出来。默认情况下,`JComponent`类将该值设置为`false`,使其透明。但是,其子类别的不透明度默认值取决于外观和感觉以及特定的组件。 |表 2-2 列出了可用于所有 Swing 组件的一些常用事件。每个 Swing 组件还支持一些专门的事件。当我讨论这些组件时,我将解释那些专门的事件。注意,表中列出的所有事件都遵循XxxEvent类、XxxListener接口、XxxAdapter抽象类和addXxxListener()方法命名约定,除非另有说明。也就是说,要处理组件的Xxx事件,需要调用它的addXxxListener(XxxListener l)方法,并传递实现XxxListener接口的类的对象。一个XxxListener接口中的所有方法都接受一个XxxEvent类型的参数。如果XxxListener中有不止一个方法,则有一个对应的XxxAdapter抽象类实现XxxListener接口,并为XxxListener方法提供空实现。
表 2-2。
Some Commonly Used Events Available for All Swing Components
| 事件类别名称 | 事件监听器接口 | 描述 | | --- | --- | --- | | `ComponentEvent` | `ComponentListener`方法:`componentShown()` `componentHidden()` `componentResized()` `componentMoved()` | 当组件的可见性、大小或位置更改时,会发生事件。 | | `FocusEvent` | `FocusListener`方法:`focusGained()` `focusLost()` | 当组件获得或失去焦点时,会发生事件。 | | `KeyEvent` | `KeyListener`方法:`keyPressed()` `keyReleased()` `keyTyped()` | 当组件获得焦点并且按下、释放或键入键盘上的某个键时,会发生事件。当您按下或释放键盘上的任何键时,都会触发按键按下和释放事件。仅当键入 Unicode 字符时,才会触发 key typed 事件。例如,当您在键盘上键入字符“a”时,按下一个键、键入一个键和释放一个键事件将按顺序触发。 | | `MouseEvent` | `MouseListener`方法:`mousePressed()` `mouseReleased()` `mouseClicked()` `mouseEntered()` `mouseExited()` | 当在组件上按下、释放和单击鼠标时,会触发鼠标按下、释放和单击事件。当鼠标进入组件的边界时,会触发鼠标进入事件。当鼠标离开组件边界时,触发鼠标退出事件。注意,`MouseAdapter`类实现了三个接口:`MouseListener`、`MouseMotionListener`和`MouseWheelListener`(参见下面的两个鼠标事件)。 | | `MouseEvent` | `MouseMotionListener`方法:`mouseDragged()` `mouseMoved()`注意:它在事件方法中使用一个`MouseEvent`对象作为参数。没有相应的`MouseMotionEvent`类。 | 当您通过按下鼠标按钮将鼠标拖动到组件上时,会触发鼠标拖动事件。即使鼠标离开组件,鼠标拖动事件也会继续触发,直到松开鼠标按钮。当您在组件上移动鼠标,但没有按下鼠标按钮时,会触发鼠标移动事件。您可以使用`MouseAdapter`或`MouseMotionAdapter`抽象类为该事件编写监听器对象。 | | `MouseWheelEvent` | `MouseWheelListener`方法:`mouseWheelMoved()` | 当组件处于焦点时,如果旋转鼠标滚轮,则会触发鼠标滚轮移动事件。如果鼠标没有滚轮,则不会触发此事件。 |一开始,Java 提供了 AWT(抽象窗口工具)来构建 GUI。所有 AWT 组件都在java.awt包中,它们使用对等体来处理它们的工作方式。如果使用 AWT 创建按钮,操作系统会创建一个相应的按钮,称为,用于处理 AWT 按钮的大部分工作方式。因为每个 AWT 组件都有一个对等体,所以 AWT 组件被称为组件。
在 JDK 1.2 中,Swing 作为 AWT 的替代成为 Java 类库的一部分。大多数 Swing 组件不使用对等体,因此,它们被称为组件。对于每个 AWT 组件,您都会找到相应的 Swing 组件。Swing 提供了一些 AWT 中没有的附加组件,比如JTabbedPane。Swing 组件的名称带有前缀J。例如,为了使用按钮组件,AWT 提供了一个Button类,Swing 提供了一个JButton类。为了显示装饰窗口,AWT 提供了一个Frame类,Swing 提供了一个JFrame类。Swing 中有些组件还是重量级组件。毕竟,基本的 GUI 功能总是由操作系统提供的。Swing 中的所有顶层容器(JFrame、JDialog、JWindow、JApplet)都是重量级组件,它们都有对等体。除了顶级容器,Swing 组件是轻量级组件。Swing 的轻量级组件使用它们的重量级容器区域进行绘制。Swing 的轻量级组件是用 Java 编写的。
AWT 的主要缺点是 GUI 在不同的操作系统上可能看起来不同。AWT 支持在所有平台上都可用的特性。由于对操作系统对等体的依赖,AWT 只能提供矩形组件。Swing 轻量级组件不存在这些限制。在 Swing 中,您可以拥有任何形状的组件,因为 Swing 使用 Java 代码绘制轻量级组件。Swing 提供了可插拔的外观和感觉,因此您不会局限于只看到操作系统绘制的 GUI 组件。虽然允许在同一个应用中混合使用 Swing 和 AWT 组件,但这是不可取的。混合使用它们可能会导致难以调试的问题。这本书只涉及秋千。
在接下来的部分中,我将详细讨论几个 Swing 组件。
JButton
JButton也称为按钮或命令按钮。用户按下或点击一个JButton来执行一个动作。通常,它会显示描述单击时所执行操作的文本。文本也称为标签。一个JButton也支持显示图标。你可以使用表 2-3 中列出的一个构造函数来创建一个。
表 2-3。
Constructors of the JButton Class
| 构造器 | 描述 | | --- | --- | | `JButton()` | 创建一个没有任何标签或图标的`JButton`。 | | `JButton(String text)` | 创建一个`JButton`并将指定的文本设置为其标签。 | | `JButton(Icon icon)` | 创建一个带有图标但没有标签的`JButton`。 | | `JButton(String text, Icon icon)` | 用指定的标签和图标创建一个`JButton`。 | | `JButton(Action action)` | 用一个`Action`对象创建一个`JButton`。在本节的后面,您将会看到一个使用`Action`对象作为`JButton`的例子。 |您可以创建一个文本为Close的JButton,如下所示:
JButton closeButton = new JButton("Close");
要创建带有图标的JButton,您需要一个图像文件。图标是固定大小的图像。实现javax.swing.Icon接口的类的对象代表一个图标。Swing 提供了一个非常有用的ImageIcon类,它实现了Icon接口。您可以在程序中使用ImageIcon类从图像文件或包含 GIF、JPEG 或 PNG 图像的 URL 创建图标。以下代码片段显示了如何创建带有图标的按钮:
// Create icons
Icon previousIcon = new ImageIcon("C:/img/previous.gif");
Icon nextIcon = new ImageIcon("C:/img/next.gif");
// Create buttons with icons
JButton previousButton = new JButton("Previous", previousIcon);
JButton nextButton = new JButton("Next", nextIcon);
建议您在ImageIcon类的构造函数中的文件路径中使用正斜杠(/)。您指定的文件路径被转换为 URL,并且正斜杠在所有平台上都有效。这个文件路径示例(C:/img/next.gif)是针对 Windows 平台的。图 2-2 显示了一个带有三个按钮的JFrame。两个按钮有图标,一个只有文本。

图 2-2。
Buttons with an icon and text, and with only text
对于一个JButton只有一个事件,你将在你的 Java 程序中使用大部分时间。它被称为ActionEvent。点击JButton时触发。该接口是一个函数接口,它只包含一个名为actionPerformed(ActionEvent e)的方法。你可以用一个 lambda 表达式来表示一个ActionListener。下面是如何使用 lambda 表达式为ActionEvent和closeButton添加代码:
closeButton.addActionListener(() -> {
// The code to handle the action event goes here
});
支持键盘助记键,也称为或。如果焦点在包含JButton的窗口中,按下该键会激活JButton。助记键通常与修饰键(如Alt键)一起按下。修饰键是平台相关的;但是,通常是一个Alt键。例如,假设您将 C 键设置为Close JButton的助记键。当您按下Alt + C时,Close JButton被点击。如果在JButton文本中找到由助记键表示的字符,其第一次出现时会加下划线。
以下代码片段将 C 设置为Close JButton的助记键:
// Set the 'C' key as mnemonic key for closeButton
closeButton.setMnemonic('C');
// You can also use the following code to set a mnemonic key.
// The KeyEvent class is in the java.awt.event package.
closeButton.setMnemonic(KeyEvent.VK_C);
该代码显示了设置助记键的两种方法。当您不使用字符键作为助记键时,可以使用第二种方法。例如,如果您想将F3键设置为助记键,您可以使用第二种方法使用KeyEvent.VK_F3常量。图 2-3 显示了Close按钮,其中文本的第一个字符带有下划线。当您按下Alt + C时,Close JButton被激活(就像您用鼠标点击它一样)。表 2-4 显示了类中常用的方法。
表 2-4。
Commonly Used Methods of the JButton Class
| 方法 | 描述 | | --- | --- | | `Action getAction()` | 返回与`JButton`相关联的`Action`对象。 | | `void setAction(Action a)` | 为`JButton`设置一个`Action`对象。当这个方法被调用时,`JButton`的所有属性都从指定的`Action`对象中刷新。如果已经设置了一个`Action`对象,新的将替换旧的。新的`Action`对象被注册为`ActionListener`。使用`addActionListener()`方法向`JButton`注册的任何其他`ActionListener`保持注册状态。 | | `Icon getIcon()` | 返回与`JButton`相关联的`Icon`对象。 | | `void setIcon(Icon icon)` | 为`JButton`设置图标。 | | `int getMnemonic()` | 返回此`JButton`的键盘助记符。 | | `void setMnemonic(int n)` `void setMnemonic(char c)` | 设置`JButton`的键盘助记符。 | | `String getText()` | 返回`JButton`的文本。 | | `void setText()` | 设置`JButton`的文本。 |
图 2-3。
A Close button with C as its keyboard mnemonic
让我们用一个对象来创建一个JButton。到目前为止,您已经看到一个JButton只有四个常用属性:文本、图标、助记符和动作监听器。使用JButton的这些属性既简单又直接。使用一个Action物体如何帮助你对付一个JButton?让我们举一个例子,你有一个按钮,比如说Close,放在窗口的不同区域,比如说不同的标签页。如果按钮在一个窗口上放置四次,并且所有按钮的外观和行为都必须相同,那么一个Action对象将帮助你只为Close按钮编写一次代码,并多次使用它。
一个Action对象封装了按钮的状态和行为。您在一个Action对象中设置文本、图标、助记符、工具提示文本、其他属性和ActionListener,并使用同一个Action对象创建JButton的所有实例。这样做的一个明显好处是,如果您想要启用/禁用所有四个 JButtons,您不需要单独启用/禁用它们。相反,您在Action对象中设置enabled属性,它将启用/禁用所有这些属性。让我们将这种用法扩展到菜单项和工具栏。通常在窗口中提供菜单项、工具栏项和按钮来执行相同的操作。在这种情况下,您使用同一个Action对象来创建它们(一个菜单项、一个工具栏项和一个按钮)以保持它们的状态同步。现在你可以意识到Action对象的好处是重用代码和保持多个组件的状态同步。
Action是一个接口。该类为Action接口提供了默认实现。AbstractAction是一个抽象类。你需要从它继承你的类。清单 2-1 定义了一个CloseAction内部类,它继承自AbstractAction类。
清单 2-1。使用 Action 对象创建和配置 JButton
// ActionJButtonTest.java
package com.jdojo.swing;
import java.awt.FlowLayout;
import javax.swing.JFrame;
import javax.swing.JButton;
import java.awt.event.ActionEvent;
import javax.swing.AbstractAction;
import javax.swing.Action;
import java.awt.Container;
public class ActionJButtonTest extends JFrame {
// Inner Class starts here
public class CloseAction extends AbstractAction {
public CloseAction() {
super("Close");
}
@Override
public void actionPerformed(ActionEvent event) {
System.exit(0);
}
} // Inner Class ends here
JButton closeButton1;
JButton closeButton2;
Action closeAction = new CloseAction(); // See inner class above
public ActionJButtonTest() {
super("Using Action object with JButton");
this.initFrame();
}
private void initFrame() {
this.setDefaultCloseOperation(EXIT_ON_CLOSE);
this.setLayout(new FlowLayout());
Container contentPane = this.getContentPane();
// Use the same closeAction object to create both Close buttons
closeButton1 = new JButton(closeAction);
closeButton2 = new JButton(closeAction);
contentPane.add(closeButton1);
contentPane.add(closeButton2);
}
public static void main(String[] args) {
ActionJButtonTest frame = new ActionJButtonTest();
frame.pack();
frame.setVisible(true);
}
}
ActionJButtonTest类创建了一个Action对象,它的类型是CloseAction,并用它来创建两个按钮closeButton1和closeButton2。CloseAction类将文本设置为Close,在其方法中,它简单地退出应用。图 2-4 显示了运行程序时得到的JFrame。它显示了两个Close按钮。单击它们中的任何一个都会调用Action对象的actionPerformed()方法,这将退出应用。

图 2-4。
Two Close buttons created using the same Action object
如果您想在使用Action对象时为JButton设置任何属性,您可以通过使用Action接口的putValue(String key, Object value)方法来实现。例如,下面的代码片段为对象closeAction设置了工具提示文本和助记键:
// Set the tool tip text for the Action object
closeAction.putValue(Action.SHORT_DESCRIPTION, "Closes the application");
// Set the mneminic key for the Action object
closeAction.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_C);
Tip
如果您使用一个Action对象来配置一个JButton,然后直接更改JButton的属性,则更改后的属性将一直有效,直到您再次在Action对象中更改该属性。假设你已经用一个CloseAction对象创建了两个Close按钮。如果调用closeButton1.setText("Exit"),第一个按钮会将文本显示为Exit。如果调用closeAction.putValue(Action.NAME, "Close/Exit"),两个按钮都会显示文本为Close/Exit。
jpanel(jpanel)
一个JPanel是一个可以包含其他组件的容器。您可以设置其布局管理器、边框和背景颜色。通常,您使用一个JPanel来分组相关的组件,并将其添加到另一个容器中,比如添加到一个JFrame的内容窗格中。注意,JPanel是一个容器,但不是顶级容器,而JFrame是一个顶级容器。因此,您不能在 Swing 应用中单独显示一个JPanel,除非您将它添加到顶级容器中。有时,在两个组件之间插入一个JPanel来产生一个间隙。您也可以使用JPanel作为画布进行绘制,例如绘制直线、矩形、圆形等。
JPanel的默认布局管理器是。注意,JFrame的内容窗格的默认布局管理器是一个BorderLayout。您可以选择在JPanel类的构造函数中指定它的布局管理器。您可以在创建它之后通过使用它的setLayout()方法来改变它的布局管理器。表 2-5 列出了JPanel类的构造函数。
表 2-5。
Constructors for the JPanel Class
| 构造器 | 描述 | | --- | --- | | `JPanel()` | 用`FlowLayout`和双缓冲创建一个`JPanel`。 | | `JPanel(boolean isDoubleBuffered)` | 用`FlowLayout`和指定的双缓冲标志创建一个`JPanel`。 | | `JPanel(LayoutManager layout)` | 用指定的布局管理器和双缓冲创建一个`JPanel`。 | | `JPanel(LayoutManager layout, boolean isDoubleBuffered)` | 用指定的布局管理器和双缓冲标志创建一个`JPanel`。 |下面的代码片段展示了如何创建一个带有BorderLayout的JPanel并向其添加四个按钮。注意,按钮被添加到JPanel,后者又被添加到JFrame的内容窗格。您还可以将一个JPanel添加到另一个JPanel来创建嵌套的复杂组件布局。
// Create a JPanel and four buttons
JPanel buttonPanel = new JPanel(new BorderLayout());
JButton northButton = new JButton("North");
JButton southButton = new JButton("South");
JButton eastButton = new JButton("East");
JButton westButton = new JButton("west");
// Add buttons to the JPanel
buttonPanel.add(northButton, BorderLayout.NORTH);
buttonPanel.add(southButton, BorderLayout.SOUTH);
buttonPanel.add(eastButton, BorderLayout.EAST);
buttonPanel.add(westButton, BorderLayout.WEST);
// Add the buttonPanel to the JFrame's content pane assuming that
// the content's pane layout is set to a BorderLayout
contentPane.add(buttonPanel, BorderLayout.SOUTH);
JLabel
顾名思义,JLabel是一个标签,用于识别或描述屏幕上的另一个组件。它可以显示文本和/或图标。通常,JLabel被放置在它所描述的组件的旁边(右边或左边)或顶部。图 2-5 显示了一个文本设置为Name:的JLabel,这是一个指示符,用户应该在它旁边的字段中输入一个名字。

图 2-5。
A JLabel component with the text Name: and the mnemonic set to N
JLabel的另一个常见用途是显示图像。Swing 不包含像JImage这样的组件来显示图像。你需要使用一个带有Icon的JLabel来显示图像。表 2-6 列出了该类的构造函数。
表 2-6。
Constructors of the JLabel Class
| 构造器 | 描述 | | --- | --- | | `JLabel()` | 创建一个空字符串作为文本并且没有图标的`JLabel`。 | | `JLabel(Icon icon)` | 创建一个带有图标和空字符串作为文本的`JLabel`。 | | `JLabel(Icon icon, int horizontalAlignment)` | 创建一个带有图标和指定水平对齐方式的`JLabel`。一个`JLabel`垂直排列在其显示区域的中心。您可以将其显示区域中的水平对齐指定为`SwingConstants`类中定义的以下常量之一:`LEFT`、`CENTER`、`RIGHT`、`LEADING`或`TRAILING`。 | | `JLabel(String text)` | 用指定的`text`创建一个`JLabel`。这是最常用的构造函数。它垂直居中对齐,并在其显示区域内与前缘水平对齐。前缘由组件的方向决定。 | | `JLabel(String text, Icon icon, int horizontalAlignment)` | 用指定的`text`、`icon`和水平对齐创建一个`JLabel`。 | | `JLabel(String text, int horizontalAlignment)` | 用指定的`text`和水平对齐创建一个`JLabel`。 |下面的代码片段展示了一些如何创建JLabel的例子:
// Create a JLabel with a Name: text
JLabel nameLabel = new JLabel("Name:");
// Display an image warning.gif in a JLabel
JLabel warningImage = new JLabel(new Icon("C:/img/warning.gif"));
一个JLabel不会产生任何有趣的事件。但是,它有一些有用的方法,您可以用来定制它。你会非常频繁地使用其中的三种方法:setText()、setDisplayedMnemonic()和setLabelFor()。setText()方法用于设置。setDisplayedMnemonic()方法用于设置JLabel的键盘助记符。如果键盘助记符是出现在JLabel文本中的字符,该字符会加下划线以提示用户。setLabelFor()方法接受对另一个组件的引用,并指出这个JLabel描述了那个组件。两种方法- setDisplayedMnemonic()和setLabelFor()协同工作。当按下JLabel的助记键时,焦点被设置到在setLabelFor()方法中使用的组件。图 2-5 所示的JLabel的助记符设置为字符N,你可以看到文本中的字符N带有下划线。当用户按下Alt + N时,焦点将被设置到显示在JLabel右侧的JTextField上。以下代码片段显示了如何创建如图 2-5 所示的组件排列:
// Create a JTextField where the user can enter a name
JTextField nameTextField = new JTextField("Please enter your name...");
// Create a JLabel with N as its mnemonic and nameTextField as its label-for component
JLabel nameLabel = new JLabel("Name:");
nameLabel.setDisplayedMnemonic('N');
nameLabel.setLabelFor(nameTextField);
// Add name label and field to a container, say a contentPane
contentPane.add(nameLabel);
contentPane.add(nameTextField);
在JLabel类中还定义了其他方法,允许您设置/获取显示区域内的对齐方式和边界内的文本。如果你观察一个JLabel组件的特性,你会发现它的存在只是为了描述另一个组件——一个真正利他的组件!
文本组件
简单地说,您可以将文本定义为一系列字符。Swing 提供了一组丰富的功能来处理文本。图 2-6 显示了代表 Swing 中文本组件的类的类图。

图 2-6。
A class diagram for text-related components in Swing
Swing 提供了如此多与文本相关的特性,以至于它有一个单独的包,java x .swing.text,其中包含了所有与文本相关的类。JTextComponent级在javax.swing.text包里。其余的类都在javax.swing包里。
有不同的 Swing 组件来处理不同类型的文本。我们可以根据两个标准对文本组件进行分类:文本中的行数和它们可以处理的文本类型。根据文本组件可以处理的文本行数,您可以将它们进一步分类如下:
- 单行文本组件
- 多行文本组件
单行文本组件设计用于处理一行文本,例如用户名、密码、出生日期等。JTextField、JPasswordField和JFormattedTextField类的实例代表单行文本组件。
多行文本组件旨在处理多行文本,例如,注释、商店中某个商品的描述、文档等。JTextArea、JEditorPane和JTextPane类的实例代表多行文本组件。
根据文本组件可以处理的文本类型,可以对文本组件进行如下分类:
- 纯文本组件
- 样式文本组件
文本(或部分文本)的样式是文本显示的方式,如粗体、斜体、下划线等。、字体和颜色。在文本组件的上下文中,纯文本意味着文本组件中包含的整个文本只使用一种样式显示。JTextField、JPasswordField、JFormattedTextField和JTextArea是纯文本组件的例子。也就是说,您不能在一个JTextArea中显示多行文本,其中文本的某些部分是粗体,而其他部分不是。您可以用粗体显示JTextArea中的整个文本,也可以用普通字体显示整个文本。注意,纯文本并不意味着文本不能有样式。这意味着只有一种样式适用于整个文本(组成文本的所有字符)。
在样式文本中,您可以将不同的样式应用于文本的不同部分。在样式文本中,文本的某些部分可以是粗体(或斜体,更大的字体大小,下划线等。)和一些不是黑体的部分。JEditorPane和JTextPane是样式化组件的例子。
所有 Swing 组件,包括 Swing 文本组件,都基于模型-视图-控制器(MVC)模式。MVC 模式使用三个组件:模型、视图和控制器。模型负责存储内容(文本)。视图负责显示内容。控制器负责响应用户操作。Swing 将视图和控制器组合成一个名为 UI 的对象,负责显示内容并对用户的动作做出反应。它保持了模型的独立性,并由Document接口的一个实例来表示,该实例在javax.swing.text包中。文本组件的模型有时也被称为它的文档。图 2-7 描述了一个 Swing 文本组件的不同部分。

图 2-7。
Components of the model-view-controller pattern for Swing text components
请注意,视图可能不总是显示文本组件的全部内容。在图 2-7 中,模型包含了威廉·华兹华斯的一首诗的四行,而视图只显示了第一行的一些单词。
Swing 提供了一个默认的Document接口实现,这使得开发人员可以轻松处理常用的文本类型。当您使用文本组件时,它会为您创建一个合适的模型(有时我会在讨论中将其称为文档),该模型适合存储文本组件的内容。图 2-8 显示了Document接口的类图,以及相关的类和接口。图中显示的所有类和接口都在javax.swing.text包中。

图 2-8。
A class diagram for the document interface and related interfaces and classes
您可以使用setDocument(Document doc)方法为文本组件设置模型。getDocument()方法返回文本组件的模型。
默认情况下,JTextField、JPasswordField、JFormattedTextField和JTextArea使用PlainDocument类的一个实例作为它们的模型。如果您想要为这些文本组件定制模型,您需要创建一个从PlainDocument类继承的类,并覆盖一些方法。
JEditorPane和JTextPane的模型取决于正在编辑和/或显示的内容类型。文本组件中字符的位置使用从零开始的索引。即,文本中的第一个字符出现在索引 0 处。
文本组件
JTextComponent是一个abstract类。它是所有 Swing 文本组件的祖先。它包括所有文本组件都可用的通用功能。表 2-7 列出了JTextComponent类中包含的文本组件的一些常用方法。
表 2-7。
Commonly Used Methods in the JTextComponent Class
| 方法 | 描述 | | --- | --- | | `Keymap addKeymap(String name, Keymap parentKeymap)` | 将新的键映射添加到组件的键映射层次结构中。 | | `void copy()` | 将选定的文本复制到系统剪贴板。 | | `void cut()` | 将选定的文本移动到系统剪贴板。 | | `Action[] getActions()` | 返回文本编辑器的命令列表。 | | `Document getDocument()` | 返回文本组件的模型。 | | `Keymap getKeymap()` | 返回文本组件的当前活动键映射。 | | `static Keymap getKeymap (String keymapName)` | 返回与名为`keymapName`的文档相关联的键映射。 | | `String getSelectedText()` | 返回组件中选定的文本。如果没有选择的文本或者文档是空的,它返回`null`。 | | `int getSelectionEnd()` | 返回选定文本的结束位置。 | | `int getselectionStart()` | 返回选定文本的起始位置。 | | `String getText()` | 返回此文本组件中包含的文本。它返回组件模型中包含的文本,而不是视图显示的内容。 | | `String getText(int offset, int length) throws BadLocationException` | 返回文本组件中包含的一部分文本,从`offset`位置开始,字符数等于`length`。如果`offset`或`length`无效,则抛出`BadLocationException`。例如,如果一个文本组件包含`Hello`作为其文本,`getText(1,3)`将返回`ell`。 | | `TextUI getUI()` | 返回文本组件的用户界面工厂。 | | `boolean isEditable()` | 如果文本组件是可编辑的,则返回`true`。否则,返回`false`。 | | `void paste()` | 将系统剪贴板的内容传输到文本组件模型。如果在组件中选择了文本,则选定的文本将被替换。如果没有选择,内容将插入到当前位置之前。如果系统剪贴板是空的,它什么也不做。 | | `void print()` | 它显示一个打印对话框,让您打印不带页眉和页脚的文本组件的内容。此方法被重载。此方法的其他版本提供了更多打印文本组件内容的功能。 | | `void read(Reader source, Object description) throws IOException` | 将内容从`source`流读入文本组件,丢弃组件的旧内容。`description`是一个描述`source`流的对象。例如,要将文件`test.txt`的文本读入名为`ta`的`JTextArea`中,您可以编写`FileReader fr =` `new FileReader("test.txt");` `ta.read(fr, "Hello");` `fr.close();` | | `void replaceSelection(String newContent)` | 用`newContent`替换所选内容。如果没有选定的内容,它会插入`newContent`。如果`newContent`为`null`或空字符串,则删除选中的内容。 | | `void select(int start, int end)` | 选择`start`和`end`位置之间的文本。 | | `void selectAll()` | 选择文本组件中的所有文本 | | `void setDocument(Document doc)` | 为文本组件设置文档(即模型)。 | | `void setEditable(boolean editable)` | 如果`editable`为`true`,则将文本组件设置为可编辑。如果`editable`为`false`,则将文本组件设置为不可编辑。 | | `void setKeymap(Keymap keymap)` | 设置文本组件的键映射。 | | `void setSelectionEnd(int end)` | 设置选择的结束位置。 | | `void setSelectionStart(int start)` | 设置选择的开始位置。 | | `void setText(String newText)` | 设置文本组件的文本。 | | `void setUI(TextUI newUI)` | 为文本组件设置新的用户界面。 | | `void updateUI()` | 重新加载文本组件的可插入用户界面。 | | `void write(Writer output)` | 将文本组件的内容写入由`output`定义的流。例如,要将名为`ta`的`JTextArea`的文本写入名为`test.txt`的文件,您应该编写`FileWriter wr = new FileWriter("test.txt");` `ta.write(wr);` `wr.close();` |文本组件最常用的方法是getText()和setText(String text)。getText()方法将文本组件的内容作为String返回,setText(String text)方法设置参数中指定的文本组件的内容。
jtextfield(jtextfield)
一个JTextFiel d 可以处理(显示和/或编辑)一行纯文本。您可以使用构造函数以多种不同的方式创建一个JTextField。它的构造函数接受
- 一根绳子
- 列数
- 一个
Document物体
该字符串指定初始文本。列数指定了宽度。Document对象指定了模型。初始文本的默认值是null,列数为零,文档(或模型)是PlainDocument类的一个实例。
如果不指定列数,其宽度由初始文本决定。它的首选宽度将足以显示整个文本。如果您指定列数,其首选宽度将足够宽,以在JTextField的当前字体中显示指定列数的m个字符。表 2-8 列出了该类的构造函数。
表 2-8。
Constructors of the JTextField Class
| 构造器 | 描述 | | --- | --- | | `JTextField()` | 用初始文本、列数和文档的默认值创建一个`JTextField`。 | | `JTextField(Document document, String text, int columns)` | 创建一个`JTextField`,将指定的`document`作为其模型,`text`作为其初始文本,`columns`作为其列数。 | | `JTextField(int columns)` | 创建一个将指定的`columns`作为列数的`JTextField`。 | | `JTextField(String text)` | 用指定的`text`创建一个`JTextField`作为它的初始文本。 | | `JTextField(String text, int columns)` | 创建一个`JTextField`,将指定的`text`作为其初始文本,将`columns`作为其列数。 |以下代码片段使用不同的构造函数创建了许多JTextField实例:
// Create an empty JTextField
JTextField emptyTextField = new JTextField();
// Create a JTextField with an initial text of Hello
JTextField helloTextField = new JTextField("Hello");
// Create a JTextField with the number of columns of 20
JTextField nameTextField = new JTextField(20);
一个JTextField可以输入多少个字符?您可以在JTextField中输入的字符数量没有限制。如果你想限制一个JTextField中的人物数量,你需要定制它的模型。请注意,JTextField的型号存储其内容。在看到运行中的定制模型之前,让我们看看在 Swing 中将文本组件的模型和视图分开的强大功能。
让我们创建两个JTextField的实例。您将设置mirroredName的型号与name的型号相同。你正在做一件非常简单的事情。两个文本字段使用相同的模型。这使得两个字段成为彼此的镜像字段。如果您在其中一个窗口中输入文本,相同的文本会自动显示在另一个窗口中。这是怎么发生的?当您在JTextField中输入文本时,它的模型被更新。它的模型中的任何更新都向它的视图(在这种情况下,这两个组件充当视图)发送通知来更新它们自己。由于两个文本字段是具有相同模型的两个视图,模型中的任何更新(通过任一文本字段)都将向两个文本字段发送通知,并且两个文本字段都将更新它们的视图以显示相同的文本。
清单 2-2 展示了如何在两个文本字段之间共享一个模型。运行该程序,并在任一文本字段中输入一些文本。您将看到另一个文本字段与相同的文本同时更新。
清单 2-2。通过与另一个 JTextField 共享模型来镜像 JTextField
// MirroredTextField.java
package com.jdojo.swing;
import javax.swing.JFrame;
import javax.swing.JTextField;
import javax.swing.JLabel;
import java.awt.GridLayout;
import java.awt.Container;
import javax.swing.text.Document;
public class MirroredTextField extends JFrame {
JLabel nameLabel = new JLabel("Name:") ;
JLabel mirroredNameLabel = new JLabel("Mirrored Name:") ;
JTextField name = new JTextField(20);
JTextField mirroredName = new JTextField(20);
public MirroredTextField() {
super("Mirrored JTextField");
this.initFrame();
}
private void initFrame() {
this.setDefaultCloseOperation(EXIT_ON_CLOSE);
this.setLayout(new GridLayout(2, 0));
Container contentPane = this.getContentPane();
contentPane.add(nameLabel);
contentPane.add(name);
contentPane.add(mirroredNameLabel);
contentPane.add(mirroredName);
// Set the model for mirroredName to be the same
// as name's model, so they share their content's storage.
Document nameModel = name.getDocument();
mirroredName.setDocument(nameModel);
}
public static void main(String[] args) {
MirroredTextField frame = new MirroredTextField();
frame.pack();
frame.setVisible(true);
}
}
为了拥有自己的JTextField模型,你需要创建一个新的类。新类既可以实现Document接口,也可以从该类继承。后一种方法更简单,也是最常用的。清单 2-3 包含了一个LimitedCharDocument类的代码,它继承自PlainDocument类。当你想限制一个JTextField中的字符数量时,你可以使用这个类作为一个JTextField的模型。默认情况下,它允许用户输入不限数量的字符。您可以在其构造函数中设置允许的字符数。
清单 2-3。表示具有有限数量字符的普通文档的类
// LimitedCharDocument.java
package com.jdojo.swing;
import javax.swing.text.PlainDocument;
import javax.swing.text.BadLocationException;
import javax.swing.text.AttributeSet;
public class LimitedCharDocument extends PlainDocument {
private int limit = -1; // < 0 means an unlimited characters
public LimitedCharDocument() {
}
public LimitedCharDocument(int limit) {
this.limit = limit;
}
@Override
public void insertString(int offset, String str, AttributeSet a)
throws BadLocationException {
String newString = str;
if (limit >=0 && str != null) {
// Check for the limit
int currentLength = this.getLength() ;
int newTextLength = str.length();
if (currentLength + newTextLength > limit) {
newString = str.substring(0, limit - currentLength);
}
}
super.insertString(offset, newString, a);
}
}
LimitedCharDocument类中感兴趣的代码是insertString()方法。Document接口声明了一个方法。PlainDocument类提供了默认的实现。LimitedCharDocument类覆盖默认实现,并检查插入的字符串是否会超过允许的字符数。如果插入的字符串超过了允许的最大字符数,它会砍掉多余的字符。如果将限制设置为负数,则允许无限数量的字符。最后,该方法简单地调用它在PlainDocument类中的实现来执行真正的动作。
每次将文本插入到JTextField中时,都会调用模型的insertString()。此方法获取以下三个参数:
int offset:这是琴弦插入JTextField的位置。第一个字符在偏移量 0 处插入,第二个字符在偏移量 1 处插入,依此类推。String str:插入JTextField的是字符串。当您在JTextField中输入文本时,对于您输入的每个字符都会调用insertString()方法,并且该参数将只包含一个字符。但是,当您将文本粘贴到JTextField中或使用其setText()方法时,该参数可能包含多个字符。AttributeSet a:必须与插入文本相关联的属性。
您可以在代码中使用LimitedCharDocument,如下所示:
// Create a JTextField, which will only allow 10 characters
Document tenCharDoc = new LimitedCharDocument(10);
JTextField t1 = new JTextField(tenCharDoc, "your name", 10);
还有另一种方法为一个JTextField设置文档。您需要创建一个继承自JTextField的新类,并覆盖它的createDefaultModel()方法。它在JTextField类中声明为protected,默认情况下,它返回一个PlainDocument。您可以从此方法返回自定义文档类的实例。您的自定义代码JTextField如下所示:
public class TenCharTextField extends JTextField {
@Override
protected Document createDefaultModel() {
// Return a document object that allows maximum 10 characters
return new LimitedCharDocument(10);
}
// Other code goes here
}
只要需要一个容量为 10 个字符的JTextField,就可以使用TenCharTextField类的实例。
从JTextField类中的构造函数调用createDefaultModel()方法。因此,您不应该向您的客户JTextField传递一个参数,并使用该参数的值在您的类的createDefaultModel()方法中构建模型。例如,以下代码片段不会产生预期的结果:
static class LimitedCharTextField extends JTextField {
private int maxChars = -1;
public LimitedCharTextField(int maxChars) {
this.maxChars = maxChars;
}
protected Document createDefaultModel() {
/* Wrong use of maxChars!!! By the time this method is called,
maxChars will have its default value of zero. This method will be
called from the constructor of the JTextField class and at that time
the constructor for this class would not start executing.
*/
return new LimitedCharDocument(maxChars);
}
}
有时,您可能希望强制用户以特定格式在文本字段中输入文本,例如以 mm/dd/yyyy 格式输入日期或仅输入数字。这可以通过为JTextField组件使用定制模型来实现。Swing 包含另一个名为JFormattedTextField的文本组件,它允许您设置文本字段的格式。如果您需要一个允许用户以特定格式添加文本的组件,那么JFormattedTextField会使这项工作变得容易得多。我将很快讨论JFormattedTextField。
jpassword field(jpassword 字段)
一个JPasswordField是一个JTextField,除了它允许隐藏字段中显示的实际字符。例如,当您使用登录表单输入密码时,您不希望别人越过您的肩膀看到您在屏幕上的密码。默认情况下,它为字段中的每个实际字符显示一个星号(*)字符。这被称为回声字符。默认的回显字符也取决于应用的外观。您可以通过使用它的 s etEchoChar(char newEchoChar)方法来设置自己的 echo 字符。
JPasswordField类与JTextField类具有相同的构造函数集。您可以使用初始文本、列数和一个Document对象的组合来创建一个JPasswordField对象。
// Create a password field 10 characters wide
JPasswordField passwordField = new JPasswordField(10);
出于安全原因,JPasswordField的getText()方法已被否决。您应该使用它的getPassword()方法,该方法返回一个数组char。在你使用完这个char数组后,你应该将它的所有元素重置为零。下面的代码片段显示了如何验证在JPasswordField中输入的密码:
// Get the password entered in the field
char c[] = passwordField.getPassword();
// Suppose you have the correct password in a string.
// Usually, you will get it from a file or database
String correctPass = "Hello";
// Do not convert your password in c[] to a String. Rather, convert the correctPass
// to a char array. Or, better you would have correctPass as char array in the first place.
char[] cp = correctPass.toCharArray();
// Use the equals() method of the java.util.Arrays class to compare c and cp for equality
if (Arrays.equals(c, cp)) {
// The password is correct
}
else {
// The password is incorrect
}
// Null out the password that you have in the char arrays
Arrays.fill(c, (char)0);
Arrays.fill(cp, (char)0);
您可以使用setEchoChar()方法设置您选择的回显字符,如下所示:
// Set # as the echo character
password.setEchoChar(‘#');
您可以将JPasswordField作为JTextField使用,方法是将其回显字符设置为零,如下所示:
// Set the echo character to 0, so the actual password characters are visible.
passwordField.setEchoChar((char)0);
Tip
您需要将JPasswordField的回送字符设置为 ASCII 值为零的字符值,这样JPasswordField将显示实际的字符。如果您将回送字符设置为'0' (ASCII 值为 48),实际的密码将不会显示。相反,将为每个实际字符回显一个'0'字符。
JFormattedTextField
一个JFormattedTextField是一个JTextField,具有以下两个附加功能:
- 它允许您指定编辑和/或显示文本的格式。
- 当字段中的值为
null时,它还允许您指定一种格式。
除了让您获取和设置字段中的文本的getText()和setText()方法之外,JFormattedTextField还提供了两个新方法,分别叫做getValue()和etValue(),让您可以处理任何类型的数据,而不仅仅是文本。
JFormattedTextField预配置为处理三种数据:数字、日期和字符串。但是,您可以格式化要在该字段中显示的任何对象。您可以使用不同的构造函数以多种方式设置JFormattedTextField的格式,这些构造函数在表 2-9 中列出。
表 2-9。
Constructors of the JFormattedTextField Class
| 构造器 | 描述 | | --- | --- | | `JFormattedTextField()` | 创建一个没有格式化程序的`JFormattedTextField`。你需要使用它的`setFormatterFactory()`或`setValue()`方法来设置一个格式化程序。 | | `JFormattedTextField(Format format)` | 创建一个`JFormattedTextField`,它将使用指定的`format`来格式化字段中的文本。 | | `JFormattedTextField(` `JFormattedTextField.AbstractFormatter formatter)` | 用指定的格式化程序创建一个`JFormattedTextField`。 | | `JFormattedTextField(JFormattedTextField.AbstractFormatterFactory` `factory)` | 用指定的工厂创建一个`JFormattedTextField`。 | | `JFormattedTextField(``JFormattedTextField.AbstractFormatterFactory` | 用指定的工厂和指定的初始值创建一个`JFormattedTextField`。 | | `JFormattedTextField(Object value)` | 用指定的值创建一个`JFormattedTextField`。该字段将根据值的类别自行配置值的格式。如果将一个`null`作为值传递,该字段就无法知道它需要格式化哪种类型的值,并且它根本不会尝试格式化该值。 |有必要了解格式、格式化程序和格式化程序工厂之间的区别。java.text.Format对象以字符串形式定义了对象的格式。也就是说,它定义了一个对象作为字符串的外观;例如,mm/dd/yyyy格式的日期对象看起来像07/09/2008。
格式化程序由一个JFormattedTextField.AbstractFormatter对象表示,它使用一个java.text.Format对象来格式化一个对象。它的工作是将对象转换成字符串,并将字符串转换回对象。
格式化程序工厂是格式化程序的集合。一个JFormattedTextField使用一个格式化程序工厂来获得一个特定类型的格式化程序。格式化程序工厂对象由JFormattedTextField.AbstractFormatterFactory类的一个实例表示。
以下代码片段将dobField配置为将其中的文本格式化为当前区域设置格式的日期:
JFormattedTextField dobField = new JFormattedTextField();
dobField.setValue(new Date());
下面的代码片段配置了一个salaryField来以当前语言环境格式显示一个数字:
JFormattedTextField salaryField = new JFormattedTextField();
salaryField.setValue(new Double(11233.98));
也可以用格式化程序创建一个JFormattedTextField。您需要使用DateFormatter、NumberFormatter和MaskFormatter类来分别格式化日期、数字和字符串。这些类都在javax.swing.text包里。
// Have a field to format a date in mm/dd/yyyy format
DateFormat dateFormat = new SimpleDateFormat("mm/dd/yyyy");
DateFormatter dateFormatter = new DateFormatter(dateFormat);
dobField = new JFormattedTextField(dateFormatter);
// Have field to format a number in $#0,000.00 format
NumberFormat numFormat = new DecimalFormat("$#0,000.00");
NumberFormatter numFormatter = new NumberFormatter(numFormat);
salaryField = new JFormattedTextField(numFormatter);
您需要使用掩码格式化程序来格式化字符串。掩码格式化程序使用表 2-10 中列出的特殊字符来指定掩码。
表 2-10。
Special Characters Used to Specify a Mask
| 性格;角色;字母 | 描述 | | --- | --- | | `#` | 一个数字 | | `?` | 一封信 | | `A` | 一个字母或一个数字 | | `*` | 任何事 | | `U` | 一个字母,小写字符映射成大写字符 | | `L` | 一个字母,大写字符映射成小写字母 | | `H` | 十六进制数字(a-f,A-F,0-9) | | `'` | 一句引言。它是一个转义字符,用于转义任何特殊格式的字符。 |为了让用户输入一个###-##-####格式的社会保险号,您创建一个JFormattedTextField,如下所示。注意,构造函数MaskFormatter(String mask)抛出了一个ParseException。
MaskFormatter ssnFormatter = null;
JFormattedTextField ssnField = null;
try {
ssnFormatter = new MaskFormatter("###-##-####");
ssnField = new JFormattedTextField(ssnFormatter);
}
catch (ParseException e) {
e.printStackTrace();
}
当使用掩码格式化程序时,您只能使用您在掩码中指定的字符数。所有非特殊字符(见表 2-10 中的特殊字符列表)显示在屏蔽中。掩码中的每个特殊字符都会显示一个占位符(默认为空格)。例如,如果您将遮罩指定为"###-##-####",则JFormattedTextField会将" - - "显示为占位符。您还可以使用MaskFormatter类的setPlaceHolderCharacter(char placeholder)方法为特殊字符指定一个占位符。要在 SNN 字段中显示000-00-0000,您需要使用“0”作为主格式化程序的占位符,如下所示:
ssnFormatter = new MaskFormatter("###-##-####");
ssnFormatter.setPlaceholderCharacter('0');
创建组件后,您可以使用JFormattedTextField的方法来更改格式化程序。例如,要为名为payDate的JFormattedTextField设置日期格式,在创建它之后,您可以编写
DateFormatter df = new DateFormatter(new SimpleDateFormat("mm/dd/yyyy"));
DefaultFormatterFactory dff = new DefaultFormatterFactory(df, df, df, df); dobField.setFormatterFactory(dff);
JFormattedTextField让您指定四种类型的格式化程序:
- 答:当字段中的值为
null时使用。 - 安:当字段有焦点时使用。
- 答:当字段没有焦点并且有一个非空值时使用。
- 答:在以上三种格式化程序都不存在的情况下使用。
您可以通过在JFormattedTextField类的构造函数中使用格式化程序工厂或者调用它的setFormatterFactory()方法来指定所有四个格式化程序。JFormattedTextField.AbstractFormatterFactory抽象类的一个实例代表一个格式化程序工厂。javax.swing.text.DefaultFormatterFactory类是JFormattedTextField.AbstractFormatterFactory类的一个实现。当指定格式化程序时,使用同一个格式化程序来代替四个格式化程序。当指定格式化程序工厂时,您可以为四种不同的情况指定不同的格式化程序。
假设您有一个名为dobField的JFormattedTextField来显示日期。当该字段获得焦点时,您希望让用户以mm/dd/yyyy的格式编辑日期(例如07/07/2008)。当它没有焦点时,您希望以 mmmm dd, yyyy(例如July 07, 2008)格式显示日期。下面的代码片段将完成这项工作:
DateFormatter df = new DateFormatter(new SimpleDateFormat("mmmm dd, yyyy"));
DateFormatter edf = new DateFormatter(new SimpleDateFormat("mm/dd/yyyy"));
DefaultFormatterFactory ddf = new DefaultFormatterFactory(df, df, edf, df);
dobField.setFormatterFactory(ddf);
如果您已经配置了JFormattedTextField来格式化日期,那么您可以使用它的getValue()方法来获得一个Date对象。getValue()方法的返回类型是Object,您需要将返回值转换为类型Date。您可以将光标放在字段中日期值的月、日、年、小时、分钟和秒部分,并使用上/下箭头键更改该特定部分。如果您想在键入时覆盖字段中的值,您需要使用方法setOverwriteMode(true)将格式化程序设置为覆盖模式。
使用JFormattedTextField的另一个好处是可以限制一个字段中可以输入的字符数。回想一下,在上一节中,您是通过为JTextField使用定制文档来实现这一点的。您可以通过设置掩码格式化程序来达到同样的目的。假设您想让用户在一个字段中最多输入两个字符。您可以按如下方式完成此操作:
JFormattedTextField twoCharField = new JFormattedTextField(new MaskFormatter("**"));
JTextArea(人名)
一个JTextArea可以处理多行纯文本。大多数情况下,当您在一个JTextArea中有多行文本时,您将需要滚动功能。一个JTextArea本身不提供滚动。相反,当您需要任何 Swing 组件的滚动功能时,您需要从另一个名为JScrollPane的 Swing 组件获得帮助。
您可以指定用于确定其首选大小的JTextArea的行数和列数。行数用于确定其首选高度。如果您将行数设置为N,这意味着它的首选高度将被设置为显示当前字体设置中文本的N行数。列数用于确定其首选宽度。如果将列数设置为M,则意味着其首选宽度被设置为当前字体设置中字符m(小写 M)宽度的M倍。
一个JTextArea提供了许多构造函数来创建一个JTextArea组件,使用初始文本、模型、行数和列数的组合作为参数,如表 2-11 所示。
表 2-11。
Constructors of the JTextArea Class
| 构造器 | 描述 | | --- | --- | | `JTextArea()` | 用默认模型创建一个`JTextArea`,初始字符串为`null`,行/列为零。 | | `JTextArea(Document doc)` | 用指定的`doc`创建一个`JTextArea`作为它的模型。它的初始字符串被设置为`null`,行/列被设置为零。 | | `JTextArea(Document doc, String text, int rows, int columns)` | 创建一个`JTextArea`,它的所有属性(模型、初始文本、行和列)都在它的参数中指定。 | | `JTextArea(int rows, int columns)` | 用默认模型创建一个`JTextArea`,初始字符串为`null`,指定行/列。 | | `JTextArea(String text)` | 用指定的初始文本创建一个`JTextArea`。设置默认模型,并将行/列设置为零。 | | `JTextArea(String text, int rows, int columns)` | 用指定的文本、行和列创建一个`JTextArea`。使用默认模型。 |以下代码片段使用不同的初始值创建了许多JTextArea实例:
// Create a blank JTextArea
JTextArea emptyTextArea = new JTextArea();
// Create a JTextArea with 10 rows and 50 columns
JTextArea commentsTextArea = new JTextArea(10, 50);
// Create a JTextArea with 10 rows and 50 columns with an initial text of "Enter resume here"
JTextArea resumeTextArea = new JTextArea("Enter resume here", 10, 50);
非常重要的是要记住,当你使用JTextArea时,通常你的文本尺寸会比它在屏幕上的尺寸大,你需要一个滚动功能。要给一个JTextArea添加滚动功能,您需要将它添加到一个JScrollPane,并将JScrollPane添加到容器,而不是JTextArea。下面的代码片段演示了这个概念。假设您有一个名为myFrame的JFrame,其内容窗格的布局设置为BorderLayout,并且您想要在中心区域添加一个可滚动的JTextArea。
// Create JTextArea
JTextArea resumeTextArea = new JTextArea("Enter resume here", 10, 50);
// Add JTextArea to a JScrollPane
JScrollPane sp = new JScrollPane(resumeTextArea);
// Get the reference of the content pane of the JFrame
Container contentPane = myFrame.getContentPane();
// Add the JScrollPane (sp) to the content pane, not the JTextArea
contentPane.add(sp, BorderLayout.CENTER);
表 2-12 中有一些JTextArea的常用方法。大多数时候,你会使用它的setText()、getText()和append()方法。
表 2-12。
Commonly Used Methods of JTextArea
| 方法 | 描述 | | --- | --- | | `void append(String text)` | 将指定的`text`追加到`JTextArea`的末尾。 | | `int getLineCount()` | 返回`JTextArea`中的行数。 | | `int getLineStartOffset(int line) throws BadLocationException` `int getLineEndOffset(int line) throws BadLocationException` | 返回指定`line`数字的开始和结束偏移量(也称为位置,从零开始)。如果`line`号超出范围,抛出异常。当你把这个方法和`getLineCount()`方法结合起来时,它是有用的。您可以在一个循环中使用这三种方法逐行解析包含在`JTextArea`中的文本。 | | `int getLineOfOffset(int offset) throws BadLocationException` | 返回指定的`offset`出现的行号。 | | `boolean getLineWrap()` | 如果设置了换行,返回`true`。否则返回`false`。 | | `int getTabSize()` | 返回用于制表符的字符数。默认情况下,它返回 8。 | | `boolean getWrapStyleWord()` | 如果自动换行设置为`true`,则返回`true`。否则,它返回`false`。 | | `void insert(String text, int offset)` | 在指定的`offset`处插入指定的`text`。如果模型是`null`或者指定的`text`是空的或者是`null`,调用这个方法没有效果。 | | `void replaceRange(String text, int start, int end)` | 用指定的`text`替换`start`和`end`位置之间的文本。 | | `void setLineWrap(boolean wrap)` | 为`JTextArea`设置换行策略。如果换行设置为`true`,如果一行不适合`JTextArea`的宽度,则换行。如果设置为`false`,即使比`JTextArea`的宽度长,也不会换行。默认设置为`false`。 | | `void setTabSize(int size)` | 设置制表符将扩展到指定大小的字符数。 | | `void setWrapStyleWord(boolean word)` | 当换行设置为`true`时,设置自动换行样式。当设置为`true`时,该行在字边界换行。否则,该行将在字符边界处换行。默认情况下,它被设置为`false`。 |JTextArea使用可配置的策略在其可显示区域换行。如果换行设置为true,并且一条线比元件的宽度长,该线将被换行。默认情况下,换行设置为false。使用setLineWrap(boolean lineWrap)方法设置换行。
一行可以在单词边界或字符边界换行,这由单词换行策略决定。使用setWrapStyleWord(boolean wordWrap)方法设置单词换行策略。只有当调用了setLineWrap(true)时,调用这个方法才会生效。也就是说,换行策略定义了换行策略的细节。图 2-9 显示了一个JFrame中显示的三个JTextArea组件。

图 2-9。
The effects of line and word wrapping in a JTextArea
对于图中的三个JTextArea组件(从左到右),换行和换行设置分别为(true、true)、(true、false)、(false、true)。第一个在单词边界处换行。第二个在字符边界换行。第三个没有换行,你不能看到整个文本的宽度。请注意,三个JTextArea组件中的每一个都添加到了JFrame中,而没有添加到 a 中。
编辑器面板
一个JEditorPane是一个文本组件,被设计用来处理不同种类的文本。默认情况下,它知道如何处理纯文本、HTML 和富文本格式(RTF)。虽然它是为编辑和显示多种类型的内容而设计的,但它主要用于显示 HTML 文档,其中只包含基本的 HTML 元素。对 RTF 内容的支持是非常基本的。
JEditorPane使用特定的对象处理特定类型的内容。如果您想在这个组件中处理新类型的内容,您将需要创建一个定制的EditorKit类,它是javax.swing.text.EditorKit类的一个子类。如果你只是使用这个组件来显示 HTML 内容,你不需要担心一个EditorKit;该组件将为您处理相关的功能。使用JEditorPane显示一个 HTML 页面只需要一行代码,如下所示:
// Create a JEditorPane to display yahoo.com 网页
JEditorPane htmlPane = new JEditorPane("http://www.yahoo.com
注意,JEditorPane类的一些构造函数抛出了一个IOException。指定 URL 时,必须使用 URL 的完整形式,以协议开头。您可以通过以下三种不同的方式让JEditorPane知道它需要安装哪种类型的EditorKit来处理它的内容:
- 通过调用方法
- 通过调用
setPage(URL url)或setPage(String url)方法 - 通过调用方法
JEditorPane预配置为理解三种类型的内容:文本/纯文本、文本/html 和文本/rtf。您可以使用下面的代码显示文本Hello,在 HTML 中使用
标签:
htmlPane.setContentType("text/html");
htmlPane.setText("<html><body><h1>Hello</h1></body></html>");
当您调用它的setPage()方法时,它使用适当的EditorKit来处理 URL 提供的内容。在下面的代码片段中,JEditorPane根据内容类型使用了EditorKit:
// Handle an HTML Page
editorPane.setPage("http://www.yahoo.com
// Handle an RTF file. When you use a file protocol, you may use three slashes instead of one
editorPane.setPage("file:///C:/test.rtf");
JEditorPane将流中的内容读入编辑器窗格。如果它的编辑器套件已经设置为处理 HTML 内容,并且指定的描述是类型javax.swing.text.html.HTMLDocument,则内容将被读取为 HTML。否则,内容将作为纯文本读取。
当您处理 HTML 文档时,您可能希望在单击超链接时导航到不同的页面。为了使用超链接,您需要向添加一个超链接侦听器,并在事件侦听器的hyperlinkUpdate()方法中,使用setPage()方法导航到新页面。超链接上的三种动作之一ENTERED、EXITED和ACTIVATED触发hyperlinkUpdate()方法。当鼠标进入超链接区域时发生ENTERED事件,当鼠标离开超链接区域时发生EXITED事件,当单击超链接时发生ACTIVATED事件。当您想使用超链接导航到另一个页面时,请确保在超链接监听器的hyperlinkUpdate()方法中检查了ACTIVATED事件。以下代码片段使用 lambda 表达式将HyperlinkListener添加到JEditorPane:
editorPane.addHyperlinkListener((HyperlinkEvent event) -> {
if (event.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {
try {
editorPane.setPage(event.getURL());
}
catch (IOException e) {
e.printStackTrace();
}
}
});
如果您想知道新页面何时被加载到JEditorPane中,您需要添加一个属性更改监听器来监听它的属性更改事件,并检查名为page的属性是否已经更改。清单 2-4 包含了使用JEditorPane作为浏览器浏览网页的完整代码。当您运行该程序时,您可以在 URL 字段中输入一个网页地址,然后按 enter 键(或按 Go 按钮),浏览器将显示新 URL 的内容。您也可以单击内容中的超链接导航到另一个网页。代码很简单,包含足够的注释来帮助你理解程序逻辑。
清单 2-4。使用 JEditorPane 组件的 HTML 浏览器
// HTMLBrowser.java
package com.jdojo.swing;
import javax.swing.JFrame;
import java.awt.Container;
import javax.swing.JLabel;
import javax.swing.JScrollPane;
import javax.swing.Box;
import javax.swing.JEditorPane;
import javax.swing.JTextField;
import javax.swing.JButton;
import java.awt.BorderLayout;
import java.net.URL;
import javax.swing.event.HyperlinkEvent;
import java.beans.PropertyChangeEvent;
import java.net.MalformedURLException;
import java.io.IOException;
public class HTMLBrowser extends JFrame {
JLabel urlLabel = new JLabel("URL:");
JTextField urlTextField = new JTextField(40);
JButton urlGoButton = new JButton("Go");
JEditorPane editorPane = new JEditorPane();
JLabel statusLabel = new JLabel("Ready");
public HTMLBrowser(String title) {
super(title);
initFrame();
}
// Initialize the JFrame and add components to it
private void initFrame() {
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
Container contentPane = this.getContentPane();
Box urlBox = this.getURLBox();
Box editorPaneBox = this.getEditPaneBox();
contentPane.add(urlBox, BorderLayout.NORTH);
contentPane.add(editorPaneBox, BorderLayout.CENTER);
contentPane.add(statusLabel, BorderLayout.SOUTH);
}
private Box getURLBox() {
// URL Box consists of a JLabel, a JTextField and a JButton
Box urlBox = Box.createHorizontalBox();
urlBox.add(urlLabel);
urlBox.add(urlTextField);
urlBox.add(urlGoButton);
// Add an action listener to urlTextField, so when the user enters a url
// and presses the enter key, the appplication navigates to the new URL.
urlTextField.addActionListener(e -> {
String urlString = urlTextField.getText();
go(urlString);
});
// Add an action listener to the Go button
urlGoButton.addActionListener(e -> go());
return urlBox;
}
private Box getEditPaneBox() {
// To display HTML, you must make the editor pane non-editable.
// Otherwise, you will see an editable HTML page that doesnot look nice.
editorPane.setEditable(false);
// URL Box consists of a JLabel, a JTextField and a JButton
Box editorBox = Box.createHorizontalBox();
// Add a JEditorPane inside a JScrollPane to provide scolling
editorBox.add(new JScrollPane(editorPane));
// Add a hyperlink listener to the editor pane, so that it
// navigates to a new page, when the user clicks a hyperlink
editorPane.addHyperlinkListener((HyperlinkEvent event) -> {
if (event.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {
go(event.getURL());
} else if (event.getEventType() == HyperlinkEvent.EventType.ENTERED) {
statusLabel.setText("Please click this link to visit the page");
} else if (event.getEventType() == HyperlinkEvent.EventType.EXITED) {
statusLabel.setText("Ready");
}
});
// Add a property change listener, so we can update
// the URL text field with url of the new page
editorPane.addPropertyChangeListener((PropertyChangeEvent e) -> {
String propertyName = e.getPropertyName();
if (propertyName.equalsIgnoreCase("page")) {
URL url = editorPane.getPage();
urlTextField.setText(url.toExternalForm());
}
});
return editorBox;
}
// Navigates to the url entered in the URL JTextField
public void go() {
try {
URL url = new URL(urlTextField.getText());
this.go(url);
}
catch (MalformedURLException e) {
setStatus(e.getMessage());
}
}
// Navigates to the specified URL
public void go(URL url) {
try {
editorPane.setPage(url);
urlTextField.setText(url.toExternalForm());
setStatus("Ready");
}
catch (IOException e) {
setStatus(e.getMessage());
}
}
// Navigates to the specified URL specified as a string
public void go(String urlString) {
try {
URL url = new URL(urlString);
go(url);
}
catch (IOException e) {
setStatus(e.getMessage());
}
}
private void setStatus(String status) {
statusLabel.setText(status);
}
public static void main(String[] args) {
HTMLBrowser browser = new HTMLBrowser("HTML Browser");
browser.setSize(700, 500);
browser.setVisible(true);
// Let us visit yahoo.com
browser.go("http://www.yahoo.com
}
}
以下是该计划的重要部分:
- 该方法将一个
JLabel、一个JTextField和一个JButton打包在一个水平框中,并将其添加到框架的北部区域。它向JTextField和JButton添加了一个动作监听器,这样当用户在输入新的 URL 后按回车键或 Go 按钮时,浏览器就会导航到新的 URL。 - 该方法将一个
JEditorPane封装在一个JScrollPane中,并将其添加到帧的中心区域。它还向JEditorPane添加了一个超链接监听器和一个属性更改监听器。超链接侦听器用于在用户单击超链接时导航到 URL。当鼠标进入和退出超链接区域时,它还会在状态栏中显示相应的帮助消息。 - 一个
JLabel用于在框架的南部区域显示一条简短信息。 - 该方法已被重载,它的主要工作是使用
setPage()方法导航到一个新页面。 main()方法用于测试。它在浏览器中显示雅虎的主页。
作为一项任务,您可以在浏览器中添加Back和Forward按钮,让用户在已经访问过的网页之间来回导航。
Tip
为了以良好的格式显示 HTML 页面,您需要通过调用setEditable(false)方法使JEditorPane不可编辑。你不应该使用一个JEditorPane来显示所有类型的 HTML 页面,因为它不能处理所有可以嵌入到 HTML 页面中的不同内容。相反,您应该只使用它来显示包含基本 HTML 内容的 HTML 页面,例如应用的 HTML 帮助文件。
耶文本字符串
JTextPane类是JEditorPane类的子类。它是一个专门的组件,用于处理带有嵌入图像和组件的样式化文档。您可以设置字符和段落的属性。如果你想显示一个 HTML,RTF,或者普通文档,JEditorPane是你最好的选择。但是,如果您需要文字处理器提供的丰富功能来编辑/显示样式化的文本,您需要使用JTextPane。这是一台小型文字处理机。它总是适用于样式化的文档,即使其内容是纯文本。本节不可能讨论它的所有特性;它本身就配得上一本小书。我将谈到它的特性,比如设置样式文本、嵌入图像和组件。
一个JTextPane使用一个样式化的文档,它是接口的一个实例。StyledDocument接口继承了Document接口。DefaultStyledDocument是StyledDocument接口的实现类。A JTextPane使用 a DefaultStyledDocument作为其默认型号。Swing 文本组件中的文档由以树状结构组织的元素组成。顶部元素称为根元素。文档中的一个元素是javax.swing.text.Element接口的一个实例。
普通文档有一个根元素。根元素可以有多个子元素。每个子元素由一行文本组成。请注意,在普通文档中,文档中的所有字符都具有相同的属性(或格式样式)。
样式化文档有一个根元素,也称为节。根元素有分支元素,也称为段落。一个段落有一连串的字符。一个字符串是一组共享相同属性的连续字符。例如,“Hello world”字符串定义了一个字符串。然而,“Hello world”字符串定义了两个字符串。注意单词“world”是粗体,而“Hello”不是。这就是为什么他们定义了两个不同的字符运行。在一个样式化的文档中,一个段落以一个换行符结束,除非是最后一个段落,它不需要以换行符结束。您可以在段落级别定义属性,如缩进、行距、文本对齐等。您可以在字符运行级别定义属性,如字体大小、字体系列、粗体、斜体等。图 2-10 和图 2-11 分别显示了普通文档和样式化文档的结构。

图 2-11。
Structure of a styled document

图 2-10。
Structure of a plain document
清单 2-5 中的程序使用一个JTextPane开发了一个基本的文字处理器。它允许您编辑文本,并对文本应用粗体、斜体、颜色和对齐等样式。
清单 2-5。使用 JTextPane 和 JButtons 的简单文字处理器
// WordProcessor.java
package com.jdojo.swing;
import javax.swing.JFrame;
import java.awt.Container;
import javax.swing.JTextPane;
import javax.swing.JButton;
import java.awt.BorderLayout;
import javax.swing.JPanel;
import javax.swing.text.StyledDocument;
import javax.swing.text.BadLocationException;
import javax.swing.text.Style;
import javax.swing.text.StyleContext;
import javax.swing.text.StyleConstants;
import java.awt.Color;
public class WordProcessor extends JFrame {
JTextPane textPane = new JTextPane();
JButton normalBtn = new JButton("Normal");
JButton boldBtn = new JButton("Bold");
JButton italicBtn = new JButton("Italic");
JButton underlineBtn = new JButton("Underline");
JButton superscriptBtn = new JButton("Superscript");
JButton blueBtn = new JButton("Blue");
JButton leftBtn = new JButton("Left Align");
JButton rightBtn = new JButton("Right Align");
public WordProcessor(String title) {
super(title);
initFrame();
}
private void initFrame() {
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
Container contentPane = this.getContentPane();
JPanel buttonPanel = this.getButtonPanel();
contentPane.add(buttonPanel, BorderLayout.NORTH);
contentPane.add(textPane, BorderLayout.CENTER);
this.addStyles(); // Add styles to the text pane for later use
insertTestStrings(); // Insert some texts to the text pane
}
private JPanel getButtonPanel() {
JPanel buttonPanel = new JPanel();
buttonPanel.add(normalBtn);
buttonPanel.add(boldBtn);
buttonPanel.add(italicBtn);
buttonPanel.add(underlineBtn);
buttonPanel.add(superscriptBtn);
buttonPanel.add(blueBtn);
buttonPanel.add(leftBtn);
buttonPanel.add(rightBtn);
// Add ation event listeners to buttons
normalBtn.addActionListener(e -> setNewStyle("normal", true));
boldBtn.addActionListener(e -> setNewStyle("bold", true));
italicBtn.addActionListener(e -> setNewStyle("italic", true));
underlineBtn.addActionListener(e -> setNewStyle("underline", true));
superscriptBtn.addActionListener(e -> setNewStyle("superscript", true));
blueBtn.addActionListener(e -> setNewStyle("blue", true));
leftBtn.addActionListener(e -> setNewStyle("left", false));
rightBtn.addActionListener(e -> setNewStyle("right", false));
return buttonPanel;
}
private void addStyles() {
// Get the default style
StyleContext sc = StyleContext.getDefaultStyleContext();
Style defaultContextStyle = sc.getStyle(StyleContext.DEFAULT_STYLE);
// Add some styles to the document, to retrieve and use later
StyledDocument document = textPane.getStyledDocument();
Style normalStyle = document.addStyle("normal", defaultContextStyle);
// Create a bold style
Style boldStyle = document.addStyle("bold", normalStyle);
StyleConstants.setBold(boldStyle, true);
// Create an italic style
Style italicStyle = document.addStyle("italic", normalStyle);
StyleConstants.setItalic(italicStyle, true);
// Create an underline style
Style underlineStyle = document.addStyle("underline", normalStyle);
StyleConstants.setUnderline(underlineStyle, true);
// Create a superscript style
Style superscriptStyle = document.addStyle("superscript", normalStyle);
StyleConstants.setSuperscript(superscriptStyle, true);
// Create a blue color style
Style blueColorStyle = document.addStyle("blue", normalStyle);
StyleConstants.setForeground(blueColorStyle, Color.BLUE);
// Create a left alignment paragraph style
Style leftStyle = document.addStyle("left", normalStyle);
StyleConstants.setAlignment(leftStyle, StyleConstants.ALIGN_LEFT);
// Create a right alignment paragraph style
Style rightStyle = document.addStyle("right", normalStyle);
StyleConstants.setAlignment(rightStyle, StyleConstants.ALIGN_RIGHT);
}
private void setNewStyle(String styleName, boolean isCharacterStyle) {
StyledDocument document = textPane.getStyledDocument();
Style newStyle = document.getStyle(styleName);
int start = textPane.getSelectionStart();
int end = textPane.getSelectionEnd();
if (isCharacterStyle) {
boolean replaceOld = styleName.equals("normal");
document.setCharacterAttributes(start, end - start,
newStyle, replaceOld);
}
else {
document.setParagraphAttributes(start, end - start, newStyle, false);
}
}
private void insertTestStrings() {
StyledDocument document = textPane.getStyledDocument();
try {
document.insertString(0, "Hello JTextPane\n", null);
}
catch (BadLocationException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
WordProcessor frame = new WordProcessor("Word Processor");
frame.setSize(700, 500);
frame.setVisible(true);
}
}
文字处理程序有点冗长。然而,它做简单、重复的事情。为了更容易理解,我把程序的逻辑分解成了更小的部分。这个程序的目的是展示一个JTextPane,用户可以在这里编辑文本并使用一些按钮对文本应用样式
有八个按钮。其中五种用于格式化文本:普通、粗体、斜体、下划线和上标。Blue按钮用于将文本颜色设置为蓝色。最后两个按钮Left Align和Right Align,用于设置段落左右对齐。
什么是样式,如何为文本和段落设置样式?简单地说,样式是属性(名称-值对)的集合。设置样式很简单;但是,您需要编写几行代码来拥有该样式本身。您可以向JTextPane的文档和JTextPane本身添加样式。你需要使用StyledDocument类的addStyle(String styleName, Style parent)方法。它返回一个Style对象。parent自变量可以是null。如果不是null,未指定的属性将以parent样式解析。一旦有了样式对象,就可以使用StyleConstants类的setXxx()方法来设置该样式中的适当属性。如果你感到困惑,这里有一个回顾。
把一个样式想象成一个有两列的表格:name和value。StyledDocument类的addStyle()方法返回一个空样式(意味着一个空表)。通过使用StyleConstants的setXxx()方法,您正在向样式添加新行(也就是向表格)。一旦表格中至少有一行(即至少定义了一个样式属性),就可以根据样式类型将该样式应用于字符或段落。请注意,您可以使用空样式。空样式可用于从字符范围或段落中移除所有当前样式。下面的代码片段创建了两种样式:第一种是bold,第二种是bold + italic。如果将第一种样式应用于文本,它会将文本格式化为粗体。如果将第二种样式应用于文本,它会将文本格式化为粗体和斜体。注意,您正在将parent样式设置为null。
// Get the styled document from the text pane
StyledDocument document = textPane.getStyledDocument();
// Add an empty style named "bold" to the document
Style bold = document.addStyle("bold", null);
// Add bold attribute to this style
StyleConstants.setBold(bold, true);
// From this point on, you can use the bold style
// Let's create a bold + italic style called boldItalic.
// Add an empty style named boldItalic to the document
Style boldItalic = document.addStyle("boldItalic", null);
// Add bold and italic attributes to the boldItalic style
StyleConstants.setBold(boldItalic, true);
StyleConstants.setItalic(boldItalic, true);
// From this point on, you can use the boldItalic style
将样式对象添加到StyledDocument后,您可能需要它的引用。您可以通过使用它的getStyle(String styleName)方法来检索相同样式的引用。
// Get the bold style from document
Style myBoldStyle = document.getStyle("bold");
一旦有了Style对象,就可以使用StyledDocument类的setCharacterAttributes(int offset, int length, AttributeSet s, boolean replace)和setParagraphAttributes (int offset, int length, AttributeSet s, boolean replace)方法将样式设置为字符范围或段落。如果 replace 参数被指定为true,该区域的任何旧样式都将被新样式替换。否则,新样式将与旧样式合并。
// Suppose a text pane has more than five characters in it.
// Make the first three characters bold
document.setCharacterAttributes(0, 3, bold, false);
一个StyleContext对象为它们的有效使用定义了一个样式池。您可以获取默认的样式集合,如下所示:
StyleContext sc = StyleContext.getDefaultStyleContext();
Style defaultContextStyle = sc.getStyle(StyleContext.DEFAULT_STYLE);
// Let's add a default context style as normal style's parent.
// We do not add any extra attribute to normal styles
StyledDocument document = textPane.getStyledDocument();
Style normal = document.addStyle("normal", defaultContextStyle);
表 2-13 包含了一系列重要的方法及其描述,可以帮助你理解清单 2-5 中的代码。图 2-12 显示了在简单的文字处理器中输入 E = mc 2 后的样子。
表 2-13。
Methods of the WordProcessor Class With Their Descriptions
| 方法 | 描述 | | --- | --- | | `initFrame()` | 通过向框架添加组件并设置`JFrame`的默认行为来初始化框架。 | | `getButtonPanel()` | 返回一个`JPanel`,它包含所有用于格式化的`JButton`。它还为所有的`JButton`添加了动作监听器 | | `addStyles()` | 它向文档添加样式。默认的上下文样式名为“normal ”,它用作所有其他样式的父样式。粗体、斜体等样式。,是字符级样式,而左和右是段落级样式。这些样式是从文档中获取的,以便在`setNewStyle()`方法中使用。 | | `setNewStyle()` | 它将样式设置为字符范围或段落范围,如其参数`isCharacterStyle`所示。请注意,如果您设置了“正常”样式,您将使用此样式替换整个样式。否则,您将合并样式。这个逻辑由下面的语句决定:`boolean replaceOld = styleName.equals("normal");` | | `insertTestStrings()` | 使用`insertString()`方法将字符串插入到`JTextPane`的文档中。 | | `main()` | 创建并显示字处理器框架。 |
图 2-12。
A simple word processor using a JTextPane and JButtons
文字处理器没有保存功能。在真实的应用中,您会提示用户保存文件的位置和名称。下面的代码片段将JTextPane的内容保存到当前工作目录中名为test.rtf的文件中:
// Save the contents of the textPane to a file
FileWriter fw = new java.io.FileWriter("test.rtf");
textPane.write(fw);
fw.close();
JTextPane的write()方法将包含在其文档中的文本写成纯文本。如果你想保存格式化的文本,你需要使用一个RTFEditorKit对象作为它的编辑器工具包,并使用该编辑器工具包的write()方法写入一个文件。下面的代码片段展示了如何使用一个RTFEditorKit对象在一个JTextPane中保存格式化的文本。注意,RTFEditorKit包含一个read()方法来将格式化的文本读回给JTextPane。
// Set an RTFEditorKit to a JTextPane right after you create it
JTextPane textPane = new JTextPane();
textPane.setEditorKit(new RTFEditorKit());
// Other code goes here
// Save formatted text from the JTextPane to a file
String fileName = "test.rtf";
FileOutputStream fos = new FileOutputStream(fileName);
RTFEditorKit kit = (RTFEditorKit)textPane.getEditorKit();
StyledDocument doc = textPane.getStyledDocument();
int len = doc.getLength();
kit.write(fos, doc, 0, len);
fos.close();
Tip
如果你想保存添加到一个JTextPane的图标和组件,你需要将一个JTextPane的文档对象序列化到一个文件中,然后加载回来显示相同的内容。
您可以向JTextPane添加任何 Swing 组件和图标。它只是将一个组件或图标包装成一种样式,并在insertString()方法中使用该样式。下面的代码片段展示了如何将一个JButton和一个图标添加到一个:
// Add a Close button to our document
JButton closeButton = new JButton("Close");
closeButton.addActionListener(e -> System.exit(0));
Style cs = doc.addStyle("componentStyle", null);
StyleConstants.setComponent(cs, closeButton);
// Insert the component at the end of the text.
try {
document.insertString(doc.getLength(), "Close Button goes", cs);
}
catch (BadLocationException e) {
e.printStackTrace();
}
向JTextPane添加图标类似于向其添加组件,只是您使用了StyleConstants类的setIcon()方法而不是setComponent()方法,并且使用了ImageIcon对象而不是组件,如图所示:
// Add an icon to a JTextPane
StyleConstants.setIcon(myIconStyle, new ImageIcon("myImageFile"));
Tip
你也可以使用JTextPane的insertComponent(Component c)和insertIcon(Icon g)方法来分别插入一个组件和一个图标。
您可以通过使用AbstractDocument类的dump(PrintStream p)方法来查看JTextPane文档的元素结构。以下代码片段在标准输出中显示转储:
// Display the document structure on the standard output
DefaultStyledDocument doc = (DefaultStyledDocument)textPane.getStyledDocument();
doc.dump(System.out);
下面是一个JTextPane的带文本的文档的转储,如图 2-12 所示。它让您对样式化文档的结构有所了解。
<section>
<paragraph
resolver=NamedStyle:default {bold=false,name=default,foreground=sun.swing.PrintColorUIResource[r=51,g=51,b=51],family=Dialog,FONT_ATTRIBUTE_KEY=javax.swing.plaf.FontUIResource[family=Dialog,name=Dialog,style=plain,size=12],size=12,italic=false,}
>
<content>
[0,16][Hello JTextPane
]
<paragraph
resolver=NamedStyle:default {bold=false,name=default,foreground=sun.swing.PrintColorUIResource[r=51,g=51,b=51],family=Dialog,FONT_ATTRIBUTE_KEY=javax.swing.plaf.FontUIResource[family=Dialog,name=Dialog,style=plain,size=12],size=12,italic=false,}
>
<content>
[16,17][
]
<paragraph
resolver=NamedStyle:default {bold=false,name=default,foreground=sun.swing.PrintColorUIResource[r=51,g=51,b=51],family=Dialog,FONT_ATTRIBUTE_KEY=javax.swing.plaf.FontUIResource[family=Dialog,name=Dialog,style=plain,size=12],size=12,italic=false,}
>
<content
bold=true
name=bold
resolver=NamedStyle:normal {name=normal,resolver=AttributeSet,}
>
[17,21][E=mc]
<content
bold=true
name=bold
resolver=NamedStyle:normal {name=normal,resolver=AttributeSet,}
superscript=true
>
[21,22][2]
<content>
[22,23][
]
<bidi root>
<bidi level
bidiLevel=0
>
[0,23][Hello JTextPane
E=mc2
]
验证文本输入
您已经看到了在文本组件中验证文本输入的例子:使用定制模型和使用JFormattedTextField。您可以将一个输入验证器对象附加到任何一个JComponent,包括一个文本组件。输入验证器对象只是一个类的对象,它继承自名为InputVerifier的抽象类。该类声明如下:
public abstract class InputVerifier {
public abstract boolean verify(JComponent input);
public boolean shouldYieldFocus(JComponent input) {
return verify(input);
}
}
您需要覆盖InputVerifier类的verify()方法。该方法包含验证文本字段中的输入的逻辑。如果文本字段中的值有效,则从此方法返回true。否则,你返回false。当文本字段将要失去焦点时,它的输入验证器的verify()方法被调用。只有当文本字段的输入验证器的verify()方法返回true时,文本字段才会失去焦点。文本组件的setInputVerifier()方法用于附加一个输入验证器。下面的代码片段将输入验证器设置为区号字段。它将在该字段中保持焦点,直到用户输入一个三位数的数字区号。如果字段为空,它允许用户导航到另一个字段。
// Create an area code JTextField
JTextField areaCodeField = new JTextField(3);
// Set an input verifier to the area code field
areaCodeField.setInputVerifier(new InputVerifier() {
public boolean verify(JComponent input) {
String areaCode = areaCodeField.getText();
if (areaCode.length() == 0) {
return true;
} else if (areaCode.length() != 3) {
return false;
}
try {
Integer.parseInt(areaCode);
return true;
}
catch(NumberFormatException e) {
return false;
}
}
});
您可以使用setInputVerifier()方法为任何JComponent设置输入验证器。通常,它仅用于文本字段。作为一个良好的 GUI 设计实践,您应该添加一些关于有效输入值的视觉提示,这样用户就可以理解字段中需要什么样的值。例如,您可能希望为“区号”字段添加一个带有文本“区号(三位数):”的标签,或者当用户在字段中输入无效值时显示一条错误消息。如果没有关于输入验证器字段的有效值的视觉线索,用户将被困在字段中,不知道输入哪种值。
做出选择
Swing 提供了以下组件,允许您从选项列表中进行选择:
JToggleButtonJCheckBoxJRadioButtonJComboBoxJList
可供从列表中选择的选项的数量可以从 2 到 N 变化,其中 N 是大于 2 的数。从选项列表中进行选择有不同的方法:
- 该选择可以是互斥的。也就是说,用户只能从选项列表中做出一个选择。在互斥选择中,如果用户更改当前选择,则会自动取消选择之前的选择。例如,
Male、Female、Unknown三个选项的性别选择列表是互斥的。用户只能选择三个选项中的一个,而不能同时选择两个或更多。 - 有一种特殊的选择情况,其中选择数 N 是 2。在这种情况下,选择类型为
boolean:true或false。有时它们也被称为Yes/No选择,或者On/Off选择。 - 有时,用户可以从选项列表中进行多项选择。例如,您可以向用户提供一个爱好列表,用户可以从列表中选择一个以上的爱好。
Swing 组件使您能够向用户呈现不同种类的选择,并让用户选择零个、一个或多个选项。图 2-13 显示了四个季节名称的秋千组件:Spring、Summer、Fall、Winter。该图显示了可用于从列表中选择选项的五种不同类型的 Swing 组件的外观。此图中显示的某些组件可能不适合它所显示的选项。例如,尽管可以使用一组复选框来显示互斥选项的列表,但这不是一个好的 GUI 实践。当选项相互排斥时,一组单选按钮被认为比一组复选框更合适。

图 2-13。
Swing components to make a selection from a list of choices
是一个双态按钮。这两种状态是选中和取消选中。当您按下切换按钮时,它会在按下和未按下之间切换。按下是其选中状态,未按下是其未选中状态。请注意,JButton与JToggleButton的工作方式和用法不同。一个JButton只有当鼠标按在它上面时才被按下,而一个JToggleButton在按下和未按下状态之间切换。一个JButton用于启动一个动作,而一个JToggleButton用于从可能的选项列表中选择一个选项。通常,一组JToggleButton用于让用户从互斥选项列表中选择一个选项。一个JToggleButton用于当用户有一个boolean选择时,他需要指示true或false(或者,是或否)。按下状态表示选择了true,未按下状态表示选择了false。
a 也有两种状态:选中和未选中。当用户可以从两个或更多选项的列表中选择零个或更多选项时,使用一组JCheckBox es。当用户有一个boolean选择来指示true或false时,使用一个JCheckBox。
a 也有两种状态:选中和未选中。当有两个或更多互斥选项的列表并且用户必须选择一个选项时,使用一组JRadioButton。一个JRadioButton永远不会作为一个独立的组件用于从两个boolean选项true和false中做出选择。它总是以两个或两个以上为一组使用。当你必须让用户在两个布尔选择true或false之间进行选择时,应该使用JCheckBox(不是JRadioButton)。
JToggleButton、JCheckBox和JRadioButton的构造函数允许您使用不同参数的组合来创建它们。您可以使用一个Action对象、一个字符串标签、一个图标和一个boolean标志(表示它是否被默认选中)的组合来创建它们。默认情况下,JToggleButton、JCheckBox和JRadioButton未选中。下面的代码片段展示了创建它们的一些方法:
// Create them with no label and no image
JToggleButton tb1 = new JToggleButton();
JCheckBox cb1 = new JCheckBox();
JRadioButton rb1 = new JRadioButton();
// Create them with text as "Multi-Lingual"
JToggleButton tb2 = new JToggleButton("Multi-Lingual");
JCheckBox cb2 = new JCheckBox("Multi-Lingual");
JRadioButton rb2 = new JRadioButton("Multi-Lingual");
// Create them with text as "Multi-Lingual" and selected by default
JToggleButton tb3 = new JToggleButton("Multi-Lingual", true);
JCheckBox cb3 = new JCheckBox("Multi-Lingual", true);
JRadioButton rb3 = new JRadioButton("Multi-Lingual", true);
要选择/取消选择一个JToggleButton、JCheckBox和JRadioButton,需要调用它们的setSelected()方法。要检查它们是否被选中,使用它们的isSelected()方法。以下代码片段显示了如何使用这些方法:
tb3.setSelected(true); // Select tb3
boolean b1 = tb3.isSelected(); // will store true in b1
tb3.setSelected(false); // Unselect tb3
boolean b2 = tb3.isSelected(); // will store false in b2
如果选择是互斥的,则必须将所有选择组合在一个按钮组中。在互斥的选项组中,如果您选择了一个选项,则所有其他选项都不会被选中。通常,您为一组互斥的JRadioButton或JToggleButton创建一个按钮组。理论上,您也可以为具有互斥选择的JCheckBox创建一个按钮组。但是,不建议在 GUI 中使用一组互斥的JCheckBox es。
类别的执行个体代表按钮群组。您可以分别通过使用按钮组的add()和remove()方法来添加和移除按钮组的JRadioButton或JToggleButton。最初,按钮组的所有成员都是未选中的。要形成一个按钮组,需要将所有互斥的选择组件添加到一个ButtonGroup类的对象中。您不能向容器添加(事实上,您不能添加)一个ButtonGroup对象。您必须将所有选项组件添加到容器中。清单 2-6 包含了显示一组三个互斥JRadioButton的完整代码。
清单 2-6。由三个 JRadioButtons 表示的一组互斥的三个选项
// ButtonGroupFrame.java
package com.jdojo.swing;
import java.awt.BorderLayout;
import java.awt.Container;
import javax.swing.Box;
import javax.swing.ButtonGroup;
import javax.swing.JFrame;
import javax.swing.JRadioButton;
public class ButtonGroupFrame extends JFrame {
ButtonGroup genderGroup = new ButtonGroup();
JRadioButton genderMale = new JRadioButton("Male");
JRadioButton genderFemale = new JRadioButton("Female");
JRadioButton genderUnknown = new JRadioButton("Unknown");
public ButtonGroupFrame() {
this.initFrame();
}
private void initFrame() {
this.setTitle("Mutually Exclusive JRadioButtons Group");
this.setDefaultCloseOperation(EXIT_ON_CLOSE);
// Add three gender JRadioButtons to a ButtonGroup,
// so they become mutually exclusive choices
genderGroup.add(genderMale);
genderGroup.add(genderFemale);
genderGroup.add(genderUnknown);
// Add gender radio button to a vertical Box
Box b1 = Box.createVerticalBox();
b1.add(genderMale);
b1.add(genderFemale);
b1.add(genderUnknown);
// Add the vertical box to the center of the frame
Container contentPane = this.getContentPane();
contentPane.add(b1, BorderLayout.CENTER);
}
public static void main(String[] args) {
ButtonGroupFrame bf = new ButtonGroupFrame();
bf.pack();
bf.setVisible(true);
}
}
是另一种类型的 Swing 组件,它允许您从选项列表中进行选择。或者,它可以包含一个可编辑字段,允许您键入新的选择值。类型参数E是它包含的元素的类型。当屏幕空间有限时,你可以用一个JComboBox代替一组JToggleButton、JCheckBox或JRadioButton。使用JComboBox可以节省屏幕空间。然而,用户必须执行两次点击来进行选择。首先,用户必须点击箭头按钮来显示下拉列表中的选项列表,然后他必须点击列表中的一个选项。用户还可以使用键盘上的上/下箭头键来滚动选项列表,并在组件处于焦点时选择一个选项。您可以通过在一个构造函数中传递选择列表来创建一个JComboBox,如下所示:
// Use an array of String as the list of choices
String[] sList = new String[]{"Spring", "Summer", "Fall", "Winter"};
JComboBox<String> seasons = new JComboBox<>(sList);
// Use a Vector of String as the list of choices
Vector<String> sList2 = new Vector<>(4);
sList2.add("Spring");
sList2.add("Summer");
sList2.add("Fall");
sList2.add("Winter");
JComboBox<String> seasons2 = new JComboBox<>(sList2);
您可以创建一个没有选择的JComboBox,然后通过使用它的一个方法向它添加选择。它还包括从列表中移除选择并获取所选选择的值的方法。表 2-14 显示了JComboBox类的常用方法列表。
表 2-14。
Commonly Used Methods of the JComboBox class
| 方法 | 描述 | | --- | --- | | `void addItem(E item)` | 将项目作为选项添加到列表中。对添加的对象调用`toString()`方法,返回的字符串显示为一个选项。 | | `E getItemAt(int index)` | 从选择列表中返回指定`index`处的项目。索引从零开始,到列表大小减一结束。如果指定的`index`越界,则返回`null`。 | | `int getItemCount()` | 返回选项列表中的项数。 | | `int getSelectedIndex()` | 返回选定项的索引。如果选定的项目不在列表中,则返回–1。请注意,对于可编辑的`JComboBox`,您可以在字段中键入一个新值,该值可能不在选项列表中。在这种情况下,该方法将返回–1。如果没有选择,它也返回–1。 | | `Object getSelectedItem()` | 返回当前选定的项目。如果没有选择,则返回`null`。 | | `void insertItemAt(E item, int index)` | 在列表中指定的`index`处插入指定的`item`。 | | `boolean isEditable()` | 如果`JComboBox`可编辑,则返回`true`。否则,它返回`false`。默认情况下,`JComboBox`是不可编辑的。 | | `void removeAllItems()` | 从列表中移除所有项目。 | | `void removeItem(Object item)` | 从列表中删除指定的`item`。 | | `void removeItemAt(int index)` | 移除指定`index`处的项目。 | | `void setEditable(boolean editable)` | 如果指定的`editable`参数是`true`,则`JComboBox`是可编辑的。否则,它是不可编辑的。用户可以在可编辑的`JComboBox`中键入一个值,这个值不在选择列表中。请注意,新键入的值不会添加到选项列表中。 | | `void setSelectedIndex(int index)` | 选择列表中指定`index`处的项目。如果指定的`index`为–1,则清除选择。如果指定的`index`小于-1 或者大于列表的大小减 1,它抛出一个`IllegalArgumentException`。 | | `void setSelectedItem(Object item)` | 选择字段中的项目。如果指定的`item`存在于列表中,它总是被选中。如果列表中不存在指定的项目,则仅当`JComboBox`可编辑时,该项目才会在字段中被选中。 |如果您想在JComboBox中选择或取消选择某个项目时得到通知,您可以为其添加一个项目监听器。每当选择或取消选择某个项目时,都会通知项目监听器。请注意,当您更改JComboBox中的选择时,它会触发取消选择的项目事件,随后是选择的事件。下面的代码片段展示了如何向JComboBox添加一个项目监听器。您可以使用ItemEvent类的getItem()方法来找出哪个项目被选中或取消选中。
String[] sList = new String[]{"Spring", "Summer", "Fall", "Winter"};
JComboBox<String> seasons = new JComboBox<>(sList);
// Add an item listener to the combobox
seasons.addItemListener((ItemEvent e) -> {
Object item = e.getItem();
if (e.getStateChange() == ItemEvent.SELECTED) {
// Item has been selected
System.out.println(item + " has been selected");
} else if (e.getStateChange() == ItemEvent.DESELECTED) {
// Item has been deselected
System.out.println(item + " has been deselected");
}
});
是另一个 Swing 组件,它显示一个选项列表,并允许您从该列表中选择一个或多个选项。类型参数T是它包含的元素的类型。一个JList与一个JComboBox的区别主要在于它显示选择列表的方式。一个JList可以在屏幕上显示多个选项,而一个JComboBox可以在你点击箭头按钮时显示选项列表。从这个意义上说,a JList是 a JComboBox的扩展版。一个JList可以在一列或多列中显示选择列表。您可以像创建JComboBox一样创建JList,如下所示:
// Create a JList using an array
String[] items = new String[]{"Spring", "Summer", "Fall", "Winter"};
JList<String> list = new JList<>(items);
// Create a JList using a Vector
Vector<String> items2 = new Vector<>(4);
items2.add("Spring");
items2.add("Summer");
items2.add("Fall");
items2.add("Winter");
JList<String> list2 = new JList<>(items2);
A JList不具备滚动能力。您必须将它添加到一个JScrollPane中,并将JScrollPane添加到容器中,以获得滚动功能,如下所示:
myContainer.add(new JScrollPane(myJList));
您可以配置JList的布局方向,以三种方式排列选项列表:
- 垂直的
- 水平环绕
- 垂直环绕
在默认的垂直排列中,JList中的所有项目都使用一列多行显示。
在水平包装中,所有项目排列成一行和多列。但是,如果一行中容纳不下所有项目,则需要添加新行来显示这些项目。请注意,根据组件的方向,该项可以从左到右或从右到左水平排列。
在垂直包装中,所有项目都排列在一列和多行中。但是,如果一列中容纳不下所有项目,则需要添加新列来显示它们。
您可以使用JList类的setVisibleRowCount(int visibleRows)方法来设置您希望在列表中看到的不需要滚动的可见行数。当您将可见行数设置为零或更小时,JList将根据字段的宽度/高度及其布局方向决定可见行数。您可以使用其setLayoutOrientation(int orientation)方法设置其布局方向,其中方向值可以是在JList类中定义的三个常量之一:JList.VERTICAL、JList.HORIZONTAL_WRAP和JList.VERTICAL_WRAP。
您可以使用setSelectionMode(int mode)方法配置JList的选择模式。模式值可以是以下三个值之一。模式值在ListSelectionModel界面中被定义为常量。
SINGLE_SELECTIONSINGLE_INTERVAL_SELECTIONMUTIPLE_INTERVAL_SELECTION
在单一选择模式下,一次只能选择一个项目。如果您更改选择,以前选择的项目将被取消选择。
在单个间隔选择模式中,您可以选择多个项目。但是,选定的项目必须总是连续的。假设您在一个JList中有十个项目,并且您选择了第七个项目。现在,您可以选择列表中的第六个或第八个项目,但不能选择任何其他项目。您可以继续选择更多连续的项目。您可以使用Ctrl键或Shift键和鼠标的组合进行连续选择。
在多间隔部分,您可以不受任何限制地选择多个项目。您可以使用 Ctrl 键或 Shift 键和鼠标的组合来进行选择。
您可以在JList中添加一个列表选择监听器,当选择发生变化时,它会通知您。当选择改变时,调用ListSelectionListener的valueChanged()方法。在一次选择更改过程中,也可能会多次调用此方法。您需要使用ListSelectionEvent对象的getValueIsAdjusting()方法来确保选择更改已经完成,如下面的代码片段所示:
myJList.addListSelectionListener((ListSelectionEvent e) -> {
// Make sure selection change is final
if (!e.getValueIsAdjusting()) {
// The selection changed logic goes here
}
});
表 2-15 列出了JList类的常用方法。注意,JList没有一个直接的方法来给出列表的大小(?? 中选择的数量)。由于每个 Swing 组件都使用一个模型,所以JList也是如此。它的模型是JListModel接口的一个实例。要知道一个JList的选择列表的大小,您需要调用它的模型的getSize()方法,就像这样:
int size = myJList.getModel().getSize();
表 2-15。
Commonly Used Methods of the JList Class
| 方法 | 描述 | | --- | --- | | `void clearSelection()` | 清除`JList`中的选择。 | | `void ensureIndexIsVisible(int index)` | 确保指定`index`处的项目可见。注意,要使不可见的项目可见,必须将`JList`添加到`JScrollPane`中。 | | `int getFirstVisibleIndex()` | 返回最小的可见索引。如果没有可见项目或列表为空,则返回–1。 | | `int getLastVisibleIndex()` | 返回最大的可见索引。如果没有可见项目或列表为空,则返回–1。 | | `int getMaxSelectionIndex()` | 返回最大的选定索引。如果没有选择,则返回–1。 | | `int getMinSelectionIndex()` | 返回最小的选定索引。如果没有选择,则返回–1。 | | `int getSelectedIndex()` | 返回最小的选定索引。如果`JList`选择模式为单选,则返回选中的索引。如果没有选择,则返回–1。 | | `int[] getSelectedIndices()` | 返回一个`int`数组中所有选定项目的索引。如果没有选择,数组将没有元素。 | | `E getSelectedValue()` | 返回第一个选定的项目。如果`JList`为单选模式,则为所选项的值。如果在`JList`中没有选择,则返回`null`。 | | `ListJSpinner
一个JSpinner组件结合了一个JFormattedTextField和一个可编辑的JComboBox的优点。它允许您在一个JComboBox中设置一个选择列表,同时,您还可以对显示的值应用一种格式。它一次只显示选项列表中的一个值。它允许您输入新值。“spinner”这个名字来源于这样一个事实,它允许您通过使用上下箭头按钮来上下旋转选项列表。在JSpinner中,选择列表的一个特别之处是它必须是一个有序列表。图 2-14 显示了三个用于选择数字、日期和季节值的 JSpinners。

图 2-14。
JSpinner components in action
因为一个JSpinner为各种选择列表提供了旋转能力,所以它在很大程度上依赖于它的创建模型。事实上,您必须在其构造函数中为JSpinner提供一个模型,除非您想要一个只有整数列表的简单的JSpinner。它支持三种不同的有序选择列表:数字列表、日期列表和任何其他对象列表。它提供了三个类来创建三种不同列表的模型:
- SpinnerNumberModel
- SpinnerDateModel
- SpinnerListModel
旋转器模型是接口的一个实例。它定义了使用JSpinner中的值的getValue()、setValue()、getPreviousValue()和getNextValue()方法。所有这些方法都与Object类的对象一起工作。
这个类为一个JSpinner提供了一个模型,可以让你浏览一个有序的数字列表。您需要在列表中指定最小值、最大值和当前值。当您使用JSpinner的向上/向下按钮时,您还可以指定步进数值,用于步进数字列表。下面的代码片段创建了一个包含从 1 到 10 的数字列表的JSpinner。它让你一步一步地浏览列表。该字段的当前值设置为 5。SpinnerNumberModel类也有一些方法,可以让您在创建 spinner 模型后获取/设置不同的值。
int minValue = 1;
int maxValue = 10;
int currentValue = 5;
int steps = 1;
SpinnerNumberModel nModel = new SpinnerNumberModel(currentValue, minValue, maxValue, steps);
JSpinner numberSpinner = new JSpinner(nModel);
这个类为一个JSpinner提供了一个模型,可以让你浏览一个有序的日期列表。您需要指定开始日期、结束日期、当前值和步骤。下面的代码片段创建了一个JSpinner来一次一天地遍历从 1950 年 1 月 1 日到 2050 年 12 月 31 日的日期列表。当前系统日期被设置为字段的当前值。
Calendar calendar = Calendar.getInstance();
calendar.set(1950, 1, 1);
Date minValue = calendar.getTime();
calendar.set(2050, 12, 31);
Date maxValue = calendar.getTime();
Date currentValue = new Date();
int steps = Calendar.DAY_OF_MONTH; // Must be a Calendar field
SpinnerDateModel dModel = new SpinnerDateModel(currentValue, minValue, maxValue, steps);
dateSpinner = new JSpinner(dModel);
请注意,日期值将以默认的区域设置格式显示。当在模型上使用getNextValue()方法时,使用步长值。带有日期列表的JSpinner可让您通过突出显示日期字段的一部分并使用向上/向下按钮来浏览任何显示的日期字段。假设你的JSpinner使用的日期格式是mm/dd/yyyy。您可以将光标放在字段的年份部分(yyyy),并使用上/下按钮根据年份浏览列表。
这个类为一个JSpinner提供了一个模型,可以让你在一个有序的对象列表中旋转。您只需指定一个对象数组或一个List对象,JSpinner将让您在列表出现在数组或List中时旋转列表。列表中对象的toString()方法返回的String显示为JSpinner中的值。下面的代码片段创建了一个JSpinner来显示四个季节的列表:
String[] seasons = new String[] {"Spring", "Summer", "Fall", "Winter"};
SpinnerListModel sModel = new SpinnerListModel(seasons);
listSpinner = new JSpinner(sModel);
一个JSpinner使用一个编辑器对象来显示当前值。它有以下三个内部类来显示三种不同的有序列表:
- JSpinner。数字编辑器
- JSpinner.DateEditor
- JSpinner。列表编辑器
如果您想以特定的格式显示数字或日期,您需要为JSpinner设置一个新的编辑器。数字和日期编辑器的编辑器类允许您指定格式。下面的代码片段将数字格式设置为“00”,因此数字 1 到 10 显示为01, 02, 03...10。它将日期格式设置为mm/dd/yyyy。
// Set the number format to "00"
JSpinner.NumberEditor nEditor = new JSpinner.NumberEditor(numberSpinner, "00");
numberSpinner.setEditor(nEditor);
// Set the date format to mm/dd/yyyy
JSpinner.DateEditor dEditor = new JSpinner.DateEditor(dateSpinner, "mm/dd/yyyy");
dateSpinner.setEditor(dEditor);
Tip
您可以使用JSpinner或SpinnerModel定义的getValue()方法来获取JSpinner中的当前值作为Object. SpinnerNumberModel,而SpinnerDateModel定义分别返回Number和Date对象的getNumber()和getDate()方法。
JScrollBar
如果您想要查看比可用空间更大的组件,您需要使用JScrollBar或JScrollPane组件。我将在下一节讨论JScrollPane。一个JScrollBar有一个方向属性,决定它是水平显示还是垂直显示。图 2-15 描绘了一个水平JScrollBar。

图 2-15。
A horizontal JScrollBar
一个JScrollBar由四部分组成:两个箭头按钮(每端一个)、一个旋钮(也称为拇指)和一个轨道。单击箭头按钮时,旋钮在轨道上向箭头按钮移动。你可以借助鼠标将旋钮向两端拖动。你也可以通过点击轨道来移动旋钮。
您可以定制一个JScrollBar的各种属性,方法是在构造函数中传递它们的值,或者在创建之后设置它们。表 2-16 列出了一些常用的属性和操作它们的方法。
表 2-16。
Commonly Used Properties of a JScrollBar and Methods to Get/Set Those Properties
| 财产 | 方法 | 描述 | | --- | --- | --- | | `Orientation` | `getOrientation()` `setOrientation()` | 确定`JScrollBar`是水平还是垂直。它的值可以是两个常量之一,`HORIZONTAL`或`VERTICAL`,它们在`JScrollBar`类中定义。 | | `Value` | `getValue()` `setValue()` | 旋钮的位置就是它的值。最初,它被设置为零。 | | `Extent` | `getVisibleAmount()` `setVisibleAmount()` | 这是旋钮的大小。它与轨道的大小成比例。例如,如果轨迹大小代表 150,而您将范围设置为 25,则旋钮大小将是轨迹大小的六分之一。其默认值为 10。 | | `Minimum Value` | `getMinimum()` `setMinimum()` | 它表示的最小值。默认值为零。 | | `Maximum Value` | `getMaximum()` `setMaximum()` | 它表示的最大值。默认值为 100。 |以下代码片段演示了如何创建具有不同属性的JScrollBar:
// Create a JScrollBar with all default properties. Its orientation
// will be vertical, current value 0, extent 10, minimum 0, and maximum 100
JScrollBar sb1 = new JScrollBar();
// Create a horizontal JScrollBar with default values
JScrollBar sb2 = new JScrollBar(JScrollBar.HORIZONTAL);
// Create a horizontal JScrollBar with a current value of 50,
// extent 15, minimum 1 and maximum 150
JScrollBar sb3 = new JScrollBar(JScrollBar.HORIZONTAL, 50, 15, 1, 150);
JScrollBar的当前值只能设置在其最小值和(最大范围)值之间。一个JScrollBar本身不会给 GUI 增加任何价值。它只有一些属性。您可以将一个AdjustmentListener添加到一个JScrollBar中,当它的值改变时会得到通知。
// Add an AdjustmentListener to a JScrollBar named myScrollBar
myScrollBar.addAdjustmentListener((AdjustmentEvent e) -> {
if (!e.getValueIsAdjusting()) {
// The logic for value changed goes here
}
});
使用一个JScrollBar来滚动一个尺寸大于其显示区域的组件并不简单。如果你想单独使用一个JScrollBar,你需要写大量的代码来完成这个任务。一个JScrollPane让这个任务变得更容易。它负责滚动,无需编写任何额外的代码。
JScrollPane
一个JScrollPane是一个最多可以容纳和展示九个组件的容器,如图 2-16 所示。它使用自己的布局管理器,它是类JScrollPaneLayout的一个对象。

图 2-16。
The components of a JScrollPane
一个JScrollPane管理的九个组件是两个JScrollBar、一个视口、一个行标题、一个列标题和四个角。
- 二:在图中,两个滚动条被命名为 HSB 和 VSB。它们是
JScrollBar类的两个实例:一个水平的,一个垂直的。一个JScrollPane将为您创建和管理两个JScrollBar。您不需要为此编写任何代码。你唯一需要指出的是你是否想要它们,以及你希望它们何时出现。 - 答:viewport 是一个区域,在这里一个
JScrollPane显示可滚动的组件,比如一个JTextArea。您可以将视口视为窥视孔,通过使用滚动条向上/向下和向右/向左滚动来查看组件。视口是一个 Swing 组件。一个JViewport类的对象代表一个视窗组件。一个JViewport只是一个 Swing 组件的包装器,用来实现该组件的可滚动视图。JScrollPane为组件创建一个JViewport对象,并在内部使用。 - 行和列标题:图中的行标题缩写为 RH。行/列标题是您可以在
JScrollPane中使用的两个可选视窗。使用水平滚动条时,列标题会随之水平滚动。使用垂直滚动条时,行标题会随之垂直滚动。行/列标题的一个很好的用途是在视口中显示图片或绘图的水平和垂直标尺。通常,不使用行/列标题。 - :一个
JScrollPane中可以存在四个角。当两个组件垂直相交时,存在一个角。图中的四个角分别是 C1、C2、C3 和 C4。这些不是JScrollPane给角取的名字。为了便于讨论,我给它们取了一个名字。如果添加行标题和列标题,则存在角 C1。如果添加列标题并且垂直滚动条可见,则角 C2 存在。如果添加行标题并且水平滚动条可见,则角 C3 存在。如果水平滚动条和垂直滚动条都可见,则存在 C4 角。您可以添加任何 Swing 组件作为角组件。唯一的限制是不能在多个角上添加相同的组件。请注意,添加角组件并不能保证它是可见的。仅当拐角根据所讨论的规则存在时,拐角组件才会在拐角中可见。例如,如果您为 C4 角添加了一个角组件,那么只有当水平和垂直滚动条都可见时,它才可见。如果滚动条中的一个或两个都不可见,则角 C4 不存在,并且为该角添加的组件将不可见。
当组件尺寸大于JScrollPane尺寸时,需要一个方向(水平或垂直)的滚动条来查看视窗中的组件。一个JScrollPane让你为垂直和水平滚动条设置滚动条策略。滚动条策略是控制滚动条何时出现的规则。您可以设置以下三种滚动条策略之一:
- :这意味着
JScrollPane应该在需要的时候显示滚动条。当视口中某个方向(水平或垂直)的组件大于其显示区域时,需要滚动条。由JScrollPane决定何时需要滚动条,如果需要,它将使滚动条可见。否则,它会使滚动条不可见。 - :这意味着
JScrollPane应该总是显示滚动条。 - :这意味着
JScrollPane不应该显示滚动条。
滚动条策略由ScrollPaneConstants界面中的六个常量定义。三个常量用于垂直滚动条,三个用于水平滚动条。JScrollPane类实现了ScrollPaneConstants接口。所以您也可以使用JScrollPane类来访问这些常量。定义滚动条策略的常量是XXX_SCROLLBAR_AS_NEEDED、XXX_SCROLLBAR_ALWAYS和XXX_SCROLLBAR_NEVER,其中您需要用VERTICAL或HORIZONTAL替换XXX,这取决于您所指的滚动条策略。垂直滚动条和水平滚动条的滚动条策略的默认值都是“按需显示”。下面的代码片段演示了如何用不同的选项创建一个JScrollPane:
// Create a JScrollPane with no component as its viewport and
// with default scrollbars policy as "As Needed"
JScrollPane sp1 = new JScrollPane();
// Create a JScrollPane with a JTextArea as its viewport and
// with default scrollbars policy as "As Needed"
JTextArea description = new JTextArea(10, 60);
JScrollPane sp2 = new JScrollPane(description);
// Create a JScrollPane with a JTextArea as its viewport and
// both scrollbars policy set to "show always"
JTextArea comments = new JTextArea(10, 60);
JScrollPane sp3 = new JScrollPane(comments,
JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);
如前所述,当您将组件添加到JScrollPane时,您将JScrollPane添加到容器,而不是组件。一个JScrollPane的视口保持对你添加到JScrollPane的组件的引用。通过查询其视窗,您可以在JScrollPane中获得组件的引用,如下所示:
// Get the reference to the viewport of the JScrollPane sp3
JViewport vp = sp3.getViewport();
// Get the reference to the comments JTextArea added
// to the JScrollPane, sp3, using its viewport reference
JTextArea comments1 = (JTextArea)vp.getView();
如果您创建一个JScrollPane而没有为其视口指定组件,您可以稍后使用其setViewportView()方法将组件添加到其视口,如下所示:
// Set a JTextPane as the viewport component for sp3
sp3.setViewportView(new JTextPane());
进程条
一个JProgressBar用于显示任务的进度。它有一个方向,可以是水平的也可以是垂直的。它有三个相关的值:当前值、最小值和最大值。您可以创建一个进度条,如下所示:
// Create a horizontal progress bar with current, minimum, and maximum values
// set to 0, 0, and 100, respectively.
JProgressBar hpBar1 = new JProgressBar();
// Create a horizontal progress bar with current, minimum, and maximum values
// set to 20, 20, and 200, respectively.
JProgressBar hpbar2 = new JProgressBar(SwingConstants.HORIZONTAL, 20, 200);
// Create a vertical progress bar with current, minimum, and maximum values
// set to 5, 5 and 50, respectively.
JProgressBar vpBar1 = new JProgressBar(SwingConstants.VERTICAL, 5, 50);
随着任务的进展,您需要使用进度条的setValue(int value)方法来设置进度条的当前值,以指示进度。组件将自动更新以反映新值。根据应用的外观和感觉,进度会有不同的反映。有时实心条用于显示进度,有时实心矩形用于显示进度。您可以使用getValue()方法来获取当前值。
您还可以使用setStringPainted()方法显示一个描述进度条当前值的字符串。向该方法传递true会显示字符串值,传递false不会显示字符串值。要绘制的字符串通过调用setString(String s)方法来指定。
有时任务进度的当前值是未知的或不确定的。在这种情况下,您不能设置进度条的当前值。相反,您可以向用户表明任务正在执行。您可以使用其setIndeterminate()方法在不确定模式下设置进度条。向该方法传递true会将进度条置于不确定的模式,传递false会将进度条置于确定的模式。一个JProgressBar组件显示一个动画来指示它的不确定状态。
图 2-17 显示了一个带有两个JProgressBar的JFrame,水平JProgressBar处于确定模式,它显示一个字符串来描述进度。垂直JProgressBar已被置于不确定模式;请注意中间显示为动画的实心矩形条。

图 2-17。
JProgressBars in action
杰利德
一个JSlider可以让你通过沿轨道滑动旋钮从两个整数之间的一组值中选择一个值。它有四个重要的属性:方向、最小值、最大值和当前值。方向决定了它是水平显示还是垂直显示。您可以使用SwingConstants.VERTICAL和SwingConstants.HORIZONTAL作为其方向的有效值。以下代码片段创建了一个水平JSlider,最小值为 0,最大值为 10,当前值设置为 5:
JSlider points = new JSlider(0, 10, 5);
您可以使用getValue()方法获得JSlider的当前值。通常,用户通过左右滑动水平JSlider旋钮和上下滑动垂直JSlider旋钮来设置JSlider的当前值。您也可以通过使用它的setValue(int value)方法以编程方式设置它的值。
您可以在JSlider上显示次要和主要刻度。您需要设置这些刻度需要显示的间隔,并调用其方法来启用刻度画,如下所示:
points.setMinorTickSpacing(1);
points.setMajorTickSpacing(2);
points.setPaintTicks(true);
您也可以在JSlider中显示显示轨道值的标签。您可以显示标准标签或自定义标签。标准标签将沿着轨迹显示整数值。您可以调用它的setPaintLabels(true)方法来显示主要刻度间距处的整数值。图 2-18 显示了一个带有刻度和标准标签的JSlider。

图 2-18。
A JSlider component with minimum = 0, maximum = 10, current value = 5, minor tick spacing = 1, major tick spacing = 2, tick painting enabled, and showing standard labels
JSlider还允许您设置自定义标签。使用JLabel组件显示JSlider上的标签。您需要创建一个带有value-label对的Hashtable,并使用它的setLabelTable()方法来设置标签。一个value-label副由一个Integer-JLabel副组成。下面的代码片段为值0设置标签Poor,为值5设置标签Average,为值10设置标签Excellent。设置标签表不会显示标签。您必须调用setPaintLabels(true)方法来显示它们。图 2-19 显示了由以下代码片段生成的带有自定义标签的JSlider:
// Create the value-label pairs in a Hashtable
Hashtable labelTable = new Hashtable();
labelTable.put(new Integer(0), new JLabel("Poor"));
labelTable.put(new Integer(5), new JLabel("Average"));
labelTable.put(new Integer(10), new JLabel("Excellent"));
// Set the labels for the JSlider and make them visible
points.setLabelTable(labelTable);
points.setPaintLabels(true);

图 2-19。
A JSlider with custom labels
JSeparator
当您想要在两个组件或两组组件之间添加分隔符时,JSeparator是一个方便的组件。通常,菜单中使用一个JSeparator来分隔相关菜单项的组。您可以通过指定方向来创建一个水平或垂直的JSeparator。您可以在任何需要使用 Swing 组件的地方使用它。
// Create a horizontal separator
JSeparator hs = new JSeparator(); // By default, the type is horizontal
// Create a vertical separator
JSeparator vs = new JSeparator(SwingConstants.VERTICAL);
一个JSeparator将自己扩展以填充布局管理器提供的尺寸。您可以使用setOrientation()和getOrientation()方法来设置和获取JSeparator的方向。
菜单
菜单组件用于以紧凑的形式向用户提供动作列表。您还可以通过使用一组JButton来提供动作列表,其中每个JButton代表一个动作。使用一个菜单或一组JButton来呈现一个动作列表是一个偏好问题。然而,使用菜单有一个明显的优势;与一组JButton相比,它占用的屏幕空间要少得多。通过将一组选项折叠(或嵌套)在另一个选项下,菜单占用的空间更少。例如,如果您使用的是文件编辑器,那么New、Open、Save和Print等选项会嵌套在顶层File菜单选项下。用户需要点击File菜单来查看其下可用的选项列表。典型地,在一组JButton的情况下,所有的JButton对用户来说一直是可见的,并且用户很容易知道哪些动作是可用的。因此,当您决定使用菜单或JButton s 时,需要在空间和可用性之间进行权衡。
还有一种叫做 a 的菜单,它根本不占用屏幕上的任何空间。通常,它会在用户单击鼠标右键时显示。一旦用户做出选择或在显示的弹出菜单区域之外单击鼠标,它就会消失。这是一个超级紧凑的菜单组件。然而,这使得用户很难知道有任何选项可用。有时,屏幕上会显示一条文本消息,说明用户需要右键单击来查看可用选项的列表。JPopupMenu类的一个对象表示 Swing 中的一个弹出菜单。现在让我们看看菜单的作用。
创建菜单并将其添加到JFrame是一个多步骤的过程。以下步骤详细描述了该过程。
创建该类的一个对象,并使用其setJMenuBar()方法将其添加到一个JFrame中。一个JMenuBar是一个空容器,它将保存一个菜单选项列表,一个JMenuBar中的每个选项代表一个选项列表。
// Create a JMenuBar and set it to a JFrame
JMenuBar menuBar = new JMenuBar();
myFrame.setJMenuBar(menuBar);
此时,您有一个空的JMenuBar与一个JFrame相关联。现在,您需要向JMenuBar添加选项列表,也称为顶级菜单选项。类别的物件代表选项清单。一个JMenu也是一个空容器,可以保存代表选项的菜单项。您将需要添加一个JMenu菜单选项。JMenu并不总是显示添加到其中的选项。相反,它会在用户选择JMenu时显示它们。当你使用菜单时,这是你得到紧凑的地方。当您选择一个JMenu时,它会弹出一个窗口,显示其中包含的选项。一旦您从弹出窗口中选择一个选项或点击JMenu外的某处,弹出窗口就会消失。
// Create two JMenu (or two top-level menu options):
// File and Help, and add them to the JMenuBar
JMenu fileMenu = new JMenu("File");
JMenu helpMenu = new JMenu("Help");
menuBar.add(fileMenu);
menuBar.add(helpMenu);
此时,你的JFrame会在顶部区域显示一个菜单栏,有两个选项叫做File和Help,如图 2-20 所示。如果您选择或点击File或Help,此时不会有任何反应。

图 2-20。
A JMenuBar With Two JMenu Options
让我们给你的JMenu添加一些选项。您想在File下显示三个菜单选项,它们是New、Open和Exit。您想要在Open和Exit选项之间添加一个分隔符(一条水平线作为分隔符)。该类的一个对象代表了一个JMenu中的选项。
// Create menu items
JMenuItem newMenuItem = new JMenuItem("New");
JMenuItem openMenuItem = new JMenuItem("Open");
JMenuItem exitMenuItem = new JMenuItem("Exit");
// Add menu items and a separator to the menu
fileMenu.add(newMenuItem);
fileMenu.add(openMenuItem);
fileMenu.addSeparator();
fileMenu.add(exitMenuItem);
此时,您已经向File菜单添加了三个JMenuItem。当您点击File菜单时,会显示如图 2-21 所示的选项。您可以使用键盘上的向下/向上箭头键滚动浏览File菜单下的选项,或者使用鼠标选择其中一个选项。当您选择File菜单下的任何一个选项时,什么都不会发生,因为您没有给它们添加任何动作。

图 2-21。
A File JMenu with three options
你可能想在一个菜单项下有两个子选项,比如在New选项下。也就是说,用户可以创建两个不同的东西,Policy和Claim,并且您希望这两个选项在New选项下可用。您没有尝试在选项中嵌套选项。File菜单是JMenu类的一个实例,它代表一个选项列表,并且您想要添加一个New菜单,它也应该显示一个选项列表。你可以很容易地做到这一点。您唯一需要理解的是,JMenu代表一个选项列表,而JMenuItem只代表一个选项。您可以在JMenu上添加一个JMenuItem或JMenu。为此,您需要对前面显示的代码片段做一点修改。现在New菜单将是JMenu类的一个实例,而不是JMenuItem类。您将向New菜单添加两个 JMenuItems。下面的代码片段将完成这项工作:
// New is a JMenu – a list of options
JMenu newMenu = new JMenu("New");
JMenuItem policyMenuItem = new JMenuItem("Policy");
JMenuItem claimMenuItem = new JMenuItem("Claim");
newMenu.add(policyMenuItem);
newMenu.add(claimMenuItem);
JMenuItem openMenuItem = new JMenuItem("Open");
JMenuItem exitMenuItem = new JMenuItem("Exit");
fileMenu.add(newMenu);
fileMenu.add(openMenuItem);
fileMenu.addSeparator();
fileMenu.add(exitMenuItem);
现在菜单显示如图 2-22 所示。当您选择File菜单时,New菜单旁边会显示一个箭头,表示它有子菜单。当您选择New菜单时,会显示两个标有Policy和Claim的子菜单。

图 2-22。
Nesting menus
一个菜单可以嵌套的层数没有限制。然而,两层以上的嵌套被认为不是好的 GUI 实践,因为用户将不得不向下钻取几层才能得到可用的选项。
让菜单工作的最后一步是向菜单项添加动作。您可以向JMenuItem添加动作监听器。当用户选择JMenuItem时,相关的动作监听器会得到通知。下面的代码片段向将退出应用的Exit菜单项添加了一个动作监听器:
// Add an action listener to the Exit menu item
exitMenuItem.addActionListener(e -> System.exit(0));
现在,您已经向 Exit 菜单项添加了一个操作。如果选择它,应用将退出。类似地,您可以向其他菜单项添加操作侦听器,以便在它们被选中时执行操作。
您可以使用setEnabled()方法启用/禁用菜单。尽管可以使菜单可见/不可见,但这样做并不是好的做法。这使得用户很难学习应用。如果您始终保持所有菜单选项可用(处于启用或禁用状态),用户将能够通过了解菜单选项的位置来更快地使用应用。如果你使菜单选项可见/不可见,菜单选项的位置会不断变化,用户每次想使用它们时都必须更加注意菜单选项的位置。
您也可以为菜单选项指定快捷方式。您可以使用setMnemonic()方法通过指定快捷键来添加菜单项的快捷方式。您可以通过按下Alt键和快捷键的组合来调用该菜单项所代表的动作。请注意,菜单项必须可见,其助记键才能起作用。例如,如果您为New菜单选项设置了助记符(N键),您必须选择File菜单,使New菜单选项可见,并按Alt + N调用由New菜单项表示的动作。
如果您想调用菜单项的相关动作,而不管它是否可见,您需要使用setAccelerator()方法来设置它的快捷键。以下代码片段将E键设置为助记键,将Ctrl + E设置为Exit菜单选项的快捷键:
// Set E as mnemonic for Exit menu and Ctrl + E as its accelerator
exitMenuItem.setMnemonic(KeyEvent.VK_E);
KeyStroke cntrlEKey = KeyStroke.getKeyStroke(KeyEvent.VK_E, ActionEvent.CTRL_MASK);
exitMenuItem.setAccelerator(cntrlEKey);
现在你可以通过两种方式调用Exit菜单选项:当Alt + E组合键可见时你可以按下它,或者你可以随时按下Ctrl + E组合键。
您可以使用弹出菜单,该菜单会根据需要显示。弹出菜单的创建类似于JMenu。您需要创建一个JPopupMenu类的实例,它代表一个空的弹出菜单容器,然后向其中添加JMenuItem的实例。您也可以在弹出菜单中包含嵌套菜单,就像在JMenu中一样。
// Create a popup menu
JPopupMenu popupMenu = new JPopupMenu();
// Create three menu items for our popup menu
JMenuItem popup1 = new JMenuItem("Poupup1");
JMenuItem popup2 = new JMenuItem("Poupup2");
JMenuItem popup3 = new JMenuItem("Poupup3");
// Add menu items to the popup menu
popupMenu.add(popup1);
popupMenu.add(popup2);
popupMenu.add(popup3);
由于弹出菜单没有固定的位置,并且是按需显示的,所以您需要知道在哪里以及何时显示它。您需要使用它的show()方法在某个位置显示它。show()方法有三个参数:其空间将用于显示弹出菜单的 invoker 组件,以及它将显示的 invoker 组件上的 x 和 y 坐标。
// Display the popup menu
popupMenu.show(myComponent, xPos, yPos);
通常,当用户单击鼠标右键时,会显示一个弹出菜单。不同的外观和感觉选项使用不同的按键事件来显示弹出菜单。例如,一个外观方案在释放鼠标右键时显示它,而另一个外观方案在按下鼠标右键时显示它。Swing 通过在MouseEvent类中提供一个isPopupTrigger()方法,让您可以轻松地显示弹出菜单。在鼠标按下或释放事件中,您需要调用此方法。如果该方法返回true,显示弹出菜单。以下代码片段将鼠标侦听器与组件相关联,并显示弹出菜单:
// Create a mouse listener
MouseListener ml = new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
if (e.isPopupTrigger()) {
popupMenu.show(e.getComponent(), e.getX(), e.getY());
}
}
@Override
public void mouseReleased(MouseEvent e) {
if (e.isPopupTrigger()) {
popupMenu.show(e.getComponent(), e.getX(), e.getY());
}
}
};
// Add a mouse listener to myComponent
myComponent.addMouseListener(ml);
每当用户右击myComponent,就会出现一个弹出菜单。注意,您需要在mousePressed()和mouseReleased()方法中添加相同的代码。它由外观和感觉决定哪个事件应该显示弹出菜单。
清单 2-7 包含了一个显示如何使用菜单的完整程序。节目很长。它执行创建和添加菜单项以及向菜单项添加动作监听器的重复性工作。
清单 2-7。使用菜单和弹出菜单
// JMenuFrame.java
package com.jdojo.swing;
import javax.swing.JFrame;
import java.awt.Container;
import javax.swing.JMenuBar;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JLabel;
import java.awt.event.ActionListener;
import javax.swing.JTextArea;
import java.awt.BorderLayout;
import java.awt.event.KeyEvent;
import javax.swing.KeyStroke;
import javax.swing.JPopupMenu;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import javax.swing.JScrollPane;
public class JMenuFrame extends JFrame {
JLabel msgLabel = new JLabel("Right click to see popup menu");
JTextArea msgText = new JTextArea(10, 60);
JPopupMenu popupMenu = new JPopupMenu();
public JMenuFrame(String title) {
super(title);
initFrame();
}
// Initialize the JFrame and add components to it
private void initFrame() {
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
Container contentPane = this.getContentPane();
// Add the message label and text area
contentPane.add(new JScrollPane(msgText), BorderLayout.CENTER);
contentPane.add(msgLabel, BorderLayout.SOUTH);
// Set the menu bar for the frame
JMenuBar menuBar = getCustomMenuBar();
this.setJMenuBar(menuBar);
// Create a popup menu and add a mouse listener to show it
createPopupMenu();
}
private JMenuBar getCustomMenuBar() {
JMenuBar menuBar = new JMenuBar();
// Get the File and Help menus
JMenu fileMenu = getFileMenu();
JMenu helpMenu = getHelpMenu();
// Add the File and Help menus to the menu bar
menuBar.add(fileMenu);
menuBar.add(helpMenu);
return menuBar;
}
private JMenu getFileMenu() {
JMenu fileMenu = new JMenu("File");
// Set Alt-F as mnemonic for the File menu
fileMenu.setMnemonic(KeyEvent.VK_F);
// Prepare a New Menu item. It will have sub menus
JMenu newMenu = getNewMenu();
fileMenu.add(newMenu);
JMenuItem openMenuItem = new JMenuItem("Open", KeyEvent.VK_O);
JMenuItem exitMenuItem = new JMenuItem("Exit", KeyEvent.VK_E);
fileMenu.add(openMenuItem);
// You can add a JSeparator or just call the convenience method
// addSeparator() on fileMenu. You can replace the following statement
// with fileMenu.add(new JSeparator());
fileMenu.addSeparator();
fileMenu.add(exitMenuItem);
// Add an ActionListener to the Exit menu item
exitMenuItem.addActionListener(e -> System.exit(0));
return fileMenu;
}
private JMenu getNewMenu() {
// New menu will have two sub menus - Policy and Claim
JMenu newMenu = new JMenu("New");
// Add submenus to New menu
JMenuItem policyMenuItem = new JMenuItem("Policy", KeyEvent.VK_P);
JMenuItem claimMenuItem = new JMenuItem("Claim", KeyEvent.VK_C);
newMenu.add(policyMenuItem);
newMenu.add(claimMenuItem);
return newMenu;
}
private JMenu getHelpMenu() {
JMenu helpMenu = new JMenu("Help");
helpMenu.setMnemonic(KeyEvent.VK_H);
JMenuItem indexMenuItem = new JMenuItem("Index", KeyEvent.VK_I);
JMenuItem aboutMenuItem = new JMenuItem("About", KeyEvent.VK_A);
// Set F1 as the accelerator key for the Index menu item
KeyStroke f1Key = KeyStroke.getKeyStroke(KeyEvent.VK_F1, 0);
indexMenuItem.setAccelerator(f1Key);
helpMenu.add(indexMenuItem);
helpMenu.addSeparator();
helpMenu.add(aboutMenuItem);
// Add an action listener to the index menu item
indexMenuItem.addActionListener(e ->
msgText.append("You have selected Help >>Index menu item.\n"));
return helpMenu;
}
private void createPopupMenu() {
// Create a popup menu and add a mouse listener to the frame,
// so a popup menu is displayed when the user clicks a right mouse button
JMenuItem popup1 = new JMenuItem("Popup1");
JMenuItem popup2 = new JMenuItem("Popup2");
JMenuItem popup3 = new JMenuItem("Popup3");
// Create an action listener
ActionListener al = e -> {
JMenuItem menuItem = (JMenuItem)e.getSource();
String menuText = menuItem.getText();
String msg = "You clicked " + menuText + " menu item.\n";
msgText.append(msg);
};
// Add the same action listener to all popup menu items
popup1.addActionListener(al);
popup2.addActionListener(al);
popup3.addActionListener(al);
// Add menu items to popup menu
popupMenu.add(popup1);
popupMenu.add(popup2);
popupMenu.add(popup3);
// Create a mouse listener to show a popup menu
MouseListener ml = new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
displayPopupMenu(e);
}
@Override
public void mouseReleased(MouseEvent e) {
displayPopupMenu(e);
}
};
// Add a mouse listener to the msg text and label
msgText.addMouseListener(ml);
msgLabel.addMouseListener(ml);
}
private void displayPopupMenu(MouseEvent e) {
// Make sure this mouse event is supposed to show the popup menu.
// Different platforms show the popup menu in different mouse events
if (e.isPopupTrigger()) {
this.popupMenu.show(e.getComponent(), e.getX(), e.getY());
}
}
// Display the CustomFrame
public static void main(String[] args) {
JMenuFrame frame = new JMenuFrame("JMenu and JPopupMenu Test");
frame.pack();
frame.setVisible(true);
}
}
您也可以使用JRadioButtonMenuItem和JCheckBoxMenuItem作为菜单中的菜单项。顾名思义,它们显示为单选按钮和复选框,其工作原理与单选按钮和复选框相同。您可以向JMenu添加任何 swing 组件。要使用单选按钮类型的菜单项,您需要将多个JRadioButtonMenuItem组件分组到一个按钮组中,以便它们代表唯一的选择。要处理单选按钮选择的更改,您可以在JRadioButtonMenuItem中添加一个ActionListener或ItemListener。要处理JCheckBoxMenuItem中的状态变化,您需要使用一个ItemListener。
Tip
我将最终揭示 Swing 中菜单的秘密。Swing 中的菜单项是一个按钮。啊哈!你用按钮工作,称它们为菜单。是的,这是正确的。一个JMenuBar和一个JPopupMenu只是带有一个BoxLayout的容器。继续操作这些容器,设置它们的属性并向它们添加不同的 Swing 组件。一个JMenuItem是一个简单的按钮。一个JMenu是一个按钮,它有一个关联的容器,当你选择它时就会显示出来。
JToolBar
工具栏是一组按钮,在JFrame中为用户提供常用的操作。通常,您会提供一个工具栏和一个菜单。工具栏包含带有小图标的小按钮。通常,它只包含菜单中可用选项的子集。
JToolBar类的一个对象代表一个工具栏。它充当工具栏按钮的容器。它是一个比其他容器更智能的容器,比如一个JPanel。它可以在运行时移动。它可以是漂浮的。如果它是浮动的,它会显示一个手柄,您可以使用它来移动它。您也可以使用句柄在单独的窗口中弹出它。以下代码片段创建了一些工具栏组件:
// Create a horizontal JToolBar
JToolBar toolBar = new JToolBar();
// Create a horizontal JToolBar with a title. The title is
// displayed as a window title, when it floats in a separate window.
JToolBar toolBarWithTitle = new JToolBar("My ToolBar Title");
// Create a Vertical toolbar
JToolBar vToolBar = new JToolBar(JToolBar.VERTICAL);
让我们给工具栏添加一些按钮。工具栏中的按钮需要比普通按钮小。通过将边距设置为零,可以缩小JButton的尺寸。您还应该为每个工具栏按钮添加一个工具提示,为用户提供有关其用法的快速提示。
// Create a button for the toolbar
JButton newButton = new JButton("New");
// Set the margins to 0 to make the button smaller
newButton.setMargin(new Insets(0, 0, 0, 0));
// Set a tooltip for the button
newButton.setToolTipText("Add a new policy");
// Add the New button to the toolbar
toolBar.add(newButton);
通常,在工具栏按钮中只显示小图标。您可以使用JButton的另一个构造函数,它只接受一个Icon对象作为参数。最后,您需要向按钮添加动作监听器,就像您已经向其他 JButtons 添加的那样。当用户单击工具栏中的按钮时,会通知操作监听器,并执行指定的操作。
您可以使用它的setFloatable(boolean floatable)方法设置工具栏浮动/不浮动。默认情况下,工具栏是可浮动的。它的setRollover(boolean rollOver)方法可以让你指定是否只在鼠标停留在工具栏按钮上时才绘制它们的边框。
应该在BorderLayout的北、南、东或西区域添加一个工具栏,以便在不同的区域移动工具栏。清单 2-8 在一个JFrame中显示了一个JToolBar。图 2-23 显示了一个JFrame在其北部区域有一个工具栏。图 2-24 显示了工具栏浮动在独立窗口中的同一JFrame。
清单 2-8。在 JFrame 中使用 JToolBar
// JToolBarFrame.java
package com.jdojo.swing;
import java.awt.Container;
import javax.swing.JFrame;
import javax.swing.JToolBar;
import javax.swing.JButton;
import java.awt.Insets;
import java.awt.BorderLayout;
import javax.swing.JTextArea;
import javax.swing.JScrollPane;
public class JToolBarFrame extends JFrame {
JToolBar toolBar = new JToolBar("My JToolBar");
JTextArea msgText = new JTextArea(3, 45);
public JToolBarFrame(String title) {
super(title);
initFrame();
}
// Initialize the JFrame and add components to it
private void initFrame() {
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
Container contentPane = this.getContentPane();
prepareToolBar();
// Add the toolbar in the north and a JTextArea in the center
contentPane.add(toolBar, BorderLayout.NORTH);
contentPane.add(new JScrollPane(msgText), BorderLayout.CENTER);
msgText.append("Move the toolbar around using its" +
" handle at the left end");
}
private void prepareToolBar() {
Insets zeroInset = new Insets(0, 0, 0, 0);
JButton newButton = new JButton("New");
newButton.setMargin(zeroInset);
newButton.setToolTipText("Add a new policy");
JButton openButton = new JButton("Open");
openButton.setMargin(zeroInset);
openButton.setToolTipText("Open a policy");
JButton exitButton = new JButton("Exit");
exitButton.setMargin(zeroInset);
exitButton.setToolTipText("Exit the application");
// Add an action listener to the Exit toolbar button
exitButton.addActionListener(e -> System.exit(0));
toolBar.add(newButton);
toolBar.add(openButton);
toolBar.addSeparator();
toolBar.add(exitButton);
toolBar.setRollover(true);
}
// Display the frame
public static void main(String[] args) {
JToolBarFrame frame = new JToolBarFrame("JToolBar Test");
frame.pack();
frame.setVisible(true);
}
}

图 2-24。
A JToolBar floating in a separate window

图 2-23。
A JToolBar with three JButtons placed in the north region of a JFrame
JToolBar 符合动作接口
三个组件:JButton、JMenuItem和JToolBar中的一个项目有什么共同点?它们都代表一个动作。有时你给用户同样的选项,作为一个菜单项,作为一个工具栏项,作为一个JButton。如何禁用您使用三个组件提供的选项?难道您不认为您需要至少在三个地方分别禁用它们,因为它们是代表同一选项的三个不同的组件吗?你可能是对的。但是,在 Swing 中有一种更简单的方法来处理这种情况。每当你必须以不同的方式为一个动作提供选项时,你应该使用Action接口。您需要将选项的逻辑和属性包装在一个Action对象中,并使用该对象构建工具栏中的JButton、JMenuItem和项目。如果需要禁用选项,只需在Action对象上调用setEnabled(false)一次,所有选项都会被禁用。在这种情况下,使用一个Action对象会让你的编程生活更容易。让我们看看它的实际效果。让我们创建一个继承自AbstractAction类的ExitAction类。它的actionPerformed()方法简单地退出应用。您使用它的putValue()方法在它的构造函数中设置一些属性,如下所示:
public class ExitAction extends AbstractAction {
public ExitAction(String action) {
super(action);
// Set tooltip text for the toolbar
this.putValue(SHORT_DESCRIPTION, "Exit the application");
// Set a mnemonic key
this.putValue(MNEMONIC_KEY, KeyEvent.VK_E);
}
@Override
public void actionPerformed(ActionEvent e) {
System.exit(0);
}
}
如果你想添加一个Exit菜单项、一个JButton和一个工具栏按钮,你可以先创建一个ExitAction类的对象,然后用它来创建你所有的选项项,如下所示:
ExitAction exitAction = new ExitAction("Exit");
JButton exitButton = new JButton(ExitAction);
JMenuItem exitMenuItem = new JMenuItem(exitAction);
JButton exitToolBarButton = new JButton(exitAction);
exitToolBarButton.setMargin(new Insets(0,0,0,0));
现在你可以将exitButton添加到你的JFrame,将exitMenuItem添加到你的菜单,将exitToolBarButton添加到你的工具栏。它们的行为都一样,因为它们共享同一个exitAction对象。如果您想在所有三个地方禁用退出选项,只需调用一次exitAction.setEnabled(false)即可。
组建
Swing 允许您使用JTable组件以表格形式显示和编辑数据。一个JTable使用行和列显示数据。您可以设置列标题的标签。您还可以在运行时对表中的数据进行排序。使用JTable可以简单到写几行代码,也可以复杂到写几百行代码。一个JTable是一个复杂而强大的 Swing 组件,它本身就值得一章。本节解释了使用JTable的基本知识,并为您提供了一些关于其强大功能的提示。一个JTable使用了许多其他的类和接口,这些都在javax.swing.table包中。JTable类本身在javax.swing包中。
先说最简单的JTable例子。您可以通过使用它的无参数构造函数来创建一个JTable。
JTable table = new JTable();
嗯,那很简单。但是,它的列、行和数据会怎么样呢?你得到的只是一张没有可视组件的空桌子。您将在一分钟内解决这些问题。
A JTable不存储数据。它只显示数据。它使用存储数据、列数和行数的模型。接口的一个实例代表了一个 ?? 的模型。DefaultTableModel类是TableModel接口的一个实现。当您使用JTable类的默认构造函数时,Java 会将DefaultTableModel类的一个实例设置为它的模型。如果要添加或删除列/行,必须使用其模型。你可以使用它的getModel()方法得到一个JTable的模型的引用。让我们在表格中添加两行和三列。
// Get the reference of the model of the table
DefaultTableModel tableModel = (DefaultTableModel)table.getModel();
// Set the number of rows to 2
tableModel.setRowCount(2);
// Set the number of columns to 3
tableModel.setColumnCount(3);
让我们为表格中的一个单元格设置值。您可以使用表格模型或表格的setValueAt(Object data, int row, int column)方法来设置其单元格中的值。您将设置“John Jacobs”作为第一行和第一列的值。请注意,第一行和第一列从 0 开始。
// Set the value at (0, 0) in the table's model
tableModel.setValueAt("John Jacobs", 0, 0);
// Set the value at (0, 0) in the table
// Works the same as setting the value using the table's model
table.setValueAt("John Jacobs", 0, 0);
如果您将表格添加到容器中,它将如图 2-25 所示。

图 2-25。
A JTable with two rows and three columns with default column header labels
确保将表格添加到一个JScrollPane中。请注意,您会得到两行三列。列标题的标签设置为 A、B 和 c。您可以双击任何单元格开始编辑单元格中的值。要获取单元格中包含的值,可以使用表格模型的getValueAt(int row, int column)方法或JTable。它返回一个Object。您还可以通过使用DefaultTableModel类的addColumn()和addRow()方法向JTable添加更多的列或行。您可以使用 its model 类的removeRow(int row)方法从模型中删除一行,从而从。
您可以使用模型的setColumnIdentifiers()方法为列标题设置定制标签,如下所示:
// Store the column headers in an array
Object[] columnHeaderLabels = new Object[]{"Name", "DOB", "Gender"};
// Set the column headers for the table using its model
tableModel.setColumnIdentifiers(columnHeaderLabels);
使用自定义列标题,表格看起来如图 2-26 所示。

图 2-26。
A JTable with two rows, three columns, and custom column header labels
如果您希望列标题始终可见,您必须在JScrollPane中添加一个JTable。如果不将其添加到JScrollPane,当行数超过组件的可用高度时,列标题将不可见。您可以使用JTable的方法获取列标题组件并自己显示(例如,如果JTable在中心区域,则显示在BorderLayout的北部区域)。您可以通过单击某行来选择该行。默认情况下,JTable允许您选择多行。您可以使用JTable的getSelectedRow()方法获取第一个选定的行号,使用getselectedRows()方法获取所有选定行的行号。getSelectedRowCount()方法返回选中的行数。
你从最简单的JTable开始。然而,与所谓的最简单的JTable一起工作并不是一件容易的事情,但是现在你知道了与JTable一起工作的基本知识。
让我们通过使用另一个构造函数创建JTable来重复这个例子。JTable类有另一个构造函数,它接受行数和列数作为参数。您可以创建一个两行三列的JTable,如下所示:
// Create a JTable with 2 rows and 3 columns
JTable table = new JTable(2, 3);
如果要将第一行和第一列的值设置为“John Jacobs”,则不需要使用表的模型。你可以使用JTable的setValueAt()方法来做同样的事情。
table.setValueAt("John Jacobs", 0, 0);
这一个比上一个稍微容易一点。然而,您仍然可以将默认的列标题标签设置为 A、B 和 c。JTable的另外两个构造函数允许您一次性设置行数和列数以及数据。它们的区别仅在于参数类型:一个让您使用一个数组Object,另一个让您使用一个Vector对象。它们声明如下:
JTable(Object[][] rowData, Object[] columnNames)JTable(Vector rowData, Vector columnNames)
如果使用二维数组Object来设置行数据,数组的第一维数决定了行数。如果使用一个Vector,Vector中的元素数量决定了表格中的行数。Vector中的每个元素都应该是一个包含一行数据的Vector对象。下面是如何使用二维数组Object构造一个JTable。图 2-27 显示了显示代码中所有数据集的表格。

图 2-27。
A JTable with two rows, three columns, and data
// Prepare the column headers
Object[] columnNames = {"ID", "Name", "Gender" } ;
// Create a two-dimensioanl array to contain the table's data
Object[][] rowData = new Object[][] {
{new Integer(100), "John Jacobs", "Male" },
{new Integer(101), "Barbara Gentry", "Female"}
};
// Create a JTable with the data and the column headers
JTable table = new JTable(rowData, columnNames);
到目前为止,您的表的数据是硬编码的。JTable将所有数据视为String,表格中的所有单元格都是可编辑的。例如,您将 ID 列的值设置为整数,但它们仍然显示为左对齐的文本。数字在单元格中应该右对齐。如果你想定制一个JTable,你需要用你自己的模型来做桌子。回想一下,TableModel接口定义了一个JTable的模型。下面是该接口的声明:
public interface TableModel
public int getRowCount();
public int getColumnCount();
public String getColumnName(int columnIndex);
public Class<?> getColumnClass(int columnIndex);
public boolean isCellEditable(int rowIndex, int columnIndex);
public Object getValueAt(int rowIndex, int columnIndex);
public void setValueAt(Object aValue, int rowIndex, int columnIndex);
public void addTableModelListener(TableModelListener l);
public void removeTableModelListener(TableModelListener l);
}
该类实现了TableModel接口。它为TableModel接口的方法提供了一个空的实现。它没有提到数据应该如何存储。如果你想实现你自己的表格模型,你需要从AbstractTableModel类继承你的类。如果您在自定义表模型类中至少实现了以下三个方法,您将获得一个只读表模型:
public int getRowCount();public int getColumnCount();public Object getValueAt(int row, int column);
该类继承自AbstractTableModel类。它为接口中的所有方法提供了默认实现。它使用Vectors的一个Vector来存储表的数据。
如果您使用自己的表格模型,您可以更好地控制JTable的工作。清单 2-9 实现了一个简单的表格模型,使用数组的数组来存储数据。
清单 2-9。实现简单的表格模型
// SimpleTableModel.java
package com.jdojo.swing;
import javax.swing.table.AbstractTableModel;
public class SimpleTableModel extends AbstractTableModel {
private Object[][] data = {};
private String[] columnNames = {"ID", "Name", "Gender"};
private Class[] columnClass = {Integer.class, String.class, String.class};
private Object[][] rowData = new Object[][]{
{new Integer(100), "John Jacobs", "Male"},
{new Integer(101), "Barbara Gentry", "Female"}
};
public SimpleTableModel() {
}
@Override
public int getRowCount() {
return rowData.length;
}
@Override
public int getColumnCount() {
return columnNames.length;
}
@Override
public String getColumnName(int columnIndex) {
return columnNames[columnIndex];
}
@Override
public Class getColumnClass(int columnIndex) {
return columnClass[columnIndex];
}
@Override
public boolean isCellEditable(int rowIndex, int columnIndex) {
boolean isEditable = true;
if (columnIndex == 0) {
isEditable = false; // Make the ID column non-editable
}
return isEditable;
}
@Override
public Object getValueAt(int rowIndex, int columnIndex) {
return rowData[rowIndex][columnIndex];
}
@Override
public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
rowData[rowIndex][columnIndex] = aValue;
}
}
在方法中,指定列数据的类;JTable将使用这个信息适当地显示列的数据。例如,它会将列中的数字显示为右对齐。如果您为一个列指定了类型Boolean,那么JTable将在该列的每个单元格中使用一个JCheckBox来显示Boolean值。注意,您已经通过从为 0 的columnIndex的isEditable()方法返回false使 ID 列不可编辑。在本例中,您再次对表的数据进行了硬编码。但是,您可以从数据库、数据文件、网络或任何其他数据源读取数据。下面的代码片段使用定制模型来创建一个JTable:
// Use the SimpleTableModel as the model for the table
JTable table = new JTable(new SimpleTableModel());
请注意,您的表模型不允许添加和删除行/列。如果您想要这些扩展功能,您最好从DefaultTableModel类继承 model 类并定制您想要改变的行为。
您可以通过调用其方法setAutoCreateRowSorter(true)将数据排序功能添加到您的JTable中。通过单击列标题,可以对列中的数据进行排序。在您调用这个方法之后,JTable将显示一个向上/向下箭头作为列标题的一部分,以指示一个列是按升序还是降序排序的。您还可以使用一个行过滤器,根据某些标准隐藏JTable中的行,如下所示:
// Set a row sorter for the table
TableRowSorter sorter = new TableRowSorter(table.getModel());
table.setRowSorter(sorter);
// Set an ID filter for the table
RowFilter<SimpleTableModel, Integer> IDFilter = new RowFilter<SimpleTableModel, Integer> () {
@Override
public boolean include(Entry<? extends SimpleTableModel, ? extends Integer> entry) {
SimpleTableModel model = entry.getModel();
int rowIndex = entry.getIdentifier().intValue();
Integer ID = (Integer) model.getValueAt(rowIndex, 0);
if (ID.intValue() <= 100) {
return false; // Do not show rows with an ID <= 100
}
return true;
}
};
sorter.setRowFilter(IDFilter);
上面的代码片段为一个名为table的JTable设置了一个过滤器,这样就不会显示 id 小于或等于 100 的行。RowFilter号是一艘abstract级;您必须覆盖它的include()方法来指定您的过滤标准。它还有几个返回不同种类的RowFilter对象的静态方法,您可以将这些方法直接用于RowSorter对象。以下是创建行过滤器的一些示例:
// Create a filter that will show only rows that starts
// with "John" in the second column (column index = 1)
RowFilter nameFilter = RowFilter.regexFilter("^John*", 1);
// Create a filter that will show only rows that has a
// "Female" value in its third column (column index = 2)
RowFilter genderFilter = RowFilter.regexFilter("^Female$", 2);
// Create a filter that will show only rows that has 3rd,
// 5th and 7th columns values starting with "A"
RowFilter anyFilter1 = RowFilter.regexFilter("^A*", 3, 5, 7);
// Create a filter that will show only rows that has any
// column whose value starts with "A"
RowFilter anyFilter2 = RowFilter.regexFilter("^A*");
您可以将一个TableModelListener添加到一个TableModel中,以监听对表格模型所做的任何更改。
Tip
由于篇幅限制,A JTable有许多特性无法在本节中描述。它还允许您设置一个自定义单元格,呈现为在单元格中显示一个值。例如,您可以在单元格中显示单选按钮,供用户选择,而不是让他们编辑纯文本值。
树形结构
A JTree用于以树状结构显示分层数据,如图 2-28 所示。你可以把一个JTree想象成颠倒显示一棵真实的树。

图 2-28。
A JTree showing departments and a list of employees in the departments
一个JTree中的每一项被称为一个节点。在图中,部门、销售、约翰等。是节点。节点被进一步分类为分支节点或叶节点。如果一个节点下可以有其他节点,称为其子节点,则称为分支节点。如果一个节点没有子节点,它被称为叶节点。部门、销售和信息技术是分支节点的示例,而 John、Elaine 和 Aarav 是叶节点的示例。现实世界的树中总有一个特殊的分支叫做根。类似地,JTree总是有一个特殊的分支节点,称为根节点。您的JTree有一个名为部门的根节点。在JTree中,您可以通过使用它的setRootVisible(boolean visibility)方法使根节点可见或不可见。
分支节点被称为其子节点的父节点。注意,子节点也可以是分支节点。“销售”、“信息技术”和“广告”是“部门”节点的子节点。销售节点有两个子节点:John 和 Elaine。John 和 Elaine 都有相同的父节点,即销售节点。
同一级别的节点称为兄弟节点。换句话说,具有相同父节点的节点称为兄弟节点。销售、信息技术和广告是兄弟姐妹;约翰和伊莱恩是兄弟姐妹;Tejas 和 Aarav 是兄弟姐妹。两个术语,祖先和后代,在节点的上下文中经常使用。作为父节点的父节点的父节点等等的节点都称为祖先节点。也就是说,从祖父开始的节点都是祖先节点。从孙开始向下的节点都称为后代。例如,Departments 节点是 Elaine 节点的祖先,而 Elaine 节点是 Departments 节点的后代。
你已经学了足够多与 a JTree相关的术语。是时候看看一个JTree在行动了。与JTree相关的类是在javax.swing和javax.swing.tree包中。一个JTree由节点组成。TreeNode接口的一个实例代表一个节点。TreeNode接口声明了提供节点基本信息的方法,比如节点类型(分支或叶子)、父节点、子节点等。
是扩展接口的接口。它声明了额外的方法,允许您通过插入/移除子节点或更改节点对象来更改节点。DefaultMutableTreeNode类是MutableTreeNode接口的一个实现。
在开始创建节点之前,您需要理解节点是 Java 对象的可视化表示(通常是一行文本)。换句话说,节点包装一个对象,通常显示该对象的单行文本表示。节点表示的对象称为该节点的用户对象。因此,在构建节点之前,必须有一个节点将表示的对象。不用担心创建新类来构建节点。您可以只使用一个String来构建您的节点。下面的代码片段创建了一些可以在JTree中使用的节点:
// Create a Departments node
DefaultMutableTreeNode root = new DefaultMutableTreeNode("Departments");
// Create a Sales node
DefaultMutableTreeNode sales = new DefaultMutableTreeNode("Sales");
// Create a John node
DefaultMutableTreeNode john = new DefaultMutableTreeNode("John");
// Create a customer node, assuming you have a Customer class.
// In this case, the node will wrap a Customer object
Customer cust101 = new Customer(101, "Joe");
DefaultMutableTreeNode c101Node = new DefaultMutableTreeNode(cust101);
// If you want to get the user object that a node wraps, you would
// use the getUserObject() method of the DefaultMutableTreeNode class
Customer c101Back = (Customer)c101Node.getUserObject();
一旦有了一个节点,使用add()或insert()方法添加子节点就很容易了。add()方法将节点追加到末尾;insert()方法允许您指定新节点的位置。例如,添加一个Sales节点作为您编写的Departments根节点的子节点
root.add(sales);
要将John作为子节点添加到sales,您需要编写
sales.add(john);
一旦准备好了节点,就很容易将它们放入JTree中。您需要通过指定根节点来创建一个JTree。
JTree tree = new JTree(root);
JTree类的其他构造函数允许你以不同的方式创建一个JTree。除非你正在学习JTree,否则无参数构造函数不是很有用。它创建了一个添加了一些节点的JTree,如果您想使用JTree进行实验,这可以省去添加节点的麻烦。您还可以通过将一个数组Object或一个Object的Vector传递给它的构造函数来创建一个JTree,作为JTree根的子节点。在添加传入的对象作为其子节点之前,一个根将被添加到新的JTree中。例如,
// Create a JTree. It will create a default root node called Root
// and it will add two, "One" and "Two", child nodes for Root.
// The Root node is not displayed by default.
JTree tree = new JTree(new Object[]{"One", "Two"});
一旦创建了JTree组件,就该在 Swing 容器中显示它了。通常,你给一个JScrollPane添加一个JTree,这样它就有了滚动能力。
myContainer.add(new JScrollPane(tree));
如何访问或浏览JTree节点?有两种方法可以访问JTree中的节点:使用行号和使用树路径。
一个JTree由节点组成。一个JTree如何显示节点?回想一下,节点是TreeNode类的一个实例,它包装任何类型的对象。因此,你可以说节点是对象的包装器。默认情况下,JTree调用节点对象的toString()方法来获取要显示的节点文本。如果您的节点包装了一个对象,该对象的方法没有返回要在JTree节点中显示的有意义的字符串,您可以通过创建一个自定义的JTree并覆盖其convertValueToText()方法来为该节点提供一个自定义字符串。在示例中,您已经将一个String对象包装在一个节点中,并且一个String对象的toString()方法返回字符串本身。假设您想要为Customer对象创建一个节点。确保覆盖Customer类的toString()方法,并返回一个有意义的字符串显示在Customer节点中,如客户名称和 id。
如果从上到下查看JTree节点,每个节点都显示在单独的水平行中。第一个节点(根节点,如果根节点可见)是第零行。第二个在第一行,依此类推。在图 2-28 中,部门、销售、John、Elaine 和信息技术的行号分别为 0、1、2、3 和 4。请注意,只有在显示节点时,才会将行号分配给该节点。当父节点折叠时,节点可能不会显示。例如,广告节点有一些未显示的子节点,并且没有为它们分配行号,因为广告节点(它们的父节点)已折叠。一个JTree的方法返回可视节点的数量。请注意,当您在JTree中展开和折叠节点时,可见节点的数量会发生变化。
一个TreePath类的对象在一个JTree中唯一地代表一个节点。它的结构类似于文件系统中用来表示文件的路径。文件路径通过指定从根文件夹开始的路径来唯一地表示文件,例如/Departments/Sales/John 表示名为 John 的文件,该文件位于根文件夹下的 Departments 文件夹下的 Sales 文件夹下。一个TreePath对象封装了相同类型的信息来表示一个JTree中的一个节点。它由从根开始的有序节点数组组成。例如,如果您需要为示例中的节点 John 构造一个TreePath对象,您可以按如下方式完成:
Object[] path = new Object[] {root, sales, john};
TreePath johnNodePath = new TreePath(path);
TreePath类的方法返回Object数组,getLastPathComponent()方法返回数组的最后一个元素,这是对节点的引用,TreePath对象表示节点的路径。通常,当你使用一个JTree时,你不会构造一个TreePath对象。相反,您可以在JTree事件中使用一个TreePath对象。如果您使用一个JTree,代表一个TreePath对象的数组对象的每个元素都是一个TreeNode的实例。如果您使用默认的树模型,TreePath将由一组对象组成。拥有一个到节点的TreePath,您可以得到节点包装的对象,如下所示:
// Suppose path is an instance of the TreePath class and it represents a node
DefaultMutableTreeNode node = (DefaultMutableTreeNode)path.getLastPathComponent();
Object myObject = node.getUserObject();
一个JTree提供了两个叫做getRowForPath()和getPathForRow()的方法来将一个行号转换成一个TreePath,反之亦然。当你很快了解到JTree事件时,你将和TreePath一起工作。
如果您没有为一个JTree的事件编写代码,您将没有一个节点的(除非您存储了节点引用本身,这不是必需的)。在这种情况下,您可以始终从根节点开始,继续沿着树向下导航。一个JTree的模型是一个TreeModel类的实例,它有一个getRoot()方法。一旦获得了根节点的句柄,就可以使用TreeNode类的children()方法,该方法返回一个TreeNode的所有子节点的枚举。下面的代码片段定义了一个方法navigateTree(),如果您将根节点的引用传递给它,它将遍历所有树节点:
public void navigateTree(TreeNode node) {
if (node.isLeaf()) {
System.out.println("Got a leaf node: " + node);
return;
}
else {
System.out.println("Got a branch node: " + node);
Enumeration e = node.children();
while(e.hasMoreElements()) {
TreeNode n = (TreeNode)e.nextElement();
navigateTree(n); // Recursive method call
}
}
}
您可以通过单击来选择树节点。一个JTree使用一个选择模型来跟踪被选择的节点。您需要与其选择模型进行交互,以选择节点或获取关于所选节点的信息。选择模型是TreeSelectionModel接口的一个实例。一个JTree允许用户在三种不同的模式下选择节点。它们由接口中定义的三个常数表示:
SINGLE_TREE_SELECTION:允许用户一次只选择一个节点。CONTIGUOUS_TREE_SELECTION:允许用户选择任意数量的相邻节点。DISCONTIGUOUS_TREE_SELECTION:允许用户选择任意数量的节点,没有任何限制。
下面的代码片段演示了如何使用JTree的选择模型的一些方法:
// Get selection model for JTree
TreeSelectionModel selectionModel = tree.getSelectionModel();
// Set the selection mode to discontinuous
selectionModel.setSelectionMode(TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION);
// Get the selected number of nodes
int selectedCount = selectionModel.getSelectionCount();
// Get the TreePath of all selected nodes
TreePath[] selectedPaths = selectionModel.getSelectionPaths();
您可以将一个TreeSelectionListener添加到一个JTree中,当一个节点被选中或取消选中时,它会得到通知。以下代码片段演示了如何将添加到JTree:
// Create a JTree. Java will add some nodes
JTree tree = new JTree();
// Add selection listener to the JTree
tree.addTreeSelectionListener((TreeSelectionEvent event) -> {
TreeSelectionModel selectionModel = tree.getSelectionModel();
TreePath[] paths = event.getPaths();
for (TreePath path : paths) {
Object node = path.getLastPathComponent();
if (selectionModel.isPathSelected(path)) {
System.out.println("Selected: " + node);
}
else {
// Node is deselected
System.out.println("DeSelected: " + node);
}
}
});
您可以通过单击加号或单击节点本身来展开节点。您可以通过单击减号或单击节点本身来折叠节点。当一个节点展开或折叠时,JTree触发两个事件。它按顺序触发树扩展事件和树扩展事件。tree-will-expand 事件在展开或折叠节点之前触发。如果你从这个事件抛出一个ExpandVetoException,展开(或者折叠)就会停止。否则,将触发树扩展事件。以下代码片段演示了如何为这些事件编写代码:
// Add a TreeWillExpandListener
tree.addTreeWillExpandListener(new TreeWillExpandListener() {
@Override
public void treeWillExpand(TreeExpansionEvent event)
throws ExpandVetoException {
System.out.println("Will Expand:" + event.getPath());
}
@Override
public void treeWillCollapse(TreeExpansionEvent event) throws ExpandVetoException {
System.out.println("Will Collapse: " + event.getPath());
}
});
// Add TreeExpansionListener
tree.addTreeExpansionListener(new TreeExpansionListener() {
@Override
public void treeExpanded(TreeExpansionEvent event) {
System.out.println("Exapanded: " + event.getPath());
}
@Override
public void treeCollapsed(TreeExpansionEvent event) {
System.out.println("Collapsed: " + event.getPath());
}
});
Tip
A JTree是一个强大而复杂的 Swing 组件。它可以让你定制几乎所有的东西。每个节点显示在一个JLabel中。分支和叶节点显示的图标不同。默认图标取决于外观。您可以通过创建自己的树单元渲染器来自定义默认图标。您还可以向JTree添加一个TreeModelListener,它会通知您其模型的任何变化。您可以通过使用setEditable(true)方法使JTree可编辑。您可以通过双击来编辑节点的标签。
JTabbedPane 和 JSplitPane
有时,由于空间限制,不可能在一个窗口中显示所有信息。您可以使用JTabbedPane对窗口中的信息进行分组和分离。图 2-29 显示了一个JFrame,它有一个带有两个标签的窗格,标题分别为General Information和Contacts,显示一个人的一般信息和联系信息。

图 2-29。
A JTabbedPane with two tabs
一个JTabbedPane组件充当其他 Swing 组件的容器,以选项卡的方式排列它们。它可以使用标题、图标或两者来显示选项卡。用户需要点击标签来查看标签的内容。使用JTabbedPane最大的好处就是空间共享。一次只能看到JTabbedPane中一个标签的内容。用户可以在选项卡之间切换,以查看另一个选项卡的内容。
一个JTabbedPane也可以让你指定在哪里显示标签。您可以指定将选项卡放置在顶部、底部、左侧或右侧。图 2-29 显示了顶部的选项卡。如果您有一个名为frame的JFrame,下面的代码片段会产生如图 2-29 所示的帧。代码向由两个JPanels表示的两个选项卡添加了一个JLabel。
JPanel generalInfoPanel = new JPanel();
JPanel contactInfoPanel = new JPanel();
JTabbedPane tabbedPane = new JTabbedPane();
generalInfoPanel.add(new JLabel("General info components go here..."));
contactInfoPanel.add(new JLabel("Contact info components go here..."));
tabbedPane.addTab("General Information", generalInfoPanel);
tabbedPane.addTab("Contacts", contactInfoPanel);
frame.getContentPane().add(tabbedPane, BorderLayout.CENTER);
getTabCount()方法返回一个JTabbedPane中选项卡的数量。一个JTabbedPane中的每个标签都有一个索引。第一个选项卡的索引为 0,第二个选项卡的索引为 1,依此类推。您可以使用其索引来获取表示选项卡的组件。
// Get the reference of the component for the Contact tabs
JPanel contactsPanel = tabbedPane.getTabComponentAt(1);
JSplitPane是一个分割器,可以用来分割两个组件之间的空间。拆分条可以水平或垂直显示。当可用空间小于显示两个组件所需的空间时,用户可以上下或左右移动拆分条,这样一个组件比另一个组件获得更多的空间。如果有足够的空间,两个组件都可以完全显示。
JSplitPane类提供了许多构造函数。您可以使用它的默认构造函数创建它,并使用它的setXxxComponent(Component c)添加两个组件,其中Xxx可以是Top、Bottom、Left或Right。它还允许您指定在更改拆分条的位置时组件的重绘方式。它可以是连续的或非连续的。如果它是连续的,当您移动拆分条时,组件将被重新绘制。如果它是不连续的,当您停止移动拆分条时,组件将被重新绘制。
下面的代码片段显示了添加到一个JSplitPane中的JPanel类的两个实例,该实例又被添加到一个名为frame的JFrame的内容窗格中。图 2-30 显示了最终的JFrame。
// Create two JPanels and a JSplitPane
JPanel generalInfoPanel = new JPanel();
JPanel contactInfoPanel = new JPanel();
JSplitPane splitPane = new JSplitPane();
generalInfoPanel.add(new JLabel("General info components go here..."));
contactInfoPanel.add(new JLabel("Contact info components go here..."));
// Add two JPanels to the JSplitPane and the JSplitPane
// to the content pane of the JFrame
splitPane.setLeftComponent(generalInfoPanel);
splitPane.setRightComponent(contactInfoPanel);
frame.getContentPane().add(splitPane, BorderLayout.CENTER);

图 2-30。
Using a JSplitPane to split space between two components
自定义对话框
一个JDialog是顶级的 Swing 容器。它被用作一个临时的顶层容器(或者一个弹出窗口)来帮助主窗口吸引用户的注意力。我不严格地使用窗口这个术语来表示一个 Swing 顶级容器。假设您有一个JFrame,您必须在其中显示一个人的信息。您可能没有足够的空间在JFrame中显示一个人的所有细节。在这种情况下,您只能在一个JFrame上显示基本的个人最低信息,并提供一个标记为“个人详细信息”的按钮。当用户点击这个按钮时,你可以打开一个JDialog,显示这个人的详细信息。这是一个使用JDialog向用户显示信息的例子。使用对话窗口的另一个例子是让用户从文件系统中选择一个文件。您可以向用户显示一个对话框,让他浏览文件系统并选择一个文件。您也可以在下列其他场合使用JDialog:
- 当您想要确认用户的操作时:这称为。例如,当用户在窗口中选择一个人记录并试图删除该人记录时,您会显示一条确认消息“您确定要删除此人吗?”该对话框显示两个标记为“是”和“否”的按钮,以指示用户的选择。
- 当您需要用户的一些输入时:这被称为。例如,当焦点移到日期字段时,您可能会在
JDialog中显示一个日历,并希望用户选择一个日期。输入对话框可以简单到输入/选择一个值或输入多个值,例如一个人的详细信息。 - 当您想要向用户显示一些消息时:这称为。例如,当用户将一些信息保存到数据库时,您想要用一条消息通知用户,该消息指示数据库事务的状态。
创建一个对话框非常简单:只需创建一个继承自JDialog类的新类。您可以将任意数量的 Swing 组件添加到您的定制JDialog中,就像您添加到JFrame中一样。一个JDialog让添加组件变得更容易。您不需要获取对其内容窗格的引用来设置其布局管理器和添加组件。相反,您可以调用JDialog本身的setLayout()和add()方法。这些方法将调用路由到其内容窗格。默认情况下,JDialog使用BorderLayout作为布局管理器。
清单 2-10 列出了一个自定义的JDialog,它在一个JLabel和一个 OK JButton中显示当前的日期和时间。当用户点击JButton时,JDialog关闭。
清单 2-10。显示当前日期和时间的自定义 JDialog
// DateTimeDialog.java
package com.jdojo.swing;
import java.awt.BorderLayout;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JLabel;
public class DateTimeDialog extends JDialog {
JLabel dateTimeLabel = new JLabel("Datetime placeholder");
JButton okButton = new JButton("OK");
public DateTimeDialog() {
initFrame();
}
private void initFrame() {
// Release all resources when JDialog is closed
this.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
this.setTitle("Current Date and Time");
this.setModal(true);
String currentDateTimeString = getCurrentDateTimeString();
dateTimeLabel.setText(currentDateTimeString);
// There is no need to add components to the content pane.
// You can directly add them to the JDialog.
this.add(dateTimeLabel, BorderLayout.NORTH);
this.add(okButton, BorderLayout.SOUTH);
// Add an action listeenr to the OK button
okButton.addActionListener(e -> DateTimeDialog.this.dispose());
}
private String getCurrentDateTimeString() {
LocalDateTime ldt = LocalDateTime.now();
DateTimeFormatter formatter =
DateTimeFormatter.ofPattern("EEEE MMMM dd, yyyy hh:mm:ss a");
String dateString = ldt.format(formatter);
return dateString;
}
}
DateTimeDialog类是自定义JDialog的一个简单例子。要在您的应用中使用它,您需要创建这个JDialog的一个实例,打包它,并使它可见,如下所示:
DateTimeDialog dateTimeDialog = new DateTimeDialog();
dateTimeDialog.pack();
dateTimeDialog.setVisible(true);
如果您正在显示来自另一个顶级容器的JDialog,比如说一个JFrame或另一个JDialog,您可能希望将它显示在顶级容器的中央。有时你可能想把它显示在屏幕中央。你可以通过使用它的setLocationRelativeTo(Component c)方法将一个JDialog放在顶层容器或屏幕的中央。如果将null作为参数传递,那么JDialog将在屏幕上居中。否则,它将在作为参数传递的组件内居中。
// Center the JDialog within a frame, assuming that myFrame exists
dateTimeDialog.setLocationRelativeTo(myFrame);
// Place the JDialog in the center of screen
dateTimeDialog.setLocationRelativeTo(null);
您可以创建一个拥有者的JDialog,它可以是另一个JDialog、JFrame或JWindow。通过指定一个JDialog的所有者,您创建了一个父子关系。当JDialog的所有者(或父母)关闭时,JDialog也关闭。当拥有者被最小化或最大化时,JDialog也被最小化或最大化。带有所有者的JDialog总是显示在其所有者的上方。您可以在构造函数中指定一个JDialog的所有者。当您使用它的无参数构造函数创建一个JDialog时,一个隐藏的Frame被创建为它的所有者。注意是个java.awt.Frame,不是javax.swing.JFrame。JFrame类继承自Frame类。您还可以创建一个以null为所有者的JDialog,在这种情况下,它没有所有者。
默认情况下,JDialog是可调整大小的。如果你不希望用户调整你的JDialog的大小,你可以通过调用它的setResizable(false)方法来实现。
根据JDialog的焦点行为,可以将其分类为
- 情态的
- 非模态的
当显示一个模态JDialog时,它会阻塞应用中其他显示的窗口。换句话说,如果显示了一个模态JDialog,您必须先关闭它,然后才能使用该应用中的任何其他窗口。要制作一个JDialog模态,可以使用它的setModal(true)方法。一些JDialog类的构造函数也让你指定JDialog应该是模态的还是非模态的。
非模态JDialog不会阻止应用中任何其他显示的窗口。您可以在其他窗口和非模态实例JDialog之间切换焦点。默认情况下,JDialog是无模式的。
您也可以为模态JDialog设置模态的范围。一个JDialog可以有四种模态中的一种。它们由java.awt.Dialog.ModalityType枚举中的四个常数定义:
MODELESSDOCUMENT_MODALAPPLICATION_MODALTOOLKIT_MODAL
您可以在其构造函数中或通过使用其方法来指定JDialog的模态类型。
MODELESS的设备类型意味着JDialog不会阻挡任何窗口。
DOCUMENT_MODAL的模态类型意味着JDialog将阻塞其父层次结构中的任何窗口(其所有者、所有者的所有者等等)。它不会阻塞其子层次结构中的任何窗口(其子、子的子等等)。假设你显示了三个窗口:frame是一个JFrame;dialog1是一只JDialog,主人是frame;dialog2是另一个JDialog,它的主人是dialog1。如果您为dialog1指定了DOCUMENT_MODAL的设备类型,您可以使用dialog2,但不能使用frame。如果dialog2的设备类型为MODELESS,您可以同时使用dialog1和dialog2,因为dialog2不会阻挡任何窗口。
APPLICATION_MODAL的模态类型意味着JDialog将阻止该 Java 应用中的任何窗口,除了其子层次结构中的窗口。
TOOLKIT_MODAL的模态类型意味着JDialog将阻止从同一工具包运行的任何窗口,除了它的子层次结构中的窗口。在 Java 应用中,它与APPLICATION_MODAL相同。当您在使用 Java Web Start 启动的 Applet 或应用中使用它时,它非常有用。你可以把一个浏览器想象成一个应用,多个 Applet 想象成顶层窗口。所有 Applet 都由同一个工具包加载。如果在一个 Applet 中显示一个设备类型为TOOLKIT_MODAL的JDialog,它将阻止输入到同一浏览器中的任何其他 Applet。您必须授予“toolkitModality”AWTPermission以使 Applet 使用TOOLKIT_MODAL设备。用 Java Web Start 启动的多个应用也会出现同样的行为。
清单 2-11 包含了一个试验JDialog模态类型的程序。对dialog1Modality和dialog2Modality变量使用不同的值,看看它如何影响其他窗口中的阻塞输入。
清单 2-11。试验 JDialog 的通道类型
// JDialogModalityTest.java
package com.jdojo.swing;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JFrame;
import java.awt.Dialog.ModalityType;
public class JDialogModalityTest {
public static void main(String[] args) {
JFrame frame = new JFrame("My JFrame");
frame.setBounds(0, 0, 400, 400);
frame.setVisible(true);
final ModalityType dialog1Modality = ModalityType.DOCUMENT_MODAL;
final ModalityType dialog2Modality = ModalityType.DOCUMENT_MODAL;
final JDialog dailog1 = new JDialog(frame, "JDialog 1");
JButton openBtn = new JButton("Open JDialog 2");
openBtn.addActionListener(e -> {
JDialog d2 = new JDialog(dailog1, "JDialog 2");
d2.setBounds(200, 200, 200, 200);
d2.setModalityType(dialog2Modality);
d2.setVisible(true);
});
dailog1.add(openBtn);
dailog1.setBounds(20, 20, 200, 200);
dailog1.setModalityType(dialog1Modality);
dailog1.setVisible(true);
}
}
例如,在 Swing 应用中经常使用JDialog来向用户显示错误消息。每当你需要一个对话窗口时,创建一个自定义的JDialog是很费时间的。秋千的设计者意识到了这一点。他们给了我们JOptionPane类,让我们在使用常用的JDialog类型时更容易。我将在下一节讨论JOptionPane。
标准对话框
JOptionPane类让你很容易创建和显示标准的模态对话框。它包含许多静态方法来创建不同种类的JDialog,用细节填充它们,并将它们显示为模态JDialog。当JDialog关闭时,该方法返回一个值来指示用户在JDialog上的动作。注意,JOptionPane类是从JComponent类继承而来的。除了被用作创建标准对话框的工厂之外,JOptionPane类与JDialog类没有任何关系。它还包含返回一个JDialog对象的方法,您可以在您的应用中定制和使用这个对象。您可以显示以下四种标准对话框:
- 消息对话框
- 确认对话框
- 输入对话框
- 选项对话框
显示标准JDialog的JOptionPane类的静态方法的名字类似于showXxxDialog()。Xxx可以替换为Message、Confirm、Input和Option。同样的方法还有另一个版本,叫做showInternalXxxDialog(),它使用一个JInternalFrame来显示对话框细节,而不是一个JDialog。所有四种类型的标准对话框都接受不同类型的参数,并返回不同类型的值。表 2-17 显示了这些方法的参数列表及其描述。
表 2-17。
List of Standard Argument Types and Their Values Used With JOptionPane
| 参数名称 | 参数类型 | 描述 | | --- | --- | --- | | `parentComponent` | `Component` | `JDialog`以指定的父组件为中心。包含该组件的顶层容器成为所显示的`JDialog`的所有者。如果是`null`,则`JDialog`在屏幕上居中。 | | `message` | `Object` | 通常,它是一个需要在对话框中显示为消息的字符串。但是,您可以传递任何对象。如果您传递一个 Swing 组件,它只是简单地显示在对话框中。如果你通过了一个`Icon`,它会显示在一个`JLabel`中。如果您传递任何其他对象,则在该对象上调用`toString()`方法,并显示返回的字符串。你也可以传递一个对象数组(通常是一个字符串数组),数组中的每个元素将一个接一个地垂直显示。 | | `messageType` | `Int` | 它表示您想要显示的消息类型。根据消息的类型,对话框中会显示合适的图标。可用的消息类型由`JOptionPane`类中的下列常量定义:`ERROR_MESSAGE,``INFORMATION_MESSAGE,WARNING_MESSAGE, QUESTION_MESSAGE,PLAIN_MESSAGE.``PLAIN_MESSAGE`类型不显示任何图标。另一个参数是`Icon`类型的,允许您指定自己的图标显示在对话框中。 | | `optionType` | `Int` | 它表示需要在对话框中显示的按钮。下面是在`JOptionPane`类中定义的常数列表,你可以用它来获得对话框中的标准按钮:`DEFAULT_OPTION, YES_NO_OPTION,``YES_NO_CANCEL_OPTION, OK_CANCEL_OPTION``DEFAULT_OPTION`显示一个`OK`按钮。其他选项显示一组按钮,顾名思义。您可以通过向`showOptionDialog()`方法提供`options`参数来定制按钮的数量及其文本。 | | `options` | `Object[]` | 此参数允许您自定义对话框中显示的一组按钮。如果您在数组中传递一个`Component`对象,该组件将显示在按钮行中。如果指定一个`Icon`对象,图标会显示在一个`JButton`中。对于您传递的任何其他类型的对象,将显示一个`JButton`,并且`JButton`的文本是从该对象的`toString()`方法返回的字符串。通常,您传递一个字符串数组作为此参数,以在对话框中显示一组自定义按钮。 | | `title` | `String` | 它是显示为对话框标题的文本。如果不传递此参数,将提供一个默认标题。 | | `initialValue` | `Object` | 该参数用于输入对话框。它表示输入对话框中显示的初始值。 |通常,当用户关闭对话框时,您希望检查用户使用了什么按钮来关闭对话框。但是也有例外,当对话框只有一个按钮时,比如一个 OK 按钮。在这种情况下,要么您用来显示对话框的方法不返回值,要么您干脆忽略返回值。以下是可用于检查返回值是否相等的常数列表:
- 确定选项
- 是 _ 选项
- 无选项
- 取消选项
- 关闭选项
CLOSED_OPTION表示用户使用标题栏上的关闭(X)菜单按钮或使用其他方式(如在 Windows 平台上按下键盘上的 Ctrl + F4 键)关闭了对话框。其他常数表示对话框上正常的按钮用法;例如,OK_OPTION表示用户点击对话框上的 OK 按钮关闭对话框。
JOptionPane还可让您自定义它所显示的按钮的标签。您也不局限于标准的按钮集。也就是说,您可以在对话框中显示任意数量的按钮。在这种情况下,用于显示对话框的JOptionPane方法将为第一次按钮点击返回 0,为第二次按钮点击返回 1,为第三次按钮点击返回 2,依此类推。当稍后讨论JOptionPane类的showOptionDialog()方法时,您将看到这种类型的一个例子。
您可以通过使用JOptionPane类的showMessageDialog()静态方法之一来显示一个消息对话框。消息对话框总是用一个按钮向用户显示某种信息,通常是 OK 按钮。该方法不返回任何值,因为用户所能做的只是单击“确定”按钮关闭对话框。showMessageDialog()方法的签名如下所示:
showMessageDialog(Component parentComponent, Object message)showMessageDialog(Component parentComponent, Object message, String title, int messageType)showMessageDialog(Component parentComponent, Object message, String title, int messageType, Icon icon)
下面的代码片段显示了一个消息对话框,如图 2-31 所示。
// Show an information message dialog
JOptionPane.showMessageDialog(null, "JOptionPane is cool!", "FYI", JOptionPane.INFORMATION_MESSAGE);

图 2-31。
An information message dialog using the JOptionPane .showMessageDialog() method
您可以使用方法显示确认对话框。当您使用此方法时,您对知道用户的响应感兴趣,这由方法的返回值指示。以下代码片段显示一个确认对话框,如图 2-32 所示,并处理用户的响应:
// Show a confirmation dialog box
int response = JOptionPane.showConfirmDialog(null,
"Are you sure you want to save the changes?",
"Confirm Save Changes",
JOptionPane.YES_NO_CANCEL_OPTION,
JOptionPane.QUESTION_MESSAGE);
switch (response) {
case JOptionPane.YES_OPTION:
System.out.println("You chose yes");
break;
case JOptionPane.NO_OPTION:
System.out.println("You chose no");
break;
case JOptionPane.CANCEL_OPTION:
System.out.println("You chose cancel");
break;
case JOptionPane.CLOSED_OPTION:
System.out.println("You closed the dialog box.");
break;
default:
System.out.println("I do not know what you did ");
}

图 2-32。
A confirmation dialog box using the JOptionPane.showConfirmDialog() method
您可以使用方法要求用户输入。您可以为用户输入指定初始值。如果希望用户从列表中选择一个值,可以传递包含该列表的对象数组。UI 将在合适的组件中显示列表,如JComboBox或JList。下面的代码片段显示了一个输入对话框,如图 2-33 所示。
// Ask the user to enter some text about JOptionPane
String response = JOptionPane.showInputDialog("Please enter your opinion about input dialog.");
if (response == null) {
System.out.println("You have cancelled the input dialog.");
}
else {
System.out.println("You entered: " + response);
}

图 2-33。
A simple input dialog
您使用的showInputDialog()方法版本返回一个String,它是用户在输入字段中输入的文本。如果用户取消输入对话框,它返回null。
下面的代码片段显示了一个带有选项列表的输入对话框。用户可以从列表中选择一个选项。对话框如图 2-34 所示该版本的方法返回一个Object,而不是一个String。
// Show an input dialog that shows the user three options: "Cool!", "Sucks", "Don't know".
// The default selected value is "Don't know".
JComponent parentComponent = null;
Object message = "Please select your opinion about JOptionPane";
String title = "JOptionPane Input Dialog";
int messageType = JOptionPane.INFORMATION_MESSAGE;
Icon icon = null;
Object[] selectionValues = new String[] {"Cool!", "Sucks", "Don't know"};
Object initialSelectionValue = selectionValues[2];
Object response = JOptionPane.showInputDialog(parentComponent, message, title, messageType, icon, selectionValues, initialSelectionValue);
if (response == null) {
System.out.println("You have cancelled the input dialog.");
}
else {
System.out.println("You entered: " + response);
}

图 2-34。
An input dialog with a list of choices
最后,您可以使用如下声明的方法自定义选项按钮:
int showOptionDialog(Component parentComponent, Object message, String title, int optionType, int messageType, Icon icon, Object[] options, Object initialValue)
options参数指定用户的选项。如果在options参数中传递组件,组件显示为选项。如果您传递任何其他对象,比如字符串,那么会为options数组中的每个元素显示一个按钮。
下面的代码片段显示了如何在对话框中显示自定义按钮。它询问用户对某个JOptionPane的看法。出现的对话框如图 2-35 所示。
JComponent parentComponent = null;
Object message = "How is JOptionPane?";
String title = "JOptionPane Option Dialog";
int messageType = JOptionPane.INFORMATION_MESSAGE;
Icon icon = null;
Object[] options = new String[] {"Cool!", "Sucks", "Don't know" };
Object initialOption = options[2];
int response = JOptionPane.showOptionDialog(null, message, title,
JOptionPane.DEFAULT_OPTION,
JOptionPane.QUESTION_MESSAGE,
icon, options, initialOption);
switch(response) {
case 0:
case 1:
case 2:
System.out.println("You selected:" + options[response]);
break;
case JOptionPane.CLOSED_OPTION:
System.out.println("You closed the dialog box.");
break;
default:
System.out.println("I don't know what you did.");
}

图 2-35。
Customizing the Option buttons using the JOptionPane.showOptionDialog() method
默认情况下,您在本节中显示的所有对话框都是不可调整大小的。您想要自定义它们,以便它们可以调整大小。通过使用JOptionPane的createDialog()方法并执行一系列步骤,你可以定制由JOptionPane的静态方法显示的对话框。
Create an object of JOptionPane. Optionally, customize the properties of JOptionPane using its methods. Use createDialog() method to get the reference of the dialog box. Customize the dialog box. Display the dialog box using its setVisible(true) method.
以下代码片段显示了如图 2-36 所示的自定义可调整大小对话框。
// Show a custom resizable dialog box using
JOptionPane pane = new JOptionPane("JOptionPane is cool!", JOptionPane.INFORMATION_MESSAGE);
String dialogTitle = "Resizable Custom Dialog Using JOptionPane";
JDialog dialog = pane.createDialog(dialogTitle);
dialog.setResizable(true);
dialog.setVisible(true);

图 2-36。
A custom dialog box using the JOptionPane.createDialog() method
文件和颜色选择器
Swing 有两个内置的 JDialogs,使得从文件系统中选择文件/目录或者以图形方式选择颜色变得更加容易。允许用户从文件系统中选择一个文件。它提供了非静态方法,不像你在JOptionPane中看到的那样,在JDialog中创建和显示文件选择器组件。
是一个 Swing 组件,允许您在JDialog中以图形方式选择颜色。它提供了一个静态方法,正如你在JOptionPane中看到的,它在JDialog中创建和显示了一个颜色选择器组件。
Tip
JFileChooser类提供了创建和显示 JDialogs 的非静态方法,而JColorChooser类提供了用于相同目的的静态方法。拥有静态或非静态方法意味着非静态方法允许您定制JDialog,而静态方法只允许您通过参数定制JDialog。这意味着您可以定制由JFileChooser显示的JDialog,但不能定制JColorChooser。另一个区别是,您必须创建一个JFileChooser类的对象来使用它。最好重用同一个JFileChooser对象,因为它记得最后访问的目录,所以当你重用它时,默认情况下它会将你导航到最后访问的目录。
对话框
以下是在JDialog中显示文件选择器需要执行的步骤。
Create an object of the JFileChooser class. Optionally, customize its properties using its methods. You can customize properties such as should it let the user choose only files, only directories, or both; should it let the user select multiple files; apply a file filter criteria to show files based on your criteria, etc. Use one of the three non-static methods, showOpenDialog(), showSaveDialog(), or showDialog(), to display it in a JDialog. Check for the return value, which is an int, from the method call in the previous step. If it returns JFileChooser.APPROVE_OPTION, the user made a selection. The other two possible return values are JFileChooser.CANCEL_OPTION and JFileChooser.ERROR_OPTION, which indicate that either user cancelled the dialog box or some kind of error occurred. To get the selected file, call the getSelectedFile() or getSelectedFiles() method, which returns a File object and a File array, respectively. Note that a JFileChooser component only lets you select a file from a file system. It does not save or read a file. You can do whatever you like with the file reference returned from it. You can reuse the file chooser object. It remembers the last visited folder.
默认情况下,JFileChooser开始显示用户默认目录中的文件。您可以在其构造函数中或使用其方法来指定初始目录。
// Create a file chooser with the default initial directory
JFileChooser fileChooser = new JFileChooser();
// Create a file chooser, with an initial directory of C:\myjava.
// You can specify a directory path according to your operating system syntax.
// C:\myjava is using Windows file path syntax.
JFileChooser fileChooser = new JFileChooser("C:\\myjava");
默认情况下,文件选择器只允许选择文件。让我们自定义它,以便您可以选择一个文件或目录。它还应该允许多重选择。以下代码片段完成了这一定制:
// Let the user select files and directories
fileChooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
// Aloow multiple selection
fileChooser.setMultiSelectionEnabled(true);
让我们显示一个打开的文件选择器对话框,并检查用户是否选择了一个文件。如果用户做出选择,在标准输出中打印文件路径。以下代码片段显示如图 2-37 所示的对话框。
// Display an open file chooser
int returnValue = fileChooser.showOpenDialog(null);
if(returnValue == JFileChooser.APPROVE_OPTION) {
File selectedFile = fileChooser.getSelectedFile();
System.out.println("You selected: " + selectedFile);
}

图 2-37。
An open file chooser dialog box using a JFileChooser
该类的所有三个方法都接受一个Component参数。它被用作它所显示的JDialog的所有者,并使对话框居中。将null作为其父组件,使其在屏幕上居中。
注意,在图 2-37 中,有两个按钮。一个被标记为Open,另一个Cancel.``Open按钮被称为批准按钮。对话框的标题是Open。当您使用JFileChooser的方法时,您会得到相同的对话框,除了按钮和标题的文本Open被替换为文本Save。您可以在显示对话框标题和批准按钮文本之前对其进行自定义,如下所示:
// Change the dialog's title
fileChooser.setDialogTitle("Open a picture file");
// Change the button's text
fileChooser.setApproveButtonText("Open File");
第三种方法showDialog(),让您指定批准按钮文本和对话框标题,如下所示:
// Open a file chooser with Attach as its title and approve button's text
int returnValue = fileChooser.showDialog(null, "Attach");
if (returnValue == JFileChooser.APPROVE_OPTION) {
File selectedFile = fileChooser.getSelectedFile();
System.out.println("Attaching file: " + selectedFile);
}
请注意,设置 approve 按钮的文本不会更改该方法的返回值。您仍然需要检查它是否返回了一个JFileChooser.APPROVE_OPTION,这样您就可以继续获取所选择的文件。
Tip
当您使用showOpenDialog()和showSaveDialog()方法时,approve 按钮的默认文本取决于外观。在 Windows 上,它们分别是Open和Save。
一个JFileChooser让你设置一个文件过滤器。文件过滤器是在对话框中显示文件之前应用的一组条件。文件过滤器是FileFilter类的一个对象,它在javax.swing.filechooser包中。FileFilter级是一个abstract级。要创建文件过滤器,您需要创建一个从FileFilter类继承而来的类,并覆盖accept()和getDescription()方法。当文件选择器想要显示文件时,使用文件引用调用该方法。如果accept()方法返回true,则显示该文件。否则,不会显示该文件。下面的代码片段创建并设置了一个文件过滤器,只显示一个目录或一个扩展名为doc的文件。请记住,用户需要导航到文件系统,您必须显示目录。
// Create a file filter to show only a directory or .doc files
FileFilter filter = new FileFilter() {
@Override
public boolean accept(File f) {
if (f.isDirectory()) {
return true;
}
String fileName = f.getName().toLowerCase();
if (fileName.endsWith(".doc")) {
return true;
}
return false; // Reject any other files
}
@Override public String getDescription() {
return "Word Document";
}
};
// Set the file filter
fileChooser.setFileFilter(filter);
int returnValue = fileChooser.showDialog(null, "Attach");
if (returnValue == JFileChooser.APPROVE_OPTION) {
// Process the file
}
基于文件扩展名设置文件过滤器是如此普遍,以至于从FileFilter类继承而来的FileNameExtensionFilter类直接支持它。它的构造函数接受文件扩展名及其描述。第二个参数是可变长度参数。请注意,文件扩展名是文件名中最后一个点之后的部分。如果文件名中没有点,则它没有扩展名。在你创建了一个FileNameExtensionFilter类的对象后,你需要调用文件选择器的方法来设置一个过滤器。下面的代码片段添加了“java”和“jav”作为文件扩展名过滤器。
FileNameExtensionFilter extFilter =
new FileNameExtensionFilter("Java Source File", "java", "jav");
fileChooser.addChoosableFileFilter(extFilter);
您可以在文件选择器中添加多个文件扩展名过滤器。它们作为文件类型显示在文件选择器下拉列表中。如果要限制用户只能选择您设置为文件过滤器的文件,您需要删除允许用户选择任何文件的文件过滤器,该过滤器称为“接受所有文件过滤器”。在 Windows 上,文件类型显示为“All Files(*.*)”。
// Disable "accept all files filter"
fileChooser.setAcceptAllFileFilterUsed(false);
您可以使用方法检查“接受所有文件过滤器”是否已启用,如果文件选择器正在使用此过滤器,该方法将返回true。您可以使用getAcceptAllFileFilter()方法获得“接受所有文件过滤器”的引用。下面的代码片段设置了“接受所有文件过滤器”(如果尚未设置)。
if (!fileChooser.isAcceptAllFileFilterUsed()) { fileChooser.setAcceptAllFileFilterUsed(true); }
Tip
一个JFileChooser有许多你可以在应用中使用的特性。有时你可能想得到一个文件类型的相关图标。您可以通过使用文件选择器的getIcon(java.io.File file)方法获得文件类型的关联图标,该方法返回一个Icon对象。注意,您可以使用一个JLabel组件来显示一个Icon对象。当它显示在对话框中时,它还提供了一种机制来侦听用户执行的选择更改和其他操作。
颜色选择对话框
JColorChooser允许您使用对话框选择颜色。它是可定制的。可以向默认颜色选择器添加更多面板。也可以将颜色选择器组件嵌入到容器中。它提供了监听颜色选择器组件上的用户操作的方法。它的常见用法非常简单。您需要调用它的showDialog()静态方法,该方法将返回一个代表用户选择的颜色的java.awt.Color对象。否则返回null。我将在本章的后面介绍Color类。
showDialog()方法的签名如下。它允许您指定对话框的父组件和标题。您还可以设置初始颜色,它将显示在对话框中。
static Color showDialog(Component parentComponent, String title, Color initialColor)
下面的代码片段让用户使用JColorChooser选择一种颜色,并在标准输出上打印一条消息:
// Display a color chooser dialog
Color color = JColorChooser.showDialog(null, "Select a color", null);
// Check if user selected a color
if (color == null) {
System.out.println("You cancelled or closed the color chooser");
}
else {
System.out.println("You selected color: " + color);
}
窗户
像JFrame一样,JWindow是另一个顶级容器。这是一座未经装饰的JFrame。它没有标题栏、窗口菜单等功能。它不是一个非常常用的顶级容器。您可以将它用作启动窗口,当应用启动时显示一次,几秒钟后自动消失。关于如何在 Java 应用中显示闪屏的更多细节,请参考java.awt.SplashScreen类的 API 文档。像JFrame一样,你可以给JWindow添加 Swing 组件。
使用颜色
java.awt.Color类的对象代表一种颜色。您可以使用 RGB(红色、绿色和蓝色)组件创建一个Color对象。RGB 值可以指定为float或int值。作为一个float值,RGB 中每个分量的范围从 0.0 到 1.0。作为一个int值,RGB 中每个分量的范围是从 0 到 255。还有一个叫做 alpha 的成分与颜色相关联。颜色的 alpha 值定义了颜色的透明度。作为一个float,其取值范围为 0.0 到 1.0,作为一个int,其取值范围为 0 到 255。alpha 值为 0.0 或 0 表示颜色完全透明,而值为 1.0 或 255 表示颜色完全不透明。
您可以创建一个Color对象,如下所示。注意构造函数Color(int red, int green, int blue)中 RGB 分量的值。
// Create red color
Color red = new Color(255, 0, 0);
// Create green color
Color green = new Color(0, 255, 0);
// Create blue color
Color blue = new Color(0, 0, 255);
// Create white color
Color white = new Color(255, 255, 255);
// Create black color
Color black = new Color(0, 0, 0);
alpha 分量被隐式设置为 1.0 或 255,这意味着如果不指定颜色的 alpha 分量,则该颜色是不透明的。以下代码片段通过将 alpha 组件指定为 0 来创建红色透明色:
// Create a transparent red color. The last argument of 0 is the alpha value.
Color transparentRed = new Color(255, 0, 0, 0);
Color类为常用的颜色定义了许多颜色常数。例如,您不需要创建红色。相反,你可以使用Color.red或Color.RED常数。Color.red常量从 Java 1.0 开始就存在了。Java 1.4 中增加了相同常量Color.RED的大写版本,以遵循常量的命名约定(常量的名称应该是大写的)。同样,你还有Color.black、Color.BLACK、Color.green、Color.GREEN、Color.darkGray、Color.DARK _ GRAY等。如果您有一个Color对象,您可以分别使用它的getRed()、getGreen()、getBlue()和getAlpha()方法获得它的红色、绿色、蓝色和 alpha 组件。
还有另一种方法来指定颜色,那就是使用 HSB(色调、饱和度和亮度)组件。Color类有两个叫做RGBtoHSB()和HSBtoRGB()的方法,可以让你从 RBG 模型转换到 HSB 模型,反之亦然。
一个Color对象与 Swing 组件的setBackground(Color c)和setForeground(Color c)方法一起使用。所有 Swing 组件都从JComponent继承了这些方法。这些方法调用可能会被外观忽略。背景色是用来绘制组件的颜色,而前景色通常是组件中显示的文本的颜色。当你设置一个组件的背景颜色时,有一件重要的事情需要考虑,那就是透明度。如果组件是透明的,它不会在其边界内绘制像素。相反,它让容器的像素显示出来。为了让背景色生效,你必须通过调用组件的setOpaque(true)方法使其不透明。下面的代码创建了一个JLabel,并将其背景色设置为红色,前景色(或文本)设置为黑色:
JLabel testLabel = new JLabel("Color Test");
// First make the JLabel opaque. By default, a JLabel is transparent.
testLabel.setOpaque(true);
testLabel.setBackground(Color.RED);
testLabel.setForeground(Color.BLACK);
Tip
Color类的对象是不可变的。它没有任何方法可以让你在创建一个Color对象后设置颜色分量值。这使得共享Color对象成为可能。
使用边框
Swing 为您提供了在组件边缘绘制边框的能力。有不同种类的边界:
- 斜角边框
- 柔和的斜面边框
- 蚀刻边框
- 线条边框
- 标题边框
- 哑光边框
- 空白边框
- 复合边框
图 2-38 显示了不同种类的边框是如何使用窗口外观来显示的。

图 2-38。
Different types of borders
尽管您可以为任何 Swing 组件设置边框,但是 Swing 组件的实现可能会忽略它。使用带JPanel的标题边框来产生分组效果是很常见的。许多 GUI 工具都有一个分组框 GUI 组件来对相关组件进行分组。Java 没有分组框组件。如果你需要一个分组效果,你需要把你的相关组件放在一个JPanel里面,并给它设置一个标题边框。图 2-39 显示了一个JPanel,它有五个与地址相关的字段,带有一个标题设置为Address的标题边框。

图 2-39。
Creating a group box effect using a JPanel with a titled border
为 Swing 组件设置边框很容易:您需要创建一个 border 对象并使用组件的setBorder(Border b)方法。Border是一个由所有类实现的接口,这些类的实例代表一种特定的边界。每种边框都有一个类。你也可以通过从AbstractBorder类继承一个类来创建一个自定义边框。所有与边界相关的类和Border接口都在javax.swing.border包中。
对象是为共享而设计的。虽然您可以直接使用 border 类来创建一个 border 对象,但是建议您使用javax.swing.BorderFactory类来创建一个边框,以便可以共享这些边框对象。BorderFactory类负责边界对象的缓存和共享。你只需要使用它的createXxxBorder()方法来创建一个特定类型的边框,其中Xxx是一个边框类型。表 2-18 列出了所有边界类型的边界等级。
表 2-18。
Available Border Classes
| 边框类型 | 边界等级 | | --- | --- | | 斜角边框 | `BevelBorder` | | 柔和的斜面边框 | `SoftBevelBorder` | | 蚀刻边框 | `EtchedBorder` | | 线条边框 | `LineBorder` | | 标题边框 | `TitledBorder` | | 哑光边框 | `MatteBorder` | | 空白边框 | `EmptyBorder` | | 复合边框 | `CompoundBorder` |以下代码片段创建了不同种类的边框:
// Create bevel borders
Border bevelRaisedBorder = BorderFactory.createBevelBorder(BevelBorder.RAISED);
Border bevelLoweredBorder = BorderFactory.createBevelBorder(BevelBorder.LOWERED);
// Create soft bevel borders
Border softBevelRaisedBorder = BorderFactory.createSoftBevelBorder(BevelBorder.RAISED);
Border softBevelLoweredBorder = BorderFactory.createSoftBevelBorder(BevelBorder.LOWERED);
// Create etched borders
Border etchedRaisedBorder = BorderFactory.createEtchedBorder(EtchedBorder.RAISED);
Border etchedLoweredBorder = BorderFactory.createEtchedBorder(EtchedBorder.LOWERED);
// Create line borders
Border lineBorder = BorderFactory.createLineBorder(Color.BLACK);
Border lineThickerBorder = BorderFactory.createLineBorder(Color.BLACK, 3);
// Create titled borders
Border titledBorderAtTop =
BorderFactory.createTitledBorder(etchedLoweredBorder,
"Title text goes here",
TitledBorder.CENTER,
TitledBorder.TOP);
Border titledBorderAtBottom =
BorderFactory.createTitledBorder(etchedLoweredBorder,
"Title text goes here",
TitledBorder.CENTER,
TitledBorder.BOTTOM);
// Create a matte border
Border matteBorder = BorderFactory.createMatteBorder(1,3,5,7, Color.BLUE);
// Create an empty border
Border emptyBorder = BorderFactory.createEmptyBorder();
// Create compound borders
Border twoCompoundBorder = BorderFactory.createCompoundBorder(etchedRaisedBorder, lineBorder);
Border threeCompoundBorder =
BorderFactory.createCompoundBorder(titledBorderAtTop, twoCompoundBorder);
您可以为组件设置边框,如下所示:
myComponent.setBorder(matteBorder);
斜角边框通过在边框的内侧和外侧边缘使用阴影和高光,为您提供三维效果。你可以提高或降低效果。柔和的斜面边框是具有柔和边角的斜面边框。
蚀刻的边框给你一种雕刻的效果。它有两种味道:抬高的和放低的。
线条边框只是画一条线。您可以指定线条的颜色和粗细。
您可以为任何边框类型提供标题。边框的标题是可以显示在边框中指定位置的文本,例如在顶部/底部边框的中间或顶部上方/底部下方。您还可以指定标题文本的对齐方式、颜色和字体。请注意,要使用标题边框,您必须有另一个边框对象。标题边框只是让你为另一种边框提供标题文本。
无光边框允许您用图标装饰边框。如果没有图标,可以指定边框的粗细。
空边框,顾名思义,不显示任何东西。你能猜到为什么你需要一个空的边框吗?边框增加了组件周围的空间。如果您只想在组件周围添加空格,可以使用空边框。空白边框允许您分别指定四条边的间距。
复合边框是一种复合边框,允许您将任意两种边框组合成一个 border 对象。嵌套的层数没有限制。您可以通过用前两个边框创建复合边框来组合三个边框,然后将复合边框与第三个边框组合来创建最终的复合边框。
使用字体
字体用于在视觉上表示文本,例如在计算机屏幕、打印纸或任何其他设备上。类的一个对象代表了 Java 程序中的一种字体。你已经在几乎每个程序中使用了Font对象,而没有直接引用Font类。Java 负责用特定的字体显示文本。例如,您一直在使用显示标签的按钮。为了显示按钮的标签,Java 一直使用默认字体。您可以使用Font对象为 Java 程序中显示的任何文本指定字体。在代码中使用Font对象很简单:创建一个Font类的对象,并使用组件的setFont(Font f)方法。在使用Font类之前,让我们定义术语“字体”和相关术语。
在计算机的内存中,一切都是用 0 和 1 表示的数字。所以一个字符在内存中也是用 0 和 1 来表示的。你如何在电脑屏幕或一张纸上表现一个字符?一个字符用一个符号显示在屏幕或纸上。代表字符的符号形状称为。您可以将字形视为字符的图形表示(或图像)。字符和字形之间的关系并不总是一对一的。
一组字符的字形的特定设计称为。注意,字样是字符(字形)的视觉表示的设计方面,它不指字形的特定实现。表 2-19 列出了一些字体类别及其描述和示例文本。如果在不支持所有字体的设备(如 Kindle)上查看,表格中的示例文本可能不会以相同的字体显示。有些字体的名称是泰晤士报、信使报、Helvetica、加拉蒙德等。
表 2-19。
Examples of Typefaces
| 字体 | 描述 | 示例文本 | | --- | --- | --- | | 衬线 | 字形在行尾有结束笔画。请注意衬线字体和无衬线字体中每个字符的结束笔画的区别。在 Windows 上,它被称为 Roman。例如:Times New Roman。 | 敏捷的棕色狐狸... | | 无衬线字体 | 与衬线不同,字形没有结束笔画。比较此类别和衬线的文本示例。你会发现无衬线字体是由普通线条组成的。在 Windows 上,它被称为瑞士。例如:Arial。 | 敏捷的棕色狐狸... | | 草书 | 它看起来像手写文本,其中一个单词中的后续字形通常是连接在一起的。它通常用于书法。在 Windows 上,它被称为脚本。例如:Mistral AV。 | 敏捷的棕色狐狸... | | 幻想 | 这是一种装饰字体。在窗户上,它被称为装饰。例如:影响。 | 敏捷的棕色狐狸... | | 单一间隔 | 代表所有字符的所有字形都具有相同的宽度。在 Windows 上,它被称为 Modern。通常,它用于计算机程序中。 | `The quick brown fox...` |除了它的形状设计,一个角色的视觉表现还有另外两个组成部分:风格和大小。风格是指其特征,如粗体(黑色或浅色)、斜体和常规(或罗马体)。尺码是 10、12、14 等。字符的高度以磅为单位,其中一磅为 1/72 英寸。字符的宽度在中指定。间距决定了一英寸中可以显示多少个字符。螺距的典型值范围从 8 到 14。
现在让我们来定义术语“字体”字体是以特定字样、风格和大小表示一组字符的一组字形。您可以拥有使用相同字样的字体,但它们具有不同的样式和大小。这种字体(相同的字样,但不同的风格和大小)的集合被称为字体族。例如,Times 是一个字体系列名称,包含 Times Roman、Times Bold、Times Bold Italic 等字体。
根据存储和呈现的方式,字体可以分为位图字体或矢量字体(也称为面向对象字体或轮廓字体)。在位图字体中,每个字符都以特定样式和大小的位图形式(代表每一位)存储。当您需要在屏幕上呈现一个字符或在纸上打印它时,您需要找到该样式和大小的字符的位图并呈现它。在矢量字体中,几何算法定义每个字符的形状,而不涉及特定的大小。当需要以特定大小的矢量字体呈现字符时,该算法适用于该大小。这就是矢量字体也被称为可缩放字体的原因。TrueType 和 PostScript 是使用矢量字体的字体技术。所有 Java 实现都需要支持 TrueType 字体。
计算机上可用的字体数量可能会有很大差异。您的操作系统可能会安装一些字体,您可能会添加一些字体,或者您可能会删除一些字体。由于 Java 是为在各种操作系统上工作而设计的,它允许你使用一种字体的逻辑字体族名称,并且它会为你找出最佳的物理(真正的)字体。这样,您就不必担心实际的字体名称,也不必担心它们是否在所有执行您的程序的计算机上都可用。Java 定义了五种逻辑字体系列名称,并根据运行它的计算机将它们映射到物理字体系列名称。五种逻辑字体系列名称如下:
- 衬线
- 无锡里夫
- 对话
- 对话输入
- 单一间隔
创建字体对象时,需要指定三个元素:逻辑系列名称、样式和大小。以下代码片段创建了一些Font对象:
// Create serif, plain font of size 10
Font f1 = new Font(Font.SERIF, Font.PLAIN, 10);
// Create SansSerif, bold font of size 10
Font f2 = new Font(Font.SANS_SERIF, Font.BOLD, 10);
// Create dialog, bold font of size 15
Font f3 = new Font(Font.DIALOG, Font.BOLD, 15);
// Create dialog input, bold and italic font of size 15
Font f4 = new Font(Font.DIALOG_INPUT, Font.BOLD|Font.ITALIC, 15);
Font类包含逻辑字体系列名称的常量。如果你想对一个字体对象应用多种样式,比如粗体和斜体,你需要像在Font.BOLD|Font.ITALIC中一样使用Font.BOLD和Font.ITALIC的位掩码联合。
要为 Swing 组件设置字体,您需要使用该组件的方法,就像这样:
JButton closeButton = new JButton("Close");
closeButton.setFont(f4);
Font类有几个方法可以让你使用字体对象。例如,您可以使用getFamily()、getStyle()和getSize()方法分别获取字体对象的系列名称、样式和大小。
验证组件
组件可以是有效的,也可以是无效的。除非另有说明,本节中的短语“组件”也包括容器。您可以使用isValid()方法来检查组件是否有效。如果组件有效,该方法返回true。否则,它返回false。如果一个组件的大小和位置已经计算出来,并且它的子组件也是有效的,那么这个组件就是有效的。如果一个组件无效,这意味着它的大小和位置需要重新计算,并且需要在它的容器中重新布局。
向容器添加组件或从容器中移除组件时,容器会被标记为无效。在容器第一次可见之前,容器被验证。容器的验证过程计算其容器层次结构中所有子容器的大小和位置。考虑下面的代码片段来显示一个框架:
MyFrame frame = new MyFrame("Test Frame");
frame.pack();
frame.setVisible(true);
pack()方法做两件事:
- 首先,它计算框架所有子框架的大小和位置(即验证框架)。
- 第二,它调整框架的大小,使其子框架正好适合它。
代码中的setVisible()方法足够聪明,不会再次验证该帧,因为pack()方法已经验证了该帧。如果你不调用pack()方法,在调用setVisible()方法之前,setVisible()方法将验证框架。
因此,组件在第一次显示之前是有效的。组件是如何失效的?在容器中添加/删除组件会使容器无效。设置某些属性(如组件的大小)也会使该组件无效。当一个组件变得无效时,它的无效性会向上传播到容器层次结构。您还可以通过调用组件或容器的invalidate()方法来使其无效。注意,调用invalidate()方法将使组件无效,并且它将无效性传播到包含层次结构中。它需要将包含层次结构中的所有容器标记为无效的原因是,如果一个组件被再次布局(通过重新计算其大小/位置),它也会影响其他组件的大小/位置。因此,如果一个组件失效了,容器层次结构中的所有组件和容器也会被标记为无效。
如何再次验证组件?你需要使用组件或者容器的validate()方法。与invalidate()方法不同,validate()方法沿着容器层次结构向下传播,它验证调用它的组件的所有子组件/容器。您可能需要在调用validate()方法之后调用repaint()方法,以便重新绘制屏幕。
您也可以重新验证组件。请注意,重新验证选项仅适用于JComponent并且不适用于容器。您可以通过调用组件的方法来重新验证组件。它在父容器上安排一个validate()方法调用。验证组件的哪个父容器?是直系父母、祖父母还是曾祖父母等。?容器可以是验证根。您可以通过使用isValidateRoot()方法来测试一个容器是否是一个验证根。如果这个方法返回true,那么这个容器就是一个验证根。当你在一个组件上调用revalidate()方法时,它在容器层次结构中一直向上,直到它找到一个作为验证根的容器。JRootPane和JScrollPane是验证根。对验证根的validate()方法的调用被安排在事件调度线程上。如果有对revalidate()的多次调用,它们都被组合起来,一个组件只被重新验证一次。
绘制组件和形状
绘画机制是任何 GUI 的核心。你知道在屏幕上显示一个JFrame需要什么吗?这是一个非常复杂的过程。这是通过绘制一个图像来完成的,你在屏幕上看到的是一个JFrame。当你按下JFrame内的一个JButton时,被那个JButton占据的区域会用不同的阴影和颜色重新绘制,给你一种按钮被按下的印象。大多数情况下,Swing 会在适当的时间绘制屏幕的适当区域。您可能会遇到需要重新绘制 Swing 组件区域的情况。例如,当您在一个 Swing 容器中添加或删除一个可见的组件时,您需要验证并重新绘制该容器,以便正确地重新绘制屏幕上修改过的区域。
Swing 中的一切都有一个经理!您还有一个重画管理器,它是该类的一个实例。它提供油漆服务。您可以通过调用组件上的repaint()方法来请求重画组件。repaint()方法被重载。也可以只重画组件的一部分,而不是整个组件。对方法的调用在事件调度线程中排队。当重画管理器开始重画组件时,如果许多重画请求未决,它将只重画组件一次。
如何在 Swing 组件上执行自定义绘制?Swing 允许您使用回调机制在组件上执行自定义绘制。JComponent类有一个名为paintComponent(Graphics g)的回调方法。Graphics级在java.awt包里。它用于在组件上绘图。注意,绘图可以在各种设备上实现,例如在计算机屏幕、屏幕外图像或打印机上。要实现一个组件的自定义绘制,覆盖它的paintComponent()方法。JComponent类中的paintComponent()方法负责绘制组件的背景。为了确保组件的背景被正确绘制,您需要从组件的paintComponent()方法中调用JComponent的paintComponent()方法。该方法的典型代码如下:
import java.awt.Graphics;
public class YourCustomSwingComponent extends ASwingComponent {
@Override
public void paintComponent(Graphics g) {
// Paint the background
super.paintComponent(g);
// Your custom painting code goes here
}
}
每当需要重画或者当程序调用repaint()方法时,组件的paintComponent()方法被调用。
当您在 Swing 组件上调用repaint()方法时,重画管理器可能会不止绘制您请求绘制的组件。在油漆一个部件之前,有许多事情要考虑。在绘制组件时,组件的背景及其与其他组件的重叠区域是需要考虑的两个最重要的事情。如果组件不是不透明的,则必须在绘制该组件之前绘制该组件的容器。这是必要的,这样你就不会看穿组件的垃圾背景。如果一个组件与另一个组件重叠,至少重叠区域必须考虑显示重叠区域的正确颜色和形状。重叠区域的涂漆将包括所有重叠部件的涂漆。
一个对象有许多方法可以用来绘制几何形状和字符串。你可以画不同的形状,如矩形、椭圆形、弧形等。一个Graphics对象有许多绘图属性,如字体、颜色、坐标系(称为平移)、剪辑(定义绘图区域)、要在其上绘图的组件等。在paintComponent()方法参数中的一个Graphics对象已经设置了许多属性。例如,
- 字体设置为组件的字体。
- 颜色设置为组件的前景色。
- 平移设置为组件的左上角。组件的左上角代表原点,即坐标(0,0)。
- 剪辑被设置为组件中需要绘制的区域。
您可以在paintComponent()方法中更改Graphics对象的这些属性。然而,如果你想改变翻译或剪辑,你需要小心。你应该创建一个Graphics对象的副本,并使用该副本进行绘图,而不是改变原始Graphics对象的属性。您可以使用Graphics类的create()方法来创建一个Graphics对象的副本。确保在Graphics对象的副本上调用dispose()方法,以释放它用尽的系统资源。复制和使用Graphics对象的典型逻辑如下所示:
public void paintComponent(Graphics g) {
// Create a copy of the passed in Graphics object
Graphics gCopy = g.create();
// Change the properties of gCopy and use it for drawing here
// Dispose the copy of the Graphics object
gCopy.dispose();
}
当您为传递给方法的组件使用Graphics对象时,有一些事情需要注意。
它使用笛卡尔坐标系,原点位于组件的左上角。
The x-axis extends to the right and y-axis extends down, as shown in Figure 2-40.

图 2-40。
The coordinate system used by a graphics object inside the paintComponent() method of a component. It shows the coordinates of four corners of a 600 X 200 JPanel
当您使用
Graphics对象绘图时,您的绘图可能会超出组件的边界。然而,重画管理器在Graphics对象中设置的剪辑区域之外的任何图形都将被忽略。事实上,在paintComponent()方法返回后,重画管理器将只使用已绘制组件的剪辑区域在屏幕上显示它。这就是为什么你不应该在一个paintComponent()方法中改变Graphics对象的 clip 属性的原因。clip 属性设置为需要绘制的组件区域。Graphics对象的 translation 属性用于设置绘图的坐标系。传递给paintComponent()方法的Graphics对象已经设置了 translation 属性,因此组件的左上角代表坐标系的原点(0,0)。如果您在paintComponent()方法中更改了Graphics对象的 translation 属性,您最好知道自己在做什么。使用
Graphics对象的当前颜色和字体进行绘制。
在Graphics类中有许多方法可以让你画出不同种类的形状,比如圆角矩形、弧形、多边形等等。表 2-20 列出了其中的一些方法。关于方法的完整列表,请参考Graphics类的 API 文档。
表 2-20。
Methods of the Graphics Class
| 方法 | 描述 | | --- | --- | | `void drawLine(int x1, int y1, int x2, int y2)` | 从点`(x1, y1)`到点`(x2, y2).`画一条直线 | | `void drawRect(int x, int y,` `int width, int height)` | 绘制左上角坐标为`(x, y)`的矩形。指定的`width`和`height`分别是矩形的宽度和高度。 | | `void fillRect(int x, int y,` `int width, int height)` | 与`drawRect()`方法相同,但有两点不同。它用`Graphics`对象的当前颜色填充该区域。它的宽度和高度比指定的`width`和`height`小一个像素。 | | `void drawOval(int x, int y, int width, int height)` | 绘制一个适合矩形的椭圆形,该矩形以点`(x, y)`作为其左上角并具有指定的宽度和高度。如果你指定相同的宽度和高度,它会画一个圆。 | | `void fillOval(int x, int y, int width, int height)` | 它绘制一个椭圆形并用当前颜色填充该区域。 | | `void drawstring(String str, int x, int y)` | 它绘制指定的字符串`str`。最左边字符的基线在点`(x, y)`。 |通常,您使用一个JPanel作为定制绘图的画布。清单 2-12 中的代码显示了一个名为的类,它继承自JPanel类。在其构造函数中,它设置自己的首选大小。它覆盖了paintComponent()方法来绘制一些自定义的形状和字符串。图 2-41 显示了运行DrawingCanvas类时的屏幕。
清单 2-12。用作绘图画布的自定义 JPanel
// DrawingCanvas.java
package com.jdojo.swing;
import javax.swing.JPanel;
import java.awt.Graphics;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.BasicStroke;
import javax.swing.JFrame;
public class DrawingCanvas extends JPanel {
public DrawingCanvas() {
this.setPreferredSize(new Dimension(600, 75));
}
@Override
public void paintComponent(Graphics g) {
// Paint its background
super.paintComponent(g);
// Draw a line
g.drawLine(10, 10, 50, 50);
// Draw a rectangle
g.drawRect(80, 10, 40, 20);
// Draw an oval
g.drawOval(140, 10, 40, 20);
// Fill an oval
g.fillOval(200, 10, 40, 20);
// Draw a circle
g.drawOval(250, 10, 40, 40);
// Draw an arc
g.drawArc(300, 10, 50, 50, 60, 120);
// Draw a string
g.drawString("Hello Swing!", 350, 30);
// Draw a thicker rectangle using Graphics2D
Graphics2D g2d = (Graphics2D)g;
g2d.setStroke(new BasicStroke(4));
g2d.drawRect(450, 10, 50, 50);
}
public static void main(String[] args) {
JFrame frame =
new JFrame("Sample Drawings Using a Graphics Object");
frame.getContentPane().add(new DrawingCanvas());
frame.pack();
frame.setVisible(true);
}
}

图 2-41。
Drawing shapes on a custom JPanel using a graphics object
在运行时,您会得到一个传递给该方法的Graphics2D类的实例。Graphics2D类继承了Graphics类,它有一个非常强大的 API 来绘制几何图形。例如,当您使用Graphics对象时,它使用 1.0 的描边(线宽)绘制形状。如果使用Graphics2D,可以使用自定义笔画。下面的代码片段在你的DrawingCanvas类的paintComponent()方法中使用 4.0 的笔画绘制一个矩形。要使用paintComponent()方法中的Graphics2D API,将传入的Graphics对象转换为Graphics2D,如下所示:
Graphics2D g2d = (Graphics2D)g;
g2d.setStroke(new BasicStroke(4));
g2d.drawRect(450, 10, 50, 50);
JComponent类有一个返回组件的Graphics对象的方法。如果您需要在组件的paintComponent()方法之外绘制组件,您可以使用这个方法来获取组件的Graphics对象,以便使用它进行绘制。
即时绘画
Swing 负责在适当的时候重新绘制可见的组件区域。你也可以通过调用组件的repaint()方法来请求组件的重画。对repaint()方法的调用是异步的。也就是说,它不是立即执行的。它在事件调度线程上排队,并将在将来的某个时间执行。有时情况可能需要立即上漆。使用组件的paintImmediately()方法立即进行绘制。该方法被重载。这两个版本声明如下:
void paintImmediately(int x, int y, int w, int h)void paintImmediately(Rectangle r)
Tip
如果需要更频繁地绘制或循环绘制,调用repaint()方法会更有效。对repaint()方法的多次调用被合并成一次调用,而对paintImmediately()方法的调用是单独进行的。
双重缓冲
可以使用不同的技术在屏幕上绘制组件。如果组件直接绘制在屏幕上,则称为屏幕绘制。如果一个组件是使用离屏缓冲区绘制的,并且该缓冲区是一步复制到屏幕上的,这就叫做双缓冲。还有一种绘制组件的技术叫做翻页。翻页使用计算机显卡的视频指针功能来显示视频,视频指针是视频内容的地址。与双缓冲类似,您绘制要在离屏缓冲区上显示的内容。当您在离屏缓冲区上完成绘制时,您将图形卡的视频指针更改到这个离屏缓冲区,图形卡将负责在屏幕上显示图像。与双缓冲不同,翻页不会将屏幕外缓冲中的内容复制到屏幕上的缓冲中。相反,它将图形卡重定向到新的缓冲区。双缓冲和翻页可以避免组件绘制时屏幕闪烁,从而提供更好的用户体验。
Swing 使用双缓冲来绘制所有组件。它允许您禁用组件的双缓冲。当您禁用双缓冲时,会有一个问题。有时候,禁用双缓冲可能真的没有任何作用。如果正在绘制一个容器,Swing 会检查该容器是否启用了双缓冲。如果为容器启用了双缓冲,那么它的所有子组件都将使用双缓冲。因此,简单地禁用组件上的双缓冲没有什么帮助。如果您想禁用双缓冲,您可能只想在容器层次结构的最顶层禁用它,即JRootPane。重画管理器还允许您为应用全局启用/禁用双缓冲,如下所示:
RepaintManager currentManager = RepaintManager.currentManager(component);
currentManager.setDoubleBufferingEnabled(false);
当启用双缓冲时,Swing 将创建一个离屏图像,并将该离屏图像的图形传递给JComponent的paintComponent()方法。当你在paintComponent()方法中使用Graphics对象绘制任何东西时,本质上你是在屏幕外的图像上绘制。最后,Swing 会将屏幕外的图像复制到屏幕上。
双缓冲还允许你在程序中创建一个离屏图像。您可以绘制该屏幕外图像,并在应用中任何需要的地方使用该图像。您需要使用组件的createImage()方法来创建一个离屏图像。下面的代码创建了一个名为OffScreenImagePanel的自定义JPanel。在它的paintComponent()方法中,它创建一个离屏图像,用红色填充该图像,并使用该图像绘制到JPanel。这是一个微不足道的例子。但是,它演示了在应用中使用离屏图像所需执行的步骤。
public class OffScreenImagePanel extends JPanel{
public OffScreenImagePanel() {
this.setPreferredSize(new Dimension(200, 200));
}
public void paintComponent(Graphics g) {
super.paintComponent(g);
// Create an offscreen image and fill a rectangle with red
int w = this.getWidth();
int h = this.getHeight();
Image offScreenImage = this.createImage(w, h);
Graphics imageGraphics = offScreenImage.getGraphics();
imageGraphics.setColor(Color.RED);
imageGraphics.fillRect(0, 0, w, h);
// Draw the offscreen image on the JPanel
g.drawImage(offScreenImage, 0, 0, null);
}
}
重新访问 JFrame
你已经在你写的几乎每个程序中使用了这一章中的 JFrames。在这一节中,我将讨论一些重要的事件和JFrame的性质。
您可以使用setExtendedState(int state)方法以编程方式设置JFrame的状态。使用JFrame类继承的java.awt.Frame类中定义的常量来指定状态。
// Display the JFrame maximized
frame.setExtendedState(JFrame.MAXIMIZED_BOTH);
通常,你可以使用标题栏角落里的状态按钮或状态菜单来改变JFrame的状态。表 2-21 列出了可用于改变JFrame状态的常数。
表 2-21。
The List of Constants That Define States of a JFrame
| JFrame 状态常数 | 描述 | | --- | --- | | `NORMAL` | `JFrame`以正常尺寸显示。 | | `ICONIFIED` | `JFrame`以最小化状态显示。 | | `MAXIMIZED_HORIZ` | `JFrame`在水平方向最大化显示,但在垂直方向以正常尺寸显示。 | | `MAXIMIZED_VERT` | `JFrame`垂直最大化显示,但水平以正常尺寸显示。 | | `MAXIMIZED_BOTH` | `JFrame`水平和垂直最大化显示。 |有时你可能想在你的JFrame或JDialog中使用一个默认按钮。默认按钮是JButton类的一个实例,当用户按下键盘上的一个键时就会被激活。激活默认按钮的键是由外观定义的。通常,激活默认按钮的键是Enter键。您可以为JRootPane设置一个默认按钮,该按钮出现在JFrame、JDialog、JWindow、JApplet和JInternalFrame中。通常,您将OK按钮设置为JDialog上的默认按钮。如果一个JRootPane有一个默认按钮集,按下Enter键将激活那个按钮,如果你有一个动作执行的事件处理程序添加到那个按钮,你的代码将被执行。
// Create a JButton
JButton okButton = new JButton("OK");
// Add an event handler to okButton here...
// Set okButton as the default button
frame.getRootPane().setDefaultButton(okButton);
您可以添加一个窗口监听器到一个JFrame或任何其他顶层 Swing 窗口,它将通知您窗口状态的七种变化。下面的代码片段向名为frame的JFrame添加了一个窗口监听器。如果您对监听少量的窗口状态变化感兴趣,您可以使用WindowAdapter类来代替WindowListener接口。WindowAdapter类提供了WindowListener接口中所有七个方法的空实现。
frame.addWindowListener(new WindowListener() {
@Override
public void windowOpened(WindowEvent e) {
System.out.println("JFrame has been made visible first time");
}
@Override
public void windowClosing(WindowEvent e) {
System.out.println("JFrame is closing.");
}
@Override
public void windowClosed(WindowEvent e) {
System.out.println("JFrame is closed.");
}
@Override
public void windowIconified(WindowEvent e) {
System.out.println("JFrame is minimized.");
}
@Override
public void windowDeiconified(WindowEvent e) {
System.out.println("JFrame is restored.");
}
@Override
public void windowActivated(WindowEvent e) {
System.out.println("JFrame is activated.");
}
@Override
public void windowDeactivated(WindowEvent e) {
System.out.println("JFrame is deactivated.");
}
});
// Use the WindowAdapter class to intercept only the window closing event
frame.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
System.out.println("JFrame is closing.");
}
});
当你使用完一个窗口(JFrame、JDialog或JWindow)时,你应该调用它的dispose()方法,这将使它不可见,并释放资源给操作系统。请注意,dispose()方法并不销毁或垃圾收集窗口对象。只要你持有窗口的引用并且它是可访问的,Java 就不会破坏你的窗口,你可以通过调用它的setVisible(true)方法再次显示它。
摘要
Swing 提供了大量组件来开发 GUI 应用。大多数 Swing 组件都是轻量级组件,它们使用 Java 代码进行重绘,而无需使用本机对等组件。JComponent类是所有 Swing 组件的基类。可以包含其他组件的组件称为容器。Swing 提供了两种类型的容器:顶级容器和非顶级容器。顶级容器不包含在另一个容器中,它可以直接显示在桌面上。JFrame类的一个实例代表一个顶级容器。
JButton类的一个对象代表一个按钮。按钮也称为按钮或命令按钮。用户按下或点击一个JButton来执行一个动作。按钮可以显示文本和/或图标。
JPanel类的对象代表一个可以包含其他组件的容器。典型地,一个JPanel被用来将相关的组件组合在一起。一个JPanel是非顶级容器。
JLabel类的对象表示显示文本、图标或两者的标签组件。通常,JLabel中的文本描述了另一个组件。
Swing 提供了几个文本组件,允许您显示和编辑不同类型的文本。JTextField类的一个对象用于处理一行纯文本。JTextArea的一个对象用于处理多行纯文本。JPasswordField的一个对象用于处理单行文本,其中文本中的实际字符被替换为回显字符。JFormattedTextField的一个对象允许您使用一行纯文本,您可以指定文本的格式,例如以 mm/dd/yyy 格式显示日期。JEditorPane的一个对象可以让你处理 HTML 和 RTF 格式的文本。JTextPane的一个对象允许您处理带有嵌入图像和组件的样式化文档。您可以向文本组件添加输入验证器,以验证用户输入的文本。InputVerifier类的一个实例充当输入验证器。您可以使用JComponent类的setInputVerifier()方法为文本组件设置输入验证器。
Swing 提供了许多组件,允许您从项目列表中选择一个或多个项目。这些组件是JToggleButton、JCheckBox、JRadioButton、JComboBox和JList类的对象。ToggleButton可以处于按下或未按下状态,代表是/否选择。JCheckBox可用于表示是/否选择。有时一组CheckBox es 用于让用户选择零个或多个选项。一组JRadioButton用来呈现给用户一组互斥的选项。ComboBox用于为用户提供一组互斥的选项,用户可以选择输入新的选项值。与其他选项相比,ComboBox在屏幕上占用更少的空间,提供组件,因为它折叠了所有选项,用户必须打开选项列表才能做出选择。一个JList让用户从选项列表中选择零个或多个选项。用户可以看到JList中的所有选项。
一个JSpinner组件结合了一个JFormattedTextField和一个可编辑的JComboBox的优点。它允许您像在JComboBox中一样设置一个选择列表,同时,您还可以对显示的值应用一种格式。它一次只显示选项列表中的一个值。它允许您输入新值。
JScrollBar用于提供滚动功能,以查看尺寸大于可用空间的组件。一个JScrollBar可以垂直放置,也可以水平放置。沿着JScrollBar的轨迹拖动一个旋钮,就可以完成划水。您需要编写逻辑来使用JScrollBar组件提供批评功能。
ScollPane是一个容器,用于包装尺寸大于可用空间的组件。ScrollPane提供水平和垂直方向的自动弯曲能力。
一个JProgressBar用于显示任务的进度。它可以具有水平或垂直方向。它有三个相关的值:当前值、最小值和最大值。如果不知道任务的进度,就说JProgressBar处于不确定状态。
一个JSlider可以让你通过沿轨道滑动旋钮从两个整数之间的一组值中选择一个值。
当您想要在两个组件或两组组件之间添加分隔符时,JSeparator是一个方便的组件。通常,菜单中使用一个JSeparator来分隔相关菜单项的组。通常,它显示为水平或垂直实线。
菜单组件用于以紧凑的形式向用户提供动作列表。一个对象JMenuBar类代表一个菜单栏。一个JMenu、JMenuItem、JCheckBoxMenuItem和JRadioButtonMenuItem类的对象代表一个菜单项。
工具栏是一组小按钮,在JFrame中为用户提供常用的操作。通常,您会提供一个工具栏和一个菜单。
JTable用于以表格形式显示和编辑数据。它以行和列的形式显示数据。每列都有一个列标题。行和列是使用从 0 开始的索引的引用。
一个JTree用于以树状结构显示分层数据。一个JTree中的每一项称为一个节点。有子节点的节点称为分支节点。没有子节点的节点称为叶节点。分支节点被称为其子节点的父节点。JTree中没有父节点的第一个节点称为根节点。
一个JTabbedPane组件充当其他 Swing 组件的容器,以选项卡的方式排列它们。它可以使用标题、图标或两者来显示选项卡。一次只能看到一个标签的内容。一个JTabbedPane让你共享多个标签之间的空间。
JSplitPane是一个分割器,可以用来分割两个组件之间的空间。拆分条可以水平或垂直显示。当可用空间小于显示两个组件所需的空间时,用户可以向上/向下或向左/向右移动拆分条,以便一个组件比另一个组件获得更多的空间。如果有足够的空间,两个组件都可以完全显示。
一个JDialog是顶级的 Swing 容器。它被用作一个临时的顶层容器(或作为一个弹出窗口)来帮助主窗口获得用户的注意或用户的输入。JOptionPane类提供了许多静态方法,使用JDialog类的实例向用户显示不同类型的对话框。
允许用户使用内置对话框从文件系统中选择文件/目录。is 允许用户使用内置对话框以图形方式选择颜色。
一个JWindow是一个未分解的顶级容器。它不是一个常用的顶级容器,除了作为一个启动窗口,当应用启动时显示一次,几秒钟后自动消失。
Swing 允许您设置组件的背景色和前景色。java.awt.Color类的对象代表一种颜色。您可以使用红色、绿色、蓝色和 alpha 分量,或者使用色调、饱和度和亮度分量来指定颜色。Color类是不可变的。它提供了几个代表常用颜色的常量,例如,Color.RED和Color.BLUE常量代表红色和蓝色。
在 Swing 中,您可以在组件周围绘制一个边框。边界由一个Border接口的实例表示。存在不同类型的边框:斜面边框、柔和斜面边框、蚀刻边框、线条边框、标题边框、无光泽边框、空白边框和复合边框。BorderFactory类提供了创建所有类型边框的工厂方法。
Swing 允许您为组件中显示的文本设置字体。类的一个对象代表了 Java 程序中的一种字体。
组件可以是有效的,也可以是无效的。如果组件无效,组件的isValid()方法返回true。无效组件表示需要重新计算其位置和大小,并且需要重新布局。组件在第一次可见之前是有效的。添加/删除组件和更改属性可能会更改组件的位置和/或大小,这可能会使组件无效。调用validate()方法使组件再次有效。
Swing 可以让你画出多种形状(圆形、矩形、直线、多边形等等。)使用Graphics对象。通常,您使用JPanel作为画布来绘制形状。
Swing 提供了两种重画组件的方式:异步和同步。调用repaint()方法异步绘制组件,调用paintImmediately()方法立即绘制组件。
组件的喷涂可以在屏幕上进行,也可以在屏幕外进行。屏幕上的绘画可能会导致闪烁。绘画可以使用缓冲区在屏幕外进行,缓冲区可以一次性复制到屏幕上以避免闪烁。这种屏幕外绘画被称为双缓冲,它通过在屏幕上提供平滑的绘画来提供更好的用户体验。
三、高级 Swing
在本章中,您将学习
- 如何在 HTML 格式的 Swing 组件中使用标签
- 关于 Swing 中的线程模型以及事件调度线程的工作方式
- 如何在事件调度线程外执行长时间运行的任务
- 如何在 Swing 中使用可插拔的外观
- 如何通过 Synth 使用可换肤的外观
- 如何在 Swing 组件之间执行拖放操作
- 如何创建多文档界面(MDI)应用
- 如何使用
Toolkit类发出哔哔声并知道屏幕细节 - 如何使用 JLayer 装饰 Swing 组件
- 如何创建半透明的窗口
- 如何创建异形窗口
在 Swing 组件中使用 HTML
通常,使用一种字体和颜色在一行中显示组件上的文本。如果要在组件上使用不同的字体和颜色显示文本,或者多行显示文本,可以使用 HTML 字符串作为组件的文本。Swing 组件内置了将 HTML 文本显示为标签的支持。您可以使用 HTML 格式的字符串作为JButton、JMenuItem、JLabel、JToolTip、JTabbedPane、JTree等的标签。使用一个 HTML 字符串,它应该分别以<html>和</html>标签开始和结束。例如,如果您想在JButton上显示文本“关闭窗口”作为其标签(以粗体显示关闭,以普通字体显示窗口),您可以如下操作:
JButton b1 = new JButton("<html><b>Close</b> Window</html>");
大多数时候,在<html>和</html>标签中放置一个 HTML 字符串就可以了。但是,如果 HTML 字符串中的一行以斜杠(/)开头,它可能无法正确显示。例如,<html>/Close Window</html>将不显示任何内容,而<html>/Close Window <b>Problem</b></html>将只显示Problem。为了避免这种问题,您可以像在<html><body>/Close Window</body></html>中一样将 HTML 格式的字符串放在<body> HTML 标签中,它将显示为/Close Window。如何将包含 HTML 标签的字符串显示为标签?Swing 允许您使用html.disable组件的客户端属性禁用默认的 HTML 解释。以下代码片段禁用了JButton的 HTML 属性,并在其标签中使用 HTML 标记:
JButton b3 = new JButton();
b3.putClientProperty("html.disable", Boolean.TRUE);
b3.setText("<html><body>HTML is disabled</body></html>");
您必须在禁用html.disable客户端属性后为组件设置文本。下面的代码片段展示了一些使用 HTML 格式的字符串作为JButton文本的例子。当代码在 Windows XP 上运行时,按钮如图 3-1 所示。
JButton b1 = new JButton();
JButton b2 = new JButton();
JButton b3 = new JButton();
b1.setText("<html><body><b>Close</b> Window</body></html>");
b2.setText("<html><body>Line 1 <br/>Line 2</body></html>");
// Disable HTML text display for b3
b3.putClientProperty("html.disable", Boolean.TRUE);
b3.setText("<html><body>HTML is disabled</body></html>");

图 3-1。
Using an HTML-formatted string as text for Swing components’ labels
Swing 中的线程模型
Swing 中的大多数类都不是线程安全的。它们被设计成只使用一个线程。这并不意味着不能在 Swing 应用中使用多线程。这意味着你必须理解 Swing 的线程模型来编写线程安全的 Swing 应用。
Swing 的线程安全规则非常简单。它指出,一旦实现了一个 Swing 组件,就必须在事件调度线程上修改或访问该组件的状态。一个组件被认为是实现了,如果它已经被油漆或准备被油漆。当你第一次调用它的pack()、setVisible(true)或show()方法时,Swing 中的一个顶级容器就实现了。当一个顶级容器被实现时,它的所有子容器也被实现。
什么是事件调度线程?它是 JVM 在检测到正在使用 Swing 应用时自动创建的线程。JVM 使用这个线程来执行 Swing 组件的事件处理程序。假设您有一个带有动作监听器的JButton。当您点击JButton时,actionPerformed()方法中的代码(也就是JButton被点击的事件处理程序代码)由事件调度线程执行。你在前几章的例子中使用了JButton。您从未注意过执行其动作监听器的actionPerformed()方法的线程。通常,在像您一直在使用的简单 Swing 应用中,您不需要担心线程问题。现在您已经知道每个 Swing 应用中都存在一个事件调度线程,让我们来揭开它是如何工作的神秘面纱。在本节的整个讨论中,您将使用两个类。它们是 Swing 应用中用来处理线程模型的助手类。这些类别是
SwingUtilitiesSwingWorker
您如何知道您的代码正在事件调度线程中执行?通过使用该类的静态方法isEventDispatchThread(),很容易知道您的代码是否正在事件分派线程中执行。如果您的代码正在事件调度线程中执行,它将返回true。否则,它返回false。出于调试目的,您可以在 Java 代码中的任何地方编写以下语句。如果它打印出true,这意味着您的代码在事件调度线程中被执行。
System.out.println(SwingUtilities.isEventDispatchThread());
考虑清单 3-1 所示的程序。
清单 3-1。糟糕的 Swing 应用
// BadSwingApp.java
package com.jdojo.swing;
import javax.swing.SwingUtilities;
import java.awt.BorderLayout;
import java.awt.Container;
import javax.swing.JFrame;
import javax.swing.JComboBox;
public class BadSwingApp extends JFrame {
JComboBox<String> combo = new JComboBox<>();
public BadSwingApp(String title) {
super(title);
initFrame();
}
private void initFrame() {
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
Container contentPane = this.getContentPane();
contentPane.add(combo, BorderLayout.NORTH);
// Add an ItemEvent listener to the combobox
combo.addItemListener(e ->
System.out.println("isEventDispatchThread(): " +
SwingUtilities.isEventDispatchThread()));
combo.addItem("First");
combo.addItem("Second");
combo.addItem("Third");
}
public static void main(String[] args) {
BadSwingApp badSwingApp = new BadSwingApp("A bad Swing App");
badSwingApp.pack();
badSwingApp.setVisible(true);
}
}
这个程序是一个简单的 Swing 应用,但是它包含了一个潜在的 bug。它在一个JFrame中显示一个JComboBox。在initFrame()方法中,它向JComboBox添加了一个项目监听器。然后它给JComboBox增加了三个项目。项目侦听器只是打印一条消息,显示它是否由事件调度线程执行。像往常一样,通过创建框架、打包它并使它可见来运行应用。应用在标准输出中打印以下文本:
isEventDispatchThread(): false
我不是说过执行所有 Swing 组件的事件是事件调度线程的工作吗?让我们不要失去希望,所以在应用运行时,从组合框中选择另一个项目,如"Second"或"Third"。您会在标准输出中看到以下消息:
isEventDispatchThread(): true
第一次,组合框的 item listener 事件在非事件调度线程上执行,从第二次开始,它在事件调度线程上执行。要知道这个小应用中为什么会发生这种情况,您需要知道事件调度线程是何时创建的,以及它何时开始处理事件。事件分派线程等待从用户与 GUI 的交互中生成的事件。一旦创建了 GUI,所有用户与它的交互都由事件调度线程自动处理。在这种情况下,“主”线程在main()方法中创建了BadSwingApp帧。甚至在 GUI 被创建和显示之前,当代码将第一个项目添加到JComboBox时,项目事件被触发。因为“主”线程运行BadSwingApp帧的创建,所以主线程也处理项目事件。这个程序有两个问题:
- 首先向组件添加事件处理程序,然后在 GUI 显示之前做一些触发事件处理程序的事情,这不是一个好的做法。将所有事件处理程序添加到 GUI 构建代码末尾的组件中,这是一个经验法则。您可以通过在
initFrame()方法中将addItem()调用移动到addItemListener()调用之前来解决这个问题。 - 您需要在事件调度线程上运行所有 GUI 代码——从 GUI 构建到使其可见。这也是一件简单的事情。你需要使用
SwingUtilities类的invokeLater(Runnable r)静态方法。该方法以一个Runnable作为它的参数。它调度Runnable在事件调度线程上运行。下面是启动 Swing 应用的正确方法。在前面章节的任何例子中,您都没有按照这种方式启动 Swing 应用。您总是用main()方法创建和显示您的框架,该方法使用main线程来构建和显示 GUI。我没有遵循构建和显示 GUI 的正确方法,因为我的重点是演示我正在讨论的主题。这是您学习如何正确启动 Swing 应用的好时机。// Correct way to start a Swing applicationSwingUtilities.invokeLater(() -> {BadSwingApp badSwingApp = new BadSwingApp("一个坏的 Swing App");badSwingApp.pack();badSwingApp.setVisible(true);});如果用这个代码替换清单 3-1 的main(String[] args)方法中的现有代码,应用将在运行时打印isEventDispatchThread(): true,因为SwingUtilities类的invokeLater()方法将调度 GUI 构建代码在事件调度线程上运行。一旦以这种方式启动应用,就可以保证应用的所有事件处理程序都将在事件调度线程上执行。对SwingUtilities.invokeLater(Runnable r)方法的调用将启动事件分派线程,如果它还没有启动的话。
SwingUtilities.invokeLater()方法调用立即返回,其Runnable参数的run()方法被异步执行。也就是说,它的run()方法的执行被排队到事件调度线程中,以便以后执行。
在SwingUtilities类中还有另一个重要的静态方法叫做invokeAndWait(Runnable r)。这个方法是同步执行的,直到它的Runnable参数的run()方法在事件分派线程上执行完毕,它才返回。这个方法可能抛出一个InterruptedException或者InvocationTargetException.
Tip
不应该从事件分派线程调用SwingUtilities.invokeAndWait(Runnable r)方法,因为执行该方法调用的线程会一直等到run()方法完成。如果您从事件分派线程执行此方法调用,它将被排队到事件分派线程,并且同一个线程(事件分派线程)将等待。在事件调度线程中执行此方法调用会生成运行时错误。
有时候你可能想使用SwingUtilities类的invokeAndWait()方法来启动一个 Swing 应用,而不是使用invokeLater()方法。例如,下面的代码片段启动一个 Swing 应用,并在控制台上打印一条消息,说明该应用已经启动:
try {
SwingUtilities.invokeAndWait(() -> {
JFrame frame = new JFrame();
frame.pack();
frame.setVisible(true);
});
System.out.println("Swing application is running...");
// You can perform some non-swing related work here
}
catch (Exception e) {
e.printStackTrace();
}
有时,您可能需要在 Swing 应用中执行一项耗时的任务。如果您在事件调度线程上执行耗时的任务,您的应用将变得没有响应,这是用户不喜欢的。您应该在单独的线程中执行长任务,而不是在事件调度线程中。请注意,当任务完成时,您可能希望更新 GUI 或者在组件中显示结果,组件是 GUI 的一部分。这将要求您从非事件调度线程访问 Swing 组件。您可以使用SwingUtilities类的invokeLater()和invokeAndWait()方法从单独的线程中更新 Swing 组件。然而,Swing 提供了一个类,这使得在 Swing 应用中使用多线程变得很容易。它负责启动一个新线程,在一个新的后台线程中执行一些代码,在事件调度线程中执行一些代码。您需要知道SwingWorker类中的哪些方法将在新线程和事件分派线程中执行。
SwingWorker<T,V>类被声明为abstract。类型参数T是这个类产生的结果类型,类型参数V是中间结果类型。您必须创建从它继承的自定义类。它包含几个有趣的方法,您可以在其中编写自定义代码:
- 这是你编写代码来执行一项耗时任务的方法。它在一个单独的工作线程中执行。如果要发布中间结果,可以从这个方法调用
SwingWorker类的publish()方法,这个方法又会调用它的process()方法。请注意,您不应该访问该方法中的任何 Swing 组件,因为该方法不会在事件调度线程上执行。 process():这个方法是作为publish()方法调用的结果而被调用的。该方法在事件调度线程上执行,您可以自由访问该方法中的任何 Swing 组件。对process()方法的调用可能是对publish()方法多次调用的结果。下面是这两个方法的方法签名:protected final void publish(V... chunks)``protected void process(List<V> chunks)``publish()方法接受一个varargs参数。process()方法将所有参数传递给打包在List中的publish()方法。如果不止一个对publish()方法的调用被组合在一起,process()方法将在它的List参数中获得所有这些参数。done():当doInBackground()方法正常或非正常结束时,在事件调度线程上调用done()方法。您可以用这种方法访问 Swing 组件。默认情况下,此方法不执行任何操作。- 当你想在一个单独的线程中开始执行你的任务时,你调用这个方法。这个方法调度
SwingWorker对象在一个工作线程上执行。 get():这个方法返回从doInBackground()方法返回的任务结果。如果SwingWorker对象还没有完成doInBackground()方法的执行,那么对这个方法的调用就会阻塞,直到结果准备好。不建议在事件调度线程上调用此方法,因为它将阻塞所有事件,直到它返回。cancel(boolean mayInterruptIfRunning):如果任务仍在运行,此方法会取消任务。如果任务尚未开始,则任务永远不会运行。确保检查取消状态和doInBackground()方法中的任何中断,并相应地退出该方法。否则,您的流程将不会响应cancel()调用。isCancelled():如果进程被取消,返回true。否则,它返回false。isDone():如果任务已经完成,返回true。任务可以正常完成,也可以通过抛出异常或取消来完成。否则,它返回false。
Tip
需要注意的是,SwingWorker对象是一种使用并抛出的类型。也就是说,您不能使用它超过一次。多次调用它的execute()方法没有任何作用。
让我们开始讨论一个简单的SwingWorker类的用法。假设您想在一个单独的线程中执行一个计算一个数字(比如一个整数)的耗时任务。您希望通过轮询来检索处理结果。也就是说,您将定期检查进程是否已经完成处理。下面是SwingWorker类的一个简单用法:
// First, create a custom SwingWorker class, say MySwingWorker.
public class MySwingWorker extends SwingWorker<Integer, Integer> {
@Override
protected Integer doInBackground() throws Exception {
int result = -1;
// Write code to perform the task
return result;
}
}
// Create an object of your SwingWorker class and execute the task
MySwingWorker mySW = new MySwingWorker();
mySW.execute();
// Keep checking for the result periodically. You need to wrap the get()
// call inside a try-catch to handle any exceptions.
if (mySW.isDone()) {
int result = mySW.get();
}
清单 3-2 和清单 3-3 展示了SwingWorker类是如何工作的。当您运行清单 3-3 中的代码时,它会显示一个框架,如图 3-2 所示。您可以通过点击Start按钮启动任务。你可以随时点击Cancel按钮取消任务。中间结果显示在JLabel中。这个SwingWorkerProcessor类很简单。它接受一个SwingWorkerFrame,一个计数器和一个时间间隔。它计算计数器的数字 1 的和。向结果中添加一个数字后,它会在指定的时间间隔内休眠。它使用process()和done()方法显示中间迭代和最终结果。
清单 3-2。自定义 SwingWorker 类
// SwingWorkerProcessor.java
package com.jdojo.swing;
import javax.swing.SwingWorker;
import java.util.List;
public class SwingWorkerProcessor extends SwingWorker<Integer, Integer> {
private final SwingWorkerFrame frame;
private int iteration;
private int intervalInMillis;
public SwingWorkerProcessor(SwingWorkerFrame frame, int iteration,
int intervalInMillis) {
this.frame = frame;
this.iteration = iteration;
if (this.iteration <= 0) {
this.iteration = 10;
}
this.intervalInMillis = intervalInMillis;
if (this.intervalInMillis <= 0) {
this.intervalInMillis = 1000;
}
}
@Override
protected Integer doInBackground() throws Exception {
int sum = 0;
for (int counter = 1; counter <= iteration; counter++) {
sum = sum + counter;
// Publish the result to the GUI
this.publish(counter);
// Make sure it listens to an interruption and exits this
// method by throwing an appropriate exception
if (Thread.interrupted()) {
throw new InterruptedException();
}
// Make sure the loop exits, when the task is cancelled
if (this.isCancelled()) {
break;
}
Thread.sleep(intervalInMillis);
}
return sum;
}
@Override
protected void process(List<Integer> data) {
for (int counter : data) {
frame.updateStatus(counter, iteration);
}
}
@Override
public void done() {
frame.doneProcessing();
}

图 3-2。
Demonstrating the use of the SwingWorker class
}
清单 3-3。演示 SwingWorker 类如何工作的 Swing 应用
// SwingWorkerFrame.java
package com.jdojo.swing;
import javax.swing.JFrame;
import java.awt.Container;
import javax.swing.JLabel;
import javax.swing.JButton;
import java.awt.BorderLayout;
import java.util.concurrent.ExecutionException;
public class SwingWorkerFrame extends JFrame {
String startMessage = "Please click the start button...";
JLabel statusLabel = new JLabel(startMessage);
JButton startButton = new JButton("Start");
JButton cancelButton = new JButton("Cancel");
SwingWorkerProcessor processor;
public SwingWorkerFrame(String title) {
super(title);
initFrame();
}
private void initFrame() {
this.setDefaultCloseOperation(EXIT_ON_CLOSE);
Container contentPane = this.getContentPane();
cancelButton.setEnabled(false);
contentPane.add(statusLabel, BorderLayout.NORTH);
contentPane.add(startButton, BorderLayout.WEST);
contentPane.add(cancelButton, BorderLayout.EAST);
startButton.addActionListener(e -> startProcessing());
cancelButton.addActionListener(e -> cancelProcessing());
}
public void setButtonStatus(boolean canStart) {
if (canStart) {
startButton.setEnabled(true);
cancelButton.setEnabled(false);
} else {
startButton.setEnabled(false);
cancelButton.setEnabled(true);
}
}
public void startProcessing() {
setButtonStatus(false);
processor = new SwingWorkerProcessor(this, 10, 1000);
processor.execute();
}
public void cancelProcessing() {
// Cancel the processing
processor.cancel(true);
setButtonStatus(true);
}
public void updateStatus(int counter, int total) {
String msg = "Processing " + counter + " of " + total;
statusLabel.setText(msg);
}
public void doneProcessing() {
if (processor.isCancelled()) {
statusLabel.setText("Process cancelled ...");
}
else {
try {
// Get the result of processing
int sum = processor.get();
statusLabel.setText("Process completed. Sum is " + sum);
}
catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
setButtonStatus(true);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
SwingWorkerFrame frame
= new SwingWorkerFrame("SwingWorker Frame");
frame.pack();
frame.setVisible(true);
});
}
}
可插拔的外观
Swing 支持可插拔的外观(L&F)。您可以使用UIManager类的setLookAndFeel(String lafClassName)静态方法来更改 Swing 应用的 L&F。该方法引发检查过的异常,这将要求您处理异常。该方法的lafClassName参数是提供 L & F 的类的完全限定名。以下代码片段使用通用 catch 块为 Windows 设置 L & F,以处理所有类型的异常:
String windowsLAF= "com.sun.java.swing.plaf.windows.WindowsLookAndFeel";
try {
UIManager.setLookAndFeel(windowsLAF);
}
catch (Exception e) {
e.printStackTrace();
}
通常,在启动 Swing 应用之前设置 L&F。如果您在 GUI 显示后更改了 L&F,您将需要使用SwingUtilities类的updateComponentTreeUI(container)方法更新 GUI。改变 L & F 可能会强制改变组件的尺寸,你可能想再次使用pack()方法包装你的容器。当你在 GUI 显示后改变应用的 L & F 时,你可能会写下下面三行代码:
// Assuming that frame is a reference to a JFrame object and windowsLAF contains the
// L&F class name for Windows L&F, set the new L&F, update the GUI, and pack the frame.
UIManager.setLookAndFeel(windowsLAF);
SwingUtilities.updateComponentTreeUI(frame);
frame.pack();
下面两个UIManager类的方法返回默认 Java L & F 和系统 L & F 的类名:
String getCrossPlatformLookAndFeelClassName()String getSystemLookAndFeelClassName()
系统 L&F 为 Swing 组件提供了本机系统的 L&F,并且会因系统而异。如果您希望您的应用看起来与本机 L&F 相同,您可以通过使用下面这段代码来实现,而不必担心在您的应用将运行的计算机上表示系统 L&F 的类的实际名称:
// Set the system (or native) L&F
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
并不总是需要为 Swing 应用设置 L&F。当您启动应用时,Swing 将自己使用默认的 Java L&F。如果对UIManager.setLookAndFeel()的调用失败,你的 Swing 应用将使用当前的 L & F,这是默认的 Java L & F,如果你是第一次尝试设置一个新的 L & F,虽然可以创建自己的 L & F,但这样做并不容易。然而,Java 5.0 添加了 Synth L & F,以便于创建可换肤的 L & F。
您可以使用UIManager类来列出您的计算机上可以在 Swing 应用中使用的所有已安装的 L & F。清单 3-4 中的程序列出了你机器上所有可用的 L & F。输出是程序在 Windows 上运行时获得的;您可能会得到不同的输出。
清单 3-4。了解机器上安装的 L&F
// InstalledLookAndFeel.java
package com.jdojo.swing;
import javax.swing.UIManager;
import javax.swing.UIManager.LookAndFeelInfo;
public class InstalledLookAndFeel {
public static void main(String[] args) {
// Get the list of installed L&F
LookAndFeelInfo[] lafList = UIManager.getInstalledLookAndFeels();
// Print the names and class names of all installed L&F
for (LookAndFeelInfo lafInfo : lafList) {
String name = lafInfo.getName();
String className = lafInfo.getClassName();
System.out.println("Name: " + name +
", Class Name: " + className);
}
}
}
Name: Metal, Class Name: javax.swing.plaf.metal.MetalLookAndFeel
Name: Nimbus, Class Name: javax.swing.plaf.nimbus.NimbusLookAndFeel
Name: CDE/Motif, Class Name: com.sun.java.swing.plaf.motif.MotifLookAndFeel
Name: Windows, Class Name: com.sun.java.swing.plaf.windows.WindowsLookAndFeel
Name: Windows Classic, Class Name: com.sun.java.swing.plaf.windows.WindowsClassicLookAndFeel
清单 3-5 构建了一个JFrame,可以让你试验当前平台上已经安装的 L & F。默认情况下,选择当前 L & F。从列表中选择一个不同的 L & F,应用的 L & F 会相应改变。你会在不同的平台上得到不同的 L & F 列表。图 3-3 和图 3-4 分别显示了应用在 Windows 和 Linux 上运行时的框架。
清单 3-5。在当前平台上试验已安装的外观
// InstalledLAF.java
package com.jdojo.swing;
import java.awt.BorderLayout;
import java.awt.Container;
import java.awt.event.ItemEvent;
import java.util.Map;
import java.util.TreeMap;
import javax.swing.AbstractButton;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.ButtonGroup;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import javax.swing.JTextField;
import javax.swing.LookAndFeel;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.UIManager.LookAndFeelInfo;
import javax.swing.border.Border;
import javax.swing.border.EtchedBorder;
public class InstalledLAF extends JFrame {
JLabel nameLbl = new JLabel("Name:");
JTextField nameFld = new JTextField(20);
JButton saveBtn = new JButton("Save");
JTextField lafClassNameFld = new JTextField();
ButtonGroup radioGroup = new ButtonGroup();
static final Map<String, String> installedLAF = new TreeMap<>();
static {
for (LookAndFeelInfo lafInfo : UIManager.getInstalledLookAndFeels()) {
installedLAF.put(lafInfo.getName(), lafInfo.getClassName());
}
}
public InstalledLAF(String title) {
super(title);
initFrame();
}
private void initFrame() {
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
Container contentPane = this.getContentPane();
// Get the current look and feel
LookAndFeel currentLAF = UIManager.getLookAndFeel();
String currentLafName = currentLAF.getName();
String currentLafClassName = currentLAF.getClass().getName();
lafClassNameFld.setText(currentLafClassName);
lafClassNameFld.setEditable(false);
// Build the panels
JPanel topPanel = buildTopPanel();
JPanel leftPanel = buildLeftPanel(currentLafName);
JPanel rightPanel = buildRightPanel();
contentPane.add(topPanel, BorderLayout.NORTH);
contentPane.add(leftPanel, BorderLayout.WEST);
contentPane.add(rightPanel, BorderLayout.CENTER);
}
private void setLAF(String lafClassName) {
try {
UIManager.setLookAndFeel(lafClassName);
SwingUtilities.updateComponentTreeUI(this);
this.pack();
}
catch (Exception e) {
e.printStackTrace();
}
}
private JPanel buildTopPanel() {
JPanel panel = new JPanel();
panel.add(lafClassNameFld);
panel.setBorder(getBorder("L&F Class Name"));
return panel;
}
private JPanel buildLeftPanel(String currentLafName) {
JPanel panel = new JPanel();
panel.setBorder(getBorder("L&F Name"));
Box vBox = Box.createVerticalBox();
// Add a radio button for each installed L&F
for (String lafName : installedLAF.keySet()) {
JRadioButton radioBtn = new JRadioButton(lafName);
if (lafName.equals(currentLafName)) {
radioBtn.setSelected(true);
}
radioBtn.addItemListener(this::changeLAF);
vBox.add(radioBtn);
radioGroup.add(radioBtn);
}
panel.add(vBox);
return panel;
}
private JPanel buildRightPanel() {
JPanel panel = new JPanel();
panel.setBorder(getBorder("Swing Components"));
Box hBox = Box.createHorizontalBox();
hBox.add(nameLbl);
hBox.add(nameFld);
hBox.add(saveBtn);
panel.add(hBox);
return panel;
}
private void changeLAF(ItemEvent e) {
if (e.getSource() instanceof AbstractButton) {
AbstractButton btn = (AbstractButton) e.getSource();
String lafName = btn.getText();
String lafClassName = installedLAF.get(lafName);
this.lafClassNameFld.setText(lafClassName);
try {
UIManager.setLookAndFeel(lafClassName);
SwingUtilities.updateComponentTreeUI(this);
this.pack();
}
catch (Exception ex) {
ex.printStackTrace();
}
}
}
private Border getBorder(String title) {
Border etched = BorderFactory.createEtchedBorder(EtchedBorder.LOWERED);
Border titledBorder = BorderFactory.createTitledBorder(etched, title);
return titledBorder;
}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
InstalledLAF lafApp = new InstalledLAF("Swing L&F");
lafApp.pack();
lafApp.setVisible(true);
});
}
}

图 3-4。
The InstalledLAF frame on Linux

图 3-3。
The InstalledLAF frame on Windows
可设置外观的外观
Swing 支持名为 Synth 的基于皮肤的 L&F。什么是皮肤?GUI 中的皮肤是定义 GUI 组件外观的一组属性。Synth 允许您在外部 XML 文件中定义皮肤,并在运行时应用皮肤来改变 Swing 应用的外观。在引入 Synth 之前,您需要编写大量的 Java 代码来拥有一个自定义的 L&F。使用 Synth,您甚至不需要编写一行 Java 代码来拥有一个新的自定义 L & f。Synth L&F 是在一个 XML 文件中定义的。您需要执行以下步骤来使用 Synth L&F:
- 创建一个 XML 文件并定义 Synth L&F。
- 创建一个
SynthLookAndFeel类的实例。SynthLookAndFeel laf = new SynthLookAndFeel(); - 使用
SynthLookAndFeel对象的load()方法从 XML 文件中加载 Synth L & F。load()方法被重载了。您可以使用 URL 或 XML 文件的输入流。laf.load(url_to_your_synth_xml_file);或laf.load(input_steam_for_your_synth_xml_file, MyClass.class); - 使用
UIManager设置合成器 L&F。UIManager.setLookAndFeel(laf);
让我们讨论一下可以用来加载 XML 文件的加载过程。合成器 L&F 可以使用两种不同的外部资源。
- 定义 Synth L&F 的 XML 文件
- Synth XML 文件中使用的图像等资源
当使用 URL 加载 Synth XML 文件时,URL 指向 XML 文件,XML 文件中引用的资源的所有路径都将相对于 URL 进行解析。以下代码片段使用 URL 加载 Synth XML 文件:
URL url = new URL("file:///C:/synth/synth_look_and_feel.xml");
laf.load(url);
您可以使用一个可能指向本地文件系统或网络的 URL 来加载 Synth XML 文件。您可以使用http或ftp协议来加载 Synth XML 文件。还可以从 JAR 文件中加载 Synth XML 文件。
当使用load(InputStream input, Class resourceBase)方法加载 Synth XML 文件时,input参数是要加载的 XML 文件的InputStream,而resourceBase类对象用于解析 XML 文件中引用的资源。假设您在 Windows 操作系统的计算机上有以下文件夹结构:
C:\javabook
C:\javabook\images\myimage.png
C:\javabook\synth\synthlaf.xml
C:\javabook\book\chapter3\images\myimage.png
C:\javabook\book\chapter3\synth\synthlaf.xml
C:\javabook\book\chapter3\MyClass.class
假设在类路径中设置了C:\javabook,并且MyClass是在com.jdojo.chapter3包中定义的 Java 类。下面的代码片段加载了synthlaf.xml:
// It will load C:\javabook\synth\synthlaf.xml because you are
// using a forward slash in the file path "/synth/synthlaf.xml"
Class cls = MyClass.class;
InputStream ins = cls.getResourceAsStream("/synth/synthlaf.xml");
laf.load(ins, cls);
// It will load C:\javabook\book\chapter3\synth\synthlaf.xml because you are
// not using a forward slash in the file path "synthlaf.xml"
Class cls = MyClass.class;
InputStream ins = cls.getResourceAsStream("synthlaf.xml");
laf.load(ins, cls);
在这两种情况下,类引用cls将用于解析 XML 文件中引用的资源的路径。例如,如果图像被称为img/myimage.png,它将从C:\javabook\book\chapter3\images\myimage.png.加载;如果图像被称为/img/myimage.png",则加载C:\javabook\images\myimage.png文件。
使用方法的第二个版本,它更灵活。您可以将所有 Synth L&F 文件和相关的资源文件打包到一个 JAR 文件中,而不用担心它们在运行时的实际位置。在开发过程中,您可以将所有 Synth 文件放在一个单独的文件夹中,这个文件夹应该在您的类路径中。您唯一需要注意的是,如果文件名以正斜杠开头,则使用类路径解析路径。如果文件名不是以正斜杠开头,则该类的包路径会添加到文件名的前面,然后使用类路径来解析文件的路径。
让我们开始构建 Synth L&F XML 文件。在开始定义你的 Synth L&F 之前设定你的目标。图 3-5 显示了一个使用 Java 默认 L & F 的示例JFrame

图 3-5。
A sample JFrame using the default Java L&F
JFrame包含三个部件:一个JLabel、一个JTextField和一个JButton。您将构建一个 XML 文件来为这些组件定义一个 Synth L & F。创建这个屏幕的 Java 代码如清单 3-6 所示。感兴趣的代码在main()方法中(如下所示)。现在,只需创建一个名为synthlaf.xml的空 XML 文件,并将其保存在类路径中。
try {
SynthLookAndFeel laf = new SynthLookAndFeel();
Class cls = SynthLookAndFeelFrame.class;
InputStream ins = cls.getResourceAsStream("/synthlaf.xml");
laf.load(ins, cls);
UIManager.setLookAndFeel(laf);
}
catch (Exception e) {
e.printStackTrace();
}
清单 3-6。为 Swing 组件使用合成 L&F
// SynthLookAndFeelFrame.java
package com.jdojo.swing;
import java.io.InputStream;
import java.awt.Container;
import java.awt.FlowLayout;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JTextField;
import javax.swing.UIManager;
import javax.swing.plaf.synth.SynthLookAndFeel;
public class SynthLookAndFeelFrame extends JFrame {
JLabel nameLabel = new JLabel("Name:");
JTextField nameTextField = new JTextField(20);
JButton closeButton = new JButton("Close");
public SynthLookAndFeelFrame(String title) {
super(title);
initFrame();
}
private void initFrame() {
this.setDefaultCloseOperation(EXIT_ON_CLOSE);
Container contentPane = this.getContentPane();
contentPane.setLayout(new FlowLayout());
contentPane.add(nameLabel);
contentPane.add(nameTextField);
contentPane.add(closeButton);
}
public static void main(String[] args) {
try {
SynthLookAndFeel laf = new SynthLookAndFeel();
Class c = SynthLookAndFeelFrame.class;
InputStream ins = c.getResourceAsStream("/synthlaf.xml");
laf.load(ins, c);
UIManager.setLookAndFeel(laf);
}
catch (Exception e) {
e.printStackTrace();
}
SynthLookAndFeelFrame frame =
new SynthLookAndFeelFrame("Synth Look-and-Feel Frame");
frame.pack();
frame.setVisible(true);
}
}
最简单的 Synth XML 文件如下所示:
<?xml version="1.0"?>
<synth version="1">
</synth>
根元素是<synth>,你可以选择指定一个版本号,应该是 1。您尚未在 XML 文件中定义任何与 L & F 相关的样式。让我们用synthlaf.xml文件中的这些内容运行SynthLookAndFeelFrame类。如果您在运行该类时遇到问题,因为它没有找到synthlaf.xml文件,请更改main()方法中的load()方法调用,以使用 URL 而不是InputStream。图 3-6 显示了运行SynthLookAndFeelFrame类时得到的JFrame。

图 3-6。
A JFrame with a Synth L&F where the Synth XML file does not define any styles
你没想到会这样,是吗?你马上就能修好它。默认情况下,Synth L & F 为所有组件设置一个没有边框的白色背景。这就是为什么JLabel、JTextField和JButton一起出现在屏幕上的原因。一个JTextField仍然在屏幕上,但是它没有边框。
我们来定义一种风格。使用<style>元素定义样式。它有一个名为id的强制属性,这是样式的唯一标识符。将样式绑定到组件时,会用到id属性的值。
<?xml version="1.0"?>
<synth version="1">
<style id="buttonStyle">
<!-- Style specific elements go here -->
</style>
</synth>
定义样式本身没有任何作用。您必须将一个样式绑定到一个或多个组件,才能看到该样式的实际效果。将一个样式绑定到一个组件是使用一个<bind>元素完成的,它有三个属性:
styletypekey
style是您绑定到该组件的样式元素的id属性的值。
属性确定绑定的类型。它的值不是region就是name。每个 Swing 部件具有至少一个区域。有些组件有多个区域。组件的所有区域都有一个名称。区域由javax.swing.plaf.synth包中的Region类中的常量定义。例如,JButton有一个名为Button的区域,由Region.BUTTON常数表示;一个JTextField有一个名为TextField的区域,由Region.TEXT_FIELD常量表示;一个JTabbedPane有四个区域,分别称为TabbedPaneContent、TabbedPaneTabArea、TabbedPaneTab和TabbedPane。请参考Region类的文档以获得完整的区域列表。如果使用值name,则引用组件的getName()方法返回的值。您可以使用组件的setName()方法为其设置一个名称。
该属性是一个正则表达式,用于根据用于type属性的值来匹配区域或名称。例如,正则表达式".*"匹配任何地区或名称。通常,您使用",*"作为key值来将默认样式绑定到所有组件。
下面是一些使用<bind>元素将样式绑定到组件的例子:
<!-- Bind a buttonStyle style to all JButtons -->
<bind style="buttonStyle" type="region" key="Button" />
<!-- Bind a defaultStyle to all Swing components -->
<bind style="defaultStyle" type="region" key=".*" />
<!-- Bind myDefaultStyle to all components whose name returned by their getName() method starts with "com.jdojo". Here \. means one dot and .* means any characters zero or more times -->
<bind style="mydefaultStyle" type="name" key="com\.jdojo.*" />
让我们为一个JButton定义一些样式。所有样式必须在一个<style>元素中定义。您可以使用<opaque>元素设置样式的不透明度。它有一个可能为真或假的value属性,如下所示:
<opaque value="true"/>
组件可以处于以下七种状态之一:ENABLED、MOUSE_OVER、PRESSED、DISABLED、FOCUSED、SELECTED或DEFAULT。并非所有组件都支持所有七种状态。您可以定义应用于特定状态或所有状态的样式属性。您可以使用元素定义特定于状态的属性。如果样式属性仅适用于特定的状态,则需要用七个状态值中的一个来指定 value 属性。如果您想要为多个状态定义一些样式属性,您可以用一个AND来分隔状态名称。下面的<style>元素将为一个组件定义当鼠标在它上面并且它也是焦点时的样式:
<state value="MOUSE_OVER AND FOCUSED">
...
</state>
如果同一个状态存在多个样式,则使用与最特定的状态关联的样式定义。假设您已经为两种状态定义了样式:MOUSE_OVER和FOCUSED以及MOUSE_OVER。当组件的区域上有鼠标并且它是焦点时,应用第一种样式;如果组件不在焦点上,但是鼠标在它的区域上,则应用第二种样式。
用显示的内容修改synthlaf.xml文件,并重新运行应用:
<?xml version="1.0"?>
<synth version="1">
<style id="buttonStyle">
<opaque value="true"/>
<insets top="4" bottom="4" left="6" right="6"/>
<imageIcon id="closeIconId" path="/img/close_icon.png"/>
<property key="Button.textShiftOffset" type="Integer" value="2"/>
<property key="Button.icon" type="idref" value="closeIconId"/>
<state>
<font name="Serif" size="14" style="BOLD"/>
<color value="LIGHT_GRAY" type="BACKGROUND"/>
<color value="BLACK" type="TEXT_FOREGROUND"/>
</state>
<state value="PRESSED">
<color value="GRAY" type="BACKGROUND"/>
<color value="BLACK" type="TEXT_FOREGROUND"/>
</state>
</style>
<bind style="buttonStyle" type="region" key="Button"/>
</synth>
按下Close键,你会发现比以前好用多了。当你按下它时,它的背景颜色会改变。当它被按下时,它的文本向右和向下移动。
让我们讨论一下这个 XML 文件中使用的所有样式:
- 样式定义了一个
JButton的样式。元素定义了JButton将是不透明的。<insets>元素为JButton设置插图。 - 元素定义了一个图像资源。这个元素本身不做任何事情。当您需要使用图像时,您将需要在其他地方引用它的
id属性的值。它的path属性指的是图像文件的路径。它使用您传递给load()方法的类对象的getResource()方法来定位图像文件。你使用了/img/close_icon.png作为路径。这意味着您需要在一个文件夹下有一个名为images的文件夹,该文件夹在类路径中,并且您需要在images文件夹下放置一个close_icon.png文件。如果您使用 URL 来加载 synth XML 文件,那么图像的路径也会相应地改变。假设您使用 URL 字符串"file:///c:/mysynth/synthlaf.xml"加载了一个 Synth XML 文件。这个 URL 以file:///c:/mysynth/为基础,XML 中的所有路径都将相对于这个基础进行解析。例如,如果您将img/close_icon.png指定为<imageIcon>元素中的路径,file:///c:/mysynth/img/close_icon.png将是用于加载图像文件的路径。如果您将/img/close_icon.png指定为<imageIcon>元素中的路径,它将被视为绝对路径,Synth 将尝试使用file://img/close_icon.png路径加载图像文件。理解使用不同版本的SynthLookAndFeel类的load()方法对资源查找的影响是非常重要的。最好使用 URL,并将所有资源放在 URL 的基本文件夹下。您可以将包括 Synth XML 文件在内的所有资源打包到一个 JAR 文件中,并使用一个 URL 版本的load()方法。
元素用于设置组件的属性。不能使用<property>元素设置组件的任何属性。一个<property>元素有三个属性:key、type和value。key属性指定属性名。type属性是属性的类型,其值可以是idref、boolean、dimension、insets、integer或string。type属性是可选的,默认为idref,这意味着value属性的值是引用另一个元素的id。您已经为JButton设置了两个属性。一个是Button.textShiftOffset属性,用于在JButton被按下时移动其文本。另一个属性是称为Button.icon的JButton的图像图标。您没有指定type属性,默认为idref。<property>元素的value属性是closeIconId,它是定义近景图像的<imageIcon>元素的id。
您可以使用元素定义颜色属性。您设置了一个<color>元素的type和value属性的值。type属性可以有以下四个值之一:FOREGROUND、BACKGROUND、TEXT_FOREGROUND、TEXT_BACKGROUND和FOCUS。您可以使用来自java.awt.Color类的常量名称或#RRGGBB或#AARRGGBB形式的十六进制值来指定value属性的值。在十六进制格式中,AA、RR、GG和BB是颜色的 alpha、红色、绿色和蓝色分量的值。
您可以使用<font>元素定义字体样式。它有三个属性:name、size和style。style属性是可选的,默认为PLAIN。style属性的其他值是BOLD和ITALIC。
最后,您组合不同的样式,并将它们放在一个<state>元素下。在您的buttonStyle中,您已经为所有状态设置了一组样式,为PRESSED状态设置了一组样式。请注意,默认情况下,JButton的背景颜色为LIGHT_GRAY。按下时其背景颜色会变为GRAY。当您使用这个 XML 文件运行SynthLookAndFeel类时,屏幕看起来如图 3-7 所示。请注意,您已经为Close按钮设置了一个图标。当您按下Close按钮时,背景颜色会改变。

图 3-7。
Using an icon with the Synth look and feel
您没有JButton和JTextField的边框。在 Synth 中设置边框有两种方法:可以使用图像或编写 Java 代码。我将讨论设置边界的两种方法。如果你想用一个图像来画一个边框,你需要使用一个<imagePainter>元素,如下所示:
<imagePainter path="/img/line_border.png"
sourceInsets="2 2 2 2"
paintCenter="false"
method="buttonBorder" />
path 属性指定用于绘制边框的图像的路径。属性指定了源图像的插入。painterCenter属性指定是应该画图像的中心还是只画边界。如果你想画一个边框,你应该把这个属性设置为false。如果要绘制一个图像作为背景,应该将这个属性设置为true。method属性是javax.swing.plaf.synth.SynthPainter类中绘制方法的名称。这个类有一个 paint 方法来绘制每个组件。方法名的形式是paintXxxYyy(),其中Xxx是组件名,Yyy是要绘制的区域。通过去掉“paint”一词并使用小写的第一个字符,method 属性的值被设置为xxxYyy。例如,要绘制按钮的边框,绘制方法名为paintButtonBorder()。此方法的方法属性值为buttonBorder。您还可以使用<imagePainter>元素将图像设置为组件的背景。以下样式将button_background.png设置为JButton的背景:
<imagePainter path="/img/button_background.png"
sourceInsets="2 2 2 2"
paintCenter="true"
method="buttonBackground" />
Tip
默认情况下,<imagePainter>元素中使用的图像被拉伸以适合组件的大小。这意味着,如果您希望多个组件周围有相同的边界,您只需要创建一个图像来表示该边界。如果不希望图像被拉伸,可以将<imagePainter>元素的 stretch 属性设置为 false。
如果你想写 Java 代码来画一个边框,你需要创建一个新的类,它将继承清单 3-7 中列出的SynthPainter类。您需要覆盖特定的绘制方法。这个类覆盖了paintTextFieldBorder()和paintButtonBorder()方法。他们只是使用自定义颜色和笔画值绘制一个矩形。
清单 3-7。用于 JTextField 和 JButton 的自定义 Synth 边框绘制器类
// SynthRectBorderPainter.java
package com.jdojo.swing;
import javax.swing.plaf.synth.SynthPainter;
import javax.swing.plaf.synth.SynthContext;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.BasicStroke;
import java.awt.Color;
public class SynthRectBorderPainter extends SynthPainter {
@Override
public void paintTextFieldBorder(SynthContext context, Graphics g,
int x, int y, int w, int h) {
Graphics2D g2 = (Graphics2D)g;
g2.setStroke(new BasicStroke(2));
g2.setColor(Color.BLUE);
g2.drawRect(x, y, w, h);
}
@Override
public void paintButtonBorder(SynthContext context, Graphics g,
int x, int y, int w, int h) {
Graphics2D g2 = (Graphics2D)g;
g2.setStroke(new BasicStroke(4));
g2.setColor(Color.RED);
g2.drawRect(x, y, w, h);
}
}
现在,您需要在 Synth XML 文件中指定您想要使用自定义 painter 类来绘制您的JButton的边框。一个<object>元素表示 Synth XML 文件中的一个 Java 对象。要指定一个定制的 Java 画师,您可以使用一个<painter>元素,它需要一个<object>元素的id和一个method名称的idref,如下所示:
<object id="borderPainterId" class="com.jdojo.swing.SynthRectBorderPainter"/>
<painter idref="borderPainterId" method="buttonBorder"/>
Synth XML 文件的最终版本如下所示。您已经使用了一个定制的 Java 代码来绘制按钮被按下时的边框,以及没有被按下时的图像图标。使用您的定制 Java 代码来绘制JTextField的边框。您可以修改 XML 内容来为JLabel设置样式。最后,JFrame如图 3-8 所示。
<?xml version="1.0"?>
<synth version="1.0">
<style id="buttonStyle">
<opaque value="true"/>
<insets top="4" bottom="4" left="6" right="6"/>
<imageIcon id="closeIconId" path="/img/close_icon.png"/>
<property key="Button.textShiftOffset" type="Integer" value="2"/>
<property key="Button.icon" type="idref" value="closeIconId"/>
<state>
<imagePainter path="/img/line_border.png" sourceInsets="2 2 2 2"
paintCenter="false" method="buttonBorder"/>
<font name="Serif" size="14" style="BOLD"/>
<color value="LIGHT_GRAY" type="BACKGROUND"/>
<color value="BLACK" type="TEXT_FOREGROUND"/>
</state>
<state value="PRESSED">
<object id="borderPainterId"
class="com.jdojo.swing.SynthRectBorderPainter"/>
<painter idref="borderPainterId" method="buttonBorder"/>
<color value="GRAY" type="BACKGROUND"/>
<color value="BLACK" type="TEXT_FOREGROUND"/>
</state>
</style>
<bind style="buttonStyle" type="region" key="Button"/>
<style id="textFieldStyle">
<insets top="4" bottom="4" left="4" right="4"/>
<state>
<color value="WHITE" type="BACKGROUND"/>
<object id="textFieldPainterId" class="com.jdojo.swing.SynthRectBorderPainter"/>
<painter idref="textFieldPainterId" method="textFieldBorder"/>
</state>
</style>
<bind style="textFieldStyle" type="region" key="TextField"/>
</synth>

图 3-8。
Using borders in a Synth L&F
拖放
拖放(DnD)是在应用中传输数据的一种方式。您也可以使用带有剪切、复制和粘贴操作的剪贴板来传输数据。
DnD 允许你通过拖拽一个组件到另一个组件上来传输数据。被拖动的组件称为拖动源;它提供要传输的数据。拖动源放在其上的组件称为放置目标;它是数据的接收者。接受拖放动作并导入拖动源提供的数据是拖放目标的责任。使用Transferable对象完成数据传输。Transferable是java.awt.datatransfer包中的一个接口。DnD 机构如图 3-9 所示。

图 3-9。
The data transfer mechanism used in DnD
Transferable接口包含以下三种方法:
DataFlavor[] getTransferDataFlavors()boolean isDataFlavorSupported(DataFlavor flavor)Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException
在您学习Transferable接口的三个方法之前,您需要知道为什么您需要一个Transferable对象来使用 DnD 传输数据。为什么拖动目标不直接从拖动源获取数据?您可以使用 DnD 在同一个 Java 应用内、两个 Java 应用之间、从本地应用到 Java 应用以及从 Java 应用到本地应用传输数据。数据传输的范围非常广,它支持多种数据的传输。接口提供了一种将数据及其类型打包到对象中的机制。接收方可以向该对象查询它保存的数据类型,如果数据符合接收方的要求,就导入数据。DataFlavor类的一个对象代表了数据的细节。DataFlavor类我就不详细讨论了。它包含几个常量来定义数据的类型;例如,DataFlavor.stringFlavor代表 Java 的 Unicode 字符串类。Transferable接口的前两个方法给出了关于数据的细节。第三个函数返回数据本身作为一个Object。拖放目标将使用getTransferData()方法获取拖动源提供的数据。
在 Swing 中使用 DnD 很容易。大多数时候,你只需要写一行代码就可以开始使用 DnD。您所需要的就是在组件上启用拖动,就像这样:
// Enable DnD for myComponent
myComponent.setDragEnabled(true);
之后,你就可以开始使用 DnD 了。使用 DnD 依赖于用户界面。在 Windows 平台上,您需要在拖动源上按下鼠标左键来启动拖动动作。要持续拖动拖动源,您需要在按住鼠标左键的同时移动鼠标。当鼠标指针在放置目标上时,释放鼠标左键执行放置操作。在整个 DnD 过程中,用户会收到视觉反馈。
所有文本组件(JFileChooser、JColorChooser、JList、JTree和JTable)都内置了对 DnD 的拖动支持。所有文本组件和JColorChooser都内置了对 DnD 的拖放支持。例如,假设你有一个名为nameFld的JTextField和一个名为descTxtArea的JTextArea。要开始在它们之间使用 DnD,您需要编写下面两行代码:
nameFld.setDragEnabled(true);
descTxtArea.setDragEnabled(true);
您可以选择JTextField中的文本,拖动它,并将其放到JTextArea上。在JTextField中选择的文本被传送到JTextArea。您也可以将文本从JTextArea拖到JTextField。
数据是如何从一个文本组件传输到另一个文本组件的?它会被复制或移动吗?答案取决于拖动源和用户的动作。拖动源声明它支持的动作。用户的动作决定了发生了什么动作。例如,在 Windows 平台上,简单的拖动表示一个MOVE动作,而按住Ctrl键拖动表示一个Copy动作,按住Ctrl + Shift键拖动表示一个LINK动作。动作由类中声明的常数表示:
TranferHandler.COPYTranferHandler.MOVETranferHandler.COPY_OR_MOVETranferHandler.LINKTranferHandler.NONE
对于JList、JTable和JTree组件,拖放动作不是内置的。原因是当拖动源放到这些组件上时,无法预测用户的意图。您需要编写代码来为这些组件设置拖放动作。请注意,它们内置了对拖动动作的支持。DnD 为您提供关于这些组件的放置位置的适当信息。这些组件允许您使用它们的setDropMode(DropMode dm)方法指定拖放模式。放置模式决定了在 DnD 操作期间如何跟踪放置位置。丢弃模式由表 3-1 中列出的java.swing.DropMode枚举中的常数表示。
表 3-1。
The List of DropMode Enum Contants for JList, JTree, and JTable
| DropMode 枚举常量 | 使用组件 | 描述 | | --- | --- | --- | | `ON` | `JList``JTree` | 使用现有项目的索引来跟踪放置位置。 | | `INSERT` | `JList``JTree` | 放置位置被跟踪为数据将被插入的位置。 | | `INSERT_COLS` | `JTable` | 根据将插入新列的列索引来跟踪放置位置。 | | `INSERT_ROWS` | `JTable` | 根据将要插入新行的行索引来跟踪放置位置。 | | `ON_OR_INSERT` | `JList``JTree` | 将放置位置作为`ON`和`INSERT`进行跟踪。 | | `ON_OR_INSERT_ROWS` `ON_OR_INSERT_COLS` | `JTable` | 用期望行或列跟踪`ON`或`INSERT`。 | | `USE_SELECTION` | `JList``JTree` | 其工作原理与`ON`相同。这是默认的丢弃模式。如果拖动到已经选定的组件上,此模式会将选择更改为鼠标光标正在拖动的项目。然而,`ON` drop 模式保持用户的选择不变,并临时选择鼠标光标所拖动的项目。`ON`是用户体验更好的选择。提供此选项只是为了向后兼容。 |让我们写一些代码来使用带有JList的 DnD。您需要执行以下操作:
- 创建一个继承自
javax.swing.TransferHandler类的新类。 - 重写新类中的一些方法来处理数据传输。
- 使用
JList的setTransferHandler()方法来设置传输处理程序类的一个实例。
清单 3-8 包含了自定义JList的代码。
清单 3-8。JList 的自定义 TransferHandler
// ListTransferHandler.java
package com.jdojo.swing;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.StringSelection;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.io.IOException;
import javax.swing.DefaultListModel;
import javax.swing.JComponent;
import javax.swing.JList;
import javax.swing.TransferHandler;
public class ListTransferHandler extends TransferHandler {
@Override
public int getSourceActions(JComponent c) {
return TransferHandler.COPY_OR_MOVE;
}
@Override
protected Transferable createTransferable(JComponent source) {
// Suppress the unchecked cast warning
@SuppressWarnings("unchecked")
JList<String> sourceList = (JList<String>)source;
String data = sourceList.getSelectedValue();
// Uses only the first selected item in the list
Transferable t = new StringSelection(data);
return t;
}
@Override
protected void exportDone(JComponent source, Transferable data, int action) {
// Suppress teh unchecked cast warning
@SuppressWarnings("unchecked")
JList<String> sourceList = (JList<String>)source;
String movedItem = sourceList.getSelectedValue();
if (action == TransferHandler.MOVE) {
// Remove the moved item
DefaultListModel<String> listModel
= (DefaultListModel<String>) sourceList.getModel();
listModel.removeElement(movedItem);
}
}
@Override
public boolean canImport(TransferHandler.TransferSupport support) {
// We only support drop, not copy-paste
if (!support.isDrop()) {
return false;
}
return support.isDataFlavorSupported(DataFlavor.stringFlavor);
}
@Override
public boolean importData(TransferHandler.TransferSupport support) {
// This is necessary to handle paste
if (!this.canImport(support)) {
return false;
}
// Get the data
Transferable t = support.getTransferable();
String data = null;
try {
data = (String) t.getTransferData(DataFlavor.stringFlavor);
if (data == null) {
return false;
}
}
catch (UnsupportedFlavorException | IOException e) {
e.printStackTrace();
return false;
}
// Get the drop location for the JList
JList.DropLocation dropLocation
= (JList.DropLocation) support.getDropLocation();
int dropIndex = dropLocation.getIndex();
// Suppress the unchecked cast warning
@SuppressWarnings("unchecked")
JList<String> targetList = (JList<String>)support.getComponent();
DefaultListModel<String> listModel
= (DefaultListModel<String>)targetList.getModel();
if (dropLocation.isInsert()) {
listModel.add(dropIndex, data);
}
else {
listModel.set(dropIndex, data);
}
return true;
}
}
如果您想只支持一个JList的放下动作,您只需要在您的传输处理程序类中覆盖两个方法:canImport()和importData()。如果拖放目标想要传输数据,canImport()方法返回true。否则返回false。在您的代码中,您要确保该操作是拖放操作,并且拖动源提供字符串数据。注意,如果你为一个组件设置一个自定义的TransferHandler对象,同样的TransferHandler对象也将用于剪切-复制-粘贴操作。您的代码仅支持拖放操作。importData()方法从Transferable对象中读取数据,并根据用户的动作在JList中插入或替换项目。
JList的默认TransferHandler处理拖动动作并提供数据。然而,一旦你设置了你自己的TransferHandler,你就失去了默认的特性,你要负责把那个特性添加到你的TransferHandler中。如果你想支持拖动动作,你需要为createTransferable()和getSourceActions()方法编写定制代码。第一个方法将数据打包成一个Transferable对象,第二个方法返回拖动源支持的动作类型。StringSelection是Transferable接口的实现,用于传输 Java 字符串。
如果你的拖动源支持一个MOVE动作,你应该提供代码在移动动作之后移除该项。您得到一个占位符来在exportDone()方法中编写清理代码,如清单 3-9 所示。
清单 3-9 中的代码显示了一个JTextField和两个JLists,这让您可以为 JList 演示 DnD。图 3-10 显示了运行清单 3-9 中的程序时得到的JFrame。你可以在三个组件中的任何一个中使用 DnD:?? 和两个 ??。代码中有一个错误。如果您在JList中拖动一个项目,并将其放在同一个JList中,什么也不会发生。这是留给你的一个练习,让你找出这个错误并修复它。我给你一个提示:在将元素添加到ListTransferHandler类的importData()方法中的同一个List之前,尝试移除该元素。此外,这个定制代码只支持JList中的单一选择。您可以在ListTransferHandler类中定制代码,以处理JList中的多重选择。
清单 3-9。使用 DnD 在 Swing 组件之间传输数据
// DragAndDropApp.java
package com.jdojo.swing;
import java.awt.BorderLayout;
import java.awt.Container;
import javax.swing.Box;
import javax.swing.DefaultListModel;
import javax.swing.DropMode;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.ListSelectionModel;
import javax.swing.SwingUtilities;
public class DragAndDropApp extends JFrame {
private JLabel newLabel = new JLabel("New:");
private JTextField newTextField = new JTextField(10);
private JLabel sourceLabel = new JLabel("Source");
private JLabel destLabel = new JLabel("Destination");
private JList<String> sourceList = new JList<>(new DefaultListModel<>());
private JList<String> destList = new JList<>(new DefaultListModel<>());
public DragAndDropApp(String title) {
super(title);
populateList();
initFrame();
}
private void initFrame() {
Container contentPane = this.getContentPane();
Box nameBox = Box.createHorizontalBox();
nameBox.add(newLabel);
nameBox.add(newTextField);
Box sourceBox = Box.createVerticalBox();
sourceBox.add(sourceLabel);
sourceBox.add(new JScrollPane(sourceList));
Box destBox = Box.createVerticalBox();
destBox.add(destLabel);
destBox.add(new JScrollPane(destList));
Box listBox = Box.createHorizontalBox();
listBox.add(sourceBox);
listBox.add(destBox);
Box allBox = Box.createVerticalBox();
allBox.add(nameBox);
allBox.add(listBox);
contentPane.add(allBox, BorderLayout.CENTER);
// Our lists support only single selection
sourceList.setSelectionMode(
ListSelectionModel.SINGLE_SELECTION);
destList.setSelectionMode(
ListSelectionModel.SINGLE_SELECTION);
// Enable Drag and Drop for components
newTextField.setDragEnabled(true);
sourceList.setDragEnabled(true);
destList.setDragEnabled(true);
// Set the drop mode to Insert
sourceList.setDropMode(DropMode.INSERT);
destList.setDropMode(DropMode.INSERT);
// Set the transfer handler
sourceList.setTransferHandler(new ListTransferHandler());
destList.setTransferHandler(new ListTransferHandler());
}
public void populateList() {
DefaultListModel<String> sourceModel
= (DefaultListModel<String>) sourceList.getModel();
DefaultListModel<String> destModel
= (DefaultListModel<String>) destList.getModel();
for (int i = 0; i < 5; i++) {
sourceModel.add(i, "Source Item " + i);
destModel.add(i, "Destination Item " + i);
}
}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
DragAndDropApp frame = new DragAndDropApp("Drag and Drop Frame");
frame.pack();
frame.setVisible(true);
});
}
}

图 3-10。
A JFrame with a few Swing components supporting DnD
多文档界面应用
从广义上讲,根据窗口在应用中的组织方式,有三种类型的应用向用户显示信息。他们是
- 单一文档界面(SDI)
- 多文档界面
- 选项卡式文档界面(TDI)
在 SDI 应用中,任何时候都只打开一个窗口。在 MDI 应用中,打开一个主窗口(也称为父窗口),并且在主窗口中打开多个子窗口。在 TDI 应用中,打开一个窗口,它有多个作为选项卡打开的窗口。Microsoft Notepad 是 SDI 应用的一个示例,Microsoft Word 97 是 MDI 应用的一个示例(Microsoft Word 的较新版本是 SDI),Google Chrome 浏览器是 TDI 应用的一个示例。
您可以使用 Swing 开发 SDI、MDI 和 TDI 应用。在 MDI 应用中,您可以打开多个框架,这些框架将成为JInternalFrame类的实例。您可以用多种方式组织多个内部框架。比如,你可以最大化和最小化它们;您可以以平铺方式并排查看它们,也可以以层叠形式查看它们。下面是您将在 MDI 应用中使用的四个类:
JInternalFrameJDesktopPaneDesktopManagerJFrame
类别的执行个体做为子视窗,永远显示在其父视窗的区域内。在很大程度上,使用它和使用JFrame是一样的。您将 Swing 组件添加到其内容窗格中,使用pack()方法打包它们,并使用setVisible(true)方法使其可见。如果你想监听窗口事件,如激活,停用等。,你需要给JInternalFrame加一个InternalFrameListener而不是一个WindowListener,?? 是用来做JFrame的。您可以在其构造函数中或使用 setter 方法设置各种属性。以下代码片段显示了如何使用JInternalFrame类的实例:
String title = "A Child Window";
Boolean resizable = true;
Boolean closable = true;
Boolean maximizable = true;
Boolean iconifiable = true;
JInternalFrame iFrame =
new JInternalFrame(title, resizable, closable, maximizable, iconifiable);
// Add components to the iFrame using iFrame.add(...)
// Pack eth frame and make it visible
iFrame.pack();
iFrame.setVisible(true);
该类的一个实例被用作作为JInternalFrame类实例的所有子窗口的容器(而不是顶级容器)。它使用一个null布局管理器。你把它加到一个JFrame里。您希望将对桌面窗格的引用作为实例变量存储在JFrame中,以便以后可以使用它来处理子窗口。
// Create a desktop pane
JDesktopPane desktopPane = new JDesktopPane();
// Add all JInternalFrames to the desktopPane
desktopPane.add(iFrame);
您可以使用getAllFrames()方法获取添加到JDesktopPane中的所有JInternalFrames。
// Get the list of child windows
JInternalFrame[] frames = desktopPane.getAllFrames();
一个JDesktopPane使用接口的一个实例来管理所有的内部框架。DefaultDesktopManager类是DesktopManager接口的一个实现。如果您想定制桌面管理器管理内部框架的方式,您需要创建自己的从DefaultDesktopManager继承的类。您可以使用JDesktopPane的setDesktopManager()方法设置您的自定义桌面管理器。桌面管理器有很多有用的方法。例如,如果您想以编程方式关闭一个内部框架,您可以使用它的closeFrame()方法。如果您使内部框架可关闭,用户也可以使用提供的上下文菜单来关闭它。您可以使用桌面窗格的getDesktopManager()方法获得桌面管理器的引用。
// Close the internal frame named frame1
desktopPane.getDesktopManager().closeFrame(frame1);
该类被用作顶级容器,并充当JInternalFrame s 的父窗口。它包含一个JDesktopPane的实例。请注意,JFrame的pack()方法在 MDI 应用中不会有任何好处,因为它的独生子,桌面窗格,使用了一个null布局管理器。您必须显式设置其大小。通常,您会最大化显示JFrame。
清单 3-10 展示了如何开发 MDI 应用。Swing 没有提供将内部框架组织成平铺或层叠窗口的方法,这在任何基于 windows 的 MDI 应用中都很常见。通过应用简单的逻辑来组织内部框架并提供菜单项来使用它们,您可以将平铺和层叠功能构建到 Swing MDI 应用中。图 3-11 显示了运行清单 3-10 中的程序时显示的屏幕。
清单 3-10。使用 Swing 开发 MDI 应用
// MDIApp.java
package com.jdojo.swing;
import java.awt.BorderLayout;
import java.awt.Dimension;
import javax.swing.JDesktopPane;
import javax.swing.JFrame;
import javax.swing.JInternalFrame;
import javax.swing.JLabel;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
public class MDIApp extends JFrame {
private final JDesktopPane desktopPane = new JDesktopPane();
public MDIApp(String title) {
super(title);
initFrame();
}
public void initFrame() {
JInternalFrame frame1
= new JInternalFrame("Frame 1", true, true, true, true);
JInternalFrame frame2
= new JInternalFrame("Frame 2", true, true, true, true);
JLabel label1 = new JLabel("Frame 1 contents...");
frame1.getContentPane().add(label1);
frame1.pack();
frame1.setVisible(true);
JLabel label2 = new JLabel("Frame 2 contents...");
frame2.getContentPane().add(label2);
frame2.pack();
frame2.setVisible(true);
// Default location is (0,0) for a JInternalFrame.
// Set the location of frame2, so that both frames are visible
int x2 = frame1.getX() + frame1.getWidth() + 10;
int y2 = frame1.getY();
frame2.setLocation(x2, y2);
// Add both internal frames to the desktop pane
desktopPane.add(frame1);
desktopPane.add(frame2);
// Finally add the desktop pane to the JFrame
this.add(desktopPane, BorderLayout.CENTER);
// Need to set minimum size for the JFrame
this.setMinimumSize(new Dimension(300, 300));
}
public static void main(String[] args) {
try {
// Set the system look and feel
UIManager.setLookAndFeel(
UIManager.getSystemLookAndFeelClassName());
}
catch (Exception e) {
e.printStackTrace();
}
SwingUtilities.invokeLater(() -> {
MDIApp frame = new MDIApp("MDI Frame");
frame.pack();
frame.setVisible(true);
frame.setExtendedState(frame.MAXIMIZED_BOTH);
});
}
}

图 3-11。
An MDI application in Swing run on Windows
当您使用 MDI 应用时,您需要使用JOptionPane的showInternalXxxDialog()方法,而不是showXxxDialog()方法。例如,在一个 MDI 应用中,您使用JOptionPane.showInternalMessageDialog()方法来代替JOptionPane.showMessageDialog()。showInternalXxxDialog()版本显示对话框,所以它们总是显示在顶层容器中,而showXxxDialog()版本显示一个对话框,它可以被拖动到 MDI 应用顶层容器的边界之外。
Tip
重要的是预先决定你是否想要开发一个 SDI、MDI 或 TDI 应用。从一种类型转换到另一种类型不是一件容易的事情。
工具箱类
Java 需要与本地系统通信,以提供大多数基本的 GUI 功能。它在每个平台上使用一个特定的类来实现这一点。java.awt.Toolkit是一个抽象类。Java 使用每个平台上的Toolkit类的一个子类与本地工具包系统通信。Toolkit类提供了一个静态的getDefaultToolkit()工厂方法来获取在特定平台上使用的工具箱对象。这个Toolkit类包含了一些有用的方法,可以让你调整屏幕尺寸和分辨率,访问系统剪贴板,发出嘟嘟声等等。表 3-2 列出了Toolkit类的一些方法。该表包含通过 HeadlessExceotion 的方法。当在不支持键盘、显示器或鼠标的环境中调用依赖于键盘、显示器或鼠标的代码时,将引发 HeadlessException。
表 3-2。
The List of a Few Useful Methods of the java.awt.Toolkit Class
| 工具包类的方法 | 描述 | | --- | --- | | `abstract void beep()` | 发出嘟嘟声。当应用中出现严重错误时,它有助于提醒用户。 | | `static Toolkit getDefaultToolkit()` | 返回应用中使用的当前`Toolkit`实例。 | | `abstract int getScreenResolution() throws HeadlessException` | 以每英寸点数的形式返回屏幕分辨率。 | | `abstract Dimension getScreenSize() throws HeadlessException` | 返回一个包含屏幕宽度和高度的`Dimension`对象,以像素为单位。 | | `abstract Clipboard getSystemClipboard() throws HeadlessException` | 返回代表系统剪贴板的`Clipboard`类的实例。 |下面的代码片段展示了一些如何使用Toolkit类的例子:
/* Copy the selected text from a JTextArea named dataTextArea to the system clipboard.
If there is no text selection, beep and display a message.
*/
Toolkit toolkit = Toolkit.getDefaultToolkit();
String data = dataTextArea.getSelectedText();
if (data == null || data.equals("")) {
toolkit.beep();
JOptionPane.showMessageDialog(null, "Please select the text to copy.");
}
else {
Clipboard clipboard = toolkit.getSystemClipboard();
// Pack data as a string in a Transferable object
Transferable transferableData = new StringSelection(data);
clipboard.setContents(transferableData, null);
}
/* Paste text from the system clipboard to a TextArea, named dataTextArea.
If there is no text in the system clipboard, beep and display a message.
*/
Toolkit toolkit = Toolkit.getDefaultToolkit();
Clipboard clipboard = toolkit.getSystemClipboard();
Transferable data = clipboard.getContents(null);
if (data != null && data.isDataFlavorSupported(DataFlavor.stringFlavor)) {
try {
String text = (String)data.getTransferData(DataFlavor.stringFlavor);
dataTextArea.replaceSelection(text);
}
catch (Exception e) {
e.printStackTrace();
}
}
else {
toolkit.beep();
JOptionPane.showMessageDialog(null, "No text in the system clipboard to paste");
}
/* Set the size of a JFrame to the size of the screen. Note that you can also use the
frame.setExtendedState(JFrame.MAXIMIZED_BOTH) method to use full screen area for a Jframe.
*/
JFrame frame = new JFrame("My Frame");
frame.setSize(Toolkit.getDefaultToolkit().getScreenSize());
使用 JLayer 装饰组件
JLayer类表示一个 Swing 组件。它用于修饰另一个组件,该组件称为目标组件。它允许您在它修饰的组件上执行自定义绘制。它还可以接收在其边界内生成的所有事件的通知。换句话说,JLayer允许您基于它所修饰的组件中发生的事件来执行定制处理。
当您使用JLayer类时,您还需要使用LayerUI类。一个JLayer将它的工作委托给一个LayerUI进行自定义绘制和事件处理。要使用JLayer做任何有意义的事情,您需要创建LayerUI类的子类,并覆盖其适当的方法来编写您的代码。
在 Swing 应用中使用JLayer需要以下步骤。
Create a subclass of the LayerUI class. Override its various methods to implement the custom processing for the component. The LayerUI class takes a type parameter that is the type of the component it will work with. Create an object of the LayerUI subclass. Create a Swing component (target component) that you want to decorate with a JLayer such as a JTextField, a JPanel, etc. Create an object of the JLayer class, passing the target component and the object of the LayerUI subclass to its constructor. Add the JLayer object to your container, not the target component.
让我们来看一个JLayer的动作。假设你想用一个JLayer在一个JTextField组件周围画一个蓝色的矩形边框。第一步是创建LayerUI的子类。清单 3-11 包含了从LayerUI类继承而来的BlueBorderUI类的代码。它覆盖了LayerUI类的paint()方法。
清单 3-11。LayerUI 类的子类,用于在图层周围绘制蓝色边框
// BlueBorderUI.java
package com.jdojo.swing;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import javax.swing.JComponent;
import javax.swing.JTextField;
import javax.swing.plaf.LayerUI;
public class BlueBorderUI extends LayerUI<JTextField> {
@Override
public void paint(Graphics g, JComponent layer) {
// Let the superclass paint the component first
super.paint(g, layer);
// Create a copy of the Graphics object
Graphics gTemp = (Graphics2D) g.create();
// Get the dimension of the layer
int width = layer.getWidth();
int height = layer.getHeight();
// Draw a blue rectangle that is custom your border
gTemp.setColor(Color.BLUE);
gTemp.drawRect(0, 0, width, height);
// Destroy the copy of the Graphics object
gTemp.dispose();
}
}
每当需要绘制目标组件时,就会调用LayerUI的paint()方法。LayerUI类的方法接收两个参数。第一个参数是可以用来在组件上绘制的Graphics对象的引用。第二个参数是对JLayer对象的引用,而不是目标组件。您可以使用第二个参数获得目标组件的引用,即JLayer正在修饰的组件。您可以将第二个参数转换为JLayer类型,并使用JLayer类的getView()方法,该方法返回目标组件的引用。paint()方法内部的逻辑很简单。它创建了一个Graphics参数的副本,并在组件周围画了一个蓝色的矩形。该方法传入的Graphics对象是为绘制该组件而设置的。建议复制传入的Graphics对象,因为更改传入的Graphics对象可能会导致意外结果。
现在你已经准备好使用带有一个JLayer的BlueBorderUI在一个JTextField周围画一个蓝色的边框。以下代码片段显示了逻辑:
// Create a JTextField as usual
JTextField firstName = new JTextField(10);
// Create an object of the BlueBorderUI
LayerUI<JTextField> ui = new BlueBorderUI();
// Create a JLayer object by wrapping the JTextField and BlueBorderUI
JLayer<JTextField> layer = new JLayer(firstName, ui);
// Add the layer object to a container, say the content pane of a frame.
// Note that you add the layer and not the component to a container.
contentPane.add(layer)
目标组件和LayerUI可能会在您创建它时被传递给一个JLayer。如果您不知道目标组件和/或JLayer的LayerUI,您可以稍后使用JLayer类的setView()和setUI()方法传递它们。JLayer类的getView()和getUI()方法让您分别获得当前目标组件的引用和JLayer的LayerUI。
清单 3-12 展示了如何使用一个JLayer在两个JTextField组件周围画一个边框。代码简单明了。当你运行这个程序时,它会在一个JFrame中显示两个带有蓝色边框的JTextField组件。
清单 3-12。使用 JLayer 装饰 JTextFeild 组件
// JLayerBlueBorderFrame.java
package com.jdojo.swing;
import java.awt.FlowLayout;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JLayer;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
import javax.swing.plaf.LayerUI;
public class JLayerBlueBorderFrame extends JFrame {
private JLabel firstNameLabel = new JLabel("First Name:");
private JLabel lastNameLabel = new JLabel("Last Name:");
private JTextField firstName = new JTextField(10);
private JTextField lastName = new JTextField(10);
public JLayerBlueBorderFrame(String title) {
super(title);
initFrame();
}
public void initFrame() {
this.setLayout(new FlowLayout());
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// Create an object of the LayerUI subclass - BlueBorderUI
LayerUI<JTextField> ui = new BlueBorderUI();
// Wrap the LayerUI and two JTextFields in two JLayers.
// Note that a LayerUI object can be shared by multiple JLayers
JLayer<JTextField> layer1 = new JLayer<>(firstName, ui);
JLayer<JTextField> layer2 = new JLayer<>(lastName, ui);
this.add(firstNameLabel);
this.add(layer1); // Add layer1, not firstName to the frame
this.add(lastNameLabel);
this.add(layer2); // Add layer2, not lastName to the frame
}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
JLayerBlueBorderFrame frame
= new JLayerBlueBorderFrame("JLayer Test Frame");
frame.pack();
frame.setVisible(true);
});
}
}
让我们看一个如何使用JLayer处理目标组件事件的例子。一个JLayer将事件处理任务委托给关联的LayerUI。您需要执行以下步骤来处理LayerUI子类中的事件。
Register for the events that a JLayer will process. Write the event handler code in an appropriate method of the LayerUI subclass.
您需要调用JLayer类的setLayerEventMask(long layerEventMask)方法来注册一个JLayer感兴趣的所有事件。该方法的layerEventMask参数必须是AWTEvent常量的位掩码。例如,如果一个名为layer的JLayer对按键和焦点事件感兴趣,您可以调用这个方法,如下所示:
int layerEventMask = AWTEvent.KEY_EVENT_MASK | AWTEvent.FOCUS_EVENT_MASK;
layer.setLayerEventMask(layerEventMask);
通常,JLayer在LayerUI子类的installUI()方法中注册事件。您需要在您的子类中覆盖LayerUI类的installUI()方法。卸载 UI 时,需要将JLayer的事件掩码设置为零。这是通过uninstallUI()方法完成的。下面的代码片段显示了一个JLayer注册一个焦点事件并重置其事件掩码:
public class SmartBorderUI extends LayerUI<JTextField> {
@Override
public void installUI(JComponent c) {
super.installUI(c);
JLayer layer = (JLayer)c;
// Register for the focus event
layer.setLayerEventMask(AWTEvent.FOCUS_EVENT_MASK);
}
@Override
public void uninstallUI(JComponent c) {
super.uninstallUI(c);
JLayer layer = (JLayer)c;
// Reset the event mask
layer.setLayerEventMask(0);
}
// Other code goes here
}
当一个注册的事件被传递给JLayer时,关联的LayerUI的eventDispatched(AWTEvent event, JLayer layer)方法被调用。您可能想在您的LayerUI子类中覆盖这个方法来处理所有注册的事件。从技术上讲,您重写此方法来处理事件是正确的。然而,有一种更好的方法在LayerUI子类中提供事件处理代码。LayerUI类的eventDispatched()方法在接收到一个事件时调用一个适当命名的方法。这些方法被声明为
protected void processXxxEvent(XxxEvent e, JLayer layer).
这里,Xxx是登记事件的名称。下面的代码片段展示了事件类型的示例以及当JLayer接收到该类事件时调用的方法声明:
public class SmartBorderUI extends LayerUI<JTextField> {
@Override
protected void processFocusEvent(FocusEvent e, JLayer layer) {
// Process the focus event here
}
@Override
protected void processKeyEvent(KeyEvent e, JLayer layer) {
// Process the key event here
}
@Override
protected void processMouseEvent(MouseEvent e, JLayer layer) {
// Process the mouse event here
}
// Other code goes here...
}
这就是在JLayer中处理事件所需要做的一切。让我们改进前面的例子。这一次,JLayer将在JTextField周围画一个边框,其颜色将取决于JTextField是否有焦点。当它获得焦点时,会绘制一个红色边框。当它失去焦点时,会绘制一个蓝色边框。
清单 3-13 包含了一个继承自LayerUI的SmartBorderUI类的代码。它的paint()方法根据目标组件是否有焦点来绘制红色或蓝色边框。它的installUI()方法为焦点事件注册。unInstallUI()方法通过将事件掩码设置为零来取消焦点事件的注册。它的processFocusEvent()方法处理焦点事件。请注意,当目标组件上发生焦点事件时,将调用此方法。它调用repaint()方法,后者又会调用paint()方法,后者根据组件的焦点状态绘制边框。
清单 3-13。基于焦点装饰 JTextField 的 LayerUI 子类
// SmartBorderUI.java
package com.jdojo.swing;
import java.awt.AWTEvent;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.FocusEvent;
import javax.swing.JComponent;
import javax.swing.JLayer;
import javax.swing.JTextField;
import javax.swing.plaf.LayerUI;
public class SmartBorderUI extends LayerUI<JTextField> {
@Override
public void paint(Graphics g, JComponent layer) {
// Let the superclass paint the component first
super.paint(g, layer);
Graphics gTemp = (Graphics2D) g.create();
int width = layer.getWidth();
int height = layer.getHeight();
// Suppress the unchecked warning
@SuppressWarnings("unchecked")
JLayer<JTextField> myLayer = (JLayer<JTextField>)layer;
JTextField field = (JTextField)myLayer.getView();
// When in focus, draw a red rectangle. Otherwise, draw a blue rectangle
Color bColor;
if (field.hasFocus()) {
bColor = Color.RED;
}
else {
bColor = Color.BLUE;
}
gTemp.setColor(bColor);
gTemp.drawRect(0, 0, width, height);
gTemp.dispose();
}
@Override
public void installUI(JComponent c) {
// Let the superclass do its job
super.installUI(c);
// Set the event mask for the layer stating that it is interested
// in listening to the focus event for its target
JLayer layer = (JLayer)c;
layer.setLayerEventMask(AWTEvent.FOCUS_EVENT_MASK);
}
@Override
public void uninstallUI(JComponent c) {
// Let the superclass do its job
super.uninstallUI(c);
JLayer layer = (JLayer) c;
// Set the event mask back to zero
layer.setLayerEventMask(0);
}
@Override
protected void processFocusEvent(FocusEvent e, JLayer layer) {
layer.repaint();
}
}
清单 3-14 包含了使用带有JLayer的SmartBorderUI类的代码。当你运行这个程序时,它会显示一个带有两个JTextField组件的JFrame。在JTextField组件之间改变焦点将会改变它们的边框颜色。
清单 3-14。基于焦点使用 Jlayer 装饰 JTextField 组件
// JLayerSmartBorderFrame.java
package com.jdojo.swing;
import java.awt.FlowLayout;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JLayer;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
import javax.swing.plaf.LayerUI;
public class JLayerSmartBorderFrame extends JFrame {
private JLabel firstNameLabel = new JLabel("First Name:");
private JLabel lastNameLabel = new JLabel("Last Name:");
private JTextField firstName = new JTextField(10);
private JTextField lastName = new JTextField(10);
public JLayerSmartBorderFrame(String title) {
super(title);
initFrame();
}
public void initFrame() {
this.setLayout(new FlowLayout());
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// Create an object of LayerUI subclass - SmartBorderUI
LayerUI<JTextField> ui = new SmartBorderUI();
// Wrap the LayerUI and two JTextFields in two JLayers
JLayer<JTextField> layer1 = new JLayer<>(firstName, ui);
JLayer<JTextField> layer2 = new JLayer<>(lastName, ui);
this.add(firstNameLabel);
this.add(layer1); // Add layer1 and not firstName to the frame
this.add(lastNameLabel);
this.add(layer2); // Add layer2 and not lastName to the frame
}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
JLayerSmartBorderFrame frame
= new JLayerSmartBorderFrame("JLayer Test Frame");
frame.pack();
frame.setVisible(true);
});
}
}
半透明的窗户
在讨论 Swing 中的半透明窗口之前,让我们定义三个术语:
- 透明的
- 半透明的
- 不透明的
如果一个东西是透明的,你就能看穿它;清水是透明的。如果某物是不透明的,你就看不透它;混凝土墙是不透明的。如果某物是半透明的,你可以看穿它,但不清楚。如果某物是半透明的,它部分允许光线通过;塑料窗帘是半透明的。术语“透明”和“不透明”描述两种相反的状态,而术语“半透明”描述透明和不透明之间的状态。
您可以定义窗口(如JFrame)的半透明程度。90%半透明的窗口是 10%不透明的。窗口的半透明程度可以使用像素的颜色分量的 alpha 值来定义。您可以使用Color类的构造函数定义颜色的 alpha 值:
Color(int red, int green, int blue, int alpha)Color(float red, float green, float blue, float alpha)
当颜色分量根据int值指定时,alpha参数的值指定在 0 到 255 之间。对于float类型参数,其值介于 0.0 和 1.0 之间。alpha值为 0 或 0.0 表示透明(100%半透明,0%不透明)。alpha值 255 或 1.0 表示不透明(0%半透明,完全不透明)。
支持窗口中的三种半透明形式。它们由枚举的以下三个常数表示:
- 在这种半透明形式中,窗口中的像素要么是不透明的,要么是透明的。也就是说,像素的 alpha 值为 0.0 或 1.0。
TRANSLUCENT:在这种形式的半透明中,一个窗口中的所有像素都具有相同的半透明性,可以用 0.0 到 1.0 之间的 alpha 值来定义。- 在这种半透明的形式中,窗口中的每个像素可以有自己的 alpha 值,在 0.0 到 1.0 之间。它可以让你在一个窗口中定义每个像素的透明度。
不是所有的平台都支持这三种半透明形式。在使用半透明之前,您必须检查程序中支持的半透明形式。否则,您的代码可能会抛出UnsupportedOperationException。GraphicsDevice类的isWindowTranslucencySupported()方法让你检查平台支持的半透明形式。清单 3-15 演示了如何检查平台上的半透明支持。清单中的代码很短,不言自明。为了缩短代码,我省略了后续示例中的检查。
清单 3-15。检查平台上的半透明支持
// TranslucencySupport.java
package com.jdojo.swing;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import static java.awt.GraphicsDevice.WindowTranslucency.*;
public class TranslucencySupport {
public static void main(String[] args) {
GraphicsEnvironment graphicsEnv
= GraphicsEnvironment.getLocalGraphicsEnvironment();
GraphicsDevice graphicsDevice
= graphicsEnv.getDefaultScreenDevice();
// Print the translucency supported by the platform
boolean isSupported
= graphicsDevice.isWindowTranslucencySupported(
PERPIXEL_TRANSPARENT);
System.out.println("PERPIXEL_TRANSPARENT supported: "
+ isSupported);
isSupported
= graphicsDevice.isWindowTranslucencySupported(TRANSLUCENT);
System.out.println("TRANSLUCENT supported: " + isSupported);
isSupported = graphicsDevice.isWindowTranslucencySupported(
PERPIXEL_TRANSLUCENT);
System.out.println("PERPIXEL_TRANSLUCENT supported: "
+ isSupported);
}
}
让我们看看一个统一的半透明JFrame在行动。你可以使用setOpacity(float opacity)方法设置JFrame的透明度。指定的opacity的值必须在 0.0f 和 1.0f 之间。在窗口上调用此方法之前,必须满足以下三个条件:
- 平台必须支持
TRANSLUCENT半透明。您可以使用清单 3-15 中的逻辑来检查平台是否支持TRANSLUCENT半透明。 - 窗户必须是未经装饰的。你可以通过调用
setUndecorated(false)方法来取消JFrame或JDialog的修饰。 - 窗口不能处于全屏模式。您可以使用
GraphicsDevice类的setFullScreenWindow(Window w)方法将窗口置于全屏模式。
如果不满足所有条件,将窗口的不透明度设置为 1.0f 以外会抛出IllegalComponentStateException。
清单 3-16 演示了如何使用一个均匀的半透明JFrame。下面两个语句在清单中的initFrame()方法中得到一个半透明的JFrame。第一条语句确保框架未被修饰,第二条语句根据不透明度设置框架的透明度。
清单 3-16。使用均匀半透明的 JFrame
// UniformTranslucentFrame.java
package com.jdojo.swing;
import java.awt.BorderLayout;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.SwingUtilities;
public class UniformTranslucentFrame extends JFrame {
private JButton closeButton = new JButton("Close");
public UniformTranslucentFrame(String title) {
super(title);
initFrame();
}
public void initFrame() {
this.setDefaultCloseOperation(EXIT_ON_CLOSE);
// Make sure the frame is undecorated
this.setUndecorated(true);
// Set 40% opacity. That is, 60% translucency.
this.setOpacity(0.40f);
// Set its size
this.setSize(200, 200);
// Center it on the screen
this.setLocationRelativeTo(null);
// Add a button to close the window
this.add(closeButton, BorderLayout.SOUTH);
// Exit the aplication when the close button is clicked
closeButton.addActionListener(e -> System.exit(0));
}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
UniformTranslucentFrame frame
= new UniformTranslucentFrame("Translucent Frame");
frame.setVisible(true);
});
}
}
// Make sure the frame is undecorated
this.setUndecorated(true);
// Set 40% opacity. That is, 60% translucency.
this.setOpacity(0.40f);
运行该程序时,您可以通过JFrame显示区看到屏幕上的内容。一个Close按钮被添加到框架上以关闭它。
让我们来看看一个每像素半透明JFrame的作用。您将在JPanel中创建一个渐变效果(渐隐效果),方法是为其显示区域中不同像素的背景色设置不同的 alpha 值。你可以用不同的方法获得每像素的透明度。最简单的方法是使用一个带有背景色的JPanel,并将 alpha 组件设置为所需的透明度。以下代码片段说明了这一点:
// Create a frame and set its properties
JFrame frame = new JFrame();
frame.setUndecorated(true);
frame.setBounds(0, 0, 200, 200);
// Set the background color of the frame to all zero, so that the per-pixel translucency works
frame.setBackground(new Color(0, 0, 0, 0));
// Create a blue JPanel with 128 alpha component
JPanel panel = new JPanel();
int alpha = 128;
Color bgColor = new Color(0, 0, 255, alpha);
panel.setBackground(bgColor);
// Add the JPanel to the frame and display it
frame.add(panel);
frame.setVisible(true);
代码中有两点不同。首先,它将框架的背景颜色设置为所有颜色组件都设置为 0,以实现每像素的半透明性。其次,它将包含 alpha 组件的JPanel的背景色设置为 128。你可以添加另一个JPanel到JFrame中,它的背景颜色使用不同的 alpha 组件。这将在JFrame上给你两个区域,它们的像素使用不同的透明度。
如果你使用一个GradientPaint类的对象来绘制你的JPanel,你可以得到一个更好的效果。一个GradientPaint对象用线性渐变图案填充一个Shape。它要求您指定两个点,p1 和 p2,以及每个点的颜色,c1 和 c2。p1 和 p2 之间连接线上的颜色将按比例从 c1 变为 c2。
清单 3-17 包含了一个自定义JPanel的代码,它使用一个GradientPaint对象来绘制它的区域。JPanel的背景颜色是在其构造函数中指定的。它覆盖了paintComponent()来提供自定义的绘画效果。渐变颜色图案由Graphics2D提供。该方法检查它是否有一个Graphics2D对象。起点 p1 是JPanel的左上角。起点 c1 的颜色与构造函数中传递的颜色相同。它使用 255 作为它的 alpha 分量。第二个点 p2 是JPanel的右上角,颜色相同,使用了零 alpha 组件。这将给JPanel一个渐变效果,从左边不透明到右边逐渐变透明。您可以通过更改这两个点和它们的 alpha 组件值来获得不同的渐变图案。它将GradientPaint对象设置为Graphics2D对象的Paint对象,并调用fillRect()方法来绘制。
清单 3-17。一个自定义 JPanel,使用每像素半透明的渐变颜色效果
// TranslucentJPanel.java
package com.jdojo.swing;
import java.awt.Color;
import java.awt.GradientPaint;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Paint;
import javax.swing.JPanel;
public class TranslucentJPanel extends JPanel {
private int red = 240;
private int green = 240;
private int blue = 240;
public TranslucentJPanel(Color bgColor) {
this.red = bgColor.getRed();
this.green = bgColor.getGreen();
this.blue = bgColor.getBlue();
}
@Override
protected void paintComponent(Graphics g) {
if (g instanceof Graphics2D) {
int width = this.getWidth();
int height = this.getHeight();
float startPointX = 0.0f;
float startPointY = 0.0f;
float endPointX = width;
float endPointY = 0.0f;
Color startColor = new Color(red, green, blue, 255);
Color endColor = new Color(red, green, blue, 0);
// Create a GradientPaint object
Paint paint = new GradientPaint(startPointX, startPointY,
startColor,
endPointX, endPointY,
endColor);
Graphics2D g2D = (Graphics2D) g;
g2D.setPaint(paint);
g2D.fillRect(0, 0, width, height);
}
}
}
清单 3-18 包含了查看每像素透明度的代码。它添加了背景色为红色、绿色和蓝色的TranslucentJPanel类的三个实例。添加了一个Close按钮来关闭框架。
清单 3-18。在 JFrame 中使用每像素半透明
// PerPixelTranslucentFrame.java
package com.jdojo.swing;
import java.awt.Color;
import java.awt.GridLayout;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.SwingUtilities;
public class PerPixelTranslucentFrame extends JFrame {
private JButton closeButton = new JButton("Close");
public PerPixelTranslucentFrame(String title) {
super(title);
initFrame();
}
public void initFrame() {
this.setDefaultCloseOperation(EXIT_ON_CLOSE);
// Make sure the frame is undecorated
this.setUndecorated(true);
// Set the background color with all components as zero,
// so per-pixel translucency is used
this.setBackground(new Color(0, 0, 0, 0));
// Set its size
this.setSize(200, 200);
// Center it on the screen
this.setLocationRelativeTo(null);
this.getContentPane().setLayout(new GridLayout(0, 1));
// Create and add three JPanel with different color gradients
this.add(new TranslucentJPanel(Color.RED));
this.add(new TranslucentJPanel(Color.GREEN));
this.add(new TranslucentJPanel(Color.BLUE));
// Add a button to close the window
this.add(closeButton);
closeButton.addActionListener(e -> System.exit(0));
}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
PerPixelTranslucentFrame frame
= new PerPixelTranslucentFrame("Per-Pixel Translucent Frame");
frame.setVisible(true);
});
}
}
图 3-12 显示了程序运行时的JFrame。请注意框架中的渐变效果。从左向右移动时,每个面板都变得更加透明。图中显示的文本不是JFrame的一部分。显示JFrame时,文本显示在背景中。可以透过JFrame的半透明部分看到。

图 3-12。
A JFrame using per-pixel translucency
异形窗
Swing 允许你创建一个定制形状的窗口,比如圆形的JFrame,椭圆形的JDialog等等。你可以通过使用Window类的setShape(Shape s)方法给一个窗口定制形状。窗户的形状只受你想象力的限制。您可以通过使用java.awt.geom包中的类组合多个形状来创建一个形状。下面的代码片段创建一个形状,该形状包含一个放置在矩形上方的椭圆。最后,它将自定义形状设置为一个JFrame。
// Create a shape with an ellipse over a rectangle
Ellipse2D.Double ellipse = new Ellipse2D.Double(0, 0, 200, 100);
Rectangle2D.Double rect = new Rectangle2D.Double(0, 100, 200, 200);
// Combine an ellipse and a rectangle into a Path2D object to get a new shape
Path2D path = new Path2D.Double();
path.append(rect, true);
path.append(ellipse, true);
// Create a JFrame
JFrame frame = new JFrame("A Custom Shaped JFrame");
// Set the custom shape to the JFrame
Frame.setShape(path);
一个Window在屏幕上拥有一个矩形区域。如果您给窗口指定了自定义形状,它的某些部分可能会被剪切掉。不属于自定义形状的形状窗口部分不可见,也不可单击。图 3-13 显示了一个定制形状的窗口,在矩形上方放置了一个椭圆。该窗口包含一个Close按钮。椭圆四个角周围的区域不可见,也不可点击。

图 3-13。
A custom shaped window with an ellipse placed above a rectangle
要使用异形窗,必须满足以下三个标准:
- 平台必须支持
PERPIXEL_TRANSPARENT半透明。您可以使用清单 3-15 中的逻辑来检查是否支持PERPIXEL_TRANSPARENT半透明。 - 窗户必须是未经装饰的。你可以通过调用
setUndecorated(false)方法来取消JFrame或JDialog的修饰。 - 窗口不能处于全屏模式。您可以使用
GraphicsDevice类的setFullScreenWindow(Window w)方法将窗口置于全屏模式。
清单 3-19 包含了显示一个如图 3-13 所示的形状JFrame的代码。
清单 3-19。使用定制形状的 JFrame
// ShapedFrame.java
package com.jdojo.swing;
import java.awt.BorderLayout;
import java.awt.geom.Path2D;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Rectangle2D;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.SwingUtilities;
public class ShapedFrame extends JFrame {
private JButton closeButton = new JButton("Close");
public ShapedFrame() {
initFrame();
}
public void initFrame() {
// Make sure the frame is undecorated
this.setUndecorated(true);
this.setDefaultCloseOperation(EXIT_ON_CLOSE);
this.setSize(200, 200);
// Create a shape with an ellipse placed over a rectangle
Ellipse2D.Double ellipse = new Ellipse2D.Double(0, 0, 200, 100);
Rectangle2D.Double rect = new Rectangle2D.Double(0, 100, 200, 200);
// Combine the ellipse and rectangle into a Path2D object and
// set it as the shape for the JFrame
Path2D path = new Path2D.Double();
path.append(rect, true);
path.append(ellipse, true);
this.setShape(path);
// Add a Close button to close the frame
this.add(closeButton, BorderLayout.SOUTH);
closeButton.addActionListener(e -> System.exit(0));
}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
// Display the custom shaped frame
ShapedFrame frame = new ShapedFrame();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
});
}
}
清单中的initFrame()方法中的以下代码部分很有意思:
// Make sure the frame is undecorated
this.setUndecorated(true);
// Create a shape with an ellipse placed over a rectangle
Ellipse2D.Double ellipse = new Ellipse2D.Double(0, 0, 200, 100);
Rectangle2D.Double rect = new Rectangle2D.Double(0, 100, 200, 200);
// Combine the ellipse and rectangle into a Path2D object and
// set it as the shape for the JFrame
Path2D path = new Path2D.Double();
path.append(rect, true);
path.append(ellipse, true);
this.setShape(path);
第一条语句确保JFrame没有被修饰。创建了两个形状,一个椭圆和一个矩形。它们的坐标和大小被设置为将椭圆放置在矩形上。一个Path2D.Double对象用于将椭圆和矩形连接成一个自定义的Shape对象。Path2D是java.awt.geom包中的一个abstract类。它声明了两个静态内部类Path2D.Double和Path2D.Float,分别以双精度和单精度浮点数存储形状的坐标。Shape是在java.awt包中声明的接口。Path2D类实现了Shape接口。注意,Window类中的setShape()方法将Shape接口的一个实例作为参数。Path2D类的append()方法将指定的Shape对象的几何图形附加到路径上。append()方法的第二个参数是一个指示器,指示您是否希望使用线段连接两个形状。如果是true,对moveTo()方法的调用被转换成lineTo()方法。在这种情况下,true的值对于这个参数来说没有意义。请研究一下java.awt.geom包中的类,了解更多可以在 Java 应用中使用的有趣形状。
摘要
Swing 组件内置了将 HTML 文本显示为标签的支持。您可以使用 HTML 格式的字符串作为JButton、JMenuItem、JLabel、JToolTip、JTabbedPane、JTree等的标签。使用一个 HTML 字符串,它应该分别以<html>和</html>标签开始和结束。如果不希望 Swing 将 HTML 标签中的文本解释为组件的 HTML,可以通过调用组件上的putClientProperty("html.disable", Boolean.TRUE)方法来禁用该特性。
Swing 组件不是线程安全的。你应该从一个叫做事件调度线程的线程中更新组件的状态。组件的所有事件处理程序都在事件调度线程中执行。Swing 自动创建事件调度线程。Swing 提供了一个名为SwingUtilities的实用程序类来处理事件调度线程;它的invokeLater(Runnable r)方法调度指定的Runnable在事件调度线程上执行。构建 Swing GUI 并在事件调度线程上显示它是安全的。如果该方法由事件调度线程执行,SwingUtilities类的isEventDispatchThread()返回 true。
在事件调度线程上运行长时间运行的任务会使您的 GUI 无响应。Swing 提供了一个SwingWorker类来在工作线程上执行长时间运行的任务,这些工作线程不是事件调度线程。SwingWorker类提供了在事件调度线程上发布任务结果的特性,可以安全地更新 Swing 组件。
Swing 提供了可插拔的 L&F。它附带了一些预定义的 L&F。您可以使用UIManager.setLookAndFeel()方法为您的应用设置一个新的 L & F。
Swing 支持名为 Synth 的可换肤 L&F,它允许您在外部 XML 文件中定义 L&F。
拖放(DnD)是一种在应用组件之间传输数据的方式。Swing 支持 Swing 组件之间、Swing 组件和本机组件之间的 DnD。使用 DnD,您可以在两个组件之间复制、移动和链接数据。
使用 Swing,您可以开发一个多文档界面(MDI)应用,它由桌面管理器管理的多个框架组成。MPI 应用中的帧可以以不同的方式排列;例如,它们可以分层排列,它们可以级联,它们可以并排放置,等等。
Swing 提供了一个Toolkit类的实例来与本地系统通信。这个类包含了很多有用的方法,比如发出哔哔声,知道屏幕分辨率和大小等等。
Swing 让你拥有半透明的窗口。半透明性可以被定义为对于窗口中的所有像素都是相同的,或者以每个像素为基础。
在 Swing 中,您并不局限于只有矩形窗口。它可以让你创建异形窗口。异形窗可以是任何形状,例如圆形、椭圆形或任何定制的形状。
四、Applet
在本章中,您将学习
- 什么是 applet
- 如何开发、部署和运行 Applet
- 如何使用
<applet>标签在 HTML 文档中嵌入 applet - 如何安装和配置运行 Applet 的 Java 插件
- 如何使用
appletviewer程序运行 Applet - Applet 的生命周期
- 如何将参数传递给 Applet
- 如何发布 Applet 的参数和描述
- 如何在 Applet 中使用图像和音频剪辑
- 如何定制 Java 策略文件以授予 Applet 权限
- 如何签署 Applet
什么是 Applet?
是一种嵌入在 HTML 文档中并在 web 浏览器中运行的 Java 程序。组成 applet 的编译后的 Java 代码存储在 web 服务器上。web 浏览器通过互联网从 web 服务器下载 applet 代码,并在浏览器的上下文中本地运行该代码。通常,applet 有一个图形用户界面(GUI)。一个 applet 有许多安全限制,包括它在客户端计算机上能访问什么和不能访问什么。对 Applet 的限制是必要的,因为 Applet 可能不是由同一个人开发和使用的。如果允许恶意编写的小应用完全访问客户端机器,它可能会对客户端机器造成有害影响。例如,安全限制不允许 applet 访问文件系统或在客户机上启动程序。假设你打开一个网页,里面有一个 applet 可以读取你机器上的文件。在你不知情的情况下,一个恶意的 Applet 会把你的私人信息发送到它的服务器上。为了保护 applet 用户免受这种伤害,有必要在运行 applet 时设置安全限制。使用策略文件可以配置许多安全限制。我将在本章后面讨论如何配置 Applet 安全策略。
虽然 a 和 Applet 没有关系,但是我还是来解释一下两者的区别。像 applet 一样,servlet 也是部署在 web 服务器上的 Java 程序。与 applet 不同,servlet 运行在 web 服务器本身上,它不包括 GUI。
开发小应用
开发 applet 有四个步骤:
- 为 applet 编写 Java 代码
- 打包和部署 applet 文件
- 安装和配置 Java 插件
- 查看子视图
为 applet 编写 Java 代码与为 Swing 应用编写代码没有太大区别。您只需要学习一些将在代码中使用的 Applet 的标准类和方法。
小应用部署在网络服务器上,并通过互联网/内联网使用网络浏览器在网页中查看。您还可以在开发和测试期间使用 applet 查看器查看 applet。JDK 发布了一个 appletviewer 程序。安装 JDK 时,appletviewer 程序会安装在您机器上的JAVA_HOME\bin目录中。我将在本章后面详细讨论如何使用 appletviewer。
要在网页中查看 applet,您需要在 HTML 文档中嵌入对 applet 的引用。您可以使用三种 HTML 标签中的任何一种,即<applet>、<object>或<embed>来将 applet 嵌入到 HTML 文档中。我将很快详细讨论这些标签的使用。
接下来的两节讨论如何为 applet 编写 Java 代码,以及如何查看 applet。
编写 Applet
您的 applet 类必须是 Java 提供的标准 applet 类的子类。有两个标准的 applet 类:
java.applet.Appletjavax.swing.JApplet
Applet类支持 AWT GUI 组件,而JApplet类支持 Swing GUI 组件。JApplet类继承自Applet类。在这一章中我将只讨论JApplet。清单 4-1 显示了你能拥有的最简单的 applet 的代码。
清单 4-1。最简单的 Applet
// SimplestApplet.java
package com.jdojo.applet;
import javax.swing.JApplet;
public class SimplestApplet extends JApplet {
// No extra code is needed for your simplest applet
}
SimplestApplet没有任何 GUI 部件或逻辑。从技术上来说,它是一个完整的 Applet。如果您在浏览器中测试这个 applet,您所看到的只是网页中的一个空白区域。
让我们创建另一个带有 GUI 的 applet,这样您可以在浏览器中看到一些东西。新的 applet 叫做HelloApplet,如清单 4-2 所示。
清单 4-2。使用 JLabel 显示消息的 HelloApplet Applet
// HelloApplet.java
package com.jdojo.applet;
import java.awt.Container;
import java.awt.FlowLayout;
import javax.swing.JApplet;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JTextField;
import static javax.swing.JOptionPane.INFORMATION_MESSAGE;
public class HelloApplet extends JApplet {
@Override
public void init() {
// Create Swing components
JLabel nameLabel = new JLabel("Your Name:");
JTextField nameFld = new JTextField(15);
JButton sayHelloBtn = new JButton("Say Hello");
// Add an action litener to the button to display the message
sayHelloBtn.addActionListener(e -> sayHello(nameFld.getText()));
// Add Swing components to the content pane of the applet
Container contentPane = this.getContentPane();
contentPane.setLayout(new FlowLayout());
contentPane.add(nameLabel);
contentPane.add(nameFld);
contentPane.add(sayHelloBtn);
}
private void sayHello(String name) {
String msg = "Hello there";
if (name.length() > 0) {
msg = "Hello " + name;
}
// Display the hello message
JOptionPane.showMessageDialog(null,
msg, "Hello", INFORMATION_MESSAGE);
}
}
这个类的代码看起来熟悉吗?这类似于使用自定义的JFrame。JApplet类包含一个init()方法。您需要重写该方法,并向 applet 添加 GUI 部件。我将很快详细讨论用 applet 的方法编写代码。像一个JFrame,JApplet有一个内容窗格,包含 applet 的组件。您在JApplet的内容窗格中添加了一个JLabel、一个JTextField和一个JButton。程序逻辑很简单。用户可以输入姓名并点击显示消息的Say Hello按钮。
与 Swing 应用不同,您不应该在 applet 的构造函数中添加任何 GUI,即使它在大多数情况下都可以工作。调用 applet 的构造函数来创建 applet 类的对象。applet 对象在创建时不会获得其“applet”状态。它还是一个普通的 Java 对象。如果在构造函数中使用 applet 的任何特性,这些特性将无法正常工作,因为 applet 对象只是一个简单的 Java 对象,而不是真正意义上的“applet”。在创建之后,它获得 applet 的状态,并且它的init()方法被显示它的环境(通常是浏览器)调用。这就是为什么您需要将所有 GUI 代码(或任何初始化代码)放在它的init()方法中的原因。Applet类提供了一些其他的标准方法,您可以覆盖这些方法并编写您的逻辑来在 applet 中执行不同种类的工作。
运行 applet 的方式与运行 Swing 应用的方式不同。注意,applet 类没有main()方法。然而,从技术上来说,可以在 applet 中添加一个方法,但这对运行 applet 没有任何帮助。要查看 applet 的运行情况,您需要一个 HTML 文件。你应该有 HTML 的基本知识来使用 applet,但是你不需要成为 HTML 的专家。我将在下一节讨论如何查看 applet。这时候你就要编译HelloApplet类了。您将拥有一个名为HelloApplet.class的类文件。
部署 Applet
Applet 是 Java 程序。但是,它们不能像其他 Java 程序一样直接运行。在运行 applet 之前,您需要做一些准备工作。Applet 需要部署后才能使用。applet 部署分为两个部分:
- 定义 applet GUI 和逻辑的 Java 代码
- 一个 HTML 文档,包含 applet 的详细信息,如类名、包含类文件的存档文件名、宽度、高度等。
在上一节中,您已经看到了如何为 applet 编写 Java 代码。
使用<applet>标签将 applet 细节嵌入到 HTML 文档中。applet 代码和 HTML 文档都被部署到 web 服务器上。客户机上的浏览器向 web 服务器请求 HTML 文档。当浏览器在 HTML 文档中找到<applet>标记时,它会读取 applet 的详细信息,从 web 服务器下载 applet 代码,并在浏览器中将代码作为 applet 运行。这是否意味着您需要一个 web 服务器来查看您的 Applet 的运行情况?答案是否定的。你可以不用网络服务器来测试你的 Applet。如果您想让用户可以使用您的 Applet,您需要一个 web 服务器来部署您的 Applet。以下部分描述了如何为 applet 创建 HTML 文档,以及如何将 applet 部署到不同的环境中。
创建 HTML 文档
一个<applet>标签被用来在一个 HTML 文档中嵌入一个 applet。下面是一个<applet>标签的例子:
<applet code="com.jdojo.applet.HelloApplet" width="300" height="100" archive="myapplets.jar">
This browser does not support Applets.
</applet>
您需要指定标签的以下强制属性:
codewidthheightarchive
属性指定了 applet 的全限定类名。可选地,您可以将.class附加到 applet 的完全限定名。例如,以下两个<applet>标签的工作原理相同:
<!-- Use fully qualified name of the applet class as code -->
<applet code="com.jdojo.applet.HelloApplet">
...
</applet>
<!-- Use fully-qualified name of the applet class followed by .class -->
<applet code="com.jdojo.applet.HelloApplet.class">
...
</applet>
您也可以使用正斜杠而不是点来分隔子包名称。例如,您也可以将 code 属性的值指定为"com/jdojo/applet/HelloApplet"和com/jdojo/applet/HelloApplet.class。
width和height属性分别指定网页中 applet 区域的初始宽度和高度。您可以用像素或百分比指定width和height属性。如果值是数字,则以像素为单位;例如,width="150"表示 150 像素的width。如果值后面有一个百分号(%),则表示显示 Applet 的容器的尺寸百分比;例如,width="50%"表示 applet 的宽度将是其容器的 50%。通常,容器是浏览器窗口。
如果您正在使用 Java 7 Update 51 或更高版本来查看 applet,archive属性是必需的。您需要将一个 applet 的所有文件——类文件和其他资源文件——捆绑到一个 JAR 文件中。将 applet 文件捆绑在一个 JAR 文件中会使文件变得更小,从而加快 applet 用户的下载速度。属性的值是包含 applet 文件的 JAR 文件的名称。
如果浏览器不支持<applet>标签,您可能希望在网页中显示一条消息。消息应该放在<applet>和</applet>标签之间,如下所示。如果浏览器支持 Applet,它将忽略该消息。
<applet>
Inform the user that the browser does not support applets.
</applet>
清单 4-3 显示了用于测试 applet 的helloapplet.html文件的内容。注意,<applet>标签不包含archive属性,该属性允许您测试 applet,而不必创建 JAR 文件。
清单 4-3。helloapplet.html 档案的内容
<html>
<head>
<title>Hello Applet</title>
</head>
<body>
<h3>Hello Applet in Action</h3>
<applet code="com.jdojo.applet.HelloApplet" width="200" height="100">
This browser does not support Applets.
</applet>
</body>
</html>
在生产中部署 Applet
在生产环境中,您必须使用 JAR 文件部署 applet,并使用可信机构颁发的证书对 JAR 文件进行签名。对 JAR 进行自签名是行不通的。在测试环境中,您可以忽略这个需求,并且您可以使用一个未签名的 JAR 文件或者简单地使用类文件。在本章中,我将向你展示如何忽略这个需求来测试 Applet。如果你是第一次学习 Applet,你可以跳到下一节。当您需要在生产环境中部署您的 applet 时,您可以重温这一节。
使用以下步骤来打包和部署 Applet。这些步骤指的是与创建的 JAR 文件相关的术语和命令。有关创建 JAR 文件的更多细节,请参考《Java 语言特性入门》( ISBN: 978-1-4302-6658-7)一书中的第八章。
Create a manifest file (say manifest.mf). It must contain a Permissions attribute. The following shows the contents of the manifest file:
Manifest-Version: 1.0
Permissions: sandbox
Permissions属性的另一个值是all-permissions。sandbox的值表示 applet 将在安全沙箱中运行,并且不需要访问客户端机器上的任何额外资源。all-permissions的值表示 applet 需要访问客户端的机器。
Create a JAR file that contains all class files for the applet and the manifest file created in the previous section. You can use the following command to create the JAR file named helloapplet.jar:
jar cvfm helloapplet.jar manifest.mf com\jdojo\applet\*.class
Sign the helloapplet.jar file with the certificate you obtained from a trusted authority. Obtaining a certificate costs money (approximately $100). If you are just learning applets, you can skip this step. The “Signing Applets” section later in this chapter explains in detail how to sign an applet. Deploy the signed helloapplet.jar file to the web server. You will need to consult the documentation of your web server on how to deploy applets. Some web servers provide deployment screens to let you deploy your JAR files and some let you drop the JAR file into a specific directory. The typical way of deploying files to a web server is to let the development IDE such as NetBeans and Eclipse package and deploy the necessary project files for you.
部署 Applet 进行测试
如果你需要按照上一节描述的步骤来进行测试,那么打包和部署一个 applet 就太麻烦了。您可以在文件系统中保存所有的类文件和 HTML 文件,并测试您的 applet。我假设您有 applet 文件,并且它们的完整路径类似于以下路径:
C:\myapplets\helloapplet.htmlC:\myapplets\com\jdojo\applet\HelloApplet.class
使用 Windows 上使用的文件路径语法显示路径。如果您使用的不是 Windows,请将它们更改为操作系统使用的路径语法。
您不需要将 applet 文件存储在特定的目录中,如C:\myapplets。您可以用任何目录的路径替换目录C:\myapplets。但是,您必须保留在C:\myapplets目录之后的文件路径。在您阅读更多章节后,您将能够使用存储 applet 文件的目录结构。
如果您已经创建了helloapplet.jar文件来测试 applet,我假设您已经将archive属性添加到了helloapplet.html文件中的<applet>标签中作为archive="helloapplet.jar",并且文件路径如下所示:
C:\myapplets\helloapplet.htmlC:\myapplets\helloapplet.jar
安装和配置 Java 插件
浏览器使用 Java 插件来运行 Applet。在运行 applet 之前,您必须安装和配置 Java 插件。
安装 Java 插件
Java 运行时环境(JRE)也称为 Java 插件或 Java 附加组件。当您安装 JDK 时,JRE(以及 Java 插件)已经为您安装好了。运行 Applet 的客户机不需要安装 JDK。它可以只安装 JRE。您可以从 www.oracle.com 下载 JRE 的最新版本,在撰写本文时是 8.0。JRE 可以免费下载、安装和使用。
在 Windows 8 上,使用 64 位 JRE 8.0,我只能在 Internet Explorer 中运行我的 Applet。我不得不卸载 64 位的 JRE 8.0,安装 32 位的 JRE 9.0,这样我的 Applet 才能在所有浏览器中运行,比如谷歌 Chrome、Mozilla Firefox 和 Internet Explorer。
在 Linux 上,您需要做一些手动设置来为 Firefox 浏览器安装 Java 插件。请按照 www.oracle.com/technetwork/java/javase/manual-plugin-install-linux-136395.html 中的说明在 Linux 上设置 Java 插件。
打开 Java 控制面板
您可以使用 Java 控制面板程序配置 Java 插件。Java 控制面板程序启动如图 4-1 所示的窗口。

图 4-1。
The Java Control Panel
在 Windows 8 上,您可以通过以下步骤访问 Java 控制面板。
Open Search by pressing the Windows logo key + W. Make sure to select “Everywhere” for the search location. By default “Settings” is selected. Enter “Java,” “Java Control Panel,” or “Configure Java” as the search term. Click the Java icon to open the Java Control Panel. If you could not find the Java Control Panel using Search, open the Control Panel by right-clicking the Start icon and selecting Control Panel from the menu. In the top-right corner in the Control Panel, you get a Search field. Enter “Java” in the Search field and you will see a program named Java. Click the program name to open the Java Control Panel.
在 Windows 7 上,您可以通过以下步骤访问 Java 控制面板。
Click the Start button, and then select the Control Panel option from the menu. Enter Java Control Panel in the Search field in Control Panel. Click the Java icon to open the Java Control Panel. Tip
在 Windows 上,您可以通过运行位于JRE_HOME\bin目录下的文件javacpl.exe直接启动 Java 控制面板。对于 JRE 8,默认路径是C:\Program Files\Java\jre8\bin\javacpl.exe。
在 Linux 上,您可以通过从终端窗口运行ControlPanel程序来访问 Java 控制面板。ControlPanel程序安装在JRE_HOME\bin目录中,其中 JRE_HOME 是您安装 JRE 的目录。假设您已经在/java8/jre目录下安装了 JRE。您需要从终端窗口运行以下命令:
[/java8/jre/bin]$ ./ControlPanel
在 Mac OS X (10.7.3 及更高版本)上,您可以使用以下步骤访问 Java 控制面板:
- 点击屏幕左上角的苹果图标,进入系统偏好设置。
- 单击 Java 图标访问 Java 控制面板。
配置 Java 插件
您可以使用 Java 控制面板为 Java 插件配置各种设置。在这一节中,我将描述如何绕过运行 applets 的签名 JAR 要求。打开 Java 控制面板,选择安全选项卡,如图 4-2 所示。

图 4-2。
Configuring the security settings in the Java Control Panel
标记为“在浏览器中启用 Java 内容”的复选框可让您启用/停用在浏览器中运行 Applet 的支持。默认情况下,此复选框处于选中状态,Applet 可以在浏览器中运行。如果未选中此复选框,您将无法在浏览器中运行 Applet。
第二个设置是安全级别,可以通过滑动垂直滑块控件的旋钮来设置。它可以设置为以下三个值:
- 非常高:这是最严格的安全级别设置。只有具有有效证书并且在主 JAR 文件的清单中包含了
Permissions属性的已签名的 Applet 才允许在有安全提示的情况下运行。所有其他 Applet 都被阻止。 - 高:这是推荐的最低默认安全级别设置。使用有效或过期证书签名的 Applet 以及在主 JAR 文件的清单中包含
Permissions属性的 Applet 允许在有安全提示的情况下运行。当无法检查证书的撤销状态时,Applet 也允许在安全提示下运行。所有其他 Applet 都被阻止。 - 中:仅阻止请求所有权限的未签名 Applet。所有其他 Applet 都允许在安全提示下运行。不建议选择此安全级别。如果你运行一个恶意的 Applet,它会使你的计算机更容易受到攻击。
出于测试目的,您可以将安全级别设置为中。这将允许您测试打包在未签名的 JAR 文件中的 Applet。您也不需要在清单文件中包含Permissions属性。它还允许您从文件系统测试您的 Applet,避免了 web 服务器部署您的 Applet 的需要。完成测试后,您应该将安全设置改回推荐的高或非常高。请注意,当您尝试运行任何不符合安全要求的 Applet 时,使用“中”安全级别设置会向您显示警告。当您收到警告时,您需要确认是否要继续运行 Applet,尽管存在安全风险。
“安全”选项卡上的第三项设置称为“例外站点列表”。这使您可以绕过指定站点的安全级别设置所需的安全要求。点击“编辑站点列表”按钮,打开异常站点列表对话框,如图 4-3 所示。

图 4-3。
The Exception Site List Dialog Box
点击Add按钮。您将看到为该位置添加了一个空行。输入位置的file:///(注意三个///)。再次点击Add按钮。第二次点击Add按钮会显示安全警告信息,说明添加file://(注二/ /)有安全风险。点击警告对话框上的Continue按钮。您会得到另一个空行位置。输入http://localhost:8080。重复此步骤再添加一个位置, http://www.jdojo.com 。异常站点列表对话框应如图 4-4 所示。现在,单击OK按钮返回到安全选项卡。

图 4-4。
The Exception Site List Dialog Box
从现在开始,无论“安全级别”设置如何,您都可以从以下三个站点运行所有 Applet:
file:///表示使用file协议的文件系统中的 Applet。http://localhost:8080是指使用http协议在您机器上的 8080 端口运行的任何 web 服务器。http://www.jdojo.com表示使用http协议从网站www.jdojo.com运行的 Applet。我维护jdojo.com。您可以使用 URLhttp://www.jdojo.com/myapplets/helloapplet.html访问 hello Applet。
一旦您完成测试您的 Applet,请从例外列表中删除这些网站,这样您的计算机就不会运行恶意 Applet。
查看 Applet
如果您已经遵循了前面几节中的步骤,查看 applet 就像在浏览器中输入hellapapplet.html文件的 URL 一样简单。按照以下步骤查看 Applet。
Open the browser of your choice, such as Google Chrome. Mozilla Firefox, or Internet Explorer. Press Ctrl + O or select the Open menu option from the File menu. You will get a browse/open dialog box. Navigate to the directory in which you have stored the helloapplet.html file and open it in the browser. Depending on the settings in the Java Control Panel, you may get security warnings, which you need to ignore. Alternatively, you can enter the URL for the HTML file directly. If you saved the helloapplet.html file in the C:\myapplets directory in windows, you can enter the URL as file:///C:/myapplets/helloapplet.html. If everything was set up correctly, you will see the applet running in your browser as shown in Figure 4-5. Enter your name and click the Say Hello button to display a greeting dialog box.

图 4-5。
The Hello Applet running from the file system in the Google Chrome browser
如果您无法使用这些步骤查看 Applet,请阅读下一节,该节将描述如何使用appletviewer在测试期间查看 Applet。
使用 appletviewer 测试 Applet
您可以使用appletviewer命令查看 Applet。它在JAVA_HOME\bin文件夹中作为appletviewer程序提供,其中JAVA_HOME是您机器上的 JDK 安装文件夹。以下是命令语法的一般形式:
appletviewer <options> <urls>
在<options>中,您可以指定命令的各种选项。您必须指定一个或多个由空格分隔的包含 Applet 文档的 URL。您可以使用以下任何命令来查看上一节中描述的 applet。在 Microsoft Windows 上,可以使用命令提示符输入命令。在 Linux 上,使用终端窗口。
appletviewerhttp://www.jdojo.com/myapplets/helloapplet.html
或者
appletviewer file:///C:/myapplets/helloapplet.html
当您运行上述命令时,可能会出现以下错误:
'appletviewer' is not recognized as an internal or external command, operable program or batch file.
如果您收到上述错误,您需要指定 appletviewer 命令的完整路径,例如C:\java8\bin\appletviewer,假设您已经在C:\java8目录中安装了 JDK。您可以在 Windows 命令提示符下尝试以下命令:
C:\java8\appletviewerhttp://www.jdojo.com/myapplets/helloapplet.html
图 4-6 显示了在 appletviewer 窗口中运行的 Applet。请注意,appletviewer 只显示 URL 中指定的文档中的 applet。所有其他 HTML 内容都会被忽略。例如,applet 不显示您在<h3>标签中添加的来自helloapplet.html文件的文本。

图 4-6。
The Hello Applet running from the file system in the Google Chrome browser
如果您想使用appletviewer命令查看多个 Applet,您可以通过在命令行上指定多个 URL 来实现。每个 Applet 将显示在单独的 Applet 查看器窗口中。以下命令可用于显示来自两个不同 web 服务器的两个 Applet,其中URL_PART1可以是http://www.myserver1.com/myapplets1URL_PART2可以是 http://www.myserver2.com/myapplets2 :
appletviewer URL_PART1/applet1.html URL_PART2/applet2.html
appletviewer 命令在单独的窗口中显示文档中的每个 Applet。例如,如果applet1.html包含两个 Applet,而applet2.html包含三个 Applet,上述命令将打开五个 Applet 查看器窗口。如果 URL 引用的文档不包含任何 applet,appletviewer命令不做任何事情。URL 引用的文档中的内容将被忽略,与 applet 相关的部分除外。appletviewer 窗口有一个名为“applet”的主菜单,可以让你重新加载、重启、停止、保存一个 Applet,等等。
您可以为appletviewer命令指定三个选项:
-debug-encoding-Jjavaoptions
–选项允许您在调试模式下启动 appletviewer。您可以使用–选项指定 URL 引用的文档编码。–选项允许您为 JVM 指定任何 Java 选项。选项的–J部分被删除,剩余部分被传递给 JVM。以下是使用这些选项的示例。注意,要为 appletviewer 指定 classpath 环境变量,需要指定两次–J选项。
appletviewer –debug your_document_url_goes_here
appletviewer –encoding ISO-8859-1 your_document_url_goes_here
appletviewer –J-classpath -Jc:\myclasses your_document_url_goes_here
Tip
如果您使用 NetBeans IDE 开发 Applet,请右键单击 Applet 文件,例如 IDE 中的HelloApplet.java,并选择Run File菜单选项,在 appletviewer 中运行您的 Applet。
使用 codebase 属性
在HelloApplet示例中,您将 Java 类文件和 HTML 文件放在同一个父目录下。您的文件放置如下:
ANY_DIR\html_fileANY_DIR\package_directories\class_file
您不必遵循上述目录结构来使用您的 Applet。存储 applet 的 HTML 文件的父目录称为文档。存储 Java 类文件(总是考虑放置 applet 类的包所需的目录结构)的父目录称为代码库。您可以使用codebase属性在<applet>标签中为您的 applet 指定一个代码库。如果不指定codebase属性,则文档库被用作codebase。codebase属性可以是相对 URL,也可以是绝对 URL。使用代码库的绝对 URL 为存储 applet 类文件开辟了另一种可能性。您可以将 applet 的 HTML 文件存储在一个 web 服务器上,而将 Java 类存储在另一个 web 服务器上。在这种情况下,您必须为 java 类指定一个绝对的codebase。
使用 HTML 文档中的<base>标签的href属性的值来解析codebase属性的相对 URL。如果 HTML 文档中没有指定<base>标签,则使用下载 HTML 文档的 URL 来解析相对的codebase URL。我们来看一些例子。
例 1
一个helloapplet.html文件的内容如下。注意,您包含了一个<base>标记,并且没有为<applet>标记指定codebase属性。
<html>
<head>
<title>Hello Applet</title>
<base href="http://www.jdojo.com/myapplets/myclasses
</head>
<body>
<applet code="com.jdojo.applet.HelloApplet" width="150" height="100">
This browser does not support Applets.
</applet>
</body>
</html>
使用 URL http://www.jdojo.com/myapplets/helloapplet.html 下载文档。既然你已经指定了<base>标签,浏览器将在 http://www.jdojo.com/myapplets/myclasses/com/jdojo/applet/HelloApplet.class 寻找 applet 的类文件。
示例 2
一个helloapplet.html文件的内容如下。注意,您包括了<base>标签,并且没有将<applet>标签的codebase属性指定为mydir。
<html>
<head>
<title>Hello Applet</title>
<base href="http://www.jdojo.com/myapplets/myclasses
</head>
<body>
<applet code="com.jdojo.applet.HelloApplet" width="150" height="100"
codebase="mydir">
This browser does not support Applets.
</applet>
</body>
</html>
使用 URL http://www.jdojo.com/myapplets/helloapplet.html 下载文档。既然你已经指定了<base>标签,浏览器将在 http://www.jdojo.com/myapplets/myclasses/mydir/com/jdojo/applet/HelloApplet.class 寻找 applet 的类文件。注意,mydir的codebase值是使用<base>标签的href值解析的。如果您将codebase值指定为../xyzdir(两个点表示向上一个目录),浏览器将在 http://www.jdojo.com/myapplets/xyzdir/com/jdojo/applet/HelloApplet.class 处查找类文件。注意,出于安全原因,有些浏览器不允许您指定两个点来表示目录层次结构中的上一级,作为codebase URL 的一部分。
例 3
一个helloapplet.html文件的内容如下。请注意,您没有包含<base>标签,而是为<applet>标签指定了codebase属性。
<html>
<head>
<title>Hello Applet</title>
</head>
<body>
<applet code="com.jdojo.applet.HelloApplet"
width="150" height="100" codebase="abcdir">
This browser does not support Applets.
</applet>
</body>
</html>
使用 URL http://www.jdojo.com/myapplets/helloapplet.html 下载文档。由于您没有指定<base>标签,代码库的相对 URL 将使用用于下载 HTML 文件的 URL 进行解析,浏览器将在 http://www.jdojo.com/myapplets/abcdir/com/jdojo/applet/HelloApplet.class 处查找类文件。
如果您为codebase使用绝对 URL,浏览器将使用该绝对 URL 查找 applet 的类文件,而不管 HTML 文件中是否存在标签,也不管 HTML 文件是从哪里下载的。让我们考虑下面的<applet>标签:
<applet code="com.jdojo.applet.HelloApplet" width="150" height="100"
codebase="http://www.jdojo.com/myclasses
This browser does not support Applets.
</applet>
浏览器会在 http://www.jdojo.com/myclasses/com/jdojo/applet/HelloApplet.class 寻找 Applet 的类文件。如果想将 applet 的类文件和 HTML 文件存储在不同的服务器上,需要将codebase值指定为绝对 URL。
Applet类提供了两个名为getDocumentBase()和getCodeBase()的方法来分别获取文档基 URL 和代码基 URL。getDocumentBase()方法返回嵌入了<applet>标签的文档的 URL。例如,如果您在浏览器中输入 URL http://www.jdojo.com/myapplets/helloapplet.html 来查看 Applet,那么getDocumentBase()方法将返回 http://www.jdojo.com/myapplets/helloapplet.html 。getCodeBase()方法返回用于下载 applet 的 Java 类的目录的 URL。从这个方法返回的 URL 取决于许多因素,正如您刚才在示例中看到的那样。
Applet 的生命周期
applet 在其存在期间会经历不同的阶段。它被创建、初始化、启动、停止和销毁。applet 首先通过调用其构造函数来创建。在创建它的时候,它是一个简单的 Java 对象,并没有获得它的“applet”状态。在创建之后,它获得它的 applet 状态,并且在Applet类中有四个方法被浏览器调用。您可以在这些方法中放置代码来执行不同种类的逻辑。这些方法如下:
init()start()stop()destroy()
init()方法
在 applet 被实例化和加载后,浏览器调用该方法。您可以重写此方法来放置为您的 applet 执行初始化逻辑的任何代码。通常,您将在这个方法中放置代码来为您的 applet 创建 GUI。这个方法在 applet 的生命周期中只被调用一次。
start()方法
紧接在init()方法之后调用start()方法。它可能被多次调用。假设您正在查看网页中的 applet,并且您通过替换 applet 的网页在同一浏览器窗口(或选项卡)中打开了另一个网页。如果你返回到前一个网页,如果 Applet 被缓存,它的start()方法将被再次调用。如果当你用另一个网页替换 Applet 的网页时,Applet 被破坏了,它的生命周期将重新开始,它的init()和start()方法将被依次调用。您可以在此方法中放置任何启动进程的代码,例如当 applet 显示在网页上时的动画。
stop()方法
该方法是start()方法的对应物。它可能被多次调用。通常,当显示 applet 的网页被另一个网页替换时,会调用该函数。它也在调用destroy()方法之前被调用。通常,在这个方法中放置代码来停止任何进程,比如在start()方法中启动的动画。
destroy()方法
当 applet 被销毁时,调用该方法。您可以放置执行逻辑的代码来释放在 applet 生命周期中被占用的任何资源。总是在调用destroy()方法之前调用stop()方法。这个方法在 applet 的生命周期中只被调用一次。
清单 4-4 包含了一个 applet 的代码,当调用 applet 的init()、start()、stop()和destroy()方法时,它会显示一个对话框。它包括消息中调用start()和stop()方法的次数。
清单 4-4。演示 Applet 生命周期的 Applet
// AppletLifeCycle.java
package com.jdojo.applet;
import javax.swing.JApplet;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
public class AppletLifeCycle extends {
private int startCount = 0;
private int stopCount = 0;
@Override
public void init() {
this.getContentPane().add(new JLabel("Applet Life Cycle!!!"));
JOptionPane.showMessageDialog(null, "init()");
}
@Override
public void start() {
startCount++;
JOptionPane.showMessageDialog(null, "start(): " + startCount);
}
@Override
public void stop() {
stopCount++;
JOptionPane.showMessageDialog(null, "stop(): " + stopCount);
}
@Override
public void destroy() {
JOptionPane.showMessageDialog(null, "destroy()");
}
}
清单 4-5 包含了查看AppletLifeCycleApplet 的 HTML 文件的内容。它假设 HTML 文件和 Java 类文件将放在如下所示的目录结构中:
ANY_DIR\appletlifecycle.html
ANY_DIR\com\jdojo\applet\AppletLifeCycle.class
如果您有不同的目录结构,您可能需要在一个<applet>标签中包含codebase属性。您可以使用前面描述的步骤查看 Applet。
清单 4-5。查看 AppletLifeCycle Applet 的 AppletLifeCycle 文件的内容
<html>
<head>
<title>Lifecycle of an Applet</title>
</head>
<body>
<applet code="com.jdojo.applet.AppletLifeCycle"
height="200" width="200">
This browser does not support Applets.
</applet>
</body>
</html>
将参数传递给 Applet
您可以让 Applet 的用户通过在 HTML 文档中向它传递参数来配置 Applet。您可以使用<applet>标签中的<param>标签向 applet 传递参数。<param>标签有两个属性叫做name和value。<param>标签的name和value属性分别用于指定参数的名称和值。您可以使用多个<param>标签向 applet 传递多个参数。下面的 HTML 代码片段向 applet 传递两个参数:
<applet code="MyApplet" width="100" height="100">
<param name="buttonHeight" value="20" />
<param name="buttonText" value="Hello" />
</applet>
参数名为buttonHeight和buttonText,其值分别为20和Hello。确保 applet 参数的名称有意义,对阅读它们的用户有意义。从技术上讲,任何字符串都可以作为参数名。比如,从技术上来说,p1和p2是和buttonHeight和buttonText一样好的参数名。然而,后者对用户更有意义。
Applet类提供了一个方法,该方法接受参数名作为其参数,并将参数值作为String返回。注意,不管参数的值是多少,它总是返回一个String。例如,如果您想将参数buttonHeight的值20作为一个整数,您需要在 applet 的 Java 代码中将String转换成一个整数。传递给getParameter()方法的参数名称不区分大小写;getParameter("buttonHeight")和getParameter("BUTTONHEIGHT")都返回与String相同的20值。如果 HTML 文档中没有设置指定的参数,getParameter()方法返回null。以下代码片段演示了如何在 applet 代码中使用getParameter()方法:
// buttonHeight and buttonText will get the values 20 and Hello
String buttonHeight = getParameter("buttonHeight");
String buttonText = getParameter("buttonText") ;
// bgColor will be null as there is no backgroundColor parameter set
String bgColor = getParameter("backgroundColor");
您可以使用参数自定义 applet 的一些方面。如果参数值发生变化,您不必更改代码。如果您向 applet 传递参数,请确保为每个参数分配一个默认值,以防在 HTML 文档中没有设置该值。例如,您可以将 applet 的背景颜色设置为 applet 的参数。如果没有设置,可以默认为灰色或白色。
清单 4-6 显示了一个AppletParameters applet 的代码。它使用两个 GUI 组件,一个显示欢迎消息的JTextArea和一个JButton。欢迎消息和按钮的文本可以通过两个名为welcomeText和helloButtonText的参数定制。applet 代码读取其init()方法中的两个参数值。如果 HTML 文档中没有设置参数的默认值,它将设置这些参数的默认值。清单 4-7 包含了 HTML 文件的内容,图 4-7 显示了 Applet 的运行。图 4-8 显示了当您点击 Say Hello 按钮时显示的消息框。
清单 4-6。使用标签向 Applet 传递参数
// AppletParameters.java
package com.jdojo.applet;
import java.awt.Container;
import java.awt.FlowLayout;
import javax.swing.JApplet;
import javax.swing.JButton;
import javax.swing.JOptionPane;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
public class AppletParameters extends JApplet {
private JTextArea welcomeTextArea = new JTextArea(2, 20);
private JButton helloButton = new JButton();
@Override
public void init() {
Container contentPane = this.getContentPane();
contentPane.setLayout(new FlowLayout());
contentPane.add(new JScrollPane(welcomeTextArea));
contentPane.add(helloButton);
// Show parameters when the button is clicked
helloButton.addActionListener(e -> showParameters());
// Make the welcome JTextArea non-editable
welcomeTextArea.setEditable(false);
// Display the welcome message
String welcomeMsg = this.getParameter("welcomeText");
if (welcomeMsg == null || welcomeMsg.equals("")) {
welcomeMsg = "Welcome!";
}
welcomeTextArea.setText(welcomeMsg);
// Set the hello button text
String helloButtonText = this.getParameter("helloButtonText");
if (helloButtonText == null || helloButtonText.equals("")) {
helloButtonText = "Hello";
}
helloButton.setText(helloButtonText);
}
private void showParameters() {
String welcomeText = this.getParameter("welcomeText");
String helloButtonText = this.getParameter("helloButtonText");
String msg = "Parameters passed from HTML are\nwelcomeText="
+ welcomeText + "\nhelloButtonText=" + helloButtonText;
JOptionPane.showMessageDialog(null, msg);
}
}
清单 4-7。用于查看 Applet 参数 Applet 的 AppletParameters 文件的内容
<html>
<head>
<title>Applet Parameters</title>
</head>
<body>
<applet code="com.jdojo.applet.AppletParameters"
width="300" height="50">
<param name="welcomeText"
value="Welcome to the applet world!"/>
<param name="helloButtonText"
value="Say Hello"/>
This browser does not support Applets.
</applet>
</body>
</html>

图 4-8。
The AppletParameters applet running in a browser

图 4-7。
The AppletParameters applet running in a browser Tip
您还可以使用Applet类的方法来获取标签的属性值。例如,您可以使用 getParameter("code")来获取
发布 Applet 的参数信息
applet 允许您发布关于它所接受的参数的信息。您可以开发一个知道其参数的 applet。您的 Applet 可能会被其他用户使用不同的 Applet 查看器查看。发布您的 applet 接受的参数可能对托管 applet 的程序和查看它的用户有所帮助。例如,applet 查看器可以让用户交互地改变 applet 的参数并重新加载 applet。Applet类提供了一个方法,您需要在 applet 类中覆盖它来发布关于 applet 参数的信息。它返回一个二维(nX3)数组String。默认情况下,它返回null。数组的行数应该等于它接受的参数数。每行应该有三列,包含参数的名称、类型和描述。在你的 Applet 中实现getParameterInfo()方法并不是你的 Applet 工作的必要条件。但是,通过这种方法提供关于 applet 参数的信息是一种很好的做法。让我们假设下面的<applet>标签用于显示您的 applet:
<applet code="MyApplet" width="100" height="100">
<param name="buttonHeight" value="20" />
<param name="buttonText" value="Hello" />
</applet>
MyApplet类方法的一个可能实现如下。注意,作为开发者,你只是 Applet 参数信息的发布者。这取决于 applet 浏览器程序以他们选择的任何方式使用它。
public class MyApplet extends JApplet {
// Other code for applet goes here...
// Public applet's parameter info
public String[][] getParameterInfo() {
String[][] parametersInfo =
{ {"buttonHeight",
"integer",
"Height for the Hello button in pixel"
},
{"buttonText",
"String",
"Hello button's text"
}
};
return parametersInfo;
}
}
发布 Applet 的信息
Applet类提供了一个应该返回 applet 文本描述的方法。该方法的默认实现返回null。从这个方法返回 applet 的简短描述是一个很好的实践,这样 applet 的用户可以对 applet 有更多的了解。该描述可以由用于查看 Applet 的工具以某种方式显示。下面的代码片段说明了如何使用getAppletInfo()方法来提供关于 applet 的信息:
public class MyApplet extends JApplet {
// Other applet's logic goes here...
public String getAppletInfo() {
return "My Demo Applet, Version 1.0, No Copyright";
}
}
表 4-1 列出了<applet>标签的所有属性及其用法。除了这个表中列出的属性,你还可以使用其他一些标准的 HTML 属性,比如id、style等。带着<applet>标签。
表 4-1。
The List of Attributes for the
