概要

JScrollBarなどにJTextAreaの文字列検索の結果をハイライト表示します。

サンプルコード

scrollbar.setUI(new WindowsScrollBarUI() {
  @Override protected void paintTrack(
      Graphics g, JComponent c, Rectangle trackBounds) {
    super.paintTrack(g, c, trackBounds);

    Rectangle rect = textArea.getBounds();
    double sy = trackBounds.getHeight() / rect.getHeight();
    AffineTransform at = AffineTransform.getScaleInstance(1d, sy);
    Highlighter highlighter = textArea.getHighlighter();
    g.setColor(Color.YELLOW);
    try {
      for (Highlighter.Highlight hh: highlighter.getHighlights()) {
        Rectangle r = textArea.modelToView(hh.getStartOffset());
        Rectangle s = at.createTransformedShape(r).getBounds();
        int h = 2; //Math.max(2, s.height - 2);
        g.fillRect(trackBounds.x, trackBounds.y + s.y, trackBounds.width, h);
      }
    } catch (BadLocationException e) {
      e.printStackTrace();
    }
  }
});
view all

解説

上記のサンプルでは、ScrollBarUI#paintTrack(...)メソッドをオーバーライドして、JTextArea内の文字列の検索結果を縦のJScrollBar内部に描画しています。

  • 注:
    • 1行分のハイライトの高さは2pxで固定
    • 検索結果の位置はJTextComponent#modelToView(Matcher#start());を利用しているため、ハイライト対象の文字列が折り返しで2行になっても、ハイライトされるのは開始位置のある1行目のみ

以下のようなIconを設定したJLabelJScrollPane#setRowHeaderView(...)で追加する方法もあります。こちらは、縦JScrollBarに直接ハイライトを描画しないので、上下の増減ボタンは考慮せず、またノブの代わりに現在表示位置を示す領域を半透明で描画しています。

JLabel label = new JLabel(new Icon() {
  private final Color THUMB_COLOR = new Color(0, 0, 255, 50);
  private final Rectangle thumbRect = new Rectangle();
  private final JTextComponent textArea;
  private final JScrollBar scrollbar;
  public HighlightIcon(JTextComponent textArea, JScrollBar scrollbar) {
    this.textArea  = textArea;
    this.scrollbar = scrollbar;
  }
  @Override public void paintIcon(Component c, Graphics g, int x, int y) {
    //Rectangle rect   = textArea.getBounds();
    //Dimension sbSize = scrollbar.getSize();
    //Insets sbInsets  = scrollbar.getInsets();
    //double sy = (sbSize.height - sbInsets.top - sbInsets.bottom) / rect.getHeight();
    int itop = scrollbar.getInsets().top;
    BoundedRangeModel range = scrollbar.getModel();
    double sy = range.getExtent() / (double) (range.getMaximum() - range.getMinimum());
    AffineTransform at = AffineTransform.getScaleInstance(1.0, sy);
    Highlighter highlighter = textArea.getHighlighter();

    //paint Highlight
    g.setColor(Color.RED);
    try {
      for (Highlighter.Highlight hh: highlighter.getHighlights()) {
        Rectangle r = textArea.modelToView(hh.getStartOffset());
        Rectangle s = at.createTransformedShape(r).getBounds();
        int h = 2; //Math.max(2, s.height - 2);
        g.fillRect(x, y + itop + s.y, getIconWidth(), h);
      }
    } catch (BadLocationException e) {
      e.printStackTrace();
    }

    //paint Thumb
    if (scrollbar.isVisible()) {
      //JViewport vport = Objects.requireNonNull(
      //  (JViewport) SwingUtilities.getAncestorOfClass(JViewport.class, textArea));
      //Rectangle thumbRect = vport.getBounds();
      thumbRect.height = range.getExtent();
      thumbRect.y = range.getValue(); //vport.getViewPosition().y;
      g.setColor(THUMB_COLOR);
      Rectangle s = at.createTransformedShape(thumbRect).getBounds();
      g.fillRect(x, y + itop + s.y, getIconWidth(), s.height);
    }
  }
  @Override public int getIconWidth() {
    return 8;
  }
  @Override public int getIconHeight() {
    JViewport vport = Objects.requireNonNull(
      (JViewport) SwingUtilities.getAncestorOfClass(JViewport.class, textArea));
    return vport.getHeight();
  }
});

scroll.setVerticalScrollBar(scrollbar);
/*
// Fixed Versions: 7 (b134)
scroll.setRowHeaderView(label);
/*/
// 6826074 JScrollPane does not revalidate the component hierarchy after scrolling
// http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6826074
// Affected Versions: 6u12,6u16,7
JViewport vp = new JViewport() {
  @Override public void setViewPosition(Point p) {
    super.setViewPosition(p);
    revalidate();
  }
};
vp.setView(label);
scroll.setRowHeader(vp);

JScrollBarの中ではなく、左横などにハイライト位置用のIconを表示したい場合は、MatteBorderを利用する方法があります。

JScrollBar scrollBar = new JScrollBar(Adjustable.VERTICAL) {
  @Override public Dimension getPreferredSize() {
    Dimension d = super.getPreferredSize();
    d.width += getInsets().left;
    return d;
  }
  @Override public void updateUI() {
    super.updateUI();
    setBorder(BorderFactory.createMatteBorder(0, 4, 0, 0, new Icon() {
      @Override public void paintIcon(Component c, Graphics g, int x, int y) {
        //...略...
      }
      @Override public int getIconWidth() {
        return getInsets().left;
      }
      @Override public int getIconHeight() {
        return getHeight();
      }
    }));
  }
};
scroll.setVerticalScrollBar(scrollBar);

コメント

  • 行ヘッダーを使用したハイライトはJava7以降でのみ有効に機能するようです。 -- 読者
    • ご指摘ありがとうございます。仰るとおり、1.6.0_45で行ヘッダ版が正常に動作しないことを確認しました。回避方法がないか、Bug Databaseあたりを調べてみようと思います。 -- aterai
    • 修正された時期などから、Bug ID: JDK-6910490 MatteBorder JScrollpane interactionが原因かもとMatteBorderは使用せずに直接IconJLabelに追加するよう変更したけど、改善しない…。 -- aterai
    • Bug ID: JDK-6826074 JScrollPane does not revalidate the component hierarchy after scrollingが原因(HeavyWeight, LightWeightだけじゃなくレイアウトがうまく更新されていない?)のようです。JViewport#setViewPosition(...)をオーバーライドしてrevalidate()すれば、1.7.0と同様の動作をするようになりました。 -- aterai
  • Highlighter.Highlight#getStartOffset()を使用するように変更。 -- aterai