QStyle实现自绘界面项目实战(一)

1.前言     

QT自绘技术系列文章链接如下: 

  1.  《QStyle类用法总结(一)》。对Qt自定义风格简单描述,对QStyle及其相关类作了概念性的描述。
  2. QStyle类用法总结(二)》。对QStyle及其相关类作了详细描述。
  3. QStyle类用法总结(三)》。对QStyle中的各widget元素的层次继承树做了详细描述。
  4. QProxyStyle用法简述》。通过一个简单的例子,演示了QProxyStyle类用法。
  5. QStyle实现自绘界面项目实战(一)》通过项目实战来理解前几篇提到的内容,理解QStyle及其子类在自绘时的工作机制。
  6. QStyle实现自绘界面项目实战(二)》通过项目实战来理解前几篇提到的内容,理解QStyle及其子类在自绘时的工作机制。

 本文通过项目实战来理解前几篇提到的内容,理解QStyle及其子类在自绘时的工作机制。在阅读本文章之前,请先阅读上述列出的相关文章,否则没有自绘基础,很难读懂本文章。

2.问题的提出

         问题的提出:在不用样式表的情况下,如何完全用代码实现如下界面,且该界面能符合各种平台风格:

图1

       Qt内建的各种widgets用QStyle类执行近乎所有关于这些widgets的绘制工作,以保证这些widgets看起来非常接近它们所处操作系统平台的风格。下面这张图显示QComboBox 在9种不同操作系统平台下的风格:

图2 QComboBox 在9种不同的操作系统平台下的风格

        如何只需写一次代码能实现图1的风格,且能像图2的QComboBox 能适应9种不同操作系统平台下的风格?

3.工程说明

    style工程存放在Qt安装目录下的Examples\Qt-x.xx.xx\widgets\widgets\styles目录下。其中
x.xx.xx为Qt的版本号。如:5.14.1

4.工程代码剖析

 本工程通过NorwegianWoodStyle类的成员函数结合PE_、CC_、SE_、PM_、CT_、SC_、CE_、开头的标识符实现自绘。这些标识符分别表示什么意思,及绘制这些标识符表示的部件元素的函数drawPrimitive、drawComplexControl、drawControl用法,请参考:

QStyle类用法总结(二)》、《QStyle类用法总结(三)》及Qt Assist,如果不理解这些标识符含义,很难理解自绘是怎么实现的。

4.1 .NorwegianWoodStyle类

4.1.1.standardPalette函数

      该函数重写了从基类QStyle继承过来的standardPalette()函数。该函数的作用是返回一个标准调色板。该函数负责各个widget部件的背景色、前景色、文本前景色、背景色、高亮、对比突出显示等颜色设置。总之,一切和界面颜色相关的设置都在该函数中进行。本函数中用到QPalette类颜色角色、颜色组等知识点,可参见《QPalette类的使用和说明》描述。读者可以更改各个颜色角色对应的颜色,从而更直观地看看每个颜色角色到底是表示的啥含义。

4.1.2.polish() 、unpolish()函数

       QStyle提供了polish() 、unpolish()函数。所有widgets在被显示之前,都会调用polish()函数,在隐藏之后,会调用unpolish()函数。可以利用这些函数在widget上设置一些属性或者做些其它工作。例如:如果你需要知道鼠标是否在widge上悬浮,可以通过QWidget::setAttribute()函数设置Qt::WA_Hover属性,之后,State_MouseOver状态标志将会在widget的QStyleOption参数中设置。

4.1.3.pixelMetric函数

        本函数计算widget各个部件元素占用像素尺寸。如:本函数中的PM_ComboBoxFrameWidth表示combo box组合框边框的宽度,其默认等同于widget中的默认边框占据的宽度,通常为2个像素。可以看到,在本函数中拦截并修改combo box组合框边框的宽度为8个像素宽。 如下为  PM_ComboBoxFrameWidth为8时的效果图:

图1

 如下为  PM_ComboBoxFrameWidth为98时的效果图: 

图2

如下为  PM_ComboBoxFrameWidth为2时的效果图:

图3

可以看到高度、宽度都发生了变化。

              PM_ScrollBarExtent表示垂直滚动条的宽度和水平滚动条的高度。可以看到,在本函数中拦截并修改垂直滚动条的宽度或水平滚动条的高度为QStyle默认的垂直滚动条的宽度或水平滚动条的高度值再加4个像素宽。效果如下:

