坐标转换

来源:互联网 发布:淘宝网夏天零食店 编辑:程序博客网 时间:2024/06/10 06:28

 如果你用GDI输出过文本、位图、或者绘制过如直线、曲线的图形,那么你必然使用过坐标系。屏幕分辨率为96dpi(dot per inch),打印机的分辨率通常为600dpi,而你使用如

    MoveTo(hDC,100,100);

    LineTo(hDC,1300,1300);

在两者之上却可能生成完全相同的两条线(此即所见即所得WYSIWYG)。就实现原理而言,坐标系在其中扮演着决定性的角色。

 

一、什么是GDI坐标系?

 

首先从数学的角度看,GDI坐标系是二维笛卡尔坐标系,通过两条轴和原点就可以确定平面上任何一点的位置。

从使用的角度看,GDI坐标系是一种转换规则,把你所制定的逻辑数据转换成最终设备驱动所能使用的数据。比如(100,100)这一点,经过实际的变换,在96dpi的屏幕上就可能是(9.6,9.6),在600dpi的打印机上则可能是(60,60)。(注一)

GDI坐标系由四层坐标空间组成(注二),按层次的高低分别为:

 

世界坐标空间(World-Space):支持affine变换,应用于下面所说的页面坐标空间之上,只在NT类操作系统中有支持。

 

页面坐标空间(Page-Space):支持大量预定义的映射模式,是必然会被使用的坐标空间。原点和相应的缩放比率的设置适合在页面坐标空间中进行设定。

 

世界坐标空间和页面坐标空间统称为逻辑坐标空间,是GDI用户所能够直接使用的坐标空间。也就是说进行GDI输出时,你所指定的位置、大小等信息只能是相对于逻辑坐标空间的数据。

 

设备坐标空间(Device-Space):同设备上下文相关联的设备空间。可以表示物理设备的一小块或者整个物理设备。由于各种GDI输出是面向设备上下文的,逻辑坐标空间中的相关数据自然也就必须要转化为设备坐标空间中的数据。

 

物理设备坐标空间(Physical-Device Space):图形设备的物理表面的部分或全部。也就是图形驱动程序所使用的坐标空间。任何GDI输出最终想在显示器或打印机上成形都要经过相关的驱动程序,进行设备坐标空间向物理设备坐标空间的转换就成为一种必然。这个过程完全由系统完成。所以DC的原点和大小信息是只读的。(为表述方便以下将用DC表示设备上下文)

 

二、输出位置到底在那儿?

 

       由上述说明可见最终输出位置的确定至少要经2次变换(如果你启用了世界坐标空间那是3次),有没有什么办法能够直接确定任意一个逻辑点最终会对应到那个物理位置呢?现成的没有,我们来自己实现一个,其实也并不复杂。首先要把逻辑坐标转化为设备坐标,这个过程要根据affine矩阵和当前映射模式做很多运算,但我们现在先不自己进行这种运算,而是使用

