开发者

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

目录
  • 常用方法
    • MediaElement
    • WinForm PictureBox
    • wpfAnimatedGif
  • 原生解码方法
    • 判断是否循环和循环次数
    • 获取画布逻辑尺寸
    • 获取每一帧信息
    • 自定义控件完整代码
      • 使用到的从 URL 获取图像流的方法
    • 调用示例
    • ImageAnimator
      • 透明 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 PropertyMewww.devze.comtadata(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 Repeathttp://www.devze.comBehavior(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# WPF 内置解码器实现 GIF 动图控件的方法

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

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

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

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

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

          相关资料

          Table of Contents

          Native Image Format Metadata Queries - Win32 apps

          WICGifGraphhttp://www.devze.comicControlExtensionProperties (wincodec.h) - Win32 apps | Microsoft Learn

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

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

          wpf GifBitmapDecoder 解析 gif 格式

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

          到此这篇关于C# WPF 内置解码器实现 GIF 动图控件的方法的文章就介绍到这了,更多相关C# GIF 动图控件内容请搜索编程客栈(www.devze.com)以前的文章或继续浏览下面的相关文章希望大家以后多多AsUxFpJxv支持编程客栈(www.devze.com)!

          0

          上一篇:

          下一篇:

          精彩评论

          暂无评论...
          验证码 换一张
          取 消

          最新开发

          开发排行榜