5.5 GDI 映射模式

摘录于《Windows程序(第5版,珍藏版).CHarles.Petzold 著》P144

        到目前为止,所有的范例程序都是相对于客户区左上角坐标以像素为单位来绘制的。这是默认的状况,但这并非唯一的选择。有一个称为“映射模式”(mapping mode)的设备环境属性,它能影响几乎所有在客户区绘制的图形。和映射模式紧密相关的还有 4 个其他的设备环境属性,分别为窗口原点(window origin)、视口原点(viewport origin)、窗口范围(window extents)、视口范围(viewport extents)。

        大多数的 GDI 绘图函数需要制定一个坐标值。例如,下面是一个 TextOut 函数的调用:

[cpp]  view plain  copy
  1. TextOut (hdc, x, y, psText, iLength);  
参数 x 和 y 表示文本的起始坐标位置。其中,参数 x 是水平方向上的位置,参数 y 是垂直方向上的位置。 通常,我们用坐标(x, y)来表示这个点。

        如同在几乎所有的 GDI 函数中一样,在 TextOut 函数中的这些坐标值是“逻辑单位(logical unit)。Windows 必须要将逻辑单位转换为 “设备单位”(device unit)也就是像素。这些转换是由映射模式、窗口原点、视口原点、窗口范围和视口范围共同控制的。映射模式同样暗含着 x 轴和 y 轴的方向,也就是说,它决定了随着向现实区域的左边移动,x 值是增加还是减小;随着向现实区域的上边移动,y 值是增加还是减小。

        Windows 定义了 8 种映射模式。它们在 WINGDI.H 中定义的标识符如下表所示。

映射模式逻辑单位值增加的方向
 x 轴y 轴
 MM_TEXT 像素 右 下
 MM_LOMETRIC 0.1 mm 右 上
 MM_HIMETRIC 0.01 mm 右 上
 MM_LOENGLISH 0.01 in. 右 上
 MM_HIENGLISH 0.001 in. 右 上
 MM_TWIPS 1/1440 in. 右 上
 MM_ISOTROPIC 任意(x = y) 可选 可选
 MM_ANISOTROPIC 任意(x != y) 可选 可选
单词 METRIC(公制)和 ENGLISH(英制)指的是两种比较通用的测量系统;LO 和 HI 是“低”(Low)和“高”(High),指的是精度的高低。“Twip”是一个杜撰的词,意思是“1/20点”。我在前面提到过,在排版中,一个点是一个基本测量单位,大约为 1/72 英寸,但是在图形程序设计中,常假定它正好是 1/72 英寸。一个 “Twip”是 1/20 点,也就是 1/1440 英寸。“isotropic” 和 “anisotropic” 是真实的单词,意思分别是“各向同性”和“各向异性”。

        映射模式可以由下面的函数设置:

[cpp]  view plain  copy
  1. SetMapMoe (hdc, iMapMode);  
其中,iMapMode 是 8 种映射模式对应的标识符之一。当前映射模式可以调用下面的函数来获取:

[cpp]  view plain  copy
  1. iMapMode = GetMapMoe (hdc);  

        在默认情况下,映射模式是 MM_TEXT。在这种映射模式下,逻辑单位和物理单位相同,这允许我们直接(或者,取决于你自己的观点,也可以说成是强迫我们必须)以像素为单位操作。在如下的 TextOut 调用方法中:

[cpp]  view plain  copy
  1. TextOut (hdc, 8, 16, TEXT("Hello"), 5);  
文本起始于距离客户区左上角向右 8 像素、向下16像素处。

        如果是在 MM_LOENGLISH 映射模式下:

[cpp]  view plain  copy
  1. SetMapMoe (hdc, MM_LOENGLISH);  
现在一个逻辑单位等于百分之一英寸。调用如下的TextOut 函数:

[cpp]  view plain  copy
  1. TextOut (hdc, 50, -100, TEXT("Hello"), 5);  
文本起始于距离客户区左上角向右 0.5 英寸,向下 1 英寸处。(在前面的调用中,y 坐标前使用了一个负号,随着我更详细地讨论映射模式,其原因会逐渐清晰。)其他的映射模式坐标允许程序以毫米、点的大小或者任意单位的坐标轴的形式来表示。

        如果感觉以像素为单位操作起来很方便,就没有必要使用默认 MM_TEXT 模式以外的任何映射模式。如果需要以英寸或者毫米为单位显示一幅图像,则可以从 GetDeviceCaps 函数获取需要的信息,自己再进行缩放。另外的映射模式都是为了避免你自己进行缩放工作而提供的一个方便的途径

        尽管在 GDI 函数中指定的坐标是 32 位的值,但是仅在 Windows NT 中才能够处理 32 位。在 Windows 98 中,坐标限制为 16 位,这样它的范围就是 -32768 到 32767。一些使用坐标作为矩形的起点和终点的 Windows 函数也需要满足这个条件,就是矩形的宽度和高度必须是 32768 或者更小。

