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

star 28

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.

openharmonyinsight By openharmonyinsight schedule Updated 6/8/2026

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. metadata: author: openharmony scope: domain stage: development domain: mmi capability: wms-dms-simulation version: 0.2.0 status: draft tags: - multimodalinput - wms-simulation - display-group - window-management - z-order related-skills: - ohos-dev-distributed-mmi-device-test-harness - ohos-dev-distributed-mmi-uinput-virtual-device


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 groupIdtypemainDisplayIdfocusWindowId 必须有一个 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 全局唯一
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 自动维护独立键盘焦点。
  • 如果需求声称“每个屏幕都有自己的焦点窗口”,测试应明确这是窗口栈/命中目标焦点,还是输入框架实际键盘焦点。两者不能混写。

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

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

热区穿透

areapointerHotAreas 可以不同。光标在 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 传入 通过 KeyboardStateByGroupSequenceSnapshots 验证实际焦点
非主组焦点切换 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 关系
Install via CLI
npx skills add https://github.com/openharmonyinsight/openharmony-skills --skill ohos-dev-distributed-mmi-wms-dms-simulation
Repository Details
star Stars 28
call_split Forks 5
navigation Branch main
article Path SKILL.md
More from Creator
openharmonyinsight
openharmonyinsight Explore all skills →