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>
<context> <context>
<name>nmWxTimeDependentSkin</name> <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> <message>
<source>Time Dependent Skin</source> <source>Time Dependent Skin</source>
<translation>时间相关表皮系数</translation> <translation>时间相关表皮系数</translation>

@ -473,6 +473,8 @@ public:
// 获取PEBI求解器需要的pvt数据 // 获取PEBI求解器需要的pvt数据
nmDataPvtParaForPebi* getPebiPvtPara(); nmDataPvtParaForPebi* getPebiPvtPara();
// 读取所属分析窗口当前选用的 PVT 单值,参数名与底层 PVT 接口一致。
bool getAnalysisPvtValues(const QStringList& names, QMap<QString, double>& values) const;
// 获取气单相变化 PVT 求解所需的压力/拟压力转换表。 // 获取气单相变化 PVT 求解所需的压力/拟压力转换表。
bool getPebiPseudoPressureTable(std::vector<double>& pressure, bool getPebiPseudoPressureTable(std::vector<double>& pressure,
std::vector<double>& pseudoPressure); 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 #define NM_DIALOG_PREVIEW_GUARD_H
#include <QEvent> #include <QEvent>
@ -20,6 +20,26 @@ public:
window->installEventFilter(this); 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: protected:
// 只监听目标窗口的显式隐藏和重新显示;重建过程产生的事件不重复处理。 // 只监听目标窗口的显式隐藏和重新显示;重建过程产生的事件不重复处理。
bool eventFilter(QObject* object, QEvent* event) bool eventFilter(QObject* object, QEvent* event)
@ -52,21 +72,7 @@ protected:
QObject::timerEvent(event); QObject::timerEvent(event);
return; return;
} }
cancelPendingReset(); resetHiddenWindow();
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;
} }
private: private:

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

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

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

