Qt QPainter 绘图系统精通指南

1. QPainter 简介

QPainter 是 Qt 绘图系统的核心类。它提供了一系列高度优化的函数,用于在不同的"绘图设备"(如 QWidgetQPixmapQImage 等)上绘制从简单线条到复杂图形的各种内容。可以把 QPainter 理解为一个"画家",他手持"画笔"(QPen)和"画刷"(QBrush),在"画布"(QPaintDevice)上进行创作。

核心理念: 在 Qt 中,所有的绘图操作都应该在 paintEvent 事件中完成。系统会在需要重绘窗口时(例如,窗口首次显示、被遮挡后重新出现、尺寸改变时)自动调用这个函数。我们不应该在 paintEvent 之外直接调用绘图函数,而是通过调用 update()repaint() 来触发一次 paintEvent 事件。

2. 核心概念详解

2.1 坐标系统 (Coordinate System)

QPainter 使用一个 2D 笛卡尔坐标系。默认情况下:

  • 原点 (0, 0) 位于绘图设备的左上角。

  • X 轴 从左向右递增。

  • Y 轴 从上向下递增。

2.2 抗锯齿 (Antialiasing)

在绘制斜线或曲线时,像素点阵的特性会导致边缘出现锯齿状。抗锯齿是一种图形技术,通过在图形边缘添加半透明的像素来平滑边缘,使其看起来更柔和、更美观。

QPainter 中,可以通过 setRenderHint() 方法轻松开启抗锯齿:

复制代码
painter.setRenderHint(QPainter::Antialiasing, true);

开启抗锯齿会带来轻微的性能开销,但对于大多数现代硬件来说,这种开销可以忽略不计,却能极大地提升绘图质量。

2.3 画笔 (QPen)

QPen 用于定义线条和轮廓的样式。把它想象成画家用来勾勒物体边缘的笔。

主要属性:

  • 颜色 (Color): pen.setColor(Qt::blue)

  • 宽度 (Width): pen.setWidth(3) (单位是像素)

  • 样式 (Style): pen.setStyle(Qt::DashLine) (实线、虚线、点线等)

  • 笔帽样式 (Cap Style): pen.setCapStyle(Qt::RoundCap) (线条端点的样式:平直、圆形、方形)

  • 连接样式 (Join Style): pen.setJoinStyle(Qt::RoundJoin) (多条线段连接处的样式:斜角、圆形、直角)

示例场景: 绘制一个 3 像素宽的蓝色虚线矩形轮廓。

复制代码
// 在 paintEvent 中
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing); // 开启抗锯齿

QPen pen; // 创建一个画笔对象
pen.setColor(QColor(0, 0, 255)); // 设置颜色为蓝色
pen.setWidth(3); // 设置宽度
pen.setStyle(Qt::DashLine); // 设置为虚线样式

painter.setPen(pen); // 将画笔应用到画家
painter.drawRect(50, 50, 200, 100); // 绘制矩形

2.4 画刷 (QBrush)

QBrush 用于定义填充区域的样式。把它想象成画家用来给闭合图形上色的刷子。

主要属性:

  • 颜色 (Color): brush.setColor(Qt::green)

  • 样式 (Style): brush.setStyle(Qt::SolidPattern) (纯色、渐变、纹理等)

  • 纹理 (Texture): brush.setTexture(QPixmap(":/images/texture.png")) 可以使用图片作为填充纹理。

样式种类 (Qt::BrushStyle):

  • Qt::SolidPattern: 纯色填充 (最常用)

  • Qt::LinearGradientPattern: 线性渐变

  • Qt::RadialGradientPattern: 径向渐变

  • Qt::TexturePattern: 纹理填充

  • Qt::NoBrush: 不进行任何填充 (图形是透明的)

示例场景: 绘制一个内部用从左到右的红黄渐变色填充的圆形。

复制代码
// 在 paintEvent 中
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);

