【C++】Win32 API中窗口尺寸与坐标操作(GetWindowRect等函数)理解与注意点

1 篇文章 0 订阅

鸽了快两个月没更新文章,前段时间期末考,最近一个月才在认真钻研计算机(づ ̄ 3 ̄)づ

编写C++ Win32窗口项目的时候时不时都要和窗口尺寸坐标及相关操作打交道,我在实际使用过程中经常发现尺寸的实际数值、理想数值和用代码获取的数值不匹配,经过几番查资料和实践,在这篇文章简单总结一下窗口尺寸与坐标相关常用函数的使用和理解

获取相关

GetWindowRect

放一下微软官方文档里对这个函数的介绍和函数原型

BOOL GetWindowRect(
  [in]  HWND   hWnd,
  [out] LPRECT lpRect
);

参数我想大家都挺明确了,一个是窗口句柄,一个是指向Rect的指针。
作用是检索指定窗口的边界矩形的尺寸。 尺寸以相对于屏幕左上角的屏幕坐标提供。意思就是以屏幕左上角为原点,获取窗口左上角和右下角的坐标(包括非客户区和阴影),并存入矩形中。

我们用一个640x480的窗口,把它生成在(100, 100)的位置来测试一下:

hwnd = CreateWindow(szAppName, TEXT("Window"), WS_OVERLAPPEDWINDOW,
        100, 100, 640, 480,
        NULL, NULL, hInstance, NULL);
        
RECT rect;
GetWindowRect(hwnd, &rect);

rect的结果是{ 100, 100, 740, 580 },符合我们的思路,左上角是(100,100),加上宽高后右下角是(740, 580)

GetClientRect

BOOL GetClientRect(
  [in]  HWND   hWnd,
  [out] LPRECT lpRect
);

参数和上一个函数一样,不多说了。作用是获取窗口工作区的坐标。 客户端坐标指定工作区的左上角和右下角。 由于客户端坐标相对于窗口工作区的左上角,因此左上角坐标为 (0,0)。
简单来讲,就是以客户区左上角为原点,获取客户区的左上角和右下角坐标,并存入矩形,这里我们获取的是客户区,又是以客户区左上角为原点,所以Rect的前两个值就一定是0,后面两个值是右下角坐标,即客户区的宽高。

同样用刚刚640x480那个窗口来测试

RECT rect;
GetClientRect(hwnd, &rect);

结果是{ 0, 0, 624, 441 },后两个值是非客户区的宽高,即窗口大小减去阴影和非客户区的大小

前两个函数使用上的注意点

  • GetWindowRect获取的是包括阴影的窗口大小和位置
    我之前一直疑惑窗口标题栏占整个窗口的宽的大小并不大,为什么实际获取到的客户区的宽会减少这么多(参考前面窗口宽是640,获取到的客户区宽就624)
    查找了文档才发现问题,原话如下:在 Windows Vista 及更高版本中,Window Rect 现在包括落影所占用的区域。
    如前文所描述,GetWindowRect获取的大小包括非客户区和阴影,这个“阴影”也占据了一块不小的空间,所以实际上GetWindowRect返回某个窗口的宽高其实是加上了窗口阴影边缘的,实际的窗口并没有640x480这么大,并且实际的起始位置也要在阴影后

我们可以在窗口内创建一个起始坐标是(0, 0)的按钮来证明上面的结论:

HWND button = CreateWindow(L"Button", L"按钮一", WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON,
        0, 0, 120, 60, hwnd, (HMENU)IDB_ONE, NULL, NULL);
        
RECT rect;
GetWindowRect(button, &rect);

执行结果:
执行结果

rect返回出来的值是{ 108, 131, 228, 191 },可以看到大小确实和我们生成的一样,是120x60,但是按钮左上角的屏幕坐标是(108, 131),按钮贴着窗口左侧,横坐标却多出来8像素,这就是因为窗口加上阴影的最左边横坐标才是100,加上阴影的大小和非客户区的一点点大小才是实际的108

因为窗口左右两边非客户区和阴影相加是一样大的,所以我们也能用窗口的客户区的宽反过来计算出108:100 + (640 - 624) / 2 = 108

我们还可以尝试使用GetClientRect获取按钮的客户区大小,因为按钮没有阴影也没有非客户区,所以理论上按钮获得到的客户区大小应该和生成大小相等

RECT rect;
GetClientRect(button, &rect);

结果是{ 0, 0, 120, 60 },完全符合预期

  • 使用屏幕截图功能获取到的大小偏大
    这个点其实就是文章开头说的“实际数值与代码数值不匹配”,还是用刚刚的窗口来看
    截图获取的窗口偏大
    用QQ的截图功能截取这块窗口,发现窗口的大小为782x591,完全偏离了代码所生成的大小
    造成这个问题的原因其实是我使用的笔记本电脑将屏幕放大到了原来的125%,代码是以缩放后的大小生成的窗口(1920x1080缩放成1536x864),但是截图功能仍把屏幕当成1920x1080的看,所以用截图功能看窗口大小也被放大到了125%,把782和591分别除1.25,就和代码生成的值也非常相近了(差了一点是因为有阴影)

ScreenToClient

刚刚我们做了那个按钮的实验,发现只能获取按钮相对于屏幕的坐标,如果要获取按钮相对于某个客户区的坐标,那就要使用ScreenToClient函数

BOOL ScreenToClient(
  [in] HWND    hWnd,
       LPPOINT lpPoint
);

介绍:ScreenToClient 函数将屏幕上指定点的屏幕坐标转换为工作区坐标。

