65.9K
CodeProject 正在变化。 阅读更多。
Home

一个更易于使用的 ListView

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (741投票s)

2006年10月17日

GPL3

110分钟阅读

viewsIcon

12863364

downloadIcon

93

.NET ListView 在咖啡因、瓜拉纳和类固醇的作用下性能达到极致。

一个更易于使用的 ListView...

Screenshot - ObjectListView.jpg

...在 Vista 及更高版本上看起来甚至更漂亮。

Screenshot - ReportModernExample.jpg

在您的列表中添加漂亮的图形、按钮和描述,让您的用户更喜爱您的应用程序
稍作修改,您甚至可以制作出像这样的酷炫作品

工作量更少,您就可以在 ListView 上添加闪亮的、吸引眼球的动画

[对于那些注意到上述图形缺少动画基本要求(即动画性)的人,CodeProject 不支持页面内动画,因此请点击此处查看实际动画。]

前言

所有项目都存在功能蔓延的问题。那些最初简单优雅的东西最终会变成减肥广告中的“before”图片。这个控件自首次编写以来已经大大增加。如果你想用 ListView 做些什么,这个控件可能有一些代码可以帮助你。对于那些时间紧迫的人,这个控件具有以下主要功能:

  • 它自动将对象集合转换为功能齐全的 ListView,包括自动排序和分组。
  • 它可以轻松编辑 ListView 中显示的值。
  • 它支持三态复选框(开、关、不确定),即使在虚拟模式下和子项中也是如此。
  • 它支持高度可定制的单元格和列标题工具提示。
  • 它可以轻松地从 ListView 生成漂亮的报告。
  • 它支持所有 ListView 视图(报告、平铺、大图标和小图标)。
  • 它支持所有者绘制,包括渲染动画 GIF。
  • 它的列可以是固定宽度或限制最小/最大宽度。
  • 当列表为空时(显然),它会显示高度可定制的“列表为空”消息。
  • 它的行高可以显式设置。
  • 它支持用户通过右键单击标题来选择可见列。
  • 它支持自动调整大小以填充任何未占用宽度的列。
  • 它支持热追踪,带有文本字体/颜色更改和装饰。
  • 它支持图像和文本叠加,以及任意叠加(个人信息框)和装饰(爱心)。
  • 它对拖放提供了广泛的支持。
  • 它支持单元格中的超链接。
  • 它支持列标题可以有复选框、图像甚至垂直文本。它们还可以根据状态(正常、热和按下状态)进行样式设置。
  • 它支持许多分组格式选项,包括可折叠组。组可以在虚拟列表中显示!
  • 它有一个版本(TreeListView),它将树结构与 ListView 的列结合在一起。
  • 它有一个版本(VirtualObjectListView),支持数百万行。
  • 它有一个版本(FastObjectListView),可以在不到0.1秒的时间内构建一个包含100,000个对象的列表。
  • 它有一个版本 (DataListView) 支持数据绑定,另一个版本 (FastDataListView) 支持大型 (100,000+) 数据集的数据绑定。
  • 它通过 IVirtualListDataSource 接口使实现自己的虚拟列表变得简单。
  • 它支持过滤,包括显示和突出显示与给定字符串匹配的行(包括正则表达式和前缀匹配)。
  • 它支持单元格、行或整个列表上的动画。
  • 支持原生背景图片及其固有的局限性。
  • 它支持 Excel 风格的筛选。[v2.5]
  • 它支持禁用行 [v2.8]
  • Has 每个控件的数据绑定版本。
  • TreeListView 支持层次结构复选框。

这个控件有自己的网站,由 SourceForge 托管:ObjectListView - 我如何学会停止担忧并爱上 .NET 的 ListView(使用酷炫的Sphinx 文档工具制作)。这不是一个空壳网站。它实际上包含许多有用的信息。在那里您可以找到一个分步教程来帮助您入门,以及一个操作指南,向您展示如何完成常见任务。本文只是一个介绍。

不着急的人现在可以阅读文章的其余部分了。 微笑 | <img src= 

引言

Perl 的作者 Larry Wall 曾写道,任何优秀程序员的三个基本性格缺陷是懒惰、不耐烦和傲慢。优秀的程序员希望做最少的工作(懒惰)。他们希望他们的程序运行得快(不耐烦)。他们对自己编写的东西感到过分的自豪(傲慢)。

ObjectListView 鼓励懒惰和傲慢的恶习,因为它允许程序员做更少的工作,但仍然能产生出色的结果。

 

ListView “问题”

我经常发现我有一组对象,我想以某种表格格式呈现给用户。它可能是企业的客户列表、已知的 FTP 服务器列表,甚至是目录中的文件列表这样平淡无奇的东西。从用户界面来看,ListView 是这些情况的完美控件。然而,一想到要使用 ListView,我就忍不住呻吟,并暗自希望我能改用 ListBox

之所以想避免使用 ListView,是因为它需要大量的样板代码才能工作:创建 ListViewItems,添加所有 SubItems,捕获标题点击事件并根据数据类型对项目进行排序。对于 ListView 的每个实例,这些任务都略有不同。如果你想支持分组,还有更大一块样板代码需要复制,然后稍作修改。

对于一个天生懒惰的人来说,这工作量太大了。ObjectListView 应运而生,旨在减轻这种工作负担。

特点

一个 ObjectListView 提供两组功能。第一组旨在使 ListView 更易于使用。这组功能包括从自动将模型对象列表转换为功能齐全的 ListView,到使拖放和单元格编辑更易于使用。第二组功能为 ListView 添加了新功能,例如图像叠加和可定制的工具提示。

1. ObjectListView 的基本用法

1.1 第一步

在您的项目中使用 ObjectListView 有两种方法:

1. 使用 ObjectListView 项目

  1. 下载 ObjectListView 捆绑包。
  2. 将适当的 ObjectListViewNNNN 项目添加到您的项目中(每个 Visual Studio 版本都有不同的项目)。为此,右键单击您的解决方案;选择“添加...”,“现有项目”,然后选择正确版本的 ObjectListViewNNNN.csproj
  3. 在您的项目中,添加对 ObjectListViewNNNN 项目的引用(右键单击您的项目;选择“添加引用...”,选择“项目”选项卡;然后双击 ObjectListViewNNNN 项目)。
  4. 构建您的项目。如果收到错误提示找不到 NUnit,只需从解决方案中删除 TestsNNNN 项目即可。

项目构建完成后,工具箱中应该会有一个新部分“ObjectListView Components”。该部分中应该有 ObjectListView 及其相关控件。然后您可以将 ObjectListView 拖放到您的窗口上,并像使用标准 ListView 控件一样使用它。

2. 使用 ObjectListView.dll

如果您不想将 ObjectListView 项目添加到您的项目中,您也可以只添加 ObjectListView.dll 文件。

  1. 下载或构建 ObjectListView.dll 文件。
  2. 在您的项目中,添加对 ObjectListView.dll 的引用(右键单击您的项目;选择“添加引用...”,选择“浏览”选项卡;导航到 ObjectListView.dll 文件并双击它)。

添加 DLL 不会自动将任何新组件添加到您的工具箱中。在您将 DLL 添加到您的项目后,您需要手动添加它们。

1.2 让它动起来

“简单的事情应该简单。复杂的事情应该成为可能。”

ObjectListView 的主要设计目标是让程序员的生活更简单。然而,这个控件提供了很多功能,如果你试图一次性吸收所有功能,可能会让人不知所措。最好从 ObjectListView 的基本功能开始:它从模型对象列表生成一个功能齐全的 ListView。通常,控件在 IDE 中配置(设置属性和创建列),然后,用一行代码,它就可以像这样投入使用:

<span id="ArticleContent">this.objectListView1.SetObjects(allPeople);</span>

就是这样!

简单、快速、不复杂、不增肥,而且没有一行样板代码。无需进一步工作,这个 ObjectListView 就是一个功能齐全的 ListView,它将处理拖放、交替行着色、列点击排序、数据格式化、分组,甚至可能还有编辑功能。演示项目中的“简单示例”选项卡展示了仅通过 IDE 配置和这一行代码可以实现什么。

2. 给出更多细节

2.1 SetObjects() 背后的原理

这里实际发生了什么?当你调用 SetObjects() 时,ObjectListView 会遍历给定的模型对象列表,提取每个列指定的外观,将该外观转换为字符串,然后将这些字符串组合起来在 ListView 中创建一行。对于那些习惯于图片思考的人,你可以这样可视化这个过程:

2.2 你必须忘记

对于那些以前挣扎于 ListView 的人,你们必须忘记。ObjectListView 不是 ListView 的直接替代品。如果你有一个现有项目,你不能简单地创建 ObjectListView 来替代 ListViewObjectListView 需要一种不同的思维模式。如果你能完成改变思维的痛苦步骤,ObjectListView 将成为你最好的朋友。

ObjectListView 比普通的 ListView 活跃得多。普通的 ListView 本质上是被动的:它在那里,你戳它、刺激它,最终它看起来像你想要的样子。使用 ObjectListView,你告诉它你想做什么,ObjectListView 会为你完成。更正式地说:ObjectListView 是声明性使用的。你配置 ObjectListView,给它你的模型对象集合,然后 ObjectListView 为你构建 ListView

使用 ObjectListView 的关键部分是配置它。大部分配置都可以在 IDE 中通过设置 ObjectListView 本身或列表中使用的列的属性来完成。有些配置无法通过属性完成:这些更复杂的配置通过响应事件或安装委托来完成(稍后会详细介绍)。一旦列和控件配置完成,将其投入使用就很简单了,正如您已经看到的:只需调用一次 SetObjects()

小心 ListViewItems。您永远不需要向 ObjectListView 添加 ListViewItems。如果您发现自己正在向 Items 集合添加东西,创建 ListViewItems,或向任何东西添加子项,那么您需要停止——您正在被黑暗面诱惑。ObjectListView 会为您完成所有这些工作。它拥有 ListViewItems,并根据您提供的信息按需销毁、更改和构建它们。抵制添加/编辑/排序或以其他方式干扰 ListViewItems 的诱惑——它不会奏效。

也没有必要在 ListViewItem 中隐藏信息。旧式 ListView 编程通常需要在每个 ListViewItem 上附加某种键,以便当用户对某一行进行操作时,程序员可以知道该行与哪个模型对象相关联。这种附加通常通过创建一列或多列零宽度列,或者通过设置 ListViewItemTag 属性来完成。使用 ObjectListView,您不再需要这样做。ObjectListView 已经知道每行背后的模型对象。在许多情况下,程序员只需使用 SelectedObjects 属性来找出用户想要操作哪个模型对象。

2.3 增加复杂性:图片

SetObjects() 的一次调用固然很好,但实际应用程序需要的不仅仅是排序和分组。它们至少需要在第一列中显示一个小图像。

这个简单示例的第一个明显增强是在 ListView 中显示图像。为此,我们需要配置 ObjectListView,使其知道在每行旁边显示哪个图像。这通常无法在 IDE 中完成。通常,要显示的图像取决于正在显示的模型对象。要决定一个图像,我们需要一种更复杂的配置类型:安装一个委托

委托基本上是你给 ObjectListView 的一段代码,说:“当你需要做这个时,调用这段代码”,其中这个可以是几个任务中的任何一个。在这种情况下,我们安装一个 ImageGetter 委托,它告诉 ObjectListView:“当你需要为这个模型对象找出图像时,调用这段代码。”[如果“委托”这个词让你担心,就把它们想象成参数和返回类型可以验证的函数指针。如果这听不明白,那就继续阅读。通过一些例子,它(可能)会变得清晰。]

首先,您需要一个与 ImageGetterDelegate 签名匹配的方法:它必须接受一个 object 参数并返回一个 object。从 ImageGetter 委托返回的值用作 ObjectListViewSmallImageList 的索引。因此,ImageGetter 可以返回 stringint。(如果 ObjectListView 是所有者绘制的,ImageGetter 还可以返回 Image)。

一个有些轻佻的例子如下:

<span id="ArticleContent">object PersonColumnImageGetter (object rowObject) {
    // People whose names start with a vowel get a star,
    // otherwise the first half of the alphabet gets hearts
    // and the second half gets music
    Person p = (Person)rowObject;
    if ("AEIOU".Contains(p.Name.Substring(0, 1)))
        return 0; // star
    else if (p.Name.CompareTo("N") < 0)
        return 1; // heart
    else
        return 2; // music
};</span>

要安装它,您需要这样做:

<span id="ArticleContent">this.personColumn.ImageGetter = new
    ImageGetterDelegate(this.PersonColumnImageGetter);</span>

在VB中

<span id="ArticleContent">this.personColumn.ImageGetter = New
    ImageGetterDelegate(AddressOf PersonColumnImageGetter)</span>

