软件是对现实世界的抽象,在设计软件接口时通常也会参考现实世界来进行设计。

在现实世界中,画家使用画笔、画刷这些工具在宣纸上作画,而在 Qt 中,画家是 QPainter 对象(画家的大脑则是 QPaintEngine),纸张可以是 QPaintDevice 的任何子类对象,如 QWidget、QImage、QPixmap、QGLPixelBuffer 等;在 QPainter 选好纸张后,就可以使用画笔来勾勒物体的轮廓了,Qt 中的画笔是 QPen 对象;画完轮廓后,可能还需要使用画刷来填充轮廓,Qt 中的画刷是 QBrush 对象。

一、QPaintDevice

本节介绍 QPaintDevice 的几个常用方法。

devicePixelRatio() 与 devicePixelRatioF()

用于获取DPI的缩放倍数,需要先在程序中开启DPI缩放特性:

1
2
3
4
5
6
7
8
9
int main(int argc, char *argv[])
{
QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
QGuiApplication::setAttribute(Qt::AA_UseHighDpiPixmaps);
#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough);
#endif
// .......
}

从 Qt 5.14 版本开始,Qt可以支持小数位的 DPI 缩放,如 1.25、1.75 等。如需获取小数位DPI值,需要先设置Qt::HighDpiScaleFactorRoundingPolicy::PassThrough策略,然后通过 devicePixelRatioF 方法获取。

width() 与 height()

用于获取 QPaintDevice 的宽和高,这个数值不会随着坐标系的改变而改变,而且也不会随着 DPI 的改变而改变。

widthMM() 与 heightMM()

用于获取 QPaintDevice 在屏幕上实际显示的宽和高,单位为毫米。

二、QPen

画笔有宽度(width/widthF)、颜料(brush)、线条样式(Style)、一条线起笔和落笔的样式(CapStyle)、两条线相交处的样式(JoinStyle)等属性。

2.1 宽度

1
2
void QPen::setWidth(int width)
void QPen::setWidthF(qreal width)

如果指定的宽度小于 0,将不会绘制任何内容;如果指定的宽度等于 0,将被视为 1 个像素宽度;如果指定的宽度大于等于 1,则按指定的宽度来绘制。

2.2 QPen的画刷

1
void QPen::setBrush(const QBrush &brush)

QPen 有画刷属性,QPainter 对象也画刷属性(下面章节会介绍),二者都有填充之意,QPen 的画刷是用来填充画笔所画的线条(可以理解为画笔的颜料)的,而QPainter 的画刷是用来填充画笔所画的轮廓区域的,如填充矩形里面的区域。

画刷可以是纯色、渐变色,也可以是图片。

2.3 线条样式

1
void QPen::setStyle(Qt::PenStyle style)

2.4 起落笔样式

1
void QPen::setCapStyle(Qt::PenCapStyle style)

2.5 拐点样式

1
void QPen::setJoinStyle(Qt::PenJoinStyle style)

2.6 实例

下面示例指定了画笔的如下属性:

  • 画笔的宽度为 70px
  • 使用红色画刷
  • 线条样式为通过若干空格分割的点线状
  • 线条起笔和落笔处为圆形
  • 线条拐点处也为圆形
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void Qt2DSample::paintEvent(QPaintEvent* e) {
QPainter painter(this);
painter.setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing);

QPen pen;
pen.setWidth(70);
pen.setBrush(Qt::red);
pen.setStyle(Qt::DotLine);
pen.setCapStyle(Qt::RoundCap);
pen.setJoinStyle(Qt::RoundJoin);

painter.setPen(pen);

painter.drawLine(50, 50, 600, 50);

painter.drawLine(600, 50, 600, 600);
}

实际绘制出来的效果如下图所示:

上面画笔使用红色作为颜料,也可以使用渐变色作为颜料,当然还可以使用图片作为颜料。

下面我们将颜料更改为一个红色的心形♥图片:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void Qt2DSample::paintEvent(QPaintEvent* e) {
QPainter painter(this);
painter.setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing);

QPen pen;
pen.setWidth(70);
pen.setBrush(QImage(":/heart.png")); // 仅修改了此行代码
~~~~~~~~~~~~~~
pen.setStyle(Qt::DotLine);
pen.setCapStyle(Qt::RoundCap);
pen.setJoinStyle(Qt::RoundJoin);

painter.setPen(pen);

painter.drawLine(50, 50, 600, 50);

painter.drawLine(600, 50, 600, 600);
}

绘制的效果如下:

三、QBrush

