创建一个带有多个复选框的组合框。

3
我已经阅读了文档和教程,并在这里进行了搜索,但没有找到相关信息。 Oracle 教程:如何为 ComboBox 使用自定义渲染器 另一个类似的问题,回答有些模糊 我认为这很重要,因为许多人都问过这个问题,但是没有人能够提供一个简单、可行的示例。所以我必须自己问: 我们如何制作一个带有下拉菜单的组合框,允许我们选择多个选项? 以下方法不起作用:
  • JList 在这里无用,因为我无法让它出现在下拉菜单中。
  • 在 Swing 中没有 CheckBoxList
我已经使用复选框在组合框的下拉菜单中进行了 SCCEE,但是复选框拒绝被选中,方框中的勾选标记消失了。
我们如何实现这一点?
import java.awt.Component;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.util.List;

import javax.swing.DefaultCellEditor;
import javax.swing.DefaultListModel;
import javax.swing.DefaultListSelectionModel;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JTable;
import javax.swing.ListCellRenderer;
import javax.swing.ListSelectionModel;
import javax.swing.SwingUtilities;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.table.TableColumn;

public class ComboOfCheckBox extends JFrame {

public ComboOfCheckBox() {
    begin();
}

private void begin() {
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    JPanel panel = new JPanel();

    JTable table = new JTable(new Object[2][2], new String[]{"COL1", "COL2"});
    final JCheckBox chx1 = new JCheckBox("Oh");
    final JCheckBox chx2 = new JCheckBox("My");
    final JCheckBox chx3 = new JCheckBox("God");
    String[] values = new String[] {"Oh", "My", "God"};
    JCheckBox[] array = new JCheckBox[] {chx1, chx2, chx3};
    final JComboBox<JCheckBox> comboBox = new JComboBox<JCheckBox>(array) {
        @Override
        public void setPopupVisible(boolean visible){
            if (visible) {
                super.setPopupVisible(visible);
            }
        }
    };

    class CheckBoxRenderer  implements ListCellRenderer {

        private boolean[] selected;
        private String[] items;

        public CheckBoxRenderer(String[] items) {
            this.items = items;
            this.selected = new boolean[items.length];
        }

        @Override
        public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected,
                boolean cellHasFocus) {
            JLabel label = null;
            JCheckBox box = null;
            if (value instanceof JCheckBox) {
                label = new JLabel(((JCheckBox)value).getText());
                box = new JCheckBox(label.getText());
            }
            return box;
        }
        public void setSelected(int i, boolean selected) {
            this.selected[i] = selected;
        }

    }

    comboBox.setRenderer(new CheckBoxRenderer(values));

    panel.add(comboBox);    
    panel.add(new JCheckBox("Another"));
    getContentPane().add(panel);
    pack();
    setVisible(true);
}

public static void main(String[] args) {
    SwingUtilities.invokeLater(new Runnable() {

        @Override
        public void run() {
            ComboOfCheckBox frame = new ComboOfCheckBox();

        }   
    });
}
}

1
@FaithReaper - 我们如何制作一个带有下拉菜单的组合框,允许我们选择多个选项? == 1. 弹出窗口在某些操作、选择(鼠标/键盘事件)后是否会消失并保持可见,2. 然后问题是你如何/你想要隐藏弹出窗口(以避免混淆用户)。 - mKorbel
2
@FaithReaper - 这里是达到你目标的一半,你可以使用JWindow,并通过检查鼠标事件(来自SwingUtilities)来实现特殊弹出窗口(不会在第一次鼠标/键盘事件后隐藏)。 - mKorbel
@mKorbel 我和Andrew在谈论这个问题...他坚持认为这是不可能的。至于你的担忧,我已经找到了解决办法,使用以下代码片段:@Override public void setPopupVisible(boolean visible){ if (visible) { super.setPopupVisible(visible); } }。关键在于:只监听visible==true的情况。当弹出菜单通知visible==false时,忽略它。而当我点击其他地方时,弹出菜单会隐藏起来。运行我的SCCEE,你就能看到效果。 - WesternGun
以下是一个可能的方法:a. 创建一个支持多选的Swing组件 b. 尝试将其用作渲染器。 - c0der
... 没有人能提供一个简单、可行的例子,因为试图把方形钉子塞进圆孔不是一个好主意(原因太多,无法在评论中一一列举)。组合框用于选择单个项目。仅仅因为组合框显示一个弹出窗口,并不意味着它应该用于选择弹出窗口中的多个项目。有更好的Swing组件可供使用,例如JPopupMenu。它允许在弹出窗口中显示JCheckBoxMenuItems。请参考Table Column Adjuster以了解这种方法的示例。 - camickr
3个回答

