12.9.4 使用ListCellRenderer改变列表项外观

前面程序中的JListJComboBox采用的都是简单的字符串列表项,实际上,JListJComboBox还可以支持图标列表项,如果在创建JListJComboBox时传入图标数组,则创建的JListJCombobox的列表项就是图标。
如果希望列表项是更复杂的组件,例如,希望像QQ程序那样每个列表项既有图标,也有字符串,那么可以通过调用JListsetCellRenderer方法来实现,该方法需要接收一个ListCellRenderer对象,该对象代表一个列表项绘制器

方法 描述
void setCellRenderer(ListCellRenderer<? super E> cellRenderer) Sets the delegate that is used to paint each cell in the list.

ListCellRenderer接口

ListCellRenderer是一个接口,该接口里包含一个方法:

方法 描述
Component getListCellRendererComponent(JList<? extends E> list, E value, int index, boolean isSelected, boolean cellHasFocus) Return a component that has been configured to display the specified value.

上面的getListCellRendererComponent()方法返回一个Component组件,该组件就代表了JListJComboBox的每个列表项

自定义绘制JListJComboBox的列表项所用的方法相同,所用的列表项绘制器也相同,故本节以JList为例

ListCellRenderer只是一个接口,它并未强制指定列表项绘制器属于哪种组件,因此可扩展任何组件来实现ListCellRenderer接口。通常采用扩展其他容器(如JPanel)的方式来实现列表项绘制器,实现列表项绘制器时可通过重写paintComponent()的方法来改变单元格的外观行为。

程序 列表项绘制器

例如下面程序,重写paintComponent方法时先绘制好友图像,再绘制好友名字。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
import java.awt.*;
import javax.swing.*;

public class ListRenderingTest
{
private JFrame mainWin = new JFrame("好友列表");
private String[] friends = new String[]
{
"李清照",
"苏格拉底",
"李白",
"弄玉",
"虎头"
};
// 定义一个JList对象
private JList<String> friendsList = new JList<>(friends);
public void init()
{
// 设置该JList使用ImageCellRenderer作为列表项绘制器
friendsList.setCellRenderer(new ImageCellRenderer());
mainWin.add(new JScrollPane(friendsList));
mainWin.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
mainWin.pack();
mainWin.setVisible(true);
}
public static void main(String[] args)
{
new ListRenderingTest().init();
}
}
class ImageCellRenderer extends JPanel
implements ListCellRenderer<String>
{
private ImageIcon icon;
private String name;
// 定义绘制单元格时的背景色
private Color background;
// 定义绘制单元格时的前景色
private Color foreground;
public Component getListCellRendererComponent(JList list
, String value , int index
, boolean isSelected , boolean cellHasFocus)
{
icon = new ImageIcon("ico/" + value + ".gif");
name = value;
background = isSelected ? list.getSelectionBackground()
: list.getBackground();
foreground = isSelected ? list.getSelectionForeground()
: list.getForeground();
// 返回该JPanel对象作为列表项绘制器
return this;
}
// 重写paintComponent方法,改变JPanel的外观
public void paintComponent(Graphics g)
{
int imageWidth = icon.getImage().getWidth(null);
int imageHeight = icon.getImage().getHeight(null);
g.setColor(background);
g.fillRect(0, 0, getWidth(), getHeight());
g.setColor(foreground);
// 绘制好友图标
g.drawImage(icon.getImage() , getWidth() / 2
- imageWidth / 2 , 10 , null);
g.setFont(new Font("SansSerif" , Font.BOLD , 18));
// 绘制好友用户名
g.drawString(name, getWidth() / 2
- name.length() * 10 , imageHeight + 30 );
}
// 通过该方法来设置该ImageCellRenderer的最佳大小
public Dimension getPreferredSize()
{
return new Dimension(60, 80);
}
}

上面程序中指定了该JList对象使用ImageCellRenderer作为列表项绘制器,ImageCellRenderer重写了paintComponent()方法来绘制单元格内容。除此之外,ImageCellRenderer还重写了getPreferredSize()方法,该方法返回一个Dimension对象,用于描述该列表项绘制器的最佳大小。
运行上面程序,会看到如图12.37所示的窗口。

定义列表项绘制器有什么用

通过使用自定义的列表项绘制器,可以让JListJComboBox的列表项是任意组件,并且可以在该组件上任意添加内容

12.9.3 强制存储列表项的DefaultListModel和DefaultComboBoxModel

前面只是介绍了如何创建JListJComboBox对象,当调用JListJComboBox构造器时传入数组或Vector作为参数,这些数组元素或集合元素将会作为列表项。当使用JListJComboBox时常常还需要动态地增加、删除列表项。

JComboBox列表项的增加 插入 删除

对于JComboBox类,它提供了如下几个方法来增加、插入和删除列表项:

方法 描述
void addItem(E item) JComboBox中的添加一个列表项。
void insertItemAt(E item, int index) JComboBox的指定索引处插入一个列表项。
void removeAllItems() 删除JComboBox中的所有列表项。
void removeItem(Object anObject) 删除JComboBox中的指定列表项。
void removeItemAt(int anIndex) 删除JComboBox指定索引处的列表项

上面这些方法的参数类型是E,这是由于Java7JComboBoxJListListModel都増加了泛型支持,这些接口都有形如JComboBox<E>JList<E>ListModel<E>的泛型声明,因此它们里面的方法可使用E作为参数或返回值的类型
通过这些方法就可以增加、插入和删除JComboBox中的列表项。

JList没有提供增加 删除列表项的方法

JList并没有提供这些类似的方法。实际上,对于直接通过数组或Vector创建的JList对象,则很难向该JList中添加或删除列表项。

如果需要创建一个可以增加、删除列表项的JList对象,则应该在创建JList时显式使用DefaultListModel作为构造参数:

方法 描述
JList(ListModel<E> dataModel) Constructs a JList that displays elements from the specified, non-null, model.

通过DefaultListModel来实现向JList中添加 插入 删除元素

因为DefaultListModel作为JListModel,它负责维护JList组件的所有列表数据,所以可以通过向DefaultListModel中添加、删除元素来实现向JList对象中增加、删除列表项

DefaultListModel提供了如下几个方法来添加、插入 删除元素。

方法 描述
void add(int index, E element) 在该ListModel的指定位置处插入指定元素。
void addElement(E element) 将指定元素添加到该ListModel的末尾。
void insertElementAt(E element, int index) 在该ListModel的指定位置处插入指定元素
E remove(int index) 删除该ListModel中指定位置处的元素。
void removeAllElements() 删除该ListModel中的所有元素,并将其的大小设置为零。
boolean removeElement(Object obj) 删除该ListModel中第一个与参数匹配的元素。
void removeElementAt(int index) 删除该ListModel中指定索引处的元素。

上面这些方法有些功能是重复的,这是由于Java的历史原因造成的。如果通过DefaultListModel来创建JList组件,则就可以通过调用上面的这些方法来添加、删除DefaultListModel中的元素,从而实现对JList里列表项的增加、删除。下面程序示范了如何向JList中添加、删除列表项。

程序 使用DefaultListModel给JList 增加 插入 删除 列表项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
import java.awt.BorderLayout;
import javax.swing.*;

public class DefaultListModelTest {
private JFrame mainWin = new JFrame("测试DefaultListModel");
// 定义一个JList对象
private JList<String> bookList;
// 定义一个DefaultListModel对象
private DefaultListModel<String> bookModel = new DefaultListModel<>();
private JTextField bookName = new JTextField(20);
private JButton removeBn = new JButton("删除选中图书");
private JButton addBn = new JButton("添加指定图书");

public void init() {
// 向bookModel中添加元素
bookModel.addElement("疯狂Java讲义");
bookModel.addElement("轻量级Java EE企业应用实战");
bookModel.addElement("疯狂Android讲义");
bookModel.addElement("疯狂Ajax讲义");
bookModel.addElement("经典Java EE企业应用实战");
// 根据DefaultListModel对象创建一个JList对象
bookList = new JList<>(bookModel);
// 设置最大可视高度
bookList.setVisibleRowCount(4);
// 只能单选
bookList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
// 为添加按钮添加事件监听器
addBn.addActionListener(evt -> {
// 当bookName文本框的内容不为空。
if (!bookName.getText().trim().equals("")) {
// 向bookModel中添加一个元素,
// 系统自动会向JList中添加对应的列表项
bookModel.addElement(bookName.getText());
}
});
// 为删除按钮添加事件监听器
removeBn.addActionListener(evt -> {
// 如果用户已经选中的一项
if (bookList.getSelectedIndex() >= 0) {
// 从bookModel中删除指定索引处的元素,
// 系统自动会删除JList对应的列表项
bookModel.removeElementAt(bookList.getSelectedIndex());
}
});
JPanel p = new JPanel();
p.add(bookName);
p.add(addBn);
p.add(removeBn);
// 添加bookList组件
mainWin.add(new JScrollPane(bookList));
// 将p面板添加到窗口中
mainWin.add(p, BorderLayout.SOUTH);
mainWin.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
mainWin.pack();
mainWin.setVisible(true);
}

public static void main(String[] args) {
new DefaultListModelTest().init();
}
}

上面程序中通过一个DefaultListModel创建了一个JList对象,然后在两个按钮的事件监听器中分别向DefaultListModel对象中添加、删除元素,从而实现了向JList对象中添加、删除列表项。
运行上面程序,会看到如图12.36所示的窗口。

为什么JComboBox提供添加删除列表项的方法 JList不提供

