5

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?

Dario
  • 48,658
  • 8
  • 97
  • 130
  • 1
    Maybe the suggestions from this question can help? They suggested using one-way async binding and scaling the image down to the size you actually need to display. http://stackoverflow.com/questions/8960110/how-can-i-make-displaying-images-on-wpf-more-snappy – rmc00 Sep 14 '16 at 17:07
  • If you try skipping many (big) images / backward quickly with Windows Photo Viewer you will discover that is actually not _that_ fast. It predicts what images you will go to and loads them ahead of time. And newer versions use hardware accelleration. Also if messing around with images and WPF binding you may find this useful http://blog.tedd.no/2011/07/28/unsafe-kernel32-mapped-memory-bitmap-wpf/ – Tedd Hansen Sep 15 '16 at 14:28
  • Thanks, that's valuable insight. Does Photo Viewer rely on Thumbs.db actually and is it worthwile to do the same in my program? – Dario Sep 15 '16 at 14:55

1 Answers1

4

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.

Evk
  • 98,527
  • 8
  • 141
  • 191
  • Thanks, that's a good first step. I edited the question in that regard. – Dario Sep 15 '16 at 12:13
  • And if you use very small number for DecodePixelWidth (like 320), is perfomance good enough (I don't mean to leave it as such of course - just if you will use that as preview) – Evk Sep 15 '16 at 12:17
  • Sort of. The combination of first loading the 320px version and a 1920 px version asynchronously would be fine. How can I achieve this with a binding? – Dario Sep 15 '16 at 12:37
  • That's a nice starting point for me. It's a bit of a shame that it has to be included in the viewmodel, but still, I'll give it a try – Dario Sep 15 '16 at 14:56
  • @Dario there are several ways to avoid putting ImageSource in view model if you so like. Maybe create control inherited from Image and do it there, or use attached properties. Main point it using DecodePixelWidth and doing that (constructing BitmapImage) on backgroup thread. – Evk Sep 15 '16 at 15:07
  • The 'attached property' suggestion seems like a good point: You can create an attached property for the Image, which will take a string (the file name) and can then asynchronously create both previews (low-res and hi-res), just as in the ViewModel. This will keep the 'business logic' in the ViewModel and the 'view logic' in the attached property. – Henrik Ilgen Sep 15 '16 at 16:01