参数:
1.hWnd 窗口的句柄,该窗口的工作区将用于转换。
2.lpPoint 指向 POINT 结构的指针,该结构指定要转换的屏幕坐标。

使用非常简单,把要当作原点的客户区的窗口句柄给第一个参数,把要转换的点存入POINT结构体然后把地址传给第二个参数就行了。

以刚刚的按钮为例,获取按钮相对于父窗口客户区的坐标:

RECT rect;
GetWindowRect(button, &rect);
POINT point = { rect.left, rect.top }; //按钮的左上角屏幕坐标
ScreenToClient(hwnd, &point);

最终获取到point是{ 0, 0 },和我们预期一样,大家也可以改一下按钮的生成坐标对比获取结果。

ClientToScreen

这个函数和ScreenToClient相反,就是将指定点的工作区坐标转换为屏幕坐标。使用方法相同,这里就不介绍了

我们就把刚刚从屏幕坐标转过来的客户区坐标再转回去:

RECT rect;
GetWindowRect(button, &rect);
POINT point = { rect.left, rect.top }; //按钮的左上角屏幕坐标
ScreenToClient(hwnd, &point);
ClientToScreen(hwnd, &point);

获取到的point为{ 108, 131 },和我们刚刚用GetWindowRect获得到的按钮屏幕坐标相同。

操作相关

如果理解了上面的那些屏幕坐标,客户区坐标,阴影等等那些概念,那操作的函数就更简单了

CreateWindow、MoveWindow与SetWindowPos

这里不具体介绍这三个函数如何使用,因为不属于文章的范畴
把这三个函数放在一起讲是因为这些函数都能对窗口大小及坐标进行改变(创建),并且基于的是“同一原则”。
第一个是改变大小时,传入的值都是包括阴影的窗口大小,即实际窗口大小要小于代码传入生成大小。
第二个是改变坐标时,如果窗口是顶层窗口(即此窗口没有父窗口),则以屏幕左上角为原点设置坐标,如果窗口有父窗口,则以父窗口客户区坐标为原点设置坐标

AdjustWindowRect

根据前文的描述,我们代码传入创建窗口的值和实际大小并不对应,但如果我们要创建和传入值匹配的窗口,我们就需要使用AdjustWindowRect函数

函数原型:

BOOL AdjustWindowRect(
  [in, out] LPRECT lpRect,
  [in]      DWORD  dwStyle,
  [in]      BOOL   bMenu
);

文档介绍:根据所需的客户区矩形大小计算窗口矩形的所需大小。 然后,窗口矩形可以传递给 CreateWindow 函数,以创建其工作区为所需大小的窗口。
非常好理解,我们把客户区大小存入矩形,然后使用这个函数能帮我们反推出传给前文提到改变(或创建)函数的值,这样创建出来的窗口客户区大小一定是我们想要的。

函数参数:
1.lpRect 存放客户区大小的矩形
2.dwStyle 窗口样式
3.bMenu 指示窗口是否具有菜单
很容易理解后面两个参数的作用:窗口样式和窗口是否具有菜单在客户区大小不改变的情况下会影响窗口整体大小

我们直接获取刚刚640x480窗口的客户区矩形,然后再通过这个函数验证是否能正确反推

RECT rect;
GetClientRect(hwnd, &rect);
AdjustWindowRect(&rect, WS_OVERLAPPEDWINDOW, false);

rect结果是{ -8,-31, 632, 449 },虽然坐标受阴影和非客户区的影响有点不正确,但是这里我们只需要宽高,即632-(-8)和449-(-31),为640x480,结果正确

这篇文章其实是在完善光栅器和封装winapi过程中遇到一点细节问题因此记录下来,希望这篇文章对你有帮助,如果文章内容有错误也请多指教!

  • 5
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
好的,以下是使用GetWindowRect获取窗口坐标并绘制边框的C代码: ``` #include <Windows.h> LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam); int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { // 创建窗口 WNDCLASS wc = { 0 }; wc.lpfnWndProc = WndProc; wc.hInstance = hInstance; wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); wc.lpszClassName = "MyWindowClass"; RegisterClass(&wc); HWND hWnd = CreateWindow("MyWindowClass", "My Window", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 500, 500, NULL, NULL, hInstance, NULL); // 显示窗口 ShowWindow(hWnd, nCmdShow); UpdateWindow(hWnd); // 消息循环 MSG msg = { 0 }; while (GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } return (int)msg.wParam; } LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { switch (message) { case WM_PAINT: { // 获取窗口坐标 RECT rect; GetWindowRect(hWnd, &rect); // 转换为客户区坐标 POINT pt1 = { rect.left, rect.top }; POINT pt2 = { rect.right, rect.bottom }; ScreenToClient(hWnd, &pt1); ScreenToClient(hWnd, &pt2); rect.left = pt1.x; rect.top = pt1.y; rect.right = pt2.x; rect.bottom = pt2.y; // 绘制边框 HDC hdc = GetDC(hWnd); HPEN hPen = CreatePen(PS_SOLID, 3, RGB(255, 0, 0)); SelectObject(hdc, hPen); SelectObject(hdc, GetStockObject(NULL_BRUSH)); Rectangle(hdc, rect.left, rect.top, rect.right, rect.bottom); DeleteObject(hPen); ReleaseDC(hWnd, hdc); } break; case WM_DESTROY: PostQuitMessage(0); break; default: return DefWindowProc(hWnd, message, wParam, lParam); } return 0; } ``` 这段代码会创建一个窗口,并在窗口的客户区绘制一个红色边框,边框的大小和位置与窗口相同。使用GetWindowRect获取窗口坐标后,需要将其转换为客户区坐标,然后使用Rectangle函数绘制矩形边框。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值