四季悠闲 發表於 2025-8-11 09:25:00

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

<p>相对于&nbsp;WinForm&nbsp;PictureBox&nbsp;控件原生支持动态&nbsp;GIF,WPF&nbsp;Image&nbsp;控件却不支持,让人摸不着头脑</p>
<h2 id="常用方法">常用方法</h2>
<p>提到&nbsp;WPF&nbsp;播放动图,常见的方法有三种</p>
<h3 id="mediaelement">MediaElement</h3>
<p>使用&nbsp;MediaElement&nbsp;控件,缺点是依赖&nbsp;Media&nbsp;Player,且不支持透明</p>
<pre><code class="language-xml">&lt;MediaElement Source="animation.gif" LoadedBehavior="Play" Stretch="Uniform"/&gt;
</code></pre>
<h3 id="winformpicturebox">WinForm&nbsp;PictureBox</h3>
<p>借助&nbsp;WindowsFormsIntegration&nbsp;嵌入&nbsp;WinForm&nbsp;PictureBox,缺点是不支持透明</p>
<pre><code class="language-xml">&lt;WindowsFormsHost&gt;
    &lt;wf:PictureBox x:Name="winFormsPictureBox"/&gt;
&lt;/WindowsFormsHost&gt;
</code></pre>
<h3 id="wpfanimatedgif">WpfAnimatedGif</h3>
<p>引用&nbsp;NuGet&nbsp;包&nbsp;WpfAnimatedGif,支持透明</p>
<pre><code class="language-xml">&lt;Image gif:ImageBehavior.AnimatedSource="Images/animation.gif"/&gt;
</code></pre>
<p>作者还有另一个性能更好、跨平台的&nbsp;XamlAnimatedGif,用法相同</p>
<h2 id="原生解码方法">原生解码方法</h2>
<p>WPF&nbsp;虽然原生&nbsp;Image&nbsp;不支持&nbsp;GIF&nbsp;动图,但是提供了&nbsp;GifBitmapDecoder&nbsp;解码器,可以获取元数据,包括循环信息、逻辑尺寸、所有帧信息等</p>
<h3 id="判断是否循环和循环次数">判断是否循环和循环次数</h3>
<pre><code class="language-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")
      &amp;&amp; data.GetQuery("/appext/Data") is byte[] array2)
    {
      loop = array2 | array2 &lt;&lt; 8;// 获取循环次数, 0 表示无限循环
      isAnimated = array2 == 1;
    }
}
</code></pre>
<h3 id="获取画布逻辑尺寸">获取画布逻辑尺寸</h3>
<pre><code class="language-csharp">var width = Convert.ToUInt16(data.GetQuery("/logscrdesc/Width"));
var height = Convert.ToUInt16(data.GetQuery("/logscrdesc/Height"));
</code></pre>
<h3 id="获取每一帧信息">获取每一帧信息</h3>
<pre><code class="language-csharp">/// &lt;summary&gt;当前帧播放完成后的处理方法&lt;/summary&gt;
enum DisposalMethod
{
    /// &lt;summary&gt;被全尺寸不透明的下一帧覆盖替换&lt;/summary&gt;
    None,
    /// &lt;summary&gt;不丢弃, 继续显示下一帧未覆盖的任何像素&lt;/summary&gt;
    DoNotDispose,
    /// &lt;summary&gt;重置到背景色&lt;/summary&gt;
    RestoreBackground,
    /// &lt;summary&gt;恢复到上一个未释放的帧的状态&lt;/summary&gt;
    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);
    }
}
</code></pre>
<h3 id="自定义控件完整代码">自定义控件完整代码</h3>
<p>将所有帧画面按其大小位置和顺序放置在&nbsp;Canvas&nbsp;中,结合所有帧的播放处理方法和持续时间,使用关键帧动画,即可实现无需依赖第三方的自定义控件,且性能和&nbsp;XamlAnimatedGif&nbsp;相差无几</p>
<pre><code class="language-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
{
    /// &lt;summary&gt;当前帧播放完成后的处理方法&lt;/summary&gt;
    enum DisposalMethod
    {
      /// &lt;summary&gt;被全尺寸不透明的下一帧覆盖替换&lt;/summary&gt;
      None,
      /// &lt;summary&gt;不丢弃, 继续显示下一帧未覆盖的任何像素&lt;/summary&gt;
      DoNotDispose,
      /// &lt;summary&gt;重置到背景色&lt;/summary&gt;
      RestoreBackground,
      /// &lt;summary&gt;恢复到上一个未释放的帧的状态&lt;/summary&gt;
      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 =&gt; (Uri)GetValue(UriSourceProperty);
      set =&gt; SetValue(UriSourceProperty, value);
    }

    public Stream StreamSource
    {
      get =&gt; (Stream)GetValue(StreamSourceProperty);
      set =&gt; SetValue(StreamSourceProperty, value);
    }

    public int FrameIndex
    {
      get =&gt; (int)GetValue(FrameIndexProperty);
      private set =&gt; SetValue(FrameIndexProperty, value);
    }

    public Stretch Stretch
    {
      get =&gt; (Stretch)GetValue(StretchProperty);
      set =&gt; SetValue(StretchProperty, value);
    }

    public StretchDirection StretchDirection
    {
      get =&gt; (StretchDirection)GetValue(StretchDirectionProperty);
      set =&gt; SetValue(StretchDirectionProperty, value);
    }

    public bool IsLoading
    {
      get =&gt; (bool)GetValue(IsLoadingProperty);
      set =&gt; 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 &amp;&amp; image.Content is Viewbox viewbox)
      {
            viewbox.Stretch = image.Stretch;
      }
    }

    private static void OnStrechDirectionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
      if (d is GifImage image &amp;&amp; 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")
                  &amp;&amp; data.GetQuery("/appext/Data") is byte[] array2)
                {
                  loop = array2 | array2 &lt;&lt; 8;// 获取循环次数, 0表示无限循环
                  isAnimated = array2 == 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;
            for (int i = 0; i &lt; count; i++)
            {
                var info = new FrameInfo(decoder.Frames);
                Image frame = info.Frame;
                frameInfos = 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 &lt; frameInfos.Length; i++)
                {
                  last += TimeSpan.FromMilliseconds(frameInfos.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.Frame.Visibility = Visibility.Visible;
            if (index &gt; 0)
            {
                var previousInfo = frameInfos;
                switch (previousInfo.DisposalMethod)
                {
                  case DisposalMethod.RestoreBackground:
                        // 隐藏之前的所有帧
                        for (int i = 0; i &lt; index - 1; i++)
                        {
                            frameInfos.Frame.Visibility = Visibility.Hidden;
                        }
                        break;
                  case DisposalMethod.RestorePrevious:
                        // 隐藏上一帧
                        previousInfo.Frame.Visibility = Visibility.Hidden;
                        break;
                }
            }
            else
            {
                // 重新循环, 只显示第一帧
                for (int i = 1; i &lt; frameInfos.Length; i++)
                {
                  frameInfos.Frame.Visibility = Visibility.Hidden;
                }
            }
      }
    }
}
</code></pre>
<h4 id="使用到的从url获取图像流的方法">使用到的从&nbsp;URL&nbsp;获取图像流的方法</h4>
<pre><code class="language-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&lt;Stream&gt; 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&lt;Stream&gt;(() =&gt;
            {
                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&lt;Stream&gt;(File.OpenRead(uri.LocalPath));
      }
      throw new FileNotFoundException(uri.OriginalString);
    }
}
</code></pre>
<h3 id="调用示例">调用示例</h3>
<pre><code class="language-xml">&lt;gif:GifImage UriSource="C:\animation.gif"/&gt;
</code></pre>
<h2 id="imageanimator">ImageAnimator</h2>
<p>WinForm&nbsp;中播放&nbsp;GIF&nbsp;用到了&nbsp;ImageAnimator,利用它也可以在&nbsp;WPF&nbsp;中实现&nbsp;GIF&nbsp;动图控件,但其是基于&nbsp;GDI&nbsp;的方法,更推荐性能更好、支持硬解的解码器方法</p>
<pre><code class="language-csharp">// 将多帧图像显示为动画,并触发事件
ImageAnimator.Animate(Image, EventHandler)

