據(jù)交換)
目錄DropArea 放置區(qū)演示代碼關(guān)鍵邏輯解析對(duì)象交換演示代碼關(guān)鍵邏輯解析運(yùn)行驗(yàn)證小結(jié)系列文章上一篇講的拖拽只是把東西搬來(lái)搬去這一篇開始講真正的拖放拖出去的東西另一個(gè)區(qū)域要能接收并且能讀懂拖過(guò)來(lái)的數(shù)據(jù)。這就是DropArea的活兒。它配合拖拽源攜帶的mimeData完成從視覺移動(dòng)到數(shù)據(jù)交換的跨越。下面兩個(gè) demo一個(gè)演示單向投放一個(gè)演示雙向交換。DropArea 放置區(qū)頂部一排四個(gè)色塊紅綠黃紫可以拖到底部的虛線框中。拖拽經(jīng)過(guò)時(shí)放置區(qū)變藍(lán)高亮松手后色塊消失實(shí)際是隱藏、放置區(qū)顯示已放入的顏色底部計(jì)數(shù)。放錯(cuò)的類型會(huì)被拒收點(diǎn)清空放置區(qū)可以還原所有色塊。演示代碼import QtQuick import QtQuick.Controls import QtQuick.Layouts FadeInAnimation { ColumnLayout { anchors.fill: parent anchors.margins: 20 spacing: 15 // ... 省略標(biāo)題組件 TitleSeparator ... // 頂部可拖拽的源色塊 Row { Layout.alignment: Qt.AlignHCenter spacing: 14 Repeater { id: sourceRepeater model: ListModel { id: sourceModel ListElement { color: #FF6B6B; label: 紅; dropped: false } ListElement { color: #4CAF50; label: 綠; dropped: false } ListElement { color: #FFC107; label: 黃; dropped: false } ListElement { color: #9C27B0; label: 紫; dropped: false } } Rectangle { // 色塊放入放置區(qū)后隱藏 visible: !model.dropped width: 70 height: 70 radius: 10 color: model.color opacity: dragHandler.active ? 0.4 : 1.0 Text { anchors.centerIn: parent text: model.label color: white font.pointSize: 16 font.bold: true } // 用 DragHandler 驅(qū)動(dòng) Drag.activeQt6 推薦自動(dòng)處理拖拽閾值 // 色塊始終保持在原位不物理移動(dòng)通過(guò) Drag 會(huì)話傳遞 mimeData 給 DropArea Drag.active: dragHandler.active Drag.dragType: Drag.Automatic Drag.supportedActions: Qt.CopyAction Drag.mimeData: { text/color: model.color, text/label: model.label } DragHandler { id: dragHandler target: null acceptedButtons: Qt.LeftButton cursorShape: Qt.OpenHandCursor } } } } // 放置區(qū) Rectangle { id: dropZone Layout.alignment: Qt.AlignHCenter width: 480 height: 200 radius: 14 color: dropArea.containsDrag ? #EAF4FF : #FAFAFA border.color: dropArea.containsDrag ? #1296FF : #CCCCCC border.width: dropArea.containsDrag ? 3 : 1 Behavior on color { ColorAnimation { duration: 150 } } Behavior on border.width { NumberAnimation { duration: 120 } } Text { anchors.centerIn: parent text: dropArea.containsDrag ? 松手以放入 : (dropZone.droppedColor ? 將上方色塊拖入此區(qū)域 : 已放入 dropZone.droppedLabel) font.pointSize: 18 font.bold: true color: dropArea.containsDrag ? #1296FF : (dropZone.droppedColor || #999) } property string droppedColor: property string droppedLabel: property int dropCount: 0 DropArea { id: dropArea anchors.fill: parent onEntered: (drag) { // 校驗(yàn)是否攜帶我們關(guān)心的 mimeData drag.accepted drag.formats.indexOf(text/color) 0 } onDropped: (drop) { if (drop.formats.indexOf(text/color) 0) { var color drop.getDataAsString(text/color) // 隱藏對(duì)應(yīng)的源色塊 for (var i 0; i sourceModel.count; i) { if (sourceModel.get(i).color color) { sourceModel.setProperty(i, dropped, true) break } } dropZone.droppedColor color dropZone.droppedLabel drop.getDataAsString(text/label) dropZone.dropCount statusText.text 已接收 dropZone.dropCount 次 → dropZone.droppedLabel ( color ) drop.accepted true } else { drop.accepted false } } } } Text { id: statusText Layout.alignment: Qt.AlignHCenter Layout.preferredWidth: 480 text: 拖拽頂部色塊到上方區(qū)域觀察 containsDrag 狀態(tài)變化 font.pointSize: 11 color: #666 horizontalAlignment: Text.AlignHCenter wrapMode: Text.Wrap } RowLayout { Layout.alignment: Qt.AlignHCenter spacing: 14 Button { text: 清空放置區(qū) onClicked: { for (var i 0; i sourceModel.count; i) { sourceModel.setProperty(i, dropped, false) } dropZone.droppedColor dropZone.droppedLabel statusText.text 已清空放置區(qū)色塊已還原 } } Text { text: 累計(jì)接收: dropZone.dropCount font.pointSize: 12 color: #1296FF font.bold: true } } Item { Layout.fillHeight: true } } }關(guān)鍵邏輯解析拖拽源用 DragHandler 驅(qū)動(dòng)不搬移物理位置源色塊上放一個(gè)DragHandler它的active綁定到Drag.active。和上一篇的 MouseArea 不同這次源塊沒有drag.target位置一動(dòng)不動(dòng)只是發(fā)起一個(gè)拖拽會(huì)話把數(shù)據(jù)通過(guò)Drag.mimeData裝進(jìn)會(huì)話里。Drag.dragType: Drag.Automatic讓 Qt 自動(dòng)畫一個(gè)半透明預(yù)覽圖跟著鼠標(biāo)走不用手寫虛影。源塊自身變半透明opacity: 0.4來(lái)提示正在被拖。mimeData拖拽過(guò)程中攜帶的數(shù)據(jù)Drag.mimeData: { text/color: model.color, text/label: model.label }用自定義 MIME 類型text/color裝顏色值、text/label裝顏色名。MIME 類型就是數(shù)據(jù)的一個(gè)標(biāo)簽接收方靠它認(rèn)出數(shù)據(jù)的含義。這里用text/前綴是因?yàn)?DropArea 只通過(guò)字符串交換文本類數(shù)據(jù)。DropArea三個(gè)信號(hào)處理完整流程放置區(qū)里嵌一個(gè)填滿自身的DropArea交互分成三個(gè)階段onEntered— 拖拽進(jìn)入時(shí)先校驗(yàn)drag.formats列出這次拖拽攜帶的所有 MIME 類型檢查有沒有text/color有才設(shè)置drag.accepted true否則拒絕onEntered觸發(fā)的同時(shí)containsDrag變?yōu)?true放置區(qū)背景變藍(lán)、邊框加粗、文字變成松手以放入——這些反饋都綁定到dropArea.containsDrag上不用寫代碼onDropped— 松手落下的地方。用drop.getDataAsString(text/color)把攜帶的顏色取出來(lái)遍歷sourceModel找到同顏色的那項(xiàng)把它的dropped置為 true色塊因visible: !model.dropped而隱藏。最后drop.accepted true收尾count 與清空復(fù)位dropZone.dropCount累計(jì)接收次數(shù)顯示在底部清空放置區(qū)把所有色塊的dropped重置回 false色塊全部還原放置區(qū)文字也清空。整個(gè)交互可以反復(fù)演示。對(duì)象交換九宮格里九個(gè)不同顏色的色塊任意兩個(gè)互相拖拽就能交換顏色和編號(hào)。拖到某個(gè)格子上時(shí)目標(biāo)格子出現(xiàn)深色描邊提示可落在這里。右下角有打亂排序按鈕隨機(jī)交換二十次順便展示了交換邏輯的另一種用法。演示代碼import QtQuick import QtQuick.Controls import QtQuick.Layouts FadeInAnimation { id: root property int swapCount: 0 ColumnLayout { anchors.fill: parent anchors.margins: 20 spacing: 15 // ... 省略標(biāo)題組件 TitleSeparator ... // 3 × 3 色塊網(wǎng)格可互相拖拽交換 Grid { id: grid Layout.alignment: Qt.AlignHCenter columns: 3 spacing: 14 property int cellSize: 100 Repeater { id: colorRepeater model: ListModel { id: colorModel ListElement { c: #FF6B6B; t: 1 } ListElement { c: #4CAF50; t: 2 } ListElement { c: #FFC107; t: 3 } ListElement { c: #9C27B0; t: 4 } ListElement { c: #1296FF; t: 5 } ListElement { c: #FF9800; t: 6 } ListElement { c: #00BCD4; t: 7 } ListElement { c: #795548; t: 8 } ListElement { c: #607D8B; t: 9 } } delegate: Rectangle { id: cell width: grid.cellSize height: grid.cellSize radius: 12 color: model.c opacity: dragHandler.active ? 0.3 : 1.0 border.color: dropHere.containsDrag ? #333333 : transparent border.width: dropHere.containsDrag ? 3 : 0 Behavior on border.width { NumberAnimation { duration: 120 } } Behavior on opacity { NumberAnimation { duration: 120 } } Text { anchors.centerIn: parent text: model.t color: white font.pointSize: 24 font.bold: true } // 用 DragHandler 驅(qū)動(dòng) Drag.activeQt6 推薦自動(dòng)處理拖拽閾值。 // dragType 默認(rèn) Drag.Automatic拖拽時(shí) Qt 自動(dòng)渲染源塊的半透明預(yù)覽圖跟隨鼠標(biāo) // 無(wú)需手寫虛影源色塊保持原位DropArea 根據(jù)拖拽位置接收 onDropped 完成數(shù)據(jù)交換 Drag.active: dragHandler.active Drag.dragType: Drag.Automatic Drag.supportedActions: Qt.CopyAction Drag.mimeData: { text/index: index.toString(), text/color: model.c, text/label: model.t } DragHandler { id: dragHandler target: null acceptedButtons: Qt.LeftButton cursorShape: Qt.OpenHandCursor } // 同時(shí)作為放置區(qū) DropArea { id: dropHere anchors.fill: parent onEntered: (drag) { drag.accepted drag.formats.indexOf(text/index) 0 } onDropped: (drop) { var srcIndex parseInt(drop.getDataAsString(text/index)) if (srcIndex index) { drop.accepted false return } // 交換顏色與文字 var srcColor colorModel.get(srcIndex).c var srcText colorModel.get(srcIndex).t colorModel.setProperty(srcIndex, c, model.c) colorModel.setProperty(srcIndex, t, model.t) colorModel.setProperty(index, c, srcColor) colorModel.setProperty(index, t, srcText) statusText.text 已交換格子 (srcIndex 1) ? 格子 (index 1) root.swapCount drop.accepted true } } } } } Text { id: statusText Layout.alignment: Qt.AlignHCenter Layout.preferredWidth: 480 text: 從一個(gè)色塊拖到另一個(gè)色塊上即可交換它們的顏色與編號(hào) font.pointSize: 11 color: #666 horizontalAlignment: Text.AlignHCenter wrapMode: Text.Wrap } RowLayout { Layout.alignment: Qt.AlignHCenter spacing: 14 Button { text: 打亂排序 onClicked: { for (var i 0; i 20; i) { var a Math.floor(Math.random() * colorModel.count) var b Math.floor(Math.random() * colorModel.count) var ca colorModel.get(a).c, ta colorModel.get(a).t colorModel.setProperty(a, c, colorModel.get(b).c) colorModel.setProperty(a, t, colorModel.get(b).t) colorModel.setProperty(b, c, ca) colorModel.setProperty(b, t, ta) } statusText.text 已隨機(jī)打亂色塊 } } Text { text: 累計(jì)交換次數(shù): root.swapCount font.pointSize: 12 color: #1296FF font.bold: true } } Item { Layout.fillHeight: true } } }關(guān)鍵邏輯解析每個(gè)格子既是源又是放置區(qū)和第一個(gè) demo 不同這里沒有獨(dú)立的 DropArea 大區(qū)域——每個(gè) delegate 里同時(shí)聲明了Drag.active源和DropArea目標(biāo)。拖誰(shuí)都可以、放到誰(shuí)身上都行這就實(shí)現(xiàn)了任意兩格互換。網(wǎng)格布局由Grid管理交換的是 model 里的數(shù)據(jù)格子位置本身不變。onDropped 里完成雙向交換drop.getDataAsString(text/index)取出來(lái)源格子下標(biāo)srcIndex。如果來(lái)源就是當(dāng)前格srcIndex index直接拒絕。真正的交換只有四行先把來(lái)源格子的顏色和文字存進(jìn)臨時(shí)變量再把當(dāng)前格子的值寫回來(lái)源格子最后把臨時(shí)值寫進(jìn)當(dāng)前格子。用setProperty改 model界面自動(dòng)刷新。containsDrag 當(dāng)落地預(yù)覽用border.color: dropHere.containsDrag ? #333333 : transparent拖拽懸停在某格上方時(shí)該格出現(xiàn)深色粗描邊提示松手就換到這。和上一個(gè) demo 的整個(gè)區(qū)域高亮不同這里是逐格高亮給用戶精確的落點(diǎn)反饋。打亂排序復(fù)用同一套交換邏輯打亂排序按鈕里的循環(huán)本質(zhì)就是隨機(jī)挑兩個(gè)下標(biāo)做同樣的setProperty交換隨機(jī)重復(fù)二十次。這和拖拽觸發(fā)的交換用的是同一份邏輯只不過(guò)一個(gè)來(lái)自鼠標(biāo)、一個(gè)來(lái)自代碼。運(yùn)行驗(yàn)證Qt Creator 打開示例的 CMakeLists.txt按 CtrlR 運(yùn)行放置區(qū)把四個(gè)色塊逐個(gè)拖進(jìn)底部區(qū)域看源塊隱藏、區(qū)域變藍(lán)、計(jì)數(shù)累加點(diǎn)清空放置區(qū)還原對(duì)象交換把任意一個(gè)色塊拖到另一個(gè)上觀察顏色和編號(hào)互換、描邊高亮再點(diǎn)打亂排序試試隨機(jī)打亂。小結(jié)mimeData 可以攜帶結(jié)構(gòu)化信息做文件管理器時(shí)用text/uri-list接收系統(tǒng)拖入的文件路徑。把源 目標(biāo) 校驗(yàn)封裝成一個(gè)可復(fù)用組件只暴露 data 屬性和 dropped 信號(hào)。交換邏輯可推廣到拖拽排序把交換兩格改成把一格插到另一格前面就是列表重排。用Drag.supportedActions配合drop.action可以區(qū)分復(fù)制還是移動(dòng)兩種語(yǔ)義。系列文章QML 拖拽篇自由移動(dòng)與單軸約束QML 拖拽篇接收拖拽與數(shù)據(jù)交換本篇QML 拖拽篇拖拽吸附與列表排序QML 拖拽篇跨列拖拽與數(shù)字拼圖已驗(yàn)證環(huán)境Qt 版本Qt 6.11.1操作系統(tǒng)Windows 11項(xiàng)目地址QML 示例合集V2.0 - qml_drag_drop