WPF DWM 边框入门:从空项目实现彩色窗口边框
做设备控制、参数编辑或日志查看界面时,一圈清晰的窗口边框有助于区分窗口边界。Windows 11 提供了原生边框颜色属性,普通 WPF 窗口也可以使用。
这篇从一个空白项目开始,完成两个效果:窗口显示时自动设置蓝色边框,点击按钮后切换为紫色。标题栏、最小化、最大化和关闭按钮继续使用系统默认实现。
这里做的是窗口外缘的彩色细线,宽度和外观由系统绘制。如果需要外扩模糊光晕,就要另外实现绘制与窗口同步。

1. 准备环境并创建项目
本文使用 Windows 11、Visual Studio 2026 和 .NET 10。Visual Studio Installer 中勾选“.NET 桌面开发”工作负载及 .NET 10 组件。创建项目的步骤与 Microsoft 的 WPF 入门教程一致。
- 打开 Visual Studio,选择“创建新项目”。
- 搜索 WPF,选择 C# 的“WPF 应用程序”模板。确认模板名称没有“.NET Framework”。
- 项目名填写
DwmBorderDemo,后面的命名空间都使用这个名称。 - 框架选择 .NET 10.0,然后创建项目。
- 按 F5,确认能看到默认的空白窗口,再关闭窗口回到编辑器。
也可以在 Windows PowerShell 中创建项目:
dotnet new wpf -n DwmBorderDemo -f net10.0
cd DwmBorderDemo
dotnet run
本文不需要安装 NuGet 包。dwmapi.dll 由 Windows 提供,WPF 也不需要接入 Windows App SDK 来完成这一步。
DWMWA_BORDER_COLOR 从 Windows 11 Build 22000 开始支持。Windows 10 即使能够编译项目,也不能使用这个属性实现相同的原生着色效果。DWMWINDOWATTRIBUTE 文档
2. 核对项目文件和启动入口
在解决方案资源管理器中,右键项目选择“编辑项目文件”。将 DwmBorderDemo.csproj 替换为:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net10.0-windows</TargetFramework>
<UseWPF>true</UseWPF>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>
net10.0-windows 指定 .NET 10 和 Windows 目标;UseWPF 启用 WPF。这里不需要 AllowUnsafeBlocks。
打开 App.xaml,确认启动窗口指向 MainWindow.xaml。完整内容如下:
<Application x:Class="DwmBorderDemo.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="MainWindow.xaml">
<Application.Resources />
</Application>
App.xaml.cs 保持为:
using System.Windows;
namespace DwmBorderDemo;
public partial class App : Application
{
}
如果模板生成的 MainWindow.xaml 或命名空间不同,将两个窗口文件替换为以下内容,先保留一个最小窗口。
MainWindow.xaml:
<Window x:Class="DwmBorderDemo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="DwmBorderDemo" Width="780" Height="460">
<Grid />
</Window>
MainWindow.xaml.cs:
using System.Windows;
namespace DwmBorderDemo;
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}
按 F5,再确认一次空白窗口能够正常启动。此时还没有任何 DWM 调用。
3. 添加原生函数声明
在项目上右键,选择“添加 → 类”,文件名填写 NativeMethods.cs,将整个文件替换为:
using System.Runtime.InteropServices;
namespace DwmBorderDemo;
internal static class NativeMethods
{
internal const int DwmwaBorderColor = 34;
[DllImport("dwmapi.dll", ExactSpelling = true)]
internal static extern int DwmSetWindowAttribute(
nint hwnd,
int attribute,
ref uint value,
int valueSize);
}
P/Invoke 可以让 C# 调用原生 DLL 中的函数。DllImport 指明函数来自 dwmapi.dll,extern 表示函数实现位于外部,而不是 C# 方法体中。
四个参数分别是:
| 参数 | 本文传入的内容 |
|---|---|
hwnd |
要修改的窗口句柄 |
attribute |
属性编号 34,即边框颜色 |
value |
一个 uint 类型的 COLORREF 颜色值,通过 ref 传入地址 |
valueSize |
颜色值占用的字节数,这里是 4 |
返回类型 int 用来接收 HRESULT。失败结果为负数;调用成功通常返回 0,即 S_OK。DwmSetWindowAttribute 文档
此时按 Ctrl+Shift+B 构建项目。应该仍然编译成功,运行效果也仍是空白窗口:声明原生函数不会主动执行它。
4. 先让窗口显示蓝色边框
先完成自动设置蓝色,不急着加按钮。将 MainWindow.xaml 整个替换为:
<Window x:Class="DwmBorderDemo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="WPF DWM 边框入门"
Width="780" Height="460"
MinWidth="660" MinHeight="400"
WindowStartupLocation="CenterScreen"
WindowStyle="SingleBorderWindow"
ResizeMode="CanResize"
AllowsTransparency="False"
Background="#202124" Foreground="#F1F3F5"
FontFamily="Segoe UI, Microsoft YaHei UI" FontSize="14">
<StackPanel Margin="28">
<TextBlock Text="给标准 WPF 窗口设置彩色边框"
FontSize="25" FontWeight="SemiBold" />
<TextBlock Text="窗口创建完成后,自动应用蓝色边框。"
Margin="0,12,0,22" Foreground="#B8C0CC" />
<Border Background="#2D3138" Padding="18">
<StackPanel>
<TextBlock x:Name="ColorText" Text="等待设置边框颜色"
FontFamily="Consolas" TextWrapping="Wrap" />
<TextBlock x:Name="StatusText" Text="等待窗口句柄创建"
Margin="0,12,0,0" TextWrapping="Wrap" />
</StackPanel>
</Border>
<TextBlock Text="彩色细线位于窗口外缘,标题栏和右上角按钮仍由 Windows 提供。"
Margin="0,22,0,0" Foreground="#B8C0CC" TextWrapping="Wrap" />
</StackPanel>
</Window>
x:Name="ColorText" 和 x:Name="StatusText" 让后台代码能访问这两个文本控件。窗口保留 SingleBorderWindow、CanResize 和非透明配置。
再将 MainWindow.xaml.cs 整个替换为:
using System;
using System.Windows;
using System.Windows.Interop;
using System.Windows.Media;
namespace DwmBorderDemo;
public partial class MainWindow : Window
{
private nint hwnd;
public MainWindow()
{
InitializeComponent();
SourceInitialized += MainWindow_SourceInitialized;
}
private void MainWindow_SourceInitialized(object? sender, EventArgs e)
{
hwnd = new WindowInteropHelper(this).Handle;
ApplyBorderColor(Color.FromRgb(0, 122, 204));
}
private void ApplyBorderColor(Color color)
{
if (hwnd == 0)
{
StatusText.Text = "窗口句柄尚未创建,请等待窗口显示后再操作。";
return;
}
if (!OperatingSystem.IsWindowsVersionAtLeast(10, 0, 22000))
{
StatusText.Text = "原生边框着色需要 Windows 11 Build 22000 或更高版本。";
return;
}
uint colorRef = color.R |
((uint)color.G << 8) |
((uint)color.B << 16);
int hr = NativeMethods.DwmSetWindowAttribute(
hwnd,
NativeMethods.DwmwaBorderColor,
ref colorRef,
sizeof(uint));
if (hr < 0)
{
StatusText.Text = $"设置失败:HRESULT=0x{hr:X8}";
return;
}
ColorText.Text =
$"RGB({color.R}, {color.G}, {color.B}) | 请求 COLORREF=0x{colorRef:X8}";
StatusText.Text = $"DWM 设置成功:HRESULT=0x{hr:X8}";
}
}
现在按 F5。应看到窗口外缘的蓝色细线,以及下面两项信息:
RGB(0, 122, 204) | 请求 COLORREF=0x00CC7A00
DWM 设置成功:HRESULT=0x00000000
如果显示成功,但边框不明显,先观察窗口最外缘,而不是内容区域中的深色信息框。原生边框很细,可以放大截图检查。
为什么等到 SourceInitialized
HWND 是 Windows 用于标识原生窗口的句柄。InitializeComponent() 创建 WPF 控件,并不代表原生窗口句柄已经准备好。
SourceInitialized 在原生窗口源初始化后触发,此时使用 WindowInteropHelper(this).Handle 获得句柄,再调用 DWM。本文不在窗口构造函数中直接提交原生属性。
为什么颜色需要转换
WPF 颜色包含 A、R、G、B,DWM 的 COLORREF 格式为 0x00BBGGRR。COLORREF 文档
以蓝色 RGB(0, 122, 204) 为例:
R = 0 → 0x00000000
G = 122 → 0x00007A00
B = 204 → 0x00CC0000
按位或 → 0x00CC7A00
<< 8 将绿色移到对应字节,<< 16 将蓝色移到对应字节,| 合并它们。Alpha 没有参与转换,因此这个属性不能通过透明度设置半透明边框。
为什么检查 hr < 0
HRESULT 表达原生调用结果。程序只在成功后更新颜色信息;失败时显示原始十六进制结果,方便排查。界面里的“请求 COLORREF”来自提交参数,不是从 DWM 读取的颜色值。
5. 加入蓝色和紫色切换按钮
自动蓝色已经运行成功后,再加入交互。将 MainWindow.xaml 替换为下面的完整内容:
<Window x:Class="DwmBorderDemo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="WPF DWM 边框入门"
Width="780" Height="460"
MinWidth="660" MinHeight="400"
WindowStartupLocation="CenterScreen"
WindowStyle="SingleBorderWindow"
ResizeMode="CanResize"
AllowsTransparency="False"
Background="#202124" Foreground="#F1F3F5"
FontFamily="Segoe UI, Microsoft YaHei UI" FontSize="14">
<StackPanel Margin="28">
<TextBlock Text="给标准 WPF 窗口设置彩色边框"
FontSize="25" FontWeight="SemiBold" />
<TextBlock Text="点击下方按钮,在蓝色与紫色之间切换。"
Margin="0,12,0,22" Foreground="#B8C0CC" />
<StackPanel Orientation="Horizontal" Margin="0,0,0,22">
<Button Content="蓝色边框" Width="140" Height="40"
Margin="0,0,12,0" Click="BlueButton_Click" />
<Button Content="紫色边框" Width="140" Height="40"
Click="PurpleButton_Click" />
</StackPanel>
<Border Background="#2D3138" Padding="18">
<StackPanel>
<TextBlock x:Name="ColorText" Text="等待设置边框颜色"
FontFamily="Consolas" TextWrapping="Wrap" />
<TextBlock x:Name="StatusText" Text="等待窗口句柄创建"
Margin="0,12,0,0" TextWrapping="Wrap" />
</StackPanel>
</Border>
<TextBlock Text="彩色细线位于窗口外缘,标题栏和右上角按钮仍由 Windows 提供。"
Margin="0,22,0,0" Foreground="#B8C0CC" TextWrapping="Wrap" />
</StackPanel>
</Window>
将 MainWindow.xaml.cs 替换为下面的完整内容:
using System;
using System.Windows;
using System.Windows.Interop;
using System.Windows.Media;
namespace DwmBorderDemo;
public partial class MainWindow : Window
{
private nint hwnd;
public MainWindow()
{
InitializeComponent();
SourceInitialized += MainWindow_SourceInitialized;
}
private void MainWindow_SourceInitialized(object? sender, EventArgs e)
{
hwnd = new WindowInteropHelper(this).Handle;
ApplyBorderColor(Color.FromRgb(0, 122, 204));
}
private void BlueButton_Click(object sender, RoutedEventArgs e)
{
ApplyBorderColor(Color.FromRgb(0, 122, 204));
}
private void PurpleButton_Click(object sender, RoutedEventArgs e)
{
ApplyBorderColor(Color.FromRgb(155, 114, 207));
}
private void ApplyBorderColor(Color color)
{
if (hwnd == 0)
{
StatusText.Text = "窗口句柄尚未创建,请等待窗口显示后再操作。";
return;
}
if (!OperatingSystem.IsWindowsVersionAtLeast(10, 0, 22000))
{
StatusText.Text = "原生边框着色需要 Windows 11 Build 22000 或更高版本。";
return;
}
uint colorRef = color.R |
((uint)color.G << 8) |
((uint)color.B << 16);
int hr = NativeMethods.DwmSetWindowAttribute(
hwnd,
NativeMethods.DwmwaBorderColor,
ref colorRef,
sizeof(uint));
if (hr < 0)
{
StatusText.Text = $"设置失败:HRESULT=0x{hr:X8}";
return;
}
ColorText.Text =
$"RGB({color.R}, {color.G}, {color.B}) | 请求 COLORREF=0x{colorRef:X8}";
StatusText.Text = $"DWM 设置成功:HRESULT=0x{hr:X8}";
}
}
Click="BlueButton_Click" 与后台同名方法对应。点击事件将颜色交给 ApplyBorderColor,其他原生调用流程不变。这里直接使用代码后台处理两个按钮,后续接入业务项目时可以再整理命令或主题配置。
按 F5,先点击“紫色边框”:
RGB(155, 114, 207) | 请求 COLORREF=0x00CF729B
DWM 设置成功:HRESULT=0x00000000

