CWndTimer, a Windows Timer Class
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:
- 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).
- 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 KbDownload source - 4 Kb

Comments
This is what I wanted :-)
Posted by balintn on 02/24/2008 03:20pmThanks 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
ReplyConsole MFC support example, Please!
Posted by Legacy on 03/30/2003 12:00amOriginally posted by: Jake
Reply