java:实现纵向菜单栏(附带源码)

一、项目背景详细介绍

在许多桌面和 Web 应用中,水平菜单栏(如 Swing 的 JMenuBar)是最常见的导航方式,但在一些管理后台、IDE 插件、辅助工具或侧边导航场景下,纵向菜单栏(Vertical Menu Bar)更符合用户习惯,可充分利用侧边栏空间,并且便于层级式导航。纵向菜单栏通常包含图标与文字、支持折叠/展开子菜单、可高亮当前激活项,并在窗口调整时保持定位稳定。常见于 Eclipse、VSCode、各类管理后台及移动端适配界面。

本项目旨在使用 Java(Swing 与 JavaFX 两种 UI 框架)实现一个功能完备的纵向菜单栏组件,具备以下特色:

  • 垂直排列主菜单项,支持可折叠的子菜单

  • 图标与文字混排,高亮选中状态

  • 可设置固定宽度,内容自适应高度滚动

  • 支持主题色与样式定制

  • 可绑定菜单项点击事件


二、项目需求详细介绍

  1. 垂直排列

    • 主菜单项纵向排列,需支持固定宽度和灵活高度;

    • 可自定义图标和文字位置(图标左/右、文字居中等)。

  2. 子菜单折叠/展开

    • 支持多级子菜单,点击父项可展开或折叠其子项;

    • 展开时子项缩进显示,并支持其自身图标与点击事件;

  3. 选中高亮

    • 当前选中项背景和文字需使用主题色高亮;

    • 可配置高亮颜色及字体样式;

  4. 滚动支持

    • 当菜单项总高度超过容器高度时,需自动出现滚动条;

  5. 事件绑定

    • 为每个菜单项绑定 ActionListener(Swing)或回调(JavaFX),触发业务逻辑;

  6. 主题与样式

    • 支持通过外部属性文件或 CSS 样式定义背景色、鼠标悬停色、选中色、图标大小;

    • 可在运行时切换主题;

  7. 集成简便

    • 提供 VerticalMenuBar 类,外部只需传入配置或调用添加菜单项方法即可;

    • 支持动态增删菜单项;

  8. 示例与测试

    • 提供 Swing 与 JavaFX 两个 Demo,演示菜单栏在主窗体侧边应用;

    • 单元测试或人工测试验证折叠、展开、点击事件;


三、相关技术详细介绍

  1. Swing 实现

    • 使用 JPanel 垂直布局(BoxLayout.Y_AXIS)或第三方布局管理器;

    • 每个菜单项可由 JButton 或自定义 JPanel 组合图标与文本;

    • 折叠逻辑通过控制子菜单容器的 setVisible(true/false) 并重绘;

    • 滚动支持则将主容器包装在 JScrollPane 中;

    • 主题样式可通过 UIManager.put 或组件 setBackgroundsetForeground 动态更新。

  2. JavaFX 实现

    • 使用 VBox 容器垂直布局;

    • 菜单项由 HBox 包含 ImageViewLabel

    • 折叠子菜单通过修改子节点的 managedvisible 属性;

    • 滚动支持使用 ScrollPane 包裹 VBox

    • 样式可通过外部 .css 文件或 setStyle 绑定样式类。

  3. 配置管理

    • 使用 Properties 或 JSON/YAML 文件定义菜单结构及样式;

    • 运行时加载配置并构建菜单;

  4. 事件处理

    • Swing:为每个按钮添加 ActionListener

    • JavaFX:为 Node 设置 setOnMouseClicked 回调;

    • 统一在管理类中维护菜单项与业务回调的映射关系。

  5. 可扩展性

    • 动态增删改:在菜单管理类中提供 addItem/removeItem/updateItem 方法;

    • 主题切换:监听配置变更,动态重新应用样式。


