ohos-dev-distributed-mmi-wms-dms-simulation

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

WMS/DMS 模拟

WMS/DMS Simulation

测试进程直接通过 InputManager IPC 构造显示组和窗口,替代 WMS(窗口管理服务)和 DMS(显示管理服务),实现输入子系统的独立功能验证。
前置 skill: 权限获取、server 绕过、编译部署、hidumper 捕获等基础设施见
ohos-dev-distributed-mmi-device-test-harness
;虚拟设备创建和事件注入见
ohos-dev-distributed-mmi-uinput-virtual-device
。
The test process directly constructs display groups and windows through InputManager IPC, replacing WMS (Window Manager Service) and DMS (Display Manager Service), to perform independent functional validation of the input subsystem.
Prerequisite skills: Infrastructure such as permission acquisition, server bypass, build and deployment, and hidumper capture is covered in
ohos-dev-distributed-mmi-device-test-harness
; virtual device creation and event injection are covered in
ohos-dev-distributed-mmi-uinput-virtual-device
.

When to Use

When to Use

  • 按测试要求模拟显示组、用户、主屏、扩展屏、镜像屏和窗口栈
  • 测试依赖显示组/窗口状态的输入功能(设备绑定、光标路由、焦点分发、命中测试)
  • 验证单显示组多屏、多显示组多用户、镜像屏比例不一致、前台用户切换等场景
  • 不想或不能启动完整 WMS/DMS 栈时
  • Simulate display groups, users, main, extended, and mirror displays, and window stacks according to test requirements
  • Test input features that depend on display group/window state, such as device binding, cursor routing, focus dispatch, and hit testing
  • Validate scenarios such as a single display group with multiple displays, multiple display groups with multiple users, mismatched mirror-display aspect ratios, and foreground-user switching
  • When the full WMS/DMS stack is not desired or cannot be started

已验证能力摘要

Verified Capability Summary

DAYU200 真机
dual_group_interleave_test
已覆盖双显示组构造、多窗口注入、per-group 光标/按钮/键盘/焦点/captureMode/滚轮隔离、热拔插绑定清理、跨绑定按键状态和 per-window 光标外观。详细步骤、dump 证据和 PARTIAL 项见
evals/test-results.md
,不要把长篇实测表格复制到
SKILL.md
主流程。
On a real DAYU200 device,
dual_group_interleave_test
has covered dual-display-group construction, multi-window injection, per-group cursor/button/keyboard/focus/captureMode/scroll-wheel isolation, hot-plug binding cleanup, cross-binding key state, and per-window cursor appearance. Detailed steps, dump evidence, and PARTIAL items are available in
evals/test-results.md
. Do not copy the lengthy real-device test table into the main
SKILL.md
workflow.

Scenario-First 建模流程

Scenario-First Modeling Process

不要从固定 demo 出发。先把用户要求归一成这张模型表,再生成
UserScreenInfo
和窗口注入:
维度必填内容关键约束
User
userId
、是否前台/ACTIVE
只模拟前台用户时,所有目标
UserScreenInfo
都应可写入 active display group 状态
DisplayGroup
groupId
、
type
、
mainDisplayId
、
focusWindowId
必须有一个
GROUP_DEFAULT
且 id=0;其他组用
GROUP_SPECIAL
Display主屏/扩展屏/镜像屏的
displayId
、位置、尺寸、dpi、direction
screenArea
必须和 display 坐标/尺寸一致;扩展屏坐标不能无意重叠
Mirror镜像源 display、镜像 display、缩放比例或 letterbox/crop 策略镜像屏比例和主屏不一致时要显式建模坐标映射,不能假设 1:1
Screen与每个 display 对应的
ScreenInfo
screens
数量和 id 必须覆盖所有 displays
Window每屏焦点窗口、背景窗口、浮窗、遮挡窗口、hotArea
win.groupId
/
displayId
必须匹配目标 display;zOrder 必须体现层叠关系
Focus每个 display 的焦点窗口,以及 group 级键盘焦点
DisplayGroupInfo.focusWindowId
是键盘路由关键;每屏焦点窗口也要体现在窗口栈中
输出模拟方案时,先给出模型表,再给出 C++ helper 或伪代码。不要只贴单一固定
SetupDualDisplayGroups()
。
Do not start from a fixed demo. First normalize the user's requirements into this model table, then generate the
UserScreenInfo
and window injections:
DimensionRequired ContentKey Constraints
User
userId
and whether the user is foreground/ACTIVE
When only foreground users are being simulated, every target
UserScreenInfo
should be writable into the active display-group state
DisplayGroup
groupId
,
type
,
mainDisplayId
, and
focusWindowId
There must be one
GROUP_DEFAULT
with id=0; other groups use
GROUP_SPECIAL
DisplayThe main, extended, or mirror display's
displayId
, position, dimensions, dpi, and direction
screenArea
must match the display's coordinates and dimensions; extended-display coordinates must not overlap unintentionally
MirrorMirror source display, mirror display, scale factor, or letterbox/crop strategyWhen the mirror display has a different aspect ratio from the main display, explicitly model the coordinate mapping; do not assume 1:1
ScreenA
ScreenInfo
corresponding to each display
The number and IDs of
screens
must cover all displays
WindowThe focus, background, floating, and occluding windows and hotArea for each display
win.groupId
/
displayId
must match the target display;
zOrder
must reflect the stacking relationships
FocusThe focus window for each display and the group-level keyboard focus
DisplayGroupInfo.focusWindowId
is critical for keyboard routing; each display's focus window must also be represented in the window stack
When producing a simulation plan, first provide the model table, followed by a C++ helper or pseudocode. Do not paste only a single fixed
SetupDualDisplayGroups()
implementation.

