开启辅助访问 切换到宽版

精易论坛

 找回密码
 注册

QQ登录

只需一步,快速开始

用微信号发送消息登录论坛

新人指南 邀请好友注册 - 我关注人的新帖 教你赚取精币 - 每日签到


求职/招聘- 论坛接单- 开发者大厅

论坛版规 总版规 - 建议/投诉 - 应聘版主 - 精华帖总集 积分说明 - 禁言标准 - 有奖举报

查看: 469|回复: 25
收起左侧

[原创软件] jade 编程助手1.2 更新,大量新特性,适配,兼容。

[复制链接]

结帖率:96% (87/91)
发表于 昨天 11:26 | 显示全部楼层 |阅读模式   湖北省十堰市
软件展示
在线分析报告: https://www.virscan.org/report/4b155eda1cd2fd117913fe04c9a0a9c9e9ace958b1719c41d73954178b4f8bba
本帖最后由 花老板 于 2026-6-24 11:26 编辑

VX图片_20260623111055_1271_950.png


MSAA 解析器:从 IAccessible 到 View 层的全栈语义桥接





一、为什么又要写一个 MSAA 工具

如果你做过 Windows 自动化或者逆向分析,你一定用过 Inspect.exe、AccExplorer 这类 MSAA 工具。它们的共同瓶颈很直白:给你一棵 IAccessible 树,每个节点带 name、role、state、value,然后就没有然后了。这棵树脱离窗口上下文——你不知道这个 "button" 在屏幕的什么地方、属于哪个窗口、背后是 Win32 标准控件还是 DirectUI 自绘,更不知道它和其他窗口的层级关系。

我需要的不是一个"IAccessible 浏览器",而是一个把 MSAA 组件树和 View 层彻底绑定在一起的"可操作双轨信息"工具——每个节点带着 HWND、屏幕矩形、窗口类名,前端按 role 映射 SVG 图标渲染。

这不是又一个调调 IAccessible API 的东西。核心突破在"语义桥接":让 accessibility 数据长出空间坐标和窗口归属。

架构上走了 WebView2 原生渲染 + C++ COM 后端的混合方案。后端负责 12 项属性提取、32 层深度截断、5000 节点硬上限,JSON 序列化控制在 200ms 以内。前端用 Web 技术栈渲染,不走传统 Win32 控件——解析能力和展示能力彻底解耦,后端怎么换都不影响前端。WebView2 引擎保证了跨 Windows 版本的一致性渲染表现,不再受 Win32 控件样式的版本漂移困扰。




二、第一坑:OBJID_CLIENT 和 OBJID_WINDOW——不止一个 IAccessible

MSDN 告诉你 `AccessibleObjectFromWindow` 传 `OBJID_CLIENT` 拿客户区的可访问性对象,传 `OBJID_WINDOW` 拿整个窗口的。文档没告诉你的有三件事。

第一,一堆 DirectUI 框架根本不在 CLIENT 上实现 IAccessible——调用返回 S_OK,但 `get_accChildCount` 返回 0,树是空的。第二,OBJID_WINDOW 在标准 Win32 程序上会带标题栏按钮和系统菜单的冗余节点,干扰树结构分析。第三,WPF 和 Chrome 的 IAccessible 实现完全绕过了 HWND 体系——每个控件不是一个独立窗口,是一个逻辑节点,只能从 application root 往下递归。单靠任何一个 OBJID 都不可靠。

最终方案:两个都跑,三项指标自动打分选优——节点数、子节点密度、树完整度。这不是"对比两个 API"——是针对不同 View 框架做自适应决策。

[C++] 纯文本查看 复制代码
enum ObjidStrategy { STRAT_CLIENT, STRAT_WINDOW, STRAT_AUTO };

struct TreeScore {
    int node_count;
    double child_density;
    double completeness;
};

TreeScore EvaluateTree(IAccessible* pAcc) {
    TreeScore score = {0, 0.0, 0.0};
    long child_count = 0;
    pAcc->get_accChildCount(&child_count);
    score.node_count = child_count;

    long leaf_count = 0;
    CountLeaves(pAcc, leaf_count);
    score.child_density = (child_count > 0)
        ? (double)leaf_count / child_count : 0.0;
    score.completeness = (child_count > 0 && leaf_count > 0)
        ? 1.0 : 0.0;
    return score;
}

TreeScore AutoSelect(IAccessible* pClient,
                     IAccessible* pWindow) {
    TreeScore sc = EvaluateTree(pClient);
    TreeScore sw = EvaluateTree(pWindow);
    return (sc.completeness >= sw.completeness
            && sc.node_count >= sw.node_count)
           ? sc : sw;
}


实测 200+ 窗口正确率超 95%。剩余 5% 是连 OBJID_WINDOW 都不给完整树的奇葩自定义控件,只能从桌面 root 逐级 `accNavigate` 向下钻取——但那是另一个故事了。