四、实现思路详细介绍

  1. 组件封装

    • 定义 VerticalMenuBar 类(针对 Swing)及 VerticalMenuBarFX(针对 JavaFX),内部持有根容器(JScrollPane/ScrollPane 包裹 JPanel/VBox);

  2. 菜单模型

    • 定义 MenuItemData 对象,包含 idtexticonPathList<MenuItemData> children

    • 可从配置文件加载出一棵树形结构;

  3. 构建渲染

    • 递归遍历 MenuItemData

      • 对于每个数据节点,创建对应的 UI 组件,设置图标、文字与事件;

      • 如果有子节点,则在父下方创建一个可折叠的子容器,并预先隐藏;

  4. 折叠/展开逻辑

    • 菜单项点击时,判断是否有子菜单:

      • 若有,切换子容器的可见性,并执行容器 revalidate()/layout()layout()

      • 若无,触发菜单点击的业务回调;

  5. 高亮选中

    • 在点击时清除之前选中项的高亮样式,为当前项添加选中样式;

  6. 滚动支持

    • 菜单整体高于可视范围时,JScrollPaneScrollPane 自动出现滚动条;

  7. 样式与主题

    • 从配置加载背景、悬停、选中等颜色值,并在构建或鼠标悬停/退出事件中 setBackground 或添加/移除样式类;

  8. 示例集成

    • MainSwingVerticalMenuMainJavaFXVerticalMenu 中,将菜单栏添加到主窗体的左侧(BorderLayout.WESTBorderPane.left),并演示点击与折叠效果。

五、完整实现代码

// =============================================
// File: MenuItemData.java
// 菜单数据模型
// =============================================
package com.example.verticalmenu;

import java.util.List;

/**
 * 菜单项数据模型,用于描述文本、图标及子菜单
 */
public class MenuItemData {
    private String id;                        // 唯一标识
    private String text;                      // 菜单显示文本
    private String iconPath;                  // 图标资源路径
    private List<MenuItemData> children;      // 子菜单项

    // 构造、getter、setter
    public MenuItemData(String id, String text, String iconPath, List<MenuItemData> children) {
        this.id = id; this.text = text; this.iconPath = iconPath; this.children = children;
    }
    public String getId() { return id; }
    public String getText() { return text; }
    public String getIconPath() { return iconPath; }
    public List<MenuItemData> getChildren() { return children; }
}

// =============================================
// File: VerticalMenuBar.java
// Swing 纵向菜单栏组件
// =============================================
package com.example.verticalmenu;

import javax.swing.*;
import javax.swing.border.EmptyBorder;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.*;
import java.util.List;
import java.util.function.Consumer;

/**
 * 纵向菜单栏 Swing 实现
 */
public class VerticalMenuBar extends JScrollPane {
    private JPanel container;                                         // 承载所有菜单项的容器
    private Map<String, JMenuItemPanel> itemPanelMap = new HashMap<>();// id->面板映射
    private Consumer<String> onItemClick;                             // 菜单点击回调

    public VerticalMenuBar(List<MenuItemData> menuData, Consumer<String> callback) {
        this.onItemClick = callback;
        container = new JPanel();
        container.setLayout(new BoxLayout(container, BoxLayout.Y_AXIS));
        container.setBackground(Color.WHITE);
        container.setBorder(null);
        this.setViewportView(container);
        this.setBorder(null);
        buildMenu(menuData, container, 0);
    }

    /** 递归构建菜单:level 表示缩进层级 */
    private void buildMenu(List<MenuItemData> list, JPanel parent, int level) {
        for (MenuItemData data : list) {
            JMenuItemPanel panel = new JMenuItemPanel(data, level);
            itemPanelMap.put(data.getId(), panel);
            parent.add(panel);
            if (!data.getChildren().isEmpty()) {
                // 子菜单容器,初始隐藏
                JPanel childContainer = new JPanel();
                childContainer.setLayout(new BoxLayout(childContainer, BoxLayout.Y_AXIS));
                childContainer.setBackground(Color.WHITE);
                childContainer.setVisible(false);
                parent.add(childContainer);
                panel.setChildContainer(childContainer);
                // 递归
                buildMenu(data.getChildren(), childContainer, level + 1);
            }
        }
    }

    /** Programmatic select by id */
    public void selectItem(String id) {
        itemPanelMap.values().forEach(p -> p.setSelected(false));
        JMenuItemPanel panel = itemPanelMap.get(id);
        if (panel != null) {
            panel.setSelected(true);
        }
    }

