窗口、视口、屏幕显示详解--计算机绘图基本功
窗口: 逻辑环境中的一小部分,是一个矩形框;世界坐标系是逻辑坐标,SetWindowOrg(X,Y )设置窗口的逻辑坐标点(X,Y)映射为的设备环境的设备点(0,0)。
设备环境:显示器、打印机等等。坐标系为设备坐标系,正Y轴向下,正X轴向右,原点在左上角,固定不变,不可修改!其X、Y的负半轴为虚设,无法显示或无法打印图形。
视口: 设备环境中的一部分,一个矩形框;坐标系同设备环境。SetViewportOrg(X,Y )设置视口矩形框的坐标原点,默认值为设备坐标原点。视口是窗口的按比例的映像(投影)。
1. 窗口和视口以及设备环境出现的缘起
窗口和视口实际上描述的是同一个客观场景,它们之间的区别仅仅在于两者坐标系的单位不同。设备环境就是显示设备。窗口和视口中的场景必须通过映射模式正确的显示到设备环境中。 默认情况下,Windows GDI 绘图中的窗口坐标原点、视口坐标原点以及设备点重合在一起都是在设备点(0,0)处。Windows GDI的设备环境原点是位于屏幕的左上角,设备点始终位于(0,0)点,x轴向右,y轴向上;OpenGL使用的窗口坐标和WindowsGDI使用的窗口坐标是不一样的。OpengL默认世界窗口坐标原点位于屏幕中心,x轴向右,y轴向上。视口原点在屏幕左下角。
视口与窗口表征的是同一个实际几何物体,所以有窗口必定有视口,有视口必定有窗口。窗口或视口可以理解为用眼睛看到的物体实体例如一个球,设备环境显示区域(显示屏)可以理解为由于计算机屏幕长宽比不同,实际物体在计算机屏幕上看到的最终显示结果,例如显示屏中的球看起来变成一个椭球。几乎在所有的GDI函数中使用的坐标值都是采用的逻辑单位,也就是物体实际多大就是多大。Windows必须将逻辑单位转换为“设备单位”---像素,也就是物体在设备环境(显示屏或打印机)显示后看起来应该多大。这种转换是由映射方式、窗口和视口的原点以及窗口和视口的范围所控制的。 通过不同的映射方式可以在逻辑单位下改变窗口原点的位置而将物体现实在设备环境中,也可以在以像素为单位的视口坐标系下移动视口将物体显示在显示屏上,不管对窗口和视口原点作什么改变,设备点(0,0)始终是客户区的左上角,两者都是为了让物体在显示屏上现实。调整窗口原点和调整视口原点可以达到相同的效果。
视口是与设备相关的一个矩形区域,坐标单位是与设备相关的,直观的视口原点坐标的移动就是dc的移动。窗口的坐标是逻辑坐标,与设备无关。窗口坐标的原点与视口坐标的原点始终对应于同一点。对于同一个图形,用窗口坐标系统表达的该区域的长和宽与视口的坐标系统表达的长和宽是不同的,因单位不同。二者就定义了这两个坐标系统的比例关系。程序作图时,使用的坐标总是是窗口坐标,而实际的显示或输出设备却各有自己的坐标。例如,有的打印机设备水平和垂直分辨率不同,其象素实际上是长方形。程序编写画一个圆,若不经任何坐标转换,在打印机上输出的就是个椭圆。
在MFC 中ONDRAW 之前已经调用了ONPREPAREDC 函数为你做好映射模式。默认情况下,其映射模式为 MM_TEXT模式,即1:1模式。 要改变默认映射模式应重载OnPrepareDC重新设置自己的映射模式。为了提高绘图精度,经常需要改变映射模式。
2. 窗口和视口理解要点
窗口与视口一向是初学者比较难以理解的难点,本人以前也是糊里糊涂的,不过最近有时间去深入研究之后,才彻底弄明白,摆脱了以前很多错误的观念。弄清楚了这些才会更好的使用不同的坐标影射模式,更灵活的为自己的绘图带来便利:
首先要清楚窗口和视口的坐标原点始终是同一个点,窗口和视口中的内容是同样的内容。设备坐标(显示器)中则会根据视口或窗口原点的改变而显示出不同的内容。在MM_TEXT映射模式下世界坐标系第三象限的内容就是窗口坐标系下第一象限的内容,所以有SetViewportOrg(x, y)与SetWindowOrg(-x,-y)具有相同的效果。这个很难理解但是这是事实。“设备环境”如显示屏始终是只有左手坐标系xoy第一象限的区域。开始时窗口坐标原点、视口坐标原点以及设备点重合在一起都是在客户区的左上角为了将物体显示在现实屏上,通过SetWindowOrg(x,y)将"画布"(世界坐标系原点)向屏幕的左边移动x个单位,向屏幕的上方移动y个单位。SetViewportOrg(x,y)是将"画笔"(视口原点)向屏幕的右边移动x个单位,向屏幕的下方移动y个单位。
现将其关键点归纳如下:
1、 视口与显示设备有关,视口等同客户区,使用设备坐标。视口是和窗口等同的一块矩形区域,它的x轴向右和y轴向下。
2、 窗口与显示设备无关,窗口与视口为同一区域,但使用逻辑坐标,它的x轴向右,y轴向上。
3、 窗口与视口使用不同的坐标系,但是两套坐标系的原点始终为同一点。但该点坐标(不管是视口坐标原点还是窗口坐标原点)不一定为(0,0)。窗口就是视口,去吧仅仅在于两者建立的坐标系不同,移动窗口的同时就是移动视口,反之亦然。
4、 视口原点的位置(就是画笔dc的初始位置)仅仅由SetViewportOrgEx (x,y) 函数来移动。(x,y)是相对于客户区左上角的设备坐标,即像素),而SetWindowOrg(X,Y )设置窗口的逻辑坐标点(X,Y)映射为的设备环境的设备点(0,0)
It's easy to get SetViewportOrg and SetWindowOrg confused, but the distinction between them is actually quite clear. Changing the viewport origin to (x,y) with SetViewportOrg tells Windows to map the logical point (0,0) to the device point (x,y). Changing the window origin to (x,y) with SetWindowOrg does essentially the reverse, telling Windows to map the logical point (x,y) to the device point (0,0)―the upper left corner of the display surface. In the MM_TEXT mapping mode, the only real difference between the two functions is the signs of x and y. In other mapping modes, there's more to it than that because SetViewportOrg deals in device coordinates and SetWindowOrg deals in logical coordinates. <<Programing windows with MFC>>中这样来描述
dc始终开始从世界坐标系的原点开始画,画笔的原点就是画布的原点就是世界坐标系的原点。在MM_TEXT下通过SetViewportOrg(X,Y)和SetWindowOrg(-X,-Y)将世界坐标系的原点移动到设备环境坐标系下的(X,Y)点。SetWindowOrg(-X,-Y)指设备点(0,0)处对应dc绘图的逻辑点(-X,-Y),逻辑点(0,0)在MM_TEXT映射模式下位于设备坐标系的(X,Y)点。
1) 移动视口原点好比移动画笔dc,如果将视口原点设置为(xViewOrg,yViewOrg),相当于dc画笔的移动,则逻辑点(0,0)就会被映射为设备点(xViewOrg,yViewOrg),初始dc绘图在逻辑点(0,0)下笔,图形将以客户区(设备点)的(xViewOrg,yViewOrg)为中心进行绘制。在MM_TEXT映射模式下SetViewportOrg(150,100);dc绘图坐标原点从(0,0)向右下方移动到(150,100)。可以看出SetViewportOrg()函数可以更改设备上下文dc的坐标原点。
void CDemoView::OnPaint()
{
CPaintDC dc(this); // device context for painting
dc.SetViewportOrg(150, 100);
CPen PenBlue;
PenBlue.CreatePen(PS_SOLID,1,RGB(0,12,255));
dc.SelectObject(&PenBlue);
dc.Ellipse(-100,-100,100,100);
}
2) 移动窗口原点好比画布的移动,如果将窗口原点改变为(xWinOrg,yWinOrg),则逻辑点(xWinOrg,yWinOrg)将会被映射为设备点(0,0),SetWindowOrg(150,100);逻辑点(150,100)对应于设备点(0,0);不管对窗口和视口原点作什么改变,应该铭记第一设备点(0,0)始终是客户区的左上角,第二窗口和视口原点是同一个点。
void CDemoView::OnPaint()
{
CPaintDC dc(this); // device context for painting
dc.SetWindowOrg(-150,-100);
CPen PenBlue;
PenBlue.CreatePen(PS_SOLID,1,RGB(0,12,255));
dc.SelectObject(&PenBlue);
dc.Ellipse(-100,-100,100,100);
//OnDraw(&dc);
}
5、 理解窗口与视口的坐标转换公式:
Xviewport=(Xwindow-Xwinorg)* Xviewext / Xwinext + Xvieworg;
Yviewport=(Ywindow-Xwinorg)* Yviewext / Ywinext + Yvieworg;
此公式初看上去不好理解,变形如下:
(Xviewport-Xvieworg)/(Xwindow-Xwinorg)= Xviewext / Xwinext;
(Yviewport-Yvieworg)/(Ywindow-Xwinorg)= Yviewext / Ywinext;
如此就很好理解了:逻辑坐标单位与设备坐标单位的比,即比例因子。
6、 对于定义的客户区域大于程序所创建的窗口时,就需要有滚动条来滚动显示,才能显示完整的客户区域。
7、 处理滚动窗口:假设未滚动窗口时,客户区左上角对应窗口和视口的原点坐标,且默认均为(0,0), 如果滚动窗口时,水平方向向右滚动了X个像素,垂直方向向下滚动了Y个像素,则应该认为客户区域的左上角为窗口原点(同时也是视口原点)一起滚动,并且窗口原点(同时也是视口的原点)的坐标始终为(0,0)不变,变的只是窗口原点在设备环境坐标系中的位置。当前看到的场景就是设备环境(显示屏)中显示的内容,设备环境的坐标系永远不会移动。通过SetViewportOrgEx (X,Y)实现将世界坐标系的原点移到显示屏的(X,Y)位置。这两种移动有个相同的结果就是窗口坐标的原点与视口坐标的原点始终对应于同一点(世界坐标系的原点),而新的世界坐标系的原点位置恰好是设备环境中的(X,Y)。通过调试MFC中CSrollView类函数发现,它就是通过SetViewportOrgEx (X,Y)函数移动世界坐标系的原点从而实现“窗口的滚动”。
8、 坐标原点(不论是视口还是窗口)不等于坐标零点即设备点(0,0)始终是客户区的左上角(必需明白)。
9、 视口的坐标原点可以任意移动,但其零点始终在客户区左上角。
10、窗口原点可任意移动,窗口类似于单张照片,仅仅是当前看到的景物。视口类似于电影胶卷,记录了以前到现在的所有信息。
为了能够在计算机中更好的描述真实的世界,必须设定一种与真实世界相符的逻辑坐标系统。当然Windows为我们提供了设备坐标与逻辑坐标进行映射的接口。
Windows提供的坐标系统映射的接口有:
SetMapMode(int nMapMode) 设置映射模式,Windows提供了8种映射模式
SetWindowOrg(int x, int y) / SetWindowOrg(POINT point) 设置与视口坐标原点相对应的窗口坐标原点,windows要求视口的坐标原点必须与窗口的坐标原点相对应,x, y和point的单位为逻辑单位
SetViewportOrg(int x, int y) / SetViewportOrg(POINT point) 设置与窗口坐标原点相对应的视口坐标原点,windows要求视口的坐标原点必须与窗口的坐标原点相对应,x, y和point的单位为设备单位,即像素
SetWindowExt(int cx, int cy) / SetWindowExt(SIZE size) 设置逻辑坐标系统中窗口的大小范围,cx, cy和size的单位为逻辑单位
SetViewportExt(int cx, int cy) / SetViewportExt(SIZE size) 设置设备坐标系统中视口的大小范围,cx, cy和size的单位为设备单位,即像素
3.孙鑫老师关于图形错位的说明
当我们在窗口中点击鼠标左键的时候,得到的是设备坐标(680,390),在MM_TEXT的映射模式下,逻辑坐标和设备坐标是相等的,所以我们利用集合类保存的这个点的坐标是以象素为单位,坐标值为(680,390)。在调用OnDraw函数前,在OnPaint函数中调用了OnPrepareDC函数,OnPrepareDC函数内调用SetViewportOrgEx ()调整了显示上下文的属性,将视口的原点设置为了(0,-150),这样的话,窗口的原点,也就是逻辑坐标(0,0)将被映射为设备坐标(0,-150),在画线的时候,因为GDI的函数使用的是逻辑坐标,而图形在显示的时候,Windows需要将逻辑坐标转化为设备坐标,因此,原先保存的坐标点(680,390)(在GDI函数中,作为逻辑坐标使用),根据转换公式
xViewport = xWindow-xWinOrg+xViewOrg
和yViewport = yWindow-yWinOrg+yViewOrg,
得到设备点的x坐标为680-0+0=680,设备点的y坐标为390-0+(-150)=240,于是我们看到图形在原先显示地方的上方出现了。
关于解决方法的说明
首先我们在绘制图形之后,在保存坐标点之前,调用OnPrepareDC函数,调整显示上下文的属性,将视口的原点设置为(0,-150),这样的话,窗口的原点,也就是逻辑坐标(0,0)将被映射为设备坐标(0,-150),然后我们调用DPtoLP函数将设备坐标(680,390)转换为逻辑坐标,根据设备坐标转换为逻辑坐标的公式:
xWindow = xViewport-xViewOrg+xWinOrg,
yWindow = yViewport-yViewOrg+yWinOrg,得到逻辑点的x坐标为680-0+0=680,y坐标为390-(-150)+0=540,将逻辑坐标(680,540)保存起来,在窗口重绘时,会先调用OnPrepareDC函数,调整显示上下文的属性,将视口的原点设置为了(0,-150),然后GDI函数用逻辑坐标点(680,540)绘制图形,被Windows转换为设备坐标点(680,390),和原先显示图形时的设备点是一样的,当然图形就还在原先的地方显示出来。
从Windows的鼠标消息可以获得鼠标指针的当前坐标值(point.x和point.y)此坐标值是设备坐标。
很多MFC库函数尤其是CRect的成员函数只能工作在设备坐标下。
还有我们有时需要利用物理坐标,物理坐标的概念就是现实世界的实际尺寸。
设备坐标-逻辑坐标-物理坐标之间如何进行转换便成为我们要考虑的一个问题,物理坐标和逻辑坐标是完全要我们自己来做的,但WINDOWS提供了函数来帮助我们转换逻辑坐标和设备坐标。
CDC的LPtoDP函数可以将逻辑坐标转换成设备坐标
CDC的DPtoLP函数可以将设备坐标转换成逻辑坐标
下面列出我们应该在什么时候使用什么样的坐标系一定要记住:
◎CDC的所有成员函数都以逻辑坐标为参数
◎CWnd的所有成员函数都以设备坐标为参数
◎区域的定义采用设备坐标
◎所有的选中测试操作应考虑使用设备坐标。
◎需要长时间使用的值用逻辑坐标或物理坐标来保存。因设备坐标会因窗口的滚动变化而改变。