2

这里有一个部分答案。它没有解决ComboBox在弹出窗口上屏蔽事件的问题,但可以解决它。问题仍然是ComboBox将每个对一个项目的选择视为对另一个项目的取消选择。但是,你面临的一个问题是,由于渲染器每次重绘时都会被调用,你的复选框不是持久的 - Map 解决了这个问题。

public class ComboOfCheckBox extends JFrame {

public ComboOfCheckBox() {
    begin();
}

private void begin() {
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    JPanel panel = new JPanel();

    JTable table = new JTable(new Object[2][2], new String[]{"COL1", "COL2"});
    String[] values = new String[] {"Oh", "My", "God"};
    final JComboBox<String> comboBox = new JComboBox<String>(values) {
        @Override
        public void setPopupVisible(boolean visible){
            if (visible) {
                super.setPopupVisible(visible);
            }
        }
    };

    class CheckBoxRenderer  implements ListCellRenderer<Object> {
        private Map<String, JCheckBox> items = new HashMap<>();
        public CheckBoxRenderer(String[] items) {
            for (String item : items) {
                JCheckBox box = new JCheckBox(item);
                this.items.put(item, box);
            }

        }
        @Override
        public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected,
                                                      boolean cellHasFocus) {
            if (items.containsKey(value)) {
                return items.get(value);
            } else {
                return null;
            }
        }

        public void setSelected(String item, boolean selected) {
            if (item.contains(item)) {
                JCheckBox cb = items.get(item);
                cb.setSelected(selected);
            }
        }
    }

    final CheckBoxRenderer renderer = new CheckBoxRenderer(values);

    comboBox.setRenderer(renderer);
    comboBox.addItemListener(e -> {
        String item = (String) e.getItem();
        if (e.getStateChange() == ItemEvent.DESELECTED) {
            renderer.setSelected(item, false);
        } else {
            renderer.setSelected(item, true);
        }
    });

    panel.add(comboBox);

    panel.add(new JCheckBox("Another"));
    getContentPane().add(panel);
    pack();
    setVisible(true);
}
public static void main(String[] args) {
    SwingUtilities.invokeLater(new Runnable() {

        @Override
        public void run() {
            ComboOfCheckBox frame = new ComboOfCheckBox();

        }

    });
}

}


所以知道什么?假设你这样做的原因是希望能够查询每个复选框的状态以进行其他类型的处理。那么你打算如何做到这一点呢?你要在渲染器中添加一个方法来获取每个复选框的状态吗?这变得越来越疯狂,甚至不应该尝试。这就是为什么像JTable和JList这样的组件有一个"SelectionModel"。JComboBox不是完成这项工作的组件,故事结束。 - camickr
我也希望在Swing中有CheckBoxList,但实际上并没有。也许@Piotr的方法是一个不错的起点。每次构造渲染器时创建复选框可以通过将Map提取出内部类来避免,但这样做并不太好看... - WesternGun
1
@camickr:说真的,我不明白为什么你这么热衷于阻止人们追求解决方案。原作者要求一个具有多选功能的ComboBox。我们知道Swing本身不支持它。我们知道有多个组件可以实现。但是它们都不适合这个任务。可能的解决方案要么是:做一个由JList和其他东西组成的复合组件来充当带箭头的标签框,要么是扩展JComboBox。两者都可以说是“hackish”。 - Piotr Wilkin
@camickr 我必须同意Piotr的观点... 如果当前的Swing组件不符合OP的要求,或者他不想使用它们,那么对于这个问题唯一可能的解决方案就是创建一个新的组件,或者像Piotr所说的,从现有的组件中创建一个复合组件。我也觉得我们提出的这些“技巧”比创建一个全新的组件或者使用一个现有的但不同的组件更好,因为它们修复了问题并直接回答了这个问题。 - nyxaria
1
做一个复合组件,由JList和其他东西组成,作为带有箭头的标签框或扩展JComboBox - 没错!这就是组合框的作用。它将JTextField、JButton、JPopup、JScrollPane和JList组合成一个可工作的组件。如果您需要不同的功能,那么您完全可以创建自己的自定义复合组件,设计用于执行特定功能。这一点并不糟糕,这正是Swing开发人员已经做过的事情。就像JScrollPane或JSplitPane一样。当您需要更复杂的功能时,您需要一个更复杂的组件。 - camickr
显示剩余2条评论