5.5.1 设备坐标和逻辑坐标

        你或许会问:如果我使用 MM_LOENGLISH 映射模式,是否会得到以百分之一英寸来度量 WM_SIZE 消息?绝对不是。WIndows 对所有消息(例如 WM_MOVE、WM_SIZE 和 WM_MOUSEMOVE),所有的非 GDI 函数,甚至一些 GDI 函数,都继续使用设备坐标。可以按这种方式考虑:映射模式是设备环境的一种属性,因此,只有当使用设备环境句柄作为参数的 GDI 函数时,映射模式才会生效。GetSystemMetrics 是一个非 GDI 函数,因此它将继续以设备单位的形式,也就是像素为单位返回。尽管 GetDeviceCaps 是一个需要设备环境句柄的 GDI 函数,但是 Windows 继续为 HORZRES 和 VERTRES 索引返回设备单位,因为这个函数的目的之一就是以像素为单位向程序返回设备的尺寸。

        然而,通过调用 GetTextMetrics 函数获取的 TEXTMETRIC 结构中的值以逻辑单位形式给出。如果调用这个函数时,映射模式是 MM_LOENGLISH,GetTextMetrics 函数就以百分之一英寸为单位来返回字符的宽度和高度。为了简化工作,当调用 GetTextMetrics 函数获取字符的高度和宽度信息时,映射模式应当和基于这些尺寸绘制文本使用的映射模式相同。

5.5.2 设备坐标系统

        Windows 会把在 GDI 函数中指定的逻辑坐标转换为设备坐标。在我们讨论用于各种映射模式的逻辑坐标系统之前,首先介绍一下 Windows 为视频显示器定义的不同的设备坐标系统。尽管在大多数情况下我们都是在我们窗口的客户区内工作,但是在有些情况下,Windows 还使用另外两种设备坐标系统。在所有的设备坐标系统中,单位都是以像素的形式表示的。水平方向上 x 值从左向右增加,垂直方向上y 值从上往下增加

        当我们使用整个屏幕,我们是以“屏幕坐标”(screen coordinate)的形式工作的。屏幕左上角是点(0, 0)。屏幕坐标用于 WM_MOVE 消息(对非子窗口)和下列的 Windows 函数中:CreateWindow 和 MoveWindows(对非子窗口)、GetMessagePos、GetCursorPos、SetCursorPos、GetWindowRect 和 WindowFormPoint。(这并不是一个完整的清单。)这些函数一般分为两类,一类是与窗口无关的函数(例如两个和鼠标指针相关的函数),还有一类是必须根据屏幕上的点移动或寻找窗口的函数。如果使用带“DISPLAY”参数的 CreateDC 函数来获取整个屏幕的设备环境,那么在 GDI 调用中,逻辑坐标值将被默认映射屏幕坐标。

        “全窗口”坐标指的是一个应用程序的整个应用窗口,包括标题栏、菜单、滚动条和边框。对一个普通的应用窗口,点(0, 0)是边框的左上角。全窗口坐标在 Windows 中很少用,但是如果设备环境是从 GetWindowDC 函数获取的,则在 GDI 函数调用中,逻辑坐标会被默认映射为全窗口坐标。

        第三种设备坐标系统(也是我们最常使用的坐标系统)是“客户区坐标”。点(0, 0)是客户区左上角。调用 GetDC 或 BeginPaint 函数获取设备环境时,在 GDI 函数中的逻辑坐标将被默认转换为客户区坐标。

        可以使用 ClientToScreen 函数将客户区坐标转换到屏幕坐标,反之亦然,可以调用 ScreenToClient 函数把屏幕坐标转换到客户区坐标。也可以调用 GetWindowRect 函数以屏幕坐标的形式获取整个窗口的位置和大小。这三个函数为把任何一种设备坐标转换为另一种设备坐标提供了足够的信息。

5.5.3 视口和窗口

         映射模式定义了 Windows 如何将 GDI 函数中指定的逻辑坐标映射到设备坐标。这里的设备坐标系统取决你获取设备环境所用函数。为了继续讨论映射模式,我们需要定义一些术语。映射模式被定义为从“窗口”(window)(逻辑坐标)到“视口”(viewport)(设备坐标) 的映射

        使用这两个术语是很不准确的。因为它们在其他情况下还有其他意思。在其他的一些图形界面系统中,视口常常含有“剪切区域”的意思。在 Windows 中,“窗口”还有一个非常具体的意思,就是描述一个程序占用屏幕的区域。在本章的讨论中,我们必须先把对这些术语的陈见放到一边。

        视口是以设备坐标(像素)的形式指定的。大多数情况下,视口与客户区相同,但是如果从 GetWindowDC 或者 CreateDC 函数获取了设备环境,视口也可以是指全窗口坐标或屏幕坐标。点(0, 0)是客户区(或者全窗口,或者屏幕)的左上角。x 值向右增加,y 值向下增加。

        窗口是以逻辑坐标的形式指定的,可能是像素、毫米、英寸或者其他你想用的任何单位。可以在 GDI 绘图函数中指定想用的逻辑窗口坐标。

        但是在真实的意义上,视口和窗口都仅仅是数学的概念。对于所有的映射模式,Windows 使用下面的公式将窗口(逻辑)坐标转换为视口(设备)坐标。

其中,(xWindow, yWindow) 是一个待转换的逻辑点坐标,(xViewport, yViewport)是转换后的设备坐标系坐标,大多数情况下是客户区坐标。

        这些公式使用两个点来分别指定窗口和视口的“原点”。点(xWinOrg, yWinOrg)是在逻辑坐标系下的窗口的原点;点(xViewOrg, yViewOrg)是在设备坐标系下视口的原点。在默认情况下,这两个点都设置为(0, 0),但是可以改变它们。公式表示逻辑点(xWinOrg, yWinOrg) 总是被映射到设备点(xViewOrg, yViewOrg)。如果窗口和视口的原点都停留在它们的默认值(0, 0),则公式可以简化如下:

这些公式还包含另外两个点来指定它们的“范围”。点(xWinExt, yWinExt)是在逻辑坐标系下的窗口范围;点(xViewExt, yViewExt)是在设备坐标系下的视口范围。在大多数的映射模式下,范围是由映射方式所隐含的,不能改变的。每个范围本身并没有多大意义,但是逻辑单位转换为设备单位的换算因子是视口范围和窗口范围的比例。

        例如,当设置使用 MM_LOENGLISH 映射模式时,Windows 将 xViewExt 设置为像素的个数,xWinExt 表示以百分之一英寸为单位被 xViewExt 个像素占据的长度。它们的比例给出了每百分之一英寸的像素数。为了提高转换性能,换算因子表示为整数的比例,而不是浮点数。

        范围也可以是负值。这暗含着逻辑 x 轴的值并不一定是向右增加,逻辑轴 y 的值也并不一定向下增加。

        Windows 也可以将视口(设备)坐标转换为窗口(逻辑)坐标:

        Windows 提供了两函数来让你的程序中设备点与逻辑点之间转换。下面的函数将设备点转换为逻辑点。

[cpp]  view plain  copy
  1. DPtoLP (hdc, nPoints, iNumber);  
其中,变量 pPoints 是一个指针,它指向一个 POINT 结构的数组,iNumber 是待转换的点的个数。例如,你会发现这个函数对于把从 GetClientRect 函数(它总以设备单位的形式)获取的客户区大小转换到逻辑坐标非常有用:

[cpp]  view plain  copy
  1. GetClientRect (hwnd, &rect);  
  2. DPtoLP (hdc, (PPOINT) &rect, 2);  
下面的函数把逻辑点转换为设备点:

[cpp]  view plain  copy
  1. LPtoP (hdc, nPoints, iNumber);  

5.5.4 使用 MM_TEXT

        在 MM_TEXT 映射模式下,默认的原点和范围显示如下:

        窗口原点:        (0, 0)         可以改变
        视口原点:        (0, 0)         可以改变
        窗口范围:        (1, 1)         不可以改变
        视口范围:        (1, 1)         不可以改变

        视口范围和窗口范围的比例是 1,因此在逻辑坐标和设备坐标之间没有执行缩放。前面给出的逻辑坐标到视口坐标的转换公式可以简化为一下形式:

这类被称为“文本”映射模式,并不是因为它对文本最适合,而是因为轴的方向与文本读写类似。在大多数语言中,我们读文本的顺序是从左到右,从上到下,MM_TEXT 以同样的方式定义轴上值的增长方向。

        Windows 提供 SetViewportOrgEx 函数和 SetWindowOrgEx 函数来改变视口原点和窗口原点。这些函数都具有移动轴的效果,这是,逻辑点(0, 0)不再对应左上角。一般来说,或者使用 SetViewportOrgEx 函数或者使用 SetWindowsOrgEx 函数,但不会同时使用它们。

        这里给出了函数的运行方式:如果想将视口原点改为(xViewOrg, yViewOrg),那么逻辑点(0, 0)将会被映射到设备点(xViewOrg, yViewOrg)。如果想将窗口原点改为(xWinOrg, yWinOrg),逻辑点(xWinOrg, yWinOrg)将会被映射到设备点(0, 0),即左上角。不管你怎么改变窗口和视口原点,设备点(0, 0)始终都位于客户区的左上角。

        例如,假定客户区宽为 cxClient 像素,高为 cyClient 像素。如果想定义逻辑点(0, 0)为客户区的中心,可以通过下面的调用实现:

[cpp]  view plain  copy
  1. SetViewportOrgEx (hdc, cxClient / 2, cyClient / 2, NULL);  
SetViewportOrgEx 函数的参数总是以设备单位的形式给出。这表示逻辑点(0, 0)将映射到设备点(cxClient/2, cyClient/2)。现在可以使用客户区了,其坐标系显示如下:

        逻辑 x 轴的范围是-cxClient/2 ~ +cxClient/2,逻辑 y 轴的范围是 -cyClient/2 ~ +cyClient/2。客户区的右下角的逻辑坐标为(cxClient/2, cyClient/2)。如果想在客户区的左上角,也就是坐标(0, 0)处开始显示文本,则需要使用负的坐标:

[cpp]  view plain  copy
  1. TextOut (hdc, -cxClient / 2, -cyClient / 2, "Hello", 5);  
可以调用 SetWindowOrgEx 函数获得与调用 SetViewportOrgEx 函数相同的结果:

[cpp]  view plain  copy
  1. SetWindowOrgEx (hdc, -cxClient / 2, -cyClient / 2, NULL);  
SetWindowOrgEx 的参数总是以逻辑单位的形式给出。调用该函数后,逻辑点(-cxClient/2, -cyClient/2)被映射到设备点(0, 0),也就是客户区的左上角。

        不推荐将这两个函数放在一起使用(除非你知道这么做的结果):

[cpp]  view plain  copy
  1. SetViewportOrgEx (hdc, cxClient / 2, cyClient / 2, NULL);  
  2. SetWindowOrgEx (hdc, -cxClient / 2, -cyClient / 2, NULL);  
这意味着逻辑点(-cxClient/2, -cyClient/2)被映射到设备点(cxClient/2, cyClient/2),坐标系统显示如下:

        可以通过调用下面两个函数获取当前视口和窗口的原点:

[cpp]  view plain  copy
  1. GetViewportOrgEx (hdc, &pt);  
  2. GetWindowOrgEx (hdc, &pt);  
这里的 pt 是一个 POINT 结构。从 GetViewportOrgEx 函数返回的值以设备坐标的形式给出;从 GetWindowOrgEx 函数返回的值则以逻辑坐标的形式给出。

        在客户区内移动显示输出时(例如,响应用户在滚动条内的输入),或许想改变视口或窗口的原点。在第 4 章的 SYSMETS2 程序中,我们使用 iVscrollPos 值(垂直滚动条的当前位置)来调整 y 坐标的显示输出:

[cpp]  view plain  copy
  1. case WM_PAINT :  
  2.          hdc = BeginPaint (hwnd, &ps) ;  
  3.   
  4.          for (i = 0 ; i < NUMLINES ; i++)  
  5.          {  
  6.               y = cyChar * (i - iVscrollPos) ;  
  7.                    [display text]  
  8.          }  
  9.          EndPaint (hwnd, &ps) ;  
  10.          return 0 ;  
调用 SetWindowOrgEx 函数可以获得同样的效果:

[cpp]  view plain  copy
  1.  case WM_PAINT :  
  2.           hdc = BeginPaint (hwnd, &ps) ;  
  3.   
  4.           SetWindowOrgEx(hdc, 0, cyChar * iVscrollPos, NULL);  
  5.   
  6.           for (i = 0 ; i < NUMLINES ; i++)  
  7.           {  
  8.                y = cyChar * i ;  
  9.                     [display text]  
  10.           }  
  11.           EndPaint (hwnd, &ps) ;  
  12.           return 0 ;  
现在,为 TextOut 函数计算 y 坐标并不需要使用 iVscrollPos 值。这意味着可以将文本的输出调用放到一个单独的函数中,而不必把 iVscrollPos 值传递给函数,因为显示是通过改变窗口原点来调整的。

        如果你有使用矩形(就是笛卡尔)坐标系统的经验,把逻辑点(0, 0)移动到客户区的中心(就像我们之前做的那样)似乎是一种合理的行为。然而,这样对于使用 MM_TEXT 映射模式会有点小问题。通常,一个笛卡尔坐标系统定义 y 轴上的值会随着你向上移动而增加,然而 MM_TEXT 定义的值会随着你向下移动而增加。从这个意义上,MM_TEXT 有点奇怪,而下面的 5 种映射能够正确处理这种情况。

5.5.5  度量映射模式

        Windows 包含 5 种映射模式,它们分别表示将逻辑坐标转换为物理坐标的不同方式。因为在 x 轴和 y 轴上的逻辑坐标都被映射到相同的物理度量单位,所以即使设备不具备正方形像素时,这些映射模式也可以帮助绘制出很“圆”的圆形和很“方”的方形。

        这 5 种映射模式按照从低到高精度如下表所示。为了对照,右边两栏分别是以英寸(in.)和毫米(mm)为单位显示的逻辑单位的大小。


        在默认情况下的窗口和视口的原点及范围如下:

        窗口原点:        (0, 0)         可以改变
        视口原点:        (0, 0)         可以改变
        窗口范围:        (?, ?)         不可以改变
        视口范围:        (?, ?)         不可以改变

这里的问号表示窗口和视口的范围取决于映射模式和设备分辨率。就如我前面提到的,范围本身并不重要,只有把他们表达为一个比值时才有意义。下面再次给出转换公式:

例如,在 MM_LOENGLISH 映射模式下,Windows 使用如下公式计算范围:

Windows 使用从 GetDeviceCaps 函数获得的可用信息来设置这些范围。不过这在 Windows 98 和 Windows NT 中有所不同。

        首先讨论它在 Windows 98 下是如何运作的。假定使用控制面板中的【显示】程序选择每英寸 96 点的系统字体。GetDeviceCaps 函数对于 LOGPIXELSX 和 LOGPIXELSY 参数的返回值是 96。Windows 对视口范围使用这些值,并设置视口和窗口的范围如下表所示。

这样,在 MM_LOENGLISH 映射模式下,96 除以 100 得到的比值就是 0.01 英寸内像素的个数。在 MM_LOMETRIC 映射模式下,96 除以 254 得到的比值就是 0.1mm 内像素的个数。

        Windows NT 使用一种不同的办法来设置视口和窗口的范围(这种方法实际上是一种与早期 16 位版本 Windows 一致的方法)。视口的范围是基于屏幕上像素的尺寸的。这个信息是使用 HORZRES 和 VERTRES 参数从 GetDeviceCaps 函数获得的。窗口的范围是基于假定的显示尺寸。正如前面提到的,这些值通常是 320mm 和 240mm。如果设置的显示器的像素规模是 1024 * 768,Windows NT 报告的视口和窗口范围的值便如下表所示。

这些窗口范围表示包含了显示器全部宽度和高度的逻辑单位的个数。一个 320mm 宽的屏幕在 MM_LOENGLISH 映射模式下的宽度为 1260 个单位或 12.6 英寸(320 除以 25.4 mm/in.)。

        y 前的这些符号表示轴方向的改变。对于这 5 种映射模式,y 值随着设备上移增加。然而,要注意在默认时窗口和视口原点都为(0, 0)。这里有一个很有意思的隐含意义。当首次改变为 5 种映射模式之一时,坐标系统如下图所示。

唯一能够在客户区显示的任何东西的方法就是使用负的 y 值。例如,下面的代码:

[cpp]  view plain  copy
  1. SetMapMode (hdc, MM_LOENGLISH);  
  2. TextOut (hdc, 100, -100, "Hello", 5);  
将文本显示在距离客户区左上角的右边和下边给一英寸的地方。

        为了保持头脑清醒,你可能想避免这样做。有一种解决办法是设置逻辑点(0, 0)为客户区的左下角。假定 cyClient 是按像素的形式表示客户区的高度,则可以通过调用 SetViewPortOrgEx 函数来完成:

[cpp]  view plain  copy
  1. SetViewportOrgEx (hdc, 0, cyClient, NULL);  
现在坐标系统如下图所示。

