在Qt圈子里,说起入门绘图,多少人第一个跑起来的程序就是自带的2D Painting示例。这个例子藏在Qt安装目录的Examples里,平时容易被忽略,但真要说把QPainter那套东西讲明白,它比市面上大半教程都靠谱。我用这个示例拆过不少项目,也带过新人,今天就把这里面的门道从头到尾捋一遍,从示例结构、核心绘制流程到实际工程里的注意事项,一次说透。
1. 项目概述与环境准备
1.1 核心需求解析
先明确这个示例到底能干什么。2D Painting示例本质上是一个基于QPainter的绘图演示程序,界面是一个主窗口,左侧是功能选择列表,右侧是自定义的绘图区域。你在左边选中不同条目,右边就展示相应的绘制效果——有基础图形、文字渲染、画笔样式、渐变填充、复合模式、坐标变换,甚至还能把SVG文件渲染到画布上。
这个示例解决的核心问题是什么?一句话:告诉你怎么用QPainter在各种场景下把东西画到屏幕上。它不是那种泛泛的demo,每个绘制页都对应一类高频需求。比如你接到一个任务要画自定义控件背景、做实时图像标注、生成统计图表,本质上都是在跟QPainter打交道。先把示例吃透,后面这些活基本都能顺手拿下来。
1.2 环境与工具选型
我用的是Qt 5.14.2,Windows 10环境。这里多说一句,Qt版本迭代很快,但5.14.2是个很稳的版本,社区资料多,编译器和工具链的兼容性问题少。如果你机器上装的是其他5.x版本,示例代码基本通用,不必担心差异。至于IDE,用Qt Creator打开示例工程文件就行,双击.pro文件自动加载,非常省事。
注意:打开示例前务必确认Qt安装时勾选了Sources和Examples组件,否则本地找不到示例源码。装完如果还是找不到,去安装目录下的Examples文件夹翻,路径大概是
Qt/Qt5.14.2/5.14.2/Examples/Qt-5.14.2/。
1.3 示例工程的打开方式
打开方式有两种:一是从Qt Creator欢迎页的示例入口进入,二是直接去安装目录打开painting示例的.pro文件。我推荐后者,因为能顺便看路径、翻源码,还能直接复制整个目录到自己的工程目录改着玩。
工程文件结构很简单,几个主要的类文件——主窗口、各个绘制页面、辅助工具类——再加一个资源文件。头文件和源文件分开存放,一个类对应一对文件。这种组织方式本身就很值得学,你后面自己做项目,也建议按照功能模块拆文件,而不是全部塞进一个main.cpp里堆着。
2. 示例架构与核心设计思路
2.1 整体框架拆解
打开主窗口的源码,你会发现这个示例的核心架构其实是一个选择列表加一个堆叠窗口。左边QListWidget列出所有绘制类型,右边QStackedWidget按索引切换对应的绘制页面。每个绘制页面继承自QWidget,重写paintEvent,在这个函数里调用QPainter完成绘制。这种"列表导航 + 多个独立绘制类"的设计模式,在正式的Qt项目里非常常见,尤其是做设置界面、多页签工具、模块化绘图软件,几乎都是这个套路。
再往细看,每个绘制页面虽然继承同一个父类,但各自实现的绘制逻辑完全独立。这样的好处是隔离性强——你想单独调试某个绘制效果,只需要跑到对应的类里面改代码,不会影响其他页面。而且以后往里加新功能也方便:新建一个类,重写paintEvent,在列表里插一项,完事。
2.2 绘制体系背后的"为什么"
为什么要用QPainter这套机制,而不是直接在控件上放一堆子组件拼出图案?这个问题的答案在于效率和灵活性。QPainter是Qt自己的2D渲染引擎,所有绘制操作最终都经过统一的栅格化管线输出到窗口或图片上。它能在极短的时间内画出大量复杂图形,而且天然支持反锯齿、半透明混合、渐变填充这些高级特性。如果用子控件拼界面,性能和效果都要大打折扣,也无法实现连续曲线、渐变色这类动态效果。
另一方面,QPainter基于状态机模型——画笔、画刷、字体、变换矩阵这些都是状态属性,设置一次后续绘制都受影响。这与直接在像素上涂色不同,更像是在"描述"一幅画,描述方式是矢量级的。这样设计的好处是:图像无论是放大缩小还是旋转,都能保持清晰,而且代码逻辑容易复用,随时可以把画的内容输出成PDF或打印出来。
2.3 各类绘制页面的功能对照表
这个示例里有几个典型的绘制页,我整理了一个对照表,方便你快速定位自己想学的技术点:
| 绘制页面名称 | 核心技术点 | 典型应用场景 |
|---|---|---|
| Basic Drawing | 基本图形绘制,直线、矩形、多边形 | 统计图表柱状图,基础控件底框 |
| Text Drawing | 文本渲染、字体度量、对齐方式 | 标注软件文字标签,自定义仪表盘读数 |
| Pen Styles | 画笔样式、线帽、连接样式 | CAD软件图形边缘绘制,地图路径显示 |
| Brush Styles | 画刷颜色与填充风格 | 图形编辑器填充色块,图表区域填充 |
| Gradient | 线性/径向/锥形渐变 | 炫酷按钮背景,玻璃质感面板 |
| Composition | 混合模式与透明度叠加 | 图像编辑软件图层混合,特效渲染 |
| Transform | 坐标系平移、缩放、旋转 | 地图缩放旋转,图元编辑器操作 |
| SVG Viewer | 矢量文件渲染 | 图标库预览工具,矢量图纸显示 |
你看,没有一个是纯花架子。把每页的绘制逻辑看懂,几乎可以应付日常工作和外包项目里80%的绘图需求。
3. 核心绘制流程与实操要点
3.1 paintEvent:一切绘制的入口
真正干活的函数是paintEvent。它是QWidget在需要重绘时自动调用的保护方法。你在里面创建一个QPainter,指定绘制目标(通常是this,也就是当前控件),然后执行各种draw方法。当窗口需要更新时,调用update()触发重绘,而不是直接手动去画。
示例代码里每个绘制页的核心框架都差不多,结构如下:
void BasicDrawingWidget::paintEvent(QPaintEvent *) { QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing, true); // 执行各类绘制操作 }这里有一个我特别强调的点:构造QPainter时传入this,意味着绘制目标就是当前窗口控件。如果是想往QPixmap或QImage上画,构造时传那个对象即可,这样离屏绘制可以做缓存,性能翻倍。
3.2 画笔、画刷与状态设置
绘制之前,通常先设置画笔轮廓和画刷填充。这里有一个容易犯迷糊的地方——画笔颜色控制线条,画刷颜色控制填充区域,两者互相独立。示例里的Pen Styles页面,专门演示了不同线宽、线型、线帽和连接样式的组合效果。
实际工程里我建议按下面这个套路设置:
QPainter painter(this); QPen pen(Qt::blue, 3, Qt::DashLine, Qt::RoundCap, Qt::RoundJoin); painter.setPen(pen); QBrush brush(QColor(100, 200, 100, 80)); // 带透明通道的绿色 painter.setBrush(brush); painter.drawRect(QRect(20, 20, 200, 120));这里面最后两个参数最容易被忽略。线帽(RoundCap)决定线条端点形状,连接样式(RoundJoin)决定折线拐角是尖角、斜角还是圆角。画折线路径时如果不用RoundJoin,拐点处会出现难看的毛刺或断裂,这在绘制地图轨迹和折线图时尤其明显。透明画刷颜色是我常用的技巧——用带Alpha值的颜色填充,就能做出半透明的背景区域,不用额外叠加图层。
还有一个很重要的点:QPainter的状态保存与恢复。你设置画笔画刷后,后面所有绘制都受影响。想画完一部分就恢复之前的状态,用save()和restore():
painter.save(); painter.setPen(QPen(Qt::red, 5)); painter.drawLine(...); painter.restore(); // 此处画笔线宽恢复为初始值3.3 文字绘制的坐标系陷阱
Text Drawing页面讲的是QPainter绘制文本。这里有一个经验:绘制文字时默认的坐标是文字的基线左下角位置,而不是文本框的左上角。很多人第一次画文字,设置坐标后文字位置飘了,就是因为没搞清楚这个参考点。
想精确控制文本范围,用QFontMetrics计算文本尺寸。示例里展示了怎么获取文本的宽度、高度、上升和下降值,从而配合绘制文本的边框。处理文本居中时,直接计算偏移量即可:
QFontMetrics fm(painter.font()); int textWidth = fm.horizontalAdvance(text); int textHeight = fm.height(); // 让文本在矩形 rect 中居中 painter.drawText(QPoint(rect.x() + (rect.width() - textWidth) / 2, rect.y() + (rect.height() - textHeight) / 2 + fm.ascent()), text);这个ascent()转换是关键,不然你算出来的垂直居中会在视觉上偏下。示例的Text页里有类似的实现,直接对照就能验证我说的坑。
4. 高级绘制技巧与渐变、混合模式实战
4.1 渐变的三种类型与选择逻辑
Gradient页面演示了线性渐变、径向渐变和锥形渐变。我实际项目里用得最多的是线性渐变,适合做按钮高光和面板背景。径向渐变多用于模拟光源或圆形凸起质感。锥形渐变比较冷门,但做仪表盘、风速盘这类圆形刻度时非常好用。
线性渐变的构造方式是给两个坐标点,然后设置颜色节点,示例大概是:
QLinearGradient gradient(0, 0, 200, 200); gradient.setColorAt(0.0, Qt::white); gradient.setColorAt(0.5, QColor(50, 150, 250)); gradient.setColorAt(1.0, Qt::blue); painter.setBrush(QBrush(gradient)); painter.drawRect(QRect(0, 0, 200, 200));注意setColorAt的第一个参数是位置,范围从0到1,对应渐变的起点到终点。这里有个细节:渐变坐标是相对于控件坐标系的,如果你在窗口里绘制多个图形使用同一个渐变对象,渐变效果会在每个图形上重复从头到尾分布,而不是全局连续。想要整个窗口一个渐变贯通,要先把painter的坐标系通过变换统一处理,或者每个图形单独设置对应区域的渐变坐标。这是个很容易踩坑的地方,但影响很大。
4.2 混合模式:为什么默认模式最抗造
Composition页面展示了QPainter的各种混合模式,比如源在上、目标在上、取亮、取暗、异或等。默认的CompositionMode_SourceOver是标准覆盖模式——新绘制的像素覆盖在原有像素上面,带半透明效果则产生看着自然的叠加。
这里给个实用建议:日常绘制中使用SourceOver就够了。除非做图像编辑类工具,否则不推荐搞复杂混合模式。原因很简单,混合模式改的是整个绘制源的合成方式,一个不留神会影响后续所有绘制。示例里看到的各种炫酷效果,实际上是把绘制流程拆分成了多次独立qingli,每次单独设置混合模式,每次set之后又马上restore。一旦你忘记恢复,后续界面元素就会出现莫名其妙的颜色错乱。
4.3 坐标系变换:把它想成摆弄相机
Transform页面教的是平移、缩放、旋转。很多人一看translate、scale、rotate就头大,其实你可以把QPainter想象成一台相机:translate是移动相机位置,scale是拉近拉远镜头,rotate是旋转镜头角度。而绘制操作的坐标原点始终是"相机"的中心。
实际用的时候有个技巧:想要实现一个图形绕自身中心旋转,不要直接rotate,而是先把painter平移到图形中心,再旋转,再平移回去,然后绘制。示例里应该也有类似操作,比如那个反复旋转的正方形图案,就是这样层层嵌套变换画出来的:
painter.save(); painter.translate(100, 100); // 移到旋转中心 painter.rotate(angle); // 旋转 painter.translate(-50, -50); // 把图形左上角移到原点位置 painter.drawRect(0, 0, 100, 100); painter.restore();这个操作序列是坐标变换里最经典的组合。重点是理解了它,画时钟表盘、雷达扫描、仪表指针这些复杂的东西都能迎刃而解。
5. 离屏绘制与SVG渲染扩展
5.1 用QPixmap做缓存绘制
示例里虽然没重点展开,但SVG渲染那页涉及到了先用QImage或QPixmap离屏绘制,再一次性贴回窗口屏幕的操作模式。这在真实项目里极其重要:如果paintEvent里频繁重绘大量复杂图形,画面会闪,性能也会垮。正确思路是:需要的时候先在QPixmap里画好静态内容,paintEvent里只负责把缓存图"贴"到窗口上。
QPixmap cachePixmap(size); QPainter cachePainter(&cachePixmap); // 这里画各种复杂图形 cachePainter.end(); // 真正窗口的paintEvent里 void paintEvent(QPaintEvent*) { QPainter painter(this); painter.drawPixmap(0, 0, cachePixmap); }这段逻辑很简单,但收益立竿见影。尤其是画那种背景网格、复杂刻度盘之类的东西,每次都实时画一遍的代价非常大。QPixmap的缓存机制可以让你只在内容变动时才更新缓存,静态期间直接走drawPixmap,性能会非常好。我在一个波形显示器项目里就是这么干的,波形重绘频率从十几帧直接提升到满帧。
注意事项:QPixmap对象建议在构造函数或初始化函数里创建,不要放在paintEvent里每次new,否则内存分配和释放会成为新的瓶颈。
5.2 SVG渲染接口与平时用的差异
SVG Viewer那页演示了用QPainter配合QPicture或QGraphicsScene来加载矢量图。实际上QPainter本身不直接解析SVG文件,示例中是通过QSvgRenderer这类类库将矢量内容转化为绘制指令,再由QPainter绘制到窗口。这里面最有价值的一点是:一旦渲染成QPainter指令,你可以应用到任何支持QPainter的目标上,比如直接打印到PDF。
做产品时经常遇到需要将图标资源打包进程序的需求,如果直接放SVG文件,资源文件管理会稍微复杂些,但换来的是清晰度和缩放灵活性。尤其在高分屏上,SVG渲染的效果比位图强太多——这也是这个示例页给开发者的一个重要提示:矢量资源是现代GUI开发的趋势。
5.3 简单加一个"导出图片"功能
基于前面说的离屏绘制思路,我们可以轻松给示例加一个截图导出功能。核心逻辑:
void MainWindow::exportImage() { QPixmap pixmap(stackedWidget->currentWidget()->size()); QPainter painter(&pixmap); stackedWidget->currentWidget()->render(&painter); painter.end(); pixmap.save("export.png"); }render这个函数是QWidget自带的,直接把控件内容绘制到指定的QPainter上。加了这个功能,示例就能当简易绘图工具用了——画出好看的图形直接导出分享。这算是一个从示例到生产力工具的跳跃,值得动手试试。
6. 踩坑记录与排查方法论
6.1 典型问题:绘制不显示或白屏
症状:运行程序弹出窗口,但绘制区域一片空白,啥也看不见。排查思路按顺序来:
- 检查
paintEvent是否真的被调用——在函数首行加一个qDebug() << "paint",运行时看控制台输出。 - 确认绘制时是否用了不可见颜色——比如画笔和画刷都是白色,画在白色背景上自然看不出来。
- 检查窗口大小——如果控件尺寸是0,paintEvent可能被调用但画布什么都没有。用
setMinimumSize或布局管理器给控件固定大小就能解决。
这一条在示例实现中不太容易出现,但你自己扩展功能时踩到的概率极高。
6.2 典型问题:绘制闪烁
闪烁几乎是所有自定义绘图控件的通病,成因是重绘时先清空背景再画内容,两帧之间白色闪一下。治本的办法有几个:
- 设置控件属性
setAttribute(Qt::WA_OpaquePaintEvent, true),告诉系统不自动清背景。 - 设置
setAttribute(Qt::WA_NoSystemBackground, true)跳过系统背景擦除。 - 在paintEvent里直接画背景色,接管整个绘制流程。
- 更彻底的是开启双缓冲,Qt控件其实默认就是双缓冲的,但配合前面两个属性效果更好。
对于实时刷新频率高的界面,闪不闪烁是体验好坏的分水岭,这个一定要重视起来。
6.3 典型问题:绘制错位,坐标对不上
坐标错位大多数情况下是坐标系变换后忘了恢复。比如你在一个函数里调用了translate,函数结束后painter的坐标系仍然偏移着,下一个绘制函数就会画在意外的地方。
我的排查方法很简单:在任何变换操作前调用painter.save(),操作完成后立刻painter.restore()。为了让这个习惯深入到骨子里,每次写完变换代码我都会读一遍检查是否成对出现。另外也可以用drawRect(0,0,10,10)这种小方块在关键位置打印出当前坐标系的情况——这是非常高效的调试图元方法。
6.4 排查工具与调试技巧
QPainter的绘制问题不适合断点单步调试,因为每步状态如何很难观察。我常用的实战技巧:
- 用
qDebug()把当前坐标、画笔颜色、线宽打印出来。 - 绘制半透明的辅助图层,比如在半透明矩形后来的图形都能"透"出来,方便对照位置是否重叠。
- 大量使用
fillRect配合不同颜色来区分不同逻辑分组区域,先确定整体布局有没有问题,再深入到单个图形细节。
这样分而治之,再怪的错位大概率半小时内能定位。
7. 扩展进阶方向与性能优化
7.1 从示例到项目:控件设计模式升级
示例里的"每个绘制页面独立类"模式,虽然适合演示,但在正式项目里还可以再往前走一步。我们可以把每个绘制模块封装成独立的控件,并对外暴露接口,让外部模块传递数据来驱动重绘。比如你做一个实时数据可视化面板,每个图表是一个控件,对外提供setData(QVector<double>)方法,任何线程都可以通过信号槽更新数据并触发重绘。
这实际上就是自定义控件开发的常规路径。你从2D Painting示例里学到的所有技术——QPainter、QPen、QBrush、渐变、复合、变换——都是这些高级控件的底座。
7.2 性能调优:哪些绘制操作是"吃帧杀手"
QPainter操作有性能差异,同样是画圆形,耗时可差数倍。实测下来,下面这几种操作最影响性能:
- 大面积的半透明渐变填充,尤其在窗口频繁resize时重算量巨大。
- 复杂的自绘字体渲染,比普通文本绘制慢很多倍。
- 高密度图形场景,比如地图路径、散点图里面有上万个点,逐个调用drawPoint性能会非常差。
对策有三板斧:一是合理使用缓存(QPixmap),把不变的部分画好存起来;二是减少绘制指令数量,能用一条QPainterPath就合并多条线,不用反复画数十条短线段;三是用update(QRect)精确指定更新区域,而不是每次整窗重绘。把这三个习惯用上,大部分性能问题能消掉八成。
7.3 后续可以继续深入的主题
这个示例吃透以后,想继续深入的话,方向很清晰:QGraphicsView框架、QOpenGLWidget、QChart、QQuick/QML的Canvas。它们各自在不同场景下都有优势——QGraphicsView适合图元管理和交互,QOpenGL适合高性能渲染,QChart是自带的图表库,QML的Canvas做动态界面很有表现力。你可以根据自己的项目方向选一个深入。
我个人体会是,真正常用的还是QPainter这套底层API,因为它最直接、最可控,而且前面的知识不会过期。解决需求的时候,手里有纯QPainter这板斧,就根本没有画不出来的东西,只有你想不到的画法。
如果你正在看这个示例却觉得理解不透,我建议你什么都不干,先把代码复制一份出来,按照自己喜欢的格式重写一遍,哪怕最后运行结果一模一样。抄一遍代码,和看一遍代码,学到的内核绝对不一样。这是我带人时一贯坚持的观点。