如何在C#中调用外部exe时使UI响应?

时间:2019-06-28 13:44:20

标签: c# wpf

我正在编写一个WPF程序,该程序使用exe文件从硬件捕获数据。完成一个通话大约需要2秒钟。我已经多次使用此exe(> 500)并使用了不同的参数。我必须等待每个进程完成之后才能进行下一次调用。因为我不能同时运行多个exe。硬件不支持它。同时,我已经显示了UI的更新,并且使UI随时响应以随时取消任务。

对于如何使用异步等待,Dispatcher.BeginInvoke或Task.run来解决我的问题,我感到困惑。任何帮助或想法将不胜感激。

    ObservableCollection < String > fileNames = new ObservableCollection < string > ();
    //fileNames is used to show the file names a ListBox in UI. It have to be
    // updated in real time.

    private void BtnStartCapture_Click(object sender, RoutedEventArgs e) {
        for (int i = 1; i <= CaptureSettings.NoOfFrames; i++) {
            String currentFile;
            if (CaptureSettings.CkBoard1 == true) {

                currentFile = CaptureSettings.CaptureFrame(1, i);
                fileNames.Add(currentFile);
            }

            if (CaptureSettings.CkBoard2 == true) {
                currentFile = CaptureSettings.CaptureFrame(2, i);
                fileNames.Add(currentFile);
            }

        }

    }

    internal String CaptureFrame(int boardId, int frameNo) {

        string arg = createCommandLIneArgument(boardId, frameNo);
        try {
            ProcessStartInfo pInfo1 = new ProcessStartInfo {
                FileName = "GrabberTest1.exe",
                Arguments = arg,
                WindowStyle = ProcessWindowStyle.Hidden
            };

            var process1 = Process.Start(pInfo1);
            process1.WaitForExit();

            return Path.GetFileNameWithoutExtension(arg);
        } catch(Exception) {
            return "Failed " + Path.GetFileNameWithoutExtension(arg);
        }
    }

private void BtnCancelCapture_Click(object sender, RoutedEventArgs e) {
//to do
}

3 个答案:

答案 0 :(得分:1)

您在这里有3个问题:

  1. 如何等待进程退出而不阻塞UI线程?
  2. 如何防止在完成按钮之前再次单击它?
  3. 如何取消?

对于单个堆栈溢出问题,这是一个 很多 ;将来,请一次只问一个问题。

  

如何等待进程退出而不阻塞UI线程?

您可以使用TaskCompletionSource<T>这样钩住Exited

public static Task<int> WaitForExitedAsync(this Process process)
{
  var tcs = new TaskCompletionSource<int>();
  EventHandler handler = null;
  handler = (_, __) =>
  {
    process.Exited -= handler;
    tcs.TrySetResult(process.ExitCode);
  };
  process.Exited += handler;
  return tcs.Task;
}

但是,此代码有一些警告:

  1. 您必须在过程开始之前将Process.EnableRaisingEvents设置为true
  2. 您必须在过程开始之前致电WaitForExitedAsync
  3. 引发Exited时,不是并不表示stdout / stderr流已完成。刷新这些流的唯一方法是调用WaitForExit(在过程退出之后)。不太直观。

为简单起见,您可能只想在后台线程上调用WaitForExit。那会使用额外的不必要线程,但是对于GUI应用程序而言,这并不重要。

在您的代码中,您可以将CaptureFrame推送到后台线程:

private async void BtnStartCapture_Click(object sender, RoutedEventArgs e)
{
  for (int i = 1; i <= CaptureSettings.NoOfFrames; i++)
  {
    String currentFile;
    if (CaptureSettings.CkBoard1 == true)
    {
      currentFile = await Task.Run(() => CaptureSettings.CaptureFrame(1, i));
      fileNames.Add(currentFile);
    }

    if (CaptureSettings.CkBoard2 == true)
    {
      currentFile = await Task.Run(() => CaptureSettings.CaptureFrame(2, i));
      fileNames.Add(currentFile);
    }
  }
}

请注意,async void is used here only because this is an event handler。通常,您应该避免使用async void

  

如何防止在完成按钮之前再次单击它?

一种常见的模式是在按钮运行时禁用它,例如:

private async void BtnStartCapture_Click(object sender, RoutedEventArgs e)
{
  BtnStartCapture.Enabled = false;
  try
  {
    for (int i = 1; i <= CaptureSettings.NoOfFrames; i++)
    {
      String currentFile;
      if (CaptureSettings.CkBoard1 == true)
      {
        currentFile = await Task.Run(() => CaptureSettings.CaptureFrame(1, i));
        fileNames.Add(currentFile);
      }

      if (CaptureSettings.CkBoard2 == true)
      {
        currentFile = await Task.Run(() => CaptureSettings.CaptureFrame(2, i));
        fileNames.Add(currentFile);
      }
    }
  }
  finally
  {
    BtnStartCapture.Enabled = true;
  }
}
  

如何取消?

Cancellation in .NET follows a standard pattern。被取消的代码遵循CancellationToken,可以从CancellationTokenSource进行设置。每个CancellationTokenSource是取消操作的一种方法,但是只能使用一次。因此,在这种情况下,每次操作开始都需要一个新的CancellationTokenSource

可以将取消请求解释为对外部流程的取消请求,但是对于您而言,我认为最好将取消请求解释为“让当前外部流程完成;仅不要捕获下一帧”。我认为这样比较好,因为外部过程与硬件设备通信(我们不想进入意外状态),并且速度相当快。

private CancellationTokenSource _cts;

private async void BtnStartCapture_Click(object sender, RoutedEventArgs e)
{
  _cts = new CancellationTokenSource();
  var token = _cts.Token;
  BtnStartCapture.Enabled = false;
  BtnCancelCapture.Enabled = true;
  try
  {
    for (int i = 1; i <= CaptureSettings.NoOfFrames; i++)
    {
      token.ThrowIfCancellationRequested();
      String currentFile;
      if (CaptureSettings.CkBoard1 == true)
      {
        currentFile = await Task.Run(() => CaptureSettings.CaptureFrame(1, i));
        fileNames.Add(currentFile);
      }

      if (CaptureSettings.CkBoard2 == true)
      {
        currentFile = await Task.Run(() => CaptureSettings.CaptureFrame(2, i));
        fileNames.Add(currentFile);
      }
    }
  }
  catch (OperationCanceledException)
  {
    // TODO: decide what to do here - clear fileNames? Display a message? Nothing?
  }
  finally
  {
    BtnStartCapture.Enabled = true;
    BtnCancelCapture.Enabled = false;
  }
}

private void BtnCancelCapture_Click(object sender, RoutedEventArgs e)
{
  _cts.Cancel();
}

答案 1 :(得分:0)

以下内容使用Invoke和WaitForExit方法来获得所需的结果。

编辑我编辑了以下代码,因此它将支持对启动该过程的线程的“背对背”调用。

    static readonly object _object = new object();
    public int iProcCount = 0;
    public void StartExe()
    {
        System.Diagnostics.Process proc;
        lock (_object) // Because WaitForExit is inside the lock all other 
                       // instances of this thread must wait before the 
                       // previous thread has finished
        {
            proc = System.Diagnostics.Process.Start(strExePath);
            proc.WaitForExit(); // This is not on the UI thread so it will not block the UI thread
        }
        this.Invoke(new Action(() => UpdateGUI(this, "Finished Proc " + iProcCount)));
        iProcCount++;
    }

    public void UpdateGUI(Form theGui, string msg)
    { // This will wil update the GUI when the GUI thread is not busy
        lblGUIUpdate.Text = msg;
    }

    private void Button_Click(object sender, EventArgs e)
    {
        Thread th = new Thread(StartExe);
        th.Start(); // After this, the GUI thread will not block
    }  

答案 2 :(得分:-1)

在按钮上创建一个新线程,以便主线程空闲

 private void Button_Click()
 {
    new Thread(() => ProxyMethod()).Start();
 }

对于每个外部方法,调用创建一个新线程,我将其限制为150个以内,您可以在系统的资源上对其进行更改

  void ProxyMethod()
        {
            try
            {
                for (int i = 1; i <= 1000; i++)
                {

                    Thread th = new Thread(() =>
                    {
                        try
                        {//New thread for every iteration
                            var result = CaptureFrame(1, 1);

                            if (result == true)
                            {
                                //File.add

                            }
                        }
                        catch
                        {

                        }
                        RunningThreads.Remove(Thread.CurrentThread);
                    });

                    RunningThreads.Add(th);
                    th.Start();

                    while(RunningThreads.Count>150)
                    {
                        Thread.Sleep(100);
                    }
                }
            }
            catch
            {

            }
        }


internal bool CaptureFrame(int boardId, int frameNo)
        {
            Thread.Sleep(5000);
            return true;
        }