目录
1、WS_CHILD和WS_POPUP
2、WS_VISIBLE
3、WS_MINIMIZE和WM_MAXIMIZE
4、WS_MINIMIZEBOX和WS_MAXIMIZEBOX
5、WS_BORDER和WS_CAPTION
6、WS_THICKFRAME和WS_SIZEBOX
7、WS_SYSTEMMENU
8、WS_EX_APPWINDOW和WS_EX_TOOLWINDOW
9、WS_EX_TOPMOST
10、WS_EX_LAYEREDWINDOW
11、WS_EX_TRANSPARENT
最近我们遇到了几个与窗口风格相关的问题,因为未设置指定的窗口风格或者错误设置了某些窗口风格导致了窗口出现了一些bug。本文借此机会将窗口风格的相关要点进行详细的总结,在此分享出来,给大家提供一些借鉴和参考。
本文讲解的内容基于Windows Win32界面编程,会涉及到directui开源库duilib的一些内容。
1、WS_CHILD和WS_POPUP
WS_CHILD: The window is a child window. A window with this style cannot have a menu bar. This style cannot be used with the WS_POPUP style.
WS_POPUP: The window is a pop-up window. This style cannot be used with the WS_CHILD style.
这两个风格是相对的,WS_CHILD是指定子窗口的,Child窗口必须要设置承载的父窗口。Child窗口自动跟随父窗口移动,即父窗口移动的过程中,子窗口相对父窗口的是不变的。
WS_POPUP是用来指定弹出窗口的,可以设置父窗口,也可以不指定父窗口(父窗口句柄为空),Popup窗口和父窗口其实不是真正意义上的父子关系,而是owner与被own的关系。
不管是Child窗口还是Popup窗口,始终是悬浮在父窗口之上的。Popup窗口不会随着父窗口的移动而自动移动,如果Popup窗口要做到对其父窗口的跟随,则需要编写代码去实现对父窗口的实时跟随。具体可以启动定时器去跟随,也可以在父窗口中实时拦截WM_MOVE、WM_WINDOWPOSCHANGING、WM_SIZE等消息,实时去跟随父窗口。
2、WS_VISIBLE
WS_VISIBLE: The window is initially visible.This style can be turned on and off by using the ShowWindow or SetWindowPos function.
该窗口风格可以直接反映窗口当前是处在显示状态,还是处于掩藏状态。特别是我们在使用SPY++探查窗口属性时会看到。
以研究QQ主窗口在任务栏不显示窗口的实现方法为例,之前听说是通过将QQ主窗口的父窗口设置为一个掩藏的窗口,实现QQ主窗口在任务栏没有窗口的。我们后来使用SPY++验证了一下,先SPY了一下QQ的主窗口:
在探测到的窗口信息页面的Tab页下查看到其父窗口的句柄:
点击父窗口句柄查看父窗口信息,看到父窗口没有WS_VISIBLE风格:
说明父窗口一直是掩藏的。另外查看了一下这个掩藏的父窗口的尺寸也是很小的:
说明这个隐藏窗口不做他用,就是为了实现QQ主窗口在任务栏不显示窗口的功能
3、WS_MINIMIZE和WM_MAXIMIZE
WS_MAXIMIZE: The window is initially maximized.
WS_MINIMIZE: The window is initially minimized. Same as the WS_ICONIC style.
这两个风格是用来指定窗口创建时的初始显示状态,WS_MINIMIZE用来指定窗口初始最小化(到任务栏)显示,WM_MAXIMIZE用来指定窗口初始最大化显示。注意要将这两个风格和WS_MINIMIZEBOX、WS_MAXIMIZEBOX两个风格区分开来。
4、WS_MINIMIZEBOX和WS_MAXIMIZEBOX
WS_MAXIMIZEBOX: The window has a maximize button. Cannot be combined with the WS_EX_CONTEXTHELP style.
WS_MINIMIZEBOX: The window has a minimize button. Cannot be combined with the WS_EX_CONTEXTHELP style.
对于win32原生的一些窗口,比如Dialog对话框窗口,设置这两种属性会给窗口添加最小化和最大化按钮。其实这点并不重要,因为我们在编程时一般不会直接使用原生的win32窗口,而是使用基于Win32的UI框架(比如duilib开源库)。
下面两点与窗口操作行为有关的才比较重要。一种情况是,如果窗口设置了WS_MAXIMIZEBOX风格,在win7以上的系统中,将窗口拖动到桌面边界时,系统自动会有个将窗口最大化的效果,如下:
仔细观察一下,桌面的边界区域有个可以放置最大化的效果。如果程序中窗口是固定大小的,不需要这个系统默认最大化的操作,则需要将WS_MAXIMIZEBOX风格去除掉。
另一种情况是目标窗口中添加了最大化和最小化按钮,窗口在任务栏也是有对应的窗口的,则必须要有WS_MINIMIZEBOX,否则点击任务栏窗口不能将目标窗口最小化到任务栏。
5、WS_BORDER和WS_CAPTION
WS_BORDER: The window has a thin-line border.
WS_CAPTION: The window has a title bar (includes the WS_BORDER style).
WS_BORDER是用来给窗口设置一个默认的四周窄边界的,WS_CAPTION则用来指定窗口有标题栏(包含了WS_BORDER,即包含了窗口四周的窄边界)。
在duilib开源库界面框架中,会自动将窗口的WS_CAPTION风格取消掉:
LONG styleValue = ::GetWindowLong( *this, GWL_STYLE );
styleValue &= ~WS_CAPTION;
即创建的win32窗口都没有默认的系统标题栏,因为WS_CAPTION属性中包含了WS_BORDER,所以取消了WS_CAPTION之后,除了窗口没有标题栏,窗口也没有边界了,这样窗口整个都是客户区域,而duilib框架是直接在客户区做各种绘制,随意的放置按钮、布局等控件,也可以设置视觉上看到的虚拟出来的标题栏区域(并不是真正的窗口标题栏,是仿系统标题栏的)。在duilib中:可以给窗口设置caption属性,即设置一个虚拟的标题栏区域,点击该区域可以拖动窗口:
LRESULT CDuiWindow::OnNcHitTest( UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled )
{
POINT pt;
pt.x = GET_X_LPARAM( lParam );
pt.y = GET_Y_LPARAM( lParam );
::ScreenToClient( *this, &pt );
RECT rcClient;
::GetClientRect( *this, &rcClient );
// 1、设置sizebox属性,设置窗口的拖动区域,拖动改变窗口大小
if( !::IsZoomed(*this) )
{
RECT rcSizeBox = m_pm.GetSizeBox();
if( pt.y < rcClient.top + rcSizeBox.top )
{
if( pt.x < rcClient.left + rcSizeBox.left ) return HTTOPLEFT;
if( pt.x > rcClient.right - rcSizeBox.right ) return HTTOPRIGHT;
return HTTOP;
}
else if( pt.y > rcClient.bottom - rcSizeBox.bottom )
{
if( pt.x < rcClient.left + rcSizeBox.left ) return HTBOTTOMLEFT;
if( pt.x > rcClient.right - rcSizeBox.right ) return HTBOTTOMRIGHT;
return HTBOTTOM;
}
if( pt.x < rcClient.left + rcSizeBox.left ) return HTLEFT;
if( pt.x > rcClient.right - rcSizeBox.right ) return HTRIGHT;
}
// 2、设置caption属性,设置虚拟标题栏,拖动标题栏以拖动窗口
RECT rcCaption = m_pm.GetCaptionRect();
if( pt.x >= rcClient.left + rcCaption.left
&& pt.x < rcClient.right - rcCaption.right
&& pt.y >= rcCaption.top
&& pt.y < rcCaption.bottom )
{
// 考虑到标题栏区域会放置控件,比如常见的右上角的最小化、最大化和关闭按钮,
// 所以要将按钮等控件过滤掉
CControlUI* pControl = static_cast<CControlUI*>( m_pm.FindControl( pt ) );
if( pControl
&& _tcscmp(pControl->GetClass(), _T("ButtonUI")) != 0
&& _tcscmp(pControl->GetClass(), _T("OptionUI")) != 0
&& _tcscmp(pControl->GetClass(), _T("TextUI")) != 0 )
{
return HTCAPTION;
}
}
return HTCLIENT;
}
6、WS_THICKFRAME和WS_SIZEBOX
WS_THICKFRAME: The window has a sizing border. Same as the WS_SIZEBOX style.
这两个风格是完全等价的风格,效果是一样的,都是用来指定窗口有个可拖动的边界,设置该风格后,可以拖动窗口的边界改变窗口的大小。
在duilib窗口编程中,因为窗口被取消了WS_CAPTION风格,所以是没有边界区域的,所以即使设置了WS_THICKFRAME属性后窗口也是不可拖动改变大小的。duilib中可以给窗口设置sizebox属性,会虚拟出一个可拖动的边界区域,以拖动改变窗口大小,相关代码如下:
LRESULT CDuiWindow::OnNcHitTest( UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled )
{
POINT pt;
pt.x = GET_X_LPARAM( lParam );
pt.y = GET_Y_LPARAM( lParam );
::ScreenToClient( *this, &pt );
RECT rcClient;
::GetClientRect( *this, &rcClient );
// 1、设置sizebox属性,设置窗口的拖动区域,拖动改变窗口大小
if( !::IsZoomed(*this) )
{
RECT rcSizeBox = m_pm.GetSizeBox();
if( pt.y < rcClient.top + rcSizeBox.top )
{
if( pt.x < rcClient.left + rcSizeBox.left ) return HTTOPLEFT;
if( pt.x > rcClient.right - rcSizeBox.right ) return HTTOPRIGHT;
return HTTOP;
}
else if( pt.y > rcClient.bottom - rcSizeBox.bottom )
{
if( pt.x < rcClient.left + rcSizeBox.left ) return HTBOTTOMLEFT;
if( pt.x > rcClient.right - rcSizeBox.right ) return HTBOTTOMRIGHT;
return HTBOTTOM;
}
if( pt.x < rcClient.left + rcSizeBox.left ) return HTLEFT;
if( pt.x > rcClient.right - rcSizeBox.right ) return HTRIGHT;
}
// 2、设置caption属性,设置虚拟标题栏,拖动标题栏以拖动窗口
RECT rcCaption = m_pm.GetCaptionRect();
if( pt.x >= rcClient.left + rcCaption.left
&& pt.x < rcClient.right - rcCaption.right
&& pt.y >= rcCaption.top
&& pt.y < rcCaption.bottom )
{
// 考虑到标题栏区域会放置控件,比如常见的右上角的最小化、最大化和关闭按钮,
// 所以要将按钮等控件过滤掉
CControlUI* pControl = static_cast<CControlUI*>( m_pm.FindControl( pt ) );
if( pControl
&& _tcscmp(pControl->GetClass(), _T("ButtonUI")) != 0
&& _tcscmp(pControl->GetClass(), _T("OptionUI")) != 0
&& _tcscmp(pControl->GetClass(), _T("TextUI")) != 0 )
{
return HTCAPTION;
}
}
return HTCLIENT;
}
7、WS_SYSTEMMENU
WS_SYSMENU: The window has a window menu on its title bar. The WS_CAPTION style must also be specified.
该风格用来给窗口标题栏设置默认的系统右键菜单,该系统菜单中包含关闭等菜单项,如下:
设置该风格时必须同时设置WS_CAPTION风格才会生效,因为设置WS_CAPTION后才会有真实的标题栏,而systemmenu右键菜单是右键点击标题栏时的右键菜单。
这里有个很重要的点需要注意一下,是在测试我们软件的过程中发现的。在win10系统中将显示比例设置成150%时,如果将没设置WS_SYSTEMMENU风格的窗口上边界移动到桌面上边界后,鼠标移动到窗口上边界与桌面上边界交接处:
鼠标光标会变成可拖动大小的光标形状,此时鼠标如果向下拖,可以改变窗口的高度,但实际上我们这些窗口是固定大小的,不允许改变大小的!
这个问题在100%、125%和175%显示比例下是没有的,只有将显示比例设置成150%才有这个问题。我们通过和其他窗口对比发现,只有我们的部分窗口才有这个问题,其他程序的窗口和系统的窗口都没有这个问题。通过对比窗口属性并添加测试代码,最终确定是因为我们软件中有问题的窗口没有设置WS_SYSTEMMENU属性引起的,设置该属性后就不再有问题了。至于为什么不设置WS_SYSTEMMENU就有问题、非150%的显示比例下才有问题,我们就不得而知了!不知道这是不是Windows系统的bug?
对于我们的duilib窗口,是不需要有这种系统默认的标题栏右键菜单的,添加该属性后我们每个dui窗口是否会添加上右键系统菜单?其实是不会有的,因为我们的dui窗口都取消了WS_CAPTION窗口风格,没有系统默认的标题栏,所以即使添加了WS_SYSTEMMENU风格后,也不会有系统默认的标题栏右键菜单的。
8、WS_EX_APPWINDOW和WS_EX_TOOLWINDOW
WS_EX_APPWINDOW: Forces a top-level window onto the taskbar when the window is visible.
WS_EX_TOOLWINDOW: The window is intended to be used as a floating toolbar. A tool window has a title bar that is shorter than a normal title bar, and the window title is drawn using a smaller font. A tool window does not appear in the taskbar or in the dialog that appears when the user presses ALT+TAB. If a tool window has a system menu, its icon is not displayed on the title bar. However, you can display the system menu by right-clicking or by typing ALT+SPACE.
WS_EX_APPWINDOW会强制让目标窗口在任务栏有个窗口,WS_EX_TOOLWINDOW则会让窗口不在任务栏显示一个窗口。
一般情况下,创建的win32窗口在任务栏都有对应的窗口,如果想让窗口在任务栏没有窗口,则可以给窗口添加WS_EX_TOOLWINDOW风格。
但这个方法可以用于临时弹出的窗口,但一般不能用于常态显示的窗口,比如程序的主窗口,主窗口会有很多UI交互和业务操作的,如果给主窗口直接设置WS_EX_TOOLWINDOW属性,则会导致主窗口在某些场景下会出现问题,几年前我们在开发客户端软件时就领教过!
对于主窗口,要让其在任务栏不显示窗口,则需要参考QQ主窗口的做法,将主窗口的父窗口设置为一个掩藏的窗口。关于这一点,我们上面已经提到过。
9、WS_EX_TOPMOST
WS_EX_TOPMOST: The window should be placed above all non-topmost windows and should stay above them, even when the window is deactivated. To add or remove this style, use the SetWindowPos function.
WS_EX_TOPMOST用来将窗口置顶。
10、WS_EX_LAYEREDWINDOW
WS_EX_LAYERED: The window is a layered window. This style cannot be used if the window has a class style of either CS_OWNDC or CS_CLASSDC.
Windows 8: The WS_EX_LAYERED style is supported for top-level windows and child windows. Previous Windows versions support WS_EX_LAYERED only for top-level windows.
WS_EX_LAYEREDWINDOW用来将窗口设置为分层窗口。设置为分层窗口后,就可以给窗口设置透明度,可以在窗口中做鼠标可以完全穿透的全透明区域,也可以做出各种奇异形状的异形窗口。
这里需要注意一下,只有具有WS_POPUP风格的窗口才能设置WS_EX_LAYEREDWINDOW,不能对WS_CHILD窗口设置,即使设置了也不会生效。
关于Layered分层窗口能实现什么样的功能和效果,之前专门写过一篇专题文章,感兴趣的可以去看一下:
两万字总结Windows系统中的Layered分层窗口技术https://blog.csdn.net/chenlycly/article/details/120960399https://blog.csdn.net/chenlycly/article/details/120960399
11、WS_EX_TRANSPARENT
WS_EX_TRANSPARENT: The window should not be painted until siblings beneath the window (that were created by the same thread) have been painted. The window appears transparent because the bits of underlying sibling windows have already been painted.To achieve transparency without these restrictions, use the SetWindowRgn function.
WS_EX_TRANSPARENT用来将窗口设置成透明的,不仅窗口区域是完全透明的(可以看到窗口后面的内容),而且鼠标可以完全穿透当前的窗口(可以用鼠标直接操作当前窗口后面的内容)。我们在实现窗口的阴影边界时会用到该窗口属性,比如360窗口边界的阴影窗口:
阴影区域可以直接穿透的!阴影窗口的实现,也可以参看我的这篇专题文章:
两万字总结Windows系统中的Layered分层窗口技术https://blog.csdn.net/chenlycly/article/details/120960399https://blog.csdn.net/chenlycly/article/details/120960399