Thursday, July 10, 2008

ListBox Flicker

Here's some not so simple code to solve C#'s ListBox flicker. The problem arises when the ListBox is automatically updated via some means. If its being updated via Timer or BackgroundWorker, or a Child Thread calling Invoke(), the result is the same. With anything more than a trivial number of elements, the ListBox periodically flickers when updated. Update it often and its down-right annoying.

You can try the standby solutions., like wrapping your call to update the item with


BeginUpdate();
//my update... and
EndUpdate();


... If that doesn't work, you can try changing your Form.DoubleBuffer property to true.


this.DoubleBuffer = true;


But that still may not work. You can even go and find the topics on ListView flicker. The ListView discusson on the web gets to the heart of the issue. The window is erasing the background when it doesn't need to. The solution for ListView is to derive a new class from ListView, set a few ControlStyles and then override the OnNotifyMessage() member.

But that doesn't solve it for ListBox. The solution below, draws upon the ListView solution, and then adds to it something posted by Niel B on EggHeadCafe. Thank you.

Like with ListView, the solution involves Deriving a new ListBox of your own and replacing your existing ListBox with it. Copy the code below into your project Form1.cs (or where ever) and then go to your form and replace references to ListBox with FlickerFreeListBox. You should see the FlickerFreeListBox in your controls tool box, so you can also just drop it on your forms. Since it overrides OnPaint() and must have OnDrawItem() called, the DrawMode is automatically set to OwnerDrawnFixed. If you need something else you will have to experiment.

But enough of all the chatter. Where's the code, right? Here you go.


internal class FlickerFreeListBox : System.Windows.Forms.ListBox
{
public FlickerFreeListBox()
{
this.SetStyle(
ControlStyles.OptimizedDoubleBuffer |
ControlStyles.ResizeRedraw |
ControlStyles.UserPaint,
true);
this.DrawMode = DrawMode.OwnerDrawFixed;
}
protected override void OnDrawItem(DrawItemEventArgs e)
{
if (this.Items.Count > 0)
{
e.DrawBackground();
e.Graphics.DrawString(this.Items[e.Index].ToString(), e.Font, new SolidBrush(this.ForeColor), new PointF(e.Bounds.X, e.Bounds.Y));
}
base.OnDrawItem(e);
}
protected override void OnPaint(PaintEventArgs e)
{
Region iRegion = new Region(e.ClipRectangle);
e.Graphics.FillRegion(new SolidBrush(this.BackColor), iRegion);
if (this.Items.Count > 0)
{
for (int i = 0; i < this.Items.Count; ++i)
{
System.Drawing.Rectangle irect = this.GetItemRectangle(i);
if (e.ClipRectangle.IntersectsWith(irect))
{
if ((this.SelectionMode == SelectionMode.One && this.SelectedIndex == i)
|| (this.SelectionMode == SelectionMode.MultiSimple && this.SelectedIndices.Contains(i))
|| (this.SelectionMode == SelectionMode.MultiExtended && this.SelectedIndices.Contains(i)))
{
OnDrawItem(new DrawItemEventArgs(e.Graphics, this.Font,
irect, i,
DrawItemState.Selected, this.ForeColor,
this.BackColor));
}
else
{
OnDrawItem(new DrawItemEventArgs(e.Graphics, this.Font,
irect, i,
DrawItemState.Default, this.ForeColor,
this.BackColor));
}
iRegion.Complement(irect);
}
}
}
base.OnPaint(e);
}
}


Update: September 24, 2008
Many of those who are having flicker problems with the ListBox are having them because they are programmatically updating the control. By that, I mean that the flicker issue is most pronounced when the program rather than the user input (mouse click, keyboard) results in an update to the ListBox. This programmatic control might be in the form of a timer handler, or a background thread, or the like. For that reason, I've decided to update this page to provide an easy reference to my articles covering multi-threading, synchronization, and timers. I hope you find these of value...







Timers Are A Changin'The first in my series of articles on "timers".
Locked-UpAn article on understanding and avoiding dead-lock.
Thread Syncrohinzed QueueAn article explaining how to make a synchronized queue.
Threading with .NET ThreadPoolThe first in a series of articles on multi-threading using the ThreadPool.
A Simple TaskQueueAn article and source code on an easy to use task sequencer that works in a different thread.

15 comments:

jack.valmadre said...

Hi, thanks for the article! It works really well for me except when the double-buffered listbox has focus. When it does have focus, the first item in the listbox flickers as it usually would.

As a test, I commented out all of the code in the overridden OnPaint() handler and it drew an empty ListBox as expected (I think... or should it just be a blank white rectangle? - I had scrollbars always enabled). However, as I resized the ListBox it would display that first item in the list still, flickering.

Any ideas how to fix this? Thanks

Les Potter - Author said...