    // 内部类:每个菜单项面板
    private class JMenuItemPanel extends JPanel {
        private JLabel iconLabel, textLabel;
        private JPanel childContainer;
        private boolean expanded = false;
        private Color normalBg = Color.WHITE;
        private Color hoverBg = new Color(230, 230, 230);
        private Color selectedBg = new Color(200, 220, 240);

        public JMenuItemPanel(MenuItemData data, int level) {
            this.setLayout(new BorderLayout());
            this.setMaximumSize(new Dimension(Integer.MAX_VALUE, 40));
            this.setBackground(normalBg);
            this.setBorder(new EmptyBorder(5, 10 + level * 20, 5, 5));

            // 图标
            iconLabel = new JLabel();
            if (data.getIconPath() != null) {
                iconLabel.setIcon(new ImageIcon(getClass().getResource(data.getIconPath())));
            }
            this.add(iconLabel, BorderLayout.WEST);

            // 文本
            textLabel = new JLabel(data.getText());
            this.add(textLabel, BorderLayout.CENTER);

            // 鼠标交互
            this.addMouseListener(new MouseAdapter() {
                @Override
                public void mouseEntered(MouseEvent e) {
                    if (!expanded) setBackground(hoverBg);
                }
                @Override
                public void mouseExited(MouseEvent e) {
                    if (!expanded) setBackground(normalBg);
                }
                @Override
                public void mouseClicked(MouseEvent e) {
                    // 有子菜单则折叠/展开
                    if (childContainer != null) {
                        expanded = !expanded;
                        childContainer.setVisible(expanded);
                        revalidate();
                    }
                    // 高亮选中
                    itemPanelMap.values().forEach(p -> p.setSelected(false));
                    setSelected(true);
                    onItemClick.accept(data.getId());
                }
            });
        }

        public void setChildContainer(JPanel child) {
            this.childContainer = child;
        }

        public void setSelected(boolean sel) {
            expanded = sel;
            setBackground(sel ? selectedBg : normalBg);
        }
    }
}

// =============================================
// File: MainSwingVerticalMenu.java
// Swing 示例:主窗体中使用纵向菜单栏
// =============================================
package com.example.verticalmenu;

import javax.swing.*;
import java.awt.*;
import java.util.Arrays;

/**
 * 演示 VerticalMenuBar 的 Swing 应用
 */
public class MainSwingVerticalMenu {
    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> {
            JFrame frame = new JFrame("Swing 纵向菜单栏示例");
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.setSize(800, 600);
            frame.setLayout(new BorderLayout());

            // 构造菜单数据
            MenuItemData sub1 = new MenuItemData("item1-1", "子项 1-1", null, Arrays.asList());
            MenuItemData sub2 = new MenuItemData("item1-2", "子项 1-2", null, Arrays.asList());
            MenuItemData m1 = new MenuItemData("item1", "菜单项 1", "/icons/icon1.png", Arrays.asList(sub1, sub2));
            MenuItemData m2 = new MenuItemData("item2", "菜单项 2", "/icons/icon2.png", Arrays.asList());
            MenuItemData m3 = new MenuItemData("item3", "菜单项 3", "/icons/icon3.png", Arrays.asList());
            java.util.List<MenuItemData> data = Arrays.asList(m1, m2, m3);

            // 创建纵向菜单栏
            VerticalMenuBar menuBar = new VerticalMenuBar(data, id -> {
                // 点击回调
                System.out.println("点击菜单: " + id);
            });
            frame.getContentPane().add(menuBar, BorderLayout.WEST);

            // 主内容区
            JTextArea text = new JTextArea("主内容区");
            frame.getContentPane().add(text, BorderLayout.CENTER);

            frame.setVisible(true);
        });
    }
}

// =============================================
// File: VerticalMenuBarFX.java
// JavaFX 纵向菜单栏组件
// =============================================
package com.example.verticalmenufx;

import javafx.geometry.Insets;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.VBox;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.Node;
import javafx.scene.control.ScrollPane;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;

/**
 * JavaFX 纵向菜单栏实现
 */
public class VerticalMenuBarFX extends ScrollPane {
    private VBox container = new VBox();
    private Map<String, HBox> itemMap = new HashMap<>();
    private Consumer<String> onItemClick;

