【问题标题】:How can I update progress bar without blocking UI? [duplicate]如何在不阻塞 UI 的情况下更新进度条? [复制]
【发布时间】:2021-10-10 04:31:37
【问题描述】:

目前我正在创建一个后台 STA 线程以保持 UI 响应,但它会减慢我在主线程上的函数调用速度。

基于此线程How to update progress bar while working in the UI thread 我尝试了以下操作,但 UI 仅在所有工作完成后才会更新。我尝试使用 Dispatcher 优先级,但它们似乎都不起作用。

我还尝试将 _frmPrg.Refresh() 添加到我的 Progress 回调中,但这似乎没有任何改变。

    Dim oProgress = New Progress(Of PrgObject)(Sub(runNumber)
                                                      _frmPrg.Invoke((Sub()
                                                                          _frmPrg.Status = runNumber
                                                                      End Sub))
                                                  End Sub)

    System.Windows.Threading.Dispatcher.CurrentDispatcher.BeginInvoke(Sub()
                                                                          DoLongRunningWork(oProgress, _cancellationToken)
                                                                      End Sub, System.Windows.Threading.DispatcherPriority.Background)

【问题讨论】:

  • 可能是您报告进度/调用过于频繁?
  • @Sinatr 我尝试将报告频率更改为大约每秒一次,但它仍然没有更新进度条。

标签: multithreading


【解决方案1】:

我无法真正帮助您解决问题,但我会尝试澄清您发布的代码中发生的情况。 当 UI 线程不忙时,DoLongRunningWork 将通过 UI 线程上的 Dispatcher 调用。但是一旦启动,它将阻塞 UI 线程,直到它完成。因此,您无法以这种方式显示进度。你唯一的机会是让DoLongRunningWork 在后台线程上运行。如果长时间运行的方法来自必须从 UI 线程访问的 office 对象,那不会给您带来任何好处...

Progress 类(参见备注部分)会自动在 UI 线程上调用您的事件处理程序,因此您的事件处理程序中不需要 _frmPrg.Invoke

也许您可以为您的进度表启动一个 STAthread 并从那里显示它。您的 Progress 类的实例也必须在此线程中创建,但不是在显示您的表单以确保该线程成为WindowsFormsSynchronisationContext 之前(或者您在启动线程后显式设置一个)。普通的SynchronisationContext 行不通! 至少您可以通过这种方式获得表单中的更新,但办公应用程序的 UI 线程仍将被阻止。当然,如果访问 office 对象,您对进度表单所做的任何操作都必须在 UI 线程上调用。

【讨论】:

  • 这是否意味着一旦我的 DoLongRunningWork 方法启动,我在 Progress 中的调用也不会在调度程序中执行?这可以解释为什么在我的进度回调中调用 _frmPrg.Refresh() 不起作用。
  • 啊,我看到在我发布的链接中,调度程序实际上被称为循环内部,所以你所说的非常有道理。也许其他人有另一个想法,否则我会接受你的回答。谢谢!
  • @chriscode 是的。您对 Progress 类的调用将被发布(不发送),因此在您的 DoLongRunningWork 完成后,它们会在 UI 线程上排队并执行。
  • 我还想尝试您在此答案中概述的方法,即在主线程上进行工作并在单独的线程上显示进度条。我为此创建了一个新问题,因为这个问题(错误地)被标记为重复。如果您有兴趣跟进此主题,请点击此处的链接。谢谢:stackoverflow.com/questions/69133412/…
  • @chriscode 你期待更好的性能(你不会得到它)还是只是好奇?
【解决方案2】:

在阅读了其他一些帖子后,我决定提出另一种解决方案。我之前的答案仍然包含有用的信息,所以我将它留在那里。我不熟悉 VB.NET 语法,因此示例使用 C#。我已经在 PowerPoint 的 VSTO 插件中测试了代码,但它应该可以在任何办公应用程序中运行。

忘记 Progress 类和后台线程。在 UI 线程上运行所有内容!

