Как сделать пользовательский интерфейс отзывчивым при вызове внешнего exe в C #? - PullRequest
3 голосов
/ 28 июня 2019

Я пишу программу WPF, которая использует exe-файл для захвата данных с оборудования.Для завершения одного звонка требуется около 2 секунд.И я использовал этот exe несколько раз (> 500) подряд с разными аргументами.Я должен ждать завершения каждого процесса до следующего вызова. Потому что я не могу запустить несколько exe одновременно.Аппаратное обеспечение не поддерживает это.В то же время я показываю обновление пользовательского интерфейса, а также поддерживаю отзывчивость пользовательского интерфейса за отмену задачи в любое время.

Я не понимаю, что и как использовать async-await, 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 ]

1 голос
/ 29 июня 2019

У вас есть 3 вопроса здесь:

  1. Как мне ждать завершения процесса, не блокируя поток пользовательского интерфейса?
  2. Как предотвратить повторное нажатие кнопки до ее завершения?
  3. Как мне отменить?

Это лот для одного вопроса переполнения стека; в будущем, пожалуйста, задавайте только один вопрос за раз.

Как мне ждать завершения процесса, не блокируя поток пользовательского интерфейса?

Вы можете использовать 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 в фоновом потоке. Это будет использовать дополнительный ненужный поток, но для приложения с графическим интерфейсом это не критично.

В своем коде вы можете отправить 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 используется здесь только , потому что это обработчик событий . Обычно вам следует избегать 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;
  }
}

Как мне отменить?

Отмена в .NET следует стандартному шаблону . Отменяемый код соответствует 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();
}
0 голосов
/ 28 июня 2019

Создать новую нить на кнопке, чтобы основная нить была свободной

 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;
        }
0 голосов
/ 28 июня 2019

Далее используются методы 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
    }  
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...