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;
virtual void OnTimer() { ASSERT(FALSE); }
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)
END_MESSAGE_MAP()
|
New |
BEGIN_MESSAGE_MAP(CMyDocument, CTimedDocument)
ON_COMMAND(ID_START, OnStart)
ON_UPDATE_COMMAND_UI(ID_START, OnUpdateStart)
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())
{
timer.Start(200, this);
}
else
{
timer.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())
{
pCmdUI->Enable(FALSE);
}
else
{
pCmdUI->Enable(TRUE);
pCmdUI->SetCheck(running());
}
}
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,
NULL,
WS_CHILD,
CRect(0,0,10,10),
AfxGetMainWnd(),
IDC_STATIC);
}
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