首页 Qt 学习之路 2 Qt 学习之路 2(28):坐标系统

Qt 学习之路 2(28):坐标系统

65 5.7K

在经历过实际操作,以及前面一节中我们见到的那个translate()函数之后,我们可以详细了解下 Qt 的坐标系统了。泛泛而谈坐标系统,有时候会觉得枯燥无味,难以理解,好在现在我们已经有了基础。

坐标系统是由QPainter控制的。我们前面说过,QPaintDeviceQPaintEngineQPainter是 Qt 绘制系统的三个核心类。QPainter用于进行绘制的实际操作;QPaintDevice是那些能够让QPainter进行绘制的“东西”(准确的术语叫做,二维空间)的抽象层(其子类有QWidgetQPixmapQPictureQImageQPrinter等);QPaintEngine提供供QPainter使用的用于在不同设备上绘制的统一的接口。

由于QPaintDeice是进行绘制的对象,因此,所谓坐标系统,也就是QPaintDevice上面的坐标。默认坐标系统位于设备的左上角,也就是坐标原点 (0, 0)。x 轴方向向右;y 轴方向向下。在基于像素的设备上(比如显示器),坐标的默认单位是像素,在打印机上则是点(1/72 英寸)。

QPainter的逻辑坐标与QPaintDevice的物理坐标进行映射的工作,是由QPainter的变换矩阵(transformation matrix)、视口(viewport)和窗口(window)完成的。如果你不理解这些术语,可以简单了解下有关图形学的内容。实际上,对图形的操作,底层的数学都是进行的矩阵变换、相乘等运算。

在 Qt 的坐标系统中,每个像素占据 1x1 的空间。你可以把它想象成一张方格纸,每个小格都是1个像素。方格的焦点定义了坐标,也就是说,像素 (x, y) 的中心位置其实是在 (x + 0.5, y + 0.5) 的位置上。这个坐标系统实际上是一个“半像素坐标系”。我们可以通过下面的示意图来理解这种坐标系:

坐标系统图示

我们使用一个像素的画笔进行绘制,可以看到,每一个绘制像素都是以坐标点为中心的矩形。注意,这是坐标的逻辑表示,实际绘制则与此不同。因为在实际设备上,像素是最小单位,我们不能像上面一样,在两个像素之间进行绘制。所以在实际绘制时,Qt 的定义是,绘制点所在像素是逻辑定义点的右下方的像素。

我们前面已经介绍过,Qt 的绘制分为走样和反走样两种。对此,我们必须分别对待。

一个像素的绘制最简单,我们从这里开始:

从上图可以看出,当我们绘制矩形左上角 (1, 2) 时,实际绘制的像素是在右下方。

当绘制大于1个像素时,情况比较复杂:如果绘制像素是偶数,则实际绘制会包裹住逻辑坐标值;如果是奇数,则是包裹住逻辑坐标值,再加上右下角一个像素的偏移。具体请看下面的图示:

多像素绘制图示

从上图可以看出,如果实际绘制是偶数像素,则会将逻辑坐标值夹在相等的两部分像素之间;如果是奇数,则会在右下方多出一个像素。

Qt 的这种处理,带来的一个问题是,我们可能获取不到真实的坐标值。由于历史原因,QRect::right()QRect::bottom()的返回值并不是矩形右下角点的真实坐标值:QRect::right()返回的是 left() + width() - 1;QRect::bottom()则返回 top() + height() - 1,上图的绿色点指出了这两个函数的返回点的坐标。

为避免这个问题,我们建议是使用QRectFQRectF使用浮点值,而不是整数值,来描述坐标。这个类的两个函数QRectF::right()QRectF::bottom()是正确的。如果你不得不使用QRect,那么可以利用 x() + width() 和 y() + height() 来替代 right() 和 bottom() 函数。

对于反走样,实际绘制会包裹住逻辑坐标值:

反走样绘制图示

