在我們尋求幫助的時候,最不願意聽到的答復是:很抱歉,在當前版本的產品中還沒有實現該功能... 在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(0, 0, 0, 0, frameList[0].delayTime * 10); frameTimer.Start(); } }
OK,我們可以像使用Image控件一樣來使用我們的GifImage控件了:
下載源代碼