在越來越重視“用戶體驗(yàn)”的今天,一個(gè)簡單的文本框也演進(jìn)的越來越智能了。比如Google的搜索,當(dāng)我們輸入搜索關(guān)鍵字的過程中,文本框就會(huì)動(dòng)態(tài)的下拉列出最常輸入的近似文字,以便我們快速輸入要查詢的內(nèi)容。當(dāng)然一直抄襲Google的百度自然也是一樣。類似的例子還有很多,例如一般的郵件客戶端,在敲入地址時(shí),也會(huì)動(dòng)態(tài)列出符合要求的地址,方便快速錄入,也會(huì)減少出錯(cuò)。


那么,Swing的文本框要做到這一點(diǎn)是否容易呢?網(wǎng)上的例子也能搜索到一些,不過要么功能做的太簡單,要么實(shí)現(xiàn)的代碼太繁瑣羅嗦。還有一些商業(yè)的Swing組件,則完全是要付費(fèi)的。本文結(jié)合了2BizBox免費(fèi)ERP軟件開發(fā)中的實(shí)踐,嘗試了一種非常簡單、有效的方法來制作這一效果。
首先仔細(xì)觀察這種效果:它外觀上、本質(zhì)上,都完全是一個(gè)文本框,而不是下拉框。所以,我們不想把它做成下拉框,也就是不想從JComboBox繼承。另外,下拉列表提示的出現(xiàn),是完全異步、動(dòng)態(tài)的,它僅僅作為提示,不能干預(yù)正常的文本框的輸入。最后,那個(gè)下拉列表的外觀和行為則完全是一個(gè)JComboBox的下拉列表行為。所以,這個(gè)“可自動(dòng)完成的JTextField”應(yīng)當(dāng)是一個(gè)JTextField和JComboBox下拉列表部分的結(jié)合體。
經(jīng)過以上分析,思路基本確定:它本質(zhì)是一個(gè)JTextField,但是又結(jié)合利用了一個(gè)JComboBox的下拉列表。二者合而為一即可。那么是從誰繼承呢?JTextField嗎?
仔細(xì)想想,繼承并不是最好的方法。俗話說:繼承是混蛋。能不繼承就不要繼承。為啥呢?繼承,意味著別人只能繼承你的類,才能使用這一功能。假如你的項(xiàng)目已經(jīng)寫了一萬多個(gè)界面,想給這里面的一些文本框增加這種智能提示功能,難道要對所有代碼進(jìn)行修改,讓那些東西重新繼承你的類嗎?這無疑是個(gè)爛主意。所以,那些剛學(xué)會(huì)OO的童鞋,總是喜歡動(dòng)不動(dòng)就要繼承的思路,并不妥當(dāng)。如果我們只是提供一個(gè)Util方法,對已經(jīng)存在的普通JTextField實(shí)例處理一下,就可以具有智能提示,豈不是更好?
要做到JTextField和JComboBox這兩個(gè)組件的結(jié)合,這里使用了非常“怪異”的一個(gè)絕招,你絕對想不到:把一個(gè)JComboBox塞到JTextField的身體里面,并讓它看不見。看一下代碼:
什么?把JTextField設(shè)置一個(gè)layout?并且還add一個(gè)JComboBox且放在SOUTH?我相信你絕對聞所未聞這種事情。怎么看都是怪胎啊。不要緊,把JComboBox的高度變成0,別人就看不出破綻了:
雖然combo看不見,但是它實(shí)實(shí)在在存在于文本框的身體里,且位于其下方。我們的思路是:當(dāng)文本框輸入內(nèi)容時(shí),我們判斷下拉框中是否有符合要求的列表,如果有,就馬上主動(dòng)彈出下拉;否則就讓下拉消失。
監(jiān)控文本框輸入并不難:給它的document增加listener就行了。這里我們使用了“不區(qū)分大小寫”、“和輸入字符串開頭相同的項(xiàng)”的規(guī)則進(jìn)行過濾。將所有備選字符串置于單獨(dú)一個(gè)數(shù)組中,每次用戶輸入后,動(dòng)態(tài)過濾出符合條件的字符串,動(dòng)態(tài)添加到JComboBox中,并將其下拉列表Popup出來即可:
此外,為了更方便操作,我們再增加幾個(gè)快捷鍵:當(dāng)輸入ESC,主動(dòng)關(guān)掉下拉列表;當(dāng)輸入回車或空格,直接把第一項(xiàng)符合要求的字符串輸入文本框:
還有一個(gè)非常重要的技術(shù)要點(diǎn)要進(jìn)行說明。在popup列表彈出的時(shí)候,我們希望用箭頭能夠上下移動(dòng)選擇條目,但是又同時(shí)希望當(dāng)前的光標(biāo)和焦點(diǎn)不要離開文本框。這個(gè)好像非常難實(shí)現(xiàn)啊!請看我們是如何做到的:在監(jiān)控到上下箭頭輸入時(shí)候,把當(dāng)前的鍵盤事件的source動(dòng)態(tài)修改為JComboBox,然后派發(fā)給JComboBox。也就是說,本來事件是輸入到文本框的,我們把郵遞員攔截下來,把收件人改一下,繼續(xù)交給郵遞員進(jìn)行派發(fā)。這樣,就做到“移花接木”了:
最后,為了演示效果,我們放一些數(shù)據(jù)到下拉列表中。放什么呢?自己造假數(shù)據(jù)太麻煩了,干脆用Java中的“所有國家”的數(shù)據(jù)吧,簡單省事:
最后看一下效果,完全符合我們的預(yù)期:
以下是完整代碼:
首先仔細(xì)觀察這種效果:它外觀上、本質(zhì)上,都完全是一個(gè)文本框,而不是下拉框。所以,我們不想把它做成下拉框,也就是不想從JComboBox繼承。另外,下拉列表提示的出現(xiàn),是完全異步、動(dòng)態(tài)的,它僅僅作為提示,不能干預(yù)正常的文本框的輸入。最后,那個(gè)下拉列表的外觀和行為則完全是一個(gè)JComboBox的下拉列表行為。所以,這個(gè)“可自動(dòng)完成的JTextField”應(yīng)當(dāng)是一個(gè)JTextField和JComboBox下拉列表部分的結(jié)合體。
經(jīng)過以上分析,思路基本確定:它本質(zhì)是一個(gè)JTextField,但是又結(jié)合利用了一個(gè)JComboBox的下拉列表。二者合而為一即可。那么是從誰繼承呢?JTextField嗎?
仔細(xì)想想,繼承并不是最好的方法。俗話說:繼承是混蛋。能不繼承就不要繼承。為啥呢?繼承,意味著別人只能繼承你的類,才能使用這一功能。假如你的項(xiàng)目已經(jīng)寫了一萬多個(gè)界面,想給這里面的一些文本框增加這種智能提示功能,難道要對所有代碼進(jìn)行修改,讓那些東西重新繼承你的類嗎?這無疑是個(gè)爛主意。所以,那些剛學(xué)會(huì)OO的童鞋,總是喜歡動(dòng)不動(dòng)就要繼承的思路,并不妥當(dāng)。如果我們只是提供一個(gè)Util方法,對已經(jīng)存在的普通JTextField實(shí)例處理一下,就可以具有智能提示,豈不是更好?
要做到JTextField和JComboBox這兩個(gè)組件的結(jié)合,這里使用了非常“怪異”的一個(gè)絕招,你絕對想不到:把一個(gè)JComboBox塞到JTextField的身體里面,并讓它看不見。看一下代碼:
1 JTextField txtInput = new JTextField();
2 JComboBox cbInput = new JComboBox();
3 txtInput.setLayout(new BorderLayout());
4 txtInput.add(cbInput, BorderLayout.SOUTH);
2 JComboBox cbInput = new JComboBox();
3 txtInput.setLayout(new BorderLayout());
4 txtInput.add(cbInput, BorderLayout.SOUTH);
什么?把JTextField設(shè)置一個(gè)layout?并且還add一個(gè)JComboBox且放在SOUTH?我相信你絕對聞所未聞這種事情。怎么看都是怪胎啊。不要緊,把JComboBox的高度變成0,別人就看不出破綻了:
1 JComboBox cbInput = new JComboBox(model) {
2 public Dimension getPreferredSize() {
3 return new Dimension(super.getPreferredSize().width, 0);
4 }
5 };
2 public Dimension getPreferredSize() {
3 return new Dimension(super.getPreferredSize().width, 0);
4 }
5 };
雖然combo看不見,但是它實(shí)實(shí)在在存在于文本框的身體里,且位于其下方。我們的思路是:當(dāng)文本框輸入內(nèi)容時(shí),我們判斷下拉框中是否有符合要求的列表,如果有,就馬上主動(dòng)彈出下拉;否則就讓下拉消失。
監(jiān)控文本框輸入并不難:給它的document增加listener就行了。這里我們使用了“不區(qū)分大小寫”、“和輸入字符串開頭相同的項(xiàng)”的規(guī)則進(jìn)行過濾。將所有備選字符串置于單獨(dú)一個(gè)數(shù)組中,每次用戶輸入后,動(dòng)態(tài)過濾出符合條件的字符串,動(dòng)態(tài)添加到JComboBox中,并將其下拉列表Popup出來即可:
1 txtInput.getDocument().addDocumentListener(new DocumentListener() {
2 public void insertUpdate(DocumentEvent e) {
3 updateList();
4 }
5
6 public void removeUpdate(DocumentEvent e) {
7 updateList();
8 }
9
10 public void changedUpdate(DocumentEvent e) {
11 updateList();
12 }
13
14 private void updateList() {
15 setAdjusting(cbInput, true);
16 model.removeAllElements();
17 String input = txtInput.getText();
18 if (!input.isEmpty()) {
19 for (String item : items) {
20 if (item.toLowerCase().startsWith(input.toLowerCase())) {
21 model.addElement(item);
22 }
23 }
24 }
25 cbInput.setPopupVisible(model.getSize() > 0);
26 setAdjusting(cbInput, false);
27 }
28 });
2 public void insertUpdate(DocumentEvent e) {
3 updateList();
4 }
5
6 public void removeUpdate(DocumentEvent e) {
7 updateList();
8 }
9
10 public void changedUpdate(DocumentEvent e) {
11 updateList();
12 }
13
14 private void updateList() {
15 setAdjusting(cbInput, true);
16 model.removeAllElements();
17 String input = txtInput.getText();
18 if (!input.isEmpty()) {
19 for (String item : items) {
20 if (item.toLowerCase().startsWith(input.toLowerCase())) {
21 model.addElement(item);
22 }
23 }
24 }
25 cbInput.setPopupVisible(model.getSize() > 0);
26 setAdjusting(cbInput, false);
27 }
28 });
此外,為了更方便操作,我們再增加幾個(gè)快捷鍵:當(dāng)輸入ESC,主動(dòng)關(guān)掉下拉列表;當(dāng)輸入回車或空格,直接把第一項(xiàng)符合要求的字符串輸入文本框:
1 txtInput.addKeyListener(new KeyAdapter() {
2
3 @Override
4 public void keyPressed(KeyEvent e) {
5 setAdjusting(cbInput, true);
6 if (e.getKeyCode() == KeyEvent.VK_SPACE) {
7 if (cbInput.isPopupVisible()) {
8 e.setKeyCode(KeyEvent.VK_ENTER);
9 }
10 }
11 if (e.getKeyCode() == KeyEvent.VK_ENTER || e.getKeyCode() == KeyEvent.VK_UP || e.getKeyCode() == KeyEvent.VK_DOWN) {
12 e.setSource(cbInput);
13 cbInput.dispatchEvent(e);
14 if (e.getKeyCode() == KeyEvent.VK_ENTER) {
15 txtInput.setText(cbInput.getSelectedItem().toString());
16 cbInput.setPopupVisible(false);
17 }
18 }
19 if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
20 cbInput.setPopupVisible(false);
21 }
22 setAdjusting(cbInput, false);
23 }
24 });
2
3 @Override
4 public void keyPressed(KeyEvent e) {
5 setAdjusting(cbInput, true);
6 if (e.getKeyCode() == KeyEvent.VK_SPACE) {
7 if (cbInput.isPopupVisible()) {
8 e.setKeyCode(KeyEvent.VK_ENTER);
9 }
10 }
11 if (e.getKeyCode() == KeyEvent.VK_ENTER || e.getKeyCode() == KeyEvent.VK_UP || e.getKeyCode() == KeyEvent.VK_DOWN) {
12 e.setSource(cbInput);
13 cbInput.dispatchEvent(e);
14 if (e.getKeyCode() == KeyEvent.VK_ENTER) {
15 txtInput.setText(cbInput.getSelectedItem().toString());
16 cbInput.setPopupVisible(false);
17 }
18 }
19 if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
20 cbInput.setPopupVisible(false);
21 }
22 setAdjusting(cbInput, false);
23 }
24 });
還有一個(gè)非常重要的技術(shù)要點(diǎn)要進(jìn)行說明。在popup列表彈出的時(shí)候,我們希望用箭頭能夠上下移動(dòng)選擇條目,但是又同時(shí)希望當(dāng)前的光標(biāo)和焦點(diǎn)不要離開文本框。這個(gè)好像非常難實(shí)現(xiàn)啊!請看我們是如何做到的:在監(jiān)控到上下箭頭輸入時(shí)候,把當(dāng)前的鍵盤事件的source動(dòng)態(tài)修改為JComboBox,然后派發(fā)給JComboBox。也就是說,本來事件是輸入到文本框的,我們把郵遞員攔截下來,把收件人改一下,繼續(xù)交給郵遞員進(jìn)行派發(fā)。這樣,就做到“移花接木”了:
1 if (e.getKeyCode() == KeyEvent.VK_ENTER || e.getKeyCode() == KeyEvent.VK_UP || e.getKeyCode() == KeyEvent.VK_DOWN) {
2 e.setSource(cbInput);
3 cbInput.dispatchEvent(e);
4 if (e.getKeyCode() == KeyEvent.VK_ENTER) {
5 txtInput.setText(cbInput.getSelectedItem().toString());
6 cbInput.setPopupVisible(false);
7 }
8 }
2 e.setSource(cbInput);
3 cbInput.dispatchEvent(e);
4 if (e.getKeyCode() == KeyEvent.VK_ENTER) {
5 txtInput.setText(cbInput.getSelectedItem().toString());
6 cbInput.setPopupVisible(false);
7 }
8 }
最后,為了演示效果,我們放一些數(shù)據(jù)到下拉列表中。放什么呢?自己造假數(shù)據(jù)太麻煩了,干脆用Java中的“所有國家”的數(shù)據(jù)吧,簡單省事:
1 Locale[] locales = Locale.getAvailableLocales();
2 for (int i = 0; i < locales.length; i++) {
3 String item = locales[i].getDisplayName();
4 items.add(item);
5 }
2 for (int i = 0; i < locales.length; i++) {
3 String item = locales[i].getDisplayName();
4 items.add(item);
5 }
最后看一下效果,完全符合我們的預(yù)期:
以下是完整代碼:
1 import java.awt.*;
2 import java.awt.event.*;
3 import java.util.*;
4
5 import javax.swing.*;
6 import javax.swing.event.*;
7
8 import twaver.*;
9
10 public class Test {
11
12 public static void main(String[] args) throws Exception {
13 UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
14 JFrame frame = new JFrame();
15 frame.setTitle("Auto Completion Test");
16 frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
17 frame.setBounds(200, 200, 500, 400);
18
19 ArrayList<String> items = new ArrayList<String>();
20 Locale[] locales = Locale.getAvailableLocales();
21 for (int i = 0; i < locales.length; i++) {
22 String item = locales[i].getDisplayName();
23 items.add(item);
24 }
25 JTextField txtInput = new JTextField();
26 setupAutoComplete(txtInput, items);
27 txtInput.setColumns(30);
28 frame.getContentPane().setLayout(new FlowLayout());
29 frame.getContentPane().add(txtInput, BorderLayout.NORTH);
30 frame.setVisible(true);
31 }
32
33 private static boolean isAdjusting(JComboBox cbInput) {
34 if (cbInput.getClientProperty("is_adjusting") instanceof Boolean) {
35 return (Boolean) cbInput.getClientProperty("is_adjusting");
36 }
37 return false;
38 }
39
40 private static void setAdjusting(JComboBox cbInput, boolean adjusting) {
41 cbInput.putClientProperty("is_adjusting", adjusting);
42 }
43
44 public static void setupAutoComplete(final JTextField txtInput, final ArrayList<String> items) {
45 final DefaultComboBoxModel model = new DefaultComboBoxModel();
46 final JComboBox cbInput = new JComboBox(model) {
47 public Dimension getPreferredSize() {
48 return new Dimension(super.getPreferredSize().width, 0);
49 }
50 };
51 setAdjusting(cbInput, false);
52 for (String item : items) {
53 model.addElement(item);
54 }
55 cbInput.setSelectedItem(null);
56 cbInput.addActionListener(new ActionListener() {
57 @Override
58 public void actionPerformed(ActionEvent e) {
59 if (!isAdjusting(cbInput)) {
60 if (cbInput.getSelectedItem() != null) {
61 txtInput.setText(cbInput.getSelectedItem().toString());
62 }
63 }
64 }
65 });
66
67 txtInput.addKeyListener(new KeyAdapter() {
68
69 @Override
70 public void keyPressed(KeyEvent e) {
71 setAdjusting(cbInput, true);
72 if (e.getKeyCode() == KeyEvent.VK_SPACE) {
73 if (cbInput.isPopupVisible()) {
74 e.setKeyCode(KeyEvent.VK_ENTER);
75 }
76 }
77 if (e.getKeyCode() == KeyEvent.VK_ENTER || e.getKeyCode() == KeyEvent.VK_UP || e.getKeyCode() == KeyEvent.VK_DOWN) {
78 e.setSource(cbInput);
79 cbInput.dispatchEvent(e);
80 if (e.getKeyCode() == KeyEvent.VK_ENTER) {
81 txtInput.setText(cbInput.getSelectedItem().toString());
82 cbInput.setPopupVisible(false);
83 }
84 }
85 if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
86 cbInput.setPopupVisible(false);
87 }
88 setAdjusting(cbInput, false);
89 }
90 });
91 txtInput.getDocument().addDocumentListener(new DocumentListener() {
92 public void insertUpdate(DocumentEvent e) {
93 updateList();
94 }
95
96 public void removeUpdate(DocumentEvent e) {
97 updateList();
98 }
99
100 public void changedUpdate(DocumentEvent e) {
101 updateList();
102 }
103
104 private void updateList() {
105 setAdjusting(cbInput, true);
106 model.removeAllElements();
107 String input = txtInput.getText();
108 if (!input.isEmpty()) {
109 for (String item : items) {
110 if (item.toLowerCase().startsWith(input.toLowerCase())) {
111 model.addElement(item);
112 }
113 }
114 }
115 cbInput.setPopupVisible(model.getSize() > 0);
116 setAdjusting(cbInput, false);
117 }
118 });
119 txtInput.setLayout(new BorderLayout());
120 txtInput.add(cbInput, BorderLayout.SOUTH);
121 }
122 }
2 import java.awt.event.*;
3 import java.util.*;
4
5 import javax.swing.*;
6 import javax.swing.event.*;
7
8 import twaver.*;
9
10 public class Test {
11
12 public static void main(String[] args) throws Exception {
13 UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
14 JFrame frame = new JFrame();
15 frame.setTitle("Auto Completion Test");
16 frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
17 frame.setBounds(200, 200, 500, 400);
18
19 ArrayList<String> items = new ArrayList<String>();
20 Locale[] locales = Locale.getAvailableLocales();
21 for (int i = 0; i < locales.length; i++) {
22 String item = locales[i].getDisplayName();
23 items.add(item);
24 }
25 JTextField txtInput = new JTextField();
26 setupAutoComplete(txtInput, items);
27 txtInput.setColumns(30);
28 frame.getContentPane().setLayout(new FlowLayout());
29 frame.getContentPane().add(txtInput, BorderLayout.NORTH);
30 frame.setVisible(true);
31 }
32
33 private static boolean isAdjusting(JComboBox cbInput) {
34 if (cbInput.getClientProperty("is_adjusting") instanceof Boolean) {
35 return (Boolean) cbInput.getClientProperty("is_adjusting");
36 }
37 return false;
38 }
39
40 private static void setAdjusting(JComboBox cbInput, boolean adjusting) {
41 cbInput.putClientProperty("is_adjusting", adjusting);
42 }
43
44 public static void setupAutoComplete(final JTextField txtInput, final ArrayList<String> items) {
45 final DefaultComboBoxModel model = new DefaultComboBoxModel();
46 final JComboBox cbInput = new JComboBox(model) {
47 public Dimension getPreferredSize() {
48 return new Dimension(super.getPreferredSize().width, 0);
49 }
50 };
51 setAdjusting(cbInput, false);
52 for (String item : items) {
53 model.addElement(item);
54 }
55 cbInput.setSelectedItem(null);
56 cbInput.addActionListener(new ActionListener() {
57 @Override
58 public void actionPerformed(ActionEvent e) {
59 if (!isAdjusting(cbInput)) {
60 if (cbInput.getSelectedItem() != null) {
61 txtInput.setText(cbInput.getSelectedItem().toString());
62 }
63 }
64 }
65 });
66
67 txtInput.addKeyListener(new KeyAdapter() {
68
69 @Override
70 public void keyPressed(KeyEvent e) {
71 setAdjusting(cbInput, true);
72 if (e.getKeyCode() == KeyEvent.VK_SPACE) {
73 if (cbInput.isPopupVisible()) {
74 e.setKeyCode(KeyEvent.VK_ENTER);
75 }
76 }
77 if (e.getKeyCode() == KeyEvent.VK_ENTER || e.getKeyCode() == KeyEvent.VK_UP || e.getKeyCode() == KeyEvent.VK_DOWN) {
78 e.setSource(cbInput);
79 cbInput.dispatchEvent(e);
80 if (e.getKeyCode() == KeyEvent.VK_ENTER) {
81 txtInput.setText(cbInput.getSelectedItem().toString());
82 cbInput.setPopupVisible(false);
83 }
84 }
85 if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
86 cbInput.setPopupVisible(false);
87 }
88 setAdjusting(cbInput, false);
89 }
90 });
91 txtInput.getDocument().addDocumentListener(new DocumentListener() {
92 public void insertUpdate(DocumentEvent e) {
93 updateList();
94 }
95
96 public void removeUpdate(DocumentEvent e) {
97 updateList();
98 }
99
100 public void changedUpdate(DocumentEvent e) {
101 updateList();
102 }
103
104 private void updateList() {
105 setAdjusting(cbInput, true);
106 model.removeAllElements();
107 String input = txtInput.getText();
108 if (!input.isEmpty()) {
109 for (String item : items) {
110 if (item.toLowerCase().startsWith(input.toLowerCase())) {
111 model.addElement(item);
112 }
113 }
114 }
115 cbInput.setPopupVisible(model.getSize() > 0);
116 setAdjusting(cbInput, false);
117 }
118 });
119 txtInput.setLayout(new BorderLayout());
120 txtInput.add(cbInput, BorderLayout.SOUTH);
121 }
122 }