ImageListView






4.93/5 (144投票s)
一个 .NET ListView 控件,用于显示带有异步加载缩略图的图像文件。

引言
ImageListView
是一个 .NET 2.0 控件,用于显示图像文件列表。它的外观和操作方式与标准的 ListView
控件类似。图像缩略图在单独的后台线程中异步加载。可以通过自定义渲染器完全定制控件的外观。
背景
这个项目最初是一个所有者绘制的 ListView
。然而,第一个版本需要过多的技巧。特别是确定第一个/最后一个可见项被证明是一个挑战。在进行到一半时,我决定自己编写一个控件。于是 ImageListView
就诞生了。
Using the Code
要使用该控件,请将 ImageListView
添加到您的控件工具箱,然后将其拖放到窗体上。之后,您可以通过更改视图模式(缩略图、画廊、窗格或详细信息)、缩略图大小、列标题等来定制控件的外观。
自定义渲染
ImageListViewRenderer
类负责绘制控件。这是一个 public
类,具有可以被派生类重写的虚函数。派生类可以修改项和列标题的显示大小,并绘制控件的任何或所有部分。

这是产生此外观的渲染器
public class DemoRenderer : ImageListView.ImageListViewRenderer
{
// Returns item size for the given view mode.
public override Size MeasureItem(View view)
{
if (view == View.Thumbnails)
{
Size itemPadding = new Size(4, 4);
Size sz = ImageListView.ThumbnailSize + ImageListView.ItemMargin +
itemPadding + itemPadding;
return sz;
}
else
return base.MeasureItem(view);
}
// Draws the background of the control.
public override void DrawBackground(Graphics g, Rectangle bounds)
{
if (ImageListView.View == View.Thumbnails)
g.Clear(Color.FromArgb(32, 32, 32));
else
base.DrawBackground(g, bounds);
}
// Draws the specified item on the given graphics.
public override void DrawItem(Graphics g, ImageListViewItem item,
ItemState state, Rectangle bounds)
{
if (ImageListView.View == View.Thumbnails)
{
// Black background
using (Brush b = new SolidBrush(Color.Black))
{
Utility.FillRoundedRectangle(g, b, bounds, 4);
}
// Background of selected items
if ((state & ItemState.Selected) == ItemState.Selected)
{
using (Brush b = new SolidBrush(Color.FromArgb(128,
SystemColors.Highlight)))
{
Utility.FillRoundedRectangle(g, b, bounds, 4);
}
}
// Gradient background
using (Brush b = new LinearGradientBrush(
bounds,
Color.Transparent,
Color.FromArgb(96, SystemColors.Highlight),
LinearGradientMode.Vertical))
{
Utility.FillRoundedRectangle(g, b, bounds, 4);
}
// Light overlay for hovered items
if ((state & ItemState.Hovered) == ItemState.Hovered)
{
using (Brush b =
new SolidBrush(Color.FromArgb(32, SystemColors.Highlight)))
{
Utility.FillRoundedRectangle(g, b, bounds, 4);
}
}
// Border
using (Pen p = new Pen(Color.FromArgb(128, SystemColors.Highlight)))
{
Utility.DrawRoundedRectangle(g, p, bounds.X, bounds.Y, bounds.Width - 1,
bounds.Height - 1, 4);
}
// Image
Image img = item.ThumbnailImage;
if (img != null)
{
int x = bounds.Left + (bounds.Width - img.Width) / 2;
int y = bounds.Top + (bounds.Height - img.Height) / 2;
g.DrawImageUnscaled(item.ThumbnailImage, x, y);
// Image border
using (Pen p = new Pen(Color.FromArgb(128, SystemColors.Highlight)))
{
g.DrawRectangle(p, x, y, img.Width - 1, img.Height - 1);
}
}
}
else
base.DrawItem(g, item, state, bounds);
}
// Draws the selection rectangle.
public override void DrawSelectionRectangle(Graphics g, Rectangle selection)
{
using (Brush b = new HatchBrush(
HatchStyle.DarkDownwardDiagonal,
Color.FromArgb(128, Color.Black),
Color.FromArgb(128, SystemColors.Highlight)))
{
g.FillRectangle(b, selection);
}
using (Pen p = new Pen(SystemColors.Highlight))
{
g.DrawRectangle(p, selection.X, selection.Y,
selection.Width, selection.Height);
}
}
}
编写自己的渲染器后,需要将其分配给 ImageListView
。
imageListView1.SetRenderer(new DemoRenderer());
异步操作
ImageListView
通过后台线程异步生成缩略图。生成的缩略图保存在一个由 ImageListViewCacheManager
类管理的缓存中。缓存管理器有两种操作模式。可以使用 CacheMode
属性在两种模式之间切换控件。
在 OnDemand
模式下,缩略图仅在被请求后生成。例如,当用户滚动视图时,新可见的项将从 ImageListViewCacheManager
请求其缩略图。然后,缓存管理器会将这些项添加到队列中,该队列由工作线程监控和消耗。用户可以限制要保留在缓存中的缩略图数量。当达到此限制时,缓存管理器将从缓存中删除一些缩略图以释放空间。此模式适用于使用大量(数千个)图像文件的控件。
另一种缓存模式是 Continuous
。在此模式下,控件将持续生成和缓存图像缩略图,而不管项是否可见。在此模式下,无法限制缓存大小。此模式可能最适合使用中等数量项的控件。
关注点
性能
ImageListView
被设计用于处理大量图像。为了在数千个图像文件的处理中保持流畅的操作,我必须进行一些优化。
合并控件绘制
上面提到的 ImageListViewRenderer
类负责绘制控件的客户区。我确保在控件需要刷新时,渲染器仅绘制可见的项。之后我做的一个优化是添加了 SuspendPaint
和 ResumePaint
函数。它们用于在控件连续刷新多次时合并渲染请求。以下示例应能阐明其用法
// Adds a range of items to the ImageListViewItemCollection.
public void AddRange(ImageListViewItem[] items)
{
// Suspend the renderer while items are being added.
mImageListView.Renderer.SuspendPaint();
// Each item addition will request the control to refresh itself.
// But since the renderer is suspended, the control will not be
// refreshed at all.
foreach (ImageListViewItem item in items)
Add(item);
// Resume the renderer. This will also refresh the control if any
// refresh requests were made between SuspendPaint/ResumePaint
// calls.
mImageListView.Renderer.ResumePaint();
}
如下所示,实现非常简单
// Suspends painting until a matching ResumePaint call is made.
internal void SuspendPaint()
{
if (suspendCount == 0) needsPaint = false;
suspendCount++;
}
// Resumes painting. This call must be matched by a prior
// SuspendPaint call.
internal void ResumePaint()
{
suspendCount--;
// Render the control if we received refresh requests
// between SuspendPaint/ResumePaint calls.
if (needsPaint)
Refresh();
}
// Redraws the control.
internal void Refresh()
{
// Render the control only after we exit the final
// suspend block.
if (suspendCount == 0)
mImageListView.Refresh();
else
needsPaint = true;
}
上面的 suspendCount
变量在调用 SuspendPaint
时递增,在调用 ResumePaint
时递减。这允许嵌套调用挂起,并且控件将在最外层的 ResumePaint
调用后并且 suspendCount
递减为零后才刷新。
缓存文件属性
在详细信息模式下,ImageListView
显示图像文件的详细信息:例如修改日期、文件大小、文件类型等。文件属性在创建项时被读取和缓存。它们仅在文件名更改时更新。我在这里识别出的一个瓶颈是文件类型检索代码。.NET Framework 没有获取文件类型的本地函数,所以我不得不使用平台调用。这是我的做法
// Get file type via platform invoke
SHFILEINFO shinfo = new SHFILEINFO();
SHGetFileInfo(path,
(FileAttributes)0,
out shinfo,
(uint)Marshal.SizeOf(shinfo),
SHGFI.TypeName
);
typeName = shinfo.szTypeName;
这是用此方法添加 1000 个项所需的时间
Added 1000 items in 1282 milliseconds.
加载一千个项需要一秒钟,这实际上听起来还不错。但回顾上述代码,我意识到文件类型可以被记住。大多数情况下,添加到控件中的所有图像都是 JPEG 图像,文件类型检索只需要调用一次。这是修改后的代码
// cachedFileTypes is the dictionary to memorize
// file types.
if (!cachedFileTypes.TryGetValue(Extension, out typeName))
{
SHFILEINFO shinfo = new SHFILEINFO();
SHGetFileInfo(path,
(FileAttributes)0,
out shinfo,
(uint)Marshal.SizeOf(shinfo),
SHGFI.TypeName
);
typeName = shinfo.szTypeName;
cachedFileTypes.Add(Extension, typeName);
}
添加字典来记住文件类型后,我得到了这个结果
Added 1000 items in 138 milliseconds.
读取嵌入的 EXIF 缩略图
现代数码相机嵌入每张拍摄照片的缩略图。ImageListViewCacheManager
可以提取这些嵌入的图像以加快缩略图加载时间。为此,我需要一种**快速**的方法来提取嵌入的缩略图。我尝试了 GetThumbnailImage
方法,也尝试手动读取 ThumbnailData
Exif 标签;这两种方法对我来说都太慢了。瓶颈是 Image.FromStream
方法。加载一个 3472x2604 1 MB JPEG 文件所需的平均时间是
Reading a 3472x2604 JPEG file: 320.2 milliseconds.
由此看来,缓存一千个缩略图(这是我为 ImageListView
制定的最低性能目标)所需的时间将是 300 秒,即 5 分钟。我需要一种更快速的方法来读取嵌入的缩略图。进一步搜索,我偶然发现了 Image.FromStream
函数的一个特定重载
public static Image FromStream(
Stream stream,
bool useEmbeddedColorManagement,
bool validateImageData
)
使用此重载,将 validateImageData
设置为 false
会大大加快图像加载速度,因为框架不会验证图像数据。这是使用此重载重复上述实验的结果
Reading a 3472x2604 JPEG file: 0.47 milliseconds.
您没有看错,0.47 毫秒。使用此方法缓存一千个缩略图将需要 0.5 秒。尽管这种(近千倍的)性能提升非常有吸引力,但在使用此方法之前,需要考虑一些问题
- 正如参数名称所示,您在此方法中使用的是未经验证的图像数据,如果图像数据碰巧损坏,可能会导致错误。例如,如果您在任何
Graphics.DrawImage
函数中使用无效图像,GDI 可能会抛出异常。这对我来说不是问题,因为我根本不需要图像数据,只需要ThumbnailData
Exif 标签。 - 第二个问题并非特定于此方法,而是与
Image.FromStream
的一般用法有关。您必须在图像的整个生命周期内持有源流;这在某些情况下可能不切实际。这对我来说也不是问题,因为我复制了ThumbnailData
Exif 标签的内容,并立即处置了源图像和流。 - 您可能会倾向于在
try/catch
块中使用此方法以安全地受益于性能提升。但是我的直觉是,不出现异常并不意味着图像数据是有效的。如果您必须确保获得有效图像,唯一的方法是让框架验证图像数据。
总之,如果您需要一种快速读取图像属性(尺寸、Exif 标签等)的方法,请使用此方法。如果您需要实际的图像数据,请使用较慢的方法,并让框架验证图像。
自定义 CodeDom 序列化器
在这个项目的过程中,我学到了很多东西。在源代码中,您会找到一个自定义的列标题编辑器、一个自定义设计器和一个设计器序列化器。我认为设计器序列化器是最有趣的,所以我会简要介绍一下。
您可能已经注意到,当您将控件拖到窗体上时,初始化代码会神奇地出现在 InitializeComponent
中。大多数情况下,默认的序列化行为就足够了。对于 ImageListView
,情况并非如此。ImageListView
的列标题集合是一个只读列表,没有 Add
方法。用户不能添加或删除列,但可以显示/隐藏列、更改显示顺序、列文本和宽度。我有一个方法可以允许用户一次性自定义列的所有属性。
public void SetColumnHeader(ColumnType type, string text,
int width, int displayIndex, bool visible)
{
// ....
}
我想让设计器使用此函数生成我的列初始化代码,而不是集合的标准 Add
方法。为此,我编写了一个新的派生自 CodeDomSerializer
的设计器序列化器类,并使用 DesignerSerializer
属性将其分配给 ImageListView
,如下所示
[DesignerSerializer(typeof(ImageListViewSerializer), typeof(CodeDomSerializer))]
我的 CodeDomSerializer
派生类重写了 Serialize
方法,并添加了我自定义的列初始化代码。
internal class ImageListViewSerializer : CodeDomSerializer
{
public override object Serialize
(IDesignerSerializationManager manager, object value)
{
CodeDomSerializer baseSerializer = (CodeDomSerializer)manager.GetSerializer(
typeof(ImageListView).BaseType,
typeof(CodeDomSerializer));
// Let the base class do its work first.
object codeObject = baseSerializer.Serialize(manager, value);
// Let us now add our own initialization code.
if (codeObject is CodeStatementCollection)
{
CodeStatementCollection statements = (CodeStatementCollection)codeObject;
// This is the code reference to our ImageListView instance.
CodeExpression imageListViewCode =
base.SerializeToExpression(manager, value);
if (imageListViewCode != null && value is ImageListView)
{
// Walk through columns...
foreach (ImageListViewColumnHeader column in
((ImageListView)value).Columns)
{
// Create a line of code that will invoke SetColumnHeader.
// Generated code will be something like this:
// myImageListView.SetColumnHeader(ColumnType.Name,
// "Column Name", 120, 1, true);
CodeMethodInvokeExpression columnSetCode =
new CodeMethodInvokeExpression(
imageListViewCode,
"SetColumnHeader",
new CodeFieldReferenceExpression(
new CodeTypeReferenceExpression(typeof(ColumnType)),
Enum.GetName(typeof(ColumnType), column.Type)),
new CodePrimitiveExpression(column.Text),
new CodePrimitiveExpression(column.Width),
new CodePrimitiveExpression(column.DisplayIndex),
new CodePrimitiveExpression(column.Visible)
);
// Add to the list of code statements.
statements.Add(columnSetCode);
}
}
return codeObject;
}
return base.Serialize(manager, value);
}
public override object Deserialize(IDesignerSerializationManager manager,
object codeObject)
{
// Let the base class handle deserialization.
CodeDomSerializer baseSerializer = (CodeDomSerializer)manager.GetSerializer(
typeof(ImageListView).BaseType,
typeof(CodeDomSerializer));
return baseSerializer.Deserialize(manager, codeObject);
}
}
这会遍历列集合,并为每个列调用 ImageListView
实例的 SetColumnHeader
方法,并使用用户设置的参数。如果这看起来很复杂,这里有一些基本示例供您开始。
创建一个单行注释
CodeCommentStatement commentCode = new CodeCommentStatement("This is a comment");
将导致
// This is a comment
带初始化的简单声明
CodePrimitiveExpression valueCode = new CodePrimitiveExpression("hello");
CodeVariableDeclarationStatement declarationCode =
new CodeVariableDeclarationStatement(typeof(string), "myString", valueCode);
将导致
string myString = "hello";
条件语句
CodeVariableReferenceExpression testCode =
new CodeVariableReferenceExpression("check");
CodeStatement[] trueBlock =
new CodeStatement[] { new CodeCommentStatement("check is true") };
CodeStatement[] falseBlock =
new CodeStatement[] { new CodeCommentStatement("check is false") };
CodeConditionStatement ifCode =
new CodeConditionStatement(testCode, trueBlock, falseBlock);
将导致
if (check)
{
// check is true
}
else
{
// check is false
}
属性访问
CodeThisReferenceExpression thisCode = new CodeThisReferenceExpression();
CodePropertyReferenceExpression propCode =
new CodePropertyReferenceExpression(thisCode, "MyProperty");
CodePropertyReferenceExpression otherPropCode =
new CodePropertyReferenceExpression(thisCode, "MyOtherProperty");
CodeAssignStatement assignCode = new CodeAssignStatement(propCode, otherPropCode);
将导致
this.MyProperty = this.MyOtherProperty;
有关更多信息,这里有一些 MSDN 的参考资料
内置渲染器
为 ImageListView
编写自定义渲染器是一项复杂的任务。与其从头开始编写渲染器,不如使用一个内置渲染器,或者使用一个内置渲染器作为自定义渲染器的起点。目前提供以下内置渲染器

