再谈向RichEdit中插入GIF动画的实现

我的前一篇文章“使用定时器显示GIF动画的ATL控件实现”中讲述了如何创建ATL项目,并实现显示GIF动画的控件,虽然能够显示,但有一些问题:

1. tphlj同学说一行插入多个GIF的时候,CPU会很高。

    这个问题我倒没有注意,因为我发现了更严重的问题,所以用那种方法实现的控件没有使用了,已经被我删掉,不能测试。

2. 删除插入的GIF后发现光标还在不停的闪烁

    显然这是由于删除对象后对象没有被释放,定时器还处于启动状态,导致不断地刷新;原因找了很久,发现是由于RichEdit支持撤消造成的,对象虽然删除了,但为了支持撤消,IRichEditOle中还保留有对象的引用,所以没有被释放,定时器也一直处于启动状态。当然,这可以通过删除对象时停止计时器完成,但是这样会发现撤消后动画不会动了;当然这也好解决,因为只要对象变得可见时,OnDraw函数会被触发,所以可以用一个BOOL值来存储定时器是否处于启动状态,停止时将这个值置为FALSE,在OnDraw中检查这个值,如果发现是FALSE则再次启动定时器就行了。

3. 插入大的GIF时滚动条不能滚动

    这个问题是不能容忍的,当RichEdit高度很小,而插入的GIF高度超过RichEdit的高度时,垂直滚动条出现,这时发现向下滚动是没有任何作用的,滚动条只会不停地滚动又还原,根本不能滚动。原因是由于FireViewChange造成的,每次FireViewChange触发的OnDraw得到的RECT都是对象被插入时的RECT,所以导致不能滚动。这个问题也是能解决的,就是不使用FireViewChange,而使用InvalidateRect,为了避免动画闪烁,首先得将RichEdit设成背景透明,再响应WM_ERASEBKGND消息绘制白色背景,要使用这个函数,就必须得到容器窗口的句柄和需要绘制的RECT,句柄好得到,可以在创建对象时传入并保存,也可使用IOleInPlaceSite::GetWindow得到;RECT同样有两种方法得到,第一种前面说过,只要对象进入可视区,OnDraw就会被触发,可以将里面的RECT保存下来,可能你会觉得这个RECT不一定正确,那么还有一种,同样使用IOleInPlaceSite接口的GetWindowContext,第三个参数就是需要绘制的RECT。

    这样问题就解决了吗?猛然一看,还真解决了,但细心一点就会发现,还有更严重的问题!当插入一个动画后,换行,输入文字,最好每一行输入的文字宽度超过动画的宽度,不停地输入,直到动画变得不可见,这时你会发现刚才插入动画的位置的文本变粗了!原因是:RichEdit背景透明了,而InvalidateRect指定不重绘背景,动画插入位置上是文字,没有擦除背景的情况下文字被不断地重绘,导致变粗的现象;造成这个问题的根本原因就是GetWindowContext获得的RECT不正确!虽然动画不可见了,但仍然能获取到一个合法的RECT,即上一次动画显示处的RECT,这不就是OnDraw中的RECT吗,没错,就是它!所以前面还怀疑OnDraw中保存的RECT不正确的同学现在不用怀疑了,它和GetWindowContext得到的RECT完全一样,所以不需要使用GetWindowContext了(这里说这么多保存这个RECT是有目的的,慢慢看后面就会知道);当然,也可以尝试InvalidateRect时重绘背景,但是会发现动画闪烁特别厉害。

    造成所有上面这些问题的根本原因是每一个对象都单独使用了一个定时器,无论对象是否可见都在更新动画!如果不出现上面的这些问题,可能还不会发现这样做的效率有多低!当插入成千上万的动画时,CPU使用不知道会多高。那么如何做才能最大限度地提高效率,并解决这些问题呢?那就是只刷新可视区域的动画!

    估计有的同学会质疑了,说得轻巧,有那么容易知道一个对象是否在可视区就好了!的确,这也是我这些天来一直苦苦思索的问题,黄天不负有心人,总算找到了一种方法:IRichEditOle里面有GetObject,根据对象索引可以获取一些对象的信息,包括CLSID、IOleObject指针和CP(Char Position,字符索引),怎么根据这些东西来判断对象是否可见呢,当然,仅有这些东西是不能判断的,但里面的CP很有用!RichEdit有一个成员函数叫做CharFromPos,可以根据客户区中的点来得到这个点对应的字符索引,可能有的同学看出点眉目了,没错,我们可以获取客户区的RECT,得到左上角和右下角的点,再获取这两个点对应的字符索引,不就得到了可视区的最小和最大字符索引了吗,再判断对象的CP是否在这个区间内就可知道对象是否可见了。

    不过GetClientRect得到的区域稍大,因为RichEdit有个成员函数叫做SetRect,可以设置文本区域的RECT,所以应该使用GetRect得到正确的矩形区域。可能有的同学会怀疑了,可视区最后一行没有满怎么办,右下角就没有东西,得到的字符索引正确吗?答案是肯定的,无论右下角是否有文本,得到的都是可见区域最大的字符索引,如果右下角没有字符,那获得的字符索引就是这一行最后一个字符的字符索引,问题解决了!

     等等,GetObject第一个参数是对象索引,我们怎么知道一个对象的索引呢,这还不好办啊,直接for不就行了,从第0个开始遍历,直到找到自己就行了!

     有人会骂了,说了这么多,虽然解决了如何判断对象是否可见的问题,但又引入新的问题,每次都遍历,那插入成千上万的动画,每个动画的定时器都去遍历,你可知道动画刷新频率是很高的,通常100毫秒,估计这效率比直接刷新更低吧!没办法,谁让那些问题那么烦人呢!

     别忙着开骂,还没说完呢,遍历效率低,我们就来提高效率,你可以测试一下,IRichEditOle中存储的对象是按照字符索引从小到大进行排序的,比如先插入个动画1,再在这个动画前插入另一个动画2,这时候你GetObject传入索引为0时,会发现得到的对象是动画2,所以可以证明对象的确是排好序的,别告诉我读到这里你还不知道怎么提高效率,二分查找啊,比如可视区的最小字符索引是cpMin,最大字符索引为cpMax,那就使用二分查找找到第一个字符索引大于或等于cpMin的对象索引,再从这个索引开始向后遍历,找到字符索引大于cpMax时停止遍历,效率不是就提高了吗。

    说了这么多,还是得每个对象自己在定时器处理函数中找到自己的字符索引,即使找的次数少得多了,但还是那句老话,成千上万的对象还是需要查找很多次,特别是当可视区较大时,找的次数会很大,这种方法不可取。

    再有点耐心,马上接近真相了。

    说了这么多,其实还是没能跳出一个对象使用一个定时器的思想,何不换个角度思考,为什么一定要每个对象一个定时器呢,为什么不在RichEdit中统一为自己的对象设置一个定时器呢?这就是真相,只需要一个定时器,即RichEdit扩展类的PreSubclassWindow中设置一个定时器,在定时器处理函数中,只需要使用二分查找找一次第一个可见对象的索引,然后从这个索引开始,遍历对象并更新动画,直到对象索引超过cpMax时停止遍历!

    这是我想到的最好的解决方案,效率已经特别高了,至少目前我还没有找到比这效率更高的方法,而且这也很好地解决了撤消时动画不动的问题,总之,前面所有的问题都解决了,而且可以证明,这种方法插入动画的个数对CPU使用率的影响不大,因为二分查找效率已经很高(1000个对象最多查找10次,1万个对象最多查找14次,10万个对象最多查找17次,1百万个对象最多查找20次,所以查找的时间基本可以忽略),CPU使用率是由可视区大小决定的,因为可视区大小直接决定了遍历的对象个数,而GetObject是比较费时的操作。

    可能你还会质疑,这样统一设置定时器,那么延迟时间应该怎么设置,每个动画每一帧的延时极有可能不一样啊!这的确是一个问题,但也不是问题,首先,我们选择一个最小的延时,这个延时选择多少呢,100毫秒比较不错,但的确有延时小于100毫秒的,比如QQ的鼓掌表情,它的延时平均只有20毫秒,不信你把QQ的鼓掌表情另存为GIF,再用其他软件(比如ACDSee)打开,会发现播放得特别快,但QQ中播放得并不快,显然QQ经过了扩大延时的处理,而且也是将最小的延时时间设为了100毫秒!

    那行嘛,就算你说的最小100毫秒是合理的,那有些帧不止100毫秒怎么办,这还不好办吗?先得到每一帧的延迟,使用四舍五入的方法得到延时为100毫秒的整数倍,再记录下一个动画总共有多少个100毫秒,比如说有N个,再分配一个长度为N的数组,里面存储每个100毫秒对应的帧索引不就行了,在更换帧时,检查当前这个100毫秒对应的帧索引,与前一次显示的帧索引进行对比,如果相同则不刷新,如果不同则刷新,这不就行了。

    但是还得解决一个问题,GetObject得到的是IOleObject的指针,怎么通过这个指针得到控件的指针?别忘了,这个IOleObject是控件实现的接口,它的地址和控件的地址之间的差值是固定的,为什么是固定的可以查看ATL::InternalQueryInterface实现的源代码。我们使用一个静态变量保存这个差值,然后使用IOleObject地址减去这个差值再强制转换一下就得到了控件的指针了。

    好了,说到这里,所有的问题都解决了,下面就来实现吧。我们现在不使用创建ATL项目,生成DLL的方式了,直接在MFC项目中添加ATL控件,添加ATL控件的步骤可以参考前一篇文章,这里控件类名我使用的是COleImage。

    创建完了在COleImage中添加成员变量(OleImage.h):

