
简介面向需要实现Office/WPS风格界面的Qt开发者这套基于Qt 5.13.2与VS2017的Ribbon封装库将RibbonBar、RibbonCategory等控件抽象为可调用的API接口上层应用只需专注业务逻辑即可构建功能分区式界面。压缩包共186个文件约213KB76个svg图标与11个png图片覆盖功能区视觉素材43个h与42个cpp构成核心控件及演示工程辅以qss样式表、ui布局文件与SARibbon.sln解决方案。已有590人学习适合具备C基础、想掌握Qt控件封装与Ribbon交互设计的开发者。通过RibbonDemo示例可理解信号槽机制、布局原则与API抽象思路为仿WPS类办公界面提供可直接落地的参考。1. 用 Qt 做一套 WPS 式 Ribbon 界面从模仿到可改代码的落地拆解很多人以为 Ribbon 只是把按钮排一排比菜单栏“好看”而已。真做过办公软件就知道菜单栏的层级深用户找功能要点两三次Ribbon 把功能摊平成标签页面板虽然垂直方向占掉 100 多像素但查找效率提升明显。这套 Qt 实现的 Ribbon 风格界面就是冲着这个目标来的仿 WPS 的布局和视觉把快速访问栏、标签页、面板、按钮组以及 QSS 主题包在一个可编译的 Qt 工程里。用 Qt 5.15.2 打开 .pro直接跑出主窗口。适合做办公套件、进销存、数据管理等复杂功能桌面应用的开发者尤其适合不想引入第三方 Ribbon 库、想完全控制界面代码的人。新手可以拿它当 Ribbon 结构的入门教材熟手可以直接挖样式表和事件映射看实现细节。2. Ribbon 界面拆解不是花架子而是三层布局结构标题栏、标签栏、面板Ribbon 看起来复杂拆开就是三层最上面的快速访问工具栏和标题栏中间的标签栏文件、开始、插入等以及下方每个标签对应的面板区。WPS 的面板区里每个标签对应一个页面页面内再按功能分成若干个面板面板里放按钮、下拉框和分隔符。这个层级关系是 Ribbon 的灵魂。如果只用一个 QTabWidget 塞一堆 QToolButton界面就会变成“带标签的工具栏”功能一多就乱。本资源里所有代码都围绕这个三层模型组织理解了它你就能自由增删标签页和面板。2.1 为什么自绘而不是直接用 QTabWidget 或第三方库有人问Qt 里不是有 QTabWidget 吗把每个标签页放一个 QToolBar 不就行了实际做过就知道QTabWidget 的标签样式和内容区是一体的你想要“标签下面是分组面板面板之间还有分割竖线”的效果QTabWidget 很难做到不 hack。第三方库里也有现成的 Ribbon 控件但版本陈旧与高版本 Qt 搭配经常编译不过。所以我一般建议在 QMainWindow 上用 QTabBar 自己管理标签页内容区用 QWidget 做容器面板用 QFrameQToolButton 组合。这样你拥有完全控制权QSS 也好写。这个资源也是这么做的它没有引入任何额外依赖只用 QtWidgets 模块。对于想嵌入现有项目的团队来说少一个第三方依赖就少一个兼容性风险。2.2 数据模型先行RibbonPage、RibbonPanel、RibbonButton 三个类怎么设计直接看资源里的核心数据结构。我把它简化了一下方便理解// ribbon_def.h #include QWidget #include QToolButton #include QTabBar #include QHBoxLayout struct RibbonButtonInfo { QString text; QString iconPath; QString tooltip; int index; }; // 一个面板比如“剪贴板”或“字体” class RibbonPanel : public QFrame { Q_OBJECT public: explicit RibbonPanel(const QString title, QWidget *parent nullptr); void addButton(const RibbonButtonInfo info); signals: void buttonClicked(const QString name); private: QVBoxLayout *m_mainLayout; QHBoxLayout *m_buttonLayout; QLabel *m_titleLabel; }; // 一个标签页比如“开始” class RibbonPage : public QWidget { Q_OBJECT public: explicit RibbonPage(const QString name, QWidget *parent nullptr); void addPanel(RibbonPanel *panel); private: QHBoxLayout *m_layout; }; // 整个 Ribbon 栏 class RibbonBar : public QTabBar { Q_OBJECT public: explicit RibbonBar(QWidget *parent nullptr); void addPage(RibbonPage *page); };这段代码定义了三层结构。RibbonPanel 是 QFrame顶部有一个小标题下方是一排按钮RibbonPage 是横向排列的多个 panelRibbonBar 继承 QTabBar用来切换页面。注意我用 QHBoxLayout 而不是直接设置几何坐标这样窗口拉伸时面板会按比例排布。每个类的构造函数里要设置 objectName否则后续 QSS 选择器会失效。构造函数里还需要把布局的内边距设成 0让面板紧贴边缘否则看起来会像悬浮块。2.3 信号槽怎么串从按钮到 QAction 的映射光有控件还不够Ribbon 里的每个按钮最终都要触发程序里的功能。资源里的做法是给每个 RibbonButtonInfo 设置一个唯一标识 index点击后通过 panel 的 buttonClicked 信号逐个向上传递QToolButton → RibbonPanel → RibbonBar → MainWindow。这样主窗口只需要连接 RibbonBar 的一个信号就能收到所有按钮点击。我在实际项目里更喜欢用 QAction 来统一管理每个 Ribbon 按钮对应一个 QAction把图标、文本、快捷键、提示都绑在 QAction 上然后通过 buttonClicked 的信号把 QAction 发给主窗口。这样做的好处是 QAction 可以被菜单、快捷键和 Ribbon 同时引用不会出现同一种功能在三个地方定义三次。下面展示一下信号转发的最小实现void RibbonPanel::addButton(const RibbonButtonInfo info) { auto *btn new QToolButton(this); btn-setText(info.text); btn-setIcon(QIcon(info.iconPath)); btn-setToolTip(info.tooltip); btn-setToolButtonStyle(Qt::ToolButtonTextUnderIcon); btn-setAutoRaise(true); // 用 index 区分转发时带出按钮名 connect(btn, QToolButton::clicked, this, [info, this]() { emit buttonClicked(info.text); }); m_buttonLayout-addWidget(btn); }这里用了 lambda把信息存下来。之后 RibbonBar 在 addPage 时会把页面上所有 panel 的信号连接起来。需要注意lambda 里捕获 info 一定要用值拷贝不要用引用捕获否则按下按钮时局部变量已经销毁了。资源里的完整写法还会给每个按钮设置 fixedHeight比如 48px确保悬停时不会因为文字换行导致高度跳动。3. 把 WPS 的视觉细节搬过来QSS 参数表与图标多状态处理代码结构再好没有 WPS 那种观感也是白搭。WPS 的 Ribbon 第一眼吸引人的是什么是那张“扁平的、带微妙渐变、悬停时蓝色高亮”的界面。这些用 QSS 完全可以模拟。关键参数我会列成表照着调就行。资源里的 ribbon.qss 已经是调试过的成品但我在拆解时发现几个数值直接改也没问题下面给出我用得最多的基线值。3.1 从 WPS 截图里提取的关键视觉参数表我用 QSS 之前先把 WPS 的界面截图放大看像素级参数。分享我测量后常用的数值基于 Windows 10 下的 WPS 2019。这些数值是“风格基线”不必死守。比如你的软件是深色主题把 #F3F3F3 改成 #2B2B2B再调整前面的渐变即可。元素参数参考值窗口背景主色#F3F3F3标签栏高度固定高度34px标签选中下边框颜色/宽度#2B7CD3 / 2px标签选中背景渐变#FFFFFF → #F0F6FC面板标题字号尺寸12px颜色 #666按钮悬停背景渐变/圆角#EAF2FB / 3px按钮按下背景渐变#C7E2F9快速访问栏图标大小16x16面板内大图标大小24x243.2 QSS 实战让 QTabBar 和 QToolButton 变成 Ribbon 样子直接看 QSS 代码。先把 QTabBar 的标签做成“细长条下划线选中”再让 QToolButton 变成扁平可悬停的按钮QTabBar::tab { background: transparent; border: none; min-width: 80px; height: 34px; padding: 4px 12px; color: #444; font-size: 14px; } QTabBar::tab:selected { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #FFFFFF, stop:1 #DEEBF2); border-left: 1px solid #D6D6D6; border-right: 1px solid #D6D6D6; border-bottom: 2px solid #2B7CD3; } QToolButton { border: none; border-radius: 3px; padding: 6px; background: transparent; } QToolButton:hover { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #EAF2FB, stop:1 #D6E4F2); } QToolButton:pressed { background: #C7E2F9; }注意QTabBar 的默认样式在 Windows 上会带一条深色边框你需要设置QTabBar::pane { border: none; }把内容区边框去掉。另外如果标签高度设置了 34pxQTabBar 的固定高度也要保持一致否则文字会被裁掉。资源里把所有控件的字体统一设置成font-family: Microsoft YaHei如果 Linux 上没这个字体中文会回退成宋体观感差距很大建议在部署时带上字体。3.3 图标资源一套 SVG 改状态最省心Ribbon 按钮小图标多每个都要准备 normal、hover、disable 三套颜色的 PNG 会非常痛苦。我的习惯是全部用 SVG。Qt 的 QSvgRenderer 可以直接加载 SVG并且通过设置 highlighter 或者简单点直接把不同颜色的 SVG 文件放在 icons/ 目录下文件名用icon-name-normal.svg、icon-name-hover.svg、icon-name-disabled.svg。加载时根据按钮状态切换图标可以用一个事件过滤器在 Hover 事件里替换 QIcon。这里给一个简化的实现bool RibbonToolButton::eventFilter(QObject *obj, QEvent *e) { if (obj m_iconLabel) { if (e-type() QEvent::Enter) { setIcon(QIcon(m_hoverIconPath)); } else if (e-type() QEvent::Leave) { setIcon(QIcon(m_normalIconPath)); } } return QWidget::eventFilter(obj, e); }不过注意替换 QIcon 会导致按钮闪一下最好在安装 eventFilter 之前先 setIcon 初始图标。还有一种做法是让 QToolButton 只显示图标并把图标放在一个 QLabel 里然后 label-setPixmap 不同颜色事件过滤放在 label 上。资源里已经预置了常见的 16x16 和 24x24 两套 SVG 占位图标你在替换成真实业务图标时注意保持同一个网格大小否则布局会跳。4. 实战搭一个最小可跑的仿 WPS 主窗口工程配置与编译全流程前面讲的层级和样式最终要落到一个能编译运行的工程里。下面这份是我从资源里整理出来的最小骨架去掉了业务代码只保留界面。用 Qt 5.15.2 测试通过Qt 6.x 也能直接编译。如果是从 Qt Creator 打开记得选择正确的套件MinGW 和 MSVC 不要混用。4.1 工程文件 .pro模块和版本选择先看 .pro 文件用 qmake 管理的工程重点在于不要混用模块。QT core gui greaterThan(QT_MAJOR_VERSION, 4): QT widgets TARGET RibbonDemo TEMPLATE app SOURCES main.cpp mainwindow.cpp ribbon.cpp HEADERS mainwindow.h ribbon.h RESOURCES icons.qrc注意如果用到 QPainter、QImage 相关功能要加QT svg。如果是 Qt 5需要加QT svg才能用 QSvgRenderer。这里的 icons.qrc 是把 svg 图标打包进二进制避免部署时外部文件丢失。如果你的图标资源很多建议把 qrc 拆成多个文件比如 icons.qrc 和 style.qrc这样替换主题时单独重编译资源文件就行。4.2 主窗口代码组装 RibbonBar 和页面下面是最小可跑的 mainwindow.cpp。创建两个标签页开始、插入。每个标签页放一个面板面板上放两个按钮。#include mainwindow.h #include ribbon.h MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { setWindowTitle(仿 WPS Ribbon 界面 - Qt 实现); resize(1200, 700); // 创建 Ribbon 栏代替原有 menuBar RibbonBar *ribbon new RibbonBar(this); ribbon-setObjectName(RibbonBar); // 第一个标签页开始 RibbonPage *pageHome new RibbonPage(开始, this); RibbonPanel *panelClipboard new RibbonPanel(剪贴板, pageHome); panelClipboard-addButton({粘贴, :/icons/paste.svg, 粘贴内容, 0}); panelClipboard-addButton({剪切, :/icons/cut.svg, 剪切选中内容, 1}); pageHome-addPanel(panelClipboard); RibbonPanel *panelFont new RibbonPanel(字体, pageHome); panelFont-addButton({加粗, :/icons/bold.svg, 粗体, 2}); panelFont-addButton({斜体, :/icons/italic.svg, 斜体, 3}); pageHome-addPanel(panelFont); // 第二个标签页插入 RibbonPage *pageInsert new RibbonPage(插入, this); RibbonPanel *panelIllustration new RibbonPanel(插图, pageInsert); panelIllustration-addButton({图片, :/icons/image.svg, 插入图片, 4}); pageInsert-addPanel(panelIllustration); ribbon-addPage(pageHome); ribbon-addPage(pageInsert); // 把 ribbon 放到主窗口顶部 setMenuWidget(ribbon); }这里用 setMenuWidget 而不是 setMenuBar是因为 setMenuBar 会强制 QMenuBar 的样式而 setMenuWidget 可以替换成一个 QWidget。我的习惯是若不想保留系统菜单框就直接 setMenuWidget 放 RibbonBar。如果你定义了 QMenuBar可以用menuBar()-setCornerWidget但控制力度不如这个方案。注意RibbonBar 内部就需要实现 addPage 时把 RibbonPage 的内容区域和 QTabBar 的信号连接起来这部分代码在 ribbon.cpp 里资源里已经写好了。4.3 编译运行常见启动命令和验证在 Qt Creator 里打开 .pro选择 Kit 后点构建通常一步就过。命令行下的编译顺序是qmake RibbonDemo.pro make -j4 ./RibbonDemo # Windows 上是 RibbonDemo.exe如果是 Qt 5.15.2 MinGW 32 位Makefile 里默认是 gcc如果是 MSVC需要先执行 vcvarsall.bat 再运行 qmake。跑起来后用 Ctrl鼠标滚轮缩放窗口看看面板布局是否按比例伸展。验证按钮事件点击“粘贴”控制台应该输出你绑定槽的信号。要快速验证资源里的样式有没有生效可以临时在 main.cpp 里加载一个外部 QSSQFile styleFile(:/style/ribbon.qss); styleFile.open(QFile::ReadOnly); qApp-setStyleSheet(styleFile.readAll());注意路径如果是外部文件要用绝对路径如果打包进资源用冒号开头。样式加载失败时界面会回到默认灰色这种情况基本就是 qrc 前缀没写对。还有个细节qrc 里的路径区分大小写/icons/paste.svg和/Icons/Paste.svg是两回事。5. 避坑Qt Ribbon 开发中绕不开的五个编译与运行问题界面代码写完之后真正消耗时间的不是写代码是跑环境。下面这几条是我自己在这个资源上折腾过的也许会让你少走弯路。现象编译报错 fatal: cannot mix incompatible Qt library (version ex50601) with this library。原因你代码编译时用的 Qt 头文件版本和运行时链接的 DLL 版本不一致。常见于电脑上装了两个 Qt比如 5.12 和 5.15qmake 指向了 5.12 的 mkspecs但 PATH 里却是 5.15 的 bin。解决打开 Qt Creator 的构建环境把 Qt 版本对应的 bin 目录放到 PATH 最前面或者在命令行里先set PATHC:\Qt\5.15.2\mingw73_64\bin;%PATH%再重新 qmake 一次。确保只用一个编译器套件。现象Linux 下启动时报 could not find the Qt platform plugin linuxfb。原因Qt 平台插件未安装或未找到。linuxfb 是嵌入式用的插件桌面环境应该用 xcb。解决在 argv 上放-platform xcb强制指定或者安装libqt5gui5和libqt5dbus5。如果你是在树莓派上交叉编译记得在 sysroot 里也要包含 plugins/platforms 目录。有的嵌入式板卡只有 linuxfb那就要把QT_QPA_PLATFORMlinuxfb显式写进启动脚本。这个问题在资源里出现过我当时在虚拟机里跑完发现平台插件被 qmake 缓存指向了错误位置。现象链接时报 cannot find -lpublic。原因.pro 文件里LIBS -lpublic而项目里从来没有一个叫 libpublic.so 或 public.lib 的文件。很多教程让你加LIBS -lxxx但实际库名不对。解决去源码目录找.so或.a文件用LIBS -L$$PWD/libs -lpublic_1.0这种写法并确保库文件在指定路径。还有一种情况是-lpublic是笔误本来想链接的是-lpub检查拼写。如果项目里根本没用到外部库就把这行 LIBS 删掉。现象窗口在高分屏下字体模糊按钮错位。原因Qt 默认没有启用高 DPI 缩放。解决在 main.cpp 的 main 函数最开始加两行QApplication::setAttribute(Qt::AA_EnableHighDpiScaling);QApplication::setAttribute(Qt::AA_UseHighDpiPixmaps);如果 Qt 6 以上版本默认开启但这行代码也兼容。Ribbon 的按钮高度是像素固定的在 150% 缩放下会显得很小建议在 QSS 里用min-height: 34px;配合font-size的 pt 单位而不是固定 px。如果用了 pixmap 图标也要给每个图标准备 2x 版本。现象点击面板按钮没有响应但按钮本身能悬停高亮。原因事件被上层控件截获了常见于把 QTabBar 的父窗口设成 RibbonBar 后QTabBar 自行处理了鼠标事件导致内容区按钮收不到。解决在 RibbonBar 里重写eventFilter或者确保按钮的父链上是 RibbonPanel而不是主窗口。另一个坑是 lambda 捕获了局部变量如果循环里添加按钮需要用[this, info]而不是[]否则悬空引用。资源里所有 addButton 都用值捕获你要改代码时注意保持这个习惯。6. 进阶玩法给 Ribbon 加状态记忆与悬停动画界面能跑不算完用户用起来顺手才叫有效。这个资源里我额外加了两个很能提升体验的小功能记住用户停靠的标签页以及让标签下划线滑动起来。它们都不需要引入第三方库只是 QSettings 和 QPropertyAnimation 的简单组合。6.1 用 QSettings 记住用户停留的标签页每次启动都回到“开始”标签用户如果一直在用“插入”就会烦。简单做法在标签切换时写入 QSettings下次启动读出来设置当前索引。void MainWindow::closeEvent(QCloseEvent *e) { QSettings settings(MyCompany, RibbonDemo); settings.setValue(lastTabIndex, m_ribbonBar-currentIndex()); QMainWindow::closeEvent(e); }在构造函数里读回m_ribbonBar-setCurrentIndex(settings.value(lastTabIndex, 0).toInt());注意要在 addPage 完成之后再 setCurrentIndex否则布局还没建好设置索引可能无效。6.2 给标签选中下划线加一个小动画纯 QSS 切换会生硬但 QTabBar 支持动画又比较麻烦。一个土办法是在标签下边放一条 QWidget用 QPropertyAnimation 移动它。需要先获取标签几何区域动画移动下划线 widget 的 pos。这个技巧可以让界面显得不那么“呆板”。QPropertyAnimation *anim new QPropertyAnimation(m_indicator, pos, this); anim-setDuration(120); anim-setStartValue(m_indicator-pos()); anim-setEndValue(QPoint(targetX, m_indicator-y())); anim-start(QAbstractAnimation::DeleteWhenStopped);注意这个下划线 widget 的父窗口必须是 QTabBar 的内容容器否则坐标不匹配。另外切换标签时如果中间有用户快速点击多个标签动画会排队最好在每次开始前anim-stop()一下。资源里还给动画设置了 EasingCurve默认是 OutCubic手感比较柔和。其实 Ribbon 按钮多了以后我更推荐把按钮配置抽成 JSON用 QJsonDocument 解析再映射到 QAction。这样客户要加功能时我只需改配置文件不用重新编译。这个习惯是某次交付被逼出来的——客户连续一周每天要求加两个按钮我每次都改代码编译打包崩溃。后来改成 JSON 驱动三分钟加一个按钮客户满意了我也轻松了。从那以后我每次写 Ribbon 界面都强制走一遍“配置-加载-信号映射”的流程先定义接口再写 JSON最后才是样式调整。希望帮到你。本文还有配套的精品资源点击获取