[WPF] WPF项目架构设计

  分享基于.NET 4.5的WFP项目架构设计。
一、项目结构
  我们的代码不可能集中在一个项目,缺少共用性,当一个git仓库存在多个项目时,我希望项目结构如下所示:
0
  • App1、App2文件夹是项目主程序,但是它们的解决方案文件放在根目录,分别是App1.sln和App2.sln,这样的好处是被引用的公共项目的Nuget包都放在根目录的packages里面,如果解决方案分别放在App1、App2里面,则Nuget包分别在不同文件夹,那么当你给App1引用的项目1添加Nuget包时,项目1.project文件会使用相对项目1所在文件的路径,如果项目1同时被App2所引用,项目1的Nuget包依然指向App1,总之,Nuget多了一份,而且引用还很乱,所以最好把解决方案文件放在一起;
  • Common1、Common2指公共项目,比如基础库,WPF UI库,附件程序等,这些项目可能被主程序共同引用,所有不要单独放在App1文件夹;
  • Module1、Module2是模块化项目,它们可能是App1里的一个模块,也可能是App1、App2共有的模块,模块化后面会讲到;
二、UI库、基础库
  这里建议将UI库和基础库放在一起,作为一个核心代码库,如果分开的话,UI库必然会引用基础库,这样基础库就没办法再引用UI库了,依赖注入可以解决双向引用的问题,但是这里我们会将依赖注入代码放在基础库,所以最好还是将这两个库放在一起。
  下面是基本控件样式,文件结构如下所示:
0
  • Themes文件夹是主题色,定义哪些颜色后面会讨论;
  • Control.XXX是特定控件的样式,如果把所有样式写在一起会很乱,当然,日历控件模板里面的按钮样式,还是写在一起比较好;
  • Effects是阴影资源;
  • Geometry是矢量图标资源;
  • Storyboard是动画资源;
  下面看看主题资源都有哪些:
<!--字体-->
<FontFamily x:Key="Font">Microsoft YaHei UI,微软雅黑,Times New Roman,Courier New,宋体</FontFamily>

<!--主题色-->
<SolidColorBrush x:Key="Brushes.Background" Color="#FFFFFF" options:Freeze="True" />
<SolidColorBrush x:Key="Brushes.Foreground" Color="#333333" options:Freeze="True" />
<SolidColorBrush x:Key="Brushes.Themes" Color="#0090FF" options:Freeze="True" />
<SolidColorBrush x:Key="Brushes.Themes.Highlight" Color="#0078D4" options:Freeze="True" />
<SolidColorBrush x:Key="Brushes.Themes.Foreground" Color="#FFFFFF" options:Freeze="True" />

<!--遮罩-->
<SolidColorBrush x:Key="Brushes.Mask" Color="#99000000" options:Freeze="True" />

<!--输入框-->
<Thickness x:Key="InputBox.Padding">10 5</Thickness>
<CornerRadius x:Key="InputBox.CornerRadius">4</CornerRadius>
<SolidColorBrush x:Key="Brushes.InputBox.Background" Color="#ffffff" options:Freeze="True" />
<SolidColorBrush x:Key="Brushes.InputBox.Background.Hover" Color="#ffffff" options:Freeze="True" />
<SolidColorBrush x:Key="Brushes.InputBox.Background.Focus" Color="#ffffff" options:Freeze="True" />
<SolidColorBrush x:Key="Brushes.InputBox.Background.Disabled" Color="#f1f1f1" options:Freeze="True" />
<SolidColorBrush x:Key="Brushes.InputBox.BorderBrush" Color="#c7ccd2" options:Freeze="True" />
<SolidColorBrush x:Key="Brushes.InputBox.BorderBrush.Hover" Color="#6facff" options:Freeze="True" />
<SolidColorBrush x:Key="Brushes.InputBox.BorderBrush.Focus" Color="#6facff" options:Freeze="True" />
<SolidColorBrush x:Key="Brushes.InputBox.BorderBrush.Disabled" Color="#c7ccd2" options:Freeze="True" />
<SolidColorBrush x:Key="Brushes.InputBox.Foreground" Color="#333" options:Freeze="True" />
<SolidColorBrush x:Key="Brushes.InputBox.Watermark.Foreground" Color="#a6a6a6" options:Freeze="True" />