.NET 2.0 增加了匿名委托的便利性(至少对于 C# 而言——VB 程序员仍然需要编写单独的函数)。在匿名 delegate 中,函数的代码是内联的,像这样:

<span id="ArticleContent">this.personColumn.ImageGetter = delegate (object rowObject) {
    Person p = (Person)rowObject;
    if ("AEIOU".Contains(p.Name.Substring(0, 1)))
        return 0; // star
    else if (p.Name.CompareTo("N") < 0)
        return 1; // heart
    else
        return 2; // music
};</span>

匿名委托可以避免您在类中添加许多小方法。但是,如果匿名委托开始变得太大,或者您发现自己逐字地将它们从一个地方复制到另一个地方,那么这是一个很好的迹象,表明您需要在模型类上添加一些新方法。

[v2.3] 如果您的模型类有一个属性可以返回应显示的图像的名称或索引,则无需安装委托。您可以将 ImageAspectName 属性设置为该属性的名称。但这跨越了模型和视图之间的界限,因此我不鼓励这种做法,只是指出它是可能的。

2.4 其他自定义设置

ObjectListView 结合使用事件和委托,以允许进一步、更复杂的自定义。以下所有内容都可以通过委托进行自定义:

  • 从行的模型对象中提取方面的方式:OLVColumn.AspectGetter。如果模型上没有提供您想要显示的数据的方法或属性,您可以安装一个 AspectGetter 委托来计算信息。
  • 将方面转换为 string 的方式:OLVColumn.AspectToStringConverter。如果没有安装委托,则使用 ToString() 方法完成。委托可以随心所欲地执行操作。
  • 编辑后的值存回模型对象的方式:OLVColumn.AspectPutter
  • 计算此行的组键的方式:OLVColumn.GroupKeyGetter。组键只是一个用于将所有模型对象分区为组的值。所有具有相同组键的模型对象都被放入同一个组中。
  • 将组键转换为组标题的方式:OLVColumn.GroupKeyToTitleConverter
  • 计算和存储复选框的方式:OLVColumn.CheckStateGetterCheckStatePutter

ObjectListView 提供了许多事件,允许程序员自定义其行为。请查看 IDE 中可用的事件。一些常用事件包括:

  • 当每行和每个单元格添加到控件时,会触发 FormatRowFormatCell 事件。这些事件让程序员有机会根据需要格式化行或单元格。
  • CellToolTipShowingHeaderToolTipShowing 事件允许配置单元格或标题的工具提示。
  • SelectionChanged 事件在用户选择一行时仅触发一次(相比之下,当用户选择新项目时,标准 SelectedIndexChanged 事件可能会触发数百次)。

演示项目中的复杂示例选项卡包含了使用所有这些委托的示例。例如,打开“显示组”并单击“烹饪技能”或“小时费率”列以查看可能实现的功能。

2.5 数据无关

该控件被设计为数据无关。它不关心它正在处理什么类型的数据。唯一的要求是传递给 SetObjects 方法的对象必须支持 IEnumerable 接口,这并不是一个太严格的要求。这意味着它可以同样好地与 ArrayListDataTable 或从 CompilerResults.Errors 返回的编译器错误列表一起工作。例如,要从 DataTable 显示信息,您可以像这样安装 AspectGetter 委托:

<span id="ArticleContent"><span>columnName.AspectGetter =
    delegate(object row) { return ((DataRow)row)["Name"]; };
columnCompany.AspectGetter =
    delegate(object row) { return ((DataRow)row)["Company"]; };
columnAge.AspectGetter =
    delegate(object row) { return ((DataRow)row)["Age"]; };</span></span>

然后像这样安装表

<span id="ArticleContent"><span>this.objectListView1.SetObjects(ds1.Tables["Persons"].Rows);</span></span>

请注意,此代码安装的是 Rows,而不是 DataTable 本身。

实际上,您不必为此定义 AspectGetters。只需将 columnName.AspectName 设置为“Name”,ObjectListView 就能从 DataRow 中提取索引属性。但该示例仍然有效。

3. ObjectListView 的风味

3.1 更了解数据 — DataListViews

应公众强烈要求——好吧,有几个人要求——有一个 DataListView 类。这是一个可数据绑定的 ObjectListView 版本,它接受各种数据源,并用该数据源的数据填充列表。

对于那些觉得一行代码都太多的程序员来说,DataListView 可以完全在 IDE 中配置。给它一个 DataSource,设置好列,它就能正常工作。DataListView 还会监听其数据源上的 ListChanged 事件,并利用这些事件使其列表与数据源的内容保持同步。向数据表添加新行,它将自动出现在 ListView 中!在 DataListView 中编辑一个值,更改会自动出现在 DataTable 中!

DataListView 可以接受多种类型的对象作为数据源:

  • 数据视图
  • DataTable
  • DataSet
  • DataViewManager
  • 绑定源

要使用 DataListView,您需要将每个列的数据列名赋予 AspectName 属性中,然后将 DataSource 成员设置为您的数据源。就是这样!更加懒惰!所以,我们可以通过为列配置 AspectNames,然后直接设置数据表来完成与上述“数据无关”示例相同的事情,如下所示:

<span id="ArticleContent"><span>this.dataListView1.DataSource = ds1.Tables["Persons"];</span></span>

或者,对于极其懒惰的人,整个过程可以通过 IDE 配置完成。在列上设置 AspectName 属性;将 DataSource 属性设置为适当的数据集,然后设置 DataMember 属性以指定要显示数据集中哪个表。瞧,一个功能齐全的数据集查看器。所有这些都无需编写一行代码。

[2.5] 现在有 FastDataListView,它结合了 DataListView 的易用性和 FastObjectListView 的速度。它基本上通过虚拟列表管理数据集。在我的中端笔记本电脑上,它可以轻松处理 100,000+ 行的数据集。

3.2 你想在那个 ListView 中有多少行?! — VirtualObjectListView

如果你曾经想用 1000 万行数据彻底淹没你的用户,那么就用 VirtualObjectListView 把他们打倒吧。

首先,让我们澄清一点:ListViews 不是处理大量项目的好接口。根本就没有。如果你试图显示一个有 1000 万行的 ListView,你需要重新思考你的界面。然而,如果你真的必须使用这么多行的 ListView,那么 VirtualObjectListView 就是你的答案。

通常 ObjectListView 会保留一个模型对象列表,可以随意读取、排序或分组。而 VirtualObjectListView 不会保留这样的列表。相反,它只在需要显示时才获取模型对象。对于大型列表,这极大地减少了资源消耗。如果用户从不查看第 400 万行,VirtualObjectListView 就永远不会请求它,因此程序也永远不必创建它。

要使用 VirtualObjectListView,您必须实现 IVirtualListDataSource 并将该数据源提供给虚拟列表(通过 VirtualListDataSource 属性)。使用该接口,虚拟列表就可以像功能齐全的 ObjectListView 一样运行。虚拟列表唯一不能做的是:它不能显示组,也不能使用平铺视图。但除此之外,它们应该以与普通 ObjectListView 相同的方式运行,包括排序、复选框和通过键入进行搜索。

如果你不想实现 IVirtualListDataSource 的所有方法,你可以继承 AbstractVirtualListDataSource,它是一个该接口的“什么都不做”实现。至少,你必须实现 GetObjectCount()GetNthObject(),否则列表中将什么都不会显示。

尽管没有文档说明,.NET 虚拟 ListView 不能有复选框。VirtualObjectListView 绕过了这个限制,但您必须使用 ObjectListView 提供的函数:CheckedObjectsCheckObject()UncheckObject() 及其相关函数。如果您使用正常的复选框属性(CheckedItemsCheckedIndicies),它们将抛出异常,因为 ListView 处于虚拟模式,而 .NET“知道”它无法在虚拟模式下处理复选框。

3.3 更快的 ListView — FastObjectListView

到目前为止,ObjectListView 一直在迎合懒惰的人,那些我们想要做最少工作并获得最大结果的人。如果急躁是你的主要性格缺陷,那么 FastObjectListView 就是为你准备的。通过牺牲一些功能,你将获得极大的速度。

它有多快?在我的低端笔记本电脑上,一个普通的 ObjectListView 构建一个包含 10,000 个对象的列表大约需要 10 秒。而 FastObjectListView 构建相同的列表则不到 0.1 秒。

你失去了什么?使用 FastObjectListView,你不能使用平铺视图,如果你在 XP 上,你不能显示组。除此之外,ObjectListView 的所有功能在 FastObjectListView 中都可用。从 v2.3 开始,在 Vista 或更高版本上运行时,FastObjectListViews 可以显示组。只需将 ShowGroups 设置为 true,控件将以与普通 ObjectListView 相同的方式处理组。

3.4 树形拥抱 — TreeListView

有时,您需要显示树形结构(如 TreeView),但您还想显示比仅显示名称更多的项目信息(如 ListView)。此时 TreeListView 就派上用场了。它显示树形结构,具有漂亮的展开和折叠功能,同时还以列的形式显示信息:

Screenshot - TreeListView in action

像所有其他 ObjectListViews 一样,TreeListView 依赖于委托。使用 TreeListView 的两个基本委托是:

  1. 判断给定模型是否可以展开:CanExpandGetter 委托。

  2. 在模型展开时获取其子项:ChildrenGetter 委托。

在演示中,有一个类似 Explorer 的示例,用于导航本地计算机上的磁盘。该演示中的树列表视图配置如下:

<span id="ArticleContent"><span>this.treeListView.CanExpandGetter = delegate(object x) {
    return (x is DirectoryInfo);
};
 
 
this.treeListView.ChildrenGetter = delegate(object x) {
    DirectoryInfo dir = (DirectoryInfo)x;
    return new ArrayList(dir.GetFileSystemInfos());
}; </span></span>

在此示例中,CanExpandGetter 委托确保只有目录可以展开。

ChildrenGetter 委托在目录展开时返回该目录的内容。ChildrenGetter 委托仅在 CanExpandGetter 返回 true 时才会被调用。因此,在这种情况下,ChildrenGetter 委托知道参数 x 必须是 DirectoryInfo 实例。

要使其工作,您必须添加一些“根”(顶级对象)。您可以通过将 Roots 属性设置为模型对象集合来完成此操作,或者像往常一样调用 SetObjects()。在 TreeListView 上,SetObjects()AddObject()RemoveObject() 都适用于根集合。

要刷新模型下的子列表,请在其父级上调用 RefreshObject()

TreeListView 在分支和叶节点之间存在显著的信息重叠时效果最佳。它们必须共享相同的列。当分支除了名称之外没有其他信息,并且列只用于叶节点时,它们也同样适用。但是,当您想显示有关分支的几条信息和有关叶节点的其他信息,并且两者之间重叠很少时,它们的效果就没那么好。由于所有空单元格,它们看起来很傻。

3.5 消除类型转换 — TypedObjectListView

ObjectListView 的一个烦恼是所有必需的类型转换。由于 ObjectListView 不对您将使用的模型对象类型做出任何假设,它将所有模型都视为 object,并且当您需要时,由您将其转换为正确的类型。这导致许多委托以这样的类型转换开头:

<span id="ArticleContent"><span>this.objectListView1.SomeDelegate = delegate(object x) {
    MyModelObject model = (MyModelObject)x;
    ...
}</span></span>

过了一段时间就会变得厌烦。如果你能告诉 ObjectListView 它总是会显示,比如说,Person 对象,那会很好。就像这样:

<span id="ArticleContent"><span>this.objectListView1 = new ObjectListView<Person>();
this.objectListView1.SomeDelegate = delegate(Person model) {
    ...
}</span></span>

不幸的是,Visual Studio 中的设计器无法处理这种参数化控件。[我记得在 Microsoft 博客上某处读到过,但我找不到了。有几位知识渊博的人说它不能实现——例如这里。如果有人知道这是否是一个有文档记录的决定,请告诉我。] 有几个技巧可以解决一些最明显的问题,但它们都卡在了代码生成上。

所以,在此期间,我们现在有了 TypedObjectListView 类。这不是另一个 ObjectListView 子类,而是一个现有 ObjectListView 的类型化包装器。要使用它,您像往常一样在 IDE 中创建一个 ObjectListView。当需要实现您的委托时,您在您的列表视图周围创建一个 TypedObjectListView 包装器,并针对该包装器声明您的委托。它比解释起来更容易使用,所以请看这个例子:

<span id="ArticleContent"><span>TypedObjectListView<Person> tlist = new TypedObjectListView<Person>(this.listViewSimple);
tlist.BooleanCheckStateGetter = delegate(Person x) {
    return x.IsActive;
};
tlist.BooleanCheckStatePutter = delegate(Person x, bool newValue) {
    x.IsActive = newValue;
    return newValue;
}; </span></span>

看哪,妈妈!没有类型转换!委托是针对类型化包装器声明的,它知道正在使用什么模型对象。

您还可以使用 TypedObjectListView 对列上的委托进行类型化访问:

<span id="ArticleContent"><span>tlist.GetColumn(0).AspectGetter = delegate(Person x) { return x.Name; };
tlist.GetColumn(1).AspectGetter = delegate(Person x) { return x.Occupation; }; </span></span>

如果您不喜欢通过索引引用列,您可以在给定的 ColumnHeader 对象周围创建 TypedColumn 对象:

<span id="ArticleContent"><span>TypedColumn<Person> tcol = new TypedColumn<Person>(this.columnHeader16);
tcol.AspectGetter = delegate(Person x) { return x.GetRate(); };
tcol.AspectPutter = delegate(Person x, object newValue) { x.SetRate((double)newValue); }; </span></span>

TypedObjectListView 的最后一个特性是它可以根据其 AspectName 自动为列生成 AspectGetter。因此,我们不必像上面那样手动编写 AspectGetters,您只需在 IDE 中配置 AspectName,然后调用 tlist.GenerateAspectGetters()。这可以(应该?)处理任意复杂度的方面,例如“Parent.HomeAddress.Phone.AreaCode”。

4. 其他功能

4.1 固定宽度和限制宽度的列

有时,允许用户调整列大小毫无意义。一个只显示 16x16 状态图标的列没有理由可调整大小。将这个想法更进一步,您可以想象在某些情况下,列不应小于给定大小或宽于最大大小。因此,如果我们可以为列提供最小和最大宽度,那会很好。将两者设置为相同的值将得到固定大小的列。

然而,控制列的调整大小被证明是一个不平凡的问题。很容易找到在 ListView 中固定所有列宽度的示例:Chris Morgan 在这里提供了一个不错的实现。不幸的是,该技术不能用于限制单个列的宽度。事实上,我在任何地方都找不到如何将列宽度限制在给定范围内的示例。

无论如何,列可以被赋予 MinimumWidthMaximumWidth。即使在 IDE 中,这些设置也将阻止用户将列的宽度设置为超出给定值。请参阅下文,以更详细地讨论我的实现的复杂性和潜在限制。

4.2 自动调整大小的列

在某些情况下,如果一列(通常是最右侧的列)能够随着 listview 的扩展而扩展,以便尽可能多地显示该列而无需水平滚动(您绝不应该让您的用户进行任何水平滚动!),那会很不错。自由空间填充列正是实现了这一点。演示的“简单”选项卡中的“注释”列展示了这一点。

ObjectListView 调整大小时,所有固定宽度列占用的空间将被汇总。然后,该总和与控件宽度之间的差值将在自由空间填充列之间共享。如果您只有一列,它将获得所有空间;如果您有两列,每列将获得一半的空间。

对这些填充空间的列要小心。它们的行为不是标准的,有时会非常令人惊讶,特别是当这些列不是最右侧的列时。一个令人惊讶的地方是这些列不能通过拖动分隔线来调整大小——它们的尺寸取决于 ListView 中可用的自由空间。

4.3 从 ListViews 生成报告

Example of ListViewPrinter output

既然您已经做了所有这些工作来制作一个非常漂亮的 ListView,如果能够直接打印它,难道不是很好吗?是的,我知道总有 PrntScrn 键,但我注意到有些高级管理人员并不认为这是一个很好的报告解决方案。

ListViewPrinter 是您打印问题的答案。在 IDE 中配置它的实例(ListView 属性控制打印哪个列表),然后调用:

<span id="ArticleContent"><span>this.listViewPrinter1.PrintPreview();</span></span>

因此,您可以免费获得一份非常漂亮的报告,就像这样。

诚然,此示例中的格式有点过多,但您可以根据自己的喜好修改所有格式。请参阅演示以获取一些更平淡的示例,并阅读代码以了解如何使其工作。它确实非常酷。

这是一个逻辑上独立的代码段,因此它位于自己的项目中。如果您想使用它,您需要将 ListViewPrinter 项目本身或 ListViewPrinter.dll 文件添加到您的项目中。该过程与上面第一步部分中给出的 ObjectListView 项目相同。

4.4 单元格编辑

ListViews 通常用于显示信息。标准的 ListView 允许编辑第 0 列(主单元格)的值,但除此之外的都不行。ObjectListView 允许所有单元格都被编辑。根据单元格的数据来源方式,编辑后的值可以自动写回模型对象。

ObjectListView 的“可编辑性”由 CellEditActivation 属性控制。该属性可以设置为以下值之一:

  • CellEditActivateMode.None - ObjectListView 不可编辑(这是默认值)。
  • CellEditActivateMode.SingleClick - 当用户单击子项单元格时,将编辑子项单元格。单击主单元格不会开始编辑操作 - 它会选择该行,就像正常情况一样。编辑主单元格仅在用户按下 F2 时开始。
  • CellEditActivateMode.DoubleClick - 双击任何单元格,包括主单元格,将开始编辑操作。此外,按 F2 将开始对主单元格的编辑操作。
  • CellEditActivateMode.F2Only - 按 F2 开始对主单元格的编辑操作。单击或双击子项单元格不执行任何操作。

可以通过 IsEditable 属性将单个列标记为可编辑(默认值为 true),尽管这只有在 ObjectListView 本身可编辑后才有意义。如果您知道不应允许用户更改特定列中的单元格,请将 IsEditable 设置为 false。但请注意,这可能会造成一些 UI 上的意外(导致诸如“为什么我不能像编辑其他所有单元格一样通过单击它来编辑这个值?”之类的抱怨)。您已被警告。

单元格编辑器激活后,将应用正常的编辑约定:

  • Enter 或 Return 结束编辑并将新值提交给模型对象。
  • Escape 取消编辑。
  • Tab 提交当前编辑,并在下一个可编辑单元格上开始新的编辑。Shift-Tab 编辑上一个可编辑单元格。

4.4.1 单元格的编辑方式以及如何自定义

默认处理会根据单元格中的数据类型创建单元格编辑器。它可以处理 boolintstringDateTimefloatdouble 数据类型。当用户完成单元格中的值编辑后,新值将被写回模型对象(如果可能)。

要执行默认处理之外的操作,您可以监听两个事件:CellEditStartingCellEditFinishing

CellEditStarting 事件在用户请求编辑单元格之后但在单元格编辑器显示在屏幕之前触发。此事件将 CellEditEventArgs 对象传递给事件处理程序。在此事件的处理程序中,如果您将 e.Cancel 设置为 true,则单元格编辑操作将不会开始。如果您不取消编辑操作,您几乎肯定会想要使用 CellEditEventArgsControl 属性。您可以使用它来自定义默认编辑器,或完全替换它。

例如,如果您的 ObjectListView 在单元格中显示 Color,则没有默认编辑器来处理 Color。您可以制作自己的 ColorCellEditor,正确设置它,然后将 Control 属性设置为您的颜色单元格编辑器。然后 ObjectListView 将使用该控件而不是默认控件。如果您的单元格编辑器有一个名为 Value 的读/写属性,ObjectListView 将使用它来获取和设置控件中的单元格值。如果没有,将使用 Text 属性。

当用户想要完成编辑操作时,会触发 CellEditFinishing 事件。如果用户已取消编辑(例如通过按 Escape 键),则 Cancel 属性将已设置为 true。在这种情况下,您应该只进行清理而不更新任何模型对象。如果用户尚未取消编辑,您可以通过将 Cancel 设置为 true 来取消编辑——这将强制 ObjectListView 忽略用户输入到单元格编辑器中的任何值。

不用猜,您可以引用 Control 属性来提取用户输入的值,然后使用该值做任何他或她想做的事情。在此事件期间,您还应该撤销在 CellEditStarting 事件中设置的任何事件监听。

您可以监听 CellEditValidating 事件来阻止单元格编辑操作完成(例如,如果用户输入的值不可接受)。如果此事件的处理程序将 Cancel 设置为 true,则编辑操作将不会完成,并且编辑器将保留在屏幕上。请确保您已向用户清楚地说明编辑操作为何未完成。

您可以在演示中查看 listViewComplex_CellEditStarting()listViewComplex_CellEditFinishing(),以了解如何处理这些事件的示例。

4.4.2 更新模型对象

一旦用户在单元格中输入新值并按下 Enter 键,ObjectListView 就会尝试将修改后的值存储回模型对象。

这可以通过三种方式实现:

  • 您为 CellEditFinishing 事件创建一个事件处理程序,编写代码从控件获取修改后的值,将该新值放入模型对象中,并将 Cancel 设置为 true,以便 ObjectListView 知道它不需要执行任何其他操作。您还需要至少调用 RefreshItem()RefreshObject(),以便模型对象的更改显示在 ObjectListView 中。在某些情况下这是必要的,但作为一般解决方案,它不符合我的懒惰哲学。
  • 您为相应的 OLVColumn 提供一个 AspectPutter delegate。如果提供,此回调将使用模型对象和用户输入的新值进行调用。这是一个简洁的解决方案。
  • 如果列的 AspectName 是可写属性的名称,ObjectListView 将尝试将新值写入该属性。这不需要编码,无疑是最懒惰的解决方案。但它仅在 AspectName 包含可写属性的名称时才有效。如果 AspectName 是点分隔的(例如 Owner.Address.Postcode),则只有最后一个属性需要可写。

如果这三件事都没有发生,用户的编辑将被丢弃。用户将在单元格编辑器中输入他或她的新值,按下 Enter 键,然后仍将显示旧值。如果似乎用户无法更新单元格,请检查以确保上述三件事之一正在发生。

单元格编辑的所有方面都在此页面上详细描述。

4.5 (所有者)绘制和分割

还记得我不想打开的那个潘多拉盒子吗?所有者绘制 ListView?嗯,有一天下午,当我无事可做(哈!),我决定这真的不会太糟糕,我拿出了我的开罐器。几个晚上后,我只能证实我最初的估计:所有者绘制是一个潘多拉盒子。它应该很容易。它应该能正常工作。但它没有。

无论如何,ObjectListViews 现在可以由所有者绘制,而且是打了兴奋剂的所有者绘制!像大多数 ObjectListView 一样,所有者绘制是通过安装一个 delegate 来实现的。在渲染器 delegate 内部,您可以绘制任何您喜欢的东西:

<span id="ArticleContent"><span>columnOD.RendererDelegate = delegate(DrawListViewSubItemEventArgs e,
    Graphics g, Rectangle r, Object rowObject) {
    g.FillRectangle(new SolidBrush(Color.Red), r);
    g.DrawString(((Person)rowObject).Name, objectListView1.Font,
        new SolidBrush(Color.Black), r.X, r.Y);
}</span></span>

安装委托工作正常,但在此委托中有许多实用的实用方法。该行当前是否被选中?背景应该是什么颜色?BaseRenderer 类封装了这些实用程序。要制作您自己的 Renderer 类,您必须继承 BaseRenderer,覆盖 Render(Graphics g, Rectangle r) 方法,并再次绘制您喜欢的任何内容,只是这次您有许多漂亮的实用方法可供使用。目前有几个 BaseRenderer 的子类可用。

目的 示例
条形渲染器 这是一个简单的水平条。行的数据值用于按比例填充“进度条”。
多图像渲染器 此渲染器根据行的数据值绘制 0 个或更多图像。iTunes 上 5 星“我的评分”列是此类型渲染器的一个示例。
映射图像渲染器 此渲染器绘制一个根据行数据值决定的图像。每个数据值都有自己的图像。一个简单的例子是布尔渲染器,它为 true 绘制一个勾号,为 false 绘制一个叉号。此渲染器也适用于 enum 或特定域代码。
图像渲染器 此渲染器尝试将其行的数据值解释为图像或图像集合。通常,如果您的数据库中存储了 Image,您将使用此渲染器来绘制这些图像。如果单元格数据值是包含 stringintImageICollection,则所有这些图像都将被绘制。
标志渲染器 此渲染器在其单元格内绘制 0 个或更多图像。单元格数据值应为位标志集合,指示应绘制哪些图像。请参阅演示以获取使用示例。

要使用这些渲染器中的任何一个或您自己的自定义子类,您需要将它们的实例分配给列的 Renderer 属性,如下所示:

<span id="ArticleContent"><span>colCookingSkill.Renderer = new MultiImageRenderer(Resource1.star16, 5, 0, 40);</span></span>

这意味着烹饪技能列将根据数据值绘制最多 5 个 star16 图像。渲染器期望数据值在 0 到 40 的范围内。值为 0 或更小将不绘制任何星星。值为 40 或更大将绘制 5 颗星星。介于两者之间的值将绘制按比例的星星数量。

从 v2.0 开始,Renderers 现在是 Components,这意味着它们可以在 IDE 中创建和操作。因此,要使用像上面这样的 MultiImageRenderer,您可以在 IDE 中创建一个,配置其属性,然后将其分配给列的 Renderer 属性。

4.5.1 关于所有者绘制的注意事项

仅在开启 OwnerDrawn 模式时才进行所有者绘制。因此,只有在 ObjectListView 处于所有者绘制模式时才能看到您的自定义渲染器。

列表视图中的行始终是固定高度,并根据 ListView 字体和/或图像列表的高度计算。行高可以使用 RowHeight 属性设置。您不能拥有不同高度的行——使用 ListView 根本无法做到这一点。

显而易见但容易被忽视的是,所有者绘制比非所有者绘制慢。所有者绘制需要比原生绘制更多的工作。同样,对于小型列表,差异并不显著。然而,当需要大量重绘时,它可能会很明显。例如,转到演示中的“虚拟列表”选项卡,然后将滚动条拖到底部。现在打开 OwnerDraw 并再次执行此操作。差别很大!

4.6 消除拖放操作中的拖沓

从 v2.2 开始,ObjectListView 现在对拖放操作具有相当完善的支持。

4.6.1 将 ObjectListView 用作拖动源

如果您希望用户能够将行从 ObjectListView 中拖出,您需要设置 DragSource 属性。此属性接受实现 IDragSource 接口的对象。通常,使用 SimpleDragSource 的实例就足够了:

<span id="ArticleContent"><span>this.objectListView1.DragSource = new SimpleDragSource();</span></span>

此拖动源会记住当前选定的行,并为拖动数据对象提供这些行的文本和 HTML 版本。有了这个简单的拖动源,您可以从 ObjectListView 中选择 10 行并将它们拖到 Microsoft Word 中,以创建这些行的格式化表格。您还可以将行拖到其他 ObjectListViews 中,这通常是您想要的。

在 IDE 中,您可以将 IsSimpleDragSource 设置为 true,以使用 SimpleDragSource 将您的 ObjectListView 变为拖动源。

4.6.2 将 ObjectListView 用作放置接收器

接受来自其他源的放置操作有点复杂,但处理方式类似。如果您希望用户能够将内容拖放到 ObjectListView 上,您需要设置 DropSink 属性。此属性接受实现 IDropSink 接口的对象。在许多情况下,您将使用 SimpleDropSink 的实例。

<span id="ArticleContent"><span>this.objectListView1.DropSink = new SimpleDropSink();</span></span>

在 IDE 中,您可以将 IsSimpleDropSink 设置为 true,以使您的 ObjectListView 成为一个放置接收器。DragSource 不需要进一步的信息,但 DropSink 至少需要知道另外两件事:

  1. 当前拖动的对象是否可以放置在当前位置?
  2. 如果对象被拖放,接下来应该发生什么?

如果您使用 SimpleDropSinkObjectListView 将触发两个事件来处理这些情况:CanDrop 事件和 Dropped 事件。要真正有用,您需要处理这些事件。您可以像往常一样在 IDE 中为这些事件设置处理程序。

您可以选择监听 ModelCanDropModelDropped 事件。当拖动源是另一个 ObjectListView 时,会触发第二对事件。这些事件的工作方式与 CanDropDropped 事件相同,只是参数块包含更多信息:

  • 启动拖动的 ObjectListView
  • 正在拖动的模型对象
  • 放置操作的当前目标模型对象

SimpleDropSink 实际上相当复杂。它可以以多种方式配置。请查看代码以了解更多选项。

DropSink 配置选项

允许在背景上放置

<span id="ArticleContent"><span>myDropSink.CanDropBetween = true;</span></span>

允许在项目之间放置

<span id="ArticleContent"><span>myDropSink.CanDropBetween = true;</span></span>

你甚至可以放到子项上

<span id="ArticleContent"><span>myDropSink.CanDropOnSubItems = true;</span></span>

并改变高亮颜色以示不同

<span id="ArticleContent"><span>myDropSink.FeedbackColor = Color.IndianRed;</span></span>

您可以在此页面上了解更多关于拖放的信息,包括如何从头开始编写自己的拖放接收器。

4.7 可折叠分组

最常请求的功能是可折叠组。我想要,你想要,你住在卧龙岗的曾姑母米尔德里德也想要。不幸的是,对于当前的 ListView,这根本不可能:在 XP 上,组无法折叠。但在 Vista 上,这个最常请求的功能成为了现实。它默认启用,因此在 Vista 下,组会自动可折叠。如果您不希望您的组可折叠,请将 HasCollapsibleGroups 设置为 false。感谢 Crustyapplesniffer 实现了此功能。

4.8 增强版分组

在 v2.3 中,组进行了重大改造。不再仅仅满足于可折叠,组现在可以拥有标题图像、副标题、任务(右侧可点击的链接)和页脚。如果做得好,这确实可以让您的列表视图看起来非常漂亮:

Groups with images, tasks and subtitles

这种扩展格式可以在 AboutToCreateGroup 事件期间设置。或者,您可以使用 MakeGroupies() 方法的扩展版本,它允许配置所有这些新属性。上面的截图是通过一次 MakeGroupies() 调用配置的:

<span id="ArticleContent"><span>this.columnCookingSkill.MakeGroupies(
    new object[]{10, 20, 30, 40},
    new string[] {"Pay to eat out", "Suggest take-away", "Passable", "Seek dinner invitation", "Hire as chef"},
    new string[] { "emptytoast", "hamburger", "toast", "dinnerplate", "chef" },
    new string[] { 
        "Pay good money -- or flee the house -- rather than eat their homecooked food", 
        "Offer to buy takeaway rather than risk what may appear on your plate", 
        "Neither spectacular nor dangerous", 
        "Try to visit at dinner time to wrangle an invitation to dinner", 
        "Do whatever is necessary to procure their services" },
    new string[] { "Call 911", "Phone PizzaHut", "", "Open calendar", "Check bank balance" }
);</span></span>

这些分组格式功能仅在 Vista 及更高版本上可用。在 XP 上,分组只能有标题。

4.9 可自定义的“列表为空”消息

ObjectListView 为空时,它可以显示一个“此列表为空”类型的消息。EmptyListMsg 属性包含当 ObjectListView 为空时出现的字符串。此字符串使用 EmptyListMsgFont 渲染。这两个属性都可以在 IDE 中配置。

但如果你想写一点代码,你可以得到更有趣的消息。空列表消息实际上是作为叠加实现的。你可以通过 EmptyListMsgOverlay 属性访问这个叠加。默认情况下,这是一个 TextOverlay,你可以随心所欲地自定义它:

<span id="ArticleContent"><span>TextOverlay textOverlay = this.objectListView1.EmptyListMsgOverlay as TextOverlay;
textOverlay.TextColor = Color.Firebrick;
textOverlay.BackColor = Color.AntiqueWhite;
textOverlay.BorderColor = Color.DarkRed;
textOverlay.BorderWidth = 4.0f;
textOverlay.Font = new Font("Chiller", 36);
textOverlay.Rotation = -5;</span></span>

这样做会显示这样的消息:

Fancy empty list message

4.10 超链接

ObjectListViews 现在可以将单元格视为超链接。为此,将 ObjectListViewUseHyperlinks 设置为 true,然后将 OLVColumnHyperlink 属性设置为 true,使该列中的所有单元格都表现为超链接。

如果您不想所有单元格都成为超链接,您可以监听 IsHyperlink 事件(在上面的截图中,以“s”开头的职业不是超链接)。在此事件中,您可以指定将附加到该单元格的 URL。默认情况下,URL 是单元格的文本。如果将 URL 设置为 null,则该单元格将不被视为超链接。如果您已经监听了 FormatCell,您也可以在该事件中设置 URL。

超链接的格式由 ObjectListViewHyperlinkStyle 属性控制。您可以在 IDE 中创建和配置 HyperLinkStyle,然后将其分配给您的 ObjectListView。相同的样式可以分配给多个 ObjectListView。在 95% 的情况下,默认样式就足够了。

当超链接被点击时,会触发一个 HyperlinkClicked 事件。如果您自己处理此事件,请将 Handled 设置为 true 以阻止默认处理发生。如果您不处理它,默认处理将尝试打开关联的 URL。

请注意不要将第 0 列设置为超链接。如果是这样,每次用户尝试选择一行时单击它,都会打开一个浏览器窗口,这很快就会变得烦人。

4.11 标题格式

4.11.1 标题样式

在 v2.4 中,ObjectListView 的标题变得完全可样式化。如果 ObjectListView.HeaderUsesThemetrue(默认值),标题将根据操作系统的当前主题绘制,并忽略任何标题样式。如果此为 false,则标题将根据其给定的标题格式样式进行格式化。

您可以一次为所有列设置样式(通过 ObjectListView.HeaderFormatStyle)或仅为一列设置样式(通过 OLVColumn.HeaderFormatStyle)。赋予特定列的样式优先于赋予整个控件的样式。与其他样式一样,HeaderFormatStyles 可以在 IDE 中创建、配置和分配。

标题样式允许标题在不同状态下呈现不同的外观:

  • Normal 控制标题在没有其他事件发生时的外观。
  • Hot 控制当鼠标悬停在标题上时标题的外观。这应该与正常状态略有不同,但仍然明显。
  • Pressed 控制当用户在标题上按下鼠标按钮但尚未释放按钮时标题的外观。这应该与正常和热状态都有明显的视觉变化。

对于每种状态,标题格式允许指定字体、字体颜色、背景颜色和边框。如果这些属性组合不当,您可能会制作出一些真正糟糕的设计,但如果运用得当,效果可能会令人愉悦。

标题格式样式
Scheme 正常 按压

XP 主题

酷之家

4.11.2 标题自动换行

ObjectListView 上还有一个 HeaderWordWrap,它允许标题中的文本自动换行。因此,如果您有点虐待狂,您可以给用户带来这样的体验:

Word wrapped headers with different colors and fonts

4.11.3 垂直文本

如果您的列很窄,并且想节省一些水平空间,您可以将 OLVColumn.IsHeaderVertical 设置为 true,然后标题文本将垂直绘制:

Vertical column header

4.11.4 标题图片

.NET 的 ColumnHeader 类具有 ImageIndexIndexKey 属性。然而,没有人知道具体原因,因为您在 IDE 中设置的任何值都不会被代码生成器持久化。

然而,ObjectListView 具有 HeaderImageKey 属性,它允许您选择将在列标题中显示的图像。

Column header with image

4.12. 无 IDE 生成 ObjectListView

良好设计的一个基本原则是将呈现与模型分离。模型类不应知道它们如何呈现给用户。

但在某些开发情况下,开发速度至关重要(商业银行和股票经纪人似乎经常属于这种情况)。在这种情况下,将某种用户界面放入模型类本身是一种可接受的权衡。

正是为了迎合这种开发,ObjectListView 现在拥有 Generator 类和 OLVColumn 属性。这些类的想法是,在您的模型类中,您决定希望哪些属性出现在 ObjectListView 中,然后为这些属性赋予 OLVColumn 属性。在这些属性中,您指定一些您通常通过 IDE 提供的特性(例如,列标题、对齐方式、图像获取器、格式字符串)。然后,当您准备好显示模型列表时,您从模型生成列,然后显示模型:

<span id="ArticleContent">List<ForexPurchase> purchases = this.GetForexPurchasesToShow();  
Generator.GenerateColumns(this.olv, purchases);  
this.olv.Objects = purchases;  </span>

在此示例中,this.olv 是一个完全未配置的 ObjectListView。在 IDE 中,除了将其放置在窗体上之外,没有对其进行任何操作:没有创建或配置任何列。Generator 使用 OLVColumn 属性中提供的信息来构建一个功能齐全的 ObjectListView

当用户稍后想查看今天的外汇销售时,她点击“销售”按钮,可能会执行这样的代码:

<span id="ArticleContent">List<ForexSale> sales = this.GetForexSalesToShow();  
Generator.GenerateColumns(this.olv, sales);  
this.olv.Objects = sales;  </span>

这重用了同一个 ObjectListView 控件,但现在它是一个功能齐全的 ObjectListView,显示外汇销售信息。

[感谢 John Kohler 提供此想法和原始实现]

4.13 过滤

在 v2.4 中,ObjectListViews 变得可过滤。为了向后兼容,此功能默认关闭。将 UseFiltering 设置为 true 以启用此功能。

如果您设置 ModelFilterListFilter 属性,则列表中只会显示与这些过滤器匹配的模型对象。列表过滤器对整个列表应用某些条件。尾部过滤器(例如,仅显示最后 500 行)是整个列表过滤器的很好示例(这由 TailFilter 类实现)。模型过滤器依次考虑每个模型对象,并决定该模型是否应包含在向用户显示的列表中。模型过滤器是最常用的。

ModelFilter 提供了一个非常有用的过滤实现。它将一个委托作为构造参数,该委托决定是否应包含给定的模型对象。要过滤电话列表以仅显示紧急呼叫,您可以像这样安装过滤器:

<span id="ArticleContent">this.olv1.ModelFilter = new ModelFilter(delegate(object x) { 
    return ((PhoneCall)x).IsEmergency; 
});</span>

当然,您可以通过实现 IModelFilterIListFilter 接口来制作自己的过滤器。例如,您可以像这样只显示紧急呼叫:

<span id="ArticleContent"><span>public class OnlyEmergenciesFilter : IModelFilter
{
    public bool Filter(object modelObject) {
        return ((PhoneCall)x).IsEmergency; 
    }
}
...
this.olv1.ModelFilter = new OnlyEmergenciesFilter();</span></span>

4.13.1 文本过滤

一个非常常见的过滤任务是只显示包含特定字符串的行。iTunes 通过其“搜索”框实现了这一点。ObjectListView 通过 TextMatchFilter 使实现此文本过滤变得非常容易。您这样使用它:

<span id="ArticleContent">this.olv1.ModelFilter = new TextMatchFilter(this.olv1, "search");</span>

执行此行后,ObjectListView 将只显示该行中至少有一个单元格包含文本“搜索”的行。

过滤器可以通过设置 Columns 属性来配置,使其仅考虑 ObjectListView 中的某些列。这对于避免在已知会返回无意义结果的列(如复选框或仅图像列)上进行搜索非常有用。

它还可以设置为执行正则表达式搜索或简单前缀匹配:

this.olv1.ModelFilter = new TextMatchFilter(this.olv1, "^[0-9]+", TextMatchFilter.MatchKind.Regex);

作为额外奖励,如果您的过滤 ObjectListView 是所有者绘制的,您可以将此文本搜索与一个特殊的渲染器 HighlightTextRenderer 结合使用。此渲染器会在任何匹配其给定文本的子字符串周围绘制一个高亮框。因此:

<span>TextMatchFilter filter = new TextMatchFilter(this.olv1, "er");
this.olv1.ModelFilter = filter;
this.olv1.DefaultRenderer = new HighlightTextRenderer(filter);</span>

会得到类似这样的东西:

Text highlighted to show matches

您可以通过调整 HighlightTextRendererCornerRoundnessFramePenFillBrush 属性来更改高亮显示。

请记住:列表必须是所有者绘制的,渲染器才能生效。

4.13.2 Excel 样式过滤

[v2.5] ObjectListView 可以向用户呈现 Excel 样式的过滤界面。如果他们右键单击列标题,将显示一个“过滤”菜单项。这将允许用户从该列中选择一个或多个不同的值。当他们单击“应用”时,将只显示那些该列具有所选值之一的行。

Excel-like filtering

如果您不希望用户拥有此过滤功能,请将 ObjectListView.ShowFilterMenuOnRightClick 设置为 false。要隐藏特定列的“过滤”菜单项,请将该列的 UsesFiltering 设置为 false

4.14 列选择

在运行时,用户可以自行选择在 ObjectListView 中希望看到的列。此功能的用户界面机制是:当用户右键单击任何标题时,将显示一个菜单,允许他们选择要查看的列。

列选择机制的确切行为由 SelectColumnsOnRightClickBehaviour 属性控制。

要阻止用户更改可见列,请将此属性设置为 ColumnSelectBehaviour.None

要将列选择菜单作为标题右键菜单的子菜单呈现,请将此属性设置为 ColumnSelectBehaviour.Submenu

要将列选择菜单作为标题右键菜单的底部项目呈现,请将此属性设置为 ColumnSelectBehaviour.Inline。这是默认值。如果 SelectColumnsMenuStaysOpentrue(这是默认值),则在用户单击列后,菜单将保持打开状态,允许他们隐藏或显示多个列而无需再次右键单击。

要向用户显示一个对话框,允许他们选择列(以及重新排列列的顺序),请将此属性设置为 ColumnSelectBehaviour.ModelDialog

如果有些列您不希望用户能够隐藏,请将 OLVColumn.Hideable 设置为 false。这将阻止用户隐藏该列。

注意:第 0 列永远不能隐藏。这是底层 Windows 控件的限制。如果您希望使您的第一列可隐藏,请将其移动到列列表中的任何其他位置,然后将其 DisplayIndex 设置为 0,以便它显示在第一位。

4.15 动画

你难道不想把那些花哨的动态视觉糖果添加到你的应用程序中吗?你知道,单元格周围闪烁的边框,或者列表中央旋转的星星?

在 v2.4 中,ObjectListView 现在集成了 Sparkle 动画库。这个库允许您在现有控件之上添加动画。由于 .NET ListViews 的工作方式,该库无法与普通 ListViews 一起使用。然而,通过 ObjectListViews 装饰的可扩展性,它现在可以与 ObjectListViews 无缝协作。

4.14.1 理解 Sparkle

要了解 Sparkle 库,您应该阅读这篇文章——当然还要查看代码。但简而言之,该库的设计目标是:

  • 在任何控件上进行短时动画。Sparkle 库旨在在现有控件之上绘制短时动画。它开箱即用只支持使用 Paint 事件的控件,但它已适配用于 ObjectListView。
  • 声明式。Sparkle 库是声明式的。您说明动画要执行什么,然后运行动画。动画在开始之前是完全定义的。
  • 非交互式。Sparkle 库不进行用户交互——它不监听鼠标移动、点击或拖动。它不进行碰撞检测或物理模型。它只绘制视觉糖果。

要使用该库本身,您需要掌握其四个主要概念:

  1. 动画。动画是精灵放置的画布。它是绘制东西的白板。
  2. 精灵。精灵是可以绘制的东西。精灵有几种类型——一种用于图像,另一种用于文本,还有一种用于形状。通常通过继承 Sprite(或实现 ISprite)来创建自己的精灵类型。
  3. 效果。效果是随着时间改变精灵的东西。它们是库中的“推动者和震撼者”,它们实际执行操作。精灵在那里静静地,完全被动,看起来很漂亮,但效果会推动它们,改变它们的可见性,旋转或调整大小。同样,您可以使用现有的 Effects,也可以通过 IEffect 接口实现您自己的效果。
  4. 定位器。定位器是知道如何计算点或矩形的东西。它们不是说“把这个精灵放在 (10, 20)”,而是允许您说“把这个精灵放在另一个精灵的右下角”。这个想法可能有点难以理解,但一旦你掌握了它,它就会非常强大。

创建 Sparkle 动画的工作流程是:

  1. 决定动画将出现在哪里。那是你的 Animation
  2. 思考你想展示什么。那是你的 Sprites
  3. 思考你想让每个精灵做什么。那是你的 Effects
  4. 每当 Effect 需要“在哪里”时,那就是你需要 Locators 的时候。

4.14.2 简单示例

好的,好的。直接给我看代码。我们将在 ObjectListView 的中间放置一个旋转、渐隐的星星。

要在 ObjectListView 上添加动画,您可以使用 AnimatedDecoration 类。根据使用的构造函数,这会为整个列表、一行或仅一个单元格创建动画:

<span>AnimatedDecoration listAnimation = new AnimatedDecoration(this.olvSimple);
AnimatedDecoration rowAnimation = new AnimatedDecoration(this.olvSimple, myModel);
AnimatedDecoration cellAnimation = new AnimatedDecoration(this.olvSimple, myModel, olvColumn);</span>

我们想要一个可以在整个列表上绘制的动画,所以我们使用这种形式:

<span>AnimatedDecoration listAnimation = new AnimatedDecoration(this.olvSimple);
Animation animation = listAnimation.Animation;</span>

现在我们有了 Animation,我们可以制作我们的 Sprites。我们只需要一个精灵,并且我们希望它显示一个图像,所以我们使用 ImageSprite。还有一个用于绘制带边框/背景文本的 TextSprite,以及用于绘制规则形状的 ShapeSprite

<span> Sprite image = new ImageSprite(Resource1.largestar);</span>

我们有了精灵。现在我们想让它做一些事情。每当我们想让一个精灵做一些事情时,我们都需要一个 Effect。对于这个例子,我们希望图像在列表中心旋转,并在旋转时逐渐消失。我们将效果添加到精灵中,说明效果何时开始以及持续多久。

<span>image.Add(0, 2000, Effects.Rotate(0, 360 * 2.0f)); 
image.Add(1000, 1000, Effects.Fade(1.0f, 0.0f));</span>

这意味着,在精灵动画开始后的前 2000 毫秒内,图像应该完全旋转两次。第二个语句表示,在精灵开始后的 1000 毫秒内,精灵应该花费 1000 毫秒逐渐完全消失。

大多数 Sprites 都会被赋予某种 MoveEffect 来移动它们。然而,我们希望这个精灵只停留在列表的中心,所以我们给它一个 FixedLocation

<span>image.FixedLocation = Locators.SpriteAligned(Corner.MiddleCenter);</span>

这表示图像 MiddleCenter 将始终与动画的 MiddleCenter 对齐(在本例中,即整个列表的 MiddleCenter)。

最后一步是将 Sprite 添加到 Animation 中。

<span>animation.Add(0, image);
</span>

这表示在动画开始后 0 毫秒(即立即)开始运行图像精灵。通常,精灵会在动画开始一段时间后才开始,但在这种情况下,只有一个精灵,所以它也可以立即开始。

动画现已完全配置,剩下的就是运行它了。

<span>animation.Start();</span>

一切顺利的话,这应该会在 ObjectListView 上生成一个看起来像这样的动画:

再次说明,CodeProject 不支持页面内的动画,因此请点击此处查看实际动画。

只需八行代码,你就可以在原本静态的 ListView 上放置一个旋转、褪色的星星(类似 Picasa 的效果)。

5. 有趣的代码片段

5.1 反射

反射以几种方式使用:从模型对象获取数据、将数据放入单元格编辑器以及将数据放入模型对象。

5.1.1 动态获取信息

从模型对象获取数据使用反射来通过其名称动态调用方法、属性或字段。

<span>protected object GetAspectByName(object rowObject) {
    if (String.IsNullOrEmpty(this.aspectName))
        return null;
 
    BindingFlags flags = BindingFlags.Public | BindingFlags.Instance |
                         BindingFlags.InvokeMethod |
                         BindingFlags.GetProperty | BindingFlags.GetField;
    try {
        return rowObject.GetType().InvokeMember(this.aspectName,
            flags, null, rowObject, null);
    }
    catch (System.MissingMethodException) {
        return String.Format("Missing method: {0}",
            this.aspectName);
    }
}</span>

此代码中需要注意的事项:

  • 如果方面名称为 null 或为空,则尝试调用任何内容都没有意义。
  • BindingFlags 表示只调用 public 实例方法、属性或字段。Static 方法以及 protectedprivate 方法将不会被调用。
  • InvokeMember() 方法是在“类型”上调用,而不是在“对象”上调用。
  • 捕获 MissingMethodException 是必要的,因为方法或属性名称可能不正确。

反射编码更容易,但你会付出速度上的代价。在我的机器上,反射比使用委托慢 5-10 倍。对于只有 10-20 个项目的列表来说,这并不重要。但是,如果你的列表有数百个项目,那么安装 AspectGetter 委托是值得的。

实际的代码实际上更复杂,因为它支持使用点符号访问子属性。将多个方法或属性名称用点连接起来作为方面名称是有效的。每个方法或属性名称都会被解引用,并且解引用结果将用作下一个方法或属性的目标。这比解释起来更直观Smile | <img src=" />

例如,Owner.Address.Postcode 是一个有效的方面名称。这将从初始模型对象中获取 Owner 属性,然后询问该所有者对象其 Address。然后,它将询问该地址其 Postcode

[自 v2.4 起,ObjectListView 使用更复杂的方案通过反射访问数据。这些改进体现在 Munger 类中。平均而言,这些改进比标准反射快 3-5 倍,这降低了(但并未完全消除)编写自定义 AspectGetters 的价值。]

5.1.2 将值放入单元格编辑器

当我们在屏幕上放置一个单元格编辑器时,我们需要从单元格中获取值并以某种方式将其提供给控件。不幸的是,没有标准的方法来给 Control 一个值。有些控件有 Value 属性,这正是我们想要的,但有些则没有。当存在 Value 属性时,我们希望使用它,但当不存在时,我们能做的最好的就是使用 Text 方法。

<span>protected void SetControlValue(Control c, Object value, String stringValue)
{
    // Look for a property called "Value". We have to look twice
    // since the first time we might get an ambiguous result
    PropertyInfo pinfo = null;
    try {
        pinfo = c.GetType().GetProperty("Value");
    } catch (AmbiguousMatchException) {
        // The lowest level class of the control must have overridden
        // the "Value" property.
        // We now have to specifically  look for only public instance properties
        // declared in the lowest level class.
        BindingFlags flags = BindingFlags.DeclaredOnly |
            BindingFlags.Instance | BindingFlags.Public;
        pinfo = c.GetType().GetProperty("Value", flags);
    }
 
    // If we found it, use it to assign a value, otherwise simply set the text
    if (pinfo == null)
        c.Text = stringValue;
    else {
        try {
            pinfo.SetValue(c, value, null);
        } catch (ArgumentException) {
            c.Text = stringValue;
        }
    }
}</span>

那么,这里发生了什么?

首先,我们使用 GetProperty 尝试获取控件上 Value 属性的信息。我们必须考虑模糊匹配,如果控件的直接类覆盖了基类的 Value 属性,就会发生这种情况。在这种情况下,我们使用一些 BindingFlags 来表示我们想要在最低级别类中声明的 Value 属性。对于任何语言律师,是的,我知道这并非万无一失,但它在几乎所有情况下都有效。

一旦我们有了属性信息,我们就可以简单地调用 SetValue 方法。我们必须捕获 ArgumentException,以防值无法设置。

如果其中任何一个出错,我们只需使用 Text 方法将值放入控件中,并希望它能实现我们想要的功能。s

5.2 在 ListView 子项上显示图像

ObjectListView 最初的目的是让 ListView 更易于使用,而不是添加大量新功能。最初,子项图像是唯一附加的功能(然而,现在它确实添加了大量新功能)。普通的 ListView 只支持第一列中的图像。ObjectListView 没有此限制;任何列都可以显示图像。要在子项上显示图像,基本上有两种策略:

  1. 所有者绘制子项
  2. 强制底层 ListView 控件绘制它们

所有者绘制是一个我不想打开的潘多拉魔盒,所以我最初选择了第二种方案。Windows 中的 ListView 控件能够绘制子项图像,但该功能并未在 .NET 中公开。我们可以向底层 ListView 控件发送消息,使其显示图像。请记住,这些技巧依赖于底层 ListView 控件,因此它们可能在未来版本的 Windows 中不起作用。它们肯定无法在非 Microsoft 平台上运行。要使 ListView 控件绘制子项图像,我们需要:

  1. ListView 控件本身上设置扩展样式 LVS_EX_SUBITEMIMAGES
  2. 告诉 ListView 控件哪个子项显示哪个图像

设置扩展样式会很简单,只是 .NET 没有公开扩展样式标志。因此,我们必须引入 SendMessage() 函数并定义我们想要使用的常量。

<span>[DllImport("user32.dll", CharSet=CharSet.Auto)]
private static extern IntPtr SendMessage(IntPtr hWnd, int msg,
    int wParam, int lParam);
 
private const int LVM_SETEXTENDEDLISTVIEWSTYLE = 0x1000 + 54; // LVM_FIRST+54
 
private const int LVS_EX_SUBITEMIMAGES         = 0x0002;</span>

然后,在某个方便的时候,你打开标志

<span>SendMessage(this.Handle, LVM_SETEXTENDEDLISTVIEWSTYLE,
    LVS_EX_SUBITEMIMAGES, LVS_EX_SUBITEMIMAGES);</span>

这已经足够了,只是 .NET Framework 在设置扩展样式时会擦除所有未知扩展样式。例如 FullRowSelectGridLines。因此,上述代码必须在所有其他初始化完成后调用。

我们的第二个任务是告诉 ListView 控件哪个子项将显示哪个图像。为此,我们需要一个新的结构 LVITEM 和更多常量。我们不使用大多数 LVIF_ 常量,但为了完整性,它们被包含在内。

<span>private const int LVM_SETITEM = 0x1000 + 76; // LVM_FIRST + 76
 
private const int LVIF_TEXT        = 0x0001;
private const int LVIF_IMAGE       = 0x0002;
private const int LVIF_PARAM       = 0x0004;
private const int LVIF_STATE       = 0x0008;
private const int LVIF_INDENT      = 0x0010;
private const int LVIF_NORECOMPUTE = 0x0800;
 
[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Auto)]
private struct LVITEM
{
    public int     mask;
    public int     iItem;
    public int     iSubItem;
    public int     state;
    public int     stateMask;
    [MarshalAs(UnmanagedType.LPTStr)]
    public string  pszText;
    public int     cchTextMax;
    public int     iImage;
    public int     lParam;
    // These are available in Common Controls >= 0x0300
    public int     iIndent;
    // These are available in Common Controls >= 0x056
    public int     iGroupId;
    public int     cColumns;
    public IntPtr  puColumns;
};</span>