QPen 与 QPainter 都有画刷属性,这里的 QBrush 是指的 QPainter 的画刷。虽然与 QPen 的画刷都为 QBrush 类型,但二者是有区别的,QPen 的画刷是用来填充画笔所画线条(可以理解为画笔的颜料)的,而 QPainter 的画刷是用来填充画笔所画的轮廓所包围的区域,如填充矩形里面的区域。

如下面示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
void Qt2DSample::paintEvent(QPaintEvent* e) {
QPainter painter(this);
painter.setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing);

QPen pen;
pen.setWidth(6);
pen.setBrush(Qt::red);

painter.setPen(pen);
painter.setBrush(Qt::blue);

painter.drawRect(50, 50, 200, 200);
}

实际绘制的矩形如下图所示:

QBrush 有样式(Style)、颜色(Color)、渐变色(Gradient)、纹理图片(Texture)等属性。

3.1 样式

画刷的作用是填充区域,画刷的样式则是用来指定通过什么模式来填充区域的。

下图列出了 QBrush 支持的样式:

QBrush的初始样式是 Qt::NoBrush,即不做填充操作,该值显然是没有任何意义的,下面是一个错误的使用示例;

1
2
3
// 错误示例
QBrush brush;
brush.setColor(Qt::red); // 画刷仍然是 Qt::NoBrush样式,不会填充任何颜色

我们可以通过构造函数和 setStyle 方法来设置样式:

1
2
3
QBrush(Qt::BrushStyle style)

void setStyle(Qt::BrushStyle style)

当通过不同的方式构造 QBrush 对象时,QBrush 的样式也会自动被设置为不同的值:

1
2
3
4
5
6
7
8
// 样式自动被设置为渐变样式:Qt::LinearGradientPattern, Qt::RadialGradientPattern 或 Qt::ConicalGradientPattern
QBrush(const QGradient &gradient)

// 样式自动被设置为纹理样式Qt::TexturePattern
QBrush(const QImage &image)
QBrush(const QPixmap &pixmap)
QBrush(Qt::GlobalColor color, const QPixmap &pixmap)
QBrush(const QColor &color, const QPixmap &pixmap)

3.2 纹理图片

上面2.6节的实例中,在设置心形图片作为QPen的画刷时,使用的就是纹理图片。

下面汇总了设置 QBrush 纹理图片的几种方法:

1
2
3
4
5
6
7
8
9
// 通过构造函数
QBrush(const QImage &image)
QBrush(const QPixmap &pixmap)
QBrush(Qt::GlobalColor color, const QPixmap &pixmap)
QBrush(const QColor &color, const QPixmap &pixmap)

// 通过成员方法
void setTexture(const QPixmap &pixmap)
void setTextureImage(const QImage &image)

通过上面方式设置纹理图片后,QBrush 样式会被自动修改为 Qt::TexturePattern,因为纹理图片只能与 Qt::TexturePattern 样式配合使用,否则纹理图片将失效。

3.3 纯色

可以为画刷指定纯色或渐变色,纯色使用 QColor 对象表示。

常用的构造 QColor 对象的方式有如下几种:

1
2
3
4
QColor("#FFFFFF"); //  r g b
QColor("#AAFFFFFF"); // a r g b
QColor(255, 255, 255, 255); // r g b a
QColor(Qt::red); // Qt::GlobalColor 枚举值

3.4 渐变色

QBrush 只提供了一种方式来指定渐变色:

1
QBrush(const QGradient &gradient)

在Qt中,渐变色使用 QGradient 及其子类表示,目前Qt支持三种类型的渐变:

  • 线性渐变,对应 QLinearGradient 类。
  • 锥形渐变,对应 QConicalGradient 类。
  • 径向渐变,又分为简单径向渐变和扩展径向渐变,二者都对应 QRadialGradient 类。

下图是一个从左到右,由白到黑的渐变过程:

填充方向既然可以从左到右、从右到左的、从里到外,是可以360度旋转等等,不同的填充方向造就了不同类型的渐变,上述三种类型的渐变主要区别也在填充方向的不同。

3.4.1 线性渐变

QLinearGradient 构造函数如下:

1
2
QLinearGradient(qreal x1, qreal y1, qreal x2, qreal y2)
QLinearGradient(const QPointF &start, const QPointF &finalStop)

通过指定一个起点和一个终点就可以确定一条线段的长度及方向,这个方向就是填充方向,渐变范围基于该长度而确定(详见后面的“渐变范围”章节)。

线性渐变的方向可以是水平的,也可以是垂直的,还可以是斜着的,具体方向由线段与X轴的夹角来决定。

下面我们定义一个起点为(0,0)、终点为(0,400),垂直方向的线性渐变,然后使用该渐变填充位置在 (0,0),宽高为 200x400 的矩形(请注意矩形的位置和大小,这个与渐变的起止位置有关,后面会详细介绍):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void Qt2DSample::paintEvent(QPaintEvent* e) {
QPainter painter(this);
painter.setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing);

