跳到主要内容

绘图与图形

Qt 提供了强大的 2D 绘图系统,允许你在控件、图片、打印机等设备上绘制各种图形。本章将详细介绍 QPainter 绑定系统的使用方法。

绑定系统概述

Qt 的绑定系统由三个核心类组成:

  • QPainter:执行绑定操作的核心类,提供各种绑定方法
  • QPaintDevice:可以被绑定的设备抽象基类(如 QWidget、QImage、QPixmap)
  • QPaintEngine:绑定的底层引擎,负责将绑定操作转换为特定平台的绑定指令

这三者构成了 Qt 绑定系统的完整架构。对于大多数应用场景,你只需要使用 QPainter 即可,底层细节由 Qt 自动处理。

QPainter 基础

基本使用模式

QPainter 必须在 paintEvent() 中使用,这是 Qt 绑定的核心规则:

#include <QPainter>
#include <QWidget>

class MyWidget : public QWidget
{
public:
MyWidget(QWidget *parent = nullptr) : QWidget(parent)
{
resize(400, 300);
}

protected:
// 重写绘制事件
void paintEvent(QPaintEvent *event) override
{
// 创建画家对象,指定绑定的设备(this 表示当前控件)
QPainter painter(this);

// 设置抗锯齿,使图形边缘更平滑
painter.setRenderHint(QPainter::Antialiasing);

// 绑定图形
painter.setPen(Qt::blue);
painter.setBrush(Qt::green);
painter.drawEllipse(50, 50, 100, 80);

// 绑定文本
painter.setFont(QFont("Arial", 16, QFont::Bold));
painter.drawText(rect(), Qt::AlignCenter, "Hello Qt!");

// painter 对象在函数结束时自动销毁,结束绑定
}
};

为什么必须在 paintEvent 中绑定?

当控件需要重绘时(如首次显示、窗口移动、调用 update()),Qt 会触发 paintEvent。如果在其他地方创建 QPainter 绑定到 QWidget,会导致不可预期的行为甚至程序崩溃。这是 Qt 绑定系统的重要设计约束。

画家状态

QPainter 维护一组状态,包括画笔(Pen)、画刷(Brush)、字体(Font)、变换矩阵等:

QPainter painter(this);

// 保存当前状态
painter.save();

// 修改状态并绑定
painter.translate(100, 100); // 平移坐标原点
painter.rotate(45); // 旋转 45 度
painter.drawRect(0, 0, 50, 50);

// 恢复到 save() 之前的状态
painter.restore();

// 此时坐标变换已被重置
painter.drawRect(0, 0, 50, 50); // 在原位置绑定

save()restore() 常用于复杂的绑定场景,可以嵌套使用。

绑定基本图形

点和线

QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);

// 绑定点
painter.drawPoint(10, 10);
painter.drawPoint(QPoint(50, 50));
painter.drawPoint(QPointF(100.5, 100.5)); // 浮点精度

// 绑定线
painter.drawLine(10, 10, 100, 100);
painter.drawLine(QPoint(10, 10), QPoint(100, 100));
painter.drawLine(QLine(10, 10, 100, 100));

// 绑制多条线
QVector<QLine> lines;
lines << QLine(10, 10, 50, 50)
<< QLine(50, 50, 100, 10)
<< QLine(100, 10, 150, 50);
painter.drawLines(lines);

矩形

QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);

// 设置填充色和边框
painter.setBrush(Qt::lightGray); // 填充色
painter.setPen(QPen(Qt::black, 2)); // 边框:黑色,2像素宽

// 绑制矩形
painter.drawRect(10, 10, 100, 80); // x, y, width, height
painter.drawRect(QRect(120, 10, 100, 80));

// 绑制圆角矩形
painter.drawRoundedRect(10, 100, 100, 80, 10, 10); // 最后两个参数是圆角半径

// 填充矩形(无边框)
painter.fillRect(120, 100, 100, 80, Qt::blue);

// 擦除矩形区域(用背景色填充)
painter.eraseRect(10, 200, 100, 80);

圆形和椭圆

QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);

// 绑制椭圆
painter.drawEllipse(50, 50, 100, 80); // 在矩形区域内绑定椭圆

// 绑制圆形(宽高相等)
painter.drawEllipse(200, 50, 100, 100);

// 指定圆心和半径
painter.drawEllipse(QPointF(350, 100), 50, 50); // 圆心,x半径,y半径

// 绑制圆弧
painter.drawArc(50, 200, 100, 100, 0 * 16, 90 * 16); // 起始角度和跨度角度以 1/16 度为单位

// 绑制扇形(饼图)
painter.drawPie(200, 200, 100, 100, 0 * 16, 90 * 16);

// 绑制弦
painter.drawChord(350, 200, 100, 100, 0 * 16, 90 * 16);

角度说明:Qt 使用 1/16 度作为角度单位,即 16 表示 1 度。这样设计是为了避免浮点运算,提高精度。0 度在 3 点钟方向,角度逆时针为正。

多边形和路径

QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);

// 绑制多边形
QPolygon polygon;
polygon << QPoint(50, 10)
<< QPoint(90, 90)
<< QPoint(10, 90);
painter.drawPolygon(polygon); // 自动闭合

// 绑制折线(不闭合)
QPolygon polyline;
polyline << QPoint(150, 10)
<< QPoint(190, 90)
<< QPoint(110, 90);
painter.drawPolyline(polyline);

// 使用 QPainterPath 绑制复杂形状
QPainterPath path;
path.moveTo(250, 10);
path.lineTo(290, 90);
path.lineTo(210, 90);
path.closeSubpath(); // 闭合路径

// 添加曲线
path.moveTo(250, 100);
path.cubicTo(QPointF(250, 150), QPointF(300, 150), QPointF(300, 100)); // 贝塞尔曲线

painter.drawPath(path);

QPainterPath 是一个强大的绘图容器,可以组合多种图形元素,适合绘制复杂形状。

画笔与画刷

QPen(画笔)

画笔控制线条的颜色、宽度、样式:

QPainter painter(this);

// 基本画笔
QPen pen(Qt::black); // 黑色画笔
pen.setWidth(2); // 线宽 2 像素
painter.setPen(pen);
painter.drawLine(10, 10, 200, 10);

// 线条样式
pen.setStyle(Qt::DashLine); // 虚线
painter.setPen(pen);
painter.drawLine(10, 30, 200, 30);

// 可用样式:
// Qt::SolidLine - 实线
// Qt::DashLine - 虚线
// Qt::DotLine - 点线
// Qt::DashDotLine - 点划线
// Qt::DashDotDotLine - 双点划线
// Qt::CustomDashLine - 自定义虚线

// 自定义虚线模式
QVector<qreal> dashes;
dashes << 4 << 2 << 1 << 2; // 4像素实线,2像素空白,1像素实线,2像素空白
pen.setDashPattern(dashes);
pen.setStyle(Qt::CustomDashLine);
painter.setPen(pen);
painter.drawLine(10, 50, 200, 50);

// 线条端点样式
pen.setCapStyle(Qt::RoundCap); // 圆形端点
// Qt::FlatCap - 平端
// Qt::SquareCap - 方形端点

// 线条连接样式
pen.setJoinStyle(Qt::RoundJoin); // 圆角连接
// Qt::MiterJoin - 尖角连接
// Qt::BevelJoin - 斜角连接

// 带颜色和宽度的构造函数
painter.setPen(QPen(Qt::red, 3, Qt::DashLine));

QBrush(画刷)

画刷控制填充的颜色和图案:

QPainter painter(this);

// 基本画刷
QBrush brush(Qt::blue); // 蓝色填充
painter.setBrush(brush);
painter.drawRect(10, 10, 80, 80);

// 图案填充
brush.setStyle(Qt::DiagCrossPattern); // 对角交叉线图案
painter.setBrush(brush);
painter.drawRect(100, 10, 80, 80);

// 可用图案:
// Qt::NoBrush - 不填充
// Qt::SolidPattern - 纯色填充
// Qt::Dense1Pattern - 密度1
// Qt::Dense2Pattern - 密度2
// ... (Dense1-Dense7)
// Qt::HorPattern - 水平线
// Qt::VerPattern - 垂直线
// Qt::CrossPattern - 十字线
// Qt::BDiagPattern - 反对角线
// Qt::FDiagPattern - 正对角线
// Qt::DiagCrossPattern - 对角交叉线

