Senden von Kommandos an MFC-Dokumente

Veröffentlicht: 28. Jan 2002 | Aktualisiert: 15. Jun 2004

Von Paul DiLascia

Die Versorgung von DokumentobjektenmitZeitgeber-Ereignissenkann auf verschiedenen Wegen erfolgen.

Diesen Artikel können Sie hier lesen dank freundlicher Unterstützung der Zeitschrift:

Bild02

Frage

Ich schreibe eine Anwendung, die dem Leistungsmonitor von Windows 2000 ähnelt. Es gibt ein Dokumentobjekt und verschiedene Darstellungen der Daten. Das Dokumentobjekt ist für die regelmäßige Erfassung der Daten zuständig und informiert die betreffenden View-Objekte, die für die Darstellung der Daten in verschiedenen Formaten zuständig sind. Damit das Dokumentobjekt regelmäßig neue Daten erfassen kann, braucht es einen Zeitgeber. Allerdings beruht das Dokumentobjekt nicht auf einem Fenster und kann daher nicht die regulären Zeitgeber bemühen. Soweit ich die Sache durchschaue, bieten sich drei verschiedene Lösungen an:

  • Man legt den Zeitgeber in einem der Views an. Sobald sich der Zeitgeber meldet, informiert der View das Dokument und fordert es zur Erfassung der Daten auf.

  • Man legt in oder für jedes Dokumentobjekt einen separaten Thread an. Der Thread kümmert sich um den Zeitgeber.

  • Man legt in CMainFrame einen Zeitgeber an und ruft von dort aus entsprechende Funktionen des Dokuments auf.

Allerdings gefällt mir keine dieser Lösungen. Lässt sich das Problem nicht besser lösen?

Antwort

Von den Lösungswegen, die Sie in Ihrer Liste angegeben haben, ist die Unterbringung des Zeitgebers in einem View keine gute Idee, weil es dann in jedem View einen Zeitgeber gibt. Zeitgeber gehören aber zu den relativ knappen Ressourcen. Das galt insbesondere in älteren Windows-Versionen, wenn es auch heute nicht mehr so gravierend ist. Die Versorgung mit separaten Threads ist eigentlich viel zu aufwendig, wenn es um eine so einfache Sache wie einen Zeitgeber geht. Threads führen mir penetranter Regelmäßigkeit dazu, dass das Programmiererleben komplizierter wird. Bleibt der dritte Vorschlag, nämlich im Hauptrahmenfenster einen Zeitgeber anzulegen und von dort aus die Dokumente aufzurufen. Ich möchte Ihnen nun zeigen, wie man diese Lösung implementieren könnte, und Ihnen anschließend eine weitere Lösung vorführen, die Sie in Ihrer Liste noch nicht berücksichtigt haben.
Ich vermute, dass Sie die dritte Lösung mit dem Aufruf der Dokumente im Hauptrahmenfenster nicht mögen, weil sie etwas hölzern und ungehobelt wirkt. Warum sollte das Rahmenfenster irgendetwas über die Dokumente wissen? Allerdings lässt sich diese Lösung sauber implementieren. Statt nämlich CMyDoc::DoTimerThing aufzurufen, können Sie die WM_TIMER-Nachricht in ein WM_COMMAND mit einer ID wie ID_APPTIMER umwandeln und diesen Befehl auf dem üblichen Weg weiterleiten, so dass er den Dokumenten via ON_COMMAND zugeht. Dokumente können zwar nicht alle Fensternachrichten auswerten, wohl aber WM_COMMAND. Tatsächlich gehört es zu den Neuerungen, die mit der MFC-Architektur zur Weiterleitung von Befehlen eingeführt wurden, dass nun auch Objekte, die nichts mit Fenstern zu tun haben, Befehle annehmen und ausführen können. Es sieht ganz so aus, als bräuchten Sie nur Folgendes zu tun:

CMainFrame::OnTimer(...) 
{ 
   SendMessage(WM_COMMAND, ID_APPTIMER); 
}