标准场景模板

Standard Scenario Templates

单显示组:主屏 + 扩展屏 + 主屏镜像屏

Single Display Group: Main Display + Extended Display + Main Display Mirror

用于验证一个用户/一个 display group 内的多屏路由、每屏焦点窗口、镜像屏坐标映射和多窗口层叠:
对象建议建模
group
groupId=0
,
GROUP_DEFAULT
,
mainDisplayId=0
主屏
displayId=0
,
screenArea={0,0,720,1280}
扩展屏
displayId=1
,
screenArea={720,0,1920,1080}
,与主屏横向拼接
镜像屏
displayId=2
,
screenArea={2640,0,1024,768}
,镜像主屏但比例不同
焦点窗口每个 display 至少一个 focus window,例如 1000/1100/1200
层叠窗口每屏背景窗口、状态栏/系统层、浮窗,使用不同 zOrder
hotArea镜像屏和扩展屏都要配置
pointerHotAreas
,用于命中测试
镜像屏比例不一致时,必须在测试说明中写清楚期望坐标策略:
  • fit/letterbox:保持主屏比例,镜像屏部分区域无有效 hotArea。
  • stretch:主屏坐标按 X/Y 独立比例映射到镜像屏。
  • crop:镜像屏只覆盖主屏裁剪区域。
MMI 的窗口命中只看注入的 display/window 几何信息;如果要验证镜像坐标换算,测试代码必须把源屏坐标转换为镜像屏 display 坐标后再注入事件或验证目标窗口。
This scenario is used to validate multi-display routing within one user/display group, the focus window on each display, mirror-display coordinate mapping, and multi-window stacking:
ObjectSuggested Modeling
group
groupId=0
,
GROUP_DEFAULT
,
mainDisplayId=0
Main display
displayId=0
,
screenArea={0,0,720,1280}
Extended display
displayId=1
,
screenArea={720,0,1920,1080}
, horizontally adjacent to the main display
Mirror display
displayId=2
,
screenArea={2640,0,1024,768}
, mirrors the main display but has a different aspect ratio
Focus windowAt least one focus window per display, for example 1000/1100/1200
Stacked windowsA background window, status bar/system layer, and floating window for each display, using different
zOrder
values
hotAreaConfigure
pointerHotAreas
for both the mirror and extended displays for hit testing
When the mirror display has a different aspect ratio, the test description must clearly state the expected coordinate strategy:
  • fit/letterbox: Preserve the main display's aspect ratio; part of the mirror display has no valid hotArea.
  • stretch: Map main-display coordinates to the mirror display using independent X/Y scale factors.
  • crop: The mirror display covers only the cropped region of the main display.
MMI window hit testing relies only on the injected display/window geometry. To validate mirror-coordinate conversion, the test code must convert source-display coordinates into mirror-display coordinates before injecting the event or validating the target window.

多显示组多用户:每组都是主屏 + 扩展屏 + 镜像屏

Multiple Display Groups and Users: Each Group Contains a Main, Extended, and Mirror Display

用于验证不同前台用户或并行前台场景下的显示组隔离:
对象建议建模
user A group
userId=100
,
groupId=0
,
GROUP_DEFAULT
user B group
userId=101
,
groupId=10
,
GROUP_SPECIAL
或按被测模型分配非 0 group
每个 group都包含 main/extended/mirror 三类 display
displayId全局唯一,不同 group 不能复用同一 displayId
windowId全局唯一,建议按 group/display 分段编号
focus每个 group 设置 group 级 focusWindowId;每个 display 都有自己的可命中焦点窗口
mirror mismatch每组至少一个镜像屏比例不同,用来验证坐标和命中隔离
如果底层
UserScreenInfo
一次只表达一个 user,则按用户分别调用
UpdateDisplayInfo
,并在报告中说明“多用户均为前台/ACTIVE”的模拟假设。不要把多个用户的窗口混到同一个 group 后声称验证了多用户隔离。
This scenario is used to validate display-group isolation under different foreground-user or parallel-foreground scenarios:
ObjectSuggested Modeling
user A group
userId=100
,
groupId=0
,
GROUP_DEFAULT
user B group
userId=101
,
groupId=10
,
GROUP_SPECIAL
, or another nonzero group assigned according to the model under test
Each groupContains main, extended, and mirror displays
displayIdGlobally unique; the same displayId cannot be reused across different groups
windowIdGlobally unique; segmented numbering by group/display is recommended
focusEach group has a group-level
focusWindowId
; each display has its own hittable focus window
mirror mismatchEach group has at least one mirror display with a different aspect ratio to validate coordinate and hit-test isolation
If the underlying
UserScreenInfo
can express only one user at a time, call
UpdateDisplayInfo
separately for each user and document the simulation assumption that "multiple users are all foreground/ACTIVE" in the report. Do not mix windows from multiple users into the same group and then claim that multi-user isolation has been verified.

