12.12.3 使用JFormattedTextField

合理输入问题

在有些情况下,程序不希望用户在输入框内随意地输入,例如,程序需要用户输入一个有效的时间,或者需要用户输入一个有效的物品价格,如果用户输入不合理,程序应该阻止用户输入

使用事件监听实现

对于这种需求,通常的做法是为该文本框添加失去焦点的监听器,再添加回车按键的监听器,当该文本框失去焦点时,或者该用户在该文本框内按回车键时,就检测用户输入是否合法。这种做法基本可以解决该问题,但编程比较烦琐!

使用JFormattedTextField

Swing提供的JFormattedTextField可以更优雅地解决该问题。

JFormattedTextField和普通文本行的区别

使用JFormattedTextField与使用普通文本行有一个区别:

  • 它需要指定一个文本格式,只有当用户的输入满足该格式时,JFormattedTextField才会接收用户输入

JFormattedTextField格式分类

JFormattedTextField可以使用如下两种类型的格式。

  • JFormattedTextField.AbstractFormatter:该内部类有一个子类DefaultFormatter,而DefaultFormatter又有一个非常实用的MaskFormatter子类,允许程序以掩码的形式指定文本格式
  • Format:主要由DateFormatNumberFormat两个格式器组成,这两个格式器可以指定JFormattedTextField所能接收的格式字符串。

JFormattedTextField方法

创建JFormattedTextField对象时可以传入上面任意一个格式器:

构造器 描述
JFormattedTextField(Format format) Creates a JFormattedTextField.

成功地创建了JFormattedTextField对象之后,JFormattedTextField对象的用法和普通TextField的用法基本相似,一样可以调用setColumns()来设置该文本框的宽度,调用setFont()来设置该文本框内的字体等。

除此之外,JFormattedTextField还包含如下三个特殊方法

JFormattedTextField类的方法 描述
Object getValue() 获取该格式化文本框里的值
void setValue(Object value) 设置该格式化文本框的初始值
void setFocusLostBehavior(int behavior) 设置该格式化文本框失去焦点时的行为。

getValue方法

上面三个方法中获取格式化文本框内容的getValue方法返回Object类型,而不是返回String类型;

setValue方法

与之对应的是,设置格式化文本框初始值的setValue方法需要传入Object类型参数,而不是String类型参数。
这都是因为格式化文本框会将文本框内容转换成指定格式对应的对象,而不再是普通字符串。

setFocusLostBehavior方法

setFocusLostBehavior方法可以接收如下4个值:

setFocusLostBehavior方法参数值 描述
JFormattedTextField.COMMIT 如果用户输入的内容满足格式器的要求,则该格式化文本框显示的文本变成用户输入的内容,调用getValue()方法返回的是该文本框内显示的内容;如果用户输入的内容不满足格式器的要求,则该格式化文本框显示的依然是用户输入的内容,但调用getValue()方法返回的不是该文本框内显示的内容,而是上一个满足要求的值
JFormattedTextField.COMMIT_OR_REVERT 这是默认值。如果用户输入的内容满足格式器的要求,则该格式化文本框显示的文本、getValue()方法返回的都是用户输入的内容;如果用户输入的内容不满足格式器的要求,则该格式化文本框显示的文本、getValue()方法返回的都是上一个满足要求的值。
JFormattedTextField.REVERT 不管用户输入的内容是否满足格式器的要求,该格式化文本框都显示用户输入的内容,getValue()方法返回的都是上一个满足要求的值。
JFormattedTextField.PERSIST 不管用户输入的内容是否满足格式器的要求,该格式化文本框显示的内容、getValue()方法返回的都是上一个满足要求的值。在这种情况下,不管用户输入什么内容对该文本框都没有任何影响。

DefaultFormatter

DefaultFormatter是一个功能非常强大的格式器,它可以格式化任何类的实例,只要该类包含带一个字符串参数的构造器,并提供对应的toString()方法(该方法的返回值就是传入给构造器字符串参数的值)即可

例如,URL类包含一个URL(String spec)构造器,且URL对象的toString()方法恰好返回刚刚传入的spec参数,因此可以使用DefaultFormatter来格式化URL对象。当格式化文本框失去焦点时,该格式器就会调用带一个字符串参数的构造器来创建新的对象,如果构造器抛出了异常,即表明用户输入无效。

DefaultFormatter格式器默认采用改写方式来处理用户输入,即当用户在格式化文本框内输入时,每输入一个字符就会替换文本框内原来的一个字符。如釆想关闭这种改写方式,采用插入方式,则可通过调用它的setOverwriteMode(false)方法来实现。

MaskFormatter

