Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Getting Timer Events in a CDocument-derived class

0.00/5 (No votes)
14 Apr 2002 1  
But WM_TIMER messages are not routable via the standard MFC "command routing" mechanism. This article shows you how to work around this.

Introduction

Getting timer events in a window class is easy. You set up a WM_TIMER handler, do a SetTimer operation, and every n milliseconds, or as close as the operating system quantizes it, and subject to standard errors imposed by thread priorities and the scheduler, you will see the timer event. As long as you don't expect much reliability in the timings you are in good shape.

But WM_TIMER messages are not routable via the standard MFC "command routing" mechanism. This is the mechanism that routes command messages from menus, toolbars, etc. to the main frame, views, documents, etc. Therefore, you can't get timer events in a CDocument.

Ah, you say, there's a simple answer: use a timer callback! Instead of doing a SetTimer of the form

SetTimer(id, ms, NULL);

which sets a timer for the current window, you can do

timerid = ::SetTimer(NULL, 0, ms, timercallback);

which sets up a callback function. This doesn't work. The reason is that the timercallback method must be a static class method or a global function. This means that it cannot be used if you have more than one instance of a CDocument, since it won't know what document is supposed to receive the timer event. There are some fairly complex methods you can use to deal with this, such as adding the timerid returned plus a pointer to the CDocument to a static member data structure that is used to maintain the map. The timer routine then looks up the event in the table and determines which document it should go to.

I have an alternative I prefer. I like it because it doesn't require the central repository of ID-to-document knowledge, which I think is hard to maintain.

It also generalizes nicely, allowing for heterogeneous documents to use the mechanism. It is not restricted to being only a single document type. This involves a bit of hand-editing of the MFC source, but is otherwise trivial.

The CTimedDocument superclass

The trick is to create an invisible window which receives the timer notifications, and couple this invisible window to the CDocument-derived class. Because I am a fan of subclassing, the way I get the generalization is to create an "intermediary class" which handles the timing. This is done by using the ClassWizard to create a new class which is a subclass of CDocument. If you don't know how to do this, check out my essay on dialog-based applications and follow the model for adding a new subclass. I call my subclass CTimedDocument.

I modify the class by adding two declarations

protected:
     CTimerWnd timer;            // the timer

     virtual void OnTimer() { ASSERT(FALSE); } // implement in subclass

     BOOL running( ) { return timer.GetSafeHwnd() != NULL; }

Note that any timed subclass of this class must implement an OnTimer method. Since it would be a mistake to have a timed document that didn't have an OnTimer handler, the parent class implements it by doing an ASSERT(FALSE), which will trap any such oversight.

You may say, "Wouldn't it have been better to have made that a pure virtual method, since you can never instantiate a class of type CTimerWnd?" Well, yes, it would, except for the fact that MFC CDocument- and CView-derived classes Do Not Play Well With Pure Virtual Methods. as in, they won't compile. 

I also use the ClassWizard to add an OnCloseDocument handler. This is a virtual method that is called when the document is closed, and performs any necessary cleanup. The body of OnCloseDocument is shown below. If timing was in progress, the timer window is destroyed.

void CTimedDocument::OnCloseDocument() 
   {
    if(timer.GetSafeHwnd() != NULL)
       timer.DestroyWindow();
    
    CDocument::OnCloseDocument();
   } 

Using CTimedDocument

The way to use this is to take the CDocument-derived class that you now want to time and change its parent class to CTimedDocument. This means that you need to change all instances of CDocument in your header and implementation files to CTimedDocument

In the header file, you have to modify the declaration

class CMyDocument : public CDocument

to read

class CMyDocument : public CTimedDocument

and in the implementation file, you must change all the superclass calls and the MESSAGE_TABLE declaration.

Examples:

Old

IMPLEMENT_DYNCREATE(CMyDocument, CDocument)

New

IMPLEMENT_DYNCREATE(CMyDocument, CTimedDocument)
Old
BEGIN_MESSAGE_MAP(CMyDocument, CDocument)
    //{{AFX_MSG_MAP(CDocument)

    //}}AFX_MSG_MAP

