两万字总结Windows系统中的Layered分层窗口技术

Posted IT老张

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了两万字总结Windows系统中的Layered分层窗口技术相关的知识,希望对你有一定的参考价值。

目录

1、WS_EX_TRANSPARENT和WS_EX_LAYERED窗口扩展风格

2、调用UpdateLayeredWindow之后不再产生WM_PAINT消息

3、调用SetLayeredWindowAttributes实现特殊效果的窗口

3.1、实现渐入渐出的窗口

3.2、实现叠加在其他窗口上面的水印窗口

4、调用UpdateLayeredWindow实现特殊效果的窗口

4.1 实现除边框外的中间区域全透明的桌面区域共享选择窗口

4.2 在目标窗口的边界加上阴影的效果

4.3 实现各种具有酷炫视觉效果的不规则的非直角窗口

5、SetLayeredWindowAttributes和UpdateLayeredWindow不能同时调用

6、UpdateLayeredWindow函数调用失败的可能原因分析

6.1、被操作的窗口时Child子窗口

6.2、没有设置WS_EX_LAYERED窗口风格

6.3、调用之前创建的位图没有alpha通道

6.4、之前调用了SetLayeredWindowAttributes,再调用UpdateLayeredWindow会失败

7、总结


       在开发Windows应用程序时,我们会时常使用Layered分层窗口实现特殊视觉效果的窗口在Windows系统中,操作Layered分层窗口实现酷炫效果的API函数分别是SetLayeredWindowAttributesUpdateLayeredWindow,其中SetLayeredWindowAttributes可以设置窗口的透明度,UpdateLayeredWindow则能实现各种特殊效果的异形窗口。

       今天就来详细介绍一下如何使用这两个函数去实现指定效果的Layered分层窗口,以及使用这两个函数时要注意的一些地方。

1、WS_EX_TRANSPARENT和WS_EX_LAYERED窗口扩展风格

       设置WS_EX_LAYERED风格的窗口是Layered分层窗口,SetLayeredWindowAttributes和UpdateLayeredWindow只对分层窗口才生效。如果对没设置WS_EX_LAYERED风格的窗口调用这两个函数,都会返回失败。

       此外,设置WS_EX_LAYERED风格的窗口必须是具有WS_POPUP风格的弹出窗口,不能是Child子窗口,否则SetLayeredWindowAttributes和UpdateLayeredWindow两个函数也会调用失败。对于新版本的API已经支持Child子窗口了,但是具体情况没有进行测试。

       还有个WS_EX_TRANSPARENT窗口风格,设置该风格的窗口鼠标可以直接穿透过去,即鼠标可以点击到窗口后面的内容,有时会和WS_EX_LAYERED组合起来使用。

       可以在创建窗口时给窗口设置WS_EX_LAYERED风格:

    // Create the shadow window
    // WS_EX_TRANSPARENT - 本shadow窗口是鼠标能穿透的
    m_hWnd = CreateWindowEx(WS_EX_LAYERED | WS_EX_TRANSPARENT | WS_EX_NOACTIVATE, lpszWndClassName, NULL,
        WS_POPUP | WS_BORDER ,
        CW_USEDEFAULT, 0, 0, 0, hParentWnd, NULL, s_hInstance, NULL);

也可以在窗口创建成功后,调用SetWindowLong去设置,先调用GetWindowLong获取窗口风格,然后与上WS_EX_LAYERED,设置给窗口:

SetWindowLong(GetSafeHwnd(), GWL_EXSTYLE , GetWindowLong(GetSafeHwnd(), GWL_EXSTYLE) | WS_EX_LAYERED);

2、调用UpdateLayeredWindow之后不再产生WM_PAINT消息

       一旦调用UpdateLayeredWindow之后就不会再产生WM_PAINT绘制窗口的消息了,所以需要会绘制到目标窗口上的内容不能在处理WM_PAINT消息时去绘制了,所有的绘制代码都必须放在调用UpdateLayeredWindow的接口中。

       一般情况下我们会把窗口绘制操作都封装在一个接口中,接口最后调用UpdateLayeredWindow完成窗口的绘制。比如窗口大小发生改变时需要调用该封装的接口去绘制窗口,即在收到WM_SIZE消息时调用封装所有绘制操作的接口。
       在封装的负责绘制的接口中,一般先创建一个兼容DC,然后在兼容DC上绘制所需要绘制的内容,最后调用UpdateLayeredWindow将兼容DC上的内容绘制到目标窗口上,实例代码如下:

