基础控件
这篇笔记我们讨论一些Swing中最基础的控件用法,它们涉及用户交互和基础的内容展示,这些都是构成GUI程序的基本元素。
JPanel容器
如果你对网页中使用的HTML语言比较熟悉,那么你就能够轻松理解JPanel有什么用。JPanel是一个“容器”,用于组织和管理控件的布局,这和HTML中的<div>类似,HTML中我们可以通过嵌套<div>标签并对其设置适当的布局CSS属性,最终一系列的标签和组件整合形成完整的Web页面,Swing也是类似的,我们的整个GUI界面其实也是由大量JPanel嵌套配合布局管理器实现的,这最终会形成一个树形结构,我们通常称之为GUI界面的“控件树”,树中的根节点通常是JFrame,叶子节点是各种具体功能的控件如按钮等,而那些中间节点其实就是JPanel。
下面是一个简单的例子,它是一个“计算器”,其中JPanel(1)是计算器主要操作区域的容器,JPanel(2)中包含了计算器中的各种按钮,JPanel(3)是计算器底部的状态栏。
JFrame
|_ JPanel(1)
|_ JTextField
|_ JPanel(2)
|_ JButton
|_ JButton
|_ ...
|_ JPanel(3)
|_ JLabel
对应的布局代码如下。
package com.gacfox.demo;
import java.awt.BorderLayout;
import java.awt.FlowLayout;
import java.awt.GridLayout;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextField;
public class MainFrame extends JFrame {
public MainFrame() {
setLayout(new BorderLayout());
JPanel pMain = new JPanel(new BorderLayout());
JTextField tfCalcDisplay = new JTextField();
pMain.add(tfCalcDisplay, BorderLayout.NORTH);
JPanel pButtons = new JPanel(new GridLayout(4, 3));
JButton btn1 = new JButton("1");
JButton btn2 = new JButton("2");
JButton btn3 = new JButton("3");
JButton btn4 = new JButton("4");
JButton btn5 = new JButton("5");
JButton btn6 = new JButton("6");
JButton btn7 = new JButton("7");
JButton btn8 = new JButton("8");
JButton btn9 = new JButton("9");
JButton btnPlus = new JButton("+");
JButton btn0 = new JButton("0");
JButton btnEqual = new JButton("=");
pButtons.add(btn1);
pButtons.add(btn2);
pButtons.add(btn3);
pButtons.add(btn4);
pButtons.add(btn5);
pButtons.add(btn6);
pButtons.add(btn7);
pButtons.add(btn8);
pButtons.add(btn9);
pButtons.add(btnPlus);
pButtons.add(btn0);
pButtons.add(btnEqual);
pMain.add(pButtons, BorderLayout.CENTER);
JPanel pStatus = new JPanel(new FlowLayout(FlowLayout.LEFT));
JLabel lStatus = new JLabel("就绪");
pStatus.add(lStatus);
add(pMain, BorderLayout.CENTER);
add(pStatus, BorderLayout.SOUTH);
pack();
setLocationByPlatform(true);
setTitle("Calculator");
}
}
代码虽然长,但实际上其布局结构是简单且容易理解的,它和前面我们规划的页面布局结构“控件树”完全一致。我们创建的JPanel都被设置了布局管理器,以便合适的在其内部放置控件,此外我们也可以为JPanel设置背景色、边框等属性,这里就不逐一演示了,具体参考JDK的标准文档即可。注意代码的最后我们调用了JFrame的pack()方法,它用于自动根据内部控件的情况计算窗体大小,如果我们手动设置了窗体大小,则不必调用该方法。上面程序运行后,显示效果如下。

