也谈向Richedit插入动态Gif的实现

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的时候没有擦除背景。如下图所示。

 e6b581e6b197

另外当我实现在汇编上的时候我发现刷新有闪动问题,可能是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

最终效果如下

e9a284e8a788

测试工程打包下载 http://www.deadc0de.com/wp-content/uploads/2009/05/richole2.rar

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
提供的源码资源涵盖了安卓应用、小程序、Python应用和Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值