Предотвратить отставание текстового поля из-за быстрых обновлений - PullRequest
7 голосов
/ 25 января 2012

С учетом следующего примера кода:

new Thread(() =>
{
    for(int i = 0; i < 10000; i++)
    {
        Invoke((MethodInvoker)() => 
        {
            myTextBox.Text += DateTime.Now.ToString() + "\r\n";
            myTextBox.SelectedIndex = myTextBox.Text.Length;
            myTextBox.ScrollToCarat();
        });
    }
}).Start();

Когда вы запускаете этот код, после завершения цикла и потока, текстовое поле все еще обновляется (предположительно, из-за буферизованных вызовов). Мое приложение использует аналогичную логику для заполнения текстового поля, и у меня та же проблема.

Мой вопрос: как я могу заполнить это текстовое поле как можно быстрее, по-прежнему прокручивая до конца каждый раз, и все же уменьшить / устранить это отставание?

Ответы [ 4 ]

8 голосов
/ 25 января 2012

Здесь вы можете выбрать несколько вариантов.Во-первых, вы можете установить двойную буферизацию в форме, в результате чего все обновления будут отображаться в базовом растровом изображении, которое затем отображает вновь нарисованное изображение (вместо индивидуального рисования элементов управления на графическом объекте).Я видел примерно 50% увеличение скорости с этим методом.Добавьте это в конструктор:

this.SetStyle(
  ControlStyles.AllPaintingInWmPaint |
  ControlStyles.UserPaint |
  ControlStyles.DoubleBuffer,true);

Еще одна вещь, которую нужно иметь в виду, это то, что конкатенация строк является МЕДЛЕННОЙ для больших объемов данных.Лучше использовать StringBuilder для построения данных, а затем просто показать их с помощью StringBuilder.ToString (хотя все же лучше разбивать обновления, возможно, один раз каждые 100 итераций).На моей машине, просто изменив его на добавление в StringBuilder, он прошел от 2,5 минут до 10 000 итераций примерно до 1,5 минут.Лучше, но все еще медленно.

new System.Threading.Thread(() =>
{
    for(int i = 0; i < 10000; i++)
    {
        sb.AppendLine(DateTime.Now.ToString());
        Invoke((Action)(() => 
        {
            txtArea.Text = sb.ToString();
            txtArea.SelectionStart = txtArea.Text.Length;
            txtArea.ScrollToCaret();
        }));
    }
}).Start();

Наконец, только что проверил ошеломление (бросил один условный код в приведенный выше код прямо перед вызовом Invoke), и оно завершилось через 2 секунды.Поскольку мы используем StringBuilder для фактического построения строки, мы по-прежнему сохраняем все данные, но теперь нам нужно выполнять обновления только 100 раз, а не 10 тысяч раз.

Итак, каковы ваши варианты?Учитывая, что это приложение WinForm, вы можете использовать один из множества объектов Timer для фактического выполнения обновления пользовательского интерфейса для этого конкретного элемента управления, или вы можете просто сохранить счетчик количества «чтений» или «обновлений» базовых данных (в твоем случае поток) и обновляй интерфейс только на Х количество изменений.Вероятно, следует использовать как параметр StringBuilder, так и поэтапное обновление.

3 голосов
/ 25 января 2012

Вы можете попробовать буферизацию : вместо записи непосредственно в TextBox и последующей прокрутки, пишите в StringBuilder (убедитесь, что вы понимаете, как это сделать потокобезопасным способом!) и имеют отдельный поток flush в TextBox через фиксированный интервал (скажем, каждую секунду).

1 голос
/ 25 января 2012

Стратегия обновления пользовательского интерфейса является наиболее сложной задачей в приложениях обработки данных. Я использую следующий шаблон:

  1. Рабочий поток выполняет работу и сохраняет результаты в хранилище результатов
  2. Поток обновления пользовательского интерфейса объединяет результаты и обновляет пользовательский интерфейс при необходимости
0 голосов
/ 25 января 2012

Я использую System.Windows.Forms.Timer для пакетной записи в текстовые поля за 50 мсек.Я использую потокобезопасный класс RingBuffer в качестве буфера между потоками записи и потоком таймера формы (потоком пользовательского интерфейса).Я не могу дать вам код для этого, но вы можете заменить его очередью с блокировками вокруг нее или, возможно, одним из параллельных классов сбора.

Настройка в соответствии с вашими потребностями.

/// <summary>
/// Ferries writes from a non-UI component to a TextBoxBase object. The writes originate
/// on a non-UI thread, while the destination TextBoxBase object can only be written
/// from the UI thread.
/// 
/// Furthermore, we want to batch writes in ~50 ms chunks so as to write to the UI as little as
/// possible.
/// 
/// This classes uses a Forms Timer (so that the timer fires from the UI thread) to create
/// write chunks from the inter-thread buffer to write to the TextBoxBase object.
/// </summary>
public class TextBoxBuffer
{
    private RingBuffer<string> buffer;

    private TextBoxBase textBox;

    private System.Windows.Forms.Timer formTimer;

    StringBuilder builder;

    public TextBoxBuffer( TextBoxBase textBox )
    {
        this.textBox = textBox;

        buffer = new RingBuffer<string>( 500 );

        builder = new StringBuilder( 500 );

        this.formTimer = new System.Windows.Forms.Timer();
        this.formTimer.Tick += new EventHandler( formTimer_Tick );
        this.formTimer.Interval = 50;
    }

    public void Start()
    {
        this.formTimer.Start();
    }

    public void Shutdown()
    {
        this.formTimer.Stop();
        this.formTimer.Dispose();
    }

    public void Write( string text )
    {
        buffer.EnqueueBlocking( text );
    }

    private void formTimer_Tick( object sender, EventArgs e )
    {
        while( WriteChunk() ) {}
        Trim();
    }

    /// <summary>
    /// Reads from the inter-thread buffer until
    /// 1) The buffer runs out of data
    /// 2) More than 50 ms has elapsed
    /// 3) More than 5000 characters have been read from the buffer.
    /// 
    /// And then writes the chunk directly to the textbox.
    /// </summary>
    /// <returns>Whether or not there is more data to be read from the buffer.</returns>
    private bool WriteChunk()
    {
        string line = null;
        int start;
        bool moreData;

        builder.Length = 0;
        start = Environment.TickCount;
        while( true )
        {
            moreData = buffer.Dequeue( ref line, 0 );

            if( moreData == false ) { break; }

            builder.Append( line );

            if( Environment.TickCount - start > 50 ) { break; }
            if( builder.Length > 5000 ) { break; }
        }

        if( builder.Length > 0 )
        {
            this.textBox.AppendText( builder.ToString() );
            builder.Length = 0;
        }

        return moreData;
    }

    private void Trim()
    {
        if( this.textBox.TextLength > 100 * 1000 )
        {
            string[] oldLines;
            string[] newLines;
            int newLineLength;

            oldLines = this.textBox.Lines;
            newLineLength = oldLines.Length / 3;

            newLines = new string[newLineLength];

            for( int i = 0; i < newLineLength; i++ )
            {
                newLines[i] = oldLines[oldLines.Length - newLineLength + i];
            }

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