Files
datatrace/source/interface.cpp
2026-07-14 17:02:23 +08:00

4183 lines
223 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#define NOMINMAX
#ifndef UNICODE
#define UNICODE
#endif
#ifndef _UNICODE
#define _UNICODE
#endif
#include "pq_app.h"
#include "json.hpp"
#include "tinyxml2.h"
#include <windows.h>
#include <commctrl.h>
#include <commdlg.h>
#include <algorithm>
#include <cctype>
#include <cwctype>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <map>
#include <memory>
#include <regex>
#include <set>
#include <sstream>
#include <unordered_map>
#include <unordered_set>
/*
* interface.cpp
*
* 本文件实现 Win32 桌面界面和页面交互,是用户操作到业务函数的“调度层”。
*
* 页面结构:
* 1. 配置项页填写前置、接口、MQ Topic/Tag/Key 等配置。
* 2. 台账列表页:获取台账/ICD选择装置和测点执行初始化 MQ、数据追踪、一键解析。
* 3. JSON 详情页展示已发送追踪请求、DATATopic jsondata、TraceTopic origin。
* 4. 表格预览页:读取 main.cpp 输出的 Excel XML按状态/相位/搜索过滤,可编辑配置列。
* 5. XML 预览页:根据预览页编辑结果反向写回 XML并支持导出。
*
* 关键交互流程:
* runGetLedger() -> pqFetchLedgerAndIcd() -> populateTree()/updatePathsForSelection()
* runTrace() -> pqSendTraceRequest() -> MQ 回调 addJsonEvent() -> autoParseTraceDir()
* runParse() -> pqRunParser() -> refreshPreviewFromWorkbook()
* runReverseXml()-> generateReverseXml() -> XML 预览页
*
* 阅读本文件时可留意的 C++/Win32 语法:
* - HWND 是窗口/控件句柄,控件并非 C++ 对象GetDlgItem、SendMessageW 按句柄操作它们;
* - wndProc 是 CALLBACK 函数Windows 通过 UINT msg 把 WM_COMMAND/WM_NOTIFY 等事件送入;
* - WPARAM/LPARAM 是随消息携带的整数或指针,必须按具体消息约定转换后使用;
* - AppState 集中保存界面模型避免依赖大量全局变量unique_ptr 表示独占所有权;
* - [&, hwnd](...) 形式是 lambda 捕获列表:& 捕获局部引用hwnd 按值保存句柄;
* - BusyPopupGuard 展示 RAII构造时进入忙碌状态析构时自动清理异常路径也安全
* - MQ 回调线程不直接操作控件,而是 PostMessage 给 GUI 线程,符合 Win32 线程约束。
*/
namespace {
using json = nlohmann::json;
namespace fs = std::filesystem;
// 所有控件 ID 集中在这里WM_COMMAND/WM_NOTIFY 根据这些 ID 路由事件。
enum ControlId {
IDC_TAB = 900,
IDC_PAGE_CONFIG,
IDC_PAGE_LEDGER,
IDC_PAGE_JSON,
IDC_PAGE_PREVIEW,
IDC_PAGE_XML_PREVIEW,
IDC_FRONT_INST = 1001,
IDC_FRONT_IP,
IDC_TERMINAL_STATUS,
IDC_MONITOR_STATUS,
IDC_ICD_FLAG,
IDC_WEB_DEVICE,
IDC_WEB_ICD,
IDC_WEB_FILEDOWNLOAD,
IDC_XML_SAVE_DIR,
IDC_PRODUCER_ACCESS_KEY,
IDC_PRODUCER_SECRET_KEY,
IDC_CONSUMER_ACCESS_KEY,
IDC_CONSUMER_SECRET_KEY,
IDC_PRODUCER,
IDC_IPPORT,
IDC_TOPIC_LOG,
IDC_TAG_LOG,
IDC_KEY_LOG,
IDC_TOPIC_ASK,
IDC_TAG_ASK,
IDC_KEY_ASK,
IDC_CONSUMER,
IDC_CONSUMER_IPPORT,
IDC_DATA_STAT_TOPIC,
IDC_DATA_REALTIME_TOPIC,
IDC_DATA_STAT_TAG,
IDC_DATA_STAT_KEY,
IDC_DATA_REALTIME_TAG,
IDC_DATA_REALTIME_KEY,
IDC_TRACE_TOPIC,
IDC_TRACE_TAG,
IDC_TRACE_KEY,
IDC_XML_PATH,
IDC_JSON_PATH,
IDC_ORIGIN_PATHS,
IDC_OUTPUT_PATH,
IDC_FRONT_TYPE,
IDC_TREE,
IDC_DETAIL,
IDC_LOG,
IDC_JSON_TREE,
IDC_JSON_DETAIL,
IDC_JSON_MONITOR,
IDC_PREVIEW_STATUS,
IDC_PREVIEW_WORKBOOK,
IDC_PREVIEW_SHEET,
IDC_PREVIEW_FILTER,
IDC_PREVIEW_PHASE,
IDC_PREVIEW_ZOOM,
IDC_PREVIEW_SEARCH,
IDC_PREVIEW_SEARCH_BUTTON,
IDC_PREVIEW_LIST,
IDC_IMPORT_ICD,
IDC_REVERSE_XML,
IDC_XML_SEARCH,
IDC_XML_SEARCH_BUTTON,
IDC_XML_PREVIEW_EDIT,
IDC_EXPORT_XML,
IDC_PREVIEW_CELL_EDIT,
IDC_GET_LEDGER = 2001,
IDC_INIT_MQ,
IDC_TRACE,
IDC_PARSE,
IDC_BROWSE_XML,
IDC_BROWSE_JSON,
IDC_ADD_ORIGIN
};
struct NodeRef {
int device = -1;
int monitor = -1;
};
// JSON 树中的一条记录,可能来自 sent/data/trace 三种来源。
struct JsonItem {
std::string kind;
std::string title;
std::string body;
std::string path;
std::string dataType;
std::string monitorId;
std::string traceDir;
};
struct JsonNodeRef {
int index = -1;
};
struct JsonMonitorChoice {
std::string label;
std::string traceDir;
int device = -1;
int monitor = -1;
};
struct PreviewSheetRow {
std::vector<std::string> cells; // 当前可见/可编辑的单元格文本。
std::vector<std::string> originalCells; // 从工作簿读出的原始单元格,用于判断逆向生成时哪些行被改过。
std::vector<std::string> cellStyles; // Excel XML 中的 ss:StyleID用于 GUI 着色。
std::string phase; // A/B/C/T用于相位过滤和同步编辑。
int status = 0; // 0 空白1 正常2 异常3 缺失4 表头5 分段6 子分组。
bool finalRangeChanged = false; // 字段数组范围因表格参数修改而变化,字段格显示深红色。
int modifiedOriginPickColumn = -1; // 修改后的原始值取值起点列GUI 用紫色标记。
};
struct PreviewSheet {
std::string name;
std::vector<PreviewSheetRow> rows;
size_t maxCols = 0;
};
struct PreviewWorkbookChoice {
std::string label;
std::string path;
};
struct AppState {
HINSTANCE inst = nullptr;
HWND hwnd = nullptr;
HWND tab = nullptr;
HWND pageConfig = nullptr;
HWND pageLedger = nullptr;
HWND pageJson = nullptr;
HWND pagePreview = nullptr;
HWND pageXmlPreview = nullptr;
HWND tree = nullptr;
HWND detail = nullptr;
HWND log = nullptr;
HWND jsonTree = nullptr;
HWND jsonDetail = nullptr;
HWND jsonMonitor = nullptr;
HWND previewStatus = nullptr;
HWND previewWorkbook = nullptr;
HWND previewSheet = nullptr;
HWND previewFilter = nullptr;
HWND previewPhase = nullptr;
HWND previewZoom = nullptr;
HWND previewSearch = nullptr;
HWND previewList = nullptr;
HWND importIcd = nullptr;
HWND reverseXml = nullptr;
HWND xmlSearch = nullptr;
HWND xmlPreview = nullptr;
HWND exportXml = nullptr;
HWND previewEdit = nullptr;
HWND busyPopup = nullptr;
HWND frontType = nullptr;
HFONT font = nullptr;
HFONT previewFont = nullptr;
HFONT xmlFont = nullptr;
bool ownsFont = false;
std::vector<PqLedgerDevice> devices; // 当前台账数据,左侧树和追踪请求都从这里取装置/测点。
std::vector<std::unique_ptr<NodeRef>> refs;
std::vector<JsonItem> jsonItems;
std::vector<std::unique_ptr<JsonNodeRef>> jsonRefs;
std::vector<JsonMonitorChoice> jsonMonitorChoices;
std::vector<PreviewWorkbookChoice> previewWorkbooks;
std::vector<PreviewSheet> previewSheets;
std::string activeTraceDir; // 当前测点追踪目录,用于过滤 JSON 记录和自动解析。
std::string activePreviewWorkbook; // 当前表格预览使用的 .xls。
std::string pendingJsonMonitorTraceDir;
std::string generatedXmlText;
std::string generatedXmlSourcePath;
std::string previewSearchText;
std::string xmlSearchText;
std::string xmlSearchLastText;
size_t xmlSearchNextPos = 0;
bool autoPreviewEnabled = false;
bool jsonMonitorRefreshing = false;
bool jsonMonitorRefreshPending = false;
bool reverseXmlDirty = true; // 预览表编辑后置 trueXML 预览/搜索前会重新生成。
bool updatingXmlPreview = false; // SetWindowText 会触发 EN_CHANGE用它区分自动刷新和人工编辑。
bool xmlPreviewEdited = false;
bool autoParseBusy = false; // MQ 数据到达后自动解析时显示忙碌提示。
int activePreviewSheet = 0;
int previewZoomPercent = 100;
int previewStatusFilter = -1;
std::string previewPhaseFilter;
int editPreviewRow = -1;
int editPreviewCol = -1;
int editPreviewTopModelRow = -1;
int editPreviewHorzPos = 0;
int selectedDevice = -1;
int selectedMonitor = -1;
int selectedJson = -1;
};
AppState* g_app = nullptr;
constexpr UINT WM_PQ_MQ_EVENT = WM_APP + 101;
constexpr UINT WM_PQ_START_PREVIEW_EDIT = WM_APP + 102;
/**
* Function: u8w
*
* @brief 实现界面层的 “u8w” 功能,并在 Win32 控件与 AppState 业务模型之间传递数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param s 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。
*/
std::wstring u8w(const std::string& s) {
if (s.empty()) return L"";
int n = MultiByteToWideChar(CP_UTF8, 0, s.data(), (int)s.size(), nullptr, 0);
std::wstring out(n, L'\0');
MultiByteToWideChar(CP_UTF8, 0, s.data(), (int)s.size(), out.data(), n);
return out;
}
/**
* Function: w8
*
* @brief 实现界面层的 “w8” 功能,并在 Win32 控件与 AppState 业务模型之间传递数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param s 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。
*/
std::string w8(const std::wstring& s) {
if (s.empty()) return "";
int n = WideCharToMultiByte(CP_UTF8, 0, s.data(), (int)s.size(), nullptr, 0, nullptr, nullptr);
std::string out(n, '\0');
WideCharToMultiByte(CP_UTF8, 0, s.data(), (int)s.size(), out.data(), n, nullptr, nullptr);
return out;
}
struct FindCtx {
int id;
HWND found;
};
/**
* Function: enumFindControl
*
* @brief 实现界面层的 “enumFindControl” 功能,并在 Win32 控件与 AppState 业务模型之间传递数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param child 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @param lp 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。
*/
BOOL CALLBACK enumFindControl(HWND child, LPARAM lp) {
auto* ctx = reinterpret_cast<FindCtx*>(lp);
if (GetDlgCtrlID(child) == ctx->id) {
ctx->found = child;
return FALSE;
}
return TRUE;
}
/**
* Function: findControl
*
* @brief 在现有数据结构中查找 “findControl” 对应的对象或位置。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param root 文件或目录路径;函数按 UTF-8/Windows 路径规则读取或写入。
* @param id 索引、数量、范围或偏移参数;具体边界由函数体校验。
* @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。
*/
HWND findControl(HWND root, int id) {
HWND direct = GetDlgItem(root, id);
if (direct) return direct;
FindCtx ctx{id, nullptr};
EnumChildWindows(root, enumFindControl, reinterpret_cast<LPARAM>(&ctx));
return ctx.found;
}
/**
* Function: getText
*
* @brief 实现界面层的 “getText” 功能,并在 Win32 控件与 AppState 业务模型之间传递数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param hwnd Win32 窗口或控件句柄;必须在有效窗口生命周期内使用。
* @param id 索引、数量、范围或偏移参数;具体边界由函数体校验。
* @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。
*/
std::string getText(HWND hwnd, int id) {
HWND h = findControl(hwnd, id);
if (!h) return "";
int len = GetWindowTextLengthW(h);
std::wstring text(len + 1, L'\0');
GetWindowTextW(h, text.data(), len + 1);
text.resize(len);
return w8(text);
}
/**
* Function: setText
*
* @brief 把 “setText” 对应的变更应用到模型、控件或输出文档。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param hwnd Win32 窗口或控件句柄;必须在有效窗口生命周期内使用。
* @param id 索引、数量、范围或偏移参数;具体边界由函数体校验。
* @param value 待解析、比较、显示或发送的 UTF-8 文本。
*/
void setText(HWND hwnd, int id, const std::string& value) {
HWND h = findControl(hwnd, id);
if (h) SetWindowTextW(h, u8w(value).c_str());
}
/**
* Function: applyFont
*
* @brief 把 “applyFont” 对应的变更应用到模型、控件或输出文档。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param app 应用状态;包含窗口句柄、当前选择、预览模型和运行期标志。
* @param ctl Win32 窗口或控件句柄;必须在有效窗口生命周期内使用。
*/
void applyFont(AppState& app, HWND ctl) {
SendMessageW(ctl, WM_SETFONT, (WPARAM)app.font, TRUE);
}
/**
* Function: updatePreviewFont
*
* @brief 根据当前模型状态刷新 “updatePreviewFont” 对应的缓存或界面。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param app 应用状态;包含窗口句柄、当前选择、预览模型和运行期标志。
*/
void updatePreviewFont(AppState& app) {
if (app.previewFont) {
DeleteObject(app.previewFont);
app.previewFont = nullptr;
}
int height = -std::max(10, app.previewZoomPercent * 16 / 100);
app.previewFont = CreateFontW(height, 0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE,
DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS,
CLEARTYPE_QUALITY, DEFAULT_PITCH | FF_SWISS, L"Microsoft YaHei UI");
if (app.previewList) {
SendMessageW(app.previewList, WM_SETFONT, (WPARAM)(app.previewFont ? app.previewFont : app.font), TRUE);
InvalidateRect(app.previewList, nullptr, TRUE);
}
}
/**
* Function: panelProc
*
* @brief 实现界面层的 “panelProc” 功能,并在 Win32 控件与 AppState 业务模型之间传递数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param hwnd Win32 窗口或控件句柄;必须在有效窗口生命周期内使用。
* @param msg 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @param wp 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @param lp 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。
*/
LRESULT CALLBACK panelProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
if ((msg == WM_COMMAND || msg == WM_NOTIFY) && g_app && g_app->hwnd) {
return SendMessageW(g_app->hwnd, msg, wp, lp);
}
return DefWindowProcW(hwnd, msg, wp, lp);
}
/**
* Function: addLabel
*
* @brief 向目标结构追加或保证存在 “addLabel” 所描述的数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param app 应用状态;包含窗口句柄、当前选择、预览模型和运行期标志。
* @param parent Win32 窗口或控件句柄;必须在有效窗口生命周期内使用。
* @param text 待解析、比较、显示或发送的 UTF-8 文本。
* @param x 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @param y 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @param w 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @param h 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。
*/
HWND addLabel(AppState& app, HWND parent, const wchar_t* text, int x, int y, int w, int h) {
HWND ctl = CreateWindowExW(0, L"STATIC", text, WS_CHILD | WS_VISIBLE | SS_LEFT | SS_NOPREFIX,
x, y + 4, w, h, parent, nullptr, app.inst, nullptr);
applyFont(app, ctl);
return ctl;
}
/**
* Function: addEdit
*
* @brief 向目标结构追加或保证存在 “addEdit” 所描述的数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param app 应用状态;包含窗口句柄、当前选择、预览模型和运行期标志。
* @param parent Win32 窗口或控件句柄;必须在有效窗口生命周期内使用。
* @param id 索引、数量、范围或偏移参数;具体边界由函数体校验。
* @param text 待解析、比较、显示或发送的 UTF-8 文本。
* @param x 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @param y 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @param w 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @param h 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。
*/
HWND addEdit(AppState& app, HWND parent, int id, const std::string& text, int x, int y, int w, int h) {
h = std::max(h, 28);
HWND ctl = CreateWindowExW(WS_EX_CLIENTEDGE, L"EDIT", u8w(text).c_str(),
WS_CHILD | WS_VISIBLE | WS_TABSTOP | ES_AUTOHSCROLL | ES_NOHIDESEL,
x, y, w, h, parent, (HMENU)(INT_PTR)id, app.inst, nullptr);
applyFont(app, ctl);
SendMessageW(ctl, EM_SETMARGINS, EC_LEFTMARGIN | EC_RIGHTMARGIN, MAKELPARAM(5, 5));
return ctl;
}
/**
* Function: addMemo
*
* @brief 向目标结构追加或保证存在 “addMemo” 所描述的数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param app 应用状态;包含窗口句柄、当前选择、预览模型和运行期标志。
* @param parent Win32 窗口或控件句柄;必须在有效窗口生命周期内使用。
* @param id 索引、数量、范围或偏移参数;具体边界由函数体校验。
* @param x 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @param y 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @param w 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @param h 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。
*/
HWND addMemo(AppState& app, HWND parent, int id, int x, int y, int w, int h) {
HWND ctl = CreateWindowExW(WS_EX_CLIENTEDGE, L"EDIT", L"",
WS_CHILD | WS_VISIBLE | WS_TABSTOP | ES_MULTILINE |
ES_AUTOVSCROLL | WS_VSCROLL,
x, y, w, h, parent, (HMENU)(INT_PTR)id, app.inst, nullptr);
applyFont(app, ctl);
return ctl;
}
/**
* Function: addButton
*
* @brief 向目标结构追加或保证存在 “addButton” 所描述的数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param app 应用状态;包含窗口句柄、当前选择、预览模型和运行期标志。
* @param parent Win32 窗口或控件句柄;必须在有效窗口生命周期内使用。
* @param id 索引、数量、范围或偏移参数;具体边界由函数体校验。
* @param text 待解析、比较、显示或发送的 UTF-8 文本。
* @param x 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @param y 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @param w 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @param h 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。
*/
HWND addButton(AppState& app, HWND parent, int id, const wchar_t* text, int x, int y, int w, int h) {
HWND ctl = CreateWindowExW(0, L"BUTTON", text, WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_PUSHBUTTON,
x, y, w, h, parent, (HMENU)(INT_PTR)id, app.inst, nullptr);
applyFont(app, ctl);
return ctl;
}
/**
* Function: addGroup
*
* @brief 向目标结构追加或保证存在 “addGroup” 所描述的数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param app 应用状态;包含窗口句柄、当前选择、预览模型和运行期标志。
* @param parent Win32 窗口或控件句柄;必须在有效窗口生命周期内使用。
* @param text 待解析、比较、显示或发送的 UTF-8 文本。
* @param x 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @param y 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @param w 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @param h 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。
*/
HWND addGroup(AppState& app, HWND parent, const wchar_t* text, int x, int y, int w, int h) {
HWND ctl = CreateWindowExW(0, L"BUTTON", text, WS_CHILD | WS_VISIBLE | BS_GROUPBOX,
x, y, w, h, parent, nullptr, app.inst, nullptr);
applyFont(app, ctl);
return ctl;
}
/**
* Function: addLabeledEdit
*
* @brief 向目标结构追加或保证存在 “addLabeledEdit” 所描述的数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param app 应用状态;包含窗口句柄、当前选择、预览模型和运行期标志。
* @param parent Win32 窗口或控件句柄;必须在有效窗口生命周期内使用。
* @param label 待解析、比较、显示或发送的 UTF-8 文本。
* @param id 索引、数量、范围或偏移参数;具体边界由函数体校验。
* @param value 待解析、比较、显示或发送的 UTF-8 文本。
* @param x 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @param y 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @param labelW 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @param editW 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
*/
void addLabeledEdit(AppState& app, HWND parent, const wchar_t* label, int id, const std::string& value,
int x, int y, int labelW, int editW) {
addLabel(app, parent, label, x, y, labelW, 22);
addEdit(app, parent, id, value, x + labelW, y, editW, 28);
}
/**
* Function: appendLogW
*
* @brief 向目标结构追加或保证存在 “appendLogW” 所描述的数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param app 应用状态;包含窗口句柄、当前选择、预览模型和运行期标志。
* @param line 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
*/
void appendLogW(AppState& app, const std::wstring& line) {
if (!app.log) return;
std::wstring text = line + L"\r\n";
int len = GetWindowTextLengthW(app.log);
SendMessageW(app.log, EM_SETSEL, len, len);
SendMessageW(app.log, EM_REPLACESEL, FALSE, (LPARAM)text.c_str());
}
/**
* Function: appendLog
*
* @brief 向目标结构追加或保证存在 “appendLog” 所描述的数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param app 应用状态;包含窗口句柄、当前选择、预览模型和运行期标志。
* @param line 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
*/
void appendLog(AppState& app, const std::string& line) {
appendLogW(app, u8w(line));
}
/**
* Function: xmlPreviewDisplayText
*
* @brief 实现界面层的 “xmlPreviewDisplayText” 功能,并在 Win32 控件与 AppState 业务模型之间传递数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param xml 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。
*/
std::string xmlPreviewDisplayText(const std::string& xml) {
// 可编辑预览必须展示原始 XML不能再混入行号否则导出后不是合法 XML。
std::string out;
out.reserve(xml.size() + xml.size() / 20);
for (size_t i = 0; i < xml.size(); ++i) {
if (xml[i] == '\r') {
if (i + 1 < xml.size() && xml[i + 1] == '\n') ++i;
out += "\r\n";
} else if (xml[i] == '\n') {
out += "\r\n";
} else {
out.push_back(xml[i]);
}
}
return out;
}
/**
* Function: highlightXmlSearch
*
* @brief 实现界面层的 “highlightXmlSearch” 功能,并在 Win32 控件与 AppState 业务模型之间传递数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param app 应用状态;包含窗口句柄、当前选择、预览模型和运行期标志。
* @param focusPreview 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @param findNext 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @return 条件成立或操作成功时为 true否则为 false。
*/
bool highlightXmlSearch(AppState& app, bool focusPreview = false, bool findNext = false) {
if (!app.xmlPreview) return false;
std::string search = app.xmlSearchText;
auto isSpace = [](unsigned char c) { return std::isspace(c) != 0; };
search.erase(search.begin(), std::find_if(search.begin(), search.end(), [&](char c) {
return !isSpace(static_cast<unsigned char>(c));
}));
search.erase(std::find_if(search.rbegin(), search.rend(), [&](char c) {
return !isSpace(static_cast<unsigned char>(c));
}).base(), search.end());
if (search.empty()) return false;
std::wstring display = u8w(xmlPreviewDisplayText(app.generatedXmlText));
std::wstring needle = u8w(search);
std::wstring haystack = display;
std::transform(haystack.begin(), haystack.end(), haystack.begin(),
[](wchar_t c) { return (wchar_t)std::towlower(c); });
std::transform(needle.begin(), needle.end(), needle.begin(),
[](wchar_t c) { return (wchar_t)std::towlower(c); });
size_t start = 0;
if (findNext && app.xmlSearchLastText == search) {
start = std::min(app.xmlSearchNextPos, haystack.size());
}
size_t pos = haystack.find(needle, start);
if (pos == std::wstring::npos && findNext && start > 0) {
pos = haystack.find(needle, 0);
}
if (pos == std::wstring::npos) return false;
if (findNext) {
app.xmlSearchLastText = search;
app.xmlSearchNextPos = pos + needle.size();
}
if (focusPreview) SetFocus(app.xmlPreview);
SendMessageW(app.xmlPreview, EM_SETSEL, (WPARAM)pos, (LPARAM)(pos + needle.size()));
SendMessageW(app.xmlPreview, EM_SCROLLCARET, 0, 0);
if (focusPreview) SetFocus(app.xmlPreview);
return true;
}
/**
* Function: updateXmlPreviewText
*
* @brief 根据当前模型状态刷新 “updateXmlPreviewText” 对应的缓存或界面。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param app 应用状态;包含窗口句柄、当前选择、预览模型和运行期标志。
*/
void updateXmlPreviewText(AppState& app) {
if (!app.xmlPreview) return;
app.updatingXmlPreview = true;
SetWindowTextW(app.xmlPreview, u8w(xmlPreviewDisplayText(app.generatedXmlText)).c_str());
app.updatingXmlPreview = false;
app.xmlPreviewEdited = false;
highlightXmlSearch(app, false, false);
}
/**
* Function: syncXmlPreviewFromEditor
*
* @brief 把 “syncXmlPreviewFromEditor” 对应的变更应用到模型、控件或输出文档。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param app 应用状态;包含窗口句柄、当前选择、预览模型和运行期标志。
*/
void syncXmlPreviewFromEditor(AppState& app) {
if (!app.xmlPreview || app.updatingXmlPreview) return;
app.generatedXmlText = getText(app.hwnd, IDC_XML_PREVIEW_EDIT);
app.reverseXmlDirty = false;
app.xmlPreviewEdited = true;
}
/**
* Function: showBusyPopup
*
* @brief 实现界面层的 “showBusyPopup” 功能,并在 Win32 控件与 AppState 业务模型之间传递数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param app 应用状态;包含窗口句柄、当前选择、预览模型和运行期标志。
* @param message 输出参数;函数在成功或失败时写入结果或诊断信息。
*/
void showBusyPopup(AppState& app, const wchar_t* message) {
if (!app.hwnd) return;
if (app.busyPopup && IsWindow(app.busyPopup)) {
SetWindowTextW(app.busyPopup, message);
} else {
RECT owner{};
GetWindowRect(app.hwnd, &owner);
int w = 380;
int h = 90;
int x = owner.left + std::max(0L, (owner.right - owner.left - w) / 2);
int y = owner.top + std::max(0L, (owner.bottom - owner.top - h) / 2);
app.busyPopup = CreateWindowExW(WS_EX_TOPMOST | WS_EX_TOOLWINDOW,
L"STATIC",
message,
WS_POPUP | WS_BORDER | SS_CENTER | SS_CENTERIMAGE,
x, y, w, h,
app.hwnd,
nullptr,
app.inst,
nullptr);
if (app.busyPopup) applyFont(app, app.busyPopup);
}
if (app.busyPopup) {
ShowWindow(app.busyPopup, SW_SHOWNOACTIVATE);
UpdateWindow(app.busyPopup);
}
}
/**
* Function: hideBusyPopup
*
* @brief 实现界面层的 “hideBusyPopup” 功能,并在 Win32 控件与 AppState 业务模型之间传递数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param app 应用状态;包含窗口句柄、当前选择、预览模型和运行期标志。
*/
void hideBusyPopup(AppState& app) {
if (app.busyPopup && IsWindow(app.busyPopup)) {
DestroyWindow(app.busyPopup);
}
app.busyPopup = nullptr;
}
struct BusyPopupGuard {
AppState& app;
bool active;
/**
* @brief 构造一个忙碌提示的作用域守卫。
* @details 这是 C++ 的 RAII资源获取即初始化写法构造时在需要的情况下设置
* autoParseBusy 并显示提示窗;无论调用流程正常结束还是抛出异常,析构函数
* 都会自动恢复状态,因此调用方不必在每个 return/catch 分支重复清理界面。
* @param state GUI 的共享 AppState本对象仅保存引用不取得其所有权。
* @param show true 时启用忙碌状态false 时守卫不产生界面副作用。
* @param message 显示在 Win32 STATIC 弹窗中的宽字符串。
*/
BusyPopupGuard(AppState& state, bool show, const wchar_t* message)
: app(state), active(show) {
if (active) {
app.autoParseBusy = true;
showBusyPopup(app, message);
}
}
/**
* @brief 结束作用域时关闭忙碌提示并恢复自动解析状态。
* @details 析构函数不能手动返回结果;它由 C++ 在局部对象离开作用域时自动调用,
* 与构造函数配对形成异常安全的清理路径。
*/
~BusyPopupGuard() {
if (active) {
hideBusyPopup(app);
app.autoParseBusy = false;
}
}
};
/**
* Function: readAllText
*
* @brief 读取或加载 “readAllText” 对应的数据,并转换为本模块后续逻辑使用的内存结构。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param path 文件或目录路径;函数按 UTF-8/Windows 路径规则读取或写入。
* @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。
*/
std::string readAllText(const fs::path& path) {
std::ifstream ifs(path, std::ios::binary);
if (!ifs) return "";
std::ostringstream ss;
ss << ifs.rdbuf();
return ss.str();
}
/**
* Function: truncateText
*
* @brief 实现界面层的 “truncateText” 功能,并在 Win32 控件与 AppState 业务模型之间传递数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param text 待解析、比较、显示或发送的 UTF-8 文本。
* @param limit 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。
*/
std::string truncateText(std::string text, size_t limit = 260) {
if (text.size() <= limit) return text;
return text.substr(0, limit) + "...";
}
/**
* Function: trimAscii
*
* @brief 规范化 “trimAscii” 对应的字符串或标识,减少格式差异对匹配造成的影响。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param s 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。
*/
std::string trimAscii(std::string s) {
auto isSpace = [](unsigned char c) { return std::isspace(c) != 0; };
s.erase(s.begin(), std::find_if(s.begin(), s.end(), [&](char c) { return !isSpace(static_cast<unsigned char>(c)); }));
s.erase(std::find_if(s.rbegin(), s.rend(), [&](char c) { return !isSpace(static_cast<unsigned char>(c)); }).base(), s.end());
return s;
}
/**
* Function: lowerAsciiCopy
*
* @brief 规范化 “lowerAsciiCopy” 对应的字符串或标识,减少格式差异对匹配造成的影响。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param s 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。
*/
std::string lowerAsciiCopy(std::string s) {
std::transform(s.begin(), s.end(), s.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
return s;
}
/**
* Function: textContainsFilter
*
* @brief 实现界面层的 “textContainsFilter” 功能,并在 Win32 控件与 AppState 业务模型之间传递数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param text 待解析、比较、显示或发送的 UTF-8 文本。
* @param filter 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @return 条件成立或操作成功时为 true否则为 false。
*/
bool textContainsFilter(const std::string& text, const std::string& filter) {
if (filter.empty()) return true;
return lowerAsciiCopy(text).find(lowerAsciiCopy(filter)) != std::string::npos;
}
/**
* Function: splitPathList
*
* @brief 实现界面层的 “splitPathList” 功能,并在 Win32 控件与 AppState 业务模型之间传递数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param text 待解析、比较、显示或发送的 UTF-8 文本。
* @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。
*/
std::vector<std::string> splitPathList(std::string text) {
for (char& c : text) {
if (c == ';' || c == '\r' || c == '\n') c = '\n';
}
std::vector<std::string> paths;
std::stringstream ss(text);
std::string part;
while (std::getline(ss, part, '\n')) {
part = trimAscii(part);
if (!part.empty()) paths.push_back(part);
}
return paths;
}
std::string joinPaths(const std::vector<std::string>& paths);
std::vector<std::string> collectOriginFiles(const fs::path& dir);
std::vector<std::string> collectJsonDataFiles(const fs::path& dir);
/**
* Function: selectedFrontTypeFromWindow
*
* @brief 实现界面层的 “selectedFrontTypeFromWindow” 功能,并在 Win32 控件与 AppState 业务模型之间传递数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param hwnd Win32 窗口或控件句柄;必须在有效窗口生命周期内使用。
* @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。
*/
std::string selectedFrontTypeFromWindow(HWND hwnd) {
HWND combo = findControl(hwnd, IDC_FRONT_TYPE);
if (!combo) return "cfg_stat_data";
int idx = (int)SendMessageW(combo, CB_GETCURSEL, 0, 0);
wchar_t buf[128]{};
SendMessageW(combo, CB_GETLBTEXT, idx < 0 ? 0 : idx, (LPARAM)buf);
std::string value = w8(buf);
return value.empty() ? "cfg_stat_data" : value;
}
/**
* Function: readConfig
*
* @brief 读取或加载 “readConfig” 对应的数据,并转换为本模块后续逻辑使用的内存结构。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param hwnd Win32 窗口或控件句柄;必须在有效窗口生命周期内使用。
* @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。
*/
PqToolConfig readConfig(HWND hwnd) {
PqToolConfig cfg = pqDefaultConfig();
// GUI 编辑框是配置最终来源;先用默认值兜底,再逐项用界面内容覆盖。
cfg.frontInst = getText(hwnd, IDC_FRONT_INST);
cfg.frontIp = getText(hwnd, IDC_FRONT_IP);
cfg.terminalStatus = getText(hwnd, IDC_TERMINAL_STATUS);
cfg.monitorStatus = getText(hwnd, IDC_MONITOR_STATUS);
cfg.icdFlag = getText(hwnd, IDC_ICD_FLAG);
cfg.webDevice = getText(hwnd, IDC_WEB_DEVICE);
cfg.webIcd = getText(hwnd, IDC_WEB_ICD);
cfg.webFiledownload = getText(hwnd, IDC_WEB_FILEDOWNLOAD);
cfg.xmlSaveDir = getText(hwnd, IDC_XML_SAVE_DIR);
cfg.producer = getText(hwnd, IDC_PRODUCER);
cfg.ipport = getText(hwnd, IDC_IPPORT);
cfg.producerAccessKey = getText(hwnd, IDC_PRODUCER_ACCESS_KEY);
cfg.producerSecretKey = getText(hwnd, IDC_PRODUCER_SECRET_KEY);
cfg.topicLOG = getText(hwnd, IDC_TOPIC_LOG);
cfg.tagLOG = getText(hwnd, IDC_TAG_LOG);
cfg.keyLOG = getText(hwnd, IDC_KEY_LOG);
cfg.topicAsk = getText(hwnd, IDC_TOPIC_ASK);
cfg.tagAsk = getText(hwnd, IDC_TAG_ASK);
cfg.keyAsk = getText(hwnd, IDC_KEY_ASK);
cfg.consumer = getText(hwnd, IDC_CONSUMER);
cfg.consumerIpport = getText(hwnd, IDC_CONSUMER_IPPORT);
cfg.consumerAccessKey = getText(hwnd, IDC_CONSUMER_ACCESS_KEY);
cfg.consumerSecretKey = getText(hwnd, IDC_CONSUMER_SECRET_KEY);
cfg.dataStatTopic = getText(hwnd, IDC_DATA_STAT_TOPIC);
cfg.dataRealtimeTopic = getText(hwnd, IDC_DATA_REALTIME_TOPIC);
cfg.dataStatTag = getText(hwnd, IDC_DATA_STAT_TAG);
cfg.dataStatKey = getText(hwnd, IDC_DATA_STAT_KEY);
cfg.dataRealtimeTag = getText(hwnd, IDC_DATA_REALTIME_TAG);
cfg.dataRealtimeKey = getText(hwnd, IDC_DATA_REALTIME_KEY);
std::string frontType = selectedFrontTypeFromWindow(hwnd);
// frontType 是统计/实时模式开关:决定本次追踪和解析关注哪个 DATATopic。
if (frontType == "cfg_3s_data") {
cfg.dataTopic = cfg.dataRealtimeTopic;
cfg.dataTag = cfg.dataRealtimeTag;
cfg.dataKey = cfg.dataRealtimeKey;
} else {
cfg.dataTopic = cfg.dataStatTopic;
cfg.dataTag = cfg.dataStatTag;
cfg.dataKey = cfg.dataStatKey;
}
cfg.traceTopic = getText(hwnd, IDC_TRACE_TOPIC);
cfg.traceTag = getText(hwnd, IDC_TRACE_TAG);
cfg.traceKey = getText(hwnd, IDC_TRACE_KEY);
cfg.xmlPath = getText(hwnd, IDC_XML_PATH);
cfg.jsonDataPath = getText(hwnd, IDC_JSON_PATH);
cfg.jsonDataPaths = splitPathList(cfg.jsonDataPath);
cfg.outputPath = getText(hwnd, IDC_OUTPUT_PATH);
cfg.originPaths = splitPathList(getText(hwnd, IDC_ORIGIN_PATHS));
return cfg;
}
/**
* Function: toInt
*
* @brief 实现界面层的 “toInt” 功能,并在 Win32 控件与 AppState 业务模型之间传递数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param s 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @return 整数状态、索引或计数;入口函数以 0 表示成功。
*/
int toInt(const std::string& s) {
try {
return s.empty() ? 0 : std::stoi(s);
} catch (...) {
return 0;
}
}
/**
* Function: makeRef
*
* @brief 构造 “makeRef” 对应的对象、消息、窗口控件或输出结构。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param app 应用状态;包含窗口句柄、当前选择、预览模型和运行期标志。
* @param device 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @param monitor 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。
*/
NodeRef* makeRef(AppState& app, int device, int monitor) {
auto ref = std::make_unique<NodeRef>();
ref->device = device;
ref->monitor = monitor;
NodeRef* p = ref.get();
app.refs.push_back(std::move(ref));
return p;
}
/**
* Function: makeJsonRef
*
* @brief 构造 “makeJsonRef” 对应的对象、消息、窗口控件或输出结构。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param app 应用状态;包含窗口句柄、当前选择、预览模型和运行期标志。
* @param index 索引、数量、范围或偏移参数;具体边界由函数体校验。
* @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。
*/
JsonNodeRef* makeJsonRef(AppState& app, int index) {
auto ref = std::make_unique<JsonNodeRef>();
ref->index = index;
JsonNodeRef* p = ref.get();
app.jsonRefs.push_back(std::move(ref));
return p;
}
/**
* Function: insertTreeItemRaw
*
* @brief 向目标结构追加或保证存在 “insertTreeItemRaw” 所描述的数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param tree Win32 窗口或控件句柄;必须在有效窗口生命周期内使用。
* @param parent Win32 窗口或控件句柄;必须在有效窗口生命周期内使用。
* @param text 待解析、比较、显示或发送的 UTF-8 文本。
* @param ref 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。
*/
HTREEITEM insertTreeItemRaw(HWND tree, HTREEITEM parent, const std::wstring& text, LPARAM ref) {
TVINSERTSTRUCTW tv{};
tv.hParent = parent;
tv.hInsertAfter = TVI_LAST;
tv.item.mask = TVIF_TEXT | TVIF_PARAM;
tv.item.pszText = const_cast<wchar_t*>(text.c_str());
tv.item.lParam = ref;
return (HTREEITEM)SendMessageW(tree, TVM_INSERTITEMW, 0, (LPARAM)&tv);
}
/**
* Function: insertTreeItem
*
* @brief 向目标结构追加或保证存在 “insertTreeItem” 所描述的数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param app 应用状态;包含窗口句柄、当前选择、预览模型和运行期标志。
* @param parent Win32 窗口或控件句柄;必须在有效窗口生命周期内使用。
* @param text 待解析、比较、显示或发送的 UTF-8 文本。
* @param ref 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。
*/
HTREEITEM insertTreeItem(AppState& app, HTREEITEM parent, const std::wstring& text, NodeRef* ref) {
return insertTreeItemRaw(app.tree, parent, text, (LPARAM)ref);
}
/**
* Function: insertJsonTreeItem
*
* @brief 向目标结构追加或保证存在 “insertJsonTreeItem” 所描述的数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param app 应用状态;包含窗口句柄、当前选择、预览模型和运行期标志。
* @param parent Win32 窗口或控件句柄;必须在有效窗口生命周期内使用。
* @param text 待解析、比较、显示或发送的 UTF-8 文本。
* @param ref 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。
*/
HTREEITEM insertJsonTreeItem(AppState& app, HTREEITEM parent, const std::wstring& text, JsonNodeRef* ref) {
return insertTreeItemRaw(app.jsonTree, parent, text, (LPARAM)ref);
}
/**
* Function: deviceLabel
*
* @brief 实现界面层的 “deviceLabel” 功能,并在 Win32 控件与 AppState 业务模型之间传递数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param d 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。
*/
std::wstring deviceLabel(const PqLedgerDevice& d) {
{
std::wstring text = L"\x88C5\x7F6E ";
text += u8w(d.name.empty() ? d.id : d.name);
text += L" id=" + u8w(d.id);
text += L" type=" + u8w(d.devType);
return text;
}
std::wstring text = L"装置 ";
text += u8w(d.name.empty() ? d.id : d.name);
text += L" id=" + u8w(d.id);
text += L" type=" + u8w(d.devType);
return text;
}
/**
* Function: monitorLabel
*
* @brief 实现界面层的 “monitorLabel” 功能,并在 Win32 控件与 AppState 业务模型之间传递数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param m 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。
*/
std::wstring monitorLabel(const PqLedgerMonitor& m) {
{
std::wstring text = L"\x6D4B\x70B9 ";
text += u8w(m.name.empty() ? m.id : m.name);
text += L" id=" + u8w(m.id);
return text;
}
std::wstring text = L"测点 ";
text += u8w(m.name.empty() ? m.id : m.name);
text += L" id=" + u8w(m.id);
return text;
}
/**
* Function: processLabel
*
* @brief 实现界面层的 “processLabel” 功能,并在 Win32 控件与 AppState 业务模型之间传递数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param processNo 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。
*/
std::wstring processLabel(int processNo) {
return L"\x8FDB\x7A0B " + std::to_wstring(processNo);
}
/**
* Function: populateTree
*
* @brief 根据当前模型状态刷新 “populateTree” 对应的缓存或界面。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param app 应用状态;包含窗口句柄、当前选择、预览模型和运行期标志。
*/
void populateTree(AppState& app) {
TreeView_DeleteAllItems(app.tree);
app.refs.clear();
app.selectedDevice = -1;
app.selectedMonitor = -1;
// 台账树按进程号分组maxProcessNum 可让没有设备的进程也显示出来。
int maxProcess = 0;
for (const auto& d : app.devices) {
maxProcess = std::max(maxProcess, toInt(d.processNo));
maxProcess = std::max(maxProcess, toInt(d.maxProcessNum));
}
if (!app.devices.empty() && maxProcess < 2) maxProcess = 2;
std::map<int, HTREEITEM> processNodes;
for (int i = 1; i <= maxProcess; ++i) {
processNodes[i] = insertTreeItem(app, TVI_ROOT, processLabel(i), makeRef(app, -1, -1));
}
for (size_t i = 0; i < app.devices.size(); ++i) {
const auto& d = app.devices[i];
int proc = toInt(d.processNo);
if (proc <= 0) proc = 1;
if (!processNodes.count(proc)) {
processNodes[proc] = insertTreeItem(app, TVI_ROOT, processLabel(proc), makeRef(app, -1, -1));
}
HTREEITEM devItem = insertTreeItem(app, processNodes[proc], deviceLabel(d), makeRef(app, (int)i, -1));
for (size_t j = 0; j < d.monitors.size(); ++j) {
insertTreeItem(app, devItem, monitorLabel(d.monitors[j]), makeRef(app, (int)i, (int)j));
}
TreeView_Expand(app.tree, processNodes[proc], TVE_EXPAND);
TreeView_Expand(app.tree, devItem, TVE_EXPAND);
}
}
/**
* Function: appendFields
*
* @brief 向目标结构追加或保证存在 “appendFields” 所描述的数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param text 待解析、比较、显示或发送的 UTF-8 文本。
* @param fields 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
*/
void appendFields(std::wstring& text, const std::vector<std::pair<std::string, std::string>>& fields) {
for (const auto& kv : fields) {
text += u8w(kv.first) + L": " + u8w(kv.second) + L"\r\n";
}
}
/**
* Function: updateDetails
*
* @brief 根据当前模型状态刷新 “updateDetails” 对应的缓存或界面。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param app 应用状态;包含窗口句柄、当前选择、预览模型和运行期标志。
*/
void updateDetails(AppState& app) {
{
std::wstring text;
if (app.selectedDevice >= 0 && app.selectedDevice < (int)app.devices.size()) {
const auto& d = app.devices[app.selectedDevice];
if (app.selectedMonitor >= 0 && app.selectedMonitor < (int)d.monitors.size()) {
const auto& m = d.monitors[app.selectedMonitor];
text += L"\x6D4B\x70B9\x53F0\x8D26\x4FE1\x606F\r\n";
text += L"\x6240\x5C5E\x88C5\x7F6E: " + u8w(d.name) + L"\r\n";
text += L"\x6240\x5C5E\x88C5\x7F6EID: " + u8w(d.id) + L"\r\n";
text += L"\x6240\x5C5E\x8FDB\x7A0B\x53F7: " + u8w(d.processNo) + L"\r\n";
text += L"------------------------------\r\n";
if (!m.fields.empty()) appendFields(text, m.fields);
else {
text += L"id: " + u8w(m.id) + L"\r\n";
text += L"name: " + u8w(m.name) + L"\r\n";
text += L"lineNo: " + u8w(m.lineNo) + L"\r\n";
text += L"voltageLevel: " + u8w(m.voltageLevel) + L"\r\n";
text += L"ptType: " + u8w(m.connectType) + L"\r\n";
text += L"status: " + u8w(m.status) + L"\r\n";
}
} else {
text += L"\x88C5\x7F6E\x53F0\x8D26\x4FE1\x606F\r\n";
if (!d.fields.empty()) appendFields(text, d.fields);
else {
text += L"id: " + u8w(d.id) + L"\r\n";
text += L"name: " + u8w(d.name) + L"\r\n";
text += L"ip: " + u8w(d.ip) + L"\r\n";
text += L"status: " + u8w(d.status) + L"\r\n";
text += L"devType: " + u8w(d.devType) + L"\r\n";
text += L"processNo: " + u8w(d.processNo) + L"\r\n";
text += L"maxProcessNum: " + u8w(d.maxProcessNum) + L"\r\n";
}
text += L"monitorDataCount: " + std::to_wstring(d.monitors.size()) + L"\r\n";
}
} else {
text = L"\x8BF7\x9009\x62E9\x5DE6\x4FA7\x6811\x4E2D\x7684\x88C5\x7F6E\x6216\x6D4B\x70B9\x3002\x70B9\x51FB\x88C5\x7F6E\x663E\x793A\x88C5\x7F6E\x53F0\x8D26\x4FE1\x606F\xFF0C\x70B9\x51FB\x6D4B\x70B9\x663E\x793A\x6D4B\x70B9\x53F0\x8D26\x4FE1\x606F\x3002";
}
SetWindowTextW(app.detail, text.c_str());
return;
}
std::wstring text;
if (app.selectedDevice >= 0 && app.selectedDevice < (int)app.devices.size()) {
const auto& d = app.devices[app.selectedDevice];
if (app.selectedMonitor >= 0 && app.selectedMonitor < (int)d.monitors.size()) {
const auto& m = d.monitors[app.selectedMonitor];
text += L"测点台账信息\r\n";
text += L"所属装置: " + u8w(d.name) + L"\r\n";
text += L"所属装置ID: " + u8w(d.id) + L"\r\n";
text += L"所属进程号: " + u8w(d.processNo) + L"\r\n";
text += L"------------------------------\r\n";
if (!m.fields.empty()) appendFields(text, m.fields);
else {
text += L"id: " + u8w(m.id) + L"\r\n";
text += L"name: " + u8w(m.name) + L"\r\n";
text += L"lineNo: " + u8w(m.lineNo) + L"\r\n";
text += L"voltageLevel: " + u8w(m.voltageLevel) + L"\r\n";
text += L"ptType: " + u8w(m.connectType) + L"\r\n";
text += L"status: " + u8w(m.status) + L"\r\n";
}
} else {
text += L"装置台账信息\r\n";
if (!d.fields.empty()) appendFields(text, d.fields);
else {
text += L"id: " + u8w(d.id) + L"\r\n";
text += L"name: " + u8w(d.name) + L"\r\n";
text += L"ip: " + u8w(d.ip) + L"\r\n";
text += L"status: " + u8w(d.status) + L"\r\n";
text += L"devType: " + u8w(d.devType) + L"\r\n";
text += L"processNo: " + u8w(d.processNo) + L"\r\n";
text += L"maxProcessNum: " + u8w(d.maxProcessNum) + L"\r\n";
}
text += L"monitorDataCount: " + std::to_wstring(d.monitors.size()) + L"\r\n";
}
} else {
text = L"请选择左侧树中的装置或测点。点击装置显示装置台账信息,点击测点显示测点台账信息。";
}
SetWindowTextW(app.detail, text.c_str());
}
/**
* Function: prettyJsonText
*
* @brief 实现界面层的 “prettyJsonText” 功能,并在 Win32 控件与 AppState 业务模型之间传递数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param text 待解析、比较、显示或发送的 UTF-8 文本。
* @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。
*/
std::string prettyJsonText(const std::string& text) {
try {
return json::parse(text).dump(2);
} catch (...) {
return text;
}
}
/**
* Function: jsonScalarString
*
* @brief 实现界面层的 “jsonScalarString” 功能,并在 Win32 控件与 AppState 业务模型之间传递数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param value 待解析、比较、显示或发送的 UTF-8 文本。
* @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。
*/
std::string jsonScalarString(const json& value) {
if (value.is_string()) return value.get<std::string>();
if (value.is_number_integer()) return std::to_string(value.get<long long>());
if (value.is_number_unsigned()) return std::to_string(value.get<unsigned long long>());
if (value.is_number_float()) {
std::ostringstream oss;
oss << value.get<double>();
return oss.str();
}
if (value.is_boolean()) return value.get<bool>() ? "true" : "false";
return value.dump();
}
/**
* Function: normalizeDataTypeText
*
* @brief 规范化 “normalizeDataTypeText” 对应的字符串或标识,减少格式差异对匹配造成的影响。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param dataType 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。
*/
std::string normalizeDataTypeText(std::string dataType) {
dataType.erase(std::remove_if(dataType.begin(), dataType.end(),
[](unsigned char c) { return std::isspace(c) != 0; }),
dataType.end());
if (dataType.size() == 1 && std::isdigit(static_cast<unsigned char>(dataType[0]))) {
dataType = "0" + dataType;
}
return dataType;
}
/**
* Function: findDataTypeInJson
*
* @brief 在现有数据结构中查找 “findDataTypeInJson” 对应的对象或位置。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param value 待解析、比较、显示或发送的 UTF-8 文本。
* @param out 输出参数;函数在成功或失败时写入结果或诊断信息。
* @return 条件成立或操作成功时为 true否则为 false。
*/
bool findDataTypeInJson(const json& value, std::string& out) {
if (value.is_object()) {
auto it = value.find("DATA_TYPE");
if (it != value.end()) {
out = normalizeDataTypeText(jsonScalarString(*it));
return true;
}
for (auto iter = value.begin(); iter != value.end(); ++iter) {
if (findDataTypeInJson(iter.value(), out)) return true;
}
} else if (value.is_array()) {
for (const auto& item : value) {
if (findDataTypeInJson(item, out)) return true;
}
}
return false;
}
/**
* Function: dataTypeFromJsonText
*
* @brief 实现界面层的 “dataTypeFromJsonText” 功能,并在 Win32 控件与 AppState 业务模型之间传递数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param text 待解析、比较、显示或发送的 UTF-8 文本。
* @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。
*/
std::string dataTypeFromJsonText(const std::string& text) {
try {
json root = json::parse(text);
std::string dataType;
if (findDataTypeInJson(root, dataType)) return dataType;
} catch (...) {
}
return "";
}
/**
* Function: appendOriginPathToUi
*
* @brief 向目标结构追加或保证存在 “appendOriginPathToUi” 所描述的数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param app 应用状态;包含窗口句柄、当前选择、预览模型和运行期标志。
* @param path 文件或目录路径;函数按 UTF-8/Windows 路径规则读取或写入。
*/
void appendOriginPathToUi(AppState& app, const std::string& path) {
if (path.empty()) return;
std::vector<std::string> paths = splitPathList(getText(app.hwnd, IDC_ORIGIN_PATHS));
if (std::find(paths.begin(), paths.end(), path) == paths.end()) {
paths.push_back(path);
setText(app.hwnd, IDC_ORIGIN_PATHS, joinPaths(paths));
}
}
/**
* Function: appendJsonPathToUi
*
* @brief 向目标结构追加或保证存在 “appendJsonPathToUi” 所描述的数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param app 应用状态;包含窗口句柄、当前选择、预览模型和运行期标志。
* @param path 文件或目录路径;函数按 UTF-8/Windows 路径规则读取或写入。
*/
void appendJsonPathToUi(AppState& app, const std::string& path) {
if (path.empty()) return;
std::vector<std::string> paths = splitPathList(getText(app.hwnd, IDC_JSON_PATH));
if (std::find(paths.begin(), paths.end(), path) == paths.end()) {
paths.push_back(path);
setText(app.hwnd, IDC_JSON_PATH, joinPaths(paths));
}
}
/**
* Function: normalizedPathPrefix
*
* @brief 规范化 “normalizedPathPrefix” 对应的字符串或标识,减少格式差异对匹配造成的影响。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param path 文件或目录路径;函数按 UTF-8/Windows 路径规则读取或写入。
* @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。
*/
std::string normalizedPathPrefix(std::string path) {
try {
path = fs::absolute(fs::u8path(path)).lexically_normal().u8string();
} catch (...) {
}
for (char& ch : path) {
if (ch == '/') ch = '\\';
}
return path;
}
/**
* Function: pathIsUnderDir
*
* @brief 实现界面层的 “pathIsUnderDir” 功能,并在 Win32 控件与 AppState 业务模型之间传递数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param filePath 文件或目录路径;函数按 UTF-8/Windows 路径规则读取或写入。
* @param dirPath 文件或目录路径;函数按 UTF-8/Windows 路径规则读取或写入。
* @return 条件成立或操作成功时为 true否则为 false。
*/
bool pathIsUnderDir(const std::string& filePath, const std::string& dirPath) {
if (filePath.empty() || dirPath.empty()) return true;
std::string file = normalizedPathPrefix(filePath);
std::string dir = normalizedPathPrefix(dirPath);
if (file == dir) return true;
if (!dir.empty() && dir.back() != '\\') dir += '\\';
return file.rfind(dir, 0) == 0;
}
/**
* Function: traceDirFromPath
*
* @brief 实现界面层的 “traceDirFromPath” 功能,并在 Win32 控件与 AppState 业务模型之间传递数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param path 文件或目录路径;函数按 UTF-8/Windows 路径规则读取或写入。
* @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。
*/
std::string traceDirFromPath(const std::string& path) {
if (path.empty()) return "";
try {
return fs::u8path(path).parent_path().u8string();
} catch (...) {
return "";
}
}
/**
* Function: eventTraceDir
*
* @brief 实现界面层的 “eventTraceDir” 功能,并在 Win32 控件与 AppState 业务模型之间传递数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param ev 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。
*/
std::string eventTraceDir(const PqMqEvent& ev) {
if (!ev.traceDir.empty()) return ev.traceDir;
return traceDirFromPath(ev.path);
}
/**
* Function: sameTraceDir
*
* @brief 判断 “sameTraceDir” 所表达的条件;该函数只读取输入,不负责修改业务状态。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param a 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @param b 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @return 条件成立或操作成功时为 true否则为 false。
*/
bool sameTraceDir(const std::string& a, const std::string& b) {
if (a.empty() || b.empty()) return false;
return normalizedPathPrefix(a) == normalizedPathPrefix(b);
}
/**
* Function: jsonItemVisible
*
* @brief 实现界面层的 “jsonItemVisible” 功能,并在 Win32 控件与 AppState 业务模型之间传递数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param app 应用状态;包含窗口句柄、当前选择、预览模型和运行期标志。
* @param item 当前处理的数据行、单元格、XML 节点或模型项。
* @return 条件成立或操作成功时为 true否则为 false。
*/
bool jsonItemVisible(const AppState& app, const JsonItem& item) {
if (app.activeTraceDir.empty()) return true;
if (!item.traceDir.empty() && sameTraceDir(item.traceDir, app.activeTraceDir)) return true;
return pathIsUnderDir(item.path, app.activeTraceDir);
}
/**
* Function: latestVisibleJsonIndex
*
* @brief 实现界面层的 “latestVisibleJsonIndex” 功能,并在 Win32 控件与 AppState 业务模型之间传递数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param app 应用状态;包含窗口句柄、当前选择、预览模型和运行期标志。
* @return 整数状态、索引或计数;入口函数以 0 表示成功。
*/
int latestVisibleJsonIndex(const AppState& app) {
for (int i = (int)app.jsonItems.size() - 1; i >= 0; --i) {
if (jsonItemVisible(app, app.jsonItems[i])) return i;
}
return -1;
}
/**
* Function: refreshTraceInputsFromDisk
*
* @brief 根据当前模型状态刷新 “refreshTraceInputsFromDisk” 对应的缓存或界面。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param app 应用状态;包含窗口句柄、当前选择、预览模型和运行期标志。
* @param traceDir 文件或目录路径;函数按 UTF-8/Windows 路径规则读取或写入。
*/
void refreshTraceInputsFromDisk(AppState& app, const std::string& traceDir) {
if (traceDir.empty() || !app.hwnd || !sameTraceDir(traceDir, app.activeTraceDir)) return;
fs::path dir = fs::u8path(traceDir);
std::vector<std::string> jsonPaths = collectJsonDataFiles(dir);
std::vector<std::string> originPaths = collectOriginFiles(dir);
setText(app.hwnd, IDC_JSON_PATH, joinPaths(jsonPaths));
setText(app.hwnd, IDC_ORIGIN_PATHS, joinPaths(originPaths));
}
/**
* Function: refreshTraceInputsFromDisk
*
* @brief 根据当前模型状态刷新 “refreshTraceInputsFromDisk” 对应的缓存或界面。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param app 应用状态;包含窗口句柄、当前选择、预览模型和运行期标志。
*/
void refreshTraceInputsFromDisk(AppState& app) {
refreshTraceInputsFromDisk(app, app.activeTraceDir);
}
void rebuildJsonTree(AppState& app);
void updateJsonDetail(AppState& app);
void refreshPreviewFromWorkbook(AppState& app, const std::string& workbookPath);
void refreshPreviewWorkbookChoices(AppState& app, const std::string& selectedPath);
void autoParseAndRefresh(AppState& app, const char* reason);
void autoParseTraceDir(AppState& app, const std::string& traceDir, const char* reason);
void refreshJsonMonitorChoices(AppState& app, const std::string& selectedTraceDir);
void selectJsonMonitorChoice(AppState& app, int index);
std::string generateReverseXml(AppState& app, bool showMessage);
/**
* Function: previewWorkbookLabel
*
* @brief 实现界面层的 “previewWorkbookLabel” 功能,并在 Win32 控件与 AppState 业务模型之间传递数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param workbook 文件或目录路径;函数按 UTF-8/Windows 路径规则读取或写入。
* @param cfg 运行配置提供接口地址、MQ 参数、路径和当前数据模式。
* @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。
*/
std::string previewWorkbookLabel(const fs::path& workbook, const PqToolConfig& cfg) {
std::error_code ec;
fs::path root = fs::u8path(cfg.traceRootDir.empty() ? "runtime\\trace_data" : cfg.traceRootDir);
fs::path parent = workbook.parent_path();
std::string monitorFolder = parent.filename().u8string();
if (monitorFolder.rfind("monitor_", 0) == 0) {
return truncateText(monitorFolder.substr(8), 90);
}
fs::path rel = fs::relative(parent, root, ec);
if (!ec && !rel.empty() && rel.u8string().find("..") != 0) return rel.u8string();
return parent.empty() ? workbook.filename().u8string() : parent.u8string();
}
/**
* Function: addPreviewWorkbookChoice
*
* @brief 向目标结构追加或保证存在 “addPreviewWorkbookChoice” 所描述的数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param choices 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @param seen 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @param workbook 文件或目录路径;函数按 UTF-8/Windows 路径规则读取或写入。
* @param cfg 运行配置提供接口地址、MQ 参数、路径和当前数据模式。
*/
void addPreviewWorkbookChoice(std::vector<PreviewWorkbookChoice>& choices,
std::unordered_set<std::string>& seen,
const fs::path& workbook,
const PqToolConfig& cfg) {
if (workbook.empty() || !fs::exists(workbook)) return;
std::string key = normalizedPathPrefix(workbook.u8string());
if (!seen.insert(key).second) return;
choices.push_back({previewWorkbookLabel(workbook, cfg), workbook.u8string()});
}
/**
* Function: refreshPreviewWorkbookChoices
*
* @brief 根据当前模型状态刷新 “refreshPreviewWorkbookChoices” 对应的缓存或界面。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param app 应用状态;包含窗口句柄、当前选择、预览模型和运行期标志。
* @param selectedPath 文件或目录路径;函数按 UTF-8/Windows 路径规则读取或写入。
*/
void refreshPreviewWorkbookChoices(AppState& app, const std::string& selectedPath) {
if (!app.previewWorkbook || !app.hwnd) return;
PqToolConfig cfg = readConfig(app.hwnd);
std::vector<PreviewWorkbookChoice> choices;
std::unordered_set<std::string> seen;
if (!selectedPath.empty()) addPreviewWorkbookChoice(choices, seen, fs::u8path(selectedPath), cfg);
if (!cfg.outputPath.empty()) addPreviewWorkbookChoice(choices, seen, fs::u8path(cfg.outputPath), cfg);
fs::path root = fs::u8path(cfg.traceRootDir.empty() ? "runtime\\trace_data" : cfg.traceRootDir);
std::error_code ec;
if (fs::exists(root, ec)) {
for (fs::recursive_directory_iterator it(root, fs::directory_options::skip_permission_denied, ec), end;
it != end && !ec;
it.increment(ec)) {
if (!it->is_regular_file(ec)) continue;
if (it->path().filename() == "final_sorted.xls") {
addPreviewWorkbookChoice(choices, seen, it->path(), cfg);
}
}
}
std::string selectedKey = selectedPath.empty() ? "" : normalizedPathPrefix(selectedPath);
int selectedIndex = -1;
SendMessageW(app.previewWorkbook, CB_RESETCONTENT, 0, 0);
for (size_t i = 0; i < choices.size(); ++i) {
SendMessageW(app.previewWorkbook, CB_ADDSTRING, 0, (LPARAM)u8w(choices[i].label).c_str());
if (!selectedKey.empty() && normalizedPathPrefix(choices[i].path) == selectedKey) selectedIndex = (int)i;
}
app.previewWorkbooks = std::move(choices);
if (selectedIndex < 0 && !app.previewWorkbooks.empty()) selectedIndex = 0;
if (selectedIndex >= 0) SendMessageW(app.previewWorkbook, CB_SETCURSEL, selectedIndex, 0);
}
/**
* Function: resetJsonSession
*
* @brief 根据当前模型状态刷新 “resetJsonSession” 对应的缓存或界面。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param app 应用状态;包含窗口句柄、当前选择、预览模型和运行期标志。
* @param traceDir 文件或目录路径;函数按 UTF-8/Windows 路径规则读取或写入。
*/
void resetJsonSession(AppState& app, const std::string& traceDir) {
app.activeTraceDir = traceDir;
// 切换测点追踪时只清掉当前 traceDir 对应的 JSON 记录,其他测点历史还能通过下拉框找回。
app.jsonItems.erase(std::remove_if(app.jsonItems.begin(), app.jsonItems.end(),
[&](const JsonItem& item) {
if (!item.traceDir.empty() && sameTraceDir(item.traceDir, traceDir)) return true;
return !item.path.empty() && pathIsUnderDir(item.path, traceDir);
}),
app.jsonItems.end());
app.jsonRefs.clear();
app.selectedJson = -1;
app.activePreviewSheet = 0;
app.previewStatusFilter = -1;
app.activePreviewWorkbook.clear();
app.previewWorkbooks.clear();
if (app.hwnd) {
setText(app.hwnd, IDC_JSON_PATH, "");
setText(app.hwnd, IDC_ORIGIN_PATHS, "");
}
refreshJsonMonitorChoices(app, traceDir);
rebuildJsonTree(app);
updateJsonDetail(app);
app.previewSheets.clear();
if (app.previewWorkbook) SendMessageW(app.previewWorkbook, CB_RESETCONTENT, 0, 0);
if (app.previewSheet) SendMessageW(app.previewSheet, CB_RESETCONTENT, 0, 0);
if (app.previewFilter) SendMessageW(app.previewFilter, CB_SETCURSEL, 0, 0);
if (app.previewPhase) SendMessageW(app.previewPhase, CB_SETCURSEL, 0, 0);
if (app.previewList) ListView_DeleteAllItems(app.previewList);
if (app.previewStatus) SetWindowTextW(app.previewStatus, L"\x5C1A\x672A\x5F00\x59CB\x6570\x636E\x8FFD\x8E2A");
}
/**
* Function: jsonGroupTitle
*
* @brief 实现界面层的 “jsonGroupTitle” 功能,并在 Win32 控件与 AppState 业务模型之间传递数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param kind 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。
*/
std::wstring jsonGroupTitle(const std::string& kind) {
if (kind == "sent") return L"\x6784\x9020\x548C\x53D1\x9001\x7684JSON";
if (kind == "data") return L"\x6536\x5230\x7684" L"DATATopic JSON";
return L"TraceTopic\x6536\x5230\x7684JSON\x5217\x8868";
}
/**
* Function: updateJsonDetail
*
* @brief 根据当前模型状态刷新 “updateJsonDetail” 对应的缓存或界面。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param app 应用状态;包含窗口句柄、当前选择、预览模型和运行期标志。
*/
void updateJsonDetail(AppState& app) {
std::wstring text;
if (app.selectedJson >= 0 && app.selectedJson < (int)app.jsonItems.size() &&
jsonItemVisible(app, app.jsonItems[app.selectedJson])) {
const auto& item = app.jsonItems[app.selectedJson];
text += L"title: " + u8w(item.title) + L"\r\n";
text += L"type: " + u8w(item.kind) + L"\r\n";
if (!item.monitorId.empty()) text += L"monitor: " + u8w(item.monitorId) + L"\r\n";
if (!item.path.empty()) text += L"path: " + u8w(item.path) + L"\r\n";
text += L"------------------------------\r\n";
text += u8w(prettyJsonText(item.body));
} else {
text = L"\x8BF7\x5728\x5DE6\x4FA7\x9009\x62E9JSON\x8BB0\x5F55\x3002";
}
if (app.jsonDetail) SetWindowTextW(app.jsonDetail, text.c_str());
}
/**
* Function: rebuildJsonTree
*
* @brief 根据当前模型状态刷新 “rebuildJsonTree” 对应的缓存或界面。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param app 应用状态;包含窗口句柄、当前选择、预览模型和运行期标志。
*/
void rebuildJsonTree(AppState& app) {
if (!app.jsonTree) return;
TreeView_DeleteAllItems(app.jsonTree);
app.jsonRefs.clear();
if (app.selectedJson < 0 || app.selectedJson >= (int)app.jsonItems.size() ||
!jsonItemVisible(app, app.jsonItems[app.selectedJson])) {
app.selectedJson = latestVisibleJsonIndex(app);
}
HTREEITEM sentRoot = insertJsonTreeItem(app, TVI_ROOT, jsonGroupTitle("sent"), makeJsonRef(app, -1));
HTREEITEM dataRoot = insertJsonTreeItem(app, TVI_ROOT, jsonGroupTitle("data"), makeJsonRef(app, -1));
HTREEITEM traceRoot = insertJsonTreeItem(app, TVI_ROOT, jsonGroupTitle("trace"), makeJsonRef(app, -1));
HTREEITEM selectedItem = nullptr;
for (size_t i = 0; i < app.jsonItems.size(); ++i) {
const auto& item = app.jsonItems[i];
if (!jsonItemVisible(app, item)) continue;
HTREEITEM parent = item.kind == "sent" ? sentRoot : item.kind == "data" ? dataRoot : traceRoot;
HTREEITEM h = insertJsonTreeItem(app, parent, u8w(item.title), makeJsonRef(app, (int)i));
if ((int)i == app.selectedJson) selectedItem = h;
}
TreeView_Expand(app.jsonTree, sentRoot, TVE_EXPAND);
TreeView_Expand(app.jsonTree, dataRoot, TVE_EXPAND);
TreeView_Expand(app.jsonTree, traceRoot, TVE_EXPAND);
if (selectedItem) TreeView_SelectItem(app.jsonTree, selectedItem);
}
/**
* Function: addJsonEvent
*
* @brief 向目标结构追加或保证存在 “addJsonEvent” 所描述的数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param app 应用状态;包含窗口句柄、当前选择、预览模型和运行期标志。
* @param ev 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
*/
void addJsonEvent(AppState& app, const PqMqEvent& ev) {
JsonItem item{ev.kind, ev.title, ev.body, ev.path, "", ev.monitorId, eventTraceDir(ev)};
if (item.kind.empty()) item.kind = "trace";
if (item.title.empty()) item.title = item.kind;
if (item.kind == "data") {
item.dataType = dataTypeFromJsonText(item.body);
if (item.dataType.empty()) item.dataType = "unknown";
// 同一追踪目录下同一个 DATA_TYPE 保留最新一条,避免列表里堆积重复 MQ 消息。
for (size_t i = 0; i < app.jsonItems.size(); ++i) {
auto& existing = app.jsonItems[i];
if (existing.kind == "data" &&
existing.dataType == item.dataType &&
sameTraceDir(existing.traceDir, item.traceDir)) {
existing = item;
existing.title += " DATA_TYPE=" + existing.dataType;
if (jsonItemVisible(app, existing)) app.selectedJson = (int)i;
if (app.autoPreviewEnabled) {
refreshTraceInputsFromDisk(app, item.traceDir);
autoParseTraceDir(app, item.traceDir, "data");
}
refreshJsonMonitorChoices(app, app.activeTraceDir);
rebuildJsonTree(app);
updateJsonDetail(app);
return;
}
}
item.title += " DATA_TYPE=" + item.dataType;
}
app.jsonItems.push_back(std::move(item));
JsonItem& stored = app.jsonItems.back();
if (jsonItemVisible(app, stored)) app.selectedJson = (int)app.jsonItems.size() - 1;
refreshJsonMonitorChoices(app, app.activeTraceDir.empty() ? stored.traceDir : app.activeTraceDir);
rebuildJsonTree(app);
updateJsonDetail(app);
if (app.autoPreviewEnabled && (stored.kind == "data" || stored.kind == "trace")) {
refreshTraceInputsFromDisk(app, stored.traceDir);
autoParseTraceDir(app, stored.traceDir, stored.kind.c_str());
}
}
/**
* Function: attrValue
*
* @brief 实现界面层的 “attrValue” 功能,并在 Win32 控件与 AppState 业务模型之间传递数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param element 当前处理的数据行、单元格、XML 节点或模型项。
* @param name 待解析、比较、显示或发送的 UTF-8 文本。
* @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。
*/
std::string attrValue(const tinyxml2::XMLElement* element, const char* name) {
if (!element) return "";
const char* value = element->Attribute(name);
return value ? value : "";
}
/**
* Function: cellText
*
* @brief 实现界面层的 “cellText” 功能,并在 Win32 控件与 AppState 业务模型之间传递数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param cell 当前处理的数据行、单元格、XML 节点或模型项。
* @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。
*/
std::string cellText(const tinyxml2::XMLElement* cell) {
if (!cell) return "";
const tinyxml2::XMLElement* data = cell->FirstChildElement("Data");
if (!data) return "";
const char* text = data->GetText();
return text ? text : "";
}
/**
* Function: compactCells
*
* @brief 实现界面层的 “compactCells” 功能,并在 Win32 控件与 AppState 业务模型之间传递数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param cells 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @param start 索引、数量、范围或偏移参数;具体边界由函数体校验。
* @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。
*/
std::string compactCells(const std::vector<std::string>& cells, size_t start) {
size_t end = cells.size();
while (end > start && cells[end - 1].empty()) --end;
std::string out;
for (size_t i = start; i < end; ++i) {
if (!out.empty()) out += ",";
out += cells[i];
}
return truncateText(out);
}
/**
* Function: previewStatusFromStyle
*
* @brief 实现界面层的 “previewStatusFromStyle” 功能,并在 Win32 控件与 AppState 业务模型之间传递数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param style 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @return 整数状态、索引或计数;入口函数以 0 表示成功。
*/
int previewStatusFromStyle(const std::string& style) {
// 这里把 main.cpp 写出的 Excel 样式名转换成 GUI 过滤/着色用的状态码。
if (style == "MatchGreen") return 1;
if (style == "MatchOrange") return 2;
if (style == "MatchRed") return 3;
if (style == "Head") return 4;
if (style == "Section") return 5;
if (style == "Sub") return 6;
return 0;
}
/**
* Function: rowCells
*
* @brief 实现界面层的 “rowCells” 功能,并在 Win32 控件与 AppState 业务模型之间传递数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param row 当前处理的数据行、单元格、XML 节点或模型项。
* @param style 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @param cellStyles 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。
*/
std::vector<std::string> rowCells(const tinyxml2::XMLElement* row,
std::string& style,
std::vector<std::string>* cellStyles = nullptr) {
std::vector<std::string> cells(64);
std::vector<std::string> styles(64);
int col = 1;
for (const tinyxml2::XMLElement* cell = row ? row->FirstChildElement("Cell") : nullptr;
cell;
cell = cell->NextSiblingElement("Cell")) {
// Excel XML 允许跳列,例如 <Cell ss:Index="8">;读取时必须补齐前面的空列。
const char* indexAttr = cell->Attribute("ss:Index");
if (indexAttr && *indexAttr) col = std::max(1, std::atoi(indexAttr));
if ((int)cells.size() < col) {
cells.resize(col);
styles.resize(col);
}
cells[col - 1] = cellText(cell);
std::string cellStyle = attrValue(cell, "ss:StyleID");
styles[col - 1] = cellStyle;
if (style.empty() && !cellStyle.empty()) style = cellStyle;
if (cellStyle.rfind("Match", 0) == 0) style = cellStyle;
++col;
}
while (cells.size() > 1 && cells.back().empty()) {
cells.pop_back();
styles.pop_back();
}
if (cellStyles) *cellStyles = std::move(styles);
return cells;
}
/**
* Function: excelColumnName
*
* @brief 实现界面层的 “excelColumnName” 功能,并在 Win32 控件与 AppState 业务模型之间传递数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param index 索引、数量、范围或偏移参数;具体边界由函数体校验。
* @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。
*/
std::string excelColumnName(int index) {
std::string name;
++index;
while (index > 0) {
int rem = (index - 1) % 26;
name.insert(name.begin(), static_cast<char>('A' + rem));
index = (index - 1) / 26;
}
return name;
}
/**
* Function: resetPreviewColumns
*
* @brief 根据当前模型状态刷新 “resetPreviewColumns” 对应的缓存或界面。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param list Win32 窗口或控件句柄;必须在有效窗口生命周期内使用。
* @param colCount 索引、数量、范围或偏移参数;具体边界由函数体校验。
*/
void resetPreviewColumns(HWND list, size_t colCount) {
if (!list) return;
while (ListView_DeleteColumn(list, 0)) {
}
colCount = std::max<size_t>(colCount, 12);
colCount = std::min<size_t>(colCount, 64);
int zoom = g_app ? std::max(60, std::min(200, g_app->previewZoomPercent)) : 100;
for (size_t i = 0; i < colCount; ++i) {
std::string name = excelColumnName(static_cast<int>(i));
std::wstring title = u8w(name);
LVCOLUMNW col{};
col.mask = LVCF_TEXT | LVCF_WIDTH | LVCF_SUBITEM;
col.pszText = title.data();
int baseWidth = i == 0 ? 210 : (i == 4 || i == 6 || i == 8 ? 300 : 110);
col.cx = std::max(45, baseWidth * zoom / 100);
col.iSubItem = static_cast<int>(i);
ListView_InsertColumn(list, static_cast<int>(i), &col);
}
}
/**
* Function: previewRowMatchesFilter
*
* @brief 实现界面层的 “previewRowMatchesFilter” 功能,并在 Win32 控件与 AppState 业务模型之间传递数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param row 当前处理的数据行、单元格、XML 节点或模型项。
* @param statusFilter 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @return 条件成立或操作成功时为 true否则为 false。
*/
bool previewRowMatchesFilter(const PreviewSheetRow& row, int statusFilter) {
if (statusFilter < 0) return true;
return row.status == statusFilter;
}
/**
* Function: phaseFromText
*
* @brief 实现界面层的 “phaseFromText” 功能,并在 Win32 控件与 AppState 业务模型之间传递数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param text 待解析、比较、显示或发送的 UTF-8 文本。
* @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。
*/
std::string phaseFromText(const std::string& text) {
if (text.rfind("A相", 0) == 0 || text == "A 相") return "A";
if (text.rfind("B相", 0) == 0 || text == "B 相") return "B";
if (text.rfind("C相", 0) == 0 || text == "C 相") return "C";
if (text.rfind("T相", 0) == 0 || text == "T 相") return "T";
return "";
}
/**
* Function: labelWithoutPhase
*
* @brief 实现界面层的 “labelWithoutPhase” 功能,并在 Win32 控件与 AppState 业务模型之间传递数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param text 待解析、比较、显示或发送的 UTF-8 文本。
* @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。
*/
std::string labelWithoutPhase(std::string text) {
if (text.rfind("A相", 0) == 0 || text.rfind("B相", 0) == 0 ||
text.rfind("C相", 0) == 0 || text.rfind("T相", 0) == 0) {
return text.substr(std::string("A相").size());
}
return text;
}
/**
* Function: previewRowMatchesPhase
*
* @brief 实现界面层的 “previewRowMatchesPhase” 功能,并在 Win32 控件与 AppState 业务模型之间传递数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param row 当前处理的数据行、单元格、XML 节点或模型项。
* @param phaseFilter 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @return 条件成立或操作成功时为 true否则为 false。
*/
bool previewRowMatchesPhase(const PreviewSheetRow& row, const std::string& phaseFilter) {
if (phaseFilter.empty()) return true;
if (row.status == 1 || row.status == 2 || row.status == 3) return row.phase == phaseFilter;
if (row.status == 6) return row.phase.empty() || row.phase == phaseFilter;
return true;
}
/**
* Function: previewRowMatchesSearch
*
* @brief 实现界面层的 “previewRowMatchesSearch” 功能,并在 Win32 控件与 AppState 业务模型之间传递数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param row 当前处理的数据行、单元格、XML 节点或模型项。
* @param searchText 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @return 条件成立或操作成功时为 true否则为 false。
*/
bool previewRowMatchesSearch(const PreviewSheetRow& row, const std::string& searchText) {
if (trimAscii(searchText).empty()) return true;
for (const auto& cell : row.cells) {
if (textContainsFilter(cell, searchText)) return true;
}
return false;
}
/**
* Function: capturePreviewViewport
*
* @brief 实现界面层的 “capturePreviewViewport” 功能,并在 Win32 控件与 AppState 业务模型之间传递数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param app 应用状态;包含窗口句柄、当前选择、预览模型和运行期标志。
* @param topModelRow 当前处理的数据行、单元格、XML 节点或模型项。
* @param horzPos 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
*/
void capturePreviewViewport(AppState& app, int& topModelRow, int& horzPos) {
topModelRow = -1;
horzPos = 0;
if (!app.previewList) return;
int topItem = (int)SendMessageW(app.previewList, LVM_GETTOPINDEX, 0, 0);
LVITEMW lv{};
lv.mask = LVIF_PARAM;
lv.iItem = topItem;
if (ListView_GetItem(app.previewList, &lv)) topModelRow = (int)lv.lParam;
SCROLLINFO si{};
si.cbSize = sizeof(si);
si.fMask = SIF_POS;
if (GetScrollInfo(app.previewList, SB_HORZ, &si)) horzPos = si.nPos;
}
/**
* Function: restorePreviewViewport
*
* @brief 实现界面层的 “restorePreviewViewport” 功能,并在 Win32 控件与 AppState 业务模型之间传递数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param app 应用状态;包含窗口句柄、当前选择、预览模型和运行期标志。
* @param topModelRow 当前处理的数据行、单元格、XML 节点或模型项。
* @param horzPos 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
*/
void restorePreviewViewport(AppState& app, int topModelRow, int horzPos) {
if (!app.previewList) return;
int count = ListView_GetItemCount(app.previewList);
if (count <= 0) return;
int targetVisibleIndex = -1;
if (topModelRow >= 0) {
for (int i = 0; i < count; ++i) {
LVITEMW lv{};
lv.mask = LVIF_PARAM;
lv.iItem = i;
if (ListView_GetItem(app.previewList, &lv) && (int)lv.lParam == topModelRow) {
targetVisibleIndex = i;
break;
}
}
}
RECT rc{};
int rowHeight = 18;
if (ListView_GetItemRect(app.previewList, 0, &rc, LVIR_BOUNDS)) {
rowHeight = std::max(1L, rc.bottom - rc.top);
}
if (targetVisibleIndex > 0) {
ListView_EnsureVisible(app.previewList, targetVisibleIndex, FALSE);
int currentTop = (int)SendMessageW(app.previewList, LVM_GETTOPINDEX, 0, 0);
int deltaRows = targetVisibleIndex - currentTop;
if (deltaRows != 0) ListView_Scroll(app.previewList, 0, deltaRows * rowHeight);
}
if (horzPos > 0) {
SCROLLINFO si{};
si.cbSize = sizeof(si);
si.fMask = SIF_POS;
int current = GetScrollInfo(app.previewList, SB_HORZ, &si) ? si.nPos : 0;
if (horzPos != current) ListView_Scroll(app.previewList, horzPos - current, 0);
}
}
/**
* Function: populatePreviewList
*
* @brief 根据当前模型状态刷新 “populatePreviewList” 对应的缓存或界面。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param app 应用状态;包含窗口句柄、当前选择、预览模型和运行期标志。
* @param forcedTopModelRow 当前处理的数据行、单元格、XML 节点或模型项。
* @param forcedHorzPos 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
*/
void populatePreviewList(AppState& app, int forcedTopModelRow = -1, int forcedHorzPos = -1) {
int oldTopModelRow = -1;
int oldHorzPos = 0;
if (forcedTopModelRow >= 0) {
oldTopModelRow = forcedTopModelRow;
oldHorzPos = std::max(0, forcedHorzPos);
} else {
capturePreviewViewport(app, oldTopModelRow, oldHorzPos);
}
if (!app.previewList) return;
SendMessageW(app.previewList, WM_SETREDRAW, FALSE, 0);
ListView_DeleteAllItems(app.previewList);
if (app.previewSheets.empty()) {
resetPreviewColumns(app.previewList, 12);
SendMessageW(app.previewList, WM_SETREDRAW, TRUE, 0);
InvalidateRect(app.previewList, nullptr, TRUE);
return;
}
if (app.activePreviewSheet < 0 || app.activePreviewSheet >= (int)app.previewSheets.size()) {
app.activePreviewSheet = 0;
}
const PreviewSheet& sheet = app.previewSheets[app.activePreviewSheet];
resetPreviewColumns(app.previewList, sheet.maxCols);
int statusFilter = app.previewStatusFilter;
int visibleRows = 0;
// ListView 只显示过滤后的行,但 lParam 仍保存原始模型行号,编辑时用它回写 previewSheets。
for (size_t i = 0; i < sheet.rows.size(); ++i) {
const PreviewSheetRow& row = sheet.rows[i];
if (!previewRowMatchesFilter(row, statusFilter)) continue;
if (!previewRowMatchesPhase(row, app.previewPhaseFilter)) continue;
if (!previewRowMatchesSearch(row, app.previewSearchText)) continue;
LVITEMW item{};
std::wstring firstText = row.cells.empty() ? L"" : u8w(row.cells[0]);
item.mask = LVIF_TEXT | LVIF_PARAM;
item.iItem = visibleRows++;
item.pszText = firstText.data();
item.lParam = static_cast<LPARAM>(i);
ListView_InsertItem(app.previewList, &item);
for (size_t c = 1; c < row.cells.size(); ++c) {
std::wstring cellTextW = u8w(row.cells[c]);
ListView_SetItemText(app.previewList, visibleRows - 1, static_cast<int>(c),
cellTextW.data());
}
}
SendMessageW(app.previewList, WM_SETREDRAW, TRUE, 0);
restorePreviewViewport(app, oldTopModelRow, oldHorzPos);
InvalidateRect(app.previewList, nullptr, TRUE);
}
/**
* Function: isAbcPhase
*
* @brief 判断 “isAbcPhase” 所表达的条件;该函数只读取输入,不负责修改业务状态。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param phase 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @return 条件成立或操作成功时为 true否则为 false。
*/
bool isAbcPhase(const std::string& phase) {
return phase == "A" || phase == "B" || phase == "C";
}
/**
* Function: isEditablePreviewCell
*
* @brief 判断 “isEditablePreviewCell” 所表达的条件;该函数只读取输入,不负责修改业务状态。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param app 应用状态;包含窗口句柄、当前选择、预览模型和运行期标志。
* @param rowIndex 索引、数量、范围或偏移参数;具体边界由函数体校验。
* @param colIndex 索引、数量、范围或偏移参数;具体边界由函数体校验。
* @return 条件成立或操作成功时为 true否则为 false。
*/
bool isEditablePreviewCell(const AppState& app, int rowIndex, int colIndex) {
if (app.activePreviewSheet < 0 || app.activePreviewSheet >= (int)app.previewSheets.size()) return false;
const auto& rows = app.previewSheets[app.activePreviewSheet].rows;
if (rowIndex < 0 || rowIndex >= (int)rows.size()) return false;
const PreviewSheetRow& row = rows[rowIndex];
// 只允许改“数组基准/取值偏移量/最终值偏移/配置项”这几列,避免误改最终值或 origin 值。
if (colIndex < 1 || colIndex > 4) return false;
if (!(row.status == 1 || row.status == 2 || row.status == 3)) return false;
return !row.cells.empty() && !row.cells[0].empty();
}
/**
* Function: previewRowIndexFromListItem
*
* @brief 实现界面层的 “previewRowIndexFromListItem” 功能,并在 Win32 控件与 AppState 业务模型之间传递数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param list Win32 窗口或控件句柄;必须在有效窗口生命周期内使用。
* @param item 当前处理的数据行、单元格、XML 节点或模型项。
* @return 整数状态、索引或计数;入口函数以 0 表示成功。
*/
int previewRowIndexFromListItem(HWND list, int item) {
LVITEMW lv{};
lv.mask = LVIF_PARAM;
lv.iItem = item;
if (!ListView_GetItem(list, &lv)) return -1;
return static_cast<int>(lv.lParam);
}
/**
* Function: syncPreviewPhaseGroup
*
* @brief 把 “syncPreviewPhaseGroup” 对应的变更应用到模型、控件或输出文档。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param app 应用状态;包含窗口句柄、当前选择、预览模型和运行期标志。
* @param rowIndex 索引、数量、范围或偏移参数;具体边界由函数体校验。
* @param colIndex 索引、数量、范围或偏移参数;具体边界由函数体校验。
* @param value 待解析、比较、显示或发送的 UTF-8 文本。
*/
void syncPreviewPhaseGroup(AppState& app, int rowIndex, int colIndex, const std::string& value) {
if (app.activePreviewSheet < 0 || app.activePreviewSheet >= (int)app.previewSheets.size()) return;
PreviewSheet& sheet = app.previewSheets[app.activePreviewSheet];
if (rowIndex < 0 || rowIndex >= (int)sheet.rows.size()) return;
const PreviewSheetRow& row = sheet.rows[rowIndex];
if (!isAbcPhase(row.phase) || row.cells.empty()) return;
std::string baseLabel = labelWithoutPhase(row.cells[0]);
// A/B/C 三相同一个指标通常应该保持同一配置;编辑一相时同步另外两相。
for (auto& other : sheet.rows) {
if (!isAbcPhase(other.phase) || other.cells.empty() || other.cells[0].empty()) continue;
if (labelWithoutPhase(other.cells[0]) != baseLabel) continue;
if ((int)other.cells.size() <= colIndex) other.cells.resize(colIndex + 1);
other.cells[colIndex] = value;
}
}
/**
* Function: parsePreviewRange
*
* @brief 解析 “parsePreviewRange” 对应的文本或结构,提取经过校验的业务字段。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param text 待解析、比较、显示或发送的 UTF-8 文本。
* @param start 索引、数量、范围或偏移参数;具体边界由函数体校验。
* @param end 索引、数量、范围或偏移参数;具体边界由函数体校验。
* @return 条件成立或操作成功时为 true否则为 false。
*/
bool parsePreviewRange(const std::string& text, int& start, int& end) {
std::smatch m;
static const std::regex re(R"(^\s*(-?\d+)\s*-->\s*(-?\d+)\s*$)");
if (!std::regex_match(text, m, re)) return false;
try {
start = std::stoi(m[1].str());
end = std::stoi(m[2].str());
return start <= end;
} catch (...) {
return false;
}
}
/**
* Function: parsePreviewFieldRange
*
* @brief 解析 “parsePreviewFieldRange” 对应的文本或结构,提取经过校验的业务字段。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param field 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @param start 索引、数量、范围或偏移参数;具体边界由函数体校验。
* @param end 索引、数量、范围或偏移参数;具体边界由函数体校验。
* @return 条件成立或操作成功时为 true否则为 false。
*/
bool parsePreviewFieldRange(const std::string& field, int& start, int& end) {
std::smatch m;
static const std::regex re(R"(\[(-?\d+)-(-?\d+)\])");
if (!std::regex_search(field, m, re)) return false;
try {
start = std::stoi(m[1].str());
end = std::stoi(m[2].str());
return start <= end;
} catch (...) {
return false;
}
}
/**
* Function: parsePreviewOffset
*
* @brief 解析 “parsePreviewOffset” 对应的文本或结构,提取经过校验的业务字段。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param text 待解析、比较、显示或发送的 UTF-8 文本。
* @return 整数状态、索引或计数;入口函数以 0 表示成功。
*/
int parsePreviewOffset(const std::string& text) {
try {
std::string value = trimAscii(text);
return value.empty() ? 0 : std::stoi(value);
} catch (...) {
return 0;
}
}
/**
* Function: replacePreviewFieldRange
*
* @brief 实现界面层的 “replacePreviewFieldRange” 功能,并在 Win32 控件与 AppState 业务模型之间传递数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param field 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @param start 索引、数量、范围或偏移参数;具体边界由函数体校验。
* @param end 索引、数量、范围或偏移参数;具体边界由函数体校验。
* @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。
*/
std::string replacePreviewFieldRange(const std::string& field, int start, int end) {
static const std::regex re(R"(\[(-?\d+)-(-?\d+)\])");
return std::regex_replace(field, re,
"[" + std::to_string(start) + "-" + std::to_string(end) + "]",
std::regex_constants::format_first_only);
}
/**
* Function: effectivePreviewField
*
* @brief 实现界面层的 “effectivePreviewField” 功能,并在 Win32 控件与 AppState 业务模型之间传递数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param field 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。
*/
std::string effectivePreviewField(const std::string& field) {
size_t arrow = field.rfind(" >> ");
return arrow == std::string::npos ? field : field.substr(arrow + 4);
}
/**
* Function: recalculatePreviewEditEffects
*
* @brief 实现界面层的 “recalculatePreviewEditEffects” 功能,并在 Win32 控件与 AppState 业务模型之间传递数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param sheet 当前处理的数据行、单元格、XML 节点或模型项。
*/
void recalculatePreviewEditEffects(PreviewSheet& sheet) {
for (auto& row : sheet.rows) {
row.finalRangeChanged = false;
row.modifiedOriginPickColumn = -1;
// 字段列不可直接编辑,因此每次从工作簿原值重新计算,恢复参数时可无损恢复。
if (!row.cells.empty() && !row.cells[0].empty() && row.originalCells.size() > 8) {
if (row.cells.size() <= 8) row.cells.resize(9);
row.cells[8] = row.originalCells[8];
}
}
for (size_t i = 0; i < sheet.rows.size(); ++i) {
PreviewSheetRow& row = sheet.rows[i];
if (!(row.status == 1 || row.status == 2 || row.status == 3)) continue;
if (row.cells.empty() || row.cells[0].empty() || row.cells.size() <= 8 || row.originalCells.size() <= 8) continue;
int oldBaseStart = 0, oldBaseEnd = 0, newBaseStart = 0, newBaseEnd = 0;
bool oldBaseOk = row.originalCells.size() > 1 && parsePreviewRange(row.originalCells[1], oldBaseStart, oldBaseEnd);
bool newBaseOk = row.cells.size() > 1 && parsePreviewRange(row.cells[1], newBaseStart, newBaseEnd);
int oldValueOffset = row.originalCells.size() > 2 ? parsePreviewOffset(row.originalCells[2]) : 0;
int newValueOffset = row.cells.size() > 2 ? parsePreviewOffset(row.cells[2]) : 0;
int oldFinalOffset = row.originalCells.size() > 3 ? parsePreviewOffset(row.originalCells[3]) : 0;
int newFinalOffset = row.cells.size() > 3 ? parsePreviewOffset(row.cells[3]) : 0;
int oldFieldStart = 0, oldFieldEnd = 0;
if (oldBaseOk && newBaseOk && parsePreviewFieldRange(row.originalCells[8], oldFieldStart, oldFieldEnd)) {
int newFieldStart = oldFieldStart + (newBaseStart - oldBaseStart) + (newFinalOffset - oldFinalOffset);
int newFieldEnd = oldFieldEnd + (newBaseEnd - oldBaseEnd) + (newFinalOffset - oldFinalOffset);
std::string newField = replacePreviewFieldRange(row.originalCells[8], newFieldStart, newFieldEnd);
if (newField != row.originalCells[8]) {
row.cells[8] = row.originalCells[8] + " >> " + newField;
row.finalRangeChanged = true;
}
}
if (!oldBaseOk || !newBaseOk || i + 1 >= sheet.rows.size()) continue;
PreviewSheetRow& originRow = sheet.rows[i + 1];
if (originRow.cells.size() <= 8 || originRow.cells[7] != "原始值") continue;
int originalBlueColumn = -1;
for (size_t col = 9; col < originRow.cellStyles.size(); ++col) {
if (originRow.cellStyles[col] == "OriginPickBlue") {
originalBlueColumn = static_cast<int>(col);
break;
}
}
if (originalBlueColumn < 0) continue;
int oldPickStart = oldBaseStart + oldValueOffset;
int newPickStart = newBaseStart + newValueOffset;
int purpleColumn = originalBlueColumn + (newPickStart - oldPickStart);
if (newPickStart != oldPickStart && purpleColumn >= 9 && purpleColumn < (int)originRow.cells.size()) {
originRow.modifiedOriginPickColumn = purpleColumn;
}
}
}
/**
* Function: commitPreviewEdit
*
* @brief 把 “commitPreviewEdit” 对应的变更应用到模型、控件或输出文档。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param app 应用状态;包含窗口句柄、当前选择、预览模型和运行期标志。
* @param save 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
*/
void commitPreviewEdit(AppState& app, bool save) {
HWND edit = app.previewEdit;
if (!edit) return;
int rowIndex = app.editPreviewRow;
int colIndex = app.editPreviewCol;
int topModelRow = app.editPreviewTopModelRow;
int horzPos = app.editPreviewHorzPos;
app.previewEdit = nullptr;
app.editPreviewRow = -1;
app.editPreviewCol = -1;
app.editPreviewTopModelRow = -1;
app.editPreviewHorzPos = 0;
if (save && app.activePreviewSheet >= 0 && app.activePreviewSheet < (int)app.previewSheets.size() &&
rowIndex >= 0 && rowIndex < (int)app.previewSheets[app.activePreviewSheet].rows.size()) {
int len = GetWindowTextLengthW(edit);
std::wstring wtext(len + 1, L'\0');
GetWindowTextW(edit, wtext.data(), len + 1);
wtext.resize(len);
std::string value = w8(wtext);
PreviewSheetRow& row = app.previewSheets[app.activePreviewSheet].rows[rowIndex];
if ((int)row.cells.size() <= colIndex) row.cells.resize(colIndex + 1);
row.cells[colIndex] = value;
syncPreviewPhaseGroup(app, rowIndex, colIndex, value);
recalculatePreviewEditEffects(app.previewSheets[app.activePreviewSheet]);
app.reverseXmlDirty = true;
}
DestroyWindow(edit);
populatePreviewList(app, topModelRow, horzPos);
}
/**
* Function: startPreviewEdit
*
* @brief 实现界面层的 “startPreviewEdit” 功能,并在 Win32 控件与 AppState 业务模型之间传递数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param app 应用状态;包含窗口句柄、当前选择、预览模型和运行期标志。
* @param listItem 当前处理的数据行、单元格、XML 节点或模型项。
* @param subItem 当前处理的数据行、单元格、XML 节点或模型项。
*/
void startPreviewEdit(AppState& app, int listItem, int subItem) {
int rowIndex = previewRowIndexFromListItem(app.previewList, listItem);
if (!isEditablePreviewCell(app, rowIndex, subItem)) return;
commitPreviewEdit(app, true);
int topModelRow = -1;
int horzPos = 0;
capturePreviewViewport(app, topModelRow, horzPos);
RECT rc{};
rc.top = subItem;
rc.left = LVIR_BOUNDS;
if (!SendMessageW(app.previewList, LVM_GETSUBITEMRECT, listItem, (LPARAM)&rc)) return;
POINT pts[2] = {{rc.left, rc.top}, {rc.right, rc.bottom}};
MapWindowPoints(app.previewList, app.hwnd, pts, 2);
PreviewSheetRow& row = app.previewSheets[app.activePreviewSheet].rows[rowIndex];
std::string text = subItem < (int)row.cells.size() ? row.cells[subItem] : "";
HWND edit = CreateWindowExW(WS_EX_CLIENTEDGE, L"EDIT", u8w(text).c_str(),
WS_CHILD | WS_VISIBLE | WS_BORDER | ES_AUTOHSCROLL | ES_NOHIDESEL,
pts[0].x, pts[0].y,
std::max<LONG>(90, pts[1].x - pts[0].x), std::max<LONG>(24, pts[1].y - pts[0].y),
app.hwnd, (HMENU)(INT_PTR)IDC_PREVIEW_CELL_EDIT, app.inst, nullptr);
applyFont(app, edit);
SetWindowPos(edit, HWND_TOP, pts[0].x, pts[0].y,
std::max<LONG>(90, pts[1].x - pts[0].x), std::max<LONG>(24, pts[1].y - pts[0].y),
SWP_SHOWWINDOW);
app.previewEdit = edit;
app.editPreviewRow = rowIndex;
app.editPreviewCol = subItem;
app.editPreviewTopModelRow = topModelRow;
app.editPreviewHorzPos = horzPos;
SendMessageW(edit, EM_SETSEL, 0, -1);
SetFocus(edit);
}
/**
* Function: refreshPreviewFromWorkbook
*
* @brief 根据当前模型状态刷新 “refreshPreviewFromWorkbook” 对应的缓存或界面。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param app 应用状态;包含窗口句柄、当前选择、预览模型和运行期标志。
* @param workbookPath 文件或目录路径;函数按 UTF-8/Windows 路径规则读取或写入。
*/
void refreshPreviewFromWorkbook(AppState& app, const std::string& workbookPath) {
if (app.previewEdit) {
if (app.previewStatus) {
SetWindowTextW(app.previewStatus, L"\u6570\u636E\u5DF2\u5199\u5165\uFF0C\u7F16\u8F91\u7ED3\u675F\u540E\u518D\u5237\u65B0\u9884\u89C8");
}
return;
}
commitPreviewEdit(app, true);
int oldActiveSheet = app.activePreviewSheet;
std::string oldWorkbook = app.activePreviewWorkbook;
app.activePreviewWorkbook = workbookPath;
app.reverseXmlDirty = true;
if (workbookPath.empty() || !fs::exists(fs::u8path(workbookPath))) {
app.previewSheets.clear();
app.activePreviewSheet = 0;
if (app.previewSheet) SendMessageW(app.previewSheet, CB_RESETCONTENT, 0, 0);
if (app.previewStatus) SetWindowTextW(app.previewStatus, L"\x5C1A\x672A\x751F\x6210\x8868\x683C");
populatePreviewList(app);
return;
}
// 工作簿是 Excel 2003 XML 格式,不需要 COM/Excel直接用 tinyxml2 读 Worksheet/Row/Cell。
tinyxml2::XMLDocument doc;
std::string xml = readAllText(fs::u8path(workbookPath));
if (doc.Parse(xml.c_str(), xml.size()) != tinyxml2::XML_SUCCESS) {
app.previewSheets.clear();
app.activePreviewSheet = 0;
if (app.previewSheet) SendMessageW(app.previewSheet, CB_RESETCONTENT, 0, 0);
if (app.previewStatus) SetWindowTextW(app.previewStatus, L"\x8868\x683C\x9884\x89C8\x89E3\x6790\x5931\x8D25");
populatePreviewList(app);
return;
}
std::vector<PreviewSheet> loadedSheets;
const tinyxml2::XMLElement* workbook = doc.FirstChildElement("Workbook");
for (const tinyxml2::XMLElement* ws = workbook ? workbook->FirstChildElement("Worksheet") : nullptr;
ws;
ws = ws->NextSiblingElement("Worksheet")) {
PreviewSheet sheet;
sheet.name = attrValue(ws, "ss:Name");
const tinyxml2::XMLElement* table = ws->FirstChildElement("Table");
std::string currentPhase;
std::string lastDataPhase;
for (const tinyxml2::XMLElement* row = table ? table->FirstChildElement("Row") : nullptr;
row;
row = row->NextSiblingElement("Row")) {
std::string style;
PreviewSheetRow previewRow;
previewRow.cells = rowCells(row, style, &previewRow.cellStyles);
previewRow.originalCells = previewRow.cells;
previewRow.status = previewStatusFromStyle(style);
std::string firstCell = previewRow.cells.empty() ? "" : previewRow.cells[0];
std::string explicitPhase = phaseFromText(firstCell);
if (previewRow.status == 6 && !explicitPhase.empty()) currentPhase = explicitPhase;
if (previewRow.status == 1 || previewRow.status == 2 || previewRow.status == 3) {
previewRow.phase = explicitPhase.empty() ? (firstCell.empty() ? lastDataPhase : currentPhase) : explicitPhase;
if (!previewRow.phase.empty()) lastDataPhase = previewRow.phase;
} else if (previewRow.status == 6) {
previewRow.phase = explicitPhase;
}
sheet.maxCols = std::max(sheet.maxCols, previewRow.cells.size());
sheet.rows.push_back(std::move(previewRow));
}
if (sheet.maxCols == 0) sheet.maxCols = 12;
loadedSheets.push_back(std::move(sheet));
}
app.previewSheets = std::move(loadedSheets);
if (oldWorkbook == workbookPath &&
oldActiveSheet >= 0 &&
oldActiveSheet < (int)app.previewSheets.size()) {
app.activePreviewSheet = oldActiveSheet;
} else {
app.activePreviewSheet = 0;
}
if (app.previewSheet) {
SendMessageW(app.previewSheet, CB_RESETCONTENT, 0, 0);
for (const auto& sheet : app.previewSheets) {
SendMessageW(app.previewSheet, CB_ADDSTRING, 0, (LPARAM)u8w(sheet.name).c_str());
}
if (!app.previewSheets.empty()) SendMessageW(app.previewSheet, CB_SETCURSEL, app.activePreviewSheet, 0);
}
populatePreviewList(app);
if (app.previewStatus) {
std::string text = "Workbook synced: " + workbookPath + ", sheets: " + std::to_string(app.previewSheets.size());
SetWindowTextW(app.previewStatus, u8w(text).c_str());
}
}
/**
* Function: makeMqEventCallback
*
* @brief 构造 “makeMqEventCallback” 对应的对象、消息、窗口控件或输出结构。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param app 应用状态;包含窗口句柄、当前选择、预览模型和运行期标志。
* @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。
*/
PqMqEventFn makeMqEventCallback(AppState& app) {
HWND hwnd = app.hwnd;
return [hwnd](const PqMqEvent& ev) {
auto* copy = new PqMqEvent(ev);
if (!PostMessageW(hwnd, WM_PQ_MQ_EVENT, 0, (LPARAM)copy)) delete copy;
};
}
/**
* Function: selectedFrontType
*
* @brief 实现界面层的 “selectedFrontType” 功能,并在 Win32 控件与 AppState 业务模型之间传递数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param app 应用状态;包含窗口句柄、当前选择、预览模型和运行期标志。
* @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。
*/
std::string selectedFrontType(AppState& app) {
int idx = (int)SendMessageW(app.frontType, CB_GETCURSEL, 0, 0);
wchar_t buf[128]{};
SendMessageW(app.frontType, CB_GETLBTEXT, idx < 0 ? 0 : idx, (LPARAM)buf);
return w8(buf);
}
/**
* Function: openFileDialog
*
* @brief 实现界面层的 “openFileDialog” 功能,并在 Win32 控件与 AppState 业务模型之间传递数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param owner 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @param title 待解析、比较、显示或发送的 UTF-8 文本。
* @param filter 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @param out 输出参数;函数在成功或失败时写入结果或诊断信息。
* @return 条件成立或操作成功时为 true否则为 false。
*/
bool openFileDialog(HWND owner, const wchar_t* title, const wchar_t* filter, std::string& out) {
wchar_t file[4096]{};
OPENFILENAMEW ofn{};
ofn.lStructSize = sizeof(ofn);
ofn.hwndOwner = owner;
ofn.lpstrTitle = title;
ofn.lpstrFilter = filter;
ofn.lpstrFile = file;
ofn.nMaxFile = (DWORD)std::size(file);
ofn.Flags = OFN_FILEMUSTEXIST | OFN_PATHMUSTEXIST;
if (!GetOpenFileNameW(&ofn)) return false;
out = w8(file);
return true;
}
/**
* Function: saveFileDialog
*
* @brief 把 “saveFileDialog” 对应的内存数据序列化并写入目标位置。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param owner 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @param title 待解析、比较、显示或发送的 UTF-8 文本。
* @param filter 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @param defaultExt 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @param out 输出参数;函数在成功或失败时写入结果或诊断信息。
* @return 条件成立或操作成功时为 true否则为 false。
*/
bool saveFileDialog(HWND owner, const wchar_t* title, const wchar_t* filter, const wchar_t* defaultExt, std::string& out) {
wchar_t file[4096]{};
OPENFILENAMEW ofn{};
ofn.lStructSize = sizeof(ofn);
ofn.hwndOwner = owner;
ofn.lpstrTitle = title;
ofn.lpstrFilter = filter;
ofn.lpstrDefExt = defaultExt;
ofn.lpstrFile = file;
ofn.nMaxFile = (DWORD)std::size(file);
ofn.Flags = OFN_PATHMUSTEXIST | OFN_OVERWRITEPROMPT;
if (!GetSaveFileNameW(&ofn)) return false;
out = w8(file);
return true;
}
/**
* Function: collectOriginFiles
*
* @brief 遍历输入结构并收集 “collectOriginFiles” 所需的候选数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param dir 文件或目录路径;函数按 UTF-8/Windows 路径规则读取或写入。
* @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。
*/
std::vector<std::string> collectOriginFiles(const fs::path& dir) {
std::vector<std::string> files;
if (!fs::exists(dir)) return files;
for (const auto& entry : fs::directory_iterator(dir)) {
if (!entry.is_regular_file()) continue;
std::string name = entry.path().filename().u8string();
if (name.rfind("origin", 0) == 0 && entry.path().extension() == ".txt") {
files.push_back(entry.path().u8string());
}
}
std::sort(files.begin(), files.end());
return files;
}
/**
* Function: collectJsonDataFiles
*
* @brief 遍历输入结构并收集 “collectJsonDataFiles” 所需的候选数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param dir 文件或目录路径;函数按 UTF-8/Windows 路径规则读取或写入。
* @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。
*/
std::vector<std::string> collectJsonDataFiles(const fs::path& dir) {
std::vector<std::string> files;
if (!fs::exists(dir)) return files;
for (const auto& entry : fs::directory_iterator(dir)) {
if (!entry.is_regular_file()) continue;
std::string name = entry.path().filename().u8string();
if (name.rfind("jsondata", 0) == 0 &&
(entry.path().extension() == ".txt" || entry.path().extension() == ".json")) {
files.push_back(entry.path().u8string());
}
}
std::sort(files.begin(), files.end());
return files;
}
/**
* Function: joinPaths
*
* @brief 实现界面层的 “joinPaths” 功能,并在 Win32 控件与 AppState 业务模型之间传递数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param paths 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。
*/
std::string joinPaths(const std::vector<std::string>& paths) {
std::string out;
for (const auto& p : paths) {
if (!out.empty()) out += "\r\n";
out += p;
}
return out;
}
/**
* Function: prepareTraceWorkspace
*
* @brief 实现界面层的 “prepareTraceWorkspace” 功能,并在 Win32 控件与 AppState 业务模型之间传递数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param app 应用状态;包含窗口句柄、当前选择、预览模型和运行期标志。
* @param cfg 运行配置提供接口地址、MQ 参数、路径和当前数据模式。
* @param d 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @param m 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @param updateUi 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
*/
void prepareTraceWorkspace(AppState& app,
PqToolConfig& cfg,
const PqLedgerDevice& d,
const PqLedgerMonitor& m,
bool updateUi) {
fs::path dir = fs::u8path(pqTraceDirectoryFor(cfg, d, m));
fs::create_directories(dir);
std::string xmlPath = pqFindDeviceXmlPath(cfg, d);
if (!xmlPath.empty()) cfg.xmlPath = xmlPath;
cfg.jsonDataPaths = collectJsonDataFiles(dir);
cfg.jsonDataPath = joinPaths(cfg.jsonDataPaths);
cfg.originPaths = collectOriginFiles(dir);
cfg.outputPath = (dir / "final_sorted.xls").u8string();
if (updateUi) {
if (!cfg.xmlPath.empty()) setText(app.hwnd, IDC_XML_PATH, cfg.xmlPath);
setText(app.hwnd, IDC_JSON_PATH, cfg.jsonDataPath);
setText(app.hwnd, IDC_ORIGIN_PATHS, joinPaths(cfg.originPaths));
setText(app.hwnd, IDC_OUTPUT_PATH, cfg.outputPath);
}
}
/**
* Function: addJsonMonitorChoice
*
* @brief 向目标结构追加或保证存在 “addJsonMonitorChoice” 所描述的数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param app 应用状态;包含窗口句柄、当前选择、预览模型和运行期标志。
* @param seen 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @param label 待解析、比较、显示或发送的 UTF-8 文本。
* @param traceDir 文件或目录路径;函数按 UTF-8/Windows 路径规则读取或写入。
* @param device 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @param monitor 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
*/
void addJsonMonitorChoice(AppState& app,
std::unordered_set<std::string>& seen,
const std::string& label,
const std::string& traceDir,
int device,
int monitor) {
std::string key = traceDir.empty() ? "all" : normalizedPathPrefix(traceDir);
if (!seen.insert(key).second) return;
app.jsonMonitorChoices.push_back({label, traceDir, device, monitor});
}
/**
* Function: contextStringValue
*
* @brief 实现界面层的 “contextStringValue” 功能,并在 Win32 控件与 AppState 业务模型之间传递数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param contextPath 文件或目录路径;函数按 UTF-8/Windows 路径规则读取或写入。
* @param key 待解析、比较、显示或发送的 UTF-8 文本。
* @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。
*/
std::string contextStringValue(const fs::path& contextPath, const char* key) {
try {
if (!fs::exists(contextPath)) return "";
json context = json::parse(readAllText(contextPath));
auto it = context.find(key);
return it == context.end() ? "" : jsonScalarString(*it);
} catch (...) {
return "";
}
}
/**
* Function: refreshJsonMonitorChoices
*
* @brief 根据当前模型状态刷新 “refreshJsonMonitorChoices” 对应的缓存或界面。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param app 应用状态;包含窗口句柄、当前选择、预览模型和运行期标志。
* @param selectedTraceDir 文件或目录路径;函数按 UTF-8/Windows 路径规则读取或写入。
*/
void refreshJsonMonitorChoices(AppState& app, const std::string& selectedTraceDir) {
if (!app.jsonMonitor) return;
if (!app.jsonMonitorRefreshing &&
SendMessageW(app.jsonMonitor, CB_GETDROPPEDSTATE, 0, 0)) {
app.jsonMonitorRefreshPending = true;
app.pendingJsonMonitorTraceDir = selectedTraceDir;
return;
}
std::string selectedKey = selectedTraceDir.empty() ? "" : normalizedPathPrefix(selectedTraceDir);
app.jsonMonitorRefreshing = true;
SendMessageW(app.jsonMonitor, CB_RESETCONTENT, 0, 0);
app.jsonMonitorChoices.clear();
std::unordered_set<std::string> seen;
addJsonMonitorChoice(app, seen, "全部测点", "", -1, -1);
PqToolConfig cfg = readConfig(app.hwnd);
for (size_t i = 0; i < app.devices.size(); ++i) {
const auto& d = app.devices[i];
for (size_t j = 0; j < d.monitors.size(); ++j) {
const auto& m = d.monitors[j];
std::string traceDir = pqTraceDirectoryFor(cfg, d, m);
std::string label = "进程 " + (d.processNo.empty() ? std::string("1") : d.processNo)
+ " / " + (d.name.empty() ? d.id : d.name)
+ " / " + (m.name.empty() ? m.id : m.name);
addJsonMonitorChoice(app, seen, truncateText(label, 120), traceDir, (int)i, (int)j);
}
}
for (const auto& item : app.jsonItems) {
if (item.traceDir.empty()) continue;
std::string label = item.monitorId.empty()
? fs::u8path(item.traceDir).filename().u8string()
: "测点 " + item.monitorId;
std::string name = contextStringValue(fs::u8path(item.traceDir) / "trace_context.json", "monitorName");
if (!name.empty()) label = name + (item.monitorId.empty() ? "" : (" / " + item.monitorId));
addJsonMonitorChoice(app, seen, truncateText(label, 120), item.traceDir, -1, -1);
}
int selectedIndex = selectedKey.empty() ? 0 : -1;
for (size_t i = 0; i < app.jsonMonitorChoices.size(); ++i) {
const auto& choice = app.jsonMonitorChoices[i];
SendMessageW(app.jsonMonitor, CB_ADDSTRING, 0, (LPARAM)u8w(choice.label).c_str());
if (!selectedKey.empty() && !choice.traceDir.empty() &&
normalizedPathPrefix(choice.traceDir) == selectedKey) {
selectedIndex = (int)i;
}
}
if (selectedIndex < 0 && !app.jsonMonitorChoices.empty()) selectedIndex = 0;
if (selectedIndex >= 0) SendMessageW(app.jsonMonitor, CB_SETCURSEL, selectedIndex, 0);
app.jsonMonitorRefreshing = false;
app.jsonMonitorRefreshPending = false;
app.pendingJsonMonitorTraceDir.clear();
}
/**
* Function: selectJsonMonitorChoice
*
* @brief 实现界面层的 “selectJsonMonitorChoice” 功能,并在 Win32 控件与 AppState 业务模型之间传递数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param app 应用状态;包含窗口句柄、当前选择、预览模型和运行期标志。
* @param index 索引、数量、范围或偏移参数;具体边界由函数体校验。
*/
void selectJsonMonitorChoice(AppState& app, int index) {
if (index < 0 || index >= (int)app.jsonMonitorChoices.size()) return;
const JsonMonitorChoice choice = app.jsonMonitorChoices[index];
app.activeTraceDir = choice.traceDir;
std::string outputPath;
if (choice.traceDir.empty()) {
app.selectedJson = latestVisibleJsonIndex(app);
rebuildJsonTree(app);
updateJsonDetail(app);
return;
}
PqToolConfig cfg = readConfig(app.hwnd);
if (choice.device >= 0 && choice.device < (int)app.devices.size() &&
choice.monitor >= 0 && choice.monitor < (int)app.devices[choice.device].monitors.size()) {
app.selectedDevice = choice.device;
app.selectedMonitor = choice.monitor;
prepareTraceWorkspace(app, cfg, app.devices[choice.device],
app.devices[choice.device].monitors[choice.monitor], true);
outputPath = cfg.outputPath;
updateDetails(app);
} else {
fs::path dir = fs::u8path(choice.traceDir);
std::string xmlPath = contextStringValue(dir / "trace_context.json", "xmlPath");
outputPath = contextStringValue(dir / "trace_context.json", "outputPath");
if (outputPath.empty()) outputPath = (dir / "final_sorted.xls").u8string();
if (!xmlPath.empty()) setText(app.hwnd, IDC_XML_PATH, xmlPath);
setText(app.hwnd, IDC_JSON_PATH, joinPaths(collectJsonDataFiles(dir)));
setText(app.hwnd, IDC_ORIGIN_PATHS, joinPaths(collectOriginFiles(dir)));
setText(app.hwnd, IDC_OUTPUT_PATH, outputPath);
}
app.selectedJson = latestVisibleJsonIndex(app);
rebuildJsonTree(app);
updateJsonDetail(app);
refreshPreviewWorkbookChoices(app, outputPath);
if (!outputPath.empty()) refreshPreviewFromWorkbook(app, outputPath);
}
/**
* Function: updatePathsForSelection
*
* @brief 根据当前模型状态刷新 “updatePathsForSelection” 对应的缓存或界面。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param app 应用状态;包含窗口句柄、当前选择、预览模型和运行期标志。
*/
void updatePathsForSelection(AppState& app) {
if (app.selectedDevice < 0 || app.selectedDevice >= (int)app.devices.size()) return;
PqToolConfig cfg = readConfig(app.hwnd);
const auto& d = app.devices[app.selectedDevice];
if (app.selectedMonitor >= 0 && app.selectedMonitor < (int)d.monitors.size()) {
app.activeTraceDir = pqTraceDirectoryFor(cfg, d, d.monitors[app.selectedMonitor]);
prepareTraceWorkspace(app, cfg, d, d.monitors[app.selectedMonitor], true);
refreshJsonMonitorChoices(app, app.activeTraceDir);
app.selectedJson = latestVisibleJsonIndex(app);
rebuildJsonTree(app);
updateJsonDetail(app);
refreshPreviewWorkbookChoices(app, cfg.outputPath);
if (fs::exists(fs::u8path(cfg.outputPath))) {
refreshPreviewFromWorkbook(app, cfg.outputPath);
}
return;
}
app.activeTraceDir.clear();
refreshJsonMonitorChoices(app, "");
rebuildJsonTree(app);
updateJsonDetail(app);
std::string xmlPath = pqFindDeviceXmlPath(cfg, d);
if (!xmlPath.empty()) setText(app.hwnd, IDC_XML_PATH, xmlPath);
}
/**
* Function: showPage
*
* @brief 实现界面层的 “showPage” 功能,并在 Win32 控件与 AppState 业务模型之间传递数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param app 应用状态;包含窗口句柄、当前选择、预览模型和运行期标志。
* @param index 索引、数量、范围或偏移参数;具体边界由函数体校验。
*/
void showPage(AppState& app, int index) {
ShowWindow(app.pageConfig, index == 0 ? SW_SHOW : SW_HIDE);
ShowWindow(app.pageLedger, index == 1 ? SW_SHOW : SW_HIDE);
ShowWindow(app.pageJson, index == 2 ? SW_SHOW : SW_HIDE);
ShowWindow(app.pagePreview, index == 3 ? SW_SHOW : SW_HIDE);
ShowWindow(app.pageXmlPreview, index == 4 ? SW_SHOW : SW_HIDE);
if (index == 3) {
PqToolConfig cfg = readConfig(app.hwnd);
std::string selected = app.activePreviewWorkbook.empty() ? cfg.outputPath : app.activePreviewWorkbook;
refreshPreviewWorkbookChoices(app, selected);
if (app.previewSheets.empty() && !selected.empty() && fs::exists(fs::u8path(selected))) {
refreshPreviewFromWorkbook(app, selected);
}
} else if (index == 4) {
// XML 有人工修改时不自动逆生成,避免仅切换页面就静默覆盖用户内容。
if (!app.xmlPreviewEdited) {
if (app.reverseXmlDirty || app.generatedXmlText.empty()) generateReverseXml(app, false);
else updateXmlPreviewText(app);
}
}
}
/**
* Function: selectPage
*
* @brief 实现界面层的 “selectPage” 功能,并在 Win32 控件与 AppState 业务模型之间传递数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param app 应用状态;包含窗口句柄、当前选择、预览模型和运行期标志。
* @param index 索引、数量、范围或偏移参数;具体边界由函数体校验。
*/
void selectPage(AppState& app, int index) {
if (app.tab) SendMessageW(app.tab, TCM_SETCURSEL, index, 0);
showPage(app, index);
}
/**
* Function: runGetLedger
*
* @brief 处理 “runGetLedger” 对应的业务入口或事件,并调度下游函数完成实际工作。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param app 应用状态;包含窗口句柄、当前选择、预览模型和运行期标志。
*/
void runGetLedger(AppState& app) {
try {
PqToolConfig cfg = readConfig(app.hwnd);
appendLogW(app, L"开始获取台账和 ICD/XML...");
std::string firstXml;
// 输出 devices 会刷新左侧树firstXml 会自动填到 XML 路径,减少手动选择。
pqFetchLedgerAndIcd(cfg, app.devices, firstXml, [&](const std::string& s) { appendLog(app, s); });
populateTree(app);
updateDetails(app);
refreshJsonMonitorChoices(app, app.activeTraceDir);
if (!firstXml.empty()) setText(app.hwnd, IDC_XML_PATH, firstXml);
appendLogW(app, L"台账树已刷新。");
} catch (const std::exception& e) {
appendLog(app, std::string("获取台账失败: ") + e.what());
MessageBoxW(app.hwnd, u8w(e.what()).c_str(), L"获取台账失败", MB_ICONERROR);
}
}
/**
* Function: runInitMq
*
* @brief 处理 “runInitMq” 对应的业务入口或事件,并调度下游函数完成实际工作。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param app 应用状态;包含窗口句柄、当前选择、预览模型和运行期标志。
*/
void runInitMq(AppState& app) {
try {
PqToolConfig cfg = readConfig(app.hwnd);
pqInitializeMq(cfg,
[&](const std::string& s) { appendLog(app, s); },
makeMqEventCallback(app));
} catch (const std::exception& e) {
appendLog(app, std::string("初始化 MQ 失败: ") + e.what());
}
}
/**
* Function: runTrace
*
* @brief 处理 “runTrace” 对应的业务入口或事件,并调度下游函数完成实际工作。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param app 应用状态;包含窗口句柄、当前选择、预览模型和运行期标志。
*/
void runTrace(AppState& app) {
try {
if (app.selectedDevice < 0 || app.selectedMonitor < 0) {
MessageBoxW(app.hwnd, L"请先在左侧选择一个测点,选中装置或进程时不会发送数据追踪消息。", L"数据追踪", MB_ICONINFORMATION);
return;
}
PqToolConfig cfg = readConfig(app.hwnd);
const auto& d = app.devices[app.selectedDevice];
const auto& m = d.monitors[app.selectedMonitor];
std::string traceDir = pqTraceDirectoryFor(cfg, d, m);
// pqSendTraceRequest 会在 MQ 互斥锁内建立唯一活动会话并清空旧记录,
// 从这一刻开始只接收 Broker 存储时间不早于本次点击的消息。
resetJsonSession(app, traceDir);
pqSendTraceRequest(cfg, d, m, selectedFrontType(app),
[&](const std::string& s) { appendLog(app, s); },
makeMqEventCallback(app));
appendLog(app, "已建立本次追踪消息时间边界并清空旧记录: " + traceDir);
prepareTraceWorkspace(app, cfg, d, m, true);
app.autoPreviewEnabled = true;
selectPage(app, 3);
autoParseTraceDir(app, traceDir, "trace-start");
} catch (const std::exception& e) {
appendLog(app, std::string("数据追踪失败: ") + e.what());
}
}
/**
* Function: runParse
*
* @brief 处理 “runParse” 对应的业务入口或事件,并调度下游函数完成实际工作。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param app 应用状态;包含窗口句柄、当前选择、预览模型和运行期标志。
*/
void runParse(AppState& app) {
PqToolConfig cfg = readConfig(app.hwnd);
std::string err;
appendLogW(app, L"开始解析生成表格...");
std::vector<std::string> jsonPaths = cfg.jsonDataPaths.empty() ? splitPathList(cfg.jsonDataPath) : cfg.jsonDataPaths;
// 一键解析是手动路径模式:用界面上的 XML/jsondata/origin/output 调用核心解析器。
bool ok = pqRunParser(cfg.xmlPath, jsonPaths, cfg.originPaths, cfg.outputPath, &err, selectedFrontType(app));
if (ok) {
refreshPreviewWorkbookChoices(app, cfg.outputPath);
refreshPreviewFromWorkbook(app, cfg.outputPath);
appendLog(app, "表格生成完成: " + cfg.outputPath);
MessageBoxW(app.hwnd, u8w("表格生成完成: " + cfg.outputPath).c_str(), L"完成", MB_ICONINFORMATION);
} else {
appendLog(app, "表格生成失败: " + err);
MessageBoxW(app.hwnd, u8w(err).c_str(), L"表格生成失败", MB_ICONERROR);
}
}
/**
* Function: currentWorkbookForPreview
*
* @brief 实现界面层的 “currentWorkbookForPreview” 功能,并在 Win32 控件与 AppState 业务模型之间传递数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param app 应用状态;包含窗口句柄、当前选择、预览模型和运行期标志。
* @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。
*/
std::string currentWorkbookForPreview(AppState& app) {
if (!app.activePreviewWorkbook.empty()) return app.activePreviewWorkbook;
if (app.previewWorkbook) {
int idx = (int)SendMessageW(app.previewWorkbook, CB_GETCURSEL, 0, 0);
if (idx >= 0 && idx < (int)app.previewWorkbooks.size()) return app.previewWorkbooks[idx].path;
}
return readConfig(app.hwnd).outputPath;
}
/**
* Function: runImportIcd
*
* @brief 处理 “runImportIcd” 对应的业务入口或事件,并调度下游函数完成实际工作。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param app 应用状态;包含窗口句柄、当前选择、预览模型和运行期标志。
*/
void runImportIcd(AppState& app) {
std::string icdPath;
if (!openFileDialog(app.hwnd,
L"\x9009\x62E9ICD\x6587\x4EF6",
L"ICD/XML Files\0*.icd;*.xml\0All Files\0*.*\0",
icdPath)) {
return;
}
std::string workbook = currentWorkbookForPreview(app);
if (workbook.empty() || !fs::exists(fs::u8path(workbook))) {
MessageBoxW(app.hwnd, L"\x8BF7\x5148\x751F\x6210\x6216\x9009\x62E9\x4E00\x4E2A\x8868\x683C\x6587\x4EF6\x3002",
L"\x5BFC\x5165ICD", MB_ICONINFORMATION);
return;
}
std::string msg;
bool ok = pqApplyIcdDescriptionsToWorkbook(icdPath, workbook, &msg);
appendLog(app, msg);
if (ok) {
refreshPreviewFromWorkbook(app, workbook);
MessageBoxW(app.hwnd, u8w(msg).c_str(), L"\x5BFC\x5165ICD", MB_ICONINFORMATION);
} else {
MessageBoxW(app.hwnd, u8w(msg).c_str(), L"\x5BFC\x5165ICD\x5931\x8D25", MB_ICONERROR);
}
}
/**
* Function: xmlLocalName
*
* @brief 实现界面层的 “xmlLocalName” 功能,并在 Win32 控件与 AppState 业务模型之间传递数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param name 待解析、比较、显示或发送的 UTF-8 文本。
* @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。
*/
std::string xmlLocalName(const char* name) {
if (!name) return "";
std::string s(name);
size_t pos = s.find(':');
return pos == std::string::npos ? s : s.substr(pos + 1);
}
/**
* Function: xmlAttr
*
* @brief 实现界面层的 “xmlAttr” 功能,并在 Win32 控件与 AppState 业务模型之间传递数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param e 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @param name 待解析、比较、显示或发送的 UTF-8 文本。
* @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。
*/
std::string xmlAttr(const tinyxml2::XMLElement* e, const char* name) {
const char* v = e ? e->Attribute(name) : nullptr;
return v ? v : "";
}
struct ReverseSheetSpec {
std::string topic;
std::string dataType;
std::string wiring;
};
/**
* Function: reverseSpecForSheet
*
* @brief 根据当前配置和编辑状态生成 “reverseSpecForSheet” 对应的输出内容。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param sheetName 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。
*/
ReverseSheetSpec reverseSpecForSheet(const std::string& sheetName) {
ReverseSheetSpec spec;
spec.topic = sheetName.find("实时") == std::string::npos ? "HISDATA" : "RTDATA";
spec.dataType = sheetName.find("03") != std::string::npos ? "03" :
sheetName.find("02") != std::string::npos ? "02" : "01";
spec.wiring = (sheetName.find("") != std::string::npos) ? "delta" : "star";
return spec;
}
/**
* Function: reverseWiringAllowed
*
* @brief 根据当前配置和编辑状态生成 “reverseWiringAllowed” 对应的输出内容。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param valueWiring 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @param sheetWiring 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @return 条件成立或操作成功时为 true否则为 false。
*/
bool reverseWiringAllowed(const std::string& valueWiring, const std::string& sheetWiring) {
return valueWiring.empty() || valueWiring == "common" || valueWiring == sheetWiring;
}
/**
* Function: cleanPhaseLabelForReverse
*
* @brief 规范化 “cleanPhaseLabelForReverse” 对应的字符串或标识,减少格式差异对匹配造成的影响。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param label 待解析、比较、显示或发送的 UTF-8 文本。
* @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。
*/
std::string cleanPhaseLabelForReverse(std::string label) {
label = labelWithoutPhase(label);
return trimAscii(label);
}
/**
* Function: reverseNameFromField
*
* @brief 根据当前配置和编辑状态生成 “reverseNameFromField” 对应的输出内容。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param field 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @param arrayBase 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。
*/
std::string reverseNameFromField(const std::string& field, const std::string& arrayBase) {
std::string name = field;
std::vector<std::string> parts;
std::stringstream ss(field);
std::string part;
while (std::getline(ss, part, '_')) parts.push_back(part);
if (parts.size() >= 3 && (parts[1] == "A" || parts[1] == "B" || parts[1] == "C" || parts[1] == "T")) {
name.clear();
for (size_t i = 2; i < parts.size(); ++i) {
if (i > 2) name += "_";
name += parts[i];
}
}
std::smatch m;
static const std::regex rangeRe(R"(^(.*)\[(\d+)-(\d+)\](.*)$)");
if (std::regex_match(name, m, rangeRe)) {
// 表格里序列显示为 [start-end],写回 XML 时恢复为 %start,end%。
std::string start = m[2].str();
std::string end = m[3].str();
static const std::regex baseRe(R"(^\s*(-?\d+)\s*-->\s*(-?\d+)\s*$)");
std::smatch bm;
if (std::regex_match(arrayBase, bm, baseRe)) {
start = bm[1].str();
end = bm[2].str();
}
return m[1].str() + "%" + start + "," + end + "%" + m[4].str();
}
return name;
}
/**
* Function: parseConfigText
*
* @brief 解析 “parseConfigText” 对应的文本或结构,提取经过校验的业务字段。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param config 运行配置提供接口地址、MQ 参数、路径和当前数据模式。
* @param doPath 文件或目录路径;函数按 UTF-8/Windows 路径规则读取或写入。
* @param daPath 文件或目录路径;函数按 UTF-8/Windows 路径规则读取或写入。
* @return 条件成立或操作成功时为 true否则为 false。
*/
bool parseConfigText(const std::string& config, std::string& doPath, std::string& daPath) {
std::smatch m;
static const std::regex doRe("DO=\"([^\"]*)\"");
static const std::regex daRe("DA=\"([^\"]*)\"");
bool ok = false;
if (std::regex_search(config, m, doRe)) {
doPath = m[1].str();
ok = true;
}
if (std::regex_search(config, m, daRe)) {
daPath = m[1].str();
ok = true;
}
return ok;
}
/**
* Function: placeholderForValueOffset
*
* @brief 实现界面层的 “placeholderForValueOffset” 功能,并在 Win32 控件与 AppState 业务模型之间传递数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param offsetText 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。
*/
std::string placeholderForValueOffset(const std::string& offsetText) {
int offset = 0;
try {
offset = offsetText.empty() ? 0 : std::stoi(offsetText);
} catch (...) {
offset = 0;
}
if (offset <= 0) return "%-" + std::to_string(-offset);
return "%" + std::to_string(offset);
}
/**
* Function: reverseDaForXml
*
* @brief 根据当前配置和编辑状态生成 “reverseDaForXml” 对应的输出内容。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param daPath 文件或目录路径;函数按 UTF-8/Windows 路径规则读取或写入。
* @param valueOffset 索引、数量、范围或偏移参数;具体边界由函数体校验。
* @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。
*/
std::string reverseDaForXml(std::string daPath, const std::string& valueOffset) {
if (valueOffset.empty()) return daPath;
static const std::regex rangeRe(R"(\[\d+-\d+\])");
// 表格展示的 [start-end] 在 XML 中是 [%offset] 占位符,按“取值偏移量”恢复。
return std::regex_replace(daPath, rangeRe, "[" + placeholderForValueOffset(valueOffset) + "]",
std::regex_constants::format_first_only);
}
struct ReverseRowEdit {
std::string label;
std::string name;
std::string arrayBase;
std::string valueOffset;
std::string finalOffset;
std::string doPath;
std::string daPath;
std::string originalDoPath;
std::string originalDaPath;
bool modified = false;
};
/**
* Function: collectReverseRows
*
* @brief 遍历输入结构并收集 “collectReverseRows” 所需的候选数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param app 应用状态;包含窗口句柄、当前选择、预览模型和运行期标志。
* @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。
*/
std::vector<ReverseRowEdit> collectReverseRows(const AppState& app) {
std::vector<ReverseRowEdit> rows;
if (app.activePreviewSheet < 0 || app.activePreviewSheet >= (int)app.previewSheets.size()) return rows;
const PreviewSheet& sheet = app.previewSheets[app.activePreviewSheet];
// 只采集正常/异常/缺失这些数据行,表头和分组行不会参与反向生成。
for (const auto& row : sheet.rows) {
if (!(row.status == 1 || row.status == 2 || row.status == 3)) continue;
if (row.cells.size() <= 8 || row.cells[0].empty()) continue;
ReverseRowEdit edit;
edit.label = cleanPhaseLabelForReverse(row.cells[0]);
edit.arrayBase = row.cells.size() > 1 ? row.cells[1] : "";
edit.valueOffset = row.cells.size() > 2 ? row.cells[2] : "";
edit.finalOffset = row.cells.size() > 3 ? row.cells[3] : "";
std::string config = row.cells.size() > 4 ? row.cells[4] : "";
parseConfigText(config, edit.doPath, edit.daPath);
std::string originalConfig = row.originalCells.size() > 4 ? row.originalCells[4] : config;
parseConfigText(originalConfig, edit.originalDoPath, edit.originalDaPath);
edit.modified = row.cells != row.originalCells;
edit.daPath = reverseDaForXml(edit.daPath, edit.valueOffset);
edit.name = reverseNameFromField(effectivePreviewField(row.cells[8]), edit.arrayBase);
if (!edit.name.empty() || !edit.label.empty()) rows.push_back(std::move(edit));
}
std::stable_sort(rows.begin(), rows.end(), [](const ReverseRowEdit& a, const ReverseRowEdit& b) {
return a.modified && !b.modified;
});
return rows;
}
struct ValueNodeRef {
tinyxml2::XMLElement* element = nullptr;
std::string topic;
std::string dataType;
std::string wiring = "common";
};
/**
* Function: collectValueNodes
*
* @brief 遍历输入结构并收集 “collectValueNodes” 所需的候选数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param e 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @param topic 待解析、比较、显示或发送的 UTF-8 文本。
* @param dataType 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @param wiring 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @param out 输出参数;函数在成功或失败时写入结果或诊断信息。
*/
void collectValueNodes(tinyxml2::XMLElement* e,
std::string topic,
std::string dataType,
std::string wiring,
std::vector<ValueNodeRef>& out) {
for (; e; e = e->NextSiblingElement()) {
std::string nodeName = xmlLocalName(e->Name());
std::string nextTopic = topic;
std::string nextDataType = dataType;
std::string nextWiring = wiring;
if (nodeName == "Topic") nextTopic = xmlAttr(e, "name");
else if (nodeName == "DataType") nextDataType = xmlAttr(e, "value");
else if (nodeName == "Sequence") {
std::string seq = xmlAttr(e, "value");
if (seq == "7") nextWiring = "star";
else if (seq == "112") nextWiring = "delta";
} else if (nodeName == "Value") {
out.push_back({e, topic, dataType, wiring});
}
collectValueNodes(e->FirstChildElement(), nextTopic, nextDataType, nextWiring, out);
}
}
/**
* Function: reverseNodeMatches
*
* @brief 根据当前配置和编辑状态生成 “reverseNodeMatches” 对应的输出内容。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param node 当前处理的数据行、单元格、XML 节点或模型项。
* @param spec 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @param edit 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @return 条件成立或操作成功时为 true否则为 false。
*/
bool reverseNodeMatches(const ValueNodeRef& node, const ReverseSheetSpec& spec, const ReverseRowEdit& edit) {
if (node.topic != spec.topic || node.dataType != spec.dataType || !reverseWiringAllowed(node.wiring, spec.wiring)) return false;
std::string nodeDo = xmlAttr(node.element, "DO");
std::string nodeDa = xmlAttr(node.element, "DA");
// 优先用原始 DO/DA 精确定位;如果用户把 DO/DA 改了,也能找到原 XML 节点。
if (!edit.originalDoPath.empty() && !edit.originalDaPath.empty() &&
nodeDo == edit.originalDoPath && nodeDa == edit.originalDaPath) {
return true;
}
if (!edit.originalDoPath.empty() && nodeDo == edit.originalDoPath && edit.originalDaPath.empty()) return true;
std::string nodeName = xmlAttr(node.element, "name");
std::string nodeDesc = cleanPhaseLabelForReverse(xmlAttr(node.element, "desc"));
if (!edit.name.empty() && nodeName == edit.name) return true;
if (!edit.label.empty() && nodeDesc == edit.label) return true;
return false;
}
/**
* Function: generateReverseXml
*
* @brief 根据当前配置和编辑状态生成 “generateReverseXml” 对应的输出内容。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param app 应用状态;包含窗口句柄、当前选择、预览模型和运行期标志。
* @param showMessage 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。
*/
std::string generateReverseXml(AppState& app, bool showMessage) {
commitPreviewEdit(app, true);
PqToolConfig cfg = readConfig(app.hwnd);
std::string xmlPath = cfg.xmlPath;
if (xmlPath.empty() || !fs::exists(fs::u8path(xmlPath))) {
if (showMessage) MessageBoxW(app.hwnd, L"\x8BF7\x5148\x9009\x62E9\x539F\x59CBXML\x6587\x4EF6\x3002", L"XML\x9006\x751F\x6210", MB_ICONINFORMATION);
return "";
}
if (app.activePreviewSheet < 0 || app.activePreviewSheet >= (int)app.previewSheets.size()) return "";
tinyxml2::XMLDocument doc;
std::string xml = readAllText(fs::u8path(xmlPath));
if (doc.Parse(xml.c_str(), xml.size()) != tinyxml2::XML_SUCCESS) {
if (showMessage) MessageBoxW(app.hwnd, L"XML\x89E3\x6790\x5931\x8D25\x3002", L"XML\x9006\x751F\x6210", MB_ICONERROR);
return "";
}
// 当前 Sheet 名决定要修改 XML 中哪个 Topic/DataType/接线分支。
ReverseSheetSpec spec = reverseSpecForSheet(app.previewSheets[app.activePreviewSheet].name);
std::vector<ReverseRowEdit> edits = collectReverseRows(app);
std::vector<ValueNodeRef> nodes;
collectValueNodes(doc.RootElement(), "", "", "common", nodes);
std::set<tinyxml2::XMLElement*> touched;
int updated = 0;
for (const auto& edit : edits) {
for (const auto& node : nodes) {
if (touched.count(node.element)) continue;
if (!reverseNodeMatches(node, spec, edit)) continue;
if (!edit.name.empty()) node.element->SetAttribute("name", edit.name.c_str());
if (!edit.doPath.empty()) node.element->SetAttribute("DO", edit.doPath.c_str());
if (!edit.daPath.empty()) node.element->SetAttribute("DA", edit.daPath.c_str());
if (!edit.finalOffset.empty()) node.element->SetAttribute("Offset", edit.finalOffset.c_str());
touched.insert(node.element);
++updated;
break;
}
}
tinyxml2::XMLPrinter printer;
doc.Print(&printer);
app.generatedXmlText = printer.CStr();
app.generatedXmlSourcePath = xmlPath;
app.reverseXmlDirty = false;
updateXmlPreviewText(app);
if (showMessage) {
MessageBoxW(app.hwnd, u8w("XML逆生成完成更新配置项: " + std::to_string(updated)).c_str(),
L"XML\x9006\x751F\x6210", MB_ICONINFORMATION);
}
return app.generatedXmlText;
}
/**
* Function: runReverseXml
*
* @brief 处理 “runReverseXml” 对应的业务入口或事件,并调度下游函数完成实际工作。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param app 应用状态;包含窗口句柄、当前选择、预览模型和运行期标志。
*/
void runReverseXml(AppState& app) {
if (app.xmlPreviewEdited) {
int choice = MessageBoxW(app.hwnd,
L"XML预览此前有过手动修改。继续逆生成会覆盖这些人工修改是否继续",
L"确认覆盖 XML 人工修改",
MB_YESNO | MB_ICONWARNING | MB_DEFBUTTON2);
if (choice != IDYES) return;
}
if (!generateReverseXml(app, true).empty()) selectPage(app, 4);
}
/**
* Function: runExportXml
*
* @brief 处理 “runExportXml” 对应的业务入口或事件,并调度下游函数完成实际工作。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param app 应用状态;包含窗口句柄、当前选择、预览模型和运行期标志。
*/
void runExportXml(AppState& app) {
if (app.xmlPreviewEdited) syncXmlPreviewFromEditor(app);
if (app.generatedXmlText.empty() || app.reverseXmlDirty) generateReverseXml(app, false);
if (app.generatedXmlText.empty()) return;
tinyxml2::XMLDocument check;
if (check.Parse(app.generatedXmlText.c_str(), app.generatedXmlText.size()) != tinyxml2::XML_SUCCESS) {
MessageBoxW(app.hwnd,
u8w(std::string("当前编辑内容不是合法 XML") + check.ErrorStr()).c_str(),
L"导出 XML 失败", MB_ICONERROR);
return;
}
std::string out;
if (!saveFileDialog(app.hwnd, L"\x5BFC\x51FAXML", L"XML Files\0*.xml;*.icd\0All Files\0*.*\0", L"xml", out)) return;
std::ofstream ofs(fs::u8path(out), std::ios::binary);
ofs << app.generatedXmlText;
appendLog(app, "XML exported: " + out);
}
/**
* Function: autoParseTraceDir
*
* @brief 实现界面层的 “autoParseTraceDir” 功能,并在 Win32 控件与 AppState 业务模型之间传递数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param app 应用状态;包含窗口句柄、当前选择、预览模型和运行期标志。
* @param traceDir 文件或目录路径;函数按 UTF-8/Windows 路径规则读取或写入。
* @param reason 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
*/
void autoParseTraceDir(AppState& app, const std::string& traceDir, const char* reason) {
if (traceDir.empty()) {
autoParseAndRefresh(app, reason);
return;
}
fs::path dir = fs::u8path(traceDir);
PqToolConfig cfg = readConfig(app.hwnd);
std::string xmlPath;
std::string outputPath = (dir / "final_sorted.xls").u8string();
std::string frontType = selectedFrontType(app);
// trace_context.json 是 pqSendTraceRequest() 写下的上下文,自动解析优先用它定位 XML 和输出路径。
fs::path contextPath = dir / "trace_context.json";
if (fs::exists(contextPath)) {
try {
json context = json::parse(readAllText(contextPath));
auto readString = [&](const char* key) -> std::string {
auto it = context.find(key);
return it == context.end() ? "" : jsonScalarString(*it);
};
std::string fromContext = readString("xmlPath");
if (!fromContext.empty()) xmlPath = fromContext;
fromContext = readString("outputPath");
if (!fromContext.empty()) outputPath = fromContext;
fromContext = readString("frontType");
if (!fromContext.empty()) frontType = fromContext;
} catch (...) {
}
}
if (xmlPath.empty() && sameTraceDir(traceDir, app.activeTraceDir)) xmlPath = cfg.xmlPath;
if (xmlPath.empty()) {
appendLog(app, "Auto parse skipped: missing XML for " + traceDir);
return;
}
std::vector<std::string> jsonPaths = collectJsonDataFiles(dir);
std::vector<std::string> originPaths = collectOriginFiles(dir);
std::string err;
std::string reasonText = reason ? reason : "";
BusyPopupGuard busy(app,
reasonText == "data" || reasonText == "trace",
L"\u6570\u636E\u5230\u8FBE\uFF0C\u6B63\u5728\u5199\u5165\u8868\u683C...");
bool ok = pqRunParser(xmlPath, jsonPaths, originPaths, outputPath, &err, frontType);
bool active = sameTraceDir(traceDir, app.activeTraceDir);
if (ok) {
if (active) {
setText(app.hwnd, IDC_XML_PATH, xmlPath);
setText(app.hwnd, IDC_JSON_PATH, joinPaths(jsonPaths));
setText(app.hwnd, IDC_ORIGIN_PATHS, joinPaths(originPaths));
setText(app.hwnd, IDC_OUTPUT_PATH, outputPath);
refreshPreviewWorkbookChoices(app, outputPath);
refreshPreviewFromWorkbook(app, outputPath);
}
appendLog(app, std::string("Auto parse updated(") + (reason ? reason : "mq") + "): " + outputPath);
} else {
if (active && app.previewStatus) SetWindowTextW(app.previewStatus, u8w("Auto parse failed: " + err).c_str());
appendLog(app, "Auto parse failed: " + err);
}
}
/**
* Function: autoParseAndRefresh
*
* @brief 实现界面层的 “autoParseAndRefresh” 功能,并在 Win32 控件与 AppState 业务模型之间传递数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param app 应用状态;包含窗口句柄、当前选择、预览模型和运行期标志。
* @param reason 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
*/
void autoParseAndRefresh(AppState& app, const char* reason) {
PqToolConfig cfg = readConfig(app.hwnd);
std::vector<std::string> jsonPaths = cfg.jsonDataPaths.empty() ? splitPathList(cfg.jsonDataPath) : cfg.jsonDataPaths;
std::string err;
std::string reasonText = reason ? reason : "";
BusyPopupGuard busy(app,
reasonText == "data" || reasonText == "trace",
L"\u6570\u636E\u5230\u8FBE\uFF0C\u6B63\u5728\u5199\u5165\u8868\u683C...");
bool ok = pqRunParser(cfg.xmlPath, jsonPaths, cfg.originPaths, cfg.outputPath, &err, selectedFrontType(app));
if (ok) {
refreshPreviewWorkbookChoices(app, cfg.outputPath);
refreshPreviewFromWorkbook(app, cfg.outputPath);
appendLog(app, std::string("Auto parse updated(") + (reason ? reason : "mq") + "): " + cfg.outputPath);
} else {
if (app.previewStatus) SetWindowTextW(app.previewStatus, u8w("Auto parse failed: " + err).c_str());
appendLog(app, "Auto parse failed: " + err);
}
}
/**
* Function: createConfigPage
*
* @brief 构造 “createConfigPage” 对应的对象、消息、窗口控件或输出结构。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param app 应用状态;包含窗口句柄、当前选择、预览模型和运行期标志。
*/
void createConfigPage(AppState& app) {
PqToolConfig cfg = pqDefaultConfig();
HWND p = app.pageConfig;
addLabel(app, p, L"配置项", 22, 10, 120, 26);
const int groupX = 18, groupW = 1110, rowGap = 32, x1 = 38, x2 = 520, x3 = 810;
addGroup(app, p, L"前置配置", groupX, 42, groupW, 82);
addLabeledEdit(app, p, L"FrontInst", IDC_FRONT_INST, cfg.frontInst, x1, 74, 118, 330);
addLabeledEdit(app, p, L"FrontIP", IDC_FRONT_IP, cfg.frontIp, x2, 74, 90, 180);
addGroup(app, p, L"台账配置", groupX, 136, groupW, 82);
addLabeledEdit(app, p, L"TerminalStatus", IDC_TERMINAL_STATUS, cfg.terminalStatus, x1, 168, 118, 140);
addLabeledEdit(app, p, L"MonitorStatus", IDC_MONITOR_STATUS, cfg.monitorStatus, x2, 168, 118, 140);
addLabeledEdit(app, p, L"IcdFlag", IDC_ICD_FLAG, cfg.icdFlag, x3, 168, 70, 80);
addGroup(app, p, L"接口配置", groupX, 230, groupW, 122);
addLabeledEdit(app, p, L"WebDevice", IDC_WEB_DEVICE, cfg.webDevice, x1, 262, 118, 520);
addLabeledEdit(app, p, L"WebIcd", IDC_WEB_ICD, cfg.webIcd, x3, 262, 90, 225);
addLabeledEdit(app, p, L"WebFiledownload", IDC_WEB_FILEDOWNLOAD, cfg.webFiledownload, x1, 296, 118, 520);
addLabeledEdit(app, p, L"XML目录", IDC_XML_SAVE_DIR, cfg.xmlSaveDir, x3, 296, 90, 225);
addGroup(app, p, L"MQ配置", groupX, 364, groupW, 316);
addLabel(app, p, L"生产者", x1, 398, 80, 22);
addLabeledEdit(app, p, L"producer", IDC_PRODUCER, cfg.producer, 120, 398, 90, 300);
addLabeledEdit(app, p, L"Ipport", IDC_IPPORT, cfg.ipport, 600, 398, 80, 260);
addLabeledEdit(app, p, L"AccessKey", IDC_PRODUCER_ACCESS_KEY, cfg.producerAccessKey, 120, 398 + rowGap, 90, 300);
addLabeledEdit(app, p, L"SecretKey", IDC_PRODUCER_SECRET_KEY, cfg.producerSecretKey, 600, 398 + rowGap, 80, 260);
addLabeledEdit(app, p, L"TopicLOG", IDC_TOPIC_LOG, cfg.topicLOG, 120, 398 + rowGap * 2, 90, 360);
addLabeledEdit(app, p, L"TagLOG", IDC_TAG_LOG, cfg.tagLOG, 575, 398 + rowGap * 2, 75, 270);
addLabeledEdit(app, p, L"KeyLOG", IDC_KEY_LOG, cfg.keyLOG, 940, 398 + rowGap * 2, 70, 120);
addLabeledEdit(app, p, L"TopicAsk", IDC_TOPIC_ASK, cfg.topicAsk, 120, 398 + rowGap * 3, 90, 360);
addLabeledEdit(app, p, L"TagAsk", IDC_TAG_ASK, cfg.tagAsk, 575, 398 + rowGap * 3, 75, 270);
addLabeledEdit(app, p, L"KeyAsk", IDC_KEY_ASK, cfg.keyAsk, 940, 398 + rowGap * 3, 70, 120);
const int consumerY = 526;
addLabel(app, p, L"消费者", x1, consumerY, 80, 22);
addLabeledEdit(app, p, L"consumer", IDC_CONSUMER, cfg.consumer, 120, consumerY, 90, 300);
addLabeledEdit(app, p, L"Ipport", IDC_CONSUMER_IPPORT, cfg.consumerIpport, 600, consumerY, 80, 260);
addLabeledEdit(app, p, L"AccessKey", IDC_CONSUMER_ACCESS_KEY, cfg.consumerAccessKey, 120, consumerY + rowGap, 90, 300);
addLabeledEdit(app, p, L"SecretKey", IDC_CONSUMER_SECRET_KEY, cfg.consumerSecretKey, 600, consumerY + rowGap, 80, 260);
addLabeledEdit(app, p, L"\x7EDF\x8BA1Topic", IDC_DATA_STAT_TOPIC, cfg.dataStatTopic, 120, consumerY + rowGap * 2, 90, 360);
addLabeledEdit(app, p, L"DATATag", IDC_DATA_STAT_TAG, cfg.dataStatTag, 575, consumerY + rowGap * 2, 75, 270);
addLabeledEdit(app, p, L"DATAKey", IDC_DATA_STAT_KEY, cfg.dataStatKey, 940, consumerY + rowGap * 2, 70, 120);
addLabeledEdit(app, p, L"\x5B9E\x65F6Topic", IDC_DATA_REALTIME_TOPIC, cfg.dataRealtimeTopic, 120, consumerY + rowGap * 3, 90, 360);
addLabeledEdit(app, p, L"DATATag", IDC_DATA_REALTIME_TAG, cfg.dataRealtimeTag, 575, consumerY + rowGap * 3, 75, 270);
addLabeledEdit(app, p, L"DATAKey", IDC_DATA_REALTIME_KEY, cfg.dataRealtimeKey, 940, consumerY + rowGap * 3, 70, 120);
addLabeledEdit(app, p, L"TraceTopic", IDC_TRACE_TOPIC, cfg.traceTopic, 120, consumerY + rowGap * 4, 90, 360);
addLabeledEdit(app, p, L"TraceTag", IDC_TRACE_TAG, cfg.traceTag, 575, consumerY + rowGap * 4, 75, 270);
addLabeledEdit(app, p, L"TraceKey", IDC_TRACE_KEY, cfg.traceKey, 940, consumerY + rowGap * 4, 70, 120);
}
/**
* Function: createLedgerPage
*
* @brief 构造 “createLedgerPage” 对应的对象、消息、窗口控件或输出结构。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param app 应用状态;包含窗口句柄、当前选择、预览模型和运行期标志。
*/
void createLedgerPage(AppState& app) {
PqToolConfig cfg = pqDefaultConfig();
HWND p = app.pageLedger;
int y = 16;
addLabeledEdit(app, p, L"XML文件", IDC_XML_PATH, cfg.xmlPath, 18, y, 70, 670);
addButton(app, p, IDC_BROWSE_XML, L"选择XML", 780, y - 1, 90, 25);
addLabeledEdit(app, p, L"输出", IDC_OUTPUT_PATH, cfg.outputPath, 890, y, 48, 235);
y += 34;
addLabel(app, p, L"jsondata", 18, y, 70, 24);
addMemo(app, p, IDC_JSON_PATH, 88, y, 670, 52);
setText(p, IDC_JSON_PATH, cfg.jsonDataPath);
addButton(app, p, IDC_BROWSE_JSON, L"添加JSON", 780, y - 1, 90, 25);
y += 66;
addLabel(app, p, L"origin", 18, y, 70, 24);
addMemo(app, p, IDC_ORIGIN_PATHS, 88, y, 670, 56);
setText(p, IDC_ORIGIN_PATHS, "origin.txt");
addButton(app, p, IDC_ADD_ORIGIN, L"添加origin", 780, y - 1, 90, 25);
y += 70;
addLabel(app, p, L"进程类型", 18, y, 70, 24);
app.frontType = CreateWindowExW(0, L"COMBOBOX", L"", WS_CHILD | WS_VISIBLE | CBS_DROPDOWNLIST | WS_TABSTOP,
88, y, 150, 120, p, (HMENU)(INT_PTR)IDC_FRONT_TYPE, app.inst, nullptr);
applyFont(app, app.frontType);
SendMessageW(app.frontType, CB_ADDSTRING, 0, (LPARAM)L"cfg_stat_data");
SendMessageW(app.frontType, CB_ADDSTRING, 0, (LPARAM)L"cfg_3s_data");
SendMessageW(app.frontType, CB_SETCURSEL, 0, 0);
addButton(app, p, IDC_GET_LEDGER, L"获取台账", 260, y - 1, 100, 26);
addButton(app, p, IDC_INIT_MQ, L"初始化MQ", 375, y - 1, 100, 26);
addButton(app, p, IDC_TRACE, L"数据追踪", 490, y - 1, 100, 26);
addButton(app, p, IDC_PARSE, L"一键解析", 605, y - 1, 100, 26);
int panelY = y + 42;
app.tree = CreateWindowExW(WS_EX_CLIENTEDGE, WC_TREEVIEWW, L"",
WS_CHILD | WS_VISIBLE | WS_TABSTOP | TVS_HASLINES | TVS_LINESATROOT | TVS_HASBUTTONS,
18, panelY, 430, 455, p, (HMENU)(INT_PTR)IDC_TREE, app.inst, nullptr);
applyFont(app, app.tree);
app.detail = addMemo(app, p, IDC_DETAIL, 465, panelY, 670, 155);
SendMessageW(app.detail, EM_SETREADONLY, TRUE, 0);
app.log = addMemo(app, p, IDC_LOG, 465, panelY + 170, 670, 285);
SendMessageW(app.log, EM_SETREADONLY, TRUE, 0);
updateDetails(app);
appendLogW(app, L"工具已启动。无参数运行显示界面;带 XML 参数运行原命令行解析。");
}
/**
* Function: createJsonPage
*
* @brief 构造 “createJsonPage” 对应的对象、消息、窗口控件或输出结构。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param app 应用状态;包含窗口句柄、当前选择、预览模型和运行期标志。
*/
void createJsonPage(AppState& app) {
HWND p = app.pageJson;
addLabel(app, p, L"\x6D4B\x70B9", 18, 14, 50, 24);
app.jsonMonitor = CreateWindowExW(0, L"COMBOBOX", L"", WS_CHILD | WS_VISIBLE | CBS_DROPDOWNLIST | WS_TABSTOP,
70, 12, 500, 320, p, (HMENU)(INT_PTR)IDC_JSON_MONITOR, app.inst, nullptr);
applyFont(app, app.jsonMonitor);
addLabel(app, p, L"JSON\x8BB0\x5F55", 18, 50, 130, 24);
addLabel(app, p, L"JSON\x8BE6\x60C5", 385, 50, 130, 24);
app.jsonTree = CreateWindowExW(WS_EX_CLIENTEDGE, WC_TREEVIEWW, L"",
WS_CHILD | WS_VISIBLE | WS_TABSTOP |
TVS_HASLINES | TVS_LINESATROOT | TVS_HASBUTTONS,
18, 80, 345, 555, p, (HMENU)(INT_PTR)IDC_JSON_TREE, app.inst, nullptr);
applyFont(app, app.jsonTree);
app.jsonDetail = addMemo(app, p, IDC_JSON_DETAIL, 385, 80, 750, 555);
SendMessageW(app.jsonDetail, EM_SETREADONLY, TRUE, 0);
refreshJsonMonitorChoices(app, app.activeTraceDir);
rebuildJsonTree(app);
updateJsonDetail(app);
}
/**
* Function: addPreviewColumn
*
* @brief 向目标结构追加或保证存在 “addPreviewColumn” 所描述的数据。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param list Win32 窗口或控件句柄;必须在有效窗口生命周期内使用。
* @param index 索引、数量、范围或偏移参数;具体边界由函数体校验。
* @param text 待解析、比较、显示或发送的 UTF-8 文本。
* @param width 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
*/
void addPreviewColumn(HWND list, int index, const wchar_t* text, int width) {
LVCOLUMNW col{};
col.mask = LVCF_TEXT | LVCF_WIDTH | LVCF_SUBITEM;
col.pszText = const_cast<wchar_t*>(text);
col.cx = width;
col.iSubItem = index;
ListView_InsertColumn(list, index, &col);
}
/**
* Function: createPreviewPage
*
* @brief 构造 “createPreviewPage” 对应的对象、消息、窗口控件或输出结构。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param app 应用状态;包含窗口句柄、当前选择、预览模型和运行期标志。
*/
void createPreviewPage(AppState& app) {
HWND p = app.pagePreview;
addLabel(app, p, L"\x6D4B\x70B9", 18, 14, 42, 24);
app.previewWorkbook = CreateWindowExW(0, L"COMBOBOX", L"", WS_CHILD | WS_VISIBLE | CBS_DROPDOWNLIST | WS_TABSTOP,
60, 12, 245, 320, p, (HMENU)(INT_PTR)IDC_PREVIEW_WORKBOOK, app.inst, nullptr);
applyFont(app, app.previewWorkbook);
addLabel(app, p, L"Sheet", 315, 14, 45, 24);
app.previewSheet = CreateWindowExW(0, L"COMBOBOX", L"", WS_CHILD | WS_VISIBLE | CBS_DROPDOWNLIST | WS_TABSTOP,
360, 12, 150, 360, p, (HMENU)(INT_PTR)IDC_PREVIEW_SHEET, app.inst, nullptr);
applyFont(app, app.previewSheet);
addLabel(app, p, L"\x7B5B\x9009", 520, 14, 42, 24);
app.previewFilter = CreateWindowExW(0, L"COMBOBOX", L"", WS_CHILD | WS_VISIBLE | CBS_DROPDOWNLIST | WS_TABSTOP,
565, 12, 100, 160, p, (HMENU)(INT_PTR)IDC_PREVIEW_FILTER, app.inst, nullptr);
applyFont(app, app.previewFilter);
SendMessageW(app.previewFilter, CB_ADDSTRING, 0, (LPARAM)L"\x5168\x90E8");
SendMessageW(app.previewFilter, CB_ADDSTRING, 0, (LPARAM)L"\x6B63\x5E38");
SendMessageW(app.previewFilter, CB_ADDSTRING, 0, (LPARAM)L"\x5F02\x5E38");
SendMessageW(app.previewFilter, CB_ADDSTRING, 0, (LPARAM)L"\x7F3A\x5931");
SendMessageW(app.previewFilter, CB_ADDSTRING, 0, (LPARAM)L"\x7A7A\x767D");
SendMessageW(app.previewFilter, CB_SETCURSEL, 0, 0);
addLabel(app, p, L"\x76F8\x4F4D", 675, 14, 42, 24);
app.previewPhase = CreateWindowExW(0, L"COMBOBOX", L"", WS_CHILD | WS_VISIBLE | CBS_DROPDOWNLIST | WS_TABSTOP,
720, 12, 70, 160, p, (HMENU)(INT_PTR)IDC_PREVIEW_PHASE, app.inst, nullptr);
applyFont(app, app.previewPhase);
SendMessageW(app.previewPhase, CB_ADDSTRING, 0, (LPARAM)L"\x5168\x90E8");
SendMessageW(app.previewPhase, CB_ADDSTRING, 0, (LPARAM)L"A");
SendMessageW(app.previewPhase, CB_ADDSTRING, 0, (LPARAM)L"B");
SendMessageW(app.previewPhase, CB_ADDSTRING, 0, (LPARAM)L"C");
SendMessageW(app.previewPhase, CB_ADDSTRING, 0, (LPARAM)L"T");
SendMessageW(app.previewPhase, CB_SETCURSEL, 0, 0);
addLabel(app, p, L"\x7F29\x653E", 800, 14, 42, 24);
app.previewZoom = CreateWindowExW(0, L"COMBOBOX", L"", WS_CHILD | WS_VISIBLE | CBS_DROPDOWNLIST | WS_TABSTOP,
845, 12, 75, 160, p, (HMENU)(INT_PTR)IDC_PREVIEW_ZOOM, app.inst, nullptr);
applyFont(app, app.previewZoom);
SendMessageW(app.previewZoom, CB_ADDSTRING, 0, (LPARAM)L"80%");
SendMessageW(app.previewZoom, CB_ADDSTRING, 0, (LPARAM)L"100%");
SendMessageW(app.previewZoom, CB_ADDSTRING, 0, (LPARAM)L"125%");
SendMessageW(app.previewZoom, CB_ADDSTRING, 0, (LPARAM)L"150%");
SendMessageW(app.previewZoom, CB_SETCURSEL, 1, 0);
app.importIcd = addButton(app, p, IDC_IMPORT_ICD, L"\x5BFC\x5165icd", 930, 12, 95, 26);
app.reverseXml = addButton(app, p, IDC_REVERSE_XML, L"xml\x9006\x751F\x6210", 1035, 12, 100, 26);
addLabel(app, p, L"\x641C\x7D22", 18, 47, 42, 24);
app.previewSearch = addEdit(app, p, IDC_PREVIEW_SEARCH, "", 60, 45, 360, 26);
app.previewStatus = nullptr;
app.previewList = CreateWindowExW(WS_EX_CLIENTEDGE, WC_LISTVIEWW, L"",
WS_CHILD | WS_VISIBLE | WS_TABSTOP | LVS_REPORT | LVS_SHOWSELALWAYS,
18, 78, 1115, 557, p, (HMENU)(INT_PTR)IDC_PREVIEW_LIST, app.inst, nullptr);
updatePreviewFont(app);
ListView_SetExtendedListViewStyle(app.previewList, LVS_EX_FULLROWSELECT | LVS_EX_GRIDLINES);
resetPreviewColumns(app.previewList, 12);
refreshPreviewWorkbookChoices(app, "");
}
/**
* Function: createXmlPreviewPage
*
* @brief 构造 “createXmlPreviewPage” 对应的对象、消息、窗口控件或输出结构。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param app 应用状态;包含窗口句柄、当前选择、预览模型和运行期标志。
*/
void createXmlPreviewPage(AppState& app) {
HWND p = app.pageXmlPreview;
addButton(app, p, IDC_EXPORT_XML, L"\x5BFC\x51FAxml", 18, 12, 110, 28);
addButton(app, p, IDC_XML_SEARCH_BUTTON, L"\x641C\x7D22", 150, 12, 70, 28);
app.xmlSearch = addEdit(app, p, IDC_XML_SEARCH, "", 230, 12, 360, 28);
app.xmlPreview = CreateWindowExW(WS_EX_CLIENTEDGE, L"EDIT", L"",
WS_CHILD | WS_VISIBLE | WS_TABSTOP | ES_MULTILINE |
ES_AUTOVSCROLL | ES_AUTOHSCROLL | ES_NOHIDESEL | ES_WANTRETURN |
WS_VSCROLL | WS_HSCROLL,
18, 50, 1115, 585, p,
(HMENU)(INT_PTR)IDC_XML_PREVIEW_EDIT, app.inst, nullptr);
app.xmlFont = CreateFontW(-15, 0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE,
DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS,
CLEARTYPE_QUALITY, FIXED_PITCH | FF_MODERN, L"Consolas");
SendMessageW(app.xmlPreview, WM_SETFONT, (WPARAM)(app.xmlFont ? app.xmlFont : app.font), TRUE);
SendMessageW(app.xmlPreview, EM_SETLIMITTEXT, 0x7FFFFFFE, 0);
}
/**
* Function: createConfigPageClean
*
* @brief 构造 “createConfigPageClean” 对应的对象、消息、窗口控件或输出结构。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param app 应用状态;包含窗口句柄、当前选择、预览模型和运行期标志。
*/
void createConfigPageClean(AppState& app) {
PqToolConfig cfg = pqDefaultConfig();
HWND p = app.pageConfig;
const int groupX = 18, groupW = 1110, rowGap = 32, x1 = 38, x2 = 520, x3 = 810;
const int mqNameX = 38, mqFieldX = 160, mqTopicW = 310, mqTagX = 575, mqTagW = 250, mqKeyX = 930, mqKeyW = 120;
addLabel(app, p, L"\x914D\x7F6E\x9879", 22, 10, 120, 26);
addGroup(app, p, L"\x524D\x7F6E\x914D\x7F6E", groupX, 42, groupW, 82);
addLabeledEdit(app, p, L"FrontInst", IDC_FRONT_INST, cfg.frontInst, x1, 74, 118, 330);
addLabeledEdit(app, p, L"FrontIP", IDC_FRONT_IP, cfg.frontIp, x2, 74, 90, 180);
addGroup(app, p, L"\x53F0\x8D26\x914D\x7F6E", groupX, 136, groupW, 82);
addLabeledEdit(app, p, L"TerminalStatus", IDC_TERMINAL_STATUS, cfg.terminalStatus, x1, 168, 118, 140);
addLabeledEdit(app, p, L"MonitorStatus", IDC_MONITOR_STATUS, cfg.monitorStatus, x2, 168, 118, 140);
addLabeledEdit(app, p, L"IcdFlag", IDC_ICD_FLAG, cfg.icdFlag, x3, 168, 70, 80);
addGroup(app, p, L"\x63A5\x53E3\x914D\x7F6E", groupX, 230, groupW, 122);
addLabeledEdit(app, p, L"WebDevice", IDC_WEB_DEVICE, cfg.webDevice, x1, 262, 118, 520);
addLabeledEdit(app, p, L"WebIcd", IDC_WEB_ICD, cfg.webIcd, x3, 262, 90, 225);
addLabeledEdit(app, p, L"WebFiledownload", IDC_WEB_FILEDOWNLOAD, cfg.webFiledownload, x1, 296, 118, 520);
addLabeledEdit(app, p, L"XML\x76EE\x5F55", IDC_XML_SAVE_DIR, cfg.xmlSaveDir, x3, 296, 90, 225);
addGroup(app, p, L"MQ\x914D\x7F6E", groupX, 364, groupW, 316);
addLabel(app, p, L"\x751F\x4EA7\x8005", mqNameX, 398, 90, 22);
addLabeledEdit(app, p, L"producer", IDC_PRODUCER, cfg.producer, mqFieldX, 398, 90, 300);
addLabeledEdit(app, p, L"Ipport", IDC_IPPORT, cfg.ipport, mqTagX, 398, 80, 260);
addLabeledEdit(app, p, L"AccessKey", IDC_PRODUCER_ACCESS_KEY, cfg.producerAccessKey, mqFieldX, 398 + rowGap, 90, 300);
addLabeledEdit(app, p, L"SecretKey", IDC_PRODUCER_SECRET_KEY, cfg.producerSecretKey, mqTagX, 398 + rowGap, 80, 260);
addLabeledEdit(app, p, L"TopicLOG", IDC_TOPIC_LOG, cfg.topicLOG, mqFieldX, 398 + rowGap * 2, 90, mqTopicW);
addLabeledEdit(app, p, L"TagLOG", IDC_TAG_LOG, cfg.tagLOG, mqTagX, 398 + rowGap * 2, 75, mqTagW);
addLabeledEdit(app, p, L"KeyLOG", IDC_KEY_LOG, cfg.keyLOG, mqKeyX, 398 + rowGap * 2, 70, mqKeyW);
addLabeledEdit(app, p, L"TopicAsk", IDC_TOPIC_ASK, cfg.topicAsk, mqFieldX, 398 + rowGap * 3, 90, mqTopicW);
addLabeledEdit(app, p, L"TagAsk", IDC_TAG_ASK, cfg.tagAsk, mqTagX, 398 + rowGap * 3, 75, mqTagW);
addLabeledEdit(app, p, L"KeyAsk", IDC_KEY_ASK, cfg.keyAsk, mqKeyX, 398 + rowGap * 3, 70, mqKeyW);
const int consumerY = 526;
addLabel(app, p, L"\x6D88\x8D39\x8005", mqNameX, consumerY, 90, 22);
addLabeledEdit(app, p, L"consumer", IDC_CONSUMER, cfg.consumer, mqFieldX, consumerY, 90, 300);
addLabeledEdit(app, p, L"Ipport", IDC_CONSUMER_IPPORT, cfg.consumerIpport, mqTagX, consumerY, 80, 260);
addLabeledEdit(app, p, L"AccessKey", IDC_CONSUMER_ACCESS_KEY, cfg.consumerAccessKey, mqFieldX, consumerY + rowGap, 90, 300);
addLabeledEdit(app, p, L"SecretKey", IDC_CONSUMER_SECRET_KEY, cfg.consumerSecretKey, mqTagX, consumerY + rowGap, 80, 260);
addLabeledEdit(app, p, L"\x7EDF\x8BA1Topic", IDC_DATA_STAT_TOPIC, cfg.dataStatTopic, mqFieldX, consumerY + rowGap * 2, 90, mqTopicW);
addLabeledEdit(app, p, L"DATATag", IDC_DATA_STAT_TAG, cfg.dataStatTag, mqTagX, consumerY + rowGap * 2, 75, mqTagW);
addLabeledEdit(app, p, L"DATAKey", IDC_DATA_STAT_KEY, cfg.dataStatKey, mqKeyX, consumerY + rowGap * 2, 70, mqKeyW);
addLabeledEdit(app, p, L"\x5B9E\x65F6Topic", IDC_DATA_REALTIME_TOPIC, cfg.dataRealtimeTopic, mqFieldX, consumerY + rowGap * 3, 90, mqTopicW);
addLabeledEdit(app, p, L"DATATag", IDC_DATA_REALTIME_TAG, cfg.dataRealtimeTag, mqTagX, consumerY + rowGap * 3, 75, mqTagW);
addLabeledEdit(app, p, L"DATAKey", IDC_DATA_REALTIME_KEY, cfg.dataRealtimeKey, mqKeyX, consumerY + rowGap * 3, 70, mqKeyW);
addLabeledEdit(app, p, L"TraceTopic", IDC_TRACE_TOPIC, cfg.traceTopic, mqFieldX, consumerY + rowGap * 4, 90, mqTopicW);
addLabeledEdit(app, p, L"TraceTag", IDC_TRACE_TAG, cfg.traceTag, mqTagX, consumerY + rowGap * 4, 75, mqTagW);
addLabeledEdit(app, p, L"TraceKey", IDC_TRACE_KEY, cfg.traceKey, mqKeyX, consumerY + rowGap * 4, 70, mqKeyW);
}
/**
* Function: createLedgerPageClean
*
* @brief 构造 “createLedgerPageClean” 对应的对象、消息、窗口控件或输出结构。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param app 应用状态;包含窗口句柄、当前选择、预览模型和运行期标志。
*/
void createLedgerPageClean(AppState& app) {
PqToolConfig cfg = pqDefaultConfig();
HWND p = app.pageLedger;
int y = 16;
addLabeledEdit(app, p, L"XML\x6587\x4EF6", IDC_XML_PATH, cfg.xmlPath, 18, y, 70, 670);
addButton(app, p, IDC_BROWSE_XML, L"\x9009\x62E9XML", 780, y - 1, 90, 25);
addLabeledEdit(app, p, L"\x8F93\x51FA", IDC_OUTPUT_PATH, cfg.outputPath, 890, y, 48, 235);
y += 34;
addLabel(app, p, L"jsondata", 18, y, 70, 24);
addMemo(app, p, IDC_JSON_PATH, 88, y, 670, 52);
setText(p, IDC_JSON_PATH, cfg.jsonDataPath);
addButton(app, p, IDC_BROWSE_JSON, L"\x6DFB\x52A0JSON", 780, y - 1, 90, 25);
y += 66;
addLabel(app, p, L"origin", 18, y, 70, 24);
addMemo(app, p, IDC_ORIGIN_PATHS, 88, y, 670, 56);
setText(p, IDC_ORIGIN_PATHS, "origin.txt");
addButton(app, p, IDC_ADD_ORIGIN, L"\x6DFB\x52A0origin", 780, y - 1, 90, 25);
y += 70;
addLabel(app, p, L"\x8FDB\x7A0B\x7C7B\x578B", 18, y, 70, 24);
app.frontType = CreateWindowExW(0, L"COMBOBOX", L"", WS_CHILD | WS_VISIBLE | CBS_DROPDOWNLIST | WS_TABSTOP,
88, y, 150, 120, p, (HMENU)(INT_PTR)IDC_FRONT_TYPE, app.inst, nullptr);
applyFont(app, app.frontType);
SendMessageW(app.frontType, CB_ADDSTRING, 0, (LPARAM)L"cfg_stat_data");
SendMessageW(app.frontType, CB_ADDSTRING, 0, (LPARAM)L"cfg_3s_data");
SendMessageW(app.frontType, CB_SETCURSEL, 0, 0);
addButton(app, p, IDC_GET_LEDGER, L"\x83B7\x53D6\x53F0\x8D26", 260, y - 1, 100, 26);
addButton(app, p, IDC_INIT_MQ, L"\x521D\x59CB\x5316MQ", 375, y - 1, 100, 26);
addButton(app, p, IDC_TRACE, L"\x6570\x636E\x8FFD\x8E2A", 490, y - 1, 100, 26);
addButton(app, p, IDC_PARSE, L"\x4E00\x952E\x89E3\x6790", 605, y - 1, 100, 26);
int panelY = y + 42;
app.tree = CreateWindowExW(WS_EX_CLIENTEDGE, WC_TREEVIEWW, L"",
WS_CHILD | WS_VISIBLE | WS_TABSTOP | TVS_HASLINES | TVS_LINESATROOT | TVS_HASBUTTONS,
18, panelY, 430, 430, p, (HMENU)(INT_PTR)IDC_TREE, app.inst, nullptr);
applyFont(app, app.tree);
app.detail = addMemo(app, p, IDC_DETAIL, 465, panelY, 670, 150);
SendMessageW(app.detail, EM_SETREADONLY, TRUE, 0);
app.log = addMemo(app, p, IDC_LOG, 465, panelY + 165, 670, 265);
SendMessageW(app.log, EM_SETREADONLY, TRUE, 0);
updateDetails(app);
appendLogW(app, L"\x5DE5\x5177\x5DF2\x542F\x52A8\x3002");
}
/**
* Function: createUi
*
* @brief 构造 “createUi” 对应的对象、消息、窗口控件或输出结构。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param app 应用状态;包含窗口句柄、当前选择、预览模型和运行期标志。
*/
void createUi(AppState& app) {
// 这里手写 Win32 控件,没有资源文件;所有页面尺寸和控件布局都在 create*Page 函数中生成。
app.font = CreateFontW(-16, 0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE,
DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS,
CLEARTYPE_QUALITY, DEFAULT_PITCH | FF_SWISS, L"Microsoft YaHei UI");
app.ownsFont = app.font != nullptr;
if (!app.font) app.font = (HFONT)GetStockObject(DEFAULT_GUI_FONT);
app.tab = CreateWindowExW(0, WC_TABCONTROLW, L"", WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS | WS_TABSTOP,
10, 10, 1170, 735, app.hwnd, (HMENU)(INT_PTR)IDC_TAB, app.inst, nullptr);
applyFont(app, app.tab);
TCITEMW item{};
item.mask = TCIF_TEXT;
item.pszText = const_cast<wchar_t*>(L"配置项");
SendMessageW(app.tab, TCM_INSERTITEMW, 0, (LPARAM)&item);
item.pszText = const_cast<wchar_t*>(L"台账列表");
SendMessageW(app.tab, TCM_INSERTITEMW, 1, (LPARAM)&item);
item.pszText = const_cast<wchar_t*>(L"JSON\x8BE6\x60C5");
SendMessageW(app.tab, TCM_INSERTITEMW, 2, (LPARAM)&item);
item.pszText = const_cast<wchar_t*>(L"\x8868\x683C\x9884\x89C8");
SendMessageW(app.tab, TCM_INSERTITEMW, 3, (LPARAM)&item);
item.pszText = const_cast<wchar_t*>(L"XML\x9884\x89C8");
SendMessageW(app.tab, TCM_INSERTITEMW, 4, (LPARAM)&item);
item.pszText = const_cast<wchar_t*>(L"\x914D\x7F6E\x9879");
SendMessageW(app.tab, TCM_SETITEMW, 0, (LPARAM)&item);
item.pszText = const_cast<wchar_t*>(L"\x53F0\x8D26\x5217\x8868");
SendMessageW(app.tab, TCM_SETITEMW, 1, (LPARAM)&item);
SendMessageW(app.tab, TCM_SETITEMSIZE, 0, MAKELPARAM(150, 34));
app.pageConfig = CreateWindowExW(0, L"PqToolPanel", L"", WS_CHILD | WS_VISIBLE,
20, 52, 1150, 680, app.hwnd, (HMENU)(INT_PTR)IDC_PAGE_CONFIG, app.inst, nullptr);
app.pageLedger = CreateWindowExW(0, L"PqToolPanel", L"", WS_CHILD,
20, 52, 1150, 680, app.hwnd, (HMENU)(INT_PTR)IDC_PAGE_LEDGER, app.inst, nullptr);
app.pageJson = CreateWindowExW(0, L"PqToolPanel", L"", WS_CHILD,
20, 52, 1150, 680, app.hwnd, (HMENU)(INT_PTR)IDC_PAGE_JSON, app.inst, nullptr);
app.pagePreview = CreateWindowExW(0, L"PqToolPanel", L"", WS_CHILD,
20, 52, 1150, 680, app.hwnd, (HMENU)(INT_PTR)IDC_PAGE_PREVIEW, app.inst, nullptr);
app.pageXmlPreview = CreateWindowExW(0, L"PqToolPanel", L"", WS_CHILD,
20, 52, 1150, 680, app.hwnd, (HMENU)(INT_PTR)IDC_PAGE_XML_PREVIEW, app.inst, nullptr);
createConfigPageClean(app);
createLedgerPageClean(app);
createJsonPage(app);
createPreviewPage(app);
createXmlPreviewPage(app);
showPage(app, 0);
}
/**
* Function: wndProc
*
* @brief 处理 “wndProc” 对应的业务入口或事件,并调度下游函数完成实际工作。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @param hwnd Win32 窗口或控件句柄;必须在有效窗口生命周期内使用。
* @param msg 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @param wp 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @param lp 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。
* @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。
*/
LRESULT CALLBACK wndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
AppState& app = *g_app;
switch (msg) {
case WM_PQ_MQ_EVENT: {
// RocketMQ 回调线程不能直接操作窗口mq.cpp 通过 PostMessage 把事件切回 GUI 线程。
std::unique_ptr<PqMqEvent> ev(reinterpret_cast<PqMqEvent*>(lp));
if (ev) addJsonEvent(app, *ev);
return 0;
}
case WM_PQ_START_PREVIEW_EDIT:
startPreviewEdit(app, static_cast<int>(wp), static_cast<int>(lp));
return 0;
case WM_CREATE:
app.hwnd = hwnd;
createUi(app);
return 0;
case WM_COMMAND:
// WM_COMMAND 处理按钮、编辑框、下拉框;每个控件 ID 对应上面的 ControlId。
if (LOWORD(wp) == IDC_PREVIEW_FILTER && HIWORD(wp) == CBN_SELCHANGE) {
int idx = (int)SendMessageW(app.previewFilter, CB_GETCURSEL, 0, 0);
int filters[] = {-1, 1, 2, 3, 0};
app.previewStatusFilter = (idx >= 0 && idx < 5) ? filters[idx] : -1;
populatePreviewList(app);
return 0;
}
if (LOWORD(wp) == IDC_PREVIEW_PHASE && HIWORD(wp) == CBN_SELCHANGE) {
int idx = (int)SendMessageW(app.previewPhase, CB_GETCURSEL, 0, 0);
const char* phases[] = {"", "A", "B", "C", "T"};
app.previewPhaseFilter = (idx >= 0 && idx < 5) ? phases[idx] : "";
populatePreviewList(app);
return 0;
}
if (LOWORD(wp) == IDC_PREVIEW_SEARCH && HIWORD(wp) == EN_CHANGE) {
app.previewSearchText = getText(hwnd, IDC_PREVIEW_SEARCH);
populatePreviewList(app);
return 0;
}
if (LOWORD(wp) == IDC_XML_SEARCH && HIWORD(wp) == EN_CHANGE) {
app.xmlSearchText = getText(hwnd, IDC_XML_SEARCH);
app.xmlSearchLastText.clear();
app.xmlSearchNextPos = 0;
highlightXmlSearch(app, false, false);
return 0;
}
if (LOWORD(wp) == IDC_XML_PREVIEW_EDIT && HIWORD(wp) == EN_CHANGE) {
if (!app.updatingXmlPreview) {
syncXmlPreviewFromEditor(app);
app.xmlSearchLastText.clear();
app.xmlSearchNextPos = 0;
}
return 0;
}
if (LOWORD(wp) == IDC_XML_SEARCH_BUTTON && HIWORD(wp) == BN_CLICKED) {
app.xmlSearchText = getText(hwnd, IDC_XML_SEARCH);
if (!app.xmlPreviewEdited && (app.reverseXmlDirty || app.generatedXmlText.empty())) {
generateReverseXml(app, false);
app.xmlSearchLastText.clear();
app.xmlSearchNextPos = 0;
updateXmlPreviewText(app);
}
highlightXmlSearch(app, true, true);
return 0;
}
if (LOWORD(wp) == IDC_PREVIEW_CELL_EDIT && HIWORD(wp) == EN_KILLFOCUS) {
commitPreviewEdit(app, true);
return 0;
}
if (LOWORD(wp) == IDC_PREVIEW_ZOOM && HIWORD(wp) == CBN_SELCHANGE) {
int idx = (int)SendMessageW(app.previewZoom, CB_GETCURSEL, 0, 0);
int zooms[] = {80, 100, 125, 150};
if (idx >= 0 && idx < 4) app.previewZoomPercent = zooms[idx];
updatePreviewFont(app);
populatePreviewList(app);
return 0;
}
if (LOWORD(wp) == IDC_PREVIEW_WORKBOOK && HIWORD(wp) == CBN_SELCHANGE) {
int idx = (int)SendMessageW(app.previewWorkbook, CB_GETCURSEL, 0, 0);
if (idx >= 0 && idx < (int)app.previewWorkbooks.size()) {
refreshPreviewFromWorkbook(app, app.previewWorkbooks[idx].path);
}
return 0;
}
if (LOWORD(wp) == IDC_PREVIEW_SHEET && HIWORD(wp) == CBN_SELCHANGE) {
app.activePreviewSheet = (int)SendMessageW(app.previewSheet, CB_GETCURSEL, 0, 0);
app.reverseXmlDirty = true;
populatePreviewList(app);
return 0;
}
if (LOWORD(wp) == IDC_JSON_MONITOR) {
int notify = HIWORD(wp);
if (notify == CBN_SELCHANGE) {
if (!app.jsonMonitorRefreshing &&
!SendMessageW(app.jsonMonitor, CB_GETDROPPEDSTATE, 0, 0)) {
int idx = (int)SendMessageW(app.jsonMonitor, CB_GETCURSEL, 0, 0);
selectJsonMonitorChoice(app, idx);
}
return 0;
}
if (notify == CBN_SELENDOK) {
if (!app.jsonMonitorRefreshing) {
int idx = (int)SendMessageW(app.jsonMonitor, CB_GETCURSEL, 0, 0);
selectJsonMonitorChoice(app, idx);
}
return 0;
}
if (notify == CBN_CLOSEUP) {
if (app.jsonMonitorRefreshPending) {
std::string selected = app.activeTraceDir.empty()
? app.pendingJsonMonitorTraceDir
: app.activeTraceDir;
app.jsonMonitorRefreshPending = false;
app.pendingJsonMonitorTraceDir.clear();
refreshJsonMonitorChoices(app, selected);
}
return 0;
}
return 0;
}
if (LOWORD(wp) == IDC_BROWSE_JSON) {
std::string path;
if (openFileDialog(hwnd, L"\x6DFB\x52A0 jsondata \x6587\x4EF6", L"Text/JSON Files\0*.txt;*.json\0All Files\0*.*\0", path)) {
std::vector<std::string> paths = splitPathList(getText(hwnd, IDC_JSON_PATH));
if (std::find(paths.begin(), paths.end(), path) == paths.end()) paths.push_back(path);
setText(hwnd, IDC_JSON_PATH, joinPaths(paths));
}
return 0;
}
switch (LOWORD(wp)) {
case IDC_GET_LEDGER: runGetLedger(app); return 0;
case IDC_INIT_MQ: runInitMq(app); return 0;
case IDC_TRACE: runTrace(app); return 0;
case IDC_PARSE: runParse(app); return 0;
case IDC_IMPORT_ICD: runImportIcd(app); return 0;
case IDC_REVERSE_XML: runReverseXml(app); return 0;
case IDC_EXPORT_XML: runExportXml(app); return 0;
case IDC_BROWSE_XML: {
std::string path;
if (openFileDialog(hwnd, L"选择 XML 文件", L"XML Files\0*.xml\0All Files\0*.*\0", path)) setText(hwnd, IDC_XML_PATH, path);
return 0;
}
case IDC_BROWSE_JSON: {
std::string path;
if (openFileDialog(hwnd, L"选择 jsondata.txt", L"Text/JSON Files\0*.txt;*.json\0All Files\0*.*\0", path)) setText(hwnd, IDC_JSON_PATH, path);
return 0;
}
case IDC_ADD_ORIGIN: {
std::string path;
if (openFileDialog(hwnd, L"选择 origin 文件", L"Text/JSON Files\0*.txt;*.json\0All Files\0*.*\0", path)) {
std::string old = getText(hwnd, IDC_ORIGIN_PATHS);
if (!old.empty() && old.back() != '\n' && old.back() != '\r') old += "\r\n";
setText(hwnd, IDC_ORIGIN_PATHS, old + path);
}
return 0;
}
}
break;
case WM_NOTIFY:
// WM_NOTIFY 处理 TreeView/ListView/TabControl 的选择、双击、自绘等通知。
if (((LPNMHDR)lp)->idFrom == IDC_PREVIEW_LIST && ((LPNMHDR)lp)->code == NM_DBLCLK) {
auto* item = reinterpret_cast<NMITEMACTIVATE*>(lp);
if (item->iItem >= 0 && item->iSubItem >= 0) {
PostMessageW(hwnd, WM_PQ_START_PREVIEW_EDIT,
static_cast<WPARAM>(item->iItem),
static_cast<LPARAM>(item->iSubItem));
}
return 0;
}
if (((LPNMHDR)lp)->idFrom == IDC_PREVIEW_LIST && ((LPNMHDR)lp)->code == NM_CUSTOMDRAW) {
auto* cd = reinterpret_cast<NMLVCUSTOMDRAW*>(lp);
if (cd->nmcd.dwDrawStage == CDDS_PREPAINT) return CDRF_NOTIFYITEMDRAW;
if (cd->nmcd.dwDrawStage == CDDS_ITEMPREPAINT) return CDRF_NOTIFYSUBITEMDRAW;
if (cd->nmcd.dwDrawStage == (CDDS_ITEMPREPAINT | CDDS_SUBITEM)) {
size_t idx = static_cast<size_t>(cd->nmcd.lItemlParam);
int status = 0;
std::string cellStyle;
bool finalRangeChanged = false;
int modifiedOriginPickColumn = -1;
if (!app.previewSheets.empty() &&
app.activePreviewSheet >= 0 &&
app.activePreviewSheet < (int)app.previewSheets.size() &&
idx < app.previewSheets[app.activePreviewSheet].rows.size()) {
const PreviewSheetRow& row = app.previewSheets[app.activePreviewSheet].rows[idx];
status = row.status;
finalRangeChanged = row.finalRangeChanged;
modifiedOriginPickColumn = row.modifiedOriginPickColumn;
if (cd->iSubItem >= 0 && cd->iSubItem < (int)row.cellStyles.size()) {
cellStyle = row.cellStyles[cd->iSubItem];
}
}
if (finalRangeChanged && cd->iSubItem == 8) {
cd->clrTextBk = RGB(153, 0, 0);
cd->clrText = RGB(255, 255, 255);
}
else if (modifiedOriginPickColumn == cd->iSubItem) cd->clrTextBk = RGB(180, 167, 214);
else if (cellStyle == "OriginPickBlue") cd->clrTextBk = RGB(207, 226, 243);
else if (status == 1) cd->clrTextBk = RGB(217, 234, 211);
else if (status == 2) cd->clrTextBk = RGB(252, 228, 214);
else if (status == 3) cd->clrTextBk = RGB(244, 204, 204);
else if (status == 4) cd->clrTextBk = RGB(217, 234, 211);
else if (status == 5) cd->clrTextBk = RGB(232, 238, 247);
else if (status == 6) cd->clrTextBk = RGB(244, 244, 244);
return CDRF_NEWFONT;
}
}
if (((LPNMHDR)lp)->idFrom == IDC_TAB && ((LPNMHDR)lp)->code == TCN_SELCHANGE) {
showPage(app, (int)SendMessageW(app.tab, TCM_GETCURSEL, 0, 0));
return 0;
}
if (((LPNMHDR)lp)->idFrom == IDC_TREE && ((LPNMHDR)lp)->code == TVN_SELCHANGEDW) {
auto* tv = (LPNMTREEVIEWW)lp;
auto* ref = (NodeRef*)tv->itemNew.lParam;
if (ref) {
app.selectedDevice = ref->device;
app.selectedMonitor = ref->monitor;
updateDetails(app);
updatePathsForSelection(app);
}
return 0;
}
if (((LPNMHDR)lp)->idFrom == IDC_JSON_TREE && ((LPNMHDR)lp)->code == TVN_SELCHANGEDW) {
auto* tv = (LPNMTREEVIEWW)lp;
auto* ref = (JsonNodeRef*)tv->itemNew.lParam;
app.selectedJson = ref ? ref->index : -1;
updateJsonDetail(app);
return 0;
}
break;
case WM_DESTROY:
if (app.previewFont) DeleteObject(app.previewFont);
if (app.xmlFont) DeleteObject(app.xmlFont);
if (app.ownsFont && app.font) DeleteObject(app.font);
PostQuitMessage(0);
return 0;
}
return DefWindowProcW(hwnd, msg, wp, lp);
}
} // namespace
/**
* Function: runPqGuiApp
*
* @brief 处理 “runPqGuiApp” 对应的业务入口或事件,并调度下游函数完成实际工作。
* @details 界面使用原生 Win32 API函数通常读取或更新 AppState并通过控件 ID、消息通知和 ListView/TreeView API 驱动交互。除 MQ 事件投递外,界面控件操作应在 GUI 线程执行。
* @return 整数状态、索引或计数;入口函数以 0 表示成功。
*/
int runPqGuiApp() {
AppState app;
g_app = &app;
app.inst = GetModuleHandleW(nullptr);
// COM 初始化主要服务于系统组件/对话框;主流程仍是普通 Win32 消息循环。
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
INITCOMMONCONTROLSEX icc{};
icc.dwSize = sizeof(icc);
icc.dwICC = ICC_TREEVIEW_CLASSES | ICC_TAB_CLASSES | ICC_STANDARD_CLASSES | ICC_LISTVIEW_CLASSES;
InitCommonControlsEx(&icc);
WNDCLASSW wc{};
wc.hInstance = app.inst;
wc.lpszClassName = L"PqToolWindow";
wc.lpfnWndProc = wndProc;
wc.hCursor = LoadCursorW(nullptr, MAKEINTRESOURCEW(32512));
wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
RegisterClassW(&wc);
WNDCLASSW panel{};
panel.hInstance = app.inst;
panel.lpszClassName = L"PqToolPanel";
panel.lpfnWndProc = panelProc;
panel.hCursor = LoadCursorW(nullptr, MAKEINTRESOURCEW(32512));
panel.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
RegisterClassW(&panel);
HWND hwnd = CreateWindowExW(0, wc.lpszClassName, L"PQ 映射解析与追踪工具",
WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 1210, 820,
nullptr, nullptr, app.inst, nullptr);
if (!hwnd) {
CoUninitialize();
return 1;
}
SetWindowTextW(hwnd, L"PQ \x6620\x5C04\x89E3\x6790\x4E0E\x8FFD\x8E2A\x5DE5\x5177");
ShowWindow(hwnd, SW_SHOW);
UpdateWindow(hwnd);
// 标准消息循环:用户操作 -> wndProc -> run* 业务函数 -> 刷新 AppState/控件。
MSG message;
while (GetMessageW(&message, nullptr, 0, 0) > 0) {
TranslateMessage(&message);
DispatchMessageW(&message);
}
CoUninitialize();
g_app = nullptr;
return (int)message.wParam;
}