3

I have read the doc and tutorial, and searchd here, to no avail.

Oracle tutorial: how to use custom render for ComboBox

Another question similar with a somehow vague answer

And I see it important because many people asked about it but no one can provide a simple, workable example. So I must ask it myself:

How can we make a combobox with a drop-down menu, allowing us to choose more than one options?

What is not working:

  • JList proved to be useless here, because I cannot make it appear in the drop-down menu.
  • There's no CheckBoxList in Swing.

I have done a SCCEE with checkbox in drop-down menu of a combo, but the checkboxes refuse to be selected, the check in the box is missing.

How can we achieve that?

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();

        }   
    });
}
}
Community
  • 1
  • 1
WesternGun
  • 11,303
  • 6
  • 88
  • 157
  • 1
    @FaithReaper - `How can we make a combobox with a drop-down menu, allowing us to choose more than one options?` == 1. popup doesn't goes away or not and stays visible after some action, selection (mouse/key event), 2. then there is a question how you can/you want to hide the popup (to avoids to confuse an user) – mKorbel Aug 25 '16 at 11:45
  • 2
    @FaithReaper - [here is half way to your goal](http://stackoverflow.com/a/18118939/714968), you can to use JWindow and by checking mouseEvents (from SwingUtilities) for special popup (thats doesn't hide on 1st. mouse/keyEvent) – mKorbel Aug 25 '16 at 11:51
  • @mKorbel I was talking to Andrew... he insists on that this is impossible. As for your concerns, I have found how to solve it, with these lines:`@Override public void setPopupVisible(boolean visible){ if (visible) { super.setPopupVisible(visible); } }`. The trick is: only listens for `visible==true`. When the popup menus is notified `visible==false`, ignores it. And when I click somewhere else, the popup menu hides. Run my SCCEE and you sees it. – WesternGun Aug 25 '16 at 11:52
  • How about the following approach: a. make a swing component which supports multiple choice b. try to use it as a renderer ? – c0der Aug 25 '16 at 11:56
  • `... no one can provide a simple, workable example.` - because it is not a good idea (for too many reasons to list in a comment) to try and fit a square peg in a round hole. A combo box is used to select a single item. Just because a combo box displays a popup does not mean it should be used to select multiple items in the popup. There are better Swing components to use, for example a `JPopupMenu`. It allows you to display `JCheckBoxMenuItems` in the popup. See [Table Column Adjuster](https://tips4java.wordpress.com/2011/05/08/table-column-manager/) for an example of this approach. – camickr Aug 25 '16 at 19:48

3 Answers3

2

Here's a partial answer to go with. It doesn't address the issue of the ComboBox masking events on the popup, but it does work around it. The problem is still that the ComboBox treats each select on one item as a deselect on another. However, one problem you were facing is that, since the renderer is called every time upon repaint, your CheckBoxes weren't persistent - the Map addresses that.

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();

        }

    });
}

}

