feat(nmNum): 调整敏感性分析运行预览列表

按 Saphir 样式显示运行编号和参数描述,调整多变量组合顺序,并以连续滚动列表替代分页预览。
feature/Sensitivity-20261012
lvjunjie 2 weeks ago
parent e18988426e
commit 7083a47786

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

@ -387,7 +387,8 @@ bool nmDataSensitive::getRunValues(quint64 runIndex, QVector<double>& 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<double> samples = variable.samplingValues();

@ -22,8 +22,12 @@
#include <QDialog>
#include <QDialogButtonBox>
#include <QApplication>
#include <QStyle>
#include <QTableWidget>
#include <QAbstractScrollArea>
#include <QScrollBar>
#include <QWheelEvent>
#include <QKeyEvent>
#include <QMouseEvent>
#include <QHeaderView>
#include <QSplitter>
@ -50,6 +54,7 @@
#include <math.h>
#include <float.h>
#include <limits.h>
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<double> 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<QWheelEvent*>(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<QKeyEvent*>(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<QMouseEvent*>(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<double> 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<int> 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<int> 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<double> 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);
}

Loading…
Cancel
Save