Qt坐标系

这几天学习QT的2D绘图,可因为坐标系统把我拦住了。不但资料少,而且这些资料都是内容雷同。本来这是件非常简单的事情,却有种越描越黑的感觉。经过几天的折腾,总算是理解了这层关系。现在总结一下,不要让大家再走冤枉路。

一、我们为什么要有两种坐标系统?

谈到QT绘图都会跟你说到好几个坐标系,然后就开始被绕晕了。那为什么会这么啰嗦呢?我们换个角度来想一下我们要在屏幕上显示一个图形,程序需要知道哪些东西?

首先你得有一个具体的可描述尺寸的对象,比如现在有一个手机150x50mm,或者一个图片800x600像素,又或者一个房子占地30x6米。这是具体的,与显示设备无关的单位。描述这些内容的坐标系称为逻辑坐标系。因为眼睛看不来那么多东西,所以你还会有一个特别关注的地方,比如说一幅上百米的清明上河图,你要是拉远了就可以看到全部,拉近了就只能看到几个人的画像。描述你需要关注的这个部分矩形就是窗口,决定了你需要显示的内容。

接下来是显示器,这是一个以像素为基本单位进行描述的设备。比如说1024x768分辨率的显示器,或者说一个100x200像素的QWidget。那么描述这种环境的坐标系就是物理坐标系。要正确显示的话,程序需要知道的也是两个方面的东西,你在设备环境的什么地方,以多大的范围给你显示出来。

先看一会图,再来扯具体的概念性的东西~~ 

二、概念理解

1、逻辑坐标与窗口

逻辑坐标:现实工作中作用的坐标系统称为逻辑坐标,使用的单位称为逻辑单位。比如说一个手机尺寸150*50mm。该系统是与显示设备无关的。比如我们常见的Y轴向上的数学坐标系。
窗口:逻辑环境中的一小部分,是一个矩形框,使用逻辑单位。可以理解为需要显示在屏幕上的显示范围。比如说一部手机,我要放大只看手机屏幕那一部分,那么手机屏幕这个部分就是窗口。如果我要缩小看放手机的桌子,那桌子范围就是窗口。可理解为需要展示的内容。

2、设备环境与视口

设备坐标:显示器、打印机,具体的显示控件等等为设备环境。为了便于理解,以下可用QWidget指代设备环境。它的坐标系为设备坐标系(或物理坐标系)。显示器以像素为单位,打印机以点为单位。原点在左上角,正X轴向右,正Y轴向下,固定不变,不可修改!其X、Y的负半轴为虚设,超出设备的部分无法显示或无法打印图形。不管最终的转换结果如何,最后图形还是要转换成屏幕上最终的像素点上。利用QT的方法取得的坐标值一般就是这个。比如QWidget内部的任一点有它位于QWidget左上角的相对坐标位置和位于整个屏幕的绝对坐标位置。利用鼠标的事件event->pos()得到的坐标值是在QWidget内的相对坐标位置,而event->globalPos()的则是位于整个屏幕的绝对位置。

视口:设备环境中的一部分,一个矩形框,使用的单位同设备环境相同。设置视口相当于指定在设备的什么地方,以多大的范围完全显示指定的窗口内容。这就好比电视的画中画功能,需要指定一个区域显示内容。

3、窗口与视口关系

视口是窗口按比例在显示设备(如QWidget)上的投影。窗口解决内容的问题,视口解决显示的问题。二者结合就定义了这两个坐标系的转换关系。注意,它们仅仅是确定转换关系,并非像画中画功能一样,把显示内容限制在视口范围内。只要显示区域足够大,指定的窗口外的其它内容同样也会显示出来。

以上的说法可能并不准确,实现的过程是基于坐标原点的偏移运算,但我觉得这样说会显得更容易理解,可以帮助理清它们之间的关系。

4、默认约定

a)在不作任何设置的默认情况下逻辑坐标和设备坐标系是重合的,它们的初始值都是原点(0,0)在设备环境(如QWidget)的左上角,范围为设备环境的原始大小。也就是说此时的逻辑坐标的位置和范围与设备环境(如QWidget)的初始值是一样的。,并不是我们熟悉的数学坐标。而是与设备坐标一样倒过来的。如果不加以设定,则你看到的绘图的结果就像下面的美女图那样是倒过来的。

