Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Prevent a text box from lagging due to fast updates

Tags:

c#

winforms

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?

like image 777
qJake Avatar asked Jan 25 '12 14:01

qJake


3 Answers

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.

like image 51
SPFiredrake Avatar answered Nov 16 '22 13:11

SPFiredrake


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).

like image 3
Daren Thomas Avatar answered Nov 16 '22 15:11

Daren Thomas


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
like image 1
Viacheslav Smityukh Avatar answered Nov 16 '22 13:11

Viacheslav Smityukh