1.前言
一個強大的可視化系統不僅需要強大的數據處理能力,也需要方便易用的交互功能。圖形處理軟件ParaView(hhttp://www.paraview.org)、德國癌症研究中心研發的MITK(http://www.mitk.org)等開源軟件系統都提供了強大的交互功能,作爲ParaView、MITK等軟件構件基礎的VTK同樣也提供了各種各樣的交互功能。
VTK的交互除了可以監聽來自鼠標、鍵盤等外部設備的消息,還可以在渲染場景中生成功能各異的交互部件(Widget),用於控制可視化過程的參數,達到用戶的渲染要求。
2.觀察者/命令模式(Observe/Command)
觀察者/命令模式是VTK裏用的比較多的設計模式。
VTK中絕大多數的類都派生自vtkObject。查看類vtkObject的接口可以找到AddObserve()、RemoveObserve()、GetCommand()等函數。
觀察者/命令模式是指一個Object可以有多個Observe,他定義了對象間的一種“一對多”的依賴關係,當一個Object對象的狀態發生改變時,所有依賴於它的Observe對象都得到通知而被自動更新。命令模式屬於對象行爲模式,他將一個請求封裝爲一個對象,並提供一致性發送請求的接口,當一個事件發生時,他不直接把事件傳遞給事件調用者,而是在命令和調用者之間增加一箇中間者,講這種直接關係切斷,同時將兩者都隔離。事件調用者只是和接口打交道,不和具體事件實現交互。
在VTK中,可以通過兩種方式來實現觀察者/命令模式,他們分別是使用時間回調函數、從VTKCommand派生出具體的子類。
2.1 觀察者-事件回調方案在vtkObject中,有如下函數:
unsigned long AddObserver(unsigned long event, vtkCommand *, float priority = 0.0f); unsigned long AddObserver(const char* event, vtkCommand *, float priority = 0.0f);
AddObserver()函數的作用就是針對某個事件添加掛插着到某個VTK對象中。
當該對象發生觀察者感興趣的事件時,就會自動調用回調函數,執行相關操作。
下面是一個非常簡單的例子演示了在VTK裏是如何使用“觀察者-事件回調函數”方案的:
#include <vtkAutoInit.h> VTK_MODULE_INIT(vtkRenderingOpenGL); VTK_MODULE_INIT(vtkInteractionStyle); #include <vtkCallbackCommand.h> long cntPress = 0; void MyCallbackFunc(vtkObject*, unsigned long eid, void* clientdata, void* calldata) { std::cout << "You have clicked : " << ++cntPress << " times" << std::endl; } #include <vtkSmartPointer.h> #include <vtkPNGReader.h> #include <vtkImageViewer2.h> #include <vtkRenderer.h> #include <vtkRenderWindow.h> #include <vtkRenderWindowInteractor.h> int main() { vtkSmartPointer<vtkPNGReader> reader = vtkSmartPointer<vtkPNGReader>::New(); reader->SetFileName("vtk.png"); reader->Update(); vtkSmartPointer<vtkImageViewer2> viewer = vtkSmartPointer<vtkImageViewer2>::New(); viewer->SetInputData(reader->GetOutput()); viewer->GetRenderer()->SetBackground(0, 0, 0); viewer->SetSize(480, 320); viewer->GetRenderWindow()->SetWindowName("Observer-Callback"); vtkSmartPointer<vtkRenderWindowInteractor> rwi = vtkSmartPointer<vtkRenderWindowInteractor>::New(); viewer->SetupInteractor(rwi); viewer->Render(); /*************************************************************/ //Step1:設置事件回調函數 vtkSmartPointer<vtkCallbackCommand> mouseCallback = vtkSmartPointer<vtkCallbackCommand>::New(); mouseCallback->SetCallback(MyCallbackFunc); //很重要!!! //Step2:將vtkCallbackCommand對象添加到觀察者列表。 rwi->SetRenderWindow(viewer->GetRenderWindow()); //喚醒顯示窗口 rwi->AddObserver(vtkCommand::LeftButtonPressEvent, mouseCallback); rwi->Initialize(); rwi->Start(); return 0; }
輸出結果:
通過上例,我們可以總結一下“觀察者-回調函數”交互方案主要可以分爲以下三個步驟:
1.定義回調函數。
回調函數的函數簽名只能是以下形式:
void long MyCallbackFunc(vtkObject* obj, unsigned long eid, void* clientdata, void* calldata);
其中,
obj:是調用事件的對象(即調用AddObserver()函數的對象,對應於本例的rwi);
eid:是所要監聽的事件ID,VTK中的事件定義在vtkCommand.h文件中;
clientdata:是與VTKCallbackCommand實例相關的數據,簡單地說,是指回調函數裏需要訪問主程序裏面得數據時,由主程序向回調函數傳遞的數據。
calldata:是執行vtkObject::InvokeEvent()函數時,隨着回調函數發送得數據,比如說,當調用ProgressEvent事件時,會自動發送當前的進度值作爲callback。
2.創建一個VTKCallbackCommand對象,並調用VTKCallbackCommand::SetCallback()函數設置所定義的回調函數。
//Step1:設置事件回調函數 vtkSmartPointer<vtkCallbackCommand> mouseCallback = vtkSmartPointer<vtkCallbackCommand>::New(); mouseCallback->SetCallback(MyCallbackFunc); //很重要!!!
3.將VTKCallbackCommand對象添加到對象的觀察者列表中。
//Step2:將vtkCallbackCommand對象添加到觀察者列表。 rwi->SetRenderWindow(viewer->GetRenderWindow()); //喚醒顯示窗口 rwi->AddObserver(vtkCommand::LeftButtonPressEvent, mouseCallback);
注意:
VTK中的vtkRenderWindowInteractor提供了一種獨立於平臺的交互機制,用來響應不同平臺的鼠標、按鍵和時鐘等消息。當渲染窗口中有事件發生時(比如說單機消息),vtkRenderWindowInteractor內部會調用與平臺相關的子類,將該消息轉換成對應平臺的消息。
因此,該例的核心在於:通過vtkRenderWindowInteractor來監聽鼠標左鍵的消息,一旦監聽到對象的觀察者列表中的消息時,程序會自動調用事件回調函數。
2.2 vtkCommand子類
觀察者/命令模式除了使用事件回調函數外,還可以直接從vtkCommand類中派生出子類來實現。
示例代碼如下:
#include <vtkAutoInit.h> VTK_MODULE_INIT(vtkRenderingOpenGL); VTK_MODULE_INIT(vtkInteractionStyle); #include <vtkCommand.h> #include <vtkSmartPointer.h> #include <vtkConeSource.h> #include <vtkPolyDataMapper.h> #include <vtkActor.h> #include <vtkRenderer.h> #include <vtkRenderWindow.h> #include <vtkRenderWindowInteractor.h> #include <vtkInteractorStyleTrackballCamera.h> class myCallback :public vtkCommand { public: static myCallback* New() { return new myCallback; } void SetObject(vtkConeSource* cone) { m_cone = cone; } virtual void Execute(vtkObject* caller, unsigned long eventId, void* callData) { std::cout << "LeftButton has been pressed: " << std::endl << "The Height: " << m_cone->GetHeight() << std::endl << "The Radius: " << m_cone->GetRadius() << std::endl; } private: vtkConeSource* m_cone; }; int main() { vtkSmartPointer<vtkConeSource> cone = vtkSmartPointer<vtkConeSource>::New(); cone->SetHeight(10); cone->SetRadius(4); cone->Update(); /*************************************************************/ vtkSmartPointer<vtkPolyDataMapper> mapper = vtkSmartPointer<vtkPolyDataMapper>::New(); mapper->SetInputConnection(cone->GetOutputPort()); vtkSmartPointer<vtkActor> actor = vtkSmartPointer<vtkActor>::New(); actor->SetMapper(mapper); vtkSmartPointer<vtkRenderer> render = vtkSmartPointer<vtkRenderer>::New(); render->AddActor(actor); render->SetBackground(1, 1, 1); vtkSmartPointer<vtkRenderWindow> rw = vtkSmartPointer<vtkRenderWindow>::New(); rw->AddRenderer(render); rw->SetWindowName("Interaction By SubCommand"); rw->SetSize(320, 320); vtkSmartPointer<vtkRenderWindowInteractor> rwi = vtkSmartPointer<vtkRenderWindowInteractor>::New(); rwi->SetRenderWindow(rw); //////////////////////// vtkSmartPointer<vtkInteractorStyleTrackballCamera> style = vtkSmartPointer<vtkInteractorStyleTrackballCamera>::New(); rwi->SetInteractorStyle(style); vtkSmartPointer<myCallback> callback = vtkSmartPointer<myCallback>::New(); callback->SetObject(cone); rwi->AddObserver(vtkCommand::LeftButtonPressEvent, callback); rwi->Initialize(); rwi->Start(); return 0; }
輸出結果爲:
這個例子實現的功能也是監聽鼠標左鍵單機的消息。
基於“觀察者-vtkCommand子類”的實現方案也遵循三個步驟。
1. 從vtkCommand類中派生出子類,並實現vtkCommand::Execute()虛函數。
該函數原型爲:
virtual void Execute(vtkObject* caller, unsigned long eventId, void* callData )= 0;
Execute()時純虛函數,所以,從vtkCommand派生類中都必須要實現這個方法。
2.實例化vtkCommand子類的對象,並調用相關的方法。
3.調用觀察者函數。
調用AddObserver()函數監聽感興趣的事件,如果所監聽的事件發生,就會調用vtkCommand子類中定義的Execute()函數。
因此,針對所監聽的事件,程序需要把實現的功能放在Execute函數中。