现在使用一些异步代码。为了留在 UI 线程上,我们需要一个“好的”SynchronizationContext。

private static void EnsureWinFormsSyncContext()
{
    // Ensure that we have a "good" SynchronisationContext
    // See https://stackoverflow.com/a/32866156/10318835
    if (SynchronizationContext.Current is not WindowsFormsSynchronizationContext)
        SynchronizationContext.SetSynchronizationContext(new WindowsFormsSynchronizationContext());
}

这是一个按钮的事件处理程序。注意手动添加的async 关键字。 SynchronizationContext.Current 被一次又一次地重置,所以请确保 EventHandler 中的正确:

private async void OnButtonClick(object sender, EventArgs e)
{
    EnsureWinFormsSyncContext();
    // Return from event handler, ensure that we are really async
    // See https://stackoverflow.com/a/22645114/10318835
    await Task.Yield();
    await RunLongOnUIThread();
}

这将是工作方法,也在 UI 线程上运行。

private async Task RunLongOnUIThread()
{
    //Dummy code, replace it with your code
    var pres = addIn.Application.Presentations.Add();
    for (int i = 0; i < 100; i++)
    {
        Debug.Print("Creating slide {0} on thread {1}", i, Thread.CurrentThread.ManagedThreadId);
        // If you have some workloads that can be run on a background 
        // thread, execute them with await Task.Run(...).
        try
        {
            var layout = pres.Designs[1].SlideMaster.CustomLayouts[1];
            var slide = pres.Slides.AddSlide(i + 1, layout);
            var shape = slide.Shapes.AddLabel(Microsoft.Office.Core.MsoTextOrientation.msoTextOrientationHorizontal, 0, 15 * i, 100, 15);
            shape.TextFrame.TextRange.Text = $"Text on slide {i + 1}";
        }
        catch (Exception ex)
        {
            Debug.Print("I don't know what am I doing here, I'm not familiar with PowerPoint... {0}", ex);
        }

        // Update UI
        statusLabel.Text = $"Slide {i + 1} done";
        progressBar1.Value = i + 1;

        // This is the magic! It gives the main thread the opportunity to update the UI.
        // It also processes input messages so you need to disable unwanted buttons etc.
        await IdleYield();
    }
}

以下方法适用于 Windows 窗体应用程序,它可以完美地完成工作。我也在 PowerPoint 中尝试过。如果您遇到问题,请尝试使用 await Dispatcher.Yield(DispatcherPriority.ApplicationIdle) 而不是 await IdleYield() 的 WPF 风格。

private static Task IdleYield()
{
    var idleTcs = new TaskCompletionSource<bool>();
    void handler(object s, EventArgs e)
    {
        Application.Idle -= handler;
        idleTcs.SetResult(true);
    }
    Application.Idle += handler;
    return idleTcs.Task;
}

这是我使用的答案的(可点击)链接(我不能将它们放在代码块中......)。

如果在您的真实代码中运行不符合预期,请检查您正在运行的线程和SynchronizationContext.Current

【讨论】:

  • 您不应该使用任何后台线程来访问对象模型,这是防止编组的想法。并行执行没有任何意义,所有调用都将被封送回 UI 线程。也许您可以显示更多代码以查看究竟是什么耗时。您的插件适用于哪个办公应用程序?什么版本?我今天出去了,这里是凌晨 3.40...
  • Async/await 并不一定意味着将启动一个新线程。这取决于你到底在做什么。在我的示例中,您不会在线程池线程上运行任何内容。我知道,这有点复杂,当我开始使用 async/await 时我也不明白 :)
  • @chriscode:这就是我所说的“好”SynchronizationContext 类。 WindowsFormsSynchonizationContextDispatcherSynchronizationContext 都使用 Windows 消息(可能是 SendMessagePostMessage API)进行同步。您可以阅读所有链接的帖子,我无法更好地解释它:)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-09-01
  • 1970-01-01
  • 1970-01-01
  • 2018-06-05
  • 1970-01-01
  • 2011-01-19
  • 2012-09-15
相关资源
最近更新 更多