使用Qt进行2D绘图

  • Post author:
  • Post category:其他


Qt中提供了强大的2D绘图功能,可以使用相同的API在屏幕和绘图设备上进行绘制。Qt中的绘图功能主要是基于QPainter,QPaintDevice和QPaintEngine这3个类。

其中,QPainter用来执行绘图操作。

QPaintDevice提供绘图设备,它是一个抽象的二维空间,可以使用QPainter在其上进行绘制;QPaintDevice类是所有可以进行绘制的对象的基类,它的子类主要有QWidget,QPixmap,QImage,QPrinter和QOpenGLPainterDevice等。

QPaintEngine则提供了一些接口,用于QPainter和QPaintDevice内部,使得QPainter可以在不同的设备上进行绘制。除了要创建自定义的绘图设备外,一般编程中不需要使用该类。

QPainter,QPaintEngine和QPaintDevice三者的关系图:

在这里插入图片描述



基本绘制和填充

在Qt绘图系统中,由QPainter完成具体的绘制操作,QPainter可以绘制一切想要的图形。从最简单的直线到其他任何复杂的图形,还可以用于绘制文本和图片。QPainter可以在继承自QPaintDevice类的任何对象进行绘制操作。

QPainter一般在一个部件重绘事件的处理函数paintEvent()中进行绘制操作。首先创建QPainter对象,图形绘制完成后再销毁QPainter对象。



基本图形的绘制和填充

QPainter中提供了一些便捷的函数来绘制常用的图形,还可以设置绘制线条,边框的画笔以及进行填充的画刷。

新建Qt Widgets应用,项目名称mydrawing,基类选择QWidget,类名Widget。

项目创建完成后,在widget.h文件中声明重绘事件处理函数。

protected:
    void paintEvent(QPaintEvent *event);

然后到widget.cpp文件中添加头文件。



1. 绘制图形

先在widget.cpp中对paintEvent()函数进行如下定义:

void Widget::paintEvent(QPaintEvent *event){
    QPainter painter(this);		// 使用QPainter::QPainter(QPaintDevice * device)构造函数创建QPainter实例对象,这里的this指定了绘图设备,即在Widget部件上进行绘制
    painter.drawLine(QPoint(0, 0), QPoint(100, 100));
}

使用QPainter::QPainter(QPaintDevice * device)构造函数创建的QPainter实例对象会立即开始在设备上进行绘制,自动调用begin()函数,然后在QPainter的析构函数中调用end()函数结束绘制。

如果在构建QPainter对象时不想指定绘图设备,那么可以使用不带参数的构造函数,然后使用QPainter::begin(QPaintDevice * device)开始在指定设备上进行绘制操作,等绘制完成后再调用end()函数结束绘制。

这样一来,上述代码实际上等价于:

void Widget::paintEvent(QPaintEvent *event){
    QPainter painter;		// 使用不带参的构造函数创建QPainter实例对象
	painter.begin(this);
    painter.drawLine(QPoint(0, 0), QPoint(100, 100));
    painter.end();
}

在这里插入图片描述

除了绘制简单的线条外,QPainter还提供了一些用于绘制其他常用图形的函数,其中最常用的几个见下表:

在这里插入图片描述



2. 使用画笔

在上面的paintEvent()函数中继续添加代码,使得其变成下面这样:

void Widget::paintEvent(QPaintEvent *event){
    QPainter painter(this);
    painter.drawLine(QPoint(0, 0), QPoint(100, 100));

    QPen pen(Qt::green, 5, Qt::DotLine, Qt::RoundCap, Qt::RoundJoin);
    // QPen类的构造函数为QPen::QPen(QBrush, Width, PenStyle, PenCapStyle, PenJoinStyle)
    painter.setPen(pen);            // 使用画笔
    QRectF rectangle(70.0, 40.0, 80.0, 60.0);	// (x, y, width, height)
    int startAngle = 30 * 16;       // 圆弧的起始角度
    int spanAngle = 120 * 16;       // 圆弧的张开角度
    painter.drawArc(rectangle, startAngle, spanAngle);
}

在这里插入图片描述

上面使用的用于绘制圆弧的函数的原型是:

// 弧线所在矩形,起始角度和张开角度
QPainter::drawArc(const QRectF & rectangle, int startAngle, int spanAngle);

在这里插入图片描述

这里使用的角度的数值为实际度数乘以16,在时钟表盘中,0度指向3时的位置。如果角度数值为正,则表示逆时针旋转;角度数值为负,则表示顺时针旋转。整个一圈的数值为5760(360 × 16)。

QPen类为QPainter提供了画笔来绘制线条和形状的轮廓,上例中使用的构造函数为:

QPen(const QBrush & brush,				// 画笔使用的画刷(为画笔提供颜色)
  qreal width,							// 画笔使用的线宽(默认为0)
  Qt::PenStyle style = Qt::SolidLine,	// 画笔线条样式(可选值有实线,点线等)
  Qt::PenCapStyle cap = Qt::SquareCap,	// 画笔线条的端点样式(square, flat和round)
  Qt::PenJoinStyle = Qt::BevelJoin		// 画笔线条的连接样式(bevelJoin, miterJoin和roundJoin)
 )

除了可以一次性在构造函数中将这些样式属性指定完毕外,也可以分别使用setBrush(),setWidth(),setStyle(),setCapStyle()和setJoinStyle()等函数进行设置。

在这里插入图片描述

下面看看不同画笔线条的样式的展示效果:

在这里插入图片描述

不同画笔线条的连接样式的展示效果:

在这里插入图片描述

画笔连接样式示意图:

在这里插入图片描述



3. 使用画刷

