从另一个线程更新 UI 只工作一次

Update UI from another thread work only one time

我知道在 Whosebug 上有很多这方面的信息,但没有找到任何解决我问题的信息。

我制作了一个程序来使用 ffmpeg 处理一些视频文件。这个过程可能需要几分钟,所以,我正在尝试在另一个表单上制作进度条。

基本上,当我单击主窗体 (FormSync) 上的按钮时,会显示一个新窗体。这个表单只有一个进度条和一个取消按钮(让我们调用 FormProgress)。

为了执行 ffmpeg,我使用另一个 class (VideoProcessing) 创建一个新进程,执行 ffmpeg,并监视 stderror(ffmpeg show progress on stderror)。每次 ffmpeg 显示进度时,此 class 解析输出、计算进度并引发事件 (OnMergeProgress)。

基本上,这是代码:

FormSync:

public partial class FormSync : Form 
{
  // this form show the progress of job
  private FormProgress _formProgress;

  // start the ffmpeg when click on button
  private void mergeButton_click(object sender, EventArgs e)
  {
    var files = new List<string>() {"file1.mp4", "file2.mp4"};
    MergeFiles(files);
  }

  // join all video files on a single file (using ffmpeg)
  private void MergeFiles(IEnumerable<string> videoFiles)
  {
    // instantiate the class that execute ffmpeg
    VideoProcessing videoProcessing = new VideoProcessing();

    // this class has a a event to show current progress
    // seconds = total seconds of video (sum length of all video files)
    // currentSeconds = current progress
    videoProcessing.OnMergeProgress += (seconds, currentSeconds) =>
    {
      Invoke((MethodInvoker) delegate()
      {
        // Instantiate the form of progress if not visible
        if (_formProgress = null)
        {
          // define the minimum and maximum value of progressbar on constructor
          _formProgress = new FormProgress(0, seconds);
          _formProgress.ShowDialog(this);
        }

        // update the progress bar value
        _formProgress.SetProgress(currentSeconds);
      }
    }
  }
}

表格进度:

public partial class FormProgress : Form
{
  public FormProgress(int min, int max)
  {
    InitializeComponent();
    progressBar.Minimum = min;
    progressBar.Maximum = max;
  }

  public void SetProgress(int value)
  {
    value = (value <= progressBar.Minimum)
        ? progressBar.Minimum
        : (value >= progressBar.Maximum) ? progressBar.Maximum : value;

    progressBar.Value = value;
    Refresh();
  }
}

视频处理:

internal class VideoProcessing
{
  // Events
  public delegate void MergeProgressHandler(int totalSeconds, int currentSeconds);
  public event MergeProgressHandler OnMergeProgress;

  private int _totalTimeVideos;

  public void MergeFiles(string[] videoFiles)
  {
    // calculate total time of all videos
    _totalTimeVideos = SomeFunctionToCalculateTotalTime();

    // create the Process object to execute FFMPEG (with stdout and stderr redirection)
    _process = CreateFFMPEGProcess(videoFiles);
  }

  // capture the stdout and stderr of ffmpeg
  private void MergeOutputHandler(object sendingProcess, DataReceivedEventArgs outline)
  {
    // capture the current progress
    // here will go a regex, and some other code to parse the info from ffmpeg

    // Raise the event
    OnMergeProgress?.Invoke(_totalTimeVideos, progressSeconds);
  }
}

基本上,FFMPEG 执行和捕获过程使用以下代码:

当我尝试执行代码时出现问题。

当我点击 que 按钮时,显示 FormProgress,但在此之后,显示进度条 "freeze"。程序运行良好,没有挂起,但进度条没有更新。

如果,在FormSync,在InvokeMethod,我用下面的内容替换原来的代码,我可以看到ffmpeg在工作,我的事件也在工作:

videoProcessing.OnMergeProgress += (seconds, currentSeconds) =>
{
  Debug.WriteLine($"{currentSeconds}/{seconds}");
}

所以,问题不是 ffmpeg 或我的视频 class,而是 UI.

更新的问题

如果我再次更改 Invoke,但这次使用 Debug,如下面的代码,Debug 仅打印第一个更新,仅此而已:

videoProcessing.OnMergeProgress += (seconds, currentSeconds) =>
{
  Invoke((MethodInvoker) delegate() {
    if (_formProgress == null) {
      _formProgress = new FormProgress(Resources.merging_video_files, 0, seconds);
      _formProgress.ShowDialog(this);
    }

    _formProgress.SetProgress(currentSeconds);
  });
  Debug.WriteLine($"{currentSeconds}/{seconds}");
}
  _formProgress.ShowDialog(this);

错误位于此处。在 window 关闭之前,ShowDialog() 不会 return。不清楚何时发生,但与错误无关。由于它不 return,Invoke() 调用会死锁且无法完成。这反过来会导致工作线程挂起。

部分问题是代码使用了 Invoke() 而不是 Begininvoke(),如果使用后一种方法,您就不会注意到相同的错误。并不是说这很漂亮,但它会隐藏问题。请注意,您不需要 Invoke(),不需要 return 值并且 BeginInvoke() 工作得很好。

您遇到此错误的最终原因是您需要初始化 ProgressBar.Maximum 属性。只是不要这样做,100 是一个很好的最大值。只需要一点数学知识,现在的进度是 (100 * currentSeconds) / 秒。

还需要调用ShowDialog(),有点尴尬,可以用Load事件调用MergeFiles()。