b)用painter.drawLine等方法绘图的时候,都是以逻辑坐标为准的。包括设定的笔刷宽度,也是用的逻辑坐标值。需要自己转换成实际的像素宽度,比如你设置画笔宽度,你设置为1就一定会以1像素显示出来吗?不一定噢!如下图所示要强制以1像素显示出来就先计算一下1像素所对应的逻辑单位值mmPerPix吧。 这个mmPerPix后面会讲到。


 
 
  1. //创建画笔
  2. QPen FastPen(Qt::yellow, 1*mmPerPix.x(), Qt::DashLine, Qt::RoundCap, Qt::RoundJoin); //1像素宽度显示
  3. QPen CutPen(Qt::green, 2*mmPerPix.x(),Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin); //2像素宽度显示

5、坐标变换

坐标变换依赖于描述坐标系变换关系的矩阵。也就是常说的齐次坐标矩阵方程。上面提到了三种坐标系统。那么在QPainter类中,也存储了对应的三种坐标转换矩阵。


 
 
  1. painter .transform();
  2. painter .worldTransform();
  3. painter .deviceTransform();

 最后还有一个重要的复合转换矩阵,直接进行逻辑坐标到设备坐标的变换。也就是说直接定义了逻辑坐标点和具体的某个像素对应关系。等一下还会提到。

painter.combinedTransform()      //复合转换矩阵
 
 

 QPainter类中,提供了直接操作变换参数的方法,如下面所示。这些变换方法是直接改变世界坐标系的变换矩阵。当然了,不管改变哪层变换关系,最后的结果也是一样的。就好比1*2和2*1的结果是一样的道理。


 
 
  1. painter.translate(); //平移
  2. painter.rotate(); //旋转
  3. painter.scale(); //比例
  4. painter.shear(); //扭曲

 你要是嫌麻烦,或者想直接调用已有的变换关系,那么可以直接把这关系塞进去:


 
 
  1. painter .setMatrix();
  2. painter .setTransform();
  3. painter .setWorldTransform();

 

三、相关转换代码的实现。

好了,废话是解决不了问题的。概念再清淅最后还不得回归到代码来解决问题么?更多的其它代码就不贴出来了,大家自行网上查找吧,下面只贴出与问题相关的代码。

1、设置窗口和视口


 
 
  1. painter .setWindow(10,12,50, -100);
  2. painter .setViewport(13,15,100,200);

这是基本的设置函数,表示了我在逻辑坐标上的起点(10mm,12mm),宽50mm,高100mm的内容要显示在QWidget起点(13px,15px),宽100px,高200px的位置上,这里有两个特殊情况需要处理。

a) 把显示区域的坐标转成数学坐标(Y轴向上),只要把高的参数改成负数就行了,比如:

painter.setWindow(10,12,50,-100);
 
 

如果这个也很难理解的话可以用另一个方法解决,看代码


 
 
  1. //代替 painter.setWindow 的另一方法
  2. WinSize.setCoords(MinX,MaxY,MaxX,MinY); //直接输入逻辑坐标系的左上角坐标和右下角坐标

 b)最终的图形显示受窗口和视口变换矩阵同时影响,如果设置不正确则可能出现图形变形的情况。解决方法就是保证窗口的高宽比和视口的高宽比相同。比如上面的50/100与100/200的结果值是相同的,这样则不会变形。最简单的就是设置窗口与视口都为正方形。下面是一段根据窗口比例计算视口高宽的例程。


 
 
  1. //按比例显示全部图形
  2. QRect WinSize; //窗口范围
  3. QRect ViewSize; //视口范围
  4. WinSize.setLeft(MinX); //设置窗口范围
  5. WinSize.setTop(MaxY);
  6. WinSize.setWidth(Width);
  7. WinSize.setHeight(-Height);
  8. int AxisOffset= 20; //视口偏移
  9. if (Width>=Height) //图形横放
  10. {
  11. ViewSize.setWidth(rect().width()-AxisOffset* 2);
  12. ViewSize.setHeight(ViewSize.width()*Height/Width);
  13. }
  14. else
  15. {
  16. ViewSize.setHeight(rect().height()-AxisOffset* 2);
  17. ViewSize.setWidth(ViewSize.height()*Width/Height);
  18. }
  19. //居中显示
  20. ViewSize.moveLeft((rect().width()-ViewSize.width())/ 2);
  21. ViewSize.moveTop((rect().height()-ViewSize.height())/ 2);
  22. painter.setWindow(WinSize);
  23. painter.setViewport(ViewSize);

 2、坐标值互换

