上一篇讲的拖拽只是移动UI元素,这一篇开始讲真正的拖放:拖出去的东西,另一个区域要能接收,并且能读懂拖过来的数据。这就是 DropArea 的活儿。它配合拖拽源携带的 mimeData,完成从"视觉移动"到"数据交换"的跨越。这两个 demo 一个演示单向投放,一个演示双向交换。
这篇介绍两个 demo:
- DropArea 放置区 --- 四个色块拖进底部区域,源块隐藏、目标高亮,附带校验与计数
- 对象交换 --- 九宫格任意两个色块互相拖拽即交换颜色与编号,双向数据互换
DropArea 放置区
顶部一排四个色块(红绿黄紫),可以拖到底部的虚线框中。拖拽经过时放置区变蓝高亮,松手后色块"消失"(实际是隐藏)、放置区显示已放入的颜色,底部计数。放错的类型会被拒收,点"清空放置区"可以还原所有色块。

演示代码
qml
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,色块全部还原,放置区文字也清空。这样整个交互可以反复演示。
对象交换
九宫格里九个不同颜色的色块,任意两个互相拖拽就能交换颜色和编号。拖到某个格子上时,目标格子出现深色描边提示"可落在这里"。右下角有"打乱排序"按钮随机交换二十次,顺便展示了交换逻辑的另一种用法。

演示代码
qml
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 里完成双向交换
这是整个 demo 的核心。drop.getDataAsString("text/index") 取出来源格子下标 srcIndex。如果来源就是当前格(srcIndex === index),直接拒绝,不然自己拖自己会白忙一场。真正的交换只有四行:先把来源格子的颜色和文字存进临时变量,再把当前格子的值写回来源格子,最后把临时值写进当前格子。用 setProperty 改 model,界面自动刷新。
containsDrag 当"落地预览"用
border.color: dropHere.containsDrag ? "#333333" : "transparent":拖拽悬停在某格上方时,该格出现深色粗描边,提示"松手就换到这"。和上一个 demo 的整个区域高亮不同,这里是逐格高亮,给用户精确的落点反馈。
打乱排序:复用同一套交换逻辑
"打乱排序"按钮里的循环,本质就是随机挑两个下标做同样的 setProperty 交换,随机重复二十次。这和拖拽触发的交换用的是同一份逻辑,只不过一个来自鼠标、一个来自代码。
运行验证
- Qt Creator 打开示例的 CMakeLists.txt;
- 按 Ctrl+R 运行;
- 放置区:把四个色块逐个拖进底部区域,看源块隐藏、区域变蓝、计数累加,点"清空放置区"还原;
- 对象交换:把任意一个色块拖到另一个上,观察颜色和编号互换、描边高亮,再点"打乱排序"试试随机打乱。
已验证环境:
- Qt 版本:Qt 6.11.1
- 操作系统:Windows 11
- GitHub:QML-Minimal-Demos/qml_drag_drop