// 定义一个线性渐变
QLinearGradient gradient(0, 0, 200, 0); // 从 (0,0) 到 (200,0) 的渐变
gradient.setColorAt(0.0, Qt::red);   // 起点是红色
gradient.setColorAt(1.0, Qt::yellow); // 终点是黄色

QBrush brush(gradient); // 使用渐变创建画刷

painter.setPen(Qt::NoPen); // 我们不希望有轮廓
painter.setBrush(brush);   // 应用画刷

painter.drawEllipse(50, 50, 200, 200); // 绘制圆形

3. 在 paintEvent 中高效绘图

所有自定义控件的绘图逻辑都应该在 paintEvent 函数中实现。这是一个受保护的虚函数,源自 QWidget

3.1 为什么是 paintEvent?

  1. 系统调度: Qt 的事件循环管理着所有重绘请求。将多个小的重绘请求合并成一次,可以有效避免不必要的屏幕刷新,提高程序效率。

  2. 状态保护:paintEvent 之外进行绘图可能会导致绘图状态不稳定或被意外擦除。paintEvent 提供了一个受保护的绘图环境。

  3. 双缓冲: 现代 UI 框架通常使用双缓冲技术,paintEvent 的绘图操作通常作用于后台缓冲区,完成后一次性交换到前台,避免闪烁。

3.2 完整示例:自定义绘图控件

下面是一个完整的例子,演示如何创建一个自定义 QWidget,并在其中使用 QPainterQPenQBrush 绘制各种图形。

customwidget.h (头文件)

复制代码
#ifndef CUSTOMWIDGET_H
#define CUSTOMWIDGET_H

#include <QWidget>

class CustomWidget : public QWidget
{
    Q_OBJECT

public:
    explicit CustomWidget(QWidget *parent = nullptr);

protected:
    // 覆盖父类的 paintEvent 函数
    void paintEvent(QPaintEvent *event) override;
};

#endif // CUSTOMWIDGET_H

customwidget.cpp (源文件)

复制代码
#include "customwidget.h"
#include <QPainter>
#include <QPen>
#include <QBrush>
#include <QPixmap>
#include <QFont>

CustomWidget::CustomWidget(QWidget *parent) : QWidget(parent)
{
    // 设置一个固定的尺寸,方便演示
    setFixedSize(600, 400);
}

void CustomWidget::paintEvent(QPaintEvent *event)
{
    // 1. 创建 QPainter 对象
    // `this` 指明绘图设备是当前窗口
    QPainter painter(this);

    // 2. 开启抗锯齿,让图形更平滑
    painter.setRenderHint(QPainter::Antialiasing, true);

    // ---------- 场景1: 绘制带样式的线条 ----------
    QPen linePen;
    linePen.setColor(Qt::darkRed);
    linePen.setWidth(5);
    linePen.setCapStyle(Qt::RoundCap); // 圆形笔帽
    painter.setPen(linePen);
    painter.drawLine(QPoint(20, 20), QPoint(200, 20));

    // ---------- 场景2: 绘制带轮廓和填充的形状 ----------
    QPen rectPen;
    rectPen.setColor(Qt::black);
    rectPen.setWidth(2);
    painter.setPen(rectPen);

    QBrush rectBrush;
    rectBrush.setColor(Qt::cyan);
    rectBrush.setStyle(Qt::SolidPattern);
    painter.setBrush(rectBrush);
    painter.drawRect(20, 50, 150, 100);

    // ---------- 场景3: 绘制渐变填充的圆形 ----------
    painter.setPen(Qt::NoPen); // 不需要轮廓
    QRadialGradient gradient(QPoint(350, 100), 80); // 中心点(350,100), 半径80
    gradient.setColorAt(0, Qt::white);
    gradient.setColorAt(1, Qt::darkBlue);
    painter.setBrush(QBrush(gradient));
    painter.drawEllipse(QPoint(350, 100), 80, 80);

    // ---------- 场景4: 绘制文本 ----------
    QPen textPen(Qt::darkGreen);
    painter.setPen(textPen);
    QFont font("Arial", 20, QFont::Bold); // 字体, 大小, 粗体
    painter.setFont(font);
    // drawText(x, y, width, height, alignment, text)
    painter.drawText(20, 200, 300, 50, Qt::AlignLeft, "Hello, QPainter!");

    // ---------- 场景5: 绘制图片 ----------
    // 确保你的项目中有这张图片,或者使用绝对路径
    // 建议使用 Qt 资源文件 (qrc)
    QPixmap pixmap(":/images/qt_logo.png"); // 假设图片在资源文件中
    if (!pixmap.isNull()) {
        painter.drawPixmap(20, 250, pixmap.scaled(150, 150, Qt::KeepAspectRatio));
    } else {
        // 如果图片加载失败,绘制提示文字
        painter.drawText(20, 250, "Image not found");
    }
    
    // ---------- 场景6: 绘制多边形 ----------
    QPen polyPen(QColor("#8866AA"), 3); // 紫色画笔
    painter.setPen(polyPen);
    painter.setBrush(QColor(200, 220, 255, 180)); // 半透明浅蓝色画刷
    
    QPoint points[] = {
        QPoint(300, 200),
        QPoint(400, 250),
        QPoint(550, 350),
        QPoint(450, 380),
        QPoint(320, 300)
    };
    painter.drawPolygon(points, 5); // 绘制一个5个顶点的多边形
}