这里我们不去解释为什么在反走样是,像素颜色不是一致的,这是由于反走样算法导致,已经超出本节的内容。

Qt 同样提供了坐标变换。前面说,图形学大部分算法依赖于矩阵计算,坐标变换便是其中的代表:每一种变换都对应着一个矩阵乘法(如果你想知道学的线性代数有什么用处,这就是应用之一了 ;-P)。我们会以一个实际的例子来了解坐标变换。在此之前,我们需要了解两个函数:QPainter::save()QPainter::restore()

前面说过,QPainter是一个状态机。那么,有时我想保存下当前的状态:当我临时绘制某些图像时,就可能想这么做。当然,我们有最原始的办法:将可能改变的状态,比如画笔颜色、粗细等,在临时绘制结束之后再全部恢复。对此,QPainter提供了内置的函数:save()restore()save()就是保存下当前状态;restore()则恢复上一次保存的结果。这两个函数必须成对出现:QPainter使用栈来保存数据,每一次save(),将当前状态压入栈顶,restore()则弹出栈顶进行恢复。

在了解了这两个函数之后,我们就可以进行示例代码了:

void PaintDemo::paintEvent(QPaintEvent *)
{
    QPainter painter(this);
    painter.fillRect(10, 10, 50, 100, Qt::red);
    painter.save();
    painter.translate(100, 0); // 向右平移 100px
    painter.fillRect(10, 10, 50, 100, Qt::yellow);
    painter.restore();
    painter.save();
    painter.translate(300, 0); // 向右平移 300px
    painter.rotate(30); // 顺时针旋转 30 度
    painter.fillRect(10, 10, 50, 100, Qt::green);
    painter.restore();
    painter.save();
    painter.translate(400, 0); // 向右平移 400px
    painter.scale(2, 3); // 横坐标单位放大 2 倍,纵坐标放大 3 倍
    painter.fillRect(10, 10, 50, 100, Qt::blue);
    painter.restore();
    painter.save();
    painter.translate(600, 0); // 向右平移 600px
    painter.shear(0, 1); // 横向不变,纵向扭曲 1 倍
    painter.fillRect(10, 10, 50, 100, Qt::cyan);
    painter.restore();
}

Qt 提供了四种坐标变换:平移 translate,旋转 rotate,缩放 scale 和扭曲 shear。在这段代码中,我们首先在 (10, 10) 点绘制一个红色的 50x100 矩形。保存当前状态,将坐标系平移到 (100, 0),绘制一个黄色的矩形。注意,translate()操作平移的是坐标系,不是矩形。因此,我们还是在 (10, 10) 点绘制一个 50x100 矩形,现在,它跑到了右侧的位置。然后恢复先前状态,也就是把坐标系重新设为默认坐标系(相当于进行translate(-100, 0)),再进行下面的操作。之后也是类似的。由于我们只是保存了默认坐标系的状态,因此我们之后的translate()横坐标值必须增加,否则就会覆盖掉前面的图形。所有这些操作都是针对坐标系的,因此在绘制时,我们提供的矩形的坐标参数都是不变的。

运行结果如下:

坐标变换示例

Qt 的坐标分为逻辑坐标和物理坐标。在我们绘制时,提供给QPainter的都是逻辑坐标。之前我们看到的坐标变换,也是针对逻辑坐标的。所谓物理坐标,就是绘制底层QPaintDevice的坐标。单单只有逻辑坐标,我们是不能在设备上进行绘制的。要想在设备上绘制,必须提供设备认识的物理坐标。Qt 使用 viewport-window 机制将我们提供的逻辑坐标转换成绘制设备使用的物理坐标,方法是,在逻辑坐标和物理坐标之间提供一层“窗口”坐标。视口是由任意矩形指定的物理坐标;窗口则是该矩形的逻辑坐标表示。默认情况下,物理坐标和逻辑坐标是一致的,都等于设备矩形。

视口坐标(也就是物理坐标)和窗口坐标是一个简单的线性变换。比如一个 400x400 的窗口,我们添加如下代码:

