如何使进度条更新速度足够快?

10
我正在使用一个进度条来展示用户的进程,它有17个步骤,并且根据天气(好吧,数据库)可能需要5秒到两三分钟的时间。
在XP下我没有遇到任何问题,进度条完美地运作。但是当我在Vista上测试时,发现情况不再如此。
例如:如果需要更接近5秒的时间,它可能只完成了1/3的进度就消失了。尽管它的进度是17/17,但是它并没有显示出来。我认为这是因为Vista在进度条上加上了动画效果,而动画无法足够快地完成。
有人知道我该如何解决这个问题吗?
这里是代码:
这是更新进度条的部分,waiting是包含进度条的窗体。
        int progress = 1;
        //1 Cash Receipt Items
        waiting.setProgress(progress, 18, progress, "Cash Receipt Items");
        tblCashReceiptsApplyToTableAdapter1.Fill(rentalEaseDataSet1.tblCashReceiptsApplyTo);
        progress++;
        //2 Cash Receipts
        waiting.setProgress(progress, "Cash Receipts");
        tblCashReceiptsTableAdapter1.Fill(rentalEaseDataSet1.tblCashReceipts);
        progress++;
        //3 Checkbook Codes
        waiting.setProgress(progress, "Checkbook Codes");
        tblCheckbookCodeTableAdapter1.Fill(rentalEaseDataSet1.tblCheckbookCode);
        progress++;
        //4 Checkbook Entries
        waiting.setProgress(progress, "Checkbook Entries");
        tblCheckbookEntryTableAdapter1.Fill(rentalEaseDataSet1.tblCheckbookEntry);
        progress++;
        //5 Checkbooks
        waiting.setProgress(progress, "Checkbooks");
        tblCheckbookTableAdapter1.Fill(rentalEaseDataSet1.tblCheckbook);
        progress++;
        //6 Companies
        waiting.setProgress(progress, "Companies");
        tblCompanyTableAdapter1.Fill(rentalEaseDataSet1.tblCompany);
        progress++;
        //7 Expenses
        waiting.setProgress(progress, "Expenses");
        tblExpenseTableAdapter1.Fill(rentalEaseDataSet1.tblExpense);
        progress++;
        //8 Incomes
        waiting.setProgress(progress, "Incomes");
        tblIncomeTableAdapter1.Fill(rentalEaseDataSet1.tblIncome);
        progress++;
        //9 Properties
        waiting.setProgress(progress, "Properties");
        tblPropertyTableAdapter1.Fill(rentalEaseDataSet1.tblProperty);
        progress++;
        //10 Rental Units
        waiting.setProgress(progress, "Rental Units");
        tblRentalUnitTableAdapter1.Fill(rentalEaseDataSet1.tblRentalUnit);
        progress++;
        //11 Tenant Status Values
        waiting.setProgress(progress, "Tenant Status Values");
        tblTenantStatusTableAdapter1.Fill(rentalEaseDataSet1.tblTenantStatus);
        progress++;
        //12 Tenants
        waiting.setProgress(progress, "Tenants");
        tblTenantTableAdapter1.Fill(rentalEaseDataSet1.tblTenant);
        progress++;
        //13 Tenant Transaction Codes
        waiting.setProgress(progress, "Tenant Transaction Codes");
        tblTenantTransCodeTableAdapter1.Fill(rentalEaseDataSet1.tblTenantTransCode);
        progress++;
        //14 Transactions
        waiting.setProgress(progress, "Transactions");
        tblTransactionTableAdapter1.Fill(rentalEaseDataSet1.tblTransaction);
        progress++;
        //15 Vendors
        waiting.setProgress(progress, "Vendors");
        tblVendorTableAdapter1.Fill(rentalEaseDataSet1.tblVendor);
        progress++;
        //16 Work Order Categories
        waiting.setProgress(progress, "Work Order Categories");
        tblWorkOrderCategoryTableAdapter1.Fill(rentalEaseDataSet1.tblWorkOrderCategory);
        progress++;
        //17 Work Orders
        waiting.setProgress(progress, "Work Orders");
        tblWorkOrderTableAdapter1.Fill(rentalEaseDataSet1.tblWorkOrder);
        progress++;
        //18 Stored procs
        waiting.setProgress(progress, "Stored Procedures");
        getAllCheckbookBalancesTableAdapter1.Fill(rentalEaseDataSet1.GetAllCheckbookBalances);
        getAllTenantBalancesTableAdapter1.Fill(rentalEaseDataSet1.GetAllTenantBalances);
        //getCheckbookBalanceTableAdapter1;
        //getTenantBalanceTableAdapter1;
        getTenantStatusID_CurrentTableAdapter1.Fill(rentalEaseDataSet1.GetTenantStatusID_Current);
        getTenantStatusID_FutureTableAdapter1.Fill(rentalEaseDataSet1.GetTenantStatusID_Future);
        getTenantStatusID_PastTableAdapter1.Fill(rentalEaseDataSet1.GetTenantStatusID_Past);
        selectVacantRentalUnitsByIDTableAdapter1.Fill(rentalEaseDataSet1.SelectVacantRentalUnitsByID);
        getRentBasedBalancesTableAdapter1.Fill(rentalEaseDataSet1.GetRentBasedBalances);
        getAgingBalanceTableAdapter2.Fill(rentalEaseDataSet1.GetAgingBalance);


        waiting.Close();

