1. 使用DirectComposition实现透明窗口的技术解析
在Windows桌面应用开发中,实现透明窗口效果一直是个有趣且实用的技术挑战。本文将深入探讨如何利用DirectComposition技术结合WS_EX_NOREDIRECTIONBITMAP窗口样式,创建高性能的透明窗口解决方案。
1.1 技术选型背景
传统的透明窗口实现通常采用WS_EX_LAYERED分层窗口方式,这种方式虽然简单易用,但存在明显的性能瓶颈。当窗口内容需要频繁更新时,CPU与GPU之间的数据传输会成为性能瓶颈。
DirectComposition是Windows 8.1引入的桌面窗口管理器(DWM)合成技术,它允许应用直接将GPU渲染的内容交给DWM进行合成,避免了传统分层窗口的额外内存拷贝开销。结合WS_EX_NOREDIRECTIONBITMAP窗口样式,可以实现更高效的透明窗口效果。
技术提示:WS_EX_NOREDIRECTIONBITMAP样式告诉系统不需要为窗口创建传统的重定向表面(redirection surface),而是直接使用应用提供的视觉内容进行合成。
1.2 环境准备与项目配置
首先需要配置项目依赖,我们将使用Vortice库来简化DirectX和DirectComposition的调用。以下是项目文件(csproj)的关键配置:
xml复制<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<IsAotCompatible>true</IsAotCompatible>
<PublishAot>true</PublishAot>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Vortice.Direct2D1" Version="3.8.2" />
<PackageReference Include="Vortice.Direct3D11" Version="3.8.2" />
<PackageReference Include="Vortice.DirectComposition" Version="3.8.2" />
<PackageReference Include="Vortice.DXGI" Version="3.8.2" />
<PackageReference Include="Vortice.Win32" Version="2.3.0" />
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.257">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>
为了简化Win32 API调用,我们使用CsWin32生成必要的P/Invoke签名。在NativeMethods.txt中添加以下API声明:
code复制EnumDisplayMonitors
GetMonitorInfo
MONITORINFOEXW
EnumDisplaySettings
GetDisplayConfigBufferSizes
QueryDisplayConfig
DisplayConfigGetDeviceInfo
DISPLAYCONFIG_SOURCE_DEVICE_NAME
DISPLAYCONFIG_TARGET_DEVICE_NAME
RegisterClassEx
GetModuleHandle
LoadCursor
IDC_ARROW
WndProc
CreateWindowEx
CW_USEDEFAULT
ShowWindow
SW_SHOW
GetMessage
TranslateMessage
DispatchMessage
DefWindowProc
GetClientRect
WM
WM_PAINT
GetWindowLong
SetWindowLong
DwmIsCompositionEnabled
UpdateLayeredWindow
DwmExtendFrameIntoClientArea
DCompositionCreateDevice
2. 窗口创建与初始化
2.1 窗口创建关键步骤
创建透明窗口的核心在于正确的窗口样式配置。以下是关键代码片段:
csharp复制private unsafe HWND CreateWindow()
{
DwmIsCompositionEnabled(out var compositionEnabled);
if (!compositionEnabled)
{
Console.WriteLine("无法启用透明窗口效果");
}
// 关键窗口样式设置
WINDOW_EX_STYLE exStyle = WINDOW_EX_STYLE.WS_EX_NOREDIRECTIONBITMAP;
var style = WNDCLASS_STYLES.CS_OWNDC | WNDCLASS_STYLES.CS_HREDRAW | WNDCLASS_STYLES.CS_VREDRAW;
var defaultCursor = LoadCursor(new HINSTANCE(IntPtr.Zero), new PCWSTR(IDC_ARROW.Value));
var className = $"lindexi-{Guid.NewGuid().ToString()}";
var title = "The Title";
_myInstanceWndProc = new WNDPROC(WndProc); // 防止GC回收
fixed (char* pClassName = className)
fixed (char* pTitle = title)
{
var wndClassEx = new WNDCLASSEXW
{
cbSize = (uint)Marshal.SizeOf<WNDCLASSEXW>(),
style = style,
lpfnWndProc = _myInstanceWndProc,
hInstance = new HINSTANCE(GetModuleHandle(null).DangerousGetHandle()),
hCursor = defaultCursor,
hbrBackground = new HBRUSH(IntPtr.Zero),
lpszClassName = new PCWSTR(pClassName)
};
ushort atom = RegisterClassEx(in wndClassEx);
var dwStyle = WINDOW_STYLE.WS_OVERLAPPEDWINDOW | WINDOW_STYLE.WS_VISIBLE;
return CreateWindowEx(
exStyle,
new PCWSTR((char*)atom),
new PCWSTR(pTitle),
dwStyle,
0, 0, 1900, 1000,
HWND.Null, HMENU.Null, HINSTANCE.Null, null);
}
}
2.2 窗口消息处理
窗口消息处理中需要特别注意WM_NCCALCSIZE消息的处理,这决定了窗口的非客户区(如标题栏)是否显示:
csharp复制private LRESULT WndProc(HWND hwnd, uint message, WPARAM wParam, LPARAM lParam)
{
switch ((WindowsMessage)message)
{
case WindowsMessage.WM_NCCALCSIZE:
return new LRESULT(0); // 声明整个区域为客户区
case WindowsMessage.WM_SIZE:
_renderManager?.ReSize();
break;
}
return DefWindowProc(hwnd, message, wParam, lParam);
}
开发经验:如果希望保留标题栏等非客户区,可以移除WM_NCCALCSIZE消息的处理。但要注意这会限制透明效果的应用范围。
3. DirectComposition核心实现
3.1 渲染管理器初始化
我们创建一个独立的RenderManager类来处理渲染逻辑,使用单独的渲染线程避免阻塞UI线程:
csharp复制unsafe class RenderManager(HWND hwnd)
{
public HWND HWND => hwnd;
private readonly Format _colorFormat = Format.B8G8R8A8_UNorm;
private bool _isReSize;
public void StartRenderThread()
{
var thread = new Thread(() => { RenderCore(); })
{
IsBackground = true,
Name = "Render",
Priority = ThreadPriority.Highest
};
thread.Start();
}
public void ReSize() => _isReSize = true;
private void RenderCore()
{
// 渲染核心逻辑
}
}
3.2 DirectComposition设备创建
初始化DirectComposition设备是透明窗口实现的关键:
csharp复制private void InitializeDirectComposition()
{
// 获取窗口客户区尺寸
RECT windowRect;
GetClientRect(HWND, &windowRect);
var clientSize = new SizeI(windowRect.right - windowRect.left,
windowRect.bottom - windowRect.top);
// 创建DXGI工厂和适配器
var dxgiFactory2 = DXGI.CreateDXGIFactory1<IDXGIFactory2>();
IDXGIAdapter1? hardwareAdapter = GetHardwareAdapter(dxgiFactory2).FirstOrDefault();
if (hardwareAdapter == null) throw new InvalidOperationException("Cannot detect D3D11 adapter");
// 创建D3D11设备
FeatureLevel[] featureLevels = {
FeatureLevel.Level_11_1, FeatureLevel.Level_11_0,
FeatureLevel.Level_10_1, FeatureLevel.Level_10_0
};
var result = D3D11.D3D11CreateDevice(
hardwareAdapter,
DriverType.Unknown,
DeviceCreationFlags.BgraSupport,
featureLevels,
out ID3D11Device d3D11Device,
out FeatureLevel featureLevel,
out ID3D11DeviceContext d3D11DeviceContext
);
// 升级到D3D11.1接口
ID3D11Device1 d3D11Device1 = d3D11Device.QueryInterface<ID3D11Device1>();
var d3D11DeviceContext1 = d3D11DeviceContext.QueryInterface<ID3D11DeviceContext1>();
// 配置交换链
const int FrameCount = 2;
SwapChainDescription1 swapChainDescription = new()
{
Width = (uint)clientSize.Width,
Height = (uint)clientSize.Height,
Format = _colorFormat,
BufferCount = FrameCount,
BufferUsage = Usage.RenderTargetOutput,
SampleDescription = SampleDescription.Default,
Scaling = Scaling.Stretch,
SwapEffect = SwapEffect.FlipSequential,
AlphaMode = AlphaMode.Premultiplied, // 关键:预乘Alpha
Flags = SwapChainFlags.None,
};
// 创建支持合成的交换链
var swapChain = dxgiFactory2.CreateSwapChainForComposition(d3D11Device1, swapChainDescription);
// 创建DirectComposition设备
IDXGIDevice dxgiDevice = d3D11Device1.QueryInterface<IDXGIDevice>();
IDCompositionDevice compositionDevice = DComp.DCompositionCreateDevice<IDCompositionDevice>(dxgiDevice);
// 创建目标并绑定到窗口
compositionDevice.CreateTargetForHwnd(HWND, true, out IDCompositionTarget compositionTarget);
// 创建视觉对象并设置内容
IDCompositionVisual compositionVisual = compositionDevice.CreateVisual();
compositionVisual.SetContent(swapChain);
compositionTarget.SetRoot(compositionVisual);
compositionDevice.Commit();
}
3.3 交换链配置要点
交换链配置中有几个关键参数直接影响透明效果:
- AlphaMode:必须设置为Premultiplied才能正确支持透明
- SwapEffect:推荐使用FlipSequential以获得最佳性能
- Scaling:必须设置为Stretch以适应窗口大小变化
技术细节:预乘Alpha(AlphaMode.Premultiplied)意味着颜色值(RGB)已经乘以了Alpha通道的值。这种格式在合成时效率更高,因为不需要在合成时再次进行乘法运算。
4. 渲染循环与性能优化
4.1 基本渲染循环实现
以下是渲染循环的核心实现:
csharp复制private void RenderCore()
{
InitializeDirectComposition();
while (!_isDisposed)
{
if (_isReSize)
{
HandleResize();
_isReSize = false;
}
// 渲染逻辑
RenderFrame();
// 呈现
_renderContext.SwapChain.Present(1, PresentFlags.None);
_renderContext.D3D11DeviceContext1.Flush();
}
}
private void RenderFrame()
{
// 获取交换链的后缓冲区
var d3D11Texture2D = _renderContext.SwapChain.GetBuffer<ID3D11Texture2D>(0);
var dxgiSurface = d3D11Texture2D.QueryInterface<IDXGISurface>();
// 创建D2D渲染目标
var renderTargetProperties = new D2D.RenderTargetProperties()
{
PixelFormat = new PixelFormat(D2DColorFormat, Vortice.DCommon.AlphaMode.Premultiplied),
Type = D2D.RenderTargetType.Hardware,
};
using var d2D1RenderTarget = _d2DFactory.CreateDxgiSurfaceRenderTarget(dxgiSurface, renderTargetProperties);
// 开始绘制
d2D1RenderTarget.BeginDraw();
// 绘制透明背景
var color = new Color4(Random.Shared.NextSingle(),
Random.Shared.NextSingle(),
Random.Shared.NextSingle(),
0.1f); // Alpha值为0.1实现透明效果
d2D1RenderTarget.Clear(color);
// 结束绘制
d2D1RenderTarget.EndDraw();
}
4.2 窗口大小变化处理
当窗口大小变化时,需要调整交换链缓冲区大小:
csharp复制private void HandleResize()
{
GetClientRect(HWND, out var pClientRect);
var clientSize = new SizeI(pClientRect.right - pClientRect.left,
pClientRect.bottom - pClientRect.top);
_renderContext.SwapChain.ResizeBuffers(
2,
(uint)clientSize.Width,
(uint)clientSize.Height,
_colorFormat,
SwapChainFlags.None
);
}
性能提示:窗口大小变化是相对耗时的操作,应尽量减少触发频率。可以在WM_SIZE消息处理中添加适当的延迟或节流逻辑。
5. 性能对比与优化建议
5.1 不同实现方式的性能对比
我们测试了多种透明窗口实现方式的性能表现(测试环境:4K分辨率,i5-12450H CPU,集成显卡):
| 实现方式 | GPU占用 | DWM占用 | 备注 |
|---|---|---|---|
| WS_EX_NOREDIRECTIONBITMAP + Premultiplied Alpha | 75-80% | 65-70% | 推荐方案 |
| WS_EX_LAYERED + Premultiplied Alpha | 77-85% | 75-80% | 传统方案 |
| WS_EX_NOREDIRECTIONBITMAP + Ignore Alpha | 5% | 0-1% | 无透明效果 |
| CreateSwapChainForHwnd传统方式 | 5% | 0-1% | 无透明效果 |
5.2 优化建议
-
按需使用透明效果:如果不需要透明效果,应将AlphaMode设置为Ignore以降低DWM合成开销。
-
合理设置更新频率:对于静态或变化不频繁的内容,可以降低渲染帧率。
-
避免全屏透明窗口:4K分辨率下全屏透明窗口会给DWM带来较大负担。
-
考虑混合方案:对不需要透明的部分使用传统渲染方式,仅对需要透明的部分使用DirectComposition。
-
资源管理:及时释放不再使用的DirectX资源,避免内存泄漏。
6. 实际应用中的问题与解决方案
6.1 常见问题排查
-
透明效果不生效
- 检查窗口样式是否包含WS_EX_NOREDIRECTIONBITMAP
- 确认交换链的AlphaMode设置为Premultiplied
- 验证DWM合成是否启用(DwmIsCompositionEnabled)
-
性能问题
- 检查是否在不需要透明效果时错误设置了Premultiplied Alpha
- 确认交换链的SwapEffect是否为FlipSequential
- 减少不必要的窗口重绘
-
窗口边框或标题栏问题
- 如果需要保留标准窗口边框,不要处理WM_NCCALCSIZE消息
- 如果需要完全自定义窗口,应处理WM_NCCALCSIZE并返回0
6.2 高级应用场景
-
部分区域透明:通过设置不同视觉对象的透明度,可以实现窗口部分区域透明效果。
-
动态透明度变化:在渲染循环中动态调整Clear使用的Color4的Alpha值,可以实现淡入淡出效果。
-
多视觉对象合成:创建多个IDCompositionVisual对象并设置不同的内容和变换,可以实现复杂的UI效果。
7. 完整代码获取与参考
本文的完整实现代码可通过以下命令获取:
bash复制git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin 369de6b65c4122cec6a6c9ffbcc0b352a419e83e
如果无法访问gitee,可以使用github镜像:
bash复制git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git
git pull origin 369de6b65c4122cec6a6c9ffbcc0b352a419e83e
获取代码后,查看DirectX/D2D/FarjairyakaBurnefuwache目录下的实现。
在实际项目中应用这种技术时,建议从简单案例开始,逐步增加复杂度。同时要注意不同Windows版本间的兼容性差异,特别是Windows 7等旧系统可能不支持某些新特性。