续 Winform中线程Timer类的使用(关于backgroundWorker类的使用)

本文介绍如何利用BackgroundWorker类处理数据复制任务,并通过Timer类每800毫秒更新一次进度条,实现数据复制过程中进度的实时显示。

      上一篇文章(Winform中线程类的Timer使用),发现其中有不少错误和不合理的地方。在上文中用到了System.Threading.Timer,这个定时器可以按照你指定的时间间隔来执行一个方法。我当时是用这个定时器来监控一个状态变量,一旦该状态变量改变了则执行线程 t 的销毁工作。但是后来想想总感觉这样做不太好,太被动。而且当时显示进度条的方法里面是一个while(true)的死循环,从而引发了这样一个问题,当线程开始执行这个死循环后就出不来了!即便后台的复制工作已经完成,也没法跳转出来,直到抛异常。

      今天重新想了一下,改了一下定时器的用处,并且加入了.NET2.0里面新添加的backgroundWorker类。现在定时器用来没隔800毫秒就执行一次显示进度条的方法,而backgroundWorker类则用来处理数据的复制工作。

      该类有三个事件:

      DoWork                            调用RunWorkAsync()方法时会触发该事件

      ProgressChanged              调用ReportProgress()方法会触发该事件

      RunWorkerCompleted       DoWork得到返回值时会触发该事件

      用定时器来执行进度条的显示,就没必要再用死循环了,每次只执行一次,在RunWorkerCompleted事件里,销毁定时器。这样基本也可以实现在复制数据的同时,基本实时的显示进度条。

      具体代码如图:

复制数据时显示进度条的代码

