概要

JTableHeaderの列順序変更ドラッグが開始可能な領域をTableColumnの上半分に限定し、マウスカーソルの変更とドラッグハンドルアイコンの描画をJLayer上で実行します。

サンプルコード

class ColumnDragLayerUI extends LayerUI<JScrollPane> {
  private final Rectangle draggableRect = new Rectangle();

  @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 public void paint(Graphics g, JComponent c) {
    super.paint(g, c);
    if (!draggableRect.isEmpty()) {
      Graphics2D g2 = (Graphics2D) g.create();
      // g2.fill(draggableRect);
      Icon icon = new DragAreaIcon();
      int x = (int) (draggableRect.getCenterX() - icon.getIconWidth() / 2d);
      int y = draggableRect.y + 1;
      icon.paintIcon(c, g2, x, y);
      g2.dispose();
    }
  }

  @Override protected void processMouseEvent(
      MouseEvent e, JLayer<? extends JScrollPane> l) {
    super.processMouseEvent(e, l);
    Component c = e.getComponent();
    if (c instanceof JTableHeader) {
      JTableHeader header = (JTableHeader) c;
      if (e.getID() == MouseEvent.MOUSE_PRESSED) {
        Point pt = e.getPoint();
        updateIconAndCursor(header, pt, l);
      } else if (e.getID() == MouseEvent.MOUSE_RELEASED) {
        header.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
        draggableRect.setSize(0, 0);
      }
    }
  }

  @Override protected void processMouseMotionEvent(
      MouseEvent e, JLayer<? extends JScrollPane> l) {
    Component c = e.getComponent();
    if (c instanceof JTableHeader) {
      JTableHeader header = (JTableHeader) c;
      if (e.getID() == MouseEvent.MOUSE_DRAGGED) {
        TableColumn draggedColumn = header.getDraggedColumn();
        if (!draggableRect.isEmpty() && draggedColumn != null) {
          EventQueue.invokeLater(() -> {
            int modelIndex = draggedColumn.getModelIndex();
            int viewIndex = header.getTable().convertColumnIndexToView(modelIndex);
            Rectangle rect = header.getHeaderRect(viewIndex);
            rect.x += header.getDraggedDistance();
            draggableRect.setRect(SwingUtilities.convertRectangle(header, rect, l));
            header.repaint(rect);
          });
        } else {
          e.consume(); // Refuse to start drag
        }
      } else if (e.getID() == MouseEvent.MOUSE_MOVED) {
        Point pt = e.getPoint();
        updateIconAndCursor(header, pt, l);
        header.repaint();
      }
    } else {
      c.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
      draggableRect.setSize(0, 0);
    }
  }

  private void updateIconAndCursor(JTableHeader header, Point pt, JLayer<?> l) {
    Rectangle r = header.getHeaderRect(header.columnAtPoint(pt));
    r.height /= 2;
    if (r.contains(pt)) {
      header.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
      draggableRect.setRect(SwingUtilities.convertRectangle(header, r, l));
    } else {
      header.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
      draggableRect.setSize(0, 0);
    }
  }
}
View in GitHub: Java, Kotlin

解説

上記のサンプルでは、JTableHeaderに直接ではなく親JScrollPaneJLayerを設定してマウスカーソルの変更、ドラッグハンドルアイコンの描画、ドラッグ開始可能領域draggableRectかの判断を実行しています。

  • LayerUI#processMouseEvent(...)をオーバーライド
    • マウスクリックしたポイントがJTableHeader#getHeaderRect(JTableHeader#columnAtPoint(Point))で取得した領域の上半分に含まれる場合マウスカーソルをCursor.HAND_CURSORに変更してその領域をdraggableRectに記憶、下半分の場合はCursor.DEFAULT_CURSORに戻してdraggableRectを空にリセット
    • マウスリリースの場合は下半分の場合と同じくCursor.DEFAULT_CURSORに戻してdraggableRectを空にリセット
  • LayerUI#processMouseEvent(...)をオーバーライド
    • マウスドラッグしたときJTableHeader#getDraggedColumn()で取得したTableColumnnullではない、かつマウスクリック、マウス移動などで記憶したdraggableRectが空ではない場合、移動中のTableColumnの位置をJTableHeader#getDraggedDistance()で取得してdraggableRectを更新
      • 移動中のTableColumnのインデックスはJTableHeader#columnAtPoint(Point)で取得すると列順序の入れ替えが発生する瞬間に領域が飛んでしまう場合があるため、TableColumn#getModelIndex()で取得したモデルインデックスをJTable#convertColumnIndexToView(modelIndex)でビューインデックスに変換して使用する必要がある
  • LayerUI#paint(...)をオーバーライド
    • draggableRectが空ではない場合、その領域の中央にドラッグハンドルアイコンを描画
    • TableCellRendererではなくJLayerでドラッグハンドルアイコンを描画するため、JLabel#setIcon(...)を使用するソートアイコンとは競合しないがLookAndFeel依存の描画位置は重なる場合がある

参考リンク

コメント