学生提问:为什么JComboBox提供了添加、删除列表项的方法?而JList没有提供添加、删除列表项的方法呢?
答:因为直接使用数组、Vector创建的JListJComboBox所对应的Model实规类不同。

  • 使用数组、Vector创建的JComboBoxModel类是DefaultComboBoxModel,
    • DefaultComboBoxModel是一个元素可变的集合类,所以使用数组、Vector创建的JComboBox可以直接派加、删除列表项,因此JComboBox提供了添加、删除列表项的方法;
  • 但使用数组、Vector创建的JList所对应的Model类分别是JList$1(JList的第一个匿名内部类)、JList$2(JList的第二个匿名内部类)
    • 这两个匿名内部类都是元素不可变的集合类,所以使用数组、Vector创建的JList不可以直接添加、删除列表项,因此JList没有提供添加、删除列表项的方法。
    • 如果想创建列表项可变的JList对象,则要显式使用DefaultListModel对象作为Model,而DefaultListModel才是元素可变的集合类,才可以直接通过修改DefaultListModel里的元素来改变JList里的列表项。

总结

DefaultListModelDefaultComboBoxModel是两个强制保存所有列表项的Model类,它们使用Vector来保存所有的列表项。
DefaultListModelTest程序中可以看出,DefaultListModel的用法和Vector的用法非常相似。实际上,DefaultListModelDefaultComboBoxModel从功能上来看,与一个Vector并没有太大的区别。如果要创建列表项可变的JList组件,使用DefaultListModel作为构造方法的参数即可

创建列表项可变的JComboBox组件,当然也可以显式使用DefaultComboBoxModel作为参数,但这并不是必需的,因为JComboBox已经默认使用DefaultComboBoxModel作为对应的model对象。

12.9.2 不强制存储列表项的ListModel和ComboBoxModel

正如前面提到的,Swing的绝大部分组件都采用了MVC的设计模式,其中JListJComboBox都只负责组件的外观显示,而组件底层的状态数据维护则由对应的Model负责。
JList对应的ModelListModel接口,
JCombobox对应的ModelComboBoxModel接口.

这两个接口负责维护JListJCombobox组件里的列表项。

ListModel接口

ListModel接口的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
package javax.swing;
import javax.swing.event.ListDataListener;
public interface ListModel<E>{
//返回列表项的数量
int getSize();
// 返回指定索引处的列表项
E getElementAt(int index);
// 为列表项添加一个监听器,当列表项发生变化时将触发该监听器
void addListDataListener(ListDataListener l);
// 删除列表项上的指定监听器
void removeListDataListener(ListDataListener l);
}

从上面接口来看,这个ListModel不管JList里的所有列表项的存储形式,它甚至不强制存储所有的列表项,只要ListModel的实现类提供了getSize()getElementAt()两个方法,JList就可以根据该ListModel对象来生成列表框

ComboBoxModel接口

ComboBoxModel继承了ListModel,它添加了“选择项”的概念,选择项代表JComboBox显示区域内可见的列表项。ComboBoxModel为“选择项”提供了两个方法,下面是ComboModel接口的代码。

1
2
3
4
5
6
7
package javax.swing;
public interface ComboBoxModel<E> extends ListModel<E> {
// 设置选中“选择项”
void setSelectedItem(Object anItem);
// 获取“选择项”的值
Object getSelectedItem();
}

创建ListModel实现类

因为ListModel不强制保存所有的列表项,因此可以为它创建一个实现类:NumberListModel,这个实现类只需要传入数字上限、数字下限和步长,程序就可以自动为之实现上面的getsize()方法和getElementAt()方法,从而允许直接使用一个数字范围来创建JList对象。

实现getSize()方法

实现getSize()方法的代码如下

1
2
3
public int getsize (){
return (int) Math.floor(end.subtract(start).divide(step).doublevalue())+ 1;
}

用“**(上限-下限)÷步长+1**”即得到该ListModel中包含的列表项的个数
程序使用BigDecimal变量来保存上限、下限和步长,而不是直接使用double变量来保存这三个属性,主要是为了实现对数值的精确计算,所以上面程序中的endstartstep都是BigDecimal类型的变量。

实现getElementAt方法

实现getElementAt()方法也很简单,“下限+步长×索引”就是指定索引处的元素,该方法的具体实现请参考ListModelTest.java

程序 创建ListModel的实现类 创建ComboBoxModel的实现类

下面程序为ListModel提供了NumberListModel实现类,并为ComboBoxModel提供了NumberComboBoxModel实现类,这两个实现类允许程序使用数值范围来创建JListJComboBox对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
import java.math.BigDecimal;
import java.util.*;
import java.awt.BorderLayout;
import javax.swing.*;

public class ListModelTest {
private JFrame mainWin = new JFrame("测试ListModel");
// 根据NumberListModel对象来创建一个JList对象
private JList<BigDecimal> numScopeList = new JList<>(new NumberListModel(1, 21, 2));
// 根据NumberComboBoxModel对象来创建JComboBox对象
private JComboBox<BigDecimal> numScopeSelector = new JComboBox<>(new NumberComboBoxModel(0.1, 1.2, 0.1));
private JTextField showVal = new JTextField(10);

public void init() {
// JList的可视高度可同时显示4个列表项
numScopeList.setVisibleRowCount(4);
// 默认选中第三项到第五项(第一项的索引是0)
numScopeList.setSelectionInterval(2, 4);
// 设置每个列表项具有指定的高度和宽度。
numScopeList.setFixedCellHeight(30);
numScopeList.setFixedCellWidth(90);
// 为numScopeList添加监听器
numScopeList.addListSelectionListener(e -> {
// 获取用户所选中的所有数字
List<BigDecimal> nums = numScopeList.getSelectedValuesList();
showVal.setText("");
// 把用户选中的数字添加到单行文本框中
for (BigDecimal num : nums) {
showVal.setText(showVal.getText() + num.toString() + ", ");
}
});
// 设置列表项的可视高度可显示5个列表项
numScopeSelector.setMaximumRowCount(5);
Box box = new Box(BoxLayout.X_AXIS);
box.add(new JScrollPane(numScopeList));
JPanel p = new JPanel();
p.add(numScopeSelector);
box.add(p);
// 为numScopeSelector添加监听器
numScopeSelector.addItemListener(e -> {
// 获取JComboBox中选中的数字
Object num = numScopeSelector.getSelectedItem();
showVal.setText(num.toString());
});
JPanel bottom = new JPanel();
bottom.add(new JLabel("您选择的值是:"));
bottom.add(showVal);
mainWin.add(box);
mainWin.add(bottom, BorderLayout.SOUTH);
mainWin.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
mainWin.pack();
mainWin.setVisible(true);
}

public static void main(String[] args) {
new ListModelTest().init();
// ComboBoxModel
}
}

class NumberListModel extends AbstractListModel<BigDecimal> {
protected BigDecimal start;
protected BigDecimal end;
protected BigDecimal step;

public NumberListModel(double start, double end, double step) {
this.start = BigDecimal.valueOf(start);
this.end = BigDecimal.valueOf(end);
this.step = BigDecimal.valueOf(step);
}

// 返回列表项的个数
public int getSize() {
return (int) Math.floor(end.subtract(start).divide(step).doubleValue()) + 1;
}

// 返回指定索引处的列表项
public BigDecimal getElementAt(int index) {
return BigDecimal.valueOf(index).multiply(step).add(start);
}
}

class NumberComboBoxModel extends NumberListModel implements ComboBoxModel<BigDecimal> {
// 用于保存用户选中项的索引
private int selectId = 0;

public NumberComboBoxModel(double start, double end, double step) {
super(start, end, step);
}

// 设置选中“选择项”
public void setSelectedItem(Object anItem) {
if (anItem instanceof BigDecimal) {
BigDecimal target = (BigDecimal) anItem;
// 根据选中的值来修改选中项的索引
selectId = target.subtract(super.start).divide(step).intValue();
}
}

// 获取“选择项”的值
public BigDecimal getSelectedItem() {
// 根据选中项的索引来取得选中项
return BigDecimal.valueOf(selectId).multiply(step).add(start);
}
}

上面程序中分别使用NumberListModelNumberComboBoxModel创建了一个JListJComboBox对象,创建这两个列表框时无须指定每个列表项,只需给出数值的上限、下限和步长即可。运行上面程序,会看到如图12.35所示的窗口。

12.9 使用JList,JComboBox创建列表框

无论从哪个角度来看,JListJComboBox都是极其相似的,它们都有一个列表框,只是JComboBox的列表框需要以下拉方式显示出来;JListJComboBox都可以通过调用setRenderer()方法来改变列表项的表现形式。甚至维护这两个组件的Model都是相似的,JList使用ListModel,JComboBox使用ComboBoxModel,而ComboBoxModelListModel的子类。

12.9.1 简单列表框

如果仅仅希望创建一个简单的列表框(包括JListJComboBox),则直接使用它们的构造器即可,它们的构造器都可接收一个对象数组或元素类型任意的Vector作为参数,这个对象数组或元素类型任意的Vector里的所有元素将转换为列表框的列表项。

创建简单列表框步骤

使用JLitJComboBox来创建简单列表框非常简单,只需要按如下步骤进行即可

创建列表框对象

  1. 使用JList或者JComboBox的构造器创建一个列表框对象,创建JListJComboBox时,应该传入一个Vector对象或者Object数组作为构造器参数,其中使用JCombobox创建的列表框必须单击右边的向下箭头才会出现。

设置列表框的外观行为

  1. 调用JListJComboBox的各种方法来设置列表框的外观行为,其中

JList常用设置外观的方法

JList可以调用如下几个常用的方法。

