目录
- 基础自由拖拽
- 演示代码
- 关键逻辑解析
- 单轴约束拖拽
- 演示代码
- 关键逻辑解析
- 运行验证
- 小结
- 系列文章
我们常常会有这样的需求:把窗口拖到屏幕边缘、拖动滑块的圆钮、把任务挪到另一个列表。QML 里实现拖拽的基础,就是MouseArea上那几个drag.*属性——指定拖谁、往哪个方向拖、最多拖多远。下面从最简单的自由拖拽开始,再讲怎么把拖拽限制在单一方向上。
基础自由拖拽
一个蓝色方块放在网格画布中间,按住拖动可以满画布跑,但永远出不了画布边界。松开时底部显示方块当前坐标,旁边有个"重置位置"按钮把它拉回中心。
演示代码
import QtQuick import QtQuick.Controls import QtQuick.Layouts FadeInAnimation { ColumnLayout { anchors.fill: parent anchors.margins: 20 spacing: 15 // ... 省略标题组件 TitleSeparator ... // 拖拽画布 Rectangle { id: canvas Layout.alignment: Qt.AlignHCenter width: 540 height: 300 radius: 10 color: "#FAFAFA" border.color: "#ddd" border.width: 1 clip: true // 网格背景,便于观察位置 Canvas { anchors.fill: parent onPaint: { var ctx = getContext("2d") ctx.strokeStyle = "#EEEEEE" ctx.lineWidth = 1 for (var x = 0; x <= width; x += 30) { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, height); ctx.stroke() } for (var y = 0; y <= height; y += 30) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(width, y); ctx.stroke() } } } // 可拖拽方块 Rectangle { id: dragBox width: 90 height: 90 radius: 12 color: dragArea.pressed ? "#0E7BD9" : "#1296FF" x: canvas.width / 2 - 45 y: canvas.height / 2 - 45 Behavior on color { ColorAnimation { duration: 120 } } Text { anchors.centerIn: parent text: "拖我" color: "white" font.bold: true font.pointSize: 14 } Drag.active: dragArea.drag.active Drag.dragType: Drag.None MouseArea { id: dragArea anchors.fill: parent drag.target: dragBox drag.axis: Drag.XAndYAxis drag.minimumX: 0 drag.maximumX: canvas.width - dragBox.width drag.minimumY: 0 drag.maximumY: canvas.height - dragBox.height cursorShape: Qt.SizeAllCursor onPositionChanged: statusText.text = "拖拽中…" onReleased: { statusText.text = "已释放 位置: (" + Math.round(dragBox.x) + ", " + Math.round(dragBox.y) + ")" } } } // 状态提示 Text { id: statusText anchors.bottom: parent.bottom anchors.horizontalCenter: parent.horizontalCenter anchors.bottomMargin: 10 text: "拖拽方块自由移动" font.pointSize: 12 color: "#999" } } RowLayout { Layout.alignment: Qt.AlignHCenter spacing: 20 Text { text: "当前坐标: (" + Math.round(dragBox.x) + ", " + Math.round(dragBox.y) + ")" font.pointSize: 13 color: "#1296FF" font.bold: true } Button { text: "重置位置" onClicked: { dragBox.x = canvas.width / 2 - 45 dragBox.y = canvas.height / 2 - 45 statusText.text = "拖拽方块自由移动" } } } Text { Layout.alignment: Qt.AlignHCenter Layout.preferredWidth: 540 text: "drag.axis: Drag.XAndYAxis → 仅可在画布范围内移动(minimumX/Y ~ maximumX/Y)" font.pointSize: 10 color: "#999" horizontalAlignment: Text.AlignHCenter wrapMode: Text.Wrap } Item { Layout.fillHeight: true } } }关键逻辑解析
四件套:target、axis、minimum、maximum
实现拖拽靠 MouseArea 上的一组drag属性:
drag.target指定被拖动的对象,这里是方块 dragBox。它填满了 MouseArea,所以鼠标按住方块就能拖drag.axis决定拖拽方向,Drag.XAndYAxis表示水平和垂直都能动drag.minimumX/drag.maximumX限定水平范围,minimumY/maximumY限定垂直范围。上界写canvas.width - dragBox.width,是因为方块有 90 宽,坐标超过这个值右侧就出界了
这套约束配齐后,方块天然出不了画布,不需要自己写"撞墙判断"。
按下变深色,给个反馈
color: dragArea.pressed ? "#0E7BD9" : "#1296FF"让方块在按住时变成深蓝色,配一个Behavior on color做 120 毫秒的颜色过渡。加上cursorShape: Qt.SizeAllCursor,鼠标悬停变成四向箭头,用户一眼就知道"这个能拖"。
监听状态,汇报坐标
onPositionChanged在拖拽过程中触发,把状态文字改成"拖拽中…";onReleased在松开时把方块当前坐标写到底部。坐标用Math.round(dragBox.x)取整,读起来干净。这里用的是纯物理移动,不牵扯 Drag 会话,所以Drag.dragType: Drag.None关掉系统拖拽,避免产生多余的拖拽预览。
单轴约束拖拽
一个横的、一个竖的,两条轨道各放一个圆形滑块。横的只能左右滑,竖的只能上下滑,滑块永远沿着轨道中线走,旁边实时显示滑到哪了。这正是滑杆类控件最常见的实现思路。
演示代码
import QtQuick import QtQuick.Layouts FadeInAnimation { ColumnLayout { anchors.fill: parent anchors.margins: 20 spacing: 18 // ... 省略标题组件 TitleSeparator ... // 水平拖拽轨道 Rectangle { id: hTrack Layout.alignment: Qt.AlignHCenter width: 480 height: 70 radius: 8 color: "#FAFAFA" border.color: "#ddd" border.width: 1 // 轨道中线 Rectangle { anchors.verticalCenter: parent.verticalCenter x: 12 width: parent.width - 24 height: 2 color: "#E0E0E0" } Text { x: 12 y: 8 text: "XAxis:仅水平拖拽" font.pointSize: 10 color: "#999" } Rectangle { id: hKnob width: 50 height: 50 radius: 25 color: hMouse.pressed ? "#0E7BD9" : "#1296FF" y: (hTrack.height - height) / 2 x: 12 Behavior on color { ColorAnimation { duration: 120 } } Text { anchors.centerIn: parent text: "↔" color: "white" font.pointSize: 20 font.bold: true } MouseArea { id: hMouse anchors.fill: parent drag.target: hKnob drag.axis: Drag.XAxis drag.minimumX: 12 drag.maximumX: hTrack.width - hKnob.width - 12 cursorShape: Qt.SizeHorCursor } } Text { anchors.bottom: parent.bottom anchors.horizontalCenter: parent.horizontalCenter anchors.bottomMargin: 4 text: "x = " + Math.round(hKnob.x) font.pointSize: 11 color: "#1296FF" } } // ... 省略垂直轨道 vTrack(与水平轨道对称,仅 axis 改为 Drag.YAxis、cursorShape 改为 Qt.SizeVerCursor)... Item { Layout.fillHeight: true } } }关键逻辑解析
drag.axis 决定自由度
水平滑块写drag.axis: Drag.XAxis,于是 x 可以变、y 被锁死——滑块被约束在中线上,不会乱跑。垂直滑块反过来写Drag.YAxis。改一个属性值,拖拽的自由度就从"二维"变成"一维"。
轴方向与光标匹配
水平拖拽配cursorShape: Qt.SizeHorCursor(左右箭头),垂直配Qt.SizeVerCursor(上下箭头),滑块上的 “↔” 和 “↕” 文字进一步提示方向。
最小/最大值的边界计算
水平滑块minimumX: 12、maximumX: hTrack.width - hKnob.width - 12,两边各留 12 像素边距。滑块中心对齐轨道中线的关键是y: (hTrack.height - height) / 2——把滑块垂直居中,水平拖拽就永远贴着中线走。
drag.axis 有四种取值:Drag.XAxis仅水平(音量条、进度条),Drag.YAxis仅垂直(滚动条、高度调节),Drag.XAndYAxis双轴自由(自由摆放的元素),Drag.None禁用拖拽(临时锁住某元素)。
运行验证
- Qt Creator 打开示例的 CMakeLists.txt;
- 按 Ctrl+R 运行;
- 自由拖拽:按住蓝色方块满画布拖动,观察坐标变化,点"重置位置"看方块回中心;
- 单轴约束:拖横滑块只能左右动,拖竖滑块只能上下动,观察 x / y 数值变化。
小结
拖动窗口标题栏移动整个窗口,把drag.target指向窗口根元素即可。单轴滑块可以扩展成音量条:用Math.round(hKnob.x)换算成 0-100 的值。双轴拖拽加一条Behavior on x动画,可以让元素松手后"弹"回某个区域,做卡片归位效果。用drag.filterChildren可以让父级 MouseArea 同时处理多个子元素。
系列文章
- QML 拖拽篇:自由移动与单轴约束(本篇)
- QML 拖拽篇:接收拖拽与数据交换
- QML 拖拽篇:拖拽吸附与列表排序
- QML 拖拽篇:跨列拖拽与数字拼图
已验证环境:
- Qt 版本:Qt 6.11.1
- 操作系统:Windows 11
- 项目地址:QML 示例合集V2.0 - qml_drag_drop