| 本文范例代码:Doctime1.zip Doctime2.zip |
| 问题: |
| 我编了一个类似Windows 2000 上性能监视器的程序。在这个程序中,我创建了一个文档对象和几个视图。文档对象负责有规律地采集数据,然后通知相关的视图以不同的格式显示这些数据。文档对象为了有规律地采集数据,它需要一个定时(time)事件。但是,文档不是一个基于窗口的对象,所以它没有这样的定时事件。我权衡了三种解决方案: |
- 在其中的一个视图中创建定时器。当事件发生时通知文档对象获取数据。
- 为每一个文档(或在每个文档内)创建一个单独的线程,以便这个线程能有规律地创建时间事件。
- 在CMainFrame中创建一个定时器并从中调用文档函数
|
| 这三种方法我都不满意。能否给出一种更好的解决方案? |
| 解答: |
在你列出的解决方案中,将定时器放入视图是最糟的想法,因为那样的话你就得为每个视图建一个定时器,你应该将定时器视为一种相对有限的资源(这一点在过去尤其如此,现在这个问题考虑得较少)。创建单独的线程对于定时器这样简单的东西又有牛刀弑鸡之嫌。线程无疑使事情复杂化。那么还有第三个方案:在主框架中创建定时器并从中调用文档函数。我会告诉你如何用这个方案直截了当地实现所要的功能,然后我还会展示另外一种你没有想到的方法。 假设你不愿意用第三种方案的原因是它需要从主框架中调用文档,这样做有点丑陋。(为什么框架要掺乎到文档中去呢?)但有一种直截了当的方法来做,不用直接调用CMyDoc::DoTimerThing,你可以将WM_TIMER消息转换成一个ID为ID_APPTIMER的WM_COMMAND,并用通常的方式广播这个命令以便文档能用ON_COMMAND处理它,文档无法处理所有的窗口消息,但它们可以处理WM_COMMAND。事实上,这是MFC命令处理例程体系结构的主要创新之一,它使非窗口对象可以处理命令。所以这样看来,你要做的事情就是: |
//
CMainFrame::OnTimer(...)
{
SendMessage(WM_COMMAND, ID_APPTIMER);
}
// |
也就是说,当主框架得到定时器信号的同时,也向自己发送了一个ID_APPTIMER命令。MFC会将这个命令发送到系统,任何具有ON_COMMAND处理器并能处理ID_APPTIMER命令的对象都可以处理这个事件。你可以用ON_COMMAND_EX来对付多个对象处理相同事件的情况。 这样做虽然能行得通,但有一个问题。MFC只把命令发送到活动视图/文档。如果其它文档处于打开状态,但没有被激活,则它们不会得到WM_COMMAND消息。虽然你可以修改程序把命令广播到非激活文档,但那样的话,像“文件|保存”这样普通的命令会被发送到所有的文档——很狼狈!因为我们只需要定时器命令到达所有文档。怎么办呢?如何发送WM_COMMAND到所有的文档? MFC将命令发送到文档这样的非窗口对象,其方法是通过虚函数CCmdTarget::OnCmdMsg来实现的。当窗口获得WM_COMMAND消息时,它要运行许多CWnd代码和虚函数。最终,控制到达CWnd::OnCommand,由它调用OnCmdMsg。 |
// in CWnd::OnCommand
OnCmdMsg(nID, CN_COMMAND, NULL, NULL);
// |
这里,nID是命令ID,编码CN_COMMAND告诉OnCmdMsg这是个命令事件——与更新UI事件相对(此时编码应该是CN_UPDATE_COMMAND_UI)。其它参数对于CN_COMMAND没用。 因此,如果你有一个文档指针,并想要发送一个命令到该文档,那么你要做的全部工作就是调用: |
//
pDoc->OnCmdMsg(nID, CN_COMMAND, NULL, NULL);
// |
这个方法是完全通用的,除了文档之外,不论是谁这样调用都不用知道关于文档的任何信息。实际上,pDoc根本就不需要某个文档;它可以指向任何CCmdTarget派生对象。从效果上讲,OnCmdMsg是CWnd用于将WM_COMMAND(对窗口而言)转换为CN_COMMAND(对命令对象而言)的一个函数。 有了这些知识,现在你该可以解决问题了。如果你把定时器放入主框架(CMainFrame),你可以写一个处理器传递WM_TIMER事件到所有文档,就象下面这样: |
//
CMainFrame::OnTimer(...)
{
while (pDoc = /* each document */) {
pDoc->OnCmdMsg(ID_APPTIMER,
CN_COMMAND, NULL, NULL);
}
}
// |
| 如何循环多有文档呢?参见下面样板代码: |
//loop.cpp
//////////////////
// Standard way to loop over all documents:
//
// for (all doc templates) {
// for (all docs in 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(....);
}
}
// |
| MFC中POSITIONs的工作模式 很奇特,我以前写了一个枚举文档的小类——名字叫CDocIterator。使用它可以列举所有文档。 |
// for (CDocIterator it; it.doc(); it++) { it.doc()->DoSomething(); } // |
| 下面是CDocIterator类的代码。构造器用CPtrList初始化所有文档,使用上面那个样板代码(loop.cpp);其它函数导航此列表。 |
//CDocIterator 类代码
//
DocIter.h
//
class CDocIterator {
protected:
CPtrList m_doclist;
CDocument* m_pDoc;
POSITION m_pos;
public:
CDocIterator();
// get current doc
CDocument* doc() {
return m_pDoc;
}
// move to 1st doc: not needed after construct
BOOL First() {
m_pDoc = NULL;
m_pos = m_doclist.GetHeadPosition();
return Next();
}
// move to next doc
BOOL Next() {
if (m_pos) {
m_pDoc = (CDocument*)m_doclist.GetNext(m_pos);
} else {
m_pDoc=NULL;
}
return m_pDoc != NULL;
}
// next, ++ style
const CDocIterator& operator++(int) {
Next();
return *this;
}
// for "for" loops
BOOL operator()() {
return m_pDoc != NULL;
}
};
DocIter.cpp
//
#include "stdafx.h"
#include "DocIter.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
// Constructor intitializes document list and goes to first doc.
//
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();
}
// |
| 与以往一样,我编了一个小程序DocTimer1,上面所讲的内容和方法都用在了这个程序中。 CMainFrame::OnCreate建立一个二分之一秒的定时器,并且CMainFrame::OnTimer使用CDocIterator将ID_APPTIMER命令发送到所有文档。 |
//
for (CDocIterator it; it.doc(); it++) {
it.doc()->OnCmdMsg(ID_APPTIMER,
CN_COMMAND, NULL, NULL);
}
// |
文档用ON_COMMAND以通常的方式处理ID_APPTIMER。注意这里不需要_EX版本,因为CMainFrame::OnTimer忽略OnCmdMsg的返回码,并将命令传递到每一个文档,不管它是否被处理。CMyDoc::OnAppTimer事件处理器增加计数并更新视图。在实际编程中,你应该做你该做的事情。 图一是运行中的DocTimer1,它打开了一批文档。所有文档每妙更新一次自己的视图——自己下载代码编译然后运行一下吧。 |
 |