以下是等待表单:

public partial class PleaseWaitDialog : Form {
    public PleaseWaitDialog() {
        CheckForIllegalCrossThreadCalls = false;
        InitializeComponent();
    }

    public void setProgress(int current, int max, int min, string loadItem) {
        Debug.Assert(min <= max, "Minimum is bigger than the maximum!");
        Debug.Assert(current >= min, "The current progress is less than the minimum progress!");
        Debug.Assert(current <= max, "The progress is greater than the maximum progress!");

        prgLoad.Minimum = min;
        prgLoad.Maximum = max;
        prgLoad.Value = current;
        lblLoadItem.Text = loadItem;
    }

    public void setProgress(int current, string loadItem) {
        this.setProgress(current, prgLoad.Maximum, prgLoad.Minimum, loadItem);
    }
}

1
CheckForIllegalCrossThreadCalls = false <- 或许你应该采取正确的措施来防止该错误(调用):) - VVS
那么,编译器不会警告您关于跨线程调用的问题吗? - VVS
不,我已经移除了那部分代码并修正了它,以确保没有跨线程调用。 - Malfist
如果你想要一个快速而(真的)不太好的解决方案,你可以使用一个禁用的“TrackBar”来指示你的进度:p - Nyerguds
我制作了一个短视频,展示如何快速解决这个问题以及如何修复它。你也可以查看这个相关问题,或许会有所帮助。 - HO LI Pin
9个回答

41

Vista引入了一个动画效果,当更新进度条时,它尝试平滑地从先前的位置滚动到新设置的位置,这会在控件更新时产生不好的时间延迟。当您大幅度跳过进度条时,例如从25%跳到50%,延迟最为明显。

正如另一位帖子指出的那样,您可以禁用Vista主题的进度条,它将模仿XP进度条的行为。

我发现另一个解决方法:如果您将进度条设置向后,则会立即绘制到该位置。因此,如果您想从25%跳到50%,则可以使用以下(虽然有点破解式的)逻辑:

progressbar.Value = 50;
progressbar.Value = 49;
progressbar.Value = 50;

我知道,我知道 - 这是一个愚蠢的黑客技巧 - 但它确实有效!


是的,这个可行。谢谢! 这在我的代码中很难看,希望微软能够解决这个问题。 - Han
哇,我大概花了2个小时真正地研究线程,并确保为UI重绘留出足够的休眠时间,但是什么都没起作用。后来我加入了这个小技巧,现在进度条更新得很干净。这是一个非常不规范的技巧,但它简单有效。 - drharris
谢谢!!! 这是我见过的最好(虽然有些糟糕)的hack,用于修复这个愚蠢的“功能”。 - Josh Stribling
标记,无论是否hackish,这都是一个解决方案!在此之前,我有一个复杂的方程式,它会让我的工作线程睡眠x毫秒,具体取决于跳跃的大小(从一个值到另一个值)。谁需要花哨的平滑更新进度条呢...应该有一种可怕的方法来强制赋值,比如.ForceValue(50)! ;) - Arvo Bowen
哇,它能用。有时我们不得不实现一些hack方法。我接受这个事实。 - Slippery Pete
1
2017年,Windows 10也存在进度条问题。我刚刚使用了这个技巧,它运行良好 :) - Dean2690