Sobald Ihr Hauptrahmen also eine Meldung vom Zeitgeber erhält, schickt er sich selbst den Befehl ID_APPTIMER. Die MFC leitet diesen Befehl auf dem üblichen Weg durch's System. Und jedes Objekt, das einen ON_COMMAND-Handler für ID_APPTIMER hat, kann den Befehl dann auswerten. Sie können auch ON_COMMAND_EX verwenden, damit mehrere Objekte denselben Befehl auswerten können.

L1 loop.cpp

////////////////// 
// Standardverfahren zur Erfassung aller Dokumente 
// in einer Schleife. 
// 
// for (alle Dokument-Templates) { 
//    for (alle Dokumente im Template) { 
//       ... 
//    } 
// } 
// 
CWinApp* pApp = AfxGetApp(); 
POSITION pos1 = pApp->GetFirstDocTemplatePosition(); 
while (pos1) { 
   CDocTemplate* ptempl = (CDocTemplate*)pApp->GetNextDocTemplate(pos1); 
   POSITION pos2 = ptempl->GetFirstDocPosition(); 
   while (pos2) { 
      CDocument *pDoc; 
      if ((pDoc=ptempl->GetNextDoc(pos2)) != NULL) 
         pDoc->DoSomething(....); 
   } 
}

L2 CDocIterator

DocIter.h 
/////////////////////////////////////////////////////// 
// System Journal, Januar/Februar 2002 
// Autor: Paul DiLascia. 
// 
// Lässt sich mit Visual C++ 6.0 kompilieren und läuft  
// unter Windows 98 (und wohl auch unter Windows 2000). 
// Setzen Sie in Ihrem Editor die Tabulatorweite auf 3. 
// 
class CDocIterator { 
protected: 
    CPtrList      m_doclist; 
    CDocument*    m_pDoc; 
    POSITION      m_pos; 
public: 
    CDocIterator(); 
    // ermittle aktuelles Dokument 
    CDocument* doc() { 
        return m_pDoc; 
    } 
    // gehe zum ersten Dokument - wird nach der  
    // Konstruktion nicht gebraucht 
    BOOL First() { 
        m_pDoc = NULL; 
        m_pos = m_doclist.GetHeadPosition(); 
        return Next(); 
    } 
    // gehe zum letzten Dokument 
    BOOL Next() { 
        if (m_pos) { 
            m_pDoc = (CDocument*)m_doclist.GetNext(m_pos); 
        } else { 
            m_pDoc=NULL; 
        } 
        return m_pDoc != NULL; 
    } 
    // nächstes Dokument, C++-Art 
    const CDocIterator& operator++(int) { 
        Next(); 
        return *this; 
    } 
    // für for-Schleifen 
    BOOL operator()() { 
        return m_pDoc != NULL; 
    } 
}; 
DocIter.cpp 
/////////////////////////////////////////////////////// 
// System Journal, Januar/Februar 2002 
// Autor: Paul DiLascia. 
// 
// Lässt sich mit Visual C++ 6.0 kompilieren und läuft  
// unter Windows 98 (und wohl auch unter Windows 2000). 
// Setzen Sie in Ihrem Editor die Tabulatorweite auf 3. 
// 
#include "stdafx.h" 
#include "DocIter.h" 
#ifdef _DEBUG 
#define new DEBUG_NEW 
#undef THIS_FILE 
static char THIS_FILE[] = __FILE__; 
#endif 
// Der Konstruktor initialisiert die Dokumentliste 
// und geht zum ersten Dokument. 
// 
CDocIterator::CDocIterator() 
{ 
    CWinApp* pApp = AfxGetApp(); 
   POSITION pos1 = pApp->GetFirstDocTemplatePosition(); 
   while (pos1) { 
      CDocTemplate* ptempl = (CDocTemplate*)pApp->GetNextDocTemplate(pos1); 
      POSITION pos2 = ptempl->GetFirstDocPosition(); 
      while (pos2) { 
         CDocument *pdoc; 
         if ((pdoc=ptempl->GetNextDoc(pos2)) != NULL) { 
                m_doclist.AddHead(pdoc); 
            } 
      } 
   } 
    First(); 
}

