2017-08-01 54 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,并增加了一些设置。