據(jù)交換)
目錄DropArea 放置區(qū)演示代碼關(guān)鍵邏輯解析對象交換演示代碼關(guān)鍵邏輯解析運行驗證小結(jié)系列文章上一篇講的拖拽只是把東西搬來搬去這一篇開始講真正的拖放拖出去的東西另一個區(qū)域要能接收并且能讀懂拖過來的數(shù)據(jù)。這就是DropArea的活兒。它配合拖拽源攜帶的mimeData完成從視覺移動到數(shù)據(jù)交換的跨越。下面兩個 demo一個演示單向投放一個演示雙向交換。DropArea 放置區(qū)頂部一排四個色塊紅綠黃紫可以拖到底部的虛線框中。拖拽經(jīng)過時放置區(qū)變藍(lán)高亮松手后色塊消失實際是隱藏、放置區(qū)顯示已放入的顏色底部計數(shù)。放錯的類型會被拒收點清空放置區(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ū)動 Drag.activeQt6 推薦自動處理拖拽閾值 // 色塊始終保持在原位不物理移動通過 Drag 會話傳遞 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) { // 校驗是否攜帶我們關(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) // 隱藏對應(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: 累計接收: dropZone.dropCount font.pointSize: 12 color: #1296FF font.bold: true } } Item { Layout.fillHeight: true } } }關(guān)鍵邏輯解析拖拽源用 DragHandler 驅(qū)動不搬移物理位置源色塊上放一個DragHandler它的active綁定到Drag.active。和上一篇的 MouseArea 不同這次源塊沒有drag.target位置一動不動只是發(fā)起一個拖拽會話把數(shù)據(jù)通過Drag.mimeData裝進(jìn)會話里。Drag.dragType: Drag.Automatic讓 Qt 自動畫一個半透明預(yù)覽圖跟著鼠標(biāo)走不用手寫虛影。源塊自身變半透明opacity: 0.4來提示正在被拖。mimeData拖拽過程中攜帶的數(shù)據(jù)Drag.mimeData: { text/color: model.color, text/label: model.label }用自定義 MIME 類型text/color裝顏色值、text/label裝顏色名。MIME 類型就是數(shù)據(jù)的一個標(biāo)簽接收方靠它認(rèn)出數(shù)據(jù)的含義。這里用text/前綴是因為 DropArea 只通過字符串交換文本類數(shù)據(jù)。DropArea三個信號處理完整流程放置區(qū)里嵌一個填滿自身的DropArea交互分成三個階段onEntered— 拖拽進(jìn)入時先校驗drag.formats列出這次拖拽攜帶的所有 MIME 類型檢查有沒有text/color有才設(shè)置drag.accepted true否則拒絕onEntered觸發(fā)的同時containsDrag變?yōu)?true放置區(qū)背景變藍(lán)、邊框加粗、文字變成松手以放入——這些反饋都綁定到dropArea.containsDrag上不用寫代碼onDropped— 松手落下的地方。用drop.getDataAsString(text/color)把攜帶的顏色取出來遍歷sourceModel找到同顏色的那項把它的dropped置為 true色塊因visible: !model.dropped而隱藏。最后drop.accepted true收尾count 與清空復(fù)位dropZone.dropCount累計接收次數(shù)顯示在底部清空放置區(qū)把所有色塊的dropped重置回 false色塊全部還原放置區(qū)文字也清空。整個交互可以反復(fù)演示。對象交換九宮格里九個不同顏色的色塊任意兩個互相拖拽就能交換顏色和編號。拖到某個格子上時目標(biāo)格子出現(xiàn)深色描邊提示可落在這里。右下角有打亂排序按鈕隨機交換二十次順便展示了交換邏輯的另一種用法。演示代碼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ū)動 Drag.activeQt6 推薦自動處理拖拽閾值。 // dragType 默認(rèn) Drag.Automatic拖拽時 Qt 自動渲染源塊的半透明預(yù)覽圖跟隨鼠標(biāo) // 無需手寫虛影源色塊保持原位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 } // 同時作為放置區(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: 從一個色塊拖到另一個色塊上即可交換它們的顏色與編號 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 已隨機打亂色塊 } } Text { text: 累計交換次數(shù): root.swapCount font.pointSize: 12 color: #1296FF font.bold: true } } Item { Layout.fillHeight: true } } }關(guān)鍵邏輯解析每個格子既是源又是放置區(qū)和第一個 demo 不同這里沒有獨立的 DropArea 大區(qū)域——每個 delegate 里同時聲明了Drag.active源和DropArea目標(biāo)。拖誰都可以、放到誰身上都行這就實現(xiàn)了任意兩格互換。網(wǎng)格布局由Grid管理交換的是 model 里的數(shù)據(jù)格子位置本身不變。onDropped 里完成雙向交換drop.getDataAsString(text/index)取出來源格子下標(biāo)srcIndex。如果來源就是當(dāng)前格srcIndex index直接拒絕。真正的交換只有四行先把來源格子的顏色和文字存進(jìn)臨時變量再把當(dāng)前格子的值寫回來源格子最后把臨時值寫進(jìn)當(dāng)前格子。用setProperty改 model界面自動刷新。containsDrag 當(dāng)落地預(yù)覽用border.color: dropHere.containsDrag ? #333333 : transparent拖拽懸停在某格上方時該格出現(xiàn)深色粗描邊提示松手就換到這。和上一個 demo 的整個區(qū)域高亮不同這里是逐格高亮給用戶精確的落點反饋。打亂排序復(fù)用同一套交換邏輯打亂排序按鈕里的循環(huán)本質(zhì)就是隨機挑兩個下標(biāo)做同樣的setProperty交換隨機重復(fù)二十次。這和拖拽觸發(fā)的交換用的是同一份邏輯只不過一個來自鼠標(biāo)、一個來自代碼。運行驗證Qt Creator 打開示例的 CMakeLists.txt按 CtrlR 運行放置區(qū)把四個色塊逐個拖進(jìn)底部區(qū)域看源塊隱藏、區(qū)域變藍(lán)、計數(shù)累加點清空放置區(qū)還原對象交換把任意一個色塊拖到另一個上觀察顏色和編號互換、描邊高亮再點打亂排序試試隨機打亂。小結(jié)mimeData 可以攜帶結(jié)構(gòu)化信息做文件管理器時用text/uri-list接收系統(tǒng)拖入的文件路徑。把源 目標(biāo) 校驗封裝成一個可復(fù)用組件只暴露 data 屬性和 dropped 信號。交換邏輯可推廣到拖拽排序把交換兩格改成把一格插到另一格前面就是列表重排。用Drag.supportedActions配合drop.action可以區(qū)分復(fù)制還是移動兩種語義。系列文章QML 拖拽篇自由移動與單軸約束QML 拖拽篇接收拖拽與數(shù)據(jù)交換本篇QML 拖拽篇拖拽吸附與列表排序QML 拖拽篇跨列拖拽與數(shù)字拼圖已驗證環(huán)境Qt 版本Qt 6.11.1操作系統(tǒng)Windows 11項目地址QML 示例合集V2.0 - qml_drag_drop