文章目录

  • Swing图形用户界面编程
  • Java图形用户界面技术
  • Swing技术基础
  • Swing类层次结构
  • Swing程序结构
  • 创建JFrame方式
  • 继承JFrame方式
  • 事件处理模型
  • 采用内部类处理事件
  • 采用Lambda表达式处理事件
  • 使用适配器
  • 布局管理
  • FlowLayout布局
  • BorderLayout布局
  • GridLayout布局
  • 不使用布局管理器
  • Swing组件
  • 标签和按钮
  • 文本输入组件
  • 复选框和单选按钮
  • 下拉列表
  • 列表
  • 分隔面板
  • 表格
  • 案例:图书库存


Swing图形用户界面编程

图形用户界面(Graphical User Interface,简称 GUI)编程对于某种语言来说非常重要。Java的应用主要方向是基于Web浏览器的应用,用户界面主要是HTML、CSS和JavaScript等基于Web的技术,这些介绍要到Java EE阶段才能学习到。
而本章介绍的Java图形用户界面技术是基于Java SE的Swing,事实上它们在实际应用中使用不多,因此本章的内容只做了解。
(那问什么还要学它,因为入门小项目要用啊…)

Java图形用户界面技术

Java图形用户界面技术主要有:AWT、Applet、Swing和JavaFX。

  1. AWT
    AWT(Abstract Window Toolkit)是抽象窗口工具包,AWT是Java 程序提供的建立图形用户界面最基础的工具集。AWT支持图形用户界面编程的功能包括:用户界面组件(控件)、事件处理模型、图形图像处理(形状和颜色)、字体、布局管理器和本地平台的剪贴板来进行剪切和粘贴等。AWT是Applet和Swing技术的基础。
    AWT在实际的运行过程中是调用所在平台的图形系统,因此同样一段AWT程序在不同的操作系统平台下运行所看到的样式是不同的。(在Windows下运行,显示的窗口是Windows风格的窗口;在UNIX下运行时,显示的是UNIX风格的窗口。)
  2. Applet
    Applet称为Java小应用程序,Applet基础是AWT,但它主要嵌入到HTML代码中,由浏览器加载和运行,由于存在安全隐患和运行速度慢等问题,已经很少使用了。
  3. Swing
    Swing是Java主要的图形用户界面技术,Swing提供跨平台的界面风格,用户可以自定义Swing的界面风格。Swing提供了比AWT更完整的组件,引入了许多新的特性。Swing API是围绕着实现AWT各个部分的API构筑的。Swing是由100%纯Java实现的,Swing组件没有本地代码,不依赖操作系统的支持,这是它与AWT组件的最大区别。(重点介绍)
  4. JavaFX
    JavaFX是开发丰富互联网应用程序(Rich Internet Application,缩写RIA)的图形用户界面技术,JavaFX期望能够在桌面应用的开发领域与Adobe公司的AIR、微软公司的Silverlight相竞争。传统的互联网应用程序基于Web,客户端是浏览器。而丰富互联网应用程序试图打造自己的客户端,替代浏览器。

Swing技术基础

AWT是Swing的基础,Swing事件处理和布局管理都是依赖于AWT,AWT内容来自java.awt包,Swing内容来自javax.swing包。AWT和Swing作为图形用户界面技术包括了4个主要的概念:组件 (Component)、容器(Container)、事件处理和布局管理器(LayoutManager)。

Swing类层次结构

容器和组件构成了Swing的主要内容。

Swing容器类主要有:JWindow、JFrame和JDialog,其他的不带“J”开头都是AWT提供的类,在Swing中大部分类都是以“J”开头。

Swing容器类层次结构:

java图形用户界面m java图形用户界面编程_事件处理

Swing组件类层次结构,Swing所有组件继承自JComponent,JComponent又间接继承自AWT的java.awt.Component类。

Swing组件类层次结构:

java图形用户界面m java图形用户界面编程_事件处理_02

Swing程序结构

图形用户界面主要是由窗口以及窗口中的组件构成的,编写Swing程序主要就是创建窗口和添加组件过程。Swing中的窗口主要是使用JFrame,很少使用JWindow。JFrame有标题、边框、菜单、大小和窗口管理按钮等窗口要素,而JWindow没有标题栏和窗口管理按钮。
构建Swing程序主要有两种方式:创建JFrame或继承JFrame。

示例:窗口标题是MyFrame,窗口中有显示字符串“Hello Swing!”。

创建JFrame方式

直接实例化JFrame对象,然后设置JFrame属性,添加窗口所需要的组件。

import java.awt.Container;
import javax.swing.JFrame;
import javax.swing.JLabel;

public class SwingDemo1 {
    
    public static void main(String[] args) {
        //创建窗口对象
        JFrame frame = new JFrame("MyFrame");
        // 创建标签
        JLabel label = new JLabel("Hello Swing!");
        // 获得窗口的内容面板
        Container contentPane = frame.getContentPane();
        // 添加标签到内容面板
        contentPane.add(label);
        // 设置窗口大小
        frame.setSize(300, 300);
        // 设置窗口可见
        frame.setVisible(true);
    }
}

第9行使用JFrame的JFrame(String title)构造方法创建JFrame对象,title是设置创建的标题。默认情况下JFrame是没有大小且不可见的,因此创建JFrame对象后还需要设置大小和可见
设置JFrame窗口大小和可见这两条语句,应该在添加完成所有组件之后调用。否则在多个组件情况下,会导致有些组件没有显示。

创建好窗口后,就需要将其中的组件添加进来,第11行是创建标签对象,构造方法中字符串参数是标签要显示的文本。创建好组件之后需要把它添加到窗口的内容面板上,第15行调用容器的add()方法将组件添加到窗口上。
在Swing中添加到JFrame上的所有可见组件,除菜单栏外,全部添加到内容面板上,不要直接添加到JFrame上,这是Swing绘制系统所要求的。内容面板是JFrame中包含的一个子容器。

几乎所有的图形用户界面技术,在构建界面时都采用层级结构(树形结构),根是顶级容器(只能包含其他容器的容器),子容器有内容面板和菜单栏,然后其他的组件添加到内容面板容器中。所有的组件都有add()方法,通过调用add()方法将其他组件添加到容器中,作为当前容器的子组件。

java图形用户界面m java图形用户界面编程_事件处理_03

继承JFrame方式

编写一个继承JFrame的子类,在构造方法中初始化窗口,添加窗口所需要的组件。

import java.awt.Container;
import javax.swing.JFrame;
import javax.swing.JLabel;

class MyFrame extends JFrame {
    
    public MyFrame(String title) {
        super(title);
        // 创建标签
        JLabel label = new JLabel("Hello Swing!");
        // 获得窗口的内容面板
        Container contentPane = getContentPane();
        // 添加标签到内容面板
        contentPane.add(label);
        // 设置窗口大小
        setSize(300, 300);
        // 设置窗口可见
        setVisible(true);
    }
}


public class SwingDemo2{
    
    public static void main(String[] args) {
        //创建窗口对象
        new MyFrame("MyFrame");
    }
}

创建JFrame方式适合于小项目,代码量少、窗口不多、组件少的情况。继承JFrame 方式,适合于大项目,可以针对不同界面自定义一个Frame类,属性可以在构造方法中进行设置;缺点是有很多类文件需要有效地管理。

