1、交互器样式
前面所讲的观察者/命令模式是 VTK实现交互的方式之一。在前面示例 所示的窗口中可以使用鼠标与柱体进行交互,比如用鼠标滚轮可以对柱体放大、缩小;按下鼠标左键不放,然后移动鼠标,可以转动柱体;按下鼠标左键,同时按下(Shif)键,移动鼠标,可以移动整个柱体;按下〈Ctrl)键时,再按下鼠标左键可以实现旋转功能;鼠标停留在柱体上然后按下(P)键可以实现对象的选取;按下〈E)键可以退出 VTK应用程序等。
2、vtkRenderWindowInteractor
vtkRenderWindowInteractor 类即渲染窗口交互器,它提供一种平台独立的响应鼠标/按键/时钟事件的交互机制,可将平台相关的鼠标/按键/时钟等消息路由至vtkInteractorObserver 或其子类。也就是说,vkRenderWindowInteractor作为一个基类,其具体的功能是由平台相关的子类(如 vtkWin32RenderWindowInteractor)来完成的。当它从窗口系统中监听到感兴趣的事件(消息)时,通过调用InvokeEvent()函数将平台相关的事件翻译成VTK事件,而这些 VTK 事件是平台独立的,然后再路由至 vtkInteractorObserver 或其子类,再由已经对该事件进行注册的 vtkInteractorObserver 或其子类响应具体的操作。
1)示例代码
private void TestInteraction()
{
vtkJPEGReader reader = vtkJPEGReader.New();
reader.SetFileName("F:\\code\\VTK\\TestActiViz\\data\\VTKBook-TestImage.jpg");
reader.Update();
vtkImageActor imageActor = vtkImageActor.New();
imageActor.SetInputData(reader.GetOutput());
vtkRenderer renderer = vtkRenderer.New();
renderer.AddActor(imageActor);
renderer.SetBackground(1, 1, 1);
vtkRenderWindow renWin = new vtkRenderWindow();// renderWindowControl.RenderWindow;
renWin.AddRenderer(renderer);
renWin.SetSize(640, 480);
this.Dispatcher.Invoke(() => renWin.Render());
renWin.SetWindowName("InteractionDemo");
vtkRenderWindowInteractor iren = vtkRenderWindowInteractor.New();
iren.SetRenderWindow(renWin);
//该交互模具工预设了针对二维图像的交互功能,如同时按下《Ctrl》键和鼠标左键可以实现图像的旋转等。
vtkInteractorStyleImage style = vtkInteractorStyleImage.New();
iren.SetInteractorStyle(style);
iren.Initialize();
// this.Dispatcher.BeginInvoke(new Action( kkk), null);
iren.Start();
}
2)效果
3)说明
示例先读入一幅 JPG 图像,然后用 vtkImageActor、vtkRenderervtkRenderWindow等建立可视化管线。值得注意的是,在以上示例中,使用类vtknteractorStylelmage 作为交互器样式。该交互器样式预设了针对二维图像的交互功能,如同时按下〈Ctrl)键和鼠标左键可以实现图像的旋转;同时按下(Shif)键和鼠标左键可以实现图像平移;按住鼠标左键并移动鼠标可以调节图像的窗宽和窗位;按(R)键可以实现图像的窗宽和窗位的重置;滑动鼠标滚轮可以实现图像的放缩等。
vkRenderWindowInteractor 是一个基类,具体的操作是由平台相关的子类实现。该示例程序是运行于Win32平台下的,因此,该平台下的消息先由vtkWin32RenderWindowInteractor 类捕获。这里以窗宽和窗位的重置功能为例,跟踪当用户按下〈R)键时,消息是如何传递的。
首先分析当用户在渲染窗口中按下(R)键时,可能引发的消息有哪些。VTK染窗口在获得焦点的前提下,当用户按下(R〉键,先是触发了“按键按下”的消息,即Windows下的 WM KEYDOWN;然后触发 WM CHAR消息(这里先不考虑 WM KEYUP 消息)。
主程序中实例化的是vtkRenderWindowInteractor 对象,程序调用的却是 vtkWin32RenderWindowInteractor 对象,VTK里是如何根据具体的平台来调用相关的类的呢?
代码是调用 vtkGraphicsFactory::CreateInstance()函数来创建 vtkRenderWindowInteractor,从类的名字可以看出,这是从对象工厂中创建所需的对象实例。
代码先根据对象类名从对象工厂中创建实例,如果成功创建即返回。会发现由 vtkObjectFactory::CreateInstance(vtkclassname)的返回值是0。后面即调用 vtkGraphicsFactory::GetRenderLibrary()来获取当前请求的渲染库类型。再继续,可以看出该返回值为“Win32OpenGL”从 vtkGraphicsFactory.cxx文件里的 vtkGraphicsFactory::CreateInstance()中。
InteractionDemo 中调用了 vtkRenderWindowInteractor 的 Start()函数,该函数会调用一个名为StartEventLoop的虚函数。vtkWin32RenderWindowInteractor 覆盖了该函数。
在函数的最后就是一个列循环,即不断地调用Windows的APIGetMessage()函数从消息队列中获取消息,并将所获取的消息进行转换,再分发到当前的窗口程序中。
4)总结
当在主程序中实例化 vkRenderWindowInteractor对象时,VTK 程序内部根据不同平台的渲染库实例化平台相关的 vtkRenderWindowInteractor 子类,由具体的平台相关的子类来响应窗口消息(如 vtkWin32RenderWindowInteractor)。vtkWin32RenderWindowInteractor::StartEventLoop()函数不断地从消息队列中获取消息,并分发给该类的回调函数vtkHandleMessage2(),该回调函数根据不同的消息调用相应的 OnXXXO消息响应函数(XXX指代消息名字),在每个消息响应函数里,通过调用 vtkObiect::InvokeEvent()将平台相关的消息再翻译成 VTK 事件,如按键按下的事件为 vtkCommand::KeyPressEvent。
3、vtkInteractorStyle
继续以WMKEYDOWN消息为例,当示例程序停留在vtkWin32RenderWindowInteractor::OnKeyDown()函数的 InvokeEvent()处(即该类源文件的第600行)时,按(F11)键进入类vtkObiect的函数InvokeEvent(),代码如下:
vtkObject::InvokeEvent()实际上调用的是SubjectHelper的同名函数。在变量名SubjectHelper 上右击,从弹出的快捷菜单中选择“Go To Definition”命令,可以看到该变量类型为 vtkSubjectHelper。该类在 vtkObject 内部定义,其主要作用是用于保存观察者(Observer)的列表,并负责注册事件,将事件分发给观察者。而vkSubiectHelper类内部事件的分发,则是由另一个辅助类 vtkObserver 来完成的,这个类也是在 vtkObiect 内部定义的。继续按(F11)键,跳至类vtkSubjectHelper::InvokeEventO函数体中,代码如下:
int vtkSubjectHelper::InvokeEvent(unsigned long event, void *callData,
vtkObject *self)
{
int focusHandled = 0;
int saveListModified = this->ListModified;
this->ListModified = 0;
typedef std::vector<unsigned long> VisitedListType;
VisitedListType visited;
vtkObserver *elem = this->Start;
const unsigned long maxTag = this->Count;
vtkObserver *next;
while (elem)
{
next = elem->Next;
if (elem->Command->GetPassiveObserver() &&
(elem->Event == event || elem->Event == vtkCommand::AnyEvent) &&
elem->Tag < maxTag)
{
VisitedListType::iterator vIter =
std::lower_bound(visited.begin(), visited.end(), elem->Tag);
if (vIter == visited.end() || *vIter != elem->Tag)
{
// Sorted insertion by tag to speed-up future searches at limited
// insertion cost because it reuses the search iterator already at the
// correct location
visited.insert(vIter, elem->Tag);
vtkCommand* command = elem->Command;
command->Register(command);
elem->Command->Execute(self,event,callData);
command->UnRegister();
}
}
if (this->ListModified)
{
vtkGenericWarningMacro(<<"Passive observer should not call AddObserver or RemoveObserver in callback.");
elem = this->Start;
this->ListModified = 0;
}
else
{
elem = next;
}
}
// 1. Focus loop
//
if (this->Focus1 || this->Focus2)
{
elem = this->Start;
while (elem)
{
next = elem->Next;
if (((this->Focus1 == elem->Command) || (this->Focus2 == elem->Command)) &&
(elem->Event == event || elem->Event == vtkCommand::AnyEvent) &&
elem->Tag < maxTag)
{
VisitedListType::iterator vIter =
std::lower_bound(visited.begin(), visited.end(), elem->Tag);
if (vIter == visited.end() || *vIter != elem->Tag)
{
// Don't execute the remainder loop
focusHandled = 1;
// Sorted insertion by tag to speed-up future searches at limited
// insertion cost because it reuses the search iterator already at the
// correct location
visited.insert(vIter, elem->Tag);
vtkCommand* command = elem->Command;
command->Register(command);
command->SetAbortFlag(0);
elem->Command->Execute(self,event,callData);
// if the command set the abort flag, then stop firing events
// and return
if(command->GetAbortFlag())
{
command->UnRegister();
this->ListModified = saveListModified;
return 1;
}
command->UnRegister();
}
}
if (this->ListModified)
{
elem = this->Start;
this->ListModified = 0;
}
else
{
elem = next;
}
}
}
// 2. Remainder loop
//
if (!focusHandled)
{
elem = this->Start;
while (elem)
{
// store the next pointer because elem could disappear due to Command
next = elem->Next;
if ((elem->Event == event || elem->Event == vtkCommand::AnyEvent) &&
elem->Tag < maxTag)
{
VisitedListType::iterator vIter =
std::lower_bound(visited.begin(), visited.end(), elem->Tag);
if (vIter == visited.end() || *vIter != elem->Tag)
{
// Sorted insertion by tag to speed-up future searches at limited
// insertion cost because it reuses the search iterator already at the
// correct location
visited.insert(vIter, elem->Tag);
vtkCommand* command = elem->Command;
command->Register(command);
command->SetAbortFlag(0);
elem->Command->Execute(self,event,callData);
// if the command set the abort flag, then stop firing events
// and return
if(command->GetAbortFlag())
{
command->UnRegister();
this->ListModified = saveListModified;
return 1;
}
command->UnRegister();
}
}
if (this->ListModified)
{
elem = this->Start;
this->ListModified = 0;
}
else
{
elem = next;
}
}
}
this->ListModified = saveListModified;
return 0;
}
该函数的实现比较复杂,这里只需关注三个while循环体里的if语句。以上函数在处理事件时,将事件观察者分为三类,分别是被动观察者(Passive)、焦点观察者(Focus)及其他类型。被动观察者是指其所监听的事件或命令是不改变系统状态的,可以通过vtkCommand::GetPassiveObserver()获取该标志的值;焦点观察者是指该观察者所监听的事件可以让窗口获得焦点,比如,用户用鼠标单击窗口后,窗口可以获得焦点,则监听VTK 事件LeftButtonPressEvent(见表8-1)的观察者即为焦点观察者。
显然,监听WM KEYDOWN 消息所对应的VTK事件KeyPressEvent的观察者是以上两种观察者之外的类型。所以,在以上代码的第三个循环体(第602行代码)放置一个断点,然后按(F5)键运行程序。
这时,程序会停留在放置的断点位置上(第602行),这时可以在VS2008窗口中将vtkSubjectHelper:InvokeEvent()里的参数event拖至变量的观测窗口中看看该变量的值。该事件的值为20,对照表8-1可知编号为20的事件为KeyPressEvent。继续按(F11〉键,程序跳至类vtkCallbackCommand::Excute()函数中,代码如下:
变量 Callback 的值是在 vtkSubjectHelper:InvokeEvent()函数里赋值的。继续按(F11)键,程序正如变量 Callback 的值所示,将跳至 vtkInteractorStyle::ProcessEvents(函数中,代码如下:
vtkInteractorStyle::ProcessEvents()函数很长,但并不复杂。从上述代码可以看出,该函数主要就是一个 switch 语句,根据不同的 VTK事件,调用 vtkInteractorStyle 不同的函数进行响应,比如所跟踪的 KeyPressEvent 事件,程序将调用 OnKeyDown()和 OnKeyPress()函数进行响应,而响应 VTK 事件的函数都声明为虚函数,换言之,这些事件都是在 vtkInteractorStyle的子类中实现的。对于KeyPressEvent 事件,vtkInteractorStyle 的子类vtkInteractorStyleTrackballCamera 和 vtkInteractorStylelmage 都没有重载 OnKeyDown()和 OnKeyPress()函数。
前面提过,当用户在渲染窗口中按下(R)键时,先触发VTK的KeyPressEvent 事件,然后触发 CharEvent 事件。
OnChar()是虚函数,vtkInteractorStyle 的子类 vtkInteractorStylelmage 已经覆盖了该函数,而且刚好子类vtkInteractorStylelmage::OnChar()函数中有针对(R〉键的响应,所以父类的 OnChar()函数也就不调用了,代码如下:
从上述代码可以看出,响应用户(R)键消息的OnChar0)函数实际实现的功能就是设置
图像的窗宽和窗位等信息(代码第440~443行)。
至此,键盘按键按下消息(KeyPressEvent和CharEvent)的传递过程已经比较清晰了。
总结如下:Windows 消息被 vtkWin32RenderWindowInteractor 捕获以后,先由该类的回调函数 vtkHandleMessage2()分发至各个消息响应函数,在每个消息响应函数的最后,通过调用vtkObject::InvokeEvent()将 Windows 消息翻译成 VTK 事件。
在 vkObject::InvokeEvent()函数里,通过类 vtkSubjectHelper::InvokeEvent()函数再将各个 VTK 事件分发到不同的观察者中,观察者调用回调函数 vtkInteractorStyle::ProcessEvents()处理不同的 VTK事件,再将这些VTK 事件分发至 vtkInteractorStyle 或其子类的消息响应函数中,从而完成整个消息的传递过程。