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