2017-08-01 68 views
1

我想實現自動完成功能。目前我有一個包含JTextField的JPanel,當用戶開始輸入時,會出現一個包含多個選項的自動完成(JPopupMenu)。使用jtextfield和jpopupmenu實現自動完成

問題是,它需要從文本字段和用戶不再可以鍵入焦點。當我將焦點返回到文本字段時,用戶不再在選項之間導航(使用向上和向下按鈕)。 也關注菜單不允許我攔截它的KeyListener(不知道爲什麼),並且當我嘗試處理文本字段的輸入時,我嘗試選擇菜單項時遇到問題。

所以,我想有:

  1. 與選項的彈出式菜單,當用戶在文本框更改文本,仍然有菜單活躍,動態變化
  2. 用戶最多可以使用選項之間和向下箭頭鍵導航,以及Enter和Escape鍵分別使用選項或關閉彈出。

是否可以處理上的菜單和前打字事件迴文本字段鍵盤事件?

接近我的問題的正確方法是什麼?

以下是下面的代碼。提前致謝!

import javax.swing.*; 
import java.awt.*; 
import java.awt.event.KeyEvent; 
import java.awt.event.KeyListener; 


class TagVisual extends JPanel { 

    private JTextField editField; 

    public TagVisual() { 

     FlowLayout layout = new FlowLayout(); 
     layout.setHgap(0); 
     layout.setVgap(0); 
     setLayout(layout); 

     editField = new JTextField(); 
     editField.setBackground(Color.RED); 

     editField.setPreferredSize(new Dimension(200, 20)); 

     editField.addKeyListener(new KeyListener() { 
      @Override 
      public void keyTyped(KeyEvent e) { 
       JPopupMenu menu = new JPopupMenu(); 
       menu.add("Item 1"); 
       menu.add("Item 2"); 
       menu.add("Item 3"); 
       menu.addKeyListener(new KeyListener() { 
        @Override 
        public void keyTyped(KeyEvent e) { 
         JOptionPane.showMessageDialog(TagVisual.this, "keyTyped"); 
        } 

        @Override 
        public void keyPressed(KeyEvent e) { 
         JOptionPane.showMessageDialog(TagVisual.this, "keyPressed"); 
        } 

        @Override 
        public void keyReleased(KeyEvent e) { 
         JOptionPane.showMessageDialog(TagVisual.this, "keyReleased"); 
        } 
       }); 
       menu.show(editField, 0, getHeight()); 
      } 

      @Override 
      public void keyPressed(KeyEvent e) { 

      } 

      @Override 
      public void keyReleased(KeyEvent e) { 

      } 
     }); 

     add(editField, FlowLayout.LEFT); 
    } 

    public void place(JPanel panel) { 
     panel.add(this); 

     editField.grabFocus(); 
    } 
} 

public class MainWindow { 

    private JPanel mainPanel; 
    private JFrame frame; 

    public MainWindow(JFrame frame) { 

     mainPanel = new JPanel(new FlowLayout()); 
     TagVisual v = new TagVisual(); 
     v.place(mainPanel); 

     this.frame = frame; 
    } 

    public static void main(String[] args) { 
     JFrame frame = new JFrame("TextFieldPopupIssue"); 

     frame.setContentPane(new MainWindow(frame).mainPanel); 
     frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 
     frame.pack(); 
     frame.setVisible(true); 
    } 
} 
+0

或使用第三方庫。請參閱https://stackoverflow.com/q/14186955/1076463 – Robin

回答

0

最簡單的辦法是使菜單不成爲焦點的:在編輯器中

menu.setFocusable(false); 

和處理鍵

