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()自身可以传递参数的功能,而消除了(第二种)临时变量。