From f3b970f938bbff55fe8a089705b138bc3beb7d2b Mon Sep 17 00:00:00 2001 From: lvjunjie Date: Mon, 26 Oct 2026 10:35:59 +0800 Subject: [PATCH] =?UTF-8?q?feat(nmNum):=20=E4=BC=98=E5=8C=96=E6=95=8F?= =?UTF-8?q?=E6=84=9F=E6=80=A7=E5=88=86=E6=9E=90=E8=AE=BE=E7=BD=AE=E7=95=8C?= =?UTF-8?q?=E9=9D=A2=E5=8F=8A=E7=BB=93=E6=9E=9C=E7=AA=97=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 调整敏感性分析布局、表格样式及单位切换。 - 统一图表白色背景,修复结果窗口最小化关闭后的尺寸恢复。 --- Include/nmNum/nmSubWxs/nmParameterField.h | 3 + Include/nmNum/nmSubWxs/nmWxSensitive.h | 3 +- Src/nmNum/nmSubWnd/nmSubWndMain.cpp | 5 +- Src/nmNum/nmSubWxs/nmParameterField.cpp | 8 +- .../nmSubWxs/nmWxAutomaticFittingStart.cpp | 5 +- Src/nmNum/nmSubWxs/nmWxSensitive.cpp | 373 +++++++++++------- 6 files changed, 246 insertions(+), 151 deletions(-) diff --git a/Include/nmNum/nmSubWxs/nmParameterField.h b/Include/nmNum/nmSubWxs/nmParameterField.h index 1b9c9b20..44fefde8 100644 --- a/Include/nmNum/nmSubWxs/nmParameterField.h +++ b/Include/nmNum/nmSubWxs/nmParameterField.h @@ -119,6 +119,9 @@ public: /** @brief 返回数值编辑列的统一最小宽度。 */ static int editorMinimumWidth(); + /** @brief 将单位别名及上标写法统一到框架单位表中的名称。 */ + static QString resolveUnitSpelling(const QString& sUnit); + /** @brief 返回单位列的统一最小宽度。 */ static int unitMinimumWidth(); diff --git a/Include/nmNum/nmSubWxs/nmWxSensitive.h b/Include/nmNum/nmSubWxs/nmWxSensitive.h index eee7b937..485de2f3 100644 --- a/Include/nmNum/nmSubWxs/nmWxSensitive.h +++ b/Include/nmNum/nmSubWxs/nmWxSensitive.h @@ -130,6 +130,7 @@ protected: void onTreeItemClicked(QTreeWidgetItem *item, int column); void onVariableParamChanged(); void onSamplingEditingFinished(); + void onSamplingUnitChanged(); void onTableCellClicked(int row, int column); void onTreeItemChanged(QTreeWidgetItem *item, int column); void onPreviewClicked(); @@ -178,7 +179,7 @@ private: QList m_minSpins; QList m_maxSpins; QList m_valueTables; - QList m_unitLabels; + QList m_unitCombos; QList m_numLabels, m_minLabels, m_maxLabels; QList m_distributionCombos; QList m_distributionPanels; diff --git a/Src/nmNum/nmSubWnd/nmSubWndMain.cpp b/Src/nmNum/nmSubWnd/nmSubWndMain.cpp index 9976120b..85cc8ef4 100644 --- a/Src/nmNum/nmSubWnd/nmSubWndMain.cpp +++ b/Src/nmNum/nmSubWnd/nmSubWndMain.cpp @@ -2155,6 +2155,10 @@ void nmSubWndMain::onReadonlyCurveDoubleClicked( bool nmSubWndMain::eventFilter(QObject* watched, QEvent* event) { + // 最小化时先恢复窗口状态,再交给默认关闭流程隐藏,避免复用时丢失正常尺寸。 + QDialog* dialog = qobject_cast(watched); + if(dialog && event->type() == QEvent::Close && dialog->isMinimized()) + dialog->setWindowState(dialog->windowState() & ~Qt::WindowMinimized); // 关闭结果对话框时同步关闭其曲线数据表、Show 和统计设置窗口。 if((event->type() == QEvent::Close || event->type() == QEvent::Hide) && qobject_cast(watched)) { @@ -2164,7 +2168,6 @@ bool nmSubWndMain::eventFilter(QObject* watched, QEvent* event) if(dialog->isWindow()) dialog->close(); } // 独立窗口不再经过 MDI 的画布适配,显示和缩放时适配自己的绘图区。 - QDialog* dialog = qobject_cast(watched); if(dialog && (event->type() == QEvent::Resize || event->type() == QEvent::Show)) { dialog->layout()->activate(); foreach(iGuiPlot* plot, dialog->findChildren()) diff --git a/Src/nmNum/nmSubWxs/nmParameterField.cpp b/Src/nmNum/nmSubWxs/nmParameterField.cpp index 30765f87..bc2a9693 100644 --- a/Src/nmNum/nmSubWxs/nmParameterField.cpp +++ b/Src/nmNum/nmSubWxs/nmParameterField.cpp @@ -221,7 +221,7 @@ void resolveParameterUnitSpelling(iParameter* pParameter) } // 参数XML和单位XML可能使用不同排版写法,绑定前切到当前单位表可识别的单位名。 - QString sResolvedUnit = resolveAvailableUnitSpelling(pParameter->m_sUnit); + QString sResolvedUnit = nmParameterField::resolveUnitSpelling(pParameter->m_sUnit); if (sResolvedUnit != pParameter->m_sUnit) { pParameter->m_sUnit = sResolvedUnit; @@ -341,6 +341,12 @@ int nmParameterField::editorMinimumWidth() return 140; } +QString nmParameterField::resolveUnitSpelling(const QString& sUnit) +{ + // 参数编辑与采样界面共用名称解析,避免普通指数与上标写法匹配到不同单位。 + return resolveAvailableUnitSpelling(sUnit); +} + int nmParameterField::unitMinimumWidth() { return 86; diff --git a/Src/nmNum/nmSubWxs/nmWxAutomaticFittingStart.cpp b/Src/nmNum/nmSubWxs/nmWxAutomaticFittingStart.cpp index be59f969..3624112c 100644 --- a/Src/nmNum/nmSubWxs/nmWxAutomaticFittingStart.cpp +++ b/Src/nmNum/nmSubWxs/nmWxAutomaticFittingStart.cpp @@ -181,7 +181,8 @@ void nmAutomaticFittingCurveChart::paintEvent(QPaintEvent* event) QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing); - painter.fillRect(rect(), QColor(240, 240, 240)); + // 图表内外统一使用白色背景。 + painter.fillRect(rect(), Qt::white); const int marginLeft = 62; const int marginTop = 18; @@ -191,7 +192,7 @@ void nmAutomaticFittingCurveChart::paintEvent(QPaintEvent* event) qMax(10, width() - marginLeft - marginRight), qMax(10, height() - marginTop - marginBottom)); - painter.fillRect(m_chartRect, QColor(252, 252, 252)); + painter.fillRect(m_chartRect, Qt::white); drawGrid(painter); drawAxes(painter); drawCurveLine(painter, m_bestPressure, QColor(250, 74, 74)); diff --git a/Src/nmNum/nmSubWxs/nmWxSensitive.cpp b/Src/nmNum/nmSubWxs/nmWxSensitive.cpp index 9ddcdc6f..da56298f 100644 --- a/Src/nmNum/nmSubWxs/nmWxSensitive.cpp +++ b/Src/nmNum/nmSubWxs/nmWxSensitive.cpp @@ -2,6 +2,9 @@ #include "nmDataAnalyzeManager.h" #include "nmWxAutomaticFitting.h" +#include "nmParameterField.h" +#include "ZxBaseUtil.h" +#include #include "nmDataSensitive.h" #include "nmDataReservoir.h" #include "nmDataWellBase.h" @@ -122,21 +125,33 @@ QString samplingBaseUnit(const QString& name) } bool convertSamplingValue(const QString& name, double value, - QString from, QString to, double& converted) -{ - // 优先复用项目单位组;旧配置的 psi 按绝对压力单位迁移。 - if (from == "psi") from = "psia"; - if (to == "psi") to = "psia"; + QString from, QString to, double& converted, bool difference = false) +{ + // 标准差是差值,单位换算只应用比例,不带入表压和绝压之间的偏移。 + if (difference) { + double zero = 0.0; + if (!convertSamplingValue(name, 0.0, from, to, zero) || + !convertSamplingValue(name, value, from, to, converted)) return false; + converted -= zero; + return qIsFinite(converted); + } + // 优先使用框架单位组中的原始名称,兼容下拉框内的全部单位。 if (from == "dimensionless") from.clear(); if (to == "dimensionless") to.clear(); if (from == to || (from.isEmpty() && to.isEmpty())) { converted = value; return qIsFinite(converted); } + // 与井参数编辑共用名称解析,使 m^3/MPa 等旧写法也能参与框架换算。 + from = nmParameterField::resolveUnitSpelling(from); + to = nmParameterField::resolveUnitSpelling(to); 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 (from == "psi") from = "psia"; + if (to == "psi") to = "psia"; 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); @@ -984,7 +999,7 @@ protected: { // 蓝线为累积概率,红色虚线为按峰值归一化的概率密度,与 Saphir 的显示一致。 QPainter painter(this); - painter.fillRect(rect(), palette().color(QPalette::Window)); + painter.fillRect(rect(), Qt::white); painter.setRenderHint(QPainter::Antialiasing); QVector x(201), cumulativeValues(201); const bool bounded = m_variable.m_distribution >= nmDataSensitive::VariableSampling::TRUNCATED_NORMAL; @@ -1005,7 +1020,7 @@ protected: painter.drawText(rect(), Qt::AlignCenter, nmWxSensitive::tr("Invalid distribution parameters")); return; } - // 坐标外侧沿用窗口底色,绘图区留白,标题放在纵轴左侧。 + // 坐标内外统一使用白底,标题放在纵轴左侧。 const QRectF plot(76, 12, width() - 96, height() - 66); painter.fillRect(plot, Qt::white); const QRectF dataArea = plot.adjusted(12, 16, -12, -16); @@ -1526,7 +1541,9 @@ void nmWxSensitive::refreshModelValues() continue; } const QString baseUnit = samplingBaseUnit(name); - QString displayUnit = attribute->getUnit(); + // 已保存的采样显示单位独立于模型单位,重开及读取项目时继续沿用。 + QString displayUnit = !fresh && m_sensitiveData.usesBaseUnits() + ? variable.getModelValue().getUnit() : attribute->getUnit(); if (displayUnit == "dimensionless") displayUnit.clear(); double converted = 0.0; if (!convertSamplingValue(name, attribute->getValue().toDouble(), baseUnit, displayUnit, converted)) @@ -1630,7 +1647,7 @@ void nmWxSensitive::reloadInputData() while(detailStack->count() > 1) delete detailStack->widget(1); m_modeCombos.clear(); m_logChecks.clear(); m_numSpins.clear(); m_modelSpins.clear(); m_minSpins.clear(); m_maxSpins.clear(); - m_valueTables.clear(); m_unitLabels.clear(); + m_valueTables.clear(); m_unitCombos.clear(); m_numLabels.clear(); m_minLabels.clear(); m_maxLabels.clear(); m_distributionCombos.clear(); m_distributionPanels.clear(); m_distributionPlots.clear(); populateTreeWidget(); @@ -1672,9 +1689,17 @@ void nmWxSensitive::loadDataToUI(bool refreshRangesOnly) m_numSpins[idx]->setValue(qMax(1, variable.getNumber())); } // 已打开窗口沿用原显示单位,避免手动表格的数值被换成另一种单位解释。 - const QString unit = refreshRangesOnly ? m_unitLabels[idx]->text() : variable.getModelValue().getUnit(); + const QString unit = nmParameterField::resolveUnitSpelling( + refreshRangesOnly ? m_unitCombos[idx]->currentText() : variable.getModelValue().getUnit()); variable.getModelValue().setUnit(unit); - m_unitLabels[idx]->setText(unit); + QComboBox* unitCombo = m_unitCombos[idx]; + unitCombo->clear(); + iUnitGroup* unitGroup = iUnitHelper::getUnitGroupByUnit( + nmParameterField::resolveUnitSpelling(samplingBaseUnit(name))); + if (unitGroup) unitCombo->addItems(unitGroup->getAllUnitNames()); + if (unitCombo->findText(unit) < 0) unitCombo->addItem(unit); + unitCombo->setCurrentIndex(unitCombo->findText(unit)); + unitCombo->setVisible(!unit.isEmpty()); double model = variable.getModelValue().getValue().toDouble(); double minimum = variable.getMinValue().getValue().toDouble(); double maximum = variable.getMaxValue().getValue().toDouble(); @@ -1689,7 +1714,7 @@ void nmWxSensitive::loadDataToUI(bool refreshRangesOnly) variable.m_distributionMin, variable.m_distributionMax}; for(int field = 0; field < 5; ++field) { double display = distributionValues[field]; - convertSamplingValue(name, display, samplingBaseUnit(name), unit, display); + convertSamplingValue(name, display, samplingBaseUnit(name), unit, display, field == 1); setSamplingEditValue(m_distributionPanels[idx]->findChild(distributionFields[field]), display); } double displayMin = variable.getModelValue().getMinValue().toDouble(); @@ -1795,8 +1820,9 @@ bool nmWxSensitive::saveDataFromUI() const QString name = variable.getVarName(); const int idx = findVariableIndex(variable); if (idx < 0 || !isSupportedSamplingVariable(name)) continue; - const QString unit = m_unitLabels[idx]->text(); + const QString unit = m_unitCombos[idx]->currentText(); const QString baseUnit = samplingBaseUnit(name); + variable.getModelValue().setUnit(unit); bool valid = true; if(candidate.getCalculationType() == nmDataSensitive::CALC_MONTE_CARLO) { double median = 0.0; @@ -1870,15 +1896,21 @@ void nmWxSensitive::setupUI() { setWindowTitle(tr("Sensitivity")); setMinimumSize(900, 550); - // 默认放大窗口,同时为屏幕边缘和窗口标题栏留出空间。 + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + // 适当加宽窗口,为整页单位下拉框和分布图保留空间。 const QSize available = QApplication::desktop()->availableGeometry(this).size() - QSize(60, 80); - resize(QSize(1250, 760).boundedTo(available)); + resize(QSize(1120, 660).boundedTo(available)); + // 沿用储层参数窗口的分组框、内容边距和底部按钮间距。 QVBoxLayout *mainLayout = new QVBoxLayout(this); + mainLayout->setContentsMargins(12, 10, 12, 0); + mainLayout->setSpacing(8); // ===== 顶部:Calculation type ===== - QHBoxLayout *calcTypeLayout = new QHBoxLayout(); - QLabel *calcTypeLabel = new QLabel(tr("Calculation type:"), this); + QGroupBox* calcTypeGroup = new QGroupBox(tr("Calculation type:"), this); + QHBoxLayout *calcTypeLayout = new QHBoxLayout(calcTypeGroup); + calcTypeLayout->setContentsMargins(10, 10, 10, 10); + calcTypeLayout->setSpacing(8); calculationTypeCombo = new QComboBox(this); calculationTypeCombo->addItem(tr("Deterministic"), nmDataSensitive::CALC_DETERMINISTIC); calculationTypeCombo->addItem(tr("Deterministic multivariate"), nmDataSensitive::CALC_DETERMINISTIC_MULTIVARIATE); @@ -1890,58 +1922,20 @@ void nmWxSensitive::setupUI() m_improveSettingsButton->setFixedHeight(calculationTypeCombo->sizeHint().height()); m_improveSettingsButton->hide(); - calcTypeLayout->addWidget(calcTypeLabel); calcTypeLayout->addWidget(calculationTypeCombo); calcTypeLayout->addWidget(m_improveSettingsButton); calcTypeLayout->addStretch(); - mainLayout->addLayout(calcTypeLayout); + mainLayout->addWidget(calcTypeGroup); // ===== 中间:左 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); - + // 左侧参数树使用标准分组框,样式交给主软件统一处理。 + QGroupBox *variablesPanel = new QGroupBox(tr("Variables"), this); + variablesPanel->setMinimumWidth(200); 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(); + variablesPanelLayout->setContentsMargins(10, 10, 10, 10); // Tree 区域 variablesTree = new QTreeWidget(variablesPanel); @@ -1949,7 +1943,6 @@ void nmWxSensitive::setupUI() variablesTree->setFrameShape(QFrame::NoFrame); // 外层已经有 frame variablesTree->setMinimumHeight(350); - variablesPanelLayout->addWidget(varHeader); variablesPanelLayout->addWidget(variablesTree, 1); // 把左侧 panel 加进 splitter @@ -1975,12 +1968,14 @@ void nmWxSensitive::setupUI() // 左边宽度基本固定,右边自适应 centerSplitter->setStretchFactor(0, 0); centerSplitter->setStretchFactor(1, 1); - centerSplitter->setSizes(QList() << 260 << 960); + centerSplitter->setSizes(QList() << 220 << 860); mainLayout->addWidget(centerSplitter, 1); // ===== 底部:Total number of models + 按钮 ===== QHBoxLayout *bottomLayout = new QHBoxLayout(); + bottomLayout->setContentsMargins(4, 5, 4, 5); + bottomLayout->setSpacing(8); QLabel *totalLabel = new QLabel(tr("Total number of models:"), this); modelCountLabel = new QLabel(tr("0"), this); @@ -2004,6 +1999,8 @@ void nmWxSensitive::setupUI() generateButton = new QPushButton(tr("Generate"), this); cancelButton = new QPushButton(tr("Cancel"), this); + generateButton->setIcon(zxLoadIcon("OK")); + cancelButton->setIcon(zxLoadIcon("Cancel")); // 查看已有计算结果,不改变采样配置,也不重新求解。 m_resultPlotsButton = new QPushButton(tr("Result plots"), this); @@ -2149,48 +2146,22 @@ QWidget *nmWxSensitive::createVariablePage(const QString &varName) pageLayout->setContentsMargins(0, 0, 0, 0); pageLayout->setSpacing(0); - // ===== 整个 Sampling 面板:QFrame + 标题栏 + 内容 ===== - QFrame *samplingPanel = new QFrame(page); - samplingPanel->setFrameShape(QFrame::StyledPanel); - samplingPanel->setFrameShadow(QFrame::Raised); - + // 采样页与左侧参数树统一使用标准分组框。 + QGroupBox *samplingPanel = new QGroupBox(tr("Sampling %1").arg(varName), page); 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); + panelLayout->setContentsMargins(10, 10, 10, 10); + panelLayout->setSpacing(8); // ---- 内容区:左参数 + 右表格 ---- QWidget *content = new QWidget(samplingPanel); QHBoxLayout *contentLayout = new QHBoxLayout(content); - contentLayout->setContentsMargins(6, 0, 6, 6); + contentLayout->setContentsMargins(0, 0, 0, 0); contentLayout->setSpacing(12); // 左:参数区(GridLayout) QGridLayout *paramLayout = new QGridLayout(); - paramLayout->setVerticalSpacing(6); + paramLayout->setVerticalSpacing(8); + paramLayout->setHorizontalSpacing(8); int row = 0; // Mode + Log? @@ -2208,7 +2179,6 @@ QWidget *nmWxSensitive::createVariablePage(const QString &varName) distributionCombo->setObjectName("sensitivityDistribution_" + varName); distributionCombo->addItems(QStringList() << tr("Normal") << tr("Lognormal") << tr("Truncated normal") << tr("Truncated lognormal") << tr("Uniform") << tr("Triangular")); - distributionCombo->setFixedWidth(180); modeRowLayout->addWidget(distributionCombo); modeRowLayout->addWidget(logCheck); modeRowLayout->addStretch(); @@ -2228,11 +2198,6 @@ QWidget *nmWxSensitive::createVariablePage(const QString &varName) paramLayout->addWidget(numSpin, row, 1); ++row; - // 模式、数量与下方采样参数之间留出约三行空白。 - int gapRow = row; - paramLayout->setRowMinimumHeight(gapRow, numSpin->sizeHint().height() * 3); - ++row; - // Model value + 单位 QLabel *modelLabel = new QLabel(tr("Model value:"), content); QLineEdit *modelSpin = new QLineEdit(content); @@ -2240,13 +2205,18 @@ QWidget *nmWxSensitive::createVariablePage(const QString &varName) // 基准值来自真实模型,只作参照;输入框支持科学计数法和完整精度。 modelSpin->setReadOnly(true); - QLabel *unitLabel = new QLabel(tr(""), content); // ⭐ 单位也从数据加载 + // 当前参数整页共用一个单位,下拉选项和换算均复用框架单位组。 + QComboBox *unitCombo = new QComboBox(content); + unitCombo->setObjectName("sensitivityUnit_" + varName); + unitCombo->setMinimumWidth(86); + unitCombo->setMaximumWidth(120); + unitCombo->setFixedHeight(25); QWidget *modelRowWidget = new QWidget(content); QHBoxLayout *modelRowLayout = new QHBoxLayout(modelRowWidget); modelRowLayout->setContentsMargins(0, 0, 0, 0); modelRowLayout->addWidget(modelSpin); - modelRowLayout->addWidget(unitLabel); + modelRowLayout->addWidget(unitCombo); modelRowLayout->addStretch(); paramLayout->addWidget(modelLabel, row, 0); @@ -2257,9 +2227,8 @@ QWidget *nmWxSensitive::createVariablePage(const QString &varName) QWidget* distributionPanel = new QWidget(content); QGridLayout* distributionLayout = new QGridLayout(distributionPanel); distributionLayout->setContentsMargins(0, 0, 0, 0); - distributionLayout->setHorizontalSpacing(6); + distributionLayout->setHorizontalSpacing(8); distributionLayout->setVerticalSpacing(0); - distributionLayout->setColumnMinimumWidth(0, modelLabel->sizeHint().width()); const int fieldHeight = modelSpin->sizeHint().height(); // Mean 与三角分布的 Mode 共用一行,其余字段保持固定位置。 distributionPanel->setFixedHeight(fieldHeight * 4 + 6 * 3); @@ -2299,9 +2268,17 @@ QWidget *nmWxSensitive::createVariablePage(const QString &varName) paramLayout->addWidget(maxSpin, row, 1); ++row; - // 统一编辑框宽度 - int fullWidth = modelSpin->sizeHint().width(); - int editWidth = qMax(150, fullWidth); + // 基本参数与分布参数使用相同标签列宽、列间距和输入宽度,保证上下对齐。 + QList labels; + labels << modeLabel << numLabel << modelLabel << minLabel << maxLabel; + labels.append(distributionPanel->findChildren()); + int labelWidth = 0; + foreach(QLabel* label, labels) labelWidth = qMax(labelWidth, label->sizeHint().width()); + foreach(QLabel* label, labels) label->setFixedWidth(labelWidth); + const int editWidth = qMax(180, modelSpin->sizeHint().width()); + distributionCombo->setFixedWidth(editWidth); + foreach(QLineEdit* edit, distributionPanel->findChildren()) + edit->setFixedWidth(editWidth); modeCombo->setFixedWidth(editWidth); numSpin->setFixedWidth(editWidth); @@ -2317,7 +2294,7 @@ QWidget *nmWxSensitive::createVariablePage(const QString &varName) QWidget *paramWidget = new QWidget(content); QVBoxLayout *leftVBox = new QVBoxLayout(paramWidget); - leftVBox->setContentsMargins(0, 6, 0, 0); + leftVBox->setContentsMargins(0, 0, 0, 0); leftVBox->addLayout(paramLayout); leftVBox->addStretch(); @@ -2329,27 +2306,40 @@ QWidget *nmWxSensitive::createVariablePage(const QString &varName) headers << "" << tr("Values"); valuesTable->setHorizontalHeaderLabels(headers); - valuesTable->verticalHeader()->setVisible(false); + // 沿用编辑井的行号、隔行底色和网格样式,保留采样行的箭头标记。 + valuesTable->verticalHeader()->setVisible(true); + valuesTable->verticalHeader()->setMovable(false); + valuesTable->verticalHeader()->setDefaultAlignment(Qt::AlignCenter); + valuesTable->verticalHeader()->setMinimumWidth(18); QHeaderView *hh = valuesTable->horizontalHeader(); hh->setResizeMode(QHeaderView::Fixed); hh->setHighlightSections(false); - // 箭头列固定,数值列在下方设置为填满加宽后的表格。 valuesTable->setColumnWidth(0, 15); - 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->setAlternatingRowColors(true); + valuesTable->setStyleSheet( + "QTableWidget {" + " background-color: palette(base);" + " alternate-background-color: rgb(232, 232, 232);" + " gridline-color: palette(midlight);" + " border: 1px solid palette(mid);" + "}" + "QHeaderView {" + " background-color: palette(button);" + " color: palette(button-text);" + "}" + "QHeaderView::section {" + " background-color: palette(button);" + " color: palette(button-text);" + " border: none;" + " border-right: 1px solid palette(mid);" + " border-bottom: 1px solid palette(mid);" + "}" + "QTableCornerButton::section {" + " background-color: palette(base);" + " border: none;" + " border-right: 1px solid palette(mid);" + " border-bottom: 1px solid palette(mid);" + "}"); valuesTable->horizontalHeader()->setStretchLastSection(true); valuesTable->setEditTriggers(QAbstractItemView::DoubleClicked); valuesTable->setSelectionBehavior(QAbstractItemView::SelectRows); @@ -2360,22 +2350,27 @@ QWidget *nmWxSensitive::createVariablePage(const QString &varName) valuesTable->setItemDelegateForColumn(1, new RightAlignDelegate(valuesTable)); - // 初始空表的列宽尚未稳定,直接固定容器宽度,不从未显示的表头反算。 - const int tableWidth = 190; - valuesTable->setFixedWidth(tableWidth); + // 与编辑井一样使用可拖动分栏,表格和分布图共用右侧空间。 + QSplitter* samplingSplitter = new QSplitter(Qt::Horizontal, content); + samplingSplitter->setChildrenCollapsible(false); + samplingSplitter->setHandleWidth(6); + // 分栏仍可拖动,但不绘制参数与图表之间的灰色竖线。 + samplingSplitter->setStyleSheet("QSplitter::handle { background: transparent; border: none; }"); + samplingSplitter->addWidget(paramWidget); - QWidget *tableContainer = new QWidget(content); + QWidget *tableContainer = new QWidget(samplingSplitter); 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); - SensitivityDistributionPlot* distributionPlot = new SensitivityDistributionPlot(content); - contentLayout->addWidget(distributionPlot, 2); + valuesTable->setMinimumWidth(190); + SensitivityDistributionPlot* distributionPlot = new SensitivityDistributionPlot(tableContainer); + tableLayout->addWidget(distributionPlot); + samplingSplitter->addWidget(tableContainer); + samplingSplitter->setStretchFactor(0, 1); + samplingSplitter->setStretchFactor(1, 0); + samplingSplitter->setSizes(QList() << 630 << 210); + contentLayout->addWidget(samplingSplitter); panelLayout->addWidget(content); pageLayout->addWidget(samplingPanel); @@ -2389,7 +2384,8 @@ QWidget *nmWxSensitive::createVariablePage(const QString &varName) m_minSpins.append(minSpin); m_maxSpins.append(maxSpin); m_valueTables.append(valuesTable); - m_unitLabels.append(unitLabel); + m_unitCombos.append(unitCombo); + connect(unitCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(onSamplingUnitChanged())); m_numLabels.append(numLabel); m_minLabels.append(minLabel); m_maxLabels.append(maxLabel); @@ -2431,13 +2427,23 @@ void nmWxSensitive::updateValuesForPage(int idx) m_maxLabels[idx]->setVisible(!monteCarlo); m_minSpins[idx]->setVisible(!monteCarlo); m_maxSpins[idx]->setVisible(!monteCarlo); - m_valueTables[idx]->parentWidget()->setVisible(!monteCarlo); + m_valueTables[idx]->setVisible(!monteCarlo); m_distributionCombos[idx]->setVisible(monteCarlo); m_distributionPanels[idx]->setVisible(monteCarlo); m_distributionPlots[idx]->setVisible(monteCarlo); - // Monte-Carlo 参数区只占所需宽度,窗口增宽时优先扩展分布图。 - QHBoxLayout* contentLayout = static_cast(m_distributionPlots[idx]->parentWidget()->layout()); - contentLayout->setStretch(0, monteCarlo ? 0 : 1); + // 表格采用较窄的默认宽度;分布图让参数区仅占所需宽度,避免沿用表格分栏。 + // 只在布局类型切换时调整,编辑参数时保留用户拖动后的宽度。 + QSplitter* splitter = static_cast(m_valueTables[idx]->parentWidget()->parentWidget()); + const QVariant previousLayout = splitter->property("monteCarloLayout"); + if (!previousLayout.isValid() || previousLayout.toBool() != monteCarlo) { + splitter->setProperty("monteCarloLayout", monteCarlo); + splitter->setStretchFactor(0, monteCarlo ? 0 : 1); + splitter->setStretchFactor(1, monteCarlo ? 1 : 0); + const int availableWidth = qMax(840, splitter->width() - splitter->handleWidth()); + const int parameterWidth = monteCarlo ? splitter->widget(0)->minimumSizeHint().width() + : availableWidth - 210; + splitter->setSizes(QList() << parameterWidth << availableWidth - parameterWidth); + } if(monteCarlo) { updateDistributionPage(idx); return; } // 自动模式由范围生成只读表格;手动模式保留每行值,不因选中行而重算。 QTableWidget* table = m_valueTables[idx]; @@ -2456,7 +2462,7 @@ void nmWxSensitive::updateValuesForPage(int idx) } QVector samples; const QString name = variable.getVarName(); - const QString unit = m_unitLabels[idx]->text(); + const QString unit = m_unitCombos[idx]->currentText(); if (automatic) { bool minOk = false, maxOk = false; double minimum = samplingEditValue(m_minSpins[idx], &minOk); @@ -2497,7 +2503,7 @@ bool nmWxSensitive::readDistributionPage(int idx, nmDataSensitive::VariableSampl // 字段转换沿用现有参数单位,仅校验当前分布实际使用的字段。 variable.m_distribution = static_cast( m_distributionCombos[idx]->currentIndex()); - variable.m_modelValue.setUnit(m_unitLabels[idx]->text()); + variable.m_modelValue.setUnit(m_unitCombos[idx]->currentText()); double* fields[] = {&variable.m_mean, &variable.m_stdDev, &variable.m_peak, &variable.m_distributionMin, &variable.m_distributionMax}; const int distribution = variable.m_distribution; @@ -2509,7 +2515,7 @@ bool nmWxSensitive::readDistributionPage(int idx, nmDataSensitive::VariableSampl bool ok = false; double value = samplingEditValue(m_distributionPanels[idx]->findChild(distributionFields[field]), &ok); if(!ok || !qIsFinite(value) || !convertSamplingValue(variable.m_varName, value, - m_unitLabels[idx]->text(), samplingBaseUnit(variable.m_varName), *fields[field])) return false; + m_unitCombos[idx]->currentText(), samplingBaseUnit(variable.m_varName), *fields[field], field == 1)) return false; } return true; } @@ -2535,6 +2541,81 @@ void nmWxSensitive::updateDistributionPage(int idx) } } +void nmWxSensitive::onSamplingUnitChanged() +{ + if (m_loading) return; + QComboBox* combo = qobject_cast(sender()); + const int idx = m_unitCombos.indexOf(combo); + if (idx < 0) return; + for (int i = 0; i < m_sensitiveData.getVariableCount(); ++i) { + nmDataSensitive::VariableSampling& variable = m_sensitiveData.getVariable(i); + if (findVariableIndex(variable) != idx) continue; + const QString from = variable.getModelValue().getUnit(); + const QString to = combo->currentText(); + if (from == to) return; + + // 先换算当前界面草稿,全部成功后再更新,避免覆盖未提交的手动输入。 + QList edits; + edits << m_modelSpins[idx] << m_minSpins[idx] << m_maxSpins[idx]; + for (int field = 0; field < 5; ++field) + edits << m_distributionPanels[idx]->findChild(distributionFields[field]); + QVector values(edits.size()); + bool valid = true; + for (int field = 0; valid && field < edits.size(); ++field) { + bool ok = false; + const double value = samplingEditValue(edits[field], &ok); + valid = ok && convertSamplingValue(variable.m_varName, value, from, to, values[field], field == 4); + } + QTableWidget* table = m_valueTables[idx]; + QVector tableValues(table->rowCount()); + for (int row = 0; valid && row < table->rowCount(); ++row) { + QTableWidgetItem* item = table->item(row, 1); + if (!item || item->text().isEmpty()) continue; + bool ok = false; + const double value = item->data(Qt::UserRole).isValid() + ? item->data(Qt::UserRole).toDouble(&ok) : item->text().trimmed().toDouble(&ok); + valid = ok && convertSamplingValue(variable.m_varName, value, from, to, tableValues[row]); + } + if (!valid) { + combo->blockSignals(true); + combo->setCurrentIndex(combo->findText(from)); + combo->blockSignals(false); + double lower = variable.getModelValue().getMinValue().toDouble(); + double upper = variable.getModelValue().getMaxValue().toDouble(); + convertSamplingValue(variable.m_varName, lower, samplingBaseUnit(variable.m_varName), from, lower); + convertSamplingValue(variable.m_varName, upper, samplingBaseUnit(variable.m_varName), from, upper); + QMessageBox::warning(this, tr("Sensitivity"), + tr("%1: invalid sampling values. Allowed range: [%2, %3] %4. Check the sampling settings.") + .arg(variable.m_varName).arg(formatSamplingValue(lower, 6)) + .arg(formatSamplingValue(upper, 6)).arg(from)); + return; + } + + // 仅更新显示单位和显示值,基准采样数据及已生成的随机样本保持不变。 + m_loading = true; + variable.getModelValue().setUnit(to); + for (int field = 0; field < edits.size(); ++field) + setSamplingEditValue(edits[field], values[field]); + for (int row = 0; row < table->rowCount(); ++row) { + QTableWidgetItem* item = table->item(row, 1); + if (!item || item->text().isEmpty()) continue; + item->setData(Qt::UserRole, tableValues[row]); + item->setText(formatSamplingValue(tableValues[row], 3)); + } + double lower = variable.getModelValue().getMinValue().toDouble(); + double upper = variable.getModelValue().getMaxValue().toDouble(); + convertSamplingValue(variable.m_varName, lower, samplingBaseUnit(variable.m_varName), to, lower); + convertSamplingValue(variable.m_varName, upper, samplingBaseUnit(variable.m_varName), to, upper); + const QString rangeHint = tr("Allowed range: [%1, %2] %3") + .arg(formatSamplingValue(lower, 6)).arg(formatSamplingValue(upper, 6)).arg(to); + m_minSpins[idx]->setToolTip(rangeHint); + m_maxSpins[idx]->setToolTip(rangeHint); + updateValuesForPage(idx); + m_loading = false; + return; + } +} + void nmWxSensitive::onSamplingEditingFinished() { // 输入结束后截断越界范围,再按六位有效数字显示,计算仍使用原始精度。 @@ -2550,7 +2631,7 @@ void nmWxSensitive::onSamplingEditingFinished() const QString name = variable.getVarName(); if (findVariableIndex(variable) != idx) continue; double lower = 0.0, upper = 0.0; - const QString unit = m_unitLabels[idx]->text(); + const QString unit = m_unitCombos[idx]->currentText(); const QString baseUnit = samplingBaseUnit(name); if (!samplingPhysicalRange(name, lower, upper) || !convertSamplingValue(name, value, unit, baseUnit, value)) return;