// 暂停动画
ImageAnimator.StopAnimate(Image, EventHandler)

// 判断图像是否支持动画
ImageAnimator.CanAnimate(Image)

// 在图像中前进帧,下次渲染图像时绘制新帧
ImageAnimator.UpdateFrames(Image)
</code></pre>
<h2 id="透明gif">透明&nbsp;GIF</h2>
<p>GIF&nbsp;本身只有&nbsp;256&nbsp;色,没有&nbsp;Alpha&nbsp;通道,但其仍支持透明,是通过其特殊的自定义颜色表调色盘实现的</p>
<p><img src="https://img2024.cnblogs.com/blog/1823651/202508/1823651-20250809145354206-1333321854.gif" alt="" loading="lazy"></p>
<p>上图是一张单帧透明&nbsp;GIF,使用&nbsp;Windows&nbsp;自带画图打开,会错误显示为橙色背景</p>
<p><img src="https://img2024.cnblogs.com/blog/1823651/202508/1823651-20250809145354314-618121648.png" alt="" loading="lazy"></p>
<p>放入&nbsp;WinForm&nbsp;PictureBox&nbsp;中,Win7&nbsp;和较旧的&nbsp;Win10&nbsp;也会错误显示为橙色背景</p>
<p>但最新的&nbsp;Win11&nbsp;和&nbsp;Win10&nbsp;上会显示为透明背景,猜测是近期&nbsp;Win11&nbsp;在截图工具中推出了录制&nbsp;GIF&nbsp;功能时顺手更新了&nbsp;.NET&nbsp;System.Drawing&nbsp;GIF&nbsp;解析方法,Win10&nbsp;也收到了这次补丁更新</p>
<p>不过使用&nbsp;WPF&nbsp;解码器方法能过获得正确的背景</p>
<h2 id="相关资料">相关资料</h2>
<p>Table&nbsp;of&nbsp;Contents</p>
<p>Native&nbsp;Image&nbsp;Format&nbsp;Metadata&nbsp;Queries&nbsp;-&nbsp;Win32&nbsp;apps</p>
<p>WICGifGraphicControlExtensionProperties&nbsp;(wincodec.h)&nbsp;-&nbsp;Win32&nbsp;apps&nbsp;|&nbsp;Microsoft&nbsp;Learn</p>
<p>WICGifImageDescriptorProperties&nbsp;(wincodec.h)&nbsp;-&nbsp;Win32&nbsp;apps&nbsp;|&nbsp;Microsoft&nbsp;Learn</p>
<p>在WPF中显示动态GIF&nbsp;-&nbsp;周银辉&nbsp;-&nbsp;博客园</p>
<p>wpf&nbsp;GifBitmapDecoder&nbsp;解析&nbsp;gif&nbsp;格式</p>
<p>浓缩的才是精华:浅析GIF格式图片的存储和压缩&nbsp;-&nbsp;腾讯云开发者&nbsp;-&nbsp;博客园</p><br><br>
来源:https://www.cnblogs.com/BluePointLilac/p/19029227
頁: [1]
查看完整版本: C# WPF 内置解码器实现 GIF 动图控件