editField.addKeyListener(new KeyAdapter() { 
      @Override 
      public void keyPressed(KeyEvent e) { 
       if(KeyEvent.VK_DOWN == e.getKeyCode()) { 
        ... 
0

我個人建議使用彈出窗口或定製JWindow代替JPopupMenu作爲後者最初只用於顯示菜單項。它通常適用於其他方面,但不同的最佳做法是使用它。

例如,您的示例中有幾個菜單項作爲自動完成選項 - 如果只有幾個結果,則工作得很好。但是如果有10個呢?如果50?還是500?您必須以某種方式爲這些情況創建額外的解決方法 - 將項目放入滾動窗格(哦,上帝,看起來很醜)或將結果降至最佳(這也不是最好的選項)。

所以我做了一個小例子,使用JWindow作爲AutocompleteField的彈出窗口。這是很簡單,但做了你會期望從它一些基本的東西,你也已經提到的那些:

import javax.swing.*; 
import javax.swing.border.EmptyBorder; 
import javax.swing.event.DocumentEvent; 
import javax.swing.event.DocumentListener; 
import java.awt.*; 
import java.awt.event.FocusEvent; 
import java.awt.event.FocusListener; 
import java.awt.event.KeyEvent; 
import java.awt.event.KeyListener; 
import java.util.ArrayList; 
import java.util.Arrays; 
import java.util.List; 
import java.util.function.Function; 
import java.util.stream.Collectors; 

/** 
* @author Mikle Garin 
* @see https://stackoverflow.com/questions/45439231/implementing-autocomplete-with-jtextfield-and-jpopupmenu 
*/ 

public final class AutocompleteField extends JTextField implements FocusListener, DocumentListener, KeyListener 
{ 
    /** 
    * {@link Function} for text lookup. 
    * It simply returns {@link List} of {@link String} for the text we are looking results for. 
    */ 
    private final Function<String, List<String>> lookup; 

    /** 
    * {@link List} of lookup results. 
    * It is cached to optimize performance for more complex lookups. 
    */ 
    private final List<String> results; 

    /** 
    * {@link JWindow} used to display offered options. 
    */ 
    private final JWindow popup; 

    /** 
    * Lookup results {@link JList}. 
    */ 
    private final JList list; 

    /** 
    * {@link #list} model. 
    */ 
    private final ListModel model; 

    /** 
    * Constructs {@link AutocompleteField}. 
    * 
    * @param lookup {@link Function} for text lookup 
    */ 
    public AutocompleteField (final Function<String, List<String>> lookup) 
    { 
     super(); 
     this.lookup = lookup; 
     this.results = new ArrayList<>(); 

     final Window parent = SwingUtilities.getWindowAncestor (this); 
     popup = new JWindow (parent); 
     popup.setType (Window.Type.POPUP); 
     popup.setFocusableWindowState (false); 
     popup.setAlwaysOnTop (true); 

     model = new ListModel(); 
     list = new JList (model); 

     popup.add (new JScrollPane (list) 
     { 
      @Override 
      public Dimension getPreferredSize() 
      { 
       final Dimension ps = super.getPreferredSize(); 
       ps.width = AutocompleteField.this.getWidth(); 
       return ps; 
      } 
     }); 

     addFocusListener (this); 
     getDocument().addDocumentListener (this); 
     addKeyListener (this); 
    } 

    /** 
    * Displays autocomplete popup at the correct location. 
    */ 
    private void showAutocompletePopup() 
    { 
     final Point los = AutocompleteField.this.getLocationOnScreen(); 
     popup.setLocation (los.x, los.y + getHeight()); 
     popup.setVisible (true); 
    } 

    /** 
    * Closes autocomplete popup. 
    */ 
    private void hideAutocompletePopup() 
    { 
     popup.setVisible (false); 
    } 

    @Override 
    public void focusGained (final FocusEvent e) 
    { 
     SwingUtilities.invokeLater (() -> { 
      if (results.size() > 0) 
      { 
       showAutocompletePopup(); 
      } 
     }); 
    } 

    private void documentChanged() 
    { 
     SwingUtilities.invokeLater (() -> { 
      // Updating results list 
      results.clear(); 
      results.addAll (lookup.apply (getText())); 

      // Updating list view 
      model.updateView(); 
      list.setVisibleRowCount (Math.min (results.size(), 10)); 

      // Selecting first result 
      if (results.size() > 0) 
      { 
       list.setSelectedIndex (0); 
      } 

      // Ensure autocomplete popup has correct size 
      popup.pack(); 

      // Display or hide popup depending on the results 
      if (results.size() > 0) 
      { 
       showAutocompletePopup(); 
      } 
      else 
      { 
       hideAutocompletePopup(); 
      } 
     }); 
    } 

    @Override 
    public void focusLost (final FocusEvent e) 
    { 
     SwingUtilities.invokeLater (this::hideAutocompletePopup); 
    } 

    @Override 
    public void keyPressed (final KeyEvent e) 
    { 
     if (e.getKeyCode() == KeyEvent.VK_UP) 
     { 
      final int index = list.getSelectedIndex(); 
      if (index != -1 && index > 0) 
      { 
       list.setSelectedIndex (index - 1); 
      } 
     } 
     else if (e.getKeyCode() == KeyEvent.VK_DOWN) 
     { 
      final int index = list.getSelectedIndex(); 
      if (index != -1 && list.getModel().getSize() > index + 1) 
      { 
       list.setSelectedIndex (index + 1); 
      } 
     } 
     else if (e.getKeyCode() == KeyEvent.VK_ENTER) 
     { 
      final String text = (String) list.getSelectedValue(); 
      setText (text); 
      setCaretPosition (text.length()); 
     } 
     else if (e.getKeyCode() == KeyEvent.VK_ESCAPE) 
     { 
      hideAutocompletePopup(); 
     } 
    } 

    @Override 
    public void insertUpdate (final DocumentEvent e) 
    { 
     documentChanged(); 
    } 

    @Override 
    public void removeUpdate (final DocumentEvent e) 
    { 
     documentChanged(); 
    } 

    @Override 
    public void changedUpdate (final DocumentEvent e) 
    { 
     documentChanged(); 
    } 

    @Override 
    public void keyTyped (final KeyEvent e) 
    { 
     // Do nothing 
    } 

    @Override 
    public void keyReleased (final KeyEvent e) 
    { 
     // Do nothing 
    } 

    /** 
    * Custom list model providing data and bridging view update call. 
    */ 
    private class ListModel extends AbstractListModel 
    { 
     @Override 
     public int getSize() 
     { 
      return results.size(); 
     } 

     @Override 
     public Object getElementAt (final int index) 
     { 
      return results.get (index); 
     } 

     /** 
     * Properly updates list view. 
     */ 
     public void updateView() 
     { 
      super.fireContentsChanged (AutocompleteField.this, 0, getSize()); 
     } 
    } 

    /** 
    * Sample {@link AutocompleteField} usage. 
    * 
    * @param args run arguments 
    */ 
    public static void main (final String[] args) 
    { 
     final JFrame frame = new JFrame ("Sample autocomplete field"); 

     // Sample data list 
     final List<String> values = Arrays.asList ("Frame", "Dialog", "Label", "Tree", "Table", "List", "Field"); 

     // Simple lookup based on our data list 
     final Function<String, List<String>> lookup = text -> values.stream() 
       .filter (v -> !text.isEmpty() && v.toLowerCase().contains (text.toLowerCase()) && !v.equals (text)) 
       .collect (Collectors.toList()); 

     // Autocomplete field itself 
     final AutocompleteField field = new AutocompleteField (lookup); 
     field.setColumns (15); 

     final JPanel border = new JPanel (new BorderLayout()); 
     border.setBorder (new EmptyBorder (50, 50, 50, 50)); 
     border.add (field); 
     frame.add (border); 

     frame.setDefaultCloseOperation (WindowConstants.EXIT_ON_CLOSE); 
     frame.pack(); 
     frame.setLocationRelativeTo (null); 
     frame.setVisible (true); 
    } 
} 

所以在這個例子彈出JWindow本身是不活動(不集中),不能獲得焦點,其強制配置如此。這使我們能夠在JTextField內保持專注並繼續打字。

在此示例中,我們還捕獲字段中的向上/向下箭頭等關鍵事件以導航自動完成結果。 ENTER和ESCAPE用於接受/取消結果選擇。

此代碼也可以稍微改寫爲使用Swing PopupFactory作爲自動完成彈出窗口的來源,但它仍然是由PopupFactory使用,因爲HeavyWeightWindow的essense一樣簡單延伸JWindow,並增加了一些設置。