打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
WPF自定义控件与样式(12)

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

其中异步用的线程池执行图片加载, 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;  }    }View Code

五.图片列表样式,支持大数据量的虚拟化

先看看效果图(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;      }    }    protected override void OnItemsChanged(object sender, ItemsChangedEventArgs args)    {      base.OnItemsChanged(sender, args);      _abstractPanel = null;      ResetScrollInfo();    }    protected override void OnInitialized(EventArgs e)    {      this.SizeChanged += new SizeChangedEventHandler(this.Resizing);      base.OnInitialized(e);      _itemsControl = ItemsControl.GetItemsOwner(this);      _children = InternalChildren;      _generator = ItemContainerGenerator;    }    protected override Size MeasureOverride(Size availableSize)    {      if (_itemsControl == null || _itemsControl.Items.Count == 0)        return availableSize;      if (_abstractPanel == null)        _abstractPanel = new WrapPanelAbstraction(_itemsControl.Items.Count);      _pixelMeasuredViewport = availableSize;      _realizedChildLayout.Clear();      Size realizedFrameSize = availableSize;      int itemCount = _itemsControl.Items.Count;      int firstVisibleIndex = GetFirstVisibleIndex();      GeneratorPosition startPos = _generator.GeneratorPositionFromIndex(firstVisibleIndex);      int childIndex = (startPos.Offset == 0) ? startPos.Index : startPos.Index + 1;      int current = firstVisibleIndex;      int visibleSections = 1;      using (_generator.StartAt(startPos, GeneratorDirection.Forward, true))      {        bool stop = false;        bool isHorizontal = Orientation == Orientation.Horizontal;        double currentX = 0;        double currentY = 0;        double maxItemSize = 0;        int currentSection = GetFirstVisibleSection();        while (current < itemCount)        {          bool newlyRealized;          // Get or create the child					          UIElement child = _generator.GenerateNext(out newlyRealized) as UIElement;          if (newlyRealized)          {            // Figure out if we need to insert the child at the end or somewhere in the middle            if (childIndex >= _children.Count)            {              base.AddInternalChild(child);            }            else            {              base.InsertInternalChild(childIndex, child);            }            _generator.PrepareItemContainer(child);            child.Measure(ChildSlotSize);          }          else          {            // The child has already been created, let's be sure it's in the right spot            Debug.Assert(child == _children[childIndex], "Wrong child was generated");          }          childSize = child.DesiredSize;          Rect childRect = new Rect(new Point(currentX, currentY), childSize);          if (isHorizontal)          {            maxItemSize = Math.Max(maxItemSize, childRect.Height);            if (childRect.Right > realizedFrameSize.Width) //wrap to a new line            {              currentY = currentY + maxItemSize;              currentX = 0;              maxItemSize = childRect.Height;              childRect.X = currentX;              childRect.Y = currentY;              currentSection++;              visibleSections++;            }            if (currentY > realizedFrameSize.Height)              stop = true;            currentX = childRect.Right;          }          else          {            maxItemSize = Math.Max(maxItemSize, childRect.Width);            if (childRect.Bottom > realizedFrameSize.Height) //wrap to a new column            {              currentX = currentX + maxItemSize;              currentY = 0;              maxItemSize = childRect.Width;              childRect.X = currentX;              childRect.Y = currentY;              currentSection++;              visibleSections++;            }            if (currentX > realizedFrameSize.Width)              stop = true;            currentY = childRect.Bottom;          }          _realizedChildLayout.Add(child, childRect);          _abstractPanel.SetItemSection(current, currentSection);          if (stop)            break;          current++;          childIndex++;        }      }      CleanUpItems(firstVisibleIndex, current - 1);      ComputeExtentAndViewport(availableSize, visibleSections);      return availableSize;    }    protected override Size ArrangeOverride(Size finalSize)    {      if (_children != null)      {        foreach (UIElement child in _children)        {          var layoutInfo = _realizedChildLayout[child];          child.Arrange(layoutInfo);        }      }      return finalSize;    }    #endregion    #region IScrollInfo Members    private bool _canHScroll = false;    public bool CanHorizontallyScroll    {      get { return _canHScroll; }      set { _canHScroll = value; }    }    private bool _canVScroll = false;    public bool CanVerticallyScroll    {      get { return _canVScroll; }      set { _canVScroll = value; }    }    public double ExtentHeight    {      get { return _extent.Height; }    }    public double ExtentWidth    {      get { return _extent.Width; }    }    public double HorizontalOffset    {      get { return _offset.X; }    }    public double VerticalOffset    {      get { return _offset.Y; }    }    public void LineDown()    {      if (Orientation == Orientation.Vertical)        SetVerticalOffset(VerticalOffset + 20);      else        SetVerticalOffset(VerticalOffset + 1);    }    public void LineLeft()    {      if (Orientation == Orientation.Horizontal)        SetHorizontalOffset(HorizontalOffset - 20);      else        SetHorizontalOffset(HorizontalOffset - 1);    }    public void LineRight()    {      if (Orientation == Orientation.Horizontal)        SetHorizontalOffset(HorizontalOffset + 20);      else        SetHorizontalOffset(HorizontalOffset + 1);    }    public void LineUp()    {      if (Orientation == Orientation.Vertical)        SetVerticalOffset(VerticalOffset - 20);      else        SetVerticalOffset(VerticalOffset - 1);    }    public Rect MakeVisible(Visual visual, Rect rectangle)    {      var gen = (ItemContainerGenerator)_generator.GetItemContainerGeneratorForPanel(this);      var element = (UIElement)visual;      int itemIndex = gen.IndexFromContainer(element);      while (itemIndex == -1)      {        element = (UIElement)VisualTreeHelper.GetParent(element);        itemIndex = gen.IndexFromContainer(element);      }      int section = _abstractPanel[itemIndex].Section;      Rect elementRect = _realizedChildLayout[element];      if (Orientation == Orientation.Horizontal)      {        double viewportHeight = _pixelMeasuredViewport.Height;        if (elementRect.Bottom > viewportHeight)          _offset.Y += 1;        else if (elementRect.Top < 0)          _offset.Y -= 1;      }      else      {        double viewportWidth = _pixelMeasuredViewport.Width;        if (elementRect.Right > viewportWidth)          _offset.X += 1;        else if (elementRect.Left < 0)          _offset.X -= 1;      }      InvalidateMeasure();      return elementRect;    }    public void MouseWheelDown()    {      PageDown();    }    public void MouseWheelLeft()    {      PageLeft();    }    public void MouseWheelRight()    {      PageRight();    }    public void MouseWheelUp()    {      PageUp();    }    public void PageDown()    {      SetVerticalOffset(VerticalOffset + _viewport.Height * 0.8);    }    public void PageLeft()    {      SetHorizontalOffset(HorizontalOffset - _viewport.Width * 0.8);    }    public void PageRight()    {      SetHorizontalOffset(HorizontalOffset + _viewport.Width * 0.8);    }    public void PageUp()    {      SetVerticalOffset(VerticalOffset - _viewport.Height * 0.8);    }    private ScrollViewer _owner;    public ScrollViewer ScrollOwner    {      get { return _owner; }      set { _owner = value; }    }    public void SetHorizontalOffset(double offset)    {      if (offset < 0 || _viewport.Width >= _extent.Width)      {        offset = 0;      }      else      {        if (offset + _viewport.Width >= _extent.Width)        {          offset = _extent.Width - _viewport.Width;        }      }      _offset.X = offset;      if (_owner != null)        _owner.InvalidateScrollInfo();      InvalidateMeasure();      firstIndex = GetFirstVisibleIndex();    }    public void SetVerticalOffset(double offset)    {      if (offset < 0 || _viewport.Height >= _extent.Height)      {        offset = 0;      }      else      {        if (offset + _viewport.Height >= _extent.Height)        {          offset = _extent.Height - _viewport.Height;        }      }      _offset.Y = offset;      if (_owner != null)        _owner.InvalidateScrollInfo();      //_trans.Y = -offset;      InvalidateMeasure();      firstIndex = GetFirstVisibleIndex();    }    public double ViewportHeight    {      get { return _viewport.Height; }    }    public double ViewportWidth    {      get { return _viewport.Width; }    }    #endregion    #region helper data structures    class ItemAbstraction    {      public ItemAbstraction(WrapPanelAbstraction panel, int index)      {        _panel = panel;        _index = index;      }      WrapPanelAbstraction _panel;      public readonly int _index;      int _sectionIndex = -1;      public int SectionIndex      {        get        {          if (_sectionIndex == -1)          {            return _index % _panel._averageItemsPerSection - 1;          }          return _sectionIndex;        }        set        {          if (_sectionIndex == -1)            _sectionIndex = value;        }      }      int _section = -1;      public int Section      {        get        {          if (_section == -1)          {            return _index / _panel._averageItemsPerSection;          }          return _section;        }        set        {          if (_section == -1)            _section = value;        }      }    }    class WrapPanelAbstraction : IEnumerable<ItemAbstraction>    {      public WrapPanelAbstraction(int itemCount)      {        List<ItemAbstraction> items = new List<ItemAbstraction>(itemCount);        for (int i = 0; i < itemCount; i++)        {          ItemAbstraction item = new ItemAbstraction(this, i);          items.Add(item);        }        Items = new ReadOnlyCollection<ItemAbstraction>(items);        _averageItemsPerSection = itemCount;        _itemCount = itemCount;      }      public readonly int _itemCount;      public int _averageItemsPerSection;      private int _currentSetSection = -1;      private int _currentSetItemIndex = -1;      private int _itemsInCurrentSecction = 0;      private object _syncRoot = new object();      public int SectionCount      {        get        {          int ret = _currentSetSection + 1;          if (_currentSetItemIndex + 1 < Items.Count)          {            int itemsLeft = Items.Count - _currentSetItemIndex;            ret += itemsLeft / _averageItemsPerSection + 1;          }          return ret;        }      }      private ReadOnlyCollection<ItemAbstraction> Items { get; set; }      public void SetItemSection(int index, int section)      {        lock (_syncRoot)        {          if (section <= _currentSetSection + 1 && index == _currentSetItemIndex + 1)          {            _currentSetItemIndex++;            Items[index].Section = section;            if (section == _currentSetSection + 1)            {              _currentSetSection = section;              if (section > 0)              {                _averageItemsPerSection = (index) / (section);              }              _itemsInCurrentSecction = 1;            }            else              _itemsInCurrentSecction++;            Items[index].SectionIndex = _itemsInCurrentSecction - 1;          }        }      }      public ItemAbstraction this[int index]      {        get { return Items[index]; }      }      #region IEnumerable<ItemAbstraction> Members      public IEnumerator<ItemAbstraction> GetEnumerator()      {        return Items.GetEnumerator();      }      #endregion      #region IEnumerable Members      System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()      {        return GetEnumerator();      }      #endregion    }    #endregion  }View Code

附录:参考引用 

WPF自定义控件与样式(1)-矢量字体图标(iconfont)

WPF自定义控件与样式(2)-自定义按钮FButton

WPF自定义控件与样式(3)-TextBox & RichTextBox & PasswordBox样式、水印、Label标签、功能扩展

WPF自定义控件与样式(4)-CheckBox/RadioButton自定义样式

WPF自定义控件与样式(5)-Calendar/DatePicker日期控件自定义样式及扩展

WPF自定义控件与样式(6)-ScrollViewer与ListBox自定义样式

WPF自定义控件与样式(7)-列表控件DataGrid与ListView自定义样式

WPF自定义控件与样式(8)-ComboBox与自定义多选控件MultComboBox

WPF自定义控件与样式(9)-树控件TreeView与菜单Menu-ContextMenu

WPF自定义控件与样式(10)-进度控件ProcessBar自定义样

WPF自定义控件与样式(11)-等待/忙/正在加载状态-控件实现

版权所有,文章来源: http://www.cnblogs.com/anding

个人能力有限,本文内容仅供学习、探讨,欢迎指正、交流。

本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
审查Java 代码的十一种常见错误
基于Android的Camera2封装,可以实时获取到预览画面和图片
android之壁纸机制
在asp.net中使用jQuery实现类似QQ网站的图片切割效果
silverlight中如何方便在多个"场景"即Xaml文件之间随意切换?
android surfaceview spriter 例子
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服