• 2539阅读
  • 1回复

Qml组件化编程7-自绘组件 [复制链接]

上一主题 下一主题
离线dd759378563
 

只看楼主 正序阅读 楼主  发表于: 2019-05-19


简介

本文是《Qml组件化编程》系列文章的第七篇,涛哥会罗列Qt中的所有自绘方案,并提供一些案例和说明。
Qt自带的组件,外观都是固定的,一般可以通过qss/Qml style等方式进行定制。
如果要实现外观特殊的组件,就需要自己绘制了。
注:文章主要发布在涛哥的博客知乎专栏-涛哥的Qt进阶之路


自绘方案

Qt中的自绘方案有这么一些:
  • QWidget+QPainter / QQuickPaintedItem+QPainter
  • Qml Canvas
  • Qml Shapes
  • QOpenGLWidget / QOpenGLWindow
  • Qml QQuickFrameBufferObject
  • Qml SceneGraph
  • Qml ShaderEffect
  • QVulkanWindow
(GraphicsView和QWidget的绘制类似,就不讨论了)


QPainter

QPainter是一个功能强大的画笔,QWidget中的各种控件如QPushButton、QLable等都是用QPainter画出来的。
(QWidget的控件在绘制时,还增加了qss样式表,让UI定制变得更加方便。)


QWidget+QPainter 示例

QWidget中使用QPainter的方法,是重载paintEvent事件,这里示例绘制一个进度条:

  1. //MainWindow.h
  2. #pragma once
  3. #include <QMainWindow>
  4. class MainWindow : public QMainWindow
  5. {
  6.     Q_OBJECT
  7. public:
  8.     MainWindow(QWidget *parent = 0);
  9.     ~MainWindow();
  10. protected:
  11.     void paintEvent(QPaintEvent *event) override;
  12.     void timerEvent(QTimerEvent *event) override;
  13. private:
  14.     QList<QColor> mColorList;
  15.     int mCurrent = 0;
  16. };

  1. //MainWindow.cpp
  2. #include "MainWindow.h"
  3. #include <QPainter>
  4. #include <QtMath>
  5. MainWindow::MainWindow(QWidget *parent)
  6.     : QMainWindow(parent)
  7. {
  8.     resize(400, 300);
  9.     mColorList << QColor(51, 52, 54)
  10.                << QColor(75, 85, 86)
  11.                << QColor(87, 103, 103)
  12.                << QColor(95, 119, 121)
  13.                << QColor(101, 132, 134)
  14.                << QColor(104, 146, 145)
  15.                << QColor(104, 158, 158)
  16.                << QColor(101, 169, 168)
  17.                << QColor(92, 182, 180)
  18.                << QColor(79, 194, 191);
  19.     //每秒触发60次定时器,即刷新率60FPS
  20.     startTimer(1000 / 60);
  21. }
  22. MainWindow::~MainWindow()
  23. {
  24. }
  25. void MainWindow::timerEvent(QTimerEvent *) {
  26.     mCurrent =(mCurrent + 3) % 360;
  27.     update();
  28. }
  29. void MainWindow::paintEvent(QPaintEvent *event)
  30. {
  31.     QPainter painter(this);
  32.     painter.setRenderHints(QPainter::Antialiasing|QPainter::TextAntialiasing);
  33.     //原点x坐标
  34.     qreal a = 100;
  35.     //原点y坐标
  36.     qreal b = 100;
  37.     //半径
  38.     qreal r = 80;
  39.     //每个小圆的半径递增值
  40.     qreal roffset = 2;
  41.     //每个小圆的角度递增值
  42.     qreal angleOffset = 30;
  43.     qreal currentangle = mCurrent ;
  44.     for (int i = 0; i < mColorList.length(); i++) {
  45.         qreal r0 = i * roffset;
  46.         qreal angle = currentangle + i * angleOffset;
  47.         qreal x0 = r * cos(qDegreesToRadians(angle)) + a;
  48.         qreal y0 = r * sin(qDegreesToRadians(angle)) + b;
  49.         painter.setPen(mColorList[i]);
  50.         painter.setBrush(QBrush(mColorList[i]));
  51.         painter.drawEllipse(x0  - r0, y0 - r0, 2 * r0, 2 * r0);
  52.     }
  53. }





QQuickPaintedItem+QPainter 示例

