【问题标题】:Implementing autocomplete with jtextfield and jpopupmenu使用 jtextfield 和 jpopupmenu 实现自动完成
【发布时间】:2018-01-08 09:41:26
【问题描述】:

我想实现一个自动完成功能。目前我有一个包含 JTextField 的 JPanel,当用户开始输入时,会出现一个自动完成(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);
    }
}

【问题讨论】:

标签: java swing jpanel jtextfield jpopupmenu


【解决方案1】:

我个人建议使用弹出窗口或自定义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 只是扩展了 JWindow 和添加了一些设置。

【讨论】:

    【解决方案2】:

    最简单的解决方案是让菜单无法聚焦:

    menu.setFocusable(false);
    

    并在编辑器中处理键

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

    【讨论】:

      猜你喜欢
      • 2012-01-31
      • 2012-05-13
      • 2012-06-06
      • 2012-08-09
      • 2012-06-06
      • 1970-01-01
      • 2019-03-07
      • 1970-01-01
      • 2016-06-23
      相关资源
      最近更新 更多