方法 描述
void addSelectionInterval(int anchor, int lead) 在已经选中列表项的基础上增加选中从anchorlead索引范围内的所有列表项。
void setFixedCellHeight(int height) 设置列表项的高度
void setFixedCellWidth(int width) 设置列表项的宽度
void setLayoutOrientation(int layoutOrientation) 设置列表框的布局方向,该属性可以接收三个值,即JList.HORIZONTAL_WRAPJList.VERTICAL_WRAPJList.VERTICAL(默认),用于指定当列表框长度不足以显示所有的列表项时,列表框如何排列所有的列表项。
void setSelectedIndex(int index) 设置默认选择哪一个列表项
void setSelectedIndices(int[] indices) 设置默认选择哪一批列表项
void setSelectedValue(Object anObject, boolean shouldScroll) 设置选中哪个列表项的值,第二个参数决定是否滚动到选中项。
void setSelectionBackground(Color selectionBackground) 设置选中项的背景色。
void setSelectionForeground(Color selectionForeground) 设置选中项的前景色。
void setSelectionInterval(int anchor, int lead) 设置选中从anchorlead索引范围内的所有列表项
void setSelectionMode(int selectionMode) 设置选中模式。支持如下3个值。
  • ListSelectionModel.SINGLE_SELECTION:每次只能选择一个列表项。在这种模式中setSelectionIntervaladdSelectionInterval是等效的
  • ListSelectionModel.SINGLE_INTERVAL_SELECTION:每次只能选择一个连续区域。在此模式中,如果需要添加的区域没有与已选择区域相邻或重叠,则不能添加该区域。简而言之,在这种模式下每次可以选择多个列表项,但多个列表项必须处于连续状态。
  • ListSelectionModel.MULTIPLE_INTERVAL_SELECTION:在此模式中,选择没有任何限制。该模式是默认设置
void setVisibleRowCount(int visibleRowCount) 设置该列表框的可视高度足以显示多少项

JComboBox常用设置外观的方法

JComboBox则提供了如下几个常用方法。

方法 描述
void setEditable(boolean aFlag) 设置是否允许直接修改JComboBox文本框的值,默认不允许
void setMaximumRowCount(int count) 设置下拉列表框的可视高度可以显示多少个列表项
void setSelectedIndex(int anIndex) 根据索引设置默认选中哪一个列表项。
void setSelectedItem(Object anObject) 根据列表项的值设置默认选中哪一个列表项

JComboBox没有设置选择模式的方法,因为JComboBox最多只能选中一项,所以没有必要设置选择模式

监听事件

如果需要监听列表框选择项的变化,则可以通过添加对应的监听器来实现。通常

  • JList使用addListSelectionListener()方法添加监听器,
  • JComboBox采用addItemListener()方法添加监听器。

程序 JList和JCombox

下面程序示范了JListJCombox的用法,并允许用户通过单选按钮来控制JList的选项布局、选择模式,在用户选择图书之后,这些图书会在窗口下面的文本域里显示出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
import java.util.*;
import java.awt.BorderLayout;
import javax.swing.*;
import javax.swing.border.*;

public class ListTest {
private JFrame mainWin = new JFrame("测试列表框");
String[] books = new String[] { "疯狂Java讲义", "轻量级Java EE企业应用实战", "疯狂Android讲义", "疯狂Ajax讲义", "经典Java EE企业应用实战" };
// 用一个字符串数组来创建一个JList对象
JList<String> bookList = new JList<>(books);
JComboBox<String> bookSelector;
// 定义布局选择按钮所在的面板
JPanel layoutPanel = new JPanel();
ButtonGroup layoutGroup = new ButtonGroup();
// 定义选择模式按钮所在的面板
JPanel selectModePanel = new JPanel();
ButtonGroup selectModeGroup = new ButtonGroup();
JTextArea favoriate = new JTextArea(4, 40);

public void init() {
// 设置JList的可视高度可同时显示三个列表项
bookList.setVisibleRowCount(3);
// 默认选中第三项到第五项(第一项的索引是0)
bookList.setSelectionInterval(2, 4);
addLayoutButton("纵向滚动", JList.VERTICAL);
addLayoutButton("纵向换行", JList.VERTICAL_WRAP);
addLayoutButton("横向换行", JList.HORIZONTAL_WRAP);
addSelectModelButton("无限制", ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
addSelectModelButton("单选", ListSelectionModel.SINGLE_SELECTION);
addSelectModelButton("单范围", ListSelectionModel.SINGLE_INTERVAL_SELECTION);
Box listBox = new Box(BoxLayout.Y_AXIS);
// 将JList组件放在JScrollPane中,再将该JScrollPane添加到listBox容器中
listBox.add(new JScrollPane(bookList));
// 添加布局选择按钮面板、选择模式按钮面板
listBox.add(layoutPanel);
listBox.add(selectModePanel);
// 为JList添加事件监听器
bookList.addListSelectionListener(e -> { // ①
// 获取用户所选择的所有图书
List<String> books = bookList.getSelectedValuesList();
favoriate.setText("");
for (String book : books) {
favoriate.append(book + "\n");
}
});
Vector<String> bookCollection = new Vector<>();
bookCollection.add("疯狂Java讲义");
bookCollection.add("轻量级Java EE企业应用实战");
bookCollection.add("疯狂Android讲义");
bookCollection.add("疯狂Ajax讲义");
bookCollection.add("经典Java EE企业应用实战");
// 用一个Vector对象来创建一个JComboBox对象
bookSelector = new JComboBox<>(bookCollection);
// 为JComboBox添加事件监听器
bookSelector.addItemListener(e -> { // ②
// 获取JComboBox所选中的项
Object book = bookSelector.getSelectedItem();
favoriate.setText(book.toString());
});
// 设置可以直接编辑
bookSelector.setEditable(true);
// 设置下拉列表框的可视高度可同时显示4个列表项
bookSelector.setMaximumRowCount(4);
JPanel p = new JPanel();
p.add(bookSelector);
Box box = new Box(BoxLayout.X_AXIS);
box.add(listBox);
box.add(p);
mainWin.add(box);
JPanel favoriatePanel = new JPanel();
favoriatePanel.setLayout(new BorderLayout());
favoriatePanel.add(new JScrollPane(favoriate));
favoriatePanel.add(new JLabel("您喜欢的图书:"), BorderLayout.NORTH);
mainWin.add(favoriatePanel, BorderLayout.SOUTH);
mainWin.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
mainWin.pack();
mainWin.setVisible(true);
}

private void addLayoutButton(String label, final int orientation) {
layoutPanel.setBorder(new TitledBorder(new EtchedBorder(), "确定选项布局"));
JRadioButton button = new JRadioButton(label);
// 把该单选按钮添加到layoutPanel面板中
layoutPanel.add(button);
// 默认选中第一个按钮
if (layoutGroup.getButtonCount() == 0)
button.setSelected(true);
layoutGroup.add(button);
button.addActionListener(event ->
// 改变列表框里列表项的布局方向
bookList.setLayoutOrientation(orientation));
}

private void addSelectModelButton(String label, final int selectModel) {
selectModePanel.setBorder(new TitledBorder(new EtchedBorder(), "确定选择模式"));
JRadioButton button = new JRadioButton(label);
// 把该单选按钮添加到selectModePanel面板中
selectModePanel.add(button);
// 默认选中第一个按钮
if (selectModeGroup.getButtonCount() == 0)
button.setSelected(true);
selectModeGroup.add(button);
button.addActionListener(event ->
// 改变列表框里的选择模式
bookList.setSelectionMode(selectModel));
}

public static void main(String[] args) {
new ListTest().init();
}
}

上面程序中实现了使用字符串数组创建个JList对象,并通过调用一些方法来改变该JList的表现外观;使用Vector创建一个JComboBox对象,并通过调用些方法来改变该JComboBox的表现外观
程序中①②号代码为JList对象和JComboBox对象添加事件监听器,当用户改变两个列表框里的选择时,程序会把用户选择的图书显示在下面的文本域内。运行上面程序,会看到如图12.34所示的效果。

从图12.34中可以看出,因为JComboBox设置了seteditable(true),所以可以直接在该组件中输入用户自己喜欢的图书,当输入结束后,输入的图书名会直接显示在窗口下面的文本域内。

通常会将JList放到JScrollPane中

JList默认没有滚动条,必须将其放在JScrollpane中才有滚动条,通常总是将JList放在JScrollPane中使用,所以程序中先将JList放到JScrollPane容器中,再将该JScrollPane添加到窗口中。
要在JList中选中多个选项,可以使用CtrlShift辅助键:

  • 按住Ctrl键才可以在原来选中的列表项基础上添加选中新的列表项;
  • Shift键可以选中连续区域的所有列表项。

12.8 使用JSpinner和SpinnerModel刨建微调控制器

JSpinner组件是一个带有两个小箭头的文本框,这个文本框只能接收满足要求的数据,用户既可以通过两个小箭头调整该微调控制器的值,也可以直接在文本框内输入内容作为该微调控制器的值。当用户在该文本框内输入时,如果输入的内容不满足要求,系统将会拒绝用户输入。典型的JSpinner组件如图12.32所示。

JSpinner组件常常需要和SpinnerModel结合使用,其中JSpinner组件控制该组件的外观表现,而SpinnerModel则控制该组件内部的状态数据。

JSpinner组件的值

JSpinner组件的值可以是数值、日期和List中的值,Swing为这三种类型的值提供了SpinnerNumberModelSpinnerDateModelSpinnerListModel三个SpinnerModel实现类;

自定义SpinnerModel实现类

除此之外,JSpinner组件的值还可以是任意序列,只要这个序列可以通过previous()方法和next()方法获取值即可。在这种情况下,用户必须自行提供SpinnerModel实现类。

使用JSpinner组件非常简单,JSpinner提供了如下两个构造器。

构造器 描述
JSpinner() 创建一个默认的微调控制器。创建的默认微调控制器只接收整数值,初始值是0,最大值和最小值没有任何限制制。每单击向下箭头或者向上箭头一次,该组件里的值分别减1或加1。
JSpinner(SpinnerModel model) 使用指定的SpinnerModel来创建微调控制器。

Swing提供的三个SpinnerModel的功能

使用JSpinner关键在于使用它对应的三个SpinnerModel,下面依次介绍这三个SpinnerModel

