单击边框时 JComboBox 弹出窗口立即出现和隐藏(糟糕的用户体验)

JComboBox popup appears and hide immediately when clicking on its border (Bad User Experience)

当您有一个 swing JComboBox 并单击其边框时,弹出窗口会立即出现并消失。当我说单击时,我的意思是按下鼠标左键并立即松开。

这可能被认为是糟糕的用户体验,因为没有用户会期望它发生。单击组合框的边框时,任何用户都会期望以下行为之一:

  1. 要打开并保持打开状态的弹出窗口,
  2. 或者根本打不开。

肯定没有用户会期望弹出窗口立即打开和关闭。

用户不是故意点击边框的。但是当组合框很小并且他试图快速点击它时,它可能会经常发生。

2000 年有人将此行为注册为 openjdk 站点中的错误:https://bugs.openjdk.java.net/browse/JDK-4346918

他们已将其识别为一个错误,但通过解决方案将其关闭:"Won't fix",观察结果如下:

I've been able to reproduce the problem but it's not significant so I'm not going to fix it. The problem is that the drop down portion of the combo box will hide when the mouse is released after clicking on the border. This bug doesn't have a very major impact.

我同意他们的看法,它并没有产生太大的影响。但我仍然认为这会导致糟糕的用户体验,我想知道是否有一种简单的解决方法可以使弹出窗口在用户单击其边框时保持打开状态或根本不打开。

可以通过在任何 JComboBox 的边框上单击鼠标左键来重现所描述的行为。请参阅下面可以复制的简单代码:

import java.awt.FlowLayout;
import javax.swing.*;