#define   DESKTOP_SHARE_AREA_SEL_WND_EDGESIZE_TOP_HEIGHT       27
#define   DESKTOP_SHARE_AREA_SEL_WND_EDGESIZE_LEFT_WIDTH       5
#define   DESKTOP_SHARE_AREA_SEL_WND_EDGESIZE_RIGHT_WIDTH      5
#define   DESKTOP_SHARE_AREA_SEL_WND_EDGESIZE_BOTTOM_HEIGHT    5
 
 
void CDesktopShareAreaSelDlg::Update()
{
	if ( m_BkImg.IsNull() )
	{
		return;
	}
 
	RECT rcWnd;
	::GetWindowRect(m_hWnd, &rcWnd);
	int nWndWidth = rcWnd.right - rcWnd.left;
	int nWndHeight = rcWnd.bottom - rcWnd.top;
 
	// Create the alpha blending bitmap
	BITMAPINFO bmi;        // bitmap header
 
	ZeroMemory(&bmi, sizeof(BITMAPINFO));
	bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
	bmi.bmiHeader.biWidth = nWndWidth;
	bmi.bmiHeader.biHeight = nWndHeight;
	bmi.bmiHeader.biPlanes = 1;
	bmi.bmiHeader.biBitCount = 32;         // four 8-bit components
	bmi.bmiHeader.biCompression = BI_RGB;
	bmi.bmiHeader.biSizeImage = nWndWidth * nWndHeight * 4;
 
	BYTE *pvBits;          // pointer to DIB section
	HBITMAP hbitmap = CreateDIBSection(NULL, &bmi, DIB_RGB_COLORS, (void **)&pvBits, NULL, 0);
	if (pvBits == NULL) {
		return;
	}
 
	ZeroMemory(pvBits, bmi.bmiHeader.biSizeImage);
 
	// 创建内存DC
	HDC hMemDC = CreateCompatibleDC(NULL);
	HBITMAP hOriBmp = (HBITMAP)SelectObject(hMemDC, hbitmap);
 
	int nImgWidth = m_BkImg.GetWidth();
	int nImgHeight = m_BkImg.GetHeight();
 
	// 将窗口left、top、right、bottom四个方向上的图片绘制到窗口边框的位置
	m_BkImg.Draw( hMemDC, 0, 0, DESKTOP_SHARE_AREA_SEL_WND_EDGESIZE_LEFT_WIDTH, nWndHeight, 0, 0, DESKTOP_SHARE_AREA_SEL_WND_EDGESIZE_LEFT_WIDTH, nImgHeight );
	m_BkImg.Draw( hMemDC, nWndWidth - DESKTOP_SHARE_AREA_SEL_WND_EDGESIZE_RIGHT_WIDTH, 0, DESKTOP_SHARE_AREA_SEL_WND_EDGESIZE_RIGHT_WIDTH, nWndHeight, nImgWidth-DESKTOP_SHARE_AREA_SEL_WND_EDGESIZE_RIGHT_WIDTH, 0, DESKTOP_SHARE_AREA_SEL_WND_EDGESIZE_RIGHT_WIDTH, nImgHeight );
	m_BkImg.Draw( hMemDC, 0, 0, nWndWidth, DESKTOP_SHARE_AREA_SEL_WND_EDGESIZE_TOP_HEIGHT, 0, 0, nImgWidth, DESKTOP_SHARE_AREA_SEL_WND_EDGESIZE_TOP_HEIGHT );
	m_BkImg.Draw( hMemDC, 0, nWndHeight-DESKTOP_SHARE_AREA_SEL_WND_EDGESIZE_BOTTOM_HEIGHT, nWndWidth, DESKTOP_SHARE_AREA_SEL_WND_EDGESIZE_BOTTOM_HEIGHT, 0, nImgHeight-DESKTOP_SHARE_AREA_SEL_WND_EDGESIZE_BOTTOM_HEIGHT, nImgWidth, DESKTOP_SHARE_AREA_SEL_WND_EDGESIZE_BOTTOM_HEIGHT );
 
	POINT ptDst = {rcWnd.left, rcWnd.top};
	POINT ptSrc = {0, 0};
	SIZE WndSize = {nWndWidth, nWndHeight};
	BLENDFUNCTION blendPixelFunction= { AC_SRC_OVER, 0, 255, AC_SRC_ALPHA };
 
	BOOL bRet= UpdateLayeredWindow(m_hWnd, NULL, &ptDst, &WndSize, hMemDC,
		&ptSrc, 0, &blendPixelFunction, ULW_ALPHA);
 
	DWORD dwRet = GetLastError();
 
	InvalidateRect( m_hWnd, &rcWnd, TRUE );
	//_ASSERT(bRet); // something was wrong....
 
	// Delete used resources
	SelectObject(hMemDC, hOriBmp);
	DeleteObject(hbitmap);
	DeleteDC(hMemDC);
}

        调用SetLayeredWindowAttributes函数则不会影响窗口的WM_PAINT消息的产生。

3、调用SetLayeredWindowAttributes实现特殊效果的窗口

       调用SetLayeredWindowAttributes可以设置窗口的透明度,让窗口具有半透明的效果,但单独这样使用没有实用价值。

