五、GDI映像方式
到目前为止,所有的程序都是相对于显示区域的左上角,以图素为单位绘图的。这是内定情况,但不是唯一选择。事实上,「映像方式」是一种几乎影响任何显示区域绘图的设备内容属性。另外有四种设备内容属性-窗口原点、视端口原点、窗口范围和视端口范围-与映像方式密切相关。
大多数GDI绘图函数需要坐标值或大小。例如,下面是TextOut函数:
参数x和y分别表示文字的开始位置。参数x是在水平轴上的位置,参数y是在垂直轴上的位置,通常用(x,y)来表示这个点。
在TextOut中,以及在几乎所有GDI函数中,这些坐标值使用的都是一种「逻辑单位」。Windows必须将逻辑单位转换为「设备单位」,即图素。这种转换是由映像方式、窗口和视端口的原点以及窗口和视端口的范围所控制的。映像方式还指示着x轴和y轴的方向(orientation);也就是说,它确定了当您在向显示器的左或者右移动时x的值是增大还是减小,以及在上下移动时y的值是增大还是减小。
Windows定义了8种映像方式,它们在WINGDI.H中相应的标识符和含义如表5-5所示。
METRIC和ENGLISH指一般通行的度量衡系统,点是印刷的测量单位,约等于1/72英寸,但在图形程序设计中假定为正好1/72英寸。「Twip」等于1/20点,也就是1/1440英寸。「Isotropic」和「anisotropic」是真正的单字,意思是「等方性」(同方向)和「异方性」(不同方向)。
您可以使用下面的叙述来设定映射方式:
其中,iMapMode是8个映像方式标识符之一。您可以通过以下呼叫取得目前的映像方式:
内定映像方式为MM_TEXT。在这种映像方式下,逻辑单位与实际单位相同,这样我们可以直接以图素为单位进行操作。在TextOut呼叫中,它看起来像这样:
文字从距离显示区域左端8图素、上端16图素的位置处开始。
如果映像方式设定为MM_LOENGLISH:
则逻辑单位是百分之一。现在,TextOut呼叫如下:
文字从距离显示区域左端0.5英寸、上端1英寸的位置处开始。至于y坐标前面的负号,随着我们对映像方式更详细的讨论,将逐渐清楚。其它映像方式允许程序按照毫米、打印机的点大小或者任意单位的坐标轴来指定坐标。
如果您认为使用图素进行工作很合适,那么就不要使用内定的MM_TEXT方式外的任何映像方式。如果需要以英寸或者毫米尺寸显示图像,那么可以从GetDeviceCaps中取得所需要的信息,自己再进行缩放。其它映像方式都是避免您自己进行缩放的一个方便途径而已。
虽然您在GDI函数中指定的坐标是32位的值,但是仅有Windows NT能够处理全32位。在Windows 98中,坐标被限制为16位,范围从-32,768到32,767。一些使用坐标表示矩形的开始点和结束点的Windows函数也要求矩形的宽和高小于或者等于32,767。
设备坐标和逻辑坐标
您也许会问:如果使用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以取得关于字符的宽度和高度信息时,映像方式必须设定成根据这些信息输出文字时所使用的映像方式,这样就可以简化工作。
设备坐标系
Windows将GDI函数中指定的逻辑坐标映像为设备坐标。在讨论以各种不同的映像方式使用逻辑坐标系之前,我们先来看一下Windows为视讯显示器区域定义的不同的设备坐标系。尽管我们大多数时间在窗口的显示区域内工作,但Windows在不同的时间使用另外两种设备坐标区域。所有设备坐标系都以图素为单位,水平轴(即x轴)上的值从左到右递增,垂直轴(即y轴)上的值从上到下递增。
当我们使用整个屏幕时,就根据「屏幕坐标」进行操作。屏幕的左上角为(0,0)点,屏幕坐标用在WM_MOVE消息(对于非子窗口)以及下列Windows函数中:CreateWindow和MoveWindow(都是对于非子窗口)、GetMessagePos、GetCursorPos、SetCursorPos、GetWindowRect以及WindowFromPoint(这不是全部函数的列表)。它们或者是与窗口无关的函数(如两个光标函数),或者是必须相对于某个屏幕点来移动(或者寻找)窗口的函数。如果以DISPLAY为参数呼叫CreateDC,以取得整个屏幕的设备内容,则内定情况下GDI呼叫中指定的逻辑坐标将被映像为屏幕坐标。
「全窗口坐标」以程序的整个窗口为基准,如标题列、菜单、滚动条和窗口框都包括在内。而对于普通窗口,点(0,0)是缩放边框的左上角。全窗口坐标在Windows中极少使用,但是如果用GetWindowDC取得设备内容,GDI函数中的逻辑坐标就会转换为显示区域坐标。
第三种坐标系是我们最常使用的「显示区域坐标系」。点(0,0)是显示区域的左上角。当使用GetDC或BeginPaint取得设备内容时,GDI函数中的逻辑坐标就会内定转换为显示区域坐标。
用函数ClientToScreen和ScreenToClient可以将显示区域坐标转换为屏幕坐标,或者反过来,将屏幕坐标转换为显示区域坐标。也可以使用GetWindowRect函数取得屏幕坐标下的整个窗口的位置和大小。这三个函数为一种设备坐标转换为另一种提供了足够的信息。
视端口和窗口
映像方式定义了Windows如何将GDI函数中指定的逻辑坐标映像为设备坐标,这里的设备坐标系取决于您用哪个函数来取得设备内容。要继续讨论映像方式,我们需要一些术语:映像方式用于定义从「窗口」(逻辑坐标)到「视端口」(设备坐标)的映像。
「窗口」和「视端口」这两个词用得并不恰当。在其它图形接口语言中,视端口通常包含有剪裁区域的意思,并且,我们已经用窗口来指程序在屏幕上占据的区域。在这里的讨论中,我们必须把关于这些词的先入之见丢到一边。
「视端口」是依据设备坐标(图素)的。通常,视端口和显示区域相同,但是,如果您已经用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提供了两个函数来让您将设备点转换为逻辑点以及将逻辑点转换为设备点。下面的函数将设备点转换为逻辑点:
其中,pPoints是一个指向POINT结构数组的指针,而iNumber是要转换的点的个数。您会发现这个函数对于将GetClientRect(它总是使用设备单位)取得的显示区域大小转换为逻辑坐标很有用:
下面的函数将逻辑点转换为设备点:
处理MM_TEXT
对于MM_TEXT映像方式,内定的原点和范围如下所示:
窗口原点:(0, 0) 可以改变
视埠原点:(0, 0) 可以改变
窗口范围:(1, 1) 不可改变
视埠范围:(1, 1) 不可改变
视端口范围与窗口范围的比例为1,所以不用在逻辑坐标与设备坐标之间进行缩放。上面所给出的公式可以简化为:
这种映像方式称为「文字」映像方式,不是因为它对于文字最适合,而是由于轴的方向。我们读文字是从左至右,从上至下的,而MM_TEXT以同样的方向定义轴上值的增长方向:
Windows提供了函数SetViewportOrgEx和SetWindowOrgEx,用来改变视端口和窗口的原点,这些函数都具有改变轴的效果,以致(0,0)不再指左上角。一般来说,您会使用SetViewportOrgEx或SetWindowOrgEx之一,但不会同时使用二者。
我们来看一看这些函数有何效果:如果将视埠原点改变为(xViewOrg,yViewOrg),则逻辑点(0.0)就会映像为设备点(xViewOrg,yViewOrg)。如果将窗口原点改变为(xWinOrg,yWinOrg),则逻辑点(xWinOrg,yWinOrg)将会映像为设备点(0,0),即左上角。不管对窗口和视端口原点作什么改变,设备点(0,0)始终是显示区域的左上角。
例如,假设显示区域为cxClient个图素宽和cyClient个图素高。如果想将逻辑点(0,0)定义为显示区域的中心,可进行如下呼叫:
SetViewportOrgEx的参数总是使用设备单位。现在,逻辑点(0,0)将映像为设备点(cxClient/2,cyClient/2),而显示区域的坐标系变成如下形状:
逻辑x轴的范围从-cxClient/2到+cxClient/2,逻辑y轴的范围从-cyClient/2到+cyClient/2,显示区域的右下角为逻辑点(cxClient/2,cyClient/2)。如果您想从显示区域的左上角开始显示文字。则需要使用负坐标:
用下面的SetWindowOrgEx叙述可以获得与上面使用SetViewportOrgEx同样的效果:
SetWindowOrgEx的参数总是使用逻辑单位。在这个呼叫之后,逻辑点(-cxClient / 2,-cyClient / 2)映像为设备点(0,0),即显示区域的左上角。
您不会将这两个函数一起用,除非您知道这么做的结果:
这意味着逻辑点(-cxClient/2,-cyClient/2)将映像为设备点(cxClient/2, cyClient/2),结果是如下所示的坐标系:
您可以使用下面两个函数取得目前视端口和窗口的原点:
其中pt是POINT结构。由GetViewportOrgEx传回的值是设备坐标,而由GetWindowOrgEx传回的值是逻辑坐标。
您可能想改变视端口或者窗口的原点,以改变窗口显示区域内的显示输出-例如,响应使用者在滚动条内的输入。但是,改变视端口和窗口原点并不能立即改变显示输出,而必须在改变原点之后更新输出。例如,在第四章的SYSMETS2程序中,我们使用了iVscrollPos值(垂直滚动条的目前位置)来调整显示输出的y坐标:
我们可以使用SetWindowOrgEx获得同样的效果:
现在,TextOut函数的y坐标的计算不需要iVscrollPos的值。这意味着您可以将文字输出函数放到一个例程中,不用将iVscrollPos值传给该例程,因为我们是通过改变窗口原点来调整文字显示的。
如果您有使用直角坐标系(即笛卡尔坐标系)的经验,那么将逻辑点(0,0)移到显示区域的中央(像我们上面所说的那样)的确值得考虑。但是,对于MM_TEXT映像方式来说,还存在着一个小小的问题:笛卡尔坐标系中,y值是随着上移而增加的,而MM_TEXT定义为下移时y值增加。从这一点来看,MM_TEXT有点古怪,而下面这五种映射方式都使用通常的增值方法。
大多数GDI绘图函数需要坐标值或大小。例如,下面是TextOut函数:
TextOut (hdc, x, y, psText, iLength) ;
在TextOut中,以及在几乎所有GDI函数中,这些坐标值使用的都是一种「逻辑单位」。Windows必须将逻辑单位转换为「设备单位」,即图素。这种转换是由映像方式、窗口和视端口的原点以及窗口和视端口的范围所控制的。映像方式还指示着x轴和y轴的方向(orientation);也就是说,它确定了当您在向显示器的左或者右移动时x的值是增大还是减小,以及在上下移动时y的值是增大还是减小。
Windows定义了8种映像方式,它们在WINGDI.H中相应的标识符和含义如表5-5所示。
表5-5 |
映像方式 | 逻辑单位 | 增加值 | |
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) | 可选 | 可选 |
您可以使用下面的叙述来设定映射方式:
SetMapMode (hdc, iMapMode) ;
iMapMode = GetMapMode (hdc) ;
TextOut (hdc, 8, 16, TEXT ("Hello"), 5) ;
如果映像方式设定为MM_LOENGLISH:
SetMapMode (hdc, MM_LOENGLISH) ;
TextOut (hdc, 50, -100, TEXT ("Hello"), 5) ;
如果您认为使用图素进行工作很合适,那么就不要使用内定的MM_TEXT方式外的任何映像方式。如果需要以英寸或者毫米尺寸显示图像,那么可以从GetDeviceCaps中取得所需要的信息,自己再进行缩放。其它映像方式都是避免您自己进行缩放的一个方便途径而已。
虽然您在GDI函数中指定的坐标是32位的值,但是仅有Windows NT能够处理全32位。在Windows 98中,坐标被限制为16位,范围从-32,768到32,767。一些使用坐标表示矩形的开始点和结束点的Windows函数也要求矩形的宽和高小于或者等于32,767。
设备坐标和逻辑坐标
您也许会问:如果使用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以取得关于字符的宽度和高度信息时,映像方式必须设定成根据这些信息输出文字时所使用的映像方式,这样就可以简化工作。
设备坐标系
Windows将GDI函数中指定的逻辑坐标映像为设备坐标。在讨论以各种不同的映像方式使用逻辑坐标系之前,我们先来看一下Windows为视讯显示器区域定义的不同的设备坐标系。尽管我们大多数时间在窗口的显示区域内工作,但Windows在不同的时间使用另外两种设备坐标区域。所有设备坐标系都以图素为单位,水平轴(即x轴)上的值从左到右递增,垂直轴(即y轴)上的值从上到下递增。
当我们使用整个屏幕时,就根据「屏幕坐标」进行操作。屏幕的左上角为(0,0)点,屏幕坐标用在WM_MOVE消息(对于非子窗口)以及下列Windows函数中:CreateWindow和MoveWindow(都是对于非子窗口)、GetMessagePos、GetCursorPos、SetCursorPos、GetWindowRect以及WindowFromPoint(这不是全部函数的列表)。它们或者是与窗口无关的函数(如两个光标函数),或者是必须相对于某个屏幕点来移动(或者寻找)窗口的函数。如果以DISPLAY为参数呼叫CreateDC,以取得整个屏幕的设备内容,则内定情况下GDI呼叫中指定的逻辑坐标将被映像为屏幕坐标。
「全窗口坐标」以程序的整个窗口为基准,如标题列、菜单、滚动条和窗口框都包括在内。而对于普通窗口,点(0,0)是缩放边框的左上角。全窗口坐标在Windows中极少使用,但是如果用GetWindowDC取得设备内容,GDI函数中的逻辑坐标就会转换为显示区域坐标。
第三种坐标系是我们最常使用的「显示区域坐标系」。点(0,0)是显示区域的左上角。当使用GetDC或BeginPaint取得设备内容时,GDI函数中的逻辑坐标就会内定转换为显示区域坐标。
用函数ClientToScreen和ScreenToClient可以将显示区域坐标转换为屏幕坐标,或者反过来,将屏幕坐标转换为显示区域坐标。也可以使用GetWindowRect函数取得屏幕坐标下的整个窗口的位置和大小。这三个函数为一种设备坐标转换为另一种提供了足够的信息。
视端口和窗口
映像方式定义了Windows如何将GDI函数中指定的逻辑坐标映像为设备坐标,这里的设备坐标系取决于您用哪个函数来取得设备内容。要继续讨论映像方式,我们需要一些术语:映像方式用于定义从「窗口」(逻辑坐标)到「视端口」(设备坐标)的映像。
「窗口」和「视端口」这两个词用得并不恰当。在其它图形接口语言中,视端口通常包含有剪裁区域的意思,并且,我们已经用窗口来指程序在屏幕上占据的区域。在这里的讨论中,我们必须把关于这些词的先入之见丢到一边。
「视端口」是依据设备坐标(图素)的。通常,视端口和显示区域相同,但是,如果您已经用GetWindowDC或CreateDC取得了一个设备内容,则视端口也可以是指整窗口坐标或者屏幕坐标。点(0,0)是显示区域(或者整个窗口或屏幕)的左上角,x的值向右增加,y的值向下增加。
「窗口」是依据逻辑坐标的,逻辑坐标可以是图素、毫米、英寸或者您想要的任何其它单位。您在GDI绘图函数中指定逻辑窗口坐标。
但是在真正的意义上,视端口和窗口仅是数学上的概念。对于所有的映像方式,Windows都用下面两个公式来将窗口(逻辑)坐标转化为视埠(设备)坐标:
这两个公式使用了分别指定窗口和视端口「原点」的点:(xWinOrg,yWinOrg)是逻辑坐标的窗口原点;(xViewOrg,yViewOrg)是设备坐标的视端口原点。在内定的设备内容中,这两个点均被设定为(0,0),但是它们可以改变。此公式意味着,逻辑点(xWinOrg,yWinOrg)总被映像为设备点(xViewOrg,yViewOrg)。如果窗口和视端口的原点是默认值(0,0),则公式简化为:
例如,当您设定MM_LOENGLISH映像方式时,Windows将xViewExt设定为某个图素数而将xWinExt设定为xViewExt图素占据的一英寸内有几百图素的长度。比值给出了一英寸内有几百个图素的数值。为了提高转换效能,换算因子表示为整数比而不是浮点数。
范围可以为负,也就是说,逻辑x轴上的值不一定非得在向右时增加;逻辑y轴上的值不一定非得在向下时增加。
Windows也能将视埠(设备)坐标转换为窗口(逻辑)坐标:
DPtoLP (hdc, pPoints, iNumber) ;
GetClientRect (hwnd, &rect) ; DPtoLP (hdc, (PPOINT) &rect, 2) ;
LPtoDP (hdc, pPoints, iNumber) ;
对于MM_TEXT映像方式,内定的原点和范围如下所示:
窗口原点:(0, 0) 可以改变
视埠原点:(0, 0) 可以改变
窗口范围:(1, 1) 不可改变
视埠范围:(1, 1) 不可改变
视端口范围与窗口范围的比例为1,所以不用在逻辑坐标与设备坐标之间进行缩放。上面所给出的公式可以简化为:
我们来看一看这些函数有何效果:如果将视埠原点改变为(xViewOrg,yViewOrg),则逻辑点(0.0)就会映像为设备点(xViewOrg,yViewOrg)。如果将窗口原点改变为(xWinOrg,yWinOrg),则逻辑点(xWinOrg,yWinOrg)将会映像为设备点(0,0),即左上角。不管对窗口和视端口原点作什么改变,设备点(0,0)始终是显示区域的左上角。
例如,假设显示区域为cxClient个图素宽和cyClient个图素高。如果想将逻辑点(0,0)定义为显示区域的中心,可进行如下呼叫:
SetViewportOrgEx (hdc, cxClient / 2, cyClient / 2, NULL) ;
TextOut (hdc, -cxClient / 2, -cyClient / 2, "Hello", 5) ;
SetWindowOrgEx (hdc, -cxClient / 2, -cyClient / 2, NULL) ;
您不会将这两个函数一起用,除非您知道这么做的结果:
SetViewportOrgEx (hdc, cxClient / 2, cyClient / 2, NULL) ; SetWindowOrgEx (hdc, -cxClient / 2, -cyClient / 2, NULL) ;
GetViewportOrgEx (hdc, &pt) ; GetWindowOrgEx (hdc, &pt) ;
您可能想改变视端口或者窗口的原点,以改变窗口显示区域内的显示输出-例如,响应使用者在滚动条内的输入。但是,改变视端口和窗口原点并不能立即改变显示输出,而必须在改变原点之后更新输出。例如,在第四章的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 ;
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 ;
如果您有使用直角坐标系(即笛卡尔坐标系)的经验,那么将逻辑点(0,0)移到显示区域的中央(像我们上面所说的那样)的确值得考虑。但是,对于MM_TEXT映像方式来说,还存在着一个小小的问题:笛卡尔坐标系中,y值是随着上移而增加的,而MM_TEXT定义为下移时y值增加。从这一点来看,MM_TEXT有点古怪,而下面这五种映射方式都使用通常的增值方法。