[cpp] view plain copy

  1. // 最小的帧延时  
  2. #define MIN_FRM_DELAY               100  
  3.   
  4. private:  
  5.     CDC *m_pDCs; // 每帧的DC  
  6.     CBitmap *m_pBitmaps; // 每一帧的位图  
  7.     CBitmap **m_ppOldBmps; // 保存每一帧DC旧的位图  
  8.     int *m_pFrmIndexes; // 每个100毫秒对应的帧索引  
  9.     int m_nFrmCount; // 总帧数  
  10.     int m_nTimeCount; // 100毫秒的总数,例如GIF所有帧加起来总的延迟是1秒,则这个值为10  
  11.     volatile int m_iPrevFrm; // 前一帧,定时器触发时用于判断是否需要更换帧  
  12.     volatile int m_iCurTime; // 当前时间(即第几个100毫秒)  
  13.     CRichEditCtrl *m_pContainer; // 容器窗口指针  
  14.     static DWORD m_dwOleObjectOffset; // IOleObject接口指针距离COleImage起始地址的偏移量  


    由于直接在MFC项目中添加ATL控件,没有必要在接口中添加方法了,直接添加成员函数:

[cpp] view plain copy

  1. public:  
  2.   
  3.     // 实例化后一定会被调用的函数  
  4.     HRESULT FinalConstruct();  
  5.   
  6.     // 删除实例前一定会被调用的函数  
  7.     void FinalRelease();  
  8.   
  9.     // 计算IOleObject接口指针在COleImage类中的偏移量  
  10.     static DWORD GetIOleObjectOffset(void);  
  11.   
  12.     // 根据IOleObjec接口指针得到COleImage的指针,其实就是减去前面的偏移量  
  13.     static COleImage *FromOleObject(IOleObject *lpOleObject);  
  14.   
  15.     // 从文件加载,您也可以再添加一个从IStream加载的函数,这里只是演示而已  
  16.     HRESULT LoadFromFile(BSTR lpszPathName, CRichEditCtrl *pContainer, UINT nMaxWidth = 0);  
  17.   
  18.     // 绘制函数  
  19.     HRESULT OnDraw(ATL_DRAWINFO& di);  
  20.   
  21.     // 更换帧  
  22.     void ChangeFrame();  
  23.   
  24.     // 这个函数一定要重载,可以不做任何事情,如果不写,双击插入的图像时会ASSERT失败  
  25.     STDMETHOD(DoVerb)(  
  26.         _In_ LONG /* iVerb */,   
  27.         _In_opt_ LPMSG /* pMsg */,   
  28.         _Inout_ IOleClientSite* /* pActiveSite */,   
  29.         _In_ LONG /* lindex */,  
  30.         _In_ HWND /* hwndParent */,   
  31.         _In_ LPCRECT /* lprcPosRect */)  
  32.     {  
  33.         return S_OK;  
  34.     }  


