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

ImageListView

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (144投票s)

2009年10月24日

Apache

10分钟阅读

viewsIcon

686575

downloadIcon

29693

一个 .NET ListView 控件,用于显示带有异步加载缩略图的图像文件。

ImageListView demo

引言

ImageListView 是一个 .NET 2.0 控件,用于显示图像文件列表。它的外观和操作方式与标准的 ListView 控件类似。图像缩略图在单独的后台线程中异步加载。可以通过自定义渲染器完全定制控件的外观。

背景

这个项目最初是一个所有者绘制的 ListView。然而,第一个版本需要过多的技巧。特别是确定第一个/最后一个可见项被证明是一个挑战。在进行到一半时,我决定自己编写一个控件。于是 ImageListView 就诞生了。

Using the Code

要使用该控件,请将 ImageListView 添加到您的控件工具箱,然后将其拖放到窗体上。之后,您可以通过更改视图模式(缩略图、画廊、窗格或详细信息)、缩略图大小、列标题等来定制控件的外观。

自定义渲染

ImageListViewRenderer 类负责绘制控件。这是一个 public 类,具有可以被派生类重写的虚函数。派生类可以修改项和列标题的显示大小,并绘制控件的任何或所有部分。

ImageListView with custom renderer

这是产生此外观的渲染器

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 类负责绘制控件的客户区。我确保在控件需要刷新时,渲染器仅绘制可见的项。之后我做的一个优化是添加了 SuspendPaintResumePaint 函数。它们用于在控件连续刷新多次时合并渲染请求。以下示例应能阐明其用法

// 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 编写自定义渲染器是一项复杂的任务。与其从头开始编写渲染器,不如使用一个内置渲染器,或者使用一个内置渲染器作为自定义渲染器的起点。目前提供以下内置渲染器

Built-in renderers

资源

历史

  • 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 添加了 ClipItemAreaBoundsColumnHeaderBounds 属性
    • 渲染器现在可以使用新的 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
    • 添加了 ItemHoverColumnHover 事件
    • 添加了 DropFiles 事件
    • 添加了 CacheMode 属性以支持连续缓存
    • 添加了 Mono 支持(使用 Mono 2.6 测试)
© . All rights reserved.