---
category: swing
folder: StickyHeaderList
title: JListに固定ヘッダを実装する
tags: [JList, JLayer, JScrollPane]
author: aterai
pubdate: 2024-11-25T03:44:44+09:00
description: JListの表示領域に存在する行を検索して特定のデータを保持するセルをヘッダとしてJLayer上に固定して描画します。
image: https://drive.google.com/uc?id=10jL6lZDucL5QnzyCncMGUCzWx43xm-SW
hreflang:
    href: https://java-swing-tips.blogspot.com/2024/12/implement-sticky-header-in-jlist.html
    lang: en
---
* 概要 [#summary]
`JList`の表示領域に存在する行を検索して特定のデータを保持するセルをヘッダとして`JLayer`上に固定して描画します。

#download(https://drive.google.com/uc?id=10jL6lZDucL5QnzyCncMGUCzWx43xm-SW)

* サンプルコード [#sourcecode]
#code(link){{
class StickyLayerUI extends LayerUI<JScrollPane> {
  private final JPanel renderer = new JPanel();
  private int currentHeaderIdx = -1;
  private int nextHeaderIdx = -1;

  @Override public void installUI(JComponent c) {
    super.installUI(c);
    if (c instanceof JLayer) {
      ((JLayer<?>) c).setLayerEventMask(
          AWTEvent.MOUSE_WHEEL_EVENT_MASK
          | AWTEvent.MOUSE_MOTION_EVENT_MASK);
    }
  }

  @Override public void uninstallUI(JComponent c) {
    if (c instanceof JLayer) {
      ((JLayer<?>) c).setLayerEventMask(0);
    }
    super.uninstallUI(c);
  }

  @Override protected void processMouseMotionEvent(
      MouseEvent e, JLayer<? extends JScrollPane> l) {
    super.processMouseMotionEvent(e, l);
    Component c = l.getView().getViewport().getView();
    if (e.getID() == MouseEvent.MOUSE_DRAGGED && c instanceof JList) {
      update((JList<?>) c);
    }
  }

  @Override protected void processMouseWheelEvent(
      MouseWheelEvent e, JLayer<? extends JScrollPane> l) {
    super.processMouseWheelEvent(e, l);
    Component c = l.getView().getViewport().getView();
    if (c instanceof JList) {
      update((JList<?>) c);
    }
  }

  private void update(JList<?> list) {
    int idx = list.getFirstVisibleIndex();
    if (idx >= 0) {
      currentHeaderIdx = getHeaderIndex1(list, idx);
      nextHeaderIdx = getNextHeaderIndex1(list, idx);
    } else {
      currentHeaderIdx = -1;
      nextHeaderIdx = -1;
    }
  }

  @Override public void paint(Graphics g, JComponent c) {
    super.paint(g, c);
    JList<?> list = getList(c);
    if (list != null && currentHeaderIdx >= 0) {
      JScrollPane scroll = (JScrollPane) ((JLayer<?>) c).getView();
      Rectangle headerRect = scroll.getViewport().getBounds();
      headerRect.height = list.getFixedCellHeight();
        Graphics2D g2 = (Graphics2D) g.create();
      int firstVisibleIdx = list.getFirstVisibleIndex();
      if (firstVisibleIdx + 1 == nextHeaderIdx) {
        Dimension d = headerRect.getSize();
        Component c1 = getComponent(list, currentHeaderIdx);
        Rectangle r1 = getHeaderRect(list, firstVisibleIdx, c, d);
        SwingUtilities.paintComponent(g2, c1, renderer, r1);
        Component c2 = getComponent(list, nextHeaderIdx);
        Rectangle r2 = getHeaderRect(list, nextHeaderIdx, c, d);
        SwingUtilities.paintComponent(g2, c2, renderer, r2);
        } else {
        Component c1 = getComponent(list, currentHeaderIdx);
        SwingUtilities.paintComponent(g2, c1, renderer, headerRect);
        }
        g2.dispose();
      }
    }

  private static JList<?> getList(JComponent layer) {
    JList<?> list = null;
    if (layer instanceof JLayer) {
      JScrollPane scroll = (JScrollPane) ((JLayer<?>) layer).getView();
      Component view = scroll.getViewport().getView();
      if (view instanceof JList) {
        list = (JList<?>) view;
      }
    }
    return list;
  }

  private static int getHeaderIndex1(JList<?> list, int start) {
    return list.getNextMatch("0", start, Position.Bias.Backward);
  }

  private static int getNextHeaderIndex1(JList<?> list, int start) {
    return list.getNextMatch("0", start, Position.Bias.Forward);
  }

  private static Rectangle getHeaderRect(
      JList<?> list, int i, Component dst, Dimension d) {
    Rectangle r = SwingUtilities.convertRectangle(
        list, list.getCellBounds(i, i), dst);
    r.setSize(d);
    return r;
  }

  private static <E> Component getComponent(JList<E> list, int idx) {
    E value = list.getModel().getElementAt(idx);
    ListCellRenderer<? super E> r = list.getCellRenderer();
    Component c = r.getListCellRendererComponent(
        list, value, idx, false, false);
    c.setBackground(Color.GRAY);
    c.setForeground(Color.WHITE);
    return c;
  }
}
}}

* 解説 [#explanation]
- `JList<String>`を配置した`JScrollPane`に`JLayer`を設定し、`JList<String>`のスクロールとは関係なく固定ヘッダを描画
-- このサンプルではセル文字列が空白文字ではなく`0`で開始する行を固定ヘッダとして使用するため、`JList#getNextMatch("0", startIndex, Position.Bias.Backward)`で前方検索している
-- このサンプルでは`1`段の固定ヘッダにのみ対応し、多段階の固定ヘッダには未対応
- 固定ヘッダは`LayerUI#paint(...)`をオーバーライドして以下のように描画する
-- `JViewport`原点直下に存在するセルのインデックス([https://docs.oracle.com/javase/jp/8/docs/api/javax/swing/JList.html#getFirstVisibleIndex-- JList#getFirstVisibleIndex()]で取得可能)を取得し、前方検索で固定ヘッダのインデックスを取得
-- 取得した固定ヘッダのインデックスから`ListCellRenderer#getListCellRendererComponent(...)`で固定ヘッダセル描画用の`JLabel`を取得
-- 取得した`JLabel`の背景色などを変更
-- 取得した`JLabel`の描画領域を縦スクロールバーの幅などを除く`JViewport#getViewRect()`の幅に変更して`SwingUtilities.paintComponent(...)`で描画
-- 次の固定ヘッダとなる文字列を所有する行のインデックスを後方検索で取得し、その表示領域が`JLayer`上に描画している固定ヘッダ内に含まれる位置までスクロールしている場合は現在の固定ヘッダと次の固定ヘッダの両方を`SwingUtilities.paintComponent(...)`で描画

* 参考リンク [#reference]
- [https://github.com/aterai/java-swing-tips/discussions/28 Ideas how to make a "StickyHeaderList" with a `JList` and or `JTable` · aterai/java-swing-tips · Discussion #28]
- [[JListのスクロールをセルユニット単位にするかを変更する>Swing/LockToPositionOnScroll]]

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