QQuickPaintedItem继承自QQuickItem,而QQuickItem就是Qml中的Item。
QQuickPaintedItem通过重载paint函数,就可以使用QPainter绘制。
自定义的QQuickPaintedItem子类需要注册到Qml中才能使用,注册类型或者注册实例都可以,具体可以参考《 Qml组件化编程5-Qml与C++交互》
这里示例QQuickPaintedItem 中使用 QPainter绘制一个阴阳八卦:

  1. //PBar.h
  2. #pragma once
  3. #include <QQuickPaintedItem>
  4. class PBar : public QQuickPaintedItem
  5. {
  6.     Q_OBJECT
  7. public:
  8.     PBar(QQuickItem *parent = nullptr);
  9.     void paint(QPainter *painter) override;
  10.     void timerEvent(QTimerEvent *event) override;
  11. private:
  12.     QList<QColor> mColorList;
  13.     int mCurrent = 0;
  14. };

  1. //PBar.cpp
  2. #include "PBar.h"
  3. #include <QPainter>
  4. #include <QtMath>
  5. PBar::PBar(QQuickItem *parent) : QQuickPaintedItem (parent)
  6. {
  7.     mColorList << QColor(51, 52, 54)
  8.                << QColor(75, 85, 86)
  9.                << QColor(87, 103, 103)
  10.                << QColor(95, 119, 121)
  11.                << QColor(101, 132, 134)
  12.                << QColor(104, 146, 145)
  13.                << QColor(104, 158, 158)
  14.                << QColor(101, 169, 168)
  15.                << QColor(92, 182, 180)
  16.                << QColor(79, 194, 191);
  17.     //每秒触发60次定时器,即刷新率60FPS
  18.     startTimer(1000 / 60);
  19. }
  20. void PBar::paint(QPainter *painter)
  21. {
  22.     //原点x坐标
  23.     qreal a = 100;
  24.     //原点y坐标
  25.     qreal b = 100;
  26.     //半径
  27.     qreal r = 80;
  28.     qreal r1 = r / 2;
  29.     qreal r2 = r / 6;
  30.     qreal currentangle = mCurrent;
  31.     painter->save();
  32.     painter->setRenderHints(QPainter::Antialiasing|QPainter::TextAntialiasing);
  33.     //red 部分
  34.     {
  35.         painter->setBrush(QBrush(QColor(128, 1, 1)));
  36.         QPainterPath path(QPointF(a + r * cos(qDegreesToRadians( currentangle )), b - r * sin(qDegreesToRadians(currentangle ))));
  37.         path.arcTo(a - r, b - r,
  38.                    r * 2, r * 2,
  39.                    currentangle, 180);
  40.         path.arcTo(a + r1 * cos(qDegreesToRadians(currentangle + 180)) - r1, b - r1 * sin(qDegreesToRadians(currentangle + 180)) - r1,
  41.                    r1 * 2, r1 * 2,
  42.                    currentangle + 180, 180);
  43.         path.arcTo(a + r1*cos(qDegreesToRadians(currentangle)) - r1, b - r1 * sin(qDegreesToRadians(currentangle)) - r1,
  44.                    r1 * 2, r1 * 2,
  45.                    currentangle + 180, -180
  46.                    );
  47.         painter->drawPath(path);
  48.     }
  49.     //blue 部分
  50.     {
  51.         painter->setBrush(QBrush(QColor(1, 1, 128)));
  52.         QPainterPath path(QPointF(a + r * cos(qDegreesToRadians( currentangle )), b - r * sin(qDegreesToRadians(currentangle ))));
  53.         path.arcTo(a - r, b - r,
  54.                    r * 2, r * 2,
  55.                    currentangle, -180);
  56.         path.arcTo(a + r1 * cos(qDegreesToRadians(currentangle + 180)) - r1, b - r1 * sin(qDegreesToRadians(currentangle + 180)) - r1,
  57.                    r1 * 2, r1 * 2,
  58.                    currentangle + 180, 180);
  59.         path.arcTo(a + r1*cos(qDegreesToRadians(currentangle)) - r1, b - r1 * sin(qDegreesToRadians(currentangle)) - r1,
  60.                    r1 * 2, r1 * 2,
  61.                    currentangle + 180, -180
  62.                    );
  63.         painter->drawPath(path);
  64.     }
  65.     {
  66.         // red 小圆
  67.         painter->setBrush(QBrush(QColor(128, 1, 1)));
  68.         QPainterPath path;
  69.         path.addEllipse(a + r1 * cos(qDegreesToRadians(currentangle)) - r2, b - r1 * sin(qDegreesToRadians(currentangle )) - r2,
  70.                         r2 * 2, r2 * 2);
  71.         painter->drawPath(path);
  72.     }
  73.     {
  74.         //blue 小圆
  75.         painter->setBrush(QBrush(QColor(1, 1, 128)));
  76.         QPainterPath path;
  77.         path.addEllipse(a + r1 * cos(qDegreesToRadians(180 + currentangle)) - r2, b - r1 * sin(qDegreesToRadians(180 + currentangle)) - r2,
  78.                         r2 * 2, r2 * 2);
  79.         painter->drawPath(path);
  80.     }
  81.     painter->restore();
  82. }
  83. void PBar::timerEvent(QTimerEvent *event)
  84. {
  85.     (void)event;
  86.     mCurrent =(mCurrent + 3) % 360;
  87.     update();
  88. }

  1. //main.cpp
  2. #include <QGuiApplication>
  3. #include <QQmlApplicationEngine>
  4. #include "PBar.h"
  5. int main(int argc, char *argv[])
  6. {
  7.     QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
  8.     QGuiApplication app(argc, argv);
  9.     qmlRegisterType<PBar>("PBar", 1, 0, "PBar");
  10.     QQmlApplicationEngine engine;
  11.     const QUrl url(QStringLiteral("qrc:/main.qml"));
  12.     QObject::connect(&engine, &QQmlApplicationEngine::objectCreated,
  13.                      &app, [url](QObject *obj, const QUrl &objUrl) {
  14.                          if (!obj && url == objUrl)
  15.                              QCoreApplication::exit(-1);
  16.                      }, Qt::QueuedConnection);
  17.     engine.load(url);
  18.     return app.exec();
  19. }

  1. //main.qml
  2. import QtQuick 2.0
  3. import QtQuick.Window 2.0
  4. import PBar 1.0
  5. Window {
  6.     visible: true
  7.     width: 640
  8.     height: 480
  9.     title: qsTr("Hello PBar")
  10.     PBar {
  11.         anchors.fill: parent
  12.     }
  13. }