main.cpp

复制代码
#include <QApplication>
#include "customwidget.h"

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    CustomWidget w;
    w.setWindowTitle("QPainter 精通指南");
    w.show();
    return a.exec();
}

3.3 绘图性能优化建议

  1. 最小化重绘区域: paintEvent 的参数 QPaintEvent *event 包含了需要重绘的区域 event->rect()。如果你的绘图逻辑很复杂,可以判断这个区域,只重绘与该区域相交的部分,避免全窗口重绘。

  2. 避免在 paintEvent 中进行复杂计算: paintEvent 应该专注于"画"。所有的数据计算、坐标点生成等耗时操作都应该在 paintEvent 之外完成并缓存起来。

  3. 使用 QPixmap 缓存: 如果一部分绘图内容是静态不变的,可以预先将这部分内容绘制到一个 QPixmap 上,然后在 paintEvent 中直接调用 painter.drawPixmap() 将其一次性画出。这对于复杂的背景或仪表盘非常有效。

  4. 按需调用 update() 当数据变化时,才调用 update() 来触发重绘。避免不必要或过于频繁的 update() 调用。

通过学习和实践以上内容,您将能够熟练运用 QPainter 来实现各种自定义的 2D 图形界面和绘图应用。精通 QPainter 是成为一名高级 Qt 开发者的必经之路。

问题思考:

  • QWidget : 是舞台或画板,是用户最终能看到的东西。

  • QPixmap : 是准备展出的画作,为在舞台上高效展示而优化。

  • QImage : 是可以任意编辑的数字文件,为修改和存取而优化。

详细讲解与场景分析

1. QWidget (控件/窗口)
  • 是什么? 它是您在屏幕上能看到并与之交互的一切 。应用程序的主窗口、按钮、标签、自定义的图表等,都是 QWidget 或其子类。

  • 核心作用:

    • 作为"画布": 它是 QPainter 最终进行绘制的目标,是所有图形的最终呈现平台。

    • 接收事件: 负责接收用户的鼠标点击、键盘输入等事件。

  • 场景: 您的整个应用程序主窗口就是一个 QWidget。当您想画一个仪表盘时,您会创建一个自定义的 Dashboard 类继承自 QWidget,这个 Dashboard 实例的区域就是您的专属"画板"。

