• category: swing folder: SpeechBalloonToolTipTail title: JTabbedPaneのツールヒントをタブ位置に対応したふきだしに変更する tags: [JToolTip, JTabbedPane] author: aterai pubdate: 2023-04-17T03:14:21+09:00 description: JTabbedPaneのタブ用ツールヒントの形状をふきだしにし、そのしっぽの方向をタブ位置に応じて変更します。 image: https://drive.google.com/uc?id=1WbgFj8zOssgmjFU6rsL0vXNgW5Lsw7OO

概要

JTabbedPaneのタブ用ツールヒントの形状をふきだしにし、そのしっぽの方向をタブ位置に応じて変更します。

サンプルコード

class BalloonToolTip extends JToolTip {
  private static final int SIZE = 4;
  private transient HierarchyListener listener;
  private transient Shape shape;

  @Override public void updateUI() {
    removeHierarchyListener(listener);
    super.updateUI();
    listener = e -> {
      Component c = e.getComponent();
      if ((e.getChangeFlags() & HierarchyEvent.SHOWING_CHANGED) != 0
           && c.isShowing()) {
        Optional.ofNullable(SwingUtilities.getWindowAncestor(c))
            .filter(w -> w.getType() == Window.Type.POPUP)
            .ifPresent(w -> w.setBackground(new Color(0x0, true)));
      }
    };
    addHierarchyListener(listener);
    setOpaque(false);
  }

  @Override public Dimension getPreferredSize() {
    Dimension d = super.getPreferredSize();
    d.height = 24;
    return d;
  }

  @Override protected void paintComponent(Graphics g) {
    Graphics2D g2 = (Graphics2D) g.create();
    g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                        RenderingHints.VALUE_ANTIALIAS_ON);
    g2.setColor(getBackground());
    g2.fill(shape);
    g2.setPaint(getForeground());
    g2.draw(shape);
    g2.dispose();
    super.paintComponent(g);
  }

  public void updateBalloonShape(int l) {
    Rectangle r = getVisibleRect();
    r.setSize(getPreferredSize());
    Path2D tail = new Path2D.Double();
    double w = r.getWidth() - 1d;
    double h = r.getHeight() - 1d;
    double arc = 10d;
    Shape bubble;
    switch (l) {
      case SwingConstants.LEFT:
        setBorder(BorderFactory.createEmptyBorder(2, 2 + SIZE, 2, 2));
        tail.moveTo(r.getMinX() + SIZE, r.getCenterY() - SIZE);
        tail.lineTo(r.getMinX(), r.getCenterY());
        tail.lineTo(r.getMinX() + SIZE, r.getCenterY() + SIZE);
        w -= SIZE;
        bubble = new RoundRectangle2D.Double(
            r.getMinX() + SIZE, r.getMinY(), w, h, arc, arc);
        break;
      case SwingConstants.RIGHT:
        setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2 + SIZE));
        tail.moveTo(r.getMaxX() - SIZE - 1d, r.getCenterY() - SIZE);
        tail.lineTo(r.getMaxX(), r.getCenterY());
        tail.lineTo(r.getMaxX() - SIZE - 1d, r.getCenterY() + SIZE);
        w -= SIZE;
        bubble = new RoundRectangle2D.Double(
            r.getMinX(), r.getMinY(), w, h, arc, arc);
        break;
      case SwingConstants.BOTTOM:
        setBorder(BorderFactory.createEmptyBorder(2, 2, 2 + SIZE, 2));
        tail.moveTo(r.getCenterX() - SIZE, r.getMaxY() - SIZE - 1d);
        tail.lineTo(r.getCenterX(), r.getMaxY());
        tail.lineTo(r.getCenterX() + SIZE, r.getMaxY() - SIZE - 1d);
        h -= SIZE;
        bubble = new RoundRectangle2D.Double(
            r.getMinX(), r.getMinY(), w, h, arc, arc);
        break;
      default: // case SwingConstants.TOP:
        setBorder(BorderFactory.createEmptyBorder(2 + SIZE, 2, 2, 2));
        tail.moveTo(r.getCenterX() - SIZE, r.getMinY() + SIZE);
        tail.lineTo(r.getCenterX(), r.getMinY());
        tail.lineTo(r.getCenterX() + SIZE, r.getMinY() + SIZE);
        h -= SIZE;
        bubble = new RoundRectangle2D.Double(
            r.getMinX(), r.getMinY() + SIZE, w, h, arc, arc);
    }
    Area area = new Area(bubble);
    area.add(new Area(tail));
    shape = area;
  }
}
View in GitHub: Java, Kotlin

解説

  • JToolTipの形状変更はJToolTipの形状を吹き出し風に変更すると同様の方法を使用
    • JToolTipのテキストが長くなるとJToolTipを常に親JFrame内に表示される方向に設定していてもHeavyWeightWindowが使用される可能性があるのでJWindowを透明する設定もそのまま使用している
  • JToolTipが初回表示される前にJToolTip#getVisibleRect()を実行しても正しいサイズが取得できないので、代わりにJToolTip#getPreferredSize()を使用する必要がある
  • JTabbedPane.TOP
    • ふきだしのしっぽの三角形を上辺中央に描画し、対象タブ領域の下辺中央に三角形の頂点が接するようにJToolTipを配置
  • JTabbedPane.BOTTOM
    • ふきだしのしっぽの三角形を下辺中央に描画し、対象タブ領域の上辺中央に三角形の頂点が接するようにJToolTipを配置
  • JTabbedPane.LEFT
    • ふきだしのしっぽの三角形を左辺中央に描画し、対象タブ領域の右辺中央に三角形の頂点が接するようにJToolTipを配置
  • JTabbedPane.RIGHT
    • ふきだしのしっぽの三角形を右辺中央に描画し、対象タブ領域の左辺中央に三角形の頂点が接するようにJToolTipを配置
  • JToolTipの表示位置はJTabbedPane#getToolTipLocation(...)を以下のようにオーバーライドして変更
JTabbedPane tabs = new JTabbedPane() {
  private transient BalloonToolTip tip;

  @Override public Point getToolTipLocation(MouseEvent e) {
    int idx = indexAtLocation(e.getX(), e.getY());
    String txt = idx >= 0 ? getToolTipTextAt(idx) : null;
    return Optional.ofNullable(txt).map(toolTipText -> {
      JToolTip tips = createToolTip();
      tips.setTipText(toolTipText);
      if (tips instanceof BalloonToolTip) {
        ((BalloonToolTip) tips).updateBalloonShape(getTabPlacement());
      }
      return getToolTipPoint(
          getBoundsAt(idx), tips.getPreferredSize());
    }).orElse(null);
  }

  private Point getToolTipPoint(Rectangle r, Dimension d) {
    double dx;
    double dy;
    switch (getTabPlacement()) {
      case LEFT:
        dx = r.getMaxX();
        dy = r.getCenterY() - d.getHeight() / 2d;
        break;
      case RIGHT:
        dx = r.getMinX() - d.width;
        dy = r.getCenterY() - d.getHeight() / 2d;
        break;
      case BOTTOM:
        dx = r.getCenterX() - d.getWidth() / 2d;
        dy = r.getMinY() - d.height;
        break;
      default: // case TOP:
        dx = r.getCenterX() - d.getWidth() / 2d;
        dy = r.getMaxY();
    }
    return new Point((int) (dx + .5), (int) (dy + .5));
  }

  @Override public JToolTip createToolTip() {
    if (tip == null) {
      tip = new BalloonToolTip();
      tip.updateBalloonShape(getTabPlacement());
      tip.setComponent(this);
    }
    return tip;
  }
};

参考リンク

コメント