构造显示组(替代 DMS)

Constructing Display Groups (Instead of DMS)

下面是最小构造片段。真实测试应按上面的 scenario model 生成多个 display、screen 和 group。
cpp
#include "input_manager.h"

UserScreenInfo screenInfo;
screenInfo.userId = 100;

// Group 0: 默认组(主屏)
DisplayGroupInfo group0;
group0.id = 0;
group0.name = "default";
group0.type = GroupType::GROUP_DEFAULT;
group0.mainDisplayId = 0;
group0.focusWindowId = 1000;

DisplayInfo disp0;
disp0.id = 0;
disp0.x = 0; disp0.y = 0;
disp0.width = 720; disp0.height = 1280;
disp0.dpi = 240;
disp0.name = "main";
disp0.direction = DIRECTION0;
disp0.displayDirection = DIRECTION0;
disp0.screenArea = { .id = 0, .area = { 0, 0, 720, 1280 } };
group0.displaysInfo.push_back(disp0);

// Group 1: 特殊组(另一个显示组;单组多屏时可改为给 group0 增加 display1/display2)
DisplayGroupInfo group1;
group1.id = 1;
group1.name = "secondary";
group1.type = GroupType::GROUP_SPECIAL;
group1.mainDisplayId = 1;
group1.focusWindowId = 2000;

DisplayInfo disp1;
disp1.id = 1;
disp1.x = 0; disp1.y = 0;
disp1.width = 1920; disp1.height = 1080;
disp1.dpi = 160;
disp1.name = "secondary";
disp1.direction = DIRECTION0;
disp1.displayDirection = DIRECTION0;
disp1.screenArea = { .id = 1, .area = { 0, 0, 1920, 1080 } };
group1.displaysInfo.push_back(disp1);

// 单组多屏时,扩展屏/镜像屏也可以 push 到同一个 group:
// group0.displaysInfo.push_back(extendedDisplay);
// group0.displaysInfo.push_back(mirrorDisplay);

screenInfo.displayGroups.push_back(group0);
screenInfo.displayGroups.push_back(group1);

// 物理屏幕信息(必须与 displaysInfo 对应)
ScreenInfo screen0;
screen0.id = 0;
screen0.uniqueId = "default0";
screen0.width = 720; screen0.height = 1280;
screen0.physicalWidth = 62; screen0.physicalHeight = 110;
screen0.dpi = 240; screen0.ppi = 295;
screen0.tpDirection = DIRECTION0;
screenInfo.screens.push_back(screen0);

ScreenInfo screen1;
screen1.id = 1;
screen1.uniqueId = "secondary1";
screen1.width = 1920; screen1.height = 1080;
screen1.physicalWidth = 531; screen1.physicalHeight = 299;
screen1.dpi = 160; screen1.ppi = 92;
screen1.tpDirection = DIRECTION0;
screenInfo.screens.push_back(screen1);

InputManager::GetInstance()->UpdateDisplayInfo(screenInfo);
Below is the minimal construction snippet. A real test should generate multiple displays, screens, and groups according to the scenario model above.
cpp
#include "input_manager.h"

UserScreenInfo screenInfo;
screenInfo.userId = 100;

// Group 0: 默认组(主屏)
DisplayGroupInfo group0;
group0.id = 0;
group0.name = "default";
group0.type = GroupType::GROUP_DEFAULT;
group0.mainDisplayId = 0;
group0.focusWindowId = 1000;

DisplayInfo disp0;
disp0.id = 0;
disp0.x = 0; disp0.y = 0;
disp0.width = 720; disp0.height = 1280;
disp0.dpi = 240;
disp0.name = "main";
disp0.direction = DIRECTION0;
disp0.displayDirection = DIRECTION0;
disp0.screenArea = { .id = 0, .area = { 0, 0, 720, 1280 } };
group0.displaysInfo.push_back(disp0);

// Group 1: 特殊组(另一个显示组;单组多屏时可改为给 group0 增加 display1/display2)
DisplayGroupInfo group1;
group1.id = 1;
group1.name = "secondary";
group1.type = GroupType::GROUP_SPECIAL;
group1.mainDisplayId = 1;
group1.focusWindowId = 2000;

DisplayInfo disp1;
disp1.id = 1;
disp1.x = 0; disp1.y = 0;
disp1.width = 1920; disp1.height = 1080;
disp1.dpi = 160;
disp1.name = "secondary";
disp1.direction = DIRECTION0;
disp1.displayDirection = DIRECTION0;
disp1.screenArea = { .id = 1, .area = { 0, 0, 1920, 1080 } };
group1.displaysInfo.push_back(disp1);

// 单组多屏时,扩展屏/镜像屏也可以 push 到同一个 group:
// group0.displaysInfo.push_back(extendedDisplay);
// group0.displaysInfo.push_back(mirrorDisplay);

screenInfo.displayGroups.push_back(group0);
screenInfo.displayGroups.push_back(group1);

// 物理屏幕信息(必须与 displaysInfo 对应)
ScreenInfo screen0;
screen0.id = 0;
screen0.uniqueId = "default0";
screen0.width = 720; screen0.height = 1280;
screen0.physicalWidth = 62; screen0.physicalHeight = 110;
screen0.dpi = 240; screen0.ppi = 295;
screen0.tpDirection = DIRECTION0;
screenInfo.screens.push_back(screen0);