void PaintDemo::paintEvent(QPaintEvent *)
{
    QPainter painter(this);
    painter.setWindow(0, 0, 200, 200);
    painter.fillRect(0, 0, 200, 200, Qt::red);
}

我们将窗口矩形设置为左上角坐标为 (0, 0),长和宽都是 200px。此时,坐标原点不变,还是左上角,但是,对于原来的 (400, 400) 点,新的窗口坐标是 (200, 200)。我们可以理解成,逻辑坐标被“重新分配”。这有点类似于translate(),但是,translate()函数只是简单地将坐标原点重新设置,而setWindow()则是将整个坐标系进行了修改。这段代码的运行结果是将整个窗口进行了填充。

试比较下面两行代码的区别(还是 400x400 的窗口):

painter.translate(200, 200);
painter.setWindow(-160, -320, 320, 640);

第一行代码,我们将坐标原点设置到 (200, 200) 处,横坐标范围是 [-200, 200],纵坐标范围是 [-200, 200]。第二行代码,坐标原点也是在窗口正中心,但是,我们将物理宽 400px 映射成窗口宽 320px,物理高 400px 映射成窗口高 640px,此时,横坐标范围是 [-160, 160],纵坐标范围是 [-320, 320]。这种变换是简单的线性变换。假设原来有个点坐标是 (64, 60),那么新的窗口坐标下对应的坐标应该是 ((-160 + 64 * 320 / 400), (-320 + 60 * 640 / 400)) = (-108.8, -224)。

下面我们再来理解下视口的含义。还是以一段代码为例:

void PaintDemo::paintEvent(QPaintEvent *)
{
    QPainter painter(this);
    painter.setViewport(0, 0, 200, 200);
    painter.fillRect(0, 0, 200, 200, Qt::red);
}

这段代码和前面一样,只是把setWindow()换成了setViewport()。前面我们说过,window 代表窗口坐标,viewport 代表物理坐标。也就是说,我们将物理坐标区域定义为左上角位于 (0, 0),长高都是 200px 的矩形。然后还是绘制和上面一样的矩形。如果你认为运行结果是 1/4 窗口被填充,那就错了。实际是只有 1/16 的窗口被填充。这是由于,我们修改了物理坐标,但是没有修改相应的窗口坐标。默认的逻辑坐标范围是左上角坐标为 (0, 0),长宽都是 400px 的矩形。当我们将物理坐标修改为左上角位于 (0, 0),长高都是 200px 的矩形时,窗口坐标范围不变,也就是说,我们将物理宽 200px 映射成窗口宽 400px,物理高 200px 映射成窗口高 400px,所以,原始点 (200, 200) 的坐标变成了 ((0 + 200 * 200 / 400), (0 + 200 * 200 / 400)) = (100, 100)。

现在我们可以用一张图示总结一下逻辑坐标、窗口坐标和物理坐标之间的关系:

逻辑坐标-窗口坐标-物理坐标图示

我们传给QPainter的是逻辑坐标(也称为世界坐标),逻辑坐标可以通过变换矩阵转换成窗口坐标,窗口坐标通过 window-viewport 转换成物理坐标(也就是设备坐标)。

65 评论

adfae 2012年11月25日 - 17:47

你好,我是初学者,请问一下,关于压入栈顶,弹出栈顶...
这些词汇和栈内存之间有什么不同吗?

回复
DevBean 2012年11月25日 - 23:43

含义是一样的,都是后入先出,你可以理解成栈的行为,但是它并不一定是使用栈实现。

回复
adfae 2012年11月27日 - 21:39

明白了,tks

回复
wavelee 2013年4月5日 - 20:04