Das funktioniert zwar, zieht aber auch ein kleines Problem nach sich: Die MFC leitet Befehle nur ans aktive Dokument/View weiter. Sind noch weitere Dokumente offen, aber nicht aktiv, erhalten diese keine WM_COMMAND-Nachricht. Nun könnte man zwar die Anwendung so ändern, dass die Befehle auch zu den passiven Dokumenten gelangen, aber dann würden auch gewöhnliche Befehle wie Datei / Speichern plötzlich an alle Dokumente übermittelt. Oops! Nur der Hinweis vom Zeitgeber soll an alle Dokumente übermittelt werden. Und wie erreicht man das? Wie schickt man ein WM_COMMAND an alle Dokumente?
Die Übermittlung von Befehlen an Objekte, die wie Dokumente nichts mit Fenstern zu tun haben, erfolgt in der MFC mit Hilfe der virtuellen Funktion CCmdTarget::OnCmdMsg. Wenn Ihr Fenster eine WM_COMMAND-Nachricht erhält, durchläuft diese einen langen Weg durch CWnd-Code und virtuelle Funktionen. Irgendwann wird im Zuge der Weiterleitung der Nachricht die Funktion CWnd::OnCommand aufgerufen, die wiederum OnCmdMsg aufruft.

// in CWnd::OnCommand 
OnCmdMsg(nID, CN_COMMAND, NULL, NULL);

Darin ist nID die Befehls-ID und CN_COMMAND ein Code, dem OnCmdMsg entnehmen kann, dass es sich um einen Befehl handelt und nicht um eine UI-Aktualisierung, die mit dem Code CN_UPDATE_COMMAND_UI einhergeht. Die zusätzlichen Parameter werden für CN_COMMAND nicht gebraucht.
Unter dem Strich bleibt also festzuhalten: Wenn Sie einen Zeiger auf ein Dokument haben und dem Dokument einen Befehl schicken möchten, rufen Sie einfach OnCmdMsg auf:

pDoc->OnCmdMsg(nID, CN_COMMAND, NULL, NULL);

Das ist eine ganz allgemeine Lösung in dem Sinne, dass jeder, der diese Funktion aufruft, nichts über das Dokument zu wissen braucht. Er braucht nur zu wissen, dass es sich überhaupt um ein Dokument handelt. Tatsächlich braucht pDoc noch nicht einmal auf ein Dokument zu verweisen. Der Zeiger könnte auf ein beliebiges, von CCmdTarget abgeleitetes Objekt verweisen. Letztlich ist OnCmdMsg die Funktion, die CWnd zur Umwandlung der WM_COMMAND-Nachricht (für Fenster) in CN_COMMAND (für Befehlsempfänger) einsetzt.
Mit diesem Hintergrundwissen können Sie nun Ihr Problem lösen. Wenn Sie den Zeitgeber im Hauptrahmenfenster (CMainFrame) unterbringen, können Sie eine entsprechende Funktion implementieren, die alle WM_TIMER-Nachrichten folgendermaßen an die Dokumente übermittelt:

CMainFrame::OnTimer(...) 
{ 
   while (pDoc = /* jedes Dokument */) { 
      pDoc->OnCmdMsg(ID_APPTIMER,  
         CN_COMMAND, NULL, NULL); 
   } 
}

Wie erreicht man nun alle Dokumente? Mit einer passenden Programmschleife. Listing L1 zeigt den Code. Da ich mir solche Dinge nie merken kann und auch ständig vergesse, wie eigentlich die POSITIONs der MFC funktionieren, habe ich schon längst eine kleine Klasse geschrieben, mit der sich die Dokumente aufzählen lassen. Sie heißt - wie sollte es anders sein - CDocIterator. Wie CDocIterator funktioniert, kann ich mir wesentlich besser merken:

for (CDocIterator it; it.doc(); it++) { 
   it.doc()->DoSomething(); 
}