ScreenInfo screen1;
screen1.id = 1;
screen1.uniqueId = "secondary1";
screen1.width = 1920; screen1.height = 1080;
screen1.physicalWidth = 531; screen1.physicalHeight = 299;
screen1.dpi = 160; screen1.ppi = 92;
screen1.tpDirection = DIRECTION0;
screenInfo.screens.push_back(screen1);

InputManager::GetInstance()->UpdateDisplayInfo(screenInfo);

关键约束

Key Constraints

字段约束后果
group0.type
必须有一个
GROUP_DEFAULT
InitDisplayGroupInfo
校验 groupId==MAIN_GROUPID
group0.id
DEFAULT 组 id 必须为 0非 0 会被拒绝
focusWindowId
必须在 DisplayGroupInfo 中预设键盘事件路由依赖此值,不设则为 -1 导致键盘事件无法分发
screenArea
必须与 width/height 一致光标边界夹紧依赖此值
screens
数量和 ID
必须与 displaysInfo 对应屏幕信息用于 DPI/PPI 计算
userState
默认
USER_ACTIVE
,无需设置
非 ACTIVE 时 displayGroupInfoMap_ 不写入
displayId
全局唯一多组/多用户复用 displayId 会导致路由和 dump 解释混乱
镜像屏比例必须显式说明映射策略不同比例镜像屏不能直接用主屏坐标断言命中
FieldConstraintConsequence
group0.type
There must be one
GROUP_DEFAULT
InitDisplayGroupInfo
validates that groupId==MAIN_GROUPID
group0.id
The DEFAULT group id must be 0A nonzero value is rejected
focusWindowId
Must be preset in
DisplayGroupInfo
Keyboard event routing depends on this value; if left unset, it becomes -1 and keyboard events cannot be dispatched
screenArea
Must match width/heightCursor boundary clamping depends on this value
Number and IDs of
screens
Must correspond to
displaysInfo
Screen information is used for DPI/PPI calculations
userState
Defaults to
USER_ACTIVE
and does not need to be set
When it is not ACTIVE,
displayGroupInfoMap_
is not written
displayId
Must be globally uniqueReusing a displayId across groups or users makes routing and dump interpretation ambiguous
Mirror-display aspect ratioThe mapping strategy must be explicitly statedA mirror display with a different aspect ratio cannot be asserted using main-display coordinates directly

返回码

Return Code

UpdateDisplayInfo
返回 -201 是正常的 — 这是
UpdateUIExtensionInfo
的错误码,不影响显示组数据的写入。
It is normal for
UpdateDisplayInfo
to return -201. This is the error code of
UpdateUIExtensionInfo
and does not affect the display-group data being written.

注入虚拟窗口(替代 WMS)

Injecting Virtual Windows (Instead of WMS)

每个 display 至少需要一个可命中的窗口。复杂场景应为每个 display 注入自己的背景窗口、焦点窗口和需要验证的层叠窗口:
cpp
WindowGroupInfo wgi0;
wgi0.focusWindowId = 1000;
wgi0.displayId = 0;

WindowInfo win0;
win0.id = 1000;
win0.pid = getpid();
win0.uid = getuid();
win0.area = { 0, 0, 720, 1280 };
win0.defaultHotAreas = { { 0, 0, 720, 1280 } };
win0.pointerHotAreas = { { 0, 0, 720, 1280 } };
win0.agentWindowId = -1;
win0.flags = 0;
win0.action = WINDOW_UPDATE_ACTION::ADD;
win0.displayId = 0;
win0.groupId = 0;
win0.zOrder = 1.0f;
wgi0.windowsInfo.push_back(win0);

InputManager::GetInstance()->UpdateWindowInfo(wgi0);
Each display requires at least one hittable window. Complex scenarios should inject each display's own background window, focus window, and layered windows that need to be validated:
cpp
WindowGroupInfo wgi0;
wgi0.focusWindowId = 1000;
wgi0.displayId = 0;

WindowInfo win0;
win0.id = 1000;
win0.pid = getpid();
win0.uid = getuid();
win0.area = { 0, 0, 720, 1280 };
win0.defaultHotAreas = { { 0, 0, 720, 1280 } };
win0.pointerHotAreas = { { 0, 0, 720, 1280 } };
win0.agentWindowId = -1;
win0.flags = 0;
win0.action = WINDOW_UPDATE_ACTION::ADD;
win0.displayId = 0;
win0.groupId = 0;
win0.zOrder = 1.0f;
wgi0.windowsInfo.push_back(win0);

InputManager::GetInstance()->UpdateWindowInfo(wgi0);

关键约束

Key Constraints

