简 述: 使用 QStyle / DTK 来实现重绘自定义需求控件(Qt-GUI 没有的),此处以重绘 ios 的控件 MySwitchButton 举例。

[TOC]


本文初发于 “偕臧的小站“,同步转载于此。


编程环境:

  💻: uos 20 amd64 📎 Qt 5.11.3

  💻: MacOS 10.14.6 📎 Qt 5.12.6

  💻: win10 x64 📎 Qt 5.9.8


背景铺垫:

​ 由上上一篇 的文章,已理解 QStyleQCommonStyle 和自定义的 MyStyle 三者的关系;对基础的风格控件也有所学习了;

​ 和上一篇 文章中,已理解如何使用 MyStyle 来绘画 Qt 自带的控件的风格,也有一次的实战重绘 QCommonStyle 里面的虚函数的项目;

​ 在此,本文收尾,分析开源社区精神小伙们,它们是怎么对 DTK 的库进行重绘控件的。想到的新学习的框架,良心说,开源,真的能够学习到很多知识。 本篇重点介绍,如何创建一个新的自定义的控件,并且对其进行绘画,写一个简单工程,进行示范。


需求分析:

​ 因 DTk/QStyle 绘画的这个系列的完整性,得自己想一个比较好的控件啊,挠了挠头,iPhone 给了我感觉。SwitchButton 控件是 ios / Andiroad 📱的控件,不属于 PC 的控件,更非是 Qt 自带的一个控件;用电脑进行绘画,正好是妙哉。其 UI 原型图如下;功能就是点击一下,就会在 开/关 状态之间来回切换,且展示不同的颜色背景。


工程文件分析:

​ 这里分析下,整个项目的构成组成;这里还是再次简单梳理一番:

  • QtStyleEx:
    • main.cpp: 程序的总入口
    • widget.h: 显示 GUI 控件的背景窗口,继承于 QWidget 的类的声明
    • widget.cpp: 显示 GUI 控件的背景窗口,继承于 QWidget 的类定义
    • mystyle.h: 自定义风格(不属于 OS 自带风格)类的声明
    • mystyle.cpp: 自定义风格(不属于 OS 自带风格)的类定义
    • myswitchbutton.h: 自定义需求的控件类的声明
    • myswitchbutton_p.h: 自定义需求的控件类的数据类 Private 的声明
    • myswitchbutton.cpp: 自定义需求的控件类和变量 Private 类的实现

其中主要涉及的几个类如下:


整理所有类的思维导图:

​ 这上面的这些问题,顺其自然的找到它们对应的类。按照各自的功能划分,可以得到如下的思维导图。提纲挈领,写代码不会迷失在细节👩‍💻;


实现流程图:

​ 有了 UI 设计图,和熟悉的功能,已经进行重绘控件;通过以上的代码逻辑流程,复盘一下 DTK 绘画自定义需求的源码思路。将自己的逻辑插入到 Qt 原本的绘画架构中,利用已经有的 QStyle 架构来进行绘画。梳理出来的流程图如下。


代码实现:

​ 其中代码实现的地方,有几处是的设计很精妙,可能我是才眼界初看开,很有必要的🌶出来单独讲;看着代码来实现复现一个功能很容易,属于借鉴技巧与学习,但容易让自己上钩思想懒惰,不去思考原理。有一些细节是含糊过去的地方;在正式实现之前,主要解决的问题有如下难点:

  • 如何创建 MySwitchButton 控件? 需要继承哪些类❓
    • 控件矩形大小❓
    • 控件的 UI 样式❓
    • 控件如何拆分部件 MyStyle::CE_SwitchButton ❓
  • 如何进入到绘画步骤中❓
    • QPainter 如何绘画该小部件?进入到控件的 paintEvent()函数❓
  • MyStyle 如何区分 Qt 原生控件和自定义的控件❓
    • 如何增加自定义枚举 CE_ , SE_ , PM_ , PE_ 等❓
    • 如何区分重绘画的控件是 Qt 自带的还是自定义枚举元素❓
  • 以及 MyStyle 如何调用这些重载的函数❓

​ 上面👆这些想的清楚了,那么这个架构也清楚了很多。也知晓了绘画的流程。


