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

完全可缩放的 WPF 图像控件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.97/5 (10投票s)

2012年6月26日

CPOL

6分钟阅读

viewsIcon

56953

downloadIcon

2791

一个WPF图像控件,能够理解多帧图像(如.ico文件),并根据其当前大小渲染其中合适的帧。

引言

WPF构建在一个非常强大且功能齐全的图形框架之上。WPF的大部分核心图形都支持平滑缩放,并且通过恰当的UI设计,应用程序可以或多或少地实现分辨率无关性。  

但在处理位图图像时,这一点就不适用了。缩放位图图像时,它们会变得模糊(放大时)或模糊且拥挤(缩小)。 如果图像控件能够根据其当前的渲染大小,从一系列图像中选择最合适的图像,那将是很好的。就像下面的图片所示。

幸运的是,这可以很容易地添加到现有的Image控件之上。  

背景

当WPF被告知渲染位图图像时,无论它是来自硬盘还是网络流,它只会加载图像中的第一帧(或唯一一帧)并进行渲染。  

这意味着,即使你有一个.ico或.tiff文件,其中包含相同图像的三种不同分辨率(24x24、64x64和128x128),并将其放在Image控件上,然后将其HeightWidth设置为128x128,也不能保证WPF会选择128x128的图像。即使它偶然选择了正确的图像,如果你缩小图像控件使其变小,它只会缩放较大的图像,最终你会得到一张模糊的图片。  

我们想要的是一个控件,它能加载图像所有可用的帧,然后在运行时根据其当前大小选择正确的帧进行渲染。

图像、ImageSource和多帧图像

WPF中的图像加载被抽象为ImageSource。这是一个抽象基类,用于向UI提供图像数据的任何对象。在WPF中最常见的渲染图像的方式是在XAML中创建一个Image对象,并将其Source属性指向要加载的文件。然后WPF会创建适当的ImageSource实现来加载图像。

<Image Source="SomeFile.png" Width="128" Height="128" />

框架本身提供了十几种ImageSource的实现,其中一些可以在上图1中看到。这些实现处理不同的图像数据类型,从简单的文件位图数据到UI元素的运行时渲染。也有一些ImageSources可以渲染Direct3D表面或WPF矢量图形。 

多帧图像是指包含不止一个图像的图像。最常见的例子是Windows图标文件(*.ico),它们通常包含相同图像的多种尺寸和质量。其他可以包含多帧的文件格式包括TIFF和GIF。尽管在GIF文件的情况下,它主要用于创建动画。 

每一帧都有自己的一组元数据,这意味着每一帧可以有不同的分辨率和不同的像素深度(每像素位数,或bpp)。例如,你可以有一个名为foo.ico的文件,包含以下帧:

0: 16x16 8bpp
1: 16x16 16bpp
2: 16x16 32bpp
3: 32x32 8bpp
4: 32x32 16bpp
5: 32x32 32bpp
6: 128x128 32bpp

下面是示例项目中包含的图像的截图,当使用能够理解多帧的编辑器查看时:  

 

控件 

我们首先创建一个名为MultiSizeImage的类,它继承自Image。我们还设置了一个事件处理程序,以便在Source属性被设置或修改时调用。  

public class MultiSizeImage : Image
{
    static MultiSizeImage()
    {
        // Tell WPF to inform us whenever the Source dependency property is changed
        SourceProperty.OverrideMetadata(typeof(MultiSizeImage), 
                new FrameworkPropertyMetadata(HandleSourceChanged));
    }
 
    private static void HandleSourceChanged(
                            DependencyObject sender,
                            DependencyPropertyChangedEventArgs e)
    { 
        MultiSizeImage img = (MultiSizeImage)sender;
        // Tell the instance to load all frames in the new image source
        //img.UpdateAvailableFrames()
    }
     // ...
}

这样,我们就有了控件的初始版本,可以作为标准WPFImage控件的即插即用替代品。但此时,它并没有什么意义,因为它并没有增加任何标准控件之外的功能。让我们来解决这个问题。

