CWndTimer, a Windows Timer Class

Environment: VC6, MFC

Introduction

This is an original solution for implementing Windows timers. This class can be used in both WIN32 or MFC applications.

How It Works

The main idea of this solution is to use a unique separate thread to manage all the created timers. Two types of timers can be created:

  1. Timers that post a registered user message (UWM_TIMER) to the associated window each time a tick is generated. This type is created using a form (overload) of the Create() function:

    int Create(HWND const& rhWnd, UINT uiID, UINT uiPeriod);

    where rhWnd is the reference to the handle of the associated window, uiID is the suggested timer's index, and uiPeriod is the timer's period. The return value is the real created timer's index, or -1 in case of error.

    The addresses of all created timers of type 1 are kept for easy access in an STL multimap:

    static multimap<HWND, CWndTimer*> sm_oMultiMap;

    It is a multimap because it is possible to associate many timers (the values CWndTimer*) to the same window (the key HWND).

  2. Timers call an associated function each time a tick is generated. This type is created using a different form (overload) of the Create() function:

    bool Create(PTIMERPROC pTimerProc, LPVOID pArg,
                UINT uiPeriod);

    where pTimerProc is the pointer to the associated function, pArg is the pointer to the argument of the associated function (a void pointer), and uiPeriod is the timer's period. The return value is true for success of false for failure.

    The addresses of all created timers of type 2 are kept for easy access in an STL deque:

    static deque<CWndTimer*> sm_oQueue;

    When the timer objects are destroyed, their addresses are also erased from the appropriate container.

    The timers are accessed by the unique separate management thread from the thread function:

    UINT TimerThreadProc(LPVOID pParam)

This function has an infinite execution loop where all the created timers are checked whether their next tick time has expired. For the expired timers, the specific actions are generated (message posted for type 1. or associated function executed for type 2.). After that, the time of the next global tick event is estimated and the thread is put to sleep until that next global tick event. If there are no active timers (all are stopped), the thread is put to wait for an Windows event reactivation.

The user interface contains handy functions for useful actions:

Starting the timer:

bool Start();

Stopping the timer:

bool Stop();

Delete from container (map) all the timers associated to a given window:

static bool Delete(HWND const& rhWnd);

This is very useful to be called when the window is destroyed. But notice that also in the thread function the existence of the associated windows is checked (using ::IsWindow()) and if they don't exist anymore, their associated timers are also erased from the map.

Check whether the timer is running:

bool IsRunning();

Get the index:

UINT GetID();

Change the period:

void SetPeriod(UINT uiPeriod);

Get the period:

UINT GetPeriod();

Using this timer class is very easy. Let's assume we have a Dialog MFC application.

For type 1. timers, you need to declare the timers and the message handler in the header file:

//Timers
CWndTimer m_oWndTimer1, m_oWndTimer2;

// Generated message map functions
//{{AFX_MSG(CMyDlg)
//...
afx_msg LRESULT OnWndTimer(WPARAM wParam, LPARAM lParam);
//}}AFX_MSG
DECLARE_MESSAGE_MAP()

Then, in the source file, you have to map the message handler to the user registered message:

BEGIN_MESSAGE_MAP(CMyDlg, CDialog)
//{{AFX_MSG_MAP(CMyDlg)
  //...
  ON_REGISTERED_MESSAGE(CWndTimer::UWM_TIMER, OnWndTimer)
//}}AFX_MSG_MAP
END_MESSAGE_MAP()

In the OnInitDialog() function, you create the timers and possibly start them:

BOOL CMyDlg::OnInitDialog()
{
  CDialog::OnInitDialog();
  //...
  m_oWndTimer1.Create(m_hWnd, 1, 200);
  m_oWndTimer1.Create(m_hWnd, 2, 400);
  //
  m_oWndTimer1.Start();
  m_oWndTimer2.Start();
  //...
  return TRUE;
}

Finally, in the handler, you implement the specific actions. You identify the timers based on the index in wParam. In lParam, you have the tick count when the message UWM_TIMER was generated in the thread:

LRESULT CMyDlg::OnWndTimer(WPARAM wParam, LPARAM lParam)
{
  if(m_oWndTimer1.GetID() == wParam)
  {
    //...
  }
  else if(m_oWndTimer2.GetID() == wParam)
  {
    //...
  }
  return 0;
}

Similarly, for type 2. timers, you need to declare the timers and the associated functions in the header file:

//Timer Function
static void TimerFunc(LPVOID);
//Timer
CWndTimer m_oWndTimer3;

In the source file, you create and possibly start the timer in function OnInitDialog():

BOOL CMyDlg::OnInitDialog()
{
  CDialog::OnInitDialog();
  //...
  m_oWndTimer3.Create(TimerFunc, this, 1000);
  //
  m_oWndTimer3.Start();
  //...
  return TRUE;
}

Implementation

The full source code is given below:

The header (.h) file:

//WndTimer.h : header file

#ifndef __WNDTIMER_H__
#define __WNDTIMER_H__

#pragma warning(disable:4786)

#include <deque>
#include <map>

using namespace std;

//Windows Timer Class
class CWndTimer
{
  typedef void (*PTIMERPROC)(void*);
public:
  //CONSTRUCTOR
  CWndTimer();
  //DESTRUCTOR
  virtual ~CWndTimer();
  //Creation Functions
  int Create(HWND const& rhWnd, UINT uiID, UINT uiPeriod);
  bool Create(PTIMERPROC pTimerProc, LPVOID pArg, UINT uiPeriod);
  //Starting
  bool Start();
  //Stopping
  bool Stop();
  //Delete the Timers of the given Window
  static bool Delete(HWND const& rhWnd);
  //Check if is active
  bool IsRunning();
  //ID Getter
  UINT GetID();
  //Period Setter
  void SetPeriod(UINT uiPeriod);
  //Period Getter
  UINT GetPeriod();
  //Thread Function is a friend
  friend UINT TimerThreadProc(LPVOID pParam);
  //Timer Message
  static const UINT UWM_TIMER;

private:
  //Disallow copy
  CWndTimer(const CWndTimer&); 
  CWndTimer& operator=(const CWndTimer&);
  //Critical Section for protecting the access to the
  //static members
  static CRITICAL_SECTION sm_CS;
  static bool sm_bIniCS;
  //Data Map
  static multimap<HWND, CWndTimer*> sm_oMultiMap;
  //Data Queue
  static deque<CWndTimer*> sm_oQueue;
  //Thread Handle
  static HANDLE  sm_hThread;
  //Event Handle
  static HANDLE sm_hEvent;
  //Thread Run Flag
  static bool sm_bThreadRunning;
  //Timer Types
  enum { TYPE_PROC=0, TYPE_WIN=1 };
  int m_iType;
  //Timer ID
  UINT m_uiID;
  //Timer Period
  UINT m_uiPeriod;
  //Timer Procedure
  //If this Timer Procedure is called from other threads too,
  //the accessed data should be protected using appropriate
  // synchronization objects
  PTIMERPROC m_pTimerProc;
  //The Argument to Timer Procedure
  LPVOID m_pArg;
  //Scheduled Ticks's Tick Count
  DWORD m_uiTickNext;
  //Creation Flag
  bool m_bCreated;
  //Run Flag
  bool m_bRunning;
};

//ID Getter
inline UINT CWndTimer::GetID()
{
  return m_uiID;
}

//Check if is active
inline bool CWndTimer::IsRunning()
{
  return m_bRunning;
}

//Period Getter
inline UINT CWndTimer::GetPeriod()
{
  return m_uiPeriod;
}

#endif    // __WNDTIMER_H__

The source (.cpp) file:

//WndTimer.cpp

#include "stdafx.h"
#include "WndTimer.h"

#define UWM_TIMER_MSG _T("UWM_TIMER_MSG-{
                          F57C90D5_EC7C_158A_AD22_015F293A5FC0}")

