深入解析 MITK/BlueBerry 编辑器:从菜单点击到 QTabBar 的完整架构与呈现链路

深入解析 MITK/BlueBerry 编辑器:从菜单点击到 QTabBar 的完整架构与呈现链路

分析对象:stdmultiwidget / mxnmultiwidget 编辑器打开新标签页的完整链路,以及标签页底层的呈现实现。

涉及模块:org.mitk.gui.qt.extorg.mitk.gui.qt.mxnmultiwidgeteditororg.blueberry.ui.qtModules/QtWidgets


一、NxM 多窗口编辑器的 UI 组织

术语先分清,这里其实是两个不同的 UI:

  • 菜单 → 打开新 tab 页 = 工作台的 Editor(编辑器标签页)机制。
  • 选择 NxM 网格的弹窗 = 该 Editor 工具栏里弹出的布局选择控件(Qt::Popup 浮动窗口,不是新标签)。

1. 菜单项 → 打开新标签页

菜单项在 QmitkExtWorkbenchWindowAdvisor.cpp:762-765 创建,仅当编辑器 org.mitk.editors.mxnmultiwidget 已注册时才有。

QmitkOpenMxNMultiWidgetEditorAction(文本 "MxN Display")的 Run() 调用工作台:

cpp 复制代码
m_Window->GetActivePage()->OpenEditor(
    editorInput, "org.mitk.editors.mxnmultiwidget", true,
    berry::IWorkbenchPage::MATCH_ID);

「新 tab 页」的正体是 BlueBerry(Eclipse RCP 移植)的编辑器区域,而非直接操作 QTabWidget 因为 MATCH_ID,同 ID 编辑器已开就激活,否则新建。DataStorage 通过 DataStorageEditorInput 作为输入传入。

2. 标签页内容 = Editor 部件

继承链:

复制代码
QmitkMxNMultiWidgetEditor
  → QmitkAbstractMultiWidgetEditor  (同时实现 berry::IPartListener)
    → QmitkAbstractRenderEditor
      → berry::EditorPart  (= 成为一个标签页的单位)

CreateQtPartControl()(QmitkMxNMultiWidgetEditor.cpp:163)组装出水平三分结构:

复制代码
QHBoxLayout(边距0)
 ├─ QmitkInteractionSchemeToolBar        左:PACS/MITK 交互切换(平时隐藏)
 ├─ QmitkMxNMultiWidget                   中:NxM 渲染窗口阵列
 └─ QmitkMultiWidgetConfigurationToolBar  右:布局设置 / 同步 / 交互方式

Impl(pimpl)持有左右工具栏;中间 multiWidget 由基类 SetMultiWidget/GetMultiWidget 管理。编辑器注册为 IPartListener(cpp:210),按标签生命周期开关十字线与菜单控件。

3. NxM 网格选择弹窗

右侧配置工具栏 QmitkMultiWidgetConfigurationToolBar 有三个 action(布局设置 / 同步 / 交互方式)。点布局设置按钮 OnSetLayout():

cpp 复制代码
m_LayoutSelectionPopup->setWindowFlags(Qt::Popup);
m_LayoutSelectionPopup->move(cursor().pos().x() - width(), cursor().pos().y());
m_LayoutSelectionPopup->show();

弹窗本体 QmitkMultiWidgetLayoutSelectionWidget(.ui 驱动):QTableWidget 拖选单元格→矩形选区确定 N×M;Set/Save/Load 按钮 + JSON 预设 QComboBox。点 Set 从选区最大 row/col 发出 LayoutSet(row+1, column+1)

4. 信号流

复制代码
QTableWidget选区 → LayoutSelectionWidget::LayoutSet(row,col)
   → ConfigurationToolBar::LayoutSet (中继)
      → QmitkMxNMultiWidgetEditor::OnLayoutSet(row,col)
         → 基类 OnLayoutSet → MultiWidget::SetLayout()
         → EnableCrosshair()
         → FirePropertyChange(PROP_INPUT) 通知工作台输入已变

预设 mxnLayout_*.json 通过 US(CppMicroServices)模块资源加载;SaveLayout/LoadLayoutQt::DirectConnectionstd::ostream* 保证指针有效。


二、编辑器机制(BlueBerry 四层分离)

一句话:BlueBerry 的「编辑器」不是一个 Qt 控件,而是由扩展点声明、由工作台按 ID 管理、延迟实例化、最终塞进标签堆(PartStack)呈现的逻辑单元。

1. 声明层:plugin.xml 扩展点

xml 复制代码
<extension point="org.blueberry.ui.editors">
  <editor id="org.mitk.editors.mxnmultiwidget"
          name="MxN Display"
          class="QmitkMxNMultiWidgetEditor"
          extensions="mitk"
          icon="resources/MxNMultiWidgetEditor.svg"/>
</extension>

