在 Qt 2D 世界中,图形基本都由点、线、矩形、多边形、椭圆、圆、折线、曲线、文本、贴图等基本元素辅以着色而构成,而且 QPainter 类已经提供了这些元素的绘制方法,花点时间掌握这些方法,也就掌握了 Qt 2D图形控件的制作,余下的就是勤学多练、孰能生巧了。
一、点
在 Qt 中使用 QPoint 或 QPointF 类型表示一个点的坐标。
QPoint 和 QPointF 的区别在于:QPoint 使用整型表示 X 和 Y 坐标,而 QPointF 使用 qreal 浮点类型表示X和Y坐标。下面如无特殊说明,所介绍的方法都适用于两个类型。
QPoint 类型虽然提供了 isNull
方法来判断对象是否为空,在 X 和 Y 坐标均为 0 时,isNull 返回 true,其他情况返回 false。在使用 QPoint::isNull 方法时要留意这个情况。
可以使用 QPoint::manhattanLength 方法返回 x 和 y 坐标的绝对值之和(也叫曼哈顿距离),如:
1 | QPoint pt(-2, 4); |
1.1 两点距离
使用两点间的距离公式可以快速计算两点的距离:
1 |
|
1.2 drawPoint 与 drawPoints
drawPoint 用于绘制单个点:
1 | void drawPoint(const QPointF &position) |
drawPoints 用于一次性绘制多个点:
1 | void drawPoints(const QPointF *points, int pointCount) |
QPolygon
继承自 QVector<QPoint>
,而QPolygonF
继承自 QVector<QPointF>
。
二、折线
折线是由多个点相连组成的非闭合线条(相邻的两个点连接成直线,但首位不相连)。
1 | void drawPolyline(const QPointF *points, int pointCount) |
下面示例演示了折线的一种绘制方法,为了使线条更加圆润,我们还设置了线条的起落笔样式和相交点样式。
1 | void Qt2DSample::paintEvent(QPaintEvent* e) { |
三、多边形
多边形是在折线的基础上首尾相连而来,并且可以使用画刷对多边形的闭合区域进行填充。
仍然使用上面示例中定义的点来绘制多边形,设置 QPainter 画刷颜色为蓝色:
1 | void Qt2DSample::paintEvent(QPaintEvent* e) { |
四、线
两点(QPoint 或 QPointF)可以确定一条直线,Qt 提供了 QLine 和 QLineF 类型来描述线段。
使用 drawLine 方法可以绘制一条直线,使用 drawLines 方法则可以一次性绘制多条直线。
1 | void drawLine(const QLineF &line) |
五、椭圆与圆
5.1 椭圆
椭圆有长轴、短轴、焦点等概念,建议在学习绘制椭圆之前先了解椭圆的这些概念,可以参考之前的文章 回顾2D绘图的数学知识 中的“椭圆”章节。
Qt提供了 QPainter::drawEllipse 方法绘制椭圆,方法原型如下:
1 | void drawEllipse(const QRectF &rectangle) |
drawEllipse 方法虽然有5种重载形式,但总体来说,都是传递一个矩形区域给 drawEllipse方法,因为根据传入的矩形可以确定椭圆的长轴、短轴和焦点,其中长轴等于矩形的长,短轴等于矩形的高,2个焦点分别位于2个长半轴的中间。
下面示例先绘制了一个浅灰色的矩形(用于观察椭圆的绘制),然后在使用该矩形绘制椭圆。
1 | void Qt2DSample::paintEvent(QPaintEvent* e) { |
5.2 圆
圆是椭圆的一种特殊情况,当椭圆的长轴等于短轴时,所绘制出来的就是圆了。
1 | void Qt2DSample::paintEvent(QPaintEvent* e) { |
六、矩形
在 Qt中使用 QRect 和 QRectF 类型来表示矩形,在使用 QRect 时需要关注 QRect 的历史遗留问题,详见:玩转Qt 2D绘图之坐标系 的“QRect遗留问题”章节。
6.1 矩形的合法性
QRect和QRectF类型均提供了isEmpty、isNull、isValid方法,但 QRect 由于历史遗留问题,官方文档中对各个方法的定义看起来比较怪异,进行逻辑下面对其进行了逻辑运算,方便理解。
QRect
1 | isEmpty() == left() > right() || top() > bottom() |
QRectF
1 | isEmpty() == width() <= 0 || height() <= 0 |
从上述定义可以发现,当前矩形的宽或高为负数时,isNull 返回的却是 false。
6.2 drawRect 与 drawRects
使用 drawRect 方法绘制单个矩形,使用 drawRects 方法一次性绘制多个矩形。
1 | void drawRect(const QRectF &rectangle) |
下面是一个简单的使用示例:
1 | void Qt2DSample::paintEvent(QPaintEvent* e) { |
6.3 圆角矩形
上面绘制的矩形都不是圆角,在Qt中绘制圆角矩形有两种方式:
QPainter::drawRoundedRect,本节主要介绍这种方法,这种方法绘制的圆角矩形的4个角的曲度一样。
QPainter还提供了 drawRoundRect 方法也可以绘制圆角矩形,但该方法已经被标记为弃用,因此不建议继续使用。
QPainter::drawPath 通过路径的方式来绘制圆角矩形,这种方法绘制的圆角矩形的4个角的曲度可以不一样,该方法会在下面章节介绍。
1 | void QPainter::drawRoundedRect(const QRectF &rect, qreal xRadius, qreal yRadius, Qt::SizeMode mode = Qt::AbsoluteSize) |
下面示例为了演示圆角矩形和普通的直角矩形的不同,先绘制了一个浅灰色直角矩形,然后再相同区域绘制了一个红色圆角矩形。
1 | void Qt2DSample::paintEvent(QPaintEvent* e) { |
7.4 xRadius 与 yRadius
xRadius 与 yRadius 参数决定了圆角的曲度,drawRoundedRect 方法绘制的圆角矩形的四个角采用同一个曲度。那么 xRadius 与 yRadius 参数是如何决定圆角的曲度的呢?
xRadius 参数实际为椭圆的长半轴,yRadius 参数实际为椭圆的短半轴。
关于椭圆的相关知识,可以参考之前的文章 回顾2D绘图的数学知识 中的“椭圆”章节。
通过 xRadius 和 yRadius 参数确定了椭圆的长轴和短轴,那么在什么位置画椭圆呢?答案是:贴着四个顶角分别画 4 个椭圆来确定曲度。
下面代码在上面示例的基础上,分别在左上角和右下角位置画了2个深黄色的椭圆,可以看到椭圆与圆角是刚好重合的。
1 | void Qt2DSample::paintEvent(QPaintEvent* e) { |
七、椭圆上的一段线
7.1 弧线(Arc)
使用 drawArc 方法可以绘制一段弧线:
1 |
|
在前面介绍的椭圆绘制方法中,我们通过指定一个矩形区域就可以绘制一个椭圆,而 drawArc 方法绘制曲线的则是该椭圆上的某一段弧线。通过 startAngle 参数指定弧线的起始角度,spanAngle 参数指定弧线所跨越的角度。
需要注意:
在 QPainter 的绘图函数中指定角度(不是弧度)时,以正三点钟方向为 0 度,角度按逆时针方向增长,整个圆按 5760 度计算(即 16 * 360)。
下面的示例绘制一段红色的弧线,弧线从 30 度方向开始,弧线跨越了 120 度。
1 | void Qt2DSample::paintEvent(QPaintEvent* e) { |
实际绘制效果如下图所示:
从上图可以看到,弧线实际是椭圆上的某一段曲线。
7.2 饼状图(Pie)
使用 drawPie 可以绘制饼状图,饼状图是在上面弧线(Arc)的基础上,分别将弧线的首尾与中心点相连而来,因此 drawPie 方法的参数也与 drawArc 方法一样。
QPainter 在绘制饼状图时,会使用当前的画刷填充饼状图。
下面的示例绘制了一个具有红色轮廓、黄色填充的饼状图。
1 | void Qt2DSample::paintEvent(QPaintEvent* e) { |
7.3 和弦图(Chrod)
此处将 Chrod 按照英文直译成“和弦”可能不太准确
和弦图(Chrod)与饼状图(Pie)一样,也是在弧线的基础上变化而来,将弧线的首尾相接就可得到和弦图(Chrod)。
使用 drawChord 方法可以绘制和弦图,drawChord 方法的参数与 drawArc 一样。
1 | void Qt2DSample::paintEvent(QPaintEvent* e) { |
八、路径
前面介绍了线、矩形、椭圆、圆、弧线、饼状图等形状,路径则是由若干个这些形状(不限于这些形状)组成的一个集合,路径可以是闭合的,也可以不闭合。
在Qt中使用QPainterPath对象来定义一个路径,并使用如下方法向路径中添加形状:
- addEllipse 添加一个椭圆或圆到路径中
- addRect 添加一个矩形到路径中
- addRoundedRect 添加一个圆角矩形到路径中
- addText 将用指定字体绘制的文件所形成的闭合形状添加到路径中
- lineTo 添加一条直线到路径中
- arcTo 添加一段圆弧到路径中
arcTo 的原型如下,和 drawArc 方法类似,也需要指定起始角度和跨越角度,但与drawArc 方法不同的是,arcTo 方法指定的角度是以360度整圆计算的,因此不需要乘以 16。1
void QPainterPath::arcTo(const QRectF &rectangle, qreal startAngle, qreal sweepLength)
- cubicTo 添加一个三次贝塞尔曲线到路径中
- quadTo 添加一个二次贝塞尔曲线到路径中
在上面方法中,命名格式为“To”的方法,如 lineTo、arcTo、quadTo 等,都是基于“当前位置”来添加形状的,默认的当前位置为当前的*坐标原点 (0,0) 处,我们可以使用 QPainterPath::currentPosition
方法获取当前位置。
我们知道可以通过 QPainter::translate 等方法变换逻辑坐标,QPainterPath 的当前位置也会受逻辑坐标变换的影响。
在使用 lineTo 添加直线到路径中时,只有指定直线的结束点,会自动将当前位置与结束点相连,形成一条直线,并重设当前位置为该结束点。
在使用 arcTo、cubicTo、quadTo 添加曲线到到路径中时,会自动将当前位置与曲线起点相连,并重设当前位置为曲线结束点。
我们使用一个示例来说明当前位置和结束点的关系,下面示例先添加一条直线,然后添加一个曲线,最好再添加一条直线:
1 | // 示例:当前位置 |
实际效果如下图所示:
8.1 填充规则
路径可以是闭合的也可以是非闭合的,例如上面的当前位置示例中的路径是非闭合的,但如果为 QPainter 指定了画刷,QPainter 会自动使用一条直线连接该非闭合形状的首尾,使其闭合,进而使用画刷对其填充。
QPainterPath 有两种填充规则,可以通过QPainterPath::setFillRule
方法来设置填充规则:
Qt::OddEvenFill
奇偶填充规则,从一点向图形外引一条水平线,该线和图形的边线相交,如果交点的个数为奇数,则该点在图形中。
Qt::WindingFill
非零弯曲规则,从一点向图形外引一条水平线,该线与图形的边线相交。如果边线是顺时针绘制的,则记为1,如果边线是逆时针绘制的,则记为-1。最后将所有结果相加的和为0则该点在图形中。(矩形和椭圆都按顺时针绘制)
九、文本
文字有颜色、字体、样式等属性。
Qt 不仅可以绘制纯色的文本,还可以使用渐变色、图片等内容来填充文字,比如下面两种方式都可以用来绘制渐变色的文字:
- 先将文字转变成路径(参考
QPainterPath::addText
),然后使用渐变画刷填充该路径。 - 直接为 QPen 设置渐变画刷。
通过为 QPainter 设置 QFont 对象来修改字体,字体有字体族、字号、粗体、斜体、下划线、删除线等样式或效果:
1 | QFont font; |
使用 QPainter::drawText 方法绘制文本,该方法原型有很多种,但大多是通过下面2个原型进行重载而来的:
1 | void QPainter::drawText(const QRectF &rectangle, int flags, const QString &text, QRectF *boundingRect = nullptr) |
9.1 间距
间距分为两种情况:
- 每个字符的间距,如单个英文字母或单个汉字的间距,称为 Letter Spacing。
- 每个英文单词的间距,称为 Word Spacing。
9.1.1 单词间距
使用 QFont::setWordSpacing 和 QFont::wordSpacing 方法设置和获取单词的间距。
1 | void QFont::setWordSpacing(qreal spacing) |
当 setWordSpacing 参数大于0时,单词间距增加相应的像素;小于0时,间距减少相应的像素。
9.1.2 字符间距
使用 QFont::setLetterSpacing 和 QFont::letterSpacing 方法设置和获取字符的间距。
1 | void QFont::setLetterSpacing(QFont::SpacingType type, qreal spacing) |
字符间距支持百分比和绝对值两种设置方法,通过QFont::SpacingType枚举类型指定。
QFont::PercentageSpacing
百分比方式。值为100时表示不做任何改变,200表示间距扩大到原来的一倍,-200表示间距缩小到原来的一倍。
QFont::AbsoluteSpacing
绝对值方式,与设置单词间距的方式一样。
9.2 小型大写字母
小型大写字母(英语:small capitals,简称 small caps)是西文字体设计中的一种字符形式。这些字母的形状(字形)和大写字母相同但尺寸较小,比如在表示键盘快捷键的时候也常用小型大写字母。
在 Qt 中通过设置 QFont 的 Capitalization 样式为 SmallCaps 风格来使用小型大写字母样式,如:
1 | void Qt2DSample::paintEvent(QPaintEvent* e) { |
效果如下图所示(留意全小写的“is”和其他首字母大写的单词):
Capitalization 还支持设置如下风格:
- QFont::MixedCase 默认,不做更改
- QFont::AllUppercase 全部小写
- QFont::AllLowercase 全部大写
- QFont::Capitalize 首字母大写
9.3 计算绘制文本所需空间
有两种方式可以计算按指定字体绘制文本所需的空间。
方式一:使用 QPainter::drawText
使用如下函数原型:
1 | void QPainter::drawText(const QRectF &rectangle, int flags, const QString &text, QRectF *boundingRect = nullptr) |
将参数 rectangle 的 宽和高都设置为 0,函数会通过 boundingRect 参数返回绘制文本所需要的宽和高。
方式二:使用 QFontMetricsF
QFontMetrics 或 QFontMetricsF 可以按照指定字体来计算给定字符和字符串的宽和高。
这种方式的弊端在于:不能将绘制风格(如居中对齐、多行文本)带入其中进行计算,只能计算给定的单行字符串按照指定的字体显示所需的宽高。
1 | QFont font; |
十、贴图
QPainter 提供了 drawPixmap 和 drawImage 两个常用的贴图函数,drawPixmap 用于绘制 QPixmap 对象,而 drawImage 用于绘制 QImage 对象,QImage 与 QPixmap 间是可以相互转化,因此 drawPixmap 和 drawImage 方法的效果是一样的。
drawPixmap 方法在屏幕上绘制速度更快,而 drawImage 方法则在 QPrinter 和其他设备上绘制的更快。
需要注意:QPicture 对象用于记录绘制步骤,而 QPainter::drawPicture 方法用于重复 QPicture 对象所记录的步骤。
drawPixmap 和 drawImage 方法有多个重载的原型,其参数从左到右依次为:
1 | 目标区域 -> QPixmap/QImage -> 源区域 |
目标区域会应用当前 QWidget 的 devicePixelRatio,而源区域则与 devicePixelRatio 无关,具体参考 QPixmap使用要点
限于政策原因,在您看到该文章时,博客可能已经关闭了评论功能🥺
您可以通过在 blog-comment 项目中提交Issue来间接地发表评论🍀