feat(nmNum): 优化敏感性分析设置界面及结果窗口

- 调整敏感性分析布局、表格样式及单位切换。

- 统一图表白色背景,修复结果窗口最小化关闭后的尺寸恢复。
feature/Forecast-Sensitivity-Dialog-20261022
lvjunjie 5 days ago
parent 0cb358db52
commit f3b970f938

@ -119,6 +119,9 @@ public:
/** @brief 返回数值编辑列的统一最小宽度。 */
static int editorMinimumWidth();
/** @brief 将单位别名及上标写法统一到框架单位表中的名称。 */
static QString resolveUnitSpelling(const QString& sUnit);
/** @brief 返回单位列的统一最小宽度。 */
static int unitMinimumWidth();

@ -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<QLineEdit*> m_minSpins;
QList<QLineEdit*> m_maxSpins;
QList<QTableWidget*> m_valueTables;
QList<QLabel*> m_unitLabels;
QList<QComboBox*> m_unitCombos;
QList<QLabel*> m_numLabels, m_minLabels, m_maxLabels;
QList<QComboBox*> m_distributionCombos;
QList<QWidget*> m_distributionPanels;

@ -2155,6 +2155,10 @@ void nmSubWndMain::onReadonlyCurveDoubleClicked(
bool nmSubWndMain::eventFilter(QObject* watched, QEvent* event)
{
// 最小化时先恢复窗口状态,再交给默认关闭流程隐藏,避免复用时丢失正常尺寸。
QDialog* dialog = qobject_cast<QDialog*>(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<QDialog*>(watched)) {
@ -2164,7 +2168,6 @@ bool nmSubWndMain::eventFilter(QObject* watched, QEvent* event)
if(dialog->isWindow()) dialog->close();
}
// 独立窗口不再经过 MDI 的画布适配,显示和缩放时适配自己的绘图区。
QDialog* dialog = qobject_cast<QDialog*>(watched);
if(dialog && (event->type() == QEvent::Resize || event->type() == QEvent::Show)) {
dialog->layout()->activate();
foreach(iGuiPlot* plot, dialog->findChildren<iGuiPlot*>())

@ -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;

@ -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));

@ -2,6 +2,9 @@
#include "nmDataAnalyzeManager.h"
#include "nmWxAutomaticFitting.h"
#include "nmParameterField.h"
#include "ZxBaseUtil.h"
#include <QGroupBox>
#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<double> 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<QLineEdit*>(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<int>() << 260 << 960);
centerSplitter->setSizes(QList<int>() << 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<QLabel*> labels;
labels << modeLabel << numLabel << modelLabel << minLabel << maxLabel;
labels.append(distributionPanel->findChildren<QLabel*>());
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<QLineEdit*>())
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<int>() << 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<QHBoxLayout*>(m_distributionPlots[idx]->parentWidget()->layout());
contentLayout->setStretch(0, monteCarlo ? 0 : 1);
// 表格采用较窄的默认宽度;分布图让参数区仅占所需宽度,避免沿用表格分栏。
// 只在布局类型切换时调整,编辑参数时保留用户拖动后的宽度。
QSplitter* splitter = static_cast<QSplitter*>(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<int>() << parameterWidth << availableWidth - parameterWidth);
}
if(monteCarlo) { updateDistributionPage(idx); return; }
// 自动模式由范围生成只读表格;手动模式保留每行值,不因选中行而重算。
QTableWidget* table = m_valueTables[idx];
@ -2456,7 +2462,7 @@ void nmWxSensitive::updateValuesForPage(int idx)
}
QVector<double> 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<nmDataSensitive::VariableSampling::Distribution>(
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<QLineEdit*>(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<QComboBox*>(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<QLineEdit*> edits;
edits << m_modelSpins[idx] << m_minSpins[idx] << m_maxSpins[idx];
for (int field = 0; field < 5; ++field)
edits << m_distributionPanels[idx]->findChild<QLineEdit*>(distributionFields[field]);
QVector<double> 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<double> 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;

Loading…
Cancel
Save