3.1、实现渐入渐出的窗口

       我们可以实现一种渐入渐出的效果,即动态的显示出来、动态的掩藏起来。可以设置定时器,动态定时地去设置窗口的透明度就可以实现这样的效果了:

// 设置定时器
::SetTimer( hParentWnd, TIMER_DYNAMIC_CONTROL_WND_TRANSPARENCY, 50, NULL );

UINT m_dwWndTransparencyVal = 0;


// 处理定时器消息的代码块
case WM_TIMER:
     {
         UINT dwTimeId = (UINT)wParam;
         if ( dwTimeId == TIMER_DYNAMIC_CONTROL_WND_TRANSPARENCY )
         {
             m_dwWndTransparencyVal += 30;
             SetLayeredWindowAttributes(m_hWnd, 0, m_dwWndTransparencyVal, LWA_ALPHA);
         }
     }

3.2、实现叠加在其他窗口上面的水印窗口

       水印窗口上绘制水印文字,水印窗口要是完全穿透的,即鼠标能点击到水印窗口下面的东西,比如如下的效果:

具体的实现方法是,先将目标窗口绘制成黑色背景,然后在黑色背景上绘制红色文字,绘制文字时设置背景透明(即文字区域没有背景色,默认情况下是由背景色的,上图中文字是有背景色的):

SetBkMode( HDC, TRANSPARENT );

然后调用SetLayeredWindowAttributes,将黑色背景色区域设置为完全透明区域(此处是内容透明):

SetLayeredWindowAttributes(m_hWnd, RGB(0,0,0), 255, LWA_COLORKEY);

这样除了红色的文字,整个窗口的内容都是透明的。
       最后还有一个最重要的一点是给窗口设置WS_EX_TRANSPARENT风格,即鼠标是完全穿透的。前面调用SetLayeredWindowAttributes只是使内容透明。

4、调用UpdateLayeredWindow实现特殊效果的窗口

4.1 实现除边框外的中间区域全透明的桌面区域共享选择窗口

       在视频会议的桌面共享功能中,除了整个桌面图像的共享,还支持选择桌面的一部分区域进行共享,即桌面区域共享,如下图所示:

多个主流视频会议厂商的软件终端都支持这个桌面区域共享的功能。这个功能的难点是在于如何实现这个除边框之外的中间区域完全透明且鼠标穿透的特殊窗口

      其实这个窗口很好实现,在窗口边界绘制图片,然后中间区域不用绘制(中间区域是创建位图时默认的黑色,黑色区域在调用UpdateLayeredWindow后会直接透明且鼠标穿透),这样调用UpdateLayeredWindow后就能实现了。相关代码如下:

#define   DESKTOP_SHARE_AREA_SEL_WND_EDGESIZE_TOP_HEIGHT       27
#define   DESKTOP_SHARE_AREA_SEL_WND_EDGESIZE_LEFT_WIDTH       5
#define   DESKTOP_SHARE_AREA_SEL_WND_EDGESIZE_RIGHT_WIDTH      5
#define   DESKTOP_SHARE_AREA_SEL_WND_EDGESIZE_BOTTOM_HEIGHT    5
 
 
void CDesktopShareAreaSelDlg::Update()
{
    if ( m_BkImg.IsNull() )
    {
        return;
    }
 
    RECT rcWnd;
    ::GetWindowRect(m_hWnd, &rcWnd);
    int nWndWidth = rcWnd.right - rcWnd.left;
    int nWndHeight = rcWnd.bottom - rcWnd.top;
 
    // Create the alpha blending bitmap
    BITMAPINFO bmi;        // bitmap header
 
    ZeroMemory(&bmi, sizeof(BITMAPINFO));
    bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
    bmi.bmiHeader.biWidth = nWndWidth;
    bmi.bmiHeader.biHeight = nWndHeight;
    bmi.bmiHeader.biPlanes = 1;
    bmi.bmiHeader.biBitCount = 32;         // four 8-bit components
    bmi.bmiHeader.biCompression = BI_RGB;
    bmi.bmiHeader.biSizeImage = nWndWidth * nWndHeight * 4;
 
    BYTE *pvBits;          // pointer to DIB section
    HBITMAP hbitmap = CreateDIBSection(NULL, &bmi, DIB_RGB_COLORS, (void **)&pvBits, NULL, 0);
    if (pvBits == NULL) {
        return;
    }
 
    ZeroMemory(pvBits, bmi.bmiHeader.biSizeImage);
 
    // 创建内存DC
    HDC hMemDC = CreateCompatibleDC(NULL);
    HBITMAP hOriBmp = (HBITMAP)SelectObject(hMemDC, hbitmap);
 
    int nImgWidth = m_BkImg.GetWidth();
    int nImgHeight = m_BkImg.GetHeight();
 
    // 将窗口left、top、right、bottom四个方向上的图片绘制到窗口边框的位置
    m_BkImg.Draw( hMemDC, 0, 0, DESKTOP_SHARE_AREA_SEL_WND_EDGESIZE_LEFT_WIDTH, nWndHeight, 0, 0, DESKTOP_SHARE_AREA_SEL_WND_EDGESIZE_LEFT_WIDTH, nImgHeight );
    m_BkImg.Draw( hMemDC, nWndWidth - DESKTOP_SHARE_AREA_SEL_WND_EDGESIZE_RIGHT_WIDTH, 0, DESKTOP_SHARE_AREA_SEL_WND_EDGESIZE_RIGHT_WIDTH, nWndHeight, nImgWidth-DESKTOP_SHARE_AREA_SEL_WND_EDGESIZE_RIGHT_WIDTH, 0, DESKTOP_SHARE_AREA_SEL_WND_EDGESIZE_RIGHT_WIDTH, nImgHeight );
    m_BkImg.Draw( hMemDC, 0, 0, nWndWidth, DESKTOP_SHARE_AREA_SEL_WND_EDGESIZE_TOP_HEIGHT, 0, 0, nImgWidth, DESKTOP_SHARE_AREA_SEL_WND_EDGESIZE_TOP_HEIGHT );
    m_BkImg.Draw( hMemDC, 0, nWndHeight-DESKTOP_SHARE_AREA_SEL_WND_EDGESIZE_BOTTOM_HEIGHT, nWndWidth, DESKTOP_SHARE_AREA_SEL_WND_EDGESIZE_BOTTOM_HEIGHT, 0, nImgHeight-DESKTOP_SHARE_AREA_SEL_WND_EDGESIZE_BOTTOM_HEIGHT, nImgWidth, DESKTOP_SHARE_AREA_SEL_WND_EDGESIZE_BOTTOM_HEIGHT );
 
    POINT ptDst = {rcWnd.left, rcWnd.top};
    POINT ptSrc = {0, 0};
    SIZE WndSize = {nWndWidth, nWndHeight};
    BLENDFUNCTION blendPixelFunction= { AC_SRC_OVER, 0, 255, AC_SRC_ALPHA };
 
    BOOL bRet= UpdateLayeredWindow(m_hWnd, NULL, &ptDst, &WndSize, hMemDC,
        &ptSrc, 0, &blendPixelFunction, ULW_ALPHA);
 
    DWORD dwRet = GetLastError();
 
    InvalidateRect( m_hWnd, &rcWnd, TRUE );
    //_ASSERT(bRet); // something was wrong....
 
    // Delete used resources
    SelectObject(hMemDC, hOriBmp);
    DeleteObject(hbitmap);
    DeleteDC(hMemDC);
}

       注意,这个区域选择窗口,是不能设置WS_EX_TRANSPARENT属性的,即不是整个窗口都是鼠标都穿透的。窗口的边框要支持点击后的响应操作,比如可以拖动任务栏移动选择窗口,点击边框可以改变窗口的大小。

4.2 在目标窗口的边界加上阴影的效果

       很多软件的窗口边界都有阴影的效果,软件弹出的每个窗口都由阴影的效果。比如360安全卫士的主窗口:

仔细地观察一下,窗口的边界有一层明显的阴影效果。

       这样的阴影边界到底是怎么实现的呢?其实和上面说到的桌面区域选择窗口实现思路有点类似,但要稍微难点。实现起来也挺简单,讲完之后你就知道没那么难了!

       实现思路是这样的:实现一个只有阴影边框的窗口,首先该窗口要设置WS_EX_TRANSPARENT风格,即鼠标是完全穿透的;其次除边框之外的区域在内容上是完全透明的;然后将目标窗口(就是软件中弹出的各个窗口)设置为父窗口。因为是父子窗口,阴影窗口始终悬浮在父窗口之上的,因为阴影窗口中间区域是完全透明的,所以能看到父窗口的内容;再加上浮在上方的阴影窗口是鼠标穿透的,鼠标点击到阴影窗口上实际上响应点击的是后面的父窗口,正好是我们的目标窗口。

       这样对于父窗口,用户就感知不到悬浮在上方的阴影窗口的存在,只有在边界区域才能看到,就是这种特殊的机制,实现了我们想要的阴影窗口的效果。

       当然除此之外,我们还有很多额外的工作要做,比如控制阴影窗口正好比父窗口(目标窗口)大一个边界的大小,并且阴影窗口要跟随父窗口实时移动。关键代码如下所示:

// 关键代码段1:创建阴影窗口
void CShadowWnd::Create( HWND hParentWnd )
{
	// Already initialized
	_ASSERT(s_hInstance != INVALID_HANDLE_VALUE);

	// Add parent window - shadow pair to the map
	_ASSERT(s_Shadowmap.find(hParentWnd) == s_Shadowmap.end());	// Only one shadow for each window
	s_Shadowmap[hParentWnd] = this;
	m_hParent = hParentWnd;
	// Create the shadow window
	// WS_EX_TRANSPARENT - 本shadow窗口是鼠标能穿透的
	m_hWnd = CreateWindowEx(WS_EX_LAYERED | WS_EX_TRANSPARENT | WS_EX_NOACTIVATE, lpszWndClassName, NULL,
		WS_POPUP | WS_BORDER ,
		CW_USEDEFAULT, 0, 0, 0, hParentWnd, NULL, s_hInstance, NULL);

	// Determine the initial show state of shadow according to Aero
	m_Status = SS_ENABLED;	// Enabled by default

// 	Show(hParentWnd);	// Show the shadow if conditions are met
	ShowWindow( m_hWnd, SW_HIDE );

	// Replace the original WndProc of parent window to steal messages
	m_OriParentProc = GetWindowLong(hParentWnd, GWL_WNDPROC);

#pragma warning(disable: 4311)	// temporrarily disable the type_cast warning in Win32
	SetWindowLong(hParentWnd, GWL_WNDPROC, (LONG)ParentProc);
#pragma warning(default: 4311)

}


// 关键代码段2:拦截目标窗口的窗口处理过程,便于截获消息
LRESULT CALLBACK CShadowWnd::ParentProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	//_ASSERT(s_Shadowmap.find(hwnd) != s_Shadowmap.end());	// Shadow must have been attached

	// 由于在Debug调试状态下,关闭带阴影窗口时可能会崩溃,所以此处规避掉
	if ( s_Shadowmap.find(hwnd) == s_Shadowmap.end() )
	{
		// s_Shadowmap列表中没找到hwnd,直接返回
		return S_OK;
	}

	CSkinShadow *pThis = s_Shadowmap[hwnd];

#pragma warning(disable: 4312)	// temporrarily disable the type_cast warning in Win32
	// Call the default(original) window procedure for other messages or messages processed but not returned
	WNDPROC pDefProc = (WNDPROC)pThis->m_OriParentProc;
#pragma warning(default: 4312)

	switch(uMsg)
	{
	case WM_MOVE:
		if(pThis->m_Status & SS_VISABLE)
		{			
			LONG lParentStyle = GetWindowLong(hwnd, GWL_STYLE);
			
			RECT WndRect;
			GetWindowRect(hwnd, &WndRect);
			SetWindowPos(pThis->m_hWnd, 0,
				WndRect.left - pThis->m_nEdgeSize, WndRect.top - pThis->m_nEdgeSize,
				0, 0, SWP_NOSIZE | SWP_NOACTIVATE | SWP_NOZORDER);
		}
		break;

	case WM_DISPLAYCHANGE:
	case WM_WINDOWPOSCHANGING:
		if(pThis->m_Status & SS_VISABLE)
		{
			::PostMessage( hwnd, WM_MOVE, 0, 0 );
			pThis->m_bUpdate = true;
		}
		break;

	case WM_SIZE:
		if(pThis->m_Status & SS_ENABLED)
		{
			if(SIZE_MAXIMIZED == wParam || SIZE_MINIMIZED == wParam)
			{
				pThis->Update(hwnd);
				ShowWindow(pThis->m_hWnd, SW_HIDE);
				pThis->m_Status &= ~SS_VISABLE;
			}
			else
			{
				LONG lParentStyle = GetWindowLong(hwnd, GWL_STYLE);
				if(WS_VISIBLE & lParentStyle)	// Parent may be resized even if invisible
				{
					pThis->m_Status |= SS_PARENTVISIBLE;
					if(!(pThis->m_Status & SS_VISABLE))
					{
						pThis->m_Status |= SS_VISABLE;
						// Update before show, because if not, restore from maximized will
						// see a glance misplaced shadow
						pThis->Update(hwnd);
						ShowWindow(pThis->m_hWnd, SW_SHOWNA);						
						// If restore from minimized, the window region will not be updated until WM_PAINT:(
						pThis->m_bUpdate = true;
					}
					// Awful! It seems that if the window size was not decreased
					// the window region would never be updated until WM_PAINT was sent.
					// So do not Update() until next WM_PAINT is received in this case
					else if(LOWORD(lParam) > LOWORD(pThis->m_WndSize) || HIWORD(lParam) > HIWORD(pThis->m_WndSize))
					{
						pThis->m_bUpdate = true;
					}
					else
					{
						pThis->Update(hwnd);
					}
				}

			}
			pThis->m_WndSize = lParam;
		}
		break;

	case WM_PAINT:
		{
			if(pThis->m_bUpdate)
			{
				pThis->Update(hwnd);
				pThis->m_bUpdate = false;
			}
		}
		break;
		// In some cases of sizing, the up-right corner of the parent window region would not be properly updated
		// Update() again when sizing is finished
	case WM_EXITSIZEMOVE:
		if( pThis->m_Status & SS_VISABLE )
		{
			pThis->Update(hwnd);
		}
		break;

	case WM_SHOWWINDOW:
		if(pThis->m_Status & SS_ENABLED)
		{
			LRESULT lResult =  pDefProc(hwnd, uMsg, wParam, lParam);
			if(!wParam)	// the window is being hidden
			{
				ShowWindow(pThis->m_hWnd, SW_HIDE);	
				pThis->m_Status &= ~(SS_VISABLE | SS_PARENTVISIBLE);
			}
			else
			{
				pThis->m_bUpdate = true;
				pThis->m_Status |= SS_VISABLE;
				pThis->Update(hwnd);
				ShowWindow(pThis->m_hWnd, SW_SHOWNA);				
			}
			return lResult;
		}
		break;

	case WM_DESTROY:
		DestroyWindow(pThis->m_hWnd);	// Destroy the shadow
		break;
		
	case WM_NCDESTROY:
// 		s_Shadowmap.erase(hwnd);	// Remove this window and shadow from the map
		break;
	}

	// Call the default(original) window procedure for other messages or messages processed but not returned
	return pDefProc(hwnd, uMsg, wParam, lParam);

}


