VTK交互Widget
widget包含两个重要的组成部分:Interaction和Representation.
Interaction是一些名叫vtk*Widget的类(比如vtkBoxWidget2)。它包含了交互的所有选项和事件处理。
Representation是显示并与之交互的一类对象,以名叫vtk*Representation.
在窗口中实现自己的小部件,如果有交互,就要写自己的Representation和Widget。可以参考vtk已有的widget。
vtk已经实现的Widget如下图
由上图可知Widget基类是vtkAbstracWidget,它定义小部件/小部件表示的API。vtkAbstractWidget定义了一个API并实现了所有使用交互/表示设计的widget所共有的方法。在这个设计中,术语交互是指小部件中执行事件处理的部分,而表示则对应于用来表示小部件的vtkProp(或子类vtkWidgetRepresentation)。vtkAbstractWidget还实现了一些所有子类共有的方法。
vtkAbstractWidget 提供对 vtkWidgetEventTranslator 的访问。 此类负责将 VTK 事件(在 vtkCommand.h 中定义)转换为小部件事件(在 vtkWidgetEvent.h 中定义)。 可以操作此类,以便将不同的 VTK 事件映射到小部件事件,从而允许修改事件绑定。 vtkAbstractWidget 的每个子类都定义了它响应的事件。
vtkAbstractWidget 派生自 vtkInteractorObserver。vtkInteractorObserver是一个抽象的超类,用于观察由vtkRenderWindowInteractor调用的事件的子类。这些子类通常是像3D部件这样的东西;与场景中的演员交互的对象,或交互地探测场景的信息。
vtkInteractorObserver定义了SetInteractor()方法,并启用和禁用了vtkInteractorObserver对事件的处理。使用EnabledOn()或SetEnabled(1)方法来打开交互器观察器,使用EnabledOff()或SetEnabled(0)方法来关闭交互器。初始值为0。
为了支持对对象的交互式操作,这个类(和子类)调用了StartInteractionEvent、InteractionEvent和EndInteractionEvent等事件。当vtkInteractorObserver进入一个需要快速反应的状态时,这些事件被调用:鼠标运动等。例如,这些事件可以用来设置所需的更新帧率(StartInteractionEvent),操作数据或更新管道(InteractionEvent),并将所需的帧率设置为正常值(EndInteractionEvent)。另外两个事件,EnableEvent和DisableEvent,在交互器观察者被启用或禁用时被调用。
所以窗口小部件Widget类本质就是一个观察者监听来自Interactor的事件
由上图可知vtkWidgetRepresentation
的基类是vtkProp
,vtkActor2D
的基类是vtkWidgetRepresentation是抽象类定义了小部件和小部件表示类之间的接口
用于定义 API 并部分实现不同类型的小部件的表示。小部件表示(即 vtkWidgetRepresentation 的子类)是一种 vtkProp; 这意味着它们可以像任何其他 vtkActor 一样与嵌入场景中的 vtkRenderer 端相关联。 但是,vtkWidgetRepresentation 还定义了一个 API,使其能够与子类 vtkAbstractWidget 配对,这意味着它可以由小部件驱动,在小部件响应注册事件时用于表示小部件。
此处定义的 API 应被视为实现小部件和小部件表示的指南。 小部件行为很复杂,表示响应已注册小部件事件的方式也很复杂,因此 API 可能因小部件而异,以反映这种复杂性。
创建交互的步骤
虽然每个Widget都提供了不同的功能以及不同的API,但是,Widget的创建以及使用基本都是类似的。创建Widget的一般步骤如下:
- 实例化Widget;
- 指定渲染窗口交互器。Widget可以通过它来监听用户事件。
- 必要时使用观察者/命令模式创建回调函数。与widget交互时,它会调用一些通用的VTK事件(94个事件列表),如StartInteractionEvent、InteractionEvent、EndInteractionEvent。用户通过监听这些事件并作出响应,从而可以更新数据、可视化参数或者应用程序的用户图形界面。
- 创建合适的几何表达实体。并用SetRepresentation()函数把他与Widget关联起来,或者使用Widget默认的几何表达实体。
- 最后,必须激活Widget,使其在渲染场景中显示。默认情况下,按键用于激活Widget,使其可以再场景中可见。
正如之前我们讨论的那样,如果对Widget默认的事件绑定不满意,需要根据自己习惯定义的事件绑定,可以使用VTKWidgetEventTranslator类。同样,也可以使用该类的RemoveTranslation()函数取消已经绑定的事件,具体操作如下:translator->RemoveTranslation(vtkCommand::LeftButtonPressEvent); translator->RemoveTranslation(vtkCommand::LeftButtonReleaseEvent);
如何使用vtkBoxWidget
设置Widget位置
void PlaceWidget(double bounds[6]) override;
获取Box的polyData
void GetPolyData(vtkPolyData* pd);
polydata由6个四边形面和15个点组成。前八个点定义了八个角点顶点;接下来的六个定义了-x、+x、-y、+y、-z、+z面点;最后一点(15个点中的第15个点)定义了六面体的中心(参考上面我标注的图)。当调用InteractionEvent或EndInteractionEvent事件时,这些点值保证是最新的。用户提供vtkPolyData并向其中添加点和单元格。
Box的vtkTransform
virtual void GetTransform(vtkTransform* t);
virtual void SetTransform(vtkTransform* t);
SetTransform设置获取表示长方体变换的线性变换矩阵信息。注意:转换与最初调用PlaceWidget的位置有关。此方法修改提供的变换。变换可用于控制vtkProp3D的位置,以及其他变换操作(例如vtkTransformorMPolyData)
获取Box的plane
void GetPlanes(vtkPlanes* planes);
GetPlanes方法可以获取描述由box小部件定义的隐式函数的平面。用户必须提供类vtkPlanes的实例。需要注意的是:vtkPlanes是vtkImplicitFunction的一个子类,这意味着各种过滤器都可以使用它来执行数据的剪裁、剪切和选择(可以反转平面法线的方向以启用InsideOut标志)。
句柄属性
- HandleProperty是句柄属性(小球就是句柄),可以设置选定和正常时控制柄的属性。
- SelectedHandleProperty是当前被选中的句柄属性。
- FaceProperty是面属性(长方体的面),可以设置选定面和法线时面的特性。
- SelectedFaceProperty是当前被选中的面属性。
- OutlineProperty是轮廓属性(外边框的轮廓),可以设置被选中的和正常时轮廓的属性。
- SelectedOutlineProperty是当前被选中的轮廓属性。
vtkGetObjectMacro(HandleProperty, vtkProperty);
vtkGetObjectMacro(SelectedHandleProperty, vtkProperty);
vtkGetObjectMacro(FaceProperty, vtkProperty);
vtkGetObjectMacro(SelectedFaceProperty, vtkProperty);
vtkGetObjectMacro(OutlineProperty, vtkProperty);
vtkGetObjectMacro(SelectedOutlineProperty, vtkProperty);
控制句柄(表面上的小球体)的显示
void HandlesOn();
void HandlesOff();
控制功能变量
TranslationEnabled/ScalingEnabled/RotationEnabled 用来控制小部件的行为,可以启用和禁用平移、旋转和缩放的行为。
vtkSetMacro(TranslationEnabled, vtkTypeBool);
vtkGetMacro(TranslationEnabled, vtkTypeBool);
vtkBooleanMacro(TranslationEnabled, vtkTypeBool);
vtkSetMacro(ScalingEnabled, vtkTypeBool);
vtkGetMacro(ScalingEnabled, vtkTypeBool);
vtkBooleanMacro(ScalingEnabled, vtkTypeBool);
vtkSetMacro(RotationEnabled, vtkTypeBool);
vtkGetMacro(RotationEnabled, vtkTypeBool);
vtkBooleanMacro(RotationEnabled, vtkTypeBool);
vtkBoxWidget2 相关源码分析
vtkBoxWidget2构造函数
构造函数主要定义了widget的事件,并将VTK事件翻译成Widget事件,从下面的代码可以看出使用SetCallbackMethod
将VTK消息与实际的操作函数联系起来SetCallbackMethod
调用了vtkWidgetEventTranslator::SetTranslation()
方法将VTK事件翻译成Widget事件。这种机制有点类似Qt里面的信号槽连接。
// Define widget events
this->CallbackMapper->SetCallbackMethod(vtkCommand::LeftButtonPressEvent, vtkEvent::NoModifier, 0,
0, nullptr, vtkWidgetEvent::Select, this, vtkBoxWidget2::SelectAction);
this->CallbackMapper->SetCallbackMethod(vtkCommand::LeftButtonReleaseEvent, vtkEvent::NoModifier,
0, 0, nullptr, vtkWidgetEvent::EndSelect, this, vtkBoxWidget2::EndSelectAction);
this->CallbackMapper->SetCallbackMethod(vtkCommand::MiddleButtonPressEvent,
vtkWidgetEvent::Translate, this, vtkBoxWidget2::TranslateAction);
this->CallbackMapper->SetCallbackMethod(vtkCommand::MiddleButtonReleaseEvent,
vtkWidgetEvent::EndTranslate, this, vtkBoxWidget2::EndSelectAction);
this->CallbackMapper->SetCallbackMethod(vtkCommand::LeftButtonPressEvent,
vtkEvent::ControlModifier, 0, 0, nullptr, vtkWidgetEvent::Translate, this,
vtkBoxWidget2::TranslateAction);
事件处理函数
// These methods handle events
static void SelectAction(vtkAbstractWidget*);
static void EndSelectAction(vtkAbstractWidget*);
static void TranslateAction(vtkAbstractWidget*);
static void ScaleAction(vtkAbstractWidget*);
static void MoveAction(vtkAbstractWidget*);
static void SelectAction3D(vtkAbstractWidget*);
static void EndSelectAction3D(vtkAbstractWidget*);
static void MoveAction3D(vtkAbstractWidget*);
static void StepAction3D(vtkAbstractWidget*);
vtkBoxRepresentation 相关代码分析
BoxWidget中15个点的顺序如下图所示,便于后面代码分析。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XJVk2Njn-1683711832364)(https://note.youdao.com/yws/res/32343/WEBRESOURCE75e8d6e859af96038582a32aa177995a)]
vtkBoxRepresentation的构造函数中主要初始化连接渲染Box的管道。如上图可知Box中有6个面,15条线段,15个点。
vtkBoxRepresentation中相关变量介绍如下
//交互box的状态控制状态定义
enum
{
Outside = 0,
MoveF0,
MoveF1,
MoveF2,
MoveF3,
MoveF4,
MoveF5,
Translating,
Rotating,
Scaling
};
// 构建6个面变量
vtkActor* HexActor;
vtkPolyDataMapper* HexMapper;
vtkPolyData* HexPolyData;
vtkPoints* Points; // 8 corners; 6 faces; 1 center
double N[6][3]; // 6个面的方向。存储顺序是(-x,+x,-y,+y,-z,+z)
//被选中的六面体其中的一个面的相关变量
vtkActor* HexFace;
vtkPolyDataMapper* HexFaceMapper;
vtkPolyData* HexFacePolyData;
// Handle 15个点的相关变量
vtkActor** Handle;
vtkPolyDataMapper** HandleMapper;
vtkSphereSource** HandleGeometry;
// 单元拾取相关,也是交互重要环节之一。
vtkCellPicker* HandlePicker;
vtkCellPicker* HexPicker;
vtkActor* CurrentHandle;
int CurrentHexFace;
vtkCellPicker* LastPicker;
vtkBoxRepresentation中相关API
//获取包围盒
double* GetBounds() VTK_SIZEHINT(6) override;
//窗口交互
void WidgetInteraction(double e[2]) override;
//复杂的交互
void ComplexInteraction(vtkRenderWindowInteractor* iren, vtkAbstractWidget* widget,
unsigned long event, void* calldata) override;
//计算交互状态
int ComputeComplexInteractionState(vtkRenderWindowInteractor* iren, vtkAbstractWidget* widget,
unsigned long event, void* calldata, int modify = 0) override;
//结束交互状态
void EndComplexInteraction(vtkRenderWindowInteractor* iren, vtkAbstractWidget* widget,
unsigned long event, void* calldata) override;
// 帮助函数,也就是具体实现box平移,旋转,缩放的具体函数实现
virtual void Translate(const double* p1, const double* p2);
virtual void Scale(const double* p1, const double* p2, int X, int Y);
virtual void Rotate(int X, int Y, const double* p1, const double* p2, const double* vpn);
void MovePlusXFace(const double* p1, const double* p2);
void MoveMinusXFace(const double* p1, const double* p2);
void MovePlusYFace(const double* p1, const double* p2);
void MoveMinusYFace(const double* p1, const double* p2);
void MovePlusZFace(const double* p1, const double* p2);
void MoveMinusZFace(const double* p1, const double* p2);
void UpdatePose(const double* p1, const double* d1, const double* p2, const double* d2);
void MoveFace(const double* p1, const double* p2, const double* dir, double* x1, double* x2,
double* x3, double* x4, double* x5);
Rotate 实现分析
void vtkBoxRepresentation::Rotate(
int X, int Y, const double* p1, const double* p2, const double* vpn)
{
double* pts = static_cast<vtkDoubleArray*>(this->Points->GetData())->GetPointer(0);
double* center = static_cast<vtkDoubleArray*>(this->Points->GetData())->GetPointer(3 * 14);
double v[3]; // vector of motion
double axis[3]; // axis of rotation
double theta; // rotation angle
int i;
v[0] = p2[0] - p1[0];
v[1] = p2[1] - p1[1];
v[2] = p2[2] - p1[2];
// Create axis of rotation and angle of rotation
vtkMath::Cross(vpn, v, axis);
if (vtkMath::Normalize(axis) == 0.0)
{
return;
}
//根据移动的L2距离和屏幕的尺寸L2比例来确定旋转的角度
const int* size = this->Renderer->GetSize();
double l2 = (X - this->LastEventPosition[0]) * (X - this->LastEventPosition[0]) +
(Y - this->LastEventPosition[1]) * (Y - this->LastEventPosition[1]);
theta = 360.0 * sqrt(l2 / (size[0] * size[0] + size[1] * size[1]));
// 旋转变换,VTK Transform的标准操作
this->Transform->Identity();
this->Transform->Translate(center[0], center[1], center[2]);
this->Transform->RotateWXYZ(theta, axis);
this->Transform->Translate(-center[0], -center[1], -center[2]);
// 设置拐角点
// 首先把Points中的点进行transform
//得到新的点,并跟新
vtkPoints* newPts = vtkPoints::New(VTK_DOUBLE);
this->Transform->TransformPoints(this->Points, newPts);
for (i = 0; i < 8; i++, pts += 3)
{
this->Points->SetPoint(i, newPts->GetPoint(i));
}
newPts->Delete();
//根据这8个点去计算其他要设置的点,并更新
this->PositionHandles();
}
vtkBoxWidget 例子
- BoxWidget2.cxx
#include <vtkActor.h>
#include <vtkBoxRepresentation.h>
#include <vtkBoxWidget2.h>
#include <vtkCommand.h>
#include <vtkConeSource.h>
#include <vtkInteractorStyleTrackballCamera.h>
#include <vtkNamedColors.h>
#include <vtkNew.h>
#include <vtkPolyDataMapper.h>
#include <vtkProperty.h>
#include <vtkRenderWindow.h>
#include <vtkRenderWindowInteractor.h>
#include <vtkRenderer.h>
#include <vtkTransform.h>
#include <vtkOrientationMarkerWidget.h>
#include <vtkAxesActor.h>
#include <vtkPlanes.h>
namespace {
class vtkBoxCallback : public vtkCommand
{
public:
static vtkBoxCallback* New()
{
return new vtkBoxCallback;
}
vtkSmartPointer<vtkActor> m_actor;
void SetActor(vtkSmartPointer<vtkActor> actor)
{
m_actor = actor;
}
virtual void Execute(vtkObject* caller, unsigned long, void*)
{
vtkSmartPointer<vtkBoxWidget2> boxWidget =
dynamic_cast<vtkBoxWidget2*>(caller);
vtkNew<vtkTransform> t;
dynamic_cast<vtkBoxRepresentation*>(boxWidget->GetRepresentation())
->GetTransform(t);
this->m_actor->SetUserTransform(t);
}
vtkBoxCallback()
{
}
};
} // namespace
int main(int vtkNotUsed(argc), char* vtkNotUsed(argv)[])
{
vtkNew<vtkNamedColors> colors;
// 显示坐标系的vtk组件
vtkSmartPointer<vtkAxesActor> axes_actor = vtkSmartPointer<vtkAxesActor>::New();
axes_actor->SetPosition(0, 0, 0);
axes_actor->SetTotalLength(2, 2, 2);
axes_actor->SetShaftType(0);
axes_actor->SetCylinderRadius(0.02);
vtkNew<vtkConeSource> coneSource;
coneSource->SetHeight(1.5);
vtkNew<vtkPolyDataMapper> mapper;
mapper->SetInputConnection(coneSource->GetOutputPort());
vtkNew<vtkActor> actor;
actor->SetMapper(mapper);
actor->GetProperty()->SetColor(colors->GetColor3d("BurlyWood").GetData());
vtkNew<vtkRenderer> renderer;
renderer->AddActor(actor);
//renderer->SetBackground(colors->GetColor3d("Blue").GetData());
renderer->SetBackground(colors->GetColor3d("Black").GetData());
renderer->ResetCamera(); // Reposition camera so the whole scene is visible
vtkNew<vtkRenderWindow> renderWindow;
renderWindow->AddRenderer(renderer);
renderWindow->SetWindowName("BoxWidget2");
vtkNew<vtkRenderWindowInteractor> renderWindowInteractor;
renderWindowInteractor->SetRenderWindow(renderWindow);
// Use the "trackball camera" interactor style, rather than the default
// "joystick camera"
vtkNew<vtkInteractorStyleTrackballCamera> style;
renderWindowInteractor->SetInteractorStyle(style);
vtkNew<vtkBoxWidget2> boxWidget;
boxWidget->SetInteractor(renderWindowInteractor);
boxWidget->GetRepresentation()->SetPlaceFactor(1); // Default is 0.5
boxWidget->GetRepresentation()->PlaceWidget(actor->GetBounds());
vtkNew<vtkPlanes> planes;
vtkBoxRepresentation::SafeDownCast(boxWidget->GetRepresentation())->GetPlanes(planes);
// Set up a callback for the interactor to call so we can manipulate the actor
vtkNew<vtkBoxCallback> boxCallback;
boxCallback->SetActor(actor);
boxWidget->AddObserver(vtkCommand::InteractionEvent, boxCallback);
boxWidget->On();
// 控制坐标系,使之随视角共同变化
vtkSmartPointer<vtkOrientationMarkerWidget> widget = vtkSmartPointer<vtkOrientationMarkerWidget>::New();
widget->SetOrientationMarker(axes_actor);
widget->SetInteractor(renderWindowInteractor);
widget->On();
renderWindow->Render();
renderWindowInteractor->Start();
return EXIT_SUCCESS;
}
- CMakeLists.txt
cmake_minimum_required(VERSION 3.12 FATAL_ERROR)
project(BoxWidget2)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/../../../bin)
set(VTK_DIR S:/VTK-build)
find_package(ITK REQUIRED)
IF(ITK_FOUND)
INCLUDE(${ITK_USE_FILE})
ELSE(ITK_FOUND)
MESSAGE(FATAL_ERROR
"ITK not found. Please set ITK_DIR.")
ENDIF(ITK_FOUND)
#message("ITK_USE_FILE BoxWidget2: ${ITK_USE_FILE}")
find_package(VTK REQUIRED)
if (NOT VTK_FOUND)
message("Skipping BoxWidget2: ${VTK_NOT_FOUND_MESSAGE}")
return()
endif()
message (STATUS "VTK_VERSION: ${VTK_VERSION}")
if (VTK_VERSION VERSION_LESS "8.90.0")
# old system
include(${VTK_USE_FILE})
add_executable(BoxWidget2 MACOSX_BUNDLE BoxWidget2.cxx )
target_link_libraries(BoxWidget2 PRIVATE ${VTK_LIBRARIES})
else()
# Prevent a "command line is too long" failure in Windows.
set(CMAKE_NINJA_FORCE_RESPONSE_FILE "ON" CACHE BOOL "Force Ninja to use response files.")
add_executable(BoxWidget2 MACOSX_BUNDLE BoxWidget2.cxx )
target_link_libraries(BoxWidget2 PRIVATE ${VTK_LIBRARIES} ${ITK_LIBRARIES})
# vtk_module_autoinit is needed
vtk_module_autoinit(
TARGETS BoxWidget2
MODULES ${VTK_LIBRARIES}
)
endif()