public class JComboBoxUX{
    public static void main(String[] args){
        SwingUtilities.invokeLater(new Runnable(){
            @Override
            public void run(){
                JComboBox<String> combobox = new JComboBox<String>(
                        new String[]{"aaaaaaaaaa","bbbbbbbb","ccccccccc"});

                JPanel panel = new JPanel(new FlowLayout(FlowLayout.CENTER, 10, 10));
                panel.add(combobox);

                JFrame frame = new JFrame("JComboBox UX");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.setContentPane(panel);
                frame.setSize(300, 150);
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }
}

问题似乎出在:

class BasicComboPopup extends ... {

    private Handler getHandler() {
        if (handler == null) {
            handler = new Handler();
        }
        return handler;
    }

    private class Handler implements ... MouseListener ... {

        public void mouseReleased(MouseEvent e) {
            //...
            Component source = (Component)e.getSource();
            Dimension size = source.getSize();
            Rectangle bounds = new Rectangle( 0, 0, size.width - 1, size.height - 1 );
            if ( !bounds.contains( e.getPoint() ) ) {
                //...
                comboBox.setPopupVisible(false);
            }
        }
    }
}

通过size.widthsize.height减一,鼠标落在箭头按钮的边界之外,弹出菜单被隐藏。

修复问题有问题。 Handler class 是 private,所以我们不能扩展它,getHandler()private,所以我们不能在 [=17] 中覆盖它=] 要么。

可以扩展 MetalComboBoxUI 并将 createPopup() 覆盖为 return 自定义 ComboPopup,例如扩展 BasicComboPopup 但扩展 createMouseListener() return 类似于上面 Handler 的 class,但没有减法。

哦,对您希望支持的每个 LAF 执行相同的操作。嗯。

从另一个方向解决问题,可以扩展 MetalComboBoxButton(由 e.getSource() 编辑 return)并将 getSize() 方法重写为 return 当显示菜单时,在两个方向上都增加一个像素。当然,您仍然需要扩展和覆盖 MetalComboBoxUI 来创建和安装这个自定义按钮。

同样,您需要为您希望支持的每个 LAF 执行相同的操作。再次,yuk。

不幸的是,Swing 似乎没有所需的钩子来轻松覆盖所需的功能,并且已将各种 classes 标记为私有内部实现细节,防止它们重用(以防止以后损坏如果他们想改变内部结构)。

A​​JNeufeld 的建议非常奏效。谢谢!

下面是代码,如果有人需要的话。

JComboBoxGoodBorder.java:

import java.awt.Component;
import java.awt.Dimension;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.Vector;
import javax.swing.ComboBoxModel;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.SwingUtilities;
import javax.swing.plaf.ComponentUI;
import javax.swing.plaf.basic.BasicComboPopup;
import javax.swing.plaf.basic.ComboPopup;
import javax.swing.plaf.metal.MetalComboBoxUI;

public class JComboBoxGoodBorder<T> extends JComboBox<T> {

    public JComboBoxGoodBorder(){
        super();
    }

    public JComboBoxGoodBorder(ComboBoxModel<T> aModel){
        super(aModel);
    }

    public JComboBoxGoodBorder(T[] items){
        super(items);
    }

    public JComboBoxGoodBorder(Vector<T> items){
        super(items);
    }

    @Override
    public void updateUI(){
        setUI(MetalComboBoxUIGoodBorder.createUI(this));
    }

    private static class MetalComboBoxUIGoodBorder extends MetalComboBoxUI {
        public static ComponentUI createUI(JComponent c) {
            return new MetalComboBoxUIGoodBorder();         
        }

        @Override
        protected ComboPopup createPopup() {
            return new BasicComboPopup(comboBox) {
                @Override
                protected MouseListener createMouseListener(){
                    return new MouseAdapter(){
                        @Override
                        public void mousePressed(MouseEvent e) {
                            if (e.getSource() == list) {
                                return;
                            }
                            if (!SwingUtilities.isLeftMouseButton(e) || !comboBox.isEnabled())
                                return;

                            if ( comboBox.isEditable() ) {
                                Component comp = comboBox.getEditor().getEditorComponent();
                                if ((!(comp instanceof JComponent)) || ((JComponent)comp).isRequestFocusEnabled()) {
                                    comp.requestFocus();
                                }
                            }
                            else if (comboBox.isRequestFocusEnabled()) {
                                comboBox.requestFocus();
                            }
                            togglePopup();
                        }

                        @Override
                        public void mouseReleased(MouseEvent e) {
                            if (e.getSource() == list) {
                                if (list.getModel().getSize() > 0) {
                                    // JList mouse listener
                                    if (comboBox.getSelectedIndex() != list.getSelectedIndex()) {
                                        comboBox.setSelectedIndex( list.getSelectedIndex() );
                                    } else {
                                        comboBox.getEditor().setItem( list.getSelectedValue() );
                                    }
                                }
                                comboBox.setPopupVisible(false);
                                // workaround for cancelling an edited item (bug 4530953)
                                if (comboBox.isEditable() && comboBox.getEditor() != null) {
                                    comboBox.configureEditor(comboBox.getEditor(),
                                            comboBox.getSelectedItem());
                                }
                                return;
                            }
                            // JComboBox mouse listener
                            Component source = (Component)e.getSource();
                            Dimension size = source.getSize();
                            Rectangle bounds = new Rectangle( 0, 0, size.width, size.height);
                            if ( !bounds.contains( e.getPoint() ) ) {
                                MouseEvent newEvent = convertMouseEvent( e );
                                Point location = newEvent.getPoint();
                                Rectangle r = new Rectangle();
                                list.computeVisibleRect( r );
                                if ( r.contains( location ) ) {
                                    if (comboBox.getSelectedIndex() != list.getSelectedIndex()) {
                                        comboBox.setSelectedIndex( list.getSelectedIndex() );
                                    } else {
                                        comboBox.getEditor().setItem( list.getSelectedValue() );
                                    }
                                }
                                comboBox.setPopupVisible(false);
                            }
                            hasEntered = false;
                            stopAutoScrolling();
                        }
                    };
                }
            };         
        }
    }
}

Test.java:

import java.awt.FlowLayout;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;

public class Test{
    public static void main(String[] args){
        SwingUtilities.invokeLater(new Runnable(){
            @Override
            public void run(){
                JComboBoxGoodBorder<String> combobox = new JComboBoxGoodBorder<String>(
                        new String[]{"aaaaaaaaaa","bbbbbbbb","ccccccccc"});

                JPanel panel = new JPanel(new FlowLayout(FlowLayout.CENTER, 10, 10));
                panel.add(combobox);

                JFrame frame = new JFrame("JComboBox Good Border");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.setContentPane(panel);
                frame.setSize(300, 300);
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }
}