不过实际开发中,我们的界面可能远比上面复杂,在MainFrame类中初始化整个控件树通常都是不可能的,此时我们可以像继承JFrame一样继承JPanel,并一层层的组合整个GUI界面。
注:上面代码中,对于GUI控件对象变量的命名方式其实类似匈牙利命名法,它使用控件的类型缩写加变量名命名,例如用于展示计算内容的JTextField被命名为tfCalcDisplay(由于Swing中的控件类都以J开头因此省略了这个字母),这种命名法惯用于GUI编程,这可能继承自Win32API的风格,总而言之这和标准的驼峰命名法有些区别,它在许多负责图形界面开发的软件工程师中流行,但你不必一定遵循这个规则。
JLabel显示内容
Swing中,JLabel是个强大且常用的组件,它能够显示纯文本、富文本和图像。
显示纯文本
下面例子代码中,我们使用JLabel显示纯文本,它将Hello, Swing!打印在JLabel控件的中间。
package com.gacfox.demo;
import javax.swing.JFrame;
import javax.swing.JLabel;
public class MainFrame extends JFrame {
public MainFrame() {
JLabel lMain = new JLabel("Hello, Swing!", JLabel.CENTER);
add(lMain);
setSize(300, 200);
setLocationByPlatform(true);
setTitle("Hello, Swing!");
}
}
JLabel.CENTER指定了内容显示的对齐方式,默认的水平靠左的。

显示HTML富文本
JLabel支持HTML4.01富文本,但其支持相对有限,仅限于几个主要的HTML标签,JLabel不支持CSS和过于复杂的HTML结构。下面代码中,我们让Swing!字符串变为蓝色且加粗。
package com.gacfox.demo;
import javax.swing.JFrame;
import javax.swing.JLabel;
public class MainFrame extends JFrame {
public MainFrame() {
JLabel lMain = new JLabel("<html>Hello, <font size='5' color='blue'>Swing!</font></html>", JLabel.CENTER);
add(lMain);
setSize(300, 200);
setLocationByPlatform(true);
setTitle("Hello, Swing!");
}
}
注意,如果需要使用JLabel展示富文本,<html></html>是必要的。

显示图像
JLabel支持ImageIcon类型作为参数并显示其中的图片,它支持主流图片格式以及GIF动画(但无法控制播放帧率等)。下面例子使用JLabel显示了一个PNG图像。
package com.gacfox.demo;
import java.net.URL;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
public class MainFrame extends JFrame {
public MainFrame() {
URL iconUrl = getClass().getResource("/icon.png");
ImageIcon icon = new ImageIcon(iconUrl);
JLabel lMain = new JLabel(icon, JLabel.CENTER);
add(lMain);
setSize(300, 200);
setLocationByPlatform(true);
setTitle("Hello, Swing!");
}
}

虽然JLabel能够方便的显示图像,但注意如果我们要实现类似“Windows图片查看器”的程序时不建议用JLabel,因为它缺乏一些必要的灵活性,这类程序建议使用Java2D API来绘制图像,使用Java2D我们能更轻松的实现图片的旋转、缩放甚至涉及图像处理算法的滤镜等操作。
JButton按钮
创建按钮并设置大小
JButton用于创建按钮,我们可以创建文本按钮、图片按钮和文本图片结合的按钮,下面例子代码我们分别创建了这3种按钮。
package com.gacfox.demo;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.net.URL;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
public class MainFrame extends JFrame {
public MainFrame() {
URL iconUrl = getClass().getResource("/icon_sm.png");
ImageIcon icon = new ImageIcon(iconUrl);
JPanel pMain = new JPanel(new FlowLayout());
JButton btnText = new JButton("Click Me!");
btnText.setPreferredSize(new Dimension(85, 28));
JButton btnIcon = new JButton(icon);
btnIcon.setPreferredSize(new Dimension(85, 28));
JButton btnTextAndIcon = new JButton("Click Me!", icon);
btnTextAndIcon.setPreferredSize(new Dimension(125, 28));
pMain.add(btnText);
pMain.add(btnIcon);
pMain.add(btnTextAndIcon);
add(pMain);
setSize(300, 200);
setLocationByPlatform(true);
setTitle("Hello, Swing!");
}
}
代码中,3个按钮被放置在一个JPanel容器中,为了保证按钮的整齐,我们这里还手动设置了按钮的大小。

设置按钮状态
我们有时需要将按钮禁用,启用和禁用状态可以调用setEnabled()方法实现,下面是一个例子。
package com.gacfox.demo;
import java.awt.FlowLayout;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
public class MainFrame extends JFrame {
public MainFrame() {
JPanel pMain = new JPanel(new FlowLayout());
JButton btnText = new JButton("Click Me!");
btnText.setEnabled(false);
pMain.add(btnText);
add(pMain);
setSize(300, 200);
setLocationByPlatform(true);
setTitle("Hello, Swing!");
}
}
被禁用的按钮会变成灰色且不可点击。

