也談向Richedit插入動態Gif的實現

來源:互聯網
上載者:User

http://www.deadc0de.com/archives/insert-gif-to-richedit.html#more-14

 

最近在做IM軟體,需要實現向Richedit插入表情,表情是動態GIF映像。

由於以前沒有做過關於richedit的開發,百度了下,需要使用OLE技術。也就是說,插入的圖片都是一個OLE對象。而RICHEDIT則是一個OLE容器,相關連結如下。

How to insert a bitmap into an RTF document using the RichEdit control in Visual C++ 6.0
http://support.microsoft.com/default.aspx?scid=kb;en-us;220844

Animated Emoticons like those in MSN Messenger(英文版)
http://www.codeproject.com/KB/edit/AnimatedEmoticon.aspx

Animated Emoticons like those in MSN Messenger(中文版)
http://blog.csdn.net/dTianx/archive/2004/11/17/184949.aspx

DynamicGif作者blog
http://blog.csdn.net/kql01

 

第一個連結是微軟提供的部分代碼,只能用於插入BMP圖片。使用了OleCreateFromFile建立的ole對象。

第二個、第三個連結都是dTianx寫的,使用QQ的ImageOle.dll建立了ole對象。提供全部原始碼。

第四個連結是一牛自己做的一個控制項,匯出函數InsertGifToRichedit2A可以方便地插入任意GIF。只提供DLL檔案,而且只有1.21版本的,據說作者開發了1.4版本但他沒有開放下載。
我對這4個方法逐一實驗,結果發現這些方案都不可靠。下面我將詳細的介紹,我假設各位讀者從前沒接觸過RICHEDIT嵌入OLE這方面內容,2個星期前我也一無所知……

首先第一個不用說,只能插入BMP圖片,不符合我們的要求,但這段代碼已經可以讓我們瞭解了插入一個OLE對象的全部步驟。

第三個DynamicGif使用匯出函數InsertGifToRichedit2A,方便地插入了任意Gif圖片,但是我發現他放出的這個1.21版本有問題,這個問題發生在拖動捲軸的時候,richedit會很不正常……

放棄使用了DynamicGif後我把所有的賭注加在了ImageOle上,可是可是待我把一切實現在彙編上的時候。我發現噩夢到來了……(-__-!誇張一小下)。

首先。上面的例子建立GifAnimator執行個體,擷取IGifAnimator介面,IGifAnimator::LoadFromFile載入圖片,然後調用神奇的IGifAnimator::TriggerFrameChange函數,調用IRichEditOle::InsertObject插入ole對象。順理成章,但是這個例子有一個很明顯的bug就是在顯示透明gif的時候沒有擦除背景。如所示。

 

另外當我實現在彙編上的時候我發現重新整理有閃動問題,可能是InvalidateRect的第三個參數bErase被設定了TRUE,可是我調試發現ImageOle在調用這個api的時候bErase為False。鬱悶地研究了好幾天,終於發現這一切的一切居然是一個擴充風格導致的,WS_EX_TRANSPARENT,透明風格,而dTianx和qq的richedit的確有這個擴充風格。-_-!!! 當我給richedit加入這個風格後,重新整理的確不閃了,但richedit背景與表單相同,這就需要子類化richedit響應WM_ERASEBKGND訊息,手工畫背景為白色。

解決了這個後又開始著手解決顯示透明gif的問題。這個問題應該源於GDI+的GdipImageDrawRectI函數,它只繪製了不透明的部分到DC上,我不知道qq是怎麼解決的,我覺得應該有一個函數可以關掉GID+的這個特性:只繪製不透明部分到DC。但對GDI+非常不瞭解,找了好久沒有找到。我想出來的解決方案是hook這段代碼,加入一個建立白色刷子刷白色背景,然後再調用GdipImageDrawRectI繪製gif幀。這樣每次繪製GIF幀背景都會被刷成白色。而且的確實現了~

在實驗過程中,我還發現如果使用類名稱為RICHEDIT20A版本的richedit會導致記憶體泄露問題,就是刪除了已經插入的圖片對象記憶體依然不釋放,使用類名稱為RICHEDIT版本的richedit沒有此問題。而qq使用的是RICHEDIT20A,我複製了qq目錄的riched20.dll仍然存在這個問題,我不知道qq有沒有這個問題,以及他是怎麼怎麼解決的。