下面就来实现这些函数(OleImage.cpp):

[cpp] view plain copy

  1. //  
  2. // 计算IOleObject接口指针在COleImage类中的偏移量  
  3. //  
  4. DWORD COleImage::GetIOleObjectOffset(void)  
  5. {  
  6.     const _ATL_INTMAP_ENTRY *pEntries = _GetEntries();  
  7.     while (pEntries->pFunc != NULL) {  
  8.         if (InlineIsEqualGUID(*(pEntries->piid), IID_IOleObject)) {  
  9.             return pEntries->dw;  
  10.         }  
  11.         pEntries++;  
  12.     }  
  13.     return 0;  
  14. }  
  15.   
  16. // 初始化静态成员m_dwOleObjectOffset  
  17. DWORD COleImage::m_dwOleObjectOffset = COleImage::GetIOleObjectOffset();  
  18.   
  19. // 根据IOleObject接口指针得到COleImage类的指针  
  20. COleImage * COleImage::FromOleObject(IOleObject *lpOleObject)  
  21. {  
  22.     // 其实就是简单地用lpOleObject减去前面计算出来的偏移量  
  23.     return (COleImage *) ((UINT_PTR) lpOleObject - m_dwOleObjectOffset);  
  24. }  
  25.   
  26. //  
  27. // 初始化成员  
  28. //  
  29. HRESULT COleImage::FinalConstruct()  
  30. {  
  31.     m_pDCs = NULL;  
  32.     m_pBitmaps = NULL;  
  33.     m_ppOldBmps = NULL;  
  34.     m_pFrmIndexes = NULL;  
  35.     m_nFrmCount = 0;  
  36.     m_nTimeCount = 0;  
  37.     m_iPrevFrm = 0;  
  38.     m_iCurTime = 0;  
  39.     m_pContainer = 0;  
  40.     return S_OK;  
  41. }  
  42.   
  43. // 释放内存空间  
  44. void COleImage::FinalRelease()  
  45. {  
  46.     for (int i = 0; i < m_nFrmCount; i++) {  
  47.         m_pDCs[i].SelectObject(m_ppOldBmps[i]);  
  48.         m_pBitmaps[i].DeleteObject();  
  49.         m_pDCs[i].DeleteDC();  
  50.     }  
  51.     if (m_pDCs != NULL) {  
  52.         delete [] m_pDCs;  
  53.     }  
  54.     if (m_pBitmaps != NULL) {  
  55.         delete [] m_pBitmaps;  
  56.     }  
  57.     if (m_ppOldBmps != NULL) {  
  58.         delete [] m_ppOldBmps;  
  59.     }  
  60.     if (m_pFrmIndexes != NULL) {  
  61.         delete [] m_pFrmIndexes;  
  62.     }  
  63.     FinalConstruct();  
  64. }  
  65.   
  66. //  
  67. // 从文件加载图像  
  68. //   
  69. // lpszPathName     -   文件路径  
  70. // pContainer       -   容器窗口指针  
  71. // nMaxWidth        -   插入图像的最大宽度,超过最大宽度的图像会被缩放,0表示不缩放  
  72. //  
  73. HRESULT COleImage::LoadFromFile(BSTR lpszPathName, CRichEditCtrl *pContainer, UINT nMaxWidth)  
  74. {  
  75.     // 先清除以前加载过的图像  
  76.     FinalRelease();  
  77.   
  78.     // 保存容器窗口指针,便于更换帧时调用容器窗口的InvalidateRect函数  
  79.     m_pContainer = pContainer;  
  80.   
  81.     // 加载图像  
  82.     Image *pImage = Image::FromFile(lpszPathName);  
  83.     if (pImage == NULL) {  
  84.         return E_FAIL;  
  85.     } else if (pImage->GetLastStatus() != Ok) {  
  86.         delete pImage;  
  87.         return E_FAIL;  
  88.     }  
  89.   
  90.     // 获取总帧数,静态图像,如JPG、PNG等得到的是0,要改为1  
  91.     GUID pageGuid = FrameDimensionTime;  
  92.     m_nFrmCount = pImage->GetFrameCount(&pageGuid);  
  93.     if (m_nFrmCount == 0) {  
  94.         m_nFrmCount = 1;  
  95.     }  
  96.   
  97.     // 得到图像尺寸  
  98.     UINT w = pImage->GetWidth();  
  99.     UINT h = pImage->GetHeight();  
  100.   
  101.     // 缩放图像  
  102.     if (nMaxWidth > 0 && w > nMaxWidth) {  
  103.         h = h * nMaxWidth / w;  
  104.         w = nMaxWidth;  
  105.     }  
  106.   
  107.     // 转化成HIMETRIC,即0.01毫米单位的尺寸  
  108.     // 设置控件尺寸  
  109.     SIZEL sizel;  
  110.     sizel.cx = w;  
  111.     sizel.cy = h;  
  112.     AtlPixelToHiMetric(&sizel, &m_sizeExtent);  
  113.     m_sizeNatural = m_sizeExtent;  
  114.       
  115.     if (m_nFrmCount > 1) { // 总帧数超过1时  
  116.   
  117.         // 得到各帧的延迟时间  
  118.         int nSize = pImage->GetPropertyItemSize(PropertyTagFrameDelay);  
  119.         PropertyItem *pItem = (PropertyItem *) new BYTE[nSize];  
  120.         pImage->GetPropertyItem(PropertyTagFrameDelay, nSize, pItem);  
  121.         LONG *pDelays = (LONG *) pItem->value;  
  122.   
  123.         // 计算总的延迟时间有几个100毫秒  
  124.         for (int i = 0; i < m_nFrmCount; i++) {  
  125.             // 得到的延迟时间单位是10毫秒,乘以10转换成毫秒  
  126.             // 再使用四舍五入的方法得到有几个100毫秒。  
  127.             // 例如这个值是35,那么就是35个10毫秒,乘以10得350毫秒,再四舍五入,得到4  
  128.             pDelays[i] = ((pDelays[i] * 10) + (MIN_FRM_DELAY / 2)) / MIN_FRM_DELAY;  
  129.             m_nTimeCount += pDelays[i];  
  130.         }  
  131.   
  132.         // 再得到每个100毫秒对应的帧索引  
  133.         // 例如第1帧200毫秒,第2帧300毫秒,那么  
  134.         // m_pFrmIndexes[0] = 0  
  135.         // m_pFrmIndexes[1] = 0  
  136.         // m_pFrmIndexes[2] = 1  
  137.         // m_pFrmIndexes[3] = 1  
  138.         // m_pFrmIndexes[4] = 1  
  139.         // 这样就可通过这个数组得到当前这100毫秒应该绘制哪一帧  
  140.         m_pFrmIndexes = new int[m_nTimeCount];  
  141.         for (int i = 0, j = 0; i < m_nFrmCount; i++) {  
  142.             for (int k = 0; k < pDelays[i]; k++) {  
  143.                 m_pFrmIndexes[j++] = i;  
  144.             }  
  145.         }  
  146.         // new出来的内存别忘了delete  
  147.         delete [] (BYTE *) pItem;  
  148.     } else { // 帧数为1时  
  149.         // 只是便于绘制,也为m_pFrmIndexes分配一个单元的空间,里面存0就行了  
  150.         m_nTimeCount = 1;  
  151.         m_pFrmIndexes = new int[1];  
  152.         *m_pFrmIndexes = 0;  
  153.     }  
  154.   
  155.     // 创建每一帧的缓存位图和DC  
  156.     m_pDCs = new CDC[m_nFrmCount];  
  157.     m_pBitmaps = new CBitmap[m_nFrmCount];  
  158.     m_ppOldBmps = new CBitmap *[m_nFrmCount];  
  159.     CDC *pDC = CDC::FromHandle(::GetDC(NULL));  
  160.     for (int i = 0; i < m_nFrmCount; i++) {  
  161.         m_pDCs[i].CreateCompatibleDC(pDC);  
  162.         m_pBitmaps[i].CreateCompatibleBitmap(pDC, w, h);  
  163.         m_ppOldBmps[i] = m_pDCs[i].SelectObject(&m_pBitmaps[i]);  
  164.         // 由于InvalidateRect时指定了不擦除背景,如果使用透明背景进行绘制  
  165.         // 那么透明部分还会保留前面帧所绘制的内容,出现重影,所以填充白色背景  
  166.         // 来解决这个问题  
  167.         m_pDCs[i].FillSolidRect(0, 0, w, h, RGB(255, 255, 255));  
  168.   
  169.         // 绘制当前帧  
  170.         pImage->SelectActiveFrame(&pageGuid, i);  
  171.         Graphics g(m_pDCs[i]);  
  172.         g.SetSmoothingMode(SmoothingModeHighQuality);  
  173.         g.DrawImage(pImage, 0, 0, w, h);  
  174.     }  
  175.     ::ReleaseDC(NULL, pDC->m_hDC);  
  176.     delete pImage;  
  177.     return S_OK;  
  178. }  
  179.   
  180. //  
  181. // 绘制函数,每一次控件进入可视区,该函数会被调用,  
  182. // 每一次调用容器窗口的InvalidateRect时该函数  
  183. // 同样也会被调用  
  184. //  
  185. HRESULT COleImage::OnDraw(ATL_DRAWINFO& di)  
  186. {  
  187.     RECT& rc = *(RECT *) di.prcBounds;  
  188.     // 将剪辑区域设置为 di.prcBounds 指定的矩形  
  189.       
  190.     // 保存绘制矩形区域,前面保存了容器窗口指针,这里保存了要绘制的矩形区域  
  191.     // 在更换帧时就可直接调用m_pContainer->InvalidateRect(&m_rcPos, FALSE)了  
  192.     m_rcPos = rc;  
  193.     if (m_nFrmCount > 0) {  
  194.         BitBlt(di.hdcDraw, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top,  
  195.             m_pDCs[m_pFrmIndexes[m_iCurTime]], 0, 0, SRCCOPY);  
  196.     }  
  197.     return S_OK;  
  198. }  
  199.   
  200. //  
  201. // 更换帧,其实这个函数名命名不准确,因为不是每一次调用这个函数都会更换帧  
  202. //  
  203. void COleImage::ChangeFrame()  
  204. {  
  205.     // 必须要帧数大于1,且容器窗口指针存在时  
  206.     if (m_nFrmCount > 1 && m_pContainer != NULL) {  
  207.         // 增加当前时间  
  208.         ++m_iCurTime;  
  209.         // 超过总的延迟时间时,又从0开始  
  210.         if (m_iCurTime >= m_nTimeCount) {  
  211.             m_iCurTime = 0;  
  212.         }  
  213.         // 当前时间对应的帧索引与上一次绘制的帧索引不同时才需要重绘  
  214.         if (m_pFrmIndexes[m_iCurTime] != m_iPrevFrm) {  
  215.             m_pContainer->InvalidateRect(&m_rcPos, FALSE);  
  216.             // 更新上一次绘制的帧索引为当前的帧索引  
  217.             m_iPrevFrm = m_pFrmIndexes[m_iCurTime];  
  218.         }  
  219.     }  
  220. }  


