[WPF疑难]在WPF中显示动态GIF

                    [WPF疑难]在WPF中显示动态GIF
                            周银辉

在我们寻求帮助的时候,最不愿意听到的答复是:很抱歉,在当前版本的产品中还没有实现该功能... 在WPF中显示动态的GIF图像时便遇到了这样的问题,WPF中强大的Image控件却不支持动态的GIF(其只能显示第一帧).当然,我们可以说WPF强大的动画能力,让我们完全有理由抛弃传统的GIF动画,但如某种情况下如果你觉得使用动态的GIF更合适的话(比如QQ表情,因为GIF是利于保存和传输的),没关系,本篇随笔将帮助你解决这个问题.

1,曾有过的尝试:
我们在实际开发过程中也遇到显示动态GIF的问题.发现普通的Image控件不能正常显示后,我们又发现网页浏览器却是可以的,以及windows XP的"图片和传真查看器"也可以,但"Window Live照片库"却不可以.所以我们最初打算使用通过包装WebBrowseControl来实现,即是在WPF中host一个.net2.0中的浏览器控件,然后让该浏览器来实现图片,成功了,但麻烦的事情是鼠标右键可以点出网页的上下文菜单.我们放弃了该方案,除了不愿意花时间来屏蔽上下文菜单和浏览器控件的多余功能外,同时我们的觉得浏览器控件过于"重量级",有点杀鸡用牛刀的感觉.另外,你可能会想到使用WPF中的Frame控件,但也会得到上述结果.另外,有网友说可以使用MediaElement控件,但大都没有成功,我也没有(可能是RP不够哈,呵呵...)

2,GifBitmapDecoder
我们发现WPF中有一个名为GifBitmapDecoder的类,其可以将动态GIF分解成很多帧并保存在一个列表中,每一帧为一个BitmapFrame类型的对象,其父类为BitmapSource,这也就意味着,我们可以将每一帧赋值给一个Image控件的Source属性,这样我们可以得到针对GIF各帧的Image系列:
            GifBitmapDecoder decoder = new GifBitmapDecoder(
                           
new Uri("OH.gif", UriKind.Relative),
                           BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.Default);

            
foreach (BitmapFrame f in decoder.Frames)
            
{
                Image image 
= new Image();
                image.Source 
= f;
                
this.panel1.Children.Add(image);
            }
下图为将一个GIF图片的12帧分解出来的所得到的一个系列图:

不过先别高兴,这还不足以解决我们的问题,因为我们不知道每一帧显示的时间(帧与帧之间切换的时间间隔),以及一帧显示结束后它的处理方法(是显示下一帧吗?是显示背景色吗?等等...)所以我们还必须一个字节一个字节的解析GIF文件以便得到足够多的信息.

3,解析GIF
要解析文件就必须知道文件的存储结构,关于动态GIF的文件存储结构,可以参考这里:http://blog.zhongmoo.cn/post/45.html
比如,得到帧的显示时间的方法是这样的:
        private int ParseGraphicControlExtension(byte[] gifData, int offset)
        
{
            
int returnOffset = offset;
            
// Extension Block
            int length = gifData[offset + 2];
            returnOffset 
= offset + length + 2 + 1;

            
byte packedField = gifData[offset + 3];
            currentParseGifFrame.disposalMethod 
= (packedField & 0x1C>> 2;

            
// Get DelayTime
            int delay = BitConverter.ToUInt16(gifData, offset + 4);
            currentParseGifFrame.delayTime = delay;
            
while (gifData[returnOffset] != 0x00)
            
{
                returnOffset 
= returnOffset + gifData[returnOffset] + 1;
            }


            returnOffset
++;

            
return returnOffset;
        }

关于如何解析就不多介绍了,你只有了解其文件结构然后不断地移动读取游标和读取相应的字节就可以完成了.

4,包装成控件
我们想要的最佳效果是,打造一个GifImage控件,就跟Image控件差不多,只要我们指定它的Source属性,然后其就自动查找GIF文件并读取或下载,然后解析并显示.
所以,我们将该控件分成了两个部分,一个部分负责将根据用户指定的Source属性查找并读取或从网络下载GIF到内存流,然后另外一部分负责将得到的内存流解析并显示出来.
gif文件在哪里?这是一个必须考虑到的问题,控件用户指定的是一个绝对路径吗,还是一个相对路径,是本地文件还是内嵌的资源文件或者是网络上的文件.还有该文件一定支持GIF动画吗,还是只是一个普通的静态图片,所以负责读取文件到内存流的代码中应该有类似于下面的代码:
            if (source.Trim().ToUpper().EndsWith(".GIF"|| ForceGifAnim)
            
{
                
if (!uri.IsAbsoluteUri)
                
{
                    

                    GetGifStreamFromPack(uri);
                }

                
else
                
{

                    
string leftPart = uri.GetLeftPart(UriPartial.Scheme);

                    
if (leftPart == "http://" || leftPart == "ftp://" || leftPart == "file://")
                    
{
                        

                        GetGifStreamFromHttp(uri);
                    }

                    
else if (leftPart == "pack://")
                    
{
                        
 
                        GetGifStreamFromPack(uri);
                    }

                    
else
                    
{
                        
//创建无动画的普通Image
                        CreateNonGifAnimationImage();
                    }

                }

            }

            
else
            
{
                
//创建无动画的普通Image
                CreateNonGifAnimationImage();
            }

        }
当读取文件成功后,一切都好办了,通过解析内存流中的数据,我们可以得到足够多的信息,比如帧的列表,每帧显示的时间以及该帧显示完成后如何处理,那么我们就可以用一个计时器(DispatcherTimer)来处理这一切而形成一个动画了.
        /// <summary>
        
/// 从内存流中创建图片
        
/// </summary>

        public void CreateGifAnimation(MemoryStream memoryStream)
        
{
            Reset();

            
byte[] gifData = memoryStream.GetBuffer();  

            GifBitmapDecoder decoder 
= new GifBitmapDecoder(memoryStream, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.Default);

            numberOfFrames 
= decoder.Frames.Count;

            
try
            
{
                ParseGif(gifData);
            }

            
catch
            
{
                
throw new FileFormatException("Unable to parse Gif file format.");
            }


            
for (int i = 0; i < decoder.Frames.Count; i++)
            
{
                frameList[i].Source 
= decoder.Frames[i];
                frameList[i].Visibility 
= Visibility.Hidden;
                canvas.Children.Add(frameList[i]);
                Canvas.SetLeft(frameList[i], frameList[i].left);
                Canvas.SetTop(frameList[i], frameList[i].top);
                Canvas.SetZIndex(frameList[i], i);
            }

            canvas.Height 
= logicalHeight;
            canvas.Width 
= logicalWidth;

            frameList[
0].Visibility = Visibility.Visible;

            
for (int i = 0; i < frameList.Count; i++)
            
{
                Console.WriteLine(frameList[i].disposalMethod.ToString() 
+ " " + frameList[i].width.ToString() + " " + frameList[i].delayTime.ToString());
            }


            
if (frameList.Count > 1)
            
{
                
if (numberOfLoops == -1)
                
{
                    numberOfLoops 
= 1;
                }

                frameTimer 
= new System.Windows.Threading.DispatcherTimer();
                frameTimer.Tick 
+= NextFrame;
                frameTimer.Interval 
= new TimeSpan(0000, frameList[0].delayTime * 10);
                frameTimer.Start();
            }

        }

OK,我们可以像使用Image控件一样来使用我们的GifImage控件了:


下载源代码

posted @ 2007-12-23 16:03 周银辉 阅读(5366) 评论(22) 编辑 收藏

 回复 引用 查看   
#1楼 2007-12-23 16:18 韩现龙      
收藏下,虽然还没开始研究wpf。
 回复 引用 查看   
#2楼 2007-12-23 18:25 ocean      
非常不错非常不错,受教!
 回复 引用 查看   
#3楼 2007-12-23 19:56 Leepy      
不错!呵呵,这图还选得满有意思:)
 回复 引用 查看   
#4楼 2007-12-24 00:24 光阴四溅      
media elment 我也试过 是不行的
 回复 引用 查看   
#5楼 2007-12-24 11:26 Clark Zheng      
过两天正要用这个功能,收藏了,谢谢博主
 回复 引用 查看   
#6楼 2007-12-24 11:36 天方      
gif好像用的越来越少了。静态图片逐渐被png代替,动画方面逐渐被flash代替。
 回复 引用   
