目录
- DropArea 放置区
- 演示代码
- 关键逻辑解析
- 对象交换
- 演示代码
- 关键逻辑解析
- 运行验证
- 小结
- 系列文章
上一篇讲的拖拽只是把东西搬来搬去,这一篇开始讲真正的拖放:拖出去的东西,另一个区域要能接收,并且能读懂拖过来的数据。这就是DropArea的活儿。它配合拖拽源携带的mimeData,完成从"视觉移动"到"数据交换"的跨越。下面两个 demo,一个演示单向投放,一个演示双向交换。
DropArea 放置区
顶部一排四个色块(红绿黄紫),可以拖到底部的虚线框中。拖拽经过时放置区变蓝高亮,松手后色块"消失"(实际是隐藏)、放置区显示已放入的颜色,底部计数。放错的类型会被拒收,点"清空放置区"可以还原所有色块。
演示代码
import QtQuick import QtQuick.Controls import QtQuick.Layouts FadeInAnimation { ColumnLayout { anchors.fill: parent anchors.margins: 20 spacing: 15 // ... 省略标题组件 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 { // 色块放入放置区后隐藏 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 驱动 Drag.active(Qt6 推荐,自动处理拖拽阈值): // 色块始终保持在原位(不物理移动),通过 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 } } } } // 放置区 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 === "" ? "将上方色块拖入此区域" : "已放入:" + 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) => { // 校验是否携带我们关心的 mimeData drag.accepted = drag.formats.indexOf("text/color") >= 0 } onDropped: (drop) => { if (drop.formats.indexOf("text/color") >= 0) { var color = drop.getDataAsString("text/color") // 隐藏对应的源色块 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: "拖拽顶部色块到上方区域,观察 containsDrag 状态变化" 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 < sourceModel.count; i++) { sourceModel.setProperty(i, "dropped", false) } dropZone.droppedColor = "" dropZone.droppedLabel = "" statusText.text = "已清空放置区,色块已还原" } } Text { text: "累计接收: " + dropZone.dropCount font.pointSize: 12 color: "#1296FF" font.bold: true } } Item { Layout.fillHeight: true } } }关键逻辑解析
拖拽源:用 DragHandler 驱动,不搬移物理位置
源色块上放一个DragHandler,它的active绑定到Drag.active。和上一篇的 MouseArea 不同,这次源块没有drag.target,位置一动不动,只是发起一个"拖拽会话",把数据通过Drag.mimeData装进会话里。Drag.dragType: Drag.Automatic让 Qt 自动画一个半透明预览图跟着鼠标走,不用手写虚影。源块自身变半透明(opacity: 0.4)来提示"正在被拖"。
mimeData:拖拽过程中携带的数据
Drag.mimeData: { "text/color": model.color, "text/label": model.label }用自定义 MIME 类型text/color装颜色值、text/label装颜色名。MIME 类型就是数据的一个标签,接收方靠它认出数据的含义。这里用text/前缀是因为 DropArea 只通过字符串交换文本类数据。
DropArea:三个信号处理完整流程
放置区里嵌一个填满自身的DropArea,交互分成三个阶段:
onEntered— 拖拽进入时先校验:drag.formats列出这次拖拽携带的所有 MIME 类型,检查有没有text/color,有才设置drag.accepted = true,否则拒绝onEntered触发的同时,containsDrag变为 true,放置区背景变蓝、边框加粗、文字变成"松手以放入"——这些反馈都绑定到dropArea.containsDrag上,不用写代码onDropped— 松手落下的地方。用drop.getDataAsString("text/color")把携带的颜色取出来,遍历sourceModel找到同颜色的那项,把它的dropped置为 true,色块因visible: !model.dropped而隐藏。最后drop.accepted = true收尾
count 与清空复位
dropZone.dropCount累计接收次数显示在底部;"清空放置区"把所有色块的dropped重置回 false,色块全部还原,放置区文字也清空。整个交互可以反复演示。
对象交换
九宫格里九个不同颜色的色块,任意两个互相拖拽就能交换颜色和编号。拖到某个格子上时,目标格子出现深色描边提示"可落在这里"。右下角有"打乱排序"按钮随机交换二十次,顺便展示了交换逻辑的另一种用法。
演示代码
import QtQuick import QtQuick.Controls import QtQuick.Layouts FadeInAnimation { id: root property int swapCount: 0 ColumnLayout { anchors.fill: parent anchors.margins: 20 spacing: 15 // ... 省略标题组件 TitleSeparator ... // 3 × 3 色块网格,可互相拖拽交换 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 驱动 Drag.active(Qt6 推荐,自动处理拖拽阈值)。 // dragType 默认 Drag.Automatic:拖拽时 Qt 自动渲染源块的半透明预览图跟随鼠标, // 无需手写虚影;源色块保持原位,DropArea 根据拖拽位置接收 onDropped 完成数据交换 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 } // 同时作为放置区 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: "累计交换次数: " + root.swapCount font.pointSize: 12 color: "#1296FF" font.bold: true } } Item { Layout.fillHeight: true } } }关键逻辑解析
每个格子既是源又是放置区
和第一个 demo 不同,这里没有独立的 DropArea 大区域——每个 delegate 里同时声明了Drag.active(源)和DropArea(目标)。拖谁都可以、放到谁身上都行,这就实现了任意两格互换。网格布局由Grid管理,交换的是 model 里的数据,格子位置本身不变。
onDropped 里完成双向交换
drop.getDataAsString("text/index")取出来源格子下标srcIndex。如果来源就是当前格(srcIndex === index),直接拒绝。真正的交换只有四行:先把来源格子的颜色和文字存进临时变量,再把当前格子的值写回来源格子,最后把临时值写进当前格子。用setProperty改 model,界面自动刷新。
containsDrag 当落地预览用
border.color: dropHere.containsDrag ? "#333333" : "transparent":拖拽悬停在某格上方时,该格出现深色粗描边,提示"松手就换到这"。和上一个 demo 的整个区域高亮不同,这里是逐格高亮,给用户精确的落点反馈。
打乱排序:复用同一套交换逻辑
"打乱排序"按钮里的循环,本质就是随机挑两个下标做同样的setProperty交换,随机重复二十次。这和拖拽触发的交换用的是同一份逻辑,只不过一个来自鼠标、一个来自代码。
运行验证
- Qt Creator 打开示例的 CMakeLists.txt;
- 按 Ctrl+R 运行;
- 放置区:把四个色块逐个拖进底部区域,看源块隐藏、区域变蓝、计数累加,点"清空放置区"还原;
- 对象交换:把任意一个色块拖到另一个上,观察颜色和编号互换、描边高亮,再点"打乱排序"试试随机打乱。
小结
mimeData 可以携带结构化信息,做文件管理器时用text/uri-list接收系统拖入的文件路径。把"源 + 目标 + 校验"封装成一个可复用组件,只暴露 data 属性和 dropped 信号。交换逻辑可推广到拖拽排序:把"交换两格"改成"把一格插到另一格前面",就是列表重排。用Drag.supportedActions配合drop.action可以区分"复制"还是"移动"两种语义。
系列文章
- QML 拖拽篇:自由移动与单轴约束
- QML 拖拽篇:接收拖拽与数据交换(本篇)
- QML 拖拽篇:拖拽吸附与列表排序
- QML 拖拽篇:跨列拖拽与数字拼图
已验证环境:
- Qt 版本:Qt 6.11.1
- 操作系统:Windows 11
- 项目地址:QML 示例合集V2.0 - qml_drag_drop