【轉】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