我们还需要第二次导入 SendMessage,但签名略有不同。我们使用参数 EntryPoint 来使用 C# 函数名称以外的名称导入函数。

<span>[DllImport("user32.dll", EntryPoint="SendMessage", CharSet=CharSet.Auto)]
private static extern IntPtr SendMessageLVI(IntPtr hWnd, int msg,
    int wParam, ref LVITEM lvi);</span>

最后,我们可以使用这样的方法设置子项图像:

<span>public void SetSubItemImage(int itemIndex, int subItemIndex, int imageIndex)
{
    LVITEM lvItem = new LVITEM();
    lvItem.mask = LVIF_IMAGE;
    lvItem.iItem = itemIndex;
    lvItem.iSubItem = subItemIndex;
    lvItem.iImage = imageIndex;
    SendMessageLVI(this.Handle, LVM_SETITEM, 0, ref lvItem);
}</span>

在上述成员中,itemIndex 是所讨论行的基于 0 的索引。subItemIndex 是子项的基于 1 的索引,imageIndex 是与 listview 关联的图像列表的基于 0 的索引。

5.3 IDE 集成

一旦我们有了漂亮的新 UI 小部件,我们还有另一个重要的步骤:让它在 IDE 中工作。这个 ListView 的全部意义在于它应该让程序员的生活更轻松。这意味着它必须与开发环境很好地集成,这让我们进入了属性和元数据的可怕世界。

