使用Swing开发图形界面比AWT更加优秀,因为Swing是一种轻量级组件,它采用100%的Java实现,不再依赖于本地平台的图形界面,所以可以在所有平台上保持相同的运行效果,对跨平台支持比较出色。
除此之外,Swing提供了比AWT更多的图形界面组件,因此可以开发出更美观的图形界面。由于AWT需要调用底层平台的GUI实现,所以AWT只能使用各种平台上GUI组件的交集,这大大限制了AWT所支持的GUI组件。对Swing而言,几乎所有组件都采用纯Java实现,所以无需考虑是否支持该组件,因此Swing可以提供如JTabbedPane、JDesktopPane、JInternalFrame等特殊的容器,也可以提供像JTree、JTable、JSpinner、JSlider等特殊的GUI组件。
除此之外,Swing组件都采用MVC(Model-View-Controller,即模型-视图-控制器)设计模式,从而可以实现GUI组件的显示逻辑和数据逻辑的分离,允许程序员自定义Render来改变GUI组件的显示外观,提供更多的灵活性。
前一章已经介绍过了AWT和Swing的关系,因此不难知道:实际使用Java开发图形界面程序时,很少使用AWT组件,绝大部分时候都是用Swing组件开发的。Swing时由100%纯Java实现的,不再依赖于本地平台的GUI,因此可以再所有平台上都保持相同的界面外观。独立于本地平台的Swing组件被称为轻量级组件;而依赖于本地平台的AWT组件被称为重量级组件。
由于Swing的所有组件完全采用Java实现,不再调用本地平台的GUI,所以导致Swing图形界面显示速度要比AWT图形界面的显示速度要快一些,但对于快速发展的硬件设施二十,这种微小的速度差别无妨大碍。
使用Swing开发图形界面由如下几个优势。
对于一些简单的Swing组件通常无须关心它对应的Model对象,但对于一些高级的Swing组件,如JTree、JTabel等需要维护复杂的数据,这些数据就是由该组件对应的Model来维护的。另外,通过创建Model类的子类或通过实现适当的接口,可以为组件建立自己的模型,然后用setModel()方法把模型与组件关联起来。
Swing提供了多种独立于各种平台的LAF(Look And Feel),默认是一种名为Metal的LAF,这种LAF吸收了Macintosh平台的风格,因此显得比较漂亮。Java7则提供了一种名为Nimbus的LAF,这种LAF更加漂亮。
为了获取到当前JRE所支持的LAF,可以借助与UIManager的getInstalledLookAndFeels()方法,如下程序所示。
import javax.swing.*;
public class Demo{
public static void main(String[] args) {
System.out.println("当前系统可用的所有LAF:");
for(var info : UIManager.getInstalledLookAndFeels()){
System.out.println(info.getName()+"--->"+info);
}
}
}
"C:\Program Files\Java\jdk-11.0.11\bin\java.exe" -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:57332,suspend=y,server=n -javaagent:C:\Users\YueDie\AppData\Local\JetBrains\IntelliJIdea2020.1\captureAgent\debugger-agent.jar -Dfile.encoding=UTF-8 -classpath "D:\DemoProject\out\production\DemoProject;D:\IntelliJ IDEA 2020.1.1\lib\idea_rt.jar" Demo
Connected to the target VM, address: '127.0.0.1:57332', transport: 'socket'
当前系统可用的所有LAF:
Metal--->javax.swing.UIManager$LookAndFeelInfo[Metal javax.swing.plaf.metal.MetalLookAndFeel]
Nimbus--->javax.swing.UIManager$LookAndFeelInfo[Nimbus javax.swing.plaf.nimbus.NimbusLookAndFeel]
CDE/Motif--->javax.swing.UIManager$LookAndFeelInfo[CDE/Motif com.sun.java.swing.plaf.motif.MotifLookAndFeel]
Windows--->javax.swing.UIManager$LookAndFeelInfo[Windows com.sun.java.swing.plaf.windows.WindowsLookAndFeel]
Windows Classic--->javax.swing.UIManager$LookAndFeelInfo[Windows Classic com.sun.java.swing.plaf.windows.WindowsClassicLookAndFeel]
Disconnected from the target VM, address: '127.0.0.1:57332', transport: 'socket'
Process finished with exit code 0
除可以使用Java默认提供的数量不多的几种LAF之外,还由大量的Java爱好者提供了各种开源的LAF,有兴趣可以去自行下载、体验各种LAF,使用不同的LAF可以让Swing应用程序更加美观。
前面已经提到过,Swing为所有的AWT组件提供了对应的实现(除Canvas组件除外,因为在Swing中无须继承Canvas组件),通常在AWT组件的组件名前添加“J”就变成了对应的Swing组件。
大部分Swing组件都是JComponent抽象类的直接或间接子类(并不是全部的Swing组件),JComponent类定义了所有子类组件的通用方法,JComponent类是AWT里java.awt.Container类的子类,这也是AWT和Swing的联系之一。绝大部分Swing组件类继承了Container类,所以Swing组件都可作为容器使用(JFrame继承了Frame类),下图显示了Swing组件继承层次图。
上面JCeckBox和JCeckBoxMenuItem与Checkbox和CheckboxMenuItem的差别主要是由于早期Java命名不太规范造成的。
从上图可以看出,Swing中包含了4个组件直接继承了AWT组件,而不是从JComponent派生的,它们分别是:JFrame、JWindow、JDialog和JApplet,它们并不是轻量级组件,而是重量级组件(需要部分委托给运行平台上的GUI组件的对等体)。
将Swing组件按功能来分,又可分为如下几类。
下面将会依次详细介绍各种Swing组件的用法。
从上图可以看出,Swing为除Canvas之外的所有AWT组件提供了相应的实现,Swing组件比AWT组件的功能更为强大。相对于AWT组件,Swing组件具有如下4个额外的功能。
每个Swing组件都又一个对应的UI类,例如JButton组件就有一个对应的ButtonUI类来作为UI代理。每个Swing组件的UI代理的类名总是将该Swing组件类目的J去掉,然后再后面添加UI后缀。
UI代理通常式一个抽象基类,不同的PLAF会有不同的UI代理实现类。Swing类库中包含了几套UI代理,每套UI代理都几乎包含了所有Swing组件的ComponentUI实现,每套这样的实现都被称为一种PLAF实现。以JButton为例,其UI代理的继承层次如下图所示。
如果需要改变程序的外观风格,则可以使用如下代码。
try{
//设置使用Windows风格
UIManager.setLookAndFeel("com.sun.java.swing.plaf.windows.WindowLookAndFeel");
SwingUtilities.updateComponentTreeUI(f);
}catch (Exception e){
e.printStackTrace();
}
下main程序示范了使用Swing组件来创建窗口应用,该窗口里包含了菜单、右键菜单以及基本AWT组件的Swing实现。
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionListener;
import java.awt.event.InputEvent;
public class SwingComponent{
JFrame f = new JFrame("测试");
//这里我们定义按钮但是不指定图标因为文章的限制,加上icon的麻烦
JButton ok = new JButton("确认");
//定义一个单选按钮,初始处于选中状态
JRadioButton male = new JRadioButton("男",true);
//定义一个单选按钮,初始处于没有选中状态
JRadioButton female = new JRadioButton("女",false);
//定义一个ButtonGroup,用于将上面两个JRadioButton组合在一起
ButtonGroup bg = new ButtonGroup();
//定义一个复选框,初始处于没有选中状态。
JCheckBox married = new JCheckBox("是否已婚?",false);
String[]colors = new String[]{"红色","绿色","蓝色"};
//定义一个下拉选择框
JComboBox<String>colorChooser = new JComboBox<>(colors);
//定义一个列表选择框
JList<String>colorList = new JList<>(colors);
//定义一个8行、20列的多行文本域
JTextArea ta = new JTextArea(8,20);
//定义一个40列的单行问北路
JTextField name = new JTextField(40);
JMenuBar mb = new JMenuBar();
JMenu file = new JMenu("文件");
JMenu edit = new JMenu("编辑");
//创建“新建”菜单项
JMenuItem newItem = new JMenuItem("新建");
//创建“保存”菜单项
JMenuItem saveItem = new JMenuItem("保存");
//创建“退出”菜单项
JMenuItem exitItem = new JMenuItem("退出");
JCheckBoxMenuItem autoWrap = new JCheckBoxMenuItem("自动换行");
//创建“复制”菜单项
JMenuItem copyItem = new JMenuItem("复制");
//创建“粘贴”菜单项
JMenuItem pasteItem = new JMenuItem("粘贴");
JMenu format = new JMenu("格式");
JMenuItem commentItem = new JMenuItem("注释");
JMenuItem cancelItem = new JMenuItem("取消注释");
//定义一个右键菜单用于设置程序风格
JPopupMenu pop = new JPopupMenu();
//用于组合3个风格菜单项的ButtonGroup
ButtonGroup flavorGroup = new ButtonGroup();
JRadioButtonMenuItem metalItem = new JRadioButtonMenuItem("Metal风格",true);
JRadioButtonMenuItem nimbusItem = new JRadioButtonMenuItem("Nimbus风格");
JRadioButtonMenuItem windowsItem = new JRadioButtonMenuItem("Windows风格");
JRadioButtonMenuItem classicItem = new JRadioButtonMenuItem("Windows经典风格");
JRadioButtonMenuItem motifItem = new JRadioButtonMenuItem("Motif风格");
//------------------------------用于执行界面初始化的init方法------------------------------
public void init(){
//创建以一个装在了文本框、按钮的JPanel
var bottom = new JPanel();
bottom.add(name);
bottom.add(ok);
f.add(bottom, BorderLayout.SOUTH);
//创建一个装在了下拉框、三个JCheckBox的JPanel
var checkPanel = new JPanel();
checkPanel.add(colorChooser);
bg.add(male);
bg.add(female);
checkPanel.add(male);
checkPanel.add(female);
checkPanel.add(married);
//创建一个垂直排列组件的Box、盛装多行文本域JPanel
var topLeft = Box.createVerticalBox();
//使用JScrollPane作为普通组件的JViewPort
var taJsp = new JScrollPane(ta);
topLeft.add(taJsp);
topLeft.add(checkPanel);
//创建一个水平排列组件的Box,盛装topLeft、colorList
var top = Box.createHorizontalBox();
top.add(topLeft);
top.add(colorList);
//将top Box容器 添加到窗口的中间
f.add(top);
//----------下面开始组合菜单,并为菜单添加监听器----------
//为newItem设置快捷键,设置亏啊借鉴时要求使用大写字母
newItem.setAccelerator(KeyStroke.getKeyStroke('N', InputEvent.CTRL_DOWN_MASK));
newItem.addActionListener(e -> ta.append("用户单击了“新建”菜单\n"));
//为file菜单添加菜单项
file.add(newItem);
file.add(saveItem);
file.add(exitItem);
//为edit菜单项添加菜单项
edit.add(autoWrap);
//使用addSeparator方法添加菜单分隔符
edit.addSeparator();
edit.add(copyItem);
edit.add(pasteItem);
//为commentItem组件添加提示信息
commentItem.setToolTipText("将程序代码注释起来!");
//为format菜单添加菜单项
format.add(commentItem);
format.add(cancelItem);
//使用new JMenuItem("-")的方式不能添加菜单分隔符
edit.add(new JMenuItem("-"));
//将format菜单组合到edit菜单中,从而形成二级菜单
edit.add(format);
//将file、edit菜单添加到mb菜单条中
mb.add(file);
mb.add(edit);
//为f窗口设置菜单条
f.setJMenuBar(mb);
//----------下面开始组合右键菜单,并安装右键菜单----------
flavorGroup.add(metalItem);
flavorGroup.add(nimbusItem);
flavorGroup.add(windowsItem);
flavorGroup.add(classicItem);
flavorGroup.add(motifItem);
pop.add(metalItem);
pop.add(nimbusItem);
pop.add(windowsItem);
pop.add(classicItem);
pop.add(motifItem);
//为5个风格菜单创建事件监听器
ActionListener flavorListener = e->{
try{
switch (e.getActionCommand()){
case "Metal风格":
changeFlavor(1);
break;
case "Nimbus风格":
changeFlavor(2);
break;
case "Windows风格":
changeFlavor(3);
break;
case "Windows经典风格":
changeFlavor(4);
break;
case "Motif风格":
changeFlavor(5);
break;
}
}catch (Exception ee){
ee.printStackTrace();
}
};
//为5个风格菜单项添加事件监听器
metalItem.addActionListener(flavorListener);
nimbusItem.addActionListener(flavorListener);
windowsItem.addActionListener(flavorListener);
classicItem.addActionListener(flavorListener);
motifItem.addActionListener(flavorListener);
//调用该方法即可设置右键菜单,无须使用事件机制
ta.setComponentPopupMenu(pop);
//设置关闭关闭窗口式,退出程序
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.pack();
f.setVisible(true);
}
//定义一个方法,用于改变界面风格
private void changeFlavor(int flavor)throws Exception{
switch (flavor){
//设置Metal风格
case 1:
UIManager.setLookAndFeel("javax.swing.plaf.metal.MetalLookAndFeel");
System.out.println("1");
break;
//设置Nimbus风格
case 2:
UIManager.setLookAndFeel("javax.swing.plaf.nimbus.NimbusLookAndFeel");
System.out.println("2");
break;
//设置Window风格
case 3:
UIManager.setLookAndFeel("com.sun.java.swing.plaf.windows.WindowsLookAndFeel");
System.out.println("3");
break;
//设置Windows经典风格
case 4:
UIManager.setLookAndFeel("com.sun.java.swing.plaf.windows.WindowsClassicLookAndFeel");
System.out.println("4");
break;
//设置Motif风格
case 5:
UIManager.setLookAndFeel("com.sun.java.swing.plaf.motif.MotifLookAndFeel");
System.out.println("5");
break;
}
}
public static void main(String[] args) {
//设置Swing窗口使用Java风格
//JFrame.setDefaultLookAndFeelDecorated(true);
new SwingComponent().init();
}
}
上面代码无法通过 new JMenuItem(“-”)的方式添加菜单分隔符,只能通过addSeparator()方法来添加菜单分隔符。
Swing专门为菜单项、工具按钮之间的分隔符提供了一个JSeparator类,通常使用JMenu或者JPopupMenu的addSeparator()方法来创建并添加JSeparator对象,而不是直接使用JSeparator。实际上,JSeparator可以用在任何需要使用分隔符的地方。
上面程序为newItem菜单项增加了快捷键,为Swing菜单项指定与为AWT菜单项指定快捷键的方式有所不同——创建AWT菜单对象时可以直接传入KeyShortcut对象为其指定快捷键;但为Swing菜单项指定快捷键时必须通过setAccelerator(KeyStroke ks)方法来设置,其中KeyStroke代表一次击键动作,可以直接通过按键对应字母在指定该击键动作。
为菜单项指定快捷键时应该使用大写字母来代表按键,例如KeyStroke.getKeyStroke(‘N’,InputEvent.CTRL_DOWN_MASK)代表"Ctrl+N",但KeyStroke.getKeyStroke(‘n’,InputEvent.CTRL_DOWN_MASK)则不代表“Ctrl+N”。
除此之外,上面程序中代码所定义的changeFlavor()方便面用于改变程序外观风格,当用户点击多行文本域里的右键菜单时将会触发该方法。该方法设置Swing组件的外观风格后,再次调用SwingUtilties类的updateComponentTreeUI()方法来更新指定容器,以及该容器内所有组件的UI。注意此处更新的是JFrame对象getContentPane()方法的返回值,而不是直接更新JFrame本身。这是因为如果直接更新JFrame本身,将会导致JFrame也被更新,JFrame是一个特殊的容器,JFrame依然依赖于本地平台的图形组件。尤其是取消最后一行的代码注释后,JFrame将会使用Java风格的标题栏、边框,如果强制JFrame更新成Windows或Motif风格,则会导致该窗口失去标题和边框。
JFrame提供了一个getContentPane()方法,这个方法用于返回该JFrame的顶级容器(即JRootPane对象),这个顶级容器会包含JFrame所显示的所有非菜单组件,可以这样理解:所有看似放在JFrame中的Swing组件,除菜单之外,其实都是放在JFrame对应的顶级容器中的,而JFrame容器里提供了getContentPane()方法返回了顶级容器。在Java5以前,Java甚至不允许向JFrame中添加组件,必须先调用JFrame的getContentPane()方法获得该窗口的顶级容器,然后将所有的组件添加到该顶级容器中。从Java5以后,Java改写了JFrame的add()和setLayout()方法,当程序调用JFrame的add()和setLayout()等方法时,实际上是对JFrame的顶级容器进行操作。
从上面代码可以看出,为Swing组件添加右键菜单无须像AWT那样繁琐,只需要简单地调用setComponentPopupMenu()方法来设置右键菜单即可,无须编写事件监听器,由此可见,使用Swing组件编写图形界面程序更加简单。
除此之外,如果程序希望用户单击窗口右上角的“X”按钮时,程序退出,也无须使用事件机制,只要调用setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE)方法即可,Swing提供的这种方式也是为了简化界面编程。
JScrollPane组件时一个特殊的组件,它不同于JFrame、JPanel等普通的容器,它甚至不能指定自己的布局管理器,它主要用于为其他的Swing组件提供滚动条支持,JScrollPane通常由普通的Swing组件,可选的垂直、水平滚动条以及可选的行、列标题组成。
简而言之,如果希望让JTextArea、JTable等组件鞥由滚动条支持,值需要将该组件放入JScrollPane中,再将JScrollPane容器添加到窗口中即可。
为什么点击Swing多行菜单文本域时不是弹出像AWT多行文本域中的右键菜单?
这是由Swing组件和AWT组件实现机制不同决定的。前面已经指出,AWT的多行文本域实际上依赖于本地平台的多行文本域。简单地说,当我们在程序中防止一个AWT多行文本域,且该程序在Windows平台上运行时,该文本域组件将和记事本工具编辑区具有相同的行为方式,因为该文本域组件和记事本编辑区的底层实现时一样的。但Swing的多行文本域是纯Java的,它无须任何本地平台GUI的支持,它在热呢平台上都具有相同的行为方式,所以Swing多行文本域组件默认是没有右键菜单的,必须由程序员显式为它分配右键菜单。而且,Swing提供的JTextArea组件默认没有滚动滚动条(AWT的TextArea是否由滚动条取决于底层平台的实现),为了让该多行文本域具有滚动条,可以将该多行文本域放到JScrollPane容器中。
JScrollPane对于JTable组件尤其重要,通常需要把JTable放在JScrollPane容器中才可以显示出JTable组件的标题栏。
可以调用JComponent提供的setBorder(Border b)方法为Swing组件设置边框,其中Border是Swing提供的一个接口,用于代表组件的边框。该接口由数量众多的实现类,如LineBorder、MatteBorder。BevelBorder等,这些Border实现类都提供了相应的构造器用于创建Border对象,一旦获取了Border对象之后,就可以调用JComponent的setBordr(Border b)方法为指定组件设置边框。
TitleBorder和CompoundBorder比较独特,其中TitledBorder的作用并不是为它组件添加边框,二十位其他边框设置标题,当创建TitledBorder对象时,需要传入一个已经存在的Border对象, 新创建的TitledBorder对象会位原有的Border对象添加标题;而CompoundBorder用于组合两个边框,因此创建CompoundBorder对象时需要传入两个Border对象,一个用作组件的内边框,一个用作组件的外边框。
除此之外,Swing还提供了一个BorderFactory静态工厂类,该类提供了大量的静态工厂方法用于返回Border实例,这些静态方法的参数于各Border实现类的构造器参数基本一致。
Border不仅提供了上面所提供的一些Border实现类,还提供了MetalBorders.oolBarBorder、MetalBorders.TextFieldBorder等Border实现类,这些实现类用作Swing组件的默认边框,程序中通常无须使用这些系统边框。
为Swing组件添加边框可按如下步骤进行。
下图显示了系统可用边框之间的继承层次。
下面的例子程序示范了为Panel容器分别添加如图所示的几种边框。
import javax.swing.*;
import javax.swing.border.*;
import java.awt.*;
public class BorderTest{
private JFrame jf = new JFrame("测试边框");
public void init(){
jf.setLayout(new GridLayout(2,4));
//使用静态工厂方法创建BevelBorder
Border bb = BorderFactory.createBevelBorder(
BevelBorder.RAISED,Color.RED,Color.GREEN,Color.BLUE,Color.GRAY
);
jf.add(getPanelWithBorder(bb,"BevelBorder"));
//使用静态工厂方法创建LineBorder
Border lb = BorderFactory.createLineBorder(Color.ORANGE,10);
jf.add(getPanelWithBorder(lb,"LineBorder"));
//使用静态工厂方法创建EtchedBorder
Border eb = BorderFactory.createEmptyBorder(20,5,10,30);
jf.add(getPanelWithBorder(eb,"EmptyBorder"));
//使用静态工厂方法创建EtchedBorder
Border etb = BorderFactory.createEtchedBorder(EtchedBorder.RAISED,Color.RED,Color.GREEN);
jf.add(getPanelWithBorder(etb,"EtchedBorder"));
//直接创建TitledBorder,TitledBorder就是为原有的边框增加标题
var tb = new TitledBorder(lb,"测试标题",TitledBorder.LEFT,TitledBorder.BOTTOM,new Font("StSong",Font.BOLD,18),Color.BLUE);
//直接创建MatterBorder,MatteBorder时EmptyBorder的子类,
//它可以指定留空区域的颜色或背景,此处时指定颜色
var mb = new MatteBorder(20,5,10,30,Color.GREEN);
jf.add(getPanelWithBorder(mb,"MatteBorder"));
//直接创建CompoundBorder,CompoundBorder将两个边框组合成新边框
var cb = new CompoundBorder(new LineBorder(Color.RED,8),tb);
jf.add(getPanelWithBorder(cb,"CompoundBorder"));
jf.pack();
jf.setVisible(true);
}
public static void main(String[] args) {
new BorderTest().init();
}
public JPanel getPanelWithBorder(Border b,String BorderName){
var p = new JPanel();
p.add(new JLabel(BorderName));
//为Panel组件设置边框
p.setBorder(b);
return p;
}
}
除此之外,Swing组件还有如下两个功能。
Swing组件默认启用双缓冲绘图技术,使用双缓冲绘图技术能改进频繁重回GUI组件的显示效果(避免闪烁现象)。JComponent组件默认启用双缓冲,无须自己实现双缓冲。如果想关闭双缓冲,可以在组件上调用setDoubleBuffered(false)方法。
JComponet类提供了getInputMap()和getActionMap()两个方法,其中getInputMap()返回一个InputMap对象,该对象用于将KeyStroke对象(代表键盘或其他类似输入设备的一次输入事件)和名字关联:getActionMap()返回一个ActionMap对象,该对象用于将指定名字和Action(Action接口时ActionListener接口的子接口,可作为一个事件监听器使用)关联,从而可以允许用户通过键盘操作来代替鼠标驱动GUI上的Swing组件,相当于为GUI组件提供快捷键。典型用法如下:
//把一次键盘事件和一个aCommand对象关联
compoent.getInputMap().put(aKeyStroke,aCommand);
//将aCommand对象和anAction事件响应关联
componet.getActionMap().put(aCommand,anAction);
下面程序实现这样一个功能:用户在单行文本框内输入内容,当输入完成后,单击后面的“发送”按钮即可将文本框的内容添加到一个多行文本域中;或者输入完成后再文本框内按“Ctrl+Enter”键也可以将文件框的内容添加到一个多行文本域中。
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.InputEvent;
public class BindKeyTest{
JFrame jf = new JFrame("测试键盘绑定");
JTextArea jta = new JTextArea(5,30);
JButton jb = new JButton("发送");
JTextField jtf = new JTextField(15);
public void init(){
jf.add(jta);
var jp = new JPanel();
jp.add(jtf);
jp.add(jb);
jf.add(jp, BorderLayout.SOUTH);
//发送消息的Action,Action是ActionListener的子接口
Action sendMsg = new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
jta.append(jtf.getText()+"\n");
jtf.setText("");
}
};
//添加事件监听器
jb.addActionListener(sendMsg);
//将Ctrl+Enter键和"send"关联
jtf.getInputMap().put(KeyStroke.getKeyStroke('\n', InputEvent.CTRL_DOWN_MASK),"send");
jtf.getActionMap().put("send",sendMsg);
jf.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
jf.pack();
jf.setVisible(true);
}
public static void main(String[] args) {
new BindKeyTest().init();
}
}
Swing提供了JToolBar类来创建工具条,创建JToolBar对象时可以指定如下两个参数。
一旦创建了JToolBar对象之后,JToolBar对象还有如下几个常用方法。
上面大多数方法都比较容易理解,比较难以理解的就是add(Action a)方法,系统如何为工具条添加Action对应的按钮呢?
Action接口时ActionListener接口的子类,它除包含ActionListener接口的actionPerformed()方法之外,还包含name和icon两个属性,其中name用于指定按钮或菜单项中的文本,而icon则用于指定按钮的图标或菜单项中的图标。也就是说,Action本身并不是按钮,也不是菜单项,只是把Action对象添加到某些容器(也可直接使用Action来创建按钮),如菜单和工具栏中时,这些容器会为该Action对象创建对应的组件(菜单项和按钮)。也就是说,这些容器需要完成如下事情。
例如,程序中有一个菜单项、一个工具按钮,还有一个普通按钮都需要完成某个“复制”动作,程序就可以将该复制动作定义成Action,并为之指定name和icon属性,然后通过该Action来创建菜单项、工具按钮和普通按钮,就可以让这三个组件具有相同的功能。另一个“粘贴”按钮也大致相似,而且“粘贴”组件默认不可用,只有当复制组件被触发后,且剪贴板中有内容时才可用。
上面程序中创建了pasteAction、copyAction两个Action,然后根据这两个Action分别创建了按钮、工具按钮、菜单项组件(程序中的序列标注部分(①~②)),开始时pasteAction处于非激活状态,则该Action对应的按钮、工具按钮、菜单项都处于不可用状态。
JColorChooser用于创建颜色选择对话框,该类的用法非常简单,该类主要提供了如下两个静态方法。
Java7为JColorChooser增加了一个HSV标签页,允许用户通过HSV模式来选择颜色。
下面程序改写了前一章的HandDraw程序,该为使用JPanel作为绘图组件,而且使用JColorChooser来弹出颜色选择器对话框。
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.image.BufferedImage;
public class HandDraw{
//画图区的宽度
private final int AREA_WIDTH = 500;
//画图区的高度
private final int AREA_HEIGHT = 400;
//下面的preX、preY保存了上一次鼠标拖动事件的鼠标坐标
private int preX = -1;
private int preY = -1;
//定义一个右键菜单用于设置画笔的颜色
JPopupMenu pop = new JPopupMenu();
JMenuItem chooseColor = new JMenuItem("选择颜色");
//定义一个BufferedImage对象
BufferedImage image = new BufferedImage(AREA_WIDTH,AREA_HEIGHT,BufferedImage.TYPE_INT_RGB);
//获取image对象的Graphics
Graphics g = image.getGraphics();
private JFrame f = new JFrame("简单手绘程序");
private DrawCanvas drawArea = new DrawCanvas();
private Color foreColor = new Color(255,0,0);
public void init(){
chooseColor.addActionListener(ae->{
//下面代码直接弹出一个模式的颜色选择对话框,并返回用户选择的颜色
//foreColor = JColorChooser.showDialog(f,"选择画笔颜色",foreColor);//①
//下面代码则弹出一个非模式的颜色选择对话框
//并可以分别为“确定”按钮“取消”按钮指定事件监听器
final var colorPane = new JColorChooser(foreColor);
var jd = JColorChooser.createDialog(f,"选择画笔颜色",false,colorPane,e->foreColor=colorPane.getColor(),null);
jd.setVisible(true);
});
//将菜单项组合成右键菜单
pop.add(chooseColor);
//将右键菜单添加到drawArea对象中
drawArea.setComponentPopupMenu(pop);
//将image对象的背景色填充成白色
g.fillRect(0,0,AREA_WIDTH,AREA_HEIGHT);
drawArea.addMouseMotionListener(new MouseMotionAdapter() {
@Override
public void mouseDragged(MouseEvent e) {
//如果preX和preY大于0
if(preX>0&&preY>0){
//设置当前颜色
g.setColor(foreColor);
//绘制从上一次鼠标拖动事件点到本次鼠标拖动事件点的线段
g.drawLine(preX,preY,e.getX(),e.getY());
}
//将当前鼠标事件点的X、Y坐标保存起来
preX = e.getX();
preY = e.getY();
//重回drawArea对象
drawArea.repaint();
}
});
//监听鼠标事件
drawArea.addMouseListener(new MouseAdapter() {
@Override
public void mouseReleased(MouseEvent e) {
//松开鼠标时,把上一次鼠标拖动X、Y坐标设为-1
preX = -1;
preY = -1;
}
});
f.add(drawArea);
//f.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
f.validate();
f.setBounds(400,150,AREA_WIDTH,AREA_HEIGHT);
f.setVisible(true);
System.out.println("----------程序已开始执行----------\n");
f.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
System.out.println("----------程序已结束----------");
System.exit(0);
}
});
}
public static void main(String[] args) {
new HandDraw().init();
}
class DrawCanvas extends JPanel{
@Override
public void paint(Graphics g) {
//将image绘制到该组件上
g.drawImage(image,0,0,null);
}
}
}
在Java6时,JcolorChooser只提供了三种颜色选择方式,上面程序的HSV、CMYK两种颜色选择方式都是新增的。
JFileChooser的功能与AWT中的FileDialog基本相似,也是用于生成“打开文件”、“保存文件”对话框;与FileDialog不同的是,JFileChooser无须依赖本地平台的GUI,它由100%纯Java实现,在所有平台上具有完全相同的行为,并可以在所有平台上具有相同的外观风格。
为了调用JFileChooser来打开一个文件对话框,必须先创建该对话框的实例,JFileChooser提供了多个构造器来创建JFileChooser对象,它的构造器总共包含两个参数。
JFileChooser并不是JDialog的子类,所以不能使用setVisible(true)方法来显示该文件对话框,这意味着在多个窗口中公用该JFileChooser对象。创建JFileChooser对象时可以指定初始化路径,如下代码所示:
//以当前路径创建文件选择器
var chooser = new JFileChooser(".");
调用JFileChooser的一系列可选的方法对JFileChooser执行初始化操作。JFileChooser大致由如下几个常用方法。
//默认选择当前路径下的123.jpg文件
chooser.setSelectedFile(new File("jpg"));
//设置既可以选择文件,也可以选择路径
chooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
JFileChooser还提供了一些改变对话框标题、改变按钮标签、改变按钮的提示文本等功能的方法,可以查阅API文档来了解它们。
如果让文件对话框实现文件过滤功能,则需要结合FileFilter类来进行文件过滤。JFileChooser提供了两个方法来安装文件过滤器。
//为文件对话框添加一个文件过滤器
chooser.addChoosableFileFilter(filter);
如果需要改变文件对话框中的视图外观,则可以结合FileView类来改变对话框中文件的视图外观。
调用showXxxDialog方法可以打开文件对话框,通常如下三个方法可用。
如果程序要使用FileFilter类进行文件过滤,则通常需要扩展该FileFilter类,并重写该类的两个抽象方法,重写accept()方法时就可以指定自己的业务规则,指定该文件过滤器可以接受哪些文件。例如,如下代码:
public boolean accept(File f){
//如果该文件是路径,则接受该文件
if(f.isDirectory()) return true;
//只接受以.gif作为后缀的文件
if(name.endsWith(".gif")){
return true;
}
return false;
}
在默认情况下,JFileChooser总会在文件对话框的“文件类型”下拉列表中增加“所有文件”选项,但可以调用JFileChooser的setAcceptAllFileFilterUsed(false)来取消显示该选项。
FileView类用于改变文件对话框中文件的视图风格,FileView类也是一个抽象类,通常程序需要扩展该抽象类,并由选择性地重写它所有包含的如下几个抽象方法。
import javax.imageio.plugins.tiff.FaxTIFFTagSet;
import javax.swing.*;
import javax.swing.filechooser.FileFilter;
import java.awt.*;
import java.io.File;
import java.security.cert.Extension;
import java.util.ArrayList;
public class ImageViewer{
//定义图片预览组件的大小
final int PREVIEW_SIZE = 1920;
final int PREVIEW_SIZE2 = 1080;
JFrame jf = new JFrame("简单图片查看器");
JMenuBar menuBar = new JMenuBar();
//该label用于显示图片
JLabel label = new JLabel();
//以当前路径创建文件选择器
JFileChooser chooser = new JFileChooser(".");
JLabel accessory = new JLabel();
//定义文件过滤器
ExtensionFileFilter filter = new ExtensionFileFilter();
public void init(){
//----------下面开始初始化JFileChooser的相关属性----------
//创建一个FileFilter
filter.addExtension("jpg");
filter.addExtension("jpeg");
filter.addExtension("gif");
filter.addExtension("png");
filter.setDescription("图片文件(*.jpg,*.jpeg*.gif,*.png");
chooser.addChoosableFileFilter(filter);
//精致“文件类型”下拉列表中显示“所有文件”的选项
chooser.setAcceptAllFileFilterUsed(false);
//为文件选择器指定一个预览图片的附件
chooser.setAccessory(accessory);
//设置预览图片组件的大小和边框
accessory.setPreferredSize(new Dimension(PREVIEW_SIZE,PREVIEW_SIZE2));
accessory.setBorder(BorderFactory.createEtchedBorder());
//用于检测被选择文件的改变事件
chooser.addPropertyChangeListener(event->{
//JFileChooser的被选中文件已经发生了改变
if(event.getPropertyName()==JFileChooser.SELECTED_FILE_CHANGED_PROPERTY){
//获取用户选择的新文件
var f = (File) event.getNewValue();
if(f==null){
accessory.setIcon(null);
return;
}
//将所有文件导入ImageIcon对象中
var icon = new ImageIcon((f.getPath()));
//如果图像太大,则缩小它
if(icon.getIconWidth()>PREVIEW_SIZE){
icon = new ImageIcon(icon.getImage().getScaledInstance(PREVIEW_SIZE,-1,Image.SCALE_DEFAULT));
}
//改变accessory Label的图标
accessory.setIcon(icon);
}
});
//----------下面代码开始为该窗口安装菜单----------
var menu = new JMenuItem("文件");
menuBar.add(menu);
var openItem = new JMenuItem("打开");
menu.add(openItem);
//单击openItem菜单项显示“打开文件”对话框
openItem.addActionListener(event->{
//设置文件对话框的当前路径
//chooser.setCurrentDirectory(new File("."));
//显示文件对话框
int result = chooser.showDialog(jf,"打开图片文件");
//如果用户选择了APPROVE(同意)按钮,即打开,保存的等效按钮
if(result==JFileChooser.APPROVE_OPTION){
String name = chooser.getSelectedFile().getPath();
//显示指定图片
label.setIcon(new ImageIcon(name));
}
});
var exitItem = new JMenuItem("Exit");
menu.add(exitItem);
//为退出菜单绑定事件监听器
exitItem.addActionListener(e->System.exit(0));
jf.setJMenuBar(menuBar);
//添加用于显示图片的JLabel组件
jf.add(new JScrollPane(label));
jf.setBounds(0,0,1920,1080);
jf.setVisible(true);
jf.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
}
public static void main(String[] args) {
//这里忽略了图标的设置,所以程序并不完整可以成功打开对话框即可
new ImageViewer().init();
}
class ExtensionFileFilter extends FileFilter{
private String description;
private ArrayList<String> extensions = new ArrayList<>();
//自定义方法,用于添加文件扩展名
public void addExtension(String extension){
if(!extension.startsWith(".")){
extension="."+extension;
extensions.add(extension.toLowerCase());
}
}
//用于设置该文件过滤器的描述文本
public void setDescription(String aDescription){
description = aDescription;
}
@Override
//继承FileFilter类必须实现的抽象方法,判断文件过滤器是否接受该文件
public boolean accept(File f) {
//如果该文件是路径,则接受该文件
if(f.isDirectory())return true;
//将文件名转为小写(全部转为小写后比较,用于忽略文件名大小写)
String name = f.getName().toLowerCase();
//遍历所有可接受的扩展名,如果扩展名相同,该文件就可接受
for(var extendsion:extensions){
if(name.endsWith(extendsion)){
return true;
}
}
return false;
}
@Override
//继承FileFilter类必须实现的抽象方法,返回该文件过滤器的描述文本
public String getDescription() {
return description;
}
}
}
这里代码并不全,看个大概就行。
通过JOptionPane可以非常方便地创建一些简单的对话框,Swing已经为这些对话框添加了相应的组件,无须程序员手动添加组件,无须程序员手动添加组件。JOptionPane提供了如下4个方法来创建对话框。
JOptionPane产生的所有对话框都是模式的,在用户完成与对话框的交互之前,showXxxDialog方法都将一直阻塞当前线程。
JOptionPane所产生的对话框总是具有如同所示的布局。
上面这些方法都提供了相应的showInternalXxxDialog版本,这种方法以InternalFrame的方式打开对话框。关于什么是InternalFrame方式,参考下一节JInternalFrame的介绍。
实际上,JOptionPane的所有showXxxDialog()方法都可以提供一个可选的icon参数,用于指定该对话框的图标。
调用showXxxDialog方法时还可以指定一个可选的title参数,该参数指定所创建对话框的标题。
大部分时候对话框的消息区都是普通字符串,但使用Component作为消息区组件则更加灵活,因为该Component参数几乎可以是任何对象,从而可以让对话框的消息区包含任何内容。
如果用户希望消息区的普通字符串能换行,则可以使用"\n"字符来实现换行。
如果使用showPtionDialog方法来创建选项对话框,则可以通过指定一个Object[]类型的options参数来设置按钮区能使用的选项按钮。与前面的message参数类似的是,options数组的数组元素可以是如下几种类型。
对于showOptionDialog方法所产生的对话框,也可能返回一个CLOSED_OPTION值,当用户单击了对话框右上角的“X”按钮后将返回该值。
下面程序允许使用JOptionPane来弹出各种对话框。
import javax.swing.*;
import javax.swing.border.EtchedBorder;
import javax.swing.border.TitledBorder;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.*;
public class JOptionPaneTest{
JFrame jf = new JFrame("测试JOptionPane");
private ButtonPanel messagePane;
private ButtonPanel messageTypePanel;
private ButtonPanel msgPanel;
private ButtonPanel confirmPanel;
private ButtonPanel optionsPanel;
private ButtonPanel inputPanel;
private String messageString = "消息区内容";
private Icon messageIcon = new ImageIcon("C:\\Users\\YueDie\\Pictures\\Saved Pictures\\依睐2021.jpg");
private Object messageObject = new Date();
private Component messageComponent = new JButton("组件信息");
private JButton msgBn = new JButton("消息对话框");
private JButton confirmBn = new JButton("确定对话框");
private JButton inputBn = new JButton("输入对话框");
private JButton optionBN = new JButton("选项对话框");
public void init(){
var top = new JPanel();
top.setBorder(new TitledBorder(new EtchedBorder(),"对话框的通用选项",TitledBorder.CENTER,TitledBorder.TOP));
top.setLayout(new GridLayout(1,2));
//消息类型Panel,该Panel中的选项决定对话框的图标
messageTypePanel = new ButtonPanel("选择消息的类型",new String[]{"ERROR_MESSAGE","information_message","WARNING_MESSAGE","QUESTION_MESSAGE","PLAIN_MESSAGE"});
//消息内容类型Panel,该Panel中的选项决定对话框消息区的内容
messagePane = new ButtonPanel("选择消息内容的类型",new String[]{"字符串消息","图标消息","组件信息","普通对象消息","Object[]消息"});
top.add(messageTypePanel);
top.add(messagePane);
var bottm = new JPanel();
bottm.setBorder(new TitledBorder(new EtchedBorder(),"弹出不同的对话框",TitledBorder.CENTER,TitledBorder.TOP));
bottm.setLayout(new GridLayout(1,4));
//创建用于弹出确认对话框的Panel
confirmPanel = new ButtonPanel("消息对话框",null);
msgBn.addActionListener(new ShowAction());
msgPanel.add(msgBn);
//创建用于弹出确定对话框的Panel
confirmPanel = new ButtonPanel("确定对话框",new String[]{"DEFAULT_OPTION","YES_NO_OPTION","YES_ON_CANCEL_OPTION","OK_CEANCEL_OPTION"});
confirmBn.addActionListener(new ShowAction());
inputPanel.add(inputBn);
//创建用于弹出输出对话框的Panel
inputPanel = new ButtonPanel("输入对话框",new String[]{"单行文本框","下拉列表选择框"});
optionBN.addActionListener(new ShowAction());
inputPanel.add(inputBn);
//创建用于弹出选项对话框的Panel
optionsPanel = new ButtonPanel("选项对话框",new String[]{"字符串选项","图标选项","对象选项"});
optionBN.addActionListener(new ShowAction());
optionsPanel.add(optionBN);
bottm.add(msgPanel);
bottm.add(confirmPanel);
bottm.add(inputPanel);
bottm.add(optionsPanel);
var box = new Box(BoxLayout.Y_AXIS);
box.add(top);
box.add(bottm);
jf.add(box);
jf.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
jf.pack();
jf.setVisible(true);
}
//根据用户选择返回消息
private Object getMessage(){
switch (messagePane.getSelection()){
case "字符串消息":
return messageString;
case "图标消息":
return messageIcon;
case "组件消息":
return messageComponent;
case "普通对象消息":
return messageObject;
default:
return new Object[]{messageString,messageIcon,messageObject,messageComponent};
}
}
//根据用户选择返回会选项类型
private int getOptionType(){
switch (confirmPanel.getSelection()){
case"DEFAULT_OPTION":
return JOptionPane.DEFAULT_OPTION;
case"YES_NO_OPTION":
return JOptionPane.YES_NO_OPTION;
case"YES_NO_CANCEL_OPTION":
return JOptionPane.YES_NO_CANCEL_OPTION;
default:
return JOptionPane.OK_CANCEL_OPTION;
}
}
private int getDialogType(){
switch (messageTypePanel.getSelection()){
case "ERROR_MESSAGE":
return JOptionPane.ERROR_MESSAGE;
case "INFORMATTON_MESSAGE":
return JOptionPane.INFORMATION_MESSAGE;
case "WARNING_MESSAGE":
return JOptionPane.WARNING_MESSAGE;
case "QUESTION_MESSAGE":
return JOptionPane.QUESTION_MESSAGE;
default:
return JOptionPane.PLAIN_MESSAGE;
}
}
private class ShowAction implements ActionListener{
@Override
public void actionPerformed(ActionEvent e) {
switch (e.getActionCommand()){
case"确认对话框":
JOptionPane.showConfirmDialog(jf,getMessage(),"确定对话框",getOptionType(),getDialogType());
break;
case"输入对话框":
if(inputPanel.getSelection().equals("单行文本框")){
JOptionPane.showInputDialog(jf,getMessage(),"输入对话框",getDialogType());
}else{
JOptionPane.showInputDialog(jf,getMessage(),"输入对话框",getDialogType(),null,new String[]{
"轻量级Java EE企业应用实战","疯狂Java讲义"
},"疯狂Java讲义");
}
break;
case "消息对话框":
JOptionPane.showMessageDialog(jf,getMessage(),"消息对话框",getDialogType());
break;
case "选项对话框":
JOptionPane.showOptionDialog(jf,getMessage(),"选择对话框",getOptionType(),getDialogType(),null,null,"a");
break;
}
}
}
class ButtonPanel extends JPanel{
private ButtonGroup group;
public ButtonPanel(String title,String[]options){
setBorder(BorderFactory.createTitledBorder(BorderFactory.createEtchedBorder(),title));
setLayout(new BoxLayout(this,BoxLayout.Y_AXIS));
group = new ButtonGroup();
for(var i =0;options!=null&&i<options.length;i++){
var b = new JRadioButton(options[i]);
b.setActionCommand(options[i]);
add(b);
group.add(b);
b.setSelected(i==0);
}
}
//定义一个方法,用于返回用户选择的选项
public String getSelection(){
return group.getSelection().getActionCommand();
}
}
public static void main(String[] args) {
new JOptionPaneTest().init();
}
}
Swing提供了一些具有特殊功能的容器,这些特殊容器可以用于创建一些更复杂的用户界面。下面将依次介绍这些特殊容器。
JSplitPane用于创建一个分割面板,它可以将一个组件(通常是一个容器)分割成两个部分,并提供一个风割条,用户可以拖动该分割条来调整大小。
new JSplitPane(方向,左/上组件,右/下组件);
JSplitPane默认关闭连续布局特性,因为使用连续布局需要不断重回两边组件,因此运行效率很低。如果需要打开指定JSplitPane面板的连续布局特性,则可以使用如下代码:
//打开JSplitPane的连续布局特性
jsp.setContinuouseLayout(true);
上下分割面板的分割条中还有两个三角箭头,这两个箭头为被称为“一触即展”键,当用户单击某个三角箭头时,将看到箭头所指的组件慢慢缩小到没有,而另一个组件则扩大到占据整个面板。如果需要打开“一触即展”特性,使用如下代码即可。
//打开“一触即展”特性
jsp.setOneTouchExpandable(true);
JSplitPane分割还有如下几个可用方法来设置该面板的相关特性。
JTabbedPane可以很方便地在窗口上放置多个标签页,每个标签页相当于获得了一个与外部容器具有相同大小的组件摆放区域。通过这种方式,就可以在一个容器里放置更多的组件,例如右键桌面上的“我的电脑”图标,在弹出的快捷菜单里单击“属性”菜单项,就可以看到一个“系统属性”对话框。
这个对话框里包含了7个标签页
如果需要使用JTabbedPane在窗口上创建标签页,则可以按如下步骤进行。
创建一个JTabbedPane在窗口上创建标签页,则可以按如下步骤进行。
即使创建JTabbedPane时没有指定者两个参数,程序也可以在后面改变JTabbedPane的者两个属性。例如,通过setTabLayoutPolicy()方法改变标签页标题的布局策略,使用setTabPlacement()方法设置标签页标题的放置位置。
例如,下面代码创建一个JTabbedPane对象,该JTabbedPane的标签页标题位于窗口左侧,当窗口的一行不能摆放所有的标签页标题时,JTabbedPane将采用换行方式来排列标签页标题。
var tabPane = new JTabbedPane(JTabbedPane.LEFT,JTabbedPane.WRAP_TAB_LAYOUT);
调用JTabbedPane对象的addTab()、insertTab()、setComponentAt()、removeTabAt()方法来增加、插入、修改和删除标签页。其中addTab()方法总是在最前面增加标签页,而insertTab()、setComponentAt()、removeTabAt()方法都可以使用一个index参数,表示在指定位置插入标签页,修改指定位置的标签页,删除指定位置的标签页。
添加标签页时可以指定该标签页的标题(title)、图标(icon),以及该Tab页面的组件(component)及提示信息(tip),这4个参数都可以是null;如果某个参数是null,则对应的内容为空。
不管使用增加、插入、修改哪种操作来改变JTabbedPane中的标签页,都是传入一个Component组件作为标签页。也就是说,如果希望在某个标签页内放置更多的组件,则必须先将这些组件放置到一个容器(例如JPanel)里,然后将该容器设置为JTabbedPane指定位置的组件。
不要使用JTabbedPane的add()方法来添加组件,该方法是JTabbedPane重写Container容器中的add()方法,如果使用该add()方法来添加Tab页面,每次添加的标签页都会直接覆盖原有的标签页。
如果需要让某个标签页显示出来,则可以通过调用JTabbedPane的setSelectdIndex()方法来实现。例如如下代码:
//设置第三个Tab页面处于显示状态
tabPane.setSelectedIndex(2);
//设置最后一个Tab页面处于显示状态
tabPane.setSelectedIndex(tabPanel.getTabCount()-1);