  • SpinnerNumberModel:这是最简单的SpinnerModel,创建该SpinnerModel时可以指定4个参数:最大值、最小值、初始值、步长,其中步长控制单击上、下箭头时相邻两个值之间的差。这4个参数既可以是整数,也可以是浮点数。
  • SpinnerDateModel:创建该SpinnerModel时可以指定4个参数:起始时间、结束时间、初始时间和时间差,其中时间差控制单击上、下箭头时相邻两个时间之间的差值。
  • SpinnerListModel:创建该SpinnerModel只需要传入一个List或者一个数组作为序列值即可。该List的集合元素和数组元素可以是任意类型的对象,但由于JSpinner组件的文本框只能显示字符串,所以JSpinner显示每个对象的toString()方法的返回值。

从图12.32中可以看出,JSpinner创建的微调控制器和ComboBox有点像(由SwingJComboBox提供,ComboBox既允许通过下拉列表框进行选择,也允许直接输入)

JSpinner和JComboBox的区别

Combobox可以产生一个下拉列表框供用户选择,而JSpinner组件只能通过上、下箭头逐项选择。
使用ComboBox通常必须明确指定下拉列表框中毎一项的值,但使用JSpinner则只需给定一个范围,并指定步长即可;当然,使用JSpinner也可以明确给出每项的值(就是对应使用SpinnerListModel)

为了控制JSpinner中值的显示格式,JSpinner还提供了一个setEditor()方法。Swing提供了如下3个特殊的Editor来控制值的显示格式:

  • JSpinner.DateEditor:控制JSpinner中日期值的显示格式。
  • JSpinner.ListEditor:控制JSpinnerList项的显示格式
  • JSpinner.NumberEditor:控制JSpinner中数值的显示格式

程序 使用JSpinner

下面程序示范了几种使用JSpinner的情形

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
import java.awt.*;
import javax.swing.*;
import java.util.Date;
import java.util.ArrayList;
import java.util.Calendar;

public class JSpinnerTest {
final int SPINNER_NUM = 6;
JFrame mainWin = new JFrame("微调控制器示范");
Box spinnerBox = new Box(BoxLayout.Y_AXIS);
JSpinner[] spinners = new JSpinner[SPINNER_NUM];
JLabel[] valLabels = new JLabel[SPINNER_NUM];
JButton okBn = new JButton("确定");

public void init() {
for (int i = 0; i < SPINNER_NUM; i++) {
valLabels[i] = new JLabel();
}
// -----------普通JSpinner-----------
spinners[0] = new JSpinner();
addSpinner(spinners[0], "普通", valLabels[0]);
// -----------指定最小值、最大值、步长的JSpinner-----------
// 创建一个SpinnerNumberModel对象,指定最小值、最大值和步长
SpinnerNumberModel numModel = new SpinnerNumberModel(3.4, -1.1, 4.3, 0.1);
spinners[1] = new JSpinner(numModel);
addSpinner(spinners[1], "数值范围", valLabels[1]);
// -----------使用SpinnerListModel的JSpinner------------
String[] books = new String[] { "轻量级Java EE企业应用实战", "疯狂Java讲义", "疯狂Ajax讲义" };
// 使用字符串数组创建SpinnerListModel对象
SpinnerListModel bookModel = new SpinnerListModel(books);
// 使用SpinnerListModel对象创建JSpinner对象
spinners[2] = new JSpinner(bookModel);
addSpinner(spinners[2], "字符串序列值", valLabels[2]);
// -----------使用序列值是ImageIcon的JSpinner------------
ArrayList<ImageIcon> icons = new ArrayList<>();
icons.add(new ImageIcon("a.gif"));
icons.add(new ImageIcon("b.gif"));
// 使用ImageIcon数组创建SpinnerListModel对象
SpinnerListModel iconModel = new SpinnerListModel(icons);
// 使用SpinnerListModel对象创建JSpinner对象
spinners[3] = new JSpinner(iconModel);
addSpinner(spinners[3], "图标序列值", valLabels[3]);
// -----------使用SpinnerDateModel的JSpinner------------
// 分别获取起始时间、结束时间、初时时间
Calendar cal = Calendar.getInstance();
Date init = cal.getTime();
cal.add(Calendar.DAY_OF_MONTH, -3);
Date start = cal.getTime();
cal.add(Calendar.DAY_OF_MONTH, 8);
Date end = cal.getTime();
// 创建一个SpinnerDateModel对象,指定最小时间、最大时间和初始时间
SpinnerDateModel dateModel = new SpinnerDateModel(init, start, end, Calendar.HOUR_OF_DAY);
// 以SpinnerDateModel对象创建JSpinner
spinners[4] = new JSpinner(dateModel);
addSpinner(spinners[4], "时间范围", valLabels[4]);
// -----------使用DateEditor来格式化JSpinner------------
dateModel = new SpinnerDateModel();
spinners[5] = new JSpinner(dateModel);
// 创建一个JSpinner.DateEditor对象,用于对指定Spinner进行格式化
JSpinner.DateEditor editor = new JSpinner.DateEditor(spinners[5], "公元yyyy年MM月dd日 HH时");
// 设置使用JSpinner.DateEditor对象进行格式化
spinners[5].setEditor(editor);
addSpinner(spinners[5], "使用DateEditor", valLabels[5]);
// 为“确定”按钮添加一个事件监听器
okBn.addActionListener(evt -> {
// 取出每个微调控制器的值,并将该值用后面的Label标签显示出来。
for (int i = 0; i < SPINNER_NUM; i++) {
// 将微调控制器的值通过指定的JLabel显示出来
valLabels[i].setText(spinners[i].getValue().toString());
}
});
JPanel bnPanel = new JPanel();
bnPanel.add(okBn);
mainWin.add(spinnerBox, BorderLayout.CENTER);
mainWin.add(bnPanel, BorderLayout.SOUTH);
mainWin.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
mainWin.pack();
mainWin.setVisible(true);
}

// 定义一个方法,用于将滑动条添加到容器中
public void addSpinner(JSpinner spinner, String description, JLabel valLabel) {
Box box = new Box(BoxLayout.X_AXIS);
JLabel desc = new JLabel(description + ":");
desc.setPreferredSize(new Dimension(100, 30));
box.add(desc);
box.add(spinner);
valLabel.setPreferredSize(new Dimension(180, 30));
box.add(valLabel);
spinnerBox.add(box);
}

public static void main(String[] args) {
new JSpinnerTest().init();
}
}

上面程序创建了6个JSpinner对象,并将它们添加到窗口中显示出来

  • 第一个JSpinner组件是一个默认的微调控制器,其初始值是0,步长是1,只能接收整数值。
  • 第二个JSpinner通过SpinnerNumberModel来创建,指定了JSpinner的最小值为-1.1、最大值为4.3、初始值为34、步长为0.1,所以用户单击该微调控制器的上、下箭头时,微调控制器的值之间的差值是0.1,并只能处于-1.1~4.3之间
  • 第三个JSpinner通过SpinnerListModel来创建的,创建SpinnerListModel对象时指定字符串数组作为多个序列值,所以当用户单击该微调控制器的上、下箭头时,微调控制器的值总是在该字符串数组之间选择。
  • 第四个JSpinner也是通过SpinnerListmodel来创建的,虽然传给SpinnerListModel对象的构造参数是集合元素为ImageIconList对象,但JSpinner只能显示字符串内容,所以它会把每个Imagelcon对象的toString方法返回值当成微调控制器的多个序列值。
  • 第五个JSpinner通过SpinnerDateModel来创建,而且指定了最小时间、最大时间和初始时间,所以用户单击该微调控制器的上、下箭头时,微调控制器里的时间只能处于指定时间范围之间。这里需要注意的是,SpinnerDateModel的第4个参数没有太大的作用,它不能控制两个相邻时间之间的差。当用户在JSpinner组件内选中该时间的指定时间域时,例如年份,则两个相邻时间的时间差就是1年。
  • 第六个JSpinner使用SPinner.DateEditor来控制时间微调控制器里日期、时间的显示格式,创建JSpinner.DateEditor对象时需要传入一个日期时间格式字符串(dateFormatPattern),该参数用于控制日期、时间的显示格式,关于这个格式字符串的定义方式可以参考SimpleDateFormat类的介绍。本例程序中使用”公元yyyy年MM月dd日HH时”作为格式字符串。

运行上面程序,会看到如图12.33所示的窗口。

图12.33

程序中还提供了一个“确定”按钮,当单击该按钮时,系统会把每个微调控制器的值通过对应的JLabel标签显示出来,如图12.33所示。

12.7 创建滑动条

JSlider的用法和JProgressBar的用法非常相似,这一点可以从它们共享同一个Model类看出来。使用JSlider可以创建一个滑动条,这个滑动条同样有最小值、最大值和当前值等属性。

JSliderJProgressBar的主要区别

  • JSlider不是采用填充颜色的方式来表示该组件的当前值,而是采用滑块的位置来表示该组件的当前值
  • JSlider允许用户手动改变滑动条的当前值。
  • JSlider允许为滑动条指定刻度值,这系列的刻度值既可以是连续的数字,也可以是自定义的刻度值,甚至可以是图标。

使用JSlider创建滑动条的步骤

创建JSlider对象

使用JSlider的构造器创建一个JSlider对象,JSlider有多个重载的构造器,但这些构造器总共可以接收如下4个参数。

