4

I found a not so funny bug in the default ListView (not owner drawed!). It flickers heavily when items are added constantly into it (by using Timer to example) and user is trying to see items slightly away from selected item (scrolled either up or down).

Am I doing something wrong here?

Here is some code to reproduce it:

  • Create WindowsFormsApplication1;
  • set form WindowState to Maximized;
  • put on form timer1, set Enabled to true;
  • put on form listView1:

        this.listView1.Dock = System.Windows.Forms.DockStyle.Fill;
        this.listView1.View = System.Windows.Forms.View.Details;
        this.listView1.VirtualMode = true;
    
  • add one column;

  • add event

        this.listView1.RetrieveVirtualItem += new System.Windows.Forms.RetrieveVirtualItemEventHandler(this.listView1_RetrieveVirtualItem);
    
  • and finally

    private void listView1_RetrieveVirtualItem(object sender, RetrieveVirtualItemEventArgs e)
    {
        e.Item = new ListViewItem(e.ItemIndex.ToString());
    }
    
    private void timer1_Tick(object sender, EventArgs e)
    {
        listView1.VirtualListSize++;
    }
    

Now run it and wait until scrollbar on listview will appears (as timer will add enough items), then:

  • Select one of the first items in the listview (with mouse or keys), then scroll down by using scrollbar or mouse wheel, so that selected item will go outside of current view (up). The more you scroll down, the heavier flickering will become! And look at what scrollbar is doing ?!?!?

  • Similar effect appears if scrolling selected item down.


Question

How do I deal with it? Idea is to have sort of constantly updating log window with possibility to stop auto-scrolling and go up/down to investigate events in close proximity. But with that kek-effect it is just not possible!

Sinatr
  • 20,892
  • 15
  • 90
  • 319

4 Answers4

3

It looks like problem is related to Selected / Focused combo (perhaps someone from Microsoft can confirm).