Listing L2 zeigt meine CDocIterator-Klasse. Der Konstruktor initialisiert eine CPtrList mit allen Dokumenten, wobei er sich auf den Code aus Listing L1 stützt. Die anderen Funktionen navigieren in der Liste. Außerdem habe ich wieder ein kleines Programm namens DocTimer1 geschrieben, das die einzelnen Bausteine zusammensetzt. CMainFrame::OnCreate legt einen Sekundenzeitgeber an und CMainFrame::OnTimer schickt allen Dokumenten mit Hilfe von CDocIterator den entsprechenden Befehl ID_APPTIMER.

for (CDocIterator it; it.doc(); it++) { 
   it.doc()->OnCmdMsg(ID_APPTIMER,  
      CN_COMMAND, NULL, NULL); 
}

Die Dokumente kümmern sich mit ON_COMMAND in der üblichen Weise um den Befehl ID_APPTIMER. Die _EX-Version wird hier nicht gebraucht. CMainFrame::OnTimer ignoriert den Ergebniswert von OnCmdMsg und leitet den Befehl stur an jedes einzelne Dokument weiter, und zwar unabhängig davon, ob er bereits bearbeitet wurde oder nicht. Der Handler CMyDoc::OnAppTimer inkrementiert einen Zähler und aktualisiert den View. In der Praxis würde man hier alle Arbeiten erledigen, die in diesem Zusammenhang anfallen.

Bild01

B1 DocTimer1.

L3 Implementierung eines Zeitgebers.

/////////////////////////////////////////////////////// 
// System Journal, Januar/Februar 2002 
// Autor: Paul DiLascia. 
// 
// Lässt sich mit Visual C++ 6.0 kompilieren und läuft  
// unter Windows 98 (und wohl auch unter Windows 2000). 
// Setzen Sie in Ihrem Editor die Tabulatorweite auf 3. 
// 
#include "stdafx.h" 
#include "doctime.h" 
#include "Doc.h" 
#include "DocIter.h" 
#ifdef _DEBUG 
#define new DEBUG_NEW 
#undef THIS_FILE 
static char THIS_FILE[] = __FILE__; 
#endif 
IMPLEMENT_DYNCREATE(CMyDoc, CDocument) 
BEGIN_MESSAGE_MAP(CMyDoc, CDocument) 
   ON_COMMAND_EX(ID_APPTIMER, OnAppTimer) 
END_MESSAGE_MAP() 
int CMyDoc::g_nIDTimer = 0; 
int CMyDoc::g_nDocObj = 0; 
// Die Zeitgeberprozedur wird bei jedem Ticken der Uhr 
// aufgerufen. 
// 
void WINAPI MyTimerProc(HWND hwnd, UINT uMsg, UINT_PTR idEvent,  
                        DWORD dwTime) 
{ 
   CDocIterator it; 
   for (it.First(); it.doc(); it.Next()) { 
      it.doc()->OnCmdMsg(ID_APPTIMER, CN_COMMAND, NULL, NULL); 
   } 
} 
// Der Konstruktor legt den Zeitgeber an, wenn das 
// erste Dokument angelegt wird. 
// 
CMyDoc::CMyDoc() 
{ 
   m_count = 0; 
   if (g_nDocObj++ == 0) { 
      g_nIDTimer = SetTimer(NULL,0,1000,MyTimerProc); 
   } 
} 
// Der Destruktor entsorgt den Zeitgeber, sobald 
// das letzte Dokument verschwindet. 
// 
CMyDoc::~CMyDoc() 
{ 
   if ( - g_nDocObj == 0) { 
      KillTimer(NULL,g_nIDTimer); 
   } 
} 
// Werte die Meldung vom Zeitgeber aus. Nun ist es an 
// der Zeit, die Hintergrunddaten einzusammeln oder zu 
// tun, was auch immer getan werden muss. Dieses Beispiel 
// inkrementiert einfach einen Zähler und aktualisiert  
// die Darstellungen. 
// 
BOOL CMyDoc::OnAppTimer(UINT nID) 
{ 
   m_count++; 
   UpdateAllViews(NULL); 
   return FALSE; 
}

