1. 程式人生 > >【轉】WPF自定義控制元件與樣式(12)-縮圖ThumbnailImage /gif動畫圖/圖片列表

【轉】WPF自定義控制元件與樣式(12)-縮圖ThumbnailImage /gif動畫圖/圖片列表

一.前言

  申明:WPF自定義控制元件與樣式是一個系列文章,前後是有些關聯的,但大多是按照由簡到繁的順序逐步釋出的等,若有不明白的地方可以參考本系列前面的文章,文末附有部分文章連結。

  本文主要針對WPF專案開發中圖片的各種使用問題,經過總結,把一些經驗分享一下。內容包括:

  • WPF常用影象資料來源ImageSource的建立;
  • 自定義縮圖控制元件ThumbnailImage,支援網路圖片、大圖片、圖片非同步載入等特性;
  • 動態圖片gif播放控制元件;
  • 圖片列表樣式,支援大資料量的虛擬化;

二. WPF常用影象資料來源ImageSource的建立

<Image Source="../Images/qq.png"></Image> 

這是一個普通Image控制元件的使用,Source的資料型別是ImageSource,在XAML中可以使用檔案絕對路徑或相對路徑,ImageSource是一個抽象類,我們一般使用BitmapSource、BitmapImage等。

  但在實際專案中,有各種各樣的需求,比如:

    • 從Bitmap建立ImageSource物件;
    • 從資料流byte[]建立ImageSource物件;
    • 從System.Drawing.Image建立ImageSource物件;
    • 從一個大圖片檔案建立一個指定大小的ImageSource物件;

2.1 從System.Drawing.Image建立指定大小ImageSource物件  

/// <summary>
        /// 使用System.Drawing.Image建立WPF使用的ImageSource型別縮圖(不放大小圖)
        /// </summary>
        /// <param name="sourceImage">System.Drawing.Image 物件</param>
        /// <param name="width">
指定寬度</param> /// <param name="height">指定高度</param> public static ImageSource CreateImageSourceThumbnia(System.Drawing.Image sourceImage, double width, double height) { if (sourceImage == null) return null; double rw = width / sourceImage.Width; double rh = height / sourceImage.Height; var aspect = (float)Math.Min(rw, rh); int w = sourceImage.Width, h = sourceImage.Height; if (aspect < 1) { w = (int)Math.Round(sourceImage.Width * aspect); h = (int)Math.Round(sourceImage.Height * aspect); } Bitmap sourceBmp = new Bitmap(sourceImage, w, h); IntPtr hBitmap = sourceBmp.GetHbitmap(); BitmapSource bitmapSource = Imaging.CreateBitmapSourceFromHBitmap(hBitmap, IntPtr.Zero, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions()); bitmapSource.Freeze(); System.Utility.Win32.Win32.DeleteObject(hBitmap); sourceImage.Dispose(); sourceBmp.Dispose(); return bitmapSource; }

2.2 從一個大圖片檔案建立一個指定大小的ImageSource物件

/// <summary>
        /// 建立WPF使用的ImageSource型別縮圖(不放大小圖)
        /// </summary>
        /// <param name="fileName">本地圖片路徑</param>
        /// <param name="width">指定寬度</param>
        /// <param name="height">指定高度</param>
        public static ImageSource CreateImageSourceThumbnia(string fileName, double width, double height)
        {
            System.Drawing.Image sourceImage = System.Drawing.Image.FromFile(fileName);
            double rw = width / sourceImage.Width;
            double rh = height / sourceImage.Height;
            var aspect = (float)Math.Min(rw, rh);
            int w = sourceImage.Width, h = sourceImage.Height;
            if (aspect < 1)
            {
                w = (int)Math.Round(sourceImage.Width * aspect); h = (int)Math.Round(sourceImage.Height * aspect);
            }
            Bitmap sourceBmp = new Bitmap(sourceImage, w, h);
            IntPtr hBitmap = sourceBmp.GetHbitmap();
            BitmapSource bitmapSource = Imaging.CreateBitmapSourceFromHBitmap(hBitmap, IntPtr.Zero, Int32Rect.Empty,
                   BitmapSizeOptions.FromEmptyOptions());

            bitmapSource.Freeze();
            System.Utility.Win32.Win32.DeleteObject(hBitmap);
            sourceImage.Dispose();
            sourceBmp.Dispose();
            return bitmapSource;
        }

2.3 從Bitmap建立指定大小的ImageSource物件  

