4、VTK可视化管线
通过第3章的学习,我们已经了解了VTK的一些基础概念。在这一章里,我们将更深入地学习VTK,其中包括VTK的系统框架结构、引用计数、智能指针、Observer/Command设计机制以及本章的重点内容——VTK可视化管线结构。通过本章的学习,可能你对VTK的设计框架将会有更深一层的理解。
所谓追根溯源,首先我们先了解一下VTK里绝大多数类的共同的父类vtkObjectBase和vtkObject。
4.1 vtkObjectBase和vtkObject
vtkObjectBase是一个抽象基类,派生出绝大多数的VTK类。它是VTK里所有引用计数(Reference Counting)类的基类,著名的子类包括:vtkCommand,vtkInformationKey和vtkObject。
4.1.1 引用计数
如果很多对象有相同的值,将这个值存储多次是很无聊的。更好的办法是让所有的对象共享这个值的实现。这么做不但节省内存,而且可以使得程序运行更快,因为不需要构造和析构这个值的拷贝。引用计数就是这样一个技巧,它允许多个有相同值的对象共享这个值的实现。引用计数是个简单的垃圾回收体系,只要其它对象引用某对象(记为对象O),对象O就会存在一个引用计数,当最后引用对象O的对象移除,O对象就会自动析构。VTK里使用引用计数的好处是,可以实现数据之间的共享而不用拷贝,从而达到节省内存的目的。
我们可以看一个简单的例子(4.1.1_ReferenceCounting):
///////////////////////////////////////////////////////ReferenceCount.cpp/////////////////////////////////////////////////////////
1: #include"vtkSmartPointer.h"
2: #include"vtkBMPReader.h"
3: #include"vtkImageData.h"
4:
5: int main(int argc, char*argv[])
6: {
7: vtkSmartPointer<vtkBMPReader>reader = vtkSmartPointer<vtkBMPReader>::New();
8: reader->SetFileName("../test.bmp");
9: reader->Update();
10:
11: std::cout<<"Reference Count of reader->GetOutput (BeforeAssignment) = "
12: <<reader->GetOutput()->GetReferenceCount()<<std::endl;
13:
14: vtkSmartPointer<vtkImageData> image1 = reader->GetOutput();
15: std::cout<<"Reference Count of reader->GetOutput (Assignto image1) = "
16: <<reader->GetOutput()->GetReferenceCount()<<std::endl;
17: std::cout<<"Reference Count of image1 = "
18: <<image1->GetReferenceCount()<<std::endl;
19:
20: vtkSmartPointer<vtkImageData> image2 = reader->GetOutput();
21: std::cout<<"Reference Count of reader->GetOutput (Assignto image2) = "
22: <<reader->GetOutput()->GetReferenceCount()<<std::endl;
23: std::cout<<"Reference Count of image2 = "
24: <<image2->GetReferenceCount()<<std::endl;
25:
26: return 0;
27: }
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
程序输出结果如图4.1所示。
图4.1ReferenceCount运行结果
在ReferenceCount示例里,我们先用vtkBMPReader读入一幅BMP图像test.bmp,在赋值之前我们输出了reader->GetOutput()的引用计数值,其值为1(使用方法New()创建对象以后,初始的引用计数值就等于1);然后我们创建了一个vtkImageData类型的对象image1,并把reader的输出赋给了image1,这时image1就指向了reader的输出,也就是说,reader的输出多了一个引用,这个时候输出的reader->GetOutput()和image1的引用计数都为2;接着我们又创建一个类型同样为vtkImageData的对象image2,同样也是把reader的输出赋值给image2,这时,image2也指向reader的输出,亦即reader的输出又多了一个引用,所以输出的reader->GetOutput()和image2的引用计数值变成了3。image1和image2的数据结构可以简单地描述为图4.2。
图4.2image1,image2,reader->GetOutput()及引用计数之间的结构关系
一旦某个对象的引用计数等于0时,就表明没有别的对象再引用它,它的使命也宣告完成,程序就会自动的析构这个对象。在ReferenceCount这个示例里,我们看不到引用计数减少的相关代码,这是因为我们使用了智能指针vtkSmartPointer。
4.1.2 智能指针
智能指针会自动管理引用计数的增加与减少,如果检测到某对象的引用计数值减少为0,则会自动地释放该对象的资源,从而达到自动管理内存的目的。
在前面的内容我们已经介绍过,VTK里,要创建一个对象可以用两种方法,一种是使用vtkObjectBase里的静态成员变量New(),用Delete()方法析构;另一种就是我们示例里使用多次的使用智能指针vtkSmartPointer<T>。
对于第一种方法,用New()创建的对象,程序最后必须要调用Delete()方法释放对应的内存,而且由于vtkObjectBase及其子类的构造函数都是声明为受保护的,这意味着它们不能在栈区(栈区上的内存是由编译器自动分配与释放的,堆区上的内存则是由程序员分配和手动释放的。)上分配内存。比如:
vtkBMPReader*reader = vtkBMPReader::New(); //创建vtkBMPReader对象
……
reader->Delete();//程序最后要调用Delete(),这里并没有直接析构对象,而是使引用计数值减1。
用New()创建的对象,如果没有用Delete()方法删除的话,程序有可能会出现内存泄漏,即用户负责对象内存的管理。
如果使用智能指针创建的对象,则无需手动调用Delete()方法让引用计数减少,因为引用计数的增加与减少都是由智能指针自动完成的。使用智能指针时,首先是要包含智能指针的头文件:#include "vtkSmartPointer.h"。vtkSmartPointer是一个模板类,所需的模板参数就是待创建的对象的类名,如:
vtkSmartPointer<vtkImageData>image = vtkSmartPointer< vtkImageData >::New();
注意上面一行代码等号右边写法,不能写为:
vtkSmartPointer<vtkImageData > image = vtkImageData::New();
也就是不能把对象的原始指针赋给智能指针,上行代码编译的时候可以通过,但程序退出时会有内存泄漏,就是因为智能指针无法自动释放该对象的内存,如图4.3所示。
图4.3 将对象的原始指针赋予智能指针会引起内存泄漏
如果没有给对象分配内存,仍然可以使用智能指针,比如:
vtkSmartPointer<vtkBMPReader>reader =vtkSmartPointer<vtkBMPReader>::New();
vtkImageData* imageData=reader->GetOutput();
或者:
vtkSmartPointer< vtkImageData> imageData = reader->GetOutput();
第一种情况,当reader超出其作用域时,数据即会被删除;第二种情况,使用了智能指针,所以数据的引用计数会自动加1,除非reader和imageData都超出它们的作用域,数据才会被删除。
智能指针类型同样也可以作为函数的返回值。正确的写法类似:
vtkSmartPointer<vtkImageData>MyFunction()
{
vtkSmartPointer<vtkImageData> myObject= vtkSmartPointer<vtkImageData>::New();
return myObject;
}
调用时则是:
vtkSmartPointer<vtkImageData>MyImageData = MyFunction();
函数MyFunction()的返回值是通过拷贝的方式,将数据赋予调用的变量,因此该数据的引用计数保持不变,而且函数MyFunction里的myObject对象也不会删除。
下面的函数形式和函数调用是错误的,应该引起注意:
vtkImageData* MyFunction()
{
vtkSmartPointer< vtkImageData >MyObject = vtkSmartPointer< vtkImageData >::New();
return MyObject;
}
vtkImageData* MyImageData = MyFunction();
在函数MyFunction()里定义的是智能指针类型的,最后返回时转换成原始指针类型,当函数调用结束时,智能指针的引用计数会减为0,即函数MyFunction里的MyObject对象会被删除掉,也就是说MyFunction()返回的是悬空指针,这时再赋予MyImageData变量就会出错。
智能指针类型也可以作为类的成员变量,而且会使得类在析构时更加容易,不用人为去做任何释放内存的事情,把这些工作都交给智能指针来完成,例如:
classMyClass
{
vtkSmartPointer<vtkFloatArray>Distances;
};
然后在类的构造函数里进行初始化:
MyClass::MyClass()
{
Distances = vtkSmartPointer<vtkFloatArray>::New();
}
在类的析构函数里不用调用Delete()去删除任何东西。
智能指针有一个让人困惑的地方:当你创建一个智能指针类型的对象,然后改变它的指向,这时引用计数就会出错。例如:
vtkSmartPointer<vtkImageData>imageData = vtkSmartPointer<vtkImageData>::New();
imageData= Reader->GetOutput();
上面两行代码里,我们首先创建一个imageData,并给他分配好了内存,接着我们又把imageData指向Reader的输出,而不是一直指向我们创建的那块内存。对于这种情况,我们只要简单地调用:
vtkImageData*imageData = Reader->GetOutput();
这里没有必要使用智能指针,因为我们没有实际创建任何新的对象。
综上所述,可以看出引用计数和智能指针是息息相关的,它们主要都是用于内存管理。使用智能指针可以免去很多手动删除变量的烦恼,所以在本教程里,我们从一开始都使用智能指针来创建VTK对象。如果你想了解更多关于引用计数和智能指针的内容,可以参考C++的经典著作《More Effective C++》这本书。
4.1.3 运行时类型识别 (Run-Time Type Information,RTTI)
在C++里,对象类型是通过typeid (需要包含头文件#include<type_info>)获取的;VTK里在vtkObjectBase定义了获取对象类型的方法:GetClassName()和IsA()。GetClassName()返回的是该对象类名的字符串(VTK用类名来识别各个对象),如:
vtkSmartPointer<vtkBMPReader>Reader = vtkSmartPointer<vtkBMPReader>::New();
constchar* type = Reader->GetClassName(); //返回“vtkBMPReader”字符串
IsA()方法用于测试某个对象是否为指定字符串的类型或其子类,比如:
if(Reader->IsA(“vtkImageReader”) ) {……}; // 这里IsA()会返回真。
类比C++里的操作RTTI操作符,除了typeid之外,还有dynamic_cast,主要用于基类向子类的类型转换,称为向下转型。VTK里同样提供了类似的方法,也就是vtkObject里定义的SafeDownCast(),它是vtkObject里的静态成员函数,意味着它是属于类的,而不是属于对象的,即可以用vtkObject::SafeDownCast()直接调用,比如:
vtkSmartPointer<vtkImageReader>ReaderBase = vtkSmartPointer<vtkImageReader>::New();
vtkBMPReader*bmpReader = vtkBMPReader::SafeDownCast(ReaderBase);
与dynamic_cast类似,SafeDownCast也是运行时才转换的,这种转换只有当bmpReader的类型确实是ReaderBase的派生类时才有效,否则返回空指针。
除了运行时类型识别,vtkObjectBase还提供了用于调试的状态输出接口Print()。虽然vtkObjectBase里除了Print()还提供PrintSelf()、PrintHeader()、PrintTrailer()等公共接口,但在调试VTK程序时,如果需要输出某个对象的状态信息时,一般都是调用Print()函数,如:
bmpReader->Print(std::cout);
4.1.4 关于vtkObject的两三事
以上的几小节都是在讨论vtkObjectBase这个VTK始祖类的一些特性,接下来我们看一下vtkObjectBase这个始祖类其中一个“儿子”的本领。vtkObject是大多数VTK类的父类,是一个抽象基类,大多数在VTK框架里的类都应该是它的子类或其某个子类的子类。这一小节我们起名为“关于vtkObject的两三事”,因为vtkObject刚好做了三件重要的事情。
第一件事是,vtkObject里定义了与程序调试相关的一些公共接口,包括:
DebugOn() / DebugOff()
GetDebug() / SetDebug(unsignedchar)
SetGlobalWarningDisplay(int) / GetGlobalWarningDisplay()
GlobalWarningDisplayOn() / GlobalWarningDisplayOff()
其中后四个是静态成员函数。我们可以通过示例(4.1.4_vtkObjectDemo)来看看vtkObject做的第一件事到底是什么。
///////////////////////////////////////////////////////vtkObjectDemo.cpp/////////////////////////////////////////////////////////
1: #include"vtkSmartPointer.h"
2: #include"vtkBMPReader.h"
3: #include "vtkImageViewer2.h"
4: #include"vtkRenderWindowInteractor.h"
5:
6: int main(int argc, char*argv[])
7: {
8: vtkSmartPointer<vtkBMPReader> reader =vtkSmartPointer<vtkBMPReader>::New();
9: reader->SetFileName("../monochrome.bmp");
10: reader->Allow8BitBMPOn();
11: reader->SetDebug(1);
12: //reader->SetDebug(0);
13: //reader->GlobalWarningDisplayOff();
14: reader->Update();
15:
16: vtkSmartPointer<vtkImageViewer2> viewer =vtkSmartPointer<vtkImageViewer2>::New();
17: viewer->SetInput(reader->GetOutput());
18:
19: vtkSmartPointer<vtkRenderWindowInteractor> interactor =
20: vtkSmartPointer<vtkRenderWindowInteractor>::New();
21: viewer->SetupInteractor(interactor);
22: viewer->Render();
23:
24: interactor->Initialize();
25: interactor->Start();
26:
27: return 0;
28: }
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
第11行SetDebug(1)作用等同于DebugOn(),第12行SetDebug(0)等同于DebugOff()。示例里我们读入的BMP图像是单色的,由于VTK不支持单色的BMP图像的读取,所以如果调用方法SetDebug(1)或者DebugOn()时,则会弹出图4.4-A所示的窗口;调用DebugOff()时,弹出的窗口如图4.4-B所示,如果不想看到vtkOutputWindow窗口,可以调用GlobalWarningDisplayOff()或者SetGlobalWarningDisplay(0)。
图4.4vtkOutputWindow窗口
因为VTK不支持1位BMP图像的读取,所以调用reader->GetOutput()时不能得到正确的输出,最终会导致程序在关闭时,出现内存溢出的错误,如图4.5所示。
图4.5 示例4.1.4_vtkObjectDemo程序退出时内存溢出错误
注:示例4.1.4_vtkObjectDemo也可以作一下更改,以便让它支持命令行参数,作为BMP图像的浏览器,修改后的工程为BMPImageViewer。比如在CMD窗口里输入命令:
或者可以直接在VS2008里输出需要的命令行参数。右击“BMPImageViewer”工程,选择属性,然后选择属性对话框左边的Debugging,接着在“Command Arguments”一栏输出所需的参数,比如..\test.bmp (可以用绝对路径,如果路径里含有空格,记得用双引号把整个路径括起来)。确定,保存所做的更改。最后在VS2008下F5运行程序。
如果你感觉程序运行时后面老是跟着控制台窗口,让你觉得不爽的话,你可以在main()函数之前加入下面的语句隐藏控制台窗口,一般建议在发布程序之前不要隐藏,方便调试程序时随时输出调试信息。
#pragmacomment(linker,"/subsystem:\"windows\"/entry:\"mainCRTStartup\"")
vtkObject的第二件事是,实现观察者/命令(Observer/Command)设计模式。本教程不是专门介绍设计模式的,如果你想更深入地了解Observer/Command设计模式,可以翻翻相关的书籍,下面只是就这两种设计模式蜻蜓点水般的做一概述。
vtkObject定义了与观察者Observer相关的方法(如,AddObserver()/RemoveObserver()),观察者模式主要针对两个对象:Object和Observer。一个Object可以有多个Observer,它定义对象间的一种一对多的依赖关系,当—个Object对象的状态发生改变时,所有依赖于它的Observer对象都得到通知被自动更新。
命令模式属于对象行为模式,它将—个请求封装为一个对象,并提供一致性发送请求的接口,当一个事件发生时,它不直接把事件传递到事件调用者,而是在命令和调用者之间增加—个中间者,将这种直接关系切断,同时两者之间都隔离。事件调用者只是和接口打交道,不和具体实现交互。命令模式的实现是由vtkObjectBase的另外一个重要子类vtkCommand及其派生类实现的。
如果你还是觉得抽象的话,我们就看看下面的示例(ObserverCommandDemo.cpp):
////////////////////////////////////////////////ObserverCommandDemo.cpp///////////////////////////////////////////////////
1: #include"vtkSmartPointer.h"
2: #include"vtkBMPReader.h"
3: #include"vtkImageViewer2.h"
4: #include"vtkRenderWindowInteractor.h"
5: #include"vtkCallbackCommand.h"
6:
7: long pressCounts = 0;
8:
9: //第一步,定义回调函数。
10: //注意回调函数的签名,不能更改。
11: void MyCallbackFunc(vtkObject*, unsigned long eid, void* clientdata,void *calldata)
12: {
13: std::cout<<"You have clicked:"<<++pressCounts<<" times."<<std::endl;
14: }
15:
16: int main(int argc, char*argv[])
17: {
18: vtkSmartPointer<vtkBMPReader> reader =
19: vtkSmartPointer<vtkBMPReader>::New();
20: reader->SetFileName("../test.bmp");
21: reader->Allow8BitBMPOn();
22: reader->SetDebug(0);
23: reader->GlobalWarningDisplayOff();
24: reader->Update();
25:
26: vtkSmartPointer<vtkImageViewer2> viewer =
27: vtkSmartPointer<vtkImageViewer2>::New();
28: viewer->SetInput(reader->GetOutput());
29:
30: vtkSmartPointer<vtkRenderWindowInteractor> interactor =
31: vtkSmartPointer<vtkRenderWindowInteractor>::New();
32: viewer->SetupInteractor(interactor);
33: viewer->Render();
34:
35: //第二步,设置回调函数。
36: vtkSmartPointer<vtkCallbackCommand> mouseCallback =
37: vtkSmartPointer<vtkCallbackCommand>::New();
38: mouseCallback->SetCallback ( MyCallbackFunc );
39:
40: //第三步,将vtkCallbackCommand对象添加到观察者列表。
41: interactor->SetRenderWindow(viewer->GetRenderWindow());
42: interactor->AddObserver(vtkCommand::LeftButtonPressEvent,mouseCallback);
43:
44: interactor->Initialize();
45: interactor->Start();
46:
47: return 0;
48: }
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
示例ObserverCommandDemo运行结果如图4.6所示。
图4.6 示例ObserverCommandDemo运行结果
从示例ObserverCommandDemo可以看到,VTK里使用事件回调函数时,需要分三步走。首先,定义回调函数。回调函数的签名只能是以下形式:
void MyCallbackFunc(vtkObject*obj,unsigned long eid, void* clientdata, void *calldata)
其次是创建一个vtkCallbackCommand对象,并调用vtkCallbackCommand::SetCallback()设置第一步定义的回调函数。
最后是将vtkCallbackCommand对象添加到对象的观察者列表。VTK里,所有的事件都是通过vtkInteractorObserver(vtkRenderWindowInteractor的父类,vtkInteractorObserver派生自vtkObject) 进行监听的,所以,我们把vtkCallbackCommand对象加入到vtkRenderWindowInteractor对象的观察者列表中,调用的就是vtkObject提供的公共接口AddObserver(),其原型为:
unsigned longAddObserver (unsigned long event, vtkCommand *, float priority=0.0f)
第一个参数是要监听的事件,这些事件定义在vtkCommand类里,如:
RenderEvent /ProgressEvent / PickEvent / StartPickEvent / EndPickEvent / ExitEvent /LeftButtonPressEvent / LeftButtonReleaseEvent / MiddleButtonPressEvent /MiddleButtonReleaseEvent / RightButtonPressEvent / RightButtonReleaseEvent /KeyPressEvent / KeyReleaseEvent /CharEvent / TimerEvent
示例中我们监听的是鼠标左键的单击事件,即LeftButtonPressEvent。
AddObserver的第二个参数是vtkCommand类型的指针,即我们创建的已经绑定回调函数的vtkCallbackCommand对象。第三个参数是设置命令响应的优先权。
AddObserver函数的返回值是unsigned long型的,可以用于把观察者从观察者列表中删除RemoveObserver(eventID)。
示例ObserverCommandDemo监听交互过程中的鼠标左键单击事件,如果监听到该事件,就在控制台中输出目前为止鼠标的单击次数,该示例仅仅是为了演示观察者/命令模式的工作方式,除此之外没有任何实用价值。除了用以上介绍的回调函数形式来完成事件/回调的工作,同样也可以用类(派生自vtkCommand)的形式来完成。示例ObserverCommandDemo2里,我们会看到稍微复杂一点的应用。
////////////////////////////////////////////////ObserverCommandDemo2.cpp/////////////////////////////////////////////////
1: #include"vtkSmartPointer.h"
2: #include"vtkConeSource.h"
3: #include"vtkPolyDataMapper.h"
4: #include"vtkRenderWindow.h"
5: #include"vtkRenderWindowInteractor.h"
6: #include"vtkCamera.h"
7: #include"vtkActor.h"
8: #include"vtkRenderer.h"
9: #include"vtkCommand.h"
10: #include"vtkBoxWidget.h"
11: #include"vtkTransform.h"
12: #include"vtkInteractorStyleTrackballCamera.h"
13:
14: //第一步
15: class vtkMyCallback : publicvtkCommand
16: {
17: public:
18: static vtkMyCallback *New()
19: { return newvtkMyCallback; }
20:
21: virtual voidExecute(vtkObject *caller, unsigned long eventId, void* callData)
22: {
23: vtkTransform *t =vtkTransform::New();
24: vtkBoxWidget *widget =reinterpret_cast<vtkBoxWidget*>(caller);
25: widget->GetTransform(t);
26: widget->GetProp3D()->SetUserTransform(t);
27: t->Delete();
28: }
29: };
30:
31: int main()
32: {
33: vtkSmartPointer<vtkConeSource> cone =vtkSmartPointer<vtkConeSource>::New();
34: cone->SetHeight( 3.0 );
35: cone->SetRadius( 1.0 );
36: cone->SetResolution( 10);
37:
38: vtkSmartPointer<vtkPolyDataMapper> coneMapper =
39: vtkSmartPointer<vtkPolyDataMapper>::New();
40: coneMapper->SetInputConnection( cone->GetOutputPort() );
41:
42: vtkSmartPointer<vtkActor> coneActor = vtkSmartPointer<vtkActor>::New();
43: coneActor->SetMapper(coneMapper );
44:
45: vtkSmartPointer<vtkRenderer> ren1=vtkSmartPointer<vtkRenderer>::New();
46: ren1->AddActor(coneActor );
47: ren1->SetBackground(0.1, 0.2, 0.4 );
48:
49: vtkSmartPointer<vtkRenderWindow> renWin =
50: vtkSmartPointer<vtkRenderWindow>::New();
51: renWin->AddRenderer(ren1 );
52: renWin->SetSize( 300,300 );
53:
54: vtkSmartPointer<vtkRenderWindowInteractor> iren =
55: vtkSmartPointer<vtkRenderWindowInteractor>::New();
56: iren->SetRenderWindow(renWin);
57:
58: vtkSmartPointer<vtkInteractorStyleTrackballCamera> style =
59: vtkSmartPointer<vtkInteractorStyleTrackballCamera>::New();
60: iren->SetInteractorStyle(style);
61:
62: //通过vtkBoxWidget可以控制coneActor的变换矩阵,从而实现coneActor的形变
63: vtkSmartPointer<vtkBoxWidget> boxWidget =vtkSmartPointer<vtkBoxWidget>::New();
64: boxWidget->SetInteractor(iren);
65: boxWidget->SetPlaceFactor(1.25);
66: boxWidget->SetProp3D(coneActor);
67: boxWidget->PlaceWidget();
68:
69: //第二步
70: vtkSmartPointer<vtkMyCallback> callback =vtkSmartPointer<vtkMyCallback>::New();
71:
72: //第三步
73: boxWidget->AddObserver(vtkCommand::InteractionEvent,callback);
74:
75: //激活Widget。按“i”键可以关闭或激活Widget。
76: boxWidget->On();
77:
78: iren->Initialize();
79: iren->Start();
80:
81: return 0;
82: }
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
示例的第31行一直到61行,都是我们比较熟悉的代码,唯一比较陌生的类是vtkBoxWidget,你可以先查看一下这个类的说明文档,关于Widget的使用后续章节也会重点介绍,这里暂且不提,我们的重点是Observer/Command的应用。
与回调函数的类似,首先我们从vtkCommand派生出类vtkMyCallback,该类主要实现两个方法,一个是New(),用于为创建的对象申请内存;一个是Execute(),这是父类vtkCommand里定义的纯虚函数,其原型为:
virtual voidExecute(vtkObject *caller, unsigned long eventId,void *callData) = 0;
这意味着,只要从vtkCommand派生的类,都必须实现这个方法。而这个方法的作用就是一旦监听到所要监听的事件,就会自动地调用该方法。监听到事件后,要完成什么样的操作,都是在Execute()方法里实现,所以我们把精力都放在这个方法上。第一个参数是caller,指向调用观察者的对象,即调用AddObserver()方法的那个对象,如本例中的boxWidget。第二个参数eventId是事件的编号。第三个参数callData,是传递给Execute函数的数据,本例我们没有给该函数传递任何数据,如果你很想知道这个参数有值时应该如何使用,可以找找你计算机上的VTK目录(D:\Toolkits\VTK\VTK-5.10\Examples\Tutorial\Step2\Cxx\Cone2.cxx)。
将Execute()与前面介绍的回调函数作一对比:
void MyCallbackFunc(vtkObject*obj,unsigned long eid, void* clientdata, void *calldata)
前两个参数都是一样的,MyCallbackFunc的第四个参数与Execute()的第三个参数一样,第三个参数是clientdata,这个数据是指回调函数里需要访问主程序里的数据时,由主程序向回调函数传递的,可以通过方法vtkCallbackCommand::SetClientData()设置。
接下来我们看看MyCallback::Execute()的实现(23到27行)。首先我们定义一个vtkTransform对象;然后把传递过来的数据caller用C++操作符reinterpret_cast<>转换成类型vtkBoxWidget*,示例里的这个转换是可以成功的,因为调用观察者的对象也是vtkBoxWidget*类型的(73行)。紧接着,我们把从vtkBoxWidget对象获取到的变换重新应用到vtkBoxWidget里的Prop对象,也就是说Prop对象会跟着vtkBoxWidget对象做同样的变换,或伸或缩。
第二步(69-70行),实例化一个vtkMyCallback对象。
第三步(72-73行),监听vtkBoxWidget对象的交互事件(InteractionEvent)。也就是说,当用户与vtkBoxWidget对象交互时,事件InteractionEvent就会被触发,程序就会自动调用vtkMyCallback里的Execute()事件。
示例ObserverCommandDemo2运行结果如图4.7。
图4.7 ObserverCommandDemo2运行结果(按“i”键可以关闭或激活Widget)
Observer/Command模式在VTK里的应用是非常广泛也是非常重要的,可以用回调函数或者类的形式来实现,以上的内容已经给出比较详细的介绍,使用VTK的话,这两种方式是没有理由不掌握的。
我们继续看vtkObject的第三件事。翻翻vtkObject.h文件,里面有一个受保护的数据成员MTime,与这个MTime相关的公共接口有GetMTime() (返回MTime的值)以及Modified()。MTime全称就是Modification Time,也就是修改时间。vtkObject的第三件事就是我们接下来要学习的内容——“可视化管线”。