qJake
qJake

Reputation: 17139

Prevent a text box from lagging due to fast updates

Given the following example code:

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

When you run this code, after the loop and thread terminate, the text box is still updating (presumably because of buffered Invokes). My application uses similar logic to fill a text box, and I'm having the same problem.

My question is: How can I fill this text box as fast as possible, still scroll to the bottom every time, and yet reduce/eliminate this lag?

Upvotes: 7

Views: 9823

Answers (5)

First of all you need to set DoubleBuffered to true.

Devices run at 60/fps (Frame Per Second).

You can create timer and set interval to at least 17:

timer1.Interval = 17

This will execute almost 59 updates per second. With this way you can avoid lag.

Good luck!

Upvotes: 0

Viacheslav Smityukh
Viacheslav Smityukh

Reputation: 5843

UI updating strategy is the most difficult task in data processing applications. I use the following pattern:

  1. Working thread is performing work and stores results in results storage
  2. UI updating thread is aggregating results and updates UI if necessary

Upvotes: 1

antiduh
antiduh

Reputation: 12435

I use a System.Windows.Forms.Timer to batch writes to text boxes in 50 ms chunks. I use a thread-safe RingBuffer class as the buffer between the writing threads and the form timer thread (ui thread). I can't give you the code for that, but you can replace it with a queue with locks around it, or perhaps one of the concurrent collection classes.

Tweak to fit your needs.

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

Upvotes: 0

SPFiredrake
SPFiredrake

Reputation: 3892

There are a few options you can take here. First, you can set double buffering on the form, which will end up drawing all the updates on an underlying bitmap, which then displays the newly drawn image (instead of individually drawing Controls on a graphics object). I saw about a 50% speed increase with this method. Throw this into the constructor:

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

The other thing to keep in mind is that string concatenation is SLOW for large amounts of data. You're better off using a StringBuilder to build the data and then just show it using StringBuilder.ToString (although still better to stagger the updates, maybe once every 100 iterations). On my machine, just changing it to append to the StringBuilder, it went from 2.5 minutes to run through 10k iterations to about 1.5 minute. Better, but still slow.

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

Finally, just tested out staggering (threw a single conditional into the above code, right before the Invoke call), and it finished in 2 seconds. Since we're using the StringBuilder to actually build the string, we still retain all the data, but now we only have to do the updates 100 times as opposed to 10k times.

So now, what are your options? Given that this is a WinForm application, you can utilize one of the many Timer objects to actually perform the UI update for that particular control, or you can just keep a counter of how many "reads" or "updates" to the underlying data (in your case, a stream) and only update UI on X number of changes. Utilizing both the StringBuilder option and staggered updates is probably the way to go.

Upvotes: 9

Daren Thomas
Daren Thomas

Reputation: 70344

You could try buffering: Instead of writing directly to the TextBox and then scrolling, write to a StringBuilder (make sure you figure out how to do this in a thread-safe way!) and have a separate thread flush to the TextBox in a fixed interval (say every second).

Upvotes: 3

Related Questions