1. Ribbon功能区介绍
Ribbon是把菜单栏和工具栏合并了,并通过一个tab控件进行展示,Ribbon是无法简单的使用Tab+Toolbar替代的,涉及到很多细节问题,SARibbon在设计时参考了MFC Ribbon接口的命名风格,标签页称之为Category(种类),每个Category下面有多个pannel(面板),面板下面管理着toolbutton,pannel有点类似传统的Toolbar,其层次结构如下图所示,这些命名参考了MFC的ribbon界面类。
- Category 类别,代表一个标签所呈现的内容,对应SARibbonCategory
- Context Category 上下文类别,这个是一种特殊的类别,它正常不显示,需要基于上下文判断是否应该显示,最常用的就是word中插入图片后,会有图片修改相关的标签出现,如果没选中图片,这个标签就消失,这个就是上下文类别,对应SARibbonContextCategory
- Pannel 面板,这个是一组菜单的集合,类似一个Toolbar,对应SARibbonPannel
- Application Button 应用按钮,标签栏最左边的按钮(word就是对应文件按钮),这个按钮会触发一些特殊的页面或菜单,对应SARibbonApplicationButton,可以隐藏
- Quick A***ess Bar 快速响应栏,位于最顶部的一个简单工具栏,用于放置一些常用的action,对应SARibbonQuickA***essBar
- Gallery 预览控件,这是Ribbon最吸引眼球的控件,用直观的图像把功能显示出来,甚至有些会根据上下文进行实时渲染,典型的就是word开始标签下的样式选择,对应SARibbonGallery
SARibbonBar的层次如下图所示:
1.1 样式
SARibbonBar::OfficeStyle Ribbon模式示例:
2. 基本功能区设置
2.1 安装动态库(推荐)
安装静态库中,将自身程序结构变得复杂,影响编写程序的整体性。
将动态库文件拷贝在项目的子文件夹中:
在项目中配置:
- 配置属性 → VC++目录 → 包含目录:头文件位置
- 链接器 → 常规 → 附加库目录:lib文件位置
- 链接器 → 输入 → 附加依赖项:.lib文件名称
部署动态库,将.dll复制到以下任一位置:
- 项目的运行程序目录下
- 系统PATH包含的目录
- 与可执行文件相同的目录
2.2 在MainWindow中使用Ribbon
操作步骤:
- 修改主窗口的父类
要MainWindow中使用SARibbon,需要把QMainWindow替换为SARibbonMainWindow,SARibbonMainWindow修改了QMainWindow对menubar的渲染方式。如下图所示:
//h文件中
#include "SARibbonMainWindow.h"
class QtMainWindowRibbon : public SARibbonMainWindow
{
Q_OBJECT
public:
QtMainWindowRibbon(QWidget *parent = nullptr);
~QtMainWindowRibbon();
private:
Ui::QtMainWindowRibbonClass *ui;
};
//cpp文件中
QtMainWindowRibbon::QtMainWindowRibbon(QWidget *parent)
: SARibbonMainWindow(parent)
, ui(new Ui::QtMainWindowRibbonClass())
{
ui->setupUi(this);
}
- 在Qt Designer中删除菜单栏
基本框架显示效果如下如所示:
2.3 在QWidget中使用SARibbonBar
SARibbonBar支持在QWidget上使用。在Dialog中使用,测试不可用。
#include "SARibbonWidget.h"
class RibbonWidget : public SARibbonWidget
{
Q_OBJECT
public:
RibbonWidget(QWidget* parent = nullptr);
};
SARibbonWidget类提供了setWidget方法,可以嵌入任意的widget
RibbonWidget::RibbonWidget(QWidget* parent) : SARibbonWidget(parent)
{
// 获取SARibbonBar
SARibbonBar* ribbonbar = ribbonBar();
// QWidget模式下,没有必要再显示标题
ribbonbar->setTitleVisible(false);
// QWidget模式下,直接使用紧凑模式效果更好
ribbonbar->setRibbonStyle(SARibbonBar::RibbonStyle***pactThreeRow);
// 取消applicationbutton
ribbonbar->setApplicationButton(nullptr);
}
2.4 创建Category和Pannel
使用到的类:
- SARibbonPannel:继承于QFrame
- SARibbonCategory:也是继承于QFrame
创建ribbon的顺序是:先创建类别(Category),再创建面板(Pannel),最后创建对应的toolbutton(action) - 使用SARibbonBar::addCategoryPage把Category添加到SARibbonBar中;
- 使用SARibbonCategory::addPannel把Pannel添加到Category中;
- 使用SARibbonPannel::addAction可以在Pannel上添加action。
SARibbonBar *ribbon = ribbonBar();
//添加主标签页 - 通过addCategoryPage工厂函数添加
SARibbonCategory* categoryMain = ribbon->addCategoryPage(tr("Main"));
//使用addPannel函数来创建SARibbonPannel,效果和new SARibbonPannel再addPannel一样
SARibbonPannel* pannel1 = categoryMain->addPannel("Panel 1");
QAction* actSave = new QAction(this);
actSave->setText("save");
actSave->setIcon(QIcon(":/File/save"));
actSave->setObjectName("actSave"); // 为QObject 及其子类的对象设置一个唯一的名称,这个名称可以通过 objectName() 方法获取。
//actSave->setShortcut(QKeySequence(QLatin1String("Ctrl+S")));
actSave->setShortcut(QKeySequence(tr("Ctrl+S"))); //确保适应不用的语言,或者使用actSave->setShortcut(QKeySequence::Save);
pannel1->addLargeAction(actSave);
//信号槽
connect(actSave, &QAction::triggered, this, [=]() {
QMessageBox::about(this, "about", u8"保存文件!!!");
}
);
Note:Ribbon的图标有大有小,通过addLargeAction、addMediumAction、addSmallAction可以组合出不同的布局样式
2.5 ContextCategory 上下文标签
所谓上下文标签是指在特殊情况下才出现的标签/标签组,例如office word在选中图片时会出现图片编辑的上下文标签,如下图所示:
- SARibbon中上下文标签对应的类为SARibbonContextCategory
创建
上下文标签一般在程序初始化的时候就创建好,平时隐藏,等待需要显示的时候再显示,创建上下文标签如下:
由于上下文标签需要使用时唤起,因此,用一个成员变量保存起来是一个比较好的选择,当然也可以遍历查找(SARibbonBar::contextCategoryList可以遍历所有的SARibbonContextCategory)
头文件:
SARibbonContextCategory* m_contextCategory;
CPP文件:
// 上下文标签
SARibbonBar* ribbon = ribbonBar();
mContextCategory = ribbon->addContextCategory(tr("context"), QColor(), 1);
SARibbonCategory* contextCategoryPage1 = mContextCategory->addCategoryPage(tr("Page1"));
createContextCategoryPage1(contextCategoryPage1);
SARibbonCategory* contextCategoryPage2 = mContextCategory->addCategoryPage(tr("Page2"));
createContextCategoryPage2(contextCategoryPage2);
由SARibbonContextCategory创建的SARibbonCategory归SARibbonContextCategory管理,只有SARibbonContextCategory“显示了”,其管理的SARibbonCategory才显示,注意: SARibbonContextCategory并不是一个窗口,所以,它的“显示”打了引号。
要显示一个上下文只需要调用SARibbonBar::showContextCategory/SARibbonBar::hideContextCategory即可:
void MainWindow::onShowContextCategory(bool on)
{
if (on) {
this->ribbonBar()->showContextCategory(m_contextCategory);
} else {
this->ribbonBar()->hideContextCategory(m_contextCategory);
}
}
注意: 如果要删除contextCategory需要调用SARibbonBar::destroyContextCategory,而不是直接delete,调用SARibbonBar::destroyContextCategory之后无需再对ContextCategory的指针delete
this->ribbonBar()->destroyContextCategory(this->m_contextCategory);
2.6 ApplicationButton
ribbon界面左上角有个特殊且明显的按钮,称之为applicationButton,这个按钮一般用于调出菜单,SARibbonBar在构造时默认就创建了applicationButton,可以通过如下方式设置其文字:
SARibbonBar* ribbon = ribbonBar();
if (!ribbon) {
return;
}
QAbstractButton* btn = ribbon->applicationButton();
if (!btn) {
//! ***: SARibbonBar默认就会创建一个SARibbonApplicationButton,因此,在正常情况下,这个位置不会进入
btn = new SARibbonApplicationButton(this);
ribbon->setApplicationButton(btn);
}
btn->setText((" &File "));
默认的applicationButton继承自SARibbonApplicationButton,而SARibbonApplicationButton继承自QPushButton,因此你可以对其进行QPushButton所有的操作,如果想设置自己的Button作为applicationButton也是可以的,只需要调用SARibbonBar::setApplicationButton函数即可。
2.7 QuickA***essBar和rightButtonGroup
QuickA***essBar是左上角的快速工具栏,rightButtonGroup是右上角的快速工具栏,在office模式下分左右两边,在wps模式下,左右将合起来,统一放到右边。
SARibbon中:
- QuickA***essBar对应SARibbonQuickA***essBar类
- rightButtonGroup对应SARibbonButtonGroupWidget类
SARibbonBar在初始化时会默认创建QuickA***essBar和RightButtonGroup,通过SARibbonBar::quickA***essBar和SARibbonBar::rightButtonGroup即可获取其指针进行操作,示例如下:
// 创建QAction
QAction* QtMainWindowRibbon::createAction(const QString& text, const QString& iconurl, const QString& objName)
{
QAction* act = new QAction(this);
act->setText(text);
act->setIcon(QIcon(iconurl));
act->setObjectName(objName);
return act;
}
// 创建QuickA***ess工具栏
void QtMainWindowRibbon::initQuickA***essBar()
{
//获取功能区工具栏
SARibbonBar* ribbon = ribbonBar();
SARibbonQuickA***essBar* quickA***essBar = ribbon->quickA***essBar();
quickA***essBar->addAction(createAction("save", ":/File/save", "save-quickbar"));
quickA***essBar->addSeparator();
quickA***essBar->addAction(createAction("undo", ":/File/undo", "undo"));
quickA***essBar->addAction(createAction("redo", ":/File/redo", "redo"));
quickA***essBar->addSeparator();
}
// 创建RightButtonGruop
void QtMainWindowRibbon::initRightButtonGroup()
{
//获取功能区工具栏
SARibbonBar* ribbon = ribbonBar();
SARibbonButtonGroupWidget* rightBar = ribbon->rightButtonGroup();
QAction* actionHelp = createAction("help", ":/File/help", "help");
connect(actionHelp, &QAction::triggered, this, [this]() {
QMessageBox::information(this,
tr("infomation"),
tr("\n ==============="
"\n SARibbonBar version:%1"
"\n ===============")
.arg(SARibbonBar::versionString()));
});
rightBar->addAction(actionHelp);
}
3. 样式设置
3.1SARibbonBar布局方案
SARibbon支持4种ribbon布局方案,布局方案参考了office的ribbon风格和WPS的ribbon风格,布局方案的切换可 通过***void SARibbonBar::setRibbonStyle(RibbonStyle v)***实现。
office模式是最常见的ribbon模式,tab和标题栏占用位置较多,WPS设计的ribbon模式进行了改良,它为了减小ribbon的高度,把标签和标题栏设置在一起,这样减少了一个标题栏高度,有效利用了垂直空间,同时还把pannel的按钮布局由最大摆放3个变为摆放两个,进一步压缩了垂直空间。
对比
office的word界面和WPS Word界面截图对比
在正常屏幕下,WPS 样式会比 Office 样式减少至少30像素左右的垂直高度,相比1920*1080的屏幕来说,相当于节约了接近3%的垂直空间。
SARibbon中把带有标题栏的称之为宽松布局(Loose),宽松布局的各个元素如下图排列,这个布局和office的默认布局是一致的。
SARibbon中把带有标题栏和tab结合一起的布局方式称之为紧凑布局(***pact),紧凑布局的各个元素如下图排列:
布局方式
SARibbonBar提供了setRibbonStyle函数,可以定义当前的布局方案,枚举SARibbonBar::RibbonStyle定义了四种布局方案:
窗口尺寸
实例
SARibbonPannel* pannelStyle = categoryMain->addPannel(tr(u8"功能区样式设置"));
//窗口功能区样式设置
QButtonGroup* g = new QButtonGroup(categoryMain);
QRadioButton* r = new QRadioButton();
r->setText(tr("use office style"));
r->setObjectName(("use office style"));
r->setWindowTitle(r->text());
r->setChecked(true);
pannelStyle->addSmallWidget(r);
g->addButton(r, SARibbonBar::RibbonStyleLooseThreeRow);
r = new QRadioButton();
r->setObjectName(("use wps style"));
r->setText(tr("use wps style"));
r->setWindowTitle(r->text());
r->setChecked(false);
pannelStyle->addSmallWidget(r);
g->addButton(r, SARibbonBar::RibbonStyle***pactThreeRow);
r = new QRadioButton();
r->setObjectName(("use office 2row style"));
r->setText(tr("use office 2 row style"));
r->setWindowTitle(r->text());
r->setChecked(false);
pannelStyle->addSmallWidget(r);
g->addButton(r, SARibbonBar::RibbonStyleLooseTwoRow);
r = new QRadioButton();
r->setObjectName(("use wps 2row style"));
r->setText(tr("use wps 2row style"));
r->setWindowTitle(r->text());
r->setChecked(false);
pannelStyle->addSmallWidget(r);
g->addButton(r, SARibbonBar::RibbonStyle***pactTwoRow);
connect(g, static_cast<void (QButtonGroup::*)(int)>(&QButtonGroup::idClicked), this, &QtMainWindowRibbon::onStyleClicked);
void QtMainWindowRibbon::onStyleClicked(int id)
{
SARibbonBar::RibbonStyles ribbonStyle = static_cast<SARibbonBar::RibbonStyles>(id);
ribbonBar()->setRibbonStyle(ribbonStyle);
//mActionWordWrap->setChecked(SARibbonToolButton::isEnableWordWrap());
}
3.2 SARibbonBar文字换行,及图标大小
通过SARibbonBar::setEnableWordWrap函数可以控制SARibbonBar的文字是否换行,SARibbonBar的高度是固定的,文字是否换行会影响图标显示的大小,因此,如果你想图标看起来更大,可以设置文字不换行。
在SARibbonBar::RibbonStyle***pactTwoRow的布局模式下,文字不换行的显示效果如下:
SARibbonBar文字设置为不换行后,会使图标的显示空间变得更大。
SARibbonPannel* pannelStyle = categoryMain->addPannel(tr(u8"功能区样式设置"));
//设置功能区图标换行操作
QAction * actWordWrap = createAction(tr(u8"名称换行"), ":/File/wordWrap","wordWarp");
actWordWrap->setCheckable(ribbonBar()->isEnableWordWrap());
pannelStyle->addLargeAction(actWordWrap);
connect(actWordWrap, &QAction::triggered, this, [this](bool b) {
this->ribbonBar()->setEnableWordWrap(b);
});
3.3 不同的“按钮”布局方式
SARibbonPannel提供了三个添加action的方法:
- addLargeAction
- addMediumAction
- addSmallAction
在标准的pannel中,一个action(按钮)有3种布局,以office word为例,pannel的三种布局其实是所占行数:
第一种,占满整个pannel,称之为large
第二种,一个pannel下可以放置2个按钮,称之为medium
第三种,一个pannel放置3个按钮,称之为samll
3行或2行模式
枚举SARibbonPannelItem::RowProportion是为了表征每个窗体在pannel所占行数的情况,在pannel布局中会常用到,这个枚举定义如下:
/**
* @brief 定义了行的占比,ribbon中有large,media和small三种占比
*/
enum RowProportion {
None ///< 为定义占比,这时候将会依据expandingDirections来判断,如果能有Qt::Vertical,就等同于Large,否则就是Small
, Large ///< 大占比,一个widget的高度会充满整个pannel
, Medium ///< 中占比,在@ref SARibbonPannel::pannelLayoutMode 为 @ref SARibbonPannel::ThreeRowMode 时才会起作用,且要同一列里两个都是Medium时,会在三行中占据两行
, Small ///< 小占比,占SARibbonPannel的一行,Medium在不满足条件时也会变为Small,但不会变为Large
};
SARibbonPannel里管理的每个action都会带有一个私有的属性(SARibbonPannelItem::RowProportion),这个属性决定了这个action在pannel里的布局
3.4 隐藏功能区
方法1:setMinimumMode()
通过SARibbonBar类中的setMinimumMode函数实现
// 设置为隐藏模式
void setMinimumMode(bool isHide);
- true为隐藏,false为显示
//功能区按钮隐藏
QAction* actHideRibbon = createAction(tr(u8"隐藏功能区"), ":/File/hideRibbon", "actHideRibbon");
actHideRibbon->setCheckable(true);
pannelStyle->addLargeAction(actHideRibbon);
connect(actHideRibbon, &QAction::toggled, this, [this](bool b) { this->ribbonBar()->setMinimumMode(b); });
//setMinimumMode用于设置ribbon的显示模式
/*connect(ribbonBar(), &SARibbonBar::ribbonModeChanged, this, [actHideRibbon](SARibbonBar::RibbonMode nowNode) {
actHideRibbon->setChecked(nowNode == SARibbonBar::MinimumRibbonMode);
});*/
方法2(推荐):showMinimumModeButton()
// 设置显示隐藏ribbon按钮
void showMinimumModeButton(bool isShow = true);
SARibbonBar *ribbon = ribbonBar();
ribbon->showMinimumModeButton(true);
4. SARibbon的自定义功能
ribbon的自定义是ribbon的一个特色,参考了office和wps的自定义界面,用户可以为自己的ribbon定义非常多的内容,甚至可以定义出一个完全和原来不一样的界面。
以下是office的自定义界面:
SARibbon参考office和wps的界面,封装了方便使用的SARibbonCustomize**类,包括如下5个类:
- SARibbonCustomizeDialog:具体的显示窗口。SARibbonCustomizeDialog把SARibbonCustomizeWidget封装为对话框,实现office那样集成到配置对话框中使用
- SARibbonCustomizeWidget:具体的显示窗口
- SARibbonCustomizeData
- SARibbonActionsManager:是用来管理QAction,把想要自定义的QAction添加到SARibbonActionsManager中管理,并可以对QAction进行分类,以便在SARibbonCustomizeDialog/SARibbonCustomizeWidget中显示
- SARibbonActionsManagerModel
实际用户使用仅会面对SARibbonActionsManager和SARibbonCustomizeDialog/SARibbonCustomizeWidget,其余类用户正常不会使用。
SARibbonCustomizeWidget,SARibbonCustomizeDialog的效果如下图所示:
4.1 给界面添加自定义功能
- 首先定义SARibbonActionsManager作为MainWindow的成员变量
//MainWindow.h 中定义成员变量
SARibbonActionsManager* m_ribbonActionMgr;///< 用于管理所有action
- 在MainWindow的初始化过程中,还需要创建大量的QAction,QAction的父对象指定为MainWindow,另外还会生成ribbon布局,例如添加category,添加pannel等操作,在上述操作完成后添加如下步骤,自动让SARibbonActionsManager管理所有的QAction
//MainWindow的初始化,生成QAction
//生成ribbon布局
m_ribbonActionMgr = new SARibbonActionsManager(mainWinowPtr);
m_ribbonActionMgr->autoRegisteActions(mainWinowPtr);
- SARibbonActionsManager的关键函数autoRegisteActions可以遍历 SARibbonMainWindow下的所有子object,找到action并注册,并会遍历所有SARibbonCategory,把SARibbonCategory下的action按SARibbonCategory的title name进行分类,此函数还会把SARibbonMainWindow下面的action,但不在任何一个category下的作为NotInRibbonCategoryTag标签注册,默认名字会赋予not in ribbon
- 在需要调用SARibbonCustomizeDialog的地方如下操作:
QString cfgpath = "customization.xml";
SARibbonCustomizeDialog dlg(this, this);
dlg.setupActionsManager(m_ribbonActionMgr);
dlg.fromXml(cfgpath);//调用这一步是为了把已经存在的自定义步骤加载进来,在保存时能基于原有的自定义步骤上追加
if (QDialog::A***epted == dlg.exec()) {
dlg.applys();//应用自定义步骤
dlg.toXml(cfgpath);//把自定义步骤保存到文件中
}
- 在MainWindow生成前还需要把自定义的内容加载,因此在构造函数最后应该加入如下语句:
//MainWindow的构造函数最后
sa_apply_customize_from_xml_file("customization.xml", this, m_ribbonActionMgr);
在函数中读取xml文件
// 只能调用一次,当文件不存在或者没有内容时不加载
if (!mHasApplyCustomizeXmlFile) {
if (!QFile::exists("customize.xml")) {
mHasApplyCustomizeXmlFile = true;
return;
}
QFile f("customize.xml");
if (f.size() <= 0) {
mHasApplyCustomizeXmlFile = true;
return;
}
QString str(QString("size of customize.xml :%1 ").arg(f.size()));
QMessageBox::about(this, "XML", str);
mHasApplyCustomizeXmlFile = sa_apply_customize_from_xml_file("customize.xml", ribbonBar(), mActionsManager);
}
sa_apply_customize_from_xml_file是SARibbonCustomizeWidget.h中提供的函数,直接把配置文件中的自定义内容应用到MainWindow中。
这样软件每次启动都会按照配置文件加载。
4.2 自定义功能区并自动加载
程序打开自动加载用户上次自定义功能区控件。
- 加载xml文件,并将标定的功能区显示出来
// 只能调用一次,当文件不存在或者没有内容时不加载。加载成功则显示
if (!mHasApplyCustomizeXmlFile) {
if (!QFile::exists("customize.xml")) {
mHasApplyCustomizeXmlFile = true;
return;
}
QFile f("customize.xml");
if (f.size() <= 0) {
mHasApplyCustomizeXmlFile = true;
return;
}
mHasApplyCustomizeXmlFile = sa_apply_customize_from_xml_file("customize.xml", ribbonBar(), mActionsManager);
}
在窗口构造函数中加载此段代码。
2. 调整功能区,并保存xml文件
SARibbonCustomizeDialog dlg(this);
dlg.setupActionsManager(mActionsManager);
// 如果启动时未成功应用上次修改,则将相关动作都清楚掉
if (!mHasApplyCustomizeXmlFile) {
QFile::remove("customize.xml");
dlg.clear();
mHasApplyCustomizeXmlFile = true;
}
//从xml中加载QList<SARibbonCustomizeData>,对于基于配置文件的设置,对话框显示前建议调用此函数,保证叠加设置的正确记录
dlg.fromXml("customize.xml");
//点击对话框上的ok确定修改自定义功能区,并将修改控件保存起来
if (SARibbonCustomizeDialog::A***epted == dlg.exec()) {
// 先apply
if (dlg.isCached())
dlg.applys();
// 无更改直接退出
if (!dlg.isApplied()) {
return;
}
QByteArray str;
QXmlStreamWriter xml(&str);
xml.setAutoFormatting(true);
xml.setAutoFormattingIndent(2);
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) // QXmlStreamWriter always encodes XML in UTF-8.
xml.setCodec("utf-8");
#endif
xml.writeStartDocument();
bool isok = dlg.toXml(&xml);
xml.writeEndDocument();
if (isok) {
//保存xml文件
QFile f("customize.xml");
if (f.open(QIODevice::ReadWrite | QIODevice::Text | QIODevice::Truncate)) {
QTextStream s(&f);
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) // QTextStream always encodes XML in UTF-8.
s.setCodec("utf-8");
#endif
s << str;
s.flush();
}
}
}