Bild B1 zeigt DocTimer1 mit einer Reihe von offenen Dokumenten. Wenn Sie das Programm starten, dessen Quelltext Sie auf der Begleit-CD dieses Hefts finden, können Sie sich selbst davon überzeugen, dass die Dokumente im Sekundenabstand aktualisiert werden.
Falls es Ihnen wirklich nicht gefällt, dass das Hauptrahmenfenster die Dokumente aufruft und sich um den Zeitgeber kümmert, können Sie einen anderen Lösungsweg beschreiten. Er bietet die vermutlich sauberste Implementierung dieser Art von Zeitgeber. Sie können nämlich eine Zeitgeberprozedur einsetzen, statt die übliche WM_TIMER-Nachricht zu verwenden. Wenn Sie einen Zeitgeber anlegen, geben Sie normalerweise auch eine HWND (oder implizit CWnd für CWnd::SetTimer) an. Somit weiß der Zeitgeber, welchem Fenster er die WM_TIMER-Nachrichten schicken soll. Sie können aber auch NULL als HWND einsetzen und statt dessen eine Prozedur angeben, die Windows aufrufen soll.

void WINAPI MyTimerProc(HWND hwnd,  
  UINT uMsg, UINT_PTR idEvent, DWORD dwTime) 
{ 
   ... 
}

Wenn Sie nun also mit

SetTimer(NULL, 0, 1000, MyTimerProc);

eine Zeitgeberprozedur vorgeben, wird Windows direkt die Zeitgeberprozedur aufrufen, statt eine WM_TIMER-Nachricht zu schicken. Allerdings brauchen Sie immer noch ein Hauptfenster oder zumindest eine Get/DispatchMessage-Schleife, damit Windows die Gelegenheit erhält, den Zeitgeber zu überprüfen. Dies erfolgt nämlich in der Nachrichtenschleife. Mit Hilfe der Zeitgeberprozedur können Sie CMainFrame aus dem Bild verschwinden lassen. Sie könnten den Zeitgeber sogar vollständig innerhalb der Dokumentenklasse implementieren. Dazu brauchen Sie nur eine Zeitgeberprozedur und ein paar statische Variablen. Listing L3 zeigt eine Dokumentklasse, die ihren eigenen Zeitgeber implementiert und die Dokumente mit Hilfe der bereits beschriebenen Klasse CDocIterator aufzählt. Wenn Sie alles in der Dokumentklasse unterbringen, könnte die Zeitgeberprozedur direkt CMyDoc::DoSomething aufrufen, statt jedes Ticken des Zeitgebers in eine Befehlsnachricht umzuwandeln.
Es gibt noch eine Variante der Zeitgeberprozedurlösung, die mir von allen Lösungen am besten gefällt. Man kann sich den Zeitgeber als einen globalen Dienst vorstellen, auf den jedes Objekt zurückgreifen kann. Man sollte also eine Art Zeitgeberobjekt implementieren können, das man einfach in die Anwendung einbaut. Andere Objekte können dann "zuhören" oder "den Zeitdurchsagen lauschen". Wie könnte man solch einen nützlichen Diener implementieren?
Es ist sehr einfach. Sie brauchen nur die bisher skizzierte dokumentzentrische Lösung in zwei Punkten zu ändern. Erstens werden die Zeitgeberprozedur und die SetTimer / KillTimer-Aufrufe in einer separaten CAppTimer-Klasse untergebracht, die für die Verwaltung des Zeitgebers zuständig ist. Zweitens wird der Befehl ID_APPTIMER nicht an alle Dokumente verschickt, sondern an eine mehr oder weniger beliebige Liste von Befehlsempfängern. Dann brauchen Sie nur noch einen Mechanismus, mit dem sich die Objekte selbst in diese Liste eintragen und auch wieder aus der Liste streichen können.

L4 CCmdTargetList

