在 Windows 11 的桌面应用中应用圆角

圆角是 Windows 11 几何的最显而易见的特征。 在 Windows 11 上,系统会将所有收件箱应用(包括所有 UWP 应用)和大多数其他应用的顶级窗口自动设为圆角。 但可能不会对某些 Win32 应用执行圆角操作。 本主题介绍在系统未自动执行圆角操作时,如何将 Win32 应用的主窗口设为圆角。

注意

根据设计,应用在最大化、贴靠、在虚拟机 (VM) 中运行,在 Windows 虚拟桌面 (WVD) 上运行时或作为 Microsoft Defender 应用程序防护 (WDAG) 窗口运行时不是圆角。

A screenshot of the Notepad app on Windows 11 with rounded corners.

我的应用为什么不是圆角的?

如果应用的主窗口未采用自动圆角设置,这是因为你以阻止采用圆角的方式自定义了框架。 从桌面窗口管理器 (DWM) 的角度来看,应用主要分为下面三类:

  1. 默认采用圆角设置的应用。

    这包括需要系统提供的完整框架和标题控件(最小值/最大值/关闭按钮)的应用,例如记事本。 还包括向系统提供足够信息以便系统可以正确地对它们进行圆角操作的应用,例如设置 WS_THICKFRAME 和 WS_CAPTION 窗口样式或提供 1 个像素的非工作区边框(系统可以使用该边框设置圆角)。

  2. 根据策略不采用圆角但实际可采用圆角的应用。

    此类别中的应用通常希望自定义大多数窗口框架,但仍然需要系统绘制的边框和阴影,例如 Microsoft Office。 如果应用根据策略未采用圆角,则可能由下述原因之一导致:

    • 缺少框架样式
    • 非工作区为空
    • 其他自定义,例如用于自定义阴影的其他非子窗口

    更改其中一项会破坏自动圆角设置。 尽管我们确实尝试了使用系统启发式方法为尽可能多的应用设置圆角,但存在一些无法预测的自定义组合,因此我们针对这些情况提供了手动选择加入 API。 如果你在应用中解决这些问题或调用选择加入 API(如以下部分所述),则系统可对应用的窗口执行圆角设置。 但请注意,该 API 是对系统的提示,不保证能执行圆角设置,具体取决于自定义项。

  3. 即使调用选择加入 API 也无法采用圆角的应用。

    这些应用没有框架或边框,通常具有高度自定义的 UI。 如果应用执行以下操作之一,则无法采用圆角设置:

    • 每像素 alpha 分层
    • 窗口区域

    例如,应用可能使用每像素 alpha 分层在其主窗口周围绘制透明像素,以实现自定义阴影效果,这会使窗口不再是矩形,因此系统无法对其采用圆角。

如何选择使用圆角

如果应用根据策略未采用圆角设置,你可以选择使用这些 API,让应用可选择使用圆角。 可以通过将 DWM_WINDOW_CORNER_PREFERENCE 枚举的值(如下表所示)传递给 DwmSetWindowAttribute 函数,为应用指定所需的圆角选项。

枚举值 说明
DWMWCP_DEFAULT 让系统决定是否对窗口采用圆角设置。
DWMWCP_DONOTROUND 绝不对窗口采用圆角设置。
DWMWCP_ROUND 适当时采用圆角设置。
DWMWCP_ROUNDSMALL 适当时可采用半径较小的圆角设置。

指向此枚举中相应值的指针将传递给 DwmSetWindowAttribute 的第三个参数。 对于指定要设置的属性的第二个参数,传递在 DWMWINDOWATTRIBUTE 枚举中定义的 DWMWA_WINDOW_CORNER_PREFERENCE 值。

对于 C# 应用

