HarmonyOS 7 API 26 外接屏多窗口:windowRect、drawableRect、globalDisplayRect 到底该用哪个
HarmonyOS 7 API 26 外接屏多窗口:windowRect、drawableRect、globalDisplayRect 到底该用哪个
同一个窗口在笔记本屏幕里正常,拖到 150% 缩放的外接屏后,右键菜单向右偏了一截;再把窗口缩到最小宽度,菜单甚至跑到可视区域外。很多排查停在“重新获取屏幕宽度”,但真正的问题是三套矩形被混成了一套:屏幕内窗口坐标、窗口内可绘制坐标、跨显示器全局坐标。
HarmonyOS 7 / API 26 的 WindowInfo 增加了 displayId、globalRect 等信息,窗口能力也提供 windowRect、drawableRect、globalDisplayRect 和矩形变化监听。本文不讨论页面断点布局,而是专门把“一个锚点弹窗如何跨屏不漂移”算清楚。

三个 Rect 分别回答什么问题
| 属性 | 原点 | 适合回答的问题 |
| windowRect | 当前显示屏左上角 | 窗口在当前屏幕的哪里、宽高多少 |
| drawableRect | 当前窗口左上角 | 业务内容真正能画多大、标题栏或系统区域占了多少 |
| globalDisplayRect | 全局显示空间原点 | 窗口跨多块屏幕后的绝对位置 |
如果组件给出的锚点是窗口内部坐标,就先加 drawableRect 的偏移;如果要和另一个屏幕或系统级位置比较,再加 globalDisplayRect。直接把局部 x/y 与外接屏宽高比较,数值没有共同坐标原点,结果偶尔正确只是巧合。
复现:两块屏幕、两种密度
主屏 1920x1080、密度 1.0;外接屏 2560x1440、密度 1.5。窗口从主屏 x=1200 拖到外接屏,内部按钮位置为 900vp。如果代码把 900vp 当作 900px,再叠加旧 windowRect.left,弹窗会同时受到密度和旧屏原点的双重误差。
排查日志至少打印以下字段:
displayId=2 density=1.5
windowRect=[320,160,1280,900]
drawableRect=[0,48,1280,852]
globalDisplayRect=[2240,160,1280,900]
anchorLocalVp=[900,64]
先建立不可混用的类型
ArkTS 最终都是 number,最容易把 vp、px 和 global px 互相传错。即使不用复杂类型系统,也应在命名和函数入口上锁死单位。
interface PointVp { xVp: number; yVp: number; }
interface PointPx { xPx: number; yPx: number; }
interface RectPx { leftPx: number; topPx: number; widthPx: number; heightPx: number; }
function vpPointToPx(point: PointVp, density: number): PointPx {
if (!Number.isFinite(density) || density <= 0) throw new Error('invalid density');
return { xPx: point.xVp * density, yPx: point.yVp * density };
}
把局部锚点换到全局空间
interface WindowGeometry {
drawable: RectPx;
globalWindow: RectPx;
density: number;
}
export function localAnchorToGlobal(anchor: PointVp, geometry: WindowGeometry): PointPx {
const localPx = vpPointToPx(anchor, geometry.density);
return {
xPx: geometry.globalWindow.leftPx + geometry.drawable.leftPx + localPx.xPx,
yPx: geometry.globalWindow.topPx + geometry.drawable.topPx + localPx.yPx
};
}
export function clampPopup(
anchor: PointPx,
popup: { widthPx: number; heightPx: number },
safe: RectPx
): RectPx {
const maxLeft = safe.leftPx + safe.widthPx - popup.widthPx;
const maxTop = safe.topPx + safe.heightPx - popup.heightPx;
return {
leftPx: Math.max(safe.leftPx, Math.min(anchor.xPx, maxLeft)),
topPx: Math.max(safe.topPx, Math.min(anchor.yPx, maxTop)),
widthPx: popup.widthPx,
heightPx: popup.heightPx
};
}
clamp 必须在统一的全局 px 空间执行。先在 vp 中钳制、最后再乘密度,会让 1.0 和 1.5 屏幕得到不同的物理边界误差。
案例一:右键菜单跟随锚点跨屏
窗口拖动过程中,globalDisplayRect 会变,按钮的窗口内坐标通常不变。处理顺序是:读取当前 displayId 与密度,计算全局锚点,再根据当前显示屏安全区钳制菜单。
function placeContextMenu(
anchor: PointVp,
geometry: WindowGeometry,
displaySafeRect: RectPx
): RectPx {
const global = localAnchorToGlobal(anchor, geometry);
return clampPopup(global, { widthPx: 360, heightPx: 280 }, displaySafeRect);
}
const rect = placeContextMenu(
{ xVp: 900, yVp: 64 },
{
density: 1.5,
drawable: { leftPx: 0, topPx: 72, widthPx: 1280, heightPx: 828 },
globalWindow: { leftPx: 2240, topPx: 160, widthPx: 1280, heightPx: 900 }
},
{ leftPx: 1920, topPx: 0, widthPx: 2560, heightPx: 1368 }
);
如果菜单属于应用子窗口,还要确认它需要的是父窗口坐标还是全局坐标;不要在调用层悄悄减一次标题栏高度,所有系统区域偏移都应进入 geometry 构造函数。
案例二:拖拽中避免每帧重建页面
on('windowRectChange') 在连续拖拽时可能高频触发。每次回调都重算整个页面,会把坐标错误变成性能抖动。几何更新可以合并到下一帧,只保留最新值。
class LatestGeometryScheduler {
private pending?: WindowGeometry;
private scheduled = false;
push(value: WindowGeometry, apply: (value: WindowGeometry) => void): void {
this.pending = value;
if (this.scheduled) return;
this.scheduled = true;
setTimeout(() => {
const latest = this.pending;
this.pending = undefined;
this.scheduled = false;
if (latest) apply(latest);
}, 16);
}
}
16ms 只是 60Hz 下的合并窗口,不是保证每次都严格一帧。关键是旧矩形不再排队执行,最终位置一定使用最新事件。窗口模式变化时尺寸可能尚未刷新,应在 windowSizeChange/windowRectChange 等真正的尺寸事件里读取新值,而不是在模式事件到达时立即取旧值。
API 26 值得关注的变化
API 26 的 WindowInfo 新增 displayId 和 globalRect,使多显示器下“窗口在哪块屏、全局位置是什么”更明确;窗口属性和布局文档同时区分 windowRect、drawableRect、globalDisplayRect。升级适配时应停止使用“屏幕宽度 + 窗口 left”的经验公式。
自由窗口在 PC/2in1 上是默认形态,窗口可拖拽、缩放、分屏组合。官方文档明确提醒:仍按固定屏幕宽高布局,缩放后会出现截断、遮挡和控件叠加。几何层修好锚点后,页面本身还要使用响应式断点和最小尺寸验证。
两种常见错误为何不可靠
| 写法 | 看似可用的原因 | 真正失败条件 |
| anchorPx = windowLeft + componentX | 单屏、密度相同 | 外接屏密度变化或标题栏偏移 |
| 在 windowStatusChange 中立即读尺寸 | 最大化时偶尔正好刷新完成 | 事件顺序变化,读到旧 windowRect |
| 每次 rect 变化重建页面 | 拖动慢时不明显 | 高频拖拽造成卡顿和旧结果覆盖 |
可执行的边界测试
const geo: WindowGeometry = {
density: 1.5,
drawable: { leftPx: 0, topPx: 72, widthPx: 1000, heightPx: 700 },
globalWindow: { leftPx: 2000, topPx: 100, widthPx: 1000, heightPx: 772 }
};
const point = localAnchorToGlobal({ xVp: 100, yVp: 20 }, geo);
console.assert(point.xPx === 2150);
console.assert(point.yPx === 202);
const bounded = clampPopup(
{ xPx: 4400, yPx: 1300 }, { widthPx: 400, heightPx: 300 },
{ leftPx: 1920, topPx: 0, widthPx: 2560, heightPx: 1368 }
);
console.assert(bounded.leftPx === 4080);
console.assert(bounded.topPx === 1068);
真机/实机验证应覆盖:100% 与 150% 密度、主屏左右排列、外接屏断开、窗口跨屏拖动、最小窗口、最大化恢复、标题栏沉浸、系统 Dock 位置变化。
收尾检查
- 变量名明确 vp、px、globalPx,不裸传 Point。
- 每次使用当前 displayId 对应的密度和安全区。
- drawableRect 的系统区域偏移只加一次。
- 跨屏比较统一使用 globalDisplayRect/globalRect。
- 模式变化后等待尺寸或矩形事件再取值。
- 连续拖拽只应用最新几何,不排队重建。
- 在最小窗口和不同显示缩放上做回归。
结论
外接屏窗口错位通常不是一个 left 算错,而是坐标原点、单位和读取时机同时失控。先把 windowRect、drawableRect、globalDisplayRect 的责任拆开,再统一到全局 px 空间钳制,右键菜单、拖拽浮层和子窗口都能复用同一个几何模块。
参考资料
- 窗口布局(2026-09-09 更新):https://developer.huawei.com/consumer/cn/doc/doccenter-capabilities/window-layout
- 自由窗口简介(2026-06-27 更新):https://developer.huawei.com/consumer/cn/doc/HarmonyOS-Guides/freeform-window-overview
- 窗口模式与尺寸监听(2026-06-23 更新):https://developer.huawei.com/consumer/cn/doc/doccenter-multi-device/bpta-multi-device-window-mode
- ArkUI API 26 新增接口清单:https://developer.huawei.com/consumer/en/doc/harmonyos-releases/js-apidiff-arkui-7001
- HarmonyOS SDK 26.0.0 Release(2026-08-29):https://developer.huawei.com/consumer/cn/doc/doccenter-release-notes/overview-2600
更多推荐


所有评论(0)