问题开始于我的最近的一次开发经历,我打算把程序的一部分界面放在DLL中,而这部分界面又需要使用到Tooltip,但DLL中的虚函数PreTranslateMessage无法被调用到,原因大家可以在 上搜索一下,这并不是我这篇文章要讲的。PreTranslateMessage不能被调,那Tooltip也就不能起作用,因为Tooltip需要在PreTranslateMessage中加入tooltip.RelayEvent(&msg)来触发事件,方可正常显示。解决方法有好几个,我用的是比较麻烦的一个——完全自己手动编写Tooltip,然后用WM_MOUSEMOVE等事件来触发Tooltip显示,写好之后发现些小问题,那就是调试运行时候IDE给了个warning,说我在析构函数中调用了DestroyWindow,这样会导致窗口OnDestry和OnNcDestroy不被正常调用,这个问题我以前遇到过,当然解决方法也是显而易见的,只需要在窗口对象(C++概念,非Windows内核对象,下文同)销毁前,调用DestroyWindow即可。对于要销毁的这个窗口的子窗口,是不需要显式调用DestroyWindow的,因为父窗口在销毁的时候也会销毁掉它们,OK,我把这个过程用个示意图说明一下:
图1
上图表示了App Window及其子窗口的关系,现在假设我们要销毁Parent Window 1(对应的对象指针是m_pWndParent1),我们可以m_pWndParent1->DestroyWindow(),这样Child Window 1,Parent Window 2,Child Window 2都被销毁了,销毁的时候这些窗口的OnDestry和OnNcDestroy都被调用了,最后delete m_pWndParent1,此时m_pWndParent1->m_hWnd已经是NULL,不会再去调用Destroy,在析构的时候也就不会出现Warning。但如果不先执行m_pWndParent1->DestroyWindow()而直接delete m_pWndParent1,那么在CWnd::~CWnd中就会调用DestroyWindow(m_hWnd),这样会产生WM_DESTROY和WM_NCDESTROY,会尝试去调用OnDestry和OnNcDestroy,但由于是在CWnd的函数~CWnd()的内部调用这两个成员,此时的虚函数表指针并不指向派生类的虚函数表,因此调用的其实是CWnd::OnDestroy和CWnd::OnNcDestroy,派生类的OnDestry和OnNcDestroy不被调用,但我们很多时候把释放内存等操作写在派生类的OnDestroy和OnNcDestroy中,这样,就容易导致内存泄露和逻辑混乱了。
上面这些道理我当然是知道的,但Warning还是出现了,而且我用排除法确定了是跟我写的那个Tooltip有关,下面是关于我的Tooltip的截图:
图2
大家看到,Tooltip显示在我的图形窗口上,它是个弹出式(popup)窗口,其内容为当前鼠标光标的坐标值,图形窗口之外,我是不想让它显示的,那么按照我的思路,Tooltip就应该设计是图形窗口的子窗口,它的窗口对象就应该作为图形窗口对象的成员,在图形窗口OnCreate的时候创建,在图形窗口被DestroyWindow的时候自动销毁,前面提到过,父窗口被销毁的时候,其子窗口会被自动销毁,没错吧,所以不需要显式去对Tooltip调用DestroyWindow。可事实证明了这样是有问题的,因为Tooltip的父窗口根本不是,也不能是图形窗口。大家可以看到我的图形窗口是作为一个子窗口嵌入到别的窗口中去的,它的属性包含了WS_CHILD,通过实验,我发现Tooltip的父窗口只能指定为程序主窗口,如果企图指定为那个图形窗口的话,它就自动变为程序主窗口,再进一步研究发现,弹出式窗口的父窗口都不能是带WS_CHILD风格的窗口,然后打开spy++查看,弹出式窗口的上一级都是桌面,可是,通过GetParent函数,得到的弹出式窗口的父窗口却是程序主窗口而不是桌面,为什么…问题越来越多,我糊涂了,上面说的都是在我深入理解前,所看到的现象,包括了我的一些概念认识方面的错误。
好吧,我们现在开始,一点点地通过实验去攻破这些难题!
一、神秘的WS_OVERLAPPED
我们从WinUser.h头文件中可以看出,窗口可分三种,其Window Styles定义如下:
- #define WS_OVERLAPPED 0x00000000L #define WS_POPUP 0x80000000L #define WS_CHILD 0x40000000L
那么我们很容易得到这个结论:style的最高位是1的,是一个popup窗口,style的次高位是1的,代表是一个child窗口,如果最高位次高位都是0,那这个窗口就是一个overlapped窗口,如果两位都是1,厄……MSDN告诉我们不能这么干,事实呢后面再讲。其实这个结论是有点过时的,甚至很能误导人,不是我们的原因,很可能是Windows的历史原因,为什么体也是后面讲。嘿嘿。
OK,我们现在开始来尝试,看看这些风格究竟影响窗口几何,对了,准备spy++,这是必备工具。
用VC++的向导创建一个Hello World的Windows程序,注意是Windows程序,不是MFC的Hello World,这样我们可以绕开MFC,专注于查看一些Windows的技术细节,编译,运行。
图3
然后用spy++查看这个窗口的风格,发现其风格显示为“WS_OVERLAPPEDWINDOW|WS_VISIBLE|WS_CLIPSIBLING|WS_OVERLAPPED”。此时它的创建函数为:
- hWnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);
只制定了一个WS_OVERLAPPEDWINDOW,但我们很快就找到了WS_OVERLAPPEDWINDOW的定义:
- #define WS_OVERLAPPEDWINDOW (WS_OVERLAPPED | / WS_CAPTION | / WS_SYSMENU | / WS_THICKFRAME | / WS_MINIMIZEBOX | / WS_MAXIMIZEBOX)
原来overlapped窗口就是有标题,系统菜单,最小最大化按钮和可调整大小边框的窗口,这个定义是正确的,但只是个我们认知上的概念的问题,因为popup和child窗口也同样可以拥有这些(后面证明)。由于WS_OVERLAPPED为0,那我们是不是可以把WS_OVERLAPPEDWINDOW定义中的WS_OVERLAPPED拿掉呢是肯定的,那也就是说WS_OVERLAPPED什么都不是!我们只作popup和child的区分,是不是这样不是,我们继续实验。
很简单,接下去我们只给这个向导生成的代码加一点点东西,就是把CreateWindow改成:
- hWnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW|WS_POPUP, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);
对,给窗口风格增一个popup风格,看看会怎么样行!这回可不得了,窗口缩到了屏幕的左上角,并且宽度高度都变为了最小,当然,你还是可以用鼠标拖动窗口边缘来调整它的大小的。如图:
图4
这是为什么呢察CreateWindow的,第四、第五、第六和第七参数,分别为窗口的x坐标,y坐标,宽度,和高度,CW_USEDEFAULT被define成0,所以窗口被缩到左上角去也就不奇怪了,可没有popup,光是overlapped风格的窗口,为什么不会缩呢MSDN的说明,对第四个参数的说明:“If this parameter is set to CW_USEDEFAULT, the system selects the default position for the window’s upper-left corner and ignores the y parameter. CW_USEDEFAULT is valid only for overlapped windows; if it is specified for a pop-up or child window, the x and y parameters are set to zero. ”其余几个参数也有类似的描述,这说明了什么明Windows对overlapped和popup还是作区分的,而这点,算是我们发现的第一个不同。哦,还有件事情,就是用spy++观察其风格,发现其确实多了一个WS_POPUP,其余没什么变化。
继续,这回还是老地方,把WS_POPUP改为WS_CHILD,试试看,这回创建窗口失败了,返回0,用GetLastError查看具体错误信息,得到的是:“1406:无法创建最上层子窗口。”看来桌面是不让我们随便搞的。继续,还是老地方,这回改成:
- hWnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW|WS_POPUP|WS_CHILD, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);
嗯没搞错,又是popup又是child,肯定不能成功吧,不试不知道,居然成功了,这个创建出来的窗口乍一看,跟popup风格的很像,但用起来有些怪异,比如:当它被别的窗口挡住的时候,不能通过点击它的客户区来让它显示在前面,即使点击它的标题栏,也是要松开鼠标左键,它才能显示在前面,还有就是用spy++的“瞄准器”没法准确捕捉到这个窗口,瞄准器对准它的时候,就显示Caption为“Program Manager”,class为“Program”,“Program Manager”是什么实就是我们所看到的这个桌面(注意,不是桌面,我说的是我们说“看到的桌面”,就是显示桌面图标的这个所能看到的桌面窗口,和前面提到的桌面窗口是有区别的)的父窗口的父窗口,这个窗口一般情况下是不能直接“瞄准”到的,这点可以通过spy++证实,如图:
图5
图6
spy++不能直接“瞄准”这个popup和child并存的怪窗口,但我们有别的办法捕捉到它,<Alt>+<F3>,输入窗口的标题来查找(记得运行程序后刷新一下才能找到),结果见下图:
图7
我们从上图中清楚地看到,popup和child并存!用spy++逐个查看桌面窗口的下属,这种情况还是无独有偶的,但这样的窗口代表了什么意义,我就不清楚了,总之用起来怪怪的,对Microsoft来说,这可能就是Undocumented,OK,我们了解到这里就行了,但一般情况下,我们不要去创建这种奇怪的窗口。这几轮实验给我们什么启示计上的启示:一个应用程序的主窗口通常是一个Overlapped类型的窗口,当然有时可以是一个popup窗口,比如基于对话框的程序,但不应该是一个child窗口,尽管上面演示了如何给应用程序主窗口加入child风格。
那还有一个问题,我为什么认为WS_OVERLAPPED神秘呢还算是拜spy++所赐,按照我们一般的想法,如果一个窗口的风格的最高两位都是0,它既不是popup也不是child的时候,那它就是Overlapped。事实上spy++的判定不是这样的,就以刚才的实验为例,当使用WS_OVERLAPPEDWINDOW|WS_POPUP风格创建窗口的时候,WS_OVERLAPPED和WS_POPUP属性同时出现了,我做了很多很多的尝试,企图找出其中规律,看看spy++是怎么判定WS_OVERLAPPED的,但至今没结论,我到MSDN上search,未果,有人提起这个问题,但没有令我满意的答复,下面这段文字是我找到的可能有点线索的答复:
Actually, Microsoft Spy++ is wrong.
There are two bits in the window style that control its type. If the high-order bit of the style DWORD is set, the window is a popup window. If the next bit is set, the window is a child window. If neither is set, the window is overlapped. (If both are set, the result is undocumented.)
Look at these definitions from WinUser.h.
- #define WS_OVERLAPPED 0x00000000L #define WS_POPUP 0x80000000L #define WS_CHILD 0x40000000L
Your window style (0x94c00880) has the high-order bit set and the next bit clear so it is a popup window, not an overlapped window.
The correct way to identify all three types of windows (this is what Spy++ should do) is
- dwStyle = GetWindowLong(hWnd, GWL_STYLE); if (dwStyle&WS_POPUP) // it‘s a popup window else if (dwStyle&WS_CHILD) // it’s a child window else // it‘s an overlapped window
这断描述跟我的想法一致。要知道,就算你只给窗口一个WS_POPUP的风格,WS_OVERLAPPED也会显示在spy++上的,我认为这十分有问题,究竟spy++如何判,估计得请教比尔盖茨了。还有一段有趣的描述,估计也有所帮助:
As long as…
WS_POPUP | WS_OVERLAPPED
…is absolutelly equivalent with…
WS_POPUP
… why do you care if Spy++ lists WS_OVERLAPPED or not/p>
Please stop playing “Thomas Unbeliever” with us.
Becomes too expensive to use “walking on the water” device here again, and again. ;)
二、Parent与Owner
这是内容最多的一节,做好心理准备。
微软和我们开了个玩笑,告诉我们,窗口和人一样,可以有父母,有主人……我们先来看一个最著名的Windows API:
- HWND CreateWindowEx( DWORD dwExStyle, // extended window style LPCTSTR lpClassName, // registered class name LPCTSTR lpWindowName, // window name DWORD dwStyle, // window style int x, // horizontal position of window int y, // vertical position of window int nWidth, // window width int nHeight, // window height HWND hWndParent, // handle to parent or owner window HMENU hMenu, // menu handle or child identifier HINSTANCE hInstance, // handle to application instance LPVOID lpParam // window-creation data );
猜对了,我就是从MSDN上copy下来的,看第九个参数的名字叫hWndParent,顾名思义哦,这就是Parent窗口了,不过我们中国人不喜欢称之“父母窗口”,我们喜欢叫它“父窗口”,简单一点。其实这个名字对我们造成了不少的误导,我只能说,可能也是由于历史原因,比如在Windows 1.0(1985年出的,当时没什么影响力)的时候,只有Parent这个概念,没有Owner的概念。
回头看看文章开始我提起的,我企图将Tooltip的父窗口设置为一个图形窗口,不能成功,Tooltip的父窗口会自动变成应用程序主窗口,这是为什么,现在开始讲概念了,都是我花了很多时间在互联 上搜索,筛选,确认,得出来的结论:
规则一:Owner window控制了Owned window的生存,当Owner window被销毁的时候,其所属的Owned window就会被销毁。
规则二:Parent window控制了Child window的绘制,Child window不可能显示在其Parent window的客户区之外。
规则三:Parent window同时控制了Child window的生存,当Parent window被销毁的时候,其所属的Child window就会被销毁。
规则四:Owner window不能是Child window。
规则五:Child window一定有Parent(否则怎么叫Child,一定没有Owner。
规则六:非Child window的Parent一定是桌面,它们不一定有Owner。
这是比较重要的几点,如果你认为这跟你以前学到的,或者认知的有所不同,先别急着抗议,先看看我是怎么理解的。除了这几条规则,下面我还会逐步给出一些规则。
先说比较好理解的Child window,上文提到了,包含了WS_CHILD风格的窗口就叫Child window,我们中文叫“子窗口”。那么我前面提到的我写的那个Tooltip,是不是“子窗口”呢—当然不是了,它没有WS_CHILD风格啊,它是popup风格的,我想当然地认为在创建它的时候给它指定了那个Parent参数,那它的Parent就是那个参数,其实是错的。这个实验最简单了,随便找些应用程序,比如“附件”里的计算器,用spy++的“瞄准器”观察上面的按钮等“子窗口”,在Styles标签中,我们可以看到WS_CHILD(或者WS_CHILDWINDOW,一样的)属性,然后在Windows标签中,我们可以清楚地看到,凡是包含了WS_CHILD属性的窗口(子窗口),都没有Owner window,不信还可以继续观察其它应用程序,省去自己编程了。再看它们的Parent window,是不是一定有的—当然一定有。
前面说了,子窗口不能显示在父窗口客户区之外,我们最常见的子窗口就是那些摆在对话框上的控件,什么button啊,listbox啊,combobox啊……都有个共同特点,不能拖动的,除非你重写它们的window procedure,然后响应WM_MOUSEMOVE等消息,实现所谓“拖动”。那么有没有能够像应用程序主窗口那样有标题栏,能够被自由拖动的子窗口呢—当然有!要创建是吗单,直接用MFC向导创建一个MDI程序即可,MDI的那些View其实就是可以自由拖动的子窗口,可以用spy++查看一下它们的属性,当然,你是不能把它们拖出主窗口的客户区的。也许你跟我一样,觉得MFC封装了过多的技术细节,想完全自己手动创建一个能拖动的子窗口,而且看起来就像个MDI的界面,OK,follow me。
首先当然是用应用程序向导生成最普通的Window应用程序了。然后增加一个窗口处理函数,也就是我们准备创建的子窗口的处理函数了。
- LRESULT CALLBACK WndProcDoNothing(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { return DefWindowProc(hWnd, message, wParam, lParam); }
DoNothing名字。注册之:
- WNDCLASSEX wcex; wcex.cbSize = sizeof(WNDCLASSEX); wcex.style = CS_HREDRAW | CS_VREDRAW; wcex.lpfnWndProc = (WNDPROC)WndProcDoNothing; wcex.cbClsExtra = 0; wcex.cbWndExtra = 0; wcex.hInstance = hInstance; wcex.hIcon = LoadIcon(hInstance, (LPCTSTR)IDI_ALLWINDOWTEST); wcex.hCursor = LoadCursor(NULL, IDC_ARROW); wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW+1); wcex.lpszMenuName = NULL; //子窗口不能拥有菜单,指定了也没有用 wcex.lpszClassName = TEXT(“child_window”); wcex.hIconSm = LoadIcon(wcex.hInstance, (LPCTSTR)IDI_SMALL); RegisterClassEx(&wcex);
最后当然是把它给创建出来了:
- g_hwndChild = CreateWindowEx(NULL, TEXT(“child_window”), TEXT(“”), WS_CHILD|WS_VISIBLE|WS_OVERLAPPEDWINDOW|WS_CLIPSIBLINGS, 30, 30, 400, 300, hWnd, NULL, hInstance, NULL);
关于WS_CLIPSIBLINGS属性,下文将提到。好,就这样,大家看看运行效果:
声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!