Jack, I'm sorry but I can't get my first element to flicker. But there are so many different ways to use this control that I'm sure its just a difference in how we use it. However, I do have an update to the code and a suggestion. First the suggestion, if you are not using the selection feature, turn it off. The original test for whether an item was selected or not was flawed for single selection. The test should be...

if ((this.SelectionMode == SelectionMode.One && this.SelectedIndex == i)
|| (this.SelectionMode == SelectionMode.MultiSimple && this.SelectedIndices.Contains(i))
|| (this.SelectionMode == SelectionMode.MultiExtended && this.SelectedIndices.Contains(i)))


I'll do a follow-up to the article at some point when I have some more of the selection issues solved.

Immortal said...

Thanks! Finally the solution that works :]

Anonymous said...

Very nice solution, it solved my problem.

I have one suggestion tho

e.Graphics.DrawString(this.Items[e.Index].ToString(), e.Font, new SolidBrush(this.ForeColor), new PointF(e.Bounds.X, e.Bounds.Y));

should be

e.Graphics.DrawString(this.Items[e.Index].ToString(), e.Font, new SolidBrush(e.ForeColor), new PointF(e.Bounds.X, e.Bounds.Y));


so it uses the correct forecolor on select.

Anonymous said...

Excellent article. One thing I'd like to ask - is there any way to reduce the flicker while scrolling fast with keyboard? Try to fill a listbox with 100 items and hold down the key to scroll down - you will see that the blue "scroll" will jump sometimes. It seems like it's on two places at the same time sometimes. I am not sure if you know what I mean, but the problem can be reproduced easily.

Anonymous said...

Add simple databinding abilities:
protected override void OnDrawItem(DrawItemEventArgs e)
{
if (this.Items.Count > 0)
{
string text;

e.DrawBackground();

if (this.DataSource != null && this.DataManager != null)
{
System.Collections.IList list = this.DataManager.List;

PropertyDescriptorCollection propList = this.DataManager.GetItemProperties();
PropertyDescriptor prop = propList.Find(this.DisplayMember, false);

text = prop.GetValue(this.DataManager.List[e.Index]).ToString();
}
else
text = this.Items[e.Index].ToString();

e.Graphics.DrawString(text, e.Font, new SolidBrush(e.ForeColor), new PointF(e.Bounds.X, e.Bounds.Y));
}

base.OnDrawItem(e);
}

ChrisF said...

Line 48 has no effect, right? Because you never use iRegion after that point.

MikeShilov said...

Thanks for the article. It did save me a lot of time!

Anonymous said...

The second code snippet should read:

this.DoubleBuffered = true;

BTW, it really irritates me that we have to resort to subclassing to get a simple control to work properly.

Anonymous said...

i just set the style in the form constructor and it eliminated all the flicker i was getting drawing a listbox with an image for the selected item background.

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

Rick said...

Hi -- how do I get multiple columns to work with your method? I had multiple columns working before (but flickering) but with your solution, multiple columns has no effect (setting ColumnWidth that is). I tried changing from OwnerDrawFixed to OwnerDrawVariable but that did not work.

yanco said...

Hi, thank you for your article!
My listbox flickers whenever it gets focus.
(the entire listbox flickers)

If I change focus to some other control on the form, the flickering stops.
When I click on the listbox again, it will start flickering again...

Do you have any solution???

Thx,
Alon

Araldo said...

Just what I was looking for. Seems to do the trick for me.

Goodbye annoying flicker and thank you very much

Aranarth said...

Some unknown functions call OnDrawItem 4 times on the last selected/clicked item (or the first one if n/a) for each WM_SIZE the control receive.

Here is how to get rid of this design flaw in 3 steps:

1. Override the control's wndproc like this:

private bool resizing;

protected override void WndProc( ref Message m )
{
if( m.Msg == 5 ) // WM_SIZE = 0x05
{
this.resizing = true;
base.WndProc( ref m );
this.resizing = false;
return;
}
base.WndProc( ref m );
}


2. When you call OnDrawItem from OnPaint, add a custom DrawItemState flag so OnDrawItem can tell it comes from OnPaint (I use 0x1000000 because it will probably never be used by Microsoft). The OnPaint line in question looks like this in my code:

this.OnDrawItem( new DrawItemEventArgs( e.Graphics, this.Font, rect, i,
(this.SelectedIndices.Contains(i) ? DrawItemState.Selected : DrawItemState.Default) |
(DrawItemState) 0x1000000 ) );


3. And finally, put the following line at the begining of OnDrawItem:

if( e.Index == -1 || ((int)e.State & 0x1000000) == 0 && this.resizing ) return;


If you're also having the issue with many item populations, make the bool public and set it to true before the ListBox.Add loop and false after (and find a better name for the bool :p).

Steve Weeks said...

For wide strings that cause the listbox's horizontal scrollbar to appear, there is garbage displayed on the right-hand side - it looks like text is being overwritten. Do you have a fix for this?