本文代碼運行效果圖如下:
摘 要:本文主要通過一些簡單的例子,介紹了如何在Visual C++的窗口和非窗口類中使用定時器。重點介紹了如何用靜態成員函數和靜態數據成員在非窗口類中使用定時器,同時,又介紹了與定時器相關的知識,例如回調函數,C++類中的靜態成員,以及模板類中的映射類等。
關鍵字 C++ 類 定時器 靜態函數 靜態成員函數 靜態數據成員 回調函數 映射類
摘 要:This page introduce how to use timer in window class and none window class of Visual C++ by some simple samples. Use timer in none window class with static member variable and static member function is the important point. At the same time, it also tell about of some knowledge such as about timer, callback function, static member of C++ class and map class CMap of template class.
關鍵字:C++ Class Timer static CALLBACK CMap
一、引言
定時器在Windows 的程序中的作用不可忽略,也隨處可見。設定一個時間間隔每0.5秒或者1秒鐘刷新一次時鐘,這樣就可以完成一個簡單的電子鐘程序。在不同的編程工具中定時器的用法也不同,Visual C++中也給我們提供了實現這種功能的方法,而且方法不只一種。在窗口類中是使用定時器比較很簡單,用SetTimer()設置了定時器之後,並在Class Wizard中添加了OnTimer消息映射後,您就可以在映射函數OnTimer()中添加代碼實現,來定時完成您的任務,而且還支持任意多個定時器,這種方法大家可能都會用。但是在非窗口的類中,使用定時器就沒那麼簡單了,在類消息映射中就找不到OnTimer()方法了,類中也沒有hWnd這個屬性,SetTimer()也不能象原來那樣使用了,下面給出了一種既不破壞類的完整性的同時又能巧妙的使用定時器的方法。
二、相關知識
在非窗口類中使用定時器,需要了解的知識比較多。首先非窗口類中沒有消息映射,也沒有象CWnd類具有的SetTimer()方法來設置定時器。沒有消息映射,就只能靠我們自己定義的回調函數來處理定時器的消息,因此大家有必要了解一下回調函數的概念。因為回調函數只能用全局函數或者靜態成員函數來實現,為了維持類的完整性,使用類的靜態成員函數來作為回調函數,所以我們又需要了解一下靜態數據成員和靜態成員函數的性質。又因為定時器是在我們的程序中產生的,這又需要來管理定時器,所以又用到了映射表類CMap,因此介紹一下CMap的簡單用法也是必不可少的。
2.1 回調函數
所謂回調函數就是按照一定的形式由你定義並編寫實現內容,當發生某種事件時,而由系統或其它函數來調用的函數。
使用回調函數實際上就是在調用某個函數(通常是API函數)時,將自己編寫的一個函數(也就是回調函數)的地址作為參數傳遞給那個函數。而那個函數在需要的時候,也就是某種事情發生的時候,利用傳遞的函數地址調用回調函數,這時你可以利用這個機會在回調函數中處理消息或完成一定的操作。回調函數只能是全局函數,或者是靜態函數,因為這個函數只是在這個類中使用,所以為了維護類的完整性,我們用類的靜態成員函數來做回調函數。
2.2 C++類中的靜態成員
在C語言中,聲明一個數據為靜態類型,意味著該變量的生存周期是靜態的,即在程序的開始時即分配,到程序終止時才釋放。但在C++中,聲明一個類中的成員為靜態類型,則意味著該類的所有實例只有該成員的一個拷貝。也就是說,不管應用程序中創建了這個類的多少個對象,其靜態成員只有一個副本,該副本為這個類的所有對象實例所共享,而對於非靜態成員,每個類對象實例都有自己的拷貝。例如:
class CPerson
{
public:
CString szName;
static CString szCompanyName;
CPerson();
virtual ~CPerson();
};
接著用該類聲明一個實例 CPerson me;
對於同一家公司員工,每個人都有不同的姓名,但是他們的公司名字是一樣的,所以就可以用一個靜態類型來保存,這樣所有的員工都共享這個公司名稱,只要一位員工更新了公司名稱,則所有員工的公司名稱就被更新了。
靜態成員被當作該類類型的全局對象,可以把一個靜態數據成員和靜態成員函數當成全局變量和函數那樣去存儲和訪問,但又被隱藏在類的內部,並且清楚地與這個類相聯系但又不是全局對象,同全局對象相比,使用靜態成員有兩個優勢:
(1) 靜態成員沒有進入程序的全局名字空間,它屬於類,它的名字只在類的范圍內有效,因此不存在與程序中其他全局名字沖突的可能性。
(2) 可以實現信息隱藏,並可以保持類的完整性,可以是private(私有的)成員、public(公有的)成員或者protected(保護的)成員,而全局對象不能。
2.2.1 靜態數據成員
使用靜態數據成員可以節省內存,因為它是所有對象所公有的,因此,對多個對象來說,靜態數據成員只存儲一處,供所有對象共用。靜態數據成員的值對每個對象都是一樣,但它的值是可以更新的。只要對靜態數據成員的值更新一次,就可以保證所有對象都能夠訪問到被更新後的值,這樣可以提高效率和節省內存空間。
在類中將一個成員變量聲明為靜態的,與聲明普通變量的唯一區別就是在其定義前加一個static。
象上面的例子中那樣聲明:
static CString szCompanyName;
靜態數據成員顯式初始化與一般數據成員初始化不同。靜態數據成員顯式初始化的格式如下:
<數據類型><類名>::<靜態數據成員名>=<值>
對於上面的例子這樣初始化:
CString CPerson::szCommpanyName = "網進科技";
這表明:
(1) 初始化在類體外進行,而前面不加static,以免與一般靜態變量或對象相混淆。
(2) 初始化時不加該成員的訪問權限控制符private,public等。
(3) 初始化時使用作用域運算符來標明它所屬類,因此,靜態數據成員是類的成員,而不是對象的成員。
在類的成員函數中可以直接引用該類的靜態數據成員,而不必使用成員訪問操作符。但是在非成員函數中,我們必須一兩種方式之一訪問靜態數據成員。
(1) 使用成員訪問操作符。
例如:me是CPerson的一個實例,在非成員函數中可以這樣應用其中的靜態數據成員:
CString TheCommpanyName = me.CommpanyName;
(2) 因為類靜態數據成員只有一個拷貝,所以它不一定要通過對象或者指針來訪問。方法二就是用被類名限定修飾的名字直接訪問它。當我們不通過類的成員訪問操作符訪問靜態數據成員時,必須指定類名以及緊跟其後的域操作符,因為靜態成員不是全局對象,所以我們不能在全局域中找到它。如:
CString TheCommpanyName = CPerson::CommpanyName;
順便說一句靜態數據成員還有兩個特點:
(1) 靜態數據成員的類型可以是其所屬類,而非靜態數據成員只能被聲明為該類的對象的指針或引用。
(2) 靜態數據成員可以被作為類成員函數的缺省實參,而非靜態成員不能。
2.2.2 靜態成員函數
靜態成員函數的聲明與普通函數的唯一區別就是在前面加一個static。
通常,當前對象的地址(this)是被隱含地傳遞到被調用的非靜態成員函數的。靜態成員函數具有類的范圍,同非靜態成員函數相比,靜態成員函數沒有this參數,因此它不能訪問一般的數據成員,而只能訪問靜態數據成員、枚舉或嵌套類型和其他的靜態成員函數。這樣使用靜態成員函數在速度上可以比全局函數有少許的增長,它不僅沒有傳遞this指針所需的額外的花費,而且還有使函數在類內的好處。如果靜態成員函數中要引用非靜態成員時,可通過對象來引用。
我們可以用成員訪問操作符點(.)和箭頭(->)為一個類對象或指向類對象的指針訪問靜態成員函數,也可以用限定修飾名直接訪問靜態成員函數,而無需聲明類對象。
靜態成員函數遵循約束:
(1) 不能用成員選擇符(.或->)訪問非靜態成員。
(2) 不能說明為虛函數。
(3) 不能與有相同參數類型的非靜態成員函同名。
(4) 不能聲明為const或volatile。
(5) 出現在類體外的函數定義不指定關鍵字static
2.3 映射表類CMap
映射表類(CMap)是MFC集合類中的一個模板類,也稱作為“字典”,就像一種只有兩列的表格,一列是關鍵字,一列是數據項,它們是一一對應的。關鍵字是唯一的,給出一個關鍵字,映射表類會很快找到對應的數據項。映射表的查找是以哈希表的方式進行的,因此在映射表中查找數值項的速度很快。舉個例子來說吧,公司的所有職員都有一個工號和自己的姓名,工號就是姓名的關鍵字,給出一個工號,就可以很快的找到相應的姓名。映射類最適用於需要根據關鍵字進行快速檢索的場合,我們的程序中就用映射表來保存計時器標志值和類實例指針,用計時器的標志值作為關鍵字。
三、讓靜態成員函數也能訪問非靜態成員函數
從上面的敘述可以看出來,在類中靜態成員函數只能引用靜態數據成員和靜態成員函數,如何才能讓靜態成員函數也能引用非靜態的成員函數和成員變量呢?這也是我們後面將會用到的。
分析一下靜態成員函數和非靜態成員函數的區別,我們會發現非靜態成員函數之所以能訪問所有的成員函數和成員變量,是因為它有個隱含的參數this,訪問成員函數和成員變量的時候,實際上是在前面添加了個引用的符號"this->",所以我們就可以試著將this這個指針作為靜態成員函數的一個參數傳遞進去,這樣不就可以在靜態成員函數中訪問所有的成員函數和成員變量了嗎?下面給出一個實現的例子:
Person.h文件如下:
class CPerson
{
public:
//該實例的一句座右銘
CString szMotto;
//用於保存該實例的指針
CPerson* pThis;
//非靜態成員函數,彈出該實例的座右銘
void GetMotto();
//靜態成員函數,彈出該實例的座右銘
static void GetMottoStaic(CPerson* pPerson);
CPerson();
virtual ~CPerson();
};
Person.cpp文件如下:
#include "stdafx.h"
#include "Person.h"
CPerson::CPerson()
{
pThis = this;
}
CPerson::~CPerson()
{
}
void CPerson::GetMotto()
{
AfxMessageBox(szMotto);
}
void CPerson::GetMottoStaic(CPerson* pPerson)
{
pPerson->GetMotto();
}
在需要的地方就可以如下訪問靜態成員函數:
m_Person.szMotto = "我的座右銘是:這是由靜態函數訪問非靜態函數的結果!";
m_Person.GetMottoStaic(m_Person.pThis);
其實這個例子在實際上是沒有什麼意義的,這樣做的目的只是為了演示如何實現這個方法而已。
四、使用定時器
Windows提供了定時器,幫助我們編寫定期發送消息的程序。定時器一般通過一下兩中方式通知應用程序間隔時間已到。
⑴ 給指定窗口發送WM_TIMER消息,也就是下面的給出在窗口類中使用的方法。
⑵ 調用一個應用程序定義的回調函數,也就是在非窗口類中使用方法。
4.1 在窗口類中使用定時器
在窗口類中使用定時器比較簡單。假如我們想讓這個窗口上放置一個電子鐘,這樣我們必須每1秒或者0.5秒鐘去更新顯示顯見。按照下面的步驟,就可以完成這個電子鐘程序,並且知道如何在窗口類中使用定時器:
首先做在我們新建項目的主窗口上添加一個Label控件,用來顯示時間。接著
⑴ 用函數SetTimer設置一個定時器,函數格式如下:
UINT SetTimer( UINT nIDEvent,
UINT nElapse,
void (CALLBACK EXPORT* lpfnTimer)(HWND, UINT, UINT, DWORD));
這個函數是CWnd類的一個成員函數,其參數意義如下:
nIDEvent: 為設定的定時器指定的定時器標志值,設置多個定時器的時候,每個定時器的值都不同,消息處理函數就是通過這個參數來判斷是哪個定時器的。這裡我們設定為1。
nElapse: 指定發送消息的時間間隔,單位是毫秒。這裡我們設定為1000,也就是一秒。
lpfnTimer: 指定定時器消息由哪個回調函數來執行,如果為空,WM_TIMER將加入到應用程序的消息隊列中,並由CWnd類來處理。這裡我們設定為NULL。
最後代碼如下:
SetTimer(1,1000,NULL);
⑵ 通過Class Wizard給主窗口類添加一個WM_TIMER消息的映射函數,默認為OnTimer(UINT nIDEvent)。
⑶ 然後我們就可以在OnTimer(UINT nIDEvent)的函數實現中添加我們的代碼了。參數nIDEvent就是我們先前設定定時器時指定的標志值,在這裡我們就可以通過它來區別不同的定時器,而作出不同的處理。添加的代碼如下:
switch(nIDEvent)
{
case 1:
CTime m_SysTime = CTime::GetCurrentTime();
SetDlgItemText(IDC_STATIC_TIME,m_SysTime.Format("%Y年%m月%d日 %H:%M:%S"));
break;
}
代碼中的IDC_STATIC_TIME就是我們先前添加的Label控件的ID。
至此,我們的電子鐘的程序就完成了。
4.2 在非窗口類中使用定時器
在非窗口類中使用定時器就要用到前面我們介紹到的所有知識了。因為是無窗口類,所以我們不能使用在窗口類中用消息映射的方法來設置定時器,這時候就必須要用到回調函數。又因為回調函數是具有一定格式的,它的參數不能由我們自己來決定,所以我們沒辦法利用參數將this傳遞進去。可是靜態成員函數是可以訪問靜態成員變量的,因此我們可以把this保存在一個靜態成員變量中,在靜態成員函數中就可以使用該指針,對於只有一個實例的指針,這種方法還是行的通的,由於在一個類中該靜態成員變量只有一個拷貝,對於有多個實例的類,我們就不能用區分了。解決的辦法就是把定時器標志值作為關鍵字,類實例的指針作為項,保存在一個靜態映射表中,因為是標志值是唯一的,用它就可以快速檢索出映射表中對應的該實例的指針,因為是靜態的,所以回調函數是可以訪問他們的。
首先介紹一下用於設置定時的函數:
UINT SetTimer(
HWND hWnd, // handle of window for timer messages
UINT nIDEvent, // timer identifier
UINT uElapse, // time-out value
TIMERPROC lpTimerFunc // address of timer procedure
);
其中的參數意義如下:
hWnd: 指定與定時器相關聯的窗口的句柄。這裡我們設為NULL。
nIDEvent: 定時器標志值,如果hWnd參數為NULL,它將會被跳過,所以我們也設定為NULL。
uElapse: 指定發送消息的時間間隔,單位是毫秒。這裡我們不指定,用參數傳入。
lpTimerFunc: 指定當間隔時間到的時候被統治的函數的地址,也就是這裡的回調函數。這個函數的格式必須為以下格式:
VOID CALLBACK TimerProc(
HWND hwnd, // handle of window for timer messages
UINT uMsg, // WM_TIMER message
UINT idEvent, // timer identifier
DWORD dwTime // current system time
);
其中的參數意義如下:
hwnd: 與定時器相關聯的窗口的句柄。
uMsg: WM_TIMER消息。
idEvent: 定時器標志值。
deTime: 系統啟動後所以經過的時間,單位毫秒。
最後設定定時器的代碼為:
m_nTimerID = SetTimer(NULL,NULL,nElapse,MyTimerProc);
先通過Class Wizard創建一個非窗口類,選擇Generic Class類類型,類名稱為CMyTimer,該類的作用是每隔一段時間提醒我們做某件事情,然後用這個類創建三個實例,每個實例以不同的時間間隔提醒我們做不同的事情。
MyTimer.h
#include <afxtempl.h>
class CMyTimer;
//用模板類中的映射表類定義一種數據類型
typedef CMap<UINT,UINT,CMyTimer*,CMyTimer*> CTimerMap;
class CMyTimer
{
public:
//設置定時器,nElapse表示時間間隔,sz表示要提示的內容
void SetMyTimer(UINT nElapse,CString sz);
//銷毀該實例的定時器
void KillMyTimer();
//保存該實例的定時器標志值
UINT m_nTimerID;
//靜態數據成員要提示的內容
CString szContent;
//聲明靜態數據成員,映射表類,用於保存所有的定時器信息
static CTimerMap m_sTimeMap;
//靜態成員函數,用於處理定時器的消息
static void CALLBACK MyTimerProc(HWND hwnd,UINT uMsg,UINT idEvent,DWORD dwTime);
CMyTimer();
virtual ~CMyTimer();
};
MyTimer.cpp
#include "stdafx.h"
#include "MyTimer.h"
//必須要在外部定義一下靜態數據成員
CTimerMap CMyTimer::m_sTimeMap;
CMyTimer::CMyTimer()
{
m_nTimerID = 0;
}
CMyTimer::~CMyTimer()
{
}
void CALLBACK CMyTimer::MyTimerProc(HWND hwnd,UINT uMsg,UINT idEvent,DWORD dwTime)
{
CString sz;
sz.Format("%d號定時器:%s",
idEvent,
m_sTimeMap[idEvent]->szContent);
AfxMessageBox(sz);
}
void CMyTimer::SetMyTimer(UINT nElapse,CString sz)
{
szContent = sz;
m_nTimerID = SetTimer(NULL,NULL,nElapse,MyTimerProc);
m_sTimeMap[m_nTimerID] = this;
}
void CMyTimer::KillMyTimer()
{
KillTimer(NULL,m_nTimerID);
m_sTimeMap.RemoveKey(m_nTimerID);
}
這樣就完成了在非窗口類中使用定時器的方法。以上這些代碼都在Windwos 2000 Professional 和 Visual C++ 6.0中編譯通過。
五、結論
通過以上的介紹,大家應該知道如何在靜態成員函數中訪問非靜態數據成員和非靜態成員函數,並了解了如何在非窗口類中使用定時器。當然這只是解決這個問題的一種方法,相信還有更好的解決辦法。這個種方法有一定的靈活性,可以在很多地方用到,例如網絡程序中的連接超時以及定時刷新等需要自己來控制,就可以使用這種方法。
參考文獻:
1 潘愛民 張麗.C++ Primer 中文版.(第三版).北京:中國電力出版社.2002
2 Jeff Prosise.MFC Windows 程序設計.(第二版).北京:清華大學出版社.2001
3 王險峰,劉寶宏.Windows環境下的多線程編程原理與應用.北京:清華大學出版社.2002
4 侯俊傑.深入淺出MFC.台灣:松崗電腦圖資料股份有限公司.1998
作者簡介:
姓 名:劉輝
筆 名:我在人間
工作單位:網進科技昆山有限公司
聯系方式:Email:[email protected]
專業職稱:軟件工程師
研究方向:利用Visual C++進行軟件開發
筆者注:這是我寫的第一篇關於編程方面的技術文章,寫這篇文章的目的只是把自己平時做項目和學習的過程中覺得比較用的方法,技巧與大家分享。其中難免有不少的錯誤,請來信指出!
本文配套源碼