/// <summary>
        /// 從一個Bitmap建立ImageSource
        /// </summary>
        /// <param name="image">Bitmap物件</param>
        /// <returns></returns>
        public static ImageSource CreateImageSourceFromImage(Bitmap image)
        {
            if (image == null) return null;
            try
            {
                IntPtr ptr = image.GetHbitmap();
                BitmapSource bs = Imaging.CreateBitmapSourceFromHBitmap(ptr, IntPtr.Zero, Int32Rect.Empty,
                                                                        BitmapSizeOptions.FromEmptyOptions());
                bs.Freeze();
                image.Dispose();
                System.Utility.Win32.Win32.DeleteObject(ptr);
                return bs;
            }
            catch (Exception)
            {
                return null;
            }
        }

2.4 從資料流byte[]建立指定大小的ImageSource物件  

/// <summary>
        /// 從資料流建立縮圖
        /// </summary>
        public static ImageSource CreateImageSourceThumbnia(byte[] data, double width, double height)
        {
            using (Stream stream = new MemoryStream(data, true))
            {
                using (Image img = Image.FromStream(stream))
                {
                    return CreateImageSourceThumbnia(img, width, height);
                }
            }
        }

三.自定義縮圖控制元件ThumbnailImage

  ThumbnailImage控制元件的主要解決的問題:

  為了能擴充套件支援多種型別的縮圖,設計了一個簡單的模式,用VS自帶的工具生成的程式碼檢視:

3.1 多種型別的縮圖擴充套件

  首先定義一個圖片型別列舉:  

/// <summary>
    /// 縮圖資料來源源型別
    /// </summary>
    public enum EnumThumbnail
    {
        Image,
        Vedio,
        WebImage,
        Auto,
        FileX,
    }

然後定義了一個介面,生成圖片資料來源ImageSource  

/// <summary>
    /// 縮圖建立服務介面
    /// </summary>
    public interface IThumbnailProvider
    {
        /// <summary>
        /// 建立縮圖。fileName:檔案路徑;width:圖片寬度;height:高度
        /// </summary>
        ImageSource GenereateThumbnail(object fileSource, double width, double height);
    }

如上面的程式碼檢視,有三個實現,視訊縮圖VedioThumbnailProvider沒有實現完成,基本方法是利用一個第三方工具ffmpeg來獲取第一幀影象然後建立ImageSource。

  ImageThumbnailProvider:普通圖片縮圖實現(呼叫的2.2方法):

/// <summary>
    /// 本地圖片縮圖建立服務
    /// </summary>
    internal class ImageThumbnailProvider : IThumbnailProvider
    {
        /// <summary>
        /// 建立縮圖。fileName:檔案路徑;width:圖片寬度;height:高度
        /// </summary>
        public ImageSource GenereateThumbnail(object fileName, double width, double height)
        {
            try
            {
                var path = fileName.ToSafeString();
                if (path.IsInvalid()) return null;
                return System.Utility.Helper.Images.CreateImageSourceThumbnia(path, width, height);
            }
            catch
            {
                return null;
            }
        }
    }

WebImageThumbnailProvider:網路圖片縮圖實現(下載圖片資料後呼叫2.1方法):  

/// <summary>
    /// 網路圖片縮圖建立服務
    /// </summary>
    internal class WebImageThumbnailProvider : IThumbnailProvider
    {
        /// <summary>
        /// 建立縮圖。fileName:檔案路徑;width:圖片寬度;height:高度
        /// </summary>
        public ImageSource GenereateThumbnail(object fileName, double width, double height)
        {
            try
            {
                var path = fileName.ToSafeString();
                if (path.IsInvalid()) return null;
                var request = WebRequest.Create(path);
                request.Timeout = 20000;
                var stream = request.GetResponse().GetResponseStream();
                var img = System.Drawing.Image.FromStream(stream);
                return System.Utility.Helper.Images.CreateImageSourceThumbnia(img, width, height);
            }
            catch
            {
                return null;
            }
        }
    }

簡單工廠ThumbnailProviderFactory實現:  

/// <summary>
    /// 縮圖建立服務簡單工廠
    /// </summary>
    public class ThumbnailProviderFactory : System.Utility.Patterns.ISimpleFactory<EnumThumbnail, IThumbnailProvider>
    {
        /// <summary>
        /// 根據key獲取例項
        /// </summary>
        public virtual IThumbnailProvider GetInstance(EnumThumbnail key)
        {
            switch (key)
            {
                case EnumThumbnail.Image:
                    return Singleton<ImageThumbnailProvider>.GetInstance();
                case EnumThumbnail.Vedio:
                    return Singleton<VedioThumbnailProvider>.GetInstance();
                case EnumThumbnail.WebImage:
                    return Singleton<WebImageThumbnailProvider>.GetInstance();
            }
            return null;
        }
    }

3.2 縮圖控制元件ThumbnailImage

  先看看效果圖吧,下面三張圖片,圖1是本地圖片,圖2是網路圖片,圖3也是網路圖片,為什麼沒顯示呢,這張圖片用的是國外的圖片連結地址,非同步載入(載入比較慢,還沒出來的!)

  ThumbnailImage實際是繼承在微軟的圖片控制元件Image,因此沒有樣式程式碼,繼承之後,主要的目的就是重寫Imagesource的處理過程,詳細程式碼:

/*
     * 較大的圖片,視訊,網路圖片要做快取處理:快取縮圖為本地檔案,或記憶體縮圖物件。
     */

    /// <summary>
    /// 縮圖圖片顯示控制元件,同時支援圖片和視訊縮圖
    /// </summary>
    public class ThumbnailImage : Image
    {
        /// <summary>
        /// 是否啟用快取,預設false不啟用
        /// </summary>
        public bool CacheEnable
        {
            get { return (bool)GetValue(CacheEnableProperty); }
            set { SetValue(CacheEnableProperty, value); }
        }
        /// <summary>
        /// 是否啟用快取,預設false不啟用.預設快取時間是180秒
        /// </summary>
        public static readonly DependencyProperty CacheEnableProperty =
            DependencyProperty.Register("CacheEnable", typeof(bool), typeof(ThumbnailImage), new PropertyMetadata(false));

        /// <summary>
        /// 快取時間,單位秒。預設180秒
        /// </summary>
        public int CacheTime
        {
            get { return (int)GetValue(CacheTimeProperty); }
            set { SetValue(CacheTimeProperty, value); }
        }
        public static readonly DependencyProperty CacheTimeProperty =
            DependencyProperty.Register("CacheTime", typeof(int), typeof(ThumbnailImage), new PropertyMetadata(180));

        /// <summary>
        /// 是否啟用非同步載入,網路圖片建議啟用,本地圖可以不需要。預設不起用非同步
        /// </summary>
        public bool AsyncEnable
        {
            get { return (bool)GetValue(AsyncEnableProperty); }
            set { SetValue(AsyncEnableProperty, value); }
        }
        public static readonly DependencyProperty AsyncEnableProperty =
            DependencyProperty.Register("AsyncEnable", typeof(bool), typeof(ThumbnailImage), new PropertyMetadata(false));

        /// <summary>
        /// 縮圖型別,預設Image圖片
        /// </summary>
        public EnumThumbnail ThumbnailType
        {
            get { return (EnumThumbnail)GetValue(ThumbnailTypeProperty); }
            set { SetValue(ThumbnailTypeProperty, value); }
        }
        public static readonly DependencyProperty ThumbnailTypeProperty =
            DependencyProperty.Register("ThumbnailType", typeof(EnumThumbnail), typeof(ThumbnailImage), new PropertyMetadata(EnumThumbnail.Image));

        /// <summary>
        /// 縮圖資料來源:檔案物理路徑
        /// </summary>
        public object ThumbnailSource
        {
            get { return GetValue(ThumbnailSourceProperty); }
            set { SetValue(ThumbnailSourceProperty, value); }
        }
        public static readonly DependencyProperty ThumbnailSourceProperty = DependencyProperty.Register("ThumbnailSource", typeof(object),
            typeof(ThumbnailImage), new PropertyMetadata(OnSourcePropertyChanged));

        /// <summary>
        /// 縮圖
        /// </summary>
        protected static ThumbnailProviderFactory ThumbnailProviderFactory = new ThumbnailProviderFactory();

        protected override void OnInitialized(EventArgs e)
        {
            base.OnInitialized(e);
            this.Loaded += ThumbnailImage_Loaded;
        }

        void ThumbnailImage_Loaded(object sender, RoutedEventArgs e)
        {
            BindSource(this);
        }

        /// <summary>
        /// 屬性更改處理事件
        /// </summary>
        private static void OnSourcePropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
        {
            ThumbnailImage img = sender as ThumbnailImage;
            if (img == null) return;
            if (!img.IsLoaded) return;
            BindSource(img);
        }
        private static void BindSource(ThumbnailImage image)
        {
            var w = image.Width;
            var h = image.Height;
            object source = image.ThumbnailSource;
            //bind
            if (image.AsyncEnable)
            {
                BindThumbnialAync(image, source, w, h);
            }
            else
            {
                BindThumbnial(image, source, w, h);
            }
        }

        /// <summary>
        /// 繫結縮圖
        /// </summary>
        private static void BindThumbnial(ThumbnailImage image, object fileSource, double w, double h)
        {
            IThumbnailProvider thumbnailProvider = ThumbnailProviderFactory.GetInstance(image.ThumbnailType);
            image.Dispatcher.BeginInvoke(new Action(() =>
            {
                var cache = image.CacheEnable;
                var time = image.CacheTime;
                ImageSource img = null;
                if (cache)
                {
                    img = CacheManager.GetCache<ImageSource>(fileSource.GetHashCode().ToString(), time, () =>
                    {
                        return thumbnailProvider.GenereateThumbnail(fileSource, w, h);
                    });
                }
                else img = thumbnailProvider.GenereateThumbnail(fileSource, w, h);
                image.Source = img;
            }), DispatcherPriority.ApplicationIdle);
        }

        /// <summary>
        /// 非同步執行緒池繫結縮圖
        /// </summary>
        private static void BindThumbnialAync(ThumbnailImage image, object fileSource, double w, double h)
        {
            IThumbnailProvider thumbnailProvider = ThumbnailProviderFactory.GetInstance(image.ThumbnailType);
            var cache = image.CacheEnable;
            var time = image.CacheTime;
            System.Utility.Executer.TryRunByThreadPool(() =>
            {
                ImageSource img = null;
                if (cache)
                {
                    img = CacheManager.GetCache<ImageSource>(fileSource.GetHashCode().ToString(), time, () =>
                    {
                        return thumbnailProvider.GenereateThumbnail(fileSource, w, h);
                    });
                }
                else img = thumbnailProvider.GenereateThumbnail(fileSource, w, h);
                image.Dispatcher.BeginInvoke(new Action(() => { image.Source = img; }), DispatcherPriority.ApplicationIdle);
            });
        }
    }