2. QPixmap (像素图 - 用于显示)
  • 是什么? 它是一个专门为在屏幕上高效绘制 而优化的图像对象。可以把它想象成一幅已经装裱好、随时可以挂到墙上(QWidget)的画。

  • 核心作用:

    • 高效绘制: painter.drawPixmap() 是一个非常快速的操作,因为 QPixmap 的内部存储格式与显示硬件的格式非常接近。

    • 作为缓存: 对于不常变化的复杂背景(如仪表盘的刻度),可以预先绘制到一个 QPixmap 上,之后在 paintEvent 中直接贴图即可,极大提升性能。

  • 不适合做什么: 不适合进行频繁的像素级修改。虽然可以,但效率远不如 QImage

  • 场景: 按钮上的图标、程序背景图、游戏中需要快速移动的精灵(Sprite),或者我们之前讨论的仪表盘背景缓存,都非常适合用 QPixmap 存储。

3. QImage (图像 - 用于操作)
  • 是什么? 它是一个独立于硬件的图像对象,专门为图像的I/O(读写)和像素级操作 而设计。可以把它想象成 Photoshop 里的一个图层或一个原始的 .png 文件,您可以对它的每一个像素进行任意修改。

  • 核心作用:

    • 像素访问: 提供了 pixel()setPixel() 等函数,可以方便、快速地读取或修改任意坐标的像素颜色。

    • 文件操作: 可以轻松地从文件加载(如 .jpg, .png)或保存为多种格式的文件。

    • 多线程安全: 可以在非 UI 线程中对 QImage 进行复杂的处理(如应用滤镜),而不会阻塞界面。

  • 不适合做什么: 直接在 paintEvent 中频繁绘制 QImage 的效率不如 QPixmap

  • 场景: 开发一个图片编辑器

    1. 加载: 用户点击"打开"按钮,您使用 QImage image("photo.jpg"); 将图片文件加载到 QImage 对象中。

    2. 编辑: 用户点击"灰度滤镜"按钮,您会写一个循环,遍历 QImage 的所有像素,使用 setPixel() 将每个像素的颜色转为灰色。

    3. 显示: 为了在 QWidget 窗口中显示处理后的图片,您需要先将 QImage 转换为 QPixmapQPixmap pixmap = QPixmap::fromImage(image);。然后在 paintEvent 中调用 painter.drawPixmap(0, 0, pixmap);

    4. 保存: 用户点击"保存",您使用 image.save("photo_gray.png"); 将修改后的 QImage 保存为新文件。


总结关系

QWidgetQImageQPixmap 构成了 Qt 绘图的黄金三角:

  • QImage 是数据的来源和处理器。 负责从文件读入,进行像素级的复杂修改。

  • QPixmap 是高效的显示媒介。 它是 QImageQWidget 之间的桥梁,保证了在屏幕上的绘制性能。

  • QWidget 是最终的展示平台。 所有图像最终都要在它上面呈现给用户。

最经典的使用流程就是: 文件/数据 -> QImage (加载/处理) -> QPixmap (转换为显示格式) -> QWidgetpaintEvent (绘制)。

它们之间的关系是这样的:

  1. QWidget :是所有界面控件 的基类。您在屏幕上看到的按钮、窗口、标签等都是 QWidget。它的主要职责是显示在屏幕上并接收用户输入(如鼠标点击)。

  2. QPixmapQImage :这两个是用来处理图像数据的类,它们本身不是界面控件,不能直接显示在屏幕上。您可以把它们看作是内存中的一张图片。

它们三者的一个共同点是,它们都继承自 QPaintDevice(绘图设备)。

  • QPaintDevice 是一个基类,它代表了任何可以被 QPainter 在上面进行绘制的对象。

所以,您可以这样理解:

  • 因为 QWidget 是一个 QPaintDevice,所以您可以在窗口上画画。

  • 因为 QPixmap 是一个 QPaintDevice,所以您可以在一张看不见的图片上(在内存里)画画。

  • 因为 QImage 也是一个 QPaintDevice,所以您也可以在另一张看不见的图片上画画。

