• 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

概要

JListの表示領域に存在する行を検索して特定のデータを保持するセルをヘッダとしてJLayer上に固定して描画します。

サンプルコード

class StickyLayerUI extends LayerUI<JScrollPane> {
  private final JPanel panel = new JPanel();

  @SuppressWarnings("unchecked")
  @Override public void paint(Graphics g, JComponent c) {
    super.paint(g, c);
    if (c instanceof JLayer) {
      JScrollPane scroll = (JScrollPane) ((JLayer<?>) c).getView();
      JViewport viewport = scroll.getViewport();
      JList<String> list = (JList<String>) viewport.getView();
      int cellHeight = list.getFixedCellHeight();
      Rectangle viewRect = viewport.getViewRect();
      Point vp = SwingUtilities.convertPoint(viewport, 0, 0, c);
      Point pt1 = SwingUtilities.convertPoint(c, vp, list);
      int idx1 = list.locationToIndex(pt1);
      Rectangle header1 = new Rectangle(
          vp.x, vp.y, viewRect.width, cellHeight);
      if (idx1 >= 0) {
        Graphics2D g2 = (Graphics2D) g.create();
        int headerIndex1 = getHeaderIndex1(list, idx1);
        Component c1 = getComponent(list, headerIndex1);
        int nhi = getNextHeaderIndex1(list, idx1);
        Point nextPt = list.getCellBounds(nhi, nhi).getLocation();
        if (header1.contains(
            SwingUtilities.convertPoint(list, nextPt, c))) {
          Dimension d = header1.getSize();
          SwingUtilities.paintComponent(
              g2, c1, panel, getHeaderRect(list, idx1, c, d));
          Component cn = getComponent(list, nhi);
          SwingUtilities.paintComponent(
              g2, cn, panel, getHeaderRect(list, nhi, c, d));
        } else {
          SwingUtilities.paintComponent(g2, c1, panel, header1);
        }
        g2.dispose();
      }
    }
  }

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

  private static int getNextHeaderIndex1(JList<String> 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> renderer = list.getCellRenderer();
    Component c = renderer.getListCellRendererComponent(
        list, value, idx, false, false);
    c.setBackground(Color.GRAY);
    c.setForeground(Color.WHITE);
    return c;
  }

  // ...
}
View in GitHub: Java, Kotlin

解説

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

参考リンク

コメント