    public VerticalMenuBarFX(List<MenuItemDataFX> data, Consumer<String> callback) {
        this.onItemClick = callback;
        container.setSpacing(2);
        container.setPadding(new Insets(5));
        this.setContent(container);
        buildMenu(data, 0);
    }

    private void buildMenu(List<MenuItemDataFX> list, int level) {
        for (MenuItemDataFX d : list) {
            HBox hbox = new HBox(5);
            hbox.setPadding(new Insets(5,5,5,5 + level * 20));
            hbox.setStyle("-fx-background-color: white;");
            if (d.getIconPath()!=null) {
                ImageView iv = new ImageView(new Image(getClass().getResourceAsStream(d.getIconPath())));
                iv.setFitWidth(16); iv.setFitHeight(16);
                hbox.getChildren().add(iv);
            }
            Label lbl = new Label(d.getText());
            hbox.getChildren().add(lbl);
            container.getChildren().add(hbox);
            itemMap.put(d.getId(), hbox);

            // 点击处理
            hbox.addEventHandler(MouseEvent.MOUSE_CLICKED, e -> {
                // 清空选中样式
                itemMap.values().forEach(node -> node.setStyle("-fx-background-color: white;"));
                hbox.setStyle("-fx-background-color: lightblue;");
                onItemClick.accept(d.getId());
                // 折叠/展开
                if (!d.getChildren().isEmpty()) {
                    if (d.isExpanded()) {
                        d.setExpanded(false);
                        collapseChildren(hbox, d);
                    } else {
                        d.setExpanded(true);
                        expandChildren(hbox, d, level+1);
                    }
                }
            });
        }
    }

    private void expandChildren(Node after, MenuItemDataFX parent, int level) {
        int idx = container.getChildren().indexOf(after);
        for (MenuItemDataFX c : parent.getChildren()) {
            HBox hbox = new HBox(5);
            hbox.setPadding(new Insets(5,5,5,5 + level * 20));
            hbox.setStyle("-fx-background-color: white;");
            if (c.getIconPath()!=null) {
                ImageView iv = new ImageView(new Image(getClass().getResourceAsStream(c.getIconPath())));
                iv.setFitWidth(16); iv.setFitHeight(16);
                hbox.getChildren().add(iv);
            }
            hbox.getChildren().add(new Label(c.getText()));
            container.getChildren().add(++idx, hbox);
            itemMap.put(c.getId(), hbox);
        }
    }

    private void collapseChildren(Node after, MenuItemDataFX parent) {
        for (MenuItemDataFX c : parent.getChildren()) {
            Node node = itemMap.get(c.getId());
            container.getChildren().remove(node);
            itemMap.remove(c.getId());
        }
    }
}

// =============================================
// File: MenuItemDataFX.java
// JavaFX 菜单数据模型,带折叠状态
// =============================================
package com.example.verticalmenufx;

import java.util.List;

/**
 * JavaFX 版菜单数据模型
 */
public class MenuItemDataFX {
    private String id, text, iconPath;
    private List<MenuItemDataFX> children;
    private boolean expanded = false;
    // 构造、getter、setter
}

// =============================================
// File: MainJavaFXVerticalMenu.java
// JavaFX 示例:主窗体中使用纵向菜单栏
// =============================================
package com.example.verticalmenufx;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import java.util.Arrays;

/**
 * 演示 VerticalMenuBarFX 的 JavaFX 应用
 */
public class MainJavaFXVerticalMenu extends Application {
    @Override
    public void start(Stage primaryStage) {
        primaryStage.setTitle("JavaFX 纵向菜单栏示例");
        BorderPane root = new BorderPane();
        Scene scene = new Scene(root, 800, 600);

        // 构造数据
        MenuItemDataFX sub1 = new MenuItemDataFX("fx1-1","子项1-1",null,Arrays.asList());
        MenuItemDataFX sub2 = new MenuItemDataFX("fx1-2","子项1-2",null,Arrays.asList());
        MenuItemDataFX m1 = new MenuItemDataFX("fx1","菜单1","/icons/icon1.png",Arrays.asList(sub1, sub2));
        MenuItemDataFX m2 = new MenuItemDataFX("fx2","菜单2","/icons/icon2.png",Arrays.asList());
        java.util.List<MenuItemDataFX> data = Arrays.asList(m1, m2);

        // 创建并放置左侧菜单栏
        VerticalMenuBarFX menu = new VerticalMenuBarFX(data, id -> {
            System.out.println("点击FX菜单: " + id);
        });
        root.setLeft(menu);

        // 中心显示
        root.setCenter(new Label("主内容区"));
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    public static void main(String[] args) { launch(args); }
}

六、代码详细解读