在paintEvent()函数中继续添加如下代码,使得这个函数的定义变成下面这样:

void Widget::paintEvent(QPaintEvent *event){
    QPainter painter(this);
    painter.drawLine(QPoint(0, 0), QPoint(100, 100));

    QPen pen(Qt::green, 5, Qt::DotLine, Qt::RoundCap, Qt::RoundJoin);
    // QPen类的构造函数为QPen::QPen(QBrush, Width, PenStyle, PenCapStyle, PenJoinStyle)
    painter.setPen(pen);            // 使用画笔
    QRectF rectangle(70.0, 40.0, 80.0, 60.0);
    int startAngle = 30 * 16;       // 圆弧的起始角度
    int spanAngle = 120 * 16;       // 圆弧的张开角度
    painter.drawArc(rectangle, startAngle, spanAngle);

    // 重新设置画笔宽度和线条样式
    pen.setWidth(2);
    pen.setStyle(Qt::SolidLine);
    painter.setPen(pen);
    painter.drawRect(160, 20, 50, 40);

    /* 使用画刷 */
    QBrush brush(QColor(0, 0, 255), Qt::Dense4Pattern);     // (画刷颜色,画刷样式)
    painter.setBrush(brush);        // 重新设置painter实例使用的画刷样式
    painter.drawEllipse(220, 20, 50, 50);

    brush.setTexture(QPixmap("../../mydrawing/linux.png")); // 为画刷设置纹理(这样设置后画刷的样式就自动转换为Qt::TexturePattern)
    painter.setBrush(brush);
    static const QPointF points[4] = {
        QPointF(270.0, 80.0),
        QPointF(290.0, 10.0),
        QPointF(350.0, 30.0),
        QPointF(390.0, 70.0)
    };
    // 绘制多(4)边形
    painter.drawPolygon(points, 4); // (指定各个顶点坐标, 顶点数量)

    painter.fillRect(QRect(10, 100, 150, 20), QBrush(Qt::darkYellow));  // 使用画刷绘制填充矩形
    painter.eraseRect(QRect(50, 0, 50, 120));       // 檫除一个矩形区域
}

在这里插入图片描述

QBrush类提供了画刷来对图形进行填充,一个画刷要使用颜色和画刷样式来定义。

在Qt中使用的颜色一般都由QColor类来表示,它支持RGB,HSV,CMYK等颜色模型,此外,QColor还支持alpha通道的轮廓和填充。QColor类定义的颜色与平台,设备无关(颜色使用QColormap类向硬件进行映射)。

Qt中还提供了20种预定义的颜色,比如以前经常使用的Qt::red等,这可以在Qt Creator的帮助中通过Qt::GlobalColor关键字来查看。

画刷的填充模式使用画刷样式Qt::BrushStyle来定义,包含基本模式填充,渐变填充和纹理填充。Qt提供的画刷样式的展示图如下:

在这里插入图片描述



渐变填充

QGradient类就是用来和QBrush一起来指定渐变填充的。Qt现在支持三种渐变填充:


  • 线性渐变(linear gradient)

    : 在开始点和结束点之间插入颜色;

  • 径向渐变(radial gradient)

    : 在焦点和环绕它的圆环间插入颜色;

  • 锥形渐变(Conical)

    : 在圆心周围插入颜色。

这三种渐变分别由QLinearGradient,QRadialGradient和QConicalGradient这三个QGradient类的子类来表示。

还是在mydrawing项目中,在前面的paintEvent函数中继续添加如下代码:

// 创建一个线性渐变
QLinearGradient linearGradient(QPointF(40, 190), QPointF(70, 190));
linearGradient.setColorAt(0, Qt::yellow);
linearGradient.setColorAt(0.5, Qt::red);
linearGradient.setColorAt(1, Qt::green);
linearGradient.setSpread(QGradient::RepeatSpread);
painter.setBrush(linearGradient);
painter.drawRect(10, 170, 90, 40);

// 创建一个径向渐变
QRadialGradient.radialGradient(QPointF(200, 190), 50, QPointF(275, 200));
radialGradient.setColorAt(0, QColor(255, 255, 100, 150))
radialGradient.setColorAt(1, QColor(0, 0, 0, 50));
painter.setBrush(radialGradient);
painter.drawEllipse(QPointF(200, 190), 50, 50);

// 创建一个锥形渐变
QConicalGradient conicalGradient(QPointF(350, 190), 60);
conicalGradient.setColorAt(0.2, Qt::cyan);
conicalGradient.setColorAt(0.9, Qt::black);
painter.setBrush(conicalGradient);
painter.drawEllipse(QPointF(350, 190), 50, 50);

// 使用线性渐变来绘制直线和文字(如果为画笔设置了渐变颜色,那么可以绘制出渐变颜色的线条和轮廓,还可以绘制出渐变颜色的文字)
painter.setPen(QPen(linearGradient, 2));
painter.drawLine(0, 280, 100, 280);
painter.drawText(150, 280, tr("helloQt!"));

在这里插入图片描述



1. 线性渐变

线性渐变

QLinearGradient::QLinarGradient(const QPointF & start, const QPointF & finalStop)

需要为其指定起始点和结束点。

然后在起始点和结束点之间进行等距划分,起始点的位置为0.0,结束点的位置为1.0,它们中间的颜色段可以使用

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

函数在指定的位置position插入指定的颜色color。

另外,还可以使用setSpread()函数来设置填充的扩散方式,即在指定区域以外的区域怎样进行填充。扩散方式由QGradient::Spread枚举类型定义,它一共有3个可取值,分别是:


  • QGradient::PadSpread

    ,使用最接近的颜色进行填充,这是默认值;

  • QGradient::RepeatSpread

    ,在渐变区域以外的区域重复渐变;

  • QGradient::ReflectSpread

    : 在渐变区域以外将反射渐变形式。