当绘制大于1个像素时,情况比较复杂:如果绘制像素是奇数,则实际绘制会包裹住逻辑坐标值;如果是偶数,则是包裹住逻辑坐标值,再加上右下角一个像素的便宜。
这句话好像有问题,我的理解应当是:
当绘制大于1个像素时,情况比较复杂:如果绘制像素是偶数,则实际绘制会包裹住逻辑坐标值;如果是奇数,则是包裹住逻辑坐标值,再加上右下角一个像素的便宜。

回复
豆子 2013年4月8日 - 08:56

的确是文中写错了,现在已经修改过来了哦~感谢指出!

回复
Jakes 2013年5月2日 - 23:18

还有一个地方没修改哦。在QRectF图的下方那段话。还有“前面说,图形学大部分算法依赖于矩形计算,坐标变换便是其中的代表”这句话中,“矩形计算”还是“矩阵计算”?

回复
豆子 2013年5月3日 - 11:11

矩阵计算,多谢指出!

回复
Gbcays 2014年2月9日 - 02:25

第28节的图片出好像出问题了

回复
豆子 2014年2月11日 - 16:15

哪里的图片有问题?我这边是正常的

回复
Gbcays 2014年3月31日 - 18:06

对不起啊,我当时的网络可能不太好

回复
Gbcays 2014年3月31日 - 18:12

顺便表示下支持,Qt5的教程太少了

回复
Const_Lin 2014年3月9日 - 14:08

豆子你好。
“从上图可以看出,如果实际绘制是奇数像素,则会将逻辑坐标值夹在相等的两部分像素之间;如果是偶数,则会在右下方多出一个像素。”
这句话好像和上面的话不对应

回复
豆子 2014年3月10日 - 11:04

这里有错误,已经修改过了,感谢指出!

回复
朦朦胧胧 2014年4月3日 - 10:02

你好,请问,这个代码:
QPainter painter(this);
painter.setWindow(0, 0, 200, 200);
painter.fillRect(0, 0, 200, 200, Qt::red);
经过测式,确实是填充了整个窗口,可是仍然不明白setwindow的实际意义,下面的更加不明白,能否再简单讲解一下呀?

回复
豆子 2014年4月3日 - 16:01

上面的例子,窗口是 400x400 的,setWindow 函数设置的大小是 200x200。以宽度为例,如果没有 setWindow,假设窗口坐标范围是 [0, 399];设置之后变成 [0, 199],也就是说,我们设置了一个新的坐标系,在这个新的坐标系中,单位长度等于原坐标系的两倍,即原坐标系 2 个单位长度相当于新坐标系 1 个单位长度。

回复
朦朦胧胧 2014年4月3日 - 21:56

感觉painter.setWindow是对这块画布的放大处理,painter.setViewport则刚好相反。

回复
豆子 2014年4月4日 - 08:55

setWindow 实际上还是对坐标的处理。因为文中示例代码是放大了坐标,如果你要缩小坐标,比如 setWindow(0, 0, 800, 800),也会是缩小的样子。这不是单纯的缩放处理。setWindow 可以应用于类似这样的场景:考虑像 Photoshop 这样的图像处理程序,一张图片有 10000x10000 像素,你肯定会提供一种缩放机制,让整个图片可以在屏幕完整呈现。此时,我们通过这种坐标“映射”,就可以很好的处理这种缩放之后的坐标运算。

回复
朦朦胧胧 2014年4月5日 - 10:28

尝试了setWindow(0, 0, 800, 800)确实变少了。我觉得我对座标,座标系之类的认识果然是一片空白,而且也不确定qt的绘图系统有哪些座标,各自处理什么事情。你的学习之路2我从头看到网络章节,就唯独绘图和gvf不太明白,特别是gvf里很多函数还不是很明白它的作用,却马上要进入食吃蛇的实战,太在是太高难度了,希望有时间的话,能再补充一些基本知识供新手了解。

朦朦胧胧 2014年4月5日 - 10:35

我觉得问题还是因为我缺少一些底层知识,例如像渲染、绘图路径、访射和非访射变形体,这些词汇从来没听说过,我想学习QT 绘图系统前应先去了解一翻,请教一下,我应该找哪些书来了解这些底层知识呢,可否推荐几本,或告诉我该向哪个方面找相关知识?

