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

More by Author

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read