Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Fully Scalable WPF Image Control

0.00/5 (No votes)
26 Jun 2012 1  
A WPF Image control that understands multi frame images (like .ico files) and renders the appropriate frame inside them based on its current size.

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()
    {
        // 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()
    }
     // ...
}

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;
    // 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);
    }
}

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)
            // We found the correct frame
            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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here