VC++例说Windows窗口、视口以及GDI映射模式

在Windows应用程序中,只要进行绘图,就要使用GDI坐标系统。Windows提供了几种映射方式,每一种映射都对应着一种坐标系。例如,绘制图形时,必须给出图形各个点在客户区的位置,其位置用x 和y两个坐标表示,x表示横坐标,y表示纵坐标。在所有的GDI绘制函数中,这些坐标使用的是一种“逻辑单位”。当GDI函数将结果输出送到某个物理设备上时,Windows将逻辑坐标转换成设备坐标(如屏幕或打印机的像素点)。本文讨论了图形环境中的各个映射模式,包括它们是什么,怎么工作的,以及它们真正的含义。
一、窗口、视口以及映射模式基本概念  
强调一个网上和教科书上没有将清楚但是至关重要的概念:窗口和视口其实是同一块矩形区域,两者坐标系的原点是同一个点。窗口和视口的区别仅仅是单位不同。窗口和视口都不是指显示屏或打印机上的区域,我们看到显示屏上的物体实际上是显示在“设备环境”上的,由于视口(也就是窗口)区域与设备环境的左手坐标系第一象限xoy平面有交集,我们才能看到视口中的物体。窗口的变换和视口的变换的目的是一样的,都是为了将物体显示在设备环境中,只不过由于视口中使用像素作为单位也就是显示屏的设备坐标,所以往往在视口中进行调整dc比较直观一些。

(一)逻辑坐标。逻辑坐标即是世界坐标系下的坐标。逻辑坐标与设备无关,客观世界中的场景可以由世界窗口或视口中进行描述,在窗口中进行描述时使用世界坐标系中的坐标单位也即逻辑坐标,在视口中进行描述使用的是设备坐标。 
(二)设备坐标。图形输出时,Windows将GDI函数中指定的逻辑坐标映射为设备坐标。在屏幕显示的设备坐标系统中,单位以像素点为准,水平值从左到右增大(正方向向右),垂直值从上到下增大(正方向向下)。注意设备坐标系的原点永远不会移动,它们仅仅与物理设备有关。

        设备空间的范围实际显示设备上的矩形区域,通常有三种范围:窗口的客户区(使用BeginPaint 或 GetDC 获取)全窗口(使用GetWindowDC 获取)全屏幕(使用GetDc(0)/ CreateDC 获取);还有一些特殊的设备空间,如内存设备空间(使用CreateCompatibleDC获取),打印机设备空间,元文件设备空间即(CreateMetaFile)Windows中包括以下3种设备坐标,以满足各种不同需要:   
  1、客户区域坐标,包括应用程序的客户区域,客户区域的左上角为(0, 0)。 
  2、屏幕坐标,包括整个屏幕,屏幕的左上角为(0, 0)。屏幕坐标用在WM_MOVE消息中(对于非子窗口)以及下面的Windows 函数中:CreateWindow 和MoveWindow(都对于非子窗口)、GetMessage、GetCursorPos、GetWindowRect、WindowFromPoint 和SetBrushOrg 中。用函数ClientToScreen和ScreenToClient可以将客户区域坐标转换成屏幕区域坐标,或反之。
  3、全窗口坐标,包括一个程序的整个窗口,包括标题条、菜单、滚动条和窗口框,窗口的左上角为(0,0)。使用GetWindowDC得到的窗口设备环境,可以将逻辑单位转换成窗口”坐标。


