From 1852111c0752e4d206d0dda7ad9eae43dfcf48f0 Mon Sep 17 00:00:00 2001 From: lh <2334563547@qq.com> Date: Mon, 24 Aug 2026 15:02:58 +0800 Subject: [PATCH] =?UTF-8?q?feat(nmSubWxs):=20=E5=A2=9E=E5=8A=A0=E7=BC=96?= =?UTF-8?q?=E8=BE=91=E4=BA=95=E5=8E=8B=E5=8A=9B=E6=B5=81=E9=87=8F=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E6=9F=A5=E7=9C=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增编辑井顶部分类导航和只读压力流量页面 - 支持流量表格与曲线双向定位及时间区间高亮 - 接入底层单位转换和显示精度配置 - 显式绑定井数据并保持时间变表皮窗口原有交互 - 优化编辑井页面间距和顶部菜单图标尺寸 - 同步调整储层特性顶部菜单图标尺寸 --- Bin/Config/Lang/cn/nmNum_cn.qm | Bin 109365 -> 110264 bytes Bin/Config/Lang/cn/nmNum_cn.ts | 20 +- .../nmNum/nmSubWxs/nmWellPressureFlowPage.h | 132 +++ .../nmNum/nmSubWxs/nmWxPressFlowChartWidget.h | 40 +- Include/nmNum/nmSubWxs/nmWxWellEditor.h | 44 +- Src/nmNum/nmSubWxs/nmWellPressureFlowPage.cpp | 772 ++++++++++++++++++ .../nmSubWxs/nmWxPressFlowChartWidget.cpp | 190 ++++- .../nmWxReservoirPropertiesEditor.cpp | 4 +- Src/nmNum/nmSubWxs/nmWxTimeDependentSkin.cpp | 8 +- Src/nmNum/nmSubWxs/nmWxWellEditor.cpp | 200 ++++- 10 files changed, 1376 insertions(+), 34 deletions(-) create mode 100644 Include/nmNum/nmSubWxs/nmWellPressureFlowPage.h create mode 100644 Src/nmNum/nmSubWxs/nmWellPressureFlowPage.cpp diff --git a/Bin/Config/Lang/cn/nmNum_cn.qm b/Bin/Config/Lang/cn/nmNum_cn.qm index 630ca98fc2ba1378c4a8248430a6ce65b18b63f0..00947bd178493d9745edf4e785a3417928ffa3a4 100644 GIT binary patch delta 8653 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 SlXT6A&LsxNiZ+o|%Krs>;ug06 delta 7936 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 L@>*?SzZm`pl`B0- diff --git a/Bin/Config/Lang/cn/nmNum_cn.ts b/Bin/Config/Lang/cn/nmNum_cn.ts index 7db0df8..d3fc99e 100644 --- a/Bin/Config/Lang/cn/nmNum_cn.ts +++ b/Bin/Config/Lang/cn/nmNum_cn.ts @@ -5944,8 +5944,8 @@ Average pressure in contour: %2 MPa 流量 - m3/d - + m^3/D + m^3/D Date Time @@ -6577,6 +6577,22 @@ Average pressure in contour: %2 MPa Perforation parameters射孔参数 Fracture parameters裂缝参数 Perforation name射孔名称 + Basic基础 + Data数据 + Well parameters井参数 + Pressure and flow压力流量 + + + nmWellPressureFlowPage + No pressure data无压力数据 + No flow data无流量数据 + No pressure or flow data无压力和流量数据 + Pressure压力 + Flow流量 + Time时间 + hh + MPaMPa + m³/dm³/d nmReservoirParameterPage diff --git a/Include/nmNum/nmSubWxs/nmWellPressureFlowPage.h b/Include/nmNum/nmSubWxs/nmWellPressureFlowPage.h new file mode 100644 index 0000000..c3c66ac --- /dev/null +++ b/Include/nmNum/nmSubWxs/nmWellPressureFlowPage.h @@ -0,0 +1,132 @@ +#pragma once + +#include "nmSubWxs_global.h" + +#include +#include +#include +#include + +class iUnitGroup; +class iUnitItem; +class nmDataWellBase; +class nmWxPressFlowChartWidget; +class QSplitter; +class QStackedWidget; +class QTableWidget; +class ZxTableHeaderViewUnit; + +/** + * @brief 显示当前井工作副本中的只读压力和流量数据。 + * @note 页面不拥有井对象,只保存以h、m³/d和MPa为基准的数据副本。 + */ +class NM_SUB_WXS_EXPORT nmWellPressureFlowPage : public QWidget +{ + Q_OBJECT + +public: + /** @brief 创建只读压力流量页面。 */ + explicit nmWellPressureFlowPage(QWidget* pParent = nullptr); + + /** + * @brief 绑定当前井工作副本,不立即读取数据。 + * @param pWell 井工作副本,不转移所有权。 + */ + void setWell(nmDataWellBase* pWell); + + /** @brief 从已绑定井重新读取原始数据并刷新全部视图。 */ + void refreshData(); + + /** @brief 解除井绑定并清空全部显示数据。 */ + void clearData(); + +private slots: + /** @brief 从原始副本重新生成指定单位的表格和曲线。 */ + void slotUnitChanged(int nColumn, + const iUnitItem* pSourceUnit, + const iUnitItem* pDestinationUnit); + + /** @brief 将表格当前行同步为两张曲线的高亮流量段。 */ + void slotFlowRowChanged(int nCurrentRow, + int nCurrentColumn, + int nPreviousRow, + int nPreviousColumn); + + /** @brief 将流量曲线点击时间映射为表格行选择。 */ + void slotFlowChartPointClicked(double dTimePoint); + +private: + /** @brief 创建左右分栏、上下图表和只读流量表格。 */ + void createUi(); + + /** @brief 重新获取框架单位组并同步双行表头。 */ + void initializeUnitSupport(); + + /** @brief 将表头恢复为当前已成功应用的单位。 */ + void updateUnitHeader(); + + /** @brief 由基准副本生成表格显示点和目标单位精度。 */ + bool buildFlowTableData( + const QString& sTimeUnit, + const QString& sFlowUnit, + QVector& vecFlowPoints, + int& nTimeDigit, + int& nFlowDigit) const; + + /** @brief 使用指定单位组转换一个值。 */ + bool convertValue(iUnitGroup* pUnitGroup, + const QString& sBaseUnit, + const QString& sDestinationUnit, + double dSourceValue, + double& dDestinationValue, + int* pDestinationDigit = nullptr) const; + + /** @brief 刷新表格、图表、空状态和选中段。 */ + void updateViews(); + + /** @brief 从原始流量持续时间生成基准单位阶梯曲线点。 */ + QVector createFlowChartPoints() const; + + /** @brief 刷新流量时序表并恢复有效选中行。 */ + void updateFlowTable(); + + /** @brief 按原始压力和流量数据的时间并集同步图表范围。 */ + void updateXAxisRange( + const QVector& vecFlowChartPoints); + + /** @brief 根据共享流量段索引刷新两张曲线高亮。 */ + void updateSelectedFlowSegment(); + + /** @brief 统一应用表格与曲线共享的零基流量点索引。 */ + void applyFlowSegmentSelection(int nSegmentIndex); + + /** @brief 按表格当前时间单位查找点击时间所属的流量点。 */ + int findFlowSegmentByTableTime(double dTimePoint) const; + + /** @brief 清空数据副本和视图,但保留当前单位及井绑定。 */ + void resetDataViews(); + + nmDataWellBase* m_pWell; ///< 当前工作副本,不拥有。 + QStackedWidget* m_pContentStack; + QSplitter* m_pHorizontalSplitter; + QSplitter* m_pChartSplitter; + QStackedWidget* m_pPressureChartStack; + QStackedWidget* m_pFlowChartStack; + QStackedWidget* m_pFlowTableStack; + nmWxPressFlowChartWidget* m_pPressureChart; + nmWxPressFlowChartWidget* m_pFlowChart; + QTableWidget* m_pFlowTable; + ZxTableHeaderViewUnit* m_pFlowHeader; + iUnitGroup* m_pTimeUnitGroup; ///< 框架拥有。 + iUnitGroup* m_pFlowUnitGroup; ///< 框架拥有。 + QVector m_vecRawPressurePoints; + QVector m_vecRawFlowPoints; + QVector m_vecTableFlowPoints; + QString m_sTimeBaseUnit; + QString m_sFlowBaseUnit; + QString m_sTimeUnit; + QString m_sFlowUnit; + int m_nTimeDigit; + int m_nFlowDigit; + int m_nSelectedFlowSegment; +}; diff --git a/Include/nmNum/nmSubWxs/nmWxPressFlowChartWidget.h b/Include/nmNum/nmSubWxs/nmWxPressFlowChartWidget.h index ed92300..bbb48ca 100644 --- a/Include/nmNum/nmSubWxs/nmWxPressFlowChartWidget.h +++ b/Include/nmNum/nmSubWxs/nmWxPressFlowChartWidget.h @@ -1,5 +1,4 @@ -#ifndef NMWXPRESSFLOWCHARTWIDGET_H -#define NMWXPRESSFLOWCHARTWIDGET_H +#pragma once #include #include @@ -89,6 +88,27 @@ class nmWxPressFlowChartWidget : public QWidget public: explicit nmWxPressFlowChartWidget(QWidget *parent = 0); + /** @brief 设置图表是否为只读模式。 */ + void setReadOnly(bool bReadOnly); + + /** @brief 设置是否允许点击绘图区发送时间选择信号。 */ + void setTimeSelectionEnabled(bool bEnabled); + + /** @brief 设置流动段编辑使用的井,不转移所有权。 */ + void setCurrentWellData(nmDataWellBase* pWellData); + + /** @brief 固定X轴显示范围,用于多图时间轴对齐。 */ + void setXAxisRange(double dMinimum, double dMaximum); + + /** @brief 恢复根据图表自身数据计算X轴范围。 */ + void clearXAxisRange(); + + /** @brief 高亮指定时间范围,零持续时间显示为竖线。 */ + void setHighlightedTimeRange(double dStartTime, double dEndTime); + + /** @brief 清除只读时间范围高亮。 */ + void clearHighlightedTimeRange(); + // 设置数据 void setData(const QVector& data); void setTitle(const QString& title); @@ -133,6 +153,7 @@ private: void drawLineData(QPainter& painter); // 绘制连续线条 void drawStepData(QPainter& painter); // 绘制阶梯图 void drawLabels(QPainter& painter); + void drawHighlightedTimeRange(QPainter& painter); void drawSelectedFlowSegments(QPainter& painter); int findBoundaryAtPosition(const QPoint& pos, double tolerance = 5.0) const; @@ -174,8 +195,17 @@ private: // 流动段数据副本 nmDataPerforation m_perforationData; - // 当前井数据 - nmDataWellBase* m_currentWell; + bool m_bReadOnly; + bool m_bTimeSelectionEnabled; + bool m_bFixedXAxisRange; + double m_dFixedMinimumX; + double m_dFixedMaximumX; + bool m_bHighlightTimeRange; + double m_dHighlightStartTime; + double m_dHighlightEndTime; + + // 当前井仅供旧窗口吸附流动段边界使用,不拥有所有权。 + nmDataWellBase* m_pCurrentWell; // 拖动相关的成员变量 bool m_isDraggingBoundary; // 是否正在拖动分界线 @@ -188,5 +218,3 @@ private: int m_externalDragBoundaryIndex; double m_externalDragBoundaryTime; }; - -#endif // NMWXPRESSFLOWCHARTWIDGET_H diff --git a/Include/nmNum/nmSubWxs/nmWxWellEditor.h b/Include/nmNum/nmSubWxs/nmWxWellEditor.h index b0a1463..fe0a7af 100644 --- a/Include/nmNum/nmSubWxs/nmWxWellEditor.h +++ b/Include/nmNum/nmSubWxs/nmWxWellEditor.h @@ -9,16 +9,28 @@ class nmDataWellBase; class nmWellEditorController; class nmWellEditorSession; class nmWellParameterPanel; +class nmWellPressureFlowPage; class nmWxWellboreTrajectoryDisplay; +class QButtonGroup; class QCheckBox; class QComboBox; class QDialogButtonBox; +class QFrame; +class QHBoxLayout; class QKeyEvent; class QLabel; class QLineEdit; class QSplitter; +class QStackedWidget; class QToolButton; +enum nmWellEditorPage +{ + NM_WELL_EDITOR_PAGE_PARAMETERS = 0, + NM_WELL_EDITOR_PAGE_PRESSURE_FLOW, + NM_WELL_EDITOR_PAGE_COUNT +}; + /** * @brief 数值试井井参数编辑器。 * @note 所有输入只修改会话副本,确认后由调用者接管最终副本。 @@ -76,6 +88,9 @@ protected: virtual void keyPressEvent(QKeyEvent* pEvent); private slots: + /** @brief 切换顶部井编辑页面。 */ + void slotCategoryButtonClicked(int nPage); + /** @brief 保存当前井型输入并切换到独立井型缓存。 */ void slotWellTypeChanged(int nIndex); @@ -98,9 +113,29 @@ private slots: void slotPanelValidationMessageChanged(const QString& sMessage); private: - /** @brief 创建左侧参数区、右侧轨迹区和底部命令区。 */ + /** @brief 创建顶部导航、页面堆栈和底部命令区。 */ void createUi(); + /** @brief 创建顶部分组导航。 */ + QWidget* createCategoryBar(); + + /** @brief 创建一个带底部分组标题的ribbon区域。 */ + QWidget* createRibbonGroup(const QString& sCaption, + QWidget* pParent, + QHBoxLayout*& pCommandLayout); + + /** @brief 创建顶部导航分组的竖向分隔线。 */ + QFrame* createRibbonSeparator(QWidget* pParent); + + /** @brief 创建一个页面切换命令按钮。 */ + QToolButton* createCategoryButton(int nPage, + const QString& sText, + const QString& sIconName, + QWidget* pParent); + + /** @brief 创建顶部帮助命令按钮。 */ + QToolButton* createHelpButton(QWidget* pParent); + /** @brief 创建固定显示的井号、井名、井型和时间变表皮信息区。 */ QWidget* createWellInformationWidget(QWidget* pParent); @@ -110,6 +145,9 @@ private: /** @brief 从当前工作副本回填时间变表皮控件。 */ void reloadTimeDependentSkinControls(nmDataWellBase* pWell); + /** @brief 从当前会话工作副本刷新只读压力流量页。 */ + void reloadPressureFlowPage(); + /** @brief 为当前井型工作副本重新创建轨迹交互控制器。 */ void createController(); @@ -127,6 +165,9 @@ private: void updateAcceptState(); nmWellEditorSession* m_pSession; ///< 对话框独占编辑会话。 + QButtonGroup* m_pCategoryButtonGroup; ///< 顶部页面命令与稳定页面ID映射。 + QStackedWidget* m_pStackedWidget; ///< 井参数和压力流量页面堆栈。 + nmWellPressureFlowPage* m_pPressureFlowPage; ///< 只读压力流量数据页。 nmWellParameterPanel* m_pParameterPanel; ///< 左侧单列参数分组。 nmWxWellboreTrajectoryDisplay* m_pTrajectoryDisplay; ///< 右侧轨迹视图。 nmWellEditorController* m_pWellEditorController; ///< 轨迹交互控制器。 @@ -139,6 +180,7 @@ private: QDialogButtonBox* m_pButtonBox; ///< 确定和取消按钮区。 QLabel* m_pStatusLabel; ///< 当前校验错误。 QSplitter* m_pMainSplitter; ///< 参数与轨迹分割器。 + int m_nCurrentPage; ///< 当前显示页面。 QString m_sPanelValidationMessage; ///< 参数面板错误。 bool m_bInitialized; ///< 编辑器是否初始化完成。 bool m_bChangingWellType; ///< 程序同步井型标记。 diff --git a/Src/nmNum/nmSubWxs/nmWellPressureFlowPage.cpp b/Src/nmNum/nmSubWxs/nmWellPressureFlowPage.cpp new file mode 100644 index 0000000..4512cbd --- /dev/null +++ b/Src/nmNum/nmSubWxs/nmWellPressureFlowPage.cpp @@ -0,0 +1,772 @@ +#include "nmWellPressureFlowPage.h" + +#include "iUnitGroup.h" +#include "iUnitHelper.h" +#include "iUnitItem.h" +#include "nmDataWellBase.h" +#include "nmWxPressFlowChartWidget.h" +#include "ZxBaseUtil.h" +#include "ZxTableHeaderViewUnit.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ +QString preferredTimeUnit() +{ + return QLatin1String("h"); +} + +QString preferredFlowUnit() +{ + return QString::fromUtf8("m\xC2\xB3/d"); +} + +iUnitGroup* resolveUnitGroup(const QStringList& listCandidates, + QString& sResolvedUnit) +{ + for (int nIndex = 0; nIndex < listCandidates.size(); ++nIndex) + { + const QString& sCandidate = listCandidates[nIndex]; + iUnitGroup* pUnitGroup = + iUnitHelper::getUnitGroupByUnit(sCandidate); + if (pUnitGroup == nullptr) + { + continue; + } + + int nUnitIndex = pUnitGroup->indexOf(sCandidate); + QStringList listUnitNames = pUnitGroup->getAllUnitNames(); + if (nUnitIndex < 0 || nUnitIndex >= listUnitNames.size()) + { + continue; + } + + sResolvedUnit = listUnitNames[nUnitIndex]; + return pUnitGroup; + } + + sResolvedUnit = listCandidates.isEmpty() + ? QString() : listCandidates.first(); + return nullptr; +} + +QLabel* createEmptyLabel(const QString& sText, QWidget* pParent) +{ + QLabel* pLabel = new QLabel(sText, pParent); + pLabel->setAlignment(Qt::AlignCenter); + QPalette oPalette = pLabel->palette(); + oPalette.setColor(QPalette::WindowText, + pParent->palette().color(QPalette::Mid)); + pLabel->setPalette(oPalette); + return pLabel; +} + +QString displayValueText(double dValue, int nDigit) +{ + if (nDigit <= 0) + { + return QString::number(dValue, 'f', 0); + } + return ZxBaseUtil::getValidStr(dValue, nDigit); +} +} + +nmWellPressureFlowPage::nmWellPressureFlowPage(QWidget* pParent) + : QWidget(pParent), + m_pWell(nullptr), + m_pContentStack(nullptr), + m_pHorizontalSplitter(nullptr), + m_pChartSplitter(nullptr), + m_pPressureChartStack(nullptr), + m_pFlowChartStack(nullptr), + m_pFlowTableStack(nullptr), + m_pPressureChart(nullptr), + m_pFlowChart(nullptr), + m_pFlowTable(nullptr), + m_pFlowHeader(nullptr), + m_pTimeUnitGroup(nullptr), + m_pFlowUnitGroup(nullptr), + m_sTimeBaseUnit(preferredTimeUnit()), + m_sFlowBaseUnit(preferredFlowUnit()), + m_sTimeUnit(preferredTimeUnit()), + m_sFlowUnit(preferredFlowUnit()), + m_nTimeDigit(6), + m_nFlowDigit(6), + m_nSelectedFlowSegment(-1) +{ + createUi(); + initializeUnitSupport(); + resetDataViews(); +} + +void nmWellPressureFlowPage::setWell(nmDataWellBase* pWell) +{ + if (m_pWell == pWell) + { + return; + } + + m_pWell = pWell; + resetDataViews(); +} + +void nmWellPressureFlowPage::refreshData() +{ + if (m_pWell == nullptr) + { + resetDataViews(); + return; + } + + // 第一步:井数据只复制到页面基准副本,后续显示换算不会回写井对象。 + QVector vecRawPressurePoints = + m_pWell->getPressurePoints(); + QVector vecRawFlowPoints = m_pWell->getFlowPoints(); + + // 第二步:单位服务可能晚于页面构造完成,再刷新一次可用单位组。 + initializeUnitSupport(); + + m_vecRawPressurePoints = vecRawPressurePoints; + m_vecRawFlowPoints = vecRawFlowPoints; + + QVector vecTableFlowPoints; + int nTimeDigit = m_nTimeDigit; + int nFlowDigit = m_nFlowDigit; + if (!buildFlowTableData(m_sTimeUnit, + m_sFlowUnit, + vecTableFlowPoints, + nTimeDigit, + nFlowDigit)) + { + // 当前显示单位不可用时回退基准单位,禁止继续展示旧井快照。 + m_sTimeUnit = m_sTimeBaseUnit; + m_sFlowUnit = m_sFlowBaseUnit; + if (!buildFlowTableData(m_sTimeUnit, + m_sFlowUnit, + vecTableFlowPoints, + nTimeDigit, + nFlowDigit)) + { + resetDataViews(); + updateUnitHeader(); + return; + } + } + + // 第三步:换算与精度读取全部成功后再提交,曲线始终读取原始数据。 + m_vecTableFlowPoints = vecTableFlowPoints; + m_nTimeDigit = nTimeDigit; + m_nFlowDigit = nFlowDigit; + updateUnitHeader(); + updateViews(); +} + +void nmWellPressureFlowPage::clearData() +{ + m_pWell = nullptr; + resetDataViews(); +} + +void nmWellPressureFlowPage::slotUnitChanged( + int nColumn, + const iUnitItem* pSourceUnit, + const iUnitItem* pDestinationUnit) +{ + Q_UNUSED(pSourceUnit); + if (pDestinationUnit == nullptr || + (nColumn != 0 && nColumn != 1)) + { + updateUnitHeader(); + return; + } + + QString sTimeUnit = m_sTimeUnit; + QString sFlowUnit = m_sFlowUnit; + if (nColumn == 0) + { + if (m_pTimeUnitGroup == nullptr) + { + updateUnitHeader(); + return; + } + sTimeUnit = pDestinationUnit->m_sName; + } + else + { + if (m_pFlowUnitGroup == nullptr) + { + updateUnitHeader(); + return; + } + sFlowUnit = pDestinationUnit->m_sName; + } + + // 每次都从基准副本换算,禁止使用当前显示值继续换算。 + QVector vecTableFlowPoints; + int nTimeDigit = m_nTimeDigit; + int nFlowDigit = m_nFlowDigit; + if (!buildFlowTableData(sTimeUnit, + sFlowUnit, + vecTableFlowPoints, + nTimeDigit, + nFlowDigit)) + { + updateUnitHeader(); + return; + } + + m_sTimeUnit = sTimeUnit; + m_sFlowUnit = sFlowUnit; + m_vecTableFlowPoints = vecTableFlowPoints; + m_nTimeDigit = nTimeDigit; + m_nFlowDigit = nFlowDigit; + updateUnitHeader(); + updateFlowTable(); +} + +void nmWellPressureFlowPage::slotFlowRowChanged( + int nCurrentRow, + int nCurrentColumn, + int nPreviousRow, + int nPreviousColumn) +{ + Q_UNUSED(nCurrentColumn); + Q_UNUSED(nPreviousRow); + Q_UNUSED(nPreviousColumn); + + applyFlowSegmentSelection(nCurrentRow); +} + +void nmWellPressureFlowPage::slotFlowChartPointClicked(double dTimePoint) +{ + // 曲线保持基准单位,点击时间先换算为表格当前单位再定位行。 + double dTableTimePoint = 0.0; + if (!convertValue(m_pTimeUnitGroup, + m_sTimeBaseUnit, + m_sTimeUnit, + dTimePoint, + dTableTimePoint)) + { + applyFlowSegmentSelection(-1); + return; + } + + applyFlowSegmentSelection( + findFlowSegmentByTableTime(dTableTimePoint)); +} + +void nmWellPressureFlowPage::createUi() +{ + QVBoxLayout* pMainLayout = new QVBoxLayout(this); + pMainLayout->setContentsMargins(8, 6, 8, 8); + pMainLayout->setSpacing(0); + + m_pContentStack = new QStackedWidget(this); + m_pHorizontalSplitter = new QSplitter( + Qt::Horizontal, m_pContentStack); + m_pHorizontalSplitter->setChildrenCollapsible(false); + + // 左侧上下两图共享时间范围,但各自保留独立空状态。 + m_pChartSplitter = new QSplitter( + Qt::Vertical, m_pHorizontalSplitter); + m_pChartSplitter->setChildrenCollapsible(false); + + m_pPressureChartStack = new QStackedWidget(m_pChartSplitter); + m_pPressureChart = new nmWxPressFlowChartWidget( + m_pPressureChartStack); + m_pPressureChart->setReadOnly(true); + m_pPressureChart->setTimeSelectionEnabled(false); + m_pPressureChart->setAxisLabels(tr("Time"), tr("Pressure")); + m_pPressureChart->setAxisUnits( + m_sTimeBaseUnit, tr("MPa")); + m_pPressureChart->setLineColor(QColor(50, 200, 50)); + m_pPressureChart->setChartType(CHART_TYPE_LINE); + m_pPressureChartStack->addWidget(m_pPressureChart); + m_pPressureChartStack->addWidget(createEmptyLabel( + tr("No pressure data"), m_pPressureChartStack)); + + m_pFlowChartStack = new QStackedWidget(m_pChartSplitter); + m_pFlowChart = new nmWxPressFlowChartWidget(m_pFlowChartStack); + m_pFlowChart->setReadOnly(true); + m_pFlowChart->setTimeSelectionEnabled(true); + m_pFlowChart->setAxisLabels(tr("Time"), tr("Flow")); + m_pFlowChart->setAxisUnits(m_sTimeBaseUnit, m_sFlowBaseUnit); + m_pFlowChart->setLineColor(QColor(255, 0, 0)); + m_pFlowChart->setChartType(CHART_TYPE_STEP); + m_pFlowChartStack->addWidget(m_pFlowChart); + m_pFlowChartStack->addWidget(createEmptyLabel( + tr("No flow data"), m_pFlowChartStack)); + + m_pChartSplitter->addWidget(m_pPressureChartStack); + m_pChartSplitter->addWidget(m_pFlowChartStack); + m_pChartSplitter->setStretchFactor(0, 1); + m_pChartSplitter->setStretchFactor(1, 1); + + // 右侧仅显示原始流量段对应的时间和流量,不提供任何编辑入口。 + m_pFlowTableStack = new QStackedWidget(m_pHorizontalSplitter); + m_pFlowTable = new QTableWidget(m_pFlowTableStack); + m_pFlowTable->setColumnCount(2); + m_pFlowTable->setEditTriggers(QAbstractItemView::NoEditTriggers); + m_pFlowTable->setSelectionBehavior(QAbstractItemView::SelectRows); + m_pFlowTable->setSelectionMode(QAbstractItemView::SingleSelection); + m_pFlowTable->setSortingEnabled(false); + m_pFlowTable->setDragEnabled(false); + m_pFlowTable->setAcceptDrops(false); + m_pFlowTable->setDropIndicatorShown(false); + m_pFlowTable->setAlternatingRowColors(true); + m_pFlowTable->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_pFlowHeader = new ZxTableHeaderViewUnit(m_pFlowTable); + m_pFlowTable->setHorizontalHeader(m_pFlowHeader); + m_pFlowTable->horizontalHeader()->setMovable(false); + m_pFlowTable->horizontalHeader()->setResizeMode(QHeaderView::Stretch); + m_pFlowTable->verticalHeader()->setMovable(false); + m_pFlowTable->verticalHeader()->setDefaultAlignment(Qt::AlignCenter); + m_pFlowTable->verticalHeader()->setMinimumWidth(18); + + m_pFlowTableStack->addWidget(m_pFlowTable); + m_pFlowTableStack->addWidget(createEmptyLabel( + tr("No flow data"), m_pFlowTableStack)); + + m_pHorizontalSplitter->addWidget(m_pChartSplitter); + m_pHorizontalSplitter->addWidget(m_pFlowTableStack); + m_pHorizontalSplitter->setStretchFactor(0, 7); + m_pHorizontalSplitter->setStretchFactor(1, 3); + QList listHorizontalSizes; + listHorizontalSizes << 700 << 300; + m_pHorizontalSplitter->setSizes(listHorizontalSizes); + + m_pContentStack->addWidget(m_pHorizontalSplitter); + m_pContentStack->addWidget(createEmptyLabel( + tr("No pressure or flow data"), m_pContentStack)); + pMainLayout->addWidget(m_pContentStack, 1); + + connect(m_pFlowHeader, + SIGNAL(sigCbxValueChanged(int,const iUnitItem*,const iUnitItem*)), + this, + SLOT(slotUnitChanged(int,const iUnitItem*,const iUnitItem*))); + connect(m_pFlowTable, + SIGNAL(currentCellChanged(int,int,int,int)), + this, + SLOT(slotFlowRowChanged(int,int,int,int))); + connect(m_pFlowChart, + SIGNAL(chartPointClicked(double)), + this, + SLOT(slotFlowChartPointClicked(double))); +} + +void nmWellPressureFlowPage::initializeUnitSupport() +{ + QStringList listTimeCandidates; + listTimeCandidates << preferredTimeUnit() << QLatin1String("hr"); + QStringList listFlowCandidates; + listFlowCandidates << preferredFlowUnit() + << QLatin1String("m^3/d") + << QLatin1String("m^3/D"); + + // 优先使用当前单位包的h、m³/d;旧英文单位包自动兼容hr、m^3/d。 + m_pTimeUnitGroup = resolveUnitGroup( + listTimeCandidates, m_sTimeBaseUnit); + m_pFlowUnitGroup = resolveUnitGroup( + listFlowCandidates, m_sFlowBaseUnit); + + // 单位组缺失或当前单位不再存在时回退基准单位,入口由框架表头禁用。 + if (m_pTimeUnitGroup == nullptr || + m_pTimeUnitGroup->indexOf(m_sTimeUnit) < 0) + { + m_sTimeUnit = m_sTimeBaseUnit; + } + if (m_pFlowUnitGroup == nullptr || + m_pFlowUnitGroup->indexOf(m_sFlowUnit) < 0) + { + m_sFlowUnit = m_sFlowBaseUnit; + } + updateUnitHeader(); +} + +void nmWellPressureFlowPage::updateUnitHeader() +{ + if (m_pFlowHeader == nullptr) + { + return; + } + + QStringList listTitles; + listTitles << tr("Time") << tr("Flow"); + QStringList listUnits; + listUnits << m_sTimeUnit << m_sFlowUnit; + m_pFlowHeader->setHeaderData(listTitles, listUnits); +} + +bool nmWellPressureFlowPage::buildFlowTableData( + const QString& sTimeUnit, + const QString& sFlowUnit, + QVector& vecFlowPoints, + int& nTimeDigit, + int& nFlowDigit) const +{ + vecFlowPoints = m_vecRawFlowPoints; + + // 空数据也必须验证目标单位,避免表头切换后留下不可用单位状态。 + double dConvertedZero = 0.0; + if (!convertValue(m_pTimeUnitGroup, + m_sTimeBaseUnit, + sTimeUnit, + 0.0, + dConvertedZero, + &nTimeDigit) || + !convertValue(m_pFlowUnitGroup, + m_sFlowBaseUnit, + sFlowUnit, + 0.0, + dConvertedZero, + &nFlowDigit)) + { + return false; + } + + // 表格每次从原始副本换算,禁止使用当前显示值继续换算。 + for (int nIndex = 0; nIndex < vecFlowPoints.size(); ++nIndex) + { + double dDuration = 0.0; + double dFlow = 0.0; + if (!convertValue(m_pTimeUnitGroup, + m_sTimeBaseUnit, + sTimeUnit, + m_vecRawFlowPoints[nIndex].x(), + dDuration) || + !convertValue(m_pFlowUnitGroup, + m_sFlowBaseUnit, + sFlowUnit, + m_vecRawFlowPoints[nIndex].y(), + dFlow)) + { + return false; + } + vecFlowPoints[nIndex] = QPointF(dDuration, dFlow); + } + return true; +} + +bool nmWellPressureFlowPage::convertValue( + iUnitGroup* pUnitGroup, + const QString& sBaseUnit, + const QString& sDestinationUnit, + double dSourceValue, + double& dDestinationValue, + int* pDestinationDigit) const +{ + int nDigit = 6; + if (pUnitGroup != nullptr) + { + iUnitItem* pUnitItem = + pUnitGroup->getUnitItem(sDestinationUnit); + if (pUnitItem != nullptr && pUnitItem->m_nDigit >= 0) + { + nDigit = pUnitItem->m_nDigit; + } + } + + if (sDestinationUnit == sBaseUnit) + { + dDestinationValue = dSourceValue; + if (pDestinationDigit != nullptr) + { + *pDestinationDigit = nDigit; + } + return true; + } + if (pUnitGroup == nullptr || + pUnitGroup->indexOf(sBaseUnit) < 0 || + pUnitGroup->indexOf(sDestinationUnit) < 0) + { + return false; + } + + int nFallbackDigit = nDigit; + bool bConverted = pUnitGroup->convert(sBaseUnit, + dSourceValue, + sDestinationUnit, + dDestinationValue, + nDigit); + if (bConverted && pDestinationDigit != nullptr) + { + *pDestinationDigit = nDigit >= 0 + ? nDigit : nFallbackDigit; + } + return bConverted; +} + +void nmWellPressureFlowPage::updateViews() +{ + QVector vecFlowChartPoints = createFlowChartPoints(); + m_pPressureChart->setAxisUnits( + m_sTimeBaseUnit, tr("MPa")); + m_pFlowChart->setAxisUnits(m_sTimeBaseUnit, m_sFlowBaseUnit); + m_pPressureChart->setData(m_vecRawPressurePoints); + m_pFlowChart->setData(vecFlowChartPoints); + updateXAxisRange(vecFlowChartPoints); + updateFlowTable(); + + bool bPressureEmpty = m_vecRawPressurePoints.isEmpty(); + bool bFlowEmpty = m_vecRawFlowPoints.isEmpty(); + m_pPressureChartStack->setCurrentIndex(bPressureEmpty ? 1 : 0); + m_pFlowChartStack->setCurrentIndex(bFlowEmpty ? 1 : 0); + m_pFlowTableStack->setCurrentIndex(bFlowEmpty ? 1 : 0); + m_pContentStack->setCurrentIndex( + bPressureEmpty && bFlowEmpty ? 1 : 0); + updateSelectedFlowSegment(); +} + +QVector nmWellPressureFlowPage::createFlowChartPoints() const +{ + QVector vecChartPoints; + double dTotalTime = 0.0; + for (int nIndex = 0; + nIndex < m_vecRawFlowPoints.size(); + ++nIndex) + { + const QPointF& oSegment = m_vecRawFlowPoints[nIndex]; + if (nIndex == 0) + { + vecChartPoints.append(QPointF(0.0, oSegment.y())); + } + + dTotalTime += oSegment.x(); + vecChartPoints.append(QPointF(dTotalTime, oSegment.y())); + if (nIndex < m_vecRawFlowPoints.size() - 1 && + qAbs(m_vecRawFlowPoints[nIndex + 1].y() - + oSegment.y()) > 1e-6) + { + vecChartPoints.append(QPointF( + dTotalTime, + m_vecRawFlowPoints[nIndex + 1].y())); + } + } + return vecChartPoints; +} + +void nmWellPressureFlowPage::updateFlowTable() +{ + bool bSignalsWereBlocked = m_pFlowTable->blockSignals(true); + m_pFlowTable->clearContents(); + m_pFlowTable->setRowCount(m_vecTableFlowPoints.size()); + + // 纵向表头显示Qt默认的1~N序号,内部仍使用零基流量点索引。 + for (int nIndex = 0; + nIndex < m_vecTableFlowPoints.size(); + ++nIndex) + { + QTableWidgetItem* pTimeItem = new QTableWidgetItem( + displayValueText(m_vecTableFlowPoints[nIndex].x(), + m_nTimeDigit)); + pTimeItem->setTextAlignment(Qt::AlignCenter); + m_pFlowTable->setItem(nIndex, 0, pTimeItem); + + QTableWidgetItem* pFlowItem = new QTableWidgetItem( + displayValueText(m_vecTableFlowPoints[nIndex].y(), + m_nFlowDigit)); + pFlowItem->setTextAlignment(Qt::AlignCenter); + m_pFlowTable->setItem(nIndex, 1, pFlowItem); + } + + if (m_nSelectedFlowSegment >= 0 && + m_nSelectedFlowSegment < m_pFlowTable->rowCount()) + { + m_pFlowTable->setCurrentCell(m_nSelectedFlowSegment, 0); + m_pFlowTable->selectRow(m_nSelectedFlowSegment); + } + else + { + m_nSelectedFlowSegment = -1; + m_pFlowTable->setCurrentCell(-1, -1); + m_pFlowTable->clearSelection(); + } + m_pFlowTable->blockSignals(bSignalsWereBlocked); +} + +void nmWellPressureFlowPage::updateXAxisRange( + const QVector& vecFlowChartPoints) +{ + bool bHasRange = false; + double dMinimum = 0.0; + double dMaximum = 0.0; + + for (int nIndex = 0; + nIndex < m_vecRawPressurePoints.size(); + ++nIndex) + { + double dTime = m_vecRawPressurePoints[nIndex].x(); + dMinimum = bHasRange ? qMin(dMinimum, dTime) : dTime; + dMaximum = bHasRange ? qMax(dMaximum, dTime) : dTime; + bHasRange = true; + } + for (int nIndex = 0; nIndex < vecFlowChartPoints.size(); ++nIndex) + { + double dTime = vecFlowChartPoints[nIndex].x(); + dMinimum = bHasRange ? qMin(dMinimum, dTime) : dTime; + dMaximum = bHasRange ? qMax(dMaximum, dTime) : dTime; + bHasRange = true; + } + + if (bHasRange) + { + m_pPressureChart->setXAxisRange(dMinimum, dMaximum); + m_pFlowChart->setXAxisRange(dMinimum, dMaximum); + } + else + { + m_pPressureChart->clearXAxisRange(); + m_pFlowChart->clearXAxisRange(); + } +} + +void nmWellPressureFlowPage::updateSelectedFlowSegment() +{ + if (m_nSelectedFlowSegment < 0 || + m_nSelectedFlowSegment >= m_vecRawFlowPoints.size()) + { + m_pPressureChart->clearHighlightedTimeRange(); + m_pFlowChart->clearHighlightedTimeRange(); + return; + } + + // 流量点X值是持续时间,段区间必须从第0段开始逐段累加。 + double dStartTime = 0.0; + for (int nIndex = 0; + nIndex < m_nSelectedFlowSegment; + ++nIndex) + { + dStartTime += m_vecRawFlowPoints[nIndex].x(); + } + double dEndTime = dStartTime + + m_vecRawFlowPoints[m_nSelectedFlowSegment].x(); + + m_pPressureChart->setHighlightedTimeRange( + dStartTime, dEndTime); + m_pFlowChart->setHighlightedTimeRange(dStartTime, dEndTime); +} + +void nmWellPressureFlowPage::applyFlowSegmentSelection(int nSegmentIndex) +{ + if (nSegmentIndex < 0 || + nSegmentIndex >= m_vecRawFlowPoints.size() || + nSegmentIndex >= m_pFlowTable->rowCount()) + { + nSegmentIndex = -1; + } + + m_nSelectedFlowSegment = nSegmentIndex; + bool bSignalsWereBlocked = m_pFlowTable->blockSignals(true); + if (m_nSelectedFlowSegment >= 0) + { + m_pFlowTable->setCurrentCell(m_nSelectedFlowSegment, 0); + m_pFlowTable->selectRow(m_nSelectedFlowSegment); + QTableWidgetItem* pItem = m_pFlowTable->item( + m_nSelectedFlowSegment, 0); + if (pItem != nullptr) + { + m_pFlowTable->scrollToItem( + pItem, QAbstractItemView::EnsureVisible); + } + } + else + { + m_pFlowTable->setCurrentCell(-1, -1); + m_pFlowTable->clearSelection(); + } + m_pFlowTable->blockSignals(bSignalsWereBlocked); + updateSelectedFlowSegment(); +} + +int nmWellPressureFlowPage::findFlowSegmentByTableTime( + double dTimePoint) const +{ + double dTotalTime = 0.0; + for (int nIndex = 0; + nIndex < m_vecTableFlowPoints.size(); + ++nIndex) + { + dTotalTime += qMax(0.0, m_vecTableFlowPoints[nIndex].x()); + } + + const double dTolerance = qMax(1.0, qAbs(dTotalTime)) * 1e-9; + if (dTimePoint < 0.0 || + dTimePoint > dTotalTime + dTolerance) + { + return -1; + } + + // 零时长点不占区间;分界点继续查找后一有效段。 + double dStartTime = 0.0; + int nLastValidSegment = -1; + for (int nIndex = 0; + nIndex < m_vecTableFlowPoints.size(); + ++nIndex) + { + double dDuration = m_vecTableFlowPoints[nIndex].x(); + if (dDuration <= dTolerance) + { + continue; + } + + nLastValidSegment = nIndex; + double dEndTime = dStartTime + dDuration; + if (dTimePoint < dEndTime - dTolerance) + { + return nIndex; + } + dStartTime = dEndTime; + } + + // 最终结束点没有后一段,归入最后一个有效流量段。 + if (nLastValidSegment >= 0 && + qAbs(dTimePoint - dTotalTime) <= dTolerance) + { + return nLastValidSegment; + } + return -1; +} + +void nmWellPressureFlowPage::resetDataViews() +{ + m_vecRawPressurePoints.clear(); + m_vecRawFlowPoints.clear(); + m_vecTableFlowPoints.clear(); + m_nSelectedFlowSegment = -1; + updateViews(); +} diff --git a/Src/nmNum/nmSubWxs/nmWxPressFlowChartWidget.cpp b/Src/nmNum/nmSubWxs/nmWxPressFlowChartWidget.cpp index 897f5ac..bdfae42 100644 --- a/Src/nmNum/nmSubWxs/nmWxPressFlowChartWidget.cpp +++ b/Src/nmNum/nmSubWxs/nmWxPressFlowChartWidget.cpp @@ -1,6 +1,4 @@ -#include "nmWxPressFlowChartWidget.h" - -#include "nmDataAnalyzeManager.h" +#include "nmWxPressFlowChartWidget.h" #include #include @@ -15,11 +13,20 @@ nmWxPressFlowChartWidget::nmWxPressFlowChartWidget(QWidget *parent) , m_chartType(CHART_TYPE_LINE) // 默认为连续线条 , m_minX(0), m_maxX(1) , m_minY(0), m_maxY(1) - , m_marginLeft(45) + , m_marginLeft(52) , m_marginRight(10) , m_marginTop(2) , m_marginBottom(40) , m_showXAxisLabels(true) // 默认显示X轴标签 + , m_bReadOnly(false) + , m_bTimeSelectionEnabled(true) + , m_bFixedXAxisRange(false) + , m_dFixedMinimumX(0.0) + , m_dFixedMaximumX(1.0) + , m_bHighlightTimeRange(false) + , m_dHighlightStartTime(0.0) + , m_dHighlightEndTime(0.0) + , m_pCurrentWell(nullptr) , m_isDraggingBoundary(false) , m_draggingBoundaryIndex(-1) , m_draggingBoundaryTime(0.0) @@ -28,12 +35,83 @@ nmWxPressFlowChartWidget::nmWxPressFlowChartWidget(QWidget *parent) , m_externalDragBoundaryIndex(-1) , m_externalDragBoundaryTime(0.0) { - m_currentWell = nmDataAnalyzeManager::getCurrentInstance()->getCurWellData(); setMinimumSize(300, 200); setMouseTracking(true); setFocusPolicy(Qt::StrongFocus); } +void nmWxPressFlowChartWidget::setReadOnly(bool bReadOnly) +{ + m_bReadOnly = bReadOnly; + if (m_bReadOnly) { + m_isDraggingBoundary = false; + m_draggingBoundaryIndex = -1; + m_draggingBoundaryTime = 0.0; + m_externalDragActive = false; + m_externalDragBoundaryIndex = -1; + m_externalDragBoundaryTime = 0.0; + setCursor(Qt::ArrowCursor); + } + update(); +} + +void nmWxPressFlowChartWidget::setTimeSelectionEnabled(bool bEnabled) +{ + m_bTimeSelectionEnabled = bEnabled; +} + +void nmWxPressFlowChartWidget::setCurrentWellData( + nmDataWellBase* pWellData) +{ + m_pCurrentWell = pWellData; +} + +void nmWxPressFlowChartWidget::setXAxisRange(double dMinimum, + double dMaximum) +{ + if (dMaximum < dMinimum) { + qSwap(dMinimum, dMaximum); + } + if (qAbs(dMaximum - dMinimum) < 1e-6) { + dMinimum -= 0.5; + dMaximum += 0.5; + } + + m_bFixedXAxisRange = true; + m_dFixedMinimumX = dMinimum; + m_dFixedMaximumX = dMaximum; + calculateDataRange(); + update(); +} + +void nmWxPressFlowChartWidget::clearXAxisRange() +{ + m_bFixedXAxisRange = false; + calculateDataRange(); + update(); +} + +void nmWxPressFlowChartWidget::setHighlightedTimeRange( + double dStartTime, + double dEndTime) +{ + if (dEndTime < dStartTime) { + qSwap(dStartTime, dEndTime); + } + m_bHighlightTimeRange = true; + m_dHighlightStartTime = dStartTime; + m_dHighlightEndTime = dEndTime; + update(); +} + +void nmWxPressFlowChartWidget::clearHighlightedTimeRange() +{ + m_bHighlightTimeRange = false; + m_dHighlightStartTime = 0.0; + m_dHighlightEndTime = 0.0; + update(); +} + void nmWxPressFlowChartWidget::setData(const QVector& data) { m_data = data; @@ -82,6 +160,7 @@ void nmWxPressFlowChartWidget::setShowXAxisLabels(bool show) void nmWxPressFlowChartWidget::clearData() { m_data.clear(); + calculateDataRange(); update(); } @@ -97,8 +176,11 @@ void nmWxPressFlowChartWidget::paintEvent(QPaintEvent *event) drawBackground(painter); drawGrid(painter); + drawHighlightedTimeRange(painter); drawAxes(painter); - drawSelectedFlowSegments(painter); // 绘制选中的流动段 + if (!m_bReadOnly) { + drawSelectedFlowSegments(painter); // 绘制选中的流动段 + } drawData(painter); drawLabels(painter); } @@ -109,6 +191,44 @@ void nmWxPressFlowChartWidget::drawBackground(QPainter& painter) painter.fillRect(m_plotArea, Qt::white); } +void nmWxPressFlowChartWidget::drawHighlightedTimeRange( + QPainter& painter) +{ + const double dXRange = m_maxX - m_minX; + if (!m_bHighlightTimeRange || dXRange <= 0.0 || + m_plotArea.width() <= 0 || m_plotArea.height() <= 0) { + return; + } + + double dStartTime = qMax(m_minX, + qMin(m_maxX, m_dHighlightStartTime)); + double dEndTime = qMax(m_minX, + qMin(m_maxX, m_dHighlightEndTime)); + double dStartX = m_plotArea.left() + + (dStartTime - m_minX) / dXRange * m_plotArea.width(); + double dEndX = m_plotArea.left() + + (dEndTime - m_minX) / dXRange * m_plotArea.width(); + + painter.save(); + QColor oBorderColor(45, 105, 175, 190); + if (qAbs(dEndX - dStartX) < 1.0) { + painter.setPen(QPen(oBorderColor, 2)); + painter.drawLine(QPointF(dStartX, m_plotArea.top()), + QPointF(dStartX, m_plotArea.bottom())); + } else { + QRectF oHighlightRect( + QPointF(qMin(dStartX, dEndX), m_plotArea.top()), + QPointF(qMax(dStartX, dEndX), m_plotArea.bottom())); + painter.fillRect(oHighlightRect, QColor(70, 135, 200, 45)); + painter.setPen(QPen(oBorderColor, 1)); + painter.drawLine(oHighlightRect.topLeft(), + oHighlightRect.bottomLeft()); + painter.drawLine(oHighlightRect.topRight(), + oHighlightRect.bottomRight()); + } + painter.restore(); +} + double nmWxPressFlowChartWidget::calculateTickInterval(double range, bool isYAxisForPressure) { double roughInterval = range / 4.0; // 目标是5个主刻度 @@ -315,7 +435,9 @@ void nmWxPressFlowChartWidget::drawData(QPainter& painter) void nmWxPressFlowChartWidget::drawLineData(QPainter& painter) { - if (m_data.size() < 2) return; + if (m_data.isEmpty()) { + return; + } // 绘制连接线 painter.setPen(QPen(m_lineColor, 2)); @@ -481,7 +603,8 @@ void nmWxPressFlowChartWidget::drawSelectedFlowSegments(QPainter& painter) void nmWxPressFlowChartWidget::calculateDataRange() { if (m_data.isEmpty()) { - m_minX = 0; m_maxX = 1; + m_minX = m_bFixedXAxisRange ? m_dFixedMinimumX : 0.0; + m_maxX = m_bFixedXAxisRange ? m_dFixedMaximumX : 1.0; m_minY = 0; m_maxY = 1; return; } @@ -496,12 +619,18 @@ void nmWxPressFlowChartWidget::calculateDataRange() m_maxY = qMax(m_maxY, point.y()); } - // 添加一些边距 - double xMargin = (m_maxX - m_minX) * 0.02; + // 固定范围由调用方统一决定,默认模式继续保留原有横向边距。 + double xMargin = m_bFixedXAxisRange + ? 0.0 : (m_maxX - m_minX) * 0.02; double yMargin = (m_maxY - m_minY) * 0.1; - m_minX -= xMargin; - m_maxX += xMargin; + if (m_bFixedXAxisRange) { + m_minX = m_dFixedMinimumX; + m_maxX = m_dFixedMaximumX; + } else { + m_minX -= xMargin; + m_maxX += xMargin; + } m_minY -= yMargin; m_maxY += yMargin; @@ -534,6 +663,12 @@ QPointF nmWxPressFlowChartWidget::screenToData(const QPointF& screenPoint) void nmWxPressFlowChartWidget::mouseMoveEvent(QMouseEvent *event) { + if (m_bReadOnly) { + setCursor(Qt::ArrowCursor); + QWidget::mouseMoveEvent(event); + return; + } + if(m_isDraggingBoundary && m_draggingBoundaryIndex >= 0) { QPointF dataPoint = screenToData(event->pos()); double newTime = dataPoint.x(); @@ -589,6 +724,12 @@ void nmWxPressFlowChartWidget::mouseMoveEvent(QMouseEvent *event) void nmWxPressFlowChartWidget::mouseReleaseEvent(QMouseEvent *event) { + if (m_bReadOnly) { + setCursor(Qt::ArrowCursor); + QWidget::mouseReleaseEvent(event); + return; + } + if(m_isDraggingBoundary && event->button() == Qt::LeftButton) { m_isDraggingBoundary = false; setCursor(Qt::ArrowCursor); @@ -665,11 +806,11 @@ bool nmWxPressFlowChartWidget::isBoundaryDraggable(int boundaryIndex) const // 将时间吸附到最近的流量阶梯边界 double nmWxPressFlowChartWidget::snapToNearestFlowStep(double time) const { - if(!m_snapToRateChanges) { + if(!m_snapToRateChanges || m_pCurrentWell == nullptr) { return time; // 未勾选对齐,直接返回 } - return m_currentWell->findSmartFlowStepBoundary(time); + return m_pCurrentWell->findSmartFlowStepBoundary(time); } // 检查是否应该合并段 @@ -706,6 +847,19 @@ bool nmWxPressFlowChartWidget::shouldMergeSegments(double newBoundaryTime, int b void nmWxPressFlowChartWidget::mousePressEvent(QMouseEvent *event) { + if (m_bReadOnly) { + setCursor(Qt::ArrowCursor); + if (m_bTimeSelectionEnabled && + event->button() == Qt::LeftButton && + m_plotArea.contains(event->pos())) { + const QPointF dataPoint = screenToData(event->pos()); + emit chartPointClicked(dataPoint.x()); + return; + } + QWidget::mousePressEvent(event); + return; + } + if (event->button() == Qt::LeftButton && m_plotArea.contains(event->pos())) { // 首先检查是否点击在分界线附近 int boundaryIndex = findBoundaryAtPosition(event->pos()); @@ -729,7 +883,9 @@ void nmWxPressFlowChartWidget::mousePressEvent(QMouseEvent *event) QPointF dataPoint = screenToData(event->pos()); double clickTime = dataPoint.x(); - emit chartPointClicked(clickTime); + if (m_bTimeSelectionEnabled) { + emit chartPointClicked(clickTime); + } //int segmentIndex = nmDataAnalyzeManager::getCurrentInstance()->findFlowSegmentByTime(clickTime); int segmentIndex = m_perforationData.findFlowSegmentByTime(clickTime); @@ -759,6 +915,10 @@ void nmWxPressFlowChartWidget::setSnapToRateChanges(bool snap) { void nmWxPressFlowChartWidget::setExternalDragState(bool isDragging, int boundaryIndex, double boundaryTime) { + if (m_bReadOnly) { + return; + } + m_externalDragActive = isDragging; m_externalDragBoundaryIndex = boundaryIndex; m_externalDragBoundaryTime = boundaryTime; diff --git a/Src/nmNum/nmSubWxs/nmWxReservoirPropertiesEditor.cpp b/Src/nmNum/nmSubWxs/nmWxReservoirPropertiesEditor.cpp index fe0bd13..899d14d 100644 --- a/Src/nmNum/nmSubWxs/nmWxReservoirPropertiesEditor.cpp +++ b/Src/nmNum/nmSubWxs/nmWxReservoirPropertiesEditor.cpp @@ -251,7 +251,7 @@ QToolButton* nmWxReservoirPropertiesEditor::createCategoryButton( QToolButton* pButton = new QToolButton(pParent); pButton->setText(sText); pButton->setIcon(zxLoadIcon(sIconName)); - pButton->setIconSize(QSize(25, 25)); + pButton->setIconSize(QSize(28, 28)); pButton->setToolButtonStyle(Qt::ToolButtonTextUnderIcon); // 分类按钮仅执行页面切换命令,不保留持续选中状态。 pButton->setCheckable(false); @@ -267,7 +267,7 @@ QToolButton* nmWxReservoirPropertiesEditor::createHelpButton( QToolButton* pButton = new QToolButton(pParent); pButton->setText(tr("Help")); pButton->setIcon(zxLoadIcon("Help")); - pButton->setIconSize(QSize(25, 25)); + pButton->setIconSize(QSize(28, 28)); pButton->setToolButtonStyle(Qt::ToolButtonTextUnderIcon); pButton->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); connect(pButton, SIGNAL(clicked()), this, SLOT(slotHelp())); diff --git a/Src/nmNum/nmSubWxs/nmWxTimeDependentSkin.cpp b/Src/nmNum/nmSubWxs/nmWxTimeDependentSkin.cpp index e76dc8f..bc54d9b 100644 --- a/Src/nmNum/nmSubWxs/nmWxTimeDependentSkin.cpp +++ b/Src/nmNum/nmSubWxs/nmWxTimeDependentSkin.cpp @@ -1,4 +1,4 @@ -#include "nmWxTimeDependentSkin.h" +#include "nmWxTimeDependentSkin.h" #include "nmWxPerforationClosing.h" #include "nmWxPressFlowChartWidget.h" #include "nmWxChartWidget.h" @@ -238,6 +238,8 @@ void nmWxTimeDependentSkin::reloadFromDataSource(nmDataWellBase* 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(); @@ -359,6 +361,7 @@ void nmWxTimeDependentSkin::initUI() // 创建压力图表 m_pPressureChart = new nmWxPressFlowChartWidget(m_pChartSplitter); + m_pPressureChart->setCurrentWellData(m_currentWell); m_pPressureChart->setAxisLabels(tr(""), tr("Pressure")); m_pPressureChart->setAxisUnits(tr("hr"), tr("MPa")); m_pPressureChart->setLineColor(QColor(50, 200, 50)); // 绿色 @@ -368,8 +371,9 @@ void nmWxTimeDependentSkin::initUI() // 创建流量图表 m_pFlowRateChart = new nmWxPressFlowChartWidget(m_pChartSplitter); + m_pFlowRateChart->setCurrentWellData(m_currentWell); m_pFlowRateChart->setAxisLabels(tr("Time"), tr("Flow")); - m_pFlowRateChart->setAxisUnits(tr("hr"), tr("m3/d")); + m_pFlowRateChart->setAxisUnits(tr("hr"), tr("m^3/D")); m_pFlowRateChart->setLineColor(QColor(255, 0, 0)); // 红色 m_pFlowRateChart->setChartType(CHART_TYPE_STEP); m_pFlowRateChart->setMinimumHeight(150); // 设置最小高度防止过度压缩 diff --git a/Src/nmNum/nmSubWxs/nmWxWellEditor.cpp b/Src/nmNum/nmSubWxs/nmWxWellEditor.cpp index 123bd96..d4705a6 100644 --- a/Src/nmNum/nmSubWxs/nmWxWellEditor.cpp +++ b/Src/nmNum/nmSubWxs/nmWxWellEditor.cpp @@ -9,15 +9,18 @@ #include "nmWellEditorController.h" #include "nmWellEditorSession.h" #include "nmWellParameterPanel.h" +#include "nmWellPressureFlowPage.h" #include "nmWxTimeDependentSkin.h" #include "nmWxWellboreTrajectoryDisplay.h" #include "ZxBaseUtil.h" #include +#include #include #include #include #include +#include #include #include #include @@ -31,6 +34,7 @@ #include #include #include +#include #include #include #include @@ -39,6 +43,9 @@ nmWxWellEditor::nmWxWellEditor(const nmWellEditContext& oContext, QWidget* pParent) : iDlgBase(pParent), m_pSession(new nmWellEditorSession(oContext)), + m_pCategoryButtonGroup(nullptr), + m_pStackedWidget(nullptr), + m_pPressureFlowPage(nullptr), m_pParameterPanel(nullptr), m_pTrajectoryDisplay(nullptr), m_pWellEditorController(nullptr), @@ -51,6 +58,7 @@ nmWxWellEditor::nmWxWellEditor(const nmWellEditContext& oContext, m_pButtonBox(nullptr), m_pStatusLabel(nullptr), m_pMainSplitter(nullptr), + m_nCurrentPage(NM_WELL_EDITOR_PAGE_PARAMETERS), m_sPanelValidationMessage(), m_bInitialized(false), m_bChangingWellType(false) @@ -69,7 +77,9 @@ nmWxWellEditor::~nmWxWellEditor() // 界面和控制器仍引用工作副本,必须在会话释放井对象前销毁。 delete m_pWellEditorController; m_pWellEditorController = nullptr; - delete m_pMainSplitter; + delete m_pStackedWidget; + m_pStackedWidget = nullptr; + m_pPressureFlowPage = nullptr; m_pMainSplitter = nullptr; m_pParameterPanel = nullptr; m_pTrajectoryDisplay = nullptr; @@ -111,6 +121,10 @@ bool nmWxWellEditor::initialize(QString& sError) createController(); m_bInitialized = true; + m_pPressureFlowPage->setWell(pWell); + m_pStackedWidget->setCurrentIndex( + NM_WELL_EDITOR_PAGE_PARAMETERS); + m_nCurrentPage = NM_WELL_EDITOR_PAGE_PARAMETERS; updateAcceptState(); sError.clear(); return true; @@ -277,9 +291,32 @@ void nmWxWellEditor::slotWellTypeChanged(int nIndex) } m_pTrajectoryDisplay->setCurrentWellboreData(pWell); createController(); + m_pPressureFlowPage->setWell(pWell); updateAcceptState(); } +void nmWxWellEditor::slotCategoryButtonClicked(int nPage) +{ + if (!m_bInitialized || m_pStackedWidget == nullptr || + nPage < 0 || nPage >= NM_WELL_EDITOR_PAGE_COUNT) + { + return; + } + + if (nPage == m_nCurrentPage) + { + return; + } + + // 查看页只读取当前工作副本,不提交或校验参数页中的临时输入。 + if (nPage == NM_WELL_EDITOR_PAGE_PRESSURE_FLOW) + { + reloadPressureFlowPage(); + } + m_pStackedWidget->setCurrentIndex(nPage); + m_nCurrentPage = nPage; +} + void nmWxWellEditor::slotWellNameEdited(const QString& sWellName) { if (m_pSession != nullptr) @@ -312,6 +349,18 @@ void nmWxWellEditor::reloadTimeDependentSkinControls( m_pTimeDependentButton->setEnabled(bEnabled); } +void nmWxWellEditor::reloadPressureFlowPage() +{ + if (m_pPressureFlowPage == nullptr || m_pSession == nullptr) + { + return; + } + + nmDataWellBase* pWell = m_pSession->currentWell(); + m_pPressureFlowPage->setWell(pWell); + m_pPressureFlowPage->refreshData(); +} + void nmWxWellEditor::slotEditTimeDependentSkin() { if (!m_bInitialized || m_pSession->currentWell() == nullptr) @@ -384,10 +433,18 @@ void nmWxWellEditor::createUi() { QVBoxLayout* pMainLayout = new QVBoxLayout(this); pMainLayout->setContentsMargins(12, 10, 12, 0); - pMainLayout->setSpacing(8); + pMainLayout->setSpacing(0); + pMainLayout->addWidget(createCategoryBar()); + + m_pStackedWidget = new QStackedWidget(this); + + // 第一步:井参数页完整保留原有参数区、轨迹视图和分割比例。 + QWidget* pParameterPage = new QWidget(m_pStackedWidget); + QVBoxLayout* pPageLayout = new QVBoxLayout(pParameterPage); + pPageLayout->setContentsMargins(0, 6, 0, 0); + pPageLayout->setSpacing(8); - // 第一步:左侧固定井信息并滚动参数分组,右侧保持常驻轨迹视图。 - m_pMainSplitter = new QSplitter(Qt::Horizontal, this); + m_pMainSplitter = new QSplitter(Qt::Horizontal, pParameterPage); m_pMainSplitter->setChildrenCollapsible(false); QWidget* pParameterContainer = new QWidget(m_pMainSplitter); @@ -423,8 +480,17 @@ void nmWxWellEditor::createUi() QList listSizes; listSizes << 410 << 730; m_pMainSplitter->setSizes(listSizes); - // 第三步:底部命令区跨越两列,并集中连接编辑状态信号。 - pMainLayout->addWidget(m_pMainSplitter, 1); + pPageLayout->addWidget(m_pMainSplitter, 1); + + // 第二步:只读压力流量页与参数页常驻同一堆栈,切换时不重建控件。 + m_pPressureFlowPage = new nmWellPressureFlowPage(m_pStackedWidget); + m_pStackedWidget->addWidget(pParameterPage); + m_pStackedWidget->addWidget(m_pPressureFlowPage); + m_pStackedWidget->setCurrentIndex( + NM_WELL_EDITOR_PAGE_PARAMETERS); + pMainLayout->addWidget(m_pStackedWidget, 1); + + // 第三步:底部命令区在两个页面间共享,并集中连接编辑状态信号。 pMainLayout->addWidget(createFooter()); connect(m_pWellNameLineEdit, SIGNAL(textChanged(QString)), @@ -438,6 +504,128 @@ void nmWxWellEditor::createUi() this, SLOT(slotPanelValidationMessageChanged(QString))); } +QWidget* nmWxWellEditor::createCategoryBar() +{ + QGroupBox* pCategoryBar = new QGroupBox(this); + QHBoxLayout* pBarLayout = new QHBoxLayout(pCategoryBar); + pBarLayout->setContentsMargins(3, 3, 3, 3); + pBarLayout->setSpacing(0); + + m_pCategoryButtonGroup = new QButtonGroup(this); + + QHBoxLayout* pBasicLayout = nullptr; + QWidget* pBasicGroup = createRibbonGroup( + tr("Basic"), pCategoryBar, pBasicLayout); + pBasicLayout->insertWidget(0, createCategoryButton( + NM_WELL_EDITOR_PAGE_PARAMETERS, + tr("Well parameters"), + "NmWell", + pBasicGroup)); + pBarLayout->addWidget(pBasicGroup); + pBarLayout->addWidget(createRibbonSeparator(pCategoryBar)); + + QHBoxLayout* pDataLayout = nullptr; + QWidget* pDataGroup = createRibbonGroup( + tr("Data"), pCategoryBar, pDataLayout); + pDataLayout->insertWidget(0, createCategoryButton( + NM_WELL_EDITOR_PAGE_PRESSURE_FLOW, + tr("Pressure and flow"), + "tAllData", + pDataGroup)); + pBarLayout->addWidget(pDataGroup); + pBarLayout->addWidget(createRibbonSeparator(pCategoryBar)); + + QHBoxLayout* pHelpLayout = nullptr; + QWidget* pHelpGroup = createRibbonGroup( + tr("Help"), pCategoryBar, pHelpLayout); + pHelpLayout->insertWidget(0, createHelpButton(pHelpGroup)); + pBarLayout->addWidget(pHelpGroup); + pBarLayout->addStretch(1); + + connect(m_pCategoryButtonGroup, SIGNAL(buttonClicked(int)), + this, SLOT(slotCategoryButtonClicked(int))); + return pCategoryBar; +} + +QWidget* nmWxWellEditor::createRibbonGroup( + const QString& sCaption, + QWidget* pParent, + QHBoxLayout*& pCommandLayout) +{ + QWidget* pGroupWidget = new QWidget(pParent); + QVBoxLayout* pGroupLayout = new QVBoxLayout(pGroupWidget); + pGroupLayout->setContentsMargins(11, 2, 11, 1); + pGroupLayout->setSpacing(4); + + QWidget* pCommandWidget = new QWidget(pGroupWidget); + pCommandLayout = new QHBoxLayout(pCommandWidget); + pCommandLayout->setContentsMargins(0, 0, 0, 0); + pCommandLayout->setSpacing(6); + pCommandLayout->addStretch(); + pGroupLayout->addWidget(pCommandWidget, 1); + + QLabel* pCaptionLabel = new QLabel(sCaption, pGroupWidget); + pCaptionLabel->setAlignment(Qt::AlignCenter); + pCaptionLabel->setFixedHeight(16); + QFont oCaptionFont = pCaptionLabel->font(); + oCaptionFont.setUnderline(true); + pCaptionLabel->setFont(oCaptionFont); + QPalette oCaptionPalette = pCaptionLabel->palette(); + oCaptionPalette.setColor( + QPalette::WindowText, + pGroupWidget->palette().color(QPalette::Mid)); + pCaptionLabel->setPalette(oCaptionPalette); + pGroupLayout->addWidget(pCaptionLabel); + return pGroupWidget; +} + +QFrame* nmWxWellEditor::createRibbonSeparator(QWidget* pParent) +{ + QFrame* pSeparator = new QFrame(pParent); + pSeparator->setFrameShape(QFrame::VLine); + pSeparator->setFrameShadow(QFrame::Plain); + pSeparator->setLineWidth(1); + QPalette oSeparatorPalette = pSeparator->palette(); + oSeparatorPalette.setColor( + QPalette::WindowText, + pParent->palette().color(QPalette::Mid)); + pSeparator->setPalette(oSeparatorPalette); + pSeparator->setSizePolicy( + QSizePolicy::Fixed, QSizePolicy::Expanding); + return pSeparator; +} + +QToolButton* nmWxWellEditor::createCategoryButton( + int nPage, + const QString& sText, + const QString& sIconName, + QWidget* pParent) +{ + QToolButton* pButton = new QToolButton(pParent); + pButton->setText(sText); + pButton->setIcon(zxLoadIcon(sIconName)); + pButton->setIconSize(QSize(28, 28)); + pButton->setToolButtonStyle(Qt::ToolButtonTextUnderIcon); + pButton->setCheckable(false); + pButton->setSizePolicy( + QSizePolicy::Preferred, QSizePolicy::Fixed); + m_pCategoryButtonGroup->addButton(pButton, nPage); + return pButton; +} + +QToolButton* nmWxWellEditor::createHelpButton(QWidget* pParent) +{ + QToolButton* pButton = new QToolButton(pParent); + pButton->setText(tr("Help")); + pButton->setIcon(zxLoadIcon("Help")); + pButton->setIconSize(QSize(28, 28)); + pButton->setToolButtonStyle(Qt::ToolButtonTextUnderIcon); + pButton->setSizePolicy( + QSizePolicy::Preferred, QSizePolicy::Fixed); + connect(pButton, SIGNAL(clicked()), this, SLOT(slotHelp())); + return pButton; +} + QWidget* nmWxWellEditor::createWellInformationWidget(QWidget* pParent) { QGroupBox* pInformationGroup = new QGroupBox(