Qt 学习之路:坐标系统
在经历过实际操作,以及前面一节中我们见到的那个translate()
函数之后,我们可以详细了解下 Qt 的坐标系统了。泛泛而谈坐标系统,有时候会觉得枯燥无味,难以理解,好在现在我们已经有了基础。
坐标系统是由QPainter
控制的。我们前面说过,QPaintDevice
、QPaintEngine
和QPainter
是 Qt 绘制系统的三个核心类。QPainter
用于进行绘制的实际操作;QPaintDevice
是那些能够让QPainter
进行绘制的“东西”(准确的术语叫做,二维空间)的抽象层(其子类有QWidget
、QPixmap
、QPicture
、QImage
和QPrinter
等);QPaintEngine
提供供QPainter
使用的用于在不同设备上绘制的统一的接口。
由于QPaintDeice
是进行绘制的对象,因此,所谓坐标系统,也就是QPaintDevice
上面的坐标。默认坐标系统位于设备的左上角,也就是坐标原点 (0, 0)。x 轴方向向右;y 轴方向向下。在基于像素的设备上(比如显示器),坐标的默认单位是像素,在打印机上则是点(1/72 英寸)。
将QPainter
的逻辑坐标与QPaintDevice
的物理坐标进行映射的工作,是由QPainter
的变换矩阵(transformation matrix)、视口(viewport)和窗口(window)完成的。如果你不理解这些术语,可以简单了解下有关图形学的内容。实际上,对图形的操作,底层的数学都是进行的矩阵变换、相乘等运算。
在 Qt 的坐标系统中,每个像素占据 1×1 的空间。你可以把它想象成一张方格纸,每个小格都是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,上图的绿色点指出了这两个函数的返回点的坐标。
为避免这个问题,我们建议是使用QRectF
。QRectF
使用浮点值,而不是整数值,来描述坐标。这个类的两个函数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()
则弹出栈顶进行恢复。
在了解了这两个函数之后,我们就可以进行示例代码了:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
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) 点绘制一个红色的 50×100 矩形。保存当前状态,将坐标系平移到 (100, 0),绘制一个黄色的矩形。注意,translate()
操作平移的是坐标系,不是矩形。因此,我们还是在 (10, 10) 点绘制一个 50×100 矩形,现在,它跑到了右侧的位置。然后恢复先前状态,也就是把坐标系重新设为默认坐标系(相当于进行translate(-100, 0)
),再进行下面的操作。之后也是类似的。由于我们只是保存了默认坐标系的状态,因此我们之后的translate()
横坐标值必须增加,否则就会覆盖掉前面的图形。所有这些操作都是针对坐标系的,因此在绘制时,我们提供的矩形的坐标参数都是不变的。
运行结果如下:
Qt 的坐标分为逻辑坐标和物理坐标。在我们绘制时,提供给QPainter
的都是逻辑坐标。之前我们看到的坐标变换,也是针对逻辑坐标的。所谓物理坐标,就是绘制底层QPaintDevice
的坐标。单单只有逻辑坐标,我们是不能在设备上进行绘制的。要想在设备上绘制,必须提供设备认识的物理坐标。Qt 使用 viewport-window 机制将我们提供的逻辑坐标转换成绘制设备使用的物理坐标,方法是,在逻辑坐标和物理坐标之间提供一层“窗口”坐标。视口是由任意矩形指定的物理坐标;窗口则是该矩形的逻辑坐标表示。默认情况下,物理坐标和逻辑坐标是一致的,都等于设备矩形。
视口坐标(也就是物理坐标)和窗口坐标是一个简单的线性变换。比如一个 400×400 的窗口,我们添加如下代码:
1
2
3
4
5
6
|
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()
则是将整个坐标系进行了修改。这段代码的运行结果是将整个窗口进行了填充。
试比较下面两行代码的区别(还是 400×400 的窗口):
1
2
|
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)。
下面我们再来理解下视口的含义。还是以一段代码为例:
1
2
3
4
5
6
|
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 转换成物理坐标(也就是设备坐标)。
Qt 学习之路:坐标系统的更多相关文章
- Qt 学习之路 2(28):坐标系统
Qt 学习之路 2(28):坐标系统 豆子 2012年11月25日 Qt 学习之路 2 59条评论 在经历过实际操作,以及前面一节中我们见到的那个translate()函数之后,我们可以详细了解下 Q ...
- Qt 学习之路 2(10):对象模型
Home / Qt 学习之路 2 / Qt 学习之路 2(10):对象模型 Qt 学习之路 2(10):对象模型 豆子 2012年9月2日 Qt 学习之路 2 45条评论 标准 C++ 对象模 ...
- 《Qt 学习之路 2》目录
<Qt 学习之路 2>目录 <Qt 学习之路 2>目录 豆子 2012年8月23日 Qt 学习之路 2 177条评论 <Qt 学习之路 2>目录 序 Qt ...
- QT学习之路--创建一个对话框
Q_OBJECT:这是一个宏,凡是定义信号槽的类都必须声明这个宏. 函数tr()全名是QObject::tr(),被他处理过的字符串可以使用工具提取出来翻译成其他语言,也就是做国际化使用. 对于QT学 ...
- 转载: Qt 学习之路 2归档
Qt 学习之路 2归档 http://www.devbean.net/2012/08/qt-study-road-2-catelog/
- Qt学习之路
Qt学习之路_14(简易音乐播放器) Qt学习之路_13(简易俄罗斯方块) Qt学习之路_12(简易数据管理系统) Qt学习之路_11(简易多文档编辑器) Qt学习之路_10(Qt ...
- Qt 学习之路 2
Qt 学习之路 2 | DevBean Tech World Qt 学习之路 2 Qt 学习之路 2 目录
- Qt 学习之路 2(76):QML 和 QtQuick 2
Home / Qt 学习之路 2 / Qt 学习之路 2(76):QML 和 QtQuick 2 Qt 学习之路 2(76):QML 和 QtQuick 2 豆子 2013年12月18日 Qt ...
- Qt 学习之路 2(74):线程和 QObject
Home / Qt 学习之路 2 / Qt 学习之路 2(74):线程和 QObject Qt 学习之路 2(74):线程和 QObject 豆子 2013年12月3日 Qt 学习之路 2 2 ...
- Qt 学习之路 2(73):Qt 线程相关类
Home / Qt 学习之路 2 / Qt 学习之路 2(73):Qt 线程相关类 Qt 学习之路 2(73):Qt 线程相关类 豆子 2013年11月26日 Qt 学习之路 2 7条评论 希 ...
随机推荐
- 精通 Oracle+Python,第 2 部分:处理时间和日期
从 Python 2.4 版开始,cx_Oracle 自身可以处理 DATE 和 TIMESTAMP 数据类型,将这些列的值映射到 Python 的 datetime 模块的 datetime 对象中 ...
- Python 手册——参数传递以及交互模式
我们先来看参数传递. 调用解释器时,脚本名和附加参数之传入一个名为sys.argv的字符串列表.没有脚本和参数时,它至少也有一个 元素:sys.argv[0]此时为空字符串.脚本名指定为‘ - ’(表 ...
- 初级SQL开发汇总指南
汇总部分内容来自网络(作者 :zhtbs),比较基础的东西,能够了解比较基础的一些东西. Select语句概要 数据库中数据的提取(查询)使用select 语法,主要有以下几点作用 l 提取的数据 ...
- HttpRuntime类的缓存以及redis,memercache
http://www.cnblogs.com/kissdodog/archive/2013/05/07/3065208.html
- BZOJ 3994 约数个数和
Description 设\(d(x)\)为\(x\)的约数个数,给定\(N,M\),求\[\sum_{i=1}^{N}\sum_{j=1}^{M}d(ij)\]. Input 输入文件包含多组测试数 ...
- java中的日期格式
时间日期标识符: yyyy:年 MM:月 dd:日 hh:~12小时制(-) HH:24小时制(-) mm:分 ss:秒 S:毫秒 E:星期几 D:一年中的第几天 F:一月中的第几个星期(会把这个月总 ...
- 修改表增加字段默认值default
对个生产库的表增加1个字段.字段类型是INT型, 表数据有2千万条, alter table table_name add xxoo number(4) default 0 ; 因此 不仅要修改字典 ...
- OpenWrt挂载USB储存设备实现Samba共享
没有USB接口的路由器不是好路由器,有了USB接口OpenWrt才有更多的玩法,比如挂载U盘.移动硬盘等USB储存设备实现Samba共享,打造小型家庭服务器. 1.安装与USB相关的软件包: opkg ...
- bzoj1853 bzoj2393
两题是类似的,这里说一下bzoj1853 首先我们求出所有的幸运号码,注意如果存在x是y的倍数则x不算在内,避免之后重复计算 下面我们就要统计幸运号码的倍数了,这显然是要用到容斥原理的 但是幸运号码很 ...
- 余弦距离、欧氏距离和杰卡德相似性度量的对比分析 by ChaoSimple
1.余弦距离 余弦距离,也称为余弦相似度,是用向量空间中两个向量夹角的余弦值作为衡量两个个体间差异的大小的度量. 向量,是多维空间中有方向的线段,如果两个向量的方向一致,即夹角接近零,那么这两个向 ...