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
Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