这是矩形坐标系统的右上角象限。

        还有一种选择是可以设置逻辑点(0, 0)为客户区的中心:

[cpp]  view plain  copy
  1. SetViewportOrgEx (hdc, cxClient / 2, cyClient / 2, NULL);  
坐标系统如下图所示。

现在,我们有了一个真实的四象限笛卡尔坐标系统,这个系统在 x 轴和 y 轴上无论以英寸、mm 或者 twips 为单位来度量都有这相同的逻辑单位。

        也可以调用 SetWindowOrgEx 函数来改变逻辑点(0, 0),但是这个任务有点困难,因为 SetWindowOrgEx 的参数必须是逻辑坐标的形式。因此你首先需要使用 DPtoLP 函数把(cxClient, cyClient) 转换为逻辑坐标。假定变量 pt 是类型为 POINT 的结构,下面的代码把逻辑坐标点(0, 0)改变为客户区的中心:

[cpp]  view plain  copy
  1. pt.x = cxClient;  
  2. pt.y = cyClient;  
  3. DPtoLP (hdc, &pt, 1);  
  4. SetWindowOrgEx (hdc, -pt.x / 2, -pt.y / 2, NULL);  

5.5.6  自定义的映射模式

        剩余的两种映射模式分别称为 MM_ISOTROPIC 和 MM_ANISOTRPOIC。只有在这两种映射模式下,Windows 才允许你改变视口和窗口的范围,也就意味着你能够改变 Windows 用于转换逻辑坐标和设备坐标的换算因子。isotropic 的意思是“各向同性”;anisotropic 是“各向异性”。和前面的度量映射模式类似,MM_ISOTROPIC 会同比例地缩放两个坐标轴,x 轴上的逻辑单位与 y 轴上的逻辑单位表示的物理尺寸时相同的。对于建立宽高比与显示设备无关的图像,这是有帮助的。

        MM_ISOTROPIC 和其他度量映射模式的区别是在使用 MM_ISOTROPIC 映射模式时,可以控制逻辑单位的物理尺寸。如果需要的话,可以依据客户区来调整逻辑单位的大小。这样会使你绘制的图像总是包含在客户区内,并相应的放大或者缩小。第 8 章中的两个时钟程序就是各向同性的图像。随着改变窗口的大小,时钟相应的调整。

        Windows 程序能够通过调整窗口和视口的范围来处理图像大小的变化。这样一来,程序代码就能够在绘图函数中使用相同的逻辑单位,而不用去管窗口的大小。

        有时候,MM_TEXT 映射模式和度量映射模式也被称为“完全受限”的映射模式。这意味着不能改变窗口和视口的范围,也不能改变 Windows 将逻辑坐标换算为设备坐标的方法。MM_ISOTROPIC 映射模式是一种“半受限”映射模式。Windows 允许改变窗口和视口的范围,但是 Windows 会调整它们的值,这是为了让 x 和 y逻辑单位表示相同的物理尺寸。MM_ANISOTROPIC 映射模式是“不受限”的,你可以改变窗口和视口的范围,并且 Windows 不会相应的调整它们的值。

      MM_ISOTROPIC 映射模式

        在两个轴上使用相同的逻辑单位时,MM_ISOTROPIC 映射模式对于要任意缩放刻度的轴是比较理想的。一个矩形如果有相同的逻辑宽度和高度,它就显示为一个正方形。一个由相同逻辑宽度和高度的椭圆就被显示为一个圆形。

        当第一次设置映射模式为 MM_ISOTROPIC 时,Windows 使用与 MM_LOMETRIC 映射模式相同的窗口和视口范围。(然而,在编程时请不要依赖这个事实。)有一个差别是现在可以根据自己的喜欢调用 SetWindowExtEx 和 SetViewportExtEx 函数来改变范围。接着 Windows 将调整范围以使得两个轴上的逻辑单位表示相同的物理距离。

        一般说来,调用 SetWindowExtEx 函数时,要把参数设定为期望得到的逻辑窗口的逻辑大小,而在调用 SetViewportExtEx 函数时,则把参数设定为客户区的实际高度和宽度。当 Windows 调整这些范围时,它必须让逻辑窗口可以容纳在对应的物理视口之内,这就有可能导致一部分的客户区落在逻辑窗口外面。应当在调用 SetViewportExtEx 之前先调用 SetWindowExtEx 来最有效地使用客户区的空间。

        例如,你想要一个传统的单象限的虚拟坐标系统,坐标系统的(0, 0)是在客户区的左下角,逻辑宽度和高度的范围为0 ~ 32 767。你希望 x 轴和y 轴的单位有着相同的物理尺寸。下面是需要作的工作:

[cpp]  view plain  copy
  1. SetMapMode       (hdc, MM_ISOTROPIC);  
  2. SetWindowExtEx   (hdc, 32767, 32767, NULL);  
  3. SetViewPortExtEx (hdc, cxClient, -cyClient, NULL);  
  4. SetViewportOrgEx (hdc, 0, cyClient, NULL);  
如果接着调用 GetWindowExtEx 和 GetViewportExtEx 获取窗口和视口的范围,你会发现它并不是你指定的值。Windows 已经根据显示设备的纵横比自动调整了范围值,这样就能够使逻辑单位在两个坐标轴上表示相同的物理尺寸。

        如果客户区的宽度比高度大(在物理尺寸上),Windows 就调整 x 的范围,这样得到逻辑窗口要逼客户区的视口窄。逻辑窗口位于客户区的左部,如下图所示。