@ -4169,6 +4169,14 @@ nmDataPvtParaForPebi* nmDataAnalyzeManager::getPebiPvtPara()
return m_pebiPvtPara; 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, bool nmDataAnalyzeManager::getPebiPseudoPressureTable(std::vector<double>& pressure,
std::vector<double>& pseudoPressure) std::vector<double>& pseudoPressure)
{ {

@ -107,8 +107,100 @@
#include <QPointer> #include <QPointer>
#include <QtCore/qnumeric.h> #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 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求解任务运行。 // 保留原来求解器单例的防重入语义:全局同一时间只允许一个DLL求解任务运行。
// QPointer会在QObject销毁后自动变空,避免保留已经释放的线程指针。 // QPointer会在QObject销毁后自动变空,避免保留已经释放的线程指针。
QPointer<nmCalculationDllPebiSolverTask> s_pRunningSolverTask; QPointer<nmCalculationDllPebiSolverTask> s_pRunningSolverTask;
@ -135,9 +227,8 @@ void clearResultCurveDataTables(iGuiPlot* plot)
void resetResultDialog(QDialog* window) void resetResultDialog(QDialog* window)
{ {
if(!window) return; if(!window) return;
// 先恢复最小化状态再隐藏,避免隐藏后调整尺寸使系统重新显示空白窗口。 // 最小化结果先在隐藏状态下恢复尺寸,再清理旧曲线。
if(window->isMinimized()) static_cast<nmResultDialog*>(window)->hideAndRestoreGeometry();
window->setWindowState(window->windowState() & ~Qt::WindowMinimized);
window->hide(); window->hide();
foreach(iGuiPlot* plot, window->findChildren<iGuiPlot*>()) clearResultCurveDataTables(plot); foreach(iGuiPlot* plot, window->findChildren<iGuiPlot*>()) clearResultCurveDataTables(plot);
foreach(nmWxSensitivityShow* dialog, window->findChildren<nmWxSensitivityShow*>()) foreach(nmWxSensitivityShow* dialog, window->findChildren<nmWxSensitivityShow*>())
@ -1978,12 +2069,11 @@ void nmSubWndMain::onForecastDataChanged()
QDialog* nmSubWndMain::createResultDialog(const QString& name, const QString& title) 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); Qt::WindowTitleHint | Qt::WindowSystemMenuHint | Qt::WindowMinMaxButtonsHint | Qt::WindowCloseButtonHint);
dialog->setObjectName(name); dialog->setObjectName(name);
dialog->setWindowTitle(title); dialog->setWindowTitle(title);
dialog->setAttribute(Qt::WA_DeleteOnClose, false); dialog->setAttribute(Qt::WA_DeleteOnClose, false);
new nmDialogPreviewGuard(dialog);
dialog->resize(900, 650); dialog->resize(900, 650);
QVBoxLayout* layout = new QVBoxLayout(dialog); QVBoxLayout* layout = new QVBoxLayout(dialog);
layout->setContentsMargins(0, 0, 0, 0); layout->setContentsMargins(0, 0, 0, 0);
@ -2154,10 +2244,10 @@ void nmSubWndMain::onReadonlyCurveDoubleClicked(
bool nmSubWndMain::eventFilter(QObject* watched, QEvent* event) bool nmSubWndMain::eventFilter(QObject* watched, QEvent* event)
{ {
// 最小化时先恢复窗口状态,再交给默认关闭流程隐藏,避免复用时丢失正常尺寸。 // 最小化结果在隐藏后恢复尺寸,关闭过程中不显示大窗。
QDialog* dialog = qobject_cast<QDialog*>(watched); QDialog* dialog = qobject_cast<QDialog*>(watched);
if(dialog && event->type() == QEvent::Close && dialog->isMinimized()) if(dialog && event->type() == QEvent::Close)
dialog->setWindowState(dialog->windowState() & ~Qt::WindowMinimized); static_cast<nmResultDialog*>(dialog)->hideAndRestoreGeometry();
// 关闭结果对话框时同步关闭其曲线数据表、Show 和统计设置窗口。 // 关闭结果对话框时同步关闭其曲线数据表、Show 和统计设置窗口。
if((event->type() == QEvent::Close || event->type() == QEvent::Hide) && if((event->type() == QEvent::Close || event->type() == QEvent::Hide) &&
qobject_cast<QDialog*>(watched)) { qobject_cast<QDialog*>(watched)) {

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

@ -13,13 +13,75 @@
#include "nmGUIComponentLineEdit.h" #include "nmGUIComponentLineEdit.h"
#include "nmPebiResultSnapshot.h" #include "nmPebiResultSnapshot.h"
#include "ZxBaseUtil.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) nmWxForecast::nmWxForecast(QWidget *parent, nmDataAnalyzeManager* manager)
: iDlgBase(parent) : iDlgBase(parent)
, m_manager(manager ? manager : nmDataAnalyzeManager::getCurrentInstance()) , m_manager(manager ? manager : nmDataAnalyzeManager::getCurrentInstance())
, m_task(NULL) , m_task(NULL)
, m_calculationRunning(false) , m_calculationRunning(false)
, m_initialPressureComponent(nullptr) , m_initialPressureEdit(nullptr)
, m_initialPressureUnit(nullptr)
, m_initialPressure(nullptr)
, m_pressureUnitBinder(nullptr)
, m_abandonmentRateComponent(nullptr) , m_abandonmentRateComponent(nullptr)
, m_maximumRateComponent(nullptr) , m_maximumRateComponent(nullptr)
, m_forecastDurationComponent(nullptr) , m_forecastDurationComponent(nullptr)
@ -44,11 +106,11 @@ nmWxForecast::nmWxForecast(QWidget *parent, nmDataAnalyzeManager* manager)
new nmDialogPreviewGuard(this); new nmDialogPreviewGuard(this);
PressureRow tempDefaults; PressureRow tempDefaults;
m_currentDurationUnit = tempDefaults.duration.getUnit(); m_currentDurationUnit = forecastUnitName(forecastUnitGroup(UNIT_TYPE_TIME), tempDefaults.duration.getUnit());
m_currentPressureUnit = tempDefaults.pressure.getUnit(); m_currentPressureUnit = forecastUnitName(forecastUnitGroup(UNIT_TYPE_PRESSURE), tempDefaults.pressure.getUnit());
FlowRow tempFlowDefaults; FlowRow tempFlowDefaults;
m_currentRateUnit = tempFlowDefaults.q0.getUnit(); m_currentRateUnit = forecastUnitName(forecastUnitGroup(UNIT_TYPE_FLOW_RATE), tempFlowDefaults.q0.getUnit());
// 先构建UI // 先构建UI
setupUI(); setupUI();
@ -59,12 +121,6 @@ nmWxForecast::nmWxForecast(QWidget *parent, nmDataAnalyzeManager* manager)
// 根据加载的数据更新界面 // 根据加载的数据更新界面
updateParametersForOption(); 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) { if(m_manager) {
connect(m_manager, SIGNAL(sigPebiResultSnapshotChanged()), this, SLOT(onModelChanged())); connect(m_manager, SIGNAL(sigPebiResultSnapshotChanged()), this, SLOT(onModelChanged()));
connect(m_manager, SIGNAL(sigPebiResultSnapshotChanged()), this, SLOT(reloadInputData())); connect(m_manager, SIGNAL(sigPebiResultSnapshotChanged()), this, SLOT(reloadInputData()));
@ -92,18 +148,20 @@ void nmWxForecast::loadDataFromManager()
if(m_manager) m_forecastData = m_manager->getForecastDataCopy(); if(m_manager) m_forecastData = m_manager->getForecastDataCopy();
m_forecastData.setResult(QSharedPointer<nmDataForecast::Result>()); m_forecastData.setResult(QSharedPointer<nmDataForecast::Result>());
const MultipleRates& data = m_forecastData.getMultipleRatesData(); const MultipleRates& data = m_forecastData.getMultipleRatesData();
if(m_rDurationUnitCombo->findText(data.durationUnit) >= 0) m_currentDurationUnit = data.durationUnit; const QString durationUnit = forecastUnitName(forecastUnitGroup(UNIT_TYPE_TIME), data.durationUnit);
if(m_rRateUnitCombo->findText(data.rateUnit) >= 0) m_currentRateUnit = data.rateUnit; 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(); nmDataAttribute& pressure = m_forecastData.getMultipleRatesData().getInitialPressure();
if(m_manager && m_forecastData.getForecastOption() != nmDataForecast::MultipleRatesOption) { if(m_manager && m_forecastData.getForecastOption() != nmDataForecast::MultipleRatesOption) {
const QSharedPointer<const nmPebiResultSnapshot> solved = m_manager->getPebiResultSnapshot(); const QSharedPointer<const nmPebiResultSnapshot> solved = m_manager->getPebiResultSnapshot();
if(!solved.isNull()) { if(!solved.isNull()) {
pressure.setValue(pressure.convertValueByUnitType( pressure.setUnit("MPa");
solved->getReservoirParameters().m_dInitialPressure, "MPa", pressure.getUnit())); pressure.setValue(solved->getReservoirParameters().m_dInitialPressure);
} }
} }
// 本页使用绝对压力,避免通用单位控件把表压当作绝压换算。
pressure.setListUnitSelections(QStringList() << "MPa" << "psia" << "kPa" << "bara");
m_forecastData.setForecastOption(nmDataForecast::MultipleRatesOption); m_forecastData.setForecastOption(nmDataForecast::MultipleRatesOption);
m_multipleRatesRadio->blockSignals(true); m_multipleRatesRadio->blockSignals(true);
m_multipleRatesRadio->setChecked(true); m_multipleRatesRadio->setChecked(true);
@ -123,9 +181,7 @@ void nmWxForecast::saveDataToManager()
// 生成前统一收集输入,编辑和关闭窗口时不提交到数据中心。 // 生成前统一收集输入,编辑和关闭窗口时不提交到数据中心。
updateDataFromUI(); updateDataFromUI();
const QList<QComboBox*> units = m_initialPressureComponent->findChildren<QComboBox*>(); m_forecastData.getMultipleRatesData().pressureUnit = m_initialPressureUnit->currentText();
if(!units.isEmpty())
m_forecastData.getMultipleRatesData().pressureUnit = units.first()->itemData(units.first()->currentIndex()).toString();
// 保存到数据中心 // 保存到数据中心
nmDataAnalyzeManager* pManager = m_manager.data(); nmDataAnalyzeManager* pManager = m_manager.data();
@ -206,28 +262,24 @@ void nmWxForecast::setupUI()
// (row 0) Initial pressure // (row 0) Initial pressure
ConstantPressure& constantPressureData = m_forecastData.getConstantPressureData(); ConstantPressure& constantPressureData = m_forecastData.getConstantPressureData();
m_initialPressureComponent = new nmGUIComponentLineEdit( // 保持原布局,用底层绑定器管理初始压力的数值和单位。
&constantPressureData.getInitialPressure(), QWidget* pressureWidget = new QWidget(this);
true, QHBoxLayout* pressureLayout = new QHBoxLayout(pressureWidget);
this pressureLayout->setContentsMargins(0, 0, 0, 0);
); pressureLayout->setSpacing(5);
// 标签按内容占宽,数值和单位按储层参数页的比例铺满剩余空间。 QLabel* pressureLabel = new QLabel(nmTranslationManager::mapToChinese("Initial pressure"), pressureWidget);
QLabel* pressureLabel = m_initialPressureComponent->findChild<QLabel*>();
// 先解除通用控件的 200 像素标签限制,再由布局按实际文字宽度分配。
pressureLabel->setMinimumWidth(0);
pressureLabel->setMaximumWidth(QWIDGETSIZE_MAX);
pressureLabel->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Fixed); pressureLabel->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Fixed);
QLineEdit* pressureEdit = m_initialPressureComponent->findChild<QLineEdit*>(); m_initialPressureEdit = new QLineEdit(pressureWidget);
QComboBox* pressureUnit = m_initialPressureComponent->findChild<QComboBox*>(); m_initialPressureUnit = new QComboBox(pressureWidget);
pressureEdit->setMinimumWidth(140); m_initialPressureEdit->setMinimumWidth(140);
pressureEdit->setMaximumWidth(QWIDGETSIZE_MAX); m_initialPressureUnit->setMinimumWidth(86);
pressureUnit->setMinimumWidth(86); m_initialPressureUnit->addItem("MPa");
pressureUnit->setMaximumWidth(QWIDGETSIZE_MAX); m_pressureUnitBinder = new iUnitWxBinder(this);
QHBoxLayout* pressureLayout = static_cast<QHBoxLayout*>(m_initialPressureComponent->layout()); m_pressureUnitBinder->registerUnitGroup(m_initialPressureEdit, m_initialPressureUnit);
delete pressureLayout->takeAt(1); // 移除标签后的弹性留白。 pressureLayout->addWidget(pressureLabel);
pressureLayout->setStretchFactor(pressureEdit, 8); pressureLayout->addWidget(m_initialPressureEdit, 8);
pressureLayout->setStretchFactor(pressureUnit, 5); pressureLayout->addWidget(m_initialPressureUnit, 5);
m_parametersLayout->addWidget(m_initialPressureComponent, row, 0, 1, 2); m_parametersLayout->addWidget(pressureWidget, row, 0, 1, 2);
row++; row++;
// (row 1) Abandonment rate // (row 1) Abandonment rate
@ -282,6 +334,45 @@ void nmWxForecast::setupUI()
m_extMultiR = buildExtMultiRatePage(); m_extMultiR = buildExtMultiRatePage();
m_extMultiR->setMinimumHeight(80); 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. 创建一个空页面--- // --- 2. 创建一个空页面---
m_extEmpty = buildExtEmptyPage(""); m_extEmpty = buildExtEmptyPage("");
@ -405,8 +496,8 @@ QWidget* nmWxForecast::buildExtMultiPressurePage()
m_pressureTableWidget->setRowHeight(0, 24); // 设置一个合适的高度 m_pressureTableWidget->setRowHeight(0, 24); // 设置一个合适的高度
PressureRow tempDefaults; // 用于获取单位列表 PressureRow tempDefaults; // 用于获取单位列表
QStringList durationUnits = tempDefaults.duration.getListUnitSelections(); QStringList durationUnits = forecastUnits(tempDefaults.duration);
QStringList pressureUnits = tempDefaults.pressure.getListUnitSelections(); QStringList pressureUnits = forecastUnits(tempDefaults.pressure);
// --- 第0列:Duration 单位下拉框 --- // --- 第0列:Duration 单位下拉框 ---
m_pDurationUnitCombo = new QComboBox(m_pressureTableWidget); m_pDurationUnitCombo = new QComboBox(m_pressureTableWidget);
@ -473,8 +564,8 @@ QWidget* nmWxForecast::buildExtMultiRatePage()
m_rateTableWidget->setRowHeight(0, 24); m_rateTableWidget->setRowHeight(0, 24);
FlowRow tempDefaults; // 使用 FlowRow 获取单位 FlowRow tempDefaults; // 使用 FlowRow 获取单位
QStringList durationUnits = tempDefaults.duration.getListUnitSelections(); QStringList durationUnits = forecastUnits(tempDefaults.duration);
QStringList rateUnits = tempDefaults.q0.getListUnitSelections(); // 获取速率单位 QStringList rateUnits = forecastUnits(tempDefaults.q0); // 获取速率单位
// --- 第0列:Duration 单位下拉框 --- // --- 第0列:Duration 单位下拉框 ---
m_rDurationUnitCombo = new QComboBox(m_rateTableWidget); m_rDurationUnitCombo = new QComboBox(m_rateTableWidget);
@ -694,8 +785,7 @@ void nmWxForecast::updateParametersForOption()
if(option == nmDataForecast::ConstantPressureOption) { if(option == nmDataForecast::ConstantPressureOption) {
ConstantPressure& cpData = m_forecastData.getConstantPressureData(); ConstantPressure& cpData = m_forecastData.getConstantPressureData();
if(m_initialPressureComponent) m_initialPressure = &cpData.getInitialPressure();
m_initialPressureComponent->setOriginValue(&cpData.getInitialPressure());
if(m_abandonmentRateComponent) if(m_abandonmentRateComponent)
m_abandonmentRateComponent->setOriginValue(&cpData.getAbandonmentRate()); m_abandonmentRateComponent->setOriginValue(&cpData.getAbandonmentRate());
if(m_maximumRateComponent) if(m_maximumRateComponent)
@ -709,8 +799,7 @@ void nmWxForecast::updateParametersForOption()
} else if(option == nmDataForecast::DecliningPressureOption) { } else if(option == nmDataForecast::DecliningPressureOption) {
DecliningPressure& dpData = m_forecastData.getDecliningPressureData(); DecliningPressure& dpData = m_forecastData.getDecliningPressureData();
if(m_initialPressureComponent) m_initialPressure = &dpData.getInitialPressure();
m_initialPressureComponent->setOriginValue(&dpData.getInitialPressure());
if(m_abandonmentRateComponent) if(m_abandonmentRateComponent)
m_abandonmentRateComponent->setOriginValue(&dpData.getAbandonmentRate()); m_abandonmentRateComponent->setOriginValue(&dpData.getAbandonmentRate());
if(m_maximumRateComponent) if(m_maximumRateComponent)
@ -725,8 +814,7 @@ void nmWxForecast::updateParametersForOption()
} else if(option == nmDataForecast::MultiplePressuresOption) { } else if(option == nmDataForecast::MultiplePressuresOption) {
MultiplePressures& mpData = m_forecastData.getMultiplePressuresData(); MultiplePressures& mpData = m_forecastData.getMultiplePressuresData();
if(m_initialPressureComponent) m_initialPressure = &mpData.getInitialPressure();
m_initialPressureComponent->setOriginValue(&mpData.getInitialPressure());
if(m_abandonmentRateComponent) if(m_abandonmentRateComponent)
m_abandonmentRateComponent->setOriginValue(&mpData.getAbandonmentRate()); m_abandonmentRateComponent->setOriginValue(&mpData.getAbandonmentRate());
if(m_maximumRateComponent) if(m_maximumRateComponent)
@ -746,8 +834,7 @@ void nmWxForecast::updateParametersForOption()
} else if(option == nmDataForecast::MultipleRatesOption) { } else if(option == nmDataForecast::MultipleRatesOption) {
MultipleRates& mrData = m_forecastData.getMultipleRatesData(); MultipleRates& mrData = m_forecastData.getMultipleRatesData();
if(m_initialPressureComponent) m_initialPressure = &mrData.getInitialPressure();
m_initialPressureComponent->setOriginValue(&mrData.getInitialPressure());
if (m_abandonmentRateCheck) m_abandonmentRateCheck->setVisible(false); if (m_abandonmentRateCheck) m_abandonmentRateCheck->setVisible(false);
if (m_abandonmentStack) m_abandonmentStack->setCurrentIndex(1); 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) if(m_abandonmentRateComponent)
m_abandonmentRateComponent->updateValueFromOrigin(); m_abandonmentRateComponent->updateValueFromOrigin();
@ -860,6 +953,8 @@ void nmWxForecast::updateUIFromData()
int displayRowCount = (dataSize == 0) ? 1 : dataSize; int displayRowCount = (dataSize == 0) ? 1 : dataSize;
m_pressureTableWidget->setRowCount(1 + displayRowCount + 1); m_pressureTableWidget->setRowCount(1 + displayRowCount + 1);
// 末尾空行用于继续输入,填入内容后再显示编号。
m_pressureTableWidget->setVerticalHeaderItem(displayRowCount + 1, new QTableWidgetItem(""));
if(m_pDurationUnitCombo) m_pDurationUnitCombo->setCurrentIndex(m_pDurationUnitCombo->findText(m_currentDurationUnit)); 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; const PressureRow& rowData = (i < dataSize) ? tableData.at(i) : tempDefaults;
int tableRow = i + 1; int tableRow = i + 1;
m_pressureTableWidget->setVerticalHeaderItem(tableRow, new QTableWidgetItem(QString::number(tableRow)));
// 转换值:从 "数据的基础单位" 转换为 "当前显示的单位" // 转换值:从 "数据的基础单位" 转换为 "当前显示的单位"
double baseDurValue = rowData.duration.getValue().toDouble(); double baseDurValue = rowData.duration.getValue().toDouble();
QString baseDurUnit = rowData.duration.getUnit(); 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(); double basePresValue = rowData.pressure.getValue().toDouble();
QString basePresUnit = rowData.pressure.getUnit(); 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)); QTableWidgetItem* durationItem = new QTableWidgetItem(QString::number(displayDurValue, 'g', 15));
durationItem->setTextAlignment(Qt::AlignCenter); durationItem->setTextAlignment(Qt::AlignCenter);
@ -916,6 +1012,8 @@ void nmWxForecast::updateUIFromData()
// 设置行数: 1(单位) + N(数据) + 1(空白) // 设置行数: 1(单位) + N(数据) + 1(空白)
m_rateTableWidget->setRowCount(1 + displayRowCount + 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)); 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; const FlowRow& rowData = (i < dataSize) ? tableData.at(i) : tempDefaults;
int tableRow = i + 1; 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(); 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(); 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)); ? rowData.duration.getValue().toString() : QString::number(displayDurValue, 'g', 15));
durationItem->setTextAlignment(Qt::AlignCenter); durationItem->setTextAlignment(Qt::AlignCenter);
m_rateTableWidget->setItem(tableRow, 0, durationItem); 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)); ? rowData.q0.getValue().toString() : QString::number(displayRateValue, 'g', 15));
rateItem->setTextAlignment(Qt::AlignCenter); rateItem->setTextAlignment(Qt::AlignCenter);
m_rateTableWidget->setItem(tableRow, 1, rateItem); m_rateTableWidget->setItem(tableRow, 1, rateItem);
@ -959,8 +1062,9 @@ void nmWxForecast::updateUIFromData()
void nmWxForecast::updateDataFromUI() void nmWxForecast::updateDataFromUI()
{ {
// 将界面数据写回到数据模型 // 将界面数据写回到数据模型
if(m_initialPressureComponent) // 存储绝对压力,界面单位单独保存,不改变求解器所需的 MPa 数值。
m_initialPressureComponent->updateValueToOrigin(); m_initialPressure->setValue(m_pressureUnitBinder->getBaseValue(m_initialPressureEdit));
m_initialPressure->setUnit("MPa");
if(m_abandonmentRateComponent) if(m_abandonmentRateComponent)
m_abandonmentRateComponent->updateValueToOrigin(); m_abandonmentRateComponent->updateValueToOrigin();
@ -1010,13 +1114,13 @@ void nmWxForecast::updateDataFromUI()
if(!durationEmpty) { if(!durationEmpty) {
double displayVal = durationItem->text().toDouble(); 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); newRow.duration.setValue(storageVal);
} }
if(!pressureEmpty) { if(!pressureEmpty) {
double displayVal = pressureItem->text().toDouble(); 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); newRow.pressure.setValue(storageVal);
} }
@ -1085,12 +1189,14 @@ void nmWxForecast::onGenerateClicked()
QMessageBox::warning(this, tr("Forecast"), error); QMessageBox::warning(this, tr("Forecast"), error);
return; 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.")); QMessageBox::warning(this, tr("Forecast"), tr("Enter a positive initial pressure."));
return; return;
} }
const double pi = pressure.convertValueByUnitType(pressure.getValue().toDouble(), pressure.getUnit(), "MPa");
// 与主界面的结果井选择保持一致,预测制度只替换当前井,其他井共同参与求解。 // 与主界面的结果井选择保持一致,预测制度只替换当前井,其他井共同参与求解。
const nmPebiResultWellSnapshot* well = solved->findWell(m_manager->getCurrentResultWellInstanceId()); const nmPebiResultWellSnapshot* well = solved->findWell(m_manager->getCurrentResultWellInstanceId());
if(!well) { if(!well) {
@ -1171,8 +1277,8 @@ bool nmWxForecast::readRateSchedule(QVector<double>& durations, QVector<double>&
bool durationOk = false, rateOk = false; bool durationOk = false, rateOk = false;
double duration = durationText.toDouble(&durationOk); double duration = durationText.toDouble(&durationOk);
double rate = rateText.toDouble(&rateOk); double rate = rateText.toDouble(&rateOk);
duration = units.duration.convertValueByUnitType(duration, m_currentDurationUnit, "hr"); duration = convertForecastValue(units.duration, duration, m_currentDurationUnit, "hr");
rate = units.q0.convertValueByUnitType(rate, m_currentRateUnit, "m^3/D"); rate = convertForecastValue(units.q0, rate, m_currentRateUnit, "m^3/D");
if(!durationOk || !rateOk || !qIsFinite(duration) || duration <= 0.0 || if(!durationOk || !rateOk || !qIsFinite(duration) || duration <= 0.0 ||
!qIsFinite(rate) || rate < 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); 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(); int newRow = m_pressureTableWidget->rowCount();
m_pressureTableWidget->insertRow(newRow); m_pressureTableWidget->insertRow(newRow);
// 原空行成为制度行,新追加的输入行暂不编号。
m_pressureTableWidget->setVerticalHeaderItem(row, new QTableWidgetItem(QString::number(row)));
m_pressureTableWidget->setVerticalHeaderItem(newRow, new QTableWidgetItem(""));
// 给新行两列预置空 item,这样用户一输入就居中 // 给新行两列预置空 item,这样用户一输入就居中
m_pressureTableWidget->setItem(newRow, 0, makeCenteredEmptyItem()); m_pressureTableWidget->setItem(newRow, 0, makeCenteredEmptyItem());
@ -1294,7 +1403,7 @@ void nmWxForecast::onPressureUnitChanged(const QString& unit)
if(dataItem && !dataItem->text().isEmpty()) { if(dataItem && !dataItem->text().isEmpty()) {
double oldDisplayValue = dataItem->text().toDouble(); 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)); dataItem->setText(QString::number(newDisplayValue));
} }
} }
@ -1322,6 +1431,9 @@ void nmWxForecast::onRateCellChanged(int row, int column)
int newRow = m_rateTableWidget->rowCount(); int newRow = m_rateTableWidget->rowCount();
m_rateTableWidget->insertRow(newRow); 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, 0, makeCenteredEmptyItem());
m_rateTableWidget->setItem(newRow, 1, makeCenteredEmptyItem()); m_rateTableWidget->setItem(newRow, 1, makeCenteredEmptyItem());
@ -1351,7 +1463,7 @@ void nmWxForecast::onSharedDurationUnitChanged(const QString& unit)
if(dataItem && !dataItem->text().isEmpty()) { if(dataItem && !dataItem->text().isEmpty()) {
double oldDisplayValue = dataItem->text().toDouble(); 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)); dataItem->setText(QString::number(newDisplayValue));
} }
} }
@ -1373,7 +1485,7 @@ void nmWxForecast::onSharedDurationUnitChanged(const QString& unit)
double oldDisplayValue = dataItem->text().toDouble(&ok); double oldDisplayValue = dataItem->text().toDouble(&ok);
if(!ok || !qIsFinite(oldDisplayValue)) continue; 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)); dataItem->setText(QString::number(newDisplayValue, 'g', 15));
} }
} }
@ -1414,7 +1526,7 @@ void nmWxForecast::onRateUnitChanged(const QString& unit)
double oldDisplayValue = dataItem->text().toDouble(&ok); double oldDisplayValue = dataItem->text().toDouble(&ok);
if(!ok || !qIsFinite(oldDisplayValue)) continue; if(!ok || !qIsFinite(oldDisplayValue)) continue;
// 使用 q0 (FlowRate) 进行单位转换 // 使用 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)); dataItem->setText(QString::number(newDisplayValue, 'g', 15));
} }
} }

