WPF DWM 边框入门:从空项目实现彩色窗口边框

做设备控制、参数编辑或日志查看界面时,一圈清晰的窗口边框有助于区分窗口边界。Windows 11 提供了原生边框颜色属性,普通 WPF 窗口也可以使用。

这篇从一个空白项目开始,完成两个效果:窗口显示时自动设置蓝色边框,点击按钮后切换为紫色。标题栏、最小化、最大化和关闭按钮继续使用系统默认实现。

这里做的是窗口外缘的彩色细线,宽度和外观由系统绘制。如果需要外扩模糊光晕,就要另外实现绘制与窗口同步。
image

1. 准备环境并创建项目

本文使用 Windows 11、Visual Studio 2026 和 .NET 10。Visual Studio Installer 中勾选“.NET 桌面开发”工作负载及 .NET 10 组件。创建项目的步骤与 Microsoft 的 WPF 入门教程一致。

  1. 打开 Visual Studio,选择“创建新项目”。
  2. 搜索 WPF,选择 C# 的“WPF 应用程序”模板。确认模板名称没有“.NET Framework”。
  3. 项目名填写 DwmBorderDemo,后面的命名空间都使用这个名称。
  4. 框架选择 .NET 10.0,然后创建项目。
  5. 按 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

image
再点击“蓝色边框”,边框与请求值应回到蓝色。反复切换几次,确认没有创建第二个窗口,也没有改变右上角系统按钮。

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 设置和切换边框颜色。下一篇沿用这个项目,为它补上窗口状态管理。

参考资料

posted @ 2026-10-07 17:56  JacePan  阅读(3)  评论(0)    收藏  举报