Windows 98 实际上不允许在客户区的右部显示任何东西,因为它受限于 16 位带符号的坐标。Windows NT 由于使用一个全 32 位的坐标系统,因此,能够在超出客户区的右部显示一些东西。

        如果客户区的高度比宽度大(在物理尺寸上),Windows 会调整 y 的范围。逻辑窗口被放置在客户区的底部,如下图所示。

Windows 98 不会允许在客户区的顶部显示任何东西。

        如果喜欢让逻辑窗口总是位于客户区的左上角,可以如下修改代码:

[cpp]  view plain  copy
  1. SetMapMode       (hdc, MM_ISOTROPIC);  
  2. SetWindowExtEx   (hdc, 32767, 32767, NULL);  
  3. SetViewportExtEx (hdc, cxClient, -cyClient, NULL);  
  4. SetWindowOrgEx   (hdc, 0, 32767, NULL);  
在 SetWindowOrgEx 调用中,我们说明了想要将逻辑点(0, 32767)映射到设备点(0, 0)。现在,如果客户区的高度大于宽度,坐标安排将如下图所示。

对于时钟程序,或许想使用如下这样一个四象限笛卡尔坐标系统:四个方向的轴可以任意缩放,并且逻辑点(0, 0)位于客户区的中心。如果希望每个轴的范围是 0 到 1000(举个例子),可以使用下面的代码:

[cpp]  view plain  copy
  1. SetMapMode       (hdc, MM_ISOTROPIC);  
  2. SetWindowExtEx   (hdc, 1000, 1000, NULL);  
  3. SetViewportExtEx (hdc, cxClient / 2, -cyClient / 2, NULL);  
  4. SetWindowOrgEx   (hdc, cxClient / 2, cyClient / 2, NULL);  
如果客户区的宽度大于高度,那么逻辑坐标如下图所示。

如果客户区的高度大于宽度,逻辑坐标也会居中,如下图所示。

请记住,在窗口或视口的范围中,并没有实现裁剪功能。当调用 GDI 函数时,仍然可以随意地使用小于 -1000 或者大于 +1000 的逻辑 x 和 y 值。根据客户区的具体形状,这些点可能可见,也可能不可见。

        使用 MM_ISOTROPIC 映射模式时,能够使逻辑单位总数大于像素总数。例如,假定想要这样一种映射模式:点(0, 0)在显示区域的左上角,y 值随着向下移动增加(就像 MM_TEXT),但是逻辑坐标的单位是 1/16 英寸。下面是实现它的一种方法:

[cpp]  view plain  copy
  1. SetMapMode       (hdc, MM_ISOTROPIC);  
  2. SetWindowExtEx   (hdc, 16, 16, NULL);  
  3. SetViewportExtEx (hdc, GetDeviceCaps (hdc, LOGPIXELSX),   
  4.                        GetDeviceCaps(hdc, LOGPIXELSY), NULL);  
SetWindowExtEx 函数的参数表示每英寸里逻辑单位的个数。SetViewportExtEx 函数的参数表示每英寸里物理单位(像素)的个数。

        然而,这种方法与 Windows NT 中的映射模式并不一致。Windows NT 中的映射模式使用了显示器的像素大小和度量尺寸。为了与度量映射模式保持一直,可以使用下面的代码:

[cpp]  view plain  copy
  1. SetMapMode       (hdc, MM_ISOTROPIC);  
  2. SetWindowExtEx   (hdc, 160 * GetDeviceCaps(hdc, HORZSIZE) / 254,   
  3.                        160 * GetDeviceCaps(hdc, VERTSIZE) / 254, NULL);  
  4. SetViewportExtEx (hdc, GetDeviceCaps(hdc, HORZERES),   
  5.                        GetDeviceCaps(hdc, VERTRES), NULL);  
在这段代码中,视口范围被设置为整个屏幕的像素尺寸。窗口的范围被设置为假定屏幕以 1/16 英寸为单位来度量。GetDeviceCaps 使用 HORESIZE 和 VERTSIZE 参数返回以 mm 为单位的设备尺寸。如果我们使用浮点数来处理,那么通过除以 25.4 将 mm 转换为英寸,接着乘以 16 将英寸转换为 1/16 英寸。然而,因为我么你使用整数处理,所以必须是先乘以 160 然后除以 254。

        当然,这样的坐标系统使得逻辑单位比物理单位更大。在设备上绘制的任何东西都将映射为以 1/16 英寸为增量的坐标值。不能绘制间隔 1/32 英寸的两条水平线,因为这需要使用到小数的逻辑坐标。

      MM_ANISOTROPIC: 拉伸图像以满足需要

        当在 MM_ISOTROPIC 映射模式下设置视口和窗口范围时,Windows 会调整它们的值,这样两个轴上的逻辑单位具有相同的物理尺寸。在 MM_ANISOTROPIC 映射模式下,Windows 不对设置的视口和窗口范围做任何调整。这也就意味着 MM_ANISOTROPIC 并不需要保持正确的高宽比。

        使用 MM_ANISOTROPIC 的一种方式是在客户区使用任意的坐标方式,就像我们在 MM_ISOTROPIC 映射模式下所做的一样。下面的代码设置点(0, 0)在客户区左下角,x 轴和 y 轴的范围为 0 ~ 32 767。

[cpp]  view plain  copy
  1. SetMapMode       (hdc, MM_ANISOTROPIC);  
  2. SetWindowExtEx   (hdc, 32767, 32767, NULL);  
  3. SetViewportExtEx (hdc, cxClient, -cyClient, NULL);  
  4. SetViewportOrgEx (hdc, 0, cyClient, NULL);  
