• 追加された行はこの色です。
  • 削除された行はこの色です。
TITLE:JScrollBarに検索結果をハイライト表示
#navi(../)
#tags(JScrollBar, JScrollPane, JTextArea, JViewport, MatteBorder, Icon)
RIGHT:Posted by &author(aterai); at 2013-01-28
*JScrollBarに検索結果をハイライト表示 [#i481e1eb]
``JScrollBar``などに``JTextArea``の文字列検索の結果をハイライト表示します。
---
category: swing
folder: ScrollBarSearchHighlighter
title: JScrollBarに検索結果をハイライト表示
tags: [JScrollBar, JScrollPane, JTextArea, JTextComponent, JViewport, Icon, Highlighter, Pattern, Matcher, MatteBorder]
author: aterai
pubdate: 2013-01-28T02:11:33+09:00
description: JScrollBarなどにJTextAreaの文字列検索の結果をハイライト表示します。
image: https://lh4.googleusercontent.com/-69jv_2q3f8g/UQT6FH3HXbI/AAAAAAAABcY/FmYcY3aLr6w/s800/ScrollBarSearchHighlighter.png
hreflang:
    href: https://java-swing-tips.blogspot.com/2013/01/jscrollbar-search-highlighter.html
    lang: en
---
* 概要 [#summary]
`JScrollBar`などに`JTextArea`の文字列検索の結果をハイライト表示します。

-&jnlp;
-&jar;
-&zip;
#download(https://lh4.googleusercontent.com/-69jv_2q3f8g/UQT6FH3HXbI/AAAAAAAABcY/FmYcY3aLr6w/s800/ScrollBarSearchHighlighter.png)

//#screenshot
#ref(https://lh4.googleusercontent.com/-69jv_2q3f8g/UQT6FH3HXbI/AAAAAAAABcY/FmYcY3aLr6w/s800/ScrollBarSearchHighlighter.png)

**サンプルコード [#g03baf61]
* サンプルコード [#sourcecode]
#code(link){{
scrollbar.setUI(new WindowsScrollBarUI() {
  @Override protected void paintTrack(
      Graphics g, JComponent c, Rectangle trackBounds) {
    super.paintTrack(g, c, trackBounds);

    Rectangle rect   = textArea.getBounds();
    Rectangle rect = textArea.getBounds();
    double sy = trackBounds.getHeight() / rect.getHeight();
    AffineTransform at = AffineTransform.getScaleInstance(1.0, sy);
    AffineTransform at = AffineTransform.getScaleInstance(1d, sy);
    Highlighter highlighter = textArea.getHighlighter();
    g.setColor(Color.YELLOW);
    try{
      for(Integer pos: poslist) {
        Rectangle r = textArea.modelToView(pos);
    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);
        int h = 2; // Math.max(2, s.height - 2);
        g.fillRect(trackBounds.x, trackBounds.y + s.y, trackBounds.width, h);
      }
    }catch(BadLocationException e) {
    } catch (BadLocationException e) {
      e.printStackTrace();
    }
  }
});
}}

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

- 注:
-- 一行分のハイライトの高さは``2px``で固定
-- 検索結果の位置取得には``JTextArea#modelToView(text.indexOf(pattern, pos));``を使用しているため、ハイライト対象の文字列が折り返しで二行になっても、ハイライトされるのは一行目のみ
- `1`行分のハイライトの高さは`2px`で固定
- 検索結果の位置は`JTextComponent#modelToView(Matcher#start());`を利用しているため、ハイライト対象の文字列が折り返しで`2`行になってもハイライトされるのは開始位置のある`1`行目のみ
- 以下のような`Icon`を設定した`JLabel`を`JScrollPane#setRowHeaderView(...)`で追加する方法もある
-- 縦`JScrollBar`に直接ハイライトを描画しないので上下の増減ボタンサイズは考慮しない
-- ノブの代わりに現在表示位置を示す領域を半透明で描画

----
以下のように、``Icon``を使った``MatteBorder``を設定したコンポーネントを作成し、これを``JViewport``に追加して``JScrollPane#setRowHeader(...)``で設定する方法もあります。こちらは、``JScrollBar``に直接ハイライトを描画しないので、増減ボタンのサイズは考慮せず、またノブの代わりに現在表示位置を示す領域を半透明で描画しています。

#code{{
final JScrollPane scroll = new JScrollPane(textArea);
JLabel label = new JLabel();
label.setBorder(BorderFactory.createMatteBorder(0, 4, 0, 0, new Icon() {
  private final Color THUMB_COLOR = new Color(0,0,255,50);
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) {
    if(poslist.isEmpty()) return;
    // 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();

    Rectangle rect   = textArea.getBounds();
    Dimension sbSize = scrollbar.getSize();
    Insets sbInsets  = scrollbar.getInsets();
    double sy=(sbSize.height-sbInsets.top-sbInsets.bottom)/rect.getHeight();
    AffineTransform at = AffineTransform.getScaleInstance(1.0, sy);
    // paint Highlight
    g.setColor(Color.RED);
    try{
      for(Integer pos: poslist) {
        Rectangle r = textArea.modelToView(pos);
    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+sbInsets.top+s.y, getIconWidth(), h);
        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 rectangle
      JViewport vport = scroll.getViewport();
      Rectangle vrect = c.getBounds();
      vrect.y = vport.getViewPosition().y;
    // 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 rr = at.createTransformedShape(vrect).getBounds();
      g.fillRect(x, y+sbInsets.top+rr.y, getIconWidth(), rr.height);
    }catch(BadLocationException e) {
      e.printStackTrace();
      Rectangle s = at.createTransformedShape(thumbRect).getBounds();
      g.fillRect(x, y + itop + s.y, getIconWidth(), s.height);
    }
  }

  @Override public int getIconWidth() {
    return 4;
    return 8;
  }

  @Override public int getIconHeight() {
    return scrollbar.getHeight();
    JViewport vport = Objects.requireNonNull(
      (JViewport) SwingUtilities.getAncestorOfClass(JViewport.class, textArea));
    return vport.getHeight();
  }
}));
});