  • MenuItemData / MenuItemDataFX

    • 数据模型类,存放 idtexticonPathchildren 列表(JavaFX 版本还带 expanded 状态)。

  • VerticalMenuBar(Swing)

    • 继承 JScrollPane,内部用 JPanel 垂直布局;

    • buildMenu(...):递归遍历数据模型,创建 JMenuItemPanel,并在有子菜单时插入一个隐藏的容器;

    • JMenuItemPanel:自定义 JPanel,显示图标与文字,处理鼠标悬停、点击事件,切换子容器可见性,触发高亮与回调;

    • selectItem(id):外部可编程选中、清除高亮。

  • MainSwingVerticalMenu

    • 构造示例数据(含子项),将 VerticalMenuBar 放置于 BorderLayout.WEST,并绑定点击回调;

  • VerticalMenuBarFX(JavaFX)

    • 继承 ScrollPane,内部用 VBox

    • buildMenu(...):遍历 MenuItemDataFX 列表,为每项创建 HBox,设置图标、标签、点击事件;

    • 点击后清除所有高亮,设置当前高亮,并调用 expandChildren / collapseChildren 管理子节点显隐;

  • MainJavaFXVerticalMenu

    • BorderPane.left 放置 VerticalMenuBarFX,展示效果并打印点击事件。


七、项目详细总结

  1. 通用数据模型:使用 MenuItemData/MenuItemDataFX 定义树形结构,UI 与数据分离,支持动态更新。

  2. 递归构建:二者均采用递归构建菜单项,支持任意层级折叠与展开。

  3. 高亮与交互:鼠标悬停与点击状态均给予视觉反馈,高亮与折叠逻辑清晰。

  4. 滚动支持:容器超高时自动出现滚动条,保证可访问全部菜单项。

  5. 跨框架对比:Swing 基于 JPanel/JScrollPane 与自定义面板,JavaFX 基于 VBox/ScrollPane 与 CSS 样式,易于对比学习。


八、项目常见问题及解答

  1. Q:子菜单展开后如何保证再次点击折叠?

    • A:Swing 版通过 expanded 标志与 childContainer.setVisible(...);JavaFX 版通过 MenuItemDataFX.expanded 与动态插入/移除子节点实现。

  2. Q:如何动态增删菜单?

    • A:可向数据模型中添加新 MenuItemData,并调用 buildMenu 或专门的 addItem 方法将其渲染到容器中。

  3. Q:如何在选中后同步更新中心内容?

    • A:在回调 onItemClick 中接收 id,再根据 id 切换主内容面板即可。

  4. Q:如何自定义主题色?

    • A:在创建面板或 HBox 时调用 setBackground/setStyle,或提供外部配置加载颜色值并应用。

  5. Q:如何支持键盘导航?

    • A:为面板或节点设置 Focusable,使用 KeyListener(Swing)或 setOnKeyPressed(JavaFX)监听上下键并移动焦点。


九、扩展方向与性能优化

  1. 性能:当菜单项众多时,可采用延迟加载:仅在点击展开时构建子菜单面板。

  2. 动画效果:在展开/折叠时添加过渡动画(JavaFX 可用 Timeline,Swing 可用 Timer 逐帧调整高度)。

  3. 多主题:支持深色/浅色模式切换,统一加载 CSS 或 UIManager 配置。

  4. 拖拽排序:可让用户通过拖拽重新排序菜单项,需监听拖放事件并更新数据模型。

  5. 响应式布局:在窗口宽度变化时自动折叠图标或文本,仅显示图标;

  6. 可访问性:支持屏幕阅读器提示、焦点可见性、增强键盘操作。

  7. Web 版本:将同一模型与逻辑移植到 Java 后端生成 HTML/CSS 或 JavaScript 前端组件。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值