如果使用 MM_ISOTROPIC 映射模式,类似的代码会引起部分的客户区超出坐标轴的范围。而使用 MM_ANISOTROPIC,无论客户区的尺寸如何,点(32767, 32767)总是位于客户区的右上角。如果客户区不是正方形的,那么逻辑 x 和 y 单位会有不同的物理尺寸。

        在前面对 MM_ISOTROPIC 映射模式的介绍中,我讨论了如何在客户区绘制一个圆形时钟的图像,其 x 和 y 的范围都是从 -1000 到 +1000。可以使用 MM_ANISOTROPIC 做类似的事情:

[cpp]  view plain  copy
  1. SetMapMode       (hdc, MM_ANISOTROPIC);  
  2. SetWindowExtEx   (hdc, 1000, 1000, NULL);  
  3. SetViewportExtEx (hdc, cxClient / 2, -cyClient / 2, NULL);  
  4. SetWindowOrgEx   (hdc, cxClient / 2, cyClient / 2, NULL);  
使用 MM_ANISOTROPIC 映射模式的不同之处在于,这个时钟通常会是椭圆,而不是正圆形。

        使用 MM_ANISOTROPIC 的另外一种方式是把 x 和 y 单位设置为固定值,但是值并不相等。例如,如果有一个显示文本的程序,可能想根据单个字符的高度和宽度设定一种并不用很精确的坐标:

[cpp]  view plain  copy
  1. SetMapMode       (hdc, MM_ANISOTROPIC);  
  2. SetWindowExtEx   (hdc, 1, 1, NULL);  
  3. SetViewportExtEx (hdc, cxChar, cyChar, NULL);  
当然,我已经假定 cxChar 和 cyChar 是那种字体字符的宽度和高度。现在,可以用字符行和列的形式指定坐标。例如,下面的语句在距离左边 3 个字符、距离顶部 2 个字符处的客户区显示文本:

[cpp]  view plain  copy
  1. TextOut (hdc, 3, 2, TEXT ("Hello"), 5);  
如果使用等宽字体,这样做可能会更合适,就像随后的 WHATSIZE 程序所示的那样。

        第一次设置为 MM_ANISOTROPIC 映射模式时,它总是继承了前面所设定的映射方式的范围。这回非常方便。对 MM_ANISOTROPIC 的一种理解是它 “不锁定”范围,也就是说,它允许你改变其他“完全受限”映射模式的范围。例如,假定想使用 MM_LOENGLISH 映射模式,因为想让逻辑单位是 0.01 英寸。但是不想在往屏幕上方移动时,y 轴的值增加,而是像 MM_TEXT 映射模式那样,随着向下方移动,y 值增加。那么可以用下面的代码:

[cpp]  view plain  copy
  1. Size size;  
  2. [other program lines..]  
  3. SetMapMode       (hdc, MM_LOENGLISH);  
  4. SetMapMode       (hdc, MM_ANISOTROPIC);  
  5. GetViewportExtEx (hdc, &size);  
  6. SetViewportExtEx (hdc, size.cx, -size.cy, NULL);  
我们首先设置映射模式为 MM_LOENGLISH。接着我们通过把映射模式设置为 MM_ANISOTROPIC 来解放范围。然后调用 GetViewportExtEx 函数获取视口的范围,并把它放到一个 SIZE 结构里。最后,我们使用这个范围来调用 SetViewportExtEx 函数,但是要在 y 的范围前面加上负号。

5.5.7  WHATSIZE 程序

        先讲一点关于 Windows 的历史:第一篇介绍 Windows 编程的文章出现在 1986 年 12 月的 Microsoft Systems Journal(微软系统杂志)上。稳重的范例程序称为 WSZ(“what size”),它分别以像素、英寸和 mm 为单位显示一个客户区的大小。WHATSIZE 就是该程序的一个简化版本,该程序以5种度量映射显示窗口客户区的尺寸。

