11.回调、观察者机制 Observer
第十阶段:进阶,回调、观察者机制 Observer(vtkCommand / AddObserver)
VTK整套交互、事件通知全部基于观察者模式。 鼠标拾取、Filter进度、窗口渲染事件、键盘按键,全部靠这套机制。 对比Qt信号槽:Qt是Qt自己的事件体系;VTK有独立的事件回调体系,两者不能直接互通,需要手动搭桥。
1. 核心基础概念
三个核心角色
- vtkObject(被观察对象):一切VTK对象的基类。
vtkRenderWindowInteractor、vtkRenderer、各种Filter全都是vtkObject。 它可以触发事件,并且可以注册观察者。 - 事件ID(unsigned long):事件枚举,例如:
vtkCommand::LeftButtonPressEvent鼠标左键按下vtkCommand::MouseMoveEvent鼠标移动vtkCommand::KeyPressEvent键盘按下vtkCommand::UpdateEvent管线更新事件 - 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()函数就会被执行。
销毁注意(工程大坑)
- 如果你这个控件销毁,一定要移除观察者
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. 常见坑总结
- 忘记RemoveObserver:AddObserver增加引用计数,不删除会内存泄漏、退出崩溃。
- 回调里面直接操作Qt控件:随机崩溃,必须走Qt信号中转。
- MouseMoveEvent回调做大量计算:鼠标一动疯狂执行,界面卡死。
- 循环引用:vtkCommand持有外部对象智能指针,外部对象又持有回调智能指针,引用计数永远不为0。👉解决方案:回调存裸指针,析构时移除观察者。
- 不要在Execute回调内部去修改正在触发事件的对象,容易死循环。
8. 和Qt信号槽对比,帮你理清边界
| VTK Observer(vtkCommand) | Qt Signal&Slot | |
|---|---|---|
| 归属 | VTK事件循环 | Qt事件循环 |
| 线程 | VTK交互线程 | Qt主线程 |
| 互通 | 不能直接调用,需要手动emit信号 | 原生qt组件互通 |
| 内存 | 引用计数,需要手动RemoveObserver | 自动管理 |
工业软件标准写法: VTK负责捕获鼠标、渲染事件 → vtkCommand拿到原始数据 → emit Qt信号 → Qt槽函数执行业务逻辑、更新UI。

浙公网安备 33010602011771号