Обновление пользовательского интерфейса с несколькими одновременными операциями - PullRequest
2 голосов
/ 29 августа 2009

Я разрабатываю приложение на C # с использованием National Instruments Daqmx для выполнения измерений на определенном оборудовании.

Моя установка состоит из нескольких детекторов, с которых мне приходится получать данные в течение заданного периода времени, все время обновляя мой интерфейс с этими данными.

 public class APD : IDevice
 {
    // Some members and properties go here, removed for clarity.

    public event EventHandler ErrorOccurred;
    public event EventHandler NewCountsAvailable;

    // Constructor
    public APD(
        string __sBoardID,
        string __sPulseGenCtr,
        string __sPulseGenTimeBase,
        string __sPulseGenTrigger,
        string __sAPDTTLCounter,
        string __sAPDInputLine)
    {
       // Removed for clarity.
    }

    private void APDReadCallback(IAsyncResult __iaresResult)
    {
        try
        {
            if (this.m_daqtskRunningTask == __iaresResult.AsyncState)
            {
                // Get back the values read.
                UInt32[] _ui32Values = this.m_rdrCountReader.EndReadMultiSampleUInt32(__iaresResult);

                // Do some processing here!

                if (NewCountsAvailable != null)
                {
                    NewCountsAvailable(this, new EventArgs());
                }

                // Read again only if we did not yet read all pixels.
                if (this.m_dTotalCountsRead != this.m_iPixelsToRead)
                {
                    this.m_rdrCountReader.BeginReadMultiSampleUInt32(-1, this.m_acllbckCallback, this.m_daqtskAPDCount);
                }
                else
                {
                    // Removed for clarity.
                }
            }
        }
        catch (DaqException exception)
        {
            // Removed for clarity.
        }
    }


    private void SetupAPDCountAndTiming(double __dBinTimeMilisec, int __iSteps)
    {
        // Do some things to prepare hardware.
    }

    public void StartAPDAcquisition(double __dBinTimeMilisec, int __iSteps)
    {
        this.m_bIsDone = false;

        // Prepare all necessary tasks.
        this.SetupAPDCountAndTiming(__dBinTimeMilisec, __iSteps);

        // Removed for clarity.

        // Begin reading asynchronously on the task. We always read all available counts.
        this.m_rdrCountReader.BeginReadMultiSampleUInt32(-1, this.m_acllbckCallback, this.m_daqtskAPDCount); 
    }

    public void Stop()
    {
       // Removed for clarity. 
    }
}

Объект, представляющий детектор, в основном вызывает операцию BeginXXX с обратным вызовом, который содержит EndXXX en, и также вызывает событие, указывающее доступные данные.

У меня до 4 таких объектов детекторов в качестве членов моей формы пользовательского интерфейса. Я вызываю метод Start () для всех из них последовательно, чтобы начать измерение. Это работает, и событие NewCountsAvailable запускается для всех четырех из них.

Из-за характера моей реализации метод BeginXXX вызывается в потоке пользовательского интерфейса, а Callback и Event также находятся в этом потоке пользовательского интерфейса. Поэтому я не могу использовать какой-то цикл while внутри моего потока пользовательского интерфейса, чтобы постоянно обновлять мой пользовательский интерфейс новыми данными, потому что события постоянно запускаются (я пробовал это). Я также не хочу использовать какой-либо метод UpdateUI () в каждом из четырех обработчиков событий NewCountsAvailable, поскольку это слишком сильно загрузит мою систему.

Поскольку я новичок в многопоточном программировании на C #, я застрял;

1) Каков «правильный» способ справиться с такой ситуацией? 2) Моя реализация объекта детектора звучит? Должен ли я вызывать методы Start () для этих четырех объектов детектора из другого потока? 3) Могу ли я использовать таймер для обновления моего пользовательского интерфейса каждые несколько сотен миллисекунд, независимо от того, что делают 4 объекта детектора?

Я действительно понятия не имею!

Ответы [ 5 ]

4 голосов
/ 30 августа 2009

Я бы использовал простую систему отложенного обновления.

