• .net类库里ListView的一个BUG


    今天在CSDN论坛里看一个帖子,说是在ListView中添加了条目后第一行内容不显示,为了还原他的问题我写了以下代码。

     
    复制代码
            private void LoadFiles(DirectoryInfo dir)
            {
     
                FileInfo[] files = dir.GetFiles();
     
                foreach (FileInfo file in files)
                {
                    ListViewItem item = new ListViewItem();
                    item.Tag = file;
                    item.SubItems.AddRange(SubItems.ToArray());
                    listView1.Items.Add(item);
                    UpdateItem(item);
                }
            }
     
            ListViewItem.ListViewSubItem[] SubItems
            {
                get
                {
                    return new ListViewItem.ListViewSubItem[] { new ListViewItem.ListViewSubItem(), new ListViewItem.ListViewSubItem() };
                }
            }
     
            private void UpdateItem(ListViewItem item)
            {
                FileInfo info = (FileInfo)item.Tag;
                
                item.Text = info.Name;
                item.SubItems[1].Text = info.Length.ToString("N0");
                item.SubItems[2].Text = info.LastWriteTime.ToString();
     
            }
    复制代码
    ListView共有3列,分别显示文件名、大小和最后修改时间,运行以后我发现,文件名可以显示,但是后面2列不能显示。经过各种调试和偶遇,终于让我发现,只要改变ListViewItem.Text的值,后面两列的内容就能够显示了,于是初步解决方案是改变ListViewItem.Text的赋值顺序,把它放在所有SubItem.Text赋值以后再赋值。
     
    为了找到根本原因,我翻查了.net类库的源代码,最后终于发现问题所在,先来看看ListViewSubItem.Text的源代码。
     
    复制代码
                public string Text {
                    get { 
                        return text == null ? "" : text; 
                    }
                    set { 
                        text = value;
                        if (owner != null) {
                            owner.UpdateSubItems(-1);
                        } 
                    }
                } 
    复制代码
    在对此属性赋值时,首先检查owner字段的值是否为空,如果不为空才调用owner.UpateSubItems方法对ListView进行更新。很明显,出现上面的问题时,owner值一定为空,通过在VS里调试证实了这点。
     
    现在的问题是,为什么这owner会为空,owner的类型是ListViewItem,从字面理解它应该是SubItem所属的那个行项目,正常情况下在添加到ListViewItem.SubItems以后就应该不会为空,于是我猜是在添加的时候这个owner没有被赋值。后来通过查看源代码以后证实了我的想法,来看看ListViewSubItemCollection关于添加子项的源码
     
    复制代码
                public ListViewSubItem Add(ListViewSubItem item) { 
                    EnsureSubItemSpace(1, -1); 
                    item.owner = this.owner;
                    owner.subItems[owner.SubItemCount] = item; 
                    owner.UpdateSubItems(owner.SubItemCount++);
                    return item;
                }
     
                public void AddRange(ListViewSubItem[] items) {
                    if (items == null) {
                        throw new ArgumentNullException("items"); 
                    }
                    EnsureSubItemSpace(items.Length, -1); 
     
                    foreach(ListViewSubItem item in items) {
                        if (item != null) { 
                            owner.subItems[owner.SubItemCount++] = item;
                        }
                    }
     
                    owner.UpdateSubItems(-1);
                } 
    复制代码
    很明显,Add方法对owner进行了赋值,但AddRange方法没有,而在帖子里所用的是AddRange方法,所以造成了这个问题。
     
    那为什么对Text赋值以后,子项里的内容又能够显示了呢?好吧,再来看看ListViewItem.Text的源码
     
    复制代码
            public string Text {
                get { 
                    if (SubItemCount == 0) { 
                        return string.Empty;
                    } 
                    else {
                        return subItems[0].Text;
                    }
                } 
                set {
                    SubItems[0].Text = value; 
                } 
            }
    复制代码
    对ListViewItem.Text的赋值实际上就是对它第0个子项的Text赋值,那为什么这个子项可以工作呢,好吧,再来看看第0个子项的来历,以下是ListViewItem.SubItems的源码。
     
    复制代码
            public ListViewSubItemCollection SubItems {
                get { 
                    if (SubItemCount == 0) {
                        subItems = new ListViewSubItem[1];
                        subItems[0] = new ListViewSubItem(this, string.Empty);
                        SubItemCount = 1; 
                    }
     
                    if (listViewSubItemCollection == null) { 
                        listViewSubItemCollection = new ListViewSubItemCollection(this);
                    } 
                    return listViewSubItemCollection;
                }
            }
    复制代码
    由于帖子里使用了ListViewItem的无参数构造函数,因此在第一次调用SubItems属性时,SubItemCount的值为0,这时就会自动插入一个子项,而这里使用的构造函数直接把当前ListViewItem传进去了,子项的owner就有了值,因此可以正常显示文字。回想前面的对子项Text赋值的源码,在赋值以后会调用owner.UpdateSubItems(-1)来更新显示,这个方法并不是仅仅更新一个子项,而是会更新所有子项,因此所有的内容又都可以看到了。
     
    最后还有一个问题,为什么调用ListView.Refresh或Invalidate方法没用呢?我没有做深入研究,只是做一个猜想。因为.net的ListView控件只是对原生Windows的ListView控件的封装,在OwnerDraw为false时,所有的绘图都由原生的ListView控件完成。从以上代码可以看出,子项的文本在托管代码里保存了一份,而我敢肯定在原生的控件里也保存了一份,当owner存在时,这两个值是相同的,而在owner不存在时,由于没有更新导致原生控件里没有更新而失去了同步,这样无论怎么Refresh都是没有用的。
     
    Bug就分析到此,原因找到了,解决办也自然有了。但我想说的不是解决办法,而是怎么利用这个BUG,再来看看ListViewItem.UpdateSubItems方法。
     
    复制代码
            internal void UpdateSubItems(int index){ 
                UpdateSubItems(index, SubItemCount);
            } 
     
            internal void UpdateSubItems(int index, int oldCount){
                if (listView != null && listView.IsHandleCreated) {
                    int subItemCount = SubItemCount; 
     
                    int itemIndex = Index; 
     
                    if (index != -1) {
                        listView.SetItemText(itemIndex, index, subItems[index].Text); 
                    }
                    else {
                        for(int i=0; i < subItemCount; i++) {
                            listView.SetItemText(itemIndex, i, subItems[i].Text); 
                        }
                    } 
     
                    for (int i = subItemCount; i < oldCount; i++) {
                        listView.SetItemText(itemIndex, i, string.Empty); 
                    }
                }
            }
    复制代码
    ListViewSubItem.set_Text在调用此方法时,专入的参数是-1,可以看出这将会导致所有的子项重绘,这点前面说过了。按此计算,如果ListView有10列,那每行需要重绘100次,其中有90次是在做无用功,不但增加了CPU的负担,还会可能会导致界面闪烁,但如果合理地利用这个BUG,可以有效改善这个情况。
     
    ==补充======================================================
     
    做了一个实地测试,30列200行,做一次所有行和列的刷新,常规方法700ms,而利用这个BUG可以降到25ms。
     
     
     
    最后做个总结
     
    在为ListView添加行项目时,各项目的SubItem如果采用AddRange方法添加,会导致在后续更新SubItem的Text时,界面上不会更改,解决办法有两种:
     
    1、不要使用ListViewItem.SubItems.AddRange方法,而改用Add。
     
    2、仍旧使用AddRange方法,但在更新内容时,第0列(也就是ListViewItem.Text)最后更新。
     
    但是这个BUG歪打正着地为提升ListView性能提供了可能,可使用上面的第2个解决办法实现,在大数量时效果尤其明显。
  • 相关阅读:
    Kendo UI开发教程(8): Kendo UI 特效概述
    6.3 计算字符在字符串中出现的次数
    有意思的GacUI
    Qt信号量QSemaphore(在线程里使用,结合生产者消费者的问题)
    Qt 自定义事件详细实例(继承QEvent,然后QCoreApplication::postEvent()、sendEvent())
    为什么选择使用 Dropbox 而不是其他品牌同步工具(不要加上多余的功能,要极致和专注)
    DropBox与Box的区别,包括直接的投资人的评价(本地Sync可能还是挺重要的)
    ASP.NET所谓前台调用后台、后台调用前台想到HTTP——实践篇
    8个免费实用的C++GUI库
    .NET 利用反射将对象数据添加到数据库
  • 原文地址:https://www.cnblogs.com/gc2013/p/4103910.html
Copyright © 2020-2023  润新知