今天看到一个特殊的业务处理场景,我们知道一般TextField用户输入时字符串都没有分开,那么如果有人告诉你要写成这种格式怎么办?
这个时候我相信很多人第一反应就是“不知所措“,这个从哪里下手,有些人就是拒绝需求,不做更改,还有人会说我就会web上这种字符串处理,说再说最后都是逃不了manager的一句话。好了,废话就说这么多吧,切入正题。首先你要又一个用户输入内容侦听的一个东西,这里就是Document,这里是oracle关于Document类的介绍,然后如果还没用过Document的同志最好先找两个简单的例子看一下,然后再看下面这个例子。好了,开始来点干货:
package lock;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.util.ArrayList;
import java.util.List;
import javax.swing.BorderFactory;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JPasswordField;
import javax.swing.JTextField;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.PlainDocument;
public class SwingFormatTextInput extends JFrame {
/**
* 第三个版本,可能后面还会对这个方法进一步补充,希望能做一个有用的工具类,之前那个版本发现在osx下每次在5,10处发生BadLocation异常,因为光标没有到达前一组数字的末尾,然后就是insertString中有改动,之前用str去做拼接,这是非常致命的错误,应该改为去处所有空格后的字符串。
* 仍需要解决的问题:如果你有这样一组数:1234 567,然后你选中567同时输入3,发现此时3并不能输入。(第二版本中,已解决)
*/
private static final long serialVersionUID = 3L;
private JTextField username;
private JPasswordField passworld;
private JLabel name;
private JLabel pass;
private JPanel root;
public SwingFormatTextInput(){
super("Document Practice");
JPanel userNamePanel = new JPanel();
JPanel passWorldPanel = new JPanel();
name = new JLabel("用户名");
pass = new JLabel("密 码");
username = new JTextField(10);
username.setDocument(new MyPlainDocument("1111 1111 1111", username));
//这个监听器个人感觉还不如直接传入一个自定义Document有用
username.getDocument().addDocumentListener(new DocumentListener() {
@Override
public void removeUpdate(DocumentEvent e) {
}
@Override
public void insertUpdate(DocumentEvent e) {
Document doc = e.getDocument();
}
@Override
public void changedUpdate(DocumentEvent e) {
// TODO Auto-generated method stub
}
});
userNamePanel.add(name);
userNamePanel.add(username);
passworld = new JPasswordField(10);
passWorldPanel.add(pass);
passWorldPanel.add(passworld);
root = new JPanel();
root.setBorder(BorderFactory.createEmptyBorder(100, 100, 90, 100));
root.add(userNamePanel);
root.add(passWorldPanel, BorderLayout.BEFORE_FIRST_LINE);
this.add(root);
this.setSize(new Dimension(400,400));
this.setLocation(300, 400);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setVisible(true);
}
public static void main(String[] args) {
new SwingFormatTextInput();
}
class MyPlainDocument extends PlainDocument{
JTextField textField;
List<Integer> formatNumber;
public MyPlainDocument(String format){
this(format, null);
}
public MyPlainDocument(String format, JTextField textField){
this.textField = textField;
String[] divide = format.split(" ");
formatNumber = new ArrayList<Integer>();
int length = 0;
//以4, 9, 14这种顺序创建一个数列
for(int i = 0; i < divide.length; i++){
if(i == 0){
length = divide[i].length();
}else {
length += divide[i].length() + 1;
}
formatNumber.add(length);
}
}
/**
* 以输入格式输出字符串
* @offs 当前光标距textfield中原始字符串首个位置的位移
* @str 输入字符串
*/
@Override
public void insertString(int offs, String str, AttributeSet a) throws BadLocationException {
if(str == null || str.equals(""))
return;
String text = str.replaceAll("\\s", "");//去掉所有空格,针对一次粘贴进去的多个字符串
String oldText = getText(0, getLength());
String origin = oldText.replaceAll("\\s", "") + text;//多当前已经输入的字符串进行初始化
if(origin.length() > 12)
return;
//如果插入位置并不结尾而是中间怎么办?
if(offs < getLength()){
StringBuilder sb = new StringBuilder(oldText);
sb.insert(offs, str);
this.remove(0, getLength());
insertString(0, sb.toString().replaceAll("\\s", ""), null);
textField.setCaretPosition(offs+str.length());//设置光标的位置为当前位置
}else{
for(int i = 0; i < formatNumber.size(); i++){
int temp = formatNumber.get(i) - oldText.length();
if(temp < text.length() && temp >= 0){
text = text.substring(0, temp) + " " + text.substring(temp);
}
}
super.insertString(offs, text, a);
}
}
@Override
public void remove(int offs, int len) throws BadLocationException {
StringBuilder sb = new StringBuilder(getText(0, getLength()));
sb.delete(offs, offs + len);
String str = sb.toString().replaceAll("\\s", "");
super.remove(0, getLength());
insertString(0, str, null);
//这个地方还有待进步,好像有平台间不同,windows下这里不需要增加判断
if(offs != 5 && offs != 10)
textField.setCaretPosition(offs);
else
textField.setCaretPosition(offs - 1);
}
@Override
public void replace(int offset, int length, String text, AttributeSet attrs) throws BadLocationException {
StringBuilder sb = new StringBuilder(getText(0, getLength()));
sb.replace(offset, offset+length, text);
remove(0, getLength());
insertString(0, sb.toString(), null);
}
}
}
在实现这个类时我尝试了多种方式,第一种就是使用documentListener来完成,但是后来发现documentListener虽然对用户输入增加、删除、更改有监听,但是最字符串的重组、用户即时输入字符串的刷新问题上没有好的解决方法,看好insertString方法,主要是因为它有一个super(xxx)对用户输入字符串会及时刷新到面板上,并且这个方法主调是jdk本身,所以一般情况下程序主动调没什么作用。
第一个版本做出来时已经可以对用户“挨个输入“和“整体复制粘贴“正常,但是同时发现两个问题(这时还没有remove方法以及insertString中对光标插入位置判断语句):
- 删除掉remove方法你会发现程序还能跑,但是当你在输入框中对已经输入的内容中间添加一个数字会发现字符串又开始不按照我们格式了,那是因为Document默认的remove方法并不会调用insertString,自然已经输入的字符串不会重排。
- 第二个问题,就算你重写了remove方法,你现在的删除操作正常了,但是当你在已经输入的字符串中添加字符串时发现问题又出现了,这个问题还比较棘手,看着代码实现比较简单,但是实际动手你会发现因为这个super()方法最后被调用,程序无数次回到原始形态,怎么插入都不能保证“1111 1111 1111“这种形态,我花了三四个小时才想出来,哎!想想都惭愧啊,不过终归最后还是想出来了,数据结构没白学啊,递归如何使用还真的回头好好研究一下咯。
之前那个版本的代码明显有很多问题,但是很可惜,很多人看后好像都没有提出问题,还是后来测试组同事告诉我部分地方可能有风险,我才及时回来重新检查了一下之前代码,这里已经更新了。但是这个仍需改进的问题已经写在了开始注释中,实在是想不出来了,jdk内部方法是先remove,然后在insert,但是这之间的衔接完全正常,但是最后却出现了这样的结果: