QT入门第八天:绘图QPainter详解 | 零基础学QT

QT入门第八天:绘图QPainter详解 | 零基础学QT

前言

前七天我们学习了环境搭建、信号与槽、常用控件、布局管理器、对话框、菜单栏工具栏状态栏、事件处理。今天我们来学习QT的绘图机制------QPainter

绘图是GUI编程的重要部分。你看到的很多炫酷效果、自定义控件、图表、动画,本质上都是"画"出来的。QT提供了强大的2D绘图系统,核心就是QPainter类。

今天我们学习:

  • QPainter的基本用法
  • 画各种基本图形(直线、矩形、圆、椭圆、多边形...)
  • 画笔QPen和画刷QBrush
  • 画文字
  • 画图片
  • 坐标变换(平移、旋转、缩放)
  • 渐变和抗锯齿

一、QPainter 基础

1.1 什么是QPainter

QPainter是QT的绘图工具类,它可以在"绘图设备"上画东西。常见的绘图设备有:

  • QWidget:控件(最常用)
  • QPixmap:图片
  • QImage:图片(更底层)
  • QPicture:记录绘图命令

💡 理解:QPainter就像一支画笔,绘图设备就是画布。你拿着画笔在画布上画画。

1.2 paintEvent 事件

绘图一般在paintEvent事件里进行。当控件需要重绘时,QT会自动调用paintEvent。

什么时候会触发重绘?

  • 窗口第一次显示时
  • 窗口大小改变时
  • 窗口被遮挡后重新显示时
  • 调用update()repaint()

1.3 基本用法

cpp 复制代码
#include <QPainter>
#include <QPaintEvent>

void MyWidget::paintEvent(QPaintEvent *event)
{
    Q_UNUSED(event);  // 这个参数暂时用不到,先忽略
    
    // 1. 创建QPainter对象,传入this(在当前控件上画)
    QPainter painter(this);
    
    // 2. 设置画笔(线条的颜色、粗细等)
    painter.setPen(QPen(Qt::blue, 2));
    
    // 3. 设置画刷(填充的颜色)
    painter.setBrush(QBrush(Qt::yellow));
    
    // 4. 画图
    painter.drawRect(50, 50, 200, 100);
}

⚠️ 注意:QPainter对象要在paintEvent里创建,不要在外面创建。

二、基本图形绘制

2.1 画直线 drawLine

cpp 复制代码
// 画一条从(50, 50)到(200, 200)的直线
painter.drawLine(50, 50, 200, 200);

// 也可以用QPoint
painter.drawLine(QPoint(50, 50), QPoint(200, 200));

2.2 画矩形 drawRect

cpp 复制代码
// 画矩形:左上角坐标(50, 50),宽200,高100
painter.drawRect(50, 50, 200, 100);

// 也可以用QRect
painter.drawRect(QRect(50, 50, 200, 100));

2.3 画圆角矩形 drawRoundedRect

cpp 复制代码
// 圆角矩形:x, y, w, h, 圆角x半径, 圆角y半径
painter.drawRoundedRect(50, 50, 200, 100, 20, 20);

2.4 画椭圆/圆 drawEllipse

cpp 复制代码
// 画椭圆:外接矩形的左上角(50, 50),宽200,高100
painter.drawEllipse(50, 50, 200, 100);

// 画圆:宽和高相等就是圆
painter.drawEllipse(100, 100, 100, 100);  // 圆心(150, 150),半径50

💡 小技巧:drawEllipse的参数是"外接矩形",不是圆心+半径。如果想按圆心画,可以这样算:

cpp 复制代码
int cx = 150, cy = 150, r = 50;  // 圆心(150,150),半径50
painter.drawEllipse(cx - r, cy - r, 2*r, 2*r);

2.5 画多边形 drawPolygon

cpp 复制代码
// 定义多边形的点
QPolygon polygon;
polygon << QPoint(100, 100);
polygon << QPoint(200, 50);
polygon << QPoint(300, 100);
polygon << QPoint(250, 200);
polygon << QPoint(150, 200);

