C# – How to quickly load and display images with WPF/BitmapImage

cimagewpf

I'm trying to write a little photo viewer in WPF, basically emulating what Windows Photo Viewer is offering.

Displaying in both windowed and fullscreen mode is done using an Image

<Image Name="ImgDisplay" Source="{Binding CurrentImage.FullPath, Converter={StaticResource FilenameToImageConverter}}"/>

where the FilenameToImageConverter does the following

public class FilenameToImageConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            string uri = value as string;

            if (uri != null && File.Exists(uri))
            {
                BitmapImage image = new BitmapImage();
                image.BeginInit();
                image.CacheOption = BitmapCacheOption.None;
                image.UriCachePolicy = new System.Net.Cache.RequestCachePolicy(System.Net.Cache.RequestCacheLevel.BypassCache);
                image.CacheOption = BitmapCacheOption.OnLoad;
                image.CreateOptions = BitmapCreateOptions.IgnoreImageCache;
                image.UriSource = new Uri(uri);
                image.EndInit();
                return image;
            }

            return null;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotSupportedException();
        }
    }

When testing the program with my photos however (around 8mpx, 4MB jpeg files), the load times for displaying the images are huge (2 or 3 seconds), while Windows Photo Viewer is able to skip through the images with ease. I see it first displaying a lower-resolution version of the image, shortly before displaying the full one. Everything however ends up way faster than my approach.

How can I achieve this? Is it all through thumbnails/preloading?
Thank you in advance

Edit

Thanks, the tips given, scaling down using DecodePixelWidth as well as Async/OneWay-Bindings have improved the situation substantially, though not enough to make everything fluent. Also with IsAsync=true, the image will always be blank before loading the next image, which is an unpleasant effect.

It would be nice to solve that through displaying a highly downscaled version immediately and afterwards display the full image when it has been loaded asynchronously. As there is some sort of temporal succession involved, I have no idea how to implement that with bindings. Any suggestions, please?

Best Answer

If you cannot use prepared preview (downscaled) images, at least do not render image at it's full size. To avoid doing that, use DecodePixelWidth (or DecodePixelHeight) property. Set it to some reasonable value (maybe based on current monitor resolution), and you will already see significant perfomance improvement:

public class FilenameToImageConverter : IValueConverter {
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
        string uri = value as string;

        if (uri != null && File.Exists(uri)) {
            BitmapImage image = new BitmapImage();
            image.BeginInit();
            image.CacheOption = BitmapCacheOption.OnLoad;
            image.UriSource = new Uri(uri);
            image.DecodePixelWidth = 1920; // should be enough, but you can experiment
            image.EndInit();
            return image;
        }

        return null;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
        throw new NotSupportedException();
    }
}

EDIT in response to comments. With just converter it's not easy to achieve what you want, but you can just add one more property to your ViewModel and do it like this (note that you now need to bind to CurrentImage directly, without converters):

    private string _currentFile;

    public string CurrentFile
    {
        get { return _currentFile; }
        set
        {
            if (value == _currentFile) return;
            _currentFile = value;
            OnPropertyChanged();
            UpdateImage();
        }
    }

    private ImageSource _currentImage;

    public ImageSource CurrentImage
    {
        get { return _currentImage; }
        set
        {
            if (Equals(value, _currentImage)) return;
            _currentImage = value;
            OnPropertyChanged();
        }
    }

    private async void UpdateImage() {
        var file = this.CurrentFile;
        // this is asynchronous and won't block UI
        // first generate rough preview
        this.CurrentImage = await Generate(file, 320);
        // then generate quality preview
        this.CurrentImage = await Generate(file, 1920);            
    }

    private Task<BitmapImage> Generate(string file, int scale) {
        return Task.Run(() =>
        {
            BitmapImage image = new BitmapImage();
            image.BeginInit();
            image.CacheOption = BitmapCacheOption.OnLoad;
            image.UriSource = new Uri(file);
            image.DecodePixelWidth = scale;
            image.EndInit();
            image.Freeze(); // important
            return image;
        });
    }

Note that's just a sample code and needs a bit of work. For example, if you change selected file in the middle of preview generation (since they are asynchronous) - you need to cancel all pending operations to not overwrite current file preview with previous one. But that should be easy enough.