1) Рабочие потоки сигнализируют «данные готовы», вызывая событие

2) Поток пользовательского интерфейса прослушивает событие. Когда он получен, он просто устанавливает флаг «требуется обновление данных» и возвращает результат, поэтому минимальная обработка происходит для самого события.

3) Поток пользовательского интерфейса использует таймер (или сидит на событиях Application.Idle), чтобы проверить флаг «требуется обновление данных» и, при необходимости, обновить пользовательский интерфейс. Во многих случаях пользовательский интерфейс необходимо обновлять только один или два раза в секунду, поэтому это не требует значительных затрат процессорного времени.

Это позволяет пользовательскому интерфейсу продолжать работать в обычном режиме все время (оставаясь интерактивным для пользователя), но в течение короткого периода времени после готовности некоторых данных он отображается в пользовательском интерфейсе.

Кроме того, и что наиболее важно для хорошего пользовательского интерфейса, этот подход можно использовать, чтобы позволить нескольким событиям «готовности данных» запускаться и объединяться в одно обновление пользовательского интерфейса. Это означает, что если 10 частей данных будут заполнены в последовательной последовательности, пользовательский интерфейс обновляется один раз, а не мигает окно в течение нескольких секунд, поскольку пользовательский интерфейс перерисовывает (без необходимости) 10 раз.

1 голос
/ 29 августа 2009

Я бы попытался переместить логику мониторинга IDevice в отдельные потоки для каждого устройства. Затем пользовательский интерфейс может запрашивать значения с помощью события таймера, нажатия кнопки или другого события, связанного с пользовательским интерфейсом. Таким образом, ваш пользовательский интерфейс останется отзывчивым, а ваши потоки выполняют всю тяжелую работу. Вот базовый пример использования непрерывного цикла. Очевидно, это очень простой пример.

public partial class Form1 : Form
{
    int count;
    Thread t = null;

    public Form1()
    {
        InitializeComponent();
    }
    private void ProcessLogic()
    {           
        //CPU intensive loop, if this were in the main thread
        //UI hangs...
        while (true)
        {
            count++;
        }
    }

    private void Form1_Load(object sender, EventArgs e)
    {
        //Cannot directly call ProcessLogic, hangs UI thread.
        //ProcessLogic();

        //instead, run it in another thread and poll needed values
        //see button1_Click
        t = new Thread(ProcessLogic);
        t.Start();

    }
    private void Form1_FormClosing(object sender, FormClosingEventArgs e)
    {
        t.Abort();
    }

    private void button1_Click(object sender, EventArgs e)
    {
        button1.Text = count.ToString();
    }
}
1 голос
/ 29 августа 2009

Некоторые обновления, отражающие предоставленные вами новые данные:

Хотя я сомневаюсь, что ваши методы EndXXX происходят в потоке пользовательского интерфейса, я все же думаю, что вы должны порождать работу в фоновом потоке и затем обновлять пользовательский интерфейс либо по мере возникновения событий, либо по мере необходимости.

Поскольку вы добавили узкий цикл while в свой пользовательский интерфейс, вам нужно вызвать Application.DoEvents , чтобы разрешить вызов других ваших событий.

Вот обновленный пример, показывающий результаты в пользовательском интерфейсе по мере их появления:

public class NewCountArgs : EventArgs
{
    public NewCountArgs(int count)
    {
         Count = count;
    }

    public int Count
    {
       get; protected set;
    }
}

public class ADP 
{
     public event EventHandler<NewCountArgs> NewCountsAvailable;

     private double _interval;
     private double _steps;
     private Thread _backgroundThread;

     public void StartAcquisition(double interval, double steps)
     {
          _interval = interval;
          _steps = steps;

          // other setup work

          _backgroundThread = new Thread(new ThreadStart(StartBackgroundWork));
          _backgroundThread.Start();
     }

     private void StartBackgroundWork()
     {
         // setup async calls on this thread
         m_rdrCountReader.BeginReadMultiSampleUInt32(-1, Callback, _steps);
     }

     private void Callback(IAsyncResult result)
     {
         int counts = 0;
         // read counts from result....

         // raise event for caller
         if (NewCountsAvailable != null)
         {
             NewCountsAvailable(this, new NewCountArgs(counts));
         }
     }
}

public class Form1 : Form
{
     private ADP _adp1;
     private TextBox txtOutput; // shows updates as they occur
     delegate void SetCountDelegate(int count);

     public Form1()
     {
         InitializeComponent(); // assume txtOutput initialized here
     }

     public void btnStart_Click(object sender, EventArgs e)
     {
          _adp1 = new ADP( .... );
          _adp1.NewCountsAvailable += NewCountsAvailable;
          _adp1.StartAcquisition(....);

          while(!_adp1.IsDone)
          {
              Thread.Sleep(100);

              // your NewCountsAvailable callbacks will queue up
              // and will need to be processed
              Application.DoEvents();
          }

          // final work here
     }

     // this event handler will be called from a background thread
     private void NewCountsAvailable(object sender, NewCountArgs newCounts)
     {
         // don't update the UI here, let a thread-aware method do it
         SetNewCounts(newCounts.Count);
     }

     private void SetNewCounts(int counts)
     {
         // if the current thread isn't the UI thread
         if (txtOutput.IsInvokeRequired)
         {
            // create a delegate for this method and push it to the UI thread
            SetCountDelegate d = new SetCountDelegate(SetNewCounts);
            this.Invoke(d, new object[] { counts });  
         }
         else
         {
            // update the UI
            txtOutput.Text += String.Format("{0} - Count Value: {1}", DateTime.Now, counts);
         }
     }
}
0 голосов
/ 30 августа 2009

Система B * * * dy captcha решила, что было бы неплохо потерять мой ответ. Я потратил полчаса, набирая текст без предупреждения или шанса исправить ... так что мы снова идем:

public class APD : IDevice
 {
    // Some members and properties go here, removed for clarity.

    public event EventHandler ErrorOccurred;
    public event EventHandler NewCountsAvailable;

    public UInt32[] BufferedCounts
    {
        // Get for the _ui32Values returned by the EndReadMultiSampleUInt32() 
        // after they were appended to a list. BufferdCounts therefore supplies 
        // all values read during the experiment.
    } 

    public bool IsDone
    {
        // This gets set when a preset number of counts is read by the hardware or when
        // Stop() is called.
    }

    // Constructor
    public APD( some parameters )
    {
       // Removed for clarity.
    }

    private void APDReadCallback(IAsyncResult __iaresResult)
    {
        try
        {
            if (this.m_daqtskRunningTask == __iaresResult.AsyncState)
            {
                // Get back the values read.
                UInt32[] _ui32Values = this.m_rdrCountReader.EndReadMultiSampleUInt32(__iaresResult);

                // Do some processing here!

                if (NewCountsAvailable != null)
                {
                    NewCountsAvailable(this, new EventArgs());
                }

                // Read again only if we did not yet read all pixels.
                if (this.m_dTotalCountsRead != this.m_iPixelsToRead)
                {
                    this.m_rdrCountReader.BeginReadMultiSampleUInt32(-1, this.m_acllbckCallback, this.m_daqtskAPDCount);
                }
                else
                {
                    // Removed for clarity.
                }
            }
        }
        catch (DaqException exception)
        {
            // Removed for clarity.
        }
    }


    private void SetupAPDCountAndTiming(double __dBinTimeMilisec, int __iSteps)
    {
        // Do some things to prepare hardware.
    }

    public void StartAPDAcquisition(double __dBinTimeMilisec, int __iSteps)
    {
        this.m_bIsDone = false;

        // Prepare all necessary tasks.
        this.SetupAPDCountAndTiming(__dBinTimeMilisec, __iSteps);

        // Removed for clarity.

        // Begin reading asynchronously on the task. We always read all available counts.
        this.m_rdrCountReader.BeginReadMultiSampleUInt32(-1, this.m_acllbckCallback, this.m_daqtskAPDCount); 
    }

    public void Stop()
    {
       // Removed for clarity. 
    }
}