  • orientation:指定该滑动条的摆放方向,默认是水平摆放。可以接收SwingConstants.VERTICALSwingConstants.HORIZONTAL两个值
  • min:指定该滑动条的最小值,该属性值默认为0。
  • max:指定该滑动条的最大值,该属性值默认是为100。
  • value:指定该滑动条的当前值,该属性值默认是为50。
方法 描述
JSlider() Creates a horizontal slider with the range 0 to 100 and an initial value of 50.
JSlider(int orientation) Creates a slider using the specified orientation with the range 0 to 100 and an initial value of 50.
JSlider(int min, int max) Creates a horizontal slider using the specified min and max with an initial value equal to the average of the min plus max.
JSlider(int min, int max, int value) Creates a horizontal slider using the specified min, max and value.
JSlider(int orientation, int min, int max, int value) Creates a slider with the specified orientation and the specified minimum, maximum, and initial values.
JSlider(BoundedRangeModel brm) Creates a horizontal slider using the specified BoundedRangeModel.

设置滑动条的外观

调用JSlider的如下方法来设置滑动条的外观样式

方法 描述
void setExtent(int extent) 设置滑动条上的保留区,用户拖动滑块时不能超过保留区。例如,最大值为100的滑动条,如果设置保留区为20,则滑块最大只能拖动到80
void setInverted(boolean b) 设置是否需要反转滑动条,滑动条的滑轨上刻度值默认从小到大、从左到右排列。如果该方法设置为true,则排列方向会反转过来。
void setLabelTable(Dictionary labels) 为该滑动条指定刻度标签。该方法的参数是Dictionary类型,Dictionary是一个古老的、抽象集合类,其子类是Hashtable。传入的Hashtable集合对象的key-value对为{Integer value,java.Swing.JComponent label}格式,刻度标签可以是任何组件。
void setMajorTickSpacing(int n) 设置主刻度标记的间隔
void setMinorTickSpacing(int n) 设置次刻度标记的间隔。
void setPaintLabels(boolean b) 设置是否在滑块上绘制刻度标签。如果没有为该滑动条指定刻度标签,则默认绘制将刻度值的数值作为标签。
void setPaintTicks(boolean b) 设置是否在滑块上绘制刻度标记
void setPaintTrack(boolean b) 设置是否为滑块绘制滑轨。
void setSnapToTicks(boolean b) 设置滑块是否必须停在滑道的有刻度处。如果设置为true,则滑块只能停在有刻度处;如果用户没有将滑块拖到有刻度处,则系统自动将滑块定位到最近的刻度处

JSlider对象添加事件监听器

如果程序需要在用户拖动滑块时做岀相应处理,则应为该JSlider对象添加事件监听器。JSlider提供了addChangeListener方法来添加事件监听器,该监听器负责监听滑动值的变化

方法 描述
void addChangeListener(ChangeListener l) Adds a ChangeListener to the

添加到其他容器中

JSlider对象添加到其他容器中显示出来。

程序 创建滑动条

下面程序示范了如何使用JSlider来创建滑动条

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
import java.awt.*;
import javax.swing.*;
import javax.swing.event.*;
import java.util.Dictionary;
import java.util.Hashtable;

public class JSliderTest {
JFrame mainWin = new JFrame("滑动条示范");
Box sliderBox = new Box(BoxLayout.Y_AXIS);
JTextField showVal = new JTextField();
ChangeListener listener;

public void init() {
// 定义一个监听器,用于监听所有滑动条
listener = event -> {
// 取出滑动条的值,并在文本中显示出来
JSlider source = (JSlider) event.getSource();
showVal.setText("当前滑动条的值为:" + source.getValue());
};
// -----------添加一个普通滑动条-----------
JSlider slider = new JSlider();
addSlider(slider, "普通滑动条");
// -----------添加保留区为30的滑动条-----------
slider = new JSlider();
slider.setExtent(30);
addSlider(slider, "保留区为30");
// ---添加带主、次刻度的滑动条,并设置其最大值,最小值---
slider = new JSlider(30, 200);
// 设置绘制刻度
slider.setPaintTicks(true);
// 设置主、次刻度的间距
slider.setMajorTickSpacing(20);
slider.setMinorTickSpacing(5);
addSlider(slider, "有刻度");
// -----------添加滑块必须停在刻度处滑动条-----------
slider = new JSlider();
// 设置滑块必须停在刻度处
slider.setSnapToTicks(true);
// 设置绘制刻度
slider.setPaintTicks(true);
// 设置主、次刻度的间距
slider.setMajorTickSpacing(20);
slider.setMinorTickSpacing(5);
addSlider(slider, "滑块停在刻度处");
// -----------添加没有滑轨的滑动条-----------
slider = new JSlider();
// 设置绘制刻度
slider.setPaintTicks(true);
// 设置主、次刻度的间距
slider.setMajorTickSpacing(20);
slider.setMinorTickSpacing(5);
// 设置不绘制滑轨
slider.setPaintTrack(false);
addSlider(slider, "无滑轨");
// -----------添加方向反转的滑动条-----------
slider = new JSlider();
// 设置绘制刻度
slider.setPaintTicks(true);
// 设置主、次刻度的间距
slider.setMajorTickSpacing(20);
slider.setMinorTickSpacing(5);
// 设置方向反转
slider.setInverted(true);
addSlider(slider, "方向反转");
// --------添加绘制默认刻度标签的滑动条--------
slider = new JSlider();
// 设置绘制刻度
slider.setPaintTicks(true);
// 设置主、次刻度的间距
slider.setMajorTickSpacing(20);
slider.setMinorTickSpacing(5);
// 设置绘制刻度标签,默认绘制数值刻度标签
slider.setPaintLabels(true);
addSlider(slider, "数值刻度标签");
// ------添加绘制Label类型的刻度标签的滑动条------
slider = new JSlider();
// 设置绘制刻度
slider.setPaintTicks(true);
// 设置主、次刻度的间距
slider.setMajorTickSpacing(20);
slider.setMinorTickSpacing(5);
// 设置绘制刻度标签
slider.setPaintLabels(true);
Dictionary<Integer, Component> labelTable = new Hashtable<>();
labelTable.put(0, new JLabel("A"));
labelTable.put(20, new JLabel("B"));
labelTable.put(40, new JLabel("C"));
labelTable.put(60, new JLabel("D"));
labelTable.put(80, new JLabel("E"));
labelTable.put(100, new JLabel("F"));
// 指定刻度标签,标签是JLabel
slider.setLabelTable(labelTable);
addSlider(slider, "JLable标签");
// ------添加绘制Label类型的刻度标签的滑动条------
slider = new JSlider();
// 设置绘制刻度
slider.setPaintTicks(true);
// 设置主、次刻度的间距
slider.setMajorTickSpacing(20);
slider.setMinorTickSpacing(5);
// 设置绘制刻度标签
slider.setPaintLabels(true);
labelTable = new Hashtable<Integer, Component>();
labelTable.put(0, new JLabel(new ImageIcon("ico/0.GIF")));
labelTable.put(20, new JLabel(new ImageIcon("ico/2.GIF")));
labelTable.put(40, new JLabel(new ImageIcon("ico/4.GIF")));
labelTable.put(60, new JLabel(new ImageIcon("ico/6.GIF")));
labelTable.put(80, new JLabel(new ImageIcon("ico/8.GIF")));
// 指定刻度标签,标签是ImageIcon
slider.setLabelTable(labelTable);
addSlider(slider, "Icon标签");
mainWin.add(sliderBox, BorderLayout.CENTER);
mainWin.add(showVal, BorderLayout.SOUTH);
mainWin.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
mainWin.pack();
mainWin.setVisible(true);
}

// 定义一个方法,用于将滑动条添加到容器中
public void addSlider(JSlider slider, String description) {
slider.addChangeListener(listener);
Box box = new Box(BoxLayout.X_AXIS);
box.add(new JLabel(description + ":"));
box.add(slider);
sliderBox.add(box);
}

public static void main(String[] args) {
new JSliderTest().init();
}
}

上面程序向窗口中添加了多个滑动条,程序通过粗体字代码来控制不同滑动条的不同外观。运行上面程序,会看到如图12.31所示的各种滑动条的效果。

这里有一张图片

JSlider也使用BoundedRangeModel作为保存其状态数据的Model对象,程序可以直接修改Model对象来改变滑动条的状态,但大部分时候程序无须使用该Model对象。

JSlider也提供了addChangeListener方法来为滑动条添加监听器,无须像JProgressBar那样监听它所对应的Model对象。

12.6.2 创建进度对话框

ProgressMonitor的用法与JProgressBar的用法基本相似,只是ProgressMonitor可以直接创建一个进度对话框。

构造器

ProgressMonitor提供了如下构造器。

方法 描述
ProgressMonitor(Component parentComponent, Object message, String note, int min, int max)
  • parentComponent参数用于设置该进度对话框的父组件,
  • message用于设置该进度对话框的描述信息,note用于设置该进度对话框的提示文本,
  • minmax用于设置该对话框所包含进度条的最小值和最大值。

代码 创建进度对话框

例如,如下代码创建了一个进度对话框。

1
final ProgressMonitor dialog = new ProgressMonitor(null,"等待任务完成","已完成:",0, target, getAmount());

使用上面代码创建的进度对话框如图12.30所示。

这里有一张图片

如何判断用户是否点击取消按钮

如图12.30所示,该对话框中包含了一个“取销”按钮,如果程序希望判断用户是否单击了该按钮,则可以通过ProgressMonitorisCanceled()方法进行判断
使用ProgressMonitor创建的对话框里包含的进度条是非常固定的,程序甚至不能设置该进度条是否包含边框(总是包含边框),不能设置进度不确定,不能改变进度条的方向(总是水平方向)。

进度对话框如何设置完成度

与普通进度条类似的是,进度对话框也不能自动监视目标任务的完成进度,程序通过调用进度对话框的setProgress方法来改变进度条的完成比例(该方法类似于JProgressBarsetValue方法)

方法 描述
void setProgress(int nv) Indicate the progress of the operation being monitored.

程序 进度对话框

下面程序同样采用前面的SimulatedTarget来模拟一个耗时的任务,并创建了一个进度对话框来监测该任务的完成百分比

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import javax.swing.*;

public class ProgressMonitorTest {
Timer timer;

public void init() {
final SimulatedActivity target = new SimulatedActivity(1000);
// 以启动一条线程的方式来执行一个耗时的任务
final Thread targetThread = new Thread(target);
targetThread.start();
final ProgressMonitor dialog = new ProgressMonitor(null, "等待任务完成", "已完成:", 0, target.getAmount());
timer = new Timer(300, e -> {
// 以任务的当前完成量设置进度对话框的完成比例
dialog.setProgress(target.getCurrent());
// 如果用户单击了进度对话框的"取消"按钮
if (dialog.isCanceled()) {
// 停止计时器
timer.stop();
// 中断任务的执行线程
targetThread.interrupt(); // ①
// 系统退出
System.exit(0);
}
});
timer.start();
}

public static void main(String[] args) {
new ProgressMonitorTest().init();
}
}

上面程序中创建了一个进度对话框,并创建了一个Timer计时器不断询问SimulatedTarget任务的完成比例,进而设置进度对话框里进度条的完成比例。而且该计时器还负责监听用户是否单击了进度对话框中的“取销”按钮,如果用户单击了该按钮,则中止执行SimulatedTarget任务的线程,并停止计时器,同时退出该程序。运行该程序,会看到如图12.30所示的对话框

图12.30

12.6 使用JProgressBar、 ProgressMonitor和BoundedRangeModel创建进度条

进度条是图形界面中广泛使用的GUI组件,当复制一个较大的文件时,操作系统会显示一个进度条,用于标识复制操作完成的比例;当启动Eclipse等程序时,因为需要加载较多的资源,故而启动速度较慢,程序也会在启动过程中显示一个进度条,用以表示该软件启动完成的比例。

12.6.1 创建JProgressBar进度条

使用JProgressBar可以非常方便地创建进度条。

使用JProgressBar创建进度条的步骤

创建JProgressBard对象

创建一个JProgressBar对象,创建该对象时可以指定三个参数,用于设置进度条的排列方向(竖直和水平)、进度条的最大值最小值。也可以在创建该对象时不传入任何参数,而是在后面程序中修改这三个属性。

例如,如下代码创建了JProgressBar对象

1
2
//创建一条垂直进度条
JProgressBar bar = new JProgressBar(JProgressBar.VERTICAL);

设置进度条的普通属性

调用该对象的常用方法设置进度条的普通属性JProgressBar除提供设置排列方向、最大值、最小值的settergetter方法之外,还提供了如下三个方法

方法 描述
void setBorderPainted(boolean b) 设置该进度条是否使用边框
void setIndeterminate(boolean newValue) 设置该进度条是否是进度不确定的进度条,如果指定一个进度条的进度不确定,将看到一个滑块在进度条中左右移动。
void setStringPainted(boolean b) 设置是否在进度条中显示完成百分比。

当然,JProgressBar也为上面三个属性提供了getter方法,但这三个getter方法通常没有太大作用。

当程序中工作进度改变时,调用JProgressBar对象的setValue()方法。当进度条的完成进度发生改变时,程序还可以调用进度条对象的如下两个方法。

方法 描述
double getPercentComplete() 返回进度条的完成百分比
String getString() 返回进度字符串的当前值

程序 简单进度条

下面程序示范了使用进度条的简单例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import java.awt.*;
import javax.swing.*;

public class JProgressBarTest {
JFrame frame = new JFrame("测试进度条");
// 创建一条垂直进度条
JProgressBar bar = new JProgressBar(JProgressBar.VERTICAL);
JCheckBox indeterminate = new JCheckBox("不确定进度");
JCheckBox noBorder = new JCheckBox("不绘制边框");

public void init() {
Box box = new Box(BoxLayout.Y_AXIS);
box.add(indeterminate);
box.add(noBorder);
frame.setLayout(new FlowLayout());
frame.add(box);
// 把进度条添加到JFrame窗口中
frame.add(bar);
// 设置进度条的最大值和最小值
bar.setMinimum(0);
bar.setMaximum(100);
// 设置在进度条中绘制完成百分比
bar.setStringPainted(true);
// 根据该选择框决定是否绘制进度条的边框
noBorder.addActionListener(event -> bar.setBorderPainted(!noBorder.isSelected()));
indeterminate.addActionListener(event -> {
// 设置该进度条的进度是否确定
bar.setIndeterminate(indeterminate.isSelected());
bar.setStringPainted(!indeterminate.isSelected());
});
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.pack();
frame.setVisible(true);
// 采用循环方式来不断改变进度条的完成进度
for (int i = 0; i <= 100; i++) {
// 改变进度条的完成进度
bar.setValue(i);
try {
// 程序暂停0.1秒
Thread.sleep(100);
} catch (Exception e) {
e.printStackTrace();
}
}
}

public static void main(String[] args) {
new JProgressBarTest().init();
}
}

上面程序中创建了一个垂直进度条,并通过方法来设置进度条的外观形式:

