目录
1.前言
QT自绘技术系列文章链接如下:
- 《QStyle类用法总结(一)》。对Qt自定义风格简单描述,对QStyle及其相关类作了概念性的描述。
- 《QStyle类用法总结(二)》。对QStyle及其相关类作了详细描述。
- 《QStyle类用法总结(三)》。对QStyle中的各widget元素的层次继承树做了详细描述。
- 《QProxyStyle用法简述》。通过一个简单的例子,演示了QProxyStyle类用法。
- 《QStyle实现自绘界面项目实战(一)》通过项目实战来理解前几篇提到的内容,理解QStyle及其子类在自绘时的工作机制。
- 《QStyle实现自绘界面项目实战(二)》通过项目实战来理解前几篇提到的内容,理解QStyle及其子类在自绘时的工作机制。
2 详述
Qt包含一套QStyle子类集合。这些QStyle子类尽可能地实现Qt以支持不同的操作系统平台,例如:QWindowsStyle, QMacStyle 等等。默认地,这些QStyle子类被内建到Qt的GUI模块中了。各种风格也可以通过Qt插件的形式得以访问和利用。
Qt内建的各种widgets用QStyle类执行近乎所有关于这些widgets的绘制工作,以保证这些widgets看起来非常和它们所处的操作系统平台所在的风格一样。下面这张图显示QComboBox 在9种不同操作系统平台下的风格:
2.1 设置风格
整个应用程序风格能够通过QApplication::setStyle() 函数进行设置。它也能够被用户通过
-style命令行指定,如下面那样:
./myapplication -style windows
如果没有风格被指定,Qt将会用最接近你所在操作系统平台或桌面环境的风格来渲染你的应用程序外观。
通过 QWidget::setStyle()函数,你可以为每个QWidget窗体部件指定单独风格。
2.2 开发自适应风格的自定义窗体
如果你想开发自定义的widgets且它们在所有的平台上看起来外观效果都非常好,那么你应该用QStyle类的相关函数去执行窗体部分区域的绘制,像:drawItemText(), drawItemPixmap(), drawPrimitive(), drawControl(), and drawComplexControl()函数。
大部分QStyle类的绘制函数接受4个参数:
- 一个枚举值,该枚举值指定哪种类型的图形元素被绘制。
- 一个QStyleOption 类型参数。该参数指示怎么绘制、在哪个地方绘制条目1中提到的图形元素。
- 一个QPainter类型参数。该参数被用来绘制条目1中提到的图形元素。
- 一个QWidget类型参数。该参数是可选的。绘制过程在该参数表示的widget上执行。
例如:如果你想在你的widget上绘制一个具有焦点的矩形,你可以像下面那样:
void MyWidget::paintEvent(QPaintEvent * /* event */)
{
QPainter painter(this);
QStyleOptionFocusRect option;
option.initFrom(this);
option.backgroundColor = palette().color(QPalette::Background);
style()->drawPrimitive(QStyle::PE_FrameFocusRect, &option, &painter, this);
}
在上面代码中,QStyle类从QStyleOption类型参数中得到需要绘制元素的所有信息。一个widget对象作为最后一个参数被传入函数。在这种情况下,QStyle类可以在传入的widget对象执行一些特殊效果(如:实现mac操作系统上的按钮动画),但这个widget对象不是必要的。事实上,你可以用QStyle在任何绘图设备上进行绘制,不仅仅是widget。
QStyleOption有很多子类,如:
QStyleOptionButton,
QStyleOptionComplex,
QStyleOptionDockWidget,
QStyleOptionFocusRect,
QStyleOptionFrame,
QStyleOptionGraphicsItem,
QStyleOptionHeader,
QStyleOptionMenuItem,
QStyleOptionProgressBar,
QStyleOptionRubberBand,
QStyleOptionTab,
QStyleOptionTabBarBase,
QStyleOptionTabWidgetFrame,
QStyleOptionToolBar,
QStyleOptionToolBox,
QStyleOptionViewItem。
这些子类对应被绘制的图形元素,例如:PE_FrameFocusRect(表示QStyle类下获得焦点的指示器)就对应于表示具有焦点风格的QStyleOptionFocusRect参数。
为保证绘制效率尽可能快,QStyleOption及其子类采用公用数据成员。出于方便,Qt提供QStylePainter类,该类组合了QStyle、QPainter、QWidget类。这使得可以保证在实现相同功能的情况下,使得下面代码:
QStylePainter painter(this);
...
painter.drawPrimitive(QStyle::PE_FrameFocusRect, option);
取代下面的:
QPainter painter(this);
...
style()->drawPrimitive(QStyle::PE_FrameFocusRect, &option, &painter, this);
2.3 创建自定义风格
可以为自己的应用程序创建自定义风格和外观。有两种方法可以创建自定义的风格:一种是静态方法,另外一种是动态方法。当用静态方法创建自定义风格时,要么选择一个Qt 已有风格类作为基类,然后从这个基类中派生自己的风格类,然后重载基类虚函数,从而实现自己要想的行为和外观;或者你可以重载实现QStyle类的虚函数。在动态方法中,你可以在应用程序运行时修改、变更应用程序外观。
静态创建自定义风格的第一步是从Qt已提供的风格基类中选择一中符合、满足自己风格类作为基类。你选择何种Qt已提供的风格类作为实现自己风格类的基类,取决于该基类是否最大化地满足你的风格需求。通常,我们选择Qt已有的QCommonStyle作为自定义风格类的基类,而不是QStyle作为基类。
根据想要改变基类的哪些风格,必须重写那些被用来绘制的部分接口。为了阐述这,我们将把Spin Box的上下箭头的风格改为QWindowsStyle风格(即在windows下的风格)。Spin Box的上下箭头是一个primitive elements。它是drawPrimitive()函数绘制而成的,所以我们需要重写这个函数。我们需要一个下面的类的声明:
class CustomStyle : public QProxyStyle
{
Q_OBJECT
public:
CustomStyle();
~CustomStyle() {}
void drawPrimitive(PrimitiveElement element, const QStyleOption *option,
QPainter *painter, const QWidget *widget) const override;
};
为实现绘制上下箭头,QSpinBox 用 PE_IndicatorSpinUp 和 PE_IndicatorSpinDown表示上下箭头。下面代码实现绘制上下箭头的drawPrimitive函数的重写,从而实现自己想要的箭头的样式:
void CustomStyle::drawPrimitive(PrimitiveElement element, const QStyleOption *option,
QPainter *painter, const QWidget *widget) const
{
// 如果要绘制的窗体元素是上下箭头
if (element == PE_IndicatorSpinUp || element == PE_IndicatorSpinDown)
{
QPolygon points(3);
// option->rect()表示上下箭头占据的最小外包围矩形
int x = option->rect.x();
int y = option->rect.y();
int w = option->rect.width() / 2;
int h = option->rect.height() / 2;
x += (option->rect.width() - w) / 2;
y += (option->rect.height() - h) / 2;
// 如果是上箭头
if (element == PE_IndicatorSpinUp)
{
// 将箭头左端点绘制在矩形的左边框向下高度一半的地方。
points[0] = QPoint(x, y + h);
// 将箭头右端点绘制在左端点向右w处,和左端点高度一致的地方。
points[1] = QPoint(x + w, y + h);
// 将箭头上端点绘制在左端点和右端点之间且比左右端点朝上h的地方
points[2] = QPoint(x + w / 2, y);
}
else // 如果是下箭头
{ // PE_SpinBoxDown
// 将左端点绘制在矩形左上顶点处
points[0] = QPoint(x, y);
// 将右端点绘制在和左端点同一高度,但比左端点向右w即矩形宽度的1/2处
points[1] = QPoint(x + w, y);
// 将箭头下端点绘制在左右端点的中间且朝下为矩形高度的一半处
points[2] = QPoint(x + w / 2, y + h);
}
// 当QSpinBox可用和不可用时,用不同的画笔、画刷绘制,以展示可用或不可用的状态
if (option->state & State_Enabled) {
painter->setPen(option->palette.mid().color());
painter->setBrush(option->palette.buttonText());
} else {
painter->setPen(option->palette.buttonText().color());
painter->setBrush(option->palette.mid());
}
// 绘制三角形,从而形成了视觉上的箭头
painter->drawPolygon(points);
}
else // 非上下箭头部件,则直接调用基类的
{
QProxyStyle::drawPrimitive(element, option, painter, widget);
}
}
上面代码绘制出的pin box的箭头效果类似如下:
上面代码中,并没有用到widget参数。前面提到:绘制需要的信息及如何绘制都在QStyleOption参数对象中指定了,所以上面代码不需要widget参数。如果需要用widget参数获取一些绘制的附加信息,请注意在使用widget参数之前确保该参数对象不为nullptr且是正确的控件类型。例如:
const QSpinBox *spinBox = qobject_cast<const QSpinBox *>(widget);
if (spinBox) {
...
}
上面代码,当实现一个自定义风格类型时,不能因为被绘制元素类型是PE_IndicatorSpinUp 或 PE_IndicatorSpinDown就认为widget参数就是一个QSpinBox类型。
注意:Qt样式表在自定义QStyle子类中当前不被支持,即自定义风格和Qt 样式表只能选其一。Qt 官方计划在Qt未来版本中,实现自定义风格中支持样式表。
Qt 官方自带的例子styles(存放在Qt安装目录下的Examples\Qt-X.XX.XX\widgets\widgets\styles, 其中Qt-X.XX.XX为Qt的版本号,如:Qt-5.14.1)和affine(存放在Qt安装目录下的Examples\Qt-XX.XX.XX\widgets\painting\affine, 其中Qt-X.XX.XX为Qt的版本号,如:Qt-5.14.1)展示了如何自定义自己风格实现GUI的绘制。参见博文
2.4 如何使用自定义风格
在Qt应用程序中,有多种使用自定义风格的方法。最简单的一种方法是:当创建QApplication对象之前,传递自定风格到QApplication::setStyle() 静态函数:
#include <QtWidgets>
#include "customstyle.h"
int main(int argc, char *argv[])
{
QApplication::setStyle(new CustomStyle);
QApplication app(argc, argv);
QSpinBox spinBox;
spinBox.show();
return app.exec();
}
能在创建QApplication对象之前的任何时候调用QApplication::setStyle() 从而设置自定义风格。为了保证、确信自定义风格是否是自己想要的或想提前预览下自定义风格,可以用如下命令行提前测试下:
./myapplication -style custom
其中myapplication是应用程序可执行文件名,cusom是自定义风格类。假如应用程序名为a,自定义风格类是前面提到的CustomStyle,则可以利用windows下的命令行或linux终端,利用如下命令行进行预览CustomStyle风格效果。
./a -style CustomStyle
2.5 View中item风格
Model/View框架中有item,如:QTableWidgetItem、QStandardItem、QTreeWidgetItem等,这些项是被代理绘制的。Qt默认代理类是 QStyledItemDelegate。默认代理类被用来计算项的外包围矩形和对项赋予各种数据角色(如:Qt::DisplayRole、Qt::EditRole等,参见Qt::ItemDataRole枚举)。关于 QStyledItemDelegate的更多详细说明和用法,请参见Qt Assistant 。
当Qt用默认代理类QStyledItemDelegate绘制view中的item时,它绘制CE_ItemViewItem(该枚举值表示view内部的item)且用 CT_ItemViewItem枚举值计算这些item占据区域的尺寸大小,用SE_ItemViewItemText枚举值设置item中文本占据的区域的尺寸大小。当实现一个自定义风格绘制views、item时,需要检测QCommonStyle (和从其它风格继承来的子类)的实现。这样就可以找出是哪一个风格且该风格是怎么样被绘制的,然后重写绘制部件元素的函数,以保证绘制是不同的,从而满足自己的风格需求。如下为一个自定义的绘制view中的item中背景色的例子:
switch (element) {
case (PE_PanelItemViewItem): {
painter->save();
QPoint topLeft = option->rect.topLeft();
QPoint bottomRight = option->rect.topRight();
QLinearGradient backgroundGradient(topLeft, bottomRight);
backgroundGradient.setColorAt(0.0, QColor(Qt::yellow).lighter(190));
backgroundGradient.setColorAt(1.0, Qt::white);
painter->fillRect(option->rect, QBrush(backgroundGradient));
painter->restore();
break;
}
default:
QProxyStyle::drawPrimitive(element, option, painter, widget);
}
其中PE_PanelItemViewItem表示绘制view中item的背景色,当QCommonStyle类在实现CE_ItemViewItem即view中的item时,PE_PanelItemViewItem即item的背景色就会被调用。
为了添加对新数据类型和item新数据角色的支持,需要创建一个自定义代理类,如果仅仅是需要支持默认代理数据类型,此时就不需要创建自定义的风格,因为默认代理都提供了。关于如何自定义代理类,请参见Qt Assistant中对 QStyledItemDelegate的描述。
view列头风格也可以自定义,可以对view列头tem的尺寸大小和行列大小通过自定义风格进行控制。