字段约束
win.groupId
必须匹配目标显示组 ID
win.displayId
必须匹配显示组内的 display ID
win.pid
用
getpid()
— 权限检查需要
win.action
首次用
ADD
,更新用
CHANGE
wgi.focusWindowId
对 MAIN_GROUPID 生效;非主组从 displayGroupInfoMap_ 读取
pointerHotAreas
鼠标事件命中测试依赖此区域
win.id
全局唯一
zOrder
按业务层级显式区分
FieldConstraint
win.groupId
Must match the target display-group ID
win.displayId
Must match the display ID within the display group
win.pid
Use
getpid()
—it is required for permission checks
win.action
Use
ADD
for the first insertion and
CHANGE
for updates
wgi.focusWindowId
Takes effect for
MAIN_GROUPID
; for non-main groups, the value is read from
displayGroupInfoMap_
pointerHotAreas
Mouse-event hit testing depends on this region
win.id
Must be globally unique; reusing a windowId across displays or groups makes dumps and dispatch results unauditable
zOrder
Must be explicitly assigned according to the business hierarchy; when all windows have the same zOrder, hit order cannot be used as evidence

每屏窗口栈模式

Per-Display Window Stack Pattern

为每个 display 建议至少构造:
窗口zOrder作用
background/app10默认命中目标
panel/status/system100验证系统层优先级
popup/floating200+验证重叠命中和 DEL 后回退
每屏焦点窗口可以是 background,也可以是专门的输入窗口;需要在模型表中标明。
Construct at least the following for each display:
WindowzOrderPurpose
background/app10Default hit target
panel/status/system100Verify system-layer priority
popup/floating200+Verify overlapping hit testing and fallback after DEL
The focus window on each display may be the background window or a dedicated input window; indicate this in the model table.

增量窗口操作

Incremental Window Operations

单个窗口的 ADD/CHANGE/DEL,无需重新注入全部窗口:
cpp
static void InjectSingleWindow(int32_t winId, int32_t groupId, int32_t displayId,
    Rect area, float zOrder, WINDOW_UPDATE_ACTION action, int32_t focusWinId = -1)
{
    WindowGroupInfo wgi;
    wgi.focusWindowId = focusWinId;
    wgi.displayId = displayId;
    WindowInfo win;
    win.id = winId;
    win.pid = getpid();
    win.uid = getuid();
    win.area = area;
    win.defaultHotAreas = { area };
    win.pointerHotAreas = { area };
    win.agentWindowId = -1;
    win.flags = 0;
    win.action = action;
    win.displayId = displayId;
    win.groupId = groupId;
    win.zOrder = zOrder;
    wgi.windowsInfo.push_back(win);
    InputManager::GetInstance()->UpdateWindowInfo(wgi);
}
UpdateDisplayInfoByIncrementalInfo
处理:
ADD
追加(已存在则替换),
CHANGE
就地替换,
DEL
从列表移除。
An ADD/CHANGE/DEL operation for an individual window does not require reinjecting all windows:
cpp
static void InjectSingleWindow(int32_t winId, int32_t groupId, int32_t displayId,
    Rect area, float zOrder, WINDOW_UPDATE_ACTION action, int32_t focusWinId = -1)
{
    WindowGroupInfo wgi;
    wgi.focusWindowId = focusWinId;
    wgi.displayId = displayId;
    WindowInfo win;
    win.id = winId;
    win.pid = getpid();
    win.uid = getuid();
    win.area = area;
    win.defaultHotAreas = { area };
    win.pointerHotAreas = { area };
    win.agentWindowId = -1;
    win.flags = 0;
    win.action = action;
    win.displayId = displayId;
    win.groupId = groupId;
    win.zOrder = zOrder;
    wgi.windowsInfo.push_back(win);
    InputManager::GetInstance()->UpdateWindowInfo(wgi);
}
UpdateDisplayInfoByIncrementalInfo
handles the following operations:
ADD
appends the window, replacing it if it already exists;
CHANGE
replaces it in place;
DEL
removes it from the list.

动态焦点切换

Dynamic Focus Switching

主组 (groupId=0)

Main Group (groupId=0)

UpdateWindowInfo
使用传入的
wgi.focusWindowId
,直接生效。
UpdateWindowInfo
uses the supplied
wgi.focusWindowId
, which takes effect immediately.

非主组 (groupId!=0)

Non-Main Group (groupId!=0)

UpdateWindowInfo
忽略传入的
focusWindowId
,始终从
displayGroupInfoMap_
读取。必须通过
UpdateDisplayInfo
更新焦点:
cpp
SetupDualDisplayGroups(1000, 2002);  // 重新调用 UpdateDisplayInfo
sleep(1s);
InjectVirtualWindows();               // 重新注入窗口(同步 focusWinId 全局变量)
sleep(1s);
多屏场景里要区分:
  • group 级键盘焦点:由
    DisplayGroupInfo.focusWindowId
    决定。
  • 每屏期望焦点窗口:由测试窗口栈和命中坐标体现,不要误认为 MMI 会为每个 display 自动维护独立键盘焦点。
  • 如果需求声称“每个屏幕都有自己的焦点窗口”,测试应明确这是窗口栈/命中目标焦点,还是输入框架实际键盘焦点。两者不能混写。
实现模式:使用全局变量跟踪焦点,
SetupDualDisplayGroups
和
InjectVirtualWindows
共享:
cpp
static int32_t g_focusWin0 = 1000;
static int32_t g_focusWin1 = 2000;

static void SetupDualDisplayGroups(int32_t focusWin0 = 1000, int32_t focusWin1 = 2000)
{
    g_focusWin0 = focusWin0;
    g_focusWin1 = focusWin1;
    // ... UpdateDisplayInfo with focusWin0/focusWin1
}

