概要

JTableのセルを選択可能に設定し、その選択背景描画をラウンド矩形に変更します。

サンプルコード

class RoundedCellSelectionTable extends JTable {
  protected RoundedCellSelectionTable(TableModel model) {
    super(model);
  }

  @Override public void updateUI() {
    super.updateUI();
    setOpaque(false);
    setFocusable(false);
    setCellSelectionEnabled(true);
    setShowGrid(false);
    setIntercellSpacing(new Dimension());
    setAutoCreateRowSorter(true);
    setBackground(new Color(0x0, true));
    setRowHeight(20);
    if (getUI() instanceof SynthTableUI) {
      setDefaultRenderer(
          Boolean.class, new SynthBooleanTableCellRenderer2());
    }
  }

  @Override public Component prepareRenderer(
        TableCellRenderer renderer, int row, int column) {
    Component c = super.prepareRenderer(renderer, row, column);
    if (c instanceof JComponent) {
      ((JComponent) c).setOpaque(false);
    }
    return c;
  }

  @Override public Component prepareEditor(
        TableCellEditor editor, int row, int column) {
    Component c = super.prepareEditor(editor, row, column);
    if (c instanceof JComponent) {
      ((JComponent) c).setOpaque(false);
    }
    return c;
  }

  @Override public void changeSelection(
        int rowIndex, int columnIndex,
        boolean toggle, boolean extend) {
    super.changeSelection(rowIndex, columnIndex, toggle, extend);
    repaint();
  }

  @Override protected void paintComponent(Graphics g) {
    if (getSelectedColumnCount() != 0 && getSelectedRowCount() != 0) {
      Graphics2D g2 = (Graphics2D) g.create();
      g2.setRenderingHint(
          RenderingHints.KEY_ANTIALIASING,
          RenderingHints.VALUE_ANTIALIAS_ON);
      g2.setPaint(getSelectionBackground());
      Area area = new Area();
      for (int row : getSelectedRows()) {
        for (int col : getSelectedColumns()) {
          addArea(area, row, col);
        }
      }
      // Arrays.stream(getSelectedRows())
      //     .boxed()
      //     .flatMap(row -> Arrays.stream(getSelectedColumns())
      //         .filter(col -> isCellSelected(row, col))
      //         .mapToObj(col -> getCellRect(row, col, true))
      //         .map(Area::new))
      //     .forEach(area::add);
      // if (!area.isEmpty()) {
      int arc = 8;
      for (Area a : GeomUtils.singularization(area)) {
        // List<Point2D> lst = GeomUtils.convertAreaToPoint2DList(a);
        // g2.fill(GeomUtils.convertRoundedPath(lst, arc / 2d));
        Rectangle r = a.getBounds();
        g2.fillRoundRect(r.x, r.y, r.width - 1, r.height - 1, arc, arc);
      }
      g2.dispose();
    }
    super.paintComponent(g);
  }

  private void addArea(Area area, int row, int col) {
    if (isCellSelected(row, col)) {
      area.add(new Area(getCellRect(row, col, true)));
    }
  }
}
View in GitHub: Java, Kotlin

解説

  • JTable#setCellSelectionEnabled(true)で列選択ではなくセル選択を可能に設定
  • JTable#setShowGrid(false)JTable#setIntercellSpacing(new Dimension())でグリッド線を非表示化
  • セルレンダラー、セルエディタを透明化してこれらを使用した選択背景の描画を無効化
    • JTable#prepareRenderer(...)JTable#prepareEditor(...)をオーバーライドしてすべてのセルレンダラー、セルエディタが使用される直前にJComponent#setOpaque(false)を実行して透明化
    • NimbusLookAndFeelなどで使用されるSynthBooleanTableCellRendererJCheckBox#isOpaque()をオーバーライドして選択行の場合は常にtrueを返すため、JTable#prepareRenderer(...)JComponent#setOpaque(false)を実行しても透明化(セル選択描画の無効化)が無視されてしまう
    • このサンプルではSynthBooleanTableCellRenderer#isOpaque()をオーバーライドしないセルレンダラーをコピーしてNimbusLookAndFeelBooleanTableCellRendererとして使用することで回避
  • JTable#paintComponent(...)をオーバーライドして選択背景をラウンド矩形として描画
    • 以下、JTreeの選択領域描画をラウンド矩形に変更すると同等の処理を実行してセル選択背景をラウンド矩形化している
    • JTable#isCellSelected(...)で選択されたセルをすべて取得、JTable#getCellRect(...)で選択領域に変換し、Area#add(new Area(Rectangle))でひとつのAreaにまとめる
    • まとめられたAreaにはセル選択状態によって複数セグメントが存在する場合があるため、これを単一の閉じられたサブパスから構成されているArea(Area#isSingular()==trueとなる)ごとに分割
    • 分割した各AreaArea#getBounds()で取得した矩形領域の4隅を丸めて選択背景として描画
  • たとえば連続する上中下の3セルが選択された状態から中セルをCtrl+クリックで選択解除した場合、上セルの下隅、下セルの上隅が丸められるが上セル、下セルの選択状態は変化しないのでこれらの再描画が実行されない
    • このサンプルではJTable#changeSelection(...)をオーバーライドして選択状態が変化したらJTable全体を再描画することで丸めの更新を再描画している

参考リンク

コメント