From 7083a47786b92230cc27a550f5fc58a5a1937418 Mon Sep 17 00:00:00 2001 From: lvjunjie Date: Mon, 12 Oct 2026 17:23:46 +0800 Subject: [PATCH] =?UTF-8?q?feat(nmNum):=20=E8=B0=83=E6=95=B4=E6=95=8F?= =?UTF-8?q?=E6=84=9F=E6=80=A7=E5=88=86=E6=9E=90=E8=BF=90=E8=A1=8C=E9=A2=84?= =?UTF-8?q?=E8=A7=88=E5=88=97=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 按 Saphir 样式显示运行编号和参数描述,调整多变量组合顺序,并以连续滚动列表替代分页预览。 --- Include/nmNum/nmSubWxs/nmWxSensitive.h | 11 +- Src/nmNum/nmData/nmDataSensitive.cpp | 3 +- Src/nmNum/nmSubWxs/nmWxSensitive.cpp | 366 ++++++++++++++++++------- 3 files changed, 277 insertions(+), 103 deletions(-) diff --git a/Include/nmNum/nmSubWxs/nmWxSensitive.h b/Include/nmNum/nmSubWxs/nmWxSensitive.h index ffdc4f34..dc899b3f 100644 --- a/Include/nmNum/nmSubWxs/nmWxSensitive.h +++ b/Include/nmNum/nmSubWxs/nmWxSensitive.h @@ -18,6 +18,7 @@ class nmDataAnalyzeManager; class QTableWidget; class QLabel; class QIcon; +class SensitivityRunsView; class NM_SUB_WXS_EXPORT nmWxSensitive : public iDlgBase { @@ -37,9 +38,7 @@ public: void onTableCellClicked(int row, int column); void onTreeItemChanged(QTreeWidgetItem *item, int column); void onPreviewClicked(); - void onPreviewPrevious(); - void onPreviewNext(); - void onPreviewJump(); + void onPreviewScrollAction(int action); private: void setupUI(); @@ -54,7 +53,6 @@ private: bool saveDataFromUI(); // 校验并从界面保存到数据 void refreshModelValues(); void updateModelCount(); - void refreshPreviewPage(); int findVariableIndex(const QString& varName); // 查找变量索引 // UI 控件 @@ -80,10 +78,7 @@ private: int m_selectedValueIndex; bool m_loading; nmDataAnalyzeManager* m_pDataManager; - QTableWidget* m_previewTable; - QLineEdit* m_previewPageEdit; - QLabel* m_previewPageLabel; - quint64 m_previewPage; + SensitivityRunsView* m_previewView; // 敏感分析数据 nmDataSensitive m_sensitiveData; diff --git a/Src/nmNum/nmData/nmDataSensitive.cpp b/Src/nmNum/nmData/nmDataSensitive.cpp index f3b0553a..e8c3b21e 100644 --- a/Src/nmNum/nmData/nmDataSensitive.cpp +++ b/Src/nmNum/nmData/nmDataSensitive.cpp @@ -387,7 +387,8 @@ bool nmDataSensitive::getRunValues(quint64 runIndex, QVector& values) co runIndex -= samples.size(); } } else { - for (int i = m_variables.size() - 1; i >= 0; --i) { + // Saphir 按第一个选中参数最快变化的顺序列出多变量组合。 + for (int i = 0; i < m_variables.size(); ++i) { const VariableSampling& variable = m_variables[i]; if (!variable.m_enabled) continue; const QVector samples = variable.samplingValues(); diff --git a/Src/nmNum/nmSubWxs/nmWxSensitive.cpp b/Src/nmNum/nmSubWxs/nmWxSensitive.cpp index 98d74386..3ec2c9cd 100644 --- a/Src/nmNum/nmSubWxs/nmWxSensitive.cpp +++ b/Src/nmNum/nmSubWxs/nmWxSensitive.cpp @@ -22,8 +22,12 @@ #include #include #include -#include #include +#include +#include +#include +#include +#include #include #include @@ -50,6 +54,7 @@ #include #include +#include namespace { @@ -208,11 +213,258 @@ public: } }; +// 表格仅承载可见行,完整组合编号使用六十四位整数,滚动过程中按编号取数。 +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_previewTable(nullptr), m_previewPageEdit(nullptr), m_previewPageLabel(nullptr), - m_previewPage(0) + m_previewView(nullptr) { // 显式绑定所属分析窗口,后续切换窗口不会改变配置保存目标。 if (m_pDataManager) m_sensitiveData = m_pDataManager->getSensitiveDataCopy(); @@ -1170,104 +1422,30 @@ void nmWxSensitive::onTreeItemClicked(QTreeWidgetItem* item, int column) void nmWxSensitive::onPreviewClicked() { if (!saveDataFromUI()) return; - // 按页解码编号,组合数再大也只创建当前页的五十行。 + // Saphir 风格的连续运行列表,组合数再大也不显示分页控件。 QDialog dialog(this); - dialog.setWindowTitle(tr("Sensitivity runs")); - dialog.resize(850, 500); + dialog.setWindowTitle(tr("Sensitivity Runs")); + dialog.setWindowFlags(dialog.windowFlags() & ~Qt::WindowContextHelpButtonHint); + dialog.setMinimumSize(420, 360); + dialog.resize(470, 650); QVBoxLayout* layout = new QVBoxLayout(&dialog); - m_previewTable = new QTableWidget(&dialog); - m_previewTable->setEditTriggers(QAbstractItemView::NoEditTriggers); - layout->addWidget(m_previewTable); - QHBoxLayout* navigation = new QHBoxLayout(); - QPushButton* previous = new QPushButton(&dialog); - previous->setIcon(style()->standardIcon(QStyle::SP_ArrowLeft)); - previous->setToolTip(tr("Previous page")); - QPushButton* next = new QPushButton(&dialog); - next->setIcon(style()->standardIcon(QStyle::SP_ArrowRight)); - next->setToolTip(tr("Next page")); - m_previewPageEdit = new QLineEdit(&dialog); - m_previewPageEdit->setFixedWidth(180); - m_previewPageLabel = new QLabel(&dialog); - navigation->addWidget(previous); - navigation->addWidget(m_previewPageEdit); - navigation->addWidget(m_previewPageLabel); - navigation->addWidget(next); - navigation->addStretch(); - layout->addLayout(navigation); + 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(previous, SIGNAL(clicked()), this, SLOT(onPreviewPrevious())); - connect(next, SIGNAL(clicked()), this, SLOT(onPreviewNext())); - connect(m_previewPageEdit, SIGNAL(returnPressed()), this, SLOT(onPreviewJump())); - m_previewPage = 0; - refreshPreviewPage(); + connect(m_previewView->verticalScrollBar(), SIGNAL(actionTriggered(int)), + this, SLOT(onPreviewScrollAction(int))); dialog.exec(); - m_previewTable = nullptr; - m_previewPageEdit = nullptr; - m_previewPageLabel = nullptr; + m_previewView = nullptr; } -void nmWxSensitive::refreshPreviewPage() +void nmWxSensitive::onPreviewScrollAction(int action) { - // 参数列与计算数据共用 getRunValues,单变量运行的其他参数显示基准值。 - if (!m_previewTable) return; - QStringList headers; - headers << tr("Run"); - QList selected; - for (int i = 0; i < m_sensitiveData.getVariableCount(); ++i) { - nmDataSensitive::VariableSampling& variable = m_sensitiveData.getVariable(i); - if (!variable.getEnabled()) continue; - selected.append(i); - const QString unit = variable.getModelValue().getUnit(); - headers << variable.getVarName() + (unit.isEmpty() ? QString() : " (" + unit + ")"); - } - m_previewTable->clear(); - m_previewTable->setColumnCount(headers.size()); - m_previewTable->setHorizontalHeaderLabels(headers); - const quint64 total = m_sensitiveData.getTotalModelCount(); - const quint64 first = m_previewPage * 50; - const int rows = int(qMin(quint64(50), total - first)); - m_previewTable->setRowCount(rows); - for (int row = 0; row < rows; ++row) { - QVector values; - if (!m_sensitiveData.getRunValues(first + row, values)) break; - m_previewTable->setItem(row, 0, new QTableWidgetItem(QString::number(first + row + 1))); - for (int column = 0; column < selected.size(); ++column) { - const int index = selected[column]; - nmDataSensitive::VariableSampling& variable = m_sensitiveData.getVariable(index); - double display = values[index]; - convertSamplingValue(variable.getVarName(), display, samplingBaseUnit(variable.getVarName()), - variable.getModelValue().getUnit(), display); - QTableWidgetItem* item = new QTableWidgetItem(formatSamplingValue(display, 3)); - item->setToolTip(formatSamplingValue(display, 6)); - item->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter); - m_previewTable->setItem(row, column + 1, item); - } - } - m_previewTable->resizeColumnsToContents(); - m_previewPageEdit->setText(QString::number(m_previewPage + 1)); - m_previewPageLabel->setText(tr("/ %1 pages; %2 runs").arg((total - 1) / 50 + 1).arg(total)); -} - -void nmWxSensitive::onPreviewPrevious() -{ - if (m_previewPage > 0) --m_previewPage; - refreshPreviewPage(); -} - -void nmWxSensitive::onPreviewNext() -{ - if ((m_previewPage + 1) * 50 < m_sensitiveData.getTotalModelCount()) ++m_previewPage; - refreshPreviewPage(); -} - -void nmWxSensitive::onPreviewJump() -{ - // 允许直接跳转到大批量组合中的指定页,页码使用六十四位整数。 - bool ok = false; - const quint64 page = m_previewPageEdit->text().trimmed().toULongLong(&ok); - const quint64 pages = (m_sensitiveData.getTotalModelCount() - 1) / 50 + 1; - if (ok && page > 0 && page <= pages) m_previewPage = page - 1; - refreshPreviewPage(); + // 将原生滚动条的单步、整屏和拖动操作转交连续列表。 + if (m_previewView) m_previewView->scrollAction(action); }