QML 拖拽篇:接收拖拽与数据交换

上一篇讲的拖拽只是移动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 交换,随机重复二十次。这和拖拽触发的交换用的是同一份逻辑,只不过一个来自鼠标、一个来自代码。

运行验证

  1. Qt Creator 打开示例的 CMakeLists.txt;
  2. 按 Ctrl+R 运行;
  3. 放置区:把四个色块逐个拖进底部区域,看源块隐藏、区域变蓝、计数累加,点"清空放置区"还原;
  4. 对象交换:把任意一个色块拖到另一个上,观察颜色和编号互换、描边高亮,再点"打乱排序"试试随机打乱。

已验证环境

相关推荐
张人玉18 小时前
基于 Python 机器学习 与 PyQt5 桌面框架开发的可视化分析系统——基于大数据的网上购物消费行为分析可视化大屏
python·qt·机器学习·echarts·three.js
「QT(C++)开发工程师」18 小时前
C++11 std::unique_ptr — 独占所有权的智能指针
c语言·开发语言·c++·qt
小镇敲码人21 小时前
【深入浅出】之Qt 事件系统实战:鼠标、键盘、滚轮、窗口与定时器事件
数据库·qt·网络协议·mysql·http
小小龙学IT1 天前
Qt 数据库编程(Qt SQL 模块)深度解析
数据库·sql·qt
Qt历险记1 天前
C++11 std::unique_ptr — 独占所有权的智能指针
qt
宏笋1 天前
一款集结古今中外文学查阅与编辑的软件
qt
机器视觉知识推荐、就业指导1 天前
为什么老说“纯 Qt 没啥就业市场”?
开发语言·qt
telepan1 天前
Qt 开发避坑与性能指南:如何优雅、安全地定义全局常量字符串?
开发语言·qt
小灰灰搞电子2 天前
完全驾驭 Qt 与数据库:C++ ORM 框架 QxOrm 原理与实践指南
数据库·c++·qt