QT绘画系统
绘画系统
继承图
QWidget继承自QPaintDevice
Qt的绘图系统(Painting System) 基于QPainter,QPainterDevice和QPaintEngine三个类
- QPainter用来执行绘制的操作,所以称为绘制器;
- QPaintDevice是一个二维空间的抽象,这个二维空间允许QPainter在其上面进行绘制,也就是QPainter工作的空间,所以将QPaintDevice称为绘图设备;
- QPaintEngine提供了画笔(QPainter)在不同的设备上进行绘制的统一的接口,所以称为绘图引擎 。
QPaintEngine类应用于QPainter和QPaintDevice之间,通常对开发人员是透明的。除非你需要自定义一个设备,否则你是不需要关心QPaintEngine这个类的。我们可以把QPainter理解成画笔;把QPaintDevice理解成使用画笔的地方,比如纸张、屏幕等;而对于纸张、屏幕而言,肯定要使用不同的画笔绘制,为了统一使用一种画笔,我们设计了QPaintEngine类,这个类让不同的纸张、屏幕都能使用一种画笔。这三者之间的关系如下图:
官网的描述
构造函数
update()
1、执行时机
update函数只是将一个paintEvent事件添加到事件队列中,等待稍后执行,它不会立即执行paintEvent。
repaint函数会立即执行paintEvent,而不会等待事件队列的处理。
2、重绘区域
update函数会合并多个重绘请求,只在最后执行一次paintEvent,可以减少不必要的重绘操作,提高性能。
repaint函数会立即执行paintEvent,不会进行任何合并操作,这意味着每次调用都会触发一次paintEvent。
3、同步性
update是异步的,它只是将重绘事件添加到事件队列,不会立即执行。
repaint是同步的,它会立即执行paintEvent,阻塞直到重绘完成。
QPen
Join Style
Cap Style
PenStyle
SetDashPattern
坐标变换(Coordinate Transformation)
1. 平移(Translate):移动坐标系原点
- 功能:将当前坐标系的原点(0,0)移动到指定点
(dx, dy)
,后续绘图的坐标均基于新原点。 - 函数:
void QPainter::translate(qreal dx, qreal dy)
- 示例:先平移再画矩形,等效于直接画
QRect(10+50, 10+50, 50, 50)
。void MyWidget::paintEvent(QPaintEvent *event) {QPainter painter(this);painter.setPen(Qt::red);// 1. 平移:将原点移到 (50, 50)painter.translate(50, 50);// 2. 绘制矩形:此时 (10,10) 是平移后的坐标,对应原坐标系 (60,60)painter.drawRect(10, 10, 50, 50); }
2. 旋转(Rotate):绕原点旋转坐标系
- 功能:将当前坐标系绕原点顺时针旋转指定角度(单位:度),注意旋转方向(顺时针为正,逆时针为负)。
- 函数:
void QPainter::rotate(qreal angle)
- 关键注意点:旋转的中心点默认是当前坐标系的原点,若需绕图形自身中心旋转,需先平移到图形中心,旋转后再平移回原位(见下文 “经典场景”)。
- 示例:旋转坐标系后画矩形,矩形会随坐标系旋转。
void MyWidget::paintEvent(QPaintEvent *event) {QPainter painter(this);painter.setPen(Qt::blue);// 1. 先平移到 (100, 100)(避免旋转后矩形超出窗口)painter.translate(100, 100);// 2. 旋转 45 度(顺时针)painter.rotate(45);// 3. 绘制矩形:基于旋转后的坐标系,矩形会倾斜 45 度painter.drawRect(-25, -25, 50, 50); // 中心在原点,避免偏移 }
3. 缩放(Scale):拉伸 / 压缩坐标系
- 功能:按比例缩放 X 轴和 Y 轴,值大于 1 为放大,小于 1 为缩小,负值为反向(如 X 轴负缩放会导致图形水平翻转)。
- 函数:
void QPainter::scale(qreal sx, qreal sy)
(sx
:X 轴缩放因子,sy
:Y 轴缩放因子) - 示例:缩放后绘制文本,文本会被拉伸或缩小。
void MyWidget::paintEvent(QPaintEvent *event) {QPainter painter(this);painter.setPen(Qt::green);// 1. 缩放:X轴放大2倍,Y轴放大1.5倍painter.scale(2.0, 1.5);// 2. 绘制文本:文本会横向拉伸2倍,纵向拉伸1.5倍painter.drawText(10, 30, "Scaled Text"); }
4. 剪切(Shear):倾斜坐标系
- 功能:使坐标系沿 X 轴或 Y 轴倾斜(剪切变换),产生 “斜切” 效果,常用于绘制平行四边形、梯形等。
- 函数:
void QPainter::shear(qreal sh, qreal sv)
(sh
:X 轴剪切因子,sv
:Y 轴剪切因子) - 示例:剪切后画矩形,矩形会变成平行四边形。
void MyWidget::paintEvent(QPaintEvent *event) {QPainter painter(this);painter.setPen(Qt::purple);// 1. 剪切:X轴倾斜0.5(沿Y轴方向偏移),Y轴不倾斜painter.shear(0.5, 0.0);// 2. 绘制矩形:矩形沿X轴倾斜,变成平行四边形painter.drawRect(50, 50, 80, 40); }
save()和restore
Anti-aliased Painting(抗锯齿绘画)
四种绘图设备介绍
Qt提供了4个类来处理图像:QImage、QPixmap、QBitmap、QPicture,为了对这几个类加以区分,分别称QImage为图像、QPixmap为像素图、QBitmap为位图、QPicture为图片,由于它们都是QPaintDevice类的子类(直接或间接),因此他们都是绘制设备,可以直接在其上进行图形绘制。
Qt各个处理图像类的区别及作用 :
QImage类提供了一个与硬件无关的图像表示方法,可以直接访问和操控像素,也就是说该类可修改或编辑图像的像素。该类还可以用于进行I/O处理,并对I/O处理操作进行了优化 ;
QPixmap类主要用于在屏幕上显示图像,QPixmap中的像素数据是由底层窗口系统进行管理的,该类不能直接访问和操控像素,只能通过QPainter的相应函数或把QPixmap转换为QImage来访问和操控像素。QPixmap可通过标签(QLabel 类)或QAbstractButton 的子类(icon 属性)显示在屏幕上 ;
QBitmap是QPixmap的子类,用于处理颜色深度为1的图像,即只能显示黑白两种颜色;
QPicture用来记录并重演QPainter命令,QPicture与分辨率无关,可在不同设备上显示。该类使用一个与平台无关的格式(.pic 格式)把绘图命令序列化到 I/O 设备,所有可绘制在QWidget部件或QPixmap上的内容都可以保存在QPicture中,该类的主要作用是把一个绘制图备上使用QPainter绘制的所有图形保存在QPicture之中,然后再把这些图形重新绘制在其他绘图设备上 。
双缓冲机制
在 Qt 绘图系统中,双缓冲机制(Double Buffering) 是一种用于解决绘图闪烁问题的技术,尤其适用于频繁刷新或复杂图形绘制的场景。它通过在内存中预先完成所有绘制操作,再一次性将结果显示到屏幕上,避免了用户看到 “半成品” 绘图过程导致的视觉闪烁。
一、为什么需要双缓冲?
在单缓冲机制中,绘图操作直接在屏幕(显示设备)上进行:
- 当绘制复杂图形(如大量线条、文本、图像)或频繁刷新时,用户会看到 “逐步绘制” 的过程(例如先画一部分,再画另一部分)。
- 若同时存在擦除旧内容(如
eraseRect()
)和绘制新内容的操作,屏幕会短暂显示空白或不完整画面,产生明显 “闪烁”。
双缓冲通过引入内存缓冲区解决这一问题,核心思路是:“先在内存中画好,再一次性展示”。
二、双缓冲的基本原理
双缓冲包含两个 “缓冲区”:
- 后台缓冲区(Off-screen Buffer):一块内存中的绘图区域(如
QPixmap
),所有绘图操作先在这块区域完成,用户看不到这个过程。 - 前台缓冲区(On-screen Buffer):屏幕上的显示区域,当后台缓冲区绘制完成后,将其内容一次性复制到前台缓冲区,用户看到的是完整的画面。
整个过程对用户来说是 “瞬时” 的,因此不会感知到中间的绘制步骤,从而消除闪烁。
三、Qt 中的双缓冲实现
Qt 对双缓冲提供了自动支持和手动实现两种方式,大多数情况下无需手动处理,Qt 已内部优化。
1. Qt 的自动双缓冲(推荐)
从 Qt 4 开始,QWidget
及其子类(如 QFrame
、QPushButton
等)默认启用了双缓冲机制,核心逻辑由 Qt 内部处理:
- 当重写
paintEvent()
时,QPainter
并非直接绘制到屏幕,而是先绘制到一个临时内存缓冲区(由 Qt 自动创建)。 - 当
paintEvent()
执行完毕,Qt 会自动将缓冲区内容复制到屏幕,完成显示。
这种机制下,开发者无需手动管理缓冲区,即可避免大部分闪烁问题。
时钟代码
//widget.h
#ifndef WIDGET_H
#define WIDGET_H#include <QWidget>
#include <QBrush>
#include <QTimer>
#include <QTime>
#include <QDebug>
#include <QPaintEvent>
#include <QPainter>class Widget : public QWidget
{Q_OBJECTpublic:Widget(QWidget *parent = nullptr);~Widget();void paintEvent(QPaintEvent *event) override;private:QTimer *m_timer;int m_count = 0;int m_hour;int m_minute;int m_second;
};
#endif // WIDGET_H
//widget.cpp
#include "widget.h"#include <QDebug>Widget::Widget(QWidget *parent)
: QWidget(parent)
{//创建定时器对象m_timer = new QTimer(this);connect(m_timer, &QTimer::timeout, [&](){m_count++;update();});m_timer->start(1000);//这句话的含义是:获取当前时间,比如:"19:20:58 下午",那么//t = QString("19:20:58 下午");//list1会将t以空格为分隔符,分为("19:20:58", "下午")//list2会将list1[0],也就是QString("19:20:58"),分为("19","20","58")//然后m_hour = 19,m_minute = 20,m_second = 58QString t = QTime::currentTime().toString("h:m:s ap");QStringList list1 = t.split(" ");QStringList list2 = list1[0].split(":");m_hour = list2[0].toUInt();m_minute = list2[1].toUInt();m_second = list2[2].toUInt();//此处使用打印可以得出结果qDebug() << "t = " << t<< "list1 = " << list1<< "list2 = " << list2<< "m_hour = " << m_hour<< "m_minute = " << m_minute<< "m_second = " << m_second;
}Widget::~Widget()
{
}void Widget::paintEvent(QPaintEvent *event)
{Q_UNUSED(event);//绘制圆QPainter pat(this);QPen pen(QColor("skyblue"));pen.setWidth(3);QBrush b("pink");pat.setPen(pen);pat.setBrush(b);//移动画家的位置,然后画圆pat.translate(this->width()/2, this->height()/2);pat.drawEllipse(QPoint(0, 0), 200, 200);//绘制刻度pen.setColor(QColor("black"));pat.setPen(pen);for (int i = 0; i < 60; ++i){pat.rotate(6);pat.drawLine(QPoint(200, 0), QPoint(195, 0));}pen.setWidth(5);pat.setPen(pen);for(int i = 0; i < 12; ++i){pat.rotate(30);pat.drawLine(QPoint(200, 0),QPoint(190, 0));pat.drawText(QPoint(-5, -165), QString("%1").arg(i + 1));}//制作时针pen.setWidth(6);pen.setColor("blue");pat.setPen(pen);pat.rotate(m_hour * 30 + 6 * m_second/60/12+ 30 * m_minute/60 + 6 * m_count/60/12);pat.drawLine(QPoint(0, -50), QPoint(0, 5));//制作分针QPainter patMinute(this);patMinute.translate(this->width()/2, this->height()/2);pen.setWidth(6);pen.setColor("green");patMinute.setPen(pen);patMinute.rotate(6 * m_count/60 + 6 * m_minute + 6 * m_second/60);patMinute.drawLine(QPoint(0, -100), QPoint(0, 2));//制作秒针QPainter patSecond(this);patSecond.translate(this->width()/2, this->height()/2);pen.setWidth(6);pen.setColor("black");patSecond.setPen(pen);patSecond.rotate(6 * m_count + 6 * m_second);patSecond.drawLine(QPoint(0, -100), QPoint(0, 2));
}
测试结果
时钟代码2
//analogclock.h
#ifndef ANALOGCLOCK_H
#define ANALOGCLOCK_H#include <QWidget>class AnalogClock : public QWidget
{Q_OBJECTpublic:AnalogClock(QWidget *parent = nullptr);protected:void paintEvent(QPaintEvent *event) override;
};#endif
//analogclock.cpp
#include "analogclock.h"#include <QPainter>
#include <QTime>
#include <QTimer>// 构造函数:初始化时钟
AnalogClock::AnalogClock(QWidget *parent): QWidget(parent)
{// 创建定时器,用于每秒更新一次时钟QTimer *timer = new QTimer(this);// 连接定时器超时信号到更新函数,实现每秒刷新connect(timer, &QTimer::timeout, this, QOverload<>::of(&AnalogClock::update));timer->start(1000); // 定时器间隔设置为1000毫秒(1秒)setWindowTitle(tr("带秒针的模拟时钟")); // 设置窗口标题resize(200, 200); // 设置初始窗口大小
}// 绘图事件:绘制时钟的表盘、时针、分针和秒针
void AnalogClock::paintEvent(QPaintEvent *)
{// 定义时针形状:由3个点组成的多边形static const QPoint hourHand[3] = {QPoint(7, 8), // 时针底部右侧点QPoint(-7, 8), // 时针底部左侧点QPoint(0, -40) // 时针顶端点};// 定义分针形状:由3个点组成的多边形static const QPoint minuteHand[3] = {QPoint(7, 8), // 分针底部右侧点QPoint(-7, 8), // 分针底部左侧点QPoint(0, -70) // 分针顶端点};// 定义秒针形状:由3个点组成的多边形,比时分针更细更长static const QPoint secondHand[3] = {QPoint(3, 6), // 秒针底部右侧点QPoint(-3, 6), // 秒针底部左侧点QPoint(0, -80) // 秒针顶端点,比分针更长};// 设置时针颜色:紫色QColor hourColor(127, 0, 127);// 设置分针颜色:蓝绿色,带透明度QColor minuteColor(0, 127, 127, 191);// 设置秒针颜色:红色,更醒目QColor secondColor(255, 0, 0);// 计算时钟绘制区域的边长(取窗口宽高的最小值,保证时钟为圆形)int side = qMin(width(), height());// 获取当前时间QTime time = QTime::currentTime();// 创建绘图工具QPainter painter(this);// 启用抗锯齿,使绘制的线条更平滑painter.setRenderHint(QPainter::Antialiasing);// 将坐标原点平移到窗口中心(时钟中心)painter.translate(width() / 2, height() / 2);// 缩放坐标系,使绘制内容适应窗口大小(以200x200为基准)painter.scale(side / 200.0, side / 200.0);// ====== 绘制时针 ======painter.setPen(Qt::NoPen); // 时针不需要边框painter.setBrush(hourColor); // 设置时针填充颜色painter.save(); // 保存当前绘图状态// 计算时针旋转角度:每小时30度(360/12),加上分钟的影响painter.rotate(30.0 * ((time.hour() + time.minute() / 60.0)));// 绘制时针多边形painter.drawConvexPolygon(hourHand, 3);painter.restore(); // 恢复到之前的绘图状态// 绘制小时刻度painter.setPen(hourColor); // 使用时针颜色绘制刻度for (int i = 0; i < 12; ++i) {// 绘制刻度线(从88到96的短线)painter.drawLine(88, 0, 96, 0);painter.rotate(30.0); // 每30度绘制一个刻度(12个刻度)}// ====== 绘制分针 ======painter.setPen(Qt::NoPen); // 分针不需要边框painter.setBrush(minuteColor); // 设置分针填充颜色painter.save(); // 保存当前绘图状态// 计算分针旋转角度:每分钟6度(360/60),加上秒数的影响painter.rotate(6.0 * (time.minute() + time.second() / 60.0));// 绘制分针多边形painter.drawConvexPolygon(minuteHand, 3);painter.restore(); // 恢复到之前的绘图状态// 绘制分钟刻度painter.setPen(minuteColor); // 使用分针颜色绘制刻度for (int j = 0; j < 60; ++j) {// 每5分钟的刻度不重复绘制(已由小时刻度覆盖)if ((j % 5) != 0)painter.drawLine(92, 0, 96, 0); // 绘制较短的刻度线painter.rotate(6.0); // 每6度绘制一个刻度(60个刻度)}// ====== 绘制秒针 ======painter.setPen(Qt::NoPen); // 秒针不需要边框painter.setBrush(secondColor); // 设置秒针填充颜色painter.save(); // 保存当前绘图状态// 计算秒针旋转角度:每秒6度(360/60)painter.rotate(6.0 * time.second());// 绘制秒针多边形painter.drawConvexPolygon(secondHand, 3);painter.restore(); // 恢复到之前的绘图状态
}