11
这整个混乱的原因是Vista和W7引入的插值动画效果。它绝对与线程阻塞问题无关。调用setProgress()或直接设置Value属性会触发动画效果,我将解释如何欺骗这种效果:
我提出了一种方法,即根据固定值设置最大值。最大值属性不会触发效果,因此您可以自由地移动进度并立即响应。
请记住,实际显示的进度由ProgressBar.Value / ProgressBar.Maximum给出。考虑到这一点,以下示例将使进度从0到100移动,用i表示:
ProgressBar works like this:  
progress = value / maximum

therefore:
maximum = value / progress

我添加了一些必要的缩放因子,应该是不难理解的:

progressBar1.Maximum *= 100;
progressBar1.Value = progressBar1.Maximum / 100;
for (int i = 1; i < 100; i++)
{
    progressBar1.Maximum = (int)((double)progressBar1.Value / (double)(i + 1) * 100);
    Thread.Sleep(20);
}

我知道... 但不幸的是,我认为对于人们来说,一开始它可能看起来太复杂了,以至于他们不考虑尝试它... - Silas Hansen
3
噢噢噢...我来自未来...这是最好的解决方案。我先试过整个进度条.Value--;进度条.Value++;的方法。如果你尝试一下这个,它会更好。 Translated: Boooh... I'm from the future... This is the best solution. I first tried the whole progressbar.Value--; progressbar.Value++; thing. But this works so much better if you give this a shot. - Gerben Rampaart
那个范围是从1到100,不包括0。这个因子100是随机的吗,还是总是相同的最大值? - Nyerguds
是的。100可以更改为您想要将范围分成的任何因子。尝试并查看是否适用于0到100。我认为应该可以,因为我除以(i + 1),这应该可以防止出现除以零异常。 - Silas Hansen
那个thread.sleep(20)仍然意味着它很慢,就好像只有在达到完整值时才显示自己。所以如果将它放在表单的加载中,仍然会有延迟,然后表单会以它的值出现。 - barlop
另外,你是在建议这个吗?也许这对于人们来说会更容易使用,不需要理解它。因为我并不完全理解它,但我可以将其封装成一个方法并使用它。http://pastebin.com/raw/GyKQi85e - barlop

3
听起来你正在UI线程上执行所有操作,因此没有释放消息队列。你尝试过使用类似BackgroundWorkerProgressChanged事件的东西吗?请参见MSDN中的示例。 BackgroundWorker非常适合加载外部数据 - 但请注意,在返回UI线程之前不要进行任何数据绑定等操作(或者只需使用Invoke/BeginInvoke将工作推送到UI线程)。

我原本以为使用MethodInvoke会创建一个新的线程。 - Malfist
Invoke会创建一个新的线程,但同时也会阻塞当前线程直到工作完成。 - Ana Betts
1
@Paul Betts,MethodInvoker 不会阻塞。这就是它的目的。如果它是阻塞的,我为什么不直接调用函数呢? - Malfist
我指的是Control.Invoke(即this.Invoke),而不是Delegate.Invoke;Control.Invoke将委托发送到UI线程 - 在执行后台处理的同时定期更新控件非常有用。 - Marc Gravell
为了完整性,Delegate.Invoke 确实会阻塞(Control.Invoke也是如此)。在这两种情况下都有一个非阻塞的BeginInvoke。 - Marc Gravell
我已经实现了这个,但是没有任何改变。 - Malfist

2
尝试调用waiting.setProgess()方法,因为waiting似乎存在于另一个线程中,这将是典型的跨线程调用(如果你让编译器警告你,则会有提示)。
由于Control.Invoke使用起来有点笨拙,所以我通常使用扩展方法,允许我传递lambda表达式。
waiting.ThreadSafeInvoke(() => waiting.setProgress(...));

.

// also see http://stackoverflow.com/questions/788828/invoke-from-different-thread
public static class ControlExtension
{
    public static void ThreadSafeInvoke(this Control control, MethodInvoker method)
    {
        if (control != null)
        {
            if (control.InvokeRequired)
            {
                control.Invoke(method);
            }
            else
            {
                method.Invoke();
            }
        }
    }
}

这没有任何区别,但是感谢你向我展示了这个模式。 - Malfist
2
这不应该被标记为答案,因为它不会改变Vista+上的缓慢ProgressBar动画。 - deegee

1

我将使用Mark Lansdown出色的答案作为进度条控件的扩展方法。

public static void ValueFast(this ProgressBar progressBar, int value)
{
    progressBar.Value = value;

    if (value > 0)    // prevent ArgumentException error on value = 0
    {
        progressBar.Value = value - 1;
        progressBar.Value = value;
    }

}