Обратите внимание, я добавил некоторые вещи, которые я по ошибке пропустил в оригинальном сообщении.

Теперь в моей форме есть такой код;

public partial class Form1 : Form
{
    private APD m_APD1;
    private APD m_APD2;
    private APD m_APD3;
    private APD m_APD4;
    private DataDocument m_Document;

    public Form1()
    {
        InitializeComponent();
    }

    private void Button1_Click()
    {           
        this.m_APD1 = new APD( ... ); // times four for all APD's

        this.m_APD1.NewCountsAvailable += new EventHandler(m_APD1_NewCountsAvailable);     // times 4 again...   

        this.m_APD1.StartAPDAcquisition( ... );
        this.m_APD2.StartAPDAcquisition( ... );
        this.m_APD3.StartAPDAcquisition( ... );
        this.m_APD4.StartAPDAcquisition( ... );

        while (!this.m_APD1.IsDone) // Actually I have to check all 4
        {
             Thread.Sleep(200);
             UpdateUI();
        }

        // Some more code after the measurement is done.
    }

    private void m_APD1_NewCountsAvailable(object sender, EventArgs e)
    {
        this.m_document.Append(this.m_APD1.BufferedCounts);

    }

    private void UpdateUI()
    {
        // use the data contained in this.m_Document to fill the UI.
    } 
}

тьфу, надеюсь, я не забуду что-нибудь еще во второй раз (это научит меня не копировать это до того, как я нажму на Post).

То, что я вижу, запустив этот код, таково:

1) Объект APD работает как рекламируется, он измеряет. 2) Возникают события NewCountsAvailable и выполняются их обработчики 3) APD.StartAPDAcquisition () вызывается в потоке пользовательского интерфейса. Таким образом, BeginXXX также вызывается в этом потоке. Следовательно, по замыслу обратный вызов также находится в этом потоке, и, очевидно, обработчики событий NewCountsAvailable выполняются в потоке пользовательского интерфейса. Единственное, чего нет в потоке пользовательского интерфейса, - это ожидание аппаратного обеспечения для возврата значений в пару вызовов BeginXXX EndXXX. 4) Поскольку события NewCountsAvailable запускаются довольно часто, цикл while, который я намеревался использовать для обновления пользовательского интерфейса, не запускается. Обычно он запускается один раз в начале, а затем каким-то образом прерывается обработчиками событий, которые необходимо обработать. Я не до конца понимаю это, но это не работает ...

Я думал решить эту проблему, избавившись от цикла while и поместив Forms.Timer в форму, где UpdateUI () будет вызываться из обработчика событий Tick. Однако я не знаю, будет ли это считаться «лучшей практикой». Я также не знаю, приведут ли все эти обработчики событий в конечном итоге к потоку пользовательского интерфейса для сканирования, возможно, мне придется добавить еще несколько таких объектов APD в будущем. Также UpdateUI () может содержать более тяжелый код для вычисления изображения на основе значений в m_Document. Таким образом, тик-обработчик событий также может быть истощением ресурсов в подходе таймера. В случае, если я использую это решение, мне также понадобится событие «Готово» в моем классе APD, чтобы уведомить о завершении каждого APD.

Должен ли я, возможно, не работать с событиями для уведомления о наличии новых счетчиков, а вместо этого работать с каким-либо чтением "APD.BufferedCounts" по требованию и помещать все это в еще один поток? Я действительно понятия не имею ...

Мне в основном нужно чистое, легкое решение, которое хорошо масштабируется, если я добавлю еще больше APD:)

0 голосов
/ 29 августа 2009

Я не знаю, полностью ли я понимаю. Что делать, если вы обновите объект, который содержит текущие данные. Таким образом, обратный вызов не взаимодействует напрямую с пользовательским интерфейсом. Затем вы можете обновить пользовательский интерфейс с фиксированной скоростью, например, n раз в секунду из другого потока. См. Этот пост по обновлению пользовательского интерфейса из фоновой темы . Я предполагаю, что вы используете Windows Forms, а не WPF.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...