MaskFormatter格式器的功能有点类似于正则表达式,它要求用户在格式化文本框内输入的内容必须匹配一定的掩码格式。例如,若要匹配广州地区的电话号码,则可采用020-########的格式,这个掩码字符串和正则表达式有一定的区别,因为该掩码字符串只支持如下通配符。

  • #:代表任何有效数字
  • ':转义字符,用于转义具有特殊格式的字符。例如,若想匹配#,则应该写成'#
  • U:任何字符,将所有小写字母映射为大写。
  • L:任何字符,将所有大写字母映射为小写。
  • A:任何字符或数字。
  • ?:任何字符。
  • *:可以匹配任何内容。
  • H:任何十六进制字符(0~9a~fA~F)。

值得指出的是,格式化文本框内的字符串总是和掩码具有相同的格式,连长度也完全相同。如果用户删除了格式化文本框内的字符,这些被删除的字符将由占位符替代。默认使用空格作为占位符,当然也可以调用MaskFormattersetPlaceholderCharacter()方法来设置该格式器的占位符。例如如下代码:

1
formatter.setPlaceholdercharacter(' ');

程序

下面程序示范了关于JFormattedTextField的简单用法。

1

上面程序添加了6个格式化文本框,其中两个是基于NumberFormat生成的整数格式器、货币格式器,两个是基于DateFormat生成的日期格式器,一个是使用DefaultFormatter创建的URL格式器,最后一个是使用MaskFormatter创建的掩码格式器,程序中的粗体字代码是创建这些格式器的关键代码。

除此之外,程序还添加了4个单选按钮,用于控制这些格式化文本框失去焦点后的行为。运行上面程序,并选中“COMMIT”行为,将看到如图12.59所示的界面

从图12.59中可以看出,虽然用户向格式化文本框内输入的内容与该文本框所要求的格式不符,但该文本框依然显示了用户输入的内容,只是后面显示该文本框的getValue()方法返回值时看到的依然是100,即上一个符合格式的值。

大部分时候,使用基于Format的格式器,DefaultFormatterMaskFormatter已经能满足绝大部分要求;但对于一些特殊的要求,则可以采用扩展DefaultFormatter的方式来定义自己的格式器。定义自己的格式器通常需要重写DefaultFormatter的如下两个方法

方法 描述
Object stringToValue(String string) 根据格式化文本框内的字符串来创建符合指定格式的对象。
String valueToString(Object value) 将符合格式的对象转换成文本框中显示的字符串

例如,若需要创建一个只能接收IP地址的格式化文本框,则可以创建一个自定义的格式化文本框,因为IP地址是由4个0~255之间的整数表示的,所以程序采用长度为4的byte数组来保存IP地址,程序可以采用如下方法将用户输入的字符串转换成byte数组。

如何保证用户输入的有效性

除此之外,Swing还提供了如下两种机制来保证用户输入的有效性

  • 输入过滤:输入过滤机制允许程序拦截用户的插入、替换、删除等操作,并改变用户所做的修改。
  • 输入校验:输入验证机制允许用户离开输入组件时,验证机制自动触发——如果用户输入不符合要求,校验器强制用户重新输入。

输入过滤器

输入过滤器需要继承DocumentFilter类,程序可以重写该类的如下三个方法来拦截用户的插入、删除和替换等操作。

方法 描述
void insertString(DocumentFilter.FilterBypass fb, int offset, String string, AttributeSet attr) 该方法会拦截用户向文档中插入字符串的操作。
void remove(DocumentFilter.FilterBypass fb, int offset, int length) 该方法会拦截用户从文档中删除字符串的操作。
void replace(DocumentFilter.FilterBypass fb, int offset, int length, String text, AttributeSet attrs) 该方法会拦截用户替换文档中字符串的操作。

输入校验器

为了创建自己的输入校验器,可以通过扩展InputVerifier类来实现。实际上,InputVerifier输入校验器可以绑定到任何输入组件,InputVerifier类里包含了一个verify(JComponent component)方法,当用户在该输入组件内输入完成,且该组件失去焦点时,该方法被调用,如果该方法返回false,即表明用户输入无效,该输入组件将自动得到焦点。也就是说,如果某个输入组件绑定了InputVerifier,则用户必须为该组件输入有效内容,否则用户无法离开该组件

有一种情况例外,如果输入焦点离开了带InputVerifier输入校验器的组件后,立即单击某个按钮,则该按钮的事件监听器将会在焦点重新回到原组件之前被触发。

程序 为格式化文本框 添加输入过滤器 输入校验器

下面程序示范了如何为格式化文本框添加输入过滤器、输入校验器,程序还自定义了一个IP地址格式器,该IP地址格式器扩展了DefaultFormatter格式器。

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
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
import java.util.*;
import java.text.*;
import java.net.*;
import java.awt.*;
import javax.swing.*;
import javax.swing.border.*;
import javax.swing.text.*;