事件处理模型

图形界面的组件要响应用户操作,就必须添加事件处理机制。Swing采用AWT的事件处理模型进行事件处理。在事件处理的过程中涉及三个要素:

  1. 事件:是用户对界面的操作,在Java中事件被封装称为事件类java.awt.AWTEvent及其子类,例如按钮单击事件类是java.awt.event.ActionEvent。
  2. 事件源:是事件发生的场所,就是各个组件,例如按钮单击事件的事件源是按钮(Button)。
  3. 事件处理者:是事件处理程序,在Java中事件处理者是实现特定接口的事件对象。

在事件处理模型中最重要的是事件处理者,它根据事件(XXXEvent)的不同会实现不同的接口,这些接口命名为XXXListener,所以事件处理者也称为事件监听器。最后事件源通过addXXXListener()方法添加事件监听,监听XXXEvent事件。

各种事件和对应的监听器接口:

事件类型

相应监听器接口

监听器接口中的方法

Action

ActionListener

actionPerformed(ActionEvent)

Item

ItemListener

itemStateChanged(ItemEvent)

Mouse

MouseListener

mousePressed(MouseEvent)

mouseReleased(MouseEvent)

mouseEntered(MouseEvent)

mouseExited(MouseEvent)

mouseClicked(MouseEvent)

MouseMotion

MouseMotionListener

mouseDragged(MouseEvent)

mouseMoved(MouseEvent)

Key

KeyListener

KeyPressed(KeyEvent)

KeyReleased(KeyEvent)

KeyTyped(KeyEvent)

Focus

FocusListener

focusGained(FocusEvent)

focusLost(FocusEvent)

Adjustment

AdjustmentListener

adjustmentValueChanged(AdjustmentEvent)

Component

ComponentListener

componentMoved(ComponentEvent)

componentHidden(ComponentEvent)

componentResized(ComponentEvent)

componentShown(ComponentEvent)

Window

WindowListener

windowClosing(WindowEvent)

windowOpened(WindowEvent)

windowIconified(WindowEvent)

windowJDeiconified(WindowEvent)

windowClosed(WindowEvent)

windowActivated(WindowEvent)

windowDeactivated(WindowEvent)

Container

ContainerListener

componentAdded(ContainerEvent)

componentAdded(ContainerEvent)

Text

TextListener

textValueChanged(TextEvent)

事件处理者可以实现XXXListener接口任何形式,即:外部类、内部类、匿名内部类和Lambda表达式;如果XXXListener接口只有一个抽象方法,事件处理者还可以是Lambda表达式。为了访问窗口中的组件方便,往往使用内部类、匿名内部类和Lambda表达式。

采用内部类处理事件

示例:界面中有两个按钮和一个标签,当单击Button1或Button2时会改变标签显示的内容。

import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;

class MyFrame extends JFrame {
    
    // 声明标签
    JLabel label;
    
    public MyFrame(String title) {
        super(title);
        // 创建标签
        label = new JLabel("Label");
        // 添加标签到内容面板
        getContentPane().add(label, BorderLayout.NORTH);
        // 创建Button1
        JButton button1 = new JButton("Button1");
        // 添加Button1到内容面板
        getContentPane().add(button1, BorderLayout.CENTER);
        // 创建Button2
        JButton button2 = new JButton("Button2");
        // 添加Button2到内容面板
        getContentPane().add(button2, BorderLayout.SOUTH);
        // 设置窗口大小
        setSize(350, 120);
        // 设置窗口可见
        setVisible(true);
        
        // 注册事件监听器,监听Button2单击事件
        button2.addActionListener(new ActionEventHandler());
        // 注册事件监听器,监听Button1单击事件
        button1.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent event) {
                label.setText("Hello Swing!");
            }
        });
    }
    
    // Button2事件处理者
    class ActionEventHandler implements ActionListener {
        @Override
        public void actionPerformed(ActionEvent e) {
            label.setText("Hello World!");
        }
    }
}

第18行通过add(button1, BorderLayout.NORTH)方法将标签添加到内容面板,这个add()方法与前面介绍的有所不同,它的第二个参数是指定组件的位置。

在事件处理模型中,内部类实现的模型,内部类会定义为成员内部类,因此不能访问其他方法中的局部变量组件,只能访问成员变量组件,所以代码第11行将标签组件声明为成员变量, 否则ActionEventHandler内部类无法访问该组件。而匿名内部类既可以访问所在方法的局部变量组件,也可以访问成员变量组件。

采用Lambda表达式处理事件

如果一个事件监听器接口只有一个抽象方法,则可以使用Lambda表达式实现事件处理,这些接口主要有:ActionListener、AdjustmentListener、ItemListener、MouseWheelListener、TextListener和 WindowStateListener等。

import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;

public class MyFrame extends JFrame implements ActionListener {
    
    // 声明标签
    JLabel label;
    
    public MyFrame(String title) {
        super(title);
        // 创建标签
        label = new JLabel("Label");
        // 添加标签到内容面板
        getContentPane().add(label, BorderLayout.NORTH);
        // 创建Button1
        JButton button1 = new JButton("Button1");
        // 添加Button1到内容面板
        getContentPane().add(button1, BorderLayout.CENTER);
        // 创建Button2
        JButton button2 = new JButton("Button2");
        // 添加Button2到内容面板
        getContentPane().add(button2, BorderLayout.SOUTH);
        // 设置窗口大小
        setSize(350, 120);
        // 设置窗口可见
        setVisible(true);
        // 注册事件监听器,监听Button2单击事件
        button2.addActionListener(this);
        // 注册事件监听器,监听Button1单击事件
        button1.addActionListener((event) -> {
            label.setText("Hello World!");
        });
    }
    
    @Override
    public void actionPerformed(ActionEvent event) {
        label.setText("Hello Swing!");
    }
}

代码第34行采用Lambda表达式实现的事件监听器,可见代码非常简单。另外,当前窗口本身也可以是事件处理者,代码第8行声明窗口实现ActionListener接口,代码第40行是实现抽象方法,那么注册事件监听器参数就是this了,见代码第32行。

使用适配器

事件监听器都是接口,在Java接口中定义的抽象方法必须全部实现(哪怕对某些方法并不关心,也要给一对空的大括号表示实现)。例如WindowListener是窗口事件(WindowEvent)监听器接口,为了在窗口中接收到窗口事件,需要在窗口中注册WindowListener事件监听器。

this.addWindowListener(new WindowListener() {
    
    @Override
    public void windowActivated(WindowEvent e) {
    }
    
    @Override
    public void windowClosed(WindowEvent e) {
    }
    
    @Override
    public void windowClosing(WindowEvent e) {
        // 退出系统
        System.exit(0);
    }
    
    @Override
    public void windowDeactivated(WindowEvent e) {
    }
    
    @Override
    public void windowDeiconified(WindowEvent e) {
    }
    
    @Override
    public void windowIconified(WindowEvent e) {
    }
    
    @Override
    public void windowOpened(WindowEvent e) {
    }
});

