深入解析 MITK/BlueBerry 编辑器:从菜单点击到 QTabBar 的完整架构与呈现链路
分析对象:
stdmultiwidget/mxnmultiwidget编辑器打开新标签页的完整链路,以及标签页底层的呈现实现。涉及模块:
org.mitk.gui.qt.ext、org.mitk.gui.qt.mxnmultiwidgeteditor、org.blueberry.ui.qt、Modules/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/LoadLayout 用 Qt::DirectConnection 传 std::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::OpenEditor → BusyOpenEditor → BusyOpenEditorBatched(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
CreateEditorTab → EditorAreaHelper::AddEditor → AddToLayout:
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. 呈现抽象
PartStack 经 QtWorkbenchPresentationFactory 创建 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
QCTabBar 是 QTabBar 子类:
- 维护平行的
AbstractTabItem*列表 (insertTab同时插tabItemList与QTabBar::insertTab),与 Qt 标签下标一一对应,每个对应一个NativeTabItem(标题/图标/关闭按钮/脏标记)。 - 自实现拖拽起手 :左键移动超
startDragDistance→emit 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 上。
四、核心要点总结
编辑器机制:
- 声明式注册(plugin.xml 扩展点 + EditorRegistry 描述符)。
- 引用/部件分离 + 延迟实例化(EditorReference 先行,IEditorPart 后建)。
- 呈现层可组合(EditorSashContainer 分屏 → PartStack 标签堆 → PartPane 单标签)。
- Site 中介 + IPartListener 感知可见性,部件不硬依赖工作台。
呈现层:
- 模型/呈现分离(PartStack 只管逻辑,画图委托 StackPresentation)。
- 三层抽象接口(IPresentablePart / AbstractTabFolder / StackPresentation),换实现即换皮肤。
- 真实控件极简(QtControlWidget 内竖排 QCTabBar + QFrame)。
- overlay 而非 reparent(contentProxy 占位,本体 SetVisible + 几何对齐,切 tab 不重建)。
- QCTabBar 双列表 + Tweaklet 适配,是 SWT→Qt 移植的关键。
std 版编辑器(QmitkStdMultiWidgetEditor)走完全相同机制,仅 plugin.xml 的 id/class 不同。