豆子 2014年4月8日 - 10:49

这些内容都是计算机图形学部分的,你可以找本图形学的书看看。

Alien You 2017年5月8日 - 17:21

意思是说假如我们的图形超过了所给定的setWindow范围,那就是会进行缩放?那么我们所给的这个坐标范围的实际意义是什么?

豆子 2017年5月8日 - 23:11

可以实现放大缩小的功能

blablabla 2014年4月4日 - 17:24

你好,关于setViewport() 那部分为什么坐标(100,100)会填充1/16的窗口?(100,100)不是转换后的物理坐标吗?

回复
Laputa 2014年6月1日 - 13:02

我也认为是这样的,而且我尝试了以下代码
painter.setViewport(0, 0, 400, 400);
painter.fillRect(0, 0, 200, 200, Qt::red);

painter.setViewport(0, 0, 200, 200);
painter.fillRect(0, 0, 200, 200, Qt::green);

的确填充的是1/4而不是1/16

回复
豆子 2014年6月3日 - 10:07

如果你说的是绿色矩形占据了红色的 1/4,那是因为 setViewport 始终针对的是整个窗口。第一个 setViewport 调用绘制了 1/4 窗口的红色矩形;第二个 setViewport 将坐标又缩小一倍,因此是第一个红色矩形的 1/4,整个窗口的 1/16。

回复
zz 2019年8月2日 - 14:18

也在学习中,理解如下:
setViewPort 默认视口与窗口一样大小,第一句没有一样,绘图以逻辑坐标为参照。
setViewPort(0,0,200,200)改为视口为绘图区域的1/4,左上角。
这时1个一个逻辑单位长度代表0.5像素,或者说以当前视口作为整个绘图来看,在画painter.fillRect(0, 0, 200, 200, Qt::green);是在1/4又画了1/4,而以像素来看,200*0.5 确实为100像素 而100像素对比整个绘图区域确实为1/16.

回复
hyperfox 2014年8月1日 - 23:55

我觉得你那个(64, 60)坐标变换有问题,按照文意(64, 60)应该是在将原点设为(200,200)以后的坐标,这样的话后面的(-108.8, -224)就错了,变换之前明明在第一象限的坐标变换完怎么跑第三象限去了?

回复
豆子 2014年8月2日 - 15:25

(64, 60)点坐标应该是在 translate() 函数调用之前的“原坐标点”,这样说明的话是不是正确了呢?

回复
lizhenneng 2015年4月21日 - 14:21

"由于历史原因,QRect::right()和QRect::bottom()的返回值并不是矩形右下角点的真实坐标值"
历史原因能具体解释下吗?

回复
豆子 2015年4月24日 - 16:28

“历史原因”就是,老版本代码就是这么返回的 ;-P

回复
zhudx6512 2015年7月26日 - 21:42

豆子你好。
关于Viewport的讲解我还不是很理解。
设置成0,0,200,200之后,文中说物理200映射窗口400,那用(0,0,400,400)不是应该填充满的吗。。。(0,0,200,200)不是1/4吗。。。实际操作起来的确是1/16,不知道为什么。想了挺久没想明白。希望你能再讲解一下。
谢谢!

回复
豆子 2015年8月1日 - 15:04

物理坐标的 200 映射成窗口坐标的 400,其比例为 200:400。在绘制时使用的是窗口坐标,因此,绘制时的 200 需要按照上述比例进行换算,才能够得到实际的坐标,换算公式为:x : 200 = 200 : 400,x = 200 * 200 / 400 = 100,由此得出实际坐标为 100,而窗口为 400,因此长度是 1/4,面积就是 1/16

回复
guoyinbo2012 2016年3月21日 - 15:36

