• 追加された行はこの色です。
  • 削除された行はこの色です。
---
category: swing
folder: CheckedComboBox
title: JComboBoxのアイテムとして表示したJCheckBoxを複数選択する
tags: [JComboBox, JCheckBox]
author: aterai
pubdate: 2016-05-23T00:06:37+09:00
description: JComboBoxのアイテムとしてJCheckBoxを表示し、ドロップダウンリストを開いたままこれを複数選択可能に設定します。
image: https://lh3.googleusercontent.com/-I-fHfvCX-IU/V0G4uliNHdI/AAAAAAAAOX0/-746I_MG_jQkqTu1cniGzJqqu3xbc1khACCo/s800/CheckedComboBox.png
hreflang:
    href: http://java-swing-tips.blogspot.com/2016/07/select-multiple-jcheckbox-in-jcombobox.html
    href: https://java-swing-tips.blogspot.com/2016/07/select-multiple-jcheckbox-in-jcombobox.html
    lang: en
---
* 概要 [#summary]
`JComboBox`のアイテムとして`JCheckBox`を表示し、ドロップダウンリストを開いたままこれを複数選択可能に設定します。

#download(https://lh3.googleusercontent.com/-I-fHfvCX-IU/V0G4uliNHdI/AAAAAAAAOX0/-746I_MG_jQkqTu1cniGzJqqu3xbc1khACCo/s800/CheckedComboBox.png)

* サンプルコード [#sourcecode]
#code(link){{
class CheckedComboBox<E extends CheckableItem> extends JComboBox<E> {
  private boolean keepOpen;
  private transient ActionListener listener;

  protected CheckedComboBox() {
    super();
  }

  protected CheckedComboBox(ComboBoxModel<E> aModel) {
    super(aModel);
  }

  protected CheckedComboBox(E[] m) {
    super(m);
  }

  @Override public Dimension getPreferredSize() {
    return new Dimension(200, 20);
  }

  @Override public void updateUI() {
    setRenderer(null);
    removeActionListener(listener);
    super.updateUI();
    listener = e -> {
      if (e.getModifiers() == InputEvent.BUTTON1_MASK) {
      if ((e.getModifiers() & AWTEvent.MOUSE_EVENT_MASK) != 0) {
        updateItem(getSelectedIndex());
        keepOpen = true;
      }
    };
    setRenderer(new CheckBoxCellRenderer<CheckableItem>());
    addActionListener(listener);
    getInputMap(JComponent.WHEN_FOCUSED).put(
        KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, 0), "checkbox-select");
    getActionMap().put("checkbox-select", new AbstractAction() {
      @Override public void actionPerformed(ActionEvent e) {
        Accessible a = getAccessibleContext().getAccessibleChild(0);
        if (a instanceof BasicComboPopup) {
          BasicComboPopup pop = (BasicComboPopup) a;
          updateItem(pop.getList().getSelectedIndex());
        }
      }
    });
  }

  private void updateItem(int index) {
    if (isPopupVisible()) {
      E item = getItemAt(index);
      item.selected ^= true;
      removeItemAt(index);
      insertItemAt(item, index);
      setSelectedIndex(-1);
      setSelectedItem(item);
    }
  }

  @Override public void setPopupVisible(boolean v) {
    if (keepOpen) {
      keepOpen = false;
    } else {
      super.setPopupVisible(v);
    }
  }

  public static <E extends CheckableItem> String getCheckedItemString(
        ListModel<E> model) {
    return IntStream.range(0, model.getSize())
        .mapToObj(model::getElementAt)
        .filter(CheckableItem::isSelected)
        .map(Objects::toString)
        .sorted()
        .collect(Collectors.joining(", "));
  }
}
}}

* 解説 [#explanation]
- タイトルと選択状態をもつアイテムオブジェクト`CheckableItem`を作成し、そのモデルとして`ComboBoxModel<CheckableItem>`を作成
- `CheckBoxCellRenderer<E extends CheckableItem>`を作成し、チェック状態を表示
-- `JComboBox`本体: レンダラーに`JLabel`を使用し、選択されている`CheckableItem`のタイトルを収集してカンマで結合して一覧表示
-- ドロップダウンリスト: レンダラーに`JCheckBox`を使用し、チェック状態とタイトルを表示
- `JComboBox`に`ActionListener`を追加し、マウスの左クリックかつドロップダウンリストが表示されている場合は、選択されたアイテムのチェック状態を反転
-- この場合は、ドロップダウンリストを閉じないように、`JComboBox#setPopupVisible(...)`をオーバーライド
- KBD{Space}キーでアイテムが選択された場合は、`BasicComboPopup`から`JList`を取得し、その選択アイテムを取得する
-- この場合、`JComboBox#getSelectedIndex()`などを使用すると、ハイライト(`cellHasFocus`)されているアイテムではなく、選択状態(`isSelected`)のアイテムが取得される
-- `JComboBox`本体: レンダラーに`JLabel`を使用し選択されている`CheckableItem`のタイトルを収集してカンマで結合して一覧表示
-- ドロップダウンリスト: レンダラーに`JCheckBox`を使用しチェック状態とタイトルを表示
- `JComboBox`に`ActionListener`を追加し、マウスの左クリックかつドロップダウンリストが表示されている場合は選択されたアイテムのチェック状態を反転
-- この場合は、ドロップダウンリストを閉じないように`JComboBox#setPopupVisible(...)`をオーバーライド
- KBD{Space}キーでアイテムが選択された場合は、`BasicComboPopup`から`JList`を取得してその選択アイテムを取得する
-- この場合`JComboBox#getSelectedIndex()`などを使用するとハイライト(`cellHasFocus`)されているアイテムではなく選択状態(`isSelected`)のアイテムが取得される
- `Ubuntu 20.04.2 LTS`環境でドロップダウンリストの再描画がおかしくなる場合がある
-- 以下のようにセルレンダラーを`DefaultListCellRenderer`、クリックイベントの取得を`MouseListener`に変更すると解消される?

//* 参考リンク [#reference] [#reference]
#code{{
Accessible a = getAccessibleContext().getAccessibleChild(0);
if (a instanceof ComboPopup) {
  ((ComboPopup) a).getList().addMouseListener(new MouseAdapter() {
    @Override public void mousePressed(MouseEvent e) {
      JList<?> list = (JList<?>) e.getComponent();
      if (SwingUtilities.isLeftMouseButton(e)) {
        keepOpen = true;
        updateItem(list.locationToIndex(e.getPoint()));
      }
    }
  });
}

DefaultListCellRenderer renderer = new DefaultListCellRenderer();
JCheckBox check = new JCheckBox();
check.setOpaque(false);
setRenderer((list, value, index, isSelected, cellHasFocus) -> {
  panel.removeAll();
  Component c = renderer.getListCellRendererComponent(
      list, value, index, isSelected, cellHasFocus);
  if (index < 0) {
    String txt = getCheckedItemString(list.getModel());
    JLabel l = (JLabel) c;
    l.setText(txt.isEmpty() ? " " : txt);
    l.setOpaque(false);
    l.setForeground(list.getForeground());
    panel.setOpaque(false);
  } else {
    check.setSelected(value.isSelected());
    panel.add(check, BorderLayout.WEST);
    panel.setOpaque(true);
    panel.setBackground(c.getBackground());
  }
  panel.add(c);
  return panel;
});
}}

* 参考リンク [#reference]
- [[JComboBoxのドロップダウンリスト中にあるアイテムの状態を更新する>Swing/UpdateComboBoxItem]]

* コメント [#comment]
#comment
#comment