2009年1月18日 星期日

JTextComponent 即時搜尋

以下範例介紹如何實做出 如Firefox 即時搜尋功能
並且將搜尋到的文字以高亮度表示. 

 2009-01-19_005344

 

JTextComponentWithSearchText.java
001 package sui.text;
002 
003 import java.awt.BorderLayout;
004 import java.awt.Color;
005 import java.awt.Dimension;
006 import java.awt.event.ActionEvent;
007 import java.awt.event.ActionListener;
008 import java.awt.event.MouseAdapter;
009 import java.awt.event.MouseEvent;
010 import java.util.regex.Matcher;
011 import java.util.regex.Pattern;
012 import java.util.regex.PatternSyntaxException;
013 
014 import javax.swing.JFrame;
015 import javax.swing.JScrollPane;
016 import javax.swing.JTextField;
017 import javax.swing.JTextPane;
018 import javax.swing.event.DocumentEvent;
019 import javax.swing.event.DocumentListener;
020 import javax.swing.text.BadLocationException;
021 import javax.swing.text.Document;
022 import javax.swing.text.JTextComponent;
023 import javax.swing.text.SimpleAttributeSet;
024 import javax.swing.text.StyleConstants;
025 
026 public class JTextComponentWithSearchText implements DocumentListener, ActionListener{
027   /** 文本元件.*/
028   private JTextComponent textComponent;
029   /** 背景顏色.*/
030   private Color selectBackgroundColor;
031   /** 比對器.*/
032   private Matcher matcher;
033   /** 符合比對文字的總數.*/
034   private int matcherCount;
035   /** 滑鼠事件.*/
036   private MouseAdapter mouseAdapter = new MouseAdapter() {
037     public void mouseClicked(MouseEvent e) {
038       // 當滑鼠連點二下時,執行搜尋.
039       if (e.getClickCount() == 2) {
040         invokeSearch();
041       }
042     }
043    
044     public void mouseReleased(MouseEvent e) {
045       invokeSearch();
046     }
047    
048     /**
049      * 呼叫搜尋.
050      */
051     private void invokeSearch() {
052       // 紀錄目前選取的範圍和文字.
053       int start = textComponent.getCaret().getDot();
054       int end = textComponent.getCaret().getMark();
055       String searchText = textComponent.getSelectedText();
056       if (searchText == null) {
057         return;
058       }
059       JTextField field = new JTextField(searchText);
060       startNewSearch(field.getDocument());
061       setSelectionText(start, end, true);
062     }
063   };
064   /** 搜尋runable.*/
065   private Runnable searchRunnable = new Runnable() {
066     public void run() {
067       highlighterSearchWord();
068     }
069   };
070   /** 搜尋緒*/
071   private Thread thread = new Thread(searchRunnable);;
072  
073   /**
074    * 建立一個搜尋的本文元件.
075    * @param textComponent 本文元件.
076    */
077   public JTextComponentWithSearchText(JTextComponent textComponent) {
078     this(textComponent, textComponent.getSelectionColor());
079   }
080  
081   /**
082    * 建立一個搜尋的文本元件, 並且設定搜尋文字的背景.
083    * @param textComponent 本文元件.
084    * @param color 文字背景顏色.
085    */
086   public JTextComponentWithSearchText(JTextComponent textComponent, Color color) {
087     this.textComponent = textComponent;
088     selectBackgroundColor = color;
089     textComponent.addMouseListener(mouseAdapter);
090   }
091  
092   /**
093    * 當輸入元件按下Enter時,將焦點移至下一個符合的文字.
094    */
095   public void actionPerformed(ActionEvent e) {
096     nextSearch();
097   }
098  
099   /**
100    * 當文字改變時,重新搜尋本文內符合的文字.
101    */
102   public void changedUpdate(DocumentEvent e) {
103     startNewSearch(e.getDocument());
104   }
105  
106   /**
107    * 當文字改變時,重新搜尋本文內符合的文字.
108    */
109   public void insertUpdate(DocumentEvent e) {
110     startNewSearch(e.getDocument());
111   }
112  
113   /**
114    * 當文字改變時,重新搜尋本文內符合的文字.
115    */
116   public void removeUpdate(DocumentEvent e) {
117     startNewSearch(e.getDocument());
118   }
119  
120   /**
121    * 搜尋本文內符合的文字.
122    * @param query_doc 搜尋文件的Document.
123    */
124   private void startNewSearch(Document query_doc) {
125     // 停止舊有的搜尋緒.
126     stopSearchThread();
127     try {
128       // 取得比對的文字.
129       String query = query_doc.getText(0, query_doc.getLength());
130      
131       // 取得本文內的文件.
132       Document content = textComponent.getDocument();
133       String body = content.getText(0, content.getLength());
134      
135       // 將給定的正則表達式編譯到網要中.
136       Pattern pattern;
137       pattern = Pattern.compile(query);
138       matcher = pattern.matcher(body);
139 
140      
141       // 創建比對給定輸入與此網要的比對器.
142       if (matcher != null) {
143         thread.start();
144         thread.join();
145       }
146     } catch (PatternSyntaxException e) {
147       // TODO: handle exception
148     } catch (InterruptedException e) {
149       // TODO: handle exception
150     } catch (BadLocationException e) {
151       // TODO: handle exception
152     } catch (Exception e) {
153       System.out.println("exception : " + e);
154       e.printStackTrace();
155     }
156   }
157  
158   /**
159    * 將搜尋符合的文字以高亮度表示.
160    */
161   private void highlighterSearchWord() {
162     matcherCount = 0;
163     if (textComponent instanceof JTextPane) {
164       JTextPane textPane = ((JTextPane) textComponent);
165       // 清除上一次搜尋的結果.
166       // 1.選取全部的文字.
167       setSelectionText(0, textComponent.getText().length(), false);
168       // 2.設定文字背景顏色為textComponent的初始值.
169       setAttributeSetBackground(textPane, textComponent.getBackground(), false);
170       // 執行搜尋
171       while (matcher.find()) {
172         // 將符合的文字以高亮度顯示.
173         // 1.選取符合的文字.
174         setSelectionText(matcher.start(), matcher.end(), false);
175         // 2.設定文字背景.
176         setAttributeSetBackground(textPane, selectBackgroundColor, false);
177         matcherCount++;
178       }
179      
180       // 將游標重新指定到第一個符合的文字.
181       if (matcher.find(0)) {
182         setSelectionText(matcher.start(), matcher.end(), true);
183       }
184     } else {
185       // 當textComponent不為JTextPane時,只需指定第一個符合的文字.
186       nextSearch();
187     }
188   }
189  
190   /**
191    * 尋找下一個符合的文字.
192    */
193   private void nextSearch() {
194     if (matcher != null) {
195       if (matcher.find()) {
196         setSelectionText(matcher.start(), matcher.end(), true);
197       }
198     }
199   }
200  
201   /**
202    * 設定被選取文字的背景顏色.
203    * @param component 本文元件.
204    * @param color 背景顏色.
205    * @param replace 是否清除原本文字的背景顏色.
206    */
207   private void setAttributeSetBackground(JTextPane component, Color color, boolean replace) {
208     SimpleAttributeSet attributeSet = new SimpleAttributeSet();
209     StyleConstants.setBackground(attributeSet, color);
210     component.setCharacterAttributes(attributeSet, replace);
211   }
212  
213   /**
214    * 設定選取文字的起迄.
215    * @param start 起.
216    * @param end 迄.
217    * @param isVisible 是否顯示選取.
218    */
219   private void setSelectionText(int start, int end, boolean isVisible) {
220     textComponent.getCaret().setDot(start);
221     textComponent.getCaret().moveDot(end);
222     textComponent.getCaret().setSelectionVisible(isVisible);
223   }
224  
225   /**
226    * 取得選取文字高亮度的顏色.
227    * @return 選取文字高亮度的顏色.
228    */
229   public Color getSelectBackgroundColor() {
230     return selectBackgroundColor;
231   }
232  
233   /**
234    * 設定選取文字高亮度的顏色.
235    * @param selectBackgroundColor 選取文字高亮度的顏色.
236    */
237   public void setSelectBackgroundColor(Color selectBackgroundColor) {
238     this.selectBackgroundColor = selectBackgroundColor;
239   }
240 
241   /**
242    * 中斷目前的執行緒,並加入新的執行緒.
243    */
244   private void stopSearchThread() {
245     thread.interrupt();
246     thread = new Thread(searchRunnable);
247   }
248 
249   /**
250    * 取得符合文字的總數.
251    * @return 符合文字的總數.
252    */
253   public int getMatcherCount() {
254     return matcherCount;
255   }
256  
257   /**
258    * 測試.
259    * @param args
260    */
261   public static void main(String[] args) {
262     JTextPane area = new JTextPane();
263     JScrollPane pane = new JScrollPane(area);
264     JTextComponentWithSearchText searchText = new JTextComponentWithSearchText(area, Color.YELLOW);
265    
266     JTextField field = new JTextField();
267     field.getDocument().addDocumentListener(searchText);
268     field.addActionListener(searchText);
269    
270     JFrame frame = new JFrame();
271     frame.getContentPane().add(BorderLayout.NORTH, field);
272     frame.getContentPane().add(BorderLayout.CENTER, pane);
273     frame.setPreferredSize(new Dimension(800,600));
274     frame.pack();
275     frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
276     frame.setVisible(true);
277   }
278 }