// 关键代码段3:封装了所有窗口绘制的内容,包括调用UpdateLayeredWindow
void CShadowWnd::Update(HWND hParent)
{
	if ( m_Image.IsNull() )
	{
		return;
	}

	Sleep(10);
	
	// 此处可以看得出来,阴影窗口比父窗口大出一个边界,上下左右都大出
	// 8个像素
	RECT WndRect;
	GetWindowRect(hParent, &WndRect);
	int nShadWndWid = WndRect.right - WndRect.left + m_nEdgeSize * 2;
	int nShadWndHei = WndRect.bottom - WndRect.top + m_nEdgeSize * 2;

	// Create the alpha blending bitmap
	BITMAPINFO bmi;        // bitmap header

	ZeroMemory(&bmi, sizeof(BITMAPINFO));
	bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
	bmi.bmiHeader.biWidth = nShadWndWid;
	bmi.bmiHeader.biHeight = nShadWndHei;
	bmi.bmiHeader.biPlanes = 1;
	bmi.bmiHeader.biBitCount = 32;         // four 8-bit components
	bmi.bmiHeader.biCompression = BI_RGB;
	bmi.bmiHeader.biSizeImage = nShadWndWid * nShadWndHei * 4;

	BYTE *pvBits;          // pointer to DIB section
	HBITMAP hbitmap = CreateDIBSection(NULL, &bmi, DIB_RGB_COLORS, (void **)&pvBits, NULL, 0);
    if (pvBits == NULL) {
        return;
    }

	ZeroMemory(pvBits, bmi.bmiHeader.biSizeImage);

	HDC hMemDC = CreateCompatibleDC(NULL);
	HBITMAP hOriBmp = (HBITMAP)SelectObject(hMemDC, hbitmap);

	int nWidth = m_Image.GetWidth();
	int nHeight = m_Image.GetHeight();
	// 四个角
	m_Image.Draw( hMemDC, 0, 0, m_nEdgeSize, m_nEdgeSize, 0, 0, m_nEdgeSize, m_nEdgeSize );
	m_Image.Draw( hMemDC, nShadWndWid-m_nEdgeSize, 0, m_nEdgeSize, m_nEdgeSize, nWidth-m_nEdgeSize, 0, m_nEdgeSize, m_nEdgeSize );
	m_Image.Draw( hMemDC, 0, nShadWndHei-m_nEdgeSize, m_nEdgeSize, m_nEdgeSize, 0, nHeight-m_nEdgeSize, m_nEdgeSize, m_nEdgeSize );
	m_Image.Draw( hMemDC, nShadWndWid-m_nEdgeSize, nShadWndHei-m_nEdgeSize, m_nEdgeSize, m_nEdgeSize, nWidth-m_nEdgeSize, nHeight-m_nEdgeSize, m_nEdgeSize, m_nEdgeSize );
	// 四条边
	m_Image.Draw( hMemDC, 0, m_nEdgeSize, m_nEdgeSize, nShadWndHei-m_nEdgeSize*2, 0, m_nEdgeSize, m_nEdgeSize, nHeight-m_nEdgeSize*2 );
	m_Image.Draw( hMemDC, nShadWndWid-m_nEdgeSize, m_nEdgeSize, m_nEdgeSize, nShadWndHei-m_nEdgeSize*2, nWidth-m_nEdgeSize, m_nEdgeSize, m_nEdgeSize, nHeight-m_nEdgeSize*2 );
	m_Image.Draw( hMemDC, m_nEdgeSize, 0, nShadWndWid-m_nEdgeSize*2, m_nEdgeSize, m_nEdgeSize, 0, nWidth-m_nEdgeSize*2, m_nEdgeSize );
	m_Image.Draw( hMemDC, m_nEdgeSize, nShadWndHei-m_nEdgeSize, nShadWndWid-m_nEdgeSize*2, m_nEdgeSize, m_nEdgeSize, nHeight-m_nEdgeSize, nWidth-m_nEdgeSize*2, m_nEdgeSize );

	POINT ptDst = {WndRect.left - m_nEdgeSize, WndRect.top - m_nEdgeSize};
	POINT ptSrc = {0, 0};
	SIZE WndSize = {nShadWndWid, nShadWndHei};
	BLENDFUNCTION blendPixelFunction= { AC_SRC_OVER, 0, 255, AC_SRC_ALPHA };

	// 调整阴影窗口的大小
	MoveWindow(m_hWnd, ptDst.x, ptDst.y, nShadWndWid, nShadWndHei, FALSE);	

	BOOL bRet= UpdateLayeredWindow(m_hWnd, NULL, &ptDst, &WndSize, hMemDC,
		&ptSrc, 0, &blendPixelFunction, ULW_ALPHA);
	
	InvalidateRect( m_hWnd, &WndRect, TRUE );
	_ASSERT(bRet); // something was wrong....

	// Delete used resources
	SelectObject(hMemDC, hOriBmp);
	DeleteObject(hbitmap);
	DeleteDC(hMemDC);
}