这三种填充的扩散方式的展示图如下:

在这里插入图片描述



2. 径向渐变

径向渐变

QRadialGradient::QRadialGradient(const QPointF & center, qreal radius, const QPointF & finalPoint)

需要指定圆心center和半径radius,这样就可以确定一个圆,然后再指定一个焦点focalPoint。

在径向渐变中,焦点的位置为0,圆环的位置为1。然后在焦点和圆环间插入颜色,径向渐变也可以使用setSpread()函数设置渐变区域以外区域的扩散方式。同样,径向渐变的三种扩散方式和线性渐变的一样,它们的效果图如下:

在这里插入图片描述



3. 锥形渐变

锥形渐变

QConicalGradient::QconicalGradient(const QPointF & center, qreal angle)

需要指定中心点center和一个角度angle(值在0 ~ 360)。然后在沿逆时针从指定的角度开始环绕中心点插入颜色。对于锥形渐变而言,这里给定的角度沿逆时针方向开始的位置为0,旋转一圈为1。另外,setSpread()函数对于锥形渐变没有效果。



坐标系统

Qt的坐标系统是由QPainter类控制的,而QPainter是在绘图设备上,而QPainter是在绘图设备上进行绘制的。

一个绘图设备的默认坐标系统中,原点(0, 0)在其左上角,x坐标向右增长,y坐标向下增长。在基于像素的设备上,默认的单位是一个像素,而在打印机上默认的单位是一个点(1/72 in)。

QPainter的逻辑坐标与绘图设备的物理坐标之间的映射由QPainter的变换矩阵,视口和窗口处理。逻辑坐标和。物理坐标默认是一致的。QPainter也支持坐标变换(比如旋转和缩放)。



抗锯齿渲染



1. 逻辑表示

一个图形的大小(宽和高)总与其数学模型相对应,下图演示了忽略其渲染时使用的画笔的宽度的样子:

在这里插入图片描述



2. 抗锯齿绘图

抗锯齿(Anti-aliased)又称为反锯齿或者反走样,就是对图像的边缘进行平滑处理,使其看起来更加柔和流畅的一种技术。

QPainter进行绘制时可以使用QPainter::RenderHint渲染提示来指定是否开启抗锯齿功能,渲染提示的可取值见下表:

在这里插入图片描述

在默认情况下,绘制图形会产生锯齿,并且使用这样的规则进行绘制:当使用宽度为一个像素的画笔进行渲染时,像素会在数学定义的点的右边和下边进行渲染。

在这里插入图片描述

当使用一个拥有偶数像素的画笔进行渲染时,像素会在数学定义的点的周围对称渲染;而当使用奇数像素的画笔进行渲染时,像素会被渲染到数学定义的点和右边和下边。

在这里插入图片描述

如果在绘制时使用了抗锯齿渲染提示,即使用

QPainter::setRenderHint(RenderHint hint, bool on = true)

函树,将参数hint设置了QPainter::Antialiasing,那么像素就会在数学定义的点的两侧对称地进行渲染。

在这里插入图片描述



坐标变换



1. 基本变换

默认情况下,QPainter会在相关设备的坐标系统上进行操作,但是它也完全支持仿射变换。绘图时可以使用QPainter::scale()函数

缩放

坐标系统,使用QPainter::rotate()函数顺时针

旋转

坐标系统,使用QPainter::translate()函数

平移

坐标系统,还可以使用QPainter::shear()围绕原点来

斜切

坐标系统。

坐标系统的2D变换由QTransform类来实现,QTransform类对象可以存储多个变换操作,因此当同样的变换要多次使用时,建议使用QTransform类对象。坐标系统的变换是通过变换矩阵实现的。

和在html5-canvas中一样,在进行变换操作时,可能需要多次改变坐标系统,然后再恢复到原来的状态,这样编码会很乱,而且容易出现操作错误。

时可以使用QPainter::save()函数来保存QPainter的变换矩阵,它会把变换矩阵保存到一个内部栈中,需要恢复变换矩阵时再使用QPainter::restore()函数将其弹出。



2. 窗口-视口变换

在使用QPainter进行绘制时,会使用逻辑坐标进行绘制,然后再转换为绘图设备的物理坐标。逻辑坐标到物理坐标的映射由QPainter的worldTransform()函数,QPainter的viewport()以及window()函数进行处理。

视口(viewport)表示物理坐标下指定的一个任意矩形,而窗口(window)表示逻辑坐标下的相同矩形。默认情况下,逻辑坐标和物理坐标是重合的,它们都相当于绘图设备上的矩形。

可以使用下面的代码来将绘图区域框定在QRect(-50, -50, 300, 300)所围成的矩形内,这样,逻辑上的(-50, -50)坐标就变成了物理坐标上的(0, 0):

QPainter painter(this);
painter.setWindow(QRect(-50, -50, 300, 300));   // 【坐标变换】 => 逻辑坐标(-50,-50)变换为物理坐标(0,0)

在这里插入图片描述

当设置窗口或者视口矩形时,实际上是执行了坐标的一个线性变换,窗口的4个角会映射到视口对应的4个角上。因此,一个很好的方法是让视口和窗口维持相同的宽高比来防止窗口-视口变换时产生的变形状况。

/* 让视口和窗口维持相同的宽高比来防止变形 */
int side = qMin(width(), height());
int x = (width() - side / 2);
int y = (height() - side / 2);
painter.setViewport(x, y, side, side);	// 将视口设置为适合绘图设备矩形的最大矩形