Here is a possible workaround (it's dirty and I liek it!):

    private void timer1_Tick(object sender, EventArgs e)
    {
        // before adding
        if (listView1.SelectedIndices.Count > 0)
        {
            if (!listView1.Items[listView1.SelectedIndices[0]].Bounds.IntersectsWith(listView1.ClientRectangle))
                listView1.TopItem.Focused = true;
            else
                listView1.Items[listView1.SelectedIndices[0]].Focused = true;
        }
        // add item
        listView1.VirtualListSize++;
    }

Trick is to check before adding new item whenever currently selected item is away (here is the topic of how to check). And if item is away, then set focus to the current TopItem temporarily (until user scroll back, so that selected item will be again "visible" and this is when it gets focus back).

Community
  • 1
  • 1
Sinatr
  • 20,892
  • 15
  • 90
  • 319
1

After some finding for a work-around, I realized that you can't prevent flicker once you select an item. I've tried using some ListView messages but fail. If you want to research more on this, I think you should pay some attention at LVM_SETITEMSTATE and maybe some other messages. After all, I thought of this idea, we have to prevent the user from selecting an item. So to fake a selected item, we have to do some custom drawing and faking like this:

public class CustomListView : ListView
{
        public CustomListView(){
            SelectedIndices = new List<int>();
            OwnerDraw = true;
            DoubleBuffered = true;
        }
        public new List<int> SelectedIndices {get;set;}
        public int SelectedIndex { get; set; }
        protected override void WndProc(ref Message m)
        {
            if (m.Msg == 0x1000 + 43) return;//LVM_SETITEMSTATE                
            else if (m.Msg == 0x201 || m.Msg == 0x202)//WM_LBUTTONDOWN and WM_LBUTTONUP
            {
                int x = m.LParam.ToInt32() & 0x00ff;
                int y = m.LParam.ToInt32() >> 16;
                ListViewItem item = GetItemAt(x, y);
                if (item != null)
                {
                    if (ModifierKeys == Keys.Control)
                    {
                        if (!SelectedIndices.Contains(item.Index)) SelectedIndices.Add(item.Index);
                    }
                    else if (ModifierKeys == Keys.Shift)
                    {
                        for (int i = Math.Min(SelectedIndex, item.Index); i <= Math.Max(SelectedIndex, item.Index); i++)
                        {
                            if (!SelectedIndices.Contains(i)) SelectedIndices.Add(i);
                        }
                    }
                    else
                    {
                        SelectedIndices.Clear();
                        SelectedIndices.Add(item.Index);
                    }
                    SelectedIndex = item.Index;                        
                    return;
                }                    
            }
            else if (m.Msg == 0x100)//WM_KEYDOWN
            {
                Keys key = ((Keys)m.WParam.ToInt32() & Keys.KeyCode);
                if (key == Keys.Down || key == Keys.Right)
                {
                    SelectedIndex++;
                    SelectedIndices.Clear();
                    SelectedIndices.Add(SelectedIndex);
                }
                else if (key == Keys.Up || key == Keys.Left)
                {
                    SelectedIndex--;
                    SelectedIndices.Clear();
                    SelectedIndices.Add(SelectedIndex);
                }
                if (SelectedIndex == VirtualListSize) SelectedIndex = VirtualListSize - 1;
                if (SelectedIndex < 0) SelectedIndex = 0;
                return;
            }
            base.WndProc(ref m);                
        }
        protected override void OnDrawColumnHeader(DrawListViewColumnHeaderEventArgs e)
        {
            e.DrawDefault = true;
            base.OnDrawColumnHeader(e);
        }
        protected override void OnDrawItem(DrawListViewItemEventArgs e)
        {
            i = 0;           
            base.OnDrawItem(e);
        }
        int i;
        protected override void OnDrawSubItem(DrawListViewSubItemEventArgs e)
        {                
            if (!SelectedIndices.Contains(e.ItemIndex)) e.DrawDefault = true;
            else
            {
                bool isItem = i == 0;
                Rectangle iBound = FullRowSelect ? e.Bounds : isItem ? e.Item.GetBounds(ItemBoundsPortion.ItemOnly) : e.SubItem.Bounds;
                Color iColor = FullRowSelect || isItem ? SystemColors.HighlightText : e.SubItem.ForeColor;
                Rectangle focusBound = FullRowSelect ? e.Item.GetBounds(ItemBoundsPortion.Entire) : iBound;
                if(FullRowSelect || isItem) e.Graphics.FillRectangle(SystemBrushes.Highlight, iBound);                    
                TextRenderer.DrawText(e.Graphics, isItem ? e.Item.Text : e.SubItem.Text,
                    isItem ? e.Item.Font : e.SubItem.Font, iBound, iColor,
                    TextFormatFlags.LeftAndRightPadding | TextFormatFlags.VerticalCenter);
                if(FullRowSelect || isItem) 
                  ControlPaint.DrawFocusRectangle(e.Graphics, focusBound);
            }
            i++;                
            base.OnDrawSubItem(e);
        }
}

NOTE: This code above will disable MouseDown, MouseUp (for Left button) and KeyDown event (for arrow keys), if you want to handle these events outside of your CustomListView, you may want to raise these events yourself. (By default, these events are raised by some code in or after base.WndProc).

There is still one case in which the user can select the item by holding mouse down and drag to select. To disable this, I think we have to catch the message WM_NCHITTEST but we have to catch and filter it on right condition. I've tried dealing with this but no luck. I hope you can do it. This is just a demo. However as I said, we seem unable to go another way. I think your problem is some kind of BUG in the ListView control.

UPDATE

In fact I thought of Focused and Selected before but that's when I've tried accessing the SelectedItem with ListView.SelectedItems (That's wrong). So I didn't trying that approach. However after finding out that we can access the SelectedItem of a ListView in virtual mode via the ListView.SelectedIndices and ListView.Items, I think this solution is the most efficient and simple one:

int selected = -1;
bool suppressSelectedIndexChanged;
private void timer1_Tick(object sender, EventArgs e)
{
  listView1.SuspendLayout();                
  if (selected > -1){
     ListViewItem item = listView1.Items[selected];
     Rectangle rect = listView1.GetItemRect(item.Index);
     suppressSelectedIndexChanged = true;                    
     item.Selected = item.Focused = !(rect.Top <= 2 || rect.Bottom >= listView1.ClientSize.Height-2);
     suppressSelectedIndexChanged = false;
  }
  listView1.VirtualListSize++;
  listView1.ResumeLayout(true);
}
private void listView1_SelectedIndexChanged(object sender, EventArgs e){
   if (suppressSelectedIndexChanged) return;
   selected = listView1.SelectedIndices.Count > 0 ? listView1.SelectedIndices[0] : -1;
}

NOTE: The code is just a demo for the case user selects just 1 item, you can add more code to deal with the case user selects more than 1 item.

King King
  • 61,710
  • 16
  • 105
  • 130
  • When I discover this bug, I was using custom `ListView` with double buffering enabled. Just tried to apply your solution, it still flickers. Maybe word *flicker* is not the right one, it looks like `ListView` is trying to repaint self in 2 points - where selected item is and where is current view point is (defined by `TopItem`?). And constant switch between them (visible on a scroll bar) causing this *flickering*. – Sinatr Aug 20 '13 at 15:53
  • your problem is not easy to solve, I've tried some work-around however in virtual mode, even we can't access (read or write) any Properties of the ListView related to indices, Count, items... – King King Aug 20 '13 at 16:28
  • well, it's not easy, but should be possible, i am working on one. – Sinatr Aug 20 '13 at 16:30
  • 2
    It is not the kind of problem that double-buffering solves, it is not actually a flicker issue. The selected item is indeed the problem, it tries to keep it visible but is then defeated by the added item. It jumps back and forth. – Hans Passant Aug 20 '13 at 17:36
  • @Sinatr see my updated answer for a work-around. BTW I think when you `auto-scroll` the `ListView` that way, you should disable user from selecting an item. If that's the case, it's much simpler to do. – King King Aug 20 '13 at 23:01
  • @KingKing, I appreciate an effort you have put into this, thanks. However, your solution looks way more complicated than one I've found yesterday by accident and it still have *issues*.I was thinking to simply remove selection from current item so if user scroll away, then he will have to select an item after again. But surprisingly this didn't work. And then I remember about `Focused`. – Sinatr Aug 21 '13 at 07:08
  • @Sinatr I don't know if your solution (if found) is OK now, but I have a simpler solution in my Update, it works OK. The `Selected Item` will be `deselected` when it's out of view, otherwise it will be `selected` again/back. (that's better than forcing user to re-select it manually). – King King Aug 21 '13 at 11:56
  • @KingKing, thanks for the update, it looks more simple now. However I can't figure out what exactly are you doing? I see you track selected item and I see you set it to be `Focused` and `Selected` or not (at once?). Could you elaborate a little bit more why? What is the solution? To remove `Selected` and `Focused` when control is not visible? Why this way (using `GetItemRect`, then checking `Top` and `Bottom`) is better than one in my solution (using `Bounds` property and `Intersect`, which I think does same check)? – Sinatr Aug 22 '13 at 07:08
  • @Sinatr there is no difference between `Bounds` the the use of `GetItemRect`. Using `Top` and `Bottom` to check of course is better. About the `Focused` and `Selected`, both of these should be set to the same value together, otherwise it won't work (of course I've tested and saw that, not from my knowledge before). – King King Aug 22 '13 at 08:09
  • @KingKing, Cảm ơn (thanks), but I like more own answer (it seems to work very well so far). – Sinatr Aug 22 '13 at 13:34
  • @Sinatr it's interesting that you know how to say thanks in `Vietnamese` :) – King King Aug 22 '13 at 13:38
0

I had the same problem and the code of @Sinatr almost works perfect, however when the selected item is right on the top border of the listview, it starts jumping between the selected and the next item on each update.

I had to include the height of the column headers to the visibility test which solved the problem for me:

if (lstLogMessages.SelectedIndices.Count > 0)
{
    Rectangle selectedItemArea = lstLogMessages.Items[lstLogMessages.SelectedIndices[0]].Bounds;
    Rectangle listviewClientArea = lstLogMessages.ClientRectangle;
    int headerHeight = lstLogMessages.TopItem.Bounds.Top;
    if (selectedItemArea.Y + selectedItemArea.Height > headerHeight && selectedItemArea.Y + selectedItemArea.Height < listviewClientArea.Height)   // if the selected item is in the visible region 
    {
        lstLogMessages.Items[lstLogMessages.SelectedIndices[0]].Focused = true;
    }
    else
    {
        lstLogMessages.TopItem.Focused = true;
    }
}

lstLogMessages.VirtualListSize = currentView.MessageCount;
0

i know this is old post and [King King] has already given a double buffer example but still posting a simple code if it helps some one & this also removes flickering even if you have a item selected, but you need to Inherit ListView to use this cause SetStyle is not accessible from outside

C# Code

public class ListViewEX : ListView
{
    public ListViewEX()
    {
        SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer, true);
    }
}

VB.NET

Public Class ListViewEX
    Inherits ListView
    Public Sub New()
        SetStyle(ControlStyles.AllPaintingInWmPaint Or ControlStyles.OptimizedDoubleBuffer, True)
    End Sub
End Class
Jack Gajanan
  • 1,596
  • 14
  • 18