接下来我们要扩展CRichEditCtrl类,在项目中添加MFC类,基类选择CRichEditCtrl,然后重写虚函数PreSubclassWindow、添加WM_ERASEBKGND、WM_DESTROY、WM_TIMER消息响应函数,添加一个CComPtr<IRichEditOle> m_pRichEditOle的成员变量,因为GetObject要使用它,而且频率很高,所以作为成员变量,并添加一个FindFirstObject方法,然后实现这些函数:

[cpp] view plain copy

  1. //  
  2. // 在这里进行一些必要的修改  
  3. //  
  4.   
  5. void CRichEditCtrlEx::PreSubclassWindow()  
  6. {  
  7.     // TODO: 在此添加专用代码和/或调用基类  
  8.   
  9.     // 下面这个可选,去掉RichEdit的双字体设置  
  10.     // 因为说实话,我不知道怎么分别设置中英文字体,如果哪位同学知道请告诉  
  11.     // 我一下  
  12.     DWORD fontStyle = SendMessage(EM_GETLANGOPTIONS);  
  13.     fontStyle &= ~IMF_DUALFONT;  
  14.     SendMessage(EM_SETLANGOPTIONS, 0, fontStyle);  
  15.   
  16.     // 修改为背景透明,避免刷新动画时闪烁,我们自己绘制白色背景  
  17.     ModifyStyleEx(0, WS_EX_TRANSPARENT);  
  18.   
  19.     // 这个接口指针使用非常频繁,存下来提高效率  
  20.     m_pRichEditOle = GetIRichEditOle();  
  21.   
  22.     // 启动定时器,用于更新动画,MIN_FRM_DELAY为一个宏,值为1,也可定义为其他不常用的值  
  23.     SetTimer(FRM_TIMER_ID, MIN_FRM_DELAY, NULL);  
  24.   
  25.     CRichEditCtrl::PreSubclassWindow();  
  26. }  
  27.   
  28. //  
  29. // 窗口销毁时进行一些必要的清理工作  
  30. //  
  31. void CRichEditCtrlEx::OnDestroy()  
  32. {  
  33.     // 停止定时器,这不是必要的  
  34.     KillTimer(FRM_TIMER_ID);  
  35.     // 清空内容,目的是删除所有插入的COleImage对象  
  36.     SetWindowText(_T(""));  
  37.     // 但是别忘了,RichEdit支持多级撤消,对象虽然删除了,但是撤消缓存  
  38.     // 中还保留有对象的引用,清空撤消缓存才能真正的释放这些对象  
  39.     EmptyUndoBuffer();  
  40.   
  41.     // 上面的工作必须在父类OnDestroy前调用,否则窗口已经销毁,清理没用  
  42.     CRichEditCtrl::OnDestroy();  
  43.   
  44.     // TODO: 在此处添加消息处理程序代码  
  45. }  
  46.   
  47. //  
  48. // 绘制背景  
  49. //  
  50. BOOL CRichEditCtrlEx::OnEraseBkgnd(CDC* pDC)  
  51. {  
  52.     // TODO: 在此添加消息处理程序代码和/或调用默认值  
  53.     CRect rc;  
  54.     GetClientRect(rc);  
  55.     pDC->FillSolidRect(rc, RGB(255, 255, 255));  
  56.     return TRUE;  
  57. }  
  58.   
  59. //  
  60. // 查找第一个字符索引大于或等于cpMin的对象在所有对象中的索引  
  61. //  
  62. int CRichEditCtrlEx::FindFirstObject(int cpMin, int nObjectCount)  
  63. {  
  64.     // 标准的二分查找算法,不用解释了  
  65.     int low = 0;  
  66.     int high = nObjectCount - 1;  
  67.     REOBJECT reoMid = {0};  
  68.     reoMid.cbStruct = sizeof(REOBJECT);  
  69.     while (low <= high) {  
  70.         int mid = (low + high) >> 1;  
  71.         if (m_pRichEditOle->GetObject(mid, &reoMid, REO_GETOBJ_POLEOBJ) != S_OK) {  
  72.             return -1;  
  73.         }  
  74.         reoMid.poleobj->Release();  
  75.         if (reoMid.cp == cpMin) {  
  76.             return mid;  
  77.         } else if (reoMid.cp < cpMin) {  
  78.             low = mid + 1;  
  79.         } else {  
  80.             high = mid - 1;  
  81.         }  
  82.     }  
  83.   
  84.     // 只不过最后没找到时不是返回-1,而是返回low,此时low必然大于high  
  85.     // 刚好满足条件  
  86.     return low;  
  87. }  
  88.   
  89. //  
  90. // 定时处理函数,更新动画  
  91. //  
  92. void CRichEditCtrlEx::OnTimer(UINT_PTR nIDEvent)  
  93. {  
  94.     // TODO: 在此添加消息处理程序代码和/或调用默认值  
  95.   
  96.     // 定时器ID必须为我们所设置的定时器ID,不要以为调用SetTimer这里收到的就一定时我们  
  97.     // 设置的定时器,别忘了父类还可能设置,这一点不用怀疑,我测试到过!  
  98.     // 当写入很多行文本,滚动条出现后,再在最后插入动画,然后向下滚动到底部,再继续向下  
  99.     // 滚动,虽然滚动条不动了,但是动画却动得特别快,显然收到了父类的定时器  
  100.     // 因此这个定时器ID最好设置一个不常用的值,这里随便设置了一个,为1  
  101.     if (nIDEvent == FRM_TIMER_ID) {  
  102.   
  103.         // 得到对象总数,大于0时才需要刷新  
  104.         int nObjectCount = m_pRichEditOle->GetObjectCount();  
  105.         if (nObjectCount > 0) {  
  106.             CRect rc;  
  107.             GetRect(rc); // 得到可视区文本的矩形区域  
  108.             // 分别使用左上角和右下角的点得到最小和最大字符索引  
  109.             // 即可见区域的最小和最大字符索引  
  110.             int cpMin = CharFromPos(rc.TopLeft());  
  111.             int cpMax = CharFromPos(rc.BottomRight());  
  112.   
  113.             // 使用二分查找算法找到第一个字符索引大于或等于cpMin的对象索引  
  114.             int iFirst = FindFirstObject(cpMin, nObjectCount);  
  115.             REOBJECT reo = {0};  
  116.             reo.cbStruct = sizeof(REOBJECT);  
  117.   
  118.             // 从第一个索引开始遍历对象更换帧  
  119.             for (int i = iFirst; i < nObjectCount; i++) {  
  120.                 if (m_pRichEditOle->GetObject(i, &reo, REO_GETOBJ_POLEOBJ) == S_OK) {  
  121.                     reo.poleobj->Release();  
  122.                     // 当前对象的字符索引大于最大字符索引,说明对象不在可见区域,停止遍历  
  123.                     if (reo.cp > cpMax) {  
  124.                         break;  
  125.                     }  
  126.   
  127.                     // 是COleImage对象时才能更新  
  128.                     if (InlineIsEqualGUID(reo.clsid, CLSID_OleImage)) {  
  129.                         // 更换帧  
  130.                         COleImage *pOleImage = COleImage::FromOleObject(reo.poleobj);  
  131.                         pOleImage->ChangeFrame();  
  132.                     }  
  133.                 }  
  134.             }  
  135.         }  
  136.     } else {  
  137.         CRichEditCtrl::OnTimer(nIDEvent);  
  138.     }  
  139. }  