在设置窗口或视口时要考虑绘图设备的大小,这样可以使绘图代码独立于绘图设备。



实例讲解

上面讲了大量关于坐标变换的知识,为了更直观的理解,下面看一个例子。

首先新建一个Qt Widgets应用,项目名称mytransformation,基类选择QWidget,类名为Widget。建立完成后,在widget.h文件中声明重绘事件处理函数:

protected:
	void paintEvent(QPaintEvent * event);

然后到widget.cpp文件中添加头文件,在添加paintEvent()函数的定义:

void Widget::paintEvent(QPaintEvent *event){
    QPainter painter(this);
    painter.fillRect(rect(), Qt::white);
    painter.setPen(QPen(Qt::red, 11));
    painter.drawLine(QPoint(5, 6), QPoint(100, 99));
    painter.translate(200, 150);    // 平移坐标系统,将(200, 150)作为原点
    // 开启抗锯齿
    painter.setRenderHint(QPainter::Antialiasing);
    painter.drawLine(QPoint(5, 6), QPoint(100, 99));    // 以相同的参数绘制一条直线
}

在这里插入图片描述

在paintEvent()中继续添加如下代码:

// 保存painter的状态
painter.save();
// 将坐标系统旋转90度
painter.rotate(90);
painter.setPen(Qt::cyan);
painter.drawLine(QPoint(5,6), QPoint(100, 99));
// 恢复painter的save()之前的状态(没有rotate()之前)
painter.restore();

在这里插入图片描述

下面继续向其中添加代码:

painter.setBrush(Qt::darkGreen);
painter.drawRect(-50, -50, 100, 50);
painter.save();
// 缩放变换
painter.scale(0.5, 0.4);
painter.setBrush(Qt::yellow);       // 改变画刷填充颜色
painter.drawRect(-50, -50, 100, 50);
painter.restore();

在这里插入图片描述

继续添加代码:

painter.setPen(Qt::blue);
painter.setBrush(Qt::darkYellow);
painter.drawEllipse(QRect(60, -100, 50, 50));
// 斜切变换
painter.shear(1.5, -0.7);
painter.setBrush(Qt::darkGray);
painter.drawEllipse(QRect(60, -100, 50, 50));

在这里插入图片描述

还是在mytransformation项目中,先将前面paintEvent()中的所有代码删除掉,将paintEvent()改为如下:

QPainter painter(this);
painter.setWindow(-50, -50, 100, 100);  // 设定窗口矩形的逻辑坐标
painter.setBrush(Qt::green);
painter.drawRect(0, 0, 20, 20);

在这里插入图片描述



分析坐标变换原因

为了更清晰的标明矩形的坐标,下面在widget.h文件中,在protected域中声明一个鼠标移动事件:

protected:
    void paintEvent(QPaintEvent *event);
    void mouseMoveEvent(QMouseEvent *event);

然后在widget.cpp中添加头文件和。为了不用按下鼠标按键也能触发鼠标移动事件,我们要在Widget类的构造函数中添加:

setMouseTracking(true);

最后添加鼠标移动事件处理函数的定义:

void Widget::mouseMoveEvent(QMouseEvent *event){
    QString pos = QString(" %1, %2").arg(event->pos().x()).arg(event->pos().y());// QString(注意是%1而不是1%).arg(这里最多可包含9个参数)
    QToolTip::showText(event->globalPos(), pos, this);  // 在event->globalPos()的位置显示tooltip提示信息
}

在这里插入图片描述

可以看到,在代码中标明从(0,0)处开始绘制的矩形实际像素坐标为(200, 150),而且矩形的宽和高也和代码中指定的20不同,而是分别变成了80和60。

那么,为什么会出现这样的情况?前面提到过,所谓逻辑坐标或者物理坐标的矩形实际上就是进行坐标的一个线性变换,逻辑坐标矩形的4个角会映射到对应坐标矩形的4个角。

而现在,Widget部件的大小为宽400,高300,所以物理坐标对应的矩形就是(0, 0, 400, 300)。显然,代码中为矩形指定的逻辑坐标宽20,高20体现在物理坐标中就是宽80高60的矩形。

因此,代码给出的逻辑坐标定位和最后窗口呈现的效果有差异的原因是发生了逻辑坐标与物理坐标的转换:

在这里插入图片描述



防止图形变形

这样一来,矩形从原先预设的正方形绘制到窗口中后变成了一个长方形。为了防止变形,需要将视口的宽和高的对应比例设置为相同值,因为逻辑坐标的矩形设置为了一个正方形,所以在这里视口(即物理坐标矩形)也应该设置为一个正方形。

更改paintEvent()函数中的代码如下:

void Widget::paintEvent(QPaintEvent *event){
     QPainter painter(this);

     int side = qMin(width(), height());
     int x = (width() / 2);
     int y = (height() / 2);
     // 设置视口
     painter.setViewport(x, y, side, side);

     painter.setWindow(0, 0, 100, 100);
     painter.setBrush(Qt::green);
     painter.drawRect(0, 0, 20, 20);
}

在这里插入图片描述



与定时器结合做一个动画

可以将定时器和2D绘图结合在一起实现一个简单的动画。

还是在mytransformation项目中,首先在widget.h中添加类前置声明:

class QTimer;

然后添加两个私有变量:

QTimer *timer;
int angle;

在这里插入图片描述

然后将widget.cpp文件中的内容改为:

#include "widget.h"
#include "ui_widget.h"
#include <QPainter>
#include <QTimer>

Widget::Widget(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::Widget)
{
    ui->setupUi(this);
    QTimer *timer = new QTimer(this);
    connect(timer, SIGNAL(timeout()), this, SLOT(update()));    // update()一次,就会执行一遍paintEvent()
    timer->start(1000); // 一秒定时器
    angle = 0;
}

