从零打造WPF任务计划程序:HandyControl控件应用实战
引言:WPF任务计划的痛点与解决方案
你是否还在为WPF应用中实现任务计划功能而烦恼?传统方式需要手动处理日期时间选择、任务列表管理、状态展示等多个复杂模块,代码冗余且维护困难。本文将展示如何利用HandyControl控件库,以最小开发成本构建一个功能完善的任务计划程序。通过本文,你将掌握:
- 日期时间选择控件(DateTimePicker)的高级应用
- 任务状态管理与视觉反馈实现
- 动态任务列表构建与数据绑定技巧
- 自定义控件样式实现专业UI设计
技术栈与环境准备
开发环境要求
| 环境/工具 | 版本要求 | 备注 |
|---|---|---|
| .NET Framework | 4.5+ | 或.NET Core 3.0+ |
| Visual Studio | 2019+ | 推荐2022版本 |
| HandyControl | 最新版 | 通过NuGet安装 |
| Git | 任意版本 | 用于获取示例代码 |
项目搭建与依赖安装
首先,创建一个新的WPF项目,然后通过NuGet安装HandyControl:
Install-Package HandyControl
或者使用.NET CLI:
dotnet add package HandyControl
核心控件解析:DateTimePicker深度应用
DateTimePicker控件架构
DateTimePicker是HandyControl提供的高级日期时间选择控件,支持日期和时间的联合选择,其核心架构如下:
关键属性与事件
| 属性 | 类型 | 描述 |
|---|---|---|
| SelectedDateTime | DateTime? | 获取或设置选中的日期时间,支持空值 |
| DateTimeFormat | string | 设置日期时间显示格式,默认"yyyy-MM-dd HH:mm:ss" |
| CalendarStyle | Style | 自定义日历部分的样式 |
| IsDropDownOpen | bool | 获取或设置下拉面板是否打开 |
| Text | string | 获取或设置文本框中的文本 |
核心事件:
- SelectedDateTimeChanged: 选中日期时间变化时触发
- PickerOpened: 下拉面板打开时触发
- PickerClosed: 下拉面板关闭时触发
基础用法示例
<Window
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"
x:Class="TaskScheduler.MainWindow"
Title="任务计划程序" Height="450" Width="800">
<Grid Margin="20">
<hc:DateTimePicker
x:Name="taskDateTimePicker"
Width="250"
DateTimeFormat="yyyy-MM-dd HH:mm"
SelectedDateTimeChanged="TaskDateTimePicker_SelectedDateTimeChanged"/>
</Grid>
</Window>
对应的C#后台代码:
private void TaskDateTimePicker_SelectedDateTimeChanged(object sender, FunctionEventArgs<DateTime?> e)
{
if (e.Info.HasValue)
{
Debug.WriteLine($"选中的任务时间: {e.Info.Value:yyyy-MM-dd HH:mm}");
}
else
{
Debug.WriteLine("未选择任务时间");
}
}
高级自定义:日期时间格式与样式
自定义日期时间格式:
<hc:DateTimePicker
DateTimeFormat="yyyy年MM月dd日 HH时mm分"
Watermark="请选择任务执行时间"/>
自定义日历样式:
<Window.Resources>
<Style x:Key="CustomCalendarStyle" TargetType="Calendar">
<Setter Property="Background" Value="#F5F5F5"/>
<Setter Property="Foreground" Value="#333333"/>
<Setter Property="CalendarDayButtonStyle">
<Setter.Value>
<Style TargetType="CalendarDayButton">
<Setter Property="MinWidth" Value="36"/>
<Setter Property="MinHeight" Value="36"/>
<Setter Property="FontSize" Value="14"/>
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="Background" Value="#2196F3"/>
<Setter Property="Foreground" Value="White"/>
</Trigger>
</Style.Triggers>
</Style>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
<hc:DateTimePicker
CalendarStyle="{StaticResource CustomCalendarStyle}"
DateTimeFormat="yyyy-MM-dd HH:mm"/>
任务计划程序核心功能实现
数据模型设计
首先定义任务模型:
public class ScheduledTask : INotifyPropertyChanged
{
private string _taskName;
private DateTime? _executeTime;
private TaskStatus _status;
private string _description;
public string TaskName
{
get => _taskName;
set { _taskName = value; OnPropertyChanged(); }
}
public DateTime? ExecuteTime
{
get => _executeTime;
set { _executeTime = value; OnPropertyChanged(); }
}
public TaskStatus Status
{
get => _status;
set { _status = value; OnPropertyChanged(); }
}
public string Description
{
get => _description;
set { _description = value; OnPropertyChanged(); }
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public enum TaskStatus
{
[Description("未执行")]
NotExecuted,
[Description("已完成")]
Completed,
[Description("执行中")]
Executing,
[Description("已取消")]
Cancelled,
[Description("失败")]
Failed
}
ViewModel实现
public class TaskSchedulerViewModel : INotifyPropertyChanged
{
private ObservableCollection<ScheduledTask> _tasks;
private ScheduledTask _selectedTask;
private string _newTaskName;
private string _newTaskDescription;
private DateTime? _newTaskTime;
public ObservableCollection<ScheduledTask> Tasks
{
get => _tasks;
set { _tasks = value; OnPropertyChanged(); }
}
public ScheduledTask SelectedTask
{
get => _selectedTask;
set { _selectedTask = value; OnPropertyChanged(); }
}
public string NewTaskName
{
get => _newTaskName;
set { _newTaskName = value; OnPropertyChanged(); }
}
public string NewTaskDescription
{
get => _newTaskDescription;
set { _newTaskDescription = value; OnPropertyChanged(); }
}
public DateTime? NewTaskTime
{
get => _newTaskTime;
set { _newTaskTime = value; OnPropertyChanged(); }
}
public ICommand AddTaskCommand { get; }
public ICommand DeleteTaskCommand { get; }
public ICommand ExecuteTaskCommand { get; }
public ICommand CancelTaskCommand { get; }
public TaskSchedulerViewModel()
{
Tasks = new ObservableCollection<ScheduledTask>();
AddTaskCommand = new RelayCommand(AddTask);
DeleteTaskCommand = new RelayCommand(DeleteTask, CanDeleteTask);
ExecuteTaskCommand = new RelayCommand(ExecuteTask, CanExecuteTask);
CancelTaskCommand = new RelayCommand(CancelTask, CanCancelTask);
// 加载示例数据
LoadSampleData();
}
private void LoadSampleData()
{
Tasks.Add(new ScheduledTask
{
TaskName = "数据备份",
ExecuteTime = DateTime.Now.AddHours(2),
Status = TaskStatus.NotExecuted,
Description = "每日数据库全量备份"
});
Tasks.Add(new ScheduledTask
{
TaskName = "日志清理",
ExecuteTime = DateTime.Now.AddHours(1),
Status = TaskStatus.NotExecuted,
Description = "清理超过30天的系统日志"
});
}
private void AddTask()
{
if (string.IsNullOrWhiteSpace(NewTaskName) || !NewTaskTime.HasValue)
return;
Tasks.Add(new ScheduledTask
{
TaskName = NewTaskName,
ExecuteTime = NewTaskTime,
Status = TaskStatus.NotExecuted,
Description = NewTaskDescription
});
// 清空输入
NewTaskName = string.Empty;
NewTaskDescription = string.Empty;
NewTaskTime = null;
}
private bool CanDeleteTask()
{
return SelectedTask != null;
}
private void DeleteTask()
{
if (SelectedTask != null)
{
Tasks.Remove(SelectedTask);
}
}
private bool CanExecuteTask()
{
return SelectedTask != null && SelectedTask.Status == TaskStatus.NotExecuted;
}
private void ExecuteTask()
{
if (SelectedTask != null)
{
SelectedTask.Status = TaskStatus.Executing;
// 模拟任务执行
Task.Delay(2000).ContinueWith(t =>
{
SelectedTask.Status = TaskStatus.Completed;
}, TaskScheduler.FromCurrentSynchronizationContext());
}
}
private bool CanCancelTask()
{
return SelectedTask != null &&
(SelectedTask.Status == TaskStatus.NotExecuted ||
SelectedTask.Status == TaskStatus.Executing);
}
private void CancelTask()
{
if (SelectedTask != null)
{
SelectedTask.Status = TaskStatus.Cancelled;
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
主界面布局与实现
<Window x:Class="TaskScheduler.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:hc="https://handyorg.github.io/handycontrol"
xmlns:local="clr-namespace:TaskScheduler"
mc:Ignorable="d"
Title="任务计划程序" Height="600" Width="900"
DataContext="{StaticResource ViewModel}">
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- 新增任务区域 -->
<Card Grid.Row="0" Margin="0 0 0 10">
<Grid Margin="10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150"/>
<ColumnDefinition Width="200"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="100"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Text="任务名称:" VerticalAlignment="Center"/>
<hc:TextBox Grid.Column="1" Text="{Binding NewTaskName}" Margin="0 5"/>
<TextBlock Grid.Row="1" Text="执行时间:" VerticalAlignment="Center"/>
<hc:DateTimePicker
Grid.Row="1" Grid.Column="1"
SelectedDateTime="{Binding NewTaskTime}"
DateTimeFormat="yyyy-MM-dd HH:mm"
Margin="0 5"/>
<hc:TextBox
Grid.Column="2" Grid.RowSpan="2"
Text="{Binding NewTaskDescription}"
Watermark="请输入任务描述"
Margin="10 5"/>
<hc:Button
Grid.Column="3" Grid.RowSpan="2"
Command="{Binding AddTaskCommand}"
Content="添加任务"
Style="{StaticResource PrimaryButton}"
Margin="0 5"/>
</Grid>
</Card>
<!-- 任务列表区域 -->
<hc:DataGrid
Grid.Row="1"
ItemsSource="{Binding Tasks}"
SelectedItem="{Binding SelectedTask}"
AutoGenerateColumns="False"
CanUserAddRows="False"
RowHeight="40">
<hc:DataGrid.Columns>
<hc:DataGridTextColumn Header="任务名称" Binding="{Binding TaskName}" Width="150"/>
<hc:DataGridTextColumn Header="执行时间" Binding="{Binding ExecuteTime, StringFormat='yyyy-MM-dd HH:mm'}" Width="150"/>
<hc:DataGridTemplateColumn Header="状态" Width="100">
<hc:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<hc:Badge
Text="{Binding Status, Converter={StaticResource EnumDescriptionConverter}}"
Background="{Binding Status, Converter={StaticResource StatusToBrushConverter}}"/>
</DataTemplate>
</hc:DataGridTemplateColumn.CellTemplate>
</hc:DataGridTemplateColumn>
<hc:DataGridTextColumn Header="描述" Binding="{Binding Description}" Width="*"/>
<hc:DataGridTemplateColumn Header="操作" Width="180">
<hc:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<hc:Button
Command="{Binding DataContext.ExecuteTaskCommand, RelativeSource={RelativeSource AncestorType=hc:DataGrid}}"
CommandParameter="{Binding}"
Content="执行"
Style="{StaticResource SuccessButton}"
Width="60" Margin="2"/>
<hc:Button
Command="{Binding DataContext.CancelTaskCommand, RelativeSource={RelativeSource AncestorType=hc:DataGrid}}"
CommandParameter="{Binding}"
Content="取消"
Style="{StaticResource WarningButton}"
Width="60" Margin="2"/>
</StackPanel>
</DataTemplate>
</hc:DataGridTemplateColumn.CellTemplate>
</hc:DataGridTemplateColumn>
</hc:DataGrid.Columns>
</hc:DataGrid>
</Grid>
</Window>
状态转换器实现
创建状态到颜色的转换器:
public class StatusToBrushConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is TaskStatus status)
{
switch (status)
{
case TaskStatus.NotExecuted:
return new SolidColorBrush(Color.FromArgb(255, 158, 158, 158));
case TaskStatus.Executing:
return new SolidColorBrush(Color.FromArgb(255, 33, 150, 243));
case TaskStatus.Completed:
return new SolidColorBrush(Color.FromArgb(255, 76, 175, 80));
case TaskStatus.Cancelled:
return new SolidColorBrush(Color.FromArgb(255, 255, 152, 0));
case TaskStatus.Failed:
return new SolidColorBrush(Color.FromArgb(255, 244, 67, 54));
}
}
return Brushes.Gray;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
// 枚举到描述的转换器
public class EnumDescriptionConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is Enum enumValue)
{
var fieldInfo = enumValue.GetType().GetField(enumValue.ToString());
if (fieldInfo.GetCustomAttributes(typeof(DescriptionAttribute), false) is DescriptionAttribute[] attributes && attributes.Any())
{
return attributes.First().Description;
}
return enumValue.ToString();
}
return value;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
在App.xaml中注册转换器:
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<hc:ThemeResources/>
<hc:Theme />
</ResourceDictionary.MergedDictionaries>
<local:StatusToBrushConverter x:Key="StatusToBrushConverter"/>
<local:EnumDescriptionConverter x:Key="EnumDescriptionConverter"/>
<local:TaskSchedulerViewModel x:Key="ViewModel"/>
</ResourceDictionary>
</Application.Resources>
任务监控与提醒功能
定时检查机制实现
public class TaskMonitor
{
private readonly TaskSchedulerViewModel _viewModel;
private readonly DispatcherTimer _timer;
public TaskMonitor(TaskSchedulerViewModel viewModel)
{
_viewModel = viewModel;
_timer = new DispatcherTimer
{
Interval = TimeSpan.FromSeconds(30)
};
_timer.Tick += Timer_Tick;
_timer.Start();
}
private void Timer_Tick(object sender, EventArgs e)
{
CheckTasks();
}
private void CheckTasks()
{
var now = DateTime.Now;
var tasksToExecute = _viewModel.Tasks
.Where(t => t.Status == TaskStatus.NotExecuted && t.ExecuteTime <= now)
.ToList();
foreach (var task in tasksToExecute)
{
ExecuteTask(task);
}
}
private void ExecuteTask(ScheduledTask task)
{
task.Status = TaskStatus.Executing;
// 模拟任务执行
Task.Run(() =>
{
// 这里是实际任务执行逻辑
Thread.Sleep(3000); // 模拟任务耗时
// 更新UI需要在UI线程
Application.Current.Dispatcher.Invoke(() =>
{
task.Status = TaskStatus.Completed;
ShowTaskCompletedNotification(task);
});
});
}
private void ShowTaskCompletedNotification(ScheduledTask task)
{
// 使用HandyControl的Notification控件显示通知
Application.Current.Dispatcher.Invoke(() =>
{
hc:Notification.Show(new TaskCompletedNotification(task),
new NotificationSettings
{
Placement = Placement.TopRight,
Width = 300,
Height = 100,
ShowTime = 3000
});
});
}
}
通知控件实现
创建任务完成通知用户控件:
<UserControl x:Class="TaskScheduler.TaskCompletedNotification"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:hc="https://handyorg.github.io/handycontrol"
mc:Ignorable="d"
d:DesignHeight="100" d:DesignWidth="300">
<Grid Background="White" Margin="5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<hc:Gravatar
Grid.Column="0"
Source="pack://application:,,,/Resources/complete.png"
Width="40" Height="40"
Margin="10"/>
<StackPanel Grid.Column="1" Margin="5 10">
<TextBlock Text="任务已完成" FontSize="14" FontWeight="Bold"/>
<TextBlock Text="{Binding TaskName}" FontSize="12" Foreground="#666"/>
<TextBlock Text="{Binding ExecuteTime, StringFormat='执行时间: {0:yyyy-MM-dd HH:mm}'}"
FontSize="12" Foreground="#999" Margin="0 2"/>
</StackPanel>
</Grid>
</UserControl>
在MainWindow中初始化监控器:
public partial class MainWindow : Window
{
private TaskMonitor _taskMonitor;
public MainWindow()
{
InitializeComponent();
// 获取ViewModel并初始化监控器
var viewModel = DataContext as TaskSchedulerViewModel;
if (viewModel != null)
{
_taskMonitor = new TaskMonitor(viewModel);
}
}
}
高级功能与优化
任务优先级与排序
扩展任务模型,添加优先级属性:
public enum TaskPriority
{
Low,
Medium,
High,
Urgent
}
// 在ScheduledTask类中添加
private TaskPriority _priority;
public TaskPriority Priority
{
get => _priority;
set { _priority = value; OnPropertyChanged(); }
}
修改DataGrid添加优先级列:
<hc:DataGridTemplateColumn Header="优先级" Width="80">
<hc:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<hc:Badge Content="{Binding Priority}" Background="{Binding Priority, Converter={StaticResource PriorityToBrushConverter}}"/>
</DataTemplate>
</hc:DataGridTemplateColumn.CellTemplate>
</hc:DataGridTemplateColumn>
添加排序功能:
// 在ViewModel中添加排序命令
public ICommand SortByTimeCommand { get; }
public ICommand SortByPriorityCommand { get; }
// 初始化命令
SortByTimeCommand = new RelayCommand(() => SortTasks(t => t.ExecuteTime));
SortByPriorityCommand = new RelayCommand(() => SortTasks(t => t.Priority));
private void SortTasks<TKey>(Func<ScheduledTask, TKey> keySelector)
{
var sorted = Tasks.OrderBy(keySelector).ToList();
Tasks.Clear();
foreach (var task in sorted)
{
Tasks.Add(task);
}
}
数据持久化
使用JSON实现任务数据的保存与加载:
public class TaskDataService
{
private readonly string _dataPath;
public TaskDataService()
{
// 获取应用数据目录
var appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
var appFolder = Path.Combine(appDataPath, "TaskScheduler");
// 确保目录存在
if (!Directory.Exists(appFolder))
{
Directory.CreateDirectory(appFolder);
}
_dataPath = Path.Combine(appFolder, "tasks.json");
}
public List<ScheduledTask> LoadTasks()
{
if (!File.Exists(_dataPath))
return new List<ScheduledTask>();
var json = File.ReadAllText(_dataPath);
return JsonConvert.DeserializeObject<List<ScheduledTask>>(json);
}
public void SaveTasks(IEnumerable<ScheduledTask> tasks)
{
var json = JsonConvert.SerializeObject(tasks, Formatting.Indented);
File.WriteAllText(_dataPath, json);
}
}
在ViewModel中使用数据服务:
private readonly TaskDataService _dataService;
public TaskSchedulerViewModel()
{
_dataService = new TaskDataService();
Tasks = new ObservableCollection<ScheduledTask>(_dataService.LoadTasks());
// 其他初始化代码...
// 当任务集合变化时保存
Tasks.CollectionChanged += (s, e) => _dataService.SaveTasks(Tasks);
}
性能优化与最佳实践
UI虚拟化
对于大量任务,启用DataGrid虚拟化:
<hc:DataGrid
EnableColumnVirtualization="True"
EnableRowVirtualization="True"
VirtualizingPanel.IsVirtualizing="True"
VirtualizingPanel.VirtualizationMode="Recycling">
延迟加载
实现任务详情的延迟加载:
private string _detailedDescription;
private bool _isDetailsLoaded;
public string DetailedDescription
{
get
{
if (!_isDetailsLoaded && !string.IsNullOrEmpty(TaskId))
{
LoadTaskDetails();
_isDetailsLoaded = true;
}
return _detailedDescription;
}
set { _detailedDescription = value; OnPropertyChanged(); }
}
private async void LoadTaskDetails()
{
// 异步加载详细信息
DetailedDescription = await TaskDetailService.GetTaskDetailsAsync(TaskId);
}
内存管理
确保正确释放资源:
// 在MainWindow的Closing事件中
private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
_taskMonitor?.Dispose();
}
// 在TaskMonitor中实现IDisposable
public void Dispose()
{
_timer.Stop();
_timer.Tick -= Timer_Tick;
}
总结与扩展
本文展示了如何使用HandyControl控件库构建功能完善的WPF任务计划程序,重点介绍了DateTimePicker控件的深度应用,以及如何结合DataGrid、Card、Badge等控件实现任务管理、状态监控和提醒功能。
功能回顾
- 使用DateTimePicker实现精确的任务时间选择
- 基于MVVM模式构建清晰的架构
- 实现任务的添加、删除、执行和取消功能
- 通过定时检查机制自动执行到期任务
- 使用Notification控件实现任务完成提醒
- 添加任务优先级和排序功能
- 实现数据持久化保存任务信息
扩展方向
- 添加任务编辑功能,允许修改已创建的任务
- 实现任务历史记录和日志查看
- 添加任务依赖关系,支持任务链执行
- 集成系统托盘图标,支持最小化到托盘
- 添加任务导出和导入功能
- 实现多语言支持
通过HandyControl提供的丰富控件,我们可以显著减少WPF应用的开发工作量,同时获得专业级的UI体验。希望本文能为你的WPF项目开发提供有价值的参考。
如果觉得本文对你有帮助,请点赞、收藏并关注作者,获取更多WPF开发技巧和HandyControl控件应用案例。下期我们将探讨如何使用HandyControl构建高级数据可视化应用。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