图4

将加4改为加88,如果如下:

图5

 对于其它元素占据的像素尺寸,则采用QStyle默认的值。

4.1.4.styleHint函数

本函数参见《QProxyStyle用法简述》博文的描述。

4.1.5.drawPrimitive函数

drawPrimitive函数switch语句代码如下:

switch (element) {
    case PE_PanelButtonCommand:
        {
            int delta = (option->state & State_MouseOver) ? 64 : 0;
            QColor slightlyOpaqueBlack(0, 0, 0, 63);
            QColor semiTransparentWhite(255, 255, 255, 127 + delta);
            QColor semiTransparentBlack(0, 0, 0, 127 - delta);

            int x, y, width, height;
            option->rect.getRect(&x, &y, &width, &height);
//! [12]

//! [13]
            QPainterPath roundRect = roundRectPath(option->rect);
//! [13] //! [14]
            int radius = qMin(width, height) / 2;
//! [14]

//! [15]
            QBrush brush;
//! [15] //! [16]
            bool darker;

            const QStyleOptionButton *buttonOption =
                    qstyleoption_cast<const QStyleOptionButton *>(option);
            if (buttonOption
                    && (buttonOption->features & QStyleOptionButton::Flat)) {
                brush = option->palette.window();
                darker = (option->state & (State_Sunken | State_On));
            } 
             else {
                if (option->state & (State_Sunken | State_On)) {
                    brush = option->palette.mid();
                    darker = !(option->state & State_Sunken);
                } 
                 else {
                    brush = option->palette.button();
                    darker = false;
//! [16] //! [17]
                }
//! [17] //! [18]
            }
//! [18]

//! [19]
            painter->save();
//! [19] //! [20]
            painter->setRenderHint(QPainter::Antialiasing, true);
//! [20] //! [21]
            painter->fillPath(roundRect, brush);
//! [21] //! [22]
            if (darker)
//! [22] //! [23]
                painter->fillPath(roundRect, slightlyOpaqueBlack);
//! [23]

//! [24]
            int penWidth;
//! [24] //! [25]
            if (radius < 10)
                penWidth = 3;
            else if (radius < 20)
                penWidth = 5;
            else
                penWidth = 7;

            QPen topPen(semiTransparentWhite, penWidth);
            QPen bottomPen(semiTransparentBlack, penWidth);

            if (option->state & (State_Sunken | State_On))
                qSwap(topPen, bottomPen);
//! [25]

//! [26]
            int x1 = x;
            int x2 = x + radius;
            int x3 = x + width - radius;
            int x4 = x + width;

            if (option->direction == Qt::RightToLeft) {
                qSwap(x1, x4);
                qSwap(x2, x3);
            }

            QPolygon topHalf;
            topHalf << QPoint(x1, y)
                    << QPoint(x4, y)
                    << QPoint(x3, y + radius)
                    << QPoint(x2, y + height - radius)
                    << QPoint(x1, y + height);

            painter->setClipPath(roundRect);
            painter->setClipRegion(topHalf, Qt::IntersectClip);
            painter->setPen(topPen);
            painter->drawPath(roundRect);
//! [26] //! [32]

            QPolygon bottomHalf = topHalf;
            bottomHalf[0] = QPoint(x4, y + height);

            painter->setClipPath(roundRect);
            painter->setClipRegion(bottomHalf, Qt::IntersectClip);
            painter->setPen(bottomPen);
            painter->drawPath(roundRect);

            painter->setPen(option->palette.windowText().color());
            painter->setClipping(false);
            painter->drawPath(roundRect);

            painter->restore();
        }
        break;
//! [32] //! [33]
    default:
//! [33] //! [34]
        QProxyStyle::drawPrimitive(element, option, painter, widget);
    }

PE_PanelButtonCommand表示按钮部件元素,按钮层次树如下:

图6

 红色方框为PE_PanelButtonCommand在层次树中的位置,其对应到按钮上表示的区域如下:

图7

 其含义为:

Button used to initiate an action, for example, a QPushButton.

即表示一个代表动作的按钮。关于更多PE_开头的各值含义及层次树,请参考:

QStyle类用法总结(三)

第14行:通过roundRectPath函数依据按钮所在外包围矩形计算出一个圆角矩形的路径类(QPainterPath)对象roundRect。

