Qt/C++入门基础学习001-绘图基础
这一节介绍 Qt 的绘图基础知识,我们都知道,Qt 里绘图使用的是 QPainter,但是首先需要弄明白:在什么上绘图和在哪里绘图,然后才是怎么绘图,我们就围绕这几个问题来展开。
在什么上绘图
The QPaintDevice class is the base class of objects that can be painted on with QPainter.
A paint device is an abstraction of a two-dimensional space that can be drawn on using a QPainter. Its default coordinate system has its origin located at the top-left position. X increases to the right and Y increases downwards. The unit is one pixel.
The drawing capabilities of QPaintDevice are currently implemented by the QWidget, QImage, QPixmap, QGLPixelBuffer, QPicture, and QPrinter subclasses.
上面的内容来自于 Qt 的帮助文档,在 QPaintDevice 的子类里用 QPainter 绘图,最常见的就是在 QWidget, QPixmap, QPixture, QPrinter 上面绘图。
在哪里绘图
知道了可以在哪些类中绘图了,总不能在这些类的子类中随便写个函数就可以绘图了吧!这需要分情况,例如在 MainWidget 的构造函数里创建一个 QPixmap,并在它上面画图,然后设置为 QLabel 的 pixmap:
MainWidget::MainWidget(QWidget *parent)
: QWidget(parent), ui(new Ui::MainWidget) {
ui->setupUi(this);
// 创建 pixmap
pixmap = QPixmap(100, 100);
pixmap.fill(Qt::gray);
QPainter painter(&pixmap);
painter.drawRect(10, 10, 80, 80);
painter.drawText(20, 30, "Hello World");
// 使用 pixmap
ui->label->setPixmap(pixmap);
}
在非 widget 上绘图,如上面的 QPixmap,在什么地方都可以,但是在 QWidget 及其子类里绘图却没有这么自由,通常都是要在哪个 widget 上绘图,就需要在它的 paintEvent() 函数里绘图,即重写 paintEvent() 函数。
例如类 PandoraWidget 是 QWidget 的子类,要在它上面画一个矩形,它的 paintEvent() 函数如下:
void PandoraWidget::paintEvent(QPaintEvent *) {
QPainter painter(this); // this 是 PandoraWidget 的指针
painter.setPen(Qt::gray);
painter.setBrush(Qt::green);
painter.drawRect(10, 10, 50, 50);
}
考虑一个问题,PandoraWidget 上有一个叫 magicLabel 的 QLabel,打算在 magicLabel 上画一个矩形,在 PandoraWidget 的 paintEvent() 函数像下面这样用 magicLabel 构造 QPainter,然后绘图可以吗?
void PandoraWidget::paintEvent(QPaintEvent *) {
QPainter painter(ui->magicLabel); // 注意这里
painter.setPen(Qt::gray);
painter.setBrush(Qt::green);
painter.drawRect(10, 10, 50, 50);
}
运行程序,结果并没有在 magicLabel 上绘制出矩形,而且还输出了下面的错误:
QWidget::paintEngine: Should no longer be called
QPainter::begin: Paint device returned engine == 0, type: 1
QPainter::setPen: Painter not active
QPainter::setBrush: Painter not active
QPainter::drawRects: Painter not active
总是提示 Painter not active,上面提到过:想要在哪个 widget 上绘图,就需要在它的 paintEvent() 函数里绘图,这里的 paintEvent() 函数是 PandoraWidget 的,所以绘图到 PandoraWidget 上成功了,但 paintEvent() 函数不是 QLabel 的,所以企图绘图到 magicLabel 上没成功。
那是不是就是说,如果想在 magicLabel 上绘图,就必须新创建一个类例如叫 MagicLabel,并且继承自 QLabel,然后在它的 paintEvent() 里绘图?如果有 10 个子 widget,都想在上面画点啥,是不是每个 widget 都要对应创建一个类来实现绘图?我就是想画个圈而已,要创建这么多类也太麻烦了,真的想画个圈圈诅咒 Qt 啊。
莫急莫急,这里传大家本人秘藏多年的一绝技,就能在 PandoraWidget 的函数里给 magicLabel 绘图了:在事件过滤器 eventFilter() 中拦截 magicLabel 的 QEvent::Paint 事件,用 magicLabel 创建 QPainter,就可以在 magicLabel 上绘图了,上代码,否则估计有人要把我画在圈圈里了:
PandoraWidget::PandoraWidget(QWidget *parent)
: QWidget(parent), ui(new Ui::PandoraWidget) {
ui->setupUi(this);
ui->magicLabel->installEventFilter(this);
}
bool PandoraWidget::eventFilter(QObject *watched, QEvent *event) {
if (watched == ui->magicLabel && event->type() == QEvent::Paint) {
magicTime();
}
return QWidget::eventFilter(watched, event);
}
void PandoraWidget::magicTime() {
QPainter painter(ui->magicLabel);
painter.setPen(Qt::gray);
painter.setBrush(Qt::green);
painter.drawRect(10, 10, 50, 50);
}
怎么绘图
下图来自《C++ GUI Programming with Qt 4》,列出了 QPainter 常用的画图方法,都是以 draw 开头,非常直观的列出了绘图函数和绘制出来的图形:

下面具体的介绍这些函数的使用,它们中很多都有重载的函数,这里只使用其中的一种,其它的用法都差不多,就不一一介绍,需要时查看帮助文档就可以了。
坐标系
数学中使用的坐标系是笛卡尔坐标系,X 轴正向向右,Y 轴正向向上。但是,QPainter 也有自己的坐标系,和笛卡尔坐标系有点不一样,原点在 widget 的左上角而不是正中心,X 轴正向向右,Y 轴正向向下。注意: 每个 widget 都有自己独立的坐标系。

画线 - drawLine()
给定 2 个点,使用 drawLine() 画一条线。
void MainWidget::paintEvent(QPaintEvent *) {
QPainter painter(this);
painter.drawLine(30, 30, 150, 150);
}
drawLine() 有什么用?例如可以用来画网格线:

void GridWidget::paintEvent(QPaintEvent *) {
QPainter painter(this);
painter.translate(30, 30);
int w = 300;
int h = 210;
int gap = 30;
// 画水平线
for (int y = 0; y <= h; y += gap) {
painter.drawLine(0, y, w, y);
}
// 画垂直线
for (int x = 0; x <= w; x += gap) {
painter.drawLine(x, 0, x, h);
}
}
画多线段 - drawLines()
给定 N 个点,第 1 和第 2 个点连成线,第 3 和第 4 个点连成线,……,N 个点练成 (N+1)/2 条线,如果 N 是奇数,第 N 个点和 (0,0) 连成线。
void MultipleLinesWidget::paintEvent(QPaintEvent *) {
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
painter.translate(20, 20);
static const QPointF points[4] = {
QPointF(0.0, 100.0),
QPointF(20.0, 0.0),
QPointF(100.0, 0.0),
QPointF(120.0, 100.0)
};
painter.drawLines(points, 2); // 4 个点连成 2 条线
}
画折线- drawPolyline()
给定 N 个点,第 1 和第 2 个点连成线,第 2 和第 3 个点连成线,……,第 N-1 和第 N 个点连成线,N 个点共连成 N-1 条线。
void PolylineWidget::paintEvent(QPaintEvent *) {
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
painter.translate(20, 20);
static const QPointF points[4] = {
QPointF(0.0, 100.0),
QPointF(20.0, 0.0),
QPointF(100.0, 0.0),
QPointF(120.0, 100.0)
};
painter.drawPolyline(points, 4);
}
画多边形 - drawPolygon()
给定 N 个点,第 1 和第 2 个点连成线,第 2 和第 3 个点连成线,……,第 N-1 和第 N 个点连成线,第 N 个点和第 1 个点连接成线形成一个封闭的多边形。
drawPolygon() 和 drawPolyline() 很像,但是 drawPolygon() 画的是一个封闭的区域,可以填充颜色,而 drawPolyline() 画的是一些线段,即使它们连成一个封闭的区域也不能填充颜色。
void PolygonWidget::paintEvent(QPaintEvent *) {
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
painter.translate(20, 20);
static const QPointF points[4] = {
QPointF(0.0, 100.0),
QPointF(20.0, 0.0),
QPointF(100.0, 0.0),
QPointF(120.0, 100.0)
};
painter.setBrush(Qt::yellow);
painter.drawPolygon(points, 4);
}
可以用 drawPolygon() 来画圆,其实本没有圆,正多边形的边多了,便成了圆,这正是计算机里绘制曲线的原理,插值逼近,在曲线上取 N 个点,点之间用线段连接起来,当 N 越大时,连接出来的图形就越平滑,越接近曲线。

