#include "nmWxPostprocessingAnimationWidget.h" #include // 用于调试输出 #include // 用于 std::sort 排序算法 // UI 相关的 Qt 头文件 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // VTK 相关的头文件 #include "QVTKWidget.h" // QVTKWidget 的实际头文件路径 #include #include #include #include // 如果有文件读取操作,需要此头文件 #include // 非结构化网格数据类型 #include #include // 点数据 #include #include // 演员属性,如颜色、线宽等 #include #include #include #include // 文本演员属性,如字体、大小、颜色 #include // 双精度浮点数数组 #include // 单元格数据 #include #include #include #include #include // === 新增的 3D 绘制相关的 VTK 头文件 === #include // 广告牌文本演员 (始终面向相机,适用于标签) #include // 多边形数据 (用于线、面等几何体) #include // 点集合 #include // 单元格数组 (用于定义线的拓扑结构) #include // 相机对象,用于获取相机位置等信息 #include // 坐标系统转换,如果使用 vtkTextActor 且要精确放置 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // 业务逻辑相关的头文件 #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 picker = vtkSmartPointer::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) : QDialog(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(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::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::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::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::New(); m_mapper->ScalarVisibilityOn(); // 开启标量数据显示 m_mapper->SetLookupTable(m_lookupTable); m_mapper->SetInputConnection(m_thresholdFilter->GetOutputPort()); // **将 mapper 的输入连接到阈值过滤器的输出** // 6. 创建actor并设置mapper m_actor = vtkSmartPointer::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::New(); m_scalarBar->SetLookupTable(m_lookupTable); // 设置颜色查找表 m_scalarBar->SetTitle("p MPa"); // 设置颜色条标题 // 8. 设置颜色条的标签文本字体属性 (保持不变或根据需要调整) vtkSmartPointer labelTextProp = vtkSmartPointer::New(); labelTextProp->SetFontFamilyToArial(); labelTextProp->SetFontSize(12); // 标签字体大小 m_scalarBar->SetLabelTextProperty(labelTextProp); // 设置标签文本属性 // 设置颜色条的标题文本字体属性 (单独设置,使其更大) vtkSmartPointer titleTextProp = vtkSmartPointer::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::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::New(); // m_trackballStyle->SetParent(this); //m_trackballStyle = vtkSmartPointer::New(); m_drawStyle = vtkSmartPointer::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 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 textActor = vtkSmartPointer::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 points = vtkSmartPointer::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 lines = vtkSmartPointer::New(); if(points->GetNumberOfPoints() >= 2) { lines->InsertNextCell(2); // 这条线只包含两个点 lines->InsertCellPoint(0); // 第一个点 lines->InsertCellPoint(1); // 第二个点 } // 创建 PolyData 并设置点和线 vtkSmartPointer polyData = vtkSmartPointer::New(); polyData->SetPoints(points); polyData->SetLines(lines); // 创建 Mapper vtkSmartPointer lineMapper = vtkSmartPointer::New(); lineMapper->SetInputData(polyData); // 创建 Actor 并设置属性 vtkSmartPointer lineActor = vtkSmartPointer::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 keys = pDataManager->getTimeStepKeys(); // 对时间步进行排序,确保动画播放顺序是正确的(从小到大) std::sort(keys.begin(), keys.end()); m_vecTimeStepKeys = QVector::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 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 windowToImageFilter = vtkSmartPointer::New(); windowToImageFilter->SetInput(pRenderWindow); // 明确设置为 RGB windowToImageFilter->SetInputBufferTypeToRGB(); // 从后台缓冲区读取以确保完整渲染 windowToImageFilter->ReadFrontBufferOff(); windowToImageFilter->Update(); // 执行图像转换 // 获取 VTK 图像数据 (未经翻转) vtkImageData* pImageData = windowToImageFilter->GetOutput(); // VTK 图像数据的原点在左下角,而 Qt 的 QImage 原点在左上角,需要垂直翻转 vtkSmartPointer flip = vtkSmartPointer::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 offscreenRenderWindow = vtkSmartPointer::New(); offscreenRenderWindow->OffScreenRenderingOn(); // 设置离屏渲染窗口的尺寸为 1920x1080,即 1080p,以生成高分辨率视频。 offscreenRenderWindow->SetSize(1920, 1080); // 创建一个VTK渲染器。 vtkSmartPointer offscreenRenderer = vtkSmartPointer::New(); // 将渲染器添加到离屏渲染窗口中。 offscreenRenderWindow->AddRenderer(offscreenRenderer); // 2. 深度复制所有渲染内容 (兼容 VTK 7.1) // 复制相机:从主渲染器的相机深度复制到离屏渲染器,确保视角一致。 vtkSmartPointer camera = vtkSmartPointer::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 offscreenGrid = vtkSmartPointer::New(); if(m_pCachedBaseGrid) { offscreenGrid->DeepCopy(m_pCachedBaseGrid); } // 复制主 Actor 的 Mapper、Actor 和 ScalarBar (手动复制属性) // 复制 Mapper:创建一个新的Mapper,并设置其输入数据和颜色映射表等属性。 vtkSmartPointer offscreenMapper = vtkSmartPointer::New(); if(m_mapper) { offscreenMapper->SetInputData(offscreenGrid); offscreenMapper->SetLookupTable(m_mapper->GetLookupTable()); offscreenMapper->ScalarVisibilityOn(); offscreenMapper->SetScalarRange(m_mapper->GetScalarRange()); } // 复制 Actor:创建一个新的Actor,并设置其Mapper,同时深度复制其属性。 vtkSmartPointer offscreenActor = vtkSmartPointer::New(); if(m_actor) { offscreenActor->SetMapper(offscreenMapper); // 手动复制 vtkProperty vtkSmartPointer prop = vtkSmartPointer::New(); prop->DeepCopy(m_actor->GetProperty()); offscreenActor->SetProperty(prop); } // 复制颜色条:创建新的颜色条,并复制所有属性,包括位置、尺寸和字体。 vtkSmartPointer offscreenScalarBar = vtkSmartPointer::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 titleTextProp = vtkSmartPointer::New(); vtkTextProperty* sourceTitleTextProp = m_scalarBar->GetTitleTextProperty(); titleTextProp->SetFontFamily(sourceTitleTextProp->GetFontFamily()); titleTextProp->SetFontSize(sourceTitleTextProp->GetFontSize()); titleTextProp->SetColor(sourceTitleTextProp->GetColor()); offscreenScalarBar->SetTitleTextProperty(titleTextProp); vtkSmartPointer labelTextProp = vtkSmartPointer::New(); vtkTextProperty* sourceLabelTextProp = m_scalarBar->GetLabelTextProperty(); labelTextProp->SetFontFamily(sourceLabelTextProp->GetFontFamily()); labelTextProp->SetFontSize(sourceLabelTextProp->GetFontSize()); labelTextProp->SetColor(sourceLabelTextProp->GetColor()); offscreenScalarBar->SetLabelTextProperty(labelTextProp); } // 复制井名和井线的 Actor QMap> offscreenWellNameActors; QMapIterator> wellNameIter(m_mapWellNameActors); while(wellNameIter.hasNext()) { wellNameIter.next(); vtkSmartPointer originalActor = wellNameIter.value(); vtkSmartPointer newActor = vtkSmartPointer::New(); newActor->SetInput(originalActor->GetInput()); newActor->SetPosition(originalActor->GetPosition()); // 手动复制文本属性 vtkSmartPointer textProp = vtkSmartPointer::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> offscreenWellLineActors; QMapIterator> wellLineIter(m_mapWellLineActors); while(wellLineIter.hasNext()) { wellLineIter.next(); vtkSmartPointer originalActor = wellLineIter.value(); vtkSmartPointer newActor = vtkSmartPointer::New(); // 复制 Mapper vtkSmartPointer originalMapper = vtkPolyDataMapper::SafeDownCast(originalActor->GetMapper()); if(originalMapper) { vtkSmartPointer newMapper = vtkSmartPointer::New(); newMapper->SetInputData(originalMapper->GetInput()); // 井线几何数据通常是静态的,可以共享 newActor->SetMapper(newMapper); } // 复制 Property vtkSmartPointer newProp = vtkSmartPointer::New(); newProp->DeepCopy(originalActor->GetProperty()); newActor->SetProperty(newProp); offscreenWellLineActors.insert(wellLineIter.key(), newActor); } // 将所有复制的 Actor 添加到离屏渲染器 offscreenRenderer->AddActor(offscreenActor); offscreenRenderer->AddActor(offscreenScalarBar); QMapIterator> offscreenNameIter(offscreenWellNameActors); while(offscreenNameIter.hasNext()) { offscreenNameIter.next(); offscreenRenderer->AddActor(offscreenNameIter.value()); } QMapIterator> offscreenLineIter(offscreenWellLineActors); while(offscreenLineIter.hasNext()) { offscreenLineIter.next(); offscreenRenderer->AddActor(offscreenLineIter.value()); } // 3. 配置视频写入器和进度条 // 创建FFmpeg视频写入器。 vtkSmartPointer pVideoWriter = vtkSmartPointer::New(); // 创建一个滤镜,用于将VTK渲染窗口的内容转换为图像数据。 vtkSmartPointer pWindowToImageFilter = vtkSmartPointer::New(); pWindowToImageFilter->SetInput(offscreenRenderWindow); pWindowToImageFilter->SetInputBufferTypeToRGB(); pWindowToImageFilter->ReadFrontBufferOff(); // 将滤镜的输出连接到视频写入器的输入。 pVideoWriter->SetInputConnection(pWindowToImageFilter->GetOutputPort()); // 设置输出文件名。 pVideoWriter->SetFileName(sFileName.toStdString().c_str()); // 设置视频帧率,每秒10帧。 pVideoWriter->SetRate(10); // 设置视频质量(0-5,0是最高质量)。 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 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::New(); m_contourWidget->SetInteractor(m_interactor); // 2. 创建新的 Representation m_contourRepresentation = vtkSmartPointer::New(); m_contourWidget->SetRepresentation(m_contourRepresentation); // 3. 获取当前帧裁剪后的数据(阈值过滤器输出) vtkSmartPointer geometry = vtkSmartPointer::New(); geometry->SetInputConnection(m_thresholdFilter->GetOutputPort()); geometry->Update(); // 计算法线 vtkSmartPointer normals = vtkSmartPointer::New(); normals->SetInputConnection(geometry->GetOutputPort()); normals->ComputePointNormalsOn(); // 关键点 normals->Update(); m_polyDataForContour = normals->GetOutput(); // 4. 设置 PointPlacer(使用已有网格) m_pointPlacer = vtkSmartPointer::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 pts = vtkSmartPointer::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 loop = vtkSmartPointer::New(); loop->SetLoop(pts); // 3. 提取落在环内(含边界)的网格单元 vtkSmartPointer extract = vtkSmartPointer::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(); }