(三)映射模式。映射方式定义了Windows如何将GDI函数中指定的逻辑坐标映射为设备坐标。Windows为了程序员方便,允许程序员在一个假想的空间上(世界坐标系中)绘制图形,这就是逻辑空间,但是最终这些图形还是需要显示到真实的屏幕或打印机,这就是设备空间。此时就会出现一个问题,即如何将逻辑空间中的图形通过怎样的关联适当的显示在屏幕上呢?Windows给出了答案—映射模式,Windows内定了8种映射模式,其中有常用的6种固定的模式(MM_TEXT、MM_LOMETRIC、MM_HIMETRIC、MM_LOENGLISH、MM_HIENGLISH、MM_TWIPS)把逻辑单位及数轴的方向都确定好了,还有2种允许自行定义逻辑单位及数轴方向。这两种模式的区别在于MM_ISOTROPIC要求必须横纵两轴的逻辑单位必须是相等的,而MM_ANISOTROPIC是绝对的自定义,没有任何限制。 设备坐标系始终是左手坐标系且以显示区域的左上角的(0,0)点作为坐标原点,水平向右为x轴的正方向,垂直向下为y轴的正方向。窗口坐标系(就是视口坐标系)是可以自定义的坐标系,其坐标轴和原点都不固定,dc绘图始终是在窗口坐标系中进行。在MM_TEXT映射模式下,窗口坐标系的x轴向右,y轴向下和设备环境的坐标系方向相同,在没有移动窗口坐标系的原点时,窗口坐标系的原点也在设备点(0,0)处。在窗口坐标系的第一象限绘图,图像会显示在设备环境中。这开始给我一种错觉:视口就是设备环境的显示区域。   需要明确在MM_TEXT的映射模式下,视口坐标系(画笔)的x轴与设备环境坐标系(显示屏)的x轴平行都是水平向右为正,视口坐标系(画笔)的y轴与设备环境坐标系(显示屏)的y轴平行都是垂直向下为正。此时将dc从世界坐标系的原点(0,0)移动到(X,Y)后,dc绘制的图形会直接显示的设备环境(显示屏)上。

在MM_LOMETRIC、MM_HIMETRIC、MM_LOENGLISH、MM_HIENGLISH、MM_TWIPS的映射模式下,窗口坐标系的x轴向右,y轴向上。所以在窗口的第一象限绘图一般图像不会显示在设备环境上(如窗口的客户区),因为此时视口(窗口)的第一象限与设备环境的第一象限的交集为空。

映射模式

逻辑单位

增加值

x

y

MM_TEXT

像素

MM_LOMETRIC

0.01 cm

MM_HIMETRIC

0.001 cm

MM_LOENGLISH

0.01 in.

MM_HIENGLISH

0.001 in.

MM_TWIPS

1/1440 in.

MM_ISOTROPIC

任意(x = y)

可选

可选

MM_ANISOTROPIC

任意(x != y)

可选

可选

如果您将显示器的像素尺寸设定为1024×768,下表就是Windows NT报告的视口和窗口范围的值。

映像方式

视口范围(x,y)

窗口范围(x,y)

MM_LOMETRIC(1024, -768)(3,200, 2,400)
MM_HIMETRIC(1024, -768)(32,000, 24,000)
MM_LOENGLISH(1024, -768)(1,260, 945)
MM_HIENGLISH(1024, -768)(12,598, 9,449)
MM_TWIPS(1024, -768)(18,142, 13,606)

映像方式

逻辑单位

英寸

毫米

MM_LOENGLISH0.01 in.0.010.254
MM_LOMETRIC0.1 mm.0.003940.1
MM_HIENGLISH0.001 in.0.0010.0254
MM_TWIPS1/1400 in.0.0006940.0176
MM_HIMETRIC0.01 mm.0.0003940.01