| 图一 运行中的DocTimer1 |
| 如果你实在不想在主框架中调用文档和操纵定时器,那么我还有个方法可以参考,这也许是最利索的方法了,即你可以使用一个定时器过程(proc)来代替WM_TIMER。通常在设置某个定时器时都提供HWND(或者暗示CWnd::SetTimer的CWnd)标示应接收WM_TIMER消息的窗口。但你可以传递值为NULL的HWND从而代替提供一个Windows要调用的过程。 |
//
void WINAPI MyTimerProc(HWND hwnd,
UINT uMsg, UINT_PTR idEvent, DWORD dwTime)
{
......
}
// |
| 如果你调用 |
//
SetTimer(NULL, 0, 1000, MyTimerProc);
// |
| Windows将直接调用定时器过程来代替发送WM_TIMER消息。(你仍然需要一个主窗口,而且不管用什么方法,Get/DispatchMessage消息循环是必须有一个的——Windows就是在这个循环中检查定时器的)。你可以用定时器过程作用与整个CMainFrame。你甚至只用一个定时器和几个静态全局变量就可以在自己的文档类中实现自己完整的文档定时器特性。下面列出的就是一个文档类,它用前面提到的CDocIterator遍历所有文档从而实现了自己的定时器。 |
//Implementing a Timer
////////////////////////////////////////////////////////////////
//
#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;
// timer proc called by windows whenever the timer clicks
//
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);
}
}
// constructor creates timer the first time a doc is created
//
CMyDoc::CMyDoc()
{
m_count = 0;
if (g_nDocObj++ == 0) {
g_nIDTimer = SetTimer(NULL,0,1000,MyTimerProc);
}
}
// destructor kills timer when the last doc goes bye-bye
//
CMyDoc::~CMyDoc()
{
if (—g_nDocObj == 0) {
KillTimer(NULL,g_nIDTimer);
}
}
// Handle timer event: this is where you would do your background data
// collection or whatever you want to do. This sample simply increments a
// counter and updates the views.
//
BOOL CMyDoc::OnAppTimer(UINT nID)
{
m_count++;
UpdateAllViews(NULL);
return FALSE;
}
// |
当然,如果你在自己的文档类中做所有的事情,定时器过程可直接调用CMyDoc::DoSomething来代替将定时器事件转换为命令事件。 使用定时器过程是稍微复杂了一些,但这是我喜欢的方法。从概念上讲,定时器是一个任何对象都能使用的全局服务。你应该能实现一些种类的定时器对象,以便可以随时信手摘来把它用到自己的程序中去,然后不同的对象能“侦听”它。那么如何实现这个小东东呢? 很容易。你要做的全部工作就是改变前面所述的两种方法中以文档为中心的做法。首先,将定时器过程和SetTimer/KillTimer调用放入一个单独的CAppTimer类中,将它广播到任意的命令目标列表。你只需要在这个列表中添加和删除对象。 DocTime2就是用这种方法实现的。它使用两个新类:CCmdTargetList 和CAppTimer。前者是一个通用的命令目标列表类,其对象可以调用这个类中的函数将自己在列表中注册和注销(添加和删除),SendCommand函数用于将某个命令ID广播到每一个列表对象。CAppTimer从CCmdTargetList派生,因为别的东西相比,此定时器类是个命令目标列表。CAppTimer操作着这个定时器并提供一个单一全局实例——theTimer,它有一个应用必须调用以设置定时器的Init函数。 |
// from CMyApp::InitInstance
theTimer.Init(1000, ID_APPTIMER);
// |
| 此代码告诉CAppTimer创建一个1000毫秒(1秒)的定时器并用ID_APPTIMER作为命令ID。要接收定时器通知的每个文档只要在文档被构造时自己注册定时器,当文档被销毁时注销定时器即可。 |
//
CMyDoc::CMyDoc()
{
theTimer.Register(this);
}
CMyDoc::~CMyDoc()
{
theTimer.Unregister(this);
}
// |
| 还有什么比这样做更简单?这个方法最棒的地方是它将定时器封装在一个单独的类中。你只需要在用它的时候信手摘来,调用Init,然后让每个对象决定自己是否在乎侦听时钟。文档、视图、框架——任何命令目标——都可以注册定时器过程,然后通过常规的ON_COMMAND机制接收通知。重申一下,这里不必使用ON_COMMAND_EX,因为CCmdTargetList::SendCommand忽略OnCmdMsg的返回码。这就是说:没有哪个对象能防碍其它对象接收通知。无论什么都阻止不了。 |
CCmdTargetList
CmdTargList.h
////////////////////////////////////////////////////////////////
//
#pragma once
// List of command targets. Targets can register and unregister, and
// SendCommand lets you send a command (CN/WM_COMMAND) to all the targets
// on the list. This is a general class that has nothing per se to do
// with timers.
//
class CCmdTargetList {
protected:
CPtrList m_list; // the list
public:
// Register command target to receive commands sent to this list
//
void Register(CCmdTarget* pTarg) {
m_list.AddHead(pTarg);
}
// Unregister command target from list
//
void Unregister(CCmdTarget* pTarg) {
POSITION pos = m_list.Find(pTarg);
if (pos) {
m_list.RemoveAt(pos);
}
}
// Send command to all command targets on this list
//
int SendCommand(UINT nID);
};
CmdTargList.cpp
////////////////////////////////////////////////////////////////
//
#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
// Send a command to all command targets on this list
//
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;
} |
|
CAppTimer
AppTimer.h
////////////////////////////////////////////////////////////////
//
#include "CmdTargList.h"
//////////////////
// Timer object is a list of cmd targets to receive timer command/event.
//
class CAppTimer : public CCmdTargetList {
protected:
static void WINAPI CAppTimer::TimerProc(HWND, UINT, UINT_PTR, DWORD);
UINT m_nIDTimer; // timer ID
UINT m_nIDTimerCmd; // command ID to send when timer clicks
public:
CAppTimer();
~CAppTimer();
void Init(int msec, UINT nIDCmd);
};
// THE timer
extern CAppTimer theTimer;
AppTimer.cpp
////////////////////////////////////////////////////////////////
//
#include "stdafx.h"
#include "resource.h"
#include "AppTimer.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
// global timer object
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); // only one global timer allowed
}
CAppTimer::~CAppTimer()
{
KillTimer(NULL, m_nIDTimer);
}
// Initialize: set timer and command ID to use
//
void CAppTimer::Init(int msec, UINT nIDCmd)
{
ASSERT(this==&theTimer);
m_nIDTimerCmd = nIDCmd;
m_nIDTimer = SetTimer(NULL, 0, msec, TimerProc);
} |