#include "nmWxSensitive.h" #include "nmDataAnalyzeManager.h" #include "nmDataSensitive.h" #include "nmDataReservoir.h" #include "nmDataWellBase.h" #include "nmDataPerforation.h" #include "nmPebiResultSnapshot.h" #include "nmCalculationDllPebiSolverTask.h" #include "iSubWndFitting.h" #include "mGui/mGuiAnal/iAnalRun.h" #include "iUnitHelper.h" #include "iUnitGroup.h" #include "iSysParaHelper.h" #include "iParameter.h" #include "mModuleDefines.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #pragma comment(lib, "mGuiAnal.lib") #include #include #include namespace { QString formatSamplingValue(double value, int digits) { // 仅对显示值取有效数字,较大或较小的数自动使用科学计数法。 return value == 0.0 ? QString("0") : QString::number(value, 'g', digits); } void setSamplingEditValue(QLineEdit* edit, double value) { // 显示文本与原始数值分开保存,打开或保存窗口不会丢失采样精度。 const QString text = formatSamplingValue(value, 6); edit->setProperty("samplingValue", value); edit->setProperty("samplingText", text); edit->setText(text); } double samplingEditValue(const QLineEdit* edit, bool* ok) { // 未修改的格式化文本读取原始值;用户的新输入按实际文本解析。 const QVariant value = edit->property("samplingValue"); if (value.isValid() && edit->text() == edit->property("samplingText").toString()) return value.toDouble(ok); return edit->text().trimmed().toDouble(ok); } QString samplingBaseUnit(const QString& name) { // 与现有 PEBI 数据层的业务基准单位保持一致。 if (name == "Pi") return "MPa"; if (name == "k") return "mD"; if (name == "h") return "m"; if (name == "C") return "m^3/MPa"; return QString(); } bool convertSamplingValue(const QString& name, double value, QString from, QString to, double& converted) { // 优先复用项目单位组;旧配置的 psi 按绝对压力单位迁移。 if (from == "psi") from = "psia"; if (to == "psi") to = "psia"; if (from == "dimensionless") from.clear(); if (to == "dimensionless") to.clear(); if (from == to || (from.isEmpty() && to.isEmpty())) { converted = value; return qIsFinite(converted); } iUnitGroup* group = iUnitHelper::getUnitGroupByUnit(from); int digit = 0; if (group && group->indexOf(to) >= 0 && group->convert(from, value, to, converted, digit)) return qIsFinite(converted); if (name == "h") converted = nmDataAttribute::convertLengthUnit(value, from, to); else if (name == "Pi") converted = nmDataAttribute::convertPressureUnit(value, from, to); else if (name == "k") converted = nmDataAttribute::convertPermeabilityUnit(value, from, to); else return false; return qIsFinite(converted); } bool isSupportedSamplingVariable(const QString& name) { return name == "Skin" || name == "C" || name == "Pi" || name == "k" || name == "h" || name == QString(QChar(0x03c6)); } bool samplingPhysicalRange(const QString& name, double& minimum, double& maximum) { // 与自动拟合共用 NM 系统参数表,数值均为业务基准单位。 QString parameterName; if (name == "Skin") parameterName = "Result_W_Skin"; else if (name == "C") parameterName = "Result_W_C"; else if (name == "Pi") parameterName = "Result_Pi"; else if (name == "k") parameterName = "Result_K"; else if (name == "h") parameterName = "Result_h"; else if (name == QString(QChar(0x03c6))) parameterName = "Result_phi"; else return false; iSysParaHelper* helper = _paraHelper; iParameter* parameter = helper ? helper->getPara(parameterName, s_Nm_Serie) : nullptr; if (!parameter || !qIsFinite(parameter->m_dMin) || !qIsFinite(parameter->m_dMax) || parameter->m_dMin >= parameter->m_dMax) return false; minimum = name == "Skin" ? 0.0 : parameter->m_dMin; maximum = parameter->m_dMax; return minimum < maximum; } bool nearlyEqualSamplingValue(double left, double right) { // 沿用自动拟合的八个机器精度容差,只处理边界的浮点舍入差。 return qAbs(left - right) <= DBL_EPSILON * 8.0 * qMax(qAbs(left), qAbs(right)); } bool validSamplingValue(const QString& name, double& value) { // 所有采样值都受系统物理范围约束,边界末位误差归一到精确边界。 if (!qIsFinite(value)) return false; double minimum = 0.0, maximum = 0.0; if (!samplingPhysicalRange(name, minimum, maximum)) return false; if (value < minimum && !nearlyEqualSamplingValue(value, minimum)) return false; if (value > maximum && !nearlyEqualSamplingValue(value, maximum)) return false; value = qBound(minimum, value, maximum); return true; } } // ========= 已完成批次的连续结果列表 ========= class SensitivityResultsModel : public QAbstractTableModel { public: explicit SensitivityResultsModel(QObject* parent) : QAbstractTableModel(parent) {} 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(); } int rowCount(const QModelIndex& parent = QModelIndex()) const override { // QTableView 按需读取行,结果列表连续滚动,不一次性创建全部表格项。 return parent.isValid() || m_results.isNull() ? 0 : int(m_results->m_runs.size()); } int columnCount(const QModelIndex& parent = QModelIndex()) const override { 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"); return titles.value(section); } QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override { // 参数显示使用冻结配置中的单位和三位有效数字,不修改原始采样值。 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 && 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 && !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() != 2) return QVariant(); QStringList descriptions; for(int i = 0; i < m_results->m_variables.size() && i < run.m_vecParameters.size(); ++i) { const nmDataSensitive::VariableSampling& variable = m_results->m_variables[i]; if(!variable.m_enabled) continue; double value = run.m_vecParameters[i]; const QString unit = variable.m_modelValue.getUnit(); convertSamplingValue(variable.m_varName, value, samplingBaseUnit(variable.m_varName), unit, value); descriptions << variable.m_varName + " = " + formatSamplingValue(value, 3) + (unit.isEmpty() ? QString() : " " + unit); } return descriptions.join(" + "); } Qt::ItemFlags flags(const QModelIndex& index) const override { // 失败和取消组只能查看,不能勾选不存在的计算曲线。 if(!index.isValid()) return Qt::NoItemFlags; Qt::ItemFlags result = Qt::ItemIsEnabled | Qt::ItemIsSelectable; const nmDataSensitive::RunResult& run = m_results->m_runs[index.row()]; if(index.column() == 0 && run.m_bSucceeded && !run.m_bCancelled) result |= Qt::ItemIsUserCheckable; return result; } bool setData(const QModelIndex& index, const QVariant& value, int role) override { // 勾选只记在本窗口,点击 Apply 或 OK 后才写入绘图使用的结果。 if(!index.isValid() || index.column() != 0 || role != Qt::CheckStateRole) return false; const nmDataSensitive::RunResult& run = m_results->m_runs[index.row()]; if(!run.m_bSucceeded || run.m_bCancelled) return false; 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(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; }; // ========= 右对齐委托:保证显示和编辑时都是右对齐 ========= class RightAlignDelegate : public QStyledItemDelegate { public: explicit RightAlignDelegate(QObject *parent = nullptr) : QStyledItemDelegate(parent) {} void initStyleOption(QStyleOptionViewItem *option, const QModelIndex &index) const override { QStyledItemDelegate::initStyleOption(option, index); option->displayAlignment = Qt::AlignRight | Qt::AlignVCenter; } QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override { QWidget *editor = QStyledItemDelegate::createEditor(parent, option, index); if (QLineEdit *line = qobject_cast(editor)) { line->setAlignment(Qt::AlignRight | Qt::AlignVCenter); } return editor; } void setEditorData(QWidget *editor, const QModelIndex &index) const override { QLineEdit* line = qobject_cast(editor); // 编辑时沿用三位有效数字显示,避免重新暴露浮点尾数。 if (line) line->setText(index.data(Qt::UserRole).isValid() ? formatSamplingValue(index.data(Qt::UserRole).toDouble(), 3) : index.data(Qt::DisplayRole).toString()); } void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override { QLineEdit* line = qobject_cast(editor); if (!line) return; bool ok = false; double value = line->text().trimmed().toDouble(&ok); // 编辑后文本未改变时保留完整采样值,不把显示舍入当成用户修改。 const QVariant original = index.data(Qt::UserRole); if (original.isValid() && line->text() == formatSamplingValue(original.toDouble(), 3)) value = original.toDouble(&ok); // 非法输入保留原文,由 Generate 或预览统一定位并提示。 model->setData(index, ok && qIsFinite(value) ? QVariant(value) : QVariant(), Qt::UserRole); model->setData(index, ok && qIsFinite(value) ? formatSamplingValue(value, 3) : line->text(), Qt::DisplayRole); } }; // 表格仅承载可见行,完整组合编号使用六十四位整数,滚动过程中按编号取数。 class SensitivityRunsView : public QAbstractScrollArea { public: SensitivityRunsView(const nmDataSensitive& data, const QIcon& arrow, QWidget* parent) : QAbstractScrollArea(parent), m_data(data), m_arrow(arrow), m_table(nullptr), m_total(data.getTotalModelCount()), m_first(0), m_selected(0), m_visibleRows(1), m_updating(false), m_wheelDelta(0) { // 使用现有表格控件绘制表头、行选中和单元格,滚动条控制完整列表。 setFrameShape(QFrame::StyledPanel); setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); m_table = new QTableWidget(viewport()); m_table->setFrameShape(QFrame::NoFrame); m_table->setColumnCount(3); m_table->setHorizontalHeaderLabels(QStringList() << "" << "#" << nmWxSensitive::tr("Description")); m_table->verticalHeader()->setVisible(false); m_table->verticalHeader()->setDefaultSectionSize(24); m_table->horizontalHeader()->setResizeMode(0, QHeaderView::Fixed); m_table->horizontalHeader()->setResizeMode(1, QHeaderView::Fixed); m_table->horizontalHeader()->setResizeMode(2, QHeaderView::Interactive); m_table->horizontalHeader()->setStretchLastSection(true); m_table->horizontalHeader()->setHighlightSections(false); m_table->horizontalHeader()->setDefaultAlignment(Qt::AlignLeft | Qt::AlignVCenter); m_table->setColumnWidth(0, 20); m_table->setColumnWidth(1, m_table->fontMetrics().width(QString::number(m_total)) + 16); m_table->setEditTriggers(QAbstractItemView::NoEditTriggers); m_table->setSelectionBehavior(QAbstractItemView::SelectRows); m_table->setSelectionMode(QAbstractItemView::SingleSelection); m_table->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); m_table->setWordWrap(false); m_table->setStyleSheet("QTableWidget { background-color: white; }"); int descriptionWidth = 0; for (int i = 0; i < m_data.getVariableCount(); ++i) { nmDataSensitive::VariableSampling& variable = m_data.getVariable(i); if (!variable.getEnabled()) continue; m_indices.append(i); int width = 0; const QVector samples = variable.samplingValues(); for (int row = 0; row < samples.size(); ++row) width = qMax(width, m_table->fontMetrics().width(parameterDescription(i, samples[row]))); if (m_data.getCalculationType() == nmDataSensitive::CALC_DETERMINISTIC) descriptionWidth = qMax(descriptionWidth, width); else descriptionWidth += width + m_table->fontMetrics().width(" + "); } // 长组合可横向滚动并通过提示查看完整文本,保持各行高度一致。 m_table->setColumnWidth(2, qMax(200, descriptionWidth + 16)); QVBoxLayout* layout = new QVBoxLayout(viewport()); layout->setContentsMargins(0, 0, 0, 0); layout->addWidget(m_table); m_table->installEventFilter(this); m_table->viewport()->installEventFilter(this); verticalScrollBar()->installEventFilter(this); } void scrollAction(int action) { // 超大列表的滚动条位置按比例映射,单步、整屏移动仍保持精确行号。 switch (action) { case QAbstractSlider::SliderSingleStepAdd: scrollRows(1); break; case QAbstractSlider::SliderSingleStepSub: scrollRows(-1); break; case QAbstractSlider::SliderPageStepAdd: scrollRows(m_visibleRows); break; case QAbstractSlider::SliderPageStepSub: scrollRows(-m_visibleRows); break; case QAbstractSlider::SliderToMinimum: m_first = 0; refreshRows(); break; case QAbstractSlider::SliderToMaximum: m_first = maximumFirst(); refreshRows(); break; case QAbstractSlider::SliderMove: m_first = firstFromPosition(verticalScrollBar()->sliderPosition()); refreshRows(); break; default: break; } } protected: void resizeEvent(QResizeEvent* event) override { // 窗口变化时仅调整可见行数量,绝不按总次数分配表格项。 QAbstractScrollArea::resizeEvent(event); refreshRows(); } void scrollContentsBy(int, int) override { // 处理拖动滚动条或系统直接改变位置的情况。 if (m_updating) return; m_first = firstFromPosition(verticalScrollBar()->value()); refreshRows(); } bool eventFilter(QObject* object, QEvent* event) override { // 滚轮、键盘和鼠标均操作完整运行列表,而不是可见行缓存。 if (event->type() == QEvent::Wheel) { QWheelEvent* wheel = static_cast(event); if (wheel->orientation() == Qt::Vertical) { m_wheelDelta += wheel->delta(); const int steps = m_wheelDelta / 120; m_wheelDelta %= 120; scrollRows(-qint64(steps) * QApplication::wheelScrollLines()); wheel->accept(); return true; } } else if (event->type() == QEvent::KeyPress) { QKeyEvent* key = static_cast(event); qint64 movement = 0; switch (key->key()) { case Qt::Key_Up: movement = -1; break; case Qt::Key_Down: movement = 1; break; case Qt::Key_PageUp: movement = -m_visibleRows; break; case Qt::Key_PageDown: movement = m_visibleRows; break; case Qt::Key_Home: m_selected = 0; break; case Qt::Key_End: m_selected = m_total - 1; break; default: return QAbstractScrollArea::eventFilter(object, event); } if (movement < 0) m_selected -= qMin(m_selected, quint64(-movement)); else if (movement > 0) m_selected += qMin(m_total - 1 - m_selected, quint64(movement)); if (m_selected < m_first) m_first = m_selected; else if (m_selected - m_first >= quint64(m_visibleRows)) m_first = m_selected - m_visibleRows + 1; refreshRows(); key->accept(); return true; } else if (object == m_table->viewport() && (event->type() == QEvent::MouseButtonPress || event->type() == QEvent::MouseButtonRelease)) { QMouseEvent* mouse = static_cast(event); const int row = m_table->rowAt(mouse->pos().y()); if (mouse->button() == Qt::LeftButton && row >= 0) { m_selected = m_first + row; refreshRows(); } } else if (object == m_table->viewport() && event->type() == QEvent::Resize) { refreshRows(); } return QAbstractScrollArea::eventFilter(object, event); } private: quint64 maximumFirst() const { return m_total > quint64(m_visibleRows) ? m_total - m_visibleRows : 0; } quint64 firstFromPosition(int position) const { // 两端准确对应首尾,中间位置不把六十四位总数截断为 int。 const int maximum = verticalScrollBar()->maximum(); if (position <= 0 || maximum == 0) return 0; if (position >= maximum) return maximumFirst(); return qMin(maximumFirst(), quint64((long double)position * maximumFirst() / maximum)); } void scrollRows(qint64 movement) { // 用差值限制移动距离,避免无符号行号在首尾发生溢出。 if (movement < 0) m_first -= qMin(m_first, quint64(-movement)); else m_first += qMin(maximumFirst() - m_first, quint64(movement)); refreshRows(); } QString parameterDescription(int index, double value) { // 描述沿用采样表的三位有效数字和当前显示单位。 nmDataSensitive::VariableSampling& variable = m_data.getVariable(index); const QString unit = variable.getModelValue().getUnit(); convertSamplingValue(variable.getVarName(), value, samplingBaseUnit(variable.getVarName()), unit, value); return variable.getVarName() + " = " + formatSamplingValue(value, 3) + (unit.isEmpty() ? QString() : " " + unit); } void refreshRows() { if (m_updating || !m_table || !viewport()->layout()) return; // 始终只生成视口内的行,所有组合共用一根连续滚动条。 m_updating = true; viewport()->layout()->activate(); m_visibleRows = qMax(1, m_table->viewport()->height() / 24); setVerticalScrollBarPolicy(m_total > quint64(m_visibleRows) ? Qt::ScrollBarAlwaysOn : Qt::ScrollBarAlwaysOff); m_first = qMin(m_first, maximumFirst()); QScrollBar* bar = verticalScrollBar(); const quint64 last = maximumFirst(); const int maximum = int(qMin(last, quint64(INT_MAX))); const bool blocked = bar->blockSignals(true); bar->setRange(0, maximum); bar->setSingleStep(1); bar->setPageStep(last ? qMax(1, int((long double)m_visibleRows * maximum / last)) : m_visibleRows); int position = last ? int((long double)m_first * maximum / last) : 0; // 比例舍入后仍保留首尾箭头的可用状态。 if (last > quint64(INT_MAX) && m_first > 0 && m_first < last) position = qBound(1, position, maximum - 1); bar->setValue(position); bar->blockSignals(blocked); const int rows = int(qMin(quint64(m_visibleRows), m_total - m_first)); m_table->setRowCount(rows); for (int row = 0; row < rows; ++row) { const quint64 run = m_first + row; QVector values; if (!m_data.getRunValues(run, values)) break; int changingIndex = -1; if (m_data.getCalculationType() == nmDataSensitive::CALC_DETERMINISTIC) { quint64 remaining = run; for (int i = 0; i < m_indices.size(); ++i) { const quint64 count = m_data.getVariable(m_indices[i]).getNumber(); if (remaining < count) { changingIndex = m_indices[i]; break; } remaining -= count; } } QStringList descriptions; for (int i = 0; i < m_indices.size(); ++i) { const int index = m_indices[i]; if (changingIndex < 0 || index == changingIndex) descriptions << parameterDescription(index, values[index]); } QTableWidgetItem* arrow = new QTableWidgetItem(); arrow->setIcon(run == m_selected ? m_arrow : QIcon()); m_table->setItem(row, 0, arrow); QTableWidgetItem* number = new QTableWidgetItem(QString::number(run + 1)); number->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter); m_table->setItem(row, 1, number); QTableWidgetItem* description = new QTableWidgetItem(descriptions.join(" + ")); description->setToolTip(description->text()); m_table->setItem(row, 2, description); } if (m_selected >= m_first && m_selected - m_first < quint64(rows)) { m_table->setCurrentCell(int(m_selected - m_first), 1); m_table->selectRow(int(m_selected - m_first)); } else { m_table->setCurrentIndex(QModelIndex()); m_table->clearSelection(); } m_updating = false; // 横向滚动条显隐可能改变视口高度,补齐最终可见行数量。 if (m_visibleRows != qMax(1, m_table->viewport()->height() / 24)) refreshRows(); } nmDataSensitive m_data; QIcon m_arrow; QTableWidget* m_table; QList m_indices; quint64 m_total; quint64 m_first; quint64 m_selected; int m_visibleRows; bool m_updating; int m_wheelDelta; }; nmWxSensitive::nmWxSensitive(QWidget *parent, nmDataAnalyzeManager* manager) : iDlgBase(parent), m_selectedValueIndex(0), m_loading(true), m_pDataManager(manager ? manager : nmDataAnalyzeManager::getCurrentInstance()), m_previewView(nullptr), m_pSensitivityTask(nullptr), m_calculationRunning(false) { // 显式绑定所属分析窗口,后续切换窗口不会改变配置保存目标。 if (m_pDataManager) m_sensitiveData = m_pDataManager->getSensitiveDataCopy(); refreshModelValues(); setupUI(); createConnections(); populateTreeWidget(); createDetailPages(); loadDataToUI(); m_loading = false; onTreeItemChanged(nullptr, 0); // 已有批次可直接重新打开,结果发布后同步入口与列表。 if(m_pDataManager) { connect(m_pDataManager, SIGNAL(sigSensitiveResultsChanged()), this, SLOT(onResultsChanged())); connect(m_pDataManager, SIGNAL(sigPebiResultSnapshotChanged()), this, SLOT(onModelResultChanged())); } onResultsChanged(); } nmWxSensitive::~nmWxSensitive() { // 父窗口强制销毁时先请求停止并等 DLL 返回,线程结束前不释放计算副本。 if(m_pSensitivityTask) { disconnect(m_pSensitivityTask, nullptr, this, nullptr); m_pSensitivityTask->requestCancel(); m_pSensitivityTask->wait(); delete m_pSensitivityTask; } } void nmWxSensitive::reject() { // Esc 与取消按钮使用相同的停止路径,保留已完成组供结束后查看。 if(m_calculationRunning) onCancelClicked(); else iDlgBase::reject(); } void nmWxSensitive::closeEvent(QCloseEvent* event) { if(m_calculationRunning) { onCancelClicked(); event->ignore(); return; } iDlgBase::closeEvent(event); } void nmWxSensitive::refreshModelValues() { // 六个参数以最后成功求解的快照为准,编辑中的模型只提供属性类型和显示单位。 const QSharedPointer snapshot = m_pDataManager ? m_pDataManager->getPebiResultSnapshot() : QSharedPointer(); const bool resetRanges = !snapshot.isNull() && snapshot->getSnapshotId() != m_sensitiveData.getModelSnapshotId(); nmDataWellBase* well = m_pDataManager ? m_pDataManager->findWellByCode(m_pDataManager->getPrimaryWellCode()) : nullptr; nmDataReservoir* reservoir = m_pDataManager ? m_pDataManager->getReservoirData() : nullptr; const nmPebiResultWellSnapshot* solvedWell = !snapshot.isNull() && well ? snapshot->findWell(well->getWellInstanceId()) : nullptr; const QStringList names = QStringList() << "Skin" << "C" << "Pi" << "k" << "h" << QString(QChar(0x03c6)); for (int n = 0; n < names.size(); ++n) { nmDataAttribute* attribute = nullptr; const QString name = names[n]; if (well && name == "Skin" && well->getPerforationCount() > 0) attribute = &well->getPerforation(0)->getSkin(); else if (well && name == "C") attribute = &well->getWellboreStorage(); else if (reservoir && name == "Pi") attribute = &reservoir->getInitialPressure(); else if (reservoir && name == "k") attribute = &reservoir->getPermeability(); else if (reservoir && name == "h") attribute = &reservoir->getThickness(); else if (reservoir && name == QString(QChar(0x03c6))) attribute = &reservoir->getPorosity(); int index = -1; for (int i = 0; i < m_sensitiveData.getVariableCount(); ++i) if (m_sensitiveData.getVariable(i).getVarName() == name) { index = i; break; } const bool fresh = index < 0; if (fresh) { nmDataSensitive::VariableSampling variable; variable.setVarName(name); variable.setVarGroup(n < 2 ? "Tested Well" : "Reservoir"); variable.setNumber(5); m_sensitiveData.getVariables().append(variable); index = m_sensitiveData.getVariableCount() - 1; } nmDataSensitive::VariableSampling& variable = m_sensitiveData.getVariable(index); // 在局部属性上替换数值,不能把未求解的编辑值带入 Model value 或默认范围。 nmDataAttribute solvedAttribute; if (attribute && !snapshot.isNull()) { solvedAttribute = *attribute; const nmPebiResultReservoirParameters& solvedReservoir = snapshot->getReservoirParameters(); if (name == "Skin" || name == "C") { if (!solvedWell || (name == "Skin" && !solvedWell->m_bHasSkin)) attribute = nullptr; else solvedAttribute.setValue(name == "Skin" ? solvedWell->m_dSkin : solvedWell->m_dWellboreStorage); } else if (name == "Pi") solvedAttribute.setValue(solvedReservoir.m_dInitialPressure); else if (name == "k") solvedAttribute.setValue(solvedReservoir.m_dPermeability); else if (name == "h") solvedAttribute.setValue(solvedReservoir.m_dThickness); else if (name == QString(QChar(0x03c6))) solvedAttribute.setValue(solvedReservoir.m_dPorosity); if (attribute) attribute = &solvedAttribute; } double physicalMin = 0.0, physicalMax = 0.0; if (!attribute || !samplingPhysicalRange(name, physicalMin, physicalMax)) { variable.setEnabled(false); variable.getModelValue().setValue(QVariant()); continue; } const QString baseUnit = samplingBaseUnit(name); QString displayUnit = attribute->getUnit(); if (displayUnit == "dimensionless") displayUnit.clear(); double converted = 0.0; if (!convertSamplingValue(name, attribute->getValue().toDouble(), baseUnit, displayUnit, converted)) displayUnit = baseUnit; double minimum = variable.getMinValue().getValue().toDouble(); double maximum = variable.getMaxValue().getValue().toDouble(); QVector values = variable.getValues(); double previousModel = variable.getModelValue().getValue().toDouble(); bool migrated = !fresh && (m_sensitiveData.usesBaseUnits() || variable.getMode() == nmDataSensitive::VariableSampling::MODE_MANUAL || !values.isEmpty()); if (migrated && !m_sensitiveData.usesBaseUnits()) { const QString oldUnit = variable.getModelValue().getUnit(); convertSamplingValue(name, previousModel, oldUnit, baseUnit, previousModel); migrated = convertSamplingValue(name, minimum, oldUnit, baseUnit, minimum) && convertSamplingValue(name, maximum, oldUnit, baseUnit, maximum); for (int i = 0; migrated && i < values.size(); ++i) migrated = convertSamplingValue(name, values[i], oldUnit, baseUnit, values[i]); } // 识别 Skin、Pi 的自动默认范围,按当前模型更新,保留用户自定义区间。 const bool previousDefault = variable.getMode() == nmDataSensitive::VariableSampling::MODE_AUTOMATIC && ((nearlyEqualSamplingValue(minimum, qMin(previousModel / 2.0, previousModel * 2.0)) && nearlyEqualSamplingValue(maximum, qMax(previousModel / 2.0, previousModel * 2.0))) || (name == "Skin" && previousModel != 0.0 && nearlyEqualSamplingValue(minimum, qMax(physicalMin, qMin(previousModel * 0.9, previousModel * 1.1))) && nearlyEqualSamplingValue(maximum, qMin(physicalMax, qMax(previousModel * 0.9, previousModel * 1.1)))) || (name == "Skin" && previousModel == 0.0 && (minimum == -1.0 || minimum == 0.0) && maximum == 1.0) || (name == "Pi" && variable.getModelValue().getMinValue().isValid() && variable.getModelValue().getMaxValue().isValid() && nearlyEqualSamplingValue(minimum, variable.getModelValue().getMinValue().toDouble()) && nearlyEqualSamplingValue(maximum, variable.getModelValue().getMaxValue().toDouble()))); // 新的正式求解结果重新建立范围,采样数量、模式及手动值保持原设置。 if (resetRanges || !migrated || ((name == "Skin" || name == "Pi") && previousDefault)) { const double model = attribute->getValue().toDouble(); minimum = qMin(model / 2.0, model * 2.0); maximum = qMax(model / 2.0, model * 2.0); if (name == "Skin") { minimum = model == 0.0 ? -1.0 : qMin(model * 0.9, model * 1.1); maximum = model == 0.0 ? 1.0 : qMax(model * 0.9, model * 1.1); } if (name == "Pi") { minimum = physicalMin; maximum = physicalMax; } } if (!migrated) { values.clear(); variable.setMode(nmDataSensitive::VariableSampling::MODE_AUTOMATIC); variable.setLog(false); variable.setNumber(5); } // 初始范围和已保存范围都截断到硬限制,手动采样值留给提交校验。 minimum = qBound(physicalMin, minimum, physicalMax); maximum = qBound(physicalMin, maximum, physicalMax); variable.setModelValue(*attribute); variable.getModelValue().setUnit(displayUnit); variable.getModelValue().setValueRange(physicalMin, physicalMax); variable.setMinValue(variable.getModelValue()); variable.setMaxValue(variable.getModelValue()); variable.getMinValue().setValue(minimum); variable.getMaxValue().setValue(maximum); variable.setValues(values); // Saphir 在重新打开对话框时同步无效 Log 范围的实际状态。 if (variable.getMode() == nmDataSensitive::VariableSampling::MODE_AUTOMATIC && (minimum <= 0.0 || maximum <= 0.0)) variable.setLog(false); } for (int i = 0; i < m_sensitiveData.getVariableCount(); ++i) if (!isSupportedSamplingVariable(m_sensitiveData.getVariable(i).getVarName())) m_sensitiveData.getVariable(i).setEnabled(false); m_sensitiveData.setUsesBaseUnits(true); if (!snapshot.isNull()) m_sensitiveData.setModelSnapshotId(snapshot->getSnapshotId()); } void nmWxSensitive::onModelResultChanged() { // 查看井切换也会发送快照通知;只有新求解快照才刷新已打开窗口。 const QSharedPointer snapshot = m_pDataManager->getPebiResultSnapshot(); if (snapshot.isNull() || snapshot->getSnapshotId() == m_sensitiveData.getModelSnapshotId()) return; m_loading = true; refreshModelValues(); loadDataToUI(true); m_loading = false; updateModelCount(); } void nmWxSensitive::loadDataToUI(bool refreshRangesOnly) { // 加载过程中阻止联动刷新,先恢复设置,再一次生成各参数表格。 if (!refreshRangesOnly) calculationTypeCombo->setCurrentIndex(int(m_sensitiveData.getCalculationType())); for (int i = 0; i < m_sensitiveData.getVariableCount(); ++i) { nmDataSensitive::VariableSampling& variable = m_sensitiveData.getVariable(i); const QString name = variable.getVarName(); const int idx = findVariableIndex(name); if (idx < 0) continue; const bool available = isSupportedSamplingVariable(name) && variable.getModelValue().getValue().isValid(); for (int group = 0; group < variablesTree->topLevelItemCount(); ++group) { QTreeWidgetItem* parent = variablesTree->topLevelItem(group); for (int child = 0; child < parent->childCount(); ++child) { QTreeWidgetItem* item = parent->child(child); if (item->text(0) != name) continue; item->setDisabled(!available); if (!refreshRangesOnly) item->setCheckState(0, available && variable.getEnabled() ? Qt::Checked : Qt::Unchecked); } } detailStack->widget(idx + 1)->setEnabled(available); // 求解后的刷新只更新模型值和范围,不覆盖窗口中尚未提交的采样设置。 if (!refreshRangesOnly) { m_modeCombos[idx]->setCurrentIndex(int(variable.getMode())); m_logChecks[idx]->setChecked(variable.getLog()); m_numSpins[idx]->setValue(qMax(1, variable.getNumber())); } // 已打开窗口沿用原显示单位,避免手动表格的数值被换成另一种单位解释。 const QString unit = refreshRangesOnly ? m_unitLabels[idx]->text() : variable.getModelValue().getUnit(); variable.getModelValue().setUnit(unit); m_unitLabels[idx]->setText(unit); double model = variable.getModelValue().getValue().toDouble(); double minimum = variable.getMinValue().getValue().toDouble(); double maximum = variable.getMaxValue().getValue().toDouble(); convertSamplingValue(name, model, samplingBaseUnit(name), unit, model); convertSamplingValue(name, minimum, samplingBaseUnit(name), unit, minimum); convertSamplingValue(name, maximum, samplingBaseUnit(name), unit, maximum); setSamplingEditValue(m_modelSpins[idx], model); setSamplingEditValue(m_minSpins[idx], minimum); setSamplingEditValue(m_maxSpins[idx], maximum); double displayMin = variable.getModelValue().getMinValue().toDouble(); double displayMax = variable.getModelValue().getMaxValue().toDouble(); convertSamplingValue(name, displayMin, samplingBaseUnit(name), unit, displayMin); convertSamplingValue(name, displayMax, samplingBaseUnit(name), unit, displayMax); const QString rangeHint = tr("Allowed range: [%1, %2] %3") .arg(formatSamplingValue(displayMin, 6)).arg(formatSamplingValue(displayMax, 6)).arg(unit); m_minSpins[idx]->setToolTip(rangeHint); m_maxSpins[idx]->setToolTip(rangeHint); if (!refreshRangesOnly && variable.getMode() == nmDataSensitive::VariableSampling::MODE_MANUAL) { QTableWidget* table = m_valueTables[idx]; table->setRowCount(variable.getNumber()); const QVector& values = variable.getValues(); for (int row = 0; row < values.size() && row < table->rowCount(); ++row) { double display = values[row]; convertSamplingValue(name, display, samplingBaseUnit(name), unit, display); QTableWidgetItem* item = new QTableWidgetItem(formatSamplingValue(display, 3)); item->setData(Qt::UserRole, display); table->setItem(row, 1, item); } } updateValuesForPage(idx); } } int nmWxSensitive::findVariableIndex(const QString& varName) { // 根据创建顺序查找变量索引 QStringList allVars; allVars << "Zw" << "Hw" << "Lw" << "Skin" << "C" // Tested Well << "Pi" << "k" << "h" << QString(QChar(0x03c6)) << "ntg" << "kz/kr" // Reservoir << "Total compressibility"; // Pvt return allVars.indexOf(varName); } void nmWxSensitive::updateModelCount() { // 勾选、数量和方法修改后立即更新总次数,不等待 Generate。 if (m_loading) return; m_sensitiveData.setCalculationType( static_cast(calculationTypeCombo->currentIndex())); for (int group = 0; group < variablesTree->topLevelItemCount(); ++group) { QTreeWidgetItem* parent = variablesTree->topLevelItem(group); for (int child = 0; child < parent->childCount(); ++child) { QTreeWidgetItem* item = parent->child(child); for (int i = 0; i < m_sensitiveData.getVariableCount(); ++i) { nmDataSensitive::VariableSampling& variable = m_sensitiveData.getVariable(i); if (variable.getVarName() != item->text(0)) continue; variable.setEnabled(!item->isDisabled() && item->checkState(0) == Qt::Checked); const int idx = findVariableIndex(variable.getVarName()); if (idx >= 0) variable.setNumber(m_numSpins[idx]->value()); } } } bool overflow = false; const quint64 count = m_sensitiveData.calculateModelCount(&overflow); m_sensitiveData.setTotalModelCount(count); modelCountLabel->setText(overflow ? tr("Too many models") : QString::number(count)); previewButton->setEnabled(!overflow && count > 0); generateButton->setEnabled(m_pDataManager && !overflow && count > 0 && !m_calculationRunning); } bool nmWxSensitive::saveDataFromUI() { // 校验选中参数后再提交副本,失败不改写数据中心已有配置。 updateModelCount(); if (m_sensitiveData.getTotalModelCount() == 0) return false; nmDataSensitive candidate(m_sensitiveData); for (int i = 0; i < candidate.getVariableCount(); ++i) { nmDataSensitive::VariableSampling& variable = candidate.getVariable(i); const QString name = variable.getVarName(); const int idx = findVariableIndex(name); if (idx < 0 || !isSupportedSamplingVariable(name)) continue; variable.setMode(static_cast(m_modeCombos[idx]->currentIndex())); variable.setLog(m_logChecks[idx]->isChecked()); const QString unit = m_unitLabels[idx]->text(); const QString baseUnit = samplingBaseUnit(name); bool valid = true; if (variable.getMode() == nmDataSensitive::VariableSampling::MODE_AUTOMATIC) { bool minOk = false, maxOk = false; double minimum = samplingEditValue(m_minSpins[idx], &minOk); double maximum = samplingEditValue(m_maxSpins[idx], &maxOk); valid = minOk && maxOk && convertSamplingValue(name, minimum, unit, baseUnit, minimum) && convertSamplingValue(name, maximum, unit, baseUnit, maximum) && minimum <= maximum && validSamplingValue(name, minimum) && validSamplingValue(name, maximum); if (valid) { variable.getMinValue().setValue(minimum); variable.getMaxValue().setValue(maximum); // Number=1 使用模型值,也必须在同一物理范围内。 QVector values = variable.samplingValues(); for (int row = 0; valid && row < values.size(); ++row) valid = validSamplingValue(name, values[row]); if (valid) variable.setValues(values); } } else { QVector values; QTableWidget* table = m_valueTables[idx]; for (int row = 0; row < table->rowCount(); ++row) { QTableWidgetItem* item = table->item(row, 1); bool ok = false; double value = 0.0; if (item) value = item->data(Qt::UserRole).isValid() ? item->data(Qt::UserRole).toDouble(&ok) : item->text().trimmed().toDouble(&ok); if (!ok || !convertSamplingValue(name, value, unit, baseUnit, value) || !validSamplingValue(name, value)) { valid = false; break; } values.append(value); } valid = valid && values.size() == variable.getNumber(); if (valid) variable.setValues(values); } if (!valid && variable.getEnabled()) { double lower = 0.0, upper = 0.0; const bool hasRange = samplingPhysicalRange(name, lower, upper) && convertSamplingValue(name, lower, baseUnit, unit, lower) && convertSamplingValue(name, upper, baseUnit, unit, upper); QMessageBox::warning(this, tr("Sensitivity"), hasRange ? tr("%1: invalid sampling values. Allowed range: [%2, %3] %4. Check the range and every manual row.") .arg(name).arg(formatSamplingValue(lower, 6)).arg(formatSamplingValue(upper, 6)).arg(unit) : tr("%1: the physical range is unavailable.").arg(name)); detailStack->setCurrentIndex(idx + 1); return false; } if (!variable.getEnabled()) continue; // 本期处理常量参数;属性场开启时常量会被插值覆盖,因此不能静默运行。 if (m_pDataManager && (name == "k" || name == "h" || name == QString(QChar(0x03c6)))) { const QString property = name == QString(QChar(0x03c6)) ? "phi" : name; const QVector dataSets = m_pDataManager->getPropertyInterpolationDataSets(); for (int field = 0; field < dataSets.size(); ++field) { if (dataSets[field].useForCalculation && dataSets[field].property == property && !dataSets[field].points.isEmpty()) { QMessageBox::warning(this, tr("Sensitivity"), tr("%1 uses a non-uniform field. Disable that field before sampling a constant value.").arg(name)); return false; } } } } m_sensitiveData = candidate; return true; } void nmWxSensitive::setupUI() { setWindowTitle(tr("Sensitivity")); setMinimumSize(900, 550); QVBoxLayout *mainLayout = new QVBoxLayout(this); // ===== 顶部:Calculation type ===== QHBoxLayout *calcTypeLayout = new QHBoxLayout(); QLabel *calcTypeLabel = new QLabel(tr("Calculation type:"), this); calculationTypeCombo = new QComboBox(this); calculationTypeCombo->addItem(tr("Deterministic")); calculationTypeCombo->addItem(tr("Deterministic multivariate")); //calculationTypeCombo->addItem(tr("Monte-Carlo")); //calculationTypeCombo->addItem(tr("Monte-Carlo + Improve")); calcTypeLayout->addWidget(calcTypeLabel); calcTypeLayout->addWidget(calculationTypeCombo); calcTypeLayout->addStretch(); mainLayout->addLayout(calcTypeLayout); // ===== 中间:左 Variables 面板 + 右 detailStack ===== QSplitter *centerSplitter = new QSplitter(Qt::Horizontal, this); centerSplitter->setChildrenCollapsible(false); // 不允许把某一侧完全折叠 centerSplitter->setHandleWidth(4); // 中间拖动条宽度 centerSplitter->setStyleSheet( "QSplitter::handle:horizontal {" " background-color: #C0C0C0;" "}" "QSplitter::handle:horizontal:hover {" " background-color: #A0A0A0;" "}" ); // --- 左侧:Variables 面板(QFrame + 标题栏 + Tree)--- QFrame *variablesPanel = new QFrame(this); variablesPanel->setFrameShape(QFrame::StyledPanel); variablesPanel->setFrameShadow(QFrame::Raised); variablesPanel->setMinimumWidth(230); QVBoxLayout *variablesPanelLayout = new QVBoxLayout(variablesPanel); variablesPanelLayout->setContentsMargins(0, 0, 0, 0); variablesPanelLayout->setSpacing(0); // 标题栏 QWidget *varHeader = new QWidget(variablesPanel); varHeader->setFixedHeight(24); varHeader->setAutoFillBackground(true); { QPalette pal = varHeader->palette(); pal.setColor(QPalette::Window, palette().color(QPalette::Midlight)); varHeader->setPalette(pal); } QHBoxLayout *varHeaderLayout = new QHBoxLayout(varHeader); varHeaderLayout->setContentsMargins(6, 0, 6, 0); varHeaderLayout->setSpacing(0); QLabel *varTitle = new QLabel(tr("Variables"), varHeader); varTitle->setAlignment(Qt::AlignVCenter | Qt::AlignLeft); QFont f = varTitle->font(); f.setBold(true); varTitle->setFont(f); varHeaderLayout->addWidget(varTitle); varHeaderLayout->addStretch(); // Tree 区域 variablesTree = new QTreeWidget(variablesPanel); variablesTree->setHeaderHidden(true); variablesTree->setFrameShape(QFrame::NoFrame); // 外层已经有 frame variablesTree->setMinimumHeight(350); variablesPanelLayout->addWidget(varHeader); variablesPanelLayout->addWidget(variablesTree, 1); // 把左侧 panel 加进 splitter centerSplitter->addWidget(variablesPanel); // --- 右侧:detailStack(后面每个 page 自己做标题栏)--- detailStack = new QStackedWidget(this); detailStack->setContentsMargins(0, 0, 0, 0); // index 0:默认提示页 QWidget *blankPage = new QWidget(this); QVBoxLayout *blankLayout = new QVBoxLayout(blankPage); blankLayout->setContentsMargins(0, 0, 0, 0); QLabel *blankLabel = new QLabel(tr("Select a variable on the left."), blankPage); blankLayout->addStretch(); blankLayout->addWidget(blankLabel, 0, Qt::AlignHCenter); blankLayout->addStretch(); detailStack->addWidget(blankPage); // 把右侧 stack 加进 splitter centerSplitter->addWidget(detailStack); // 左边宽度基本固定,右边自适应 centerSplitter->setStretchFactor(0, 0); centerSplitter->setStretchFactor(1, 1); mainLayout->addWidget(centerSplitter, 1); // ===== 底部:Total number of models + 按钮 ===== QHBoxLayout *bottomLayout = new QHBoxLayout(); QLabel *totalLabel = new QLabel(tr("Total number of models:"), this); modelCountLabel = new QLabel(tr("0"), this); bottomLayout->addWidget(totalLabel); bottomLayout->addWidget(modelCountLabel); previewButton = new QPushButton(this); previewButton->setIcon(QIcon(QCoreApplication::applicationDirPath().section('/', 0, -2) + "/Res/Icon/Eyes.png")); previewButton->setFixedSize(28, 24); previewButton->setToolTip(tr("Sensitivity runs")); bottomLayout->addWidget(previewButton); bottomLayout->addStretch(); resultsButton = new QPushButton(tr("Results"), this); generateButton = new QPushButton(tr("Generate"), this); cancelButton = new QPushButton(tr("Cancel"), this); bottomLayout->addWidget(resultsButton); bottomLayout->addWidget(generateButton); bottomLayout->addWidget(cancelButton); mainLayout->addLayout(bottomLayout); } void nmWxSensitive::createConnections() { connect(generateButton, SIGNAL(clicked()), this, SLOT(onGenerateClicked())); connect(cancelButton, SIGNAL(clicked()), this, SLOT(onCancelClicked())); connect(previewButton, SIGNAL(clicked()), this, SLOT(onPreviewClicked())); connect(resultsButton, SIGNAL(clicked()), this, SLOT(onResultsClicked())); connect(calculationTypeCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(onCalculationTypeChanged(int))); connect(variablesTree, SIGNAL(itemClicked(QTreeWidgetItem*,int)), this, SLOT(onTreeItemClicked(QTreeWidgetItem*,int))); connect(variablesTree, SIGNAL(itemChanged(QTreeWidgetItem*,int)), this, SLOT(onTreeItemChanged(QTreeWidgetItem*,int))); } void nmWxSensitive::populateTreeWidget() { variablesTree->clear(); // ===== Tested Well ===== QTreeWidgetItem *testedWellItem = new QTreeWidgetItem(variablesTree); testedWellItem->setText(0, tr("Tested Well")); testedWellItem->setCheckState(0, Qt::Unchecked); testedWellItem->setFlags(testedWellItem->flags() | Qt::ItemIsUserCheckable); QStringList testedWellChildren; testedWellChildren << "Zw" << "Hw" << "Lw" << "Skin" << "C"; int i; for (i = 0; i < testedWellChildren.size(); ++i) { QTreeWidgetItem *childItem = new QTreeWidgetItem(testedWellItem); childItem->setText(0, testedWellChildren.at(i)); childItem->setCheckState(0, Qt::Unchecked); childItem->setFlags(childItem->flags() | Qt::ItemIsUserCheckable); childItem->setDisabled(!isSupportedSamplingVariable(testedWellChildren.at(i))); } // ===== Reservoir ===== QTreeWidgetItem *reservoirItem = new QTreeWidgetItem(variablesTree); reservoirItem->setText(0, tr("Reservoir")); reservoirItem->setCheckState(0, Qt::Unchecked); reservoirItem->setFlags(reservoirItem->flags() | Qt::ItemIsUserCheckable); QStringList reservoirChildren; reservoirChildren << "Pi" << "k" << "h" << QString(QChar(0x03c6)) << "ntg" << "kz/kr"; for (i = 0; i < reservoirChildren.size(); ++i) { QTreeWidgetItem *childItem = new QTreeWidgetItem(reservoirItem); childItem->setText(0, reservoirChildren.at(i)); childItem->setCheckState(0, Qt::Unchecked); childItem->setFlags(childItem->flags() | Qt::ItemIsUserCheckable); childItem->setDisabled(!isSupportedSamplingVariable(reservoirChildren.at(i))); } // ===== Pvt ===== QTreeWidgetItem *pvtItem = new QTreeWidgetItem(variablesTree); pvtItem->setText(0, tr("Pvt")); pvtItem->setCheckState(0, Qt::Unchecked); pvtItem->setFlags(pvtItem->flags() | Qt::ItemIsUserCheckable); QTreeWidgetItem *compressibilityItem = new QTreeWidgetItem(pvtItem); compressibilityItem->setText(0, tr("Total compressibility")); compressibilityItem->setCheckState(0, Qt::Unchecked); compressibilityItem->setFlags(compressibilityItem->flags() | Qt::ItemIsUserCheckable); compressibilityItem->setDisabled(true); variablesTree->expandAll(); } void nmWxSensitive::createDetailPages() { int i, j; // 依次为每个"叶子变量"创建一个页面,并把 index 存到 item->data 里 for (i = 0; i < variablesTree->topLevelItemCount(); ++i) { QTreeWidgetItem *topItem = variablesTree->topLevelItem(i); for (j = 0; j < topItem->childCount(); ++j) { QTreeWidgetItem *childItem = topItem->child(j); if (childItem == nullptr) { continue; } QString varName = childItem->text(0); QWidget *page = createVariablePage(varName); int pageIndex = detailStack->addWidget(page); // index 从 1 开始 childItem->setData(0, Qt::UserRole, pageIndex); } } } QIcon nmWxSensitive::createTriangleIcon() { QPixmap trianglePixmap(16, 16); trianglePixmap.fill(Qt::transparent); QPainter painter(&trianglePixmap); painter.setRenderHint(QPainter::Antialiasing, true); // 绘制小三角形(指向右的箭头) QPolygon triangle; triangle << QPoint(4, 6) << QPoint(4, 10) << QPoint(10, 8); painter.setBrush(QBrush(QColor(0, 0, 0))); // 黑色三角形 painter.setPen(QPen(QColor(0, 0, 0), 1)); painter.drawPolygon(triangle); return QIcon(trianglePixmap); } QWidget *nmWxSensitive::createVariablePage(const QString &varName) { QWidget *page = new QWidget(this); QHBoxLayout *pageLayout = new QHBoxLayout(page); pageLayout->setContentsMargins(0, 0, 0, 0); pageLayout->setSpacing(0); // ===== 整个 Sampling 面板:QFrame + 标题栏 + 内容 ===== QFrame *samplingPanel = new QFrame(page); samplingPanel->setFrameShape(QFrame::StyledPanel); samplingPanel->setFrameShadow(QFrame::Raised); QVBoxLayout *panelLayout = new QVBoxLayout(samplingPanel); panelLayout->setContentsMargins(0, 0, 0, 0); panelLayout->setSpacing(0); // ---- 标题栏:Sampling Xxx ---- QWidget *header = new QWidget(samplingPanel); header->setFixedHeight(24); header->setAutoFillBackground(true); { QPalette pal = header->palette(); pal.setColor(QPalette::Window, palette().color(QPalette::Midlight)); header->setPalette(pal); } QHBoxLayout *headerLayout = new QHBoxLayout(header); headerLayout->setContentsMargins(6, 0, 6, 0); headerLayout->setSpacing(0); QLabel *title = new QLabel(tr("Sampling %1").arg(varName), header); QFont tf = title->font(); tf.setBold(true); title->setFont(tf); headerLayout->addWidget(title); headerLayout->addStretch(); panelLayout->addWidget(header); // ---- 内容区:左参数 + 右表格 ---- QWidget *content = new QWidget(samplingPanel); QHBoxLayout *contentLayout = new QHBoxLayout(content); contentLayout->setContentsMargins(6, 0, 6, 6); contentLayout->setSpacing(12); // 左:参数区(GridLayout) QGridLayout *paramLayout = new QGridLayout(); paramLayout->setVerticalSpacing(6); int row = 0; // Mode + Log? QLabel *modeLabel = new QLabel(tr("Mode"), content); QComboBox *modeCombo = new QComboBox(content); modeCombo->addItem(tr("Automatic")); // 0 modeCombo->addItem(tr("Manual")); // 1 QCheckBox *logCheck = new QCheckBox(tr("Log?"), content); QWidget *modeRowWidget = new QWidget(content); QHBoxLayout *modeRowLayout = new QHBoxLayout(modeRowWidget); modeRowLayout->setContentsMargins(0, 0, 0, 0); modeRowLayout->addWidget(modeCombo); modeRowLayout->addWidget(logCheck); modeRowLayout->addStretch(); paramLayout->addWidget(modeLabel, row, 0); paramLayout->addWidget(modeRowWidget, row, 1); ++row; // Number QLabel *numLabel = new QLabel(tr("Number"), content); QSpinBox *numSpin = new QSpinBox(content); numSpin->setRange(1, 1000); //numSpin->setValue(1); numSpin->setButtonSymbols(QAbstractSpinBox::NoButtons); paramLayout->addWidget(numLabel, row, 0); paramLayout->addWidget(numSpin, row, 1); ++row; // 空行(Number 和 Model value 之间) int gapRow = row; paramLayout->setRowMinimumHeight(gapRow, 12); ++row; // Model value + 单位 QLabel *modelLabel = new QLabel(tr("Model value:"), content); QLineEdit *modelSpin = new QLineEdit(content); modelSpin->setObjectName("sensitivityModel_" + varName); // 基准值来自真实模型,只作参照;输入框支持科学计数法和完整精度。 modelSpin->setReadOnly(true); QLabel *unitLabel = new QLabel(tr(""), content); // ⭐ 单位也从数据加载 QWidget *modelRowWidget = new QWidget(content); QHBoxLayout *modelRowLayout = new QHBoxLayout(modelRowWidget); modelRowLayout->setContentsMargins(0, 0, 0, 0); modelRowLayout->addWidget(modelSpin); modelRowLayout->addWidget(unitLabel); modelRowLayout->addStretch(); paramLayout->addWidget(modelLabel, row, 0); paramLayout->addWidget(modelRowWidget, row, 1); ++row; // Minimum QLabel *minLabel = new QLabel(tr("Minimum:"), content); QLineEdit *minSpin = new QLineEdit(content); minSpin->setObjectName("sensitivityMinimum_" + varName); paramLayout->addWidget(minLabel, row, 0); paramLayout->addWidget(minSpin, row, 1); ++row; // Maximum QLabel *maxLabel = new QLabel(tr("Maximum:"), content); QLineEdit *maxSpin = new QLineEdit(content); maxSpin->setObjectName("sensitivityMaximum_" + varName); paramLayout->addWidget(maxLabel, row, 0); paramLayout->addWidget(maxSpin, row, 1); ++row; // 统一编辑框宽度 int fullWidth = modelSpin->sizeHint().width(); int editWidth = qMax(150, fullWidth); modeCombo->setFixedWidth(editWidth); numSpin->setFixedWidth(editWidth); modelSpin->setFixedWidth(editWidth); minSpin->setFixedWidth(editWidth); maxSpin->setFixedWidth(editWidth); modeCombo->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); numSpin->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); modelSpin->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); minSpin->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); maxSpin->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); QWidget *paramWidget = new QWidget(content); QVBoxLayout *leftVBox = new QVBoxLayout(paramWidget); leftVBox->setContentsMargins(0, 6, 0, 0); leftVBox->addLayout(paramLayout); leftVBox->addStretch(); // 右:Values 表格(双列:箭头 + 值) QTableWidget *valuesTable = new QTableWidget(content); valuesTable->setObjectName("sensitivityValues_" + varName); valuesTable->setColumnCount(2); QStringList headers; headers << "" << tr("Values"); valuesTable->setHorizontalHeaderLabels(headers); valuesTable->verticalHeader()->setVisible(false); valuesTable->setColumnWidth(0, 15); valuesTable->setColumnWidth(1, 130); QHeaderView *hh = valuesTable->horizontalHeader(); hh->setResizeMode(QHeaderView::Fixed); hh->setHighlightSections(false); valuesTable->setStyleSheet( "QHeaderView::section {" " border-top: 0px;" " border-left: 0px;" " border-right: 1px solid lightgray;" " border-bottom: 1px solid lightgray;" "}" "QTableCornerButton::section {" " border-top: 0px;" " border-left: 0px;" " border-right: 1px solid lightgray;" " border-bottom: 1px solid lightgray;" "}" ); valuesTable->horizontalHeader()->setStretchLastSection(false); valuesTable->setEditTriggers(QAbstractItemView::DoubleClicked); valuesTable->setSelectionBehavior(QAbstractItemView::SelectRows); valuesTable->setSelectionMode(QAbstractItemView::NoSelection); valuesTable->setFocusPolicy(Qt::NoFocus); valuesTable->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); valuesTable->setShowGrid(true); valuesTable->setItemDelegateForColumn(1, new RightAlignDelegate(valuesTable)); int tableWidth = valuesTable->frameWidth() * 2 + valuesTable->verticalHeader()->sizeHint().width() + valuesTable->columnWidth(0) + valuesTable->columnWidth(1); valuesTable->setFixedWidth(tableWidth); QWidget *tableContainer = new QWidget(content); QVBoxLayout *tableLayout = new QVBoxLayout(tableContainer); tableLayout->setContentsMargins(0, 0, 0, 0); tableLayout->setSpacing(0); tableLayout->addWidget(valuesTable); tableContainer->setFixedWidth(tableWidth); contentLayout->addWidget(paramWidget, 1); contentLayout->addStretch(); contentLayout->addWidget(tableContainer, 0, Qt::AlignRight); panelLayout->addWidget(content); pageLayout->addWidget(samplingPanel); page->setEnabled(isSupportedSamplingVariable(varName)); // ===== 保存控件指针 ===== m_modeCombos.append(modeCombo); m_logChecks.append(logCheck); m_numSpins.append(numSpin); m_modelSpins.append(modelSpin); m_minSpins.append(minSpin); m_maxSpins.append(maxSpin); m_valueTables.append(valuesTable); m_unitLabels.append(unitLabel); connect(numSpin, SIGNAL(valueChanged(int)), this, SLOT(onVariableParamChanged())); connect(minSpin, SIGNAL(textChanged(QString)), this, SLOT(onVariableParamChanged())); connect(maxSpin, SIGNAL(textChanged(QString)), this, SLOT(onVariableParamChanged())); connect(minSpin, SIGNAL(editingFinished()), this, SLOT(onSamplingEditingFinished())); connect(maxSpin, SIGNAL(editingFinished()), this, SLOT(onSamplingEditingFinished())); connect(modeCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(onVariableParamChanged())); connect(logCheck, SIGNAL(toggled(bool)), this, SLOT(onVariableParamChanged())); connect(valuesTable, SIGNAL(cellClicked(int,int)), this, SLOT(onTableCellClicked(int,int))); return page; } void nmWxSensitive::updateValuesForPage(int idx) { if (idx < 0 || idx >= m_numSpins.size()) return; // 自动模式由范围生成只读表格;手动模式保留每行值,不因选中行而重算。 QTableWidget* table = m_valueTables[idx]; const bool automatic = m_modeCombos[idx]->currentIndex() == 0; m_minSpins[idx]->setEnabled(automatic); m_maxSpins[idx]->setEnabled(automatic); m_logChecks[idx]->setEnabled(automatic); const int number = m_numSpins[idx]->value(); table->setRowCount(number); nmDataSensitive::VariableSampling variable; for (int i = 0; i < m_sensitiveData.getVariableCount(); ++i) { if (findVariableIndex(m_sensitiveData.getVariable(i).getVarName()) == idx) { variable = m_sensitiveData.getVariable(i); break; } } QVector samples; const QString name = variable.getVarName(); const QString unit = m_unitLabels[idx]->text(); if (automatic) { bool minOk = false, maxOk = false; double minimum = samplingEditValue(m_minSpins[idx], &minOk); double maximum = samplingEditValue(m_maxSpins[idx], &maxOk); if (minOk && maxOk && minimum <= maximum && convertSamplingValue(name, minimum, unit, samplingBaseUnit(name), minimum) && convertSamplingValue(name, maximum, unit, samplingBaseUnit(name), maximum) && validSamplingValue(name, minimum) && validSamplingValue(name, maximum)) { variable.setMode(nmDataSensitive::VariableSampling::MODE_AUTOMATIC); variable.setNumber(number); variable.setLog(m_logChecks[idx]->isChecked()); variable.getMinValue().setValue(minimum); variable.getMaxValue().setValue(maximum); samples = variable.samplingValues(); } } for (int row = 0; row < number; ++row) { QTableWidgetItem* arrow = table->item(row, 0); if (!arrow) { arrow = new QTableWidgetItem(); table->setItem(row, 0, arrow); } arrow->setFlags(Qt::ItemIsEnabled); arrow->setIcon(row == m_selectedValueIndex ? createTriangleIcon() : QIcon()); QTableWidgetItem* item = table->item(row, 1); if (!item) { item = new QTableWidgetItem(); table->setItem(row, 1, item); } item->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter); item->setFlags(automatic ? Qt::ItemIsEnabled : Qt::ItemIsEnabled | Qt::ItemIsEditable); if (automatic) { double display = row < samples.size() ? samples[row] : 0.0; const bool valid = row < samples.size() && convertSamplingValue(name, display, samplingBaseUnit(name), unit, display); item->setData(Qt::UserRole, valid ? QVariant(display) : QVariant()); item->setText(valid ? formatSamplingValue(display, 3) : QString()); } } } void nmWxSensitive::onSamplingEditingFinished() { // 输入结束后截断越界范围,再按六位有效数字显示,计算仍使用原始精度。 QLineEdit* edit = qobject_cast(sender()); if (m_loading || !edit) return; bool ok = false; double value = samplingEditValue(edit, &ok); if (!ok || !qIsFinite(value)) return; for (int idx = 0; idx < m_minSpins.size(); ++idx) { if (edit != m_minSpins[idx] && edit != m_maxSpins[idx]) continue; for (int i = 0; i < m_sensitiveData.getVariableCount(); ++i) { const QString name = m_sensitiveData.getVariable(i).getVarName(); if (findVariableIndex(name) != idx) continue; double lower = 0.0, upper = 0.0; const QString unit = m_unitLabels[idx]->text(); const QString baseUnit = samplingBaseUnit(name); if (!samplingPhysicalRange(name, lower, upper) || !convertSamplingValue(name, value, unit, baseUnit, value)) return; value = qBound(lower, value, upper); if (!convertSamplingValue(name, value, baseUnit, unit, value)) return; setSamplingEditValue(edit, value); updateValuesForPage(idx); return; } } } void nmWxSensitive::onVariableParamChanged() { // 数据加载时不响应控件逐项赋值,编辑时只刷新所属参数页。 if (m_loading) return; for (int i = 0; i < m_numSpins.size(); ++i) { if (sender() == m_numSpins[i] || sender() == m_minSpins[i] || sender() == m_maxSpins[i] || sender() == m_modeCombos[i] || sender() == m_logChecks[i]) { m_selectedValueIndex = qMin(m_selectedValueIndex, m_numSpins[i]->value() - 1); updateValuesForPage(i); break; } } updateModelCount(); } void nmWxSensitive::onTableCellClicked(int row, int column) { Q_UNUSED(column); // 选中标记不改写表格内容,避免自动重算或丢失手动输入。 m_selectedValueIndex = row; QTableWidget* table = qobject_cast(sender()); if (!table) return; for (int i = 0; i < table->rowCount(); ++i) if (table->item(i, 0)) table->item(i, 0)->setIcon(i == row ? createTriangleIcon() : QIcon()); } void nmWxSensitive::onTreeItemChanged(QTreeWidgetItem* item, int column) { Q_UNUSED(column); if (m_loading) return; // 分组选择与子项选择双向同步,禁用的参数不参与全选和总次数。 m_loading = true; variablesTree->blockSignals(true); if (item && item->childCount() > 0) { const Qt::CheckState state = item->checkState(0); for (int i = 0; i < item->childCount(); ++i) if (!item->child(i)->isDisabled()) item->child(i)->setCheckState(0, state); } for (int group = 0; group < variablesTree->topLevelItemCount(); ++group) { QTreeWidgetItem* parent = variablesTree->topLevelItem(group); int available = 0, checked = 0; for (int i = 0; i < parent->childCount(); ++i) { if (parent->child(i)->isDisabled()) continue; ++available; if (parent->child(i)->checkState(0) == Qt::Checked) ++checked; } parent->setCheckState(0, checked == 0 ? Qt::Unchecked : checked == available ? Qt::Checked : Qt::PartiallyChecked); parent->setDisabled(available == 0); } variablesTree->blockSignals(false); m_loading = false; updateModelCount(); } void nmWxSensitive::onGenerateClicked() { if(m_calculationRunning || !m_pDataManager || !saveDataFromUI()) return; nmDataWellBase* well = m_pDataManager->findWellByCode(m_pDataManager->getPrimaryWellCode()); if(!well || well->getWellName().isEmpty()) { QMessageBox::warning(this, tr("Sensitivity"), tr("The target well is unavailable.")); return; } // 气井转换器使用本窗口所属分析的配置,不能随当前活动窗口切换。 if(m_pDataManager->getSolverModelType() == SMT_Gas_VariablePvt) { iSubWndFitting* fitting = nullptr; QMap::const_iterator it = nmDataAnalyzeManager::s_mapDataAnalManager.constBegin(); for(; it != nmDataAnalyzeManager::s_mapDataAnalManager.constEnd(); ++it) { if(it.value() == m_pDataManager) { fitting = it.key(); break; } } iAnalRun* analRun = fitting ? fitting->getAnalRun() : nullptr; if(!analRun || !analRun->configPsAbouts(true, fitting->getModelOption(), false, fitting->getAllWxPtr())) { QMessageBox::warning(this, tr("Sensitivity"), tr("Gas pseudo-pressure data is unavailable or invalid.")); return; } } QString error; QScopedPointer task; try { // 在管理器所属的界面线程捕获基准并复制一次,后台任务不持有管理器。 nmCalculationDllPebiSolverTask source(QString(), m_pDataManager, well->getWellName()); task.reset(source.createSensitivityBatch(m_sensitiveData, &error)); } catch(const std::bad_alloc&) { error = tr("Insufficient memory to prepare solver input."); } if(!task) { QMessageBox::warning(this, tr("Sensitivity"), error); return; } m_pDataManager->updateSensitiveData(m_sensitiveData); // 新任务准备成功后才替换上次临时结果,准备失败仍可保留旧结果。 delete m_pSensitivityTask; m_pSensitivityTask = task.take(); m_pSensitivityTask->setParent(this); // 进度框和停止按钮由主窗口统一管理;接管失败时不启动本次任务。 emit sigSensitivityTaskReady(this, m_pSensitivityTask); if(!m_calculationRunning) { delete m_pSensitivityTask; m_pSensitivityTask = nullptr; } } void nmWxSensitive::startSensitivityCalculation() { // 读取和清理必须等 QThread 真正结束,不能由 run() 内的提前通知触发。 connect(m_pSensitivityTask, SIGNAL(finished()), this, SLOT(onSensitivityFinished()), Qt::QueuedConnection); setCalculationRunning(true); // 仅关闭参数界面;控制器及任务仍存活,结束后继续保留临时计算结果。 hide(); m_pSensitivityTask->start(); } void nmWxSensitive::onCancelClicked() { if(m_calculationRunning && m_pSensitivityTask) { // 关闭或 Esc 也走主窗口现有停止路径,保持进度框的等待状态一致。 emit sigSensitivityCancelRequested(); return; } reject(); } void nmWxSensitive::setCalculationRunning(bool running) { // 计算过程中固定采样输入,仅保留停止操作;结束后恢复编辑与再次生成。 m_calculationRunning = running; calculationTypeCombo->setEnabled(!running); variablesTree->setEnabled(!running); detailStack->setEnabled(!running); previewButton->setEnabled(!running); cancelButton->setEnabled(!running); if(running) { generateButton->setEnabled(false); } else { updateModelCount(); } } void nmWxSensitive::onSensitivityFinished() { if(sender() != m_pSensitivityTask) return; m_pSensitivityTask->wait(); // 数据层接管曲线后释放计算任务及网格输入,结果窗口不依赖任务存活。 QSharedPointer batch = m_pSensitivityTask->getSensitivityBatchResult(); delete m_pSensitivityTask; m_pSensitivityTask = nullptr; setCalculationRunning(false); m_sensitiveData.setResults(batch); m_pDataManager->publishSensitiveResults(m_sensitiveData); const std::vector& results = batch->m_runs; QStringList errors; for(size_t i = 0; i < results.size() && errors.size() < 5; ++i) { const nmDataSensitive::RunResult& result = results[i]; if(!result.m_bSucceeded && !result.m_bCancelled) errors.append(tr("Run %1: %2").arg(result.m_nRunIndex + 1).arg(result.m_sFailureReason)); } const QString fatalError = batch->m_sFailureReason; // 实际计算错误仍需提示原因,取消组不作为错误报告。 if(fatalError.isEmpty() && errors.isEmpty()) return; QString message = fatalError; if(!errors.isEmpty()) { if(!message.isEmpty()) message += "\n\n"; message += errors.join("\n"); } QWidget* ownerWindow = parentWidget() ? parentWidget()->window() : nullptr; QMessageBox::warning(ownerWindow, tr("Sensitivity"), message); } void nmWxSensitive::onResultsClicked() { // 绘图窗口仍由主界面创建;本窗口只提供运行列表与显示选择。 onResultsChanged(); if(!resultsButton->isEnabled()) return; emit sigSensitiveResultsRequested(); if(m_resultsDialog.isNull()) { m_resultsDialog = new QDialog(this); m_resultsDialog->setAttribute(Qt::WA_DeleteOnClose); m_resultsDialog->setWindowTitle(tr("Sensitivity Results")); m_resultsDialog->resize(700, 380); QVBoxLayout* layout = new QVBoxLayout(m_resultsDialog.data()); QTableView* table = new QTableView(m_resultsDialog.data()); m_resultsModel = new SensitivityResultsModel(m_resultsDialog.data()); m_resultsModel->setResults(m_pDataManager->getSensitiveData()->getResults()); table->setModel(m_resultsModel.data()); table->setSelectionBehavior(QAbstractItemView::SelectRows); table->setSelectionMode(QAbstractItemView::SingleSelection); table->setEditTriggers(QAbstractItemView::NoEditTriggers); table->setWordWrap(false); table->verticalHeader()->setVisible(false); table->horizontalHeader()->setResizeMode(0, QHeaderView::ResizeToContents); table->horizontalHeader()->setResizeMode(1, QHeaderView::ResizeToContents); table->horizontalHeader()->setResizeMode(2, QHeaderView::Stretch); 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()); // 明确按钮顺序,避免系统的标准按钮布局把 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(apply); buttons->addWidget(ok); buttons->addWidget(cancel); layout->addLayout(buttons); connect(showAll, SIGNAL(clicked()), this, SLOT(onShowAllResults())); connect(hideAll, SIGNAL(clicked()), this, SLOT(onHideAllResults())); 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(); m_resultsDialog->show(); m_resultsDialog->raise(); m_resultsDialog->activateWindow(); } void nmWxSensitive::onResultsChanged() { // 新结果替换旧批次时,已经打开的列表也切到同一份结果。 QSharedPointer results; if(m_pDataManager && m_pDataManager->getSensitiveData()) results = m_pDataManager->getSensitiveData()->getResults(); resultsButton->setEnabled(!results.isNull()); m_sensitiveData.setResults(results); if(!m_resultsModel.isNull()) m_resultsModel->setResults(results); // 新模型清除旧批次后关闭结果列表,不能继续操作上一模型的运行记录。 if(results.isNull()) { if(!m_resultsDialog.isNull()) m_resultsDialog->close(); return; } if(!m_resultsDialog.isNull()) { const QString status = results.isNull() ? QString() : results->m_bStopped ? tr("Stopped") : results->m_bCompleted ? tr("Completed") : tr("Failed"); m_resultsDialog->setWindowTitle(tr("Sensitivity Results") + (status.isEmpty() ? QString() : " - " + status)); } } void nmWxSensitive::onApplyResults() { // 应用选择后通知主界面刷新,结果选择窗口保持打开。 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); } void nmWxSensitive::onCalculationTypeChanged(int index) { Q_UNUSED(index); // 两种确定性方法共用参数设置,仅切换总次数及组合规则。 updateModelCount(); } void nmWxSensitive::onTreeItemClicked(QTreeWidgetItem* item, int column) { Q_UNUSED(column); if (item->childCount() > 0) { detailStack->setCurrentIndex(0); return; } const int pageIndex = item->data(0, Qt::UserRole).toInt(); if (pageIndex <= 0) return; detailStack->setCurrentIndex(pageIndex); // 页面切换只更新箭头,保留用户正在编辑的采样表。 m_selectedValueIndex = 0; QTableWidget* table = m_valueTables[pageIndex - 1]; for (int row = 0; row < table->rowCount(); ++row) if (table->item(row, 0)) table->item(row, 0)->setIcon(row == 0 ? createTriangleIcon() : QIcon()); } void nmWxSensitive::onPreviewClicked() { if (!saveDataFromUI()) return; // Saphir 风格的连续运行列表,组合数再大也不显示分页控件。 QDialog dialog(this); dialog.setWindowTitle(tr("Sensitivity Runs")); dialog.setWindowFlags(dialog.windowFlags() & ~Qt::WindowContextHelpButtonHint); dialog.setMinimumSize(420, 360); dialog.resize(470, 650); QVBoxLayout* layout = new QVBoxLayout(&dialog); layout->setContentsMargins(16, 16, 16, 12); layout->setSpacing(8); m_previewView = new SensitivityRunsView(m_sensitiveData, createTriangleIcon(), &dialog); layout->addWidget(m_previewView); QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Close, Qt::Horizontal, &dialog); buttons->setCenterButtons(true); buttons->button(QDialogButtonBox::Close)->setMinimumWidth(110); layout->addWidget(buttons); connect(buttons, SIGNAL(rejected()), &dialog, SLOT(reject())); connect(m_previewView->verticalScrollBar(), SIGNAL(actionTriggered(int)), this, SLOT(onPreviewScrollAction(int))); dialog.exec(); m_previewView = nullptr; } void nmWxSensitive::onPreviewScrollAction(int action) { // 将原生滚动条的单步、整屏和拖动操作转交连续列表。 if (m_previewView) m_previewView->scrollAction(action); }