最后添加一个InserImage函数,并实现它:

[cpp] view plain copy

  1. //  
  2. // 插入图像  
  3. //  
  4. HRESULT CRichEditCtrlEx::InsertImage(LPCTSTR lpszPathName)  
  5. {  
  6.     // 全部使用智能指针  
  7.     CComPtr<IStorage> spStorage;  
  8.     CComPtr<ILockBytes> spLockBytes;  
  9.     CComPtr<IOleClientSite> spOleClientSite;    
  10.     CComPtr<COleImage> spOleImage;  
  11.     CComPtr<IOleObject> spOleObject;  
  12.     CLSID clsid;  
  13.     REOBJECT reobject;  
  14.     HRESULT hr = E_FAIL;  
  15.   
  16.     do {  
  17.   
  18.         // 创建LockBytes  
  19.         hr = CreateILockBytesOnHGlobal(NULL, TRUE, &spLockBytes);  
  20.         if (hr != S_OK) {  
  21.             break;  
  22.         }  
  23.   
  24.         ASSERT(spLockBytes != NULL);  
  25.   
  26.         // 创建Storage  
  27.         hr = StgCreateDocfileOnILockBytes(spLockBytes,  
  28.             STGM_SHARE_EXCLUSIVE | STGM_CREATE | STGM_READWRITE, 0, &spStorage);  
  29.         if (hr != S_OK) {  
  30.             break;  
  31.         }  
  32.   
  33.         // 获取ClientSite  
  34.         hr = m_pRichEditOle->GetClientSite(&spOleClientSite);  
  35.         if (hr != S_OK) {  
  36.             break;  
  37.         }  
  38.   
  39.         // 创建COleImage实例  
  40.         hr = CoCreateInstance(CLSID_OleImage, NULL, CLSCTX_INPROC, IID_IOleImage, (LPVOID *) &spOleImage);  
  41.         if (hr != S_OK) {  
  42.             break;  
  43.         }  
  44.   
  45.         // 加载图像  
  46.         hr = spOleImage->LoadFromFile(_bstr_t(lpszPathName), this, 400);  
  47.         if (hr != S_OK) {  
  48.             break;  
  49.         }  
  50.   
  51.         // 获取IOleObject接口  
  52.         hr = spOleImage->QueryInterface(IID_IOleObject, (LPVOID *) &spOleObject);  
  53.         if (hr != S_OK) {  
  54.             break;  
  55.         }  
  56.   
  57.         // 获取IOleObject的用户CLSID  
  58.         hr = spOleObject->GetUserClassID(&clsid);  
  59.         if (hr != S_OK) {  
  60.             break;  
  61.         }  
  62.   
  63.         // 填充OLE对象属性  
  64.         ZeroMemory(&reobject, sizeof(REOBJECT));          
  65.         reobject.cbStruct   = sizeof(REOBJECT);  
  66.         reobject.clsid      = clsid;  
  67.         reobject.cp         = REO_CP_SELECTION;  
  68.         reobject.dvaspect   = DVASPECT_CONTENT;  
  69.         reobject.dwFlags    = REO_BELOWBASELINE;  
  70.         reobject.poleobj    = spOleObject;  
  71.         reobject.polesite   = spOleClientSite;  
  72.         reobject.pstg       = spStorage;  
  73.         SIZEL sizel = {0};  
  74.         reobject.sizel = sizel;  
  75.   
  76.         // 插入OLE对象  
  77.         hr = m_pRichEditOle->InsertObject(&reobject);  
  78.         if (hr != S_OK) {  
  79.             break;  
  80.         }  
  81.   
  82.         // 通知OLE容器保证OLE对象被正确引用  
  83.         hr = OleSetContainedObject(spOleObject, TRUE);  
  84.   
  85.     } while (0);  
  86.   
  87.     return hr;  
  88. }  

然后就可使用这个函数来插入图像,不局限于GIF,任何GDI+支持的图像都可以,当然,别忘了在App类的InitInstance中调用GdiplusStartup,在ExitInstance中调用GdiplusShutdown,否则是不能插入任何图像的。

CSND的审核速度可真够慢的,Demo我都传了两天了,才审核完,下面是Demo的下载地址,里面包含了完整的源代码和注释:

http://download.csdn.net/detail/haoekin/5360045

猜你喜欢

转载自blog.csdn.net/u014421422/article/details/115798990