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.