OHIF WorkflowStepService:为 DICOM 影像应用编排多步骤临床工作流
2026/9/18 5:27:56 网站建设 项目流程

OHIF WorkflowStepService:为 DICOM 影像应用编排多步骤临床工作流

【免费下载链接】ViewersOHIF zero-footprint DICOM viewer and oncology specific Lesion Tracker, plus shared extension packages项目地址: https://gitcode.com/GitHub_Trending/vi/Viewers

在 OHIF 中,复杂的多模态影像任务(如预临床 4D PET/CT 的配准、ROI 定量、动力学分析)往往需要用户按既定顺序逐阶段操作。WorkflowStepService 正是为此设计:它允许把一个模式(Mode)的工作流拆分成若干“步骤”,每个步骤独立配置挂片协议(Hanging Protocol)、面板布局、工具栏按钮与进入/退出回调,并随用户切换步骤动态驱动查看器界面。读完本文,你可以掌握工作流步骤的完整结构定义、在 Mode 中的集成时机、步骤切换时的底层执行顺序,以及配套的 ProgressDropdown 导航 UI 的实现原理。

什么是 WorkflowStepService

WorkflowStepService 是 OHIF 核心平台提供的一个服务,其源码位于 WorkflowStepsService.ts,并通过 services/index.ts 注册进服务集合(REGISTRATION.nameworkflowStepsService,见 appInit.js 的服务列表),类型声明挂载在 Services.ts 与 AppTypes.ts 上,因此任何 Mode 中都可以从servicesManager.services.workflowStepsService拿到它。

从源码结构看,该服务继承自 PubSubService,对外暴露两个事件(L5-L8):

事件常量事件名触发时机
EVENTS.ACTIVE_STEP_CHANGEDevent::workflowStepsService:activateStepChanged激活的步骤切换成功后广播,载荷为{ activeWorkflowStep }
EVENTS.STEPS_CHANGEDevent::workflowStepsService:stepsChanged步骤列表发生新增后广播

这两个事件是 UI 组件(如进度下拉框)与核心状态保持同步的关键通道。

工作流步骤的解剖结构(Anatomy of a Workflow Step)

每个工作流步骤由若干组件/属性共同定义,用于定制该阶段的应用界面、可用工具与行为。源码中的 TypeScript 类型WorkflowStep(L62-L81)给出了完整的字段约束:

字段类型必填作用
idstring步骤的唯一标识,重复注册会直接抛出Duplicated workflow step id错误(L111-L130)
namestring人类可读的名称,显示在界面中帮助用户理解当前阶段
hangingProtocol{ protocolId, stageId? }指定本步骤使用的挂片协议与 stage,确保显示正确的数据视口与呈现方式
layout{ panels: { left?, right? }, options? }定义左右两侧面板的组成与可见性选项(如leftPanelClosedrightPanelClosed
toolbarButtons{ buttonSection, buttons: string[] }[]本步骤工具栏可用的按钮。注意:按钮必须已事先通过toolbarService注册,这里只是引用按钮 id
onEnter函数或{ commandName, options }[]进入步骤时执行的回调或命令
onExit函数或{ commandName, options }[]离开步骤时执行的回调或命令
infostring附加说明,在 UI 中作为 tooltip 显示

需要注意toolbarButtons的写法:源码_updateToolBar(L132-L142)会将单个对象或数组统一归一化为数组,然后对每个{ buttonSection, buttons }clearButtonSectionupdateSection,即切换步骤会清空并重建对应按钮分区,保证不同步骤呈现完全不同的工具集。

一个完整的步骤配置示例

以下示例展示了 pre-clinical 4D 工作流的步骤定义(与仓库中 getWorkflowSettings.ts 的实际用法一致):

const dynamicVolume = { sopClassHandler: "@ohif/extension-cornerstone-dynamic-volume.sopClassHandlerModule.dynamic-volume", leftPanel: "@ohif/extension-cornerstone-dynamic-volume.panelModule.dynamic-volume", toolBox: "@ohif/extension-cornerstone-dynamic-volume.panelModule.dynamic-toolbox", export: "@ohif/extension-cornerstone-dynamic-volume.panelModule.dynamic-export", } const cs3d = { segmentation: "@ohif/extension-cornerstone-dicom-seg.panelModule.panelSegmentation", } const steps = [ { id: "dataPreparation", name: "Data Preparation", layout: { panels: { left: [dynamicVolume.leftPanel], }, }, toolbarButtons: { buttonSection: "primary", buttons: ["MeasurementTools", "Zoom", "WindowLevel", "Crosshairs", "Pan"], }, hangingProtocol: { protocolId: "default4D", stageId: "dataPreparation", }, info: "In the Data Preparation step...", }, { id: "roiQuantification", name: "ROI Quantification", layout: { panels: { left: [dynamicVolume.leftPanel], right: [ [dynamicVolume.toolBox, cs3d.segmentation, dynamicVolume.export], ], }, options: { leftPanelClosed: false, rightPanelClosed: false, }, }, toolbarButtons: [ { buttonSection: "primary", buttons: [ "MeasurementTools", "Zoom", "WindowLevel", "Crosshairs", "Pan", ], }, { buttonSection: "dynamic-toolbox", buttons: ["BrushTools", "RectangleROIStartEndThreshold"], }, ], hangingProtocol: { protocolId: "default4D", stageId: "roiQuantification", }, info: "The ROI quantification step ...", }, { id: "kineticAnalysis", name: "Kinetic Analysis", layout: { panels: { left: [dynamicVolume.leftPanel], right: [], }, }, toolbarButtons: { buttonSection: "primary", buttons: ["MeasurementTools", "Zoom", "WindowLevel", "Crosshairs", "Pan"], }, hangingProtocol: { protocolId: "default4D", stageId: "kineticAnalysis", }, onEnter: [ { commandName: "updateSegmentationsChartDisplaySet", options: { servicesManager }, }, ], info: "The Kinetic Analysis step ...", }, ]

示例中三个步骤分别对应“数据准备 → ROI 定量 → 动力学分析”:第二步启用了dynamic-toolbox分区用于分割工具,第三步通过onEnter命令在进入时刷新分割结果对应的图表 displaySet。完整的四步配置(含registration步骤)可参考 getWorkflowSettings.ts。

在 Mode 中集成工作流步骤

定义好步骤后,需要在 Mode 工厂中完成集成。官方文档明确指出:这些步骤应当在onSetupRouteComplete中注册,而不能在onModeEnter中调用——因为onModeEnter触发时 Mode 尚未完全初始化(挂片协议匹配等流程可能还未完成,它会依据工作流 stage 设置改写 protocol/stage)。

仓库中 preclinical-4d 模式 给出了完整示例。Mode 工厂的onSetupRouteComplete回调(L127-L131)委托给一个初始化函数:

// modes/preclinical-4d/src/initWorkflowSteps.ts export default function initWorkflowSteps({ servicesManager }: withAppTypes): void { const { workflowStepsService } = servicesManager.services; const workflowSettings = getWorkflowSettings({ servicesManager }); workflowStepsService.addWorkflowSteps(workflowSettings.steps); workflowStepsService.setActiveWorkflowStep(workflowSettings.steps[0].id); }

对应文档中的最简集成形式为:

onSetupRouteComplete: ({ servicesManager }) => { workflowStepsService.addWorkflowSteps(workflowSettings.steps); workflowStepsService.setActiveWorkflowStep(workflowSettings.steps[0].id); },

这里有两个值得注意的事实:

  1. addWorkflowSteps幂等性检查id重复会抛出异常;全部新增成功且至少有一个新步骤时,广播STEPS_CHANGED事件。
  2. Mode 生命周期中的重置WorkflowStepsService实现了onModeEnter()(L231-L233),内部调用reset()清空步骤列表与激活步骤。也就是说每次进入 Mode 时状态都是干净的,随后由onSetupRouteComplete重新注册——这正是“不能在onModeEnter里注册步骤、要等路由完成后再注册”的生命周期约束的由来。

此外,preclinical-4d 模式还通过hangingProtocol: 'default4D'(index.tsx L200)声明了默认协议,步骤配置中的protocolId即引用它。

步骤切换时的底层执行顺序

理解setActiveWorkflowStep(workflowStepId)(L194-L224)的内部时序,是掌握该服务行为的关键。源码中的执行顺序为:

  1. 快速返回:目标 id 与当前激活步骤相同则不做任何事;
  2. 查找步骤:找不到对应 id 时抛出Invalid workflowStepId (...)
  3. 执行旧步骤的onExit回调(若存在激活步骤);
  4. 执行新步骤的onEnter回调——源码注释特别强调这一步必须先于挂片协议更新执行,因为某些 displaySet(例如把分割转换为图表 displaySet)需要在切换到新的 HP stage 之前先创建出来;
  5. 更新工具栏_updateToolBartoolbarButtons逐分区清空并重建按钮;
  6. 更新面板_updatePanels调用panelService.setPanels(panels, options),把layout.options(如leftPanelClosed)一并传入(L144-L153);
  7. 更新挂片协议_updateHangingProtocol通过commandsManager.runCommand('setHangingProtocol', { protocolId, stageId, stageIndex })切换显示(L155-L167);
  8. 广播ACTIVE_STEP_CHANGED事件,载荷包含新的activeWorkflowStep

回调本身由_invokeCallbacks(L169-L192)统一处理:它兼容两种形态——直接的函数,或{ commandName, options }形式的命令对象(后者会被包装成commandsManager.runCommand的调用),并且非数组的单回调会被归一化为数组后逐个执行。

ProgressDropdown:步骤导航 UI

OHIF 提供了一个简单的下拉组件用于在步骤间导航。集成方式为向工具栏注册一个按钮并放入secondary分区:

toolbarService.addButtons([ { id: 'ProgressDropdown', uiType: 'ohif.progressDropdown', }, ]) toolbarService.createButtonSection('secondary', ['ProgressDropdown']);

在 preclinical-4d 模式中,ProgressDropdown按钮定义在 toolbarButtons.tsx 中,Mode 的onModeEnter通过toolbarService.updateSection(toolbarService.sections.secondary, ['ProgressDropdown'])将其放入 secondary 位置(index.tsx L59)。如果想把进度下拉框放到其他位置,可以使用 Toolbar 模块的 Toolbox 组件创建自定义按钮分区,详见 Toolbar 模块文档。

从源码结构看,uiType: 'ohif.progressDropdown'背后由 ProgressDropdownWithService.tsx 实现,它的工作方式包括:

  • workflowStepsService.workflowSteps映射为下拉选项,其中每个步骤的name作为 label、id作为 value、info字段作为该选项的 tooltip 提示(这正是步骤info字段在 UI 中的落点);
  • 用户选中某个步骤后,调用workflowStepsService.setActiveWorkflowStep(selectedOption.value)触发整套界面更新,并把该步骤及其之前的步骤标记为completed(源码注释中说明“步骤完成状态目前由导航行为推导,尚非基于用户完成特定动作判定”);
  • 组件订阅STEPS_CHANGEDACTIVE_STEP_CHANGED两个事件,在服务端状态变化时刷新本地下拉选项与选中项,实现 UI 与服务状态的双向同步(L79-L95)。

小结与参考

WorkflowStepService 把“挂片协议 stage、面板布局、工具栏按钮、进入/退出回调”四个维度的配置收敛到单一的步骤对象中,并通过onSetupRouteComplete注册、setActiveWorkflowStep驱动的时序化更新机制,使查看器能够随着临床工作流的推进自动变形。编写新的分步工作流时,建议按以下清单落地:

  1. 在扩展/Mode 中预先用toolbarService注册所有按钮定义(步骤里只引用 id);
  2. 定义步骤数组:id全局唯一,hangingProtocol.stageId与协议中的 stage 对齐;
  3. 在 Mode 工厂的onSetupRouteCompleteaddWorkflowSteps+setActiveWorkflowStep激活首步骤;
  4. 注册ProgressDropdown按钮并放入目标工具栏分区,供用户导航。

关键文件索引:

文件说明
WorkflowStepsService.ts服务核心实现:事件、WorkflowStep类型、addWorkflowStepssetActiveWorkflowStep
services/index.ts服务注册入口
index.tsxpreclinical-4d 模式:Mode 工厂与onSetupRouteComplete集成
getWorkflowSettings.ts四步工作流的完整步骤配置
initWorkflowSteps.ts步骤注册的最简封装
ProgressDropdownWithService.tsx步骤导航下拉框组件实现
Toolbar 模块文档自定义按钮分区(Toolbox)的进一步配置

【免费下载链接】ViewersOHIF zero-footprint DICOM viewer and oncology specific Lesion Tracker, plus shared extension packages项目地址: https://gitcode.com/GitHub_Trending/vi/Viewers

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询