C# Winforms - 进度条无法正确显示和重置

3

我正在使用Visual Basic中的Windows Forms编写一个Windows桌面应用程序。在这个应用程序中,我想显示一个简单的进度条,但是我遇到了一个奇怪的问题。下面是一个简单的for循环示例,该示例更新进度条:

pBar.Visible = true;
// pBar.Minimum and pBar.Step are both set to 1 using the properties pane in the design page
pBar.Maximum = 100;
for (int j = 0; j < 100; j++) {
    double pow = Math.Pow(j, j); //Calculation
    pBar.PerformStep();
}

以上代码运行正常,但是我希望能够重置进度条以便稍后使用。我原本以为只需要在 for 循环之后设置 pBar.Value = 1 就可以了,但是奇怪的是这样会破坏进度条的显示。进度条不像之前一样稳步增加,而是根本没有填满。如果我设置 pBar.Value = 50,进度条就会从中间开始,也不会动了。它的行为好像是 pBar.Value 在 for 循环之前执行,或者在每次循环迭代期间设置了 pBar.Value(我确保没有错误地将 pBar.Value 语句放在 for 循环内部)。
我能想到的唯一原因是,C# 正在异步运行我的 for 循环,值被立即设置,然后由于某种原因没有更新。我尝试寻找解决方法,但目前我找到的都是创建后台程序的方法(我对此不感兴趣),并且我重置进度条的方式应该可以工作...
为什么会出现这种情况?

1
如果您想重置进度条,可以使用按钮将进度条设置为1。我不理解它为什么与异步有关。 - Jack J Jun
@ChrisCatignani,我上面提供的代码是重现我遇到问题所需的所有代码。如果你将它粘贴到附加到按钮点击事件的函数中,按照代码注释设置进度条,并在末尾添加pBar.Value = 1,你应该能得到我描述的相同结果。 - ExecutionByFork
3个回答

3

好的,这是我回答你问题的尝试:

我认为当您将进度条的值重置为1后,它并没有“更新”。我认为整个进度条的动画都相当虚假。

我查看了ProgressBar 这里 的源代码。我们注意到ProgressBar.ValueProgressBar.PerformStep最终都调用UpdatePos,然后该函数向底层的本地控件发送PBM_SETPOS消息。然后MSDN告诉我们,每次接收到此消息时,进度条都应该重新绘制。

然而,我怀疑控件会将进度条的“真实”动画虚拟化。我的猜测是,PBM_SETPOS消息会立即考虑到位置值(我们不希望进度条在任何时候都不知道自己的值),但它们也必须发送到某个队列中进行动画处理。
动画可能需要相当长的时间,如果发生需要停止或修改动画的事件,例如向后移动,则动画将被取消... 假设您将当前值替换为交替值1和51,您期望得到什么样的动画效果?
我通过将您代码中的 pBar.PerformStep(); 替换为以下内容来模拟此过程:
pBar.Value = j % 2 * 50 + 1;

当然,不要有 pBar.Value = 1; 这样的尾随。

最终的动画不是在1和51之间令人讨厌的来回循环,而是从1到51的干净和平滑的过渡:)

我认为这证明了我的观点,即动画会自行选择它想要的值,因此当在100后立即快速设置为1时,动画引擎决定不渲染任何内容。

因此,如果您想避免这种效果,最简单的方法就是在使用进度条之前将其重置为1,而不是在之后重置(顺便说一下,最好不要假设进度条值为1时使用它)。


PS:最后需要注意的是,同样的现象可以被用作一个技巧,在动画完成之前强制使进度条显示高值 这里

在您的情况下,您可以:

private void GoSlowlyTo100() => pBar.Value = 100;
private void GoQuicklyTo100()
{
    pBar.Maximum = 101;
    pBar.Value = 101;
    pBar.Maximum = 100;
    pBar.Value = 100;
}

希望这回答了您的问题!


1
“value+1” 的技巧实际上相当出名!在这里:https://inneka.com/windows/progressbar-lag-when-setting-position-with-pbm_setpos-duplicate/ 和这里:https://dev59.com/kW435IYBdhLWcg3wlBGD#5332770 - odalet
嗯,非常有趣。我运行了您提供的代码,确实进度条的行为与您期望的完全不同。我怀疑后端处理存在某些乱序,并且我假设事情正在异步处理,但是您的解释更加合理。 - ExecutionByFork

2
在 for 循环后,pBar.Value = 1 会导致进度条不更新。可能控件没有被刷新/更新,从而导致绘制事件发生。用以下代码测试这个理论:
for (int j = 0; j < 100; j++) {
...
}

pBar.Value = 1
pBar.Invalidate()  //<-- cause a paint event

我在你提到的进度条后台工作者线程之一中讨论了这个问题。我知道你对这些不感兴趣,但是在单个线程中运行进度条会加剧问题。虽然在线程之外生成控件不好,但在后台线程中进行计算并Invoke主线程更新控件是一种好方法。
在单线程方案中,一个不太理想且相当笨拙的解决方案是在for循环期间Invalidate()进度条,从而导致Paint_Event重绘自己(每5次循环一次),例如:
VB
pBar.Value = j
If j Mod 5 = 0 Then pBar.Invalidate()

C#

pBar.Value = j;
if (j % 5 == 0) pBar.Invalidate();

“使用第二个线程可以使显示更加平滑,尽管单线程进度条也是完全有效的。将 pBar.Invalidate(); 放在循环的结尾应该能回答你的问题,我想用一个你能理解的例子来解释它发生的原因。”
请注意,有些人更喜欢使用Update

使控件在其客户区域内重绘无效的区域。

而不是Invalidate

使控件的整个表面无效并导致控件重新绘制。

我在故障排除整个控件表面的情况下使用后者。
参考:MSDN ProgressBar Class

最后的提示:不要费心去使用 pBar.PerformStep();,我有一种感觉它会导致问题。相反,在循环中设置值为1-100,并使用 hacky Invalidate 自己 - 只是为了测试这是否是根本原因,而不是问题的症状。 - Jeremy Thompson

1
使用计时器线程来监视ProgressBar的值是否达到最大值(100),如果是,则将其重置为0。您还需要使用这个小技巧来绕过Win7\10慢渲染问题slow rendering issue。我制作了一个VIDEO,可以帮助您更好地理解解决方案。
以下是基于您原始代码的完整代码。
        private void button1_Click(object sender, EventArgs e)
    {
        timer1.Enabled=true;

        pBar.Visible = true;
        pBar.Step = 1;
        pBar.Minimum = 0;

        pBar.Maximum = 100;
        for (int j = 1; j < 101; j++)
        {
            // double pow = Math.Pow(j, j); //Calculation
            pBar.Value = j;
            pBar.Value = j-1;
            pBar.PerformStep();
            Thread.Sleep(20);
        }



    }

    private void timer1_Tick(object sender, EventArgs e)
    {
        if (pBar.Value == 100)
        {
            pBar.Value = 0;
            timer1.Enabled = false; // stop timer process
        }
    }

谢谢你抽出时间为我制作那个视频,我找到了一种不需要计时器就能让它工作的方法。然而,这个答案仍然留下了一个问题,即为什么在for循环之后设置pBar.Value = 1会导致进度条不更新。你知道这可能是什么原因吗? - ExecutionByFork
多线程,Windows 使用不同的线程来控制 UX 和处理,这就是为什么你不能在 for 循环后放置 pBar.Value = 1,因为两个进程不是按顺序运行的。 - HO LI Pin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接