From 259508166be6c9a4b30b91b42cec1c1daaff06b2 Mon Sep 17 00:00:00 2001 From: lvjunjie Date: Thu, 29 Oct 2026 10:13:19 +0800 Subject: [PATCH 1/9] =?UTF-8?q?fix(nmNum):=20=E4=BF=AE=E5=A4=8D=E6=97=B6?= =?UTF-8?q?=E5=8F=98=E8=A1=A8=E7=9A=AE=E7=AA=97=E5=8F=A3=E9=87=8D=E5=BC=80?= =?UTF-8?q?=E5=8F=8A=E8=B7=A8=E5=88=86=E6=9E=90=E7=8A=B6=E6=80=81=E6=AE=8B?= =?UTF-8?q?=E7=95=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nmNum/nmSubWxs/nmWxTimeDependentSkin.h | 6 +- Src/nmNum/nmSubWxs/nmWxNumericalDesign.cpp | 4 +- Src/nmNum/nmSubWxs/nmWxTimeDependentSkin.cpp | 93 +++++++------------ 3 files changed, 37 insertions(+), 66 deletions(-) diff --git a/Include/nmNum/nmSubWxs/nmWxTimeDependentSkin.h b/Include/nmNum/nmSubWxs/nmWxTimeDependentSkin.h index 72b3229c..c514510b 100644 --- a/Include/nmNum/nmSubWxs/nmWxTimeDependentSkin.h +++ b/Include/nmNum/nmSubWxs/nmWxTimeDependentSkin.h @@ -52,6 +52,7 @@ public: // 确认和取消按钮 void onAccept(); void onReject(); + void onWellDestroyed(); // 右侧按钮槽函数 void onRightAddClicked(); @@ -90,9 +91,6 @@ private: // 初始化UI void initUI(); - void reloadFromDataSource(nmDataWellBase* pSpecificWellData); - - // 更新图表 void updateCharts(); @@ -235,8 +233,6 @@ private: // 静态成员变量 static nmWxTimeDependentSkin* s_instance; - static QString s_currentWellName; - static nmDataWellBase* s_currentWellData; }; #endif // NMWXTIMEDEPENDENTSKIN_H diff --git a/Src/nmNum/nmSubWxs/nmWxNumericalDesign.cpp b/Src/nmNum/nmSubWxs/nmWxNumericalDesign.cpp index 13f79352..bbda9ebd 100644 --- a/Src/nmNum/nmSubWxs/nmWxNumericalDesign.cpp +++ b/Src/nmNum/nmSubWxs/nmWxNumericalDesign.cpp @@ -1087,7 +1087,9 @@ void nmWxNumericalDesign::onCustomNumericalSettingsClicked() // 针对高级组中的"时间相关"按钮的槽函数 void nmWxNumericalDesign::onTimeIconClicked() { - nmWxTimeDependentSkin::showForCurrentWell(); + // 使用所属分析的当前井,并让子窗口随本分析面板一起销毁。 + if(!m_pDataManager || !m_pDataManager->getCurWellData()) return; + nmWxTimeDependentSkin::showForCurrentWell(m_pDataManager->getCurWellData(), this); } void nmWxNumericalDesign::onOutputResultFieldsAutomaticToggled(bool checked) diff --git a/Src/nmNum/nmSubWxs/nmWxTimeDependentSkin.cpp b/Src/nmNum/nmSubWxs/nmWxTimeDependentSkin.cpp index bcd36785..49b87d51 100644 --- a/Src/nmNum/nmSubWxs/nmWxTimeDependentSkin.cpp +++ b/Src/nmNum/nmSubWxs/nmWxTimeDependentSkin.cpp @@ -37,44 +37,33 @@ // 静态成员变量定义 nmWxTimeDependentSkin* nmWxTimeDependentSkin::s_instance = nullptr; -QString nmWxTimeDependentSkin::s_currentWellName = ""; -nmDataWellBase* nmWxTimeDependentSkin::s_currentWellData = nullptr; void nmWxTimeDependentSkin::showForCurrentWell(nmDataWellBase* pSpecificWellData, QWidget* parent) { - QString currentWellName; - if (pSpecificWellData) currentWellName = pSpecificWellData->getWellName(); - else { - auto* cur = nmDataAnalyzeManager::getCurrentInstance()->getCurWellData(); - if (!cur) return; - currentWellName = cur->getWellName(); + // 绑定实际井对象;同名井或不同分析不能共用旧窗口的数据和图表状态。 + nmDataWellBase* pWellData = pSpecificWellData; + if (!pWellData) { + nmDataAnalyzeManager* pManager = nmDataAnalyzeManager::getCurrentInstance(); + if (!pManager) return; + pWellData = pManager->getCurWellData(); } - if (currentWellName.isEmpty()) return; - - if (s_instance && s_currentWellName == currentWellName) { - bool isSameDataSource = (pSpecificWellData && s_currentWellData == pSpecificWellData) - || (!pSpecificWellData && !s_currentWellData); - if (isSameDataSource) { - // 同步父子关系和模态 - if (parent && s_instance->parent() != parent) - s_instance->setParent(parent, Qt::Dialog); - - // 刷新数据 - s_instance->reloadFromDataSource(pSpecificWellData); + if (!pWellData) return; + if (s_instance) { + // 只激活仍在编辑的同一窗口,关闭后重新构造默认的压力/流量界面。 + if (s_instance->isVisible() && s_instance->m_currentWell == pWellData && + s_instance->parentWidget() == parent) { s_instance->show(); s_instance->raise(); s_instance->activateWindow(); return; } - s_instance->close(); delete s_instance; } - // 新建实例 - s_instance = new nmWxTimeDependentSkin(pSpecificWellData, parent); - s_currentWellName = currentWellName; - s_currentWellData = pSpecificWellData; + // 仅入口创建的堆对象关闭即释放,井编辑器中的栈对象仍由调用方管理。 + s_instance = new nmWxTimeDependentSkin(pWellData, parent); + s_instance->setAttribute(Qt::WA_DeleteOnClose, true); s_instance->show(); s_instance->raise(); @@ -105,6 +94,10 @@ nmWxTimeDependentSkin::nmWxTimeDependentSkin(nmDataWellBase* pWellData, QWidget // 使用当前井数据, 来自nmWxNumericalDesign m_currentWell = nmDataAnalyzeManager::getCurrentInstance()->getCurWellData(); } + // 分析关闭或井对象被替换时立即关闭窗口,避免继续访问上一分析的数据。 + if (m_currentWell) { + connect(m_currentWell, SIGNAL(destroyed(QObject*)), this, SLOT(onWellDestroyed())); + } if(m_currentWell != nullptr) { // 获取压力和流量数据 @@ -227,44 +220,9 @@ nmWxTimeDependentSkin::~nmWxTimeDependentSkin() // 如果静态实例指针指向当前对象,清空静态变量 if (s_instance == this) { s_instance = nullptr; - s_currentWellName = ""; - s_currentWellData = nullptr; } } -void nmWxTimeDependentSkin::reloadFromDataSource(nmDataWellBase* pWellData) -{ - // 1) 重新决定数据源 - m_currentWell = (pWellData != nullptr) - ? pWellData - : nmDataAnalyzeManager::getCurrentInstance()->getCurWellData(); - - if (!m_currentWell) return; - if (m_pPressureChart) m_pPressureChart->setCurrentWellData(m_currentWell); - if (m_pFlowRateChart) m_pFlowRateChart->setCurrentWellData(m_currentWell); - - // 2) 重新抓取并处理数据 - pressurePoints = m_currentWell->getPressurePoints(); - QVector rawFlowData = m_currentWell->getFlowPoints(); - flowPoints = processFlowData(rawFlowData); - - // 3) 重新取选中射孔段的副本(保留索引边界) - int perfCount = m_currentWell->getPerforationCount(); - if (m_currentPerforationIndex >= perfCount) m_currentPerforationIndex = 0; - m_perforationData = m_currentWell->getPerforationCopy(m_currentPerforationIndex); - - // 4) 做一次与构造期相同的默认值兜底 - if (m_perforationData.getFlowSegments().isEmpty()) initializeDefaultFlowSegment(); - if (m_perforationData.getSkin0().getValue().toString().isEmpty()) m_perforationData.getSkin0().setValue("0.0"); - if (m_perforationData.getdSkindq().getValue().toString().isEmpty()) m_perforationData.getdSkindq().setValue("0.0"); - double x1,y1,x2,y2; m_perforationData.getLinePositions(x1,y1,x2,y2); - if (x1==-999.0 || y1==-999.0 || x2==-999.0 || y2==-999.0) m_perforationData.setLinePositions(100.0,0.0,200.0,0.0); - - // 5) 刷新UI - updateCharts(); - updateTableFromFlowSegments(); -} - void nmWxTimeDependentSkin::initUI() { // 创建主布局 @@ -2024,6 +1982,12 @@ void nmWxTimeDependentSkin::onAccept() return; } + // 对比模式临时禁用了流量相关项,提交前恢复原设置并保存当前拟合线。 + if(m_pChartWidget) { + saveChartPositionsToData(); + restoreStateAfterSkinVsRate(); + } + // 副本数据同步回原始射孔段 m_currentWell->updatePerforation(m_currentPerforationIndex, m_perforationData); @@ -2035,6 +1999,15 @@ void nmWxTimeDependentSkin::onReject() reject(); } +void nmWxTimeDependentSkin::onWellDestroyed() +{ + // 数据源已失效,只关闭窗口,不再提交副本或处理图表交互。 + m_currentWell = nullptr; + m_pPressureChart->setCurrentWellData(nullptr); + m_pFlowRateChart->setCurrentWellData(nullptr); + reject(); +} + void nmWxTimeDependentSkin::initializeDefaultFlowSegment() { if(m_currentWell == nullptr) { From ea1af96909cd7c16768dca4f416f2fd45b51854c Mon Sep 17 00:00:00 2001 From: lvjunjie Date: Thu, 29 Oct 2026 11:17:40 +0800 Subject: [PATCH 2/9] =?UTF-8?q?fix(nmNum):=20=E8=B0=83=E6=95=B4=E6=97=B6?= =?UTF-8?q?=E5=8F=98=E8=A1=A8=E7=9A=AE=E5=9B=BE=E8=A1=A8=E8=BD=B4=E6=A0=87?= =?UTF-8?q?=E9=A2=98=E9=97=B4=E8=B7=9D=E5=B9=B6=E4=BF=9D=E6=8C=81=E7=BB=98?= =?UTF-8?q?=E5=9B=BE=E5=8C=BA=E5=B0=BA=E5=AF=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Src/nmNum/nmSubWxs/nmWxChartWidget.cpp | 19 +++++++----- .../nmSubWxs/nmWxPressFlowChartWidget.cpp | 21 +++++++------ Src/nmNum/nmSubWxs/nmWxTimeDependentSkin.cpp | 30 +++++++++++++++++-- 3 files changed, 51 insertions(+), 19 deletions(-) diff --git a/Src/nmNum/nmSubWxs/nmWxChartWidget.cpp b/Src/nmNum/nmSubWxs/nmWxChartWidget.cpp index 36b7202d..e99795bb 100644 --- a/Src/nmNum/nmSubWxs/nmWxChartWidget.cpp +++ b/Src/nmNum/nmSubWxs/nmWxChartWidget.cpp @@ -49,10 +49,11 @@ void nmWxChartWidget::paintEvent(QPaintEvent *event) QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing); - int marginLeft = 55; + // 补偿窗口让出的左侧和底部空白,移动标题时保持绘图区位置和尺寸。 + int marginLeft = 55 + contentsMargins().left(); int marginRight = 10; int marginTop = 4; - int marginBottom = 45; + int marginBottom = 45 + contentsMargins().bottom(); chartRect = QRectF(marginLeft, marginTop, width() - marginLeft - marginRight, @@ -249,15 +250,19 @@ void nmWxChartWidget::drawAxes(QPainter& painter) } } - // 轴标题 + // 按文字实际边界贴近外沿,保持原有字号、绘图区和刻度位置。 painter.setFont(QFont("Arial", 10)); - painter.drawText(chartRect.center().x() - 50, chartRect.bottom() + 35, - tr("Liquid rate [STB/D]")); + const QString xTitle = tr("Liquid rate [STB/D]"); + const QRect xTitleRect = painter.fontMetrics().tightBoundingRect(xTitle); + painter.drawText(QPointF(chartRect.center().x() - xTitleRect.center().x(), + height() - 2 - xTitleRect.bottom()), xTitle); painter.save(); - painter.translate(15, chartRect.center().y()); + painter.translate(1, chartRect.center().y()); painter.rotate(-90); - painter.drawText(-15, 0, tr("Skin")); + const QString yTitle = tr("Skin"); + const QRect yTitleRect = painter.fontMetrics().tightBoundingRect(yTitle); + painter.drawText(QPointF(-yTitleRect.center().x(), -yTitleRect.top()), yTitle); painter.restore(); } diff --git a/Src/nmNum/nmSubWxs/nmWxPressFlowChartWidget.cpp b/Src/nmNum/nmSubWxs/nmWxPressFlowChartWidget.cpp index bdfae427..9c8a25cc 100644 --- a/Src/nmNum/nmSubWxs/nmWxPressFlowChartWidget.cpp +++ b/Src/nmNum/nmSubWxs/nmWxPressFlowChartWidget.cpp @@ -169,9 +169,10 @@ void nmWxPressFlowChartWidget::paintEvent(QPaintEvent *event) QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing, true); - // 计算绘图区域 - m_plotArea = QRect(m_marginLeft, m_marginTop, - width() - m_marginLeft - m_marginRight, + // 补偿窗口让出的左侧空白,使标题左移时绘图区保持原位和原尺寸。 + const int plotLeft = m_marginLeft + contentsMargins().left(); + m_plotArea = QRect(plotLeft, m_marginTop, + width() - plotLeft - m_marginRight, height() - m_marginTop - m_marginBottom); drawBackground(painter); @@ -504,9 +505,10 @@ void nmWxPressFlowChartWidget::drawLabels(QPainter& painter) if (!m_xUnit.isEmpty()) { xLabelText += " (" + m_xUnit + ")"; } - QRect xLabelRect = painter.fontMetrics().boundingRect(xLabelText); - int xLabelX = (width() - xLabelRect.width()) / 2; - painter.drawText(xLabelX, height() - 10, xLabelText); + // 按文字实际边界贴近底部外沿,保持原有字号和绘图区。 + const QRect xLabelRect = painter.fontMetrics().tightBoundingRect(xLabelText); + painter.drawText(QPointF(m_plotArea.center().x() - xLabelRect.center().x(), + height() - 2 - xLabelRect.bottom()), xLabelText); painter.restore(); } @@ -520,10 +522,11 @@ void nmWxPressFlowChartWidget::drawLabels(QPainter& painter) yLabelText += " (" + m_yUnit + ")"; } - painter.translate(15, height() / 2); + // 去掉字体矩形内的额外留白,将文字实际左边缘放在控件边缘内一像素处。 + painter.translate(1, m_plotArea.center().y()); painter.rotate(-90); - QRect yLabelRect = painter.fontMetrics().boundingRect(yLabelText); - painter.drawText(-yLabelRect.width()/2, 0, yLabelText); + const QRect yLabelRect = painter.fontMetrics().tightBoundingRect(yLabelText); + painter.drawText(QPointF(-yLabelRect.center().x(), -yLabelRect.top()), yLabelText); painter.restore(); } } diff --git a/Src/nmNum/nmSubWxs/nmWxTimeDependentSkin.cpp b/Src/nmNum/nmSubWxs/nmWxTimeDependentSkin.cpp index 49b87d51..b2cd474d 100644 --- a/Src/nmNum/nmSubWxs/nmWxTimeDependentSkin.cpp +++ b/Src/nmNum/nmSubWxs/nmWxTimeDependentSkin.cpp @@ -309,9 +309,14 @@ void nmWxTimeDependentSkin::initUI() splitter->setHandleWidth(1); // 创建左侧区域(图表+下拉框) - QWidget* leftWidget = new QWidget(); + QWidget* leftWidget = new QWidget(splitter); QVBoxLayout* leftLayout = new QVBoxLayout(leftWidget); + // 将左侧布局留白让给图表标题,绘图区用相同宽度补偿,保持位置和大小。 + int chartLeftExtension, topMargin, rightMargin, bottomMargin; + leftLayout->getContentsMargins(&chartLeftExtension, &topMargin, &rightMargin, &bottomMargin); + leftLayout->setContentsMargins(0, topMargin, rightMargin, bottomMargin); + // 创建图表区域容器 QWidget* chartWidget = new QWidget(); m_pChartLayout = new QVBoxLayout(chartWidget); @@ -325,6 +330,8 @@ void nmWxTimeDependentSkin::initUI() // 创建压力图表 m_pPressureChart = new nmWxPressFlowChartWidget(m_pChartSplitter); + m_pPressureChart->setContentsMargins(chartLeftExtension, 0, 0, 0); + m_pPressureChart->setMinimumWidth(300 + chartLeftExtension); m_pPressureChart->setCurrentWellData(m_currentWell); m_pPressureChart->setAxisLabels(tr(""), tr("Pressure")); m_pPressureChart->setAxisUnits(tr("hr"), tr("MPa")); @@ -335,6 +342,8 @@ void nmWxTimeDependentSkin::initUI() // 创建流量图表 m_pFlowRateChart = new nmWxPressFlowChartWidget(m_pChartSplitter); + m_pFlowRateChart->setContentsMargins(chartLeftExtension, 0, 0, 0); + m_pFlowRateChart->setMinimumWidth(300 + chartLeftExtension); m_pFlowRateChart->setCurrentWellData(m_currentWell); m_pFlowRateChart->setAxisLabels(tr("Time"), tr("Flow")); m_pFlowRateChart->setAxisUnits(tr("hr"), tr("m^3/D")); @@ -363,7 +372,13 @@ void nmWxTimeDependentSkin::initUI() // 将图表和下拉框添加到左侧布局 leftLayout->addWidget(chartWidget, 1); // 图表占据大部分空间 - leftLayout->addWidget(m_pComboBox); // 下拉框固定高度 + // 下拉框仍使用原来的左边距,只有图表标题向左扩展。 + QHBoxLayout* comboLayout = new QHBoxLayout(); + comboLayout->setContentsMargins(0, 0, 0, 0); + comboLayout->addSpacing(chartLeftExtension); + comboLayout->addWidget(m_pComboBox); + comboLayout->addStretch(); + leftLayout->addLayout(comboLayout); // 创建中间区域(表格+确定取消按钮) QWidget* middleWidget = new QWidget(); @@ -923,7 +938,14 @@ void nmWxTimeDependentSkin::showInteractiveChart() // 创建交互式图表widget m_pChartWidget = new nmWxChartWidget(this); - m_pChartWidget->setMinimumSize(400, 400); + // 切换模式后沿用压力图的左侧扩展宽度,保持绘图区位置不变。 + const int chartLeftExtension = m_pPressureChart->contentsMargins().left(); + // 将图表与下拉框之间的空白交给横轴标题,绘图区补偿相同高度。 + QLayout* leftLayout = m_pComboBox->parentWidget()->layout(); + const int chartBottomExtension = qMax(0, leftLayout->spacing()); + leftLayout->setSpacing(0); + m_pChartWidget->setContentsMargins(chartLeftExtension, 0, 0, chartBottomExtension); + m_pChartWidget->setMinimumSize(400 + chartLeftExtension, 400 + chartBottomExtension); // 连接信号槽 connect(m_pChartWidget, SIGNAL(skinValuesChanged(double, double)), @@ -1089,6 +1111,8 @@ void nmWxTimeDependentSkin::restoreOriginalCharts() // 移除交互式图表 if(m_pChartWidget != nullptr) { + // 切回压力/流量图时恢复原来的布局间距。 + m_pComboBox->parentWidget()->layout()->setSpacing(m_pChartWidget->contentsMargins().bottom()); m_pChartLayout->removeWidget(m_pChartWidget); m_pChartWidget->deleteLater(); m_pChartWidget = nullptr; From 6fc7b29bc1255ee73fe91b6f111704d539e98ce4 Mon Sep 17 00:00:00 2001 From: lvjunjie Date: Thu, 29 Oct 2026 17:07:16 +0800 Subject: [PATCH 3/9] =?UTF-8?q?fix(nmNum):=20=E9=9A=90=E8=97=8F=E6=97=B6?= =?UTF-8?q?=E5=8F=98=E8=A1=A8=E7=9A=AE=E8=AE=A1=E7=AE=97=E6=8C=89=E9=92=AE?= =?UTF-8?q?=E5=B9=B6=E5=89=8D=E7=A7=BB=E5=B0=84=E5=AD=94=E5=B0=81=E5=A0=B5?= =?UTF-8?q?=E5=85=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Src/nmNum/nmSubWxs/nmWxTimeDependentSkin.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Src/nmNum/nmSubWxs/nmWxTimeDependentSkin.cpp b/Src/nmNum/nmSubWxs/nmWxTimeDependentSkin.cpp index b2cd474d..98dab9cd 100644 --- a/Src/nmNum/nmSubWxs/nmWxTimeDependentSkin.cpp +++ b/Src/nmNum/nmSubWxs/nmWxTimeDependentSkin.cpp @@ -273,6 +273,8 @@ void nmWxTimeDependentSkin::initUI() createButton(m_pRemoveBtn, appDir + "/Res/Icon/nmTimeDepSkin3.png", tr("Remove")); createButton(m_pSplitStepsBtn, appDir + "/Res/Icon/nmTimeDepSkin4.png", tr("Split Steps")); createButton(m_pComputeBtn, appDir + "/Res/Icon/nmTimeDepSkin5.png", tr("Compute")); + // 计算功能尚未实现,隐藏图标和文字容器,让射孔封堵前移到该位置。 + m_pComputeBtn->parentWidget()->hide(); createButton(m_pPerforationClosingBtn, appDir + "/Res/Icon/nmTimeDepSkin6.png", tr("Perforation Closing")); m_pToolbarLayout->addStretch(); @@ -1218,8 +1220,9 @@ void nmWxTimeDependentSkin::restoreToolbarState() m_pSplitStepsBtn->parentWidget()->setVisible(true); } + // 切回默认界面时仍保持计算入口隐藏。 if(m_pComputeBtn && m_pComputeBtn->parentWidget()) { - m_pComputeBtn->parentWidget()->setVisible(true); + m_pComputeBtn->parentWidget()->setVisible(false); } if(m_pPerforationClosingBtn && m_pPerforationClosingBtn->parentWidget()) { From 10d3ce41168427ac9703111a732e52b99997eb93 Mon Sep 17 00:00:00 2001 From: lvjunjie Date: Fri, 30 Oct 2026 14:28:28 +0800 Subject: [PATCH 4/9] =?UTF-8?q?fix(nmNum):=20=E6=97=B6=E5=8F=98=E8=A1=A8?= =?UTF-8?q?=E7=9A=AE=E5=A4=8D=E7=94=A8=E5=BA=95=E5=B1=82=E7=BB=84=E4=BB=B6?= =?UTF-8?q?=E5=B9=B6=E4=BF=AE=E6=AD=A3=E5=8D=95=E4=BD=8D=E5=88=87=E6=8D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nmNum/nmSubWxs/nmWxTimeDependentSkin.h | 2 +- Src/nmNum/nmSubWxs/nmWxPerforationClosing.cpp | 53 ++++-- Src/nmNum/nmSubWxs/nmWxTimeDependentSkin.cpp | 177 ++++++++---------- 3 files changed, 114 insertions(+), 118 deletions(-) diff --git a/Include/nmNum/nmSubWxs/nmWxTimeDependentSkin.h b/Include/nmNum/nmSubWxs/nmWxTimeDependentSkin.h index c514510b..d5fa3a17 100644 --- a/Include/nmNum/nmSubWxs/nmWxTimeDependentSkin.h +++ b/Include/nmNum/nmSubWxs/nmWxTimeDependentSkin.h @@ -137,7 +137,7 @@ private: void snapAllBoundariesToFlowStepsAndMerge(); // 工具函数 - double convertTime(double value, const QString& fromUnit, const QString& toUnit) const; + static double convertTime(double value, const QString& fromUnit, const QString& toUnit); void refreshTableStructure(); // 根据是否启用“流量相关”重建表头/单位行 double convertDSdQ(double v, const QString& fromU, const QString& toU) const; diff --git a/Src/nmNum/nmSubWxs/nmWxPerforationClosing.cpp b/Src/nmNum/nmSubWxs/nmWxPerforationClosing.cpp index d6d10980..ef000e0e 100644 --- a/Src/nmNum/nmSubWxs/nmWxPerforationClosing.cpp +++ b/Src/nmNum/nmSubWxs/nmWxPerforationClosing.cpp @@ -1,4 +1,8 @@ -#include "nmWxPerforationClosing.h" +#include "nmWxPerforationClosing.h" +#include "iUnitGroup.h" +#include "iUnitHelper.h" +#include "ZxDataEditorDelegate.h" +#include "iBase_global.h" #include #include "nmDialogPreviewGuard.h" @@ -69,9 +73,6 @@ void nmWxPerforationClosing::initUI() // 创建主布局 QVBoxLayout* mainLayout = new QVBoxLayout(this); - QString appDir = QCoreApplication::applicationDirPath(); - appDir = appDir.section('/', 0, -2); // 获取上一级目录 - // 创建工具栏容器 QWidget* toolbarContainer = new QWidget(this); toolbarContainer->setMinimumHeight(70); @@ -86,7 +87,8 @@ void nmWxPerforationClosing::initUI() addContainerLayout->setContentsMargins(2, 2, 2, 2); m_pAddBtn = new QPushButton(addButtonContainer); - m_pAddBtn->setIcon(QIcon(appDir + "/Res/Icon/nmTimeDepSkin2.png")); + // 复用框架图标加载及缓存,保持原有工具栏布局。 + m_pAddBtn->setIcon(zxLoadIcon("nmTimeDepSkin2")); m_pAddBtn->setIconSize(QSize(36, 36)); m_pAddBtn->setFixedSize(40, 40); @@ -106,7 +108,7 @@ void nmWxPerforationClosing::initUI() removeContainerLayout->setContentsMargins(2, 2, 2, 2); m_pRemoveBtn = new QPushButton(removeButtonContainer); - m_pRemoveBtn->setIcon(QIcon(appDir + "/Res/Icon/nmTimeDepSkin3.png")); + m_pRemoveBtn->setIcon(zxLoadIcon("nmTimeDepSkin3")); m_pRemoveBtn->setIconSize(QSize(36, 36)); m_pRemoveBtn->setFixedSize(40, 40); @@ -130,16 +132,15 @@ void nmWxPerforationClosing::initUI() toolbarLayout->addSpacing(5); // 标签和下拉框之间的小间距 + // 单位和主窗口统一从底层配置读取。 m_pTimeUnitComboBox = new QComboBox(); - m_pTimeUnitComboBox->addItem("ms"); - m_pTimeUnitComboBox->addItem("sec"); - m_pTimeUnitComboBox->addItem("min"); - m_pTimeUnitComboBox->addItem("hr"); - m_pTimeUnitComboBox->addItem("day"); - m_pTimeUnitComboBox->addItem("week"); - m_pTimeUnitComboBox->addItem("month"); - m_pTimeUnitComboBox->addItem("year"); - m_pTimeUnitComboBox->setCurrentIndex(3); // "hr" + iUnitGroup* group = iUnitHelper::getUnitGroupByUnit("s"); + const QStringList units = group ? group->getAllUnitNames() : (QStringList() << "hr"); + m_pTimeUnitComboBox->addItems(units); + if(m_currentTimeUnit == "hr" && units.contains("h")) m_currentTimeUnit = "h"; + int unitIndex = m_pTimeUnitComboBox->findText(m_currentTimeUnit); + m_pTimeUnitComboBox->setCurrentIndex(unitIndex >= 0 ? unitIndex : 0); + m_currentTimeUnit = m_pTimeUnitComboBox->currentText(); m_pTimeUnitComboBox->setFixedWidth(60); toolbarLayout->addWidget(m_pTimeUnitComboBox); @@ -152,6 +153,8 @@ void nmWxPerforationClosing::initUI() // 创建单个树形表格控件 m_pTreeWidget = new QTreeWidget(this); m_pTreeWidget->setColumnCount(3); + // 时间列使用框架编辑代理,保留树节点及日期模式的只读规则。 + m_pTreeWidget->setItemDelegateForColumn(1, new ZxDataEditorDelegate(m_pTreeWidget)); // 设置表头 QStringList headers; @@ -797,9 +800,23 @@ void nmWxPerforationClosing::loadDataFromModel() double nmWxPerforationClosing::convertTime(double value, const QString& fromUnit, const QString& toUnit) const { - nmDataAttribute a; - a.setAttributeUnitType(UNIT_TYPE_TIME); - return a.convertValueByUnitType(value, fromUnit, toUnit); + // 直接使用底层单位组,兼容内部 hr/sec 与配置中的 h/s 写法。 + iUnitGroup* group = iUnitHelper::getUnitGroupByUnit("s"); + if(group) { + QString source = fromUnit == "sec" ? "s" : fromUnit; + QString destination = toUnit == "sec" ? "s" : toUnit; + if(group->indexOf("h") >= 0) { + if(source == "hr") source = "h"; + if(destination == "hr") destination = "h"; + } + double converted = value; + int digits = 6; + if(group->convert(source, value, destination, converted, digits)) return converted; + } + // 单位配置未加载时,沿用原有的内部时间换算。 + nmDataAttribute attribute; + attribute.setAttributeUnitType(UNIT_TYPE_TIME); + return attribute.convertValueByUnitType(value, fromUnit, toUnit); } double nmWxPerforationClosing::roundToSecondDouble(double sec) const diff --git a/Src/nmNum/nmSubWxs/nmWxTimeDependentSkin.cpp b/Src/nmNum/nmSubWxs/nmWxTimeDependentSkin.cpp index 98dab9cd..06bd79df 100644 --- a/Src/nmNum/nmSubWxs/nmWxTimeDependentSkin.cpp +++ b/Src/nmNum/nmSubWxs/nmWxTimeDependentSkin.cpp @@ -3,6 +3,10 @@ #include "nmDialogPreviewGuard.h" #include "nmWxPressFlowChartWidget.h" #include "nmWxChartWidget.h" +#include "iUnitGroup.h" +#include "iUnitHelper.h" +#include "ZxDataEditorDelegate.h" +#include "iBase_global.h" #include #include @@ -237,7 +241,7 @@ void nmWxTimeDependentSkin::initUI() m_pToolbarLayout->setContentsMargins(0, 0, 0, 0); // 创建按钮并设置统一样式 - auto createButton = [&](QPushButton*& button, const QString & iconPath, const QString & text) { + auto createButton = [&](QPushButton*& button, const QString & iconName, const QString & text) { QWidget* buttonContainer = new QWidget(this); QVBoxLayout* containerLayout = new QVBoxLayout(buttonContainer); containerLayout->setSpacing(0); @@ -245,7 +249,8 @@ void nmWxTimeDependentSkin::initUI() // 图标按钮 button = new QPushButton(buttonContainer); - button->setIcon(QIcon(iconPath)); + // 统一使用框架的图标查找和缓存,保留原按钮大小和位置。 + button->setIcon(zxLoadIcon(iconName)); button->setIconSize(QSize(36, 36)); button->setFixedSize(40, 40); @@ -264,18 +269,15 @@ void nmWxTimeDependentSkin::initUI() m_pToolbarLayout->addWidget(buttonContainer); }; - QString appDir = QCoreApplication::applicationDirPath(); - appDir = appDir.section('/', 0, -2); // 获取上一级目录 - // 创建所有按钮 - createButton(m_pSkinVsRateBtn, appDir + "/Res/Icon/nmTimeDepSkin1.png", tr("Skin vs Rate")); - createButton(m_pAddBtn, appDir + "/Res/Icon/nmTimeDepSkin2.png", tr("Add")); - createButton(m_pRemoveBtn, appDir + "/Res/Icon/nmTimeDepSkin3.png", tr("Remove")); - createButton(m_pSplitStepsBtn, appDir + "/Res/Icon/nmTimeDepSkin4.png", tr("Split Steps")); - createButton(m_pComputeBtn, appDir + "/Res/Icon/nmTimeDepSkin5.png", tr("Compute")); + createButton(m_pSkinVsRateBtn, "nmTimeDepSkin1", tr("Skin vs Rate")); + createButton(m_pAddBtn, "nmTimeDepSkin2", tr("Add")); + createButton(m_pRemoveBtn, "nmTimeDepSkin3", tr("Remove")); + createButton(m_pSplitStepsBtn, "nmTimeDepSkin4", tr("Split Steps")); + createButton(m_pComputeBtn, "nmTimeDepSkin5", tr("Compute")); // 计算功能尚未实现,隐藏图标和文字容器,让射孔封堵前移到该位置。 m_pComputeBtn->parentWidget()->hide(); - createButton(m_pPerforationClosingBtn, appDir + "/Res/Icon/nmTimeDepSkin6.png", tr("Perforation Closing")); + createButton(m_pPerforationClosingBtn, "nmTimeDepSkin6", tr("Perforation Closing")); m_pToolbarLayout->addStretch(); @@ -369,6 +371,7 @@ void nmWxTimeDependentSkin::initUI() m_pChartLayout->addWidget(m_pChartSplitter, 1); // 创建图表下方的下拉框 + // 使用普通下拉框,按现有固定宽度显示完整的射孔名称。 m_pComboBox = new QComboBox(); m_pComboBox->setFixedWidth(200); // 增加宽度以容纳射孔段名称 @@ -405,24 +408,9 @@ void nmWxTimeDependentSkin::initUI() // 表头下的“单位”行 m_pDataTable->insertRow(0); - // 第二列:时间单位下拉框(统一用小写,匹配 convertTimeUnit) - m_pTimeUnitCombo = new QComboBox(m_pDataTable); - QStringList units; - units << "sec" << "min" << "hr" << "day" << "week" << "month" << "year"; - - if(m_timeDisplayUnit.isEmpty()) m_timeDisplayUnit = "hr"; - - int idx = units.indexOf(m_timeDisplayUnit); - m_pTimeUnitCombo->addItems(units); - m_pTimeUnitCombo->setCurrentIndex(idx >= 0 ? idx : units.indexOf("hr")); - m_pTimeUnitCombo->setEditable(true); - m_pTimeUnitCombo->lineEdit()->setAlignment(Qt::AlignCenter); - m_pTimeUnitCombo->lineEdit()->setReadOnly(true); - m_pTimeUnitCombo->setStyleSheet( - "QComboBox { border: none; background: transparent; padding-left: 4px; }" - "QComboBox::drop-down { border: none; }" - ); - m_pDataTable->setCellWidget(0, 1, m_pTimeUnitCombo); + // 初次创建和切换表格结构时共用同一套框架单位控件。 + createTimeUnitCombo(); + m_pDataTable->setItemDelegate(new ZxDataEditorDelegate(m_pDataTable)); // 第三列:占位(Skin 无单位) QTableWidgetItem* empty = new QTableWidgetItem(""); @@ -432,10 +420,6 @@ void nmWxTimeDependentSkin::initUI() // 行高 m_pDataTable->setRowHeight(0, 24); - // 信号连接 - connect(m_pTimeUnitCombo, SIGNAL(currentIndexChanged(const QString &)), - this, SLOT(onTimeUnitChanged(const QString &))); - // 设置第一列的固定宽度 m_pDataTable->setColumnWidth(0, 20); @@ -655,7 +639,8 @@ void nmWxTimeDependentSkin::updateTableFromFlowSegments() m_pDataTable->setItem(row, 3, dsdqItem); } - dsdqItem->setText(QString::number(disp, 'f', 5)); + // 不同流量单位的倍率差异较大,保留有效位以免小系数显示为零。 + dsdqItem->setText(QString::number(disp, 'g', 15)); } } @@ -708,16 +693,15 @@ void nmWxTimeDependentSkin::createTimeUnitCombo() return; } - // 原有的创建时间单位下拉框的代码 + // 单位列表由底层配置提供,旧的 hr 名称映射到配置中的小时单位。 m_pTimeUnitCombo = new QComboBox(m_pDataTable); - QStringList units; - units << "sec" << "min" << "hr" << "day" << "week" << "month" << "year"; - - if(m_timeDisplayUnit.isEmpty()) m_timeDisplayUnit = "hr"; - + iUnitGroup* group = iUnitHelper::getUnitGroupByUnit("s"); + const QStringList units = group ? group->getAllUnitNames() : (QStringList() << "hr"); + if(m_timeDisplayUnit == "hr" && units.contains("h")) m_timeDisplayUnit = "h"; int idx = units.indexOf(m_timeDisplayUnit); m_pTimeUnitCombo->addItems(units); - m_pTimeUnitCombo->setCurrentIndex(idx >= 0 ? idx : units.indexOf("hr")); + m_pTimeUnitCombo->setCurrentIndex(idx >= 0 ? idx : 0); + m_timeDisplayUnit = m_pTimeUnitCombo->currentText(); m_pTimeUnitCombo->setEditable(true); m_pTimeUnitCombo->lineEdit()->setAlignment(Qt::AlignCenter); m_pTimeUnitCombo->lineEdit()->setReadOnly(true); @@ -733,15 +717,23 @@ void nmWxTimeDependentSkin::createTimeUnitCombo() void nmWxTimeDependentSkin::createDSdQUnitCombo() { + // 复用底层流量单位组,显示为整个流量单位的倒数。 m_pDSdQUnitCombo = new QComboBox(m_pDataTable); + iUnitGroup* group = iUnitHelper::getUnitGroupByUnit("stb/d"); QStringList qUnits; - qUnits << "1/B/D" << "1/MMm^3/D" << "1/Mcf/D" << "1/Mm^3/D" << "1/Mm^3/hr"; + if(group) { + const QStringList units = group->getAllUnitNames(); + for(int i = 0; i < units.size(); ++i) qUnits << "1/(" + units[i] + ")"; + } + if(qUnits.isEmpty()) qUnits << "1/B/D"; m_pDSdQUnitCombo->addItems(qUnits); - if(m_dSdQDisplayUnit.isEmpty()) m_dSdQDisplayUnit = "1/B/D"; + if(m_dSdQDisplayUnit == "1/B/D") + m_dSdQDisplayUnit = "1/(stb/d)"; int qIdx = qUnits.indexOf(m_dSdQDisplayUnit); - m_pDSdQUnitCombo->setCurrentIndex(qIdx >= 0 ? qIdx : qUnits.indexOf("1/B/D")); + m_pDSdQUnitCombo->setCurrentIndex(qIdx >= 0 ? qIdx : 0); + m_dSdQDisplayUnit = m_pDSdQUnitCombo->currentText(); m_pDSdQUnitCombo->setStyleSheet( "QComboBox { border: none; background: transparent; padding-left: 4px; }" "QComboBox::drop-down { border: none; }" @@ -1266,8 +1258,7 @@ void nmWxTimeDependentSkin::restoreToolbarState() void nmWxTimeDependentSkin::createChartModeBtn() { - const QString sIconDir = QCoreApplication::applicationDirPath() - .section('/', 0, -2) + "/Res/Icon/"; + // 图表模式也通过框架加载图标,与普通模式保持一致。 // 创建Filter按钮 m_pFilterContainer = new QWidget(this); @@ -1276,7 +1267,7 @@ void nmWxTimeDependentSkin::createChartModeBtn() filterLayout->setContentsMargins(2, 2, 2, 2); m_pFilterBtn = new QPushButton(m_pFilterContainer); - m_pFilterBtn->setIcon(QIcon(sIconDir + "NmFilter.png")); + m_pFilterBtn->setIcon(zxLoadIcon("NmFilter")); m_pFilterBtn->setIconSize(QSize(36, 36)); m_pFilterBtn->setFixedSize(40, 40); m_pFilterBtn->setEnabled(true); @@ -1298,7 +1289,7 @@ void nmWxTimeDependentSkin::createChartModeBtn() resetFilterLayout->setContentsMargins(2, 2, 2, 2); m_pResetFilterBtn = new QPushButton(m_pResetFilterContainer); - m_pResetFilterBtn->setIcon(QIcon(sIconDir + "NmResetFilter.png")); + m_pResetFilterBtn->setIcon(zxLoadIcon("NmResetFilter")); m_pResetFilterBtn->setIconSize(QSize(36, 36)); m_pResetFilterBtn->setFixedSize(40, 40); m_pResetFilterBtn->setEnabled(true); @@ -1320,7 +1311,7 @@ void nmWxTimeDependentSkin::createChartModeBtn() resetLinesLayout->setContentsMargins(2, 2, 2, 2); m_pResetLinesBtn = new QPushButton(m_pResetLinesContainer); - m_pResetLinesBtn->setIcon(QIcon(sIconDir + "NmResetLines.png")); + m_pResetLinesBtn->setIcon(zxLoadIcon("NmResetLines")); m_pResetLinesBtn->setIconSize(QSize(36, 36)); m_pResetLinesBtn->setFixedSize(40, 40); @@ -1649,13 +1640,14 @@ void nmWxTimeDependentSkin::onTableDataChanged(int row, int column) // 回退显示 double base = segments[segRow].dSdQ.getValue().toDouble(); double disp = convertDSdQ(base, "1/B/D", m_dSdQDisplayUnit); - item->setText(QString::number(disp, 'f', 5)); + item->setText(QString::number(disp, 'g', 15)); return; } // 转回“内部基准单位 1/B/D”存储 double base = convertDSdQ(dispVal, m_dSdQDisplayUnit, "1/B/D"); - segments[segRow].dSdQ.setValue(QString::number(base, 'f', 5)); + // 使用有效位保存,避免换算到内部单位后小系数被截断为零。 + segments[segRow].dSdQ.setValue(QString::number(base, 'g', 15)); // 强制其单位为基准(可选:不设置也行,因为我们按基准解读) segments[segRow].dSdQ.setUnit("1/B/D"); @@ -1664,7 +1656,7 @@ void nmWxTimeDependentSkin::onTableDataChanged(int row, int column) // 再把显示值(按当前显示单位)回填一次,确保格式 double disp = convertDSdQ(base, "1/B/D", m_dSdQDisplayUnit); - item->setText(QString::number(disp, 'f', 5)); + item->setText(QString::number(disp, 'g', 15)); return; } } @@ -2605,11 +2597,25 @@ void nmWxTimeDependentSkin::snapAllBoundariesToFlowStepsAndMerge() } -double nmWxTimeDependentSkin::convertTime(double value, const QString& fromUnit, const QString& toUnit) const +double nmWxTimeDependentSkin::convertTime(double value, const QString& fromUnit, const QString& toUnit) { - nmDataAttribute a; - a.setAttributeUnitType(UNIT_TYPE_TIME); - return a.convertValueByUnitType(value, fromUnit, toUnit); + // 直接使用底层单位组,兼容内部 hr/sec 与配置中的 h/s 写法。 + iUnitGroup* group = iUnitHelper::getUnitGroupByUnit("s"); + if(group) { + QString source = fromUnit == "sec" ? "s" : fromUnit; + QString destination = toUnit == "sec" ? "s" : toUnit; + if(group->indexOf("h") >= 0) { + if(source == "hr") source = "h"; + if(destination == "hr") destination = "h"; + } + double converted = value; + int digits = 6; + if(group->convert(source, value, destination, converted, digits)) return converted; + } + // 单位配置未加载时,沿用原有的内部时间换算。 + nmDataAttribute attribute; + attribute.setAttributeUnitType(UNIT_TYPE_TIME); + return attribute.convertValueByUnitType(value, fromUnit, toUnit); } void nmWxTimeDependentSkin::onTimeUnitChanged(const QString& unit) @@ -2618,33 +2624,21 @@ void nmWxTimeDependentSkin::onTimeUnitChanged(const QString& unit) m_timeDisplayUnit = unit; - const QVector& segments = m_perforationData.getFlowSegments(); - - for(int i = 0; i < segments.size(); ++i) { - int row = i + 1; - double startHr = segments[i].segmentStart.getValue().toDouble(); - double startSec = toSeconds(startHr, "hr"); // 量化 - double startDisp = fromSeconds(startSec, m_timeDisplayUnit); // 按新单位显示 - QTableWidgetItem* item = m_pDataTable->item(row, 1); - - if(item) item->setText(QString::number(startDisp, 'f', 5)); - } + // 复用已屏蔽编辑信号的刷新入口,切换显示单位不回写分段数据。 + updateTableFromFlowSegments(); } double nmWxTimeDependentSkin::toSeconds(double value, const QString& unit) { - nmDataAttribute a; - a.setAttributeUnitType(UNIT_TYPE_TIME); - double sec = a.convertValueByUnitType(value, unit, "sec"); + double sec = convertTime(value, unit, "sec"); // 四舍五入到整数秒 return static_cast(qRound(sec)); } double nmWxTimeDependentSkin::fromSeconds(double sec, const QString& unit) { - nmDataAttribute a; - a.setAttributeUnitType(UNIT_TYPE_TIME); - return a.convertValueByUnitType(sec, "sec", unit); + // 从内部秒值转换为框架下拉框选中的单位。 + return convertTime(sec, "sec", unit); } bool nmWxTimeDependentSkin::moveFlowSegmentBoundarySec(int segmentIndex, double newBoundarySec) @@ -2679,9 +2673,15 @@ bool nmWxTimeDependentSkin::moveFlowSegmentBoundarySec(int segmentIndex, double double nmWxTimeDependentSkin::convertDSdQ(double value, const QString& fromUnit, const QString& toUnit) const { - nmDataAttribute a; - a.setAttributeUnitType(UNIT_TYPE_FLOW_RATE_RECIPROCAL); - return a.convertValueByUnitType(value, fromUnit, toUnit); + // 直接复用流量组反向换算;旧数据 1/B/D 对应底层 stb/d。 + if(fromUnit == toUnit) return value; + const QString source = fromUnit == "1/B/D" ? "stb/d" : fromUnit.mid(3, fromUnit.size() - 4); + const QString destination = toUnit == "1/B/D" ? "stb/d" : toUnit.mid(3, toUnit.size() - 4); + iUnitGroup* group = iUnitHelper::getUnitGroupByUnit("stb/d"); + double factor = 1.0; + int digits = 6; + if(group && group->convert(destination, 1.0, source, factor, digits)) return value * factor; + return value; } void nmWxTimeDependentSkin::onAddRateDependentToggled(bool checked) @@ -2701,29 +2701,8 @@ void nmWxTimeDependentSkin::onDSdQUnitChanged(const QString& unit) m_dSdQDisplayUnit = unit; - const QVector& segs = m_perforationData.getFlowSegments(); - - if(!m_perforationData.isRateDependentEnabled()) return; - - for(int i = 0; i < segs.size(); ++i) { - int row = i + 1; - const FlowSegmentData& s = segs[i]; - - // 内部一律以 "1/B/D" 存储,这里只负责回显 - double base = s.dSdQ.getValue().toDouble(); // unit 视为 "1/B/D" - double disp = convertDSdQ(base, "1/B/D", m_dSdQDisplayUnit); - - QTableWidgetItem* item = m_pDataTable->item(row, 3); - - if(!item) { - item = new QTableWidgetItem(); - m_pDataTable->setItem(row, 3, item); - } - - item->setText(QString::number(disp, 'f', 5)); - item->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter); - item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsEditable); - } + // 切换单位只刷新显示,避免 cellChanged 把换算结果重复写回数据。 + updateTableFromFlowSegments(); } void nmWxTimeDependentSkin::refreshTableStructure() From cbc3e02b2720217bbadd6d27593f8c1409403d6a Mon Sep 17 00:00:00 2001 From: lvjunjie Date: Fri, 30 Oct 2026 15:15:26 +0800 Subject: [PATCH 5/9] =?UTF-8?q?fix(nmNum):=20=E4=BF=AE=E5=A4=8D=E7=BB=93?= =?UTF-8?q?=E6=9E=9C=E7=AA=97=E5=8F=A3=E6=9C=80=E5=B0=8F=E5=8C=96=E9=A2=84?= =?UTF-8?q?=E8=A7=88=E5=8F=8A=E5=85=B3=E9=97=AD=E9=87=8D=E5=BC=80=E7=8A=B6?= =?UTF-8?q?=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Include/nmNum/nmSubWxs/nmDialogPreviewGuard.h | 38 ++++--- Src/nmNum/nmSubWnd/nmSubWndMain.cpp | 106 ++++++++++++++++-- 2 files changed, 120 insertions(+), 24 deletions(-) diff --git a/Include/nmNum/nmSubWxs/nmDialogPreviewGuard.h b/Include/nmNum/nmSubWxs/nmDialogPreviewGuard.h index 73d8b71a..64052aa0 100644 --- a/Include/nmNum/nmSubWxs/nmDialogPreviewGuard.h +++ b/Include/nmNum/nmSubWxs/nmDialogPreviewGuard.h @@ -1,4 +1,4 @@ -#ifndef NM_DIALOG_PREVIEW_GUARD_H +#ifndef NM_DIALOG_PREVIEW_GUARD_H #define NM_DIALOG_PREVIEW_GUARD_H #include @@ -20,6 +20,26 @@ public: window->installEventFilter(this); } + // 已隐藏的窗口可立即清理,供结果窗在恢复尺寸前解除原生最小化状态。 + void resetHiddenWindow() + { + cancelPendingReset(); + if(m_window.isNull() || m_window->isVisible() || !m_window->isWindow() || + !m_window->internalWinId()) return; + + m_rebuilding = true; + // 重建原生窗口会触发事件,期间本对象可能被释放,用弱指针检查是否仍然存活。 + QPointer alive(this); + const QRect geometry = m_window->geometry(); + // 在当前界面库的系统后端,即使父对象和标志不变,双参数调用也会重建原生窗口。 + // 子控件的原生父窗口由界面库同步调整,不能绕过界面库直接销毁系统句柄。 + m_window->setParent(m_window->parentWidget(), m_window->windowFlags()); + if(alive.isNull()) return; + // 恢复原有位置和尺寸,确保下次打开时窗口布局不因句柄重建而改变。 + if(!m_window.isNull()) m_window->setGeometry(geometry); + if(!alive.isNull()) m_rebuilding = false; + } + protected: // 只监听目标窗口的显式隐藏和重新显示;重建过程产生的事件不重复处理。 bool eventFilter(QObject* object, QEvent* event) @@ -52,21 +72,7 @@ protected: QObject::timerEvent(event); return; } - cancelPendingReset(); - if(m_window.isNull() || m_window->isVisible() || !m_window->isWindow() || - !m_window->internalWinId()) return; - - m_rebuilding = true; - // 重建原生窗口会触发事件,期间本对象可能被释放,用弱指针检查是否仍然存活。 - QPointer alive(this); - const QRect geometry = m_window->geometry(); - // 在当前界面库的系统后端,即使父对象和标志不变,双参数调用也会重建原生窗口。 - // 子控件的原生父窗口由界面库同步调整,不能绕过界面库直接销毁系统句柄。 - m_window->setParent(m_window->parentWidget(), m_window->windowFlags()); - if(alive.isNull()) return; - // 恢复原有位置和尺寸,确保下次打开时窗口布局不因句柄重建而改变。 - if(!m_window.isNull()) m_window->setGeometry(geometry); - if(!alive.isNull()) m_rebuilding = false; + resetHiddenWindow(); } private: diff --git a/Src/nmNum/nmSubWnd/nmSubWndMain.cpp b/Src/nmNum/nmSubWnd/nmSubWndMain.cpp index 87cbfd21..bd355dd5 100644 --- a/Src/nmNum/nmSubWnd/nmSubWndMain.cpp +++ b/Src/nmNum/nmSubWnd/nmSubWndMain.cpp @@ -107,8 +107,100 @@ #include #include +#ifdef Q_OS_WIN +#include +#include +#pragma comment(lib, "dwmapi.lib") +#pragma comment(lib, "gdi32.lib") +#endif + namespace { +// 结果窗保留原生最小化小框,仅替换最小化期间的系统预览。 +class nmResultDialog : public QDialog +{ +public: + explicit nmResultDialog(QWidget* parent, Qt::WindowFlags flags) + : QDialog(parent, flags), m_previewGuard(new nmDialogPreviewGuard(this)), + m_nativeMinimized(false) + { + } + + void hideAndRestoreGeometry() + { + // 先隐藏再恢复,避免关闭小框时在原位置短暂显示大窗。 + if(!isMinimized()) return; + const QRect geometryBeforeMinimize = m_geometryBeforeMinimize; + const Qt::WindowStates restoredState = windowState() & ~Qt::WindowMinimized; + hide(); + setWindowState(restoredState); + // 复用预览清理来重建隐藏句柄,否则 Qt 4 调整原生最小化窗口尺寸时会重新显示它。 + m_previewGuard->resetHiddenWindow(); + if(!(restoredState & Qt::WindowMaximized) && geometryBeforeMinimize.isValid()) + setGeometry(geometryBeforeMinimize); + setWindowState(restoredState); + m_nativeMinimized = false; + } + +#ifdef Q_OS_WIN +protected: + bool winEvent(MSG* message, long* result) override + { + // 主窗口恢复时仅显示原来的小框,避免系统默认处理将结果窗展开。 + if(message->message == WM_SHOWWINDOW && message->wParam && + message->lParam == SW_PARENTOPENING && IsIconic(message->hwnd)) { + ShowWindow(message->hwnd, SW_SHOWMINNOACTIVE); + *result = 0; + return true; + } + + if(message->message == WM_SIZE) { + // 恢复或最大化后交还实时预览;窗口句柄重建后也由尺寸消息重新设置。 + const BOOL minimized = message->wParam == SIZE_MINIMIZED; + // Qt 此时尚未处理最小化尺寸消息,仍保留大窗的客户区位置和尺寸。 + if(minimized && !m_nativeMinimized) m_geometryBeforeMinimize = geometry(); + m_nativeMinimized = minimized; + DwmSetWindowAttribute(message->hwnd, DWMWA_FORCE_ICONIC_REPRESENTATION, + &minimized, sizeof(minimized)); + DwmSetWindowAttribute(message->hwnd, DWMWA_HAS_ICONIC_BITMAP, + &minimized, sizeof(minimized)); + DwmInvalidateIconicBitmaps(message->hwnd); + } + + if((message->message == WM_DWMSENDICONICTHUMBNAIL || + message->message == WM_DWMSENDICONICLIVEPREVIEWBITMAP) && + IsIconic(message->hwnd)) { + // 透明位图只作用于任务栏预览,不隐藏结果窗或替换其原生标题栏。 + BITMAPINFO info = {0}; + info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); + info.bmiHeader.biWidth = 1; + info.bmiHeader.biHeight = -1; + info.bmiHeader.biPlanes = 1; + info.bmiHeader.biBitCount = 32; + void* pixels = NULL; + HBITMAP bitmap = CreateDIBSection(NULL, &info, DIB_RGB_COLORS, &pixels, NULL, 0); + if(bitmap) { + *static_cast(pixels) = 0; + const HRESULT status = message->message == WM_DWMSENDICONICTHUMBNAIL + ? DwmSetIconicThumbnail(message->hwnd, bitmap, 0) + : DwmSetIconicLivePreviewBitmap(message->hwnd, bitmap, NULL, 0); + DeleteObject(bitmap); + if(SUCCEEDED(status)) { + *result = 0; + return true; + } + } + } + return QDialog::winEvent(message, result); + } +#endif + +private: + nmDialogPreviewGuard* m_previewGuard; + QRect m_geometryBeforeMinimize; + bool m_nativeMinimized; // 主窗口恢复时可能重复通知最小化,不重复记录小框尺寸。 +}; + // 保留原来求解器单例的防重入语义:全局同一时间只允许一个DLL求解任务运行。 // QPointer会在QObject销毁后自动变空,避免保留已经释放的线程指针。 QPointer s_pRunningSolverTask; @@ -135,9 +227,8 @@ void clearResultCurveDataTables(iGuiPlot* plot) void resetResultDialog(QDialog* window) { if(!window) return; - // 先恢复最小化状态再隐藏,避免隐藏后调整尺寸使系统重新显示空白窗口。 - if(window->isMinimized()) - window->setWindowState(window->windowState() & ~Qt::WindowMinimized); + // 最小化结果先在隐藏状态下恢复尺寸,再清理旧曲线。 + static_cast(window)->hideAndRestoreGeometry(); window->hide(); foreach(iGuiPlot* plot, window->findChildren()) clearResultCurveDataTables(plot); foreach(nmWxSensitivityShow* dialog, window->findChildren()) @@ -1978,12 +2069,11 @@ void nmSubWndMain::onForecastDataChanged() QDialog* nmSubWndMain::createResultDialog(const QString& name, const QString& title) { // 关闭只隐藏,窗口及绘图控件由所属分析统一销毁。 - QDialog* dialog = new QDialog(m_pResultRefreshFitting.data(), + QDialog* dialog = new nmResultDialog(m_pResultRefreshFitting.data(), Qt::WindowTitleHint | Qt::WindowSystemMenuHint | Qt::WindowMinMaxButtonsHint | Qt::WindowCloseButtonHint); dialog->setObjectName(name); dialog->setWindowTitle(title); dialog->setAttribute(Qt::WA_DeleteOnClose, false); - new nmDialogPreviewGuard(dialog); dialog->resize(900, 650); QVBoxLayout* layout = new QVBoxLayout(dialog); layout->setContentsMargins(0, 0, 0, 0); @@ -2154,10 +2244,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); + if(dialog && event->type() == QEvent::Close) + static_cast(dialog)->hideAndRestoreGeometry(); // 关闭结果对话框时同步关闭其曲线数据表、Show 和统计设置窗口。 if((event->type() == QEvent::Close || event->type() == QEvent::Hide) && qobject_cast(watched)) { From d3085cdd822fe540251bc84858926e31e33f4b77 Mon Sep 17 00:00:00 2001 From: lvjunjie Date: Fri, 30 Oct 2026 16:36:34 +0800 Subject: [PATCH 6/9] =?UTF-8?q?feat(nmNum):=20=E7=BB=9F=E4=B8=80=E9=A2=84?= =?UTF-8?q?=E6=B5=8B=E7=AA=97=E5=8F=A3=E8=A1=A8=E6=A0=BC=E6=A0=B7=E5=BC=8F?= =?UTF-8?q?=E5=8F=8A=E5=8D=95=E4=BD=8D=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 参考敏感性分析统一制度表表头、行号、隔行底色及网格样式。 - 初始压力、持续时间和产量复用底层单位绑定及换算接口。 - 兼容旧单位数据与保存重开,保留压力精度及制度输入校验。 --- Include/nmNum/nmSubWxs/nmWxForecast.h | 5 +- Src/nmNum/nmSubWxs/nmWxForecast.cpp | 262 ++++++++++++++++++-------- 2 files changed, 191 insertions(+), 76 deletions(-) diff --git a/Include/nmNum/nmSubWxs/nmWxForecast.h b/Include/nmNum/nmSubWxs/nmWxForecast.h index 6895b9fc..fe02a558 100644 --- a/Include/nmNum/nmSubWxs/nmWxForecast.h +++ b/Include/nmNum/nmSubWxs/nmWxForecast.h @@ -120,7 +120,10 @@ private: QGridLayout *m_parametersLayout; // Base parameters - nmGUIComponentLineEdit *m_initialPressureComponent; + QLineEdit *m_initialPressureEdit; + QComboBox *m_initialPressureUnit; + nmDataAttribute *m_initialPressure; ///< 当前预测模式的初始压力。 + iUnitWxBinder *m_pressureUnitBinder; QCheckBox *m_abandonmentRateCheck; nmGUIComponentLineEdit *m_abandonmentRateComponent; diff --git a/Src/nmNum/nmSubWxs/nmWxForecast.cpp b/Src/nmNum/nmSubWxs/nmWxForecast.cpp index 37e98109..345775f1 100644 --- a/Src/nmNum/nmSubWxs/nmWxForecast.cpp +++ b/Src/nmNum/nmSubWxs/nmWxForecast.cpp @@ -13,13 +13,75 @@ #include "nmGUIComponentLineEdit.h" #include "nmPebiResultSnapshot.h" #include "ZxBaseUtil.h" +#include "iUnitGroup.h" +#include "iUnitHelper.h" +#include "iUnitWxBinder.h" +#include "nmTranslationManager.h" + +namespace { +iUnitGroup* forecastUnitGroup(UnitType type) +{ + // 与参数面板共用底层时间、绝对压力和体积流量单位组。 + return iUnitHelper::getUnitGroupByUnit(type == UNIT_TYPE_TIME ? "s" : + type == UNIT_TYPE_PRESSURE ? "MPa" : "stb/d"); +} + +QString forecastUnitName(iUnitGroup* group, QString unit) +{ + // 只统一旧数据的名称写法,倍率仍由底层单位组提供。 + unit.replace("^", "").replace(QChar(0x00b3), "3"); + unit.replace("/D", "/d").replace("sec", "s").replace("hr", "h"); + if(unit == "day" || unit == "Day") unit = "d"; + if(unit == "psia") unit = "psi"; + if(unit == "bara") unit = "bar"; + if(unit == "KPa") unit = "kPa"; + if(unit == "B/d") unit = "stb/d"; + if(group) { + foreach(const QString& name, group->getAllUnitNames()) { + if(forecastUnitName(NULL, name) == unit) return name; + } + } + return unit; +} + +QStringList forecastUnits(const nmDataAttribute& attribute) +{ + // 下拉选项直接取软件单位配置。 + iUnitGroup* group = forecastUnitGroup(attribute.getAttributeUnitType()); + return group ? group->getAllUnitNames() : attribute.getListUnitSelections(); +} + +double convertForecastValue(const nmDataAttribute& attribute, double value, + const QString& fromUnit, const QString& toUnit) +{ + if(fromUnit == toUnit) return value; + iUnitGroup* group = forecastUnitGroup(attribute.getAttributeUnitType()); + if(!group) return attribute.convertValueByUnitType(value, fromUnit, toUnit); + QString source = forecastUnitName(group, fromUnit); + const QString destination = forecastUnitName(group, toUnit); + // 旧项目可能保存了周、月或旧流量前缀,先按原定义转入底层支持的单位。 + if(group->indexOf(source) < 0 && (attribute.getListUnitSelections().contains(fromUnit) || + nmDataAttribute::detectUnitType(fromUnit) == attribute.getAttributeUnitType())) { + const QString bridge = attribute.getAttributeUnitType() == UNIT_TYPE_TIME ? "sec" : + attribute.getAttributeUnitType() == UNIT_TYPE_PRESSURE ? "MPa" : "m^3/D"; + value = attribute.convertValueByUnitType(value, fromUnit, bridge); + source = forecastUnitName(group, bridge); + } + double converted = 0.0; + int digits = 6; + return group->convert(source, value, destination, converted, digits) ? converted : qQNaN(); +} +} nmWxForecast::nmWxForecast(QWidget *parent, nmDataAnalyzeManager* manager) : iDlgBase(parent) , m_manager(manager ? manager : nmDataAnalyzeManager::getCurrentInstance()) , m_task(NULL) , m_calculationRunning(false) - , m_initialPressureComponent(nullptr) + , m_initialPressureEdit(nullptr) + , m_initialPressureUnit(nullptr) + , m_initialPressure(nullptr) + , m_pressureUnitBinder(nullptr) , m_abandonmentRateComponent(nullptr) , m_maximumRateComponent(nullptr) , m_forecastDurationComponent(nullptr) @@ -44,11 +106,11 @@ nmWxForecast::nmWxForecast(QWidget *parent, nmDataAnalyzeManager* manager) new nmDialogPreviewGuard(this); PressureRow tempDefaults; - m_currentDurationUnit = tempDefaults.duration.getUnit(); - m_currentPressureUnit = tempDefaults.pressure.getUnit(); + m_currentDurationUnit = forecastUnitName(forecastUnitGroup(UNIT_TYPE_TIME), tempDefaults.duration.getUnit()); + m_currentPressureUnit = forecastUnitName(forecastUnitGroup(UNIT_TYPE_PRESSURE), tempDefaults.pressure.getUnit()); FlowRow tempFlowDefaults; - m_currentRateUnit = tempFlowDefaults.q0.getUnit(); + m_currentRateUnit = forecastUnitName(forecastUnitGroup(UNIT_TYPE_FLOW_RATE), tempFlowDefaults.q0.getUnit()); // 先构建UI setupUI(); @@ -59,12 +121,6 @@ nmWxForecast::nmWxForecast(QWidget *parent, nmDataAnalyzeManager* manager) // 根据加载的数据更新界面 updateParametersForOption(); - // 编辑内容保留在窗口中,点击生成后再提交到数据中心。 - const QList units = m_initialPressureComponent->findChildren(); - foreach(QComboBox* unit, units) { - const int index = unit->findData(m_forecastData.getMultipleRatesData().pressureUnit); - if(index >= 0) unit->setCurrentIndex(index); - } if(m_manager) { connect(m_manager, SIGNAL(sigPebiResultSnapshotChanged()), this, SLOT(onModelChanged())); connect(m_manager, SIGNAL(sigPebiResultSnapshotChanged()), this, SLOT(reloadInputData())); @@ -92,18 +148,20 @@ void nmWxForecast::loadDataFromManager() if(m_manager) m_forecastData = m_manager->getForecastDataCopy(); m_forecastData.setResult(QSharedPointer()); const MultipleRates& data = m_forecastData.getMultipleRatesData(); - if(m_rDurationUnitCombo->findText(data.durationUnit) >= 0) m_currentDurationUnit = data.durationUnit; - if(m_rRateUnitCombo->findText(data.rateUnit) >= 0) m_currentRateUnit = data.rateUnit; + const QString durationUnit = forecastUnitName(forecastUnitGroup(UNIT_TYPE_TIME), data.durationUnit); + const QString rateUnit = forecastUnitName(forecastUnitGroup(UNIT_TYPE_FLOW_RATE), data.rateUnit); + m_currentDurationUnit = m_rDurationUnitCombo->findText(durationUnit) >= 0 ? durationUnit : + forecastUnitName(forecastUnitGroup(UNIT_TYPE_TIME), "hr"); + m_currentRateUnit = m_rRateUnitCombo->findText(rateUnit) >= 0 ? rateUnit : + forecastUnitName(forecastUnitGroup(UNIT_TYPE_FLOW_RATE), "B/D"); nmDataAttribute& pressure = m_forecastData.getMultipleRatesData().getInitialPressure(); if(m_manager && m_forecastData.getForecastOption() != nmDataForecast::MultipleRatesOption) { const QSharedPointer solved = m_manager->getPebiResultSnapshot(); if(!solved.isNull()) { - pressure.setValue(pressure.convertValueByUnitType( - solved->getReservoirParameters().m_dInitialPressure, "MPa", pressure.getUnit())); + pressure.setUnit("MPa"); + pressure.setValue(solved->getReservoirParameters().m_dInitialPressure); } } - // 本页使用绝对压力,避免通用单位控件把表压当作绝压换算。 - pressure.setListUnitSelections(QStringList() << "MPa" << "psia" << "kPa" << "bara"); m_forecastData.setForecastOption(nmDataForecast::MultipleRatesOption); m_multipleRatesRadio->blockSignals(true); m_multipleRatesRadio->setChecked(true); @@ -123,9 +181,7 @@ void nmWxForecast::saveDataToManager() // 生成前统一收集输入,编辑和关闭窗口时不提交到数据中心。 updateDataFromUI(); - const QList units = m_initialPressureComponent->findChildren(); - if(!units.isEmpty()) - m_forecastData.getMultipleRatesData().pressureUnit = units.first()->itemData(units.first()->currentIndex()).toString(); + m_forecastData.getMultipleRatesData().pressureUnit = m_initialPressureUnit->currentText(); // 保存到数据中心 nmDataAnalyzeManager* pManager = m_manager.data(); @@ -206,28 +262,24 @@ void nmWxForecast::setupUI() // (row 0) Initial pressure ConstantPressure& constantPressureData = m_forecastData.getConstantPressureData(); - m_initialPressureComponent = new nmGUIComponentLineEdit( - &constantPressureData.getInitialPressure(), - true, - this - ); - // 标签按内容占宽,数值和单位按储层参数页的比例铺满剩余空间。 - QLabel* pressureLabel = m_initialPressureComponent->findChild(); - // 先解除通用控件的 200 像素标签限制,再由布局按实际文字宽度分配。 - pressureLabel->setMinimumWidth(0); - pressureLabel->setMaximumWidth(QWIDGETSIZE_MAX); + // 保持原布局,用底层绑定器管理初始压力的数值和单位。 + QWidget* pressureWidget = new QWidget(this); + QHBoxLayout* pressureLayout = new QHBoxLayout(pressureWidget); + pressureLayout->setContentsMargins(0, 0, 0, 0); + pressureLayout->setSpacing(5); + QLabel* pressureLabel = new QLabel(nmTranslationManager::mapToChinese("Initial pressure"), pressureWidget); pressureLabel->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Fixed); - QLineEdit* pressureEdit = m_initialPressureComponent->findChild(); - QComboBox* pressureUnit = m_initialPressureComponent->findChild(); - pressureEdit->setMinimumWidth(140); - pressureEdit->setMaximumWidth(QWIDGETSIZE_MAX); - pressureUnit->setMinimumWidth(86); - pressureUnit->setMaximumWidth(QWIDGETSIZE_MAX); - QHBoxLayout* pressureLayout = static_cast(m_initialPressureComponent->layout()); - delete pressureLayout->takeAt(1); // 移除标签后的弹性留白。 - pressureLayout->setStretchFactor(pressureEdit, 8); - pressureLayout->setStretchFactor(pressureUnit, 5); - m_parametersLayout->addWidget(m_initialPressureComponent, row, 0, 1, 2); + m_initialPressureEdit = new QLineEdit(pressureWidget); + m_initialPressureUnit = new QComboBox(pressureWidget); + m_initialPressureEdit->setMinimumWidth(140); + m_initialPressureUnit->setMinimumWidth(86); + m_initialPressureUnit->addItem("MPa"); + m_pressureUnitBinder = new iUnitWxBinder(this); + m_pressureUnitBinder->registerUnitGroup(m_initialPressureEdit, m_initialPressureUnit); + pressureLayout->addWidget(pressureLabel); + pressureLayout->addWidget(m_initialPressureEdit, 8); + pressureLayout->addWidget(m_initialPressureUnit, 5); + m_parametersLayout->addWidget(pressureWidget, row, 0, 1, 2); row++; // (row 1) Abandonment rate @@ -282,6 +334,45 @@ void nmWxForecast::setupUI() m_extMultiR = buildExtMultiRatePage(); m_extMultiR->setMinimumHeight(80); + // 制度表沿用敏感性分析和编辑井的表头、行号、隔行底色及网格样式。 + QTableWidget* scheduleTables[] = {m_pressureTableWidget, m_rateTableWidget}; + for(int i = 0; i < 2; ++i) { + QTableWidget* table = scheduleTables[i]; + table->verticalHeader()->setVisible(true); + table->verticalHeader()->setMovable(false); + table->verticalHeader()->setDefaultAlignment(Qt::AlignCenter); + table->verticalHeader()->setMinimumWidth(18); + table->horizontalHeader()->setHighlightSections(false); + table->setAlternatingRowColors(true); + table->setShowGrid(true); + table->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);" + "}"); + // 第零行只显示单位,不参与制度行编号。 + table->setVerticalHeaderItem(0, new QTableWidgetItem("")); + } + // --- 2. 创建一个空页面--- m_extEmpty = buildExtEmptyPage(""); @@ -405,8 +496,8 @@ QWidget* nmWxForecast::buildExtMultiPressurePage() m_pressureTableWidget->setRowHeight(0, 24); // 设置一个合适的高度 PressureRow tempDefaults; // 用于获取单位列表 - QStringList durationUnits = tempDefaults.duration.getListUnitSelections(); - QStringList pressureUnits = tempDefaults.pressure.getListUnitSelections(); + QStringList durationUnits = forecastUnits(tempDefaults.duration); + QStringList pressureUnits = forecastUnits(tempDefaults.pressure); // --- 第0列:Duration 单位下拉框 --- m_pDurationUnitCombo = new QComboBox(m_pressureTableWidget); @@ -473,8 +564,8 @@ QWidget* nmWxForecast::buildExtMultiRatePage() m_rateTableWidget->setRowHeight(0, 24); FlowRow tempDefaults; // 使用 FlowRow 获取单位 - QStringList durationUnits = tempDefaults.duration.getListUnitSelections(); - QStringList rateUnits = tempDefaults.q0.getListUnitSelections(); // 获取速率单位 + QStringList durationUnits = forecastUnits(tempDefaults.duration); + QStringList rateUnits = forecastUnits(tempDefaults.q0); // 获取速率单位 // --- 第0列:Duration 单位下拉框 --- m_rDurationUnitCombo = new QComboBox(m_rateTableWidget); @@ -694,8 +785,7 @@ void nmWxForecast::updateParametersForOption() if(option == nmDataForecast::ConstantPressureOption) { ConstantPressure& cpData = m_forecastData.getConstantPressureData(); - if(m_initialPressureComponent) - m_initialPressureComponent->setOriginValue(&cpData.getInitialPressure()); + m_initialPressure = &cpData.getInitialPressure(); if(m_abandonmentRateComponent) m_abandonmentRateComponent->setOriginValue(&cpData.getAbandonmentRate()); if(m_maximumRateComponent) @@ -709,8 +799,7 @@ void nmWxForecast::updateParametersForOption() } else if(option == nmDataForecast::DecliningPressureOption) { DecliningPressure& dpData = m_forecastData.getDecliningPressureData(); - if(m_initialPressureComponent) - m_initialPressureComponent->setOriginValue(&dpData.getInitialPressure()); + m_initialPressure = &dpData.getInitialPressure(); if(m_abandonmentRateComponent) m_abandonmentRateComponent->setOriginValue(&dpData.getAbandonmentRate()); if(m_maximumRateComponent) @@ -725,8 +814,7 @@ void nmWxForecast::updateParametersForOption() } else if(option == nmDataForecast::MultiplePressuresOption) { MultiplePressures& mpData = m_forecastData.getMultiplePressuresData(); - if(m_initialPressureComponent) - m_initialPressureComponent->setOriginValue(&mpData.getInitialPressure()); + m_initialPressure = &mpData.getInitialPressure(); if(m_abandonmentRateComponent) m_abandonmentRateComponent->setOriginValue(&mpData.getAbandonmentRate()); if(m_maximumRateComponent) @@ -746,8 +834,7 @@ void nmWxForecast::updateParametersForOption() } else if(option == nmDataForecast::MultipleRatesOption) { MultipleRates& mrData = m_forecastData.getMultipleRatesData(); - if(m_initialPressureComponent) - m_initialPressureComponent->setOriginValue(&mrData.getInitialPressure()); + m_initialPressure = &mrData.getInitialPressure(); if (m_abandonmentRateCheck) m_abandonmentRateCheck->setVisible(false); if (m_abandonmentStack) m_abandonmentStack->setCurrentIndex(1); @@ -816,8 +903,14 @@ void nmWxForecast::updateUIFromData() } // 刷新所有组件的显示值 - if(m_initialPressureComponent) - m_initialPressureComponent->updateValueFromOrigin(); + // 基准值交给绑定器,显示单位恢复为上次保存的选项。 + QString pressureUnit = forecastUnitName(forecastUnitGroup(UNIT_TYPE_PRESSURE), + option == nmDataForecast::MultipleRatesOption ? m_forecastData.getMultipleRatesData().pressureUnit : m_initialPressure->getUnit()); + if(m_initialPressureUnit->findText(pressureUnit) < 0) + pressureUnit = forecastUnitName(forecastUnitGroup(UNIT_TYPE_PRESSURE), m_initialPressure->getUnit()); + m_pressureUnitBinder->setCurrentUnit(m_initialPressureEdit, pressureUnit); + m_pressureUnitBinder->setBaseValue(m_initialPressureEdit, convertForecastValue(*m_initialPressure, + m_initialPressure->getValue().toDouble(), m_initialPressure->getUnit(), "MPa")); if(m_abandonmentRateComponent) m_abandonmentRateComponent->updateValueFromOrigin(); @@ -860,6 +953,8 @@ void nmWxForecast::updateUIFromData() int displayRowCount = (dataSize == 0) ? 1 : dataSize; m_pressureTableWidget->setRowCount(1 + displayRowCount + 1); + // 末尾空行用于继续输入,填入内容后再显示编号。 + m_pressureTableWidget->setVerticalHeaderItem(displayRowCount + 1, new QTableWidgetItem("")); if(m_pDurationUnitCombo) m_pDurationUnitCombo->setCurrentIndex(m_pDurationUnitCombo->findText(m_currentDurationUnit)); @@ -870,15 +965,16 @@ void nmWxForecast::updateUIFromData() const PressureRow& rowData = (i < dataSize) ? tableData.at(i) : tempDefaults; int tableRow = i + 1; + m_pressureTableWidget->setVerticalHeaderItem(tableRow, new QTableWidgetItem(QString::number(tableRow))); // 转换值:从 "数据的基础单位" 转换为 "当前显示的单位" double baseDurValue = rowData.duration.getValue().toDouble(); QString baseDurUnit = rowData.duration.getUnit(); - double displayDurValue = tempDefaults.duration.convertValueByUnitType(baseDurValue, baseDurUnit, m_currentDurationUnit); + double displayDurValue = convertForecastValue(tempDefaults.duration, baseDurValue, baseDurUnit, m_currentDurationUnit); double basePresValue = rowData.pressure.getValue().toDouble(); QString basePresUnit = rowData.pressure.getUnit(); - double displayPresValue = tempDefaults.pressure.convertValueByUnitType(basePresValue, basePresUnit, m_currentPressureUnit); + double displayPresValue = convertForecastValue(tempDefaults.pressure, basePresValue, basePresUnit, m_currentPressureUnit); QTableWidgetItem* durationItem = new QTableWidgetItem(QString::number(displayDurValue, 'g', 15)); durationItem->setTextAlignment(Qt::AlignCenter); @@ -916,6 +1012,8 @@ void nmWxForecast::updateUIFromData() // 设置行数: 1(单位) + N(数据) + 1(空白) m_rateTableWidget->setRowCount(1 + displayRowCount + 1); + // 单位行和末尾输入空行不编号,数据行从一开始。 + m_rateTableWidget->setVerticalHeaderItem(displayRowCount + 1, new QTableWidgetItem("")); // 设置下拉框为当前单位 if(m_rDurationUnitCombo) m_rDurationUnitCombo->setCurrentIndex(m_rDurationUnitCombo->findText(m_currentDurationUnit)); @@ -926,23 +1024,28 @@ void nmWxForecast::updateUIFromData() const FlowRow& rowData = (i < dataSize) ? tableData.at(i) : tempDefaults; int tableRow = i + 1; + m_rateTableWidget->setVerticalHeaderItem(tableRow, new QTableWidgetItem(QString::number(tableRow))); // 转换值:从 "数据的基础单位" 转换为 "当前显示的单位" - double baseDurValue = rowData.duration.getValue().toDouble(); + bool durationOK = false; + double baseDurValue = rowData.duration.getValue().toString().toDouble(&durationOK); QString baseDurUnit = rowData.duration.getUnit(); - double displayDurValue = tempDefaults.duration.convertValueByUnitType(baseDurValue, baseDurUnit, m_currentDurationUnit); + double displayDurValue = convertForecastValue(tempDefaults.duration, baseDurValue, baseDurUnit, m_currentDurationUnit); - double baseRateValue = rowData.q0.getValue().toDouble(); + bool rateOK = false; + double baseRateValue = rowData.q0.getValue().toString().toDouble(&rateOK); QString baseRateUnit = rowData.q0.getUnit(); - double displayRateValue = tempDefaults.q0.convertValueByUnitType(baseRateValue, baseRateUnit, m_currentRateUnit); + double displayRateValue = convertForecastValue(tempDefaults.q0, baseRateValue, baseRateUnit, m_currentRateUnit); // 同单位直接恢复原输入文本,半空行不能补成默认数值。 - QTableWidgetItem* durationItem = new QTableWidgetItem(baseDurUnit == m_currentDurationUnit && rowData.duration.getValue().type() == QVariant::String + QTableWidgetItem* durationItem = new QTableWidgetItem(rowData.duration.getValue().type() == QVariant::String && + (baseDurUnit == m_currentDurationUnit || !durationOK) ? rowData.duration.getValue().toString() : QString::number(displayDurValue, 'g', 15)); durationItem->setTextAlignment(Qt::AlignCenter); m_rateTableWidget->setItem(tableRow, 0, durationItem); - QTableWidgetItem* rateItem = new QTableWidgetItem(baseRateUnit == m_currentRateUnit && rowData.q0.getValue().type() == QVariant::String + QTableWidgetItem* rateItem = new QTableWidgetItem(rowData.q0.getValue().type() == QVariant::String && + (baseRateUnit == m_currentRateUnit || !rateOK) ? rowData.q0.getValue().toString() : QString::number(displayRateValue, 'g', 15)); rateItem->setTextAlignment(Qt::AlignCenter); m_rateTableWidget->setItem(tableRow, 1, rateItem); @@ -959,8 +1062,9 @@ void nmWxForecast::updateUIFromData() void nmWxForecast::updateDataFromUI() { // 将界面数据写回到数据模型 - if(m_initialPressureComponent) - m_initialPressureComponent->updateValueToOrigin(); + // 存储绝对压力,界面单位单独保存,不改变求解器所需的 MPa 数值。 + m_initialPressure->setValue(m_pressureUnitBinder->getBaseValue(m_initialPressureEdit)); + m_initialPressure->setUnit("MPa"); if(m_abandonmentRateComponent) m_abandonmentRateComponent->updateValueToOrigin(); @@ -1010,13 +1114,13 @@ void nmWxForecast::updateDataFromUI() if(!durationEmpty) { double displayVal = durationItem->text().toDouble(); // 转换值:从 "当前显示的单位" 转换回 "数据的基础单位" - double storageVal = tempDefaults.duration.convertValueByUnitType(displayVal, m_currentDurationUnit, baseDurUnit); + double storageVal = convertForecastValue(tempDefaults.duration, displayVal, m_currentDurationUnit, baseDurUnit); newRow.duration.setValue(storageVal); } if(!pressureEmpty) { double displayVal = pressureItem->text().toDouble(); - double storageVal = tempDefaults.pressure.convertValueByUnitType(displayVal, m_currentPressureUnit, basePresUnit); + double storageVal = convertForecastValue(tempDefaults.pressure, displayVal, m_currentPressureUnit, basePresUnit); newRow.pressure.setValue(storageVal); } @@ -1085,12 +1189,14 @@ void nmWxForecast::onGenerateClicked() QMessageBox::warning(this, tr("Forecast"), error); return; } - nmDataAttribute& pressure = m_forecastData.getMultipleRatesData().getInitialPressure(); - if(!m_initialPressureComponent->updateValueToOrigin()) { + // 校验当前文本,计算取绑定器保存的精确基准值,避免显示舍入影响压力。 + bool pressureOK = false; + const double inputPressure = m_initialPressureEdit->text().toDouble(&pressureOK); + const double pi = m_pressureUnitBinder->getBaseValue(m_initialPressureEdit); + if(!pressureOK || !qIsFinite(inputPressure) || inputPressure <= 0.0 || !qIsFinite(pi) || pi <= 0.0) { QMessageBox::warning(this, tr("Forecast"), tr("Enter a positive initial pressure.")); return; } - const double pi = pressure.convertValueByUnitType(pressure.getValue().toDouble(), pressure.getUnit(), "MPa"); // 与主界面的结果井选择保持一致,预测制度只替换当前井,其他井共同参与求解。 const nmPebiResultWellSnapshot* well = solved->findWell(m_manager->getCurrentResultWellInstanceId()); if(!well) { @@ -1171,8 +1277,8 @@ bool nmWxForecast::readRateSchedule(QVector& durations, QVector& bool durationOk = false, rateOk = false; double duration = durationText.toDouble(&durationOk); double rate = rateText.toDouble(&rateOk); - duration = units.duration.convertValueByUnitType(duration, m_currentDurationUnit, "hr"); - rate = units.q0.convertValueByUnitType(rate, m_currentRateUnit, "m^3/D"); + duration = convertForecastValue(units.duration, duration, m_currentDurationUnit, "hr"); + rate = convertForecastValue(units.q0, rate, m_currentRateUnit, "m^3/D"); if(!durationOk || !rateOk || !qIsFinite(duration) || duration <= 0.0 || !qIsFinite(rate) || rate < 0.0) { error = tr("Row %1: enter a positive duration and a non-negative production rate (0 for shut-in).").arg(row); @@ -1265,6 +1371,9 @@ void nmWxForecast::onPressureCellChanged(int row, int column) int newRow = m_pressureTableWidget->rowCount(); m_pressureTableWidget->insertRow(newRow); + // 原空行成为制度行,新追加的输入行暂不编号。 + m_pressureTableWidget->setVerticalHeaderItem(row, new QTableWidgetItem(QString::number(row))); + m_pressureTableWidget->setVerticalHeaderItem(newRow, new QTableWidgetItem("")); // 给新行两列预置空 item,这样用户一输入就居中 m_pressureTableWidget->setItem(newRow, 0, makeCenteredEmptyItem()); @@ -1294,7 +1403,7 @@ void nmWxForecast::onPressureUnitChanged(const QString& unit) if(dataItem && !dataItem->text().isEmpty()) { double oldDisplayValue = dataItem->text().toDouble(); - double newDisplayValue = tempDefaults.pressure.convertValueByUnitType(oldDisplayValue, oldUnit, unit); + double newDisplayValue = convertForecastValue(tempDefaults.pressure, oldDisplayValue, oldUnit, unit); dataItem->setText(QString::number(newDisplayValue)); } } @@ -1322,6 +1431,9 @@ void nmWxForecast::onRateCellChanged(int row, int column) int newRow = m_rateTableWidget->rowCount(); m_rateTableWidget->insertRow(newRow); + // 原空行成为制度行,新追加的输入行暂不编号。 + m_rateTableWidget->setVerticalHeaderItem(row, new QTableWidgetItem(QString::number(row))); + m_rateTableWidget->setVerticalHeaderItem(newRow, new QTableWidgetItem("")); m_rateTableWidget->setItem(newRow, 0, makeCenteredEmptyItem()); m_rateTableWidget->setItem(newRow, 1, makeCenteredEmptyItem()); @@ -1351,7 +1463,7 @@ void nmWxForecast::onSharedDurationUnitChanged(const QString& unit) if(dataItem && !dataItem->text().isEmpty()) { double oldDisplayValue = dataItem->text().toDouble(); // 转换值 - double newDisplayValue = tempDefaults.duration.convertValueByUnitType(oldDisplayValue, oldUnit, unit); + double newDisplayValue = convertForecastValue(tempDefaults.duration, oldDisplayValue, oldUnit, unit); dataItem->setText(QString::number(newDisplayValue)); } } @@ -1373,7 +1485,7 @@ void nmWxForecast::onSharedDurationUnitChanged(const QString& unit) double oldDisplayValue = dataItem->text().toDouble(&ok); if(!ok || !qIsFinite(oldDisplayValue)) continue; // 转换值 - double newDisplayValue = tempDefaults.duration.convertValueByUnitType(oldDisplayValue, oldUnit, unit); + double newDisplayValue = convertForecastValue(tempDefaults.duration, oldDisplayValue, oldUnit, unit); dataItem->setText(QString::number(newDisplayValue, 'g', 15)); } } @@ -1414,7 +1526,7 @@ void nmWxForecast::onRateUnitChanged(const QString& unit) double oldDisplayValue = dataItem->text().toDouble(&ok); if(!ok || !qIsFinite(oldDisplayValue)) continue; // 使用 q0 (FlowRate) 进行单位转换 - double newDisplayValue = tempDefaults.q0.convertValueByUnitType(oldDisplayValue, oldUnit, unit); + double newDisplayValue = convertForecastValue(tempDefaults.q0, oldDisplayValue, oldUnit, unit); dataItem->setText(QString::number(newDisplayValue, 'g', 15)); } } From c891178676977ed85c76b4a0743bb1a5753c5216 Mon Sep 17 00:00:00 2001 From: lvjunjie Date: Fri, 30 Oct 2026 17:37:51 +0800 Subject: [PATCH 7/9] =?UTF-8?q?fix(nmNum):=20=E5=AE=8C=E5=96=84=E6=97=B6?= =?UTF-8?q?=E5=8F=98=E8=A1=A8=E7=9A=AE=E5=88=86=E6=AE=B5=E7=BC=96=E8=BE=91?= =?UTF-8?q?=E5=8F=8A=E9=A1=B5=E9=9D=A2=E5=88=87=E6=8D=A2=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nmNum/nmSubWxs/nmWxPerforationClosing.h | 6 +- .../nmNum/nmSubWxs/nmWxTimeDependentSkin.h | 9 +- Src/nmNum/nmSubWxs/nmWxPerforationClosing.cpp | 421 +++++++++++------- Src/nmNum/nmSubWxs/nmWxTimeDependentSkin.cpp | 274 ++++++------ 4 files changed, 387 insertions(+), 323 deletions(-) diff --git a/Include/nmNum/nmSubWxs/nmWxPerforationClosing.h b/Include/nmNum/nmSubWxs/nmWxPerforationClosing.h index 57f2607c..dffe5270 100644 --- a/Include/nmNum/nmSubWxs/nmWxPerforationClosing.h +++ b/Include/nmNum/nmSubWxs/nmWxPerforationClosing.h @@ -9,6 +9,7 @@ #include #include #include +#include #include "nmDataPerforation.h" #include "nmDataWellBase.h" @@ -30,13 +31,14 @@ public: void onOkClicked(); void onCancelClicked(); void onItemChanged(QTreeWidgetItem* item, int column); + void onCurrentItemChanged(QTreeWidgetItem* current, QTreeWidgetItem* previous); private: void initUI(); - QString formatNumber(double value); void refreshDisplay(); void processDateEdit(QTreeWidgetItem* item); void processValueEdit(QTreeWidgetItem* item); + void storeCurrentPerforation(); void saveDataToModel(); // 保存界面数据到模型 void loadDataFromModel(); // 从模型加载数据到界面 @@ -72,7 +74,7 @@ private: bool m_showDatesMode; bool m_hasBaseTime; - nmDataPerforation m_perforationData; + QMap m_perforations; // 各射孔的编辑副本,确认前不写入井对象 nmDataWellBase* m_currentWell; int m_currentPerforationIndex; }; diff --git a/Include/nmNum/nmSubWxs/nmWxTimeDependentSkin.h b/Include/nmNum/nmSubWxs/nmWxTimeDependentSkin.h index d5fa3a17..fe7ca7de 100644 --- a/Include/nmNum/nmSubWxs/nmWxTimeDependentSkin.h +++ b/Include/nmNum/nmSubWxs/nmWxTimeDependentSkin.h @@ -46,6 +46,7 @@ public: void onComputeClicked(); void onSkinVsRateClicked(); void onPerforationClosingBtnClicked(); + void onPerforationClosingAccepted(); // 表格数据变化事件 void onTableDataChanged(int row, int column); void onTableRowClicked(int row, int column); @@ -146,10 +147,6 @@ private: static double fromSeconds(double sec, const QString& unit); bool moveFlowSegmentBoundarySec(int segmentIndex, double newBoundarySec); - // SkinVsRate模式的状态保存和恢复 - void saveStateBeforeSkinVsRate(); - void restoreStateAfterSkinVsRate(); - // 添加辅助方法 QString formatTimeDisplay(double timeInHours, int segmentIndex) const; void updateTimeUnitComboVisibility(); @@ -227,10 +224,6 @@ private: bool m_hasPerformedInitialFit; bool m_rateDependentState; - // 保存进入SkinVsRate模式前的完整数据状态 - nmDataPerforation m_savedFlowSegmentData; - bool m_savedRateDependentState; - // 静态成员变量 static nmWxTimeDependentSkin* s_instance; }; diff --git a/Src/nmNum/nmSubWxs/nmWxPerforationClosing.cpp b/Src/nmNum/nmSubWxs/nmWxPerforationClosing.cpp index ef000e0e..97c78797 100644 --- a/Src/nmNum/nmSubWxs/nmWxPerforationClosing.cpp +++ b/Src/nmNum/nmSubWxs/nmWxPerforationClosing.cpp @@ -1,7 +1,8 @@ #include "nmWxPerforationClosing.h" #include "iUnitGroup.h" #include "iUnitHelper.h" -#include "ZxDataEditorDelegate.h" +#include +#include #include "iBase_global.h" #include @@ -19,6 +20,91 @@ #include "nmDataAnalyzeManager.h" +namespace +{ +// 树形分组保留原生折叠行为,只为组内两列表格补画网格和当前行标记。 +class PerforationClosingTree : public QTreeWidget +{ +public: + explicit PerforationClosingTree(QWidget* parent) : QTreeWidget(parent) {} + +protected: + void drawRow(QPainter* painter, const QStyleOptionViewItem& option, + const QModelIndex& index) const override + { + QTreeWidget::drawRow(painter, option, index); + painter->save(); + painter->setPen(palette().color(QPalette::Mid)); + // 分组行跨列;子行从缩进后的表格左边缘开始绘制,包含箭头栏。 + const int left = index.parent().isValid() + ? visualRect(index).left() - indentation() : option.rect.left(); + painter->drawLine(left, option.rect.bottom(), option.rect.right(), option.rect.bottom()); + if(index.parent().isValid()) { + if(index.row() == 0) + painter->drawLine(left, option.rect.top(), option.rect.right(), option.rect.top()); + painter->drawLine(left, option.rect.top(), left, option.rect.bottom()); + const int markerRight = visualRect(index).left() - 1; + painter->drawLine(markerRight, option.rect.top(), markerRight, option.rect.bottom()); + const int divider = columnViewportPosition(1) - 1; + painter->drawLine(divider, option.rect.top(), divider, option.rect.bottom()); + } + painter->restore(); + } + + void drawBranches(QPainter* painter, const QRect& rect, + const QModelIndex& index) const override + { + // 射孔分组使用系统展开箭头,数据行的缩进区域显示当前行三角标记。 + if(!index.parent().isValid()) { + QTreeWidget::drawBranches(painter, rect, index); + return; + } + if(index.row() == currentIndex().row() && index.parent() == currentIndex().parent() + && (index.flags() & Qt::ItemIsEditable)) { + painter->save(); + const QPoint center(rect.right() - indentation() / 2, rect.center().y()); + QPolygon arrow; + arrow << center + QPoint(-2, -4) << center + QPoint(3, 0) << center + QPoint(-2, 4); + painter->setPen(Qt::NoPen); + painter->setBrush(palette().color(QPalette::Text)); + painter->drawPolygon(arrow); + painter->restore(); + } + } +}; + +class PerforationClosingDelegate : public QStyledItemDelegate +{ +public: + explicit PerforationClosingDelegate(QObject* parent) : QStyledItemDelegate(parent) {} + + QWidget* createEditor(QWidget* parent, const QStyleOptionViewItem& option, + const QModelIndex& index) const override + { + // 射孔状态由时间顺序决定,仅开始时间允许编辑。 + if(index.column() != 0) return nullptr; + QWidget* editor = QStyledItemDelegate::createEditor(parent, option, index); + if(QLineEdit* line = qobject_cast(editor)) { + line->setAlignment(Qt::Alignment(index.data(Qt::TextAlignmentRole).toInt())); + } + return editor; + } + +protected: + void initStyleOption(QStyleOptionViewItem* option, const QModelIndex& index) const override + { + QStyledItemDelegate::initStyleOption(option, index); + // 三位小数仅用于绘制,编辑与保存仍读取模型中的完整数值。 + if(index.column() == 0 && index.parent().isValid() && (index.flags() & Qt::ItemIsEditable)) { + bool ok = false; + const double value = index.data(Qt::EditRole).toString().toDouble(&ok); + QStyleOptionViewItemV4* textOption = qstyleoption_cast(option); + if(ok && textOption) textOption->text = QString::number(value, 'f', 3); + } + } +}; +} + nmWxPerforationClosing::nmWxPerforationClosing(nmDataWellBase* pWellData, QWidget *parent) : iDlgBase(parent) { @@ -36,19 +122,15 @@ nmWxPerforationClosing::nmWxPerforationClosing(nmDataWellBase* pWellData, QWidge m_pCurrentPerforationItem = 0; m_currentWell = pWellData; - - if(m_currentWell != nullptr) { - int perforationCount = m_currentWell->getPerforationCount(); - // 选择操作的射孔段索引 - m_currentPerforationIndex = perforationCount - 1; - m_perforationData = m_currentWell->getPerforationCopy(m_currentPerforationIndex); - - // 从副本中读取封堵数据 - m_baseTimeValues = m_perforationData.getTimeValues(); - m_baseTime = m_perforationData.getBaseTime(); - m_hasBaseTime = m_baseTime.isValid(); - } - + m_currentPerforationIndex = -1; + // 按射孔保存独立副本,切换目标不会覆盖其他射孔的封堵记录。 + if(m_currentWell) { + for(int i = 0; i < m_currentWell->getPerforationCount(); ++i) { + m_perforations.insert(i, m_currentWell->getPerforationCopy(i)); + } + m_currentPerforationIndex = m_currentWell->getPerforationCount() - 1; + } + loadDataFromModel(); // 初始化UI initUI(); @@ -61,7 +143,11 @@ nmWxPerforationClosing::nmWxPerforationClosing(nmDataWellBase* pWellData, QWidge connect(m_pOkBtn, SIGNAL(clicked()), this, SLOT(onOkClicked())); connect(m_pCancelBtn, SIGNAL(clicked()), this, SLOT(onCancelClicked())); connect(m_pTreeWidget, SIGNAL(itemChanged(QTreeWidgetItem*, int)), this, SLOT(onItemChanged(QTreeWidgetItem*, int))); - loadDataFromModel(); + connect(m_pTreeWidget, SIGNAL(currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)), + this, SLOT(onCurrentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*))); + refreshDisplay(); + m_pAddBtn->setEnabled(m_pCurrentPerforationItem != nullptr); + m_pRemoveBtn->setEnabled(m_pCurrentPerforationItem != nullptr); } nmWxPerforationClosing::~nmWxPerforationClosing() @@ -127,10 +213,14 @@ void nmWxPerforationClosing::initUI() toolbarLayout->addSpacing(15); // 复选框和时间单位之间的固定间距 - m_pTimeUnitLabel = new QLabel(tr("Time unit:")); // 保存指针以便隐藏/显示 - toolbarLayout->addWidget(m_pTimeUnitLabel); - - toolbarLayout->addSpacing(5); // 标签和下拉框之间的小间距 + // 单位控件单独保留占位,切换日期显示时前面的控件不移动。 + QWidget* timeUnitContainer = new QWidget(toolbarContainer); + QHBoxLayout* timeUnitLayout = new QHBoxLayout(timeUnitContainer); + timeUnitLayout->setContentsMargins(0, 0, 0, 0); + timeUnitLayout->setSpacing(toolbarLayout->spacing()); + m_pTimeUnitLabel = new QLabel(tr("Time unit:")); + timeUnitLayout->addWidget(m_pTimeUnitLabel); + timeUnitLayout->addSpacing(5); // 标签和下拉框之间的小间距 // 单位和主窗口统一从底层配置读取。 m_pTimeUnitComboBox = new QComboBox(); @@ -142,7 +232,9 @@ void nmWxPerforationClosing::initUI() m_pTimeUnitComboBox->setCurrentIndex(unitIndex >= 0 ? unitIndex : 0); m_currentTimeUnit = m_pTimeUnitComboBox->currentText(); m_pTimeUnitComboBox->setFixedWidth(60); - toolbarLayout->addWidget(m_pTimeUnitComboBox); + timeUnitLayout->addWidget(m_pTimeUnitComboBox); + timeUnitContainer->setFixedSize(timeUnitContainer->sizeHint()); + toolbarLayout->addWidget(timeUnitContainer); // 避免控件贴边 toolbarLayout->addSpacing(10); @@ -151,35 +243,54 @@ void nmWxPerforationClosing::initUI() mainLayout->addWidget(toolbarContainer); // 创建单个树形表格控件 - m_pTreeWidget = new QTreeWidget(this); - m_pTreeWidget->setColumnCount(3); - // 时间列使用框架编辑代理,保留树节点及日期模式的只读规则。 - m_pTreeWidget->setItemDelegateForColumn(1, new ZxDataEditorDelegate(m_pTreeWidget)); + m_pTreeWidget = new PerforationClosingTree(this); + // 数值时间和日期共用第一列,第二列显示射孔状态。 + m_pTreeWidget->setColumnCount(2); + m_pTreeWidget->setItemDelegate(new PerforationClosingDelegate(m_pTreeWidget)); // 设置表头 QStringList headers; - headers << "" << "" << ""; + headers << "" << ""; m_pTreeWidget->setHeaderLabels(headers); - m_pTreeWidget->setHeaderHidden(false); // 显示表头 + m_pTreeWidget->setHeaderHidden(true); // 列标题显示在各射孔分组内部 // 设置列宽 - m_pTreeWidget->setColumnWidth(0, 200); // 树形结构列 - m_pTreeWidget->setColumnWidth(1, 120); // 时间列 - m_pTreeWidget->setColumnWidth(2, 120); // 状态列 + m_pTreeWidget->setColumnWidth(0, 260); // 分组和开始时间,预留日期文字及树缩进宽度 + m_pTreeWidget->setColumnWidth(1, 180); // 射孔状态 // 设置表头拉伸策略 m_pTreeWidget->header()->setStretchLastSection(true); + m_pTreeWidget->header()->setHighlightSections(false); + m_pTreeWidget->header()->setDefaultAlignment(Qt::AlignCenter); // 设置树形控件的其他属性 m_pTreeWidget->setRootIsDecorated(true); // 显示根节点的展开/折叠图标 m_pTreeWidget->setAlternatingRowColors(true); // 交替行颜色 m_pTreeWidget->setSelectionBehavior(QAbstractItemView::SelectRows); // 整行选择 + m_pTreeWidget->setSelectionMode(QAbstractItemView::SingleSelection); m_pTreeWidget->setIndentation(20); // 设置缩进距离 + // 选中行保持浅蓝底色;网格由树控件统一绘制,不依赖单元格代理。 + m_pTreeWidget->setEditTriggers(QAbstractItemView::DoubleClicked); m_pTreeWidget->setStyleSheet( - "QTreeWidget::branch {" - "background: transparent;" - "border: none;" - "width: 0px;" + "QTreeWidget {" + " background-color: palette(base);" + " alternate-background-color: rgb(232, 232, 232);" + " 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);" + "}" + "QTreeWidget::item:selected {" + " background-color: rgb(225, 235, 252);" + " color: palette(text);" "}" ); @@ -192,6 +303,7 @@ void nmWxPerforationClosing::initUI() // 添加表头行 m_pPerforationRoot = new QTreeWidgetItem(m_pTreeWidget); m_pPerforationRoot->setText(0, tr("Perforation")); + m_pPerforationRoot->setFirstColumnSpanned(true); m_pPerforationRoot->setChildIndicatorPolicy(QTreeWidgetItem::DontShowIndicator); // 为每个射孔段添加树形项目 @@ -205,6 +317,8 @@ void nmWxPerforationClosing::initUI() QTreeWidgetItem* perforationItem = new QTreeWidgetItem(m_pTreeWidget); perforationItem->setText(0, perforationName); + perforationItem->setData(0, Qt::UserRole, i); + perforationItem->setFirstColumnSpanned(true); perforationItem->setExpanded(false); // 默认收起 // 如果是当前选中的射孔段,保存引用 @@ -249,7 +363,7 @@ void nmWxPerforationClosing::initUI() } } - m_pAllComboBox->setFixedWidth(120); // 增加宽度以适应射孔段名称 + m_pAllComboBox->setFixedWidth(180); // 加宽下拉框,便于查看射孔段名称 //m_pAllComboBox = new QComboBox(); //m_pAllComboBox->addItem(tr("All")); //m_pAllComboBox->addItem(tr("Option 1")); @@ -277,16 +391,6 @@ void nmWxPerforationClosing::initUI() mainLayout->setSpacing(5); } -QString nmWxPerforationClosing::formatNumber(double value) -{ - // 如果数值很大或很小,使用科学计数法 - if(qAbs(value) >= 1000000.0 || (qAbs(value) < 0.001 && value != 0.0)) { - return QString::number(value, 'e', 5); // 科学计数法,5位小数 - } else { - return QString::number(value, 'f', 5); // 固定小数点格式,5位小数 - } -} - void nmWxPerforationClosing::onAddClicked() { if(!m_pCurrentPerforationItem) @@ -298,29 +402,27 @@ void nmWxPerforationClosing::onAddClicked() m_hasBaseTime = true; } - // 直接基于 m_baseTimeValues 来决定要添加的值 + // 子行的第一行是表头,实际时间记录从第二行开始。 + QTreeWidgetItem* selected = m_pTreeWidget->currentItem(); + int selectedIndex = selected && selected->parent() == m_pCurrentPerforationItem + ? m_pCurrentPerforationItem->indexOfChild(selected) - 1 : -1; QList newValuesInHr; if(m_baseTimeValues.isEmpty()) { // 第一次Add:添加100.0小时 newValuesInHr.append(100.0); - } else if(m_baseTimeValues.size() == 1) { - // 第二次Add:添加200.0小时 - newValuesInHr.append(200.0); + } else if(m_baseTimeValues.size() == 1 || selectedIndex == m_baseTimeValues.size() - 1) { + // 最后一行没有后续区间,沿用初始的100小时步长向后追加一条。 + newValuesInHr.append(m_baseTimeValues.last() + 100.0); } else { - // 第三次及以后:只在第一个区间中插入两个点 - QList sortedValues = m_baseTimeValues; - std::sort(sortedValues.begin(), sortedValues.end()); - - // 只在第一个区间插入两个均匀分布的点 - if(sortedValues.size() >= 2) { - double start = sortedValues[0]; - double end = sortedValues[1]; - double interval = (end - start) / 3.0; - - newValuesInHr.append(start + interval); - newValuesInHr.append(start + 2 * interval); - } + // 保留三等分插入两个点的规则,但使用所选行与下一行之间的区间。 + // 只选中射孔名称或表头时,沿用原来的第一个区间。 + if(selectedIndex < 0) selectedIndex = 0; + const double start = m_baseTimeValues[selectedIndex]; + const double end = m_baseTimeValues[selectedIndex + 1]; + const double interval = (end - start) / 3.0; + newValuesInHr.append(start + interval); + newValuesInHr.append(start + 2 * interval); } // 合并原有数值和新数值,然后排序 @@ -330,7 +432,7 @@ void nmWxPerforationClosing::onAddClicked() // 刷新显示 refreshDisplay(); - // 展开Perforation #1项目以显示新添加的内容 + // 展开当前选中的射孔分组以显示新添加的内容 m_pCurrentPerforationItem->setExpanded(true); // 更新计数器 @@ -357,8 +459,8 @@ void nmWxPerforationClosing::onRemoveClicked() isValidDataRow = !selectedItem->text(0).isEmpty() && QDateTime::fromString(selectedItem->text(0), "yyyy-MM-dd hh:mm:ss").isValid(); } else { - // 数值模式下检查第1列是否有数值数据 - isValidDataRow = !selectedItem->text(1).isEmpty(); + // 数值时间也位于第一列。 + isValidDataRow = !selectedItem->text(0).isEmpty(); } if(isValidDataRow) { @@ -410,77 +512,69 @@ void nmWxPerforationClosing::onRemoveClicked() void nmWxPerforationClosing::refreshDisplay() { - if(!m_pCurrentPerforationItem) return; - - m_pTreeWidget->blockSignals(true); - - // 清理除“Start time”外的子行 - for(int i = m_pCurrentPerforationItem->childCount() - 1; i >= 0; --i) { - QTreeWidgetItem* it = m_pCurrentPerforationItem->child(i); - if(it == nullptr) { - continue; + // 显示单位和日期模式属于整个窗口,刷新时同时更新所有射孔分组。 + storeCurrentPerforation(); + const bool blocked = m_pTreeWidget->blockSignals(true); + QTreeWidgetItem* selected = m_pTreeWidget->currentItem(); + QTreeWidgetItem* selectedGroup = selected && selected->parent() ? selected->parent() : selected; + const int selectedRow = selected && selected->parent() ? selectedGroup->indexOfChild(selected) : -1; + + for(int groupIndex = 0; groupIndex < m_pTreeWidget->topLevelItemCount(); ++groupIndex) { + QTreeWidgetItem* group = m_pTreeWidget->topLevelItem(groupIndex); + if(group == m_pPerforationRoot) continue; + const nmDataPerforation& data = m_perforations[group->data(0, Qt::UserRole).toInt()]; + const QList values = data.getTimeValues(); + const QDateTime baseTime = data.getBaseTime(); + const bool wasExpanded = group->isExpanded(); + const bool hadRows = group->childCount() > 0; + qDeleteAll(group->takeChildren()); + if(!values.isEmpty()) { + QTreeWidgetItem* heading = new QTreeWidgetItem(group); + heading->setText(0, tr("Start time")); + heading->setText(1, tr("Perforation state")); + heading->setChildIndicatorPolicy(QTreeWidgetItem::DontShowIndicator); } - - if(it->text(0) != tr("Start time")) { - m_pCurrentPerforationItem->removeChild(it); - delete it; - } - } - - // 如有数据,确保有“Start time”行 - if(!m_baseTimeValues.isEmpty()) { - bool hasStart = false; - - for(int i = 0; i < m_pCurrentPerforationItem->childCount(); ++i) { - QTreeWidgetItem* child = m_pCurrentPerforationItem->child(i); - if(child != nullptr && child->text(0) == tr("Start time")) { - hasStart = true; break; + for(int i = 0; i < values.size(); ++i) { + QTreeWidgetItem* row = new QTreeWidgetItem(group); + const double seconds = toSecondsQuantized(values[i], "hr"); + if(m_showDatesMode && baseTime.isValid()) { + row->setText(0, baseTime.addSecs(static_cast(seconds)).toString("yyyy-MM-dd hh:mm:ss")); + row->setTextAlignment(0, Qt::AlignLeft | Qt::AlignVCenter); + } else { + row->setText(0, QString::number(fromSeconds(seconds, m_currentTimeUnit), 'g', 15)); + row->setTextAlignment(0, Qt::AlignRight | Qt::AlignVCenter); } - } - - if(!hasStart) { - QTreeWidgetItem* startTimeItem = new QTreeWidgetItem(m_pCurrentPerforationItem); - startTimeItem->setText(0, tr("Start time")); - startTimeItem->setText(2, tr("Perforation state")); - startTimeItem->setChildIndicatorPolicy(QTreeWidgetItem::DontShowIndicator); - } - } - - for(int i = 0; i < m_baseTimeValues.size(); ++i) { - QTreeWidgetItem* row = new QTreeWidgetItem(m_pCurrentPerforationItem); - - // 先把 “小时” -> “秒” - const double secQuant = toSecondsQuantized(m_baseTimeValues[i], "hr"); - - if(m_showDatesMode && m_hasBaseTime) { - // 用整数秒 + 基准时间显示; - const auto dt = m_baseTime.addSecs(static_cast(secQuant)); - row->setText(0, dt.toString("yyyy-MM-dd hh:mm:ss")); - row->setText(1, ""); - row->setTextAlignment(0, Qt::AlignLeft | Qt::AlignVCenter); - row->setFlags(row->flags() | Qt::ItemIsEditable); - } else { - // 数值模式:整数秒 -> 当前显示单位 - const double dispVal = fromSeconds(secQuant, m_currentTimeUnit); - row->setText(0, ""); - row->setText(1, formatNumber(dispVal)); - row->setTextAlignment(1, Qt::AlignRight | Qt::AlignVCenter); row->setFlags(row->flags() | Qt::ItemIsEditable); + row->setText(1, i % 2 == 0 ? tr("Closed") : tr("Open")); + row->setTextAlignment(1, Qt::AlignLeft | Qt::AlignVCenter); + row->setChildIndicatorPolicy(QTreeWidgetItem::DontShowIndicator); } - - const QString state = (i % 2 == 0) ? tr("Closed") : tr("Open"); - row->setText(2, state); - row->setTextAlignment(2, Qt::AlignCenter | Qt::AlignVCenter); - row->setChildIndicatorPolicy(QTreeWidgetItem::DontShowIndicator); + group->setExpanded(wasExpanded || (!hadRows && !values.isEmpty())); } - - m_pTreeWidget->blockSignals(false); - - if(!m_baseTimeValues.isEmpty()) { - m_pCurrentPerforationItem->setExpanded(true); + // 重建子行后按分组及行号恢复选择,不能继续使用已释放的行指针。 + if(selectedGroup) { + m_pTreeWidget->setCurrentItem(selectedRow >= 0 && selectedGroup->childCount() > 0 + ? selectedGroup->child(qMin(selectedRow, selectedGroup->childCount() - 1)) : selectedGroup); + } else if(m_pCurrentPerforationItem) { + m_pTreeWidget->setCurrentItem(m_pCurrentPerforationItem); } + m_pTreeWidget->blockSignals(blocked); } +void nmWxPerforationClosing::onCurrentItemChanged(QTreeWidgetItem* current, QTreeWidgetItem* previous) +{ + Q_UNUSED(previous); + // 点击射孔名称、组内表头或数据行,都以其所属射孔作为增删和编辑目标。 + QTreeWidgetItem* group = current && current->parent() ? current->parent() : current; + if(group == m_pPerforationRoot) group = nullptr; + if(group == m_pCurrentPerforationItem) return; + storeCurrentPerforation(); + m_pCurrentPerforationItem = group; + m_currentPerforationIndex = group ? group->data(0, Qt::UserRole).toInt() : -1; + loadDataFromModel(); + m_pAddBtn->setEnabled(group != nullptr); + m_pRemoveBtn->setEnabled(group != nullptr); +} void nmWxPerforationClosing::onTimeUnitChanged(const QString &unit) { // 更新当前时间单位 @@ -550,6 +644,7 @@ void nmWxPerforationClosing::showSpecificPerforationItem(int perforationIndex) if(item) { item->setHidden(false); item->setExpanded(true); // 展开显示内容 + m_pTreeWidget->setCurrentItem(item); // 筛选射孔时同步切换操作目标 } } } @@ -600,12 +695,12 @@ void nmWxPerforationClosing::onItemChanged(QTreeWidgetItem* item, int column) // 确保是子项 if(item->parent() != m_pCurrentPerforationItem) return; - // 根据显示模式处理不同列的编辑 + // 两种模式都编辑开始时间列,按显示模式选择解析方式。 if(m_showDatesMode && column == 0) { // 日期模式下编辑第0列(时间字符串) processDateEdit(item); - } else if(!m_showDatesMode && column == 1) { - // 数值模式下编辑第1列(数值) + } else if(!m_showDatesMode && column == 0) { + // 数值模式下编辑第0列(数值) processValueEdit(item); } } @@ -689,7 +784,8 @@ void nmWxPerforationClosing::processValueEdit(QTreeWidgetItem* item) guard = true; bool ok = false; - const double newDisp = item->text(1).toDouble(&ok); + // 数值时间与日期统一从第一列读取。 + const double newDisp = item->text(0).toDouble(&ok); if(!ok) { guard = false; @@ -752,52 +848,43 @@ void nmWxPerforationClosing::processValueEdit(QTreeWidgetItem* item) guard = false; } -void nmWxPerforationClosing::saveDataToModel() +void nmWxPerforationClosing::storeCurrentPerforation() { - // 将界面数据保存到副本中 - m_perforationData.setTimeValues(m_baseTimeValues); - - // 生成状态数组 + // 切换分组和刷新前只更新本地副本,取消窗口时可丢弃全部修改。 + if(!m_perforations.contains(m_currentPerforationIndex)) return; + nmDataPerforation& data = m_perforations[m_currentPerforationIndex]; + data.setTimeValues(m_baseTimeValues); QStringList states; + for(int i = 0; i < m_baseTimeValues.size(); ++i) + states << (i % 2 == 0 ? tr("Closed") : tr("Open")); + data.setTimeStates(states); + data.setBaseTime(m_hasBaseTime ? m_baseTime : QDateTime()); +} - for(int i = 0; i < m_baseTimeValues.size(); ++i) { - QString state = (i % 2 == 0) ? tr("Closed") : tr("Open"); - states.append(state); - } - - m_perforationData.setTimeStates(states); - - // 保存基准时间(如果有的话) - if(m_hasBaseTime && m_baseTime.isValid()) { - m_perforationData.setBaseTime(m_baseTime); +void nmWxPerforationClosing::saveDataToModel() +{ + // 统一提交各射孔的封堵字段,保留井对象中其他参数的最新值。 + storeCurrentPerforation(); + if(!m_currentWell) return; + for(QMap::const_iterator it = m_perforations.constBegin(); + it != m_perforations.constEnd(); ++it) { + nmDataPerforation data = m_currentWell->getPerforationCopy(it.key()); + data.setTimeValues(it.value().getTimeValues()); + data.setTimeStates(it.value().getTimeStates()); + data.setBaseTime(it.value().getBaseTime()); + m_currentWell->updatePerforation(it.key(), data); } - - m_currentWell->updatePerforation(m_currentPerforationIndex, m_perforationData); - } void nmWxPerforationClosing::loadDataFromModel() { - // 直接获取时间值数据 - m_baseTimeValues = m_perforationData.getTimeValues(); - - // 恢复基准时间 - QDateTime savedBaseTime = m_perforationData.getBaseTime(); - - if(savedBaseTime.isValid()) { - m_baseTime = savedBaseTime; - m_hasBaseTime = true; - } else { - m_hasBaseTime = false; - } - - // 更新计数器 + // 切换编辑缓冲区时不重建树,避免删除正在点击或编辑的子行。 + const nmDataPerforation data = m_perforations.value(m_currentPerforationIndex); + m_baseTimeValues = data.getTimeValues(); + m_baseTime = data.getBaseTime(); + m_hasBaseTime = m_baseTime.isValid(); m_timeEntryCounter = m_baseTimeValues.size(); - - // 刷新界面显示 - refreshDisplay(); } - double nmWxPerforationClosing::convertTime(double value, const QString& fromUnit, const QString& toUnit) const { // 直接使用底层单位组,兼容内部 hr/sec 与配置中的 h/s 写法。 diff --git a/Src/nmNum/nmSubWxs/nmWxTimeDependentSkin.cpp b/Src/nmNum/nmSubWxs/nmWxTimeDependentSkin.cpp index 06bd79df..ab867e27 100644 --- a/Src/nmNum/nmSubWxs/nmWxTimeDependentSkin.cpp +++ b/Src/nmNum/nmSubWxs/nmWxTimeDependentSkin.cpp @@ -373,7 +373,7 @@ void nmWxTimeDependentSkin::initUI() // 创建图表下方的下拉框 // 使用普通下拉框,按现有固定宽度显示完整的射孔名称。 m_pComboBox = new QComboBox(); - m_pComboBox->setFixedWidth(200); // 增加宽度以容纳射孔段名称 + m_pComboBox->setFixedWidth(240); // 加宽下拉框,便于查看射孔段名称 // 将图表和下拉框添加到左侧布局 leftLayout->addWidget(chartWidget, 1); // 图表占据大部分空间 @@ -397,11 +397,43 @@ void nmWxTimeDependentSkin::initUI() headers << tr("") << tr("Start time") << tr("Skin"); m_pDataTable->setHorizontalHeaderLabels(headers); - m_pDataTable->verticalHeader()->setVisible(false); + // 沿用敏感性分析的行号、隔行底色和表头样式,单位行不计入分段编号。 + m_pDataTable->verticalHeader()->setVisible(true); + m_pDataTable->verticalHeader()->setMovable(false); + m_pDataTable->verticalHeader()->setDefaultAlignment(Qt::AlignCenter); + m_pDataTable->verticalHeader()->setMinimumWidth(18); + m_pDataTable->setAlternatingRowColors(true); + m_pDataTable->setShowGrid(true); + m_pDataTable->setEditTriggers(QAbstractItemView::DoubleClicked); + m_pDataTable->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);" + "}"); m_pDataTable->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); m_pDataTable->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); QHeaderView* header = m_pDataTable->horizontalHeader(); + header->setHighlightSections(false); header->setResizeMode(0, QHeaderView::Fixed); // 第一列(箭头列)固定宽度 header->setResizeMode(1, QHeaderView::Stretch); // 第二列(时间列)拉伸 header->setResizeMode(2, QHeaderView::Stretch); // 第三列(Skin列)拉伸 @@ -572,10 +604,13 @@ void nmWxTimeDependentSkin::updateTableFromFlowSegments() // 设置总行数 m_pDataTable->setRowCount(segments.size() + 1); + QStringList rowLabels; + rowLabels << ""; // 填充数据行(从第1行开始,第0行是单位行) for(int i = 0; i < segments.size(); ++i) { const int row = i + 1; + rowLabels << QString::number(row); const FlowSegmentData& segment = segments[i]; // 更新选择箭头 @@ -644,6 +679,12 @@ void nmWxTimeDependentSkin::updateTableFromFlowSegments() } } + // 第二页隐藏流量相关列;保留数据,返回第一页时恢复显示。 + if(enableRateDep) m_pDataTable->setColumnHidden(3, m_pChartWidget != nullptr); + + // 表格重建或增删分段后同步行号,第零行仅显示单位。 + m_pDataTable->setVerticalHeaderLabels(rowLabels); + // 恢复信号状态 if(m_pTimeUnitCombo) m_pTimeUnitCombo->blockSignals(oldTimeBlocked); @@ -651,8 +692,8 @@ void nmWxTimeDependentSkin::updateTableFromFlowSegments() m_pDataTable->blockSignals(oldTblBlocked); - // 更新按钮状态 - m_pSkinVsRateBtn->setEnabled(segments.size() >= 2); + // 仅单射孔且至少有两个表皮分段时,允许进入表皮系数与流量页面。 + m_pSkinVsRateBtn->setEnabled(m_currentWell && m_currentWell->getPerforationCount() == 1 && segments.size() >= 2); } void nmWxTimeDependentSkin::ensureHeaderRowExists() @@ -864,9 +905,21 @@ void nmWxTimeDependentSkin::onComputeClicked() void nmWxTimeDependentSkin::onPerforationClosingBtnClicked() { nmWxPerforationClosing* dlg = new nmWxPerforationClosing(m_currentWell, this); + // 子窗口确认后同步封堵字段,防止主窗口随后提交旧副本覆盖这些修改。 + connect(dlg, SIGNAL(accepted()), this, SLOT(onPerforationClosingAccepted())); dlg->show(); } +void nmWxTimeDependentSkin::onPerforationClosingAccepted() +{ + if(!m_currentWell) return; + const nmDataPerforation data = m_currentWell->getPerforationCopy(m_currentPerforationIndex); + // 仅同步封堵数据,保留当前表皮编辑值。 + m_perforationData.setTimeValues(data.getTimeValues()); + m_perforationData.setTimeStates(data.getTimeStates()); + m_perforationData.setBaseTime(data.getBaseTime()); +} + void nmWxTimeDependentSkin::onSkinVsRateClicked() { // 如果已经显示了图表,就恢复原始状态 @@ -876,53 +929,11 @@ void nmWxTimeDependentSkin::onSkinVsRateClicked() return; } - // 进入SkinVsRate模式前,保存当前完整状态 - saveStateBeforeSkinVsRate(); - // 创建交互式图表 showInteractiveChart(); hideToolbarButtons(); } -void nmWxTimeDependentSkin::saveStateBeforeSkinVsRate() -{ - // 保存完整的流动段数据副本(包括所有dSdQ值) - m_savedFlowSegmentData = m_perforationData; - - // 保存"添加流量相关"复选框状态 - m_savedRateDependentState = m_pAddRateDependentCheckBox ? - m_pAddRateDependentCheckBox->isChecked() : false; -} - -void nmWxTimeDependentSkin::restoreStateAfterSkinVsRate() -{ - // 只恢复dSdQ值,保留skin值的修改 - QVector currentSegments = m_perforationData.getFlowSegments(); - const QVector& savedSegments = m_savedFlowSegmentData.getFlowSegments(); - - // 确保两个数组大小一致 - if(currentSegments.size() == savedSegments.size()) { - for(int i = 0; i < currentSegments.size(); ++i) { - // 只恢复dSdQ值,保留当前的skin值 - currentSegments[i].dSdQ = savedSegments[i].dSdQ; - // skin值保持不变:currentSegments[i].skinValue 不修改 - } - - // 更新流动段数据 - m_perforationData.setFlowSegments(currentSegments); - } - - // 恢复RateDependent状态 - m_perforationData.setRateDependentEnabled(m_savedRateDependentState); - - // 恢复"添加流量相关"复选框状态 - if(m_pAddRateDependentCheckBox) { - bool wasBlocked = m_pAddRateDependentCheckBox->blockSignals(true); - m_pAddRateDependentCheckBox->setChecked(m_savedRateDependentState); - m_pAddRateDependentCheckBox->blockSignals(wasBlocked); - } -} - void nmWxTimeDependentSkin::showInteractiveChart() { if(m_pChartSplitter == nullptr) return; @@ -953,9 +964,8 @@ void nmWxTimeDependentSkin::showInteractiveChart() m_pChartLayout->addWidget(m_pChartWidget, 1); m_pChartWidget->show(); - // 设置对齐到流量段的初始状态 - bool snapToRateChanges = m_pSnapToRateChangesCheckBox->isChecked(); - m_pChartWidget->setSnapToRateChanges(snapToRateChanges); + // 第二页不使用流量变化点对齐,第一页的设置仍保留在数据中。 + m_pChartWidget->setSnapToRateChanges(false); m_pChartWidget->setFlowSegmentData(m_perforationData); @@ -1170,31 +1180,16 @@ void nmWxTimeDependentSkin::hideToolbarButtons() m_pEditContainer->setVisible(true); } - // 隐藏"显示日期"复选框 - if(m_pShowDatesCheckBox) { - m_pShowDatesCheckBox->setStyleSheet( - "QCheckBox { color: transparent; }" - "QCheckBox::indicator { width: 0px; height: 0px; }" - "QCheckBox::indicator:unchecked { image: none; }" - "QCheckBox::indicator:checked { image: none; }" - ); - m_pShowDatesCheckBox->setEnabled(false); - } - - // 保存并禁用"添加流量相关"复选框 - if(m_pAddRateDependentCheckBox) { - // 阻止信号触发 - bool wasBlocked = m_pAddRateDependentCheckBox->blockSignals(true); - m_pAddRateDependentCheckBox->setChecked(false); - m_pAddRateDependentCheckBox->blockSignals(wasBlocked); - m_pAddRateDependentCheckBox->setEnabled(false); - - // 临时禁用数据的RateDependent状态(不会清零dSdQ值,因为我们有备份) - m_perforationData.setRateDependentEnabled(false); - - // 刷新表格显示(会自动隐藏dSdQ列) - updateTableFromFlowSegments(); - } + // 仅改变第二页的控件状态,屏蔽信号以保留第一页设置及原有dS/dQ数据。 + bool snapBlocked = m_pSnapToRateChangesCheckBox->blockSignals(true); + m_pSnapToRateChangesCheckBox->setChecked(false); + m_pSnapToRateChangesCheckBox->setEnabled(false); + m_pSnapToRateChangesCheckBox->blockSignals(snapBlocked); + bool rateBlocked = m_pAddRateDependentCheckBox->blockSignals(true); + m_pAddRateDependentCheckBox->setChecked(false); + m_pAddRateDependentCheckBox->setEnabled(false); + m_pAddRateDependentCheckBox->blockSignals(rateBlocked); + updateTableFromFlowSegments(); } void nmWxTimeDependentSkin::restoreToolbarState() @@ -1239,18 +1234,16 @@ void nmWxTimeDependentSkin::restoreToolbarState() m_pEditContainer->setVisible(false); } - // 恢复"显示日期"复选框 - if(m_pShowDatesCheckBox) { - m_pShowDatesCheckBox->setStyleSheet(""); // 清除样式 - m_pShowDatesCheckBox->setEnabled(true); - } - - restoreStateAfterSkinVsRate(); - - // 启用"添加流量相关"复选框 - if(m_pAddRateDependentCheckBox) { - m_pAddRateDependentCheckBox->setEnabled(true); - } + // 从保留的数据恢复第一页设置,不触发重新吸附或清零流量相关系数。 + bool snapBlocked = m_pSnapToRateChangesCheckBox->blockSignals(true); + m_pSnapToRateChangesCheckBox->setChecked(m_perforationData.isSnapToRateChangesEnabled()); + m_pSnapToRateChangesCheckBox->setEnabled(true); + m_pSnapToRateChangesCheckBox->blockSignals(snapBlocked); + bool rateBlocked = m_pAddRateDependentCheckBox->blockSignals(true); + m_pAddRateDependentCheckBox->setChecked(m_perforationData.isRateDependentEnabled()); + m_pAddRateDependentCheckBox->setEnabled(true); + m_pAddRateDependentCheckBox->blockSignals(rateBlocked); + updateCharts(); // 刷新表格显示 updateTableFromFlowSegments(); @@ -1906,8 +1899,8 @@ bool nmWxTimeDependentSkin::insertFlowSegmentAtMiddle(int segmentIndex) // 更新流动段数据 m_perforationData.setFlowSegments(segments); - // 选中新插入的段(segment2) - selectFlowSegment(prevSegmentIndex + 1); + // 保留原选中行,连续插入时始终取该行前一个区间的中点。 + selectFlowSegment(segmentIndex + 1); return true; } @@ -2001,10 +1994,9 @@ void nmWxTimeDependentSkin::onAccept() return; } - // 对比模式临时禁用了流量相关项,提交前恢复原设置并保存当前拟合线。 + // 对比模式直接保留当前选项设置,提交前保存拟合线。 if(m_pChartWidget) { saveChartPositionsToData(); - restoreStateAfterSkinVsRate(); } // 副本数据同步回原始射孔段 @@ -2430,81 +2422,71 @@ bool nmWxTimeDependentSkin::mergeSegmentWithPrevious(int segmentIndex) bool nmWxTimeDependentSkin::insertFlowSegmentBeforeSegment(int segmentIndex) { - QVector segments = m_perforationData.getFlowSegments(); - + const QVector& segments = m_perforationData.getFlowSegments(); if(segmentIndex <= 0 || segmentIndex >= segments.size()) return false; - double insertTime = m_currentWell->findNearestFlowStepBoundaryBeforeSegment(segmentIndex, m_currentPerforationIndex); - - if(insertTime < 0) return false; - - // 量化 - double insertSec = toSeconds(insertTime, "hr"); - insertTime = fromSeconds(insertSec, "hr"); - - int prevSegmentIndex = segmentIndex - 1; - bool success = splitFlowSegmentAtTime(prevSegmentIndex, insertTime); - - if(success) selectFlowSegment(prevSegmentIndex + 1); + // 使用窗口当前副本的区间,避免刚添加的分段尚未保存到井对象而无法插入。 + const int prevSegmentIndex = segmentIndex - 1; + const double startSec = toSeconds(segments[prevSegmentIndex].segmentStart.getValue().toDouble(), "hr"); + const double endSec = qMin(toSeconds(segments[segmentIndex].segmentStart.getValue().toDouble(), "hr"), + toSeconds(m_currentWell->getTotalTimeRange(), "hr")); + const QVector boundaries = m_currentWell->getAllFlowStepBoundaries(); + double insertSec = -1.0; + for(int i = 0; i < boundaries.size(); ++i) { + const double boundarySec = toSeconds(boundaries[i], "hr"); + if(boundarySec > startSec && boundarySec < endSec && boundarySec > insertSec) { + insertSec = boundarySec; + } + } + if(insertSec < 0) return false; + const bool success = splitFlowSegmentAtTime(prevSegmentIndex, fromSeconds(insertSec, "hr")); + // 插入后仍选中原来的时间行,保持图表与表格的当前流动段一致。 + if(success) selectFlowSegment(segmentIndex + 1); return success; } void nmWxTimeDependentSkin::addLastFlowSegmentIfMissing(bool snapToRateChanges) { - double totalTime = m_currentWell->getTotalTimeRange(); - - if(totalTime <= 0) { - return; - } + const double totalTime = m_currentWell->getTotalTimeRange(); + if(totalTime <= 0) return; - // 如果没有任何段,初始化默认段并返回 QVector segments = m_perforationData.getFlowSegments(); - if(segments.isEmpty()) { initializeDefaultFlowSegment(); - return; + segments = m_perforationData.getFlowSegments(); } + if(segments.isEmpty()) return; - // 找到最后一个理想的100间隔分割点 - double lastIdealSplitTime = -1.0; - - for(double time = 100.0; time < totalTime; time += 100.0) { - double actualTime = time; - - if(snapToRateChanges) { - actualTime = m_currentWell->findNearestFlowStepBoundary(time); + double addTime = -1.0; + if(snapToRateChanges) { + // 对齐时只补最后一个流量变化点,不把曲线最右侧的终止时间当作变化点。 + const QVector boundaries = m_currentWell->getAllFlowStepBoundaries(); + const double totalSec = toSeconds(totalTime, "hr"); + for(int i = 0; i < boundaries.size(); ++i) { + const double boundarySec = toSeconds(boundaries[i], "hr"); + if(boundarySec > 0 && boundarySec < totalSec) { + addTime = qMax(addTime, fromSeconds(boundarySec, "hr")); + } } - - if(actualTime > 0 && actualTime < totalTime) { - lastIdealSplitTime = actualTime; + if(addTime < 0) return; + for(int i = 0; i < segments.size(); ++i) { + if(toSeconds(segments[i].segmentStart.getValue().toDouble(), "hr") == toSeconds(addTime, "hr")) { + return; // 已有结束点时不重复添加。 + } } - } - - // 如果没有找到理想分割点,说明总时间小于100,不需要添加 - if(lastIdealSplitTime < 0) { - return; - } - - // 检查这个最后的理想分割点是否已经存在 - bool exists = false; - - for(int i = 0; i < segments.size(); ++i) { - double startTime = segments[i].segmentStart.getValue().toDouble(); - - if(qAbs(startTime - lastIdealSplitTime) < 1e-6) { - exists = true; - break; + } else { + // 未对齐时按内部小时从末行递增100,允许连续添加到原始曲线范围之后。 + addTime = segments.last().segmentStart.getValue().toDouble() + 100.0; + if(addTime >= segments.last().segmentEnd.getValue().toDouble()) { + segments.last().segmentEnd.setValue(addTime + 100.0); + m_perforationData.setFlowSegments(segments); } } - // 如果不存在,则添加这个分割点 - if(!exists) { - int segmentIndex = m_perforationData.findFlowSegmentByTime(lastIdealSplitTime); - - if(segmentIndex >= 0) { - splitFlowSegmentAtTime(segmentIndex, lastIdealSplitTime); - } + const int segmentIndex = m_perforationData.findFlowSegmentByTime(addTime); + if(segmentIndex >= 0 && splitFlowSegmentAtTime(segmentIndex, addTime)) { + selectFlowSegment(segmentIndex + 1); } } From 8a78c5443ddbde4e702647e43617725c2eab0ed5 Mon Sep 17 00:00:00 2001 From: lvjunjie Date: Mon, 2 Nov 2026 10:35:13 +0800 Subject: [PATCH 8/9] =?UTF-8?q?fix(nmNum):=20=E8=B0=83=E6=95=B4=E5=B0=84?= =?UTF-8?q?=E5=AD=94=E4=BA=8B=E4=BB=B6=E5=88=A0=E9=99=A4=E8=A7=84=E5=88=99?= =?UTF-8?q?=E5=8F=8A=E8=A1=A8=E7=9A=AE=E5=88=86=E5=89=B2=E8=BE=B9=E7=95=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Src/nmNum/nmSubWxs/nmWxPerforationClosing.cpp | 14 ++++++++++---- Src/nmNum/nmSubWxs/nmWxPressFlowChartWidget.cpp | 13 +------------ Src/nmNum/nmSubWxs/nmWxTimeDependentSkin.cpp | 6 ++++++ 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/Src/nmNum/nmSubWxs/nmWxPerforationClosing.cpp b/Src/nmNum/nmSubWxs/nmWxPerforationClosing.cpp index 97c78797..943650c2 100644 --- a/Src/nmNum/nmSubWxs/nmWxPerforationClosing.cpp +++ b/Src/nmNum/nmSubWxs/nmWxPerforationClosing.cpp @@ -494,12 +494,18 @@ void nmWxPerforationClosing::onRemoveClicked() qSort(timeValuesInHr.begin(), timeValuesInHr.end()); - if(hasSelection && selectedIndex >= 0 && selectedIndex < timeValuesInHr.size()) { - timeValuesInHr.removeAt(selectedIndex); - } else if(!timeValuesInHr.isEmpty()) { - timeValuesInHr.removeLast(); + // 未选中具体事件时,沿用删除末尾事件的入口。 + if(!hasSelection || selectedIndex < 0 || selectedIndex >= timeValuesInHr.size()) { + selectedIndex = timeValuesInHr.size() - 1; } + // 按Saphir的行操作规则删除当前行及下一行,先删下一行以免索引移位。 + if(selectedIndex + 1 < timeValuesInHr.size()) { + timeValuesInHr.removeAt(selectedIndex + 1); + } + // 当前行位于末尾时,只删除自身。 + timeValuesInHr.removeAt(selectedIndex); + m_baseTimeValues = timeValuesInHr; if(m_baseTimeValues.isEmpty()) { diff --git a/Src/nmNum/nmSubWxs/nmWxPressFlowChartWidget.cpp b/Src/nmNum/nmSubWxs/nmWxPressFlowChartWidget.cpp index 9c8a25cc..e942067e 100644 --- a/Src/nmNum/nmSubWxs/nmWxPressFlowChartWidget.cpp +++ b/Src/nmNum/nmSubWxs/nmWxPressFlowChartWidget.cpp @@ -564,35 +564,24 @@ void nmWxPressFlowChartWidget::drawSelectedFlowSegments(QPainter& painter) } } - // 2. 绘制所有流动段的边界线(蓝色) + // 2. 只绘制各流动段的起点边界线,不额外绘制末尾终点(蓝色) painter.setPen(QPen(QColor(0, 0, 255), 2)); // 蓝色,2像素宽 for(int i = 0; i < segments.size(); ++i) { const FlowSegmentData& segment = segments[i]; double startTime = segment.segmentStart.getValue().toDouble(); - double endTime = segment.segmentEnd.getValue().toDouble(); // 如果是外部拖动状态,并且是被拖动的边界,使用拖动中的时间 if(m_externalDragActive && i == m_externalDragBoundaryIndex) { startTime = m_externalDragBoundaryTime; } - // 检查下一个段是否受到拖动影响 - if(m_externalDragActive && i + 1 < segments.size() && (i + 1) == m_externalDragBoundaryIndex) { - endTime = m_externalDragBoundaryTime; - } - // 检查是否处于合并状态 // 转换为屏幕坐标 int leftX = m_plotArea.left() + (startTime - m_minX) / (m_maxX - m_minX) * m_plotArea.width(); // 绘制起点边界线 painter.drawLine(leftX, m_plotArea.top(), leftX, m_plotArea.bottom()); - // 只有最后一个段画终点 - if (i == segments.size() - 1) { - int rightX = m_plotArea.left() + (endTime - m_minX) / (m_maxX - m_minX) * m_plotArea.width(); - painter.drawLine(rightX, m_plotArea.top(), rightX, m_plotArea.bottom()); - } } diff --git a/Src/nmNum/nmSubWxs/nmWxTimeDependentSkin.cpp b/Src/nmNum/nmSubWxs/nmWxTimeDependentSkin.cpp index ab867e27..94149fe3 100644 --- a/Src/nmNum/nmSubWxs/nmWxTimeDependentSkin.cpp +++ b/Src/nmNum/nmSubWxs/nmWxTimeDependentSkin.cpp @@ -2133,6 +2133,12 @@ void nmWxTimeDependentSkin::splitAllFlowSegments() if(flowStepBoundaries.isEmpty()) return; + // 末尾关井段沿用前一段表皮,不单独生成分割点;仍保留总时长作为最后一段终点。 + const QVector flowSteps = m_currentWell->getFlowPoints(); + for(int i = flowSteps.size() - 1; i > 0 && qFuzzyIsNull(flowSteps[i].y()); --i) { + flowStepBoundaries.remove(i); + } + // 统一量化 for(int i = 0; i < flowStepBoundaries.size(); ++i) { double sec = toSeconds(flowStepBoundaries[i], "hr"); From 596ca7723eae18c26596751aa0a24ef9236e5b17 Mon Sep 17 00:00:00 2001 From: lvjunjie Date: Wed, 4 Nov 2026 17:30:50 +0800 Subject: [PATCH 9/9] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E5=8F=98?= =?UTF-8?q?=E8=A1=A8=E7=9A=AE=E8=87=AA=E5=8A=A8=E8=AE=A1=E7=AE=97=EF=BC=8C?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=8D=95=E7=9B=B8=E5=8F=8A=E6=B2=B9=E6=B0=B4?= =?UTF-8?q?=E4=B8=A4=E7=9B=B8=E6=A8=A1=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Bin/Config/Lang/cn/nmNum_cn.qm | Bin 195952 -> 198539 bytes Bin/Config/Lang/cn/nmNum_cn.ts | 60 +++ Include/nmNum/nmData/nmDataAnalyzeManager.h | 2 + .../nmNum/nmSubWxs/nmWxTimeDependentSkin.h | 6 + Src/nmNum/nmData/nmDataAnalyzeManager.cpp | 8 + Src/nmNum/nmSubWxs/nmWxTimeDependentSkin.cpp | 383 +++++++++++++++++- 6 files changed, 453 insertions(+), 6 deletions(-) diff --git a/Bin/Config/Lang/cn/nmNum_cn.qm b/Bin/Config/Lang/cn/nmNum_cn.qm index fe7f48bbc9f0f0ff59a9280607c2e4d8799cc8d0..a7529eb56a318e80d1cdfeb93c073580de3612cd 100644 GIT binary patch delta 3488 zcmZ`)dsGuw9=`K~BoYxHmExn~U<)EkB2}bnfudl2mEjShuVA&d-@G?Ro{R7y*2 zE%-WjLlgxcE%*eK6p&U#>9(Gw2kkj+_w?-6O8=m{wY9dpuC{B@y*HCc+H;n3!tcz? zo%wy=cfap)lv4BR^C8jO0ZZ{v~PYfBMY7P~imJDmIJY~!=ctOGd+ zsh&pA{)tk%7bvyopGxh05v#xnE#Km;pojNw;8t?gkmiXFDqyrdPaq^eg5W|Xem&T! zZM}zr`y~YDZ()@x2KLXyI#ndJXX8xO570Ror>G0y&=%}cCqeThJgDa4PJe+^S1%*D zREJbw#v}Ci%S!##>aC3RBX5Ocxt?X37zR%4#N}f+=yKuSF_F-@1YaAo9a_yEJ|u}e zG~h<qP*G183S-A=qy~;nX&Ss#FL6grIjW-Z|qA98bruN3M+EzQ0dc?NwzREkw+^ORC)G2#~(D_*!Jr zwBw*E9l4K~{@W@cZ7qV}5~X%r#=4nA-i3H@W+5C+!8-Onynh($qDm&6ZBYHXlWbUY zvKn2N5gh#hH%D!ocj_heY?0_0_K8x(DD~`t3gV+x>N%-Rh?%%aJ!eNMLVZ)zahX8~ z8c*WUKkR2T=RVdWc*lTy=k1Lseb&RJmB@&=@BJO==uO|NlA$*`3XQny9lZZb|TcmkM zB^sUWjP$tZ4?1zGQYatLO2yS@Z1Q?;=6ZqACVDo5-FLvH47oWtkUZOmQQ@MfvA z%`vlR!ltM-wc9>5AAAs+v$OU{r8_UUtdl+qfUtHsFYz{`oo9_~xuuvF*$Tm4N?P)) z$zdaS?5rSJ9d-(!n5;2bB-w5xcjYocn!y(FB^Dc5YAGjc1j+2Z&B}J(>I3RNbPdc^ ztHdMb5W}skgi5!wh9bySmvtsU?cLIg=3zr^?g8Fvk`fqqVf~%*C3~-ybs7f5$ytT~ z>BySbf(mX2Am7AU0=f|h4zZZvVR_juloLY5dN#e3Hzqp=7J( za*y*pc@sHQ9jF|aC5XBT($D4>iz2h}FFFK5n=KI}fnc$g8fD%KOCI%vY%C`z!$eC3 zTTB+4C?}sIlfHs@rYep)a!*Lnw>cH&5Q@g{L1zu;qCWJ?6)2E_7s;oHyQn%!Gva@t z*yl(B${`X5`po9n#^<)*2;@!~f=8-W)ZSea`}_3OUZ|2d-ScHR`zb7E4NYT~Ve_pm z_jqewUDMUVmItA{H9L93hx;nm&gOR2-wA+dFz|NPYLNxIKz434s!ogOpo$nxc8g8P zh*Ov?o4hNDmV)Gsl0Up&Z^^u!M#UeHqU&I*im$$rd)zNg$*bHrey)kBywwABB;7-zJfVvXOjbE6&Aln0G{$-lPs z<<+fgDqrot3TT!5e_JVoiVmc5-bj;OOhWDpESXwP-bc%q8m}oHPY7hEaC83b&Biut;NDZiL`=y3U0*jB4H0n`L5!#4A)Zx|L%WlEz8rZG$*e^ZH~Zr7*>y-~q*;`y zvxY`9H>t3t=uyhkR8;AvuH%RJ6#;ygCoV-n@fmKY$ zlCwdOym3)Fm&GVOrHDP_63TZSO{V?4TUqe+@ zhT)6RQ-zA+pP&@+o26WIX|f@|F6cq%Z%4zL<=kQ_TbYTs2H+M8j5fhoB=S0XW>ZN5 z)A%qN%0!JP10>luL6&+obPCF(*z7;<$*$64m?YRD38qn|cG|;dtopCvZ`1z=x7gic delta 1412 zcmXBSc~FyA5CHIfOTGXJA&Nv1Bove&S40pz3dj{&D5O>kgSDgBDuY!)Yrz?-)Qebc z)zSqcM(W0p6@&h>`g800>`4GAU1bU#c51r~sVty!5dilpU{kn5^Op|0G!FU8#9=WS zQfU{evF9~13sEJ<2RdalN#`DbZPNgI+o)7jENRRE(YXRZK7%+;g~v}&IHyFDnKHSD zcycSniL7bZnRC$Di@GTN_?}A=PxsX{6gx=4cphGOE}E`*q3 z!Mr#gSlkn^MkNDw9e^{Yc);8yI9nS9kktj>EzANud7VbxwAdC$8rdDJji7LMBUa6z zL3gD`y(e4dCH(Tx1GYSAFJQ;JH0G|tQ=t?n*J8b$isS`o&7@)Zok-&dvdDXY1}TbC z3IME9be-}Jp!pKI^>+Z)e2lV1Pk~+UME86lZh4Czo1%7-w4TGeVw4~q7jOY1#5 zu=8tDD2!++B#T0ey>2`=U5(f{PSIYMy-hbn?t8ice$r0LvI0a?ZmtAY;Y5Kmt=Msz z>@&4!xIy9G;{lDKqT^}b2+5Niw);8koN(Bqqf8$a*11rT&vw^^qV_PMlb(w->T`u{ zr1P_~k(ZFKZ?WX~RgSAE7xw)*8GN6iv6d>7Y4SJgR|8vhmRp{o2DDgdR2k!KT*Kv$ zjRQOPl-rQ75>S-quyqb;{ES%nCk^|p#_Dv^yfTj6W2EuV^Jsp~y*w=JDlk?Ix3>XS z4^f|gNo3u1anL4Vl39MnV?|vmKFC{dXC#^)YXX5;L@fkaaTafgQ9EJF6B=Bs z!yBG6;pxf`s5RNYXNFj%O^e+4sK`L2H`}S z_9bZ!Bx#r}NrOM=ZHMhBVfrF;nyKG-8?}1 nmWxTimeDependentSkin + + Select a well with valid model results before estimating skin. + 请先选择具有有效模型结果的井,再估算表皮。 + + + Skin estimation requires a single perforation. + 自动估算表皮仅支持单射孔。 + + + Skin estimation does not yet support perforation closing intervals. + 自动估算表皮暂不支持设有射孔封堵区间的模型。 + + + Generate a model with the current inputs and a constant skin before computing skin steps. + 请先使用当前输入和恒定表皮生成模型结果,再计算各段表皮。 + + + The model result does not match this well's pressure and rate data. Generate the model again. + 模型结果与当前井的压力、流量数据不一致,请重新生成模型。 + + + Observation wells have no controlled rate for skin estimation. + 观察井没有可用于估算表皮的自身控制流量。 + + + Skin estimation requires model and measured pressure curves. + 自动估算表皮需要模型压力曲线和实测压力曲线。 + + + The model requires valid permeability, thickness and skin. + 模型的渗透率、厚度和表皮参数必须有效。 + + + Skin estimation supports single-phase oil, gas, water and oil-water models only. + 自动估算表皮支持单相油、气、水以及油水两相模型。 + + + Set positive reference volume factors and viscosities in the analysis PVT settings. + 请在分析的 PVT 设置中填写有效的参考体积系数和黏度,数值必须大于零。 + + + Oil-water estimation requires finite phase rates with the same flow direction. + 油水两相估算要求各相流量有效,且油、水流动方向一致。 + + + Variable-PVT skin estimation requires finite, positive B and viscosity values on a monotonic pressure table. + 变 PVT 表皮估算要求压力表严格单调,体积系数和黏度均为有效正数。 + + + Cannot read reference PVT values from this analysis. + 无法读取本分析的 PVT 参考值。 + + + The model rate schedule or pressure conversion factor is invalid. + 模型的流量制度或压力换算系数无效。 + + + No skin steps could be estimated. Check pressure data, non-zero rate steps and the gas PVT pressure range. + 没有可估算的表皮分段。请检查压力数据、非零流量段及气体 PVT 压力范围。 + Time Dependent Skin 时间相关表皮系数 diff --git a/Include/nmNum/nmData/nmDataAnalyzeManager.h b/Include/nmNum/nmData/nmDataAnalyzeManager.h index 57d3cd56..eebfc868 100644 --- a/Include/nmNum/nmData/nmDataAnalyzeManager.h +++ b/Include/nmNum/nmData/nmDataAnalyzeManager.h @@ -473,6 +473,8 @@ public: // 获取PEBI求解器需要的pvt数据 nmDataPvtParaForPebi* getPebiPvtPara(); + // 读取所属分析窗口当前选用的 PVT 单值,参数名与底层 PVT 接口一致。 + bool getAnalysisPvtValues(const QStringList& names, QMap& values) const; // 获取气单相变化 PVT 求解所需的压力/拟压力转换表。 bool getPebiPseudoPressureTable(std::vector& pressure, std::vector& pseudoPressure); diff --git a/Include/nmNum/nmSubWxs/nmWxTimeDependentSkin.h b/Include/nmNum/nmSubWxs/nmWxTimeDependentSkin.h index fe7ca7de..5fc29164 100644 --- a/Include/nmNum/nmSubWxs/nmWxTimeDependentSkin.h +++ b/Include/nmNum/nmSubWxs/nmWxTimeDependentSkin.h @@ -6,6 +6,7 @@ #include #include #include +#include #include "nmDataAnalyzeManager.h" #include "nmDataWellBase.h" @@ -44,6 +45,7 @@ public: void onRemoveClicked(); void onSplitStepsClicked(); void onComputeClicked(); + void updateComputeState(); void onSkinVsRateClicked(); void onPerforationClosingBtnClicked(); void onPerforationClosingAccepted(); @@ -95,6 +97,9 @@ private: // 更新图表 void updateCharts(); + // 检查自动估算表皮所需的基准结果和模型范围。 + QString skinComputeUnavailableReason() const; + void showInteractiveChart(); // 显示交互式图表 void restoreOriginalCharts(); // 恢复原始占位符 void initializeChartFromData(); @@ -211,6 +216,7 @@ private: QString m_dSdQDisplayUnit; // 数据成员 + QPointer m_pDataManager; // 窗口所属分析,计算时不随当前页签切换 nmDataWellBase* m_currentWell; // 当前井数据 nmDataPerforation m_perforationData; // 射孔段数据 int m_currentPerforationIndex; // 当前射孔段索引 diff --git a/Src/nmNum/nmData/nmDataAnalyzeManager.cpp b/Src/nmNum/nmData/nmDataAnalyzeManager.cpp index e0b8f5ec..9f8559d7 100644 --- a/Src/nmNum/nmData/nmDataAnalyzeManager.cpp +++ b/Src/nmNum/nmData/nmDataAnalyzeManager.cpp @@ -4169,6 +4169,14 @@ nmDataPvtParaForPebi* nmDataAnalyzeManager::getPebiPvtPara() return m_pebiPvtPara; } +bool nmDataAnalyzeManager::getAnalysisPvtValues(const QStringList& names, QMap& values) const +{ + // 按创建本 Manager 时绑定的分析读取,避免切换页签后串用其他分析的参考物性。 + values.clear(); + nmDataAnalyzeContextProvider* context = nmDataAnalyzeContext::provider(); + return context && m_pOwnerFitting && context->getPvtParaValues(m_pOwnerFitting, names, values); +} + bool nmDataAnalyzeManager::getPebiPseudoPressureTable(std::vector& pressure, std::vector& pseudoPressure) { diff --git a/Src/nmNum/nmSubWxs/nmWxTimeDependentSkin.cpp b/Src/nmNum/nmSubWxs/nmWxTimeDependentSkin.cpp index 94149fe3..f655c35b 100644 --- a/Src/nmNum/nmSubWxs/nmWxTimeDependentSkin.cpp +++ b/Src/nmNum/nmSubWxs/nmWxTimeDependentSkin.cpp @@ -3,6 +3,7 @@ #include "nmDialogPreviewGuard.h" #include "nmWxPressFlowChartWidget.h" #include "nmWxChartWidget.h" +#include "nmPebiResultSnapshot.h" #include "iUnitGroup.h" #include "iUnitHelper.h" #include "ZxDataEditorDelegate.h" @@ -38,6 +39,274 @@ #include #include +#include +#include + +namespace { + +const double skinPressureUnitFactor = 1e-3 / (86400.0 * 2.0 * 3.14159265358979323846 * 9.869233e-16 * 1e6); + +bool validSkinPressureCurve(const QVector >& curve) +{ + // 插值只接受同一时间基准下有限、严格递增的压力序列。 + if(curve.size() < 2 || curve[0].size() < 2 || curve[0].size() != curve[1].size()) return false; + for(int i = 0; i < curve[0].size(); ++i) { + if(!qIsFinite(curve[0][i]) || !qIsFinite(curve[1][i]) || + (i > 0 && curve[0][i] <= curve[0][i - 1])) return false; + } + return true; +} + +bool interpolateSkinPressure(const QVector >& curve, double time, double& pressure, + bool extrapolate = false) +{ + // 压力取样按步骤末端线性插值、沿端点斜率外推;PVT 查值默认仍限制在表内。 + const QVector& times = curve[0]; + if(!extrapolate && (time < times.first() || time > times.last())) return false; + const int index = static_cast(std::lower_bound(times.constBegin(), times.constEnd(), time) - times.constBegin()); + const int right = qBound(1, index, times.size() - 1); + const double fraction = (time - times[right - 1]) / (times[right] - times[right - 1]); + pressure = curve[1][right - 1] + fraction * (curve[1][right] - curve[1][right - 1]); + return qIsFinite(pressure); +} + +// 只保存本次估算用的值数据;液体参考物性读取所属分析,曲线和制度沿用模型快照。 +struct SkinComputeData +{ + QVector rates; + QVector > pressureMobility; + double pressureFactor; +}; + +bool skinPvtCurve(const QVector& pressure, const QVector& values, + QVector >& curve) +{ + // PVT 表可按压力升序或降序保存,统一方向后复用压力曲线的校验和插值。 + curve.resize(2); + curve[0] = pressure; + curve[1] = values; + if(pressure.size() > 1 && pressure.first() > pressure.last()) { + std::reverse(curve[0].begin(), curve[0].end()); + std::reverse(curve[1].begin(), curve[1].end()); + } + if(!validSkinPressureCurve(curve)) return false; + for(int i = 0; i < curve[0].size(); ++i) { + if(curve[0][i] <= 0.0 || curve[1][i] <= 0.0) return false; + } + return true; +} + +QString prepareSkinCompute(const nmPebiResultWellSnapshot& well, + const nmPebiResultReservoirParameters& reservoir, + const nmPebiResultPvtParameters& pvt, int modelType, + const QMap& referencePvt, + SkinComputeData& data) +{ + data = SkinComputeData(); + // mD、m、mPa.s、m³/d、MPa 的单位换算;普通压差用参考物性,气拟压力模式保留积分。 + data.pressureFactor = reservoir.m_dPermeability * reservoir.m_dThickness / skinPressureUnitFactor; + if(!qIsFinite(reservoir.m_dPermeability) || reservoir.m_dPermeability <= 0.0 || + !qIsFinite(reservoir.m_dThickness) || reservoir.m_dThickness <= 0.0 || + !qIsFinite(data.pressureFactor) || data.pressureFactor <= 0.0 || !qIsFinite(well.m_dSkin)) + return nmWxTimeDependentSkin::tr("The model requires valid permeability, thickness and skin."); + + const QVector* volume = nullptr; + const QVector* viscosity = nullptr; + double constantVolume = 0.0, constantViscosity = 0.0; + NM_PHASE_TYPE phase = PHASE_UNKNOWN; + switch(modelType) { + case SMT_Oil_ConstPvt: + case SMT_Oil_VariablePvt: + phase = PHASE_Oil; + constantVolume = referencePvt.value("Bo"); + constantViscosity = referencePvt.value("Miuo"); + break; + case SMT_Water_ConstPvt: + case SMT_Water_VariablePvt: + phase = PHASE_Water; + constantVolume = referencePvt.value("Bw"); + constantViscosity = referencePvt.value("Miuw"); + break; + case SMT_Gas_VariablePvt: + // 气变化 PVT 按不使用拟压力的方式估算表皮,与油、水共用参考单值换算。 + phase = PHASE_Gas; + constantVolume = referencePvt.value("Bg"); + constantViscosity = referencePvt.value("Miug"); + break; + case SMT_Gas_PseudoPressure: + // 气拟压力模式的结果压力为 MPa;Bg 已包含气体标准条件换算。 + phase = PHASE_Gas; + volume = &pvt.m_vecBg; + viscosity = &pvt.m_vecMiug; + break; + case SMT_Oil_Water_TwoPhase: + break; + default: + return nmWxTimeDependentSkin::tr("Skin estimation supports single-phase oil, gas, water and oil-water models only."); + } + + if(modelType == SMT_Oil_Water_TwoPhase) { + // 按模型读取已冻结的油水制度,只检查数组长度以保证逐段取值有效。 + if(well.m_vecOilRates.size() != well.m_vecFlowDurations.size() || + well.m_vecWaterRates.size() != well.m_vecFlowDurations.size()) + return nmWxTimeDependentSkin::tr("The model rate schedule or pressure conversion factor is invalid."); + + // 未启用 Perrine:使用模型渗透率和油相参考物性,水量通过 Bw/Bo 换算。 + const double bo = referencePvt.value("Bo"), bw = referencePvt.value("Bw"); + const double muo = referencePvt.value("Miuo"); + if(!qIsFinite(bo) || bo <= 0.0 || !qIsFinite(bw) || bw <= 0.0 || + !qIsFinite(muo) || muo <= 0.0) + return nmWxTimeDependentSkin::tr("Set positive reference volume factors and viscosities in the analysis PVT settings."); + + data.pressureFactor /= bo * muo; + for(int i = 0; i < well.m_vecFlowDurations.size(); ++i) { + const double qo = well.m_vecOilRates[i]; + const double qw = well.m_vecWaterRates[i]; + // 两相反向流动不适用当前的等效生产流量计算。 + if(!qIsFinite(qo) || !qIsFinite(qw) || (qo < 0.0 && qw > 0.0) || (qo > 0.0 && qw < 0.0)) + return nmWxTimeDependentSkin::tr("Oil-water estimation requires finite phase rates with the same flow direction."); + data.rates.append(qo + qw * bw / bo); + } + } else { + // 求解输入已将未使用的相补零,这里直接取模型对应相的实际计算流量。 + data.rates = phase == PHASE_Oil ? well.m_vecOilRates : + (phase == PHASE_Water ? well.m_vecWaterRates : well.m_vecGasRates); + if(volume) { + QVector > volumeCurve, viscosityCurve; + if(!skinPvtCurve(pvt.m_vecPressure, *volume, volumeCurve) || + !skinPvtCurve(pvt.m_vecPressure, *viscosity, viscosityCurve)) + return nmWxTimeDependentSkin::tr("Variable-PVT skin estimation requires finite, positive B and viscosity values on a monotonic pressure table."); + // 底层 calPS 依赖全局模式且缺少冻结的配套尺度,这里保留同一快照的常规气体积分。 + data.pressureMobility = volumeCurve; + for(int i = 0; i < volumeCurve[0].size(); ++i) { + const double inverse = 1.0 / (volumeCurve[1][i] * viscosityCurve[1][i]); + if(!qIsFinite(inverse) || inverse <= 0.0) + return nmWxTimeDependentSkin::tr("Variable-PVT skin estimation requires finite, positive B and viscosity values on a monotonic pressure table."); + data.pressureMobility[1][i] = inverse; + } + } else { + // 普通压差模式使用本次读取的参考 B、黏度,不按 Pi 重新查表。 + if(!qIsFinite(constantVolume) || constantVolume <= 0.0 || + !qIsFinite(constantViscosity) || constantViscosity <= 0.0) + return nmWxTimeDependentSkin::tr("Set positive reference volume factors and viscosities in the analysis PVT settings."); + data.pressureFactor /= constantVolume * constantViscosity; + } + } + + if(!qIsFinite(data.pressureFactor) || data.pressureFactor <= 0.0 || data.rates.isEmpty() || + data.rates.size() != well.m_vecFlowDurations.size()) + return nmWxTimeDependentSkin::tr("The model rate schedule or pressure conversion factor is invalid."); + for(int i = 0; i < data.rates.size(); ++i) { + if(!qIsFinite(data.rates[i]) || !qIsFinite(well.m_vecFlowDurations[i]) || well.m_vecFlowDurations[i] <= 0.0) + return nmWxTimeDependentSkin::tr("The model rate schedule or pressure conversion factor is invalid."); + } + return QString(); +} + +bool skinPressureDifference(const SkinComputeData& data, double measuredPressure, + double modelPressure, double& difference) +{ + difference = modelPressure - measuredPressure; + if(data.pressureMobility.isEmpty()) return qIsFinite(difference); + + // 直接对冻结 PVT 的 1/(Bμ) 积分,等价于配套系数消去归一化常数后的拟压力差。 + // 分段线性插值积分密度;端点也参与积分,避免仅对累计积分值插值的区间内误差。 + const double lower = qMin(measuredPressure, modelPressure); + const double upper = qMax(measuredPressure, modelPressure); + double lowerValue, upperValue; + if(!interpolateSkinPressure(data.pressureMobility, lower, lowerValue) || + !interpolateSkinPressure(data.pressureMobility, upper, upperValue)) return false; + double integral = 0.0, left = lower, leftValue = lowerValue; + const QVector& pressure = data.pressureMobility[0]; + int index = static_cast(std::upper_bound(pressure.constBegin(), pressure.constEnd(), lower) - pressure.constBegin()); + for(; index < pressure.size() && pressure[index] < upper; ++index) { + const double value = data.pressureMobility[1][index]; + integral += (pressure[index] - left) * (leftValue + value) * 0.5; + left = pressure[index]; + leftValue = value; + } + integral += (upper - left) * (leftValue + upperValue) * 0.5; + difference = modelPressure >= measuredPressure ? integral : -integral; + return qIsFinite(difference); +} + +int estimateSkinSegments(const nmPebiResultWellSnapshot& well, + const SkinComputeData& data, + const QVector >& measured, + QVector& segments) +{ + const QVector >& model = well.m_oCurves.m_vecResultPressure; + const QVector& rates = data.rates; + if(!validSkinPressureCurve(model) || !validSkinPressureCurve(measured) || + rates.size() != well.m_vecFlowDurations.size() || rates.isEmpty()) return 0; + + QVector ends; + double end = 0.0; + for(int i = 0; i < rates.size(); ++i) { + const double duration = well.m_vecFlowDurations[i]; + if(!qIsFinite(duration) || duration <= 0.0 || !qIsFinite(rates[i])) return 0; + end += duration; + if(!qIsFinite(end)) return 0; + ends.append(end); + } + + int computedCount = 0; + QVector unmatchedSegments; + for(int i = 0; i < segments.size(); ++i) { + const double start = segments[i].segmentStart.getValue().toDouble(); + const double finish = segments[i].segmentEnd.getValue().toDouble(); + if(!qIsFinite(start) || !qIsFinite(finish) || finish <= start) { + continue; + } + + // 按 Saphir 取起点落在当前表皮段内的最后一个非零流量段,并在该流量段末端取样。 + double sampleTime = 0.0; + double rate = 0.0; + int rateIndex = -1; + for(int j = 0; j < rates.size(); ++j) { + const double rateStart = j == 0 ? 0.0 : ends[j - 1]; + if(rateStart > finish - 1e-5) break; + if(rateStart >= start - 1e-5 && rates[j] != 0.0) { + sampleTime = ends[j]; + rate = rates[j]; + rateIndex = j; + } + } + // 没有符合条件的非零流量段时删除该分界;先完成原始分段取样,避免合并改变本次计算。 + if(rateIndex < 0) { + unmatchedSegments.append(i); + continue; + } + double modelPressure = 0.0, measuredPressure = 0.0; + if(!interpolateSkinPressure(model, sampleTime, modelPressure, true) || + !interpolateSkinPressure(measured, sampleTime, measuredPressure, true)) { + continue; + } + + double pressureDifference = 0.0; + if(!skinPressureDifference(data, measuredPressure, modelPressure, pressureDifference)) { + continue; + } + // 修正始终相对目标井的冻结表皮,重复点击或换井不会累加、串用基准值。 + const double skin = well.m_dSkin + data.pressureFactor * pressureDifference / rate; + if(!qIsFinite(skin)) { + continue; + } + segments[i].skinValue.setValue(qMax(-5.0, skin)); + // 与 Compute 的单点校正一致,只估计总表皮,不从一个压力差同时反演 dS/dQ。 + segments[i].dSdQ.setValue(0.0); + ++computedCount; + } + // 从后往前移除无匹配段,前段延伸至被删段末端,保留其已算出的表皮值。 + for(int i = unmatchedSegments.size() - 1; i >= 0; --i) { + const int index = unmatchedSegments[i]; + if(index > 0) segments[index - 1].segmentEnd = segments[index].segmentEnd; + segments.remove(index); + } + return computedCount; +} + +} // 静态成员变量定义 nmWxTimeDependentSkin* nmWxTimeDependentSkin::s_instance = nullptr; @@ -76,6 +345,7 @@ void nmWxTimeDependentSkin::showForCurrentWell(nmDataWellBase* pSpecificWellData nmWxTimeDependentSkin::nmWxTimeDependentSkin(nmDataWellBase* pWellData, QWidget *parent) : iDlgBase(parent) + , m_pDataManager(nmDataAnalyzeManager::getCurrentInstance()) , m_pChartWidget(nullptr) , m_pOriginalPressureChart(nullptr) , m_pOriginalFlowRateChart(nullptr) @@ -217,6 +487,11 @@ nmWxTimeDependentSkin::nmWxTimeDependentSkin(nmDataWellBase* pWellData, QWidget connect(m_pShowDatesCheckBox, SIGNAL(toggled(bool)), this, SLOT(onShowDatesToggled(bool))); connect(m_pComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(onPerforationChanged(int))); + + // 新结果发布或失效后同步按钮状态;点击时仍重新检查,避免使用旧分析结果。 + nmDataAnalyzeManager* manager = m_pDataManager.data(); + if(manager) connect(manager, SIGNAL(sigPebiResultSnapshotChanged()), this, SLOT(updateComputeState())); + updateComputeState(); } nmWxTimeDependentSkin::~nmWxTimeDependentSkin() @@ -275,8 +550,6 @@ void nmWxTimeDependentSkin::initUI() createButton(m_pRemoveBtn, "nmTimeDepSkin3", tr("Remove")); createButton(m_pSplitStepsBtn, "nmTimeDepSkin4", tr("Split Steps")); createButton(m_pComputeBtn, "nmTimeDepSkin5", tr("Compute")); - // 计算功能尚未实现,隐藏图标和文字容器,让射孔封堵前移到该位置。 - m_pComputeBtn->parentWidget()->hide(); createButton(m_pPerforationClosingBtn, "nmTimeDepSkin6", tr("Perforation Closing")); m_pToolbarLayout->addStretch(); @@ -576,8 +849,6 @@ void nmWxTimeDependentSkin::updateTableFromFlowSegments() const QVector& segments = m_perforationData.getFlowSegments(); - if(segments.isEmpty()) return; - const bool enableRateDep = m_perforationData.isRateDependentEnabled(); const int requiredCols = enableRateDep ? 4 : 3; @@ -897,9 +1168,107 @@ void nmWxTimeDependentSkin::onSplitStepsClicked() updateTableFromFlowSegments(); } +QString nmWxTimeDependentSkin::skinComputeUnavailableReason() const +{ + // 多井按井 UUID 查找各自的冻结结果,观察井没有可用于表皮反算的自身流量。 + nmDataAnalyzeManager* manager = m_pDataManager.data(); + if(!manager || !m_currentWell) + return tr("Select a well with valid model results before estimating skin."); + if(m_currentWell->getPerforationCount() != 1) + return tr("Skin estimation requires a single perforation."); + if(!m_perforationData.getTimeValues().isEmpty()) + return tr("Skin estimation does not yet support perforation closing intervals."); + if(!manager->isPebiResultSnapshotCurrent()) + return tr("Generate a model with the current inputs and a constant skin before computing skin steps."); + + QSharedPointer snapshot = manager->getPebiResultSnapshot(); + const nmPebiResultWellSnapshot* well = snapshot->findWell(m_currentWell->getWellInstanceId()); + if(!well || !well->m_bHasSkin || + well->m_sGaugeInputSha1.isEmpty() || well->m_sGaugeInputSha1 != m_currentWell->calculateGaugeInputSha1()) + return tr("The model result does not match this well's pressure and rate data. Generate the model again."); + if(well->m_eWellMode != NM_CaseWell_RateControlled) + return tr("Observation wells have no controlled rate for skin estimation."); + // 直井、压裂井和多段压裂井共用模型压力与井总流量,估算整井等效表皮。 + if(!validSkinPressureCurve(well->m_oCurves.m_vecResultPressure) || m_currentWell->getPressurePoints().size() < 2) + return tr("Skin estimation requires model and measured pressure curves."); + return QString(); +} + +void nmWxTimeDependentSkin::updateComputeState() +{ + // 仅更新计算按钮的可用状态,切换页面不改变原有模型设置。 + const QString reason = skinComputeUnavailableReason(); + m_pComputeBtn->setEnabled(reason.isEmpty() && !m_pChartWidget); +} + void nmWxTimeDependentSkin::onComputeClicked() { + const QString reason = skinComputeUnavailableReason(); + if(!reason.isEmpty()) { + updateComputeState(); + QMessageBox::information(this, tr("Compute"), reason); + return; + } + QSharedPointer snapshot = + m_pDataManager->getPebiResultSnapshot(); + const nmPebiResultWellSnapshot* well = snapshot->findWell(m_currentWell->getWellInstanceId()); + const nmPebiResultPvtParameters& pvt = snapshot->getPvtParameters(); + const int modelType = snapshot->getSolverSettings().m_nSolverModelType; + // 每次点击从所属分析重新读取参考单值,不回退到旧快照或默认物性。 + QStringList names; + if(modelType == SMT_Oil_ConstPvt || modelType == SMT_Oil_VariablePvt || modelType == SMT_Oil_Water_TwoPhase) + names << "Bo" << "Miuo"; + if(modelType == SMT_Water_ConstPvt || modelType == SMT_Water_VariablePvt) + names << "Bw" << "Miuw"; + else if(modelType == SMT_Oil_Water_TwoPhase) + names << "Bw"; + if(modelType == SMT_Gas_VariablePvt) + names << "Bg" << "Miug"; + QMap referencePvt; + if(!names.isEmpty() && !m_pDataManager->getAnalysisPvtValues(names, referencePvt)) { + QMessageBox::information(this, tr("Compute"), tr("Cannot read reference PVT values from this analysis.")); + return; + } + SkinComputeData data; + const QString preparationError = prepareSkinCompute(*well, snapshot->getReservoirParameters(), pvt, modelType, referencePvt, data); + if(!preparationError.isEmpty()) { + QMessageBox::information(this, tr("Compute"), preparationError); + return; + } + QVector > measured(2); + const QVector points = m_currentWell->getPressurePoints(); + for(int i = 0; i < points.size(); ++i) { + measured[0].append(points[i].x()); + measured[1].append(points[i].y()); + } + // 只更新本窗口副本;计算后移除无匹配流量段的分界,取消仍可放弃本次估算。 + QVector segments = m_perforationData.getFlowSegments(); + const int originalSegmentCount = segments.size(); + const int computedCount = estimateSkinSegments(*well, data, measured, segments); + if(computedCount == 0 && segments.size() == originalSegmentCount) { + QMessageBox::information(this, tr("Compute"), + tr("No skin steps could be estimated. Check pressure data, non-zero rate steps and the gas PVT pressure range.")); + return; + } + // 分段删除后重新定位选中项,避免旧索引指向其他行或越界;空结果由表格刷新清除旧行。 + int selectedIndex = 0; + if(segments.size() != originalSegmentCount && !segments.isEmpty()) { + for(int i = 0; i < segments.size(); ++i) { + if(segments[i].isSelected) { + selectedIndex = i; + break; + } + } + for(int i = 0; i < segments.size(); ++i) segments[i].isSelected = i == selectedIndex; + } + m_perforationData.setFlowSegments(segments); + if(segments.size() != originalSegmentCount && !segments.isEmpty()) + m_perforationData.setSelectedSegmentIndex(selectedIndex); + // 原来的表皮/流量拟合线已不对应新估算值,下次进入第二页时重新拟合。 + m_perforationData.setLinePositions(-999.0, -999.0, -999.0, -999.0); + updateCharts(); + updateTableFromFlowSegments(); } void nmWxTimeDependentSkin::onPerforationClosingBtnClicked() @@ -918,6 +1287,7 @@ void nmWxTimeDependentSkin::onPerforationClosingAccepted() m_perforationData.setTimeValues(data.getTimeValues()); m_perforationData.setTimeStates(data.getTimeStates()); m_perforationData.setBaseTime(data.getBaseTime()); + updateComputeState(); } void nmWxTimeDependentSkin::onSkinVsRateClicked() @@ -1207,9 +1577,10 @@ void nmWxTimeDependentSkin::restoreToolbarState() m_pSplitStepsBtn->parentWidget()->setVisible(true); } - // 切回默认界面时仍保持计算入口隐藏。 + // 计算入口仅在默认的表皮分段页面显示。 if(m_pComputeBtn && m_pComputeBtn->parentWidget()) { - m_pComputeBtn->parentWidget()->setVisible(false); + m_pComputeBtn->parentWidget()->setVisible(true); + updateComputeState(); } if(m_pPerforationClosingBtn && m_pPerforationClosingBtn->parentWidget()) {