@ -1087,7 +1087,9 @@ void nmWxNumericalDesign::onCustomNumericalSettingsClicked()
// 针对高级组中的"时间相关"按钮的槽函数 // 针对高级组中的"时间相关"按钮的槽函数
void nmWxNumericalDesign::onTimeIconClicked() void nmWxNumericalDesign::onTimeIconClicked()
{ {
nmWxTimeDependentSkin::showForCurrentWell(); // 使用所属分析的当前井,并让子窗口随本分析面板一起销毁。
if(!m_pDataManager || !m_pDataManager->getCurWellData()) return;
nmWxTimeDependentSkin::showForCurrentWell(m_pDataManager->getCurWellData(), this);
} }
void nmWxNumericalDesign::onOutputResultFieldsAutomaticToggled(bool checked) 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 <QVBoxLayout>
#include "nmDialogPreviewGuard.h" #include "nmDialogPreviewGuard.h"
@ -15,6 +20,91 @@
#include "nmDataAnalyzeManager.h" #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) nmWxPerforationClosing::nmWxPerforationClosing(nmDataWellBase* pWellData, QWidget *parent)
: iDlgBase(parent) : iDlgBase(parent)
{ {
@ -32,19 +122,15 @@ nmWxPerforationClosing::nmWxPerforationClosing(nmDataWellBase* pWellData, QWidge
m_pCurrentPerforationItem = 0; m_pCurrentPerforationItem = 0;
m_currentWell = pWellData; m_currentWell = pWellData;
m_currentPerforationIndex = -1;
if(m_currentWell != nullptr) { // 按射孔保存独立副本,切换目标不会覆盖其他射孔的封堵记录。
int perforationCount = m_currentWell->getPerforationCount(); if(m_currentWell) {
// 选择操作的射孔段索引 for(int i = 0; i < m_currentWell->getPerforationCount(); ++i) {
m_currentPerforationIndex = perforationCount - 1; m_perforations.insert(i, m_currentWell->getPerforationCopy(i));
m_perforationData = m_currentWell->getPerforationCopy(m_currentPerforationIndex); }
m_currentPerforationIndex = m_currentWell->getPerforationCount() - 1;
// 从副本中读取封堵数据 }
m_baseTimeValues = m_perforationData.getTimeValues(); loadDataFromModel();
m_baseTime = m_perforationData.getBaseTime();
m_hasBaseTime = m_baseTime.isValid();
}
// 初始化UI // 初始化UI
initUI(); initUI();
@ -57,7 +143,11 @@ nmWxPerforationClosing::nmWxPerforationClosing(nmDataWellBase* pWellData, QWidge
connect(m_pOkBtn, SIGNAL(clicked()), this, SLOT(onOkClicked())); connect(m_pOkBtn, SIGNAL(clicked()), this, SLOT(onOkClicked()));
connect(m_pCancelBtn, SIGNAL(clicked()), this, SLOT(onCancelClicked())); connect(m_pCancelBtn, SIGNAL(clicked()), this, SLOT(onCancelClicked()));
connect(m_pTreeWidget, SIGNAL(itemChanged(QTreeWidgetItem*, int)), this, SLOT(onItemChanged(QTreeWidgetItem*, int))); 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() nmWxPerforationClosing::~nmWxPerforationClosing()
@ -69,9 +159,6 @@ void nmWxPerforationClosing::initUI()
// 创建主布局 // 创建主布局
QVBoxLayout* mainLayout = new QVBoxLayout(this); QVBoxLayout* mainLayout = new QVBoxLayout(this);
QString appDir = QCoreApplication::applicationDirPath();
appDir = appDir.section('/', 0, -2); // 获取上一级目录
// 创建工具栏容器 // 创建工具栏容器
QWidget* toolbarContainer = new QWidget(this); QWidget* toolbarContainer = new QWidget(this);
toolbarContainer->setMinimumHeight(70); toolbarContainer->setMinimumHeight(70);
@ -86,7 +173,8 @@ void nmWxPerforationClosing::initUI()
addContainerLayout->setContentsMargins(2, 2, 2, 2); addContainerLayout->setContentsMargins(2, 2, 2, 2);
m_pAddBtn = new QPushButton(addButtonContainer); 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->setIconSize(QSize(36, 36));
m_pAddBtn->setFixedSize(40, 40); m_pAddBtn->setFixedSize(40, 40);
@ -106,7 +194,7 @@ void nmWxPerforationClosing::initUI()
removeContainerLayout->setContentsMargins(2, 2, 2, 2); removeContainerLayout->setContentsMargins(2, 2, 2, 2);
m_pRemoveBtn = new QPushButton(removeButtonContainer); 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->setIconSize(QSize(36, 36));
m_pRemoveBtn->setFixedSize(40, 40); m_pRemoveBtn->setFixedSize(40, 40);
@ -125,23 +213,28 @@ void nmWxPerforationClosing::initUI()
toolbarLayout->addSpacing(15); // 复选框和时间单位之间的固定间距 toolbarLayout->addSpacing(15); // 复选框和时间单位之间的固定间距
m_pTimeUnitLabel = new QLabel(tr("Time unit:")); // 保存指针以便隐藏/显示 // 单位控件单独保留占位,切换日期显示时前面的控件不移动。
toolbarLayout->addWidget(m_pTimeUnitLabel); QWidget* timeUnitContainer = new QWidget(toolbarContainer);
QHBoxLayout* timeUnitLayout = new QHBoxLayout(timeUnitContainer);
toolbarLayout->addSpacing(5); // 标签和下拉框之间的小间距 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 = new QComboBox();
m_pTimeUnitComboBox->addItem("ms"); iUnitGroup* group = iUnitHelper::getUnitGroupByUnit("s");
m_pTimeUnitComboBox->addItem("sec"); const QStringList units = group ? group->getAllUnitNames() : (QStringList() << "hr");
m_pTimeUnitComboBox->addItem("min"); m_pTimeUnitComboBox->addItems(units);
m_pTimeUnitComboBox->addItem("hr"); if(m_currentTimeUnit == "hr" && units.contains("h")) m_currentTimeUnit = "h";
m_pTimeUnitComboBox->addItem("day"); int unitIndex = m_pTimeUnitComboBox->findText(m_currentTimeUnit);
m_pTimeUnitComboBox->addItem("week"); m_pTimeUnitComboBox->setCurrentIndex(unitIndex >= 0 ? unitIndex : 0);
m_pTimeUnitComboBox->addItem("month"); m_currentTimeUnit = m_pTimeUnitComboBox->currentText();
m_pTimeUnitComboBox->addItem("year");
m_pTimeUnitComboBox->setCurrentIndex(3); // "hr"
m_pTimeUnitComboBox->setFixedWidth(60); m_pTimeUnitComboBox->setFixedWidth(60);
toolbarLayout->addWidget(m_pTimeUnitComboBox); timeUnitLayout->addWidget(m_pTimeUnitComboBox);
timeUnitContainer->setFixedSize(timeUnitContainer->sizeHint());
toolbarLayout->addWidget(timeUnitContainer);
// 避免控件贴边 // 避免控件贴边
toolbarLayout->addSpacing(10); toolbarLayout->addSpacing(10);
@ -150,33 +243,54 @@ void nmWxPerforationClosing::initUI()
mainLayout->addWidget(toolbarContainer); mainLayout->addWidget(toolbarContainer);
// 创建单个树形表格控件 // 创建单个树形表格控件
m_pTreeWidget = new QTreeWidget(this); m_pTreeWidget = new PerforationClosingTree(this);
m_pTreeWidget->setColumnCount(3); // 数值时间和日期共用第一列,第二列显示射孔状态。
m_pTreeWidget->setColumnCount(2);
m_pTreeWidget->setItemDelegate(new PerforationClosingDelegate(m_pTreeWidget));
// 设置表头 // 设置表头
QStringList headers; QStringList headers;
headers << "" << "" << ""; headers << "" << "";
m_pTreeWidget->setHeaderLabels(headers); m_pTreeWidget->setHeaderLabels(headers);
m_pTreeWidget->setHeaderHidden(false); // 显示表头 m_pTreeWidget->setHeaderHidden(true); // 列标题显示在各射孔分组内部
// 设置列宽 // 设置列宽
m_pTreeWidget->setColumnWidth(0, 200); // 树形结构列 m_pTreeWidget->setColumnWidth(0, 260); // 分组和开始时间,预留日期文字及树缩进宽度
m_pTreeWidget->setColumnWidth(1, 120); // 时间列 m_pTreeWidget->setColumnWidth(1, 180); // 射孔状态
m_pTreeWidget->setColumnWidth(2, 120); // 状态列
// 设置表头拉伸策略 // 设置表头拉伸策略
m_pTreeWidget->header()->setStretchLastSection(true); m_pTreeWidget->header()->setStretchLastSection(true);
m_pTreeWidget->header()->setHighlightSections(false);
m_pTreeWidget->header()->setDefaultAlignment(Qt::AlignCenter);
// 设置树形控件的其他属性 // 设置树形控件的其他属性
m_pTreeWidget->setRootIsDecorated(true); // 显示根节点的展开/折叠图标 m_pTreeWidget->setRootIsDecorated(true); // 显示根节点的展开/折叠图标
m_pTreeWidget->setAlternatingRowColors(true); // 交替行颜色 m_pTreeWidget->setAlternatingRowColors(true); // 交替行颜色
m_pTreeWidget->setSelectionBehavior(QAbstractItemView::SelectRows); // 整行选择 m_pTreeWidget->setSelectionBehavior(QAbstractItemView::SelectRows); // 整行选择
m_pTreeWidget->setSelectionMode(QAbstractItemView::SingleSelection);
m_pTreeWidget->setIndentation(20); // 设置缩进距离 m_pTreeWidget->setIndentation(20); // 设置缩进距离
// 选中行保持浅蓝底色;网格由树控件统一绘制,不依赖单元格代理。
m_pTreeWidget->setEditTriggers(QAbstractItemView::DoubleClicked);
m_pTreeWidget->setStyleSheet( m_pTreeWidget->setStyleSheet(
"QTreeWidget::branch {" "QTreeWidget {"
"background: transparent;" " background-color: palette(base);"
"border: none;" " alternate-background-color: rgb(232, 232, 232);"
"width: 0px;" " 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 = new QTreeWidgetItem(m_pTreeWidget);
m_pPerforationRoot->setText(0, tr("Perforation")); m_pPerforationRoot->setText(0, tr("Perforation"));
m_pPerforationRoot->setFirstColumnSpanned(true);
m_pPerforationRoot->setChildIndicatorPolicy(QTreeWidgetItem::DontShowIndicator); m_pPerforationRoot->setChildIndicatorPolicy(QTreeWidgetItem::DontShowIndicator);
// 为每个射孔段添加树形项目 // 为每个射孔段添加树形项目
@ -202,6 +317,8 @@ void nmWxPerforationClosing::initUI()
QTreeWidgetItem* perforationItem = new QTreeWidgetItem(m_pTreeWidget); QTreeWidgetItem* perforationItem = new QTreeWidgetItem(m_pTreeWidget);
perforationItem->setText(0, perforationName); perforationItem->setText(0, perforationName);
perforationItem->setData(0, Qt::UserRole, i);
perforationItem->setFirstColumnSpanned(true);
perforationItem->setExpanded(false); // 默认收起 perforationItem->setExpanded(false); // 默认收起
// 如果是当前选中的射孔段,保存引用 // 如果是当前选中的射孔段,保存引用
@ -246,7 +363,7 @@ void nmWxPerforationClosing::initUI()
} }
} }
m_pAllComboBox->setFixedWidth(120); // 增加宽度以适应射孔段名称 m_pAllComboBox->setFixedWidth(180); // 加宽下拉框,便于查看射孔段名称
//m_pAllComboBox = new QComboBox(); //m_pAllComboBox = new QComboBox();
//m_pAllComboBox->addItem(tr("All")); //m_pAllComboBox->addItem(tr("All"));
//m_pAllComboBox->addItem(tr("Option 1")); //m_pAllComboBox->addItem(tr("Option 1"));
@ -274,16 +391,6 @@ void nmWxPerforationClosing::initUI()
mainLayout->setSpacing(5); 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() void nmWxPerforationClosing::onAddClicked()
{ {
if(!m_pCurrentPerforationItem) if(!m_pCurrentPerforationItem)
@ -295,29 +402,27 @@ void nmWxPerforationClosing::onAddClicked()
m_hasBaseTime = true; 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; QList<double> newValuesInHr;
if(m_baseTimeValues.isEmpty()) { if(m_baseTimeValues.isEmpty()) {
// 第一次Add:添加100.0小时 // 第一次Add:添加100.0小时
newValuesInHr.append(100.0); newValuesInHr.append(100.0);
} else if(m_baseTimeValues.size() == 1) { } else if(m_baseTimeValues.size() == 1 || selectedIndex == m_baseTimeValues.size() - 1) {
// 第二次Add:添加200.0小时 // 最后一行没有后续区间,沿用初始的100小时步长向后追加一条。
newValuesInHr.append(200.0); newValuesInHr.append(m_baseTimeValues.last() + 100.0);
} else { } else {
// 第三次及以后:只在第一个区间中插入两个点 // 保留三等分插入两个点的规则,但使用所选行与下一行之间的区间。
QList<double> sortedValues = m_baseTimeValues; // 只选中射孔名称或表头时,沿用原来的第一个区间。
std::sort(sortedValues.begin(), sortedValues.end()); if(selectedIndex < 0) selectedIndex = 0;
const double start = m_baseTimeValues[selectedIndex];
// 只在第一个区间插入两个均匀分布的点 const double end = m_baseTimeValues[selectedIndex + 1];
if(sortedValues.size() >= 2) { const double interval = (end - start) / 3.0;
double start = sortedValues[0]; newValuesInHr.append(start + interval);
double end = sortedValues[1]; newValuesInHr.append(start + 2 * interval);
double interval = (end - start) / 3.0;
newValuesInHr.append(start + interval);
newValuesInHr.append(start + 2 * interval);
}
} }
// 合并原有数值和新数值,然后排序 // 合并原有数值和新数值,然后排序
@ -327,7 +432,7 @@ void nmWxPerforationClosing::onAddClicked()
// 刷新显示 // 刷新显示
refreshDisplay(); refreshDisplay();
// 展开Perforation #1项目以显示新添加的内容 // 展开当前选中的射孔分组以显示新添加的内容
m_pCurrentPerforationItem->setExpanded(true); m_pCurrentPerforationItem->setExpanded(true);
// 更新计数器 // 更新计数器
@ -354,8 +459,8 @@ void nmWxPerforationClosing::onRemoveClicked()
isValidDataRow = !selectedItem->text(0).isEmpty() && isValidDataRow = !selectedItem->text(0).isEmpty() &&
QDateTime::fromString(selectedItem->text(0), "yyyy-MM-dd hh:mm:ss").isValid(); QDateTime::fromString(selectedItem->text(0), "yyyy-MM-dd hh:mm:ss").isValid();
} else { } else {
// 数值模式下检查第1列是否有数值数据 // 数值时间也位于第一列。
isValidDataRow = !selectedItem->text(1).isEmpty(); isValidDataRow = !selectedItem->text(0).isEmpty();
} }
if(isValidDataRow) { if(isValidDataRow) {
@ -389,12 +494,18 @@ void nmWxPerforationClosing::onRemoveClicked()
qSort(timeValuesInHr.begin(), timeValuesInHr.end()); qSort(timeValuesInHr.begin(), timeValuesInHr.end());
if(hasSelection && selectedIndex >= 0 && selectedIndex < timeValuesInHr.size()) { // 未选中具体事件时,沿用删除末尾事件的入口。
timeValuesInHr.removeAt(selectedIndex); if(!hasSelection || selectedIndex < 0 || selectedIndex >= timeValuesInHr.size()) {
} else if(!timeValuesInHr.isEmpty()) { selectedIndex = timeValuesInHr.size() - 1;
timeValuesInHr.removeLast();
} }
// 按Saphir的行操作规则删除当前行及下一行,先删下一行以免索引移位。
if(selectedIndex + 1 < timeValuesInHr.size()) {
timeValuesInHr.removeAt(selectedIndex + 1);
}
// 当前行位于末尾时,只删除自身。
timeValuesInHr.removeAt(selectedIndex);
m_baseTimeValues = timeValuesInHr; m_baseTimeValues = timeValuesInHr;
if(m_baseTimeValues.isEmpty()) { if(m_baseTimeValues.isEmpty()) {
@ -407,77 +518,69 @@ void nmWxPerforationClosing::onRemoveClicked()
void nmWxPerforationClosing::refreshDisplay() void nmWxPerforationClosing::refreshDisplay()
{ {
if(!m_pCurrentPerforationItem) return; // 显示单位和日期模式属于整个窗口,刷新时同时更新所有射孔分组。
storeCurrentPerforation();
m_pTreeWidget->blockSignals(true); const bool blocked = m_pTreeWidget->blockSignals(true);
QTreeWidgetItem* selected = m_pTreeWidget->currentItem();
// 清理除“Start time”外的子行 QTreeWidgetItem* selectedGroup = selected && selected->parent() ? selected->parent() : selected;
for(int i = m_pCurrentPerforationItem->childCount() - 1; i >= 0; --i) { const int selectedRow = selected && selected->parent() ? selectedGroup->indexOfChild(selected) : -1;
QTreeWidgetItem* it = m_pCurrentPerforationItem->child(i);
if(it == nullptr) { for(int groupIndex = 0; groupIndex < m_pTreeWidget->topLevelItemCount(); ++groupIndex) {
continue; 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);
} }
for(int i = 0; i < values.size(); ++i) {
if(it->text(0) != tr("Start time")) { QTreeWidgetItem* row = new QTreeWidgetItem(group);
m_pCurrentPerforationItem->removeChild(it); const double seconds = toSecondsQuantized(values[i], "hr");
delete it; 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 {
// 如有数据,确保有“Start time”行 row->setText(0, QString::number(fromSeconds(seconds, m_currentTimeUnit), 'g', 15));
if(!m_baseTimeValues.isEmpty()) { row->setTextAlignment(0, Qt::AlignRight | Qt::AlignVCenter);
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;
} }
}
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->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);
} }
group->setExpanded(wasExpanded || (!hadRows && !values.isEmpty()));
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);
} }
// 重建子行后按分组及行号恢复选择,不能继续使用已释放的行指针。
m_pTreeWidget->blockSignals(false); if(selectedGroup) {
m_pTreeWidget->setCurrentItem(selectedRow >= 0 && selectedGroup->childCount() > 0
if(!m_baseTimeValues.isEmpty()) { ? selectedGroup->child(qMin(selectedRow, selectedGroup->childCount() - 1)) : selectedGroup);
m_pCurrentPerforationItem->setExpanded(true); } 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) void nmWxPerforationClosing::onTimeUnitChanged(const QString &unit)
{ {
// 更新当前时间单位 // 更新当前时间单位
@ -547,6 +650,7 @@ void nmWxPerforationClosing::showSpecificPerforationItem(int perforationIndex)
if(item) { if(item) {
item->setHidden(false); item->setHidden(false);
item->setExpanded(true); // 展开显示内容 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(item->parent() != m_pCurrentPerforationItem) return;
// 根据显示模式处理不同列的编辑 // 两种模式都编辑开始时间列,按显示模式选择解析方式。
if(m_showDatesMode && column == 0) { if(m_showDatesMode && column == 0) {
// 日期模式下编辑第0列(时间字符串) // 日期模式下编辑第0列(时间字符串)
processDateEdit(item); processDateEdit(item);
} else if(!m_showDatesMode && column == 1) { } else if(!m_showDatesMode && column == 0) {
// 数值模式下编辑第1列(数值) // 数值模式下编辑第0列(数值)
processValueEdit(item); processValueEdit(item);
} }
} }
@ -686,7 +790,8 @@ void nmWxPerforationClosing::processValueEdit(QTreeWidgetItem* item)
guard = true; guard = true;
bool ok = false; bool ok = false;
const double newDisp = item->text(1).toDouble(&ok); // 数值时间与日期统一从第一列读取。
const double newDisp = item->text(0).toDouble(&ok);
if(!ok) { if(!ok) {
guard = false; guard = false;
@ -749,57 +854,62 @@ void nmWxPerforationClosing::processValueEdit(QTreeWidgetItem* item)
guard = false; 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; 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) { void nmWxPerforationClosing::saveDataToModel()
QString state = (i % 2 == 0) ? tr("Closed") : tr("Open"); {
states.append(state); // 统一提交各射孔的封堵字段,保留井对象中其他参数的最新值。
} storeCurrentPerforation();
if(!m_currentWell) return;
m_perforationData.setTimeStates(states); for(QMap<int, nmDataPerforation>::const_iterator it = m_perforations.constBegin();
it != m_perforations.constEnd(); ++it) {
// 保存基准时间(如果有的话) nmDataPerforation data = m_currentWell->getPerforationCopy(it.key());
if(m_hasBaseTime && m_baseTime.isValid()) { data.setTimeValues(it.value().getTimeValues());
m_perforationData.setBaseTime(m_baseTime); 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() void nmWxPerforationClosing::loadDataFromModel()
{ {
// 直接获取时间值数据 // 切换编辑缓冲区时不重建树,避免删除正在点击或编辑的子行。
m_baseTimeValues = m_perforationData.getTimeValues(); const nmDataPerforation data = m_perforations.value(m_currentPerforationIndex);
m_baseTimeValues = data.getTimeValues();
// 恢复基准时间 m_baseTime = data.getBaseTime();
QDateTime savedBaseTime = m_perforationData.getBaseTime(); m_hasBaseTime = m_baseTime.isValid();
if(savedBaseTime.isValid()) {
m_baseTime = savedBaseTime;
m_hasBaseTime = true;
} else {
m_hasBaseTime = false;
}
// 更新计数器
m_timeEntryCounter = m_baseTimeValues.size(); m_timeEntryCounter = m_baseTimeValues.size();
// 刷新界面显示
refreshDisplay();
} }
double nmWxPerforationClosing::convertTime(double value, const QString& fromUnit, const QString& toUnit) const double nmWxPerforationClosing::convertTime(double value, const QString& fromUnit, const QString& toUnit) const
{ {
nmDataAttribute a; // 直接使用底层单位组,兼容内部 hr/sec 与配置中的 h/s 写法。
a.setAttributeUnitType(UNIT_TYPE_TIME); iUnitGroup* group = iUnitHelper::getUnitGroupByUnit("s");
return a.convertValueByUnitType(value, fromUnit, toUnit); 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 double nmWxPerforationClosing::roundToSecondDouble(double sec) const

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