<!--下拉框-->
<sys:Double x:Key="ComboBox.Icon.Width">14</sys:Double>
<sys:Double x:Key="ComboBoxItem.MinHeight">36</sys:Double>
<sys:Double x:Key="ComboBox.Popup.VerticalOffset">0</sys:Double>
<Thickness x:Key="ComboBox.Icon.Margin">10 0</Thickness>
<Thickness x:Key="ComboBox.Popup.BorderThickness">0</Thickness>
<CornerRadius x:Key="ComboBox.Popup.CornerRadius">4</CornerRadius>
<SolidColorBrush x:Key="Brushes.ComboBox.Icon.Fill" Color="#a5a9ae" options:Freeze="True" />
<SolidColorBrush x:Key="Brushes.ComboBox.Icon.Fill.Hover" Color="#a5a9ae" options:Freeze="True" />
<SolidColorBrush x:Key="Brushes.ComboBoxItem.Background" Color="Transparent" options:Freeze="True" />
<SolidColorBrush x:Key="Brushes.ComboBoxItem.Background.Hover" Color="#f3f3f3" options:Freeze="True" />
<SolidColorBrush x:Key="Brushes.ComboBoxItem.Background.Select" Color="#f3f3f3" options:Freeze="True" />
<SolidColorBrush x:Key="Brushes.ComboBoxItem.Foreground" Color="#333" options:Freeze="True" />
<SolidColorBrush x:Key="Brushes.ComboBoxItem.Foreground.Hover" Color="#333" options:Freeze="True" />
<SolidColorBrush x:Key="Brushes.ComboBoxItem.Foreground.Select" Color="#333" options:Freeze="True" />
<SolidColorBrush x:Key="Brushes.ComboBox.Popup.Background" Color="White" options:Freeze="True" />
<SolidColorBrush x:Key="Brushes.ComboBox.Popup.BorderBrush" Color="Transparent" options:Freeze="True" />

<!--列表-->
<Thickness x:Key="TabelCell.Padding">12 4</Thickness>
<SolidColorBrush x:Key="Brushes.TabelHeader.Foreground" Color="#333" options:Freeze="True" />
<SolidColorBrush x:Key="Brushes.TabelCell.Foreground" Color="#5c5c5c" options:Freeze="True" />
<SolidColorBrush x:Key="Brushes.ListItem.Background1" Color="#f5f6f7" options:Freeze="True" />
<SolidColorBrush x:Key="Brushes.ListItem.Background2" Color="White" options:Freeze="True" />
<SolidColorBrush x:Key="Brushes.ListItem.Background.Hover" Color="#ebf5ff" options:Freeze="True" />
<SolidColorBrush x:Key="Brushes.ListItem.Background.Select" Color="#cdeafd" options:Freeze="True" />
<SolidColorBrush x:Key="Brushes.ListItem.Line" Color="#e3e3e3" options:Freeze="True" />

<!--文字-->
<SolidColorBrush x:Key="Brushes.Foreground.Label" Color="#6f6f6f" options:Freeze="True" />
<SolidColorBrush x:Key="Brushes.Foreground.Warning" Color="#ff8f0a" options:Freeze="True" />
<SolidColorBrush x:Key="Brushes.Foreground.Danger" Color="#ed1414" options:Freeze="True" />
<SolidColorBrush x:Key="Brushes.Foreground.Link" Color="#2d8cf0" options:Freeze="True" />
<SolidColorBrush x:Key="Brushes.Foreground.Link.Hover" Color="#63afff" options:Freeze="True" />
<SolidColorBrush x:Key="Brushes.Foreground.Link.Disabled" Color="#999" options:Freeze="True" />
<SolidColorBrush x:Key="Brushes.Foreground.Link.Split" Color="#999" options:Freeze="True" />
  上面定义了很多控件相关的资源,比如输入框和下拉框的边框颜色肯定是一样的,所以共有一个资源,当有切换主题色的需求时,或某个页面使用另一个主题时,使用动态资源DynamicResource,引入暗色主题资源即可。