// 画多边形(自动闭合)
painter.drawPolygon(polygon);

2.6 画弧线 drawArc

cpp 复制代码
// 画弧线:外接矩形, 起始角度, 跨越角度
// 角度单位是1/16度,90度就是 90*16 = 1440
// 0度在3点钟方向,逆时针为正
painter.drawArc(50, 50, 200, 200, 0, 90 * 16);  // 从0度画90度

2.7 画扇形 drawPie

cpp 复制代码
// 扇形:和弧线类似,但会连接圆心
painter.drawPie(50, 50, 200, 200, 30 * 16, 120 * 16);

2.8 画弦 drawChord

cpp 复制代码
// 弦:弧线的两个端点连起来
painter.drawChord(50, 50, 200, 200, 0, 120 * 16);

2.9 画点 drawPoint

cpp 复制代码
painter.drawPoint(100, 100);

// 画多个点
QPoint points[] = {QPoint(10, 10), QPoint(20, 20), QPoint(30, 30)};
painter.drawPoints(points, 3);

三、QPen 画笔

QPen控制"线条"的样式:颜色、粗细、线型、端点样式、连接样式。

3.1 基本设置

cpp 复制代码
// 创建画笔:蓝色,线宽2像素
QPen pen(Qt::blue, 2);

// 设置给painter
painter.setPen(pen);

// 也可以直接设置
painter.setPen(QPen(Qt::red, 3));

3.2 颜色

cpp 复制代码
pen.setColor(Qt::red);           // 预定义颜色
pen.setColor(QColor(255, 0, 0)); // RGB
pen.setColor(QColor("#FF0000"));  // 十六进制
pen.setColor(QColor(255, 0, 0, 128)); // RGBA,带透明度

QT预定义的颜色有很多:Qt::red, Qt::green, Qt::blue, Qt::black, Qt::white, Qt::yellow, Qt::cyan, Qt::magenta, Qt::gray, Qt::darkGray, Qt::lightGray 等等。

3.3 线宽

cpp 复制代码
pen.setWidth(3);  // 线宽3像素
pen.setWidthF(2.5);  // 支持小数

3.4 线条样式

cpp 复制代码
pen.setStyle(Qt::SolidLine);      // 实线(默认)
pen.setStyle(Qt::DashLine);       // 虚线
pen.setStyle(Qt::DotLine);        // 点线
pen.setStyle(Qt::DashDotLine);    // 点划线
pen.setStyle(Qt::DashDotDotLine); // 双点划线
pen.setStyle(Qt::NoPen);          // 没有线(不画边框)

3.5 端点样式

cpp 复制代码
pen.setCapStyle(Qt::FlatCap);    // 平端点(默认)
pen.setCapStyle(Qt::SquareCap);  // 方端点(延长一点)
pen.setCapStyle(Qt::RoundCap);   // 圆端点

3.6 连接样式

cpp 复制代码
pen.setJoinStyle(Qt::MiterJoin);  // 斜接(默认,尖角)
pen.setJoinStyle(Qt::BevelJoin);  // 斜切
pen.setJoinStyle(Qt::RoundJoin);  // 圆角连接

四、QBrush 画刷

QBrush控制"填充"的样式:颜色、图案、渐变等。

4.1 纯色填充

cpp 复制代码
// 创建画刷:红色
QBrush brush(Qt::red);
painter.setBrush(brush);

// 直接设置
painter.setBrush(QBrush(Qt::yellow));

4.2 填充样式

cpp 复制代码
brush.setStyle(Qt::SolidPattern);      // 纯色填充(默认)
brush.setStyle(Qt::NoBrush);           // 不填充
brush.setStyle(Qt::HorPattern);        // 水平线
brush.setStyle(Qt::VerPattern);        // 垂直线
brush.setStyle(Qt::CrossPattern);      // 十字线
brush.setStyle(Qt::BDiagPattern);      // 左斜线
brush.setStyle(Qt::FDiagPattern);      // 右斜线
brush.setStyle(Qt::DiagCrossPattern);  // 交叉斜线
brush.setStyle(Qt::Dense1Pattern);     // 密点1
brush.setStyle(Qt::Dense2Pattern);     // 密点2
// ... 还有 Dense3 ~ Dense7

