Skip to content

🧩 面板布局详解 ​

ClassySharkPanelMVM 中介者

ClassySharkPanel 是 GUI 的中枢,采用 MVM(Model-View-Mediator) 模式 —— Model 是 SilverGhost Shark API,View 是各 Swing 面板,本类同时实现 ToolbarController + ViewerController + KeyListener 三接口充当唯一中介者,所有事件都汇聚到这里再分发。

中介者三重身份 ​

mermaid
flowchart TB
    subgraph Mediator["ClassySharkPanel(中介者)"]
        TC[ToolbarController<br/>工具栏回调]
        VC[ViewerController<br/>查看区回调]
        KL[KeyListener<br/>键盘事件]
    end

    Toolbar -.回调.-> TC
    FilesTree -.选择.-> VC
    MethodsCountPanel -.选节点.-> VC
    RingChartPanel -.点击扇区.-> VC
    DisplayArea -.链接点击.-> VC
    TC & VC & KL --> SG[SilverGhost<br/>Shark API 后台]
    SG -->|SwingWorker done| Views[刷新各面板]

ClassySharkPanel 把三个接口都实现到自己身上,意味着 所有子组件只需持有这一个回调对象,互相之间不直接通信,彻底解耦。

接口来源谁会回调它典型方法
ToolbarControllerextends ArchiveDisplayerToolbaronGoBackPressed / onViewTopClassPressed / onExportButtonPressed
ViewerControllerextends ArchiveDisplayerFilesTree / RingChartPanel / DisplayAreaonSelectedClassName / onSelectedMethodCount
KeyListenerjava.awt.eventToolbar 输入框keyPressed → 左/右箭头、Delete、字母数字

buildUI 组装过程 ​

buildUI() 是布局核心,用 BorderLayout + JSplitPane + JTabbedPane 拼装:

mermaid
flowchart TD
    Root["ClassySharkPanel<br/>BorderLayout"] --> N["NORTH: Toolbar"]
    Root --> C["CENTER: JSplitPane<br/>HORIZONTAL_SPLIT"]
    C --> L["LEFT: JTabbedPane"]
    L --> T1["Tab0 'Classes'<br/>JScrollPane + FilesTree.getJTree()"]
    L --> T2["Tab1 'Methods count'<br/>MethodsCountPanel"]
    C --> R["RIGHT: 动态切换"]
    R -->|默认/Tab0| RS["JScrollPane<br/>DisplayArea.onAddComponentToPane()"]
    R -->|Tab1| RC["RingChartPanel"]

关键代码骨架(简化自源码):

java
private void buildUI() {
    setLayout(new BorderLayout());

    ringChartPanel = new RingChartPanel(this);   // 注入 ViewerController
    toolbar        = new Toolbar(this);          // 注入 ToolbarController
    add(toolbar, BorderLayout.NORTH);
    toolbar.addKeyListenerToTypingArea(this);    // 注入 KeyListener

    displayArea = new DisplayArea(this);
    final JScrollPane rightScrollPane =
            new JScrollPane(displayArea.onAddComponentToPane());

    filesTree = new FilesTree(this);
    final JTabbedPane jTabbedPane = new JTabbedPane();
    jTabbedPane.addTab("Classes", new JScrollPane(filesTree.getJTree()));
    methodsCountPanel = new MethodsCountPanel(this);
    jTabbedPane.addTab("Methods count", methodsCountPanel);

    // 切 Tab 切右侧视图
    jTabbedPane.addChangeListener(e -> {
        int loc = jSplitPane.getDividerLocation();
        if (((JTabbedPane) e.getSource()).getSelectedIndex() == 0) {
            jSplitPane.setRightComponent(rightScrollPane);  // 类树 → 显示区
        } else {
            jSplitPane.setRightComponent(ringChartPanel);   // 方法计数 → 环形图
        }
        jSplitPane.setDividerLocation(loc);  // 保持分隔条位置
    });

    jSplitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT);
    jSplitPane.setDividerSize(3);
    jSplitPane.setDividerLocation(300);
    jSplitPane.add(jTabbedPane, JSplitPane.LEFT);
    jSplitPane.add(rightScrollPane, JSplitPane.RIGHT);
    add(jSplitPane, BorderLayout.CENTER);
}