添加自定义的枚举:

​ 这里新增加的枚举,是属于 MyStyle:: , 而非 QStyle:: 范围。

且要从 CE_CustomBase = QStyle::CE_CustomBase + 0xf00000 开始,新的枚举按照 int 类型依次加一。

class MyStyle : public QCommonStyle
{
public:
    //这里新增加的枚举,是属于 MyStyle:: , 而非 QStyle:: 范围
    enum ControlElement {
        CE_SwitchButton = QStyle::CE_CustomBase + 1,                   //switchButton 控件
        CE_CustomBase = QStyle::CE_CustomBase + 0xf00000
    };
  ...
}

重写 QCommonStyle 的虚函数:

​ 这里的快捷方式创建的枚举,都是不带QStyle::, 但是快捷方式的定义是带是QStyle:: , 此处声明的地方必须加上 QStyle::,后面改写更复杂的得写上MyStyle:: 因添加自定义的枚举。

​ 这里 override 的虚函数,只能够调用旧有的 QStyle:: 的函数。主要用来绘画 Qt 、 自定义新增 的控件枚举。

​ 后面改写更复杂的得写上MyStyle:: 因添加自定义的枚举

class MyStyle : public QCommonStyle
{
public:
    virtual void drawControl(QStyle::ControlElement element, const QStyleOption *opt, QPainter *p, const QWidget *w) const override;
    virtual QRect subElementRect(QStyle::SubElement subElement, const QStyleOption *option, const QWidget *widget) const override;
    virtual void drawComplexControl(QStyle::ComplexControl cc, const QStyleOptionComplex *opt, QPainter *p, const QWidget *widget) const override;
    virtual QSize sizeFromContents(QStyle::ContentsType ct, const QStyleOption *opt, const QSize &contentsSize, const QWidget *w) const override;
    virtual void polish(QWidget *widget) override;
    virtual void unpolish(QWidget *widget) override;
  ...
}

内敛函数调用 MyStyle:: 强制转换为 QStyle:: 调用:

​ 新增加的枚举属 MyStyle:: , 之能够在此内敛函数里面调用;主要用来绘画 自定义新增 的控件枚举 –> 实际调用在 下面的 virtual 里面绘画。

class MyStyle : public QCommonStyle
{
public:  //声明如下:
    inline void drawPrimitive(MyStyle::PrimitiveElement pe, const QStyleOption *opt, QPainter *p, const QWidget *w = nullptr) const;
    inline void drawControl(MyStyle::ControlElement element, const QStyleOption *opt, QPainter *p, const QWidget *w) const;
    ...
}

//定义如下:
void MyStyle::drawPrimitive(MyStyle::PrimitiveElement pe, const QStyleOption *opt, QPainter *p, const QWidget *w) const
{
    proxy()->drawPrimitive(static_cast<QStyle::PrimitiveElement>(pe), opt, p, w);
}

void MyStyle::drawControl(MyStyle::ControlElement element, const QStyleOption *opt, QPainter *p, const QWidget *w) const
{
    proxy()->drawControl(static_cast<QStyle::ControlElement>(element), opt, p, w);
}

设计静态函数同名接口让 MyStylrHelp 调用:

​ static 函数,供 MyStylrHelp 调用 [用来绘画自增加的控件枚举]。

class MyStyle : public QCommonStyle
{
public:  //声明如下:
  static void drawPrimitive(const QStyle *style, MyStyle::PrimitiveElement pe, const QStyleOption *opt, QPainter *p, const QWidget *w = nullptr);
    static void drawControl(const QStyle *style, MyStyle::ControlElement element, const QStyleOption *opt, QPainter *p, const QWidget *w);
  ...
}

//定义如下:
void MyStyle::drawPrimitive(const QStyle *style, MyStyle::PrimitiveElement pe, const QStyleOption *opt, QPainter *p, const QWidget *w)
{
		//没有使用到,因为具体的设计因为每个人的思路设计不同,决定的具体的绘画地方也不一样;这里我是放到 重写的虚函数里面,和绘画 Qt 的虚函数的框架里面一起绘画
}

辅助类 MyStyleHelp , 区分绘画控件:

​ 辅助类 MyStyleHelp , 帮助区分绘画到底是绘画 MyStyle::PrimitiveElement 还是,它的类的实现如下:

class MyStyleHelp
{
public:
    inline MyStyleHelp (const QStyle* style = nullptr);
    inline void drawPrimitive(MyStyle::PrimitiveElement pe, const QStyleOption *opt, QPainter *p, const QWidget *w = nullptr) const;
    inline void drawControl(MyStyle::ControlElement element, const QStyleOption *opt, QPainter *p, const QWidget *w) const;
  ...

private:
    const QStyle* m_qStyle;
    const MyStyle* m_myStyle;
};

​ 其中放一个函数的实现,会简单发现这个 三目表达式,很是精华,属于一段点睛之笔;设计上面,既可以专门绘画自定义的新增加的控件,也可以绘画 Qt 的控件;有着很好的扩展性和代码的健壮性。

void MyStyleHelp::drawPrimitive(MyStyle::PrimitiveElement pe, const QStyleOption *opt, QPainter *p, const QWidget *w) const
{
    m_myStyle ? m_myStyle->drawPrimitive(pe, opt, p, w)
              : MyStyle::drawPrimitive(m_qStyle, pe, opt, p, w);
}

MyStylePainter 画家,在指定的“画板”上绘画:

​ 其直接继承于 class MyStylePainter : public QPainter;本也可以不这样再单独设计一个画家类,为了展现此架构的可扩展性,此处会再封装一下,是的分风格更为统一。其类的设计如下:

class MyStylePainter : public QPainter
{
public:
    MyStylePainter(QWidget* w);
    void drawPrimitive(MyStyle::PrimitiveElement pe, const QStyleOption *opt);
    void drawPrimitive(QStyle::PrimitiveElement pe, const QStyleOption *opt);
  ...

private:
    QWidget* m_widget;
    QStyle* m_qStyle;
    MyStyleHelp m_myStyleHelp;
};

这里面,相反,我觉得最重要的除了三个成员变量之外,就是这个构造函数,里面调用父类的函数 QPainter::begin(w);

MyStylePainter::MyStylePainter(QWidget* w)
{
    m_widget = w;
    m_qStyle = w->style();
    m_myStyleHelp.setStyle(m_qStyle);
    QPainter::begin(w); //是调用父类的 begin(), 调试半天才发现
}

控件 MySwitchButton 设计:

​ 作为新的控件,使用 xxx 和它的数据类 xxxPrivate 进行构建,两个类之间依靠宏 Q_D 和 Q_Q 来进行互相的调用;

  • MySwitchButton && MySwitchButtonPrivate
  • Q_D(MySwitchButton) && Q_Q(MySwitchButton)
class MySwitchButtonPrivate;
class MySwitchButton : public QAbstractButton
{
    Q_OBJECT
public:
    explicit MySwitchButton(QWidget* parent = nullptr);
    ~MySwitchButton();

    virtual QSize sizeHint() const override;
protected:
    virtual void paintEvent(QPaintEvent *event) override;

private:
    void initStyleOption(QStyleOptionButton *opt) const;
    Q_DECLARE_PRIVATE(MySwitchButton)
};

这里的自定义控件, 在 paintEvent 函数里面,通过画笔进行绘画,与 MyStyle::CE_SwitchButton 关联上。

void MySwitchButton::paintEvent(QPaintEvent *event)
{
    Q_UNUSED(event)

    QStyleOptionButton opt;
    initStyleOption(&opt);

    MyStylePainter painter(this);
    painter.setRenderHint(QPainter::Antialiasing);
    painter.drawControl(MyStyle::CE_SwitchButton, &opt); //重点
}

运行效果:

​ 附上一张完整的 gif 的动图:当然是重点看的 switchButton 控件效果,咳咳,👀 不要跟着鼠标乱动。


下载地址:

QtExample03

欢迎 star 和 fork 这个系列的 qt/dtk 学习,附学习由浅入深的目录。


系列文章:

  1. QStyle设置界面的外观和QCommonStyle继承关系图讲解和使用
  2. QStyle/DTK重绘Qt-GUI已有控件,举例QScrollBar
  3. QStyle/DTK重绘自定义需求控件,举例MySwitchButton