DwmSetWindowAttribute 是本机 Win32 API,不直接向 .NET 代码公开。 你将需要使用你的语言的 P/Invoke 实现来声明函数(在下例中提供了 C# 代码)。 系统会将所有标准的 WinForms 和 WPF 应用自动设为圆角,但如果你自定义了窗口框架或使用第三方框架,则可能需要选择加入采用圆角。 有关更多详细信息,请参阅“示例”部分。

示例

以下示例展示当应用未根据策略采用圆角时,可如何调用 DwmSetWindowAttributeDwmGetWindowAttribute 来控制应用的圆角体验 。

注意

为了简洁、清晰起见,这些示例中不包含错误处理。

示例 1 - 在 C# 中对应用的主窗口采用圆角 - WPF

此示例演示如何使用 [DllImport] 属性从 C# 调用 DwmSetWindowAttribute。 请注意,此定义特定于圆角;根据所提供的标志,DwmSetWindowAttribute 函数设计为采用不同的参数,因此这不是通用的签名。 该示例还包括 dwmapi 头文件中的相关枚举的副本。 由于 Win32 API 采用第三个参数的指针,因此请确保使用 ref 关键字,以便可以在调用该函数时传递变量的地址。 可以在 MainWindow.xaml.cs 的 MainWindow 类中执行此操作。

using System.Runtime.InteropServices;
using System.Windows.Interop;

public partial class MainWindow : Window
{
    // The enum flag for DwmSetWindowAttribute's second parameter, which tells the function what attribute to set.
    // Copied from dwmapi.h
    public enum DWMWINDOWATTRIBUTE
    {
        DWMWA_WINDOW_CORNER_PREFERENCE = 33
    }

    // The DWM_WINDOW_CORNER_PREFERENCE enum for DwmSetWindowAttribute's third parameter, which tells the function
    // what value of the enum to set.
    // Copied from dwmapi.h
    public enum DWM_WINDOW_CORNER_PREFERENCE
    {
        DWMWCP_DEFAULT      = 0,
        DWMWCP_DONOTROUND   = 1,
        DWMWCP_ROUND        = 2,
        DWMWCP_ROUNDSMALL   = 3
    }

    // Import dwmapi.dll and define DwmSetWindowAttribute in C# corresponding to the native function.
    [DllImport("dwmapi.dll", CharSet = CharSet.Unicode, PreserveSig = false)]
    internal static extern void DwmSetWindowAttribute(IntPtr hwnd,
                                                     DWMWINDOWATTRIBUTE attribute,
                                                     ref DWM_WINDOW_CORNER_PREFERENCE pvAttribute,
                                                     uint cbAttribute);
    // ...
    // Various other definitions
    // ...
}

接下来,在 MainWindow 构造函数中,在调用 InitializeComponent 后创建 WindowInteropHelper 类的新实例,以获取指向基础 HWND(窗口句柄)的指针。 请确保在显示窗口之前使用 EnsureHandle 方法强制系统为窗口创建 HWND,因为通常系统仅在退出构造函数后创建 HWND。

public MainWindow()
{
    InitializeComponent();

    IntPtr hWnd = new WindowInteropHelper(GetWindow(this)).EnsureHandle();
    var attribute = DWMWINDOWATTRIBUTE.DWMWA_WINDOW_CORNER_PREFERENCE;
    var preference = DWM_WINDOW_CORNER_PREFERENCE.DWMWCP_ROUND;
    DwmSetWindowAttribute(hWnd, attribute, ref preference, sizeof(uint));
    
    // ...
    // Perform any other work necessary
    // ...
}

示例 2 - 在 C# 中对应用的主窗口采用圆角 - WinForms

与 WPF 一样,对于 WinForms 应用,首先需要使用 P/Invoke 导入 dwmapi.dll 和 DwmSetWindowAttribute 函数签名。 可以在主 Form 类中执行此操作。

using System;
using System.Runtime.InteropServices;

public partial class Form1 : Form
{
    // The enum flag for DwmSetWindowAttribute's second parameter, which tells the function what attribute to set.
    // Copied from dwmapi.h
    public enum DWMWINDOWATTRIBUTE
    {
        DWMWA_WINDOW_CORNER_PREFERENCE = 33
    }

    // The DWM_WINDOW_CORNER_PREFERENCE enum for DwmSetWindowAttribute's third parameter, which tells the function
    // what value of the enum to set.
    // Copied from dwmapi.h
    public enum DWM_WINDOW_CORNER_PREFERENCE
    {
        DWMWCP_DEFAULT      = 0,
        DWMWCP_DONOTROUND   = 1,
        DWMWCP_ROUND        = 2,
        DWMWCP_ROUNDSMALL   = 3
    }

    // Import dwmapi.dll and define DwmSetWindowAttribute in C# corresponding to the native function.
    [DllImport("dwmapi.dll", CharSet = CharSet.Unicode, PreserveSig = false)]
    internal static extern void DwmSetWindowAttribute(IntPtr hwnd,
                                                     DWMWINDOWATTRIBUTE attribute, 
                                                     ref DWM_WINDOW_CORNER_PREFERENCE pvAttribute, 
                                                     uint cbAttribute);
    
    // ...
    // Various other definitions
    // ...
}

调用 DwmSetWindowAttribute 也与使用 WPF 应用时相同,但不必使用帮助程序类获取 HWND,因为它只是 Form 的属性。 在调用 InitializeComponent 后,从 Form 构造函数中调用它。

public Form1()
{
    InitializeComponent();

    var attribute = DWMWINDOWATTRIBUTE.DWMWA_WINDOW_CORNER_PREFERENCE;
    var preference = DWM_WINDOW_CORNER_PREFERENCE.DWMWCP_ROUND;
    DwmSetWindowAttribute(this.Handle, attribute, ref preference, sizeof(uint));
    
    // ...
    // Perform any other work necessary
    // ...
}

示例 3 - 在 C++ 中对应用的主窗口采用圆角

对于本机 C++ 应用,可以在创建窗口后,在消息处理函数中调用 DwmSetWindowAttribute,以要求系统执行圆角设置。

LRESULT ExampleWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) 
{
    switch (message)
    {
    // ...
    // Handle various window messages...
    // ...

    case WM_CREATE:
        // ...
        // Perform app resource initialization after window creation
        // ...
        
        if(hWnd)
        {
            DWM_WINDOW_CORNER_PREFERENCE preference = DWMWCP_ROUND;
            DwmSetWindowAttribute(hWnd, DWMWA_WINDOW_CORNER_PREFERENCE, &preference, sizeof(preference));
        }
        break;

    // ...
    // Handle various other window messages...
    // ...
    }

    return 0;
}

示例 4 - 对菜单采用半径较小的圆角设置 - C++

默认情况下,菜单是弹出窗口,不采用圆角设置。 如果你的应用创建一个自定义菜单,而你希望它沿用其他标准菜单的圆角策略,那么你可调用 API 来让系统知道该窗口应该采用圆角设置,即使它貌似与默认的圆角策略不匹配也是如此。

HWND CreateCustomMenu()
{
    // Call an app-specific helper to make the window, using traditional APIs.
    HWND hWnd = CreateMenuWindowHelper();

    if (hWnd)
    {
        // Make sure we round the window, using the small radius 
        // because menus are auxiliary UI.
        DWM_WINDOW_CORNER_PREFERENCE preference = DWMWCP_ROUNDSMALL;
        DwmSetWindowAttribute(hWnd, DWMWA_WINDOW_CORNER_PREFERENCE, &preference, sizeof(preference));
    }

    return hWnd;
}