public class JFormattedTextFieldTest
{
private JFrame mainWin = new JFrame("测试格式化文本框");
private JButton okButton = new JButton("确定");
// 定义用于添加格式化文本框的容器
private JPanel mainPanel = new JPanel();
JFormattedTextField[] fields = new JFormattedTextField[6];
String[] behaviorLabels = new String[]
{
"COMMIT",
"COMMIT_OR_REVERT",
"PERSIST",
"REVERT"
};
int[] behaviors = new int[]
{
JFormattedTextField.COMMIT,
JFormattedTextField.COMMIT_OR_REVERT,
JFormattedTextField.PERSIST,
JFormattedTextField.REVERT
};
ButtonGroup bg = new ButtonGroup();
public void init()
{
// 添加按钮
JPanel buttonPanel = new JPanel();
buttonPanel.add(okButton);
mainPanel.setLayout(new GridLayout(0, 3));
mainWin.add(mainPanel, BorderLayout.CENTER);
// 使用NumberFormat的integerInstance创建一个JFormattedTextField
fields[0] = new JFormattedTextField(NumberFormat
.getIntegerInstance());
// 设置初始值
fields[0].setValue(100);
addRow("整数格式文本框 :", fields[0]);
// 使用NumberFormat的currencyInstance创建一个JFormattedTextField
fields[1] = new JFormattedTextField(NumberFormat
.getCurrencyInstance());
fields[1].setValue(100.0);
addRow("货币格式文本框:", fields[1]);
// 使用默认的日期格式创建一个JFormattedTextField对象
fields[2] = new JFormattedTextField(DateFormat.getDateInstance());
fields[2].setValue(new Date());
addRow("默认的日期格式器:", fields[2]);
// 使用SHORT类型的日期格式创建一个JFormattedTextField对象,
// 且要求采用严格日期格式
DateFormat format = DateFormat.getDateInstance(DateFormat.SHORT);
// 要求采用严格的日期格式语法
format.setLenient(false);
fields[3] = new JFormattedTextField(format);
fields[3].setValue(new Date());
addRow("SHORT类型的日期格式器(语法严格):", fields[3]);
try
{
// 创建默认的DefaultFormatter对象
DefaultFormatter formatter = new DefaultFormatter();
// 关闭overwrite状态
formatter.setOverwriteMode(false);
fields[4] = new JFormattedTextField(formatter);
// 使用DefaultFormatter来格式化URL
fields[4].setValue(new URL("http://www.crazyit.org"));
addRow("URL:", fields[4]);
}
catch (MalformedURLException e)
{
e.printStackTrace();
}
try
{
MaskFormatter formatter = new MaskFormatter("020-########");
// 设置占位符
formatter.setPlaceholderCharacter('□');
fields[5] = new JFormattedTextField(formatter);
// 设置初始值
fields[5].setValue("020-28309378");
addRow("电话号码:", fields[5]);
}
catch (ParseException ex)
{
ex.printStackTrace();
}

JPanel focusLostPanel = new JPanel();
// 采用循环方式加入失去焦点行为的单选按钮
for (int i = 0; i < behaviorLabels.length ; i++ )
{
final int index = i;
final JRadioButton radio = new JRadioButton(behaviorLabels[i]);
// 默认选中第二个单选按钮
if (i == 1)
{
radio.setSelected(true);
}
focusLostPanel.add(radio);
bg.add(radio);
// 为所有单选按钮添加事件监听器
radio.addActionListener(e -> {
// 如果当前该单选按钮处于选中状态,
if (radio.isSelected())
{
// 设置所有的格式化文本框的失去焦点的行为
for (int j = 0 ; j < fields.length ; j++)
{
fields[j].setFocusLostBehavior(behaviors[index]);
}
}
});
}
focusLostPanel.setBorder(new TitledBorder(new EtchedBorder(),
"请选择焦点失去后的行为"));
JPanel p = new JPanel();
p.setLayout(new BorderLayout());
p.add(focusLostPanel , BorderLayout.NORTH);
p.add(buttonPanel , BorderLayout.SOUTH);

mainWin.add(p , BorderLayout.SOUTH);
mainWin.pack();
mainWin.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
mainWin.setVisible(true);
}
// 定义添加一行格式化文本框的方法
private void addRow(String labelText, final JFormattedTextField field)
{
mainPanel.add(new JLabel(labelText));
mainPanel.add(field);
final JLabel valueLabel = new JLabel();
mainPanel.add(valueLabel);
// 为"确定"按钮添加事件监听器
// 当用户单击“确定”按钮时,文本框后
okButton.addActionListener(event -> {
Object value = field.getValue();
// 输出格式化文本框的值
valueLabel.setText(value.toString());
});
}
public static void main(String[] args)
{
new JFormattedTextFieldTest().init();
}
}

运行上面程序,会看到窗口中出现三个格式化文本框,其中第一个格式化文本框只能输入数字,其他字符无法输入到该文本框内;第二个格式化文本框有输入校验器,只有当用户输入的内容符合该文本框的要求时,用户才可以离开该文本框;第三个格式化文本框的格式器是自定义的格式器,它要求用户输入的内容是一个合法的IP地址。