4183 lines
223 KiB
C++
4183 lines
223 KiB
C++
#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; // 预览表编辑后置 true,XML 预览/搜索前会重新生成。
|
||
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;
|
||
}
|