#include "nmWxVTKRenderContainerWidget.h" #include "nmVTKCameraController.h" #include "nmVTKImageExporter.h" #include "nmVTKInteractionManager.h" #include "nmVTKOrientationMarkerController.h" #include "nmVTKScene.h" #include "nmVTKViewCommandController.h" #include #include #include #include #include #include #include #include #include #include #include #include VTK_MODULE_INIT(vtkRenderingOpenGL) VTK_MODULE_INIT(vtkInteractionStyle) #include #include #include nmWxVTKRenderContainerWidget::nmWxVTKRenderContainerWidget(QWidget* pParent) : QWidget(pParent), m_pMainLayout(nullptr), m_pStateStack(nullptr), m_pStatePage(nullptr), m_pStateLabel(nullptr), m_pVtkWidget(nullptr), m_pRenderer(nullptr), m_pScene(nullptr), m_pCameraController(nullptr), m_pInteractionManager(nullptr), m_pImageExporter(nullptr), m_pViewCommandController(nullptr), m_pOrientationMarkerController(nullptr) { Q_ASSERT(isGuiThread()); // 1. 先创建稳定布局和状态页,避免无数据时改变容器尺寸。 m_pMainLayout = new QVBoxLayout(this); m_pMainLayout->setContentsMargins(0, 0, 0, 0); m_pMainLayout->setSpacing(0); m_pStateStack = new QStackedWidget(this); m_pStatePage = new QWidget(m_pStateStack); m_pStateLabel = new QLabel(m_pStatePage); QVBoxLayout* pStateLayout = new QVBoxLayout(m_pStatePage); pStateLayout->setContentsMargins(24, 24, 24, 24); pStateLayout->addStretch(); m_pStateLabel->setAlignment(Qt::AlignCenter); m_pStateLabel->setWordWrap(true); pStateLayout->addWidget(m_pStateLabel); pStateLayout->addStretch(); // 2. QVTKWidget 由 Qt 父子关系拥有,并作为就绪状态页面。 m_pVtkWidget = new QVTKWidget(m_pStateStack); m_pStateStack->addWidget(m_pVtkWidget); m_pStateStack->addWidget(m_pStatePage); m_pMainLayout->addWidget(m_pStateStack); // 3、4. RenderWindow 属于 QVTKWidget;Renderer 由本容器智能指针独占。 vtkRenderWindow* pRenderWindow = getRenderWindow(); vtkRenderWindowInteractor* pInteractor = getInteractor(); m_pRenderer = vtkSmartPointer::New(); if(pRenderWindow != nullptr && m_pRenderer != nullptr) { pRenderWindow->AddRenderer(m_pRenderer); m_pRenderer->SetGradientBackground(true); m_pRenderer->SetBackground(0.67, 0.82, 0.94); m_pRenderer->SetBackground2(0.0, 0.5, 1.0); } // 5. 管理对象、图片服务和命令对象均只服务当前容器实例。 m_pScene = new nmVTKScene(m_pRenderer); m_pCameraController = new nmVTKCameraController(m_pRenderer); m_pInteractionManager = new nmVTKInteractionManager(); m_pImageExporter = new nmVTKImageExporter(); m_pImageExporter->setRenderWindow(pRenderWindow); m_pViewCommandController = new nmVTKViewCommandController(this, this); m_pViewCommandController->setCapabilities( nmVTKViewCommandController::ViewCommand_ZoomIn | nmVTKViewCommandController::ViewCommand_ZoomOut | nmVTKViewCommandController::ViewCommand_Fit | nmVTKViewCommandController::ViewCommand_Top | nmVTKViewCommandController::ViewCommand_Front | nmVTKViewCommandController::ViewCommand_Right | nmVTKViewCommandController::ViewCommand_Isometric | nmVTKViewCommandController::ViewCommand_ResetRotationCenter | nmVTKViewCommandController::ViewCommand_SaveImage | nmVTKViewCommandController::ViewCommand_CopyImage | nmVTKViewCommandController::ViewCommand_Print | nmVTKViewCommandController::ViewCommand_PrintPreview); // 命令控制器负责菜单顺序,容器继续向业务层转发同一个扩展时机。 connect(m_pViewCommandController, SIGNAL(contextMenuAboutToShow(QMenu*)), this, SIGNAL(contextMenuAboutToShow(QMenu*))); const bool bInteractionReady = m_pInteractionManager->bindInteractor(pInteractor); // 方向轴是独立辅助 Renderer;绑定失败只隐藏方向轴,不影响主视图初始化。 m_pOrientationMarkerController = new nmVTKOrientationMarkerController(); m_pOrientationMarkerController->bindInteractor( pInteractor, m_pRenderer); // 6. 只接管右键菜单,左键、中键和滚轮继续交给 VTK。 m_pVtkWidget->installEventFilter(this); // 7. 基础 VTK 对象完整时进入就绪页,否则保留明确错误状态。 if(pRenderWindow != nullptr && m_pRenderer != nullptr && bInteractionReady) { showReadyState(); } else { showErrorState(tr("Unable to initialize the render view.")); } } nmWxVTKRenderContainerWidget::~nmWxVTKRenderContainerWidget() { Q_ASSERT(isGuiThread()); // 1. 先阻止新的右键菜单回调进入销毁中的对象。 if(m_pVtkWidget != nullptr) { m_pVtkWidget->removeEventFilter(this); } // 2. 先销毁 QAction,避免后续服务拆除期间再触发容器槽函数。 if(m_pViewCommandController != nullptr) { delete m_pViewCommandController; m_pViewCommandController = nullptr; } // 3. MarkerWidget 持有 Interactor 和辅助 Renderer 引用,必须优先解绑。 if(m_pOrientationMarkerController != nullptr) { m_pOrientationMarkerController->unbindInteractor(); delete m_pOrientationMarkerController; m_pOrientationMarkerController = nullptr; } // 4. 图片服务不拥有 RenderWindow,先解除引用再销毁服务。 if(m_pImageExporter != nullptr) { m_pImageExporter->setRenderWindow(nullptr); delete m_pImageExporter; m_pImageExporter = nullptr; } // 5. Interactor 必须先释放对默认 Style 的 VTK 引用。 if(m_pInteractionManager != nullptr) { m_pInteractionManager->unbindInteractor(); delete m_pInteractionManager; m_pInteractionManager = nullptr; } // 6. Scene 在 Renderer 移除前清空所有 Layer 和 Prop。 if(m_pScene != nullptr) { m_pScene->clear(); delete m_pScene; m_pScene = nullptr; } // 7. 相机控制器仅解除 Renderer 非所有权引用。 if(m_pCameraController != nullptr) { m_pCameraController->setRenderer(nullptr); delete m_pCameraController; m_pCameraController = nullptr; } // 8. 最后从 RenderWindow 移除并释放当前实例 Renderer。 vtkRenderWindow* pRenderWindow = getRenderWindow(); if(pRenderWindow != nullptr && m_pRenderer != nullptr) { pRenderWindow->RemoveRenderer(m_pRenderer); } m_pRenderer = nullptr; // QVTKWidget、状态页和布局随后由 Qt 父子关系自动销毁。 } QVTKWidget* nmWxVTKRenderContainerWidget::getVtkWidget() const { return m_pVtkWidget; } vtkRenderer* nmWxVTKRenderContainerWidget::getRenderer() const { return m_pRenderer; } vtkRenderWindow* nmWxVTKRenderContainerWidget::getRenderWindow() const { return m_pVtkWidget != nullptr ? m_pVtkWidget->GetRenderWindow() : nullptr; } vtkRenderWindowInteractor* nmWxVTKRenderContainerWidget::getInteractor() const { vtkRenderWindow* pRenderWindow = getRenderWindow(); return pRenderWindow != nullptr ? pRenderWindow->GetInteractor() : nullptr; } nmVTKScene* nmWxVTKRenderContainerWidget::getScene() const { return m_pScene; } nmVTKViewCommandController* nmWxVTKRenderContainerWidget::getViewCommandController() const { return m_pViewCommandController; } nmVTKOrientationMarkerController* nmWxVTKRenderContainerWidget::getOrientationMarkerController() const { return m_pOrientationMarkerController; } void nmWxVTKRenderContainerWidget::setViewCommandCapabilities( nmVTKViewCommandController::ViewCommands eCommands) { if(m_pViewCommandController != nullptr) { m_pViewCommandController->setCapabilities(eCommands); } } bool nmWxVTKRenderContainerWidget::installInteractionStyle( vtkInteractorStyle* pStyle) { Q_ASSERT(isGuiThread()); return isGuiThread() && m_pInteractionManager != nullptr && m_pInteractionManager->installInteractionStyle(pStyle); } bool nmWxVTKRenderContainerWidget::restoreDefaultInteraction() { Q_ASSERT(isGuiThread()); return isGuiThread() && m_pInteractionManager != nullptr && m_pInteractionManager->restoreDefaultInteraction(); } void nmWxVTKRenderContainerWidget::clearInteractionStyle() { Q_ASSERT(isGuiThread()); if(isGuiThread() && m_pInteractionManager != nullptr) { m_pInteractionManager->clearInteractionStyle(); } } void nmWxVTKRenderContainerWidget::renderView() { Q_ASSERT(isGuiThread()); vtkRenderWindow* pRenderWindow = getRenderWindow(); // Scene、Layer 和相机均不隐式渲染,批量更新最终汇聚到此入口。 if(isGuiThread() && pRenderWindow != nullptr) { pRenderWindow->Render(); } } bool nmWxVTKRenderContainerWidget::fitTopViewWithoutRender() { Q_ASSERT(isGuiThread()); // 相机控制器本身不触发 Render,便于业务图层完成整批更新后只渲染一次。 return isGuiThread() && m_pCameraController != nullptr && m_pCameraController->setTopView(); } void nmWxVTKRenderContainerWidget::showReadyState() { Q_ASSERT(isGuiThread()); if(!isGuiThread()) { return; } // 状态页与 VTK 页尺寸稳定,切换页面不会重建 RenderWindow 或 Renderer。 m_pStateStack->setCurrentWidget(m_pVtkWidget); if(m_pViewCommandController != nullptr) { m_pViewCommandController->setViewAvailable(true); } if(m_pOrientationMarkerController != nullptr) { m_pOrientationMarkerController->setViewAvailable(true); } renderView(); } void nmWxVTKRenderContainerWidget::showEmptyState(const QString& sMessage) { showMessageState(ViewState_Empty, sMessage); } void nmWxVTKRenderContainerWidget::showErrorState(const QString& sMessage) { showMessageState(ViewState_Error, sMessage); } void nmWxVTKRenderContainerWidget::showRenderingState( const QString& sMessage) { showMessageState(ViewState_Rendering, sMessage); } void nmWxVTKRenderContainerWidget::zoomIn() { if(m_pCameraController != nullptr && m_pCameraController->zoomIn()) { renderView(); } } void nmWxVTKRenderContainerWidget::zoomOut() { if(m_pCameraController != nullptr && m_pCameraController->zoomOut()) { renderView(); } } void nmWxVTKRenderContainerWidget::fitView() { if(m_pCameraController != nullptr && m_pCameraController->fitView()) { renderView(); } } void nmWxVTKRenderContainerWidget::resetCamera() { // 控制器用返回值表示相机是否确实改变,成功时只提交一次渲染。 if(m_pCameraController != nullptr && m_pCameraController->resetCamera()) { renderView(); } } void nmWxVTKRenderContainerWidget::setTopView() { if(m_pCameraController != nullptr && m_pCameraController->setTopView()) { renderView(); } } void nmWxVTKRenderContainerWidget::setFrontView() { if(m_pCameraController != nullptr && m_pCameraController->setFrontView()) { renderView(); } } void nmWxVTKRenderContainerWidget::setRightView() { if(m_pCameraController != nullptr && m_pCameraController->setRightView()) { renderView(); } } void nmWxVTKRenderContainerWidget::setIsometricView() { if(m_pCameraController != nullptr && m_pCameraController->setIsometricView()) { renderView(); } } void nmWxVTKRenderContainerWidget::setRotationCenter( double dX, double dY, double dZ) { if(m_pCameraController != nullptr && m_pCameraController->setRotationCenter(dX, dY, dZ)) { renderView(); } } void nmWxVTKRenderContainerWidget::resetRotationCenter() { if(m_pCameraController != nullptr && m_pCameraController->resetRotationCenter()) { renderView(); } } bool nmWxVTKRenderContainerWidget::eventFilter( QObject* pObject, QEvent* pEvent) { if(pObject == m_pVtkWidget && pEvent != nullptr && pEvent->type() == QEvent::MouseButtonPress) { QMouseEvent* pMouseEvent = static_cast(pEvent); if(pMouseEvent->button() == Qt::RightButton) { // 菜单引用命令控制器长期持有的 QAction,不在每次右键时重复创建。 QMenu oMenu(this); if(m_pViewCommandController != nullptr) { m_pViewCommandController->populateContextMenu(&oMenu); } if(!oMenu.isEmpty()) { oMenu.exec(pMouseEvent->globalPos()); } // 返回 true 只消费右键按下,避免 VTK 同时启动一次交互动作。 return true; } return false; } return QWidget::eventFilter(pObject, pEvent); } void nmWxVTKRenderContainerWidget::saveImage() { if(m_pImageExporter == nullptr) { return; } // 实际编码格式由 QImage 按调用方选择的扩展名决定。 const QString sFileName = QFileDialog::getSaveFileName( this, tr("Save Image"), QString(), tr("PNG Image (*.png);;JPEG Image (*.jpg);;BMP Image (*.bmp)")); if(!sFileName.isEmpty() && !m_pImageExporter->saveImage(sFileName)) { QMessageBox::warning(this, tr("Save Failed"), tr("Unable to save the image.")); } } void nmWxVTKRenderContainerWidget::copyImage() { if(m_pImageExporter != nullptr && !m_pImageExporter->copyImageToClipboard()) { QMessageBox::warning(this, tr("Copy Failed"), tr("Unable to capture the image.")); } } void nmWxVTKRenderContainerWidget::printImage() { if(m_pImageExporter != nullptr) { m_pImageExporter->printImage(this); } } void nmWxVTKRenderContainerWidget::printPreview() { if(m_pImageExporter != nullptr) { m_pImageExporter->printPreview(this); } } void nmWxVTKRenderContainerWidget::showMessageState( ViewState eState, const QString& sMessage) { Q_ASSERT(isGuiThread()); if(!isGuiThread() || m_pStateStack == nullptr || m_pStatePage == nullptr || m_pStateLabel == nullptr) { return; } // 所有非就绪状态共用同一页面,仅错误状态使用警示颜色。 if(eState == ViewState_Error) { m_pStateLabel->setStyleSheet("QLabel { color: #a32626; }"); } else { m_pStateLabel->setStyleSheet("QLabel { color: #4a4a4a; }"); } m_pStateLabel->setText(sMessage); m_pStateStack->setCurrentWidget(m_pStatePage); if(m_pViewCommandController != nullptr) { m_pViewCommandController->setViewAvailable(false); } if(m_pOrientationMarkerController != nullptr) { m_pOrientationMarkerController->setViewAvailable(false); } } bool nmWxVTKRenderContainerWidget::isGuiThread() const { QCoreApplication* pApplication = QCoreApplication::instance(); return pApplication == nullptr || pApplication->thread() == QThread::currentThread(); }