三、MVVM、IOC、消息发布-订阅、模块化
  这里放在一起是因为直接借鉴了Prism.Wpf,但Prism.Wpf最新稳定版最低支持是.NET4.6,无法在.NET4.5使用,所幸源码并不复杂,我们把需要的代码抄下来就可以了,一方面去掉了用不到的功能,比如Region Navigation,甚至ViewModelLocator也是不用的,另一方面还可以根据需求修改源码,比如支持泛型的DelegateCommand,支持异步的AsyncDelegateCommand。
  MVVM的话,我习惯ViewModelFirst,就是根据ViewModel自动查询View,这样做的好处是在适配多种分辨率时,可以动态的查找View,相反,如果你使用new View()的话,动态创建适当分辨率的View很麻烦,后面讲多分辨率适配时会细说。
  IOC就按推荐使用Unity库,IOC就注册和使用,用什么库应该都差不多,重要的是把IOC容器这个全局对象放基础库。
  消息发布-订阅直接抄自Prism.Wpf,但是修改了源码,添加了继承自EventArgs的RoutePubSubEventArgs,它有个Handled属性,就像路由事件一样,如果已处理就不会往下传递了,消息发布-订阅在模块之间传递消息,真的能降低很大的耦合度。
  模块化也是必要的,也是直接抄Prism.Wpf,模块化真的是很好的代码习惯,至少你不会把多个模块的代码写在一起,虽然明知道很多模块只有一个项目使用,但谁知道后面会不会多个项目使用呢,万一微软的跨平台成功了呢,你写的模块化代码是不是就能很好地迁移?
四、多分辨率适配
  该方案仅适合单个屏幕且全屏(或最大化)显示的应用,思路就是根据ViewModel动态加载当前分辨率最适合的View,假设当前分辨率是1920x1080,则创建App.ViewModels.ShellViewModel匹配的View是App.View._1920x1080.ShellView,倘若不存在,则查找邻近分辨率的View,最后再查找App.View.ShellView(自适应所有分辨率),核心代码如下:
protected override FrameworkElement CreateView()
{
    var type = this.GetType();

    // 按命名规则获取View的类型
    var name_space = type.Namespace.Replace("ViewModel", "View");
    var name = type.Name.Replace("ViewModel", "View");

    // 按最优分辨率查找
    Type view_type = null;

    foreach (var screen in Screens)
    {
        var fullName = $"{name_space}.{screen.Key}.{name}";

        view_type = type.Assembly.DefinedTypes.FirstOrDefault(q => q.FullName == fullName);

        if (view_type != null)
            break;
    }

    if (view_type == null)
    {
        var fullName = $"{name_space}.{name}";
        view_type = type.Assembly.DefinedTypes.FirstOrDefault(q => q.FullName == fullName);

        if (view_type == null)
            throw new Exception(type.FullName + "对应的View未找到!");
    }

    return Activator.CreateInstance(view_type) as FrameworkElement;
}
  最后还需要ViewBox进行拉伸,拉伸的严重程度取决于当前分辨率与支持的分辨率的差异,假设分辨率一样,则几乎是没有拉伸的;ViewBox需要放在窗口的最外层,实例代码如下:
<Window x:Class="AppAnalyser.Views.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d"
        d:DesignHeight="1080" d:DesignWidth="1920"
        x:Name="window">
    <Viewbox x:Name="viewBox" Stretch="Fill">
        <Grid x:Name="layerPanel" Width="1920" Height="1080" />
    </Viewbox>
</Window>
  这样一来,页面就不需要担心页面大小是多少了,直接按1920的设计图实现就行;假设你的应用是最大化显示的,则layerPanel的高度减去任务栏高度即可,每种分辨率的任务栏高度最好是个固定值,因为后面还要计算,这里固定40单位;
  最后适配了N种分辨率并不意味着View要写N遍,可以用下面偷懒的办法:
namespace App.Views._1280x720
{
    public class HomeView : _1920x1080.HomeView
    {
        public HomeView()
        {
            LayoutTransform = new ScaleTransform(1280d / 1920d, 680d / 1040d);
        }
    }
}
  这里是减去任务栏高度之后的缩放,注意要使用LayoutTransform。
五、应对弹窗的层级架构
  相信大多数新人弹窗都喜欢用模态的Window,Loading也用Window,遮罩也用Window,而且还喜欢用全屏的Window,虽然简单粗暴,但是体验是很不好的,下面一步步地优化吧。
  从简单的开始吧,Loading应该作为一个用户控件加载到类似Grid的面板中,这样既轻巧又可以实现一个局部Loading,在做多Tab页的应用时尤为适用;
  同理,遮罩也应该作为用户控件加载到Grid中,这样可以实现局部的遮罩,当一个窗口有多个子窗口时,遮罩还是那一个,只是全部子窗口消失时才隐藏遮罩,相比全屏窗口的遮罩,这样的好处是遮罩只有恰当的一层,不会因为多个遮罩而黑麻麻的一片,当然这样的遮罩不止是有弹窗才显示,有时Loading也需要显示遮罩,或者是其他用户控件"弹"出来,如何使遮罩兼容用户控件和真正的弹窗,我使用IMaskHost、IMaskHolder和IMaskHolderHandler等接口处理,接口的定义如下:
/// <summary>
/// 遮罩容器
/// </summary>
public interface IMaskHost
{
    void ShowMask<T>(T t) where T : IMaskHolder;

    void DismissMask<T>(T t) where T : IMaskHolder;

    IMaskHolder[] MaskHolders { get; }
}
/// <summary>
/// 遮罩持有者
/// </summary>
public interface IMaskHolder
{
    IMaskHolderHandler MaskHolderHandler { get; }
}
/// <summary>
/// 遮罩持有者处理器
/// </summary>
public interface IMaskHolderHandler
{
    bool IsActiveMaskHolder { get; set; }
    WeakDelegate<Action<bool>> PreviewSetActiveMaskHolder { get; set; }
    WeakDelegate<Action<bool>> IsActiveMaskHolderChanged { get; set; }

    Window OwnerWindow { get; set; }
    WeakDelegate<Action<Window, Window>> OwnerWindowChanged { get; set; }
}
  为了方便代码管理,这里使用了统一的遮罩处理器MaskHolderHandler,有点效仿CefSharp的IHandler吧,具体实现就不说了。
  接下来介绍如何用异步的方式实现模态窗效果的非模态窗口,演示代码如下:
public virtual Task<bool?> ShowAsync(CancellationToken token)
{
    base.Show(layerPanel);

    if (token.CanBeCanceled)
        token.Register((() => Close(null)););

    _tcs = new TaskCompletionSource<bool?>();
    return _tcs.Task;
}

public async void Close(bool? ret = null)
{
    base.Close();

    if (_tcs != null && !_tcs.Task.IsCompleted)
        _tcs.TrySetResult(ret);
}
  原理就是使用TaskCompletionSource返回一个Task,同理这种方式不止适用于Window,用户控件的"弹窗"也是这么实现的。
  到目前为止,介绍了用非模态弹窗实现模态窗效果,用户控件形式的"弹窗",以及遮罩的实现,那么把这些串起来就可以实现下面的层级架构:
 
0
  各控件之间的关系一定会符合上图的树状图关系结构,实现的层级架构实现如下功能:
  1. 假设标签页1子节点和标签页2一样,则点击标签页1时,前置标签页1的子窗口1->用户控件3->子窗口2;
  2. 在标签页2添加用户控件2时,用户控件2在用户控件1之上,并且隐藏子窗口1,在移除用户控件2之后,显示子窗口1;
  3. 子窗口1的父窗口是主窗口,当标签页2窗口化时,子窗口1的父窗口变更为标签页2所在新窗口;
  4. 关闭子窗口1时,关闭子窗口2;
  5. 当子窗口2最小化或隐藏时,用户控件3显示遮罩,点击遮罩前置显示子窗口2;
  6. 当标签页2是选中项时,即可在标签页3弹窗子窗口也会立即隐藏;
  要实现上述功能,需要确定主线,上图的主线为:主窗口->Tab标签页->标签页2->子窗口1->用户控件3->子窗口2,当选中标签页2时,依次前置后续节点,最终看到子窗口2在最前面;当主线设置为主窗口->Tab标签页->标签页2->用户控件1时,子窗口1和子窗口2自动隐藏;IMaskHost和IMaskHolder正是为此设计。
  当这里,重要项便说完了,有兴趣看的就继续看下去吧。
六、程序异常处理
  异常分为UI线程和非UI线程,使用下面方式捕获并打印这些异常:
public App()
{
    DispatcherUnhandledException += OnDispatcherUnhandledException;
    AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
    TaskScheduler.UnobservedTaskException += OnUnobservedTaskException;
}

private void OnDispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
{
    ILog.Error(e.Exception);
    e.Handled = true;   //把 Handled 属性设为true,表示此异常已处理,程序可以继续运行,不会强制退出    
}

private void OnUnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e)
{
    ILog.Error(e.Exception);
    e.SetObserved();    //设置该异常已察觉(这样处理后就不会引起程序崩溃)
}

