自定义 Interactor Style 实现鼠标位置缩放

在 ParaView/VTK 中,默认的缩放行为是以视图中心为基准进行缩放。然而在很多场景下,用户更希望以鼠标当前位置为中心进行缩放,这样可以快速定位到感兴趣的区域。本文将介绍如何通过自定义 Interactor Style 实现这一功能。

问题背景

VTK 默认的交互方式:

  • 滚轮缩放:以视图中心为基准,前滚放大,后滚缩小
  • 右键拖拽缩放:以视图中心为基准

用户期望的交互方式:

  • 滚轮缩放:以鼠标当前位置为中心进行缩放
  • 类似于 Google Maps、CAD 软件的缩放体验

实现原理

以鼠标位置为中心缩放的核心思路:

  1. 记录缩放前鼠标在世界坐标系中的位置
  2. 执行标准的相机缩放操作
  3. 计算缩放后鼠标位置的偏移
  4. 调整相机焦点和位置,补偿这个偏移
1
2
3
4
5
6
7
8
9
缩放前:鼠标位置 P_world

执行缩放(以相机焦点为中心)

缩放后:鼠标位置变为 P_world'

计算偏移:delta = P_world - P_world'

调整相机:focal_point += delta, position += delta

完整实现

头文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#pragma once

#include <vtkInteractorStyleTrackballCamera.h>
#include <vtkSmartPointer.h>

class vtkRenderer;

/**
* @brief 自定义交互样式,支持以鼠标位置为中心进行缩放
*/
class MousePositionZoomInteractorStyle : public vtkInteractorStyleTrackballCamera {
public:
static MousePositionZoomInteractorStyle* New();
vtkTypeMacro(MousePositionZoomInteractorStyle, vtkInteractorStyleTrackballCamera);

/**
* @brief 重写鼠标滚轮前滚事件
*/
void OnMouseWheelForward() override;

/**
* @brief 重写鼠标滚轮后滚事件
*/
void OnMouseWheelBackward() override;

/**
* @brief 设置缩放灵敏度
* @param factor 缩放因子,默认 1.1
*/
void SetZoomFactor(double factor) { m_zoomFactor = factor; }
double GetZoomFactor() const { return m_zoomFactor; }

/**
* @brief 启用/禁用鼠标位置缩放
*/
void SetMousePositionZoomEnabled(bool enabled) { m_mousePositionZoomEnabled = enabled; }
bool GetMousePositionZoomEnabled() const { return m_mousePositionZoomEnabled; }

protected:
MousePositionZoomInteractorStyle();
~MousePositionZoomInteractorStyle() override = default;

private:
/**
* @brief 以鼠标位置为中心进行缩放
* @param zoomIn true 为放大,false 为缩小
*/
void ZoomAtMousePosition(bool zoomIn);

/**
* @brief 获取鼠标位置对应的世界坐标
* @param x 屏幕 X 坐标
* @param y 屏幕 Y 坐标
* @param worldPos 输出的世界坐标
* @return 是否成功获取
*/
bool GetMouseWorldPosition(int x, int y, double worldPos[3]);

double m_zoomFactor = 1.1;
bool m_mousePositionZoomEnabled = true;

private:
MousePositionZoomInteractorStyle(const MousePositionZoomInteractorStyle&) = delete;
void operator=(const MousePositionZoomInteractorStyle&) = delete;
};

实现文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
#include "mouse_position_zoom_interactor_style.h"

#include <vtkCamera.h>
#include <vtkObjectFactory.h>
#include <vtkRenderWindow.h>
#include <vtkRenderWindowInteractor.h>
#include <vtkRenderer.h>
#include <vtkRendererCollection.h>
#include <vtkWorldPointPicker.h>

vtkStandardNewMacro(MousePositionZoomInteractorStyle);

MousePositionZoomInteractorStyle::MousePositionZoomInteractorStyle() {
// 使用 vtkWorldPointPicker 进行世界坐标拾取
}

void MousePositionZoomInteractorStyle::OnMouseWheelForward() {
if (m_mousePositionZoomEnabled) {
ZoomAtMousePosition(true);
} else {
vtkInteractorStyleTrackballCamera::OnMouseWheelForward();
}
}

void MousePositionZoomInteractorStyle::OnMouseWheelBackward() {
if (m_mousePositionZoomEnabled) {
ZoomAtMousePosition(false);
} else {
vtkInteractorStyleTrackballCamera::OnMouseWheelBackward();
}
}

