Merge branch 'feature/Fix-TimeDependentSkin-20261029' into develop

# Conflicts:
#	Bin/Config/Lang/cn/nmNum_cn.qm   resolved by develop version
develop
lvjunjie 1 week ago
commit 02d3c62f72

@ -7784,6 +7784,66 @@ Average pressure in contour: %2 MPa</source>
</context>
<context>
<name>nmWxTimeDependentSkin</name>
<message>
<source>Select a well with valid model results before estimating skin.</source>
<translation>请先选择具有有效模型结果的井,再估算表皮。</translation>
</message>
<message>
<source>Skin estimation requires a single perforation.</source>
<translation>自动估算表皮仅支持单射孔。</translation>
</message>
<message>
<source>Skin estimation does not yet support perforation closing intervals.</source>
<translation>自动估算表皮暂不支持设有射孔封堵区间的模型。</translation>
</message>
<message>
<source>Generate a model with the current inputs and a constant skin before computing skin steps.</source>
<translation>请先使用当前输入和恒定表皮生成模型结果,再计算各段表皮。</translation>
</message>
<message>
<source>The model result does not match this well&apos;s pressure and rate data. Generate the model again.</source>
<translation>模型结果与当前井的压力、流量数据不一致,请重新生成模型。</translation>
</message>
<message>
<source>Observation wells have no controlled rate for skin estimation.</source>
<translation>观察井没有可用于估算表皮的自身控制流量。</translation>
</message>
<message>
<source>Skin estimation requires model and measured pressure curves.</source>
<translation>自动估算表皮需要模型压力曲线和实测压力曲线。</translation>
</message>
<message>
<source>The model requires valid permeability, thickness and skin.</source>
<translation>模型的渗透率、厚度和表皮参数必须有效。</translation>
</message>
<message>
<source>Skin estimation supports single-phase oil, gas, water and oil-water models only.</source>
<translation>自动估算表皮支持单相油、气、水以及油水两相模型。</translation>
</message>
<message>
<source>Set positive reference volume factors and viscosities in the analysis PVT settings.</source>
<translation>请在分析的 PVT 设置中填写有效的参考体积系数和黏度,数值必须大于零。</translation>
</message>
<message>
<source>Oil-water estimation requires finite phase rates with the same flow direction.</source>
<translation>油水两相估算要求各相流量有效,且油、水流动方向一致。</translation>
</message>
<message>
<source>Variable-PVT skin estimation requires finite, positive B and viscosity values on a monotonic pressure table.</source>
<translation>变 PVT 表皮估算要求压力表严格单调,体积系数和黏度均为有效正数。</translation>
</message>
<message>
<source>Cannot read reference PVT values from this analysis.</source>
<translation>无法读取本分析的 PVT 参考值。</translation>
</message>
<message>
<source>The model rate schedule or pressure conversion factor is invalid.</source>
<translation>模型的流量制度或压力换算系数无效。</translation>
</message>
<message>
<source>No skin steps could be estimated. Check pressure data, non-zero rate steps and the gas PVT pressure range.</source>
<translation>没有可估算的表皮分段。请检查压力数据、非零流量段及气体 PVT 压力范围。</translation>
</message>
<message>
<source>Time Dependent Skin</source>
<translation>时间相关表皮系数</translation>

@ -473,6 +473,8 @@ public:
// 获取PEBI求解器需要的pvt数据
nmDataPvtParaForPebi* getPebiPvtPara();
// 读取所属分析窗口当前选用的 PVT 单值,参数名与底层 PVT 接口一致。
bool getAnalysisPvtValues(const QStringList& names, QMap<QString, double>& values) const;
// 获取气单相变化 PVT 求解所需的压力/拟压力转换表。
bool getPebiPseudoPressureTable(std::vector<double>& pressure,
std::vector<double>& pseudoPressure);

@ -1,4 +1,4 @@
#ifndef NM_DIALOG_PREVIEW_GUARD_H
#ifndef NM_DIALOG_PREVIEW_GUARD_H
#define NM_DIALOG_PREVIEW_GUARD_H
#include <QEvent>
@ -20,6 +20,26 @@ public:
window->installEventFilter(this);
}
// 已隐藏的窗口可立即清理,供结果窗在恢复尺寸前解除原生最小化状态。
void resetHiddenWindow()
{
cancelPendingReset();
if(m_window.isNull() || m_window->isVisible() || !m_window->isWindow() ||
!m_window->internalWinId()) return;
m_rebuilding = true;
// 重建原生窗口会触发事件,期间本对象可能被释放,用弱指针检查是否仍然存活。
QPointer<nmDialogPreviewGuard> alive(this);
const QRect geometry = m_window->geometry();
// 在当前界面库的系统后端,即使父对象和标志不变,双参数调用也会重建原生窗口。
// 子控件的原生父窗口由界面库同步调整,不能绕过界面库直接销毁系统句柄。
m_window->setParent(m_window->parentWidget(), m_window->windowFlags());
if(alive.isNull()) return;
// 恢复原有位置和尺寸,确保下次打开时窗口布局不因句柄重建而改变。
if(!m_window.isNull()) m_window->setGeometry(geometry);
if(!alive.isNull()) m_rebuilding = false;
}
protected:
// 只监听目标窗口的显式隐藏和重新显示;重建过程产生的事件不重复处理。
bool eventFilter(QObject* object, QEvent* event)
@ -52,21 +72,7 @@ protected:
QObject::timerEvent(event);
return;
}
cancelPendingReset();
if(m_window.isNull() || m_window->isVisible() || !m_window->isWindow() ||
!m_window->internalWinId()) return;
m_rebuilding = true;
// 重建原生窗口会触发事件,期间本对象可能被释放,用弱指针检查是否仍然存活。
QPointer<nmDialogPreviewGuard> alive(this);
const QRect geometry = m_window->geometry();
// 在当前界面库的系统后端,即使父对象和标志不变,双参数调用也会重建原生窗口。
// 子控件的原生父窗口由界面库同步调整,不能绕过界面库直接销毁系统句柄。
m_window->setParent(m_window->parentWidget(), m_window->windowFlags());
if(alive.isNull()) return;
// 恢复原有位置和尺寸,确保下次打开时窗口布局不因句柄重建而改变。
if(!m_window.isNull()) m_window->setGeometry(geometry);
if(!alive.isNull()) m_rebuilding = false;
resetHiddenWindow();
}
private:

@ -120,7 +120,10 @@ private:
QGridLayout *m_parametersLayout;
// Base parameters
nmGUIComponentLineEdit *m_initialPressureComponent;
QLineEdit *m_initialPressureEdit;
QComboBox *m_initialPressureUnit;
nmDataAttribute *m_initialPressure; ///< 当前预测模式的初始压力。
iUnitWxBinder *m_pressureUnitBinder;
QCheckBox *m_abandonmentRateCheck;
nmGUIComponentLineEdit *m_abandonmentRateComponent;

@ -9,6 +9,7 @@
#include <QTreeWidget>
#include <QLabel>
#include <QDateTime>
#include <QMap>
#include "nmDataPerforation.h"
#include "nmDataWellBase.h"
@ -30,13 +31,14 @@ public:
void onOkClicked();
void onCancelClicked();
void onItemChanged(QTreeWidgetItem* item, int column);
void onCurrentItemChanged(QTreeWidgetItem* current, QTreeWidgetItem* previous);
private:
void initUI();
QString formatNumber(double value);
void refreshDisplay();
void processDateEdit(QTreeWidgetItem* item);
void processValueEdit(QTreeWidgetItem* item);
void storeCurrentPerforation();
void saveDataToModel(); // 保存界面数据到模型
void loadDataFromModel(); // 从模型加载数据到界面
@ -72,7 +74,7 @@ private:
bool m_showDatesMode;
bool m_hasBaseTime;
nmDataPerforation m_perforationData;
QMap<int, nmDataPerforation> m_perforations; // 各射孔的编辑副本,确认前不写入井对象
nmDataWellBase* m_currentWell;
int m_currentPerforationIndex;
};

@ -6,6 +6,7 @@
#include <QPair>
#include <QPainter>
#include <QPolygon>
#include <QPointer>
#include "nmDataAnalyzeManager.h"
#include "nmDataWellBase.h"
@ -44,14 +45,17 @@ public:
void onRemoveClicked();
void onSplitStepsClicked();
void onComputeClicked();
void updateComputeState();
void onSkinVsRateClicked();
void onPerforationClosingBtnClicked();
void onPerforationClosingAccepted();
// 表格数据变化事件
void onTableDataChanged(int row, int column);
void onTableRowClicked(int row, int column);
// 确认和取消按钮
void onAccept();
void onReject();
void onWellDestroyed();
// 右侧按钮槽函数
void onRightAddClicked();
@ -90,12 +94,12 @@ private:
// 初始化UI
void initUI();
void reloadFromDataSource(nmDataWellBase* pSpecificWellData);
// 更新图表
void updateCharts();
// 检查自动估算表皮所需的基准结果和模型范围。
QString skinComputeUnavailableReason() const;
void showInteractiveChart(); // 显示交互式图表
void restoreOriginalCharts(); // 恢复原始占位符
void initializeChartFromData();
@ -139,7 +143,7 @@ private:
void snapAllBoundariesToFlowStepsAndMerge();
// 工具函数
double convertTime(double value, const QString& fromUnit, const QString& toUnit) const;
static double convertTime(double value, const QString& fromUnit, const QString& toUnit);
void refreshTableStructure(); // 根据是否启用“流量相关”重建表头/单位行
double convertDSdQ(double v, const QString& fromU, const QString& toU) const;
@ -148,10 +152,6 @@ private:
static double fromSeconds(double sec, const QString& unit);
bool moveFlowSegmentBoundarySec(int segmentIndex, double newBoundarySec);
// SkinVsRate模式的状态保存和恢复
void saveStateBeforeSkinVsRate();
void restoreStateAfterSkinVsRate();
// 添加辅助方法
QString formatTimeDisplay(double timeInHours, int segmentIndex) const;
void updateTimeUnitComboVisibility();
@ -216,6 +216,7 @@ private:
QString m_dSdQDisplayUnit;
// 数据成员
QPointer<nmDataAnalyzeManager> m_pDataManager; // 窗口所属分析,计算时不随当前页签切换
nmDataWellBase* m_currentWell; // 当前井数据
nmDataPerforation m_perforationData; // 射孔段数据
int m_currentPerforationIndex; // 当前射孔段索引
@ -229,14 +230,8 @@ private:
bool m_hasPerformedInitialFit;
bool m_rateDependentState;
// 保存进入SkinVsRate模式前的完整数据状态
nmDataPerforation m_savedFlowSegmentData;
bool m_savedRateDependentState;
// 静态成员变量
static nmWxTimeDependentSkin* s_instance;
static QString s_currentWellName;
static nmDataWellBase* s_currentWellData;
};
#endif // NMWXTIMEDEPENDENTSKIN_H