CmdTargList.h 
/////////////////////////////////////////////////////// 
// System Journal, Januar/Februar 2002 
// Autor: Paul DiLascia. 
// 
// Lässt sich mit Visual C++ 6.0 kompilieren und läuft  
// unter Windows 98 (und wohl auch unter Windows 2000). 
// Setzen Sie in Ihrem Editor die Tabulatorweite auf 3. 
// 
#pragma once 
// Liste der Befehlsempfänger. Die Empfänger können sich 
// eintragen und wieder streichen. Mit SendCommand lässt 
// sich ein Befehl (CN/WM_COMMAND) an alle Empfänger 
// übermitteln, die sich in die Liste eingetragen haben. 
// Dies ist eine allgemeine Klasse, die per se nichts mit 
// Zeitgebern zu tun hat. 
// 
class CCmdTargetList { 
protected: 
   CPtrList m_list;                     // die Liste 
public: 
   // Trage einen Befehlsempfänger in die Liste ein. 
   // 
   void Register(CCmdTarget* pTarg) { 
      m_list.AddHead(pTarg);      
   } 
   // Streiche einen Befehlsempfänger aus der Liste. 
   // 
   void Unregister(CCmdTarget* pTarg) { 
      POSITION pos = m_list.Find(pTarg); 
      if (pos) { 
         m_list.RemoveAt(pos); 
      } 
   } 
   // Übermittle einen Befehl an alle Empfänger aus 
   // der Liste. 
   // 
   int SendCommand(UINT nID); 
}; 
CmdTargList.cpp 
/////////////////////////////////////////////////////// 
// System Journal, Januar/Februar 2002 
// Autor: Paul DiLascia. 
// 
// Lässt sich mit Visual C++ 6.0 kompilieren und läuft  
// unter Windows 98 (und wohl auch unter Windows 2000). 
// Setzen Sie in Ihrem Editor die Tabulatorweite auf 3. 
// 
#include "stdafx.h" 
#include "doctime.h" 
#include "CmdTargList.h" 
#include "AppTimer.h" 
#ifdef _DEBUG 
#define new DEBUG_NEW 
#undef THIS_FILE 
static char THIS_FILE[] = __FILE__; 
#endif 
// Übermittle einen Befehl an alle Empfänger aus der 
// Liste. 
// 
int CCmdTargetList::SendCommand(UINT nID) 
{ 
   int count=0; 
   POSITION pos = m_list.GetHeadPosition(); 
   while (pos) { 
      CCmdTarget* pTarg = (CCmdTarget*)m_list.GetNext(pos); 
      if (pTarg) { 
         pTarg->OnCmdMsg(nID, CN_COMMAND, NULL, NULL); 
         count++; 
      } 
   } 
   return count; 
}

DocTime2 implementiert diesen Lösungsansatz. DocTime2 setzt zwei neue Klassen ein, nämlich CCmdTargetList (Listing L4) und CAppTimer (Listing L5). CCmdTargetList ist eine generische Liste mit Befehlsempfängern. Sie bietet die entsprechenden Funktionen an, mit denen sich ein Objekt in die Liste eintragen und auch wieder aus der Liste streichen kann. Außerdem hat sie eine SendCommand-Funktion, die den Befehl in Form der Befehls-ID an jedes Objekt in der Liste übermittelt. CAppTimer leitet sich von CCmdTargetList ab, da der Zeitgeber unter anderem eine Liste mit Befehlsempfängern enthält. CAppTimer ist für den Zeitgeber zuständig und liegt als einzelne globale Instanz namens theTimer vor. Zur Initialisierung des Zeitgebers muss die Anwendung dessen Init-Funktion aufrufen.

// aus CMyApp::InitInstance 
theTimer.Init(1000, ID_APPTIMER);

Auf diese Weise wird CAppTimer angewiesen, einen Zeitgeber mit einer Periode von 1000 Millisekunden (1 Sekunde) anzulegen und ID_APPTIMER als Befehlskennung zu verwenden. Jedes Dokument, das vom Zeitgeber informiert werden möchte, trägt sich einfach in den Zeitgeber ein, sobald es angelegt wird. Im Zuge seiner eigenen Entsorgung streicht sich das Dokument wieder aus der Liste.