Java提供了一些与监听器相配套的适配器。监听器是接口,命名采用XXXListener,而适配器是类,命名采用XXX Adapter。在使用时通过继承事件所对应的适配器类,覆盖所需要的方法,无关方法不用实现。

this.addWindowListener(new WindowAdapter(){
    @Override
    public void windowClosing(WindowEvent e) {
        // 退出系统
        System.exit(0);
    }
});

由于Java的单一继承机制,当需要多种监听器或此类已有父类时,就无法采用事件适配器了。

并非所有的监听器接口都有对应的适配器类,一般定义了多个方法的监听器接口,例如 WindowListener有多个方法对应多种不同的窗口事件时,才需要配套的适配器,主要的适配器如下:
ComponentAdapter 组件适配器
ContainerAdapter 容器适配器
FocusAdapter 焦点适配器
KeyAdapter 键盘适配器
MouseAdapter 鼠标适配器
MouseMotionAdapter 鼠标运动适配器
WindowAdapter 窗口适配器

布局管理

Java为了实现图形用户界面的跨平台,并实现动态布局等效果,Java将容器内的所有组件布局交给布局管理器管理。布局管理器负责组件的排列顺序、大小、位置,当窗口移动或调整大小后组件如何变化等。

Java SE提供了7种布局管理器:FlowLayout、BorderLayout、GridLayout、BoxLayout、 CardLayout、SpringLayout和GridBagLayout,其中最基础的是FlowLayout、BorderLayout和GridLayout 布局管理器。

FlowLayout布局

FlowLayout布局摆放组件的规律是:从上到下、从左到右进行摆放,如果容器足够宽,第一个组件先添加到容器中第一行的最左边,后续的组件依次添加到上一个组件的右边,如果当前行已摆放不下该组件,则摆放到下一行的最左边。

FlowLayout构造方法:
FlowLayout(int align, int hgap, int vgap) 创建一个FlowLayout对象,它具有指定的对齐方式以及指定的水平和垂直间隙,hgap参数是组件之间的水平间隙,vgap参数是组件之间的垂直间隙, 单位是像素。
FlowLayout(int align) 创建一个FlowLayout对象,指定的对齐方式,默认的水平和垂直间隙是5个单位。
FlowLayout() 创建一个FlowLayout对象,它是居中对齐的,默认的水平和垂直间隙是5个单位。

上述参数align是对齐方式,它是通过FlowLayout的常量指定的,这些常量说明如下:

FlowLayout.CENTER 指示每一行组件都应该是居中的。
FlowLayout.LEADING 指示每一行组件都应该与容器方向的开始边对齐,例如,对于从左到右的方向,则与左边对齐。
FlowLayout.LEFT 指示每一行组件都应该是左对齐的。
FlowLayout.RIGHT 指示每一行组件都应该是右对齐的。
FlowLayout.TRAILING 指示每行组件都应该与容器方向的结束边对齐,例如,对于从左到右的方向,则与右边对齐。

import java.awt.FlowLayout;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;

public class MyFrame extends JFrame {
    
    // 声明标签
    JLabel label;
    
    public MyFrame(String title) {
        super(title);
        setLayout(new FlowLayout(FlowLayout.LEFT, 20, 20));
        // 创建标签
        label = new JLabel("Label");
        // 添加标签到内容面板
        getContentPane().add(label);
        // 创建Button1
        JButton button1 = new JButton("Button1");
        // 添加Button1到内容面板
        getContentPane().add(button1);
        // 创建Button2
        JButton button2 = new JButton("Button2");
        // 添加Button2到内容面板
        getContentPane().add(button2);
        // 设置窗口大小
        setSize(350, 120);
        // 设置窗口可见
        setVisible(true);
        // 注册事件监听器,监听Button2单击事件
        button2.addActionListener((event) -> {
            label.setText("Hello Swing!");
        });
        // 注册事件监听器,监听Button1单击事件
        button1.addActionListener((event) -> {
            label.setText("Hello World!");
        });
    }
}

第13行是设置当前窗口的布局是FlowLayout布局,采用FlowLayout(int align, int hgap, int vgap) 构造方法。一旦设置了FlowLayout布局,就可以通过add(Component comp)方法添加组件到窗口的内容面板。
采用FlowLayout布局如果水平空间比较小,组件会垂直摆放(拖曳窗口的边缘使窗口变窄,最后一个组件换行)。

BorderLayout布局

BorderLayout布局是窗口的默认布局管理器。
BorderLayout是JWindow、JFrame和JDialog的默认布局管理器。BorderLayout布局管理器把容器分成5个区域:North、South、East、West和Center,每个区域只能放置一个组件。(North与South占整行,其余横向中间部分分为West、Center、East)

BorderLayout构造方法:
BorderLayout(int hgap, int vgap) 创建一个BorderLayout对象,指定水平和垂直间隙,hgap参数是组件之间的水平间隙,vgap参数是组件之间的垂直间隙,单位是像素。
BorderLayout() 创建一个BorderLayout对象,组件之间没有间隙。

BorderLayout布局有5个区域,为此BorderLayout中定义了5个约束常量,说明如下:

BorderLayout.CENTER 中间区域的布局约束(容器中央)
BorderLayout.EAST 东区域的布局约束(容器右边)
BorderLayout.NORTH 北区域的布局约束(容器顶部)
BorderLayout.SOUTH 南区域的布局约束(容器底部)
BorderLayout.WEST 西区域的布局约束(容器左边)

import java.awt.BorderLayout;
import java.awt.Button;
import javax.swing.JFrame;

public class MyFrame extends JFrame {
    
    public MyFrame(String title) {
        super(title);
        // 设置BorderLayout布局
        setLayout(new BorderLayout(10, 10));
        // 添加按钮到容器的North区域
        getContentPane().add(new Button("北"), BorderLayout.NORTH);
        // 添加按钮到容器的South区域
        getContentPane().add(new Button("南"), BorderLayout.SOUTH);
        // 添加按钮到容器的East区域
        getContentPane().add(new Button("东"), BorderLayout.EAST);
        // 添加按钮到容器的West区域
        getContentPane().add(new Button("西"), BorderLayout.WEST);
        // 添加按钮到容器的Center区域
        getContentPane().add(new Button("中"), BorderLayout.CENTER);
        setSize(300, 300);
        setVisible(true);
    }
}

添加方法是add(Component comp, Object constraints),第二个参数constraints指定约束。

当使用BorderLayout时,如果容器的大小发生变化,其变化规律为:组件的相对位置不变,大小发生变化。如果容器变高或矮,则North和South不变,West、Center和East变高或矮;如果容器变宽或窄,West和East区域不变,North、Center和South变宽或窄。

在5个区域中不一定都放置了组件,如果某个区域缺少组件,界面布局会有比较大的影响。

GridLayout布局

GridLayout布局以网格形式对组件进行摆放,容器被分成大小相等的矩形,一个矩形中放置一个组件。

GridLayout构造方法:
GridLayout() 创建具有默认值的GridLayout对象,即每个组件占据一行一列。
GridLayout(int rows, int cols) 创建具有指定行数和列数的GridLayout对象。
GridLayout(int rows, int cols, int hgap, int vgap) 创建具有指定行数和列数的GridLayout对象,并指定水平和垂直间隙。

