3. 基本图形绘制:直线、矩形、椭圆、圆弧、多边形、路径

好,咱们今天来聊聊 Qt 绘图里最基础、也最核心的东西——基本图形的绘制。

说实话,我刚开始接触 Qt 绘图时,觉得这玩意儿不就是画几个形状嘛,有啥难的?后来真正做项目才发现,越是基础的东西,坑越多。你想想看,一个界面好不好看,交互流不流畅,很大程度上就取决于这些基本图形画得对不对、快不快。

3.1 绘制直线

直线是最简单的图形。在 Qt 里,我们用 QPainter::drawLine() 来画。

void Widget::paintEvent(QPaintEvent *)
{
    QPainter painter(this);
    painter.setPen(QPen(Qt::blue, 2));

    // 画一条从 (20, 20) 到 (200, 20) 的直线
    painter.drawLine(20, 20, 200, 20);

    // 也可以用 QPoint 来指定
    QPoint p1(20, 50);
    QPoint p2(200, 50);
    painter.drawLine(p1, p2);
}

嗯,这里要注意:drawLine 不包含终点像素。什么意思呢?就是如果你画一条从 (0,0) 到 (10,0) 的线,它实际上只画了 10 个像素,而不是 11 个。我在做波形图控件时,就因为这个细节导致线条末端总是缺一截,排查了半天。

小技巧:如果你需要画多条连续的线段,别一条一条画。用 drawPolyline() 或者 QPainterPath 效率更高。

3.2 绘制矩形

矩形在 UI 里太常见了——按钮背景、边框、进度条,到处都是。

void Widget::paintEvent(QPaintEvent *)
{
    QPainter painter(this);
    painter.setPen(QPen(Qt::red, 2));
    painter.setBrush(QBrush(Qt::yellow));

    // 画一个普通矩形
    painter.drawRect(20, 20, 150, 100);

    // 画圆角矩形
    painter.drawRoundedRect(200, 20, 150, 100, 10, 10);
}

我个人习惯用 QRect 来管理矩形区域,这样代码更清晰。比如:

QRect rect(20, 20, 150, 100);
painter.drawRect(rect);
注意:如果你设置了 setBrush,矩形内部会被填充。如果不想要填充,记得设置 setBrush(Qt::NoBrush)。我曾经在做一个图表控件时,忘了取消画刷,结果所有矩形都带了个白色背景,把底下的网格线全盖住了。

3.3 绘制椭圆和圆

椭圆其实是用矩形来定义的。你给一个矩形,Qt 就在这个矩形内画一个内切椭圆。

void Widget::paintEvent(QPaintEvent *)
{
    QPainter painter(this);
    painter.setPen(QPen(Qt::green, 2));

    // 画椭圆
    painter.drawEllipse(20, 20, 150, 100);  // 宽150,高100

    // 画正圆(宽高相等)
    painter.drawEllipse(200, 20, 100, 100); // 直径100的圆
}

说白了,drawEllipse 的参数和 drawRect 是一样的。你给一个矩形区域,它就在里面画椭圆。所以想画正圆?让宽高相等就行。

我建议你记住这个规律:所有用矩形定义的图形,都可以用 QRect 来管理。这样代码复用性会好很多。

3.4 绘制圆弧

圆弧稍微复杂一点。它需要指定起始角度和跨度角度。

void Widget::paintEvent(QPaintEvent *)
{
    QPainter painter(this);
    painter.setPen(QPen(Qt::magenta, 2));

    QRect rect(20, 20, 150, 150);

    // 从0度开始,画90度的弧
    int startAngle = 0 * 16;      // 起始角度(单位:1/16度)
    int spanAngle = 90 * 16;      // 跨度角度(单位:1/16度)

    painter.drawArc(rect, startAngle, spanAngle);
}

为什么角度要乘以 16?这是 Qt 的历史遗留设计。角度单位是 1/16 度,所以 90 度要写成 90 * 16。说实话,我第一次用的时候也觉得这个设计挺奇怪的,但用习惯了也就那样。

核心要点:
  • startAngle:起始角度,0 度在 3 点钟方向
  • spanAngle:跨度角度,正值为逆时针,负值为顺时针
  • 角度单位:1/16 度,所以 360 度 = 360 * 16

3.5 绘制多边形

多边形用 QPolygon 来定义,然后传给 drawPolygon。

void Widget::paintEvent(QPaintEvent *)
{
    QPainter painter(this);
    painter.setPen(QPen(Qt::darkCyan, 2));
    painter.setBrush(QBrush(Qt::lightGray));

    QPolygon polygon;
    polygon << QPoint(50, 10)
            << QPoint(10, 80)
            << QPoint(90, 80);

    painter.drawPolygon(polygon);
}

这里有个细节:drawPolygon 会自动闭合图形,也就是把最后一个点和第一个点连起来。如果你不想闭合,用 drawPolyline。

我在做地图标注控件时,经常用多边形来画各种形状的标记。比如三角形箭头、五角星什么的。用 QPolygon 配合数学计算,可以画出任意形状。

3.6 绘制路径

路径是 Qt 绘图里最强大的工具。它可以把直线、曲线、圆弧组合成一个复杂的图形。

void Widget::paintEvent(QPaintEvent *)
{
    QPainter painter(this);
    painter.setPen(QPen(Qt::darkRed, 2));
    painter.setBrush(QBrush(Qt::lightYellow));

    QPainterPath path;
    path.moveTo(20, 20);           // 移动到起点
    path.lineTo(100, 20);          // 画直线到 (100, 20)
    path.quadTo(150, 20, 150, 70); // 画二次贝塞尔曲线到 (150, 70)
    path.cubicTo(150, 120, 100, 120, 100, 70); // 画三次贝塞尔曲线
    path.closeSubpath();           // 闭合路径

    painter.drawPath(path);
}

路径的好处是:你可以先定义好整个形状,然后一次性画出来。而且路径支持各种变换,比如平移、旋转、缩放。

我的经验:如果你要画一个复杂的自定义控件,比如仪表盘、波形图、自定义按钮,用 QPainterPath 是最优雅的方式。它比一条条画线要清晰得多,也更容易维护。

3.7 知识体系总览

下面这张图总结了本章的核心内容,你可以把它当作一个快速参考。

基本图形绘制知识体系 QPainter 基本图形 drawLine 直线 drawRect 矩形 drawEllipse 椭圆 drawArc 圆弧 drawPolygon 多边形 drawPath 路径 核心:所有图形都依赖 QPen(轮廓)和 QBrush(填充) 路径是最强大的组合工具,推荐复杂图形优先使用

3.8 避坑指南

最后,分享几个我踩过的坑:

  • 画笔宽度为奇数时,线条会模糊——这是因为 Qt 默认的对齐方式导致的。解决办法:把坐标偏移 0.5 像素,或者用偶数宽度。
  • 圆弧角度搞反了——记住:正角度是逆时针,负角度是顺时针。我刚开始总是记反。
  • 路径不闭合时填充效果很奇怪——如果你用 drawPath 并且设置了画刷,路径会自动闭合后再填充。如果不想要这个效果,用 setBrush(Qt::NoBrush)。
  • 性能问题——如果你在 paintEvent 里频繁创建 QPen、QBrush、QPainterPath 对象,会导致性能下降。我建议把它们缓存成成员变量,只在需要改变时才重新创建。

好了,基本图形就讲到这里。这些内容虽然基础,但它们是所有复杂绘图的基础。你把这些练熟了,后面学渐变、变换、文本绘制就会轻松很多。

可可精品资料库