You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
nmWTAI-Platform/Src/nmNum/nmSubWxs/nmWxPostprocessingAnimation...

1597 lines
65 KiB
C++

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

#include "nmWxPostprocessingAnimationWidget.h"
#include <QDebug> // 用于调试输出
#include <algorithm> // 用于 std::sort 排序算法
// UI 相关的 Qt 头文件
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QFrame>
#include <QPushButton>
#include <QLabel>
#include <QTimer>
#include <QComboBox>
#include <QSlider>
#include <QList>
#include <QEvent>
#include <QMouseEvent>
#include <QPrinter>
#include <QMenu>
#include <QAction>
#include <QPixmap>
#include <QFileDialog>
#include <QApplication>
#include <QClipboard>
#include <QPrinter>
#include <QPrintDialog>
#include <QPrintPreviewDialog>
#include <QCoreApplication>
// VTK 相关的头文件
#include "QVTKWidget.h" // QVTKWidget 的实际头文件路径
#include <vtkSmartPointer.h>
#include <vtkRenderWindow.h>
#include <vtkRenderer.h>
#include <vtkDataSetReader.h> // 如果有文件读取操作,需要此头文件
#include <vtkUnstructuredGrid.h> // 非结构化网格数据类型
#include <vtkDataSetMapper.h>
#include <vtkPointData.h> // 点数据
#include <vtkActor.h>
#include <vtkProperty.h> // 演员属性,如颜色、线宽等
#include <vtkScalarBarActor.h>
#include <vtkLookupTable.h>
#include <vtkTextActor.h>
#include <vtkTextProperty.h> // 文本演员属性,如字体、大小、颜色
#include <vtkDoubleArray.h> // 双精度浮点数数组
#include <vtkCellData.h> // 单元格数据
#include <vtkWindowToImageFilter.h>
#include <vtkImageData.h>
#include <vtkImageFlip.h>
#include <vtkPNGWriter.h>
#include <vtkUnsignedCharArray.h>
// === 新增的 3D 绘制相关的 VTK 头文件 ===
#include <vtkBillboardTextActor3D.h> // 广告牌文本演员 (始终面向相机,适用于标签)
#include <vtkPolyData.h> // 多边形数据 (用于线、面等几何体)
#include <vtkPoints.h> // 点集合
#include <vtkCellArray.h> // 单元格数组 (用于定义线的拓扑结构)
#include <vtkCamera.h> // 相机对象,用于获取相机位置等信息
#include <vtkCoordinate.h> // 坐标系统转换,如果使用 vtkTextActor 且要精确放置
#include <vtkPolyDataMapper.h>
#include <vtkFFMPEGWriter.h>
#include <vtkThreshold.h>
#include <vtkContourWidget.h>
#include <vtkObject.h>
#include <vtkPolygonalSurfacePointPlacer.h>
#include <vtkOrientedGlyphContourRepresentation.h>
#include <vtkPolyDataCollection.h>
#include <vtkGeometryFilter.h>
#include <vtkContourRepresentation.h>
#include <vtkRendererCollection.h>
#include <vtkInteractorStyleTrackballCamera.h>
#include <vtkCellPicker.h>
#include <vtkPolyDataNormals.h>
#include <vtkCollectionIterator.h>
#include <vtkPolygonalSurfaceContourLineInterpolator.h>
#include <vtkExtractUnstructuredGrid.h>
#include <vtkImplicitSelectionLoop.h>
#include <vtkExtractGeometry.h>
#include <cmath>
// 业务逻辑相关的头文件
#include "nmDataAnalyzeManager.h" // 数据管理器
// 数据过滤对话框
#include "nmWxPostprocessingAnimationDataFiltering.h"
// 定义一个统一的 Z 坐标高度,用于绘制井和井名。
// 这个值非常重要,需要根据您的三维场景的实际尺寸和范围进行调整。
// 它应该足够高,以便井的绘制内容清晰可见,并且不会被地层模型遮挡。
const double WELL_DRAWING_Z_HEIGHT = 200.0; // 示例值,请根据您的模型范围进行调整
//=======================================================
//nmCustomInteractorStyle 的实现
//=======================================================
vtkStandardNewMacro(nmCustomInteractorStyle);
void nmCustomInteractorStyle::SetParent(nmWxPostprocessingAnimationWidget* parent) {
m_parent = parent;
}
void nmCustomInteractorStyle::OnLeftButtonDown() {
if (!m_parent) {
vtkInteractorStyleTrackballCamera::OnLeftButtonDown();
return;
}
if (m_parent->isDrawingMode()) {
int* clickPos = this->GetInteractor()->GetEventPosition();
auto contourWidget = m_parent->getContourWidget();
// 使用 vtkContourRepresentation 类型保持一致
auto rep = vtkContourRepresentation::SafeDownCast(contourWidget->GetRepresentation());
if (rep) {
vtkSmartPointer<vtkCellPicker> picker = vtkSmartPointer<vtkCellPicker>::New();
picker->SetTolerance(0.01);
picker->Pick(clickPos[0], clickPos[1], 0, m_parent->getRenderer());
if (picker->GetCellId() != -1) {
double worldPos[3];
picker->GetPickPosition(worldPos);
m_parent->addContourPoint(worldPos);
}
}
} else {
vtkInteractorStyleTrackballCamera::OnLeftButtonDown();
}
}
nmContourResultDialog::nmContourResultDialog(QWidget *parent)
: iDlgBase(parent)
{
setWindowTitle(tr("Contour Pressure"));
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
m_resultLabel = new QLabel(this);
m_resultLabel->setWordWrap(true);
m_clearButton = new QPushButton(tr("OK"), this);
connect(m_clearButton, SIGNAL(clicked()), this, SIGNAL(clearContourRequested()));
QVBoxLayout* layout = new QVBoxLayout(this);
layout->addWidget(m_resultLabel);
layout->addWidget(m_clearButton);
setLayout(layout);
}
void nmContourResultDialog::showResult(const QString& message)
{
m_resultLabel->setText(message);
this->show(); // 使用 show() 来显示非模态对话框
}
nmWxPostprocessingAnimationWidget::nmWxPostprocessingAnimationWidget(QWidget* parent) :
QWidget(parent) // 调用 QWidget 基类的构造函数
{
// 初始化成员变量
m_nCurrentIndex = 0;
m_bIsPlaying = false;
m_nShowMode = 0;
m_pSlider = nullptr; // 滑块指针初始化为空
m_pProgress = nullptr; // 进度文本标签指针初始化为空
// **** VTK SmartPointer 成员变量的初始化 ****
m_renderer = nullptr;
m_mapper = nullptr;
m_actor = nullptr;
m_scalarBar = nullptr;
m_lookupTable = nullptr;
m_textActor = nullptr;
m_pCachedBaseGrid = nullptr;
m_thresholdFilter = nullptr;
// 初始化过滤器设置内容
m_bFilteringEnabled = false;
m_bAboveMinEnabled = false;
m_dMinValue = 0.0;
m_bBelowMaxEnabled = false;
m_dMaxValue = 0.0;
// 初始化区域平均压力计算内容
m_bIsDrawingMode = false;
m_contourWidget = nullptr;
m_contourRepresentation = nullptr;
m_pointPlacer = nullptr;
m_polyDataForContour = nullptr;
m_trackballStyle = nullptr;
m_drawStyle = nullptr;
m_interactor = nullptr;
m_resultDialog = new nmContourResultDialog();
// 连接对话框的信号到槽函数
connect(m_resultDialog, SIGNAL(clearContourRequested()),
this, SLOT(slotClearContour()));
// 初始化时间步列表 (在 initUI 之前,因为它会影响 UI 控件的最大值)
this->initTimeSteps();
// 初始化用户界面
this->initUI();
}
// 析构函数:用于在对象销毁时进行清理
nmWxPostprocessingAnimationWidget::~nmWxPostprocessingAnimationWidget()
{
// Qt 的父子对象机制会自动删除 QVTKWidget 和其他 Qt 控件(如 m_pSlider, m_pProgress, m_pPlayTimer
// vtkSmartPointer 会自动管理其内部 VTK 对象的内存。
// 但为了确保 VTK 渲染器从 RenderWindow 中被正确移除(避免潜在的 VTK 内部引用问题),
// 可以在这里显式执行移除操作。
if(m_pVtkWidget && m_pVtkWidget->GetRenderWindow() && m_renderer) {
m_pVtkWidget->GetRenderWindow()->RemoveRenderer(m_renderer);
}
// 显式清除所有井相关的 Actor。
clearWellActors();
// 其他 vtkSmartPointer 成员 (m_mapper, m_actor, etc.) 会在 nmWxPostprocessingAnimationWidget 销毁时自动释放其 VTK 对象。
// 在销毁前禁用轮廓小部件
if (m_contourWidget) {
m_contourWidget->EnabledOff();
m_contourWidget = nullptr;
}
if (m_resultDialog != nullptr)
{
delete m_resultDialog;
m_resultDialog = nullptr;
}
}
QImage nmWxPostprocessingAnimationWidget::createQImage1(int nWidth, int nHeight, vtkUnsignedCharArray* pScalars)
{
QImage qImage(nWidth, nHeight, QImage::Format_ARGB32);
vtkIdType tupleIndex = 0;
int nImageBitIndex = 0;
QRgb* pImageBits = (QRgb*)qImage.bits();
unsigned char* scalarTuples = pScalars->GetPointer(0);
for(int j = 0; j < nHeight; j++) {
for(int i = 0; i < nWidth; i++) {
unsigned char* tuple = scalarTuples + (tupleIndex++);
QRgb color = qRgba(tuple[0], tuple[0], tuple[0], 255);
*(pImageBits + (nImageBitIndex++)) = color;
}
}
return qImage;
}
QImage nmWxPostprocessingAnimationWidget::createQImage2(int nWidth, int nHeight, vtkUnsignedCharArray* pScalars)
{
QImage qImage(nWidth, nHeight, QImage::Format_ARGB32);
vtkIdType tupleIndex = 0;
int nImageBitIndex = 0;
QRgb* pImageBits = (QRgb*)qImage.bits();
unsigned char* scalarTuples = pScalars->GetPointer(0);
for(int j = 0; j < nHeight; j++) {
for(int i = 0; i < nWidth; i++) {
unsigned char* tuple = scalarTuples + (tupleIndex++ * 2);
QRgb color = qRgba(tuple[0], tuple[0], tuple[0], tuple[1]);
*(pImageBits + (nImageBitIndex++)) = color;
}
}
return qImage;
}
QImage nmWxPostprocessingAnimationWidget::createQImage3(int nWidth, int nHeight, vtkUnsignedCharArray* pScalars)
{
QImage qImage(nWidth, nHeight, QImage::Format_ARGB32);
vtkIdType tupleIndex = 0;
int nImageBitIndex = 0;
QRgb* pImageBits = (QRgb*)qImage.bits();
unsigned char* scalarTuples = pScalars->GetPointer(0);
for(int j = 0; j < nHeight; j++) {
for(int i = 0; i < nWidth; i++) {
unsigned char* tuple = scalarTuples + (tupleIndex++ * 3);
QRgb color = qRgba(tuple[0], tuple[1], tuple[2], 255);
*(pImageBits + (nImageBitIndex++)) = color;
}
}
return qImage;
}
QImage nmWxPostprocessingAnimationWidget::createQImage4(int nWidth, int nHeight, vtkUnsignedCharArray* pScalars)
{
QImage qImage(nWidth, nHeight, QImage::Format_ARGB32);
vtkIdType tupleIndex = 0;
int nImageBitIndex = 0;
QRgb* pImageBits = (QRgb*)qImage.bits();
unsigned char* scalarTuples = pScalars->GetPointer(0);
for(int j = 0; j < nHeight; j++) {
for(int i = 0; i < nWidth; i++) {
unsigned char* tuple = scalarTuples + (tupleIndex++ * 4);
QRgb color = qRgba(tuple[0], tuple[1], tuple[2], tuple[3]);
*(pImageBits + (nImageBitIndex++)) = color;
}
}
return qImage;
}
QImage nmWxPostprocessingAnimationWidget::createQImage(vtkImageData* pImageData)
{
if(!pImageData) {
return QImage();
}
int nWidth = pImageData->GetDimensions()[0];
int nHeight = pImageData->GetDimensions()[1];
vtkUnsignedCharArray* pScalars = vtkUnsignedCharArray::SafeDownCast(pImageData->GetPointData()->GetScalars());
if(!nWidth || !nHeight || !pScalars) {
return QImage();
}
switch(pScalars->GetNumberOfComponents()) {
case 1:
return createQImage1(nWidth, nHeight, pScalars);
case 2:
return createQImage2(nWidth, nHeight, pScalars);
case 3:
return createQImage3(nWidth, nHeight, pScalars);
case 4:
return createQImage4(nWidth, nHeight, pScalars);
}
return QImage();
}
bool nmWxPostprocessingAnimationWidget::eventFilter(QObject *obj, QEvent *event)
{
// 检查事件是否来自我们的 VTK 部件 (m_pVtkWidget),并且是鼠标按下事件
if(obj == m_pVtkWidget && event->type() == QEvent::MouseButtonPress) {
QMouseEvent *pMouseEVent = static_cast<QMouseEvent*>(event);
// 如果是鼠标右键点击
if(pMouseEVent->button() == Qt::RightButton) {
// 在右键点击 m_pVtkWidget 时,直接显示我们的自定义菜单
QMenu menu(this); // 菜单的父对象仍然是 nmWxPostprocessingAnimationWidget
QString appDir = QCoreApplication::applicationDirPath();
appDir = appDir.section('/', 0, -2); // 获取上一级目录(通常是应用程序的根目录)
// 设置菜单图标
QIcon saveIcon(appDir + "/Res/Icon/SaveImg.png");
QIcon copyIcon(appDir + "/Res/Icon/Copy.png");
QIcon printIcon(appDir + "/Res/Icon/Print.png");
QIcon printPreviewIcon(appDir + "/Res/Icon/PrePrint.png");
QIcon videoIcon(appDir + "/Res/Icon/SaveImg.png");
// 添加“保存为图片”动作,并为其添加图标
QAction *pSaveAction = new QAction(saveIcon, tr("Save as Image"), this);
// 连接动作的 triggered 信号到本类的 saveWidgetAsImage 槽函数
connect(pSaveAction, SIGNAL(triggered()), this, SLOT(saveWidgetAsImage()));
menu.addAction(pSaveAction);
// 添加“复制图片”动作,并为其添加图标
QAction *pCopyAction = new QAction(copyIcon, tr("Copy Image"), this);
// 连接动作的 triggered 信号到本类的 copyWidgetImage 槽函数
connect(pCopyAction, SIGNAL(triggered()), this, SLOT(copyWidgetImage()));
menu.addAction(pCopyAction);
menu.addSeparator(); // 添加分隔线
QAction *pExportVideoAction = new QAction(videoIcon, tr("Export as Video"), this);
connect(pExportVideoAction, SIGNAL(triggered()), this, SLOT(exportAnimationAsVideo()));
menu.addAction(pExportVideoAction);
menu.addSeparator();
// 添加“打印”动作,并为其添加图标
QAction *pPrintAction = new QAction(printIcon, tr("Print"), this);
// 连接动作的 triggered 信号到本类的 printWidget 槽函数
connect(pPrintAction, SIGNAL(triggered()), this, SLOT(printWidget()));
menu.addAction(pPrintAction);
// 添加“打印预览”动作,并为其添加图标
QAction *pPrintPreviewAction = new QAction(printPreviewIcon, tr("Print Preview"), this);
// 连接动作的 triggered 信号到本类的 printPreviewWidget 槽函数
connect(pPrintPreviewAction, SIGNAL(triggered()), this, SLOT(printPreviewWidget()));
menu.addAction(pPrintPreviewAction);
// 在鼠标的全局位置执行菜单,确保菜单在鼠标点击的位置弹出
menu.exec(pMouseEVent->globalPos());
return true; // **事件已处理,停止 QVTKWidget 的进一步处理**
}
// 左键、中键等事件继续传递给VTK
return false;
}
// 对于其他事件或对象,将它们传递给基类的事件过滤器,确保其他事件仍能正常处理
return QWidget::eventFilter(obj, event);
}
void nmWxPostprocessingAnimationWidget::initUI()
{
this->initLayout(); // 设置主布局和框架布局
this->initVTKWidget(); // 初始化 VTK 渲染管道和 QVTKWidget
this->initOperPannel(); // 初始化操作面板的按钮和滑块
}
void nmWxPostprocessingAnimationWidget::initLayout()
{
m_pMainLayout = new QVBoxLayout; // 创建主垂直布局
this->setLayout(m_pMainLayout); // 将此布局设置给当前 Widget
QFrame* frame = new QFrame; // 创建一个 QFrame 用于包含 QVTKWidget
m_pFrameLaoyt = new QVBoxLayout; // 创建框架的垂直布局
m_pFrameLaoyt->setContentsMargins(0, 0, 0, 0); // 设置布局边距为0
frame->setLayout(m_pFrameLaoyt); // 将布局设置给框架
m_pMainLayout->addWidget(frame); // 将框架添加到主布局
m_pMainLayout->setContentsMargins(0, 0, 0, 0); // 设置主布局边距为0
}
void nmWxPostprocessingAnimationWidget::initVTKWidget()
{
// 1. 创建基础VTK组件
m_pVtkWidget = new QVTKWidget(this); // 将 this (当前 nmWxPostprocessingAnimationWidget) 设置为父对象
m_pFrameLaoyt->addWidget(m_pVtkWidget); // 将 QVTKWidget 添加到框架布局中
// 为每个 nmWxPostprocessingAnimationWidget 实例创建独立的 VTK 渲染器
m_renderer = vtkSmartPointer<vtkRenderer>::New();
vtkRenderWindow* renderWindow = m_pVtkWidget->GetRenderWindow();
renderWindow->AddRenderer(m_renderer); // 将渲染器添加到 QVTKWidget 的渲染窗口
m_interactor = renderWindow->GetInteractor();
// 设置渲染器背景
m_renderer->SetGradientBackground(true);
m_renderer->SetBackground(0.67, 0.82, 0.94); // 浅蓝
m_renderer->SetBackground2(0.0, 0.5, 1.0); // 深蓝
// 2. 安装事件过滤器,以便捕获 m_pVtkWidget 的鼠标事件
m_pVtkWidget->installEventFilter(this);
// 3. 创建颜色查找表 (Lookup Table)
m_lookupTable = vtkSmartPointer<vtkLookupTable>::New();
m_lookupTable->SetNumberOfColors(256); // 设置颜色数量
m_lookupTable->SetHueRange(0.67, 0); // 设置色调范围(从蓝色到红色)
m_lookupTable->Build(); // 构建颜色表
// 4. 获取基础结果网格数据
nmDataAnalyzeManager* pDataManager = nmDataAnalyzeManager::getCurrentInstance();
if(pDataManager) {
//m_pCachedBaseGrid = pDataManager->getResultBaseGridCopy();
m_pCachedBaseGrid = pDataManager->getResultBaseGrid();
}
// 5. 创建阈值过滤器和mapper并设置输入数据
m_thresholdFilter = vtkSmartPointer<vtkThreshold>::New();
if(m_pCachedBaseGrid) {
m_thresholdFilter->SetInputData(m_pCachedBaseGrid);
}
m_thresholdFilter->SetInputArrayToProcess(0, 0, 0, vtkDataObject::FIELD_ASSOCIATION_CELLS, "p");
// 上面这行是关键!它告诉过滤器对哪个数组进行操作,
// "p" 是压力数据的名称vtkDataObject::FIELD_ASSOCIATION_CELLS 表示数据在单元格上。
// 创建mapper并设置输入数据
m_mapper = vtkSmartPointer<vtkDataSetMapper>::New();
m_mapper->ScalarVisibilityOn(); // 开启标量数据显示
m_mapper->SetLookupTable(m_lookupTable);
m_mapper->SetInputConnection(m_thresholdFilter->GetOutputPort()); // **将 mapper 的输入连接到阈值过滤器的输出**
// 6. 创建actor并设置mapper
m_actor = vtkSmartPointer<vtkActor>::New();
m_actor->SetMapper(m_mapper); // 永久关联
vtkProperty* actorProperty = m_actor->GetProperty();
actorProperty->SetLineWidth(1.0); // 设置几何体的线宽
actorProperty->EdgeVisibilityOff(); // 默认关闭网格线的显示
actorProperty->SetRepresentationToSurface(); // 设置为表面渲染模式
m_renderer->AddActor(m_actor); // 将演员添加到渲染器中
// 7. 创建颜色条Scalar Bar显示标量数据的范围和颜色映射
m_scalarBar = vtkSmartPointer<vtkScalarBarActor>::New();
m_scalarBar->SetLookupTable(m_lookupTable); // 设置颜色查找表
m_scalarBar->SetTitle("p MPa"); // 设置颜色条标题
// 8. 设置颜色条的标签文本字体属性 (保持不变或根据需要调整)
vtkSmartPointer<vtkTextProperty> labelTextProp = vtkSmartPointer<vtkTextProperty>::New();
labelTextProp->SetFontFamilyToArial();
labelTextProp->SetFontSize(12); // 标签字体大小
m_scalarBar->SetLabelTextProperty(labelTextProp); // 设置标签文本属性
// 设置颜色条的标题文本字体属性 (单独设置,使其更大)
vtkSmartPointer<vtkTextProperty> titleTextProp = vtkSmartPointer<vtkTextProperty>::New();
titleTextProp->SetFontFamilyToArial();
titleTextProp->SetFontSize(20);
//titleTextProp->SetBold(1); // 可以选择加粗
m_scalarBar->SetTitleTextProperty(titleTextProp); // 设置标题文本属性
m_scalarBar->SetPosition(0.05, 0.1); // 设置在渲染窗口中的位置 (归一化坐标)
m_scalarBar->SetWidth(0.1); // 设置宽度
m_scalarBar->SetHeight(0.8); // 设置高度
m_renderer->AddActor(m_scalarBar); // 将颜色条添加到渲染器中
// 创建文本演员,用于显示当前时间步的信息
//m_textActor = vtkSmartPointer<vtkTextActor>::New();
//m_textActor->GetPositionCoordinate()->SetCoordinateSystemToNormalizedDisplay(); // 使用归一化显示坐标
//m_textActor->SetPosition(0.05, 0.9); // 设置位置为左上角 (稍微向内偏移)
//m_textActor->GetTextProperty()->SetFontSize(20); // 设置字体大小
//m_textActor->GetTextProperty()->SetColor(1.0, 1.0, 1.0); // 设置字体颜色为白色 (RGB)
//m_textActor->GetTextProperty()->SetFontFamilyToArial(); // 设置字体
//m_textActor->GetTextProperty()->SetItalic(1); // 设置斜体
//m_textActor->GetTextProperty()->SetBold(1); // 设置加粗
//m_renderer->AddActor(m_textActor); // 将文本演员添加到渲染器中
// 9. 初始化井的绘制(井名和井线),这部分会创建 3D Actor
this->initWellDrawing();
//10. 初始渲染
// 设置压力标量范围
double scalarRange[2];
pDataManager->getScalarRangeP(scalarRange);
m_mapper->SetScalarRange(scalarRange);
m_lookupTable->SetRange(scalarRange);
m_lookupTable->Build();
// 在初始渲染前,为阈值过滤器设置一个默认的范围
// 默认设置为与颜色条相同的范围,这样会显示所有网格单元
m_thresholdFilter->ThresholdBetween(scalarRange[0], scalarRange[1]);
m_thresholdFilter->Modified(); // 通知过滤器其参数已修改
// 修改过滤设置
m_bFilteringEnabled = true;
m_bAboveMinEnabled = true;
m_dMinValue = scalarRange[0];
m_bBelowMaxEnabled = true;
m_dMaxValue = scalarRange[1];
// // 创建自定义交互样式
// m_trackballStyle = vtkSmartPointer<nmCustomInteractorStyle>::New();
// m_trackballStyle->SetParent(this);
//m_trackballStyle = vtkSmartPointer<vtkInteractorStyleTrackballCamera>::New();
m_drawStyle = vtkSmartPointer<nmCustomInteractorStyle>::New();
m_drawStyle->SetParent(this);
// 设置当前交互模式
m_pVtkWidget->GetRenderWindow()->GetInteractor()->SetInteractorStyle(m_drawStyle);
// 加载初始文件 (现在是从内存加载第一个时间步的数据)
if(!m_vecTimeStepKeys.isEmpty()) {
loadDataForIndex(m_nCurrentIndex); // 加载第一个时间步的数据
} else {
qDebug() << "No time step data available to load initially."; // 调试输出:没有时间步数据
}
// 重置相机
m_renderer->ResetCamera();
}
// 新增函数:初始化井的绘制(井名和井线)
void nmWxPostprocessingAnimationWidget::initWellDrawing()
{
clearWellActors(); // 确保每次初始化时清空旧的 Actor避免重复添加
nmDataAnalyzeManager* pDataManager = nmDataAnalyzeManager::getCurrentInstance();
if(!pDataManager) {
return;
}
// 获取所有井的二维位置信息 (QMap<井名, QPointF(X,Y)>)
QMap<QString, QPointF> mapWellLocations = pDataManager->getAllWellLocations();
// 遍历所有井,创建并配置它们的 3D 文本和线 Actor
for(auto it = mapWellLocations.constBegin(); it != mapWellLocations.constEnd(); ++it) {
const QString& sWellName = it.key();
const QPointF& ptLocation = it.value(); // 井的二维位置 (X, Y)
// === 1. 创建井名称的 3D 文本 Actor ===
// 使用 vtkBillboardTextActor3D它始终面向相机文字可读性最好且是 3D 对象。
vtkSmartPointer<vtkBillboardTextActor3D> textActor = vtkSmartPointer<vtkBillboardTextActor3D>::New();
textActor->SetInput(sWellName.toStdString().c_str()); // 设置文本内容为井名称 (转换为 C 字符串)
// 设置 3D 位置:使用井的二维坐标 (X, Y) 和预设的统一 Z 高度
textActor->SetPosition(ptLocation.x(), ptLocation.y(), WELL_DRAWING_Z_HEIGHT);
vtkTextProperty* textProperty = textActor->GetTextProperty();
textProperty->SetFontSize(15); // 调整字体大小,使其在 3D 视图中清晰可见
textProperty->SetColor(0.898, 0.898, 0.0);
textProperty->SetBold(true); // 文本加粗
//textActor->GetTextProperty()->SetJustificationToCentered(); // 文本中心对齐到其位置点
m_renderer->AddActor(textActor); // 对于所有 3D Actor都使用 m_renderer->AddActor()
m_mapWellNameActors.insert(sWellName, textActor); // 将 Actor 存储在 QMap 中,以便后续管理和清除
// === 2. 绘制井的线 (为每口井绘制,基于 wellLocations 的 X, Y并延伸到 WELL_DRAWING_Z_HEIGHT) ===
// 创建点集合
vtkSmartPointer<vtkPoints> points = vtkSmartPointer<vtkPoints>::New();
// 井口位置:使用 location.x() + 20.0, location.y() 和 WELL_DRAWING_Z_HEIGHT
points->InsertNextPoint(ptLocation.x() + 20.0, ptLocation.y(), WELL_DRAWING_Z_HEIGHT);
// 井底位置
points->InsertNextPoint(ptLocation.x(), ptLocation.y(), 0); // 向下延伸 WELL_DRAWING_Z_HEIGHT 个单位
// 创建线段拓扑
vtkSmartPointer<vtkCellArray> lines = vtkSmartPointer<vtkCellArray>::New();
if(points->GetNumberOfPoints() >= 2) {
lines->InsertNextCell(2); // 这条线只包含两个点
lines->InsertCellPoint(0); // 第一个点
lines->InsertCellPoint(1); // 第二个点
}
// 创建 PolyData 并设置点和线
vtkSmartPointer<vtkPolyData> polyData = vtkSmartPointer<vtkPolyData>::New();
polyData->SetPoints(points);
polyData->SetLines(lines);
// 创建 Mapper
vtkSmartPointer<vtkPolyDataMapper> lineMapper = vtkSmartPointer<vtkPolyDataMapper>::New();
lineMapper->SetInputData(polyData);
// 创建 Actor 并设置属性
vtkSmartPointer<vtkActor> lineActor = vtkSmartPointer<vtkActor>::New();
lineActor->SetMapper(lineMapper);
lineActor->GetProperty()->SetColor(0.647, 0.165, 0.165);
lineActor->GetProperty()->SetLineWidth(1.1);
m_renderer->AddActor(lineActor); // 将 3D Actor 添加到渲染器
m_mapWellLineActors.insert(sWellName, lineActor); // 存储井线 Actor
}
// 强制渲染以显示新增的 Actor
m_pVtkWidget->GetRenderWindow()->Render();
}
// 新增函数:清除所有井相关的 Actor
void nmWxPostprocessingAnimationWidget::clearWellActors()
{
// 移除所有井名称的 Actor
for(auto it = m_mapWellNameActors.constBegin(); it != m_mapWellNameActors.constEnd(); ++it) {
if(m_renderer && it.value()) {
m_renderer->RemoveActor(it.value()); // 对于 3D Actor 使用 RemoveActor
}
}
m_mapWellNameActors.clear(); // 清空 QMap释放智能指针持有的 VTK 对象
// 移除所有井线的 Actor
for(auto it = m_mapWellLineActors.constBegin(); it != m_mapWellLineActors.constEnd(); ++it) {
if(m_renderer && it.value()) {
m_renderer->RemoveActor(it.value()); // 对于 3D Actor 使用 RemoveActor
}
}
m_mapWellLineActors.clear(); // 清空 QMap释放智能指针持有的 VTK 对象
// 渲染更新以反映移除,确保屏幕刷新
if(m_pVtkWidget && m_pVtkWidget->GetRenderWindow()) {
m_pVtkWidget->GetRenderWindow()->Render();
}
}
void nmWxPostprocessingAnimationWidget::initOperPannel()
{
QHBoxLayout* operLayout = new QHBoxLayout; // 创建操作面板的水平布局
// 初始化进度条滑块
m_pSlider = new QSlider(Qt::Horizontal, this); // 创建水平滑块,并设置父对象
m_pSlider->setMinimum(1); // 滑块最小值设置为 1
m_pSlider->setMaximum(m_vecTimeStepKeys.size()); // 最大值是时间步的数量
m_pSlider->setValue(1); // 初始值设置为 1
m_pSlider->setTickInterval(1); // 刻度间隔为 1
m_pSlider->setTickPosition(QSlider::TicksBelow); // 刻度显示在滑块下方
// 连接滑块值改变信号到槽函数
connect(m_pSlider, SIGNAL(valueChanged(int)), this, SLOT(on_updateProgress(int)));
// 连接滑块的按下信号 (旧版连接方式)
connect(m_pSlider, SIGNAL(sliderPressed()), this, SLOT(on_sliderPressed()));
operLayout->addWidget(m_pSlider, 2); // 将滑块添加到布局,伸缩因子为 2
// 初始化进度文本标签
m_pProgress = new QLabel(this); // 创建标签,并设置父对象
int totalSteps = m_vecTimeStepKeys.size(); // 获取总时间步数
// 设置进度文本的初始值
if(totalSteps > 0) {
int iProgress = (int)(((m_nCurrentIndex + 1) * 100) / totalSteps);
m_pProgress->setText(QString("%1%(%2/%3)").arg(iProgress).arg(m_nCurrentIndex + 1).arg(totalSteps));
} else {
m_pProgress->setText("0%(0/0)"); // 如果没有数据,显示 0/0
}
operLayout->addWidget(m_pProgress, 1); // 将标签添加到布局,伸缩因子为 1
// 创建“播放”按钮
QPushButton* startAutoClickButton = new QPushButton(tr("play"), this); // 创建按钮
operLayout->addWidget(startAutoClickButton, 2); // 添加到布局,伸缩因子为 2
connect(startAutoClickButton, SIGNAL(clicked()), this, SLOT(on_start())); // 连接点击信号到 on_start 槽
// 创建“停止”按钮
QPushButton* stopAutoClickButton = new QPushButton(tr("stop"), this); // 创建按钮
operLayout->addWidget(stopAutoClickButton, 2); // 添加到布局,伸缩因子为 2
connect(stopAutoClickButton, SIGNAL(clicked()), this, SLOT(on_stop())); // 连接点击信号到 on_stop 槽
// 添加一个用于弹出数据过滤设置对话框的按钮
QPushButton* filterButton = new QPushButton(tr("Data Filtering"), this);
operLayout->addWidget(filterButton, 2);
// 连接按钮的点击信号到新的槽函数
connect(filterButton, SIGNAL(clicked()), this, SLOT(slotShowDataFilterDialog()));
// 用于启动绘制模式的按钮
QPushButton* drawButton = new QPushButton(tr("Draw Region"), this);
operLayout->addWidget(drawButton, 2);
connect(drawButton, SIGNAL(clicked()), this, SLOT(slotStartDrawPolygon()));
// 创建切换显示模式的下拉框
QComboBox* showModeSelector = new QComboBox(this); // 创建下拉框
showModeSelector->addItem(tr("face")); // 添加“面”模式
showModeSelector->addItem(tr("face+edge")); // 添加“面+边”模式
connect(showModeSelector, SIGNAL(currentIndexChanged(int)), this,
SLOT(on_modeChanged(int))); // 连接选中项改变信号
operLayout->addWidget(showModeSelector, 2); // 添加到布局,伸缩因子为 2
// 将操作面板的水平布局添加到主垂直布局中
m_pMainLayout->addLayout(operLayout);
// 初始化定时器
m_pPlayTimer = new QTimer(this); // 创建定时器,设置父对象
connect(m_pPlayTimer, SIGNAL(timeout()), this, SLOT(on_play())); // 连接定时器超时信号到 on_play 槽
m_pPlayTimer->setInterval(50); // 设置定时器间隔为 50 毫秒(即每 50 毫秒更新一帧)
}
// 初始化时间步列表,从 nmDataAnalyzeManager 单例获取数据
void nmWxPostprocessingAnimationWidget::initTimeSteps()
{
nmDataAnalyzeManager* pDataManager = nmDataAnalyzeManager::getCurrentInstance(); // 获取数据管理器单例实例
if(!pDataManager) {
return;
}
// 通过公共方法获取所有时间步的键(时间戳)列表
QList<double> keys = pDataManager->getTimeStepKeys();
// 对时间步进行排序,确保动画播放顺序是正确的(从小到大)
std::sort(keys.begin(), keys.end());
m_vecTimeStepKeys = QVector<double>::fromList(keys); // 将 QList 转换为 QVector 存储
}
// 根据指定索引加载并渲染一个时间步的数据
void nmWxPostprocessingAnimationWidget::loadDataForIndex(int index)
{
// 确保索引在有效范围内
if(index < 0 || index >= m_vecTimeStepKeys.size() || !m_pCachedBaseGrid) {
return;
}
nmDataAnalyzeManager* pDataManager = nmDataAnalyzeManager::getCurrentInstance(); // 获取数据管理器单例实例
if(!pDataManager) return;
// 2. 获取当前时间步对应的压力数据
double currentTime = m_vecTimeStepKeys[index]; // 获取当前时间步的时间戳
vtkSmartPointer<vtkDoubleArray> currentPressureData = pDataManager->getTimeStepData(
currentTime); // 获取对应的压力数据
if(!currentPressureData) return;
const char* arrayName = currentPressureData->GetName();
if (arrayName) {
qDebug() << "Loaded data array name is:" << arrayName;
} else {
qDebug() << "Loaded data array has no name.";
}
// 3. 将压力数据设置到PEBI单元数据中
m_pCachedBaseGrid->GetCellData()->SetScalars(currentPressureData);
m_thresholdFilter->SetInputArrayToProcess(0, 0, 0, vtkDataObject::FIELD_ASSOCIATION_CELLS, "p");
// 更新时间文本演员显示的时间戳
//m_textActor->SetInput(QString("Time step: %1").arg(currentTime, 0, 'f', 6).toStdString().c_str());
// 渲染更新后的场景
m_pVtkWidget->GetRenderWindow()->Render(); // 触发 QVTKWidget 的渲染窗口进行更新显示
}
// 槽函数:动画播放的逻辑,由定时器周期性触发
void nmWxPostprocessingAnimationWidget::on_play()
{
// 如果没有时间步数据,则停止播放并退出
if(m_vecTimeStepKeys.isEmpty()) {
on_stop(); // 停止播放
return;
}
// 如果当前索引已达到或超过总时间步数,则循环播放,将索引重置为 0
if(m_nCurrentIndex >= m_vecTimeStepKeys.size()) {
on_stop(); // 播放完毕,停止定时器
m_nCurrentIndex = 0; // 从头开始播放
}
// 加载并渲染当前帧的数据
loadDataForIndex(m_nCurrentIndex);
// 更新进度滑块的值 (滑块值从 1 开始,索引从 0 开始)
m_pSlider->setValue(m_nCurrentIndex + 1);
// 更新进度文本标签
int iProgress = (int)(((m_nCurrentIndex + 1) * 100) / m_vecTimeStepKeys.size());
m_pProgress->setText(QString("%1%(%2/%3)").arg(iProgress).arg(m_nCurrentIndex + 1).arg(m_vecTimeStepKeys.size()));
m_nCurrentIndex++; // 索引递增,准备播放下一帧
}
// 槽函数:点击“播放”按钮后触发,启动动画播放
void nmWxPostprocessingAnimationWidget::on_start()
{
// 如果没有时间步数据,无法开始播放
if(m_vecTimeStepKeys.isEmpty()) {
return;
}
m_bIsPlaying = true; // 设置播放状态为真
// 如果当前索引已达到或超过总时间步数,则从头开始播放
if(m_nCurrentIndex >= m_vecTimeStepKeys.size()) {
m_nCurrentIndex = 0;
}
// 确保滑块的最大值和当前值与时间步数量同步
m_pSlider->setMaximum(m_vecTimeStepKeys.size());
m_pSlider->setValue(m_nCurrentIndex + 1); // 设置滑块到当前帧的位置
m_pPlayTimer->start(); // 启动定时器,开始周期性调用 on_play()
}
// 槽函数:点击“停止”按钮后触发,停止动画播放
void nmWxPostprocessingAnimationWidget::on_stop()
{
m_pPlayTimer->stop(); // 停止定时器,停止周期性调用 on_play()
m_bIsPlaying = false; // 设置播放状态为假
}
// 槽函数:这个槽函数在您的代码中未被连接。
// 如果不再使用,可以从头文件和源文件中删除此函数。
void nmWxPostprocessingAnimationWidget::on_currentIndexChanged()
{
// 如果正在播放中,忽略此手动更改,避免冲突
if(m_bIsPlaying) {
return;
}
// (此处原代码中没有实际逻辑,若要使用应在此添加)
}
// 槽函数:监听显示模式(例如面模式、面+边模式)变化
void nmWxPostprocessingAnimationWidget::on_modeChanged(int newModeIndex)
{
// 如果新模式与当前模式相同,则不进行任何操作
if(m_nShowMode == newModeIndex) {
return;
}
m_nShowMode = newModeIndex; // 更新当前显示模式
// 根据选择的新模式更新演员Actor的显示属性
if(m_nShowMode == 0) { // 0 代表“面”模式
m_actor->GetProperty()->EdgeVisibilityOff(); // 关闭网格线的显示
m_actor->GetProperty()->SetRepresentationToSurface(); // 设置为表面渲染模式
} else { // 1 代表“面+边”模式
m_actor->GetProperty()->SetEdgeVisibility(true); // 开启网格线的显示
m_actor->GetProperty()->SetRepresentationToSurface(); // 仍然是表面渲染模式,只是显示边
}
// 渲染更新后的场景,使改变生效
m_pVtkWidget->GetRenderWindow()->Render(); // 触发 QVTKWidget 的渲染窗口进行更新显示
}
// 槽函数:监听进度滑块值变化时触发
void nmWxPostprocessingAnimationWidget::on_updateProgress(int value)
{
// 如果正在自动播放过程中,忽略滑块的手动拖动,防止冲突
if(m_bIsPlaying) {
return;
}
// 如果没有时间步数据,则不进行更新
if(m_vecTimeStepKeys.isEmpty()) {
return;
}
value--; // 滑块值从 1 开始 (m_pSlider->setMinimum(1)),但内部索引从 0 开始,所以需要减 1
// 根据滑块值更新当前时间步索引
m_nCurrentIndex = value;
// 加载并渲染当前索引对应的数据帧
loadDataForIndex(m_nCurrentIndex);
// 更新进度文本标签
int iProgress = (int)(((m_nCurrentIndex + 1) * 100) / m_vecTimeStepKeys.size());
m_pProgress->setText(QString("%1%(%2/%3)").arg(iProgress).arg(m_nCurrentIndex + 1).arg(m_vecTimeStepKeys.size()));
}
// 槽函数:监听进度滑块被按下(拖动开始)
void nmWxPostprocessingAnimationWidget::on_sliderPressed()
{
// 如果正在自动播放过程中,当滑块被手动按下时,停止自动播放
if(m_bIsPlaying) {
m_pPlayTimer->stop(); // 停止定时器
}
m_bIsPlaying = false; // 确保播放状态为停止,因为用户正在手动操作
}
// saveWidgetAsImage: 将 m_pVtkWidget 的内容保存到图像文件。
void nmWxPostprocessingAnimationWidget::saveWidgetAsImage()
{
// 检查 QVTKWidget 及其底层 VTK 渲染窗口是否已初始化。
if(!m_pVtkWidget || !m_pVtkWidget->GetRenderWindow()) {
return;
}
// 将 QVTKWidget 的当前内容抓取为 QPixmap。
QPixmap pixmap = QPixmap::grabWidget(m_pVtkWidget);
// 打开文件保存对话框,让用户选择保存位置和格式。
QString sFileName = QFileDialog::getSaveFileName(this,
tr("Save Image"), // 对话框标题
"", // 默认目录 (空字符串表示当前工作目录)
tr("PNG Image (*.png);;JPEG Image (*.jpg);;BMP Image (*.bmp)")); // 文件过滤器
// 如果用户提供了文件名 (没有取消对话框)。
if(!sFileName.isEmpty()) {
// 尝试将 pixmap 保存到选定的文件。
pixmap.save(sFileName);
}
}
// copyWidgetImage: 将 m_pVtkWidget 的内容复制到剪贴板。
void nmWxPostprocessingAnimationWidget::copyWidgetImage()
{
// 检查 QVTKWidget 及其底层 VTK 渲染窗口是否已初始化。
if(!m_pVtkWidget || !m_pVtkWidget->GetRenderWindow()) {
return;
}
// 将 QVTKWidget 的当前内容抓取为 QPixmap。
QPixmap pixmap = QPixmap::grabWidget(m_pVtkWidget);
// 获取全局剪贴板实例并设置抓取的 pixmap。
QClipboard *pClipboard = QApplication::clipboard();
pClipboard->setPixmap(pixmap);
}
// printWidget: 打印 m_pVtkWidget 的内容。
void nmWxPostprocessingAnimationWidget::printWidget()
{
// 检查 QVTKWidget 及其底层 VTK 渲染窗口是否已初始化。
if(!m_pVtkWidget || !m_pVtkWidget->GetRenderWindow()) {
return;
}
QPrinter printer; // 创建 QPrinter 对象。
QPrintDialog printDialog(&printer, this); // 创建用于打印设置的打印对话框。
// 如果用户接受打印对话框(点击“打印”)。
if(printDialog.exec() == QDialog::Accepted) {
// 调用 renderWidgetForPrint 槽函数,将打印逻辑统一。
// 这样,实际打印也会应用缩放和居中。
renderWidgetForPrint(&printer);
}
}
// printPreviewWidget: 显示 m_pVtkWidget 的打印预览。
void nmWxPostprocessingAnimationWidget::printPreviewWidget()
{
// 检查 QVTKWidget 及其底层 VTK 渲染窗口是否已初始化。
if(!m_pVtkWidget || !m_pVtkWidget->GetRenderWindow()) {
return;
}
QPrinter printer(QPrinter::HighResolution); // 创建高分辨率打印机以获得更好的预览质量。
QPrintPreviewDialog preview(&printer, this); // 创建打印预览对话框。
preview.resize(800, 600); // 设置预览窗体大小
// 将预览对话框的 paintRequested 信号连接到我们的自定义渲染槽。
// 当预览对话框需要绘制页面时,将调用此槽。
connect(&preview, SIGNAL(paintRequested(QPrinter*)),
this, SLOT(renderWidgetForPrint(QPrinter*)));
preview.exec(); // 显示打印预览对话框。
}
// renderWidgetForPrint: 辅助槽函数,用于渲染 m_pVtkWidget 的内容以进行打印/预览。
void nmWxPostprocessingAnimationWidget::renderWidgetForPrint(QPrinter *printer)
{
// 调用辅助函数获取包含VTK渲染内容的QImage
QImage image = getVTKRenderWindowAsImage();
if(image.isNull()) {
return;
}
QPainter painter(printer); // 创建与给定打印机关联的 QPainter。
// 计算缩放因子,以按比例将部件内容适应到打印页面。
// 这里使用捕获到的QImage的尺寸而不是QVTKWidget的尺寸因为QImage已经是实际渲染内容的准确捕获。
double xscale = printer->pageRect().width() / (double)image.width();
double yscale = printer->pageRect().height() / (double)image.height();
// 使用较小的缩放因子,以确保整个内容完全适应页面内,不会被裁剪。
double scale = qMin(xscale, yscale);
painter.scale(scale, scale); // 然后应用缩放变换
// 将捕获到的 QImage 绘制到打印机上。
// 这样VTK渲染的所有内容包括渐变背景都会被完整地打印出来。
painter.drawImage(0, 0, image);
}
QImage nmWxPostprocessingAnimationWidget::getVTKRenderWindowAsImage()
{
// 检查 QVTKWidget 及其底层的 VTK 渲染窗口是否已初始化。
if(!m_pVtkWidget || !m_pVtkWidget->GetRenderWindow()) {
qWarning() << "QVTKWidget or RenderWindow is not initialized.";
return QImage(); // 返回空图像
}
// 获取 VTK 渲染窗口的指针
vtkRenderWindow* pRenderWindow = m_pVtkWidget->GetRenderWindow();
// 确保在捕获前渲染最新帧
pRenderWindow->Render();
// 使用 vtkWindowToImageFilter 将渲染窗口的内容转换为 VTK 图像数据
vtkSmartPointer<vtkWindowToImageFilter> windowToImageFilter =
vtkSmartPointer<vtkWindowToImageFilter>::New();
windowToImageFilter->SetInput(pRenderWindow);
// 明确设置为 RGB
windowToImageFilter->SetInputBufferTypeToRGB();
// 从后台缓冲区读取以确保完整渲染
windowToImageFilter->ReadFrontBufferOff();
windowToImageFilter->Update(); // 执行图像转换
// 获取 VTK 图像数据 (未经翻转)
vtkImageData* pImageData = windowToImageFilter->GetOutput();
// VTK 图像数据的原点在左下角,而 Qt 的 QImage 原点在左上角,需要垂直翻转
vtkSmartPointer<vtkImageFlip> flip = vtkSmartPointer<vtkImageFlip>::New();
flip->SetInputData(pImageData);
//flip->SetInputConnection(windowToImageFilter->GetOutputPort()); // 用这个方法翻转后图像显示的是黑色的!!!
flip->SetFilteredAxis(1); // 翻转 Y 轴(垂直翻转)
flip->Update(); // 翻转操作在这里执行
// 获取翻转后的 VTK 图像数据
vtkImageData* pFlippedImageData = flip->GetOutput(); // 获取翻转后的数据
return createQImage(pFlippedImageData);
}
void nmWxPostprocessingAnimationWidget::exportAnimationAsVideo()
{
// 检查动画数据和渲染窗口是否可用,如果不可用则弹出警告并退出。
if(m_vecTimeStepKeys.isEmpty() || !m_pVtkWidget || !m_pVtkWidget->GetRenderWindow()) {
QMessageBox::warning(this, tr("Warning"), tr("No animation data or render window available."));
return;
}
// 弹出文件保存对话框,让用户选择视频保存的路径和文件名。
// 这里列出了多种常见的视频格式作为过滤器,方便用户选择。
QString sFileName = QFileDialog::getSaveFileName(
this,
tr("Save Animation as Video"),
"",
tr("MP4 Video (*.mp4);;" // 最常用、兼容性最好的视频格式,几乎所有设备和软件都支持。
"AVI Video (*.avi);;" // 一种较老但兼容性好的格式文件体积通常比MP4大。
"QuickTime Video (*.mov);;" // 苹果公司开发的视频格式在Apple生态中应用广泛。
"MPEG Video (*.mpg);;" // 通用的视频文件格式通常指MPEG-1或MPEG-2编码。
"WebM Video (*.webm);;" // 一种现代、开放、免版税的视频格式由Google推出常用于网页视频。
"Matroska Video (*.mkv);;" // 一种强大的“容器”格式,可以包含多种视频、音频和字幕轨道,但并非所有播放器都原生支持。
"Windows Media Video (*.wmv);;" // 微软开发的视频格式在Windows平台常用。
"Flash Video (*.flv);;" // 一种曾广泛用于网页视频的格式现在已逐渐被HTML5技术取代。
"All Files (*.*)")); // “所有文件”选项,允许用户选择或输入任何文件扩展名。
// 如果用户取消了保存对话框,则直接返回。
if(sFileName.isEmpty()) {
return;
}
// 记录动画当前的播放状态,以便在导出完成后恢复。
bool wasPlaying = m_bIsPlaying;
// 停止正在进行的动画播放,避免与导出过程冲突。
on_stop();
// 1. 创建离屏渲染管线
// 创建一个VTK渲染窗口但设置为离屏渲染这意味着它不会在屏幕上显示。
vtkSmartPointer<vtkRenderWindow> offscreenRenderWindow = vtkSmartPointer<vtkRenderWindow>::New();
offscreenRenderWindow->OffScreenRenderingOn();
// 设置离屏渲染窗口的尺寸为 1920x1080即 1080p以生成高分辨率视频。
offscreenRenderWindow->SetSize(1920, 1080);
// 创建一个VTK渲染器。
vtkSmartPointer<vtkRenderer> offscreenRenderer = vtkSmartPointer<vtkRenderer>::New();
// 将渲染器添加到离屏渲染窗口中。
offscreenRenderWindow->AddRenderer(offscreenRenderer);
// 2. 深度复制所有渲染内容 (兼容 VTK 7.1)
// 复制相机:从主渲染器的相机深度复制到离屏渲染器,确保视角一致。
vtkSmartPointer<vtkCamera> camera = vtkSmartPointer<vtkCamera>::New();
camera->DeepCopy(m_renderer->GetActiveCamera());
offscreenRenderer->SetActiveCamera(camera);
// 复制渲染器背景颜色和渐变设置:将主渲染器的背景属性复制到离屏渲染器。
double background1[3], background2[3];
m_renderer->GetBackground(background1);
m_renderer->GetBackground2(background2);
offscreenRenderer->SetBackground(background1);
offscreenRenderer->SetBackground2(background2);
offscreenRenderer->SetGradientBackground(m_renderer->GetGradientBackground());
// 复制基础数据对象:深度复制网格数据,确保离屏渲染使用独立的数据副本。
vtkSmartPointer<vtkUnstructuredGrid> offscreenGrid = vtkSmartPointer<vtkUnstructuredGrid>::New();
if(m_pCachedBaseGrid) {
offscreenGrid->DeepCopy(m_pCachedBaseGrid);
}
// 复制主 Actor 的 Mapper、Actor 和 ScalarBar (手动复制属性)
// 复制 Mapper创建一个新的Mapper并设置其输入数据和颜色映射表等属性。
vtkSmartPointer<vtkDataSetMapper> offscreenMapper = vtkSmartPointer<vtkDataSetMapper>::New();
if(m_mapper) {
offscreenMapper->SetInputData(offscreenGrid);
offscreenMapper->SetLookupTable(m_mapper->GetLookupTable());
offscreenMapper->ScalarVisibilityOn();
offscreenMapper->SetScalarRange(m_mapper->GetScalarRange());
}
// 复制 Actor创建一个新的Actor并设置其Mapper同时深度复制其属性。
vtkSmartPointer<vtkActor> offscreenActor = vtkSmartPointer<vtkActor>::New();
if(m_actor) {
offscreenActor->SetMapper(offscreenMapper);
// 手动复制 vtkProperty
vtkSmartPointer<vtkProperty> prop = vtkSmartPointer<vtkProperty>::New();
prop->DeepCopy(m_actor->GetProperty());
offscreenActor->SetProperty(prop);
}
// 复制颜色条:创建新的颜色条,并复制所有属性,包括位置、尺寸和字体。
vtkSmartPointer<vtkScalarBarActor> offscreenScalarBar = vtkSmartPointer<vtkScalarBarActor>::New();
if(m_scalarBar) {
offscreenScalarBar->SetLookupTable(m_scalarBar->GetLookupTable());
offscreenScalarBar->SetTitle(m_scalarBar->GetTitle());
offscreenScalarBar->SetPosition(m_scalarBar->GetPosition());
offscreenScalarBar->SetWidth(m_scalarBar->GetWidth());
offscreenScalarBar->SetHeight(m_scalarBar->GetHeight());
// 手动复制 TitleTextProperty 和 LabelTextProperty
vtkSmartPointer<vtkTextProperty> titleTextProp = vtkSmartPointer<vtkTextProperty>::New();
vtkTextProperty* sourceTitleTextProp = m_scalarBar->GetTitleTextProperty();
titleTextProp->SetFontFamily(sourceTitleTextProp->GetFontFamily());
titleTextProp->SetFontSize(sourceTitleTextProp->GetFontSize());
titleTextProp->SetColor(sourceTitleTextProp->GetColor());
offscreenScalarBar->SetTitleTextProperty(titleTextProp);
vtkSmartPointer<vtkTextProperty> labelTextProp = vtkSmartPointer<vtkTextProperty>::New();
vtkTextProperty* sourceLabelTextProp = m_scalarBar->GetLabelTextProperty();
labelTextProp->SetFontFamily(sourceLabelTextProp->GetFontFamily());
labelTextProp->SetFontSize(sourceLabelTextProp->GetFontSize());
labelTextProp->SetColor(sourceLabelTextProp->GetColor());
offscreenScalarBar->SetLabelTextProperty(labelTextProp);
}
// 复制井名和井线的 Actor
QMap<QString, vtkSmartPointer<vtkBillboardTextActor3D>> offscreenWellNameActors;
QMapIterator<QString, vtkSmartPointer<vtkBillboardTextActor3D>> wellNameIter(m_mapWellNameActors);
while(wellNameIter.hasNext()) {
wellNameIter.next();
vtkSmartPointer<vtkBillboardTextActor3D> originalActor = wellNameIter.value();
vtkSmartPointer<vtkBillboardTextActor3D> newActor = vtkSmartPointer<vtkBillboardTextActor3D>::New();
newActor->SetInput(originalActor->GetInput());
newActor->SetPosition(originalActor->GetPosition());
// 手动复制文本属性
vtkSmartPointer<vtkTextProperty> textProp = vtkSmartPointer<vtkTextProperty>::New();
vtkTextProperty* sourceTextProp = originalActor->GetTextProperty();
textProp->SetFontFamily(sourceTextProp->GetFontFamily());
textProp->SetFontSize(sourceTextProp->GetFontSize());
textProp->SetColor(sourceTextProp->GetColor());
textProp->SetBold(sourceTextProp->GetBold());
newActor->SetTextProperty(textProp);
offscreenWellNameActors.insert(wellNameIter.key(), newActor);
}
QMap<QString, vtkSmartPointer<vtkActor>> offscreenWellLineActors;
QMapIterator<QString, vtkSmartPointer<vtkActor>> wellLineIter(m_mapWellLineActors);
while(wellLineIter.hasNext()) {
wellLineIter.next();
vtkSmartPointer<vtkActor> originalActor = wellLineIter.value();
vtkSmartPointer<vtkActor> newActor = vtkSmartPointer<vtkActor>::New();
// 复制 Mapper
vtkSmartPointer<vtkPolyDataMapper> originalMapper = vtkPolyDataMapper::SafeDownCast(originalActor->GetMapper());
if(originalMapper) {
vtkSmartPointer<vtkPolyDataMapper> newMapper = vtkSmartPointer<vtkPolyDataMapper>::New();
newMapper->SetInputData(originalMapper->GetInput()); // 井线几何数据通常是静态的,可以共享
newActor->SetMapper(newMapper);
}
// 复制 Property
vtkSmartPointer<vtkProperty> newProp = vtkSmartPointer<vtkProperty>::New();
newProp->DeepCopy(originalActor->GetProperty());
newActor->SetProperty(newProp);
offscreenWellLineActors.insert(wellLineIter.key(), newActor);
}
// 将所有复制的 Actor 添加到离屏渲染器
offscreenRenderer->AddActor(offscreenActor);
offscreenRenderer->AddActor(offscreenScalarBar);
QMapIterator<QString, vtkSmartPointer<vtkBillboardTextActor3D>> offscreenNameIter(offscreenWellNameActors);
while(offscreenNameIter.hasNext()) {
offscreenNameIter.next();
offscreenRenderer->AddActor(offscreenNameIter.value());
}
QMapIterator<QString, vtkSmartPointer<vtkActor>> offscreenLineIter(offscreenWellLineActors);
while(offscreenLineIter.hasNext()) {
offscreenLineIter.next();
offscreenRenderer->AddActor(offscreenLineIter.value());
}
// 3. 配置视频写入器和进度条
// 创建FFmpeg视频写入器。
vtkSmartPointer<vtkFFMPEGWriter> pVideoWriter = vtkSmartPointer<vtkFFMPEGWriter>::New();
// 创建一个滤镜用于将VTK渲染窗口的内容转换为图像数据。
vtkSmartPointer<vtkWindowToImageFilter> pWindowToImageFilter = vtkSmartPointer<vtkWindowToImageFilter>::New();
pWindowToImageFilter->SetInput(offscreenRenderWindow);
pWindowToImageFilter->SetInputBufferTypeToRGB();
pWindowToImageFilter->ReadFrontBufferOff();
// 将滤镜的输出连接到视频写入器的输入。
pVideoWriter->SetInputConnection(pWindowToImageFilter->GetOutputPort());
// 设置输出文件名。
pVideoWriter->SetFileName(sFileName.toStdString().c_str());
// 设置视频帧率每秒10帧。
pVideoWriter->SetRate(10);
// 设置视频质量0-50是最高质量
pVideoWriter->SetQuality(2);
// 启动视频写入过程。
pVideoWriter->Start();
// 创建进度对话框,为用户提供导出进度反馈。
QProgressDialog progressDialog(tr("Exporting Video..."), tr("Cancel"), 0, m_vecTimeStepKeys.size(), this);
progressDialog.setWindowTitle(tr("Video Export"));
progressDialog.setWindowModality(Qt::WindowModal);
progressDialog.setMinimumDuration(0);
// 强制显示对话框并处理事件
progressDialog.show();
QApplication::processEvents(); // 确保对话框在进入循环前被渲染
// 4. 遍历所有时间步,更新离屏数据,渲染并写入每一帧
for(int i = 0; i < m_vecTimeStepKeys.size(); ++i) {
// 如果用户点击了“取消”,则停止导出。
if(progressDialog.wasCanceled()) {
pVideoWriter->End();
if(wasPlaying) on_start();
QMessageBox::information(this, tr("Cancelled"), tr("Video export was cancelled by the user."));
return;
}
// 获取数据管理器实例。
nmDataAnalyzeManager* pDataManager = nmDataAnalyzeManager::getCurrentInstance();
if(pDataManager) {
// 获取当前时间步的数据。
double currentTime = m_vecTimeStepKeys[i];
vtkSmartPointer<vtkDoubleArray> currentPressureData = pDataManager->getTimeStepData(currentTime);
if(currentPressureData) {
// 将数据设置到离屏网格中,并标记数据已修改。
vtkDataSet* dataSet = vtkDataSet::SafeDownCast(offscreenGrid);
if(dataSet) {
dataSet->GetCellData()->SetScalars(currentPressureData);
dataSet->Modified();
}
}
}
// 渲染离屏窗口。
offscreenRenderWindow->Render();
// 通知滤镜数据已更新,需要重新处理。
pWindowToImageFilter->Modified();
// 将渲染结果写入视频文件。
pVideoWriter->Write();
// 更新进度对话框的进度条。
progressDialog.setValue(i + 1);
// 处理Qt事件保持界面响应。
QApplication::processEvents();
}
// 5. 导出结束
pVideoWriter->End();
// 6. 恢复动画播放状态
// 如果导出前动画正在播放,则重新启动动画。
if(wasPlaying) {
on_start();
}
// 弹出成功消息框。
QMessageBox::information(this, tr("Success"), tr("Video exported successfully to %1.").arg(sFileName));
}
void nmWxPostprocessingAnimationWidget::slotShowDataFilterDialog()
{
nmWxPostprocessingAnimationDataFiltering dlg;
// 在显示对话框前,设置其初始值
dlg.setFilterSettings(m_bFilteringEnabled,
m_bAboveMinEnabled, m_dMinValue,
m_bBelowMaxEnabled, m_dMaxValue);
// 将对话框的自定义信号连接到本类的槽函数
connect(&dlg, SIGNAL(applySettingsRequested(bool, bool, double, bool, double)),
this, SLOT(slotApplyFilter(bool, bool, double, bool, double)));
// 运行对话框,它会阻塞直到用户点击 OK 或 Cancel
if (dlg.exec() == QDialog::Accepted) {
// 用户点击了 OK获取对话框中的值
bool bEnableFiltering = dlg.isFilteringEnabled();
bool bEnableAboveMin = dlg.isAboveMinEnabled();
double minVal = dlg.getMinValue();
bool bEnableBelowMax = dlg.isBelowMaxEnabled();
double maxVal = dlg.getMaxValue();
// 调用槽函数应用过滤
slotApplyFilter(bEnableFiltering, bEnableAboveMin, minVal, bEnableBelowMax, maxVal);
}
}
void nmWxPostprocessingAnimationWidget::slotApplyFilter(bool bEnableFiltering,
bool bEnableAboveMin, double minVal,
bool bEnableBelowMax, double maxVal)
{
if (!m_thresholdFilter) {
qWarning() << "vtkThreshold filter is not initialized.";
return;
}
// 更新成员变量来记录当前的过滤设置
m_bFilteringEnabled = bEnableFiltering;
m_bAboveMinEnabled = bEnableAboveMin;
m_dMinValue = minVal;
m_bBelowMaxEnabled = bEnableBelowMax;
m_dMaxValue = maxVal;
if (bEnableFiltering) {
// 如果启用过滤
// 阈值过滤的组合逻辑
if (bEnableAboveMin && bEnableBelowMax) {
// 设置一个完整的范围
m_thresholdFilter->ThresholdBetween(minVal, maxVal);
} else if (bEnableAboveMin) {
// 只设置下限
m_thresholdFilter->ThresholdByUpper(minVal); // 这里的 lower 在 VTK API 中实际被视为 upper
// 或者,为了更清晰,可以使用 VTK_DOUBLE_MAX 作为上限
m_thresholdFilter->ThresholdBetween(minVal, VTK_DOUBLE_MAX);
} else if (bEnableBelowMax) {
// 只设置上限
m_thresholdFilter->ThresholdByLower(maxVal); // 这里的 upper 在 VTK API 中实际被视为 lower
// 或者,为了更清晰,可以使用 -VTK_DOUBLE_MAX 作为下限
m_thresholdFilter->ThresholdBetween(-VTK_DOUBLE_MAX, maxVal);
} else {
// 如果都未选中,则相当于不进行阈值过滤
// 此时可以显示全部数据
m_thresholdFilter->ThresholdBetween(-VTK_DOUBLE_MAX, VTK_DOUBLE_MAX);
}
// 关键:告诉 VTK 过滤器已经被修改,需要重新执行
m_thresholdFilter->Modified();
} else {
// 如果没有启用过滤
// 恢复到显示所有网格单元
m_thresholdFilter->ThresholdBetween(-VTK_DOUBLE_MAX, VTK_DOUBLE_MAX);
m_thresholdFilter->Modified();
}
m_pVtkWidget->GetRenderWindow()->Render();
}
void nmWxPostprocessingAnimationWidget::createContourWidget()
{
// 如果之前已存在,先清理(可选,但安全)
if (m_contourWidget) {
m_contourWidget->EnabledOff();
m_contourWidget->RemoveAllObservers();
}
// 1. 创建新的 ContourWidget
m_contourWidget = vtkSmartPointer<vtkContourWidget>::New();
m_contourWidget->SetInteractor(m_interactor);
// 2. 创建新的 Representation
m_contourRepresentation = vtkSmartPointer<vtkOrientedGlyphContourRepresentation>::New();
m_contourWidget->SetRepresentation(m_contourRepresentation);
// 3. 获取当前帧裁剪后的数据(阈值过滤器输出)
vtkSmartPointer<vtkGeometryFilter> geometry =
vtkSmartPointer<vtkGeometryFilter>::New();
geometry->SetInputConnection(m_thresholdFilter->GetOutputPort());
geometry->Update();
// 计算法线
vtkSmartPointer<vtkPolyDataNormals> normals =
vtkSmartPointer<vtkPolyDataNormals>::New();
normals->SetInputConnection(geometry->GetOutputPort());
normals->ComputePointNormalsOn(); // 关键点
normals->Update();
m_polyDataForContour = normals->GetOutput();
// 4. 设置 PointPlacer使用已有网格
m_pointPlacer = vtkSmartPointer<vtkPolygonalSurfacePointPlacer>::New();
m_pointPlacer->GetPolys()->AddItem(m_polyDataForContour);
m_pointPlacer->AddProp(m_actor);
m_contourRepresentation->SetPointPlacer(m_pointPlacer);
// 5. 设置样式
m_contourRepresentation->GetProperty()->SetColor(1.0, 1.0, 1.0);
m_contourRepresentation->GetProperty()->SetPointSize(8.0);
m_contourRepresentation->GetLinesProperty()->SetColor(1.0, 1.0, 1.0);
m_contourRepresentation->GetLinesProperty()->SetLineWidth(3.0);
m_contourRepresentation->SetRenderer(m_renderer);
// 6. 设置插值器为空,轮廓线为直线
m_contourRepresentation->SetLineInterpolator(nullptr);
// 7. 注册回调
m_contourWidget->AddObserver(vtkCommand::EndInteractionEvent, this,
&nmWxPostprocessingAnimationWidget::slotContourFinished);
}
void nmWxPostprocessingAnimationWidget::slotStartDrawPolygon() {
if (m_bIsDrawingMode) {
// 当前正在绘制,点击按钮表示“退出绘制”
slotClearContour();
return;
}
// 进入绘制模式
createContourWidget(); // 重新创建并初始化
m_contourWidget->EnabledOn();
m_bIsDrawingMode = true;
// 可见性如果为不可见,则重新设置
if (m_contourRepresentation->GetVisibility() == 0)
{
m_contourRepresentation->SetVisibility(1);
}
m_pVtkWidget->GetRenderWindow()->Render();
}
void nmWxPostprocessingAnimationWidget::slotContourFinished() {
// 1. 提取 contour 节点
vtkSmartPointer<vtkPoints> pts = vtkSmartPointer<vtkPoints>::New();
const int nNodes = m_contourRepresentation->GetNumberOfNodes();
for (int i = 0; i < nNodes; ++i) {
double p[3];
m_contourRepresentation->GetNthNodeWorldPosition(i, p);
p[2] = 0.0; // 强制投影到 Z=0
pts->InsertNextPoint(p);
}
// 保证首尾闭合
//double first[3], last[3];
//pts->GetPoint(0, first);
//pts->GetPoint(pts->GetNumberOfPoints() - 1, last);
//if (vtkMath::Distance2BetweenPoints(first, last) > 1e-12)
// pts->InsertNextPoint(first);
// 2. 2D 隐式选择环
vtkSmartPointer<vtkImplicitSelectionLoop> loop = vtkSmartPointer<vtkImplicitSelectionLoop>::New();
loop->SetLoop(pts);
// 3. 提取落在环内(含边界)的网格单元
vtkSmartPointer<vtkExtractGeometry> extract = vtkSmartPointer<vtkExtractGeometry>::New();
extract->SetInputConnection(m_thresholdFilter->GetOutputPort());
extract->SetImplicitFunction(loop);
extract->ExtractInsideOn();
extract->ExtractBoundaryCellsOn();
extract->Update();
vtkUnstructuredGrid* enclosedCellsGrid = extract->GetOutput();
// 4. 计算平均压力
double avgPressure;
int enclosedCellCount;
vtkDataArray* pArray = enclosedCellsGrid->GetCellData()->GetArray("p");
if (!pArray)
{
qDebug() << "No 'p' array in cell data.";
return;
}
double total = 0.0;
enclosedCellCount = enclosedCellsGrid->GetNumberOfCells();
for (vtkIdType i = 0; i < enclosedCellCount; ++i)
total += pArray->GetTuple1(i);
QString message;
if (enclosedCellCount > 0) {
avgPressure = total / enclosedCellCount;
message = QString(tr("Total cells in contour: %1\nAverage pressure in contour: %2 MPa"))
.arg(enclosedCellCount)
.arg(avgPressure, 0, 'f', 4);
} else {
message = tr("No cells were found within the defined contour.");
}
// 调用非模态对话框显示结果
m_resultDialog->showResult(message);
}
bool nmWxPostprocessingAnimationWidget::isDrawingMode() const
{
return m_bIsDrawingMode;
}
vtkRenderer* nmWxPostprocessingAnimationWidget::getRenderer() const
{
return m_renderer;
}
QVTKWidget* nmWxPostprocessingAnimationWidget::getVtkWidget() const
{
return m_pVtkWidget;
}
void nmWxPostprocessingAnimationWidget::addContourPoint(double pos[3])
{
if (!m_contourRepresentation) {
qDebug() << "Error: Contour representation is null.";
return;
}
double offsetPos[3] = {pos[0], pos[1], pos[2]};
int oldNodeCount = m_contourRepresentation->GetNumberOfNodes();
// 尝试添加一个新节点
m_contourRepresentation->AddNodeAtWorldPosition(offsetPos);
int newNodeCount = m_contourRepresentation->GetNumberOfNodes();
if (newNodeCount > oldNodeCount) {
// 强制渲染更新
m_contourRepresentation->Modified();
m_contourWidget->Render();
m_pVtkWidget->GetRenderWindow()->Render();
} else {
qDebug() << "Warning: Node was not added. PointPlacer might be rejecting the position.";
}
}
vtkContourWidget* nmWxPostprocessingAnimationWidget::getContourWidget() const {
return m_contourWidget;
}
void nmWxPostprocessingAnimationWidget::slotClearContour() {
if (m_contourWidget) {
m_contourWidget->EnabledOff();
m_contourWidget = nullptr; // 让智能指针自动释放
}
m_bIsDrawingMode = false;
if (m_resultDialog) {
m_resultDialog->hide();
}
m_pVtkWidget->GetRenderWindow()->Render();
}