GithubHelp home page GithubHelp logo

walklang / uilib Goto Github PK

View Code? Open in Web Editor NEW
44.0 4.0 6.0 12 KB

A simply and powerful ui script framework library. via http://www.uilib.cn

License: Apache License 2.0

C++ 9.00% C 91.00%
cpp opengl directx gdi

uilib's Introduction

uilib's People

Contributors

suhao avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

uilib's Issues

透明窗口:UpdateLayeredWindow failed, error code is 317

原因:

  1. 大小不一样
  2. 电脑开启了16位增强色

电脑开启16位增强色解决办法

::UpdateLayeredWindow的源位图应该使用独立位图,而不是兼容位图

# 创建独立位图:

BITMAPINFO bmi;
memset(&bmi, 0, sizeof(bmi));
bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
bmi.bmiHeader.biWidth = width;
bmi.bmiHeader.biHeight = -height; // top-down image
bmi.bmiHeader.biPlanes = 1;
bmi.bmiHeader.biBitCount = 32;
bmi.bmiHeader.biCompression = BI_RGB;
bmi.bmiHeader.biSizeImage = 0;
BYTE* lpBitmapBits = NULL;
m_hbmpOffscreen = ::CreateDIBSection(m_hDcOffscreen, &bmi, DIB_RGB_COLORS, (LPVOID*)&lpBitmapBits, NULL, 0);

# 创建兼容位图:
m_hbmpOffscreen = ::CreateCompatibleBitmap(hdc, width, height);

触发16位增强色

屏幕右键--个性化--左下角--显示--左上角--更改显示器设置--右边中部--高级设置--监示器--下方--颜色--有真彩色32位和增强色16位选择

高级设置
监视器

透明窗口:实现方式

DWM: Desktop Window Manager

从Windows Vista开始,Aero Glass效果被应用在了Home Premium以上的系统中(Home Basic不具有该效果)。这种效果是由DWM(Desktop Window Manager)来控制的。对于一般的程序,缺省将在窗口边框应用这种效果。但如果我们想要更多的控制,比如让客户区的一部分也呈现这种效果,那也非常的简单。不需要我们在程序里做任何复杂的算法,我们只需要调API,交给DWM去做就可以了。

DWM相关操作的MSDN说明:Desktop Window Manager (DWM) APIs.

http://msdn.microsoft.com/en-us/library/aa969527(v=VS.85).aspx

Header Dwmapi.h
Library Dwmapi.lib
DLL Dwmapi.dll

一、Composition(窗口合成) and Non-client Rendering(非客户区渲染)

非客户区通常包括窗口标题栏和窗口边框。缺省状态下,非客户区会被渲染成毛玻璃效果,这也称为Compostion。有几个函数可以控制系统和当前窗口的渲染方式。同时也有Windows消息用于接受渲染模式的改变。

   1.检测系统是否开启Aero Glass。使用函数DwmIsCompositionEnabled检测系统当前是否开启了Aero Glass特效。它接受一个BOOL参数,并将当前状态存储到其中。函数原型:HRESULT DwmIsCompositionEnabled(BOOL *pfEnabled);

   2.开启/关闭Aero Glass。使用函数DwmEnableComposition开启或关闭系统Aero Glass效果,传入DWM_EC_ENABLECOMPOSITION开启,传入DWM_EC_DISABLECOMPOSITION关闭。

   3.开启/关闭当前窗口的非客户区渲染。函数DwmSetWindowAttribute用于设置窗口属性,属性DWMWA_NCRENDERING_POLICY控制当前窗口是否使用非客户区渲染。DWMNCRP_ENABLED开启,DWMNCRP_DISABLED关闭。当系统的Aero Glass关闭时,设置无效。与之对应,使用函数DwmGetWindowAttribute可以检测当前窗口属性。

   4.响应系统Aero Glass的开启或关闭。当Aero Glass被开启或关闭时,Windows会发送消息WM_DWMCOMPOSITIONCHANGED,使用函数DwmIsCompositionEnabled检测状态。

   5.响应窗口非客户区渲染的开启或关闭。当前窗口的非客户区渲染开启或关闭时,Windows会发送消息WM_DWMNCRENDERINGCHANGED,wParam指示当前状态。

二、Transition(窗口动画) and ColorizationColor(主题颜色)

Transition控制是否以动画方式显示窗口的最小化和还原。通过使用函数DwmSetWindowAttribute,设置属性DWMWA_TRANSITIONS_FORCEDISABLED,开启或关闭窗口动画。该设置只对当前窗口有效。

   当用户通过控制面板修改主题颜色时,Windows将发送消息WM_DWMCOLORIZATIONCOLORCHANGED,程序中通过函数DwmGetColorizationColor取得当前主题颜色,以及是否透明。通过响应颜色的变更,可以让程序的颜色风格随主题风格而变化。

三、开启客户区域Aero Glass效果

函数DwmEnableBlurBehindWindow开启客户区的Aero Glass效果,第一个参数为窗口句柄,第二个参数为一个DWM_BLURBEHIND结构。其中fEnable设置是否开启客户区Glass效果。hRgnBlur设置Glass效果的区域,该项设置为NULL将使整个客户区呈现Glass效果,设置为一个正确的区域后,该区域将呈现Glass效果, 而区域以外为完全透明。要呈现透明效果需要客户区原始的颜色为黑色,可以在WM_PAINT消息中绘制客户区,下面的代码使用GDI+,在Aero Glass开启时将整个窗口绘制为黑色,Aero Glass关闭时绘制为灰色:

	case WM_PAINT:
		{
			PAINTSTRUCT ps;
			HDC hDC = BeginPaint(hWnd, &ps);
			//不要直接使用窗口句柄创建Graphics,会导致闪烁
			Graphics graph(hDC);
			//清除客户区域
			RECT rcClient;
			GetClientRect(hWnd, &rcClient);
			BOOL bCompEnabled;
			DwmIsCompositionEnabled(&bCompEnabled);
			SolidBrush br(bCompEnabled? Color::Black : Color::DarkGray);
			graph.FillRectangle(&br, Rect(rcClient.left, rcClient.top, 
				rcClient.right, rcClient.bottom));
			EndPaint(hWnd, &ps);
		}
		break;

GDI+的初始化和关闭仍然是必须的:

	//初始化GDI+
	ULONG_PTR token;
	GdiplusStartupInput input;
	GdiplusStartup(&token, &input, NULL);
	//*********************************
	//关闭GDI+
	GdiplusShutdown(token);

下面代码将整个客户区设置为Glass效果:

	DWM_BLURBEHIND bb = {0};
	bb.dwFlags = DWM_BB_ENABLE | DWM_BB_BLURREGION;
	bb.fEnable = true;
	bb.hRgnBlur = NULL;
	DwmEnableBlurBehindWindow(hWnd, &bb);

image

下面代码将客户区中心一个椭圆的区域设置为Glass效果:

	RECT rect;
	GetWindowRect(hWnd, &rect);
	int width = 300, height = 200;
	//居中椭圆形
	HRGN hRgn = CreateEllipticRgn((rect.right - rect.left)/2 - width/2, 
		(rect.bottom - rect.top)/2 - height/2, (rect.right - rect.left)/2 + width/2, 
		(rect.bottom - rect.top)/2 + height/2);
	DWM_BLURBEHIND bb = {0};
	bb.dwFlags = DWM_BB_ENABLE | DWM_BB_BLURREGION;
	bb.fEnable = true;
	bb.hRgnBlur = hRgn;
	DwmEnableBlurBehindWindow(hWnd, &bb);

image

四、窗口边框向客户区扩展

上面的方式中,非客户区和客户区之间仍然有界限。如何增大Glass效果的范围,并且消除界限呢?那就是使窗口边框向客户区扩展,利用函数DwmExtendFrameIntoClientArea实现。函数接受一个窗口句柄和一个MARGINS类型的参数。MARGINS指定了在上下左右4个方向上扩展的范围。如果4个值均为-1,则扩展到整个客户区。

				MARGINS margins = {50, 50, 50, 50};
				DwmExtendFrameIntoClientArea(hWnd, &margins);

image

				MARGINS margins2 = {-1};	//将扩展到整个客户区
				DwmExtendFrameIntoClientArea(hWnd, &margins2);

image

五、在窗口上绘制图形

PNG图片带有alpha通道,可以与Aero Glass很好的配合。利用GDI+显示PNG图片非常方便,下面的代码将一张PNG图片加载到内存中:

Bitmap bmp  = Bitmap::FromFile(L"Ferrari.png", false);

在WM_PAINT消息处理中,将整个客户区绘制为黑色以后,利用GDI+将图片绘制到窗口客户区:

			//绘制图形
			int width = bmp->GetWidth();
			int height = bmp->GetHeight();
			Rect rc(30, 30, width, height);
			graph.DrawImage(bmp, rc, 0, 0, width, height, UnitPixel);

image

六、文本的绘制

当窗口大范围的透明之后,窗口上的文字的阅读成了一个问题。Windows的解决办法是为文字加上发光效果(Glowing),标题栏的文本使用的就是这种方式。我们在自己的程序中可以使用DrawThemeTextEx函数来绘制发光的文字。该函数的原型定义如下:

HRESULT DrawThemeTextEx(          HTHEME hTheme,
    HDC hdc,
    int iPartId,
    int iStateId,
    LPCWSTR pszText,
    int iCharCount,
    DWORD dwFlags,
    LPRECT pRect,
    const DTTOPTS *pOptions
);

hTheme是一个主题句柄,可以使用OpenThemeData获得,OpenThemeData函数接受一个窗口句柄,和主题类的名称。iPartId和iStateId分别代表主题类中的Part和State,所有可用的主题类、Part和state在SDK的帮助文档中可以查看到。pszText是要绘制的文本。iCharCount为文字个数,-1代表绘制全部文本。dwFlags指定文本格式。pRect为文本绘制区域。pOptions中可以设定文本的发光、阴影等效果。HDC是一个设备上下文句柄,为了实现类似于标题栏中文本的发光效果,这里不能使用由BeginPaint得到的句柄,而是要使用CreateCompatibleDC创建一个内存中的句柄,并且要创建一张位图,通过内存句柄将文本绘制到位图上。然后再将位图转移到窗口上。下面的函数封装了绘制发光文本的过程:

//绘制发光文字
void DrawGlowingText(HDC hDC, LPWSTR szText, RECT &rcArea, 
	DWORD dwTextFlags = DT_LEFT | DT_VCENTER | DT_SINGLELINE, int iGlowSize = 10)
{
	//获取主题句柄
	HTHEME hThm = OpenThemeData(GetDesktopWindow(), L"TextStyle");
	//创建DIB
	HDC hMemDC = CreateCompatibleDC(hDC);
	BITMAPINFO bmpinfo = {0};
	bmpinfo.bmiHeader.biSize = sizeof(bmpinfo.bmiHeader);
	bmpinfo.bmiHeader.biBitCount = 32;
	bmpinfo.bmiHeader.biCompression = BI_RGB;
	bmpinfo.bmiHeader.biPlanes = 1;
	bmpinfo.bmiHeader.biWidth = rcArea.right - rcArea.left;
	bmpinfo.bmiHeader.biHeight = -(rcArea.bottom - rcArea.top);
	HBITMAP hBmp = CreateDIBSection(hMemDC, &bmpinfo, DIB_RGB_COLORS, 0, NULL, 0);
	if (hBmp == NULL) return;
	HGDIOBJ hBmpOld = SelectObject(hMemDC, hBmp);
	//绘制选项
	DTTOPTS dttopts = {0};
	dttopts.dwSize = sizeof(DTTOPTS);
	dttopts.dwFlags = DTT_GLOWSIZE | DTT_COMPOSITED;
	dttopts.iGlowSize = iGlowSize;	//发光的范围大小
	//绘制文本
	RECT rc = {0, 0, rcArea.right - rcArea.left, rcArea.bottom - rcArea.top};
	HRESULT hr = DrawThemeTextEx(hThm, hMemDC, TEXT_LABEL, 0, szText, -1, dwTextFlags , &rc, &dttopts);
	if(FAILED(hr)) return;
	BitBlt(hDC, rcArea.left, rcArea.top, rcArea.right - rcArea.left, 
		rcArea.bottom - rcArea.top, hMemDC, 0, 0, SRCCOPY | CAPTUREBLT);
	//Clear
	SelectObject(hMemDC, hBmpOld);
	DeleteObject(hBmp);
	DeleteDC(hMemDC);
	CloseThemeData(hThm);
}

在绘制了图形后,加入下面代码绘制一段文本:

			//绘制文本
			RECT rcText = {10, 10, 300, 40};
			DrawGlowingText(hDC, L"  一点点中文 and some english", rcText);

因为字体发光的缘故,在文本左侧留下一个空格看起来会舒服一些。效果如下:
image

七、缩略图关联

DWM API中还有一个功能,即缩略图关联。它允许我们将一个窗口的缩略图显示到自己窗口的客户区。缩略图不同于截图,它是实时更新的。下面的代码将在窗口客户区显示QQ影音播放器的缩略图:

	HRESULT hr = S_OK;
	HTHUMBNAIL thumbnail = NULL;
	HWND hWndSrc = FindWindow(_T("QQPlayer Window"), NULL);
	hr = DwmRegisterThumbnail(hWnd, hWndSrc, &thumbnail);
	if (SUCCEEDED(hr))
	{
		RECT rc;
		GetClientRect(hWnd, &rc);
		DWM_THUMBNAIL_PROPERTIES dskThumbProps;
		dskThumbProps.dwFlags = DWM_TNP_RECTDESTINATION | DWM_TNP_VISIBLE | DWM_TNP_OPACITY ;
		dskThumbProps.fVisible = TRUE;
		dskThumbProps.opacity = 200;
		dskThumbProps.rcDestination = rc;
		hr = DwmUpdateThumbnailProperties(thumbnail,&dskThumbProps);
	}

