GDI映射方式
映射方式的作用:定义了windows如何将GDI函数中指定的逻辑坐标映射为设备坐标。
1、概述
<1>、在TextOut中,以及在几乎所有的GDI函数中,坐标值使用的都是一种逻辑单位,windows必须将逻辑单位转换为设备单位,即图素,这种转换由映射方式、窗口原点、视口原点、窗口范围、视口范围控制。映射方式还指示者x轴、y轴的方向。
<2>、Windows定义了8种映射方式: MM_TEXT、MM_LOMETRIC、MM_HIMETRIC、MM_LOENGLISH、MM_HIENGLISH、MM_TWIPS、MM_ISOTROPIC、MM_ANISOTROPIC。
<3>、调用SetMapMode函数来设置映射方式:SetMapMode(hdc, iMapMode);
调用GetMapMode函数来得到当前的映射方式:iMapMode = GetMapMode(hdc);
<4>、系统内定的映射方式是:MM_TEXT,在这种映射方式下,逻辑单位与实际单位相同,用户可以直接以图素为单位进行操作。
在MM_TEXT映射模式下调用TextOut: TextOut(hdc, 8, 16, TEXT("Hello"),5); 表示文字从距离显示区域左端8图素、距离显示区域上端16图素的位置处开始;
在MM_LOENGLISH映射模式下调用TextOut: TextOut(hdc, 50, -100, TEXT("Hello"),5); 表示文字从距离显示区域左端0.5英寸、距离显示区域上端1英寸的位置处开始
<5>、用户如果需要以英寸或毫米显示图像,可以从GetDeviceCaps中取得所需要的信息,然后自己在进行缩放。其他映射方式只是避免用户自己缩放的一种简便方式。
2、设备坐标和逻辑坐标
由于映射方式是一种设备描述表属性,所以,只有对需要设备描述表句柄作参数的GDI函数,映射方式才会起作用。Windows对所有消息(如WM_MOVE、WM_SIZE和WM_MOUSEMOVE),对所有的非GDI函数,甚至对一些GDI函数,永远使用设备坐标(即图素单位)。
注意:从GetTextMetrics调用中返回的TEXTMETRIC结构的值是使用逻辑单位的。如使用MM_TEXT,从该结构得到的字元高度和宽度是图素单位(该模式下逻辑单位与设备单位一样),如果使用MM_LOENGLISH,则字元尺寸的单位是百分之一英寸。
3、设备坐标系
windows将GDI函数中指定的逻辑坐标值映射为设备坐标。
Windows为显示器定义了3种设备坐标系,所有的设备坐标系都以图素为单位,水平轴(x轴)上的值从左向右递增,垂直轴(y轴)上的值从上向下递增。
屏幕坐标系:屏幕左上角为原点,如果以DISPLAY为参数调用CreateDC,来取得整个屏幕的设备描述表,则在内定的逻辑坐标将映射为屏幕坐标。
窗口坐标系:以程序窗口为基准,如标题列、功能列、卷动列和窗口框架都包含在内,窗口边框的左上角为原点,如果使用GetWindowDC获得设备描述表,GDI函数中的逻辑坐标将被转换为窗口坐标。
客户区坐标系:客户区的左上角为原点,GetDC或BeginPaint获得设备描述表,GDI函数中的逻辑坐标将被映射为客户区坐标。
坐标系之间的转换:
调用ClientToScreen函数,可以将客户区坐标转换为屏幕坐标
调用ScreenToClient函数,可以将屏幕坐标转换为客户区坐标
窗口坐标系很少使用,不过经常使用GetWindowRect函数取得屏幕坐标系下窗口的位置和大小。
4、窗口(windowport)与视口(viewport)
自己理解:“窗口”是逻辑坐标下的程序视窗范围,“视口”是设备坐标下的程序视窗范围。
注意设备坐标点(0,0),始终是在屏幕(或窗口或客户区)的左上角,但是点(0,0)不一定是映射关系中的原点。
映射方式用于定义从“窗口”(逻辑坐标)到"视口"(设备坐标)的映射。
窗口和视口仅是数学上的概念,要记住“窗口”是依据逻辑坐标的,单位可以是图素、毫米、英寸或者其他单位。“视口”是依据设备单位的,单位只能是图素。它们存在以下映射关系:
其中,点(xWindow, yWindow)是待转换的逻辑点,点(xViewport,yViewport)是转换后的设备坐标点。(xWinExt, yWinExt)是逻辑坐标的窗口范围,(xViewExt, yViewExt)是设备坐标系下的窗口范围。
注:调用DPtoLP函数,可以将设备坐标点转换为逻辑坐标点;
DPtoLP(hdc, pPoints, iNumber);
调用LPtoDP函数,可以将逻辑点转换为设备点:
LPtoDP(hdc, pPoints, iNumber);
经常使用的调用:将GetClientRect(设备坐标)取得的客户区大小转换为逻辑坐标:
GetClientRect(hwnd, &rect);
DPtoLP(hdc, (PPOINT)&rect, 2);
5、MM_TEXT映射方式
这种映射方式又称为文字映射方式,视口范围与窗口范围的比例为1:,映射公式如下:
xViewport = xWindow - xWinOrg + xViewOrg;
yViewport = yWindow - yWinOrg + yViewOrg;
Windows提供SetViewportOrgEx和SetWindowOrgEx函数来改变视口和窗口的原点。注意:一般只会使用SetViewportOrgEx和SetWindowOrgEx之一,但不会同时使用它们。
例如:(1)、假设客户区为cxClient个像素宽,有cyClient个像素高,如果将逻辑点(0,0)映射到客户区的中心,可以调用:SetViewportOrgEx(hdc, cxClient/2, cyClient/2);
这时,客户区中逻辑x轴的范围是-cxClient到cxClient,逻辑y轴的范围是-cyClient到cyClient;
注意:使用SetWindowOrg(hdc, -cxClient/2, -cyClient/2, NULL);可以得到相同的效果。该函数是将逻辑点(-cxClient, -cyClient)设备点(0, 0)即左上角。注意这里的参数是逻辑单位。
问题1:屏幕(或窗口或客户区)的左上角始终是设备点(0,0),与通过SetWindowOrg来改变窗口原点是否矛盾?
答:虽然SetViewportOrgEx和SetWndowOrgEx是改变视口和窗口的原点,但是可以这样理解,SetViewportOrgEx(hdc, cxClient/2, cyClient/2)是改变逻辑原点,即将原来逻辑坐标系中的点(cxClient/2, cyClient/2)作为新的逻辑坐标系中的原点(0,0),SetWindowsOrgEx(hdc, -cxClient/2,-cyClient/2,NULL),实际上并未改变设备坐标系原点的位置,它改变的仍然是逻辑原点,它将逻辑点(-cxClient/2,-cyClient/2)映射到设备原点(0,0)即左上角,等价于原来的逻辑原点(0,0)变为新逻辑坐标系中的点(-cxClient/2,-cyClient/2)。
问题2:既然这样,那么根据映射公式,“窗口”原点始终映射到"视口"原点的现象又该如何解释?
答:首先,在同一个公式中描述的坐标是在同一个坐标系下的,例如逻辑坐标系的原点在公式中描述为逻辑点(xWinOrg,yWinOrg),而设备坐标系的原点在公式中描述为逻辑点(xViewOrg,yViewOrg);例如,我们将逻辑坐标系的原点设置为(cxClient,cyClient),在公式中描述为(cxClient,cyClient),设备坐标系的原点在设备点(0,0),在公式中描述为逻辑点(-cxClient,-cyClient),即现在点(cxClient,cyClient)到点(-cxClient,-cyClient)的映射就表示了逻辑坐标系到设备坐标系中的映射,因此,我们可以说新逻辑坐标系的原点映射到了设备坐标系的原点(该点在逻辑坐标下描述为(-cxClient,-cyClient),在设备坐标系中描述为设备点(0,0))。
问题3:屏幕(或窗口或客户区)左上角的点与原点有什么关系?
答:设备坐标系原点始终在这个左上角,它在设备坐标系中的描述始终是(0,0),在逻辑坐标系中的描述却可以是任意逻辑点(这取决于逻辑坐标系的原点位置);
逻辑坐标系原点与这个左上角没有必然关系,只不过在MM_TEXT模式下,逻辑坐标系原点与设备坐标系原点重合,都在屏幕左上角。
注意:记住以下两点,将非常重要:
SetViewportOrgEx(hdc, cxClient/2,cyClient/2,NULL);//将逻辑原点平移(cxClient/2,cyClient/2)距离(矢量);
SetWindowOrgEx(hdc, -cxClient/2, cyCLient/2, NULL);//将逻辑原点平移(cxClint/2,cyCLient/2)距离(矢量);
因此,当两个函数调用同时使用时,逻辑原点平移的距离是(cxClient,cyClient)。
得到当前的视口和窗口原点:
GetViewportOrgEx(hdc, &pt);//返回值为设备坐标。
GetWindowOrgEx(hdc, &pt);//返回值为逻辑坐标。
6、“度量”映射方式
*Windows包含五种以实际尺寸来表示逻辑坐标的映射方式。由于x轴和y轴的逻辑坐标映射为相同的实际单位,这些映射方式能使我们画出不变形的圆和矩形。
*这五种“度量”映射方式是:MM_LOENGLISH、MM_LOMETRIC、MM_HIENGLISH、MM_TWIPS、MM_HIMETRIC。注意:这五种映射方式的y轴是随着上升而增长的。
*关于视口与窗口的范围:
假定在windowsNT系统下,屏幕分辨率是1024*768,则视口与窗口的范围如下表:
其中视口范围单位是:设备单位(图素),窗口单位是:逻辑单位(具体的英寸、毫米等)。
视口范围与窗口范围的比例,反映每逻辑单位内图素的个数。
*注意:当把映射方式改为这五种之一时,初始的窗口原点在屏幕左上角,x向右增加;y轴向上增加。(显示文字必须使用负的y轴值)。
*改变逻辑坐标原点:在这些映射模式下使用SetViewportOrgEx来改变原点很方便。
如:SetViewportOrgEx(hdc, 0, cyClient, NULL);//假定cyClient是以图素为单位的显示区域高度。
另一种方法,通过SetWindowOrgEx函数来改变逻辑坐标原点,因为它的参数要求使用逻辑单位,因此要先将图素单位描述的点通过DPtoLP函数转换为逻辑点,然后再使用:
如:pt.x = cxClient/2; pt.y = cyClient/2;
DPtoLP(hdc, &pt, 1);
SetWindowOrgEx(hdc, -pt.x, -pt.y, NULL);
7、“自行决定的”映射方式
MM_ISOTROPIC和MM_ANISOTROPIC,只有这两种映射方式可以让用户来改变视口和窗口范围。单词“isotropic”的意思是“同方向性”,“anisotropic”的意思是“异方向性”。
MM_ISOTROPIC映射方式,x轴和y轴的逻辑单位的实际尺寸相等。它与“度量”映射方式之间的区别是:使用MM_ISOTROPIC,用户可以控制逻辑单位的实际尺寸。可以根据显示区域的大小来调整逻辑单位大小,并相应地放大或缩小显示区域内所画的图像。
注:MM_TEXT和“度量”映射方式,称为“完全局限性”映射方式,即视口、窗口的范围不能改变,以及Windows将逻辑坐标换算为设备坐标的方法不能改变;
MM_ISOTROPIC是“半局限性”映射方式,即视口、窗口的范围允许用户改变,但是Windows只会调整它们,使得x轴和y轴的逻辑单位代表同样的实际尺寸;
MM_ANISOTROPIC是“非局限性”映射方式,即视口、窗口的范围允许用户改变,而且Windows不会调整这些值。
<1>、MM_ISOTROPIC映射方式:
在这种映射方式下,用户可以通过SetWindowExtEx和SetViewportExtEx来设定窗口、视口的范围,然后Windows将调整范围的值,以保证x轴与y轴上的逻辑单位有相同的实际距离。
一般说来,用用户期望的逻辑窗口的逻辑尺寸作为SetWindowExtEx的参数,用显示区域的实际宽和高作为SetViewportExtEx的参数。为了更有效地使用显示区域的空间,必须先调用SetWindowExtEx,然后调用SetVieportExtEx函数。
<2>、MM_ANISOTROPIC映射方式:
在MM_ANISOTROPIC映射方式下,Windows不调整用户设定的视口、窗口范围,逻辑x轴和y轴可以具有不等的实际尺寸。
二、矩形、区域、剪切
“区域”是屏幕上的一块地方,它是矩形、多边形、椭圆的组合。
1、矩形函数
FillRect(hdc, &rect, hBrush);//用指定的画刷来填入矩形,该函数不需要先将画刷选进设备描述表。
FrameRect(hdc, &rect, hBrush);//使用画刷画矩形框,但不填入矩形。(允许使用者画一个不一定为纯色的矩形框)
InvertRect(hdc, &rect);//将矩形中所有图素翻转(1变为0, 0变为1)
SetRect(&rect, xLeft, yTop, xRight, yBottom);//设置矩形的左上角与右下角坐标。
OffsetRect(&rect, x, y);//将矩形沿x轴和y轴移动几个单位。
InflateRect(&rect, x, y);//增减矩形的尺寸。
SetRectEmpty(&rect);//矩形各栏位设置为0。
CopyRect(&DestRect, &SrcRect);//将矩形复制给另一个矩形。
IntersectRect(&DestRect, &ScrRect1, &ScrRect2);//取得两个矩形的交集
UnionRect(&DestRect, &ScrRect1, &ScrRect2);//取得两个矩形的并集
bEmpty = IsRectEmpty(&rect);//确定矩形是否为空
bInRect = PtInRect(&rect, point);//确定点是否在矩形内
矩形对象可以直接相互赋值,如DestRect = ScrRect;
2、随机矩形的实现(PeekMessage的使用)
<1>、如何利用程序的空闲时间
程序一定有许多空闲时间,在这个时间里,所有消息队列为空,Windows只停在一个小循环中等待键盘uozhe鼠标输入。要想利用这些空闲时间,可以使用PeekMessage函数。
<2>这里着重介绍PeekMesage的使用
函数原型:BOOL PeekMessage(LPMSG lpMsg, HWND hWnd, UINT wMsgFilterMin, UINT wMsgFilterMax, UINT wRemoveMsg );
PeekMessage的最后一个参数指定如何处理消息:
PM_NOREMOVE:消息在被PeekMessage处理后,不从消息队列中移除
PM_REMOVE:消息在被PeekMessage处理后,从消息队列中移除
PeekMessage的返回值:
消息队列中有有效消息时,返回非零;消息队列中没有有效消息时返回零。
PeekMessage和GetMessage的对比:
相同点:PeekMessage和GetMessage都用于查看应用程序的消息队列,有消息时将队列中的消息派发出去。
不同点:无论应用程序消息队列中是否有消息,PeekMessage函数都立刻返回,程序能够继续执行后面的语句(无消息时执行其他指令,有消息时,先将消息派发出去,再执行其他指令);
GetMessage函数只有在消息队列中有消息时才返回,对列中无消息时会一直等待,直到下一个消息出现时才返回,在这段等待时间里,应用程序不执行任何指令。
用PeekMessage改写普通的消息循环:
改写前: while(GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return msg.wParam;
改写后:
while(true)
{
if(PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{
if(msg.message == WM_QUIT)
break;
TranslateMessage(&msg);
DispatchMessage(&msg);
}
else
{
//这是程序空闲时间,可以做想要做的事情
}
}
return msg.wParam;
3、建立与绘制剪切区域
剪切区域可以用于绘制和剪切,通过将剪切区域选进设备描述表,就可以用剪切区域来进行剪切(也就是说,将绘图的范围限制在显示区域的一部分)。
注意:与画笔、画刷、点阵图一样,剪切区域也是GDI物件,在最后要用DeleteObject来删除使用者建立的剪切区域。
*建立矩形剪切区域:
hRgn = CreateRectRgn(xLeft, yTop, xRight, yBottom);
或者hRgn = CreateRectRgnIndirect(&rect);
*建立椭圆剪切区域:
hRgn = CreateEllipticRgn(xLeft, yTop, xRight, yBottom);
或者hRgn = CreateEllipticRgnIndirect(&rect);
*建立圆角矩形剪切区域:
hRgn = CreateRoundRectRgn(xLeft,yTop, xRight,yBottom, nWidth, nHeight);
*建立多边形剪切区域:
hRgn = CreatePolygonRgn(&point, iCount, iPolyFileMode);
*组合剪切区域:
iRgnType = CombineRgn(hDestRgn, hSrcRgn1, hSrcRgn2, iCombine);
hSrcRgn1,hSrcRgn2标识两个将要组合的区域,hDestRgn指向组合后的新区域,iCombine标识组合的方式,可以是如下值:RGN_AND:两个剪切区域的公共部分;RGN_OR:两个剪切区域的全部;RGN_XOR:两个剪切区域的全部除去公共部分; RGN_DIFF:hSrcRgn1 不在hSrcRgn2中的部分; RGN_COPY:hSrcRgn1的全部(忽略hSrcRgn2)。
返回值:NULLREGION, 或SIMPLEREGION, 或COMPLEXREGION, 或ERROR。
剪切区域句柄可以用于的四个函数:
FillRgn, FrameRgn, InvertRgn, PaintRgn;
其中PaintRgn函数用设备描述表中当前画刷填入选定区域。
注意:这些函数都假定区域是用逻辑坐标定义的。
4、矩形与区域的剪切
*类似于InvlidateRect和ValidateRect函数,Windows还有两个作用于区域的函数:
InvalidateRgn(hwnd, hRgn, bErase ); 和 ValidateRgn(hwnd, hRgn);
注意:当无效区域引起WM_PAINT消息时,剪切区域不一定是矩形。
*将剪切区域选进设备描述表:(该剪切区域使用设备坐标)
SelectObject(hdc, hRgn); 或 SelectClipRgn(hdc, hRgn);
其他几个函数:
ExcludeClipRect函数:将一个矩形从剪切区域中排除掉;
IntersectClipRect 函数:用于建立一个新的剪切区域,它是前一个剪切区域与一个矩形的交集;
OffsetClipRgn函数:用于将剪切区域移动到显示区域的另一部分。
5、实例
创建两个椭圆区域:
HRGN hRgnTemp1 = CreateEllipticRgn(0, 10, 20, 30);
HRGN hRgnTemp2 = CreateEllipticRgn(12,10, 35,50);
将两个区域组合:
HRGN hRgnClip = CreateRectRgn(0,0,1,1);
CombineRgn(hRgnClip, hRgnTemp1, hRgnTemp2, RGN_OR);
删除原来的两个区域(因为不再需要了)
DeleteObject(hRgnTemp1);
DeleteObject(hRgnTemp2);
这时,在响应WM_PAINT消息时,可以将组合后的剪切区域选进设备描述表:
SelectClipRgn(hdc, hRgnClip);
注意:使用BeginPaint获得设备描述表时,已经限制只能在剪切区域内绘图,因此用户不用关心绘制的图形会超出剪切区域边界,这样就可以画出在复杂的剪切区域内呈现的图画。