Overview
探讨Windows窗体程序执行周期性任务的几种方法,涉及定时器方法、WaitFor方法等。
故事
假设是一个对话框应用程序,用户在对话框上单击了Start之后,就启动周期性的一项任务。
为了简化问题,假定在整个过程中不关闭对话框。——如果要关闭,只需要发消息或直接函数调用中断任务即可。不过本文不讨论这些方面。
UI
UI可以简化为,只有一个Start按钮。这个对话框对应于class CPeriodTaskDlg。
IDD_PERIODTASK_DIALOG DIALOGEX 0, 0, 184, 59
STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU
EXSTYLE WS_EX_APPWINDOW
CAPTION "PeriodTask"
FONT 8, "MS Shell Dlg", 0, 0, 0x1
BEGIN
PUSHBUTTON "Start",IDC_START,25,17,117,20
END
任务执行体
假定由class CFoo来执行具体的周期性任务。
任务假定为每隔2s弹出一个对话框,5次后任务结束。
定时器方案
定时器是一种很好地周期性触发任务的机制。对应的SetTimer或KillTimer的第一个参数是HWND,如果CFoo不是一个窗体,那么可以借用CPeriodTaskDlg来完成。具体地,
- CFoo在开始执行任务的时候,用SetTimer创建定时器;窗体指向CPeriodTaskDlg;
- 窗体处理WM_TIMER消息,检测到是CFoo创建的定时器,则调用CFoo的执行周期性任务的函数。
下面给出示例代码。
CFoo
头文件:
#pragma once
class CFoo
{
public:
explicit CFoo(CWnd *pWnd);
~CFoo(void);
void Start();
void Doit();
enum {FOO_TIMER_ID = 100};
private:
int m_iCounter;
CWnd *m_pWnd;
};
实现文件:
#include "StdAfx.h"
#include "Foo.h"
CFoo::CFoo(CWnd *pWnd): m_iCounter(0), m_pWnd(pWnd)
{
}
CFoo::~CFoo(void)
{
}
void CFoo::Start()
{
Doit();
::SetTimer(m_pWnd->m_hWnd, FOO_TIMER_ID, 2000, NULL);
}
void CFoo::Doit()
{
if (m_iCounter < 5) {
::AfxMessageBox("Do something ...");
m_iCounter++;
return;
}
::AfxMessageBox("Something has been done. Kill the timer ...");
KillTimer(m_pWnd->m_hWnd, FOO_TIMER_ID);
}
CPeriodTaskDlg
省略掉部分自动生成的代码。
头文件:
#include "Foo.h"
class CPeriodTaskDlg : public CDialogEx
{
// ..........
afx_msg void OnBnClickedStart();
afx_msg void OnTimer(UINT_PTR nIDEvent);
CFoo m_foo;
};
实现文件:
CPeriodTaskDlg::CPeriodTaskDlg(CWnd* pParent /*=NULL*/)
: CDialogEx(CPeriodTaskDlg::IDD, pParent), m_foo(this)
{
m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
}
BEGIN_MESSAGE_MAP(CPeriodTaskDlg, CDialogEx)
ON_WM_SYSCOMMAND()
ON_WM_PAINT()
ON_WM_QUERYDRAGICON()
ON_BN_CLICKED(IDC_START, &CPeriodTaskDlg::OnBnClickedStart)
ON_WM_TIMER()
END_MESSAGE_MAP()
// ...........
void CPeriodTaskDlg::OnBnClickedStart()
{
m_foo.Start();
}
void CPeriodTaskDlg::OnTimer(UINT_PTR nIDEvent)
{
if (CFoo::FOO_TIMER_ID == nIDEvent) {
m_foo.Doit();
}
CDialogEx::OnTimer(nIDEvent);
}
讨论
这种方法中,CFoo借用了CPeriodTaskDlg的窗口及其定时器消息处理。其实这种交互(相互配合)的机制是比较清晰的,可能看起来两者有点强耦合,——毕竟为了这种配合添加了不少成员函数(如m_pWnd)和相互调用。事实上,CFoo本身要做的事情,让CPeriodTaskDlg帮忙了,即没有真正实现职责唯一性准则。
无论如何,代码易懂,维护成本也可接受。
无窗体定时器
SetTimer和KillTimer的第一个参数HWND其实也可以传入NULL。但这个时候,CFoo就需要自己定义一个定时器处理函数(TIMERPROC/静态成员函数)。——CFoo还不是那么轻量级地可以把自身对象传给这个静态成员函数,这里借用了一个临时(模块级)变量。
CFoo
注意此时SetTimer()会生成一个定时器ID,后续KillTimer需要传入这个ID。
头文件:
#pragma once
class CFoo
{
public:
CFoo();
~CFoo(void);
void Start();
void Doit();
static void CALLBACK TimerProc(HWND hwnd, UINT uMsg, UINT_PTR idEvent, DWORD dwTime);
enum {FOO_TIMER_ID = 100};
private:
int m_iCounter;
UINT_PTR m_iTimerID;
};
实现文件:
#include "StdAfx.h"
#include "Foo.h"
static CFoo *m_pTemp = NULL;
CFoo::CFoo(): m_iCounter(0), m_iTimerID(0)
{
}
CFoo::~CFoo(void)
{
}
void CFoo::Start()
{
m_pTemp = this;
Doit();
m_iTimerID = ::SetTimer(NULL, 0/*FOO_TIMER_ID*/, 2000, &CFoo::TimerProc);
}
void CALLBACK CFoo::TimerProc(HWND hwnd, UINT uMsg, UINT_PTR idEvent, DWORD dwTime)
{
m_pTemp->Doit();
}
// refactored
void CFoo::Doit()
{
m_iCounter++;
::AfxMessageBox("Do something ...");
if (m_iCounter < 5) return;
::AfxMessageBox("Something has been done. Kill the timer ...");
KillTimer(NULL, m_iTimerID/*FOO_TIMER_ID*/);
}
CPeriodTaskDlg
头文件:
#pragma once
#include "Foo.h"
class CPeriodTaskDlg : public CDialogEx
{
// ...........
afx_msg void OnBnClickedStart();
CFoo m_foo;
};
实现文件:
CPeriodTaskDlg::CPeriodTaskDlg(CWnd* pParent /*=NULL*/)
: CDialogEx(CPeriodTaskDlg::IDD, pParent), m_foo()
{
m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
}
void CPeriodTaskDlg::OnBnClickedStart()
{
m_foo.Start();
}
讨论
这种方式比前一种更加松耦合。——只是CFoo中借用了一个临时变量,让静态成员函数能够取到对象的值。
WaitFor+CreateThread
另一种实现周期性的机制是WaitFor,但WaitFor会阻塞,所以这里搭配上线程调用。在对话框调用CFoo的Start的时候,由Start创建一个线程去执行周期性的任务,而Start()立即返回。
CFoo
头文件:
#pragma once
class CFoo
{
public:
CFoo();
~CFoo(void);
void Start();
void Doit();
static DWORD WINAPI ThreadProc(LPVOID lpThreadParameter);
private:
//int m_iCounter;
};
实现文件:
#include "StdAfx.h"
#include "Foo.h"
CFoo::CFoo()//: m_iCounter(0)
{
}
CFoo::~CFoo(void)
{
}
void CFoo::Start()
{
HANDLE hThread = ::CreateThread(NULL, 0, ThreadProc, this, 0, NULL);
}
DWORD WINAPI CFoo::ThreadProc(LPVOID lpThreadParameter)
{
CFoo* foo = (CFoo*)lpThreadParameter;
HANDLE hEvent = ::CreateEvent(NULL, TRUE, FALSE, NULL);
for (int iCounter = 1; iCounter <= 5; iCounter++) {
foo->Doit();
if (iCounter == 5) break; // end immediately
::WaitForSingleObject(hEvent, 2000);
}
::AfxMessageBox("Something has been done.");
return 0;
}
void CFoo::Doit()
{
::AfxMessageBox("Do something ...");
}
删除线程对象
上面的代码可以运行得很好,不过作为习惯,最好等待线程结束,并CloseHandle。
void CFoo::Start()
{
HANDLE hThread = ::CreateThread(NULL, 0, ThreadProc, this, 0, NULL);
WaitForSingleObject(hThread, INFINITE);
::AfxMessageBox("Thread end.");
CloseHandle(hThread);
}
不过需要注意的是,直接用这里的方法会阻塞函数调用,所以可以把线程句柄设计成类的成员变量,然后在类的其他合适的成员函数中添加这些代码。
CPeriodTaskDlg
和第二种方式一样,代码略。
讨论
这种方式看起来更加流畅一些,借由CreateThread()自身可以传递参数的功能,而消除了(第二种)临时变量。