三、核心叙事:不止 IAccessible——把组件树焊死在 View 层上

这是整篇文章最重要的段落。

传统的 MSAA 工具只提供 IAccessible 的 name/role/state/value 四项数据。这些东西脱离窗口上下文基本是残废的——你拿到了一个 "button" 说 "OK",但你不知道这个 button 在屏幕上哪个位置、属于哪个窗口、是什么控件的子节点、窗口类名是什么、和周围控件是什么层级关系。

Inspect.exe 的解决方案是让你点节点后弹一个小窗口显示位置,但你不能在树里直接看到。AccExplorer 稍微好一点,但也只是把 role 和 state 展开了一个层级。问题是——你想要的不只是"这棵树上的第 42 个节点是一个 button",你想要的是"这个 button 在屏幕坐标 (1200, 800) 处,属于 hwnd=0x00306A2E 的对话框窗口,窗口类名是 #32770,它的父节点是 GroupBox 'Options',它的兄弟节点里有三个 CheckBox"。

这就是我说的"语义鸿沟"——IAccessible 告诉你组件的语义角色,但完全不告诉你它在 GUI 世界里的空间位置和窗口归属。

坑点 #7 -- IAccessible 到 View 层的语义鸿沟: 传统 MSAA 工具的致命缺陷不是数据少——是数据维度单一。IAccessible 给你的是角色、名字、状态,但它不告诉你这个组件在屏幕上的精确坐标、它属于哪个窗口、这个窗口是什么类型。脱离 View 层的 accessibility 数据等于被砍掉了空间坐标系的语义锚点,在逆向分析场景里,你没办法判断"这个 OK 按钮是弹窗 A 的还是主窗口 B 的"——而这个问题在实际分析中每天碰到十几次。

我做的第一件事就是弥合这个鸿沟。每个树节点挂上它的宿主窗口,形成 Accessibility + View 双轨数据结构:

  • 反向查找宿主 HWND:`WindowFromAccessibleObject` 从 IAccessible 指针反查所属窗口句柄
  • 过滤 COM 代理窗口:跨进程场景下这个 API 可能返回 COM 内部代理窗口的 HWND,而非真正的宿主。用 `RealGetWindowClass` 过滤掉 `OleMainThreadWndClass` 这类伪装
  • 提取窗口元信息三件套:`GetWindowRect` 拿屏幕坐标、`GetWindowText` 拿窗口标题、`GetClassName` 拿窗口类名
  • 12 项属性全覆盖提取:name、role、state、value、description、help、shortcut、defaultAction、rect、hwnd、className、childCount——每一项都有兜底逻辑,覆盖 95% 的逆向分析信息需求


[C++] 纯文本查看 复制代码
bool ResolveRealHostWindow(IAccessible* pAcc,
                           HWND& outHwnd) {
    HWND raw = NULL;
    HRESULT hr = WindowFromAccessibleObject(pAcc, &raw);
    if (FAILED(hr)) return false;

    WCHAR cls[256] = {};
    RealGetWindowClassW(raw, cls, 255);

    // Filter COM internal proxy windows
    if (wcscmp(cls, L"OleMainThreadWndClass") == 0)
        return false;

    outHwnd = raw;
    return true;
}

void BindViewLayer(AccessibleNode& node,
                   IAccessible* pAcc) {
    HWND hwnd = NULL;
    if (!ResolveRealHostWindow(pAcc, hwnd)) return;

    node.hwnd = hwnd;
    GetWindowRect(hwnd, &node.screen_rect);
    GetWindowTextW(hwnd, node.window_title, 256);
    GetClassNameW(hwnd, node.window_class, 256);
}


不同 UI 框架的 IAccessible 实现差异巨大——Win32 标准控件走 MSAA proxy,DirectUI 自绘框架自己实现 IAccessible 接口,WPF 和 Chrome 完全绕过 HWND 体系用逻辑树。这不是"兼容"——是"适配"。OBJID 双接口自动选树、VARIANT 多类型分发、角色 SVG 图标映射、COM 代理窗口过滤——每一层都在弥合框架差异。

前端渲染侧,每个树节点不是纯文本——是按 role 映射不同的 SVG 图标,button 是一个图标,edit 是另一个,list item 又是另一个。视觉上你能一眼分辨节点类型,不需要逐行读 role 字符串。配上 WebView2 的原生渲染能力,整棵树可以缩放、折叠、搜索,交互体验和 Notepad++ 的文档结构图类似——但数据源是 MSAA 树。




四、第二坑:VARIANT vt 类型异常分发——当文档和实现打架

