Java8 API 入门手册(三)
三、高级 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 中,您并不局限于只有矩形窗口。它可以让你创建异形窗口。异形窗可以是任何形状,例如圆形、椭圆形或任何定制的形状。