关于 winui-design
面向 WinUI 开发的界面设计技能,借助 WinApp CLI 查找示例与 API,并提供控件选择、窗口尺寸、XAML、主题和无障碍方面的指导。
主要功能
- 01编码前搜索 WinUI 示例
- 02按项目引用核验 API
- 03按需求映射平台控件
- 04窗口尺寸与 XAML 指引
- 05主题与无障碍检查
适用场景
- 01为新 WinUI 页面选择控件
- 02根据还原的包检查 API
- 03避免绑定与布局错误
- 04检查主题和界面无障碍
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ microsoft/ win-dev-skills --skill winui-design安装后在 Claude Code 中输入/winui-design确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/brushes-and-icons.mdlayout-review.mdtheme-accessibility.md详细介绍
编写 XAML 前搜索示例
此工作流需要 WinApp CLI 0.7+。使用 winapp find-ui 查找有依据的控件和示例。先查找,再编码:
winapp find-ui "<focused feature>" # compact matches + scenario IDs
winapp find-ui --id <scenario-id> # full XAML/C# + prerequisite notes
winapp find-ui --id <id-1> --id <id-2> --json # batch, structured output
winapp find-ui --list # browse all default-source scenarios
winapp find-ui "<feature>" --refresh # force a corpus refresh
默认搜索涵盖 WinUI Gallery、Windows Community Toolkit 和精选核心模式。Reactor 的纯 C#/MVU 示例需通过 --source reactor 显式启用;仅在 Reactor 项目中使用。WinApp CLI 会获取并缓存 Gallery/Toolkit/Reactor 语料库,而核心模式可离线使用。
根据已还原的项目验证 API
在编写 API 假设前,请从已还原的应用项目目录运行命令,确保 winapp find-api 使用该项目的确切引用,而非某个示例中可能更新的包版本:
winapp find-api InfoBar --json --project-dir .
winapp find-api check-property InfoBar Severity IsOpen Message Title --json --project-dir .
winapp find-api members NavigationView --filter selected --json --project-dir .
选择多个项目时,使用 --project-dir <app-directory>(默认为当前工作目录)或 --project <name>,不能同时使用两者。从仓库根目录或同级项目发起未指定范围的查询,可能会查到错误的 API 范围。--project sdk 只搜索已安装的机器 SDK,不搜索应用的 NuGet 引用。如果结果看起来过时,请先检查还原和项目选择,再刷新。使用 winapp find-api --help 查看发现/索引命令,不要凭记忆猜测 API;使用 find-ui 查找用法模式。
应用形态参考
开始布局页面前,先选择最接近的已发布应用形态:
| 应用类型 | 参考控件 | 参考应用 |
|---|---|---|
| 设置 / 配置工具 | NavigationView 左侧导航 + SettingsCard / SettingsExpander | Windows 设置、Slack |
| 文档 / 会话编辑器 | TabView + 全出血内容、简洁界面 | Windows Terminal、VS Code、Notepad |
| 层级浏览器 | TreeView + ListView + BreadcrumbBar | 文件资源管理器、Outlook |
| 开发者工具 / 仪表板 | NavigationView + 卡片布局 | Dev Home、GitHub Desktop |
| 单一用途实用工具 | 模式切换器 + 紧凑网格 | 计算器、截图工具 |
| 媒体 / 画布 / 主视觉 | 使用主视觉区域和浮动命令的 Grid,不使用 NavigationView | 照片、Spotify、Clipchamp |
常用控件速查表
编写 XAML 前,将需求映射到平台控件。这些映射旨在避免沿用其他框架的习惯(WPF DataGrid、Web <select>、HTML <input type=date>):
- 导航: 2–7 个分区 →
NavigationView;文档/会话选项卡 →TabView;面包屑路径 →BreadcrumbBar;2–3 种模式 →SelectorBar。 - 数据显示: 垂直列表 →
ListView;平铺/网格 →GridView或ItemsRepeater+UniformGridLayout;层级结构 →TreeView;表格数据 → 使用基于Grid的ItemTemplate的ListView,并在上方放置标题Grid(WinUI 没有DataGrid;不要默认使用CommunityToolkit.WinUI.Controls.DataGrid——其列无法使用x:Bind);主从视图 →ListView+ 详细信息Grid。 - 输入: 文本 →
TextBox;数字 →NumberBox;搜索 →AutoSuggestBox;日期 →CalendarDatePicker;布尔值 →ToggleSwitch;从 2–3 项中选一项 →RadioButtons;从 4 项或更多中选一项 →ComboBox。 - 反馈: 阻塞式决策 →
ContentDialog;上下文操作 →Flyout/MenuFlyout;引导 / 提示 →TeachingTip;内联状态 / 异步进度 →InfoBar;系统通知 →AppNotification。
如果以上映射不适用,请先运行 winapp find-ui "<intent>",再自行尝试。
窗口尺寸(WinUI 3 特有)
WinUI 3 没有
SizeToContent。 如果不显式设置尺寸,Windows 会默认将主窗口设为约 1024×768,这对大多数实用工具来说都太大了。请在MainWindow的构造函数中设置尺寸。
参考标准。 宽度 = 最宽一行的宽度 + 48 像素边距,再向上取整到最接近的 20 的倍数。高度 = 32(标题栏)+ Σ(各行高度)+ Σ(间距)+ 48 像素边距,再向上取整到 20 的倍数。向上取整——内容被裁切比窗口稍微偏宽更糟。合理范围(请根据参考标准自行推导):
- 单一用途的实用工具 → 宽约 440–560
- 表单 / 单页工具 → 宽约 600–800,高约 640–800
- 多窗格(导航 + 内容)→ 宽约 1100–1300,高约 720–840
- 文档 / 画布 / 媒体编辑器 → 宽 1280 以上
AppWindow.Resize 接受的是物理像素,而不是 DIP——请乘以显示器的 DPI 缩放比例。构造函数中 XamlRoot.RasterizationScale 为 null,且在 AppWindow.Move 后会过时,因此 [DllImport] GetDpiForWindow 是最简洁的做法:
using Microsoft.UI;
using Microsoft.UI.Windowing;
using System.Runtime.InteropServices;
using Windows.Graphics;
public sealed partial class MainWindow : Window
{
[DllImport("user32.dll")]
private static extern uint GetDpiForWindow(IntPtr hWnd);
public MainWindow()
{
InitializeComponent();
var hwnd = Win32Interop.GetWindowFromWindowId(AppWindow.Id);
var scale = GetDpiForWindow(hwnd) / 96.0;
// widthDip / heightDip come from the rubric above — derive, don't copy.
AppWindow.Resize(new SizeInt32((int)(widthDip * scale), (int)(heightDip * scale)));
}
}
不要通过设置根 Grid 的 Width/Height 来调整窗口尺寸——这样裁切的是内容,而不是窗口。
XAML 易踩的坑(否则你会把有问题的代码交付出去)
检查生效的 x:Bind 模式
除非继承的 x:DefaultBindMode 修改了默认值,否则 x:Bind 默认为 OneTime。动态数据应使用 OneWay/TwoWay 并提供更改通知;显式指定的 Mode 会覆盖继承的默认值。稳定的命令 / 事件绑定无需统一重写模式。
<TextBlock Text="{x:Bind Vm.Status}" />
<TextBlock Text="{x:Bind Vm.Status, Mode=OneWay}" />
在页面 / 窗口中,x:Bind 会针对代码隐藏解析(例如其 Vm 属性),而不是针对 DataContext。请在有类型的 DataTemplates 上使用 x:DataType,不要在 Page 上用它来设置 VM。运行时 {Binding}/DisplayMemberPath 也可能合适;对于 AOT,其源类可能需要添加 partial 并标注 [WinRT.GeneratedBindableCustomProperty]。请参阅 winui-packaging 的 references/sourcegen-patterns.md,不要将所有运行时绑定都视为不受支持。
TextBox 双向绑定需要 UpdateSourceTrigger=PropertyChanged
<TextBox Text="{x:Bind Vm.Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
默认触发器对 TextBox.Text 来说是 LostFocus(大多数其他属性的默认值为 PropertyChanged)。每次按键后都不会更新 VM,因此 UIA 键盘模拟测试(WinAppDriver SendKeys 等)如果在输入后立即断言,就会在焦点移动前看到过时的 VM 状态。
在 C# 中使用静态 setter 设置附加属性,而不是使用初始化器
using Microsoft.UI.Xaml.Automation;
// ❌ WRONG — does not compile. CS0117: 'Button' does not contain a definition for 'AutomationProperties'.
// AutomationProperties is a static class of attached-property accessors, not an instance member.
var btn = new Button { AutomationProperties = { AutomationId = "BtnSave" } };
// ✅ CORRECT
var btn = new Button { Content = "Save" };
AutomationProperties.SetAutomationId(btn, "BtnSave");
AutomationProperties.SetName(btn, "Save button");
Grid.SetRow(btn, 1);
ToolTipService.SetToolTip(btn, "Save the current document");
Converter={x:Null} 会导致 x:Bind 在运行时崩溃
{x:Bind} 要求 Converter 使用 {StaticResource} 查找。Converter={x:Null} 虽然可以编译,但生成的代码会调用 LookupConverter(""),该调用返回 null,随后代码会对其解引用——首次激活绑定时就会出现 Resource Dictionary Key can only be String-typed / NullReferenceException。如果不需要转换器,请完全省略该属性。
优先使用 x:Bind 静态函数,而不是 IValueConverter
// MainPage.xaml.cs
public static Visibility BoolToVisibility(bool v) => v ? Visibility.Visible : Visibility.Collapsed;
public static Visibility InvertBoolToVisibility(bool v) => v ? Visibility.Collapsed : Visibility.Visible;
public static bool Not(bool v) => !v;
<TextBlock Visibility="{x:Bind local:MainPage.BoolToVisibility(Vm.IsLoading), Mode=OneWay}" />
<Button IsEnabled="{x:Bind local:MainPage.Not(Vm.IsLoading), Mode=OneWay}" />
Acrylic 和 ThemeShadow 的渲染规则
Border和Control的BackgroundSizing默认值都是InnerBorderEdge,这会将 Acrylic 正确裁切到内侧描边。风险与直觉相反:带描边的 Acrylic 表面不要将其改为OuterBorderEdge——这会使材质溢出描边。ThemeShadow会根据投影对象的TranslationZ 投射阴影。Microsoft 推荐的高度为:工具提示使用16,弹出 / Flyout UI 使用32,对话框使用128——请根据表面类型选择。对于非弹出式投影对象,请将阴影应落到的表面添加到ThemeShadow.Receivers;否则阴影无处可落,看起来会像被裁切一样。
主题规则(简版)
- 在使用位置使用
{ThemeResource ...}(切换主题时会更新)。在ThemeDictionaries中使用{StaticResource}定义主题专属资源;SystemAccentColor/SystemColor*是例外,仍使用{ThemeResource}。 - 自定义主题字典必须明确涵盖
Light、Dark和HighContrast— 绝不能使用Default。 - 按用途命名资源(
CardBackgroundBrush、DangerTextBrush),而不是按色相命名。 - 浅色/深色主题可用 ≠ 高对比度主题可用。请单独在对比度主题中测试。
- 除非你的应用已全面提供适配系统的画笔,否则绝不要设置
HighContrastAdjustment="None"。
反模式
| ❌ 不要 | ✅ 改为 |
|---|---|
不假思索地把每个应用都做成左侧 NavigationView | 在轮廓表中选最接近的一行;主视觉、文档和实用工具布局同样适用 |
| 把品牌色或着色背景视为不符合规范 | 覆盖 SystemAccentColor 或使用着色的 DesktopAcrylicBackdrop,正是 Microsoft 自家第一方应用实现差异化的方式 |
| 在过大的窗口中放置很小的内容区域 | 将窗口大小调整到与内容相适应(参见 窗口大小调整),或让内容填满可用空间 |
| 手动构建自定义胶囊式/分段式标签切换器 | 使用 NavigationView Top 或 SelectorBar |
| 两列等宽 50/50 分割,其中一个窗格仅用于布局结构 | 结构窗格使用固定尺寸,内容区域灵活调整 — 仅当布局轮廓确实包含结构窗格时才采用 |
硬编码颜色字面值(#RRGGBB、White) | 使用按语义命名的 {ThemeResource} 画笔 |
在 ListView / GridView 外包一层 ScrollViewer | 集合控件本身已有滚动功能 — 为其指定受限高度 |
为标准控件自定义 ControlTemplate | 使用内置控件 + 轻量样式覆盖 |
| 仅用占位文本作为字段标签 | 始终提供可见标签 |
| 在小屏幕上隐藏必需命令,且不提供其他入口 | 使用溢出菜单、次级界面或响应式提升规则 |
使用模态 ContentDialog 显示非阻塞提示 | 使用 TeachingTip、InfoBar 或内联状态 |
| 不经确认就触发破坏性操作(删除/丢弃/重置) | 使用 ContentDialog,主操作按钮以动词命名,次要按钮为“取消”;在正文中显示项目标识信息(名称、数量) |
在 ListView / GridView 已适用时仍自定义列表控件 | 使用平台集合控件 + 虚拟化 |
仅当以下条件全部满足时才构建自定义 UI:没有适用的平台/Gallery/Toolkit 控件;你将实现键盘、焦点、UI 自动化、主题资源、高对比度和响应式行为;你有默认/悬停/按下/禁用/选中/聚焦/错误状态的规范;你已使用键盘和对比度主题完成测试。
参考资料(按需加载)
| 文件 | 在以下情况下加载… |
|---|---|
references/brushes-and-icons.md | 按用途查找画笔键、在 Icon / IconSource 插槽之间做选择,或在 FontIcon / SymbolIcon / PathIcon 等图标中做选择时。 |
references/theme-accessibility.md | 编写主题字典、自定义画笔/样式/模板或高对比度支持时。 |
references/layout-review.md | 审查响应式行为、断点,或数据驱动页面的空内容/加载中/错误状态覆盖时。 |