@ -4169,6 +4169,14 @@ nmDataPvtParaForPebi* nmDataAnalyzeManager::getPebiPvtPara()
return m_pebiPvtPara;
}
bool nmDataAnalyzeManager::getAnalysisPvtValues(const QStringList& names, QMap<QString, double>& values) const
{
// 按创建本 Manager 时绑定的分析读取,避免切换页签后串用其他分析的参考物性。
values.clear();
nmDataAnalyzeContextProvider* context = nmDataAnalyzeContext::provider();
return context && m_pOwnerFitting && context->getPvtParaValues(m_pOwnerFitting, names, values);
}
bool nmDataAnalyzeManager::getPebiPseudoPressureTable(std::vector<double>& pressure,
std::vector<double>& pseudoPressure)
{

@ -107,8 +107,100 @@
#include <QPointer>
#include <QtCore/qnumeric.h>
#ifdef Q_OS_WIN
#include <qt_windows.h>
#include <dwmapi.h>
#pragma comment(lib, "dwmapi.lib")
#pragma comment(lib, "gdi32.lib")
#endif
namespace
{
// 结果窗保留原生最小化小框,仅替换最小化期间的系统预览。
class nmResultDialog : public QDialog
{
public:
explicit nmResultDialog(QWidget* parent, Qt::WindowFlags flags)
: QDialog(parent, flags), m_previewGuard(new nmDialogPreviewGuard(this)),
m_nativeMinimized(false)
{
}
void hideAndRestoreGeometry()
{
// 先隐藏再恢复,避免关闭小框时在原位置短暂显示大窗。
if(!isMinimized()) return;
const QRect geometryBeforeMinimize = m_geometryBeforeMinimize;
const Qt::WindowStates restoredState = windowState() & ~Qt::WindowMinimized;
hide();
setWindowState(restoredState);
// 复用预览清理来重建隐藏句柄,否则 Qt 4 调整原生最小化窗口尺寸时会重新显示它。
m_previewGuard->resetHiddenWindow();
if(!(restoredState & Qt::WindowMaximized) && geometryBeforeMinimize.isValid())
setGeometry(geometryBeforeMinimize);
setWindowState(restoredState);
m_nativeMinimized = false;
}
#ifdef Q_OS_WIN
protected:
bool winEvent(MSG* message, long* result) override
{
// 主窗口恢复时仅显示原来的小框,避免系统默认处理将结果窗展开。
if(message->message == WM_SHOWWINDOW && message->wParam &&
message->lParam == SW_PARENTOPENING && IsIconic(message->hwnd)) {
ShowWindow(message->hwnd, SW_SHOWMINNOACTIVE);
*result = 0;
return true;
}
if(message->message == WM_SIZE) {
// 恢复或最大化后交还实时预览;窗口句柄重建后也由尺寸消息重新设置。
const BOOL minimized = message->wParam == SIZE_MINIMIZED;
// Qt 此时尚未处理最小化尺寸消息,仍保留大窗的客户区位置和尺寸。
if(minimized && !m_nativeMinimized) m_geometryBeforeMinimize = geometry();
m_nativeMinimized = minimized;
DwmSetWindowAttribute(message->hwnd, DWMWA_FORCE_ICONIC_REPRESENTATION,
&minimized, sizeof(minimized));
DwmSetWindowAttribute(message->hwnd, DWMWA_HAS_ICONIC_BITMAP,
&minimized, sizeof(minimized));
DwmInvalidateIconicBitmaps(message->hwnd);
}
if((message->message == WM_DWMSENDICONICTHUMBNAIL ||
message->message == WM_DWMSENDICONICLIVEPREVIEWBITMAP) &&
IsIconic(message->hwnd)) {
// 透明位图只作用于任务栏预览,不隐藏结果窗或替换其原生标题栏。
BITMAPINFO info = {0};
info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
info.bmiHeader.biWidth = 1;
info.bmiHeader.biHeight = -1;
info.bmiHeader.biPlanes = 1;
info.bmiHeader.biBitCount = 32;
void* pixels = NULL;
HBITMAP bitmap = CreateDIBSection(NULL, &info, DIB_RGB_COLORS, &pixels, NULL, 0);
if(bitmap) {
*static_cast<DWORD*>(pixels) = 0;
const HRESULT status = message->message == WM_DWMSENDICONICTHUMBNAIL
? DwmSetIconicThumbnail(message->hwnd, bitmap, 0)
: DwmSetIconicLivePreviewBitmap(message->hwnd, bitmap, NULL, 0);
DeleteObject(bitmap);
if(SUCCEEDED(status)) {
*result = 0;
return true;
}
}
}
return QDialog::winEvent(message, result);
}
#endif
private:
nmDialogPreviewGuard* m_previewGuard;
QRect m_geometryBeforeMinimize;
bool m_nativeMinimized; // 主窗口恢复时可能重复通知最小化,不重复记录小框尺寸。
};
// 保留原来求解器单例的防重入语义:全局同一时间只允许一个DLL求解任务运行。
// QPointer会在QObject销毁后自动变空,避免保留已经释放的线程指针。
QPointer<nmCalculationDllPebiSolverTask> s_pRunningSolverTask;
@ -135,9 +227,8 @@ void clearResultCurveDataTables(iGuiPlot* plot)
void resetResultDialog(QDialog* window)
{
if(!window) return;
// 先恢复最小化状态再隐藏,避免隐藏后调整尺寸使系统重新显示空白窗口。
if(window->isMinimized())
window->setWindowState(window->windowState() & ~Qt::WindowMinimized);
// 最小化结果先在隐藏状态下恢复尺寸,再清理旧曲线。
static_cast<nmResultDialog*>(window)->hideAndRestoreGeometry();
window->hide();
foreach(iGuiPlot* plot, window->findChildren<iGuiPlot*>()) clearResultCurveDataTables(plot);
foreach(nmWxSensitivityShow* dialog, window->findChildren<nmWxSensitivityShow*>())
@ -1978,12 +2069,11 @@ void nmSubWndMain::onForecastDataChanged()
QDialog* nmSubWndMain::createResultDialog(const QString& name, const QString& title)
{
// 关闭只隐藏,窗口及绘图控件由所属分析统一销毁。
QDialog* dialog = new QDialog(m_pResultRefreshFitting.data(),
QDialog* dialog = new nmResultDialog(m_pResultRefreshFitting.data(),
Qt::WindowTitleHint | Qt::WindowSystemMenuHint | Qt::WindowMinMaxButtonsHint | Qt::WindowCloseButtonHint);
dialog->setObjectName(name);
dialog->setWindowTitle(title);
dialog->setAttribute(Qt::WA_DeleteOnClose, false);
new nmDialogPreviewGuard(dialog);
dialog->resize(900, 650);
QVBoxLayout* layout = new QVBoxLayout(dialog);
layout->setContentsMargins(0, 0, 0, 0);
@ -2154,10 +2244,10 @@ void nmSubWndMain::onReadonlyCurveDoubleClicked(
bool nmSubWndMain::eventFilter(QObject* watched, QEvent* event)
{
// 最小化时先恢复窗口状态,再交给默认关闭流程隐藏,避免复用时丢失正常尺寸。
// 最小化结果在隐藏后恢复尺寸,关闭过程中不显示大窗。
QDialog* dialog = qobject_cast<QDialog*>(watched);
if(dialog && event->type() == QEvent::Close && dialog->isMinimized())
dialog->setWindowState(dialog->windowState() & ~Qt::WindowMinimized);
if(dialog && event->type() == QEvent::Close)
static_cast<nmResultDialog*>(dialog)->hideAndRestoreGeometry();
// 关闭结果对话框时同步关闭其曲线数据表、Show 和统计设置窗口。
if((event->type() == QEvent::Close || event->type() == QEvent::Hide) &&
qobject_cast<QDialog*>(watched)) {

@ -49,10 +49,11 @@ void nmWxChartWidget::paintEvent(QPaintEvent *event)
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
int marginLeft = 55;
// 补偿窗口让出的左侧和底部空白,移动标题时保持绘图区位置和尺寸。
int marginLeft = 55 + contentsMargins().left();
int marginRight = 10;
int marginTop = 4;
int marginBottom = 45;
int marginBottom = 45 + contentsMargins().bottom();
chartRect = QRectF(marginLeft, marginTop,
width() - marginLeft - marginRight,
@ -249,15 +250,19 @@ void nmWxChartWidget::drawAxes(QPainter& painter)
}
}
// 轴标题
// 按文字实际边界贴近外沿,保持原有字号、绘图区和刻度位置。
painter.setFont(QFont("Arial", 10));
painter.drawText(chartRect.center().x() - 50, chartRect.bottom() + 35,
tr("Liquid rate [STB/D]"));
const QString xTitle = tr("Liquid rate [STB/D]");
const QRect xTitleRect = painter.fontMetrics().tightBoundingRect(xTitle);
painter.drawText(QPointF(chartRect.center().x() - xTitleRect.center().x(),
height() - 2 - xTitleRect.bottom()), xTitle);
painter.save();
painter.translate(15, chartRect.center().y());
painter.translate(1, chartRect.center().y());
painter.rotate(-90);
painter.drawText(-15, 0, tr("Skin"));
const QString yTitle = tr("Skin");
const QRect yTitleRect = painter.fontMetrics().tightBoundingRect(yTitle);
painter.drawText(QPointF(-yTitleRect.center().x(), -yTitleRect.top()), yTitle);
painter.restore();
}

@ -13,13 +13,75 @@
#include "nmGUIComponentLineEdit.h"
#include "nmPebiResultSnapshot.h"
#include "ZxBaseUtil.h"
#include "iUnitGroup.h"
#include "iUnitHelper.h"
#include "iUnitWxBinder.h"
#include "nmTranslationManager.h"
namespace {
iUnitGroup* forecastUnitGroup(UnitType type)
{
// 与参数面板共用底层时间、绝对压力和体积流量单位组。
return iUnitHelper::getUnitGroupByUnit(type == UNIT_TYPE_TIME ? "s" :
type == UNIT_TYPE_PRESSURE ? "MPa" : "stb/d");
}
QString forecastUnitName(iUnitGroup* group, QString unit)
{
// 只统一旧数据的名称写法,倍率仍由底层单位组提供。
unit.replace("^", "").replace(QChar(0x00b3), "3");
unit.replace("/D", "/d").replace("sec", "s").replace("hr", "h");
if(unit == "day" || unit == "Day") unit = "d";
if(unit == "psia") unit = "psi";
if(unit == "bara") unit = "bar";
if(unit == "KPa") unit = "kPa";
if(unit == "B/d") unit = "stb/d";
if(group) {
foreach(const QString& name, group->getAllUnitNames()) {
if(forecastUnitName(NULL, name) == unit) return name;
}
}
return unit;
}
QStringList forecastUnits(const nmDataAttribute& attribute)
{
// 下拉选项直接取软件单位配置。
iUnitGroup* group = forecastUnitGroup(attribute.getAttributeUnitType());
return group ? group->getAllUnitNames() : attribute.getListUnitSelections();
}
double convertForecastValue(const nmDataAttribute& attribute, double value,
const QString& fromUnit, const QString& toUnit)
{
if(fromUnit == toUnit) return value;
iUnitGroup* group = forecastUnitGroup(attribute.getAttributeUnitType());
if(!group) return attribute.convertValueByUnitType(value, fromUnit, toUnit);
QString source = forecastUnitName(group, fromUnit);
const QString destination = forecastUnitName(group, toUnit);
// 旧项目可能保存了周、月或旧流量前缀,先按原定义转入底层支持的单位。
if(group->indexOf(source) < 0 && (attribute.getListUnitSelections().contains(fromUnit) ||
nmDataAttribute::detectUnitType(fromUnit) == attribute.getAttributeUnitType())) {
const QString bridge = attribute.getAttributeUnitType() == UNIT_TYPE_TIME ? "sec" :
attribute.getAttributeUnitType() == UNIT_TYPE_PRESSURE ? "MPa" : "m^3/D";
value = attribute.convertValueByUnitType(value, fromUnit, bridge);
source = forecastUnitName(group, bridge);
}
double converted = 0.0;
int digits = 6;
return group->convert(source, value, destination, converted, digits) ? converted : qQNaN();
}
}
nmWxForecast::nmWxForecast(QWidget *parent, nmDataAnalyzeManager* manager)
: iDlgBase(parent)
, m_manager(manager ? manager : nmDataAnalyzeManager::getCurrentInstance())
, m_task(NULL)
, m_calculationRunning(false)
, m_initialPressureComponent(nullptr)
, m_initialPressureEdit(nullptr)
, m_initialPressureUnit(nullptr)
, m_initialPressure(nullptr)
, m_pressureUnitBinder(nullptr)
, m_abandonmentRateComponent(nullptr)
, m_maximumRateComponent(nullptr)
, m_forecastDurationComponent(nullptr)
@ -44,11 +106,11 @@ nmWxForecast::nmWxForecast(QWidget *parent, nmDataAnalyzeManager* manager)
new nmDialogPreviewGuard(this);
PressureRow tempDefaults;
m_currentDurationUnit = tempDefaults.duration.getUnit();
m_currentPressureUnit = tempDefaults.pressure.getUnit();
m_currentDurationUnit = forecastUnitName(forecastUnitGroup(UNIT_TYPE_TIME), tempDefaults.duration.getUnit());
m_currentPressureUnit = forecastUnitName(forecastUnitGroup(UNIT_TYPE_PRESSURE), tempDefaults.pressure.getUnit());
FlowRow tempFlowDefaults;
m_currentRateUnit = tempFlowDefaults.q0.getUnit();
m_currentRateUnit = forecastUnitName(forecastUnitGroup(UNIT_TYPE_FLOW_RATE), tempFlowDefaults.q0.getUnit());
// 先构建UI
setupUI();
@ -59,12 +121,6 @@ nmWxForecast::nmWxForecast(QWidget *parent, nmDataAnalyzeManager* manager)
// 根据加载的数据更新界面
updateParametersForOption();
// 编辑内容保留在窗口中,点击生成后再提交到数据中心。
const QList<QComboBox*> units = m_initialPressureComponent->findChildren<QComboBox*>();
foreach(QComboBox* unit, units) {
const int index = unit->findData(m_forecastData.getMultipleRatesData().pressureUnit);
if(index >= 0) unit->setCurrentIndex(index);
}
if(m_manager) {
connect(m_manager, SIGNAL(sigPebiResultSnapshotChanged()), this, SLOT(onModelChanged()));
connect(m_manager, SIGNAL(sigPebiResultSnapshotChanged()), this, SLOT(reloadInputData()));
@ -92,18 +148,20 @@ void nmWxForecast::loadDataFromManager()
if(m_manager) m_forecastData = m_manager->getForecastDataCopy();
m_forecastData.setResult(QSharedPointer<nmDataForecast::Result>());
const MultipleRates& data = m_forecastData.getMultipleRatesData();
if(m_rDurationUnitCombo->findText(data.durationUnit) >= 0) m_currentDurationUnit = data.durationUnit;
if(m_rRateUnitCombo->findText(data.rateUnit) >= 0) m_currentRateUnit = data.rateUnit;
const QString durationUnit = forecastUnitName(forecastUnitGroup(UNIT_TYPE_TIME), data.durationUnit);
const QString rateUnit = forecastUnitName(forecastUnitGroup(UNIT_TYPE_FLOW_RATE), data.rateUnit);
m_currentDurationUnit = m_rDurationUnitCombo->findText(durationUnit) >= 0 ? durationUnit :
forecastUnitName(forecastUnitGroup(UNIT_TYPE_TIME), "hr");
m_currentRateUnit = m_rRateUnitCombo->findText(rateUnit) >= 0 ? rateUnit :
forecastUnitName(forecastUnitGroup(UNIT_TYPE_FLOW_RATE), "B/D");
nmDataAttribute& pressure = m_forecastData.getMultipleRatesData().getInitialPressure();
if(m_manager && m_forecastData.getForecastOption() != nmDataForecast::MultipleRatesOption) {
const QSharedPointer<const nmPebiResultSnapshot> solved = m_manager->getPebiResultSnapshot();
if(!solved.isNull()) {
pressure.setValue(pressure.convertValueByUnitType(
solved->getReservoirParameters().m_dInitialPressure, "MPa", pressure.getUnit()));
pressure.setUnit("MPa");
pressure.setValue(solved->getReservoirParameters().m_dInitialPressure);
}
}
// 本页使用绝对压力,避免通用单位控件把表压当作绝压换算。
pressure.setListUnitSelections(QStringList() << "MPa" << "psia" << "kPa" << "bara");
m_forecastData.setForecastOption(nmDataForecast::MultipleRatesOption);
m_multipleRatesRadio->blockSignals(true);
m_multipleRatesRadio->setChecked(true);
@ -123,9 +181,7 @@ void nmWxForecast::saveDataToManager()
// 生成前统一收集输入,编辑和关闭窗口时不提交到数据中心。
updateDataFromUI();
const QList<QComboBox*> units = m_initialPressureComponent->findChildren<QComboBox*>();
if(!units.isEmpty())
m_forecastData.getMultipleRatesData().pressureUnit = units.first()->itemData(units.first()->currentIndex()).toString();
m_forecastData.getMultipleRatesData().pressureUnit = m_initialPressureUnit->currentText();
// 保存到数据中心
nmDataAnalyzeManager* pManager = m_manager.data();
@ -206,28 +262,24 @@ void nmWxForecast::setupUI()
// (row 0) Initial pressure
ConstantPressure& constantPressureData = m_forecastData.getConstantPressureData();
m_initialPressureComponent = new nmGUIComponentLineEdit(
&constantPressureData.getInitialPressure(),
true,
this
);
// 标签按内容占宽,数值和单位按储层参数页的比例铺满剩余空间。
QLabel* pressureLabel = m_initialPressureComponent->findChild<QLabel*>();
// 先解除通用控件的 200 像素标签限制,再由布局按实际文字宽度分配。
pressureLabel->setMinimumWidth(0);
pressureLabel->setMaximumWidth(QWIDGETSIZE_MAX);
// 保持原布局,用底层绑定器管理初始压力的数值和单位。
QWidget* pressureWidget = new QWidget(this);
QHBoxLayout* pressureLayout = new QHBoxLayout(pressureWidget);
pressureLayout->setContentsMargins(0, 0, 0, 0);
pressureLayout->setSpacing(5);
QLabel* pressureLabel = new QLabel(nmTranslationManager::mapToChinese("Initial pressure"), pressureWidget);
pressureLabel->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Fixed);
QLineEdit* pressureEdit = m_initialPressureComponent->findChild<QLineEdit*>();
QComboBox* pressureUnit = m_initialPressureComponent->findChild<QComboBox*>();
pressureEdit->setMinimumWidth(140);
pressureEdit->setMaximumWidth(QWIDGETSIZE_MAX);
pressureUnit->setMinimumWidth(86);
pressureUnit->setMaximumWidth(QWIDGETSIZE_MAX);
QHBoxLayout* pressureLayout = static_cast<QHBoxLayout*>(m_initialPressureComponent->layout());
delete pressureLayout->takeAt(1); // 移除标签后的弹性留白。
pressureLayout->setStretchFactor(pressureEdit, 8);
pressureLayout->setStretchFactor(pressureUnit, 5);
m_parametersLayout->addWidget(m_initialPressureComponent, row, 0, 1, 2);
m_initialPressureEdit = new QLineEdit(pressureWidget);
m_initialPressureUnit = new QComboBox(pressureWidget);
m_initialPressureEdit->setMinimumWidth(140);
m_initialPressureUnit->setMinimumWidth(86);
m_initialPressureUnit->addItem("MPa");
m_pressureUnitBinder = new iUnitWxBinder(this);
m_pressureUnitBinder->registerUnitGroup(m_initialPressureEdit, m_initialPressureUnit);
pressureLayout->addWidget(pressureLabel);
pressureLayout->addWidget(m_initialPressureEdit, 8);
pressureLayout->addWidget(m_initialPressureUnit, 5);
m_parametersLayout->addWidget(pressureWidget, row, 0, 1, 2);
row++;
// (row 1) Abandonment rate
@ -282,6 +334,45 @@ void nmWxForecast::setupUI()
m_extMultiR = buildExtMultiRatePage();
m_extMultiR->setMinimumHeight(80);
// 制度表沿用敏感性分析和编辑井的表头、行号、隔行底色及网格样式。
QTableWidget* scheduleTables[] = {m_pressureTableWidget, m_rateTableWidget};
for(int i = 0; i < 2; ++i) {
QTableWidget* table = scheduleTables[i];
table->verticalHeader()->setVisible(true);
table->verticalHeader()->setMovable(false);
table->verticalHeader()->setDefaultAlignment(Qt::AlignCenter);
table->verticalHeader()->setMinimumWidth(18);
table->horizontalHeader()->setHighlightSections(false);
table->setAlternatingRowColors(true);
table->setShowGrid(true);
table->setStyleSheet(
"QTableWidget {"
" background-color: palette(base);"
" alternate-background-color: rgb(232, 232, 232);"
" gridline-color: palette(midlight);"
" border: 1px solid palette(mid);"
"}"
"QHeaderView {"
" background-color: palette(button);"
" color: palette(button-text);"
"}"
"QHeaderView::section {"
" background-color: palette(button);"
" color: palette(button-text);"
" border: none;"
" border-right: 1px solid palette(mid);"
" border-bottom: 1px solid palette(mid);"
"}"
"QTableCornerButton::section {"
" background-color: palette(base);"
" border: none;"
" border-right: 1px solid palette(mid);"
" border-bottom: 1px solid palette(mid);"
"}");
// 第零行只显示单位,不参与制度行编号。
table->setVerticalHeaderItem(0, new QTableWidgetItem(""));
}
// --- 2. 创建一个空页面---
m_extEmpty = buildExtEmptyPage("");
@ -405,8 +496,8 @@ QWidget* nmWxForecast::buildExtMultiPressurePage()
m_pressureTableWidget->setRowHeight(0, 24); // 设置一个合适的高度
PressureRow tempDefaults; // 用于获取单位列表
QStringList durationUnits = tempDefaults.duration.getListUnitSelections();
QStringList pressureUnits = tempDefaults.pressure.getListUnitSelections();
QStringList durationUnits = forecastUnits(tempDefaults.duration);
QStringList pressureUnits = forecastUnits(tempDefaults.pressure);
// --- 第0列:Duration 单位下拉框 ---
m_pDurationUnitCombo = new QComboBox(m_pressureTableWidget);
@ -473,8 +564,8 @@ QWidget* nmWxForecast::buildExtMultiRatePage()
m_rateTableWidget->setRowHeight(0, 24);
FlowRow tempDefaults; // 使用 FlowRow 获取单位
QStringList durationUnits = tempDefaults.duration.getListUnitSelections();
QStringList rateUnits = tempDefaults.q0.getListUnitSelections(); // 获取速率单位
QStringList durationUnits = forecastUnits(tempDefaults.duration);
QStringList rateUnits = forecastUnits(tempDefaults.q0); // 获取速率单位
// --- 第0列:Duration 单位下拉框 ---
m_rDurationUnitCombo = new QComboBox(m_rateTableWidget);
@ -694,8 +785,7 @@ void nmWxForecast::updateParametersForOption()
if(option == nmDataForecast::ConstantPressureOption) {
ConstantPressure& cpData = m_forecastData.getConstantPressureData();
if(m_initialPressureComponent)
m_initialPressureComponent->setOriginValue(&cpData.getInitialPressure());
m_initialPressure = &cpData.getInitialPressure();
if(m_abandonmentRateComponent)
m_abandonmentRateComponent->setOriginValue(&cpData.getAbandonmentRate());
if(m_maximumRateComponent)
@ -709,8 +799,7 @@ void nmWxForecast::updateParametersForOption()
} else if(option == nmDataForecast::DecliningPressureOption) {
DecliningPressure& dpData = m_forecastData.getDecliningPressureData();
if(m_initialPressureComponent)
m_initialPressureComponent->setOriginValue(&dpData.getInitialPressure());
m_initialPressure = &dpData.getInitialPressure();
if(m_abandonmentRateComponent)
m_abandonmentRateComponent->setOriginValue(&dpData.getAbandonmentRate());
if(m_maximumRateComponent)
@ -725,8 +814,7 @@ void nmWxForecast::updateParametersForOption()
} else if(option == nmDataForecast::MultiplePressuresOption) {
MultiplePressures& mpData = m_forecastData.getMultiplePressuresData();
if(m_initialPressureComponent)
m_initialPressureComponent->setOriginValue(&mpData.getInitialPressure());
m_initialPressure = &mpData.getInitialPressure();
if(m_abandonmentRateComponent)
m_abandonmentRateComponent->setOriginValue(&mpData.getAbandonmentRate());
if(m_maximumRateComponent)
@ -746,8 +834,7 @@ void nmWxForecast::updateParametersForOption()
} else if(option == nmDataForecast::MultipleRatesOption) {
MultipleRates& mrData = m_forecastData.getMultipleRatesData();
if(m_initialPressureComponent)
m_initialPressureComponent->setOriginValue(&mrData.getInitialPressure());
m_initialPressure = &mrData.getInitialPressure();
if (m_abandonmentRateCheck) m_abandonmentRateCheck->setVisible(false);
if (m_abandonmentStack) m_abandonmentStack->setCurrentIndex(1);
@ -816,8 +903,14 @@ void nmWxForecast::updateUIFromData()
}
// 刷新所有组件的显示值
if(m_initialPressureComponent)
m_initialPressureComponent->updateValueFromOrigin();
// 基准值交给绑定器,显示单位恢复为上次保存的选项。
QString pressureUnit = forecastUnitName(forecastUnitGroup(UNIT_TYPE_PRESSURE),
option == nmDataForecast::MultipleRatesOption ? m_forecastData.getMultipleRatesData().pressureUnit : m_initialPressure->getUnit());
if(m_initialPressureUnit->findText(pressureUnit) < 0)
pressureUnit = forecastUnitName(forecastUnitGroup(UNIT_TYPE_PRESSURE), m_initialPressure->getUnit());
m_pressureUnitBinder->setCurrentUnit(m_initialPressureEdit, pressureUnit);
m_pressureUnitBinder->setBaseValue(m_initialPressureEdit, convertForecastValue(*m_initialPressure,
m_initialPressure->getValue().toDouble(), m_initialPressure->getUnit(), "MPa"));
if(m_abandonmentRateComponent)
m_abandonmentRateComponent->updateValueFromOrigin();
@ -860,6 +953,8 @@ void nmWxForecast::updateUIFromData()
int displayRowCount = (dataSize == 0) ? 1 : dataSize;
m_pressureTableWidget->setRowCount(1 + displayRowCount + 1);
// 末尾空行用于继续输入,填入内容后再显示编号。
m_pressureTableWidget->setVerticalHeaderItem(displayRowCount + 1, new QTableWidgetItem(""));
if(m_pDurationUnitCombo) m_pDurationUnitCombo->setCurrentIndex(m_pDurationUnitCombo->findText(m_currentDurationUnit));
@ -870,15 +965,16 @@ void nmWxForecast::updateUIFromData()
const PressureRow& rowData = (i < dataSize) ? tableData.at(i) : tempDefaults;
int tableRow = i + 1;
m_pressureTableWidget->setVerticalHeaderItem(tableRow, new QTableWidgetItem(QString::number(tableRow)));
// 转换值:从 "数据的基础单位" 转换为 "当前显示的单位"
double baseDurValue = rowData.duration.getValue().toDouble();
QString baseDurUnit = rowData.duration.getUnit();
double displayDurValue = tempDefaults.duration.convertValueByUnitType(baseDurValue, baseDurUnit, m_currentDurationUnit);
double displayDurValue = convertForecastValue(tempDefaults.duration, baseDurValue, baseDurUnit, m_currentDurationUnit);
double basePresValue = rowData.pressure.getValue().toDouble();
QString basePresUnit = rowData.pressure.getUnit();
double displayPresValue = tempDefaults.pressure.convertValueByUnitType(basePresValue, basePresUnit, m_currentPressureUnit);
double displayPresValue = convertForecastValue(tempDefaults.pressure, basePresValue, basePresUnit, m_currentPressureUnit);
QTableWidgetItem* durationItem = new QTableWidgetItem(QString::number(displayDurValue, 'g', 15));
durationItem->setTextAlignment(Qt::AlignCenter);
@ -916,6 +1012,8 @@ void nmWxForecast::updateUIFromData()
// 设置行数: 1(单位) + N(数据) + 1(空白)
m_rateTableWidget->setRowCount(1 + displayRowCount + 1);
// 单位行和末尾输入空行不编号,数据行从一开始。
m_rateTableWidget->setVerticalHeaderItem(displayRowCount + 1, new QTableWidgetItem(""));
// 设置下拉框为当前单位
if(m_rDurationUnitCombo) m_rDurationUnitCombo->setCurrentIndex(m_rDurationUnitCombo->findText(m_currentDurationUnit));
@ -926,23 +1024,28 @@ void nmWxForecast::updateUIFromData()
const FlowRow& rowData = (i < dataSize) ? tableData.at(i) : tempDefaults;
int tableRow = i + 1;
m_rateTableWidget->setVerticalHeaderItem(tableRow, new QTableWidgetItem(QString::number(tableRow)));
// 转换值:从 "数据的基础单位" 转换为 "当前显示的单位"
double baseDurValue = rowData.duration.getValue().toDouble();
bool durationOK = false;
double baseDurValue = rowData.duration.getValue().toString().toDouble(&durationOK);
QString baseDurUnit = rowData.duration.getUnit();
double displayDurValue = tempDefaults.duration.convertValueByUnitType(baseDurValue, baseDurUnit, m_currentDurationUnit);
double displayDurValue = convertForecastValue(tempDefaults.duration, baseDurValue, baseDurUnit, m_currentDurationUnit);
double baseRateValue = rowData.q0.getValue().toDouble();
bool rateOK = false;
double baseRateValue = rowData.q0.getValue().toString().toDouble(&rateOK);
QString baseRateUnit = rowData.q0.getUnit();
double displayRateValue = tempDefaults.q0.convertValueByUnitType(baseRateValue, baseRateUnit, m_currentRateUnit);
double displayRateValue = convertForecastValue(tempDefaults.q0, baseRateValue, baseRateUnit, m_currentRateUnit);
// 同单位直接恢复原输入文本,半空行不能补成默认数值。
QTableWidgetItem* durationItem = new QTableWidgetItem(baseDurUnit == m_currentDurationUnit && rowData.duration.getValue().type() == QVariant::String
QTableWidgetItem* durationItem = new QTableWidgetItem(rowData.duration.getValue().type() == QVariant::String &&
(baseDurUnit == m_currentDurationUnit || !durationOK)
? rowData.duration.getValue().toString() : QString::number(displayDurValue, 'g', 15));
durationItem->setTextAlignment(Qt::AlignCenter);
m_rateTableWidget->setItem(tableRow, 0, durationItem);
QTableWidgetItem* rateItem = new QTableWidgetItem(baseRateUnit == m_currentRateUnit && rowData.q0.getValue().type() == QVariant::String
QTableWidgetItem* rateItem = new QTableWidgetItem(rowData.q0.getValue().type() == QVariant::String &&
(baseRateUnit == m_currentRateUnit || !rateOK)
? rowData.q0.getValue().toString() : QString::number(displayRateValue, 'g', 15));
rateItem->setTextAlignment(Qt::AlignCenter);
m_rateTableWidget->setItem(tableRow, 1, rateItem);
@ -959,8 +1062,9 @@ void nmWxForecast::updateUIFromData()
void nmWxForecast::updateDataFromUI()
{
// 将界面数据写回到数据模型
if(m_initialPressureComponent)
m_initialPressureComponent->updateValueToOrigin();
// 存储绝对压力,界面单位单独保存,不改变求解器所需的 MPa 数值。
m_initialPressure->setValue(m_pressureUnitBinder->getBaseValue(m_initialPressureEdit));
m_initialPressure->setUnit("MPa");
if(m_abandonmentRateComponent)
m_abandonmentRateComponent->updateValueToOrigin();
@ -1010,13 +1114,13 @@ void nmWxForecast::updateDataFromUI()
if(!durationEmpty) {
double displayVal = durationItem->text().toDouble();
// 转换值:从 "当前显示的单位" 转换回 "数据的基础单位"
double storageVal = tempDefaults.duration.convertValueByUnitType(displayVal, m_currentDurationUnit, baseDurUnit);
double storageVal = convertForecastValue(tempDefaults.duration, displayVal, m_currentDurationUnit, baseDurUnit);
newRow.duration.setValue(storageVal);
}
if(!pressureEmpty) {
double displayVal = pressureItem->text().toDouble();
double storageVal = tempDefaults.pressure.convertValueByUnitType(displayVal, m_currentPressureUnit, basePresUnit);
double storageVal = convertForecastValue(tempDefaults.pressure, displayVal, m_currentPressureUnit, basePresUnit);
newRow.pressure.setValue(storageVal);
}
@ -1085,12 +1189,14 @@ void nmWxForecast::onGenerateClicked()
QMessageBox::warning(this, tr("Forecast"), error);
return;
}
nmDataAttribute& pressure = m_forecastData.getMultipleRatesData().getInitialPressure();
if(!m_initialPressureComponent->updateValueToOrigin()) {
// 校验当前文本,计算取绑定器保存的精确基准值,避免显示舍入影响压力。
bool pressureOK = false;
const double inputPressure = m_initialPressureEdit->text().toDouble(&pressureOK);
const double pi = m_pressureUnitBinder->getBaseValue(m_initialPressureEdit);
if(!pressureOK || !qIsFinite(inputPressure) || inputPressure <= 0.0 || !qIsFinite(pi) || pi <= 0.0) {
QMessageBox::warning(this, tr("Forecast"), tr("Enter a positive initial pressure."));
return;
}
const double pi = pressure.convertValueByUnitType(pressure.getValue().toDouble(), pressure.getUnit(), "MPa");
// 与主界面的结果井选择保持一致,预测制度只替换当前井,其他井共同参与求解。
const nmPebiResultWellSnapshot* well = solved->findWell(m_manager->getCurrentResultWellInstanceId());
if(!well) {
@ -1171,8 +1277,8 @@ bool nmWxForecast::readRateSchedule(QVector<double>& durations, QVector<double>&
bool durationOk = false, rateOk = false;
double duration = durationText.toDouble(&durationOk);
double rate = rateText.toDouble(&rateOk);
duration = units.duration.convertValueByUnitType(duration, m_currentDurationUnit, "hr");
rate = units.q0.convertValueByUnitType(rate, m_currentRateUnit, "m^3/D");
duration = convertForecastValue(units.duration, duration, m_currentDurationUnit, "hr");
rate = convertForecastValue(units.q0, rate, m_currentRateUnit, "m^3/D");
if(!durationOk || !rateOk || !qIsFinite(duration) || duration <= 0.0 ||
!qIsFinite(rate) || rate < 0.0) {
error = tr("Row %1: enter a positive duration and a non-negative production rate (0 for shut-in).").arg(row);
@ -1265,6 +1371,9 @@ void nmWxForecast::onPressureCellChanged(int row, int column)
int newRow = m_pressureTableWidget->rowCount();
m_pressureTableWidget->insertRow(newRow);
// 原空行成为制度行,新追加的输入行暂不编号。
m_pressureTableWidget->setVerticalHeaderItem(row, new QTableWidgetItem(QString::number(row)));
m_pressureTableWidget->setVerticalHeaderItem(newRow, new QTableWidgetItem(""));
// 给新行两列预置空 item,这样用户一输入就居中
m_pressureTableWidget->setItem(newRow, 0, makeCenteredEmptyItem());
@ -1294,7 +1403,7 @@ void nmWxForecast::onPressureUnitChanged(const QString& unit)
if(dataItem && !dataItem->text().isEmpty()) {
double oldDisplayValue = dataItem->text().toDouble();
double newDisplayValue = tempDefaults.pressure.convertValueByUnitType(oldDisplayValue, oldUnit, unit);
double newDisplayValue = convertForecastValue(tempDefaults.pressure, oldDisplayValue, oldUnit, unit);
dataItem->setText(QString::number(newDisplayValue));
}
}
@ -1322,6 +1431,9 @@ void nmWxForecast::onRateCellChanged(int row, int column)
int newRow = m_rateTableWidget->rowCount();
m_rateTableWidget->insertRow(newRow);
// 原空行成为制度行,新追加的输入行暂不编号。
m_rateTableWidget->setVerticalHeaderItem(row, new QTableWidgetItem(QString::number(row)));
m_rateTableWidget->setVerticalHeaderItem(newRow, new QTableWidgetItem(""));
m_rateTableWidget->setItem(newRow, 0, makeCenteredEmptyItem());
m_rateTableWidget->setItem(newRow, 1, makeCenteredEmptyItem());
@ -1351,7 +1463,7 @@ void nmWxForecast::onSharedDurationUnitChanged(const QString& unit)
if(dataItem && !dataItem->text().isEmpty()) {
double oldDisplayValue = dataItem->text().toDouble();
// 转换值
double newDisplayValue = tempDefaults.duration.convertValueByUnitType(oldDisplayValue, oldUnit, unit);
double newDisplayValue = convertForecastValue(tempDefaults.duration, oldDisplayValue, oldUnit, unit);
dataItem->setText(QString::number(newDisplayValue));
}
}
@ -1373,7 +1485,7 @@ void nmWxForecast::onSharedDurationUnitChanged(const QString& unit)
double oldDisplayValue = dataItem->text().toDouble(&ok);
if(!ok || !qIsFinite(oldDisplayValue)) continue;
// 转换值
double newDisplayValue = tempDefaults.duration.convertValueByUnitType(oldDisplayValue, oldUnit, unit);
double newDisplayValue = convertForecastValue(tempDefaults.duration, oldDisplayValue, oldUnit, unit);
dataItem->setText(QString::number(newDisplayValue, 'g', 15));
}
}
@ -1414,7 +1526,7 @@ void nmWxForecast::onRateUnitChanged(const QString& unit)
double oldDisplayValue = dataItem->text().toDouble(&ok);
if(!ok || !qIsFinite(oldDisplayValue)) continue;
// 使用 q0 (FlowRate) 进行单位转换
double newDisplayValue = tempDefaults.q0.convertValueByUnitType(oldDisplayValue, oldUnit, unit);
double newDisplayValue = convertForecastValue(tempDefaults.q0, oldDisplayValue, oldUnit, unit);
dataItem->setText(QString::number(newDisplayValue, 'g', 15));
}
}

@ -1087,7 +1087,9 @@ void nmWxNumericalDesign::onCustomNumericalSettingsClicked()
// 针对高级组中的"时间相关"按钮的槽函数
void nmWxNumericalDesign::onTimeIconClicked()
{
nmWxTimeDependentSkin::showForCurrentWell();
// 使用所属分析的当前井,并让子窗口随本分析面板一起销毁。
if(!m_pDataManager || !m_pDataManager->getCurWellData()) return;
nmWxTimeDependentSkin::showForCurrentWell(m_pDataManager->getCurWellData(), this);
}
void nmWxNumericalDesign::onOutputResultFieldsAutomaticToggled(bool checked)

@ -1,4 +1,9 @@
#include "nmWxPerforationClosing.h"
#include "nmWxPerforationClosing.h"
#include "iUnitGroup.h"
#include "iUnitHelper.h"
#include <QStyledItemDelegate>
#include <QPainter>
#include "iBase_global.h"
#include <QVBoxLayout>
#include "nmDialogPreviewGuard.h"
@ -15,6 +20,91 @@
#include "nmDataAnalyzeManager.h"
namespace
{
// 树形分组保留原生折叠行为,只为组内两列表格补画网格和当前行标记。
class PerforationClosingTree : public QTreeWidget
{
public:
explicit PerforationClosingTree(QWidget* parent) : QTreeWidget(parent) {}
protected:
void drawRow(QPainter* painter, const QStyleOptionViewItem& option,
const QModelIndex& index) const override
{
QTreeWidget::drawRow(painter, option, index);
painter->save();
painter->setPen(palette().color(QPalette::Mid));
// 分组行跨列;子行从缩进后的表格左边缘开始绘制,包含箭头栏。
const int left = index.parent().isValid()
? visualRect(index).left() - indentation() : option.rect.left();
painter->drawLine(left, option.rect.bottom(), option.rect.right(), option.rect.bottom());
if(index.parent().isValid()) {
if(index.row() == 0)
painter->drawLine(left, option.rect.top(), option.rect.right(), option.rect.top());
painter->drawLine(left, option.rect.top(), left, option.rect.bottom());
const int markerRight = visualRect(index).left() - 1;
painter->drawLine(markerRight, option.rect.top(), markerRight, option.rect.bottom());
const int divider = columnViewportPosition(1) - 1;
painter->drawLine(divider, option.rect.top(), divider, option.rect.bottom());
}
painter->restore();
}
void drawBranches(QPainter* painter, const QRect& rect,
const QModelIndex& index) const override
{
// 射孔分组使用系统展开箭头,数据行的缩进区域显示当前行三角标记。
if(!index.parent().isValid()) {
QTreeWidget::drawBranches(painter, rect, index);
return;
}
if(index.row() == currentIndex().row() && index.parent() == currentIndex().parent()
&& (index.flags() & Qt::ItemIsEditable)) {
painter->save();
const QPoint center(rect.right() - indentation() / 2, rect.center().y());
QPolygon arrow;
arrow << center + QPoint(-2, -4) << center + QPoint(3, 0) << center + QPoint(-2, 4);
painter->setPen(Qt::NoPen);
painter->setBrush(palette().color(QPalette::Text));
painter->drawPolygon(arrow);
painter->restore();
}
}
};
class PerforationClosingDelegate : public QStyledItemDelegate
{
public:
explicit PerforationClosingDelegate(QObject* parent) : QStyledItemDelegate(parent) {}
QWidget* createEditor(QWidget* parent, const QStyleOptionViewItem& option,
const QModelIndex& index) const override
{
// 射孔状态由时间顺序决定,仅开始时间允许编辑。
if(index.column() != 0) return nullptr;
QWidget* editor = QStyledItemDelegate::createEditor(parent, option, index);
if(QLineEdit* line = qobject_cast<QLineEdit*>(editor)) {
line->setAlignment(Qt::Alignment(index.data(Qt::TextAlignmentRole).toInt()));
}
return editor;
}
protected:
void initStyleOption(QStyleOptionViewItem* option, const QModelIndex& index) const override
{
QStyledItemDelegate::initStyleOption(option, index);
// 三位小数仅用于绘制,编辑与保存仍读取模型中的完整数值。
if(index.column() == 0 && index.parent().isValid() && (index.flags() & Qt::ItemIsEditable)) {
bool ok = false;
const double value = index.data(Qt::EditRole).toString().toDouble(&ok);
QStyleOptionViewItemV4* textOption = qstyleoption_cast<QStyleOptionViewItemV4*>(option);
if(ok && textOption) textOption->text = QString::number(value, 'f', 3);
}
}
};
}
nmWxPerforationClosing::nmWxPerforationClosing(nmDataWellBase* pWellData, QWidget *parent)
: iDlgBase(parent)
{
@ -32,19 +122,15 @@ nmWxPerforationClosing::nmWxPerforationClosing(nmDataWellBase* pWellData, QWidge
m_pCurrentPerforationItem = 0;
m_currentWell = pWellData;
if(m_currentWell != nullptr) {
int perforationCount = m_currentWell->getPerforationCount();
// 选择操作的射孔段索引
m_currentPerforationIndex = perforationCount - 1;
m_perforationData = m_currentWell->getPerforationCopy(m_currentPerforationIndex);
// 从副本中读取封堵数据
m_baseTimeValues = m_perforationData.getTimeValues();
m_baseTime = m_perforationData.getBaseTime();
m_hasBaseTime = m_baseTime.isValid();
}
m_currentPerforationIndex = -1;
// 按射孔保存独立副本,切换目标不会覆盖其他射孔的封堵记录。
if(m_currentWell) {
for(int i = 0; i < m_currentWell->getPerforationCount(); ++i) {
m_perforations.insert(i, m_currentWell->getPerforationCopy(i));
}
m_currentPerforationIndex = m_currentWell->getPerforationCount() - 1;
}
loadDataFromModel();
// 初始化UI
initUI();
@ -57,7 +143,11 @@ nmWxPerforationClosing::nmWxPerforationClosing(nmDataWellBase* pWellData, QWidge
connect(m_pOkBtn, SIGNAL(clicked()), this, SLOT(onOkClicked()));
connect(m_pCancelBtn, SIGNAL(clicked()), this, SLOT(onCancelClicked()));
connect(m_pTreeWidget, SIGNAL(itemChanged(QTreeWidgetItem*, int)), this, SLOT(onItemChanged(QTreeWidgetItem*, int)));
loadDataFromModel();
connect(m_pTreeWidget, SIGNAL(currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)),
this, SLOT(onCurrentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)));
refreshDisplay();
m_pAddBtn->setEnabled(m_pCurrentPerforationItem != nullptr);
m_pRemoveBtn->setEnabled(m_pCurrentPerforationItem != nullptr);
}
nmWxPerforationClosing::~nmWxPerforationClosing()
@ -69,9 +159,6 @@ void nmWxPerforationClosing::initUI()
// 创建主布局
QVBoxLayout* mainLayout = new QVBoxLayout(this);
QString appDir = QCoreApplication::applicationDirPath();
appDir = appDir.section('/', 0, -2); // 获取上一级目录
// 创建工具栏容器
QWidget* toolbarContainer = new QWidget(this);
toolbarContainer->setMinimumHeight(70);
@ -86,7 +173,8 @@ void nmWxPerforationClosing::initUI()
addContainerLayout->setContentsMargins(2, 2, 2, 2);
m_pAddBtn = new QPushButton(addButtonContainer);
m_pAddBtn->setIcon(QIcon(appDir + "/Res/Icon/nmTimeDepSkin2.png"));
// 复用框架图标加载及缓存,保持原有工具栏布局。
m_pAddBtn->setIcon(zxLoadIcon("nmTimeDepSkin2"));
m_pAddBtn->setIconSize(QSize(36, 36));
m_pAddBtn->setFixedSize(40, 40);
@ -106,7 +194,7 @@ void nmWxPerforationClosing::initUI()
removeContainerLayout->setContentsMargins(2, 2, 2, 2);
m_pRemoveBtn = new QPushButton(removeButtonContainer);
m_pRemoveBtn->setIcon(QIcon(appDir + "/Res/Icon/nmTimeDepSkin3.png"));
m_pRemoveBtn->setIcon(zxLoadIcon("nmTimeDepSkin3"));
m_pRemoveBtn->setIconSize(QSize(36, 36));
m_pRemoveBtn->setFixedSize(40, 40);
@ -125,23 +213,28 @@ void nmWxPerforationClosing::initUI()
toolbarLayout->addSpacing(15); // 复选框和时间单位之间的固定间距
m_pTimeUnitLabel = new QLabel(tr("Time unit:")); // 保存指针以便隐藏/显示
toolbarLayout->addWidget(m_pTimeUnitLabel);
toolbarLayout->addSpacing(5); // 标签和下拉框之间的小间距
// 单位控件单独保留占位,切换日期显示时前面的控件不移动。
QWidget* timeUnitContainer = new QWidget(toolbarContainer);
QHBoxLayout* timeUnitLayout = new QHBoxLayout(timeUnitContainer);
timeUnitLayout->setContentsMargins(0, 0, 0, 0);
timeUnitLayout->setSpacing(toolbarLayout->spacing());
m_pTimeUnitLabel = new QLabel(tr("Time unit:"));
timeUnitLayout->addWidget(m_pTimeUnitLabel);
timeUnitLayout->addSpacing(5); // 标签和下拉框之间的小间距
// 单位和主窗口统一从底层配置读取。
m_pTimeUnitComboBox = new QComboBox();
m_pTimeUnitComboBox->addItem("ms");
m_pTimeUnitComboBox->addItem("sec");
m_pTimeUnitComboBox->addItem("min");
m_pTimeUnitComboBox->addItem("hr");
m_pTimeUnitComboBox->addItem("day");
m_pTimeUnitComboBox->addItem("week");
m_pTimeUnitComboBox->addItem("month");
m_pTimeUnitComboBox->addItem("year");
m_pTimeUnitComboBox->setCurrentIndex(3); // "hr"
iUnitGroup* group = iUnitHelper::getUnitGroupByUnit("s");
const QStringList units = group ? group->getAllUnitNames() : (QStringList() << "hr");
m_pTimeUnitComboBox->addItems(units);
if(m_currentTimeUnit == "hr" && units.contains("h")) m_currentTimeUnit = "h";
int unitIndex = m_pTimeUnitComboBox->findText(m_currentTimeUnit);
m_pTimeUnitComboBox->setCurrentIndex(unitIndex >= 0 ? unitIndex : 0);
m_currentTimeUnit = m_pTimeUnitComboBox->currentText();
m_pTimeUnitComboBox->setFixedWidth(60);
toolbarLayout->addWidget(m_pTimeUnitComboBox);
timeUnitLayout->addWidget(m_pTimeUnitComboBox);
timeUnitContainer->setFixedSize(timeUnitContainer->sizeHint());
toolbarLayout->addWidget(timeUnitContainer);
// 避免控件贴边
toolbarLayout->addSpacing(10);
@ -150,33 +243,54 @@ void nmWxPerforationClosing::initUI()
mainLayout->addWidget(toolbarContainer);
// 创建单个树形表格控件
m_pTreeWidget = new QTreeWidget(this);
m_pTreeWidget->setColumnCount(3);
m_pTreeWidget = new PerforationClosingTree(this);
// 数值时间和日期共用第一列,第二列显示射孔状态。
m_pTreeWidget->setColumnCount(2);
m_pTreeWidget->setItemDelegate(new PerforationClosingDelegate(m_pTreeWidget));
// 设置表头
QStringList headers;
headers << "" << "" << "";
headers << "" << "";
m_pTreeWidget->setHeaderLabels(headers);
m_pTreeWidget->setHeaderHidden(false); // 显示表头
m_pTreeWidget->setHeaderHidden(true); // 列标题显示在各射孔分组内部
// 设置列宽
m_pTreeWidget->setColumnWidth(0, 200); // 树形结构列
m_pTreeWidget->setColumnWidth(1, 120); // 时间列
m_pTreeWidget->setColumnWidth(2, 120); // 状态列
m_pTreeWidget->setColumnWidth(0, 260); // 分组和开始时间,预留日期文字及树缩进宽度
m_pTreeWidget->setColumnWidth(1, 180); // 射孔状态
// 设置表头拉伸策略
m_pTreeWidget->header()->setStretchLastSection(true);
m_pTreeWidget->header()->setHighlightSections(false);
m_pTreeWidget->header()->setDefaultAlignment(Qt::AlignCenter);
// 设置树形控件的其他属性
m_pTreeWidget->setRootIsDecorated(true); // 显示根节点的展开/折叠图标
m_pTreeWidget->setAlternatingRowColors(true); // 交替行颜色
m_pTreeWidget->setSelectionBehavior(QAbstractItemView::SelectRows); // 整行选择
m_pTreeWidget->setSelectionMode(QAbstractItemView::SingleSelection);
m_pTreeWidget->setIndentation(20); // 设置缩进距离
// 选中行保持浅蓝底色;网格由树控件统一绘制,不依赖单元格代理。
m_pTreeWidget->setEditTriggers(QAbstractItemView::DoubleClicked);
m_pTreeWidget->setStyleSheet(
"QTreeWidget::branch {"
"background: transparent;"
"border: none;"
"width: 0px;"
"QTreeWidget {"
" background-color: palette(base);"
" alternate-background-color: rgb(232, 232, 232);"
" border: 1px solid palette(mid);"
"}"
"QHeaderView {"
" background-color: palette(button);"
" color: palette(button-text);"
"}"
"QHeaderView::section {"
" background-color: palette(button);"
" color: palette(button-text);"
" border: none;"
" border-right: 1px solid palette(mid);"
" border-bottom: 1px solid palette(mid);"
"}"
"QTreeWidget::item:selected {"
" background-color: rgb(225, 235, 252);"
" color: palette(text);"
"}"
);
@ -189,6 +303,7 @@ void nmWxPerforationClosing::initUI()
// 添加表头行
m_pPerforationRoot = new QTreeWidgetItem(m_pTreeWidget);
m_pPerforationRoot->setText(0, tr("Perforation"));
m_pPerforationRoot->setFirstColumnSpanned(true);
m_pPerforationRoot->setChildIndicatorPolicy(QTreeWidgetItem::DontShowIndicator);
// 为每个射孔段添加树形项目
@ -202,6 +317,8 @@ void nmWxPerforationClosing::initUI()
QTreeWidgetItem* perforationItem = new QTreeWidgetItem(m_pTreeWidget);
perforationItem->setText(0, perforationName);
perforationItem->setData(0, Qt::UserRole, i);
perforationItem->setFirstColumnSpanned(true);
perforationItem->setExpanded(false); // 默认收起
// 如果是当前选中的射孔段,保存引用
@ -246,7 +363,7 @@ void nmWxPerforationClosing::initUI()
}
}
m_pAllComboBox->setFixedWidth(120); // 增加宽度以适应射孔段名称
m_pAllComboBox->setFixedWidth(180); // 加宽下拉框,便于查看射孔段名称
//m_pAllComboBox = new QComboBox();
//m_pAllComboBox->addItem(tr("All"));
//m_pAllComboBox->addItem(tr("Option 1"));
@ -274,16 +391,6 @@ void nmWxPerforationClosing::initUI()
mainLayout->setSpacing(5);
}
QString nmWxPerforationClosing::formatNumber(double value)
{
// 如果数值很大或很小,使用科学计数法
if(qAbs(value) >= 1000000.0 || (qAbs(value) < 0.001 && value != 0.0)) {
return QString::number(value, 'e', 5); // 科学计数法,5位小数
} else {
return QString::number(value, 'f', 5); // 固定小数点格式,5位小数
}
}
void nmWxPerforationClosing::onAddClicked()
{
if(!m_pCurrentPerforationItem)
@ -295,29 +402,27 @@ void nmWxPerforationClosing::onAddClicked()
m_hasBaseTime = true;
}
// 直接基于 m_baseTimeValues 来决定要添加的值
// 子行的第一行是表头,实际时间记录从第二行开始。
QTreeWidgetItem* selected = m_pTreeWidget->currentItem();
int selectedIndex = selected && selected->parent() == m_pCurrentPerforationItem
? m_pCurrentPerforationItem->indexOfChild(selected) - 1 : -1;
QList<double> newValuesInHr;
if(m_baseTimeValues.isEmpty()) {
// 第一次Add:添加100.0小时
newValuesInHr.append(100.0);
} else if(m_baseTimeValues.size() == 1) {
// 第二次Add:添加200.0小时
newValuesInHr.append(200.0);
} else if(m_baseTimeValues.size() == 1 || selectedIndex == m_baseTimeValues.size() - 1) {
// 最后一行没有后续区间,沿用初始的100小时步长向后追加一条。
newValuesInHr.append(m_baseTimeValues.last() + 100.0);
} else {
// 第三次及以后:只在第一个区间中插入两个点
QList<double> sortedValues = m_baseTimeValues;
std::sort(sortedValues.begin(), sortedValues.end());
// 只在第一个区间插入两个均匀分布的点
if(sortedValues.size() >= 2) {
double start = sortedValues[0];
double end = sortedValues[1];
double interval = (end - start) / 3.0;
newValuesInHr.append(start + interval);
newValuesInHr.append(start + 2 * interval);
}
// 保留三等分插入两个点的规则,但使用所选行与下一行之间的区间。
// 只选中射孔名称或表头时,沿用原来的第一个区间。
if(selectedIndex < 0) selectedIndex = 0;
const double start = m_baseTimeValues[selectedIndex];
const double end = m_baseTimeValues[selectedIndex + 1];
const double interval = (end - start) / 3.0;
newValuesInHr.append(start + interval);
newValuesInHr.append(start + 2 * interval);
}
// 合并原有数值和新数值,然后排序
@ -327,7 +432,7 @@ void nmWxPerforationClosing::onAddClicked()
// 刷新显示
refreshDisplay();
// 展开Perforation #1项目以显示新添加的内容
// 展开当前选中的射孔分组以显示新添加的内容
m_pCurrentPerforationItem->setExpanded(true);
// 更新计数器
@ -354,8 +459,8 @@ void nmWxPerforationClosing::onRemoveClicked()
isValidDataRow = !selectedItem->text(0).isEmpty() &&
QDateTime::fromString(selectedItem->text(0), "yyyy-MM-dd hh:mm:ss").isValid();
} else {
// 数值模式下检查第1列是否有数值数据
isValidDataRow = !selectedItem->text(1).isEmpty();
// 数值时间也位于第一列。
isValidDataRow = !selectedItem->text(0).isEmpty();
}
if(isValidDataRow) {
@ -389,12 +494,18 @@ void nmWxPerforationClosing::onRemoveClicked()
qSort(timeValuesInHr.begin(), timeValuesInHr.end());
if(hasSelection && selectedIndex >= 0 && selectedIndex < timeValuesInHr.size()) {
timeValuesInHr.removeAt(selectedIndex);
} else if(!timeValuesInHr.isEmpty()) {
timeValuesInHr.removeLast();
// 未选中具体事件时,沿用删除末尾事件的入口。
if(!hasSelection || selectedIndex < 0 || selectedIndex >= timeValuesInHr.size()) {
selectedIndex = timeValuesInHr.size() - 1;
}
// 按Saphir的行操作规则删除当前行及下一行,先删下一行以免索引移位。
if(selectedIndex + 1 < timeValuesInHr.size()) {
timeValuesInHr.removeAt(selectedIndex + 1);
}
// 当前行位于末尾时,只删除自身。
timeValuesInHr.removeAt(selectedIndex);
m_baseTimeValues = timeValuesInHr;
if(m_baseTimeValues.isEmpty()) {
@ -407,77 +518,69 @@ void nmWxPerforationClosing::onRemoveClicked()
void nmWxPerforationClosing::refreshDisplay()
{
if(!m_pCurrentPerforationItem) return;
m_pTreeWidget->blockSignals(true);
// 清理除“Start time”外的子行
for(int i = m_pCurrentPerforationItem->childCount() - 1; i >= 0; --i) {
QTreeWidgetItem* it = m_pCurrentPerforationItem->child(i);
if(it == nullptr) {
continue;
// 显示单位和日期模式属于整个窗口,刷新时同时更新所有射孔分组。
storeCurrentPerforation();
const bool blocked = m_pTreeWidget->blockSignals(true);
QTreeWidgetItem* selected = m_pTreeWidget->currentItem();
QTreeWidgetItem* selectedGroup = selected && selected->parent() ? selected->parent() : selected;
const int selectedRow = selected && selected->parent() ? selectedGroup->indexOfChild(selected) : -1;
for(int groupIndex = 0; groupIndex < m_pTreeWidget->topLevelItemCount(); ++groupIndex) {
QTreeWidgetItem* group = m_pTreeWidget->topLevelItem(groupIndex);
if(group == m_pPerforationRoot) continue;
const nmDataPerforation& data = m_perforations[group->data(0, Qt::UserRole).toInt()];
const QList<double> values = data.getTimeValues();
const QDateTime baseTime = data.getBaseTime();
const bool wasExpanded = group->isExpanded();
const bool hadRows = group->childCount() > 0;
qDeleteAll(group->takeChildren());
if(!values.isEmpty()) {
QTreeWidgetItem* heading = new QTreeWidgetItem(group);
heading->setText(0, tr("Start time"));
heading->setText(1, tr("Perforation state"));
heading->setChildIndicatorPolicy(QTreeWidgetItem::DontShowIndicator);
}
if(it->text(0) != tr("Start time")) {
m_pCurrentPerforationItem->removeChild(it);
delete it;
}
}
// 如有数据,确保有“Start time”行
if(!m_baseTimeValues.isEmpty()) {
bool hasStart = false;
for(int i = 0; i < m_pCurrentPerforationItem->childCount(); ++i) {
QTreeWidgetItem* child = m_pCurrentPerforationItem->child(i);
if(child != nullptr && child->text(0) == tr("Start time")) {
hasStart = true; break;
for(int i = 0; i < values.size(); ++i) {
QTreeWidgetItem* row = new QTreeWidgetItem(group);
const double seconds = toSecondsQuantized(values[i], "hr");
if(m_showDatesMode && baseTime.isValid()) {
row->setText(0, baseTime.addSecs(static_cast<qint64>(seconds)).toString("yyyy-MM-dd hh:mm:ss"));
row->setTextAlignment(0, Qt::AlignLeft | Qt::AlignVCenter);
} else {
row->setText(0, QString::number(fromSeconds(seconds, m_currentTimeUnit), 'g', 15));
row->setTextAlignment(0, Qt::AlignRight | Qt::AlignVCenter);
}
}
if(!hasStart) {
QTreeWidgetItem* startTimeItem = new QTreeWidgetItem(m_pCurrentPerforationItem);
startTimeItem->setText(0, tr("Start time"));
startTimeItem->setText(2, tr("Perforation state"));
startTimeItem->setChildIndicatorPolicy(QTreeWidgetItem::DontShowIndicator);
}
}
for(int i = 0; i < m_baseTimeValues.size(); ++i) {
QTreeWidgetItem* row = new QTreeWidgetItem(m_pCurrentPerforationItem);
// 先把 “小时” -> “秒”
const double secQuant = toSecondsQuantized(m_baseTimeValues[i], "hr");
if(m_showDatesMode && m_hasBaseTime) {
// 用整数秒 + 基准时间显示;
const auto dt = m_baseTime.addSecs(static_cast<qint64>(secQuant));
row->setText(0, dt.toString("yyyy-MM-dd hh:mm:ss"));
row->setText(1, "");
row->setTextAlignment(0, Qt::AlignLeft | Qt::AlignVCenter);
row->setFlags(row->flags() | Qt::ItemIsEditable);
} else {
// 数值模式:整数秒 -> 当前显示单位
const double dispVal = fromSeconds(secQuant, m_currentTimeUnit);
row->setText(0, "");
row->setText(1, formatNumber(dispVal));
row->setTextAlignment(1, Qt::AlignRight | Qt::AlignVCenter);
row->setFlags(row->flags() | Qt::ItemIsEditable);
row->setText(1, i % 2 == 0 ? tr("Closed") : tr("Open"));
row->setTextAlignment(1, Qt::AlignLeft | Qt::AlignVCenter);
row->setChildIndicatorPolicy(QTreeWidgetItem::DontShowIndicator);
}
const QString state = (i % 2 == 0) ? tr("Closed") : tr("Open");
row->setText(2, state);
row->setTextAlignment(2, Qt::AlignCenter | Qt::AlignVCenter);
row->setChildIndicatorPolicy(QTreeWidgetItem::DontShowIndicator);
group->setExpanded(wasExpanded || (!hadRows && !values.isEmpty()));
}
m_pTreeWidget->blockSignals(false);
if(!m_baseTimeValues.isEmpty()) {
m_pCurrentPerforationItem->setExpanded(true);
// 重建子行后按分组及行号恢复选择,不能继续使用已释放的行指针。
if(selectedGroup) {
m_pTreeWidget->setCurrentItem(selectedRow >= 0 && selectedGroup->childCount() > 0
? selectedGroup->child(qMin(selectedRow, selectedGroup->childCount() - 1)) : selectedGroup);
} else if(m_pCurrentPerforationItem) {
m_pTreeWidget->setCurrentItem(m_pCurrentPerforationItem);
}
m_pTreeWidget->blockSignals(blocked);
}
void nmWxPerforationClosing::onCurrentItemChanged(QTreeWidgetItem* current, QTreeWidgetItem* previous)
{
Q_UNUSED(previous);
// 点击射孔名称、组内表头或数据行,都以其所属射孔作为增删和编辑目标。
QTreeWidgetItem* group = current && current->parent() ? current->parent() : current;
if(group == m_pPerforationRoot) group = nullptr;
if(group == m_pCurrentPerforationItem) return;
storeCurrentPerforation();
m_pCurrentPerforationItem = group;
m_currentPerforationIndex = group ? group->data(0, Qt::UserRole).toInt() : -1;
loadDataFromModel();
m_pAddBtn->setEnabled(group != nullptr);
m_pRemoveBtn->setEnabled(group != nullptr);
}
void nmWxPerforationClosing::onTimeUnitChanged(const QString &unit)
{
// 更新当前时间单位
@ -547,6 +650,7 @@ void nmWxPerforationClosing::showSpecificPerforationItem(int perforationIndex)
if(item) {
item->setHidden(false);
item->setExpanded(true); // 展开显示内容
m_pTreeWidget->setCurrentItem(item); // 筛选射孔时同步切换操作目标
}
}
}
@ -597,12 +701,12 @@ void nmWxPerforationClosing::onItemChanged(QTreeWidgetItem* item, int column)
// 确保是子项
if(item->parent() != m_pCurrentPerforationItem) return;
// 根据显示模式处理不同列的编辑
// 两种模式都编辑开始时间列,按显示模式选择解析方式。
if(m_showDatesMode && column == 0) {
// 日期模式下编辑第0列(时间字符串)
processDateEdit(item);
} else if(!m_showDatesMode && column == 1) {
// 数值模式下编辑第1列(数值)
} else if(!m_showDatesMode && column == 0) {
// 数值模式下编辑第0列(数值)
processValueEdit(item);
}
}
@ -686,7 +790,8 @@ void nmWxPerforationClosing::processValueEdit(QTreeWidgetItem* item)
guard = true;
bool ok = false;
const double newDisp = item->text(1).toDouble(&ok);
// 数值时间与日期统一从第一列读取。
const double newDisp = item->text(0).toDouble(&ok);
if(!ok) {
guard = false;
@ -749,57 +854,62 @@ void nmWxPerforationClosing::processValueEdit(QTreeWidgetItem* item)
guard = false;
}
void nmWxPerforationClosing::saveDataToModel()
void nmWxPerforationClosing::storeCurrentPerforation()
{
// 将界面数据保存到副本中
m_perforationData.setTimeValues(m_baseTimeValues);
// 生成状态数组
// 切换分组和刷新前只更新本地副本,取消窗口时可丢弃全部修改。
if(!m_perforations.contains(m_currentPerforationIndex)) return;
nmDataPerforation& data = m_perforations[m_currentPerforationIndex];
data.setTimeValues(m_baseTimeValues);
QStringList states;
for(int i = 0; i < m_baseTimeValues.size(); ++i)
states << (i % 2 == 0 ? tr("Closed") : tr("Open"));
data.setTimeStates(states);
data.setBaseTime(m_hasBaseTime ? m_baseTime : QDateTime());
}
for(int i = 0; i < m_baseTimeValues.size(); ++i) {
QString state = (i % 2 == 0) ? tr("Closed") : tr("Open");
states.append(state);
}
m_perforationData.setTimeStates(states);
// 保存基准时间(如果有的话)
if(m_hasBaseTime && m_baseTime.isValid()) {
m_perforationData.setBaseTime(m_baseTime);
void nmWxPerforationClosing::saveDataToModel()
{
// 统一提交各射孔的封堵字段,保留井对象中其他参数的最新值。
storeCurrentPerforation();
if(!m_currentWell) return;
for(QMap<int, nmDataPerforation>::const_iterator it = m_perforations.constBegin();
it != m_perforations.constEnd(); ++it) {
nmDataPerforation data = m_currentWell->getPerforationCopy(it.key());
data.setTimeValues(it.value().getTimeValues());
data.setTimeStates(it.value().getTimeStates());
data.setBaseTime(it.value().getBaseTime());
m_currentWell->updatePerforation(it.key(), data);
}
m_currentWell->updatePerforation(m_currentPerforationIndex, m_perforationData);
}
void nmWxPerforationClosing::loadDataFromModel()
{
// 直接获取时间值数据
m_baseTimeValues = m_perforationData.getTimeValues();
// 恢复基准时间
QDateTime savedBaseTime = m_perforationData.getBaseTime();
if(savedBaseTime.isValid()) {
m_baseTime = savedBaseTime;
m_hasBaseTime = true;
} else {
m_hasBaseTime = false;
}
// 更新计数器
// 切换编辑缓冲区时不重建树,避免删除正在点击或编辑的子行。
const nmDataPerforation data = m_perforations.value(m_currentPerforationIndex);
m_baseTimeValues = data.getTimeValues();
m_baseTime = data.getBaseTime();
m_hasBaseTime = m_baseTime.isValid();
m_timeEntryCounter = m_baseTimeValues.size();
// 刷新界面显示
refreshDisplay();
}
double nmWxPerforationClosing::convertTime(double value, const QString& fromUnit, const QString& toUnit) const
{
nmDataAttribute a;
a.setAttributeUnitType(UNIT_TYPE_TIME);
return a.convertValueByUnitType(value, fromUnit, toUnit);
// 直接使用底层单位组,兼容内部 hr/sec 与配置中的 h/s 写法。
iUnitGroup* group = iUnitHelper::getUnitGroupByUnit("s");
if(group) {
QString source = fromUnit == "sec" ? "s" : fromUnit;
QString destination = toUnit == "sec" ? "s" : toUnit;
if(group->indexOf("h") >= 0) {
if(source == "hr") source = "h";
if(destination == "hr") destination = "h";
}
double converted = value;
int digits = 6;
if(group->convert(source, value, destination, converted, digits)) return converted;
}
// 单位配置未加载时,沿用原有的内部时间换算。
nmDataAttribute attribute;
attribute.setAttributeUnitType(UNIT_TYPE_TIME);
return attribute.convertValueByUnitType(value, fromUnit, toUnit);
}
double nmWxPerforationClosing::roundToSecondDouble(double sec) const

@ -169,9 +169,10 @@ void nmWxPressFlowChartWidget::paintEvent(QPaintEvent *event)
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing, true);
// 计算绘图区域
m_plotArea = QRect(m_marginLeft, m_marginTop,
width() - m_marginLeft - m_marginRight,
// 补偿窗口让出的左侧空白,使标题左移时绘图区保持原位和原尺寸。
const int plotLeft = m_marginLeft + contentsMargins().left();
m_plotArea = QRect(plotLeft, m_marginTop,
width() - plotLeft - m_marginRight,
height() - m_marginTop - m_marginBottom);
drawBackground(painter);
@ -504,9 +505,10 @@ void nmWxPressFlowChartWidget::drawLabels(QPainter& painter)
if (!m_xUnit.isEmpty()) {
xLabelText += " (" + m_xUnit + ")";
}
QRect xLabelRect = painter.fontMetrics().boundingRect(xLabelText);
int xLabelX = (width() - xLabelRect.width()) / 2;
painter.drawText(xLabelX, height() - 10, xLabelText);
// 按文字实际边界贴近底部外沿,保持原有字号和绘图区。
const QRect xLabelRect = painter.fontMetrics().tightBoundingRect(xLabelText);
painter.drawText(QPointF(m_plotArea.center().x() - xLabelRect.center().x(),
height() - 2 - xLabelRect.bottom()), xLabelText);
painter.restore();
}
@ -520,10 +522,11 @@ void nmWxPressFlowChartWidget::drawLabels(QPainter& painter)
yLabelText += " (" + m_yUnit + ")";
}
painter.translate(15, height() / 2);
// 去掉字体矩形内的额外留白,将文字实际左边缘放在控件边缘内一像素处。
painter.translate(1, m_plotArea.center().y());
painter.rotate(-90);
QRect yLabelRect = painter.fontMetrics().boundingRect(yLabelText);
painter.drawText(-yLabelRect.width()/2, 0, yLabelText);
const QRect yLabelRect = painter.fontMetrics().tightBoundingRect(yLabelText);
painter.drawText(QPointF(-yLabelRect.center().x(), -yLabelRect.top()), yLabelText);
painter.restore();
}
}
@ -561,35 +564,24 @@ void nmWxPressFlowChartWidget::drawSelectedFlowSegments(QPainter& painter)
}
}
// 2. 绘制所有流动段的边界线(蓝色)
// 2. 只绘制各流动段的起点边界线,不额外绘制末尾终点(蓝色)
painter.setPen(QPen(QColor(0, 0, 255), 2)); // 蓝色,2像素宽
for(int i = 0; i < segments.size(); ++i) {
const FlowSegmentData& segment = segments[i];
double startTime = segment.segmentStart.getValue().toDouble();
double endTime = segment.segmentEnd.getValue().toDouble();
// 如果是外部拖动状态,并且是被拖动的边界,使用拖动中的时间
if(m_externalDragActive && i == m_externalDragBoundaryIndex) {
startTime = m_externalDragBoundaryTime;
}
// 检查下一个段是否受到拖动影响
if(m_externalDragActive && i + 1 < segments.size() && (i + 1) == m_externalDragBoundaryIndex) {
endTime = m_externalDragBoundaryTime;
}
// 检查是否处于合并状态
// 转换为屏幕坐标
int leftX = m_plotArea.left() + (startTime - m_minX) / (m_maxX - m_minX) * m_plotArea.width();
// 绘制起点边界线
painter.drawLine(leftX, m_plotArea.top(), leftX, m_plotArea.bottom());
// 只有最后一个段画终点
if (i == segments.size() - 1) {
int rightX = m_plotArea.left() + (endTime - m_minX) / (m_maxX - m_minX) * m_plotArea.width();
painter.drawLine(rightX, m_plotArea.top(), rightX, m_plotArea.bottom());
}
}

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save