<think>我们在处理WinForms中的多线问题时,需要特别注意:WinForms控件不是线安全的,这意味着我们不能直接在非UI线(即创建控件的线)中更新控件的内容。否则,可能会导致不可预知的错误或序崩溃。 根据引用[^1]的建议,对于倒计时这简单的应用,使用System.Windows.Forms.Timer可以避免跨线问题,因为它的事件处理是在UI线上执行的。但是,如果我们的需求是在后台线(例如,通过Task、Thread等创建)中执行耗时操作,并在操作过中更新UI控件,那么就需要使用其他方法。 常见的解决方案有以下几种: 1. 使用Control.Invoke或Control.BeginInvoke方法 这是最传统的方法。这些方法会将委托封送到创建控件的线(UI线)上执行。Invoke是同步的,而BeginInvoke是异步的。 2. 使用BackgroundWorker组件 BackgroundWorker.NET Framework提供的一个组件,它封装了后台线和UI线的交互,通过事件驱动的方式,可以在后台任务执行过中报告进度,并在任务完成时在UI线上触发事件。 3. 使用Task和async/await(.NET 4.0及以上) 在.NET Framework 4.0及以上,我们可以使用Task和async/await来简化异步编。通过将方法标记为async,然后在需要更新UI的地方使用await和控件的调度器(例如,使用TaskScheduler.FromCurrentSynchronizationContext())来确保更新操作在UI线上执行。 4. 使用SynchronizationContext SynchronizationContext提供了一个通用的方式来处理线同步。在WinForms中,我们可以捕获UI线的SynchronizationContext,然后在后台线使用它来将委托发送到UI线执行。 下面我们分别举例说明: ### 方法1:使用Control.Invoke ```csharp // 在后台线中更新控件 private void UpdateControl(string text) { if (textBox1.InvokeRequired) // 检查是否在非UI线 { // 创建一个委托来调用自身,并通过Invoke封送到UI线 textBox1.Invoke(new Action<string>(UpdateControl), text); } else { textBox1.Text = text; } } ``` ### 方法2:使用BackgroundWorker ```csharp private BackgroundWorker worker = new BackgroundWorker(); public Form1() { InitializeComponent(); worker.DoWork += Worker_DoWork; worker.ProgressChanged += Worker_ProgressChanged; worker.WorkerReportsProgress = true; } private void StartButton_Click(object sender, EventArgs e) { worker.RunWorkerAsync(); } private void Worker_DoWork(object sender, DoWorkEventArgs e) { for (int i = 0; i <= 100; i++) { // 模拟耗时操作 Thread.Sleep(100); // 报告进度,会触发ProgressChanged事件 worker.ReportProgress(i, $"Processing {i}%"); } } private void Worker_ProgressChanged(object sender, ProgressChangedEventArgs e) { // 这里是在UI线中执行的 progressBar1.Value = e.ProgressPercentage; label1.Text = e.UserState.ToString(); } ``` ### 方法3:使用async/await ```csharp private async void StartButton_Click(object sender, EventArgs e) { // 在后台线执行耗时操作 await Task.Run(() => { for (int i = 0; i <= 100; i++) { Thread.Sleep(100); // 更新UI,通过Invoke到UI线 this.Invoke(new Action(() => { progressBar1.Value = i; label1.Text = $"Processing {i}%"; })); } }); } ``` 或者更好的方式是,在需要更新UI的地方使用await和UI线的上下文: ```csharp private async void StartButton_Click(object sender, EventArgs e) { // 获取UI线的上下文 var context = SynchronizationContext.Current; await Task.Run(() => { for (int i = 0; i <= 100; i++) { Thread.Sleep(100); // 使用捕获的上下文将更新发送到UI线 context.Post(new SendOrPostCallback((state) => { progressBar1.Value = i; label1.Text = $"Processing {i}%"; }), null); } }); } ``` 但实际上,在async/await中,我们更常见的做法是使用Progress<T>模式,或者直接在需要更新UI的地方使用Invoke。 ### 方法4:使用SynchronizationContext ```csharp private void StartButton_Click(object sender, EventArgs e) { // 获取当前同步上下文(UI线的) var context = SynchronizationContext.Current; // 启动一个任务 Task.Run(() => { for (int i = 0; i <= 100; i++) { Thread.Sleep(100); // 通过同步上下文将更新发送到UI线 context.Post(new SendOrPostCallback((state) => { progressBar1.Value = i; label1.Text = $"Processing {i}%"; }), null); } }); } ``` ### 总结 - 对于简单的定时更新(如倒计时),使用`System.Windows.Forms.Timer`是最简单的方法,因为它的事件在UI线触发。 - 对于需要后台执行长时间任务并更新UI的情况,推荐使用`async/await`结合`Task.Run`,并在更新UI时使用`Control.Invoke`(或者使用`Progress<T>`报告进度)或者使用`BackgroundWorker`(在.NET Framework 2.0中引入,适用于早期版本)。 - 使用`SynchronizationContext`也是一种通用方法,但代码量略多。 根据你的具体场景选择合适的方法。通常,使用`async/await`模式是现代的、推荐的做法。 ### 注意事项 - 在非UI线中更新UI控件会引发`InvalidOperationException`异常,因此必须确保在UI线中更新。 - 使用Invoke或BeginInvoke时,注意避免死锁(例如,在UI线上阻塞等待后台任务完成,而后台任务又需要Invoke到UI线)。 - 在窗体关闭时,要妥善处理后台任务,避免尝试更新已销毁的控件。 希望这些方法能帮助你解决在WinForm线中更改控件内容的问题。 </think>### WinForm在线中更改控件内容的解决方案 在WinForm开发中,**控件不是线安全**的,直接在其他线修改控件内容会导致`InvalidOperationException`异常("跨线操作无效")。以下是三种标准解决方案: --- ### 方案1:使用Control.Invoke(同步调用) 最安全的方法,强制在UI线执行操作 ```csharp // 在其他线中调用 this.Invoke((MethodInvoker)delegate { // 此处操作控件(UI线执行) label1.Text = "更新内容"; progressBar1.Value = 50; }); ``` **优点**:保证线安全 **缺点**:阻塞当前线直到UI线执行完毕 --- ### 方案2:使用Control.BeginInvoke(异步调用) 非阻塞式调用,适用于实时性要求高的场景 ```csharp // 在其他线中调用 this.BeginInvoke((MethodInvoker)(() => { // 此处操作控件(UI线异步执行) button1.Enabled = false; textBox1.AppendText("操作完成\n"); })); ``` **优点**:不阻塞当前线 **注意**:需处理可能的操作延迟 --- ### 方案3:使用BackgroundWorker组件(推荐) .NET内置线安全组件,适合后台任务 ```csharp BackgroundWorker worker = new BackgroundWorker(); worker.DoWork += (s, e) => { // 后台操作(非UI线) Thread.Sleep(5000); // 模拟耗时操作 }; worker.RunWorkerCompleted += (s, e) => { // 自动在UI线执行 labelResult.Text = "任务完成"; }; worker.RunWorkerAsync(); // 启动后台任务 ``` **最佳实践**: 1. 耗时操作放在`DoWork` 2. UI更新放在`RunWorkerCompleted` 3. 进度报告用`ProgressChanged`事件 --- ### 特殊场景优化 #### 1. 跨窗体操作 ```csharp Form mainForm = Application.OpenForms["MainForm"]; mainForm?.Invoke(() => { /* 更新操作 */ }); ``` #### 2. 进度实时更新 ```csharp // 在BackgroundWorker中 worker.ReportProgress(30); // 触发ProgressChanged事件 worker.ProgressChanged += (s, e) => { progressBar.Value = e.ProgressPercentage; // UI线自动执行 }; ``` #### 3. 扩展方法简化调用 ```csharp public static void UIThread(this Control control, Action action) { if (control.InvokeRequired) control.Invoke(action); else action(); } // 使用示例 label1.UIThread(() => label1.Text = "更新内容"); ``` --- ### 关键注意事项 1. **InvokeRequired检查**:调用前检查是否需跨线 ```csharp if (label1.InvokeRequired) { label1.Invoke(...); } else { // 直接操作 } ``` 2. **避免死锁**:勿在UI线同步等待工作线 3. **资源释放**:使用`using`语句管理BackgroundWorker 4. **线安全控件**:.NET 5+的`Control.IsAncestorSiteInDesignMode`可检测设计时状态 > 对于倒计时器等简单场景,`System.Windows.Forms.Timer`可直接在UI线安全更新控件[^1]
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值