//User Defined Timer Message
const UINT CWndTimer::UWM_TIMER = ::RegisterWindowMessage(
                                    UWM_TIMER_MSG);

//Critical Section for protecting the access to the
//static members
CRITICAL_SECTION CWndTimer::sm_CS;
bool CWndTimer::sm_bIniCS = false;
//Data Map
multimap<HWND, CWndTimer*> CWndTimer::sm_oMultiMap;
//Data Queue
deque<CWndTimer*> CWndTimer::sm_oQueue;
//Thread Handle
HANDLE CWndTimer::sm_hThread = NULL;
//Event Handle
HANDLE CWndTimer::sm_hEvent = NULL;
//Thread Run Flag
bool CWndTimer::sm_bThreadRunning = false;

//CONSTRUCTOR
CWndTimer::CWndTimer() : m_bCreated(false), m_bRunning(false),
                         m_pTimerProc(NULL), m_pArg(NULL),
                         m_uiID(-1), m_iType(-1)
{
  if(false == sm_bIniCS)
  {
    //First Object
    //The Critical section needs Initialization
    ::InitializeCriticalSection(&sm_CS);
    sm_bIniCS = true;
  }
}

//DESTRUCTOR
CWndTimer::~CWndTimer()
{
  if(false == m_bCreated)
    //Nothing to Destroy
    return;
  ::EnterCriticalSection(&sm_CS);
  //==============================================================
  ASSERT(sm_hThread != NULL);
  if(TYPE_WIN == m_iType)
  {
    multimap<HWND, CWndTimer*>::iterator it=
                                      sm_oMultiMap.begin(),
                                      itEnd=sm_oMultiMap.end();
    while(it != itEnd)
    {
      if(it->second == this)
      {
        sm_oMultiMap.erase(it);
        break;
      }
      it++;
    }
  }
  else    //TYPE_PROC == m_iType
  {
    deque<CWndTimer*>::iterator it1=sm_oQueue.begin(),
                                itEnd1=sm_oQueue.end();
    while(it1 != itEnd1)
    {
      if(*it1 == this)
      {
        sm_oQueue.erase(it1);
        break;
      }
      it1++;
    }
  }
  //==============================================================
  ::LeaveCriticalSection(&sm_CS);
  if(true==sm_oMultiMap.empty() && true==sm_oQueue.empty())
  {
    //Last Object
    ::DeleteCriticalSection(&sm_CS);
    sm_bIniCS = false;
  }
}

//Creation Functions
int CWndTimer::Create(HWND const& rhWnd, UINT uiID, UINT uiPeriod)
{
  if(true == m_bCreated)
    //Already Created
    return m_uiID;
 //Check the Window
  if((NULL==rhWnd) || (FALSE==::IsWindow(rhWnd)))
    //Error
    return -1;
  ::EnterCriticalSection(&sm_CS);
  //==============================================================
  m_iType = TYPE_WIN;
  m_uiPeriod = uiPeriod;
  //Check if the ID is already taken
  multimap<HWND, CWndTimer*>::iterator it =
                    sm_oMultiMap.find(rhWnd);
  if(it != sm_oMultiMap.end())
  {
    UINT uiMax = 0;
    bool bFound = false;
    while(it != sm_oMultiMap.upper_bound(rhWnd))
    {
      if(false == bFound)
      {
        if(it->second->m_uiID == uiID)
          bFound = true;
      }
      if(it->second->m_uiID > uiMax)
        uiMax = it->second->m_uiID;
      it++;
    }
    if(true == bFound)
      m_uiID = uiMax+1;
    else
      m_uiID = uiID;
  }
  else
    m_uiID = uiID;
  //Save in Map
  sm_oMultiMap.insert(pair<HWND, CWndTimer*>(rhWnd, this));
  if(NULL == sm_hThread)
  {
    //First object is creating the Thread in Suspended State
    DWORD dwThreadID;
    sm_hThread = ::CreateThread((LPSECURITY_ATTRIBUTES)NULL,
                                (DWORD)0,
      (LPTHREAD_START_ROUTINE)TimerThreadProc, NULL,
                              (DWORD)CREATE_SUSPENDED,
                              (LPDWORD)&dwThreadID);
    ASSERT(sm_hThread != NULL);
    //Create the Event (the system closes the handle
    //automatically when the process terminates)
    sm_hEvent = ::CreateEvent(NULL, FALSE, FALSE, NULL);
    ASSERT(sm_hEvent != NULL);
    //Initialized to Above Normal Priority to ensure higher
    //priority than the User Interface threads
    ::SetThreadPriority(sm_hThread, THREAD_PRIORITY_ABOVE_NORMAL);
    //Start the Thread
    ::ResumeThread(sm_hThread);
  }
  //==============================================================
  ::LeaveCriticalSection(&sm_CS);
  m_bCreated = true;
  return m_uiID;
}

