【问题标题】:Flickering ListView in virtual mode在虚拟模式下闪烁的 ListView
【发布时间】:2023-03-30 03:40:01
【问题描述】:

我在默认 ListView 中发现了一个不太有趣的错误(不是所有者绘制的!)。当不断向其中添加项目(例如使用Timer)并且用户试图看到项目稍微远离所选项目(向上滚动或下)。

我在这里做错了吗?

这里有一些代码可以重现它:

  • 创建 WindowsFormsApplication1;
  • 将表单WindowState 设置为最大化;
  • 输入表单timer1,将Enabled设置为true;
  • 放入表单listView1:

        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-effect 这是不可能的!

【问题讨论】:

  • 您的TimerInterval 怎么样?
  • @KingKing,默认为100。或者你是什么意思?
  • @Sinatr 如果计时器的Interval 很小,则可能会发生很多闪烁,但是即使启用了DoubleBuffered,即使是最小的1 也无关紧要,请参阅我的答案。

标签: c# winforms listview virtualmode


【解决方案1】:

我知道这是旧帖子,[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

【讨论】:

    【解决方案2】:

    我遇到了同样的问题,@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;
    

    【讨论】:

      【解决方案3】:

      在寻找解决方法后,我意识到您一旦选择项目就无法防止闪烁。我尝试使用一些 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);
              }
      }
      

      注意:上面的代码将禁用MouseDownMouseUp(用于左键)和KeyDown 事件(用于箭头键),如果您想在您的外部处理这些事件CustomListView,您可能想自己提出这些事件。 (默认情况下,这些事件由base.WndProc 中或之后的某些代码引发)。

      还有一种情况,用户可以通过holding mouse down and drag to select选择项目。要禁用此功能,我认为我们必须捕获消息WM_NCHITTEST,但我们必须在正确的条件下捕获并过滤它。我试过处理这个但没有运气。我希望你能做到。这只是一个演示。然而,正如我所说,我们似乎无法走另一条路。我认为您的问题是ListView 控件中的某种BUG

      更新

      事实上,我之前想到了FocusedSelected,但那是我尝试使用ListView.SelectedItems 访问SelectedItem 的时候(这是错误的)。所以我没有尝试这种方法。但是在发现我们可以通过ListView.SelectedIndicesListView.Items在虚拟模式下访问ListViewSelectedItem后,我认为这个解决方案是最有效和最简单的一个:

      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;
      }
      

      注意:代码只是用户选择仅1项的示例,您可以添加更多代码来处理用户选择更多的情况超过 1 项

      【讨论】:

      • 当我发现这个错误时,我使用的是自定义 ListView 并启用了双缓冲。刚刚尝试应用您的解决方案,它仍然闪烁。也许单词 flicker 不是正确的,看起来ListView 试图在 2 点内重新绘制自我 - 选定项目在哪里以及当前视点在哪里(由 TopItem 定义?) .并在它们之间不断切换(在滚动条上可见)导致此闪烁
      • 您的问题不容易解决,我尝试了一些解决方法,但是在虚拟模式下,即使我们无法访问(读取或写入)任何与索引、计数相关的 ListView 属性, 物品...
      • 嗯,这并不容易,但应该有可能,我正在努力。
      • 不是双缓冲解决的那种问题,其实也不是闪烁问题。所选项目确实是问题所在,它试图使其保持可见,但随后被添加的项目击败。它来回跳跃。
      • @Sinatr 查看我更新后的解决方法。顺便说一句,我认为当你 auto-scroll ListView 这样时,你应该禁止用户选择一个项目。如果是这样的话,做起来就简单多了。
      【解决方案4】:

      看起来问题与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++;
          }
      

      诀窍是检查在添加新项目之前,只要当前选定的项目不在(here 是如何检查的主题)。如果项目不在,则暂时将焦点设置为当前的TopItem(直到用户向后滚动,这样选定的项目将再次“可见”,这就是它重新获得焦点的时候)。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2012-03-18
        • 2010-09-25
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-08-04
        相关资源
        最近更新 更多