按照这样算出来的100是物理坐标吧,绘制图形是用的窗口坐标,把这个物理坐标和窗口坐标比没有物理意义啊?一直没懂,虽然显示结果是对的。还有,此时的窗口坐标范围是0—400,若想让红色填满整个窗口,岂不是要达到1600,能够这样吗?

回复
豆子 2016年3月21日 - 21:27

物理坐标其实就是视口坐标,按照计算机图形学原理,屏幕显示的内容是由视口和实际内容共同决定的。这类似于你使用窗口坐标绘制图形,但是实际的显示不仅取决于你绘制的内容,还取决于你观察的范围(也就是视口)。所以,既然你已经调整了视口,实际绘制的坐标肯定也要变化的。

回复
bin 2016年9月9日 - 17:42

呈现给我们的图案是以什么坐标来显示的

白杨 2015年8月2日 - 19:13

豆子老师您好 我现在想做一个旋转动画。用到QMatrix,但是我用translate函数调整旋转中心却不起作用。像这样
QPixmap m(100,100);
...
t.translate(50,50);
...
可这样旋转中心还是在左上角。请问该怎么改?

回复
奋斗的蜗牛 2015年8月20日 - 16:27

你好博主,我现在重写了Qgraphicview类,加载一张图像item,我想实现的是图像随着鼠标的拖动循环平移的感觉,就是一边超出边界的话会从另一边接着进入,但是现在用translate进行图像的平移,只可以实现平移并没有循环,请问要实现我那样的效果有啥方法么?

回复
wild_kid 2015年8月29日 - 19:57

博主你好,非常感谢你的教程,让我可以学到QT,再我想请教下void PaintDemo::paintEvent(QPaintEvent *)里面的形产就这样为什么不会报错,我有点不理解,可以指点一下吗?

回复
豆子 2015年8月31日 - 22:16

这是标准 C++ 的语法,并不是 Qt 特有的。对一般 C++ 编译器而言,如果函数体中有未使用的形参,一般会给出一个警告。为了避免这种警告,可以选择不写形参名字,即如文中所示,代表该形参并不会在函数中使用到。因此,这种写法仅仅是为了避免生成编译器警告的。

回复
yes_man 2015年10月7日 - 20:58

豆子兄,照目前来看,诺基亚确实不行了,那QT的出路最终会是什么?嵌入式设备还是单纯的linux应用软件开发?反正在Windows系统是看不到什么优势,效率不如C++,容易程度不如C#。最要命的是用QT写个界面体积真的好大好大好大。。。

回复
豆子 2015年10月9日 - 10:03

Qt 现在已经不属于诺基亚了,之前出售给 digia 公司。Qt 最大的特性还是跨平台吧,单一 Windows 平台效率的确不如 MFC 和 C#。

回复
hlx1996 2015年11月26日 - 20:52

Qt的这种处理,带来的一个问题是,我们可能获取不到真实的坐标值。由于历史原因,QRect::right()和QRect::bottom()的返回值并不是矩形右下角点的真实坐标值:QRect::right()返回的是 left() + width() – 1;QRect::bottom()则返回 top() + height() – 1,上图的绿色点指出了这两个函数的返回点的坐标。

为避免这个问题,我们建议是使用QRectF。QRectF使用浮点值,而不是整数值,来描述坐标。这个类的两个函数QRectF::right()和QRectF::bottom()是正确的。如果你不得不使用QRect,那么可以利用 x() + width() 和 y() + height() 来替代 right() 和 bottom() 函数。

还是没能理解QRect和QRectF的区别。请问QRectF最后返回的是哪个点?
是说笔的粗细为1时QRectF返回的是真实坐标;还是无论笔的粗细,QRectF都返回真实坐标?

回复
豆子 2015年11月26日 - 20:56

可以认为 QRectF 返回的就是真实的坐标,这样理解可能更好一些。

回复
张文 2016年1月8日 - 16:26