然後當我把代碼轉移到我的IM工程上我發現了更嚴重的問題。嵌入過多的ole會導致程式崩潰,崩潰點是riched20.dll,查看呼叫堆疊發現是其內部造成的,我無法查明原因。這個錯誤so奇怪,在實驗工程上無論插入多少個圖片都不會崩潰,一轉移到IM工程上就有問題,我屏蔽了很多很多代碼,最終發覺可能跟線程有關。在WM_INITDIALOG中插入多少個都沒問題,在另外的線程插入就有問題,我想,把插入ole的代碼放在視窗過程裡。用SendMessage觸發來添加,結果還是不行,很奇怪很奇怪。。。

還有另外一個很令我崩潰的問題,第一次建立richedit,插入gif圖片可以,但當你銷毀這個帶有richedit的視窗,然後再建立,再次插入gif圖片,圖片不會動了(開啟聊天視窗,插入gif,正常;關閉聊天視窗,開啟聊天視窗,插入gif,gif不會動)。又另我百思不得其解。為此我調試了ImageOle,曉得了ImageOle的原理。

ImageOle讀取了一個圖片後,依賴介面IViewObject::OnDraw實現重新整理ole地區顯示gif的不同幀。而這個OnDraw是需要InvalidateRect觸發的。這就需要一個定時器來定時調用InvalidateRect實現重新整理,這個定時器是由ImageOle內部建立的,是在CreateInstance的時候。而且他有個判斷,如果已經建立過(控制代碼不為0),就不需要再建立了。當我銷毀我的聊天視窗的時候,這個定時器居然神奇般地不見了……所以再次插入gif就不能自主動態重新整理gif了。dTianx的mfc工程,他並未銷毀視窗,用spy++可以看到當關閉視窗的時候,視窗只是變為了invisable……所以沒有這個bug。

這種種的問題逼迫我去尋找其他的替代DLL。

我發現浩方平台也有個ImageOle,介面名換了,但也有LoadFromFile,TriggerFrameChange,和QQ的ImageOle驚人的相似,好神奇~兩者難道有某種關聯?ImageOle的開發人員究竟是誰呢?不管這些,我實驗調用浩方的ImageOle.dll也不成。然後又尋找了飛信、百度Hi……

找到了很多。都用不明白。

這迫使我選擇另一條崎嶇的路。

另一條最有可能成功的路。。

自己造個ActiveX實現OLE對象嵌入。。

需要編程實現n個COM介面
IDispatch
IOleObject
IOleInPlaceObject
IOleInPlaceActiveObject
IOleControl
IDataObject
IProvideClassInfo
IPersistStorage
IPersistStreamInit
IPersistPropertyBag
IViewObject2
ISpecifyPropertyPages
ICategorizeProperties
IConnectionPointContainer
IRunnableObject

介面太多了,工程量巨大,我肯定自己搞不定的,不過好在印象裡masm32包裡有個asmctrl的工程,是一個activex控制項,可以被vb調用,肯定已經實現了這些東西,找來masm32 v9果然有。嘗試把他插入richedit裡,發現插入1個以上ole的繪製就有問題。看readme可以知道這個工程的作者。

他就是japheth,一個德國的彙編超人……

作品有牛x閃閃的COMView和牛x閃閃閃的masm相容編譯器JWasm。otz

他對COM的研究可謂是非常的透徹了,不然怎麼可能敢用彙編寫ActiveX~~

超人的網站 http://www.japheth.de

網站的 COM & Assembly 欄目裡有一個ASMCtrl工程。版本是2.5.5,v9裡的可能是1.0版本,看記錄可知後續版本修正了bugs。而且2.0版本後使用了大量的宏,因為這些宏,我讀他的代碼變得非常非常難。。看了超人japheth的代碼我才知道彙編原來是這麼這麼玩的,太牛x的……太令人無語了。看不懂,給超人寫信要來了1.3的版本,1.3代碼比2.x好理解,只用了少量的宏,不過編譯後插入richedit依然存在bug。無奈只有使用2.5.5版本當模板做我的PicOlePlus了。

然後結合我對調試ImageOle得出的經驗和對GDIPlus的學習。很費力地修改了ASMCtrl工程,得到了偉大的PicOlePlus.dll這個東東

下面是我做的一些主要更改。

1.CAsmClass.inc裡面添加一些類私人變數,還需要在CAsmClass的MEMBER裡申明

  hGdiplus dd ?
  pImage dd ?
  dwWidth dd ?
  dwHeight dd ?
  dwFrameCount dd ?
  pPropertyItem dd ? ;id len type pvalue[]
  dwFrame0Tick dd ?