弄清楚如何与 IDE 集成的一个问题是它没有很好的文档记录。也就是说,有些部分有文档记录,但通常不清楚我们应该如何处理这些部分。你可能读到可以使用 EditorAttribute 来控制特定属性的编辑方式,但很难看出如何使用这些信息来为你的自定义 DataSourceDataMember 属性添加正确类型的编辑器。

这就是准神奇的 Lutz Roeder 的 .NET Reflector 如此有用的原因 [它已被 RedGate 收购,现在仅供商业使用(他们有 14 天的试用期)];不仅是公共的,还有每个类的所有方法。然后它对这些方法进行逆向工程,生成源代码。它是一款令人惊叹且极其有用的软件。使用 Reflector,结果表明,我们的 DataSource 属性的正确“咒语”是相对简单但不太直观的:

<span>[AttributeProvider(typeof(IListSource))]
public Object DataSource { ... }</span>

然而,对于 DataMember 属性,我们需要调用这个“咒语”:

<span>[Editor("System.Windows.Forms.Design.DataMemberListEditor, System.Design,
    Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a",
    typeof(UITypeEditor))]
public string DataMember { ... }</span>

鉴于 SDK 文档中甚至没有提及 DataMemberListEditor,这至少可以被认为是不明显的。

5.4 实现限制宽度的列

为了限制列的宽度,我们必须找到一种方法来拦截修改它们的尝试。有三种 UI 机制可以改变列的宽度:

  • 用鼠标拖动分隔线
  • 双击分隔线以自动调整列的大小
  • 按 Ctrl-NumPad+ 自动调整所有列的大小

幸运的是,所有这三种机制最终都使用相同的 HDN_ITEMCHANGING 消息。我们只需要捕获该消息,一切都应该没问题。解决方案的第一部分需要处理 WM_NOTIFY 事件,如下所示:

<span>protected override void WndProc(ref Message m) {
    switch (m.Msg) {
        case 0x4E: // WM_NOTIFY
        if (!this.HandleNotify(ref m))
            base.WndProc(ref m);
        break;
    default:
        base.WndProc(ref m);
        break;
    }
} </span>

然后我们可以处理 HDN_ITEMCHANGING 消息。如果更改会使列比应有的宽度更宽或更窄,我们只需通过返回结果 1 来否决更改。

