#include "nmWellParameterPanel.h" #include "nmDataAttribute.h" #include "nmDataHorizontalFracturedWell.h" #include "nmDataPerforation.h" #include "nmDataWellBase.h" #include "nmParameterField.h" #include "nmWellParameterCatalog.h" #include #include #include #include #include #include #include #include #include #include namespace { struct PerforationCandidate { QString m_sName; double m_dStart; double m_dEnd; double m_dSkin; }; } nmWellParameterPanel::nmWellParameterPanel( nmIParameterUnitService* pUnitService, QWidget* pParent) : QWidget(pParent), m_pUnitService(pUnitService), m_pWell(nullptr), m_pMainLayout(new QVBoxLayout(this)), m_pPerforationComboBox(nullptr), m_listFields(), m_mapFields(), m_nCurrentPerforationIndex(0), m_bReloading(false), m_bUpdatingModel(false) { // 参数分组使用透明背景,由面板统一填充主题基础色。 setAutoFillBackground(true); setBackgroundRole(QPalette::Base); m_pMainLayout->setContentsMargins(8, 8, 8, 8); m_pMainLayout->setSpacing(10); setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum); } nmWellParameterPanel::~nmWellParameterPanel() { // 井对象由会话拥有,析构时只解除连接,不在面板中释放。 if (m_pWell != nullptr) { disconnect(m_pWell, nullptr, this, nullptr); } m_pUnitService = nullptr; m_pWell = nullptr; } bool nmWellParameterPanel::setWellData(nmDataWellBase* pWell, QString& sError) { if (pWell == nullptr) { sError = tr("The well working copy is unavailable."); return false; } if (m_pWell != nullptr) { disconnect(m_pWell, nullptr, this, nullptr); } // 第一步:井型切换时只重建一次目录分组。 m_pWell = pWell; m_nCurrentPerforationIndex = 0; clearGroups(); QList listGroups = nmWellParameterCatalog::createGroups(m_pWell->getWellType()); for (int nIndex = 0; nIndex < listGroups.size(); ++nIndex) { if (!buildGroup(listGroups[nIndex], sError)) { return false; } } m_pMainLayout->addStretch(1); // 第二步:数据交互和字段刷新复用现有控件,不重复创建窗口对象。 connect(m_pWell, SIGNAL(sigWellDataChanged()), this, SLOT(slotWellDataChanged())); connect(m_pWell, SIGNAL(sigParameterChanged()), this, SLOT(slotWellDataChanged())); reloadPerforationSelector(); reloadFromWell(); sError.clear(); return true; } bool nmWellParameterPanel::commitPendingEdits(QString& sError) { // 第一步:先收集并校验全部字段,任何错误都不能产生部分写入。 QMap mapValues; QVector vecPerforations; if (!collectValues(mapValues, sError, &vecPerforations)) { emit sigValidityChanged(false); emit sigValidationMessageChanged(sError); return false; } // 第二步:普通属性直接更新;井筒范围和射孔留给同一次批量提交。 QString sBottomholeMdId = nmWellParameterCatalog::parameterId( NM_WELL_PARAMETER_BOTTOMHOLE_MD); QString sWellLengthId = nmWellParameterCatalog::parameterId( NM_WELL_PARAMETER_LENGTH); bool bWellboreSnapshotChanged = false; m_bUpdatingModel = true; for (int nIndex = 0; nIndex < m_listFields.size(); ++nIndex) { nmParameterField* pField = m_listFields[nIndex]; if (pField == nullptr || pField->isEnum()) { continue; } nmDataAttribute* pAttribute = nmWellParameterCatalog::attribute( m_pWell, pField->parameterId(), m_nCurrentPerforationIndex); if (pAttribute == nullptr) { continue; } QVariant oValue = mapValues.value(pField->parameterId()); if (pField->isText()) { oValue = oValue.toString().trimmed(); } bool bChanged = pField->isText() ? pAttribute->getValue().toString() != oValue.toString() : qAbs(pAttribute->getValue().toDouble() - oValue.toDouble()) > qMax(1.0, qAbs(oValue.toDouble())) * 1.0e-12; if (bChanged) { QString sParameterId = pField->parameterId(); if (sParameterId == sBottomholeMdId || sParameterId == sWellLengthId || nmWellParameterCatalog::isPerforationParameter( sParameterId)) { bWellboreSnapshotChanged = true; } else { pAttribute->setValue(oValue); } } } // 第三步:模型跳过井长隐式缩放,原子写入刚刚校验通过的完整快照。 if (bWellboreSnapshotChanged) { double dBottomholeMd = mapValues.value( sBottomholeMdId, m_pWell->getBottomholeMD().getValue()).toDouble(); double dWellLength = mapValues.value( sWellLengthId, m_pWell->getWellLength().getValue()).toDouble(); if (!m_pWell->applyWellboreEdit( dBottomholeMd, dWellLength, vecPerforations)) { m_bUpdatingModel = false; sError = tr("Failed to apply the validated wellbore parameters."); emit sigValidityChanged(false); emit sigValidationMessageChanged(sError); return false; } } m_bUpdatingModel = false; // 第四步:只更新当前射孔名称,避免提交时重建选择器吞掉切换操作。 updateCurrentPerforationItem(); sError.clear(); emit sigValidityChanged(true); emit sigValidationMessageChanged(QString()); return true; } bool nmWellParameterPanel::isInputValid() { QMap mapValues; QString sError; return collectValues(mapValues, sError); } void nmWellParameterPanel::focusFirstInvalid() { // 按界面显示顺序定位错误,使用户无需自行查找红色字段。 for (int nIndex = 0; nIndex < m_listFields.size(); ++nIndex) { nmParameterField* pField = m_listFields[nIndex]; if (pField != nullptr && !pField->validate() && pField->valueEditor() != nullptr) { pField->valueEditor()->setFocus(); pField->valueEditor()->selectAll(); return; } } // 非当前射孔出错时保留未提交输入,仅将焦点移到射孔选择器供用户定位。 if (m_pPerforationComboBox != nullptr && !m_pPerforationComboBox->toolTip().isEmpty()) { m_pPerforationComboBox->setFocus(); } } void nmWellParameterPanel::reloadFromWell() { if (m_pWell == nullptr || m_bUpdatingModel) { return; } // 程序回填期间屏蔽编辑槽,避免刷新再次触发模型提交。 m_bReloading = true; for (int nIndex = 0; nIndex < m_listFields.size(); ++nIndex) { nmParameterField* pField = m_listFields[nIndex]; nmDataAttribute* pAttribute = pField != nullptr ? nmWellParameterCatalog::attribute( m_pWell, pField->parameterId(), m_nCurrentPerforationIndex) : nullptr; // 目录可复用于多个派生井型,不存在的数据属性直接隐藏。 if (pField != nullptr) { pField->setFieldVisible(pAttribute != nullptr); } if (pField != nullptr && pAttribute != nullptr) { pField->setExternalValidationError(QString()); pField->setBaseValue(pAttribute->getValue()); } } m_bReloading = false; emit sigValidityChanged(isInputValid()); } void nmWellParameterPanel::slotFieldValueEdited() { if (m_bReloading) { return; } // 输入过程中只更新状态,避免每个按键都重建裂缝或刷新轨迹。 bool bValid = isInputValid(); emit sigValidityChanged(bValid); if (bValid) { emit sigValidationMessageChanged(QString()); } } void nmWellParameterPanel::slotFieldEditingFinished() { if (m_bReloading) { return; } // 失去焦点时一次性写工作副本,轨迹只响应最终有效值。 QString sError; if (!commitPendingEdits(sError)) { focusFirstInvalid(); } } void nmWellParameterPanel::slotPerforationChanged(int nIndex) { if (m_bReloading || m_pPerforationComboBox == nullptr || nIndex < 0) { return; } QVariant oPerforationIndex = m_pPerforationComboBox->itemData(nIndex); if (!oPerforationIndex.isValid()) { return; } int nTargetPerforationIndex = oPerforationIndex.toInt(); if (nTargetPerforationIndex == m_nCurrentPerforationIndex) { return; } // 第一步:离开当前射孔前提交输入,失败则恢复原选择。 int nPreviousPerforationIndex = m_nCurrentPerforationIndex; QString sError; if (!commitPendingEdits(sError)) { int nPreviousComboIndex = m_pPerforationComboBox->findData( nPreviousPerforationIndex); m_bReloading = true; if (nPreviousComboIndex >= 0) { m_pPerforationComboBox->setCurrentIndex( nPreviousComboIndex); } m_bReloading = false; focusFirstInvalid(); return; } // 第二步:切换成功后复用现有字段显示目标射孔数据。 m_nCurrentPerforationIndex = nTargetPerforationIndex; reloadFromWell(); } void nmWellParameterPanel::slotWellDataChanged() { if (m_bUpdatingModel) { return; } // 轨迹控制器可能修改射孔集合,选择器和字段必须一起同步。 reloadPerforationSelector(); reloadFromWell(); } void nmWellParameterPanel::clearGroups() { m_listFields.clear(); m_mapFields.clear(); m_pPerforationComboBox = nullptr; // 字段对象以分组为QObject父对象,删除分组即可统一释放字段和控件。 while (m_pMainLayout->count() > 0) { QLayoutItem* pItem = m_pMainLayout->takeAt(0); if (pItem->widget() != nullptr) { delete pItem->widget(); } delete pItem; } } bool nmWellParameterPanel::buildGroup( const nmWellParameterGroupInfo& oGroupInfo, QString& sError) { // 第一步:建立与通用参数字段一致的标签、值和单位三列布局。 QGroupBox* pGroupBox = new QGroupBox(oGroupInfo.m_sTitle, this); 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, nmParameterField::editorMinimumWidth()); pFieldLayout->setColumnMinimumWidth( 2, nmParameterField::unitMinimumWidth()); // 第二步:射孔选择器只占第二列,不侵入单位列。 int nRowOffset = 0; if (oGroupInfo.m_bPerforationGroup) { QLabel* pSelectorLabel = new QLabel(tr("Perforation"), pGroupBox); pSelectorLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter); m_pPerforationComboBox = new QComboBox(pGroupBox); m_pPerforationComboBox->setFixedHeight( nmParameterField::editorHeight()); pFieldLayout->addWidget(pSelectorLabel, 0, 0); pFieldLayout->addWidget(m_pPerforationComboBox, 0, 1); connect(m_pPerforationComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(slotPerforationChanged(int))); nRowOffset = 1; } // 第三步:按目录顺序创建字段,并建立稳定ID到字段对象的索引。 for (int nIndex = 0; nIndex < oGroupInfo.m_listFields.size(); ++nIndex) { nmParameterField* pField = new nmParameterField( oGroupInfo.m_listFields[nIndex], m_pUnitService, pGroupBox); if (!pField->initialize(pFieldLayout, nIndex + nRowOffset, 0, sError)) { delete pGroupBox; return false; } pField->setValueRightAligned(!pField->isText()); if (pField->valueEditor() != nullptr) { connect(pField->valueEditor(), SIGNAL(textChanged(QString)), this, SLOT(slotFieldValueEdited())); connect(pField->valueEditor(), SIGNAL(editingFinished()), this, SLOT(slotFieldEditingFinished())); } m_listFields.append(pField); m_mapFields.insert(pField->parameterId(), pField); } m_pMainLayout->addWidget(pGroupBox); return true; } void nmWellParameterPanel::reloadPerforationSelector() { if (m_pPerforationComboBox == nullptr || m_pWell == nullptr) { return; } // 第一步:增量同步列表,避免清空控件导致焦点切换和点击事件失效。 bool bWasReloading = m_bReloading; m_bReloading = true; int nPerforationCount = m_pWell->getPerforationCount(); if (nPerforationCount <= 0) { while (m_pPerforationComboBox->count() > 1) { m_pPerforationComboBox->removeItem( m_pPerforationComboBox->count() - 1); } if (m_pPerforationComboBox->count() <= 0) { m_pPerforationComboBox->addItem(tr("No perforations")); } else { m_pPerforationComboBox->setItemText( 0, tr("No perforations")); m_pPerforationComboBox->setItemData(0, QVariant()); } m_pPerforationComboBox->setEnabled(false); m_nCurrentPerforationIndex = -1; m_pPerforationComboBox->setCurrentIndex(0); } else { while (m_pPerforationComboBox->count() > nPerforationCount) { m_pPerforationComboBox->removeItem( m_pPerforationComboBox->count() - 1); } for (int nIndex = 0; nIndex < nPerforationCount; ++nIndex) { QString sName = perforationDisplayName(nIndex); if (nIndex >= m_pPerforationComboBox->count()) { m_pPerforationComboBox->addItem(sName, nIndex); } else { if (m_pPerforationComboBox->itemText(nIndex) != sName) { m_pPerforationComboBox->setItemText(nIndex, sName); } QVariant oItemData = m_pPerforationComboBox->itemData(nIndex); if (!oItemData.isValid() || oItemData.toInt() != nIndex) { m_pPerforationComboBox->setItemData(nIndex, nIndex); } } } // 第二步:集合变化后约束业务索引,再通过itemData定位界面项。 m_pPerforationComboBox->setEnabled(true); m_nCurrentPerforationIndex = qBound( 0, m_nCurrentPerforationIndex, nPerforationCount - 1); int nCurrentComboIndex = m_pPerforationComboBox->findData( m_nCurrentPerforationIndex); if (nCurrentComboIndex >= 0 && m_pPerforationComboBox->currentIndex() != nCurrentComboIndex) { m_pPerforationComboBox->setCurrentIndex(nCurrentComboIndex); } } m_bReloading = bWasReloading; } QString nmWellParameterPanel::perforationDisplayName( int nPerforationIndex) const { nmDataPerforation* pPerforation = m_pWell != nullptr ? m_pWell->getPerforation(nPerforationIndex) : nullptr; QString sName = pPerforation != nullptr ? pPerforation->getName().getValue().toString().trimmed() : QString(); // 旧工程可能没有射孔名称,界面提供稳定的序号回退文本。 return sName.isEmpty() ? tr("Perforation %1").arg(nPerforationIndex + 1) : sName; } void nmWellParameterPanel::updateCurrentPerforationItem() { if (m_pPerforationComboBox == nullptr || m_pWell == nullptr || m_nCurrentPerforationIndex < 0) { return; } int nComboIndex = m_pPerforationComboBox->findData( m_nCurrentPerforationIndex); if (nComboIndex < 0) { return; } QString sName = perforationDisplayName( m_nCurrentPerforationIndex); if (m_pPerforationComboBox->itemText(nComboIndex) == sName) { return; } // 只修改文本并屏蔽选择信号,不改变当前索引和弹出列表状态。 bool bWasReloading = m_bReloading; m_bReloading = true; m_pPerforationComboBox->setItemText(nComboIndex, sName); m_bReloading = bWasReloading; } bool nmWellParameterPanel::collectValues( QMap& mapValues, QString& sError, QVector* pVecPerforations) { // 第一步:清除上一次跨字段校验留下的错误,重新计算完整状态。 mapValues.clear(); if (m_pPerforationComboBox != nullptr) { m_pPerforationComboBox->setToolTip(QString()); } for (int nIndex = 0; nIndex < m_listFields.size(); ++nIndex) { if (m_listFields[nIndex] != nullptr) { m_listFields[nIndex]->setExternalValidationError(QString()); } } // 第二步:字段统一转换为业务基准单位后写入临时映射。 for (int nIndex = 0; nIndex < m_listFields.size(); ++nIndex) { nmParameterField* pField = m_listFields[nIndex]; if (pField == nullptr || !pField->isFieldVisible() || pField->isEnum()) { continue; } bool bValid = false; QVariant oValue = pField->baseValue(bValid); if (!bValid || !oValue.isValid()) { sError = tr("Please correct the invalid parameter values."); return false; } mapValues.insert(pField->parameterId(), oValue); } // 第三步:单字段合法后再执行依赖多个参数的射孔规则。 if (!validatePerforationValues( mapValues, sError, pVecPerforations)) { return false; } sError.clear(); return true; } bool nmWellParameterPanel::validatePerforationValues( const QMap& mapValues, QString& sError, QVector* pVecPerforations) { if (pVecPerforations != nullptr) { pVecPerforations->clear(); } if (m_pWell == nullptr) { return true; } // 使用稳定ID读取临时值,校验逻辑不依赖界面语言和XML显示名称。 QString sNameId = nmWellParameterCatalog::parameterId( NM_WELL_PARAMETER_PERFORATION_NAME); QString sStartId = nmWellParameterCatalog::parameterId( NM_WELL_PARAMETER_PERFORATION_MD_START); QString sEndId = nmWellParameterCatalog::parameterId( NM_WELL_PARAMETER_PERFORATION_MD_END); QString sSkinId = nmWellParameterCatalog::parameterId( NM_WELL_PARAMETER_PERFORATION_SKIN); QString sBottomholeMdId = nmWellParameterCatalog::parameterId( NM_WELL_PARAMETER_BOTTOMHOLE_MD); QString sWellLengthId = nmWellParameterCatalog::parameterId( NM_WELL_PARAMETER_LENGTH); int nPerforationCount = m_pWell->getPerforationCount(); if (nPerforationCount <= 0) { if (dynamic_cast(m_pWell) != nullptr) { sError = tr( "A multistage fractured horizontal well requires at least one perforation."); if (m_pPerforationComboBox != nullptr) { m_pPerforationComboBox->setToolTip(sError); } return false; } return true; } // 第一步:输入过程只构造轻量候选值,正式提交时才按需深拷贝完整射孔。 QVector vecCandidates; vecCandidates.reserve(nPerforationCount); if (pVecPerforations != nullptr) { pVecPerforations->reserve(nPerforationCount); } for (int nIndex = 0; nIndex < nPerforationCount; ++nIndex) { nmDataPerforation* pPerforation = m_pWell->getPerforation(nIndex); if (pPerforation == nullptr) { sError = tr("Please correct the invalid parameter values."); return false; } PerforationCandidate oCandidate; oCandidate.m_sName = pPerforation->getName().getValue().toString().trimmed(); oCandidate.m_dStart = pPerforation->getMdStart().getValue().toDouble(); oCandidate.m_dEnd = pPerforation->getMdEnd().getValue().toDouble(); oCandidate.m_dSkin = pPerforation->getSkin().getValue().toDouble(); if (nIndex == m_nCurrentPerforationIndex) { oCandidate.m_sName = mapValues.value( sNameId, oCandidate.m_sName).toString().trimmed(); oCandidate.m_dStart = mapValues.value( sStartId, oCandidate.m_dStart).toDouble(); oCandidate.m_dEnd = mapValues.value( sEndId, oCandidate.m_dEnd).toDouble(); oCandidate.m_dSkin = mapValues.value( sSkinId, oCandidate.m_dSkin).toDouble(); } vecCandidates.append(oCandidate); if (pVecPerforations != nullptr) { nmDataPerforation oPerforationSnapshot(*pPerforation); oPerforationSnapshot.getName().setValue(oCandidate.m_sName); oPerforationSnapshot.getMdStart().setValue(oCandidate.m_dStart); oPerforationSnapshot.getMdEnd().setValue(oCandidate.m_dEnd); oPerforationSnapshot.getSkin().setValue(oCandidate.m_dSkin); pVecPerforations->append(oPerforationSnapshot); } } // 第二步:逐项检查名称和MD顺序,错误文字带序号便于定位非当前射孔。 for (int nIndex = 0; nIndex < vecCandidates.size(); ++nIndex) { const PerforationCandidate& oCandidate = vecCandidates[nIndex]; if (oCandidate.m_sName.isEmpty()) { QString sRuleError = tr("Perforation name cannot be empty."); sError = perforationErrorText(nIndex, sRuleError); if (nIndex == m_nCurrentPerforationIndex) { nmParameterField* pNameField = field(sNameId); if (pNameField != nullptr) { pNameField->setExternalValidationError(sError); } } else if (m_pPerforationComboBox != nullptr) { m_pPerforationComboBox->setToolTip(sError); } return false; } if (oCandidate.m_dStart > oCandidate.m_dEnd) { QString sRuleError = tr( "Perforation start MD cannot exceed end MD."); sError = perforationErrorText(nIndex, sRuleError); setPerforationError(nIndex, sError); return false; } } // 第三步:以候选井身参数检查全部射孔;水平井从第一段射孔起点延伸。 double dWellTop = mapValues.value( sBottomholeMdId, m_pWell->getBottomholeMD().getValue()).toDouble(); double dWellLength = mapValues.value( sWellLengthId, m_pWell->getWellLength().getValue()).toDouble(); nmDataHorizontalFracturedWell* pHorizontal = dynamic_cast(m_pWell); if (pHorizontal != nullptr && !vecCandidates.isEmpty()) { dWellTop = vecCandidates[0].m_dStart; } double dWellBottom = dWellTop + dWellLength; double dBoundaryTolerance = qMax( 1.0, qMax(qAbs(dWellTop), qAbs(dWellBottom))) * 1.0e-10; for (int nIndex = 0; nIndex < vecCandidates.size(); ++nIndex) { const PerforationCandidate& oCandidate = vecCandidates[nIndex]; if (oCandidate.m_dStart < dWellTop - dBoundaryTolerance || oCandidate.m_dEnd > dWellBottom + dBoundaryTolerance) { QString sRuleError = tr( "The perforation interval is outside the wellbore."); sError = perforationErrorText(nIndex, sRuleError); setPerforationError(nIndex, sError); return false; } } // 第四步:两两检查全部候选区间,端点相接允许但区间不能相交。 for (int nFirstIndex = 0; nFirstIndex < vecCandidates.size(); ++nFirstIndex) { for (int nSecondIndex = nFirstIndex + 1; nSecondIndex < vecCandidates.size(); ++nSecondIndex) { const PerforationCandidate& oFirst = vecCandidates[nFirstIndex]; const PerforationCandidate& oSecond = vecCandidates[nSecondIndex]; if (oFirst.m_dStart < oSecond.m_dEnd - dBoundaryTolerance && oFirst.m_dEnd > oSecond.m_dStart + dBoundaryTolerance) { int nErrorIndex = nFirstIndex == m_nCurrentPerforationIndex ? nFirstIndex : nSecondIndex; QString sRuleError = tr( "Perforation intervals cannot overlap."); sError = perforationErrorText(nErrorIndex, sRuleError); setPerforationError(nErrorIndex, sError); return false; } } } return true; } nmParameterField* nmWellParameterPanel::field( const QString& sParameterId) const { return m_mapFields.value(sParameterId, nullptr); } QString nmWellParameterPanel::perforationErrorText( int nPerforationIndex, const QString& sRuleError) const { return QString("%1 %2: %3") .arg(tr("Perforation")) .arg(nPerforationIndex + 1) .arg(sRuleError); } void nmWellParameterPanel::setPerforationError( int nPerforationIndex, const QString& sError) { if (m_pPerforationComboBox != nullptr) { m_pPerforationComboBox->setToolTip(sError); } if (nPerforationIndex != m_nCurrentPerforationIndex) { return; } nmParameterField* pStartField = field( nmWellParameterCatalog::parameterId( NM_WELL_PARAMETER_PERFORATION_MD_START)); nmParameterField* pEndField = field( nmWellParameterCatalog::parameterId( NM_WELL_PARAMETER_PERFORATION_MD_END)); if (pStartField != nullptr) { pStartField->setExternalValidationError(sError); } if (pEndField != nullptr) { pEndField->setExternalValidationError(sError); } }