// 纹理填充(使用图片作为填充图案)
brush.setTexture(QPixmap(":/images/texture.png"));
painter.setBrush(brush);
painter.drawRect(190, 10, 80, 80);

渐变填充

Qt 支持三种渐变类型:

QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);

// 1. 线性渐变
QLinearGradient linearGradient(10, 10, 110, 110);
linearGradient.setColorAt(0.0, Qt::red); // 起点颜色
linearGradient.setColorAt(0.5, Qt::yellow); // 中间颜色
linearGradient.setColorAt(1.0, Qt::blue); // 终点颜色
painter.setBrush(QBrush(linearGradient));
painter.drawRect(10, 10, 100, 100);

// 2. 径向渐变(圆形渐变)
QRadialGradient radialGradient(200, 60, 50); // 圆心x, 圆心y, 半径
radialGradient.setColorAt(0.0, Qt::white);
radialGradient.setColorAt(0.5, Qt::yellow);
radialGradient.setColorAt(1.0, Qt::red);
painter.setBrush(QBrush(radialGradient));
painter.drawEllipse(150, 10, 100, 100);

// 3. 锥形渐变
QConicalGradient conicalGradient(350, 60, 0); // 中心点x, 中心点y, 起始角度
conicalGradient.setColorAt(0.0, Qt::red);
conicalGradient.setColorAt(0.33, Qt::green);
conicalGradient.setColorAt(0.66, Qt::blue);
conicalGradient.setColorAt(1.0, Qt::red); // 回到起点颜色形成循环
painter.setBrush(QBrush(conicalGradient));
painter.drawPie(300, 10, 100, 100, 0, 360 * 16);

// 设置渐变的扩散方式
linearGradient.setSpread(QGradient::PadSpread); // 边缘颜色延伸(默认)
// QGradient::ReflectSpread - 反射渐变
// QGradient::RepeatSpread - 重复渐变

绑定文本

QPainter painter(this);

// 设置字体
QFont font("Microsoft YaHei", 16, QFont::Bold);
font.setItalic(true);
painter.setFont(font);

// 绑定文本
painter.drawText(10, 30, "Hello Qt!"); // x, y 是基线位置

// 在矩形区域内绑定文本
QRect rect(10, 50, 200, 100);
painter.drawRect(rect);
painter.drawText(rect, Qt::AlignCenter | Qt::TextWordWrap,
"这是一段在矩形区域内居中显示的长文本,支持自动换行");

// 计算文本所需空间
QFontMetrics fm(font);
int textWidth = fm.horizontalAdvance("Hello Qt!");
int textHeight = fm.height();
qDebug() << "文本宽度:" << textWidth << "文本高度:" << textHeight;

// 获取文本的边界矩形
QRect boundingRect = painter.boundingRect(rect, Qt::AlignLeft, "测试文本");

// 绑制富文本(HTML 子集)
painter.drawText(10, 180, "<b>粗体</b> 和 <i>斜体</i>");

// 使用 QTextDocument 绑制复杂格式文本
#include <QTextDocument>
QTextDocument doc;
doc.setHtml("<h2>标题</h2><p style='color:blue;'>蓝色段落</p>");
doc.setTextWidth(200);
doc.drawContents(&painter, QRectF(10, 200, 200, 150));

绑制图片

QPainter painter(this);

// 绑制 QPixmap(适合屏幕显示,性能更好)
QPixmap pixmap(":/images/photo.png");
painter.drawPixmap(10, 10, pixmap);

// 缩放绘制
painter.drawPixmap(10, 10, 100, 80, pixmap);

// 绑制 QImage(适合图像处理)
QImage image(":/images/photo.png");
painter.drawImage(150, 10, image);

// 绑制部分图片
painter.drawImage(10, 100, image,
QRect(50, 50, 100, 100)); // 源矩形区域

// 平铺图片
painter.drawTiledPixmap(10, 200, 200, 100,
QPixmap(":/images/tile.png"));

QPixmap vs QImage

特性QPixmapQImage
存储位置显存(通常)内存
显示性能更好一般
图像处理不支持支持像素级操作
线程安全
适用场景屏幕显示图像处理、跨线程传递

坐标变换

QPainter 支持各种坐标变换,可以方便地实现平移、旋转、缩放等效果:

QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);