<span>private bool HandleNotify(ref Message m) {
 
    bool isMsgHandled = false;
 
    const int HDN_FIRST = (0 - 300);
    const int HDN_ITEMCHANGINGA = (HDN_FIRST - 0);
    const int HDN_ITEMCHANGINGW = (HDN_FIRST - 20);
 
    NMHDR nmhdr = (NMHDR)m.GetLParam(typeof(NMHDR));
 
    if (nmhdr.code == HDN_ITEMCHANGINGW) {
        NMHEADER nmheader = (NMHEADER)m.GetLParam(typeof(NMHEADER));
        if (nmheader.iItem >= 0 && nmheader.iItem < this.Columns.Count) {
            HDITEM hditem = (HDITEM)Marshal.PtrToStructure(
                nmheader.pHDITEM, typeof(HDITEM));
            OLVColumn column = this.GetColumn(nmheader.iItem);
            if (IsOutsideOfBounds(hditem, column) {
                m.Result = (IntPtr)1; // prevent the change
                isMsgHandled = true;
            }
        }
    }
 
    return isMsgHandled;
}
 
private bool IsOutsideOfBounds(HDITEM hditem, OLVColumn col) {
    // Check the mask to see if the width field is valid,
    if ((hditem.mask & 1) != 1)
        return false;
 
    // Now check that the value is in range
    return (hditem.cxy < col.MinimumWidth ||
           (col.MaximumWidth != -1 && hditem.cxy > col.MaximumWidth));
}</span>

这个解决方案看起来不是很复杂。然而,现实往往没那么简单。例如,那些对标题控件有所了解的人可能会想:“嘿!HDN_TRACK 和它的朋友们呢?你为什么不处理它们?”

嗯,根据 KB 文章 #183258,微软声明当标题控件具有 HDS_FULLDRAG 样式时,它将接收 HDN_ITEMCHANGING 消息而不是 HDN_TRACK 消息。自通用控件版本 4.71 以来,标题控件总是接收 HDS_FULLDRAG 样式。所以,看起来我们只需要处理 HDN_ITEMCHANGING 消息。

问题是这并非总是如此。在 XP SP2(至少)下,具有 HDS_FULLDRAG 样式的标题控件并不总是发送 HDN_ITEMCHANGING 消息而不是 HDN_TRACK 消息。这可能就是微软撤回该特定 KB 文章的原因。在某些机器上,标题控件会按预期发送 HDN_ITEMCHANGING 事件,但在其他机器上,标题控件会发送旧的消息序列:HDN_BEGINTRACKHDN_TRACKHDN_ENDTRACKHDN_ITEMCHANGINGHDN_ITEMCHANGED

经过一番深入研究,关键设置似乎是 Explorer 选项“拖动时显示窗口内容”。在一个“真正奇怪的副作用”的例子中,如果此选项打开,标题将发送 HDN_ITEMCHANGING 消息而不是 HDN_TRACK 消息(应该如此)。但是,如果关闭此选项,标题将发送大量 HDN_TRACK 消息,并且在过程的最后只发送一条 HDN_ITEMCHANGING 消息。

有两种可能的事件序列使我的简单计划复杂化。如果“拖动时显示窗口内容”选项打开,则当前代码完美运行。如果关闭,则情况会更糟。

总的来说,如果我们在接收到多个 HDN_TRACK 消息和仅一个 HDN_ITEMCHANGING 消息,则更难控制调整大小过程。原因是无法取消单个 HDN_TRACK 消息。如果从 HDN_TRACK 消息返回结果 1,我们将取消整个拖动操作,而不仅仅是该特定跟踪事件。从用户的角度来看,当他们将列拖动到其最小或最大宽度时,即使他们没有松开鼠标,拖动操作也会简单地停止。这显然不是我们想要的。

[v2.0] 自 v2.0 起,ObjectListView 修改了 HDN_TRACK 消息本身,在原地改变列的大小,这是最好的解决方案。

5.5 ListView 中的奇怪错误

以防其他人遇到此问题,ListView 代码中存在一个奇怪的错误。此错误的影响是,在 BeginUpdate()EndUpdate() 对之间,如果 ListView 句柄尚未创建,则遍历 Items 集合并调用 Items.Clear() 不会可靠地工作。例如,如果在创建 listView1 的句柄之前调用以下方法,则调试输出中将不会写入任何内容:

<span>private void InitListView1()
{
    this.listView1.BeginUpdate();
    this.listView1.Items.Add("first one");
    this.listView1.Items.Add("second one");
    foreach (ListViewItem lvi in this.listView1.Items)
        System.Diagnostics.Debug.WriteLine(lvi);
    this.listView1.EndUpdate();
}</span>

如果删除 BeginUpdate()EndUpdate() 对,或者在创建句柄后调用该方法,则该方法将按预期工作。

此错误源于,在 ListView 代码的深层,当调用 BeginUpdate() 时,ListView 开始缓存列表更新。当调用 EndUpdate() 时,缓存被刷新。然而,GetEnumerator() 不会刷新缓存或考虑它。因此,在调用 BeginUpdate()EndUpdate() 之间遍历 Items 将只返回在 BeginUpdate() 之前存在的项目。至少有两种简单的方法可以解决此错误:

  1. 不要使用 BeginUpdate()/EndUpdate() 对。
  2. 在遍历集合之前调用 Items.Count,这将刷新缓存。

感谢 ereigo 帮助我找到这个错误。

5.6 实现图像水印

我喜欢图片。我认为 Explorer 中可以在列表视图右下角放置一个小图形很酷。我想用 ObjectListView 做同样的事情。当然,这不应该那么困难。但事实并非如此。

我具体想要这个功能实现什么?我想要 ObjectListView 上的背景图像。它必须固定在原地,当 ListView 滚动时不会滚动。它必须在 XP 和 Vista 上都能工作。它必须易于自定义,理想情况下只需在 IDE 中设置图像即可。如果图像可以放置在任何角落,或者具有不同级别的透明度,那将是额外的奖励。显然,我希望它完美无瑕地工作——尽管我满足于它出色地工作。

5.6.1 WM_ERASEBKGROUND

经典的解决方案是拦截 WM_ERASEBKGROUND 消息,擦除 ClientRectangle,绘制你想要的任何东西,然后控件的其余部分会在你已经绘制的内容之上绘制。很简单。

但它不起作用。实际上,只要你不双缓冲 ListView,它就有效。当 ListView 未缓冲时,在 WM_ERASEBKGROUND 处理程序中绘制的图像看起来很好。但是,当控件双缓冲时,它就不起作用了。当 DoubleBuffered 设置为 true 时,它还会设置样式 AllPaintingInWmPaint,这意味着:不要使用 WM_ERASEBKGROUND,绘制处理程序将完成所有工作,包括擦除背景。因此,对于双缓冲的 ListView(这正是我想要的),在 WM_ERASEBKGROUND 处理程序中绘制不起作用。

5.6.2 LVM_SETBKIMAGE

第二次尝试是使用 LVM_SETBKIMAGE。这个 WinSDK 消息告诉 ListView 在控件下方绘制图像。这正是我想要的。但生活很少那么容易。

第一个困难实际上是让它工作。TortoiseSVN 有时会有列表视图背景图像,Stefan 已经好心地记录了他的一些麻烦,以便让它工作。利用那里的信息,我设法在控件下方放置了一个图像!太棒了……嗯,也不是真的。它确实在 ListView 下方放置了一个图像,但带来了一些令人不快的副作用:

  • 有了背景图像后,行背景颜色不再起作用。
  • 背景图像总是被硬压在右下角。LVBKIMAGE 结构有 xOffsetyOffset 字段,据称可以让你改变这一点,但据我所知,它们没有任何效果。
  • 在 XP 下,带有透明区域的背景图像不会透明绘制——透明区域总是显示为蓝色。即使设置了 LVBKIF_FLAG_ALPHABLEND 标志也是如此。
  • 网格线绘制在图像上方,这看起来很奇怪。
  • 子项上的图标使子项单元格在图像上方绘制。
  • 所有者绘制的单元格总是擦除图像(我怀疑这可以修复)。

主要问题出在 Details 视图。第 0 列总是擦除图像。我可以忍受其他问题,但是当第 0 列将其擦除时,底层图像有什么用呢?我检查了 Stefan 是否找到了这个问题的解决方案,但他没有。

[2012 年 5 月更新] Windows 7 在一定程度上改善了这种情况。它没有第 0 列或子项图像的相同问题。网格线仍然是一个问题,而且显然,它在所有者绘制模式下不起作用(在所有者绘制模式下,每个单元格都会绘制自己,包括其背景,这会覆盖背景图像)。如果你能接受这些限制,原生水印就非常整洁。它们是真正的背景,而不是像 OverlayImage 使用的半透明叠加层。它们还比叠加层具有明显的优势,即即使在 MDI 应用程序中也能正常工作。

ObjectListView 现在 [v2.5.1] 内置支持原生背景。

// Set a watermark in the bottom right of the control  
this.olv.SetNativeBackgroundWatermark(Resource1.redback1);
  
// Set the background image positioned 50% horizontally and 75% vertically  
this.olv.SetNativeBackgroundImage(Resource1.redback1, 50, 75));

// Set a tiled background to the control  
this.olv.SetNativeBackgroundTiledImage(Resource1.limeleaf);  

5.6.3 分层窗口 API

我最终决定使用分层窗口 API,.NET 通过 FormOpacityTransparencyKey 属性公开了它。

这个想法是:在 ObjectListView 上方放置一个完全透明的窗体,然后在该窗体上绘制(Mathieu Jacques 他的 LoadingCurtain 想法也做了同样的事情)。从用户的角度来看,图像似乎是绘制在 ObjectListView 上的,但从 ObjectListView 的角度来看,叠加层并不存在。

这个想法是好的,但在实际实现之前还有许多其他的复杂性。点击此处查看过于详细的故事版本

5.6.4 只管做

但它最终还是奏效了。因此,从 v2.2 开始,ObjectListViews 支持叠加层,这些叠加层是不可滚动、半透明的图像或文本,绘制在列表内容的顶部。

叠加方案是可扩展的,但最常见的两种叠加是图像叠加和文本叠加。

  • ImageOverlayObjectListView 上绘制图像
  • TextOverlay 是一个高度可配置的叠加层,可以绘制带边框、带颜色和带背景的文本。文本可以旋转。

这两种叠加层非常常见,因此 ObjectListView 预定义了每种一个:OverlayImageOverlayText 属性。这些预定义叠加层暴露给 IDE,可以直接在那里配置。因此,对于大多数情况,这只是您需要了解的叠加层知识。

5.6.5 MDI 限制

尽管我尽了最大的努力,这个方案在 MDI 应用程序中仍然不起作用。叠加层不遵守 MDI 窗体所遵守的 z 轴顺序。这意味着叠加层会出现在所有 MDI 窗体的上方。这个问题没有解决方案。只需不要在 MDI 应用程序中使用叠加层。你唯一的选择是使用原生背景图像(及其限制)。

5.7 启用禁用行

引用

什么蠢 ListView 不允许禁用行?我要放弃你的控件,自己写一个。

并非所有 ObjectListView 的功能请求都措辞礼貌 :)

其他人以更委婉的方式提出了相同的功能,所以我认为看看是否可行会很好。能够禁用行将是一个很棒的功能。尽管它很容易被滥用(从 UI 的角度来看),但我可以很容易地想象到它会非常有用的情况。

5.7.1 定义禁用

禁用行将具有以下特征:

  • 无法选择
  • 无法激活(双击、按 Enter 键)
  • 无法编辑
  • 无法选中
  • 可以聚焦
  • 必须在视觉上有所区别(通常是灰色文本和灰度图像)

除了焦点部分,没什么太令人惊讶的。[快速提醒:焦点是接收键盘输入的 UI 元素。可以选中多行;只能聚焦一行] 禁用行需要能够聚焦,以便用户可以使用 UpArrow/DownArrowPageUp/PageDown 在行之间移动。如果禁用行无法聚焦,则使用箭头键在列表中移动将简单地在禁用行之前停止。我们可以尝试跳过禁用行,但这会导致一个糟糕的兔子洞——当下一页只包含禁用项时,PageDown/PageUp 会发生什么?当所有行都被禁用时会发生什么?

在编辑时,用户可以使用 Alt-UpAlt-Down 来开始编辑当前单元格上方或下方的行。如果该方向的行被禁用,编辑将跳过该行,直到该方向的下一个启用行。

在我发布此功能后,首先会发生的是有人会想要稍微不同的行为:看起来被禁用,但仍然可以编辑,或者被禁用但仍然可以选中。只需说不!目前,这就是禁用行的行为,你不能挑三拣四 :)

5.7.2 实现它

有了这些功能描述,什么看起来技术上最困难?我的代码控制编辑、检查和(大部分)显示,所以这不应该有问题。但如何防止行被选中呢?

对于 ListView,有几种方法可以选中行:

  • 单击(显然)
  • Shift + 单击选择锚点和单击行之间的所有行
  • Ctrl-A 选择所有行
  • 套索选择

根据 MSDN 的说法,所有这些选择行的方式最终都会导致相同的 LVN_ITEMCHANGING 反射通知。我们可以处理此通知并查找选择状态的变化。

bool isSelected = (nmlistviewPtr.uNewState & LVIS_SELECTED) == LVIS_SELECTED;

更好的是,LVN_ITEMCHANGING 是可取消的,所以当用户尝试选择禁用行时,我们只需设置 m.Result = (IntPtr)1,选择就会被取消。

