☰
Qt实现仿WPS Ribbon界面:从结构设计到QSS样式落地
2026/10/3 14:53:59 网站建设 项目流程

简介:面向需要实现Office/WPS风格界面的Qt开发者,这套基于Qt 5.13.2与VS2017的Ribbon封装库,将RibbonBar、RibbonCategory等控件抽象为可调用的API接口,上层应用只需专注业务逻辑即可构建功能分区式界面。压缩包共186个文件,约213KB,76个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 做容器,面板用 QFrame+QToolButton 组合。这样你拥有完全控制权,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 是横向排列的多个 panel;RibbonBar 继承 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
面板内大图标大小24x24

3.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; }把内容区边框去掉。另外,如果标签高度设置了 34px,QTabBar 的固定高度也要保持一致,否则文字会被裁掉。资源里把所有控件的字体统一设置成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 里加载一个外部 QSS:

QFile 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.15),qmake 指向了 5.12 的 mkspecs,但 PATH 里却是 5.15 的 bin。解决:打开 Qt Creator 的构建环境,把 Qt 版本对应的 bin 目录放到 PATH 最前面,或者在命令行里先set PATH=C:\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_PLATFORM=linuxfb显式写进启动脚本。这个问题在资源里出现过,我当时在虚拟机里跑完发现平台插件被 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,最后才是样式调整。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询