关于QPainter

QPainter底层使用CPU做光栅化渲染,这种方式在没有GPU的设备中能够很好地工作。
(我的好友"Qt侠-刘典武"就是这方面的实战专家,他手上有将近150个精美的自绘组件,比官方还要多,有需要的同学可以联系他 QQ517216493)
然而时代在飞速发展,很多设备都带上了GPU,QPainter在GPU设备上,将不能发挥GPU的全部实力。
(刘典武也在积极跟进GPU绘制)
这里提一下,有个叫QUItCoding的组织,开发了一套QNanoPainter,接口和QPainter一致,
在大部分场景下都拥有不错的性能。其底层是基于nanovg的GPU加速。
不过QNanoPainter并没有合并进Qt官方,具体原因不清楚, 有可能是因为性能并不是100%达标的。


Qml Canvas

Qml中提供了Canvas组件,接口和html中的Canvas基本一致,可以直接copy html中的Canvas代码(极少部分不能用)。
当然QPainter实现的功能,也都可以移植到Canvas中。
Canvas渲染性能并不太好,如果有性能要求,还是不要用Canvas了。
这里示例绘制一个笑脸

  1. //main.qml
  2. import QtQuick 2.0
  3. import QtQuick.Window 2.0
  4. Window {
  5.     visible: true
  6.     width: 640
  7.     height: 480
  8.     title: qsTr("Hello Canvas")
  9.     Canvas {
  10.         id: canvas
  11.         anchors.fill: parent
  12.         onPaint: {
  13.             var ctx = canvas.getContext('2d');
  14.             ctx.beginPath();
  15.             ctx.arc(75,75,50,0,Math.PI*2,true); // 绘制
  16.             ctx.moveTo(110,75);
  17.             ctx.arc(75,75,35,0,Math.PI,false);   // 口(顺时针)
  18.             ctx.moveTo(65,65);
  19.             ctx.arc(60,65,5,0,Math.PI*2,true);  // 左眼
  20.             ctx.moveTo(95,65);
  21.             ctx.arc(90,65,5,0,Math.PI*2,true);  // 右眼
  22.             ctx.stroke();
  23.         }
  24.     }
  25. }





Qml Shapes