scroll.setVerticalScrollBar(scrollbar);
JViewport vp = new JViewport();
/*
// Fixed Versions: 7 (b134)
scroll.setRowHeaderView(label);
/*/
// 6826074 JScrollPane does not revalidate the component hierarchy after scrolling
// https://bugs.openjdk.org/browse/JDK-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);
add(scroll);
}}

//**参考リンク
**コメント [#jab8403b]
----
- 縦`JScrollBar`の中ではなく左横などにハイライト位置用の`Icon`を表示したい場合は、`MatteBorder`を利用する方法がある

#code{{
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);
}}

* 参考リンク [#reference]
- [[JLabelとIconで作成した検索位置表示バーをマウスで操作する>Swing/ScrollBarSearchHighlighter]]

* コメント [#comment]
#comment
- 行ヘッダーを使用したハイライトは`Java7`以降でのみ有効に機能するようです。 -- &user(読者); &new{2013-08-18 (日) 23:10:11};
-- ご指摘ありがとうございます。仰るとおり、`1.6.0_45`で行ヘッダ版が正常に動作しないことを確認しました。回避方法がないか、`Bug Database`あたりを調べてみようと思います。 -- &user(aterai); &new{2013-08-19 (月) 00:04:59};
-- 修正された時期などから、[https://bugs.openjdk.org/browse/JDK-6910490 Bug ID: JDK-6910490 MatteBorder JScrollpane interaction]が原因かもと`MatteBorder`は使用せずに直接`Icon`を`JLabel`に追加するよう変更したけど、改善しない…。 -- &user(aterai); &new{2013-08-19 (月) 11:24:23};
-- [https://bugs.openjdk.org/browse/JDK-6826074 Bug ID: JDK-6826074 JScrollPane does not revalidate the component hierarchy after scrolling]が原因(`HeavyWeight`、`LightWeight`だけではなくレイアウトがうまく更新されていない?)のようです。`JViewport#setViewPosition(...)`をオーバーライドして`revalidate()`すれば、`1.7.0`と同様の動作をするようになりました。 -- &user(aterai); &new{2013-08-19 (月) 14:43:11};
- `Highlighter.Highlight#getStartOffset()`を使用するように変更。 -- &user(aterai); &new{2013-08-23 (金) 16:14:35};

#comment