C#如何取消进度条?

3
我正在尝试改进这个进度条教程的代码,并添加取消按钮,但目前还没有成功。
public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
    }

    private CancellationTokenSource cts;

    private void Calculate(int i)
    {
        Math.Pow(i, i);
    }

    public void DoWork(IProgress<int> progress, CancellationToken cancelToken)
    {
        for (int j = 0; j < 100000; j++)
        {
            if (cancelToken.IsCancellationRequested)
                cancelToken.ThrowIfCancellationRequested();
            Calculate(j);
            progress?.Report((1 + j) * 100 / 100000);
        }
    }

    private async void run_Click(object sender, EventArgs e)
    {
        cts = new CancellationTokenSource();
        var cancelToken = cts.Token;
        progressBar.Maximum = 100;
        progressBar.Step = 1;
        var progress = new Progress<int>(v => progressBar.Value = v);

        try
        {
            await Task.Run(() => { DoWork(progress, cancelToken); }, cts.Token);
        }
        catch (OperationCanceledException ex)
        {
            Console.WriteLine($"{nameof(OperationCanceledException)} thrown with message: {ex.Message}");
        }
        finally
        {
            cts = null;
        }
    }

    private void cancel_Click(object sender, EventArgs e)
    {
        Console.WriteLine("Cancel");
        cts?.Cancel();
    }
}
点击运行后,UI冻结,我无法点击取消按钮。我已经阅读了以下博客: 但找不到答案,我也尝试过以下变化:
    await Task.Factory.StartNew(() => { DoWork(progress, cancelToken); }, cts.Token);

这个操作不起作用,无法在加载期间点击取消按钮。有什么想法吗?(我确定这非常简单)。


1
看起来你在每次迭代时都报告进度,而且没有延迟。这样你的循环运行得太快了,无法让UI更新和响应。在你的DoWork中添加类似于await Task.Delay(TimeSpan)的内容。 - JSteward
2
请查看 Progress<> 的文档:Progress 类在创建时捕获同步上下文(这里是 UI 线程),然后在调用 Report 时切换到此同步上下文。因此,对于非常紧密的循环(比如这里),您不断地切换到 UI 线程,并且考虑到更新进度比您的 Compute 函数更加计算密集,您的 DoWork 方法大多数时间都在 UI 线程上运行并阻塞它。您应该只报告每1000个项目左右。 - ckuri
谢谢你们两位,我只是在 progress?.Report((1 + j) * 100 / 100000000); 前面添加了 if (j % 500 == 0),并将循环增加到了 100000000,现在它完美地工作了。代码实际上是正确的,正如你所说的,DoWork 没有做足够的工作。 - Hydraxize
1个回答

阿里云服务器只需要99元/年,新老用户同享,点击查看详情
3
在加载过程中无法点击取消按钮。有什么想法?(我确定这非常简单)。 正如其他人所指出的那样,您的DoWork需要在使用UI线程进行另一个更新之前完成更多的工作。因此,以下代码可能有效:
private void Calculate(int i)
{
  for (int j = 0; j != 10000; ++j)
    Math.Pow(i, i);
}

目前,UI线程被进度更新所淹没,因此它没有时间响应用户输入。

如果您的实际代码无法轻松分成较大的块,则可以使用速率节流 IProgress<T> 实现,例如我在更新我的书时编写的一个实现

public static class ObservableProgress
{
  public static (IObservable<T> Observable, IProgress<T> Progress) CreateForUi<T>(TimeSpan? sampleInterval = null)
  {
    var (observable, progress) = Create<T>();
    observable = observable.Sample(sampleInterval ?? TimeSpan.FromMilliseconds(100))
        .ObserveOn(SynchronizationContext.Current);
    return (observable, progress);
  }

  public static (IObservable<T> Observable, IProgress<T> Progress) Create<T>()
  {
    var progress = new EventProgress<T>();
    var observable = Observable.FromEvent<T>(handler => progress.OnReport += handler, handler => progress.OnReport -= handler);
    return (observable, progress);
  }

  private sealed class EventProgress<T> : IProgress<T>
  {
    public event Action<T> OnReport;
    void IProgress<T>.Report(T value) => OnReport?.Invoke(value);
  }
}

使用方法:

private async void run_Click(object sender, EventArgs e)
{
  cts = new CancellationTokenSource();
  var cancelToken = cts.Token;
  progressBar.Maximum = 100;
  progressBar.Step = 1;
  var (observable, progress) = ObservableProgress.CreateForUi<int>();

  try
  {
    using (observable.Subscribe(v => progressBar.Value = v))
      await Task.Run(() => { DoWork(progress, cancelToken); }, cts.Token);
  }
  catch (OperationCanceledException ex)
  {
    Console.WriteLine($"{nameof(OperationCanceledException)} thrown with message: {ex.Message}");
  }
  finally
  {
    cts = null;
  }
}

速率限制的IProgress<T>会丢弃额外的进度更新,每100毫秒向UI线程发送一个进度更新,这应该很容易处理并保持响应用户交互。


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