总结一下: QImageQPixmap数据 ,而 QWidget舞台 。您必须在 QWidgetpaintEvent 中,使用 QPainterQPixmapQImage 这些"数据"画到"舞台"上,用户才能最终看到它们。

您好!您的理解非常接近了,这是一个很好的总结,只在一个小细节上可以更精确一些。我们来梳理一下,让这个流程完全清晰。

您说的:

"比如汽车仪表盘那先静态的我们可以提前使用QPixmap画完..."

完全正确! 这就是 QPixmap 缓存的核心思想。把所有不会动的、复杂的背景(刻度盘、Logo、警告灯的位置 等)一次性画在一个 QPixmap 对象上。

"...然后有一些图片图标贴上去就用QImage..."

这里可以优化一下。 QImage 的强项是修改 像素和文件操作 。如果您的图标只是加载进来然后显示,并不需要去修改它的颜色或形状 ,那么直接将图标文件加载到 QPixmap 中会更直接、更高效。

  • 正确流程 :在准备静态背景的那一步,就可以把那些图标(比如机油灯图标 oil_icon.png)直接加载成 QPixmap,然后画到那个大的背景 QPixmap 缓存里去。

  • 何时用QImage :如果您需要动态地改变图标颜色(比如机油灯平时是灰色,有问题时变成红色),那么可以先把图标加载到 QImage,通过 setPixel() 等函数修改颜色,然后再转换为 QPixmap 来显示。

"...最终画指针和速度值就再QWidget"

完全正确! 这是最关键的一步。无论您之前用 QPixmap 准备了多么精美的背景,最终它们都必须在 QWidgetpaintEvent 函数里被"画"到屏幕上,用户才能看见。

总结一下最理想的流程:

  1. 准备阶段 (在 paintEvent 之外,只做一次)

    • 创建一个 QPixmap 对象作为背景缓存

    • 用一个 QPainter 在这个背景缓存 QPixmap 上画画:

      • 画出刻度盘、刻度线。

      • 加载图标文件(如 logo.png, oil_icon.png)到临时的 QPixmap 对象中,再把它们画到背景缓存 QPixmap 上。

    • 此时,您得到了一张包含了所有静态元素的、完整的背景图片 m_backgroundCache

  2. 显示阶段 (在 paintEvent 中,每次需要更新时都执行)

    • 创建一个 QPainter,这次是在您的仪表盘 QWidget 上画画。

    • 第一步 (极速) :调用 painter.drawPixmap(m_backgroundCache),把准备好的整个背景一次性贴到 QWidget 上。

    • 第二步 (画动态元素) :在背景之上,继续使用 painter 画那根会动的指针和变化的里程数字。

所以,您的理解主体上是完全正确的:QPixmap 用于准备和缓存,而 QWidget 是最终所有东西(包括缓存和动态元素)汇合与呈现的最终舞台。

相关推荐
非凡的世界6 小时前
Thinkphp8 Redis队列与消息队列topthink/think-queue 原创
数据库·redis·bootstrap·thinkphp
yookay zhang6 小时前
DM线程的管理知识学习
数据库
l1t7 小时前
测试DuckDB电子表格读取插件rusty_sheet 0.2版
数据库·rust·插件·xlsx·duckdb
TiAmo zhang7 小时前
SQL Server 2019实验 │ 表数据插入、修改和删除
数据库·oracle
慕容雪_7 小时前
MySQL去除表、字段注释
数据库·mysql
zym大哥大7 小时前
C++客服端访问redis
数据库·redis·缓存
洛克大航海7 小时前
Ubuntu安装Hbase
大数据·linux·数据库·ubuntu·hbase
小蒜学长8 小时前
springboot餐厅信息管理系统设计(代码+数据库+LW)
java·数据库·spring boot·后端
Justin_198 小时前
mysql数据库高级特性(一)
数据库·mysql
邂逅you9 小时前
用python操作mysql之pymysql库基本操作
数据库·python·mysql