4.3 渐变填充(后面详细讲)

cpp 复制代码
QLinearGradient gradient(0, 0, 200, 200);
gradient.setColorAt(0, Qt::red);
gradient.setColor(1, Qt::blue);
painter.setBrush(gradient);

4.4 纹理填充

cpp 复制代码
brush.setTexture(QPixmap(":/images/texture.png"));

五、画文字 drawText

5.1 基本用法

cpp 复制代码
// 在指定位置画文字
painter.drawText(50, 50, "Hello QT!");

// 在指定矩形内画文字
painter.drawText(QRect(50, 50, 200, 100), Qt::AlignCenter, "居中显示的文字");

5.2 设置字体

cpp 复制代码
QFont font;
font.setFamily("微软雅黑");  // 字体
font.setPointSize(16);       // 字号
font.setBold(true);          // 加粗
font.setItalic(true);        // 斜体
font.setUnderline(true);     // 下划线
font.setStrikeOut(true);     // 删除线

painter.setFont(font);

5.3 文字颜色

文字颜色是由画笔控制的:

cpp 复制代码
painter.setPen(QPen(Qt::red));  // 文字红色
painter.drawText(50, 50, "红色文字");

5.4 对齐方式

cpp 复制代码
// 水平对齐
Qt::AlignLeft      // 左对齐
Qt::AlignRight     // 右对齐
Qt::AlignHCenter   // 水平居中
Qt::AlignJustify   // 两端对齐

// 垂直对齐
Qt::AlignTop       // 顶部
Qt::AlignBottom    // 底部
Qt::AlignVCenter   // 垂直居中

// 组合使用
Qt::AlignCenter    // 水平垂直都居中(= AlignHCenter | AlignVCenter)

5.5 测量文字大小

有时候需要知道文字有多宽多高,用来布局:

cpp 复制代码
QFontMetrics fm(font);
int width = fm.horizontalAdvance("Hello");  // 文字宽度
int height = fm.height();                    // 文字高度

六、画图片

6.1 drawPixmap

cpp 复制代码
#include <QPixmap>

// 加载图片
QPixmap pixmap(":/images/logo.png");

// 在(50, 50)位置绘制原图
painter.drawPixmap(50, 50, pixmap);

// 绘制到指定大小(自动缩放)
painter.drawPixmap(50, 50, 200, 100, pixmap);

// 只画图片的一部分(裁剪)
painter.drawPixmap(50, 50, pixmap, 0, 0, 100, 100);

6.2 drawImage

QImage和QPixmap类似,但QImage更底层,可以直接操作像素:

cpp 复制代码
#include <QImage>

QImage image(":/images/photo.jpg");
painter.drawImage(50, 50, image);

💡 区别:

  • QPixmap:适合显示,针对屏幕显示优化
  • QImage:适合编辑,可以直接读写像素
  • 一般显示图片用QPixmap就够了

七、坐标变换

QPainter支持坐标变换,可以平移、旋转、缩放、扭曲坐标系。

7.1 平移 translate

cpp 复制代码
painter.translate(100, 100);  // 把原点移到(100, 100)
painter.drawRect(0, 0, 50, 50);  // 实际画在(100, 100)的位置

7.2 旋转 rotate

cpp 复制代码
painter.translate(150, 150);     // 先移到中心点
painter.rotate(45);               // 旋转45度(顺时针)
painter.drawRect(-50, -50, 100, 100);  // 画一个旋转了45度的正方形

⚠️ 注意:rotate是绕原点旋转的,所以一般先translate把原点移到旋转中心,再rotate。

7.3 缩放 scale

cpp 复制代码
painter.scale(2, 1.5);  // x方向放大2倍,y方向放大1.5倍
painter.drawRect(0, 0, 50, 50);  // 实际大小是 100 x 75

7.4 扭曲 shear

cpp 复制代码
painter.shear(0.5, 0);  // x方向扭曲
painter.drawRect(0, 0, 100, 100);

