• category: swing folder: RoundedSelectionTree title: JTreeの選択領域描画をラウンド矩形に変更する tags: [JTree, NimbusLookAndFeel] author: aterai pubdate: 2024-04-01T02:30:20+09:00 description: JTreeの選択を行全体に拡張し、その隅を丸めてラウンド矩形で描画します。 image: https://drive.google.com/uc?id=11-4gPgFYz6fegNlPLellh5ZExqAoFeDc

概要

JTreeの選択を行全体に拡張し、その隅を丸めてラウンド矩形で描画します。

サンプルコード

class RoundedSelectionTree extends JTree {
  private static final Color SELECTED_COLOR = new Color(0xC8_00_78_D7, true);

  @Override protected void paintComponent(Graphics g) {
    int[] selectionRows = getSelectionRows();
    if (selectionRows != null) {
      Graphics2D g2 = (Graphics2D) g.create();
      g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                          RenderingHints.VALUE_ANTIALIAS_ON);
      g2.setPaint(SELECTED_COLOR);
      Rectangle innerArea = SwingUtilities.calculateInnerArea(this, null);
      Area area = new Area();
      Arrays.stream(selectionRows)
          .mapToObj(this::getRowBounds)
          .map(r -> new Rectangle(innerArea.x, r.y, innerArea.width, r.height))
          .forEach(r -> area.add(new Area(r)));
      int arc = 10;
      for (Area a : singularization(area)) {
        Rectangle r = a.getBounds();
        g2.fillRoundRect(r.x, r.y, r.width - 1, r.height - 1, arc, arc);
      }
      g2.dispose();
    }
    super.paintComponent(g);
  }

  private static List<Area> singularization(Area rect) {
    List<Area> list = new ArrayList<>();
    Path2D path = new Path2D.Double();
    PathIterator pi = rect.getPathIterator(null);
    double[] coords = new double[6];
    while (!pi.isDone()) {
      int pathSegmentType = pi.currentSegment(coords);
      switch (pathSegmentType) {
        case PathIterator.SEG_MOVETO:
          path.moveTo(coords[0], coords[1]);
          break;
        case PathIterator.SEG_LINETO:
          path.lineTo(coords[0], coords[1]);
          break;
        case PathIterator.SEG_CLOSE:
          path.closePath();
          list.add(new Area(path));
          path.reset();
          break;
        default:
          break;
      }
      pi.next();
    }
    return list;
  }

  @Override public void updateUI() {
    super.updateUI();
    UIManager.put("Tree.repaintWholeRow", Boolean.TRUE);
    setCellRenderer(new TransparentTreeCellRenderer());
    setOpaque(false);
    setRowHeight(20);
    UIDefaults d = new UIDefaults();
    String key = "Tree:TreeCell[Enabled+Selected].backgroundPainter";
    d.put(key, new TransparentTreeCellPainter());
    putClientProperty("Nimbus.Overrides", d);
    putClientProperty("Nimbus.Overrides.InheritDefaults", false);
    addTreeSelectionListener(e -> repaint());
  }
}
View in GitHub: Java, Kotlin

解説

  • JTreeの選択背景色をDefaultTreeCellRenderer#getBackgroundSelectionColor()をオーバーライドして透明色new Color(0x0, true)に変更し、セルレンダラーでは選択背景を描画しないよう設定
    • NimubsLookAndFeelの場合はなにも描画しないRegionPainterを設定することで選択背景を描画しないよう設定
  • JTree#paintComponent(...)をオーバーライドして選択背景をラウンド矩形として描画
    • JTree#getSelectionRows()で取得した選択行をJTree#getRowBounds(row)で選択領域に変換し、Area#add(new Area(Rectangle))でひとつのAreaにまとめる
    • TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTIONで選択範囲の項目数に制限はなく、各項目は連続している必要がない場合は上記のAreaに複数セグメントが存在するので、これを単一の閉じられたサブパスから構成されているArea(Area#isSingular()==trueとなる)ごとに分割
    • 分割した各AreaArea#getBounds()で取得した矩形領域の4隅を丸めて選択背景として描画
  • たとえば連続する上中下の3セルが選択された状態から中セルをCtrl+クリックで選択解除した場合、上セルの下隅、下セルの上隅が丸められるが上セル、下セルの選択状態は変化しないのでこれらの再描画が実行されない
    • このサンプルでは選択状態が変化したらJTree全体を再描画することで丸めの更新を再描画している

参考リンク

コメント