其中非同步用的執行緒池執行圖片載入, Executer.TryRunByThreadPool是一個輔助方法,用於線上程池中執行一個委託方法。快取的實現用的是另外一個輕量級記憶體快取組建(使用微軟HttpRuntime.Cache的快取機制),關於快取的方案網上很多,這裡就不介紹了。

  示例程式碼:  

<core:ThumbnailImage Width="120" Height="120" Margin="3" ThumbnailSource="Images/qq.png" />
            <core:ThumbnailImage Width="120" Height="120" Margin="3" ThumbnailType="WebImage" AsyncEnable="True" ThumbnailSource="http://img0.bdstatic.com/img/image/shouye/fsxzqnghbxzzzz.jpg" />
            <core:ThumbnailImage Width="160" Height="120" Margin="3" CacheEnable="True" ThumbnailType="WebImage" AsyncEnable="True" ThumbnailSource="http://www.wallsave.com/wallpapers/1920x1080/beautiful-girl/733941/beautiful-girl-girls-hd-733941.jpg" />
            <core:ThumbnailImage Width="160" Height="120" Margin="3" ThumbnailType="WebImage" AsyncEnable="True" ThumbnailSource="http://wallpaperpassion.com/upload_puzzle_thumb/16047/hot-girl-hd-wallpaper.jpg" />
            <core:FButton Width="120" Click="FButton_Click">CacheEnable</core:FButton>
            <core:ThumbnailImage x:Name="ImageCache" Width="160" CacheEnable="True" Height="120" Margin="3" ThumbnailType="WebImage" AsyncEnable="True" />