哦,是的!半小时后,最困难的部分就完成了!除了……这对于普通的 ObjectListView 来说工作正常,但对于虚拟列表视图(FastObjectListView 和 TreeListView)来说根本不起作用。LVN_ITEMCHANGING 从未针对虚拟列表触发 :( 文档中甚至也提到了这一点。

LVN_ITEMCHANGED(注意是 -ED)对普通和虚拟列表视图都触发,但它不能被取消。它只是说:“嘿。这一行已经改变了,你对此无能为力!”我尝试了各种方法来调整通知,改变其数据,跳过基本处理,取消事件,但没有一个可靠地工作。

如果优雅的解决方案不起作用,我们总是可以使用大锤!当我们收到行已选中的通知并且我们希望该行被禁用时,我们可以强制反转选择:

OLVListItem olvi = this.GetItem(nmlistviewPtr.iItem); 
if (olvi != null && !olvi.Enabled) 
    NativeMethods.SetItemState(this, nmlistviewPtr.iItem, LVIS_SELECTED, 0);

令人惊讶的是,这确实有效。对于普通列表和虚拟列表,这都可以阻止点击选择和套索选择。然而……

5.7.3 虚拟列表视图的复杂性

虚拟列表是 ListView 系列中的问题儿童。它们乐于挑战程序员的生活。如果有什么事情会变得困难,那总是与虚拟列表有关。例如……

在虚拟列表上,通过 Shift-Click 选择范围或通过 Ctrl-A 选择所有项目会带来问题。

问题在于,对于 Shift-ClickCtrl-A,listview 收到单个通知,但选择了多行。通知只能指示单个项目 nmlistviewPtr.iItem,但我们需要知道其他被选中的行。我们可以解决 Ctrl-A 问题。当通过 Ctrl-A 选择所有项目时,通知在 nmlistviewPtr.iItem 中有一个特殊值 -1。当我们看到此值时,我们知道所有行都已选中,因此我们可以遍历所有禁用的对象并再次显式取消选择它们:

// -1 indicates that all rows are to be selected -- in fact, they already have been. 
// We now have to deselect all the disabled objects. 
if (nmlistviewPtr2.iItem == -1) { 
    foreach (var disabledModel in this.DisabledObjects) { 
        int modelIndex = this.IndexOf(disabledModel); 
        if (modelIndex >= 0) 
            NativeMethods.SetItemState(this, modelIndex, LVIS_SELECTED, 0); 
    } 
}

Shift-Click 更加麻烦。它麻烦是因为很难精确解释 Shift-Click 的作用。当你在列表视图中 Shift-Click 一行时,点击行和锚定行之间的所有行都会被选中。但是哪一行是锚定行呢?这才是困难的部分。它根据上次选择的方式和你使用的操作系统而有所不同。点击、Ctrl-Click、套索都以不同的方式设置锚点。另一个复杂之处在于,如果列表是分组或未分组的,“之间”的计算方式也大不相同。

在与所有这些复杂性搏斗之后,我灵光一闪!我真的不在乎选择了什么,我只是想确保所有禁用的行仍然未被选中。一旦我意识到这一点,我就可以使用与 Ctrl-A 处理相同的处理方式。搞定!

5.7.4 正确的颜色

好的。现在行可以被禁用,并且不能被选中。但是这些行看起来仍然一样。现在我们必须让它们看起来不同。

禁用的项目通常会以某种方式变灰。这很简单——只需将项目的 ForeColor 设置为灰色即可。它确实有效,但效果不佳。禁用行的文本是灰色的,但图像仍然是彩色的,复选框看起来仍然是启用的。

对于非所有者绘制的列表,这是我们能做的最好的。底层的 ListView 控件允许设置文本颜色,但图像仍然是彩色的。[是的,我们可以创建所有图像的灰度版本,然后在禁用行时以某种方式重新映射图像——但这对我来说工作量太大,而且很脆弱]。

对于所有者绘制的列表,我们有更多的控制。首先,如果一行被禁用,我们可以将任何图像绘制成灰度。有一个非常方便的方法 ControlPaint.DrawImageDisabled(),它(几乎)完全符合我们的要求。它唯一的限制是它不会自动缩放图像,所以如果图像需要缩放,我们将不得不手动完成(我目前没有这样做,因此禁用的图像不会缩放到更窄的空间)。

这对于来自 ImageLists 的图像效果不佳。目前,大多数图像都是使用 ImageList_Draw() WinApi 函数绘制的。这为我们提供了很好的功能,而且速度很快!但它不支持灰度绘制。

它有一个“老大哥”ImageList_DrawIndirect(),这是一个拥有数万个开关和表盘的“飞机驾驶舱”版本。但表面上,它也不支持灰度绘制!哼!但让我们更深入地了解一下。

ImageList_DrawIndirect() 接受一个(大型)参数块:

[StructLayout(LayoutKind.Sequential)] 
public struct IMAGELISTDRAWPARAMS { 
    public int cbSize; 
    public IntPtr himl; 
    public int i; 
    public IntPtr hdcDst; 
    public int x; 
    public int y; 
    public int cx; 
    public int cy; 
    public int xBitmap; 
    public int yBitmap; 
    public uint rgbBk; 
    public uint rgbFg; 
    public uint fStyle; 
    public uint dwRop; 
    public uint fState; 
    public uint Frame; 
    public uint crEffect; 
}

事实证明,如果将 ILS_SATURATE 包含在 fState 中并将 rgbFg 设置为某种深色(例如,黑色或 CLR_DEFAULT),它实际上会以灰度绘制图像。

因此,通过调整我们的渲染器以使用这两种技术(并记住将复选框绘制为禁用),我们的禁用行现在看起来像这样:

这看起来确实相当不错。

6. 结论

我用其他两种语言——Smalltalk 和 Python——编写了这个控件,它始终是我工具箱中最有用的项目之一。我在用这些语言完成的每个项目中都使用过它,我相信它在这里也同样有用。我希望代码能鼓励更多的“懒惰”,让程序员有时间改进项目中的其他部分,从而也鼓励自负。

7. 未来方向

v2.9 也将尽可能地转向使用 IEnumerableSelectedObjectsCheckedObjectsAddObjects()InsertObjects()RefreshObjects()RemoveObjects()CopyObjectsToClipboard() 都将更改为使用 IEnumerable。这是为了在未来版本中使用 LINQ。

v3.0 将是一个重大变化。到目前为止,每个版本都努力保持向后兼容性。v3.0 将不以之为严格目标。它将尽可能向后兼容,但会删除不符合新方案的属性、事件和方法。特别是,那些设计软弱的特性(我在说你 AlwaysGroupByColumn 和你的朋友们)将会消失。

  • 对 .NET 2.0 的支持将被取消。.NET 4.0 将成为最低要求。
  • 大多数委托将被事件取代(事件实际上比委托更快)。
  • 所有样式(单元格、标题、工具提示)将统一,并将包括自定义渲染器和点击检测。
  • 通用清理/重构

版本 3.0 没有确定的时间表。

8. 历史

2015 年 11 月 - 版本 2.9.0

新功能

  • 按钮!
  • 增加了 ObjectListView.CellEditUsesWholeCellOLVColumn.CellEditUsesWholeCell 属性。如果这些设置为 trueObjectListView 在编辑单元格值时将使用单元格的整个宽度。在 OLVColumn 上设置属性仅影响该列,在 ObjectListView 上设置属性则影响所有列。
  • TreeListViews 现在可以绘制三角形作为展开符号,并具有热点行为。将 TreeListView.TreeRenderer.UseTriangles 设置为 true。它也可以通过将 IsShowGlyphs 设置为 false 来不绘制任何展开符号。
  • 添加了 ObjectListView.FocusedObjectObjectListView.DefaultHotItemStyleObjectListView.HeaderMinimumHeight 属性。每个都完全可预测。

大型改进

  • 规范化了各种格式选项的交互,特别是 FormatRowFormatCell 和超链接。
  • 完全重写了演示。每个选项卡现在都是自己的 UserControl,并且有更多的注释和解释。
  • DescribedTaskRenderer 脱离实验状态,成为项目的一等公民。请参阅演示中的“漂亮任务”选项卡。
  • 列表现在默认是所有者绘制的。我收到的所有投诉中大约有四分之一是有人试图使用只有在 OwnerDraw 为 true 时才起作用的功能。这使得 ObjectListView 的所有强大功能都能正常工作,只是在渲染时会增加一点处理成本。它还避免了原生 ListView 恼人的“热点项背景在第 0 列中被忽略”行为。如果您愿意,仍然可以将其设置回 false

小改进

  • 正常的 DefaultRenderer 现在是 HighlightTextRenderer,因为这似乎更普遍有用。
  • 允许 ImageGetter 返回一个 Image(我真不敢相信这从一开始就没起作用!)。
  • 添加了 SimpleDropSink.EnableFeedback,允许关闭拖动操作期间所有漂亮有用的用户反馈。
  • OLVColumn.HeaderTextAlign 变为可空,以便可以“未设置”,在这种情况下,标题的对齐方式将跟随列的对齐方式(这始终是意图)。
  • 通过删除冗余的 BuildList() 调用,使数据绑定列表上的解冻更高效。
  • 自动生成列时,没有 [OLVColumn] 属性的列将自动调整大小。

Bug 修复

  • 又一次尝试禁用 ListView 的“Shift + 点击切换复选框”行为。上次尝试产生了恶劣的副作用——我收到了全套“嘿!当我左键单击控件时,我收到右键单击事件”的电子邮件,表达方式各有不同。新策略(希望)更隐蔽。
  • FastObjectListView 现在在应用过滤器时触发 Filter 事件。
  • 更改过滤器时,现在会触发 SelectionChanged 事件。
  • 在设计器中将 View 设置为 LargeIcon 现在会持久化。
  • 在执行动画时,更严格地检查死控件。
  • 更正了 TreeListView 中焦点丢失且 HideSelectionfalse 时的小 UI 故障。

2014 年 10 月 - 版本 2.8

新功能

  • 增加了禁用行的功能
  • 在列标题中添加了复选框

其他更改

  • 增加了 CollapsedGroups 属性。
  • 扩展了点击测试信息,包括标题组件(标题、分隔线、复选框)。
  • 当鼠标移动到标题上方时,现在会触发 CellOver 事件。将 TriggerCellOverEventsWhenOverHeader 设置为 false 可禁用此行为。
  • Freeze/Unfreeze 现在使用 BeginUpdate()/EndUpdate() 来在冻结时禁用窗口级别的绘图。
  • ObjectListView.HeaderUsesThemes 的默认值从 true 更改为 false。太多人被混淆,试图在标题中显示一些有趣的东西,但什么也没有出现。
  • 最后一次尝试修复多个超链接事件被触发的问题。这涉及到将 NM_CLICK 通知转换为 NM_RCLICK。感谢 aaron 的初步报告和调查。
  • TreeListView.CollapseAll() 现在确实会折叠所有分支。
  • ObjectListView 下载中的预构建 ObjectListView.dll 现在是针对 .NET 4.0 构建的。这将使其能够直接在 VS 2010 及更高版本中使用。对于 VS 2008 和 2005,DLL 必须从包含的源中构建。
  • 添加了 NuGet 支持。ObjectListView 现已提供为 ObjectListView.Official

修复的 Bug

  • 修复了调用 TreeListView.RefreshObject() 可能抛出异常的各种问题。
  • 修复了虚拟列表上复选框的各种问题。
  • 修复了包含单行的虚拟列表在 MouseOver 时不更新超链接的问题。
  • 修复了在安装过滤器时调用 TreeListView.CollapseAll() 可能抛出异常的问题。
  • 修复了一些由于滥用 TryGetValue() 导致的细微错误。
  • 解决了 Resharper 的一些小抱怨。

2014 年 3 月 - 版本 2.7

经过漫长的休息,ObjectListView 的下一个版本可用。

新功能

  • TreeListView 添加了 HierarchicalCheckBoxes(说起来很快,但做了很多工作)。
  • 添加了 TreeListView.Reveal() 以通过展开其所有祖先来显示深度嵌套的模型对象。

其他更改

  • 添加了 CellEditEventArgs.AutoDispose 以允许在单元格编辑器使用后处置。默认为 true。这允许将重型控件缓存以供重用,并处置轻型控件而不会泄漏。
  • ShowHeaderInAllViews 现在适用于虚拟列表。
  • 添加了 TreeListView.TreeFactory 以允许底层 Tree 被其他实现替换。
  • CollapseAll()ExpandAll() 现在触发可取消的事件。
  • 添加了静态属性 ObjectListView.GroupTitleDefault 以允许默认组标题本地化。
  • 添加了 Visual Studio 2012 支持。
  • 在显示组时单击不可分组的列标题现在将按该列对组内容进行排序。
  • TreeListView 现在支持 SecondarySortColumnSecondarySortOrder

修复的 Bug

  • ClearObjects() 现在确实会清除对象 :)
  • 修复了 ShowHeaderInAllViews 的一些问题/错误/烦恼。
  • 修复了与过滤器和列表修改相关的各种错误。
  • 修复了一些错误,以便树展开事件总是被触发,但每个动作只触发一次。
  • RebuildChildren() 在重建之前不再检查 CanExpand 是否为 true。
  • 修复了 RefreshObject() 中的一个长期存在的错误,该错误有时对覆盖了 Equals() 的对象不起作用。

2012 年 7 月 - 版本 2.6

新功能

  • 添加了 DataTreeListView – 一个数据绑定的 TreeListView
  • 添加了 UseNotifyPropertyChanged 属性,允许 ObjectListViews 监听模型上的 INotifyPropertyChanged 事件。
  • Generator 现在可以在普通的模型对象上工作,而不需要属性标记 [OLVColumn] 属性。
  • 添加了 FlagClusteringStrategy – 一种基于位异或整数字段的新聚类策略。
  • 添加了 ObjectListViewOLVColumnCellPaddingCellHorizontalAlignmentCellVerticalAlignment 属性。在所有者绘制的控件上,这些属性控制单元格内容在单元格内的位置。
  • 添加了 OLVExporter – 用于从 ObjectListView 导出数据的实用程序。

其他更改

  • 添加了 Reset() 方法,该方法明确地从所有类型的 ObjectListView 中删除所有行和列。
  • GetItemIndexInDisplayOrder() 重命名为 GetDisplayOrderOfItemIndex() 以更好地反映其功能。
  • 更改了列过滤的工作方式,以便同一个模型对象现在可以存在于多个簇中。这对于过滤 xor 标志字段或多值字符串(例如,以逗号分隔值存储的爱好)很有用。
  • 增加了 SimpleDropSink.UseDefaultCursors 属性。将其设置为 false 以在拖放操作中使用自定义光标。
  • FastObjectListView 中添加了更高效的 FilteredObjects 属性版本。
  • 增加了 ObjectListView.EditModel() 便利方法。
  • 添加了 ObjectListView.AutoSizeColumns() 以根据内容或标题调整所有列的大小。
  • 添加了静态属性 ObjectListView.IgnoreMissingAspects。如果此属性设置为 true,则所有 ObjectListViews 都将静默忽略缺失方面错误。阅读备注以了解此属性的用处。
  • 在排序/分组、添加/删除列或展开分支期间不触发选择更改事件。
  • 剪贴板和拖放现在包括 CSV 格式。
  • 重新实现了 Generator 以便可被子类化。添加了 IGenerator 以允许完全替换列生成。

修复的 Bug (部分)

  • 修复了单击单元格编辑时单元格编辑在第一次鼠标移动后才开始的错误。这修复了许多与单元格编辑和鼠标移动相关的错误。
  • 修复了从 LargeIcon 或 SmallIcon 视图中删除列会导致控件崩溃的错误。
  • 修复了在显示组时,FastObjectListView 上按类型搜索不起作用的错误。
  • 修复了与虚拟列表上的组相关的几个错误。
  • 覆盖层现在会记住所有 ObjectListView 的父级,以便在处置时可以明确地解绑所有这些父级。这可以保护我们免受可视化层次结构中意外更改的影响(例如,将父级 UserControl 从一个选项卡移动到另一个选项卡)。
  • TreeListView.RebuildAll() 现在将保留滚动位置。

2012 年 5 月 - 版本 2.5.1

新功能

  • 增加了对组的更好支持。这包括点击检测、可取消的组展开/折叠事件(GroupExpandingCollapsing)和组状态更改事件(不出所料是 GroupStateChanged)。有关更多详细信息,请参阅此博客
  • 增加了 UsePersistentCheckboxes 属性,允许 ObjectListView 在列表重建时正确记住复选框值。没有它,将过滤器应用于普通的 ObjectListView 总是会导致复选框丢失其值。此属性默认值为 true。设置为 false 可返回到 v2.5 及更早版本的行为。
  • 添加了 AdditionalFilter 属性。通过 AdditionalFilter 属性安装的任何 IModelFilter 将与用户在运行时指定的任何基于列的过滤器相结合。这与 ModelFilter 属性不同,因为设置 ModelFilter替换任何用户给定的列过滤,反之亦然。
  • 添加了 CanUseApplicationIdle 属性,用于处理 Application.Idle 事件未触发的情况。在某些情况下——特别是 VisualStudio 和 Office 扩展——Application.Idle 事件从不触发。如果您将 CanUseApplicationIdle 设置为 falseObjectListView 将正确处理这些情况。
  • 支持原生背景图像。

其他更改

  • 基于 CodeProject 上的“继承内部 WinForms 设计器”中的信息,极大地改进了运行时设计器。
  • 改进了TreeListView 拖动示例。现在还展示了如何处理接受来自外部源的拖放。

修复的 Bug

  • 避免了 .NET 的 ListView.VirtualListSize setter 中导致列表大小改变时闪烁的 bug/特性(阅读此文了解完整细节)。
  • 修复了一个强制组之间始终有 20 像素左右额外空间的 bug。现在这已由 SpaceBetweenGroups 属性正确控制。
  • 修复了当列表的第一个组(仅第一个组)折叠时装饰未绘制的错误。
  • 修复了当视图分组时,向 VirtualObjectListView(包括 FastObjectListViewTreeListView)添加/删除项时发生的错误。
  • 修复了一个错误,即在只有一个可编辑列的 ObjectListView 中,通过 Tab 键更改行会编辑上方单元格而不是下方单元格。
  • 修复了 TreeListView.CheckedObjects 中的一个错误,它会返回已被过滤掉的模型对象。
  • 单击列选择菜单上的分隔符不再导致崩溃。
  • 修复了尝试分组/聚类空列表时可能发生的罕见错误。
  • 处理模型对象同时具有 Item 属性和 Item[] 访问器的情况。
  • 修复了过滤器以正确处理空字符串搜索。
  • 处理在 ObjectListView 上安装第二个工具提示的情况。
  • 在插入或移动后正确重新着色行。
  • 移除了可能导致 Win7/64 位上溢出问题的 m.LParam 类型转换。

支持系统

又一次硬盘崩溃,我最后一台 XP 机器也寿终正寝了。我不再能访问 XP 甚至 Vista——只有 Windows 7。

我可能会尝试购买一台便宜的笔记本电脑专门运行 XP,但目前,我无法在 Windows 7 以外的任何系统上测试 ObjectListView。

2011 年 6 月 - 版本 2.5

新功能
  • 类似 Excel 的过滤功能。右键单击标题将显示“过滤器”菜单,允许您选择将保留下来的值。
  • FastDataListView。就像普通的 DataListView,只是更快。在我的笔记本电脑上,它可以轻松处理 100,000 行的数据集。注意:这不会虚拟化数据访问部分——只虚拟化 UI 部分。因此,如果你的查询返回一百万行,所有行仍然会从数据库加载。但是,一旦加载,它们将由虚拟列表管理。
  • 单元格编辑模式下完全可自定义的字符映射。这是一个针对各种“Tab 键换行”请求的过度解决方案。作为方便的包装器,已添加 CellEditTabChangesRowsCellEditEnterChangesRows 属性。
  • 支持 VS 2010。目标框架必须是 .Net 的“完整”版本。它不适用于“客户端配置文件”(不幸的是,这是 VS 2010 中新项目的默认设置)。
  • 列现在可以禁用排序、分组、搜索和“可隐藏性”(分别为 SortableGroupableSearchableHideable 属性)。
破坏性更改
  • [大改动] 在 VirtualObjectListView 上,DataSource 重命名为 VirtualListDataSource。这是为了允许 FastDataListView 既是 DataListView 又是 VirtualListView——两者都使用了 DataSource 属性 Frown | <img src= /> /li>
  • [小改动] GetNextItem()GetPreviousItem() 现在接受并返回 OLVListView 而不是 ListViewItems
  • [小改动] 树列的渲染器现在必须是 TreeRenderer 的子类,而不仅仅是普通的 IRenderer
  • [小改动] SelectObject()SelectObjects() 不再取消选择所有其他行。这提供了一种更容易将对象添加到选择中的方法。属性 SelectedObjectSelectedObjects 仍然会取消选择所有其他行。
次要功能
  • TextMatchFilter 进行了重大重写。一个文本过滤器现在可以匹配多个字符串。TextMatchFilter 有新的工厂方法(这使得 TextMatchFilter.MatchKind 变得多余)。
  • 在获得新的 VS 2005 Express 副本后,重新恢复了对 VS 2005 的支持。
  • 列选择机制可以通过 SelectColumnsOnRightClickBehaviour 进行自定义。默认是 InlineMenu,其行为与以前的版本相同。其他选项是 SubMenuModalDialog。这需要将 ColumnSelectionForm 从演示项目移动到 ObjectListView 项目中。
  • 添加了 OLVColumn.AutoCompleteEditorMode,优先于 AutoCompleteEditor(现在只是一个包装器)。感谢 Clive Haskins。
  • 添加了 ObjectListView.IncludeColumnHeadersInCopy
  • 添加了 ObjectListView.Freezing 事件。
  • 添加了 TreeListView.ExpandedObjects 属性。
  • TreeListView 中添加了 ExpandingExpandedCollapsingCollapsed 事件。
  • 添加了 ObjectListView.SubItemChecking 事件,当子项上的复选框被选中/取消选中时触发。
  • 允许委托来所有者绘制标题。
  • 所有模型对象比较现在都使用 Equals() 而不是 ==(感谢 vulkanino)。
  • 调整了 UseTranslucentSelectionUseTranslucentHotItem,使其(稍微)更像 Vista/Win7。
  • 添加了在 BorderDecoration 上具有渐变背景的功能。
  • Ctrl-C 复制现在能够使用 DragSource 创建数据传输对象(通过 CopySelectionOnControlCUsesDragSource 属性控制)。
  • 编辑单元格时,Alt-[箭头] 将尝试编辑该方向上的单元格(展示了单元格编辑字符映射可以实现的功能)。
  • 添加了关于如何使 TreeListView 可重排的冗长、教程式分步说明
  • 将文件重新组织到文件夹中。
