上一节讲述设置绘图模式时,包括设置背景模式、混合模式,还有一个就是映射模式。本节我们将详细讲述映射模式。
本节必须掌握的知识点:
设备坐标和逻辑坐标
视口和窗口
MM_TEXT映射模式
度量映射模式
自定义映射模式
第27练:GDI映射模式
4.5.1 设备坐标和逻辑坐标
■ 8种映射模式
映射模式是设备环境HDC的一个设备属性,它能影响几乎所有在客户区绘制的图形。和映射模式紧密相关的还有4个其他的设备环境属性,分别为窗口原点(window origin)、视口原点(viewport origin)、窗口范围 (window extents)、视口范围(viewport extents)。
Windows定义了 8种映射模式。它们在WINGDI.H中定义的标识符如表4-3所示。
表4-3 Windows映射模式
单词METRIC(公制)和ENGLISH(英制)指的是两种比较通用的测量系统;LO和HI是 “低”(Low)和“高”(High),指的是精度的高低。“Twip”是一个杜撰的词,意思是“1/20 点”。在排版中,一个点是一个基本测量单位,大约为1/72英寸,但是在图形程序设计中,常假定它正好是1/72英寸。一个“Twip”是1/20点,因此,也就是 1/1440英寸。“isotropic”和“anisotropic”是真实的单词,意思分别是“各向同性”和“各向异性”。
■设置和获取映射模式
●设置映射模式:
SetMapMode (hdc, iMapMode);
其中,iMapMode是8种映射模式对应的标识符之一。
●获取当前映射模式:
iMapMode = GetMapMode (hdc);
在默认情况下,映射模式是MM_TEXT。在这种映射模式下,以像素为单位,逻辑坐标等同于物理坐标。在如下的TextOut调用方法中:
TextOut (hdc, 8, 16, TEXT ("Hello"), 5) ;
文本起始于距离客户区左上角向右8像素、向下16像素处。
如果是在MM_LOENGLISH映射模式下:
SetMapMode (hdc, MM_LOENGLISH);
现在一个逻辑单位等于百分之一英寸。调用如下的TextOut函数:
TextOut (hdc, 50, -100, TEXT ("Hello"), 5) ;
文本起始于距离客户区左上角向右0.5英寸,向下1英寸处。(在前面的调用中,y坐标前使用了一个负号,随着我们更详细地讨论映射模式,其原因会逐渐淸晰。)其他的映射模式坐标允许程序以毫米、点的大小或者任意单位的坐标轴的形式来表示。
如果感觉以像素为单位操作起来很方便,就没有必要使用默认MM_TEXT模式以外的任何映射模式。如果需要以英寸或者毫米为单位显示一幅图像,则可以从GetDeviceCaps 确数获取需要的信息,自己再进行缩放。MM_TEXT之外的映射模式都是为了避免你自己进行缩放工作而提供的一个方便的途径。
【注意】尽管在GDI函数中指定的坐标是32位的值,但是仅在Windows NT中才能够处理32 位。在Windows 98中,坐标限制为16位,这样它的范围就是-32768到32767。一些使用坐标作为矩形的起点和终点的Windows函数也需要满足这个条件,就是矩形的宽度和高度 必须是32767或者更少。
■设备坐标
设备坐标是指在特定的输出设备(例如屏幕或打印机)上的物理像素坐标。
在Windows图形编程中,使用设备坐标来进行绘图操作,例如画线、画圆、展示文本,等等。设备坐标通常以整数表示,并与硬件设备的实际像素点一一对应。
设备坐标系中的原点(0, 0)通常是在屏幕的左上角,横向(X轴)的坐标值向右增加,纵向(Y轴)的坐标值向下增加。例如,设备坐标(100, 100)代表从屏幕左上角向右和向下各移动100个像素的位置。
值得注意的是,设备坐标与你的实际分辨率有关,同一的设备坐标在不同的设备或设置上,显示出来的效果可能会有所不同。例如,如果你在一个高DPI(分辨率)设置的显示屏和低DPI设置的显示屏上使用相同的设备坐标,显示的内容的物理大小(例如以毫米或英寸为单位)可能会有区别,即所谓的DPI敏感或DPI无关问题。
如果我们使用MM_LOENGLISH映射模式,是否会得到以百分之一英寸来度量的WM_SIZE消息?绝对不是。Windows对所有消息(例如WM_MOVE、WM_SIZE和 WM_MOUSEMOVE),所有的非GDI函数,甚至一些GDI函数,都继续使用设备坐标。可以按这种方式考虑:映射模式是设备环境的一种属性,因此,只有当使用以设备环境句柄作为参数的GDI函数时,映射模式才会生效。
举例
1.GetSystemMetrics是一个非GDI函数,因此它将继续以设备单位的形式,也就是像素为单位返回。
2.尽管GetDeviceCaps是一个需要设备环境句柄的GDI函数,但是Windows继续为HORZRES和VERTRES索引返回设备单位,因为这个函数的目的之一就是以像素为单位向程序返回设备的尺寸。
3.然而,通过调用GetTextMetrics函数获取的TEXTMETRIC结构中的值是以逻辑单位形式给出的。如果调用这个函数时,映射模式是MM_LOENGLISH,GetTextMetrics函数就以百分之一英寸为单位来返回字符的宽度和高度。为了简化工作,当调用GetTextMetrics 函数获取字符的高度和宽度信息时,映射模式应当和基于这些尺寸绘制文本时使用的映射模式相同。
■逻辑坐标
在计算机图形学中,逻辑坐标(Logical Coordinate)是与特定设备无关的坐标系统。逻辑坐标不同于设备坐标,它独立于任何特定设备的硬件特性,如屏幕分辨率或打印机dpi等。
逻辑坐标通常用于定义和描述图形对象和它们的位置。因为逻辑坐标与任何特定设备无关,所以可以让编程人员在编写图形应用程序时,不需要关注特定屏幕或打印机的物理细节,而只需要关注逻辑坐标中图形对象的位置和关系。
逻辑坐标和设备坐标之间的转换,由映射模式(Mapping Mode)定义。例如,在Windows图形编程,一个常见的映射模式是MM_TEXT,它的特点就是逻辑坐标等于设备坐标。但在更复杂的图形处理中,可能需要用到其他映射模式(如MM_LOMETRIC、MM_HIMETRIC等),这些映射模式可以实现更复杂的坐标转换和比例缩放。
逻辑坐标到设备坐标的转换过程是通过保存在DC(Device Context设备上下文)的转换矩阵(Transformation Matrix)和映射模式(Mapping Mode)来完成的。
■设备坐标系统
Windows会把在GDI函数中指定的逻辑坐标转换为设备坐标。在我们讨论用于各种映 射模式的逻辑坐标系统之前,首先介绍一下Windows为视频显示器定义的不同的设备坐标 系统。尽管在大多数情况下我们都是在我们窗口的客户区内工作,但是在有些情况下, Windows还使用另外两种设备坐标系统。在所有的设备坐标系统中,单位都是以像素的形式表示的。水平方向上x值从左向右增加,垂直方向上值从上往下增加。
■屏幕坐标
当我们使用整个屏幕时,我们是以“屏幕坐标”(screen coordinate)的形式工作的。屏幕左上角是点(0,0)。屏幕坐标用于WM_MOVE消息(对非子窗口)和下列的Windows函数 中:CreateWindow 和 MoveWindows(对非子窗口)、GetMessagePos、GetCursorPos、 SetCursorPos、GetWindowRect 和 WindowFromPoint (这并不是一个完整的清单)。这些函 数一般分两类,一类是与窗口无关的函数(例如两个和鼠标指针相关的函数),还有一类是必须根据屏幕上的点移动或寻找窗口的函数。如果使用带“DISPLAY”参数的CreateDC 函数来获取整个屏幕的设备环境,那么在GDI调用中,逻辑坐标值将被默认映射屏幕坐标。
■窗口坐标
“全窗口”坐标指的是一个应用程序的整个应用窗口,包括标题栏、菜单、滚动条和边框。对一个普通的应用窗口,点(0,0)是边框的左上角。全窗口坐标在Windows中很少用, 但是如果设备环境是从GetWindowDC函数获取的,则在GDI函数调用中,逻辑坐标会被默认映射为全窗口坐标。
■客户区坐标”
客户区坐标系统也称为设备坐标系统(也是我们最常使用的坐标系统)。点(0,0)是客户区左上角。调用GetDC或BeginPaint函数获取设备环境时,在GDI函数中的逻辑坐标将被默认转换为客户区坐标。
可以使用ClientToScreen函数将客户区坐标转换到屏幕坐标,反之亦然,可以调用 ScreenToClient函数把屏幕坐标转换到客户区坐标。也可以调用GetWindowRect函数以屏幕坐标的形式获取整个窗口的位置和大小。这三个函数为把任何一种设备坐标转换为另一种设备坐标提供了足够的信息。
4.5.2 视口和窗口
在计算机图形学中,窗口(Window)和视口(Viewport)是常见的两个概念,它们在一些上下文中可能被混淆,但实际上是有区别的。
■窗口(Window)
在Windows操作系统中,我们常说的窗口就是一个区域,这个区域用于承载和展示应用程序的数据内容和用户界面。例如,你打开一个文档,文档的内容就是在一个窗口中显示的。通常在Windows程序中窗口指的是窗口客户区,而不是“全窗口”。
■视口(Viewport)
视口则是一个更为抽象的概念,它通常用于3D图形处理中。简单来说,我们可把视口理解为一个“相机”。这个“相机”决定了我们从哪个角度,看哪一部分三维世界的内容,并把这个视角的内容投影到2D屏幕上。
为了更好理解,可以做个比喻:比如你在一个房间里(这个房间就是窗口),里面有许多物品(这些物品就是你的3D图形内容,比如在游戏中的环境、角色等)。这时,你拿出了一个小洞相机(这个相机就是视口),只有通过这个小洞,你才能看到房间中的部分物体,并把看到的内容记录下来。视口决定了你看到的场景的大小、角度、距离等。
所以,窗口和视口的主要区别就在于,窗口是2D的,主要用来呈现应用的界面元素;而视口则是一个图形处理的概念,用来把3D图形内容投影到2D的窗口中。
■窗口(逻辑)坐标转换为视口(设备)坐标
映射模式定义了 Windows如何将GDI函数中指定的逻辑坐标映射到设备坐标。这里的 设备坐标系统取决于你获取设备环境所用函数。为了继续讨论映射模式,我们需要定义一些术语。映射模式被定义为从“窗口”(Window逻辑坐标)到“视口”(viewportK设备坐标)的映射。
使用这两个术语是很不准确的,因为它们在其他情况下还有其他意思。在其他的一些图形界面系统中,视口常常含有“剪切区域”的意思。在Windows中,“窗口”还有一个非常具体的意思,就是描述一个程序占用屏幕的区域。在本章的讨论中,我们必须先把对这些术语的陈见放到一边。
视口是以设备坐标(像素)的形式指定的。大多数情况下,视口与客户区相同,但是如 果从GetWindowDC或者CreateDC函数获取了设备环境,视口也可以是指全窗口坐标或屏 幕坐标。点(0,0)是客户区(或者全窗口,或者屏幕)的左上角。x值向右增加,y值向下增加。
窗口是以逻辑坐标的形式指定的,可能是像素、毫米、英寸或者其他你想用的任何单位。可以在GDI绘图函数中指定想用的逻辑窗口坐标。
但是在真实的意义上,视口和窗口都仅仅是数学的概念。对于所有的映射模式, Windows使用下面公式将窗口(逻辑)坐标转换为视口(设备)坐标。
其中,(xWindow,yWindow)是一个待转换的逻辑点坐标,(xViewport,yViewport)
是转换后的设备坐标点坐标,大多数情况下是客户区坐标。
这些公式使用两个点来分别指定窗口和视口的“原点”。点(xWindow,yWindow)是在逻辑坐标系下的窗口的原点:点(xViewport,yViewport)是在设备坐标系下视口的原点。在默认情况下,这两个点都设置为(0,0),但是可以改变它们。公式表示逻辑点总是被映射到设备点(xWindow,yWindow)。
●窗口和视口的原点都为(0,0)
如果窗口和视口的原点都停留在它们的默认值 (0,0)上,则公式可以简化如下:
●窗口范围和视口范围
这些公式还包含另外两个点来指定它们的“范围”:点(xWinExt,yWinExt)是在逻辑坐标系下的窗口范围;点(xViewExt,yViewExt)是在设备坐标系下的视口范围。在大多数的映射模式下,范围是由映射方式所隐含的,不能改变。每个范围本身并没有多大意义,但是逻辑单位转换为设备单位的换算因子是视口范围和窗口范围的比例。
例如,当设置使用MM_LOENGLISH映射模式时,Windows将xViewExt设置为像素的个数,xWinExt表示以百分之一英寸为单位被xViewExt个像素占据的长度。它们的比例给出了每百分一英寸的像素数。为了提高转换性能,换算因子表示为整数的比例,而不是浮点数。
范围也可以是负值。这暗含着逻辑x轴的值并不一定是向右增加,逻辑轴y的值也并不一定向下增加。
Windows也可以将视口(设备)坐标转换为窗口(逻辑)坐标:
Windows提供了两个函数来让你在程序中在设备点与逻辑点之间转换。下面的函数将 设备点转换为逻辑点:
DPtoLP (hdc, pPoints, iNumber);
其中,变量pPoints是一个指针,它指向一个POINT结构的数组,iNumber是待转换的点的个数。例如,你会发现这个函数对于把从GetClientRect函数(它总以设备单位的形式)获取的客户区大小转换到逻辑坐标非常有用。
下面的函数把逻辑点转换为设备点:
LPtoDP (hdc, pPoints, iNumber)
4.5.3 MM_TEXT映射模式
■MM_TEXT映射模式
在Windows的图形设备接口(GDI)编程中,映射模式(Mapping Mode)定义了物理坐标(像素)与逻辑坐标之间的转换方式。MM_TEXT是其中一种常见的映射模式。
在MM_TEXT映射模式下,逻辑坐标直接对应于设备坐标,即1单位的逻辑坐标恰好对应1个设备像素。如果在逻辑坐标(10,10)处绘制一个点,那么在设备上就是在(10,10)的像素点上进行绘制。它是最简单和最直观的映射方式,让程序员更方便地将绘制操作与屏幕像素对应起来。
但是需要注意的是,在MM_TEXT映射模式下,Y轴的正方向是向下的,这与我们常见的数学坐标系是不一样的。在这种模式下,如果你想在屏幕上向上移动,那么应将Y的逻辑坐标值减少,而不是增加。
此外,因为在此映射模式下逻辑和设备坐标是一一对应的,所以在不同的设备和不同的屏幕分辨率下,相同的逻辑坐标可能会导致实际显示结果的差异。例如,在高分辨率下,相同的逻辑尺寸可能会显得较小。因此,在需要精确控制图形大小以适应不同设备和分辨率的应用中,可能会选择使用其他的映射模式(如MM_LOMETRIC,MM_HIMETRIC等)。
■坐标转换
在MM_TEXT映射模式下,默认的原点和范围显示如下:
窗口原点: (0, 0) 可以改变
视口原点: (0, 0) 可以改变
窗口范围: (1, 1) 不可改变
视口范围: (1, 1) 不可改变
视口范围和窗口范围的比例是1,因此在逻辑坐标和设备坐标之间没有执行缩放。前面给出的从逻辑坐标到视口坐标的转换公式可以简化为以下形式:
MM_TEXT映射模式被称为“文本”映射模式,并不是因为它对文本最适合,而是因为轴的方向与文本读写 类似。在大多数语言中,我们读文本的顺序是从左到右,从上到下,MM_TEXT以同样的方式定义轴上值的增长方向。
图4-9 MM_TEXT映射模式坐标
■更改原点坐标
Windows提供SetViewportOrgEx函数和SetWindowOrgEx函数来改变视口原点和窗口 原点。这些函数都具有移动轴的效果,这时,逻辑点(0,0)不再对应左上角。一般来说,或者使用SetViewportOrgEx函数或者使用SetWindowsOrgEx函数,但不会同时使用它们。
这里给出了函数的运行方式:
如果想将视口原点改为(xViewOrg,yViewOrg),那么逻辑点(0,0)将会被映射到设备点(xViewOrg,yViewOrg)。如果想将窗口原点改为(xWinOrg,yWinOrg),逻辑点(xWinOrg,yWinOrg)仍将会被映射到设备点(0,0),即左上角。不管你怎么改变窗口和视口原点,设备点(0,0)始终都位于客户区的左上角。
●例如,假定客户区宽为cxClient像素,高为cyCHent像索。如果想定义逻辑点(0,0)为客户区的中心,可以通过下面的调用实现:
SetViewportOrgEx (hdc, cxClient / 2, cyClient / 2, NULL);
SetViewportOrgEx函数的参数总是以设备单位的形式给出。这表示逻辑点(0,0)将映射到设备点(cxClienl/2,cyClient/2)。现在可以使用客户区了,其坐标系显示如下:
图4-10 视口设备原点坐标在中心位置
逻辑x轴的范围是-cxClient/2 〜+cxClient/2,逻辑y轴的范围是-cyClient/2 〜+cyClient/2。 客户区的右下角的逻辑坐标为(cxClient/2,cyClient/2)。如果想在客户区的左上角,也就是设备坐标点(0,0)处开始显示文本,则需要使用负的坐标:
TextOut (hdc, -cxClient / 2, -cyClient / 2, "Hello", 5) ;
●可以调用SetWindowOrgEx函数获得与调用SetViewportOrgEx函数相同的结果:
SetWindowOrgEx (hdc, -cxClient / 2, -cyClient / 2, NULL) ;
SetWindowOrgEx的参数总是以逻辑单位的形式给出。调用该函数后,逻辑点
(-cxClient / 2,-cyClient / 2)被映射到设备点(0,0),也就是客户区的左上角。
【注意】不推荐将这两个函数放在一起使用(除非你知道这么做的结果):
SetViewportOrgEx (hdc, cxClient / 2, cyClient / 2, NULL);
SetWindowOrgEx (hdc, -cxClient / 2, -cyClient / 2, NULL);
这意味着逻辑点(-cxClient/2,-cyClient/2)被映射到设备点(cxClient/2,cyClient/2),坐标系统显示如下:
图4-11 同时改变窗口和视口原点坐标
可以通过调用下面两个函数获取当前视口和窗口的原点:
GetViewportOrgEx (hdc, &pt);
GetWindowOrgEx (hdc, &pt);
这里pt是一个POINT结构,从GetViewportOrgEx函数返回的值以设备坐标的形式给出; 从GetWindowOrgEx函数返回的值则以逻辑坐标的形式给出。
■移动窗口
在客户区内移动显示输出时(例如,响应用户在滚动条内的输入),或许想改变视口或窗口的原点。在第4章的SYSMETS2程序中,我们使用iVscrollPos值(垂直滚动条的当前位置)来调整y坐标的显示输出:
●方法一:
case WM_PAINT:
hdc = BeginPaint (hwnd, &ps) ;
for (i = 0 ; i < NUMLINES ; i++)
{
y = cyChar * (i - iVscrollPos) ;
// 显示文字
}
EndPaint (hwnd, &ps) ;
return 0 ;
调用SetWindowOrgEx函数可以获得同样的效果:
●方法二:
case WM_PAINT:
hdc = BeginPaint (hwnd, &ps) ;
SetWindowOrgEx (hdc, 0, cyChar * iVscrollPos) ;
for (i = 0 ; i < NUMLINES ; i++)
{
y = cyChar * i ;
// 显示文字
}
EndPaint (hwnd, &ps) ;
return 0 ;
现在,为TextOut函数计算y坐标并不需要使用iVscrollPos值。这意味着可以将文本的输出调用放到一个单独的函数中,而不必把iVscrollPos值传递给函数,因为显示是通过改变 窗口原点来调整的。
如果你有使用矩形(就是笛卡儿)坐标系统的经验,把逻辑点(0,0)移动到客户区的中心(就像我们之前做的那样)似乎是一种合理的行为。然而,这样对于使用MM_TEXT映射模式会有点小问题。通常,一个笛卡儿坐标系统定义y轴上的值会随着你向上移动而增加,然而MM_TEXT定义的值会随着你向下移动而增加。从这个意义上,MM_TEXT有点奇怪,而下面的5种映射能够正确处理这种情况。
4.5.4 度量映射模式
■ 5种度量映射模式
Windows包含5种映射模式,它们分别表示将逻辑坐标转换为物理坐标的不同方式。 因为在x轴和 y 轴上的逻辑坐标都被映射到相同的物理度量单位,所以即使设备不具备正方形像素时,这些映射模式也可以帮助绘制出很“圆”的圆形和很“方”的方形。
这5种映射模式按照从低精度到高精度如下表所示。为了对照,右边两栏分别是以英寸(in.)和毫米(mm)为单位显示的逻辑单位的大小。
映射模式 | 逻辑单位 | 英寸(in.) | 毫米(mm) |
MM_LOENGLISH | 0.01in. | 0.01 | 0.254 |
MM_LOMETRIC | 0.1mm | 0.00394 | 0.1 |
MM_HIENGLISH | 0.001in. | 0.001 | 0.0254 |
MM_TWIPS | 1/1400in. | 0.000694 | 0.0176 |
MM_HIMETRIC | 0.01mm | 0.000394 | 0.01 |
表4-4 物理单位映射模式
■坐标转换
在默认情况下的窗口和视口的原点及范围如下:
窗口原点: (0,0) 可以改变
视口原点: (0, 0) 可以改变
窗口范围: (?, ?) 不可改变
视口范围: (?,?) 不可改变
这里的问号表示窗口和视口的范围取决于映射模式和设备分辨率。就如前面提到的,范围本身并不重要,只有把它们表达为一个比值时才有意义。下面再次给出转换公式:
■窗口和视口范围
例如,在MM_LOENGLISH映射模式下,Windows使用如下公式计算范围:
Windows使用从GetDeviceCaps函数获得的可用信息来设置这些范围。不过这在Windows 98和Windows NT中有所不同。
●首先讨论它在Windows98下是如何运作的。假定使用控制面板中的【显示】程序选择 每英寸96点的系统字体。GetDeviceCaps函数对于LOGPIXELSX和LOGPIXELSY参数的返回值是96。Windows对视口范围使用这些值,并设置视口和窗口的范围如下表所示。
映射模式 | 视口范围(x,y) | 窗口范围(x,y) |
MM_LOMETRIC | (96,96) | (254,-254) |
MM_HIMETRIC | (96,96) | (2540,-2540) |
MM_LOENGLISH | (96,96) | (100,-100) |
MM_HIENGLISH | (96,96) | (1000,-1000) |
MM_TWIPS | (96,96) | (1440,-1440) |
表4-5 Windows 98不同映射模式的默认视口和窗口范围
这样,在MM_LOENGLISH映射模式下,96除以100得到的此值就是0.01英寸内像素的 个数。在MM_LOMETRIC映射模式下,96除以254得到的比值就是0.1mm内像素的个数。
●Windows NT使用一种不同的办法来设置视口和窗口的范围(这种方法实际上是一种与早期16位版本Windows—致的方法)。视口的范围是基于屏幕上像素的尺寸的。这个信息 是使用HORZRES和VERTRES参数从GetDeviceCaps函数获得的。窗口的范围是基于假 定的显示尺寸,该显示尺寸是在使用HORZSIZE和VERTSIZE参数时调用GetDeviceCaps 函数返回的。正如前面提到的,这些值通常是320mm和240mm。如果设置的显示器的像 素规模是1024X768, Windows NT报告的视口和窗口范围的值便如下表所示。
映射模式 | 视口范围(x,y) | 窗口范围(x,y) |
MM_LOMETRIC | (1024,-768) | (3200,2400) |
MM_HIMETRIC | (1024,-768) | (32000,24000) |
MM_LOENGLISH | (1024,-768) | (1260,945) |
MM_HIENGLISH | (1024,-768) | (12598,9449) |
MM_TWIPS | (1024,-768) | (18142,13606) |
表4-6 WinNT不同映射模式的默认视口和窗口范围
这些窗口范围表示包含了显示器全部宽度和高度的逻辑单位的个数。一个320mm宽的屏幕在MM_LOENGLISH映射模式下的宽度为1260个单位或12.6英寸(320除以25.4mm/in.)。
1.y前的这些符号表示轴方向的改变。对于这5种映射模式,y值随若设备上移增加。然而,要注意在默认时窗口和视口原点都为(0,0)。这里有一个很有意思的隐含意义。当首次改变为5种映射模式之一时,坐标系统如下图所示:
图4-12 度量映射模式下的坐标系统
如果想要在客户区显示任何东西的方法就是使用负的y值。例如,下面的代码:
SetMapMode (hdc, MM_LOENGLISH) ;
TextOut (hdc, 100, -100, "Hello", 5) ;
将文本显示在距离客户区左上角的右边和下边各一英寸的地方。
2.为了保持头脑清醒,你可能想避免这样做。有一种解决办法是设置逻辑点(0,0)为客户区的左下角。假定cyClient是按像素的形式表示的客户区的高度,则可以通过调用 SetViewportOrgEx 函数来完成:
SetViewportOrgEx (hdc, 0, cyClient, NULL);
现在坐标系统如下图所示。
图4-13 将原点坐标设为(0,cyClient)后的坐标系统
这是矩形坐标系统的右上角象限。
3.还有一种选择是可以设置逻辑点(0,0)为客户区的中心:
SetViewportOrgEx (hdc, cxClient / 2, cyClient / 2, NULL);
坐标系统如下图所示。
图4-14 将原点坐标设为(cxClient,cyClient)后的坐标系统
现在,我们有了一个真实的四象限笛卡儿坐标系统,这个系统在x轴和y轴上无论以英寸、 mm或者twips为单位来度量都有着相同的逻辑单位。
4.也可以调用SetWindowOrgEx函数来改变逻辑点(0,0),但是这个任务有点困难,因为 SetWindowOrgEx的参数必须是逻辑坐标的形式。因此你首先要使用DPtoLP函数把 (cxclient,cyclient)转换为逻辑坐标。假定变量pt是类型为POINT的结构,下面的代码把逻辑坐标点(0,0)改变为客户区的中心:
pt.x = cxClient;
pt.y = cyGlient;
DptoLP (hdc, &pt, 1);
SetWindowOrgEx (hdc, -pt.x / 2, -pc.y / 2, NULL);
4.5.5 自定义映射模式
前面的章节中我们已经讲述了6中映射模式,它们的共同点是:都不能改变窗口和视口的范围。接下来还有另外两种映射模式是可以改变窗口和视口范围的,可以对图形进行拉伸和缩放。
■MM_ISOTROPIC映射模式
MM_ISOTROPIC是Windows图形设备接口(GDI)中的一个映射模式,用于指定设备上下文(Device Context)的坐标系统。
在GDI中,坐标系统用于确定绘图操作的位置和尺寸。通过映射模式,可以将逻辑单位(例如点)映射到设备单位(例如像素),从而实现绘图的精确控制。
MM_ISOTROPIC映射模式是一种等比例缩放的映射模式,它将逻辑单位的水平和垂直方向的比例相同地映射到设备单位的水平和垂直方向。这意味着在MM_ISOTROPIC模式下,绘图操作可以在水平和垂直方向上以相同的比例进行缩放,保持图形的纵横比。
使用MM_ISOTROPIC映射模式时,需要调用SetMapMode函数来设置设备上下文的映射模式。以下是使用MM_ISOTROPIC映射模式的示例代码:
HDC hdc = GetDC(hwnd); // 获取窗口的设备上下文
SetMapMode(hdc, MM_ISOTROPIC); // 设置映射模式为MM_ISOTROPIC
// 设置逻辑单位的范围(左、上、右、下)
SetWindowExtEx(hdc, 0, 100, 0, 100);
// 设置设备单位的范围(左、上、右、下)
SetViewportExtEx(hdc, 0, height, 0, width);
// 绘制操作,使用逻辑单位进行绘图
ReleaseDC(hwnd, hdc); // 释放设备上下文
上述代码中,通过调用SetMapMode函数并传递MM_ISOTROPIC参数,将设备上下文的映射模式设置为MM_ISOTROPIC。然后,通过SetWindowExtEx函数和SetViewportExtEx函数分别设置逻辑单位和设备单位的范围。
需要注意的是,MM_ISOTROPIC模式下,逻辑单位的范围和设备单位的范围应该保持相同的纵横比,以确保绘制操作的等比例缩放。
■MM_ANISOTROPIC映射模式
MM_ANISOTROPIC映射模式是一种非等比例缩放的映射模式,它允许将逻辑单位的水平和垂直方向分别缩放到不同的比例上。这意味着在MM_ANISOTROPIC模式下,绘图操作可以在水平和垂直方向上以不同的比例进行缩放。
使用MM_ANISOTROPIC映射模式时,需要调用SetMapMode函数来设置设备上下文的映射模式,并使用SetWindowExtEx函数和SetViewportExtEx函数来设置逻辑单位和设备单位的范围。
以下是使用MM_ANISOTROPIC映射模式的示例代码:
HDC hdc = GetDC(hwnd); // 获取窗口的设备上下文
SetMapMode(hdc, MM_ANISOTROPIC); // 设置映射模式为MM_ANISOTROPIC
// 设置逻辑单位的范围(左、上、右、下)
SetWindowExtEx(hdc, -100, -100, 100, 100);
// 设置设备单位的范围(左、上、右、下)
SetViewportExtEx(hdc, 0, height, 0, width);
// 绘制操作,使用逻辑单位进行绘图
ReleaseDC(hwnd, hdc); // 释放设备上下文
上述代码中,通过调用SetMapMode函数并传递MM_ANISOTROPIC参数,将设备上下文的映射模式设置为MM_ANISOTROPIC。然后,通过SetWindowExtEx函数和SetViewportExtEx函数分别设置逻辑单位和设备单位的范围。
需要注意的是,在MM_ANISOTROPIC模式下,逻辑单位和设备单位的范围可以设置为不同的值,从而实现不同的缩放比例。
■MM_ISOTROPIC应用
一般说来,调用SetWindowExtEx函数时,要把参数设定为期望得到的逻辑窗口的逻辑大小,而在调用SetViewportExtEx函数时,则把参数设定为客户区的实际高度和宽度。当Windows调整这些范围时,它必须让逻辑窗口可以容纳在对应的物理视口之内,这就有可能导致一部分的客户区落在逻辑窗口外面。应当在调用SetViewportExtEx之前先调用 SetWindowExtEx来最有效地使用客户区的空间。
例如,你想要一个传统的单象限的虚拟坐标系统,坐标系统的(0,0)是在客户区的左下角,逻辑宽度和高度的范围为0〜32 767。你希望x轴和y轴的单位有着相同的物理尺寸。下面是需要做的工作:
SetMapMode (hdc, MM_IS0TR0PXC);
SetWindowExtEx (hdc, 32767, 32767, NULL);
SetViewportExtEx (hdc, cxClient, -cyClient, NULL);
SetViewportOrgEx (hdc, 0, cyClient, NULL);
如果接着调用GetWindowExtEx和GetViewportExtEx获取窗口和视口的范围,你会发现它 们并不是你指定的值。Windows已经根据显示设备的纵横比自动调整了范围值,这样就能够使逻辑单位在两个坐标轴上表示相同的物理尺寸。
如果客户区的宽度比高度大(在物理尺寸上)Windows就调整x的范围,这样得到的逻辑窗口要比客户区的视口窄。逻辑窗口位于客户区的左部,如下图所示。
Windows 98实际上不允许在客户区的右部显示任何东西,因为它受限于16位带符号的坐标。WindowsNT由于使用一个全32位的坐标系统,因此,能够在超出客户区的右部显示一些东西。
如果客户区的高度比宽度大(在物理尺寸上),Windows会调整的范围。逻辑窗口被放置在客户区的底部,如下图所示。
Windows 98不会允许在客户区的顶部显示任何东西。
如果喜欢让逻辑窗口总是位于客户区的左上角,可以如下修改代码:
SetMapMode (MM_ISOTROPIC) ;
SetWindowExtEx (hdc, 32767, 32767, NULL) ;
SetViewportExtEx (hdc, cxClient, -cyClient, NULL) ;
SetWindowOrgEx (hdc, 0, 32767, NULL) ;
在SetWindowOrgEx调用中,我们说明了想要将逻辑点(0,32 767)映射到设备点(0,0).现在, 如果客户区的高度大于宽度,坐标的安排将如下图所示。
对于时钟程序,或许想使用如下这样一个四象限笛卡儿坐标系统:四个方向的轴可以任意缩放,并且逻辑点(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) ;
如果客户区的宽度大于高度,那么逻辑坐标如下图所示。
如果客户区的高度大于宽度,逻辑坐标也会居中,如下图所示。
请记住,在窗口或视口的范围中,并没有实现剪裁功能。当调用GDI函数时,仍然可以随 意地使用小于-1000或者大于+1000的逻辑X和Y值。根据客户区的具体形状,这些点可能可见,也可能不可见。
使用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函数的参数 表示每英寸里物理单位(像素)的个数。
然而,这种方法与WindowsNT中的映射模式并不一致。WindowsNT中的映射模式使 用了显示器的像素大小和度量尺寸。为了与度量映射模式保持一致,可以使用下面的代码:
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使用HORZRESA 和VERTRES参数返回以 mm为单位的设备尺寸。如果我们使用浮点数来处理,那么通过除以25.4将mm转换为英 寸,接着乘以16将英寸转换为1/6英寸。然而,因为我们使用整数处理,所以必须是先乘以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。
SetMapMode (hdc, MM_ANISOTROPIC) ;
SetWindowExtEx (hdc, 32767, 32767, NULL) ;
SetViewportExtEx (hdc, cxClient, -cyClient, NULL) ;
SetViewportOrgEx (hdc, 0, cyClient, NULL) ;
如果使用MM_ISOTROPIC映射模式,类似的代码会引起部分的客户区超出坐标轴的范围。 而使用MM_ANISOTROPIC,无论客户区的尺寸如何,点(32 767,32 767)总是位于客户区 的右上角。如果客户区不是正方形的,那么逻辑x和y单位会有不同的物理尺寸。
在前面对MM_ISOTROPIC映射模式的介绍中,我讨论了如何在客户区绘制一个圆形时钟的图像,其x和y轴的范围都是从-1000到+1000。可以使用MM_ANISOTROPIC做类似的事情:
SetMapMode (hdc, MM_ANISOTROPIC) ;
SetWindowExtEx (hdc, 1000, 1000, NULL) ;
SetViewportExtEx (hdc, cxClient / 2, -cyClient / 2, NULL) ;
SetViewportOrgEx (hdc, cxClient / 2, cyClient / 2, NULL) ;
使用MM_ANISOTROPIC映射模式的不同之处在于,这个时钟通常会是椭圆,而不是正圆形。
使用MM_ANISOTROPIC的另外一种方式是把x和y单位设置为固定值,但是值并不相等。例如,如果有一个显示文本的程序,可能想根据单个字符的高度和宽度设定一种并不用很精确的坐标:
SetMapMode (hdc, MM_ANISOTROPIC) ;
SetWindowExtEx (hdc, 1, 1, NULL) ;
SetViewportExtEx (hdc, cxChar, cyChar, NULL) ;
当然,我们已经假定cxChar和cyChar是那种字体字符的宽度和高度。现在,可以用字符行 和列的形式指定坐标。例如,下面的语句在距离左边3个字符、距离顶部2个字符处的客 户区显示文本:
TextOut (hdc, 3, 2, TEXT ("Hello"), 5) ;
如果使用等宽字体,这样做可能会更合适,就像随后的WHATSIZE程序所示的那样。
第一次设置为MM_ANISOTROPIC映射模式时,它总是继承了前面所设定的映射方式的范围。这会非常方便。对MM_ANISOTROPIC的一种理解是它“不锁定”范围,也就是 说,它允许你改变其他“完全受限”映射模式的范围。例如,假定想使用MM_LOENGLISH 映射模式,因为想让逻辑单位是0.01英寸。但是不想在往屏幕上方移动时,y轴的值增加,而是像MM_TEXT映射模式那样,随着向下方移动,y 值增加。那么可以用下面的代码:
SIZE size ;
其他行程式
SetMapMode (hdc, MM_LOENGLISH) ;
SetMapMode (hdc, MM_ANISOTROPIC) ;
GetViewportExtEx (hdc, &size) ;
SetViewportExtEx (hdc, size.cx, -size.cy, NULL) ;
我们首先设置映射模式为MM_LOENGLISH。接着我们通过把映射模式设置为 MM_ANISOTROPIC来解放范围。然后调用GetViewportExtEx函数获取视口的范围,并把它放到一个SIZE结构里。最后,我们使用这个范围来调用SetViewportExtEx函数,但是要在 y的范围前面加上负号。接下来我们通过实例来验证。
4.5.6 第27练:GDI映射模式
/*------------------------------------------------------------------
027 WIN32 API 每日一练
第27个例子WHATSIZE.C:GDI映射模式
SetMapMode函数
SetWindowExtEx函数
SetViewportExtEx函数
MM_ANISOTROPIC映射模式
MM_ISOTROPIC映射模式
MM_HIENGLISH
MM_HIMETRIC
MM_LOENGLISH
MM_LOMETRIC
MM_TEXT
MM_TWIPS
(c) www.bcdaren.com, 2020
----------------------------------------------------------------*/
#include <windows.h>
LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ;
int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
PSTR szCmdLine, int iCmdShow)
{
static TCHAR szAppName[] = TEXT ("WhatSize.C") ;
…(略)
return msg.wParam ;
}
//显示窗口客户区的尺寸
void Show(HWND hwnd,HDC hdc,int xText,int yText,int iMapMode,TCHAR *szMapMode)
{
TCHAR szBuffer[60];
RECT rect;
SaveDC(hdc);//保存当前设备环境句柄
SetMapMode(hdc,iMapMode);//指定新的映射模式
GetClientRect(hwnd,&rect);//获取窗口客户区的大小,---使用当前HDC中映射模式的度量单位
DPtoLP(hdc,(PPOINT)&rect,2);//设备坐标转换成逻辑坐标
RestoreDC(hdc,-1);//恢复原来设备环境,-1 恢复最近保存的状态
TextOut(hdc,xText,yText,szBuffer,
wsprintf(szBuffer, TEXT("%-20s %7d %7d %7d %7d"), szMapMode,
rect.left, rect.right, rect.top, rect.bottom));
}
//窗口过程
LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
static TCHAR szHeading[]=TEXT ("Mapping Mode Left Right Top Bottom") ;
static TCHAR szUndLine[] =
TEXT("------------------------ ---- ----- --- ------");
static int cxChar,cyChar;//字符的宽和高
HDC hdc ;
PAINTSTRUCT ps ;
TEXTMETRIC tm;
switch (message)
{
case WM_CREATE:
hdc = GetDC(hwnd);
//选入字体
SelectObject(hdc,GetStockObject(SYSTEM_FIXED_FONT));//等宽字体
//获取字体参数
GetTextMetrics(hdc,&tm);
cxChar = tm.tmAveCharWidth;//字符平均宽度
cyChar = tm.tmHeight + tm.tmExternalLeading;//总高度
ReleaseDC(hwnd,hdc);
return 0 ;
case WM_PAINT :
hdc = BeginPaint(hwnd,&ps);
//选入字体
SelectObject(hdc,GetStockObject(SYSTEM_FIXED_FONT));
//指定新的映射模式,逻辑单位被映射到具有任意缩放轴的任意单位
SetMapMode(hdc,MM_ANISOTROPIC);//拉伸图像以满足需要
//指定新的映射模式,逻辑单位被映射到具有同等比例轴的任意单位
//SetMapMode(hdc, MM_ISOTROPIC);//测试
//SetWindowExtEx和SetViewportExtEx决定了窗口和视口之间的缩放因子
//MM_ANISOTROPIC和MM_ISOTROPIC两种映射模式需要设置窗口和视口设备范围
//设置窗口设备上下文的水平和垂直范围,该窗口是指页面空间的逻辑坐标系
SetWindowExtEx(hdc,1,1,NULL);//x轴=1个像素,y轴=1个像素
//设置视口设备上下文的水平和垂直范围,x轴=1个字符宽,y轴=1个字符高
SetViewportExtEx(hdc,cxChar,cyChar,NULL);
//该映射模式显示范围不受限制,可以任意拉伸以满足需求
TextOut(hdc,1,1,szHeading,lstrlen(szHeading));
TextOut(hdc,1,2,szUndLine,lstrlen(szUndLine));
//在距离左边1个字符、距离顶部3个字符处的客户区显示文本
//每个逻辑单元映射到一个设备像素
Show(hwnd, hdc, 1, 3, MM_TEXT, TEXT("TEXT (pixels)"));
//每个逻辑单位映射到 0.1 毫米
Show(hwnd, hdc, 1, 4, MM_LOMETRIC, TEXT("LOMETRIC (.1 mm)"));
//每个逻辑单位映射到 0.01 毫米
Show(hwnd, hdc, 1, 5, MM_HIMETRIC, TEXT("HIMETRIC (.01 mm)"));
//每个逻辑单元都映射到 0.1 英寸
Show(hwnd, hdc, 1, 6, MM_LOENGLISH, TEXT("LOENGLISH (.01 in)"));
//每个逻辑单元都映射到 0.01 英寸
Show(hwnd, hdc, 1, 7, MM_HIENGLISH, TEXT("HIENGLISH (.001 in)"));
//逻辑缇,每个逻辑单位都映射到打印机点的二十分之一
Show(hwnd, hdc, 1, 8, MM_TWIPS, TEXT("TWIPS (1/1440 in)"));
EndPaint(hwnd,&ps);
return 0;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hwnd, message, wParam, lParam);
}
/***************************************************************************
SetMapMode函数:指定设备上下文的映射模式
int SetMapMode(
HDC hdc,
int iMode //新的映射模式,供8种
);
*****************************************************************************
SetWindowExtEx函数:使用指定的值设置窗口设备上下文的水平和垂直范围。
BOOL SetWindowExtEx(
HDC hdc,
int x,//窗口的水平范围,以逻辑单位表示。
int y,//窗口的垂直范围,以逻辑单位表示。
LPSIZE lpsz//指向SIZE结构的指针,该结构以逻辑单位接收先前的窗口范围
);
*****************************************************************************
SetViewportExtEx函数:使用指定的值设置为视口设备上下文的水平和垂直范围。
BOOL SetViewportExtEx(
HDC hdc,
int x,
int y,
LPSIZE lpsz
);
*****************************************************************************
SaveDC函数:用于保存设备上下文(Device Context)的当前状态,
并创建一个新的状态,以便在之后的绘图操作中进行修改,
然后可以通过RestoreDC函数将设备上下文恢复到之前保存的状态。
int SaveDC(
HDC hdc //目标设备上下文的句柄
);
RestoreDC函数:用于恢复设备上下文(Device Context)到之前保存的状态。
BOOL RestoreDC(
HDC hdc, //目标设备上下文的句柄
int savedDC //之前使用SaveDC函数保存的状态的索引。
);
*****************************************************************************
DPtoLP函数:用于将设备相关的点(Device-Dependent Points,DP)转换为逻辑点(Logical Points,LP)。
BOOL DPtoLP(
HDC hdc, //目标设备上下文的句柄
LPPOINT lpPoints,//指向用于输入设备坐标和输出逻辑坐标数组的指针
int nCount//指定要转换的坐标点的数量
);
DPtoLP函数对于在不同分辨率或缩放模式下进行坐标转换非常有用,以便正确地在不同设备上绘制图形或进行布局。
*****************************************************************************
GetClientRect函数:用于获取指定窗口客户区域的坐标。
BOOL GetClientRect(
HWND hWnd, //指向目标窗口的句柄
LPRECT lpRect //指向RECT结构的指针,用于接收客户区域的坐标信息。
);
该函数返回一个布尔值,表示是否成功获取客户区域的坐标信息。如果函数调用成功,lpRect将包含客户区域的坐标信息。
*****************************************************************************
MM_ISOTROPIC映射模式:等比拉伸图像以满足需要。
MM_ANISOTROPIC映射模式:不等比拉伸图像以满足需要。
映射模式是“不受限”的,可以改变窗口和视口的范围,并且 Windows不会相应的调整它们的值。
*/
运行结果:
图4-15 GDI MM_ANISOTROPIC映射模式
总结
1.上述实例中,在窗口客户区显示六种映射模式的一些参数。有意思的是显示文本的方式。
//设置窗口设备上下文的水平和垂直范围,该窗口是指页面空间的逻辑坐标系
SetWindowExtEx(hdc,1,1,NULL);//x轴=1个像素,y轴=1个像素
//设置视口设备上下文的水平和垂直范围,x轴=1个字符宽,y轴=1个字符高
SetViewportExtEx(hdc,cxChar,cyChar,NULL);
//该映射模式显示范围不受限制,可以任意拉伸以满足需求
TextOut(hdc,1,1,szHeading,lstrlen(szHeading));
TextOut(hdc,1,2,szUndLine,lstrlen(szUndLine));
上述TextOut函数输出的文本字符的窗口坐标 (1,1)以像素为单位,而在视口设备输出文本的范围是(cxChar,cyChar)一个字符的宽和高。如果按照这种方式,无论如何都是没有办法正常显示出完整的文本字符串。
解决的方案是将映射模式改为不等比拉伸映射模式MM_ANISOTROPIC,将一个1*1像素拉伸为cxChar*cyChar。
2.动手实验:将映射模式改为等比例拉伸映射模式MM_ISOTROPIC,测试一下显示效果。