概览 - Snet Docs

🖥️ WPF 概览

Snet.Windows 是一个面向 .NET 8/10 Windows 桌面应用的现代化 WPF UI 库。它提供了干净、可直接用于生产环境的基础设施,帮助您以最少的样板代码构建丰富的 WPF 应用程序。

📦 包

包名 版本 描述
Snet.Windows.Core 基础库 — MVVM、主题、本地化、依赖注入、DPI 感知
Snet.Windows.Controls 控件库 — 自定义控件、属性网格、消息框、数据模型

目标框架

  • net8.0-windows
  • net10.0-windows

依赖项

依赖 版本
WPF-UI 4.3.0
MaterialDesignThemes 5.3.2
CommunityToolkit.Mvvm 8.4.2
Snet.Core

✨ 功能特性

功能 描述
MVVM 基于表达式的属性容器(BindNotify)+ CommunityToolkit.Mvvm
深色/浅色主题 一键切换主题,持久化至 config/skin.json
多语言 简体中文(zh)/ 英文(en),基于 RESX 资源
自定义控件 ButtonControlTextBoxControlComboBoxControlPropertyControl
属性网格 基于特性的属性编辑器,支持导入/导出
DPI 感知 通过 WindowBase 实现每个显示器 DPI 感知和 DWM 颜色同步
系统托盘 内置系统托盘支持
DI 集成 为 Window、UserControl 和 Page 提供依赖注入

许可证

MIT — 个人和商业用途均免费。


▶️ 快速开始

安装控件包 — 它会自动拉取 Snet.Windows.Core

dotnet add package Snet.Windows.Controls

创建一个支持主题和语言切换的最小化窗口:

MainWindow.xaml

<snet:WindowBase
    xmlns:snet="https://shunnet.top"
    LanguageEnabled="True"
    SkinEnabled="True"
    LoadAnimationEnabled="True"
    TitleLeft="True"
    VerEnabled="True">
    <Grid>
        <snet:ButtonControl
            Content="点击我"
            Command="{Binding Hello}" />
    </Grid>
</snet:WindowBase>

MainWindow.xaml.cs

public partial class MainWindow : WindowBase
{
    public MainWindow()
    {
        InitializeComponent();
    }
}

仅此而已——主题切换、语言切换、加载动画和版本显示均由基类自动处理。


⚙️ 安装与配置

NuGet 包

包名 描述 依赖项
Snet.Windows.Core 基础功能:MVVM 基础设施、主题、本地化、依赖注入、DPI 助手、系统托盘 Snet.CoreWPF-UIMaterialDesignThemesCommunityToolkit.Mvvm
Snet.Windows.Controls 控件:自定义控件、属性网格、消息框、下拉框模型、选项卡模型 Snet.Windows.Core

安装命令:

# 仅核心库(用于无界面或自定义 UI)
dotnet add package Snet.Windows.Core

# 控件库(推荐 — 会传递性地包含 Core)
dotnet add package Snet.Windows.Controls

配置文件

库会在运行时写入两个配置文件:

文件 用途
config/skin.json 持久化用户的主题选择(Dark / Light
config/language.json 持久化用户的语言选择(zh / en

无需手动配置,文件会在首次启动时自动创建。


🧠 核心概念

1. MVVM 模式

Snet.Windows 提供了两种互补的 MVVM 方法:

BindNotify — 基于表达式的属性容器

无需声明后台字段。属性值存储在以表达式树为键的内部字典中:

public class MyViewModel : BindNotify
{
    public string Name
    {
        get => GetProperty(() => Name);
        set => SetProperty(() => Name, value);
    }

    public int Count
    {
        get => GetProperty(() => Count);
        set => SetProperty(() => Count, value);
    }
}

GetProperty<T>()SetProperty<T>() 使用表达式 () => PropertyName 作为键。这消除了声明私有后台字段的需要,减少了样板代码。

CommunityToolkit.Mvvm

对于需要 ObservableObjectAsyncRelayCommandObservableValidator 的场景,该库与 CommunityToolkit.Mvvm 完全互操作。BindNotify 本身继承自 ObservableObject

public class MyViewModel : BindNotify
{
    public IAsyncRelayCommand SaveCommand => new AsyncRelayCommand(async () =>
    {
        // 异步逻辑
    });
}

2. 深色/浅色主题

SkinHandler 管理 MaterialDesignThemesWPF-UI 主题资源:

// 设置深色主题
SkinHandler.SetSkin(SkinType.Dark);

// 设置浅色主题
SkinHandler.SetSkin(SkinType.Light);

// 切换
var current = SkinHandler.GetSkin();
SkinHandler.SetSkin(current == SkinType.Dark ? SkinType.Light : SkinType.Dark);

主题选择会自动持久化到 config/skin.json。下次启动时,窗口会恢复已保存的主题。

事件: 订阅 SkinHandler.OnSkinEventSkinHandler.OnSkinEventAsync 以响应主题变化(例如,重新加载图标)。

3. 多语言

LanguageHandler 在**简体中文(zh)英文(en)**之间切换:

// 设置语言
LanguageHandler.SetLanguage(LanguageType.zh); // 中文
LanguageHandler.SetLanguage(LanguageType.en); // 英文

// 获取当前语言
var lang = LanguageHandler.GetLanguage();

// 获取本地化文本
var value = LanguageHandler.GetLanguageValue("HelloWorld", GetType().Name);

内部使用 LocalizeDictionary 进行基于 WPF 资源的本地化,并结合 RESX 文件存储字符串资源。添加新语言时,只需添加一个包含翻译字符串的 Language.xx.resx 资源文件。

4. 依赖注入

InjectionWpf 为 MVVM 提供了轻量级的 DI 容器:

// 创建 Window 及其 ViewModel
var window = InjectionWpf.Window<MyWindow, MyViewModel>();

// 创建 UserControl 及其 ViewModel
var control = InjectionWpf.UserControl<MyControl, MyViewModel>();

// 创建 Page 及其 ViewModel
var page = InjectionWpf.Page<MyPage, MyViewModel>();

cache 参数控制实例是重用(单例模式)还是每次重新创建。

5. DPI 感知

WindowBase 自动处理每个显示器的 DPI 变化:

  • 拦截 WM_GETMINMAXINFO 以根据 DPI 缩放调整最小/最大跟踪尺寸
  • 将窗口标题栏颜色与当前 DWM 强调色同步
  • 无需额外代码 —— 只需继承 WindowBase

6. 自定义控件

Snet.Windows.Controls 包在 MaterialDesign 原生控件之上提供了可扩展的封装。每个控件都是可直接拖放的 UserControl,具有支持数据绑定的依赖属性和可选的图标。


📚 API 参考

Snet.Windows.Core — WindowBase

依赖属性

属性 类型 默认值 描述
LanguageEnabled bool false 在标题栏中启用语言切换按钮
SkinEnabled bool false 在标题栏中启用主题切换按钮
TitleLeft bool false 将窗口标题移至左侧
VerEnabled bool false 在标题栏中显示版本号
LoadAnimationEnabled bool false 窗口首次加载时播放淡入动画
AnimationTime int 1000 加载动画持续时间(毫秒)
MaximizeBorderThickness Thickness (8,8,8,8) 窗口最大化时的边框厚度

方法

方法 签名 描述
WindowShake void WindowShake() 抖动窗口以提示错误或吸引注意
LoadAnimationAsync Task LoadAnimationAsync() 手动触发加载动画

Snet.Windows.Core.Mvvm — BindNotify

继承自 ObservableObject。无需显式声明后台字段。

方法

方法 描述
T GetProperty<T>(Expression<Func<T>> propertyExpression) 使用表达式作为键检索属性值
void SetProperty<T>(Expression<Func<T>> propertyExpression, T value) 设置属性值并触发 PropertyChanged
void SetProperty<T>(Expression<Func<T>> propertyExpression, T value, Action<T> callback) 设置属性值并在变更后执行回调
void SetProperty<T>(Expression<Func<T>> propertyExpression, T value, Func<T, Task> asyncCallback) 设置属性值并在变更后执行异步回调

ObservableObject.SetProperty 的关键区别: BindNotify.SetProperty 不需要后台字段。属性值存储在以表达式成员名称为键的内部 ConcurrentDictionary<string, object> 中。这意味着您只需声明属性访问器——无需单独的字段声明。


Snet.Windows.Core.Mvvm — EventCommand

将路由事件绑定到 ICommand 对象——适用于需要绑定没有 Command 属性的事件的场景。

属性 类型 描述
Command ICommand 事件触发时执行的命令
CommandParameter object 传递给命令的可选参数

在 XAML 中使用:

<snet:EventCommand Command="{Binding MouseDownCommand}" />

Snet.Windows.Core.Handler — SkinHandler

管理应用程序主题的静态类。

方法

方法 签名 描述
SetSkin void SetSkin(SkinType type, bool isSave = true) 切换主题;isSave 控制是否持久化
GetSkin SkinType GetSkin() 返回当前主题
ReplaceResources void ReplaceResources() 强制完全重建资源字典

事件

事件 类型 描述
OnSkinEvent Action<SkinType>? 主题更改后同步触发
OnSkinEventAsync Func<SkinType, Task>? 主题更改后异步触发

主题颜色

主题 主色 强调色
Dark(深色) #505050 MaterialDesign 深紫色(默认)
Light(浅色) #F5F5F5 MaterialDesign 深紫色(默认)

Snet.Windows.Core.Handler — LanguageHandler

管理应用内本地化的静态类。

方法

方法 签名 描述
GetLanguage LanguageType GetLanguage() 返回当前语言
SetLanguage void SetLanguage(LanguageType type) 切换语言并重新加载资源
GetLanguageValue string GetLanguageValue(string key, string model) 按键和模型名查找本地化字符串
GetLanguageValueAsync Task<string> GetLanguageValueAsync(string key, string model) 异步变体,用于延迟查找

支持的语言

语言 枚举值
English(英文) LanguageType.en
简体中文 LanguageType.zh

Snet.Windows.Core.Handler — IconsHandler

线程安全的图标缓存系统,支持主题变更感知。

方法

方法 签名 描述
GetIcon string GetIcon(string key, string path) 返回缓存的图标;缓存未命中时从 path 加载
Loading string Loading(string path) 从文件路径加载并缓存图标

使用 ConcurrentDictionary 实现线程安全的缓存。缓存会在主题更改时(通过 SkinHandler.OnSkinEvent)失效,从而确保图标以正确的颜色变体重新加载。


Snet.Windows.Core.Handler — InjectionWpf

用于 MVVM 三元组创建的轻量级依赖注入容器。

方法

方法 签名 描述
Window<T,M> T Window<T,M>(bool cache = false) 创建类型为 T 的 Window,使用类型为 M 的 ViewModel
UserControl<T,M> T UserControl<T,M>(bool cache = false) 创建类型为 T 的 UserControl,使用类型为 M 的 ViewModel
Page<T,M> T Page<T,M>(bool cache = false) 创建类型为 T 的 Page,使用类型为 M 的 ViewModel

Snet.Windows.Core.Enum — SkinType

名称 描述
0 Dark 深色主题 — 主色 #505050
1 Light 浅色主题 — 主色 #F5F5F5

Snet.Windows.Controls — ButtonControl

可自定义的按钮,支持可选的图标和圆角。

依赖属性

属性 类型 默认值 描述
CornerRadius CornerRadius (4,4,4,4) 边框圆角
Command ICommand null 点击按钮时执行的命令
Content string "" 按钮上显示的文字
Icon string null 显示在文字左侧的图标路径

布局

[图标 (15x15)] + [8px 间距] + [文字]

Snet.Windows.Controls — TextBoxControl

带可选图标、提示文字和清除按钮的文本输入框。

依赖属性

属性 类型 默认值 描述
Height double 30 控件高度
Icon string null 显示在文本框内的图标路径
Text string "" 绑定的文本值(默认为双向绑定)
Hint string "" 占位提示文字
ClearButtonEnabled bool true 当文本非空时显示清除(X)按钮

Snet.Windows.Controls — ComboBoxControl

带可选图标和提示文字的下拉选择器。

依赖属性

属性 类型 默认值 描述
Height double 30 控件高度
Icon string null 图标路径
Hint string "" 占位提示文字
DisplayMemberPath string "Value" 下拉列表中显示的属性名称
ItemsSource IEnumerable null 要显示的项集合
SelectedItem object null 当前选中的项(默认为双向绑定)

Snet.Windows.Controls — PropertyControl

一个卡片包裹的属性网格,支持导入/导出功能。

依赖属性

属性 类型 默认值 描述
BasicsData object null 要编辑的对象(默认为双向绑定)
ExpCommand ICommand null 导出按钮的命令
IncCommand ICommand null 导入按钮的命令
ButtonVisibility Visibility Visible 控制导入/导出按钮的可见性

方法

方法 签名 描述
GetBasics T GetBasics<T>() 返回当前数据(带类型转换)
SetBasics void SetBasics<T>(T data) 替换当前数据并刷新网格

封装了 MaterialDesign Card + 属性网格,并带有两个操作按钮(导入/导出)。使用模型类上的 [Category][Description] 特性来自动组织网格。


Snet.Windows.Controls — MessageBox

封装了 DialogHost 的静态类,用于模态对话框。

方法

Show() 有五个重载:

重载 参数
Show(string content) 仅内容文字
Show(string content, string title) 内容 + 标题
Show(string content, string title, MessageBoxButton button) 内容 + 标题 + 按钮配置
Show(string content, string title, MessageBoxButton button, MessageBoxImage image) 内容 + 标题 + 按钮 + 图标
Show(string content, string title, MessageBoxButton button, MessageBoxImage image, string ok, string cancel, string yes, string no) 完全自定义,包括自定义按钮文字

MessageBoxButton

描述
OK 单个 OK 按钮
OKCancel OK + 取消按钮
Yes 单个 Yes 按钮
YesNo Yes + No 按钮

MessageBoxImage

10 种图标类型,映射到 SystemIcons

系统图标
Information SystemIcons.Information
Warning SystemIcons.Warning
Error SystemIcons.Error
Question SystemIcons.Question
Shield SystemIcons.Shield
Asterisk SystemIcons.Asterisk
Exclamation SystemIcons.Exclamation
Hand SystemIcons.Hand
Stop SystemIcons.Error
None 无图标

Snet.Windows.Controls.Data — Models

模型 属性 适用场景
ComboBoxModel Key(string)、Value(string) 下拉列表项
EditModel Name(string)、Description(string)、Color(string) 可编辑实体
ItemsControlModel Key(string)、IsChecked(bool)、Title(string)、Content(string) 可勾选列表项
TabControlModel Title(string)、Icon(string)、Content(object) 选项卡界面

💻 代码示例

完整的 MVVM 窗口

MainWindow.xaml.cs

public partial class MainWindow : WindowBase
{
    public MainWindow()
    {
        InitializeComponent();
        DataContext = new MainViewModel();
    }
}

MainViewModel.cs

public class MainViewModel : BindNotify
{
    public string Name
    {
        get => GetProperty(() => Name);
        set => SetProperty(() => Name, value);
    }

    public IAsyncRelayCommand Hello => new AsyncRelayCommand(async () =>
    {
        await MessageBox.Show(
            $"你好, {Name}!",
            "问候",
            MessageBoxButton.OK,
            MessageBoxImage.Information);
    });
}

MainWindow.xaml

<snet:WindowBase
    xmlns:snet="https://shunnet.top"
    LanguageEnabled="True"
    SkinEnabled="True"
    LoadAnimationEnabled="True"
    AnimationTime="1500">
    <StackPanel Margin="20">
        <snet:TextBoxControl
            Text="{Binding Name}"
            Hint="请输入您的名字" />
        <snet:ButtonControl
            Margin="0,10,0,0"
            Content="打招呼"
            Command="{Binding Hello}" />
    </StackPanel>
</snet:WindowBase>

带导入/导出功能的属性网格

public class AppSettings : BindNotify
{
    [Category("常规")]
    [Description("应用程序显示名称")]
    public string AppName
    {
        get => GetProperty(() => AppName);
        set => SetProperty(() => AppName, value);
    }

    [Category("常规")]
    [Description("最大显示条目数")]
    public int MaxItems
    {
        get => GetProperty(() => MaxItems);
        set => SetProperty(() => MaxItems, value);
    }

    [Category("高级")]
    [Description("启用调试日志")]
    public bool DebugMode
    {
        get => GetProperty(() => DebugMode);
        set => SetProperty(() => DebugMode, value);
    }
}
<snet:PropertyControl
    BasicsData="{Binding Settings}"
    ExpCommand="{Binding ExportCommand}"
    IncCommand="{Binding ImportCommand}" />

从代码切换主题

private void ToggleTheme()
{
    var current = SkinHandler.GetSkin();
    SkinHandler.SetSkin(
        current == SkinType.Dark ? SkinType.Light : SkinType.Dark);
}

使用依赖注入

// 通过 DI 创建主窗口及其 ViewModel
var mainWindow = InjectionWpf.Window<MainWindow, MainViewModel>();

// 等价于 Application.Run
mainWindow.ShowDialog();

❓ 常见问题

1. 如何使用 MVVM 创建新窗口?

使用 InjectionWpf.Window<MyWindow, MyViewModel>()。它会构造 View 和 ViewModel,绑定 DataContext,并返回完全初始化的窗口。

2. 如何以编程方式切换主题?

SkinHandler.SetSkin(SkinType.Light);  // 浅色主题
SkinHandler.SetSkin(SkinType.Dark);   // 深色主题

选择会持久化到 config/skin.json,并在下次启动时自动恢复。

3. 如何添加新语言?

  1. 添加一个包含翻译字符串的 Language.xx.resx 资源文件
  2. xx 变体添加到 LanguageType 枚举中(如果尚未存在)
  3. 在启动时调用 LanguageHandler.SetLanguage(LanguageType.xx)

库默认提供 Language.zh.resx(中文)和 Language.en.resx(英文)。

4. BindNotify.GetProperty/SetPropertyObservableObject.SetProperty 有什么区别?

BindNotify.GetProperty<T>(() => Property) / SetProperty<T>(() => Property, value) 使用基于表达式的属性容器——值存储在以表达式成员名称为键的内部字典中。您无需声明私有后台字段。

CommunityToolkit.Mvvm 的 ObservableObject.SetProperty<T>(ref field, value) 需要一个显式的后台字段,并通过引用传递它。

方法 后台字段 样板代码
BindNotify.SetProperty 不需要 较少
ObservableObject.SetProperty 需要(private T _field; 较多

5. 如何使用属性网格(PropertyGrid)?

BasicsData 属性设置为任何使用 System.ComponentModel 中的 [Category][Description] 特性装饰的对象:

using System.ComponentModel;

public class MyConfig
{
    [Category("常规")]
    [Description("应用程序标题")]
    public string Title { get; set; }
}

PropertyControl 会自动生成带有分类的可编辑网格。使用 GetBasics<T>() 获取当前值,使用 SetBasics<T>() 替换它们。

6. 是否支持 .NET 8 和 .NET 10?

是的。这些包同时面向 net8.0-windowsnet10.0-windows。您可以在任一目标框架上使用相同版本的包。


📅 版本历史

日期 版本 变更
2026-07-23 当前版本 — .NET 8 / .NET 10 支持,WPF-UI 4.3.0,MaterialDesignThemes 5.3.2,CommunityToolkit.Mvvm 8.4.2