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