如何在WPF / BitmapImage中快速加载和显示图像?

5

我正在尝试使用WPF编写一个小型照片查看器,基本上是模拟 Windows 照片查看器 的功能。

使用 Image 来在窗口模式和全屏模式下显示图片。

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

FilenameToImageConverter 执行以下操作:

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

当我使用我的照片(大约8兆像素,4MB的jpeg文件)测试程序时,显示图像的加载时间非常长(2或3秒),而Windows照片查看器能够轻松跳过图像。我首先看到它显示图像的低分辨率版本,不久之后才会显示完整的图像。然而,一切都比我的方法快得多。
如何实现这一点?是通过缩略图/预加载吗? 提前感谢您的回答。
编辑
感谢所给的提示,使用DecodePixelWidth进行缩小以及Async / OneWay-Bindings已经显著改善了情况,但还不足以使所有内容流畅。此外,使用IsAsync=true,在加载下一张图片之前,图像将始终为空白,这是一种不愉快的效果。
希望通过立即显示高度缩小的版本,然后在异步加载完整图像后显示完整图像来解决这个问题。由于涉及某种时间上的连续性,我不知道如何使用绑定来实现。请问是否有任何建议?

1
也许这个问题的建议可以帮到您?他们建议使用单向异步绑定,并将图像缩小到实际需要显示的大小。https://dev59.com/SV_Va4cB1Zd3GeqPUY1M - rmc00
如果您尝试使用Windows照片查看器跳过许多(大)图像/快速向后,您会发现它实际上并不是那么快。它会预测您要转到哪些图像并提前加载它们。较新版本还使用硬件加速。此外,如果您在处理图像和WPF绑定方面遇到问题,您可能会发现这个链接有用:http://blog.tedd.no/2011/07/28/unsafe-kernel32-mapped-memory-bitmap-wpf/ - Tedd Hansen
谢谢,这是有价值的见解。照片查看器实际上是否依赖于Thumbs.db,而在我的程序中是否值得这样做? - Dario
1个回答

4
如果你无法使用预览(缩小)的图片,那么至少不要以完整大小渲染图片。为了避免这样做,请使用DecodePixelWidth(或DecodePixelHeight)属性。将其设置为一些合理的值(也许基于当前显示器分辨率),你将会看到显著的性能提升。
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();
    }
}

根据评论的反馈,只使用转换器很难实现您想要的功能,但是您可以在ViewModel中添加一个属性,像这样进行操作(请注意,现在需要直接绑定到CurrentImage,而不是使用转换器):

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

请注意这只是样本代码,需要进行一些修改。例如,如果您在预览生成过程中更改了所选文件(因为它们是异步的),则需要取消所有待处理操作,以免将当前文件预览覆盖为先前的预览。但这应该很容易解决。

谢谢,这是一个不错的第一步。我已经在那方面编辑了问题。 - Dario
如果您使用非常小的DecodePixelWidth(例如320),性能是否足够好(我当然不是要将其保留为此,只是作为预览)? - Evk
有点。先异步加载320像素版本和1920像素版本的结合是可以的。我如何使用绑定实现这一点? - Dario
这对我来说是一个不错的起点。有点遗憾的是它必须包含在视图模型中,但我还是会尝试一下。 - Dario
@Dario,如果你不想在视图模型中放置ImageSource,有几种方法可以避免这样做。也许创建一个从Image继承的控件并在那里完成,或者使用附加属性。主要是使用DecodePixelWidth,并在后台线程上执行(构造BitmapImage)。 - Evk
“附加属性”建议似乎是一个好点子:您可以为图像创建一个附加属性,该属性将接受一个字符串(文件名),然后可以异步创建预览图(低分辨率和高分辨率),就像在ViewModel中一样。这将使“业务逻辑”保留在ViewModel中,而将“视图逻辑”保留在附加属性中。 - Henrik Ilgen

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接