From efc590edc611b872576698c721af680b62867ee1 Mon Sep 17 00:00:00 2001 From: lh <2334563547@qq.com> Date: Fri, 14 Aug 2026 09:54:09 +0800 Subject: [PATCH] =?UTF-8?q?feat(nmNum):=20=E9=87=8D=E6=9E=84=E5=82=A8?= =?UTF-8?q?=E5=B1=82=E5=8F=82=E6=95=B0=E8=AE=BE=E7=BD=AE=E5=AF=B9=E8=AF=9D?= =?UTF-8?q?=E6=A1=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增模块化参数页面、字段目录和编辑会话 - 通过数据源注入实现工作副本及统一提交 - 支持单位换算、参数校验和分类联动 - 无对应对象时禁用分类按钮 - 统一渗透率基准单位为 D --- Bin/Config/Lang/cn/nmNum_cn.qm | Bin 108029 -> 113262 bytes Bin/Config/Lang/cn/nmNum_cn.ts | 76 +- Bin/XmlFiles/ModelParaDefinesNM_cn.xml | 21 +- Bin/XmlFiles/ModelParaDefinesNM_en.xml | 21 +- .../nmSubWxs/nmReservoirParameterCatalog.h | 143 +++ .../nmSubWxs/nmReservoirParameterField.h | 155 ++++ .../nmNum/nmSubWxs/nmReservoirParameterPage.h | 128 +++ .../nmReservoirPropertiesDataSource.h | 92 ++ .../nmSubWxs/nmReservoirPropertiesSession.h | 98 ++ .../nmSubWxs/nmWxReservoirPropertiesEditor.h | 148 +++ Src/nmNum/nmData/nmDataAutomaticFitting.cpp | 7 +- Src/nmNum/nmData/nmDataReservoir.cpp | 26 +- Src/nmNum/nmSubWnd/nmSubWndMain.cpp | 28 +- .../nmSubWxs/nmReservoirParameterCatalog.cpp | 852 ++++++++++++++++++ .../nmSubWxs/nmReservoirParameterField.cpp | 429 +++++++++ .../nmSubWxs/nmReservoirParameterPage.cpp | 539 +++++++++++ .../nmReservoirPropertiesDataSource.cpp | 119 +++ .../nmSubWxs/nmReservoirPropertiesSession.cpp | 242 +++++ Src/nmNum/nmSubWxs/nmWxAutomaticFitting.cpp | 2 +- .../nmWxReservoirPropertiesEditor.cpp | 544 +++++++++++ Src4/nmNum/nmSubWxs/nmSubWxs.pro | 4 +- 21 files changed, 3638 insertions(+), 36 deletions(-) create mode 100644 Include/nmNum/nmSubWxs/nmReservoirParameterCatalog.h create mode 100644 Include/nmNum/nmSubWxs/nmReservoirParameterField.h create mode 100644 Include/nmNum/nmSubWxs/nmReservoirParameterPage.h create mode 100644 Include/nmNum/nmSubWxs/nmReservoirPropertiesDataSource.h create mode 100644 Include/nmNum/nmSubWxs/nmReservoirPropertiesSession.h create mode 100644 Include/nmNum/nmSubWxs/nmWxReservoirPropertiesEditor.h create mode 100644 Src/nmNum/nmSubWxs/nmReservoirParameterCatalog.cpp create mode 100644 Src/nmNum/nmSubWxs/nmReservoirParameterField.cpp create mode 100644 Src/nmNum/nmSubWxs/nmReservoirParameterPage.cpp create mode 100644 Src/nmNum/nmSubWxs/nmReservoirPropertiesDataSource.cpp create mode 100644 Src/nmNum/nmSubWxs/nmReservoirPropertiesSession.cpp create mode 100644 Src/nmNum/nmSubWxs/nmWxReservoirPropertiesEditor.cpp diff --git a/Bin/Config/Lang/cn/nmNum_cn.qm b/Bin/Config/Lang/cn/nmNum_cn.qm index 32bc151c051c51952484720af60633b42170db74..3092a7f82694ec20f4b0c0073dd0dd867a5ee791 100644 GIT binary patch delta 14466 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 MEp{|cV<&C?4=Cq{AOHXW delta 10592 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 + @@ -6781,4 +6781,78 @@ Average pressure in contour: %2 MPa Kriging 结果中包含无效数值。 + + nmReservoirParameterField + The parameter layout is unavailable.参数布局不可用。 + Parameter definition not found: %1未找到参数定义:%1 + Failed to bind the unit for parameter: %1参数单位绑定失败:%1 + Dimensionless无量纲 + + + nmReservoirParameterPage + Reservoir parameters储层参数 + Well parameters井参数 + Fault parameters断层参数 + Fracture parameters裂缝参数 + Composite region parameters复合区参数 + Region mark parameters区域标记参数 + Select well:选择井: + Select fault:选择断层: + Select fracture:选择裂缝: + Select composite region:选择复合区: + Select region mark:选择区域标记: + Select object:选择对象: + No editable objects in this category.此分类中没有可编辑对象。 + + + nmWxReservoirPropertiesEditor + Reservoir properties settings储层特性设置 + Reservoir properties储层参数 + Help帮助 + OK确定 + Cancel取消 + Basic基础 + Wells + Geometry objects几何对象 + Reservoir parameters储层参数 + Well parameters井参数 + Faults断层 + Fractures裂缝 + Composite regions复合区 + Region marks区域标记 + Please correct the invalid parameter values.请修正无效的参数值。 + Reservoir type储层类型 + Homogeneous均质储层 + Dual porosity pseudo steady state双重介质拟稳态 + Fault flow model断层流动模型 + Region flow model复合区流动模型 + Leaky泄漏 + Composite limit复合边界 + Fracture flow model裂缝流动模型 + Finite conductivity有限导流 + Infinite conductivity无限导流 + Vertical well直井 + Vertical fractured well垂直压裂井 + Horizontal fractured well水平压裂井 + Position and wellbore位置与井筒 + Storage and fracture储集与裂缝 + Basic properties基础特性 + Fluid parameters流体参数 + Compression and saturation压缩与饱和度 + Dual-medium parameters双重介质参数 + Geometry几何位置 + Flow properties流动属性 + Conductivity properties导流属性 + Region properties区域属性 + + + nmSubWndMain + Reservoir properties储层参数 + The current numerical well-test data is unavailable.当前数值试井数据不可用。 + + + QObject + The current numerical well-test data is unavailable.当前数值试井数据不可用。 + The reservoir properties editing session is unavailable.储层参数编辑会话不可用。 + diff --git a/Bin/XmlFiles/ModelParaDefinesNM_cn.xml b/Bin/XmlFiles/ModelParaDefinesNM_cn.xml index 367fb1c..8788ebc 100644 --- a/Bin/XmlFiles/ModelParaDefinesNM_cn.xml +++ b/Bin/XmlFiles/ModelParaDefinesNM_cn.xml @@ -17,8 +17,11 @@ - + + + + @@ -32,11 +35,11 @@ - + - - + + @@ -44,7 +47,8 @@ - + + @@ -52,6 +56,7 @@ + @@ -85,12 +90,12 @@ - + - + @@ -99,7 +104,7 @@ - + diff --git a/Bin/XmlFiles/ModelParaDefinesNM_en.xml b/Bin/XmlFiles/ModelParaDefinesNM_en.xml index fb7e17f..02be24f 100644 --- a/Bin/XmlFiles/ModelParaDefinesNM_en.xml +++ b/Bin/XmlFiles/ModelParaDefinesNM_en.xml @@ -18,8 +18,11 @@ - + + + + @@ -33,11 +36,11 @@ - + - - + + @@ -45,7 +48,8 @@ - + + @@ -53,6 +57,7 @@ + @@ -86,12 +91,12 @@ - + - + @@ -100,7 +105,7 @@ - + diff --git a/Include/nmNum/nmSubWxs/nmReservoirParameterCatalog.h b/Include/nmNum/nmSubWxs/nmReservoirParameterCatalog.h new file mode 100644 index 0000000..0824746 --- /dev/null +++ b/Include/nmNum/nmSubWxs/nmReservoirParameterCatalog.h @@ -0,0 +1,143 @@ +#pragma once + +#include "nmSubWxs_global.h" +#include "nmDefines.h" + +#include +#include + +class nmAttrRegistry; +struct nmReservoirPropertiesSnapshot; + +/** @brief 储层参数编辑页面分类。 */ +enum nmReservoirPropertiesCategory +{ + NM_RESERVOIR_PAGE_RESERVOIR = 0, ///< 储层。 + NM_RESERVOIR_PAGE_WELL, ///< 井。 + NM_RESERVOIR_PAGE_FAULT, ///< 断层。 + NM_RESERVOIR_PAGE_FRACTURE, ///< 裂缝。 + NM_RESERVOIR_PAGE_COMPOSITE_REGION, ///< 复合区。 + NM_RESERVOIR_PAGE_REGION_MARK, ///< 区域标记。 + NM_RESERVOIR_PAGE_COUNT ///< 分类数量。 +}; + +/** @brief 参数对象的具体类型。 */ +enum nmReservoirPropertiesObjectType +{ + NM_RESERVOIR_OBJECT_RESERVOIR = 0, ///< 储层对象。 + NM_RESERVOIR_OBJECT_VERTICAL_WELL, ///< 直井对象。 + NM_RESERVOIR_OBJECT_VERTICAL_FRAC_WELL, ///< 垂直压裂井对象。 + NM_RESERVOIR_OBJECT_HORIZONTAL_FRAC_WELL,///< 水平压裂井对象。 + NM_RESERVOIR_OBJECT_REGION_MARK, ///< 区域标记对象。 + NM_RESERVOIR_OBJECT_FAULT, ///< 断层对象。 + NM_RESERVOIR_OBJECT_FRACTURE, ///< 裂缝对象。 + NM_RESERVOIR_OBJECT_REGION ///< 复合区对象。 +}; + +/** @brief 参数字段的编辑器类型。 */ +enum nmReservoirParameterEditorType +{ + NM_RESERVOIR_EDITOR_NUMBER = 0, ///< 浮点数输入框。 + NM_RESERVOIR_EDITOR_INTEGER, ///< 整数输入框。 + NM_RESERVOIR_EDITOR_ENUM ///< 稳定code枚举下拉框。 +}; + +/** @brief 枚举下拉框选项。 */ +struct NM_SUB_WXS_EXPORT nmReservoirEnumOption +{ + QString m_sCode; ///< 不随语言变化的业务代码。 + QString m_sText; ///< 当前语言下的显示文本。 +}; + +/** + * @brief 单个参数字段的界面描述。 + * @note 稳定参数ID用于会话读写,基础参数ID用于读取XML单位和范围。 + */ +struct NM_SUB_WXS_EXPORT nmReservoirParameterFieldInfo +{ + QString m_sParameterId; ///< 编辑会话中的稳定参数ID。 + QString m_sBaseParameterId; ///< 中英文参数XML中的基础参数ID。 + QString m_sLabel; ///< 枚举字段的显示名称;数值字段留空时读取XML别名。 + nmReservoirParameterEditorType m_eEditorType; ///< 字段编辑器类型。 + bool m_bUnitEnabled; ///< 是否为该字段创建单位下拉框。 + QList m_listOptions; ///< 枚举字段的稳定选项。 +}; + +/** @brief 参数分组的界面描述。 */ +struct NM_SUB_WXS_EXPORT nmReservoirParameterGroupInfo +{ + QString m_sTitle; ///< 分组标题。 + int m_nRow; ///< 分组在页面网格中的行。 + int m_nColumn; ///< 分组在页面网格中的列。 + int m_nRowSpan; ///< 分组纵向跨越的行数。 + int m_nColumnSpan; ///< 分组横跨的列数。 + bool m_bValueRightAligned; ///< 数值输入是否右对齐。 + QList m_listFields; ///< 分组内按显示顺序排列的字段。 +}; + +/** @brief 单个可编辑对象的完整界面描述。 */ +struct NM_SUB_WXS_EXPORT nmReservoirPropertyObjectInfo +{ + nmReservoirPropertiesCategory m_eCategory; ///< 对象所属页面分类。 + nmReservoirPropertiesObjectType m_eObjectType; ///< 对象具体类型。 + int m_nSourceIndex; ///< 对象在工作副本容器中的索引。 + QString m_sObjectCode; ///< 编辑会话内稳定的对象编码。 + QString m_sDisplayName; ///< 对象显示名称。 + QList m_listGroups; ///< 原型式分组和字段描述。 +}; + +/** + * @brief 储层参数规范目录。 + * @note 对象映射、稳定ID、页面分组和联动所需的基础ID全部在此集中维护。 + */ +class NM_SUB_WXS_EXPORT nmReservoirParameterCatalog +{ +public: + /** + * @brief 将工作副本中的属性注册到独立注册表。 + * @param pRegistry 编辑会话独占的属性注册表。 + * @param oSnapshot 可写工作副本。 + */ + static void registerSnapshot(nmAttrRegistry* pRegistry, + nmReservoirPropertiesSnapshot& oSnapshot); + + /** + * @brief 构建指定分类的对象和原型式分组描述。 + * @param eCategory 页面分类。 + * @param oSnapshot 当前工作副本。 + * @return 按界面显示顺序排列的对象描述。 + */ + static QList createObjectInfos( + nmReservoirPropertiesCategory eCategory, + nmReservoirPropertiesSnapshot& oSnapshot); + + /** + * @brief 将数据类中的存储文本转换为稳定枚举code。 + * @param sParameterId 枚举参数ID。 + * @param sStoredValue 数据类中的存储文本。 + * @return 稳定业务code。 + */ + static QString enumCodeFromStoredValue(const QString& sParameterId, + const QString& sStoredValue); + + /** + * @brief 将稳定枚举code转换为数据类的存储文本。 + * @param sParameterId 枚举参数ID。 + * @param sCode 稳定业务code。 + * @return 数据类使用的存储文本。 + */ + static QString storedValueFromEnumCode(const QString& sParameterId, + const QString& sCode); + + /** @brief 读取工作副本中的枚举code。 */ + static QString enumCode(const nmReservoirPropertiesSnapshot& oSnapshot, + const QString& sParameterId); + + /** @brief 修改工作副本中的枚举code。 */ + static bool setEnumCode(nmReservoirPropertiesSnapshot& oSnapshot, + const QString& sParameterId, + const QString& sCode); + +private: + nmReservoirParameterCatalog(); +}; diff --git a/Include/nmNum/nmSubWxs/nmReservoirParameterField.h b/Include/nmNum/nmSubWxs/nmReservoirParameterField.h new file mode 100644 index 0000000..8e7a3fe --- /dev/null +++ b/Include/nmNum/nmSubWxs/nmReservoirParameterField.h @@ -0,0 +1,155 @@ +#pragma once + +#include "nmSubWxs_global.h" +#include "nmReservoirParameterCatalog.h" + +#include +#include + +class iParameter; +class iUnitGroup; +class QComboBox; +class QGridLayout; +class QLabel; +class QLineEdit; + +/** + * @brief 储层参数字段所需的最小单位服务接口。 + * @note 主对话框负责适配iDlgBase,字段不直接依赖对话框基类或全局对象。 + */ +class NM_SUB_WXS_EXPORT nmIReservoirParameterUnitService +{ +public: + /** @brief 析构单位服务接口。 */ + virtual ~nmIReservoirParameterUnitService() {} + + /** @brief 按NM参数定义绑定一个数值编辑器和单位下拉框。 */ + virtual bool bindUnitField(QLineEdit* pLineEdit, + QComboBox* pUnitComboBox, + const QString& sBaseParameterId) = 0; + + /** @brief 将基准单位数值写入指定编辑器。 */ + virtual void setUnitBaseValue(QLineEdit* pLineEdit, + double dBaseValue) = 0; + + /** @brief 读取指定编辑器的基准单位数值。 */ + virtual double unitBaseValue(QLineEdit* pLineEdit) const = 0; + + /** @brief 校验指定单位编辑器。 */ + virtual bool isUnitInputValid(QLineEdit* pLineEdit) = 0; + + /** @brief 返回指定编辑器当前显示单位。 */ + virtual QString currentUnit(QLineEdit* pLineEdit) const = 0; + + /** @brief 返回指定编辑器的单位组,不转移所有权。 */ + virtual iUnitGroup* unitGroup(QLineEdit* pLineEdit) const = 0; +}; + +/** + * @brief 原型式参数行的轻量逻辑单元。 + * @note 本类不创建外框或子面板,只把标签、编辑器和单位控件直接加入分组网格。 + */ +class NM_SUB_WXS_EXPORT nmReservoirParameterField : public QObject +{ +public: + /** + * @brief 构造参数字段。 + * @param oFieldInfo 稳定字段描述。 + * @param pUnitService 主对话框注入的单位服务,不转移所有权。 + * @param pParent 字段生命周期父对象。 + */ + nmReservoirParameterField( + const nmReservoirParameterFieldInfo& oFieldInfo, + nmIReservoirParameterUnitService* pUnitService, + QObject* pParent = nullptr); + + /** @brief 析构字段,不单独删除由分组控件拥有的界面控件。 */ + virtual ~nmReservoirParameterField(); + + /** + * @brief 将字段的三个直接控件加入分组网格。 + * @param pLayout 所属分组的网格布局。 + * @param nRow 字段所在行。 + * @param sError 初始化失败原因。 + * @return 参数元数据和单位绑定均成功时返回true。 + */ + bool initialize(QGridLayout* pLayout, int nRow, QString& sError); + + /** @brief 返回编辑会话使用的稳定参数ID。 */ + QString parameterId() const; + + /** @brief 返回XML参数定义使用的基础参数ID。 */ + QString baseParameterId() const; + + /** @brief 返回字段是否为稳定code枚举。 */ + bool isEnum() const; + + /** @brief 设置数值输入文字是否右对齐。 */ + void setValueRightAligned(bool bRightAligned); + + /** @brief 返回数值编辑列的统一最小宽度。 */ + static int editorMinimumWidth(); + + /** @brief 返回单位列的统一最小宽度。 */ + static int unitMinimumWidth(); + + /** @brief 返回输入框和下拉框的统一紧凑高度。 */ + static int editorHeight(); + + /** @brief 设置基准单位下的数值并刷新显示。 */ + void setBaseValue(const QVariant& oValue); + + /** + * @brief 获取基准单位下的当前数值。 + * @param bValid 输出当前输入是否有效。 + * @return 有效时返回基准值,否则返回无效QVariant。 + */ + QVariant baseValue(bool& bValid); + + /** @brief 设置枚举稳定code;不会触发业务联动。 */ + void setEnumCode(const QString& sCode); + + /** @brief 返回当前枚举稳定code。 */ + QString enumCode() const; + + /** @brief 校验当前输入,并更新参数范围提示。 */ + bool validate(); + + /** @brief 同时显示或隐藏该参数行的三个直接控件。 */ + void setFieldVisible(bool bVisible); + + /** @brief 返回参数行当前是否参与编辑和校验。 */ + bool isFieldVisible() const; + + /** @brief 返回数值输入框,供页面集中连接变化信号。 */ + QLineEdit* valueEditor() const; + + /** @brief 返回枚举下拉框,供页面集中连接变化信号。 */ + QComboBox* enumEditor() const; + +private: + /** @brief 创建数值输入框及单位控件。 */ + bool createNumericEditor(QGridLayout* pLayout, + int nRow, + QString& sError); + + /** @brief 创建稳定code枚举下拉框。 */ + void createEnumEditor(QGridLayout* pLayout, int nRow); + + /** @brief 按XML精度格式化无单位或整数参数。 */ + QString formatBaseValue(double dValue) const; + + /** @brief 更新输入框的有效状态和帮助文字。 */ + void updateValidationAppearance(bool bValid); + + nmReservoirParameterFieldInfo m_oFieldInfo; ///< 稳定字段描述。 + nmIReservoirParameterUnitService* m_pUnitService; ///< 注入的单位服务,不拥有所有权。 + iParameter* m_pParameter; ///< 共享XML参数定义,不拥有所有权。 + QLabel* m_pLabel; ///< 参数名称标签。 + QLineEdit* m_pLineEdit; ///< 数值输入框。 + QComboBox* m_pValueComboBox; ///< 枚举值下拉框。 + QComboBox* m_pUnitComboBox; ///< 可换算单位下拉框。 + QLabel* m_pUnitLabel; ///< 无单位字段的空白对齐占位。 + bool m_bVisible; ///< 参数行是否参与编辑。 + bool m_bUpdating; ///< 程序写值时阻止业务信号。 +}; diff --git a/Include/nmNum/nmSubWxs/nmReservoirParameterPage.h b/Include/nmNum/nmSubWxs/nmReservoirParameterPage.h new file mode 100644 index 0000000..10b80f8 --- /dev/null +++ b/Include/nmNum/nmSubWxs/nmReservoirParameterPage.h @@ -0,0 +1,128 @@ +#pragma once + +#include "nmSubWxs_global.h" +#include "nmReservoirParameterCatalog.h" + +#include +#include +#include + +class nmIReservoirParameterUnitService; +class nmReservoirParameterField; +class nmReservoirPropertiesSession; +class QComboBox; +class QGridLayout; +class QLabel; +class QVBoxLayout; + +/** + * @brief 储层参数分类的原型式共享页面。 + * @note 页面是普通QWidget,只负责一层内容布局,不拥有单位系统或真实数据。 + */ +class NM_SUB_WXS_EXPORT nmReservoirParameterPage : public QWidget +{ + Q_OBJECT + +public: + /** + * @brief 构造共享参数页面。 + * @param eCategory 页面分类。 + * @param pSession 编辑会话,不转移所有权。 + * @param pUnitService 主对话框单位服务,不转移所有权。 + * @param pParent 父窗口。 + */ + nmReservoirParameterPage(nmReservoirPropertiesCategory eCategory, + nmReservoirPropertiesSession* pSession, + nmIReservoirParameterUnitService* pUnitService, + QWidget* pParent = nullptr); + + /** @brief 析构参数页面。 */ + virtual ~nmReservoirParameterPage(); + + /** + * @brief 重新读取当前分类对象并显示第一个对象。 + * @param sError 输出字段构建错误。 + * @return 页面构建成功返回true。 + */ + bool reloadObjects(QString& sError); + + /** + * @brief 校验并将当前可见数值写入会话工作副本。 + * @return 输入全部有效且写入成功时返回true。 + */ + bool commitPendingEdits(); + + /** @brief 返回当前页面的可见输入是否全部有效。 */ + bool isInputValid(); + + /** @brief 返回当前分类是否存在可编辑对象。 */ + bool hasEditableObjects() const; + + /** @brief 返回页面分类。 */ + nmReservoirPropertiesCategory category() const; + +signals: + /** @brief 当前页有效状态发生变化。 */ + void sigValidityChanged(bool bValid); + +private slots: + /** @brief 切换当前编辑对象。 */ + void slotObjectChanged(int nIndex); + + /** @brief 数值输入变化后刷新整体有效状态。 */ + void slotFieldValueEdited(); + + /** @brief 枚举code变化后保存并执行集中联动。 */ + void slotEnumIndexChanged(int nIndex); + +private: + /** @brief 创建页标题、对象选择器和单层分组区域。 */ + void createUi(); + + /** @brief 根据当前分类返回标题。 */ + QString categoryTitle() const; + + /** @brief 根据当前分类返回对象选择标签。 */ + QString objectSelectorLabel() const; + + /** @brief 构建指定对象的全部分组和字段。 */ + bool buildObject(int nObjectIndex, QString& sError); + + /** @brief 删除上一个对象的分组控件和字段对象。 */ + void clearObjectEditors(); + + /** @brief 创建一个原型式参数分组。 */ + bool buildGroup(const nmReservoirParameterGroupInfo& oGroupInfo, + QString& sError); + + /** @brief 从工作副本加载字段初始值。 */ + void loadFieldValue(nmReservoirParameterField* pField); + + /** @brief 根据当前稳定code统一刷新字段可见性。 */ + void applyVisibilityRules(); + + /** @brief 设置指定稳定参数ID的字段可见性。 */ + void setFieldVisible(const QString& sParameterId, bool bVisible); + + /** @brief 刷新有效状态并通知主对话框。 */ + void updateValidity(); + + nmReservoirPropertiesCategory m_eCategory; ///< 页面分类。 + nmReservoirPropertiesSession* m_pSession; ///< 编辑会话,不拥有所有权。 + nmIReservoirParameterUnitService* m_pUnitService; ///< 主对话框单位服务,不拥有所有权。 + QList m_listObjectInfos; ///< 当前分类对象描述。 + int m_nCurrentObjectIndex; ///< 当前对象索引。 + bool m_bUpdatingObject; ///< 阻止对象下拉框回滚重入。 + bool m_bBuildingFields; ///< 阻止加载数据时触发业务信号。 + + QLabel* m_pTitleLabel; ///< 页面标题。 + QLabel* m_pObjectLabel; ///< 对象选择标签。 + QComboBox* m_pObjectComboBox; ///< 多对象选择下拉框。 + QWidget* m_pGroupsWidget; ///< 无边框的分组布局载体。 + QGridLayout* m_pGroupsLayout; ///< 两列分组布局。 + QVBoxLayout* m_pMainLayout; ///< 页面主布局。 + QLabel* m_pEmptyLabel; ///< 空对象提示。 + QList m_listFields; ///< 当前对象全部字段。 + QMap m_mapFields; ///< 稳定ID到字段映射。 + QMap m_mapEditorFields; ///< 编辑器到字段映射。 +}; diff --git a/Include/nmNum/nmSubWxs/nmReservoirPropertiesDataSource.h b/Include/nmNum/nmSubWxs/nmReservoirPropertiesDataSource.h new file mode 100644 index 0000000..02df385 --- /dev/null +++ b/Include/nmNum/nmSubWxs/nmReservoirPropertiesDataSource.h @@ -0,0 +1,92 @@ +#pragma once + +#include "nmSubWxs_global.h" + +#include "nmDataReservoir.h" +#include "nmDataVerticalWell.h" +#include "nmDataVerticalFracturedWell.h" +#include "nmDataHorizontalFracturedWell.h" +#include "nmDataRegionMark.h" +#include "nmDataFault.h" +#include "nmDataFracture.h" +#include "nmDataRegion.h" + +class nmDataAnalyzeManager; + +/** + * @brief 储层参数编辑快照。 + * @note 快照保存一次编辑会话所需的全部数据副本,不包含普通水平井。 + */ +struct NM_SUB_WXS_EXPORT nmReservoirPropertiesSnapshot +{ + /** @brief 构造空快照。 */ + nmReservoirPropertiesSnapshot(); + + nmDataReservoir m_oReservoir; ///< 储层参数副本。 + QVector m_vecVerticalWells; ///< 直井参数副本。 + QVector m_vecVerticalFracturedWells; ///< 垂直压裂井参数副本。 + QVector m_vecHorizontalFracturedWells; ///< 水平压裂井参数副本。 + QVector m_vecRegionMarks; ///< 区域标记参数副本。 + QVector m_vecFaults; ///< 断层参数副本。 + QVector m_vecFractures; ///< 裂缝参数副本,包含DFN以保持提交索引稳定。 + QVector m_vecRegions; ///< 复合区参数副本。 +}; + +/** + * @brief 储层参数数据源接口。 + * @note 对话框只依赖此接口,不直接访问全局数据管理器。 + */ +class NM_SUB_WXS_EXPORT nmIReservoirPropertiesDataSource +{ +public: + /** @brief 析构数据源接口。 */ + virtual ~nmIReservoirPropertiesDataSource(); + + /** + * @brief 读取当前分析窗口的数据快照。 + * @param oSnapshot 输出的工作快照。 + * @param sError 输出的错误信息。 + * @return 读取成功返回true。 + */ + virtual bool loadSnapshot(nmReservoirPropertiesSnapshot& oSnapshot, + QString& sError) const = 0; + + /** + * @brief 一次性提交编辑后的工作快照。 + * @param oSnapshot 待提交的工作快照。 + * @param bGeometryChanged 井坐标或几何参数是否发生变化。 + * @param sError 输出的错误信息。 + * @return 提交成功返回true。 + */ + virtual bool commitSnapshot(const nmReservoirPropertiesSnapshot& oSnapshot, + bool bGeometryChanged, + QString& sError) = 0; +}; + +/** + * @brief 基于nmDataAnalyzeManager的数据源适配器。 + */ +class NM_SUB_WXS_EXPORT nmAnalyzeManagerReservoirDataSource : + public nmIReservoirPropertiesDataSource +{ +public: + /** + * @brief 构造数据源适配器。 + * @param pDataManager 当前分析窗口的数据管理器,不转移所有权。 + */ + explicit nmAnalyzeManagerReservoirDataSource(nmDataAnalyzeManager* pDataManager); + + /** @brief 析构数据源适配器。 */ + virtual ~nmAnalyzeManagerReservoirDataSource(); + + virtual bool loadSnapshot(nmReservoirPropertiesSnapshot& oSnapshot, + QString& sError) const; + + virtual bool commitSnapshot(const nmReservoirPropertiesSnapshot& oSnapshot, + bool bGeometryChanged, + QString& sError); + +private: + nmDataAnalyzeManager* m_pDataManager; ///< 当前分析窗口的数据管理器,不拥有所有权。 +}; + diff --git a/Include/nmNum/nmSubWxs/nmReservoirPropertiesSession.h b/Include/nmNum/nmSubWxs/nmReservoirPropertiesSession.h new file mode 100644 index 0000000..fa93af9 --- /dev/null +++ b/Include/nmNum/nmSubWxs/nmReservoirPropertiesSession.h @@ -0,0 +1,98 @@ +#pragma once + +#include "nmSubWxs_global.h" +#include "nmReservoirPropertiesDataSource.h" +#include "nmReservoirParameterCatalog.h" + +#include +#include + +class nmAttrRegistry; + +/** + * @brief 储层参数编辑会话。 + * @note 会话持有独立工作副本和独立属性注册表;取消时直接销毁会话即可。 + */ +class NM_SUB_WXS_EXPORT nmReservoirPropertiesSession +{ +public: + /** + * @brief 构造编辑会话。 + * @param pDataSource 参数数据源,不转移所有权。 + */ + explicit nmReservoirPropertiesSession(nmIReservoirPropertiesDataSource* pDataSource); + + /** @brief 析构编辑会话及其独立注册表。 */ + ~nmReservoirPropertiesSession(); + + /** + * @brief 从数据源创建原始快照和工作副本。 + * @param sError 输出的错误信息。 + * @return 初始化成功返回true。 + */ + bool initialize(QString& sError); + + /** + * @brief 获取指定分类的对象描述。 + * @param eCategory 页面分类。 + * @return 对象描述列表。 + */ + QList objectInfos( + nmReservoirPropertiesCategory eCategory); + + /** + * @brief 获取工作副本中的参数值。 + * @param sParameterId 稳定参数ID。 + * @return 参数值;不存在时返回无效QVariant。 + */ + QVariant value(const QString& sParameterId) const; + + /** + * @brief 修改工作副本中的参数值。 + * @param sParameterId 稳定参数ID。 + * @param oValue 基准单位下的新值。 + * @return 参数存在并完成修改时返回true。 + */ + bool setValue(const QString& sParameterId, const QVariant& oValue); + + /** + * @brief 批量修改工作副本中的参数值。 + * @param mapValues 稳定参数ID到基准值的映射。 + */ + void setValues(const QMap& mapValues); + + /** + * @brief 获取枚举参数的稳定code。 + * @param sParameterId 稳定枚举参数ID。 + * @return 稳定业务code。 + */ + QString enumCode(const QString& sParameterId) const; + + /** + * @brief 修改枚举参数的稳定code。 + * @param sParameterId 稳定枚举参数ID。 + * @param sCode 稳定业务code。 + * @return 修改成功返回true。 + */ + bool setEnumCode(const QString& sParameterId, const QString& sCode); + + /** + * @brief 提交工作副本。 + * @param sError 输出的错误信息。 + * @return 提交成功返回true。 + */ + bool commit(QString& sError); + + /** @brief 获取只读工作副本,供后续导入预览或测试使用。 */ + const nmReservoirPropertiesSnapshot& workingSnapshot() const; + +private: + /** @brief 判断井坐标或几何参数是否发生变化。 */ + bool isGeometryChanged() const; + + nmIReservoirPropertiesDataSource* m_pDataSource; ///< 参数数据源,不拥有所有权。 + nmAttrRegistry* m_pAttrRegistry; ///< 编辑会话独占的属性注册表。 + nmReservoirPropertiesSnapshot m_oOriginalSnapshot; ///< 打开对话框时的原始快照。 + nmReservoirPropertiesSnapshot m_oWorkingSnapshot; ///< 所有界面编辑写入的工作副本。 + bool m_bInitialized; ///< 会话是否初始化成功。 +}; diff --git a/Include/nmNum/nmSubWxs/nmWxReservoirPropertiesEditor.h b/Include/nmNum/nmSubWxs/nmWxReservoirPropertiesEditor.h new file mode 100644 index 0000000..c76658d --- /dev/null +++ b/Include/nmNum/nmSubWxs/nmWxReservoirPropertiesEditor.h @@ -0,0 +1,148 @@ +#pragma once + +#include "iDlgBase.h" +#include "nmSubWxs_global.h" +#include "nmReservoirParameterCatalog.h" +#include "nmReservoirParameterField.h" + +class nmIReservoirPropertiesDataSource; +class nmReservoirPropertiesSession; +class nmReservoirParameterPage; +class QButtonGroup; +class QDialogButtonBox; +class QFrame; +class QHBoxLayout; +class QLabel; +class QStackedWidget; +class QToolButton; + +/** + * @brief 数值试井储层特性参数编辑器。 + * @note 对话框是唯一可见面板,负责原型式导航、单位服务、整体校验和统一提交。 + */ +class NM_SUB_WXS_EXPORT nmWxReservoirPropertiesEditor : + public iDlgBase, + public nmIReservoirParameterUnitService +{ + Q_OBJECT + +public: + /** + * @brief 构造储层特性参数编辑器。 + * @param pDataSource 参数数据源,不转移所有权。 + * @param pParent 父窗口。 + */ + nmWxReservoirPropertiesEditor( + nmIReservoirPropertiesDataSource* pDataSource, + QWidget* pParent = nullptr); + + /** @brief 析构编辑器。 */ + virtual ~nmWxReservoirPropertiesEditor(); + + /** + * @brief 初始化编辑会话和全部页面。 + * @param sError 输出初始化错误。 + * @return 初始化成功返回true。 + */ + bool initialize(QString& sError); + + /** @brief 通过iDlgBase按NM参数系列绑定单位控件。 */ + virtual bool bindUnitField(QLineEdit* pLineEdit, + QComboBox* pUnitComboBox, + const QString& sBaseParameterId); + + /** @brief 将基准单位数值写入字段。 */ + virtual void setUnitBaseValue(QLineEdit* pLineEdit, + double dBaseValue); + + /** @brief 读取字段的基准单位数值。 */ + virtual double unitBaseValue(QLineEdit* pLineEdit) const; + + /** @brief 校验单位字段输入。 */ + virtual bool isUnitInputValid(QLineEdit* pLineEdit); + + /** @brief 返回字段当前显示单位。 */ + virtual QString currentUnit(QLineEdit* pLineEdit) const; + + /** @brief 返回字段单位组,不转移所有权。 */ + virtual iUnitGroup* unitGroup(QLineEdit* pLineEdit) const; + +public slots: + /** @brief 校验并一次性提交工作副本。 */ + virtual void accept(); + + /** @brief 丢弃工作副本并关闭窗口。 */ + virtual void reject(); + +private slots: + /** @brief 切换顶部参数分类。 */ + void slotCategoryButtonClicked(int nCategory); + + /** @brief 根据各页面状态更新确定按钮。 */ + void slotPageValidityChanged(bool bValid); + + /** @brief 进入当前界面的上下文帮助模式。 */ + void slotHelp(); + +private: + /** @brief 创建编辑器的原型式固定布局。 */ + void createUi(); + + /** @brief 创建顶部分组导航。 */ + QWidget* createCategoryBar(); + + /** + * @brief 创建一个带底部分组标题的ribbon区域。 + * @param sCaption 分组标题。 + * @param pParent 父控件。 + * @param pCommandLayout 输出的命令按钮布局,不转移所有权。 + * @return 创建完成的分组控件。 + */ + QWidget* createRibbonGroup(const QString& sCaption, + QWidget* pParent, + QHBoxLayout*& pCommandLayout); + + /** + * @brief 创建框架风格的导航分组竖向分隔线。 + * @param pParent 分隔线所属的顶部导航面板。 + * @return 创建完成的Qt原生分隔线。 + */ + QFrame* createRibbonSeparator(QWidget* pParent); + + /** + * @brief 创建一个参数分类按钮。 + * @param eCategory 分类枚举。 + * @param sText 按钮文字。 + * @param sIconName 框架图标名称。 + * @param pParent 所属ribbon分组。 + * @return 创建完成的工具按钮。 + */ + QToolButton* createCategoryButton( + nmReservoirPropertiesCategory eCategory, + const QString& sText, + const QString& sIconName, + QWidget* pParent); + + /** @brief 创建帮助命令按钮。 */ + QToolButton* createHelpButton(QWidget* pParent); + + /** @brief 创建六个共享参数页面。 */ + void createPages(); + + /** @brief 根据各分类的实际对象数量更新顶部按钮启用状态。 */ + void updateCategoryButtonStates(); + + /** @brief 校验所有页面并更新确定按钮。 */ + bool validateAllPages(); + + nmIReservoirPropertiesDataSource* m_pDataSource; ///< 参数数据源,不拥有所有权。 + nmReservoirPropertiesSession* m_pSession; ///< 对话框独占编辑会话。 + QButtonGroup* m_pCategoryButtonGroup; ///< 顶部分类按钮及稳定页面ID映射。 + QStackedWidget* m_pStackedWidget; ///< 无边框分类页面堆栈。 + QDialogButtonBox* m_pButtonBox; ///< 确定和取消按钮区。 + QToolButton* m_pFooterHelpButton; ///< 底部帮助命令。 + QLabel* m_pStatusLabel; ///< 校验或提交错误提示。 + nmReservoirParameterPage* m_pPages[NM_RESERVOIR_PAGE_COUNT]; ///< 六个参数页。 + int m_nCurrentCategory; ///< 当前显示分类。 + bool m_bInitialized; ///< 编辑器是否初始化完成。 +}; diff --git a/Src/nmNum/nmData/nmDataAutomaticFitting.cpp b/Src/nmNum/nmData/nmDataAutomaticFitting.cpp index a4876ed..3af9c33 100644 --- a/Src/nmNum/nmData/nmDataAutomaticFitting.cpp +++ b/Src/nmNum/nmData/nmDataAutomaticFitting.cpp @@ -13,7 +13,7 @@ nmDataAutomaticFitting::nmDataAutomaticFitting() m_swiSelected = false; // 默认不选中 // 初始化参数最大值 - m_permeabilityMax = nmDataAttribute("Permeability Max", 10.0, "Darcy"); // 1000 mD + m_permeabilityMax = nmDataAttribute("Permeability Max", 10.0, "D"); m_skinMax = nmDataAttribute("Skin Max", 10.0, ""); // 100 m_wellboreStorageMax = nmDataAttribute("Wellbore Storage Max", 2.0, "m^3/MPa"); m_porosityMax = nmDataAttribute("Porosity Max", 0.5, ""); // 50% @@ -23,7 +23,7 @@ nmDataAutomaticFitting::nmDataAutomaticFitting() m_swiMax = nmDataAttribute("Swi Max", 1.0, ""); // 初始化参数最小值 - m_permeabilityMin = nmDataAttribute("Permeability Min", 0.001, "Darcy"); // 0.001 mD + m_permeabilityMin = nmDataAttribute("Permeability Min", 0.001, "D"); m_skinMin = nmDataAttribute("Skin Min", -10.0, ""); // 允许负表皮 m_wellboreStorageMin = nmDataAttribute("Wellbore Storage Min", 1e-4, "m^3/MPa"); m_porosityMin = nmDataAttribute("Porosity Min", 0.01, ""); // 1% @@ -164,6 +164,8 @@ void nmDataAutomaticFitting::FromJsonValue(const rapidjson::Value& jsonValue) // 反序列化参数最大值 if (jsonValue.HasMember("PermeabilityMax") && jsonValue["PermeabilityMax"].IsObject()) { m_permeabilityMax.FromJsonValue(jsonValue["PermeabilityMax"]); + // 自动拟合上下限与求解器使用同一基准单位,不按旧JSON单位文本换算。 + m_permeabilityMax.setUnit("D"); } if (jsonValue.HasMember("SkinMax") && jsonValue["SkinMax"].IsObject()) { m_skinMax.FromJsonValue(jsonValue["SkinMax"]); @@ -190,6 +192,7 @@ void nmDataAutomaticFitting::FromJsonValue(const rapidjson::Value& jsonValue) // 反序列化参数最小值 if (jsonValue.HasMember("PermeabilityMin") && jsonValue["PermeabilityMin"].IsObject()) { m_permeabilityMin.FromJsonValue(jsonValue["PermeabilityMin"]); + m_permeabilityMin.setUnit("D"); } if (jsonValue.HasMember("SkinMin") && jsonValue["SkinMin"].IsObject()) { m_skinMin.FromJsonValue(jsonValue["SkinMin"]); diff --git a/Src/nmNum/nmData/nmDataReservoir.cpp b/Src/nmNum/nmData/nmDataReservoir.cpp index e07e007..0122f55 100644 --- a/Src/nmNum/nmData/nmDataReservoir.cpp +++ b/Src/nmNum/nmData/nmDataReservoir.cpp @@ -1,17 +1,18 @@ -#include "nmDataReservoir.h" +#include "nmDataReservoir.h" #include "ZxSerializer.h" nmDataReservoir::nmDataReservoir() { // 基础属性 m_initialPressure = nmDataAttribute("Initial Pressure",40.0, "MPa", UNIT_TYPE_PRESSURE, QStringList(), QStringList() << "psia" << "Pa" << "kPa" << "atm" << "bara" << "kg/cm^2" << "m" << "psig" << "bar" << "MPa" << "kPag"); - m_permeability = nmDataAttribute("Permeability", 0.025, "Darcy", UNIT_TYPE_PERMEABILITY, QStringList(), QStringList() << "md" << "Darcy" << "m^2" << "cm^2" << "um^2"); + // 求解器直接读取该数值,内部基准单位固定为D;显示换算由框架单位绑定器负责。 + m_permeability = nmDataAttribute("Permeability", 0.025, "D"); m_thickness = nmDataAttribute("Thickness", 10.0, "m", UNIT_TYPE_LENGTH, QStringList(), QStringList() << "ft" << "m" << "cm" << "mm" << "in" << "0.1 in" << "mile" << "km"); m_porosity = nmDataAttribute("Porosity", 0.1, "", UNIT_TYPE_DIMENSIONLESS, QStringList(), QStringList()); m_kxKy = nmDataAttribute("Kx/Ky", 1.0, "", UNIT_TYPE_DIMENSIONLESS, QStringList(), QStringList()); // 油相PVT单值 m_Bo = nmDataAttribute("Bo", 1.2, ""); - m_Miuo = nmDataAttribute("Miuo", 0.5, "", UNIT_TYPE_DIMENSIONLESS, QStringList(), QStringList()); + m_Miuo = nmDataAttribute("Miuo", 0.5, "mPa.s"); // 气相PVT单值 m_Bg = nmDataAttribute("Bg", 1.0, "", UNIT_TYPE_DIMENSIONLESS, QStringList(), QStringList()); @@ -22,8 +23,8 @@ nmDataReservoir::nmDataReservoir() { m_Miuw = nmDataAttribute("Miuw", 1.0, "", UNIT_TYPE_DIMENSIONLESS, QStringList(), QStringList()); // 压缩系数 - m_Ct = nmDataAttribute("Ct", 0.001, "", UNIT_TYPE_DIMENSIONLESS, QStringList(), QStringList()); - m_Cf = nmDataAttribute("Cf", 0.0001, "", UNIT_TYPE_DIMENSIONLESS, QStringList(), QStringList()); + m_Ct = nmDataAttribute("Ct", 0.001, "1/MPa"); + m_Cf = nmDataAttribute("Cf", 0.0001, "1/MPa"); // 储层类型与传导率 m_reservoirType = nmDataAttribute("Reservoir type", "Homogeneous", "", UNIT_TYPE_DIMENSIONLESS, QStringList() << "Homogeneous" << "Dual porosity pseudo steady state", QStringList()); @@ -147,6 +148,8 @@ void nmDataReservoir::FromJsonValue(const rapidjson::Value& jsonValue) } if (jsonValue.HasMember("Permeability") && jsonValue["Permeability"].IsObject()) { m_permeability.FromJsonValue(jsonValue["Permeability"]); + // JSON中的单位文本不参与兼容换算,读取后的内部单位始终按求解器标准固定为D。 + m_permeability.setUnit("D"); } if (jsonValue.HasMember("Thickness") && jsonValue["Thickness"].IsObject()) { m_thickness.FromJsonValue(jsonValue["Thickness"]); @@ -240,11 +243,11 @@ bool nmDataReservoir::_parseData(VecVariant vec, int& n) { m_initialPressure = nmDataAttribute("Initial Pressure", vec[n++].toString(), "MPa"); m_reservoirType = nmDataAttribute("Reservoir Type", vec[n++].toString(), ""); m_Bo = nmDataAttribute("Bo", vec[n++].toString(), ""); - m_Miuo = nmDataAttribute("Miuo", vec[n++].toString(), "mPa.s(cp)"); - m_permeability = nmDataAttribute("Permeability", vec[n++].toString(), "mD"); + m_Miuo = nmDataAttribute("Miuo", vec[n++].toString(), "mPa.s"); + m_permeability = nmDataAttribute("Permeability", vec[n++].toString(), "D"); m_thickness = nmDataAttribute("Thickness", vec[n++].toString(), "m"); m_porosity = nmDataAttribute("Porosity", vec[n++].toString(), ""); - m_Ct = nmDataAttribute("Ct", vec[n++].toString(), ""); + m_Ct = nmDataAttribute("Ct", vec[n++].toString(), "1/MPa"); m_kxKy = nmDataAttribute("Kx/Ky", vec[n++].toString(), ""); return true; } @@ -280,11 +283,12 @@ void nmDataReservoir::onDeserialize(ZxSerializer* ser) { ser->read("InitialPressure", tempValue); m_initialPressure = nmDataAttribute("Initial Pressure", tempValue, "MPa"); ser->read("ReservoirType", tempValue); m_reservoirType = nmDataAttribute("Reservoir Type", tempValue, ""); ser->read("Bo", tempValue); m_Bo = nmDataAttribute("Bo", tempValue, ""); - ser->read("Miuo", tempValue); m_Miuo = nmDataAttribute("Miuo", tempValue, "mPa.s(cp)"); - ser->read("Permeability", tempValue); m_permeability = nmDataAttribute("Permeability", tempValue, "mD"); + ser->read("Miuo", tempValue); m_Miuo = nmDataAttribute("Miuo", tempValue, "mPa.s"); + // 序列化格式只保存数值,读取时明确按求解器基准单位D解释。 + ser->read("Permeability", tempValue); m_permeability = nmDataAttribute("Permeability", tempValue, "D"); ser->read("Thickness", tempValue); m_thickness = nmDataAttribute("Thickness", tempValue, "m"); ser->read("Porosity", tempValue); m_porosity = nmDataAttribute("Porosity", tempValue, ""); - ser->read("NetToGross", tempValue); m_Ct = nmDataAttribute("Ct", tempValue, ""); + ser->read("Ct", tempValue); m_Ct = nmDataAttribute("Ct", tempValue, "1/MPa"); ser->read("KxKy", tempValue); m_kxKy = nmDataAttribute("Kx/Ky", tempValue, ""); } diff --git a/Src/nmNum/nmSubWnd/nmSubWndMain.cpp b/Src/nmNum/nmSubWnd/nmSubWndMain.cpp index 1f2b236..c955245 100644 --- a/Src/nmNum/nmSubWnd/nmSubWndMain.cpp +++ b/Src/nmNum/nmSubWnd/nmSubWndMain.cpp @@ -56,7 +56,8 @@ #include "nmWxRCDialog.h" #include "nmWxWellLayerDlg.h" #include "nmWxGeometryLayerDlg.h" -#include "nmWxReservoirPropertiesDlg.h" +#include "nmReservoirPropertiesDataSource.h" +#include "nmWxReservoirPropertiesEditor.h" #include "nmWxPropertyInterpolationDlg.h" #include "nmWxPostprocessingAnimationWidget.h" #include "nmWxGeoRefDlg.h" @@ -1961,10 +1962,29 @@ bool nmSubWndMain::onConfirmClosing() void nmSubWndMain::reservoirCharacteristics() { - nmWxReservoirPropertiesDlg dlg; - dlg.exec(); + // 第一步:取得当前分析窗口对应的数据管理器,避免新对话框访问全局单例。 + nmDataAnalyzeManager* pDataManager = nmDataAnalyzeManager::getCurrentInstance(); + if (pDataManager == nullptr) + { + QMessageBox::warning(this, tr("Reservoir properties"), + tr("The current numerical well-test data is unavailable.")); + return; + } + + // 第二步:在入口处组装数据源和编辑器,两个对象的生命周期覆盖整个模态会话。 + nmAnalyzeManagerReservoirDataSource oDataSource(pDataManager); + nmWxReservoirPropertiesEditor oEditor(&oDataSource, this); + + // 第三步:先建立工作副本;初始化失败时不打开不完整的参数界面。 + QString sError; + if (!oEditor.initialize(sError)) + { + QMessageBox::warning(this, tr("Reservoir properties"), sError); + return; + } - // TODO: 修改完参数应该刷新画布 + // 第四步:确定时由编辑会话一次性提交,取消时真实数据保持不变。 + oEditor.exec(); } void nmSubWndMain::geoLayering() diff --git a/Src/nmNum/nmSubWxs/nmReservoirParameterCatalog.cpp b/Src/nmNum/nmSubWxs/nmReservoirParameterCatalog.cpp new file mode 100644 index 0000000..4115645 --- /dev/null +++ b/Src/nmNum/nmSubWxs/nmReservoirParameterCatalog.cpp @@ -0,0 +1,852 @@ +#include "nmReservoirParameterCatalog.h" + +#include "nmReservoirPropertiesDataSource.h" +#include "nmAttrRegistry.h" +#include "nmDataAttribute.h" +#include "nmDataPerforation.h" + +#include + +namespace +{ + +/* + * Qt 4.8的lupdate无法跟踪editorText()的间接参数,以下标记只用于 + * 收集中英文资源;运行时仍由同一翻译上下文返回文字。 + */ +static const char* const s_pCatalogTranslationMarkers[] = +{ + QT_TRANSLATE_NOOP("nmWxReservoirPropertiesEditor", "Reservoir type"), + QT_TRANSLATE_NOOP("nmWxReservoirPropertiesEditor", "Homogeneous"), + QT_TRANSLATE_NOOP("nmWxReservoirPropertiesEditor", "Dual porosity pseudo steady state"), + QT_TRANSLATE_NOOP("nmWxReservoirPropertiesEditor", "Fault flow model"), + QT_TRANSLATE_NOOP("nmWxReservoirPropertiesEditor", "Region flow model"), + QT_TRANSLATE_NOOP("nmWxReservoirPropertiesEditor", "Leaky"), + QT_TRANSLATE_NOOP("nmWxReservoirPropertiesEditor", "Composite limit"), + QT_TRANSLATE_NOOP("nmWxReservoirPropertiesEditor", "Fracture flow model"), + QT_TRANSLATE_NOOP("nmWxReservoirPropertiesEditor", "Finite conductivity"), + QT_TRANSLATE_NOOP("nmWxReservoirPropertiesEditor", "Infinite conductivity"), + QT_TRANSLATE_NOOP("nmWxReservoirPropertiesEditor", "Position and wellbore"), + QT_TRANSLATE_NOOP("nmWxReservoirPropertiesEditor", "Storage and fracture"), + QT_TRANSLATE_NOOP("nmWxReservoirPropertiesEditor", "Basic properties"), + QT_TRANSLATE_NOOP("nmWxReservoirPropertiesEditor", "Fluid parameters"), + QT_TRANSLATE_NOOP("nmWxReservoirPropertiesEditor", "Compression and saturation"), + QT_TRANSLATE_NOOP("nmWxReservoirPropertiesEditor", "Reservoir parameters"), + QT_TRANSLATE_NOOP("nmWxReservoirPropertiesEditor", "Vertical well"), + QT_TRANSLATE_NOOP("nmWxReservoirPropertiesEditor", "Vertical fractured well"), + QT_TRANSLATE_NOOP("nmWxReservoirPropertiesEditor", "Horizontal fractured well"), + QT_TRANSLATE_NOOP("nmWxReservoirPropertiesEditor", "Dual-medium parameters"), + QT_TRANSLATE_NOOP("nmWxReservoirPropertiesEditor", "Geometry"), + QT_TRANSLATE_NOOP("nmWxReservoirPropertiesEditor", "Flow properties"), + QT_TRANSLATE_NOOP("nmWxReservoirPropertiesEditor", "Conductivity properties"), + QT_TRANSLATE_NOOP("nmWxReservoirPropertiesEditor", "Region properties") +}; + +/** @brief 返回当前语言下的界面文字。 */ +QString editorText(const char* pText) +{ + Q_UNUSED(s_pCatalogTranslationMarkers); + return QCoreApplication::translate("nmWxReservoirPropertiesEditor", pText); +} + +/** @brief 创建一个稳定枚举选项。 */ +nmReservoirEnumOption enumOption(const QString& sCode, const char* pText) +{ + nmReservoirEnumOption oOption; + oOption.m_sCode = sCode; + oOption.m_sText = editorText(pText); + return oOption; +} + +/** @brief 创建一个数值字段描述。 */ +nmReservoirParameterFieldInfo numericField( + const QString& sParameterId, + const QString& sBaseParameterId, + nmReservoirParameterEditorType eEditorType) +{ + nmReservoirParameterFieldInfo oField; + oField.m_sParameterId = sParameterId; + oField.m_sBaseParameterId = sBaseParameterId; + oField.m_eEditorType = eEditorType; + oField.m_bUnitEnabled = true; + return oField; +} + +/** @brief 创建一个浮点数字段描述。 */ +nmReservoirParameterFieldInfo numericField( + const QString& sParameterId, + const QString& sBaseParameterId) +{ + return numericField(sParameterId, sBaseParameterId, + NM_RESERVOIR_EDITOR_NUMBER); +} + +/** @brief 创建一个明确不显示单位的浮点数字段。 */ +nmReservoirParameterFieldInfo unitlessNumericField( + const QString& sParameterId, + const QString& sBaseParameterId) +{ + nmReservoirParameterFieldInfo oField = + numericField(sParameterId, sBaseParameterId); + oField.m_bUnitEnabled = false; + return oField; +} + +/** @brief 创建储层类型字段。 */ +nmReservoirParameterFieldInfo reservoirModelField( + const QString& sParameterId) +{ + nmReservoirParameterFieldInfo oField; + oField.m_sParameterId = sParameterId; + oField.m_sLabel = editorText("Reservoir type"); + oField.m_eEditorType = NM_RESERVOIR_EDITOR_ENUM; + oField.m_bUnitEnabled = false; + oField.m_listOptions.append(enumOption("homogeneous", "Homogeneous")); + oField.m_listOptions.append(enumOption( + "dual_porosity_pss", "Dual porosity pseudo steady state")); + return oField; +} + +/** @brief 创建断层或复合区流动模型字段。 */ +nmReservoirParameterFieldInfo leakageModelField( + const QString& sParameterId, + const char* pLabel) +{ + nmReservoirParameterFieldInfo oField; + oField.m_sParameterId = sParameterId; + oField.m_sLabel = editorText(pLabel); + oField.m_eEditorType = NM_RESERVOIR_EDITOR_ENUM; + oField.m_bUnitEnabled = false; + oField.m_listOptions.append(enumOption("leaky", "Leaky")); + oField.m_listOptions.append(enumOption( + "composite_limit", "Composite limit")); + return oField; +} + +/** @brief 创建裂缝导流模型字段。 */ +nmReservoirParameterFieldInfo fractureModelField( + const QString& sParameterId) +{ + nmReservoirParameterFieldInfo oField; + oField.m_sParameterId = sParameterId; + oField.m_sLabel = editorText("Fracture flow model"); + oField.m_eEditorType = NM_RESERVOIR_EDITOR_ENUM; + oField.m_bUnitEnabled = false; + oField.m_listOptions.append(enumOption( + "finite_conductivity", "Finite conductivity")); + oField.m_listOptions.append(enumOption( + "infinite_conductivity", "Infinite conductivity")); + return oField; +} + +/** @brief 创建页面上的一个参数分组。 */ +nmReservoirParameterGroupInfo parameterGroup( + const char* pTitle, + int nRow, + int nColumn, + int nColumnSpan) +{ + nmReservoirParameterGroupInfo oGroup; + oGroup.m_sTitle = editorText(pTitle); + oGroup.m_nRow = nRow; + oGroup.m_nColumn = nColumn; + oGroup.m_nRowSpan = 1; + oGroup.m_nColumnSpan = nColumnSpan; + oGroup.m_bValueRightAligned = false; + return oGroup; +} + +/** @brief 为参数ID添加对象编码前缀。 */ +QString prefixed(const QString& sObjectCode, const char* pParameterId) +{ + return sObjectCode + "_" + QString::fromLatin1(pParameterId); +} + +/** @brief 生成无井编码时的会话编码。 */ +QString wellCode(const nmDataWellBase& oWell, + const QString& sFallbackPrefix, + int nIndex) +{ + QString sCode = oWell.getWellCode(); + if (sCode.isEmpty()) + { + sCode = QString("%1%2").arg(sFallbackPrefix) + .arg(nIndex + 1, 4, 10, QChar('0')); + } + return sCode; +} + +/** @brief 创建井对象的分组描述。 */ +void createWellGroups(nmReservoirPropertyObjectInfo& oInfo, + NM_WELL_MODEL eWellType, + bool bHasPerforation) +{ + nmReservoirParameterGroupInfo oPosition = + parameterGroup("Position and wellbore", 0, 0, 1); + oPosition.m_listFields + << numericField(prefixed(oInfo.m_sObjectCode, "W_X"), "W_X") + << numericField(prefixed(oInfo.m_sObjectCode, "W_Y"), "W_Y") + << numericField(prefixed(oInfo.m_sObjectCode, "W_Rw"), "W_Rw"); + if (bHasPerforation) + { + oPosition.m_listFields.append(numericField( + prefixed(oInfo.m_sObjectCode, "W_Skin"), "W_Skin")); + } + + nmReservoirParameterGroupInfo oStorage = + parameterGroup("Storage and fracture", 0, 1, 1); + oStorage.m_listFields.append(numericField( + prefixed(oInfo.m_sObjectCode, "W_C"), "W_C")); + if (eWellType == Vertical_Fractured_Well) + { + oStorage.m_listFields + << numericField(prefixed(oInfo.m_sObjectCode, + "W_FractureHalfLength"), + "W_FractureHalfLength") + << numericField(prefixed(oInfo.m_sObjectCode, + "W_FractureAngle"), + "W_FractureAngle") + << numericField(prefixed(oInfo.m_sObjectCode, + "W_Dfc"), "W_Dfc"); + } + else if (eWellType == Horizontal_Fractured_Well) + { + oStorage.m_listFields + << numericField(prefixed(oInfo.m_sObjectCode, + "W_WellLength"), + "W_WellLength") + << numericField(prefixed(oInfo.m_sObjectCode, + "W_DrainAngle"), + "W_DrainAngle") + << numericField(prefixed(oInfo.m_sObjectCode, + "W_NumberOfFractures"), + "W_NumberOfFractures", + NM_RESERVOIR_EDITOR_INTEGER) + << numericField(prefixed(oInfo.m_sObjectCode, + "W_FractureHalfLength"), + "W_FractureHalfLength") + << numericField(prefixed(oInfo.m_sObjectCode, + "W_FractureAngle"), + "W_FractureAngle") + << numericField(prefixed(oInfo.m_sObjectCode, + "W_Dfc"), "W_Dfc"); + } + oInfo.m_listGroups << oPosition << oStorage; +} + +/** @brief 注册一口井的全部可编辑数值属性。 */ +void registerWell(nmAttrRegistry* pRegistry, + nmDataWellBase& oWell, + const QString& sCode) +{ + pRegistry->regAttr(prefixed(sCode, "W_X"), &oWell.getX()); + pRegistry->regAttr(prefixed(sCode, "W_Y"), &oWell.getY()); + pRegistry->regAttr(prefixed(sCode, "W_Rw"), &oWell.getRadius()); + pRegistry->regAttr(prefixed(sCode, "W_C"), &oWell.getWellboreStorage()); + if (oWell.getPerforationCount() > 0 && + oWell.getPerforation(0) != nullptr) + { + pRegistry->regAttr(prefixed(sCode, "W_Skin"), + &oWell.getPerforation(0)->getSkin()); + } +} + +/** @brief 判断对象描述中是否包含指定枚举字段。 */ +bool containsEnumField(const nmReservoirPropertyObjectInfo& oInfo, + const QString& sParameterId) +{ + for (int nGroupIndex = 0; + nGroupIndex < oInfo.m_listGroups.size(); + ++nGroupIndex) + { + const nmReservoirParameterGroupInfo& oGroup = + oInfo.m_listGroups[nGroupIndex]; + for (int nFieldIndex = 0; + nFieldIndex < oGroup.m_listFields.size(); + ++nFieldIndex) + { + const nmReservoirParameterFieldInfo& oField = + oGroup.m_listFields[nFieldIndex]; + if (oField.m_eEditorType == NM_RESERVOIR_EDITOR_ENUM && + oField.m_sParameterId == sParameterId) + { + return true; + } + } + } + return false; +} + +} + +void nmReservoirParameterCatalog::registerSnapshot( + nmAttrRegistry* pRegistry, + nmReservoirPropertiesSnapshot& oSnapshot) +{ + if (pRegistry == nullptr) + { + return; + } + + // 第一步:重新建立编辑会话独占的注册表,杜绝与实时数据交叉连接。 + pRegistry->clear(); + pRegistry->regAttr("h", &oSnapshot.m_oReservoir.getThickness()); + pRegistry->regAttr("Pi", &oSnapshot.m_oReservoir.getInitialPressure()); + pRegistry->regAttr("K", &oSnapshot.m_oReservoir.getPermeability()); + pRegistry->regAttr("phi", &oSnapshot.m_oReservoir.getPorosity()); + pRegistry->regAttr("Cti", &oSnapshot.m_oReservoir.getCt()); + pRegistry->regAttr("Cf", &oSnapshot.m_oReservoir.getCf()); + pRegistry->regAttr("Soi", &oSnapshot.m_oReservoir.getSoi()); + pRegistry->regAttr("Swi", &oSnapshot.m_oReservoir.getSwi()); + pRegistry->regAttr("Bo", &oSnapshot.m_oReservoir.getBo()); + pRegistry->regAttr("Miuo", &oSnapshot.m_oReservoir.getMiuo()); + pRegistry->regAttr("KxKy", &oSnapshot.m_oReservoir.getKxKy()); + + // 第二步:注册求解器支持的三类井及其压裂参数。 + for (int nIndex = 0; + nIndex < oSnapshot.m_vecVerticalWells.size(); + ++nIndex) + { + nmDataVerticalWell& oWell = oSnapshot.m_vecVerticalWells[nIndex]; + registerWell(pRegistry, oWell, wellCode(oWell, "VWELL", nIndex)); + } + for (int nIndex = 0; + nIndex < oSnapshot.m_vecVerticalFracturedWells.size(); + ++nIndex) + { + nmDataVerticalFracturedWell& oWell = + oSnapshot.m_vecVerticalFracturedWells[nIndex]; + QString sCode = wellCode(oWell, "VFWELL", nIndex); + registerWell(pRegistry, oWell, sCode); + pRegistry->regAttr(prefixed(sCode, "W_FractureHalfLength"), + &oWell.getFractureHalfLength()); + pRegistry->regAttr(prefixed(sCode, "W_FractureAngle"), + &oWell.getFractureAngle()); + pRegistry->regAttr(prefixed(sCode, "W_Dfc"), &oWell.getDfc()); + } + for (int nIndex = 0; + nIndex < oSnapshot.m_vecHorizontalFracturedWells.size(); + ++nIndex) + { + nmDataHorizontalFracturedWell& oWell = + oSnapshot.m_vecHorizontalFracturedWells[nIndex]; + QString sCode = wellCode(oWell, "HFWELL", nIndex); + registerWell(pRegistry, oWell, sCode); + pRegistry->regAttr(prefixed(sCode, "W_WellLength"), + &oWell.getWellLength()); + pRegistry->regAttr(prefixed(sCode, "W_DrainAngle"), + &oWell.getDrainAngle()); + pRegistry->regAttr(prefixed(sCode, "W_NumberOfFractures"), + &oWell.getNumberOfFractures()); + pRegistry->regAttr(prefixed(sCode, "W_FractureHalfLength"), + &oWell.getFractureHalfLength()); + pRegistry->regAttr(prefixed(sCode, "W_FractureAngle"), + &oWell.getFractureAngle()); + pRegistry->regAttr(prefixed(sCode, "W_Dfc"), &oWell.getDfc()); + } + + // 第三步:注册区域标记和复合区数值参数。 + for (int nIndex = 0; + nIndex < oSnapshot.m_vecRegionMarks.size(); + ++nIndex) + { + QString sCode = QString("RMARK%1") + .arg(nIndex + 1, 4, 10, QChar('0')); + nmDataRegionMark& oMark = oSnapshot.m_vecRegionMarks[nIndex]; + pRegistry->regAttr(prefixed(sCode, "RM_ComW"), &oMark.getComW()); + pRegistry->regAttr(prefixed(sCode, "RM_ComKr"), &oMark.getComKr()); + pRegistry->regAttr(prefixed(sCode, "RM_NetToGross"), + &oMark.getNetToGross()); + } + for (int nIndex = 0; + nIndex < oSnapshot.m_vecRegions.size(); + ++nIndex) + { + QString sCode = QString("REGION%1") + .arg(nIndex + 1, 4, 10, QChar('0')); + pRegistry->regAttr(prefixed(sCode, "R_Leakage"), + &oSnapshot.m_vecRegions[nIndex] + .getRegionLeakage()); + } + + // 第四步:注册断层和普通裂缝;DFN由专用功能维护。 + for (int nIndex = 0; + nIndex < oSnapshot.m_vecFaults.size(); + ++nIndex) + { + QString sCode = QString("FAULT%1") + .arg(nIndex + 1, 4, 10, QChar('0')); + nmDataFault& oFault = oSnapshot.m_vecFaults[nIndex]; + pRegistry->regAttr(prefixed(sCode, "FT_X0"), &oFault.getStartX()); + pRegistry->regAttr(prefixed(sCode, "FT_Y0"), &oFault.getStartY()); + pRegistry->regAttr(prefixed(sCode, "FT_X1"), &oFault.getEndX()); + pRegistry->regAttr(prefixed(sCode, "FT_Y1"), &oFault.getEndY()); + pRegistry->regAttr(prefixed(sCode, "FT_Leakage"), + &oFault.getFaultLeakage()); + } + + int nVisibleFracture = 0; + for (int nIndex = 0; + nIndex < oSnapshot.m_vecFractures.size(); + ++nIndex) + { + nmDataFracture& oFracture = oSnapshot.m_vecFractures[nIndex]; + if (oFracture.getFractureType().getValue().toString() == "DFN") + { + continue; + } + QString sCode = QString("FRAC%1") + .arg(++nVisibleFracture, 4, 10, QChar('0')); + pRegistry->regAttr(prefixed(sCode, "F_X0"), + &oFracture.getStartX()); + pRegistry->regAttr(prefixed(sCode, "F_Y0"), + &oFracture.getStartY()); + pRegistry->regAttr(prefixed(sCode, "F_X1"), + &oFracture.getEndX()); + pRegistry->regAttr(prefixed(sCode, "F_Y1"), + &oFracture.getEndY()); + pRegistry->regAttr(prefixed(sCode, "F_FC"), + &oFracture.getFractureDfc()); + pRegistry->regAttr(prefixed(sCode, "F_DW"), + &oFracture.getFractureDw()); + } +} + +QList +nmReservoirParameterCatalog::createObjectInfos( + nmReservoirPropertiesCategory eCategory, + nmReservoirPropertiesSnapshot& oSnapshot) +{ + QList listInfos; + + if (eCategory == NM_RESERVOIR_PAGE_RESERVOIR) + { + nmReservoirPropertyObjectInfo oInfo; + oInfo.m_eCategory = eCategory; + oInfo.m_eObjectType = NM_RESERVOIR_OBJECT_RESERVOIR; + oInfo.m_nSourceIndex = 0; + oInfo.m_sObjectCode = "RESERVOIR"; + oInfo.m_sDisplayName = editorText("Reservoir parameters"); + + nmReservoirParameterGroupInfo oBasic = + parameterGroup("Basic properties", 0, 0, 1); + oBasic.m_nRowSpan = 2; + oBasic.m_bValueRightAligned = true; + oBasic.m_listFields + << numericField("Pi", "Pi") + << numericField("K", "K") + << numericField("h", "h") + << numericField("phi", "phi"); + + nmReservoirParameterGroupInfo oFluid = + parameterGroup("Fluid parameters", 0, 1, 1); + oFluid.m_listFields + << reservoirModelField("ReservoirType") + << numericField("Miuo", "Miuo") + << unitlessNumericField("Bo", "Bo") + << numericField("KxKy", "KxKy"); + + nmReservoirParameterGroupInfo oCompression = + parameterGroup("Compression and saturation", 1, 1, 1); + oCompression.m_listFields + << numericField("Cti", "Cti") + << numericField("Cf", "Cf") + << numericField("Soi", "Soi") + << numericField("Swi", "Swi"); + oInfo.m_listGroups << oBasic << oFluid << oCompression; + listInfos.append(oInfo); + return listInfos; + } + + if (eCategory == NM_RESERVOIR_PAGE_WELL) + { + for (int nIndex = 0; + nIndex < oSnapshot.m_vecVerticalWells.size(); + ++nIndex) + { + nmDataVerticalWell& oWell = + oSnapshot.m_vecVerticalWells[nIndex]; + nmReservoirPropertyObjectInfo oInfo; + oInfo.m_eCategory = eCategory; + oInfo.m_eObjectType = NM_RESERVOIR_OBJECT_VERTICAL_WELL; + oInfo.m_nSourceIndex = nIndex; + oInfo.m_sObjectCode = wellCode(oWell, "VWELL", nIndex); + oInfo.m_sDisplayName = oWell.getWellName() + " - " + + editorText("Vertical well"); + createWellGroups(oInfo, Vertical_Well, + oWell.getPerforationCount() > 0); + listInfos.append(oInfo); + } + for (int nIndex = 0; + nIndex < oSnapshot.m_vecVerticalFracturedWells.size(); + ++nIndex) + { + nmDataVerticalFracturedWell& oWell = + oSnapshot.m_vecVerticalFracturedWells[nIndex]; + nmReservoirPropertyObjectInfo oInfo; + oInfo.m_eCategory = eCategory; + oInfo.m_eObjectType = + NM_RESERVOIR_OBJECT_VERTICAL_FRAC_WELL; + oInfo.m_nSourceIndex = nIndex; + oInfo.m_sObjectCode = wellCode(oWell, "VFWELL", nIndex); + oInfo.m_sDisplayName = oWell.getWellName() + " - " + + editorText("Vertical fractured well"); + createWellGroups(oInfo, Vertical_Fractured_Well, + oWell.getPerforationCount() > 0); + listInfos.append(oInfo); + } + for (int nIndex = 0; + nIndex < oSnapshot.m_vecHorizontalFracturedWells.size(); + ++nIndex) + { + nmDataHorizontalFracturedWell& oWell = + oSnapshot.m_vecHorizontalFracturedWells[nIndex]; + nmReservoirPropertyObjectInfo oInfo; + oInfo.m_eCategory = eCategory; + oInfo.m_eObjectType = + NM_RESERVOIR_OBJECT_HORIZONTAL_FRAC_WELL; + oInfo.m_nSourceIndex = nIndex; + oInfo.m_sObjectCode = wellCode(oWell, "HFWELL", nIndex); + oInfo.m_sDisplayName = oWell.getWellName() + " - " + + editorText("Horizontal fractured well"); + createWellGroups(oInfo, Horizontal_Fractured_Well, + oWell.getPerforationCount() > 0); + listInfos.append(oInfo); + } + return listInfos; + } + + if (eCategory == NM_RESERVOIR_PAGE_REGION_MARK) + { + for (int nIndex = 0; + nIndex < oSnapshot.m_vecRegionMarks.size(); + ++nIndex) + { + nmReservoirPropertyObjectInfo oInfo; + oInfo.m_eCategory = eCategory; + oInfo.m_eObjectType = NM_RESERVOIR_OBJECT_REGION_MARK; + oInfo.m_nSourceIndex = nIndex; + oInfo.m_sObjectCode = QString("RMARK%1") + .arg(nIndex + 1, 4, 10, QChar('0')); + oInfo.m_sDisplayName = + oSnapshot.m_vecRegionMarks[nIndex].getRegionMarkName(); + + nmReservoirParameterGroupInfo oDual = + parameterGroup("Dual-medium parameters", 0, 0, 2); + oDual.m_listFields + << reservoirModelField(prefixed( + oInfo.m_sObjectCode, "RM_ReservoirType")) + << numericField(prefixed( + oInfo.m_sObjectCode, "RM_ComW"), "RM_ComW") + << numericField(prefixed( + oInfo.m_sObjectCode, "RM_ComKr"), "RM_ComKr") + << numericField(prefixed( + oInfo.m_sObjectCode, "RM_NetToGross"), + "RM_NetToGross"); + oInfo.m_listGroups.append(oDual); + listInfos.append(oInfo); + } + return listInfos; + } + + if (eCategory == NM_RESERVOIR_PAGE_FAULT) + { + for (int nIndex = 0; + nIndex < oSnapshot.m_vecFaults.size(); + ++nIndex) + { + nmReservoirPropertyObjectInfo oInfo; + oInfo.m_eCategory = eCategory; + oInfo.m_eObjectType = NM_RESERVOIR_OBJECT_FAULT; + oInfo.m_nSourceIndex = nIndex; + oInfo.m_sObjectCode = QString("FAULT%1") + .arg(nIndex + 1, 4, 10, QChar('0')); + oInfo.m_sDisplayName = + oSnapshot.m_vecFaults[nIndex].getFaultName(); + + nmReservoirParameterGroupInfo oGeometry = + parameterGroup("Geometry", 0, 0, 1); + oGeometry.m_listFields + << numericField(prefixed( + oInfo.m_sObjectCode, "FT_X0"), "FT_X0") + << numericField(prefixed( + oInfo.m_sObjectCode, "FT_Y0"), "FT_Y0") + << numericField(prefixed( + oInfo.m_sObjectCode, "FT_X1"), "FT_X1") + << numericField(prefixed( + oInfo.m_sObjectCode, "FT_Y1"), "FT_Y1"); + + nmReservoirParameterGroupInfo oFlow = + parameterGroup("Flow properties", 0, 1, 1); + oFlow.m_listFields + << leakageModelField(prefixed( + oInfo.m_sObjectCode, "FT_FlowModel"), + "Fault flow model") + << numericField(prefixed( + oInfo.m_sObjectCode, "FT_Leakage"), + "FT_Leakage"); + oInfo.m_listGroups << oGeometry << oFlow; + listInfos.append(oInfo); + } + return listInfos; + } + + if (eCategory == NM_RESERVOIR_PAGE_FRACTURE) + { + int nVisibleFracture = 0; + for (int nIndex = 0; + nIndex < oSnapshot.m_vecFractures.size(); + ++nIndex) + { + nmDataFracture& oFracture = + oSnapshot.m_vecFractures[nIndex]; + if (oFracture.getFractureType().getValue().toString() == + "DFN") + { + continue; + } + nmReservoirPropertyObjectInfo oInfo; + oInfo.m_eCategory = eCategory; + oInfo.m_eObjectType = NM_RESERVOIR_OBJECT_FRACTURE; + oInfo.m_nSourceIndex = nIndex; + oInfo.m_sObjectCode = QString("FRAC%1") + .arg(++nVisibleFracture, 4, 10, QChar('0')); + oInfo.m_sDisplayName = oFracture.getFractureName(); + + nmReservoirParameterGroupInfo oGeometry = + parameterGroup("Geometry", 0, 0, 1); + oGeometry.m_listFields + << numericField(prefixed( + oInfo.m_sObjectCode, "F_X0"), "F_X0") + << numericField(prefixed( + oInfo.m_sObjectCode, "F_Y0"), "F_Y0") + << numericField(prefixed( + oInfo.m_sObjectCode, "F_X1"), "F_X1") + << numericField(prefixed( + oInfo.m_sObjectCode, "F_Y1"), "F_Y1"); + + nmReservoirParameterGroupInfo oFlow = + parameterGroup("Conductivity properties", 0, 1, 1); + oFlow.m_listFields + << fractureModelField(prefixed( + oInfo.m_sObjectCode, "F_FlowModel")) + << numericField(prefixed( + oInfo.m_sObjectCode, "F_FC"), "F_FC") + << numericField(prefixed( + oInfo.m_sObjectCode, "F_DW"), "F_DW"); + oInfo.m_listGroups << oGeometry << oFlow; + listInfos.append(oInfo); + } + return listInfos; + } + + if (eCategory == NM_RESERVOIR_PAGE_COMPOSITE_REGION) + { + for (int nIndex = 0; + nIndex < oSnapshot.m_vecRegions.size(); + ++nIndex) + { + nmReservoirPropertyObjectInfo oInfo; + oInfo.m_eCategory = eCategory; + oInfo.m_eObjectType = NM_RESERVOIR_OBJECT_REGION; + oInfo.m_nSourceIndex = nIndex; + oInfo.m_sObjectCode = QString("REGION%1") + .arg(nIndex + 1, 4, 10, QChar('0')); + oInfo.m_sDisplayName = + oSnapshot.m_vecRegions[nIndex].getRegoinName(); + + nmReservoirParameterGroupInfo oRegion = + parameterGroup("Region properties", 0, 0, 2); + oRegion.m_listFields + << leakageModelField(prefixed( + oInfo.m_sObjectCode, "R_FlowModel"), + "Region flow model") + << numericField(prefixed( + oInfo.m_sObjectCode, "R_Leakage"), + "R_Leakage"); + oInfo.m_listGroups.append(oRegion); + listInfos.append(oInfo); + } + } + return listInfos; +} + +QString nmReservoirParameterCatalog::enumCodeFromStoredValue( + const QString& sParameterId, + const QString& sStoredValue) +{ + QString sValue = sStoredValue.trimmed().toLower(); + if (sParameterId.endsWith("ReservoirType")) + { + return sValue.contains("dual") + ? "dual_porosity_pss" : "homogeneous"; + } + if (sParameterId.endsWith("F_FlowModel")) + { + return sValue.contains("infinite") + ? "infinite_conductivity" : "finite_conductivity"; + } + if (sParameterId.endsWith("FlowModel")) + { + return sValue.contains("leaky") ? "leaky" : "composite_limit"; + } + return QString(); +} + +QString nmReservoirParameterCatalog::storedValueFromEnumCode( + const QString& sParameterId, + const QString& sCode) +{ + if (sParameterId.endsWith("ReservoirType")) + { + return sCode == "dual_porosity_pss" + ? "Dual porosity pseudo steady state" : "Homogeneous"; + } + if (sParameterId.endsWith("F_FlowModel")) + { + return sCode == "infinite_conductivity" + ? "Infinite Conductivity" : "Finite Conductivity"; + } + if (sParameterId.endsWith("FlowModel")) + { + return sCode == "leaky" ? "Leaky" : "Composite limit"; + } + return QString(); +} + +QString nmReservoirParameterCatalog::enumCode( + const nmReservoirPropertiesSnapshot& oSnapshot, + const QString& sParameterId) +{ + if (sParameterId == "ReservoirType") + { + nmDataReservoir& oReservoir = + const_cast(oSnapshot.m_oReservoir); + return enumCodeFromStoredValue( + sParameterId, + oReservoir.getReservoirType().getValue().toString()); + } + + QList listAll; + nmReservoirPropertiesSnapshot& oWritable = + const_cast(oSnapshot); + listAll << createObjectInfos(NM_RESERVOIR_PAGE_FAULT, + oWritable) + << createObjectInfos(NM_RESERVOIR_PAGE_FRACTURE, + oWritable) + << createObjectInfos(NM_RESERVOIR_PAGE_COMPOSITE_REGION, + oWritable) + << createObjectInfos(NM_RESERVOIR_PAGE_REGION_MARK, + oWritable); + for (int nInfoIndex = 0; + nInfoIndex < listAll.size(); + ++nInfoIndex) + { + const nmReservoirPropertyObjectInfo& oInfo = + listAll[nInfoIndex]; + if (!containsEnumField(oInfo, sParameterId)) + { + continue; + } + if (oInfo.m_eObjectType == NM_RESERVOIR_OBJECT_REGION_MARK) + { + nmDataRegionMark& oMark = + oWritable.m_vecRegionMarks[oInfo.m_nSourceIndex]; + return enumCodeFromStoredValue( + sParameterId, + oMark.getReservoirType().getValue().toString()); + } + if (oInfo.m_eObjectType == NM_RESERVOIR_OBJECT_FAULT) + { + nmDataFault& oFault = + oWritable.m_vecFaults[oInfo.m_nSourceIndex]; + return enumCodeFromStoredValue( + sParameterId, + oFault.getFaultFlowModel().getValue().toString()); + } + if (oInfo.m_eObjectType == NM_RESERVOIR_OBJECT_FRACTURE) + { + nmDataFracture& oFracture = + oWritable.m_vecFractures[oInfo.m_nSourceIndex]; + return enumCodeFromStoredValue( + sParameterId, + oFracture.getFractureFlowModel() + .getValue().toString()); + } + if (oInfo.m_eObjectType == NM_RESERVOIR_OBJECT_REGION) + { + nmDataRegion& oRegion = + oWritable.m_vecRegions[oInfo.m_nSourceIndex]; + return enumCodeFromStoredValue( + sParameterId, + oRegion.getRegionFlowModel().getValue().toString()); + } + } + return QString(); +} + +bool nmReservoirParameterCatalog::setEnumCode( + nmReservoirPropertiesSnapshot& oSnapshot, + const QString& sParameterId, + const QString& sCode) +{ + QString sStoredValue = + storedValueFromEnumCode(sParameterId, sCode); + if (sStoredValue.isEmpty()) + { + return false; + } + + if (sParameterId == "ReservoirType") + { + oSnapshot.m_oReservoir.getReservoirType() + .setValue(sStoredValue); + return true; + } + + QList listAll; + listAll << createObjectInfos(NM_RESERVOIR_PAGE_FAULT, + oSnapshot) + << createObjectInfos(NM_RESERVOIR_PAGE_FRACTURE, + oSnapshot) + << createObjectInfos(NM_RESERVOIR_PAGE_COMPOSITE_REGION, + oSnapshot) + << createObjectInfos(NM_RESERVOIR_PAGE_REGION_MARK, + oSnapshot); + for (int nInfoIndex = 0; + nInfoIndex < listAll.size(); + ++nInfoIndex) + { + const nmReservoirPropertyObjectInfo& oInfo = + listAll[nInfoIndex]; + if (!containsEnumField(oInfo, sParameterId)) + { + continue; + } + if (oInfo.m_eObjectType == NM_RESERVOIR_OBJECT_REGION_MARK) + { + oSnapshot.m_vecRegionMarks[oInfo.m_nSourceIndex] + .getReservoirType().setValue(sStoredValue); + return true; + } + if (oInfo.m_eObjectType == NM_RESERVOIR_OBJECT_FAULT) + { + oSnapshot.m_vecFaults[oInfo.m_nSourceIndex] + .getFaultFlowModel().setValue(sStoredValue); + return true; + } + if (oInfo.m_eObjectType == NM_RESERVOIR_OBJECT_FRACTURE) + { + oSnapshot.m_vecFractures[oInfo.m_nSourceIndex] + .getFractureFlowModel().setValue(sStoredValue); + return true; + } + if (oInfo.m_eObjectType == NM_RESERVOIR_OBJECT_REGION) + { + oSnapshot.m_vecRegions[oInfo.m_nSourceIndex] + .getRegionFlowModel().setValue(sStoredValue); + return true; + } + } + return false; +} diff --git a/Src/nmNum/nmSubWxs/nmReservoirParameterField.cpp b/Src/nmNum/nmSubWxs/nmReservoirParameterField.cpp new file mode 100644 index 0000000..23a1755 --- /dev/null +++ b/Src/nmNum/nmSubWxs/nmReservoirParameterField.cpp @@ -0,0 +1,429 @@ +#include "nmReservoirParameterField.h" + +#include "iParaItemCtrl.h" +#include "iParameter.h" +#include "iSysParaHelper.h" +#include "mModuleDefines.h" + +#include +#include +#include +#include +#include +#include + +namespace +{ + +/* Qt 4.8的lupdate需要显式标记间接翻译字符串。 */ +static const char* const s_pFieldTranslationMarkers[] = +{ + QT_TRANSLATE_NOOP("nmReservoirParameterField", "The parameter layout is unavailable."), + QT_TRANSLATE_NOOP("nmReservoirParameterField", "Parameter definition not found: %1"), + QT_TRANSLATE_NOOP("nmReservoirParameterField", "Failed to bind the unit for parameter: %1") +}; + +/** @brief 返回轻量字段类使用的翻译文字。 */ +QString fieldText(const char* pText) +{ + Q_UNUSED(s_pFieldTranslationMarkers); + return QCoreApplication::translate( + "nmReservoirParameterField", pText); +} + +} + +nmReservoirParameterField::nmReservoirParameterField( + const nmReservoirParameterFieldInfo& oFieldInfo, + nmIReservoirParameterUnitService* pUnitService, + QObject* pParent) + : QObject(pParent), + m_oFieldInfo(oFieldInfo), + m_pUnitService(pUnitService), + m_pParameter(nullptr), + m_pLabel(nullptr), + m_pLineEdit(nullptr), + m_pValueComboBox(nullptr), + m_pUnitComboBox(nullptr), + m_pUnitLabel(nullptr), + m_bVisible(true), + m_bUpdating(false) +{ +} + +nmReservoirParameterField::~nmReservoirParameterField() +{ + m_pUnitService = nullptr; + m_pParameter = nullptr; + m_pLabel = nullptr; + m_pLineEdit = nullptr; + m_pValueComboBox = nullptr; + m_pUnitComboBox = nullptr; + m_pUnitLabel = nullptr; +} + +bool nmReservoirParameterField::initialize(QGridLayout* pLayout, + int nRow, + QString& sError) +{ + if (pLayout == nullptr) + { + sError = fieldText("The parameter layout is unavailable."); + return false; + } + + // 第一步:枚举字段不依赖XML数值定义,直接创建稳定code下拉框。 + if (isEnum()) + { + createEnumEditor(pLayout, nRow); + sError.clear(); + return true; + } + + // 第二步:数值字段必须取得NM系列的基础参数定义。 + m_pParameter = _paraHelper->getPara( + m_oFieldInfo.m_sBaseParameterId, s_Nm_Serie); + if (m_pParameter == nullptr) + { + sError = fieldText("Parameter definition not found: %1") + .arg(m_oFieldInfo.m_sBaseParameterId); + return false; + } + + // 第三步:创建原型中的标签、输入框和单位列,不增加字段外框。 + return createNumericEditor(pLayout, nRow, sError); +} + +QString nmReservoirParameterField::parameterId() const +{ + return m_oFieldInfo.m_sParameterId; +} + +QString nmReservoirParameterField::baseParameterId() const +{ + return m_oFieldInfo.m_sBaseParameterId; +} + +bool nmReservoirParameterField::isEnum() const +{ + return m_oFieldInfo.m_eEditorType == NM_RESERVOIR_EDITOR_ENUM; +} + +void nmReservoirParameterField::setValueRightAligned(bool bRightAligned) +{ + if (m_pLineEdit != nullptr) + { + m_pLineEdit->setAlignment( + (bRightAligned ? Qt::AlignRight : Qt::AlignLeft) | + Qt::AlignVCenter); + } +} + +int nmReservoirParameterField::editorMinimumWidth() +{ + return 140; +} + +int nmReservoirParameterField::unitMinimumWidth() +{ + return 86; +} + +int nmReservoirParameterField::editorHeight() +{ + return 25; +} + +bool nmReservoirParameterField::createNumericEditor( + QGridLayout* pLayout, + int nRow, + QString& sError) +{ + QWidget* pParentWidget = pLayout->parentWidget(); + m_pLabel = new QLabel(m_pParameter->m_sAlias, pParentWidget); + m_pLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter); + m_pLabel->setWordWrap(false); + m_pLabel->setSizePolicy(QSizePolicy::Preferred, + QSizePolicy::Fixed); + + m_pLineEdit = new QLineEdit(pParentWidget); + m_pLineEdit->setAlignment(Qt::AlignLeft | Qt::AlignVCenter); + m_pLineEdit->setFixedHeight(editorHeight()); + m_pLineEdit->setMinimumWidth(editorMinimumWidth()); + m_pLineEdit->setSizePolicy(QSizePolicy::Expanding, + QSizePolicy::Fixed); + + pLayout->addWidget(m_pLabel, nRow, 0); + pLayout->addWidget(m_pLineEdit, nRow, 1); + + // 有单位参数直接注册到主对话框绑定器;字段只按控件指针读写, + // 因此多口井共用同一基础参数ID时也不会发生串值。 + if (m_oFieldInfo.m_bUnitEnabled && + !m_pParameter->m_sUnit.isEmpty()) + { + m_pUnitComboBox = new QComboBox(pParentWidget); + m_pUnitComboBox->setFixedHeight(editorHeight()); + m_pUnitComboBox->setMinimumWidth(unitMinimumWidth()); + m_pUnitComboBox->setSizePolicy(QSizePolicy::Expanding, + QSizePolicy::Fixed); + pLayout->addWidget(m_pUnitComboBox, nRow, 2); + if (m_pUnitService == nullptr || + !m_pUnitService->bindUnitField( + m_pLineEdit, + m_pUnitComboBox, + m_oFieldInfo.m_sBaseParameterId)) + { + sError = fieldText( + "Failed to bind the unit for parameter: %1") + .arg(m_oFieldInfo.m_sBaseParameterId); + return false; + } + } + else + { + // 无单位参数保留空列用于对齐,不显示额外的单位说明文字。 + m_pUnitLabel = new QLabel(pParentWidget); + m_pUnitLabel->setMinimumWidth(unitMinimumWidth()); + m_pUnitLabel->setSizePolicy(QSizePolicy::Expanding, + QSizePolicy::Fixed); + pLayout->addWidget(m_pUnitLabel, nRow, 2); + } + + updateValidationAppearance(true); + sError.clear(); + return true; +} + +void nmReservoirParameterField::createEnumEditor(QGridLayout* pLayout, + int nRow) +{ + QWidget* pParentWidget = pLayout->parentWidget(); + m_pLabel = new QLabel(m_oFieldInfo.m_sLabel, pParentWidget); + m_pLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter); + m_pLabel->setWordWrap(false); + m_pLabel->setSizePolicy(QSizePolicy::Preferred, + QSizePolicy::Fixed); + + m_pValueComboBox = new QComboBox(pParentWidget); + m_pValueComboBox->setFixedHeight(editorHeight()); + m_pValueComboBox->setMinimumWidth(editorMinimumWidth()); + m_pValueComboBox->setSizePolicy(QSizePolicy::Expanding, + QSizePolicy::Fixed); + for (int nIndex = 0; + nIndex < m_oFieldInfo.m_listOptions.size(); + ++nIndex) + { + const nmReservoirEnumOption& oOption = + m_oFieldInfo.m_listOptions[nIndex]; + m_pValueComboBox->addItem(oOption.m_sText, oOption.m_sCode); + } + + // 枚举参数没有单位,空白单位列仅用于保持各行编辑器对齐。 + m_pUnitLabel = new QLabel(pParentWidget); + m_pUnitLabel->setMinimumWidth(unitMinimumWidth()); + m_pUnitLabel->setSizePolicy(QSizePolicy::Expanding, + QSizePolicy::Fixed); + + pLayout->addWidget(m_pLabel, nRow, 0); + pLayout->addWidget(m_pValueComboBox, nRow, 1); + pLayout->addWidget(m_pUnitLabel, nRow, 2); +} + +void nmReservoirParameterField::setBaseValue(const QVariant& oValue) +{ + if (m_pLineEdit == nullptr || !oValue.isValid()) + { + return; + } + + m_bUpdating = true; + double dValue = oValue.toDouble(); + if (m_pUnitComboBox != nullptr && m_pUnitService != nullptr) + { + m_pUnitService->setUnitBaseValue(m_pLineEdit, dValue); + } + else + { + m_pLineEdit->setText(formatBaseValue(dValue)); + } + m_bUpdating = false; + validate(); +} + +QVariant nmReservoirParameterField::baseValue(bool& bValid) +{ + bValid = validate(); + if (!bValid || m_pLineEdit == nullptr) + { + return QVariant(); + } + + if (m_pUnitComboBox != nullptr && m_pUnitService != nullptr) + { + return QVariant(m_pUnitService->unitBaseValue(m_pLineEdit)); + } + + bool bNumberOk = false; + double dValue = m_pLineEdit->text().trimmed().toDouble(&bNumberOk); + return bNumberOk ? QVariant(dValue) : QVariant(); +} + +void nmReservoirParameterField::setEnumCode(const QString& sCode) +{ + if (m_pValueComboBox == nullptr) + { + return; + } + int nIndex = m_pValueComboBox->findData(sCode); + m_bUpdating = true; + m_pValueComboBox->setCurrentIndex(nIndex >= 0 ? nIndex : 0); + m_bUpdating = false; +} + +QString nmReservoirParameterField::enumCode() const +{ + if (m_pValueComboBox == nullptr || + m_pValueComboBox->currentIndex() < 0) + { + return QString(); + } + return m_pValueComboBox->itemData( + m_pValueComboBox->currentIndex()).toString(); +} + +bool nmReservoirParameterField::validate() +{ + if (!m_bVisible || isEnum()) + { + return true; + } + if (m_pLineEdit == nullptr || m_pParameter == nullptr) + { + return false; + } + + bool bValid = false; + if (m_pUnitComboBox != nullptr && m_pUnitService != nullptr) + { + bValid = m_pUnitService->isUnitInputValid(m_pLineEdit); + } + else + { + bool bNumberOk = false; + double dValue = m_pLineEdit->text().trimmed() + .toDouble(&bNumberOk); + bValid = bNumberOk && dValue == dValue && + dValue >= m_pParameter->m_dMin && + dValue <= m_pParameter->m_dMax; + if (bValid && + m_oFieldInfo.m_eEditorType == NM_RESERVOIR_EDITOR_INTEGER) + { + bValid = dValue == static_cast(dValue); + } + } + updateValidationAppearance(bValid); + return bValid; +} + +void nmReservoirParameterField::setFieldVisible(bool bVisible) +{ + m_bVisible = bVisible; + if (m_pLabel != nullptr) + { + m_pLabel->setVisible(bVisible); + } + if (m_pLineEdit != nullptr) + { + m_pLineEdit->setVisible(bVisible); + } + if (m_pValueComboBox != nullptr) + { + m_pValueComboBox->setVisible(bVisible); + } + if (m_pUnitComboBox != nullptr) + { + m_pUnitComboBox->setVisible(bVisible); + } + if (m_pUnitLabel != nullptr) + { + m_pUnitLabel->setVisible(bVisible); + } +} + +bool nmReservoirParameterField::isFieldVisible() const +{ + return m_bVisible; +} + +QLineEdit* nmReservoirParameterField::valueEditor() const +{ + return m_pLineEdit; +} + +QComboBox* nmReservoirParameterField::enumEditor() const +{ + return m_pValueComboBox; +} + +QString nmReservoirParameterField::formatBaseValue(double dValue) const +{ + if (m_pParameter == nullptr) + { + return QString::number(dValue, 'g', 15); + } + if (m_oFieldInfo.m_eEditorType == NM_RESERVOIR_EDITOR_INTEGER || + m_pParameter->m_nDigit == 0) + { + return QString::number(dValue, 'f', 0); + } + if (m_pParameter->m_bScientific) + { + return QString::number(dValue, 'e', m_pParameter->m_nDigit); + } + + QString sValue = QString::number( + dValue, 'f', m_pParameter->m_nDigit); + while (sValue.contains('.') && sValue.endsWith('0')) + { + sValue.chop(1); + } + if (sValue.endsWith('.')) + { + sValue.chop(1); + } + return sValue; +} + +void nmReservoirParameterField::updateValidationAppearance(bool bValid) +{ + Q_UNUSED(bValid); + if (m_pLineEdit == nullptr || m_pParameter == nullptr) + { + return; + } + + QString sCurrentUnit; + iUnitGroup* pUnitGroup = nullptr; + if (m_pUnitComboBox != nullptr && m_pUnitService != nullptr) + { + sCurrentUnit = m_pUnitService->currentUnit(m_pLineEdit); + pUnitGroup = m_pUnitService->unitGroup(m_pLineEdit); + } + QString sRange = iParaItemCtrl::getScaleStrOf( + m_pParameter, sCurrentUnit, pUnitGroup); + QString sHelp = m_pParameter->m_sDesc; + if (!sRange.isEmpty()) + { + if (!sHelp.isEmpty()) + { + sHelp += "\n"; + } + sHelp += sRange; + } + m_pLineEdit->setToolTip(sHelp); + m_pLineEdit->setWhatsThis(sHelp); + if (m_pLabel != nullptr) + { + m_pLabel->setWhatsThis(sHelp); + } +} diff --git a/Src/nmNum/nmSubWxs/nmReservoirParameterPage.cpp b/Src/nmNum/nmSubWxs/nmReservoirParameterPage.cpp new file mode 100644 index 0000000..51565fd --- /dev/null +++ b/Src/nmNum/nmSubWxs/nmReservoirParameterPage.cpp @@ -0,0 +1,539 @@ +#include "nmReservoirParameterPage.h" + +#include "nmReservoirParameterField.h" +#include "nmReservoirPropertiesSession.h" + +#include +#include +#include +#include +#include +#include +#include + +nmReservoirParameterPage::nmReservoirParameterPage( + nmReservoirPropertiesCategory eCategory, + nmReservoirPropertiesSession* pSession, + nmIReservoirParameterUnitService* pUnitService, + QWidget* pParent) + : QWidget(pParent), + m_eCategory(eCategory), + m_pSession(pSession), + m_pUnitService(pUnitService), + m_nCurrentObjectIndex(-1), + m_bUpdatingObject(false), + m_bBuildingFields(false), + m_pTitleLabel(nullptr), + m_pObjectLabel(nullptr), + m_pObjectComboBox(nullptr), + m_pGroupsWidget(nullptr), + m_pGroupsLayout(nullptr), + m_pMainLayout(nullptr), + m_pEmptyLabel(nullptr) +{ + createUi(); +} + +nmReservoirParameterPage::~nmReservoirParameterPage() +{ + m_listFields.clear(); + m_mapFields.clear(); + m_mapEditorFields.clear(); + m_pSession = nullptr; + m_pUnitService = nullptr; +} + +void nmReservoirParameterPage::createUi() +{ + // 第一步:创建原型中的单层内容头,储层页自动隐藏对象选择器。 + m_pMainLayout = new QVBoxLayout(this); + m_pMainLayout->setContentsMargins(8, 6, 8, 8); + m_pMainLayout->setSpacing(8); + + QWidget* pHeaderWidget = new QWidget(this); + QHBoxLayout* pHeaderLayout = new QHBoxLayout(pHeaderWidget); + pHeaderLayout->setContentsMargins(8, 4, 8, 4); + pHeaderLayout->setSpacing(9); + + m_pTitleLabel = new QLabel(categoryTitle(), pHeaderWidget); + pHeaderLayout->addWidget(m_pTitleLabel, 1); + + m_pObjectLabel = new QLabel(objectSelectorLabel(), pHeaderWidget); + m_pObjectComboBox = new QComboBox(pHeaderWidget); + m_pObjectComboBox->setMinimumWidth(260); + m_pObjectComboBox->setMinimumHeight(27); + pHeaderLayout->addWidget(m_pObjectLabel); + pHeaderLayout->addWidget(m_pObjectComboBox); + // 储层页只有一个固定对象,顶部分类按钮已经说明当前页面, + // 不再重复显示无操作价值的页标题;其他页面保留对象选择区。 + if (m_eCategory == NM_RESERVOIR_PAGE_RESERVOIR) + { + pHeaderWidget->hide(); + } + else + { + m_pMainLayout->addWidget(pHeaderWidget); + } + + // 第二步:分组载体没有边框,仅用于实现原型中的两列网格。 + m_pGroupsWidget = new QWidget(this); + m_pGroupsLayout = new QGridLayout(m_pGroupsWidget); + m_pGroupsLayout->setContentsMargins(0, 0, 0, 0); + m_pGroupsLayout->setHorizontalSpacing(14); + m_pGroupsLayout->setVerticalSpacing(14); + m_pGroupsLayout->setColumnStretch(0, 1); + m_pGroupsLayout->setColumnStretch(1, 1); + m_pMainLayout->addWidget(m_pGroupsWidget); + m_pMainLayout->addStretch(1); + + connect(m_pObjectComboBox, SIGNAL(currentIndexChanged(int)), + this, SLOT(slotObjectChanged(int))); +} + +QString nmReservoirParameterPage::categoryTitle() const +{ + switch (m_eCategory) + { + case NM_RESERVOIR_PAGE_RESERVOIR: + return tr("Reservoir parameters"); + case NM_RESERVOIR_PAGE_WELL: + return tr("Well parameters"); + case NM_RESERVOIR_PAGE_FAULT: + return tr("Fault parameters"); + case NM_RESERVOIR_PAGE_FRACTURE: + return tr("Fracture parameters"); + case NM_RESERVOIR_PAGE_COMPOSITE_REGION: + return tr("Composite region parameters"); + case NM_RESERVOIR_PAGE_REGION_MARK: + return tr("Region mark parameters"); + default: + return QString(); + } +} + +QString nmReservoirParameterPage::objectSelectorLabel() const +{ + switch (m_eCategory) + { + case NM_RESERVOIR_PAGE_WELL: + return tr("Select well:"); + case NM_RESERVOIR_PAGE_FAULT: + return tr("Select fault:"); + case NM_RESERVOIR_PAGE_FRACTURE: + return tr("Select fracture:"); + case NM_RESERVOIR_PAGE_COMPOSITE_REGION: + return tr("Select composite region:"); + case NM_RESERVOIR_PAGE_REGION_MARK: + return tr("Select region mark:"); + default: + return tr("Select object:"); + } +} + +bool nmReservoirParameterPage::reloadObjects(QString& sError) +{ + // 第一步:重新取得会话对象描述,并把稳定对象code放入UserRole。 + m_listObjectInfos = m_pSession != nullptr + ? m_pSession->objectInfos(m_eCategory) + : QList(); + m_bUpdatingObject = true; + m_pObjectComboBox->clear(); + for (int nIndex = 0; nIndex < m_listObjectInfos.size(); ++nIndex) + { + const nmReservoirPropertyObjectInfo& oInfo = + m_listObjectInfos[nIndex]; + m_pObjectComboBox->addItem(oInfo.m_sDisplayName, + oInfo.m_sObjectCode); + } + m_bUpdatingObject = false; + + bool bShowSelector = m_eCategory != NM_RESERVOIR_PAGE_RESERVOIR; + m_pObjectLabel->setVisible(bShowSelector); + m_pObjectComboBox->setVisible(bShowSelector); + + // 第二步:构建首个对象;空分类显示无额外面板的空状态。 + return buildObject(m_listObjectInfos.isEmpty() ? -1 : 0, sError); +} + +bool nmReservoirParameterPage::hasEditableObjects() const +{ + return !m_listObjectInfos.isEmpty(); +} + +void nmReservoirParameterPage::slotObjectChanged(int nIndex) +{ + if (m_bUpdatingObject || nIndex == m_nCurrentObjectIndex) + { + return; + } + + // 第一步:离开旧对象前校验并保存,非法输入时回退对象选择。 + int nPreviousIndex = m_nCurrentObjectIndex; + if (nPreviousIndex >= 0 && !commitPendingEdits()) + { + m_bUpdatingObject = true; + m_pObjectComboBox->setCurrentIndex(nPreviousIndex); + m_bUpdatingObject = false; + return; + } + + // 第二步:构建新对象;目录已在初始化阶段验证,正常切换不会失败。 + QString sError; + if (!buildObject(nIndex, sError)) + { + m_bUpdatingObject = true; + m_pObjectComboBox->setCurrentIndex(nPreviousIndex); + m_bUpdatingObject = false; + } +} + +bool nmReservoirParameterPage::buildObject(int nObjectIndex, + QString& sError) +{ + m_bBuildingFields = true; + clearObjectEditors(); + m_nCurrentObjectIndex = nObjectIndex; + + if (nObjectIndex < 0 || nObjectIndex >= m_listObjectInfos.size()) + { + m_pEmptyLabel = new QLabel( + tr("No editable objects in this category."), + m_pGroupsWidget); + m_pEmptyLabel->setAlignment(Qt::AlignCenter); + m_pEmptyLabel->setMinimumHeight(260); + m_pGroupsLayout->addWidget(m_pEmptyLabel, 0, 0, 1, 2); + m_bBuildingFields = false; + sError.clear(); + updateValidity(); + return true; + } + + // 第一步:按目录直接创建分组,页面不再包含对象类型专用布局代码。 + const nmReservoirPropertyObjectInfo& oObjectInfo = + m_listObjectInfos[nObjectIndex]; + for (int nGroupIndex = 0; + nGroupIndex < oObjectInfo.m_listGroups.size(); + ++nGroupIndex) + { + if (!buildGroup(oObjectInfo.m_listGroups[nGroupIndex], sError)) + { + m_bBuildingFields = false; + return false; + } + } + + // 第二步:加载工作副本,随后只调整行可见性,不重建页面。 + for (int nFieldIndex = 0; + nFieldIndex < m_listFields.size(); + ++nFieldIndex) + { + loadFieldValue(m_listFields[nFieldIndex]); + } + applyVisibilityRules(); + m_bBuildingFields = false; + sError.clear(); + updateValidity(); + return true; +} + +void nmReservoirParameterPage::clearObjectEditors() +{ + m_listFields.clear(); + m_mapFields.clear(); + m_mapEditorFields.clear(); + m_pEmptyLabel = nullptr; + + // 分组载体本身保留,只删除其直接子控件和布局项。 + while (m_pGroupsLayout->count() > 0) + { + QLayoutItem* pItem = m_pGroupsLayout->takeAt(0); + if (pItem->widget() != nullptr) + { + delete pItem->widget(); + } + delete pItem; + } +} + +bool nmReservoirParameterPage::buildGroup( + const nmReservoirParameterGroupInfo& oGroupInfo, + QString& sError) +{ + // 承载器始终横向铺满网格单元;只通过内部弹簧控制垂直位置, + // 避免直接设置对齐标志后分组框退回建议宽度。 + QWidget* pGroupContainer = new QWidget(m_pGroupsWidget); + pGroupContainer->setSizePolicy(QSizePolicy::Expanding, + QSizePolicy::Expanding); + QVBoxLayout* pGroupContainerLayout = + new QVBoxLayout(pGroupContainer); + pGroupContainerLayout->setContentsMargins(0, 0, 0, 0); + pGroupContainerLayout->setSpacing(0); + + QGroupBox* pGroupBox = new QGroupBox( + oGroupInfo.m_sTitle, pGroupContainer); + // 参数框只在水平方向随页面宽度调整,垂直方向始终采用内容建议高度, + // 避免跨行分组被网格拉伸成大面积空白框。 + pGroupBox->setSizePolicy(QSizePolicy::Expanding, + QSizePolicy::Fixed); + QGridLayout* pFieldLayout = new QGridLayout(pGroupBox); + pFieldLayout->setContentsMargins(8, 15, 8, 10); + pFieldLayout->setHorizontalSpacing(6); + pFieldLayout->setVerticalSpacing(6); + // 标签列由本组最长标签的自然宽度决定,数值列和单位列按比例使用剩余空间。 + pFieldLayout->setColumnStretch(0, 0); + pFieldLayout->setColumnStretch(1, 8); + pFieldLayout->setColumnStretch(2, 5); + pFieldLayout->setColumnMinimumWidth( + 1, nmReservoirParameterField::editorMinimumWidth()); + pFieldLayout->setColumnMinimumWidth( + 2, nmReservoirParameterField::unitMinimumWidth()); + + // 每个字段只把三个直接控件放入此网格,不产生字段外框或子面板。 + for (int nFieldIndex = 0; + nFieldIndex < oGroupInfo.m_listFields.size(); + ++nFieldIndex) + { + nmReservoirParameterField* pField = + new nmReservoirParameterField( + oGroupInfo.m_listFields[nFieldIndex], + m_pUnitService, + pGroupBox); + if (!pField->initialize(pFieldLayout, nFieldIndex, sError)) + { + delete pGroupContainer; + return false; + } + pField->setValueRightAligned( + oGroupInfo.m_bValueRightAligned); + if (pField->valueEditor() != nullptr) + { + connect(pField->valueEditor(), SIGNAL(textChanged(QString)), + this, SLOT(slotFieldValueEdited())); + m_mapEditorFields.insert(pField->valueEditor(), pField); + } + if (pField->enumEditor() != nullptr) + { + connect(pField->enumEditor(), SIGNAL(currentIndexChanged(int)), + this, SLOT(slotEnumIndexChanged(int))); + m_mapEditorFields.insert(pField->enumEditor(), pField); + } + m_listFields.append(pField); + m_mapFields.insert(pField->parameterId(), pField); + } + + // 跨越多行的分组在承载区域垂直居中,普通分组保持顶部排列。 + if (oGroupInfo.m_nRowSpan > 1) + { + pGroupContainerLayout->addStretch(1); + } + pGroupContainerLayout->addWidget(pGroupBox); + pGroupContainerLayout->addStretch(1); + + m_pGroupsLayout->addWidget( + pGroupContainer, + oGroupInfo.m_nRow, + oGroupInfo.m_nColumn, + oGroupInfo.m_nRowSpan, + oGroupInfo.m_nColumnSpan); + return true; +} + +void nmReservoirParameterPage::loadFieldValue( + nmReservoirParameterField* pField) +{ + if (pField == nullptr || m_pSession == nullptr) + { + return; + } + if (pField->isEnum()) + { + pField->setEnumCode(m_pSession->enumCode( + pField->parameterId())); + } + else + { + pField->setBaseValue(m_pSession->value( + pField->parameterId())); + } +} + +bool nmReservoirParameterPage::commitPendingEdits() +{ + if (m_pSession == nullptr) + { + return false; + } + + // 第一步:先完整校验,避免部分字段已写入而后续字段失败。 + if (!isInputValid()) + { + updateValidity(); + return false; + } + + // 第二步:逐行按编辑器指针获取基准值,再统一写入工作副本。 + QMap mapValues; + for (int nFieldIndex = 0; + nFieldIndex < m_listFields.size(); + ++nFieldIndex) + { + nmReservoirParameterField* pField = + m_listFields[nFieldIndex]; + if (pField == nullptr || pField->isEnum() || + !pField->isFieldVisible()) + { + continue; + } + bool bValid = false; + QVariant oValue = pField->baseValue(bValid); + if (!bValid || !oValue.isValid()) + { + updateValidity(); + return false; + } + mapValues.insert(pField->parameterId(), oValue); + } + m_pSession->setValues(mapValues); + updateValidity(); + return true; +} + +bool nmReservoirParameterPage::isInputValid() +{ + for (int nFieldIndex = 0; + nFieldIndex < m_listFields.size(); + ++nFieldIndex) + { + nmReservoirParameterField* pField = + m_listFields[nFieldIndex]; + if (pField != nullptr && pField->isFieldVisible() && + !pField->validate()) + { + return false; + } + } + return true; +} + +void nmReservoirParameterPage::slotFieldValueEdited() +{ + if (!m_bBuildingFields) + { + nmReservoirParameterField* pField = + m_mapEditorFields.value(sender(), nullptr); + if (pField != nullptr) + { + pField->validate(); + } + updateValidity(); + } +} + +void nmReservoirParameterPage::slotEnumIndexChanged(int nIndex) +{ + Q_UNUSED(nIndex); + if (m_bBuildingFields || m_pSession == nullptr) + { + return; + } + + nmReservoirParameterField* pField = + m_mapEditorFields.value(sender(), nullptr); + if (pField == nullptr) + { + return; + } + QString sParameterId = pField->parameterId(); + + // 第一步:联动前保存当前可见数值;非法时回滚枚举code。 + QString sPreviousCode = m_pSession->enumCode(sParameterId); + if (!commitPendingEdits()) + { + m_bBuildingFields = true; + pField->setEnumCode(sPreviousCode); + m_bBuildingFields = false; + return; + } + + // 第二步:只用稳定code更新工作副本,显示文字不参与业务判断。 + if (!m_pSession->setEnumCode(sParameterId, pField->enumCode())) + { + m_bBuildingFields = true; + pField->setEnumCode(sPreviousCode); + m_bBuildingFields = false; + return; + } + + // 第三步:仅显示或隐藏行,隐藏参数继续保留在工作副本中。 + applyVisibilityRules(); + updateValidity(); +} + +void nmReservoirParameterPage::applyVisibilityRules() +{ + if (m_pSession == nullptr || m_nCurrentObjectIndex < 0 || + m_nCurrentObjectIndex >= m_listObjectInfos.size()) + { + return; + } + + const nmReservoirPropertyObjectInfo& oInfo = + m_listObjectInfos[m_nCurrentObjectIndex]; + + // 区域标记为均质储层时隐藏双重介质参数。 + if (oInfo.m_eObjectType == NM_RESERVOIR_OBJECT_REGION_MARK) + { + QString sModelId = oInfo.m_sObjectCode + + "_RM_ReservoirType"; + bool bDual = m_pSession->enumCode(sModelId) != "homogeneous"; + setFieldVisible(oInfo.m_sObjectCode + "_RM_ComW", bDual); + setFieldVisible(oInfo.m_sObjectCode + "_RM_ComKr", bDual); + setFieldVisible(oInfo.m_sObjectCode + "_RM_NetToGross", bDual); + } + + // 无限导流裂缝不显示有限导流能力和裂缝宽度参数。 + if (oInfo.m_eObjectType == NM_RESERVOIR_OBJECT_FRACTURE) + { + QString sModelId = oInfo.m_sObjectCode + "_F_FlowModel"; + bool bFinite = m_pSession->enumCode(sModelId) != + "infinite_conductivity"; + setFieldVisible(oInfo.m_sObjectCode + "_F_FC", bFinite); + setFieldVisible(oInfo.m_sObjectCode + "_F_DW", bFinite); + } + + // 非泄漏断层或复合区不显示泄漏系数。 + if (oInfo.m_eObjectType == NM_RESERVOIR_OBJECT_FAULT) + { + bool bLeaky = m_pSession->enumCode( + oInfo.m_sObjectCode + "_FT_FlowModel") == "leaky"; + setFieldVisible(oInfo.m_sObjectCode + "_FT_Leakage", bLeaky); + } + if (oInfo.m_eObjectType == NM_RESERVOIR_OBJECT_REGION) + { + bool bLeaky = m_pSession->enumCode( + oInfo.m_sObjectCode + "_R_FlowModel") == "leaky"; + setFieldVisible(oInfo.m_sObjectCode + "_R_Leakage", bLeaky); + } +} + +void nmReservoirParameterPage::setFieldVisible( + const QString& sParameterId, + bool bVisible) +{ + nmReservoirParameterField* pField = + m_mapFields.value(sParameterId, nullptr); + if (pField != nullptr) + { + pField->setFieldVisible(bVisible); + } +} + +void nmReservoirParameterPage::updateValidity() +{ + emit sigValidityChanged(isInputValid()); +} + +nmReservoirPropertiesCategory nmReservoirParameterPage::category() const +{ + return m_eCategory; +} diff --git a/Src/nmNum/nmSubWxs/nmReservoirPropertiesDataSource.cpp b/Src/nmNum/nmSubWxs/nmReservoirPropertiesDataSource.cpp new file mode 100644 index 0000000..c10ada0 --- /dev/null +++ b/Src/nmNum/nmSubWxs/nmReservoirPropertiesDataSource.cpp @@ -0,0 +1,119 @@ +#include "nmReservoirPropertiesDataSource.h" + +#include "nmDataAnalyzeManager.h" +#include "nmDataWellBase.h" +#include "nmWxParameterProperty.h" + +nmReservoirPropertiesSnapshot::nmReservoirPropertiesSnapshot() +{ +} + +nmIReservoirPropertiesDataSource::~nmIReservoirPropertiesDataSource() +{ +} + +nmAnalyzeManagerReservoirDataSource::nmAnalyzeManagerReservoirDataSource( + nmDataAnalyzeManager* pDataManager) + : m_pDataManager(pDataManager) +{ +} + +nmAnalyzeManagerReservoirDataSource::~nmAnalyzeManagerReservoirDataSource() +{ + m_pDataManager = nullptr; +} + +bool nmAnalyzeManagerReservoirDataSource::loadSnapshot( + nmReservoirPropertiesSnapshot& oSnapshot, + QString& sError) const +{ + // 第一步:确认入口已经注入有效的数据管理器。 + if (m_pDataManager == nullptr) + { + sError = QObject::tr("The current numerical well-test data is unavailable."); + return false; + } + + // 第二步:复制储层和几何对象,确保界面编辑不会触碰实时数据。 + oSnapshot.m_oReservoir = m_pDataManager->getReservoirDataCopy(); + oSnapshot.m_vecRegionMarks = m_pDataManager->getRegionMarkDataListCopy(); + oSnapshot.m_vecFaults = m_pDataManager->getFaultDataListCopy(); + oSnapshot.m_vecFractures = m_pDataManager->getFractureDataListCopy(); + oSnapshot.m_vecRegions = m_pDataManager->getRegionDataListCopy(); + + oSnapshot.m_vecVerticalWells.clear(); + oSnapshot.m_vecVerticalFracturedWells.clear(); + oSnapshot.m_vecHorizontalFracturedWells.clear(); + + // 第三步:按最具体的井型复制数据,显式排除不受求解器支持的普通水平井。 + QVector vecWells = m_pDataManager->getWellDataList(); + for (int nIndex = 0; nIndex < vecWells.size(); ++nIndex) + { + nmDataWellBase* pWell = vecWells[nIndex]; + if (pWell == nullptr) + { + continue; + } + + nmDataVerticalFracturedWell* pVerticalFractured = + dynamic_cast(pWell); + nmDataHorizontalFracturedWell* pHorizontalFractured = + dynamic_cast(pWell); + nmDataVerticalWell* pVertical = dynamic_cast(pWell); + + if (pVerticalFractured != nullptr) + { + oSnapshot.m_vecVerticalFracturedWells.append(*pVerticalFractured); + } + else if (pHorizontalFractured != nullptr) + { + oSnapshot.m_vecHorizontalFracturedWells.append(*pHorizontalFractured); + } + else if (pVertical != nullptr) + { + oSnapshot.m_vecVerticalWells.append(*pVertical); + } + } + + sError.clear(); + return true; +} + +bool nmAnalyzeManagerReservoirDataSource::commitSnapshot( + const nmReservoirPropertiesSnapshot& oSnapshot, + bool bGeometryChanged, + QString& sError) +{ + // 第一步:提交前再次校验数据管理器生命周期。 + if (m_pDataManager == nullptr) + { + sError = QObject::tr("The current numerical well-test data is unavailable."); + return false; + } + + // 第二步:按对象类型批量替换现有数据,不改变对象地址和列表结构。 + m_pDataManager->updateVerticalWells(oSnapshot.m_vecVerticalWells); + m_pDataManager->updateVerticalFracturedWells(oSnapshot.m_vecVerticalFracturedWells); + m_pDataManager->updateHorizontalFracturedWells(oSnapshot.m_vecHorizontalFracturedWells); + m_pDataManager->updateFaultData(oSnapshot.m_vecFaults); + m_pDataManager->updateFractureData(oSnapshot.m_vecFractures); + m_pDataManager->updateRegionData(oSnapshot.m_vecRegions); + m_pDataManager->updateRegionMarkData(oSnapshot.m_vecRegionMarks); + m_pDataManager->updateReservoirData(oSnapshot.m_oReservoir); + + // 第三步:重建实时注册表映射,并在几何变化时统一通知画布和网格。 + m_pDataManager->syncWellAttrs(); + m_pDataManager->syncGeometryAttrs(); + if (bGeometryChanged) + { + m_pDataManager->notifyDataChanged(); + m_pDataManager->updateWellPlotByDataManager(); + } + + // 第四步:兼容仍在使用旧参数树的窗口,提交完成后只刷新一次表格入口。 + nmWxParameterProperty::notifyUpdateTable(); + + sError.clear(); + return true; +} + diff --git a/Src/nmNum/nmSubWxs/nmReservoirPropertiesSession.cpp b/Src/nmNum/nmSubWxs/nmReservoirPropertiesSession.cpp new file mode 100644 index 0000000..293b74c --- /dev/null +++ b/Src/nmNum/nmSubWxs/nmReservoirPropertiesSession.cpp @@ -0,0 +1,242 @@ +#include "nmReservoirPropertiesSession.h" + +#include "nmAttrRegistry.h" +#include "nmDataAttribute.h" + +namespace +{ + +/** @brief 比较两个QVariant数值,保留字符串和整数的精确语义。 */ +bool sameValue(const QVariant& oLeft, const QVariant& oRight) +{ + return oLeft == oRight; +} + +/** @brief 比较两口井的网格相关参数。 */ +bool sameWellGeometry(nmDataWellBase& oLeft, nmDataWellBase& oRight) +{ + return sameValue(oLeft.getX().getValue(), oRight.getX().getValue()) && + sameValue(oLeft.getY().getValue(), oRight.getY().getValue()) && + sameValue(oLeft.getRadius().getValue(), oRight.getRadius().getValue()); +} + +} + +nmReservoirPropertiesSession::nmReservoirPropertiesSession( + nmIReservoirPropertiesDataSource* pDataSource) + : m_pDataSource(pDataSource), + m_pAttrRegistry(new nmAttrRegistry), + m_bInitialized(false) +{ +} + +nmReservoirPropertiesSession::~nmReservoirPropertiesSession() +{ + delete m_pAttrRegistry; + m_pAttrRegistry = nullptr; + m_pDataSource = nullptr; +} + +bool nmReservoirPropertiesSession::initialize(QString& sError) +{ + // 第一步:读取一次真实数据,作为取消和变更检测的基线。 + if (m_pDataSource == nullptr || + !m_pDataSource->loadSnapshot(m_oOriginalSnapshot, sError)) + { + m_bInitialized = false; + return false; + } + + // 第二步:复制出独立工作副本,后续界面修改只写入此对象。 + m_oWorkingSnapshot = m_oOriginalSnapshot; + nmReservoirParameterCatalog::registerSnapshot(m_pAttrRegistry, + m_oWorkingSnapshot); + m_bInitialized = true; + sError.clear(); + return true; +} + +QList nmReservoirPropertiesSession::objectInfos( + nmReservoirPropertiesCategory eCategory) +{ + if (!m_bInitialized) + { + return QList(); + } + return nmReservoirParameterCatalog::createObjectInfos(eCategory, + m_oWorkingSnapshot); +} + +QVariant nmReservoirPropertiesSession::value(const QString& sParameterId) const +{ + if (!m_bInitialized || m_pAttrRegistry == nullptr) + { + return QVariant(); + } + QStringList listIds; + listIds << sParameterId; + return m_pAttrRegistry->collect(listIds).value(sParameterId, QVariant()); +} + +bool nmReservoirPropertiesSession::setValue(const QString& sParameterId, + const QVariant& oValue) +{ + if (!m_bInitialized || m_pAttrRegistry == nullptr || + !m_pAttrRegistry->registeredNames().contains(sParameterId)) + { + return false; + } + m_pAttrRegistry->setValue(sParameterId, oValue); + return true; +} + +void nmReservoirPropertiesSession::setValues( + const QMap& mapValues) +{ + QMap::const_iterator oIterator = mapValues.constBegin(); + for (; oIterator != mapValues.constEnd(); ++oIterator) + { + setValue(oIterator.key(), oIterator.value()); + } +} + +QString nmReservoirPropertiesSession::enumCode(const QString& sParameterId) const +{ + if (!m_bInitialized) + { + return QString(); + } + return nmReservoirParameterCatalog::enumCode(m_oWorkingSnapshot, + sParameterId); +} + +bool nmReservoirPropertiesSession::setEnumCode(const QString& sParameterId, + const QString& sCode) +{ + if (!m_bInitialized) + { + return false; + } + return nmReservoirParameterCatalog::setEnumCode(m_oWorkingSnapshot, + sParameterId, + sCode); +} + +bool nmReservoirPropertiesSession::commit(QString& sError) +{ + if (!m_bInitialized || m_pDataSource == nullptr) + { + sError = QObject::tr("The reservoir properties editing session is unavailable."); + return false; + } + + // 第一步:在提交前计算几何变更,供数据源决定是否重建网格和刷新画布。 + bool bGeometryChanged = isGeometryChanged(); + + // 第二步:将完整工作副本一次性交给数据源;失败时继续保留当前编辑内容。 + if (!m_pDataSource->commitSnapshot(m_oWorkingSnapshot, + bGeometryChanged, + sError)) + { + return false; + } + + // 第三步:提交成功后更新基线,防止同一会话重复识别旧变更。 + m_oOriginalSnapshot = m_oWorkingSnapshot; + sError.clear(); + return true; +} + +const nmReservoirPropertiesSnapshot& +nmReservoirPropertiesSession::workingSnapshot() const +{ + return m_oWorkingSnapshot; +} + +bool nmReservoirPropertiesSession::isGeometryChanged() const +{ + // 第一步:对象数量变化视为几何变化,虽然当前对话框本身不提供增删操作。 + if (m_oOriginalSnapshot.m_vecVerticalWells.size() != + m_oWorkingSnapshot.m_vecVerticalWells.size() || + m_oOriginalSnapshot.m_vecVerticalFracturedWells.size() != + m_oWorkingSnapshot.m_vecVerticalFracturedWells.size() || + m_oOriginalSnapshot.m_vecHorizontalFracturedWells.size() != + m_oWorkingSnapshot.m_vecHorizontalFracturedWells.size()) + { + return true; + } + + // 第二步:比较三类井的位置和井径。 + for (int nIndex = 0; nIndex < m_oWorkingSnapshot.m_vecVerticalWells.size(); ++nIndex) + { + nmDataVerticalWell& oOriginal = + const_cast(m_oOriginalSnapshot.m_vecVerticalWells[nIndex]); + nmDataVerticalWell& oWorking = + const_cast(m_oWorkingSnapshot.m_vecVerticalWells[nIndex]); + if (!sameWellGeometry(oOriginal, oWorking)) + { + return true; + } + } + for (int nIndex = 0; nIndex < m_oWorkingSnapshot.m_vecVerticalFracturedWells.size(); ++nIndex) + { + nmDataVerticalFracturedWell& oOriginal = const_cast( + m_oOriginalSnapshot.m_vecVerticalFracturedWells[nIndex]); + nmDataVerticalFracturedWell& oWorking = const_cast( + m_oWorkingSnapshot.m_vecVerticalFracturedWells[nIndex]); + if (!sameWellGeometry(oOriginal, oWorking) || + !sameValue(oOriginal.getFractureHalfLength().getValue(), + oWorking.getFractureHalfLength().getValue()) || + !sameValue(oOriginal.getFractureAngle().getValue(), + oWorking.getFractureAngle().getValue())) + { + return true; + } + } + for (int nIndex = 0; nIndex < m_oWorkingSnapshot.m_vecHorizontalFracturedWells.size(); ++nIndex) + { + nmDataHorizontalFracturedWell& oOriginal = + const_cast( + m_oOriginalSnapshot.m_vecHorizontalFracturedWells[nIndex]); + nmDataHorizontalFracturedWell& oWorking = + const_cast( + m_oWorkingSnapshot.m_vecHorizontalFracturedWells[nIndex]); + if (!sameWellGeometry(oOriginal, oWorking) || + !sameValue(oOriginal.getWellLength().getValue(), oWorking.getWellLength().getValue()) || + !sameValue(oOriginal.getDrainAngle().getValue(), oWorking.getDrainAngle().getValue()) || + !sameValue(oOriginal.getNumberOfFractures().getValue(), + oWorking.getNumberOfFractures().getValue()) || + !sameValue(oOriginal.getFractureHalfLength().getValue(), + oWorking.getFractureHalfLength().getValue()) || + !sameValue(oOriginal.getFractureAngle().getValue(), + oWorking.getFractureAngle().getValue())) + { + return true; + } + } + + // 第三步:断层和裂缝端点也直接参与网格,因此比较完整点坐标。 + if (m_oOriginalSnapshot.m_vecFaults.size() != m_oWorkingSnapshot.m_vecFaults.size() || + m_oOriginalSnapshot.m_vecFractures.size() != m_oWorkingSnapshot.m_vecFractures.size()) + { + return true; + } + for (int nIndex = 0; nIndex < m_oWorkingSnapshot.m_vecFaults.size(); ++nIndex) + { + if (m_oOriginalSnapshot.m_vecFaults[nIndex].getFaultPoints() != + m_oWorkingSnapshot.m_vecFaults[nIndex].getFaultPoints()) + { + return true; + } + } + for (int nIndex = 0; nIndex < m_oWorkingSnapshot.m_vecFractures.size(); ++nIndex) + { + if (m_oOriginalSnapshot.m_vecFractures[nIndex].getFracturePoints() != + m_oWorkingSnapshot.m_vecFractures[nIndex].getFracturePoints()) + { + return true; + } + } + return false; +} + diff --git a/Src/nmNum/nmSubWxs/nmWxAutomaticFitting.cpp b/Src/nmNum/nmSubWxs/nmWxAutomaticFitting.cpp index 202d14c..3d31d8d 100644 --- a/Src/nmNum/nmSubWxs/nmWxAutomaticFitting.cpp +++ b/Src/nmNum/nmSubWxs/nmWxAutomaticFitting.cpp @@ -408,7 +408,7 @@ void nmWxAutomaticFitting::setupParameterTable() m_parameterTable->setItem(0, 2, new QTableWidgetItem(QString::number(automaticFittingData.getPermeabilityMin().getValue().toDouble()))); m_parameterTable->setItem(0, 3, new QTableWidgetItem(QString::number(reservoirData.getPermeability().getValue().toDouble()))); m_parameterTable->setItem(0, 4, new QTableWidgetItem(QString::number(automaticFittingData.getPermeabilityMax().getValue().toDouble()))); - m_parameterTable->setItem(0, 5, new QTableWidgetItem(tr("Darcy"))); + m_parameterTable->setItem(0, 5, new QTableWidgetItem(tr("D"))); // 表皮系数 (Skin) m_parameterTable->setItem(1, 0, new QTableWidgetItem("2")); diff --git a/Src/nmNum/nmSubWxs/nmWxReservoirPropertiesEditor.cpp b/Src/nmNum/nmSubWxs/nmWxReservoirPropertiesEditor.cpp new file mode 100644 index 0000000..d71d944 --- /dev/null +++ b/Src/nmNum/nmSubWxs/nmWxReservoirPropertiesEditor.cpp @@ -0,0 +1,544 @@ +#include "nmWxReservoirPropertiesEditor.h" + +#include "nmReservoirPropertiesDataSource.h" +#include "nmReservoirPropertiesSession.h" +#include "nmReservoirParameterPage.h" +#include "iUnitGroup.h" +#include "iUnitHelper.h" +#include "iUnitWxBinder.h" +#include "mModuleDefines.h" +#include "ZxBaseUtil.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +nmWxReservoirPropertiesEditor::nmWxReservoirPropertiesEditor( + nmIReservoirPropertiesDataSource* pDataSource, + QWidget* pParent) + : iDlgBase(pParent), + m_pDataSource(pDataSource), + m_pSession(new nmReservoirPropertiesSession(pDataSource)), + m_pCategoryButtonGroup(nullptr), + m_pStackedWidget(nullptr), + m_pButtonBox(nullptr), + m_pFooterHelpButton(nullptr), + m_pStatusLabel(nullptr), + m_nCurrentCategory(NM_RESERVOIR_PAGE_RESERVOIR), + m_bInitialized(false) +{ + for (int nIndex = 0; nIndex < NM_RESERVOIR_PAGE_COUNT; ++nIndex) + { + m_pPages[nIndex] = nullptr; + } + + // 单位绑定器必须先切换到数值试井参数系列,再创建任何参数字段。 + setParaSrcTag(s_Nm_Serie); + createUi(); + setWindowTitle(tr("Reservoir properties settings")); + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + // 首次打开使用建议尺寸,后续最小宽高均由页面内容和布局自动决定。 + resize(880, 520); +} + +nmWxReservoirPropertiesEditor::~nmWxReservoirPropertiesEditor() +{ + delete m_pSession; + m_pSession = nullptr; + m_pDataSource = nullptr; +} + +void nmWxReservoirPropertiesEditor::createUi() +{ + // 第一步:创建原型中的顶部ribbon和无边框页面堆栈。 + QVBoxLayout* pMainLayout = new QVBoxLayout(this); + pMainLayout->setContentsMargins(12, 10, 12, 0); + pMainLayout->setSpacing(0); + pMainLayout->addWidget(createCategoryBar()); + + m_pStackedWidget = new QStackedWidget(this); + pMainLayout->addWidget(m_pStackedWidget, 1); + + // 第二步:底部左侧放帮助与状态,右侧只放确定和取消。 + QWidget* pFooterWidget = new QWidget(this); + QHBoxLayout* pFooterLayout = new QHBoxLayout(pFooterWidget); + pFooterLayout->setContentsMargins(4, 5, 4, 5); + pFooterLayout->setSpacing(8); + + m_pFooterHelpButton = new QToolButton(pFooterWidget); + m_pFooterHelpButton->setText(tr("Help")); + m_pFooterHelpButton->setIcon(zxLoadIcon("Help")); + m_pFooterHelpButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + m_pFooterHelpButton->setAutoRaise(true); + pFooterLayout->addWidget(m_pFooterHelpButton); + + m_pStatusLabel = new QLabel(pFooterWidget); + m_pStatusLabel->setWordWrap(true); + pFooterLayout->addWidget(m_pStatusLabel, 1); + + m_pButtonBox = new QDialogButtonBox( + QDialogButtonBox::Ok | QDialogButtonBox::Cancel, + Qt::Horizontal, + pFooterWidget); + QPushButton* pOkButton = m_pButtonBox->button(QDialogButtonBox::Ok); + QPushButton* pCancelButton = + m_pButtonBox->button(QDialogButtonBox::Cancel); + if (pOkButton != nullptr) + { + pOkButton->setIcon(zxLoadIcon("OK")); + pOkButton->setText(tr("OK")); + } + if (pCancelButton != nullptr) + { + pCancelButton->setIcon(zxLoadIcon("Cancel")); + pCancelButton->setText(tr("Cancel")); + } + pFooterLayout->addWidget(m_pButtonBox); + pMainLayout->addWidget(pFooterWidget); + + connect(m_pButtonBox, SIGNAL(accepted()), this, SLOT(accept())); + connect(m_pButtonBox, SIGNAL(rejected()), this, SLOT(reject())); + connect(m_pFooterHelpButton, SIGNAL(clicked()), + this, SLOT(slotHelp())); +} + +QWidget* nmWxReservoirPropertiesEditor::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_RESERVOIR_PAGE_RESERVOIR, + tr("Reservoir parameters"), + "ResData", + pBasicGroup)); + pBarLayout->addWidget(pBasicGroup); + pBarLayout->addWidget(createRibbonSeparator(pCategoryBar)); + + // 第二步:井筒参数独立成组,避免与储层内部区域对象混在一起。 + QHBoxLayout* pWellLayout = nullptr; + QWidget* pWellGroup = createRibbonGroup( + tr("Wells"), pCategoryBar, pWellLayout); + pWellLayout->insertWidget(0, createCategoryButton( + NM_RESERVOIR_PAGE_WELL, + tr("Well parameters"), + "NmWell", + pWellGroup)); + pBarLayout->addWidget(pWellGroup); + pBarLayout->addWidget(createRibbonSeparator(pCategoryBar)); + + // 第三步:四类储层平面对象按用户工作顺序放入同一个几何对象组。 + QHBoxLayout* pGeometryLayout = nullptr; + QWidget* pGeometryGroup = createRibbonGroup( + tr("Geometry objects"), + pCategoryBar, + pGeometryLayout); + pGeometryLayout->insertWidget(0, createCategoryButton( + NM_RESERVOIR_PAGE_FAULT, + tr("Faults"), + "NmFault", + pGeometryGroup)); + pGeometryLayout->insertWidget(1, createCategoryButton( + NM_RESERVOIR_PAGE_FRACTURE, + tr("Fractures"), + "NmFault", + pGeometryGroup)); + pGeometryLayout->insertWidget(2, createCategoryButton( + NM_RESERVOIR_PAGE_COMPOSITE_REGION, + tr("Composite regions"), + "NmRegion", + pGeometryGroup)); + pGeometryLayout->insertWidget(3, createCategoryButton( + NM_RESERVOIR_PAGE_REGION_MARK, + tr("Region marks"), + "RegionSelect", + pGeometryGroup)); + pBarLayout->addWidget(pGeometryGroup); + 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* nmWxReservoirPropertiesEditor::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* nmWxReservoirPropertiesEditor::createRibbonSeparator( + QWidget* pParent) +{ + // 使用单像素平直竖线,避免Sunken双线在端点形成视觉折角。 + 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* nmWxReservoirPropertiesEditor::createCategoryButton( + nmReservoirPropertiesCategory eCategory, + const QString& sText, + const QString& sIconName, + QWidget* pParent) +{ + QToolButton* pButton = new QToolButton(pParent); + pButton->setText(sText); + pButton->setIcon(zxLoadIcon(sIconName)); + pButton->setIconSize(QSize(25, 25)); + pButton->setToolButtonStyle(Qt::ToolButtonTextUnderIcon); + // 分类按钮仅执行页面切换命令,不保留持续选中状态。 + pButton->setCheckable(false); + pButton->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); + m_pCategoryButtonGroup->addButton( + pButton, static_cast(eCategory)); + return pButton; +} + +QToolButton* nmWxReservoirPropertiesEditor::createHelpButton( + QWidget* pParent) +{ + QToolButton* pButton = new QToolButton(pParent); + pButton->setText(tr("Help")); + pButton->setIcon(zxLoadIcon("Help")); + pButton->setIconSize(QSize(25, 25)); + pButton->setToolButtonStyle(Qt::ToolButtonTextUnderIcon); + pButton->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); + connect(pButton, SIGNAL(clicked()), this, SLOT(slotHelp())); + return pButton; +} + +bool nmWxReservoirPropertiesEditor::initialize(QString& sError) +{ + // 第一步:建立工作副本,失败时不创建不完整的字段控件。 + if (m_pSession == nullptr || !m_pSession->initialize(sError)) + { + m_bInitialized = false; + return false; + } + + // 第二步:创建页面并逐页验证XML参数定义和单位绑定。 + createPages(); + for (int nIndex = 0; nIndex < NM_RESERVOIR_PAGE_COUNT; ++nIndex) + { + if (!m_pPages[nIndex]->reloadObjects(sError)) + { + m_bInitialized = false; + return false; + } + } + + // 第三步:没有对应对象的分类直接禁用,避免用户进入无操作价值的空页面。 + updateCategoryButtonStates(); + m_pStackedWidget->setCurrentIndex(NM_RESERVOIR_PAGE_RESERVOIR); + m_bInitialized = true; + validateAllPages(); + sError.clear(); + return true; +} + +bool nmWxReservoirPropertiesEditor::bindUnitField( + QLineEdit* pLineEdit, + QComboBox* pUnitComboBox, + const QString& sBaseParameterId) +{ + // 第一步:仍由框架绑定器读取参数定义、范围和常规单位组。 + if (!bindUnitPairWxs(pLineEdit, pUnitComboBox, + sBaseParameterId, false)) + { + return false; + } + + // 第二步:单位帮助器对单位名执行了不区分大小写的匹配,D可能先命中 + // 时间组中的d(天)。渗透率以mD这个无歧义单位定位正确单位组, + // 但基准单位仍明确保持为求解器使用的D。 + if (sBaseParameterId == QString::fromLatin1("K")) + { + iUnitGroup* pPermeabilityGroup = + iUnitHelper::getUnitGroupByUnit( + QString::fromLatin1("mD")); + zxBindWxPair* pBindPair = m_pUnitBinder != nullptr + ? m_pUnitBinder->getWxPairOf(pLineEdit) : nullptr; + if (pPermeabilityGroup == nullptr || pBindPair == nullptr || + pPermeabilityGroup->m_sType.compare( + QString::fromLatin1("permeability"), + Qt::CaseInsensitive) != 0 || + pPermeabilityGroup->indexOf( + QString::fromLatin1("D")) < 0) + { + return false; + } + + // 第三步:替换错误单位组并重建单位选项。后续数值设置、切换、 + // 范围校验和基准值读取继续全部通过框架绑定器完成。 + pBindPair->isUpdating = true; + pBindPair->pUnitGroup = pPermeabilityGroup; + pBindPair->sBaseUnit = QString::fromLatin1("D"); + pUnitComboBox->clear(); + pUnitComboBox->addItems(pPermeabilityGroup->getAllUnitNames()); + int nBaseUnitIndex = pUnitComboBox->findText( + QString::fromLatin1("D"), + Qt::MatchExactly | Qt::MatchCaseSensitive); + pUnitComboBox->setCurrentIndex(nBaseUnitIndex); + pBindPair->isUpdating = false; + } + return true; +} + +void nmWxReservoirPropertiesEditor::setUnitBaseValue( + QLineEdit* pLineEdit, + double dBaseValue) +{ + if (m_pUnitBinder != nullptr) + { + m_pUnitBinder->setBaseValue(pLineEdit, dBaseValue); + } +} + +double nmWxReservoirPropertiesEditor::unitBaseValue( + QLineEdit* pLineEdit) const +{ + return m_pUnitBinder != nullptr + ? m_pUnitBinder->getBaseValue(pLineEdit) : 0.0; +} + +bool nmWxReservoirPropertiesEditor::isUnitInputValid( + QLineEdit* pLineEdit) +{ + return m_pUnitBinder != nullptr && + m_pUnitBinder->isInputOK(pLineEdit, true, nullptr); +} + +QString nmWxReservoirPropertiesEditor::currentUnit( + QLineEdit* pLineEdit) const +{ + return m_pUnitBinder != nullptr + ? m_pUnitBinder->getCurrentUnit(pLineEdit) : QString(); +} + +iUnitGroup* nmWxReservoirPropertiesEditor::unitGroup( + QLineEdit* pLineEdit) const +{ + return m_pUnitBinder != nullptr + ? m_pUnitBinder->getUnitGroup(pLineEdit) : nullptr; +} + +void nmWxReservoirPropertiesEditor::createPages() +{ + if (m_pStackedWidget->count() > 0) + { + return; + } + + for (int nIndex = 0; nIndex < NM_RESERVOIR_PAGE_COUNT; ++nIndex) + { + nmReservoirPropertiesCategory eCategory = + static_cast(nIndex); + m_pPages[nIndex] = new nmReservoirParameterPage( + eCategory, + m_pSession, + this, + m_pStackedWidget); + connect(m_pPages[nIndex], SIGNAL(sigValidityChanged(bool)), + this, SLOT(slotPageValidityChanged(bool))); + m_pStackedWidget->addWidget(m_pPages[nIndex]); + } +} + +void nmWxReservoirPropertiesEditor::slotCategoryButtonClicked( + int nCategory) +{ + if (!m_bInitialized || nCategory < 0 || + nCategory >= NM_RESERVOIR_PAGE_COUNT) + { + return; + } + + // 分类按钮状态由对象列表决定;此处再次防御程序主动触发的非法切换。 + QAbstractButton* pCategoryButton = m_pCategoryButtonGroup != nullptr + ? m_pCategoryButtonGroup->button(nCategory) : nullptr; + if (pCategoryButton == nullptr || !pCategoryButton->isEnabled()) + { + return; + } + + // 离开当前分类前先校验并保存,非法时保持当前页。 + if (m_pPages[m_nCurrentCategory] != nullptr && + !m_pPages[m_nCurrentCategory]->commitPendingEdits()) + { + m_pStatusLabel->setText( + tr("Please correct the invalid parameter values.")); + return; + } + + m_nCurrentCategory = nCategory; + m_pStackedWidget->setCurrentIndex(nCategory); + m_pStatusLabel->clear(); +} + +void nmWxReservoirPropertiesEditor::updateCategoryButtonStates() +{ + if (m_pCategoryButtonGroup == nullptr) + { + return; + } + + // 逐类读取页面已经加载的对象列表,页面不存在或对象为空时禁用命令。 + for (int nIndex = 0; nIndex < NM_RESERVOIR_PAGE_COUNT; ++nIndex) + { + QAbstractButton* pCategoryButton = + m_pCategoryButtonGroup->button(nIndex); + bool bEnabled = m_pPages[nIndex] != nullptr && + m_pPages[nIndex]->hasEditableObjects(); + if (pCategoryButton != nullptr) + { + pCategoryButton->setEnabled(bEnabled); + } + } +} + +void nmWxReservoirPropertiesEditor::slotPageValidityChanged(bool bValid) +{ + Q_UNUSED(bValid); + validateAllPages(); +} + +void nmWxReservoirPropertiesEditor::slotHelp() +{ + // 顶部和底部帮助命令共用Qt上下文帮助;字段说明和范围来自XML。 + QWhatsThis::enterWhatsThisMode(); +} + +bool nmWxReservoirPropertiesEditor::validateAllPages() +{ + bool bAllValid = m_bInitialized; + if (m_bInitialized) + { + for (int nIndex = 0; nIndex < NM_RESERVOIR_PAGE_COUNT; ++nIndex) + { + if (m_pPages[nIndex] != nullptr && + !m_pPages[nIndex]->isInputValid()) + { + bAllValid = false; + break; + } + } + } + + QPushButton* pOkButton = m_pButtonBox != nullptr + ? m_pButtonBox->button(QDialogButtonBox::Ok) : nullptr; + if (pOkButton != nullptr) + { + pOkButton->setEnabled(bAllValid); + } + return bAllValid; +} + +void nmWxReservoirPropertiesEditor::accept() +{ + if (!m_bInitialized) + { + return; + } + + // 第一步:依次提交各页面当前对象尚未离开焦点的输入。 + for (int nIndex = 0; nIndex < NM_RESERVOIR_PAGE_COUNT; ++nIndex) + { + if (m_pPages[nIndex] != nullptr && + !m_pPages[nIndex]->commitPendingEdits()) + { + m_nCurrentCategory = nIndex; + m_pStackedWidget->setCurrentIndex(nIndex); + m_pStatusLabel->setText( + tr("Please correct the invalid parameter values.")); + validateAllPages(); + return; + } + } + + // 第二步:把完整工作副本一次性交给数据源。 + QString sError; + if (!m_pSession->commit(sError)) + { + m_pStatusLabel->setText(sError); + QMessageBox::warning(this, tr("Reservoir properties"), sError); + return; + } + + // 第三步:仅在提交成功后关闭对话框。 + iDlgBase::accept(); +} + +void nmWxReservoirPropertiesEditor::reject() +{ + // 工作副本从未提前写入真实数据,取消时直接关闭即可。 + iDlgBase::reject(); +} diff --git a/Src4/nmNum/nmSubWxs/nmSubWxs.pro b/Src4/nmNum/nmSubWxs/nmSubWxs.pro index 370bf5b..74d2e04 100644 --- a/Src4/nmNum/nmSubWxs/nmSubWxs.pro +++ b/Src4/nmNum/nmSubWxs/nmSubWxs.pro @@ -1,4 +1,6 @@ -QT += core sql gui widgets +QT += core sql gui + +greaterThan(QT_MAJOR_VERSION, 4) : QT += widgets greaterThan(QT_MAJOR_VERSION, 4) : QT += printsupport #greaterThan(QT_MAJOR_VERSION, 4) : QT += axcontainer printsupport