QLinearGradient linearGrad(QPointF(0, 0), QPointF(0, 400));
linearGrad.setColorAt(0, Qt::black);
linearGrad.setColorAt(1, Qt::white);

QBrush brush(linearGrad);

painter.setBrush(brush);

painter.drawRect(0, 0, 200, 400);
}

效果如下图所示:

由于是垂直方面的渐变,所以渐变色从上到下变化的,颜色起始于黑色,终止于白色。

渐变色的起止颜色由setColorAt方法设置,该方法原型如下:

1
void QGradient::setColorAt(qreal position, const QColor &color)

setColorAt 不仅接受颜色参数,还支持 postion 参数,position 参数的范围是[0,1]。线性渐变通过起止位置确定了一条带方向的线段,而 position 参数则用于确定线段上的一个具体位置,结合position和color参数就可以表示在某一位置的颜色值。

可以使用 setColorAt 方法指定多个在[0,1]之间的位置及该位置的颜色,Qt会自动在多个颜色之间插入过渡色,达到平滑过渡的效果。

现在我们在 0.3 位置插入一个骚气的红色,0.5位置插入黄色,其他都保持不变:

1
2
3
4
linearGrad.setColorAt(0, Qt::black);
linearGrad.setColorAt(0.3, Qt::red);
linearGrad.setColorAt(0.8, Qt::yellow);
linearGrad.setColorAt(1, Qt::white);

效果如下图所示:

3.4.2 渐变范围

在上面一节,我们强调了所绘制的矩形的位置/大小与渐变的起止位置有关,而且又强调了 position 参数所确定是线段上的具体位置。其根本原因就在于渐变是有位置和范围的。

对于线性渐变而言,渐变的范围是起始点和终止点间的线段,但颜色在垂直于该线段的方向却是无限扩充的,下面示例可以很好的说明这一特性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Qt2DSample窗口长宽为600x600
void Qt2DSample::paintEvent(QPaintEvent* e) {
QPainter painter(this);
painter.setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing);

QLinearGradient linearGrad(QPointF(150, 150), QPointF(450, 450));
linearGrad.setColorAt(0, Qt::yellow);
linearGrad.setColorAt(0.5, Qt::red);
linearGrad.setColorAt(1, Qt::white);

QBrush brush(linearGrad);
painter.setBrush(brush);
painter.drawRect(0, 0, 600, 600);

// 不使用画刷,绘制 左上角 -> 右下角 为线性渐变起止点的矩形
painter.setBrush(Qt::NoBrush);
painter.drawRect(150, 150, 300, 300);
}

效果如下图所示:

从上图可以看到,渐变色在垂直于渐变线段(中间矩形的左上角和右下角连成的斜线)方向无限扩展,但为什么在渐变范围边界以外还有颜色呢?如左上角的黄色和右下角的白色,这个与渐变的另一个属性特性有关 – 渐变传播,后面章节有详细介绍。

3.4.3 锥向渐变

QConicalGradient 类的构造函数如下:

1
2
QConicalGradient(qreal cx, qreal cy, qreal angle)
QConicalGradient(const QPointF &center, qreal angle)

锥向渐变通过中心点(center)和起始角度来(angle)来所连接的直线确定填充的初始方向,然后从该直线开始逆时针进行360度填充,而且每个角度的颜色都是无限向外传播的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Qt2DSample窗口长宽为200x200
void Qt2DSample::paintEvent(QPaintEvent* e) {
QPainter painter(this);
painter.setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing);

// 设定中心点刚好在窗口中心,起始角度为45度
QConicalGradient conicalGrad(QPointF(100, 100), 45);
conicalGrad.setColorAt(0, Qt::yellow);
conicalGrad.setColorAt(0.5, Qt::red);
conicalGrad.setColorAt(1, Qt::white);

QBrush brush(conicalGrad);
painter.setBrush(brush);

// 绘制的矩形区域为整个窗口大小
painter.drawRect(0, 0, 200, 200);
}

效果如下图所示,渐变色从45度开始渐变,并且每个角度都无限向外传播,填充满了整个矩形:

3.4.5 径向渐变

径向渐变分为简单径向渐变和扩展径向渐变,在介绍二者的不同之前,我们先看看QRadialGradient 类的构造函数可以大致分为以下三类:

1
2
3
4
5
6
7
8
// 指定中心点、半径、焦点位置、焦半径
QRadialGradient(const QPointF &center, qreal centerRadius, const QPointF &focalPoint, qreal focalRadius)

// 仅指定中心点、半径,此时焦点位置与中心点重合
QRadialGradient(const QPointF &center, qreal radius)