  • 是否包含边框,
  • 是否在进度条中显示完成百分比,

并通过一个循环来不断改变进度条的value属性,该value将会自动转换成进度条的完成百分比

运行该程序,将看到如图12.29所示的效果

这里有一张图片

在上面程序中,在主程序中使用循环来改变进度条的value属性,即修改进度条的完成百分比,这是没有任何意义的事情。

通常会希望用进度条去检测其他任务的完成情况,而不是在其他任务的执行过程中主动修改进度条的value属性,因为其他任务可能根本不知道进度条的存在。此时可以使用一个计时器来不断取得目标任务的完成情况,并根据其完成情况来修改进度条的value属性

程序 进度条检测完成度

下面程序改写了上面程序,用一个SimulatedTarget来模拟一个耗时的任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
import java.awt.*;
import javax.swing.*;

public class JProgressBarTest2 {
JFrame frame = new JFrame("测试进度条");
// 创建一条垂直进度条
JProgressBar bar = new JProgressBar(JProgressBar.VERTICAL);
JCheckBox indeterminate = new JCheckBox("不确定进度");
JCheckBox noBorder = new JCheckBox("不绘制边框");

public void init() {
Box box = new Box(BoxLayout.Y_AXIS);
box.add(indeterminate);
box.add(noBorder);
frame.setLayout(new FlowLayout());
frame.add(box);
// 把进度条添加到JFrame窗口中
frame.add(bar);
// 设置在进度条中绘制完成百分比
bar.setStringPainted(true);
// 根据该选择框决定是否绘制进度条的边框
noBorder.addActionListener(event -> bar.setBorderPainted(!noBorder.isSelected()));
final SimulatedActivity target = new SimulatedActivity(1000);
// 以启动一条线程的方式来执行一个耗时的任务
new Thread(target).start();
// 设置进度条的最大值和最小值,
bar.setMinimum(0);
// 以总任务量作为进度条的最大值
bar.setMaximum(target.getAmount());
// 以任务的当前完成量设置进度条的value
Timer timer = new Timer(300, e -> bar.setValue(target.getCurrent()));
timer.start();
indeterminate.addActionListener(event -> {
// 设置该进度条的进度是否确定
bar.setIndeterminate(indeterminate.isSelected());
bar.setStringPainted(!indeterminate.isSelected());
});
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.pack();
frame.setVisible(true);
}

public static void main(String[] args) {
new JProgressBarTest2().init();
}
}

// 模拟一个耗时的任务
class SimulatedActivity implements Runnable {
// 任务的当前完成量
private volatile int current;
// 总任务量
private int amount;

public SimulatedActivity(int amount) {
current = 0;
this.amount = amount;
}

public int getAmount() {
return amount;
}

public int getCurrent() {
return current;
}

// run方法代表不断完成任务的过程
public void run() {
while (current < amount) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
}
current++;
}
}
}

上面程序的运行效果与前一个程序的运行效果大致相同,但这个程序中的JProgressBars就实用多了,它可以检测并显示SimulatedTarget的完成进度。

SimulatedActivity类实现了Runnable接口,这是一个特殊的接口,实现该接口可以实现多线程功能。

BoundedRangeModel

Swing组件大都将外观显示和内部数据分离,JProgressBar也不例外,JProgressBar组件有一个用于保存其状态数据的Model对象,这个对象由BoundedRangeModel对象表示,程序调用JProgressBar对象的setValue方法时,实际上是设置BoundedRangeModel对象的value属性。

程序可以修改BoundedRangeModel对象的minimum属性和maximum属性,当该Model对象的这两个属性被修改后,它所对应的JProgressBar对象的这两个属性也会随之修改,因为JProgressBar对象的所有状态数据都是保存在该Model对象中的。

程序监听JProgressBar完成比例的变化,也是通过为BoundedRangeModel提供监听器来实现的BoundedRangeModel提供了如下一个方法来添加监听器。

方法 描述
void addChangeListener(ChangeListener x) 用于监听JProgressBar完成比例的变化,每当JProgressBarvalue属性被改变时,系统都会触发ChangeListener监听器的stateChanged()方法。

代码 进度条状态变化监听器

下面代码为进度条的状态变化添加了一个监听器

1
2
3
4
// JProgressBar的完成比例发生变化时会触发该方法
bar.getModel().addChangeListener(ce -> {
//对进度变化进行合适处理
});

12.5.2 创建透明、不规则形状窗口

Java7Frame提供了如下两个方法:

方法 描述
void setShape(Shape shape) 设置窗口的形状,可以将窗口设置成任意不规则的形状。
void setOpacity(float opacity) 设置窗口的透明度,可以将窗口设置成半透明的。当opacity为1.0f时,该窗口完全不透明

这两个方法简单、易用,可以直接改变窗口的形状和透明度。除此之外,如果希望开发出渐变透明的窗口,则可以考虑使用一个渐变透明的JPanel来代替JFrameContentPane;按照这种思路,还可以开发出有图片背景的窗口。

程序 透明不规则窗口

下面程序示范了如何开发出透明、不规则的窗口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
import java.io.*;
import java.awt.*;
import javax.imageio.*;
import java.awt.event.*;
import javax.swing.*;
import java.awt.geom.Ellipse2D;