处理按钮点击事件
我们可以调用按钮的addActionListener()方法为其绑定点击事件的回调函数,下面是一个例子。
package com.gacfox.demo;
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.JPanel;
public class MainFrame extends JFrame {
public MainFrame() {
JPanel pMain = new JPanel(new FlowLayout());
JButton btnText = new JButton("Click Me!");
btnText.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.exit(0);
}
});
pMain.add(btnText);
add(pMain);
setSize(300, 200);
setLocationByPlatform(true);
setTitle("Hello, Swing!");
}
}
代码中,点击按钮后会退出程序。有关事件机制将在后续章节详细介绍。
菜单
窗体菜单栏
Swing中窗体菜单栏由JMenuBar、JMenu、JMenuItem这3个类封装,下面例子为JFrame设置菜单栏。
package com.gacfox.demo;
import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
public class MainFrame extends JFrame {
public MainFrame() {
JMenuBar mbMain = new JMenuBar();
JMenu mFile = new JMenu("文件");
JMenuItem miOpen = new JMenuItem("打开");
mFile.add(miOpen);
JMenuItem miSave = new JMenuItem("保存");
mFile.add(miSave);
mFile.addSeparator();
JMenuItem miExit = new JMenuItem("退出");
mFile.add(miExit);
mbMain.add(mFile);
setJMenuBar(mbMain);
setSize(300, 200);
setTitle("Hello, Swing!");
}
}
代码中,我们设置了窗体菜单文件,它包含3个子菜单,分别是打开、保存和退出,在退出菜单的上方我们还加入了一个分隔线。

上下文菜单
上下文菜单俗称“右键菜单”,Swing需要使用JPopupMenu实现。
package com.gacfox.demo;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import javax.swing.JFrame;
import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;
public class MainFrame extends JFrame {
public MainFrame() {
final JPopupMenu pmMain = new JPopupMenu();
JMenuItem miOpen = new JMenuItem("打开");
JMenuItem miSave = new JMenuItem("保存");
JMenuItem miExit = new JMenuItem("退出");
pmMain.add(miOpen);
pmMain.add(miSave);
pmMain.add(miExit);
addMouseListener(new MouseAdapter() {
@Override
public void mouseReleased(MouseEvent e) {
if (e.isPopupTrigger()) {
pmMain.show(e.getComponent(), e.getX(), e.getY());
}
}
});
setSize(300, 200);
setTitle("Hello, Swing!");
}
}
代码中创建JPopupMenu的方式和窗体菜单是类似的,不过上下文菜单的触发比较特殊,它需要绑定到鼠标松开触发的mouseReleased事件上并使用事件对象的isPopupTrigger()方法判断是否是要弹出上下文菜单,当该事件触发时,我们将上下文菜单显示在当前触发组件的鼠标相对位置上。

动态更新控件树
和浏览器中的DHTML页面类似,Swing图形界面的控件树也不是静态的,我们其实可以动态修改图形界面的控件树,不过稍微复杂一点的是Swing中修改控件树涉及线程安全问题和手动触发的控件重绘。
package com.gacfox.demo;
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.JPanel;
import javax.swing.SwingUtilities;
public class MainFrame extends JFrame {
public MainFrame() {
final JPanel pMain = new JPanel(new FlowLayout());
JButton btnText = new JButton("Click Me!");
pMain.add(btnText);
btnText.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent arg0) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
JButton btnNew = new JButton("Click Me!");
pMain.add(btnNew);
pMain.revalidate();
pMain.repaint();
}
});
}
});
add(pMain);
setSize(300, 200);
setLocationByPlatform(true);
setTitle("Hello, Swing!");
}
}
上面代码中,我们实现每次点击按钮就在JPanel中新增一个按钮,注意我们对控件树的修改操作是放在事件分发线程中执行的。此外由于Swing是事件驱动绘制的GUI框架(与之相对的是游戏中常用的即时绘制imGUI),每次修改后我们都调用了pMain.revalidate()和pMain.repaint()触发了JPanel的重绘,否则可能修改不会及时显示出来。