From 6c6ba205ba973a911bb9bb1fc2ffbf5ee03d6aa4 Mon Sep 17 00:00:00 2001 From: lvjunjie Date: Tue, 13 Oct 2026 16:59:23 +0800 Subject: [PATCH] =?UTF-8?q?feat(nmNum):=20=E5=AE=8C=E5=96=84=E6=95=8F?= =?UTF-8?q?=E6=84=9F=E6=80=A7=E5=88=86=E6=9E=90=E6=9B=B2=E7=BA=BF=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=E4=B8=8E=E9=80=89=E6=8B=A9=E6=8E=A7=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 敏感性分析曲线统一使用实线,叠加并保存原模型数值解,沿用普通双对数图样式。 - 结果列表移除状态与颜色显示,支持应用、确定和取消,应用后保持窗口打开。 - 刷新绘图时移除隐藏及上一批残留曲线,修复取消勾选后曲线仍显示的问题。 --- Include/nmNum/nmData/nmDataSensitive.h | 1 + Include/nmNum/nmSubWxs/nmWxSensitive.h | 4 +- .../nmCalculationDllPebiSolverTask.cpp | 2 + Src/nmNum/nmData/nmDataSensitive.cpp | 2 + Src/nmNum/nmSubWnd/nmSubWndMain.cpp | 29 +++++- Src/nmNum/nmSubWxs/nmWxSensitive.cpp | 95 +++++++++++++------ 6 files changed, 98 insertions(+), 35 deletions(-) diff --git a/Include/nmNum/nmData/nmDataSensitive.h b/Include/nmNum/nmData/nmDataSensitive.h index 5d3b1eef..dd1b195b 100644 --- a/Include/nmNum/nmData/nmDataSensitive.h +++ b/Include/nmNum/nmData/nmDataSensitive.h @@ -120,6 +120,7 @@ public: QString m_sAxisYName; QString m_sAxisYUnit; QVector > m_vecHistoryLogLog; + QVector > m_vecBaseLogLog; // 批次启动时原模型已有的数值解。 std::vector m_runs; bool m_bCompleted; bool m_bStopped; diff --git a/Include/nmNum/nmSubWxs/nmWxSensitive.h b/Include/nmNum/nmSubWxs/nmWxSensitive.h index 75c52b2c..31722655 100644 --- a/Include/nmNum/nmSubWxs/nmWxSensitive.h +++ b/Include/nmNum/nmSubWxs/nmWxSensitive.h @@ -60,7 +60,8 @@ protected: void onSensitivityFinished(); void onResultsClicked(); void onResultsChanged(); - void onResultsVisibilityChanged(); + void onApplyResults(); + void onAcceptResults(); void onShowAllResults(); void onHideAllResults(); @@ -91,7 +92,6 @@ private: QPushButton *resultsButton; QPointer m_resultsDialog; QPointer m_resultsModel; - bool m_refreshingResults; // 各个变量页面的控件列表 QList m_modeCombos; diff --git a/Src/nmNum/nmCalculation/nmCalculationDllPebiSolverTask.cpp b/Src/nmNum/nmCalculation/nmCalculationDllPebiSolverTask.cpp index a916bde6..98d0c21b 100644 --- a/Src/nmNum/nmCalculation/nmCalculationDllPebiSolverTask.cpp +++ b/Src/nmNum/nmCalculation/nmCalculationDllPebiSolverTask.cpp @@ -920,6 +920,8 @@ nmCalculationDllPebiSolverTask* nmCalculationDllPebiSolverTask::createSensitivit if(well != nullptr) { task->m_pSensitivityResult->m_sWellName = well->getWellName(); task->m_pSensitivityResult->m_vecHistoryLogLog = well->getHistoryLogLog(); + // 复用原模型已有数值解作为对比基准,不增加一次求解或跟随后续结果变化。 + task->m_pSensitivityResult->m_vecBaseLogLog = well->getResultLogLog(); } } return task.take(); diff --git a/Src/nmNum/nmData/nmDataSensitive.cpp b/Src/nmNum/nmData/nmDataSensitive.cpp index fa779ba0..b9b4fdaa 100644 --- a/Src/nmNum/nmData/nmDataSensitive.cpp +++ b/Src/nmNum/nmData/nmDataSensitive.cpp @@ -86,6 +86,7 @@ rapidjson::Value nmDataSensitive::BatchResult::ToJsonValue( value.AddMember("Stopped", m_bStopped, allocator); value.AddMember("FailureReason", rapidjson::Value(m_sFailureReason.toUtf8().constData(), allocator).Move(), allocator); value.AddMember("HistoryLogLog", sensitiveCurvesToJson(m_vecHistoryLogLog, allocator), allocator); + value.AddMember("BaseLogLog", sensitiveCurvesToJson(m_vecBaseLogLog, allocator), allocator); rapidjson::Value variables(rapidjson::kArrayType); for(int i = 0; i < m_variables.size(); ++i) variables.PushBack(m_variables[i].ToJsonValue(allocator), allocator); @@ -127,6 +128,7 @@ void nmDataSensitive::BatchResult::FromJsonValue(const rapidjson::Value& value) m_bCompleted = value.HasMember("Completed") && value["Completed"].IsBool() && value["Completed"].GetBool(); m_bStopped = value.HasMember("Stopped") && value["Stopped"].IsBool() && value["Stopped"].GetBool(); m_vecHistoryLogLog = sensitiveCurvesFromJson(value, "HistoryLogLog"); + m_vecBaseLogLog = sensitiveCurvesFromJson(value, "BaseLogLog"); if(value.HasMember("Variables") && value["Variables"].IsArray()) { const rapidjson::Value& variables = value["Variables"]; for(rapidjson::SizeType i = 0; i < variables.Size(); ++i) { diff --git a/Src/nmNum/nmSubWnd/nmSubWndMain.cpp b/Src/nmNum/nmSubWnd/nmSubWndMain.cpp index cd7fc061..8d420447 100644 --- a/Src/nmNum/nmSubWnd/nmSubWndMain.cpp +++ b/Src/nmNum/nmSubWnd/nmSubWndMain.cpp @@ -1897,15 +1897,27 @@ void nmSubWndMain::onShowSensitiveResults() data[0].size() != data[2].size() || data[0].isEmpty()) continue; QVector runCurves; if(!initloglog(data, runCurves, false)) continue; - // 每组使用稳定名称和颜色,压力实线、导数虚线;组间不会覆盖同名曲线。 + // 每组压力与导数均使用实线,稳定名称和颜色避免组间曲线覆盖。 const QColor color = QColor::fromHsv(int((run.m_nRunIndex % 360) * 137 % 360), 190, 190); runCurves[0].m_sCurveName = nmWxSensitive::tr("Run %1 - Pressure").arg(run.m_nRunIndex + 1); runCurves[1].m_sCurveName = nmWxSensitive::tr("Run %1 - Derivative").arg(run.m_nRunIndex + 1); runCurves[0].m_oPen.setColor(color); runCurves[1].m_oPen.setColor(color); - runCurves[1].m_oPen.setStyle(Qt::DashLine); + runCurves[0].m_oPen.setStyle(Qt::SolidLine); + runCurves[1].m_oPen.setStyle(Qt::SolidLine); curves += runCurves; } + // 原模型数值解沿用普通双对数图的压力、导数样式,仅区分曲线名称。 + QVector > base = result->m_vecBaseLogLog; + if(base.size() == 3 && base[0].size() == base[1].size() && + base[0].size() == base[2].size() && !base[0].isEmpty()) { + QVector baseCurves; + if(initloglog(base, baseCurves, false)) { + baseCurves[0].m_sCurveName = nmWxSensitive::tr("Base model - Pressure"); + baseCurves[1].m_sCurveName = nmWxSensitive::tr("Base model - Derivative"); + curves += baseCurves; + } + } // 对数坐标只接受有限正数,分别过滤压力与导数,保持各自时间和值对应。 for(int i = 0; i < curves.size(); ++i) { QVector x, y; @@ -1925,6 +1937,19 @@ void nmSubWndMain::onShowSensitiveResults() plot->setAxisXYLog(true, 1); if(!result->m_sAxisXName.isEmpty()) plot->setAxisX(result->m_sAxisXName, result->m_sAxisXUnit); if(!result->m_sAxisYName.isEmpty()) plot->setAxisY(result->m_sAxisYName, result->m_sAxisYUnit); + // 框架刷新只创建或更新传入曲线,不删除缺席曲线,先移除已隐藏或上一批残留的图元。 + QSet visibleNames; + for(int i = 0; i < curves.size(); ++i) visibleNames.insert(curves[i].m_sCurveName); + bool removed = false; + for(int i = plot->m_pPlot->getObjCount() - 1; i >= 0; --i) { + ZxObjCurve* curve = qobject_cast(plot->m_pPlot->getObjByIndex(i)); + if(curve && !visibleNames.contains(curve->getName())) { + plot->m_pPlot->removeObjByIndex(i); + removed = true; + } + } + // 清理删除曲线相关的撤销记录,避免后续操作恢复已经隐藏的图元。 + if(removed) plot->clearUndoStack(); plot->setCurveData(curves); plot->runUpdate(); plot->setReadOnly(true); diff --git a/Src/nmNum/nmSubWxs/nmWxSensitive.cpp b/Src/nmNum/nmSubWxs/nmWxSensitive.cpp index 316b5221..3c787243 100644 --- a/Src/nmNum/nmSubWxs/nmWxSensitive.cpp +++ b/Src/nmNum/nmSubWxs/nmWxSensitive.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -178,13 +179,14 @@ public: void setResults(const QSharedPointer& results) { - // 同一批次可能在其他结果列表中改变可见性,同步勾选而不重置滚动位置。 + // 同一批次同步已应用的选择,并保留本窗口尚未应用的勾选。 if(m_results == results) { if(rowCount() > 0) emit dataChanged(index(0, 0), index(rowCount() - 1, 0)); return; } beginResetModel(); m_results = results; + m_pendingVisibility.clear(); endResetModel(); } @@ -196,15 +198,15 @@ public: int columnCount(const QModelIndex& parent = QModelIndex()) const override { - return parent.isValid() ? 0 : 4; + return parent.isValid() ? 0 : 3; } QVariant headerData(int section, Qt::Orientation orientation, int role) const override { - // 表格显示运行编号、冻结参数与实际计算状态。 + // 表格仅保留显示选择、运行编号与冻结参数。 if(orientation != Qt::Horizontal || role != Qt::DisplayRole) return QVariant(); const QStringList titles = QStringList() << nmWxSensitive::tr("Show") << "#" - << nmWxSensitive::tr("Parameters") << nmWxSensitive::tr("Status"); + << nmWxSensitive::tr("Parameters"); return titles.value(section); } @@ -214,16 +216,13 @@ public: if(!index.isValid() || index.row() >= rowCount()) return QVariant(); const nmDataSensitive::RunResult& run = m_results->m_runs[index.row()]; if(role == Qt::CheckStateRole && index.column() == 0) - return run.m_bSucceeded && !run.m_bCancelled && run.m_bVisible ? Qt::Checked : Qt::Unchecked; - if(role == Qt::DecorationRole && index.column() == 0) - return QColor::fromHsv(int((run.m_nRunIndex % 360) * 137 % 360), 190, 190); + return run.m_bSucceeded && !run.m_bCancelled && + m_pendingVisibility.value(index.row(), run.m_bVisible) ? Qt::Checked : Qt::Unchecked; if(role == Qt::TextAlignmentRole && index.column() == 1) return int(Qt::AlignRight | Qt::AlignVCenter); - if(role == Qt::ToolTipRole && index.column() == 3) return run.m_sFailureReason; + if(role == Qt::ToolTipRole && !run.m_sFailureReason.isEmpty()) return run.m_sFailureReason; if(role != Qt::DisplayRole && role != Qt::ToolTipRole) return QVariant(); if(index.column() == 1) return QString::number(run.m_nRunIndex + 1); - if(index.column() == 3) return run.m_bCancelled ? nmWxSensitive::tr("Stopped") - : run.m_bSucceeded ? nmWxSensitive::tr("Succeeded") : nmWxSensitive::tr("Failed"); if(index.column() != 2) return QVariant(); QStringList descriptions; for(int i = 0; i < m_results->m_variables.size() && i < run.m_vecParameters.size(); ++i) { @@ -251,26 +250,49 @@ public: bool setData(const QModelIndex& index, const QVariant& value, int role) override { - // 可见性只在结果发布后的界面线程改变,不触发再次求解。 + // 勾选只记在本窗口,点击 Apply 或 OK 后才写入绘图使用的结果。 if(!index.isValid() || index.column() != 0 || role != Qt::CheckStateRole) return false; - nmDataSensitive::RunResult& run = m_results->m_runs[index.row()]; + const nmDataSensitive::RunResult& run = m_results->m_runs[index.row()]; if(!run.m_bSucceeded || run.m_bCancelled) return false; - run.m_bVisible = value.toInt() == Qt::Checked; + const bool visible = value.toInt() == Qt::Checked; + if(visible == run.m_bVisible) m_pendingVisibility.remove(index.row()); + else m_pendingVisibility.insert(index.row(), visible); emit dataChanged(index, index); return true; } void setAllVisible(bool visible) { - // 批量切换后只发送一次通知,整批曲线也只重新绘制一次。 + // 全选与全隐藏也只改变待应用选择,不立即刷新曲线。 if(m_results.isNull() || m_results->m_runs.empty()) return; - for(size_t i = 0; i < m_results->m_runs.size(); ++i) - m_results->m_runs[i].m_bVisible = visible; + for(int i = 0; i < rowCount(); ++i) { + const nmDataSensitive::RunResult& run = m_results->m_runs[i]; + if(!run.m_bSucceeded || run.m_bCancelled) continue; + if(visible == run.m_bVisible) m_pendingVisibility.remove(i); + else m_pendingVisibility.insert(i, visible); + } emit dataChanged(index(0, 0), index(rowCount() - 1, 0)); } + bool applyVisibility() + { + // 只提交本窗口改过的组,其他结果列表已应用的选择保持不变。 + bool changed = false; + QMap::const_iterator it = m_pendingVisibility.constBegin(); + for(; it != m_pendingVisibility.constEnd(); ++it) { + nmDataSensitive::RunResult& run = m_results->m_runs[it.key()]; + if(run.m_bVisible != it.value()) { + run.m_bVisible = it.value(); + changed = true; + } + } + m_pendingVisibility.clear(); + return changed; + } + private: QSharedPointer m_results; + QMap m_pendingVisibility; }; // ========= 右对齐委托:保证显示和编辑时都是右对齐 ========= @@ -575,7 +597,7 @@ private: }; nmWxSensitive::nmWxSensitive(QWidget *parent, nmDataAnalyzeManager* manager) - : iDlgBase(parent), m_refreshingResults(false), m_selectedValueIndex(0), m_loading(true), + : iDlgBase(parent), m_selectedValueIndex(0), m_loading(true), m_pDataManager(manager ? manager : nmDataAnalyzeManager::getCurrentInstance()), m_previewView(nullptr), m_pSensitivityTask(nullptr), m_calculationRunning(false) { @@ -1672,22 +1694,29 @@ void nmWxSensitive::onResultsClicked() table->horizontalHeader()->setResizeMode(0, QHeaderView::ResizeToContents); table->horizontalHeader()->setResizeMode(1, QHeaderView::ResizeToContents); table->horizontalHeader()->setResizeMode(2, QHeaderView::Stretch); - table->horizontalHeader()->setResizeMode(3, QHeaderView::ResizeToContents); layout->addWidget(table); QHBoxLayout* buttons = new QHBoxLayout(); QPushButton* showAll = new QPushButton(tr("Show all"), m_resultsDialog.data()); QPushButton* hideAll = new QPushButton(tr("Hide all"), m_resultsDialog.data()); - QPushButton* close = new QPushButton(tr("Close"), m_resultsDialog.data()); + // 明确按钮顺序,避免系统的标准按钮布局把 Apply 排到最后。 + QPushButton* apply = new QPushButton(tr("Apply"), m_resultsDialog.data()); + QPushButton* ok = new QPushButton(tr("OK"), m_resultsDialog.data()); + QPushButton* cancel = new QPushButton(tr("Cancel"), m_resultsDialog.data()); + apply->setAutoDefault(false); + cancel->setAutoDefault(false); + ok->setDefault(true); buttons->addWidget(showAll); buttons->addWidget(hideAll); buttons->addStretch(); - buttons->addWidget(close); + buttons->addWidget(apply); + buttons->addWidget(ok); + buttons->addWidget(cancel); layout->addLayout(buttons); - connect(m_resultsModel.data(), SIGNAL(dataChanged(QModelIndex,QModelIndex)), - this, SLOT(onResultsVisibilityChanged())); connect(showAll, SIGNAL(clicked()), this, SLOT(onShowAllResults())); connect(hideAll, SIGNAL(clicked()), this, SLOT(onHideAllResults())); - connect(close, SIGNAL(clicked()), m_resultsDialog.data(), SLOT(close())); + connect(apply, SIGNAL(clicked()), this, SLOT(onApplyResults())); + connect(ok, SIGNAL(clicked()), this, SLOT(onAcceptResults())); + connect(cancel, SIGNAL(clicked()), m_resultsDialog.data(), SLOT(reject())); } onResultsChanged(); hide(); @@ -1704,10 +1733,7 @@ void nmWxSensitive::onResultsChanged() results = m_pDataManager->getSensitiveData()->getResults(); resultsButton->setEnabled(!results.isNull()); m_sensitiveData.setResults(results); - // 同步其他列表的勾选只刷新显示,不能再次发布结果造成循环通知。 - m_refreshingResults = true; if(!m_resultsModel.isNull()) m_resultsModel->setResults(results); - m_refreshingResults = false; if(!m_resultsDialog.isNull()) { const QString status = results.isNull() ? QString() : results->m_bStopped ? tr("Stopped") : results->m_bCompleted ? tr("Completed") : tr("Failed"); @@ -1716,22 +1742,29 @@ void nmWxSensitive::onResultsChanged() } } -void nmWxSensitive::onResultsVisibilityChanged() +void nmWxSensitive::onApplyResults() { - // 结果选择与项目保存共用数据,通知主界面刷新图形而不重新计算。 - if(m_pDataManager && !m_refreshingResults) + // 应用选择后通知主界面刷新,结果选择窗口保持打开。 + if(m_pDataManager && !m_resultsModel.isNull() && m_resultsModel->applyVisibility()) m_pDataManager->publishSensitiveResults(m_pDataManager->getSensitiveDataCopy()); } +void nmWxSensitive::onAcceptResults() +{ + // OK 与 Apply 提交相同的选择,随后关闭窗口。 + onApplyResults(); + if(!m_resultsDialog.isNull()) m_resultsDialog->accept(); +} + void nmWxSensitive::onShowAllResults() { - // 恢复所有成功组的叠加曲线。 + // 勾选所有成功组,等待 Apply 或 OK 更新曲线。 if(!m_resultsModel.isNull()) m_resultsModel->setAllVisible(true); } void nmWxSensitive::onHideAllResults() { - // 隐藏所有计算组,保留测量曲线作为参考。 + // 取消所有采样组的勾选,测量曲线和原模型数值解仍作为参考。 if(!m_resultsModel.isNull()) m_resultsModel->setAllVisible(false); }