优化了xml修改和表格逆生成功能
This commit is contained in:
@@ -29,11 +29,31 @@
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
|
||||
/*
|
||||
* interface.cpp
|
||||
*
|
||||
* 本文件实现 Win32 桌面界面和页面交互,是用户操作到业务函数的“调度层”。
|
||||
*
|
||||
* 页面结构:
|
||||
* 1. 配置项页:填写前置、接口、MQ Topic/Tag/Key 等配置。
|
||||
* 2. 台账列表页:获取台账/ICD,选择装置和测点,执行初始化 MQ、数据追踪、一键解析。
|
||||
* 3. JSON 详情页:展示已发送追踪请求、DATATopic jsondata、TraceTopic origin。
|
||||
* 4. 表格预览页:读取 main.cpp 输出的 Excel XML,按状态/相位/搜索过滤,可编辑配置列。
|
||||
* 5. XML 预览页:根据预览页编辑结果反向写回 XML,并支持导出。
|
||||
*
|
||||
* 关键交互流程:
|
||||
* runGetLedger() -> pqFetchLedgerAndIcd() -> populateTree()/updatePathsForSelection()
|
||||
* runTrace() -> pqSendTraceRequest() -> MQ 回调 addJsonEvent() -> autoParseTraceDir()
|
||||
* runParse() -> pqRunParser() -> refreshPreviewFromWorkbook()
|
||||
* runReverseXml()-> generateReverseXml() -> XML 预览页
|
||||
*/
|
||||
|
||||
namespace {
|
||||
|
||||
using json = nlohmann::json;
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
// 所有控件 ID 集中在这里,WM_COMMAND/WM_NOTIFY 根据这些 ID 路由事件。
|
||||
enum ControlId {
|
||||
IDC_TAB = 900,
|
||||
IDC_PAGE_CONFIG,
|
||||
@@ -59,6 +79,9 @@ enum ControlId {
|
||||
IDC_TOPIC_LOG,
|
||||
IDC_TAG_LOG,
|
||||
IDC_KEY_LOG,
|
||||
IDC_TOPIC_ASK,
|
||||
IDC_TAG_ASK,
|
||||
IDC_KEY_ASK,
|
||||
IDC_CONSUMER,
|
||||
IDC_CONSUMER_IPPORT,
|
||||
IDC_DATA_STAT_TOPIC,
|
||||
@@ -111,6 +134,7 @@ struct NodeRef {
|
||||
int monitor = -1;
|
||||
};
|
||||
|
||||
// JSON 树中的一条记录,可能来自 sent/data/trace 三种来源。
|
||||
struct JsonItem {
|
||||
std::string kind;
|
||||
std::string title;
|
||||
@@ -133,11 +157,13 @@ struct JsonMonitorChoice {
|
||||
};
|
||||
|
||||
struct PreviewSheetRow {
|
||||
std::vector<std::string> cells;
|
||||
std::vector<std::string> originalCells;
|
||||
std::vector<std::string> cellStyles;
|
||||
std::string phase;
|
||||
int status = 0;
|
||||
std::vector<std::string> cells; // 当前可见/可编辑的单元格文本。
|
||||
std::vector<std::string> originalCells; // 从工作簿读出的原始单元格,用于判断逆向生成时哪些行被改过。
|
||||
std::vector<std::string> cellStyles; // Excel XML 中的 ss:StyleID,用于 GUI 着色。
|
||||
std::string phase; // A/B/C/T,用于相位过滤和同步编辑。
|
||||
int status = 0; // 0 空白,1 正常,2 异常,3 缺失,4 表头,5 分段,6 子分组。
|
||||
bool finalRangeChanged = false; // 字段数组范围因表格参数修改而变化,字段格显示深红色。
|
||||
int modifiedOriginPickColumn = -1; // 修改后的原始值取值起点列,GUI 用紫色标记。
|
||||
};
|
||||
|
||||
struct PreviewSheet {
|
||||
@@ -186,15 +212,15 @@ struct AppState {
|
||||
HFONT previewFont = nullptr;
|
||||
HFONT xmlFont = nullptr;
|
||||
bool ownsFont = false;
|
||||
std::vector<PqLedgerDevice> devices;
|
||||
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 activeTraceDir; // 当前测点追踪目录,用于过滤 JSON 记录和自动解析。
|
||||
std::string activePreviewWorkbook; // 当前表格预览使用的 .xls。
|
||||
std::string pendingJsonMonitorTraceDir;
|
||||
std::string generatedXmlText;
|
||||
std::string generatedXmlSourcePath;
|
||||
@@ -205,8 +231,10 @@ struct AppState {
|
||||
bool autoPreviewEnabled = false;
|
||||
bool jsonMonitorRefreshing = false;
|
||||
bool jsonMonitorRefreshPending = false;
|
||||
bool reverseXmlDirty = true;
|
||||
bool autoParseBusy = false;
|
||||
bool reverseXmlDirty = true; // 预览表编辑后置 true,XML 预览/搜索前会重新生成。
|
||||
bool updatingXmlPreview = false; // SetWindowText 会触发 EN_CHANGE,用它区分自动刷新和人工编辑。
|
||||
bool xmlPreviewEdited = false;
|
||||
bool autoParseBusy = false; // MQ 数据到达后自动解析时显示忙碌提示。
|
||||
int activePreviewSheet = 0;
|
||||
int previewZoomPercent = 100;
|
||||
int previewStatusFilter = -1;
|
||||
@@ -361,28 +389,21 @@ 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;
|
||||
// 可编辑预览必须展示原始 XML,不能再混入行号,否则导出后不是合法 XML。
|
||||
std::string out;
|
||||
out.reserve(xml.size() + xml.size() / 20);
|
||||
for (size_t i = 0; i < xml.size(); ++i) {
|
||||
if (xml[i] == '\r') {
|
||||
if (i + 1 < xml.size() && xml[i + 1] == '\n') ++i;
|
||||
out += "\r\n";
|
||||
} else if (xml[i] == '\n') {
|
||||
out += "\r\n";
|
||||
} else {
|
||||
out.push_back(xml[i]);
|
||||
}
|
||||
}
|
||||
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();
|
||||
return out;
|
||||
}
|
||||
|
||||
bool highlightXmlSearch(AppState& app, bool focusPreview = false, bool findNext = false) {
|
||||
@@ -425,10 +446,20 @@ bool highlightXmlSearch(AppState& app, bool focusPreview = false, bool findNext
|
||||
|
||||
void updateXmlPreviewText(AppState& app) {
|
||||
if (!app.xmlPreview) return;
|
||||
app.updatingXmlPreview = true;
|
||||
SetWindowTextW(app.xmlPreview, u8w(xmlPreviewDisplayText(app.generatedXmlText)).c_str());
|
||||
app.updatingXmlPreview = false;
|
||||
app.xmlPreviewEdited = false;
|
||||
highlightXmlSearch(app, false, false);
|
||||
}
|
||||
|
||||
void syncXmlPreviewFromEditor(AppState& app) {
|
||||
if (!app.xmlPreview || app.updatingXmlPreview) return;
|
||||
app.generatedXmlText = getText(app.hwnd, IDC_XML_PREVIEW_EDIT);
|
||||
app.reverseXmlDirty = false;
|
||||
app.xmlPreviewEdited = true;
|
||||
}
|
||||
|
||||
void showBusyPopup(AppState& app, const wchar_t* message) {
|
||||
if (!app.hwnd) return;
|
||||
if (app.busyPopup && IsWindow(app.busyPopup)) {
|
||||
@@ -545,6 +576,7 @@ std::string selectedFrontTypeFromWindow(HWND hwnd) {
|
||||
|
||||
PqToolConfig readConfig(HWND hwnd) {
|
||||
PqToolConfig cfg = pqDefaultConfig();
|
||||
// GUI 编辑框是配置最终来源;先用默认值兜底,再逐项用界面内容覆盖。
|
||||
cfg.frontInst = getText(hwnd, IDC_FRONT_INST);
|
||||
cfg.frontIp = getText(hwnd, IDC_FRONT_IP);
|
||||
cfg.terminalStatus = getText(hwnd, IDC_TERMINAL_STATUS);
|
||||
@@ -561,6 +593,9 @@ PqToolConfig readConfig(HWND hwnd) {
|
||||
cfg.topicLOG = getText(hwnd, IDC_TOPIC_LOG);
|
||||
cfg.tagLOG = getText(hwnd, IDC_TAG_LOG);
|
||||
cfg.keyLOG = getText(hwnd, IDC_KEY_LOG);
|
||||
cfg.topicAsk = getText(hwnd, IDC_TOPIC_ASK);
|
||||
cfg.tagAsk = getText(hwnd, IDC_TAG_ASK);
|
||||
cfg.keyAsk = getText(hwnd, IDC_KEY_ASK);
|
||||
cfg.consumer = getText(hwnd, IDC_CONSUMER);
|
||||
cfg.consumerIpport = getText(hwnd, IDC_CONSUMER_IPPORT);
|
||||
cfg.consumerAccessKey = getText(hwnd, IDC_CONSUMER_ACCESS_KEY);
|
||||
@@ -572,6 +607,7 @@ PqToolConfig readConfig(HWND hwnd) {
|
||||
cfg.dataRealtimeTag = getText(hwnd, IDC_DATA_REALTIME_TAG);
|
||||
cfg.dataRealtimeKey = getText(hwnd, IDC_DATA_REALTIME_KEY);
|
||||
std::string frontType = selectedFrontTypeFromWindow(hwnd);
|
||||
// frontType 是统计/实时模式开关:决定本次追踪和解析关注哪个 DATATopic。
|
||||
if (frontType == "cfg_3s_data") {
|
||||
cfg.dataTopic = cfg.dataRealtimeTopic;
|
||||
cfg.dataTag = cfg.dataRealtimeTag;
|
||||
@@ -674,6 +710,7 @@ void populateTree(AppState& app) {
|
||||
app.selectedDevice = -1;
|
||||
app.selectedMonitor = -1;
|
||||
|
||||
// 台账树按进程号分组;maxProcessNum 可让没有设备的进程也显示出来。
|
||||
int maxProcess = 0;
|
||||
for (const auto& d : app.devices) {
|
||||
maxProcess = std::max(maxProcess, toInt(d.processNo));
|
||||
@@ -999,6 +1036,7 @@ void refreshPreviewWorkbookChoices(AppState& app, const std::string& selectedPat
|
||||
|
||||
void resetJsonSession(AppState& app, const std::string& traceDir) {
|
||||
app.activeTraceDir = traceDir;
|
||||
// 切换测点追踪时只清掉当前 traceDir 对应的 JSON 记录,其他测点历史还能通过下拉框找回。
|
||||
app.jsonItems.erase(std::remove_if(app.jsonItems.begin(), app.jsonItems.end(),
|
||||
[&](const JsonItem& item) {
|
||||
if (!item.traceDir.empty() && sameTraceDir(item.traceDir, traceDir)) return true;
|
||||
@@ -1084,6 +1122,7 @@ void addJsonEvent(AppState& app, const PqMqEvent& ev) {
|
||||
if (item.kind == "data") {
|
||||
item.dataType = dataTypeFromJsonText(item.body);
|
||||
if (item.dataType.empty()) item.dataType = "unknown";
|
||||
// 同一追踪目录下同一个 DATA_TYPE 保留最新一条,避免列表里堆积重复 MQ 消息。
|
||||
for (size_t i = 0; i < app.jsonItems.size(); ++i) {
|
||||
auto& existing = app.jsonItems[i];
|
||||
if (existing.kind == "data" &&
|
||||
@@ -1142,6 +1181,7 @@ std::string compactCells(const std::vector<std::string>& cells, size_t start) {
|
||||
}
|
||||
|
||||
int previewStatusFromStyle(const std::string& style) {
|
||||
// 这里把 main.cpp 写出的 Excel 样式名转换成 GUI 过滤/着色用的状态码。
|
||||
if (style == "MatchGreen") return 1;
|
||||
if (style == "MatchOrange") return 2;
|
||||
if (style == "MatchRed") return 3;
|
||||
@@ -1160,6 +1200,7 @@ std::vector<std::string> rowCells(const tinyxml2::XMLElement* row,
|
||||
for (const tinyxml2::XMLElement* cell = row ? row->FirstChildElement("Cell") : nullptr;
|
||||
cell;
|
||||
cell = cell->NextSiblingElement("Cell")) {
|
||||
// Excel XML 允许跳列,例如 <Cell ss:Index="8">;读取时必须补齐前面的空列。
|
||||
const char* indexAttr = cell->Attribute("ss:Index");
|
||||
if (indexAttr && *indexAttr) col = std::max(1, std::atoi(indexAttr));
|
||||
if ((int)cells.size() < col) {
|
||||
@@ -1205,7 +1246,7 @@ void resetPreviewColumns(HWND list, size_t colCount) {
|
||||
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);
|
||||
int baseWidth = i == 0 ? 210 : (i == 4 || i == 6 || i == 8 ? 300 : 110);
|
||||
col.cx = std::max(45, baseWidth * zoom / 100);
|
||||
col.iSubItem = static_cast<int>(i);
|
||||
ListView_InsertColumn(list, static_cast<int>(i), &col);
|
||||
@@ -1327,6 +1368,7 @@ void populatePreviewList(AppState& app, int forcedTopModelRow = -1, int forcedHo
|
||||
resetPreviewColumns(app.previewList, sheet.maxCols);
|
||||
int statusFilter = app.previewStatusFilter;
|
||||
int visibleRows = 0;
|
||||
// ListView 只显示过滤后的行,但 lParam 仍保存原始模型行号,编辑时用它回写 previewSheets。
|
||||
for (size_t i = 0; i < sheet.rows.size(); ++i) {
|
||||
const PreviewSheetRow& row = sheet.rows[i];
|
||||
if (!previewRowMatchesFilter(row, statusFilter)) continue;
|
||||
@@ -1359,6 +1401,7 @@ bool isEditablePreviewCell(const AppState& app, int rowIndex, int colIndex) {
|
||||
const auto& rows = app.previewSheets[app.activePreviewSheet].rows;
|
||||
if (rowIndex < 0 || rowIndex >= (int)rows.size()) return false;
|
||||
const PreviewSheetRow& row = rows[rowIndex];
|
||||
// 只允许改“数组基准/取值偏移量/最终值偏移/配置项”这几列,避免误改最终值或 origin 值。
|
||||
if (colIndex < 1 || colIndex > 4) return false;
|
||||
if (!(row.status == 1 || row.status == 2 || row.status == 3)) return false;
|
||||
return !row.cells.empty() && !row.cells[0].empty();
|
||||
@@ -1379,6 +1422,7 @@ void syncPreviewPhaseGroup(AppState& app, int rowIndex, int colIndex, const std:
|
||||
const PreviewSheetRow& row = sheet.rows[rowIndex];
|
||||
if (!isAbcPhase(row.phase) || row.cells.empty()) return;
|
||||
std::string baseLabel = labelWithoutPhase(row.cells[0]);
|
||||
// A/B/C 三相同一个指标通常应该保持同一配置;编辑一相时同步另外两相。
|
||||
for (auto& other : sheet.rows) {
|
||||
if (!isAbcPhase(other.phase) || other.cells.empty() || other.cells[0].empty()) continue;
|
||||
if (labelWithoutPhase(other.cells[0]) != baseLabel) continue;
|
||||
@@ -1387,6 +1431,108 @@ void syncPreviewPhaseGroup(AppState& app, int rowIndex, int colIndex, const std:
|
||||
}
|
||||
}
|
||||
|
||||
bool parsePreviewRange(const std::string& text, int& start, int& end) {
|
||||
std::smatch m;
|
||||
static const std::regex re(R"(^\s*(-?\d+)\s*-->\s*(-?\d+)\s*$)");
|
||||
if (!std::regex_match(text, m, re)) return false;
|
||||
try {
|
||||
start = std::stoi(m[1].str());
|
||||
end = std::stoi(m[2].str());
|
||||
return start <= end;
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool parsePreviewFieldRange(const std::string& field, int& start, int& end) {
|
||||
std::smatch m;
|
||||
static const std::regex re(R"(\[(-?\d+)-(-?\d+)\])");
|
||||
if (!std::regex_search(field, m, re)) return false;
|
||||
try {
|
||||
start = std::stoi(m[1].str());
|
||||
end = std::stoi(m[2].str());
|
||||
return start <= end;
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
int parsePreviewOffset(const std::string& text) {
|
||||
try {
|
||||
std::string value = trimAscii(text);
|
||||
return value.empty() ? 0 : std::stoi(value);
|
||||
} catch (...) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
std::string replacePreviewFieldRange(const std::string& field, int start, int end) {
|
||||
static const std::regex re(R"(\[(-?\d+)-(-?\d+)\])");
|
||||
return std::regex_replace(field, re,
|
||||
"[" + std::to_string(start) + "-" + std::to_string(end) + "]",
|
||||
std::regex_constants::format_first_only);
|
||||
}
|
||||
|
||||
std::string effectivePreviewField(const std::string& field) {
|
||||
size_t arrow = field.rfind(" >> ");
|
||||
return arrow == std::string::npos ? field : field.substr(arrow + 4);
|
||||
}
|
||||
|
||||
void recalculatePreviewEditEffects(PreviewSheet& sheet) {
|
||||
for (auto& row : sheet.rows) {
|
||||
row.finalRangeChanged = false;
|
||||
row.modifiedOriginPickColumn = -1;
|
||||
// 字段列不可直接编辑,因此每次从工作簿原值重新计算,恢复参数时可无损恢复。
|
||||
if (!row.cells.empty() && !row.cells[0].empty() && row.originalCells.size() > 8) {
|
||||
if (row.cells.size() <= 8) row.cells.resize(9);
|
||||
row.cells[8] = row.originalCells[8];
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < sheet.rows.size(); ++i) {
|
||||
PreviewSheetRow& row = sheet.rows[i];
|
||||
if (!(row.status == 1 || row.status == 2 || row.status == 3)) continue;
|
||||
if (row.cells.empty() || row.cells[0].empty() || row.cells.size() <= 8 || row.originalCells.size() <= 8) continue;
|
||||
|
||||
int oldBaseStart = 0, oldBaseEnd = 0, newBaseStart = 0, newBaseEnd = 0;
|
||||
bool oldBaseOk = row.originalCells.size() > 1 && parsePreviewRange(row.originalCells[1], oldBaseStart, oldBaseEnd);
|
||||
bool newBaseOk = row.cells.size() > 1 && parsePreviewRange(row.cells[1], newBaseStart, newBaseEnd);
|
||||
int oldValueOffset = row.originalCells.size() > 2 ? parsePreviewOffset(row.originalCells[2]) : 0;
|
||||
int newValueOffset = row.cells.size() > 2 ? parsePreviewOffset(row.cells[2]) : 0;
|
||||
int oldFinalOffset = row.originalCells.size() > 3 ? parsePreviewOffset(row.originalCells[3]) : 0;
|
||||
int newFinalOffset = row.cells.size() > 3 ? parsePreviewOffset(row.cells[3]) : 0;
|
||||
|
||||
int oldFieldStart = 0, oldFieldEnd = 0;
|
||||
if (oldBaseOk && newBaseOk && parsePreviewFieldRange(row.originalCells[8], oldFieldStart, oldFieldEnd)) {
|
||||
int newFieldStart = oldFieldStart + (newBaseStart - oldBaseStart) + (newFinalOffset - oldFinalOffset);
|
||||
int newFieldEnd = oldFieldEnd + (newBaseEnd - oldBaseEnd) + (newFinalOffset - oldFinalOffset);
|
||||
std::string newField = replacePreviewFieldRange(row.originalCells[8], newFieldStart, newFieldEnd);
|
||||
if (newField != row.originalCells[8]) {
|
||||
row.cells[8] = row.originalCells[8] + " >> " + newField;
|
||||
row.finalRangeChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!oldBaseOk || !newBaseOk || i + 1 >= sheet.rows.size()) continue;
|
||||
PreviewSheetRow& originRow = sheet.rows[i + 1];
|
||||
if (originRow.cells.size() <= 8 || originRow.cells[7] != "原始值") continue;
|
||||
int originalBlueColumn = -1;
|
||||
for (size_t col = 9; col < originRow.cellStyles.size(); ++col) {
|
||||
if (originRow.cellStyles[col] == "OriginPickBlue") {
|
||||
originalBlueColumn = static_cast<int>(col);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (originalBlueColumn < 0) continue;
|
||||
int oldPickStart = oldBaseStart + oldValueOffset;
|
||||
int newPickStart = newBaseStart + newValueOffset;
|
||||
int purpleColumn = originalBlueColumn + (newPickStart - oldPickStart);
|
||||
if (newPickStart != oldPickStart && purpleColumn >= 9 && purpleColumn < (int)originRow.cells.size()) {
|
||||
originRow.modifiedOriginPickColumn = purpleColumn;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void commitPreviewEdit(AppState& app, bool save) {
|
||||
HWND edit = app.previewEdit;
|
||||
if (!edit) return;
|
||||
@@ -1411,6 +1557,7 @@ void commitPreviewEdit(AppState& app, bool save) {
|
||||
if ((int)row.cells.size() <= colIndex) row.cells.resize(colIndex + 1);
|
||||
row.cells[colIndex] = value;
|
||||
syncPreviewPhaseGroup(app, rowIndex, colIndex, value);
|
||||
recalculatePreviewEditEffects(app.previewSheets[app.activePreviewSheet]);
|
||||
app.reverseXmlDirty = true;
|
||||
}
|
||||
|
||||
@@ -1473,6 +1620,7 @@ void refreshPreviewFromWorkbook(AppState& app, const std::string& workbookPath)
|
||||
return;
|
||||
}
|
||||
|
||||
// 工作簿是 Excel 2003 XML 格式,不需要 COM/Excel,直接用 tinyxml2 读 Worksheet/Row/Cell。
|
||||
tinyxml2::XMLDocument doc;
|
||||
std::string xml = readAllText(fs::u8path(workbookPath));
|
||||
if (doc.Parse(xml.c_str(), xml.size()) != tinyxml2::XML_SUCCESS) {
|
||||
@@ -1803,8 +1951,11 @@ void showPage(AppState& app, int index) {
|
||||
refreshPreviewFromWorkbook(app, selected);
|
||||
}
|
||||
} else if (index == 4) {
|
||||
if (app.reverseXmlDirty || app.generatedXmlText.empty()) generateReverseXml(app, false);
|
||||
updateXmlPreviewText(app);
|
||||
// XML 有人工修改时不自动逆生成,避免仅切换页面就静默覆盖用户内容。
|
||||
if (!app.xmlPreviewEdited) {
|
||||
if (app.reverseXmlDirty || app.generatedXmlText.empty()) generateReverseXml(app, false);
|
||||
else updateXmlPreviewText(app);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1818,6 +1969,7 @@ void runGetLedger(AppState& app) {
|
||||
PqToolConfig cfg = readConfig(app.hwnd);
|
||||
appendLogW(app, L"开始获取台账和 ICD/XML...");
|
||||
std::string firstXml;
|
||||
// 输出 devices 会刷新左侧树;firstXml 会自动填到 XML 路径,减少手动选择。
|
||||
pqFetchLedgerAndIcd(cfg, app.devices, firstXml, [&](const std::string& s) { appendLog(app, s); });
|
||||
populateTree(app);
|
||||
updateDetails(app);
|
||||
@@ -1851,16 +2003,17 @@ void runTrace(AppState& app) {
|
||||
const auto& d = app.devices[app.selectedDevice];
|
||||
const auto& m = d.monitors[app.selectedMonitor];
|
||||
std::string traceDir = pqTraceDirectoryFor(cfg, d, m);
|
||||
// pqSendTraceRequest 会在 MQ 互斥锁内建立唯一活动会话并清空旧记录,
|
||||
// 从这一刻开始只接收 Broker 存储时间不早于本次点击的消息。
|
||||
resetJsonSession(app, traceDir);
|
||||
pqClearTraceRecords(cfg, d, m);
|
||||
appendLog(app, "已清空当前测点旧文本记录: " + traceDir);
|
||||
pqSendTraceRequest(cfg, d, m, selectedFrontType(app),
|
||||
[&](const std::string& s) { appendLog(app, s); },
|
||||
makeMqEventCallback(app));
|
||||
appendLog(app, "已建立本次追踪消息时间边界并清空旧记录: " + traceDir);
|
||||
prepareTraceWorkspace(app, cfg, d, m, true);
|
||||
app.autoPreviewEnabled = true;
|
||||
selectPage(app, 3);
|
||||
autoParseTraceDir(app, traceDir, "trace-start");
|
||||
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());
|
||||
}
|
||||
@@ -1871,6 +2024,7 @@ void runParse(AppState& app) {
|
||||
std::string err;
|
||||
appendLogW(app, L"开始解析生成表格...");
|
||||
std::vector<std::string> jsonPaths = cfg.jsonDataPaths.empty() ? splitPathList(cfg.jsonDataPath) : cfg.jsonDataPaths;
|
||||
// 一键解析是手动路径模式:用界面上的 XML/jsondata/origin/output 调用核心解析器。
|
||||
bool ok = pqRunParser(cfg.xmlPath, jsonPaths, cfg.originPaths, cfg.outputPath, &err, selectedFrontType(app));
|
||||
if (ok) {
|
||||
refreshPreviewWorkbookChoices(app, cfg.outputPath);
|
||||
@@ -1971,6 +2125,7 @@ std::string reverseNameFromField(const std::string& field, const std::string& ar
|
||||
std::smatch m;
|
||||
static const std::regex rangeRe(R"(^(.*)\[(\d+)-(\d+)\](.*)$)");
|
||||
if (std::regex_match(name, m, rangeRe)) {
|
||||
// 表格里序列显示为 [start-end],写回 XML 时恢复为 %start,end%。
|
||||
std::string start = m[2].str();
|
||||
std::string end = m[3].str();
|
||||
static const std::regex baseRe(R"(^\s*(-?\d+)\s*-->\s*(-?\d+)\s*$)");
|
||||
@@ -2014,6 +2169,7 @@ std::string placeholderForValueOffset(const std::string& offsetText) {
|
||||
std::string reverseDaForXml(std::string daPath, const std::string& valueOffset) {
|
||||
if (valueOffset.empty()) return daPath;
|
||||
static const std::regex rangeRe(R"(\[\d+-\d+\])");
|
||||
// 表格展示的 [start-end] 在 XML 中是 [%offset] 占位符,按“取值偏移量”恢复。
|
||||
return std::regex_replace(daPath, rangeRe, "[" + placeholderForValueOffset(valueOffset) + "]",
|
||||
std::regex_constants::format_first_only);
|
||||
}
|
||||
@@ -2035,6 +2191,7 @@ 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;
|
||||
@@ -2049,7 +2206,7 @@ std::vector<ReverseRowEdit> collectReverseRows(const AppState& app) {
|
||||
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);
|
||||
edit.name = reverseNameFromField(effectivePreviewField(row.cells[8]), edit.arrayBase);
|
||||
if (!edit.name.empty() || !edit.label.empty()) rows.push_back(std::move(edit));
|
||||
}
|
||||
std::stable_sort(rows.begin(), rows.end(), [](const ReverseRowEdit& a, const ReverseRowEdit& b) {
|
||||
@@ -2092,6 +2249,7 @@ bool reverseNodeMatches(const ValueNodeRef& node, const ReverseSheetSpec& spec,
|
||||
if (node.topic != spec.topic || node.dataType != spec.dataType || !reverseWiringAllowed(node.wiring, spec.wiring)) return false;
|
||||
std::string nodeDo = xmlAttr(node.element, "DO");
|
||||
std::string nodeDa = xmlAttr(node.element, "DA");
|
||||
// 优先用原始 DO/DA 精确定位;如果用户把 DO/DA 改了,也能找到原 XML 节点。
|
||||
if (!edit.originalDoPath.empty() && !edit.originalDaPath.empty() &&
|
||||
nodeDo == edit.originalDoPath && nodeDa == edit.originalDaPath) {
|
||||
return true;
|
||||
@@ -2121,6 +2279,7 @@ std::string generateReverseXml(AppState& app, bool showMessage) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// 当前 Sheet 名决定要修改 XML 中哪个 Topic/DataType/接线分支。
|
||||
ReverseSheetSpec spec = reverseSpecForSheet(app.previewSheets[app.activePreviewSheet].name);
|
||||
std::vector<ReverseRowEdit> edits = collectReverseRows(app);
|
||||
std::vector<ValueNodeRef> nodes;
|
||||
@@ -2156,12 +2315,28 @@ std::string generateReverseXml(AppState& app, bool showMessage) {
|
||||
}
|
||||
|
||||
void runReverseXml(AppState& app) {
|
||||
if (app.xmlPreviewEdited) {
|
||||
int choice = MessageBoxW(app.hwnd,
|
||||
L"XML预览此前有过手动修改。继续逆生成会覆盖这些人工修改,是否继续?",
|
||||
L"确认覆盖 XML 人工修改",
|
||||
MB_YESNO | MB_ICONWARNING | MB_DEFBUTTON2);
|
||||
if (choice != IDYES) return;
|
||||
}
|
||||
if (!generateReverseXml(app, true).empty()) selectPage(app, 4);
|
||||
}
|
||||
|
||||
void runExportXml(AppState& app) {
|
||||
if (app.xmlPreviewEdited) syncXmlPreviewFromEditor(app);
|
||||
if (app.generatedXmlText.empty() || app.reverseXmlDirty) generateReverseXml(app, false);
|
||||
if (app.generatedXmlText.empty()) return;
|
||||
|
||||
tinyxml2::XMLDocument check;
|
||||
if (check.Parse(app.generatedXmlText.c_str(), app.generatedXmlText.size()) != tinyxml2::XML_SUCCESS) {
|
||||
MessageBoxW(app.hwnd,
|
||||
u8w(std::string("当前编辑内容不是合法 XML:") + check.ErrorStr()).c_str(),
|
||||
L"导出 XML 失败", MB_ICONERROR);
|
||||
return;
|
||||
}
|
||||
std::string out;
|
||||
if (!saveFileDialog(app.hwnd, L"\x5BFC\x51FAXML", L"XML Files\0*.xml;*.icd\0All Files\0*.*\0", L"xml", out)) return;
|
||||
std::ofstream ofs(fs::u8path(out), std::ios::binary);
|
||||
@@ -2181,6 +2356,7 @@ void autoParseTraceDir(AppState& app, const std::string& traceDir, const char* r
|
||||
std::string outputPath = (dir / "final_sorted.xls").u8string();
|
||||
std::string frontType = selectedFrontType(app);
|
||||
|
||||
// trace_context.json 是 pqSendTraceRequest() 写下的上下文,自动解析优先用它定位 XML 和输出路径。
|
||||
fs::path contextPath = dir / "trace_context.json";
|
||||
if (fs::exists(contextPath)) {
|
||||
try {
|
||||
@@ -2279,21 +2455,25 @@ void createConfigPage(AppState& app) {
|
||||
addLabeledEdit(app, p, L"TopicLOG", IDC_TOPIC_LOG, cfg.topicLOG, 120, 398 + rowGap * 2, 90, 360);
|
||||
addLabeledEdit(app, p, L"TagLOG", IDC_TAG_LOG, cfg.tagLOG, 575, 398 + rowGap * 2, 75, 270);
|
||||
addLabeledEdit(app, p, L"KeyLOG", IDC_KEY_LOG, cfg.keyLOG, 940, 398 + rowGap * 2, 70, 120);
|
||||
addLabeledEdit(app, p, L"TopicAsk", IDC_TOPIC_ASK, cfg.topicAsk, 120, 398 + rowGap * 3, 90, 360);
|
||||
addLabeledEdit(app, p, L"TagAsk", IDC_TAG_ASK, cfg.tagAsk, 575, 398 + rowGap * 3, 75, 270);
|
||||
addLabeledEdit(app, p, L"KeyAsk", IDC_KEY_ASK, cfg.keyAsk, 940, 398 + rowGap * 3, 70, 120);
|
||||
|
||||
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);
|
||||
const int consumerY = 526;
|
||||
addLabel(app, p, L"消费者", x1, consumerY, 80, 22);
|
||||
addLabeledEdit(app, p, L"consumer", IDC_CONSUMER, cfg.consumer, 120, consumerY, 90, 300);
|
||||
addLabeledEdit(app, p, L"Ipport", IDC_CONSUMER_IPPORT, cfg.consumerIpport, 600, consumerY, 80, 260);
|
||||
addLabeledEdit(app, p, L"AccessKey", IDC_CONSUMER_ACCESS_KEY, cfg.consumerAccessKey, 120, consumerY + rowGap, 90, 300);
|
||||
addLabeledEdit(app, p, L"SecretKey", IDC_CONSUMER_SECRET_KEY, cfg.consumerSecretKey, 600, consumerY + rowGap, 80, 260);
|
||||
addLabeledEdit(app, p, L"\x7EDF\x8BA1Topic", IDC_DATA_STAT_TOPIC, cfg.dataStatTopic, 120, consumerY + rowGap * 2, 90, 360);
|
||||
addLabeledEdit(app, p, L"DATATag", IDC_DATA_STAT_TAG, cfg.dataStatTag, 575, consumerY + rowGap * 2, 75, 270);
|
||||
addLabeledEdit(app, p, L"DATAKey", IDC_DATA_STAT_KEY, cfg.dataStatKey, 940, consumerY + rowGap * 2, 70, 120);
|
||||
addLabeledEdit(app, p, L"\x5B9E\x65F6Topic", IDC_DATA_REALTIME_TOPIC, cfg.dataRealtimeTopic, 120, consumerY + rowGap * 3, 90, 360);
|
||||
addLabeledEdit(app, p, L"DATATag", IDC_DATA_REALTIME_TAG, cfg.dataRealtimeTag, 575, consumerY + rowGap * 3, 75, 270);
|
||||
addLabeledEdit(app, p, L"DATAKey", IDC_DATA_REALTIME_KEY, cfg.dataRealtimeKey, 940, consumerY + rowGap * 3, 70, 120);
|
||||
addLabeledEdit(app, p, L"TraceTopic", IDC_TRACE_TOPIC, cfg.traceTopic, 120, consumerY + rowGap * 4, 90, 360);
|
||||
addLabeledEdit(app, p, L"TraceTag", IDC_TRACE_TAG, cfg.traceTag, 575, consumerY + rowGap * 4, 75, 270);
|
||||
addLabeledEdit(app, p, L"TraceKey", IDC_TRACE_KEY, cfg.traceKey, 940, consumerY + rowGap * 4, 70, 120);
|
||||
}
|
||||
|
||||
void createLedgerPage(AppState& app) {
|
||||
@@ -2429,7 +2609,7 @@ void createXmlPreviewPage(AppState& app) {
|
||||
app.xmlSearch = addEdit(app, p, IDC_XML_SEARCH, "", 230, 12, 360, 28);
|
||||
app.xmlPreview = CreateWindowExW(WS_EX_CLIENTEDGE, L"EDIT", L"",
|
||||
WS_CHILD | WS_VISIBLE | WS_TABSTOP | ES_MULTILINE |
|
||||
ES_AUTOVSCROLL | ES_AUTOHSCROLL | ES_NOHIDESEL |
|
||||
ES_AUTOVSCROLL | ES_AUTOHSCROLL | ES_NOHIDESEL | ES_WANTRETURN |
|
||||
WS_VSCROLL | WS_HSCROLL,
|
||||
18, 50, 1115, 585, p,
|
||||
(HMENU)(INT_PTR)IDC_XML_PREVIEW_EDIT, app.inst, nullptr);
|
||||
@@ -2437,7 +2617,7 @@ void createXmlPreviewPage(AppState& app) {
|
||||
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);
|
||||
SendMessageW(app.xmlPreview, EM_SETLIMITTEXT, 0x7FFFFFFE, 0);
|
||||
}
|
||||
|
||||
void createConfigPageClean(AppState& app) {
|
||||
@@ -2472,21 +2652,25 @@ void createConfigPageClean(AppState& app) {
|
||||
addLabeledEdit(app, p, L"TopicLOG", IDC_TOPIC_LOG, cfg.topicLOG, mqFieldX, 398 + rowGap * 2, 90, mqTopicW);
|
||||
addLabeledEdit(app, p, L"TagLOG", IDC_TAG_LOG, cfg.tagLOG, mqTagX, 398 + rowGap * 2, 75, mqTagW);
|
||||
addLabeledEdit(app, p, L"KeyLOG", IDC_KEY_LOG, cfg.keyLOG, mqKeyX, 398 + rowGap * 2, 70, mqKeyW);
|
||||
addLabeledEdit(app, p, L"TopicAsk", IDC_TOPIC_ASK, cfg.topicAsk, mqFieldX, 398 + rowGap * 3, 90, mqTopicW);
|
||||
addLabeledEdit(app, p, L"TagAsk", IDC_TAG_ASK, cfg.tagAsk, mqTagX, 398 + rowGap * 3, 75, mqTagW);
|
||||
addLabeledEdit(app, p, L"KeyAsk", IDC_KEY_ASK, cfg.keyAsk, mqKeyX, 398 + rowGap * 3, 70, mqKeyW);
|
||||
|
||||
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);
|
||||
const int consumerY = 526;
|
||||
addLabel(app, p, L"\x6D88\x8D39\x8005", mqNameX, consumerY, 90, 22);
|
||||
addLabeledEdit(app, p, L"consumer", IDC_CONSUMER, cfg.consumer, mqFieldX, consumerY, 90, 300);
|
||||
addLabeledEdit(app, p, L"Ipport", IDC_CONSUMER_IPPORT, cfg.consumerIpport, mqTagX, consumerY, 80, 260);
|
||||
addLabeledEdit(app, p, L"AccessKey", IDC_CONSUMER_ACCESS_KEY, cfg.consumerAccessKey, mqFieldX, consumerY + rowGap, 90, 300);
|
||||
addLabeledEdit(app, p, L"SecretKey", IDC_CONSUMER_SECRET_KEY, cfg.consumerSecretKey, mqTagX, consumerY + rowGap, 80, 260);
|
||||
addLabeledEdit(app, p, L"\x7EDF\x8BA1Topic", IDC_DATA_STAT_TOPIC, cfg.dataStatTopic, mqFieldX, consumerY + rowGap * 2, 90, mqTopicW);
|
||||
addLabeledEdit(app, p, L"DATATag", IDC_DATA_STAT_TAG, cfg.dataStatTag, mqTagX, consumerY + rowGap * 2, 75, mqTagW);
|
||||
addLabeledEdit(app, p, L"DATAKey", IDC_DATA_STAT_KEY, cfg.dataStatKey, mqKeyX, consumerY + rowGap * 2, 70, mqKeyW);
|
||||
addLabeledEdit(app, p, L"\x5B9E\x65F6Topic", IDC_DATA_REALTIME_TOPIC, cfg.dataRealtimeTopic, mqFieldX, consumerY + rowGap * 3, 90, mqTopicW);
|
||||
addLabeledEdit(app, p, L"DATATag", IDC_DATA_REALTIME_TAG, cfg.dataRealtimeTag, mqTagX, consumerY + rowGap * 3, 75, mqTagW);
|
||||
addLabeledEdit(app, p, L"DATAKey", IDC_DATA_REALTIME_KEY, cfg.dataRealtimeKey, mqKeyX, consumerY + rowGap * 3, 70, mqKeyW);
|
||||
addLabeledEdit(app, p, L"TraceTopic", IDC_TRACE_TOPIC, cfg.traceTopic, mqFieldX, consumerY + rowGap * 4, 90, mqTopicW);
|
||||
addLabeledEdit(app, p, L"TraceTag", IDC_TRACE_TAG, cfg.traceTag, mqTagX, consumerY + rowGap * 4, 75, mqTagW);
|
||||
addLabeledEdit(app, p, L"TraceKey", IDC_TRACE_KEY, cfg.traceKey, mqKeyX, consumerY + rowGap * 4, 70, mqKeyW);
|
||||
}
|
||||
|
||||
void createLedgerPageClean(AppState& app) {
|
||||
@@ -2534,6 +2718,7 @@ void createLedgerPageClean(AppState& app) {
|
||||
}
|
||||
|
||||
void createUi(AppState& app) {
|
||||
// 这里手写 Win32 控件,没有资源文件;所有页面尺寸和控件布局都在 create*Page 函数中生成。
|
||||
app.font = CreateFontW(-16, 0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE,
|
||||
DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS,
|
||||
CLEARTYPE_QUALITY, DEFAULT_PITCH | FF_SWISS, L"Microsoft YaHei UI");
|
||||
@@ -2583,6 +2768,7 @@ LRESULT CALLBACK wndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
|
||||
AppState& app = *g_app;
|
||||
switch (msg) {
|
||||
case WM_PQ_MQ_EVENT: {
|
||||
// RocketMQ 回调线程不能直接操作窗口,mq.cpp 通过 PostMessage 把事件切回 GUI 线程。
|
||||
std::unique_ptr<PqMqEvent> ev(reinterpret_cast<PqMqEvent*>(lp));
|
||||
if (ev) addJsonEvent(app, *ev);
|
||||
return 0;
|
||||
@@ -2595,6 +2781,7 @@ LRESULT CALLBACK wndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
|
||||
createUi(app);
|
||||
return 0;
|
||||
case WM_COMMAND:
|
||||
// WM_COMMAND 处理按钮、编辑框、下拉框;每个控件 ID 对应上面的 ControlId。
|
||||
if (LOWORD(wp) == IDC_PREVIEW_FILTER && HIWORD(wp) == CBN_SELCHANGE) {
|
||||
int idx = (int)SendMessageW(app.previewFilter, CB_GETCURSEL, 0, 0);
|
||||
int filters[] = {-1, 1, 2, 3, 0};
|
||||
@@ -2621,9 +2808,17 @@ LRESULT CALLBACK wndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
|
||||
highlightXmlSearch(app, false, false);
|
||||
return 0;
|
||||
}
|
||||
if (LOWORD(wp) == IDC_XML_PREVIEW_EDIT && HIWORD(wp) == EN_CHANGE) {
|
||||
if (!app.updatingXmlPreview) {
|
||||
syncXmlPreviewFromEditor(app);
|
||||
app.xmlSearchLastText.clear();
|
||||
app.xmlSearchNextPos = 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (LOWORD(wp) == IDC_XML_SEARCH_BUTTON && HIWORD(wp) == BN_CLICKED) {
|
||||
app.xmlSearchText = getText(hwnd, IDC_XML_SEARCH);
|
||||
if (app.reverseXmlDirty || app.generatedXmlText.empty()) {
|
||||
if (!app.xmlPreviewEdited && (app.reverseXmlDirty || app.generatedXmlText.empty())) {
|
||||
generateReverseXml(app, false);
|
||||
app.xmlSearchLastText.clear();
|
||||
app.xmlSearchNextPos = 0;
|
||||
@@ -2726,6 +2921,7 @@ LRESULT CALLBACK wndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
|
||||
}
|
||||
break;
|
||||
case WM_NOTIFY:
|
||||
// WM_NOTIFY 处理 TreeView/ListView/TabControl 的选择、双击、自绘等通知。
|
||||
if (((LPNMHDR)lp)->idFrom == IDC_PREVIEW_LIST && ((LPNMHDR)lp)->code == NM_DBLCLK) {
|
||||
auto* item = reinterpret_cast<NMITEMACTIVATE*>(lp);
|
||||
if (item->iItem >= 0 && item->iSubItem >= 0) {
|
||||
@@ -2743,17 +2939,26 @@ LRESULT CALLBACK wndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
|
||||
size_t idx = static_cast<size_t>(cd->nmcd.lItemlParam);
|
||||
int status = 0;
|
||||
std::string cellStyle;
|
||||
bool finalRangeChanged = false;
|
||||
int modifiedOriginPickColumn = -1;
|
||||
if (!app.previewSheets.empty() &&
|
||||
app.activePreviewSheet >= 0 &&
|
||||
app.activePreviewSheet < (int)app.previewSheets.size() &&
|
||||
idx < app.previewSheets[app.activePreviewSheet].rows.size()) {
|
||||
const PreviewSheetRow& row = app.previewSheets[app.activePreviewSheet].rows[idx];
|
||||
status = row.status;
|
||||
finalRangeChanged = row.finalRangeChanged;
|
||||
modifiedOriginPickColumn = row.modifiedOriginPickColumn;
|
||||
if (cd->iSubItem >= 0 && cd->iSubItem < (int)row.cellStyles.size()) {
|
||||
cellStyle = row.cellStyles[cd->iSubItem];
|
||||
}
|
||||
}
|
||||
if (cellStyle == "OriginPickBlue") cd->clrTextBk = RGB(207, 226, 243);
|
||||
if (finalRangeChanged && cd->iSubItem == 8) {
|
||||
cd->clrTextBk = RGB(153, 0, 0);
|
||||
cd->clrText = RGB(255, 255, 255);
|
||||
}
|
||||
else if (modifiedOriginPickColumn == cd->iSubItem) cd->clrTextBk = RGB(180, 167, 214);
|
||||
else if (cellStyle == "OriginPickBlue") cd->clrTextBk = RGB(207, 226, 243);
|
||||
else if (status == 1) cd->clrTextBk = RGB(217, 234, 211);
|
||||
else if (status == 2) cd->clrTextBk = RGB(252, 228, 214);
|
||||
else if (status == 3) cd->clrTextBk = RGB(244, 204, 204);
|
||||
@@ -2802,6 +3007,7 @@ int runPqGuiApp() {
|
||||
AppState app;
|
||||
g_app = &app;
|
||||
app.inst = GetModuleHandleW(nullptr);
|
||||
// COM 初始化主要服务于系统组件/对话框;主流程仍是普通 Win32 消息循环。
|
||||
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
|
||||
|
||||
INITCOMMONCONTROLSEX icc{};
|
||||
@@ -2836,6 +3042,7 @@ int runPqGuiApp() {
|
||||
ShowWindow(hwnd, SW_SHOW);
|
||||
UpdateWindow(hwnd);
|
||||
|
||||
// 标准消息循环:用户操作 -> wndProc -> run* 业务函数 -> 刷新 AppState/控件。
|
||||
MSG message;
|
||||
while (GetMessageW(&message, nullptr, 0, 0) > 0) {
|
||||
TranslateMessage(&message);
|
||||
|
||||
Reference in New Issue
Block a user