4. 多屏布局基础
在单屏 Qt Quick 应用里,开发者往往只关心一个窗口内的控件摆放;一旦进入多屏交互场景,布局问题会立刻复杂化:窗口可能横跨两块物理屏幕,鼠标坐标来自全局桌面,而控件坐标却属于某个窗口或某个 Item。如果坐标系没有理清,就会出现“控件明明放在屏幕 A,却显示在屏幕 B”“拖拽偏移量随窗口位置变化”等典型问题。本节围绕四种布局手段与跨屏坐标转换展开,帮助建立可预测的多屏布局心智模型。
一、四种布局手段的定位与适用边界
Qt Quick 提供了从“手动”到“自动”的连续谱系,理解它们的差异比记住 API 更重要。
- 绝对定位(x / y):直接给 Item 的 x、y 赋值,坐标相对于父项左上角。它最直观,适合固定尺寸的装饰元素、覆盖层或需要精确像素对齐的场景,但父项尺寸变化时不会自适应。
- 锚点布局(anchors):通过 anchors.fill、anchors.centerIn、anchors.left/right/top/bottom 等把 Item 的边缘“钉”在父项或兄弟项上。它表达的是相对关系,窗口缩放时能自动跟随,是多屏下最常用的弹性布局方式。
- 布局管理器(RowLayout / ColumnLayout / GridLayout):由 Layout 附加属性驱动,自动分配空间、处理间距与对齐,适合表单、工具栏、卡片列表等结构化区域。它内部仍依赖锚点与尺寸约束,但把“排列规则”交给引擎。
- 跨屏坐标系统转换:当窗口跨越多个屏幕,或需要把全局鼠标位置映射到某个 Item 时,必须借助 mapToItem、mapFromItem、mapToGlobal、mapFromGlobal 完成坐标系换算。
核心原则:布局决定“控件在窗口内怎么摆”,坐标转换决定“窗口与屏幕、Item 与 Item 之间怎么换算”。两者必须分开思考,再组合使用。
二、跨屏坐标系统转换的原理与流程
Qt Quick 中存在多个坐标系:屏幕全局坐标(Screen)、窗口坐标(Window)、Item 局部坐标(Item)。多屏环境下,全局坐标的原点通常位于主屏左上角,副屏可能位于其右侧、左侧或下方,因此全局坐标可能是负数。转换的本质是“找到两个坐标系之间的平移与缩放关系”。
常用转换方法如下:
// 将鼠标在窗口中的位置转换为某个 Item 的局部坐标
var localPos = someItem.mapFromItem(null, mouseArea.mouseX, mouseArea.mouseY)
// 将 Item 局部坐标转换为全局屏幕坐标
var globalPos = someItem.mapToGlobal(0, 0)
// 将全局坐标映射回另一个 Item
var targetPos = targetItem.mapFromGlobal(globalPos.x, globalPos.y)
// 获取当前屏幕信息
var screen = window.screen
console.log(screen.name, screen.geometry, screen.devicePixelRatio)
典型流程是:先通过 MouseArea 或 hover 事件拿到窗口内坐标,再用 mapToGlobal 得到全局坐标,判断该坐标落在哪个 Screen 的 geometry 内,最后用目标 Item 的 mapFromGlobal 得到局部坐标。这样无论窗口如何跨屏移动,拖拽和命中判断都能保持一致。
三、案例:跨屏拖拽一个卡片到副屏
假设主屏在左、副屏在右,窗口横跨两屏。用户从主屏窗口内拖拽一张卡片,希望卡片能准确落在副屏窗口的某个容器中。实现要点如下:
- 在 MouseArea 的 onPositionChanged 中,用 mapToGlobal 把当前鼠标位置转成全局坐标。
- 遍历 Qt.application.screens,判断全局坐标落在哪个 screen.geometry 内,得到目标屏幕。
- 用目标容器的 mapFromGlobal 把全局坐标转成容器局部坐标,作为拖拽代理的 x、y。
- 释放时用 contains() 或边界判断决定是否接受放置,并更新数据模型。
MouseArea {
anchors.fill: parent
drag.target: card
onPositionChanged: {
var g = mapToGlobal(mouseX, mouseY)
var targetScreen = null
for (var i = 0; i < Qt.application.screens.length; i++) {
var s = Qt.application.screens[i]
if (g.x >= s.geometry.x && g.x <= s.geometry.x + s.geometry.width &&
g.y >= s.geometry.y && g.y <= s.geometry.y + s.geometry.height) {
targetScreen = s
break
}
}
if (targetScreen) {
var local = dropContainer.mapFromGlobal(g.x, g.y)
card.x = local.x
card.y = local.y
}
}
}
这个案例把“布局”和“坐标转换”串了起来:卡片在容器内用锚点或布局管理器定位,而拖拽过程用全局坐标做跨屏判断,最后再映射回目标容器。缺少任何一环,跨屏拖拽都会出现偏移或落点错误。
四、常见误区与复盘清单
误区一:直接用 mouseX / mouseY 当作全局坐标。它们只是当前 Item 的局部坐标,窗口一移动就失效。
误区二:忽略 devicePixelRatio。在高 DPI 多屏下,逻辑坐标与物理像素不一致,截图或命中判断会偏移。
误区三:在布局管理器中混用 anchors 和 Layout 属性,导致尺寸约束冲突,界面抖动或警告。
误区四:认为全局坐标原点一定在 (0,0)。副屏在主屏左侧或上方时,全局坐标可能为负。
误区二:忽略 devicePixelRatio。在高 DPI 多屏下,逻辑坐标与物理像素不一致,截图或命中判断会偏移。
误区三:在布局管理器中混用 anchors 和 Layout 属性,导致尺寸约束冲突,界面抖动或警告。
误区四:认为全局坐标原点一定在 (0,0)。副屏在主屏左侧或上方时,全局坐标可能为负。
| 检查项 | 正确做法 | 常见错误 |
|---|---|---|
| 坐标来源 | 用 mapToGlobal 获取全局坐标 | 直接使用 mouseX / mouseY |
| 屏幕判断 | 遍历 Qt.application.screens 的 geometry | 硬编码主屏尺寸 |
| 布局选择 | 弹性区域用 anchors 或 Layout | 全部用绝对 x/y |
| 高 DPI | 关注 devicePixelRatio 与逻辑坐标 | 假设 1 逻辑像素等于 1 物理像素 |
| 跨屏窗口 | 用 Screen 对象动态获取当前屏幕 | 假设窗口只在一个屏幕内 |
复盘建议:在多屏调试时,先打印每个 Screen 的 name、geometry 和 devicePixelRatio,再打印关键节点的全局坐标与局部坐标。把“坐标从哪来、到哪去”画成链路,问题通常一眼可见。