概要

JListのセルに項目選択チェックボックスを追加表示してキー操作なしで複数選択可能にします。

サンプルコード

@Override public void setSelectionInterval(int anchor, int lead) {
  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) {
    rollOverIndex = -1;
    e.getComponent().repaint();
  }

  @Override public void mouseMoved(MouseEvent e) {
    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));
      }
      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);
        }
      });
    }
  }
}
View in GitHub: Java, Kotlin

解説

  • JList#setLayoutOrientation(JList.HORIZONTAL_WRAP)メソッドを使用してセルを左上から水平方向に並べる水平ニュースペーパー・スタイルレイアウトJListを作成
  • 各セルの右上隅にJCheckBoxを表示するセルレンダラーを作成
    • セルが選択されている場合はJCheckBox#setSelected(true)で選択状態
    • セルがロールオーバーの場合はJCheckBox#setVisible(true)で項目選択チェックボックスを表示
      • 幅がJCheckBoxと同じで高さ0のコンポーネントとあわせてJPanel(new BorderLayout())に配置することでロールオーバーなしで非表示状態の場合でもセル内のレイアウトが変化しないよう設定
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を配置する
    • コンポーネントがJCheckBoxの場合、オーバーライドしたJList#setSelectionInterval(...)で現状の選択状態は維持したままクリックしたJCheckBoxの存在するセルのみsuper.addSelectionInterval(checkedIndex, checkedIndex)で選択状態に追加
    • コンポーネントがJCheckBoxではない場合、通常のセル選択を実行

参考リンク

コメント