WinForms 里放一个 ProgressBar,循环里每处理一条就加 1,结果界面一直白着,进度条到最后直接跳到 100。这个现象我见过太多次,原因几乎都一样:循环跑在 UI 线程上,界面根本没机会重绘。

为什么会卡

WinForms 是单线程模型,控件操作都在 UI 线程完成。你在按钮事件里写了个几千次的 for 循环,这期间消息循环被占住,Paint 消息排在队列里进不来,界面就冻住了。DoEvents 能缓解,但重入问题很难查,不建议当常规方案。

正确姿势:后台干活,UI 只收进度

现在推荐 async/await 配合 IProgress<T>,代码短也不容易写错。

private async void btnStart_Click(object sender, EventArgs e)
{
    progressBar1.Maximum = total;
    var progress = new Progress<int>(v => progressBar1.Value = v);
    await Task.Run(() => DoWork(progress));
}

private void DoWork(IProgress<int> p)
{
    for (int i = 0; i < total; i++)
    {
        Thread.Sleep(50);
        p.Report(i + 1);
    }
}

Progress<T> 创建时捕获当前同步上下文,Report 会自己切回 UI 线程,回调里改控件是安全的,不用 Invoke。用 BackgroundWorker 则在 ProgressChanged 里更新。

值设了却没动,检查这几处

  • Maximum 没设:默认是 100,你按 1000 条累加,超过上限就不动了。
  • Value 溢出:超过 Maximum 会抛异常,不是静默截断。
  • Style 是 Marquee:这种样式表示不确定进度,忽略 Value,只做动画。
  • 没刷新:真在 UI 线程里跑循环,需要偶尔 DoEvents,但更该改架构。

拿不到真实进度怎么办

比如等一个外部接口返回,这种场景不要伪造一个 37% 然后卡住,用户会以为程序坏了。直接把样式设成 Marquee 做循环动画就好。

progressBar1.Style = ProgressBarStyle.Marquee;
progressBar1.MarqueeAnimationSpeed = 30;

WPF 那边思路一样,只是更推荐把进度做成 ViewModel 属性并通知,测试也方便。

最后补一句:进度更新别太频繁,每处理一条就 Report 一次,上万条时光刷新 UI 就能拖慢一半。按百分比或每 100 条报一次,视觉上完全够用。