一、QPainter绘图:从基础到进阶

绘图,说白了就是把数据变成画面。在Qt里,QPainter就是那个拿画笔的人。我刚开始学Qt时,觉得绘图很神秘,后来才发现,它其实就是一套固定的流程——你告诉QPainter画什么、怎么画,它就帮你搞定。

这一章,我会带你从零开始,把QPainter的底裤都扒干净。嗯,准备好了吗?

1.1 绘图基础:QPainter的启动与关闭

QPainter不能单独存在,它必须依附在一个“画布”上。这个画布通常是QWidget、QPixmap或者QImage。我习惯把QPainter想象成一个临时工——你把它叫来,它画完就走,不能赖着不走。

基本流程就三步:

  • 创建QPainter对象,指定画布
  • 调用绘图函数(画线、画圆、画文字等)
  • 销毁QPainter对象(或调用end())

看个最简单的例子:

void MyWidget::paintEvent(QPaintEvent *)
{
    QPainter painter(this);  // 把画家请到当前窗口上
    painter.drawLine(10, 10, 100, 100);  // 画一条从(10,10)到(100,100)的线
    // painter 离开作用域时自动析构,相当于自动收工
}

这里有个坑,我曾经踩过:千万别在paintEvent之外调用QPainter的绘图函数。为什么?因为Qt的绘图系统是事件驱动的,你强行在别处画,要么画不上,要么程序崩。记住,paintEvent才是画家的主场。

我的小习惯: 如果要在paintEvent里多次绘图,我会用QPainter painter(this);一次性创建,然后连续调用drawXXX。不要画一条线就创建一次QPainter,那样效率低,而且容易出问题。

1.2 画笔与画刷:线条与填充的灵魂

QPainter有两大法宝:画笔(QPen)画刷(QBrush)。画笔管线条(轮廓),画刷管填充(内部)。我刚开始时总搞混,后来记住一句话:“笔是线,刷是面”

画笔可以设置颜色、宽度、样式(实线、虚线、点线等)。画刷可以设置颜色、样式(纯色、渐变、纹理等)。

看个例子:

void MyWidget::paintEvent(QPaintEvent *)
{
    QPainter painter(this);
    
    // 设置画笔:红色,3像素宽,实线
    QPen pen(Qt::red, 3, Qt::SolidLine);
    painter.setPen(pen);
    
    // 设置画刷:蓝色,纯色填充
    QBrush brush(Qt::blue, Qt::SolidPattern);
    painter.setBrush(brush);
    
    // 画一个矩形,轮廓红色,内部蓝色
    painter.drawRect(50, 50, 200, 100);
}

你想想看,如果只设置画笔不设置画刷,那图形就只有轮廓,内部是透明的。反过来,只设置画刷不设置画笔,图形就只有填充,没有轮廓。两者配合,才能画出完整的图形。

避坑指南: 我曾经在画一个圆时,忘了设置画刷,结果圆内部是透明的,和背景混在一起,调试了半天才发现。所以,画封闭图形时,记得检查画刷

1.3 绘制基本图形:点、线、矩形、椭圆、圆弧

QPainter提供了丰富的drawXXX函数,覆盖了几乎所有基本图形。我列个表,方便你查阅:

函数 作用 示例
drawPoint(x, y) 画一个点 painter.drawPoint(10, 20);
drawLine(x1, y1, x2, y2) 画一条直线 painter.drawLine(0, 0, 100, 100);
drawRect(x, y, w, h) 画矩形 painter.drawRect(10, 10, 80, 60);
drawEllipse(x, y, w, h) 画椭圆(w=h时是圆) painter.drawEllipse(10, 10, 50, 50);
drawArc(x, y, w, h, startAngle, spanAngle) 画圆弧(角度单位是1/16度) painter.drawArc(10, 10, 100, 100, 0, 90*16);
drawChord(x, y, w, h, startAngle, spanAngle) 画弦(圆弧+连接两端点的线) painter.drawChord(10, 10, 100, 100, 0, 90*16);
drawPie(x, y, w, h, startAngle, spanAngle) 画扇形(圆弧+圆心连线) painter.drawPie(10, 10, 100, 100, 0, 90*16);

注意:drawArc、drawChord、drawPie的角度单位是1/16度。也就是说,90度要写成90*16。这个设计有点反人类,但Qt就是这么定的,习惯就好。我刚开始时总忘记乘以16,画出来的圆弧永远不对。

另外,drawEllipse的宽高参数是外接矩形的宽高。如果你想画一个圆心在(50,50)、半径为30的圆,应该这样:

painter.drawEllipse(QPointF(50, 50), 30, 30);

这个重载版本更直观,我推荐你用这个。

1.4 渐变与填充:让图形活起来

纯色填充太单调了。Qt提供了三种渐变:线性渐变(QLinearGradient)径向渐变(QRadialGradient)锥形渐变(QConicalGradient)。说白了,就是让颜色从A平滑过渡到B。

我项目中用得最多的是线性渐变,比如做一个按钮的悬停效果:

