概要

JComboBoxのアイテムとしてJCheckBoxを表示し、ドロップダウンリストを開いたままこれを複数選択可能に設定します。

サンプルコード

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() & 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;
      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(", "));
  }
}
View in GitHub: Java, Kotlin

解説

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

参考リンク

コメント