现代Qt开发教程新手篇2.2——坐标系与 QTransform 变换基础相关仓库仍然已经开源正在积极火热的建设之中欢迎各位大佬提Issue和PR链接地址https://github.com/Awesome-Embedded-Learning-Studio/Tutorial_AwesomeQt1. 前言 / 为什么需要坐标变换说实话刚开始学 QPainter 的时候我以为只要会画矩形和圆就够了。直到有一天我需要画一个旋转 45 度的指针仪表盘我盯着屏幕上那根斜着的线脑子里只有一个想法难道我要手算每个点的坐标你想想看一个时钟的秒针每秒钟旋转 6 度。如果纯用手算三角函数去算端点坐标代码会变成什么样子每画一个旋转图形就得写一堆 sin/cos维护起来简直噩梦。更别提还要处理缩放和平移的组合了。坐标变换就是为了解决这个问题而生的。你可以把坐标变换理解成移动画布——你不用重新计算每个点的位置只需要告诉 QPainter把坐标原点移到这儿、“旋转 45 度”、“放大两倍”然后像什么都没发生一样用原来的坐标画画就行。更直白地说坐标变换让你在局部坐标系里工作不用管全局坐标系是什么样。你在 (0, 0) 画一个圆通过变换这个圆可以出现在屏幕的任何位置、任何角度、任何大小。这就是变换的威力。这篇文章我们搞清楚三件事基础的 translate/rotate/scale 怎么用、save/restore 怎么管理状态、viewport 和 window 坐标映射是什么意思。2. 环境说明本篇代码适用于 Qt 6.5 版本CMake 3.26C17 或更高标准。示例代码依赖 QtGui 和 QtWidgets 模块和上一篇一样。坐标变换是 QPainter 的一部分不需要额外的库。3. 核心概念讲解3.1 translate —— 平移坐标原点translate(dx, dy)的作用是把坐标原点从当前位置移动(dx, dy)的距离。移动之后所有绘图操作的坐标都基于新的原点。voidpaintEvent(QPaintEvent*)override{QPainterpainter(this);painter.setRenderHint(QPainter::Antialiasing);// 默认原点在左上角 (0, 0)painter.setPen(Qt::black);painter.drawRect(0,0,50,50);// 在左上角画一个 50x50 的矩形// 平移原点到 (100, 100)painter.translate(100,100);// 现在原点在 (100, 100)同样的代码画出来的矩形位置不同了painter.setPen(Qt::red);painter.drawRect(0,0,50,50);// 这个矩形在 (100, 100) 位置// 再次平移painter.translate(100,50);// 累加现在原点在 (200, 150)painter.setPen(Qt::blue);painter.drawRect(0,0,50,50);// 这个矩形在 (200, 150) 位置}这里有个非常重要的概念变换是累加的。每次 translate 都是在当前坐标系的基础上继续偏移不是从初始位置重新开始。这就像你给人指路“往前走 100 米再往右走 50 米”而不是每次都从起点开始算。translate 最常见的用途是在循环里画重复图形// 在不同位置画 5 个一模一样的矩形for(inti0;i5;i){painter.drawRect(0,0,40,40);painter.translate(60,0);// 每次往右移 60 像素}不用变换的话你得手动算每个矩形的 x 坐标。用了变换代码清爽多了。3.2 rotate —— 旋转坐标系rotate(angle)的作用是绕当前原点顺时针旋转angle度。注意单位是度不是弧度(挺好的不用算pi什么的)voidpaintEvent(QPaintEvent*)override{QPainterpainter(this);painter.setRenderHint(QPainter::Antialiasing);// 先把原点移到窗口中央painter.translate(width()/2.0,height()/2.0);// 画一个十字标记中心点painter.setPen(Qt::gray);painter.drawLine(-20,0,20,0);painter.drawLine(0,-20,0,20);// 绘制旋转的矩形 —— 每次旋转 30 度QPenpen(Qt::blue,2);painter.setPen(pen);painter.setBrush(QBrush(QColor(100,150,255,80)));for(inti0;i12;i){painter.rotate(30);// 每次旋转 30 度painter.drawRect(50,-10,80,20);// 离原点 50 像素处画矩形}}rotate 有个特别容易踩的坑旋转中心是当前坐标原点不是你画的图形的中心。如果你想让一个矩形绕自己的中心旋转你必须先把原点移到矩形中心然后再旋转。// 想让一个矩形绕自己的中心旋转 45 度// 矩形大小 100x60中心在 (200, 150)// 方法先 translate 到矩形中心再 rotate再 translate 回去偏移一半宽高painter.translate(200,150);// 把原点移到旋转中心painter.rotate(45);// 旋转 45 度painter.translate(-50,-30);// 偏移半个宽高让矩形居中painter.drawRect(0,0,100,60);记住这个口诀先移到旋转中心再旋转再偏移回去。这个顺序不能乱因为变换的叠加顺序和数学上的矩阵乘法一样顺序不同结果完全不同。3.3 scale —— 缩放坐标系scale(sx, sy)的作用是对坐标轴进行缩放。sx是 x 轴缩放比例sy是 y 轴缩放比例。// 放大两倍painter.scale(2.0,2.0);painter.drawRect(10,10,50,50);// 实际显示为 20, 20, 100, 100// 水平翻转x 轴镜像painter.scale(-1,1);// 垂直翻转y 轴镜像painter.scale(1,-1);scale 有个不太直觉的副作用它不仅缩放坐标还缩放画笔宽度和字体大小。如果你scale(2, 2)原本 1 像素宽的线条会变成 2 像素16 号字体会变成 32 号。有时候这是你要的效果有时候不是。如果你只想缩放图形而不缩放线条可以在 scale 之前把画笔宽度设成期望宽度除以缩放比例doubles2.0;painter.scale(s,s);QPenpen(Qt::black,1.0/s);// 这样缩放后线条仍然是 1 物理像素painter.setPen(pen);3.4 save() / restore() —— 保存恢复画笔状态前面说了变换是累加的但很多时候你需要试一试某个变换然后回到之前的状态。这时候save()和restore()就派上用场了。save()会把当前 QPainter 的所有状态画笔、画刷、变换矩阵、裁剪区域等压入一个内部栈。restore()从栈顶弹出并恢复之前保存的状态。voidpaintEvent(QPaintEvent*)override{QPainterpainter(this);painter.setRenderHint(QPainter::Antialiasing);// 初始状态黑色画笔原点在左上角painter.setPen(Qt::black);painter.drawText(10,20,原始状态);// ---- 第一段变换 ----painter.save();// 保存当前状态painter.translate(100,100);painter.rotate(45);painter.setPen(Qt::red);painter.setBrush(QBrush(QColor(255,0,0,80)));painter.drawRect(-30,-30,60,60);painter.restore();// 恢复到 save() 时的状态// ---- 此时回到初始状态 ----// 画笔又变回黑色原点又回到左上角painter.drawText(10,50,恢复后仍然是黑色文字);// ---- 第二段变换 ----painter.save();painter.translate(300,150);painter.scale(1.5,1.5);painter.setPen(Qt::blue);painter.drawRect(0,0,60,40);painter.restore();// ---- 又回到初始状态 ----painter.drawText(10,80,第二次恢复后还是黑色文字);}save/restore 可以嵌套使用最多嵌套大约 32 层具体取决于实现。日常开发中 3-5 层嵌套已经很罕见了。养成一个好习惯每次做变换之前先 save()画完后立刻 restore()。这样你的代码不会因为变换的累加效应而变得难以理解。3.5 视口viewport与窗口window坐标映射这一节讲的是 QPainter 的窗口-视口映射机制它听起来很学术但实际用途很简单让你用自己定义的坐标系来画图而不是被迫使用像素坐标。viewport视口是 Widget 的物理矩形区域单位是像素window窗口是你自定义的逻辑矩形区域单位是你自己定义的。两者之间建立映射关系后QPainter 会自动把你的逻辑坐标转换为物理坐标。voidpaintEvent(QPaintEvent*)override{QPainterpainter(this);// 设置视口使用整个 Widget 区域物理像素painter.setViewport(0,0,width(),height());// 设置窗口逻辑坐标范围 (-100, -100) 到 (100, 100)// 这样原点 (0, 0) 就在窗口正中央了painter.setWindow(-100,-100,200,200);// 现在坐标系变成了// x 范围 [-100, 100]y 范围 [-100, 100]// (0, 0) 在窗口正中央// y 轴仍然是向下的// 画一个以中心为原点的坐标系painter.setPen(Qt::black);painter.drawLine(-100,0,100,0);// x 轴painter.drawLine(0,-100,0,100);// y 轴// 在第一象限画一个矩形注意 y 轴向下painter.setBrush(QBrush(QColor(100,200,100,150)));painter.drawRect(10,-50,40,40);}window/viewport 最经典的用法是画数学图形。比如你想画一个函数图像x 范围 [-pi, pi]y 范围 [-1, 1]。你不用手动把数学坐标换算成像素坐标直接设 window 就行// 数学坐标系x 从 -3.14 到 3.14y 从 -1.5 到 1.5painter.setWindow(-314,-150,628,300);// 画 sin(x) 曲线QPolygonF curve;for(inti-314;i314;i){doublexi/100.0;doubleystd::sin(x);curveQPointF(i*100,static_castint(-y*100));// y 取反是因为屏幕 y 轴向下}painter.drawPolyline(curve);说实话window/viewport 这个功能在一般的应用开发中用得不算多大部分时候 translate/rotate/scale 就够用了。但如果你要画地图、图表、数学图形这种需要特定坐标系的场景这个功能能省你大量的坐标换算代码。3.6 局部坐标系与全局坐标系理解了变换之后你需要建立两个概念全局坐标系是 Widget 的原始坐标系原点在左上角单位是像素局部坐标系是经过 translate/rotate/scale 变换后的坐标系。当你在局部坐标系里画(0, 0)的时候这个点在全局坐标系里的位置取决于你做了什么变换。你可以用QPainter::transform()获取当前的变换矩阵用QPainter::worldTransform().map(QPointF(x, y))把局部坐标转换为全局坐标。painter.translate(100,50);painter.rotate(30);// 局部坐标系里的 (0, 0)在全局坐标系里是哪QPointF globalPospainter.worldTransform().map(QPointF(0,0));qDebug()全局坐标:globalPos;// 大约 (100, 50)// 局部坐标系里的 (50, 0)经过 translate rotate 后的全局坐标QPointF globalPos2painter.worldTransform().map(QPointF(50,0));qDebug()全局坐标:globalPos2;实际开发中最常见的需求是把全局坐标转换成局部坐标——比如鼠标点击事件给你的是全局坐标但你想知道它对应到画面的哪个位置。这时候需要用逆变换voidmousePressEvent(QMouseEvent*event)override{QPainterpainter(this);// ... 设置各种变换 ...// 把鼠标的窗口坐标转换到局部坐标QPointF localPospainter.worldTransform().inverted().map(event-position());qDebug()局部坐标:localPos;}到这里你可以停下来想一想translate、rotate、scale 分别改变了坐标系的什么如果你想让一个矩形绕自身的中心旋转代码的执行顺序应该是什么样的搞清楚这些问题后面写复杂绘图代码就不会手忙脚乱了。4. 踩坑预防坐标变换这块的坑还真不少我们逐个来说。第一个坑也是最经典的rotate 的旋转中心不是图形中心。很多人写了painter.rotate(45)再画矩形发现矩形飞到屏幕外面去了以为没画出来。实际上 rotate 是绕当前坐标原点旋转的不是绕你画的图形的中心。如果你想绕图形自身中心旋转必须先 translate 到图形中心再 rotate再偏移回去半个宽高。这个顺序不能乱——先做什么后做什么直接影响结果因为变换叠加的顺序和矩阵乘法一样顺序不同结果完全不同// 错误直接 rotate图形绕原点旋转飞走了painter.rotate(45);painter.drawRect(100,100,80,60);// 不是绕矩形中心旋转是绕原点旋转// 正确先 translate 到旋转中心再 rotate再偏移回去painter.translate(140,130);// 移到矩形中心 (10040, 10030)painter.rotate(45);painter.translate(-40,-30);// 偏移半个宽高painter.drawRect(0,0,80,60);第二个坑是忘记 save/restore 导致变换累加。这个坑特别隐蔽因为它不会立刻报错而是让你的图形位置和角度变得完全不可预测。比如你在循环里不断 translate 和 rotate但没有用 save/restore 重置状态每循环一次偏移量和旋转角度就累加一次。到最后你完全不知道原点跑哪去了。解决方法很简单每次做变换前 save()画完后 restore()养成习惯就好// 错误循环里不断累加for(inti0;i10;i){painter.translate(50,0);// 每次累加 50 像素painter.rotate(15);// 每次累加 15 度painter.drawRect(0,0,30,30);}// 循环结束后原点已经偏移了 500 像素旋转了 150 度// 正确每次循环内 save/restorefor(inti0;i10;i){painter.save();painter.translate(50i*60,100);painter.rotate(i*15);painter.drawRect(-15,-15,30,30);painter.restore();}第三个坑是 scale 会影响画笔宽度和字体大小。scale(3, 3)之后你会发现线条粗得离谱、文字大得吓人整个画面一团糊。这是因为 scale 缩放的是整个坐标系线宽和字号也跟着缩放了。如果你只想缩放图形本身而保持线宽不变需要手动补偿把画笔宽度设成期望值除以缩放比例字体大小同理doubles3.0;painter.scale(s,s);QPenpen(Qt::black,1.0/s);// 缩放后仍然是 1 像素painter.setPen(pen);QFontfont(Arial,16.0/s);// 缩放后仍然是 16 号painter.setFont(font);第四个坑是 setWindow 的参数理解错误。setWindow(x, y, w, h)的后两个参数是宽度和高度不是右下角坐标——这和drawRect的参数约定是一样的但还是有很多人搞混。比如你想让坐标范围是 [-50, 50]正确的写法是setWindow(-50, -50, 100, 100)因为宽度是 100。如果你写成setWindow(-50, -50, 50, 50)范围就变成了 [-50, 0]画出来的图形位置完全不对或者只显示一部分。接下来做一个代码填空练习补全下面的代码在窗口中央画一个旋转 30 度的矩形voidpaintEvent(QPaintEvent*)override{QPainterpainter(this);painter.setRenderHint(QPainter::________);// 1. 抗锯齿提示// 先保存状态painter.________();// 2. 保存当前状态// 移动原点到窗口中央painter.________(width()/2.0,height()/2.0);// 3. 平移// 旋转 30 度painter.________(30);// 4. 旋转// 偏移半个矩形大小使矩形居中painter.translate(________,-25);// 5. 矩形宽 100偏移量是多少painter.setPen(QPen(Qt::red,2));painter.setBrush(QBrush(QColor(255,100,100,150)));painter.drawRect(0,0,100,50);// 恢复状态painter.________();// 6. 恢复之前保存的状态}再来一道调试挑战这段代码想画一个绕自身中心旋转的矩形但运行后发现矩形飞到了窗口外面。问题出在哪里voidpaintEvent(QPaintEvent*)override{QPainterpainter(this);painter.rotate(45);painter.translate(200,150);painter.drawRect(-40,-30,80,60);}提示想想变换的执行顺序和累加效果。5. 练习项目我们来做一个实战练习创建一个模拟时钟 Widget显示当前时间的时、分、秒针。秒针每秒更新一次使用 QTimer 驱动。完成标准是自定义AnalogClockWidget继承 QWidget使用 translate 把原点移到窗口中央用 rotate 旋转不同角度来画时针、分针、秒针用 QTimer 每秒触发update()刷新画面画表盘包括圆形外框和 12 个刻度标记时针短粗、分针中等、秒针细长颜色区分明显save/restore 正确使用每次画指针前 save画完后 restore。几个提示用QTime::currentTime()获取当前时间秒针角度等于秒数乘以 6每秒 6 度分针角度等于分钟加秒除以 60 再乘以 6时针角度等于小时对 12 取模加分钟除以 60 再乘以 30画指针时先 translate 到中心再 rotate 对应角度再画一条从原点向上的线注意 y 轴向下所以向上是负方向。6. 官方文档参考链接Qt 文档 · QPainter Coordinate System – Qt 坐标系统的完整说明涵盖逻辑坐标、物理坐标、变换矩阵Qt 文档 · QTransform Class – 变换矩阵类的详细文档支持平移、旋转、缩放、剪切等仿射变换Qt 文档 · QPainter::save/restore – 状态栈的保存与恢复机制说明Qt 文档 · Analog Clock Example – Qt 官方的模拟时钟示例非常好的坐标变换学习参考到这里坐标变换的基础你应该掌握了。核心就三件事translate 移原点、rotate 绕原点转、scale 缩放坐标——加上 save/restore 管理状态。变换的顺序很重要先做什么后做什么会直接影响结果。下一篇文章我们换一个话题聊聊 Qt 里的图像处理QImage、QPixmap 和 QIcon。相关阅读现代Qt开发教程新手篇2.1——QPainter 绘图基础 - 相似度 71%通用GUI编程技术——Win32 原生编程实战五十三——子类化与超类化 - 相似度 58%
现代Qt开发教程(新手篇)2.2——坐标系与 QTransform 变换基础
现代Qt开发教程新手篇2.2——坐标系与 QTransform 变换基础相关仓库仍然已经开源正在积极火热的建设之中欢迎各位大佬提Issue和PR链接地址https://github.com/Awesome-Embedded-Learning-Studio/Tutorial_AwesomeQt1. 前言 / 为什么需要坐标变换说实话刚开始学 QPainter 的时候我以为只要会画矩形和圆就够了。直到有一天我需要画一个旋转 45 度的指针仪表盘我盯着屏幕上那根斜着的线脑子里只有一个想法难道我要手算每个点的坐标你想想看一个时钟的秒针每秒钟旋转 6 度。如果纯用手算三角函数去算端点坐标代码会变成什么样子每画一个旋转图形就得写一堆 sin/cos维护起来简直噩梦。更别提还要处理缩放和平移的组合了。坐标变换就是为了解决这个问题而生的。你可以把坐标变换理解成移动画布——你不用重新计算每个点的位置只需要告诉 QPainter把坐标原点移到这儿、“旋转 45 度”、“放大两倍”然后像什么都没发生一样用原来的坐标画画就行。更直白地说坐标变换让你在局部坐标系里工作不用管全局坐标系是什么样。你在 (0, 0) 画一个圆通过变换这个圆可以出现在屏幕的任何位置、任何角度、任何大小。这就是变换的威力。这篇文章我们搞清楚三件事基础的 translate/rotate/scale 怎么用、save/restore 怎么管理状态、viewport 和 window 坐标映射是什么意思。2. 环境说明本篇代码适用于 Qt 6.5 版本CMake 3.26C17 或更高标准。示例代码依赖 QtGui 和 QtWidgets 模块和上一篇一样。坐标变换是 QPainter 的一部分不需要额外的库。3. 核心概念讲解3.1 translate —— 平移坐标原点translate(dx, dy)的作用是把坐标原点从当前位置移动(dx, dy)的距离。移动之后所有绘图操作的坐标都基于新的原点。voidpaintEvent(QPaintEvent*)override{QPainterpainter(this);painter.setRenderHint(QPainter::Antialiasing);// 默认原点在左上角 (0, 0)painter.setPen(Qt::black);painter.drawRect(0,0,50,50);// 在左上角画一个 50x50 的矩形// 平移原点到 (100, 100)painter.translate(100,100);// 现在原点在 (100, 100)同样的代码画出来的矩形位置不同了painter.setPen(Qt::red);painter.drawRect(0,0,50,50);// 这个矩形在 (100, 100) 位置// 再次平移painter.translate(100,50);// 累加现在原点在 (200, 150)painter.setPen(Qt::blue);painter.drawRect(0,0,50,50);// 这个矩形在 (200, 150) 位置}这里有个非常重要的概念变换是累加的。每次 translate 都是在当前坐标系的基础上继续偏移不是从初始位置重新开始。这就像你给人指路“往前走 100 米再往右走 50 米”而不是每次都从起点开始算。translate 最常见的用途是在循环里画重复图形// 在不同位置画 5 个一模一样的矩形for(inti0;i5;i){painter.drawRect(0,0,40,40);painter.translate(60,0);// 每次往右移 60 像素}不用变换的话你得手动算每个矩形的 x 坐标。用了变换代码清爽多了。3.2 rotate —— 旋转坐标系rotate(angle)的作用是绕当前原点顺时针旋转angle度。注意单位是度不是弧度(挺好的不用算pi什么的)voidpaintEvent(QPaintEvent*)override{QPainterpainter(this);painter.setRenderHint(QPainter::Antialiasing);// 先把原点移到窗口中央painter.translate(width()/2.0,height()/2.0);// 画一个十字标记中心点painter.setPen(Qt::gray);painter.drawLine(-20,0,20,0);painter.drawLine(0,-20,0,20);// 绘制旋转的矩形 —— 每次旋转 30 度QPenpen(Qt::blue,2);painter.setPen(pen);painter.setBrush(QBrush(QColor(100,150,255,80)));for(inti0;i12;i){painter.rotate(30);// 每次旋转 30 度painter.drawRect(50,-10,80,20);// 离原点 50 像素处画矩形}}rotate 有个特别容易踩的坑旋转中心是当前坐标原点不是你画的图形的中心。如果你想让一个矩形绕自己的中心旋转你必须先把原点移到矩形中心然后再旋转。// 想让一个矩形绕自己的中心旋转 45 度// 矩形大小 100x60中心在 (200, 150)// 方法先 translate 到矩形中心再 rotate再 translate 回去偏移一半宽高painter.translate(200,150);// 把原点移到旋转中心painter.rotate(45);// 旋转 45 度painter.translate(-50,-30);// 偏移半个宽高让矩形居中painter.drawRect(0,0,100,60);记住这个口诀先移到旋转中心再旋转再偏移回去。这个顺序不能乱因为变换的叠加顺序和数学上的矩阵乘法一样顺序不同结果完全不同。3.3 scale —— 缩放坐标系scale(sx, sy)的作用是对坐标轴进行缩放。sx是 x 轴缩放比例sy是 y 轴缩放比例。// 放大两倍painter.scale(2.0,2.0);painter.drawRect(10,10,50,50);// 实际显示为 20, 20, 100, 100// 水平翻转x 轴镜像painter.scale(-1,1);// 垂直翻转y 轴镜像painter.scale(1,-1);scale 有个不太直觉的副作用它不仅缩放坐标还缩放画笔宽度和字体大小。如果你scale(2, 2)原本 1 像素宽的线条会变成 2 像素16 号字体会变成 32 号。有时候这是你要的效果有时候不是。如果你只想缩放图形而不缩放线条可以在 scale 之前把画笔宽度设成期望宽度除以缩放比例doubles2.0;painter.scale(s,s);QPenpen(Qt::black,1.0/s);// 这样缩放后线条仍然是 1 物理像素painter.setPen(pen);3.4 save() / restore() —— 保存恢复画笔状态前面说了变换是累加的但很多时候你需要试一试某个变换然后回到之前的状态。这时候save()和restore()就派上用场了。save()会把当前 QPainter 的所有状态画笔、画刷、变换矩阵、裁剪区域等压入一个内部栈。restore()从栈顶弹出并恢复之前保存的状态。voidpaintEvent(QPaintEvent*)override{QPainterpainter(this);painter.setRenderHint(QPainter::Antialiasing);// 初始状态黑色画笔原点在左上角painter.setPen(Qt::black);painter.drawText(10,20,原始状态);// ---- 第一段变换 ----painter.save();// 保存当前状态painter.translate(100,100);painter.rotate(45);painter.setPen(Qt::red);painter.setBrush(QBrush(QColor(255,0,0,80)));painter.drawRect(-30,-30,60,60);painter.restore();// 恢复到 save() 时的状态// ---- 此时回到初始状态 ----// 画笔又变回黑色原点又回到左上角painter.drawText(10,50,恢复后仍然是黑色文字);// ---- 第二段变换 ----painter.save();painter.translate(300,150);painter.scale(1.5,1.5);painter.setPen(Qt::blue);painter.drawRect(0,0,60,40);painter.restore();// ---- 又回到初始状态 ----painter.drawText(10,80,第二次恢复后还是黑色文字);}save/restore 可以嵌套使用最多嵌套大约 32 层具体取决于实现。日常开发中 3-5 层嵌套已经很罕见了。养成一个好习惯每次做变换之前先 save()画完后立刻 restore()。这样你的代码不会因为变换的累加效应而变得难以理解。3.5 视口viewport与窗口window坐标映射这一节讲的是 QPainter 的窗口-视口映射机制它听起来很学术但实际用途很简单让你用自己定义的坐标系来画图而不是被迫使用像素坐标。viewport视口是 Widget 的物理矩形区域单位是像素window窗口是你自定义的逻辑矩形区域单位是你自己定义的。两者之间建立映射关系后QPainter 会自动把你的逻辑坐标转换为物理坐标。voidpaintEvent(QPaintEvent*)override{QPainterpainter(this);// 设置视口使用整个 Widget 区域物理像素painter.setViewport(0,0,width(),height());// 设置窗口逻辑坐标范围 (-100, -100) 到 (100, 100)// 这样原点 (0, 0) 就在窗口正中央了painter.setWindow(-100,-100,200,200);// 现在坐标系变成了// x 范围 [-100, 100]y 范围 [-100, 100]// (0, 0) 在窗口正中央// y 轴仍然是向下的// 画一个以中心为原点的坐标系painter.setPen(Qt::black);painter.drawLine(-100,0,100,0);// x 轴painter.drawLine(0,-100,0,100);// y 轴// 在第一象限画一个矩形注意 y 轴向下painter.setBrush(QBrush(QColor(100,200,100,150)));painter.drawRect(10,-50,40,40);}window/viewport 最经典的用法是画数学图形。比如你想画一个函数图像x 范围 [-pi, pi]y 范围 [-1, 1]。你不用手动把数学坐标换算成像素坐标直接设 window 就行// 数学坐标系x 从 -3.14 到 3.14y 从 -1.5 到 1.5painter.setWindow(-314,-150,628,300);// 画 sin(x) 曲线QPolygonF curve;for(inti-314;i314;i){doublexi/100.0;doubleystd::sin(x);curveQPointF(i*100,static_castint(-y*100));// y 取反是因为屏幕 y 轴向下}painter.drawPolyline(curve);说实话window/viewport 这个功能在一般的应用开发中用得不算多大部分时候 translate/rotate/scale 就够用了。但如果你要画地图、图表、数学图形这种需要特定坐标系的场景这个功能能省你大量的坐标换算代码。3.6 局部坐标系与全局坐标系理解了变换之后你需要建立两个概念全局坐标系是 Widget 的原始坐标系原点在左上角单位是像素局部坐标系是经过 translate/rotate/scale 变换后的坐标系。当你在局部坐标系里画(0, 0)的时候这个点在全局坐标系里的位置取决于你做了什么变换。你可以用QPainter::transform()获取当前的变换矩阵用QPainter::worldTransform().map(QPointF(x, y))把局部坐标转换为全局坐标。painter.translate(100,50);painter.rotate(30);// 局部坐标系里的 (0, 0)在全局坐标系里是哪QPointF globalPospainter.worldTransform().map(QPointF(0,0));qDebug()全局坐标:globalPos;// 大约 (100, 50)// 局部坐标系里的 (50, 0)经过 translate rotate 后的全局坐标QPointF globalPos2painter.worldTransform().map(QPointF(50,0));qDebug()全局坐标:globalPos2;实际开发中最常见的需求是把全局坐标转换成局部坐标——比如鼠标点击事件给你的是全局坐标但你想知道它对应到画面的哪个位置。这时候需要用逆变换voidmousePressEvent(QMouseEvent*event)override{QPainterpainter(this);// ... 设置各种变换 ...// 把鼠标的窗口坐标转换到局部坐标QPointF localPospainter.worldTransform().inverted().map(event-position());qDebug()局部坐标:localPos;}到这里你可以停下来想一想translate、rotate、scale 分别改变了坐标系的什么如果你想让一个矩形绕自身的中心旋转代码的执行顺序应该是什么样的搞清楚这些问题后面写复杂绘图代码就不会手忙脚乱了。4. 踩坑预防坐标变换这块的坑还真不少我们逐个来说。第一个坑也是最经典的rotate 的旋转中心不是图形中心。很多人写了painter.rotate(45)再画矩形发现矩形飞到屏幕外面去了以为没画出来。实际上 rotate 是绕当前坐标原点旋转的不是绕你画的图形的中心。如果你想绕图形自身中心旋转必须先 translate 到图形中心再 rotate再偏移回去半个宽高。这个顺序不能乱——先做什么后做什么直接影响结果因为变换叠加的顺序和矩阵乘法一样顺序不同结果完全不同// 错误直接 rotate图形绕原点旋转飞走了painter.rotate(45);painter.drawRect(100,100,80,60);// 不是绕矩形中心旋转是绕原点旋转// 正确先 translate 到旋转中心再 rotate再偏移回去painter.translate(140,130);// 移到矩形中心 (10040, 10030)painter.rotate(45);painter.translate(-40,-30);// 偏移半个宽高painter.drawRect(0,0,80,60);第二个坑是忘记 save/restore 导致变换累加。这个坑特别隐蔽因为它不会立刻报错而是让你的图形位置和角度变得完全不可预测。比如你在循环里不断 translate 和 rotate但没有用 save/restore 重置状态每循环一次偏移量和旋转角度就累加一次。到最后你完全不知道原点跑哪去了。解决方法很简单每次做变换前 save()画完后 restore()养成习惯就好// 错误循环里不断累加for(inti0;i10;i){painter.translate(50,0);// 每次累加 50 像素painter.rotate(15);// 每次累加 15 度painter.drawRect(0,0,30,30);}// 循环结束后原点已经偏移了 500 像素旋转了 150 度// 正确每次循环内 save/restorefor(inti0;i10;i){painter.save();painter.translate(50i*60,100);painter.rotate(i*15);painter.drawRect(-15,-15,30,30);painter.restore();}第三个坑是 scale 会影响画笔宽度和字体大小。scale(3, 3)之后你会发现线条粗得离谱、文字大得吓人整个画面一团糊。这是因为 scale 缩放的是整个坐标系线宽和字号也跟着缩放了。如果你只想缩放图形本身而保持线宽不变需要手动补偿把画笔宽度设成期望值除以缩放比例字体大小同理doubles3.0;painter.scale(s,s);QPenpen(Qt::black,1.0/s);// 缩放后仍然是 1 像素painter.setPen(pen);QFontfont(Arial,16.0/s);// 缩放后仍然是 16 号painter.setFont(font);第四个坑是 setWindow 的参数理解错误。setWindow(x, y, w, h)的后两个参数是宽度和高度不是右下角坐标——这和drawRect的参数约定是一样的但还是有很多人搞混。比如你想让坐标范围是 [-50, 50]正确的写法是setWindow(-50, -50, 100, 100)因为宽度是 100。如果你写成setWindow(-50, -50, 50, 50)范围就变成了 [-50, 0]画出来的图形位置完全不对或者只显示一部分。接下来做一个代码填空练习补全下面的代码在窗口中央画一个旋转 30 度的矩形voidpaintEvent(QPaintEvent*)override{QPainterpainter(this);painter.setRenderHint(QPainter::________);// 1. 抗锯齿提示// 先保存状态painter.________();// 2. 保存当前状态// 移动原点到窗口中央painter.________(width()/2.0,height()/2.0);// 3. 平移// 旋转 30 度painter.________(30);// 4. 旋转// 偏移半个矩形大小使矩形居中painter.translate(________,-25);// 5. 矩形宽 100偏移量是多少painter.setPen(QPen(Qt::red,2));painter.setBrush(QBrush(QColor(255,100,100,150)));painter.drawRect(0,0,100,50);// 恢复状态painter.________();// 6. 恢复之前保存的状态}再来一道调试挑战这段代码想画一个绕自身中心旋转的矩形但运行后发现矩形飞到了窗口外面。问题出在哪里voidpaintEvent(QPaintEvent*)override{QPainterpainter(this);painter.rotate(45);painter.translate(200,150);painter.drawRect(-40,-30,80,60);}提示想想变换的执行顺序和累加效果。5. 练习项目我们来做一个实战练习创建一个模拟时钟 Widget显示当前时间的时、分、秒针。秒针每秒更新一次使用 QTimer 驱动。完成标准是自定义AnalogClockWidget继承 QWidget使用 translate 把原点移到窗口中央用 rotate 旋转不同角度来画时针、分针、秒针用 QTimer 每秒触发update()刷新画面画表盘包括圆形外框和 12 个刻度标记时针短粗、分针中等、秒针细长颜色区分明显save/restore 正确使用每次画指针前 save画完后 restore。几个提示用QTime::currentTime()获取当前时间秒针角度等于秒数乘以 6每秒 6 度分针角度等于分钟加秒除以 60 再乘以 6时针角度等于小时对 12 取模加分钟除以 60 再乘以 30画指针时先 translate 到中心再 rotate 对应角度再画一条从原点向上的线注意 y 轴向下所以向上是负方向。6. 官方文档参考链接Qt 文档 · QPainter Coordinate System – Qt 坐标系统的完整说明涵盖逻辑坐标、物理坐标、变换矩阵Qt 文档 · QTransform Class – 变换矩阵类的详细文档支持平移、旋转、缩放、剪切等仿射变换Qt 文档 · QPainter::save/restore – 状态栈的保存与恢复机制说明Qt 文档 · Analog Clock Example – Qt 官方的模拟时钟示例非常好的坐标变换学习参考到这里坐标变换的基础你应该掌握了。核心就三件事translate 移原点、rotate 绕原点转、scale 缩放坐标——加上 save/restore 管理状态。变换的顺序很重要先做什么后做什么会直接影响结果。下一篇文章我们换一个话题聊聊 Qt 里的图像处理QImage、QPixmap 和 QIcon。相关阅读现代Qt开发教程新手篇2.1——QPainter 绘图基础 - 相似度 71%通用GUI编程技术——Win32 原生编程实战五十三——子类化与超类化 - 相似度 58%