static void InjectVirtualWindows()
{
    // wgi.focusWindowId = g_focusWin0;  // 不再硬编码
    // ...
}
UpdateWindowInfo
ignores the supplied
focusWindowId
and always reads it from
displayGroupInfoMap_
. The focus must be updated through
UpdateDisplayInfo
:
cpp
SetupDualDisplayGroups(1000, 2002);  // 重新调用 UpdateDisplayInfo
sleep(1s);
InjectVirtualWindows();               // 重新注入窗口(同步 focusWinId 全局变量)
sleep(1s);
In multi-display scenarios, distinguish the following:
  • Group-level keyboard focus: Determined by
    DisplayGroupInfo.focusWindowId
    .
  • Expected focus window on each display: Represented by the test window stack and hit coordinates. Do not assume that MMI automatically maintains independent keyboard focus for each display.
  • If a requirement states that "each screen has its own focus window," the test must explicitly clarify whether this means window-stack/hit-target focus or actual input-framework keyboard focus. The two must not be conflated.
Implementation pattern: Use global variables to track focus and share them between
SetupDualDisplayGroups
and
InjectVirtualWindows
:
cpp
static int32_t g_focusWin0 = 1000;
static int32_t g_focusWin1 = 2000;

static void SetupDualDisplayGroups(int32_t focusWin0 = 1000, int32_t focusWin1 = 2000)
{
    g_focusWin0 = focusWin0;
    g_focusWin1 = focusWin1;
    // ... UpdateDisplayInfo with focusWin0/focusWin1
}

static void InjectVirtualWindows()
{
    // wgi.focusWindowId = g_focusWin0;  // 不再硬编码
    // ...
}

多窗口 z-order 命中测试

Multi-Window z-Order Hit Testing

基本模式:不同区域不同层级

Basic Pattern: Different Regions and Different Layers

cpp
// Group 0: 3 个窗口
// win1000: 应用窗口(背景)  area={0,80,720,1200}   zOrder=10
// win1001: 状态栏          area={0,0,720,80}       zOrder=100
// win1002: 悬浮球          area={600,600,120,120}   zOrder=200
SelectWindowInfo
按 zOrder 降序遍历,第一个
pointerHotAreas
命中的窗口为目标。重叠区域 zOrder 高的优先。
cpp
// Group 0: 3 个窗口
// win1000: 应用窗口(背景)  area={0,80,720,1200}   zOrder=10
// win1001: 状态栏          area={0,0,720,80}       zOrder=100
// win1002: 悬浮球          area={600,600,120,120}   zOrder=200
SelectWindowInfo
iterates in descending zOrder, and the first window whose
pointerHotAreas
contains the point becomes the target. In overlapping regions, the window with the higher zOrder takes priority.

全屏重叠模式:位置无关验证

Full-Screen Overlap Pattern: Position-Independent Validation

ADD 多个全屏窗口(不同 zOrder),无需光标定位 — 全屏窗口在任意光标位置都命中:
cpp
InjectSingleWindow(1005, 0, 0, {0,0,720,1280}, 50.0f, WINDOW_UPDATE_ACTION::ADD, g_focusWin0);
InjectSingleWindow(1006, 0, 0, {0,0,720,1280}, 150.0f, WINDOW_UPDATE_ACTION::ADD, g_focusWin0);
InjectSingleWindow(1007, 0, 0, {0,0,720,1280}, 250.0f, WINDOW_UPDATE_ACTION::ADD, g_focusWin0);
// 点击 → Cursor Info.windowId = 1007 (z=250 最高)
// DEL win1007 → 点击 → windowId 变为原有最高层(可能是 win1002=z200,不一定是 win1006=z150)
注意:DEL 全屏窗口后,命中目标取决于剩余所有窗口的 zOrder,包括原有非全屏窗口。上例中 DEL win1007(z=250) 后,如果光标位置在 win1002(z=200) 的 hotArea 内,则命中 win1002 而非 win1006(z=150)。
ADD multiple full-screen windows with different zOrder values so that cursor positioning is unnecessary. A full-screen window is hit at any cursor position:
cpp
InjectSingleWindow(1005, 0, 0, {0,0,720,1280}, 50.0f, WINDOW_UPDATE_ACTION::ADD, g_focusWin0);
InjectSingleWindow(1006, 0, 0, {0,0,720,1280}, 150.0f, WINDOW_UPDATE_ACTION::ADD, g_focusWin0);
InjectSingleWindow(1007, 0, 0, {0,0,720,1280}, 250.0f, WINDOW_UPDATE_ACTION::ADD, g_focusWin0);
// 点击 → Cursor Info.windowId = 1007 (z=250 最高)
// DEL win1007 → 点击 → windowId 变为原有最高层(可能是 win1002=z200,不一定是 win1006=z150)
Note: After a full-screen window is deleted, the hit target depends on the zOrder of all remaining windows, including existing non-full-screen windows. In the example above, after deleting win1007 (z=250), if the cursor is within the hotArea of win1002 (z=200), win1002 is hit rather than win1006 (z=150).

热区穿透

Hot-Area Passthrough