上面提到过,用鼠标的QMouseEvent得到的是设备的底层物理坐标。这个事件中可以得到两种值分别是:


 
 
  1. event->pos();  //QWidget上的绝对坐标  
  2. event->globalPos(); //屏幕上的绝对坐标

 还可以通过QWidget上的函数相互转换:


 
 
  1. mapTo(); //转换成某个QWidget内的坐标
  2. mapToParent(); //转换成在父对象上的坐标
  3. mapToGlobal(); //转换成屏幕绝对坐标

 mapForm的是反向转换,不多说了,自己看图。

3、转换成逻辑坐标

然而对于专门画图的我来说,上面几个坐标值对我没什么用。比如用CAD画一个图,我需要知道的是mm这些逻辑坐标而已。那怎么把鼠标当前的位置转换成逻辑坐标呢?用矩阵!刚才提到的painter.combinedTransform()这时就派上用场了。在QTransform类中提供了一个map方法,可以把输入点转换成最后的坐标点。

QPoint NowDragPos = painter.combinedTransform().map(event->pos());
 
 

看到这里,你以为就完了吗?No……No……No…… 忘了告诉你,这个转换都是从逻辑坐标向下转成物理坐标的。所以你把event事件中取得的pos()坐标输进去后会得到一堆不知什么鬼的东西。我明明是要从物理坐标转成逻辑坐标呢,也就是说我要反向转换。好吧,QTransform还提供了一种方法inverted()可以返回该变换的逆矩阵。正确的做法是 :

QPoint NowDragPos = painter.combinedTransform().inverted().map(event->pos());
 
 

 这个时候就从像素点返回了逻辑坐标点了。还有一件事需要提醒下,在每次的paintEvent事件中,这些矩阵都是恢复为默认值的,所以你需要在类中定义一个QTransform对象来保存这种变换关系以便随时调用。

ViewToWinTrans=painter.combinedTransform().inverted()();   //保存当前变换的逆矩阵
 
 

 4、精度控制

 刚才只是用到了QTransform类中map方法的一个重载,用于QPoint的转换。在QTransform中还提供了多种类型的变换,比如QRect。对于QPoint你懂的,xy坐标是int值。对于我这要求严格的来说远远不够用。那就可以用下面这两个重载。


 
 
  1. void QTransform:: map(qreal x, qreal y, qreal *tx, qreal *ty) const
  2. QPointF QTransform:: map( const QPointF &p) const

5、最后上个效果图吧,在鼠标移动过程中返回该鼠标在逻辑坐标系上的位置,不要在意误差哈。

6、准确返回每像素对应的逻辑坐标值

上面说到画笔时提到了mmPerPix这个变量,QT没有直接提供这个方法返回。好吧,自己动手。原理也很简单,直接返回一条1像素的直线长度不就可以了吗?看下面的代码:


 
 
  1. ViewToWinTrans=painter.combinedTransform().inverted(); //保存当前变换的逆矩阵,即每像素的逻辑尺寸
  2. QLineF LineXY(0,0,1,1);
  3. LineXY=ViewToWinTrans. map(LineXY);
  4. mmPerPix.setX(LineXY.dx());
  5. mmPerPix.setY(LineXY.dy());

7、关于绘图速度

如果你需要绘制的图形线段非常多,那可能会造成一点卡滞。因为经测试后发现,设置画笔宽度后会大大增加绘图时间。如果把画笔宽度设为0,则默认以1像素去绘图,此时画图速度飞快。原因我也不情楚,希望有知道的大神提点一下原因和解决方法。当然也不是没有办法。给个效果图,速度还是可以的。当然了,这个跟电脑配置也有关系,下面也把我的配置贴出来给大家参考一下。至少对于我来说,这个效率不会比所谓的OpenGL差了。所以说如果觉得绘图速度慢的要好好优化一下程序了。

四、最后的话

为了这个坐标转换,花了我不少时间,本来是挺简单的一个事情,却搞得好像很复杂一样。技术就是一层纸,捅破了也就没什么了。刚学QT不久,也是第一次发贴,写得不好的地方希望大家多多包含。有错误的地方还请指出,谢谢!

修改于:2018-11-24

  • 3
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值