C# WPF 內置解碼器實現 GIF 動圖控件
相對于 WinForm PictureBox 控件原生支持動態 GIF,WPF Image 控件卻不支持,讓人摸不著頭腦
常用方法
提到 WPF 播放動圖,常見的方法有三種
MediaElement
使用 MediaElement 控件,缺點是依賴 Media Player,且不支持透明
<MediaElement Source="animation.gif" LoadedBehavior="Play" Stretch="Uniform"/>
WinForm PictureBox
借助 WindowsFormsIntegration 嵌入 WinForm PictureBox,缺點是不支持透明
<WindowsFormsHost>
<wf:PictureBox x:Name="winFormsPictureBox"/>
</WindowsFormsHost>
WpfAnimatedGif
引用 NuGet 包 WpfAnimatedGif,支持透明
<Image gif:ImageBehavior.AnimatedSource="Images/animation.gif"/>
作者還有另一個性能更好、跨平臺的 XamlAnimatedGif,用法相同
原生解碼方法
WPF 雖然原生 Image 不支持 GIF 動圖,但是提供了 GifBitmapDecoder 解碼器,可以獲取元數據,包括循環信息、邏輯尺寸、所有幀信息等
判斷是否循環和循環次數
int loop = 1;
bool isAnimated = true;
var decoder = new GifBitmapDecoder(stream, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.Default);
var data = decoder.Metadata;
if (data.GetQuery("/appext/Application") is byte[] array1)
{
string appName = Encoding.ASCII.GetString(array1);
if ((appName == "NETSCAPE2.0" || appName == "ANIMEXTS1.0")
&& data.GetQuery("/appext/Data") is byte[] array2)
{
loop = array2[2] | array2[3] << 8;// 獲取循環次數, 0 表示無限循環
isAnimated = array2[1] == 1;
}
}
獲取畫布邏輯尺寸
var width = Convert.ToUInt16(data.GetQuery("/logscrdesc/Width"));
var height = Convert.ToUInt16(data.GetQuery("/logscrdesc/Height"));
獲取每一幀信息
/// <summary>當前幀播放完成后的處理方法</summary>
enum DisposalMethod
{
/// <summary>被全尺寸不透明的下一幀覆蓋替換</summary>
None,
/// <summary>不丟棄, 繼續顯示下一幀未覆蓋的任何像素</summary>
DoNotDispose,
/// <summary>重置到背景色</summary>
RestoreBackground,
/// <summary>恢復到上一個未釋放的幀的狀態</summary>
RestorePrevious,
}
sealed class FrameInfo
{
public Image Frame { get; }
public int DelayTime { get; }
public DisposalMethod DisposalMethod { get; }
public FrameInfo(BitmapFrame frame)
{
Frame = new Image { Source = frame };
var data = (BitmapMetadata)frame.Metadata;
DelayTime = Convert.ToUInt16(data.GetQuery("/grctlext/Delay"));
DisposalMethod = (DisposalMethod)Convert.ToByte(data.GetQuery("/grctlext/Disposal"));
ushort left = Convert.ToUInt16(data.GetQuery("/imgdesc/Left"));
ushort top = Convert.ToUInt16(data.GetQuery("/imgdesc/Top"));
ushort width = Convert.ToUInt16(data.GetQuery("/imgdesc/Width"));
ushort height = Convert.ToUInt16(data.GetQuery("/imgdesc/Height"));
Canvas.SetLeft(Frame, left);
Canvas.SetTop(Frame, top);
Canvas.SetRight(Frame, left + width);
Canvas.SetBottom(Frame, top + height);
}
}
自定義控件完整代碼
將所有幀畫面按其大小位置和順序放置在 Canvas 中,結合所有幀的播放處理方法和持續時間,使用關鍵幀動畫,即可實現無需依賴第三方的自定義控件,且性能和 XamlAnimatedGif 相差無幾
using System;
using System.IO;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Media.Imaging;
public sealed class GifImage : ContentControl
{
/// <summary>當前幀播放完成后的處理方法</summary>
enum DisposalMethod
{
/// <summary>被全尺寸不透明的下一幀覆蓋替換</summary>
None,
/// <summary>不丟棄, 繼續顯示下一幀未覆蓋的任何像素</summary>
DoNotDispose,
/// <summary>重置到背景色</summary>
RestoreBackground,
/// <summary>恢復到上一個未釋放的幀的狀態</summary>
RestorePrevious,
}
sealed class FrameInfo
{
public Image Frame { get; }
public int DelayTime { get; }
public DisposalMethod DisposalMethod { get; }
public FrameInfo(BitmapFrame frame)
{
Frame = new Image { Source = frame };
var data = (BitmapMetadata)frame.Metadata;
DelayTime = Convert.ToUInt16(data.GetQuery("/grctlext/Delay"));
DisposalMethod = (DisposalMethod)Convert.ToByte(data.GetQuery("/grctlext/Disposal"));
ushort left = Convert.ToUInt16(data.GetQuery("/imgdesc/Left"));
ushort top = Convert.ToUInt16(data.GetQuery("/imgdesc/Top"));
ushort width = Convert.ToUInt16(data.GetQuery("/imgdesc/Width"));
ushort height = Convert.ToUInt16(data.GetQuery("/imgdesc/Height"));
Canvas.SetLeft(Frame, left);
Canvas.SetTop(Frame, top);
Canvas.SetRight(Frame, left + width);
Canvas.SetBottom(Frame, top + height);
}
}
public static readonly DependencyProperty UriSourceProperty =
DependencyProperty.Register(nameof(UriSource), typeof(Uri), typeof(GifImage), new PropertyMetadata(null, OnSourceChanged));
public static readonly DependencyProperty StreamSourceProperty =
DependencyProperty.Register(nameof(StreamSource), typeof(Stream), typeof(GifImage), new PropertyMetadata(null, OnSourceChanged));
public static readonly DependencyProperty FrameIndexProperty =
DependencyProperty.Register(nameof(FrameIndex), typeof(int), typeof(GifImage), new PropertyMetadata(0, OnFrameIndexChanged));
public static readonly DependencyProperty StretchProperty =
DependencyProperty.Register(nameof(Stretch), typeof(Stretch), typeof(GifImage), new PropertyMetadata(Stretch.None, OnStrechChanged));
public static readonly DependencyProperty StretchDirectionProperty =
DependencyProperty.Register(nameof(StretchDirection), typeof(StretchDirection), typeof(GifImage), new PropertyMetadata(StretchDirection.Both, OnStrechDirectionChanged));
public static readonly DependencyProperty IsLoadingProperty =
DependencyProperty.Register(nameof(IsLoading), typeof(bool), typeof(GifImage), new PropertyMetadata(false));
public Uri UriSource
{
get => (Uri)GetValue(UriSourceProperty);
set => SetValue(UriSourceProperty, value);
}
public Stream StreamSource
{
get => (Stream)GetValue(StreamSourceProperty);
set => SetValue(StreamSourceProperty, value);
}
public int FrameIndex
{
get => (int)GetValue(FrameIndexProperty);
private set => SetValue(FrameIndexProperty, value);
}
public Stretch Stretch
{
get => (Stretch)GetValue(StretchProperty);
set => SetValue(StretchProperty, value);
}
public StretchDirection StretchDirection
{
get => (StretchDirection)GetValue(StretchDirectionProperty);
set => SetValue(StretchDirectionProperty, value);
}
public bool IsLoading
{
get => (bool)GetValue(IsLoadingProperty);
set => SetValue(IsLoadingProperty, value);
}
private static void OnSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((GifImage)d)?.OnSourceChanged();
}
private static void OnFrameIndexChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((GifImage)d)?.OnFrameIndexChanged();
}
private static void OnStrechChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is GifImage image && image.Content is Viewbox viewbox)
{
viewbox.Stretch = image.Stretch;
}
}
private static void OnStrechDirectionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is GifImage image && image.Content is Viewbox viewbox)
{
viewbox.StretchDirection = image.StretchDirection;
}
}
Stream stream;
Canvas canvas;
FrameInfo[] frameInfos;
Int32AnimationUsingKeyFrames animation;
public GifImage()
{
IsVisibleChanged += OnIsVisibleChanged;
Unloaded += OnUnloaded;
}
private void OnUnloaded(object sender, RoutedEventArgs e)
{
Release();
}
private void OnIsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
{
if (IsVisible)
{
StartAnimation();
}
else
{
StopAnimation();
}
}
private void StartAnimation()
{
BeginAnimation(FrameIndexProperty, animation);
}
private void StopAnimation()
{
BeginAnimation(FrameIndexProperty, null);
}
private void Release()
{
StopAnimation();
canvas?.Children.Clear();
stream?.Dispose();
animation = null;
frameInfos = null;
}
private async void OnSourceChanged()
{
Release();
IsLoading = true;
FrameIndex = 0;
if (UriSource != null)
{
stream = await ResourceHelper.GetStream(UriSource);
}
else
{
stream = StreamSource;
}
if (stream != null)
{
int loop = 1;
bool isAnimated = true;
var decoder = new GifBitmapDecoder(stream, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.Default);
var data = decoder.Metadata;
if (data.GetQuery("/appext/Application") is byte[] array1)
{
string appName = Encoding.ASCII.GetString(array1);
if ((appName == "NETSCAPE2.0" || appName == "ANIMEXTS1.0")
&& data.GetQuery("/appext/Data") is byte[] array2)
{
loop = array2[2] | array2[3] << 8;// 獲取循環次數, 0表示無限循環
isAnimated = array2[1] == 1;
}
}
if (!(Content is Viewbox viewbox))
{
Content = viewbox = new Viewbox
{
Stretch = Stretch,
StretchDirection = StretchDirection,
};
}
if (canvas == null || canvas.Parent != Content)
{
canvas = new Canvas();
viewbox.Child = canvas;
}
canvas.Width = Convert.ToUInt16(data.GetQuery("/logscrdesc/Width"));
canvas.Height = Convert.ToUInt16(data.GetQuery("/logscrdesc/Height"));
int count = decoder.Frames.Count;
frameInfos = new FrameInfo[count];
for (int i = 0; i < count; i++)
{
var info = new FrameInfo(decoder.Frames[i]);
Image frame = info.Frame;
frameInfos[i] = info;
canvas.Children.Add(frame);
Panel.SetZIndex(frame, i);
canvas.Width = Math.Max(canvas.Width, Canvas.GetRight(frame));
canvas.Height = Math.Max(canvas.Height, Canvas.GetBottom(frame));
}
OnFrameIndexChanged();
if (isAnimated)
{
var keyFrames = new Int32KeyFrameCollection();
var last = TimeSpan.Zero;
for (int i = 0; i < frameInfos.Length; i++)
{
last += TimeSpan.FromMilliseconds(frameInfos[i].DelayTime * 10);
keyFrames.Add(new DiscreteInt32KeyFrame(i, last));
}
animation = new Int32AnimationUsingKeyFrames
{
KeyFrames = keyFrames,
RepeatBehavior = loop == 0 ? RepeatBehavior.Forever : new RepeatBehavior(loop)
};
StartAnimation();
}
}
IsLoading = false;
}
private void OnFrameIndexChanged()
{
if (frameInfos != null)
{
int index = FrameIndex;
frameInfos[index].Frame.Visibility = Visibility.Visible;
if (index > 0)
{
var previousInfo = frameInfos[index - 1];
switch (previousInfo.DisposalMethod)
{
case DisposalMethod.RestoreBackground:
// 隱藏之前的所有幀
for (int i = 0; i < index - 1; i++)
{
frameInfos[i].Frame.Visibility = Visibility.Hidden;
}
break;
case DisposalMethod.RestorePrevious:
// 隱藏上一幀
previousInfo.Frame.Visibility = Visibility.Hidden;
break;
}
}
else
{
// 重新循環, 只顯示第一幀
for (int i = 1; i < frameInfos.Length; i++)
{
frameInfos[i].Frame.Visibility = Visibility.Hidden;
}
}
}
}
}
使用到的從 URL 獲取圖像流的方法
using System;
using System.IO;
using System.IO.Packaging;
using System.Net;
using System.Threading.Tasks;
using System.Windows;
public static class ResourceHelper
{
public static Task<Stream> GetStream(Uri uri)
{
if (!uri.IsAbsoluteUri)
{
throw new ArgumentException("uri must be absolute");
}
if (uri.Scheme == Uri.UriSchemeHttps
|| uri.Scheme == Uri.UriSchemeHttp
|| uri.Scheme == Uri.UriSchemeFtp)
{
return Task.Run<Stream>(() =>
{
using (var client = new WebClient())
{
byte[] data = client.DownloadData(uri);
return new MemoryStream(data);
}
});
}
else if (uri.Scheme == PackUriHelper.UriSchemePack)
{
var info = uri.Authority == "siteoforigin:,,,"
? Application.GetRemoteStream(uri)
: Application.GetResourceStream(uri);
if (info != null)
{
return Task.FromResult(info.Stream);
}
}
else if (uri.Scheme == Uri.UriSchemeFile)
{
return Task.FromResult<Stream>(File.OpenRead(uri.LocalPath));
}
throw new FileNotFoundException(uri.OriginalString);
}
}
調用示例
<gif:GifImage UriSource="C:\animation.gif"/>
ImageAnimator
WinForm 中播放 GIF 用到了 ImageAnimator,利用它也可以在 WPF 中實現 GIF 動圖控件,但其是基于 GDI 的方法,更推薦性能更好、支持硬解的解碼器方法
// 將多幀圖像顯示為動畫,并觸發事件
ImageAnimator.Animate(Image, EventHandler)
// 暫停動畫
ImageAnimator.StopAnimate(Image, EventHandler)
// 判斷圖像是否支持動畫
ImageAnimator.CanAnimate(Image)
// 在圖像中前進幀,下次渲染圖像時繪制新幀
ImageAnimator.UpdateFrames(Image)
透明 GIF
GIF 本身只有 256 色,沒有 Alpha 通道,但其仍支持透明,是通過其特殊的自定義顏色表調色盤實現的

上圖是一張單幀透明 GIF,使用 Windows 自帶畫圖打開,會錯誤顯示為橙色背景

放入 WinForm PictureBox 中,Win7 和較舊的 Win10 也會錯誤顯示為橙色背景
但最新的 Win11 和 Win10 上會顯示為透明背景,猜測是近期 Win11 在截圖工具中推出了錄制 GIF 功能時順手更新了 .NET System.Drawing GIF 解析方法,Win10 也收到了這次補丁更新
不過使用 WPF 解碼器方法能過獲得正確的背景
相關資料
Native Image Format Metadata Queries - Win32 apps
WICGifGraphicControlExtensionProperties (wincodec.h) - Win32 apps | Microsoft Learn
WICGifImageDescriptorProperties (wincodec.h) - Win32 apps | Microsoft Learn
[WPF疑難]在WPF中顯示動態GIF - 周銀輝 - 博客園

浙公網安備 33010602011771號