bool CWndTimer::Create(PTIMERPROC pTimerProc,
                                  LPVOID pArg,
                                  UINT uiPeriod)
{
  if(true == m_bCreated)
    //Already Created
  return false;
  ::EnterCriticalSection(&sm_CS);
  //==============================================================
  m_iType = TYPE_PROC;
  m_uiPeriod = uiPeriod;
  m_pTimerProc = pTimerProc;
  m_pArg = pArg;
  //Save in Queue
  sm_oQueue.push_back(this);
  if(NULL == sm_hThread)
  {
    //First object is creating the Thread in Suspended State
    DWORD dwThreadID;
    sm_hThread = ::CreateThread((LPSECURITY_ATTRIBUTES)NULL,
                                (DWORD)0,
      (LPTHREAD_START_ROUTINE)TimerThreadProc, NULL,
                              (DWORD)CREATE_SUSPENDED,
                              (LPDWORD)&dwThreadID);
    ASSERT(sm_hThread != NULL);
    //Create the Event (the system closes the handle
    //automatically when the process terminates)
    sm_hEvent = ::CreateEvent(NULL, FALSE, FALSE, NULL);
    ASSERT(sm_hEvent != NULL);
    //Initialized to Above Normal Priority to ensure higher
    //priority than the User Interface threads
    ::SetThreadPriority(sm_hThread, THREAD_PRIORITY_ABOVE_NORMAL);
    //Start the Thread
    ::ResumeThread(sm_hThread);
  }
  //==============================================================
  ::LeaveCriticalSection(&sm_CS);
  m_bCreated = true;
  return true;
}

//Starting
bool CWndTimer::Start()
{
  if(false == m_bCreated)
    //Not Created
    return false;
  ::EnterCriticalSection(&sm_CS);
  //==============================================================
  if(false == m_bRunning)
  {
    //Next Scheduled Tick
    m_bRunning = true;
    m_uiTickNext = ::GetTickCount() + m_uiPeriod;
    //First Tick
    if(TYPE_WIN == m_iType)
    {
      multimap<HWND, CWndTimer*>::iterator
                        it=sm_oMultiMap.begin(),
                        itEnd=sm_oMultiMap.end();
      while(it != itEnd)
      {
        if(it->second == this)
        {
          ::PostMessage(it->first, UWM_TIMER, m_uiID,
                        ::GetTickCount());
          break;
        }
        it++;
      }
    }
    else    //TYPE_PROC == m_iType
      m_pTimerProc(m_pArg);
    if(false == sm_bThreadRunning)
    {
      //Resume the thread
      sm_bThreadRunning = true;
      ::LeaveCriticalSection(&sm_CS);
      ::SetEvent(sm_hEvent);
      return true;
    }
  }
  //==============================================================
  ::LeaveCriticalSection(&sm_CS);
  return true;
}

//Stopping
bool CWndTimer::Stop()
{
  if(false == m_bCreated)
    //Not Created
    return false;
  ::EnterCriticalSection(&sm_CS);
  //==============================================================
  m_bRunning = false;
  //==============================================================
  ::LeaveCriticalSection(&sm_CS);
  return true;
}