import java.awt.Button;
import java.awt.GridLayout;
import javax.swing.JFrame;

public class MyFrame extends JFrame {
    
    public MyFrame(String title) {
        super(title);
        // 设置3行3列的GridLayout布局管理器
        setLayout(new GridLayout(3, 3));
        // 添加按钮到第一行的第一格
        getContentPane().add(new Button("1"));
        // 添加按钮到第一行的第二格
        getContentPane().add(new Button("2"));
        // 添加按钮到第一行的第三格
        getContentPane().add(new Button("3"));
        // 添加按钮到第二行的第一格
        getContentPane().add(new Button("4"));
        // 添加按钮到第二行的第二格
        getContentPane().add(new Button("5"));
        // 添加按钮到第二行的第三格
        getContentPane().add(new Button("6"));
        // 添加按钮到第三行的第一格
        getContentPane().add(new Button("7"));
        // 添加按钮到第三行的第二格
        getContentPane().add(new Button("8"));
        // 添加按钮到第三行的第三格
        getContentPane().add(new Button("9"));
        setSize(400, 400);
        setVisible(true);
    }
}

第10行是设置当前窗口布局采用3行3列的GridLayout布局,它有9个区域,分别从左到右,从上到下摆放。

GridLayout布局将容器分成几个区域,也会出现某个区域缺少组件情况,GridLayout布局会根据行列划分的不同,会平均占据容器的空间,实际情况比较复杂。

不使用布局管理器

如果要开发的图形用户界面应用不考虑跨平台,不考虑动态布局,窗口大小不变的,那么布局管理器就失去使用的意义。容器也可以不设置布局管理器,那么此时的布局是由开发人员自己管理的。
组件有三个与布局有关的方法setLocation()、setSize()和setBounds(),在设置了布局管理的容器中组件的这几个方法不起作用,不设置布局管理时它们才起作用。

void setLocation(int x, int y) 方法是设置组件的位置。
void setSize(int width, int height) 方法是设置组件的大小。
void setBounds(int x, int y, int width, int height) 方法是设置组件的大小和位置。

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.SwingConstants;

public class MyFrame extends JFrame {
    
    public MyFrame(String title) {
        super(title);
        //设置窗口大小不变的
        setResizable(false);
        // 不设置布局管理器
        getContentPane().setLayout(null);
        // 创建标签
        JLabel label = new JLabel("Label");
        // 设置标签的位置和大小
        label.setBounds(89, 13, 100, 30);
        // 设置标签文本水平居中
        label.setHorizontalAlignment(SwingConstants.CENTER);
        // 添加标签到内容面板
        getContentPane().add(label);
        
        // 创建Button1
        JButton button1 = new JButton("Button1");
        // 设置Button1的位置和大小
        button1.setBounds(89, 59, 100, 30);
        // 添加Button1到内容面板
        getContentPane().add(button1);
        
        // 创建Button2
        JButton button2 = new JButton("Button2");
        // 设置Button2的位置
        button2.setLocation(89, 102);
        // 设置Button2的大小
        button2.setSize(100, 30);
        // 添加Button2到内容面板
        getContentPane().add(button2);
        // 设置窗口大小
        setSize(300, 200);
        // 设置窗口可见
        setVisible(true);
        // 注册事件监听器,监听Button2单击事件
        button2.addActionListener((event) -> {
            label.setText("Hello Swing!");
        });
        // 注册事件监听器,监听Button1单击事件
        button1.addActionListener((event) -> {
            label.setText("Hello World!");
        });
    }
}

第11行是设置不能调整窗口大小,没有设置布局管理器后,容器中的组件都绝对布局,容器大小如果变化,那么其中的组件大小和位置都不会变化,将窗口拉大后,组件还是在原来的位置。

(我要放弃Eclipse了…IDEA我来了)

Swing组件

Swing所有组件都继承自JComponent,主要有文本处理、按钮、标签、列表、面板、组合框、滚动条、滚动面板、菜单、表格和树等组件。

标签和按钮

Swing中标签类是JLabel,它不仅可以显示文本还可以显示图标。

JLabel构造方法:
JLabel() 创建一个无图标无标题标签对象。
JLabel(Icon image) 创建一个具有图标的标签对象。
JLabel(Icon image, int horizontalAlignment) 通过指定图标和水平对齐方式创建标签对象。
JLabel(String text) 创建一个标签对象,并指定显示的文本。
JLabel(String text, Icon icon, int horizontalAlignment) 通过指定显示的文本、图标和水平对齐方式创建标签对象。
JLabel(String text, int horizontalAlignment) 通过指定显示的文本和水平对齐方式创建标签对象。

上述构造方法horizontalAlignment参数是水平对齐方式,它的取值是SwingConstants中定义的以下常量之一:LEFT、CENTER、RIGHT、LEADING 或 TRAILING。

Swing中的按钮类是JButton,JButton不仅可以显示文本还可以显示图标。

JButton构造方法:
JButton() 创建不带文本或图标的按钮对象。
JButton(Icon icon) 创建一个带图标的按钮对象。
JButton(String text):创建一个带文本的按钮对象。
JButton(String text, Icon icon) 创建一个带初始文本和图标的按钮对象。

示例:界面中上面图标是标签, 下面两个图标是按钮,当单击按钮时标签可以切换图标。

import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.SwingConstants;

public class MyFrame extends JFrame {
    
    // 用于标签切换的图标
    private static Icon images[] = {
        new ImageIcon("./icon/0.png"),
        new ImageIcon("./icon/1.png"),
        new ImageIcon("./icon/2.png"),
        new ImageIcon("./icon/3.png"),
        new ImageIcon("./icon/4.png"),
        new ImageIcon("./icon/5.png") };
    
    // 当前页索引
    private static int currentPage = 0;
    
    public MyFrame(String title) {
        super(title);
        
        // 设置窗口大小不变
        setResizable(false);
        
        // 不设置布局管理器
        getContentPane().setLayout(null);
        
        // 创建标签
        JLabel label = new JLabel(images[0]);
        // 设置标签的位置和大小
        label.setBounds(94, 27, 100, 50);
        // 设置标签文本水平居中
        label.setHorizontalAlignment(SwingConstants.CENTER);
        // 添加标签到内容面板
        getContentPane().add(label);
        
        // 创建向后翻页按钮
        JButton backButton = new JButton(new ImageIcon("./icon/ic_menu_back.png"));
        // 设置按钮的位置和大小
        backButton.setBounds(77, 90, 47, 30);
        // 添加按钮到内容面板
        getContentPane().add(backButton);
        
        // 创建向前翻页按钮
        JButton forwardButton = 
            new JButton(new ImageIcon("./icon/ic_menu_forward.png"));
        // 设置按钮的位置和大小
        forwardButton.setBounds(179, 90, 47, 30);
        // 添加按钮到内容面板
        getContentPane().add(forwardButton);
        
        // 设置窗口大小
        setSize(300, 200);
        // 设置窗口可见
        setVisible(true);
        // 注册事件监听器,监听向后翻页按钮单击事件
        backButton.addActionListener((event) -> {
            if (currentPage < images.length - 1) {
                currentPage++;
            }
            label.setIcon(images[currentPage]);
        });
        
        // 注册事件监听器,监听向前翻页按钮单击事件
        forwardButton.addActionListener((event) -> {
            if (currentPage > 0) {
                currentPage--;
            }
            label.setIcon(images[currentPage]);
        });
    }
}