#7楼 2007-12-24 14:57 在线代理[未注册用户]
高啊,可惜公司没有机会研究wpf。

 回复 引用   
#8楼 2007-12-25 11:09 Y[未注册用户]
请教下~
项目打不开,估计会是什么问题呢?
在用GifBitmapDecoder的时候就出现异常,XamlParseException。。。什么东西?

 回复 引用 查看   
#9楼[楼主] 2007-12-25 11:14 周银辉      
@Y
打开项目需要VS2008,至于异常应该不会,你清理项目然后重新生成一下试试

 回复 引用   
#10楼 2007-12-25 12:43 Y[未注册用户]
GifBitmapDecoder gif = new GifBitmapDecoder(new Uri("xiongmao.gif", UriKind.Relative), BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.Default);

运行到这句异常的,重新生成还是会,不会是VS2005的关系吧?

 回复 引用 查看   
#11楼[楼主] 2007-12-25 12:59 周银辉      
@Y
呵呵,我猜想,你的异常是不是找不到图片资源啊?你新添加的图片资源是什么形式的?内嵌还是Content,复制到输出目录还是不复制,你打开我的工程,在解决方案管理器的图片文件上右击->属性,对比一下我的图片资源属性和你的有何不同

 回复 引用   
#12楼 2007-12-25 13:24 Y[未注册用户]
就是打不开你的项目啊T_T
图片资源是content的,没有复制到输出目录。。。应该是怎么样?

 回复 引用 查看   
#13楼[楼主] 2007-12-25 13:31 周银辉      
@Y
郁闷...打开项目要VS2008!!!

 回复 引用   
#14楼 2007-12-25 13:53 Y[未注册用户]
知道啊,只有2005没办法嘛= =
 回复 引用   
#15楼 2008-01-08 22:02 二十三画生[未注册用户]
加入这个讨论空间吧 主要是针对.NET方向的 6037854
 回复 引用   
#16楼 2008-01-31 11:42 微笑装坚强[未注册用户]
呵呵,看了你这个DEMO
代码杂那么多呢
显示一个GIF就需要这么多代码吗?
还有就是你的代码怎么没有写注释勒,o(∩_∩)o...
看的好费劲啊。

 回复 引用 查看   
#17楼 2010-06-02 16:34 伊雪      
添加到RichTextBox中去以后,对其Document进行序列化报错.
无法序列化非公共类型System.Windows.Media.Imaging.BitmapFrameDecode
请问如何解决?

 回复 引用 查看   
#18楼 2010-06-02 16:35 伊雪      
zhizhewuhuo@yeha.net我的联系邮箱
 回复 引用 查看   
#19楼 2011-04-21 15:30 Bennet      
做成一个图片浏览功能的话,显示第一张是没问题的,切换到另一张的时候在GifImage类的private void CreateGifAnimation(MemoryStream memoryStream)方法中AddChild的时候报错,错误信息为"ContentControl 的内容必须为单个元素。"请问有什么办法解决吗?
 回复 引用 查看   
#20楼 2011-08-16 11:33 Evil-Steve      
引用Bennet:做成一个图片浏览功能的话,显示第一张是没问题的,切换到另一张的时候在GifImage类的private void CreateGifAnimation(MemoryStream memoryStream)方法中AddChild的时候报错,错误信息为"ContentControl 的内容必须为单个元素。"请问有什么办法解决吗?


你的gif到里面的 播放速度 也是一样的吗?

 回复 引用 查看   
#21楼 2011-11-30 19:19 荒原风雪      
用了之后不错,但是内存暴涨得的很快,释放不了,还得麻烦楼主改造改造。
 回复 引用 查看   
#22楼 2012-01-20 19:46 爱让一切都对了      
if (frameList[frameCounter].disposalMethod == 2)
{
frameList[frameCounter].Visibility = Visibility.Hidden;
}
if (frameList[frameCounter].disposalMethod >= 3)
{
frameList[frameCounter].Visibility = Visibility.Hidden;
}

如果我理解得没错的话,以上可以优化为
if (frameList[frameCounter].disposalMethod >= 2)
{
frameList[frameCounter].Visibility = Visibility.Hidden;
}

发表评论

昵称: [登录] [注册]

主页:

邮箱:(仅博主可见)

评论内容:

  登录  注册

[使用Ctrl+Enter键快速提交评论]

0 1011555 41pfjnOHKGA=