2.在CAsmClass::Create裡面初始化GDI+,CAsmClass::Destroy裡面釋放映像,釋放GDI+。代碼略

3.修改IAsmClass的介面,添加了一個LoadFromFile方法,調用LoadImageFile函數
LoadImageFile Proc lpszFile
  Local wszFile[260]:WORD
  
  invoke MultiByteToWideChar,CP_OEMCP,MB_PRECOMPOSED,lpszFile,-1,addr wszFile,255
  invoke GdipLoadImageFromFile,addr wszFile,addr m_pImage
  .if !eax
   invoke GdipGetImageWidth,m_pImage,addr m_dwWidth
   invoke GdipGetImageHeight,m_pImage,addr m_dwHeight
   invoke SetOleExtent   ;設定Ole尺寸
   invoke SendViewChange@CAsmClass,ebx ;!!! 通知OLE容器OLE大小改變
   invoke IsAnimatedGIF
   xor eax,eax
  .endif
  ret
LoadImageFile EndP

IsAnimatedGIF Proc
  Local dwDimensionCount
  Local pDimensionIDs
  Local nSize
  
  invoke GdipImageGetFrameDimensionsCount,m_pImage,addr dwDimensionCount
  mov eax,dwDimensionCount
  shl eax,4
  invoke LocalAlloc,LPTR,eax
  mov pDimensionIDs,eax
  invoke GdipImageGetFrameDimensionsList,m_pImage,pDimensionIDs,dwDimensionCount
  invoke GdipImageGetFrameCount,m_pImage,pDimensionIDs,addr m_dwFrameCount
  invoke GdipGetPropertyItemSize,m_pImage,PropertyTagFrameDelay,addr nSize
  .if !eax
   invoke LocalAlloc,LPTR,nSize
   mov m_pPropertyItem,eax
   invoke GdipGetPropertyItem,m_pImage,PropertyTagFrameDelay,nSize,m_pPropertyItem
  .endif
  invoke LocalFree,pDimensionIDs
  invoke timeGetTime
  mov m_dwFrame0Tick,eax
  ret
IsAnimatedGIF EndP

SetOleExtent Proc ;計算尺寸
  pushad
  invoke GetDC,0
  mov esi,eax
  invoke GetDeviceCaps, esi, LOGPIXELSX
  push eax
  mov eax,m_dwWidth
  mov m_pixelExtent.cx_, eax
  mov ecx,HIMETRIC_PER_INCH
  mul ecx
  pop ecx
  xor edx,edx
  div ecx
  mov m_himetricExtent.cx_, eax
  
  invoke GetDeviceCaps, esi, LOGPIXELSY
  push eax
  mov eax,m_dwHeight
  mov m_pixelExtent.cy, eax
  mov ecx,HIMETRIC_PER_INCH
  mul ecx
  pop ecx
  xor edx,edx
  div ecx
  mov m_himetricExtent.cy, eax
  invoke DeleteObject,esi
  popad
  ret
SetOleExtent EndP

4.修改IViewObject::OnDraw的代碼調用DrawImage

sIID_FrameDimensionTime TEXTEQU <{06aedbd6dH, 03fb5H, 0418aH, {083h,0a6h,07fh,045h,022h,09dh,0c8h,072h}}>
FrameDimensionTime GUID sIID_FrameDimensionTime

DrawImage Proc uses esi edi ebx hDC,X,Y
  Local dwTicks
  Local hGraphics
  Local rt:RECT
  Local hScrDC,hTempDC,hBitmap
  
  .if m_pPropertyItem  ;通過幀延遲資料和已經經過的時間計算當前應該顯示的幀
   invoke timeGetTime ;timeGetTime精度是1ms
   sub eax,m_dwFrame0Tick
   xor edx,edx
   mov ecx,10
   div ecx
   mov dwTicks,eax
   
   mov esi,m_pPropertyItem
   mov esi,dword ptr [esi+12]
   xor edi,edi
   .While TRUE
    mov eax,[esi+edi*4]
    .Break .if dwTicks < eax
    sub dwTicks,eax
    inc edi
    .if edi == m_dwFrameCount
     xor edi,edi
    .endif
   .EndW
   
   invoke GdipImageSelectActiveFrame,m_pImage,addr FrameDimensionTime,edi ;選擇幀
  .endif
  
  invoke GetDC,0  ;建立緩衝dc
  mov hScrDC,eax
  invoke CreateCompatibleDC,hScrDC
  mov hTempDC,eax
  invoke CreateCompatibleBitmap,hScrDC,m_dwWidth,m_dwHeight
  mov hBitmap,eax
  invoke SelectObject,hTempDC,hBitmap
  invoke DeleteObject,eax
  invoke DeleteDC,hScrDC
  
  invoke SetRect,addr rt,0,0,m_dwWidth,m_dwHeight ;填充背景為白色
  invoke GetStockObject,WHITE_BRUSH
  invoke FillRect,hTempDC,addr rt,eax
  
  invoke GdipCreateFromHDC,hTempDC,addr hGraphics
  invoke GdipDrawImageRectI,hGraphics,m_pImage,0,0,m_dwWidth,m_dwHeight ;畫gif幀
  invoke GdipDeleteGraphics,hGraphics
  
  invoke BitBlt,hDC,X,Y,m_dwWidth,m_dwHeight,hTempDC,0,0,SRCCOPY
  invoke DeleteObject,hBitmap
  invoke DeleteDC,hTempDC
  ret
