防止文本框因快速更新而滞后

时间:2012-01-25 14:33:21

标签: c# winforms

给出以下示例代码:

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();

运行此代码时,在循环和线程终止后,文本框仍在更新(可能是因为缓冲的Invokes)。我的应用程序使用类似的逻辑来填充文本框,我遇到了同样的问题。

我的问题是:如何尽可能快地填写此文本框,每次仍然滚动到底部,然后减少/消除这种滞后?

5 个答案:

答案 0 :(得分:8)

您可以在这里选择几个选项。首先,您可以在窗体上设置双缓冲,最终将在底层位图上绘制所有更新,然后显示新绘制的图像(而不是在图形对象上单独绘制控件)。我用这种方法看到了大约50%的速度提升。把它扔进构造函数:

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

要记住的另一件事是字符串连接对于大量数据来说是SLOW。你最好使用StringBuilder来构建数据,然后使用StringBuilder.ToString显示它(尽管更好地错开更新,可能每100次迭代一次)。在我的机器上,只需更改它以附加到StringBuilder,它从2.5分钟开始运行10k迭代到大约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();

最后,刚刚测试出惊人的结果(在I​​nvoke调用之前将一个条件输入上面的代码),并在2秒内完成。由于我们使用StringBuilder实际构建字符串,我们仍然保留所有数据,但现在我们只需要进行100次更新,而不是10k次。

现在,你有什么选择?鉴于这是一个WinForm应用程序,您可以利用众多Timer对象中的一个来实际执行该特定控件的UI更新,或者您可以只保留对底层数据“读取”或“更新”的计数器(在您的情况下,一个流)并且只更新X个更改的UI。利用StringBuilder选项和交错更新可能是最佳选择。

答案 1 :(得分:3)

你可以尝试缓冲:不要直接写入TextBox然后滚动,而是写一个StringBuilder(确保你弄明白如何在线程安全的方式!)并在固定的时间间隔内(例如每秒)为TextBox提供一个单独的线程 flush

答案 2 :(得分:1)

UI更新策略是数据处理应用程序中最困难的任务。 我使用以下模式:

  1. 工作线程正在执行工作并将结果存储在结果存储中
  2. UI更新线程正在聚合结果并在必要时更新UI

答案 3 :(得分:0)

我使用System.Windows.Forms.Timer以50毫秒的块批量写入文本框。我使用线程安全的RingBuffer类作为写线程和表单计时器线程(ui线程)之间的缓冲区。我无法为您提供代码,但您可以将其替换为带有锁的队列,或者可能是其中一个并发集合类。

调整以满足您的需求。

/// <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;
        }
    }
}

答案 4 :(得分:0)

首先,您需要将DoubleBuffered设置为true

设备以60 / fps(每秒帧)的速度运行。

您可以创建计时器并将间隔设置为至少17:

timer1.Interval = 17

这将每秒执行近59次更新。这样可以避免滞后。

祝你好运!