[cpp]  view plain  copy
  1. /*------------------------------------------------------------ 
  2.     WHATSIZE.C  --  What Size is the window ? 
  3.                     (c) Charles Petzold, 1998 
  4. ------------------------------------------------------------*/  
  5. #include <windows.h>  
  6.   
  7. LRESULT CALLBACK WndProc(HWNDUINTWPARAMLPARAM);  
  8.   
  9. int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,  
  10.                    PSTR szCmdLine, int iCmdShow)  
  11. {  
  12.     static TCHAR szAppName[] = TEXT ("WhatSize");  
  13.     HWND         hwnd;  
  14.     MSG          msg;  
  15.     WNDCLASS     wndclass;  
  16.   
  17.     wndclass.style          = CS_HREDRAW | CS_VREDRAW;  
  18.     wndclass.lpfnWndProc    = WndProc;  
  19.     wndclass.cbClsExtra     = 0;  
  20.     wndclass.cbWndExtra     = 0;  
  21.     wndclass.hInstance      = hInstance;  
  22.     wndclass.hIcon          = LoadIcon (NULL, IDI_APPLICATION);  
  23.     wndclass.hCursor        = LoadCursor (NULL, IDC_ARROW);  
  24.     wndclass.hbrBackground  = (HBRUSH) GetStockObject (WHITE_BRUSH);  
  25.     wndclass.lpszMenuName   = NULL;  
  26.     wndclass.lpszClassName  = szAppName;  
  27.   
  28.     if (!RegisterClass (&wndclass))  
  29.     {  
  30.         MessageBox (NULL, TEXT ("This program requires Windows NT!"),  
  31.                     szAppName, MB_ICONERROR);  
  32.         return 0;  
  33.     }  
  34.   
  35.     hwnd = CreateWindow (szAppName, TEXT("What Size is the window?"),  
  36.                          WS_OVERLAPPEDWINDOW,  
  37.                          CW_USEDEFAULT, CW_USEDEFAULT,  
  38.                          CW_USEDEFAULT, CW_USEDEFAULT,  
  39.                          NULL, NULL, hInstance, NULL);  
  40.     ShowWindow (hwnd, iCmdShow);  
  41.     UpdateWindow (hwnd);  
  42.   
  43.     while (GetMessage (&msg, NULL, 0, 0))  
  44.     {  
  45.         TranslateMessage (&msg);  
  46.         DispatchMessage (&msg);  
  47.     }  
  48.     return msg.wParam;  
  49. }  
  50. void Show (HWND hwnd, HDC hdc, int xText, int yText, int iMapMode,  
  51.            TCHAR * szMapMode)  
  52. {  
  53.     TCHAR szBuffer[60];  
  54.     RECT    rect;  
  55.   
  56.     SaveDC (hdc);  
  57.     SetMapMode (hdc, iMapMode);  
  58.     GetClientRect (hwnd, &rect);    // (left, top) (right, bottom)  
  59.     DPtoLP(hdc, (PPOINT) &rect, 2);  
  60.   
  61.     RestoreDC(hdc, -1);  
  62.   
  63.     TextOut (hdc, xText, yText, szBuffer,  
  64.              wsprintf(szBuffer, TEXT ("%-20s %7d %7d %7d %7d"), szMapMode,  
  65.                       rect.left, rect.right, rect.top, rect.bottom));  
  66.   
  67. }  
  68. LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)  
  69. {  
  70.    static TCHAR szHeading [] =  
  71.                 TEXT ("Mapping Mode            Left   Right     Top  Bottom") ;  
  72.     static TCHAR szUndLine [] =  
  73.                 TEXT ("------------            ----   -----     ---  ------") ;  
  74.     static int  cxChar, cyChar;  
  75.     HDC         hdc;  
  76.     PAINTSTRUCT ps;  
  77.     TEXTMETRIC  tm;  
  78.   
  79.     switch (message)  
  80.     {  
  81.     case WM_CREATE:  
  82.         hdc = GetDC (hwnd);  
  83.   
  84.         SelectObject(hdc, GetStockObject(SYSTEM_FIXED_FONT));  
  85.   
  86.         GetTextMetrics (hdc, &tm);  
  87.         cxChar = tm.tmAveCharWidth;  
  88.         cyChar = tm.tmHeight + tm.tmExternalLeading;  
  89.   
  90.         ReleaseDC(hwnd, hdc);  
  91.         return 0;  
  92.     case WM_PAINT:  
  93.         hdc = BeginPaint(hwnd, &ps);  
  94.   
  95.         SelectObject(hdc, GetStockObject(SYSTEM_FIXED_FONT));  
  96.         SetMapMode (hdc, MM_ANISOTROPIC);  
  97.         SetWindowExtEx(hdc, 1, 1, NULL);  
  98.         SetViewportExtEx(hdc, cxChar, cyChar, NULL);  
  99.   
  100.         TextOut(hdc, 1, 1, szHeading, lstrlen(szHeading));  
  101.         TextOut(hdc, 1, 2, szUndLine, lstrlen(szUndLine));  
  102.   
  103.         Show (hwnd, hdc, 1, 3, MM_TEXT, TEXT("TEXT (pixels)"));  
  104.         Show (hwnd, hdc, 1, 4, MM_LOMETRIC, TEXT("LOMETRIC (.1 mm)"));  
  105.         Show (hwnd, hdc, 1, 5, MM_HIMETRIC, TEXT("HIMETRIC (.01 mm)"));  
  106.         Show (hwnd, hdc, 1, 6, MM_LOENGLISH, TEXT("LOENGLISH (.01 in)"));  
  107.         Show (hwnd, hdc, 1, 7, MM_HIENGLISH, TEXT("HIENGLISH (.001 in)"));  
  108.         Show (hwnd, hdc, 1, 8, MM_TWIPS, TEXT("TWIPS (1/1440 in)"));  
  109.   
  110.         EndPaint(hwnd, &ps);  
  111.         return 0;  
  112.     case WM_DESTROY:  
  113.         PostQuitMessage(0);  
  114.         return 0;  
  115.     }  
  116.     return DefWindowProc(hwnd, message, wParam, lParam);  
  117. }  

        为了方便使用 TextOut 函数来显示信息,WHATSIZE 使用了等宽字体。下面的语句表示开始使用系统等宽字体(在 Windows 3.0 之前系统等宽字体是默认的字体):

[cpp]  view plain  copy
  1. SelectObject (hdc, GetStockObject (SYSTEM_FIXED_FONT));  
其中的两个函数与用于选择备用画笔和画刷的函数相同。如前所述,WHATSIZE 也使用 MM_ANISTROPIC 映射模式将轴上的逻辑单位设置为字号。

        当 WHATSIZE 需要为 6 种映射模式中的某一种获得客户区的大小时,它先保存当前设备环境,设置一个新的映射模式,然后获取客户区坐标,并将它们转换为逻辑坐标,接着在显示信息之前恢复成原来的映射模式。下面便是 WHATSIZE 的 Show 函数中的代码:

[cpp]  view plain  copy
  1. SaveDC (hdc);  
  2. SetMapMode (hdc, iMapMode);  
  3. GetClientRect (hwnd, &rect);  
  4. DPtoLP(hdc, (PPOINT) &rect, 2);  
  5. RestoreDC(hdc, -1);  

        图 5-25 显示了 WHATSIZE 的典型输出。

图 5-25  WHATSIZE 的典型输出结果

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值