启动时 EditorRegistryReader 为每个 <editor> 生成 EditorDescriptor(仅记录 id/name/class/icon,不创建实例 ),存入 EditorRegistry。菜单里 FindEditor("...") 查的就是这张注册表。

2. 触发层:WorkbenchPage::OpenEditor

WorkbenchPage::OpenEditorBusyOpenEditorBusyOpenEditorBatched(berryWorkbenchPage.cpp:2632):

cpp 复制代码
editor = GetEditorManager()->FindEditor(editorID, input, matchFlags);
if (editor != 0) {
    this->ShowEditor(activate, editor);   // 命中 → 提到前面
    return editor;
}
IEditorReference::Pointer ref = GetEditorManager()->OpenEditor(editorID, input, true, editorState);
editor = ref->GetEditor(true);

「打开新 tab」与「切到已有 tab」同一入口,由 FindEditor 分流。

3. 管理层:EditorManager → EditorReference(引用先于实例)

EditorManager::OpenEditorFromDescriptor(berryEditorManager.cpp:462):

cpp 复制代码
result = new EditorReference(this, input, desc, editorState);  // ① 只建"引用"
this->CreateEditorTab(result.Cast<EditorReference>(), "");     // ② 建 tab 外壳

EditorReference(轻量句柄,持 input+descriptor)与 IEditorPart(真正部件)分离。标签先建(标题/图标来自 descriptor),部件本体延迟到真正显示才实例化------这是 Eclipse 著名的 lazy loading。

4. 呈现层:EditorAreaHelper → EditorSashContainer → PartStack

CreateEditorTabEditorAreaHelper::AddEditorAddToLayout:

复制代码
EditorAreaHelper          编辑器区域门面
 └─ EditorSashContainer   可分割容器(拖动分屏, ID_EDITOR_AREA)
     └─ PartStack         "工作簿" = 一组标签的堆叠 = tab folder
         └─ PartPane      单标签容器(承载编辑器物理 QWidget)

5. 实例化层:延迟创建

EditorReference::GetEditor(true)CreatePartHelper(berryEditorReference.cpp:436):

cpp 复制代码
part = manager->CreatePart(desc);            // 按 descriptor.class 反射创建
PartPane::Pointer pane = this->GetPane();
pane->CreateControl(...);
site = manager->CreateSite(this, part, desc, editorInput);
part->CreatePartControl(pane->GetControl()); // ★ 回调到 CreateQtPartControl()
PartTester::TestEditor(part);

6. Site 与 IPartListener

EditorSite(IEditorSite)是部件与工作台的桥:GetSite()->GetPage()->AddPartListener(this)。部件不直接依赖工作台,全走 Site 解耦。

IPartListener 生命周期回调:

回调 时机 MxN 编辑器动作
PartOpened tab 首次打开 EnableCrosshair() + 激活菜单控件
PartVisible tab 切到前台 激活菜单控件
PartHidden tab 被盖住 关闭菜单控件
PartClosed tab 关闭 关闭菜单控件

GetPartEventTypes() 只订阅 CLOSED|OPENED|HIDDEN|VISIBLE

完整调用链

复制代码
菜单 "MxN Display"
  → QmitkOpenMxNMultiWidgetEditorAction::Run()
     → WorkbenchPage::OpenEditor(input, id, MATCH_ID)
        → BusyOpenEditorBatched()
           ├─ FindEditor() 命中 → ShowEditor() 复用 【结束】
           └─ 未命中:
              → EditorManager::OpenEditor()
                 → new EditorReference(input, desc)     ← 引用(未实例化)
                 → CreateEditorTab() → EditorAreaHelper::AddEditor()
                    → EditorSashContainer / PartStack    ← 生成 tab 外壳
              → ref->GetEditor(true)                     ← 延迟实例化
                 → CreatePartHelper()
                    → manager->CreatePart(desc)          ← 反射创建编辑器
                    → CreateSite()
                    → part->CreatePartControl(pane)      ← 回调 CreateQtPartControl()

三、呈现层的具体实现(PartStack → QTabBar)

0. 分层

复制代码
模型层     PartStack (LayoutPart 树节点)                ← 逻辑:一堆标签的容器
   │ 工厂创建
呈现抽象   StackPresentation → TabbedStackPresentation  ← 平台无关的"标签堆"抽象
   │ 组合
折叠器抽象 PresentablePartFolder + AbstractTabFolder    ← 抽象 tab folder 接口
   │ 具体实现
Qt 实现    NativeTabFolder + QCTabBar + NativeTabItem   ← 真正的 QWidget
   │ 内容
内容控件   PartPane::control (CreateQtPartControl 的父)

1. 模型层 PartStack

只维护「堆里有哪些 part」,画图全委托给 StackPresentation(PartStack::GetPresentation())。

2. 呈现抽象

PartStackQtWorkbenchPresentationFactory 创建 TabbedStackPresentation。两个关键抽象:

  • IPresentablePart :包裹 PartPane,暴露 SetVisible/SetBounds/GetName/GetTitleImage,呈现层不知背后是编辑器还是视图。
  • AbstractTabFolder :抽象标签折叠器,定义 Add/SetSelection/SetContent/GetItems