布局要素一览 ​

区域组件角色文档
🧭 顶部Toolbar打开/后退/查看/映射/导出/最近/设置 + 输入框工具栏
🌳 左侧 Tab0FilesTree + JTree按 dex/包分组导航类名类树导航
📈 左侧 Tab1MethodsCountPanel按包树展示方法数方法计数
📝 右侧(Tab0)DisplayArea渲染源码存根/搜索/涂鸦显示区
📊 右侧(Tab1)RingChartPanel方法数旭日图扇区环形图

分隔条与左窗格显隐 ​

JSplitPane 分隔条宽度仅 3px,初始位置 300px。onChangeLeftPaneVisibility(boolean) 实现左窗格折叠/展开:

java
public void onChangeLeftPaneVisibility(boolean visible) {
    if (visible) {
        jSplitPane.setDividerLocation(dividerLocation);  // 恢复记忆位置
    } else {
        dividerLocation = jSplitPane.getDividerLocation();  // 记忆当前位置
    }
    jSplitPane.getLeftComponent().setVisible(visible);
    jSplitPane.updateUI();
}

工具栏按钮触发此回调,让 Classes 树可临时隐藏以扩大显示区。

Tab 切换驱动右侧视图 ​

JTabbedPane 的 ChangeListener 是布局的"活点":选 Classes 标签时右侧显示 DisplayArea,选 Methods count 标签时右侧换为 RingChartPanel。这样左侧树与右侧可视化始终语义对齐,且分隔条位置在切换前后保持不变。

mermaid
sequenceDiagram
    participant U as 用户
    participant TP as JTabbedPane
    participant SP as JSplitPane
    participant DA as DisplayArea
    participant RC as RingChartPanel

    U->>TP: 切到 "Methods count"
    TP->>SP: setRightComponent(RingChartPanel)
    SP->>RC: 显示环形图
    U->>TP: 切回 "Classes"
    TP->>SP: setRightComponent(rightScrollPane)
    SP->>DA: 显示源码存根
    Note over SP: 分隔条位置始终保留

重活全交 SwingWorker ​

EDT(事件分发线程)绝不做解析。ClassySharkPanel 内部所有耗时操作都包进 SwingWorker,doInBackground 跑解析、done() 回 EDT 刷新 UI:

方法后台任务done() 回调
readArchiveAndFillDisplayAreasilverGhost.readContents() 读档填类树 / 显示 Sharkey 或指定类 / 置 isDataLoaded
fillDisplayAreatranslateArchiveElement / filter / getManifestMatches显示单类 / 搜索结果 / 错误
onExportButtonPressedExporter.writeCurrentClass + writeArchive空 done
readMappingFilesilverGhost.readMappingFilesilverGhost.addMappings
MethodsCountPanel.NodeWorkerRootBuilder.fillClassesWithMethods建树 + onSelectedMethodCount 触发环形图

⚠️ fillDisplayArea 是最复杂的 SwingWorker:它根据 viewTopClass / viewMouseClickedClass 两个布尔分支决定是"自动补全首选类"、"点击某类"还是"边输入边过滤",并在 done() 里再判断零结果/单结果/多结果分别渲染。

事件流:从点击到渲染 ​

以"在类树点击一个类"为例,完整链路:

mermaid
sequenceDiagram
    participant FT as FilesTree
    participant CSP as ClassySharkPanel<br/>(ViewerController)
    participant W as SwingWorker
    participant SG as SilverGhost
    participant DA as DisplayArea

    FT->>CSP: onSelectedClassName("com.foo.Bar")
    CSP->>W: fillDisplayArea(name, true, true)
    W->>SG: translateArchiveElement(className)
    SG-->>W: getArchiveElementTokens()
    W->>DA: done() → displayClass(tokens)
    DA->>DA: 渲染源码存根 + 高亮

环形图点击扇区走 onSelectedMethodCount(ClassNode),直接 ringChartPanel.setRootNode(node) 重新 paint(),无需 Worker(已是内存对象)。

相关节点 ​

最后更新:

基于 Apache 2.0 协议发布