// 指定中心点、半径、焦点位置
QRadialGradient(const QPointF &center, qreal radius, const QPointF &focalPoint)
  • 中心点(center)和半径(centerRadius)组成了一个圆,记为圆A,径向渐变的范围只限于圆A以内,圆A以外的区域按照指定的渐变传播方式进行传播;

  • 焦点(focalPoint)为平面上的的一个点,简单径向渐变和扩展径向渐变对焦点的位置有不同的要求,这个区别在后面会介绍;

  • 焦半径(focalRadius)则是围绕焦点所作的另外一个圆(记为圆B)的半径。

扩展径向渐变是在简单径向渐变的基础上额外使用了 focalRadius(焦点半径)这个参数。

简单径向渐变

焦点和圆A圆弧上的每一个点都可以组成一条线段,如果按照1度取一个点,就有360个线段,简单径向渐变就是对这360个线段都进行线性渐变。

对于简单径向渐变而言,允许焦点位于圆弧上和圆外,如果焦点位于圆外,会自动将其退缩到圆弧上。

下面是一个简单径向渐变的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Qt2DSample窗口长宽为600x600
void Qt2DSample::paintEvent(QPaintEvent* e) {
QPainter painter(this);
painter.setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing);

// 焦点位于圆内右上角部分
QRadialGradient radialGrad(QPointF(300, 300), 200, QPointF(400, 200));
radialGrad.setColorAt(0, Qt::white);
radialGrad.setColorAt(1, Qt::black);

QBrush brush(radialGrad);
painter.setBrush(brush);

painter.drawRect(0, 0, 600, 600);
}

效果如下图所示,径向渐变的范围只限于圆内,圆外的区域默认采用PadSpread方式传播:

扩展径向渐变

扩展径向渐变使用焦点和焦半径又组成了一个圆B,与简单径向渐变类似,也是通过对圆A圆弧上的每个点的连线做线性渐变来实现,但这次不是焦点与圆A圆弧上的点进行连线,而是“圆B圆弧上的点”与“圆A圆弧上的点”的连线,对这些连线做线性渐变。

圆B始终填充起始颜色。

与简单径向渐变不同的是,圆B必须完全在圆A里面。

下面是一个扩展径向渐变的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Qt2DSample窗口长宽为600x600
void Qt2DSample::paintEvent(QPaintEvent* e) {
QPainter painter(this);
painter.setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing);

// 焦点位于圆内右上角部分,焦半径为30
QRadialGradient radialGrad(QPointF(300, 300), 200, QPointF(400, 200), 30);
radialGrad.setColorAt(0, Qt::white);
radialGrad.setColorAt(1, Qt::black);

QBrush brush(radialGrad);
painter.setBrush(brush);

painter.drawRect(0, 0, 600, 600);
}

效果如下图所示:

3.4.6 渐变传播

在Qt中通过 setSpread 方法设置渐变传播方式:

1
void QGradient::setSpread(QGradient::Spread method)

QGradient::Spread枚举定义了三种传播方式:

  • QGradient::PadSpread
  • QGradient::RepeatSpread
  • QGradient::ReflectSpread

默认为传播方式为 QGradient::PadSpread,而且由锥向渐变的行为可知,锥向渐变是没有渐变传播一说的,因此它始终都是360度渐变。

下图形象说明了三种传播方式的差异:

  • PadSpread 方式延续了起点与终点处各自的颜色,无限传播
  • RepeatSpread 方式不断重复渐变边界内的这一颜色的渐变模式
  • ReflectSpread 方式将渐变边界内的模式不断往外镜像反射

3.4.7 渐变主题

设计一个漂亮的渐变色并非易事,Qt提供了内置的渐变主题(Preset),这些主题通过 QGradient::Preset 枚举类型提供(该枚举从 Qt 5.12 开始引入)。

从Qt官方文档得知,QGradient::Preset预置渐变主题方案是基于https://webgradients.com/实现的。

QGradient 类支持通过QGradient::Preset 枚举类型直接构造:

1
QGradient::QGradient(QGradient::Preset preset)

下面是使用QGradient::Preset的示例:

1
2
3
4
5
6
7
8
9
10
11
void Qt2DSample::paintEvent(QPaintEvent* e) {
QPainter painter(this);
painter.setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing);

QGradient grad(QGradient::HealthyWater);
QBrush brush(grad);

painter.setBrush(brush);

painter.drawRect(0, 0, 600, 600);
}

3.4.8 网站推荐

下面几个网站也提供了漂亮的渐变色方案,可以用作参考:


限于政策原因,在您看到该文章时,博客可能已经关闭了评论功能🥺

您可以通过在 blog-comment 项目中提交Issue来间接地发表评论🍀