ohos-dev-distributed-mmi-wms-dms-simulation
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseWMS/DMS 模拟
WMS/DMS Simulation
测试进程直接通过 InputManager IPC 构造显示组和窗口,替代 WMS(窗口管理服务)和 DMS(显示管理服务),实现输入子系统的独立功能验证。
前置 skill: 权限获取、server 绕过、编译部署、hidumper 捕获等基础设施见 ;虚拟设备创建和事件注入见 。
ohos-dev-distributed-mmi-device-test-harnessohos-dev-distributed-mmi-uinput-virtual-deviceThe 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 ; virtual device creation and event injection are covered in .
ohos-dev-distributed-mmi-device-test-harnessohos-dev-distributed-mmi-uinput-virtual-deviceWhen 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 真机 已覆盖双显示组构造、多窗口注入、per-group
光标/按钮/键盘/焦点/captureMode/滚轮隔离、热拔插绑定清理、跨绑定按键状态和 per-window
光标外观。详细步骤、dump 证据和 PARTIAL 项见 ,不要把长篇实测表格复制到
主流程。
dual_group_interleave_testevals/test-results.mdSKILL.mdOn a real DAYU200 device, 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 . Do not copy the lengthy real-device test table into the main workflow.
dual_group_interleave_testevals/test-results.mdSKILL.mdScenario-First 建模流程
Scenario-First Modeling Process
不要从固定 demo 出发。先把用户要求归一成这张模型表,再生成 和窗口注入:
UserScreenInfo| 维度 | 必填内容 | 关键约束 |
|---|---|---|
| User | | 只模拟前台用户时,所有目标 |
| DisplayGroup | | 必须有一个 |
| Display | 主屏/扩展屏/镜像屏的 | |
| Mirror | 镜像源 display、镜像 display、缩放比例或 letterbox/crop 策略 | 镜像屏比例和主屏不一致时要显式建模坐标映射,不能假设 1:1 |
| Screen | 与每个 display 对应的 | |
| Window | 每屏焦点窗口、背景窗口、浮窗、遮挡窗口、hotArea | |
| Focus | 每个 display 的焦点窗口,以及 group 级键盘焦点 | |
输出模拟方案时,先给出模型表,再给出 C++ helper 或伪代码。不要只贴单一固定 。
SetupDualDisplayGroups()Do not start from a fixed demo. First normalize the user's requirements into this model table, then generate the and window injections:
UserScreenInfo| Dimension | Required Content | Key Constraints |
|---|---|---|
| User | | When only foreground users are being simulated, every target |
| DisplayGroup | | There must be one |
| Display | The main, extended, or mirror display's | |
| Mirror | Mirror source display, mirror display, scale factor, or letterbox/crop strategy | When the mirror display has a different aspect ratio from the main display, explicitly model the coordinate mapping; do not assume 1:1 |
| Screen | A | The number and IDs of |
| Window | The focus, background, floating, and occluding windows and hotArea for each display | |
| Focus | The focus window for each display and the group-level keyboard focus | |
When producing a simulation plan, first provide the model table, followed by a C++ helper or pseudocode. Do not paste only a single fixed implementation.
SetupDualDisplayGroups()标准场景模板
Standard Scenario Templates
单显示组:主屏 + 扩展屏 + 主屏镜像屏
Single Display Group: Main Display + Extended Display + Main Display Mirror
用于验证一个用户/一个 display group 内的多屏路由、每屏焦点窗口、镜像屏坐标映射和多窗口层叠:
| 对象 | 建议建模 |
|---|---|
| group | |
| 主屏 | |
| 扩展屏 | |
| 镜像屏 | |
| 焦点窗口 | 每个 display 至少一个 focus window,例如 1000/1100/1200 |
| 层叠窗口 | 每屏背景窗口、状态栏/系统层、浮窗,使用不同 zOrder |
| hotArea | 镜像屏和扩展屏都要配置 |
镜像屏比例不一致时,必须在测试说明中写清楚期望坐标策略:
- 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:
| Object | Suggested Modeling |
|---|---|
| group | |
| Main display | |
| Extended display | |
| Mirror display | |
| Focus window | At least one focus window per display, for example 1000/1100/1200 |
| Stacked windows | A background window, status bar/system layer, and floating window for each display, using different |
| hotArea | Configure |
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 | |
| user B group | |
| 每个 group | 都包含 main/extended/mirror 三类 display |
| displayId | 全局唯一,不同 group 不能复用同一 displayId |
| windowId | 全局唯一,建议按 group/display 分段编号 |
| focus | 每个 group 设置 group 级 focusWindowId;每个 display 都有自己的可命中焦点窗口 |
| mirror mismatch | 每组至少一个镜像屏比例不同,用来验证坐标和命中隔离 |
如果底层 一次只表达一个 user,则按用户分别调用 ,并在报告中说明“多用户均为前台/ACTIVE”的模拟假设。不要把多个用户的窗口混到同一个 group 后声称验证了多用户隔离。
UserScreenInfoUpdateDisplayInfoThis scenario is used to validate display-group isolation under different foreground-user or parallel-foreground scenarios:
| Object | Suggested Modeling |
|---|---|
| user A group | |
| user B group | |
| Each group | Contains main, extended, and mirror displays |
| displayId | Globally unique; the same displayId cannot be reused across different groups |
| windowId | Globally unique; segmented numbering by group/display is recommended |
| focus | Each group has a group-level |
| mirror mismatch | Each group has at least one mirror display with a different aspect ratio to validate coordinate and hit-test isolation |
If the underlying can express only one user at a time, call 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.
UserScreenInfoUpdateDisplayInfo构造显示组(替代 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
| 字段 | 约束 | 后果 |
|---|---|---|
| 必须有一个 | |
| DEFAULT 组 id 必须为 0 | 非 0 会被拒绝 |
| 必须在 DisplayGroupInfo 中预设 | 键盘事件路由依赖此值,不设则为 -1 导致键盘事件无法分发 |
| 必须与 width/height 一致 | 光标边界夹紧依赖此值 |
| 必须与 displaysInfo 对应 | 屏幕信息用于 DPI/PPI 计算 |
| 默认 | 非 ACTIVE 时 displayGroupInfoMap_ 不写入 |
| 全局唯一 | 多组/多用户复用 displayId 会导致路由和 dump 解释混乱 |
| 镜像屏比例 | 必须显式说明映射策略 | 不同比例镜像屏不能直接用主屏坐标断言命中 |
| Field | Constraint | Consequence |
|---|---|---|
| There must be one | |
| The DEFAULT group id must be 0 | A nonzero value is rejected |
| Must be preset in | Keyboard event routing depends on this value; if left unset, it becomes -1 and keyboard events cannot be dispatched |
| Must match width/height | Cursor boundary clamping depends on this value |
Number and IDs of | Must correspond to | Screen information is used for DPI/PPI calculations |
| Defaults to | When it is not ACTIVE, |
| Must be globally unique | Reusing a displayId across groups or users makes routing and dump interpretation ambiguous |
| Mirror-display aspect ratio | The mapping strategy must be explicitly stated | A mirror display with a different aspect ratio cannot be asserted using main-display coordinates directly |
返回码
Return Code
UpdateDisplayInfoUpdateUIExtensionInfoIt is normal for to return -201. This is the error code of and does not affect the display-group data being written.
UpdateDisplayInfoUpdateUIExtensionInfo注入虚拟窗口(替代 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
| 字段 | 约束 |
|---|---|
| 必须匹配目标显示组 ID |
| 必须匹配显示组内的 display ID |
| 用 |
| 首次用 |
| 对 MAIN_GROUPID 生效;非主组从 displayGroupInfoMap_ 读取 |
| 鼠标事件命中测试依赖此区域 |
| 全局唯一 |
| 按业务层级显式区分 |
| Field | Constraint |
|---|---|
| Must match the target display-group ID |
| Must match the display ID within the display group |
| Use |
| Use |
| Takes effect for |
| Mouse-event hit testing depends on this region |
| Must be globally unique; reusing a windowId across displays or groups makes dumps and dispatch results unauditable |
| 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/app | 10 | 默认命中目标 |
| panel/status/system | 100 | 验证系统层优先级 |
| popup/floating | 200+ | 验证重叠命中和 DEL 后回退 |
每屏焦点窗口可以是 background,也可以是专门的输入窗口;需要在模型表中标明。
Construct at least the following for each display:
| Window | zOrder | Purpose |
|---|---|---|
| background/app | 10 | Default hit target |
| panel/status/system | 100 | Verify system-layer priority |
| popup/floating | 200+ | 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);
}UpdateDisplayInfoByIncrementalInfoADDCHANGEDELAn 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);
}UpdateDisplayInfoByIncrementalInfoADDCHANGEDEL动态焦点切换
Dynamic Focus Switching
主组 (groupId=0)
Main Group (groupId=0)
UpdateWindowInfowgi.focusWindowIdUpdateWindowInfowgi.focusWindowId非主组 (groupId!=0)
Non-Main Group (groupId!=0)
UpdateWindowInfofocusWindowIddisplayGroupInfoMap_UpdateDisplayInfocpp
SetupDualDisplayGroups(1000, 2002); // 重新调用 UpdateDisplayInfo
sleep(1s);
InjectVirtualWindows(); // 重新注入窗口(同步 focusWinId 全局变量)
sleep(1s);多屏场景里要区分:
- group 级键盘焦点:由 决定。
DisplayGroupInfo.focusWindowId - 每屏期望焦点窗口:由测试窗口栈和命中坐标体现,不要误认为 MMI 会为每个 display 自动维护独立键盘焦点。
- 如果需求声称“每个屏幕都有自己的焦点窗口”,测试应明确这是窗口栈/命中目标焦点,还是输入框架实际键盘焦点。两者不能混写。
实现模式:使用全局变量跟踪焦点, 和 共享:
SetupDualDisplayGroupsInjectVirtualWindowscpp
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; // 不再硬编码
// ...
}UpdateWindowInfofocusWindowIddisplayGroupInfoMap_UpdateDisplayInfocpp
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 and :
SetupDualDisplayGroupsInjectVirtualWindowscpp
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=200SelectWindowInfopointerHotAreascpp
// 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=200SelectWindowInfopointerHotAreas全屏重叠模式:位置无关验证
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
areapointerHotAreasareapointerHotAreascpp
// 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 内 → 命中此窗口areapointerHotAreasareapointerHotAreascpp
// 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 相对移动无法精确到绝对坐标 | 使用全屏窗口避免依赖光标位置 |
| dump 读 | 通过 |
| 非主组焦点切换 | | 必须通过 |
| DEL 全屏后命中非预期窗口 | 剩余窗口中 zOrder 最高的命中,包括原有非全屏窗口 | 注意已有窗口的 zOrder 关系 |
| Limitation | Cause | Workaround |
|---|---|---|
| Cursor positioning is inaccurate | libinput mouse acceleration makes it impossible to map uinput relative movement precisely to absolute coordinates | Use full-screen windows to avoid depending on cursor position |
| The dump reads | Verify the actual focus through |
| Focus switching in a non-main group | | The focus must be updated through |
| An unexpected window is hit after deleting a full-screen window | The highest zOrder among all remaining windows is hit, including existing non-full-screen windows | Check the zOrder relationships of all windows |
Common Mistakes
Common Mistakes
| 错误 | 后果 | 修复 |
|---|---|---|
不设 | 键盘事件不分发到任何窗口 | 在 DisplayGroupInfo 中预设 |
| | 非 0 组用 |
窗口的 | 事件路由到错误的组 | 确保一致 |
| 多屏/多组复用 displayId 或 windowId | dump 和路由证据不可审计 | 全局唯一编号 |
| 镜像屏比例不同但沿用主屏坐标 | 命中断言错误 | 先定义 fit/stretch/crop 映射策略 |
| 声称每屏独立键盘焦点但只设置 group focus | 结论不成立 | 区分 group 级键盘焦点和每屏窗口命中目标 |
| 非主组通过 UpdateWindowInfo 切焦点 | focusWindowId 被忽略 | 必须通过 UpdateDisplayInfo 切焦点 |
| 所有窗口用相同 zOrder | 命中顺序不确定 | 按业务层级分配不同 zOrder |
| 忘记注入窗口 | 鼠标事件无 hit test 目标 | 每组至少一个全屏窗口 |
| UpdateDisplayInfo 返回 -201 就认为失败 | 实际数据已写入 | -201 来自 UIExtension,可忽略 |
| InjectVirtualWindows 硬编码 focusWindowId | 动态焦点切换后被覆盖 | 使用全局变量同步焦点 |
| DEL 全屏窗口后假设次高全屏命中 | 原有非全屏窗口可能 zOrder 更高 | 检查所有窗口的 zOrder 关系 |
| Mistake | Consequence | Fix |
|---|---|---|
Not setting | Keyboard events are not dispatched to any window | Preset it in |
Using | | Use |
The window's | Events are routed to the wrong group | Ensure that they match |
| Reusing displayId or windowId across displays/groups | Dump and routing evidence is unauditable | Use globally unique IDs |
| Using main-display coordinates when the mirror display has a different aspect ratio | Hit assertions are incorrect | Define the fit/stretch/crop mapping strategy first |
| Claiming independent keyboard focus for each screen while only setting the group focus | The conclusion is invalid | Distinguish group-level keyboard focus from per-screen hit targets |
Switching focus in a non-main group through | | Switch focus through |
| Using the same zOrder for all windows | Hit order becomes indeterminate | Assign different zOrder values according to the business layer |
| Forgetting to inject windows | Mouse events have no hit-test target | Inject at least one full-screen window per group |
Treating an | The data was actually written | -201 comes from UIExtension and can be ignored |
Hard-coding | The value is overwritten after dynamic focus switching | Use global variables to synchronize the focus |
| Assuming the second-highest full-screen window will be hit after DEL | Existing non-full-screen windows may have a higher zOrder | Check the zOrder relationships of all windows |