首页 文章

使用jtextfield和jpopupmenu实现自动完成

提问于
浏览
1

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

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

所以我想拥有:

  • 一个弹出菜单,其中包含当用户更改文本字段中的文本时仍会激活菜单的动态更改的选项

  • 用户可以使用向上和向下箭头键在选项之间导航,也可以使用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);
    }
}

2 回答

  • 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 作为自动完成弹出窗口的源,但它在essense中仍然是相同的,因为 PopupFactory 使用的 HeavyWeightWindow 只是扩展 JWindow 并添加了一些设置 .

  • 2

    最简单的解决方案是使菜单不可聚焦:

    menu.setFocusable(false);
    

    并在编辑器中处理键

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

相关问题