或者您也可以采用这种方式,只需设置ProgressBar的值属性两次,而不是三次:

public static void ValueFast(this ProgressBar progressBar, int value)
{
    if (value < 100)    // prevent ArgumentException error on value = 100
    {
        progressBar.Value = value + 1;    // set the value +1
    }

    progressBar.Value = value;    // set the actual value

}

只需使用扩展方法在任何进度条控件上调用它:

this.progressBar.ValueFast(50);

如果你真的想要的话,也可以检查当前的Windows环境,并且只对Windows Vista+执行代码中的黑客部分,因为Windows XP的ProgressBar没有缓慢的进度动画。

如果进度条在处理不同长度的项目列表的不同场合中使用,通常最好将其最大值适应于列表的长度,这意味着您应该将您的 '100' 更改为 progressBar.Maximum - Nyerguds
如果你在结尾处遇到烦人的卡顿,你可以增加最大值,将值设置为最大值,然后减少值-1,并将最大值设置为该值。这似乎可以解决问题。 - user1274820

1
扩展Silas Hansen的答案,这个似乎每次都能给我完美的结果。
protected void UpdateProgressBar(ProgressBar prb, Int64 value, Int64 max)
{
    if (max < 1)
        max = 1;
    if (value > max)
        value = max;
    Int32 finalmax = 1;
    Int32 finalvalue = 0;
    if (value > 0)
    {
        if (max > 0x8000)
        {
            // to avoid overflow when max*max exceeds Int32.MaxValue.
            // 0x8000 is a safe value a bit below the actual square root of Int32.MaxValue
            Int64 progressDivideValue = 1;
            while ((max / progressDivideValue) > 0x8000)
                progressDivideValue *= 0x10;
            finalmax = (Int32)(max / progressDivideValue);
            finalvalue = (Int32)(value / progressDivideValue);
        }
        else
        {
            // Upscale values to increase precision, since this is all integer division
            // Again, this can never exceed 0x8000.
            Int64 progressMultiplyValue = 1;
            while ((max * progressMultiplyValue) < 0x800)
                progressMultiplyValue *= 0x10;
            finalmax = (Int32)(max * progressMultiplyValue);
            finalvalue = (Int32)(value * progressMultiplyValue);
        }
    }
    if (finalvalue <= 0)
    {
        prb.Maximum = (Int32)Math.Min(Int32.MaxValue, max);
        prb.Value = 0;
    }
    else
    {
        // hacky mess, but it works...
        // Will pretty much empty the bar for a split second, but this is normally never visible.
        prb.Maximum = finalmax * finalmax;
        // Makes sure the value will DEcrease in the last operation, to ensure the animation is skipped.
        prb.Value = Math.Min(prb.Maximum, (finalmax + 1));
        // Sets the final values.
        prb.Maximum = (finalmax * finalmax) / finalvalue;
        prb.Value = finalmax;
    }
}

我已经扩展了这个内容,编写了一个完整的类来处理进度条更新:http://nyerguds.arsaneus-design.com/project_stuff/nyertools/ProgressBarUpdater.cs。这个类使用委托来更新UI(正如应该做的那样),并且去掉了Thread.Sleep。 - Nyerguds

0

0

首先,我永远不会关闭CheckForIllegalCrossThreadCalls选项。

其次,在更新进度后添加Refresh()。仅仅因为你在不同的线程中进行工作,并不意味着你的GUI线程会及时更新。


我非常确定,如果GUI线程没有被阻塞,它正在更新。 - VVS

0
我有同样的问题。 我有一个带有多个进度条的表单(顶部进度条例如为文件x/n,底部进度条则是任务y/m) 顶部进度条没有及时更新,而底部进度条则正常更新。 编程上我尝试了update、invalidate、显式处理消息、刷新或者睡眠等方法都不能解决。有趣的是底部进度条和其他组件(例如已用时间)可以正常更新。 这完全是Vista+主题的问题(如之前所建议的动画效果,XP或Vista经典主题均可正常工作)。 当在顶部进度条已达到100(程序上,非视觉上)后显示一个消息框,我先看到消息框,然后再看到进度完成。

我发现像Disabling progress bar animation on Vista Aero中所解释的那样使用SetWindowTheme(ProgressBar.Handle, ' ', ' ');可以解决问题(但这会使我的进度条变成老式样式)。


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