// 初始绑定一个矩形作为参考
painter.setPen(QPen(Qt::black, 2));
painter.drawRect(0, 0, 50, 30);

// 1. 平移
painter.save();
painter.translate(100, 50); // 原点移到 (100, 50)
painter.drawRect(0, 0, 50, 30);
painter.restore();

// 2. 旋转(角度单位:度,逆时针为正)
painter.save();
painter.translate(200, 50); // 先平移到旋转中心
painter.rotate(45); // 旋转 45 度
painter.drawRect(-25, -15, 50, 30); // 以中心点绑定
painter.restore();

// 3. 缩放
painter.save();
painter.translate(300, 50);
painter.scale(1.5, 0.5); // x 方向放大 1.5 倍,y 方向缩小一半
painter.drawRect(0, 0, 50, 30);
painter.restore();

// 4. 错切
painter.save();
painter.translate(100, 150);
painter.shear(0.5, 0); // 水平错切
painter.drawRect(0, 0, 50, 30);
painter.restore();

// 使用变换矩阵(更灵活)
QTransform transform;
transform.translate(200, 150);
transform.rotate(30);
transform.scale(1.2, 1.2);
painter.setTransform(transform);
painter.drawRect(-25, -15, 50, 30);
painter.resetTransform(); // 重置变换

坐标系统

Qt 使用两种坐标系统:

  • 逻辑坐标:绑定操作使用的坐标
  • 物理坐标:设备实际的像素坐标

通过设置视口(Viewport)和窗口(Window),可以在逻辑坐标和物理坐标之间建立映射:

QPainter painter(this);

// 设置视口(物理坐标)
painter.setViewport(0, 0, width(), height());

// 设置窗口(逻辑坐标)
painter.setWindow(0, 0, 100, 100); // 逻辑坐标范围 0-100

// 现在可以使用逻辑坐标绑定
// 无论控件实际大小如何,绑定区域都是 0-100
painter.drawRect(10, 10, 80, 80); // 占据控件的大部分区域

这种方式特别适合需要在不同尺寸设备上保持比例的场景。

裁剪区域

可以限制绑定只在特定区域内生效:

QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);

// 设置矩形裁剪区域
painter.setClipRect(50, 50, 100, 100);
painter.fillRect(0, 0, 300, 300, Qt::blue); // 只有裁剪区域内的部分会被绘制

// 使用路径作为裁剪区域
QPainterPath clipPath;
clipPath.addEllipse(200, 100, 100, 100);
painter.setClipPath(clipPath);
painter.fillRect(0, 0, 400, 300, Qt::red);

// 组合裁剪区域
painter.setClipRect(0, 0, 150, 150);
painter.setClipRect(50, 50, 150, 150, Qt::IntersectClip); // 与现有裁剪区域取交集

// 启用/禁用裁剪
painter.setClipping(false);

双缓冲绑定

在复杂的绑定场景中,使用双缓冲可以避免闪烁:

class MyWidget : public QWidget
{
public:
MyWidget(QWidget *parent = nullptr) : QWidget(parent)
{
// 开启 Qt 的双缓冲(默认开启)
setAttribute(Qt::WA_OpaquePaintEvent, false);
}

protected:
void paintEvent(QPaintEvent *event) override
{
// 方式 1:Qt 自动双缓冲
QPainter painter(this);
// ... 绑定操作

// 方式 2:手动双缓冲(适合复杂场景)
QPixmap buffer(size());
buffer.fill(Qt::transparent);

QPainter bufferPainter(&buffer);
bufferPainter.setRenderHint(QPainter::Antialiasing);
// 在 buffer 上绘制复杂内容
bufferPainter.drawRect(10, 10, 100, 100);
bufferPainter.end();

// 一次性绘制到屏幕
QPainter widgetPainter(this);
widgetPainter.drawPixmap(0, 0, buffer);
}
};

完整示例:自定义图表控件

#include <QWidget>
#include <QPainter>
#include <QVector>
#include <QMouseEvent>