Bug 修复(不完整列表)
  • 修复了(一劳永逸地)GeneratorDisplayIndex 问题。
  • 虚拟列表可以(最终)将 CheckBoxes 设置回 false,如果它已设置为 true。(这有点投机取巧,可能无法可靠工作)。
  • TreeListView 上保留自动换行设置。
  • 调整最后一个组的大小以使其保持在屏幕上。
  • 更改了 SaveState()/RestoreState() 中使用的序列化器,使其仅按名称解析类。
  • 分组时,组比较器、可折叠组和 GroupByOrderNone 现在都得到正确支持。
  • 尝试在虚拟列表中使用动态 GIF 不再导致崩溃。它仍然不起作用,但不会崩溃。
  • GetNextItem()GetPreviousItem() 现在在分组的虚拟列表上也能正常工作。
  • 修复了 GroupWithItemCountSingularFormatOrDefault 中的错误。
  • 修复了使用 RefreshObject() 时分组、所有者绘制的 OLV 中的奇怪闪烁。
  • 隔行颜色现在仅应用于 Details 视图(就像它们一直应该的那样)。
  • 删除对象后,隔行颜色现在会正确重新计算。
  • 虚拟列表上的 CheckedObjects 现在只返回当前在列表中的对象。
  • 虚拟列表上的 ClearObjects() 现在会重置所有选中状态信息。
  • 分组虚拟列表上的过滤不再行为异常。
  • ModelDropEventArgs.RefreshObjects() 现在在 TreeListViews 上也能正常工作。
  • 在 IDE 表单设计器中拖动列分隔符现在会正确调整列的大小。
  • 从过滤或排序的 FastObjectListView 中删除对象现在无需清除过滤器或排序即可工作。

2010 年 8 月 31 日 - 版本 2.4.1

新功能
  • 列标题改进:它们可以垂直渲染;它们可以显示图像;它们可以与单元格内容对齐方式不同(使用OLVColumn.HeaderTextAlign 属性)。
  • 组排序现在可以完全自定义,项目内部排序也可以。请参阅此配方
  • 改进了文本过滤功能,允许前缀匹配和完整正则表达式。
  • 子项复选框改进:复选框现在遵守列的 IsEditable 设置,可以热点,可以禁用。
  • 添加了 EditingCellBorderDecoration,以便更清楚地显示正在编辑的单元格
  • 添加了 OLVColumn.Wrap,以便轻松地对列单元格进行文本换行。
小调整
  • 在单元格之间切换时不再出现选择闪烁。
  • 添加了 ObjectListView.SmoothingMode 以控制所有图形操作的平滑度。
  • DLL 现在已签名。
  • 在单元格编辑之前和之后使控件无效,以确保其外观正确。
  • BuildList(true) 现在即使在显示组时也能保持垂直滚动位置。
  • CellEdit 验证和完成事件现在具有 NewValue 属性。
  • AllowExternalRearrangableDropSink 移至 SimpleDropSink 的层次结构上方,因为它可能普遍有用。
  • 添加了 ObjectListView.HeaderMaximumHeight 以限制标题部分的高度。
Bug 修复
  • 避免标准 ListView 中的错误,即虚拟列表在非 Details 视图中发送工具提示消息时会发送无效的项目索引。
  • 修复了 FastObjectListView 在除了 Details 之外的任何视图中显示超链接时会抛出异常的错误。
  • 修复了 ChangeToFilteredColumns() 中当列被隐藏时导致列显示顺序丢失的错误。
  • 修复了长期存在的 bug,即列数为 0 会导致 InvalidCast 异常。
  • 列现在会缓存其组项目格式字符串,以便在将其从列表视图中删除后,它们仍然可以作为分组列正常工作。此缓存值仅在列不属于列表视图时使用。
  • 当鼠标点击时,正确触发 Click 事件。
  • 右键单击复选框不再使它们混淆。
  • 修复了 FastObjectListViewTreeListView 中阻止对象被删除(或至少看起来被删除)的错误。
  • 避免标准 ListView 中当 FullRowSelecttrue 时,在非主列上 Shift + 点击时复选框混淆的错误。
  • OLVColumn.ValueToString() 现在总是返回一个 String(就像它一直应该的那样)。

2010 年 4 月 10 日 - 版本 2.4

新功能
  • 过滤。
  • 单元格、行或整个列表的动画(说起来很容易,做起来太多工作)。
  • 标题样式。这使得 HeaderFontHeaderForeColor 属性变得不必要。它们将在下一版本中标记为过时,并在此后移除。
  • [次要] Ctrl-A 现在选择所有行(这没什么好奇怪的)。将 SelectAllOnControlA 设置为 false 即可禁用。
  • [次要] Ctrl-C 将所有选定的行复制到剪贴板(就像以前一样),但现在可以通过将 CopySelectionOnControlC 设置为 false 来禁用此功能。
  • [次要] OLVColumn 属性(与 Generator 一起使用)现在允许指定创建列的 Name
Bug 修复
  • 改变了对象检查方式,以便对象在添加到列表之前可以预先选中。普通的 ObjectListViewsListViewItem 中管理“选中状态”,因此你仍然无法在对象添加到列表之前选中它们(除非已安装选中状态获取器和设置器)。它将在虚拟列表(因此是快速列表和树视图)上工作,因为它们管理自己的选中状态。
  • 覆盖层可以关闭(将 UseOverlays 设置为 false)。它们也只在 32 位显示器上绘制自己。
  • ObjectListView 的覆盖层现在与 MDI 更好地配合,但仍不完美。当 MDI 应用程序中使用 ObjectListView 覆盖层时,它不再崩溃,但仍然无法处理重叠的窗口。一个 ObjectListView 的覆盖层也会绘制在其他控件上方。当前的建议:不要在 MDI 应用程序中使用覆盖层。
  • F2 键按下不再静默吞噬。
  • ShowHeaderInAllViews 更好但并非完美。在创建控件之前设置它或将其设置为 true 都完美工作。但是,如果将其设置为 false,则主复选框会消失!我可以在创建控件后忽略更改,但最好让人们即时更改它并记录其特性。
  • 修复了组排序中的一个错误,使其现在确实使用 GroupByOrder(感谢 Michael Ehrt)。
  • 在鼠标事件期间销毁 ObjectListView(例如,在双击处理程序中关闭窗体)不再抛出“已处置对象”异常。
  • 避免标准 ListView 中当 FullRowSelecttrue 时,在非主列上 Shift + 点击时复选框混淆的错误。

2009 年 10 月 12 日 - 版本 2.3

此版本侧重于格式化——为程序员提供了更多玩转 ObjectListView 外观的机会。

装饰

装饰允许您在 ObjectListView 上放置漂亮的图像、文本和效果。

组标题格式

此版本对组进行了全面改进。XP 下的组保持不变,但在 Vista 和 Windows 7 下,现在提供了更多格式选项。

超链接

ObjectListViews 现在可以具有超链接单元格。

标题格式

现在可以更改 ObjectListView 标题的字体和文本颜色。您还可以对标题文本进行自动换行。

FormatRow 和 FormatCell 事件

在以前的版本中,RowFormatter 是更改行或单元格格式(字体/文本颜色/背景颜色)的认可方式。但它有一些限制:

  1. 它与 AlternateBackgroundColors 属性配合不佳。
  2. 它是在 OLVListItem 添加到 ObjectListView 之前调用的,因此它的许多属性尚未初始化。
  3. 使用它只格式化一个单元格非常痛苦。
  4. 也许最重要的是,程序员不知道该行在 ObjectListView 中将出现在哪里,因此他们无法实现更复杂的行交替背景颜色方案。

为了解决所有这些问题,现在有一个 FormatRow 事件。它在 OLVListItem 添加到控件后调用。此外,它还具有一个 DisplayIndex 属性,精确指定该行在列表中出现的位置(即使在显示组时也正确)。

还有一个 FormatCell 事件。这允许程序员轻松地只格式化一个单元格。

生成器

通过使用编译器属性,ObjectListViews 现在可以直接从模型类生成。[感谢 John Kohler 提供此想法和原始实现]

虚拟列表上的组

在 Vista 及更高版本上运行时,虚拟列表现在可以分组!FastObjectListView 开箱即用支持分组。对于你自己的 VirtualObjectListView,你必须自己做更多的工作。

[这对我来说更多的是一个技术挑战,而不是我认为会非常有用的东西。如果你确实在虚拟列表上使用组,请告诉我]

小改动
  • 添加了 UseTranslucentSelection 属性,它模拟了 Vista 中使用的选择高亮方案。这在 Vista 和 XP 上,当列表是 OwnerDrawn 时工作良好,但在非 OwnerDrawn 时效果一般,因为原生控件坚持绘制其正常的选择方案,此外还有半透明选择。
  • 添加了 ShowHeaderInAllViews 属性。当此属性为 true 时,标题在所有视图中都可见,而不仅仅是 Details 视图,并且可以用于控制项目的排序。
  • 添加了 UseTranslucentHotItem 属性,它在当前热点项目上方绘制一个半透明区域。
  • 添加了 ShowCommandMenuOnRightClick 属性,当右键单击标题时显示额外命令。此属性默认为 false。
  • 添加了 ImageAspectName,它是一个属性的名称,将用于获取应在列上显示的图像。这允许从模型中检索列的图像,而无需安装 ImageGetter 委托。
  • 添加了 HotItemChanged 事件和 Hot* 属性,允许程序员在鼠标移动到不同行或单元格时执行操作。
  • 添加了 UseExplorerTheme 属性,当其为 true 时,强制 ObjectListView 使用与资源管理器相同的视觉样式。在 XP 上,这不起作用,但在 Vista 上它会改变热点项目和选择机制。请注意:设置此属性会搞乱其他几个属性。请参阅 30. ObjectListView 可以使用类似 Vista 的选择方案吗?
  • 添加了 OLVColumn.AutoCompleteEditor,它允许您关闭单元格编辑器上的自动完成功能。
  • 即使 FullRowSelect 为 false,OlvHitTest() 现在也能正常工作。在 .NET ListView 中存在一个 bug,即对于位于第 0 列但不在文本或图标上方的点,HitTest() 会失败(即无法识别它位于第 0 列上方)。OlvHitTest() 没有这个缺陷。
  • 添加了 OLVListItem.GetSubItemBounds(),它甚至可以正确计算第 0 列单元格的边界。在 .NET ListView 中,任何子项 0 的边界始终是整个行的边界。
  • 第 0 列现在遵循其 TextAlign 设置,但仅在 OwnerDrawn 时。在普通 ListView 中,第 0 列总是左对齐。** 此功能是实验性的。如果您想使用它,请随意。如果它不起作用,请不要抱怨 Smile | <img src=" /> **
  • LastSortColumn 重命名为 PrimarySortColumn,这更好地表明了其用途。类似地,LastSortOrder 变为 PrimarySortOrder
  • 单元格编辑器在使用后不再被强制处置。这允许它们被缓存和重用。
  • 重新实现了 OLVListItem.Bounds,因为如果给定项属于已折叠组,则基础版本会抛出异常。
  • 移除了对 Mono 的偶数令牌支持。
  • 移除了 IncrementalUpdate() 方法,该方法已于 2008 年 2 月标记为已过时。

8 月 4 日 - 版本 2.2.1

这主要是一个错误修复版本。

新功能
  • 添加了单元格事件 (CellClickedCellOverCellRightClicked)。
  • 使 BuildList()AddObject()RemoveObject() 线程安全。
Bug 修复
  • 避免了 .NET 框架中的一个 bug,即当列表视图水平滚动时,所有者绘制的列表视图的第 0 列未重绘(这花费了大量精力来跟踪和修复!)
  • 子项编辑矩形总是为单元格中的图像留出空间,即使没有图像。现在,它们只在实际有图像时才为图像留出空间。
  • 当列表视图水平滚动时,单元格编辑矩形现在可以正确计算。
  • 如果用户单击/双击树列表单元格,如果单击位于展开器左侧,则不再开始编辑操作。这种实现方式使得其他渲染器也可以有类似的“死区”。
  • CalculateCellBounds() 影响了 FullRowSelect 属性,这使得底层控件上的工具提示处理混乱。它不再这样做。
  • 现在,对于所有者绘制的非详细信息视图,单元格编辑矩形可以正确计算。
  • 空格键现在可以正确切换选定行的选中状态。
  • 修复了当底层 Windows 控件被销毁时工具提示的 bug。
  • CellToolTipShowing 事件现在在所有视图中触发。

2009 年 5 月 15 日 - 版本 2.2

此版本的两大功能是拖放支持和图像叠加。

拖放支持

ObjectListViews 现在对拖放操作具有完善的支持。通过设置 DragSource 属性,可以将 ObjectListView 设置为拖动操作的源。类似地,通过设置 DropSink 属性,可以将其设置为放置操作的接收器。拖动基于 IDragSource 接口,放置处理围绕 IDropSink 接口。SimpleDragSourceSimpleDropSink 为这些接口提供了合理的默认实现。

由于 ObjectListView 的整个目标是鼓励懒惰,因此对于大多数简单情况,您可以忽略这些细节,只需将 IsSimpleDragSourceIsSimpleDropSink 属性设置为 true,然后侦听 CanDropDropped 事件。

可重排列表通过 RearrangeableDropSink 类支持。

图像和文本叠加

此版本增加了在 ObjectListView 内容上方绘制半透明图像和文本的功能。这些叠加层不会随列表内容滚动而滚动。这些叠加层适用于所有 Views。您可以使用 OverlayImageOverlayText 属性在 IDE 中设置叠加图像。叠加层设计是可扩展的,您可以通过 AddOverlay() 方法添加任意叠加层。

其他新功能
  • 有史以来最受要求的功能——可折叠组——现已可用(但仅限于 Vista)。
  • 用于显示单元格工具提示和标题工具提示的工具提示控件现在通过 CellToolTipControlHeaderToolTipControl 属性公开。通过这些属性,您可以自定义工具提示的显示方式。您还可以侦听 CellToolTipShowingHeaderToolTipShowing 事件,以根据单个单元格自定义工具提示。
  • 添加了 SelectedColumn 属性,该属性会使该列略微着色。当与 TintSortColumnSelectedColumnTint 属性结合使用时,排序列将自动着色为您想要的任何颜色。
  • 添加了 Scroll 事件(感谢 Christophe Hosten 实现此功能)
  • 该项目不再使用不安全代码,因此可以在有限信任环境中使用。
  • 使多个属性可本地化。
Bug 修复(不完整列表)
  • 修复了所有者绘制的虚拟列表闪烁的长期问题。除了现在无闪烁之外,这意味着网格线不再混乱,拖选也不再闪烁。这意味着 TreeListView 现在闪烁明显减少(它始终是一个所有者绘制的虚拟列表)。
  • 双击一行不再切换复选框(MS 为什么会包含这个?)。
  • 双击复选框不再使复选框混乱。
  • RowHeight 非标准时,正确渲染复选框。
  • 即使 ObjectListView 没有 SmallImageList,复选框也可见。

2009 年 2 月 23 日 - 版本 2.1

所有者绘制的全面改进

就像 2.0 版改进了虚拟列表处理一样,此版本完全重构了所有者绘制的渲染过程。然而,这种改进是为了透明地向后兼容。

唯一的重大更改是针对所有者绘制的非详细信息视图(我怀疑除了我之外,没有人使用过)。以前,列 0 上的渲染器兼顾渲染单元格 0 和在非详细信息视图中渲染整个项。现在,第二项职责明确属于 ItemRenderer 属性。

  • 渲染器现在基于 IRenderer 接口。
  • 渲染器现在是组件,可以在 IDE 中创建、配置和分配。
  • 渲染器现在也可以进行命中测试。
  • 所有者绘制的文本现在看起来像原生 ListView
  • 文本和位图现在遵循列的对齐方式。以前只有文本对齐。
  • 添加了 ItemRenderer 以处理非详细信息所有者绘制。
  • 图像现在尽可能直接从图像列表绘制。比以前的版本快 30%。
其他重大更改
  • 添加了热跟踪。
  • 添加了子项复选框。
  • AspectNames 现在可以用作模型对象的索引——实际上类似于:modelObject[this.AspectName]。这对于 DataListView 特别有用,因为 DataRowsDataRowViews 支持这种类型的索引。
  • 添加了 EditorRegistry 以更容易更改或添加单元格编辑器。
次要更改
  • 添加了 TriStateCheckBoxesUseCustomSelectionColorsUseHotItem 属性。
  • 添加了 TreeListView.RevealAfterExpand 属性。
  • VirtualObjectListViews(包括 FastObjectListViewsTreeListViews)现在触发 ItemCheckItemChecked 事件。
  • 在单元格中编辑枚举时,会创建一个 ComboBox,显示所有可能的值。
  • 将模型比较更改为使用 Equals() 而不是 ==。这允许模型对象实现自己的相等概念。
  • ImageRenderer 现在可以处理多个图像。这使得 ImagesRenderer 变得过时。
  • FlagsRenderer<T> 不再是泛型的。它只是 FlagsRenderer
Bug 修复
  • RefreshItem() 现在可以正确地重新计算背景颜色。
  • 修复了简单复选框的 bug,这意味着 CheckedObjects 总是返回空。
  • 当视觉样式被禁用时,TreeListView 现在可以工作。
  • DataListView 现在更好地处理布尔类型。当数据源重新设置时,它也不再崩溃。
  • 修复了 AlwaysGroupByColumn 的 bug,即列标题单击不会重新排序组。
  • 如果可能,焦点项在 BuildList() 期间保留。

2009 年 1 月 10 日 - 版本 2.0.1

此版本添加了一些小功能,并修复了 2.0 版本中的一些 bug。