DrawImage EndP
這樣,一個支援gif的OLE組件就搞定了,哈哈哈~不過這裡沒有重新整理,重新整理我給弄到外面去了。有外面代碼控制重新整理。

下面是插入GIF的代碼

InsertObject Proc uses esi edi ebx hWnd,ID,lpszFile
  Local lpOleInterface
  Local lpLockBytes
  Local lpStorage
  Local lpClientSite
  Local ppv
  Local lpOleObject
  Local lpPicOlePlus
  Local clsid:GUID
  Local reo:REOBJECT
  
  mov lpOleInterface,0
  mov lpLockBytes,0
  mov lpStorage,0
  mov lpClientSite,0
  mov ppv,0
  mov lpOleObject,0
  mov lpPicOlePlus,0
  
  invoke SendDlgItemMessage,hWnd,ID,EM_GETOLEINTERFACE,0,addr lpOleInterface
  cmp lpOleInterface,0
  jz exit
  
  invoke CreateILockBytesOnHGlobal,NULL,TRUE,addr lpLockBytes
  cmp lpLockBytes,0
  jz exit
  
  invoke StgCreateDocfileOnILockBytes,lpLockBytes,STGM_SHARE_EXCLUSIVE or STGM_CREATE or STGM_READWRITE,0,addr lpStorage
  cmp lpStorage,0
  jz exit
  
  mov esi,lpOleInterface
  mov esi,dword ptr [esi]
  assume esi:ptr IRichEditOle
  invoke [esi].GetClientSite,lpOleInterface,addr lpClientSite
  cmp lpClientSite,0
  jz exit
  
  invoke CoInitializeEx,NULL,COINIT_APARTMENTTHREADED
  invoke CoCreateInstance,addr IID_PicOlePlus,0,CLSCTX_INPROC_SERVER or CLSCTX_INPROC_HANDLER or CLSCTX_LOCAL_SERVER,addr IID_IUnknown,addr ppv
  invoke OleRun,ppv
  
  mov ecx,ppv
  mov ecx,[ecx]
  invoke [ecx][IUnknown.QueryInterface],ppv,addr IID_IPicOlePlus,addr lpPicOlePlus
  cmp lpPicOlePlus,0
  jz exit
  
  mov eax,ppv
  mov eax,[eax]
  invoke [eax][IUnknown.Release],ppv
  
  mov edi,lpPicOlePlus
  mov edi,dword ptr [edi]
  assume edi:ptr IPicOlePlus
  
  invoke [edi].LoadFromFile,lpPicOlePlus,lpszFile
  cmp eax,0
  jnz exit
  
  invoke [edi].QueryInterface,lpPicOlePlus,addr IID_IOleObject,addr lpOleObject
  cmp lpOleObject,0
  jz exit
  
  invoke OleSetContainedObject,lpOleObject,TRUE
  .if eax
   ret
  .endif
  
  mov edi,lpOleObject
  mov edi,dword ptr [edi]
  assume edi:ptr IOleObject
  invoke [edi].GetUserClassID,lpOleObject,addr clsid
  
  mov reo.cbStruct,sizeof reo
  mov reo.cp,-1
  invoke RtlMoveMemory,addr reo.clsid,addr clsid,sizeof clsid
  m2m reo.poleobj,lpOleObject
  m2m reo.pstg,lpStorage
  m2m reo.polesite,lpClientSite
  mov reo.sizel.x,0
  mov reo.sizel.y,0
  mov reo.dvaspect,1 ;DVASPECT_CONTENT
  mov reo.dwFlags,2 ;REO_BELOWBASELINE
  mov reo.dwUser,0
  invoke [esi].InsertObject,lpOleInterface,addr reo
  
