在很多应用场景中,我们可能需要实现一个计时器功能,比如应用中的计时器、倒计时等。本文将介绍如何使用C#中的Task
和CancellationToken
来创建一个简单的计时器,并结合WinForms
界面实现计时、暂停、继续和停止功能。
项目需求
我们将实现一个具有以下功能的计时器:
- 开始计时:点击开始按钮后,计时器开始计时。
- 暂停计时:点击暂停按钮后,暂停计时。
- 继续计时:点击继续按钮后,恢复暂停的计时。
- 停止计时:点击停止按钮后,结束计时并显示总计时。
实现步骤
我们将分步实现这些功能,以下是完整代码。
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace Time
{
public partial class Form1 : Form
{
// 计时任务
private Task _timerTask;
// 计时开始时间
private DateTime _startTime;
// 计时暂停时间
private DateTime _pauseTime;
// 已用时间
private TimeSpan _elapsedTime = TimeSpan.Zero;
// 是否处于暂停状态
private bool _isPaused = false;
// 用于取消计时任务的取消令牌
private CancellationTokenSource _cts;
private ManualResetEvent ma;
public Form1()
{
InitializeComponent();
}
/// <summary>
/// 开始按钮,开始计时并重置时间
/// </summary>
private void btnStart_Click(object sender, EventArgs e)
{
_elapsedTime = TimeSpan.Zero;
_startTime = DateTime.Now;
_isPaused = false;
_pauseTime = DateTime.MinValue;
StartNewTimerTask();
UpdateButtonState(ButtonState.Started);
}
/// <summary>
/// 暂停按钮,暂停计时
/// </summary>
private void btnPause_Click(object sender, EventArgs e)
{
_isPaused = true;
_pauseTime = DateTime.Now;
_cts.Cancel();
UpdateButtonState(ButtonState.Paused);
}
/// <summary>
/// 继续按钮,继续计时
/// </summary>
private void btnContinue_Click(object sender, EventArgs e)
{
_isPaused = false;
_startTime = DateTime.Now - _elapsedTime;
_pauseTime = DateTime.MinValue;
StartNewTimerTask();
UpdateButtonState(ButtonState.Continued);
}
/// <summary>
/// 启动新的计时任务
/// </summary>
private void StartNewTimerTask()
{
_cts?.Dispose();
_cts = new CancellationTokenSource();
var token = _cts.Token;
// ma.WaitOne(); 模拟量
_timerTask = Task.Run(() => StartTimer(token), token);
}
/// <summary>
/// 停止按钮,停止计时并保存时间
/// </summary>
private void btnStop_Click(object sender, EventArgs e)
{
if (_isPaused && _pauseTime != DateTime.MinValue)
{
_elapsedTime = _pauseTime - _startTime;
}
else
{
_elapsedTime = DateTime.Now - _startTime;
}
lblTime.Text = _elapsedTime.ToString(@"hh\:mm\:ss\:fff");
UpdateButtonState(ButtonState.Stopped);
txtSavedTime.Text = _elapsedTime.ToString(@"hh\:mm\:ss\:fff");
_cts.Cancel();
}
/// <summary>
/// 计时逻辑,支持取消令牌
/// </summary>
private void StartTimer(CancellationToken token)
{
while (!_isPaused && !token.IsCancellationRequested)
{
_elapsedTime = DateTime.Now - _startTime;
// Stopwatch timer = Stopwatch.StartNew();//高精度定时器 ----Timer
Invoke(new Action(() =>
{
lblTime.Text = _elapsedTime.ToString(@"hh\:mm\:ss\:fff");
}));
Task.Delay(10).Wait();
}
}
/// <summary>
/// 更新按钮状态
/// </summary>
/// 状态机设计模式
private void UpdateButtonState(ButtonState state)
{
switch (state)
{
case ButtonState.Started:
btnStart.Enabled = false;
btnPause.Enabled = true;
btnStop.Enabled = true;
btnContinue.Enabled = false;
break;
case ButtonState.Paused:
btnPause.Enabled = false;
btnContinue.Enabled = true;
break;
case ButtonState.Continued:
btnPause.Enabled = true;
btnContinue.Enabled = false;
break;
case ButtonState.Stopped:
btnStart.Enabled = true;
btnPause.Enabled = false;
btnStop.Enabled = false;
btnContinue.Enabled = false;
break;
}
}
/// <summary>
/// 窗体关闭时,确保取消计时任务并释放资源
/// </summary>
protected override void OnFormClosing(FormClosingEventArgs e)
{
_cts?.Cancel();
if (_timerTask != null && !_timerTask.IsCompleted)
{
_timerTask.Wait();
}
base.OnFormClosing(e);
}
}
/// <summary>
/// 按钮状态枚举
/// </summary>
public enum ButtonState
{
Started, // 已开始
Paused, // 已暂停
Continued, // 已继续
Stopped // 已停止
}
/// <summary>
/// 枚举扩展方法
/// </summary>
public static class ButtonStateExtensions
{
public static string GetDescription(this ButtonState state)
{
return state switch
{
ButtonState.Started => "计时已开始",
ButtonState.Paused => "计时已暂停",
ButtonState.Continued => "计时已继续",
ButtonState.Stopped => "计时已停止",
_ => "未知状态"
};
}
}
}
代码说明
-
计时逻辑实现
- 通过
Task
和CancellationToken
来管理计时任务的启动、暂停、继续和停止。 - 计时器的时间计算是通过
DateTime.Now
和开始时间 (_startTime
) 来实现的,计算出已用时间 (_elapsedTime
)。
- 通过
-
UI 更新
- 通过
Invoke
方法在计时过程中更新界面上的显示(lblTime
)。 - 使用了格式化的字符串
@"hh\:mm\:ss\:fff"
来显示小时、分钟、秒和毫秒。
- 通过
-
按钮状态管理
- 通过
ButtonState
枚举来管理按钮的启用/禁用状态。 UpdateButtonState
方法根据当前的按钮状态来更新界面的按钮状态,确保界面交互的合理性。
- 通过
-
取消令牌
- 使用
CancellationTokenSource
来支持在计时过程中取消计时任务。 - 通过
Cancel()
方法可以取消当前的计时任务,达到暂停和停止计时的目的。
- 使用
-
窗体关闭时处理
- 在窗体关闭时确保取消正在进行的计时任务,避免资源泄露。
designer代码如下:
namespace Time
{
partial class Form1
{
// 组件容器
private System.ComponentModel.IContainer components = null;
// 释放组件的方法
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
// 窗体控件初始化
private void InitializeComponent()
{
lblTime = new Label();
btnStart = new Button();
btnPause = new Button();
btnStop = new Button();
btnContinue = new Button();
txtSavedTime = new TextBox();
SuspendLayout();
//
// lblTime
//
lblTime.AutoSize = true;
lblTime.Font = new Font("Segoe UI", 36F, FontStyle.Regular, GraphicsUnit.Point);
lblTime.ForeColor = Color.FromArgb(60, 60, 60);
lblTime.Location = new Point(62, 49);
lblTime.Name = "lblTime";
lblTime.Size = new Size(362, 81);
lblTime.TabIndex = 0;
lblTime.Text = "00:00:00:000";
//
// btnStart
//
btnStart.BackColor = Color.FromArgb(240, 240, 240);
btnStart.FlatStyle = FlatStyle.Flat;
btnStart.Font = new Font("Segoe UI", 14F, FontStyle.Regular, GraphicsUnit.Point);
btnStart.ForeColor = Color.FromArgb(60, 60, 60);
btnStart.Location = new Point(24, 160);
btnStart.Name = "btnStart";
btnStart.Size = new Size(100, 50);
btnStart.TabIndex = 1;
btnStart.Text = "开始";
btnStart.UseVisualStyleBackColor = false;
btnStart.Click += btnStart_Click;
//
// btnPause
//
btnPause.BackColor = Color.FromArgb(240, 240, 240);
btnPause.Enabled = false;
btnPause.FlatStyle = FlatStyle.Flat;
btnPause.Font = new Font("Segoe UI", 14F, FontStyle.Regular, GraphicsUnit.Point);
btnPause.ForeColor = Color.FromArgb(60, 60, 60);
btnPause.Location = new Point(140, 160);
btnPause.Name = "btnPause";
btnPause.Size = new Size(100, 50);
btnPause.TabIndex = 2;
btnPause.Text = "暂停";
btnPause.UseVisualStyleBackColor = false;
btnPause.Click += btnPause_Click;
//
// btnStop
//
btnStop.BackColor = Color.FromArgb(240, 240, 240);
btnStop.Enabled = false;
btnStop.FlatStyle = FlatStyle.Flat;
btnStop.Font = new Font("Segoe UI", 14F, FontStyle.Regular, GraphicsUnit.Point);
btnStop.ForeColor = Color.FromArgb(60, 60, 60);
btnStop.Location = new Point(374, 160);
btnStop.Name = "btnStop";
btnStop.Size = new Size(100, 50);
btnStop.TabIndex = 4;
btnStop.Text = "停止";
btnStop.UseVisualStyleBackColor = false;
btnStop.Click += btnStop_Click;
//
// btnContinue
//
btnContinue.BackColor = Color.FromArgb(240, 240, 240);
btnContinue.Enabled = false;
btnContinue.FlatStyle = FlatStyle.Flat;
btnContinue.Font = new Font("Segoe UI", 14F, FontStyle.Regular, GraphicsUnit.Point);
btnContinue.ForeColor = Color.FromArgb(60, 60, 60);
btnContinue.Location = new Point(259, 160);
btnContinue.Name = "btnContinue";
btnContinue.Size = new Size(100, 50);
btnContinue.TabIndex = 3;
btnContinue.Text = "继续";
btnContinue.UseVisualStyleBackColor = false;
btnContinue.Click += btnContinue_Click;
//
// txtSavedTime
//
txtSavedTime.BackColor = Color.FromArgb(245, 245, 245);
txtSavedTime.BorderStyle = BorderStyle.None;
txtSavedTime.Enabled = false;
txtSavedTime.Font = new Font("Segoe UI", 14F, FontStyle.Regular, GraphicsUnit.Point);
txtSavedTime.ForeColor = Color.FromArgb(60, 60, 60);
txtSavedTime.Location = new Point(140, 230);
txtSavedTime.Name = "txtSavedTime";
txtSavedTime.Size = new Size(300, 32);
txtSavedTime.TabIndex = 5;
//
// Form1
//
ClientSize = new Size(500, 300);
Controls.Add(txtSavedTime);
Controls.Add(btnStop);
Controls.Add(btnContinue);
Controls.Add(btnPause);
Controls.Add(btnStart);
Controls.Add(lblTime);
Name = "Form1";
Text = "计时器";
ResumeLayout(false);
PerformLayout();
}
// 窗体控件
private System.Windows.Forms.Label lblTime; // 显示时间的标签
private System.Windows.Forms.Button btnStart; // 开始按钮
private System.Windows.Forms.Button btnPause; // 暂停按钮
private System.Windows.Forms.Button btnStop; // 停止按钮
private System.Windows.Forms.Button btnContinue; // 继续按钮
private System.Windows.Forms.TextBox txtSavedTime; // 用于保存时间的文本框
}
}
效果图:
总结
本示例实现了一个简单的计时器,涵盖了计时的启动、暂停、继续和停止功能。通过使用 Task
、CancellationToken
、ManualResetEvent
和按钮状态管理,我们可以更好地控制计时器的运行,并确保 UI 在多线程环境下保持响应。这个结构也便于扩展,可以根据需求进一步增加功能,比如记录多个计时任务,添加更多的操作等。