private void OnUnhandledException(object sender, UnhandledExceptionEventArgs e)
{
    if (e.ExceptionObject is Exception ex)
        ILog.Error(ex);
}
  可以发现UI线程异常和Task异常都可以标志为已处理,防止程序退出,防止AppDomain.CurrentDomain.UnhandledException闪退的方式是修改App.config,如下修改即可:
<runtime>
    <legacyUnhandledExceptionPolicy enabled="1" />
</runtime>
  最后如何将异常信息转换为优美文本提示给用户看,毕竟总提示用户xx操作失败,却不给点具体提示吧,可以对特定的异常进行翻译,代码如下:
/// <summary>
/// 错误提示
/// </summary>
public interface IErrorTips
{
    string this[int code, params object[] args] { get; }
    string Error(Exception ex);
    string FtpError(Exception ex);
    string HttpError(Exception ex, string requestName);
}

public class ErrorTips : IErrorTips
{
    public string this[int code, params object[] args]
    {
        get
        {
            // ...异常编码表
        }
    }

    public string Error(Exception ex)
    {
        if (ex is DbException)
            return this[103102];
        if (ex is TaskCanceledException)
            return null;
        if (ex is ZipException)
            return this[103104];
        if (ex is UnzipException)
            return this[103105];

        return this[103100];
    }

    public string FtpError(Exception ex)
    {
        if (ex is WebException || ex is SocketException || ex is SshConnectionException)
            return this[100102];
        if (ex is SecurityException)
            return this[100101];
        if (ex is InvalidOperationException)
            return this[100103];

        return Error(ex);
    }

    public string HttpError(Exception ex, string requestName)
    {
        if (ex is HttpRequestException || ex is WebException)
            return this[100100, requestName];
        if (ex is SocketException && ex.InnerException is IOException)
            return this[100105];
        if (ex is HttpRequestTimeoutException)
            return this[100106];

        return Error(ex);
    }
}
七、保留系统动画的透明窗口
  相信Windows 11之后,大家实现的动画都难以操作系统动画,特别是透明的窗体被剥夺了动画等特性,如何实现保留系统动画的透明窗口?请查看必读博客:保留系统动画的透明窗口
八、子进程处理数据
  使用子进程的有很多好处,例如处理大内存文件避免主程序内存不足,32位程序与64位子进程交互,执行无法立即结束的操作(杀掉进程可立即退出),子进程不需要单独创建一个项目,只需要IpcProcess32和IpcProcess64两个控制台进程即可,公共的DLL生成为Any Cpu,这样既可以被32程序引用又可以被64程序引用,这里发现了没有?我们可以根据平台动态启动32位程序还是64位,我指是主程序。
<!--字体-->
<FontFamily x:Key="Font">Microsoft YaHei UI,微软雅黑,Times New Roman,Courier New,宋体</FontFamily>

<!--主题色-->
<SolidColorBrush x:Key="Brushes.Background" Color="#FFFFFF" options:Freeze="True" />
<SolidColorBrush x:Key="Brushes.Foreground" Color="#333333" options:Freeze="True" />
<SolidColorBrush x:Key="Brushes.Themes" Color="#0090FF" options:Freeze="True" />
<SolidColorBrush x:Key="Brushes.Themes.Highlight" Color="#0078D4" options:Freeze="True" />
<SolidColorBrush x:Key="Brushes.Themes.Foreground" Color="#FFFFFF" options:Freeze="True" />

<!--遮罩-->
<SolidColorBrush x:Key="Brushes.Mask" Color="#99000000" options:Freeze="True" />

<!--输入框-->
<Thickness x:Key="InputBox.Padding">10 5</Thickness>
<CornerRadius x:Key="InputBox.CornerRadius">4</CornerRadius>
<SolidColorBrush x:Key="Brushes.InputBox.Background" Color="#ffffff" options:Freeze="True" />
<SolidColorBrush x:Key="Brushes.InputBox.Background.Hover" Color="#ffffff" options:Freeze="True" />
<SolidColorBrush x:Key="Brushes.InputBox.Background.Focus" Color="#ffffff" options:Freeze="True" />
<SolidColorBrush x:Key="Brushes.InputBox.Background.Disabled" Color="#f1f1f1" options:Freeze="True" />
<SolidColorBrush x:Key="Brushes.InputBox.BorderBrush" Color="#c7ccd2" options:Freeze="True" />
<SolidColorBrush x:Key="Brushes.InputBox.BorderBrush.Hover" Color="#6facff" options:Freeze="True" />
<SolidColorBrush x:Key="Brushes.InputBox.BorderBrush.Focus" Color="#6facff" options:Freeze="True" />
<SolidColorBrush x:Key="Brushes.InputBox.BorderBrush.Disabled" Color="#c7ccd2" options:Freeze="True" />
<SolidColorBrush x:Key="Brushes.InputBox.Foreground" Color="#333" options:Freeze="True" />
<SolidColorBrush x:Key="Brushes.InputBox.Watermark.Foreground" Color="#a6a6a6" options:Freeze="True" />

