大多数Swing应用都被构建在基础的JFrame内部,JFrame在你使用的任何操作系统中都可以创建视窗应用。视窗的标题可以像下面这样使用JFrame的构造器来设置:
package gui;import javax.swing.JFrame;public class HelloSwing {public static void main(String[] args) {JFrame frame = new JFrame("Hello Swing");frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);frame.setSize(300, 100);frame.setVisible(true);}
}
setDefaultCloseOperation()告诉JFrame当用户执行关闭操作时应该做些什么。EXIT_ON_CLOSE常量告诉它要退出程序。如果没有这个调用,默认的行为是什么也不做,因此应用将不会关闭。setSize()以像素为单位设置视窗的尺寸。请注意最后一行:
frame.setVisible(true);
如果没有这行,你在屏幕上将什么也看不到。
我们可以通过在JFrame中添加一个JLabel来使事情变得更有趣一些:
package gui;import java.util.concurrent.TimeUnit;import javax.swing.JFrame;
import javax.swing.JLabel;public class HelloLabel {public static void main(String[] args) throws InterruptedException {JFrame frame = new JFrame("Hello Swing");JLabel label = new JLabel("A Label");frame.add(label);frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);frame.setSize(300, 100);frame.setVisible(true);TimeUnit.SECONDS.sleep(1);label.setText("Hey! This is Different!");}
}
在一秒钟之后,JLabel的文本发生了变化。尽管这对于这个小程序来说既有趣又安全,但是对于main()线程来说,直接对GUI组件编写代码并非是一种好的想法。Swing有它自己的专用线程来接受UI事件并更新屏幕,如果你从其他线程着手对屏幕进行操作,那么就可能会产生“并发”章中所描述的冲突和死锁。
取而代之的是,其他线程,例如这里是像main()这样的线程,应该通过Swing事件分发线程提交要执行的任务。你可以通过将任务提交给SwingUtilities.invokeLater()来实现这种方式,这个方法会通过事件分发线程将任务放置到(最终将得到执行的)待执行事件队列中。如果我们将这种方式应用于上面的示例,那么它就会变成下面的样子:
package gui;import java.util.concurrent.TimeUnit;import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.SwingUtilities;public class SubmitLabelManipulationTask {public static void main(String[] args) throws InterruptedException {JFrame frame = new JFrame("Hello Swing");final JLabel label = new JLabel("A Label");frame.add(label);frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);frame.setSize(300, 100);frame.setVisible(true);TimeUnit.SECONDS.sleep(1);SwingUtilities.invokeLater(() -> {label.setText("Hey! This is Different!");});}
}
现在你再也不用直接操作JLabel了。取而代之的是,你提交一个Runnable,当事件分发线程在事件队列中获取这项任务时,它将执行实际的操作。并且在执行这个Runnable时,不会做其他任何事情,因此也就不会产生任何冲突,当然,前提是程序中的所有代码都遵循这种通过SwingUtilities.invokeLater()来提交操作的方式。这包括启动程序自身,即main()也不应该调用Swing的方法,就像上面的程序一样,它应该向事件队列提交任务。因此,所编写的恰当的程序看起来应该是下面的样子:
package gui;import java.util.concurrent.TimeUnit;import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.SwingUtilities;public class SubmitSwingProgram extends JFrame {JLabel label;public SubmitSwingProgram() {super("Hello Swing");label = new JLabel("A Label");add(label);setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);setSize(300, 100);setVisible(true);}static SubmitSwingProgram ssp;public static void main(String[] args) throws InterruptedException {SwingUtilities.invokeLater(() -> {ssp = new SubmitSwingProgram();});TimeUnit.SECONDS.sleep(1);SwingUtilities.invokeLater(() -> {ssp.label.setText("Hey! This is Different!");});}
}
注意,对sleep()的调用不在构造器的内部。如果你将它放在构造器内部,JLabel的初始文本就永远都不会出现。这要是因为构造器在sleep()调用完毕和新的标签插入之前不会结束,如果sleep()在构造器的内部,或者在任何UI操作的内部,那么就意味着你在sleep()期间将中止事件分发线程,这通常是个糟糕的主意。
一、一个显示框架
我们可以创建一个显示框架,将其用于本章剩余部分的Swing示例中,从而使得上面的想法得以结合,并减少冗余代码:
package com.buba.util;import javax.swing.JFrame;
import javax.swing.SwingUtilities;public class SwingConsole {public static void run(final JFrame f, final int width, final int height) {SwingUtilities.invokeLater(() -> {f.setTitle(f.getClass().getSimpleName());f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);f.setSize(width, height);f.setVisible(true);});}
}
这可能是一个你想要自己使用的工具,因此它被放到util类库中。要想使用它,你的应用就必须位于一个JFrame中。静态的run()方法可以将视窗的标题设置为类的简单名。
二、创建按钮
创建一个按钮非常简单:只要用你希望出现在按钮上的标签调用JButton的构造器即可。在后面你会看到一些更有趣的功能,比如在按钮上显示图形。
一般来说,要在类中为按钮创建一个字段,以便以后可以引用这个按钮。
JButton是一个组件,它有自己的小窗口,能作为整个更新过程的一部分而自动被重绘。也就是说,你不必显式绘制一个按钮或者别的类型的控件;只要把它们放在窗体上,它们可以自动绘制自己。通常你会在构造器内部把按钮加入窗体:
package gui;import java.awt.FlowLayout;import javax.swing.JButton;
import javax.swing.JFrame;import com.buba.util.SwingConsole;public class Button1 extends JFrame {private JButton b1 = new JButton("Button 1"), b2 = new JButton("Button 2");public Button1() {setLayout(new FlowLayout());add(b1);add(b2);}public static void main(String[] args) {SwingConsole.run(new Button1(), 200, 100);}
}
这里引入了一些新内容:在向JFrame添加任何组件之前,先给出一个新的FlowLayout类型的“布局管理器”。布局管理器是面板用来隐式地决定控件在窗体上位置的工具。JFrame通常使用BorderLayout管理布局,但这里不能使用(在本章后面部分将学习它),因为它的默认行为是每加入一个控件,将完全覆盖其他控件。FlowLayout使得控件可以在窗体上从左到右、从上到下连续均匀分布。
三、捕获事件
如果编译并运行前面的程序。那么当按下按钮的时候,什么也不会发生。现在是必须深入进去编写一些代码以决定会发生什么事情的时候了。事件驱动编程(包含了许多关于GUI的内部)的基础,就是把事件同处理事件的代码连接起来。
在Swing中,这种关联的方式就是通过清楚地分离接口(图形组件)和实现(当和组件相关的事件发生时,你要执行的代码)而做到的。每个Swing组件都能够报告其上所有可能发生的事件,并且它能单独报告每种事件。所以,你要是对诸如“鼠标移动到按钮上”这样的事件不感兴趣的话,那么你不注册这样的事件就可以了。这种处理事件驱动编程的方式非常直接和优雅,一旦你理解了其基本概念,就能够很容易将其应用到甚至从未见过的Swing组件之上。实际上,只要是JavaBean(本章后面讨论),这个模式都适用。
首先,对所使用的组件,我们只把重点放在它感兴趣的主要事件上。对于JButton。“感兴趣的事件”就是按钮被按下。为了表明(注册)你对按钮按下事件感兴趣,可以调用JButton的addActionListener()方法。这个方法接受一个实现ActionListener接口的对象作为参数,ActionListener接口只包含一个actionPerformed()方法。所以要想把事件处理代码和JButton关联,需要在一个类中实现ActionListener接口,然后把这个类的对象通过addActionListener()方法注册给JButton。这样按钮按下的时候就会调用actionPerformed()方法(通常这也称为回调)。
但是按钮按下的时候应该有什么结果呢?我们希望看到屏幕有所改变,所以这里介绍一个新的Swing组件——JTextField。这个组件支持用户输入文本,在本例中,或者像本例一样由程序插入文本。尽管有很多方法可以创建JTextField,但是最简单的方式就是告诉构造器你所希望的文本域宽度。一旦JTextField被放置到窗体上,就可以使用setText()方法来修改它的内容(JTextField还有很多方法,不过你应该先看一下JDK文档)。下面就是其具体程序:
package gui;import java.awt.FlowLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JTextField;import com.buba.util.SwingConsole;public class Button2 extends JFrame {private JButton b1 = new JButton("Button 1"), b2 = new JButton("Button 2");private JTextField txt = new JTextField(10);class ButtonListener implements ActionListener {@Overridepublic void actionPerformed(ActionEvent e) {String name = ((JButton) e.getSource()).getText();txt.setText(name);}}private ButtonListener bl = new ButtonListener();public Button2() {b1.addActionListener(bl);b2.addActionListener(bl);setLayout(new FlowLayout());add(b1);add(b2);add(txt);}public static void main(String[] args) {SwingConsole.run(new Button2(), 200, 150);}
}
创建JTextField并把它放置在画布上的步骤,同JButton或者其他Swing组件所采用的步骤相同。这里与前面例子的不同之处在于创建一个ButtonListener对象,它实现了前面提到过的ActionListener接口。actionPerformed()方法的参数是ActionEvent类型,它包含事件和事件源的所有信息。本例中,我希望表明是哪个按钮被按下;getSource()方法产生的对象表明了事件的来源,我假设(使用类型转换)这个对象是JButton,getText()方法返回按钮上的文本,这个文本被放进JTextField,以证明当按钮按下的时候代码确实被调用了。
在构造器中,使用addActionListener()方法来将ButtonListener对象注册给两个按钮。
通常,把ActionListener实现成匿名内部类会更方便,尤其是对每个监听器类只使用一个实例的时候更是如此。可以像下面这样修改Button2.java,这里使用一个匿名内部类:
package gui;import java.awt.FlowLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JTextField;import com.buba.util.SwingConsole;public class Button2b extends JFrame {private JButton b1 = new JButton("Button 1"), b2 = new JButton("Button 2");private JTextField txt = new JTextField(10);private ActionListener bl = new ActionListener() {@Overridepublic void actionPerformed(ActionEvent e) {String name = ((JButton) e.getSource()).getText();txt.setText(name);}};public Button2b() {b1.addActionListener(bl);b2.addActionListener(bl);setLayout(new FlowLayout());add(b1);add(b2);add(txt);}public static void main(String[] args) {SwingConsole.run(new Button2b(), 200, 150);}
}
本章中的例子倾向于(只要可能)使用匿名内部类的方式。
四、文本区域
除了可以有多行本文以及更多的功能不同之外,JTextArea与JTextField在其他方面都很相似。JTextArea有一个比较常用的方法append()。因为可以往回滚动,所以比起在命令行程序中把文本打印到标准输出的做法,这就成为了一种进步。例如,下面的程序使用“容器深入研究”章节中的Countries生成器的输出来填充JTextArea。
package gui;import java.awt.FlowLayout;
import java.util.HashMap;
import java.util.Map;import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;import com.buba.util.Countries;
import com.buba.util.SwingConsole;public class TextArea extends JFrame {private JButton b = new JButton("Add Data"), c = new JButton("Clear Data");private JTextArea t = new JTextArea(20, 40);private Map<String, String> m = new HashMap<>();public TextArea() {// 用完所有的数据m.putAll(Countries.capitals());b.addActionListener(e -> {for (Map.Entry me : m.entrySet()) {t.append(me.getKey() + ": " + me.getValue() + "\n");}});c.addActionListener(e -> {t.setText("");});setLayout(new FlowLayout());add(new JScrollPane(t));add(b);add(c);}public static void main(String[] args) {SwingConsole.run(new TextArea(), 475, 425);}
}
在构造器中,用国家及其首都名称来填充Map。注意,对于其中的两个按钮,因为在程序中你不再需要引用监听器,所以直接创建ActionListener对象并添加,而没有定义中间变量。“Add Data”按钮格式化并添加所有数据,“Clear Data”按钮使用setText()方法来清理JTextArea中的所有文本。
在JTextArea被添加到JFrame中之前,先被包装进了JScrollPane(当屏幕的文本太多的时候用它来进行滚动控制)。这么做就足以得到完整的滚动功能。由于我曾试图在其他GUI编程环境中得到类似功能,所以我对像JScrollPane这样设计良好、使用简单的组件印象非常深刻。
如果本文对您有很大的帮助,还请点赞关注一下。