diff --git a/Bin/Config/Lang/cn/nmNum_cn.qm b/Bin/Config/Lang/cn/nmNum_cn.qm
index 32bc151..3092a7f 100644
Binary files a/Bin/Config/Lang/cn/nmNum_cn.qm and b/Bin/Config/Lang/cn/nmNum_cn.qm differ
diff --git a/Bin/Config/Lang/cn/nmNum_cn.ts b/Bin/Config/Lang/cn/nmNum_cn.ts
index e5a7886..138c4fa 100644
--- a/Bin/Config/Lang/cn/nmNum_cn.ts
+++ b/Bin/Config/Lang/cn/nmNum_cn.ts
@@ -1,4 +1,4 @@
-
+
@@ -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