横坐标范围是 [-160, 160],纵坐标范围是 [-320, 320]。这种变换是简单的线性变换。假设原来有个点坐标是 (64, 60),那么新的窗口坐标下对应的坐标应该是 ((-160 + 64 * 320 / 400), (-320 + 60 * 640 / 400)) = (-108.8, -224)。这个计算没有问题?为什么要加上-160和-320呢?这个又不是原点啊,你算出来的位置跟之前给出的坐标(64,60),这明显对应的位置不同啊

回复
11 2016年1月19日 - 16:45

将坐标系平移到 (100, 0)
应该是
将坐标系平移 (100, 0)

回复
Zaccurli 2016年1月27日 - 10:05

一个400*400的窗口:
如果setWindow(0, 0, 200, 200)就是原本窗口大小不变,只是坐标系变成了200*200的大小。逻辑上,即原本(400,400)的点变成了(200,200);
如果setViewport(0, 0, 200, 200)就是原本窗口大小不变,只是逻辑坐标上400*400的坐标系变成窗口大小的1/4。逻辑上,即原本(200,200)的点变成了(400,400);
这样理解对吗?

回复
豆子 2016年1月27日 - 12:54

是的,基本是这么理解的

回复
songcymai 2016年3月7日 - 21:59

你回复的上一个最后一段话“如果setViewport(0, 0, 200, 200)就是原本窗口大小不变,只是逻辑坐标上400*400的坐标系变成窗口大小的1/4。逻辑上,即原本(200,200)的点变成了(400,400);”的最后一句不应该是“即原本(200,200)的点变成了(400,400);”吗?

回复
songcymai 2016年3月7日 - 22:04

豆子你好!这里所说的“逻辑坐标”、“视口坐标”、“窗口坐标”单位是一样的吗?是不是都是在同一个界面设备下的一个像素的大小?

回复
豆子 2016年3月8日 - 10:17

这个文档上没有明确指出,不过个人感觉应该是的

回复
程可可 2016年3月17日 - 17:28

请问你是用什么工具写博客呢?

回复
豆子 2016年3月18日 - 15:54

就是 wordpress 自己的编辑器

回复
holy 2016年7月9日 - 08:03

关于坐标系统,感觉得您对坐标系统解释的可能有一点问题,我有以下的修改建议:

Qt明显使用了引入着色器前固定管线下的OpenGL的术语。

1. translate、rotate这些函数,在图形学中统称为模型变换(Model),意思就是对单个对象施加变换。通常对于每一个对象进行变换前,首先使用resetMatrix()(加载单位矩阵到栈顶,即重置变换),然后在进行rotate、shear等等变换(本质是对象的顶点坐标与栈顶的矩阵做乘法),最后进行translate(移动到世界坐标系中)。

简单来说,这些函数就是决定了一下要绘制的对象的位置、角度等参数,始终没有改变逻辑坐标系(世界坐标系)。

2. setViewport是设置视口的意思,本质上就是将物理设备的坐标映射到逻辑坐标系上。具体就是物理设备的原点(左上角,(0,0)),映射为前两个参数指定的逻辑坐标;物理设备的尺寸映射为后面两个坐标。

总的来说,绘制一个图形,可以大致认为经历了连个阶段。第一个阶段是把这个图形放到了逻辑坐标系中(包括放大缩小斜切各种折腾),这个过程始终没有改变逻辑坐标系;第二个阶段是把逻辑坐标系内所有的图形整体经过变换,放到了物理坐标上。

这些函数在现在的OpenGL绘制中,都已经变成了由用户手写的矩阵乘法。

回复
zx 2017年1月26日 - 13:58

豆子哥 你的这个网站直接用的现成博客程序吗 有没有自己修改过

回复
豆子 2017年1月27日 - 15:40

直接使用的 wordpress 的,没怎么修改

回复
Alien You 2017年5月8日 - 15:33

豆哥,我觉得您在解释偶数像素和奇数像素的时候为什么不用更直观的坐标图?我觉得那样更清晰的可以看出实际坐标和像素位之间的关系吧?

回复
豆子 2017年5月8日 - 23:08