void MyWidget::paintEvent(QPaintEvent *)
{
    QPainter painter(this);
    
    // 线性渐变:从(0,0)到(100,0),即从左到右
    QLinearGradient gradient(0, 0, 100, 0);
    gradient.setColorAt(0.0, Qt::red);   // 起点红色
    gradient.setColorAt(0.5, Qt::green); // 中间绿色
    gradient.setColorAt(1.0, Qt::blue);  // 终点蓝色
    
    painter.setBrush(gradient);
    painter.drawRect(10, 10, 200, 100);
}

setColorAt的第一个参数是位置(0.0到1.0),第二个参数是颜色。你可以设置任意多个颜色节点,Qt会自动插值。

径向渐变是从一个点向外扩散:

QRadialGradient gradient(50, 50, 50); // 圆心(50,50),半径50
gradient.setColorAt(0.0, Qt::white);
gradient.setColorAt(1.0, Qt::black);

锥形渐变是围绕一个点旋转:

QConicalGradient gradient(50, 50, 0); // 中心(50,50),起始角度0度
gradient.setColorAt(0.0, Qt::red);
gradient.setColorAt(1.0, Qt::blue);
注意: 渐变对象必须在设置给画刷之前完成所有颜色节点的设置。一旦设置给画刷,再修改渐变是无效的。我吃过这个亏,后来都是先配好渐变,再设给画刷。

1.5 抗锯齿与渲染:让边缘不再“狗啃”

默认情况下,Qt绘图是不开抗锯齿的。你画一条斜线,边缘会有明显的锯齿,像楼梯一样。我刚开始做UI时,客户总说“这个边缘怎么毛毛的”,其实就是没开抗锯齿。

开启抗锯齿很简单:

painter.setRenderHint(QPainter::Antialiasing, true);

就这么一行,画出来的线条就平滑多了。代价是性能稍微下降一点,但现代硬件完全扛得住。

除了抗锯齿,还有几个渲染提示:

  • QPainter::Antialiasing:抗锯齿,最常用
  • QPainter::TextAntialiasing:文字抗锯齿,默认开启
  • QPainter::SmoothPixmapTransform:图片缩放时平滑处理
  • QPainter::HighQualityAntialiasing:高质量抗锯齿(Qt5.14+)

我一般这样写:

painter.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform, true);

一次性开启多个。注意,渲染提示要在绘图之前设置,否则可能不生效。

我的经验: 如果你画的是水平或垂直线,开不开抗锯齿区别不大。但画斜线、圆弧、椭圆时,一定要开抗锯齿,否则效果惨不忍睹。另外,如果你在paintEvent里频繁切换渲染提示,会影响性能,建议在paintEvent开头一次性设好。

1.6 知识体系总览

下面这张图,是我为你梳理的本章知识结构。你可以把它当作一个地图,随时回来查看:

QPainter绘图 绘图基础 paintEvent QPainter创建/销毁 画笔与画刷 QPen: 颜色/宽度/样式 QBrush: 颜色/样式/渐变 基本图形 点/线/矩形/椭圆 圆弧/弦/扇形 渐变与填充 线性/径向/锥形 setColorAt() 抗锯齿与渲染 setRenderHint() Antialiasing 核心:paintEvent → 设置画笔/画刷 → 绘图 → 开启抗锯齿

这张图把本章的五个知识点串起来了。你可以看到,所有操作都围绕paintEvent展开,画笔和画刷是基础,基本图形是输出,渐变让图形更丰富,抗锯齿让输出更漂亮。

1.7 综合示例:画一个带渐变的圆角矩形

最后,我写一个综合示例,把今天讲的东西都用上:

void MyWidget::paintEvent(QPaintEvent *)
{
    QPainter painter(this);
    painter.setRenderHint(QPainter::Antialiasing, true);
    
    // 线性渐变
    QLinearGradient gradient(0, 0, 0, 100);
    gradient.setColorAt(0.0, QColor(100, 200, 255));
    gradient.setColorAt(1.0, QColor(30, 80, 160));
    
    // 画笔:白色,2像素宽
    QPen pen(Qt::white, 2);
    painter.setPen(pen);
    painter.setBrush(gradient);
    
    // 画圆角矩形
    painter.drawRoundedRect(50, 50, 200, 100, 20, 20);
    
    // 画文字
    painter.setPen(Qt::white);
    painter.setFont(QFont("微软雅黑", 16));
    painter.drawText(QRect(50, 50, 200, 100), Qt::AlignCenter, "Hello Qt!");
}

这段代码画了一个带渐变填充的圆角矩形,中间写了白色文字。你运行一下,效果应该很漂亮。嗯,这就是QPainter的魅力——几行代码,就能画出专业的界面。

总结一下: QPainter绘图并不难,关键是掌握paintEvent的用法,理解画笔和画刷的区别,熟悉基本图形的绘制函数,再学会用渐变和抗锯齿提升效果。多练几次,你就能画出任何你想要的图形。

好了,这一章就到这里。记住,绘图是UI的基础,也是Qt最有趣的部分之一。下一章,我们会深入更高级的绘图技巧,比如坐标变换和路径绘制。但今天,先把这些基础打牢。


公众号:蓝海资料掘金营,微信deep3321