Introduction
WPF is built on top of a very powerful and capable graphics framework.
Much of the core graphics in WPF supports smooth scaling and with proper
UI design, an application can be made more or less resolution
independent.
But there is one area where this doesn't work - and that is when working
with bitmap images. When scaling bitmap images, they become blurry (when
scaling up) or blurry and crowded (when scaling down). It would be nice
if the image control could select the most suitable image from a list of
images depending on its current rendering size. Like in this picture below:
Fortunately, this can be easily added on top of the existing
Image
control.
Background
When WPF is told to render a bitmap image, regardless of whether it comes
from the hard drive or from a network stream, it will simply load the
first (or only) frame within the image and render it.
This means that even if you have, say an .ico or .tiff file containing
the same image in three different resolutions (24x24, 64x64 and 128x128)
and put it on an Image control and then set its Height
and
Width
to 128x128 there is no guarantee that WPF will pick
the 128x128 image. And even if it did by chance pick the correct one, if
you resize your image control to make it smaller, it would just scale
down the larger image and you'd end up with a blurry picture.
What we want is a control that loads all the available frames of the
image and then, at runtime, picks the correct one to render based on its
current size.
Image, ImageSource and Multi Frame Images
Image loading in WPF is abstracted away into something called
an ImageSource
. This is an abstract base class for anything
that can provide image data to be rendered by the UI. The most common way
to render an image in WPF is to create an Image
object in
XAML and specify its Source
to point at the file to load.
WPF will then create the appropriate ImageSource
implementation to load the image.
<Image Source="SomeFile.png" Width="128" Height="128" />
The framework itself comes with a dozen implementations of
ImageSource
, some of which can be seen in figure 1 above.
Each of these deal with different types of image data, ranging from
simple bitmap data from file to runtime rendering of UI Elements. There
are also ImageSources that can render Direct3D surfaces or WPF vector
drawings.
Multi Frame Images are images that contains more than one image. The most
common example are windows icon files (*.ico) which typically contain the
same image in multiple sizes and qualities. Other examples of file
formats that can contain multiple frames are TIFF and GIF. Although in
the case of GIF files, it's mostly used to create animations.
Each frame has its own set of metadata, which effectively means that each
frame can have different resolution and different pixel depth (bits per
pixel, or bpp). You could for example have a file called foo.ico
containing the following frames:
0: 16x16 8bpp
1: 16x16 16bpp
2: 16x16 32bpp
3: 32x32 8bpp
4: 32x32 16bpp
5: 32x32 32bpp
6: 128x128 32bpp
Below is a screenshot of the image that is included in the sample project
when viewed in an editor that understands multiple frames:
The Control
We start by creating a class called MultiSizeImage that inherits from
Image. We also setup an event handler to get called whenever the Source
property is set or modified.
public class MultiSizeImage : Image
{
static MultiSizeImage()
{
SourceProperty.OverrideMetadata(typeof(MultiSizeImage),
new FrameworkPropertyMetadata(HandleSourceChanged));
}
private static void HandleSourceChanged(
DependencyObject sender,
DependencyPropertyChangedEventArgs e)
{
MultiSizeImage img = (MultiSizeImage)sender;
}
}
So now we have the initial version of our control that can be used as a
drop in replacement for the standard WPF Image
control. At
this point however, it would make little sense since it doesn't add
anything beyond what the standard control does. Let's fix that.
To load the actual frames from an image we use the
BitmapDecoder
class. An instance of this class is
available on all BitmapFrame
objects, which incidentally,
is the object that normally gets created by WPF if you specify your
source from XAML. If for some reason the current source is some other
type of ImageSource
, we'll just skip loading frames and
always render the actual source. When that happens, our control will
behave just like a normal WPF Image
control.
From the decoder's list of frames, we'll run a LINQ query to sort them
by resolution and quality. To make it easier to handle the different
sizes, we'll project the width and hight into a single integer so that
we can easily compare two sizes. Otherwise we'd run into the question
of whether e.g. 100x100 is larger or smaller than 50x200. In the name
of simplicity, we'll assume all frames in the image have the same or
similar aspect ratio.
private void UpdateAvailableFrames()
{
_availableFrames.Clear();
var bmFrame = this.Source as BitmapFrame;
if (bmFrame == null)
return;
var decoder = bmFrame.Decoder;
if (decoder != null && decoder.Frames != null)
{
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);
}
}
What the above code does is that it sorts all the frames by their size
(and size is defined as width * height
) and then picks the
highest quality frame of that size. Remember from the description above
that there can be multiple frames with the same size, and if that
happens we only want the one with the highest quality - that is, the
one with the highest bits per pixels. For more details about LINQ and it's syntax, see
MSDN Introduction to LINQ.
The end result is that the variable _availableFrames
will
contain a list of BitmapSource
instances that are sorted
by size, from small to large. And there will only be one frame of each
size.
Rendering the Image
Now that we have a list of frames already sorted, its trivial to pick
the one to draw. We'll override the
OnRender method which will get
called by WPF whenever it determines that the image needs to be drawn.
Since both the Width
and Height
dependency
properties are defined to affect the rendering pipeline of the element,
we know that OnRender
will get called whenever our size
changes. We can therefore put our frame picking algorithm inside the
OnRender
method.
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)
break;
}
dc.DrawImage(src, new Rect(new Point(0, 0), RenderSize));
}
The above code will loop through the sorted list of frames and look for
the first frame that has the same or larger size than the current
rendering size of the Image
control. It will then draw
that image using the standard
DrawImage
method.
In the case where we don't have any frames at all, for example if the
ImageSource
is a drawing, or if it's an image without
multiple frames, OnRender
will just render the original
Source
object.
Using the code
The sample contains the MultiSizeImage
control which you
can copy to your own projects. It's only a single file
(MultiSizeImage.cs
) - no XAML files is needed. You can
then use it from XAML like a normal Image control:
<local:MultiSizeImage Source="SomeFile.png" .... />
To test the control, you can load the included project into Visual
Studio 2010 and hit F5.
Points of Interest
Ultimately, I think something like this should be included in the
framework. The Image control could easily support this functionality
out of the box - either by automatically picking the appropriate frame,
or by some property that lets the user specify which size and/or frame
to use.
History
- 2012-06-26 - Initial version.