在虚拟模式下闪烁ListView
本文关键字:闪烁 ListView 模式 虚拟 | 更新日期: 2023-09-27 18:28:30
我在默认ListView中发现了一个不那么有趣的错误(不是所有者绘制的!)。当项目不断添加到其中时(以Timer
为例),并且用户试图看到距离所选项目稍远的项目时(向上或向下滚动),它会严重闪烁。
我是不是做错了什么
这里有一些代码来复制它:
- 创建WindowsFormsApplication1
- 将表单
WindowState
设置为Maximized - 在表格timer1上,将
Enabled
设置为true -
放入表单列表View1:
this.listView1.Dock = System.Windows.Forms.DockStyle.Fill; this.listView1.View = System.Windows.Forms.View.Details; this.listView1.VirtualMode = true;
-
增加一列;
-
添加事件
this.listView1.RetrieveVirtualItem += new System.Windows.Forms.RetrieveVirtualItemEventHandler(this.listView1_RetrieveVirtualItem);
-
最后是
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++; }
现在运行它,等待列表视图上的滚动条出现(因为计时器会添加足够的项目),然后:
-
选择列表视图中的第一个项目之一(使用鼠标或按键),然后使用滚动条或鼠标滚轮向下滚动,使所选项目超出当前视图(向上)。向下滚动的次数越多,闪烁就会变得越重!看看滚动条在做什么?!?!?
-
如果向下滚动所选项目,也会出现类似的效果。
问题
我该如何处理?想法是有一种不断更新的日志窗口,可以停止自动滚动并向上/向下调查附近的事件。但有了kek效应,这是不可能的!
看起来问题和Selected
/Focused
组合有关(也许微软的人可以确认)。
这里有一个可能的解决方法(它很脏,我相信!):
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++;
}
技巧是在当前选择的项目不在时,在添加新项目之前检查(这是如何检查的主题)。如果项目不在,则将焦点暂时设置为当前TopItem
(直到用户向后滚动,这样所选项目将再次"可见",此时它将获得焦点)。
在找到解决方案后,我意识到一旦选择一个项目,就无法防止闪烁。我尝试使用一些ListView
消息,但失败了。如果你想对此进行更多的研究,我认为你应该关注LVM_SETITEMSTATE
,也许还有其他一些信息。毕竟,我想到了这个想法,我们必须阻止用户选择项目。因此,要伪造所选项目,我们必须进行一些自定义绘图和伪造,如下所示:
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);
}
}
注意:上面的代码将禁用MouseDown
、MouseUp
(用于左键)和KeyDown
事件(用于箭头键),如果您想在CustomListView
之外处理这些事件,您可能需要自己引发这些事件。(默认情况下,这些事件由base.WndProc
中或之后的某些代码引发)。
仍然存在用户可以通过holding mouse down and drag to select
来选择项目的一种情况。要禁用此功能,我认为我们必须捕获消息WM_NCHITTEST
,但我们必须在正确的条件下捕获并过滤它。我试过处理这个问题,但没有成功。我希望你能做到。这只是一个演示。然而,正如我所说,我们似乎无法走另一条路。我认为你的问题是ListView
控件中的某种BUG
。
更新
事实上,我以前想过Focused
和Selected
,但那是我尝试用ListView.SelectedItems
访问SelectedItem
的时候(这是错误的)。所以我没有尝试那种方法。然而,在发现我们可以通过ListView.SelectedIndices
和ListView.Items
在虚拟模式下访问ListView
的SelectedItem
之后,我认为这个解决方案是最有效和简单的:
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;
}
注意:该代码只是用户选择只有一个项目的情况的演示,您可以添加更多代码来处理用户选择多于一个项目的情况。
我也遇到了同样的问题,@Sinatr的代码几乎完美,但当所选项目位于列表视图的顶部边界时,它会在每次更新时开始在所选项目和下一个项目之间跳跃。
我不得不将列标题的高度包括在可见性测试中,这为我解决了问题:
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;
我知道这是一篇旧文章,[King King]已经给出了一个双缓冲区的例子,但如果它对某些人有帮助的话,仍然会发布一个简单的代码&这也可以消除闪烁,即使您选择了一个项目,但您需要继承ListView才能使用这一点,因为无法从外部访问SetStyle
C#代码
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