GDI绘图里面也讲究坐标系,地道!
逻辑坐标与设备坐标
坐标空间是一个二维笛卡尔坐标系,通过使用相互垂直的两个参考轴来定位二维对象。系统中有四层坐标空间:世界、页面、设备和物理设备(客户区、桌面或打印纸页面),如下所示。
- 世界坐标空间
可选,用作图形对象变换的起始坐标空间,可以对图形对象进行平移、缩放、旋转、剪切(倾斜、变形)和反射(镜像)。世界坐标空间高2单位,宽2单位
- 页面坐标空间
用作世界坐标空间之后的下一个坐标空间,或图形变换的起始坐标空间,该坐标空间可以设置映射模式。页面坐标空间也是高2单位,宽2单位
- 设备坐标空间
用作页面坐标空间之后的下一个坐标空间,该坐标空间只允许平移操作,这样可以确保设备坐标空间的原点映射到物理设备坐标空间中的正确位置。设备坐标空间高2个单位,宽2个单位
- 物理设备坐标空间
图形对象变换的最终(输出)空间,通常指应用程序窗口的客户区,也可以是整个桌面、全窗口(整个程序窗口,包括标题栏、菜单栏、客户区、边框等)或一页打印机或绘图仪纸张,具体取决于获取的是哪一种DC
本节的话题比较抽象,但是相信读者通过阅读已经掌握了不少。世界坐标空间是可选的,所以只是简要介绍一下,页面坐标空间、设备坐标空间以及映射模式是需要我们学习的。
世界坐标空间和页面坐标空间都称为逻辑坐标空间,这两种坐标空间配合使用,为应用程序提供与设备无关的单位,如毫米和英寸。系统使用变换技术将一个矩形区域从一个坐标空间复制(或映射)到下一个坐标空间,直到输出全部显示在物理设备上,变换是一种改变对象大小、方向和形状的算法。一些工程或机械绘图类程序使用像素作为绘图单位是不合适的,因为像素的大小因设备而异(例如一个手机屏幕分辨率可能高达2560 1440,而一个14寸笔记本考1366 768,高分屏14寸笔记本可能是1920 1080),所以这类程序一般使用与设备无关的单位(例如毫米、英寸)。这类程序通常也使用变换技术,例如CAD程序的旋转对象、缩放图形或创建透视图等功能。
世界坐标空间到页面坐标空间的变换
DC默认运行在兼容图形模式下。兼容图形模式只支持一种逻辑坐标空间,即页面坐标空间,而不支持世界坐标空间。如果应用程序需要支持世界坐标空间,就必须调用SetGraphicsMode(hdc,GM_ADVANCED)
函数改变DC的图形模式为高级图形模式,这样一来,DC就支持两层逻辑坐标空间∶世界坐标空间和页面坐标空间以及两种坐标空间之间的变换矩阵。
世界坐标空间到页面坐标空间的变换支持平移、缩放、旋转、剪切(倾斜、变形)和反射(镜像)等功能,这都是通过调用SetWorldTransform函数实现的。调用该函数以后,映射将从世界坐标空间开始﹔否则,映射将从页面坐标空间开始。
SetWorldTransform函数为指定的DC设置世界坐标空间和页面坐标空间之间的二维线性变换,该函数使用的是逻辑单位∶
BOOL SetWorldTransform(
_In_ HDC hdc, //设备环境句柄
_In_ const XFORM *lpXform //包含变换数据的XFORM结构的指针
);
参数lpXform是一个指向XFORM结构的指针,包含世界坐标空间到页面坐标空间变换的数据,XFORM结构在wingdi.h头文件中定义如下∶
typedef struct tagXFORM
{
FLOAT eM11;
FLOAT eM12;
FLOAT eM21;
FLOAT eM22;
FLOAT eDx;
FLOAT eDy;
}XFORM,*PXFORM,FAR*LPXFORM;
这6个字段构成了一个2 3矩阵,不同的操作需要设置不同的字段,如下表所示。
接下来让我们看一下不同变换的效果。对于WorldPage程序,读者分别用NORMAL TRANSLATE,SCALE-ROTATE,SHEAR- REFLECT为参数调用TransformAndDraw函数,看一下相等、平移、缩放、旋转、剪切、反射的效果,在学习了页面坐标空间到设备坐标空间的变换以后就可以读懂本程序。具体代码实现如下所示:
#include <Windows.h>
#include <tchar.h>
// 函数声明,窗口过程
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
WNDCLASSEX wndclass;
TCHAR szAppName[] = TEXT("世界空间到页面空间的变换");
HWND hwnd;
MSG msg;
wndclass.cbSize = sizeof(WNDCLASSEX);
wndclass.style = CS_HREDRAW | CS_VREDRAW;
wndclass.lpfnWndProc = WindowProc;
wndclass.cbClsExtra = 0;
wndclass.cbWndExtra = 0;
wndclass.hInstance = hInstance;
wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
wndclass.hbrBackground = (HBRUSH)(COLOR_3DFACE + 1);
wndclass.lpszMenuName = NULL;
wndclass.lpszClassName = szAppName;
wndclass.hIconSm = NULL;
RegisterClassEx(&wndclass);
hwnd = CreateWindowEx(0, szAppName, szAppName, WS_OVERLAPPEDWINDOW,
100, 100, 500, 400, NULL, NULL, hInstance, NULL);
ShowWindow(hwnd, nCmdShow);
UpdateWindow(hwnd);
while (GetMessage(&msg, NULL, 0, 0) != 0)
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return msg.wParam;
}
enum MyEnum
{
NORMAL, TRANSLATE, SCALE, ROTATE, SHEAR, REFLECT,
};
void TransformAndDraw(int iTransform, HWND hWnd)
{
HDC hdc;
XFORM xForm;
RECT rect;
hdc = GetDC(hWnd);
// 将图形模式设置为高级图形模式
SetGraphicsMode(hdc, GM_ADVANCED);
// 将映射模式设置为MM_LOMETRIC,以0.1毫米为单位,这个函数稍后介绍
SetMapMode(hdc, MM_LOMETRIC);
switch (iTransform)
{
case NORMAL: // 相等
xForm.eM11 = (FLOAT)1.0;
xForm.eM12 = (FLOAT)0.0;
xForm.eM21 = (FLOAT)0.0;
xForm.eM22 = (FLOAT)1.0;
xForm.eDx = (FLOAT)0.0;
xForm.eDy = (FLOAT)0.0;
SetWorldTransform(hdc, &xForm);
break;
case TRANSLATE: // 向右平移
xForm.eM11 = (FLOAT)1.0;
xForm.eM12 = (FLOAT)0.0;
xForm.eM21 = (FLOAT)0.0;
xForm.eM22 = (FLOAT)1.0;
xForm.eDx = (FLOAT)200.0;
xForm.eDy = (FLOAT)0.0;
SetWorldTransform(hdc, &xForm);
break;
case SCALE: // 缩放到原始大小的1/2
xForm.eM11 = (FLOAT)0.5;
xForm.eM12 = (FLOAT)0.0;
xForm.eM21 = (FLOAT)0.0;
xForm.eM22 = (FLOAT)0.5;
xForm.eDx = (FLOAT)0.0;
xForm.eDy = (FLOAT)0.0;
SetWorldTransform(hdc, &xForm);
break;
case ROTATE: // 逆时针旋转30度
xForm.eM11 = (FLOAT)0.8660;
xForm.eM12 = (FLOAT)0.5000;
xForm.eM21 = (FLOAT)-0.5000;
xForm.eM22 = (FLOAT)0.8660;
xForm.eDx = (FLOAT)0.0;
xForm.eDy = (FLOAT)0.0;
SetWorldTransform(hdc, &xForm);
break;
case SHEAR: // 倾斜变形
xForm.eM11 = (FLOAT)1.0;
xForm.eM12 = (FLOAT)1.0;
xForm.eM21 = (FLOAT)0.0;
xForm.eM22 = (FLOAT)1.0;
xForm.eDx = (FLOAT)0.0;
xForm.eDy = (FLOAT)0.0;
SetWorldTransform(hdc, &xForm);
break;
case REFLECT: // 沿X轴镜像
xForm.eM11 = (FLOAT)1.0;
xForm.eM12 = (FLOAT)0.0;
xForm.eM21 = (FLOAT)0.0;
xForm.eM22 = (FLOAT)-1.0;
xForm.eDx = (FLOAT)0.0;
xForm.eDy = (FLOAT)0.0;
SetWorldTransform(hdc, &xForm);
break;
}
GetClientRect(hWnd, (LPRECT)&rect);
// 设备坐标转换为逻辑坐标,此处逻辑单位为0.1毫米
DPtoLP(hdc, (LPPOINT)&rect, 2);
SelectObject(hdc, GetStockObject(NULL_BRUSH));
// 还记得吧,下面的绘图函数均使用逻辑单位
// 画外圆
Ellipse(hdc, (rect.right / 2 - 300), (rect.bottom / 2 + 300),
(rect.right / 2 + 300), (rect.bottom / 2 - 300));
// 画内圆
Ellipse(hdc, (rect.right / 2 - 270), (rect.bottom / 2 + 270),
(rect.right / 2 + 270), (rect.bottom / 2 - 270));
// 画小矩形
Rectangle(hdc, (rect.right / 2 - 20), (rect.bottom / 2 + 360),
(rect.right / 2 + 20), (rect.bottom / 2 + 210));
// 画水平线
MoveToEx(hdc, (rect.right / 2 - 400), (rect.bottom / 2 + 0), NULL);
LineTo(hdc, (rect.right / 2 + 400), (rect.bottom / 2 + 0));
// 画垂直线
MoveToEx(hdc, (rect.right / 2 + 0), (rect.bottom / 2 + 400), NULL);
LineTo(hdc, (rect.right / 2 + 0), (rect.bottom / 2 - 400));
ReleaseDC(hWnd, hdc);
}
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
HDC hdc;
PAINTSTRUCT ps;
if (uMsg == WM_CREATE)
{
return 0;
}
else if (uMsg == WM_PAINT)
{
hdc = BeginPaint(hwnd, &ps);
// 请依次测试:NORMAL, TRANSLATE, SCALE, ROTATE, SHEAR, REFLECT
TransformAndDraw(NORMAL, hwnd);
EndPaint(hwnd, &ps);
return 0;
}
else if (uMsg == WM_DESTROY)
{
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
页面坐标空间到设备坐标空间的变换
页面坐标空间到设备坐标空间的变换决定了与DC关联的所有图形输出的映射模式。映射模式指定用于绘图操作的逻辑单位的大小。Windows提供了8种映射模式,如下表所示。
映射模式逻辑单位XY轴正方向
×坐标轴从左向右增加,
-
MM_TEXT
页面空间中的每个逻辑单位都映射到一个像素,也就是说,根本不执行缩放,这种映射模式下的页面空间相当于设备空间 Y坐标轴从上到下增加,X坐标轴从左向右增加. -
MM_HIENGLISH
页面空间中的每个逻辑单位映射到设备空间中的0.001英寸,Y坐标轴从下到上增加; X坐标轴从左向右增加 -
MM_LOENGLISH
页面空间中的每个逻辑单位映射到设备空间中的0.01英寸;Y坐标轴从下到上增加;X坐标轴从左向右增加 -
MM_HIMETRIC
页面空间中的每个逻辑单位映射到设备空间中的0.01毫米;Y坐标轴从下到上增加;X坐标轴从左向右增加 -
MM_LOMETRIC
页面空间中的每个逻辑单位映射到设备空间中的0.1毫米;Y坐标轴从下到上增加;X坐标轴从左向右增加 -
MM_TWIPS
页面空间中的每个逻辑单位映射到一个点的二十分之一(1/1440英寸);Y坐标轴从下到上增加;坐标轴总是等量缩放 -
MM_ISOTROPIC
页面空间中的每个逻辑单位映射到设备空间中应用程序定义的单元
坐标轴的方向由应用程序指定坐标轴不一定等量缩放 -
MM_ANISOTROPIC
页面空间中的每个逻辑单位映射到设备空间中应用程序定义的单元;坐标轴的方向由应用程序指定
单词METRIC (公制)和ENGLISH(英制)指的是两种比较通用的测量系统,LO和HI是低(Low)和高(High),指的是精度的高低。在排版中,一个点是一个基本测量单位,大约为1/72英寸,但是在图形程序设计中,通常假定它正好是1/72英寸,一个Twip是1/20点,也就是1/1440英寸。ISOTROPIC和ANISOTROPIC的意思分别是各向同性和各向异性。
前6种映射模式属于系统预定义映射模式,MM_ISOTROPIC和MM_ANISOTROPIC属于程序自定义映射模式。
在6种预定义的映射模式中,—种依赖于设备(MM_TEXT),其余(MM_HIENGLISH MM_LOENGLISH MM_HIMETRIC MM_LOMETRIC MM_TWIPS)称为度量映射模式,度量映射模式独立于设备,即与设备无关。
在6种预定义的映射模式中,X坐标轴都是从左向右增加﹔除了MM_TEXT映射模式Y坐标轴从上到下增加以外,其余5种的Y坐标轴都是从下到上增加。
要设置映射模式,需要调用SetMapMode函数﹔调用GetMapMode酗数可以获取DC的当前映射模式︰
int SetMapMode(
_In_ HDC hdc, //设备环境句柄
_In_int fnMapMode //8种映射模式之一
);
int GetMapMode(_In_ HDC hdc); //设备环境句柄
前面鲁多次提到过逻辑坐标和逻辑单位的概念,默认的页面坐标空间到设备坐标空间变换的映射模式是MM_TEXT
.1个逻辑单位就等于1像素,X坐标轴从左向右增加,Y坐标轴从上到下增加,这种映射模式直接映射到设备的坐标系。
回忆一下HelloWindows程序输出文本的那句TextOut调用︰TextOut(hdc,10,10,szStr,_tcslen(szStr));
,文本起始于距离客户区左上角向右10像素、向下10像素处。
如果设置为MM_LOENGLISH
映射模式,每个逻辑单位将映射为设备坐标空间中的0.01英寸(1英寸~2.54厘米)∶
SetMapMode(hdc,MM_LOENGLISH);
TextOut(hdc,100,-100,szStr,_tcslen(szStr));
文本起始于距离客户区左上角向右1英寸,向下1英寸处;Y坐标使用负值,因为在MM_LOENGLISH映射模式下,Y坐标轴从下到上增加。
如果不需要实现一些变换效果,大部分Windows程序可能不需要调用SetGraphicsMode(hdc,GMl_ADVANCED)后用世界坐标空间,也不需要MM_TEXT以外的映射模式,因为以像素为单位操作起来很方便。有些程序可能需要与设备无关的映射模式(例如度量映射模式MM_HIENGLISH MM_LOENGLISH·MM_HIMETRIC
MM_LOMETRIC·MM_TWIPS),假设电子表格程序提供图表功能,如果希望每个饼图的直径为2英寸,那么可以使用MM_LOENGLISH映射模式并调用绘图函数来绘制图表,这样一来图表的直径在任何显示器或打印机上都是一致的,前面说过像素的大小因设备而异。
几乎所有需要一个DC句柄hdc参数的GDI函数,都是使用逻辑单位。具体一个逻辑单位映射为多少像素或度量单位,取决于映射模式。也有个别不需要DC句柄的函数,例如创建画笔的CreatePen函数也是使用逻辑单位。
设备坐标系统
讲到这里,不得不说明设备坐标空间到物理设备坐标空间的变换问题。设备坐标空间到物理设备坐标空间的变换是由Windows控制的,没有设置设备坐标空间到物理设备坐标空间变换的函数,也没有获取相关数据的函数
这种变换的唯一目的是确保设备坐标空间的原点映射到物理设备上的适当位置。设备坐标空间到物理设备坐标空间(客户区、桌面或打印机纸张)的变换始终是一对一映射,它确保无论程序窗口在桌面上移动到何处,图形输出都可以正确显示在应用程序的窗口中。所以我认为设备坐标空间近似等于物理设备坐标空间。
映射模式是DC的一种属性,只有当使用以DC句柄作为参数的GDl函数时,才存在映射模式的概念,因此几乎所有的非GDI函数都使用设备坐标。
在图形对象最终输出之前,Windows会把在GDI函数中指定的逻辑坐标转换为设备坐标。
在Windows中有3种设备坐标系统∶屏幕坐标、全窗口坐标和客户区坐标。注意,在所有的设备坐标系统中,都是以像素为单位,水平方向上X值从左向右增加,垂直方向上Y值从上往下增加。
- 屏幕坐标∶很多函数的操作都是相对于屏幕的,比如创建一个程序窗口的CreateWindowEx函数,获取一个窗口位置、大小的
GetWindowRect函数,获取光标位置的GetCursorPos函数,MSG结构的pt字段(消息发生时的光标位置)等,都是使用屏幕坐标。 - 全窗口坐标︰全窗口坐标在Windows中用得不多,调用GetWindowDC函数获取的DC的原点是窗口的左上角而非客户区左上角。
- 客户区坐标︰这是最常使用的设备坐标系统,调用GetDC或BeginPaint函数获取的DC的原点是客户区左上角。
可以调用ClientToScreen函数把客户区坐标转换为屏幕坐标,调用ScreenToClient函数把屏幕坐标转换为客户区坐标∶
BOOL ClientToScreen(
_In_ HWND hWnd, //窗口句柄
lnout_LPPOINT lpPoint);//要转换的客户区坐标的点结构,函数返回后屏幕坐标将被复制到该结构中
BOOL ScreenToClient(
_In_ HWNDhWnd,//窗口句柄
_In_ out_LPPOINT IpPoint //要转换的屏幕坐标的点结构,函数返回后客户区坐标将被复制到该结构中
);
GetWindowRect函数获取指定窗口的尺寸,尺寸以屏幕坐标表示,相对于屏幕左上角∶
BOOL WINAPl GetWindowRect(
_In_ HWND hWnd, //窗口句柄
_Out_ LPRECT lpRect //接收窗口左上角和右下角屏幕坐标的RECT结构
);
全窗口坐标是相对于一个程序窗口的。如果想将一组点从相对于一个窗口的坐标空间转换(映射)到相对于另一个窗口的坐标空间,则可以调用MapWindowPoints函数︰
int MapWindowPoints(
_In_ HWND hWndFrom,
_In_ HWND hWndTo,
_Inout_ LPPOINT IpPoints,//指向POINT结构数组的指针,其中包含要转换的点
_In_ uINT cPoints // IpPoints参数指向的数组中POINT结构的数量
);
窗口和视口
映射模式定义了Windows如何将GDI函数中指定的逻辑坐标映射到设备坐标,这里的设备坐标系统取决于获取DC句柄所用的函数。
到目前为止,本书中"窗口"一词恐怕是出现次数最多的,读者都明白窗口的含义,但是本节讨论的窗口却是另外的含义。窗口指的是页面坐标空间的逻辑坐标系,窗口以逻辑坐标表示(可能是像素、毫米、英寸等)
视口指的是设备坐标空间的设备坐标系,视口以设备坐标(像素)表示。窗口和视口分别由原点、水平(X)范围和垂直(Y)范围组成,即窗口原点、窗口水平范围、窗口垂直范围、视口原点、视口水平范围、视口垂直范围、系统根据窗口和视口的原点、范围来进行页面坐标空间到设备坐标空间的变换。系统将窗口原点映射到视口原点,窗口范围映射到视口范围,如下图所示。
视口指的是设备坐标空间的设备坐标系,通常是客户区坐标,也可以是屏幕坐标或全窗口坐标,这取决于获取DC句柄所用的函数。
对于所有的映射模式,Windows使用下面的公式将窗口(逻辑)坐标转换为视口(设备)坐标∶
xViewport = (xwindow - xWinOrg) xViewExt / xWinExt + xViewOrg;
yViewport = (yWindow - yWinOrg) yViewExt / yWinExt + yViewOrg;
其中,点(xWindow, yWindow)是一个待转换的窗口逻辑点坐标,是逻辑单位;
点(xViewport, yViewport)是转换以后的视口设备点坐标,是设备单位,大多数情况下是客户区坐标。
点(xWinOrg, yWinOrg)是逻辑坐标系下的窗口原点,点(xViewOrg, yViewOrg)是设备坐标系下的视口原点,在默认情况下这两个点都被设置为(0,0),但是可以改变它们。
(xWinExt, yWinExt)是逻辑坐标系下的窗口范围;
(xViewExt, yViewExt)是设备坐标系下的视口范围。
在大多数的映射模式中,范围是由映射方式所隐含的,不能改变,每个范围本身并没有多大意义,但是可以看到逻辑单位转换为设备单位的换算因子是视口范围和窗口范围的比例。窗口原点和视口原点都可以改变,但不变的是窗口原点(xWinOrg, yWinOrg) ,它总是会被映射到视口原点(xViewOrg, yViewOrg) 。
如果窗口和视口的原点都停留在它们的默认值(0,0)上,则公式可以简化如下∶
xViewport = xWindow xViewExt / xWinExt;
yViewport = yWindow yViewExt / yWinExt;
为了加深理解,先介绍几个函数,然后举一个例子。
GetWindowExtEx函数可以获取指定DC的当前窗口的x范围和y范围,
GetViewportExtEx函数可以获取指定DC的当前视口的X范围和Y范围︰
BOOL GetWindowExtEx(
_In_ HDC hdc, //设备环境句柄
_Out_ LPSIZE lpSize); //接收窗口X范围和Y范围的SIZE结构的指针,逻辑单位
BOOL GetViewportExtEx(
_In_ HDC hdc, //设备环境句柄
_out_ LPSIZE lpSize); //接收视口X范围和Y范围的SIZE结构的指针,设备单位
GetDeviceCaps函数可以获取一些设备信息
int GetDeviceCaps(
_In_ HDC hdc, //设备环境句柄
_ln_ int nIndex //索引
);
参数nIndex指定索引,可用的索引值有37个,常用的值及含义如下表所示。
宏常量 | 含义 |
---|---|
HORZRES | 屏幕的宽度(像素),对于打印机则是页面可打印区域的宽度 |
VERTRES | 屏幕的高度(像素),对于打印机则是页面可打印区域的高度 |
HORZSIZE | 屏幕的物理宽度(毫米) |
LOGPIXELSX | 沿屏幕宽度每逻辑英寸的像素数 |
LOGPIXELSY | 沿屏幕高度每逻辑英寸的像素数 |
PLANES | 颜色平面数 |
BITSPIXEL | 每个像素的相邻颜色位数 |
我的笔记本是14英寸1366 768分辨率,设置映射模式为MM_LOENGLISH,看一看窗口范围和视口范围∶
//获取设备信息
nHorzRes = GetDeviceCaps(hdc, HORZRES); // 1366像素
nVertRes = GetDeviceCaps(hdc, VERTRES); // 768像素
nHorzSize = GetDeviceCaps(hdc, HORZSIZE); // 309毫米
nVertSize = GetDeviceCaps(hdc, VERTSIZE); // 174毫米
SetMapMode(hdc, MM_LOENGLISH);
GetWindowExtEx(hdc, &size); //{ cx = 1217 cy = 685 }
GetViewportExtEx(hdc, &size); //{ cx = 1366 cy = -768 }
GetWindowExtEx函数获取的窗口的X范围和Y范围分别是1217和685个0.01英寸,1英寸(in) ~25.4毫米(mm):
309 100 /25.4 = 1216.535个0.01英寸
174 100 / 25.4 = 685.039个0.01英寸
可知,当使用MM_LOENGLISH映射模式时,Windows将xViewExt设置为水平像素数,xWinExt表示以0.01英寸为单位被xViewExt像素占据的长度,它们的比例(xViewExt / xWinExt)表示每0.01英寸的像素数。为了提高转换性能,比例因子表示为整数,而不是浮点数。
- GetViewportExtEx函数获取的视口的X范围和Y范围分别是1366像素和-768像素,-768表示Y逻辑坐标轴从下到上增加。
下面的公式可以将视口(设备)坐标转换为窗口(逻辑)坐标∶
xWindow = (xViewport - xViewOrg) xWinExt / xViewExt + xWinOrg;
yWindow = (yViewport - yViewOrg) yWinExt / yViewExt + yWinOrg;
Windows提供了两个函数在设备坐标与逻辑坐标之间转换。DPtoLP函数将设备坐标转换为逻辑坐标,LPtoDP函数将逻辑坐标转换为设备坐标︰
BOOL DPtoLP(
_In_ HDC hdc, //设备环境句柄
_Inout_ LPPOINT IpPoints,//点结构数组的指针,将转换每个点结构中包含的X和Y坐标
_In_ int nCount); //点结构的个数
BOOL LPtoDP(
_In_ HDC hdc,
_In_ out_LPPOINT lpPoints,
_In_ intnCount);
例如∶
SetMapMode(hdc,MM_LOENGLISH);
GetClientRect(hwnd, &rect); //{ LT(O,O)RB(384,261)[384 x 261] }
DPtoLP(hdc, (LPPOINT)&rect, 2); //{ LT(O,0)RB(342,-233)[342 x - 233] }
TextOut(hdc, rect.right / 2, rect.bottom / 2, TEXT("映射模式"), _tcslen(TEXT("映射模式")));
文本会从客户区中间开始输出。GetClientRect函数获取的客户区大小为[384 261],经过DPtoLP函数转换以后变为[342-233],-233的负号大家应该懂了,但是客户区矩形尺寸怎么发生变化了呢?
因为一个是设备单位,一个是逻辑单位。
默认的MM_TEXT映射模式
默认的页面坐标空间到设备坐标空间变换的映射模式是MM_TEXT,1个逻辑单位映射为1像素,正X在右边,正Y在下面,这种映射模式直接映射到设备的坐标系。在MM_TEXT映射模式下,默认的窗口、视口原点和范围如下︰
- 窗口原点∶(0,0)可以改变
- 视口原点∶(0,0)可以改变
- 窗口范围∶(1,1)不可改变
- 视口范围∶(1,1)不可改变
窗口和视口范围都设置为1,不可改变,即创建一对一映射,在逻辑坐标和设备坐标之间没有执行缩放;窗口原点和视口原点可以改变,即逻辑坐标到设备坐标的变换支持平移。
MM_TEXT称为文本映射模式,这是因为坐标轴的方向与文本读写类似,我们读文本的顺序是从左到右、从上到下,MM_TEXT以同样的方式定义坐标轴上值的增长方向,如下图所示。
前面给出的窗口(逻辑)坐标转换为视口(设备)坐标的公式可以简化为以下形式∶
xViewport = xWindow - xWinOrg + xViewOrg;
yViewport = yWindow - yWinOrg + yViewOrg;
MM_TEXT映射模式下的窗口原点和视口原点都可以改变,可以通过SetWindowOrgEx函数和SetViewportOrgEx函数来改变窗口原点和视口原点。
SetWindowOrgEx函数把指定的窗口逻辑坐标点映射到视口原点(0,0),然后逻辑点(0,0)不再指向客户区左上角;
SetViewportOrgEx函数把指定的视口设备坐标点映射到窗口原点(0,0),然后逻辑点(0,0)不再指向客户区左上角。
BOoL SetWindowOrgEx(
_In_HDC hdc, //设备环境句柄
_In_ int x, //新窗口原点的X坐标,逻辑单位
_In_ int Y, //新窗口原点的Y坐标,逻辑单位
_out_LPPOINT IpPoint); //在这个POINT结构中返回窗口的上一个原点
BOOL SetViewportOrgEx(
_In_ HDC hdc, //设备环境句柄
_In_ int x, //新视口原点的X坐标,设备单位
_In_ intY,//新视口原点的Y坐标,设备单位
_out_ LPPOINT lpPoint); //在这个POINT结构中返回视口的上一个原点
换句话说,如果将窗口原点改为(xWinOrg, yWinOrg),逻辑点(xWinOrg, yWinorg)将会被映射到设备点(0,0),即客户区左上角;如果将视口原点改为(xViewOrg, yViewOrg),那么逻辑点(0,O)将会被映射到设备点(xViewOrg, yViewOrg)。窗口原点始终会映射到视口原点。
SetWindowOrgEx和SetViewportOrgEx函数的作用是平移坐标轴,这两个函数只使用一个即可,应该尽量避免两个函数同时使用。不管怎么调用SetWindowOrgEx或SetViewportOrgEx函数改变窗口或视口原点,设备点(0,0)始终是客户区左上角,但是GDI图形输出函数使用的是逻辑坐标。
举个例子,假设客户区宽度为cxClient像素,高度为cyClient像素,如果想定义逻辑点(0,0)为客户区的中心,可以这样∶
SetViewportOrgEx(hdc, cxClient / 2, cyClient / 2,NULL);
SetViewportOrgEx函数的XY参数均是设备单位,上面的函数调用表示将逻辑点(0,0)映射到设备点(cxClient/2, cyClient/2)。现在的逻辑坐标系(是说逻辑坐标) 如下图所示。
逻辑X轴的范围是 cxClient/2~cxClient/2,逻辑Y轴的范围是 cyClient/2~cyClient/2,客户区右下角的逻辑坐标为(cxClient/2, cyClient/2)。如果想在客户区的左上角,也就是设备坐标点(0,0)处开始显示文本,则需要使用负的逻辑坐标
TextOut(hdc,
-cxClient / 2,
-cyClient / 2,TEXT("Hello MM_TEXT"),_tcslen(TEXT("Hello MM_TEXT")));
因为使用设备单位比较方便,所以我个人倾向于使用SetViewportOrgEx函数,当然可以通过调用SetWindowOrgEx函数取得相同的效果,把逻辑点(-cxClient/2,-cyClient/2)映射到视口原点(0,0),也就是客户区的左上角.
SetWindowOrgEx(hdc, -cxClient / 2, -cyClient / 2,NULL);//MM_TEXT映射模式下1个逻辑单位就是1像素
SetViewportOrgEx函数的X·Y参数均是逻辑单位,但是在MM_TEXT映射模式下1个逻辑单位映射为1像素,默认的页面坐标空间到设备坐标空间变换的映射模式就是MM_TEXT。如果需要使用MM_TEXT映射模式,则不需要调用SetMapMode(hdc,MM_TEXT);
SetWindowOrgEx和SetViewportOrgEx函数的作用是平移坐标轴,响应滚动条滚动请求的时候,可以通过调用这两个函数达到滚动客户区内容的目的。打开前面SystemMetrics2
项目,在WM_PAINT消息中是这样根据垂直滚动条的当前位置输出文本的︰
for (int i = 0; i < NUMLINES; i++)
{
y = s_iHeight * (i - s _ivscrollPos);
//输出文本
}
调用SetWindowOrgEx函数也可以获得同样的效果︰
SetWindowOrgEx(hdc,0, s_iHeight * s_iVscrollPos, NULL);
for (int i = o; i <NUMLINES; i++)
{
y = s_iHeight * i;
//输出文本
}
度量映射模式
在6种系统预定义的映射模式中,MM_TEXT依赖于设备,其余5种MM_HIENGLISH MM_LOENGLISH MM_HIMETRIC MM_LOMETRIC MM_TWIPS 称为度量映射模式,度量映射模式提供了与设备无关的逻辑单位。MM_TEXT映射模式Y坐标轴从上到下增加,度量映射模式的Y坐标轴都是从下到上增加的。有些程序可能需要与设备无关的度量映射模式,例如CAD程序通常以毫米为单位。
这5种映射模式按照从低精度到高精度依次如下表所示。为了对照,最后一列以毫米(mm)为单位表示了该逻辑单位的大小。
-
MM_LOENGLISH0.01in0.254
-
MM_LOMETRIC0.1mm0.1
-
MM_HIENGLISH0.001in0.0254
-
MM_TWIPS1/1400in0.0176
-
MM_HIMETRICO.01mm0.01
在讲解窗口和视口的时候,我用14英寸1366 768分辨率的笔记本,设置映射模式为MM_LOENGLISH做了测试,当时的窗口范围是{ cx=1217 cy=685 },视口范围是{ cx=1366 cy=-768},比例xViewExt / xWinExt表示每0.01英寸的像素数。
现在我说,这5种映射模式的窗口范围和视口范围取决于哪种映射模式和设备分辨率是多少,相信大家可以理解这句话。范围本身并不重要,只有把它们表达为一个比值时才有意义,逻辑单位转换为设备单位的换算因子是视口范围和窗口范围的比例。
在我的计算机上我一一测试每种映射模式的窗口范围,视口范围当然都是{ cx=1366 cy=-768},如下表所示。
- MM_LOENGLISH{ cx=1217 cy=685 }个0.01in{ cx=1366 cy=-768 }
- MM_LOMETRIC{ cx=3090 cy=1740 }个0.1mm{ cx=1366 cy=-768 }
- MM_HIENGLISH{ cx=12165 cy=6850 }个0.001 in{ cx=1366 cy=-768 }
- MM_TWIPS{ cx=17518 cy=9865}个1/1400 in{ cx=1366 cy=-768 }
- MM_HIMETRIC{ cx=30900 cy=17400 }个0.01mm{ cx=1366 cy=-768 }
- MM_TEXT{ cx=1 cy=1 }{ cx=1 cy=1 }
在这里只需知道视口范围Y值前面的负号是什么意思,表示Y逻辑坐标轴从下到上增加。除此之外,这5种映射模式的用法和MM_TEXT没有多大不同。和MM_TEXT一样,窗口原点和视口原点可以改变,即逻辑坐标到设备坐标的变换支持平移。
当改变为5种映射模式之一时,逻辑坐标系统如下图所示。
如果需要在客户区显示图形对象,只能使用负的Y坐标值,例如∶
SetMapMode(hdc,MM_LOENGLISH);
TextOut(hdc,100,-100,TEXT("Hello"),_tcslen(TEXT("Hello")));
文本将显示在距离客户区左上角右边和下面各1英寸的地方。再例如,在距离客户区左上角1/4~3/4的区域画一个椭圆:
SetMapMode(hdc, MM_LOENGLISH);
GetClientRect(hwnd, &rect); //{ LT(O,O)RB(384,261)[384 x 261] }
DPtoLP(hdc, (LPPOINT)&rect, 2); //{ LT(O,0)RB(342,-233)[342 x - 233] }
Ellipse(hdc, rect.right / 4, rect.bottom / 4, rect.right * 3 / 4, rect.bottom * 3 / 4);
对于这5种映射模式,DPtoLP函数为我们提供了很大的便利。
自定义映射模式
MM_ISOTROPIC和MM_ANISOTROPIC属于程序自定义的映射模式,对于6种预定义的映射模式MM_TEXT MM_HIENGLISH MM_LOENGLISH MM_HIMETRIC MM_LOMETRIC和
MM_TWIPS),当调用SetMapMode函数设置为其中一种映射模式时,系统将设置其窗口范围和视口范围,这两个范围不能改变。其他两种映射模式(MM_ISOTROPIC和MM_ANISOTROPIC)要求我们自己指定范围,这是通过调用SetWindowExtEx和SetViewportExtEx函数来实现的;
对于MM_ISOTROPIC映射模式,在调用SetViewportExtEx之前通常先调用SetWindowExtEx函数设置DC的窗口水平和垂直范围,SetViewportExtEx设置DC的视口水平和垂直范围.
BooL SetWindowExtEx(
_In_ HDC hdc, //设备环境句柄
_In_ int nXExtent,//窗口的水平范围,逻辑单位
_In_intnYExtent,//窗口的垂直范围,逻辑单位
_out_LPSIZE lpSize); //在这个SIZE结构中返回以前的窗口范围,可以设置为NULL
BOoL SetViewportExtEx(
_In_ HDC hdc, //设备环境句柄
_In_ int nXExtent, //视口的水平范围,设备单位
_In_ int nYExtent, //视口的垂直范围,逻辑单位
_out_LPSIZE lpSize); //在这个SIZE结构中返回以前的视口范围,可以设置为NULL
MM_ISOTROPIC映射模式可以确保X方向和Y方向的逻辑单位相同,而MM_ANISOTROPIC映射模式则允许X方向和Y方向的逻辑单位不同。逻辑单位相同的意思是两个轴上的逻辑单位表示相等的物理距离。
MM ISOTROPIC映射模式
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
HDC hdc;
PAINTSTRUCT ps;
RECT rect;
SIZE size;
switch (uMsg)
{
case WM_PAINT:
hdc = BeginPaint(hwnd, &ps);
// 设置映射模式为MM_ISOTROPIC
SetMapMode(hdc, MM_ISOTROPIC);
//窗口范围设置为(100, 100)
SetWindowExtEx(hdc, 100, 100, &size); //size返回{ cx = 3090 cy = 1740 }
// 视口范围设置为客户区的宽度和高度
GetClientRect(hwnd, &rect); //{ LT(0,0)RB(384,261)[384 x 261] }
SetViewportExtEx(hdc, rect.right, rect.bottom, &size); // size返回{ cx = 1366 cy = -768 }
//获取窗口范围和视口范围
GetWindowExtEx(hdc, &size); //{ cx = 100 cy = 100 }
GetViewportExtEx(hdc, &size); //{ cx = 261 cy = 261 }
//画一个和客户区宽度或高度相同大小的圆
Ellipse(hdc, 0, 0, 100, 100);
EndPaint(hwnd, &ps);
return 0;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
我们调用SetWindowExtEx函数把窗口的水平范围和垂直范围都设置为100逻辑单位
调用SetViewportExtEx函数把视口的水平范围和垂直范围设置为客户区的宽度和高度,此处为[384 261],
没有使用-261,这说明我们希望逻辑坐标和MM_TEXT一样,X坐标轴从左向右增加,Y坐标轴从上到下增加。
通常,在调用SetWindowExtEx函数时,要把窗口范围设置为期望得到的逻辑窗口的逻辑大小;而在调用SetViewportExtEx函数时,则把视口范围设置为客户区的实际宽度和高度。
调用SetWindowExtEx函数以后在size参数中返回原窗口范围为{ cx=3090 cy=1740 },调用SetViewportExtEx函数以后在size参数中返回原视口范围为{ cx=1366 cy=-768 }。在讲解度量映射模式的时候,我曾经在我的计算机上一一测试每种度量映射模式的窗口和视口范围,可以看出当设置映射模式为MM_ISOTROPIC时,Windows使用与MM_LOMETRIC映射模式相同的窗口和视口范围,不过不要依赖这一事实。
设置好窗口范围和视口范围以后,我们调用GetWindowExtEx和GetViewportExtEx函数分别获取窗口范围和视口范围,窗口范围还是我们设置的{ cx=100 cy=100 },视口范围变为{ cx=261 cy=261 },即Windows取客户区宽度和高度中较小的一个。Windows之所以会调整它们的值,是为了让X和Y坐标轴上的逻辑单位表示相同的物理尺寸。当Windows调整这些范围时,它必须让逻辑窗口可以容纳在对应的物理视口之内,这就有可能导致X轴或Y轴的一部分客户区落在逻辑窗口的外面。
有什么意义呢?现在我们随意拖拉调整程序窗口的宽度或高度,程序都会在客户区的左上角显示一个直径为客户区宽度或高度的正圆。请读者把映射模式设置为MM_ANISOTROPIC,看一看又会是什么现象。
上面的圆形总是位于客户区的左上角,我们希望不管窗口大小如何调整,圆形总是位于客户区的中心,可以实现一个四象限二维笛卡儿坐标系。逻辑点(0,0)位于客户区的中心,4个方向的轴可以任意缩放,×轴向右增加,Y轴向上增加。可以使用下面的代码∶
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
HDC hdc;
PAINTSTRUCT ps;
RECT rect;
SIZE size;
switch (uMsg)
{
case WM_PAINT:
hdc = BeginPaint(hwnd, &ps);
// 设置映射模式为MM_ISOTROPIC
SetMapMode(hdc, MM_ISOTROPIC);
//窗口范围设置为(100, 100)
SetWindowExtEx(hdc, 100, 100, &size); //size返回{ cx = 3090 cy = 1740 }
// 视口范围设置为客户区的宽度和高度
GetClientRect(hwnd, &rect);
SetViewportExtEx(hdc, rect.right, -rect.bottom, &size); // -rect.bottom
//设置视口原点
SetViewportOrgEx(hdc,rect.right /2 ,rect.bottom /2 ,NULL);
//画一个和客户区宽度或高度相同大小的圆
Ellipse(hdc, -50,50, 50,-50);
EndPaint(hwnd, &ps);
return 0;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
如果客户区的宽度大于高度,那么逻辑坐标如下图所示。
如果客户区的高度大于宽度,那么逻辑坐标如下图所示。
但是,在窗口或视口的范围中,Windows并没有实现裁剪。当调用GDI函数时,仍然可以随意使用小于-50或大于50的逻辑X和Y值。
MM_ANISOTROPIC映射模式
Windows不会对使用MM_ANISOTROPIC映射模式设置的窗口和视口范围做任何调整,该模式允许X方向和Y方向的逻辑单位不同。再看一下前面MM_ISOTROPIC映射模式画圆的部分代码∶
// 设置映射模式为MM_ISOTROPIC
SetMapMode(hdc, MM_ISOTROPIC);
//窗口范围设置为(100, 100)
SetWindowExtEx(hdc, 100, 100, &size);
// 视口范围设置为客户区的宽度和高度
GetClientRect(hwnd, &rect);
SetViewportExtEx(hdc, rect.right, -rect.bottom, &size); // -rect.bottom
//设置视口原点
SetViewportOrgEx(hdc,rect.right /2 ,rect.bottom /2 ,NULL);
使用MM_ISOTROPIC映射模式,上面的代码会导致X轴或Y轴的一部分客户区落在逻辑窗口的外面;而使用MM_ANISOTROPIC映射模式,不管客户区的尺寸如何调整,窗口范围(100,100)总是覆盖整个客户区的宽度和高度,如果客户区不是正方形的,那么X和Y轴的每个逻辑单位会有不同的物理尺寸。
学习了这么多,读者应该明白一个函数是使用逻辑单位还是设备单位。几乎所有GDI函数,需要一个DC句柄hdc参数的函数,以及操作GDI图形对象的函数,它们都是使用逻辑单位。具体一个逻辑单位映射为多少像素或度量单位,取决于映射模式。此外,与屏幕、窗口或者客户区有关的函数都是使用设备单位。
度大于宽度,那么逻辑坐标如下图所示。
[外链图片转存中…(img-RydHwc2E-1707037593165)]
但是,在窗口或视口的范围中,Windows并没有实现裁剪。当调用GDI函数时,仍然可以随意使用小于-50或大于50的逻辑X和Y值。
MM_ANISOTROPIC映射模式
Windows不会对使用MM_ANISOTROPIC映射模式设置的窗口和视口范围做任何调整,该模式允许X方向和Y方向的逻辑单位不同。再看一下前面MM_ISOTROPIC映射模式画圆的部分代码∶
// 设置映射模式为MM_ISOTROPIC
SetMapMode(hdc, MM_ISOTROPIC);
//窗口范围设置为(100, 100)
SetWindowExtEx(hdc, 100, 100, &size);
// 视口范围设置为客户区的宽度和高度
GetClientRect(hwnd, &rect);
SetViewportExtEx(hdc, rect.right, -rect.bottom, &size); // -rect.bottom
//设置视口原点
SetViewportOrgEx(hdc,rect.right /2 ,rect.bottom /2 ,NULL);
使用MM_ISOTROPIC映射模式,上面的代码会导致X轴或Y轴的一部分客户区落在逻辑窗口的外面;而使用MM_ANISOTROPIC映射模式,不管客户区的尺寸如何调整,窗口范围(100,100)总是覆盖整个客户区的宽度和高度,如果客户区不是正方形的,那么X和Y轴的每个逻辑单位会有不同的物理尺寸。
学习了这么多,读者应该明白一个函数是使用逻辑单位还是设备单位。几乎所有GDI函数,需要一个DC句柄hdc参数的函数,以及操作GDI图形对象的函数,它们都是使用逻辑单位。具体一个逻辑单位映射为多少像素或度量单位,取决于映射模式。此外,与屏幕、窗口或者客户区有关的函数都是使用设备单位。