系列入门篇05 策略模式 · Qt / VTK 设计模式本文基于 VTK 官方 API 与 Kitware 开源源码梳理不依赖任何特定业务项目。主要参考vtkRenderWindowInteractor、vtkInteractorStyle、vtkMapper、vtkAbstractPicker、VTK Examples StyleSwitch写在前面VTK 里「策略」到底指什么GoF策略模式的一句话定义一族算法把它们封装起来并且使它们可以互相替换。调用方依赖抽象接口运行期换实现而不是在业务代码里堆if (mode A) ... else if (mode B) ...。VTK 没有一个叫vtkStrategy的基类但策略思想散落在多个模块里最典型的是维度Strategy 抽象Context 持有方典型切换 API交互vtkInteractorStylevtkRenderWindowInteractorSetInteractorStyle()绘制vtkMappervtkActor/vtkVolumeSetMapper()拾取vtkAbstractPickervtkRenderWindowInteractor或业务代码SetPicker()/ 局部使用扩展插值、坐标映射、体绘制后端等对应 Filter / Prop各模块SetXxx()本篇按交互 → 绘制 → 拾取 → 组合与其他模式展开读完应能回答为什么SetInteractorStyle是教科书级策略换 Mapper 和换 Style 有什么相同与不同自定义交互工具时Style、Widget、Observer 怎么分工策略模式在 VTK 里经常和哪些模式叠在一起1. GoF 策略模式先对齐概念1.1 经典结构┌─────────────┐ │ Context │ 持有 Strategy*把请求委托给它 └──────┬──────┘ │ uses ▼ ┌─────────────┐ │ Strategy │ 抽象接口 └──────┬──────┘ ┌───────┼───────┐ ▼ ▼ ▼ ConcreteA B C 可互换的具体算法1.2 策略模式要满足的性质性质含义VTK 交互层的对应算法可替换运行期换实现调用方代码少改iren-SetInteractorStyle(newStyle)开闭原则新增算法 新子类不必改 Context新建vtkInteractorStyle子类消除条件分支Context 不写满屏 mode 判断Interactor 只转发事件不解析手势同一接口所有策略同一组虚函数OnLeftButtonDown()等1.3 策略 vs 状态 vs 模板方法VTK 里极易混模式谁决定用哪个算法VTK 例子策略客户端显式SetInteractorStyle用户点「框选工具」→ 换 RubberBand Style状态对象内部状态自动切换行为vtkAlgorithm管线状态已修改 / 已执行模板方法基类固定骨架子类填步骤vtkAlgorithm::ProcessRequest管线vtkInteractorStyle里部分流程固定、子类重写On*vtkInteractorStyle本身是策略 模板方法的混合基类负责事件绑定与InvokeEvent骨架子类重写OnLeftButtonDown()等实现具体手势——但选用哪个子类仍是策略语义由SetInteractorStyle决定。1.4 策略 vs 桥接桥接把「抽象」与「实现」两个维度拆开例如 Prop 与 Mapper 分离。策略强调同一时刻只选一种算法执行。vtkActor持vtkMapper*既有桥接味道表现与绘制分离也有策略味道换 Mapper 换绘制算法。VTK 文档很少用模式术语但从用法上按策略理解最实用。2. 总览从鼠标到像素的一条链OS 输入鼠标/键盘/触摸 │ ▼ vtkRenderWindowInteractor ← Context壳路由事件 │ GetInteractorStyle() ▼ vtkInteractorStyle ← Strategy手势算法旋转 / 框选 / 窗宽窗位 │ 更新 Camera / Actor / Selection ▼ vtkRenderer::Render() │ ▼ vtkActor / vtkVolume │ GetMapper() ▼ vtkMapper ← Strategy如何把数据画出来 │ ▼ GPU / 帧缓冲拾取点击选中物体可发生在 Style 处理过程中或业务层主动调用算法由vtkAbstractPicker子类决定——又是策略。3. 交互策略vtkInteractorStyle深度剖析3.1 ContextvtkRenderWindowInteractor只是「壳」VTK 官方对vtkRenderWindowInteractor的说明意译与早期版本不同现在它主要作为外壳保存用户偏好并把平台相关消息路由到vtkInteractorStyle。关键 APIvirtualvoidSetInteractorStyle(vtkInteractorObserver*);virtualvtkInteractorObserver*GetInteractorStyle();内部持有vtkSmartPointervtkInteractorObserverInteractorStyle;默认构造出来的往往是vtkInteractorStyleSwitch见下节——本身就是一个「策略选择器」。Context 的职责平台层Windows / macOS / Qt / X11 事件循环、Initialize()/Start()把LeftButtonPress等中性事件转给当前 Style持有Picker、帧率、灯光跟随相机等全局偏好不关心左键到底是旋转还是框选——那是 Style 的事这就是策略模式里 Context只委托、不实现算法的典型形态。3.2 Strategy 接口vtkInteractorStyle家族继承关系简化vtkObject └── vtkInteractorObserver └── vtkInteractorStyle ├── vtkInteractorStyleTrackballCamera ├── vtkInteractorStyleTrackballActor ├── vtkInteractorStyleJoystickCamera ├── vtkInteractorStyleJoystickActor ├── vtkInteractorStyleRubberBand2D ├── vtkInteractorStyleRubberBand3D ├── vtkInteractorStyleRubberBandPick ├── vtkInteractorStyleImage // 2D 图像窗宽窗位 ├── vtkInteractorStyleTerrain ├── vtkInteractorStyleFlight ├── vtkInteractorStyleMultiTouchCamera ├── vtkInteractorStyleSwitch // 内嵌多策略 └── 你的 MyInteractorStyleTrackball vs Joystick同一类问题的两种算法风格交互隐喻鼠标按住时典型场景Trackball抓住物体/相机拖动按下移动才动作松手停止网格查看、CAD 浏览Joystick摇杆持续推动按住产生连续事件流旧式 VTK 示例、飞行浏览vtkInteractorStyleTrackballCamera绕相机旋转场景医学影像、通用 3D 浏览最常见。vtkInteractorStyleTrackballActor绕单个 Actor变换操纵模型本身。vtkInteractorStyleImage2D 切片上的平移、缩放、窗宽窗位。专用交互策略类策略语义vtkInteractorStyleRubberBand2D屏幕矩形框选发SelectionChangedEventvtkInteractorStyleRubberBand3D3D 视锥框选vtkInteractorStyleRubberBandPick框选 拾取组合vtkInteractorStyleMultiTouchCamera触摸缩放/旋转3.3 事件如何流到策略调用栈概念平台鼠标左键按下 → vtkRenderWindowInteractor::LeftButtonPressEvent() → vtkInteractorStyle::OnLeftButtonDown() // 虚函数子类实现 → Trackball旋转相机 / RubberBand记录起点 → InvokeEvent(LeftButtonPressEvent) // 仍可被外部 observer 监听 → 可能 RenderWindow-Render()vtkInteractorStyle文档说明除处理手势外还会InvokeEvent发出标准事件LeftButtonPressEvent、StartInteractionEvent、EndInteractionEvent等供 Observer vtkCommand 订阅。因此Style 默认手势算法策略AddObserver 在同一条事件总线上挂额外逻辑观察者二者常一起用但不是同一模式。3.4 运行期切换策略最小示例#includevtkRenderWindowInteractor.h#includevtkInteractorStyleTrackballCamera.h#includevtkInteractorStyleRubberBand2D.hclassInteractionTool{public:explicitInteractionTool(vtkRenderWindowInteractor*iren):m_iren(iren){m_navigatevtkSmartPointervtkInteractorStyleTrackballCamera::New();m_selectvtkSmartPointervtkInteractorStyleRubberBand2D::New();m_iren-SetInteractorStyle(m_navigate);// 默认浏览}voidenterNavigateMode(){m_iren-SetInteractorStyle(m_navigate);}voidenterSelectMode(){m_iren-SetInteractorStyle(m_select);}private:vtkRenderWindowInteractor*m_iren;vtkSmartPointervtkInteractorStyleTrackballCameram_navigate;vtkSmartPointervtkInteractorStyleRubberBand2Dm_select;};要点复用vtkSmartPointer保存策略实例避免每次new切换前可GetInteractorStyle()存为「上一模式」便于工具栏退出时恢复SetInteractorStyle后检查CurrentRenderer是否仍正确多视口时见 §73.5vtkInteractorStyleSwitch策略的策略Context 嵌套VTK 默认 Interactor 常配vtkInteractorStyleSwitch。它内部持有 45 个子 Style在OnChar()里根据按键切换j/tJoystick ↔ Trackballc/aCamera ↔ ActorvtkNewvtkInteractorStyleSwitchstyle;renderWindowInteractor-SetInteractorStyle(style);// 运行期按键切换或代码style-SetCurrentStyleToTrackballCamera();这是策略模式套策略模式对外Interactor 只看到一个vtkInteractorStyleSwitch对内CurrentStyle指向真正的 ConcreteStrategy官方示例StyleSwitch应用层可仿照它写自己的 Switch例如「浏览 / 测量 / 标注」三模式用工具栏改CurrentStyle而不是让 Interactor 直接感知模式枚举。3.6 自定义vtkInteractorStyle推荐步骤继承最接近的现有类多数情况选vtkInteractorStyleTrackballCamera只重写需要的OnLeftButtonDown/OnMouseMove/OnChar等需要额外逻辑时InvokeEvent不要绕过基类除非有意为之用vtkSetMacro/ 成员保存工具状态橡皮筋起点、测量点等在析构或工具退出时RemoveObservers避免悬空clientDataclassMeasureStyle:publicvtkInteractorStyleTrackballCamera{public:staticMeasureStyle*New();vtkTypeMacro(MeasureStyle,vtkInteractorStyleTrackballCamera);voidOnLeftButtonDown()override{// 记录测距点或交给 vtkPointHandleWidgetthis-InvokeEvent(vtkCommand::LeftButtonPressEvent,nullptr);// 若仍要保留旋转vtkInteractorStyleTrackballCamera::OnLeftButtonDown();}};3.7 Style 与 3D Widget、AbortFlag 的分工机制模式角色适用场景vtkInteractorStyle全局交互策略浏览、框选、图像窗宽窗位vtkAbstractWidgetvtkInteractorObserver局部工具可GrabFocusgizmo、测距手柄、裁剪盒vtkCommand::AbortFlag责任链截断Widget 消费事件后阻止 Style 继续处理不要把所有交互都塞进一个巨型 Style 子类常见做法默认TrackballCamera浏览进入编辑工具时启用 WidgetGrabFocus或临时换 RubberBand Style退出时恢复默认 Style详见 10 责任链。4. 绘制策略vtkMapper深度剖析4.1 为什么 Mapper 是策略渲染管线里数据vtkPolyData、vtkImageData…与怎么画是分开的vtkAlgorithm (Filter) → vtkDataSet ↓ vtkMapper ← Strategy着色、图元类型、GPU 路径 ↓ vtkActor / vtkVolume ↓ vtkRenderervtkActor::SetMapper(vtkMapper*)与SetInteractorStyle同构Prop 不关心具体绘制算法只持有抽象vtkMapper。4.2 常见 Mapper 策略表多边形面片Mapper输入策略语义vtkPolyDataMappervtkPolyData经典三角面渲染vtkOpenGLPolyDataMappervtkPolyDataOpenGL 路径现代默认后端vtkCompositePolyDataMapper复合vtkMultiBlockDataSet多块数据一次绘制体绘制VolumeMapper策略语义vtkFixedPointVolumeRayCastMapper固定点 CPU 光线投射vtkGPUVolumeRayCastMapperGPU 光线投射大数据常用vtkSmartVolumeMapper根据数据与硬件自动选择后端策略 简单工厂点、线、实例化Mapper策略语义vtkGlyph3DMapper每个点实例化一个图元箭头、球vtkPointGaussianMapper大范围点云近似高斯 splatvtkLabeledDataMapper在点上画数值标签图像 / 2DMapper策略语义vtkImageSliceMapper轴位切片vtkImageResliceMapper任意平面重切片4.3 切换 Mapper 示例同一 Actor从面片显示切到点云高亮vtkNewvtkPolyDataMappersolidMapper;solidMapper-SetInputConnection(polySource-GetOutputPort());vtkNewvtkPointGaussianMapperpointMapper;pointMapper-SetInputConnection(polySource-GetOutputPort());pointMapper-EmissiveOff();vtkNewvtkActoractor;actor-SetMapper(solidMapper);renderer-AddActor(actor);voidshowAsPoints(vtkActor*actor){actor-SetMapper(pointMapper);actor-GetProperty()-SetPointSize(3);}voidshowAsSurface(vtkActor*actor){actor-SetMapper(solidMapper);}4.4 Mapper 策略的底层注意点输入数据类型必须匹配vtkPolyDataMapper不能接vtkImageData否则空渲染或断言。Modified()与管线更新换 Mapper 后通常actor-Modified()Render()若接 Filter 输出确保Update()已执行。Scalar 与 LookupTable同一 Mapper 换标量数组时策略仍是「多边形绘制」但着色规则靠vtkMapper::SetScalarVisibility、SetColorMode等——属于 Mapper内部参数不必换子类。Volume 的 GPU / CPU体数据很大时vtkGPUVolumeRayCastMapper与 CPU 实现在性能与精度上差异大vtkSmartVolumeMapper用策略 工厂减轻选择负担。与 Filter 策略的区别Filter改数据几何、拓扑、标量Mapper改绘制方式面扫软件里「网格简化」是 Filter「线框 / 实体显示」常是 Mapper vtkProperty。5. 拾取策略vtkAbstractPicker家族点击「选中牙齿 / 选中模型」依赖拾取算法又是一组可替换策略。5.1 继承结构简化vtkAbstractPicker ├── vtkPicker // 基于图形硬件 遍历 Prop常用 ├── vtkCellPicker // 精确到单元可获取世界坐标与 CellId ├── vtkPointPicker // 精确到点 ├── vtkPropPicker // 只选 Prop不深入几何 ├── vtkWorldPointPicker // 屏幕坐标 → 世界射线与平面交点 └── vtkAreaPicker // 区域拾取5.2 Context 与使用方式vtkRenderWindowInteractor内建vtkAbstractPicker* Picker可通过SetPicker更换默认拾取策略。两种用法// A. 全局默认 pickervtkNewvtkCellPickercellPicker;cellPicker-SetTolerance(0.001);iren-SetPicker(cellPicker);// B. 单次拾取临时策略不影响 Interactor 默认vtkNewvtkPropPickerpropPicker;if(propPicker-Pick(x,y,0,renderer)){vtkProp*proppropPicker-GetViewProp();}5.3 策略选择指南需求推荐策略快速选中物体vtkPropPicker需要 CellId / 重心坐标vtkCellPicker需要最近顶点vtkPointPicker框选多个 ActorvtkAreaPicker或 RubberBand Style 回调在空白处点地平面vtkWorldPointPicker 约束平面拾取与交互 Style独立Style 决定手势Picker 决定「这一点的几何命中是谁」RubberBand Style 常在SelectionChangedEvent里再调 AreaPicker。6. 其他 VTK 模块中的策略思想扩展阅读模块抽象接口替换的是什么vtkImageInterpolator插值核最近邻 / 线性 / 三次vtkCoordinate坐标系策略World / Display / NormalizedViewportvtkAbstractImageInterpolator重切片采样策略质量 vs 速度vtkTextRenderer字体栅格化后端FreeType 等vtkColorTransferFunctionvtkPiecewiseFunction传递函数体绘制颜色/透明度映射更像策略化配置vtkAlgorithm子类滤波算法严格说是Strategy 管道见 03 管道这些不如 InteractorStyle / Mapper 典型但在读源码时可用同一 lens接口稳定实现可换。7. 多视口与多 Interactor 场景vtkRenderWindow ├── Renderer0 ←→ Interactor0 Style0 └── Renderer1 ←→ Interactor1 Style1 // 或共享一个 Interactor策略每个 Interactor 独立 Style左侧 3D 轨道球右侧 2DvtkInteractorStyleImage共享 InteractorvtkInteractorObserver::SetCurrentRenderer()决定事件发给哪个 Renderer切换 Style 时同步SetDefaultRenderer/SetCurrentRenderervtkInteractorStyleSwitch会转发给子 Style陷阱只换 Style 不设 Renderer会出现「鼠标在动但相机不动」——事件到了 Style但CurrentRenderer为空。8. 策略与其他模式的组合组合说明VTK 例子策略 工厂工厂创建具体策略vtkInteractorStyle::New()、vtkSmartVolumeMapper选后端策略 观察者策略执行时发事件StyleInvokeEvent(InteractionEvent)策略 责任链多 listener 顺序处理Widget AbortFlag Style策略 装饰增强策略对象而非替换vtkLODActor包装绘制见 06 装饰策略 状态管线对象内部阶段vtkAlgorithm执行状态 vs 交互 Style 由用户切换9. 完整示例浏览 / 框选工具切换下面示例展示Context 不变两种ConcreteStrategy热切换并监听框选结果。#includevtkActor.h#includevtkCallbackCommand.h#includevtkCommand.h#includevtkInteractorStyleRubberBand2D.h#includevtkInteractorStyleTrackballCamera.h#includevtkPolyDataMapper.h#includevtkRenderWindow.h#includevtkRenderWindowInteractor.h#includevtkRenderer.h#includevtkSphereSource.hnamespace{voidOnSelection(vtkObject*caller,unsignedlong,void*clientData,void*callData){auto*stylevtkInteractorStyleRubberBand2D::SafeDownCast(caller);if(!style||!callData)return;auto*regionstatic_castint*(callData);// [x0,y0,x1,y1] 屏幕像素// 用 region 做 AreaPicker 或自定义逻辑(void)region;(void)clientData;}}// namespaceintmain(){vtkNewvtkSphereSourcesphere;vtkNewvtkPolyDataMappermapper;mapper-SetInputConnection(sphere-GetOutputPort());vtkNewvtkActoractor;actor-SetMapper(mapper);vtkNewvtkRendererrenderer;renderer-AddActor(actor);vtkNewvtkRenderWindowrw;rw-AddRenderer(renderer);vtkNewvtkRenderWindowInteractoriren;iren-SetRenderWindow(rw);vtkNewvtkInteractorStyleTrackballCameranavStyle;vtkNewvtkInteractorStyleRubberBand2DselectStyle;vtkNewvtkCallbackCommandselCmd;selCmd-SetCallback(OnSelection);selectStyle-AddObserver(vtkCommand::SelectionChangedEvent,selCmd);iren-SetInteractorStyle(navStyle);// 默认浏览// 模拟用户点「框选」按钮时// iren-SetInteractorStyle(selectStyle);// 完成后恢复 iren-SetInteractorStyle(navStyle);rw-Render();iren-Start();return0;}设计要点iren自始至终是 ContextnavStyle/selectStyle是长期复用的策略实例框选结果通过Observer传出不把业务逻辑写进 Style 源码恢复浏览 再次SetInteractorStyle(navStyle)—— 纯策略切换10. 最佳实践与常见陷阱10.1 实践清单依赖抽象类型函数参数写vtkInteractorStyle*/vtkMapper*不要写死具体子类策略实例复用热切换用vtkSmartPointer缓存避免每帧New()保存默认 Style进入工具前auto* prev iren-GetInteractorStyle()退出时恢复自定义 Style 继承最近子类少重写、多Superclass::OnLeftButtonDown()Mapper 与数据类型对照表写进项目文档防止接错拾取容差SetTolerance按场景调单元过密时CellPicker可能变慢多视口换 Style 时检查CurrentRenderer10.2 陷阱现象常见原因换了 Style 鼠标无反应Enable()未开、Interactor 未Initialize、Renderer 未绑定框选后无回调监听对象应是RubberBand Style上的SelectionChangedEvent换 Mapper 后全黑输入数据类型不匹配、标量未开启、Camera 未 ResetCamera旋转与框选同时触发Widget 未GrabFocus/ 未AbortFlagOn()内存泄漏感Style 被 Interactor 持有引用计数但clientData原始指针仍需自己保证生命周期把 Filter 当 Strategy 换Filter 改数据应SetInputConnection更新管线不是SetInteractorStyle11. 一张总图┌──────────────────────────────────────┐ │ vtkRenderWindowInteractor │ │ SetInteractorStyle( vtkInteractorStyle* ) │ └─────────────────┬────────────────────┘ │ ┌───────────────────────┼───────────────────────┐ ▼ ▼ ▼ TrackballCamera RubberBand2D ImageStyle 浏览策略 框选策略 窗宽窗位策略 │ │ └───────────┬───────────┘ ▼ vtkRenderer::Render() │ ┌───────────┴───────────┐ ▼ ▼ vtkActor vtkVolume SetMapper(…) SetMapper(…) │ │ PolyDataMapper GPUVolumeRayCastMapper 面片策略 体绘制策略 │ │ └───────────┬───────────┘ ▼ 帧缓冲 / 显示 点击选中vtkAbstractPicker 策略Cell / Prop / Point …12. 总结问题答案VTK 策略模式最典型落点vtkInteractorStyle交互与vtkMapper绘制Context 是谁主要是vtkRenderWindowInteractor、vtkActor/vtkVolume怎么切换SetInteractorStyle()、SetMapper()、SetPicker()和 Observer 关系Style 处理手势并InvokeEvent切换策略本身不依赖 Observer和 State 区别Style 由应用显式切换不是对象内部自动变模式进阶组合vtkInteractorStyleSwitch、Widget AbortFlag、SmartVolumeMapperVTK 的策略模式不需要额外框架虚函数 SetXxx()持有指针 引用计数就是运行期可替换算法。读懂 Interactor 壳与 Style 委托、Actor 与 Mapper 分离就掌握了 VTK 里一半以上的「可扩展交互与显示」扩展点。重点与注意进阶速记重点vtkRenderWindowInteractor官方自述为shell手势算法全在vtkInteractorStyle—— 这是 Context / Strategy 最干净的一例。重点SetInteractorStyle与SetMapper同一套路换实现不改调用方前者管输入后者管输出。重点默认vtkInteractorStyleSwitch说明 VTK 自己也把「多种 Style」再封装成一层可切换策略。注意策略显式Set≠ 状态对象内部自动变≠ 模板方法vtkAlgorithm管线骨架。注意复杂工具用Widget GrabFocus/AbortFlag不要无限膨胀 Style 子类。注意vtkCommand是事件回调不是策略Strategy 是InteractorStyle / Mapper / Picker这类「一整条算法」的可替换对象。
VTK 策略模式详解:InteractorStyle、Mapper、Picker 与可替换算法
发布时间:2026/7/8 2:54:15
系列入门篇05 策略模式 · Qt / VTK 设计模式本文基于 VTK 官方 API 与 Kitware 开源源码梳理不依赖任何特定业务项目。主要参考vtkRenderWindowInteractor、vtkInteractorStyle、vtkMapper、vtkAbstractPicker、VTK Examples StyleSwitch写在前面VTK 里「策略」到底指什么GoF策略模式的一句话定义一族算法把它们封装起来并且使它们可以互相替换。调用方依赖抽象接口运行期换实现而不是在业务代码里堆if (mode A) ... else if (mode B) ...。VTK 没有一个叫vtkStrategy的基类但策略思想散落在多个模块里最典型的是维度Strategy 抽象Context 持有方典型切换 API交互vtkInteractorStylevtkRenderWindowInteractorSetInteractorStyle()绘制vtkMappervtkActor/vtkVolumeSetMapper()拾取vtkAbstractPickervtkRenderWindowInteractor或业务代码SetPicker()/ 局部使用扩展插值、坐标映射、体绘制后端等对应 Filter / Prop各模块SetXxx()本篇按交互 → 绘制 → 拾取 → 组合与其他模式展开读完应能回答为什么SetInteractorStyle是教科书级策略换 Mapper 和换 Style 有什么相同与不同自定义交互工具时Style、Widget、Observer 怎么分工策略模式在 VTK 里经常和哪些模式叠在一起1. GoF 策略模式先对齐概念1.1 经典结构┌─────────────┐ │ Context │ 持有 Strategy*把请求委托给它 └──────┬──────┘ │ uses ▼ ┌─────────────┐ │ Strategy │ 抽象接口 └──────┬──────┘ ┌───────┼───────┐ ▼ ▼ ▼ ConcreteA B C 可互换的具体算法1.2 策略模式要满足的性质性质含义VTK 交互层的对应算法可替换运行期换实现调用方代码少改iren-SetInteractorStyle(newStyle)开闭原则新增算法 新子类不必改 Context新建vtkInteractorStyle子类消除条件分支Context 不写满屏 mode 判断Interactor 只转发事件不解析手势同一接口所有策略同一组虚函数OnLeftButtonDown()等1.3 策略 vs 状态 vs 模板方法VTK 里极易混模式谁决定用哪个算法VTK 例子策略客户端显式SetInteractorStyle用户点「框选工具」→ 换 RubberBand Style状态对象内部状态自动切换行为vtkAlgorithm管线状态已修改 / 已执行模板方法基类固定骨架子类填步骤vtkAlgorithm::ProcessRequest管线vtkInteractorStyle里部分流程固定、子类重写On*vtkInteractorStyle本身是策略 模板方法的混合基类负责事件绑定与InvokeEvent骨架子类重写OnLeftButtonDown()等实现具体手势——但选用哪个子类仍是策略语义由SetInteractorStyle决定。1.4 策略 vs 桥接桥接把「抽象」与「实现」两个维度拆开例如 Prop 与 Mapper 分离。策略强调同一时刻只选一种算法执行。vtkActor持vtkMapper*既有桥接味道表现与绘制分离也有策略味道换 Mapper 换绘制算法。VTK 文档很少用模式术语但从用法上按策略理解最实用。2. 总览从鼠标到像素的一条链OS 输入鼠标/键盘/触摸 │ ▼ vtkRenderWindowInteractor ← Context壳路由事件 │ GetInteractorStyle() ▼ vtkInteractorStyle ← Strategy手势算法旋转 / 框选 / 窗宽窗位 │ 更新 Camera / Actor / Selection ▼ vtkRenderer::Render() │ ▼ vtkActor / vtkVolume │ GetMapper() ▼ vtkMapper ← Strategy如何把数据画出来 │ ▼ GPU / 帧缓冲拾取点击选中物体可发生在 Style 处理过程中或业务层主动调用算法由vtkAbstractPicker子类决定——又是策略。3. 交互策略vtkInteractorStyle深度剖析3.1 ContextvtkRenderWindowInteractor只是「壳」VTK 官方对vtkRenderWindowInteractor的说明意译与早期版本不同现在它主要作为外壳保存用户偏好并把平台相关消息路由到vtkInteractorStyle。关键 APIvirtualvoidSetInteractorStyle(vtkInteractorObserver*);virtualvtkInteractorObserver*GetInteractorStyle();内部持有vtkSmartPointervtkInteractorObserverInteractorStyle;默认构造出来的往往是vtkInteractorStyleSwitch见下节——本身就是一个「策略选择器」。Context 的职责平台层Windows / macOS / Qt / X11 事件循环、Initialize()/Start()把LeftButtonPress等中性事件转给当前 Style持有Picker、帧率、灯光跟随相机等全局偏好不关心左键到底是旋转还是框选——那是 Style 的事这就是策略模式里 Context只委托、不实现算法的典型形态。3.2 Strategy 接口vtkInteractorStyle家族继承关系简化vtkObject └── vtkInteractorObserver └── vtkInteractorStyle ├── vtkInteractorStyleTrackballCamera ├── vtkInteractorStyleTrackballActor ├── vtkInteractorStyleJoystickCamera ├── vtkInteractorStyleJoystickActor ├── vtkInteractorStyleRubberBand2D ├── vtkInteractorStyleRubberBand3D ├── vtkInteractorStyleRubberBandPick ├── vtkInteractorStyleImage // 2D 图像窗宽窗位 ├── vtkInteractorStyleTerrain ├── vtkInteractorStyleFlight ├── vtkInteractorStyleMultiTouchCamera ├── vtkInteractorStyleSwitch // 内嵌多策略 └── 你的 MyInteractorStyleTrackball vs Joystick同一类问题的两种算法风格交互隐喻鼠标按住时典型场景Trackball抓住物体/相机拖动按下移动才动作松手停止网格查看、CAD 浏览Joystick摇杆持续推动按住产生连续事件流旧式 VTK 示例、飞行浏览vtkInteractorStyleTrackballCamera绕相机旋转场景医学影像、通用 3D 浏览最常见。vtkInteractorStyleTrackballActor绕单个 Actor变换操纵模型本身。vtkInteractorStyleImage2D 切片上的平移、缩放、窗宽窗位。专用交互策略类策略语义vtkInteractorStyleRubberBand2D屏幕矩形框选发SelectionChangedEventvtkInteractorStyleRubberBand3D3D 视锥框选vtkInteractorStyleRubberBandPick框选 拾取组合vtkInteractorStyleMultiTouchCamera触摸缩放/旋转3.3 事件如何流到策略调用栈概念平台鼠标左键按下 → vtkRenderWindowInteractor::LeftButtonPressEvent() → vtkInteractorStyle::OnLeftButtonDown() // 虚函数子类实现 → Trackball旋转相机 / RubberBand记录起点 → InvokeEvent(LeftButtonPressEvent) // 仍可被外部 observer 监听 → 可能 RenderWindow-Render()vtkInteractorStyle文档说明除处理手势外还会InvokeEvent发出标准事件LeftButtonPressEvent、StartInteractionEvent、EndInteractionEvent等供 Observer vtkCommand 订阅。因此Style 默认手势算法策略AddObserver 在同一条事件总线上挂额外逻辑观察者二者常一起用但不是同一模式。3.4 运行期切换策略最小示例#includevtkRenderWindowInteractor.h#includevtkInteractorStyleTrackballCamera.h#includevtkInteractorStyleRubberBand2D.hclassInteractionTool{public:explicitInteractionTool(vtkRenderWindowInteractor*iren):m_iren(iren){m_navigatevtkSmartPointervtkInteractorStyleTrackballCamera::New();m_selectvtkSmartPointervtkInteractorStyleRubberBand2D::New();m_iren-SetInteractorStyle(m_navigate);// 默认浏览}voidenterNavigateMode(){m_iren-SetInteractorStyle(m_navigate);}voidenterSelectMode(){m_iren-SetInteractorStyle(m_select);}private:vtkRenderWindowInteractor*m_iren;vtkSmartPointervtkInteractorStyleTrackballCameram_navigate;vtkSmartPointervtkInteractorStyleRubberBand2Dm_select;};要点复用vtkSmartPointer保存策略实例避免每次new切换前可GetInteractorStyle()存为「上一模式」便于工具栏退出时恢复SetInteractorStyle后检查CurrentRenderer是否仍正确多视口时见 §73.5vtkInteractorStyleSwitch策略的策略Context 嵌套VTK 默认 Interactor 常配vtkInteractorStyleSwitch。它内部持有 45 个子 Style在OnChar()里根据按键切换j/tJoystick ↔ Trackballc/aCamera ↔ ActorvtkNewvtkInteractorStyleSwitchstyle;renderWindowInteractor-SetInteractorStyle(style);// 运行期按键切换或代码style-SetCurrentStyleToTrackballCamera();这是策略模式套策略模式对外Interactor 只看到一个vtkInteractorStyleSwitch对内CurrentStyle指向真正的 ConcreteStrategy官方示例StyleSwitch应用层可仿照它写自己的 Switch例如「浏览 / 测量 / 标注」三模式用工具栏改CurrentStyle而不是让 Interactor 直接感知模式枚举。3.6 自定义vtkInteractorStyle推荐步骤继承最接近的现有类多数情况选vtkInteractorStyleTrackballCamera只重写需要的OnLeftButtonDown/OnMouseMove/OnChar等需要额外逻辑时InvokeEvent不要绕过基类除非有意为之用vtkSetMacro/ 成员保存工具状态橡皮筋起点、测量点等在析构或工具退出时RemoveObservers避免悬空clientDataclassMeasureStyle:publicvtkInteractorStyleTrackballCamera{public:staticMeasureStyle*New();vtkTypeMacro(MeasureStyle,vtkInteractorStyleTrackballCamera);voidOnLeftButtonDown()override{// 记录测距点或交给 vtkPointHandleWidgetthis-InvokeEvent(vtkCommand::LeftButtonPressEvent,nullptr);// 若仍要保留旋转vtkInteractorStyleTrackballCamera::OnLeftButtonDown();}};3.7 Style 与 3D Widget、AbortFlag 的分工机制模式角色适用场景vtkInteractorStyle全局交互策略浏览、框选、图像窗宽窗位vtkAbstractWidgetvtkInteractorObserver局部工具可GrabFocusgizmo、测距手柄、裁剪盒vtkCommand::AbortFlag责任链截断Widget 消费事件后阻止 Style 继续处理不要把所有交互都塞进一个巨型 Style 子类常见做法默认TrackballCamera浏览进入编辑工具时启用 WidgetGrabFocus或临时换 RubberBand Style退出时恢复默认 Style详见 10 责任链。4. 绘制策略vtkMapper深度剖析4.1 为什么 Mapper 是策略渲染管线里数据vtkPolyData、vtkImageData…与怎么画是分开的vtkAlgorithm (Filter) → vtkDataSet ↓ vtkMapper ← Strategy着色、图元类型、GPU 路径 ↓ vtkActor / vtkVolume ↓ vtkRenderervtkActor::SetMapper(vtkMapper*)与SetInteractorStyle同构Prop 不关心具体绘制算法只持有抽象vtkMapper。4.2 常见 Mapper 策略表多边形面片Mapper输入策略语义vtkPolyDataMappervtkPolyData经典三角面渲染vtkOpenGLPolyDataMappervtkPolyDataOpenGL 路径现代默认后端vtkCompositePolyDataMapper复合vtkMultiBlockDataSet多块数据一次绘制体绘制VolumeMapper策略语义vtkFixedPointVolumeRayCastMapper固定点 CPU 光线投射vtkGPUVolumeRayCastMapperGPU 光线投射大数据常用vtkSmartVolumeMapper根据数据与硬件自动选择后端策略 简单工厂点、线、实例化Mapper策略语义vtkGlyph3DMapper每个点实例化一个图元箭头、球vtkPointGaussianMapper大范围点云近似高斯 splatvtkLabeledDataMapper在点上画数值标签图像 / 2DMapper策略语义vtkImageSliceMapper轴位切片vtkImageResliceMapper任意平面重切片4.3 切换 Mapper 示例同一 Actor从面片显示切到点云高亮vtkNewvtkPolyDataMappersolidMapper;solidMapper-SetInputConnection(polySource-GetOutputPort());vtkNewvtkPointGaussianMapperpointMapper;pointMapper-SetInputConnection(polySource-GetOutputPort());pointMapper-EmissiveOff();vtkNewvtkActoractor;actor-SetMapper(solidMapper);renderer-AddActor(actor);voidshowAsPoints(vtkActor*actor){actor-SetMapper(pointMapper);actor-GetProperty()-SetPointSize(3);}voidshowAsSurface(vtkActor*actor){actor-SetMapper(solidMapper);}4.4 Mapper 策略的底层注意点输入数据类型必须匹配vtkPolyDataMapper不能接vtkImageData否则空渲染或断言。Modified()与管线更新换 Mapper 后通常actor-Modified()Render()若接 Filter 输出确保Update()已执行。Scalar 与 LookupTable同一 Mapper 换标量数组时策略仍是「多边形绘制」但着色规则靠vtkMapper::SetScalarVisibility、SetColorMode等——属于 Mapper内部参数不必换子类。Volume 的 GPU / CPU体数据很大时vtkGPUVolumeRayCastMapper与 CPU 实现在性能与精度上差异大vtkSmartVolumeMapper用策略 工厂减轻选择负担。与 Filter 策略的区别Filter改数据几何、拓扑、标量Mapper改绘制方式面扫软件里「网格简化」是 Filter「线框 / 实体显示」常是 Mapper vtkProperty。5. 拾取策略vtkAbstractPicker家族点击「选中牙齿 / 选中模型」依赖拾取算法又是一组可替换策略。5.1 继承结构简化vtkAbstractPicker ├── vtkPicker // 基于图形硬件 遍历 Prop常用 ├── vtkCellPicker // 精确到单元可获取世界坐标与 CellId ├── vtkPointPicker // 精确到点 ├── vtkPropPicker // 只选 Prop不深入几何 ├── vtkWorldPointPicker // 屏幕坐标 → 世界射线与平面交点 └── vtkAreaPicker // 区域拾取5.2 Context 与使用方式vtkRenderWindowInteractor内建vtkAbstractPicker* Picker可通过SetPicker更换默认拾取策略。两种用法// A. 全局默认 pickervtkNewvtkCellPickercellPicker;cellPicker-SetTolerance(0.001);iren-SetPicker(cellPicker);// B. 单次拾取临时策略不影响 Interactor 默认vtkNewvtkPropPickerpropPicker;if(propPicker-Pick(x,y,0,renderer)){vtkProp*proppropPicker-GetViewProp();}5.3 策略选择指南需求推荐策略快速选中物体vtkPropPicker需要 CellId / 重心坐标vtkCellPicker需要最近顶点vtkPointPicker框选多个 ActorvtkAreaPicker或 RubberBand Style 回调在空白处点地平面vtkWorldPointPicker 约束平面拾取与交互 Style独立Style 决定手势Picker 决定「这一点的几何命中是谁」RubberBand Style 常在SelectionChangedEvent里再调 AreaPicker。6. 其他 VTK 模块中的策略思想扩展阅读模块抽象接口替换的是什么vtkImageInterpolator插值核最近邻 / 线性 / 三次vtkCoordinate坐标系策略World / Display / NormalizedViewportvtkAbstractImageInterpolator重切片采样策略质量 vs 速度vtkTextRenderer字体栅格化后端FreeType 等vtkColorTransferFunctionvtkPiecewiseFunction传递函数体绘制颜色/透明度映射更像策略化配置vtkAlgorithm子类滤波算法严格说是Strategy 管道见 03 管道这些不如 InteractorStyle / Mapper 典型但在读源码时可用同一 lens接口稳定实现可换。7. 多视口与多 Interactor 场景vtkRenderWindow ├── Renderer0 ←→ Interactor0 Style0 └── Renderer1 ←→ Interactor1 Style1 // 或共享一个 Interactor策略每个 Interactor 独立 Style左侧 3D 轨道球右侧 2DvtkInteractorStyleImage共享 InteractorvtkInteractorObserver::SetCurrentRenderer()决定事件发给哪个 Renderer切换 Style 时同步SetDefaultRenderer/SetCurrentRenderervtkInteractorStyleSwitch会转发给子 Style陷阱只换 Style 不设 Renderer会出现「鼠标在动但相机不动」——事件到了 Style但CurrentRenderer为空。8. 策略与其他模式的组合组合说明VTK 例子策略 工厂工厂创建具体策略vtkInteractorStyle::New()、vtkSmartVolumeMapper选后端策略 观察者策略执行时发事件StyleInvokeEvent(InteractionEvent)策略 责任链多 listener 顺序处理Widget AbortFlag Style策略 装饰增强策略对象而非替换vtkLODActor包装绘制见 06 装饰策略 状态管线对象内部阶段vtkAlgorithm执行状态 vs 交互 Style 由用户切换9. 完整示例浏览 / 框选工具切换下面示例展示Context 不变两种ConcreteStrategy热切换并监听框选结果。#includevtkActor.h#includevtkCallbackCommand.h#includevtkCommand.h#includevtkInteractorStyleRubberBand2D.h#includevtkInteractorStyleTrackballCamera.h#includevtkPolyDataMapper.h#includevtkRenderWindow.h#includevtkRenderWindowInteractor.h#includevtkRenderer.h#includevtkSphereSource.hnamespace{voidOnSelection(vtkObject*caller,unsignedlong,void*clientData,void*callData){auto*stylevtkInteractorStyleRubberBand2D::SafeDownCast(caller);if(!style||!callData)return;auto*regionstatic_castint*(callData);// [x0,y0,x1,y1] 屏幕像素// 用 region 做 AreaPicker 或自定义逻辑(void)region;(void)clientData;}}// namespaceintmain(){vtkNewvtkSphereSourcesphere;vtkNewvtkPolyDataMappermapper;mapper-SetInputConnection(sphere-GetOutputPort());vtkNewvtkActoractor;actor-SetMapper(mapper);vtkNewvtkRendererrenderer;renderer-AddActor(actor);vtkNewvtkRenderWindowrw;rw-AddRenderer(renderer);vtkNewvtkRenderWindowInteractoriren;iren-SetRenderWindow(rw);vtkNewvtkInteractorStyleTrackballCameranavStyle;vtkNewvtkInteractorStyleRubberBand2DselectStyle;vtkNewvtkCallbackCommandselCmd;selCmd-SetCallback(OnSelection);selectStyle-AddObserver(vtkCommand::SelectionChangedEvent,selCmd);iren-SetInteractorStyle(navStyle);// 默认浏览// 模拟用户点「框选」按钮时// iren-SetInteractorStyle(selectStyle);// 完成后恢复 iren-SetInteractorStyle(navStyle);rw-Render();iren-Start();return0;}设计要点iren自始至终是 ContextnavStyle/selectStyle是长期复用的策略实例框选结果通过Observer传出不把业务逻辑写进 Style 源码恢复浏览 再次SetInteractorStyle(navStyle)—— 纯策略切换10. 最佳实践与常见陷阱10.1 实践清单依赖抽象类型函数参数写vtkInteractorStyle*/vtkMapper*不要写死具体子类策略实例复用热切换用vtkSmartPointer缓存避免每帧New()保存默认 Style进入工具前auto* prev iren-GetInteractorStyle()退出时恢复自定义 Style 继承最近子类少重写、多Superclass::OnLeftButtonDown()Mapper 与数据类型对照表写进项目文档防止接错拾取容差SetTolerance按场景调单元过密时CellPicker可能变慢多视口换 Style 时检查CurrentRenderer10.2 陷阱现象常见原因换了 Style 鼠标无反应Enable()未开、Interactor 未Initialize、Renderer 未绑定框选后无回调监听对象应是RubberBand Style上的SelectionChangedEvent换 Mapper 后全黑输入数据类型不匹配、标量未开启、Camera 未 ResetCamera旋转与框选同时触发Widget 未GrabFocus/ 未AbortFlagOn()内存泄漏感Style 被 Interactor 持有引用计数但clientData原始指针仍需自己保证生命周期把 Filter 当 Strategy 换Filter 改数据应SetInputConnection更新管线不是SetInteractorStyle11. 一张总图┌──────────────────────────────────────┐ │ vtkRenderWindowInteractor │ │ SetInteractorStyle( vtkInteractorStyle* ) │ └─────────────────┬────────────────────┘ │ ┌───────────────────────┼───────────────────────┐ ▼ ▼ ▼ TrackballCamera RubberBand2D ImageStyle 浏览策略 框选策略 窗宽窗位策略 │ │ └───────────┬───────────┘ ▼ vtkRenderer::Render() │ ┌───────────┴───────────┐ ▼ ▼ vtkActor vtkVolume SetMapper(…) SetMapper(…) │ │ PolyDataMapper GPUVolumeRayCastMapper 面片策略 体绘制策略 │ │ └───────────┬───────────┘ ▼ 帧缓冲 / 显示 点击选中vtkAbstractPicker 策略Cell / Prop / Point …12. 总结问题答案VTK 策略模式最典型落点vtkInteractorStyle交互与vtkMapper绘制Context 是谁主要是vtkRenderWindowInteractor、vtkActor/vtkVolume怎么切换SetInteractorStyle()、SetMapper()、SetPicker()和 Observer 关系Style 处理手势并InvokeEvent切换策略本身不依赖 Observer和 State 区别Style 由应用显式切换不是对象内部自动变模式进阶组合vtkInteractorStyleSwitch、Widget AbortFlag、SmartVolumeMapperVTK 的策略模式不需要额外框架虚函数 SetXxx()持有指针 引用计数就是运行期可替换算法。读懂 Interactor 壳与 Style 委托、Actor 与 Mapper 分离就掌握了 VTK 里一半以上的「可扩展交互与显示」扩展点。重点与注意进阶速记重点vtkRenderWindowInteractor官方自述为shell手势算法全在vtkInteractorStyle—— 这是 Context / Strategy 最干净的一例。重点SetInteractorStyle与SetMapper同一套路换实现不改调用方前者管输入后者管输出。重点默认vtkInteractorStyleSwitch说明 VTK 自己也把「多种 Style」再封装成一层可切换策略。注意策略显式Set≠ 状态对象内部自动变≠ 模板方法vtkAlgorithm管线骨架。注意复杂工具用Widget GrabFocus/AbortFlag不要无限膨胀 Style 子类。注意vtkCommand是事件回调不是策略Strategy 是InteractorStyle / Mapper / Picker这类「一整条算法」的可替换对象。