因为这个图是官方给出的...

回复
Alien You 2017年5月8日 - 17:11

豆哥,我想知道那个算映射坐标的式子是怎么得来的,为什么这么算

回复
豆子 2017年5月8日 - 23:10

列方程计算即可,映射前后的比例是相同的

回复
ben 2017年10月10日 - 16:19

博主,看了视口的内容,也看到问题中有好几个都是不理解1/16这个问题,还是不理解为什么是1/16。
painter.setViewport(0, 0, 200, 200);
painter.fillRect(0, 0, 200, 200, Qt::red);

1. setViewport()后,painter.fillRect()使用的还是窗口坐标(逻辑坐标),转换成物理坐标是(100,100),实际的物理窗口大小是200X200,那不是占1/4吗?
2.如果说实际的物理窗口大小是(400X400),那是占1/16没错,但是窗口虽然是(400, 400),但是这不是逻辑坐标吗?还是说resize(400, 400)是QWidget的大小,所以(400, 400)是物理坐标?

回复
Booth 2019年5月14日 - 18:17

窗口的部分,我实验了几次后是这么理解的:
1. 我们的实际窗口大小为400*400(在类构造函数的resize函数里设定),然后将视口坐标范围设置200*200,就是0-200的坐标范围只占了实际窗口大小的1/4;
2. 将200*200的视口坐标映射为400*400逻辑坐标(即逻辑坐标刻度比视口坐标更密一倍),实际上400*400物理坐标的整个窗口如果映射为逻辑坐标,其大小应该是800*800,因此填充矩形200*200正好是1/16。
3. 由此可以推断,setViewport()的物理坐标大小和整个窗口的物理大小,两者本身是相对独立的(都是由自身上级的逻辑坐标映射而来的),所以我们调整实际窗口大小时,红色矩形的大小不变

回复
davy 2019年8月14日 - 20:26

豆子兄, Qt绘图中只要调用update()函数进行窗口部件更新时,不管是局部更新还是整体更新,都会调用paintEvent()函数的,不是都把paintEvent函数里面的代码重新执行了一遍吗,那Qt是怎么做到局部更新的呢??

回复
豆子 2019年8月16日 - 19:43

这个我没有看源代码,但的确是重新执行了一遍,可能是在执行之前先全部清空,然后再全部绘制。所以说这样的全部绘制性能较低。

回复
enak 2020年8月26日 - 19:13

最后一个例子, fillRect是针对窗口还是视口坐标的?如果是针对窗口,窗口400没变,200还是占400的一半啊,为啥是1/16。应该是我理解错了,但200*(200/400)的根据在哪里,窗口和视口之间除了对应两个不同概念的坐标系,没有什么其他联系吗,这里并没有讲好像。如果两者需要有一种比例上的对应关系,那么这个关系是什么呢。
painter.setViewport(0, 0, 200, 200);
painter.fillRect(0, 0, 200, 200, Qt::red);

回复
Nephren 2021年6月26日 - 13:11

通过实验大概理解了:
1.窗口和视口坐标的设置就是为了得到一个逻辑坐标到物理坐标的变换,不设置的话就是真实的设备坐标。
2.绘制都是用的逻辑坐标。
3.都不设置那变换算出来就是单位阵,和设置为一样的作用是一样的。如果固定其中一个,那缩放窗口是=时变换就是动态的,图形就会跟着缩放。只是一个正反方向的问题。
4.而至于QPainter本身对逻辑坐标的变换就是改了原本逻辑坐标的坐标系(原点和坐标轴)。具体要怎么用还得多实践。

回复

回复 guoyinbo2012 取消回复

关于我

devbean

devbean

豆子,生于山东,定居南京。毕业于山东大学软件工程专业。软件工程师,主要关注于 Qt、Angular 等界面技术。

主题 Salodad 由 PenciDesign 提供 | 静态文件存储由又拍云存储提供 | 苏ICP备13027999号-2