资源
- 源代码也可以在 Google Code 上找到,那里可能更新更频繁。
- 我为演示项目使用了免费的 FamFamFam Silk Icons。
历史
- 2009年10月25日 - 初始发布
- 2009年10月26日 - 更新了演示和源文件
- 2009年10月29日 - 添加了读取嵌入式缩略图的功能
- 2009年11月01日 - 添加了拖放支持和少量 bug 修复
- 2009年11月04日 - 更新了文章并进行了少量 bug 修复
- 2009年11月09日 - 添加了 .NET 2.0 版本和少量 bug 修复
- 2009年11月12日
- .NET 3.5 版本已停用
- 现在可以通过在控件中拖动来重新排序项
- 项属性现在由后台线程获取。添加项应该会快得多
- 为图像尺寸和分辨率添加了项详细信息
- 2009年11月15日 - 缓存了项索引以加快项查找速度
- 2009年12月16日
- 添加了画廊视图模式
- 为常见的图像元数据添加了新的列类型
- 向
ImageListViewItem
添加了BeginEdit()
和EndEdit()
方法。在编辑项时应使用它们,以防止与缓存线程发生冲突。 - 向
ImageListViewItem
添加了GetImage()
方法 - 向
ImageListViewRenderer
添加了新的可重写方法OnLayout
。自定义渲染器可以使用它来修改项区域的大小。 - 向
ImageListViewRenderer
添加了Clip
、ItemAreaBounds
和ColumnHeaderBounds
属性 - 渲染器现在可以使用新的
ImageListViewRenderer.ItemDrawOrder
属性按特定顺序绘制项。使用新的ImageListViewItem.ZOrder
属性可以实现更精细化的控制。 - 添加了内置渲染器
- 用户现在可以使用新的
ImageListView.CacheLimit
属性(大约)设置缩略图缓存的最大大小 - 默认列文本现在从资源加载,以便进行本地化
- 缓存的图像现在已正确处置
- 自定义渲染器现在使用中央缩略图缓存,而不是自己的工作线程
- 2009年12月29日
- 内置渲染器的可调属性现在是
public
的 - 删除了
ImageListView.ItemMargin
属性,改为使用新的可重写ImageListViewRenderer.MeasureItemMargin
方法 - 编辑项后,现在会更新画廊图像
- 将列排序图标移至中性资源
- 清理了实用工具类
- 修复了更新项未更新项缩略图的 bug
- 删除了
ImageListViewRenderer.GetSortArrow
函数。排序箭头现在在DrawColumnHeader
方法中绘制 - 修复了 GIF 文件中缺少分号的问题
- 删除了
SortOrder enum
,它与Windows.Forms.SortOrder
重复 - 修复了双击分隔线会引发列单击事件的问题
- 添加了
NewYear2010Renderer
。您需要定义预处理器符号BONUSPACK
才能将其包含在二进制文件中。祝大家新年快乐!
- 内置渲染器的可调属性现在是
- 2010年1月4日
- 添加了新的
Pane
视图模式,移除了PanelRenderer
- 添加了
NoirRenderer
- 将
ImageListViewRenderer.OnDispose
重命名为Dispose
- 移除了
ImageListViewRenderer.DrawScrollBarFiller
虚方法
- 添加了新的
- 2010年2月17日
- 添加了对虚拟项的支持
- 拖动项到客户区边缘时,控件现在会滚动
- 添加了
RetryOnError
属性。设置为true
时,缓存线程将持续轮询控件以获取缩略图,直到获得有效图像。设置为false
时,缓存线程将在第一次出错后放弃,并显示ErrorImage
。 - 添加了
ItemHover
和ColumnHover
事件 - 添加了
DropFiles
事件 - 添加了
CacheMode
属性以支持连续缓存 - 添加了 Mono 支持(使用 Mono 2.6 测试)