Qt5.10开始,Qml增加了Quick.Shapes功能。这是目前官方提供的自绘途径中,兼顾性能和易用性的最佳选择。
Shapes底层为GPU渲染(基于SceneGraph),QPainter能绘制的基础图元,都可以用Shapes实现。Shapes再配合上Qml中的
属性绑定和属性动画,可以轻易实现各式各样的动态、酷炫的UI。
(后续的自定义组件,涛哥将会优先使用Shapes。)
这里示例实现一个任意圆角的Rectangle组件:

  1. // TRoundRect.qml
  2. import QtQuick 2.12
  3. import QtQuick.Controls 2.5
  4. import QtQuick.Shapes 1.12
  5. Shape {
  6.     id: root
  7.     //左上角是否圆角
  8.     property bool leftTopRound: true
  9.     //左下角是否圆角
  10.     property bool leftBottomRound: true
  11.     //右上角是否圆角
  12.     property bool rightTopRound: true
  13.     //右下角是否圆角
  14.     property bool rightBottomRound: true
  15.     //圆角半径
  16.     property real radius
  17.     //颜色
  18.     property color color: "red"
  19.     //多重采样抗锯齿
  20.     layer.enabled: true
  21.     layer.samples: 8
  22.     
  23.     //平滑处理
  24.     smooth: true
  25.     //反走样抗锯齿
  26.     antialiasing: true
  27.     ShapePath {
  28.         fillColor: color
  29.         startX: leftTopRound ? radius : 0
  30.         startY: 0
  31.         fillRule: ShapePath.WindingFill
  32.         PathLine {
  33.             x: rightTopRound ? root.width - radius : root.width
  34.             y: 0
  35.         }
  36.         PathArc {
  37.             x: root.width
  38.             y: rightTopRound ? radius : 0
  39.             radiusX: rightTopRound ? radius : 0
  40.             radiusY: rightTopRound ? radius : 0
  41.         }
  42.         PathLine {
  43.             x: root.width
  44.             y: rightBottomRound ? root.height - radius : root.height
  45.         }
  46.         PathArc {
  47.             x: rightBottomRound ? root.width - radius : root.width
  48.             y: root.height
  49.             radiusX: rightBottomRound ? radius : 0
  50.             radiusY: rightBottomRound ? radius : 0
  51.         }
  52.         PathLine {
  53.             x: leftBottomRound ? radius : 0
  54.             y: root.height
  55.         }
  56.         PathArc {
  57.             x: 0
  58.             y: leftBottomRound ? root.height - radius : root.height
  59.             radiusX: leftBottomRound ? radius : 0
  60.             radiusY: leftBottomRound ? radius : 0
  61.         }
  62.         PathLine {
  63.             x: 0
  64.             y: leftTopRound ? radius : 0
  65.         }
  66.         PathArc {
  67.             x: leftTopRound ? radius : 0
  68.             y: 0
  69.             radiusX: leftTopRound ? radius : 0
  70.             radiusY: leftTopRound ? radius : 0
  71.         }
  72.     }
  73. }



看一下TRoundRect的用法
  1. import QtQuick 2.0
  2. import QtQuick.Controls 2.5
  3. Rectangle {
  4.     width: 800
  5.     height: 600
  6.     Rectangle { //背景红色,衬托一下
  7.         x: 10
  8.         width: 100
  9.         height: 160
  10.         color: "red"
  11.     }
  12.     TRoundRect {
  13.         id: roundRect
  14.         x: 40
  15.         y: 10
  16.         width: 200
  17.         height: 160
  18.         radius: 40
  19.         leftTopRound: lt.checked
  20.         rightTopRound: rt.checked
  21.         leftBottomRound: lb.checked
  22.         rightBottomRound: rb.checked
  23.         color: "#A0333666"      //半透明色
  24.     }
  25.     Grid {
  26.         x: 300
  27.         y: 10
  28.         columns: 2
  29.         spacing: 10
  30.         CheckBox {
  31.             id: lt
  32.             text: "LeftTop"
  33.             checked: true
  34.         }
  35.         CheckBox {
  36.             id: rt
  37.             text: "RightTop"
  38.             checked: true
  39.         }
  40.         CheckBox {
  41.             id: lb
  42.             text: "LeftBottom"
  43.             checked: true
  44.         }
  45.         CheckBox {
  46.             id: rb
  47.             text: "rightBottom"
  48.             checked: true
  49.         }
  50.     }
  51. }





QOpenGLWidget / QOpenGLWindow

