VC 多个定时器
SetTimer函数的原型:
UINT_PTR SetTimer( HWND hWnd, // 窗口句柄 UINT_PTR nIDEvent, // 定时器ID,多个定时器时,可以通过该ID判断是哪个定时器 UINT uElapse, // 时间间隔,单位为毫秒 TIMERPROC lpTimerFunc // 回调函数);
在MFC程序中SetTimer被封装在CWnd类中,调用就不用指定窗口句柄了,例如:
SetTimer(1,100,NULL); //1为ID号.100为时间
例如:
// 添加两个定时器
SetTimer(1,500,NULL);
SetTimer(2,1000,NULL);
//停止定时器
KillTimer(1);//停止ID为1的定时器
KillTimer(2);//停止ID为2的定时器
定时器:
void CTimerTestDlg::OnTimer(UINT nIDEvent)
{
switch (nIDEvent)
{
case 1: ///处理ID为1的定时器
...
break;
case 2: ///处理ID为2的定时器
...
break;
}
很多应用程序都需要使用定时器,以便定期检查状态,并重新绘图。学过VB的朋友知道,VB中的定时器是一个控件,我们只需要放置一个定时器控件到窗体之上,然后设置其属性,并在程序中处理该控件产生的定时事件就行了。不过在VC中并没有定时器控件,我们可以通过两种方法来使用定时器:一种是直接调用WIN32函数SetTimer(),另一种则是调用CWnd::SetTimer(),实际上这两种方法的本质是一样的,下面我们就来看看后者的使用方法。
l 设置和释放定时器
CWnd::SetTimer()的功能是给当前窗口设置一个定时器,该函数的原型为:
UINT SetTimer( UINT nIDEvent, UINT nElapse, void (CALLBACK EXPORT* lpfnTimer)(HWND, UINT, UINT, DWORD) );
由于一个窗口可以同时设置几个定时器,SetTimer()的第一个参数nIDEvent起标识不同定时器的作用,我们可以任意取值,只要不与其它定时器的标识重复就行了,以便将来能正确分辨出是哪个定时器发出的定时消息。nElapse表示每隔多少毫秒产生一次定时消息。lpfnTimer是一个函数指针,如果传递一个NULL指针,那么在产生定时中断时,Windows会把一条WM_TIMER消息放入到程序的消息队列之中,最终由CWnd对象相应的处理函数来处理该消息;如果传递了一个真正的函数指针,那么在产生定时中断时,Windows将直接调用该函数,由该函数来处理定时消息。这也就是说,我们可以使用两种方式来处理定时消息,它们有什么区别呢?
首先,WM_TIMER是一条低优先级的消息。如果消息队列之中还有其它高优先级的消息,那么WM_TIMER将被堵塞,直到最后才会被处理。其次,WM_TIMER被程序从消息队列中取回之后,还要经过一系列的过程才能被传递到它的处理函数。把这两点结合起来可以看出,在第一种方式下,定时消息有可能要拖延一段时间才会被处理,而第二种方式则不同,由于Windows将直接调用指定的函数,所以延迟时间要短得多。
由于Schedule对实时性要求并不苛刻,只要能精确到分钟左右就可以了,另外Schedule内部要进行的运算和处理也不多,所以心铃决定选用第一种方式,准备在视类中处理定时消息。在第十二讲给出的CScheduleView::OnInitialUpdate()的最后一行语句便是设置定时器的代码,现在我们可以把它前面的注释符号删掉了,这个定时器将使Schedule在每过20秒左右的时间得到一次重新检查各事件状态的机会。
定时器是一种有限的系统资源,如果多个程序都需要使用定时器,那么有可能会出现设置不成功的情况,因此我们应在调用SetTimer()函数时检查它的返回值,如果返回值是0,就表示系统中已经没有可用的定时器了。Schedule没有进行这步检查,心铃想请大家自己添上,并决定在出现这种情况时应该怎么办。
SetTimer()还有一个作用是它能够重新设置已分配的定时器的定时间隔,只要在nIDEvent中传递已分配的定时器的标识,那么该定时器的定时间隔就会改变成nElapse中指定的新值。
在程序退出时,我们应该将已分配的定时器释放掉。为此我们利用ClassWizard为视类添加一个处理WM_DESTROY消息的函数OnDestroy(),并编写以下代码:
void CScheduleView::OnDestroy() {
KillTimer(1);
CListCtrl & lst=(this->GetListCtrl());
CImageList *pImageList=lst.GetImageList(LVSIL_SMALL);
delete pImageList;
CListView::OnDestroy();
}
KillTimer()用于释放定时器,其参数应等于SetTimer()的nIDEvent。如果程序分配了多个定时器,那么应调用多次KillTimer()。OnDestroy()同时也把与CListView内部的List控件关联的图象列表释放了。
l 编写消息处理函数
使用定时器的第一种方式需要用到一个回调函数,该函数必须具有以下原型:
void CALLBACK EXPORT TimerProc( HWND hWnd, UINT nMsg, UINT nIDEvent,
DWORD dwTime );
由于我们不使用这种方式,所以TimerProc各参数的含义请大家自己查阅MSDN库,下面我们来看看如何为第二种方式编写消息处理函数。
首先我们利用ClassWizard为视类添加一个处理WM_TIMER消息的函数OnTimer()。它只有一个参数nIDEvent,检查这个值便可以知道WM_TIMER消息是由哪个定时器发出的。下面便是OnTimer()的实现代码:
void CScheduleView::OnTimer(UINT nIDEvent) {
CTime tiNow=CTime::GetCurrentTime();
CTimeSpan tdif(0,0,15,0);
CScheduleDoc * pDoc=GetDocument();
CString Message;
struct ScheduleItem* pUp,*pDown;
pUp=pDoc->m_pNearestTaskUp;
pDown=pDoc->m_pNearestTaskDown;
if(nIDEvent==1) {
if( ( (pUp!=NULL) && (pUp->ti<tiNow-tdif) ) ||
( (pDown!=NULL) && (pDown->ti<=tiNow+tdif) ) ) {
pDoc->SearchNearestTask();
//更新事件条目的状态
pDoc->UpdateState();
pDoc->UpdateAllViews(NULL,0,NULL);
//更新显示
if((pDown!=NULL) && ( pDown->ti <= (tiNow+tdif) )) {
AfxGetMainWnd()->ShowWindow(SW_NORMAL);
SetForegroundWindow(); //把窗口显示在前台
Message="请注意:\""; //准备提示信息
Message += pDown->des;
Message += "\" 的时间很快就要到了!";
KillTimer(1); //必须先暂时中断定时器
AfxMessageBox(Message);
SetTimer(1,20000,NULL); //重新设置定时器
}
}
}
//CListView::OnTimer(nIDEvent); 将ClassWizard生成的这行代码注释掉
}
图17-1:报警的消息框
在上面的代码中,我们首先检查处于时间边界的两个事件条目,判断是否需要改变它们的状态。如果是的话,就重新搜索处于时间边界的两个新事件条目,更新链表中各事件的状态,并更新显示。对于刚从等待报警状态(状态2)进入报警状态(状态1)的事件,我们有必要以某种方式通知用户,提醒他已经到了应做某某事情的时间。上面的代码首先将程序主窗口以正常方式显示,然后把窗口设置成前台,这两条语句组合起来便可以把Schedule显示在前台,即便它当时处于最小化状态,上述语句也能将它的窗口弹出来。窗口显示出来后,我们接着再显示一个消息框(见图17-1),其中给出了某某事情的时间已到了的提示信息。
消息框本质上是一个非常简单的由系统控制的对话框。上一讲给出的代码中也用到了它,我们使用的函数是MFC类库包装过的AfxMessageBox(),它比直接使用WIN32函数MessageBox()要稍微简单一些,但缺点是不能设置消息的标题条。除了上述两个函数外,CWnd类也有一个MessageBox()成员函数,其功能与WIN32函数类似,大家以后可以根据需要选择使用。
大家是否注意到了,上面的代码在显示消息框之前调用了一次KillTimer(),从消息框返回后又调用了一次SetTimer(),为什么要这样做呢?这个问题实际上涉及到了WM_TIMER消息的一些内在特点。
设置了定时器后,程序的窗口处理函数将会定期收到一条WM_TIMER消息,如果OnTimer()内部的处理需要花费很长的时间,就有可能在还未退出它之前,程序又收到一条WM_TIMER消息。此时会出现两种情况:一种是OnTimer()的内部调用了消息框或对话框,当前正在等待用户输入。在这种情况下,当前CPU的控制权实际上掌握在Windows手中,而不在程序手中,因此Windows将再次调用OnTimer()(称为“重入”,重复进入的意思),这样可能会造成不良后果。例如将再次显示出一个消息框或对话框,或者由于后一次调用改变了全局变量,从而对前一次调用产生影响。另一种情况是OnTimer()内部正在进行大量的处理,但没有调用任何可能把控制权交还给Windows的函数,那么第二个WM_TIMER消息将被保留在程序的消息队列之中。如果因为OnTimer()迟迟不能返回,最后导致第三个、第四个甚至更多的WM_TIMER消息也到来了,此时后面来的WM_TIMER消息将会冲掉前面的WM_TIMER消息,也即消息队列中只保留最后一条WM_TIMER消息,其余的都被丢弃了,这与WM_PAINT消息是类似的。
总之,上面两种情况均说明了OnTimer()或其它处理定时消息的函数不适合完成耗时很长的处理,如果不得已要进行这种处理,那么应考虑设立一些保护措施,以防止出现异常现象。
Schedule属于第一种情况,于是心铃选择了在显示消息框之前先暂时中断定时器,从消息框返回之后再重置定时器的方法。除此之外,我们也可以不暂停定时器,但必须设置一个初始值为假的重入标志,每次进入OnTimer()时首先检查该标志是否为真。如果是真(表示本次调用是重入)则马上退出函数,如果是假则把该标志置为真,然后进行各种处理,最后在退出函数之前又把该标志置为假。这种方法可以有效地避免因重入而带来不良的后果。
l 时间精度问题
现在我们来讨论一下定时器的精度问题。SetTimer()设置的定时器被称为系统定时器,该函数的参数nElapse是以毫秒为单位,但这并不意味着系统定时器的最小精度可达1毫秒。我们知道,PC机有一块时钟芯片连接在可编程中断控制器的IRQ0上,并以每秒钟18.2次的速度产生中断,相当于55毫秒一次,这便是Windows 9x(不同的操作系统和硬件平台可能会有不同的最小精度)的系统定时器的最小精度。也就是说,即使我们为nElapse设置了小于55的值,定时消息实际到达的间隔也在55毫秒之上。另一方面,由于Windows是一个多任务操作系统,应用程序遵循消息驱动模式,定时消息到达程序的消息队列之后可能要经过一段时间后才会被处理,这样就为两条定时消息之间的间隔加上了随机性,也即下一条定时消息被处理的时间是无法确定的。
Schedule对时间的精度并不敏感,因此我们使用系统时钟便可完全满足要求,然而系统时钟对那些实时性要求较高的应用程序就不够了。例如以每秒25帧速度播放动画或视频数据的多媒体程序,如果使用定时器,那么最低的精度要求都是40毫秒,系统时钟显然达不到这一精度。为此,Windows的多媒体子系统提供了另外一种定时器——多媒体定时器(Multimedia Timer),在多数系统上,它都能达到1毫秒的精度,基本上能满足各种多媒体应用的要求。不过需要注意的是,当定时消息发生间隔小于10毫秒时,系统性能就会受到比较明显的影响,因此我们在使用多媒体定时器时应把间隔定为能满足要求的最大值,而不能去追求最小值
CDialog::OnTimer(nIDEvent);
}