第11行定义ImageIcon数组,用于标签切换图标,注意Icon是接口,ImageIcon是实现Icon接口。代码第20行currentPage变量记录了当前页索引,前后翻页按钮会改变前页索引。

文本输入组件

文本输入组件主要有:文本框(JTextField)、密码框(JPasswordField)和文本区(JTextArea)。文本框和密码框都只能输入和显示单行文本。当按下Enter键时,可以触发ActionEvent事件。而文本区可以输入和显示多行多列文本。

文本框(JTextField)构造方法:
JTextField() 创建一个空的文本框对象。
JTextField(int columns) 指定列数,创建一个空的文本框对象,列数是文本框显示的宽度,列数主要用于FlowLayout布局。
JTextField(String text) 创建文本框对象,并指定初始化文本。
JTextField(String text, int columns) 创建文本框对象,并指定初始化文本和列数。

JPasswordField继承自JTextField,构造方法类似。

文本区(JTextArea)构造方法:
JTextArea() 创建一个空的文本区对象。
JTextArea(int rows, int columns) 创建文本区对象,并指定行数和列数。
JTextArea(String text) 创建文本区对象,并指定初始化文本。
JTextArea(String text, int rows, int columns) 创建文本区对象,并指定初始化文本、行数和列数。

示例:界面中有三个标签(TextField、Password、TextArea)。
可以采用布局嵌套,将TextField标签、Password标签、文本框和密码框都放到一个面板(panel1) 中;将TextArea和文本区放到一个面板(panel2)中。两个面板panel1和panel2放到内容视图中,内容视图采用BorderLayout布局,每个面板内部采用FlowLayout布局。

import java.awt.BorderLayout;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JPasswordField;
import javax.swing.JTextArea;
import javax.swing.JTextField;

public class MyFrame extends JFrame {
    
    private JTextField textField;
    private JPasswordField passwordField;
    
    public MyFrame(String title) {
        super(title);
        // 设置布局管理BorderLayout
        getContentPane().setLayout(new BorderLayout());
        
        // 创建一个面板panel1放置TextField和Password
        JPanel panel1 = new JPanel();
        // 将面板panel1添加到内容视图
        getContentPane().add(panel1, BorderLayout.NORTH);
        
        // 创建标签
        JLabel lblTextFieldLabel = new JLabel("TextField:");
        // 添加标签到面板panel1
        panel1.add(lblTextFieldLabel);
        
        // 创建文本框
        textField = new JTextField(12); // 指定列数12
        // 添加文本框到面板panel1
        panel1.add(textField);
        
        // 创建标签
        JLabel lblPasswordLabel = new JLabel("Password:");
        // 添加标签到面板panel1
        panel1.add(lblPasswordLabel);
        
        // 创建密码框
        passwordField = new JPasswordField(12); // 指定列数12
        // 添加密码框到面板panel1
        panel1.add(passwordField);
        
        // 创建一个面板panel2放置TextArea
        JPanel panel2 = new JPanel();
        getContentPane().add(panel2, BorderLayout.SOUTH);
        
        // 创建标签
        JLabel lblTextAreaLabel = new JLabel("TextArea:");
        // 添加标签面板panel2
        panel2.add(lblTextAreaLabel);
        
        // 创建文本区
        JTextArea textArea = new JTextArea(3, 20); // 指定行数3,列数20
        // 添加文本区到面板panel2
        panel2.add(textArea);
        
        // 设置窗口大小
        pack(); // 紧凑排列(将容器中所有组件刚好包裹),其作用相当于setSize()
        
        // 设置窗口可见
        setVisible(true);
        
        textField.addActionListener((event)->{
            textArea.setText("在文本框上按下Enter键");
        });
    }
}

第20、45行,面板(JPanel)是一种没有标题栏和边框的容器,经常用于嵌套布局。然后再将这两个面板,添加到内容视图中。

复选框和单选按钮

Swing中提供了用于多选和单选功能的组件。

多选组件是复选框(JCheckBox),有时也单独使用,能提供两种状态的开和关。
单选组件是单选按钮(JRadioButton),同一组的多个单选按钮应该具有互斥特性,又名收音机按钮(RadioButton),一个按钮按下时,其他按钮一定抬起。同一组多个单选按钮应该放到同一个ButtonGroup对象,ButtonGroup对象不属于容器,它会创建一个互斥作用范围。

JCheckBox构造方法:
JCheckBox() 创建一个没有文本、没有图标并且最初未被选定的复选框对象。
JCheckBox(Icon icon) 创建有一个图标、最初未被选定的复选框对象。
JCheckBox(Icon icon, boolean selected) 创建一个带图标的复选框对象,并指定其最初是否处于选定状态。
JCheckBox(String text) 创建一个带文本的、最初未被选定的复选框对象。
JCheckBox(String text, boolean selected) 创建一个带文本的复选框对象,并指定其最初是否处于选定状态。
JCheckBox(String text, Icon icon) 创建带有指定文本和图标的、最初未被选定的复选框对象。
JCheckBox(String text, Icon icon, boolean selected) 创建一个带文本和图标的复选框对象,并指定其最初是否处于选定状态。

JCheckBox和JRadioButton它们有着相同的父类JToggleButton,有着相同方法和类似的构造方法。

示例:界面中有一组复选框和一组单选按钮。

public class MyFrame extends JFrame implements ItemListener {
    
    // 声明并创建RadioButton对象
    private JRadioButton radioButton1 = new JRadioButton("男");
    private JRadioButton radioButton2 = new JRadioButton("女");
    
    public MyFrame(String title) {
        super(title);
        
        // 设置布局管理BorderLayout
        getContentPane().setLayout(new BorderLayout());
        
        // 创建一个面板panel1放置TextField和Password
        JPanel panel1 = new JPanel();
        FlowLayout flowLayout_1 = (FlowLayout) panel1.getLayout();
        flowLayout_1.setAlignment(FlowLayout.LEFT);
        // 将面板panel1添加到内容视图
        getContentPane().add(panel1, BorderLayout.NORTH);
        
        // 创建标签
        JLabel lblTextFieldLabel = new JLabel("选择你喜欢的编程语言:");
        // 添加标签到面板panel1
        panel1.add(lblTextFieldLabel);
        
        JCheckBox checkBox1 = new JCheckBox("Java");
        panel1.add(checkBox1);
        
        JCheckBox checkBox2 = new JCheckBox("C++");
        panel1.add(checkBox2);
        
        JCheckBox checkBox3 = new JCheckBox("Objective-C");
        // 注册checkBox3对ActionLEvent事件监听
        checkBox3.addActionListener((event) -> {
            // 打印checkBox3状态
            System.out.println(checkBox3.isSelected());
        });
        panel1.add(checkBox3);
        
        // 创建一个面板panel2放置TextArea
        JPanel panel2 = new JPanel();
        FlowLayout flowLayout = (FlowLayout) panel2.getLayout();
        flowLayout.setAlignment(FlowLayout.LEFT);
        getContentPane().add(panel2, BorderLayout.SOUTH);
        
        // 创建标签
        JLabel lblTextAreaLabel = new JLabel("选择性别:");
        // 添加标签到面板panel2
        panel2.add(lblTextAreaLabel);
        
        // 创建ButtonGroup对象
        ButtonGroup buttonGroup = new ButtonGroup();
        // 添加RadioButton到ButtonGroup对象
        buttonGroup.add(radioButton1);
        buttonGroup.add(radioButton2);
        
        // 添加RadioButton到面板panel2
        panel2.add(radioButton1);
        panel2.add(radioButton2);
        
        //注册ItemEvent事件监听器
        radioButton1.addItemListener(this);
        radioButton2.addItemListener(this);
        
        // 设置窗口大小
        pack(); // 紧凑排列,其作用相当于setSize()
        
        // 设置窗口可见
        setVisible(true);
    }
    