public class NonRegularWindow extends JFrame implements ActionListener {

private static final long serialVersionUID = 8373752799905574018L;
// 定义3个窗口
JFrame transWin = new JFrame("透明窗口");
JFrame gradientWin = new JFrame("渐变透明窗口");
JFrame bgWin = new JFrame("背景图片窗口");
JFrame shapeWin = new JFrame("椭圆窗口");

public NonRegularWindow() {
super("不规则窗口测试");
setLayout(new FlowLayout());
JButton transBn = new JButton("透明窗口");
JButton gradientBn = new JButton("渐变透明窗口");
JButton bgBn = new JButton("背景图片窗口");
JButton shapeBn = new JButton("椭圆窗口");
// 为3个按钮添加事件监听器
transBn.addActionListener(this);
gradientBn.addActionListener(this);
bgBn.addActionListener(this);
shapeBn.addActionListener(this);
add(transBn);
add(gradientBn);
add(bgBn);
add(shapeBn);
// -------设置透明窗口-------
transWin.setLayout(new GridBagLayout());
transWin.setSize(300, 200);
transWin.add(new JButton("透明窗口里的简单按钮"));
// 设置透明度为0.65f,透明度为1时完全不透明。
transWin.setOpacity(0.65f);
// -------设置渐变透明的窗口-------
gradientWin.setBackground(new Color(0, 0, 0, 0));
gradientWin.setSize(new Dimension(300, 200));
// 使用一个JPanel对象作为渐变透明的背景
JPanel panel = new JPanel() {
private static final long serialVersionUID = -3046929062708838980L;

protected void paintComponent(Graphics g) {
if (g instanceof Graphics2D) {
final int R = 240;
final int G = 240;
final int B = 240;
// 创建一个渐变画笔
Paint p = new GradientPaint(0.0f, 0.0f, new Color(R, G, B, 0), 0.0f, getHeight(),
new Color(R, G, B, 255), true);
Graphics2D g2d = (Graphics2D) g;
g2d.setPaint(p);
g2d.fillRect(0, 0, getWidth(), getHeight());
}
}
};
// 使用JPanel对象作为JFrame的contentPane
gradientWin.setContentPane(panel);
panel.setLayout(new GridBagLayout());
gradientWin.add(new JButton("渐变透明窗口里的简单按钮"));
// -------设置有背景图片的窗口-------
bgWin.setBackground(new Color(0, 0, 0, 0));
bgWin.setSize(new Dimension(300, 200));
// 使用一个JPanel对象作为背景图片
JPanel bgPanel = new JPanel() {
private static final long serialVersionUID = -4650113493486706945L;

protected void paintComponent(Graphics g) {
try {
Image bg = ImageIO.read(new File("images/java.png"));
// 绘制一张图片作为背景
g.drawImage(bg, 0, 0, getWidth(), getHeight(), null);
} catch (IOException ex) {
ex.printStackTrace();
}
}
};
// 使用JPanel对象作为JFrame的contentPane
bgWin.setContentPane(bgPanel);
bgPanel.setLayout(new GridBagLayout());
bgWin.add(new JButton("有背景图片窗口里的简单按钮"));
// -------设置椭圆形窗口-------
shapeWin.setLayout(new GridBagLayout());
shapeWin.setUndecorated(true);
shapeWin.setOpacity(0.7f);
// 通过为shapeWin添加监听器来设置窗口的形状。
// 当shapeWin窗口的大小被改变时,程序动态设置该窗口的形状
shapeWin.addComponentListener(new ComponentAdapter() {
// 当窗口大小被改变时,椭圆的大小也会相应地改变
public void componentResized(ComponentEvent e) {
// 设置窗口的形状
shapeWin.setShape(new Ellipse2D.Double(0, 0, shapeWin.getWidth(), shapeWin.getHeight())); // ①
}
});
shapeWin.setSize(300, 200);
shapeWin.add(new JButton("椭圆形窗口里的简单按钮"));
// -------设置主程序的窗口-------
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
pack();
setVisible(true);
}

public void actionPerformed(ActionEvent event) {
switch (event.getActionCommand()) {
case "透明窗口":
transWin.setVisible(true);
break;
case "渐变透明窗口":
gradientWin.setVisible(true);
break;
case "背景图片窗口":
bgWin.setVisible(true);
break;
case "椭圆窗口":
shapeWin.setVisible(true);
break;
}
}

public static void main(String[] args) {
JFrame.setDefaultLookAndFeelDecorated(true);
new NonRegularWindow();
}
}

上面程序中的粗体字代码就是设置透明窗口、渐变透明窗口、有背景图片窗口的关键代码;当需要开发不规则形状的窗口时,程序往往会为该窗口实现一个ComponentListener,该监听器负责监听窗口大小发生改变的事件——当窗口大小发生改变时,程序调用窗口的setShape()方法来控制窗口的形状,如①号粗体字代码所示。

运行上面程序,打开透明窗口和渐变透明窗口,效果如图12.27所示。

这里有一张图片

打开有背景图片窗口和椭圆窗口,效果如图12.28所示。

这里有一张图片

12.5 Java7新增的Swing功能

Java7提供的重大更新就包括了对Swing的更新,对Swing的更新除前面介绍的Nimbus外观、改进的JColorchooser组件之外,还有两个很有用的更新:JLayer创建不规则窗口。下面将会详细介绍这两个知识点。

12.5.1 使用JLayer装饰组件

JLayer的功能是在指定组件上额外地添加一个装饰层,开发者可以在这个装饰层上进行任意绘制(直接重写paint(Graphics g,JComponent c)方法),这样就可以为指定组件添加任意装饰。

JLayer一般总是要和LayerUI一起使用,而LayerUI用于被扩展,扩展LayerUI时重写它的paint(Graphics g,JComponent c)方法,在该方法中绘制的内容会对指定组件进行装饰。

实际上,使用JLayer很简单,只要如下两行代码即可

1
2
3
4
//创建LayerUI对象
LayerUI<JComponent> layerUI = new XxxLayerUI();
//使用layerUI来装饰指定的JPanel组件
JLayer<JComponent> layer = new JLayer<JComponent>(panel, layerUI);

上面程序中的XxxLayerUI就是开发者自己扩展的子类,这个子类会重写paint(Graphics g,JComponent c)方法,重写该方法来完成“装饰层”的绘制。

上面第二行代码中的panel组件就是被装饰的组件,接下来把layer对象(layer对象包含了被装饰对象和LayerUI对象)添加到指定容器中即可

程序 蒙版效果

下面程序示范了使用JLayer为窗口添加一层“蒙版”的效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class FirstLayerUI extends LayerUI<JComponent> {
private static final long serialVersionUID = 5993086002698144927L;

public void paint(Graphics g, JComponent c) {
super.paint(g, c);
Graphics2D g2 = (Graphics2D) g.create();
// 设置透明效果
g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, .5f));
// 使用渐变画笔绘图
g2.setPaint(new GradientPaint(0, 0, Color.RED, 0, c.getHeight(), Color.BLUE));
// 绘制一个与被装饰组件相同大小的矩形
g2.fillRect(0, 0, c.getWidth(), c.getHeight()); // ①
g2.dispose();
}
}

程序 主类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class JLayerTest {
public void init() {
JFrame f = new JFrame("JLayer测试");
JPanel p = new JPanel();
ButtonGroup group = new ButtonGroup();
JRadioButton radioButton;
// 创建3个RadioButton,并将它们添加成一组
p.add(radioButton = new JRadioButton("网购购买", true));
group.add(radioButton);
p.add(radioButton = new JRadioButton("书店购买"));
group.add(radioButton);
p.add(radioButton = new JRadioButton("图书馆借阅"));
group.add(radioButton);
// 添加3个JCheckBox
p.add(new JCheckBox("疯狂Java讲义"));
p.add(new JCheckBox("疯狂Android讲义"));
p.add(new JCheckBox("疯狂Ajax讲义"));
p.add(new JCheckBox("轻量级Java EE企业应用"));
JButton orderButton = new JButton("投票");
p.add(orderButton);
// 创建LayerUI对象
// LayerUI<JComponent> layerUI = new SpotlightLayerUI(); // ②
// LayerUI<JComponent> layerUI = new FirstLayerUI(); // ②
LayerUI<JComponent> layerUI = new BlurLayerUI(); // ②
// 使用layerUI来装饰指定的JPanel组件
JLayer<JComponent> layer = new JLayer<JComponent>(p, layerUI);
// 将装饰后的JPanel组件添加到容器中
f.add(layer);
f.setSize(300, 170);
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.setVisible(true);
}

public static void main(String[] args) {
new JLayerTest().init();
}
}

上面程序中开发了一个FirstLayerUI,它扩展了LayerUI,重写paint(graphics g, JComponent c)方法时绘制了一个半透明的、与被装饰组件具有相同大小的矩形。接下来在main方法中使用这个LayerUI来装饰指定的JPanel组件,并把JLayer添加到JFrame容器中,这就达到了对JPanel进行包装的效果。

运行该程序,可以看到如图12.23所示的效果

这里有一张图片
由于开发者可以重写paint(graphics g, JComponent c)方法,因此获得对被装饰层的全部控制权——想怎么绘制,就怎么绘制!因此开发者可以“随心所欲”地对指定组件进行装饰。

程序 模糊效果

例如,下面提供的LayerUI则可以为被装饰组件增加“模糊”效果。程序如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class BlurLayerUI extends LayerUI<JComponent> {
private static final long serialVersionUID = -2386080677384858056L;
private BufferedImage screenBlurImage;
private BufferedImageOp operation;

public BlurLayerUI() {
float ninth = 1.0f / 9.0f;
// 定义模糊参数
float[] blurKernel = { ninth, ninth, ninth, ninth, ninth, ninth, ninth, ninth, ninth };
// ConvolveOp代表一个模糊处理,它将原图片的每一个像素与周围
// 像素的颜色进行混合,从而计算出当前像素的颜色值,
operation = new ConvolveOp(new Kernel(3, 3, blurKernel), ConvolveOp.EDGE_NO_OP, null);
}

public void paint(Graphics g, JComponent c) {
int w = c.getWidth();
int h = c.getHeight();
// 如果被装饰窗口大小为0X0,直接返回
if (w == 0 || h == 0)
return;
// 如果screenBlurImage没有初始化,或它的尺寸不对。
if (screenBlurImage == null || screenBlurImage.getWidth() != w || screenBlurImage.getHeight() != h) {
// 重新创建新的BufferdImage
screenBlurImage = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
}
Graphics2D ig2 = screenBlurImage.createGraphics();
// 把被装饰组件的界面绘制到当前screenBlurImage上
ig2.setClip(g.getClip());
super.paint(ig2, c);
ig2.dispose();
Graphics2D g2 = (Graphics2D) g;
// 对JLayer装饰的组件进行模糊处理
g2.drawImage(screenBlurImage, operation, 0, 0);
}
}