再点击“蓝色边框”,边框与请求值应回到蓝色。反复切换几次,确认没有创建第二个窗口,也没有改变右上角系统按钮。
6. 对照验收与常见问题
| 操作 | 预期结果 |
|---|---|
| 启动程序 | 自动设置蓝色,HRESULT 为 0 |
| 点击紫色按钮 | 请求值变为 0x00CF729B,外缘呈紫色 |
| 点击蓝色按钮 | 请求值回到 0x00CC7A00,外缘呈蓝色 |
| 拖动系统标题栏 | 窗口正常移动 |
| 从窗口边缘缩放 | 窗口尺寸正常变化 |
| 点击系统关闭按钮 | 程序退出 |
本篇在 Windows 11 ARM64、200% 缩放环境中完成了构建、启动及蓝紫色切换验证,截图来自本篇这个独立项目。其他设备上的缩放、系统主题和渲染表现,应按上表继续核对。
找不到 WPF 模板或 .NET 10
打开 Visual Studio Installer,检查“.NET 桌面开发”工作负载和 .NET 10 组件。命令行输入 dotnet --list-sdks,确认存在 10.x SDK;通过命令行运行还需要对应的 .NET Desktop Runtime。
InitializeComponent 或 ColorText 不存在
先检查 MainWindow.xaml 的 x:Class 是否为 DwmBorderDemo.MainWindow,后台命名空间是否为 DwmBorderDemo,类名是否为 MainWindow 并带有 partial。再检查 XAML 中两个 x:Name 是否完整,窗口文件是否按同一阶段一起替换。
找不到 NativeMethods 或按钮事件
确认 NativeMethods.cs 已添加到项目中且命名空间一致。按钮 Click 方法存在于最终版 MainWindow.xaml.cs 中;不要把最终 XAML 与蓝色阶段的后台代码混用。
颜色对不上
蓝色请求值应为 0x00CC7A00。如果数值不同,检查是否按 R | (G << 8) | (B << 16) 转换,避免直接传入 ARGB 或把红蓝顺序写反。
显示“不支持”或 HRESULT 失败
“不支持”来自版本检查,先用 winver 核对系统。HRESULT 失败则检查窗口句柄获取时机、属性编号是否为 34、传入值是否为 uint、数据大小是否为 4。记录实际错误码,不把失败归结为“边框颜色没设置”。
最大化和失去焦点后,边框还没有跟随变化
本篇只在窗口创建和按钮点击时更新颜色。下一篇增加激活、失焦、最大化、还原事件,并在系统不支持或调用失败时回退到窗口内部描边。
7. 本篇完整文件清单
DwmBorderDemo/
├─ DwmBorderDemo.csproj
├─ App.xaml
├─ App.xaml.cs
├─ MainWindow.xaml
├─ MainWindow.xaml.cs
└─ NativeMethods.cs
正文已经列出六个文件的完整内容。配套源码中的 DwmBorderDemo 是最终可运行项目;stage-empty、stage-blue 只保存各阶段需要替换的两个窗口文件,不能单独作为项目启动。
在最终项目目录执行:
dotnet build
dotnet run
完成这一篇后,窗口已经能通过原生 API 设置和切换边框颜色。下一篇沿用这个项目,为它补上窗口状态管理。

从空白 WPF 项目开始,使用 DwmSetWindowAttribute 设置 Windows 11 原生彩色窗口边框,实现蓝色与紫色切换。包含完整 XAML、C# 代码、COLORREF 转换说明及常见问题排查,保留系统标题栏和窗口按钮。
浙公网安备 33010602011771号