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 得到局部坐标。这样无论窗口如何跨屏移动,拖拽和命中判断都能保持一致。

多屏布局:布局手段与坐标转换协作关系 屏幕 A(主屏) geometry: (0,0,1920,1080) Window(跨屏窗口) 绝对定位 x/y 锚点 anchors 布局管理器 Row/Column/Grid 屏幕 B(副屏) geometry: (1920,0,1920,1080) 目标 Item(副屏窗口内) 局部坐标 (x, y) 跨屏坐标转换链路 窗口坐标 mapToGlobal 全局坐标 mapFromGlobal 目标局部 窗口跨屏

三、案例:跨屏拖拽一个卡片到副屏

假设主屏在左、副屏在右,窗口横跨两屏。用户从主屏窗口内拖拽一张卡片,希望卡片能准确落在副屏窗口的某个容器中。实现要点如下:

  1. 在 MouseArea 的 onPositionChanged 中,用 mapToGlobal 把当前鼠标位置转成全局坐标。
  2. 遍历 Qt.application.screens,判断全局坐标落在哪个 screen.geometry 内,得到目标屏幕。
  3. 用目标容器的 mapFromGlobal 把全局坐标转成容器局部坐标,作为拖拽代理的 x、y。
  4. 释放时用 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)。副屏在主屏左侧或上方时,全局坐标可能为负。
检查项 正确做法 常见错误
坐标来源 用 mapToGlobal 获取全局坐标 直接使用 mouseX / mouseY
屏幕判断 遍历 Qt.application.screens 的 geometry 硬编码主屏尺寸
布局选择 弹性区域用 anchors 或 Layout 全部用绝对 x/y
高 DPI 关注 devicePixelRatio 与逻辑坐标 假设 1 逻辑像素等于 1 物理像素
跨屏窗口 用 Screen 对象动态获取当前屏幕 假设窗口只在一个屏幕内
复盘建议:在多屏调试时,先打印每个 Screen 的 name、geometry 和 devicePixelRatio,再打印关键节点的全局坐标与局部坐标。把“坐标从哪来、到哪去”画成链路,问题通常一眼可见。