LAYERED窗口
如果你不考虑任何性能问题,那么使用 WS_EX_LAYERED 样式创建透明窗口,无疑是最简单的方式。如下代码所示:
CreateWindowEx(WS_EX_LAYERED, wcx.lpszClassName, wcx.lpszClassName,
WS_CLIPCHILDREN | WS_CLIPSIBLINGS, x, y, w, h,
NULL, NULL, hinstance, NULL);
通过这种方式创建的窗口收不到 WM_PAINT 消息,要想重绘窗口得自己写方法,如下代码所示:
//这4行代码用于创建内存DC和BITMAP对象
auto hdc = GetDC(hwnd);
auto compDC = CreateCompatibleDC(NULL);
auto bitmap = CreateCompatibleBitmap(hdc, w, h);
DeleteObject(SelectObject(compDC, bitmap));
//这2行代码用于拷贝img里的像素数据到BITMAP对象
BITMAPINFO info = { sizeof(BITMAPINFOHEADER), w, 0 - h, 1, 32, BI_RGB, w * 4 * h, 0, 0, 0, 0 };
SetDIBits(hdc, bitmap, 0, h, img.pixelData, &info, DIB_RGB_COLORS);
//这5行代码用于更新窗口
BLENDFUNCTION blend = { .BlendOp{AC_SRC_OVER}, .SourceConstantAlpha{255}, .AlphaFormat{AC_SRC_ALPHA} };
POINT pSrc = { 0, 0 };
SIZE sizeWnd = { w, h };
UpdateLayeredWindow(hwnd, hdc, NULL, &sizeWnd, compDC, &pSrc, NULL, &blend, ULW_ALPHA);
ReleaseDC(hwnd, hdc);
//最后释放资源
DeleteDC(compDC);
DeleteObject(bitmap);
在这段代码中,我们把img图像里的像素数据渲染到窗口上了。我代码里写了注释,这里就不多做解释了(示例仓储里的代码用的是Blend2D,想搞Qt的QImage你可以问我,看本文最后)。
程序运行效果如下:
很多开发者用这种方式创建异型窗口,因为系统会为这类窗口执行命中测试的任务(如果鼠标所在点的像素是透明的,则允许鼠标消息透传到该窗口下面的窗口),
这样的窗口有四个性能上的问题:
- 它用不到GPU硬件加速的能力,所有绘制操作都是在CPU和内存中完成的。
- CPU要为这类窗口执行命中测试的工作(鼠标移动就会检测,就会产生CPU消耗)。
- 重绘窗口必须更新整个窗口区域,无法做使InvalidateRect这样的API更新窗口的某一部分区域(可以去看看UpdateLayeredWindow 和 UpdateLayeredWindowIndirect 的文档)
- 第四个问题我们待会儿再说。
Desktop Window Manager
在正式介绍新方案之前,我们先介绍一下 Windows 操作系统的:Desktop Window Manager,微软自 Windows Vista 就引入了这个模块,这个模块在系统中的进程如下所示:
此模块的作用:
- 桌面窗口合成:使用硬件加速技术将所有窗口的内容合成到到一起并显示在屏幕上。
- 桌面窗口管理:跟踪每个窗口的位置、大小和 Z 序,处理窗口的呈现顺序(确保不同Z序,互相覆盖的窗口能正确的显示在屏幕上)。
- 桌面窗口特效:使用硬件加速技术处理窗口透明、窗口切换动画等。
这个模块彻底改变了应用程序窗口在桌面上的呈现方式。它不再允许每个窗口直接呈现在屏幕上,而是迫使每个窗口都呈现到一个屏外缓冲区中,各窗口的GDI 绘制命令、 Direct3D 显示请求都被重定向到这些缓冲区。因此这些缓冲区被称为窗口的重定向表面。
DWM 进程把所有窗口的显示请求组合到一起(合并每个窗口的缓冲区内容),再添加一些自己的内容(比如窗口阴影等),再呈现到屏幕上。这个工作是 DWM 借助 GPU 硬件加速能力完成的。
一般情况下,窗口的重定向表面都是不透明的。从性能角度来看,这很合理,因为 alpha 混合非常昂贵(两个半透明窗口重叠时最终呈现到屏幕上的是什么像素,这样的计算工作就是 alpha 混合工作)。
虽然 WS_EX_LAYERED 样式的窗口必须驻留在系统内存中,但 DWM 仍会将其复制到 GPU 显存中,以便 DWM 能正常完成自己的职责,也就是说 WS_EX_LAYERED 样式的窗口还要为内存的复制工作支付成本(这就是 WS_EX_LAYERED 样式的窗口的第四个性能问题)。
使用模糊API创建透明窗口
DWM 为开发者提供了一个API:DwmEnableBlurBehindWindow 这个API本来是给 Windows Vista 窗口启用背景模糊效果的,自 Windows8 开始这个方法已经不能再生成模糊效果了,但我们可以用它来为窗口设置透明效果,如下代码所示:
HRGN region = CreateRectRgn(0, 0, -1, -1);
DWM_BLURBEHIND bb = { 0 };
bb.dwFlags = DWM_BB_ENABLE | DWM_BB_BLURREGION;
bb.hRgnBlur = region;
bb.fEnable = TRUE;
DwmEnableBlurBehindWindow(hWnd, &bb);
DeleteObject(region);
这样设置了窗口之后,我们就可以使用 Direct2D 在窗口上绘制半透明图像了,下面是创建Direct2D 资源的示例代码:
static int w{ 800 }, h{600};
static ID2D1Factory* d2d1Factory = nullptr;
static ID2D1HwndRenderTarget* renderTarget = nullptr;
D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED,&d2d1Factory);
auto pixelFormat = D2D1::PixelFormat(DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED);
auto renderProps = D2D1::RenderTargetProperties(D2D1_RENDER_TARGET_TYPE_DEFAULT,pixelFormat);
auto size = D2D1::SizeU(w, h);
auto hwndRenderProps = D2D1::HwndRenderTargetProperties(hwnd, size);
d2d1Factory->CreateHwndRenderTarget(renderProps, hwndRenderProps, &renderTarget);
简单介绍一下这段代码(以后我还会写文章详细介绍)
D2D1CreateFactory 负责创建一个 ID2D1Factory 对象,这里我们使用了单线程模型:D2D1_FACTORY_TYPE_SINGLE_THREADED,假设你需要在多个不同线程中访问 Direct2D 资源,那么就要使用多线程模型:D2D1_FACTORY_TYPE_MULTI_THREADED。
接着创建了一个像素格式对象:D2D1_PIXEL_FORMAT pixelFormat,像素格式为:DXGI_FORMAT_B8G8R8A8_UNORM,一个像素占据 32 位空间,蓝、绿、红、透明度依次分别占8位空间。D2D1_ALPHA_MODE_PREMULTIPLIED 代表着透明度值预先与蓝绿红颜色值相乘,这主要是为了提高渲染半透明像素的效率。
接着创建:D2D1_RENDER_TARGET_PROPERTIES renderProps 对象,这个对象存储着渲染目标的属性,D2D1_RENDER_TARGET_TYPE_DEFAULT 参数指定使用硬件加速(GPU渲染),如果硬件加速不可用,则呈现目标使用软件渲染。(Direct2D是支持软渲染的哦,没显卡的机器上也可以用)
之后创建:D2D1_HWND_RENDER_TARGET_PROPERTIES hwndRenderProps 对象,这个对象决定我们将在哪个窗口上绘图。
最后创建了 ID2D1HwndRenderTarget renderTarget 对象,我们将使用这个对象在窗口中绘图。
下面是绘制图像的示例代码:
if (uMsg == WM_PAINT)
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hWnd, &ps);
renderTarget->BeginDraw();
renderTarget->Clear(D2D1::ColorF(0.2f, 0.3f, 0.5f, 0.5f));
renderTarget->EndDraw();
EndPaint(hWnd, &ps);
return 0;
}
如你所见,与 Layered 样式的窗口不同,这个窗口是可以接收到 WM_PAINT 消息的,也就是说这个窗口可以根据 InvalidateRect 方法来更新指定的窗口区域。
代码中我们并没有做复杂的绘制操作,仅仅是用一个半透明颜色覆盖了整个画布。
最终渲染结果如下图所示:
这个方案也有缺点,如下所示:
- 只能在顶层窗口中使用,不能在子窗口中使用。
- 系统不会为它执行命中测试,即使是全透明区域,鼠标事件也不会穿透。
- 虽然使用了GPU的能力但DWM仍会为其创建重定向表面,性能比第三个方案略差。
无重定向表面的透明窗口
从 Windows 8 开始,开发者可以在创建一个窗口时请求不使用重定向表面。如下代码所示:
CreateWindowEx(WS_EX_NOREDIRECTIONBITMAP,wc.lpszClassName, L"Sample",
WS_OVERLAPPEDWINDOW | WS_VISIBLE,x, y, w, h,
nullptr, nullptr, module, nullptr);
关键就是这个窗口样式:WS_EX_NOREDIRECTIONBITMAP,通过这个样式创建的窗口没有重定向表面,DWM可以少为它做一些工作,但类似窗口位置、阴影之类的工作还得做。
窗口没有重定向表面,DWM就不知道到哪里去合成此窗口的像素数据,因此我们要自己搞一个这样的表面,并把它提交给DWM。
这个过程比较漫长,我们一点一点介绍:
ComPtr<ID3D11Device> d3dDevice;
D3D11CreateDevice(NULL,
D3D_DRIVER_TYPE_HARDWARE,
NULL,
D3D11_CREATE_DEVICE_SINGLETHREADED | D3D11_CREATE_DEVICE_BGRA_SUPPORT,
NULL,
0,
D3D11_SDK_VERSION,
&d3dDevice,
NULL,
NULL);
这段代码创建了一个ID3D11Device对象,D3D11_CREATE_DEVICE_BGRA_SUPPORT标志配置此对象与Direct2D的互操作性,最终我们使用Direct2D绘图。
ComPtr<IDXGIDevice> dxgiDevice;
d3dDevice.As(&dxgiDevice);
ComPtr<IDXGIFactory2> dxgiFactory;
CreateDXGIFactory2(0,__uuidof(dxgiFactory),
reinterpret_cast<void**>(dxgiFactory.GetAddressOf()));
这段代码创建了IDXGIDevice和IDXGIFactory2对象,DirectX一系列对象都由DXGI管理,它提供了跨各种版本DirectX的通用GPU资源管理设施。
ComPtr<IDXGISwapChain1> swapChain;
DXGI_SWAP_CHAIN_DESC1 description = {};
description.Width = w;
description.Height = h;
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;
dxgiFactory->CreateSwapChainForComposition(dxgiDevice.Get(), &description, nullptr, swapChain.GetAddressOf());
dxgiFactory->MakeWindowAssociation(hwnd, DXGI_MWA_NO_ALT_ENTER);
这段代码创建了一个IDXGISwapChain1对象(交换链),这个对象管理着窗口待渲染的像素数据的大小和格式,DXGI_USAGE_RENDER_TARGET_OUTPUT将屏幕呈现目标的输出内存定向到交换链对象管理的像素内存(GPU里的内存)。
DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL指定所有像素内存都与合成引擎共享。合成引擎可以直接从交换链的缓冲区组合桌面,而无需额外的复制。BufferCount是缓冲区数量,这里设置的是双缓冲,避免更新画面时出现闪烁的问题。SampleDesc.Count设置为1,禁止多重采样。MakeWindowAssociation方法用于改变窗口大小时提升渲染效率。
ComPtr<IDXGISurface2> dxgiSurface;
swapChain->GetBuffer(0, __uuidof(dxgiSurface),
reinterpret_cast<void**>(dxgiSurface.GetAddressOf()));
这段代码创建了一个IDXGISurface2对象,这就是我们前面说的,要自己创建的表面。这个表面是基于交换链双缓冲区的索引为0的缓冲区构建的。
D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, d2dFactory.GetAddressOf());
D2D1_RENDER_TARGET_PROPERTIES targetProperties = {};
targetProperties.type = D2D1_RENDER_TARGET_TYPE_DEFAULT;
targetProperties.pixelFormat.format = DXGI_FORMAT_UNKNOWN;
targetProperties.pixelFormat.alphaMode = D2D1_ALPHA_MODE_PREMULTIPLIED;
d2dFactory->CreateDxgiSurfaceRenderTarget(dxgiSurface.Get(),
&targetProperties, &renderTarget);
这段代码创建了ID2D1Factory2对象和ID2D1RenderTarget对象,renderTarget对象可以创建并执行绘图指令。绘图指令将在dxgiSurface表面上执行。
ComPtr<IDCompositionDevice> compositionDevice;
ComPtr<IDCompositionTarget> compositionTarget;
ComPtr<IDCompositionVisual> compositionVisual;
DCompositionCreateDevice(dxgiDevice.Get(), IID_PPV_ARGS(&compositionDevice));
compositionDevice->CreateTargetForHwnd(hwnd, true, &compositionTarget);
compositionDevice->CreateVisual(&compositionVisual);
compositionVisual->SetContent(swapChain.Get());
compositionTarget->SetRoot(compositionVisual.Get());
compositionDevice->Commit();
这段代码创建与图像合成相关的一系列对象,这些合成对象把交换链、窗口关联到一起。
renderTarget->BeginDraw();
renderTarget->Clear(D2D1::ColorF(0.6f, 0.6f, 0.2f, 0.5f));
renderTarget->EndDraw();
swapChain->Present(1, 0);
这段代码用于在绘制表面绘制半透明颜色,绘制完成后,使用交换链提交给窗口。
当窗口大小变换时,需要对renderTarget、swapChain等几个对象进行重置
renderTarget.Reset();
swapChain->ResizeBuffers(0, w, h, DXGI_FORMAT_UNKNOWN, 0);
ComPtr<IDXGISurface2> dxgiSurface;
swapChain->GetBuffer(0, IID_PPV_ARGS(&dxgiSurface));
D2D1_RENDER_TARGET_PROPERTIES targetProperties = {};
targetProperties.type = D2D1_RENDER_TARGET_TYPE_DEFAULT;
targetProperties.pixelFormat.format = DXGI_FORMAT_UNKNOWN;
targetProperties.pixelFormat.alphaMode = D2D1_ALPHA_MODE_PREMULTIPLIED;
d2dFactory->CreateDxgiSurfaceRenderTarget(dxgiSurface.Get(), &targetProperties, &renderTarget);
swapChain->ResizeBuffers方法负责重置交换链管理的内存的大小(几个0参数的意义是不改变原有配置)。
交换链管理的内存变化了,就要重新创建renderTarget对象。
程序运行结果如下图所示:
这个方案的缺点如下:
- 系统不会为它执行命中测试,即使是全透明区域,鼠标事件也不会穿透。
- 实现相对复杂,有一定的门槛
尽管如此,这个方案也无疑是三个方案中最好的,WPF/WinUI等框架的底层实现都是第三个方案。在以后这个系列的文章中,我们也将使用此方案为大家演示Direct2D的作用。
代码下载
相信我,本文里的代码绝对都是关键代码。
如果看文章仍旧无法满足你的需求,可以站内私信我获取源码下载地址。
代码我都用 CMake 配置好了,一般情况下你用 Visual Studio 打开就能用。
代码就是为读者学习准备的,非常精练,没有什么乱七八糟的东西干扰你的学习。
这个私有项目还会不断增加新的示例,有BUG我也会修复。