7.5 save() 和 restore()

坐标变换是累积的,变换一次后后面的画图都会受影响。如果只想让某个图形应用变换,可以用save()保存状态,画完再restore()恢复:

cpp 复制代码
painter.save();              // 保存当前状态
painter.translate(100, 100);
painter.rotate(30);
painter.drawRect(0, 0, 50, 50);  // 这个矩形会平移+旋转
painter.restore();           // 恢复状态

painter.drawRect(0, 0, 50, 50);  // 这个矩形不受影响

💡 好习惯:每次变换前save,画完restore,避免影响后面的绘图。

八、抗锯齿

默认情况下,画出来的线条会有锯齿(马赛克感)。开启抗锯齿后,边缘会更平滑。

cpp 复制代码
// 开启抗锯齿
painter.setRenderHint(QPainter::Antialiasing);

// 文字抗锯齿
painter.setRenderHint(QPainter::TextAntialiasing);

// 平滑变换(图片缩放时更平滑)
painter.setRenderHint(QPainter::SmoothPixmapTransform);

💡 建议:一般都把抗锯齿打开,画出来的效果好看很多。性能影响不大,不用太担心。

九、渐变

QT支持三种渐变:线性渐变、径向渐变、锥形渐变。渐变可以作为画刷来填充图形。

9.1 线性渐变 QLinearGradient

从一条线到另一条线的渐变:

cpp 复制代码
// 从(0, 0)到(200, 200)的线性渐变
QLinearGradient gradient(0, 0, 200, 200);

// 设置渐变颜色点(0是起点,1是终点)
gradient.setColorAt(0, Qt::red);
gradient.setColorAt(0.5, Qt::yellow);
gradient.setColorAt(1, Qt::blue);

// 作为画刷
painter.setBrush(gradient);
painter.setPen(Qt::NoPen);
painter.drawRect(0, 0, 200, 200);

9.2 径向渐变 QRadialGradient

从圆心向外辐射的渐变:

cpp 复制代码
// 圆心(100, 100),半径100
QRadialGradient gradient(100, 100, 100);

gradient.setColorAt(0, Qt::white);    // 中心白色
gradient.setColor(1, Qt::blue);       // 边缘蓝色

painter.setBrush(gradient);
painter.setPen(Qt::NoPen);
painter.drawEllipse(0, 0, 200, 200);

9.3 锥形渐变 QConicalGradient

绕一个点旋转的渐变:

cpp 复制代码
// 中心(100, 100),起始角度0度
QConicalGradient gradient(100, 100, 0);

gradient.setColorAt(0, Qt::red);
gradient.setColorAt(0.5, Qt::green);
gradient.setColorAt(1, Qt::blue);

painter.setBrush(gradient);
painter.setPen(Qt::NoPen);
painter.drawEllipse(0, 0, 200, 200);

9.4 渐变扩散方式

渐变还可以设置超出范围时的扩散方式:

cpp 复制代码
gradient.setSpread(QGradient::PadSpread);      // 填充(默认)
gradient.setSpread(QGradient::RepeatSpread);   // 重复
gradient.setSpread(QGradient::ReflectSpread);  // 镜像反射

十、综合实战:绘制漂亮的仪表盘

我们来画一个漂亮的仪表盘,综合运用今天学的知识。

10.1 完整代码

cpp 复制代码
#include <QWidget>
#include <QPainter>
#include <QPaintEvent>
#include <QTimer>

class GaugeWidget : public QWidget
{
    Q_OBJECT
public:
    GaugeWidget(QWidget *parent = nullptr) : QWidget(parent) {
        m_value = 0;
        m_targetValue = 60;
        
        // 定时器,让指针动起来
        QTimer *timer = new QTimer(this);
        connect(timer, &QTimer::timeout, this, [=](){
            if (m_value < m_targetValue) {
                m_value += 1;
                update();  // 触发重绘
            } else {
                timer->stop();
            }
        });
        timer->start(20);
        
        setWindowTitle("漂亮仪表盘");
        resize(400, 400);
    }
    