/*--------------------------插入GetDeviceCaps的使用说明Begin-----------------------------

//所有像素数
int pagecx=dc.GetDeviceCaps(HORZRES);
int pagecy=dc.GetDeviceCaps(VERTRES);

//即每英寸点数
short cxInch = dc.GetDeviceCaps(LOGPIXELSX);
short cyInch = dc.GetDeviceCaps(LOGPIXELSY);

// 计算一个设备单位等于多少0.1mm
double scaleX = 254.0 / (double)GetDeviceCaps(dc.m_hAttribDC,LOGPIXELSX);
double scaleY = 254.0 / (double)GetDeviceCaps(dc.m_hAttribDC, LOGPIXELSY);

说明:
主要用到的参数见例子中的:HORZRES,VERTRES,LOGPIXELSX,LOGPIXELSY.总的来说是为了方便控制打印或重画时的控制,如为了定制打印时,一般依据的是物理的长度,而不是像素,而DC一般是用像素的映射模式,所以需要一下转换,上面这个函数就为这种转换设计的.

以上三者的关系通常满足:HORZSIZE = 25.4 * HORZRES/LOGPIXELSX
    HORZSIZE为屏幕水平尺寸(定为度量尺寸,以mm计),HORZRES为水平的像素总数(定为像素大小,平时所说的屏幕分辨率,但在这不这么称呼。这里,分辨率定为“每英寸的像素数”),LOGPIXELSX为逻辑像素(假设的每英寸的像素数,并不是刚才所说的实际的“分辨率”)。因此HORZSIZE也称为逻辑宽度。
    当我们选择“显示”属性里的大字体时,LOGPIXELSX(通常分为96dpi与120dpi)变大了,这样假设原来的字体为10磅,则原来的字体横向所占像素(实际所占的像素数)为10*(1/72)*LOGPIXELSX,现在LOGPIXELSX变大了,则字体所占像素也大了,因此看起来字体大了。如果HORZRES不变的话,则HORZSIZE应该变小。然后这是和Windows有关的,在16位OS中,HORZSIZE值是固定的。
    在XP系统上验证了一下,发现HORZSIZE值与LOGPIXELSX的值也是不变的,如果改变HORZRES的话,则HORZSIZE会发生相应变化,但LOGPIXELSX不变,一直是96。

/*--------------------------插入GetDeviceCaps的使用说明End-----------------------------

这些窗口范围表示包含显示器全部宽度和高度的逻辑单位元数值。320毫米宽的屏幕(MM_HIMETRIC下屏幕宽度的逻辑单位为0.01mm,共32000个间隔,故1024像素实际中对应32000*0.01=320mm)也为1260 MM_LOENGLISH单位或12.6英寸(320除以25.4毫米/英寸)。
首先要明确图形只能在设备环境的坐标系中显示。其次要注意视口范围中,视口坐标系的y轴前面的负号表示改变了画笔dc移动时y轴的方向。对于这五种映像方式,视口范围相当于显示屏沿着x轴向屏幕上方折叠后位于实际显示屏上方的矩形区域,想象一下当前显示屏上面放置了一个同样的大小的虚拟显示屏,dc的y值随dc的上升而增加,dc向上移动然后然后绘制图形将显示在虚拟屏幕中,这个事实有一个有趣的结果。此时要想在设备环境的显示区域显示任何东西,必须使用负的y值才能将画笔移动到真实的显示屏上。VC++例说Windows窗口、视口以及GDI映射模式 - 阿英 - Mr.Right

例如下面的程序代码:
void CDemoView::OnPaint() 
{
 CPaintDC dc(this); // device context for painting
 dc.SetMapMode(MM_LOMETRIC);

 //dc.TextOut(400,400,"Hello"); //看不到屏幕上的Hello,此时dc画到显示屏上方的虚拟显示屏上了。
 dc.TextOut(400,-400,"Hello"); //可以看出屏幕上的Hello
}

为了使自己保持头脑清醒,您可能想避免这样做。

1)一种解决办法是将逻辑的(0,0)点设为显示区域的左下角,您可以通过呼叫SetViewportOrgEx (hdc, 0, cyClient, NULL) ;(假设cyClient是以像素为单位的显示区域的高度)。此时的坐标系是直角坐标系的右上象限。这相当于将视口的原点从设备环境的左上角移动到左下角,将当前显示屏正上方那个虚拟显示屏向下移动,覆盖掉当前的显示屏区域,此时视口的区域和设备环境显示区域重合,所以视口中的物体能显示到计算机屏幕上。例如下面的程序代码:
void CDemoView::OnPaint() 
{
 CPaintDC dc(this); // device context for painting
 CRect Recto;
 GetClientRect(&Recto);
 dc.SetMapMode(MM_LOMETRIC);
 dc.SetViewportOrg(0,Recto.Height());
 dc.TextOut(400,400,"Hello"); //结果与题设的结果相同!
}
2)另一种方法是将逻辑(0,0)点设为显示区域的中心:
SetViewportOrgEx (hdc, cxClient / 2, cyClient / 2, NULL) ;代码如下

void CDemoView::OnPaint() 
{
 CPaintDC dc(this); // device context for painting
 CRect Recto;
 GetClientRect(&Recto);
 dc.SetMapMode(MM_LOMETRIC);
 dc.SetViewportOrg(Recto.Width()/2,Recto.Height()/2);
 dc.TextOut(400,400,"Hello");
}   现在,我们有了一个真正的4象限笛卡尔坐标系,在x轴和y轴上有相等的按英寸、毫米或twip计算的逻辑单位。
3)您还可以使用SetWindowOrgEx函数来改变逻辑(0,0)点,但是这稍微困难一些,因为SetWindowOrgEx的参数必须使用逻辑单位,先要将(cxClient,cyClient)用DPtoLP函数转换为逻辑坐标。假设变量pt是型态为POINT的结构,下面的代码将逻辑(0,0)点改变到显示区域的中央:
pt.x = cxClient ;
pt.y = cyClient ;
DptoLP (hdc, &pt, 1) ;
SetWindowOrgEx (hdc, -pt.x / 2, -pt.y / 2, NULL) ;代码如下

void CDemoView::OnPaint() 
{
 CPaintDC dc(this); // device context for painting
 CRect Recto;
 CPoint pt;
 GetClientRect(&Recto);
 dc.SetMapMode(MM_LOMETRIC);
 pt.x = Recto.Width()/2;
 pt.y = Recto.Height()/2;
 DPtoLP(dc,&pt,1);
 dc.SetWindowOrg(-pt.x,-pt.y);
 dc.TextOut(400,400,"Hello"); //结果与2)相同
}

二、MM_ISOTROPIC和MM_ANISOTROPIC映射模式

 1)Isotropic的意思是“同方向性”。 如果想要在使用任意的轴时都保证两个轴上的逻辑单位相同,逻辑坐标系中的圆形在设备环境中现实为圆形,则MM_ISOTROPIC映射方式就是理想的映射方式。这时具有相同逻辑宽度和高度的矩形显示为正方形,具有相同逻辑宽度和高度的椭圆显示为圆。当您刚开始将映射方式设定为MM_ISOTROPIC时,Windows使用与MM_LOMETRIC同样的窗口和视口范围(但是,不要对此有所依赖)。区别在於,您现在可以呼叫SetWindowExt和SetViewportExt来根据自己的偏好改变范围了,然後,Windows将调整范围的值,以便两条轴上的逻辑单位有相同的实际距离。

SetWindowExt(int Lwidth, int Lheight) //参数的单位为逻辑单位(Logical);
SetViewportExt(int Pwidth, int Pheight) //参数的单位为像素(Pixel); 
以x轴为例(y轴类似),逻辑坐标系中的x轴的单位刻度=| Pwidth | / | Lwidth |表示x轴上一个逻辑单位等于多少个像素。比如我们先通过GetDeviceCap(LOGPIXELSX)获得在我们的显示器上每英寸等于多少个像素,设为p,然后我们将它赋给Pwidth,将Lwidth赋成2,即Pwidth / Lwidth=p / 2。那么此时逻辑坐标系x轴上的单位刻度就是p / 2个像素;又由于p个像素是代表一个英寸的,所以此时的逻辑坐标系x轴上的单位刻度同时也是半个英寸。还有一点要注意的是,如果Lwidth与Pwidth同号,逻辑坐标的x轴方向与设备坐标系中的x轴方向相同,否则相反。
此外,当使用MM_ISOTROPIC模式时,如果通过计算window与viewport范围的比值得到两个方向的单位刻度值不同,那么将会以较小的那个为准

 

例如,假设您想要一个「传统的」单象限虚拟坐标系,其中(0,0)在显示区域的左下角,宽度和高度的范围都是从0到2000,并且希望x和y轴的单位具有同样的实际尺寸。以下就是所需的程序:
SetMapMode (hdc, MM_ISOTROPIC) ;
SetWindowExtEx (hdc, 2000, 2000, NULL) ;
SetViewportExtEx (hdc, cxClient, -cyClient, NULL) ;
SetViewportOrgEx (hdc, 0, cyClient, NULL) ;
如果其后用GetWindowExtEx和GetViewportExtEx函数获得了窗口和视端口的范围,可以发现,它们并不是先前指定的值。Windows将根据显示设备的纵横比来调整范围,以便两条轴上的逻辑单位表示相同的实际尺寸。如果显示区域的宽度大于高度(以实际尺寸为准),Windows将调整x的范围,以便逻辑窗口比显示区域视口窄。这样,逻辑窗口将放置在显示区域的左边。代码如下:

void CDemoView::OnPaint() 
{
 CPaintDC dc(this); // device context for painting
 CRect Recto;
 GetClientRect(&Recto);
  dc.SetMapMode(MM_ISOTROPIC);
  dc.SetWindowExt(2000,2000); //现实中的逻辑坐标系的x轴有2000个单位,y轴也有2000个单位。
  dc.SetViewportExt(Recto.Width(),-Recto.Height()); // Lwidth与Pwidth异号,所以逻辑坐标系的y轴垂直向上
 dc.SetViewportOrg(0,Recto.Height());//将视口原点区域下移到设备环境的左下方,此时窗口中的物体可以显示到设备环境上(显示屏)

  dc.Ellipse(-2000,-2000,2000,2000);
 dc.MoveTo(0,0);
 dc.LineTo(2000,2000);

}

前面给出的程序代码等价的用SetWindowOrg改为:
SetMapMode (MM_ISOTROPIC) ;
SetWindowExtEx (hdc, 2000, 2000, NULL) ;
SetViewportExtEx (hdc, cxClient, -cyClient, NULL) ;
SetWindowOrgEx (hdc, 0, 2000, NULL) ;

在呼叫SetWindowOrgEx中,我们将逻辑点(0, 2000)映像为设备点(0,0)。程序代码如下

void CDemoView::OnPaint() 
{
 CPaintDC dc(this); // device context for painting
 CRect Recto;
 GetClientRect(&Recto);
  dc.SetMapMode(MM_ISOTROPIC);
  dc.SetWindowExt(2000,2000); //现实中的逻辑坐标系的x轴有500个单位,y轴也有500个单位。
  dc.SetViewportExt(Recto.Width(),-Recto.Height()); // Lwidth与Pwidth异号,所以逻辑坐标系的y轴垂直向上,与设备环境的y轴相反
  dc.SetWindowOrg(0,2000); //将逻辑点(0, 2000)映射为设备点(0,0),逻辑点(0,0)恰好在设备点(0,cyClient)
  dc.Ellipse(-2000,-2000,2000,2000);
  dc.MoveTo(0,0);
  dc.LineTo(2000,2000);
}

您也许想要使用一个四象限的笛卡尔坐标系,四个方向的坐标尺度可以任意指定,(0,0) 必须居于显示区域的中央。如果您想要每条轴的范围从0到1000,则可以使用以下程序代码:
SetMapMode (hdc, MM_ISOTROPIC) ;
SetWindowExtEx (hdc, 1000, 1000, NULL) ;
SetViewportExtEx (hdc, cxClient / 2, -cyClient / 2, NULL) ;
SetViewportOrgEx (hdc, cxClient / 2, cyClient / 2, NULL) ;

void CDemoView::OnPaint() 
{
 CPaintDC dc(this); // device context for painting
 CRect Recto;
 GetClientRect(&Recto);
  dc.SetMapMode(MM_ISOTROPIC);
  dc.SetWindowExt(2000,2000); //现实中的逻辑坐标系的x轴有2000个单位,y轴也有2000个单位。
  dc.SetViewportExt(Recto.Width(),-Recto.Height()); // Lwidth与Pwidth异号,所以逻辑坐标系的y轴垂直向上,与设备环境的y轴相反
  dc.SetViewportOrg(Recto.Width()/2,Recto.Height()/2);
  dc.Ellipse(-1000,-1000,1000,1000);
  dc.MoveTo(0,0);
  dc.LineTo(2000,2000);
}

在MM_ISOTROPIC映像方式下,可以使逻辑单位大于像素。例如,假设您想要一种映像方式,使点(0,0)显示在屏幕的左上角,y的值向下增长(和MM_TEXT相似),但是逻辑坐标单位为1/16英寸。以下是一种方法:
SetMapMode (hdc, MM_ISOTROPIC) ;
SetWindowExtEx (hdc, 16, 16, NULL) ;
SetViewportExtEx (hdc, GetDeviceCaps (hdc, LOGPIXELSX),
GetDeviceCaps (hdc, LOGPIXELSY), NULL) ;
SetWindowExtEx函数的参数指出了每一英寸中逻辑单位数。SetViewportExtEx函数的参数指出了每一英寸中实际单位数(像素)。
然而,这种方法与Windows NT中的度量映像方式不一致。这些映射方式使用显示器的像素大小和公制大小。要与度量映像方式保持一致,可以这样做:
SetMapMode (hdc, MM_ISOTROPIC) ;
SetWindowExtEx (hdc, 160 * GetDeviceCaps (hdc, HORZSIZE) / 254,
160 * GetDeviceCaps (hdc, VERTSIZE) / 254, NULL) ;
SetViewportExtEx (hdc, GetDeviceCaps (hdc, HORZRES),
GetDeviceCaps (hdc, VERTRES), NULL) ;

在这个程序代码中,视埠范围设定为按像素计算的整个屏幕的大小,窗口范围则必须设定为以1/16英寸为单位的整个屏幕的大小。GetDeviceCaps以HORZRES和VERTRES为参数,传回以毫米为单位的设备尺寸。如果我们使用浮点数,将把毫米数除以25.4,转换为英寸,然后,再乘以16以转换为l/16英寸。但是,由于我们使用的是整数,所以先乘以160,再除以254。
当然,这种坐标系会使逻辑单位大于实际单位。在设备上输出的所有东西都将映像为按1/16英寸增量的坐标值。当然,这样就不能画两条间隔l/32英寸的水平直线,因为这样将需要小数逻辑坐标。

2)在MM_ANISOTROPIC映射方式下,Windows不对您所设定的值进行调整,这就是说,MM_ANISOTROPIC不需要维持正确的纵横比。使用MM_ANISOTROPIC的一种方法是对显示区域使用任意坐标,就像我们对MM_ISOTROPIC所做的一样。下面的程序代码将点(0,0)设定为显示区域的左下角,x轴和y轴都从0到2000:
SetMapMode (hdc, MM_ANISOTROPIC) ;
SetWindowExtEx (hdc, 2000, 2000, NULL) ;
SetViewportExtEx (hdc, cxClient, -cyClient, NULL) ;
SetViewportOrgEx (hdc, 0, cyClient, NULL) ;
 在MM_ISOTROPIC方式下,相似的程序代码导致显示区域的一部分在轴的范围之外。但是对于MM_ANISOTROPIC,不论其尺度多大,显示区域的右上角总是(2000, 2000)。如果显示区域不是正方形的,则逻辑x和y的单位具有不同的实际尺度。代码如下:
void CDemoView::OnPaint() 
{
 CPaintDC dc(this); // device context for painting
 CRect Recto;
 GetClientRect(&Recto);
 dc.SetMapMode(MM_ANISOTROPIC); //和MM_ISOTROPIC进行对比便清楚
 dc.SetWindowExt(2000,2000); //现实中的逻辑坐标系的x轴有2000个单位,y轴也有2000个单位。
 dc.SetViewportExt(Recto.Width(),-Recto.Height()); // Lwidth与Pwidth异号,所以逻辑坐标系的y轴垂直向上
 dc.SetViewportOrg(0,Recto.Height());//将视口原点区域下移到设备环境的左下方,此时窗口中的物体可以显示到设备环境上(显示屏)
 dc.Ellipse(-2000,-2000,2000,2000);
 dc.MoveTo(0,0);
 dc.LineTo(2000,2000);
}

 另一种使用MM_ANISOTROPIC的方法是将x和y轴的单位固定,但其值不相等。例如,如果有一个只显示文字的程序,您可能想根据单个字符的高度和宽度设定一种粗刻度的坐标:
SetMapMode (hdc, MM_ANISOTROPIC) ;
SetWindowExtEx (hdc, 1, 1, NULL) ;
SetViewportExtEx (hdc, cxChar, cyChar, NULL) ;
当然,这里假设cxChar和cyChar分别是那种字体的字符宽度和高度。现在,您可以按字符行和列指定坐标。下面的叙述在距离显示区域左边三个字符,上边二个字符处显示文字:
TextOut (hdc, 3, 2, TEXT ("Hello"), 5) ;

三、窗口、视口以及映射模式的意义

MFC缺省地使用MM_TEXT映射模式,一个逻辑单位等于设备中的一个象素。它是实现“所见即所得”的基础。其实大部分情况下使用像素进行工作是很合适的,不需要使用除了MM_TEXT方式外的任何映射方式。但是如果你需要进行类似CAD这样的绘图时,需要以英寸或者厘米尺寸显示图像,就需要使用其他映射方式,以方便编程。因为只要你把映射方式确定后,你只需要在逻辑空间使用逻辑单位绘制图像就好了,无需担心怎样显示在真实的屏幕或打印机上,这些繁琐的工作Windows会做好的。

这时可能会有人问,要是使用cm作为逻辑单位,Windows是如何确定1cm的真实长度呢?我悄悄地告诉你,“Windows 它根本不知道!”,别惊讶,听我接着说,Windows它确实不知道,但是Windows知道像素,这个很重要,它会以像素为依据计算得出1cm的长度(不一定是真实长度)。当程序需要绘制了一个10cm×10cm的矩形时,如果需要显示在打印机上,那么Windows首先通过GetDeviceCaps获取打印机相关信息如每英寸显示320像素(点),而1英寸≈2.54厘米,那么Windows就可以计算出10cm其实就是(320/2.54)*101259.8点。10cm×10cm矩形=1259.8×1259.8点矩形,打印机上完美的显示了10cm×10cm的矩形没有任何问题,10cm的长度绝对正确,不信你可以用尺子量!

打印机上可以这样做,但是显示器上是不行的,此时Windows给出了一个办法,当然它也是根据人眼的视觉经验,即让显示器每英寸显示96点(正常字体),此时96/2.5437.8点,人眼看着很好,但是1cm的长度可能是不真实的。这些不重要,重要的是打印出来绝对正确就好了。 

四、VC++中窗口、视口以及映射模式的应用举例

例1. 验证世界窗口的原点和视口的原点位于同一个点,初始时它们与设备坐标系的原点重合均在客户区的左上角。

void CDemoView::OnPaint() 
{
 CPaintDC dc(this); // 绘图的设备上下文
 CRect Recto;
 CPen PenBlue;
 PenBlue.CreatePen(PS_SOLID, 1, RGB(0, 12, 255));

 //dc.SetViewportOrg(100,100);  // Step 1: 仅加上这一句后可以看出,视口原点的下移和客户区的下移

 //dc.SetWindowOrg(-100,-100); // Setp 2: 将Step 1注释掉,加上此句可以得到和Step 1相同的结果
 dc.SelectObject(&PenBlue);
 dc.Ellipse(-100, -100, 100, 100);  //圆心位于屏幕的左上角,仅仅只有圆的四分之一部分(270度到360度的部分)显示在屏幕上。

 dc.MoveTo(0,0);
 dc.LineTo(100,100);
 CPen PenBlack; 
 PenBlack.CreatePen(PS_SOLID, 1, BLACK_PEN);
 dc.SelectObject(&PenBlack);

 GetClientRect(&Recto); // 得到客户区域的尺寸,我以前没看出客户区的左上角就是视口的原点,而误以为是设备坐标的原点了。
 dc.MoveTo(Recto.Width() / 2, 0); //dc的移动是参考视口原点画笔的相对移动,注意这里视口的原点(也是窗口原点)并没有移动
 dc.LineTo(Recto.Width() / 2, Recto.Height());
 dc.MoveTo(0, Recto.Height() / 2);
 dc.LineTo(Recto.Width(), Recto.Height() / 2);
}

例2. 验证世界窗口的原点和视口的原点位于同一个点,默认MM_TEXT映射模式下x轴水平向右,y轴垂直向下。其他固定映射模式的使用

void CDemoView::OnPaint() 
{
CPaintDC dc(this); // device context for painting

 dc.SetMapMode(MM_TEXT); //读者可以将MM_TEXT换为MM_LOMETRIC等其他的固定映射模式

 dc.SetViewportOrg(380, 220);
 // Use a red pen
 CPen PenRed(PS_SOLID, 1, RGB(255, 0, 0));
 dc.SelectObject(PenRed);
 // A circle whose center is at the origin (0, 0)
 dc.Ellipse(-100, -100, 100, 100);
 // Use a blue pen
 CPen PenBlue(PS_SOLID, 1, RGB(0, 0, 255));
 dc.SelectObject(PenBlue);
 // Horizontal axis
 dc.MoveTo(-380, 0);
 dc.LineTo(380, 0);
 // Vertical axis
 dc.MoveTo(0, -220);
 dc.LineTo(0, 220);
 // An orange pen
 CPen PenOrange(PS_SOLID, 1, RGB(255, 128, 0));
 dc.SelectObject(PenOrange);
 // A diagonal line at 45 degrees
 dc.MoveTo(0, 0);
 dc.LineTo(120, 120); //直线没有在笛卡尔坐标系的45度位置,而是位于笛卡尔坐标系统的第四象限
}

 例3. 自定义映射模式MM_ISOTROPIC的使用

  void CDemoView::OnPaint() 
{
 CPaintDC dc(this); // device context for painting
 dc.SetMapMode(MM_ISOTROPIC);
 dc.SetWindowExt(10240,7680);
 dc.SetViewportExt(1024,768);
 dc.Ellipse(-100,-100,100,100); //以客户区左上角(0,0)画出一个半径为10pixels的圆
}其本质就是,X方向,每个逻辑单位有1024/10240个象素,Y方向每个逻辑单位有768/7680个象素。因此以下代码有相同的作用。

void CDemoView::OnPaint() 
{
 CPaintDC dc(this); // device context for painting
 dc.SetMapMode(MM_ISOTROPIC);
 dc.SetWindowExt(102400,76800);
 dc.SetViewportExt(10240,7680);
 dc.Ellipse(-100,-100,100,100);
}

例4. 自定义映射模式MM_ANISOTROPIC的使用以左上角为原点,X轴和Y轴为1000的坐标

void CDemoView::OnDraw(CDC* pDC)
{
 CDemoDoc* pDoc = GetDocument();
 ASSERT_VALID(pDoc);
 // TODO: add draw code for native data here
 CRect rect;
 GetClientRect(&rect);
 pDC->SetMapMode(MM_ANISOTROPIC);
 //pDC->SetViewportOrg(0,0); //可以注释掉
 pDC->SetViewportExt(rect.right,rect.bottom);
 pDC->SetWindowOrg(0,0);
 pDC->SetWindowExt(1000,1000);
 
 pDC->MoveTo(50,50);
 pDC->LineTo(50,950);
 pDC->LineTo(950,950);
 pDC->LineTo(50,50);
}

代码分析:
1. GetClientRect(&rect); 取得客户区矩形区域,将其存放在rect中
2. 用pDC->SetMapMode(MM_ANISOTROPIC); 设置映射模式
3. 通过pDC->SetViewportOrg(0,0);设置逻辑坐标的原点。
4. 通过pDC->SetViewportExt(rect.right,rect.bottom);和 
pDC->SetWindowExt(1000,1000);来确定逻辑坐标下和设备坐标下的尺寸对应关系
5. 在MM_ANISOTROPIC模式下,X轴单位和Y轴单位可以不相同
6. 坐标方向的确定方法是如果逻辑窗范围和视口范围符号相同,则逻辑坐标的方向和视口的方向相同,即X轴向右为正,Y轴向下为正。
7. 如果将显示模式改为MM_ISOTROPIC,那么X轴单位和Y轴单位一定相同,感兴趣的读者可以自己使一下。

例5.绘制点状网格

void CDemoView::OnPaint() 
{
 CPaintDC dc(this); // device context for painting 
 CRect Recto;
 GetClientRect(&Recto);
 CBrush bgBrush(BLACK_BRUSH); 
 
 dc.SelectObject(bgBrush);
 dc.Rectangle(Recto); 
 for(int x = 0; x < Recto.Width(); x += 20)
 { 
  for(int y = 0; y < Recto.Height(); y += 20)
  { 
   dc.SetPixel(x, y, RGB(255, 255, 255));
  } 
 }
}注意屏幕闪的厉害时,可以添加如下代码

BOOL CDemoView::OnEraseBkgnd(CDC* pDC) 
{
 return TRUE;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值