新功能或更改的功能
  • 添加了 ObjectListView.EnsureGroupVisible()
  • 添加了 TreeView.UseWaitCursorWhenExpanding 属性。
  • 使所有公共和受保护方法成为虚拟方法,以便它们可以在子类中被覆盖。在 TreeListView 中,一些类从 internal 更改为 protected,以便子类可以访问它们。
  • 使 TreeRenderer 公开,以便可以对其进行子类化。
  • ObjectListView.FinishCellEditing()ObjectListView.PossibleFinishCellEditing()ObjectListView.CancelCellEditing() 现在是公共的。
  • 添加了 TreeRenderer.LinePen 属性,以允许更改连接绘制笔。
Bug 修复
  • 修复了长期存在的“生成多列”问题。感谢 pinkjones 在解决这个问题方面的帮助!
  • 修复了 TreeListView 上只有一个根时的连接线问题。
  • HideSelectiontrue 时,所有者绘制的文本现在可以正确渲染。
  • 修复了一些渲染问题,其中文本高亮矩形计算错误。
  • 修复了当组键为 null 时组比较的 bug。
  • 修复了填充列和布局事件的 bug。
  • 修复了 RowHeight,使其只改变行高,而不改变图像宽度。
  • 即使 TreeListView 没有 SmallImageList,它现在也可以工作。

2008 年 11 月 30 日 - 版本 2.0

版本 2.0 是 ObjectListView 的重大更改。

主要更改
  • 添加了 TreeListView,它将树结构与 ListView 上的列结合在一起。
  • 添加了 TypedObjectListView,它是 ObjectListView 的类型安全包装器。
  • VirtualObjectListView 进行了重大改进,现在使用 IVirtualListDataSource。新版本的 FastObjectListView 和新的 TreeListView 都使用了这种新结构。
  • ObjectListView 构建为 DLL,然后可以合并到您的 .NET 项目中。这使得从其他 .NET 语言(包括 VB)使用它变得更加容易。
  • ListViewPrinter 与 IDE 的交互得到了很大改进。所有 PensBrushes 现在都可以通过 IDE 指定。
  • 支持三态复选框,即使是虚拟列表。
  • 通过 CellToolTipGetterHeaderToolTipGetter 委托,分别支持单元格和列标题的动态工具提示。
  • 将 ObjectListView.cs 分裂成多个文件,希望能使代码更容易理解。
  • 添加了许多新事件,包括 BeforeSortingAfterSorting
  • 使用 TypedObjectListView.GenerateAspectGetters()AspectNames 生成动态方法。具有手写 AspectGetters 的速度,而无需手写。这是此版本中最具实验性的部分。感谢 Craig Neuwirt 的初步实现。
次要更改
  • 添加了 CheckedAspectName,允许在不需要任何代码的情况下获取和设置复选框。
  • 现在,默认情况下,即使在普通的 ObjectListViews 上,键入列表也会在排序列中搜索值。以前此行为仅在虚拟列表中可用,并且默认情况下是关闭的。将 IsSearchOnSortColumn 设置为 false 以恢复到 v1.x 行为。
  • 所有者绘制的主列现在可以正确渲染复选框(以前即使 CheckBoxes 属性为 true,复选框也不会被绘制)。
破坏性更改
  • CheckStateGetterCheckStatePutter 现在只使用 CheckState,而不是同时使用 CheckStatebooleans。使用 BooleanCheckStateGetterBooleanCheckStatePutter 以实现与 v1.x 兼容的行为。
  • FastObjectListViews 不再可以拥有 CustomSorter。在 v1.x 中,如果棘手,可以使 CustomSorterFastObjectListView 一起工作,但在 v2.0 中已不再可能。在 v2.0 中,如果您想自定义排序 FastObjectListView,您必须子类化 FastObjectListDataSource 并覆盖 SortObjects() 方法。有关示例,请参见此处

2008 年 7 月 24 日 - 版本 1.13

主要更改
  • 允许 FastObjectListViews 上有复选框。.NET 的 ListView 不支持虚拟列表上的复选框。我们无法为普通的 VirtualObjectListViews 解决此限制,但我们可以为 FastObjectListViews 解决。这是一项重要的工作,我可能遗漏了错误。此实现不修改传统的 CheckedIndicies/CheckedItems 属性,这些属性仍然会失败。它使用新的 CheckedObjects 属性作为访问已检查行的方式。一旦在 FastObjectListView 上设置了 CheckBoxes,尝试再次将其关闭将抛出异常。
  • 现在有一个 CellEditValidating 事件,允许在单元格编辑器失去焦点之前对其进行验证。如果验证失败,单元格编辑器将保留。以前的版本无法阻止单元格编辑器失去焦点。感谢 Artiom Chilaru 的想法和初步实现。
  • 允许更改选择前景色和背景色。Windows 不允许自定义这些颜色,因此我们只能在 ObjectListView 为所有者绘制时执行此操作。要查看实际效果,请设置 HighlightForegroundColorHighlightBackgroundColor 属性,然后调用 EnableCustomSelectionColors()
  • 添加了 AlwaysGroupByColumnAlwaysGroupBySortOrder 属性,强制列表视图始终按特定列分组。
次要改进
  • 添加了 CheckObject() 及其所有相关方法,以及 CheckedObjectCheckedObjects 属性。
  • 添加了 LastSortColumnLastSortOrder 属性。
  • 使 SORT_INDICATOR_UP_KEYSORT_INDICATOR_DOWN_KEY 公开,以便它们可以用于指定排序时列标题上使用的图像。
  • 将更常用的 CopyObjectsToClipboard() 方法从 CopySelectionToClipboard() 中分离出来。现在,CopyObjectsToClipboard() 可以用于,例如,将所有已检查的对象复制到剪贴板。
  • 类似地,构建列选择上下文菜单与显示该上下文菜单是分开的。这是为了让外部代码可以使用菜单构建方法,然后在显示菜单之前进行任何所需的修改。上下文菜单的构建现在由 MakeColumnSelectMenu() 处理。
  • VirtualObjectListView 添加了 RefreshItem(),以便刷新对象实际上会做一些事情。
  • AddObject(s)/RemoveObject(s) 方法中始终使用写时复制语义。以前,如果 SetObjects() 给出 ArrayList,则该列表将由 Add/RemoveObject(s) 方法直接修改。现在,总是进行复制和修改,使原始集合保持不变。
Bug 修复(不完整列表)
  • 修复了虚拟列表上 GetItem() 的一个 bug,即返回的项并非总是完整的。
  • 修复了一个 bug/限制,阻止 ObjectListViewUserControl 中使用时响应右键单击(感谢 Michael Coffey)。
  • 纠正了列表中的最后一个对象无法通过 SelectedObject 选中的 bug。
  • 修复了 GetAspectByName() 中的一个 bug,其中如果中间某个方面返回 null,则链式方面会崩溃(感谢 philippe dykmans)。

2008 年 5 月 10 日 - 版本 1.12

  • 添加了 AddObject/AddObjects/RemoveObject/RemoveObjects 方法。这些方法允许程序员从 ObjectListView 中添加和删除特定的模型对象。这些方法适用于 ObjectListViewFastObjectListView。它们对 DataListViewVirtualObjectListView 没有影响,因为这两个数据源都不受 ObjectListView 控制。
  • 非详细信息视图现在可以由所有者绘制。主列的渲染器有机会渲染整个项。有关示例,请参阅演示中的 BusinessCardRenderer。在演示中,转到“复杂”选项卡,打开“所有者绘制”,然后切换到“平铺视图”以查看实际效果。
  • 重大更改。RenderDelegate 的签名已更改。它现在返回一个 boolean 以指示是否应执行默认渲染。此 delegate 以前返回 void。这仅在您的代码直接使用 RendererDelegate 时才重要。从 BaseRenderer 派生的渲染器没有更改。
  • TopItemIndex 属性现在适用于虚拟列表。
  • MappedImageRenderer 现在将渲染一组值。
  • 修复了所需数量的错误。
    • 即使在 ObjectListView 上安装了上下文菜单,当右键单击标题时,列选择菜单也会出现。
    • 在非详细信息视图中编辑主列时按 Tab 键不再尝试编辑新列的值。
    • 当垂直滚动的虚拟列表被清除时,底层 ListView 会对滚动位置感到困惑,并在之后错误地渲染项。ObjectListView 现在避免了这个问题。

2008 年 5 月 1 日 - 版本 1.11

  • 添加了 SaveState()RestoreState()。这些方法保存和恢复 ObjectListView 的用户可修改状态。它们对于在应用程序运行之间保存和恢复 ObjectListView 的状态非常有用。有关如何使用它们的示例,请参阅演示。
  • 添加了 ColumnRightClick 事件。
  • 添加了 SelectedIndex 属性。
  • 添加了 TopItemIndex 属性。由于底层 ListView 控件的问题,此属性有几个怪癖和限制。请参阅属性本身的文档。
  • 调用 BuildList(true) 现在将尝试保留滚动位置和选择(不幸的是,在显示组时无法保留滚动位置)。
  • ObjectListView 现在符合 CLS 标准。
  • 各种错误修复。特别是,ObjectListView 现在应该在 64 位版本的 Windows 上完全正常运行。

2008 年 3 月 18 日 - 版本 1.10

  • 添加了填充列。填充列填充了其他列未占据的全部(或部分)宽度。
  • 添加了 Chris Marlowe 建议的一些方法:ClearObjects()GetCheckedObject()GetCheckedObjects()、一种返回点下项和列的 GetItemAt() 变体。感谢 Chris 的建议。
  • 添加了对 Mono 的最小支持。要创建 Mono 版本,请使用条件编译符号“MONO”进行编译。Mono 下的 Windows.Forms 支持仍在开发中——listview 仍然存在一些严重问题(我说的是虚拟模式)。如果您在 Mono 上取得了成功,我很高兴包含您可能做出的任何修复(特别是来自 Linux 或 Mac 程序员)。请不要问我 Mono 问题。
  • 修复了在使用所有者绘制列表和 RowFormatter 时子项颜色不正确的 bug。

2008 年 2 月 2 日 - 版本 1.9.1

  • 为所有不耐烦的程序员添加了 FastObjectListView
  • 添加了 FlagRenderer 以帮助绘制按位或的标志(在演示项目中搜索 FlagRenderer 以查看示例)。
  • 修复了不可避免的错误。
    • 带有组的交替行着色略有偏差。
    • 在某些情况下,所有者绘制的虚拟列表会使用 100% 的 CPU。
    • 确保在更改可见列后正确显示排序指示器。

2008 年 1 月 16 日 - 版本 1.9

  • 添加了隐藏列的功能,即 ObjectListView 知道但用户不可见的列。这由 OLVColumn.IsVisible 控制。我在演示项目中添加了 ColumnSelectionForm,以展示它在应用程序中的使用方式。此外,右键单击列标题将允许用户选择哪些列可见。将 SelectColumnsOnRightClick 设置为 false 以防止此行为。
  • 添加了 CopySelectionToClipboard(),它将所选行的文本和 HTML 表示粘贴到剪贴板。默认情况下,这绑定到 Ctrl-C。
  • 通过 CheckStateGetterCheckStatePutter 属性添加了对复选框的支持。有关如何使用的示例,请参阅 ColumnSelectionForm
  • 添加了 ImagesRenderer 以在列中绘制多个图像。
  • ObjectListViewOLVColumn 制作成部分类,以便其他人可以扩展它们。
  • 添加了实验性的 IncrementalUpdate() 方法,其操作类似于 SetObjects(),但不改变滚动位置、选择或排序顺序。而且它没有一点闪烁。适用于定期更新的列表。[最好使用 FastObjectListViewObjects 属性]
  • 修复了所需数量的小错误。

2007 年 11 月 30 日 - 版本 1.8

  • 添加了单元格编辑——说起来容易,做起来却费力。
  • 添加了 SelectionChanged 事件,该事件在每次用户操作时触发一次,无论选择或取消选择多少项。相比之下,SelectedIndexChanged 事件在选择或取消选择的每个项上触发。因此,如果选择了 100 项,并且用户单击不同的项以仅选择该项,则将触发 101 个 SelectedIndexChanged 事件,但只有一个 SelectionChanged 事件。感谢 lupokehl42 的此建议和改进。
  • 添加了在主排序列为两行提供相同排序值时使用次要排序列的功能。有关详细信息,请参阅 SecondarySortColumnSecondarySortOrder 属性。这些项没有用户界面——它们必须由程序员设置。
  • 对于所有使用希伯来语和阿拉伯语的用户,ObjectListView 现在在所有者绘制模式下正确处理 RightToLeftLayout(尽管仍在努力使 ListViewPrinter 工作)。感谢 dschilo 的帮助和意见。

2007 年 11 月 13 日 - 版本 1.7.1

  • 修复了所有者绘制代码中的 bug,其中选定项的文本背景色计算不正确。
  • 修复了 ListViewPrinter 和所有者绘制模式之间的错误交互。

2007 年 11 月 7 日 - 版本 1.7

  • 添加了使用 ListViewPrinter 打印 ObjectListViews 的功能。

2007 年 10 月 30 日 - 版本 1.6

  • 主要更改
    1. 添加了为每个列设置最小和最大宽度的功能(将最小宽度设置为等于最大宽度以创建固定宽度列)。感谢 Andrew Philips 的建议和意见。
    2. DataListView 进行了全面改进,使其现在成为一个功能齐全、可数据绑定的控件。这基于 Ian Griffiths 的出色示例,该示例应该在此处可用:这里,但不幸的是似乎已从 Web 上消失。感谢 ereigo 在调试此新代码方面提供了重要帮助。
    3. 添加了当 ListView 为空时(显然)显示“此列表为空”类型消息的功能。这由 EmptyListMsgEmptyListMsgFont 属性控制。请查看演示中的“文件浏览器”选项卡,以查看其外观。
  • 次要更改
    1. 添加了在调用 BuildList() 时保留选择的功能。默认情况下,此功能已启用。
    2. 添加了 GetNextItem()GetPreviousItem() 方法,这些方法按顺序遍历 ListView 项,即使视图已分组(感谢 eriego 的建议)。
    3. 允许按列设置组上的项计数标签(感谢 cmarlow 的想法)。
    4. 添加了 SelectedItem 属性以及 GetColumn()GetItem() 方法。
    5. 优化了方面到 string 的转换。BuildList() 速度提高了 15%。
    6. 纠正了 VirtualObjectListView 中自定义排序器的 bug(感谢 mpgjunky)。
    7. 纠正了 DrawAlignedImage() 中的图像缩放 bug(感谢 krita970)。
    8. 在 Windows XP 或更高版本上使用内置排序指示器(感谢 gravybod 的示例实现)。
    9. 以及所需数量的小错误修复。

2007 年 8 月 3 日 - 版本 1.5

  • ObjectListView 现在有一个 RowFormatter delegate。每当添加或刷新 ListItem 时,都会调用此 delegate。这允许更改项及其子项的格式以适应显示的数据,例如会计软件中负数的红色。演示中的 DataView 选项卡有一个 RowFormatter 实际操作的示例。在单元格的值中包含以下任何单词,看看会发生什么:red、blue、green、yellow、bold、italic、underline、bk-red、bk-green。请注意,使用 RowFormatter 并尝试为行提供交替颜色背景可能会产生意外结果。通常,RowFormatterUseAlternatingBackColors 不兼容。
  • ObjectListView 现在有一个 RowHeight 属性。将其设置为整数值,ListView 中的行将是该高度。普通的 ListView 不允许指定行高;它根据小图像列表的大小和 ListView 字体计算。RowHeight 属性通过隐藏小图像列表来覆盖此计算。此功能应被视为高度实验性的。一个已知问题是,如果您在垂直滚动条不为零时更改行高,则控件的渲染会变得混乱。
  • 动画 GIF 支持:如果您将动画 GIF 作为 Image 提供给具有 ImageRenderer 的列,则 GIF 将被动画。与所有渲染器一样,这仅在 OwnerDrawn 模式下有效。有关示例,请参阅演示中的 DataView 选项卡。
  • 排序指示器现在可以禁用,因此您可以在列标题上放置自己的图像。
  • 更好地处理只有单个成员的组的项计数:感谢 cmarlow 的建议和示例实现。
  • 例行的小错误修复。

2007 年 4 月 30 日 - 版本 1.4

  • 所有者绘制和渲染器。
  • ObjectListView 现在支持所有 ListView.View 模式,而不仅仅是 Details。平铺视图有其内置支持。
  • 列标题现在显示排序指示器。
  • 方面名称可以使用“点”语法链接。例如,Owner.Workgroup.Name 现在是一个有效的 AspectName。感谢 OlafD 的此建议和示例实现。
  • ImageGetter 委托现在可以返回 intstringImage 对象,而不仅仅是以前版本中的 intintstring 用作图像列表的索引。图像仅在 OwnerDrawn 模式下显示。
  • 添加了 OLVColumn.MakeGroupies() 以简化组分区。

2007 年 4 月 5 日 - 版本 1.3

  • 添加了 DataListView
  • 添加了 VirtualObjectListView
  • 添加了 Freeze()/Unfreeze()/Frozen 功能。
  • 添加了将排序交给 CustomSorter delegate 的功能。
  • 修复了未排序列表的交替行着色中的 bug:感谢 cmarlow 发现此问题。
  • 更好地处理 null 条件,例如 SetObjects(null) 或零列。
  • 简化了排序比较策略。以前的策略是经典的过度设计:用户可扩展,处理所有可能的情况,并且对非专业人士来说难以理解。更简单的解决方案处理 98% 的情况,完全显而易见,并且在 6 行代码中实现。

2007 年 1 月 5 日 - 版本 1.2

  • 添加了交替行颜色。
  • 在构建列表之前取消设置排序器。速度提高了 10 倍!感谢 aaberg 发现此问题。
  • 小错误修复。

2006 年 10 月 26 日 - 版本 1.1

  • 添加了“数据不可知”和“IDE 集成”文章部分。
  • 添加了模型对象级别操作方法,例如 SelectObject()GetSelectedObjects()
  • 改进了 IDE 集成。
  • 重构了排序比较以消除令人讨厌的 if...else 级联。

2006 年 10 月 14 日 - 版本 1.0

许可说明

此代码受 GNU 通用公共许可证 v3 保护。

 

 

 

© . All rights reserved.