    void setValue(int value) {
        m_targetValue = value;
    }

protected:
    void paintEvent(QPaintEvent *event) override {
        Q_UNUSED(event);
        QPainter painter(this);
        painter.setRenderHint(QPainter::Antialiasing);
        
        int w = width();
        int h = height();
        int side = qMin(w, h);
        
        // 平移到中心,缩放坐标系
        painter.translate(w / 2, h / 2);
        painter.scale(side / 200.0, side / 200.0);
        
        // ===== 1. 画外框(渐变圆环)=====
        QRadialGradient outerGradient(0, 0, 100, 0, 0, 90);
        outerGradient.setColorAt(0, QColor(60, 60, 80));
        outerGradient.setColorAt(1, QColor(30, 30, 50));
        painter.setBrush(outerGradient);
        painter.setPen(Qt::NoPen);
        painter.drawEllipse(-95, -95, 190, 190);
        
        // ===== 2. 画内圈(深色)=====
        QRadialGradient innerGradient(0, 0, 85, 0, 0, 80);
        innerGradient.setColorAt(0, QColor(40, 40, 60));
        innerGradient.setColorAt(1, QColor(20, 20, 40));
        painter.setBrush(innerGradient);
        painter.drawEllipse(-85, -85, 170, 170);
        
        // ===== 3. 画刻度 =====
        painter.save();
        painter.rotate(-135);  // 从-135度开始
        
        QPen tickPen;
        tickPen.setCapStyle(Qt::RoundCap);
        
        for (int i = 0; i <= 100; i++) {
            if (i % 10 == 0) {
                // 长刻度
                tickPen.setColor(QColor(200, 200, 220));
                tickPen.setWidth(2);
                painter.setPen(tickPen);
                painter.drawLine(0, -75, 0, -60);
                
                // 刻度数字
                painter.save();
                painter.translate(0, -50);
                painter.rotate(135 - i * 2.7);  // 转回来让文字正的
                QFont font;
                font.setPointSize(8);
                font.setBold(true);
                painter.setFont(font);
                painter.setPen(QColor(200, 200, 220));
                painter.drawText(QRect(-15, -8, 30, 16), Qt::AlignCenter, QString::number(i));
                painter.restore();
                
            } else if (i % 5 == 0) {
                // 中刻度
                tickPen.setColor(QColor(150, 150, 180));
                tickPen.setWidth(1.5);
                painter.setPen(tickPen);
                painter.drawLine(0, -72, 0, -62);
            } else {
                // 短刻度
                tickPen.setColor(QColor(100, 100, 140));
                tickPen.setWidth(1);
                painter.setPen(tickPen);
                painter.drawLine(0, -70, 0, -65);
            }
            
            painter.rotate(2.7);  // 总共270度,100格,每格2.7度
        }
        painter.restore();
        
        // ===== 4. 画数值范围颜色条 =====
        painter.save();
        painter.rotate(-135);
        
        for (int i = 0; i < 100; i++) {
            QColor color;
            if (i < 60) {
                color = QColor(0, 200, 100);  // 绿色
            } else if (i < 80) {
                color = QColor(255, 200, 0);  // 黄色
            } else {
                color = QColor(255, 80, 80);   // 红色
            }
            
            if (i <= m_value) {
                painter.setPen(QPen(color, 3));
            } else {
                painter.setPen(QPen(QColor(50, 50, 70), 3));
            }
            
            painter.drawLine(0, -80, 0, -77);
            painter.rotate(2.7);
        }
        painter.restore();
        
        // ===== 5. 画指针 =====
        painter.save();
        painter.rotate(-135 + m_value * 2.7);
        
        // 指针主体
        QLinearGradient needleGradient(0, -70, 0, 10);
        needleGradient.setColorAt(0, QColor(255, 100, 100));
        needleGradient.setColorAt(1, QColor(200, 50, 50));
        
        painter.setBrush(needleGradient);
        painter.setPen(Qt::NoPen);
        
        QPolygon needle;
        needle << QPoint(-3, 10);
        needle << QPoint(0, -70);
        needle << QPoint(3, 10);
        painter.drawPolygon(needle);
        
        painter.restore();
        
        // ===== 6. 画中心圆 =====
        QRadialGradient centerGradient(0, 0, 15, 0, -5, 5);
        centerGradient.setColorAt(0, QColor(80, 80, 100));
        centerGradient.setColorAt(1, QColor(40, 40, 60));
        painter.setBrush(centerGradient);
        painter.setPen(QPen(QColor(20, 20, 40), 2));
        painter.drawEllipse(-12, -12, 24, 24);
        
        // ===== 7. 画数值显示 =====
        painter.setPen(QColor(200, 200, 220));
        QFont valueFont;
        valueFont.setPointSize(16);
        valueFont.setBold(true);
        painter.setFont(valueFont);
        painter.drawText(QRect(-50, 20, 100, 30), Qt::AlignCenter, QString::number(m_value));
        
        QFont unitFont;
        unitFont.setPointSize(7);
        painter.setFont(unitFont);
        painter.setPen(QColor(150, 150, 180));
        painter.drawText(QRect(-50, 45, 100, 20), Qt::AlignCenter, "km/h");
    }

private:
    int m_value;
    int m_targetValue;
};

