---
category: swing
folder: ListCellItemCheckBoxes
title: JListのセルに項目選択チェックボックスを追加する
tags: [JList, ListCellRenderer, JCheckBox]
author: aterai
pubdate: 2023-07-10T00:36:53+09:00
description: JListのセルに項目選択チェックボックスを追加表示してキー操作なしで複数選択可能にします。
image: https://drive.google.com/uc?id=1VwvQktyjmLrWzhYNd_jdwr5DdArY9mqD
hreflang:
    href: https://java-swing-tips.blogspot.com/2023/07/add-item-check-boxes-to-jlist-cells.html
    lang: en
---
* 概要 [#summary]
`JList`のセルに項目選択チェックボックスを追加表示してキー操作なしで複数選択可能にします。

#download(https://drive.google.com/uc?id=1VwvQktyjmLrWzhYNd_jdwr5DdArY9mqD)

* サンプルコード [#sourcecode]
#code(link){{
@Override public void setSelectionInterval(int anchor, int lead) {
  if (checkedIndex < 0 && !getRubberBand().getBounds().isEmpty()) {
  if (checkedIndex < 0 && isDragging()) {
    super.setSelectionInterval(anchor, lead);
  } else {
    EventQueue.invokeLater(() -> {
      if (checkedIndex >= 0 && lead == anchor && checkedIndex == anchor) {
        super.addSelectionInterval(checkedIndex, checkedIndex);
      } else {
        super.setSelectionInterval(anchor, lead);
      }
    });
  }
}

protected boolean isDragging() {
  Rectangle r = getRubberBand().getBounds();
  return r.width != 0 || r.height != 0;
}

@Override public void removeSelectionInterval(int index0, int index1) {
  if (checkedIndex < 0) {
    super.removeSelectionInterval(index0, index1);
  } else {
    EventQueue.invokeLater(() -> super.removeSelectionInterval(index0, index1));
  }
}

private static <E> Optional<AbstractButton> getItemCheckBox(
    JList<E> list, MouseEvent e, int index) {
  if (e.isShiftDown() || e.isControlDown() || e.isAltDown()) {
    return Optional.empty();
  }
  E proto = list.getPrototypeCellValue();
  ListCellRenderer<? super E> cr = list.getCellRenderer();
  Component c = cr.getListCellRendererComponent(
      list, proto, index, false, false);
  Rectangle r = list.getCellBounds(index, index);
  c.setBounds(r);
  Point pt = e.getPoint();
  pt.translate(-r.x, -r.y);
  return Optional
    .ofNullable(SwingUtilities.getDeepestComponentAt(c, pt.x, pt.y))
    .filter(AbstractButton.class::isInstance)
    .map(AbstractButton.class::cast);
}

private final class ItemCheckBoxesListener extends MouseAdapter {
  private final Point srcPoint = new Point();

  @Override public void mouseDragged(MouseEvent e) {
    checkedIndex = -1;
    JList<?> l = (JList<?>) e.getComponent();
    l.setFocusable(true);
    Point destPoint = e.getPoint();
    Path2D rb = getRubberBand();
    rb.reset();
    rb.moveTo(srcPoint.x, srcPoint.y);
    rb.lineTo(destPoint.x, srcPoint.y);
    rb.lineTo(destPoint.x, destPoint.y);
    rb.lineTo(srcPoint.x, destPoint.y);
    rb.closePath();

    int[] indices = IntStream.range(0, l.getModel().getSize())
            .filter(i -> rb.intersects(l.getCellBounds(i, i))).toArray();
    l.setSelectedIndices(indices);
    l.repaint();
  }

  @Override public void mouseExited(MouseEvent e) {
    rollOverRowIndex = -1;
    rollOverIndex = -1;
    e.getComponent().repaint();
  }

  @Override public void mouseMoved(MouseEvent e) {
    int row = locationToIndex(e.getPoint());
    if (row != rollOverRowIndex) {
      Rectangle rect = getCellBounds(row, row);
      if (rollOverRowIndex >= 0) {
        rect.add(getCellBounds(rollOverRowIndex, rollOverRowIndex));
    Point pt = e.getPoint();
    int idx = locationToIndex(pt);
    if (!getCellBounds(idx, idx).contains(pt)) {
      idx = -1;
    }
    Rectangle rect = new Rectangle();
    if (idx > 0) {
      rect.add(getCellBounds(idx, idx));
      if (rollOverIndex >= 0 && idx != rollOverIndex) {
        rect.add(getCellBounds(rollOverIndex, rollOverIndex));
      }
      rollOverRowIndex = row;
      ((JComponent) e.getComponent()).repaint(rect);
      rollOverIndex = idx;
    } else {
      if (rollOverIndex >= 0) {
        rect.add(getCellBounds(rollOverIndex, rollOverIndex));
      }
      rollOverIndex = -1;
    }
    ((JComponent) e.getComponent()).repaint(rect);
  }

  @Override public void mouseReleased(MouseEvent e) {
    getRubberBand().reset();
    Component c = e.getComponent();
    c.setFocusable(true);
    c.repaint();
  }

  @Override public void mousePressed(MouseEvent e) {
    JList<?> l = (JList<?>) e.getComponent();
    int index = l.locationToIndex(e.getPoint());
    if (l.getCellBounds(index, index).contains(e.getPoint())) {
      l.setFocusable(true);
      cellPressed(l, e, index);
    } else {
      l.setFocusable(false);
      l.clearSelection();
      l.getSelectionModel().setAnchorSelectionIndex(-1);
      l.getSelectionModel().setLeadSelectionIndex(-1);
    }
    srcPoint.setLocation(e.getPoint());
    l.repaint();
  }

  private void cellPressed(JList<?> l, MouseEvent e, int index) {
    if (e.getButton() == MouseEvent.BUTTON1 && e.getClickCount() > 1) {
      ListItem item = getModel().getElementAt(index);
      JOptionPane.showMessageDialog(l.getRootPane(), item.title);
    } else {
      checkedIndex = -1;
      getItemCheckBox(l, e.getPoint(), index).ifPresent(rb -> {
        checkedIndex = index;
        if (l.isSelectedIndex(index)) {
          l.setFocusable(false);
          removeSelectionInterval(index, index);
        } else {
          setSelectionInterval(index, index);
        }
      });
    }
  }
}
}}

* 解説 [#explanation]
- `JList#setLayoutOrientation(JList.HORIZONTAL_WRAP)`メソッドを使用してセルを左上から水平方向に並べる[https://docs.oracle.com/javase/jp/8/docs/api/javax/swing/JList.html#HORIZONTAL_WRAP 水平ニュースペーパー・スタイルレイアウト]の`JList`を作成
-- [[JListのアイテムを範囲指定で選択>Swing/RubberBanding]]
- 各セルの右上隅に`JCheckBox`を表示するセルレンダラーを作成
-- セルが選択されている場合は`JCheckBox#setSelected(true)`で選択状態
-- セルがロールオーバーの場合は`JCheckBox#setVisible(true)`で項目選択チェックボックスを表示
--- 幅が`JCheckBox`と同じで高さ`0`のコンポーネントとあわせて`JPanel(new BorderLayout())`に配置することでロールオーバーなしで非表示状態の場合でもセル内のレイアウトが変化しないよう設定

#code{{
class ListItemCellRenderer implements ListCellRenderer<E> {
    private final JPanel renderer = new JPanel(new BorderLayout(0, 0));
    private final AbstractButton check = new JCheckBox();
    private final JLabel icon = new JLabel("", null, SwingConstants.CENTER);
    private final JLabel label = new JLabel("", SwingConstants.CENTER);
    private final JPanel itemPanel = new JPanel(new BorderLayout(2, 2)) {
      @Override protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        if (SELECTED_COLOR.equals(getBackground())) {
          Graphics2D g2 = (Graphics2D) g.create();
          g2.setPaint(SELECTED_COLOR);
          g2.fillRect(0, 0, getWidth(), getHeight());
          g2.dispose();
        }
      }
    };
    protected ListItemCellRenderer() {
      // ...
      Dimension d = check.getPreferredSize();
      JPanel p = new JPanel(new BorderLayout(0, 0));
      p.setOpaque(false);
      p.add(check, BorderLayout.NORTH);
      p.add(Box.createHorizontalStrut(d.width), BorderLayout.SOUTH);
      itemPanel.add(p, BorderLayout.EAST);
      itemPanel.add(Box.createHorizontalStrut(d.width), BorderLayout.WEST);
      itemPanel.add(icon);
      itemPanel.add(label, BorderLayout.SOUTH);
      itemPanel.setOpaque(true);
      renderer.add(itemPanel);
      renderer.setOpaque(false);
      // ...
}}

- セルがクリックされたとき、その位置に存在するコンポーネントを`SwingUtilities.getDeepestComponentAt(...)`メソッドで検索
-- [[JListのセル内にJButtonを配置する>Swing/ButtonsInListCell]]
-- コンポーネントが`JCheckBox`の場合、オーバーライドした`JList#setSelectionInterval(...)`で現状の選択状態は維持したままクリックした`JCheckBox`の存在するセルのみ`super.addSelectionInterval(checkedIndex, checkedIndex)`で選択状態に追加
--- [[JListをマウスクリックのみで複数選択する>Swing/ListMouseSelection]]
--- すでにクリックした`JCheckBox`のセルが選択状態の場合は、オーバーライドした`JList#removeSelectionInterval(...)`で対象セルのみ選択状態を解除
-- コンポーネントが`JCheckBox`ではない場合、通常のセル選択を実行

* 参考リンク [#reference]
- [[JListのアイテムを範囲指定で選択>Swing/RubberBanding]]
- [[JListのセル内にJButtonを配置する>Swing/ButtonsInListCell]]
- [[JListをマウスクリックのみで複数選択する>Swing/ListMouseSelection]]

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