END_MESSAGE_MAP()
New
BEGIN_MESSAGE_MAP(CMyDocument, CTimedDocument)
    //{{AFX_MSG_MAP(CTimedDocument)

    ON_COMMAND(ID_START, OnStart)
    ON_UPDATE_COMMAND_UI(ID_START, OnUpdateStart)
    //}}AFX_MSG_MAP

END_MESSAGE_MAP()
Old
BOOL CMyDocument::OnNewDocument()
   {
    if (!CDocument::OnNewDocument())
      return FALSE;
    ...
New
BOOL CMyDocument::OnNewDocument()
   {
    if (!CTimedDocument::OnNewDocument())
      return FALSE;
    ...
Old
void CMyDocument::AssertValid() const
{
    CDocument::AssertValid();
}
New
void CMyDocument::AssertValid() const
{
    CTimedDocument::AssertValid();
}
Old
void CMyDocument::OnCloseDocument() 
   {
    ...your cleanup here
    CDocument::OnCloseDocument();
 
   }
New
void CMyDocument::OnCloseDocument() 
   {
    CTimedDocument::OnCloseDocument();
    ...your cleanup here
   }
Note that the order of cleanup and superclass invocation was changed! This is because I discovered that if I started doing cleanup while timer events were still happening, Bad Things Happened.

Note that the document has two new methods added. This is an example of how the particular application activates the timing: it has a menu item and a toolbar icon that start the simulation running. These are done as a toggle mode: selecting the menu item or clicking the toolbar button

void CMyDocument::OnStart() 
   {
    if(!running())
       { /* start */
    timer.Start(200, this);
       } /* start */
    else
       { /* stop */
    timer.Stop();
       } /* stop */
   }

Note that the running is a method of the CTimerWnd class which returns TRUE if the timer is running and FALSE if the timer is not running. If the timer is not running, it is started, and if it is running, it is stopped.

To handle the reporting of the status, add an UpdateCommandUI handler

void CMyDocument::OnUpdateStart(CCmdUI* pCmdUI) 
   {
    if(!running() && !CouldRun())
       { /* disable */
    pCmdUI->Enable(FALSE);
       } /* disable */
    else
       { /* enable/mark */
    pCmdUI->Enable(TRUE);
    pCmdUI->SetCheck(running());
       } /* enable/mark */
   }

The method CouldRun is a method of CMyDocument which tells if it is feasible to run. For example, in my simulation, this returns TRUE if there are objects to simulate, and FALSE if no objects have been added to the simulation. So if there is nothing to simulate, and the simulation is not already running (the simulation can destroy simulation objects when their meaning expires, and other objects could be introduced later, so there is a transient period when the simulation is running but there is nothing to simulate. During this period, if I had not added the !running() conditional as well, it would not be possible to stop the simulation.

If the simulation is running, or if the simulation is not running but there are objects in the simulation set, I enable the menu item and toolbar item. In addition, if the simulation is actually running, I set a checkmark on the menu item, and if the item has a toolbar icon, the icon is "checked" by having it stay in the depressed position.

For example, here is the toolbar when the simulation is not running but there are objects to simulate. The simulation icon is the third one from the right, a little airplane icon. The program name has been blocked out to protect some proprietary information (I can say that this simulates something dealing with airplane traffic, but that's as far as I can go right now).

When the SetCheck method is called with TRUE, the toolbar button appears like this, indicating the simulation is active. If I showed the menu dropped down, there would be a checkmark next to the menu item.

 

When there is nothing to simulate and the simulation is not active, the control is disabled.

Creating CTimedDocument-derived classes

When you create a new CDocument class that you want timed, due to a fundamental design flaw in ClassWizard, it ignores all your classes and only allows you to derive from known base classes (it knows about all your classes but refuses to acknowledge their existence when subclassing, which is just silly!) So you have to create a class derived from CDocument and hand-edit it as I've just described.

The CTimerWnd class

The heart of all of this is the CTimerWnd class. This is a dummy window which is created to handle the timer events. It is created in the ClassWizard by using the New Class option and creating a window called CTimerWnd which is a subclass of CWnd. You can also download my version. Note that to get this incorporated into ClassWizard, you will need to add it to your project (right-click on the Source Files folder in the project, select Add Files To Folder..., and add in the TimerWnd.cpp file. Repeat in the Header Files folder and add TimerWnd.h. Then go to the Windows Explorer or My Computer and delete the .clw file from your project. When you next invoke ClassWizard, it will prompt you to re-create the file, and when it does, it will include the new class.

To the methods and variables of the CTimerWnd class, I added

public:
        BOOL Start(UINT ms, CTimedDocument * d);
    void Stop() { DestroyWindow(); }
protected:
        CTimedDocument * doc;
        UINT interval;

I then added a WM_CREATE and WM_TIMER handler using ClassWizard.

It is necessary to initialize the doc member to NULL, so the indicated line must be added to the constructor. It is not necessary to initialize the interval variable, since this is guaranteed to be initialized properly by the time it is needed.

CTimerWnd::CTimerWnd()
   {
    doc = NULL;
   }

The implementation is simple. The new methods I added are shown below

#define IDT_TIMER 1

BOOL CTimerWnd::Start(UINT ms, CTimedDocument * d)
   {
    interval = ms;
    doc = d;
    return CWnd::Create(NULL,  // class name, let MFC invent one

        NULL,     // title, meaningless

        WS_CHILD, // child window

                  // conspicuous by its absense:

                  // WS_VISIBLE

        CRect(0,0,10,10), // useless nonempty rectangle

        AfxGetMainWnd(),  // attach to main window, need someplace

        IDC_STATIC);      // window ID

   }

The Create method has some interesting features. The class name can be NULL, which causes MFC to synthesize a class name. Since there is no need to have a specific class name, this simplifies the programming and means you don't have to worry about clashes of class names. There is no title, because the window has no text. It is a child window. This is the only style that is needed; in particular, you don't need visibility, a caption, buttons, etc. I had to give a valid CRect & parameter, so I chose some random numbers, such as 0,0,10,10 which are a valid rectangle that is otherwise meaningless. Since this is a child window, it must have a non-NULL parent. You might consider using one of the views of the document, but this is actually a bad idea; since views can come and go, and we want this associated with the document, not any particular view, using a view would be a fundamentally Bad Idea. If the particular view were closed, the timer events would stop, which is not what is intended. So I associate this window with the application's main window. I arbitrarily chose to assign the ID IDC_STATIC, because that is the least-likely ID that would cause a problem with the main window. Note there is one side effect here: something that is enumerating windows of the child frame could possibly have unexpected side effects if it encounters this window. However, very few operations actually do this.

The OnCreate handler actually starts the timer. The information required could be passed via the lParam of the CREATESTRUCT, but this is not common in MFC, and it is sufficient to store the interval in the member variable called interval. MFC is easier to deal with than pure-C in this respect.

int CTimerWnd::OnCreate(LPCREATESTRUCT lpCreateStruct) 
{
    if (CWnd::OnCreate(lpCreateStruct) == -1)
        return -1;
    
        SetTimer(IDT_TIMER, interval, NULL);    
    return 0;
}

The only remaining method is the OnTimer handler, which is

void CTimerWnd::OnTimer(UINT nIDEvent) 
   {
    if(doc != NULL)
        doc->OnTimer(); 
    CWnd::OnTimer(nIDEvent);
   }

Since this is calling the virtual method of CTimedDocument, it will actually call the OnTimer virtual method of whatever document you passed in (and if you didn't override the method in your document, it will call the base class implementation which does an ASSERT(FALSE)).

Other classes that were developed during this project are documented as a polygon manager, a polygon editor, and self-registering controls. The technique of using "http://www.pgh.net/~newcomer/presubclasswindow.htm">PreSublassWindow, which I've used before, is finally documented.

The views expressed in these essays are those of the author, and in no way represent, nor are they endorsed by, Microsoft.

Send mail to newcomer@flounder.com with questions or comments about this web site.
Copyright � 1999 All Rights Reserved

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here