目录
- 常用方法
- 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 通道,但其仍支持透明,是通过其特殊的自定义颜色表调色盘实现的
上图是一张单帧透明 GIF,使用 Windows 自带画图打开,会错误显示为橙色背景
放入 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)!
精彩评论