17

My WinForms application has a TextBox that I'm using as a log file. I'm appending text without the form flickering using TextBox.AppendText(string);, however when I try to purge old text (as the control's .Text property reaches the .MaxLength limit), I get awful flicker.

The code I'm using is as follows:

public static void AddTextToConsoleThreadSafe(TextBox textBox, string text)
{
    if (textBox.InvokeRequired)
    {
        textBox.Invoke(new AddTextToConsoleThreadSafeDelegate(AddTextToConsoleThreadSafe), new object[] { textBox, text });
    }
    else
    {
        // Ensure that text is purged from the top of the textbox
        // if the amount of text in the box is approaching the
        // MaxLength property of the control

        if (textBox.Text.Length + text.Length > textBox.MaxLength)
        {
            int cr = textBox.Text.IndexOf("\r\n");
            if (cr > 0)
            {
                textBox.Select(0, cr + 1);
                textBox.SelectedText = string.Empty;
            }
            else
            {
                textBox.Select(0, text.Length);
            }
        }


        // Append the new text, move the caret to the end of the
        // text, and ensure the textbox is scrolled to the bottom

        textBox.AppendText(text);
        textBox.SelectionStart = textBox.Text.Length;
        textBox.ScrollToCaret();
    }
}

Is there a neater way of purging lines of text from the top of the control that doesn't cause flickering? A textbox doesn't have the BeginUpdate()/EndUpdate() methods that a ListView has.

Is a TextBox control even the best suited control for a console log?

Edit: The TextBox flickering appears to be the textbox scrolling up to the top (while I purge the text at the top of the control), and then it immediately scrolls back down to the bottom. - it all happens very quickly, so I just see repeated flickering.

I've also just seen this question, and the suggestion was to use a ListBox, however I don't know if this will work in my situation, as (in most cases) I'm receiving the text for the ListBox one character at a time.

Community
  • 1
  • 1
Bryan
  • 3,224
  • 9
  • 41
  • 58
  • 1
    Might want to change that "if" into "while" - in case deleting the first line of text is not enough to allow the new text to fit the TextBox. – Noam Gal Oct 11 '09 at 10:15
  • 2
    This post has some more information about this - http://stackoverflow.com/questions/1333393/how-to-prevent-a-windows-forms-textbox-from-flickering-on-resize – Chris Vig Oct 11 '09 at 11:30
  • 1
    In fact it has a very clear solution, that double buffering doesn't apply to textboxes so you should do it manually... – Vinko Vrsalovic Oct 12 '09 at 05:59
  • Vinko, please post the above comment as an answer, so I can accept it. – Bryan Oct 12 '09 at 06:40
  • JUST USE RichTextBox instead. IT IS FLICKER FREE. Set DetectUrls and ShortcutsEnabled property to FALSE for more compatibility with TextBox'es. It really works great. – TomeeNS May 31 '16 at 21:07

6 Answers6

19

Mathijs answer is works for me. I've modified it slightly so I can use with any control - a control extension:

namespace System.Windows.Forms
{
    public static class ControlExtensions
    {
        [System.Runtime.InteropServices.DllImport("user32.dll")]
        public static extern bool LockWindowUpdate(IntPtr hWndLock);

        public static void Suspend(this Control control)
        {
            LockWindowUpdate(control.Handle);
        }

        public static void Resume(this Control control)
        {
            LockWindowUpdate(IntPtr.Zero);
        }

    }
}

So all you need to do is:

myTextBox.Suspend();
// do something here.
myTextBox.Resume();

Works well. All flickering stops.

mkaj
  • 3,421
  • 1
  • 31
  • 23
  • FYI, This doesn't work well if `// do something here.` is a resize event. – Dan Bechard May 01 '14 at 15:45
  • 1
    "LockWindowUpdate is not intended for general-purpose suppression of window redraw. Use the WM_SETREDRAW message to disable redrawing of a particular window." https://learn.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-lockwindowupdate – Sverrir Sigmundarson Nov 06 '18 at 12:40
14

I found a solution looking on the internet:

    [System.Runtime.InteropServices.DllImport("user32.dll")]

    public static extern bool LockWindowUpdate(IntPtr hWndLock);

    internal void FillTB(TextBox tb, string mes) 
    {
       try
       {
          LockWindowUpdate(tb.Handle);

          // Do your thingies with TextBox tb
       }
       finally
       {
          LockWindowUpdate(IntPtr.Zero);
       }
    }
Seany84
  • 5,526
  • 5
  • 42
  • 67
4

Have you set double-buffering on your main window?

this code in your constructor after the InitializeComponent call will add double buffering and possibly reduce flicker.

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

Alastair Pitts
  • 19,423
  • 9
  • 68
  • 97
  • I've tried this, unfortunately it doesn't make any difference. – Bryan Oct 11 '09 at 11:06
  • This won't help; TextBox is inherently flickery and the best you can hope is reduce it. http://stackoverflow.com/questions/1333393/how-to-prevent-a-windows-forms-textbox-from-flickering-on-resize – Roman Starkov Jan 26 '10 at 16:11
3

I find that using SelectedText = text will reduce the flicker dramatically. For very fast updates, the flicker will be localized to the new text only and you won't get any weird behavior from the scrollbar jumping around.

void UpdateTextBox(string message)
{
   myTextBox.SelectionStart = myTextBox.Text.Length;
   myTextBox.SelectedText = message;
}

You can also use this to overwrite text written previously -- as you would need for updating a counter or download percentage for example:

void UpdateTextBox(string message, int jumpBack)
{
   myTextBox.SelectionStart = Math.Max(myTextBox.Text.Length - jumpBack, 0);
   myTextBox.SelectionLength = jumpBack;
   myTextBox.SelectedText = message;
}

Other than that, there doesn't seem to be any simple method for reducing flicker in the .NET TextBox.

Eric
  • 422
  • 3
  • 7
2

Did you try SuspendLayout() / ResumeLayout() around all your update operations?

You could also call Clear() on the textbox then reassign the truncated text.

If you are trying to implement some kind of log file viewer, you could use a ListBox instead.

Ian Robertson
  • 2,652
  • 3
  • 28
  • 36
codymanix
  • 28,510
  • 21
  • 92
  • 151
  • I tried both double buffering and SuspendLayout() / ResumeLayout(), unfortunately they don't seem to make any difference. I've updated my question about using a ListBox, I'm not sure it will work due to the fact that I (usually) add one character at a time. – Bryan Oct 11 '09 at 10:17
  • you can update the last item in the listbox to add a character and if the line is full you can append a new ListBox item. – codymanix Oct 11 '09 at 10:35
  • Did you include *all* of your operations in SuspendLayout()/ResumeLayout(), include the ScrollToCaret call? – codymanix Oct 11 '09 at 10:36
  • Yes, I put the suspend/resume as the first and last statement in the invoked part of the method. – Bryan Oct 11 '09 at 10:54
  • Also, thinking about it some more, I don't think a listbox will work, as the users need to be able to copy text to the clipboard. Theyneed to be able to select the text they want to copy using click/drag. – Bryan Oct 11 '09 at 11:00
  • I've also tried passing the form object to the static method and using Suspend/resume on that too. – Bryan Oct 11 '09 at 11:15
  • You can implement copy functionality for a ListBox with the ClipBoard class, if that helps. – codymanix Oct 11 '09 at 23:16
2

The problem is that you are adding (removing) one character at a time repeatedly and quickly. One solution would be to buffer the characters as they are being added and update the textbox at greater intervals (regardless of the amount of characters), for example, every 250 milliseconds.

This would require:

  • to have an array or stack of characters where they get added
  • to have a timer that would call a delegate that would actually do the update with the characters stored in the stack

Another option is to use both every 250 ms and 100 chars, whatever happens first. But this would probably complicate the code more without any tangible benefit.

Vinko Vrsalovic
  • 330,807
  • 53
  • 334
  • 373
  • Would that not just increase the rate of flickering? – Bryan Oct 11 '09 at 11:05
  • That's given me an idea though which I can use until I find a better solution. When the control fills, I now purge around 20% of the content (I can afford to drop 20%, although I'd rather have it work properly). This way instead of seeing the flickering constantly when the control is full, you are unlikely to notice it as the flicker might only occur once an hour instead of several times a second. – Bryan Oct 11 '09 at 11:25
  • The periods were without much a thought, I must admit :) – Vinko Vrsalovic Oct 11 '09 at 19:53
  • Despite me accepting this question, the best answer I found was Vinko's comment in reply to my original question. – Bryan Nov 16 '09 at 20:39
  • 1
    People please, USE RichTextBox instead. It is flicker free. Set DetectUrls and ShortcutsEnabled property to FALSE for more compatibility with TextBox'es. It really works great, otherwise you need to override PAINT message of the TextBox and control clipping/validating regions by yourself. – TomeeNS May 31 '16 at 21:06