void MousePositionZoomInteractorStyle::ZoomAtMousePosition(bool zoomIn) {
vtkRenderWindowInteractor* interactor = this->GetInteractor();
if (!interactor)
return;

vtkRenderWindow* renderWindow = interactor->GetRenderWindow();
if (!renderWindow)
return;

vtkRenderer* renderer = renderWindow->GetRenderers()->GetFirstRenderer();
if (!renderer)
return;

vtkCamera* camera = renderer->GetActiveCamera();
if (!camera)
return;

// 1. 获取当前鼠标位置
int mouseX, mouseY;
interactor->GetEventPosition(mouseX, mouseY);

// 2. 获取缩放前鼠标位置对应的世界坐标
double worldPosBefore[3];
if (!GetMouseWorldPosition(mouseX, mouseY, worldPosBefore)) {
// 如果无法获取世界坐标,使用默认缩放
if (zoomIn) {
vtkInteractorStyleTrackballCamera::OnMouseWheelForward();
} else {
vtkInteractorStyleTrackballCamera::OnMouseWheelBackward();
}
return;
}

// 3. 执行缩放
double factor = zoomIn ? m_zoomFactor : (1.0 / m_zoomFactor);

if (camera->GetParallelProjection()) {
// 平行投影:调整 ParallelScale
double parallelScale = camera->GetParallelScale() / factor;
camera->SetParallelScale(parallelScale);
} else {
// 透视投影:移动相机位置
double* focalPoint = camera->GetFocalPoint();
double* position = camera->GetPosition();

// 计算相机到焦点的方向向量
double direction[3];
for (int i = 0; i < 3; ++i) {
direction[i] = focalPoint[i] - position[i];
}

// 计算新的相机位置(沿方向向量移动)
double distance = sqrt(direction[0] * direction[0] +
direction[1] * direction[1] +
direction[2] * direction[2]);
double newDistance = distance / factor;
double ratio = newDistance / distance;

double newPosition[3];
for (int i = 0; i < 3; ++i) {
newPosition[i] = focalPoint[i] - direction[i] * ratio;
}

camera->SetPosition(newPosition);
}

// 4. 获取缩放后鼠标位置对应的世界坐标
double worldPosAfter[3];
if (GetMouseWorldPosition(mouseX, mouseY, worldPosAfter)) {
// 5. 计算偏移量
double delta[3];
for (int i = 0; i < 3; ++i) {
delta[i] = worldPosBefore[i] - worldPosAfter[i];
}

// 6. 调整相机焦点和位置,补偿偏移
double* focalPoint = camera->GetFocalPoint();
double* position = camera->GetPosition();

double newFocalPoint[3], newPosition[3];
for (int i = 0; i < 3; ++i) {
newFocalPoint[i] = focalPoint[i] + delta[i];
newPosition[i] = position[i] + delta[i];
}

camera->SetFocalPoint(newFocalPoint);
camera->SetPosition(newPosition);
}

// 7. 更新渲染
renderer->ResetCameraClippingRange();
renderWindow->Render();
}

bool MousePositionZoomInteractorStyle::GetMouseWorldPosition(int x, int y,
double worldPos[3]) {
vtkRenderWindowInteractor* interactor = this->GetInteractor();
if (!interactor)
return false;

vtkRenderWindow* renderWindow = interactor->GetRenderWindow();
if (!renderWindow)
return false;

vtkRenderer* renderer = renderWindow->GetRenderers()->GetFirstRenderer();
if (!renderer)
return false;

// 方法一:使用 vtkWorldPointPicker(基于深度缓冲)
vtkSmartPointer<vtkWorldPointPicker> picker =
vtkSmartPointer<vtkWorldPointPicker>::New();

if (picker->Pick(x, y, 0, renderer)) {
picker->GetPickPosition(worldPos);
return true;
}

// 方法二:如果 pick 失败,使用焦平面投影
vtkCamera* camera = renderer->GetActiveCamera();
if (!camera)
return false;

// 将屏幕坐标转换为归一化视口坐标
int* size = renderWindow->GetSize();
double viewX = static_cast<double>(x) / size[0];
double viewY = static_cast<double>(y) / size[1];

// 获取焦点的深度值
double focalPoint[3];
camera->GetFocalPoint(focalPoint);

// 将焦点转换为显示坐标获取深度
renderer->SetWorldPoint(focalPoint[0], focalPoint[1], focalPoint[2], 1.0);
renderer->WorldToDisplay();
double* displayCoord = renderer->GetDisplayPoint();
double focalDepth = displayCoord[2];

// 使用鼠标位置和焦点深度构造显示坐标
renderer->SetDisplayPoint(x, y, focalDepth);
renderer->DisplayToWorld();
double* worldCoord = renderer->GetWorldPoint();

if (worldCoord[3] != 0.0) {
worldPos[0] = worldCoord[0] / worldCoord[3];
worldPos[1] = worldCoord[1] / worldCoord[3];
worldPos[2] = worldCoord[2] / worldCoord[3];
return true;
}

return false;
}

在 ParaView 中应用

方法一:替换 pqRenderView 的 Interactor Style

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <pqRenderView.h>
#include <vtkSMRenderViewProxy.h>
#include <vtkRenderWindowInteractor.h>

void setupCustomInteractorStyle(pqRenderView* renderView) {
vtkSMRenderViewProxy* viewProxy = renderView->getRenderViewProxy();
vtkRenderWindowInteractor* interactor = viewProxy->GetInteractor();

vtkSmartPointer<MousePositionZoomInteractorStyle> style =
vtkSmartPointer<MousePositionZoomInteractorStyle>::New();

// 可选:设置缩放因子
style->SetZoomFactor(1.15);

interactor->SetInteractorStyle(style);
}

