🖥️ WPF 概览
Snet.Windows 是一个面向 .NET 8/10 Windows 桌面应用的现代化 WPF UI 库。它提供了干净、可直接用于生产环境的基础设施,帮助您以最少的样板代码构建丰富的 WPF 应用程序。
📦 包
| 包名 | 版本 | 描述 |
|---|---|---|
Snet.Windows.Core |
— | 基础库 — MVVM、主题、本地化、依赖注入、DPI 感知 |
Snet.Windows.Controls |
— | 控件库 — 自定义控件、属性网格、消息框、数据模型 |
目标框架
net8.0-windowsnet10.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 资源 |
| 自定义控件 | ButtonControl、TextBoxControl、ComboBoxControl、PropertyControl |
| 属性网格 | 基于特性的属性编辑器,支持导入/导出 |
| 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.Core、WPF-UI、MaterialDesignThemes、CommunityToolkit.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
对于需要 ObservableObject、AsyncRelayCommand 或 ObservableValidator 的场景,该库与 CommunityToolkit.Mvvm 完全互操作。BindNotify 本身继承自 ObservableObject。
public class MyViewModel : BindNotify
{
public IAsyncRelayCommand SaveCommand => new AsyncRelayCommand(async () =>
{
// 异步逻辑
});
}
2. 深色/浅色主题
SkinHandler 管理 MaterialDesignThemes 和 WPF-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.OnSkinEvent 或 SkinHandler.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. 如何添加新语言?
- 添加一个包含翻译字符串的
Language.xx.resx资源文件 - 将
xx变体添加到LanguageType枚举中(如果尚未存在) - 在启动时调用
LanguageHandler.SetLanguage(LanguageType.xx)
库默认提供 Language.zh.resx(中文)和 Language.en.resx(英文)。
4. BindNotify.GetProperty/SetProperty 和 ObservableObject.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-windows 和 net10.0-windows。您可以在任一目标框架上使用相同版本的包。
📅 版本历史
| 日期 | 版本 | 变更 |
|---|---|---|
| 2026-07-23 | — | 当前版本 — .NET 8 / .NET 10 支持,WPF-UI 4.3.0,MaterialDesignThemes 5.3.2,CommunityToolkit.Mvvm 8.4.2 |