有的同学学习过OpenGL这类图形渲染API,Qt为OpenGL提供了便利的窗口和上下文环境。
QOpenGLWidget用来在QWidget框架中集成OpenGL渲染,QOpenGLWindow用在Qml框架。
使用方法都是子类重载下面三个函数:
  1. void initializeGL();
  2. void paintGL();
  3. void resizeGL(int w, int h);



这里可以参考官方的示例:
QOpenGLWidget示例
QOpenGLWindow示例
Qt对OpenGL系列的函数都做了封装,一般使用QOpenGLFunctions就够了,QOpenGLFunctions是基于OpenGL ES 2.0 API的跨平台实现,删减了个别API。
相应的有一个未删减的OpenGLES2 的封装:QOpenGLFunctions_ES2。
当然为了兼容所有OpenGL版本,Qt分别封装了相应的类

有特殊版本需要的时候,可以把QOpenGLFunctions换成相应的类。
还有一个OpenGL ES3.0的封装, QOpenGLExtraFunctions,可以在支持OpenGL ES 3.0的设备上使用。
使用这些functions,一定要在有OpenGL上下文环境的地方,先调用一下initializeOpenGLFunctions。有些版本的init有返回值的,要注意判断并处理。


Qml SceneGraph

Qml基于GPU实现了一套渲染框架,这个框架就是SceneGraph。
SceneGraph提供了很多GPU渲染相关的功能,以方便进行自绘制,都是以QSG开头的类,如下图所示:

使用方式是在QQuickItem的子类中,重载updatePaintNode函数:
  1. QSGNode *TaoItem::updatePaintNode(QSGNode *node, UpdatePaintNodeData *)
  2.   {
  3.       QSGSimpleRectNode *n = static_cast<QSGSimpleRectNode *>(node);
  4.       if (!n) {
  5.           n = new QSGSimpleRectNode();
  6.           n->setColor(Qt::red);
  7.       }
  8.       n->setRect(boundingRect());
  9.       return n;
  10.   }



在使用Qml框架的程序中,使用这些QSG功能,将自定义渲染直接加入SceneGraph框架的渲染流程,无疑是性能最优的。
不过问题在于,这些QSG有点难以使用。需要有一定的OpenGL或DirectX相关图形学知识,并理解SceneGraph的节点交换机制,才能用好。
而懂OpenGL的人,有更好的选择,就是直接使用OpenGL的API。下面的QQuickFrameBufferObject就是一种途径。


Qml QQuickFrameBufferObject

QQuickFramebufferObject继承于QQuickItem(Qml中将它当作一个Item就可以了),用来在一个framebuffer object(FBO)上做渲染,
SceneGraph框架会将这个FBO渲染到屏幕上。
使用的方式是,实现一个QQuickFramebufferObject::Renderer类。
这个类里面始终是拥有OpenGL上下文环境的,内存也是被SceneGraph框架管理的,只要理解了渲染流程,用起来还是很方便的。
涛哥在Qml中集成 视频播放器 和 3D模型渲染的时候,就使用了这个FBO。
可以参考这两个例子:
Qml渲染3D模型
FFmpeg解码,Qml/OpenGL转码渲染


Qml ShaderEffect

学习过图形学的人,都应该听说过大名鼎鼎的Shadertoy
只要一点奇妙的Shader代码,就能渲染出各种酷炫的效果。
Qml中提供了ShaderEffect组件,就可以用来做ShaderToy那样的特效。
可以参考qyvlik的代码仓库:
qyvlik-ShaderToy.qml
以及我很久以前写的例子:
Tao-ShaderToy
360能量球
Qml中还有个神奇的ShaderEffectSource,可以用在普通Item的layer.effect中,
比如这个例子,就用ShaderEffectSource做了倒影特效:
倒影特效


QVulkanWindow

OpenGL的下一代,已经进化为vulkan了。
Qt 5.10开始,也提供了vulkan的支持。
涛哥水平有限,这次只提一下,就先不展开说了。


转载声明

文章采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可, 转载请注明出处, 谢谢合作 © 涛哥
文章出自涛哥的博客

涛哥是个Qml高手,著有《Qml组件化编程》《Qml特效》系列教程,见知乎专栏-Qt进阶之路:https://zhuanlan.zhihu.com/TaoQt
或微信公众号:Qt进阶之路
离线big_mouse

只看该作者 1楼 发表于: 2020-04-15
快速回复
限100 字节
 
上一个 下一个