3. 黏合层 PresentablePartFolder(内容切换关键)

构造时(berryPresentablePartFolder.cpp:189):

cpp 复制代码
contentProxy = new QWidget(folder->GetContentParent());  // 空的"占位代理"
folder->SetContent(contentProxy);                        // 塞进 contentFrame

内容区放的不是编辑器本体,而是空 contentProxy 占位符。 编辑器本体独立存在,靠「显示/隐藏 + 对齐占位符几何」呈现------Eclipse 经典 overlay 叠加 策略,而非 QStackedWidget 的 reparent。

Select()(cpp:292):

cpp 复制代码
if (toSelect != 0)  toSelect->SetVisible(true);   // 新部件显示
if (current != 0)   current->SetVisible(false);   // 旧部件隐藏
folder->SetSelection(this->GetTab(toSelect));      // 同步 tab bar 高亮

4. Qt 实现 NativeTabFolder

构造函数搭出真实控件树(berryNativeTabFolder.cpp:55):

cpp 复制代码
viewForm = new QtControlWidget(parent, nullptr);   // tab folder 根 = GetControl()
QVBoxLayout(边距0,间距0):
  ├─ tabControl  = new QCTabBar;  setMinimumSize(0,27);   // 标签条
  └─ contentFrame = new QFrame(viewForm);                 // 内容区
connect(tabControl, currentChanged(int), this, TabSelectionChanged(int));
connect(tabControl, dragStarted(QPoint), this, DragStarted(QPoint));

一个 tab 页的物理结构 = QVBoxLayout 竖排 QCTabBar + QFrame 激活/非激活用不同 QSS(IQtStyleManager)。

5. 标签条 QCTabBar + NativeTabItem

QCTabBarQTabBar 子类:

  • 维护平行的 AbstractTabItem* 列表 (insertTab 同时插 tabItemListQTabBar::insertTab),与 Qt 标签下标一一对应,每个对应一个 NativeTabItem(标题/图标/关闭按钮/脏标记)。
  • 自实现拖拽起手 :左键移动超 startDragDistanceemit dragStarted(),交上层做拖出分屏/浮动。

6. 点击标签 → 内容切换事件链

复制代码
用户点标签
 → QTabBar currentChanged(int)
   → NativeTabFolder::TabSelectionChanged(index)
     → FireEvent(EVENT_TAB_SELECTED, ...)
       → TabbedStackPresentation
         → PresentablePartFolder::Select(newPart)
           → newPart->SetVisible(true) / oldPart->SetVisible(false)
             → PresentablePart::SetVisible → PartPane::SetVisible
               → Tweaklet 显示/隐藏 PartPane::control (= CreateQtPartControl 的 parent)

编辑器本体从不 reparent,只 show/hide + 对齐几何。 多个编辑器控件都活着叠在同一内容区,切换很快、不重建。

7. Tweaklet:SWT→Qt 适配缝

PartPane::CreateControl(berryPartPane.cpp:53)不直接 new QWidget:

cpp 复制代码
control = Tweaklets::Get(WorkbenchPageTweaklet::KEY)->CreatePaneControl(parent);
Tweaklets::Get(GuiWidgetsTweaklet::KEY)->SetVisible(control, false);   // 默认不可见
Tweaklets::Get(GuiWidgetsTweaklet::KEY)->MoveAbove(control, nullptr);

Tweaklet 是可插拔适配层:把 SWT 的 Control 操作(创建/显隐/置顶/焦点监听)抽象成接口,Qt 版提供实现,让 Java 直译的上层逻辑几乎不改就能跑在 Qt 上。


四、核心要点总结

编辑器机制:

  1. 声明式注册(plugin.xml 扩展点 + EditorRegistry 描述符)。
  2. 引用/部件分离 + 延迟实例化(EditorReference 先行,IEditorPart 后建)。
  3. 呈现层可组合(EditorSashContainer 分屏 → PartStack 标签堆 → PartPane 单标签)。
  4. Site 中介 + IPartListener 感知可见性,部件不硬依赖工作台。

呈现层:

  1. 模型/呈现分离(PartStack 只管逻辑,画图委托 StackPresentation)。
  2. 三层抽象接口(IPresentablePart / AbstractTabFolder / StackPresentation),换实现即换皮肤。
  3. 真实控件极简(QtControlWidget 内竖排 QCTabBar + QFrame)。
  4. overlay 而非 reparent(contentProxy 占位,本体 SetVisible + 几何对齐,切 tab 不重建)。
  5. QCTabBar 双列表 + Tweaklet 适配,是 SWT→Qt 移植的关键。

std 版编辑器(QmitkStdMultiWidgetEditor)走完全相同机制,仅 plugin.xml 的 id/class 不同。