源码下载:ActiveX-Clock-OCX
参照孙鑫的<<VC++深入详解>>中第18章自定义ActiveX中的Clock例子(到18.3节之前),完成了OCX控件的制作,而且也编译(Debug模式)、注册成功了!于是又创建了一个MFC基于对话框的测试程序,在对话框中放入了这个Clock控件,界面如下:
接下来右击Clock控件,选择“属性”,切换到“设置时间间隔面板”,更改时间后,切换到其他一个属性页,这时就出现assert宏异常了,看图:
真是奇怪,孙鑫的教程里也没提到有这个问题。后来试了下在Release模式下编译成功的OCX,发现在切换属性页时却没有这样的问题,一切都是正常的。
看来是优化的问题吧,详情未知,猜测而已!
看到第18章的总结(Page708)才发现,原来孙鑫老师还是提到了这个问题的。
他说出现这种错误的原因是:当将Clock控件放到VB的Form上时,该控件的窗口已经创建,也就是说,CClockCtrl类的OnCreate()方法被执行了,这样就设置了定时器。而在VC的对话框上插入Clock控件时,却没有调用CClockCtrl类的OnCreate()方法,当修改Interval属性时,会调用CClockCtrl类的OnIntervalChanged()方法,在这个方法中,调用了KillTimer(1),因为定时器根本没有创建,因此就出现了非法操作。解决办法是:用一个变量保存定时器的返回值,然后在OnIntervalChanged()方法中对返回值进行判断。
于是我将代码改成下面的样子:
void CClockCtrl::OnIntervalChanged()
{
if(m_nInterval < 0 || m_nInterval > 6000)
m_nInterval = 1000;
else
m_nInterval = m_nInterval / 1000 * 1000;
//if(timer_flag != 0)
{
MessageBox("OnIntervalChanged: going to do KillTimer()");
//KillTimer(1);
timer_flag = 0;
}
MessageBox("OnIntervalChanged: going to do SetTimer()");
//timer_flag = SetTimer(1, m_nInterval, NULL);
SetTimer(1, m_nInterval, NULL);
char message[100] = {0};
sprintf(message, "timer_flag = %d", timer_flag);
MessageBox(message);
SetModifiedFlag();
}
再经过测试,发现在切换属性页时,弹出窗口输出了“OnIntervalChanged: going to do SetTimer()”后就出现了ASSERT()宏异常,可见这个异常是出现在SetTimer()内部的。我们都知道,ASSERT()宏只有在Debug模式下才会起作用,在Release下是不会起作用的,这就是为什么使用Release时生成的ocx时不会弹出ASSERT()宏异常窗口的原因了。可是为什么SetTimer()会失败呢???
先来看下Plateform SDK中的SetTimer()原型吧:
UINT_PTR SetTimer(
HWND hWnd, // handle to window
UINT_PTR nIDEvent, // timer identifier
UINT uElapse, // time-out value
TIMERPROC lpTimerFunc // timer procedure
);
看到第一个参数hWnd了吗,这是与窗口的句柄相关联的,但是孙鑫老师也说了“将Clock控件放到VB的Form上时,该控件的窗口已经创建”,但是“在VC的对话框上插入Clock控件时,却没有调用CClockCtrl类的OnCreate()方法 ”,这里的关键不是指“OnCreate()中的SetTimer()”,而是指“窗口没有创建”,所以“窗口对应的句柄又将是多少”呢?正是因为控件窗口没有创建,所以“CClockCtrl::OnIntervalChanged() ”中的“SetTimer()”和“KillTimer()”都将会失败,而且失败的主要原因是在其函数内部对“窗口句柄”的ASSERT()判断。因此,我认为孙鑫老师说的“解决办法”是行不通的,除非不调用KillTimer()和SetTimer(),但是这样的话,就达不到控制多少秒触发一次OnDraw()的效果了!
经过调试,终于在“D:\Program Files\Microsoft Visual Studio\VC98\MFC\Include\AFXWIN2.INL文件中的第166-171行”找到了SetTimer()和KillTimer()的具体实现:
_AFXWIN_INLINE UINT CWnd::SetTimer(UINT nIDEvent, UINT nElapse,
void (CALLBACK* lpfnTimer)(HWND, UINT, UINT, DWORD))
{ ASSERT(::IsWindow(m_hWnd)); return ::SetTimer(m_hWnd, nIDEvent, nElapse,
(TIMERPROC)lpfnTimer); }
_AFXWIN_INLINE BOOL CWnd::KillTimer(int nIDEvent)
{ ASSERT(::IsWindow(m_hWnd)); return ::KillTimer(m_hWnd, nIDEvent); }
因此,我认为解决的办法有3种:
1. 使用“Release方式生成的OCX”
2. 越过KillTimer()和SetTimer()中“ASSERT(::IsWindow(m_hWnd)); ”,即将CClockCtrl::OnIntervalChanged() 中的内容修改如下:
void CClockCtrl::OnIntervalChanged()
{
if(m_nInterval < 0 || m_nInterval > 6000)
m_nInterval = 1000;
else
m_nInterval = m_nInterval / 1000 * 1000;
//KillTimer(1); // 为了越过KillTimer()中的ASSERT(::IsWindow(m_hWnd));
::KillTimer(m_hWnd, 1);
::SetTimer(m_hWnd, 1, m_nInterval, NULL);
//SetTimer(1, m_nInterval, NULL);
SetModifiedFlag();
}
这时,在切换属性页时虽然也会执行::KillTimer和::SetTimer(),而且其中的m_hWnd可能为一个非法的值,但是起码不会弹出ASSERT()宏异常窗口,大不了就是这两个函数调用失败而已,所以也解决了这个问题。
3. 判断控件当前状态是否为运行状态,如果是才调用SetTimer()和KillTimer(),即修改CClockCtrl::OnIntervalChanged()的内容如下:
void CClockCtrl::OnIntervalChanged()
{
if(m_nInterval < 0 || m_nInterval > 6000)
m_nInterval = 1000;
else
m_nInterval = m_nInterval / 1000 * 1000;
if(AmbientUserMode())
{
KillTimer(1); // 为了越过KillTimer()中的ASSERT(::IsWindow(m_hWnd));
//::KillTimer(m_hWnd, 1);
//::SetTimer(m_hWnd, 1, m_nInterval, NULL);
SetTimer(1, m_nInterval, NULL);
}
SetModifiedFlag();
}
如果各位有什么不同的看法,欢迎提出来探讨!