exit:  .if lpClientSite
   mov eax,lpClientSite
   mov eax,[eax]
   invoke [eax][IUnknown.Release],lpClientSite
  .endif
  
  .if lpOleObject
   mov eax,lpOleObject
   mov eax,[eax]
   invoke [eax][IUnknown.Release],lpOleObject
  .endif
  
  .if lpStorage
   mov eax,lpStorage
   mov eax,[eax]
   invoke [eax][IUnknown.Release],lpStorage
  .endif
  
  .if lpPicOlePlus
   mov eax,lpPicOlePlus
   mov eax,[eax]
   invoke [eax][IUnknown.Release],lpPicOlePlus
  .endif
  
  .if lpLockBytes
   mov eax,lpLockBytes
   mov eax,[eax]
   invoke [eax][IUnknown.Release],lpLockBytes
  .endif
  ret
InsertObject EndP
在視窗過程中註冊一個定時器調用RedrawObject來重新整理。這個代碼和ImageOle的有些不同。

HiMetricX2Pixel Proc dwHiMetricX
  Local hDC
  
  invoke GetDC,0
  mov hDC,eax
  invoke GetDeviceCaps,hDC,LOGPIXELSX
  push eax
  invoke DeleteObject,hDC
  pop eax
  mov ecx,dwHiMetricX
  mul ecx
  xor edx,edx
  mov ecx,2540
  div ecx
  .if edx
   inc eax
  .endif
  ret
HiMetricX2Pixel EndP

RedrawObject Proc uses esi edi ebx hWnd,ID
  Local hRichEdit
  Local lpOleInterface
  Local dwObjectCount
  Local reo:REOBJECT
  Local szTemp[256]:BYTE
  Local pt:POINT
  Local rcClient:RECT
  Local rc:RECT
  Local IsInClient
  
  invoke GetDlgItem,hWnd,ID
  mov hRichEdit,eax
  
  invoke SendMessage,hRichEdit,EM_GETOLEINTERFACE,0,addr lpOleInterface
  cmp lpOleInterface,0
  jz exit
  
  mov esi,lpOleInterface
  mov esi,[esi]
  assume esi:ptr IRichEditOle
  
  invoke [esi].GetObjectCount,lpOleInterface ;取OLE對象數量
  mov dwObjectCount,eax
  
  invoke GetClientRect,hRichEdit,addr rcClient
  
  xor ebx,ebx     ;迴圈擷取每個OLE然後根據需要重新整理之
  .While ebx < dwObjectCount
   mov IsInClient,0
   
   invoke RtlZeroMemory,addr reo,sizeof reo
   mov reo.cbStruct,sizeof reo
   invoke [esi]._GetObject,lpOleInterface,ebx,addr reo,1 ;1 = REO_GETOBJ_POLEOBJ
   
   invoke SendMessage,hRichEdit,EM_POSFROMCHAR,addr pt,reo.cp
   m2m rc.left,pt.x
   m2m rc.top,pt.y
   m2m rc.right,pt.x
   invoke HiMetricX2Pixel,reo.sizel.x
   add rc.right,eax
   m2m rc.bottom,rcClient.bottom
   
   mov eax,reo.poleobj
   mov eax,[eax]
   invoke [eax.IOleObject.Release],reo.poleobj
   
   invoke PtInRect,addr rcClient,rc.left,rc.top
   .if eax
    inc IsInClient
    jmp @f
   .endif
   
   invoke PtInRect,addr rcClient,rc.right,rc.top
   .if eax
    inc IsInClient
    jmp @f
   .endif
   
   invoke PtInRect,addr rcClient,rc.left,rc.bottom
   .if eax
    inc IsInClient
    jmp @f
   .endif
   
   invoke PtInRect,addr rcClient,rc.right,rc.bottom
   .if eax
    inc IsInClient
    jmp @f
   .endif
   
@@:   
   .if IsInClient
    invoke InvalidateRect,hRichEdit,addr rc,0
   .endif
   inc ebx
  .EndW
exit:  
  ret
RedrawObject EndP

最終效果如下

測試工程打包下載 http://www.deadc0de.com/wp-content/uploads/2009/05/richole2.rar

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.