//Period Setter
void CWndTimer::SetPeriod(UINT uiPeriod)
{
  ::EnterCriticalSection(&sm_CS);
  //==============================================================
  m_uiPeriod = uiPeriod;
  UINT uiTickNext = ::GetTickCount() + m_uiPeriod;
  if(uiTickNext < m_uiTickNext)
    m_uiTickNext = uiTickNext;
  //==============================================================
  ::LeaveCriticalSection(&sm_CS);
}

//Delete the Timers of the given Window
bool CWndTimer::Delete(HWND const& rhWnd)
{
  //Check if the Window is valid
  if((rhWnd!=NULL) && ::IsWindow(rhWnd))
  {
    multimap<HWND, CWndTimer*>::iterator it, itEnd;
    ::EnterCriticalSection(&sm_CS);
    //============================================================
    it=CWndTimer::sm_oMultiMap.find(rhWnd);
    if(it!=sm_oMultiMap.end())
    {
      itEnd = CWndTimer::sm_oMultiMap.upper_bound(it->first);
      while(it != itEnd)
        it = CWndTimer::sm_oMultiMap.erase(it);
      //==========================================================
      ::LeaveCriticalSection(>sm_CS);
      //OK, deleted
      return true;
    }
    //============================================================
    ::LeaveCriticalSection(>sm_CS);
    //Cannot Find Window
    return false;
  }
  //Invalid Window
  return false;
}

//Thread Function
//The Timer's resolution is imposed to about 10 ms. The Timer's
//Period should be > 20ms, but for good results is recommended
//>= 100 ms.

#define MAX_TICK  200
#define MIN_TICK  20
#define MIN_TICK2 10

UINT TimerThreadProc(LPVOID pParam)
{
  DWORD dwTick;
  bool bSuspend;
  multimap<HWND, CWndTimer*>::iterator it, itEnd, _itEnd;
  deque<CWndTimer*>::iterator it1, itEnd1;
  while(TRUE)
  {
    ::EnterCriticalSection(&CWndTimer::sm_CS);
    //============================================================
    it = CWndTimer::sm_oMultiMap.begin();
    itEnd = CWndTimer::sm_oMultiMap.end();
    while(it != itEnd)
    {
      if(true == it->second->m_bRunning)
      {
        dwTick = ::GetTickCount();
        if(dwTick + MIN_TICK2 >= it->second->m_uiTickNext)
        {
          //Check if the Window is still valid
          if(::IsWindow(it->first))
          {
            //Reschedule Tick
            it->second->m_uiTickNext += it->second->m_uiPeriod;
            //Current Tick
            ::PostMessage(it->first, CWndTimer::UWM_TIMER,
                          it->second->m_uiID, dwTick);
          }
          else
          {
            //Destroy everything related to that Window from
            //the Map
            _itEnd = CWndTimer::sm_oMultiMap.upper_bound(
                                             it->first);
            while(it != _itEnd)
              it = CWndTimer::sm_oMultiMap.erase(it);
            continue;
          }
        }
      }
      it++;
    }
    //
    it1 = CWndTimer::sm_oQueue.begin();
    itEnd1 = CWndTimer::sm_oQueue.end();
    while(it1 != itEnd1)
    {
      if(true == (*it1)->m_bRunning)
      {
        dwTick = ::GetTickCount();
        if(dwTick + MIN_TICK2 >= (*it1)->m_uiTickNext)
        {
          //Reschedule Tick
          (*it1)->m_uiTickNext += (*it1)->m_uiPeriod;
          //Current Tick - call the Timer Procedure
          (*it1)->m_pTimerProc((*it1)->m_pArg);
        }
      }
      it1++;
    }
    //
    bSuspend = true;
    //Schedule the Next Global Tick
    dwTick = UINT_MAX;    //4294967295 or 0xffffffff
    if(CWndTimer::sm_oMultiMap.size() > 0)
    {
      for(it=CWndTimer::sm_oMultiMap.begin();
                        it!=CWndTimer::sm_oMultiMap.end(); it++)
      {
        if(true == it->second->m_bRunning)
        {
          bSuspend = false;
          if(it->second->m_uiTickNext < dwTick)
            dwTick = it->second->m_uiTickNext;
        }
      }
    }
    //
    if(CWndTimer::sm_oQueue.size() > 0)
    {
      for(it1=CWndTimer::sm_oQueue.begin();
              it1!=CWndTimer::sm_oQueue.end(); it1++)
      {
        if(true == (*it1)->m_bRunning)
        {
          bSuspend = false;
          if((*it1)->m_uiTickNext < dwTick)
            dwTick = (*it1)->m_uiTickNext;
        }
      }
    }
    if(false == bSuspend)
    {
      if(dwTick > ::GetTickCount())
      {
        dwTick -= ::GetTickCount();
        if(dwTick < MIN_TICK)
          dwTick = MIN_TICK;
      }
      else
        dwTick = MIN_TICK;
    }
    else    //true == bSuspend
    {
      //Suspend the Thread
      CWndTimer::sm_bThreadRunning = false;
      ::LeaveCriticalSection(&CWndTimer::sm_CS);
      ::WaitForSingleObject(CWndTimer::sm_hEvent, INFINITE);
      continue;
    }
    //============================================================
    ::LeaveCriticalSection(&CWndTimer::sm_CS);
    //Check at most every MAX_TICK ms because some Timer Periods
    //could be changed
    if(dwTick > MAX_TICK)
      dwTick = MAX_TICK;
    //Sleep until the Next Event
    ::Sleep(dwTick);
  }
  return 0;
}

