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 通道,但其仍支持透明,是通过其特殊的自定义颜色表调色盘实现的

C#&#160;WPF&#160;内置解码器实现&#160;GIF&#160;动图控件

上图是一张单帧透明 GIF,使用 Windows 自带画图打开,会错误显示为橙色背景

C#&#160;WPF&#160;内置解码器实现&#160;GIF&#160;动图控件

放入 WinForm PictureBox 中,Win7 和较旧的 Win10 也会错误显示为橙色背景

但最新的 Win11 和 Win10 上会显示为透明背景,猜测是近期 Win11 在截图工具中推出了录制 GIF 功能时顺手更新了 .NET System.Drawing GIF 解析方法,Win10 也收到了这次补丁更新

不过使用 WPF 解码器方法能过获得正确的背景

相关资料

Table of Contents

Native Image Format Metadata Queries - Win32 apps

WICGifGraphicControlExtensionProperties (wincodec.h) - Win32 apps | Microsoft Learn

WICGifImageDescriptorProperties (wincodec.h) - Win32 apps | Microsoft Learn

[WPF疑难]在WPF中显示动态GIF - 周银辉 - 博客园

wpf GifBitmapDecoder 解析 gif 格式

浓缩的才是精华:浅析GIF格式图片的存储和压缩 - 腾讯云开发者 - 博客园

发表评论

评论已关闭。

相关文章