
OHIF Viewer ToolbarService 深度指南工具栏按钮定义、分组管理与命令执行机制【免费下载链接】ViewersOHIF zero-footprint DICOM viewer and oncology specific Lesion Tracker, plus shared extension packages项目地址: https://gitcode.com/GitHub_Trending/vi/Viewers本文以 OHIF Viewer 官方文档中关于ToolbarService工具栏服务的说明为主体结合当前仓库中 ToolbarService 源码实现、类型定义 与cornerstone扩展的 工具栏模块、按钮定义 等实战代码系统讲解如何在 OHIF 模式下注册、分组、管理工具栏按钮理解按钮点击后的命令执行链路、evaluate 状态求值机制与嵌套下拉按钮配置帮助开发者快速上手自定义自己的 DICOM 查看器工具栏。OverviewToolbarService 的职责边界ToolbarService是一个“轻量级”服务它只负责工具栏的结构管理添加按钮add buttons、配置按钮configure them、按分组sections组织按钮。当按钮被点击时它负责执行按钮定义中指定的命令。在早期版本中该服务还承担了按钮状态管理与交互逻辑但这些功能如今已经全部移交给了ToolBarModule即扩展中注册的 UI 组件与 evaluate 函数和evaluators求值器。也就是说ToolbarService 管“有什么按钮、按钮在哪个分组”ToolBarModule / evaluators 管“按钮长什么样、什么时候可用、什么时候高亮”。从源码看ToolbarService继承自PubSubService发布订阅基类其内部状态只有两块ToolbarService.tsstate: { // 所有按钮及其 props buttons: Recordstring, Button; // 按钮按 section 分组值是按钮 id 数组 buttonSections: Recordstring, string[]; } { buttons: {}, buttonSections: {} };服务通过CommandsManager执行命令、通过ExtensionManager查找toolbarModule中注册的 UI 组件、通过ServicesManager访问其他服务如viewportGridService。其注册方式定义在静态属性REGISTRATION中服务名为toolbarService别名ToolBarServicepublic static REGISTRATION { name: toolbarService, altName: ToolBarService, create: ({ commandsManager, extensionManager, servicesManager }) { return new ToolbarService(commandsManager, extensionManager, servicesManager); }, };Events工具栏事件ToolbarService继承自PubSubService其EVENTS定义如下ToolbarService.tsEvent触发时机源码中的事件字符串TOOL_BAR_MODIFIED工具栏中添加/移除按钮时触发event::toolBarService:toolBarModifiedTOOL_BAR_STATE_MODIFIED发生交互且 ToolbarService 状态被修改时触发event::toolBarService:toolBarStateModifiedTOOL_BAR_MODIFIED在register()、removeButton()、setButtons()、updateSection()、clearButtonSection()等方法中都会通过_broadcastEvent广播而TOOL_BAR_STATE_MODIFIED主要在按钮options交互如滑条/单选控件改变值时广播见 ToolbarService.ts。消费方例如 UI 层或其它服务可以通过toolbarService.subscribe(...)订阅这些事件来刷新界面。API 速览官方文档给出了服务对外提供的核心 API结合源码可以归纳如下createButton(options)静态工厂方法规范化按钮定义。options支持id、label、commands必填、icon、tooltip缺省时等于label、evaluate、listeners见 ToolbarService.ts。addButtons(buttons, replace)向服务中添加按钮定义。⚠️ 注意当前版本中该方法已被标记为deprecated控制台会输出警告建议改用register(buttons, replace)见 ToolbarService.ts。register接受replace参数为true时覆盖已存在的同名按钮另外当按钮的props.buttonSection true时会将该按钮的id作为其分组键ToolbarService.ts。removeButton(buttonId)从工具栏移除按钮同时会从所有 section 中清除该按钮的 id并广播TOOL_BAR_MODIFIED。setButtons(buttons)整体覆盖式设置按钮会替换掉之前所有按钮。它不会触发 evaluate 求值因此不适合用来记录一次交互适合“已知全部按钮、一次性设置”的场景。getOptionById(button, optionId)在给定按钮的props.options数组中查找指定optionId的选项。updateSection(key, buttons)创建/更新按钮分组向已存在的分组追加按钮时不会重复添加相同 id。⚠️ 原文档中的createButtonSection(key, buttons)同样已被标记为 deprecated建议改用updateSectionToolbarService.ts。其它常用方法getButton(id)、getButtons()、getButtonProps(id)、getButtonSection(sectionId, props)、getButtonPropsInButtonSection(sectionId)、clearButtonSection(buttonSection)、isInAnySection(buttonId)、recordInteraction(interaction, options)、refreshToolbarState(refreshProps)、registerEvaluateFunction(name, handler)、registerEventForToolbarUpdate(service, events)、getToolNameForButton(button)等。预设工具栏分组TOOLBAR_SECTIONS服务静态导出了一组预设分组键ToolbarService.ts开发者在引用分组时可以直接使用避免手写字符串出错export const TOOLBAR_SECTIONS { primary: primary, // 主工具栏 secondary: secondary, // 次要工具栏 viewportActionMenu: { // 视口角落操作菜单 topLeft: viewportActionMenu.topLeft, topRight: viewportActionMenu.topRight, bottomLeft: viewportActionMenu.bottomLeft, bottomRight: viewportActionMenu.bottomRight, topMiddle: viewportActionMenu.topMiddle, bottomMiddle: viewportActionMenu.bottomMiddle, leftMiddle: viewportActionMenu.leftMiddle, rightMiddle: viewportActionMenu.rightMiddle, }, // 模式专用分组 labelMapSegmentationToolbox: labelMapSegmentationToolbox, contourSegmentationToolbox: contourSegmentationToolbox, labelMapSegmentationUtilities: labelMapSegmentationUtilities, contourSegmentationUtilities: contourSegmentationUtilities, dynamicToolbox: dynamic-toolbox, roiThresholdToolbox: ROIThresholdToolbox, };例如 toolbarButtonsCustomization.ts 中就是通过const { TOOLBAR_SECTIONS } ToolbarService;引用的。此外服务还提供ButtonLocation枚举TopLeft、TopMiddle、TopRight、LeftMiddle、RightMiddle、BottomLeft、BottomMiddle、BottomRight配合getAlignAndSide(location)方法可计算角落菜单的对齐方式与弹出方向ToolbarService.ts。Button Definitions按钮定义详解基础按钮Basic最简单的基础按钮定义只包含以下几个属性{ id: Zoom, uiType: ohif.radioGroup, props: { icon: tool-zoom, label: Zoom, commands: [ { commandName: setToolActive, commandOptions: { toolName: Zoom, }, context: CORNERSTONE, }, ], evaluate: evaluate.cornerstoneTool, }, }属性说明取值id按钮的唯一字符串标识任意唯一字符串icon图标名称字符串由消费应用提供对应图标任意label显示在 UI 上的用户友好名称任意commands可选点击按钮时执行的命令包含commandName、commandOptions和/或context任意由CommandModule注册的命令evaluate可选状态求值函数名或函数决定按钮的可用/高亮状态如evaluate.cornerstoneTooltooltip可选悬浮提示文本缺省时等于label任意从类型定义看Button由id、props、uiType和可选的component组成types.ts其中uiType对应扩展通过toolbarModule注册的 UI 组件名称如ohif.radioGroup、ohif.toolButton、ohif.splitButton、ohif.toolButtonListToolbarService._getButtonUITypes()会从extensionManager.modules[toolbarModule]收集这些注册项ToolbarService.ts。命令字段的三种写法在实际源码中commands字段非常灵活ToolbarService.ts对象或对象数组最常用如{ commandName: setToolActive, commandOptions: {...}, context: CORNERSTONE }字符串直接引用命令名如commands: toggleEnabledDisabledToolbar见 toolbarButtonsCustomization.ts函数会被包装成接收{ ...commandOptions, commandsManager, servicesManager }的回调。在recordInteraction()中interaction可以是字符串此时视为按钮 id会自动getButtonProps取出完整 props最终通过this._commandsManager.run(commands, commandOptions)执行命令随后调用refreshToolbarState()刷新按钮状态。基础命令示例setToolActive 与 setToolActiveToolbar官方文档示例中的setToolActive命令来自cornerstone扩展的 commandsModule.ts其核心逻辑为默认绑定主鼠标键bindings [{ mouseButton: Enums.MouseBindings.Primary }]先取当前 tool group 的活动工具若其配置了disableOnPassive则setToolDisabled否则setToolPassive再将新工具setToolActive。而setToolActiveToolbarcommandsModule.ts是面向工具栏的批量版本toolName可以从toolName、itemId或value中获取toolGroupIds未指定时默认对所有 tool group 生效。这也是 toolbarButtonsCustomization.ts 中定义并复用的命令export const setToolActiveToolbar { commandName: setToolActiveToolbar, commandOptions: { toolGroupIds: [default, mpr, SRToolGroup, volume3d], }, };Nested (dropdown)嵌套下拉按钮ohif.splitButton类型的按钮可以构建带下拉菜单的“主按钮 更多工具”结构primary主按钮的工具定义secondary次要部分通常是一个向下箭头chevron-down图标items下拉列表中的额外工具。官方文档以longitudinal模式中的MeasurementTools嵌套按钮为例完整展示了这种写法。下面是文档示例的完整版其中ToolbarService.createButton用于规范化子按钮定义{ id: MeasurementTools, uiType: ohif.splitButton, props: { groupId: MeasurementToolsGroupId, // group evaluate 决定哪个 item 应被提升到 primary 位置 evaluate: evaluate.group.promoteToPrimaryIfCornerstoneToolNotActiveInTheList, primary: ToolbarService.createButton({ id: Length, icon: tool-length, label: Length, tooltip: Length Tool, commands: _createSetToolActiveCommands(Length), evaluate: evaluate.cornerstoneTool, }), secondary: { icon: chevron-down, tooltip: More Measure Tools, }, items: [ ToolbarService.createButton({ id: Length, icon: tool-length, label: Length, tooltip: Length Tool, commands: [ { commandName: setToolActive, commandOptions: { toolName: Length }, context: CORNERSTONE, }, { commandName: setToolActive, commandOptions: { toolName: SRLength, toolGroupId: SRToolGroup }, // 该命令同样来自 Cornerstone commandsModule可用于 SR 工具组 context: CORNERSTONE, }, ], evaluate: evaluate.cornerstoneTool, }), ToolbarService.createButton({ id: Bidirectional, icon: tool-bidirectional, label: Bidirectional, tooltip: Bidirectional Tool, commands: [ { commandName: setToolActive, commandOptions: { toolName: Bidirectional }, context: CORNERSTONE, }, { commandName: setToolActive, commandOptions: { toolName: SRBidirectional, toolGroupId: SRToolGroup }, context: CORNERSTONE, }, ], evaluate: evaluate.cornerstoneTool, }), ], }, }分组求值器group evaluator的关键作用官方文档用两个 tip 强调了 group evaluator 的行为Tip 1split button 可以配置一个分组求值器上例中的evaluate.group.promoteToPrimaryIfCornerstoneToolNotActiveInTheList它决定用户与按钮交互时发生什么。在上述示例中如果列表中的 cornerstone 工具未被激活就把该按钮提升到 primary 位置。还有其它求值器例如evaluate.group.promoteToPrimary它不关心 cornerstone 工具状态直接无条件提升到 primary。Tip 2如果不提供group evaluator则什么都不会发生——按钮将一直停留在 secondary 下拉区域中。从源码看refreshToolbarState()中对于带buttonSection的嵌套按钮会先执行groupEvaluate取disabled/disabledText再对该分组内的所有按钮逐一执行 evaluateToolbarService.ts这正是“分组求值器决定子按钮状态”的底层机制。Evaluators状态求值机制按钮的evaluate字段决定按钮的可用性disabled、高亮isActive、可见性visible与类名className。evaluate支持四种写法types.ts字符串直接引用已注册的求值函数名如evaluate.cornerstoneTool函数(props) ({ disabled, className })对象{ name: ..., ...额外参数 }执行时额外参数会被合并进入参数组多个求值器组合结果为依次合并只要有一个求值器返回disabled按钮整体即为禁用。求值函数通过toolbarService.registerEvaluateFunction(name, handler)注册。在cornerstone扩展的 getToolbarModule.tsx 中注册了大量内置求值器其中最核心的是evaluate.cornerstoneToolgetToolbarModule.tsx{ name: evaluate.cornerstoneTool, evaluate: ({ viewportId, button, toolNames, disabledText }) { const toolGroup toolGroupService.getToolGroupForViewport(viewportId); if (!toolGroup) return; const toolName toolbarService.getToolNameForButton(button); if (!toolGroup || (!toolGroup.hasTool(toolName) !toolNames)) { return getDisabledState(disabledText); // 工具不在 tool group 中 禁用 } const isPrimaryActive toolNames ? toolNames.includes(toolGroup.getActivePrimaryMouseButtonTool()) : toolGroup.getActivePrimaryMouseButtonTool() toolName; return { disabled: false, isActive: isPrimaryActive }; }, }其它常用内置求值器还包括evaluate.cornerstoneTool.toggle切换型工具offModes为Disabled与Passiveevaluate.cornerstoneTool.toggleWithModifier支持修饰键绑定的切换工具可根据toggledOnIcon动态切换图标evaluate.cornerstoneTool.toggle.ifStrictlyDisabled仅在工具被Disabled时视为“关闭”evaluate.cornerstone.synchronizer同步器相关evaluate.action普通动作按钮恒可用evaluate.modalityLoadBadge、evaluate.navigationComponent、evaluate.trackingStatus、evaluate.orientationMenu、evaluate.dataOverlayMenu等视口/模态相关求值器。在handleEvaluate()ToolbarService.ts中字符串/对象/数组形式的evaluate会被解析并转换为真正的函数引用若找不到对应函数会抛出错误并提示“you can register an evaluate function with the getToolbarModule in your extensions”。另外refreshToolbarState()还会处理hideWhenDisabled当求值结果未显式给出visible时若hideWhenDisabled为true且按钮处于禁用状态则该按钮自动隐藏ToolbarService.ts。例如 toolbarButtonsCustomization.ts 中的modalityLoadBadge按钮就使用了{ name: evaluate.modalityLoadBadge, hideWhenDisabled: true }。Listeners按钮事件监听有时按钮需要监听特定事件才能正确响应。可以通过listeners字段注册事件监听器每个事件对应一组命令RunCommand。官方文档以Reference Lines 工具为例它需要在活动视口变化时设置自己的参考源因此监听了视口网格服务的两个事件ViewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED活动视口变化时ViewportGridService.EVENTS.VIEWPORTS_READY视口网格就绪时。const ReferenceLinesListeners [ { commandName: setSourceViewportForReferenceLinesTool, context: CORNERSTONE, }, ]; ToolbarService.createButton({ id: ReferenceLines, icon: tool-referenceLines, label: Reference Lines, tooltip: Show Reference Lines, commands: [ { commandName: setToolEnabled, commandOptions: { toolName: ReferenceLines, toggle: true }, context: CORNERSTONE, }, ], listeners: { [ViewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED]: ReferenceLinesListeners, [ViewportGridService.EVENTS.VIEWPORTS_READY]: ReferenceLinesListeners, }, evaluate: evaluate.cornerstoneTool.toggle, });当前仓库的实际实现与此一脉相承toolbarButtonsCustomization.ts 中的ReferenceLines按钮使用commands: toggleEnabledDisabledToolbar并同样通过listeners监听上述两个视口网格事件触发setViewportForToolConfiguration回调callbacks(ReferenceLines)。此外事件监听并非按钮专属——ToolbarService自身也提供registerEventForToolbarUpdate(service, events)用于订阅其它服务如viewportGridService的事件并在事件发生时自动refreshToolbarState()ToolbarService.ts。Button Sections按钮分组管理为了组织按钮可以创建多个分组section并分别向各分组分配按钮。OHIF 默认提供primary分组。除此之外官方文档提示可以按需增加更多分组并特别提到toolBox实现利用了“为带高级选项的工具提供专用分组”的能力——该能力在 segmentation 模式中被使用。官方文档示例longitudinal模式在onModeEnter钩子中注册按钮并分配到 primary 分组toolbarService.addButtons([...toolbarButtons, ...moreTools]); toolbarService.createButtonSection(primary, [ MeasurementTools, Zoom, info, WindowLevel, Pan, Capture, Layout, Crosshairs, MoreTools, ]);如上所示创建分组时直接以按钮id引用按钮即可把按钮归入对应分组。Tip同一个按钮可以出现在多个分组中得益于求值系统它在所有分组中会保持状态同步。Note不要忘记配置好toolGroups才能保证按钮正常工作。按钮只是视觉界面交互时它执行命令交互后的状态由 evaluators 决定——而命令如setToolActive能否真正生效取决于对应工具是否已加入相应 tool group。当前版本更推荐的组合方式结合当前仓库源码longitudinal模式的onModeEnter写法可以直接替换为更新版本的等价 API。以 modes/basic/src/index.tsx 中onModeEnter的实现为例现代写法通过customizationService.getCustomization(toolbarButtons)/getCustomization(toolbarSections)读取模式组合再交由registerModeToolbar()统一注册modeCustomization.tsexport function registerModeToolbar({ toolbarService }, { toolbarButtons, toolbarSections }): void { toolbarService.register(toArray(toolbarButtons)); // 等价于旧的 addButtons const sections: Recordstring, string[] Object.assign({}, ...toArray(toolbarSections)); for (const [key, section] of Object.entries(sections)) { toolbarService.updateSection(key, section); // 等价于旧的 createButtonSection } }其中register与updateSection正是addButtons/createButtonSection的替代 API。modeInstance中通过toolbarButtons: [{ $reference: cornerstone.toolbarButtons }]与toolbarSections: [{ $reference: cornerstone.toolbarSections }]声明组合modes/basic/src/index.tsx由 customization service 在运行时解析展开。cornerstone扩展贡献的默认toolbarButtons定义位于 toolbarButtonsCustomization.ts其中大量使用了buttonSection: true的分组按钮如MeasurementTools、MoreTools、AdvancedRenderingControls与前述setToolActiveToolbar命令、evaluate求值器组合是研究按钮定义的绝佳样板。总结一次工具栏交互的完整链路将以上内容串起来一次典型的工具栏交互完整链路如下注册阶段模式onModeEnter调用toolbarService.register(buttons)注册按钮定义调用updateSection(key, ids)或旧的createButtonSection组织分组渲染阶段UI 层调用getButtonSection(sectionId, props)取分组按钮_mapButtonToDisplay()依据uiType从toolbarModule查找组件并绑定 propsToolbarService.ts交互阶段用户点击按钮 → UI 组件触发toolbarService.recordInteraction(...)→ 解析commands并经CommandsManager.run()执行如setToolActive/setToolActiveToolbar状态刷新阶段执行完毕后调用refreshToolbarState({ viewportId })对每个按钮执行evaluate求值器如evaluate.cornerstoneTool更新disabled/isActive/visible/className并广播TOOL_BAR_MODIFIED事件监听补充需要响应视口变化等事件的按钮通过listeners订阅ViewportGridService等事件并执行对应命令。通过掌握 ToolbarService 的 API、按钮定义结构、嵌套分组、evaluate 求值器与 listeners 监听机制你可以在 OHIF Viewer 中灵活搭建符合自己产品形态的工具栏——无论是简单的工具激活按钮还是带下拉的高级测量工具组亦或是随视口状态自动显隐的智能按钮。【免费下载链接】ViewersOHIF zero-footprint DICOM viewer and oncology specific Lesion Tracker, plus shared extension packages项目地址: https://gitcode.com/GitHub_Trending/vi/Viewers创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考