Files
datatrace/source/interface.cpp
2026-07-13 15:12:55 +08:00

2849 lines
120 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

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

#define NOMINMAX
#ifndef UNICODE
#define UNICODE
#endif
#ifndef _UNICODE
#define _UNICODE
#endif
#include "pq_app.h"
#include "json.hpp"
#include "tinyxml2.h"
#include <windows.h>
#include <commctrl.h>
#include <commdlg.h>
#include <algorithm>
#include <cctype>
#include <cwctype>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <map>
#include <memory>
#include <regex>
#include <set>
#include <sstream>
#include <unordered_map>
#include <unordered_set>
namespace {
using json = nlohmann::json;
namespace fs = std::filesystem;
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_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;
};
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;
std::string phase;
int status = 0;
};
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;
std::string activePreviewWorkbook;
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;
bool autoParseBusy = false;
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;
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;
}
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;
};
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;
}
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;
}
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);
}
void setText(HWND hwnd, int id, const std::string& value) {
HWND h = findControl(hwnd, id);
if (h) SetWindowTextW(h, u8w(value).c_str());
}
void applyFont(AppState& app, HWND ctl) {
SendMessageW(ctl, WM_SETFONT, (WPARAM)app.font, TRUE);
}
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);
}
}
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);
}
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;
}
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;
}
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;
}
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;
}
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;
}
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);
}
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());
}
void appendLog(AppState& app, const std::string& line) {
appendLogW(app, u8w(line));
}
std::string paddedLineNumber(int line, int width) {
std::string text = std::to_string(line);
if ((int)text.size() < width) text.insert(text.begin(), width - text.size(), ' ');
return text;
}
std::string xmlPreviewDisplayText(const std::string& xml) {
if (xml.empty()) return "";
int lines = 1;
for (char c : xml) {
if (c == '\n') ++lines;
}
int width = std::max(3, (int)std::to_string(lines).size());
std::ostringstream out;
std::stringstream ss(xml);
std::string line;
int lineNo = 1;
while (std::getline(ss, line)) {
if (!line.empty() && line.back() == '\r') line.pop_back();
out << paddedLineNumber(lineNo++, width) << " " << line << "\r\n";
}
return out.str();
}
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;
}
void updateXmlPreviewText(AppState& app) {
if (!app.xmlPreview) return;
SetWindowTextW(app.xmlPreview, u8w(xmlPreviewDisplayText(app.generatedXmlText)).c_str());
highlightXmlSearch(app, false, false);
}
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);
}
}
void hideBusyPopup(AppState& app) {
if (app.busyPopup && IsWindow(app.busyPopup)) {
DestroyWindow(app.busyPopup);
}
app.busyPopup = nullptr;
}
struct BusyPopupGuard {
AppState& app;
bool active;
BusyPopupGuard(AppState& state, bool show, const wchar_t* message)
: app(state), active(show) {
if (active) {
app.autoParseBusy = true;
showBusyPopup(app, message);
}
}
~BusyPopupGuard() {
if (active) {
hideBusyPopup(app);
app.autoParseBusy = false;
}
}
};
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();
}
std::string truncateText(std::string text, size_t limit = 260) {
if (text.size() <= limit) return text;
return text.substr(0, limit) + "...";
}
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;
}
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;
}
bool textContainsFilter(const std::string& text, const std::string& filter) {
if (filter.empty()) return true;
return lowerAsciiCopy(text).find(lowerAsciiCopy(filter)) != std::string::npos;
}
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);
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;
}
PqToolConfig readConfig(HWND hwnd) {
PqToolConfig cfg = pqDefaultConfig();
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.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);
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;
}
int toInt(const std::string& s) {
try {
return s.empty() ? 0 : std::stoi(s);
} catch (...) {
return 0;
}
}
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;
}
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;
}
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);
}
HTREEITEM insertTreeItem(AppState& app, HTREEITEM parent, const std::wstring& text, NodeRef* ref) {
return insertTreeItemRaw(app.tree, parent, text, (LPARAM)ref);
}
HTREEITEM insertJsonTreeItem(AppState& app, HTREEITEM parent, const std::wstring& text, JsonNodeRef* ref) {
return insertTreeItemRaw(app.jsonTree, parent, text, (LPARAM)ref);
}
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;
}
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;
}
std::wstring processLabel(int processNo) {
return L"\x8FDB\x7A0B " + std::to_wstring(processNo);
}
void populateTree(AppState& app) {
TreeView_DeleteAllItems(app.tree);
app.refs.clear();
app.selectedDevice = -1;
app.selectedMonitor = -1;
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);
}
}
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";
}
}
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());
}
std::string prettyJsonText(const std::string& text) {
try {
return json::parse(text).dump(2);
} catch (...) {
return text;
}
}
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();
}
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;
}
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;
}
std::string dataTypeFromJsonText(const std::string& text) {
try {
json root = json::parse(text);
std::string dataType;
if (findDataTypeInJson(root, dataType)) return dataType;
} catch (...) {
}
return "";
}
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));
}
}
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));
}
}
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;
}
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;
}
std::string traceDirFromPath(const std::string& path) {
if (path.empty()) return "";
try {
return fs::u8path(path).parent_path().u8string();
} catch (...) {
return "";
}
}
std::string eventTraceDir(const PqMqEvent& ev) {
if (!ev.traceDir.empty()) return ev.traceDir;
return traceDirFromPath(ev.path);
}
bool sameTraceDir(const std::string& a, const std::string& b) {
if (a.empty() || b.empty()) return false;
return normalizedPathPrefix(a) == normalizedPathPrefix(b);
}
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);
}
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;
}
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));
}
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);
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();
}
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()});
}
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);
}
void resetJsonSession(AppState& app, const std::string& traceDir) {
app.activeTraceDir = traceDir;
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");
}
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";
}
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());
}
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);
}
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";
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());
}
}
std::string attrValue(const tinyxml2::XMLElement* element, const char* name) {
if (!element) return "";
const char* value = element->Attribute(name);
return value ? value : "";
}
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 : "";
}
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);
}
int previewStatusFromStyle(const std::string& style) {
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;
}
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")) {
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;
}
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;
}
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 ? 300 : 110);
col.cx = std::max(45, baseWidth * zoom / 100);
col.iSubItem = static_cast<int>(i);
ListView_InsertColumn(list, static_cast<int>(i), &col);
}
}
bool previewRowMatchesFilter(const PreviewSheetRow& row, int statusFilter) {
if (statusFilter < 0) return true;
return row.status == statusFilter;
}
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 "";
}
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;
}
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;
}
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;
}
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;
}
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);
}
}
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;
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);
}
bool isAbcPhase(const std::string& phase) {
return phase == "A" || phase == "B" || phase == "C";
}
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];
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();
}
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);
}
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]);
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;
}
}
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);
app.reverseXmlDirty = true;
}
DestroyWindow(edit);
populatePreviewList(app, topModelRow, horzPos);
}
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);
}
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;
}
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());
}
}
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;
};
}
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);
}
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;
}
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;
}
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;
}
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;
}
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;
}
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);
}
}
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});
}
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 "";
}
}
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();
}
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);
}
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);
}
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) {
if (app.reverseXmlDirty || app.generatedXmlText.empty()) generateReverseXml(app, false);
updateXmlPreviewText(app);
}
}
void selectPage(AppState& app, int index) {
if (app.tab) SendMessageW(app.tab, TCM_SETCURSEL, index, 0);
showPage(app, index);
}
void runGetLedger(AppState& app) {
try {
PqToolConfig cfg = readConfig(app.hwnd);
appendLogW(app, L"开始获取台账和 ICD/XML...");
std::string firstXml;
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);
}
}
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());
}
}
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);
resetJsonSession(app, traceDir);
pqClearTraceRecords(cfg, d, m);
appendLog(app, "已清空当前测点旧文本记录: " + traceDir);
prepareTraceWorkspace(app, cfg, d, m, true);
app.autoPreviewEnabled = true;
selectPage(app, 3);
autoParseTraceDir(app, traceDir, "trace-start");
pqSendTraceRequest(cfg, d, m, selectedFrontType(app),
[&](const std::string& s) { appendLog(app, s); },
makeMqEventCallback(app));
} catch (const std::exception& e) {
appendLog(app, std::string("数据追踪失败: ") + e.what());
}
}
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;
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);
}
}
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;
}
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);
}
}
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);
}
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;
};
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;
}
bool reverseWiringAllowed(const std::string& valueWiring, const std::string& sheetWiring) {
return valueWiring.empty() || valueWiring == "common" || valueWiring == sheetWiring;
}
std::string cleanPhaseLabelForReverse(std::string label) {
label = labelWithoutPhase(label);
return trimAscii(label);
}
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)) {
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;
}
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;
}
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);
}
std::string reverseDaForXml(std::string daPath, const std::string& valueOffset) {
if (valueOffset.empty()) return daPath;
static const std::regex rangeRe(R"(\[\d+-\d+\])");
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;
};
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(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";
};
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);
}
}
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");
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;
}
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 "";
}
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;
}
void runReverseXml(AppState& app) {
if (!generateReverseXml(app, true).empty()) selectPage(app, 4);
}
void runExportXml(AppState& app) {
if (app.generatedXmlText.empty() || app.reverseXmlDirty) generateReverseXml(app, false);
if (app.generatedXmlText.empty()) 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);
}
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);
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);
}
}
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);
}
}
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);
addLabel(app, p, L"消费者", x1, 512, 80, 22);
addLabeledEdit(app, p, L"consumer", IDC_CONSUMER, cfg.consumer, 120, 512, 90, 300);
addLabeledEdit(app, p, L"Ipport", IDC_CONSUMER_IPPORT, cfg.consumerIpport, 600, 512, 80, 260);
addLabeledEdit(app, p, L"AccessKey", IDC_CONSUMER_ACCESS_KEY, cfg.consumerAccessKey, 120, 512 + rowGap, 90, 300);
addLabeledEdit(app, p, L"SecretKey", IDC_CONSUMER_SECRET_KEY, cfg.consumerSecretKey, 600, 512 + rowGap, 80, 260);
addLabeledEdit(app, p, L"\x7EDF\x8BA1Topic", IDC_DATA_STAT_TOPIC, cfg.dataStatTopic, 120, 512 + rowGap * 2, 90, 360);
addLabeledEdit(app, p, L"DATATag", IDC_DATA_STAT_TAG, cfg.dataStatTag, 575, 512 + rowGap * 2, 75, 270);
addLabeledEdit(app, p, L"DATAKey", IDC_DATA_STAT_KEY, cfg.dataStatKey, 940, 512 + rowGap * 2, 70, 120);
addLabeledEdit(app, p, L"\x5B9E\x65F6Topic", IDC_DATA_REALTIME_TOPIC, cfg.dataRealtimeTopic, 120, 512 + rowGap * 3, 90, 360);
addLabeledEdit(app, p, L"DATATag", IDC_DATA_REALTIME_TAG, cfg.dataRealtimeTag, 575, 512 + rowGap * 3, 75, 270);
addLabeledEdit(app, p, L"DATAKey", IDC_DATA_REALTIME_KEY, cfg.dataRealtimeKey, 940, 512 + rowGap * 3, 70, 120);
addLabeledEdit(app, p, L"TraceTopic", IDC_TRACE_TOPIC, cfg.traceTopic, 120, 512 + rowGap * 4, 90, 360);
addLabeledEdit(app, p, L"TraceTag", IDC_TRACE_TAG, cfg.traceTag, 575, 512 + rowGap * 4, 75, 270);
addLabeledEdit(app, p, L"TraceKey", IDC_TRACE_KEY, cfg.traceKey, 940, 512 + rowGap * 4, 70, 120);
}
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 参数运行原命令行解析。");
}
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);
}
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);
}
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, "");
}
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 |
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_SETREADONLY, TRUE, 0);
}
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);
addLabel(app, p, L"\x6D88\x8D39\x8005", mqNameX, 512, 90, 22);
addLabeledEdit(app, p, L"consumer", IDC_CONSUMER, cfg.consumer, mqFieldX, 512, 90, 300);
addLabeledEdit(app, p, L"Ipport", IDC_CONSUMER_IPPORT, cfg.consumerIpport, mqTagX, 512, 80, 260);
addLabeledEdit(app, p, L"AccessKey", IDC_CONSUMER_ACCESS_KEY, cfg.consumerAccessKey, mqFieldX, 512 + rowGap, 90, 300);
addLabeledEdit(app, p, L"SecretKey", IDC_CONSUMER_SECRET_KEY, cfg.consumerSecretKey, mqTagX, 512 + rowGap, 80, 260);
addLabeledEdit(app, p, L"\x7EDF\x8BA1Topic", IDC_DATA_STAT_TOPIC, cfg.dataStatTopic, mqFieldX, 512 + rowGap * 2, 90, mqTopicW);
addLabeledEdit(app, p, L"DATATag", IDC_DATA_STAT_TAG, cfg.dataStatTag, mqTagX, 512 + rowGap * 2, 75, mqTagW);
addLabeledEdit(app, p, L"DATAKey", IDC_DATA_STAT_KEY, cfg.dataStatKey, mqKeyX, 512 + rowGap * 2, 70, mqKeyW);
addLabeledEdit(app, p, L"\x5B9E\x65F6Topic", IDC_DATA_REALTIME_TOPIC, cfg.dataRealtimeTopic, mqFieldX, 512 + rowGap * 3, 90, mqTopicW);
addLabeledEdit(app, p, L"DATATag", IDC_DATA_REALTIME_TAG, cfg.dataRealtimeTag, mqTagX, 512 + rowGap * 3, 75, mqTagW);
addLabeledEdit(app, p, L"DATAKey", IDC_DATA_REALTIME_KEY, cfg.dataRealtimeKey, mqKeyX, 512 + rowGap * 3, 70, mqKeyW);
addLabeledEdit(app, p, L"TraceTopic", IDC_TRACE_TOPIC, cfg.traceTopic, mqFieldX, 512 + rowGap * 4, 90, mqTopicW);
addLabeledEdit(app, p, L"TraceTag", IDC_TRACE_TAG, cfg.traceTag, mqTagX, 512 + rowGap * 4, 75, mqTagW);
addLabeledEdit(app, p, L"TraceKey", IDC_TRACE_KEY, cfg.traceKey, mqKeyX, 512 + rowGap * 4, 70, mqKeyW);
}
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");
}
void createUi(AppState& app) {
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);
}
LRESULT CALLBACK wndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
AppState& app = *g_app;
switch (msg) {
case WM_PQ_MQ_EVENT: {
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:
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_SEARCH_BUTTON && HIWORD(wp) == BN_CLICKED) {
app.xmlSearchText = getText(hwnd, IDC_XML_SEARCH);
if (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:
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;
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;
if (cd->iSubItem >= 0 && cd->iSubItem < (int)row.cellStyles.size()) {
cellStyle = row.cellStyles[cd->iSubItem];
}
}
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
int runPqGuiApp() {
AppState app;
g_app = &app;
app.inst = GetModuleHandleW(nullptr);
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);
MSG message;
while (GetMessageW(&message, nullptr, 0, 0) > 0) {
TranslateMessage(&message);
DispatchMessageW(&message);
}
CoUninitialize();
g_app = nullptr;
return (int)message.wParam;
}