QAbstractItemViewにおけるQDragを使用しないドラッグ操作のカスタム実装

前回の記事で紹介したQDragクラスを利用したドラッグ&ドロップの実装には、exec()メソッド実行中にマウスイベントがブロックされるという制限があります。これにより、ドラッグ操作中にアイテムのホバーエフェクトが無効化される問題が発生します。この制限を回避し、ドラッグ中も視覚的なフィードバックを維持するためには、QDragに依存せず、マウスイベントを直接制御する独自の実装が必要です。

本記事では、QListWidgetを継承し、マウスイベントハンドラをオーバーライドすることで、イベントルックを阻塞せずにドラッグ&ドロップ機能を実現する手法を解説します。

実装の概要

このアプローチでは、主に以下の3つのマウスイベントをオーバーライドして処理を行います。

  1. mousePressEvent: ドラッグを開始するアイテムとマウスの開始位置を記録します。
  2. mouseMoveEvent: マウスの移動に合わせて、アイテムのスナップショット(ゴーストイメージ)と挿入位置を示すインジケーターを移動させ、カーソル形状を更新します。
  3. mouseReleaseEvent: ドラッグ操作の完了時に、アイテムのデータを目標位置へ移動し、一時的なウィジェットを破棄します。

コード実装

以下に、CustomListWidgetクラスとして実装したコード例を示します。変数名や構造を変更し、可読性を向上させています。

まず、マウス押下イベントでドラッグの起点を設定します。

void CustomListWidget::mousePressEvent(QMouseEvent *event)
{
    if (event->button() == Qt::LeftButton) {
        isDragging = true;
        dragStartPosition = event->pos();
        draggedItem = itemAt(event->pos());
        
        if (draggedItem) {
            // ゴーストイメージ用のオフセットを計算
            QRect itemRect = visualItemRect(draggedItem);
            dragOffset = event->pos() - itemRect.topLeft();
        }
    }
    QListWidget::mousePressEvent(event);
}

次に、マウス移動イベントで視覚的な要素を制御します。ここでは、マウス位置に基づいて挿入インジケーターの位置とゴーストイメージを更新します。

void CustomListWidget::mouseMoveEvent(QMouseEvent *event)
{
    if (!isDragging || !draggedItem) {
        QListWidget::mouseMoveEvent(event);
        return;
    }

    // ウィジェットの初期化
    if (!ghostLabel) {
        createGhostWidget();
    }
    if (!indicatorLine) {
        createIndicatorLine();
    }

    // ホバーしているアイテムの判定
    QListWidgetItem *hoveredItem = itemAt(event->pos());
    
    // 挿入位置インジケーターの制御
    if (hoveredItem) {
        QRect hoverRect = visualItemRect(hoveredItem);
        
        // アイテムの上半分か下半分かで挿入位置を決定
        bool insertAfter = (event->pos().y() > hoverRect.center().y());
        int lineY = insertAfter ? hoverRect.bottom() : hoverRect.top();
        
        indicatorLine->resize(width(), 2);
        indicatorLine->move(mapToGlobal(QPoint(0, lineY)));
        indicatorLine->show();
    } else {
        indicatorLine->hide();
    }

    // ゴーストイメージの移動
    ghostLabel->move(event->globalPos() - dragOffset);
    
    // カーソル形状の変更
    if (hoveredItem == draggedItem) {
        setCursor(Qt::ForbiddenCursor);
    } else {
        setCursor(Qt::DragMoveCursor);
    }
}

マウス解放イベントで、実際のデータ移動処理と後片付けを行います。

void CustomListWidget::mouseReleaseEvent(QMouseEvent *event)
{
    if (event->button() == Qt::LeftButton && isDragging) {
        if (ghostLabel) {
            ghostLabel->close();
            ghostLabel->deleteLater();
            ghostLabel = nullptr;
        }
        if (indicatorLine) {
            indicatorLine->close();
            indicatorLine->deleteLater();
            indicatorLine = nullptr;
        }
        
        processDropAction(event);
        isDragging = false;
    }

    setCursor(Qt::ArrowCursor);
    QListWidget::mouseReleaseEvent(event);
}

以下は、ドラッグ中に表示するアイテムのスナップショット(ゴースト)を作成するヘルパー関数です。

void CustomListWidget::createGhostWidget()
{
    ghostLabel = new QLabel(nullptr);
    ghostLabel->setWindowFlags(Qt::FramelessWindowHint | Qt::Tool | Qt::WindowStaysOnTopHint);
    ghostLabel->setAttribute(Qt::WA_TransparentForMouseEvents, true);
    ghostLabel->setWindowOpacity(0.8);

    if (draggedItem) {
        QWidget *itemWidget = itemWidget(draggedItem);
        if (itemWidget) {
            // アイテムウィジェットのスクリーンショットを取得
            ghostLabel->setPixmap(itemWidget->grab());
            ghostLabel->resize(visualItemRect(draggedItem).size());
        }
    }
    ghostLabel->show();
}

挿入位置を示すラインを作成する関数です。

void CustomListWidget::createIndicatorLine()
{
    indicatorLine = new QLabel(nullptr);
    indicatorLine->setObjectName("DragIndicatorLine");
    indicatorLine->setWindowFlags(Qt::FramelessWindowHint | Qt::Tool | Qt::WindowStaysOnTopHint);
    indicatorLine->setAttribute(Qt::WA_TransparentForMouseEvents, true);
    indicatorLine->setStyleSheet("QLabel#DragIndicatorLine { background-color: #00FF00; }");
    indicatorLine->show();
}

最後に、ドロップ時の実際のデータ並べ替え処理を行う関数です。

void CustomListWidget::processDropAction(QMouseEvent *event)
{
    QListWidgetItem *targetItem = itemAt(event->pos());
    if (!targetItem || targetItem == draggedItem) {
        return;
    }

    int targetRow = row(targetItem);
    QWidget *targetWidget = itemWidget(targetItem);
    
    // 挿入位置の調整(下半分なら+1)
    if (targetWidget && event->pos().y() > visualItemRect(targetItem).center().y()) {
        targetRow += 1;
    }

    // 新しいアイテムを作成して挿入
    int sourceRow = row(draggedItem);
    if (sourceRow < targetRow) {
        targetRow -= 1; // 元の位置が手前にある場合の補正
    }

    // データの複製と移動ロジック(仮のクラス名 ListItem を使用)
    // 実際のアプリケーションでは、独自のデータ取得・設定メソッドを呼び出します
    QListWidgetItem *newItem = draggedItem->clone();
    takeItem(sourceRow);
    insertItem(targetRow, newItem);
    
    // ウィジェットの再設定が必要な場合(元の実装に倣う)
    // 必要に応じてここでsetItemWidgetなどを再設定してください
    
    setCurrentItem(newItem);
}

7月6日 18:01 投稿