11.回调、观察者机制 Observer

第十阶段:进阶,回调、观察者机制 Observer(vtkCommand / AddObserver)

VTK整套交互、事件通知全部基于观察者模式。 鼠标拾取、Filter进度、窗口渲染事件、键盘按键,全部靠这套机制。 对比Qt信号槽:Qt是Qt自己的事件体系;VTK有独立的事件回调体系,两者不能直接互通,需要手动搭桥。

1. 核心基础概念

三个核心角色

  1. vtkObject(被观察对象):一切VTK对象的基类。 vtkRenderWindowInteractor、vtkRenderer、各种Filter全都是vtkObject。 它可以触发事件,并且可以注册观察者。
  2. 事件ID(unsigned long):事件枚举,例如: vtkCommand::LeftButtonPressEvent 鼠标左键按下 vtkCommand::MouseMoveEvent鼠标移动 vtkCommand::KeyPressEvent键盘按下 vtkCommand::UpdateEvent管线更新事件
  3. vtkCommand(回调对象):事件触发时,被执行的回调逻辑。通过AddObserver挂载到被观察对象上。

API总览

// 注册观察者:对象监听某个事件,绑定回调
unsigned long observerId = obj->AddObserver(事件ID, vtkCommand实例);

// 移除单个观察者,需要保存返回的observerId
obj->RemoveObserver(observerId);

// 移除这个对象上全部观察者(析构时很常用)
obj->RemoveAllObservers();

⚠️重点:AddObserver会增加vtkCommand对象的引用计数。 如果不手动RemoveObserver,就算外部smartPointer销毁,回调对象依然存活,造成内存泄漏。

2. 自定义vtkCommand回调写法(继承重写Execute)

这是最标准、工程最常用写法。

#include <vtkCommand.h>

//自定义回调类
class MyMouseCallback : public vtkCommand
{
public:
    //VTK固定写法,New()返回智能指针管理实例
    static MyMouseCallback* New()
    {
        return new MyMouseCallback();
    }

    //真正事件触发就执行这个函数
    virtual void Execute(vtkObject* caller,
                         unsigned long eventId,
                         void* callData) override
    {
        // caller:触发事件的对象,这里就是 interactor
        // eventId:事件类型
        // callData:部分事件附带额外数据,大部分鼠标事件为nullptr

        qDebug()<<"VTK鼠标事件触发";
    }
};

挂载到交互器上

//创建回调实例
auto mouseCb = vtkSmartPointer<MyMouseCallback>::New();

//绑定:监听鼠标左键按下事件
unsigned long m_observerID =
m_vtkWidget->GetInteractor()->AddObserver(vtkCommand::LeftButtonPressEvent, mouseCb);

当你在三维窗口按下鼠标左键,Execute()函数就会被执行。

销毁注意(工程大坑)

  1. 如果你这个控件销毁,一定要移除观察者
m_vtkWidget->GetInteractor()->RemoveObserver(m_observerID);

如果忘记移除,interactor还持有回调对象引用,会内存泄漏,程序退出崩溃。 也可以直接:m_vtkWidget->GetInteractor()->RemoveAllObservers();,简单粗暴。

3. Lambda简易回调(VTK9+支持 vtkCallbackCommand)

不想写一整个子类,可以用lambda快速写回调,适合简单逻辑。

#include <vtkCallbackCommand.h>

auto lambdaCb = vtkSmartPointer<vtkCallbackCommand>::New();

lambdaCb->SetCallback([](vtkObject* caller, unsigned long eventId, void* callData)
{
    qDebug()<<"lambda回调执行";
});

m_vtkWidget->GetInteractor()->AddObserver(vtkCommand::MouseMoveEvent, lambdaCb);

同样,也要RemoveObserver,否则引用计数不会释放。

4. 常用事件列表(机械开发高频)