Advantages compared to the classical Windows solution (using WM_TIMER) are:

  • Easy to use class;
  • Offers more possibilities for controlling the timer objects;
  • The accuracy is better compared to the classical Windows solution which is unpredictable. For this class the accuracy is limited to about 10ms.

Disadvantages:

  • There can be some problems when the period is changed on the fly (with the timer running). The first tick event can be generated later for periods less than 200ms (the defined MAX_TICK). This is so because the management thread may be already put to sleep for a time period longer than the new set period. After at most the first 200ms everything should be fine.
  • For type 2. timers the functions associated to timers are called directly from the thread function. It can create problems if these functions are lengthy (with a lot of internal processing). It can disturb the operation of all the other timers. Also if you use synchronization objects inside these functions, the management thread can be stopped in unpredictable manner. So for type 2. timers it is recommended to use fast executing functions and to be careful with the use of synchronization objects in the timer functions.

Please contact me if you have solutions for the mentioned problems, any improvement ideas, or you detect any bugs or other weaknesses (besides the above mentioned) in the implementation.

Downloads

Download demo project - 16 Kb
Download source - 4 Kb



Comments

  • This is what I wanted :-)

    Posted by balintn on 02/24/2008 03:20pm

    Thanks a lot, this is the exact simple behaviour and usage that I was looking for, congratulations. I have to admit I havent checked the implementation, but the interface is just what I need. One comment: am I right that you can't delete timers of type 2? It would be nice to have, perhaps based on the argument - I pass in this pointers anyway. Thanks, B

    Reply
  • Console MFC support example, Please!

    Posted by Legacy on 03/30/2003 12:00am

    Originally posted by: Jake

    This is an awesome code. Could we get an example for console MFC support, Please! thank.. 
    
    

    Reply
Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • The explosion in mobile devices and applications has generated a great deal of interest in APIs. Today's businesses are under increased pressure to make it easy to build apps, supply tools to help developers work more quickly, and deploy operational analytics so they can track users, developers, application performance, and more. Apigee Edge provides comprehensive API delivery tools and both operational and business-level analytics in an integrated platform. It is available as on-premise software or through …

  • A global data storage provider whose business is booming needed a best-in-class data center to serve as the backbone of its technical operations going forward—and it needed it delivered within a year.

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds