一.前言
申明: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;
}
}
五.圖檔清單樣式,支援大資料量的虛拟化
先看看效果圖(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
}
附錄:參考引用
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
個人能力有限,本文内容僅供學習、探讨,歡迎指正、交流。