BOOL LPtoDP(  HDC hdc,           // handle to device context   LPPOINT lpPoints,  // array of points   int nCount         // count of points in array);

这个函数负责把lpPoints中的逻辑坐标转换为同hdc相关联的设备坐标。(稍后我们来自己完成这个函数)。

因为设备坐标和物理设备坐标的单位是一致的,都是物理设备点。所以从设备坐标到物理设备坐标的转换没那么麻烦,只要知道了DC的原点就可以了(注意这里的原点是指设备坐标空间的原点在物理设备坐标空间中的位置)。这个任务的本质是去读取OS的DC结构,我们使用

BOOL GetDCOrgEx(  HDC hdc,          // handle to a DC  LPPOINT lpPoint   // translation origin);来完成这一任务。

这样的话,对于任意逻辑点logicalPoint,其DC原点为dcorgPoint有:

 

physicaldevicePoint就是logicalPoint理论上在物理设备坐标空间中的位置。此处需要说明的是由于物理设备坐标空间并不一定涵盖图形设备的整个物理表面,所以从physicaldevicePoint计算出的位置并不一定就是物理表面上的位置。比如说打印的时候就还要加上四周的不可打印区域才是其实际位置。同时显示器一类的设备还要考虑物理设备本身有没有进行缩放,如果没有(也就是说没用显示器下边的那些钮)那么你就会发现我们计算出的尺寸同相应点在屏幕上的位置符合的挺好。

 

void GetPhysicalPosition(HDC hDC,LPPOINT lpPoint ,int nCount)用于完成上述功能。具体实现见源码1。
源码1
一、逻辑空间的坐标是如何转化为设备空间的坐标的?

 

让我们先来澄清逻辑坐标空间内部是如何转化的。

 

世界坐标空间到页面坐标空间的变换(二维affine(注三)变换):

    此过程中涉及到的各种变换,比如:相等、平移、缩放、映像、旋转、剪切、合并等都是通过为affine矩阵的各个成员指定适当的值来实现的。

这个矩阵所对应的结构如下:

         typedef struct  _XFORM {                  FLOAT eM11;                  FLOAT eM12;                  FLOAT eM21;                  FLOAT eM22;                  FLOAT eDx;                  FLOAT eDy;          } XFORM, *PXFORM; 一个世界坐标空间的点转换为页面坐标空间的点的公式为:

xpage=xworld*eM11+yworld*eM21+eDx;

ypage=xworld*eM12+yworld*eM22+eDy;(公式一)

其中xworld、yworld为世界坐标空间的点。xpage、ypage为上述点在页面坐标空间中对应的位置。至于与affine变换的数学属性及如何才能实现页面的相等、平移、缩放、映像、旋转、剪切、合并此处不进行详细说明,因为那样将使这篇文章的规模膨胀许多(注四)。

 

页面坐标空间到设备坐标空间的转换:

这个过程涉及到几个概念,他们分别是:

 

视口原点:当前页面坐标空间所认为的设备坐标空间的原点位置。用SetViewportOrgEx、GetViewportOrgEx分别进行设置和读取。用这两个函数进行操作时,所涉及的坐标为设备空间的坐标。

视口范围:  视口范围并不是一个绝对的用于表示设备坐标空间大小的值。而是一个相对值,它同窗口范围的比例最终决定页面坐标空间到设备坐标空间是一种缩小还是放大的转换。用SetViewportExtEx、GetViewportExtEx对视口范围进行存取。

 

窗口原点:页面坐标空间的原点。用SetWindowOrgEx、GetWindowOrgEx对窗口原点进行存取,所涉及的坐标为逻辑坐标。

窗口范围:见视口范围的说明。用SetWindowExtEx、GetWindowExtEx进行存取。

 

由了两个坐标空间的原点值和范围的比例值,在这两个坐标空间间进行坐标转换也就不是什么太难的事了。比较容易的可以得出下面的公式:

 

页面坐标空间到设备坐标空间:

xdevice=(xpage-WOrgx)*VExtx /WExtx+VOrgx;

ydevice=(ypage-WOrgy)*VExty /WExty+VOrgy;(公式二)

其中(WOrgx,WOrgy)为窗口原点。(VOrgx VOrgy,)为视口原点。(WExtx, WExty)为窗口范围。(VExtx,VExty)为视口范围。

 

设备坐标空间到页面坐标空间的转换大家可以自己推导。

 

为了更好的理解坐标空间的转换,我们将利用上述两组公式,动手来实现自己的LPtoDP。我们的这个函数将只适合nt类的平台。(9x没有世界坐标空间,会更简单)具体实现见源码2。实现MyLPtoDP的过程比较简单,此处仅对要用到的几个主要函数做些说明。

 

 

int GetGraphicsMode(  HDC hdc   // handle to device context);这个函数用来得到指定DC的图形模式。图形模式有两种GM_COMPATIBLE和GM_ADVANCED只有在GM_ADVANCED才可能使用世界坐标空间。可以用SetGraphicsMode在两者间切换。

 

BOOL GetWorldTransform(  HDC hdc,         // handle to device context  LPXFORM lpXform  // transformation);此函数用来得到与当前DC相关联的affine矩阵。通过公式一,应该可以知道缺省的affine矩阵具有{1.0,0,0,1.0,0,0}的形式。 MyLPtoDP虽然有返回值但此值无意义,并且实现过程中也并没有进行任何出错处理。见源码2。 二、关于GDI+的补充说明 就各种坐标空间而言GDI+的更新更多的体现在操作方式上而非在本质上。理解了上述概念再看GDI+的坐标空间,会有一种一目了然感觉。而本文更侧重于概念的树立,因此就不单独再对GDI+进行特别说明了。

 

注一:这里仅是一种可能的情形,具体的转换后数值要由当前的坐标空间来具体确定。

注二:我个人认为GDI的坐标空间其实就是坐标系,但由于对应英文术语为Coordinate Space,并且大多书籍译为坐标空间,所以此处亦如是。

注三:大家都知道通过乘上一个2x2的矩阵可以完成诸如缩放、旋转、镜像等操作。

      如下图:
如上这几种变换被称作linear transformations。但通过乘上2x2的矩阵你无法完成平移一类的操作。为达到平移的目的就还需要加上一组偏移量(分别对应于x轴和y轴)。一个2x2矩阵和一组偏移量就构成了Affine矩阵。

注四:请参考Feng Yuan的《windows 图形编程》

 

/*
 Function:

 把逻辑位置转换为最终的物理坐标空间中的位置

 Parameter:

 hDC---待转换逻辑坐标所处的空间

 lpPoint---待转换的逻辑点转换前为逻辑点,转换后为取整后的毫米

 nCount----待转换点的个数

 RetValue:

 TRUE or FALSE

 History:

 2003-10-25 11:13
*/
void GetPhysicalPosition(HDC hDC,LPPOINT lpPoint ,int nCount)
{
 POINT originPoint;

 int widthmm=GetDeviceCaps(hDC,HORZSIZE);
 int heightmm=GetDeviceCaps(hDC,VERTSIZE);

 int widthres=GetDeviceCaps(hDC,HORZRES);
 int heightres=GetDeviceCaps(hDC,VERTRES);

 LPtoDP(hDC,lpPoint,nCount);

 GetDCOrgEx(hDC,&originPoint);

 for(int i=0; i<nCount; ++i)
 {
  lpPoint[i].x +=originPoint.x;
  lpPoint[i].y +=originPoint.y;

  lpPoint[i].x=lpPoint[i].x*widthmm/widthres;
  lpPoint[i].y=lpPoint[i].y*heightmm/heightres;
 }

}

源码2
/*
 Function:

  我们自己的把逻辑坐标转换为设备坐标的函数
*/
BOOL MyLPtoDP(
  HDC hdc,           // handle to device context
  LPPOINT lpPoints,  // array of points
  int nCount         // count of points in array
)
{
 int graphicsMode=GetGraphicsMode(hdc);

 if(graphicsMode ==GM_ADVANCED) //处理启用了世界坐标系的情况
 {
  XFORM curForm;

  GetWorldTransform(hdc,&curForm);

  for(int i=0; i<nCount; ++i)//应用公式一完成世界坐标空间向页面坐标空间的转换
  {
   float xpage=lpPoints[i].x*curForm.eM11+lpPoints[i].y*curForm.eM21+curForm.eDx;
   float ypage=lpPoints[i].x*curForm.eM12+lpPoints[i].y*curForm.eM22+curForm.eDy;

   lpPoints[i].x=(int)xpage;
   lpPoints[i].y=(int)ypage;
  }
 }

 POINT pointOrgView,pointOrgWin;
 SIZE winSize,viewSize;

 //得到窗口、视口的原点和范围
 GetViewportOrgEx(hdc,&pointOrgView);
 GetViewportExtEx(hdc,&viewSize);
 GetWindowOrgEx(hdc,&pointOrgWin);
 GetWindowExtEx(hdc,&winSize);

 //根据公式二进行页面坐标空间到设备坐标空间的转换
 for(int i=0; i<nCount; ++i)
 {
  float xdevice=(lpPoints[i].x-pointOrgWin.x)*viewSize.cx/(float)winSize.cx+pointOrgView.x;
  float ydevice=(lpPoints[i].y-pointOrgWin.y)*viewSize.cy/(float)winSize.cy+pointOrgView.y;

  lpPoints[i].x=(int)xdevice;
  lpPoints[i].y=(int)ydevice;
 
 }

 return TRUE;
}

 

原创粉丝点击