area
和
pointerHotAreas
可以不同。光标在
area
内但
pointerHotAreas
外时,点击穿透到下层窗口:
cpp
// win1004: area 大于 hotArea
WindowInfo win;
win.area = { 100, 200, 400, 400 };
win.defaultHotAreas = { { 200, 300, 200, 200 } };  // hotArea 小于 area
win.pointerHotAreas = { { 200, 300, 200, 200 } };
// 点击 (150,250) — area 内但 hotArea 外 → 穿透到下层
// 点击 (350,450) — hotArea 内 → 命中此窗口
area
and
pointerHotAreas
may differ. When the pointer is within
area
but outside
pointerHotAreas
, the click passes through to the window below:
cpp
// win1004: area 大于 hotArea
WindowInfo win;
win.area = { 100, 200, 400, 400 };
win.defaultHotAreas = { { 200, 300, 200, 200 } };  // hotArea 小于 area
win.pointerHotAreas = { { 200, 300, 200, 200 } };
// 点击 (150,250) — area 内但 hotArea 外 → 穿透到下层
// 点击 (350,450) — hotArea 内 → 命中此窗口

窗口生命周期 (ADD → CHANGE → DEL)

Window Lifecycle (ADD → CHANGE → DEL)

cpp
// ADD: 新增弹窗
InjectSingleWindow(1003, 0, 0, {200,200,320,320}, 300.0f, WINDOW_UPDATE_ACTION::ADD, 1000);
// 点击 (360,360) → 命中 win1003

// CHANGE: 移动弹窗位置
InjectSingleWindow(1003, 0, 0, {400,800,320,320}, 300.0f, WINDOW_UPDATE_ACTION::CHANGE, 1000);
// 点击原位置 (360,360) → 穿透到 win1000

// DEL: 删除弹窗
InjectSingleWindow(1003, 0, 0, {0,0,0,0}, 0.0f, WINDOW_UPDATE_ACTION::DEL, 1000);
// 窗口列表恢复
cpp
// ADD: 新增弹窗
InjectSingleWindow(1003, 0, 0, {200,200,320,320}, 300.0f, WINDOW_UPDATE_ACTION::ADD, 1000);
// 点击 (360,360) → 命中 win1003

// CHANGE: 移动弹窗位置
InjectSingleWindow(1003, 0, 0, {400,800,320,320}, 300.0f, WINDOW_UPDATE_ACTION::CHANGE, 1000);
// 点击原位置 (360,360) → 穿透到 win1000

// DEL: 删除弹窗
InjectSingleWindow(1003, 0, 0, {0,0,0,0}, 0.0f, WINDOW_UPDATE_ACTION::DEL, 1000);
// 窗口列表恢复

可用的验证 API

Available Validation APIs

cpp
// 设备绑定(详见 ohos-dev-distributed-mmi-device-test-harness)
InputManager::GetInstance()->BindDeviceToDisplayGroupByDisplay(deviceId, displayId, msg);
InputManager::GetInstance()->UnbindDeviceFromDisplayGroup(deviceId, msg);

// 光标样式(per-window)
PointerStyle style;
style.id = 13;            // MOUSE_ICON::CROSS
style.size = 3;
style.color = 0xFF0000;   // red
InputManager::GetInstance()->SetPointerStyle(windowId, style);
InputManager::GetInstance()->GetPointerStyle(windowId, style);

// 全局光标大小/颜色
InputManager::GetInstance()->SetPointerSize(4);
InputManager::GetInstance()->SetPointerColor(0x00FF00);

// 捕获模式
InputManager::GetInstance()->EnterCaptureMode(windowId);
InputManager::GetInstance()->LeaveCaptureMode(windowId);
cpp
// 设备绑定(详见 ohos-dev-distributed-mmi-device-test-harness)
InputManager::GetInstance()->BindDeviceToDisplayGroupByDisplay(deviceId, displayId, msg);
InputManager::GetInstance()->UnbindDeviceFromDisplayGroup(deviceId, msg);

// 光标样式(per-window)
PointerStyle style;
style.id = 13;            // MOUSE_ICON::CROSS
style.size = 3;
style.color = 0xFF0000;   // red
InputManager::GetInstance()->SetPointerStyle(windowId, style);
InputManager::GetInstance()->GetPointerStyle(windowId, style);

// 全局光标大小/颜色
InputManager::GetInstance()->SetPointerSize(4);
InputManager::GetInstance()->SetPointerColor(0x00FF00);

// 捕获模式
InputManager::GetInstance()->EnterCaptureMode(windowId);
InputManager::GetInstance()->LeaveCaptureMode(windowId);

执行顺序

Execution Order

1. InitNativeToken()           — 权限 (见 ohos-dev-distributed-mmi-device-test-harness)
2. 建立 scenario model         — 用户、显示组、主/扩展/镜像屏、窗口栈、焦点
3. UpdateDisplayInfo()         — 注入 display group/screen 信息(等 1 秒)
4. UpdateWindowInfo() x N      — 每个 display 注入窗口栈(等 1 秒)
5. 创建虚拟设备 + FindDevice   — 见 ohos-dev-distributed-mmi-uinput-virtual-device
6. BindDeviceToDisplayGroupByDisplay()
7. warm-up 事件注入(2 次,初始化 group state)
8. 测试逻辑 + DumpPhase()     — dump 捕获见 ohos-dev-distributed-mmi-device-test-harness
9. 清理:解绑 → UI_DEV_DESTROY → close(fd)
1. InitNativeToken()           — 权限 (见 ohos-dev-distributed-mmi-device-test-harness)
2. 建立 scenario model         — 用户、显示组、主/扩展/镜像屏、窗口栈、焦点
3. UpdateDisplayInfo()         — 注入 display group/screen 信息(等 1 秒)
4. UpdateWindowInfo() x N      — 每个 display 注入窗口栈(等 1 秒)
5. 创建虚拟设备 + FindDevice   — 见 ohos-dev-distributed-mmi-uinput-virtual-device
6. BindDeviceToDisplayGroupByDisplay()
7. warm-up 事件注入(2 次,初始化 group state)
8. 测试逻辑 + DumpPhase()     — dump 捕获见 ohos-dev-distributed-mmi-device-test-harness
9. 清理:解绑 → UI_DEV_DESTROY → close(fd)

