Agent Skills: WMS/DMS 模拟

Use when testing OpenHarmony input features that depend on window/display state without running real WMS/DMS. Builds requested simulation scenarios from display groups, users, main/extended/mirror displays, per-display focus windows, z-order window stacks, hot areas, dynamic focus switching, pointer style, capture mode, scroll wheel, and device binding.

UncategorizedID: openharmonyinsight/openharmony-skills/ohos-dev-distributed-mmi-wms-dms-simulation

Install this agent skill to your local

pnpm dlx add-skill https://github.com/openharmonyinsight/openharmony-skills/tree/HEAD/skills/ohos-dev-distributed-mmi-wms-dms-simulation

Skill Files

Browse the full folder contents for ohos-dev-distributed-mmi-wms-dms-simulation.

Download Skill

Loading file tree…

skills/ohos-dev-distributed-mmi-wms-dms-simulation/SKILL.md

Skill Metadata

Name
ohos-dev-distributed-mmi-wms-dms-simulation
Description
Use when testing OpenHarmony input features that depend on window/display state without running real WMS/DMS. Builds requested simulation scenarios from display groups, users, main/extended/mirror displays, per-display focus windows, z-order window stacks, hot areas, dynamic focus switching, pointer style, capture mode, scroll wheel, and device binding.

WMS/DMS 模拟

测试进程直接通过 InputManager IPC 构造显示组和窗口,替代 WMS(窗口管理服务)和 DMS(显示管理服务),实现输入子系统的独立功能验证。

前置 skill: 权限获取、server 绕过、编译部署、hidumper 捕获等基础设施见 ohos-dev-distributed-mmi-device-test-harness;虚拟设备创建和事件注入见 ohos-dev-distributed-mmi-uinput-virtual-device。

When to Use

  • 按测试要求模拟显示组、用户、主屏、扩展屏、镜像屏和窗口栈
  • 测试依赖显示组/窗口状态的输入功能(设备绑定、光标路由、焦点分发、命中测试)
  • 验证单显示组多屏、多显示组多用户、镜像屏比例不一致、前台用户切换等场景
  • 不想或不能启动完整 WMS/DMS 栈时

已验证能力摘要

DAYU200 真机 dual_group_interleave_test 已覆盖双显示组构造、多窗口注入、per-group 光标/按钮/键盘/焦点/captureMode/滚轮隔离、热拔插绑定清理、跨绑定按键状态和 per-window 光标外观。详细步骤、dump 证据和 PARTIAL 项见 evals/test-results.md,不要把长篇实测表格复制到 SKILL.md 主流程。

Scenario-First 建模流程

不要从固定 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()。

标准场景模板

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

用于验证一个用户/一个 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 坐标后再注入事件或验证目标窗口。

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

用于验证不同前台用户或并行前台场景下的显示组隔离:

| 对象 | 建议建模 | |------|----------| | 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 后声称验证了多用户隔离。

构造显示组(替代 DMS)

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

#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);

关键约束

| 字段 | 约束 | 后果 | |------|------|------| | 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 解释混乱 | | 镜像屏比例 | 必须显式说明映射策略 | 不同比例镜像屏不能直接用主屏坐标断言命中 |

返回码

UpdateDisplayInfo 返回 -201 是正常的 — 这是 UpdateUIExtensionInfo 的错误码,不影响显示组数据的写入。

注入虚拟窗口(替代 WMS)

每个 display 至少需要一个可命中的窗口。复杂场景应为每个 display 注入自己的背景窗口、焦点窗口和需要验证的层叠窗口:

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);

关键约束

| 字段 | 约束 | |------|------| | win.groupId | 必须匹配目标显示组 ID | | win.displayId | 必须匹配显示组内的 display ID | | win.pid | 用 getpid() — 权限检查需要 | | win.action | 首次用 ADD,更新用 CHANGE | | wgi.focusWindowId | 对 MAIN_GROUPID 生效;非主组从 displayGroupInfoMap_ 读取 | | pointerHotAreas | 鼠标事件命中测试依赖此区域 | | win.id | 全局唯一 | 多屏/多组复用 windowId 会让 dump 和分发结果不可审计 | | zOrder | 按业务层级显式区分 | 所有窗口同 zOrder 时命中顺序不可作为证据 |

每屏窗口栈模式

为每个 display 建议至少构造:

| 窗口 | zOrder | 作用 | |------|-------:|------| | background/app | 10 | 默认命中目标 | | panel/status/system | 100 | 验证系统层优先级 | | popup/floating | 200+ | 验证重叠命中和 DEL 后回退 |

每屏焦点窗口可以是 background,也可以是专门的输入窗口;需要在模型表中标明。

增量窗口操作

单个窗口的 ADD/CHANGE/DEL,无需重新注入全部窗口:

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 从列表移除。

动态焦点切换

主组 (groupId=0)

UpdateWindowInfo 使用传入的 wgi.focusWindowId,直接生效。

非主组 (groupId!=0)

UpdateWindowInfo 忽略传入的 focusWindowId,始终从 displayGroupInfoMap_ 读取。必须通过 UpdateDisplayInfo 更新焦点:

SetupDualDisplayGroups(1000, 2002);  // 重新调用 UpdateDisplayInfo
sleep(1s);
InjectVirtualWindows();               // 重新注入窗口(同步 focusWinId 全局变量)
sleep(1s);

多屏场景里要区分:

  • group 级键盘焦点:由 DisplayGroupInfo.focusWindowId 决定。
  • 每屏期望焦点窗口:由测试窗口栈和命中坐标体现,不要误认为 MMI 会为每个 display 自动维护独立键盘焦点。
  • 如果需求声称“每个屏幕都有自己的焦点窗口”,测试应明确这是窗口栈/命中目标焦点,还是输入框架实际键盘焦点。两者不能混写。

实现模式:使用全局变量跟踪焦点,SetupDualDisplayGroups 和 InjectVirtualWindows 共享:

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 命中测试

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

// 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 高的优先。

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

ADD 多个全屏窗口(不同 zOrder),无需光标定位 — 全屏窗口在任意光标位置都命中:

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)。

热区穿透

area 和 pointerHotAreas 可以不同。光标在 area 内但 pointerHotAreas 外时,点击穿透到下层窗口:

// 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)

// 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

// 设备绑定(详见 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);

执行顺序

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)

已知限制

| 限制 | 原因 | 规避方案 | |------|------|---------| | 光标定位不精确 | libinput 鼠标加速导致 uinput 相对移动无法精确到绝对坐标 | 使用全屏窗口避免依赖光标位置 | | focusWindowId=-1 在 DisplayGroups dump 中 | dump 读 displayGroupInfoMap_,MAIN_GROUPID 的焦点从 UpdateWindowInfo 传入 | 通过 KeyboardStateByGroup 或 SequenceSnapshots 验证实际焦点 | | 非主组焦点切换 | UpdateWindowInfo 忽略非主组 focusWindowId | 必须通过 UpdateDisplayInfo 更新 | | DEL 全屏后命中非预期窗口 | 剩余窗口中 zOrder 最高的命中,包括原有非全屏窗口 | 注意已有窗口的 zOrder 关系 |

Common Mistakes

| 错误 | 后果 | 修复 | |------|------|------| | 不设 focusWindowId | 键盘事件不分发到任何窗口 | 在 DisplayGroupInfo 中预设 | | groupId != 0 的组用 GROUP_DEFAULT | InitDisplayGroupInfo 拒绝 | 非 0 组用 GROUP_SPECIAL | | 窗口的 groupId 与显示组不匹配 | 事件路由到错误的组 | 确保一致 | | 多屏/多组复用 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 关系 |