10.2 运行效果

运行后你会看到一个非常漂亮的仪表盘:

  • 深色渐变背景,很有科技感
  • 绿色→黄色→红色的颜色条
  • 指针从0开始动画到60
  • 中心有数值显示
  • 刻度和数字清晰

只用了一个paintEvent,就画出了这么炫酷的效果!

十一、今日总结

今天我们学习了QT的绘图系统QPainter,这是做自定义控件和炫酷效果的基础。

知识点汇总

类别 内容 常用方法/类
基本图形 直线、矩形、圆、椭圆、多边形、弧线 drawLine, drawRect, drawEllipse, drawPolygon
画笔 线条颜色、粗细、样式 QPen, setColor, setWidth, setStyle
画刷 填充颜色、图案、渐变 QBrush, setColor, setStyle, QGradient
文字 字体、大小、对齐 drawText, QFont, QFontMetrics
图片 绘制图片 drawPixmap, drawImage, QPixmap
坐标变换 平移、旋转、缩放 translate, rotate, scale, save/restore
效果 抗锯齿 setRenderHint, Antialiasing
渐变 线性、径向、锥形 QLinearGradient, QRadialGradient, QConicalGradient

重要概念

  • paintEvent:绘图都在这里面进行
  • QPainter:画笔,在绘图设备上画画
  • QPen:控制线条样式
  • QBrush:控制填充样式
  • 坐标变换:translate/rotate/scale,记得save和restore
  • 抗锯齿:打开后效果更平滑
  • 渐变:三种渐变方式,做出炫酷效果

经验分享

  1. 先平移再旋转:旋转是绕原点转的,想绕某个点转,先把原点移过去
  2. 善用save/restore:变换前保存,画完恢复,避免影响后面的绘图
  3. 用相对坐标:尽量用相对坐标(比如以中心为原点),这样控件缩放时不会变形
  4. 开启抗锯齿:一般都打开,效果好很多
  5. 复杂图形分步骤画:先画背景,再画中间层,最后画前景,一层一层来
  6. update()触发重绘:数据变了调用update(),不要直接调用paintEvent

十二、明日预告

明天我们将学习QT的文件操作(QFile、QTextStream、QDataStream)

文件操作是编程的基本功,我们会学习:

  • QFile读写文本文件
  • QTextStream文本流
  • QDataStream数据流(读写二进制)
  • QFileInfo文件信息
  • QDir目录操作
  • 文件和目录的增删改查

学会文件操作,你就能保存和读取数据了!


📝 学习建议:绘图是QT的重点也是难点,多动手画才能掌握。

练习建议:

  • 把今天的仪表盘代码敲一遍运行看看
  • 试试画一个进度条(圆形的)
  • 试试画一个饼图
  • 试试画一个笑脸表情

绘图掌握了,你就能做出各种自定义控件和炫酷效果了!明天见,继续加油!💪