一.前言
申明: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 < )
{
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 < )
{
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 = ;
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()); /// <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(, );
private Size _extent = new Size(, );
private Size _viewport = new Size(, );
private int firstIndex = ;
private Size childSize;
private Size _pixelMeasuredViewport = new Size(, );
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 != )
{
int firstIndexCache = firstIndex;
_abstractPanel = null;
MeasureOverride(_viewport);
SetFirstRowViewItemIndex(firstIndex);
firstIndex = firstIndexCache;
}
} public int GetFirstVisibleSection()
{
int section;
if (_abstractPanel == null) return ;
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 ;
int section = GetFirstVisibleSection();
var item = _abstractPanel.Where(x => x.Section == section).FirstOrDefault();
if (item != null)
return item._index;
return ;
} private void CleanUpItems(int minDesiredGenerated, int maxDesiredGenerated)
{
for (int i = _children.Count - ; i >= ; i--)
{
GeneratorPosition childGeneratorPos = new GeneratorPosition(i, );
int itemIndex = _generator.IndexFromGeneratorPosition(childGeneratorPos);
if (itemIndex < minDesiredGenerated || itemIndex > maxDesiredGenerated)
{
_generator.Remove(childGeneratorPos, );
RemoveInternalChildRange(i, );
}
}
} 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 - ; }
else
{
_extent.Width = _abstractPanel.SectionCount + ViewportWidth - ;
}
_owner.InvalidateScrollInfo();
} private void ResetScrollInfo()
{
_offset.X = ;
_offset.Y = ;
} private int GetNextSectionClosestIndex(int itemIndex)
{
var abstractItem = _abstractPanel[itemIndex];
if (abstractItem.Section < _abstractPanel.SectionCount - )
{
var ret = _abstractPanel.
Where(x => x.Section == abstractItem.Section + ).
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 > )
{
var ret = _abstractPanel.
Where(x => x.Section == abstractItem.Section - ).
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 = ;
while (itemIndex == -)
{
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 + );
UpdateLayout();
next = gen.ContainerFromIndex(nextIndex);
}
}
else
{
if (itemIndex == _abstractPanel._itemCount - )
return;
next = gen.ContainerFromIndex(itemIndex + );
while (next == null)
{
SetHorizontalOffset(HorizontalOffset + );
UpdateLayout();
next = gen.ContainerFromIndex(itemIndex + );
}
}
while (depth != )
{
next = VisualTreeHelper.GetChild(next, );
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 = ;
while (itemIndex == -)
{
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 - );
UpdateLayout();
next = gen.ContainerFromIndex(nextIndex);
}
}
else
{
if (itemIndex == )
return;
next = gen.ContainerFromIndex(itemIndex - );
while (next == null)
{
SetVerticalOffset(VerticalOffset - );
UpdateLayout();
next = gen.ContainerFromIndex(itemIndex - );
}
}
while (depth != )
{
next = VisualTreeHelper.GetChild(next, );
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 = ;
while (itemIndex == -)
{
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 + );
UpdateLayout();
next = gen.ContainerFromIndex(nextIndex);
}
}
else
{
if (itemIndex == _abstractPanel._itemCount - )
return;
next = gen.ContainerFromIndex(itemIndex + );
while (next == null)
{
SetVerticalOffset(VerticalOffset + );
UpdateLayout();
next = gen.ContainerFromIndex(itemIndex + );
}
}
while (depth != )
{
next = VisualTreeHelper.GetChild(next, );
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 = ;
while (itemIndex == -)
{
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 - );
UpdateLayout();
next = gen.ContainerFromIndex(nextIndex);
}
}
else
{
if (itemIndex == )
return;
next = gen.ContainerFromIndex(itemIndex - );
while (next == null)
{
SetHorizontalOffset(HorizontalOffset - );
UpdateLayout();
next = gen.ContainerFromIndex(itemIndex - );
}
}
while (depth != )
{
next = VisualTreeHelper.GetChild(next, );
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 == )
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 == ) ? startPos.Index : startPos.Index + ;
int current = firstVisibleIndex;
int visibleSections = ;
using (_generator.StartAt(startPos, GeneratorDirection.Forward, true))
{
bool stop = false;
bool isHorizontal = Orientation == Orientation.Horizontal;
double currentX = ;
double currentY = ;
double maxItemSize = ;
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 = ;
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 = ;
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 - ); 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 + );
else
SetVerticalOffset(VerticalOffset + );
} public void LineLeft()
{
if (Orientation == Orientation.Horizontal)
SetHorizontalOffset(HorizontalOffset - );
else
SetHorizontalOffset(HorizontalOffset - );
} public void LineRight()
{
if (Orientation == Orientation.Horizontal)
SetHorizontalOffset(HorizontalOffset + );
else
SetHorizontalOffset(HorizontalOffset + );
} public void LineUp()
{
if (Orientation == Orientation.Vertical)
SetVerticalOffset(VerticalOffset - );
else
SetVerticalOffset(VerticalOffset - );
} public Rect MakeVisible(Visual visual, Rect rectangle)
{
var gen = (ItemContainerGenerator)_generator.GetItemContainerGeneratorForPanel(this);
var element = (UIElement)visual;
int itemIndex = gen.IndexFromContainer(element);
while (itemIndex == -)
{
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 += ;
else if (elementRect.Top < )
_offset.Y -= ;
}
else
{
double viewportWidth = _pixelMeasuredViewport.Width;
if (elementRect.Right > viewportWidth)
_offset.X += ;
else if (elementRect.Left < )
_offset.X -= ;
}
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 < || _viewport.Width >= _extent.Width)
{
offset = ;
}
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 < || _viewport.Height >= _extent.Height)
{
offset = ;
}
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 = -;
public int SectionIndex
{
get
{
if (_sectionIndex == -)
{
return _index % _panel._averageItemsPerSection - ;
}
return _sectionIndex;
}
set
{
if (_sectionIndex == -)
_sectionIndex = value;
}
} int _section = -;
public int Section
{
get
{
if (_section == -)
{
return _index / _panel._averageItemsPerSection;
}
return _section;
}
set
{
if (_section == -)
_section = value;
}
}
} class WrapPanelAbstraction : IEnumerable<ItemAbstraction>
{
public WrapPanelAbstraction(int itemCount)
{
List<ItemAbstraction> items = new List<ItemAbstraction>(itemCount);
for (int i = ; 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 = -;
private int _currentSetItemIndex = -;
private int _itemsInCurrentSecction = ;
private object _syncRoot = new object(); public int SectionCount
{
get
{
int ret = _currentSetSection + ;
if (_currentSetItemIndex + < Items.Count)
{
int itemsLeft = Items.Count - _currentSetItemIndex;
ret += itemsLeft / _averageItemsPerSection + ;
}
return ret;
}
} private ReadOnlyCollection<ItemAbstraction> Items { get; set; } public void SetItemSection(int index, int section)
{
lock (_syncRoot)
{
if (section <= _currentSetSection + && index == _currentSetItemIndex + )
{
_currentSetItemIndex++;
Items[index].Section = section;
if (section == _currentSetSection + )
{
_currentSetSection = section;
if (section > )
{
_averageItemsPerSection = (index) / (section);
}
_itemsInCurrentSecction = ;
}
else
_itemsInCurrentSecction++;
Items[index].SectionIndex = _itemsInCurrentSecction - ;
}
}
} 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
}