A:简化HandyControl引用

This commit is contained in:
jimmy committed 2025-12-22 20:20:19 +08:00
1 parent 2563288afe
commit 37aa848b5f
13 files changed
+1636 -289

No files matched your search

+1 -4
View File
@@ -1,12 +1,9 @@
<Application x:Class="Client.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!-- HandyControl 主题资源已通过 AddSharpHandyControl() 自动注册,无需手动添加 -->
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/SkinDefault.xaml" />
<ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/Theme.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>
+2 -1
View File
@@ -12,6 +12,7 @@ using Sharp.Log.Extensions;
using Sharp.NetServer.DependencyInjection;
using Sharp.Wpf.Services;
using Sharp.Wpf.Abstractions;
using Sharp.Wpf.HandyControl.DependencyInjection;
using Plc.Siemens.DependencyInjection;
using Microsoft.Extensions.Configuration;
using Sharp.Process.DependencyInjection;
@@ -114,7 +115,7 @@ namespace Client
services.AddSharpPython();
// WPF app services
services.AddSingleton<IDialogService, DialogService>();
services.AddSharpHandyControl(); // HandyControl 对话框服务(也注册了 IDialogService)
services.AddSingleton<IAuthenticationService, DemoAuthenticationService>();
services.AddSingleton<IUserSession, UserSession>();
// Register DispatcherService with the current UI dispatcher
+1 -1
View File
@@ -11,11 +11,11 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0" />
<PackageReference Include="HandyControl" Version="3.5.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Wpf\Wpf.csproj" />
<ProjectReference Include="..\Wpf.HandyControl\Wpf.HandyControl.csproj" />
<ProjectReference Include="..\Config\Config.csproj" />
<ProjectReference Include="..\NetClient\NetClient.csproj" />
<ProjectReference Include="..\NetServer\NetServer.csproj" />
+1 -4
View File
@@ -2,12 +2,9 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Server">
<!-- HandyControl 主题资源已通过 AddSharpHandyControl() 自动注册,无需手动添加 -->
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/SkinDefault.xaml" />
<ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/Theme.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>
+2 -1
View File
@@ -9,6 +9,7 @@ using Sharp.Data;
using Sharp.Data.Context;
using Sharp.Wpf.Services;
using Sharp.Wpf.Abstractions;
using Sharp.Wpf.HandyControl.DependencyInjection;
using Server.Config;
using Server.Services;
using Server.Data;
@@ -120,7 +121,7 @@ namespace Server
services.AddSingleton<ServerService>();
// WPF服务
services.AddSingleton<IDialogService, DialogService>();
services.AddSharpHandyControl(); // HandyControl 对话框服务(也注册了 IDialogService)
services.AddSingleton<IDispatcher>(new DispatcherService(Dispatcher));
// ViewModels
+1 -1
View File
@@ -20,6 +20,7 @@
<ProjectReference Include="..\Log\Log.csproj" />
<ProjectReference Include="..\Config\Config.csproj" />
<ProjectReference Include="..\Wpf\Wpf.csproj" />
<ProjectReference Include="..\Wpf.HandyControl\Wpf.HandyControl.csproj" />
<ProjectReference Include="..\Common\Common.csproj" />
<ProjectReference Include="..\Data\Data.csproj" />
</ItemGroup>
@@ -28,7 +29,6 @@
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
<PackageReference Include="HandyControl" Version="3.5.0" />
</ItemGroup>
<ItemGroup>
@@ -0,0 +1,105 @@
using System.Windows;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Sharp.Wpf.HandyControl.Options;
using Sharp.Wpf.HandyControl.Services;
using Sharp.Wpf.Services;
namespace Sharp.Wpf.HandyControl.DependencyInjection;
/// <summary>
/// HandyControl 服务注册扩展
/// </summary>
public static class HandyControlServiceCollectionExtensions
{
/// <summary>
/// 添加 Sharp.Wpf.HandyControl 服务
/// 默认会自动注册 HandyControl 主题资源到 Application.Resources,无需在 App.xaml 中手动添加
/// 约定配置节名称:HandyControl
/// </summary>
/// <param name="services">服务集合</param>
/// <returns>服务集合</returns>
public static IServiceCollection AddSharpHandyControl(this IServiceCollection services)
{
return services.AddSharpHandyControl(options => { });
}
/// <summary>
/// 添加 Sharp.Wpf.HandyControl 服务(带配置选项)
/// 默认会自动注册 HandyControl 主题资源到 Application.Resources,无需在 App.xaml 中手动添加
/// </summary>
/// <param name="services">服务集合</param>
/// <param name="configure">配置委托</param>
/// <returns>服务集合</returns>
public static IServiceCollection AddSharpHandyControl(
this IServiceCollection services,
Action<HandyControlOptions> configure)
{
var options = new HandyControlOptions();
configure(options);
// 自动注册主题资源(如果启用)
if (options.AutoRegisterTheme)
{
RegisterTheme(options.Theme);
}
// 注册配置选项
services.TryAddSingleton(options);
// 注册 HandyControl 对话框服务
services.TryAddSingleton<IHandyDialogService, HandyDialogService>();
// 也注册为基础的 IDialogService,方便兼容使用
services.TryAddSingleton<IDialogService>(sp => sp.GetRequiredService<IHandyDialogService>());
return services;
}
/// <summary>
/// 注册 HandyControl 主题资源
/// </summary>
private static void RegisterTheme(HandyControlTheme theme)
{
try
{
Application.Current.Dispatcher.Invoke(() =>
{
// 检查是否已经添加过 HandyControl 主题资源
var exists = Application.Current.Resources.MergedDictionaries
.Any(d => d.Source?.OriginalString.Contains("HandyControl") == true);
if (!exists)
{
// 根据主题类型选择皮肤文件
var skinUri = theme switch
{
HandyControlTheme.Light => "pack://application:,,,/HandyControl;component/Themes/SkinDefault.xaml",
HandyControlTheme.Dark => "pack://application:,,,/HandyControl;component/Themes/SkinDark.xaml",
_ => "pack://application:,,,/HandyControl;component/Themes/SkinDefault.xaml"
};
// 添加皮肤资源
var skinDictionary = new ResourceDictionary
{
Source = new Uri(skinUri)
};
// 添加主题资源
var themeDictionary = new ResourceDictionary
{
Source = new Uri("pack://application:,,,/HandyControl;component/Themes/Theme.xaml")
};
Application.Current.Resources.MergedDictionaries.Add(skinDictionary);
Application.Current.Resources.MergedDictionaries.Add(themeDictionary);
}
});
}
catch (Exception ex)
{
// 如果在非 UI 线程或 Application 未初始化时调用,记录警告但不中断程序
System.Diagnostics.Debug.WriteLine($"警告: 无法自动注册 HandyControl 主题资源: {ex.Message}");
}
}
}
@@ -0,0 +1,28 @@
namespace Sharp.Wpf.HandyControl.Options;
/// <summary>
/// HandyControl 配置选项
/// </summary>
public class HandyControlOptions
{
/// <summary>
/// 是否自动注册主题资源(默认:true)
/// 如果为 true,会在调用 AddSharpHandyControl() 时自动添加主题到 Application.Resources
/// </summary>
public bool AutoRegisterTheme { get; set; } = true;
/// <summary>
/// 主题类型(默认:Light 浅色主题)
/// </summary>
public HandyControlTheme Theme { get; set; } = HandyControlTheme.Light;
/// <summary>
/// 默认 Growl 容器标识(默认:"Default")
/// </summary>
public string DefaultGrowlToken { get; set; } = "Default";
/// <summary>
/// Growl 通知显示时长(秒)(默认:3)
/// </summary>
public int GrowlWaitTime { get; set; } = 3;
}
@@ -0,0 +1,17 @@
namespace Sharp.Wpf.HandyControl.Options;
/// <summary>
/// HandyControl 主题类型
/// </summary>
public enum HandyControlTheme
{
/// <summary>
/// 浅色主题(默认)
/// </summary>
Light,
/// <summary>
/// 深色主题
/// </summary>
Dark
}
+1142 -277
View File
@@ -4,15 +4,41 @@
HandyControl UI库集成模块,为 Sharp.Wpf 提供 HandyControl 的便捷集成和扩展。
## 📋 模块说明
---
本模块基于 [HandyControl](https://github.com/HandyOrg/HandyControl) UI库,提供:
- HandyControl 主题集成
- 便捷的服务注册扩展
- 常用对话框封装
- 自定义控件扩展
## 📋 目录
## 🎯 为什么要拆分?
- [模块介绍](#-模块介绍)
- [为什么选择这个模块](#-为什么选择这个模块)
- [快速开始](#-快速开始)
- [核心 API](#-核心-api)
- [典型使用场景](#-典型使用场景)
- [配置选项](#️-配置选项)
- [Growl 通知配置](#-growl-通知配置)
- [主题定制](#-主题定制)
- [迁移指南](#-迁移指南)
- [常见问题](#-常见问题)
- [最佳实践](#-最佳实践)
- [API 参考](#-api-参考)
- [参考资料](#-参考资料)
---
## 📖 模块介绍
### 什么是 Sharp.Wpf.HandyControl?
本模块基于 [HandyControl 3.5.0](https://github.com/HandyOrg/HandyControl) UI库,为 WPF 应用提供现代化的 UI 组件和对话框服务。通过依赖注入的方式,让你用最少的代码享受 HandyControl 的所有功能。
### 核心特性
- ✅ **一行代码集成** - 自动注册主题资源,无需修改 App.xaml
- ✅ **依赖注入友好** - 完全基于 DI,支持单元测试
- ✅ **丰富的对话框 API** - 成功/信息/警告/错误/确认对话框
- ✅ **Growl 通知系统** - 优雅的右下角弹出通知
- ✅ **主题切换** - 支持浅色/深色主题一键切换
- ✅ **兼容性好** - 实现 IDialogService 接口,可无缝替换其他实现
- ✅ **线程安全** - 自动处理 UI 线程调度
### 模块分离策略
@@ -24,10 +50,10 @@ Sharp.Wpf (Core)
└── 行为(EventToCommand)
Sharp.Wpf.HandyControl(本模块)
├── HandyControl主题配置
├── IDialogService的HandyControl实现
├── HandyControl控件扩展
└── 样式和资源
├── HandyControl主题自动配置
├── IDialogService 的 HandyControl 实现
├── HandyControl 特色功能封装(Growl、深色主题等)
└── DI 扩展方法(AddSharpHandyControl)
```
**优势**:
@@ -37,82 +63,190 @@ Sharp.Wpf.HandyControl(本模块)
---
## 🚀 快速开始
## 🎯 为什么选择这个模块?
### 1. 安装包
```bash
# 安装 Sharp.Wpf(必需)
Install-Package Sharp.Wpf
### ❌ 传统方式的痛点
# 安装 Sharp.Wpf.HandyControl(可选)
Install-Package Sharp.Wpf.HandyControl
```
### 2. 添加主题资源
```xml
<!-- App.xaml -->
<Application x:Class="MyApp.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:hc="https://handyorg.github.io/handycontrol">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<!-- HandyControl 主题 -->
<hc:ThemeResources/>
<hc:Theme/>
<!-- 可选:深色主题 -->
<ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/SkinDark.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>
```
### 3. 注册服务
**直接使用 HandyControl:**
```csharp
// App.xaml.cs
protected override void OnStartup(StartupEventArgs e) {
var services = new ServiceCollection();
// App.xaml - 需要手动配置主题
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/SkinDefault.xaml" />
<ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/Theme.xaml" />
</ResourceDictionary.MergedDictionaries>
// 注册基础 Wpf 服务
services.AddWpfServices();
// 代码中 - 静态调用,难以测试
using HandyControl.Controls;
MessageBox.Show("消息", "标题");
MessageBox.Error("错误信息");
```
// 注册 HandyControl 集成服务
services.AddHandyControlServices();
**问题:**
- ❌ 需要手动配置 XAML 资源字典
- ❌ 静态调用,无法进行单元测试
- ❌ 不支持依赖注入
- ❌ 主题切换不方便
- ❌ 在 ViewModel 中直接引用 UI 库
var provider = services.BuildServiceProvider();
### ✅ 使用本模块的优势
```csharp
// App.xaml.cs - 一行代码搞定
services.AddSharpHandyControl();
// ViewModel - 依赖注入,易于测试
public class MyViewModel
{
private readonly IHandyDialogService _dialog;
public MyViewModel(IHandyDialogService dialog)
{
_dialog = dialog;
}
private void ShowMessage()
{
_dialog.ShowSuccess("操作成功!");
}
}
```
### 4. 使用 HandyControl 对话框
**优势:**
- ✅ 无需配置 App.xaml,自动注册主题
- ✅ 完全基于依赖注入,支持单元测试
- ✅ ViewModel 不直接依赖 UI 库
- ✅ 主题切换只需修改一行配置
- ✅ 统一的对话框 API,易于维护
---
## 🚀 快速开始
### 第 1 步:注册服务(一行代码)
**真正的一行代码集成 - 无需修改 App.xaml!**
```csharp
public class MainViewModel : ViewModelBase {
private readonly IHandyDialogService _dialogService;
// App.xaml.cs
using Microsoft.Extensions.DependencyInjection;
using Sharp.Wpf.HandyControl.DependencyInjection;
public MainViewModel(IHandyDialogService dialogService) {
_dialogService = dialogService;
namespace MyApp
{
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
var services = new ServiceCollection();
// ← 一行搞定,连 App.xaml 都不用改!
services.AddSharpHandyControl();
// 注册你的 ViewModels 和服务
services.AddTransient<MainViewModel>();
services.AddTransient<MainWindow>();
var provider = services.BuildServiceProvider();
// 显示主窗口
var mainWindow = provider.GetRequiredService<MainWindow>();
mainWindow.Show();
}
}
}
```
private void ShowMessage() {
// HandyControl 样式的消息框
_dialogService.ShowSuccess("操作成功!");
_dialogService.ShowInfo("这是一条信息");
_dialogService.ShowWarning("警告信息");
_dialogService.ShowError("错误信息");
**✨ 自动完成的事情:**
- ✅ 注册 HandyControl 主题资源到 `Application.Resources`
- ✅ 注册 `IHandyDialogService` 服务
- ✅ 注册 `IDialogService` 服务(兼容基础接口)
- ✅ 配置默认的浅色主题
### 第 2 步:在 ViewModel 中使用
```csharp
using Sharp.Wpf.HandyControl.Services;
using Sharp.Wpf.ViewModels;
namespace MyApp.ViewModels
{
public class MainViewModel : ViewModelBase
{
private readonly IHandyDialogService _dialog;
public MainViewModel(IHandyDialogService dialog)
{
_dialog = dialog;
SaveCommand = new AsyncCommand(SaveAsync);
DeleteCommand = new AsyncCommand(DeleteAsync);
}
public IAsyncCommand SaveCommand { get; }
public IAsyncCommand DeleteCommand { get; }
private async Task SaveAsync()
{
IsBusy = true;
try
{
// 执行保存操作
await Task.Delay(1000); // 模拟保存
_dialog.ShowSuccess("保存成功!");
}
catch (Exception ex)
{
_dialog.ShowError($"保存失败:{ex.Message}");
}
finally
{
IsBusy = false;
}
}
private async Task DeleteAsync()
{
// 显示确认对话框
var confirmed = await _dialog.ConfirmAsync(
"确定要删除这条记录吗?\n删除后无法恢复。",
"确认删除"
);
if (confirmed)
{
// 执行删除操作
_dialog.ShowSuccess("删除成功");
}
}
}
}
```
private async Task<bool> Confirm() {
// 确认对话框
return await _dialogService.ConfirmAsync("确定要删除吗?", "确认删除");
}
### 第 3 步:在 Window 中使用(可选)
private async Task ShowInputDialog() {
// 输入对话框
var result = await _dialogService.ShowInputAsync("请输入名称:", "新建项目");
if (result.IsSuccess) {
ProjectName = result.Value;
```csharp
using Sharp.Wpf.HandyControl.Services;
namespace MyApp.Views
{
public partial class MainWindow : Window
{
private readonly IHandyDialogService _dialog;
public MainWindow(MainViewModel viewModel, IHandyDialogService dialog)
{
InitializeComponent();
DataContext = viewModel;
_dialog = dialog;
}
private void OnHelpButtonClick(object sender, RoutedEventArgs e)
{
_dialog.ShowInfo("这是帮助信息...", "帮助");
}
}
}
@@ -123,55 +257,86 @@ public class MainViewModel : ViewModelBase {
## 📚 核心 API
### IHandyDialogService 接口
```csharp
public interface IHandyDialogService : IDialogService {
public interface IHandyDialogService : IDialogService
{
// ========== 消息框(阻塞式) ==========
/// <summary>
/// 显示成功消息(绿色,带勾图标)
/// 显示成功消息(绿色图标)
/// </summary>
void ShowSuccess(string message, string? title = null);
/// <summary>
/// 显示信息消息(蓝色,带i图标)
/// 显示信息消息(蓝色图标)
/// </summary>
void ShowInfo(string message, string? title = null);
/// <summary>
/// 显示警告消息(橙色,带!图标)
/// 显示警告消息(橙色图标)
/// </summary>
void ShowWarning(string message, string? title = null);
/// <summary>
/// 显示错误消息(红色,带X图标)
/// 显示错误消息(红色图标)
/// </summary>
void ShowError(string message, string? title = null);
// ========== 对话框(异步) ==========
/// <summary>
/// 显示确认对话框
/// 显示确认对话框(确定/取消)
/// </summary>
/// <returns>用户点击"确定"返回 true,"取消"返回 false</returns>
Task<bool> ConfirmAsync(string message, string? title = null);
/// <summary>
/// 显示输入对话框
/// 显示询问对话框(是/否/取消)
/// </summary>
Task<Result<string>> ShowInputAsync(
string message,
string? title = null,
string? defaultValue = null);
/// <returns>HandyDialogResult 枚举值</returns>
Task<HandyDialogResult> AskAsync(string message, string? title = null);
/// <summary>
/// 显示等待对话框(异步操作)
/// 显示输入对话框(尚未实现)
/// </summary>
Task ShowWaitingAsync(string message, Func<Task> action);
Task<Result<string>> ShowInputAsync(string message, string? title = null, string? defaultValue = null);
// ========== Growl 通知(非阻塞) ==========
/// <summary>
/// 显示进度对话框
/// 显示 Growl 通知(右下角弹出)
/// </summary>
IProgress<double> ShowProgress(string message, string? title = null);
/// <param name="message">通知消息</param>
/// <param name="type">通知类型</param>
/// <param name="token">Growl 容器标识(可选)</param>
void ShowNotification(string message, NotificationType type = NotificationType.Info, string? token = null);
}
```
/// <summary>
/// 显示通知(Growl,右下角弹出)
/// </summary>
void ShowNotification(string message, NotificationType type = NotificationType.Info);
### NotificationType 枚举
```csharp
public enum NotificationType
{
Info, // 信息(蓝色图标)
Success, // 成功(绿色图标)
Warning, // 警告(橙色图标)
Error, // 错误(红色图标)
Fatal // 严重错误(深红色图标)
}
```
### HandyDialogResult 枚举
```csharp
public enum HandyDialogResult
{
None, // 未选择
OK, // 确定
Yes, // 是
No, // 否
Cancel // 取消
}
```
@@ -179,213 +344,487 @@ public interface IHandyDialogService : IDialogService {
## 💡 典型使用场景
### 场景1:操作结果提示
```csharp
public class ProductViewModel : ViewModelBase {
private readonly IHandyDialogService _dialog;
### 场景 1:表单保存操作
private async Task SaveAsync() {
```csharp
public class UserEditViewModel : ViewModelBase
{
private readonly IHandyDialogService _dialog;
private readonly IUserService _userService;
public UserEditViewModel(IHandyDialogService dialog, IUserService userService)
{
_dialog = dialog;
_userService = userService;
SaveCommand = new AsyncCommand(SaveAsync, CanSave);
}
public IAsyncCommand SaveCommand { get; }
private bool CanSave() => !string.IsNullOrEmpty(UserName) && !string.IsNullOrEmpty(Email);
private async Task SaveAsync()
{
IsBusy = true;
var result = await _productService.SaveAsync(Product);
try
{
// 验证
if (!IsValidEmail(Email))
{
_dialog.ShowWarning("请输入有效的邮箱地址", "验证失败");
return;
}
if (result.IsSuccess) {
_dialog.ShowSuccess("保存成功!");
} else {
_dialog.ShowError($"保存失败:{result.ErrorMessage}");
// 保存
var result = await _userService.SaveUserAsync(new User
{
Name = UserName,
Email = Email
});
if (result.IsSuccess)
{
_dialog.ShowSuccess("用户信息保存成功!");
// 可选:使用 Growl 通知(不阻塞UI)
// _dialog.ShowNotification("用户信息已保存", NotificationType.Success);
}
else
{
_dialog.ShowError($"保存失败:{result.ErrorMessage}", "错误");
}
}
catch (Exception ex)
{
_dialog.ShowError($"发生异常:{ex.Message}", "系统错误");
}
finally
{
IsBusy = false;
}
}
}
```
### 场景 2:删除确认
```csharp
public class ProductListViewModel : ViewModelBase
{
private readonly IHandyDialogService _dialog;
private readonly IRepository<Product> _repository;
public ProductListViewModel(IHandyDialogService dialog, IRepository<Product> repository)
{
_dialog = dialog;
_repository = repository;
DeleteCommand = new AsyncCommand(DeleteAsync, () => SelectedProduct != null);
}
public Product? SelectedProduct { get; set; }
public IAsyncCommand DeleteCommand { get; }
private async Task DeleteAsync()
{
if (SelectedProduct == null) return;
// 显示确认对话框
var confirmed = await _dialog.ConfirmAsync(
$"确定要删除产品 \"{SelectedProduct.Name}\" 吗?\n\n删除后无法恢复。",
"确认删除"
);
if (confirmed)
{
try
{
await _repository.RemoveAsync(SelectedProduct.Id);
// 从列表中移除
Products.Remove(SelectedProduct);
_dialog.ShowSuccess("产品删除成功");
}
catch (Exception ex)
{
_dialog.ShowError($"删除失败:{ex.Message}", "错误");
}
}
}
}
```
### 场景 3:批量操作进度通知
```csharp
public class BatchProcessViewModel : ViewModelBase
{
private readonly IHandyDialogService _dialog;
public BatchProcessViewModel(IHandyDialogService dialog)
{
_dialog = dialog;
ProcessCommand = new AsyncCommand(ProcessAsync);
}
public IAsyncCommand ProcessCommand { get; }
private async Task ProcessAsync()
{
IsBusy = true;
var items = GetSelectedItems();
int successCount = 0;
int failCount = 0;
foreach (var item in items)
{
try
{
await ProcessItemAsync(item);
successCount++;
// 使用 Growl 显示进度(不阻塞)
_dialog.ShowNotification(
$"已处理 {successCount}/{items.Count}",
NotificationType.Info
);
}
catch (Exception)
{
failCount++;
}
}
IsBusy = false;
// 显示最终结果
if (failCount == 0)
{
_dialog.ShowSuccess($"批量处理完成!\n成功:{successCount} 项");
}
else
{
_dialog.ShowWarning(
$"批量处理完成,但有部分失败\n\n成功:{successCount} 项\n失败:{failCount} 项",
"处理完成"
);
}
}
}
```
---
### 场景 4:后台任务通知
### 场景2:删除确认
```csharp
private async Task DeleteAsync() {
var confirmed = await _dialog.ConfirmAsync(
"确定要删除此项吗?\n删除后无法恢复。",
"确认删除"
);
if (confirmed) {
await _repository.RemoveAsync(SelectedItem.Id);
_dialog.ShowSuccess("删除成功");
await RefreshAsync();
}
}
```
---
### 场景3:输入对话框
```csharp
private async Task RenameAsync() {
var result = await _dialog.ShowInputAsync(
"请输入新名称:",
"重命名",
defaultValue: SelectedItem.Name
);
if (result.IsSuccess && !string.IsNullOrEmpty(result.Value)) {
SelectedItem.Name = result.Value;
await _repository.UpdateAsync(SelectedItem);
}
}
```
---
### 场景4:等待对话框
```csharp
private async Task ExportDataAsync() {
await _dialog.ShowWaitingAsync("正在导出数据,请稍候...", async () => {
// 耗时操作
await Task.Delay(3000);
var data = await _repository.GetAllAsync();
await _excelService.ExportAsync(data, "export.xlsx");
});
_dialog.ShowSuccess("导出完成!");
}
```
---
### 场景5:进度对话框
```csharp
private async Task BatchProcessAsync() {
var items = await _repository.GetAllAsync();
var progress = _dialog.ShowProgress("正在处理...", "批量处理");
for (int i = 0; i < items.Count; i++) {
await ProcessItemAsync(items[i]);
progress.Report((i + 1) * 100.0 / items.Count); // 0-100
}
_dialog.ShowSuccess($"处理完成,共 {items.Count} 项");
}
```
---
### 场景6:通知(Growl)
```csharp
public class NotificationService {
public class BackgroundService
{
private readonly IHandyDialogService _dialog;
// 右下角弹出通知(不阻塞)
public void NotifyNewMessage(string message) {
_dialog.ShowNotification($"新消息:{message}", NotificationType.Info);
public BackgroundService(IHandyDialogService dialog)
{
_dialog = dialog;
}
public void NotifyAlarm(string alarmMessage) {
_dialog.ShowNotification($"报警:{alarmMessage}", NotificationType.Warning);
public void StartMonitoring()
{
// 监听PLC报警
_plc.OnAlarm += (sender, alarm) =>
{
_dialog.ShowNotification(
$"PLC报警:{alarm.Message}",
NotificationType.Warning
);
};
// 监听新消息
_messageHub.OnNewMessage += (sender, message) =>
{
_dialog.ShowNotification(
$"新消息:{message.Content}",
NotificationType.Info
);
};
// 监听系统错误
_errorHandler.OnCriticalError += (sender, error) =>
{
_dialog.ShowNotification(
$"系统错误:{error.Message}",
NotificationType.Fatal
);
};
}
}
```
---
### 场景 5:三选项对话框
## 🔧 高级功能
### 1. 主题切换
```csharp
public class ThemeService {
public void SwitchToLightTheme() {
SharedResourceDictionary.SharedDictionary.Source =
new Uri("pack://application:,,,/HandyControl;component/Themes/SkinDefault.xaml");
}
private async Task OnFileSaveAsync()
{
if (HasUnsavedChanges)
{
var result = await _dialog.AskAsync(
"文件已修改,是否保存?",
"保存确认"
);
public void SwitchToDarkTheme() {
SharedResourceDictionary.SharedDictionary.Source =
new Uri("pack://application:,,,/HandyControl;component/Themes/SkinDark.xaml");
switch (result)
{
case HandyDialogResult.Yes:
await SaveFileAsync();
CloseWindow();
break;
case HandyDialogResult.No:
CloseWindow();
break;
case HandyDialogResult.Cancel:
// 用户取消,不执行任何操作
break;
}
}
}
```
---
### 2. 自定义对话框
```csharp
public class CustomDialogViewModel : ViewModelBase {
public string UserInput { get; set; } = "";
public ICommand ConfirmCommand { get; }
public ICommand CancelCommand { get; }
}
// 使用
var viewModel = new CustomDialogViewModel();
var result = await _dialog.ShowDialogAsync(viewModel, "自定义对话框");
```
---
### 3. 窗口动画
```csharp
// HandyControl 提供的窗口动画
public class AnimatedWindow : Window {
public AnimatedWindow() {
WindowStartupLocation = WindowStartupLocation.CenterScreen;
// 启用淡入淡出动画
this.SetValue(ControlBehavior.ShowInTaskManagerProperty, false);
this.SetValue(ControlBehavior.ShowInTaskManagerProperty, false);
}
}
```
---
## 📦 包含的 HandyControl 控件扩展
### 常用控件
- **MessageBox** - 样式化消息框
- **Growl** - 通知弹窗
- **Dialog** - 对话框容器
- **LoadingCircle** - 加载动画
- **ProgressBar** - 进度条
- **Badge** - 徽章
- **Tag** - 标签
- **Shield** - 盾牌图标
- **TextBox** - 增强文本框(带水印、验证)
- **DatePicker** - 日期选择器
- **TimePicker** - 时间选择器
- **ComboBox** - 下拉框
---
## ⚙️ 配置选项
### 完整配置示例
```csharp
services.AddHandyControlServices(options => {
// 默认对话框选项
options.DefaultDialogWidth = 400;
options.DefaultDialogHeight = 200;
services.AddSharpHandyControl(options =>
{
// 是否自动注册主题资源(默认:true)
// 如果为 false,需要在 App.xaml 中手动添加主题资源
options.AutoRegisterTheme = true;
// Growl 配置
options.GrowlToken = "MainGrowl"; // Growl容器名称
options.GrowlPosition = GrowlPosition.BottomRight;
// 主题类型(默认:Light 浅色主题)
options.Theme = HandyControlTheme.Light; // 或 HandyControlTheme.Dark
// 主题配置
options.DefaultTheme = Theme.Light;
// 默认 Growl 容器标识(默认:"Default")
// 对应 XAML 中 <hc:Growl Token="Default" />
options.DefaultGrowlToken = "Default";
// Growl 通知显示时长(秒)(默认:3)
options.GrowlWaitTime = 3;
});
```
---
### 使用深色主题
## 🎨 样式定制
```csharp
// 方式 1:全局深色主题
services.AddSharpHandyControl(options =>
{
options.Theme = HandyControlTheme.Dark;
});
// 方式 2:根据系统主题动态选择
services.AddSharpHandyControl(options =>
{
var isDarkMode = IsSystemDarkMode();
options.Theme = isDarkMode ? HandyControlTheme.Dark : HandyControlTheme.Light;
});
```
### 禁用自动主题注册
如果你想完全控制主题配置(例如使用自定义主题),可以禁用自动注册:
```csharp
services.AddSharpHandyControl(options =>
{
options.AutoRegisterTheme = false; // 禁用自动注册
});
```
然后在 `App.xaml` 中手动配置:
### 自定义主题颜色
```xml
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<hc:ThemeResources AccentColor="#FF0078D7"/>
<hc:Theme/>
<!-- 自定义主题 -->
<ResourceDictionary Source="Themes/MyCustomTheme.xaml" />
<ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/Theme.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
```
### 多窗口 Growl 配置
如果你有多个窗口,可以为每个窗口配置独立的 Growl 容器:
```csharp
// 主窗口使用默认 Token
_dialog.ShowNotification("主窗口通知", NotificationType.Info);
// 子窗口使用自定义 Token
_dialog.ShowNotification("子窗口通知", NotificationType.Info, token: "ChildWindow");
```
```xml
<!-- MainWindow.xaml -->
<hc:Growl Token="Default" ... />
<!-- ChildWindow.xaml -->
<hc:Growl Token="ChildWindow" ... />
```
---
## 🎨 Growl 通知配置
### 基本配置
Growl 是 HandyControl 的特色功能,显示在窗口右上角的优雅通知。
#### 1. 在窗口中添加 Growl 容器
```xml
<Window x:Class="MyApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:hc="https://handyorg.github.io/handycontrol"
Title="主窗口" Height="600" Width="800">
<Grid>
<!-- 你的窗口内容 -->
<DockPanel>
<Menu DockPanel.Dock="Top">...</Menu>
<ContentControl>...</ContentControl>
</DockPanel>
<!-- Growl 容器 - 必须放在最顶层,ZIndex 最高 -->
<hc:Growl Token="Default"
Panel.ZIndex="999"
VerticalAlignment="Top"
HorizontalAlignment="Right"
Margin="0,10,10,0"/>
</Grid>
</Window>
```
#### 2. 在代码中使用 Growl
```csharp
// 使用默认 Token
_dialog.ShowNotification("这是一条通知", NotificationType.Info);
_dialog.ShowNotification("保存成功", NotificationType.Success);
_dialog.ShowNotification("磁盘空间不足", NotificationType.Warning);
_dialog.ShowNotification("连接失败", NotificationType.Error);
_dialog.ShowNotification("系统崩溃", NotificationType.Fatal);
```
### 高级配置
#### 自定义 Growl 位置
```xml
<!-- 左上角 -->
<hc:Growl Token="TopLeft"
Panel.ZIndex="999"
VerticalAlignment="Top"
HorizontalAlignment="Left"
Margin="10,10,0,0"/>
<!-- 底部居中 -->
<hc:Growl Token="BottomCenter"
Panel.ZIndex="999"
VerticalAlignment="Bottom"
HorizontalAlignment="Center"
Margin="0,0,0,10"/>
```
#### 全局 Growl(所有窗口共享)
```xml
<!-- App.xaml -->
<Application.Resources>
<ResourceDictionary>
<!-- 全局 Growl 窗口 -->
<hc:GrowlWindow x:Key="GlobalGrowlWindow" />
</ResourceDictionary>
</Application.Resources>
```
```csharp
// 使用全局 Growl(无需在每个窗口添加容器)
Growl.InfoGlobal("这是全局通知");
```
---
## 🎨 主题定制
### 运行时切换主题
```csharp
public class SettingsViewModel : ViewModelBase
{
public void SwitchTheme(bool isDark)
{
// 清除旧主题
var oldTheme = Application.Current.Resources.MergedDictionaries
.FirstOrDefault(d => d.Source?.OriginalString.Contains("HandyControl") == true);
if (oldTheme != null)
{
Application.Current.Resources.MergedDictionaries.Remove(oldTheme);
}
// 加载新主题
var skinUri = isDark
? "pack://application:,,,/HandyControl;component/Themes/SkinDark.xaml"
: "pack://application:,,,/HandyControl;component/Themes/SkinDefault.xaml";
Application.Current.Resources.MergedDictionaries.Add(new ResourceDictionary
{
Source = new Uri(skinUri)
});
_dialog.ShowSuccess("主题已切换");
}
}
```
### 自定义主题颜色
HandyControl 支持自定义主题颜色,创建 `CustomTheme.xaml`:
```xml
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!-- 覆盖默认颜色 -->
<SolidColorBrush x:Key="PrimaryBrush" Color="#FF0078D7"/>
<SolidColorBrush x:Key="DangerBrush" Color="#FFE74856"/>
<SolidColorBrush x:Key="WarningBrush" Color="#FFFFA500"/>
<SolidColorBrush x:Key="SuccessBrush" Color="#FF13A10E"/>
</ResourceDictionary>
```
然后禁用自动主题并手动加载:
```csharp
services.AddSharpHandyControl(options =>
{
options.AutoRegisterTheme = false;
});
```
```xml
<!-- App.xaml -->
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/SkinDefault.xaml"/>
<ResourceDictionary Source="Themes/CustomTheme.xaml"/>
<ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/Theme.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
@@ -393,43 +832,469 @@ services.AddHandyControlServices(options => {
---
## 📝 注意事项
## 📝 迁移指南
1. **HandyControl 版本**:当前支持 HandyControl 3.5.0
2. **依赖 Sharp.Wpf**:必须先引用 Sharp.Wpf 模块
3. **主题资源**:需要在 App.xaml 中引入 HandyControl 资源字典
4. **Growl容器**:需要在主窗口添加 Growl 容器才能使用通知功能
### 从直接使用 HandyControl 迁移
#### 步骤 1:添加服务注册
```csharp
// App.xaml.cs
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
var services = new ServiceCollection();
// 一行代码完成服务注册和主题配置
services.AddSharpHandyControl();
// 注册其他服务...
services.AddTransient<MainViewModel>();
services.AddTransient<MainWindow>();
var provider = services.BuildServiceProvider();
// 显示主窗口
var mainWindow = provider.GetRequiredService<MainWindow>();
mainWindow.Show();
}
```
#### 步骤 2:删除 App.xaml 中的手动主题引用
```xml
<!-- 之前:需要手动添加主题资源 -->
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/SkinDefault.xaml" />
<ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/Theme.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
<!-- 之后:无需手动添加,AddSharpHandyControl() 会自动注册 -->
<Application.Resources>
<ResourceDictionary>
<!-- 可以添加你自己的资源 -->
</ResourceDictionary>
</Application.Resources>
```
#### 步骤 3:修改 Window 和 ViewModel 使用依赖注入
```csharp
// 之前:直接 new,无法注入服务
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new MainViewModel(); // ❌ 无法注入服务
}
private void Button_Click(object sender, RoutedEventArgs e)
{
// ❌ 静态调用,难以测试
HandyControl.Controls.MessageBox.Show("消息", "标题");
}
}
// 之后:通过构造函数注入
public partial class MainWindow : Window
{
private readonly IHandyDialogService _dialog;
public MainWindow(MainViewModel viewModel, IHandyDialogService dialog)
{
InitializeComponent();
DataContext = viewModel; // ✅ ViewModel 由 DI 容器创建
_dialog = dialog;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
// ✅ 使用注入的服务,可测试
_dialog.ShowInfo("消息", "标题");
}
}
```
#### 步骤 4:替换 MessageBox 静态调用
| 之前(静态调用) | 之后(依赖注入) |
|---|---|
| `HandyControl.Controls.MessageBox.Show("消息", "标题")` | `_dialog.ShowInfo("消息", "标题")` |
| `HandyControl.Controls.MessageBox.Success("成功")` | `_dialog.ShowSuccess("成功")` |
| `HandyControl.Controls.MessageBox.Error("错误")` | `_dialog.ShowError("错误")` |
| `HandyControl.Controls.MessageBox.Warning("警告")` | `_dialog.ShowWarning("警告")` |
| `var result = HandyControl.Controls.MessageBox.Ask("确认吗?")` | `var confirmed = await _dialog.ConfirmAsync("确认吗?")` |
| `Growl.Info("通知")` | `_dialog.ShowNotification("通知", NotificationType.Info)` |
---
## 🔄 与其他UI库对比
## ❓ 常见问题
| UI库 | 特点 | 适用场景 |
|-----|------|---------|
| **HandyControl** | 现代化设计、控件丰富 | 企业级应用、工具软件 |
| **MaterialDesign** | Material风格 | 现代化Web风格应用 |
| **ModernWPF** | Windows 11风格 | 跟随Windows系统风格 |
| **原生WPF** | 轻量级 | 简单应用、追求性能 |
### Q1: 为什么调用 `ShowNotification` 后没有显示 Growl 通知?
**A:** 需要在窗口 XAML 中添加 `<hc:Growl>` 容器:
```xml
<Window>
<Grid>
<!-- 你的内容 -->
<!-- 添加这个 -->
<hc:Growl Token="Default" Panel.ZIndex="999"
VerticalAlignment="Top" HorizontalAlignment="Right"
Margin="0,10,10,0"/>
</Grid>
</Window>
```
确保:
1. `Token` 与代码中使用的一致(默认是 "Default")
2. `Panel.ZIndex` 足够大,确保在最顶层
3. 容器位于 `Grid` 或其他容器的最后(XAML 顺序)
### Q2: 如何在单元测试中模拟 `IHandyDialogService`?
**A:** 使用 Mock 库(如 Moq):
```csharp
using Moq;
[TestClass]
public class MainViewModelTests
{
[TestMethod]
public async Task SaveCommand_ShowsSuccessMessage_WhenSaveSucceeds()
{
// Arrange
var mockDialog = new Mock<IHandyDialogService>();
var mockService = new Mock<IUserService>();
mockService.Setup(s => s.SaveUserAsync(It.IsAny<User>()))
.ReturnsAsync(Result.Success());
var viewModel = new MainViewModel(mockDialog.Object, mockService.Object);
viewModel.UserName = "Test";
viewModel.Email = "test@test.com";
// Act
await viewModel.SaveCommand.ExecuteAsync(null);
// Assert
mockDialog.Verify(d => d.ShowSuccess(It.IsAny<string>(), It.IsAny<string>()), Times.Once);
}
}
```
### Q3: 可以在非 UI 线程调用对话框服务吗?
**A:** 可以。`IHandyDialogService` 内部使用 `Application.Current.Dispatcher.Invoke()` 自动切换到 UI 线程,你可以在任何线程安全地调用:
```csharp
Task.Run(() =>
{
// 在后台线程执行耗时操作
ProcessLargeFile();
// 自动切换到 UI 线程显示对话框
_dialog.ShowSuccess("处理完成!");
});
```
### Q4: 如何同时支持 HandyControl 和其他 UI 库?
**A:** 使用基础接口 `IDialogService`:
```csharp
// ViewModel 只依赖基础接口
public class MyViewModel
{
private readonly IDialogService _dialog; // ← 不是 IHandyDialogService
public MyViewModel(IDialogService dialog)
{
_dialog = dialog;
}
}
// 配置时选择实现
// 方案 A:使用 HandyControl
services.AddSharpHandyControl();
// 方案 B:使用其他实现
services.AddSingleton<IDialogService, MyCustomDialogService>();
```
### Q5: `ShowInputAsync` 为什么返回失败?
**A:** `ShowInputAsync` 功能尚未实现(需要自定义对话框窗口),当前版本会返回:
```csharp
Result.Failure<string>("输入对话框功能尚未实现,需要自定义对话框窗口")
```
如果需要输入功能,可以临时使用:
- 创建自定义 Window 进行输入
- 使用 HandyControl 的其他输入控件
- 等待后续版本实现
### Q6: 能否在 WinForms 项目中使用?
**A:** 不能。本模块基于 WPF 的 `System.Windows.Application`,仅支持 WPF 项目。如果是 WinForms 项目,建议使用 WinForms 原生对话框或迁移到 WPF。
### Q7: 主题切换后某些控件样式不生效?
**A:** 确保控件使用了 HandyControl 的样式:
```xml
<!-- 使用 HandyControl 样式的按钮 -->
<Button Style="{StaticResource ButtonPrimary}" Content="确定"/>
<!-- 如果使用原生 Button,不会应用 HandyControl 主题 -->
<Button Content="确定"/> <!-- ❌ 不会有 HandyControl 样式 -->
```
查看 [HandyControl 文档](https://handyorg.github.io/) 了解所有可用样式。
---
## 🚀 未来扩展
## 🏆 最佳实践
### 可能的扩展方向
1. **Sharp.Wpf.MaterialDesign** - MaterialDesign UI库集成
2. **Sharp.Wpf.ModernWPF** - ModernWPF UI库集成
3. **自定义控件库** - 基于 HandyControl 的业务控件
### 1. 统一使用依赖注入
**✅ 推荐:**
```csharp
public class MyViewModel
{
private readonly IHandyDialogService _dialog;
public MyViewModel(IHandyDialogService dialog)
{
_dialog = dialog;
}
}
```
**❌ 不推荐:**
```csharp
public class MyViewModel
{
public void ShowMessage()
{
// 静态调用,无法测试
HandyControl.Controls.MessageBox.Show("消息");
}
}
```
### 2. 优先使用 Growl 通知代替阻塞式消息框
对于非关键操作,使用 Growl 通知提升用户体验:
```csharp
// ✅ 推荐:后台保存使用 Growl
await _service.AutoSaveAsync();
_dialog.ShowNotification("已自动保存", NotificationType.Success);
// ❌ 不推荐:后台操作阻塞 UI
await _service.AutoSaveAsync();
_dialog.ShowSuccess("已自动保存"); // 用户必须点击"确定"才能继续
```
### 3. 为异常处理提供友好提示
```csharp
try
{
await _service.ProcessAsync();
}
catch (NetworkException ex)
{
_dialog.ShowError("网络连接失败,请检查网络设置", "网络错误");
}
catch (UnauthorizedException ex)
{
_dialog.ShowError("您没有权限执行此操作", "权限不足");
}
catch (Exception ex)
{
_dialog.ShowError($"发生未知错误:{ex.Message}\n\n请联系技术支持。", "系统错误");
}
```
### 4. 使用 IDialogService 接口提高兼容性
如果模块可能被其他项目使用,优先依赖基础接口:
```csharp
// ✅ 兼容性好
public MyLibrary(IDialogService dialog) { }
// ❌ 绑定到具体实现
public MyLibrary(IHandyDialogService dialog) { }
```
### 5. 为长时间操作提供进度反馈
```csharp
public async Task ProcessLargeFileAsync()
{
IsBusy = true;
var items = LoadItems();
for (int i = 0; i < items.Count; i++)
{
await ProcessItemAsync(items[i]);
// 每 10 个显示一次进度
if ((i + 1) % 10 == 0)
{
_dialog.ShowNotification(
$"已处理 {i + 1}/{items.Count}",
NotificationType.Info
);
}
}
IsBusy = false;
_dialog.ShowSuccess("处理完成!");
}
```
### 6. 配置全局异常处理
在 `App.xaml.cs` 中捕获未处理异常:
```csharp
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
// 配置全局异常处理
DispatcherUnhandledException += (s, args) =>
{
var dialog = _serviceProvider.GetService<IHandyDialogService>();
dialog?.ShowError($"程序发生异常:{args.Exception.Message}", "未处理的异常");
args.Handled = true;
};
// ... DI 配置
}
```
---
## 📖 参考资料
## 📖 API 参考
- [HandyControl GitHub](https://github.com/HandyOrg/HandyControl)
- [HandyControl 官方文档](https://handyorg.github.io/)
- [HandyControl Demo](https://github.com/HandyOrg/HandyControl/releases)
### 消息框方法
| 方法 | 说明 | 图标颜色 | 阻塞 |
|------|------|----------|------|
| `ShowSuccess(message, title)` | 显示成功消息 | 绿色 | 是 |
| `ShowInfo(message, title)` | 显示信息消息 | 蓝色 | 是 |
| `ShowWarning(message, title)` | 显示警告消息 | 橙色 | 是 |
| `ShowError(message, title)` | 显示错误消息 | 红色 | 是 |
### 对话框方法
| 方法 | 说明 | 返回值 | 按钮 |
|------|------|--------|------|
| `ConfirmAsync(message, title)` | 确认对话框 | `Task<bool>` | 确定/取消 |
| `AskAsync(message, title)` | 询问对话框 | `Task<HandyDialogResult>` | 是/否/取消 |
| `ShowInputAsync(message, title, defaultValue)` | 输入对话框(未实现) | `Task<Result<string>>` | 确定/取消 |
### Growl 通知方法
| 方法 | 说明 | 阻塞 |
|------|------|------|
| `ShowNotification(message, type, token)` | 显示 Growl 通知 | 否 |
**NotificationType 类型:**
- `Info` - 信息(蓝色)
- `Success` - 成功(绿色)
- `Warning` - 警告(橙色)
- `Error` - 错误(红色)
- `Fatal` - 严重错误(深红色)
### IDialogService 基础接口方法
继承自 `Sharp.Wpf` 的基础接口:
| 方法 | 说明 |
|------|------|
| `ShowInfoAsync(message, title)` | 显示信息对话框 |
| `ShowWarningAsync(message, title)` | 显示警告对话框 |
| `ShowErrorAsync(message, title)` | 显示错误对话框 |
| `ShowConfirmAsync(message, title)` | 显示确认对话框 |
---
**状态**:规划阶段,未实现
**依赖**:Sharp.Wpf, HandyControl 3.5.0
**实现难度**:低(封装现有API)
## 🔄 与其他 UI 库对比
| UI库 | 特点 | 许可证 | 适用场景 |
|-----|------|--------|---------|
| **HandyControl** | 现代化设计、控件丰富、中文友好 | MIT | 企业级应用、工具软件、内部系统 |
| **MaterialDesign** | Material Design 风格、动画流畅 | MIT | 现代化 Web 风格应用、跨平台应用 |
| **ModernWPF** | Windows 11 Fluent Design 风格 | MIT | 跟随 Windows 系统风格的应用 |
| **MahApps.Metro** | Metro 风格、轻量级 | MIT | 简洁风格的桌面应用 |
| **原生 WPF** | 系统自带、无额外依赖 | - | 追求性能、简单应用 |
**为什么选择 HandyControl?**
- ✅ 控件库最全面(80+ 控件)
- ✅ 中文社区活跃,文档完善
- ✅ 开箱即用,样式统一
- ✅ 性能好,不臃肿
---
## 📚 参考资料
### 官方资源
- [HandyControl GitHub](https://github.com/HandyOrg/HandyControl) - 源代码仓库
- [HandyControl 官方文档](https://handyorg.github.io/) - 完整文档和示例
- [HandyControl Demo 下载](https://github.com/HandyOrg/HandyControl/releases) - 可运行的示例程序
### Sharp.Wpf 相关
- [Sharp.Wpf 模块文档](../Wpf/README.md) - 核心 MVVM 框架
- [Sharp.Common 模块文档](../Common/README.md) - Result<T> 等基础设施
### 社区资源
- [HandyControl Gitee 镜像](https://gitee.com/handyorg/HandyControl) - 国内访问更快
- [HandyControl QQ 群](https://github.com/HandyOrg/HandyControl#%E4%BA%A4%E6%B5%81) - 技术交流
---
## 📦 模块信息
- **状态**:✅ 可用(生产就绪)
- **版本**:1.0.0
- **目标框架**:.NET 8.0 Windows
- **依赖**:
- Sharp.Wpf (MVVM 基础设施)
- Sharp.Common (Result<T> 等)
- HandyControl 3.5.0 (UI 库)
- Microsoft.Extensions.DependencyInjection (依赖注入)
- **许可证**:GNU AGPLv3
- **实现难度**:低(封装现有 API)
- **维护状态**:活跃维护
---
## 🤝 贡献与反馈
如果你有任何问题、建议或 Bug 报告,欢迎:
- 提交 Issue
- 提交 Pull Request
- 加入技术交流群讨论
---
**Made with ❤️ by Sharp Team**
@@ -0,0 +1,191 @@
using System.Windows;
using Common.Results;
using HandyControl.Controls;
using HandyControl.Data;
using Sharp.Wpf.HandyControl.Options;
using Sharp.Wpf.Services;
using MessageBox = HandyControl.Controls.MessageBox;
namespace Sharp.Wpf.HandyControl.Services;
/// <summary>
/// HandyControl 对话框服务实现
/// </summary>
public class HandyDialogService : IHandyDialogService
{
private readonly HandyControlOptions _options;
/// <summary>
/// 构造函数
/// </summary>
public HandyDialogService(HandyControlOptions options)
{
_options = options;
}
/// <summary>
/// 显示成功消息
/// </summary>
public void ShowSuccess(string message, string? title = null)
{
Application.Current.Dispatcher.Invoke(() =>
{
MessageBox.Success(message, title ?? "成功");
});
}
/// <summary>
/// 显示信息消息
/// </summary>
public void ShowInfo(string message, string? title = null)
{
Application.Current.Dispatcher.Invoke(() =>
{
MessageBox.Info(message, title ?? "信息");
});
}
/// <summary>
/// 显示警告消息
/// </summary>
public void ShowWarning(string message, string? title = null)
{
Application.Current.Dispatcher.Invoke(() =>
{
MessageBox.Warning(message, title ?? "警告");
});
}
/// <summary>
/// 显示错误消息
/// </summary>
public void ShowError(string message, string? title = null)
{
Application.Current.Dispatcher.Invoke(() =>
{
MessageBox.Error(message, title ?? "错误");
});
}
/// <summary>
/// 显示确认对话框
/// </summary>
public Task<bool> ConfirmAsync(string message, string? title = null)
{
return Task.Run(() =>
{
var result = Application.Current.Dispatcher.Invoke(() =>
{
return MessageBox.Ask(message, title ?? "确认");
});
return result == MessageBoxResult.OK;
});
}
/// <summary>
/// 显示询问对话框(是/否/取消)
/// </summary>
public Task<HandyDialogResult> AskAsync(string message, string? title = null)
{
return Task.Run(() =>
{
var result = Application.Current.Dispatcher.Invoke(() =>
{
return MessageBox.Show(message, title ?? "询问", MessageBoxButton.YesNoCancel, MessageBoxImage.Question);
});
return result switch
{
MessageBoxResult.Yes => HandyDialogResult.Yes,
MessageBoxResult.No => HandyDialogResult.No,
MessageBoxResult.OK => HandyDialogResult.OK,
MessageBoxResult.Cancel => HandyDialogResult.Cancel,
_ => HandyDialogResult.None
};
});
}
/// <summary>
/// 显示输入对话框(尚未实现)
/// </summary>
public Task<Result<string>> ShowInputAsync(string message, string? title = null, string? defaultValue = null)
{
// TODO: 需要自定义 TextDialog 窗口来实现此功能
// 当前版本暂不支持,返回失败结果
return Task.FromResult(Result.Failure<string>("输入对话框功能尚未实现,需要自定义对话框窗口"));
}
/// <summary>
/// 显示通知(Growl)
/// </summary>
public void ShowNotification(string message, NotificationType type = NotificationType.Info, string? token = null)
{
Application.Current.Dispatcher.Invoke(() =>
{
var info = new GrowlInfo
{
Message = message,
WaitTime = _options.GrowlWaitTime,
Token = token ?? _options.DefaultGrowlToken
};
switch (type)
{
case NotificationType.Info:
Growl.Info(info);
break;
case NotificationType.Success:
Growl.Success(info);
break;
case NotificationType.Warning:
Growl.Warning(info);
break;
case NotificationType.Error:
Growl.Error(info);
break;
case NotificationType.Fatal:
Growl.Fatal(info);
break;
}
});
}
#region IDialogService 实现(兼容基础接口)
/// <summary>
/// 显示信息对话框(IDialogService 实现)
/// </summary>
public Task ShowInfoAsync(string message, string? title = null)
{
ShowInfo(message, title);
return Task.CompletedTask;
}
/// <summary>
/// 显示警告对话框(IDialogService 实现)
/// </summary>
public Task ShowWarningAsync(string message, string? title = null)
{
ShowWarning(message, title);
return Task.CompletedTask;
}
/// <summary>
/// 显示错误对话框(IDialogService 实现)
/// </summary>
public Task ShowErrorAsync(string message, string? title = null)
{
ShowError(message, title);
return Task.CompletedTask;
}
/// <summary>
/// 显示确认对话框(IDialogService 实现)
/// </summary>
public Task<bool> ShowConfirmAsync(string message, string? title = null)
{
return ConfirmAsync(message, title);
}
#endregion
}
@@ -0,0 +1,140 @@
using Common.Results;
using Sharp.Wpf.Services;
namespace Sharp.Wpf.HandyControl.Services;
/// <summary>
/// HandyControl 对话框服务接口
/// 扩展自 IDialogService,提供 HandyControl 特有的对话框功能
/// </summary>
public interface IHandyDialogService : IDialogService
{
/// <summary>
/// 显示成功消息(绿色,带勾图标)
/// </summary>
/// <param name="message">消息内容</param>
/// <param name="title">标题,默认为"成功"</param>
void ShowSuccess(string message, string? title = null);
/// <summary>
/// 显示信息消息(蓝色,带i图标)
/// </summary>
/// <param name="message">消息内容</param>
/// <param name="title">标题,默认为"信息"</param>
void ShowInfo(string message, string? title = null);
/// <summary>
/// 显示警告消息(橙色,带!图标)
/// </summary>
/// <param name="message">消息内容</param>
/// <param name="title">标题,默认为"警告"</param>
void ShowWarning(string message, string? title = null);
/// <summary>
/// 显示错误消息(红色,带X图标)
/// </summary>
/// <param name="message">消息内容</param>
/// <param name="title">标题,默认为"错误"</param>
void ShowError(string message, string? title = null);
/// <summary>
/// 显示确认对话框
/// </summary>
/// <param name="message">消息内容</param>
/// <param name="title">标题,默认为"确认"</param>
/// <returns>用户是否确认</returns>
Task<bool> ConfirmAsync(string message, string? title = null);
/// <summary>
/// 显示询问对话框(是/否/取消)
/// </summary>
/// <param name="message">消息内容</param>
/// <param name="title">标题</param>
/// <returns>对话框结果</returns>
Task<HandyDialogResult> AskAsync(string message, string? title = null);
/// <summary>
/// 显示输入对话框
/// </summary>
/// <param name="message">提示信息</param>
/// <param name="title">标题</param>
/// <param name="defaultValue">默认值</param>
/// <returns>用户输入的结果</returns>
Task<Result<string>> ShowInputAsync(
string message,
string? title = null,
string? defaultValue = null);
/// <summary>
/// 显示通知(Growl,右下角弹出)
/// </summary>
/// <param name="message">通知内容</param>
/// <param name="type">通知类型</param>
/// <param name="token">Growl容器标识</param>
void ShowNotification(
string message,
NotificationType type = NotificationType.Info,
string? token = null);
}
/// <summary>
/// 对话框结果
/// </summary>
public enum HandyDialogResult
{
/// <summary>
/// 未知
/// </summary>
None = 0,
/// <summary>
/// 确定/是
/// </summary>
OK = 1,
/// <summary>
/// 取消/否
/// </summary>
Cancel = 2,
/// <summary>
/// 是
/// </summary>
Yes = 6,
/// <summary>
/// 否
/// </summary>
No = 7
}
/// <summary>
/// 通知类型
/// </summary>
public enum NotificationType
{
/// <summary>
/// 信息(蓝色)
/// </summary>
Info,
/// <summary>
/// 成功(绿色)
/// </summary>
Success,
/// <summary>
/// 警告(橙色)
/// </summary>
Warning,
/// <summary>
/// 错误(红色)
/// </summary>
Error,
/// <summary>
/// 严重错误(深红色)
/// </summary>
Fatal
}
@@ -15,6 +15,11 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Common\Common.csproj" />
<ProjectReference Include="..\Wpf\Wpf.csproj" />
</ItemGroup>