共计 5074 个字符,预计需要花费 13 分钟才能阅读完成。
(内容来自我于 2026 年 3 月 21 日在 LINUX DO 平台上的帖子,原帖:https://linux.do/t/1790806)
本文档用于说明在 WinUI 3 / Windows App SDK 场景下,如何把一个普通顶层窗口逐步改造成 " 无边框、透明背景、可选置顶、可选点击穿透 " 的窗口。
提醒: 使用到的部分 API 仅 Windows 11 Build 22000(21H2)及以上版本系统支持
1. 介绍
在制作一个项目的屏幕批注功能过程中,被窗口样式和层级反复折磨,查了许多内容才最终形成以下方案(当然代码是 GPT 写的,这份文档也有一部分使用 AI 完成)
相关实现代码供参考:WindBoard/WindBoard/Features/ScreenAnnotation at develop · Jerry-Z07/WindBoard
典型目标
- 不显示系统标题栏与标准边框
- 窗口背景真正透明,而不是黑底
- 在 Win11 21H2 及以上系统不残留系统圆角外轮廓或边框描边
- 可选保持置顶
- 可选切换点击穿透
WinUI 顶层窗口的 " 透明 "" 边框 "" 系统描边 " 分别来自 XAML 内容树、Win32 样式、DWM 非客户区绘制 三个层面,必须一起处理。
2. 效果示意图

3. 总体实现链路
推荐按下面顺序处理:
- 创建普通 WinUI 顶层窗口
- 通过 AppWindow / OverlappedPresenter 关闭标题栏与标准边框
- 通过 Win32 样式清理标准窗口边框相关位
- 把窗口切到 WS_EX_LAYERED
- 为窗口添加透明 SystemBackdrop
- 拦截背景擦除,避免退化成黑底
- 通过 DWM 属性关闭系统描边与圆角外轮廓
- 如有需要,再补置顶、相对层级、点击穿透
4. 第一步:关闭 WinUI 标题栏与基础边框
先通过 AppWindow 和 OverlappedPresenter 关闭标准标题栏:
AppWindow appWindow = GetAppWindow(window);
if (appWindow.Presenter is OverlappedPresenter presenter)
{
presenter.IsResizable = false;
presenter.IsMaximizable = false;
presenter.IsMinimizable = false;
presenter.SetBorderAndTitleBar(false, false);
}
appWindow.IsShownInSwitchers = false;
appWindow.MoveAndResize(bounds);5. 第二步:清理 Win32 标准窗口样式
需要进一步移除会导致系统边框残留的标准样式位:
- WS_CAPTION
- WS_THICKFRAME
- WS_MINIMIZEBOX
- WS_MAXIMIZEBOX
- WS_SYSMENU
推荐做法
const int GWL_STYLE = -16;
const uint BorderlessMask =
0x00C00000 | // WS_CAPTION
0x00040000 | // WS_THICKFRAME
0x00020000 | // WS_MINIMIZEBOX
0x00010000 | // WS_MAXIMIZEBOX
0x00080000; // WS_SYSMENU
uint newStyle = currentStyle & ~BorderlessMask;更新样式后,必须再触发一次窗口样式刷新:
SetWindowPos(
hwnd,
IntPtr.Zero,
0, 0, 0, 0,
SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED | SWP_SHOWWINDOW);说明:
- 这是去除 " 普通系统边框 " 的核心步骤
- 如果缺少 SWP_FRAMECHANGED,样式修改有时不会立刻反映到窗口外观
6. 第三步:启用 Layered Window
透明顶层窗口通常需要启用扩展样式:
- WS_EX_LAYERED
- WS_EX_TOOLWINDOW
常见目的
- WS_EX_LAYERED:允许透明混合与透明属性生效
- WS_EX_TOOLWINDOW:避免作为普通主窗口参与 Alt+Tab 等切换器展示
示例
const int GWL_EXSTYLE = -20;
const uint WS_EX_TOOLWINDOW = 0x00000080;
const uint WS_EX_LAYERED = 0x00080000;
uint newExStyle = currentExStyle | WS_EX_TOOLWINDOW | WS_EX_LAYERED;设置完扩展样式后,建议再调用一次:
SetLayeredWindowAttributes(hwnd, 0, 255, LWA_ALPHA);说明:
- 即使窗口目标是 " 看起来完全透明 ",也建议显式完成这一层设置
- 这一步主要解决透明渲染路径与后续 DWM 行为的兼容性问题
7. 第四步:不要只依赖 XAML 透明,必须补透明 SystemBackdrop
此处思路来自 DevWinUI,感谢原仓库的方案提供
很多人会先写:
<Grid Background="Transparent" />这一步应该保留,但 不能只做这一层。
在 WinUI 3 顶层窗口中,仅有 XAML 透明时,窗口仍可能退化成黑底。更稳妥的做法是挂一个真正输出透明色的 SystemBackdrop。
这个透明 backdrop 的职责通常包括:
- 输出 ARGB(0, 0, 0, 0) 的透明画刷
- 配置 DWM 透明相关行为
- 在收到 WM_ERASEBKGND 时主动清空背景
核心思路如下
var backdrop = new TransparentBackdrop(hwnd);
window.SystemBackdrop = backdrop;说明: 这里的 TransparentBackdrop 是一个自定义的透明系统背景实现。
8. 第五步:处理背景擦除,避免黑底
为避免 WinUI 顶层窗体在透明场景下出现黑底,通常需要处理:
- WM_ERASEBKGND
- WM_DWMCOMPOSITIONCHANGED
推荐做法
- 为窗口挂一个轻量级消息监听
- 收到 WM_ERASEBKGND 时,用透明画刷填充客户区
- 收到 WM_DWMCOMPOSITIONCHANGED 时重新应用 DWM 透明配置
典型逻辑
if (messageId == WM_ERASEBKGND)
{ClearBackgroundWithTransparentBrush(hwnd, hdc);
handled = true;
}说明:
- 这是 " 真正透明 " 实现里非常容易漏掉的一环
- 很多黑底问题并不是 XAML 没透明,而是系统背景擦除仍在按默认路径执行
9. 第六步:通过 DWM 去掉 Win11 外轮廓与系统描边
在 Win11 上,即使已经:
- 去掉 Win32 标准边框
- 使用透明 backdrop
- 启用 layered window
仍可能残留一圈外轮廓。这个外轮廓往往来自 DWM 的窗口圆角与系统描边。
推荐继续设置
- DWMWA_WINDOW_CORNER_PREFERENCE = DWMWCP_DONOTROUND
- DWMWA_BORDER_COLOR = DWMWA_COLOR_NONE
示例
uint cornerPreference = 1; // DWMWCP_DONOTROUND
uint borderColor = 0xFFFFFFFE; // DWMWA_COLOR_NONE
DwmSetWindowAttribute(hwnd, 33, ref cornerPreference, sizeof(uint));
DwmSetWindowAttribute(hwnd, 34, ref borderColor, sizeof(uint));说明:
- 这一步主要用于去掉 Win11 的圆角轮廓和系统描边
- 某些旧系统不支持这些属性,建议做 " 尽力而为 " 处理:
- 支持则应用
- 不支持则忽略
- 不要因为该步骤失败而让窗口初始化整体失败
10. 第七步:可选的置顶与点击穿透
10.1 置顶
如果窗口需要始终浮在桌面或其它应用之上,可通过:
SetWindowPos(
hwnd,
HWND_TOPMOST,
0, 0, 0, 0,
SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_FRAMECHANGED | SWP_SHOWWINDOW);说明: 如果存在多个顶层窗口(例如批注层 + 工具栏),还需要显式维护二者相对层级。
10.2 点击穿透
若窗口在某些模式下需要把输入传给下层应用,可切换:
- WS_EX_TRANSPARENT
示例
uint exStyle = enabled
? currentExStyle | WS_EX_TRANSPARENT
: currentExStyle & ~WS_EX_TRANSPARENT;说明:
- 点击穿透不等于视觉透明
- 工具栏类窗口一般不应该永久穿透,否则用户会失去 " 切回可交互模式 " 的入口
11. 推荐的初始化顺序
推荐顺序如下:
- 获取 HWND
- 获取 AppWindow
- 通过 OverlappedPresenter 关标题栏和基础边框
- 调整窗口大小和位置
- 挂透明 SystemBackdrop
- 清理 Win32 标准样式
- 设置扩展样式为 WS_EX_TOOLWINDOW | WS_EX_LAYERED
- 调用 SetLayeredWindowAttributes
- 调用 DWM 去圆角与去描边
- 如有需要,再设置置顶与点击穿透
这个顺序的核心原因是:
- 先让 WinUI 窗口对象稳定创建
- 再处理透明背景
- 最后做原生样式与 DWM 外观收敛
12. 最小伪代码示例
下面给出一个通用示意:
void InitializeTransparentOverlayWindow(Window window, RectInt32 bounds)
{IntPtr hwnd = GetWindowHandle(window);
AppWindow appWindow = GetAppWindow(hwnd);
ConfigurePresenter(appWindow, bounds);
window.SystemBackdrop = new TransparentBackdrop(hwnd);
RemoveStandardWindowFrame(hwnd);
ApplyToolLayeredExStyle(hwnd);
SetLayeredWindowAttributes(hwnd, 0, 255, LWA_ALPHA);
TryDisableDwmRoundedCorners(hwnd);
TryDisableDwmBorder(hwnd);
SetTopMost(hwnd);
}如果还需要模式切换:
void SetPassThrough(IntPtr hwnd, bool enabled)
{UpdateTransparentExStyle(hwnd, enabled);
}13. 常见问题排查
13.1 只有 XAML 透明,但窗口还是黑底
优先检查:
- 是否添加了透明 SystemBackdrop
- 是否处理了 WM_ERASEBKGND
- 是否启用了 WS_EX_LAYERED
13.2 标题栏没了,但窗口仍然有边框
优先检查:
- 是否真正清除了 WS_CAPTION / WS_THICKFRAME
- 样式修改后是否调用了 SWP_FRAMECHANGED
13.3 Win11 下仍然有一圈外轮廓
优先检查:
- 是否设置 DWMWA_WINDOW_CORNER_PREFERENCE = DWMWCP_DONOTROUND
- 是否设置 DWMWA_BORDER_COLOR = DWMWA_COLOR_NONE
13.4 窗口透明了,但拖动或切换时闪烁
优先检查:
- WM_ERASEBKGND 是否被透明清空
- WM_DWMCOMPOSITIONCHANGED 后是否重新应用 DWM 配置
14. 验证清单
建议至少人工验证以下项目:
- 窗口启动后无标题栏、无标准系统边框
- 背景是真正透明,而不是黑底
- Win11 下没有残留外轮廓或圆角描边
- 置顶行为符合预期
- 点击穿透开启后,下层应用可接收输入
- 点击穿透关闭后,窗口恢复可交互
- 多窗口场景下,相对层级稳定
- 窗口关闭时无残留资源、无异常日志
15. 兼容性注意事项
DWMWA_BORDER_COLOR、DWMWA_WINDOW_CORNER_PREFERENCE 仅在 Windows 11 Build 22000 以上系统支持。
对旧系统建议采用 " 忽略不支持属性 " 的降级策略。
16. 参考
- DWMWINDOWATTRIBUTE(dwmapi.h)- Win32 apps | Microsoft Learn
- DevWinUI/dev/DevWinUI/Common/Backdrop at main · ghost1372/DevWinUI
现在这套方案有些复杂,如果有更好的方案欢迎交流!