简介:基于对话框的多线程进度条更新示例,面向MFC初学者和Windows桌面开发者,演示在VC6环境下利用工作线程执行后台任务,再通过自定义消息通知主线程刷新对话框中的进度条,以避免耗时操作导致界面卡顿。压缩包采用RAR格式,共25个文件,大小约1.95MB,内含对话框程序与工作线程类的源文件、VC6工程文件、资源脚本、说明文档及可直接运行的exe,便于对照源码开展编译调试。目前已有607人学习下载。示例围绕线程类派生、运行函数重写、线程间消息通信与进度条控件更新展开,并介绍了临界区等同步对象的使用,可帮助开发者理解如何安全共享界面资源。通过自定义消息声明、消息发送和进度条SetPos调用等具体实现,读者可以完整掌握后台任务进度反馈的写法;工程中还包含调试信息和构建辅助文件,能直观展示VC6下MFC项目的完整结构,适合正在学习多线程编程的开发者动手实践。
1. 基于对话框的多线程进度条更新示例:先想清楚这三件事再动手
很多桌面程序的第一版都会这样写:在按钮点击事件里 for 循环,循环体里调 progressBar.Value = i,跑完再把 result 弹出来。结果任务一重,窗口直接白屏,系统提示“该程序无响应”;任务轻点窗口倒是动了,但进度条像抽风一样跳。真正要解决这件事,靠的正是“基于对话框的多线程进度条更新”这套经典的组合:对话框负责交互,工作线程跑耗时逻辑,UI 线程只做进度的忠实呈现。想让这套组合可靠,得先想清楚三件事:为什么后台线程不能直接改控件,更新进度用哪种回调不掉消息,以及任务被取消时对话框和线程怎么同时收场。这篇文章把这些拆开讲,配合一段能直接复制的 WinForms 示例,新手能跟着落地,熟手也能看看边界条件。
2. 为什么界面必须由UI线程更新:跨线程操作背后的线程模型
2.1 控件的线程关联(Thread Affinity)让“直接改进度条”变成高风险动作
Windows 上面的窗体、按钮、进度条,本质上都是窗口(HWND)。窗口有一个非常重要的属性:它归属于创建它的那个线程,也就是通常说的 UI 线程或主线程。消息循环处理键盘、鼠标、重绘、系统命令,都在这个线程里排队执行。控件的线程关联(Thread Affinity)决定了:只有 UI 线程可以直接对控件做实质性的写入操作,包括设置位置、文本、进度值。
后台线程直接设置 progressBar.Value 时,在 WinForms 里,系统会检测并告诉你“跨线程操作无效:从不是创建控件……的线程访问它”,然后抛一个 InvalidOperationException。MFC 里的 CProgressCtrl::SetPos 不一定抛异常,但可能把进度条内部状态改乱,或者根本不重绘,表现出来就是任务跑完了,进度条还停在老位置。更隐蔽的情况是,某些第三方控件不报错,但界面偶发闪烁、内存里句柄混乱,这种问题最不好排查。
所以业界立了一条规矩:后台线程永远不要碰控件。后台线程只负责“生产数据”,把要显示的进度值作为一条消息交还给 UI 线程去“消费”。这么理解其实就是一个生产者-消费者模型,后面所有更新方式都围绕它转。
2.2 让更新“排队”到UI线程:Invoke、BeginInvoke与消息队列
WinForms 里最直接的工具是 Control.Invoke 和 Control.BeginInvoke。它们做的事情都是在 UI 线程的消息循环里插入一个委托,让 UI 线程在空闲的时候去执行。Invoke 是同步的:后台线程会一直等着,直到 UI 线程把委托执行完;BeginInvoke 是异步的:调用立刻返回,委托排队等待执行。对进度条更新来说,用 BeginInvoke 是至少不出错的起点:
// 在后台线程中调用,把进度值交给 UI 线程 private void UpdateProgress(int value, int total) { if (progressBar1.IsHandleCreated) { progressBar1.BeginInvoke(new Action(() => { progressBar1.Value = value; labelStatus.Text = $"{value} / {total}"; })); } }这段代码的逻辑是:先判断控件句柄是否已创建,避免对话框还没显示出来时调用 BeginInvoke 导致异常;然后用 BeginInvoke 投递一个委托,在 UI 线程上把进度条和状态文字一起更新。参数 value 和 total 是当前完成量和总量,Minimum 和 Maximum 已经映射好,这里只需要把 Value 设置过去。BeginInvoke 的委托在 UI 线程空闲时执行,不会阻塞后台干活,是导入导出、文件复制这类任务的标准写法。
不过 BeginInvoke 不是银弹。如果后台循环很快,比如每毫秒都更新一次,消息队列里会堆积大量更新委托,UI 线程处理不过来,界面照样卡。所以更严格的做法是给更新加上节流,这往往被很多人忽略,后面第 4 章专门讲。另外,Invoke 同步方式最大的坑是容易死锁:后台线程 Invoke 等待 UI 线程,而 UI 线程又在等待后台线程结束,两个线程相互等,就翻车了。所以我的习惯是:进度更新一律用 BeginInvoke,不用 Invoke。
提示:如果后台任务已经退出,UI 线程正在 await 另一个操作,此时 Invoke 的委托会一直排不到,尽量别在紧耦合的任务模型里依赖同步调用。
2.3 为什么不能只用Timer轮询共享变量:可见性、误报和浪费
另一个常见替代方案是后台线程写一个共享变量,UI 线程用 Timer 每隔 100ms 读一次进度。这种做法能跑,但有两个明显问题:第一,Java、C++、C# 都要求对多线程共享变量做同步,一个简单 int 也许侥幸不出错,但只要变成进度字符串、阶段枚举、批量完成列表,就说不清了;第二,UI 定时器本身是多余的唤醒,轮询周期内进度没变化还要执行一次,白白耗 CPU。
private volatile int _currentProgress; private System.Windows.Forms.Timer _timer; private void StartWorkWithPolling() { _timer = new System.Windows.Forms.Timer { Interval = 100 }; _timer.Tick += (s, e) => progressBar1.Value = _currentProgress; _timer.Start(); Task.Run(() => { for (int i = 0; i < 100; i++) { Thread.Sleep(30); // 模拟耗时 _currentProgress = i; } }); }这段示意代码里,_currentProgress 是 volatile 的 int,后台线程持续写入,UI Timer 持续读取。参数 Interval=100 表示每 100ms 一次,30ms 的 Sleep 模拟每个零件的处理时间。如果要支持取消,Thread.Sleep 无法被中断,必须改成 CancellationToken。实际任务里,如果总进度在一个线程里计算,这种方法也还能用;但如果出现多线程累加进度,就得上锁或使用 Interlocked,复杂度立刻上来了。
相比之下,事件/回调驱动的更新更符合 GUI 的工作方式:后台线程在某个里程碑时抛一个事件或发一个消息,UI 线程被动地刷新。这个思路贯穿后面的示例,也是 MFC、Qt、WPF 等框架共同使用的模型。
3. 从零搭一个进度对话框:WinForms最小可运行代码
3.1 界面布局与参数设定:进度条三个关键属性
实现前先把界面搭清楚。新建一个 WinForms 应用,添加一个名为 ProgressDialog 的窗体,在窗体上放一个 ProgressBar、一个 Label 和一个 Button(取消)。按实际需求,可以把 Button 的 DialogResult 设成 Cancel,也可以不用。模态展示由主窗体调用 ShowDialog 完成,对话框内使用独立的后台任务。
进度条要正确显示,三个属性必须从数据模型里映射好。Minimum 表示任务起点,Maximum 表示终点,Value 表示当前进度。对于“处理文件、上传分块”这类步数明确的任务,Minimum 用 0,Maximum 用总步数,Value 等于已完成步数。对于“耗时循环但总量不明”的任务,也可以用 Style 为 Marquee 的跑马灯模式,此时 Value 由系统管理,不需要手动设置。常见的翻车点是 Maximum 设置得过小或过大,加法运算时又没做范围限制,Value 超出 Maximum 或小于 Minimum 会直接抛异常。如果 Maximum 比真实任务步数少,进度条会提前满格,后面只能干瞪眼。
我一般还会把 Label 的初始文本写成“准备中...”,在后台任务第一次回调时再改成具体数字。这一步看似多余,却能让用户在第一毫秒就获得反馈,避免“点了按钮界面像死了”的糟糕体验。
3.2 后台线程跑任务,UI线程刷进度:一段可以抄的最小代码
下面这个类是完整的进度对话框骨架。它把任务放在 Task.Run 里执行,UpdateProgress 方法用 BeginInvoke 把进度值投递到 UI 线程,取消按钮通过 CancellationTokenSource 通知任务停止。
public partial class ProgressDialog : Form { private readonly CancellationTokenSource _cts = new CancellationTokenSource(); public ProgressDialog(int totalSteps) { InitializeComponent(); progressBar1.Minimum = 0; progressBar1.Maximum = totalSteps; progressBar1.Value = 0; } protected override void OnShown(EventArgs e) { base.OnShown(e); Task.Run(() => RunTaskAsync(_cts.Token)); } private async Task RunTaskAsync(CancellationToken ct) { int total = progressBar1.Maximum; for (int i = 0; i < total; i++) { ct.ThrowIfCancellationRequested(); await Task.Delay(80, ct); // 模拟耗时操作,比如处理一条记录 UpdateProgress(i + 1, total); } BeginInvoke(new Action(() => { DialogResult = DialogResult.OK; })); } private void UpdateProgress(int value, int total) { if (progressBar1.IsHandleCreated) { BeginInvoke(new Action(() => { progressBar1.Value = value; labelStatus.Text = $"{value} / {total}"; })); } } private void btnCancel_Click(object sender, EventArgs e) { _cts.Cancel(); btnCancel.Enabled = false; // 防止重复点击 } protected override void OnFormClosed(FormClosedEventArgs e) { _cts?.Cancel(); _cts?.Dispose(); base.OnFormClosed(e); } }逻辑说明:进度条参数由构造函数的 totalSteps 决定,任务运行时在 for 循环的每个迭代里做三件事:检查取消令牌、模拟耗时、更新界面。中段的await Task.Delay(80, ct)不是自旋等待,它是让出当前线程,避免为了演示而真的去占 CPU。Task.Run 把执行体丢到线程池,UI 线程得以继续响应用户的取消点击。最终任务结束时通过 BeginInvoke 设置 DialogResult,让 ShowDialog 返回。注意这里的 DialogResult 只在外部用 ShowDialog 打开时才自动关闭。
参数说明:totalSteps 应该由调用方根据任务的实际总量传入,不要随手写死 100。Task.Delay 的第二参数是 CancellationToken,取消时会抛 OperationCanceledException,当前代码没有捕获它,所以取消路径还不完善,第 4 章会补全。
3.3 对话框关闭时怎么收尾:不能把线程丢在后台自生自灭
很多从示例抄下来的人,会在任务结束后直接调用 Close(),但忽略了用户可能在任务中点击对话框右上角 X。这样窗口销毁了,线程还在运行,轻则资源泄漏,重则回调时访问已销毁的句柄,抛 ObjectDisposedException。稳妥的做法是在 OnFormClosed 里做三件事:
protected override void OnFormClosed(FormClosedEventArgs e) { if (!_cts.IsCancellationRequested) _cts.Cancel(); _cts.Dispose(); base.OnFormClosed(e); }这段代码在窗体被关闭时,无论如何都通知后台任务停止,并释放令牌资源。因为 Task.Run 的执行体里已经设置了ct.ThrowIfCancellationRequested(),任务会在下一个循环迭代退出。如果后台是长阻塞的 IO 操作,还需要把 OperationCanceledException 接住,或者让 IO 操作尽早感知取消。如果担心业务线程没有及时退出,可以在 OnFormClosing 里task.Wait(1000)等一秒,但千万不要无限期等待,否则 UI 线程又会卡死。更好的办法是保持取消通知,在资源清理时依赖 finally 块。
这种“窗口关闭即取消”的习惯,在实际业务中非常重要。文件复制、批量导入这类操作,如果用户关闭对话框就彻底放弃,就不会留下一个没人管的后台线程。
4. 从示例到生产可用:取消、节流与异常回传怎么接
4.1 真正的取消:任务循环里要响应,异常路径要处理
上一章里的取消操作只是让ThrowIfCancellationRequested()抛出一个异常,如果不捕获,这个异常会跑到 Task 内部,最终变成未观察异常,用户点击取消却什么都没发生。正确做法是在 RunTaskAsync 里捕获 OperationCanceledException,再把取消结果回传给 UI。
private async Task RunTaskAsync(CancellationToken ct) { try { int total = progressBar1.Maximum; for (int i = 0; i < total; i++) { ct.ThrowIfCancellationRequested(); await Task.Delay(80, ct); UpdateProgress(i + 1, total); } BeginInvoke(new Action(() => DialogResult = DialogResult.OK)); } catch (OperationCanceledException) { BeginInvoke(new Action(() => { labelStatus.Text = "已取消"; if (progressBar1.Style == ProgressBarStyle.Continuous) progressBar1.Value = progressBar1.Value; DialogResult = DialogResult.Cancel; })); } catch (Exception ex) { BeginInvoke(new Action(() => { MessageBox.Show(ex.Message, "任务失败", MessageBoxButtons.OK, MessageBoxIcon.Error); DialogResult = DialogResult.Abort; })); } }逻辑说明:try-catch 把任务跑完、用户取消、未知异常三种结果分开处理。取消时,UI 线程会收到 DialogResult.Cancel,主窗体可以据此判断用户是否要重新选择任务参数。未知异常不能静默丢失,否则用户看到的是“进度条卡在某个位置,对话框也不关”,但不知道任务为什么失败。这里把异常信息弹出来,是一种简单粗暴却有效的回传方式。
参数说明:MessageBox 的最后一个参数 MessageBoxIcon.Error 只影响图标,不会自动中止对话框,所以还要设置 DialogResult。注意取消分支里重新给 Value 赋值,是为了触发重绘,去掉也行,但按我的经验,高 DPI 或主题切换后有些控件的重绘时机很怪,手动碰一下能少一次翻车。
4.2 进度刷新节流三选一:限定频率、消抖、定时上报
节流是进度条生产化最容易被忽视的一环。假设后台任务每一秒能处理几千条短记录,如果每条记录都调用一次 BeginInvoke,消息队列里会堆积几千个委托,消息循环根本处理不完,任务结束后进度条还在原地“拖影”。常见做法是在回调处限制最短更新间隔。
private long _lastUpdateTime; private const long MinUpdateIntervalMs = 50; private void UpdateProgressThrottled(int value, int total) { long now = DateTime.UtcNow.Ticks / TimeSpan.TicksPerMillisecond; if (now - Interlocked.Read(ref _lastUpdateTime) < MinUpdateIntervalMs) return; Interlocked.Exchange(ref _lastUpdateTime, now); if (progressBar1.IsHandleCreated) { BeginInvoke(new Action(() => { progressBar1.Value = Math.Min(value, progressBar1.Maximum); labelStatus.Text = $"{value} / {total}"; })); } }逻辑说明:进入方法后先取当前毫秒时间,与上一次被放行的更新时间做差,小于 50 毫秒就直接扔掉本次更新,避免高频刷屏。Interlocked.Exchange 保证跨线程时间戳读写是原子的,不会出现两个线程同时写入,把时间戳改乱。Value 被 Math.Min 钳制到 Maximum,是为了在调度过程中总进度可能微超的情况下少一次异常。
参数说明:MinUpdateIntervalMs 取 50 毫秒,对应一秒最多 20 次刷新,既保证视觉流畅,又不会压垮 UI 线程。如果你的任务每步耗时长,间隔可以放宽到 100;如果只是显示百分比,30 毫秒也够。节流会丢掉部分更新,但在界面任务里,用户只看最后一帧,中间值丢了不可惜。
另一种做法是周期上报:后台任务用一个 Stopwatch 计时,每一个时间片额外汇报一次。这种做法的优点是不依赖系统时间,逻辑更集中;缺点是多一层状态位,代码不如上面简洁。我一般先用最简的节流,出现卡顿再往 Stopwatch 上切。
4.3 BackgroundWorker、async/await 与 Task.Run,这些工具怎么选
WinForms 里除了手动写 Task,还有 BackgroundWorker 和原生 async/await。老项目中经常见到 BackgroundWorker,它自带 ReportProgress 事件和进度百分比,还内置对取消的支持,代码写起来很直观:
worker = new BackgroundWorker(); worker.WorkerReportsProgress = true; worker.WorkerSupportsCancellation = true; worker.DoWork += (s, e) => { for (int i = 0; i < 100 && !worker.CancellationPending; i++) { Thread.Sleep(50); worker.ReportProgress((i + 1) * 10); } }; worker.ProgressChanged += (s, e) => progressBar1.Value = e.ProgressPercentage; worker.RunWorkerAsync();这段代码把业务放 DoWork,UI 更新放在 ProgressChanged,BackgroundWorker 内部已经把跨线程切换做了,不需要手动 Invoke。参数 WorkerReportsProgress 必须置为 true,否则 ReportProgress 会抛异常;进度百分比用 0 到 100 的整数表示,对更细粒度任务不友好。
三种方式的选型,我用一张表总结:
| 方式 | 跨线程切换 | 取消支持 | 异常捕获 | 适用场景 |
|---|---|---|---|---|
| BackgroundWorker | 自动 | 内置 CancelAsync | 自动回传 RunWorkerCompleted | 老项目、简单进度 |
| async/await + Task.Run | 手动 Invoke/await | CancellationToken | 可以自然是 await 捕获 | 新开发,网络/IO |
| Task.Run + 手动回传 | 手动 Invoke | CancellationToken | 需要 try-catch 或事件 | 更自由的控制 |
我个人偏爱 async/await 为主,因为它能把流程写成同步逻辑,取消也更规范。但注意await后面默认回到 UI 线程,不需要额外 Invoke;如果有ConfigureAwait(false),又会切回线程池,反而又要 Invoke。这个开关是很多“偶发跨线程异常”的来源,后面第 5 章再说。
如果项目是 MFC,则没必要套 WinForms 的概念。MFC 常用做法是工作组线程(AfxBeginThread)里 PostMessage 给对话框窗口,由对话框的 ON_MESSAGE 处理函数更新 CProgressCtrl。Qt 则用 worker 线程发信号到对话框的槽,Qt 的队列连接会自动在不同线程间投递。这些都属于“消息回传 UI 线程”的同一思路,换平台只换通信机制。
注意:如果你在一个类库项目里写进度上报,别让类库引用 WinForms 控件。把进度定义成事件或接口,UI 层再去订阅,这样单元测试和跨平台复用都更从容。
5. 避坑:进度条开发中我踩过的5个具体问题
5.1 进度条不刷新:卡在 0% 或一直满格
现象是任务跑完了,进度条还停在同一位置。第一反应是检查调用次数,打印日志发现 UpdateProgress 根本没进来,或者进来了但 Value 没有变化。原因有几种:后台线程修改的不是 UI 控件,而是把进度写到一个共享 int 后,UI 线程取用的时机不对;或者 BeginInvoke 确实调了,但 UI 线程自己卡在某个同步等待上,没空处理消息。还有一种常见误用:用户在主线程里直接跑耗时循环,然后又调用 DoEvents 希望刷新界面,这是把消息队列当垃圾桶,进度条只能在循环间隙刷新,而循环本身没有让出控制权。
解决:首先保证耗时逻辑在独立线程/任务里,不要在 UI 线程里循环。其次,在回调方法里用 Debug.WriteLine 打印当前值和控件句柄,确认 BeginInvoke 真的被调用。最后,检查 UI 线程是不是在 await 某个任务且用了.Result或.Wait(),这会把 UI 线程阻塞,导致消息队列停止。我在排查时会把所有同步等待都换成 await,这个问题立刻消失。
5.2 跨线程操作无效异常:WinForms 的规则不是警告是崩溃
现象是直接设置 progressBar.Value 时抛出“跨线程操作无效:从不是创建控件……的线程访问它”。原因不一定是后台线程直接赋值,还有一种隐藏情况:async 方法里的 await 后面没有ConfigureAwait(false),线程上下文被切走,后续代码跑到线程池又回来操作 UI,接着就抛异常。
解决:最简单的方法是在 UI 事件里不写复杂逻辑,把更新操作统一收敛到 BeginInvoke。使用 async/await 时,保持 await 后续自然回到 UI 线程,不要给耗时的操作添加 ConfigureAwait(false),除非你非常明确后续代码不碰 UI。我见过很多代码为了“避免上下文切换开销”到处加 ConfigureAwait(false),结果进度条更新到处抛异常,实际上这点开销远小于调试成本。严格来说,ConfigureAwait(false) 的语义不是“切到后台”,而是“不强制回原上下文”,在纯类库里用没问题,在 UI 层能不用就不用。
5.3 进度条在任务结束后才一下跳到满格
现象是任务执行期间界面流畅,进度条纹丝不动,到最后瞬间拉满。原因几乎总是消息积压。后台循环把每个中间值都塞进消息队列,UI 线程还没来得及处理,任务就结束并直接设置了最终 Value,中间所有更新被合并成一次绘制。使用性能分析,在任务前后各抓一次队列里的消息数量,能看到成千上万条待处理委托。
解决:采用第 4.2 的节流策略,限制最小更新间隔,或者只在每 N 条记录后汇报一次。这里有个经验值:如果一次任务需要 10 秒,进度条最低刷新频率 10Hz 就足够,用户不会觉得“死”。可以把 Minimum 到 Maximum 切成 200 个阶梯,超过该比例才汇报,这样消息量最多几百条。另外,Value 的变化最好保持单调递增,不要一会回退一会前进,否则人眼看起来很神经,而且更容易触发无意义的重绘。
5.4 点击取消没反应,或者取消后任务还在后台跑
现象是点取消按钮按钮变灰,但进度条继续前进,或者任务日志里还在打印。原因是没有在任务执行体内检查取消令牌,只是把 Cancel 状态设置好,然后 UI 线程就等在那里。后台循环还在继续跑,只有到了下一个ThrowIfCancellationRequested才退出;如果那个检查点在很后面,用户就会觉得点了没反应。还有的代码在 finally 里调用cts.Dispose(),这会让 CancellationToken 无法再被内部检查。
解决:把取消检查放在每个工作单元的起点和长耗时操作的周围;长阻塞操作无法用令牌打断时,改为异步 IO(如文件读写带 Cancel)或者超时退出。在按钮点击事件里,如果判断取消已经足够,可以直接把 DialogResult 设为 Cancel,但不要在工作线程里调用 Close,务必让对话框被启动方关闭。我在项目里习惯用一个BeginClose方法,里面保证只设置 DialogResult,不直接销毁控件,避免后台线程还在飞的时候访问已销毁的句柄。
5.5 任务完成后对话框不关闭,或资源释放得太早
现象:进度条到 100 之后窗口还开着,或者窗口关闭了但后台线程还在打日志。原因:DialogResult 设置时机不对,比如把它放在 Task.Run 内部却忘记用 BeginInvoke,导致窗体根本没感知;或者在 OnFormClosed 里立即 Dispose 了 CancellationTokenSource,而任务检查令牌时发现已释放,抛 ObjectDisposedException。解决:DialogResult 只从 UI 线程设置,Task.Run 内部用 BeginInvoke;关于令牌释放,必须在确认任务退出后的 finally 里做,或者用“先取消,再等待任务完成”规避。我给一个典型代码:
protected override void OnFormClosing(FormClosingEventArgs e) { _cts.Cancel(); _workerTask?.Wait(500); // 给任务500毫秒收尾,超时不再等 base.OnFormClosing(e); }逻辑说明:OnFormClosing 时先发取消,再试图等待任务最多500毫秒,确保资源可以安全释放。如果任务卡在 IO 上,超时会继续关闭,但至少要有一条日志让人知道任务没有及时结束。参数500毫秒是经验值,可调。这种写法能避开大部分因过早 Dispose 引发的异常。
6. 进阶:验证进度更新是否准确,以及MFC、Qt场景下的迁移思路
进度条在绝大多数应用里不是核心逻辑,但它承担着“用户信任”的任务。我做过几次数据导入功能,进度条从 0 到 100 只用了 2 秒,实际导入用了 20 秒,用户立刻在群里反馈“这软件是不是造假”。后来我养成一个习惯:验证进度更新是否与任务真实完成度挂钩。做法很简单,在任务的关键阶段打时间戳和当前完成量,输出成表,人工比对值和时间的走势是否单调。如果任务本身是并行处理的,还要把每个子任务的完成量按权重相加,保证最终总量等于 Maximum。没有这一步,进度条做出来就是一个会动的假货。
迁移到别的技术栈时,本质只有一套话术。MFC 工程里弹出一个模态对话框,后台线程通过PostMessage(hDlg, WM_MY_PROGRESS, (WPARAM)value, 0)把进度投递到对话框窗口,ON_MESSAGE处理函数里调用CProgressCtrl.SetPos(value)。Qt 则用QThread或QtConcurrent跑任务,通过信号currentProgress(int)连接到主界面槽函数,Qt 的队列连接保证跨线程安全。Java Swing 里是SwingUtilities.invokeLater(() -> progressBar.setValue(v)),概念一模一样。换平台只是把 BeginInvoke、PostMessage、signal、invokeLater 换一下名字。
我自己的收尾习惯很朴素:只要模态对话框出现,就必须有取消按钮和错误区域;只要进度小于 100,就不允许窗口无故消失。曾经因为跳过取消导致一个批处理任务跑了三个小时才发现参数错了,只能傻等它跑完,那时候真希望有人告诉我这个坑。现在每写一个进度对话框,我都会先把取消和异常路径打通,再回头去优化进度条动画。希望帮到你。
本文还有配套的精品资源,点击获取