class SimpleChart : public QWidget
{
Q_OBJECT

public:
SimpleChart(QWidget *parent = nullptr) : QWidget(parent)
{
setMinimumSize(400, 300);

// 初始化示例数据
m_data << 10 << 25 << 45 << 30 << 60 << 40 << 75 << 50;
m_labels << "一月" << "二月" << "三月" << "四月"
<< "五月" << "六月" << "七月" << "八月";
}

void setData(const QVector<int> &data, const QStringList &labels)
{
m_data = data;
m_labels = labels;
update(); // 触发重绘
}

protected:
void paintEvent(QPaintEvent *event) override
{
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);

// 背景
painter.fillRect(rect(), Qt::white);

// 边距
const int leftMargin = 60;
const int rightMargin = 20;
const int topMargin = 30;
const int bottomMargin = 40;

// 图表区域
QRect chartRect(leftMargin, topMargin,
width() - leftMargin - rightMargin,
height() - topMargin - bottomMargin);

// 绘制坐标轴
painter.setPen(QPen(Qt::black, 1));
painter.drawLine(chartRect.bottomLeft(), chartRect.topLeft());
painter.drawLine(chartRect.bottomLeft(), chartRect.bottomRight());

// 计算数据范围
int maxValue = 0;
for (int value : m_data) {
maxValue = qMax(maxValue, value);
}
maxValue = (maxValue / 10 + 1) * 10; // 向上取整到10的倍数

// 绘制Y轴刻度
painter.setFont(QFont("Arial", 9));
for (int i = 0; i <= 5; i++) {
int y = chartRect.bottom() - (chartRect.height() * i / 5);
int value = maxValue * i / 5;

// 刻度线
painter.drawLine(chartRect.left() - 5, y, chartRect.left(), y);

// 刻度值
painter.drawText(chartRect.left() - 50, y + 5,
QString::number(value));

// 网格线
painter.setPen(QPen(QColor(220, 220, 220), 1, Qt::DashLine));
painter.drawLine(chartRect.left(), y, chartRect.right(), y);
painter.setPen(QPen(Qt::black, 1));
}

// 绘制柱状图
if (!m_data.isEmpty()) {
int barWidth = chartRect.width() / m_data.size() - 10;

for (int i = 0; i < m_data.size(); i++) {
int barHeight = chartRect.height() * m_data[i] / maxValue;
int x = chartRect.left() + (chartRect.width() / m_data.size()) * i + 5;
int y = chartRect.bottom() - barHeight;

// 渐变填充
QLinearGradient gradient(x, y, x, chartRect.bottom());
gradient.setColorAt(0.0, QColor(70, 130, 180));
gradient.setColorAt(1.0, QColor(100, 149, 237));

painter.setBrush(QBrush(gradient));
painter.setPen(QPen(QColor(70, 130, 180), 1));
painter.drawRect(x, y, barWidth, barHeight);

// X轴标签
if (i < m_labels.size()) {
painter.drawText(x, chartRect.bottom() + 20,
barWidth, 20, Qt::AlignCenter,
m_labels[i]);
}

// 数值标签
painter.drawText(x, y - 15, barWidth, 15, Qt::AlignCenter,
QString::number(m_data[i]));
}
}

// 标题
painter.setFont(QFont("Arial", 12, QFont::Bold));
painter.drawText(rect().adjusted(0, 5, 0, 0),
Qt::AlignHCenter | Qt::AlignTop, "月度销售数据");
}

private:
QVector<int> m_data;
QStringList m_labels;
};

小结

本章介绍了 Qt 2D 绑定系统的核心内容:

  1. QPainter 基础:绑定必须在 paintEvent 中进行,使用 save/restore 管理状态
  2. 基本图形:点、线、矩形、椭圆、多边形、路径等绑定方法
  3. 画笔与画刷:控制线条样式和填充效果,包括渐变填充
  4. 文本与图片:绑制文本和图像的方法
  5. 坐标变换:平移、旋转、缩放、错切等变换操作
  6. 裁剪与双缓冲:优化绑定效果的技术

掌握这些内容,你就可以创建自定义控件、图表、绑定效果等复杂的图形界面。

练习

  1. 创建一个自定义控件,绘制一个带阴影效果的圆形按钮
  2. 实现一个简单的柱状图控件,支持传入数据自动绘制
  3. 使用 QPainterPath 绘制一个五角星
  4. 实现一个带动画效果的进度环(圆环形状的进度条)
  5. 创建一个简单的签名控件,支持鼠标绘制并保存为图片

参考资源