已知限制

Known Limitations

限制原因规避方案
光标定位不精确libinput 鼠标加速导致 uinput 相对移动无法精确到绝对坐标使用全屏窗口避免依赖光标位置
focusWindowId=-1
在 DisplayGroups dump 中
dump 读
displayGroupInfoMap_
,MAIN_GROUPID 的焦点从
UpdateWindowInfo
传入
通过
KeyboardStateByGroup
或
SequenceSnapshots
验证实际焦点
非主组焦点切换
UpdateWindowInfo
忽略非主组 focusWindowId
必须通过
UpdateDisplayInfo
更新
DEL 全屏后命中非预期窗口剩余窗口中 zOrder 最高的命中,包括原有非全屏窗口注意已有窗口的 zOrder 关系
LimitationCauseWorkaround
Cursor positioning is inaccuratelibinput mouse acceleration makes it impossible to map uinput relative movement precisely to absolute coordinatesUse full-screen windows to avoid depending on cursor position
focusWindowId=-1
appears in the DisplayGroups dump
The dump reads
displayGroupInfoMap_
, while the MAIN_GROUPID focus is supplied through
UpdateWindowInfo
Verify the actual focus through
KeyboardStateByGroup
or
SequenceSnapshots
Focus switching in a non-main group
UpdateWindowInfo
ignores the non-main group's
focusWindowId
The focus must be updated through
UpdateDisplayInfo
An unexpected window is hit after deleting a full-screen windowThe highest zOrder among all remaining windows is hit, including existing non-full-screen windowsCheck the zOrder relationships of all windows

Common Mistakes

Common Mistakes

错误后果修复
不设
focusWindowId
键盘事件不分发到任何窗口在 DisplayGroupInfo 中预设
groupId != 0
的组用
GROUP_DEFAULT
InitDisplayGroupInfo
拒绝
非 0 组用
GROUP_SPECIAL
窗口的
groupId
与显示组不匹配
事件路由到错误的组确保一致
多屏/多组复用 displayId 或 windowIddump 和路由证据不可审计全局唯一编号
镜像屏比例不同但沿用主屏坐标命中断言错误先定义 fit/stretch/crop 映射策略
声称每屏独立键盘焦点但只设置 group focus结论不成立区分 group 级键盘焦点和每屏窗口命中目标
非主组通过 UpdateWindowInfo 切焦点focusWindowId 被忽略必须通过 UpdateDisplayInfo 切焦点
所有窗口用相同 zOrder命中顺序不确定按业务层级分配不同 zOrder
忘记注入窗口鼠标事件无 hit test 目标每组至少一个全屏窗口
UpdateDisplayInfo 返回 -201 就认为失败实际数据已写入-201 来自 UIExtension,可忽略
InjectVirtualWindows 硬编码 focusWindowId动态焦点切换后被覆盖使用全局变量同步焦点
DEL 全屏窗口后假设次高全屏命中原有非全屏窗口可能 zOrder 更高检查所有窗口的 zOrder 关系
MistakeConsequenceFix
Not setting
focusWindowId
Keyboard events are not dispatched to any windowPreset it in
DisplayGroupInfo
Using
GROUP_DEFAULT
for a group with
groupId != 0
InitDisplayGroupInfo
rejects the group
Use
GROUP_SPECIAL
for nonzero groups
The window's
groupId
does not match the display group
Events are routed to the wrong groupEnsure that they match
Reusing displayId or windowId across displays/groupsDump and routing evidence is unauditableUse globally unique IDs
Using main-display coordinates when the mirror display has a different aspect ratioHit assertions are incorrectDefine the fit/stretch/crop mapping strategy first
Claiming independent keyboard focus for each screen while only setting the group focusThe conclusion is invalidDistinguish group-level keyboard focus from per-screen hit targets
Switching focus in a non-main group through
UpdateWindowInfo
focusWindowId
is ignored
Switch focus through
UpdateDisplayInfo
Using the same zOrder for all windowsHit order becomes indeterminateAssign different zOrder values according to the business layer
Forgetting to inject windowsMouse events have no hit-test targetInject at least one full-screen window per group
Treating an
UpdateDisplayInfo
return value of -201 as a failure
The data was actually written-201 comes from UIExtension and can be ignored
Hard-coding
focusWindowId
in
InjectVirtualWindows
The value is overwritten after dynamic focus switchingUse global variables to synchronize the focus
Assuming the second-highest full-screen window will be hit after DELExisting non-full-screen windows may have a higher zOrderCheck the zOrder relationships of all windows