CMyDoc::CMyDoc() 
{ 
   theTimer.Register(this); 
} 
CMyDoc::~CMyDoc() 
{ 
   theTimer.Unregister(this); 
}

Was könnte einfacher sein? Das Schöne an dieser Lösung ist, dass der Zeitgeber in einer einzigen Klasse gekapselt wird. Sie brauchen diese Klasse nur noch in Ihr Projekt aufzunehmen und Init aufzurufen. Anschließend kann jedes Objekt selbst entscheiden, ob es auf die tickende Uhr hören möchte oder nicht. Dokumente, Views, Rahmenfenster - jeder Befehlsempfänger - kann sich bei der Uhr eintragen und dem Ticken über den normalen ON_COMMAND-Mechanismus lauschen. Auch in diesem Fall gibt es keinen Bedarf für ON_COMMAND_EX, da CCmdTargetList::SendCommand die Ergebnisse von OnCmdMsg ignoriert. Das ist auch richtig so, denn kein Objekt soll in der Lage sein, anderen Objekten die Nachricht vorenthalten zu können. Was auch immer geschieht, die Uhr tickt weiter.

L5 CAppTimer

AppTimer.h 
/////////////////////////////////////////////////////// 
// System Journal, Januar/Februar 2002 
// Autor: Paul DiLascia. 
// 
// Lässt sich mit Visual C++ 6.0 kompilieren und läuft  
// unter Windows 98 (und wohl auch unter Windows 2000). 
// Setzen Sie in Ihrem Editor die Tabulatorweite auf 3. 
// 
#include "CmdTargList.h" 
////////////////// 
// Das Zeitgeberobjekt stellt eine Liste mit Befehls- 
// empfängern dar, die in regelmäßigen Abständen einen 
// bestimmten Befehl erhalten. 
// 
class CAppTimer : public CCmdTargetList { 
protected: 
   static void WINAPI CAppTimer::TimerProc(HWND, UINT, UINT_PTR, DWORD); 
   UINT m_nIDTimer;        // Zeitgeber-ID 
   UINT m_nIDTimerCmd;     // Befehls-ID 
public: 
   CAppTimer(); 
   ~CAppTimer(); 
   void Init(int msec, UINT nIDCmd); 
}; 
// THE timer 
extern CAppTimer theTimer; 
AppTimer.cpp 
/////////////////////////////////////////////////////// 
// System Journal, Januar/Februar 2002 
// Autor: Paul DiLascia. 
// 
// Lässt sich mit Visual C++ 6.0 kompilieren und läuft  
// unter Windows 98 (und wohl auch unter Windows 2000). 
// Setzen Sie in Ihrem Editor die Tabulatorweite auf 3. 
// 
#include "stdafx.h" 
#include "resource.h" 
#include "AppTimer.h" 
#ifdef _DEBUG 
#define new DEBUG_NEW 
#undef THIS_FILE 
static char THIS_FILE[] = __FILE__; 
#endif 
// globales Zeitgeberobjekt 
CAppTimer theTimer; 
void WINAPI CAppTimer::TimerProc(HWND hwnd, 
   UINT uMsg, UINT_PTR idEvent, DWORD dwTime) 
{ 
   theTimer.SendCommand(theTimer.m_nIDTimerCmd); 
} 
CAppTimer::CAppTimer() 
{ 
   ASSERT(this==&theTimer); // Es ist nur ein globales  
                            // Zeitgeberobjekt zulässig. 
} 
CAppTimer::~CAppTimer() 
{ 
   KillTimer(NULL, m_nIDTimer); 
} 
// Initialisieren - setze Zeitgeber und lege die  
// Befehls-ID fest. 
// 
void CAppTimer::Init(int msec, UINT nIDCmd) 
{ 
   ASSERT(this==&theTimer); 
   m_nIDTimerCmd = nIDCmd; 
   m_nIDTimer = SetTimer(NULL, 0, msec, TimerProc); 
}