四.動態圖片gif播放控制元件

  由於WPF沒有提供Gif的播放控制元件,網上有不少開源的方案,這裡實現的Gif播放也是來自網上的開原始碼(程式碼地址:http://1code.codeplex.com/)。效果不錯哦!:

實現程式碼:  

/// <summary>
    /// 支援GIF動畫圖片播放的圖片控制元件,GIF圖片源GIFSource
    /// </summary>
    public class AnimatedGIF : Image
    {
        public static readonly DependencyProperty GIFSourceProperty = DependencyProperty.Register(
            "GIFSource", typeof(string), typeof(AnimatedGIF), new PropertyMetadata(OnSourcePropertyChanged));

        /// <summary>
        /// GIF圖片源,支援相對路徑、絕對路徑
        /// </summary>
        public string GIFSource
        {
            get { return (string)GetValue(GIFSourceProperty); }
            set { SetValue(GIFSourceProperty, value); }
        }

        internal Bitmap Bitmap; // Local bitmap member to cache image resource
        internal BitmapSource BitmapSource;
        public delegate void FrameUpdatedEventHandler();

        /// <summary>
        /// Delete local bitmap resource
        /// Reference: http://msdn.microsoft.com/en-us/library/dd183539(VS.85).aspx
        /// </summary>
        [DllImport("gdi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        static extern bool DeleteObject(IntPtr hObject);

        protected override void OnInitialized(EventArgs e)
        {
            base.OnInitialized(e);
            this.Loaded += AnimatedGIF_Loaded;
            this.Unloaded += AnimatedGIF_Unloaded;
        }

        void AnimatedGIF_Unloaded(object sender, RoutedEventArgs e)
        {
            this.StopAnimate();
        }

        void AnimatedGIF_Loaded(object sender, RoutedEventArgs e)
        {
            BindSource(this);
        }

        /// <summary>
        /// Start animation
        /// </summary>
        public void StartAnimate()
        {
            ImageAnimator.Animate(Bitmap, OnFrameChanged);
        }

        /// <summary>
        /// Stop animation
        /// </summary>
        public void StopAnimate()
        {
            ImageAnimator.StopAnimate(Bitmap, OnFrameChanged);
        }

        /// <summary>
        /// Event handler for the frame changed
        /// </summary>
        private void OnFrameChanged(object sender, EventArgs e)
        {
            Dispatcher.BeginInvoke(DispatcherPriority.Normal,
                                   new FrameUpdatedEventHandler(FrameUpdatedCallback));
        }

        private void FrameUpdatedCallback()
        {
            ImageAnimator.UpdateFrames();

            if (BitmapSource != null)
                BitmapSource.Freeze();

            // Convert the bitmap to BitmapSource that can be display in WPF Visual Tree
            BitmapSource = GetBitmapSource(this.Bitmap, this.BitmapSource);
            Source = BitmapSource;
            InvalidateVisual();
        }

        /// <summary>
        /// 屬性更改處理事件
        /// </summary>
        private static void OnSourcePropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
        {
            AnimatedGIF gif = sender as AnimatedGIF;
            if (gif == null) return;
            if (!gif.IsLoaded) return;
            BindSource(gif);
        }
        private static void BindSource(AnimatedGIF gif)
        {
            gif.StopAnimate();
            if (gif.Bitmap != null) gif.Bitmap.Dispose();
            var path = gif.GIFSource;
            if (path.IsInvalid()) return;
            if (!Path.IsPathRooted(path))
            {
                path = File.GetPhysicalPath(path);
            }
            gif.Bitmap = new Bitmap(path);
            gif.BitmapSource = GetBitmapSource(gif.Bitmap, gif.BitmapSource);
            gif.StartAnimate();
        }

        private static BitmapSource GetBitmapSource(Bitmap bmap, BitmapSource bimg)
        {
            IntPtr handle = IntPtr.Zero;

            try
            {
                handle = bmap.GetHbitmap();
                bimg = Imaging.CreateBitmapSourceFromHBitmap(
                    handle, IntPtr.Zero, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions());
            }
            finally
            {
                if (handle != IntPtr.Zero)
                    DeleteObject(handle);
            }

            return bimg;
        }
    }

五.圖片列表樣式,支援大資料量的虛擬化

  先看看效果圖(gif圖,有點大):

 

  用的是ListView作為列表容器,因為Listview支援靈活的擴充套件,為了實現上面的效果,集合容器ItemsPanel只能使用WrapPanel,樣式本身並不複雜:  

<Page.Resources>
        <DataTemplate x:Key="ThumbImageItem">
            <Grid Width="140" Height="120" ToolTip="{Binding Path=DataContext.FullPath}">
                <Grid.RowDefinitions>
                    <RowDefinition Height="*"/>
                    <RowDefinition Height="20"/>
                </Grid.RowDefinitions>
                <core:ThumbnailImage ThumbnailSource="{Binding File}" Width="140" Height="100" CacheEnable="True" AsyncEnable="True"  VerticalAlignment="Center" HorizontalAlignment="Center" Stretch="None"/>
                <TextBlock Grid.Row="1" Text="{Binding Name}" FontSize="12" Height="20" HorizontalAlignment="Center" VerticalAlignment="Center" TextAlignment="Center" TextTrimming="CharacterEllipsis"/>
                <!--<CheckBox VerticalAlignment="Top" HorizontalAlignment="Right" xly:ControlAttachProperty.FIconSize="20"/>-->
            </Grid>
        </DataTemplate>

        <Style x:Key="ImageListViewItem" TargetType="{x:Type ListViewItem}">
            <Setter Property="Foreground" Value="{StaticResource TextForeground}" />
            <Setter Property="HorizontalContentAlignment" Value="Stretch" />
            <Setter Property="VerticalContentAlignment" Value="Center" />
            <Setter Property="Margin" Value="2" />
            <Setter Property="SnapsToDevicePixels" Value="True" />
            <Setter Property="Background" Value="Transparent"></Setter>
            <Setter Property="Padding" Value="2,0,2,0"></Setter>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type ListViewItem}">
                        <Border x:Name="Bd" Background="{TemplateBinding Background}" SnapsToDevicePixels="true" BorderThickness="1"
                                BorderBrush="Transparent" Margin="{TemplateBinding Margin}">
                            <ContentPresenter x:Name="contentPresenter" VerticalAlignment="{TemplateBinding VerticalContentAlignment}" Margin="{TemplateBinding Padding}" />
                        </Border>
                        <ControlTemplate.Triggers>
                            <Trigger Property="IsSelected" Value="true">
                                <Setter TargetName="Bd" Property="Background" Value="{StaticResource ItemSelectedBackground}" />
                                <Setter Property="Foreground" Value="{StaticResource ItemSelectedForeground}" />
                                <Setter TargetName="Bd" Property="BorderBrush" Value="{StaticResource FocusBorderBrush}" />
                            </Trigger>
                            <Trigger Property="IsMouseOver" Value="True">
                                <Setter TargetName="Bd" Property="Background" Value="{StaticResource ItemMouseOverBackground}" />
                                <Setter Property="Foreground" Value="{StaticResource ItemMouseOverForeground}" />
                                <Setter TargetName="Bd" Property="BorderBrush" Value="{StaticResource MouseOverBorderBrush}" />
                            </Trigger>
                            <MultiTrigger>
                                <MultiTrigger.Conditions>
                                    <Condition Property="IsSelected" Value="true" />
                                    <Condition Property="Selector.IsSelectionActive" Value="True" />
                                </MultiTrigger.Conditions>
                                <Setter Property="Background" Value="{StaticResource ItemSelectedBackground}" />
                                <Setter Property="Foreground" Value="{StaticResource ItemSelectedForeground}" />
                                <Setter TargetName="Bd" Property="BorderBrush" Value="{StaticResource FocusBorderBrush}" />
                            </MultiTrigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>

    </Page.Resources>

    <Grid Margin="3">
        <Grid.RowDefinitions>
            <RowDefinition Height="50"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <StackPanel Orientation="Horizontal">
            <TextBox x:Name="txtFolder"  Style="{StaticResource LabelOpenFolderTextBox}" Height="30" Width="400" Margin="5">D:\Doc\Resource</TextBox>
            <core:FButton Content="繫結" Margin="5" Click="FButton_Click"></core:FButton>
        </StackPanel>

        <ListView Grid.Row="1" x:Name="timgViewer" AlternationCount="0" ScrollViewer.IsDeferredScrollingEnabled="True" SelectionMode="Multiple"
                  ItemTemplate="{StaticResource ThumbImageItem}" ItemContainerStyle="{StaticResource ImageListViewItem}">
            <ListView.ItemsPanel>
                <ItemsPanelTemplate>
                    <core:VirtualizingWrapPanel  ItemHeight="200" ItemWidth="240" Orientation="Horizontal" 
                                                VirtualizingStackPanel.IsVirtualizing="True" VirtualizingStackPanel.VirtualizationMode="Recycling"
                                                CanVerticallyScroll="True" CanHorizontallyScroll="False" />
                </ItemsPanelTemplate>
            </ListView.ItemsPanel>
        </ListView>
    </Grid>

主要難道在於 WrapPanel是不支援虛擬化的,網上找了一個開源的WrapPanel虛擬化實現=VirtualizingWrapPanel,它有點小bug(滑動條長度計算有時候不是很準確),不過完全不影響使用,程式碼: 

public class VirtualizingWrapPanel : VirtualizingPanel, IScrollInfo
    {

        #region Fields

        UIElementCollection _children;
        ItemsControl _itemsControl;
        IItemContainerGenerator _generator;
        private Point _offset = new Point(0, 0);
        private Size _extent = new Size(0, 0);
        private Size _viewport = new Size(0, 0);
        private int firstIndex = 0;
        private Size childSize;
        private Size _pixelMeasuredViewport = new Size(0, 0);
        Dictionary<UIElement, Rect> _realizedChildLayout = new Dictionary<UIElement, Rect>();
        WrapPanelAbstraction _abstractPanel;


        #endregion

        #region Properties

        private Size ChildSlotSize
        {
            get
            {
                return new Size(ItemWidth, ItemHeight);
            }
        }

        #endregion

        #region Dependency Properties

        [TypeConverter(typeof(LengthConverter))]
        public double ItemHeight
        {
            get
            {
                return (double)base.GetValue(ItemHeightProperty);
            }
            set
            {
                base.SetValue(ItemHeightProperty, value);
            }
        }

        [TypeConverter(typeof(LengthConverter))]
        public double ItemWidth
        {
            get
            {
                return (double)base.GetValue(ItemWidthProperty);
            }
            set
            {
                base.SetValue(ItemWidthProperty, value);
            }
        }

        public Orientation Orientation
        {
            get { return (Orientation)GetValue(OrientationProperty); }
            set { SetValue(OrientationProperty, value); }
        }

        public static readonly DependencyProperty ItemHeightProperty = DependencyProperty.Register("ItemHeight", typeof(double), typeof(VirtualizingWrapPanel), new FrameworkPropertyMetadata(double.PositiveInfinity));
        public static readonly DependencyProperty ItemWidthProperty = DependencyProperty.Register("ItemWidth", typeof(double), typeof(VirtualizingWrapPanel), new FrameworkPropertyMetadata(double.PositiveInfinity));
        public static readonly DependencyProperty OrientationProperty = StackPanel.OrientationProperty.AddOwner(typeof(VirtualizingWrapPanel), new FrameworkPropertyMetadata(Orientation.Horizontal));

        #endregion

        #region Methods

        public void SetFirstRowViewItemIndex(int index)
        {
            SetVerticalOffset((index) / Math.Floor((_viewport.Width) / childSize.Width));
            SetHorizontalOffset((index) / Math.Floor((_viewport.Height) / childSize.Height));
        }

        private void Resizing(object sender, EventArgs e)
        {
            if (_viewport.Width != 0)
            {
                int firstIndexCache = firstIndex;
                _abstractPanel = null;
                MeasureOverride(_viewport);
                SetFirstRowViewItemIndex(firstIndex);
                firstIndex = firstIndexCache;
            }
        }

        public int GetFirstVisibleSection()
        {
            int section;
            if (_abstractPanel == null) return 0;
            var maxSection = _abstractPanel.Max(x => x.Section);
            if (Orientation == Orientation.Horizontal)
            {
                section = (int)_offset.Y;
            }
            else
            {
                section = (int)_offset.X;
            }
            if (section > maxSection)
                section = maxSection;
            return section;
        }

        public int GetFirstVisibleIndex()
        {
            if (_abstractPanel == null) return 0;
            int section = GetFirstVisibleSection();
            var item = _abstractPanel.Where(x => x.Section == section).FirstOrDefault();
            if (item != null)
                return item._index;
            return 0;
        }

        private void CleanUpItems(int minDesiredGenerated, int maxDesiredGenerated)
        {
            for (int i = _children.Count - 1; i >= 0; i--)
            {
                GeneratorPosition childGeneratorPos = new GeneratorPosition(i, 0);
                int itemIndex = _generator.IndexFromGeneratorPosition(childGeneratorPos);
                if (itemIndex < minDesiredGenerated || itemIndex > maxDesiredGenerated)
                {
                    _generator.Remove(childGeneratorPos, 1);
                    RemoveInternalChildRange(i, 1);
                }
            }
        }

        private void ComputeExtentAndViewport(Size pixelMeasuredViewportSize, int visibleSections)
        {
            if (Orientation == Orientation.Horizontal)
            {
                _viewport.Height = visibleSections;
                _viewport.Width = pixelMeasuredViewportSize.Width;
            }
            else
            {
                _viewport.Width = visibleSections;
                _viewport.Height = pixelMeasuredViewportSize.Height;
            }

            if (Orientation == Orientation.Horizontal)
            {
                _extent.Height = _abstractPanel.SectionCount + ViewportHeight - 1;

            }
            else
            {
                _extent.Width = _abstractPanel.SectionCount + ViewportWidth - 1;
            }
            _owner.InvalidateScrollInfo();
        }

        private void ResetScrollInfo()
        {
            _offset.X = 0;
            _offset.Y = 0;
        }

        private int GetNextSectionClosestIndex(int itemIndex)
        {
            var abstractItem = _abstractPanel[itemIndex];
            if (abstractItem.Section < _abstractPanel.SectionCount - 1)
            {
                var ret = _abstractPanel.
                    Where(x => x.Section == abstractItem.Section + 1).
                    OrderBy(x => Math.Abs(x.SectionIndex - abstractItem.SectionIndex)).
                    First();
                return ret._index;
            }
            else
                return itemIndex;
        }

        private int GetLastSectionClosestIndex(int itemIndex)
        {
            var abstractItem = _abstractPanel[itemIndex];
            if (abstractItem.Section > 0)
            {
                var ret = _abstractPanel.
                    Where(x => x.Section == abstractItem.Section - 1).
                    OrderBy(x => Math.Abs(x.SectionIndex - abstractItem.SectionIndex)).
                    First();
                return ret._index;
            }
            else
                return itemIndex;
        }

        private void NavigateDown()
        {
            var gen = _generator.GetItemContainerGeneratorForPanel(this);
            UIElement selected = (UIElement)Keyboard.FocusedElement;
            int itemIndex = gen.IndexFromContainer(selected);
            int depth = 0;
            while (itemIndex == -1)
            {
                selected = (UIElement)VisualTreeHelper.GetParent(selected);
                itemIndex = gen.IndexFromContainer(selected);
                depth++;
            }
            DependencyObject next = null;
            if (Orientation == Orientation.Horizontal)
            {
                int nextIndex = GetNextSectionClosestIndex(itemIndex);
                next = gen.ContainerFromIndex(nextIndex);
                while (next == null)
                {
                    SetVerticalOffset(VerticalOffset + 1);
                    UpdateLayout();
                    next = gen.ContainerFromIndex(nextIndex);
                }
            }
            else
            {
                if (itemIndex == _abstractPanel._itemCount - 1)
                    return;
                next = gen.ContainerFromIndex(itemIndex + 1);
                while (next == null)
                {
                    SetHorizontalOffset(HorizontalOffset + 1);
                    UpdateLayout();
                    next = gen.ContainerFromIndex(itemIndex + 1);
                }
            }
            while (depth != 0)
            {
                next = VisualTreeHelper.GetChild(next, 0);
                depth--;
            }
            (next as UIElement).Focus();
        }

        private void NavigateLeft()
        {
            var gen = _generator.GetItemContainerGeneratorForPanel(this);

            UIElement selected = (UIElement)Keyboard.FocusedElement;
            int itemIndex = gen.IndexFromContainer(selected);
            int depth = 0;
            while (itemIndex == -1)
            {
                selected = (UIElement)VisualTreeHelper.GetParent(selected);
                itemIndex = gen.IndexFromContainer(selected);
                depth++;
            }
            DependencyObject next = null;
            if (Orientation == Orientation.Vertical)
            {
                int nextIndex = GetLastSectionClosestIndex(itemIndex);
                next = gen.ContainerFromIndex(nextIndex);
                while (next == null)
                {
                    SetHorizontalOffset(HorizontalOffset - 1);
                    UpdateLayout();
                    next = gen.ContainerFromIndex(nextIndex);
                }
            }
            else
            {
                if (itemIndex == 0)
                    return;
                next = gen.ContainerFromIndex(itemIndex - 1);
                while (next == null)
                {
                    SetVerticalOffset(VerticalOffset - 1);
                    UpdateLayout();
                    next = gen.ContainerFromIndex(itemIndex - 1);
                }
            }
            while (depth != 0)
            {
                next = VisualTreeHelper.GetChild(next, 0);
                depth--;
            }
            (next as UIElement).Focus();
        }

        private void NavigateRight()
        {
            var gen = _generator.GetItemContainerGeneratorForPanel(this);
            UIElement selected = (UIElement)Keyboard.FocusedElement;
            int itemIndex = gen.IndexFromContainer(selected);
            int depth = 0;
            while (itemIndex == -1)
            {
                selected = (UIElement)VisualTreeHelper.GetParent(selected);
                itemIndex = gen.IndexFromContainer(selected);
                depth++;
            }
            DependencyObject next = null;
            if (Orientation == Orientation.Vertical)
            {
                int nextIndex = GetNextSectionClosestIndex(itemIndex);
                next = gen.ContainerFromIndex(nextIndex);
                while (next == null)
                {
                    SetHorizontalOffset(HorizontalOffset + 1);
                    UpdateLayout();
                    next = gen.ContainerFromIndex(nextIndex);
                }
            }
            else
            {
                if (itemIndex == _abstractPanel._itemCount - 1)
                    return;
                next = gen.ContainerFromIndex(itemIndex + 1);
                while (next == null)
                {
                    SetVerticalOffset(VerticalOffset + 1);
                    UpdateLayout();
                    next = gen.ContainerFromIndex(itemIndex + 1);
                }
            }
            while (depth != 0)
            {
                next = VisualTreeHelper.GetChild(next, 0);
                depth--;
            }
            (next as UIElement).Focus();
        }

        private void NavigateUp()
        {
            var gen = _generator.GetItemContainerGeneratorForPanel(this);
            UIElement selected = (UIElement)Keyboard.FocusedElement;
            int itemIndex = gen.IndexFromContainer(selected);
            int depth = 0;
            while (itemIndex == -1)
            {
                selected = (UIElement)VisualTreeHelper.GetParent(selected);
                itemIndex = gen.IndexFromContainer(selected);
                depth++;
            }
            DependencyObject next = null;
            if (Orientation == Orientation.Horizontal)
            {
                int nextIndex = GetLastSectionClosestIndex(itemIndex);
                next = gen.ContainerFromIndex(nextIndex);
                while (next == null)
                {
                    SetVerticalOffset(VerticalOffset - 1);
                    UpdateLayout();
                    next = gen.ContainerFromIndex(nextIndex);
                }
            }
            else
            {
                if (itemIndex == 0)
                    return;
                next = gen.ContainerFromIndex(itemIndex - 1);
                while (next == null)
                {
                    SetHorizontalOffset(HorizontalOffset - 1);
                    UpdateLayout();
                    next = gen.ContainerFromIndex(itemIndex - 1);
                }
            }
            while (depth != 0)
            {
                next = VisualTreeHelper.GetChild(next, 0);
                depth--;
            }
            (next as UIElement).Focus();
        }


        #endregion

        #region Override

        protected override void OnKeyDown(KeyEventArgs e)
        {
            switch (e.Key)
            {
                case Key.Down:
                    NavigateDown();
                    e.Handled = true;
                    break;
                case Key.Left:
                    NavigateLeft();
                    e.Handled = true;
                    break;
                case Key.Right:
                    NavigateRight();
                    e.Handled = true;
                    break;
                case Key.Up:
                    NavigateUp();
                    e.Handled = true;
                    break;
                default:
                    base.OnKeyDown(e);
                    break