4.3 实现各种具有酷炫视觉效果的不规则的非直角窗口

       比如网上很多使用实现的各种好看不规则图样的(非直角边界)例子,以360的加速球为例:

其实原始的窗口都是矩形的,我们在实现时做一个带透明区域的png图片就可以了,除了不规则的有效颜色区域之外都做成透明的。将图片绘制到黑色底板的位图上,调用后就能实现各种异形窗口了。如果要在窗口上做其他的控件或按钮都需要自己绘图绘上去的,就像directui窗口中的控件绘制一样。具体要实现什么样的效果,还是看具体的需求去调试代码实现吧!

5、SetLayeredWindowAttributes和UpdateLayeredWindow不能同时调用

       不能对同一个窗口同时调用SetLayeredWindowAttributes和UpdateLayeredWindow两个函数。第一个调用的函数会成功,后面调用的那个会失败,使用GetLastError获取API调用后的LastError值为87,到VS的错误查找工具中查看该错误码的含义:

6、UpdateLayeredWindow函数调用失败的可能原因分析

        在调试代码过程中,我们时常会遇到,窗口显示后没有达到我们预想的效果,往往都是因为UpdateLayeredWindow函数调用失败了。可以调用GetLastError函数获取调用失败的错误码,然后到VS的错误查找工具中查看错误码的含义,然后去查找可能调用失败的原因。
       根据以前调试UpdateLayeredWindow函数调用的经验,主要有以下几种情况会导致该函数执行失败。

6.1、被操作的窗口时Child子窗口

        WS_EX_LAYERED窗口风格只对Popup弹出窗口有效,对Child子窗口无效,所以对Child子窗口调用UpdateLayeredWindow会失败。在微软MSDN的对SetLayeredWindowAttributes函数说明中,有提到win8系统中WS_EX_LAYERED已经做到了对Child子窗口的支持:

        但只在win8及以上系统中才支持,至少目前意义不是很大。因为我们开发的应用软件要支持win7、win8和win10所有的系统,甚至要支持古老的XP系统,所以只有win8及以系统上才支持child子窗口是不行的。目前也有不少人还在用win7系统的。等到哪天将XP和win7等系统彻底淘汰,没人再用了,大家都用win10及以上系统了,就可以使用这个新的支持了。

6.2、没有设置WS_EX_LAYERED窗口风格

       有可能最开始忘记设置WS_EX_LAYERED窗口风格,也有可能最开始设置了,但是在代码执行中途风格被其他地方的代码取消掉了。

       之前实现桌面区域共享窗口时就遇到中途被取消的问题。当时窗口使用的是dui窗口,在窗口创建后,dui框架中会检查窗口的透明度值,如果是255,则会将WS_EX_LAYERED风格取消:

	// 设置透明度,调用内核库函数
	void CPaintManagerUI::SetTransparent(int nOpacity)
	{
		if (NULL == m_hWndPaint)
		{
			return;
		}

		typedef BOOL(__stdcall *PFUNCSETLAYEREDWINDOWATTR)(HWND, COLORREF, BYTE, DWORD);
		PFUNCSETLAYEREDWINDOWATTR fSetLayeredWindowAttributes;

		HMODULE hUser32 = ::GetModuleHandle(_T("User32.dll"));
		if (hUser32)
		{
			fSetLayeredWindowAttributes =
				(PFUNCSETLAYEREDWINDOWATTR)::GetProcAddress(hUser32, "SetLayeredWindowAttributes");

			if (NULL == fSetLayeredWindowAttributes)
			{
				return;
			}
		}

		DWORD dwStyle = ::GetWindowLong(m_hWndPaint, GWL_EXSTYLE);
		DWORD dwNewStyle = dwStyle;
		if (nOpacity >= 0 && nOpacity < 255)
		{
			dwNewStyle |= WS_EX_LAYERED;
		}
		else
		{
			dwNewStyle &= ~WS_EX_LAYERED;  // 就是这个地方将WS_EX_LAYERED窗口风格取消了
		}

		if (dwStyle != dwNewStyle)
		{
			::SetWindowLong(m_hWndPaint, GWL_EXSTYLE, dwNewStyle);
		}

		fSetLayeredWindowAttributes(m_hWndPaint, 0, nOpacity, LWA_ALPHA);
	}