事件 含义
vtkCommand::LeftButtonPressEvent 鼠标左键按下
vtkCommand::LeftButtonReleaseEvent 鼠标左键抬起
vtkCommand::RightButtonPressEvent 鼠标右键按下
vtkCommand::MouseMoveEvent 鼠标移动(高频,每一帧鼠标动就触发,不要放重运算)
vtkCommand::KeyPressEvent 键盘按键
vtkCommand::RenderEvent 渲染窗口每一次渲染完成之后触发
vtkCommand::UpdateEvent Filter管线执行完成事件

⚠️MouseMoveEvent会疯狂触发,回调内部不要做耗时操作,不要无脑Update大Filter,会直接卡顿。

5. 最重要工程原则:VTK回调不要直接操作Qt UI

❌严重警告:vtkCommand的Execute执行在VTK内部事件循环,并不是Qt主线程。 在Execute里面直接操作Qt控件、修改UI,会出现随机崩溃、诡异的界面错乱。

✅正确工程方案:回调内部只做数据采集,不碰UI,通过Qt信号把数据抛到Qt主线程,槽函数操作界面。

完整可运行示例:

//mainwindow.h
signals:
    void sigMouseLeftDown(int x, int y); //自定义信号
//回调类
class MyMouseCallback : public vtkCommand
{
public:
    static MyMouseCallback* New(){return new MyMouseCallback;}

    //保存外部Qt对象裸指针,不要vtkSmartPointer,避免循环引用!
    void SetReceiver(QObject* obj)
    {
        m_receiver = obj;
    }

    virtual void Execute(vtkObject* caller, unsigned long eventId, void* callData) override
    {
        auto interactor = static_cast<vtkRenderWindowInteractor*>(caller);
        int x = interactor->GetEventPosition()[0];
        int y = interactor->GetEventPosition()[1];

        //回调内部:仅仅发射Qt信号,不操作UI!
        emit m_receiver->sigMouseLeftDown(x,y);
    }
private:
    QObject* m_receiver = nullptr;
};
//mainwindow构造函数中绑定
auto mouseCb = vtkSmartPointer<MyMouseCallback>::New();
mouseCb->SetReceiver(this);

unsigned long obsId = m_vtkWidget->GetInteractor()->AddObserver(vtkCommand::LeftButtonPressEvent, mouseCb);

//信号槽绑定,真正UI逻辑写在lambda槽函数,运行在Qt主线程
connect(this,&MainWindow::sigMouseLeftDown,this,[](int x,int y){
    qDebug()<<"qt主线拿到鼠标坐标:"<<x<<" "<<y;
});

⚠️注意:回调里面保存外部对象,只用裸指针,禁止vtkSmartPointer,防止循环引用内存泄漏。 外部Qt对象销毁前,务必调用RemoveObserver,保证回调不会访问已经销毁的Qt对象。

6. 监听Filter管线事件(进度监听)

比如大模型加载、Filter计算,监听进度事件,可以给Qt进度条赋值。

filter->AddObserver(vtkCommand::ProgressEvent, callback);

callData可以拿到0~1的进度值。

7. 常见坑总结

  1. 忘记RemoveObserver:AddObserver增加引用计数,不删除会内存泄漏、退出崩溃。
  2. 回调里面直接操作Qt控件:随机崩溃,必须走Qt信号中转。
  3. MouseMoveEvent回调做大量计算:鼠标一动疯狂执行,界面卡死。
  4. 循环引用:vtkCommand持有外部对象智能指针,外部对象又持有回调智能指针,引用计数永远不为0。👉解决方案:回调存裸指针,析构时移除观察者。
  5. 不要在Execute回调内部去修改正在触发事件的对象,容易死循环。

8. 和Qt信号槽对比,帮你理清边界

VTK Observer(vtkCommand) Qt Signal&Slot
归属 VTK事件循环 Qt事件循环
线程 VTK交互线程 Qt主线程
互通 不能直接调用,需要手动emit信号 原生qt组件互通
内存 引用计数,需要手动RemoveObserver 自动管理

工业软件标准写法: VTK负责捕获鼠标、渲染事件 → vtkCommand拿到原始数据 → emit Qt信号 → Qt槽函数执行业务逻辑、更新UI。

posted @ 2026-08-24 16:11  Drunker•L  阅读(20)  评论(0)    收藏  举报