在ATL无窗口ACTIVEX控件中响应timer消息

  最近,在尝试着写一个能够显示静态和动态图片的ACTIVEX控件,就像QQ的一样,能插入到RICHEDIT控件中。由于只是在尝试,很多功能都没有实现,只实现显示。如果单单显示静态图片,是很简单的。但要是显示动态GIF图片,就有一定的难度,首先要分析图片的帧数,还要控件图片按顺序显示。如果写成控件,还要保证插入到RICHEDIT控件中还能动起来,就更有难度了,因为ATL在响应TIMER事件的时候有很多困难要克服。

  对于图片的显示,由于只是在尝试,我只提供一个接口函数Play(),图片是写死在控件里的。让控件显示图片只要调用该函数即可。基于原理是,在Play()中,载入图片,判断图片是不是动态的GIF图片,不是,则直接显示,是动态的GIF,则创建一个TIMER对象,将TIMER的时间间隔设置为图片的第一帧的应该显示的时间长度,在OnTimer()中,则将图片应该显示的帧切换到下一帧,并修改TIMER的时间间隔为该帧应该显示的时间长度。至于如何显示动态GIF图片,可以看我写的另一篇文章。

  网上有文章说,要让ATL控件响应TIMER消息,只要将控件设为有窗口,然后调用SetTimer()来设置TIMER,然后在OnTimer()中响应timer消息即可。我照着做了,当要生成的控件显示动态图片时,在测试容器中测试的时候是会动的,但当把控件插入到richedit控件中时,图片就不会动了。据检查,原因应该是窗口句柄为NULL。(明明设为有窗口控件了,m_hWnd仍然为NULL,真的不知道为什么。如果有人知道的话,希望你能告诉我。因为我也是刚刚接触COM组件的开发)。

  不得已,只好改成无窗口的控件。但无窗口怎么响应timer消息呢,没有窗口句柄啊。确实如此,没有句柄是无法接收TIMER消息的。有一种方法是可以用TimeProc来响应,就是说,在SetTimer()的时候,窗口句柄可以为空,但我们给这个函数的最后一个参数传一个回调函数。然后,在我们的回调函数中实现换帧。但这种方法有一个不好之处是你要在回调函数中访问控件对象的成员变量,还要管理每个控件的timer对象,必须将每一个控件和它的timer映射起来进行集中管理,因为TimeProc回调函数是由系统调用的,不管哪个控件的timer间隔到了,它都被调用,就是说TimeProc是被所以的控件实例共享的,你要在TimeProc中判断现在是哪个控件的时候来了。此外,刚才说了回调函数中访问控件对象的成员变量,像必须传一个对象指针给TimeProc,而TimeProc的格式是固定的,你只能通过全局函数来实现。总之,管理起来非常复杂。

  既然没有句柄,我们就给它一个。怎么给?有一个比较笨但看起来还不错的方法,就是引入一个辅助对象,该对像是个窗口对象,当然,它就有句柄了。有句柄,我们就可以给它SetTimer,并能通过OnTimer()响应来响应timer消息,我们只要在该OnTimer中,调用控件的实现换帧的函数即可。下面是代码的实现:

class CSImgCtl;//我们的控件类

class CWinHidden :
public CWindowImpl
{
 BEGIN_MSG_MAP(CWinHidden)
        MESSAGE_HANDLER(WM_TIMER, OnTimer)
  END_MSG_MAP()
public:
 CWinHidden():m_nTimer(0), m_pFullCtrl(NULL){};
 ~CWinHidden()
 {
  if (m_nTimer != 0)
  {
   KillTimer(m_nTimer);
   m_nTimer = 0;
  }
 }
 void AttachCtl(CSImgCtl* pFullCtrl)
 {
  m_pFullCtrl = pFullCtrl;
 }
 BOOL SetThisTimer(UINT nIDEvent, UINT uElapse, TIMERPROC lpTimerFunc )
 {
  if (m_hWnd == NULL)
  {
   RECT rt = {0, 0, 0, 0};
   Create(NULL, rt);
  }
  m_nTimer = ::SetTimer(m_hWnd, nIDEvent, uElapse, lpTimerFunc);
  return m_nTimer == 0;
 }
 void KillThisTimer()
 {
  if (m_nTimer != 0)
  {
   KillTimer(m_nTimer);
   m_nTimer = 0;
  }
 }
public:
 LRESULT OnTimer(UINT, WPARAM, LPARAM, BOOL&);
private:
 CSImgCtl*   m_pFullCtrl;
 UINT        m_nTimer;
};

LRESULT CWinHidden::OnTimer(UINT uMsg, WPARAM wParam,
       LPARAM lParam, BOOL& bHandled)
{
 if ((UINT)wParam != m_nTimer)
  return -1;

 if (m_pFullCtrl != NULL)
  m_pFullCtrl->OnTimer(uMsg, wParam, lParam, bHandled);//调用控件对象的函数
 return 0;
}

首先,在我们的控件类中,添加一个辅助类的成员

CWinHidden m_HideWnd;

然后,当我们要显示图片时,我们在Play()函数中添加如下代码

if (m_pImg == NULL)
 {
  m_pImg = new ImageEx(L"wk.gif");
  if (m_pImg == NULL)
   return S_OK;

 if (m_pImg->IsAnimatedGif())
  {  
   long lFrameTime = m_pImg->GetFrameTime();
   m_HideWnd.AttachCtl(this);                //将辅助类对象与本对象绑定
   m_HideWnd.SetThisTimer(1, lFrameTime, NULL);//为辅助类对象设置时钟
  }

  FireViewChange();

}

最后,在我们控件的OnTimer()中,实现换帧即可。

  从代码上看来,我们只不过是间接实现了OnTimer而已,确实如此,这也是不得已的办法,原因上面说了。但是,我们可以看到,如果我们要写一个无窗口控件,而又要通过消息来完成一些功能时,我们也可以通过这个函数来实现想要的功能,只要在辅助类的OnTimer()中调用控件实现你要的功能的函数,然后立即关闭时钟。要调用这些功能时,只要在辅助类对象上调用SetTimer()就行。比如:

m_HideWnd.AttachCtl(this);                //将辅助类对象与本对象绑定
m_HideWnd.SetThisTimer(1, 50, NULL);//为辅助类对象设置时钟

执行上面的代码50MS后,OnTimer()将被调用,我们在我们的OnTimer()中这样实现即可

LRESULT CWinHidden::OnTimer(UINT uMsg, WPARAM wParam,
       LPARAM lParam, BOOL& bHandled)
{
 if ((UINT)wParam != m_nTimer)
  return -1;

 if (m_pFullCtrl != NULL)
  m_pFullCtrl->SomeFunc();//调用控件对象的函数,在这个函数中实现我们要的功能
 return 0;
}

补充:我以前写的时候,没有注意到会存在BUG,如果按我上面写的话,在删除控件的时候会出现严重的BUG并导致程序崩溃。出错的原因在于CWinHidden析构的时候没有将窗口删除,所以,我们要在把它的析构函数修改为:

~CWinHidden()
 {
  if (m_nTimer != 0)
  {
   KillTimer(m_nTimer);
   m_nTimer = 0;
  }
  if (m_hWnd != NULL)
  {
   ::DestroyWindow(m_hWnd);
   m_hWnd = NULL;
  }
 }

加粗的几行是补充上去的。

你可能感兴趣的:(COM/ATL/ACTIVEX)