为了从图像中加载实际的帧,我们使用BitmapDecoder类。这个类的一个实例可以在所有BitmapFrame对象上获得,而BitmapFrame对象恰好是WPF通常在XAML中指定源时创建的对象。如果当前源是其他类型的ImageSource,我们将跳过加载帧,并始终渲染实际源。在这种情况下,我们的控件将像普通的WPFImage控件一样工作。 

从解码器的帧列表中,我们将运行一个LINQ查询,按分辨率和质量对它们进行排序。为了方便处理不同的尺寸,我们将宽度和高度投影到一个整数中,以便我们能够轻松地比较两个尺寸。否则,我们会遇到例如100x100是否大于或小于50x200的问题。为了简单起见,我们假设图像中的所有帧都具有相同或相似的纵横比。 

private void UpdateAvailableFrames()
{
    _availableFrames.Clear();
    var bmFrame = this.Source as BitmapFrame;
    // We may have some other type of ImageSource
    // that doesn't have a notion of frames or decoder
    if (bmFrame == null)
        return;
    var decoder = bmFrame.Decoder;
    if (decoder != null && decoder.Frames != null)
    {
        // This will result in an IEnumerable<bitmapframe>
        // with one frame per size, ordered by their size
        var framesInSizeOrder = from frame in decoder.Frames
                                let frameSize = frame.PixelHeight * frame.PixelWidth 
                                group frame by frameSize into g
                                orderby g.Key
                                select g.OrderByDescending(GetFramePixelDepth).First();
        _availableFrames.AddRange(framesInSizeOrder);
    }
}

上面的代码的作用是按尺寸(尺寸定义为width * height)对所有帧进行排序,然后选择该尺寸下质量最高的帧。请记住,根据上面的描述,可能有多帧具有相同的尺寸,如果发生这种情况,我们只想要质量最高的那个——也就是每像素位数最高的那个。有关LINQ及其语法的更多详细信息,请参阅MSDN LINQ简介

最终结果是,变量_availableFrames将包含一个BitmapSource实例列表,这些实例按尺寸从大到小排序。并且每个尺寸只会有一个帧。  

渲染图像

既然我们已经有了一个排序好的帧列表,选择要绘制的帧就变得微不足道了。我们将重写OnRender方法,当WPF认为图像需要绘制时,它就会调用这个方法。由于WidthHeight依赖属性都定义为影响元素的渲染管道,所以我们知道当我们的尺寸改变时,OnRender就会被调用。因此,我们可以将我们的帧选择算法放在OnRender方法中。 

protected override void OnRender(DrawingContext dc)
{
    if (Source == null)
    {
        base.OnRender(dc);
        return;
    }
    ImageSource src = Source;
    var ourSize = RenderSize.Width * RenderSize.Height;
    foreach (var frame in _availableFrames)
    {
        src = frame;
        if (frame.PixelWidth * frame.PixelHeight >= ourSize)
            // We found the correct frame
            break;
    }
    
    dc.DrawImage(src, new Rect(new Point(0, 0), RenderSize));
}

上面的代码将遍历排序好的帧列表,查找第一个大小与当前Image控件的渲染大小相同或更大的帧。然后它将使用标准的DrawImage方法绘制该图像。 

在我们没有任何帧的情况下,例如,如果ImageSource是一个绘图,或者它是一个没有多帧的图像,OnRender将只渲染原始的Source对象。 

使用代码

该示例包含MultiSizeImage控件,您可以将其复制到自己的项目中。它只有一个文件(MultiSizeImage.cs)——不需要XAML文件。然后您就可以像普通Image控件一样在XAML中使用它。

<local:MultiSizeImage Source="SomeFile.png" .... />

要测试该控件,您可以将包含的项目加载到Visual Studio 2010中,然后按F5。 

关注点

最终,我认为应该将类似这样的东西包含在框架中。Image控件可以轻松地开箱即用地支持此功能——无论是自动选择合适的帧,还是通过某个属性允许用户指定使用哪个尺寸和/或帧。

历史

  • 2012-06-26 - 初始版本。
© . All rights reserved.