0

你忘记了与你的comboBox关联的action listener。另一方面,每当选择其他项目时,都会调用CheckBoxRenderer,因此如果你将一个JCheckBox对象作为JComboBox的项目,你必须从外部改变它的状态(选中或未选中),也就是从你的comboBox的action listener方法中调用的方法。但是你可以使用CheckBoxRenderer的自动调用,这里我写了一个简单的代码来向你展示如何做到:

public class ComboOfChechBox extends JFrame {

    public ComboOfChechBox() {
        begin();
    }

    //a custom item for comboBox
    public class CustomerItem {

        public String label;
        public boolean status;

        public CustomerItem(String label, boolean status) {
            this.label = label;
            this.status = status;
        }
    }

    //the class that implements ListCellRenderer
    public class RenderCheckComboBox implements ListCellRenderer {

        //a JCheckBox is associated for one item
        JCheckBox checkBox;

        Color selectedBG = new Color(112, 146, 190);

        public RenderCheckComboBox() {
            this.checkBox = new JCheckBox();
        }

        @Override
        public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected,
                boolean cellHasFocus) {

            //recuperate the item value
            CustomerItem value_ = (CustomerItem) value;

            if (value_ != null) {
                //put the label of item as a label for the associated JCheckBox object
                checkBox.setText(value_.label);

                //put the status of item as a status for the associated JCheckBox object
                checkBox.setSelected(value_.status);
            }

            if (isSelected) {
                checkBox.setBackground(Color.GRAY);
            } else {
                checkBox.setBackground(Color.WHITE);
            }
            return checkBox;
        }

    }

    private void begin() {
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        JPanel panel = new JPanel();

        JComboBox<CustomerItem> combo = new JComboBox<CustomerItem>() {
            @Override
            public void setPopupVisible(boolean visible) {
                if (visible) {
                    super.setPopupVisible(visible);
                }
            }
        };

        CustomerItem[] items = new CustomerItem[3];
        items[0] = new CustomerItem("oh", false);
        items[1] = new CustomerItem("My", false);
        items[2] = new CustomerItem("God", false);
        combo.setModel(new DefaultComboBoxModel<CustomerItem>(items));
        combo.setRenderer(new RenderCheckComboBox());

        //the action listener that you forget
        combo.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent ae) {
                CustomerItem item = (CustomerItem) ((JComboBox) ae.getSource()).getSelectedItem();
                item.status = !item.status;

                // update the ui of combo
                combo.updateUI();

                //keep the popMenu of the combo as visible
                combo.setPopupVisible(true);
            }
        });
        panel.add(combo);
        panel.add(new JCheckBox("Another"));
        getContentPane().add(panel);
        pack();
        setVisible(true);
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {

            @Override
            public void run() {
                new ComboOfChechBox();
            }
        });
    }
}

0
我也找到了一个解决办法,但是使用ActionListener。事实上,你不能直接在JCheckBox上添加监听器,因为渲染器每个周期都会创建一个新的JCheckBox,而Piotr Wilkin提供的解决办法解决了这个问题。你还可以使用以下解决方案,在点击JComboBox时检查鼠标的位置:
    comboBox.addActionListener(new ActionListener() {
        @Override
        public void actionPerformed(ActionEvent e) {
            JComboBox combo = (JComboBox) e.getSource();
            int y = MouseInfo.getPointerInfo().getLocation().y - combo.getLocationOnScreen().y;
            int item =  y / combo.getHeight();
            ((CheckBoxRenderer) combo.getRenderer()).selected[item] = !((CheckBoxRenderer) combo.getRenderer()).selected[item];
        }
    });

另外,在`getListCellRendererComponent`方法中,你需要检查`index >= 0`,因为当渲染器第一次创建时,`selected`数组为空会导致错误出现。:)

这很好。查看鼠标位置确实是一种丑陋的技巧,但可能由于事件屏蔽而需要。 - Piotr Wilkin
@PiotrWilkin 我同意这是非常不正规的做法,从那个角度来看,我认为你的解决方案更简洁。但就个人而言,我喜欢找到问题的解决办法,而且越是不正规的方法,我越感到兴奋。 - nyxaria
编程中问题的解决方式真是多种多样,太神奇了:D - nyxaria
我的解决方案更简洁,但它并不是一个完整的解决方案 - 只是对其中一个问题的修复。而你的解决方案则是针对遮罩事件问题的完整解决方案 :) - Piotr Wilkin
请看我对Piotr的评论。 - camickr
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接