嵌入式Linux驱动开发——设备树语法与编译工具——读懂这张“藏宝图“
现代Qt开发教程新手篇2.1——QPainter 绘图基础相关仓库仍然已经开源正在积极火热的建设之中欢迎各位大佬提Issue和PR链接地址https://github.com/Awesome-Embedded-Learning-Studio/Tutorial_AwesomeQt1. 前言 / 为什么需要 QPainter嘿嘿到QtGUI的部分了这里是QWidget的基建算是Qt是一个GUI框架对吧。如果您是从一个标准路径我是指对着Console那个默认的控制台程序走过来的朋友咱们以前学 C 的控制台程序输出文字就printf搞定了。但到了 GUI 的世界里想画一个矩形、画一条直线居然要专门学一整套绘图 API吗又寸后来做项目的时候才发现QPainter 是 Qt 里最核心的绘图工具之前撸桌面CCIMXDesktop写控件简直天天见几乎所有的自定义 UI 都离不开它。你看到的按钮、进度条、图表——底层全都是 QPainter 一笔一笔画出来的。当你需要做一个控件样式不满足需求的自定义界面或者想画一个数据可视化图表QPainter 就是你唯一的出路。有人问QML还有QSS呢额底层是这个对不对更现实一点说如果你要做嵌入式设备上的界面很多现成的控件根本用不了几乎全靠 QPainter 手绘QSS也行但是笔者发现但凡动态一点QSS是真的不好用还要花费时间parse字符串其实不算特别好的方案。所以这门手艺早点练比晚点练好。这篇文章我们一起来搞清楚怎么在paintEvent里正确拿到 QPainter、怎么设置画笔和画刷、怎么画各种图形、坐标系的坑在哪里。这些都是 QPainter 的基本功搞明白了后面学坐标系变换、双缓冲绘图才有基础。2. 环境说明本篇代码适用于 Qt 6.5 版本CMake 3.26C17 或更高标准。示例代码依赖 QtGui 和 QtWidgets 模块QPainter 在 QtGui 里但 paintEvent 需要 QWidget所以两个都要可以在任何支持 Qt6 的桌面平台上编译运行。3. 核心概念讲解3.1 paintEvent 中获取 QPainter 的正确姿势QPainter 不能凭空使用它必须绑定一个画布——在 Qt 里这个画布通常就是 QWidget。你要在一个 Widget 上画东西唯一正确的入口就是重写paintEvent(QPaintEvent *)函数。classMyWidget:publicQWidget{Q_OBJECTprotected:voidpaintEvent(QPaintEvent*event)override{QPainterpainter(this);// 以 this当前 Widget为画布// 接下来所有的绘图操作都通过 painter 来完成painter.drawLine(0,0,100,100);}};这里有个非常重要的细节QPainter 的构造函数传入this意思是我要在这个 Widget 上画画。QPainter 构造完成的那一刻它就拿到了 Widget 的绘制上下文可以开始绑定了。你可能在网上看到过另一种写法先构造 QPainter再调begin()画完再调end()。这种写法完全可行但对于paintEvent来说没必要。直接用构造函数传入this是最简洁也最不容易出错的方式。begin和end可以更精确的控制但是大哥玩意你忘记释放了。。。我只能说自求多福没必要给自己上风险。// 不推荐手动 begin/end容易忘记voidpaintEvent(QPaintEvent*)override{QPainter painter;painter.begin(this);painter.drawLine(0,0,100,100);painter.end();// 忘了就等着出 bug 吧}// 推荐RAII 风格构造即开始析构即结束voidpaintEvent(QPaintEvent*)override{QPainterpainter(this);painter.drawLine(0,0,100,100);// 函数结束painter 析构自动 end}还有一个关键点不要在paintEvent之外创建 QPainter 绑定到 Widget 上。Qt 的绘图系统是基于事件的只有在paintEvent被调用时系统才准备好了一切绘制资源。在别的地方画画要么直接崩溃要么画出来被下一次 paintEvent 覆盖掉。当你需要主动触发重绘时不要直接调用paintEvent()而是调用update()。update()会在下一个事件循环周期统一处理绘制请求避免频繁重绘带来的性能问题。// 想更新画面这样触发voidchangeColor(){m_colorQt::red;update();// 安排一次 paintEvent不要自己调用 paintEvent()}3.2 QPen线条与 QBrush填充与 QColor画图之前你得先搞清楚两个概念画笔QPen和画刷QBrush。画笔决定线条的样子——颜色、宽度、线型实线、虚线、点线。画刷决定填充的样子——纯色、渐变色、图案填充。一句话区分画笔管边框画刷管内部。QPainterpainter(this);// 设置画笔红色、3 像素宽、实线QPenpen(Qt::red,3,Qt::SolidLine);painter.setPen(pen);// 设置画刷蓝色填充QBrushbrush(Qt::blue);painter.setBrush(brush);// 画一个有边框有填充的矩形painter.drawRect(10,10,200,100);QColor 是颜色的基础类。你可以用预定义的颜色常量Qt::red、Qt::blue也可以用 RGB/RGBA 值创建任意颜色。// 各种创建 QColor 的方式QColorcolor1(Qt::red);// 预定义颜色QColorcolor2(255,128,0);// RGBQColorcolor3(255,128,0,200);// RGBA带透明度QColorcolor4(#FF8000);// 十六进制字符串QColorcolor5(rgba(255, 128, 0, 200));// CSS 风格字符串// 判断颜色是否有效构造失败时 isValid() 返回 falseQColorbad(not_a_color);if(!bad.isValid()){qDebug()这个颜色不合法;}QPen 有几个常用属性你需要知道QPen pen;pen.setColor(Qt::darkBlue);// 颜色pen.setWidth(2);// 宽度像素pen.setStyle(Qt::DashLine);// 线型实线、虚线、点线等pen.setCapStyle(Qt::RoundCap);// 线段端点样式平头、圆头、方头pen.setJoinStyle(Qt::RoundJoin);// 拐角样式尖角、圆角、斜角QBrush 也有几种填充模式// 纯色填充QBrushsolidBrush(Qt::blue);// 渐变填充线性渐变QLinearGradientgradient(0,0,200,0);// 从左到右gradient.setColorAt(0.0,Qt::white);// 起始颜色gradient.setColorAt(1.0,Qt::blue);// 结束颜色QBrushgradientBrush(gradient);// 图案填充QBrushpatternBrush(Qt::DiagCrossPattern);// 交叉斜线图案说实话日常开发中 80% 的情况你只需要纯色画笔和纯色画刷。渐变和图案属于锦上添花用到的时候查文档就行。3.3 绘制基本图形QPainter 提供了一整套绘图函数名字都很好记QPainterpainter(this);// ---- 直线 ----painter.drawLine(10,10,200,200);// 两点画线painter.drawLine(QPoint(10,10),QPoint(200,200));// QPoint 版本// ---- 矩形 ----painter.drawRect(50,50,150,100);// x, y, width, heightpainter.drawRect(QRect(50,50,150,100));// QRect 版本// ---- 填充矩形无边框 ----painter.fillRect(50,200,150,100,Qt::green);// ---- 椭圆 ----painter.drawEllipse(100,100,120,80);// 外接矩形定义椭圆// 特殊情况画正圆宽高相等painter.drawEllipse(300,100,80,80);// ---- 圆角矩形 ----painter.drawRoundedRect(50,350,200,100,15,15);// 最后两个是圆角半径// ---- 多边形 ----QPolygon polygon;polygonQPoint(200,400)QPoint(250,350)QPoint(300,400)QPoint(275,450)QPoint(225,450);painter.drawPolygon(polygon);// ---- 文字 ----painter.setFont(QFont(Arial,16,QFont::Bold));painter.drawText(50,500,Hello QPainter!);这里有个容易搞混的地方drawRect(x, y, w, h)中的x, y是矩形左上角的坐标w, h是宽和高——不是右下角坐标。初学者经常把w, h当成第二个点的坐标画出来的东西位置完全不对。drawEllipse同理参数不是圆心坐标和半径而是外接矩形的左上角和宽高。想以(cx, cy)为圆心画半径为r的圆要这样写intcx200,cy200,r50;painter.drawEllipse(cx-r,cy-r,2*r,2*r);drawText的坐标(x, y)是文字基线的左端位置不是文字矩形的左上角。这意味着如果你把 y 设成 0文字的大半部分会画到 Widget 上方看不见。初学者很容易在这里栽跟头。3.4 坐标系原点与单位QPainter 的坐标系默认是这样的原点 (0, 0) 在 Widget 的左上角x 轴向右递增y 轴向下递增——这跟数学里的笛卡尔坐标系不一样很多新手在这里会很别扭。这意味着painter.drawLine(0, 0, 100, 0)是从左上角往右画一条水平线。关于单位QPainter 默认使用的是逻辑坐标单位是像素。但这里有个坑——在高 DPI 屏幕上一个逻辑像素可能对应 2 个甚至 3 个物理像素。Qt6 默认开启了高 DPI 缩放所以你设置的 100 像素宽度在 2x 缩放的屏幕上实际会占据 200 个物理像素。// 获取当前 Widget 的实际像素尺寸intlogicalWidthwidth();// 逻辑尺寸intlogicalHeightheight();// 如果需要获取物理像素尺寸高 DPIqreal dprdevicePixelRatioF();// 设备像素比2x 屏幕返回 2.0intphysicalWidthlogicalWidth*dpr;日常绘图你不需要关心物理像素Qt 会自动处理缩放。但如果你要做像素级的精确操作比如截图或者图像处理这个区别就会变得很重要。还有一个常见的困惑width()和rect().width()返回的值一样吗大多数情况下是一样的都是 Widget 的逻辑宽度。但contentsRect()返回的是去除边距后的可用区域——如果你给 Widget 设了 stylesheet 的 padding 或者设置了 margin两者就不一样了。到这里你可以停下来想一想QPen 和 QBrush 各自负责什么如果你要画一个红色边框、蓝色填充的圆角矩形代码逻辑应该是怎样的理解了这两个概念的区别后面使用起来就不会搞混了。很好概念讲完了我们来看看实际开发中最容易踩的坑。4. 踩坑预防第一个坑也是最常见的一个在 paintEvent 外面创建 QPainter。我看到过不少人在按钮点击的槽函数里直接QPainter painter(this)想着我要在这里画个东西。这几乎一定会出问题。Qt 的绘图系统是基于事件的系统只在paintEvent被调用时才会准备好绘制所需的资源。在别的地方创建 QPainter 绑定到 Widget要么直接崩溃要么画出来的东西瞬间被下一次 paintEvent 覆盖。正确的做法是设置一个标志位然后调update()让 Qt 在合适的时机帮你重绘// 错误在 paintEvent 外面画画voidMyWidget::onButtonClicked(){QPainterpainter(this);// 危险不在 paintEvent 里painter.drawRect(0,0,50,50);}// 正确通过标志位 update() 触发重绘voidMyWidget::onButtonClicked(){m_drawRecttrue;update();// 触发 paintEvent}voidMyWidget::paintEvent(QPaintEvent*){QPainterpainter(this);if(m_drawRect){painter.drawRect(0,0,50,50);}}第二个坑跟第一个坑是配套的直接调用paintEvent()。有人知道不该在外面创建 QPainter但又想立即刷新画面就干脆直接paintEvent(nullptr)。这也不行。直接调用 paintEvent 会跳过 Qt 的绘制流程管理可能导致绘制不完整、闪烁、或者和系统的绘制事件冲突。想重绘就老老实实update()让它自己安排时机。第三个坑是 drawText 的坐标问题。drawText(x, y, text)里的 y 是文字基线位置不是文字矩形的顶部。所以如果你写drawText(0, 0, Hello)文字大部分内容会跑到 Widget 上方被裁剪掉看起来就像没画出来一样。解决这个问题有两种方式要么用QFontMetrics算出 ascent 然后加上去要么用 QRect 版本的 drawText 配合对齐标志后者更推荐// 方法 1手动计算基线位置QFontMetricsfm(painter.font());inttextYfm.ascent();// 基线到顶部的距离painter.drawText(0,textY,Hello);// 方法 2推荐用 QRect 指定绘制区域 对齐标志painter.drawText(rect(),Qt::AlignTop|Qt::AlignLeft,Hello);第四个坑是画笔和画刷搞混。想画红色边框的矩形结果只设了 QBrush 没设 QPen画出来的矩形内部是红色、边框却是默认的黑色 1 像素线。这不是 bug是因为 QPen 和 QBrush 是完全独立的——QPen 管线条和边框QBrush 管内部填充。你需要分别设置才能得到想要的效果QPenpen(Qt::red,2);// 红色边框2px 宽QBrushbrush(Qt::lightGray);// 浅灰填充painter.setPen(pen);painter.setBrush(brush);painter.drawRect(10,10,100,80);接下来做一个代码填空练习补全下面的代码实现一个画红框蓝底矩形的自定义 WidgetclassRectWidget:public________// 1. 应该继承什么{Q_OBJECTprotected:voidpaintEvent(________)override// 2. 参数类型是什么{________painter(this);// 3. 创建 QPainterQPenpen(________,2);// 4. 红色画笔painter.________(pen);// 5. 设置画笔QBrushbrush(________);// 6. 蓝色画刷painter.________(brush);// 7. 设置画刷painter.drawRect(________,50,200,100);// 8. 从 (20, 50) 开始画}};再来一道调试挑战这段代码有什么问题为什么画面上什么都看不到classMyWidget:publicQWidget{Q_OBJECTpublic:voiddrawSomething(){QPainterpainter(this);painter.setPen(Qt::red);painter.drawLine(0,0,300,300);}};提示想想 QPainter 和 paintEvent 的关系。5. 练习项目我们来做一个实战练习创建一个自定义 Widget接收一组数据比如{85, 60, 95, 40, 75}用 QPainter 画出一个简易柱状图。每根柱子高度按数据值的比例计算柱子底部有标签顶部显示数值。完成标准是自定义一个BarChartWidget继承自 QWidget重写paintEvent根据数据数组画柱状图柱子之间有间距颜色不同可以用渐变底部显示标签“A”、“B”、“C”…顶部显示数值提供一个setData(const QListint data)方法调用后自动刷新窗口大小改变时柱状图自适应缩放利用width()和height()。几个提示在 paintEvent 里用width()和height()获取当前 Widget 尺寸据此计算柱子宽度和最大高度留出底部和顶部的边距给标签和数值用QFontMetrics计算文字尺寸画柱子用drawRect填颜色用QLinearGradient做渐变效果会更专业setData()里存好数据后调update()触发重绘。6. 官方文档参考链接Qt 文档 · QPainter Class – QPainter 的完整 API 参考包含所有绘图函数和坐标变换方法Qt 文档 · QPen Class – 画笔类的详细文档涵盖线型、端点样式、连接样式等所有属性Qt 文档 · QBrush Class – 画刷类的详细文档包含纯色、渐变、纹理等填充模式Qt 文档 · QColor Class – 颜色类的完整文档RGB/HSV/CMYK 等颜色空间支持Qt 文档 · Paint System Overview – Qt 绘图系统的总览文档帮助理解 QPainter 在整个绘图架构中的位置到这里QPainter 的基本功你算是入门了。记住核心要点只在 paintEvent 里创建 QPainter、QPen 管线条 QBrush 管填充、坐标系 y 轴朝下。下一篇文章我们会进入坐标变换的世界——学会 translate、rotate、scale 之后你就能画出各种旋转、缩放的复杂图形了。更好的阅读体验https://awesome-embedded-learning-studio.github.io/Tutorial_AwesomeQt/beginner/02-qtgui/01-qpainter-basic-beginner相关阅读通用GUI编程技术——Win32 原生编程实战五十三——子类化与超类化 - 相似度 82%现代Qt开发教程新手篇1.15——正则与文本处理 - 相似度 71%