void PolygonCircleWidget::paintEvent(QPaintEvent *) {
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing); // 启用抗锯齿效果
painter.translate(width() / 2, height() / 2); // 把坐标原点移动到 widget 的中心
painter.setBrush(Qt::lightGray);
const int COUNT = 10; // 边数,越大就越像圆
const int RADIUS = 100; // 圆的半径
QPointF points[COUNT]; // 顶点数组
for (int i = 0; i < COUNT; ++i) {
float radians = 2 * M_PI / COUNT * i; // M_PI 是 QtMath 里定义的,就是 PI
float x = RADIUS * qCos(radians);
float y = RADIUS * qSin(radians);
points[i] = QPointF(x, y);
}
painter.drawPolygon(points, COUNT);
}
为了介绍方便,数组 points 是在 paintEvent() 里创建的,每次调用 paintEvent() 时都会重新生成一次 points,实际项目里可不能这么做,因为 paintEvent() 会被多次的调用,每次调用都会生成 points。数组 points 只有在必要的时候才重新生成,否则就是浪费计算资源,所以可以放到构造函数里,或者点击按钮改变 COUNT 的值后在对应的槽函数里重新生成 points,然后调用 update() 函数刷新界面。
画矩形 - drawRect()
给定矩形左上角的坐标和矩形的长、宽就可以绘制矩形了。
void MainWidget::paintEvent(QPaintEvent *) {
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
painter.translate(30, 30);
int x = 0;
int y = 0;
int w = 100;
int h = 100;
painter.drawRect(x, y, w, h);
}
画圆角矩形- drawRoundRect() & drawRoundedRect()
绘制圆角矩形有 2 个方法:drawRoundRect() 和 drawRoundedRect(),需要给定圆角矩形左上角的坐标、长、宽、圆角的半径。
当 drawRoundedRect() 中第 7 个参数 Qt::SizeMode 为 Qt::RelativeSize 时,表示圆角半径的单位是百分比,取值范围是 [0, 100],此时 drawRoundedRect() 等价于 drawRoundRect(),其实底层是用这个百分比和对应边长的一半相乘得到圆角的半径(单位是像素)。Qt::SizeMode 为 Qt::AbsoluteSize 时,表示圆角半径的单位是像素。
有意思的是,在 QSS 中圆角半径大于对应边长的一半,圆角效果就没了,但是使用 drawRoundedRect() 时,圆角的半径大于对应边长的一半时,圆角效果仍然有效,个人认为这个是 QSS 的 bug,但是已经存在很久了。
下面使用不同的参数绘制了 3 个圆角矩形,便于比较他们之间的异同:

void RoundRectWidget::paintEvent(QPaintEvent *) {
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
painter.setBrush(Qt::lightGray);
painter.translate(30, 30);
painter.drawRoundRect(0, 0, 100, 100, 50, 50); // 50%, 50%
painter.drawRoundedRect(130, 0, 100, 100, 50, 50, Qt::AbsoluteSize); // 50px, 50px
painter.drawRoundedRect(260, 0, 100, 100, 100, 100, Qt::RelativeSize); // 100%, 100%
}
画圆、椭圆 - drawEllipse()
给定椭圆的包围矩形(bounding rectangle),使用 drawEllipse() 绘制椭圆。圆是特殊的椭圆,椭圆有两个焦点,这两个焦点合为一个的时候就是一个正圆了,当包围矩形是正方形时,drawEllipse() 绘制的就是圆。
当然,画圆的方法很多,上面我们就使用了 drawPolygon(),drawRounedRect() 的方法画圆,不过从语义上来说,用 drawEllipse() 来画圆显得更适合一些。
void EllipseWidget::paintEvent(QPaintEvent *) {
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
painter.translate(30, 30);
painter.drawRect(0, 0, 200, 100); // 椭圆的包围矩形
painter.setBrush(Qt::lightGray);
painter.drawEllipse(0, 0, 200, 100); // 椭圆
painter.drawEllipse(230, 0, 100, 100); // 圆
}
画弧、弦、饼图 - drawArc()、drawChord()、drawPie()
画弧使用 drawArc()
画弦使用 drawChord()
画饼图用 drawPie()
把这三个函数放在一起介绍,因为它们的参数都一样,而且 arc, chord, pie 外形也有很多相似之处:
void QPainter::drawArc(const QRectF & rectangle, int startAngle, int spanAngle)
void QPainter::drawPie(const QRectF & rectangle, int startAngle, int spanAngle)
void QPainter::drawChord(const QRectF & rectangle, int startAngle, int spanAngle)
- rectangle: 包围矩形
- startAngle: 开始的角度,单位是十六分之一度,如果要从 45 度开始画,则 startAngle 为 45 * 16
- spanAngle: 覆盖的角度,单位是十六分之一度
- 绘制圆心为包围矩形的正中心,0 度在圆心的 X 轴正方向上
- 角度的正方向为逆时针方向
下面程序的结果如图:

void ArcChordPieWidget::paintEvent(QPaintEvent *) {
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
static int startAngle = 45 * 16; // 开始角度是 45 度
static int spanAngle = 130 * 16; // 覆盖角度为 130 度
static QRectF boundingRect(0, 0, 150, 150); // 包围矩形
painter.translate(30, 30);
painter.setBrush(Qt::NoBrush);
painter.drawRect(boundingRect); // 绘制包围矩形
painter.setBrush(Qt::lightGray);
painter.drawArc(boundingRect, startAngle, spanAngle); // 画弧
painter.translate(180, 0);
painter.setBrush(Qt::NoBrush);
painter.drawRect(boundingRect); // 绘制包围矩形
painter.setBrush(Qt::lightGray);
painter.drawChord(boundingRect, startAngle, spanAngle); // 画弦
painter.translate(180, 0);
painter.setBrush(Qt::NoBrush);
painter.drawRect(boundingRect); // 绘制包围矩形
painter.setBrush(Qt::lightGray);
painter.drawPie(boundingRect, startAngle, spanAngle); // 画饼图
}
修改 startAngle 和 spanAngle 为负值看看是什么效果。
绘制 QPixmap - drawPixmap()
void MainWidget::paintEvent(QPaintEvent *) {
QPainter painter(this);
QPixmap pixmap(":/img/Butterfly.png"); // 从资源文件读取 pixmap
painter.drawPixmap(0, 0, pixmap); // 绘制 pixmap
}
绘制 QImage - drawImage()
void MainWidget::paintEvent(QPaintEvent *) {
QPainter painter(this);
QImage image(":/img/Butterfly.png"); // 从资源文件读取 image
painter.drawImage(0, 0, image); // 绘制 image
}
- 国内开源:https://gitee.com/feiyangqingyun
- 国际开源:https://github.com/feiyangqingyun
- 项目大全:https://qtchina.blog.csdn.net/article/details/97565652
- 联系方式:QQ 517216493 / 微信 feiyangqingyun
Qt/C++入门基础学习001-绘图基础的更多相关文章
- matplotlib学习之绘图基础
matplotlib:http://www.cnblogs.com/jasonhaven/p/7609059.html 1.基本图形 散点图:显示两组数据的值,每个点的坐标位置由变量的值决定,头一组不 ...
- Python学习(001)--计算机基础
操作系统发展历史 操作系统并不是与计算机硬件一起诞生的,它是在人们使用计算机的过程中,为了满足两大需求:提高资源利用率.增强计算机系统性能,伴随着计算机技术本身及其应用的日益发展,而逐步地形成和完善起 ...
- 【java基础学习001】概述
001.1 一个简单的Java程序 public class hello { public static void main(String[] args) { System.out.printl ...
- 2019-9-9:渗透测试,基础学习,windows基础命令,笔记
windows系统基础命令学习 1,命令提示符界面进入方法 方法一: 某分区按住shift,右键单击选择在此处打开windows powershell,进入之后输入cmd 方法二:标题栏输入 方法三: ...
- <java基础学习>02JAVA的基础组成
Java的基础组成 1 关键字 (被赋予了特殊含义的单词) 2 标识符 3 注释 4 常量和变量 5 运算符 6 语句 7 函数 8 数组 关键字 class Demo{ public static ...
- java基础学习05(面向对象基础02)
面向对象基础02 实现的目标 1.String类的使用2.掌握this关键字的使用3.掌握static关键字的使用4.了解内部类 String类 实例化String对象一个字符串就是一个String类 ...
- java基础学习05(面向对象基础01--类实例分析)
面向对象基础01(类实例分析) 实现的目标 1.如何分析一个类(类的基本分析思路) 分析的思路 1.根据要求写出类所包含的属性2.所有的属性都必须进行封装(private)3.封装之后的属性通过set ...
- java基础学习05(面向对象基础01)
面向对象基础01 1.理解面向对象的概念 2.掌握类与对象的概念3.掌握类的封装性4.掌握类构造方法的使用 实现的目标 1.类与对象的关系.定义.使用 2.对象的创建格式,可以创建多个对象3.对象的内 ...
- C++基础学习一(基础之基础)
开篇:做了这么多年的软件,第一次使用博客的方式记录学习过程,之前都是笔记本(都有一摞了),因为之前一直从事的都是.NET的开发工作,对C++知之甚少,但一直想了解C++这门鼻祖级的语言,现在终于下定决 ...
- JAVA多线程基础学习一:基础知识
我们知道多线程是Java编程中重要的一块内容,也是面试重点覆盖区域,所以学好多线程对我们来说极其重要,下面跟我一起开启本次的学习之旅吧. 一.线程基本概念 1 线程:进程中负责程序执行的执行单元(执行 ...
随机推荐
- day10-变量、常量、作用域及命名规范
变量 变量是什么?就是可以变化的量! Java是一种强类型语言,每个变量都必须声明其类型. Java变量是程序中最基本的存储单元,其要素包括变量名.变量类型和作用域. type varName [ ...
- 多平台文章同步浏览器插件 – ArticleSync
ArticleSync - 多平台文章同步插件 ArticleSync 是一个浏览器扩展,帮助用户轻松将文章同步发布到多个社交平台.支持将文章从本地草稿发布到各大平台,如知乎.Bilibili 等.它 ...
- 我在大厂做 CR——如何体系化防控空指针异常
大家好,我是木宛哥,今天和大家分享下--代码 CR 时针对恼人的空指针异常(NullPointerException)如何做到体系化去防控: 什么是空指针异常 从内存角度看,对象的实例化需要在堆内存中 ...
- C语言(从入门到入土)
1.C语言整体上需要记住的 总体上必须清楚的: 1)程序结构是三种: 顺序结构 , 循环结构 (三个循环结构), 选择结构 (if 和 switch) 2)读程序都要从main()入口, 然后从最上面 ...
- php如何快速入门
PHP交流群 656679284 为PHP广大爱好者提供技术交流,有问必答,相互学习相互进步! 学习教程 学习前期,首要的WEB前端基础知识,比如html5/css3/java/jquery有个简 ...
- 量子线路设计:减少CNOT和T门的意义。
在量子线路的设计中,我们往往希望减少线路中的CNOT门和T门的数目,原因如下: 一般文献宣称减少T门的数量是为了更高效地执行容错量子计算(fault-tolerant quantum computat ...
- 2-5 C++ 类型别名与自动类型
目录 2.5.1 类型别名(Type Alias) typedef using 2.5.2 auto类型说明符 基本说明 注意点 2.5.3 decltype类型说明符 基本说明 注意点 2.5.1 ...
- 基于surging 的木舟平台如何通过Tcp或者UDP网络组件接入设备
一.概述 上篇文章介绍了木舟通过HTTP网络组件接入设备,那么此篇文章将介绍如何利用Tcp或者UDP网络组件接入设备. 木舟 (Kayak) 是什么? 木舟(Kayak)是基于.NET6.0软件环境下 ...
- 批量归一化(BN, Batch Normalization)
现在的神经网络通常都特别深,在输出层向输入层传播导数的过程中,梯度很容易被激活函数或是权重以指数级的规模缩小或放大,从而产生"梯度消失"或"梯度爆炸"的现象,造 ...
- VUE 前端读取excel表格内容
<el-upload class="upload-demo" :action="''" :show-file-list="false" ...