前两天简单说了下标题栏菜单栏合为一体时有哪些解决方案。同时也尝试着解决了一些方案中存在的问题。其中关于最大化的问题一直存留还未解决。今天就来说说如何解决这些和窗口放缩相关的问题。
放缩首先要解决的一个问题是,如何保证在最大化时不覆盖任务栏,怎样可以动态地获得除去任务栏以外的桌面工作区域?要获得这部分区域,我们需要借助SystemParametersInfo函数并传递SPI_GETWORKAREA。解决了区域问题,那在最大化的什么时候去设置这个区域大小呢,WM_SIZE?如果在WM_SIZE时候去设置最大化的话,你会发现这是没有用的。这里,我们要倚仗Windows的另外一个消息:WM_GETMINMAXINFO。直接套用MSDN的话:
Sent to a window when the size or position of the window is about to change. An application can use this message to override the window's default maximized size and position, or its default minimum or maximum tracking size.
不多说了,直接看代码吧:
1: void Cls_OnGetMinMaxInfo(HWND hwnd, LPMINMAXINFO lpMinMaxInfo)
2: {
3: FORWARD_WM_GETMINMAXINFO(hwnd, lpMinMaxInfo, DefWindowProc);
4:
5: RECT rcWorkArea = {0, 0, 0, 0};
6: SystemParametersInfo(SPI_GETWORKAREA, 0, &rcWorkArea, 0);
7:
8: lpMinMaxInfo->ptMaxSize.x = rcWorkArea.right - rcWorkArea.left;
9: lpMinMaxInfo->ptMaxSize.y = rcWorkArea.bottom - rcWorkArea.top;
10: lpMinMaxInfo->ptMaxPosition.x = rcWorkArea.left;
11: lpMinMaxInfo->ptMaxPosition.y = rcWorkArea.top;
12: }
逻辑没什么好细说的。运行程序你会发现,可以正常最大化了。但是在Win7 Aero效果下,还是会发现一点点的小问题。边框在最大化时仍旧显示出来了。我们知道,Win7玻璃效果下,窗口的一些熟悉都由WDM来管理了。这个东西没怎么接触过的话,还是比较头疼的事情。整个边框的厚度从XP时代简单的border width演进到现在的border width加border padding了。通过SystemParametersInfo加上SPI_GETNONCLIENTMETRICS可以获得这些非客户区相关的所有属性值。另外,我在使用这个API的时候还发现返回来的NONCLIENTMETRICS::iPaddedBorderWidth始终都是0,不管你有没有通过系统设置过Border Padding。并且NONCLIENTMETRICS::iBorderWidth的值等于我们前面说的border width+border padding?不知都是不是我理解错了还是其他。同时,GetSystemMetrics(SM_CXPADDEDBORDER)同样返回0。真的是相当的奇怪。
回过来说边框,先说简单的,在XP时代,事实上一个窗口的边框大小(厚度)是由很多因素决定的。WS_BORDER可以给窗口加边框,WS_THICKFRAME也可以。通过SetSystemMetrics我们知道至少有3种以上的边框:SM_CXBORDER、SM_CXDLGFRAME(SM_CXFIXEDFRAME)、SM_CXEDGE和SM_CXFRAME(SM_CXSIZEFRAME)等。就本文来说,相关的边框有2中:SM_CXBORDER和SM_CXSIZEFRAME。
经过一番实验发现的一个现象是当窗口包含SM_CXBORDER风格时,边框大小由SM_CXSIZEFRAME决定;当SM_CXBORDER风格被拿走后,边框大小等于SM_CXSIZEFRAME-SM_CXBORDER。这个结论可以通过打断点在上面的代码行5,并观察lpMinMaxInfo所保存的值来得出。
既然如此,那么上面的代码就需要修改为:
1: void Cls_OnGetMinMaxInfo(HWND hwnd, LPMINMAXINFO lpMinMaxInfo)
2: {
3: FORWARD_WM_GETMINMAXINFO(hwnd, lpMinMaxInfo, DefWindowProc);
4:
5: RECT rcWorkArea = {0, 0, 0, 0};
6: SystemParametersInfo(SPI_GETWORKAREA, 0, &rcWorkArea, 0);
7:
8: lpMinMaxInfo->ptMaxSize.x = rcWorkArea.right - rcWorkArea.left;
9: lpMinMaxInfo->ptMaxSize.y = rcWorkArea.bottom - rcWorkArea.top;
10: lpMinMaxInfo->ptMaxPosition.x = rcWorkArea.left;
11: lpMinMaxInfo->ptMaxPosition.y = rcWorkArea.top;
12:
13: int nCxSizeFrm = GetSystemMetrics(SM_CXSIZEFRAME);
14: int nCySizeFrm = GetSystemMetrics(SM_CYSIZEFRAME);
15:
16: lpMinMaxInfo->ptMaxSize.x += 2 * nCxSizeFrm;
17: lpMinMaxInfo->ptMaxSize.y += 2 * nCySizeFrm;
18:
19: lpMinMaxInfo->ptMaxPosition.x -= nCxSizeFrm;
20: lpMinMaxInfo->ptMaxPosition.y -= nCySizeFrm;
21:
22: DWORD dwStyle = GetWindowLongPtr(hwnd, GWL_STYLE);
23: if (0 == (dwStyle & WS_BORDER))
24: {
25: // 没有WS_BORDER风格时,边框的厚度:SM_CXSIZEFRAME - SM_CXBORDER
26: int nCxBorder = GetSystemMetrics(SM_CXBORDER);
27: int nCyBorder = GetSystemMetrics(SM_CYBORDER);
28:
29: lpMinMaxInfo->ptMaxSize.x -= 2 * nCxBorder;
30: lpMinMaxInfo->ptMaxSize.y -= 2 * nCyBorder;
31:
32: lpMinMaxInfo->ptMaxPosition.x += nCxBorder;
33: lpMinMaxInfo->ptMaxPosition.y += nCyBorder;
34: }
35: }
运行下,OK完美了,和普通情况下的最大化没啥两样了。但是,真心告诉你,不要高兴太早。我们还有一种情况没有考虑到。当用户在安装有两个以上的显示器时,这个程序可以很正常地工作么?告诉你吧,不正常的。特别是两个显示器的分辨率不一致时,就更明显了。怎么办?只有继续分析了解决了。
涉及到多窗口,我们要用到HMONITOR类型的句柄。具体关于多显示的知识请猛击这里:Multiple Display Monitors。我们这儿就不多费口舌介绍多显示器相关的知识了,还是着手先解决问题。
先说下可能用到的API。其实和多显示器相关的API也不多,大概六个吧:
- EnumDisplayMonitors
- GetMonitorInfo
- MonitorEnumProc
- MonitorFromPoint
- MonitorFromRect
- MonitorFromWindow
光从名字看,我们可以猜测GetMonitorInfo是个必要函数。可能几个MonitorFromXXXX中,还是最后一个比较好用。好吧一起来,先写出来试试看:
1: void Cls_OnGetMinMaxInfo(HWND hwnd, LPMINMAXINFO lpMinMaxInfo)
2: {
3: FORWARD_WM_GETMINMAXINFO(hwnd, lpMinMaxInfo, DefWindowProc);
4:
5: // RECT rcWorkArea = {0, 0, 0, 0};
6: // SystemParametersInfo(SPI_GETWORKAREA, 0, &rcWorkArea, 0);
7:
8: MONITORINFOEX mi;
9: SecureZeroMemory(&mi, sizeof(mi));
10: mi.cbSize = sizeof(MONITORINFOEX);
11:
12: HMONITOR hMonitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);
13: GetMonitorInfo(hMonitor, &mi);
14:
15: const RECT& rcWorkArea = mi.rcWork;
16:
17: lpMinMaxInfo->ptMaxSize.x = rcWorkArea.right - rcWorkArea.left;
18: lpMinMaxInfo->ptMaxSize.y = rcWorkArea.bottom - rcWorkArea.top;
19: lpMinMaxInfo->ptMaxPosition.x = rcWorkArea.left;
20: lpMinMaxInfo->ptMaxPosition.y = rcWorkArea.top;
21:
22: int nCxSizeFrm = GetSystemMetrics(SM_CXSIZEFRAME);
23: int nCySizeFrm = GetSystemMetrics(SM_CYSIZEFRAME);
24:
25: lpMinMaxInfo->ptMaxSize.x += 2 * nCxSizeFrm;
26: lpMinMaxInfo->ptMaxSize.y += 2 * nCySizeFrm;
27:
28: lpMinMaxInfo->ptMaxPosition.x -= nCxSizeFrm;
29: lpMinMaxInfo->ptMaxPosition.y -= nCySizeFrm;
30:
31: DWORD dwStyle = GetWindowLongPtr(hwnd, GWL_STYLE);
32: if (0 == (dwStyle & WS_BORDER))
33: {
34: // 没有WS_BORDER风格时,边框的厚度:SM_CXSIZEFRAME - SM_CXBORDER
35: int nCxBorder = GetSystemMetrics(SM_CXBORDER);
36: int nCyBorder = GetSystemMetrics(SM_CYBORDER);
37:
38: lpMinMaxInfo->ptMaxSize.x -= 2 * nCxBorder;
39: lpMinMaxInfo->ptMaxSize.y -= 2 * nCyBorder;
40:
41: lpMinMaxInfo->ptMaxPosition.x += nCxBorder;
42: lpMinMaxInfo->ptMaxPosition.y += nCyBorder;
43: }
44: }
很不幸,你试过之后会发现,这段代码没有用。根本没有起到任何作用。当窗口在副屏显示时,位置是不对的。那么原因到底出在什么地方?
事实上,我们有个参数设置错误了。MINMAXINFO::ptMaxPosition的使用是错误的。
ptMaxPosition
The position of the left side of the maximized window (x member) and the position of the top of the maximized window (y member). For top-level windows, this value is based on the position of the primary monitor.
---- From MSDN
表述很简单,但是最后一句话是亮点?怎么理解?这么说吧,如果上面的代码里ptMaxPosition的初始化从rcWorkArea.left和rcWorkArea.top变成0和0,我们就会发现,问题解决了。再回过头来看上面这句话,我想你的理解更深刻了吧?
好了,关于移除标题栏的这个事情,就说到这里了。接下来要让现在的”标题栏“更好看些,你需要对菜单做一些自绘。我不是UI自绘高手,还是不献丑了。
更新(2012/4/19):还有一种情况需要考虑。我们知道任务栏是可以左停靠或者上停靠的。当我们把任务栏左停靠在副屏时,你会发现程序在副屏的最大化是有问题的。问题的原因还是在ptMaxPosition,解决方法是:
lpMinMaxInfo->ptMaxPosition.x = rcWorkArea.left - rcWorkMonitor.left;
lpMinMaxInfo->ptMaxPosition.y = rcWorkArea.top - rcWorkMonitor.top;
其中rcWorkMonitor是mi.rcMonitor。
在Win7下,当任务栏移动到副屏右侧时,还存在问题。这个问题处理起来相对比较麻烦(好像,XP不存在这个问题)。根据MSDN以及Spy++截获的信息来看,需要对WM_WINDOWPOSCHANGING消息进行处理,甚至可能需要处理WM_NCCALCSIZE。
通过这个标题栏菜单栏这个案例的分析,我们基本上把和窗口大小处理相关的信息研究得差不多了。特别是不要忘记使用Spy++,这回帮助你更加深刻得理解MSDN。