    // 实现ItemListener接口方法
    @Override
    public void itemStateChanged(ItemEvent) {
        if (e.getStateChange() == ItemEvent.SELECTED) {
            JRadioButton button = (JRadioButton) e.getItem();
            System.out.println(button.getText());
        }
    }
}

第4、5行创建了两个单选按钮对象,为了能让这两个单选按钮互斥,需要把它们添加到一个ButtonGroup对象(51行),并把它们添加进来。为了监听两单选按钮的选择状态,注册ItemEvent事件监听器(61、62行),为了一起处理两个单选按钮事件,它们需要使用同一个事件处理者,本例是this,这说明当前窗口是事件处理者,它实现了ItemListener接口,见代码第1行代码第73行实现了ItemListener接口的抽象方法。两个单选按钮使用同一个事件处理者,第74行是判断按钮是否被选中,如果选中通过e.getItem() 方法获得按钮引用,然后再通过getText()方法获得按钮的文本标签。

第25行是创建了一个复选框对象,并且应该把它添加到面板panel1中。复选框和单选按钮都属于按钮,也能响应ActionEvent事件,代码第33行是注册checkBox3对ActionLEvent事件监听。

下拉列表

Swing中提供了下拉列表(JComboBox)组件,每次只能选择其中的一项。

JComboBox构造方法:
JComboBox() 创建一个下拉列表对象。
JComboBox(Object [] items) 创建一个下拉列表对象,items设置下拉列表中选项。下拉列表中选项内容可以是任意类,而不再局限于String。

public class MyFrame extends JFrame {
    
    // 声明下拉列表JComboBox
    private JComboBox choice1;
    private JComboBox choice2;
    
    private String[] s1 = { "Java", "C++", "Objective-C" };
    private String[] s2 = { "男", "女" };
    
    public MyFrame(String title) {
        super(title);
        
        getContentPane().setLayout(new GridLayout(2, 2, 0, 0));
        
        // 创建标签
        JLabel lblTextFieldLabel = new JLabel("选择你喜欢的编程语言:");
        lblTextFieldLabel.setHorizontalAlignment(SwingConstants.RIGHT);
        getContentPane().add(lblTextFieldLabel);
        
        // 实例化JComboBox对象
        choice1 = new JComboBox(s1);
        // 注册Action事件监听器,采用Lambda表达式
        choice1.addActionListener(e -> {
            JComboBox cb = (JComboBox) e.getSource(); // 通过e事件参数获得事件源
            // 获得选择的项目
            String itemString = (String) cb.getSelectedItem();
            System.out.println(itemString);
        });
        
        getContentPane().add(choice1);
        
        // 创建标签
        JLabel lblTextAreaLabel = new JLabel("选择性别:");
        lblTextAreaLabel.setHorizontalAlignment(SwingConstants.RIGHT);
        getContentPane().add(lblTextAreaLabel);
        
        // 实例化JComboBox对象,采用Lambda表达式
        choice2 = new JComboBox(s2);
        // 注册项目选择事件侦听器
        choice2.addItemListener(e -> {
            // 项目选择
            if (e.getStateChange() == ItemEvent.SELECTED) {
                // 获得选择的项目
                String itemString = (String) e.getItem(); // 从e事件参数中取出项目对象
                System.out.println(itemString);
            }
        });
        getContentPane().add(choice2);
        
        // 设置窗口大小
        setSize(400, 150);
        
        // 设置窗口可见
        setVisible(true);
    }
}

下拉列表组件在进行事件处理时,可以注册两种事件监听器:ActionListener和ItemListener,这两个监听器都只有一 个抽象方法需要实现,因此可以采用Lambda表达式作为事件处理者。

列表

Swing中提供了列表(JList)组件,可以单选或多选。

JList构造方法:
JList() 创建一个列表对象。
JList(Object [] listData) 创建一个列表对象,listData设置列表中选项。列表中选项内容可以是任意类,而不再局限于String。

public class MyFrame extends JFrame {
    
    private String[] s1 = { "Java", "C++", "Objective-C" };
    
    public MyFrame(String title) {
        super(title);
        // 创建标签
        JLabel lblTextFieldLabel = new JLabel("选择你喜欢的编程语言:");
        getContentPane().add(lblTextFieldLabel, BorderLayout.NORTH);
        
        // 列表组件JList
        JList list1 = new JList(s1);
        list1.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); // 设置列表为单选
        // 注册项目选择事件监听器,采用Lambda表达式
        list1.addListSelectionListener(e -> {
            if (e.getValueIsAdjusting() == false) { // false鼠标释放,true鼠标按下
                // 获得选择的内容
                String itemString = (String) list1.getSelectedValue();
                System.out.println(itemString);
            }
        });
        getContentPane().add(list1);
        
        // 设置窗口大小
        setSize(300, 200);
        // 设置窗口可见
        setVisible(true);
    }
}

18行取出getSelectedValue()获得选中的项目值,如果是多选时可以通过getSelectedValues()获得选中的项目值。

分隔面板

Swing中提供了一种分隔面板(JSplitPane)组件,可以将屏幕分成左右或上下两部分。

JSplitPane构造方法:
JSplitPane(int newOrientation) 创建一个分隔面板,参数newOrientation指定布局方向,newOrientation取值是JSplitPane.HORIZONTAL_SPLIT(水平)或JSplitPane.VERTICAL_SPLIT(垂直)。
JSplitPane(int newOrientation, Component newLeftComponent, Component newRightComponent) 创建一个分隔面板,参数newOrientation指定布局方向,newLeftComponent指定左侧面板组件, newRightComponent指定右侧面板组件。

示例:界面分左右两部分,左边有列表组件, 选中列表项目时右边会显示相应的图片。

public class MyFrame extends JFrame {
    
    private String[] data = { "bird1.gif", "bird2.gif", "bird3.gif",                 "bird4.gif", "bird5.gif", "bird6.gif" };
    
