ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

WinUI 3 主题机制与深色模式切换:资源字典、动态资源与 Qt 对照

WinUI 3 主题机制与深色模式切换:资源字典、动态资源与 Qt 对照 1. 先把 WinUI 3 的主题机制拆开看Win UI3 设置主题这件事看着像是改一个属性就完事真动手之后才发现里面牵扯到资源字典、依赖属性继承、窗口非客户区、弹窗宿主的层级关系一路挖下去能挖出不少东西。我自己是从一个晚上看白色界面太刺眼想加个深色切换按钮的需求开始的结果折腾了整整两个晚上。这篇笔记就按我踩坑的顺序把 WinUI 3 的主题体系从底层到应用层捋一遍同时把 Qt 那边的主题和字体设置拿来做横向对照方便有 Qt 背景的朋友快速迁移思路。不管你是刚上手 WinUI 3 想加个深色模式还是已经写过几页界面但在切换主题时颜色死活不刷新这篇里的方案和排查表应该都能直接用。1.1 内置的 Light / Dark / HighContrast 三套字典WinUI 3 的主题不是靠给每个控件设置颜色实现的而是靠资源字典的键值切换。XamlControlsResources在应用启动时被合并进来它内部挂了ThemeDictionaries这个字典有三个固定的键Light、Dark、HighContrast。每个键对应一个ResourceDictionary里面装的是同一批资源名但颜色值不同。举个最直白的例子SystemControlBackgroundBaseLowBrush这个名字在 Light 字典里指向一层淡灰色的画刷在 Dark 字典里指向一层深灰。控件模板里写的是{ThemeResource SystemControlBackgroundBaseLowBrush}运行时框架根据当前生效的主题去对应的那本字典里取值。所以换主题的本质不是重绘控件而是换一本字典让所有ThemeResource重新求值一次。这三本字典的键名是框架约定死的不能用Blue、Night之类自定义名字去顶替。HighContrast比较特殊它会读取系统的高对比度配色里面的资源名以SystemColor打头比如SystemColorWindowTextColor。你在自定义资源时如果想让高对比度模式下依然可用必须为这个键补一份资源否则高对比度用户会看到控件消失或者对比度不足。提示如果你只在 Light 和 Dark 里写了自定义资源高对比度模式下会自动回退到XamlControlsResources的默认值但你自己新增的资源名找不到对应项时会直接抛ResourceNotFound界面白屏。补HighContrast那本字典不是可选项是必选项。1.2 ThemeResource 与 StaticResource 的差别决定了能不能刷新这两个标记扩展的名字很像实际行为差别会直接决定你的主题切换能不能生效。StaticResource在 XAML 加载阶段求值一次之后就是固定的对象引用。ThemeResource每次主题变化或者RequestedTheme变化时都会重新查表求值并且会自动更新引用它的依赖属性。所以一个最典型的翻车场景是这样你在页面里写Border Background{StaticResource MyCardBrush}然后期待点击切换深色之后这个 Border 变深结果它纹丝不动。原因就是StaticResource已经把这个画刷实例锁死了。正确的写法就是换成ThemeResourceBorder Background{ThemeResource MyCardBrush}代价是ThemeResource每次求值都要走一遍字典查找比StaticResource略慢但这点开销在界面渲染里基本可以忽略。我自己的原则是跟颜色、画刷、字体相关的资源一律用ThemeResource尺寸、间距、模板这类跟主题无关的用StaticResource。还有一个更隐蔽的坑ThemeResource只对依赖属性生效。如果你在代码里手动new SolidColorBrush()赋值给某个控件的Background那它跟资源系统就彻底断开了主题切换时不会有任何反应。这种问题在排查时最容易被忽略因为 XAML 看起来完全正确。1.3 ApplicationTheme、ElementTheme、RequestedTheme 各自管什么这三个名字第一次看会懵理清它们的分工之后其实很清楚。ApplicationTheme是个枚举只有Light和Dark两个值它描述的是当前生效的主题这个概念你很少直接拿到它做判断。Application.RequestedTheme是应用级属性但它只能在App构造函数里设置一次启动之后再赋值会直接抛InvalidOperationException。这是很多人第一次写运行时代码切主题时踩的第一个坑。ElementTheme是给控件用的取值多了一个Default。FrameworkElement.RequestedTheme就是拿ElementTheme来赋值的它管的是这个元素及其子树用什么主题。注意Default的含义是跟随父级如果一路往上都是Default最后就落到Application.RequestedTheme上。对应的还有ActualTheme这是个只读属性告诉你这个元素最终到底渲染成什么主题还会在主题真正变化时触发ActualThemeChanged事件。要监听某个页面主题是否真的变了用这个事件比监听RequestedTheme更靠谱因为RequestedTheme从Default改成Light如果实际没变化事件不会触发而ActualThemeChanged只在真正生效时触发。2. 运行时切换主题的三种粒度搞清机制之后实现切换就变成了选择粒度的问题。三种粒度分别对应不同的场景我一般会根据需求挑最轻的那种别一上来就搞最重的。2.1 最小粒度给单个控件挂 ElementTheme有些时候产品只要求某个区域独立深色比如一个代码预览卡片、一个日志面板这时候直接给那个容器设ElementTheme就行Grid x:NameCodePanel RequestedThemeDark !-- 这里面的所有控件都会走 Dark 字典 -- /Grid代码里切换CodePanel.RequestedTheme isDark ? ElementTheme.Dark : ElementTheme.Light;这种方式的好处是影响范围小、可控而且不需要动任何全局状态。要注意的是RequestedTheme的继承是按视觉树走的Popup、Flyout、ContentDialog这些是挂到XamlRoot上的另一棵树上不会继承你在页面里设的值。这点后面第 5 节会细说。另外给Window.Content直接设RequestedTheme也能覆盖整窗这是很多人用的全局切换方案比改Application.RequestedTheme方便得多因为后者不支持运行时改。2.2 页面级在导航框架里做统一切换如果你的应用是NavigationView加多个页面的结构页面级切换会更好管理。做法是在每个Page的根节点上设public sealed partial class MainPage : Page { public MainPage() { this.InitializeComponent(); this.RequestedTheme ThemeService.Current; } }但这样每个页面都要写一遍容易漏。更稳的方式是把主题值放在一个静态服务里然后在Frame.Navigated事件里统一刷新ContentFrame.Navigated (s, e) { if (e.Content is FrameworkElement root) { root.RequestedTheme ThemeService.Current; } };这样不管导航到哪个页面主题都会自动套上新增页面也不用改代码。我实测下来这个方式在页面数量多的时候特别省事。2.3 应用级托管一个 ThemeService 并持久化用户选择实际项目里我会单独写一个ThemeService职责就三件事读配置、存配置、广播变化。public static class ThemeService { private const string Key AppTheme; public static event EventHandlerElementTheme? ThemeChanged; public static ElementTheme Current { get { var v ApplicationData.Current.LocalSettings.Values[Key] as string; return v switch { Dark ElementTheme.Dark, Light ElementTheme.Light, _ ElementTheme.Default }; } } public static void Apply(ElementTheme theme) { ApplicationData.Current.LocalSettings.Values[Key] theme switch { ElementTheme.Dark Dark, ElementTheme.Light Light, _ Default }; ThemeChanged?.Invoke(null, theme); } }为什么用LocalSettings存字符串而不是直接存枚举值因为ApplicationData的Values只支持 WinRT 基础类型枚举会被装箱跨版本读取时如果枚举定义变了容易出问题存字符串最稳。然后把窗口根节点订阅上ThemeService.ThemeChanged (s, theme) { if (Window.Current?.Content is FrameworkElement root) { root.RequestedTheme theme; } // 标题栏要单独处理见下一节 };注意ElementTheme.Default表示跟随系统用户选了这个之后系统主题变化时应用要能自动响应这就引出第 3 节的内容。3. 跟随系统主题监听颜色变化并同步跟随系统这个选项看着简单实现上要处理两件事判断当前系统是深还是浅以及系统主题变化时收到通知。3.1 UISettings.ColorValuesChanged 的正确用法WinRT 里的Windows.UI.ViewManagement.UISettings提供了这个事件WinUI 3 里依然可以用private readonly UISettings _uiSettings new(); public void StartWatching() { _uiSettings.ColorValuesChanged OnColorValuesChanged; } private void OnColorValuesChanged(UISettings sender, object args) { // 这个回调不在 UI 线程 _dispatcherQueue.TryEnqueue(() { var bg sender.GetColorValue(UIColorType.Background); bool isDark (bg.R bg.G bg.B) 384; ApplySystemTheme(isDark); }); }这里有三个关键点我第一次写的时候全踩了。第一回调线程不是 UI 线程直接在里面改控件属性会抛跨线程异常必须通过DispatcherQueue.TryEnqueue切回来。DispatcherQueue可以从窗口或者Microsoft.UI.Dispatching.DispatcherQueue.GetForCurrentThread()拿。第二ColorValuesChanged会连续触发多次特别是在系统切换主题的动画过程中如果每次都跑一遍完整逻辑界面会闪。我一般加一个 50 到 100 毫秒的防抖或者判断主题值和上次相比有没有真变没变就直接 return。第三UISettings实例要长期持有不能写在方法里用局部变量否则会被 GC 掉事件就没了。这是个很隐蔽的 bug。3.2 判断系统当前是深色还是浅色判断方式就是用GetColorValue(UIColorType.Background)拿背景色然后算 RGB 之和。理论上阈值是 384255 乘 1.5 再乘大概但我实测用 384判断在各种 Windows 版本上都稳定。也有人用bg.R 128判断原理一样都是看这个色偏黑还是偏白。顺便说UIColorType还有Foreground、Accent、AccentDark1等取值做强调色联动的时候会用到。3.3 标题栏与窗口边框颜色的同步这一步是最容易被漏掉的。你切了深色模式之后会发现客户区全黑了标题栏还是白的看着特别割裂。WinUI 3 里改标题栏要走AppWindowTitleBarvar titleBar AppWindow.TitleBar; if (AppWindowTitleBar.IsCustomizationSupported()) { if (isDark) { titleBar.ButtonBackgroundColor Colors.Transparent; titleBar.ButtonForegroundColor Colors.White; titleBar.ButtonHoverBackgroundColor Color.FromArgb(0x20, 0xFF, 0xFF, 0xFF); titleBar.ButtonHoverForegroundColor Colors.White; titleBar.ButtonPressedBackgroundColor Color.FromArgb(0x40, 0xFF, 0xFF, 0xFF); titleBar.ButtonPressedForegroundColor Colors.White; titleBar.ButtonInactiveBackgroundColor Colors.Transparent; titleBar.ButtonInactiveForegroundColor Color.FromArgb(0x66, 0xFF, 0xFF, 0xFF); } else { titleBar.ButtonBackgroundColor Colors.Transparent; titleBar.ButtonForegroundColor Colors.Black; // 其余同理 } }IsCustomizationSupported()这个判断别省某些环境或者老系统版本上不支持自定义不判断会直接抛异常。另外如果你没做ExtendsContentIntoTitleBarButtonBackgroundColor设成Transparent之后会露出下面系统默认的标题栏底色效果可能不是你要的这种情况下建议直接设成跟客户区一致的实色。实操心得标题栏按钮的Hover和Pressed颜色一定要一起设。只设前景色的话悬停时会用系统默认的浅蓝高亮在深色背景上非常刺眼。4. 自定义主题色板从强调色到整套颜色默认的 Light / Dark 用久了会腻很多项目想换成品牌色系。这块 WinUI 3 给了一个专门的类型叫ColorPaletteResources。4.1 ColorPaletteResources 能改哪些键它继承自ResourceDictionary属性是一批固定名字的颜色Accent、AltHigh、AltLow、AltMedium、AltMediumHigh、AltMediumLow、BaseHigh、BaseLow、BaseMedium、BaseMediumHigh、BaseMediumLow、ChromeAltLow、ChromeBlackHigh、ChromeBlackLow、ChromeBlackMedium、ChromeBlackMediumLow、ChromeDisabledHigh、ChromeDisabledLow、ChromeGray、ChromeHigh、ChromeLow、ChromeMedium、ChromeMediumLow、ChromeWhite、RegionColor。这批名字的命名逻辑是Base系列用于文字和前景Chrome系列用于容器和背景Alt是语义反转的那一套。数字后缀代表强度BaseHigh是最强的前景色正文文字BaseLow是最弱的分隔线。改法很直接var palette new ColorPaletteResources { Accent Color.FromArgb(255, 0, 120, 212), BaseHigh Color.FromArgb(255, 32, 32, 32), BaseMedium Color.FromArgb(255, 96, 96, 96), BaseLow Color.FromArgb(255, 224, 224, 224), ChromeMedium Color.FromArgb(255, 243, 243, 243), ChromeLow Color.FromArgb(255, 251, 251, 251) }; Application.Current.Resources.MergedDictionaries.Add(palette);这套色板是单一套的不区分 Light/Dark。换句话说ColorPaletteResources更适合做一套自定义配色如果你要的是 Light 和 Dark 两套独立色板得走自定义ThemeDictionaries的路子或者准备两个ColorPaletteResources实例在切主题时替换。4.2 资源字典的合并顺序与覆盖规则MergedDictionaries的查找顺序是从后往前也就是说后加进去的优先级更高。这一条决定了你的自定义色板要加在XamlControlsResources之后。Application.Resources ResourceDictionary ResourceDictionary.MergedDictionaries XamlControlsResources xmlnsusing:Microsoft.UI.Xaml.Controls / ResourceDictionary Sourcems-appx:///Themes/BrandColors.xaml / /ResourceDictionary.MergedDictionaries /ResourceDictionary /Application.Resources顺序反过来的话你的品牌色会被XamlControlsResources覆盖掉界面上一点变化都没有然后你会开始怀疑人生。另外Application.Resources的优先级低于页面级资源页面级低于控件内联资源。所以如果你想全局覆盖某个画刷加到Application.Resources.MergedDictionaries是最合适的层级。4.3 自定义资源在 Light/Dark 双套下的写法自己的业务资源比如卡片背景、分割线、状态色最规范的做法是在一个独立文件里用ThemeDictionaries写ResourceDictionary xmlnshttp://schemas.microsoft.com/winfx/2006/xaml/presentation xmlns:xhttp://schemas.microsoft.com/winfx/2006/xaml ResourceDictionary.ThemeDictionaries ResourceDictionary x:KeyLight SolidColorBrush x:KeyCardBackgroundBrush Color#FFFFFFFF / SolidColorBrush x:KeyCardBorderBrush Color#FFE5E5E5 / SolidColorBrush x:KeySuccessTextBrush Color#FF107C10 / /ResourceDictionary ResourceDictionary x:KeyDark SolidColorBrush x:KeyCardBackgroundBrush Color#FF2B2B2B / SolidColorBrush x:KeyCardBorderBrush Color#FF3D3D3D / SolidColorBrush x:KeySuccessTextBrush Color#FF6CCB5F / /ResourceDictionary ResourceDictionary x:KeyHighContrast SolidColorBrush x:KeyCardBackgroundBrush Color{ThemeResource SystemColorWindowColor} / SolidColorBrush x:KeyCardBorderBrush Color{ThemeResource SystemColorWindowTextColor} / SolidColorBrush x:KeySuccessTextBrush Color{ThemeResource SystemColorWindowTextColor} / /ResourceDictionary /ResourceDictionary.ThemeDictionaries /ResourceDictionary注意 Dark 里的状态色要比 Light 里更亮一些。这是配色上的经验深色背景下的文字要降低饱和度、提高明度否则会显得脏。绿色从#107C10换成#6CCB5F就是这个道理直接照搬浅色值到深色下视觉上会糊成一团。5. 容易被漏掉的角落弹窗、Popup 与 Mica 背景前面几节处理的是主界面实际项目里最容易出问题的恰恰是那些浮在上面的东西。5.1 ContentDialog 的主题归属与 XamlRootContentDialog在 WinUI 3 里必须指定XamlRoot否则会抛异常。它的主题默认走Application.RequestedTheme而不是你当前页面设的ElementTheme。所以你会遇到这种情况页面切成深色了弹出来的对话框还是白的。解决办法是显式给 Dialog 设主题var dialog new ContentDialog { XamlRoot this.XamlRoot, Title 确认操作, Content 这个操作不可撤销。, PrimaryButtonText 确定, CloseButtonText 取消, RequestedTheme ThemeService.Current };如果你在页面里做的是局部深色而不是全局主题那就要传页面当前的实际主题RequestedTheme this.ActualTheme。用ActualTheme而不是RequestedTheme是因为后者可能是Default赋给 Dialog 之后依然跟随全局。5.2 Flyout、MenuFlyout 的主题继承MenuFlyout、Flyout、ToolTip这些同样挂在XamlRoot上行为跟ContentDialog一致。它们的默认主题来源也是应用级设置。对于MenuFlyout你可以直接设var flyout new MenuFlyout { MenuFlyoutPresenterStyle (Style)Application.Current.Resources[CustomMenuStyle] };或者在 XAML 里给每个MenuFlyoutItem单独设RequestedTheme。但如果MenuFlyout项目多一个个设太麻烦更省事的方式还是走全局主题方案让Application.Resources里的ThemeDictionaries真正被替换掉这样所有弹窗都会自然跟随。这也解释了一个现象为什么改Window.Content.RequestedTheme这个方案在有些项目里不好用因为它只影响客户区的视觉树所有弹窗都得手动补。如果你的应用弹窗很多我更建议走全局替换字典的方案虽然实现略重但一次到位。5.3 Mica / Acrylic 背景与主题切换的配合WinUI 3 支持MicaBackdrop和DesktopAcrylicBackdrop设置方式this.SystemBackdrop new MicaBackdrop { Kind MicaKind.BaseAlt };Mica 有个特性它的颜色跟着系统主题走不跟你应用内的RequestedTheme走。这就导致一个很尴尬的场景——用户选了应用内深色但系统还是浅色客户区控件全黑了Mica 底色却是浅的界面看起来像拼接的。我这个项目最终的处理方式是用户选深色时把SystemBackdrop设成null用实色背景选跟随系统时才启用 Mica。这是个取舍牺牲了一点视觉效果换来一致性。如果你必须两者都要那就在主题切换时重建 backdrop 对象但实测切换瞬间会有一次闪烁体验不算好。DesktopAcrylicBackdrop同理它的采样也跟系统主题绑定。6. 把 Qt 的主题与字体设置拿来对照我之前做过几年 Qt 的项目切到 WinUI 3 之后发现两边在很多设计思路上是相通的但也有根本差异对照着看能少走弯路。6.1 Qt Widgets 的调色板与 QSS 路线Qt Widgets 有两套并行的主题机制。一套是QPaletteQPalette pal; pal.setColor(QPalette::Window, QColor(32, 32, 32)); pal.setColor(QPalette::WindowText, QColor(240, 240, 240)); pal.setColor(QPalette::Base, QColor(24, 24, 24)); qApp-setPalette(pal);另一套是 QSS 样式表写法类似 CSSqApp-setStyleSheet(R( QWidget { background: #202020; color: #F0F0F0; } QPushButton { border: 1px solid #3D3D3D; padding: 6px 12px; } QPushButton:hover { background: #2B2B2B; } ));这跟 WinUI 3 的ThemeResource加ThemeDictionaries是两种哲学。Qt 更偏向命令式地告诉每个控件改什么WinUI 3 更偏向声明式地换一本字典让资源引用重新求值。Qt 的方式灵活但容易漏一个控件忘了写 QSS 就是白的WinUI 3 的方式依赖你全部用ThemeResource只要有一处写了StaticResource或者硬编码颜色同样会漏。Qt 里切换主题需要手动qApp-setStyleSheet(...)再触发一次重绘有时候还得调用style()-unpolish()和polish()才能让某些控件刷新这个坑跟 WinUI 3 里资源没刷新是同一类问题本质都是缓存了旧值。Qt 6.5 之后也引入了系统主题监听connect(QGuiApplication::styleHints(), QStyleHints::colorSchemeChanged, this, [](Qt::ColorScheme scheme) { applyTheme(scheme Qt::ColorScheme::Dark); });这跟 WinUI 3 的UISettings.ColorValuesChanged作用一样但 Qt 这个信号是在主线程触发的不需要自己做线程切换比 WinUI 3 省一步。Qt 6.5 之前只能靠定时轮询注册表或者读系统设置比较难受。6.2 字体设置的两种思路Qt 里设全局字体很简单QApplication::setFont(QFont(Microsoft YaHei UI, 10));或者走样式表qApp-setStyleSheet(QWidget { font-family: Microsoft YaHei UI; font-size: 10pt; });WinUI 3 里没有Application.SetFont这种方法得改资源Application.Current.Resources[ContentControlThemeFontFamily] new FontFamily(Microsoft YaHei UI);常用的字体资源键还有BodyFontFamily、CaptionFontFamily、BodyStrongFontFamily、TitleTextFontFamily等。改ContentControlThemeFontFamily影响面最大基本所有标准控件都会跟着变。要注意FontFamily的字符串格式带空格的字体名必须加引号吗在 WinUI 3 里不需要写Microsoft YaHei UI就行但如果你想指定 fallback格式是Segoe UI, Microsoft YaHei UI。字号没法通过资源一次性改因为很多控件模板里写死了FontSize{ThemeResource BodyFontSize}你可以覆盖BodyFontSize这个资源Application.Current.Resources[BodyFontSize] 15.0;但只对用了这个资源的控件生效Button、TextBox这类控件的模板里用的资源名不完全一样得逐个查。这是 WinUI 3 相比 Qt 在字体统一设置上不够方便的地方。Qt 的QApplication::setFont之所以能一刀切是因为 Qt 控件的绘制是统一的QStyle在管字体从应用级别往下传。WinUI 3 的控件模板由各个控件自己定义粒度更细但统一控制的成本更高。6.3 两套体系的取舍从我的使用感受看如果你的项目需要高度定制的视觉风格Qt 的 QSS 更直接改起来心理负担小如果是做 Windows 原生风格的桌面应用WinUI 3 的资源体系跟系统融合得更好Mica、云母、系统强调色这些能白拿代价是学习曲线陡一些而且很多行为需要实测才知道。还有一点Qt 的主题切换是同步生效的切完立刻能看到结果WinUI 3 的RequestedTheme变化会走一层布局和渲染流程某些复杂页面上会有一帧的延迟快速连续切换时能看到闪烁。这个在 Qt 里基本不存在。7. 踩坑记录与排查速查最后把这几个月的坑集中列一下都是我在真实项目里撞过的。7.1 切换后不刷新的四类原因按出现频率排序第一类是资源引用写成了StaticResource。这个问题占比最高排查方法是在 XAML 里搜StaticResource逐个检查是不是有颜色类的资源被这么引用了。第二类是颜色硬编码在代码里比如new SolidColorBrush(Colors.Gray)这种根本没进资源系统只能改成从资源里取。第三类是控件模板内部用了{TemplateBinding Background}而你没给对方设Background导致它拿的是默认值。第四类是ContentDialog、Flyout这类挂在另一棵视觉树上的元素需要单独处理。7.2 高对比度与特殊场景高对比度模式下系统会用一套完全不同的颜色很多自定义的强调色会被自动忽略。你如果自己画了图标或者自定义控件必须确保前景和背景用的是SystemColorWindowTextColor这类系统资源否则在黑色高对比度主题下可能完全看不见。判断当前是不是高对比度的方式是var uiSettings new UISettings(); bool isHighContrast uiSettings.HighContrast;还有HighContrastChanged事件可以监听。虽然占比不高但真出了问题是可访问性级别的逃不掉。另一个场景是远程桌面或者虚拟机里跑UISettings的某些值可能返回异常建议所有取值逻辑都加try/catch兜底失败时退回默认主题。7.3 排查速查表现象可能原因排查动作切换主题后部分控件不变色用了StaticResource或代码里硬编码颜色搜StaticResource检查 C# 里的画刷创建弹窗颜色跟主界面不一致ContentDialog/Flyout未继承页面主题显式给弹窗设RequestedTheme this.ActualTheme标题栏一直是白色未处理AppWindowTitleBar检查ButtonBackgroundColor等属性是否设置Mica 背景不跟着应用内主题变Mica 只跟随系统主题应用内主题切换时禁用 Mica 或置空 backdrop系统主题变化后应用没反应UISettings实例被 GC 或没用DispatcherQueue把实例提升为字段回调里切回 UI 线程每次系统切主题界面闪多次事件连续触发加防抖或对比主题值去重高对比度下控件消失自定义资源缺HighContrast键补一份高对比度资源字典运行时设Application.RequestedTheme抛异常该属性只能在启动前设置改用Window.Content.RequestedTheme这张表我基本每次开新项目都会过一遍能省掉不少重复排查的时间。最后补一个我自己用下来比较顺手的做法把一个ElementTheme的切换逻辑封成一个附加属性或者行为挂在窗口根节点上XAML 里写一行就能绑定到 ViewModel 的属性省掉后面的事件订阅代码。这个小封装在项目里复用了三四次收益比预想的高。至于字体资源那块的统一覆盖我的建议是尽量在App.xaml里静态声明别在构造函数里到处赋值否则后期改一处就得全项目搜一遍。
返回列表