这篇笔记我们讨论一些Swing中最基础的控件用法,它们涉及用户交互和基础的内容展示,这些都是构成GUI程序的基本元素。
如果你对网页中使用的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的风格,总而言之这和标准的驼峰命名法有些区别,它在许多负责图形界面开发的软件工程师中流行,但你不必一定遵循这个规则。
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
指定了内容显示的对齐方式,默认的水平靠左的。
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
用于创建按钮,我们可以创建文本按钮、图片按钮和文本图片结合的按钮,下面例子代码我们分别创建了这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
的重绘,否则可能修改不会及时显示出来。