首先通过窗口标题查找到源窗口句柄,然后使用DwmRegisterThumbnail注册缩略图关联,注册成功后,通过DwmUpdateThumbnailProperties更新缩略图属性,其中设定了是否可视、透明度以及目标绘制区域。得到下面的效果:

image

使用 Windows 组合引擎实现高性能窗口分层

借助 C++ 进行 Windows 开发:使用 Windows 组合引擎实现高性能窗口分层

自从我第一次看到 Windows XP 中的分层窗口,我就对它情有独钟。我好像一直对消除传统桌面窗口的矩形或近矩形边框非常感兴趣。后来出现了 Windows Vista。这个备受非议的 Windows 版本从一开始就预示着更具吸引力和灵活性的功能的降临。我们才开始认同 Windows Vista 带来的概念启发。虽然现在 Windows 8 出现了,但它也标志着分层窗口在慢慢地走下坡路。

Windows Vista 引入了一项名为“桌面窗口管理器”的服务。这个名字一直以来都具有误导性。请把它看作是 Windows 组合引擎或合成程序。此组合引擎完全改变了应用程序窗口在桌面上的呈现方式。每个窗口不是直接呈现给显示器或显示适配卡,而是呈现给屏外表面或缓冲区。系统为每个顶层窗口都分配了一个这样的表面,所有 GDI、Direct3D(当然还有 Direct2D)图形都呈现给此类表面。此类屏外表面称为重定向表面,因为 GDI 绘图命令以及 Direct3D 交换链呈现请求都会重定向或(在 GPU 中)复制到重定向表面。

在某些时候,不受任何指定窗口的制约,组合引擎会鉴于最新一批更改决定是否该组合桌面。这就涉及将所有重定向表面组合在一起、添加非工作区(通常称为窗口镶边)、添加一些阴影和其他效果以及将最终结果呈现给显示适配卡。

此组合过程具有许多显著优势,我将在接下来的几个月中一边深入探索 Windows 组合,一边详细说明这些优势;不过它也存在一个潜在的严重限制,即这些重定向表面不透明。大多数应用程序都可以接受此限制,而且从性能角度来看此限制也是非常有意义的,因为 alpha 值混合处理的费用高昂。但这就导致分层窗口遭到排斥。

如果我要开发分层窗口,就需要掀起一场性能冲击。我在专栏“使用 Direct2D 绘制分层窗口”(msdn.microsoft.com/magazine/ee819134) 中介绍了具体的体系结构限制。总而言之,分层窗口由 CPU 处理,主要用于支持 alpha 值混合处理像素的命中测试。也就是说,CPU 需要复制分层窗口的表面区域的组成像素。不管我是在 CPU 上呈现(往往比 GPU 呈现慢很多)还是在 GPU 上呈现,我都必须支付总线带宽税,因为我呈现的所有内容都必须从视频存储器复制到系统内存。在之前提到的专栏中,我还介绍了如何充分利用 Direct2D 尽可能发挥系统性能,因为我只有通过 Direct2D 才能在 CPU 和 GUP 呈现之间做出选择。存在的隐患就是,即使分层窗口一定要位于系统内存中,组合引擎也可以立即将其复制到视频存储器中,这样分层窗口的实际组合就仍为硬件加速。

虽然我无法带来传统分层窗口即将重返主导地位的希望,但我确实带来了一些好消息。传统的分层窗口具有两项特定的相关功能。第一项功能是每像素 alpha 值混合处理。我呈现给分层窗口的任何内容都会与桌面以及任意给定时刻窗口后面的所有内容进行 alpha 值混合处理。第二项功能是 User32 能够根据像素 alpha 值对分层窗口执行命中测试,允许鼠标消息在特定点的像素为透明时贯透过去。从 Windows 8 和 8.1 开始,虽然 User32 一直没有发生显著变化,但细微变化也有,即完全支持 GPU 上的每像素 alpha 值混合处理,且将窗口表面传输到系统内存的费用也取消了。也就是说,如果我不需要执行每像素命中测试,现在就能够生成分层窗口效果,同时不会对性能造成影响。整个窗口将统一执行命中测试。撇开命中测试不谈,只是这样就已经令我兴奋不已,因为这是系统显然可以执行的任务,但应用程序就从来没能利用这种功能。如果您对此感兴趣,请继续阅读本文,我将向您介绍这是如何实现的。

实现此功能的关键点在于使用 Windows 组合引擎。组合引擎一开始作为桌面窗口管理器出现在 Windows Vista 中,它包含受限制的 API 并具有流行的半透明 Aero 毛玻璃效果。接下来 Windows 8 出现了,它引入了 DirectComposition API。此 API 适用于相同的组合引擎,只是受限制程度更低。随着 Windows 8 版本的发布,Microsoft 最终允许第三方开发人员利用这一面世已久的组合引擎。当然,您还需要使用由 Direct3D 强力驱动的图形 API(如 Direct2D)。但您首先需要处理不透明的重定向表面。

正如我之前提到的,系统为每个顶层窗口都分配了一个重定向表面。从 Windows 8 开始,您现在可以创建顶层窗口,并请求创建没有重定向表面的顶层窗口。严格来说,这与分层窗口并不相关。因此,请勿使用 WS_EX_LAYERED 扩展的窗口样式(实际上,分层窗口的相关支持在 Windows 8 中有了细微改进,不过我将在即将发表的专栏文章中深入介绍这些改进)。您需要改用 WS_EX_NOREDIRECTIONBITMAP 扩展的窗口样式,此样式可以指示组合引擎不为窗口分配重定向表面。我将从简单的传统桌面窗口入手。图 1 中的示例介绍了如何填充 WNDCLASS 结构、注册窗口类、创建窗口以及抽取窗口消息。其中并没有什么新内容,但这些基本原理仍然至关重要。窗口变量处于未使用状态,但您马上就需要使用这个变量。您可以将此变量复制到 Visual Studio 内的 Visual C++ 项目中,也可以只是从以下命令提示符编译此变量:

cl /W4 /nologo Sample.cpp

#ifndef UNICODE
#define UNICODE 
#endif

#include <windows.h> 
#pragma comment(lib, "user32.lib") 
int __stdcall wWinMain(HINSTANCE module, HINSTANCE, PWSTR, int) {
 WNDCLASS wc = {};
 wc.hCursor       = LoadCursor(nullptr, IDC_ARROW);
 wc.hInstance     = module;
 wc.lpszClassName = L"window";
 wc.style         = CS_HREDRAW | CS_VREDRAW;
 wc.lpfnWndProc = [] (HWND window, UINT message, WPARAM wparam, LPARAM lparam) -> LRESULT {
 if (WM_DESTROY == message) { 
     PostQuitMessage(0);
     return 0;
 }
return DefWindowProc(window, message, wparam, lparam);
};
RegisterClass(&wc);

HWND const window = CreateWindow(wc.lpszClassName, L"Sample", WS_OVERLAPPEDWINDOW | WS_VISIBLE, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, nullptr, nullptr, module, nullptr);
MSG message; 
while (BOOL result = GetMessage(&message, 0, 0, 0)) { 
    if (-1 != result) DispatchMessage(&message);
 }
}

图 2 展示了窗口在我的桌面上的显示效果。请注意,此示例没有什么特别之处。虽然该示例未提供任何绘制和呈现命令,但窗口的工作区不透明,并且组合引擎添加了非工作区、边框和标题栏。要应用 WS_EX_NOREDIRECTIONBITMAP 扩展的窗口样式删除不透明的重定向表面(表示此工作区),只需使用接受扩展的窗口样式的主要参数将 CreateWindow 函数切换为 CreateWindowEx 函数即可:

HWND const window = CreateWindowEx(WS_EX_NOREDIRECTIONBITMAP, wc.lpszClassName, L"Sample", WS_OVERLAPPEDWINDOW | WS_VISIBLE, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, nullptr, nullptr, module, nullptr);

image
图 2:桌面上的传统窗口

变化只包括添加了主要参数、WS_EX_NOREDIRECTIONBITMAP 扩展的窗口样式,以及使用了 CreateWindowEx 函数(而不是更简单的 CreateWindow 函数)。不过,桌面上呈现的结果变化更为彻底。图 3 展示了窗口在我的桌面上的显示效果。请注意,窗口的工作区现在是完全透明。移动窗口可以说明这一点。我甚至可以在背景中播放视频,任何部分都不会被遮挡。另一方面,整个窗口统一执行命中测试,当您在工作区内单击时,不会失去窗口焦点。这是因为负责命中测试和鼠标输入的子系统没有意识到工作区是透明的。

image
图 3:不含重定向表面的窗口

当然,接下来您会问,如果没有可向组合引擎提供的重定向表面,如何能够将任意内容呈现给窗口?答案就是利用 DirectComposition API 及其与 DirectX 图形基础结构 (DXGI) 的深度集成。此技术同样可强力驱动 Windows 8.1 XAML 实施,从而在 XAML 应用程序内提供性能极高的内容组合。Internet Explorer Trident 呈现引擎也将 DirectComposition 广泛用于触控平移、缩放、CSS3 动画、切换和转换。

我就是要用它来组合通过每像素预乘 alpha 值支持透明度的交换链,并将其与桌面的其余部分混合。传统的 DirectX 应用程序通常使用 DXGI 工厂的 CreateSwapChainForHwnd 方法创建交换链。此交换链受到在呈现过程中有效交换的一对或一组缓冲区的支持,允许应用程序在前一帧得到复制的同时呈现下一帧。应用程序呈现到的交换链表面是不透明的屏外缓冲区。当应用程序呈现交换链时,DirectX 会将内容从交换链的后台缓冲区复制到窗口的重定向表面。正如我之前提到的,组合引擎最终会将所有的重定向表面都组合到一起,从而形成整个桌面。

在这种情况下,由于窗口不包含任何重定向表面,因此不能使用 DXGI 工厂的 CreateSwapChainForHwnd 方法。不过,我仍然需要使用交换链来支持 Direct3D 和 Direct2D 呈现。这正是 DXGI 工厂的 CreateSwapChainForComposition 方法的用途所在。我可以使用此方法创建一个无窗口的交换链及其缓冲区,但呈现此交换链不会将位数复制到不存在的重定向表面,而是直接向组合引擎提供。然后,组合引擎可以获取此表面,直接用它来取代窗口的重定向表面。由于此表面不是不透明的,而是像素格式完全支持每像素预乘 alpha 值,因此结果就是在桌面上进行完全适合像素的 alpha 值混合处理。速度也极快,这是因为不必在 GPU 内进行复制,进而也就不必通过总线复制到系统内存。

这些都是理论。现在是进行实践的时候了。由于 DirectX 是一种 COM 对象,因此我将使用 Windows 运行时 C++ 模板库中的 ComPtr 类模板来管理接口指针。我还需要添加并关联至 DXGI、Direct3D、Direct2D 和 DirectComposition API。以下代码展示了这是如何实现的:

#include <wrl.h> 
using namespace Microsoft::WRL; 
#include <dxgi1_3.h> 
#include <d3d11_2.h> 
#include <d2d1_2.h> 
#include <d2d1_2helper.h> 
#include <dcomp.h> 
#pragma comment(lib, "dxgi") 
#pragma comment(lib, "d3d11") 
#pragma comment(lib, "d2d1") 
#pragma comment(lib, "dcomp")

我通常会在预编译头中添加这些代码。在这种情况下,我会省略 using 指令,并且只在我的应用程序的源文件中添加此指令。

我不喜欢错误处理通篇存在并且偏离主题本身细节的示例代码,因此我将使用一个异常类和 HR 函数妥善处理错误检查。图 4 中展示了一个简单的实施示例,当然您也可以自行决定错误处理策略。

将 HRESULT 错误转变为异常

struct ComException { 
HRESULT result; 
ComException(HRESULT const value) :result(value) {} 
}; 
void HR(HRESULT const result) { 
if (S_OK != result) { 
throw ComException(result); 
} 
}

我现在可以开始组装呈现堆栈,自然就从 Direct3D 设备入手了。我将会快速介绍此主题,因为我已经在 2013 年 3 月的专栏“Direct2D 1.1 简介”(msdn.microsoft.com/magazine/dn198239) 中详细介绍了 DirectX 基础结构。以下是 Direct3D 11 接口指针:ComPtr direct3dDevice;

这是设备的接口指针,您可以使用 D3D11Create­Device 函数创建设备:

HR(D3D11CreateDevice(nullptr,    // 适配卡 D3D_DRIVER_TYPE_HARDWARE, nullptr,    // 模块 D3D11_CREATE_DEVICE_BGRA_SUPPORT, nullptr, 0, // 最高可用功能级别 D3D11_SDK_VERSION, &direct3dDevice, nullptr,    // 实际功能级别 nullptr));  // 设备上下文

代码中没有什么太出人意料的内容。我将创建受 GPU 支持的 Direct3D 设备。D3D11_CREATE_DEVICE_BGRA_SUPPORT 标志实现了与 Direct2D 的互操作性。DirectX 系列通过 DXGI 紧密结合在一起,这样就针对各种 DirectX API 提供了常见 GPU 资源管理工具。因此,我必须查询 Direct3D 设备的 DXGI 接口:

ComPtr<IDXGIDevice> dxgiDevice; HR(direct3dDevice.As(&dxgiDevice));

ComPtr As 方法只是 QueryInterface 方法的包装器。在创建 Direct3D 设备后,我便可以创建用于组合的交换链。为此,我首先需要获取 DXGI 工厂:

ComPtr<IDXGIFactory2> dxFactory; HR(CreateDXGIFactory2( DXGI_CREATE_FACTORY_DEBUG, __uuidof(dxFactory), reinterpret_cast<void **>(dxFactory.GetAddressOf())));

此时,我将选择获取额外的调试信息,这是开发过程中非常宝贵的辅助信息。创建交换链时的最棘手部分就是确定如何向 DXGI 工厂说明预期的交换链。此调试信息非常有助于对必要的 DXGI_SWAP_CHAIN_DESC1 结构进行微调:DXGI_SWAP_CHAIN_DESC1 description = {};

这会将结构全部初始化为零。然后,我可以开始填充任何相关属性:

description.Format           = DXGI_FORMAT_B8G8R8A8_UNORM;
description.BufferUsage      = DXGI_USAGE_RENDER_TARGET_OUTPUT; 
description.SwapEffect       = DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL; 
description.BufferCount      = 2; 
description.SampleDesc.Count = 1; 
description.AlphaMode        = DXGI_ALPHA_MODE_PREMULTIPLIED;

以下特定格式并不是您的唯一选项,但可为各种设备和 API 带来最佳的性能和兼容性:32 位像素格式,每个颜色通道为 8 位,再加上一个预乘的 8 位 alpha 分量。

必须将交换链的缓冲区使用设置为允许将呈现器目标输出定向到它。您必须进行此设置,这样 Direct2D 设备上下文才能创建位图来使用绘图命令定位 DXGI 表面。Direct2D 位图本身只是受到交换链支持的抽象概念。

组合交换链仅支持依序翻转的交换效果。这就是交换链取代重定向表面与组合引擎相关联的方式。在翻转模式下,所有缓冲区都直接与组合引擎共享。然后,组合引擎可以直接从交换链后台缓冲区组合桌面,无需进行其他任何复制操作。通常情况下,这是最有效的模式。组合也需要使用此模式,因此这就是我使用的模式。翻转模式也需要至少两个缓冲区,但不支持多重采样,因此将 BufferCount 设置为 2,并将 SampleDesc.Count 设置为 1。此计数是每像素的多重采样数量。将它设置为 1 可以有效禁用多重采样。

最后需要说明的是,alpha 模式至关重要。不透明的交换链通常会忽略 alpha 模式,但在本示例中,我确实想将透明行为包括在内。预乘的 alpha 值通常会带来最佳性能,而且它也是翻转模式支持的唯一选项。

在我可以创建交换链之前必须完成的最后一项操作是确定缓冲区的预期大小。调用 CreateSwapChainForHwnd 方法时,我通常会忽略大小,但 DXGI 工厂会向窗口查询工作区的大小。在这种情况下,DXGI 不知道我打算对交换链做什么,因此我需要告诉它所需的具体大小。在窗口创建后,您可以轻松查询窗口的工作区并相应地更新交换链说明:

RECT rect = {}; 
GetClientRect(window, &rect); 
description.Width  = rect.right - rect.left; 
description.Height = rect.bottom - rect.top;

我现在可以创建包含此说明的组合交换链,并创建 Direct3D 设备指针。Direct3D 或 DXGI 接口指针均可使用:

ComPtr<IDXGISwapChain1> swapChain; 
HR(dxFactory->CreateSwapChainForComposition(dxgiDevice.Get(), &description, nullptr, // 不限制 
swapChain.GetAddressOf()));

现在交换链已创建,我可以使用任何 Direct3D 或 Direct2D 图形呈现代码来绘制应用程序(使用创建预期透明度所需的 alpha 值)。这里没有什么新内容,因此,我将再次引用我的 2013 年 3 月专栏,介绍使用 Direct2D 呈现给交换链的细节。图 5 介绍了一个您可以遵循的简单示例。请务必注意支持按监视器 DPI 感知,我在 2014 年 2 月的专栏“为 Windows 8.1 编写高 DPI 应用程序”(msdn.microsoft.com/magazine/dn574798) 中对此进行了介绍。

// 使用调试信息创建单线程 Direct2D 工厂 ComPtr<ID2D1Factory2> d2Factory; D2D1_FACTORY_OPTIONS const options = { D2D1_DEBUG_LEVEL_INFORMATION }; HR(D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, options, d2Factory.GetAddressOf())); // 创建与 Direct3D 设备恢复关联的 Direct2D 设备 ComPtr<ID2D1Device1> d2Device; HR(d2Factory->CreateDevice(dxgiDevice.Get(), d2Device.GetAddressOf())); // 创建作为实际呈现器目标 // 并揭示绘图命令的 Direct2D 设备上下文 ComPtr<ID2D1DeviceContext> dc; HR(d2Device->CreateDeviceContext(D2D1_DEVICE_CONTEXT_OPTIONS_NONE, dc.GetAddressOf())); // 检索交换链的后台缓冲区 ComPtr<IDXGISurface2> surface; HR(swapChain->GetBuffer( 0, // 索引 __uuidof(surface), reinterpret_cast<void **>(surface.GetAddressOf()))); // 创建指向交换链表面的 Direct2D 位图 D2D1_BITMAP_PROPERTIES1 properties = {}; properties.pixelFormat.alphaMode = D2D1_ALPHA_MODE_PREMULTIPLIED; properties.pixelFormat.format    = DXGI_FORMAT_B8G8R8A8_UNORM; properties.bitmapOptions         = D2D1_BITMAP_OPTIONS_TARGET | D2D1_BITMAP_OPTIONS_CANNOT_DRAW; ComPtr<ID2D1Bitmap1> bitmap; HR(dc->CreateBitmapFromDxgiSurface(surface.Get(), properties, bitmap.GetAddressOf())); // 将设备上下文指向位图以进行呈现 dc->SetTarget(bitmap.Get()); // 绘制内容 dc->BeginDraw(); dc->Clear(); ComPtr<ID2D1SolidColorBrush> brush; D2D1_COLOR_F const brushColor = D2D1::ColorF(0.18f,  // 红色 0.55f,  // 绿色 0.34f,  // 蓝色 0.75f); // alpha HR(dc->CreateSolidColorBrush(brushColor, brush.GetAddressOf())); D2D1_POINT_2F const ellipseCenter = D2D1::Point2F(150.0f,  // x 150.0f); // y D2D1_ELLIPSE const ellipse = D2D1::Ellipse(ellipseCenter, 100.0f,  // x 半径 100.0f); // y 半径 dc->FillEllipse(ellipse, brush.Get()); HR(dc->EndDraw()); // 为组合引擎提供交换链 HR(swapChain->Present(1,   // 同步 0)); // 标志

我现在终于可以开始使用 DirectComposition API 来呈现所有内容。虽然 Windows 组合引擎处理呈现和组合整个桌面,但您也可以通过 DirectComposition API 使用同一技术为应用程序组合视觉对象。应用程序将不同的元素(称为视觉对象)组合在一起,形成应用程序窗口本身的外观。这些视觉对象可以通过各种方式进行动画处理和转换,从而形成丰富流畅的 UI。组合过程本身也随整个桌面的组合一同执行,因此,更多的应用程序展示会从应用程序线程中独立出来,以改善响应能力。

DirectComposition 主要用于将不同的位图组合在一起。与 Direct2D 一样,这里的位图概念在更大程度上是一个抽象概念,可允许不同的呈现堆栈互相合作,共同带来顺畅且具有吸引力的应用程序用户体验。

类似于 Direct3D 和 Direct2D,DirectComposition 是由 GPU 支持且强力驱动的 DirectX API。DirectComposition 设备是通过重新指向 Direct3D 设备而创建,与 Direct2D 设备重新指向基础 Direct3D 设备的方式基本相同。我使用之前用来创建交换链的同一 Direct3D 设备和 Direct2D 呈现器目标来创建 DirectComposition 设备:

ComPtr<IDCompositionDevice> dcompDevice; HR(DCompositionCreateDevice( dxgiDevice.Get(), __uuidof(dcompDevice), reinterpret_cast<void **>(dcompDevice.GetAddressOf())));

DCompositionCreateDevice 函数需要 Direct3D 设备的 DXGI 接口,并向新建的 DirectComposition 设备返回 IDCompositionDevice 接口指针。DirectComposition 设备可用作其他 DirectComposition 对象的工厂,并提供至关重要的 Commit 方法,此方法用于将一批呈现命令提交给组合引擎以进行最后的组合和呈现。

接下来,我需要创建与视觉对象相关联的组合目标,这些视觉对象将与目标(即应用程序窗口)一起组合:

ComPtr<IDCompositionTarget> target; HR(dcompDevice->CreateTargetForHwnd(window, true, // 顶级 target.GetAddressOf()));

CreateTargetForHwnd 方法的第一个参数是 CreateWindowEx 函数返回的窗口句柄。第二个参数表示视觉对象与其他任何窗口元素的组合方式。结果是 IDCompositionTarget 接口指针,它只有一个方法,称为 SetRoot。通过此方法,我可以将可能形成的可视化树中的根 Visual 设置为一起组合。我不需要整个可视化树,但需要至少一个视觉对象,为此我可以再次利用 DirectComposition 设备:

ComPtr<IDCompositionVisual> visual; HR(dcompDevice->CreateVisual(visual.GetAddressOf()));

此视觉对象包含对位图的引用,并提供一组属性,这些属性会影响该视觉对象相对于树中的其他视觉对象和目标本身的呈现和组合方式。我已获取自己希望由此视觉对象传递给组合引擎的内容。此为我之前创建的交换链:

HR(visual->SetContent(swapChain.Get()));

视觉对象已准备就绪,我只需将其设置为组合目标的根即可:

HR(target->SetRoot(visual.Get()));

最后,在可视化树成形后,我便只需通过调用 DirectComposition 设备上的 Commit 方法即可告知组合引擎我已完成操作:

HR(dcompDevice->Commit());

对于可视化树不变化的此特定应用程序,我只需在应用程序开始时调用 Commit 一次,之后再也不会调用此方法了。我最初假设需要在交换链呈现之后调用 Commit 方法,但情况并非如此,因为交换链呈现与可视化树变化并不同步。

图 6 展示了在 Direct2D 已呈现给交换链且 Direct­Composition 已将部分透明的交换链提供给组合引擎之后的应用程序窗口外观。
image
图 6:DirectComposition 表面上的 Direct2D 绘制

我终于找到了老问题的解决方案,我感到非常兴奋:可以生成与桌面的其余部分进行 alpha 值混合处理的高性能窗口。让我激动不已的是 DirectComposition API 的功能及其如何影响在本机代码中设计和开发应用程序用户体验的未来。

想要绘制您自己的窗口镶边吗?没问题,只需在创建窗口时将 WS_OVERLAPPEDWINDOW 窗口样式替换为 WS_POPUP 窗口样式即可。祝您工作愉快!

DirectComposition API

DirectComposition API 适用于了解 C/C++ 的经验丰富的且支持高度支持的图形开发人员,对组件对象模型 (COM) 有扎实的理解,并熟悉Windows编程概念。DirectComposition 是在 Windows 8 中引入的。 它包含在 32 位、64 位和 ARM 平台中。

将视觉层与 Win32 结合使用

可以在 Win32 应用中使用 Windows 运行时 Composition API(也称为视觉层)来创建面向 Windows 用户的现代特色体验。

GitHub 上提供了本教程的完整代码:Win32 HelloComposition 示例

需要精确控制其 UI 合成的通用 Windows 应用程序可以访问 Windows.UI.Composition 命名空间,以便对其 UI 的合成和呈现方式进行精细控制。 但是,此 Composition API 并不局限于 UWP 应用。 Win32 桌面应用程序可以利用 UWP 和 Windows 中的现代合成系统。

如何从 Win32 桌面应用程序使用 Composition API

image

为了在 Win32 应用中使用 Windows 运行时 (WinRT) API,我们将使用 C++/WinRT。 需要配置 Visual Studio 项目才能添加 C++/WinRT 支持。

若要使用视觉层承载创建的内容,请创建一个类 (CompositionHost) 用于管理互操作并创建合成元素。 将在此类中完成用于承载 Composition API 的大部分配置,包括:

我们将此类设为单一实例,以免出现线程问题。 例如,只能为每个线程创建一个调度程序队列,因此,在同一线程中实例化 CompositionHost 的另一个实例会导致出错。

Docking Toolbars in Plain C

Docking Toolbars in Plain C

How to create docking toolbars/windows in plain C (no MFC, ATL, COM, nor WTL).

Introduction

This tutorial will show you how to create docking tool windows, using only the standard Win32 graphics and Windows functions (i.e. no MFC, WTL, Automation, etc.). A docking tool window is a window that can either be "attached" (i.e. "docked") to the inner border of some other window, or be "torn off" of that border (by the end user) to float freely and be moved around by the mouse. A window, to which some tool window may be docked, is called the owner (or container) window.

image

A tool window floating above its owner window.

image

The same tool window docked to the bottom inner border of its owner window.

First, we'll discuss the "floating" aspect of tool windows - i.e. how to get tool windows to stay floating on top of all other windows, how to get the window activation working correctly, etc. Later, we'll see how to get these floating tool windows to "dock" to a side of the owner window, and discuss various methods of window management.

The source code presented will culminate in a Win32 DLL called DockWnd.dll whose functions can be used by any Win32 program to easily support docking tool windows. There are many different ways to create a docking window. I imagine most of the code on the Internet uses well-designed C++ classes to hide the implementation. However, I still prefer to code in C, so the design of this library will be a non-object oriented approach and therefore easily useable by an application written in any language.

This code/article is based upon some original free code provided by James Brown. His website, contains an earlier, different version of this code (as well as numerous other free Win32 tutorials/examples).

Contents

DOCKINFO structure
How DockingCreateFrame creates a floating tool window
Prevent tool window deactivation
Show all tool windows as active
Sync the activation of all tool windows
Sync the enabled state of all tool windows
Docking a tool window
Floating versus docked size
Moving a window with a drag-rectangle
Drawing a drag-rectangle
Redrawing the docked windows
Enumerating tool windows
Various other features of the library
An application
Creating a tool window
Handling WM_SIZE message in the owner
Handling WM_NCACTIVATE message in the owner
Handling WM_ENABLE message in the owner
Handling messages sent to standard controls
Multiple child windows inside a tool window
Closing a tool window
Saving/Restoring a tool window size/position
Miscellaneous remarks

DOCKINFO structure

Because our docking library needs to maintain some information about each tool window it manages, we need some structure to store the information. We'll define a DOCKINFO structure (in dockwnd.h), and allocate a DOCKINFO for each tool window created. The application will call the DockingAlloc function in our docking library to allocate a DOCKINFO (initialized to default values), and then pass it to the DockingCreateFrame function which creates the actual tool window associated with that DOCKINFO. This struct will hold information such as the handle (HWND) to the tool window, the handle to the tool window's owner window, whether the tool window is currently docked to its owner or floating, and other information for our private use in managing docking tool windows. We store the handle to the tool window in the DOCKINFO's hwnd field. We store the handle to the owner window in the DOCKINFO's container field. And the value of the DOCKINFO's uDockedState field tells whether the tool window is floating or docked. This field is OR'ed with DWS_FLOATING (and therefore a negative value) when a tool window is floating. We'll discuss the other DOCKINFO fields later.

Note: It is the application's responsibility to create and manage the owner window. Our library deals only with creating/managing the tool windows.

We register our own window class (with the class name of "DockWnd32") for our tool windows. The window procedure (dockWndProc) for this class is inside of our library. We use the GWL_USERDATA field of the tool window to store a pointer to that tool window's DOCKINFO struct. In this way, we can easily and quickly fetch the appropriate DOCKINFO given only a handle (HWND) to a particular tool window.

We don't want to limit the application to only one owner window, and its set of docked windows. For example, perhaps an application will have two owner windows, each with its own set of docked windows. Nor, do we want to limit the application to a particular number of tool windows. So, we may be asked to create DOCKINFOs for numerous sets of tool windows and owners. By storing a pointer to a tool window's DOCKINFO in the tool window itself, and storing handles to the tool window and its owner window inside the DOCKINFO, we can easily get all of the information we need for our library to do what it needs to do, with minimal work on the part of the application.

There are times when our library needs to be able to enumerate all tool windows for a particular owner window. We'll get to the particulars of that later. In some of our example code below, we'll just refer to a placeholder function called DockingNextToolWindow which you should assume will fetch the next tool window (actually, that tool window's DOCKINFO) for a particular owner window. In the actual source code for the library, this is replaced by more complex code that we'll examine later.

How DockingCreateFrame creates a floating tool window

A floating tool window is just a standard window with the WS_POPUP style. When a popup window is created with an owner window, the popup is positioned so that it always stays on top of that owner window. This is how we can create and display a floating tool window:

// Create a floating (popup) tool window
HWND hwnd = CreateWindowEx(
    WS_EX_TOOLWINDOW,
    "ToolWindowClass", "ToolWindow",
    WS_POPUP | WS_SYSMENU | WS_THICKFRAME | WS_CAPTION | WS_VISIBLE,
    200, 200, 400, 64,
    hwndOwner, NULL, GetModuleHandle(0), NULL
    );

Note: In the above example, it is assumed that hwndOwner is a handle to some other window the application created to be the owner window for our tool window. The application must create this window, and then pass its handle to DockingCreateFrame. In other words, the application must create the owner window before any tool window can be created for it.

The WS_EX_TOOLWINDOW extra style doesn't do anything special, other than to make a window with a smaller titlebar. It doesn't make the window magically float - this is achieved automatically by specifying WS_POPUP style and an owner window. Here's what the above CreateWindowEx may display:

image

A tool window floating above its owner window ("Main window").

Prevent window deactivation

The image above shows the owner window ("Main window") with an inactive titlebar. This is entirely normal, because only one window at a time can have the input focus, and the operating system normally shows only that one window with an active titlebar. So, when we create our tool window, the operating system shows the tool window as active, and shows the owner window as deactivated.

But, it is normal practice for tool windows and their owner window to appear active at the same time. It looks more natural this way. So we need to devise a strategy to keep our tool window and owner window both appear active, even if only one technically has the input focus.

The solution involves the WM_NCACTIVATE message. The operating system sends this message when a window's non-client area (the titlebar and border) needs to be activated or deactivated. As with all window messages, WM_NCACTIVATE is sent with two parameters - wParam and lParam. When a window receives WM_NCACTIVATE with wParam=TRUE, this indicates that the titlebar and border should be shown as active. When wParam=FALSE, this indicates that the titlebar and border should be shown as inactive.

Note: MSDN states that WM_NCACTIVATE's lParam will always be 0. However, I have observed that lParam indicates the window handle of the window being deactivated. This appears to be true under Win95, 98 and NT, 2000, XP. Our solution relies upon this undocumented feature.

So, when we create our tool window, our owner window receives a WM_NCACTIVATE with wParam=FALSE and our tool window receives a WM_NCACTIVATE with wParam=TRUE.

When this message is passed to DefWindowProc(), the operating system does two things. First, the titlebar is drawn as either active or inactive, depending upon whether wParam is TRUE or FALSE respectively. Secondly, the operating system sets an internal flag for the window which remembers if the window was painted as active or inactive. This enables DefWindowProc() to process subsequent WM_NCPAINT messages to paint the titlebar with the proper activation. It is advisable to always pass WM_NCACTIVATE to DefWindowProc() so that this internal flag is set, even if you also do your own processing of WM_NCACTIVATE.

This WM_NCACTIVATE message provides us with a way to make all our tool windows, and owner window, look active, even if only one window technically has the focus. To do this, whenever our tool windows or owner window receive a WM_NCACTIVATE, we will always substitute TRUE for wParam when we pass the WM_NCACTIVATE to DefWindowProc(). The result is that the operating system always renders the titlebars of our tool windows and owner window as active.

Here is some code we could add to the window procedure of all our tool windows, and owner window, to show them all as simultaneously active:

case WM_NCACTIVATE:
    return DefWindowProc(hwnd, msg, TRUE, lParam);

image
Both the tool window and its owner window are shown active.

Incidentally, MDI child windows also use this same technique to keep their titlebars active. The only difference is that MDI windows have the WS_CHILD style, instead of WS_POPUP.

Show all tool windows as active

The above method seems to do what we want, but there is a problem. Our owner window and tool windows will always appear active, even if our application is not in the foreground. For example, if the end user switches to some other application's window, our tool window and owner window still will look active, which can be a bit disconcerting to the end user.

Also, whenever we display a message box or a normal dialog box, the owner window and tool windows will still appear active, when in this scenario we ideally want to make them look inactive.

This calls for a more careful study of window activation messages. Here is a description of the series of window activation messages sent when one window becomes active, and another inactive:

  1. WM_MOUSEACTIVATE is sent to the window about to become active, to ask it whether or not the activation request should be allowed. What your window procedure returns (i.e., MA_ACTIVATE or MA_NOACTIVATE) affects the subsequent activation messages.
  2. WM_ACTIVATEAPP is sent if a window belonging to a different application is about to become active (or inactive). This message is sent both to the window that is currently active (to tell it that it is about to become inactive) as well as the window that is about to become active. The return value should always be zero, and never affects subsequent messages' behaviour.
  3. As described above, WM_NCACTIVATE is sent when a window's non-client area needs to be shown activated or deactivated.
  4. WM_ACTIVATE is sent last of all to the window becoming active. When this message is passed to DefWindowProc(), the operating system sets the input focus to that window.

With all of these activation messages, only two windows are actually involved - the window being deactivated, and the window being activated. So, even if we have many floating tool windows, not all of them will receive these messages. Only the one window being activated, and the one window being deactivated, receive the messages. But to make things look and feel right, we want the displayed state (i.e., whether a tool window's titlebar is shown active or inactive) of each tool window to be the same as all other tool windows. So, even though not all of our windows will receive the above messages, we still need to have all windows synced to the same state.

This same discussion applies whenever we need to disable or enable our owner window. If the owner window is to be disabled or enabled, we want to sync all the tool windows to that same state. (But, a different set of messages are sent for a window being disabled or enabled.)

So, our docking library has a bit of work to do in order to make things look and feel right:

  1. When our application is activated / deactivated, we need to sync the active / inactive display of all tool windows with each other.
    This also applies to activation within our own application. For example, if the user activates a window we create that isn't one of our tool windows, then we want to show all tool windows deactivated. And if the user switches back to a tool window from that window, we want all tool windows shown active.

  2. When the owner window is disabled due to a modal dialog or message box being displayed, then we must disable all tool windows (and any modeless dialogs) to prevent the user from interacting with them while the modal dialog / message box is on screen.

A first try at a solution

Our first stab at a solution will be to concentrate on the WM_ACTIVATE message. This message is received whenever a window is activated or deactivated. The direction we will take will be to decide if the window receiving this message is active or inactive, and synchronise all other windows to the same state by manually sending them a "spoof" WM_NCACTIVATE message. This spoof message will force the other windows to update their titlebars to the same state as the window receiving the WM_ACTIVATE.

Here's a function that we could add to our docking library. Whenever one of our tool windows, or owner window, receives a WM_ACTIVATE message, it will call this function to sync the state of all tool windows:

/*********************** DockingActivate() **********************
 * Sends WM_NCACTIVATE to all the owner's tool windows. A
 * tool or owner window calls this in response to receiving
 * a WM_ACTIVATE message.
 *
 * container =  Handle to owner window.
 * hwnd =       Handle to window which received WM_ACTIVATE (can
 *              be the owner, or one of its tool windows).
 * wParam =     WPARAM of the WM_ACTIVATE message.
 * lParam =     LPARAM of the WM_ACTIVATE message.
 */

LRESULT WINAPI DockingActivate(HWND container, 
               HWND hwnd, WPARAM wParam, LPARAM lParam)
{
   DOCKINFO * dwp;
   BOOL       fKeepActive;

   fKeepActive = (wParam != WA_INACTIVE);

   // Get the DOCKINFO of the next tool window for this owner window. when 0
   // is returned, there are no more tool windows for this owner.
   while ((dwp = DockingNextToolWindow(container)))
   {
      // Sync this tool window to the same state as the window that called
      // DockingActivate.
      SendMessage(dwp->hwnd, WM_NCACTIVATE, fKeepActive, 0);
   }

   // Allow the window that called DockingActivate to handle its WM_NCACTIVATE
   // as normally it would.
   return DefWindowProc(hwnd, WM_ACTIVATE, wParam, lParam);
}

It works, after a fashion. All tool windows activate and deactivate correctly, and all at the same time. This solution is not the best though.

The problem is that every tool window's titlebar flashes whenever the active window changes. This is because of the way the operating system sends the WM_ACTIVATE message. This message is first sent to the window that is being deactivated. If that happens to be a tool window or the owner window, it will call DockingActivate to deactivate all the tool windows. WM_ACTIVATE is then sent to the active window. If that window also happens to be a tool window or owner window, it will call DockingActivate to (correctly) activate all the tool windows. It is the fact that DockingActivate is quickly called twice (once to deactivate the tool windows, and then to activate them) that causes all the windows to flash.

A partial solution is to perform a check before deactivating the tool windows. We know that if a window is being deactivated, lParam identifies the (other) window about to be activated. And if this other window is one of our tool windows, we can skip deactivating the tool windows, because we know the other (tool) window is going to subsequently activate them anyway.

if (fKeepActive == FALSE)
{
   while ((dwp = DockingNextToolWindow(container)))
   {
      if (dwp->hwnd == (HWND)lParam)
         return DefWindowProc(hwnd, WM_ACTIVATE, wParam, lParam);
   }
}

This prevents every tool window from briefly deactivating, then activating again. There is still a problem, albeit a minor one. The problem is, the single tool window that is being deactivated will still flicker briefly before being activated again. This is because it will already have received its WM_NCACTIVATE message, which caused the window to be redrawn deactivated. The window gets its activated look eventually, but this brief flicker is still visible.

Sync the activation of all tool windows

We need to take a step back and approach the problem from a slightly different direction. Instead of handling WM_ACTIVATE, which is called after a window's titlebar is redrawn, we'll go straight to the heart of the problem, and rewrite DockingActivate to be called whenever a window receives a WM_NCACTIVATE message. This will ensure that no unnecessary activation or deactivation will take place.

The function presented below performs several tasks on behalf of the tool (or owner) window that calls DockingActivate:

  1. Search the list for the other window being activated/deactivated (the window specified by lParam, rather than the window receiving WM_NCACTIVATE). If this other window is a tool window, then we force all tool windows as activated.
  2. Synchronize all current tool windows to our (possibly new) state.
  3. Activate/deactivate the window that calls DockingActivate, depending on our new state.

The code looks like this:

LRESULT WINAPI DockingActivate(HWND container, 
            HWND hwnd, WPARAM wParam, LPARAM lParam)
{
   DOCKINFO * dwp;
   BOOL       fKeepActive;
   BOOL       fSyncOthers;

   // If this is a spoof'ed message we sent, then handle it
   // normally (but reset LPARAM to 0).
   if (lParam == -1)
      return DefWindowProc(hwnd, WM_NCACTIVATE, wParam, 0);

   fKeepActive = wParam;
   fSyncOthers = TRUE;

   while ((dwp = DockingNextToolWindow(container)))
   {
      // UNDOCUMENTED FEATURE:
      // If the other window being activated/deactivated (i.e. not the one that
      // called here) is one of our tool windows, then go (or stay) active.
      if ((HWND)lParam == dwp->hwnd)
      {
         fKeepActive = TRUE;
         fSyncOthers = FALSE;
         break;
      }
   }

   if (fSyncOthers == TRUE)
   {
      // Sync all other tool windows to the same state.
      while ((dwp = DockingNextToolWindow(container)))
      {
         // Send a spoof'ed WM_NCACTIVATE message to this tool window,
         // but not if it is the same window that called here. Note that
         // we substitute a -1 for LPARAM to indicate that this is a
         // spoof'ed message we sent. The operating system would never
         // send a WM_NCACTIVATE with LPARAM = -1.
         if (dwp->hwnd != hwnd && hwnd != (HWND)lParam)
            SendMessage(dwp->hwnd, WM_NCACTIVATE, fKeepActive, -1);
      }
   }

   return DefWindowProc(hwnd, WM_NCACTIVATE, fKeepActive, lParam);
}

The code above uses an undocumented feature of the WM_NCACTIVATE message which I observed while experimenting with these activation messages. The MSDN documentation states that lParam is unused (presumably zero), but this is not the case under Windows 95, 98, ME, and NT, 2000, XP.

Instead, lParam is a handle to the other window being activated/deactivated in our place (i.e., if we are being deactivated, lParam will be the handle to the window being activated). This is not always the case, specifically when the other window being activated/deactivated belongs to another process. In this case, lParam will be zero.

Sync the enabled state of all tool windows

Now, we need to tackle the other problem. When our owner window is disabled (perhaps because a modal dialog or message box has popped up), we need to disable all tool windows too. This feature prevents the user from clicking on and activating not only the main window, but also any tool window, while the modal dialog or message box is displayed.

The solution is similar to how we solved the activation problem, except that this time we write a function that a tool window or the owner window calls whenever it receives a WM_ENABLE message. DockingEnable simply enables/disables all the tool windows to the same state as the owner window.

/*********************** DockingEnable() **********************
 * Sends WM_ENABLE to all the owner's tool windows.
 * A window calls this in response to receiving a
 * WM_ENABLE message.
 *
 * container =  Handle to owner window.
 * hwnd =       Handle to window which received WM_ENABLE (can
 *              be the owner, or one of its tool windows).
 * wParam =     WPARAM of the WM_ENABLE message.
 * lParam =     LPARAM of the WM_ENABLE message.
 */

LRESULT WINAPI DockingEnable(HWND container, 
               HWND hwnd, WPARAM wParam, >LPARAM lParam)
{
   DOCKINFO * dwp;

   while ((dwp = DockingNextToolWindow(container)))
   {
      // Sync this tool window to the same state as the window that called
      // DockingEnable (but not if it IS the window that called here).
      if (dwp->hwnd != hwnd) EnableWindow(dwp->hwnd, wParam);
   }

   // Allow the window that called DockingEnable to handle its WM_ENABLE
   // as normally it would.
   return DefWindowProc(hwnd, WM_ENABLE, wParam, lParam);
}

Docking a tool window

The previous discussion took you through the steps necessary to create floating tool windows. Now, we'll discuss the techniques necessary to get these floating windows to "dock" with their owner window. I'm not going to reproduce all the library's source code in this tutorial, because quite a lot is involved. I'm instead going to give an overview of the approach taken, and you can study the profusely commented source code for details.

First of all, we need to define the terms "Docked" and "Undocked". A tool window is undocked when it is floating. And as we already know, in order to make that happen, the tool window must have the WS_POPUP style.

On the other hand, a tool window is docked when it is visually contained completely within its owner window, alongside one of the owner's borders. In order to make this happen, we must create the tool window with the WS_CHILD (not WS_POPUP) style (or change the style from WS_POPUP to WS_CHILD), and make its owner window also its parent window. When a tool window has the WS_CHILD style, the operating system restricts it to the area inside of its parent window, and the tool window is graphically "anchored" to its parent window (i.e., when the end user moves the parent window, the child window automatically moves with it).

But note that when the parent window is resized, the parent window will need to also move/resize the docked tool window so that the tool window remains "attached" to the border. (Of course, our library has functions the owner window can call to make this as easy as possible.)

A good docking library must allow the end user to be able to dock and undock any tool window by grabbing the tool window with the mouse and dragging it over to a dockable or undockable area. There are many different ways to implement docking windows. This is because there is no standard, built-in docking window support in Windows. Application developers have had to implement their own docking windows, or rely upon third party libraries to do the work for them (such as MFC).

There are two common types of docking window implementations. The most common (and intuitive, in my opinion) is the type where you grab the tool window (by a "gripper bar" or its title bar) with the mouse, and drag it around the screen. When you drag the tool window, instead of the window itself moving, a drag-rectangle (feedback rectangle) is XORed on the screen, showing the outline of where the window will move to when you release the mouse - like the way windows work when full-window dragging is turned off. With this method, when a window is dragged to / from a window, the feedback rectangle visibly changes to indicate that the window can be dropped. This is the docking implementation that our docking library uses.
image
A tool window being dragged. You can see the drag rectangle.

The second type of docking implementation can be found in some newer style applications (such as Microsoft Outlook). Instead of a feedback rectangle, windows can be directly "teared" or "snapped" on or off the owner window - i.e., they snap into place as soon as you manipulate them. Personally, I don't like this type of user-interface, and our docking library does not use it.

Our tool windows will have the following characteristics:

  • A docked tool window will have a "gripper bar" along its left side to allow the user to grab it and undock it.
  • A tool window will use a feedback (drag) rectangle as it's moved around the screen - even if the "full window drag" system setting is in effect. This is shown in the picture above.
  • While a drag-rectangle is dragged around the screen, at some point it will intersect one of the borders of its owner window. When this happens, the drag-rectangle will need to visibly change in order to reflect the fact that the tool window is now within a docking "region". Normal convention is for a wide (say three pixel) shaded rectangle to represent a floating position, and for a single-pixel rectangle to represent a docked position.
  • When the mouse is released after dragging a tool window, a test must be made to see if the window should be made to dock or float. (i.e., was the drag-rectangle ultimately moved to one of these docking "regions", or is it outside of any such region and therefore the tool window is floating?)
  • At the end user's discretion, a tool window can be forced to float, even when the drag-rectangle is released over a dockable area. This is usually achieved by the end user holding the key down.
  • When floating, a tool window can be resized just like any normal window. No special processing is required to do this - the standard Windows sizing behaviour can be used in this case.
  • When docked, a tool window can be resized either vertically or horizontally (but not both) to decrease or increase its size. A tool window docked to the top or bottom border of its owner can be resized horizontally. A tool window docked to the left or right border can be resized vertically.
  • When the user double-clicks a floating tool window's titlebar, or a docked window's gripper bar, the tool window is toggled from floating to docked, or vice versa.

Our docking library keeps track of whether a tool window is docked or floating. And if it is docked, we need to know to which of the owner's borders the tool window is docked. The uDockedState field of the DOCKINFO is used to store this state. As mentioned, if this field is OR'ed with DWS_FLOATING, then the tool window is floating. If not OR'ed with DWS_FLOATING, then the tool window is docked, and the remaining bits of the field are either DWS_DOCKED_LEFT, DWS_DOCKED_RIGHT, DWS_DOCKED_TOP, or DWS_DOCKED_BOTTOM depending upon to which border the tool window is docked.

We need to be able to toggle a tool window between being a child window (docked) and being a popup window (floating). This is basically accomplished with the code shown below.

// Assume "dwp" is a pointer to the tool window's DOCKINFO.

DWORD dwStyle = GetWindowLong(dwp->hwnd, GWL_STYLE);

// Is the window currently floating?
if (dwp->uDockedState & DWS_FLOATING)
{
    // Toggle from WS_POPUP to WS_CHILD. We do this by altering
    // the window's style flags to remove WS_POPUP, and add
    // WS_CHILD. Then, we set the owner window as the parent.
    SetWindowLong(dwp->hwnd, GWL_STYLE, (dwStyle & ~WS_POPUP) | WS_CHILD);
    SetParent(dwp->hwnd, dwp->container);
}
else
{
    // Toggle from WS_CHILD to WS_POPUP. We do this by altering
    // the window's style flags to remove WS_CHILD, and add
    // WS_POPUP. Then, we make sure it has no parent.
    SetWindowLong(dwp->hwnd, GWL_STYLE, (dwStyle & ~WS_CHILD) | WS_POPUP);
    SetParent(dwp->hwnd, NULL);
}

Look at the second SetParent API call in the code above. The only way to make a child (docked) window into a popup (floating) window is to set its parent window to zero (NULL). Because the tool window no longer has a parent, it is not visually confined to some other window. It can float freely around the desktop. But because it still has an owner window, the operating system keeps it floating above that owner window. In other words, when a window is docked, its owner window is also its parent window. When a window is floating, its owner window is no longer its parent as well.

Floating versus docked size

As mentioned, a tool window can be in one of two states: docked, or floating (undocked). We will remember the size of a tool window both in its floating state, and its docked state, and store this information in the DOCKINFO. In this way, the end user can give the tool window different sizes for its two states. Because we also allow the end user to quickly toggle between the two states by double-clicking on the gripper/titlebar, we need to remember where the tool window was last positioned in both states.

When a tool window is floating, it can be resized just like a normal window. This means that we will need to store both the width and height in the DOCKINFO. And of course, in order to remember its position, we need to store its X and Y position (in screen coordinates). These values are stored in the DOCKINFO's cxFloating, cyFloating, xpos, and ypos fields respectively.

Note: cxFloating and cyFloating are actually set to the size of the floating tool window's client (inner) area instead of the physical size of the tool window itself (including its titlebar and borders). This is because we always want the client area to remain the same size, even when the system settings change (i.e. the titlebar height is modified using the Control Panel).

When a tool window is docked, it can be resized in only one direction -- vertically or horizontally. This means that we need to remember only its width or height, but not both. If the tool window is docked to the top or bottom border of the owner, then we remember its height. If the tool window is docked to the left or right border of the owner, then we remember its width. Whichever value we remember, we store it in DOCKINFO's nDockedSize field. As far as its position is concerned, that is already remembered in the DOCKINFO's uDockedState field.

Moving a window with a drag-rectangle

The first obstacle we encounter is getting Windows to show a feedback rectangle when the end user moves a floating window around. Starting with Windows 95, a new user-interface feature was introduced. This feature is normally referred to as "Show window contents while dragging". When enabled, windows are no longer moved and sized using the standard feedback rectangle.

Unfortunately, there is no way to turn this feature off for specific windows. The SystemParametersInfo API call (with the SPI_GETDRAGFULLWINDOWS setting) can turn this feature on and off, but this is a system-wide setting, and is not really suitable. Of course, we could devise a method where we temporarily turn off the drag-window system setting just during the window movement (actually, this would be very straight-forward). The point is, it's a bit of a hack, and I prefer proper solutions to problems like this.

The only solution is to override the standard Windows behaviour and manually provide a feedback rectangle. This means processing a few mouse messages. Now, I don't want to show any code - again, the source code clearly demonstrates how to get this working (in the window procedure for a tool window, dockWndProc). What I will do is give a basic outline of the processing that is required.

The most important task is to stop the user from dragging the window around with the mouse. I know this sounds counter-productive, but we need to completely take over the standard window movement logic. This is actually quite simple - our docking window procedure just needs to handle WM_NCLBUTTONDOWN, and return 0 if the mouse is clicked in the caption area. By preventing the default window procedure from handling this message, window dragging is completely disabled.

In order to simulate the window being moved, we need to handle a few mouse messages. Only three need processing:

  1. WM_NCLBUTTONDOWN - This message is received when the end user clicks on a tool window. In addition to returning 0 to prevent the operating system from doing normal window dragging, we draw the drag-rectangle at its initial position, and set the mouse capture using the SetCapture API call. We also install a keyboard hook so we can check if the end user presses the CTRL key (to force the tool window floating) or the ESC key (to abort the operation).
  2. WM_MOUSEMOVE - This message is received whenever the mouse is moved. Our response is to redraw the drag-rectangle in the new position (erase it in the old position and draw it in the new position). In addition, we need to decide what type of rectangle to draw, depending on if the end user has moved the rectangle into a dockable region, or not.
  3. WM_LBUTTONUP - This message is received when the mouse is released. We remove the drag-rectangle from the screen, release the mouse capture, and then take the appropriate action to physically reposition the tool window. This may mean docking / undocking, or simply moving the window if it was already floating.

As you can see, there's a little bit of work involved, but nothing particularly complicated. The big advantage of using this method is that the same mouse code can be used when the window is docked or floating. This keeps the code short and simple.

Drawing a drag-rectangle

A drag-rectangle is basically just a simple rectangle. This rectangle ideally needs to be drawn using XOR blitting logic, so that we can easily draw / erase the rectangle as it is moving around.

image
A tool window being dragged. You can see the drag rectangle.

The code below draws a shaded rectangle with the specified coordinates. The equivalent function in the source code does a little more than the code below (it draws both types of drag-rectangles), but I've stripped it down to keep it simple.

void DrawXorFrame(int x, int y, int width, int height)
{
    // Raw bits for bitmap - enough for an 8x8 monochrome image
    static WORD _dotPatternBmp1[] = 
    {
        0x00aa, 0x0055, 0x00aa, 0x0055, 0x00aa, 0x0055, 0x00aa, 0x0055
    };

    HBITMAP hbm;
    HBRUSH  hbr;
    HANDLE  hbrushOld;
    WORD    *bitmap;

    int border = 3;

    HDC hdc = GetDC(0);

    // Create a patterned bitmap to draw the borders
    hbm = CreateBitmap(8, 8, 1, 1, _dotPatternBmp1);
    hbr = CreatePatternBrush(hbm);

    hbrushOld = SelectObject(hdc, hbr);

    // Draw the rectangle in four stages - top, right, bottom, left
    PatBlt(hdc, x+border, y, width-border,  border, PATINVERT);
    PatBlt(hdc, x+width-border, y+border, border, height-border, PATINVERT);
    PatBlt(hdc, x, y+height-border, width-border, border, PATINVERT);
    PatBlt(hdc, x, y, border, height-border, PATINVERT);

    // Clean up
    SelectObject(hdc, hbrushOld);
    DeleteObject(hbr);
    DeleteObject(hbm);

    ReleaseDC(0, hdc);
}

As you can see, we have the bitmap data for our rectangle as global data in our docking library. And we simply call some graphics functions to blit in onto the screen (in a rectangular shape) at the screen position where the end user has currently moved the mouse.

Redrawing the docked windows

When a tool window's state changes from docked to floating, or vice versa, this means that the layout of the owner window needs to be redrawn. For example, if a tool window was floating, and then is docked to the owner window, then other tool windows already docked may need to be resized/repositioned to accommodate the new docked tool window.

And if a tool window was docked to the owner window, but is torn off and left floating, that means the other, remaining docked windows may likewise need to be resized/repositioned to fill the "hole" left by the formerly docked window.

Whenever a tool window's state toggles between states, our docking library has a function named updateLayout that is called to send a spoofed WM_SIZE message to the owner window to inform it that it needs to redraw itself. The owner window then is expected to redraw its contents and call a docking library function named DockingArrangeWindows. DockingArrangeWindows does all the work of repositioning and redrawing the docked tool windows.

Enumerating tool windows

In the above code excerpts, we had a placeholder function named DockingNextToolWindow that enumerated the tool windows for a given owner window. We don't actually have such a function in the docking library. Let's examine how our docking library actually enumerates tool windows.

Unfortunately, the Windows operating system does not have a function to enumerate all the windows owned by a particular window. If it did, we could just pass our owner window to that function. What the operating system does have is a function called EnumChildWindows. This enumerates all of the child windows of a given parent window. Since a docked tool window has its owner window as its parent also, EnumChildWindows will enumerate all the docked tool windows for a given owner. But EnumChildWindows will not enumerate any of the floating tool windows, because the owner window is not also the parent of the floating tool windows.

There is another operating system function called EnumWindows. This enumerates all of the top-level (i.e., popup) windows on the desktop. Since our floating tool windows are WS_POPUP style, this works to enumerate them. (But, it will enumerate all windows on the desktop in addition to our tool windows, so we have a little extra work to do to isolate only the desired tool windows). EnumWindows does not enumerate any of the children (WS_CHILD windows) of those top-level windows. So, EnumWindows will not enumerate any docked tool windows.

Therefore, enumerating all the tool windows will be a two-step process. First, we'll call EnumChildWindows to enumerate the docked windows for a given parent window (which also happens to be the owner window). Then, we will call EnumWindows to enumerate the floating tool windows for a given owner window, and do some extra processing to make sure that the windows we isolate are for the desired owner window.

Let's examine a function that counts how many total tool windows a given owner window has, both floating and docked.

typedef struct {
   UINT    count;
   HWND    container;
} DOCKCOUNTPARAMS;

/***************** DockingCountFrames() *****************
 * Counts the number of tool windows for the specified
 * owner window.
 *
 * container =   Handle to owner window.
 */

UINT WINAPI DockingCountFrames(HWND container)
{
   DOCKCOUNTPARAMS   dockCount;

   // Initialize count to 0, and store the desired owner window
   dockCount.count = 0;
   dockCount.container = container;

   // Enumerate/count the floating tool windows
   EnumWindows(countProc, (LPARAM)&dockCount);

   // Enumerate/count the docked tool windows
   EnumChildWindows(container, countProc, (LPARAM)&dockCount);

   // Return the total count
   return dockCount.count;
}

/******************* countProc() ********************
 * This is called by EnumChildWindows or EnumWindows
 * for each window.
 *
 * hwnd =       Handle of a window.
 * lParam =     The LPARAM arg we passed to EnumChildWindows
 *              or EnumWindows. That would be our DOCKCOUNTPARAMS.
 */

static BOOL CALLBACK countProc(HWND hwnd, LPARAM lParam)
{
   DOCKINFO *  dwp;
    
   // Is this one of the tool windows for the desired owner window?
   if (GetClassWord(hwnd, GCW_ATOM) == DockingFrameAtom &&
      (dwp = (DOCKINFO *)GetWindowLong(hwnd, GWL_USERDATA)) &&
      dwp->container == ((DOCKCOUNTPARAMS *)lParam)->container)
   {
      // Yes it is. Increment count.
      ((DOCKCOUNTPARAMS *)lParam)->count += 1;
   }

   // Tell operating system to continue.
   return TRUE;
}

Notice that we use countProc() as the callback for both EnumWindows and EnumChildWindows. And we pass our own initialized DOCKCOUNTPARAMS structure to our callback. First, we call EnumWindows to enumerate the floating windows. Then we call EnumChildWindows to enumerate the docked windows for our desired owner. So, let's examine countProc(). The entire key to making this work is to fetch and check the class ATOM for the window. If it matches the ATOM we got when we registered our own docking window class (returned by RegisterClassEx), then we know this is one of our tool windows. And if it is one of our tool windows, we know that its GWL_USERDATA field should contain its DOCKINFO. And note that the owner window handle has been stored in the DOCKINFO's container field. So we need only compare this handle with the owner handle passed to DockingCountFrames in order to determine if it is a tool window for the desired owner window.

Various other features of the library

The discussion above details all of the most important aspects of our docking library's features. But, there are some more, incidental features which are optional. You can enable any of these features for a given tool window just by setting the appropriate value into its DOCKINFO's dwStyle field. For example, you can force a tool window to always stay docked or floating. You can force it to keep its original size. You can restrict to which sides of the owner window the tool window may be docked.

When DockingAlloc creates a DOCKINFO, none of these extra features are enabled.

An application

Up to this point, we've discussed only the code in the docking library. Since the whole intent of the library is to be used by an application, now we'll turn our attention to a sample application. There is a sample C application called DockTest included with the library. This example creates one owner window. The owner has a View -> Tool Window menu item you can select to create a tool window. You can then move the tool window around, docking and undocking it, to get a feel for how the implementation works. Each time you select this menu item, another tool window is created, so you can see how multiple tool windows can be floated and docked.

The owner window we create is an MDI window, and its window procedure is frameWndProc. You can open a document window with the File -> New menu item, and see how the docked tool windows interact with a document window. (But, as we'll see later, the application needs to do a little work to manage this interaction.)

Creating a tool window

Let's examine how the application creates a tool window. This happens when the View -> Tool Window menu item is selected, so the place where we create the tool window is in frameWndProc's handling of WM_COMMAND for menu ID IDM_VIEW_TOOLWINDOW. Below is a slightly simplified version of what needs to be done to create a tool window:

void createToolWindow(HWND owner)
{
   DOCKINFO    *dw;
   HWND        frame;

   // Allocate a DOCKINFO structure.
   if ((dw = DockingAlloc(DWS_DOCKED_BOTTOM)))
   {
      // Create a Docking Frame window (ie, the tool window).
      if ((frame = DockingCreateFrame(dw, owner, "My title")))
      {
         // Create the child window that will be hosted inside of the Docking
         // Frame window (ie, the contents of the tool window's client area)
         // and save it in the DOCKINFO's focusWindow field. We'll create an
         // EDIT control to be the contents, but you can utilize any standard
         // control, or a child window of your own class.
         if((dw->focusWindow = CreateWindow("EDIT", 0, 
              ES_MULTILINE|WS_VSCROLL|WS_CHILD|WS_VISIBLE,
              0,0,0,0,
              frame,
              (HMENU)IDC_MYEDIT, GetModuleHandle(0), 0)))
         {
            // Show the Docking Frame.
            DockingShowFrame(dw);

            // Success!
            return;
         }

         // Destroy the tool window if we can't create its contents.
         // NOTE: The docking library will free the above DOCKINFO.
         DestroyWindow(frame);
      }
      MessageBox(0, "Can't create tool window", "ERROR", MB_OK);
   }
   else
      MessageBox(0, "No memory for a DOCKINFO", "ERROR", MB_OK);
}

First, we call DockingAlloc to get a DOCKINFO structure. We pass the desired initial state, which will be one of DWS_FLOATING, DWS_DOCKED_LEFT, DWS_DOCKED_RIGHT, DWS_DOCKED_TOP, or DWS_DOCKED_BOTTOM, depending upon whether we want the tool window initially created floating, or docked to one of the four borders. The docking library creates a DOCKINFO and initializes it to default values, returning a pointer.

At this point, we could modify the DOCKINFO if we want something other than the default features. In the above code, we simply go with the defaults.

Next, we call DockingCreateFrame to create the actual tool window. We pass the DOCKINFO we just got, the handle to our owner window, and the desired title for the tool window (which is shown only when the tool window is floating). DockingCreateFrame will create the tool window and return its handle. The tool window is not created visible, so nothing has yet shown up onscreen.

Now, a tool window with nothing inside of it would not be of much use. So we need to create something inside of the tool window that is of use to the end user. We can say that the tool window needs some "contents". Specifically, we need to create some WS_CHILD window which has the tool window as its parent. This can be any standard control, such as an Edit box, list box, tree-view control, etc. Or it could be a window of our own class. In the above code, we simply create a multi-line Edit control. Note that we have set the tool window to be this control's parent, and also specified the WS_CHILD style. This will cause the Edit control to be visually embedded inside of the tool window, and automatically move with the tool window. The size and position of the Edit control is not important now, because it will be sized and positioned later, before the tool window is finally made visible. We stuff the handle to this control into the DOCKINFO's focusWindow field. The docking library will automatically size this control to fill the client area of the tool window, and also give the control the focus whenever the user activates that tool window.

Finally, we call DockingShowFrame. This first sends a WM_SIZE message to our owner window (which is where we will do the final sizing/positioning of the tool window and its contents-window), and then makes the tool window visible.

That's all there is to creating a tool window. At this point, the docking library will manage the docking and undocking of this window.

Handling WM_SIZE message in the owner

The docking library transparently handles most aspects of the tool windows. But there are a couple times when it needs help from the application. One such time is whenever the owner window is resized. Given a new size for the owner window, it stands to reason that any docked tool window may also need to be resized and repositioned so that it stays docked to the desired side of the owner window. For this reason, the owner window will have to do the following when it receives a WM_SIZE:

case WM_SIZE:
{
   HDWP   hdwp;
   RECT   rect;

   // Do the default handling of this message.
   DefFrameProc(hwnd, MainWindow, msg, wParam, lParam);

   // Set the area where tool windows are allowed.
   // (Take into account any status bar, toolbar etc).
   rect.left = rect.top = 0;
   rect.right = LOWORD(lParam);
   rect.bottom = HIWORD(lParam);

   // Allocate enough space for all tool windows which are docked.
   hdwp = BeginDeferWindowPos(DockingCountFrames(hwnd,
                      1) + 1); // + 1 for the MDI client

   // Position the docked tool windows for this owner
   // window. rect will be modified to contain the "inner" client
   // rectangle, where we can position an MDI client.
   DockingArrangeWindows(hwnd, hdwp, &rect);

   // Here we resize our MDI client window so that it fits into the area
   // described by "rect". Do not let it extend outside of this
   // area or it (and the client windows inside of it) will be obscured
   // by docked tool windows (or vice versa).
   DeferWindowPos(hdwp, MainWindow, 0, rect.left, rect.top,
            rect.right - rect.left, rect.bottom - rect.top,
            SWP_NOACTIVATE|SWP_NOZORDER);

   EndDeferWindowPos(hdwp);

   return 0;
}

First, we pass the WM_SIZE to DefFrameProc to let the operating system do the default sizing of the owner window. We need to do this first so that the owner window's size is finalized before we go ahead and resize/reposition the docked tool windows.

The docking library has a function called DockingArrangeWindows that redraws all of the docked windows for a given owner. So to completely redraw its docked windows, all the owner needs to do is call this one function. But, there are a couple prerequisites. First, the owner must fill in a RECT with the dimensions of its client area, and pass this to DockingArrangeWindows. Secondly, the owner window must call the Windows API BeginDeferWindowPos to reserve enough space for all the docked windows. (The docking library has a function called DockingCountFrames which can be called to retrieve the total number of docked tool windows in an owner.) We use BeginDeferWindowPos so that, if there are many tool windows, we defer the final painting until after all of them are sized and positioned. This is more efficient and doesn't cause any unsightly visual artifacts for the end user to witness.

One very important aspect to note is that, after DockingArrangeWindows resizes and repositions all of the docked tool windows, it updates the RECT so that it encompasses only the owner client area not occupied by the tool windows. In other words, the RECT is the remaining, blank client area. We take this remaining area, and we resize/reposition our MDI child so that it fills only this remaining area. In this way, our document windows are not obscured by docked tool windows, and vice versa.

Handling WM_NCACTIVATE message in the owner

Another instance where our docking library needs help from the application is whenever the owner window receives a WM_NCACTIVATE message. Remember earlier we discussed how to keep all tool windows' titlebar activation in sync with the owner window. Now, we need the owner window to let the docking library know whenever it receives a WM_NCACTIVATE message. The owner window will need to do the following:

case WM_NCACTIVATE:
{
   DOCKPARAMS   dockParams;

   dockParams.container = dockParams.hwnd = hwnd;
   dockParams.wParam = wParam;
   dockParams.lParam = lParam;
   return(DockingActivate(&dockParams));
}

We simply fill in a DOCKPARAMS struct (defined in DockWnd.h) with the values we receive from the WM_ACTIVATE message, and also fill in the owner window handle (in DOCKPARAMS container field) and the handle of the window receiving the WM_ACTIVATE (which here, is the owner window). Then we call DockingActivate which takes care of syncing all tool windows' titlebar activation.

Handling WM_ENABLE message in the owner

Another instance where our docking library needs help from the application is whenever the owner window receives a WM_ENABLE message. Remember earlier we discussed how to keep all tool windows' enabled state in sync with the owner window. Now, we need the owner window to let the docking library know whenever it receives a WM_ENABLE message. The owner window will need to do the following:

case WM_ENABLE:
{
   DOCKPARAMS   dockParams;

   dockParams.container = dockParams.hwnd = hwnd;
   dockParams.wParam = wParam;
   dockParams.lParam = lParam;
   return(DockingEnable(&dockParams));
}

This is almost the same as the WM_NCACTIVATE handling, but it concerns the WM_ENABLE message, and we call a function named DockingEnable. DockingEnable takes care of syncing all tool windows' enabled state.

Handling messages sent to standard controls

You'll note that we used a standard Edit control as the content of our tool window. As you should know, an Edit window sends messages to its parent for certain actions. For example, when the end user alters the contents of the Edit control, a WM_COMMAND message is sent with a notification code of EN_CHANGE.

But remember that the parent window is the tool window, and our tool window procedure (dockWndProc) is in the docking library. So how does an application get a hold of that message?

There is a DockMsg field in the DOCKINFO. Into this field, we will stuff a pointer to a function in our application. We do this after we DockAlloc the DOCKINFO as so:

// Allocate a DOCKINFO structure.
if ((dw = DockingAlloc(DWS_DOCKED_BOTTOM)))
{
   // Set our own function for the docking library to call.
   dw->DockMsg = myMessages;

   ...

Whenever dockWndProc receives a message that it doesn't handle, such as a WM_COMMAND or WM_NOTIFY, it will call our application function, passing the DOCKINFO of the tool window, as well as the message, WPARAM, and LPARAM parameters.

Here is an example of the function we could add to handle a EN_CHANGE from our IDC_MYEDIT edit control:

LRESULT WINAPI myMessages(DOCKINFO * dwp, UINT message, WPARAM wParam, LPARAM lParam)
{
   switch (message)
   {
      case WM_COMMAND:
      {
         if (LOWORD(wParam) == IDC_MYEDIT)
         {
            if (HIWORD(wParam) == EN_CHANGE)
            {
               // Here we would handle the EN_CHANGE, and then return
               // 0 to tell the docking library we handled it.
               return 0;
           }
         }
      }
   }

   // Return -1 if we want the docking library to do default handling.
   return -1;
}

Note: Each DOCKINFO can have its own DockMsg function, so you do not need to worry about control ID conflicts between tool windows.

Multiple child windows inside a tool window

Above, we used a single Edit control to fill the client area of the tool window. But what if we would like several controls inside the tool window, for example, an Edit control as well as a push button labeled "Clear" which clears the text from the Edit control?

This is entirely possible, but there are a couple of prerequisites. First, when we create the Edit and button controls, we must make both of them children of the tool window. Secondly, we must provide a function that will resize and reposition the controls, and stuff a pointer to this in the DOCKINFO's DockResize field after we DockAlloc the DOCKINFO.

// Allocate a DOCKINFO structure.
if ((dw = DockingAlloc(DWS_DOCKED_BOTTOM)))
{
   // Set our own functions for the docking library to call.
   dw->DockMsg = myMessages;
   dw->DockResize = myResize;

   ...

When the docking library calls our function, it passes the DOCKINFO for the tool window, as well as a RECT that encompasses the area that we need to fill. Here is an example of a function we could add to resize and reposition the edit and button controls so that the button stays near the bottom border of the tool window, and the edit control fills the rest of the area:

void WINAPI< myResize(DOCKINFO * dwp, RECT * area)
{
   HWND   child;

   // Position the button above the bottom border
   child = GetDlgItem(dw->hwnd, IDC_MYBUTTON);
   SetWindowPos(child, 0, rect->left + 10, 
     rect->bottom - 20, 50, 18, SWP_NOZORDER|SWP_NOACTIVATE);

   // Let the edit fill the remaining area
   child = GetDlgItem(dw->hwnd, IDC_MYEDIT);
   SetWindowPos(child, 0, rect->left, rect->top, 
     rect->right - rect->left, (rect->bottom - rect->top) - 22, 

SWP_NOZORDER|SWP_NOACTIVATE);
}

Closing a tool window

When an owner window is destroyed, all of its tool windows are also automatically destroyed (except if you use the DWS_FREEFLOAT style. In that case, your owner window should handle WM_DESTROY and call DockingDestroyFreeFloat). The docking library will normally free the DOCKINFO for each tool window destroyed.

If you wish to manually close a tool window, you simply call the Windows API DestroyWindow, passing the handle to the desired tool window. Again, the docking library will normally free the DOCKINFO.

If you wish to override the docking library's default behavior of freeing the DOCKINFO, then you must write your own function, and stuff a pointer into the DOCKINFO's DockDestroy field. The docking library will call this function (passing it the DOCKINFO) whenever the tool window has been destroyed. It is your responsibility to eventually free the DOCKINFO by passing it to DockingFree. One use for this is to keep a DOCKINFO allocated (for a given tool window) throughout the lifetime of your application. You will reuse this same DOCKINFO with DockingCreateFrame each time the end user reopens that tool window. Because the DOCKINFO stores the last size and position of the tool window, this means that the tool window will reappear where it was located right before it was previously destroyed. You will then free the DOCKINFO only when your application is ready to terminate.

Saving/Restoring a tool window size/position

Each time that you run your application, you will normally want to restore the same sizes and positions for the tool windows that the end user set upon the last time your application was run. The docking library has two functions to help save and restore tool window positions. The data is saved to the Windows registry under some key of your choosing. The size/position of each tool window is saved separately as values under that key.

Before you free a tool window's DOCKINFO, you should first create/open some registry key of your choosing in order to save that tool window's settings. Then, you will pass the DOCKINFO, and a handle to this open key, to DockingSavePlacement. The docking library will save that tool window's settings. Here is an example of how we could save the settings of a tool window under the registry key "HKEY_CURRENT_USER\Software\MyKey\MyToolWindow":

HKEY   hKey;
DWORD  temp;

// Open/Create the "Software\MyKey\MyToolWindow" key under CURRENT_USER.
if (!RegCreateKeyEx(HKEY_CURRENT_USER, "Software\\MyKey\\MyToolWindow",
   0, 0, REG_OPTION_NON_VOLATILE, KEY_ALL_ACCESS, 0, &hKey, &temp))
{
   // Let the docking library save this tool window's settings.
   DockingSavePlacement(dw, hKey);

   // Close registry key.
   RegCloseKey(hKey);
}

Whenever your program runs, it should restore those settings by calling DockingLoadPlacement right after DockAlloc'ing the DOCKINFO for that tool window. Here is an example of restoring the previously saved settings:

// Allocate a DOCKINFO structure.
if ((dw = DockingAlloc(DWS_DOCKED_BOTTOM)))
{
   HKEY   hKey;

   // Open the "Software\\MyKey\\MyToolWindow" key under CURRENT_USER.
   if (!RegOpenKeyEx(HKEY_CURRENT_USER, "Software\\MyKey\\MyToolWindow",
      o, KEY_ALL_ACCESS, &hKey))
   {
      // Let the docking library restore this tool window's settings.
      DockingLoadPlacement(dw, hKey);

      // Close registry key.
      RegCloseKey(hKey);
   }

   ...

Note: If there are no previously saved settings for the tool window, then none of the DOCKINFO fields are altered.

Miscellaneous remarks

Your source code needs to #include the file DockWnd.h. Also, if you want to statically link with the docking library, you must feed the file DockWnd.lib to your linker. The Visual C++ project files already have these settings made.

For best viewing of the source code, set your editor's TAB width to 3.

In the docking library sources, all function names that begin with a capital letter are functions that are callable by an application. All functions beginning with a lower case letter are called only internally. All variable names that begin with a capital letter are global variables. All variables beginning with a lower case letter are local variables (passed on, or declared on, the stack).

Included with both the library and example sources are Microsoft Visual C++ (4.0 or better) project files to quickly get you up and running.

Conclusion

The source code download presents a complete docking window library, and a small sample C application to show you how to use it. Hopefully you should be able to use this library with your own applications and get instant docking windows!

跨进程窗口

在实际应用中,会因为各种奇奇怪怪的原因,需要跨进程进行窗口交互。那么跨进程窗口会有问题吗?

在理论上是完全没有问题的,从系统层面上各个进程的窗口本身就是跨进程被系统所管理的,只是在窗口的互相之间没有太深的关联(例如父子关系、拥有被拥有关系)。并且窗口句柄可以跨进程传递,跨进程可以枚举系统窗口资源。

在实际应用和文档记载上,跨进程窗口可以实现父子窗口-SetParent。

跨进程窗口 SetParent

使用 SetParent 跨进程设置父子窗口时的一些问题:小心卡死

在微软的官方文档中,说 SetParent 可以在进程内设置,也可以跨进程设置。当使用跨进程设置窗口的父子关系时,你需要注意本文提到的一些问题,避免踩坑。

跨进程设置 SetParent

关于 SetParent 函数设置窗口父子关系的文档可以看这个:

SetParent function (winuser.h) - Microsoft Docs

在这篇文章的 DPI 感知一段中明确写明了在进程内以及跨进程设置父子关系时的一些行为。虽然没有明确说明支持跨进程设置父子窗口,不过这段文字就几乎说明 Windows 系统对于跨进程设置窗口父子关系还是支持的。

但 Raymond Chen 在 Is it legal to have a cross-process parent/child or owner/owned window relationship? 一文中有另一段文字:

If I remember correctly, the documentation for Set­Parent used to contain a stern warning that it is not supported, but that remark does not appear to be present any more. I have a customer who is reparenting windows between processes, and their application is experiencing intermittent instability.
如果我没记错的话,SetParent 的文档曾经包含一个严厉的警告表明它不受支持,但现在这段备注似乎已经不存在了。我就遇到过一个客户跨进程设置窗口之间的父子关系,然后他们的应用程序间歇性不稳定。

这里表明了 Raymond Chen 对于跨进程设置父子窗口的一些担忧,但从文档趋势来看,还是支持的。只是这种担忧几乎说明跨进程设置 SetParent 存在一些坑。

跨进程的坑:消息循环强制同步

消息循环

我们会感觉到 Windows 中某个窗口有响应(比如鼠标点击有反应),是因为这个窗口在处理 Windows 消息。窗口进行消息循环不断地处理消息使得各种各样的用户输入可以被处理,并正确地在界面上显示。

一个典型的消息循环大概像这样:

while(GetMessage(ref msg, IntPtr.Zero, 0, 0))
{
    TranslateMessage(ref msg);
    DispatchMessage(ref msg);
}

对于显示了窗口的某个线程调用了 GetMessage 获取了消息,Windows 系统就会认为这个线程有响应。相反,如果长时间不调用 GetMessage,Windows 就会认为这个线程无响应。TranslateMessage 则是翻译一些消息(比如从按键消息翻译成字符消息)。真正处理 GetMessage 中的内容则是后面的调度消息 DispatchMessage,是这个函数的调用使得我们 UI 界面上的内容可以有可见的反映。

一般来说,每个创建了窗口的线程都有自己独立的消息循环,且不会互相影响。然而一旦这些窗口之间建立了父子关系之后就会变得麻烦起来。

强制同步

Windows 会让具有父子关系的所有窗口的消息循环强制同步。具体指的是,所有具有父子关系的窗口消息循环,其消息循环会串联成一个队列(这样才可以避免消息循环的并发)。

也就是说,如果你有 A、B、C、D 四个窗口,分属不同进程,A 是 B、C、D 窗口的父窗口,那么当 A 在处理消息的时候,B、C、D 的消息循环就会卡在 GetMessage 的调用。同样,无论是 B、C 还是 D 在处理消息的时候,其他窗口也会同样卡在 GetMessage 的调用。这样,所有进程的 UI 线程实际上会互相等待,所有通过消息循环执行的代码都不会同时执行。然而实际上 Windows GUI 应用程序的开发中基本上 UI 代码都是通过消息循环来执行的,所以这几乎等同于所有进程的 UI 线程强制同步成类似一个 UI 线程的效果了。

带来的副作用也就相当明显,任何一个进程卡了 UI,其他进程的 UI 将完全无响应。当然,不依赖消息循环的代码不会受此影响,比如 WPF 应用程序的动画和渲染。

如何解决

对于 SetParent 造成的这些问题,实际上没有官方的解决方案,你需要针对你不同的业务采用不同的解决办法。

正如 Raymond Chen 所说:

(It’s one of those “if you don’t already know what the consequences are, then you are not smart enough to do it correctly” things. You must first become the master of the rules before you can start breaking them.)
正如有些人说的“如果你不知道后果,那么你也不足以正确地完成某件事情”。在开始破坏规则之前,您必须先成为规则的主人。

你必须清楚跨进程设置父子窗口带来的各种副作用,然后针对性地给出解决方案:

  • 比如所有窗口会强制串联成一个队列,那么可以考虑将暂时不显示的窗口断开父子关系;
  • 比如设置窗口的位置大小等操作,必须考虑此窗口不是顶层窗口的问题,需要跨越进程到顶层窗口来操作;

参考:

WindowFromPoint捕获鼠标悬停的实际窗口

大部分情况下,我们都可以使用WindowFromPoint来捕获鼠标当前悬停所在的窗口句柄。但是,总是有一些特殊情况,需要我们进行特殊处理。

  1. 获取组合控件或者嵌入式窗口的实际窗口
  2. 截图时获取窗口:我们可能需要使用到ChildWindowFromPointEx
  3. 获取透明窗口

如上各自问题的解决方案有:

  1. 侵入式场景方案一:利用框架层解决
  • 界面库提供一个统一的窗口层级管理系统,来记录窗口的zorder和区域
  • 实现z-order动态实时更新
  • 此时我们可以快速来处理如上的问题
  1. 非框架层或者非侵入式场景:利用窗口遍历
  • 系统级别:遍历桌面层级的所有窗口,记录z-order和区域,然后进行获取
  • 进程级别:GetWindowThreadProcessId进行进程过滤
  • 窗口级别:WindowFromPoint获取当前窗口,然后遍历获取同z-order级别窗口
  1. 侵入式场景方案二:在进程中利用窗口遍历来实现
  • 例如通过EnumThreadWindow来遍历,减少遍历的窗口数量

在实际应用场景下,可以根据要求来选取具体方案

使用 SetWindowCompositionAttribute 来控制程序的窗口边框和背景(可以做 Acrylic 亚克力效果、模糊效果、主题色效果等)

Windows 系统中有一个没什么文档的 API,SetWindowCompositionAttribute,可以允许应用的开发者将自己窗口中的内容渲染与窗口进行组合。这可以实现很多系统中预设的窗口特效,比如 Windows 7 的毛玻璃特效,Windows 8/10 的前景色特效,Windows 10 的模糊特效,以及 Windows 10 1709 的亚克力(Acrylic)特效。而且这些组合都发生在 dwm 进程中,不会额外占用应用程序的渲染性能。

本文介绍 SetWindowCompositionAttribute 可以实现的所有效果。你可以通过阅读本文了解到与系统窗口可以组合渲染到哪些程度。

试验用的源代码

本文将创建一个简单的 WPF 程序来验证 SetWindowCompositionAttribute 能达到的各种效果。你也可以不使用 WPF,得到类似的效果。

简单的项目文件结构是这样的:

[项目] Walterlv.WindowComposition
App.xaml
App.xaml.cs
MainWindow.xaml
MainWindow.xaml.cs
WindowAccentCompositor
其中,App.xaml 和 App.xaml.cs 保持默认生成的不动。

为了验证此 API 的效果,我需要将 WPF 主窗口的背景色设置为纯透明或者 null,而设置 ControlTemplate 才能彻彻底底确保所有的样式一定是受我们自己控制的,我们在 ControlTemplate 中没有指定任何可以显示的内容。MainWindow.xaml 的全部代码如下:

<Window x:Class="Walterlv.WindowComposition.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="欢迎访问吕毅的博客:blog.walterlv.com" Height="450" Width="800">
    <Window.Template>
        <ControlTemplate TargetType="Window">
            <AdornerDecorator>
                <ContentPresenter />
            </AdornerDecorator>
        </ControlTemplate>
    </Window.Template>
    <!-- 我们注释掉 WindowChrome,是因为即将验证 WindowChrome 带来的影响。 -->
    <!--<WindowChrome.WindowChrome>
        <WindowChrome GlassFrameThickness="-1" />
    </WindowChrome.WindowChrome>-->
    <Grid>
    </Grid>
</Window>

而 MainWindow.xaml.cs 中,我们简单调用一下我们即将写的调用 SetWindowCompositionAttribute 的类型。

using System.Windows;
using System.Windows.Media;
using Walterlv.Windows.Effects;

namespace Walterlv.WindowComposition
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            var compositor = new WindowAccentCompositor(this);
            compositor.Composite(Color.FromRgb(0x18, 0xa0, 0x5e));
        }
    }
}

还剩下一个 WindowAccentCompositor.cs 文件,因为比较长放到博客里影响阅读,所以建议前往这里查看:Walterlv.Packages/WindowAccentCompositor.cs at master · walterlv/Walterlv.Packages

而其中对我们最终渲染效果有影响的就是 AccentPolicy 类型的几个属性。其中 AccentState 属性是下面这个枚举,而 GradientColor 将决定窗口渲染时叠加的颜色。

private enum AccentState
{
    ACCENT_DISABLED = 0,
    ACCENT_ENABLE_GRADIENT = 1,
    ACCENT_ENABLE_TRANSPARENTGRADIENT = 2,
    ACCENT_ENABLE_BLURBEHIND = 3,
    ACCENT_ENABLE_ACRYLICBLURBEHIND = 4,
    ACCENT_INVALID_STATE = 5,
}

影响因素

经过试验,对最终显示效果有影响的有这些:

  • 选择的 AccentState 枚举值
  • 使用的 GradientColor 叠加色
  • 是否使用 WindowChrome 让客户区覆盖非客户区
  • 目标操作系统(Windows 7/8/8.1/10)

使用 WindowChrome,你可以用你自己的 UI 覆盖掉系统的 UI 窗口样式。关于 WindowChrome 让客户区覆盖非客户区的知识,可以阅读:

[WPF 自定义控件] Window(窗体)的 UI 元素及行为 - dino.c - 博客园

需要注意的是,WindowChrome 的 GlassFrameThickness 属性可以设置窗口边框的粗细,设置为 0 将导致窗口没有阴影,设置为负数将使得整个窗口都是边框。

排列组合

AccentState=ACCENT_DISABLED
使用 ACCENT_DISABLED 时,GradientColor 叠加色没有任何影响,唯一影响渲染的是 WindowChrome 和操作系统。

总结

由于 Windows 7 上所有的值都是同样的效果,所以下表仅适用于 Windows 10。

image

在以上的特效之下,WindowChrome 可以让客户区覆盖非客户区,或者让整个窗口都获得特效,而不只是标题栏。

附源代码

请参见 GitHub 地址以获得最新代码。如果不方便访问,那么就看下面的吧。

Walterlv.Packages/WindowAccentCompositor.cs at master · walterlv/Walterlv.Packages

using System;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Interop;
using System.Windows.Media;

namespace Walterlv.Windows.Effects
{
    /// <summary>
    /// 为窗口提供模糊特效。
    /// </summary>
    public class WindowAccentCompositor
    {
        private readonly Window _window;

        /// <summary>
        /// 创建 <see cref="WindowAccentCompositor"/> 的一个新实例。
        /// </summary>
        /// <param name="window">要创建模糊特效的窗口实例。</param>
        public WindowAccentCompositor(Window window) => _window = window ?? throw new ArgumentNullException(nameof(window));

        public void Composite(Color color)
        {
            Window window = _window;
            var handle = new WindowInteropHelper(window).EnsureHandle();

            var gradientColor =
                // 组装红色分量。
                color.R << 0 |
                // 组装绿色分量。
                color.G << 8 |
                // 组装蓝色分量。
                color.B << 16 |
                // 组装透明分量。
                color.A << 24;

            Composite(handle, gradientColor);
        }

        private void Composite(IntPtr handle, int color)
        {
            // 创建 AccentPolicy 对象。
            var accent = new AccentPolicy
            {
                AccentState = AccentState.ACCENT_ENABLE_ACRYLICBLURBEHIND,
                GradientColor = 0,
            };

            // 将托管结构转换为非托管对象。
            var accentPolicySize = Marshal.SizeOf(accent);
            var accentPtr = Marshal.AllocHGlobal(accentPolicySize);
            Marshal.StructureToPtr(accent, accentPtr, false);

            // 设置窗口组合特性。
            try
            {
                // 设置模糊特效。
                var data = new WindowCompositionAttributeData
                {
                    Attribute = WindowCompositionAttribute.WCA_ACCENT_POLICY,
                    SizeOfData = accentPolicySize,
                    Data = accentPtr,
                };
                SetWindowCompositionAttribute(handle, ref data);
            }
            finally
            {
                // 释放非托管对象。
                Marshal.FreeHGlobal(accentPtr);
            }
        }

        [DllImport("user32.dll")]
        private static extern int SetWindowCompositionAttribute(IntPtr hwnd, ref WindowCompositionAttributeData data);

        private enum AccentState
        {
            ACCENT_DISABLED = 0,
            ACCENT_ENABLE_GRADIENT = 1,
            ACCENT_ENABLE_TRANSPARENTGRADIENT = 2,
            ACCENT_ENABLE_BLURBEHIND = 3,
            ACCENT_ENABLE_ACRYLICBLURBEHIND = 4,
            ACCENT_INVALID_STATE = 5,
        }

        [StructLayout(LayoutKind.Sequential)]
        private struct AccentPolicy
        {
            public AccentState AccentState;
            public int AccentFlags;
            public int GradientColor;
            public int AnimationId;
        }

        [StructLayout(LayoutKind.Sequential)]
        private struct WindowCompositionAttributeData
        {
            public WindowCompositionAttribute Attribute;
            public IntPtr Data;
            public int SizeOfData;
        }

        private enum WindowCompositionAttribute
        {
            // 省略其他未使用的字段
            WCA_ACCENT_POLICY = 19,
            // 省略其他未使用的字段
        }
    }
}

透明窗口:WS_EX_TRANSPARENT扩展样式下,窗口鼠标消息

透明窗口使用WS_EX_TRANSPARENT来实现透明,将可以利用系统的窗口混合实现自下而上的依次渲染,类似于窗口透明效果。但是针对鼠标消息,却没有WS_EX_LAYERED的穿透特性。

讨论场景:多层窗口,最顶层窗口为透明属性。在窗口层级中,可能会插入外部进程的窗口,例如使用other process实现的浏览器窗口。

一、插入窗口为同线程窗口

此种情况,我们可以简单的处理WM_NCHITTEST即可实现鼠标的穿透处理。

二、插入窗口为非同线程窗口

根据MSDN的描述,WM_NCHITTEST的测试只能在当前线程的窗口之间进行。可以参见WindowFromPoint功能。针对此种情况,我们做如下情况的分析和尝试。插入窗口我们这里简单称作InsertWindow,原窗口我们简称为Window。

  1. InsertWidow与Window之间无任何关系:非父子、非owner,可能是程序手动做了层级处理

无关窗口,却插入到原生窗口层级之间,应为手动设置了窗口层级,此种情况只能枚举系统窗口来查找和判断窗口是否重叠。

  1. InsertWindow与Window:父子窗口

父子关系窗口,我们可以使用EnumchildWindow来枚举窗口,判断窗口显示、位置重叠;然后可以尝试如下几种方式,第一种Window的ncHitTest返回HTNOWHERE看系统是否可以查找到响应窗口,第二种消息转发,第三种模拟鼠标操作。

  1. InsertWindow与Window:owner关系

owner关系类似于父子关系处理。

不同线程不知道是否可以hook窗口过程函数? 如果可以就可以监听窗口位置和显示隐藏状态,不然就需要时时刻刻来进行窗口状态的判断。

三、从应用的角度分析如上问题

同线程的对逻辑处理无任何影响,不同线程的插入窗口若无关系窗口可以不考虑,我们更多的是处理自身的相关窗口逻辑。

  1. owner关系窗口:owner插入的窗口应该是想要实现透明,利用popup窗口实现。在win10上可以使用子窗口替代实现,在win10以下系统,我们自身的原有窗口也只能使用Layered窗口,故而不存在此问题。

  2. 父子关系窗口:子窗口不管是否有透明属性,不管系统如何处理的融合渲染以及对展示是否有影响。我们只是想要处理鼠标穿透,故而其他都不做扩展讨论。在nchittest时,检测insertWindow为非client区域则应由原生窗口来响应鼠标消息,客户区则需要进行HTNOWHERE或者鼠标消息转发处理。不同线程的窗口,在NCHITTEST时,是否可以设置SetFoucs、setCapture、SetActivate等给InsertWindow? 是否会有效果?

四、测试实验

  1. HTNOWHERE:经过实验,对NCHitTest返回HTNOWHERE时,系统并不会继续去测试窗口层级下方的窗口。故而此种方法直接被废。

  2. 是否可以激活InsertWindow?

  3. 是否可以进行消息转发?

  4. 是否可以进行鼠标模拟?

向窗口发送ESC

在Windows系统的界面编程中,浏览器编程是一种热门技术。

内嵌一个浏览器窗口并全屏播放,如果浏览器窗口在A屏,此时在B屏执行相关的窗口操作,此时可能浏览器未正常退出全屏,我们可以向窗口发送ESC命令。

    POINT position = {0};
    ::GetCursorPos(&position);

    CRect full_rect;
    ::GetWindowRect(full_sreen_hwnd_, &full_rect);

    INPUT input[2];
    memset(input, 0, sizeof(input));

    input[0].type = INPUT_MOUSE;
    input[0].mi.dx =
        static_cast<long>(65535.0f / (GetSystemMetrics(SM_CXSCREEN) - 1) *
                          (full_rect.left + 100));
    input[0].mi.dy = static_cast<long>(
        65535.0f / (GetSystemMetrics(SM_CYSCREEN) - 1) * (full_rect.top + 100));
    input[0].mi.dwFlags = MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_MOVE |
                          MOUSEEVENTF_MIDDLEDOWN | MOUSEEVENTF_MIDDLEUP;

    input[1].ki.wVk = VK_ESCAPE;
    input[1].type = INPUT_KEYBOARD;
    input[1].ki.wScan =
        static_cast<BYTE>(MapVirtualKey(VK_ESCAPE, MAPVK_VK_TO_VSC));

    ::SendInput(2, input, sizeof(INPUT));

    ::SetCursorPos(position.x, position.y);

其中,full_sreen_hwnd_可以通过OnActivateApp消息时取GetForegroundWindow,然后判断与浏览器同进程、同线程等约束条件而得。

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.