第26~30行:如果按钮是扁平按钮,则将绘制roundRect的画刷设置为QPalette::Window 即一般背景色。如果按钮被勾选(State_On为true,此时按钮是QCheckBox)或被按下(State_Sunken为true,此时按钮是QPushButton),则将darker置为true。

第31~35行:如果按钮不是扁平按钮,且如果按钮被勾选(State_On为true,此时按钮是QCheckBox)或被按下(State_Sunken为true,此时按钮是QPushButton),此时将darker置为false,否则置为true,且将绘制roundRect的画刷设置为QPalette::mid。

第46~54,70-71行:用上面的画刷或颜色绘制出圆角矩形roundRect,通过交换画笔,以便按钮呈现不同的样式从而实现按钮勾选或按下状态。

第80-83行:如果布局方向是从右到左,则对x1,x4和x2, x3进行交换,即将原来的左变为右,原来的右变为左。需要说明的是:在一些阿拉伯语的国家,他们的文字的读写方向是从右到左的。

第85-90行:将一些点赋给QPolygon类型的topHalf对象。为了便于后文的描述,暂且称为:

A表示QPoint(x1, y)点;B表示QPoint(x4, y)点;C表示QPoint(x3, y + radius)点;D表示QPoint(x2, y + height - radius)点;E表示QPoint(x1, y + height)点;

第92-108行:绘制圆角矩形等,从而绘制出不规则按钮。第92行通过设置一个QPainterPath类型的裁剪路径对象,则此时除了绘制出如下圆角矩形外,按钮外包围矩形不在此圆角矩形范围内的部分都不会绘制,故经过此代码后,按钮外观如下:(注意:下文为了突出对比,画刷颜色选择蓝色, 画笔颜色选择红色,具体颜色请以本工程代码设置的颜色为准,下同):

图8

       第93 ~ 95 行通过设置一个由A、B、C、D、E点构成的多边形且和92行绘制的roundRect路径对象以交集模式裁剪,则此时按钮所占且绘制区域是如下红色边界多边形和蓝色路径表示的圆角矩形相交部分:

图9

 即如下:

图10 

 同样的:第101~ 104 行通过设置一个由A、B、C、D、E点构成的多边形且和101行绘制的roundRect路径对象以交集模式裁剪,则此时按钮所占且绘制区域是如下红色边界多边形和蓝色路径表示的圆角矩形相交部分:

图11

 注意:

       第4~7行设置鼠标在按钮上悬浮和不悬浮状态的颜色。当在67、68行时,用该颜色分别作为图10、图11的画笔颜色,从而使鼠标移动到按钮上面或移出按钮上面时,呈现不同的边框颜色,这样就会让用户能形成鼠标移进移出按钮的视觉冲击感。

第106~ 108 行:关闭裁剪,以调色板的windowText().color()颜色画出整个圆角矩形。这么做的目的是利用palette.windowText().color()作为画笔颜色、画笔宽度为1,将圆角矩形的边框再绘制一遍,即改变最外面圆角矩形边框颜色,使其颜色一致。 因为整个过程都是用的同一种颜色的画刷,所以本次画出整个圆角矩形,不会将图10、图11画出的部分覆盖。

4.1.6.drawControl函数

该函数中的CE_PushButtonLabel处于QPushButton层次结构树的位置参见

QStyle类用法总结(三)》博文2.2.1节描述。

从Qt Assist得知:

 CE_PushButtonLabel其实就是按钮上的文本子控件即QLabel,如果该QLabel有icon或pixmap,则还包括icon或pixmap。程序能进入到CE_PushButtonLabel所在的case语句,则窗体部件必定是QPushButton、QCheckBox、QRadioButton类型中某种按钮。接下来将option通过qstyleoption_cast强制转换为QStyleOptionButton,因为此时的窗体部件肯定是一个按钮类型,这种转化肯定能成功。接下来判断按钮是否处于禁用状态,如果不是禁用状态,且按钮被按下(此时是QPushButton类型按钮)或被选中状态(此时是QCheckBox或QRadioButton),则将按钮前景色即按钮文本颜色设置为明亮的对比度颜色,一般为白色。

4.2 main.cpp

要使自定义风格样式起作用,必须在main函数QApplication对象之前加入下句代码:

  QApplication::setStyle(new NorwegianWoodStyle);

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值