Qt学习之路2(28)坐标系统在经历过实际操作,以及前面一节中我们见到的那个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的变换矩阵(transformationmatrix)、视口(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同样提供了坐标变换。前面说,图形学大部分算法依赖于矩阵计算,坐标变换便是其中的代表:每一种变换都对应着一个矩阵乘法。我们会以一个实际的例子来了解坐标变换。在此之前,我们需要了解两个函数:QPainter::save()和QPainter::restore()。前面说过,QPainter是一个状态机。那么,有时我想保存下当前的状态:当我临时绘制某些图像时,就可能想这么做。当然,我们有最原始的办法:将可能改变的状态,比如画笔颜色、粗细等,在临时绘制结束之后再全部恢复。对此,QPainter提供了内置的函数:save()和restore()。save()就是保存下当前状态;restore()则恢复上一次保存的结果。这两个函数必须成对出现:QPainter使用栈来保存数据,每一次save(),将当前状态压入栈顶,restore()则弹出栈顶进行恢复。在了解了这两个函数之后,我们就可以进行示例代码了:voidPaintDemo::paintEvent(QPaintEvent*){QPainterpainter(this);painter.fillRect(10,10,50,100,Qt::red);painter.save();painter.translate(100,0);//向右平移100pxpainter.fillRect(10,10,50,100,Qt::yellow);painter.restore();painter.save();painter.translate(300,0);//向右平移300pxpainter.rotate(30);//顺时针旋转30度painter.fillRect(10,10,50,100,Qt::green);painter.restore();painter.save();painter.translate(400,0);//向右平移400pxpainter.scale(2,3);//横坐标单位放大2倍,纵坐标放大3倍painter.fillRect(10,10,50,100,Qt::blue);painter.restore();painter.save();painter.translate(600,0);//向右平移600pxpainter.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机制将我们提供的逻辑坐标转换成绘制设备使用的物理坐标,方法是,在逻辑坐标和物理坐标之间提供一层“窗口”坐标。视口是由任意矩形指定的物理坐标;窗口则是该矩形的逻辑坐标表示,绘制图形时写的坐标都是逻辑坐标如painter.drawLine(QPoint(0,0),QPoint(100,100));。默认情况下,物理坐标和逻辑坐标是一致的,都等于设备矩形。视口坐标(也就是物理坐标)和窗口坐标(逻辑坐标)是一个简单的线性变换,两个坐标默认是一致的,是重合的(1:1,即是相同的)。比如一个QPaintDevice原来为400×400(物理坐标)的窗口,其默认视口坐标与窗口坐标均为(0,0,400,400)。当设置窗口setWindow或者视口矩形setViewport时,实际上执行了坐标的一个线性变换,窗口的4个角会映射到视口对应的4个角,反之宜然。因此视口和窗口应该维持相同的宽高比来防止变形,如果setWindow为正方形,则setViewport也为正方形。我们添加如下代码:voidPaintDemo::paintEvent(QPaintEvent*){QPainterpainter(this);painter.setWindow(0,0,200,200);painter.fillRect(0,0,200,200,Qt::red);}我们将窗口矩形设置为左上角坐标为(0,0),长和宽都是200px。此时,坐标原点不变,还是左上角,但是,对于原来的(400,400)点,新的窗口坐标是(200,200)。我们可以理解成,逻辑坐标被“重新分配”即现在坐标系的1个单位等于原来的2个单位。这有点类似于translate(),但是,translate()函数只是简单地将坐标原点重新设置,而setWindow()则是将整个坐标系进行了修改。这段代码的运行结果是将整个窗口进行了填充。试比较下面两行代码的区别(还是400×400的窗口):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)。计算公式为:窗口下的起始值(-160)+视口下的坐标值(64)*(窗口宽度320/视口宽度400)下面我们再来理解下视口的含义。还是以一段代码为例:voidPaintDemo::paintEvent(QPaintEvent*){QPainterpainter(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操作的坐标系是逻辑坐标系(窗口坐标,windowport),其各种绘制函数