Widget::~Widget()
{
    delete ui;
}

void Widget::paintEvent(QPaintEvent *event){
    angle += 10;
    if(angle == 360) angle = 0;
    int side = qMin(width(), height());
    QPainter painter(this);
    painter.setRenderHint(QPainter::Antialiasing);
    QTransform transform;
    transform.translate(width() / 2, height() / 2);
    transform.scale(side/300.0, side/300.0);	// 根据部件的大小进行缩放,这样当窗口大小改变时绘制的内容也会跟着改变大小
    transform.rotate(angle);		// 由于angle在不断变化,所以每次旋转的角度都不同
    painter.setWorldTransform(transform);
    painter.drawEllipse(-120, -120, 240, 240);  // 画钟圆
    painter.drawLine(0, 0, 100, 0);             // 画时针*/
}

在这里插入图片描述

由于上述代码连续使用了多个坐标变换,所以使用了QTransform类对象。



其他绘制



绘制文字

除了绘制图形以外,还可以使用QPainter::drawText()函数来绘制文字,也可以使用QPainter::setFont来设置绘制文字所使用的字体,使用QPainter::fontInfo()函数可以获取字体信息,它返回QFontInfo类对象。

绘制文字时会默认使用抗锯齿。

新建Qt Widgets应用,项目名称mtdrawing1,基类选择QWidget,类名Widget。项目创建完成后,在widget.h文件中声明重绘事件函数:

protected:
    void paintEvent(QPaintEvent *event);

然后将widget.cpp文件改为如下内容:

#include "widget.h"
#include "ui_widget.h"
#include <QPainter>

Widget::Widget(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::Widget)
{
    ui->setupUi(this);
}

Widget::~Widget()
{
    delete ui;
}

void Widget::paintEvent(QPaintEvent *event){
        QPainter painter(this);
        QRect rect(10.0, 10.0, 380.0, 280.0);
        painter.setPen(Qt::red);
        painter.drawRect(rect);
        painter.setPen(Qt::blue);
        painter.drawText(rect, Qt::AlignCenter, tr("AlignHCenter"));        // 文字矩形框,文字在框内的对齐方式,文字内容
        painter.drawText(rect, Qt::AlignCenter, tr("AlignLeft"));
        painter.drawText(rect, Qt::AlignCenter, tr("AlignRight"));
        painter.drawText(rect, Qt::AlignCenter, tr("AlignVCenter"));
        painter.drawText(rect, Qt::AlignCenter, tr("AlignBottom"));
        painter.drawText(rect, Qt::AlignCenter, tr("AlignCenter"));
        painter.drawText(rect, Qt::AlignCenter | Qt::AlignRight, tr("AlignBottom\n AlignRight"));
}

在这里插入图片描述

这里用于绘制文字的drawText()函数的原型是:

// (绘制文字所在的矩形盒子,文字在盒子中的对齐方式,要绘制的文字内容,第四个参数一般可省)
QPainter::drawText(const QRectF & rectangle, int flags, const QString & text, QRectF * boundingRect = 0)

文字的多个不同的对齐方式可以使用’|’符号连接它们以同时使用。

如果要绘制的文字和它的布局不用经常改动,那么也可以直接使用drawStaticText()函数来完成文字绘制,相比drawText(),它其实更高效。

下面继续向paintEvent()函数中增加如下代码:

QFont font("宋体", 15, QFont::Bold, true);        // 字体族,字体大小(默认12),字体weight,是否使用斜体
font.setUnderline(true);	// 下划线
font.setOverline(true);		// 中划线
font.setCapitalization(QFont::SmallCaps);	// 字母大写
font.setLetterSpacing(QFont::AbsoluteSpacing, 10);      // 设置字符间间距
painter.setFont(font);
painter.setPen(Qt::green);
painter.drawText(120, 80, tr("drawText()的另一种重载形式"));
painter.translate(100, 100);
painter.rotate(90);
painter.drawText(0, 0, tr("heloQt"));

在这里插入图片描述

上面用于指定字体的QFont()函数的原型是:

/*
	(
		指定字体族[可以使用QFontDataBase类查看所有支持的字体],
		点大小[默认是12],
		字体的weight属性,
		是否使用斜体
	)
QFont::QFont(const QString & family, int pointSize = -1, int weight = -1, bool italic = false)



绘制路径

如果要绘制一个复杂的图形,尤其是要重复绘制这样的图形,可以使用QPainterPath类,并使用QPainter::drawPath()进行绘制。QPainterPath类为绘制操作提供了一个容器,可以用来创建图形并且重复使用。

一个绘图路径就是由多个矩形,椭圆,线条或者曲线等组成的对象。一个路径可以是封闭的,如果矩形和椭圆;也可以是非封闭的,如线条和曲线。



1. 组成一个路径

还是之前的mydrawing2项目,将paintEvent()函数中的内容全部删掉,然后更改如下:

void Widget::paintEvent(QPaintEvent *event){
    QPainter painter(this);
    QPainterPath path;
    path.moveTo(50, 250);
    path.lineTo(50, 230);       // 类似的函数还有arcTo(), cubicTo(), quadTo()
    path.cubicTo(QPointF(105, 40), QPointF(115, 80), QPointF(120, 60)); // 绘制三次贝塞尔曲线(使用quadTo()绘制二次贝塞尔曲线)
    path.lineTo(130, 130);
    path.addEllipse(QPoint(130,130), 30, 30);       // 向路径中添加一个圆,与之类似的函数还有addPath(),addRect(),addRegion(),addText(),addPolygon()
    painter.setPen(Qt::darkYellow);
    painter.drawPath(path);		// 路径构建完成后使用drawPath()将路径绘制出来
    path.translate(200,0);
    painter.setPen(Qt::darkBlue);
    painter.drawPath(path);
}

在这里插入图片描述

上面用于绘制曲线的函数cubicTo()的原型是:

QPainterPath::cubicTo(const QPointF & c1, const QPointF & c2, const QPointF & endPoint)

这个函数可以在当前点与endPoint之间绘制出一条贝塞尔曲线,其中c1和c2为控制点。绘制结束后,endPoint就成为了”新的当前点”,可以使用currentPosition()函数来获取当前点,使用moveTo()来改变当前点。

绘制三次贝塞尔曲线示意图:

在这里插入图片描述



2. 填充规则

填充路径一共有两个填充规则:


  • Qt::OddEvenFill

  • Qt::WindingFill

其中,Qt::OddEvenFill使用的是奇偶填充规则,具体来说就是:

如果要判断一个点是否在图形中,那么就可以从该点出发向图形外引一条水平线,如果该水平线与图形的交点的个数为奇数,那么说明该点就在图形中。Qt::OddEvenFill是路径填充规则的默认值。

而Qt::WindingFill使用的是非零弯曲规则,具体来说就是:

如果要判断一个点是否在图形中,那么可以从该点向图形外引一条水平线,如果该水平线与图形的边线相交,且这个边线是顺时针绘制的,就记为1;是逆时针的,就记为-1。将所有这些记录值相加,如果结果不为0,那么就说该点在图形中。

下图展示了这两种填充规则:

在这里插入图片描述

对于Qt::OddEvenFill规则,第一个交点记为1,第二个交点记为2。而对于Qt::WindingFill规则,因为椭圆和矩形都是以顺时针进行绘制的,所以各个交点对应的边都使用1来表示。

还是在mydrawing2项目中,将paintEvent()中之前的代码清空,更改为如下:

void Widget::paintEvent(QPaintEvent *event){
        QPainter painter(this);
        QPainterPath path;
        path.addEllipse(10, 50, 100, 100);
        path.addRect(50, 100,100, 100);
        painter.setBrush(Qt::cyan);
        painter.drawPath(path);

        painter.translate(180, 0);
        path.setFillRule(Qt::WindingFill); // 使用非零弯曲填充规则,还有一个奇偶填充规则(默认值,Qt::OddEventFill)
        painter.drawPath(path);
}

在这里插入图片描述

对于路径的绘制,其实还有很多实用的函数没有讲到。例如可以使用QPainter::fillPath()来填充一个路径;使用QPainter::strokePath()函数来绘制路径的轮廓;QPainterPath::elementAt()来获取路径中的一个元素;QPainterPath::elementCount()获取路径中元素的个数;使用QPainterPath::contains()来判断一个点是否在路径中;还可以使用QPainterPath::toFillPolygon()函数将路径转换为一个多边形。



绘制图像

Qt提供了4个类来处理图像数据:QImage,QPixmap,QBitmap和QPicture,都是常用的绘图设备。

其中QImage主要用来进行I/O处理,它对I/O处理操作进行了优化,而且也可以用来直接访问和操作像素。

QPixmap主要用来在屏幕上显示图像,它对在屏幕上显示图像进行了优化。

QBitmap是QPixmap的子类,用来处理颜色深度为1的图像,即只能显示黑白两种颜色。

QPicture用来记录并重演QPainter命令。

新建一个Qt Widgets应用,项目名称为mydrawing3,基类选择QWidget,类名为Widget。项目创建完成后,在widget.h文件中声明事件重绘函数:

protected:
    void paintEvent(QPaintEvent *);

然后转到widget.cpp文件中添加头文件:

#include <QPainter>
#include <QImage>
#include <QPixmap>
#include <QBitmap>
#include <QPicture>

下面添加paintEvent()函数的定义:

void Widget::paintEvent(QPaintEvent *){
        // 绘制image
        QPainter painter;
        QImage image(100, 100, QImage::Format_ARGB32);
        painter.begin(&image);
        painter.setPen(QPen(Qt::green));
        painter.setBrush(Qt::yellow);
        painter.drawRect(10, 10, 60, 60);
        painter.drawText(10, 10, 60, 60, Qt::AlignCenter, tr("QImage"));
        painter.end();

        // 绘制pixmap
        QPixmap pix(100, 100);
        painter.begin(&pix);
        painter.setPen(QPen(Qt::green, 3));
        painter.setBrush(Qt::yellow);
        painter.drawRect(10, 10, 6, 60);
        painter.drawText(10, 10, 60, 60, Qt::AlignCenter, tr("QPixmap"));
        painter.setBrush(QColor(0, 0, 0, 100));
        painter.drawRect(50, 50, 40, 40);
        painter.end();

        // 绘制bitmap
        QBitmap bit(100, 100);
        painter.begin(&bit);
        painter.setPen(QPen(Qt::green, 3));
        painter.setBrush(Qt::yellow);
        painter.drawRect(10, 10, 60, 60);
        painter.drawText(10, 10, 60, 60, Qt::AlignCenter, tr("QBitmap"));
        painter.setBrush(QColor(0, 0, 0, 100));
        painter.drawRect(50, 50, 40, 40);
        painter.end();

        // 绘制picture
        QPicture picture;
        painter.begin(&picture);
        painter.setPen(QPen(Qt::green, 3));
        painter.setBrush(Qt::yellow);
        painter.drawRect(10, 10, 60, 60);
        painter.drawText(10, 10, 60, 60, Qt::AlignCenter, tr("QPicture"));
        painter.setBrush(QColor(0, 0, 0, 100));
        painter.drawRect(50, 50, 40, 40);
        painter.end();

        // 在widget部件上进行绘制
        painter.begin(this);
        painter.drawImage(50, 20, image);
        painter.drawPixmap(200, 20, pix);
        painter.drawPixmap(50, 170, bit);
        painter.drawPicture(200, 170, picture);
}

在这里插入图片描述

上述提到的4个绘图设备都有自己的坐标系统,它们的左上角为原点。当在不同的绘图设备中进行绘制时,都使用了begin()函数来指定设备,等绘制完成后再使用end()函数来结束绘制。



1. QImage

QImage类提供了一个与硬件无关的图像表示方法,可以直接访问像素数据,也可以作为绘图设备。

因为QImage也是QPaintDevice的子类,所以QPainter可以直接在QImage对象上进行绘制。当在QImage上使用QPainter时,绘制操作会在当前GUI线程以外的其他线程中执行。QImage提供了获取图像各种信息的相关函数,还提供了一些用于转换图像的函数。QImage支持的图像格式见下表:

在这里插入图片描述

将前面程序里的paintEvent()函数里的内容删掉:

void Widget::paintEvent(QPaintEvent *event)
{
    QPainter painter(this);
    QImage image;
    image.load("../mydrawing3/image.png");
    qDebug() << image.size() <<image.format() << image.depth();
    painter.drawImage(QPoint(10, 10), image);
    // QImage::mirrored(bool horizontal = false, bool vertical = true)
    QImage mirror = image.mirrored();   // 生成图片的镜像文件(即倒影)
    QTransform transform;
    transform.shear(0.2, 0);
    QImage image2 = mirror.transformed(transform);  // 对图片应用上面定义的变换(斜切)
    painter.drawImage(QPoint(10, 160), image2);
    image2.save("../mydrawing3/mirror.png");        // 保存image2
}

在这里插入图片描述



2. QPixmap

QPixmap可以作为一个绘图设备将图像显示在屏幕上。QPixmap中的像素在内部由底层的窗口系统进行管理。因为QPixmap是QPaintDevice的子类,所以QPainter也可以直接在它上面进行绘制。

要想访问像素,只能使用QPainter提供的相应函数,或者将QPixmap转换为QImage。而与QImage不同的是,QPixmap中的fill()函数可以使用指定的颜色初始化整个pixmap图像。

可以使用toImage()和fromImage()函数在QImage和QPixmap之间进行转换。通常情况下,QImage类用来加载一个图像文件,随意操纵图像数据,然后将QImage对象转换为QPixmap类型以将结果显示在屏幕上。当然,如果不需要对图像进行操作,那么也可以直接使用QPixmap来加载图像文件。

另外,与QImage不同的是,QPixmap依赖于具体的硬件。

QPixmap可以很容易地通过QLabel或QAbstractButton的子类(如QPushButton)显示在屏幕上。QLabel拥有一个pixmap属性,而QAbstractButton拥有一个icon属性。QPixmap可以使用copy()复制图像上的一个区域,还可以使用mask()实现遮罩效果。

将paintEvent()中之前的代码删掉,将其内容改为:

void Widget::paintEvent(QPaintEvent *event)
{
    QPainter painter(this);
    QPixmap pix;
    pix.load("../mydrawing3/linux.png");
    painter.drawPixmap(0, 0, pix.width(), pix.height(), pix);
    painter.setBrush(QColor(255, 255, 255, 100));
    painter.drawRect(0, 0, pix.width(), pix.height());
    painter.drawPixmap(100, 0, pix.width(), pix.height(), pix);
    painter.setBrush(QColor(0, 0, 255, 100));
    painter.drawRect(100, 0, pix.width(), pix.height());
}

在这里插入图片描述



屏幕截取功能

还是在mydrawing3项目中,将widget.cpp的内容改为:

#include "widget.h"
#include "ui_widget.h"
#include <QPainter>
#include <QImage>
#include <QPixmap>
#include <QBitmap>
#include <QPicture>
#include <QDebug>
#include <QDesktopWidget>
#include <QLabel>
#include <QWindow>
#include <QScreen>

Widget::Widget(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::Widget)
{
    ui->setupUi(this);
    QWindow window;         // 使用window.screen->gradWindow()来截屏
    QPixmap grab = window.screen() -> grabWindow(QApplication::desktop()->winId());
    grab.save("../mydrawing3/screen.png");
    QLabel * label = new QLabel(this);
    label->resize(400, 200);    // 设置label部件的大小
    // 为了在窗口中能完整的显示图像的内容,这里将截屏得到的grab图像按照label的大小进行了缩放
    QPixmap pix = grab.scaled(label->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation);
    label->setPixmap(pix);      // 用label显示pix图片
    label->move(0, 100);        // 将label部件放在窗口(0, 100)坐标的位置
}

Widget::~Widget()
{
    delete ui;
}

void Widget::paintEvent(QPaintEvent *event)
{
    QPainter painter(this);
    QPixmap pix;
    pix.load("../mydrawing3/linux.png");
    painter.drawPixmap(0, 0, pix.width(), pix.height(), pix);
    painter.setBrush(QColor(255, 255, 255, 100));
    painter.drawRect(0, 0, pix.width(), pix.height());
    painter.drawPixmap(100, 0, pix.width(), pix.height(), pix);
    painter.setBrush(QColor(0, 0, 255, 100));
    painter.drawRect(100, 0, pix.width(), pix.height());
}

在这里插入图片描述


window.screen()

返回QPixmapQScreen对象,上面使用的用于保存截屏图像信息的grabWindow()函数的原型是:

QPixmapQScreen::grabWindow(WId window, int x = 0, int y = 0, int width = -1, int height = -1)

这个函数可以将截屏得到的图像信息保存在一个QPixmap中,这里要指定窗口系统标识符(The window system identifier, Wid),还有要截取的矩形区域,默认是截取整个屏幕的内容。

除了截取屏幕,还可以使用QWidget::grab()来截取窗口部件上的内容。



3. QPicture

QPicture是一个可以记录和重演QPainter命令的绘图设备。QPicture可以使用一个平台无关的格式(.pic格式)将绘图命令序列化到I/O设备中,所有可以绘制在QWidget部件或者QPixmap上的内容,都可以保存在QPicture中。QPicture与分辨率无关,在不同设备上的显示效果都是一样的。

要使用QPicture记录QPainter命令,可以向这样进行:

QPicture picture;
QPainter painter;
painter.begin(&picture);
painter.drawEllipse(10, 20, 80, 70);
painter.end();
picture.save("drawing.pic");

要重演QPainter命令,可以像下面这样进行:

QPicture picture;
picture.load("drawing.pic");
QPainter painter;
painter.begin(&myImage);
painter.drawPicture(0, 0, picture);
painter.end();



复合模式

QPainter提供了复合模式(Composition Modes)来定义如何完成数字图像的复合,即如何将源图像的像素和目标图像的像素进行合并。QPainter提供的常用复合模式及其效果如下图所示:

在这里插入图片描述

所有的复合模式可以在QPainter的帮助文档中进行查看。其中,最普通的类型是SourceOver(通常被称为alpha混合),就是正在绘制的源像素混合在已经绘制的目标像素上,源像素的alpha分量定义了它的透明度,这样源图像就会以透明效果在目标图像上显示出来。

若绘图设备是QImage,图像的格式一定要指定为Format_ARGB32Premultiplied或者Format_ARGB32,不然复合模式就不会产生任何效果。当设置了复合模式,它就会应用到所有的绘图操作中,如画笔,画刷,渐变和pixmap/image绘制等。

新建Qt Widgets应用,项目名称mycomposition,基类选择QWidget,类名为Widget,项目创建完成后,在widget.h文件中声明重绘事件处理函数paintEvent(QPaintEvent * ),然后到widget.cpp文件中添加头文件,并且定义paintEvent()函数如下:

void Widget::paintEvent(QPaintEvent * )
{
    QPainter painter;
    QImage image(400, 300, QImage::Format_ARGB32_Premultiplied);
    painter.begin(&image);
    painter.setBrush(Qt::green);
    painter.drawRect(100, 50, 200, 200);
    painter.setBrush(QColor(0, 0, 255, 150));
    painter.drawRect(50, 0, 100, 100);
    painter.setCompositionMode(QPainter::CompositionMode_SourceIn);
    painter.drawRect(250, 0, 100, 100);
    painter.setCompositionMode(QPainter::CompositionMode_DestinationOver);
    painter.drawRect(50, 200, 100, 100);
    painter.end();
    painter.begin(this);
    painter.drawImage(0, 0, image);
}

在这里插入图片描述



绘图中的其他问题



1. 重绘事件

上面讲到的所有绘制操作都是在重绘事件处理函数paintEvent()中完成的,它是QWidget类中定义的函数。一个重绘事件可以用来绘制一个部件的全部或者部分区域。

那么,什么时候会触发重绘事件?

  • repaint()函数或者update()函数被调用时;
  • 被隐藏的部件现在被重新显示;

要注意的是,不要在paintEvent()函数中调用update()或者repaint()函数。

当重绘事件发生时,要更新的区域一般会先被檫除,然后再重新在部件的背景上进行绘制,部件的背景一般可以使用setBackgroundRole()来指定,然后使用setAutoFillBackground(true)来启用指定的颜色。

例如,如果要使界面显示比较深的颜色,则可以在部件的构造函数中添加如下代码:

setBackgroundRole(QPalette::Dark);
setAutoFillBackground(true);



2. 裁剪区域

QPainter可以剪切任何的绘制操作,它可以剪切一个矩形,一个区域或者一个路径中的内容,这分别可以使用setClipRect(),setClipRegion()和setClipPath()函数来实现。剪切会在QPainter的逻辑坐标系统中进行。

QPainter painter(this);
painter.setClipRect(10, 0, 20, 10);		// 设置裁剪矩形
painter.drawText(10, 10, tr("linux"));



3. 读取和写入图像

要读取图像,最普通的方法是使用QImage或者QPixmap的构造函数,或者调用QImage::load()和QPixmap::load()函数。

Qt中还有一个QImageReaderr类,该类提供了一个格式无关的接口,可以从文件或者其他设备中读取图像。QImageReader类可以在读取图像是提供更多的控制,例如,可以使用setScaledSize()函数将图像以指定的大小进行读取,还可以使用setClipRect()读取图像的一个区域。由于依赖于图像格式底层的支持,QImageReader类的这些操作可以节省内存和加快图像的读取。

另外,Qt还提供了QImageWriter类来存储图像,它支持一些设置图像格式的特定选项,比如压缩等级和品质等。当然,如果不需要设置这些选项,那么也可以直接使用更为方便的QImage::save()和QPixmap::save()函数。



4. 播放GIF动画

QMovie类是使用QImageReader来播放动画的便捷类,使用它可以播放不带声音的简单的动画,比如.gif格式。这个类提供了很方便的函数来进行动画的开始,暂停和停止等操作。



5. 渲染SVG文件

在Qt中可以使用QSvgWidget类来加载一个SVG文件,而使用QSVGRender类在QSVGWidget中进行SVG文件的渲染。



版权声明:本文为qq_39153720原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。