/** * @file OffscreenSupport.hpp * @brief 离屏验证工具的公共支撑:后备缓冲抓帧、区域哈希、隐藏窗口会话。 * * ⚠️ **为什么要 `#define private public`**:本工具是"白盒验证"—— * 它需要读取后备缓冲区(`Application::m_backBits`)、驱动鼠标消息、 * 直接调用面板几何函数(`PlotView::EdgePanelLayoutFor`)。这些都不是给终端用户用的 * 公开 API,因此不为了测试而污染产品接口,改成在工具侧临时放宽访问级别。 * * ⚠️ **先引入全部标准库 / 系统头,再放宽访问级别**:否则随后被 include 的 * 标准库头里的 `private:` 会被宏替换成 `public:`,造成莫名其妙的编译错误。 * * ⚠️ 窗口一律 `SW_HIDE`:不显示、不抢焦点、不占用户的屏幕与鼠标键盘。 * 抓帧读的是**后备缓冲区**(不是窗口 DC),因此完全不需要窗口可见。 */ #ifndef __OFFSCREENSUPPORT_HPP__ #define __OFFSCREENSUPPORT_HPP__ // ---- 先铺满标准库与系统头 --------------------------------------------------- #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // ---- 临时放宽访问级别,引入被测项目头 -------------------------------------- #define private public #define protected public #include "Application.hpp" #include "DataModel.hpp" #include "PlotView.hpp" #include "PlotViewInternal.hpp" #include "SignalPanel.hpp" #include "TabBar.hpp" #include "Theme.hpp" #include "Toolbar.hpp" #include "UiSurface.hpp" #undef protected #undef private namespace jdi { namespace tools { /** @brief 后备缓冲区里的一块像素矩形(设备像素、客户区坐标)。 */ struct PixelRect { int x = 0; /**< 左边界。 */ int y = 0; /**< 上边界。 */ int width = 0; /**< 宽度。 */ int height = 0; /**< 高度。 */ }; /** * @brief 一帧后备缓冲区像素(top-down 32bpp BGRA)。 * * ⚠️ **不要跨帧缓存 `pixels.data()` 的内部指针**:窗口尺寸 / DPI 变化会重建后备缓冲区; * 每次 `RenderFrame()` 都会重新 `CaptureFrame`。 */ struct Frame { int width = 0; /**< 像素宽。 */ int height = 0; /**< 像素高。 */ int stride = 0; /**< 行字节数。 */ std::vector pixels; /**< BGRA 像素(逐行自上而下)。 */ /** @brief 是否抓到了有效像素。 */ bool Valid() const { return width > 0 && height > 0 && !pixels.empty(); } /** @brief 第 `y` 行的起始地址。 */ const std::uint8_t* Row(const int y) const { return pixels.data() + static_cast(y) * static_cast(stride); } }; /** @brief 从应用后备缓冲区抓一帧(不依赖窗口可见性)。 */ bool CaptureFrame(const Application& app, Frame& frame); /** @brief 计算区域内像素的 FNV-1a 64 位哈希(只混 BGR,忽略 alpha)。 */ std::uint64_t HashRegion(const Frame& frame, const PixelRect& rect); /** @brief 统计区域内"墨迹"像素数(与区域主色不同的像素)。 */ int CountInk(const Frame& frame, const PixelRect& rect); /** @brief 统计两帧在同一区域里差异超过容差的像素数。 */ int CountDiff(const Frame& left, const Frame& right, const PixelRect& rect, int tolerance); /** @brief 把一帧保存成 PNG(路径必须是 ASCII,目录不存在会自动创建)。 */ bool SavePng(const Frame& frame, const std::string& path); /** @brief 递归创建目录(ASCII 路径)。 */ void EnsureDirectory(const std::string& asciiPath); /** @brief 递归删除目录(ASCII 路径,用于"清空临时输出")。 */ void RemoveDirectoryTree(const std::string& asciiPath); /** * @brief 一次离屏会话:隐藏窗口 + 可选的数据文件 + 逐帧抓取。 * * 生命周期:`Start()` → 若干 `Load()` / `RenderFrame()` → `Stop()`。 * ⚠️ `Stop()` 之后**不要**再调用成员函数;进程退出用 `std::_Exit()` 收尾, * 避免单例析构晚于 `GdiplusShutdown`(历史踩过的退出访问违例)。 */ class Session { public: /** @brief 创建隐藏窗口并初始化各组件(不显示、不抢焦点)。 */ bool Start(); /** @brief 关闭窗口并释放 GDI+ 相关资源。 */ void Stop(); /** @brief 加载一个数据文件(同步路径,便于断言)。 */ bool Load(const std::string& path); /** @brief 应用本体引用。 */ Application& App() { return *m_app; } /** @brief 只读应用本体引用。 */ const Application& App() const { return *m_app; } /** @brief 强制整帧重绘并抓帧;返回是否成功。 */ bool RenderFrame(); /** @brief 最近一帧。 */ const Frame& LastFrame() const { return m_lastFrame; } /** @brief 最近一次 `Render()` 的耗时(毫秒)。 */ double LastRenderMs() const { return m_lastRenderMs; } /** @brief 只显示给定信号(其余全部隐藏),并重算视图。 */ void ShowSignals(const std::vector& refs); /** @brief 显示前 `limit` 个信号(跨分组,`limit <= 0` 表示全部)。 */ void ShowFirstSignals(int limit); /** @brief 全部隐藏。 */ void HideAll(); /** @brief 注入一次鼠标移动(客户区坐标),返回组件是否认为状态有变化。 */ bool MouseMove(int x, int y); /** @brief 绘图区矩形(客户区坐标)。 */ gd::RectF PlotArea() const; /** @brief 第 `index` 个网格的波形区域(客户区坐标)。 */ gd::RectF LaneRect(int index) const; /** @brief 当前网格数。 */ int LaneCount() const; /** @brief 客户区尺寸。 */ void ClientSize(int& width, int& height) const; /** @brief 当前加载的路径(窄字符串,仅用于打印)。 */ const std::string& LoadedPath() const { return m_loadedPath; } private: Application* m_app = nullptr; /**< 进程级单例。 */ bool m_started = false; /**< 是否已 `Initialize`。 */ Frame m_lastFrame; /**< 最近一帧像素。 */ double m_lastRenderMs = 0.0; /**< 最近一帧渲染耗时。 */ std::string m_loadedPath; /**< 当前数据文件路径(ASCII 打印用)。 */ }; } // namespace tools } // namespace jdi #endif // __OFFSCREENSUPPORT_HPP__