---
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
hreflang:
    href: https://java-swing-tips.blogspot.com/2023/04/change-tooltip-of-tab-in-jtabbedpane-to.html
    lang: en
---
* 概要 [#summary]
`JTabbedPane`のタブ用ツールヒントの形状をふきだしにし、そのしっぽの方向をタブ位置に応じて変更します。

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

* サンプルコード [#sourcecode]
#code(link){{
class BalloonToolTip extends JToolTip {
  private static final int SIZE = 4;
  private static final double ARC = 4d;
  private transient HierarchyListener listener;
  private transient Shape shape;

  @Override public void updateUI() {
    removeHierarchyListener(listener);
    super.updateUI();
    setLayout(new BorderLayout());
    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);
    setBorder(BorderFactory.createEmptyBorder(SIZE, SIZE, SIZE, SIZE));
  }

  @Override public Dimension getPreferredSize() {
    Dimension d = super.getPreferredSize();
    d.width += SIZE;
    d.height += SIZE;
    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 placement) {
    Insets i = getInsets();
    Dimension d = getPreferredSize();
    Path2D tail = new Path2D.Double();
    double w = d.getWidth() - i.left - i.right - 1d;
    double h = d.getHeight() - i.top - i.bottom - 1d;
    double cx = w / 2d;
    double cy = h / 2d;
    switch (placement) {
      case SwingConstants.LEFT:
        tail.moveTo(0, cy - SIZE);
        tail.lineTo(-SIZE, cy);
        tail.lineTo(0, cy + SIZE);
        break;
      case SwingConstants.RIGHT:
        tail.moveTo(w, cy - SIZE);
        tail.lineTo(w + SIZE, cy);
        tail.lineTo(w, cy + SIZE);
        break;
      case SwingConstants.BOTTOM:
        tail.moveTo(cx - SIZE, h);
        tail.lineTo(cx, h + SIZE);
        tail.lineTo(cx + SIZE, h);
        break;
      default: // case SwingConstants.TOP:
        tail.moveTo(cx - SIZE, 0);
        tail.lineTo(cx, -SIZE);
        tail.lineTo(cx + SIZE, 0);
    }
    Area area = new Area(new RoundRectangle2D.Double(0, 0, w, h, ARC, ARC));
    area.add(new Area(tail));
    AffineTransform at = AffineTransform.getTranslateInstance(i.left, i.top);
    shape = at.createTransformedShape(area);
  }
}
}}

* 解説 [#explanation]
- `JToolTip`の形状変更は[[JToolTipの形状を吹き出し風に変更する>Swing/BalloonToolTip]]と同様の方法を使用
-- `JToolTip`のテキストが長くなると`JToolTip`を常に親`JFrame`内に表示される方向に設定していても`HeavyWeightWindow`が使用される可能性があるので`JWindow`を透明する設定もそのまま使用している
- `JToolTip`が初回表示される前に`JToolTip#getVisibleRect()`を実行しても正しいサイズが取得できないので、代わりに`JToolTip#getPreferredSize()`を使用する必要がある
-- `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(...)`を以下のようにオーバーライドして変更
-- `NimbusLookAndFeel`などで`JToolTip`の形が変更できない
--- `UIDefault#put("ToolTip[Enabled].backgroundPainter", Painter<JToolTip>...);`などで背景を描画しない`Painter<JToolTip>`を設定する、または`JToolTip`に直接テキストを描画するのではなく`JLabel`を追加してテキストを描画すれば回避可能
--- `UIDefault#put("ToolTip[Enabled].backgroundPainter", Painter<JToolTip>...)`などで背景を描画しない`Painter<JToolTip>`を設定する、または`JToolTip`に直接テキストを描画するのではなく`JLabel`を追加してテキストを描画すれば回避可能

#code{{
JTabbedPane tabs = new JTabbedPane(
    SwingConstants.TOP, JTabbedPane.SCROLL_TAB_LAYOUT) {
  private transient BalloonToolTip tip;
  private final JLabel label = new JLabel(" ", CENTER);

  @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);
      label.setText(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();
      LookAndFeel.installColorsAndFont(
          label,
          "ToolTip.background",
          "ToolTip.foreground",
          "ToolTip.font");
      tip.add(label);
      tip.updateBalloonShape(getTabPlacement());
      tip.setComponent(this);
    }
    return tip;
  }

  @Override public void updateUI() {
    tip = null;
    super.updateUI();
  }
};
}}

* 参考リンク [#reference]
- [[JToolTipの形状を吹き出し風に変更する>Swing/BalloonToolTip]]
- [[JToolTipを半透明にする>Swing/TranslucentToolTips]]
- [[JTabbedPaneのサムネイルをJToolTipで表示>Swing/TabThumbnail]]

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