java - 使用 jtextfield 和 jpopupmenu 实现自动完成

标签 java swing jpanel jtextfield jpopupmenu

我想实现自动完成功能。目前我有一个包含 JTextField 的 JPanel,当用户开始键入时,会出现一个自动完成(JPopupMenu),其中包含多个选项。

问题是它从文本字段中获取焦点,并且用户不再可以键入。当我将焦点返回到文本字段时,用户不再可以在选项之间导航(使用向上和向下按钮)。 另外,将焦点放在菜单上不允许我拦截其 KeyListener(不知道为什么),当我尝试处理文本字段侧的输入时,我在尝试选择菜单项时遇到问题。

所以我想要:

  1. 带有选项的弹出菜单,当用户更改文本字段中的文本时,该菜单会动态更改,但菜单仍然处于 Activity 状态
  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);
    }
}

最佳答案

我个人建议使用弹出窗口或自定义的 JWindow 而不是 JPopupMenu,因为后者最初仅用于显示菜单项。它确实适用于其他事情,但以不同方式使用它并不是最佳实践。

例如,您的示例中有一些菜单项作为自动完成选项 - 如果只有几个结果,则效果很好。但如果有 10 个呢?如果50个呢?还是500?您将不得不以某种方式为这些情况创建额外的解决方法 - 要么将项目放入滚动 Pane (天哪,这看起来很难看),要么将结果减少到一些最佳结果(这也不是最好的选择)。

因此,我使用 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 本身不处于 Activity 状态(未聚焦),并且无法获得焦点,因为它被强制配置为如此。这使我们能够将焦点保持在 JTextField 内并继续输入。

在此示例中,我们还捕获字段中的向上/向下箭头等关键事件,以在自动完成结果中导航。 ENTER和ESCAPE用于接受/取消结果选择。

此代码也可以稍微重写以使用 Swing PopupFactory 作为自动完成弹出窗口的源,但本质上仍然是相同的,因为使用了 HeavyWeightWindow by PopupFactory 只是扩展了 JWindow 并添加了一些设置。

关于java - 使用 jtextfield 和 jpopupmenu 实现自动完成,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/45439231/

相关文章:

java - 在线程的构造函数中调用 Looper.prepare() 会导致 RunTimeException

java - 从 JOptionPane 创建对话框并处理 OK_CANCEL_OPTION

java - 如何通过单击从数组中绘制?

java - 我应该取消中断的线程吗?

java - 这里如何同时遵守 "composition over inheritance"和DRY原则呢?

java - 如何创建一个内部包含多个图像的矩形?

java - 如何保持 Swing 组件更新

java - 如何从其 JPanel 内容更改 JDialog 图标?

java - 为什么 java.util.Map.get(...) 不是通用的?

java - 从 JFrame 调用时,ActionListener 在 JPanel 上执行两次