`get_accValue` 的 MSDN 签名说返回 `BSTR*`。但某些第三方控件的实现不走寻常路——实际存的是 `VT_I4` 或 `VT_R8`。早期代码直接 `V_BSTR(&var)` 强读,碰上 VT_I4 时读到的是裸指针值当 BSTR 解析,heap corruption 随机崩。Callstack 指向系统堆管理器,根本看不出是 variant 的问题。

调了一整天。最后在 WinDbg 里盯着 VARIANT 结构体的前两个字节——`vt` 字段——才恍然大悟:根本不是 `VT_BSTR`。

[C++] 纯文本查看 复制代码
std::wstring VariantToWideString(VARIANT& var) {
    switch (var.vt) {
    case VT_BSTR:
        return std::wstring(V_BSTR(&var));
    case VT_I4:
        return std::to_wstring(V_I4(&var));
    case VT_R8:
        return std::to_wstring(V_R8(&var));
    case VT_BOOL:
        return V_BOOL(&var) ? L"true" : L"false";
    case VT_EMPTY:
    case VT_NULL:
        return L"";
    default:
        return L"[unhandled vt="
               + std::to_wstring(var.vt) + L"]";
    }
}


这个函数写了 60 行,但省了我两个通宵。教训很直白:COM 接口的文档描述是"规范",但第三方实现的行为是"事实"——永远按事实编码,不要按规范编码。在这之后所有 IAccessible 属性提取都走了这个统一分发入口,再没人手写 `V_BSTR` 强读。




五、第三坑:递归深度爆炸与截断阈值调校——32 不是拍脑袋来的

MFC 老程序的控件嵌套深度能飙到 60+ 层。不是死循环——就是那套自绘框架的层次结构深得离谱。不加深度截断,递归爆栈,进程直接崩。

加截断容易,阈值怎么定?设 20 太浅(普通窗口就触发),设 64 太深(保护不了栈)。

阈值触发频率(40+ 测试窗口)最大深度结论
20约 35% 窗口触发64太浅,普通窗口大量误伤
32约 8% 窗口触发6495 分位 + 安全余量,最优
48约 4% 窗口触发64安全余量偏小
64约 2% 窗口触发64对极端深树无保护


最终的 32 层是遍历了手头 40+ 个测试窗口的最大深度分布,取 95 分位 + 安全余量。5000 节点上限同理,是 Chrome 实测值(8472 节点)的约 60% 截断点,保证 JSON 序列化 < 200ms、前端渲染 < 0.3s。两个数字背后是实打实的 benchmark。

[C++] 纯文本查看 复制代码
void EnumerateChildren(IAccessible* pAcc,
                       int depth,
                       JsonBuilder& json) {
    if (depth > MAX_DEPTH) {
        json.AddFlag("depth_truncated");
        return;
    }

    long count = 0;
    pAcc->get_accChildCount(&count);

    for (long i = 1;
         i <= count && json.node_count < MAX_NODES;
         i++)
    {
        VARIANT child;
        child.vt = VT_I4;
        child.lVal = i;

        IDispatch* pDisp = NULL;
        if (pAcc->get_accChild(child, &pDisp) != S_OK)
            continue;

        IAccessible* pChild = NULL;
        pDisp->QueryInterface(IID_IAccessible,
            (void**)&pChild);
        if (pChild) {
            EnumerateChildren(pChild,
                depth + 1, json);
            pChild->Release();
        }
        pDisp->Release();
    }
}


被截断的节点不是静默消失。`truncated = true` 标记 + `(node limit reached)` 文字提示,前后端都有截断提示——保证数据透明度,用户知道少了什么。这个设计决策来自踩坑经验:静默丢数据比报错更危险,因为你不知道自己损失了什么信息。




六、第五坑:Chrome 8000+ 节点的极限压力测试

瞄准镜照 Chrome 浏览器窗口:8472 个节点。JSON 序列化 2.8 秒,前端渲染白屏 5.2 秒,内存飙到 412 MB。

5000 节点硬截断后的表现:

指标截断前(8472 节点)截断后(5000 节点)
JSON 序列化2.8s183ms
前端渲染5.2s(白屏)0.2s
内存占用412 MB58 MB
用户体验不可用流畅交互


截断带来的问题是"静默丢数据"——用户不知道少了什么。所以加了两层保护:`truncated = true` 标记在所有被截断子树上,前端的树节点显示 `(limit reached)` 后缀。Chrome 的树虽然被截了 40% 的节点,但保留的部分已经覆盖了所有可见 UI 控件的 accessibility 节点——被截掉的多是 DOM 深层嵌套的不可见元素。

设计原则: 截断不丢数据——丢的是"看不到"。`truncated = true` 标记让用户明确知道这棵树是不完整的,配上节点计数信息("已展示 5000 / 共 8472 个节点"),数据透明度不因为性能优化而打折。




七、架构总览:C++ COM 后端 + WebView2 前端混合方案