    public MyFrame(String title) {
        super(title);
        // 右边面板
        JPanel rightPane = new JPanel();
        rightPane.setLayout(new BorderLayout(0, 0));
        JLabel lblImage = new JLabel();
        lblImage.setHorizontalAlignment(SwingConstants.CENTER);
        rightPane.add(lblImage, BorderLayout.CENTER);
        
        // 左边面板
        JPanel leftPane = new JPanel();
        leftPane.setLayout(new BorderLayout(0, 0));
        JLabel lblTextFieldLabel = new JLabel("选择鸟儿:");
        leftPane.add(lblTextFieldLabel, BorderLayout.NORTH);
        
        // 列表组件JList
        JList list1 = new JList(data);
        list1.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
        // 注册项目选择事件监听器,采用Lambda表达式
        list1.addListSelectionListener(e -> {
            if (e.getValueIsAdjusting() == false) {
                // 获得选择的内容
                String itemString = (String) list1.getSelectedValue();
                // 获得图片的相对路径
                String petImage = String.format("/images/%s", itemString);
                // 创建图片ImageIcon对象
                // MyFrame.class.getResource(petImage)获取资源图片的绝对路径
                Icon icon = new ImageIcon(MyFrame.class.getResource(petImage));
                lblImage.setIcon(icon);
            }
        });
        leftPane.add(list1, BorderLayout.CENTER);
        
        // 分隔面板
        JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT,                                                     leftPane, rightPane);
        splitPane.setDividerLocation(100); // 设置分隔条位置
        
        // 将分隔面板添加到内容面板中
        getContentPane().add(splitPane, BorderLayout.CENTER);
        
        // 设置窗口大小
        setSize(300, 200);
        // 设置窗口可见
        setVisible(true);
    }
}

(32行)资源文件是放在字节码文件夹中的文件,可通过XXX.class.getResource()方法获得它的运行时绝对路径。

表格

当有大量数据需要展示时,可以使用二维表格,有时也可以使用表格修改数据。表格是非常重要的组件。
Swing提供了表格组件JTable类,但是表格组件比较复杂,它的表现形式与数据是分离的,Swing的很多组件都是按照MVC设计模式进行设计的,JTable最有代表性。
按照MVC设计理念JTable属于视图,对应的模型是javax.swing.table.TableModel接口,根据自己的业务逻辑和数据实现TableModel接口实现类。TableModel接口要求实现所有抽象方法,使用起来比较麻烦,有时只是使用很简单的表格, 此时可以使用AbstractTableModel抽象类。实际开发时需要继承AbstractTableModel抽象类。

MVC是一种设计理念,将一个应用分为:模型(Model)、视图(View)和控制器(Controller),它将业务逻辑、数据、界面表示进行分离的方法组织代码,界面表示的变化不会影响到业务逻辑组件,不需要重新编写业务逻辑。

JTable构造方法:
JTable(TableModel dm) 通过模型创建表格,dm是模型对象,其中包含了表格要显示的数据。
JTable(Object[][] rowData, Object[] columnNames) 通过二维数组和指定列名,创建一个表格对象,rowData是表格中的数据,columnNames是列名。
JTable(int numRows, int numColumns) 指定行和列数创建一个空的表格对象。

先介绍通过二维数组和列名实现表格。这种方式创建表格不需要模型,实现起来比较简单。但是表格只能接受二维数组作为数据。

public class MyFrameTable extends JFrame {
    
    // 获得当前屏幕的宽高
    private double screenWidth = Toolkit.getDefaultToolkit().
        getScreenSize().getWidth();
    private double screenHeight = Toolkit.getDefaultToolkit().
        getScreenSize().getHeight();
    
    private JTable table;
    
    public MyFrameTable(String title) {
        super(title);
        table = new JTable(rowData, columnNames);
        // 设置表中内容字体
        table.setFont(new Font("微软雅黑", Font.PLAIN, 16));
        // 设置表列标题字体
        table.getTableHeader().setFont(new Font("微软雅黑", Font.BOLD, 16));
        // 设置表行高
        table.setRowHeight(40);
        // 设置为单行选中模式
        table.setSelectionMode(javax.swing.ListSelectionModel.SINGLE_SELECTION);
        // 返回当前行的状态模型
        ListSelectionModel rowSM = table.getSelectionModel();
        
        // 注册侦听器,选中行发生更改时触发
        rowSM.addListSelectionListener(new ListSelectionListener() {
            public void valueChanged(ListSelectionEvent e) {
                // 只处理鼠标按下
                if (e.getValueIsAdjusting() == false) {
                    return;
                }
                ListSelectionModel lsm = (ListSelectionModel) e.getSource();
                if (lsm.isSelectionEmpty()) {
                    System.out.println("没有选中行");
                } else {
                    int selectedRow = lsm.getMinSelectionIndex();
                    System.out.println("第" + selectedRow + "行被选中");
                }
            }
        });
        
        JScrollPane scrollPane = new JScrollPane();
        scrollPane.setViewportView(table);
        getContentPane().add(scrollPane, BorderLayout.CENTER);
        
        // 设置窗口大小
        setSize(960, 640);
        // 计算窗口位于屏幕中心的坐标
        int x = (int) (screenWidth - 960) / 2; // (屏幕宽度-窗口宽度) / 2
        int y = (int) (screenHeight - 640) / 2;
        // 设置窗口位于屏幕中心
        setLocation(x, y);
        
        // 设置窗口可见
        setVisible(true);
    }
    
    // 表格列标题
    String[] columnNames = { "书籍编号", "书籍名称", "作者", "出版社", "出版日期", "库存数量" };
    // 表格数据
    Object[][] rowData = {{ "0036", "高等数学", "李放", "人民邮电出版社", "20000812", 1}, 
                       { "0004", "FLASH精选", "刘扬", "中国纺织出版社", "19990312", 2 },
                       { "0026", "软件工程", "牛田", "经济科学出版社", "20000328", 4 },
                       { "0015", "人工智能", "周未", "机械工业出版社", "19991223", 3 },
                       { "0037", "南方周末", "邓光明", "南方出版社", "20000923", 3 },
                          ...,
                       { "0032", "SOL使用手册", "贺民", "电子工业出版社", "19990425", 2 } 	};
}

第26~40行注册事件监听器,监听器当行选择变化时触发。由于ListSelectionListener接口虽然不是函数式接口,但只有一个方法,所以可以使用Lambda表达式实现该接口:

// 也可换成Lambda表达式
rowSM.addListSelectionListener(e -> {
    ListSelectionModel lsm = (ListSelectionModel) e.getSource();
    if (lsm.isSelectionEmpty()) {
        System.out.println("没有选中行");
    } else {
        int selectedRow = lsm.getMinSelectionIndex();
        System.out.println("第" + selectedRow + "行被选中");
    }
});

表格一般都会放到一个滚动面板(JScrollPane)中,这可以保证数据很多超出屏幕时,能够出现滚动条。把表格添加到滚动面板并不是使用add()方法,而是使用第43行的 scrollPane.setViewportView(table)语句。滚动面板是非常特殊的面板,它管理着一个视口或窗口,当里面的内容超出视口会出现滚动条,setViewportView()方法可以设置一个容器或组件作为滚动面板的视口。

案例:图书库存

在进行数据库设计时,数据库中每一个表对应Java一个实体类,实体类是系统的“人”、“事”、“物”等一些名词。
图书(Book)就是一个实体类了,实体类Book代码:

// 图书实体类
public class Book {
    
    // 图书编号
    private String bookid;
    // 图书名称
    private String bookname;
    // 图书作者
    private String author;
    // 出版社
    private String publisher;
    // 出版日期
    private String pubtime;
    // 库存数量
    private int inventory;
    
