(原帖:http://blog.csdn.net/haoekin/article/details/8877979)
最近在做一个IM软件,IM软件非常重要的功能就有动态表情的插入和显示。实现GIF图片显示的方法主要有两种,一种是通过HTML控件,一种是通过RichEdit控件。HTML控件显示GIF动画的效率是很高的,但是可操控性不好(比如不允许表情修改尺寸,插入后自动滚动到聊天记录窗口的底部等);RichEdit的可操控性就好得多了,但是插入和显示GIF动画就比较麻烦。
网上流传的向CRichEditCtrl中插入GIF动画的方法不外乎有两类:一类是使用第三方的控件(如QQ的ImageOle或Gif89a),另一类是自己实现一个ATL控件,在控件中实现GIF的重绘。
我也用过QQ的ImageOle,能成功插入GIF动画,还是挺高兴的,但不久就发现了ImageOle的致命问题,插入的GIF动画会闪烁,这样的东西给用户用还不被骂啊,不行,一定得解决这个问题,不断地使用谷歌和度娘,提到的唯一解决办法就是设置CRichEditCtrl的风格为WS_EX_TRANSPARENT(透明),然后在WM_ERASEBKGND中绘制白色背景,这样试了一下,的确不闪了,其他绘制很正常,但是GIF透明背景中还会保留以前绘制的帧,就是所谓的重影,这个用户也不能接受。
没办法,只能将方向转向自己实现一个类似于ImageOle的ATL控件,对于没有什么COM开发经验的我来说,连怎么创建ATL控件的工程都不会,但是不会也得会,谁让我已经走上了这条不归路呢,经过不断折腾,终于知道怎么创建工程了。
然而待解决的问题还有一箩筐呢,第一个要解决的就是如何绘制GIF!绘制方法也基本上只有两种,开线程和定时器。
开线程的方法我感觉效率有点低,而且开销也有点大(也许你们有不同的想法);于是决定有定时器,但是ATL无窗口控件怎么使用定时器呢,使用定时器需要有一个窗口句柄,这个好解决,创建一个隐藏的小窗口就有了嘛;那么怎么定时,所有的GIF使用同一个定时器还是不同的GIF使用不同的定时器呢,从用户体验上来说,肯定是不同GIF使用不同的定时器较好,那么定时的时间怎么办,因为GIF各帧之间的持续时间可能是不同的,这个也好办,先将定时器时间设置为第一帧,定时时间到时再重新设置成第二帧……这样就行了,而且使用定时器的效率比开线程要高,开销也要小得多。
总结一下,我们的方法就是自己实现一个显示GIF动画的ATL控件,GIF绘制方法使用定时器的方式,对每一帧设置不同的定时时间,定时时间到时更换帧。
接下来好戏上场了,先一步步来创建一个空的ATL控件工程吧,我使用的是VS2010。首先点击“文件”,“新建”,“项目”,选择ATL项目,取个名字(比如叫EmotionOle),点击确定,如下图所示。
第一个对话框,无条件点击下一步。
在这个对话框中,勾上“允许合并代理/存根代码”,为了创建出来只有一个项目,否则有两个项目,对于我这种有强迫症的人来说不能忍受;如果你想使用MFC的东东(比如使用CDC、CBitmap类等)时,勾上“支持MFC”;把“支持COM+ 1.0”和“支持部件注册器”也勾上,点击完成,VS会自己帮你创建好项目,等它创建完成。
在EmotionOle项目上点击鼠标右键,选择“添加”,“类”,如下图所示。
选择“ATL控件”,点击添加。
在“简称”中填写控件名,比如“EmotionOle”,其他的输入框中内容会自动填上,除了ProgID,在ProgID中随便填点什么吧,比如“EmotionOle.EmotionOle”,如下图所示,点击下一步。
这个对话框中勾上“连接点”,因为可能需要处理一些事件,点击下一步。
这个对话框中一切保持默认即可,点击下一步。
这个对话框也一样,保持默认,点击下一步。
还是一样,保持默认,点击完成即可。
这时ATL控件已经创建好了,现在这个项目是可以编译,也可以插入到RichEdit中,只不过只是一张没有实际意义的图片,接下来是重头戏上场的时间了,我们来创建显示GIF。
显示GIF首先得加载图片吧,先添加加载图片的方法,我们使用从文件读取的方式,也可使用HGLOBAL和IStream的方式。
转到类视图,在IEmotionOle上点击鼠标右键,选择“添加”,“添加方法”,如下图所示。
输入方法名,参数特性的“in”可勾可不勾,勾了只是有一个空的宏说明是入参而已,参数类型选择BSTR,参数名随便写吧,点击添加,如下图所示。
如果还想添加参数再继续上面的步骤,我们这里只添加一个路径参数就可以了,点击完成。
这时候CEmotionOle类中已经自动添加了Load函数,我们使用Gdiplus来读取和绘制GIF图片,所以需要在EmotionOle.h文件中包含GdiPlus.h文件,同时别忙了在项目启动时调用GdiplusStartup,在项目退出时调用GdiplusShutdown,如下图所示。
接下来添加几个成员变量:
Image *m_pImage; // GDI+的Image对象指针,保存读取的GIF图片
LONG *m_pFrameTimes; // 各帧的持续时间数组,单位为毫秒
UINT m_nCurFrame; // 当前帧序号
UINT m_nFrameCount; // 总帧数
下面这个变量的作用相信大家也看出来了,就是为了使用定时器而创建的隐藏窗口,CHiddenWnd的声明和定义在后面介绍。
CHiddenWnd m_wndHidden;
friend class CHiddenWnd;
成员变量定义完成后就该实现Load函数了(别忙了在构造函数中初始化这些成员变量,在析构函数中释放内存),Load函数的实现如下:
STDMETHODIMPCEmotionOle::Load(BSTRlpszPathName)
{
AFX_MANAGE_STATE(AfxGetStaticModuleState());
Image*pImage=Image::FromFile(lpszPathName);// 载入图片
if (pImage==NULL){
return E_FAIL;
} elseif(pImage->GetLastStatus()!=Ok){
delete pImage;
return E_FAIL;
}
//获取总帧数
GUIDpageGuid=FrameDimensionTime;
m_nFrameCount=pImage->GetFrameCount(&pageGuid);
int nWidth=pImage->GetWidth();
int nHeight=pImage->GetHeight();
// m_sizeExtent的单位是HiMetric(0.01毫米),将像素转换成HiMetric
SIZELsizel;
sizel.cx=nWidth;
sizel.cy=nHeight;
AtlPixelToHiMetric(&sizel,&m_sizeExtent);
m_sizeNatural=m_sizeExtent;
//帧数大于1时获取各帧的持续时间
if(m_nFrameCount > 1) {
intnSize=pImage->GetPropertyItemSize(PropertyTagFrameDelay);
PropertyItem*pItem=(PropertyItem*)newCHAR[nSize];
pImage->GetPropertyItem(PropertyTagFrameDelay,nSize,pItem);
m_pFrameTimes= new LONG[m_nFrameCount];
CopyMemory(m_pFrameTimes,pItem->value,m_nFrameCount*sizeof(LONG));
delete[]((CHAR*)pItem);
// 获取的时间单位是10毫米,乘以10转换成毫秒
for(UINTi=0;i<m_nFrameCount;i++){
m_pFrameTimes[i]*=10;
}
//帧数大于1需要定时绘制
m_wndHidden.AttachCtrl(this);
m_wndHidden.SetTimer(m_pFrameTimes[0]);
}
m_pImage=pImage;
return S_OK;
}
然后修改绘制函数,如下所示:
HRESULTCEmotionOle::OnDraw(ATL_DRAWINFO&di)
{
RECT&rc=*(RECT*)di.prcBounds;
if (m_nFrameCount>0){
Graphicsgrah(di.hdcDraw);
Rectrect(rc.left,rc.top,rc.right-rc.left,rc.bottom-rc.top);
GUIDpageGuid=FrameDimensionTime;
m_pImage->SelectActiveFrame(&pageGuid,m_nCurFrame);
grah.DrawImage(m_pImage,rect);
}
return S_OK;
}
再为CEmotionOle(不是IEmotionOle)添加一个函数(可以是private的,因为已经声明了CHiddenWnd为friend,可以访问),函数实现如下:
void CEmotionOle::ChangeFrame(void)
{
//增加当前帧,增加到总帧数时又从0开始
m_nCurFrame++;
if (m_nCurFrame==m_nFrameCount){
m_nCurFrame=0;
}
//调用FireViewChange会使控件重绘,而且比在这里获取DC,然后重绘安全得多
FireViewChange();
//修改定时时间为当前帧(帧已经增加了)的持续时间
m_wndHidden.SetTimer(m_pFrameTimes[m_nCurFrame]);
}
下面是CHiddenWnd的声明和实现:
HiddenWnd.h
#pragma once
#include <atlwin.h>
using namespace ATL;
class CEmotionOle;
class CHiddenWnd:publicCWindowImpl<CHiddenWnd,CWindow,CNullTraits>
{
BEGIN_MSG_MAP(CHiddenWnd)
MESSAGE_HANDLER(WM_TIMER,OnTimer)
END_MSG_MAP()
public:
CHiddenWnd(void);
virtual ~CHiddenWnd(void);
LRESULTOnTimer(UINTuMsg,WPARAMwParam,LPARAMlParam,BOOL&bHandled);
void AttachCtrl(CEmotionOle*pCtrl);
void SetTimer(UINTnElapse);
private:
CEmotionOle*m_pCtrl;
UINTm_nTimerId;
};
HiddenWnd.cpp
#include "StdAfx.h"
#include "HiddenWnd.h"
#include "EmotionOle.h"
CHiddenWnd::CHiddenWnd(void):m_pCtrl(NULL),m_nTimerId(0)
{
}
CHiddenWnd::~CHiddenWnd(void)
{
if (m_nTimerId!=0){
::KillTimer(m_hWnd,m_nTimerId);
}
if (m_hWnd){
::DestroyWindow(m_hWnd);
m_hWnd=NULL;
}
}
// OnTimer处理很简单,只调用CEmotionOle的ChangeFrame函数即可
LRESULTCHiddenWnd::OnTimer(UINTuMsg,WPARAMwParam,LPARAMlParam,BOOL&bHandled)
{
bHandled=TRUE;
m_pCtrl->ChangeFrame();
return S_OK;
}
// 随着到CEmotionOle控件上
void CHiddenWnd::AttachCtrl(CEmotionOle*pCtrl)
{
if (!m_hWnd){
Create(NULL);
}
m_pCtrl=pCtrl;
}
// 设置定时时间
void CHiddenWnd::SetTimer(UINTnElapse)
{
//由于每个GIF只需要一个定时器,而且有不同的窗口句柄,所以ID可设置为任意非0值
//SetTimer如果发现ID相同,会替换已存在的定时器
::SetTimer(m_hWnd,1,nElapse,NULL);
}
ATL控件的开发大功告成!很简单是吧,迫不及待地编译工程吧!接下来介绍如何使用这个控件,相信大家都知道怎么使用了,这里还是简单介绍一下吧。首先创建一个基于对话框的MFC项目,添加一个RichEdit控件,并映射为对话框的成员变量假设为m_edit。
然后在对话框的CPP文件include最后添加如下一句:
#import “../Debug/EmotionOle.dll”no_namespace
然后为对话框添加一个函数,如下所示:
LPOLEOBJECTCTestDlg::InsertDynamicImage(CStringstrPath)
{
IStorage*lpStorage=NULL;
IOleObject*lpOleObject=NULL;
LPLOCKBYTESlpLockBytes=NULL;
IOleClientSite*lpOleClientSite=NULL;
IEmotionOle*lpEmotionOle=NULL;
CLSIDclsid;
REOBJECTreobject;
HRESULThr;
IRichEditOle*lpRichEditOle=m_edit.GetIRichEditOle();
BOOLbRet=TRUE;
try {
hr=::CoCreateInstance(__uuidof(EmotionOle),NULL,CLSCTX_INPROC,__uuidof(IEmotionOle),(LPVOID*)&lpEmotionOle);
if (hr!=S_OK){
AfxThrowOleException(hr);
}
hr=lpEmotionOle->Load(_bstr_t(strPath));
if (hr!=S_OK){
AfxThrowOleException(hr);
}
hr=lpEmotionOle->QueryInterface(&lpOleObject);
if (hr!=S_OK){
AfxThrowOleException(hr);
}
hr=lpOleObject->GetUserClassID(&clsid);
if (hr!=S_OK){
AfxThrowOleException(hr);
}
hr=CreateILockBytesOnHGlobal(NULL,TRUE,&lpLockBytes);
if (hr!=S_OK){
AfxThrowOleException(hr);
}
ASSERT(lpLockBytes!=NULL);
hr=StgCreateDocfileOnILockBytes(lpLockBytes,
STGM_SHARE_EXCLUSIVE|STGM_CREATE|STGM_READWRITE,0,&lpStorage);
if (hr!=S_OK){
AfxThrowOleException(hr);
}
lpRichEditOle->GetClientSite(&lpOleClientSite);
hr=lpOleObject->SetClientSite(lpOleClientSite);
if (hr!=S_OK){
AfxThrowOleException(hr);
}
ZeroMemory(&reobject, sizeof(REOBJECT));
reobject.cbStruct= sizeof(REOBJECT);
reobject.clsid =clsid;
reobject.cp =REO_CP_SELECTION;
reobject.dvaspect=DVASPECT_CONTENT;
reobject.dwFlags =REO_BELOWBASELINE;
reobject.poleobj =lpOleObject;
reobject.polesite=lpOleClientSite;
reobject.pstg =lpStorage;
SIZELsizel={0};
reobject.sizel=sizel;
hr=lpRichEditOle->InsertObject(&reobject);
if (hr!=S_OK){
AfxThrowOleException(hr);
}
OleSetContainedObject(lpOleObject,TRUE);
bRet=TRUE;
} catch(COleException*e){
TRACE(_T("CImageRichEditCtrl::InsertGifAnimator()OleException code:%d"),e->m_sc);
e->Delete();
bRet=FALSE;
}
lpRichEditOle->Release();
if (lpLockBytes){
lpLockBytes->Release();
}
if (lpEmotionOle!=NULL){
lpEmotionOle->Release();
}
if (lpOleObject!=NULL){
lpOleObject->Release();
}
if (lpOleClientSite!=NULL){
lpOleClientSite->Release();
}
if (lpStorage!=NULL){
lpStorage->Release();
}
return bRet?lpOleObject:NULL;
}
是不是很爽!!!添加的GIF是不是动起来了,而且也不会闪烁,并且插入很多GIF时CPU使用也不高,慢慢享受吧!
PS:
1. 文章中有一些个人观点如果不正确,还请大家指正;
2. 最好屏蔽掉插入的GIF图像上的鼠标双击事件,因为我测试到当RichEdit为ReadOnly时,双击图像后GIF显示有点不正常;
3. 大家有更好的想法请踊跃发表;
4. 最后给大家一个Demo吧,俗话说得好:没“代码”说个J8!不过由于我的CSDN积分全支持其他同学了,木有分了,小气一回,有钱的捧个钱场,没钱的捧个人场,嘿嘿……。
由于这篇文章的方法有些问题,所以就不提供下载链接了。
PS:这种方法已经发现有些问题了,具体的问题描述和最新的方法请看我的另一篇文章“再谈向RichEdit中插入GIF动画的实现”,里面有详细的方法介绍和源代码下载链接。