上面程序扩展了LayerUI,重写了paint(Graphics g,JComponent c)方法,重写该方法时也是绘制了一个与被装饰组件具有相同大小的矩形,只是这种绘制添加了模糊效果。

JLayerTestJava中的②号粗体字代码改为使用BlurLayerUI,再次运行该程序,将可以看到如图12.24所示的“毛玻璃”窗口

这里有一张图片

给LayerUI增加事件机制

除此之外,开发者自定义的LayerUI还可以增加事件机制,这种事件机制能让装饰层响应用户动作,随着用户动作动态地改变LayerUI上的绘制效果。比如下面的LayerUI示例。

程序 探照灯效果

程序通过响应鼠标事件可以在窗口上增加“探照灯”效果。程序如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
class SpotlightLayerUI extends LayerUI<JComponent> {
private static final long serialVersionUID = -5931810232610714874L;
private boolean active;
private int cx, cy;

public void installUI(JComponent c) {
super.installUI(c);
JLayer layer = (JLayer) c;
// 设置JLayer可以响应鼠标、鼠标动作事件
layer.setLayerEventMask(AWTEvent.MOUSE_EVENT_MASK | AWTEvent.MOUSE_MOTION_EVENT_MASK);
}

public void uninstallUI(JComponent c) {
JLayer layer = (JLayer) c;
// 设置JLayer不响应任何事件
layer.setLayerEventMask(0);
super.uninstallUI(c);
}

public void paint(Graphics g, JComponent c) {
Graphics2D g2 = (Graphics2D) g.create();
super.paint(g2, c);
// 如果处于激活状态
if (active) {
// 定义一个cx、cy位置的点
Point2D center = new Point2D.Float(cx, cy);
float radius = 72;
float[] dist = { 0.0f, 1.0f };
Color[] colors = { Color.YELLOW, Color.BLACK };
// 以center为中心、colors为颜色数组创建环形渐变
RadialGradientPaint p = new RadialGradientPaint(center, radius, dist, colors);
g2.setPaint(p);
// 设置渐变效果
g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, .6f));
// 绘制矩形
g2.fillRect(0, 0, c.getWidth(), c.getHeight());
}
g2.dispose();
}

// 处理鼠标事件的方法
public void processMouseEvent(MouseEvent e, JLayer layer) {
if (e.getID() == MouseEvent.MOUSE_ENTERED)
active = true;
if (e.getID() == MouseEvent.MOUSE_EXITED)
active = false;
layer.repaint();
}

// 处理鼠标动作事件的方法
public void processMouseMotionEvent(MouseEvent e, JLayer layer) {
Point p = SwingUtilities.convertPoint(e.getComponent(), e.getPoint(), layer);
// 获取鼠标动作事件的发生点的坐标
cx = p.x;
cy = p.y;
layer.repaint();
}
}

上面程序中重写了LayerUIinstallUI(JComponent c)方法,重写该方法时控制该组件能响应鼠标事件和鼠标动作事件。
接下来程序

重写processMouseMotionEvent()方法,该方法负责为LayerUI上的鼠标事件提供响应:当鼠标在界面上移动时,程序会改变cxcy的坐标值
重写paint(Graphics g, JComponent c)方法时会在cxey对应的点绘制一个环形渐变,这就可以充当“探照灯”效果了。

JLayerTestJava中的②号粗体字代码改为使用SpotlightLayerUI,再次运行该程序,即可看到如图12.25所示的效果。

这里有一张图片

绘制动画

既然可以让LayerUI上的绘制效果响应鼠标动作,当然也可以在LayerUI上绘制“动画”,所谓动画,就是通过定时器控制LayerUI上绘制的图形动态地改变即可。

接下来重写的LayerUI使用了Timer来定时地改变LayerUI上的绘制,程序绘制了一个旋转中的“齿轮”,这个旋转的齿轮可以提醒用户“程序正在处理中”

程序 转动的齿轮

下面程序重写LayerUl时绘制了12条辐射状的线条,并通过Timer来不断地改变这12条线条的排列角度,这样就可以形成“转动的齿轮”了。

程序提供的WaitingLayerUI类代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
class WaitingLayerUI extends LayerUI<JComponent> {
private static final long serialVersionUID = -443302447858652985L;
private boolean isRunning;
private Timer timer;
// 记录转过的角度
private int angle; // ①

public void paint(Graphics g, JComponent c) {
super.paint(g, c);
int w = c.getWidth();
int h = c.getHeight();
// 已经停止运行,直接返回
if (!isRunning)
return;
Graphics2D g2 = (Graphics2D) g.create();
Composite urComposite = g2.getComposite();
g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, .5f));
// 填充矩形
g2.fillRect(0, 0, w, h);
g2.setComposite(urComposite);
// -----下面代码开始绘制转动中的“齿轮”----
// 计算得到宽、高中较小值的1/5
int s = Math.min(w, h) / 5;
int cx = w / 2;
int cy = h / 2;
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
// 设置笔触
g2.setStroke(new BasicStroke(s / 2, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
g2.setPaint(Color.BLUE);
// 画笔绕被装饰组件的中心转过angle度
g2.rotate(Math.PI * angle / 180, cx, cy);
// 循环绘制12条线条,形成“齿轮”
for (int i = 0; i < 12; i++) {
float scale = (11.0f - (float) i) / 11.0f;
g2.drawLine(cx + s, cy, cx + s * 2, cy);
g2.rotate(-Math.PI / 6, cx, cy);
g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, scale));
}
g2.dispose();
}

// 控制等待(齿轮开始转动)的方法
public void start() {
// 如果已经在运行中,直接返回
if (isRunning)
return;
isRunning = true;
// 每隔0.1秒重绘一次
timer = new Timer(100, e -> {
if (isRunning) {
// 触发applyPropertyChange()方法,让JLayer重绘。
// 在这行代码中,后面两个参数没有意义。
firePropertyChange("crazyitFlag", 0, 1);
// 角度加6
angle += 6; // ②
// 到达360后再从0开始
if (angle >= 360)
angle = 0;
}
});
timer.start();
}

// 控制停止等待(齿轮停止转动)的方法
public void stop() {
isRunning = false;
// 最后通知JLayer重绘一次,清除曾经绘制的图形
firePropertyChange("crazyitFlag", 0, 1);
timer.stop();
}

public void applyPropertyChange(PropertyChangeEvent pce, JLayer layer) {
// 控制JLayer重绘
if (pce.getPropertyName().equals("crazyitFlag")) {
layer.repaint();
}
}
}

程序 主类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public class WaitingJLayerTest {
public void init() {
JFrame f = new JFrame("转动的“齿轮”");
JPanel p = new JPanel();
ButtonGroup group = new ButtonGroup();
JRadioButton radioButton;
// 创建3个RadioButton,并将它们添加成一组
p.add(radioButton = new JRadioButton("网购购买", true));
group.add(radioButton);
p.add(radioButton = new JRadioButton("书店购买"));
group.add(radioButton);
p.add(radioButton = new JRadioButton("图书馆借阅"));
group.add(radioButton);
// 添加3个JCheckBox
p.add(new JCheckBox("疯狂Java讲义"));
p.add(new JCheckBox("疯狂Android讲义"));
p.add(new JCheckBox("疯狂Ajax讲义"));
p.add(new JCheckBox("轻量级Java EE企业应用"));
JButton orderButton = new JButton("投票");
p.add(orderButton);
// 创建LayerUI对象
final WaitingLayerUI layerUI = new WaitingLayerUI();
// 使用layerUI来装饰指定JPanel组件
JLayer<JComponent> layer = new JLayer<JComponent>(p, layerUI);
// 设置4秒之后执行指定动作:调用layerUI的stop()方法
final Timer stopper = new Timer(4000, ae -> layerUI.stop());
// 设置stopper定时器只触发一次。
stopper.setRepeats(false);
// 为orderButton绑定事件监听器:单击该按钮时:调用layerUI的start()方法
orderButton.addActionListener(ae -> {
layerUI.start();
// 如果stopper定时器已停止,启动它
if (!stopper.isRunning()) {
stopper.start();
}
});
// 将装饰后的JPanel组件添加到容器中
f.add(layer);
f.setSize(300, 170);
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.setVisible(true);
}

public static void main(String[] args) {
new WaitingJLayerTest().init();
}
}

上面程序中的①号粗体字代码定义了一个angle变量,它负责控制12条线条的旋转角度。程序使用Timer定时地改变angle变量的值(每隔0.1秒angle加6),如③号粗体字代码所示。控制了angle角度之后,程序根据该angle角度绘制12条线条,如②号粗体字代码所示。

提供了WaitingLayerUI之后,接下来使用该WaitingLayerUI与使用前面的UI没有任何区别。不过程序需要通过特定事件来显示WaitingLayerUI的绘制(就是调用它的start方法),下面程序为按钮添加了事件监听器:当用户单击该按钮时,程序会调用WaitingLayerUI对象的start方法。

1
2
3
4
5
6
7
8
// 为orderButton绑定事件监听器:单击该按钮时:调用layerUI的start()方法
orderButton.addActionListener(ae -> {
layerUI.start();
// 如果stopper定时器已停止,启动它
if (!stopper.isRunning()) {
stopper.start();
}
});

除此之外,上面代码中还用到了stopper计时器,它会控制在一段时间(比如4秒)之后停止绘制WaitingLayerUI,因此程序还通过如下代码进行控制

1
2
3
4
// 设置4秒之后执行指定动作:调用layerUI的stop()方法
final Timer stopper = new Timer(4000, ae -> layerUI.stop());
// 设置stopper定时器只触发一次。
stopper.setRepeats(false);

再次运行该程序,可以看到如图12.26所示的“动画装饰”效果。

这里有一张图片

通过上面几个例子可以看出,Swing提供的JLayer为窗口美化提供了无限可能性。只要你想做的,比如希望用户完成输入之后,立即在后面显示一个简单的提示按钮(钩表示输入正确,叉表示输入错误)……都可以通过JLayer绘制。