完全可缩放的 WPF 图像控件
一个WPF图像控件,能够理解多帧图像(如.ico文件),并根据其当前大小渲染其中合适的帧。
引言
WPF构建在一个非常强大且功能齐全的图形框架之上。WPF的大部分核心图形都支持平滑缩放,并且通过恰当的UI设计,应用程序可以或多或少地实现分辨率无关性。
但在处理位图图像时,这一点就不适用了。缩放位图图像时,它们会变得模糊(放大时)或模糊且拥挤(缩小)。 如果图像控件能够根据其当前的渲染大小,从一系列图像中选择最合适的图像,那将是很好的。就像下面的图片所示。
幸运的是,这可以很容易地添加到现有的Image
控件之上。
背景
当WPF被告知渲染位图图像时,无论它是来自硬盘还是网络流,它只会加载图像中的第一帧(或唯一一帧)并进行渲染。
这意味着,即使你有一个.ico或.tiff文件,其中包含相同图像的三种不同分辨率(24x24、64x64和128x128),并将其放在Image控件上,然后将其Height
和Width
设置为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认为图像需要绘制时,它就会调用这个方法。由于Width
和Height
依赖属性都定义为影响元素的渲染管道,所以我们知道当我们的尺寸改变时,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 - 初始版本。