【本节对应原书中的第29页至第39页】
3.2创建VTK应用程序
本章内容包括利用Tcl,C++,Java和Python四种语言开发VTK应用程序的基本知识。阅读完引言后,你应该了解用你擅长的语言进行VTK开发的相关内容。为了指导你怎么去创建和运行一个简单的VTK程序,接下来内容都会针对不同的编程语言演示怎样使用Callback。
用户事件、观察者以及命令模式
Callback(又称用户方法UserMethod)采用Subject/Observer和Command设计模式进行设计。VTK中几乎所有的类都可以通过SetObserver()方法建立Callback。Observer观察者监听对象中的所有激活事件,一旦其中一个事件与其监听事件类型一致的话,则其相应的Command就会执行(如Callback)。例如,所有Filter在执行前都会激活StartEvent事件。如果为Filter添加一个Observer来监听StartEvent事件,那么每次Filter执行前,该Observer都会被调用响应Callback。下面的Tcl脚本中创建了一个vtkElevation的实例,并为添加一个Observer来监听StartEvent事件。当Observer监听到该事件时,则自动响应函数PrintStatus()。
- proc PrintStatus {} {
- puts “Starting to execute the elevation filter”
- }
- vtkElevationFilter foo
- foo AddObserver StartEvent PrintStatus
VTK支持的所有语言都可使用这种类型的函数(Callback)。接下来每个小节中都会给出一个简单的例子来说明如何使用它。关于用户方法的深入探讨请参考421页“Integratingwith The Windowing System”(与窗口系统的整合)(该章中还涉及了用户接口整合问题)。
我们建议从VTK自带的例子开始学习如何创建应用程序。这些例子在VTK源文件VTK\Examples目录下。目录中根据不同的主题进行细分,在每个主题目录中会根据不同的语言再分为不同的子目录。
Tcl
学习使用VTK创建应用程序时,Tcl脚本语言是最简单的语言之一。VTK安装完毕后,即可执行VTK自带的Tcl例子。Unix系统下,根据“Unix平台下安装VTK”一节介绍,编译VTK时需要选择支持Tcl。而Windows系统下只需要编译安装自解压目录即可,参考第10页“WindowsXP, Vista及以上版本平台下安装VTK”。
Windows:Windows系统下只需双击Tcl脚本文件即可执行(如本例中Cone.tcl)。如果双击没有反应的话,脚本中可能存在错误,或者是相应的Tcl文件与vtk.exe有错误。如果要进一步检测具体问题,首先需要运行vtk.exe程序,该程序在vtk启动菜单中可以找到。执行后,会弹出一个命令提示行的控制台窗口,在提示行中,键入“cd”命令定位到Tcl文件目录,如下:
- % cd“ c:/VTK/Examples/Tutorial/Step1/Tcl”
然后键入如下命令定位示例脚本:
- %source Cone.tcl
Tcl会执行该脚本,这时所有的错误或者警告信息会输出来。
Unix:Unix下Tcl开发可以通过运行Binary编译目录下(如VTK-bin/bin/VTK,VTK-Solaris/bin/VTK)的可执行文件(编译完成后),然后将脚本文件作为第一个参数输入,如下所示。
- unixmachine> cd VTK/Examples/Tutorial/Step1/Tcl
- unixmachine> /home/ VTK-Solaris/bin/VTK Cone.tcl
用户方法使用如前所述。Examples/Tutorial/Step2/Tcl/Cone2.tcl中有相关的使用示例。下面仅列出了其中的关键部分。
- Proc mycallback {} {
- Puts “Starting to render”
- }
- vtkRendererren1
- ren1AddObserver StartEvent mycallback
你也可以直接给AddObserver()提供函数体。
- vtkRenderer ren1
- ren1 AddObserver StartEvent {puts “Starting to render”}
C++
相对于其他语言,C++应用程序体积更小,运行更快,而且容易部署安装。此外,采用C++语言开发VTK应用程序,你不需要编译额外的Tcl、Java和Python支持。本节中主要说明怎么在PC机上用MicrosoftVisual C++或者Unix下的编译器来用C++开发简单的VTK应用程序。
我们以Examples/Tutorial/Step1/Cxx下的Cone.cxx为例进行讲解。无论Windows平台还是Unix,都可以使用源代码编译版本或者是发布的可执行版本,两个版本都支持这些例子。
编译C++程序的第一步是利用CMake生成依赖于编译器的makefile或者是项目工程文件。Cone.txx目录下的CMakeLists.txt利用CMake的FindVTK和UseVTK模块来定位VTK目录并设置包含路径和链接库等。如果没有成功找到VTK的话,你需要手动设置CMake相关的参数并重新运行CMake。
- PROJECT(Step1)
- FIND_PACKAGE(VTKREQUIRED)
- IF(NOTVTK_USE_RENDERING)
- MESSAGE(FATAL_ERROR "Example${PROJECT_NAME} requires VTK_USE_RENDERING.")
- ENDIF(NOTVTK_USE_RENDERING)
- INCLUDE(${VTK_USE_FILE})
- ADD_EXECUTABLE(ConeCone.cxx)
- TARGET_LINK_LIBRARIES(ConevtkRendering)
MicrosoftVisual C++:用CMake对Cone.cxx配置完成后,启动MicrosoftVisual C++并载入生成的解决方案。当前.net版本下的解决方案名字是Cone.sln。根据需要选择Release或者Debug版本编译程序。如果想把将VTK整合到其他不采用CMake的工程中,可以直接拷贝该工程的设置到相应的工程中。
下面看一个Windows应用程序示例。其创建过程与上述例子基本相同,除了我们建立的是一个Windows窗口程序而不是控制台程序。大部分代码都是Windows开发者熟悉的标准Windows窗口语言。该例子在VTK/Examples/GUI/Win32/SimpleCxx/Win32Cone.cxx目录下。注意CMakeLists.txt文件中的一个重要变化是ADD_EXECUTABLE命令的WIN32参数。
- #include "windows.h"
- #include "vtkConeSource.h"
- #include "vtkPolyDataMapper.h"
- #include "vtkRenderWindow.h"
- #include "vtkRenderWindowInteractor.h"
- #include "vtkRenderer.h"
- static HANDLE hinst;
- LRESULTCALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
- //define the vtk part as a simple c++ class
- class myVTKApp
- {
- public:
- myVTKApp(HWND parent);
- ~myVTKApp();
- private:
- vtkRenderWindow *renWin;
- vtkRenderer *renderer;
- vtkRenderWindowInteractor *iren;
- vtkConeSource *cone;
- vtkPolyDataMapper *coneMapper;
- vtkActor *coneActor;
- };
程序开始包含进必须的VTK头文件。然后是两个原型声明,接下来定义了一个myVTKApp类。使用C++开发时,要尽量采用面向对象的编程方法,而不是像Tcl例子中的脚本语言样式。这里我们将VTK应用程序的组件封装到一个简单类中。
下面是myVTKApp类的构造函数。首先创建每个VTK对象并进行相应的设置,然后将各个组件对象连接形成可视化管线。除了vtkRenderWindow的代码外,其他都是很简洁的VTK代码。构造函数接收一个父窗口的HWND的句柄以便使父窗口包含VTK渲染窗口。然后利用vtkRenderWindow的SetParentId()函数设置父窗口,并创建自己的窗口作为父窗口的子窗口。
- myVTKApp::myVTKApp(HWNDhwnd)
- {
- // Similar toExamples/Tutorial/Step1/Cxx/Cone.cxx
- // We create the basic parts of a pipelineand connect them
- this->renderer = vtkRenderer::New();
- this->renWin = vtkRenderWindow::New();
- this->renWin->AddRenderer(this->renderer);
- // setup the parent window
- this->renWin->SetParentId(hwnd);
- this->iren =vtkRenderWindowInteractor::New();
- this->iren->SetRenderWindow(this->renWin);
- this->cone = vtkConeSource::New();
- this->cone->SetHeight( 3.0 );
- this->cone->SetRadius( 1.0 );
- this->cone->SetResolution( 10 );
- this->coneMapper =vtkPolyDataMapper::New();
- this->coneMapper->SetInputConnection(this->cone->GetOutputPort());
- this->coneActor = vtkActor::New();
- this->coneActor->SetMapper(this->coneMapper);
- this->renderer->AddActor(this->coneActor);
- this->renderer->SetBackground(0.2,0.4,0.3);
- this->renWin->SetSize(400,400);
- // Finally we start the interactor so thatevent will be handled
- this->renWin->Render();
- }
析构函数中释放所有构造函数中创建的所有VTK对象。
- myVTKApp::~myVTKApp()
- {
- renWin->Delete();
- renderer->Delete();
- iren->Delete();
- cone->Delete();
- coneMapper->Delete();
- coneActor->Delete();
- }
WinMain函数中都是标准的Windows编程语言,没有用到VTK引用。该应用程序中具有消息循环控制功能,消息处理由下面介绍的WinProc函数实现。
- int PASCAL WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
- LPSTR /* lpszCmdParam */,int nCmdShow)
- {
- static char szAppName[] ="Win32Cone";
- HWND hwnd ;
- MSG msg ;
- WNDCLASS wndclass ;
- if (!hPrevInstance)
- {
- wndclass.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC;
- wndclass.lpfnWndProc = WndProc ;
- wndclass.cbClsExtra = 0 ;
- wndclass.cbWndExtra = 0 ;
- wndclass.hInstance = hInstance;
- wndclass.hIcon = LoadIcon(NULL,IDI_APPLICATION);
- wndclass.hCursor = LoadCursor (NULL, IDC_ARROW);
- wndclass.lpszMenuName = NULL;
- wndclass.hbrBackground =(HBRUSH)GetStockObject(BLACK_BRUSH);
- wndclass.lpszClassName = szAppName;
- RegisterClass (&wndclass);
- }
- hinst = hInstance;
- hwnd = CreateWindow ( szAppName,
- "DrawWindow",
- WS_OVERLAPPEDWINDOW,
- CW_USEDEFAULT,
- CW_USEDEFAULT,
- 400,
- 480,
- NULL,
- NULL,
- hInstance,
- NULL);
- ShowWindow (hwnd, nCmdShow);
- UpdateWindow (hwnd);
- while (GetMessage (&msg, NULL, 0, 0))
- {
- TranslateMessage (&msg);
- DispatchMessage (&msg);
- }
- return msg.wParam;
- }
WinProc是一个简单的消息处理函数。对于一个完整的应用程序来说,它可能要比这个复杂的多,但是关键部分都是相同的。函数开始定义了一个myVTKApp实例的静态引用变量。当处理WM_CREATE消息时,我们创建了一个Exit按钮,然后创建myVTKApp实例并传入当前窗口的句柄。vtkRendererWindowInteractor会为vtkRendererWindow处理所有的消息,因此这里不需要处理消息。你很可能想添加代码来处理resizing消息,这样窗口的大小就可以随着用户界面的改变而自动调整。如果没有设置vtkRendererWindow的ParentId的话,vtkRendererWindow就作为一个顶层的独立窗口显示。其他的部分则没有变化。
- LRESULTCALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
- {
- static HWND ewin;
- static myVTKApp *theVTKApp;
- switch (message)
- {
- case WM_CREATE:
- {
- ewin =CreateWindow("button","Exit",
- WS_CHILD | WS_VISIBLE| SS_CENTER,
- 0,400,400,60,
- hwnd,(HMENU)2,
- (HINSTANCE)vtkGetWindowLong(hwnd,vtkGWL_HINSTANCE),
- NULL);
- theVTKApp = new myVTKApp(hwnd);
- return 0;
- }
- case WM_COMMAND:
- switch (wParam)
- {
- case 2:
- PostQuitMessage (0);
- if (theVTKApp)
- {
- delete theVTKApp;
- theVTKApp = NULL;
- }
- break;
- }
- return 0;
- case WM_DESTROY:
- PostQuitMessage (0);
- if (theVTKApp)
- {
- delete theVTKApp;
- theVTKApp = NULL;
- }
- return 0;
- }
- return DefWindowProc (hwnd, message, wParam,lParam);
- }
Unix: Unix下主要通过运行CMake和make来创建应用程序。CMake生成一个makefile文件,该文件中设置了包含路径、链接路径(link lines)和依赖库等。然后make程序利用makefile文件编译应用程序,生成一个可执行文件。如果Cone.txx编译失败,则需要检查和改正代码中的错误。确保CMakeCache.txt文件中开始的变量值是有效的。如果可以编译,但是运行出错的话,则需要参考第二章中设置LD_LIBRARY_PATH变量。
C++中的用户方法:通过实例化vtkCommand的派生类,并重载Execute()函数,你可以添加自己的用户方法(利用Observer/Command设计模式)。下面例子截取自VTK/Examples/Tutorial/Step2/Cxx/Cone2.cxx。
- class vtkMyCallback : public vtkCommand
- {
- public:
- static vtkMyCallback *New() { return new vtkMyCallback; }
- virtual void Execute(vtkObject *caller,unsigned long, void*)
- {
- vtkRenderer *renderer =reinterpret_cast<vtkRenderer*>(caller);
- cout <<renderer->GetActiveCamera()->GetPosition()[0] << " "
- <<renderer->GetActiveCamera()->GetPosition()[1] << " "
- <<renderer->GetActiveCamera()->GetPosition()[2] << "\n";
- }
- };
Execute()函数会传递一个不常使用的caller对象。当你的确需要调用caller时,你需要使用SafeDownCast函数将其转换为其实际的类型。例如:
- virtual void Execute (vtkObject *caller, unsigned long, void *callData)
- {
- vtkRenderer *ren = vtkRenderer::SafeDownCast(caller);
- if (ren) { ren->SetBackground(0.2, 0.3,0.4); }
- }
当创建了你的vtkCommand派生类对象时,你就可以添加一个Observer来监听某个事件。当事件发生时,就会通过Observer执行你的Command命令。如下所示。
- //Hereis where we setup the observer
- //wedo a new and ren1 will eventually free the observer
- vtkMyCallback*mol = vtkMyCallback::New();
- ren1->AddObserver(vtkCommand::StartEvent,mol);
上面代码创建了一个myCallback实例,然后为ren1添加了一个Observer来监听StartEvent事件。当ren1开始渲染时,vtkMyCallback的Execute方法就会被调用。ren1被删除后,Callback对象也会相应的删除。
Java
创建Java应用程序,首先需要一个Java开发环境。本小节主要为Windows或者Unix下利用sun公司JDK1.3或者后续版本开发VTK应用程序提供指导。当JDK和VTK都成功安装后,你需要设置CLASSPATH环境变量来包含VTK类库。微软Windows系统下右击“我的电脑”图标,选择“属性”选项卡,然后选择“高级”tab页,点击“环境变量”按钮。然后添加一个CLASSPATH环境变量,设置值vtk.jar文件路径、Wrapping/java路径及当前路径。Windows下其值如“C:\vtk-bin\bin\vtk.jar;C:\vtk-bin\Wrapping/java;.”。Unix系统下CLASSPATH应设置如“/yourdisk/vtk-bin/bin/vtk.jar;/yourdisk/vtk-bin/Wrapping/java;.”。
接着是字节编译(ByteCompile)Java应用程序。对于新手可以尝试编译(用javac命令)VTK自带例子VTK/Examples/Tutorial/Step1/Java/Cone.java。编译完成后,通过java命令即可运行生成的应用程序。该程序绘制了一个旋转360度的圆锥,然后退出。以该例子作为起点,可以开始创建自己的应用程序了。
- public void myCallback()
- {
- System.out.println("Starting torender");
- }
设置Callback需要三个参数。首先是你关心的事件,第二是类的实例,第三是调用的函数名。在本例中我们设置监听StartEvent事件以调用me(Cone2的实例)的方法myCallback。为了避免错误,确保myCallback函数是Cone2的成员函数(该例子源码见VTK/Examples/Tutorial/Step2/Java/cone2.java)。
- Cone2 me = new Cone2();
- ren1.AddObserver("StartEvent",me, "myCallback");
Python
如果编译VTK时设置支持Python,那么编译后会生成一个vtkpython可执行文件。利用这个可执行文件,可以如下运行“Examples/Tutorial/Step1/Python/Cone.py”。
- vtkpython Cone.py
以VTK自带的示例脚本作为参照来创建Python脚本会比较简单。用户方法通过定义一个函数并将其作为参数传入AddObserver来定义,如下:
- def myCallback(obj, event)
- print "Starting to render"
- ren1.AddObserver("StartEvent",myCallback)
完整的源代码见VTK/Examples/Tutorial/Step2/Python/Cone2.py。
3.3语言间的转换
VTK核心代码采用C++语言实现,然后用Tcl,Java和Python编程语言包装。这样在应用程序开发时,你可以有多种语言选择。语言选择主要依赖于个人的语言习惯,应用程序的特点,是否需要访问内部数据以及是否对性能有特殊要求。与其他语言相比,C++在访问内部数据结构、应用程序执行效率方面具有更多的优势。但是,采用C++语言也带来了编译/链接循环的负担,从而降低了软件开发效率。
你可以使用解释性语言如Tcl来开发应用程序原型,然后再转换为C++程序。或者搜索示例代码(VTK目录或者其他用户)然后将其转换为最终的实现语言。
VTK代码的语言转换比较直接。各个语言中都采用相同的类名和方法名;不同的只是实现细节和GUI接口。例如:
C++语言:
- anActor->GetProperty()->SetColor(red,green, blue)
Tcl语言则为:
- [anActorGetProperty] Set Color $red $green $blue
Java语言为:
- anActor.GetProperty().SetColor(red,green, blue);
Python为:
- anActor->GetProperty().SetColor(red,green, blue);
由于指针操作问题,一些C++应用程序不能转换为其他三种语言,这也是语言转换时的一个主要限制。包装语言可以方便的获取或者设置对象的值,但是不能直接获取一个指针来快速遍历、检查或者修改大的数据结构。如果你的应用程序需要类似的操作,那你可以直接采用C++语言实现,或者利用C++扩展VTK生成你需要的类,然后使用你喜欢的解释性语言来使用新的类。