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

在经历过实际操作,以及前面一节中我们见到的那个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 的坐标系统中,每个像素占据 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,上图的绿色点指出了这两个函数的返回点的坐标。

为避免这个问题,我们建议是使用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();
}
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 的窗口,我们添加如下代码:

 
 
void PaintDemo::paintEvent(QPaintEvent *)
{
QPainter painter(this);
painter.setWindow(0, 0, 200, 200);
painter.fillRect(0, 0, 200, 200, Qt::red);
}
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 的窗口):

 
 
painter.translate(200, 200);
painter.setWindow(-160, -320, 320, 640);
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)。

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

 
 
void PaintDemo::paintEvent(QPaintEvent *)
{
QPainter painter(this);
painter.setViewport(0, 0, 200, 200);
painter.fillRect(0, 0, 200, 200, Qt::red);
}
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 转换成物理坐标(也就是设备坐标)。

总觉得这里理解得不够透彻

    1. Zaccurli 2016年1月27日

      一个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日

        是的,基本是这么理解的

        • songcymai 2016年3月7日

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

Qt 学习之路 2(28):坐标系统的更多相关文章

  1. Qt 学习之路 2(29):绘制设备

    Qt 学习之路 2(29):绘制设备 豆子 2012年12月3日 Qt 学习之路 2 28条评论 绘图设备是继承QPainterDevice的类.QPaintDevice就是能够进行绘制的类,也就是说 ...

  2. Qt 学习之路 2(71):线程简介

    Qt 学习之路 2(71):线程简介 豆子 2013年11月18日 Qt 学习之路 2 30条评论 前面我们讨论了有关进程以及进程间通讯的相关问题,现在我们开始讨论线程.事实上,现代的程序中,使用线程 ...

  3. Qt 学习之路 2(67):访问网络(3)

    Qt 学习之路 2(67):访问网络(3) 豆子 2013年11月5日 Qt 学习之路 2 16条评论 上一章我们了解了如何使用我们设计的NetWorker类实现我们所需要的网络操作.本章我们将继续完 ...

  4. Qt 学习之路 2(66):访问网络(2)

    Home / Qt 学习之路 2 / Qt 学习之路 2(66):访问网络(2) Qt 学习之路 2(66):访问网络(2)  豆子  2013年10月31日  Qt 学习之路 2  27条评论 上一 ...

  5. Qt 学习之路 2(65):访问网络(1)

    Home / Qt 学习之路 2 / Qt 学习之路 2(65):访问网络(1) Qt 学习之路 2(65):访问网络(1)  豆子  2013年10月11日  Qt 学习之路 2  18条评论 现在 ...

  6. Qt 学习之路 2(64):使用 QJsonDocument 处理 JSON

    Home / Qt 学习之路 2 / Qt 学习之路 2(64):使用 QJsonDocument 处理 JSON Qt 学习之路 2(64):使用 QJsonDocument 处理 JSON  豆子 ...

  7. Qt 学习之路 2(63):使用 QJson 处理 JSON

    Home / Qt 学习之路 2 / Qt 学习之路 2(63):使用 QJson 处理 JSON Qt 学习之路 2(63):使用 QJson 处理 JSON  豆子  2013年9月9日  Qt ...

  8. Qt 学习之路 2(62):保存 XML

    Home / Qt 学习之路 2 / Qt 学习之路 2(62):保存 XML Qt 学习之路 2(62):保存 XML  豆子  2013年8月26日  Qt 学习之路 2  9条评论 前面几章我们 ...

  9. Qt 学习之路 2(60):使用 DOM 处理 XML

    Qt 学习之路 2(60):使用 DOM 处理 XML  豆子  2013年8月3日  Qt 学习之路 2  9条评论 DOM 是由 W3C 提出的一种处理 XML 文档的标准接口.Qt 实现了 DO ...

随机推荐

  1. 3D Text & GUI Text & GUI Texture

    [Unity3D Text] 3D Text通过"Text Mesh"组件."MeshRender共同完成",通过如下方法要以创建一个3D Text. 3D T ...

  2. 581. Shortest Unsorted Continuous Subarray连续数组中的递增异常情况

    [抄题]: Given an integer array, you need to find one continuous subarray that if you only sort this su ...

  3. 基于Nginx实现集群原理

    1)安装Nginx 2)配置多个Tomcat,并修改端口号(两个端口号不一样即可) 3)在Nginx的Nginx.conf添加如下配置:

  4. Apache apachectl命令

    一.简介 apachectl命令是Apache的Web服务器前端控制工具,用以启动.关闭和重新启动Web服务器进程. 二.语法 http://www.jinbuguo.com/apache/menu2 ...

  5. xgboost dmatrix中的 weight的重要性

    https://stackoverflow.com/questions/35983565/how-is-the-parameter-weight-dmatrix-used-in-the-gradien ...

  6. javascript总结7:算术运算符

    1  运算符: 加号+ 如果是数字类型的变量相加,那么结果为数字类型; 如果是非数字类型的变量相加,结果为字符串类型 2  减号- 如果是非数字类型的变量相减结果为  NaN 3 乘号 * 如果是非数 ...

  7. LIRE教程之源码分析 | LIRE Tutorial of Analysis of the Source Code

    LIRE教程之源码分析 |LIRE Tutorial of Analysis of the Source Code 最近在做地理图像识别和检索的研究,发现了一个很好用的框架LIRE,遂研究了一通.网上 ...

  8. 快速入手Web幻灯片制作

    在线幻灯片 使用markdown可以快速的写出优美的文档,接下来我介绍一些简单的语法,快速的用浏览器制作幻灯片. 最基本使用格式 <!DOCTYPE html> <html> ...

  9. c++基础之向量Vector

    首先和string一样要在开头 #include <vector> #include <string> 和string一样,也算是一种容器,而且同属于STL(standard ...

  10. 建造者(Builder)模式 *

    一. 建造者(Builder)模式 建造者模式可以将一个产品的内部表象与产品的生成过程分割开来,从而可以使一个建造过程生成具有不同的内部表象的产品对象. 二. Builder模式的结构: 建造者(Bu ...