方法二:在 ViewFrame 初始化时设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void MyViewFrameActions::setupInteractorStyle(pqView* view) {
pqRenderView* renderView = qobject_cast<pqRenderView*>(view);
if (!renderView)
return;

QWidget* viewWidget = renderView->widget();
pqQVTKWidget* vtkWidget = qobject_cast<pqQVTKWidget*>(viewWidget);
if (!vtkWidget)
return;

vtkRenderWindow* renderWindow = vtkWidget->GetRenderWindow();
vtkRenderWindowInteractor* interactor = renderWindow->GetInteractor();

m_customStyle = vtkSmartPointer<MousePositionZoomInteractorStyle>::New();
interactor->SetInteractorStyle(m_customStyle);
}

扩展:其他自定义交互

自定义右键拖拽行为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void MousePositionZoomInteractorStyle::OnRightButtonDown() {
// 记录起始位置
int* pos = this->GetInteractor()->GetEventPosition();
m_rightButtonDownPos[0] = pos[0];
m_rightButtonDownPos[1] = pos[1];

// 调用父类处理
vtkInteractorStyleTrackballCamera::OnRightButtonDown();
}

void MousePositionZoomInteractorStyle::OnRightButtonUp() {
// 检查是否是点击(而非拖拽)
int* pos = this->GetInteractor()->GetEventPosition();
int dx = pos[0] - m_rightButtonDownPos[0];
int dy = pos[1] - m_rightButtonDownPos[1];

if (abs(dx) < 5 && abs(dy) < 5) {
// 右键点击,可以触发上下文菜单
InvokeEvent(vtkCommand::RightButtonReleaseEvent);
}

vtkInteractorStyleTrackballCamera::OnRightButtonUp();
}

自定义中键行为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void MousePositionZoomInteractorStyle::OnMiddleButtonDown() {
// 中键按下时记录起始位置,用于平移
m_middleButtonDown = true;
int* pos = this->GetInteractor()->GetEventPosition();
m_lastPos[0] = pos[0];
m_lastPos[1] = pos[1];
}

void MousePositionZoomInteractorStyle::OnMouseMove() {
if (m_middleButtonDown) {
// 实现自定义平移逻辑
int* pos = this->GetInteractor()->GetEventPosition();
Pan(pos[0] - m_lastPos[0], pos[1] - m_lastPos[1]);
m_lastPos[0] = pos[0];
m_lastPos[1] = pos[1];
} else {
vtkInteractorStyleTrackballCamera::OnMouseMove();
}
}

禁用某些默认交互

1
2
3
4
5
6
7
8
9
void MousePositionZoomInteractorStyle::OnLeftButtonDown() {
// 禁用左键旋转,改为其他功能
// 不调用父类方法即可禁用

// 或者根据条件决定是否执行默认行为
if (m_rotationEnabled) {
vtkInteractorStyleTrackballCamera::OnLeftButtonDown();
}
}

注意事项

1. 与 ParaView 其他功能的兼容性

ParaView 的一些功能(如选择、测量)会临时更改 Interactor Style。确保在这些操作完成后恢复自定义样式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 监听 ParaView 的信号
connect(pqApplicationCore::instance()->getSelectionModel(),
&pqSelectionManager::selectionChanged,
this, &MyClass::onSelectionChanged);

void MyClass::onSelectionChanged() {
// 恢复自定义 Interactor Style
if (m_renderView) {
vtkRenderWindowInteractor* interactor =
m_renderView->getRenderViewProxy()->GetInteractor();
if (interactor->GetInteractorStyle() != m_customStyle) {
interactor->SetInteractorStyle(m_customStyle);
}
}
}

2. 平行投影与透视投影

1
2
3
4
5
6
7
if (camera->GetParallelProjection()) {
// 平行投影:调整 ParallelScale
camera->SetParallelScale(camera->GetParallelScale() / factor);
} else {
// 透视投影:移动相机位置
// ...
}

3. 相机裁剪范围

缩放后需要更新裁剪范围,否则可能出现裁剪问题:

1
renderer->ResetCameraClippingRange();

总结

自定义 Interactor Style 是扩展 VTK/ParaView 交互功能的强大方式。通过继承 vtkInteractorStyleTrackballCamera 并重写相应的事件处理方法,可以实现:

  • 鼠标位置缩放
  • 自定义旋转、平移行为
  • 添加新的交互手势
  • 禁用或修改默认交互

关键要点:

  1. 使用 vtkWorldPointPicker 或坐标变换获取鼠标的世界坐标
  2. 缩放后计算偏移并补偿,实现”以鼠标为中心”的效果
  3. 注意平行投影和透视投影的不同处理方式
  4. 处理好与 ParaView 其他功能的兼容性

参考资料


自定义 Interactor Style 实现鼠标位置缩放
http://aojian-blog.oss-cn-wuhan-lr.aliyuncs.com/2026/02/04/interactorstyle/
作者
遨见
发布于
2026年2月4日
许可协议