TWaver - 專注UI技術

          http://twaver.servasoft.com/
          posts - 171, comments - 191, trackbacks - 0, articles - 2
            BlogJava :: 首頁 :: 新隨筆 :: 聯系 :: 聚合  :: 管理

          讓JTextField添加“自動完成”功能

          Posted on 2012-06-12 14:28 TWaver 閱讀(2565) 評論(4)  編輯  收藏
               在越來越重視“用戶體驗”的今天,一個簡單的文本框也演進的越來越智能了。比如Google的搜索,當我們輸入搜索關鍵字的過程中,文本框就會動態的下拉列出最常輸入的近似文字,以便我們快速輸入要查詢的內容。當然一直抄襲Google的百度自然也是一樣。類似的例子還有很多,例如一般的郵件客戶端,在敲入地址時,也會動態列出符合要求的地址,方便快速錄入,也會減少出錯。


               那么,Swing的文本框要做到這一點是否容易呢?網上的例子也能搜索到一些,不過要么功能做的太簡單,要么實現的代碼太繁瑣羅嗦。還有一些商業的Swing組件,則完全是要付費的。本文結合了2BizBox免費ERP軟件開發中的實踐,嘗試了一種非常簡單、有效的方法來制作這一效果。

               首先仔細觀察這種效果:它外觀上、本質上,都完全是一個文本框,而不是下拉框。所以,我們不想把它做成下拉框,也就是不想從JComboBox繼承。另外,下拉列表提示的出現,是完全異步、動態的,它僅僅作為提示,不能干預正常的文本框的輸入。最后,那個下拉列表的外觀和行為則完全是一個JComboBox的下拉列表行為。所以,這個“可自動完成的JTextField”應當是一個JTextField和JComboBox下拉列表部分的結合體。
               經過以上分析,思路基本確定:它本質是一個JTextField,但是又結合利用了一個JComboBox的下拉列表。二者合而為一即可。那么是從誰繼承呢?JTextField嗎?
               仔細想想,繼承并不是最好的方法。俗話說:繼承是混蛋。能不繼承就不要繼承。為啥呢?繼承,意味著別人只能繼承你的類,才能使用這一功能。假如你的項目已經寫了一萬多個界面,想給這里面的一些文本框增加這種智能提示功能,難道要對所有代碼進行修改,讓那些東西重新繼承你的類嗎?這無疑是個爛主意。所以,那些剛學會OO的童鞋,總是喜歡動不動就要繼承的思路,并不妥當。如果我們只是提供一個Util方法,對已經存在的普通JTextField實例處理一下,就可以具有智能提示,豈不是更好?
               要做到JTextField和JComboBox這兩個組件的結合,這里使用了非常“怪異”的一個絕招,你絕對想不到:把一個JComboBox塞到JTextField的身體里面,并讓它看不見。看一下代碼:
          1 JTextField txtInput = new JTextField();
          2 JComboBox cbInput = new JComboBox();
          3 txtInput.setLayout(new BorderLayout());
          4 txtInput.add(cbInput, BorderLayout.SOUTH);

               什么?把JTextField設置一個layout?并且還add一個JComboBox且放在SOUTH?我相信你絕對聞所未聞這種事情。怎么看都是怪胎啊。不要緊,把JComboBox的高度變成0,別人就看不出破綻了:
          1 JComboBox cbInput = new JComboBox(model) {
          2     public Dimension getPreferredSize() {
          3         return new Dimension(super.getPreferredSize().width, 0);
          4     }
          5 };

               雖然combo看不見,但是它實實在在存在于文本框的身體里,且位于其下方。我們的思路是:當文本框輸入內容時,我們判斷下拉框中是否有符合要求的列表,如果有,就馬上主動彈出下拉;否則就讓下拉消失。
               監控文本框輸入并不難:給它的document增加listener就行了。這里我們使用了“不區分大小寫”、“和輸入字符串開頭相同的項”的規則進行過濾。將所有備選字符串置于單獨一個數組中,每次用戶輸入后,動態過濾出符合條件的字符串,動態添加到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 });

              此外,為了更方便操作,我們再增加幾個快捷鍵:當輸入ESC,主動關掉下拉列表;當輸入回車或空格,直接把第一項符合要求的字符串輸入文本框:
           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 });

                還有一個非常重要的技術要點要進行說明。在popup列表彈出的時候,我們希望用箭頭能夠上下移動選擇條目,但是又同時希望當前的光標和焦點不要離開文本框。這個好像非常難實現啊!請看我們是如何做到的:在監控到上下箭頭輸入時候,把當前的鍵盤事件的source動態修改為JComboBox,然后派發給JComboBox。也就是說,本來事件是輸入到文本框的,我們把郵遞員攔截下來,把收件人改一下,繼續交給郵遞員進行派發。這樣,就做到“移花接木”了:
          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 }

                最后,為了演示效果,我們放一些數據到下拉列表中。放什么呢?自己造假數據太麻煩了,干脆用Java中的“所有國家”的數據吧,簡單省事:
          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 }

                最后看一下效果,完全符合我們的預期:
                以下是完整代碼:
            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 }

          評論

          # re: 讓JTextField添加“自動完成”功能  回復  更多評論   

          2012-06-12 14:37 by 杭州房產
          老師說的真的很清楚,對于我們想要學習變成的人真的很有幫助,謝謝老師。

          # re: 讓JTextField添加“自動完成”功能  回復  更多評論   

          2012-06-18 09:25 by allenny
          幾行

          # re: 讓JTextField添加“自動完成”功能  回復  更多評論   

          2012-07-05 10:13 by 唐軍虎
          誰都知道,繼承是面向對象的基本思想之一,不鼓勵繼承,我懷疑作者的水平。
          全部的代碼,從上到下,過程執行,有幾個函數,但是總體來說,封裝性極差。

          # re: 讓JTextField添加“自動完成”功能  回復  更多評論   

          2012-07-05 10:15 by 唐軍虎
          另外,程序的可讀性極差,這不是用來欣賞的代碼,而是用來害人的代碼,不看為好。

          只有注冊用戶登錄后才能發表評論。


          網站導航:
           
          主站蜘蛛池模板: 资源县| 随州市| 铜陵市| 济源市| 分宜县| 揭西县| 冀州市| 周口市| 房山区| 抚顺市| 县级市| 南郑县| 盘锦市| 孙吴县| 九龙城区| 富阳市| 仙游县| 钦州市| 金门县| 安溪县| 林口县| 卢龙县| 岫岩| 灵山县| 旌德县| 麟游县| 齐河县| 芜湖市| 马龙县| 建昌县| 裕民县| 绥宁县| 湘西| 濉溪县| 康定县| 禄劝| 宁海县| 浦江县| 繁昌县| 沅江市| 简阳市|