使用定时器显示GIF动画的ATL控件实现

(原帖: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),点击确定,如下图所示。

使用定时器显示GIF动画的ATL控件实现_第1张图片

第一个对话框,无条件点击下一步。

使用定时器显示GIF动画的ATL控件实现_第2张图片

在这个对话框中,勾上“允许合并代理/存根代码”,为了创建出来只有一个项目,否则有两个项目,对于我这种有强迫症的人来说不能忍受;如果你想使用MFC的东东(比如使用CDC、CBitmap类等)时,勾上“支持MFC”;把“支持COM+ 1.0”和“支持部件注册器”也勾上,点击完成,VS会自己帮你创建好项目,等它创建完成。

 使用定时器显示GIF动画的ATL控件实现_第3张图片

在EmotionOle项目上点击鼠标右键,选择“添加”,“类”,如下图所示。

使用定时器显示GIF动画的ATL控件实现_第4张图片

选择“ATL控件”,点击添加。

使用定时器显示GIF动画的ATL控件实现_第5张图片

在“简称”中填写控件名,比如“EmotionOle”,其他的输入框中内容会自动填上,除了ProgID,在ProgID中随便填点什么吧,比如“EmotionOle.EmotionOle”,如下图所示,点击下一步。

使用定时器显示GIF动画的ATL控件实现_第6张图片

这个对话框中勾上“连接点”,因为可能需要处理一些事件,点击下一步。

使用定时器显示GIF动画的ATL控件实现_第7张图片

这个对话框中一切保持默认即可,点击下一步。

使用定时器显示GIF动画的ATL控件实现_第8张图片

这个对话框也一样,保持默认,点击下一步。

使用定时器显示GIF动画的ATL控件实现_第9张图片

还是一样,保持默认,点击完成即可。

使用定时器显示GIF动画的ATL控件实现_第10张图片

这时ATL控件已经创建好了,现在这个项目是可以编译,也可以插入到RichEdit中,只不过只是一张没有实际意义的图片,接下来是重头戏上场的时间了,我们来创建显示GIF。

显示GIF首先得加载图片吧,先添加加载图片的方法,我们使用从文件读取的方式,也可使用HGLOBAL和IStream的方式。

转到类视图,在IEmotionOle上点击鼠标右键,选择“添加”,“添加方法”,如下图所示。

 使用定时器显示GIF动画的ATL控件实现_第11张图片

输入方法名,参数特性的“in”可勾可不勾,勾了只是有一个空的宏说明是入参而已,参数类型选择BSTR,参数名随便写吧,点击添加,如下图所示。

使用定时器显示GIF动画的ATL控件实现_第12张图片

如果还想添加参数再继续上面的步骤,我们这里只添加一个路径参数就可以了,点击完成。

使用定时器显示GIF动画的ATL控件实现_第13张图片

这时候CEmotionOle类中已经自动添加了Load函数,我们使用Gdiplus来读取和绘制GIF图片,所以需要在EmotionOle.h文件中包含GdiPlus.h文件,同时别忙了在项目启动时调用GdiplusStartup,在项目退出时调用GdiplusShutdown,如下图所示。

使用定时器显示GIF动画的ATL控件实现_第14张图片

 

接下来添加几个成员变量:

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动画的实现”,里面有详细的方法介绍和源代码下载链接。

你可能感兴趣的:(使用定时器显示GIF动画的ATL控件实现)