duilib这点明显是有问题的,即使窗口没设置透明,也有可能调用的,所以是不合理的,要修改。
也可以通过spy++去查看目标窗口有没有设置WS_EX_LAYERED风格,或者有没有设置成功。

       但当函数UpdateLayeredWindow执行失败,窗口可能是不可见的,这样没法使用spy++拖动按钮去spy目标窗口,但我们可以直接使用窗口句柄、窗口标题或窗口类名去搜索:

        也可以通过添加测试代码去测试,获取窗口的风格,看看有没有风格:

GetWindowLong(hWnd, GWL_EXSTYLE) & WS_EX_LAYERED == WS_EX_LAYERED;

6.3、调用之前创建的位图没有alpha通道

       调用之前要求创建的位图必须是包含alpha通道的32位位图,否则调用后的错误码会是87。如果使用API函数CreateCompatibleBitmap去创建,则依赖于系统中设置的色彩值,如果当前设置的16位真彩色,则创建的不是32位位图。所以要调用CreateDIBSection函数去创建设备无关的位图,创建时可以指定位图的位数,其中DIB就是Device Independent Bitmap,设备无关位图。

        此外调用CreateCompatibleBitmap和CreateDIBSection两个函数去创建位图的方式,在内存占用上也有明显的区别,CreateCompatibleBitmap创建位图需要的内存比CreateDIBSection大的多,所以一般为了防止系统内存不够用导致创建位图失败,我们需要调用CreateDIBSection去创建位图。在内存不够的时使用CreateCompatibleBitmap创建失败的情况,我们在项目中遇到好几次了!

6.4、之前调用了SetLayeredWindowAttributes,再调用UpdateLayeredWindow会失败

       两个函数不能同时调用,之前开发过程中就遇到这样的问题,也是duilib框架的问题,duilib框架自动调用了函数SetLayeredWindowAttributes:

	// 设置透明度,调用内核库函数
	void CPaintManagerUI::SetTransparent(int nOpacity)
	{
		if (NULL == m_hWndPaint)
		{
			return;
		}

		typedef BOOL(__stdcall *PFUNCSETLAYEREDWINDOWATTR)(HWND, COLORREF, BYTE, DWORD);
		PFUNCSETLAYEREDWINDOWATTR fSetLayeredWindowAttributes;

		HMODULE hUser32 = ::GetModuleHandle(_T("User32.dll"));
		if (hUser32)
		{
			fSetLayeredWindowAttributes =
				(PFUNCSETLAYEREDWINDOWATTR)::GetProcAddress(hUser32, "SetLayeredWindowAttributes");

			if (NULL == fSetLayeredWindowAttributes)
			{
				return;
			}
		}

		DWORD dwStyle = ::GetWindowLong(m_hWndPaint, GWL_EXSTYLE);
		DWORD dwNewStyle = dwStyle;
		if (nOpacity >= 0 && nOpacity < 255)
		{
			dwNewStyle |= WS_EX_LAYERED;
		}
		else
		{
			dwNewStyle &= ~WS_EX_LAYERED;  
		}

		if (dwStyle != dwNewStyle)
		{
			::SetWindowLong(m_hWndPaint, GWL_EXSTYLE, dwNewStyle);
		}

        // 是此处调用了SetLayeredWindowAttributes
		fSetLayeredWindowAttributes(m_hWndPaint, 0, nOpacity, LWA_ALPHA);
	}

导致后面我们再调用UpdateLayeredWindow就失败了。

7、总结

        本文详细介绍了分层窗口的应用场景及相关实现方法,并对调用UpdateLayeredWindow失败的原因进行了总结,希望能给大家提供一个参考。
 

以上是关于两万字总结Windows系统中的Layered分层窗口技术的主要内容,如果未能解决你的问题,请参考以下文章

两万字长文,史上最全 C++ 年度总结!

两万字长文,史上最全 C++ 年度总结!

两万字长文,史上最全 C++ 年度总结!

长达两万字的Elasticsearch分布式集群运维方方面面总结 #yyds干货盘点#

整洁面向对象分层架构 (Clean Object-Oriented and Layered Architecture)

必知必会面试10多家中大厂后的两万字总结——❤️JVM篇❤️(建议收藏)