Qtに関する覚え書き[ドラッグ&ドロップ]

ウィジェット間のドラッグ

リストウィジェット間のドラッグ

QAbstractItemViewを継承しているリストウィジェットなどは、 簡単にドラッグ&ドロップができます。

リストウィジェットを2つ作って、その間でドラッグ&ドロップをテストします。
※このウィジェットを作っただけでは表示されません。適当な所に配置する必要があります。
※ドラッグドロップモードを設定するだけで、ドラッグ&ドロップができるようになります。
※acceptDropsをfalseにするとドロップできなくなります。ドラッグはできます。
※最後の2行はデフォルトなので省略できます。

■モード定義[enum QAbstractItemView::DragDropMode]
定義説明
NoDragDrop0ドラッグとドロップを受け付けない
DragOnly1ドラッグだけ受け付ける
DropOnly2ドロップだけ受け付ける
DragDrop3ドラッグとドロップの両方を受け付ける
InternalMove4 該当ウィジェット内の移動だけ受け付ける(コピーはしない)

list1の「五月晴れ」をlist2にドラッグすると、list2にも表示されます。
list1の「秋深し」をシフトキーを押しながらドラッグすると、list2に移動します。
dragDropOverwriteModeをtrueにすると、移動した後に元の位置が空いた状態になります。
コピー先や移動先は横線表示が出るので、好きな位置にドロップできます。


ドラッグ&ドロップについて

ドラッグ&ドロップの基本

通常のドラッグ&ドロップは3つのイベント関数で行います。

1. マウスのボタンを押した時(mousePressEvent)、mimeDataのセットとドラッグ開始
2. ドラッグを開始した時(dragEnterEvent)、mimeDataの種類を調べて受け付けを許可
3. ドロップした時(dropEvent)、mimeDataの種類別処理

QAbstractItemViewを継承しているウィジェットなどはドラッグできません。
ドロップはできるので、mousePressEventの優先順位の低さが原因だと思います。
※イベント処理については「イベント全般」参照。

例えば、”テキスト文字列”をドラッグ&ドロップで渡す場合は次のようになります。

ヘッダーで3つの関数を宣言します。

コンストラクタで、すべてのウィジェットに対してドロップを許可します。
※これは1回だけ行えばよいので、どこで指定しても構いません。

ドロップを受け付けたくないウィジェットは、個別にAcceptDropsをオフにしておきます。

widget->setAcceptDrops(false);

マウスの左ボタンを押した時、mimeDataに「テキスト文字列」をセットしてドラッグを開始します。
※ドロップアクションを省略するとMoveActionになります。
※execを実行するとドロップするまで制御が戻りません。

■ドロップアクション[enum Qt::DropAction]
定義説明
CopyAction0x01コピー
MoveAction0x02移動
LinkAction0x04リンク作成
ActionMask0xff
IgnoreAction0x00何もしない
TargetMoveAction0x8002所有権を考慮(Windowsのみ)

mimeDataがテキストの時だけ受け付けます。

mimeDataがテキストの時だけドロップ処理を行います。
※”テキスト文字列”をデバッグ出力します。

■MIMEタイプのチェック
テスターMIMEタイプ取得設定
hasText()text/plain QString text()setText()
hasHtml()text/html QString html()setHtml()
hasUrls()text/uri-list QList urls()setUrls()
hasImage()image/* QVariant imageData()setImageData()
hasColor()application/x-color QVariant colorData()setColorData()
※uri-listはurl-listの誤りかと思いましたが、QMimeDataクラスの説明はuriになっています。

画像の形式まで指定してチェックする場合はhasFormatを使います。

hasFormat("image/png")

上の表にはないタイプもhasFormatで調べられます。例えば圧縮データの場合です。

hasFormat("application/zip") // 圧縮データか?
QByteArray data("application/zip") // データ取得
setData("application/zip", zipファイルの内容) // データのセット

MIMEデータの受け渡し例

複数ファイル名

外部のエクスプローラーウインドウから、複数のファイル名を受け取ります。
※外部からドラッグしてくるのでmousePressEventは不要です。

ファイル名はURL形式で渡されるので、toLocalFileでローカルファイル名に変換しています。

"file:///C:/Temp/test.txt" → "C:/Temp/test.txt"

先頭のファイル名だけ取り出す場合はfirstを使います。

QString str = e->mimeData()->urls().first().toLocalFile();

ファイル名はテキストなので、hasUrlsの代わりにhasTextで調べることができます。
ただし複数ファイルをtext()で受け取ると、改行コードでつながった文字列になります。

"file:///C:/Temp/dt1.txt\nfile:///C:/Temp/dt2.txt"

色データ

QColorデータをドラッグします。
※mousePressEventでMIMEデータに半透明の赤(255,0,0,128)をセットします。
※テスターや取得の関数名が違うだけで、テキストの場合とほとんど同じです。
※色データを取り出す時はキャストする必要があります。

画像データ

QImageデータをドラッグします。
※mousePressEventでMIMEデータにリソースの画像をセットします。
※テスターや取得の関数名が違うだけで、テキストの場合とほとんど同じです。
※画像データを取り出す時はキャストする必要があります。
※取り出した画像はPixmapに変換してメッセージボックスに表示しています。

ウィジェット

MIMEデータのImageDataとColorDataはQVariantです。
ウィジェットをQObjectにキャストすれば、そこに押し込むことができます。

QLabelを作ってColorDataとしてMIMEにセットします。
※mousePressEventでMIMEデータにlabelをセットします。
※取り出したlabelのテキストをデバッグ出力しています。
※MIMEタイプの想定データからは完全に外れていますが、ちょっと使えるかもしれません。

ウィジェットのドラッグ

ラベルの移動

ウィジェットはQDataStreamクラスを使えばコピーできるらしいです。
でも難しそうなので移動だけにしておきます。

テスト用に2つのウィジェットを作り、片方にラベルを置きます。
※このウィジェットを作っただけでは表示されません。 widget1とwidget2を適当な所に配置する必要があります。

QLabelのpos()とx()とy()は親ウィジェットの左上(緑)が原点です。
QMouseEventとQDropEventのpos()とx()とy()はクライアント領域の左上(赤)が原点です。
QMouseEventのglobalPos()とglobalX()とglobalY()は画面の左上(青)が原点です。


※QDropEventにはglobalPos()とglobalX()とglobalY()はありません。

マウスボタンを押した位置のオブジェクトが"MoveWidget"の時だけドラッグを開始します。
※マウスボタンを押した位置からラベルの左上(黄色)までの距離を differenceプロパティに保存します。
※ドラッグの引数にラベルを指定します。これはQDropEventのsource()に渡ります。

MIMEデータをセットしないでドラッグを開始するとメッセージが出ます。

QDrag: No mimedata set before starting the drag

※ダミーのMIMEデータをセットすることで、これを回避しています。

一応何でも受け付けますが、クラスやオブジェクト名で絞ったほうが安全かもしれません。

移動元が"MoveWidget"で、移動先が"widget1"か"widget2"の時だけドロップ処理をします。
※QDropEventはglobalPos()がないので、pos()をグローバル座標に変換しています。
※移動元(MoveWidget)と移動先が同じ場合は移動元の親を移動先にします。
※移動先(widget1/widget2)の左上のグローバル座標を求めます。
※移動元のdifferenceプロパティ(差分)を取り出してラベルの新しい位置を計算します。
※移動元の親を移動先に変更し、位置を調整して表示します。
※setParentで自分自身を親にすると暴走します。上の例では srcWidとtgtWidが同じ場合です。

ドロップの処理対象をオブジェクト名で制限していますが、 例えば次のようにクラス単位にすることもできます。

if (e->source()->inherits("QLabel")) {

行編集ウィジェットの移動

ラベルの代わりに行編集(QLineEdit)をwidget1に置いてテストします。
※行編集のマウスイベントはmousePressEventでは取れないので eventFilterで処理します。
※行編集にイベントフィルターをインストールすれば eventFilter関数のobjectに行編集が渡ります。

ヘッダーにイベントフィルターの宣言を入れます。

イベントフィルターにマウスのボタンを押した時の処理を記述します。
※やっていることは前述のmousePressEventと同じです。
※戻り値をtrueにするとイベントを破棄します。
※dragEnterEvent関数とdropEvent関数の内容はラベルの時と同じです。

ドラッグ開始の猶予

マウスのボタンを押した時に、すぐドラッグに入りたくない場合があります。

ボタンを押した時に座標を保存して、一定の距離を移動してからドラッグを開始します。
※マウスのボタンを押した時、startGposに座標を保存します。
※マウスが移動した時、保存したstartGposからの距離をチェックしてドラッグを開始します。
※マウスのボタンを離した時、startGposをクリアします。
※マウスのボタンを押した時の処理をマウスが移動した時に行うだけで、やっていることは同じです。
※dragEnterEvent関数とdropEvent関数の内容はラベルの時と同じです。
※MouseMoveイベントは発生頻度が高いので、あまり余計なことはしないほうがいいかもしれません。

リストウィジェットの移動

行編集の代わりにリスト(QListWidget)をwidget1に置いてテストします。
※リスト(list)の場合は子要素(viewport)にイベントフィルターをインストールします。
※リストにインストールしてもイベントを取得できません。

objectはリストの子要素なので、objectの親がリストになります。
※リスト以外でマウスのボタンを押した時に暴走したので、親の存在チェックを入れています。

ドロップイベントは座標のオブジェクトを調べる所だけ行編集と違います。
※e->source()にはdragの引数(リスト)が入っています。
※widgetAtで座標からオブジェクトを取得すると、リストの場合は子要素(viewport)を渡されます。

その他

MIMEデータの処遇

MIMEデータを外に持ち出したくない場合は自分のウインドウを出た時に消します。
※自分のウインドウに戻って来ても、MIMEデータがクリアされているので何もしなくなります。
※ドラッグの中止はQDragクラスのcancel関数(Ver5.6以降?)を使うらしいです。

ドラッグ画像

ドラッグ中のカーソルに画像を指定することができます。

指定した画像の上にドラッグ&ドロップの矢印が乗るような形になります。
execでCopyActionを指定すると矢印の右下にプラスのマークが付きます。
ホットスポットは矢印の先端の座標です。ここでは画像の中央より少し左上にしています。
ホットスポットに画像の右下を指定すると矢印の左上に画像が表示されます。

クリックとドラッグ

クリックとドラッグの両方の処理がある場合は、少し移動した時にドラッグを開始します。

ヘッダーにmouseMoveEventとmouseReleaseEventの宣言を追加します。
※startPosはマウスのボタンが押された座標を保存する変数です。

startPosに初期値をセットします。
※これらは1回だけ行えばよいので、どこで指定しても構いません。

イベント処理です。
※マウスのボタンを押した時に座標を保存します。
※マウスが一定の距離を移動した時にドラッグを開始します。
※マウスのボタンを離した時に保存した座標をクリアします。

移動距離の初期値は10pxですが、変更することができます。

QApplication::setStartDragDistance(10);

※ウィジェットによってはmouse~Eventでイベントが取れないことがあります。

2016年12月