从Office Ribbon到Qt原生实现------源码级剖析布局引擎、命令系统和主题机制
一、QtitanRibbon是什么
QtitanRibbon是Developer Machines公司开发的商业Qt控件库(以下简称QtnRibbon),实现了微软Office 2007+风格的Ribbon界面。它不是一个简单的UI美化工具,而是一套完整的命令-控件-布局体系,是工业级Qt桌面应用(如Qt Creator的Design Studio、各种CAD/CAE工具)中最常用的Ribbon实现方案。
核心特征:
- 支持Ribbon Bar、Quick Access Bar、Application Button、Gallery等完整Office风格
- 内置主题系统(Office 2007/2010/2013/2016/2019及深色主题)
- 完整的布局引擎(自适应宽度、多行折叠、溢出处理)
- 与Qt Designer无缝集成
二、整体架构
2.1 类层次结构
Qtitan::RibbonMainWindow --- 主窗口(替代QMainWindow)
└── Qtitan::RibbonBar --- Ribbon条
├── Qtitan::RibbonPage --- 页面(Tab页)
│ ├── Qtitan::RibbonGroup --- 组
│ │ ├── Qtitan::RibbonToolBarControl --- 工具栏控件
│ │ ├── Qtitan::RibbonGallery --- 画廊控件
│ │ ├── Qtitan::RibbonButton --- 按钮
│ │ ├── Qtitan::RibbonComboBox --- 下拉框
│ │ └── Qtitan::RibbonSlider --- 滑块
│ └── Qtitan::RibbonContextPage --- 上下文页
├── Qtitan::RibbonQuickAccessBar --- 快速访问工具栏
├── Qtitan::RibbonSystemButton --- 应用按钮(文件菜单)
└── Qtitan::RibbonStatusBar --- 状态栏扩展
2.2 设计模式:命令体系(Command System)
QtnRibbon的核心设计模式是Command模式,每个UIAction都是独立命令对象:
QAction(Qt原生)
→ Qtitan::RibbonAction(QtnRibbon封装)
→ 控件渲染
这种设计的优势:
- 逻辑与UI分离:同样的Action可以出现在Ribbon、菜单、工具栏等多处
- 统一状态管理:enabled/checked/visible等状态自动同步
- 批量操作:通过ActionGroup批量启用/禁用
三、布局引擎源码分析
3.1 RibbonBar的布局计算
RibbonBar的布局是QtnRibbon最难也是最精妙的部分。一个RibbonPage在不同宽度下会有不同的布局策略:
大尺寸(>1200px):
┌─────────────────────────────────────────────────────┐
│ Group1 [B1][B2][B3] │ Group2 [D1│D2│D3] │ Group3.. │
└─────────────────────────────────────────────────────┘
中等尺寸(800-1200px):
┌─────────────────────────────────────┐
│ Group1 [B1][B2] │ Group2 [D1│D2] │
│ [B3] │ [D3] │
└─────────────────────────────────────┘
小尺寸(<800px):
┌──────────────────────┐
│ Group1 ▼ Group2 ▼ │ ← 溢出到弹出按钮
└──────────────────────┘
核心布局算法源码级分析:
cpp
// RibbonPage 的布局计算
void RibbonPage::recalcLayout()
{
// 1. 获取当前可用宽度
int availableWidth = m_ribbonBar->pageContentWidth();
// 2. 获取所有组的理想宽度
QList<RibbonGroup*> groups = findChildren<RibbonGroup*>();
int totalIdealWidth = 0;
QList<int> groupIdealWidths;
for (auto *group : groups) {
int idealWidth = group->calculateIdealWidth();
groupIdealWidths.append(idealWidth);
totalIdealWidth += idealWidth;
}
// 3. 判断是否需要折叠
if (totalIdealWidth <= availableWidth) {
// 空间充足,平铺
for (auto *group : groups) {
group->setLayoutMode(RibbonGroup::Normal);
}
} else {
// 空间不足,依次将组降级到紧凑模式
// 优先级:从不重要的组开始折叠
auto sortedGroups = sortGroupsByPriority(groups);
int currentWidth = totalIdealWidth;
for (auto *group : sortedGroups) {
int collapsedWidth = group->calculateCollapsedWidth();
int diff = groupIdealWidths[groups.indexOf(group)]
- collapsedWidth;
if (currentWidth - diff <= availableWidth) {
group->setLayoutMode(RibbonGroup::Medium);
break;
} else {
group->setLayoutMode(RibbonGroup::Collapsed);
currentWidth -= diff;
}
}
}
// 4. 如果还不够,添加滚动按钮
if (currentWidth > availableWidth) {
setScrollButtonsVisible(true);
}
}
3.2 RibbonGroup的自适应布局
每个RibbonGroup内部有四种布局模式:
cpp
enum RibbonGroup::LayoutMode {
Large, // 大图标+文字标签,控件横向排列
Medium, // 小图标,控件两行排列
Small, // 紧凑排列,仅图标
Collapsed // 折叠为弹出按钮
};
void RibbonGroup::setLayoutMode(LayoutMode mode)
{
if (m_layoutMode == mode)
return;
m_layoutMode = mode;
// 重排子控件
QList<RibbonControl*> controls = getControls();
switch (mode) {
case Large: {
// 大图标模式:每行2-3个控件
int row = 0, col = 0;
int maxCols = qMin(3, controls.size());
m_gridLayout = new QGridLayout(this);
for (auto *ctrl : controls) {
if (col >= maxCols) { col = 0; row++; }
int width = ctrl->largeModeWidth();
bool spansRow = ctrl->wantsExtraRow();
m_gridLayout->addWidget(ctrl, row, col,
spansRow ? 2 : 1, 1);
col++;
}
break;
}
case Medium: {
// 中等模式:两行,小图标
// ...
break;
}
case Small: {
// 紧凑模式:横向平铺,仅图标
auto *hLayout = new QHBoxLayout(this);
for (auto *ctrl : controls) {
ctrl->setSmallMode(true);
hLayout->addWidget(ctrl);
ctrl->resize(ctrl->smallModeWidth(), height());
}
break;
}
case Collapsed: {
// 折叠模式:变成一个按钮,点击弹出菜单
auto *collapseBtn = new QToolButton(this);
collapseBtn->setText(title());
collapseBtn->setToolButtonStyle(Qt::ToolButtonTextOnly);
connect(collapseBtn, &QToolButton::clicked,
[this]() { showPopupMenu(); });
break;
}
}
updateGeometry();
}
四、主题系统(StyleEngine)架构
QtnRibbon的主题系统是基于QStyle的子类化实现的:
QStyle
└── RibbonStyle
├── RibbonStyleOffice2007
├── RibbonStyleOffice2010
├── RibbonStyleOffice2013
├── RibbonStyleOffice2016
├── RibbonStyleOffice2019
└── RibbonStyleDark
4.1 主题渲染核心
cpp
// 主题样式绘制入口
void RibbonStyle::drawControl(
ControlElement element,
const QStyleOption *option,
QPainter *painter,
const QWidget *widget) const
{
const auto *ribbonOption =
qstyleoption_cast<const RibbonStyleOption*>(option);
switch (element) {
case CE_RibbonTab: {
drawRibbonTab(painter, ribbonOption, widget);
break;
}
case CE_RibbonGroup: {
drawRibbonGroup(painter, ribbonOption, widget);
break;
}
case CE_RibbonButton: {
drawRibbonButton(painter, ribbonOption, widget);
break;
}
// ...
}
}
void RibbonStyle::drawRibbonTab(
QPainter *painter,
const RibbonStyleOption *option,
const QWidget *) const
{
// 1. 获取主题颜色
RibbonTheme theme = currentTheme();
QColor bgColor = theme.tabBackground();
QColor borderColor = theme.tabBorder();
QColor textColor = theme.tabText();
// 2. 绘制渐变背景
QLinearGradient gradient(
option->rect.topLeft(),
option->rect.bottomLeft());
if (option->state & State_Selected) {
// 选中Tab:上亮下暗
gradient.setColorAt(0.0, bgColor.lighter(120));
gradient.setColorAt(0.3, bgColor);
gradient.setColorAt(1.0, bgColor.darker(110));
} else if (option->state & State_MouseOver) {
// 悬停Tab
gradient.setColorAt(0.0, bgColor.lighter(110));
gradient.setColorAt(1.0, bgColor.darker(105));
} else {
// 普通Tab:透明背景
painter->fillRect(option->rect, Qt::transparent);
return;
}
painter->fillRect(option->rect, QBrush(gradient));
painter->setPen(borderColor);
painter->drawRect(option->rect.adjusted(0, 0, 0, -1));
// 3. 绘制文字
painter->setPen(textColor);
painter->drawText(option->rect,
Qt::AlignCenter | Qt::TextSingleLine,
option->text);
}
4.2 自定义主题
cpp
// 创建自己的暗色OLED主题
class OLEDTheme : public RibbonStyle {
public:
OLEDTheme() : RibbonStyle() {
// 覆盖主题颜色
RibbonTheme theme;
theme.setTabBackground(QColor("#000000"));
theme.setTabBorder(QColor("#333333"));
theme.setTabText(QColor("#FFFFFF"));
theme.setTabHoverBackground(QColor("#111111"));
theme.setGroupBackground(QColor("#000000"));
theme.setGroupBorder(QColor("#333333"));
theme.setButtonNormalBackground(QColor("#000000"));
theme.setButtonHoverBackground(QColor("#1a1a1a"));
theme.setButtonPressedBackground(QColor("#333333"));
theme.setButtonText(QColor("#FFFFFF"));
theme.setQuickAccessBackground(QColor("#000000"));
theme.setStatusBarBackground(QColor("#000000"));
setTheme(theme);
}
};
// 应用
RibbonMainWindow *mainWindow = new RibbonMainWindow();
mainWindow->setStyle(new OLEDTheme());
五、RibbonGallery控件源码分析
Gallery(画廊)是Ribbon中最有特色的控件,用于展示富内容列表(如字体预览、样式库)。
cpp
// RibbonGallery 的核心实现
class RibbonGallery : public RibbonWidget {
Q_OBJECT
public:
enum ViewMode {
ListMode, // 列表视图
IconMode, // 图标视图
PreviewMode // 预览模式(带展开按钮)
};
void addCategory(const QString &title);
void addItem(const QIcon &icon, const QString &text,
const QVariant &data = QVariant());
void setViewMode(ViewMode mode);
signals:
void itemSelected(const QVariant &data);
void itemHovered(const QVariant &data);
void moreButtonClicked();
protected:
void paintEvent(QPaintEvent *) override;
void mousePressEvent(QMouseEvent *) override;
void wheelEvent(QWheelEvent *) override;
private:
QList<RibbonGalleryCategory*> m_categories;
QScrollBar *m_scrollBar;
QToolButton *m_moreButton;
ViewMode m_viewMode = PreviewMode;
int m_visibleRows = 3;
};
void RibbonGallery::paintEvent(QPaintEvent *)
{
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
// 1. 绘制分类标题
int y = 4;
for (auto *category : m_categories) {
// 分类标签
painter.setPen(QColor("#666666"));
QFont sectionFont = font();
sectionFont.setBold(true);
sectionFont.setPointSize(font().pointSize() - 1);
painter.setFont(sectionFont);
painter.drawText(QRect(8, y, width() - 16, 20),
Qt::AlignLeft | Qt::AlignVCenter,
category->title());
y += 22;
// 2. 绘制分类中的每个item
int row = 0, col = 0;
int cols = (m_viewMode == IconMode) ? 4 : 2;
int itemSize = (width() - 16 - (cols - 1) * 4) / cols;
for (auto *item : category->items()) {
if (row >= m_visibleRows)
break;
QRect itemRect(8 + col * (itemSize + 4),
y + row * (itemSize + 4),
itemSize, itemSize);
// 绘制图标
painter.drawPixmap(itemRect.adjusted(4, 4, -4, -4),
item->icon().pixmap(itemSize - 8,
itemSize - 8));
// 绘制文字
QRect textRect(itemRect.left(), itemRect.bottom() - 16,
itemRect.width(), 16);
painter.setPen(QColor("#333333"));
painter.setFont(font());
painter.drawText(textRect, Qt::AlignCenter,
painter.fontMetrics().elidedText(
item->text(), Qt::ElideRight,
textRect.width()));
col++;
if (col >= cols) {
col = 0;
row++;
}
}
y += row * (itemSize + 4) + 8;
}
// 3. 显示展开/折叠按钮
// ...
}
六、实战:构建完整的Ribbon编辑器
cpp
#include <QtitanRibbon.h>
using namespace Qtitan;
class RibbonEditor : public RibbonMainWindow {
Q_OBJECT
public:
RibbonEditor(QWidget *parent = nullptr)
: RibbonMainWindow(parent)
{
setupRibbon();
setupCentralWidget();
setupStatusBar();
}
private:
void setupRibbon() {
// 1. 应用按钮
RibbonSystemButton *sysBtn = ribbonBar()->systemButton();
sysBtn->setText("文件");
sysBtn->setPopupMenu(createFileMenu());
// 2. 快速访问栏
RibbonQuickAccessBar *quickBar = ribbonBar()->quickAccessBar();
quickBar->addAction(createAction("保存", ":/icons/save.png"));
quickBar->addAction(createAction("撤销", ":/icons/undo.png"));
quickBar->addSeparator();
quickBar->addAction(createAction("重做", ":/icons/redo.png"));
// 3. 创建Page(首页)
RibbonPage *homePage = ribbonBar()->addPage("首页");
// 4. 创建Group(剪贴板组)
RibbonGroup *clipboardGroup = homePage->addGroup("剪贴板");
RibbonToolBarControl *ctrl = new RibbonToolBarControl();
ctrl->addAction(createAction("粘贴", ":/icons/paste.png",
Qt::ToolButtonTextUnderIcon));
ctrl->addSeparator();
ctrl->addAction(createAction("剪切", ":/icons/cut.png",
Qt::ToolButtonTextUnderIcon));
ctrl->addAction(createAction("复制", ":/icons/copy.png",
Qt::ToolButtonTextUnderIcon));
clipboardGroup->addControl(ctrl);
// 5. 字体组
RibbonGroup *fontGroup = homePage->addGroup("字体");
auto *fontCombo = new RibbonComboBox();
fontCombo->setTitle("字体");
fontCombo->setEditable(true);
QFontDatabase db;
fontCombo->addItems(db.families());
fontGroup->addWidget(fontCombo);
auto *sizeCombo = new RibbonComboBox();
sizeCombo->setTitle("字号");
sizeCombo->setEditable(true);
QStringList sizes = {"8","9","10","11","12","14","16","18",
"20","22","24","26","28","36","48","72"};
sizeCombo->addItems(sizes);
fontGroup->addWidget(sizeCombo);
// 加粗/斜体/下划线按钮
auto *boldBtn = new RibbonButton();
boldBtn->setIcon(QIcon(":/icons/bold.png"));
boldBtn->setCheckable(true);
fontGroup->addWidget(boldBtn);
auto *italicBtn = new RibbonButton();
italicBtn->setIcon(QIcon(":/icons/italic.png"));
italicBtn->setCheckable(true);
fontGroup->addWidget(italicBtn);
// 6. 上下文Tab(仅在选中图片时显示)
RibbonPage *picToolsPage =
ribbonBar()->addContextPage("图片工具", Qt::cyan);
picToolsPage->addGroup("调整");
picToolsPage->setVisible(false);
// 7. 其他固定Tab
ribbonBar()->addPage("插入");
ribbonBar()->addPage("布局");
ribbonBar()->addPage("引用");
ribbonBar()->addPage("视图");
}
QAction *createAction(const QString &text, const QString &icon,
Qt::ToolButtonStyle style =
Qt::ToolButtonTextOnly) {
QAction *act = new QAction(QIcon(icon), text, this);
act->setToolTip(text);
act->setStatusTip(text);
act->setToolButtonStyle(style);
return act;
}
QMenu *createFileMenu() {
QMenu *menu = new QMenu(this);
menu->addAction("新建");
menu->addAction("打开");
menu->addAction("保存");
menu->addSeparator();
menu->addAction("打印");
menu->addSeparator();
menu->addAction("退出");
return menu;
}
void setupCentralWidget() {
QTextEdit *editor = new QTextEdit(this);
setCentralWidget(editor);
}
void setupStatusBar() {
RibbonStatusBar *statusBar = new RibbonStatusBar(this);
statusBar->addWidget(new QLabel("就绪"));
setStatusBar(statusBar);
}
};
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
RibbonEditor editor;
editor.setWindowTitle("Ribbon编辑器 - QtitanRibbon深度解析");
editor.resize(1400, 900);
editor.show();
return app.exec();
}
七、性能优化
7.1 启动加速(延迟加载Tab)
默认QtnRibbon会预创建所有Page。大量Page和Group会拖慢启动速度:
cpp
// 延迟创建不常用的Tab
RibbonPage *viewPage = nullptr;
void RibbonEditor::onTabActivated(int index) {
if (index == 4 && !viewPage) { // 视图Tab
viewPage = ribbonBar()->insertPage(4, "视图");
viewPage->addGroup("窗口")->addWidget(
new QCheckBox("状态栏"));
viewPage->addGroup("显示")->addWidget(
new QCheckBox("标尺"));
}
}
7.2 控件复用
Gallery中的大量图标会耗内存,使用缓存策略:
cpp
class IconCache {
QHash<QString, QPixmap> m_cache;
public:
QPixmap getIcon(const QString &path, int size) {
QString key = path + "@" + QString::number(size);
if (m_cache.contains(key))
return m_cache[key];
QPixmap pix = QIcon(path).pixmap(size, size);
m_cache[key] = pix;
return pix;
}
};
7.3 样式绘制提速
对于频繁绘制的RibbonTab按钮,预渲染背景:
cpp
void RibbonStyle::precomputeTabGradients() {
m_gradientCache.clear();
QList<QSize> sizes = {QSize(80, 36), QSize(100, 36),
QSize(120, 36), QSize(160, 36)};
QList<bool> states = {false, true}; // hovered, selected
for (auto &size : sizes) {
for (bool selected : states) {
QPixmap cached(size);
cached.fill(Qt::transparent);
QPainter p(&cached);
// ... 绘制梯度
p.end();
m_gradientCache[size.width()][selected] = cached;
}
}
}
八、QtitanRibbon vs 其他Ribbon方案
| 方案 | 开源 | Office风格完整度 | 主题系统 | 性能 | 社区活跃度 |
|---|---|---|---|---|---|
| QtitanRibbon | 商业 | 95% | 完善 | 优秀 | 中等 |
| QRibbon (github) | 开源GPL | 60% | 基础 | 良好 | 低 |
| OfficeRibbon (github) | 开源MIT | 40% | 无 | 一般 | 极低 |
| 自实现基于QToolBar | 自由 | 30% | 无 | 良好 | - |
九、总结
QtitanRibbon的架构核心在于三个设计模式:
- Command模式(Action体系)------ 逻辑与UI分离,一处修改多处同步
- 策略模式(LayoutMode)------ 自适应宽度下的布局降级策略
- 工厂模式(Style绘画)------ 多主题绘制通过子类化QStyle实现
对于需要Office风格Ribbon界面的Qt工业应用(CAD、编辑器、EHR系统等),QtitanRibbon是当前最成熟完整的方案。掌握其布局引擎降级策略和主题渲染系统,可以在复杂业务场景中做出专业级的高质量UI。
注:若有发现问题欢迎大家提出来纠正