Winform 进度条弹窗和任务控制
目录
Winform 进度条弹窗和任务控制
一、弹窗前台
二、弹窗后台
三、使用方法
四、效果展示和代码地址
独立观察员 2020 年 11 月 17 日
最近要给一个 Winform 项目添加功能,需要一个能显示进度条的弹窗,还要求能够中止任务,所以就做了一个,在此做个记录总结。虽然用的是比较老的 Winform 技术,不过其中的原理都是相通的。
一、弹窗前台
首先提供一个 Winform 控件居中的小技巧:
将控件放在 TableLayoutPanel 容器中,然后将控件的 Anchor 属性设置为 None,这样控件就能在容器中居中了:
将容器的 Anchor 属性设置为 Top, Left, Right,这样容器就能随着窗口左右拉伸了:
最终弹窗界面如下:
使用了 CSkin 界面库(v16.1.14.3),(注意:如果拖拽 dll 到工具箱拖不了,可以使用右键复制粘贴的方式),窗体继承 Skin_DevExpress,进度条使用 SkinProgressBar,按钮使用 SkinButton,主要是使用了一些圆角效果:
二、弹窗后台
先添加两个事件供外界订阅,分别为窗体载入时触发的执行操作事件,和点击中止按钮后触发的终止操作事件:
/// <summary> | |
/// 执行操作事件 | |
/// </summary> | |
public event Action OperateAction; | |
/// <summary> | |
/// 终止操作事件 | |
/// </summary> | |
public event Action AbortAction; | |
/// <summary> | |
/// 中止按钮点击事件 | |
/// </summary> | |
private void btn_Abort_Click(object sender, EventArgs e) | |
{ | |
AbortAction?.Invoke(); | |
DialogResult = DialogResult.Abort; | |
//Close (); // 不需要手动关闭; | |
} | |
/// <summary> | |
/// 窗体载入事件 | |
/// </summary> | |
private void FormProgressDialog_Load(object sender, EventArgs e) | |
{ | |
Task.Factory.StartNew(() => | |
{ | |
OperateAction?.Invoke(); | |
DialogResult = DialogResult.OK; | |
}); | |
} |
点击中止按钮后还将弹窗结果设为 Abort,会自动关闭弹窗;而业务操作正常执行完毕,弹窗结果为 OK。
供外界设置文本信息以及进度条进度的方法如下:
/// <summary> | |
/// 设置显示信息 (值为 null 时保持不变) | |
/// </summary> | |
/// <param name="rtfTitleContent"> 富文本格式的标题内容 </param> | |
/// <param name="totalMessage"> 总体消息 </param> | |
/// <param name="currentMessage"> 当前消息 </param> | |
public void SetInfo(string rtfTitleContent = null, string totalMessage = null, string currentMessage = null) | |
{ | |
if (rtfTitleContent != null) rtb_Title.Rtf = rtfTitleContent; | |
if (totalMessage != null) lbl_Total.Text = totalMessage; | |
if (currentMessage != null) lbl_Current.Text = currentMessage; | |
} | |
/// <summary> | |
/// 设置进度 | |
/// </summary> | |
/// <param name="currentValue"> 当前数值 </param> | |
/// <param name="totalValue"> 总数值 </param> | |
public void SetProsess(double currentValue, double totalValue) | |
{ | |
try | |
{ | |
progressBar.Value = (int)(currentValue / totalValue * 100); | |
} | |
catch (Exception ex) | |
{ | |
Console.WriteLine(ex); | |
} | |
} |
剩下就是两个设置富文本框 RichTextBox 的方法,包括设置彩色内容和隐藏 RichTextBox 光标的方法,文末会给出代码地址,此处不再赘述。
三、使用方法
首先映入眼帘的是两个成员变量,一个是用于任务取消的 CancellationTokenSource 对象,另一个是用于线程同步的 AutoResetEvent 对象(用于取消任务后的一些信息同步);然后是主测试方法(一个按钮点击事件方法)中的一些信息设置:
然后设置 CancellationTokenSource 对象的 Token,给它注册一个取消任务时调用的委托方法,里面先等待同步信号结果再进行本次执行结果的判断:
接下来订阅弹窗中的那两个事件,在执行操作事件中开启任务,并传递 Token;在中止事件中停止任务:
需要注意的是,停止任务后,任务内部并不会自己停止,需要判断 Token 的 IsCancellationRequested 字段来决定相应的操作,比如结束循环。然后,因为在之前注册的取消的委托方法中,进行了等待,所以我们在执行完业务方法(BusinessMethod)并设置好相关状态值后,需要判断任务是否取消,如果取消,说明注册的取消的委托方法中已经在等待了,所以要调用 Set () 进行放行。
有人可能就会问了,foreach 循环开始时不是判断过是否取消了吗?这里怎么又判断?这是因为,比如在一轮循环中,已经执行过了开头的是否已取消的判断(IsCancellationRequested 为 false),开始执行耗时的业务方法了,此时用户点击中止按钮,IsCancellationRequested 被置为 true,所以业务方法执行后再次判断会得到最新的状态,然后,循环将在下一轮开始时结束。
另外,由于实际使用这个的项目是 .NET 4.0 框架,所以 Task 的一些方法没有,大家用新框架的话可以使用新方法。或者使用 Microsoft.Bcl.Async 包,然后使用 TaskEx。
继续流程,接下来以模态框方式弹出窗口,并获取结果。业务处理方法中模拟了耗时操作并返回是否成功。
最后给出完整代码:
private CancellationTokenSource _Cts; // 任务取消令牌; | |
private AutoResetEvent _AutoResetEvent = new AutoResetEvent(false);// 参数传 false,则 WaitOne 时阻塞等待; | |
/// <summary> | |
/// 测试任务进度弹窗 | |
/// </summary> | |
private void BtnProgressDialog_Click(object sender, EventArgs e) | |
{ | |
_AutoResetEvent.Reset(); | |
string businessName = "业务 1"; | |
FormProgressDialog progressWindow = new FormProgressDialog() | |
{ | |
Text = "任务处理窗口", | |
}; | |
progressWindow.SetColorfulTitle("业务 1", Color.DarkOrange, true); | |
progressWindow.SetColorfulTitle("正在执行中......", Color.Black); | |
progressWindow.SetInfo(null, "", ""); | |
List<string> orders = new List<string>(){"订单 1", "订单 2", "订单 3", "订单 4", "订单 5" }; // 业务数据; | |
List<string> leftList = orders.Select(x => x).ToList(); // 剩余(未处理)数据; | |
int successCount = 0; // 成功数量; | |
_Cts = new CancellationTokenSource(); | |
// 注册一个将在取消此 CancellationToken 时调用的委托; | |
_Cts.Token.Register(async () => | |
{ | |
ShowInfo("操作终止"); | |
await Task.Run(() => | |
{ | |
_AutoResetEvent.WaitOne(1000 * 5); // 等待有可能还在执行的业务方法; | |
if (successCount < orders.Count) | |
{ | |
ShowInfo($"{businessName} 有 {orders.Count - successCount} 项任务被终止,可在消息框中查看具体项。"); | |
foreach (var leftName in leftList) | |
{ | |
ShowInfo($"【{businessName}】的【{leftName}】执行失败,失败原因:【手动终止】。"); | |
} | |
} | |
}); | |
}); | |
progressWindow.OperateAction += () => | |
{ | |
Task task = new Task(() => | |
{ | |
foreach (var order in orders) | |
{ | |
// 判断是否被取消; | |
if (_Cts.Token.IsCancellationRequested) | |
{ | |
break; | |
} | |
progressWindow.TryBeginInvoke(new Action(() => | |
{ | |
progressWindow.SetInfo(null, $" 共 {orders.Count} 项,已执行 {successCount} 项 ", $" 当前正在执行:{order}"); | |
})); | |
if (BusinessMethod(order, businessName)) | |
{ | |
successCount++; | |
leftList.RemoveAll(x => x == order); | |
if (_Cts.Token.IsCancellationRequested) | |
{ | |
_AutoResetEvent.Set(); // 放行 Register 委托处的等待; | |
} | |
} | |
progressWindow.TryBeginInvoke(new Action(() => | |
{ | |
progressWindow.SetProsess(orders.IndexOf(order) + 1, orders.Count); | |
})); | |
} | |
}, _Cts.Token); | |
task.Start(); | |
task.Wait(); | |
}; | |
progressWindow.AbortAction += () => | |
{ | |
_Cts.Cancel(); | |
}; | |
var result = progressWindow.ShowDialog(); | |
int leftCount = orders.Count - successCount; | |
if (result == DialogResult.OK || leftCount <= 0) | |
{ | |
ShowInfo($"{businessName} 整体完成。"); | |
} | |
else if (result == DialogResult.Abort) | |
{ | |
// 移到 _Cts.Token.Register 处一起判断,不然数目可能不准; | |
//ShowInfo ($"{businessName} 有 {leftCount} 项任务被终止,可在消息框中查看具体项。"); | |
} | |
} | |
/// <summary> | |
/// 业务处理方法 | |
/// </summary> | |
private bool BusinessMethod(string order, string businessName) | |
{ | |
string errStr = $"【{businessName}】的 {order} 任务失败,失败原因:"; | |
// 测试 | |
Thread.Sleep(1000 * 2); | |
try | |
{ | |
// 业务方法; | |
ShowInfo($"【{businessName}】的 {order} 任务执行成功。"); | |
return true; | |
} | |
catch (Exception ex) | |
{ | |
ShowInfo($"{errStr}{ex.Message}"); | |
} | |
return false; | |
} | |
四、效果展示和代码地址
正常执行(动图):
中止执行(动图):
代码地址:https://gitee.com/dlgcy/Practice/tree/master/WinFormPractice