概要

JScrollPaneJButtonを描画するJLayerを設定し、ここでクリックイベントを取得したら先頭領域までのスクロールバックを実行します。

サンプルコード

class ScrollBackToTopLayerUI extends LayerUI<JScrollPane> {
  private static final int GAP = 5;
  private final Container rubberStamp = new JPanel();
  private final Point mousePt = new Point();
  private final JButton button = new JButton(new ScrollBackToTopIcon()) {
    @Override public void updateUI() {
      super.updateUI();
      setBorder(BorderFactory.createEmptyBorder());
      setFocusPainted(false);
      setBorderPainted(false);
      setContentAreaFilled(false);
      setRolloverEnabled(false);
    }
  };
  private final Rectangle buttonRect = new Rectangle(button.getPreferredSize());

  private void updateButtonRect(JScrollPane scroll) {
    JViewport viewport = scroll.getViewport();
    int x = viewport.getX() + viewport.getWidth() - buttonRect.width - GAP;
    int y = viewport.getY() + viewport.getHeight() - buttonRect.height - GAP;
    buttonRect.setLocation(x, y);
  }

  @Override public void paint(Graphics g, JComponent c) {
    super.paint(g, c);
    if (c instanceof JLayer) {
      JScrollPane scroll = (JScrollPane) ((JLayer<?>) c).getView();
      updateButtonRect(scroll);
      if (scroll.getViewport().getViewRect().y > 0) {
        button.getModel().setRollover(buttonRect.contains(mousePt));
        SwingUtilities.paintComponent(g, button, rubberStamp, buttonRect);
      }
    }
  }

  @Override public void installUI(JComponent c) {
    super.installUI(c);
    if (c instanceof JLayer) {
      ((JLayer<?>) c).setLayerEventMask(AWTEvent.MOUSE_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 processMouseEvent(MouseEvent e, JLayer<? extends JScrollPane> l) {
    JScrollPane scroll = l.getView();
    Rectangle r = scroll.getViewport().getViewRect();
    Point p = SwingUtilities.convertPoint(e.getComponent(), e.getPoint(), scroll);
    mousePt.setLocation(p);
    int id = e.getID();
    if (id == MouseEvent.MOUSE_CLICKED) {
      if (buttonRect.contains(mousePt)) {
        scrollBackToTop(l.getView());
      }
    } else if (id == MouseEvent.MOUSE_PRESSED && r.y > 0 && buttonRect.contains(mousePt)) {
      e.consume();
    }
  }

  @Override protected void processMouseMotionEvent(MouseEvent e, JLayer<? extends JScrollPane> l) {
    Point p = SwingUtilities.convertPoint(e.getComponent(), e.getPoint(), l.getView());
    mousePt.setLocation(p);
    l.repaint(buttonRect);
  }

  private void scrollBackToTop(JScrollPane scroll) {
    JComponent c = (JComponent) scroll.getViewport().getView();
    Rectangle current = scroll.getViewport().getViewRect();
    new Timer(20, e -> {
      Timer animator = (Timer) e.getSource();
      if (0 < current.y && animator.isRunning()) {
        current.y -= Math.max(1, current.y / 2);
        c.scrollRectToVisible(current);
      } else {
        animator.stop();
      }
    }).start();
  }
}
View in GitHub: Java, Kotlin

解説

  • ScrollBackToTopLayerUI#paint(...)
    • JScrollPane相対でその子JViewportの右下隅領域に半透明アイコンを設定したJButtonSwingUtilities.paintComponent(...)メソッドで描画
      • すでに先頭領域が表示中(JViewport#getViewRect()で取得される領域のy座標が0)の場合はJButtonを描画しない
    • 上記の右下隅領域内にマウスカーソルが存在する場合はJButton#getModel()#setRollover(true)JButtonをロールオーバー状態に変更
  • ScrollBackToTopLayerUI#processMouseEvent(...)
    • SwingUtilities.convertPoint(...)メソッドでマウスイベントをJScrollPane基準に変換
    • 変換したマウスイベントがJButtonの描画領域内(JViewportの右下隅領域)のクリックイベントの場合、JComponent#scrollRectToVisible(...)を繰り返し実行するTimerを起動して先頭領域までスクロールバック
    • JButtonが非表示(すでに先頭領域が表示中)かつ、変換したマウスイベントがJButtonの描画領域内のプレスイベントの場合、子コンポーネントにイベントが伝達しないようにMouseEvent#consume()で消費
      • ここでプレスイベントを消費しないと、JButtonのプレスだけではなくたとえばJScrollPane内のJTableのセル選択状態まで変化してしまう

参考リンク

コメント