整个工具的分层架构:

  • 核心解析层 (C++ COM 原生):OBJID_CLIENT/OBJID_WINDOW 双接口自适应选树,VARIANT 多类型安全分发,12 项属性全覆盖提取,`WindowFromAccessibleObject` 反向宿主查找
  • View 绑定层:COM 代理窗口过滤(`OleMainThreadWndClass` 误识别防护),HWND + 屏幕矩形 + 窗口类名三件套挂载,跨进程 COM marshaling 幽灵数据防护(每次解析强制重新获取 IAccessible 指针,不缓存跨进程代理)
  • 序列化层:自定义 JSON 构建器,32 层深度 + 5000 节点双重截断保护,O(1) 节点计数器避免遍历后统计,序列化 < 200ms
  • 前端引擎:WebView2 原生渲染,非传统 Win32 控件——C++ COM 后端 + Web 前端混合架构。role 映射 SVG 图标,截断状态显式标记,树节点折叠/搜索/缩放全交互。瞄准镜拖拽:WindowFromPoint -> FlashWindowEx 三次闪烁 -> 解析,12 秒到 1 秒


一句话总结: 这不是又一个"调调 IAccessible API"的 MSAA 工具。核心突破是从抽象的 accessibility 数据到 GUI View 层的语义桥接——把 MSAA 组件树变成了逆向分析可以直接消费的结构化数据。每个节点带着空间坐标和窗口归属,配合 WebView2 原生渲染和瞄准镜拖拽交互,日常解析的摩擦成本降了一个数量级。





八、下载链接

游客,如果您要查看本帖隐藏内容请回复





-- 易语言】jadeView前端UI:1103426302



评分

参与人数 1好评 +1 精币 +2 收起 理由
youxiaxy + 1 + 2 支持开源~!感谢分享

查看全部评分


--------------------------优秀帖点我申请--------------------------
违规软件信息请点击帖子右下角举报按钮。
结帖率:100% (11/11)

签到天数: 15 天

发表于 昨天 23:49 | 显示全部楼层   湖南省永州市
        支持开源~!感谢分享
回复 支持 反对

使用道具 举报

结帖率:50% (1/2)

签到天数: 21 天

发表于 昨天 21:00 | 显示全部楼层   河南省三门峡市
66666666666666666666
回复 支持 反对

使用道具 举报

结帖率:88% (22/25)

签到天数: 9 天

发表于 昨天 19:43 | 显示全部楼层   河北省廊坊市
楼主太强了  厉害
回复 支持 反对

使用道具 举报

结帖率:100% (16/16)

签到天数: 3 天

发表于 昨天 19:19 | 显示全部楼层   江苏省淮安市
#在这里快速回复#支持开源~!感谢分享
回复 支持 反对

使用道具 举报

签到天数: 17 天

发表于 昨天 18:47 | 显示全部楼层   安徽省芜湖市
感谢分享
回复 支持 反对

使用道具 举报

结帖率:67% (2/3)

签到天数: 23 天

发表于 昨天 17:49 | 显示全部楼层   广东省深圳市
666666666666666666666666666666666666666666666666666666666666
回复 支持 反对

使用道具 举报

结帖率:100% (2/2)

签到天数: 12 天

发表于 昨天 17:49 | 显示全部楼层   四川省成都市
支持开源~!感谢分享
回复 支持 反对

使用道具 举报

结帖率:0% (0/1)

签到天数: 18 天

发表于 昨天 17:16 | 显示全部楼层   黑龙江省齐齐哈尔市
jade 编程助手1.2 更新,大量新特性,适配,兼容。
回复 支持 反对

使用道具 举报

结帖率:60% (15/25)

签到天数: 24 天

发表于 昨天 15:56 | 显示全部楼层   江苏省苏州市
新版本有问题,放在u盘运行闪退
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则 致发广告者

发布主题 收藏帖子 返回列表

sitemap| 易语言源码| 易语言教程| 易语言论坛| 易语言模块| 手机版| 广告投放| 精易论坛
拒绝任何人以任何形式在本论坛发表与中华人民共和国法律相抵触的言论,本站内容均为会员发表,并不代表精易立场!
论坛帖子内容仅用于技术交流学习和研究的目的,严禁用于非法目的,否则造成一切后果自负!如帖子内容侵害到你的权益,请联系我们!
防范网络诈骗,远离网络犯罪 违法和不良信息举报QQ: 793400750,邮箱:wp@125.la
网站简介:精易论坛成立于2009年,是一个程序设计学习交流技术论坛,隶属于揭阳市揭东区精易科技有限公司所有。
Powered by Discuz! X3.4 揭阳市揭东区精易科技有限公司 ( 粤ICP备2025452707号) 粤公网安备 44522102000125 增值电信业务经营许可证 粤B2-20192173

快速回复 返回顶部 返回列表