<!--下拉框-->
<sys:Double x:Key="ComboBox.Icon.Width">14</sys:Double>
<sys:Double x:Key="ComboBoxItem.MinHeight">36</sys:Double>
<sys:Double x:Key="ComboBox.Popup.VerticalOffset">0</sys:Double>
<Thickness x:Key="ComboBox.Icon.Margin">10 0</Thickness>
<Thickness x:Key="ComboBox.Popup.BorderThickness">0</Thickness>
<CornerRadius x:Key="ComboBox.Popup.CornerRadius">4</CornerRadius>
<SolidColorBrush x:Key="Brushes.ComboBox.Icon.Fill" Color="#a5a9ae" options:Freeze="True" />
<SolidColorBrush x:Key="Brushes.ComboBox.Icon.Fill.Hover" Color="#a5a9ae" options:Freeze="True" />
<SolidColorBrush x:Key="Brushes.ComboBoxItem.Background" Color="Transparent" options:Freeze="True" />
<SolidColorBrush x:Key="Brushes.ComboBoxItem.Background.Hover" Color="#f3f3f3" options:Freeze="True" />
<SolidColorBrush x:Key="Brushes.ComboBoxItem.Background.Select" Color="#f3f3f3" options:Freeze="True" />
<SolidColorBrush x:Key="Brushes.ComboBoxItem.Foreground" Color="#333" options:Freeze="True" />
<SolidColorBrush x:Key="Brushes.ComboBoxItem.Foreground.Hover" Color="#333" options:Freeze="True" />
<SolidColorBrush x:Key="Brushes.ComboBoxItem.Foreground.Select" Color="#333" options:Freeze="True" />
<SolidColorBrush x:Key="Brushes.ComboBox.Popup.Background" Color="White" options:Freeze="True" />
<SolidColorBrush x:Key="Brushes.ComboBox.Popup.BorderBrush" Color="Transparent" options:Freeze="True" />

<!--列表-->
<Thickness x:Key="TabelCell.Padding">12 4</Thickness>
<SolidColorBrush x:Key="Brushes.TabelHeader.Foreground" Color="#333" options:Freeze="True" />
<SolidColorBrush x:Key="Brushes.TabelCell.Foreground" Color="#5c5c5c" options:Freeze="True" />
<SolidColorBrush x:Key="Brushes.ListItem.Background1" Color="#f5f6f7" options:Freeze="True" />
<SolidColorBrush x:Key="Brushes.ListItem.Background2" Color="White" options:Freeze="True" />
<SolidColorBrush x:Key="Brushes.ListItem.Background.Hover" Color="#ebf5ff" options:Freeze="True" />
<SolidColorBrush x:Key="Brushes.ListItem.Background.Select" Color="#cdeafd" options:Freeze="True" />
<SolidColorBrush x:Key="Brushes.ListItem.Line" Color="#e3e3e3" options:Freeze="True" />

<!--文字-->
<SolidColorBrush x:Key="Brushes.Foreground.Label" Color="#6f6f6f" options:Freeze="True" />
<SolidColorBrush x:Key="Brushes.Foreground.Warning" Color="#ff8f0a" options:Freeze="True" />
<SolidColorBrush x:Key="Brushes.Foreground.Danger" Color="#ed1414" options:Freeze="True" />
<SolidColorBrush x:Key="Brushes.Foreground.Link" Color="#2d8cf0" options:Freeze="True" />
<SolidColorBrush x:Key="Brushes.Foreground.Link.Hover" Color="#63afff" options:Freeze="True" />
<SolidColorBrush x:Key="Brushes.Foreground.Link.Disabled" Color="#999" options:Freeze="True" />
<SolidColorBrush x:Key="Brushes.Foreground.Link.Split" Color="#999" options:Freeze="True" />
posted @ 2022-09-26 19:05  孤独成派  阅读(2936)  评论(1编辑  收藏  举报