GDI中的坐标映射问题
阿里
在我们进行绘图程序的开发时,不可避免地会遇到坐标映射的问题,而这恰恰是一个很伤脑筋、绕也绕不明白的问题。我就经常为此而一卡就是几个小时,恨得要命,终于有一天心一横,豁出一个周末的晚上,啃了所有找得到的资料,特别是那蝌蚪一般的MSDN,发现了相关问题的冰山之一角,不过就这就已经有一种豁然开朗的感觉了,把它写出来还希望能够对受到同样问题困扰的各位看官有一点点帮助,同时也希望编程大侠们不要因为对这样一个简单的问题不屑一顾而见笑。
首先要明确的一点是,绘图语句中使用的坐标始终是逻辑空间的坐标值,而我们最终要绘制的目的地则是物理设备空间(physical device space)。
1.预备知识:GDI中所规定的四种坐标空间(或者叫坐标系)。
1.1 world坐标空间:引入world空间的目的是对图像进行平移、缩放、剪切等操作,其最大坐标范围为2^32个单位高,2^32个单位宽,初始状态时x轴正向向右,y轴正向向上。World坐标空间可以成为逻辑空间。
1.2 page坐标空间:当没有world空间时,它就是逻辑空间,而且这种情况是最普遍的。最大坐标范围为2^32个单位高,2^32个单位宽,初始状态时x轴正向向右,y轴正向向上。
1.3 device空间:设备空间,是坐标变换的常规目的地。最大坐标范围为2^27个像素高,2^27个像素宽。其特点是x轴正向向右,y轴正向向下,原点在物理设备左上角,而且这些规则我们不能改变。
1.4 physical Device空间:这一空间代表着具体的物理设备,是我们实际能看到的坐标空间,也是图形绘制的最终目的地,我们绘制的一个大尺寸图形到底能让我们看到多少,完全取决于它的大小。它可以是Windows窗口的客户区,或者是整个桌面,或者是打印机的一页纸,或者是绘图仪的一页纸。
图1 坐标映射流程
2.从Windows系统的角度来看GDI坐标映射。
首先我们从Windows系统的角度来看坐标映射是如何进行的,或者说来看看,Windows是如何将我们在程序中使用的逻辑空间坐标值转换成为物理设备空间坐标值的。它通常分成以下3个步骤。
第1步,world空间 → page空间。如果程序员使用SetWorldTransform函数明确定义了world空间向page空间映射的公式,那么windows将进行这种映射,具体规则由SetWorldTransform函数定义,此时的逻辑空间是world空间,。
如果没有出现SetWorldTransform函数,Windows将不进行world空间到page空间的映射,而直接进行page空间到device空间的映射,此时的逻辑空间是page空间。
事实上world空间是Windows98以后才引入的,我们一般情况下是用不到它的。但是如果我们要将逻辑空间以一种“扭曲”的方式在物理设备上表现出来,world坐标空间是一个非常好的工具。
第2步,page空间 → device空间。这是我们程序员最关心的一个映射步骤,映射规则是:
其中,Di表示x或者y方向的设备空间坐标,单位是像素(pixel);
Li表示x或者y方向的page空间坐标,单位是逻辑单位(即自己定义);
L0表示window的原点在page空间中的坐标值,单位是逻辑单位;
WE表示window的宽(高)度,由SetWindowExtEx(W, H)函数确定,单位是逻辑单位。
VE表示viewport的宽(高)度,由SetViewportExtEx(W, H)函数确定,单位是像素。
D0表示viewport的原点在device空间中的坐标值,单位是像素。
看不太明白不要紧,因为我们并不需要操心这个公式,让Windows去头疼好了,不过基本的原理我们还是要了解的,这样才能对坐标映射有更深的了解,这也是我将公式写出来的原因。
第3步,device空间 → physical device空间。这一映射遵从一对一原则,即device空间的一个像素就是physical device空间的一个像素,并且它们的坐标原点在物理设备的左上角,坐标方向是x轴正向向右,y轴正向向下,记住是向下!。这个映射的规则我们程序员是不能改变的,这也就是所谓的设备无关性。比如说,我们要在一个客户区窗口(physical device)进行绘图,我们根本不要管这个客户区具体在哪里,又是如何显示的,我们只需把它对应的device空间作为“画布”,在这个画布上进行输出就行了,其它工作完全由Windows自动完成。
3.从程序员的角度来看坐标映射
坐标映射在程序员的眼中就是要根据自己实际问题的要求,构造出一个满足要求的逻辑空间。所谓的满足要求就是指每一个我们在程序中使用的点,都能出现在physical device上我们预期的相应位置。由于device空间到physical device空间是一对一的映射,因此,我们完全可以将绘图目的地看成device空间,所构造出的逻辑空间也只需正确映射到device空间就可以了。
3.1 page空间 → device空间
如果我们不使用world空间,此时的逻辑空间就是page空间。下面来看如何确定它的三个要素:单位刻度值、方向、原点。
首先要使用SetMapMode(int)函数选择映射模式。这其中有6种事先已经定义好了的模式,可以直接拿来就用,比如MM_HIMETRIC模式表示page空间的单位刻度是0.01毫米,x轴正向向右,y轴正向向上,原点与device空间的原点重合。如果此时程序中有一条值为10的线段,那么在程序员的眼中,这就是一条10×0.01=0.1毫米的线段,不管使用多大分辨率的显示器它都是这么长,我们甚至可以用尺子在屏幕上量量试试。如果选择预定义的映射模式,相当于微软已经为我们构造好了page空间,下面的事我们就都不用做了。
但是很多时候,微软的东西不一定适合我们,此时就要将映射模式设定为MM_ISOTROPIC或者MM_ANISOTROPIC,使用下面的四个函数定义我们自己的坐标系:
SetWindowExt(int Lwidth, int Lheight) //参数的单位为逻辑单位(Logical),如果参数为负值表示window相应的坐标轴与page空间相反。
SetViewportExt(int Pwidth, int Pheight) //参数的单位为像素(Pixel),如果参数为负值表示viewport相应的坐标轴与device空间相反。
SetWindowOrg(int Lx, int Ly)。
SetViewportOrg(int Px, int Py)。
这四个函数提出了两个新的概念:window和viewport,它们分别与page空间和device空间对应,但请记住并不是对等。引入它们的目的仅仅是为了确定page空间的单位刻度、方向、原点。
1.x轴的单位刻度=| Pwidth | / | Lwidth |。
这表示x轴上一个逻辑单位等于多少个像素。下面举例加以解释。
比如我们先通过GetDeviceCap(LOGPIXELSX)获得在我们的显示器上每英寸等于多少个像素,设为p,然后我们将它赋给Pwidth,将Lwidth赋成2,即Pwidth / Lwidth=p / 2。那么,此时page空间x轴上的单位刻度就是p / 2个像素;又由于p个像素是代表一个英寸的,所以此时的page空间x轴上的单位刻度同时也是半个英寸。
请注意这个例子中,虽说viewport的x方向“范围”是p个像素,但是device空间x轴的“范围”决不仅仅是p个像素,而是2^27个像素,至于可视的范围到底是多少,则取决于物理设备空间。
2.x轴的方向:这个好确定,Lwidth与Pwidth同号,则page空间的x轴方向与device空间x轴方向相同,否则相反。
3.原点。这个就有一点麻烦了,我们需将window与viewport进行重叠,包括原点和坐标轴方向,然后才可以确定page空间的原点。下面通过一个例子来加以说明。
例:假设我们通过下面的语句构造了一个page空间:
SetMapeMode(MM_ANISOTROPIC);
SetWindowExt(10, 100);
SetWindowOrg(0, -100);
SetViewportExt(20, 200);
SetViewPortOrg(0,-200);
图2 page空间映射到device空间的例子
(由于100个逻辑单位相当于200个像素,因此我将它们的示意长度画成一样。)
从这些语句中我们可以很快确定出page空间的单位刻度(比如y轴上每逻辑单位200 / 100=2个像素),以及y轴的方向与device空间相同(100与200同号),但是page空间的原点在哪里呢?请看:
首先我们分别在page空间中画出window坐标系、在device空间中画出viewport坐标系(如图2的左边部分)。然后由于例子中的window坐标方向与viewport相反,还需将page空间翻转(见图2中间部分)。最后将window与viewport重叠(见图2右边部分),使它们的原点和坐标方向都一致。此时我们可以清楚地看到,page空间的原点就对应于device空间的原点,而且方向也和它相同。
通过以上的1、2、3点我们就可以完全确定一个适合我们自己要求的page空间,当我们不要world空间时,它就是逻辑空间。
另外还有一个问题就是要注意MM_ANISOTROPIC与MM_ISOTROPIC的区别。对于前者来说,x方向的单位刻度与y方向的单位刻度可以不同(当然也可以相同),但是后者x方向的单位刻度与y方向的单位刻度一定是相同的,如果通过计算window与viewport范围的比值得到两个方向的单位刻度值不同,那么将会以较小的那个为准。
3.2 world空间 → page空间
有时候我们需要从一个倾斜的角度显示一个圆或者其它什么图形,但是我们在使用绘图语句时,心目中仍然要当这个圆正对着我们来考虑问题,因为只有这样,我们在构造图形时的思维才不至于混乱,怎么实现呢?就可以通过加上world空间达到这个目的。由于一般很少使用这种映射,我在这里只以一个例子简单加以说明。
void CSampleView::DrawShearCircle()
{
CClientDC dc(this);
dc.SetMapMode(MM_ANISOTROPIC); //映射模式设定为各向异性。
//以下语句将page空间最小刻度值设为1mm,原点位于客户区矩形中心,x正向向右,y正向向上。
dc.SetWindowExt(1, -1);
int PperMMX = dc.GetDeviceCaps(HORZRES) / dc.GetDeviceCaps(HORZSIZE);
int PperMMY = dc.GetDeviceCaps(VERTRES) / dc.GetDeviceCaps(VERTSIZE);
dc.SetViewportExt(PperMMX, PperMMY);
CRect cr;
GetClientRect(&cr);
dc.SetViewportOrg(cr.right/2, cr.bottom/2);
dc.SetWindowOrg(0, 0);
//以下语句设置world空间到page空间的映射规则,将会产生一个y轴的剪切。
SetGraphicsMode(dc.GetSafeHdc(), GM_ADVANCED); //一定要首先打开GM_ADVANCED。
XFORM xf;
xf.eM11 = 1.0;
xf.eM12 = 1.0; //y轴方向的剪切常量为1.0
xf.eM21 = 0.0; //x轴方向的剪切常量为0.0
xf.eM22 = 1.0;
xf.eDx = 0.0;
xf.eDy = 0.0;
SetWorldTransform(dc.GetSafeHdc(), &xf);
dc.Rectangle(-50, 50, 50, -50); //这个矩形的中心在客户区中心,长度为100mm。不过由于设置了world空间,尽管从语句上来看是一个正方形,但是实际显示的却是一个锐角为45°的菱形。
dc.Ellipse(-50, 50, 50, -50); //尽管从语句上来看是一个圆,但是实际显示的却是一个椭圆。
}
4.结语
以上只是我的一些不成熟的看法,如果有不实之处,还望来信探讨:alialili@163.net