Piotr Wilkin
  • 3,446
  • 10
  • 18
  • (1-) So know what? Presumably the reason for doing this is that you want to be able to query the state of each check box for some other kind of processing. So how are you going to do this? Are you going to add a method to the renderer to retrieve the state of each check box? This is getting crazier and crazier and should not even be attempted. This is why components like JTable and JList have a "SelectionModel". A JComboBox is NOT the component for the job, end of story. – camickr Aug 25 '16 at 21:39
  • I also expect that we have `CheckBoxList` in Swing, but there's not.Maybe the approach of @Piotr is a good start. Creating the checkboxes everytime when a renderer is constructed may be avoided by extracting the Map out of the inner class but it's not so nice... – WesternGun Aug 26 '16 at 07:20
  • I have had a close look at the JComboBox's source, and found that by design selecting 1+ items is not possible, because in `JComboBox.class` there's a `protected void selectedItemChanged()` method which fires an `ItemEvent` to notify the deselected state change, and in it's JavaDoc, it is stated that **This protected method is implementation specific. Do not access directly or override.**. The original idea of consume this Event is not possible now. But good try here. I may adopt the idea of adding a `JPopupMenu` with a button or shows upon click... – WesternGun Aug 26 '16 at 07:46
  • 1
    @camickr: seriously, I do not understand why you are so keen on dissuading people from chasing a solution. The original author asked for a ComboBox with multiselect capabilities. We know that natively Swing doesn't support it. We know that there are multiple components that do. None of them are suitable for the job. The possible solutions are either: doing a compound component consisting of a JList and something else to act as the label box with the arrow or extending JComboBox. Both of them are, in a sense, hackish. – Piotr Wilkin Aug 26 '16 at 12:28
  • @camickr I have to agree with Piotr... If the current components in `Swing` do not meet OP's requirements, or by his choice he *does not want to use them*, then the only possible solution to the question is to make a new component or, as Piotr said, make a compound one from existing components. I also feel like these 'hacks' that we proposed are a better solution than making a whole new component OR using an existing, but different component as they mend the problems and answer the question directly. – nyxaria Aug 26 '16 at 13:39
  • 1
    `doing a compound component consisting of a JList and something else to act as the label box with the arrow or extending JComboBox` - Exactly!!! This is what a combo box does. It combines a JTextField, JButton, JPopup, JScrollPane, JList into a workable component. If you need different functionality then there is no reason you can't create your own custom compound component designed to do a specific function. It is not hackis at all, it is what Swing developers have already done. Same as a JScrollPane or JSplitPane. When you want more complex functionality you need a more complex component. – camickr Aug 26 '16 at 14:32
  • The OP doesn't understand how Swing works (look at his previous questions). The OP has stated there are no answers in the forum. That is because we don't encourage misuse of components like this. `...Both of them are, in a sense, hackish.` - they are far less hackish than this approach. Any custom component you create will have a better API and its functionality will be documented properly because it was designed for a specific function. A JComboBox is just not designed for this and should not be used! – camickr Aug 26 '16 at 14:32
0

I too found a work around, but using ActionListener. The fact is that you don't can't make a direct listener on JCheckBox since the render makes a new one every cycle, and the work-around by Piotr Wilkin adresses that issue. You could also use this solution which checks the position of the mouse when the JComboBox is clicked:

    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];
        }
    });

also, in the getListCellRendererComponent method, you need to check that index >= 0 because when the renderer first gets created it throws an error as the selected array is null. :)

nyxaria
  • 479
  • 1
  • 3
  • 13
  • This is nice. Looking at the mouse position is indeed an ugly hack, but probably needed due to the event masking. – Piotr Wilkin Aug 25 '16 at 12:52
  • @PiotrWilkin I agree it is very hackish, and in that sense I think your solution is cleaner.. but personally I love finding solutions to problems and the more hackish it is the more excited I get – nyxaria Aug 25 '16 at 13:01
  • It's amazing how many different ways problems in programming can be solved :D – nyxaria Aug 25 '16 at 13:01
  • In some free time, I might take my solution, your solution and fuse it into some `JMultipleCheckboxComboBox` class. – Piotr Wilkin Aug 25 '16 at 13:04
  • My solution is cleaner, but my solution is not a solution - just a fix for one of the problems. Yours, on the other hand, is a solution for the masked events problem :) – Piotr Wilkin Aug 25 '16 at 13:04
  • I'd love to see that class, give me a shout when you are done with it ;) – nyxaria Aug 25 '16 at 13:12
  • (1-) see my comment to Piotr. – camickr Aug 25 '16 at 21:40
0

You forget the action listener associated to your comboBox. In other side, CheckBoxRenderer is called every time an other item is selected, so if you put a JCheckBox object as JComboBox item you have to change its status(checked or not) from outside, it means from the method called in action listener of your comboBox. But you can use the automatic calling of CheckBoxRenderer, here I made a simple code to show you how to do it:

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();
            }
        });
    }
}