相对于 WinForm PictureBox 控件原生支持动态 GIF,WPF Image 控件却不支持,让人摸不着头脑
常用方法
提到 WPF 播放动图,常见的方法有三种
MediaElement
使用 MediaElement 控件,缺点是依赖 Media Player,且不支持透明
xml
<MediaElement Source="animation.gif" LoadedBehavior="Play" Stretch="Uniform"/>
WinForm PictureBox
借助 WindowsFormsIntegration 嵌入 WinForm PictureBox,缺点是不支持透明
xml
<WindowsFormsHost>
<wf:PictureBox x:Name="winFormsPictureBox"/>
</WindowsFormsHost>
WpfAnimatedGif
引用 NuGet 包 WpfAnimatedGif,支持透明
xml
<Image gif:ImageBehavior.AnimatedSource="Images/animation.gif"/>
作者还有另一个性能更好、跨平台的 XamlAnimatedGif,用法相同
原生解码方法
WPF 虽然原生 Image 不支持 GIF 动图,但是提供了 GifBitmapDecoder 解码器,可以获取元数据,包括循环信息、逻辑尺寸、所有帧信息等
判断是否循环和循环次数
csharp
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;
}
}
获取画布逻辑尺寸
csharp
var width = Convert.ToUInt16(data.GetQuery("/logscrdesc/Width"));
var height = Convert.ToUInt16(data.GetQuery("/logscrdesc/Height"));
获取每一帧信息
csharp
/// <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 相差无几
csharp
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 获取图像流的方法
csharp
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);
}
}
ImageAnimator
WinForm 中播放 GIF 用到了 ImageAnimator,利用它也可以在 WPF 中实现 GIF 动图控件,但其是基于 GDI 的方法,更推荐性能更好、支持硬解的解码器方法
csharp
// 将多帧图像显示为动画,并触发事件
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 - 周银辉 - 博客园