    public String getBookid() {
        return bookid;
    }
    public void setBookid(String bookid) {
        this.bookid = bookid;
    }
    public String getBookname() {
        return bookname;
    }
    public void setBookname(String bookname) {
        this.bookname = bookname;
    }
    public String getAuthor() {
        return author;
    }
    public void setAuthor(String author) {
        this.author = author
    }
    
    // ...
    // 省略Getter和Setter方法
}

实体类有很多私有属性(成员变量),为了在类外部能够访问它们,一般都会提供公有的Getter和Setter方法。

本例表格中的数据是从JSON文件Books.json中读取的,Books.json位于项目的db目录中,JSON文件Books.json的内容:

[{"bookid":"0036","bookname":"高等数学","author":"李放","publisher":"人民邮电出版社","pubtime":"20000812","inventory":1},
  {"bookid":"0004","bookname":"FLASH精选","author":"刘扬","publisher":"中国纺织出版社","pubtime":"19990312","inventory":2},
  ...
  {"bookid":"0005","bookname":"java基础","author":"王一","publisher":"电子工业出版社","pubtime":"19990528","inventory":3},
  {"bookid":"0032","bookname":"SOL使用手册","author":"贺民","publisher":"电子工业出版社","pubtime":"19990425","inventory":2}]
public class HelloWorld {
    
    public static void main(String[] args) {
        List<Book> data = readData();
        new MyFrameTable("图书库存", data); 
    }
    
    // 从文件中读取数据
    private static List<Book> readData() {
        // 返回的数据列表
        List<Book> list = new ArrayList<Book>();
        // 数据文件
        String dbFile = "./db/Books.json";
        
        try (FileInputStream fis = new FileInputStream(dbFile);
             InputStreamReader ir = new InputStreamReader(fis);
             BufferedReader in = new BufferedReader(ir)) {
            
            // 1.读取文件
            StringBuilder sbuilder = new StringBuilder();
            String line = in.readLine();
            
            while (line != null) {
                sbuilder.append(line);
                line = in.readLine();
            }
            
            // 2.JSON解码
            // 读取JSON字符完成
            System.out.println("读取JSON字符完成...");
            // JSON解码,解码成功返回JSON数组
            JSONArray jsonArray = new JSONArray(sbuilder.toString());
            System.out.println("JSON解码成功完成...");
            
            // 3.将JSON数组放到List<Book>集合中
            // 遍历集合
            for (Object item : jsonArray) {
                JSONObject row = (JSONObject) item;
                Book book = new Book();
                book.setBookid((String) row.get("bookid"));
                book.setBookname((String) row.get("bookname"));
                book.setAuthor((String) row.get("author"));
                book.setPublisher((String) row.get("publisher"));
                book.setPubtime((String) row.get("pubtime"));
                book.setInventory((Integer) row.get("inventory"));
                list.add(book);
            }
        } catch (Exception e) {
        }
        return list;
    }
}
  1. 读取文件。通过Java IO取得文件./db/Books.json,每次读取的字符串保存到StringBuilder的sbuilder对象中。文件读完sbuilder中就是全部的JSON字符串。
  2. JSON解码:读取JSON字符完成后,需要对其进行解码。由于JSON字符串是数组结构,因此解码时候使用JSONArray,创建JSONArray对象过程就是对字符串进行解码的过程,如果没有发生异常,说明成功解码。
  3. 将JSON数组放到List<Book>集合中。本例表格使用的数据格式不是JSON数组形式,而是 List<Book>,这种结构就是List集合中每一个元素都是Book类型。这个过程需要遍历JSON数组,把数据重新组装到Book对象中。

模型BookTableModel:

import java.util.List;
import javax.swing.table.AbstractTableModel;

public class BookTableModel extends AbstractTableModel {
    
    // 列名数组
    private String[] columnNames = { "书籍编号", "书籍名称", "作者", "出版社", "出版日期", "库存数量" };
    
    // data保存了表格中数据,data类型是List集合
    private List<Book> data = null;
    
    public BookTableModel(List<Book> data) {
        this.data = data;
    }
    
    // 获得列数
    @Override
    public int getColumnCount() {
        return columnNames.length;
    }
    
    // 获得行数
    @Override
    public int getRowCount() {
        return data.size();
    }
    
    // 获得某行某列的数据
    @Override
    public Object getValueAt(int row, int col) {
        Book book = (Book) data.get(row);
        switch (col) {
            case 0:
                return book.getBookid();
            case 1:
                return book.getBookname();
            case 2:
                return book.getAuthor();
            case 3:
                return book.getPublisher();
            case 4:
                return book.getPubtime();
            case 5:
                return new Integer(book.getInventory());
        }
        return null;
    }
    
    // 获得某列的名字
    @Override
    public String getColumnName(int col) {
        return columnNames[col];
    }
}

抽象类AbstractTableModel要求必须实现getColumnCount()(提供表格列数)、getRowCount()(提供表格行数)和getValueAt()(指定行和列时单元格内容)三个抽象方法。第51行的getColumnName()方法不是抽象类要求实现的方法,重写该方法能够给表格提供有意义的列名。

窗口代码:

public class MyFrameTable extends JFrame {
    
    // 获得当前屏幕的宽高
    private double screenWidth = Toolkit.getDefaultToolkit().
        getScreenSize().getWidth();
    private double screenHeight = Toolkit.getDefaultToolkit().
        getScreenSize().getHeight();
    
    private JTable table;
    // 图书列表
    private List<Book> data;
    
    public MyFrameTable(String title, List<Book> data) {
        super(title);
        
        this.data = data;
        TableModel model = new BookTableModel(data);
        
        table = new JTable(model);
        // 设置表中内容字体
        table.setFont(new Font("微软雅黑", Font.PLAIN, 16));
        // 设置表列标题字体
        table.getTableHeader().setFont(new Font("微软雅黑", Font.BOLD, 16));
        // 设置表行高
        table.setRowHeight(40);
        // 设置为单行选中模式
        table.setSelectionMode(javax.swing.ListSelectionModel.SINGLE_SELECTION);
        // 返回当前行的状态模型
        ListSelectionModel rowSM = table.getSelectionModel();
        // 注册侦听器,选中行发生更改时触发
        rowSM.addListSelectionListener(e -> {
            // 只处理鼠标按下
            if (e.getValueIsAdjusting() == false) {
                return;
            }
            ListSelectionModel lsm = (ListSelectionModel) e.getSource();
            if (lsm.isSelectionEmpty()) {
                System.out.println("没有选中行");
            } else {
                int selectedRow = lsm.getMinSelectionIndex();
                System.out.println("第" + selectedRow + "行被选中");
            }
        });
        
        JScrollPane scrollPane = new JScrollPane();
        scrollPane.setViewportView(table);
        getContentPane().add(scrollPane, BorderLayout.CENTER);
        
        // 设置窗口大小
        setSize(960, 640);
        // 计算窗口位于屏幕中心的坐标
        int x = (int) (screenWidth - 960) / 2;
        int y = (int) (screenHeight - 640) / 2;
        // 设置窗口位于屏幕中心
        setLocation(x, y);
        // 设置窗口可见
        setVisible(true);
    }
}