8. 計時器

Post date: 2012/3/23 上午 05:37:49

8. 計時器

Microsoft Windows計時器是一種輸入設備,它周期性地在每經過一個指定的時間間隔後就通知應用程式一次。您的程式將時間間隔告訴Windows,例如「每10秒鐘通知我一聲」,然後Windows給您的程式發送周期性發生的WM_TIMER訊息以表示時間到了。

初看之下,Windows計時器似乎不如鍵盤和滑鼠設備重要,而且對許多應用程式來說確實如此。但是,計時器比您可能認為的要重要得多,它不只用於計時程式,比如出現在工具列中的Windows時鐘和這一章中的兩個時鐘程式。下面是Windows計時器的其他應用,有些可能並不那麼明顯:

  • 多工 雖然Windows 98是一個優先權式的多工環境,但有時候如果程式儘快將控制傳回給Windows效率會更高。如果一個程式必須進行大量的處理,那麼它可以將作業分成小塊,每接收到一個WM_TIMER訊息處理一塊(我將在 第二十章 中對此做更多的討論)。
  • 維護更新過的狀態報告 程式可以利用計時器來顯示持續變化資訊的「即時」更新,比如關於系統資源的變化或某個任務的進展情況。
  • 實作「自動儲存」功能 計時器提示Windows程式在指定的時間過去後把使用者的工作儲存到磁片上。
  • 終止程式展示版本的執行 一些程式的展示版本被設計成在其開始後,多長時間結束,比如說,30分鐘。如果時間已到,那麼計時器就會通知應用程式。
  • 步進移動 遊戲中的圖形物件或電腦輔助教學程式中的連續顯示,需要按指定的速率來處理。利用計時器可以消除由於微處理器速度不同而造成的不一致。
  • 多媒體 播放CD聲音、聲音或音樂的程式通常在背景播放聲音資料。一個程式可以使用計時器來周期性地檢查已播放了多少聲音資料,並據此協調螢幕上的視覺資訊。

另一項應用可以保證程式在退出視窗訊息處理程式後,能夠重新得到控制。在大多數時情況下,程式不能夠知道何時下一個訊息會到來。

計時器入門

您可以通過呼叫SetTimer函式為您的Windows程式分配一個計時器。SetTimer有一個時間間隔範圍為1毫秒到4,294,967,295毫秒(將近50天)的整數型態參數,這個值指示Windows每隔多久時間給您的程式發送WM_TIMER訊息。例如,如果間隔為1000毫秒,那麼Windows將每秒給程式發送一個WM_TIMER訊息。

當您的程式用完計時器時,它呼叫KillTimer函式來停止計時器訊息。在處理WM_TIMER訊息時,您可以通過呼叫KillTimer函式來編寫一個「限用一次」的計時器。KillTimer呼叫清除訊息佇列中尚未被處理的WM_TIMER訊息,從而使程式在呼叫KillTimer之後就不會再接收到WM_TIMER訊息。

系統和計時器

Windows計時器是PC硬體和ROM BIOS架構下之計時器一種相對簡單的擴充。回到Windows以前的MS-DOS程式寫作環境下,應用程式能夠通過攔截者稱為timer tick的BIOS中斷來實作時鐘或計時器。一些為MS-DOS編寫的程式自己攔截這個硬體中斷以實作時鐘和計時器。這些中斷每54.915毫秒產生一次,或者大約每秒18.2次。這是原始的IBM PC的微處理器時脈值4.772720 MHz被218所除而得出的結果。

Windows應用程式不攔截BIOS中斷,相反地,Windows本身處理硬體中斷,這樣應用程式就不必進行處理。對於目前擁有計時器的每個程式,Windows儲存一個每次硬體timer tick減少的計數。當這個計數減到0時,Windows在應用程式訊息佇列中放置一個WM_TIMER訊息,並將計數重置為其最初值。

因為Windows應用程式從正常的訊息佇列中取得WM_TIMER訊息,所以您的程式在進行其他處理時不必擔心WM_TIMER訊息會意外中斷了程式。在這方面,計時器類似於鍵盤和滑鼠。驅動程式處理非同步硬體中斷事件,Windows把這些事件翻譯為規律、結構化和順序化的訊息。

在Windows 98中,計時器與其下的PC計時器一樣具有55毫秒的解析度。在Microsoft Windows NT中,計時器的解析度為10毫秒。

Windows應用程式不能以高於這些解析度的頻率(在Windows 98下,每秒18.2次,在Windows NT下,每秒大約100次)接收WM_TIMER訊息。在SetTimer呼叫中指定的時間間隔總是截尾後tick數的整數倍。例如,1000毫秒的間隔除以54.925毫秒,得到18.207個tick,截尾後是18個tick,它實際上是989毫秒。對每個小於55毫秒的間隔,每個tick都會產生一個WM_TIMER訊息。

計時器訊息不是非同步的

因為計時器使用硬體計時器中斷,程式寫作者有時會誤解,認為他們的程式會非同步地被中斷來處理WM_TIMER訊息。

然而,WM_TIMER訊息並不是非同步的。WM_TIMER訊息放在正常的訊息佇列之中,和其他訊息排列在一起,因此,如果在SetTimer呼叫中指定間隔為1000毫秒,那麼不能保證程式每1000毫秒或者989毫秒就會收到一個WM_TIMER訊息。如果其他程式的執行事件超過一秒,在此期間內,您的程式將收不到任何WM_TIMER訊息。您可以使用本章的程式來展示這一點。事實上, Windows對WM_TIMER訊息的處理非常類似於對WM_PAINT訊息的處理,這兩個訊息都是低優先順序的,程式只有在訊息佇列中沒有其他訊息時才接收它們。

WM_TIMER還在另一方面和WM_PAINT相似:Windows不能持續向訊息佇列中放入多個WM_TIMER訊息,而是將多餘的WM_TIMER訊息組合成一個訊息。因此,應用程式不會一次收到多個這樣的訊息,儘管可能在短時間內得到兩個WM_TIMER訊息。應用程式不能確定這種處理方式所導致的WM_TIMER訊息「遺漏」的數目。

這樣,WM_TIMER訊息僅僅在需要更新時才提示程式,程式本身不能經由統計WM_TIMER訊息的數目來計時(在本章後面,我們將編寫兩個每秒更新一次的時鐘程式,並可以看到如何做到這一點)。

為了方便起見,下面在討論時鐘時,我將使用「每秒得到一次WM_TIMER訊息」這樣的敘述,但是請記住,這些訊息並非精確的tick中斷。

計時器的使用:三種方法

如果您需要在整個程式執行期間都使用計時器,那麼您將得從WinMain函式中或者在處理WM_CREATE訊息時呼叫SetTimer,並在退出WinMain或回應WM_DESTROY訊息時呼叫KillTimer。根據呼叫SetTimer時使用的參數,可以下列三種方法之一使用計時器。

方法一

這是最方便的一種方法,它讓Windows把WM_TIMER訊息發送到應用程式的正常視窗訊息處理程式中,SetTimer呼叫如下所示:

第一個參數是其視窗訊息處理程式將接收WM_TIMER訊息的視窗代號。第二個參數是計時器ID,它是一個非0數值,在整個例子中假定為1。第三個參數是一個32位元無正負號整數,以毫秒為單位指定一個時間間隔,一個60,000的值將使Windows每分鐘發送一次WM_TIMER訊息。

您可以通過呼叫

在任何時刻停止WM_TIMER訊息(即使正在處理WM_TIMER訊息)。此函式的第二個參數是SetTimer呼叫中所用的同一個計時器ID。在終止程式之前,您應該回應WM_DESTROY訊息停止任何活動的計時器。

當您的視窗訊息處理程式收到一個WM_TIMER訊息時,wParam參數等於計時器的ID值(上述情形為1),lParam參數為0。如果需要設定多個計時器,那麼對每個計時器都使用不同的計時器ID。wParam的值將隨傳遞到視窗訊息處理程式的WM_TIMER訊息的不同而不同。為了使程式更具有可讀性,您可以使用#define敘述定義不同的計時器ID:

然後您可以使用兩個SetTimer呼叫來設定兩個計時器:

WM_TIMER的處理如下所示:

SetTimer (hwnd, 1, uiMsecInterval, NULL) ;

KillTimer (hwnd, 1) ;

#define TIMER_SEC 1 #define TIMER_MIN 2

SetTimer (hwnd, TIMER_SEC, 1000, NULL) ; SetTimer (hwnd, TIMER_MIN, 60000, NULL) ;

case WM_TIMER: switch (wParam) { case TIMER_SEC: //每秒一次的處理 break ; case TIMER_MIN: //每分鐘一次的處理 break ; } return 0 ;

如果您想將一個已經存在的計時器設定為不同的時間間隔,您可以簡單地用不同的時間值再次呼叫SetTimer。在時鐘程式裏,如果顯示秒或不顯示秒是可以選擇的,您就可以這樣做,只需簡單地將時間間隔在1000毫秒和60 000毫秒間切換就可以了。

程式8-1顯示了一個使用計時器的簡單程式,名為BEEPER1,計時器的時間間隔設定為1秒。當它收到WM_TIMER訊息時,它將顯示區域的顏色由藍色變為紅色或由紅色變為藍色,並通過呼叫MessageBeep函式發出響聲。(雖然MessageBeep通常用於MessageBox,但它確實是一個全功能的鳴叫函式。在有音效卡的PC機上,一般可以使用不同的MB_ICON參數作為MessageBeep的一個參數以用於MessageBox,來播放使用者在「控制台」的「聲音」程式中選擇的不同聲音)。

BEEPER1在視窗訊息處理程式處理WM_CREATE訊息時設定計時器。在處理WM_TIMER訊息處理期間,BEEPER1呼叫MessageBeep,翻轉bFlipFlop的值並使視窗無效以產生WM_PAINT訊息。在處理WM_PAINT訊息處理期間,BEEPER1通過呼叫GetClientRect獲得視窗大小的RECT結構,並通過呼叫FillRect改變視窗的顏色。

程式8-1 BEEPER1 BEEPER1.C /*------------------------------------------------------------------------- BEEPER1.C -- Timer Demo Program No. 1 (c) Charles Petzold, 1998 -------------------------------------------------------------------------*/ #include <windows.h> #define ID_TIMER 1 LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("Beeper1") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) { MessageBox ( NULL, TEXT ("Program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow ( szAppName, TEXT ("Beeper1 Timer Demo"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; } LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam,LPARAM lParam) { static BOOL fFlipFlop = FALSE ; HBRUSH hBrush ; HDC hdc ; PAINTSTRUCT ps ; RECT rc ; switch (message) { case WM_CREATE: SetTimer (hwnd, ID_TIMER, 1000, NULL) ; return 0 ; case WM_TIMER : MessageBeep (-1) ; fFlipFlop = !fFlipFlop ; InvalidateRect (hwnd, NULL, FALSE) ; return 0 ; case WM_PAINT : hdc = BeginPaint (hwnd, &ps) ; GetClientRect (hwnd, &rc) ; hBrush = CreateSolidBrush (fFlipFlop ? RGB(255,0,0) : RGB(0,0,255)) ; FillRect (hdc, &rc, hBrush) ; EndPaint (hwnd, &ps) ; DeleteObject (hBrush) ; return 0 ; case WM_DESTROY : KillTimer (hwnd, ID_TIMER) ; PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }

因為BEEPER1每次收到WM_TIMER訊息時,都用顏色的變換顯示出來,所以您可以通過呼叫BEEPER1來查看WM_TIMER訊息的性質,並完成Windows內部的一些其他操作。

例如,首先呼叫 控制台 顯示器 程式,選擇 效果 ,確定 拖曳時顯示視窗內容 核取方塊沒有被選中。現在,試著移動或者縮放BEEPER1視窗,這將導致程式進入「模態訊息迴圈」。Windows通過在內部訊息而非您程式的訊息迴圈中攔截所有訊息,來禁止對移動或者縮放操作的任何干擾。通過此迴圈到達程式視窗的大多數訊息都被丟棄,這就是BEEPER1停止蜂鳴的原因。當完成了移動與縮放之後,您將會注意到BEEPER1不能取得它所丟棄的所有WM_TIMER訊息,儘管前兩個訊息的間隔可能少於1秒。

在「拖曳時顯示視窗內容」核取方塊被選中時,Windows中,的模態訊息迴圈會試圖給您的視窗訊息處理程式傳遞一些丟失的訊息。這樣做有時工作得很好,有時卻不行。

方法二

設定計時器的第一種方法是把WM_TIMER訊息發送到通常的視窗訊息處理程式,而第二種方法是讓Windows直接將計時器訊息發送給您程式的另一個函式。

接收這些計時器訊息的函式被稱為「callback」函式,這是一個在您的程式之中但是由Windows呼叫的函式。您先告訴Windows此函式的位址,然後Windows呼叫此函式。這看起來也很熟悉,因為程式的視窗訊息處理程式實際上也是一種callback函式。當註冊視窗類別時,要將函式的位址告訴Windows,當發送訊息給程式時,Windows會呼叫此函式。

SetTimer並非是唯一使用callback函式的Windows函式。CreateDialog和DialogBox函式(將在 第十一章 中介紹)使用callback函式處理對話方塊中的訊息;有幾個Windows函式(EnumChildWindow、EnumFonts、EnumObjects、EnumProps和EnumWindow)把列舉資訊傳遞給callback函式;還有幾個不那麼常用的函式(GrayString、LineDDA和SetWindowHookEx)也要求callback函式。

像視窗訊息處理程式一樣,callback函式也必須定義為CALLBACK,因為它是由Windows從程式的程式碼段呼叫的。callback函式的參數和callback函式的傳回值取決於callback函式的目的。跟計時器有關的callback函式中,輸入參數與視窗訊息處理程式的輸入參數一樣。計時器callback函式不向Windows傳回值。

我們把以下的callback函式稱為TimerProc(您能夠選擇與其他一些用語不會發生衝突的任何名稱),它只處理WM_TIMER訊息:

VOID CALLBACK TimerProc ( HWND hwnd, UINT message, UINT iTimerID, DWORD dwTime) { 處理WM_TIMER訊息 }

TimerProc的參數hwnd是在呼叫SetTimer時指定的視窗代號。Windows只把WM_TIMER訊息送給TimerProc,因此訊息參數總是等於WM_TIMER。iTimerID值是計時器ID,dwTimer值是與從GetTickCount函式的傳回值相容的值。這是自Windows啟動後所經過的毫秒數。

在BEEPER1中已經看到過,用第一種方法設定計時器時要求下面格式的SetTimer呼叫:

您使用callback函式處理WM_TIMER訊息時,SetTimer的第四個參數由callback函式的位址取代,如下所示:

我們來看看一些範例程式碼,這樣您就會瞭解這些東西是如何組合在一起的。在功能上,除了Windows發送一個計時器訊息給TimerProc而非WndProc之外,程式8-2所示的BEEPER2程式與BEEPER1是相同的。注意,TimerProc和WndProc一起被宣告在程式的開始處。

SetTimer (hwnd, iTimerID, iMsecInterval, NULL) ;

SetTimer (hwnd, iTimerID, iMsecInterval, TimerProc) ;

程式8-2 BEEPER2 BEEPER2.C /*--------------------------------------------------------------------------- BEEPER2.C -- Timer Demo Program No. 2 (c) Charles Petzold, 1998 ---------------------------------------------------------------------------*/ #include <windows.h> #define ID_TIMER 1 LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; VOID CALLBACK TimerProc (HWND, UINT, UINT, DWORD ) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static char szAppName[] = "Beeper2" ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) { MessageBox ( NULL, TEXT ("Program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow ( szAppName, "Beeper2 Timer Demo", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; } LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { switch (message) { case WM_CREATE: SetTimer (hwnd, ID_TIMER, 1000, TimerProc) ; return 0 ; case WM_DESTROY: KillTimer (hwnd, ID_TIMER) ; PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; } VOID CALLBACK TimerProc (HWND hwnd, UINT message, UINT iTimerID, DWORD dwTime) { static BOOL fFlipFlop = FALSE ; HBRUSH hBrush ; HDC hdc ; RECT rc ; MessageBeep (-1) ; fFlipFlop = !fFlipFlop ; GetClientRect (hwnd, &rc) ; hdc = GetDC (hwnd) ; hBrush = CreateSolidBrush (fFlipFlop ? RGB(255,0,0) : RGB(0,0,255)) ; FillRect (hdc, &rc, hBrush) ; ReleaseDC (hwnd, hdc) ; DeleteObject (hBrush) ; }

方法三

設定計時器的第三種方法類似於第二種方法,只是傳遞給SetTimer的hwnd參數被設定為NULL,並且第二個參數(通常為計時器ID)被忽略了,最後,此函式傳回計時器ID:

如果沒有可用的計時器,那麼從SetTimer傳回的iTimerID值將為NULL。

KillTimer的第一個參數(通常是視窗代號)也必須為NULL,計時器ID必須是SetTimer的傳回值:

傳遞給TimerProc計時器函式的hwnd參數也必須是NULL。這種設定計時器的方法很少被使用。如果在您的程式在不同時刻有一系列的SetTimer呼叫,而又不希望追蹤您已經用過了那些計時器ID,那麼使用此方法是很方便的。

既然您已經知道了如何使用Windows計時器,就可以開始討論一些有用的計時器程式了。

計時器用於時鐘

時鐘是計時器最明顯的應用,因此讓我們來看看兩個時鐘,一個數位時鐘,一個類比時鐘。

建立數位時鐘

程式8-3所示的DIGCLOCK程式,使用類似LED的7個顯示方塊顯示了目前的時間。

iTimerID = SetTimer (NULL, 0, wMsecInterval, TimerProc) ;

KillTimer (NULL, iTimerID) ;

程式8-3 DIGCLOCK DIGCLOCK.C /*---------------------------------------------------------------------------- DIGCLOCK.C -- Digital Clock (c) Charles Petzold, 1998 ----------------------------------------------------------------------------*/ #include <windows.h> #define ID_TIMER 1 LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("DigClock") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) { MessageBox ( NULL, TEXT ("Program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow ( szAppName, TEXT ("Digital Clock"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; } void DisplayDigit (HDC hdc, int iNumber) { static BOOL fSevenSegment [10][7] = { 1, 1, 1, 0, 1, 1, 1, // 0 0, 0, 1, 0, 0, 1, 0, // 1 1, 0, 1, 1, 1, 0, 1, // 2 1, 0, 1, 1, 0, 1, 1, // 3 0, 1, 1, 1, 0, 1, 0, // 4 1, 1, 0, 1, 0, 1, 1, // 5 1, 1, 0, 1, 1, 1, 1, // 6 1, 0, 1, 0, 0, 1, 0, // 7 1, 1, 1, 1, 1, 1, 1, // 8 1, 1, 1, 1, 0, 1, 1 } ; // 9 static POINT ptSegment [7][6] = { 7, 6, 11, 2, 31, 2, 35, 6, 31, 10, 11, 10, 6, 7, 10, 11, 10, 31, 6, 35, 2, 31, 2, 11, 36, 7, 40, 11, 40, 31, 36, 35, 32, 31, 32, 11, 7 , 36, 11, 32, 31, 32, 35, 36, 31, 40, 11, 40, 6 , 37, 10, 41, 10, 61, 6, 65, 2, 61, 2, 41, 36, 37, 40, 41, 40, 61, 36, 65, 32, 61, 32, 41, 7 , 66, 11, 62, 31, 62, 35, 66, 31, 70, 11, 70 } ; int iSeg ; for (iSeg = 0 ; iSeg < 7 ; iSeg++) if (fSevenSegment [iNumber][iSeg]) Polygon (hdc, ptSegment [iSeg], 6) ; } void DisplayTwoDigits (HDC hdc, int iNumber, BOOL fSuppress) { if (!fSuppress || (iNumber / 10 != 0)) DisplayDigit (hdc, iNumber / 10) ; OffsetWindowOrgEx (hdc, -42, 0, NULL) ; DisplayDigit (hdc, iNumber % 10) ; OffsetWindowOrgEx (hdc, -42, 0, NULL) ; } void DisplayColon (HDC hdc) { POINT ptColon [2][4] = { 2, 21, 6, 17, 10, 21, 6, 25, 2, 51, 6, 47, 10, 51, 6, 55 } ; Polygon (hdc, ptColon [0], 4) ; Polygon (hdc, ptColon [1], 4) ; OffsetWindowOrgEx (hdc, -12, 0, NULL) ; } void DisplayTime (HDC hdc, BOOL f24Hour, BOOL fSuppress) { SYSTEMTIME st ; GetLocalTime (&st) ; if (f24Hour) DisplayTwoDigits (hdc, st.wHour, fSuppress) ; else DisplayTwoDigits (hdc, (st.wHour %= 12) ? st.wHour : 12, fSuppress) ; DisplayColon (hdc) ; DisplayTwoDigits (hdc, st.wMinute, FALSE) ; DisplayColon (hdc) ; DisplayTwoDigits (hdc, st.wSecond, FALSE) ; } LRESULT CALLBACK WndProc ( HWND hwnd, UINT message, WPARAM wParam,LPARAM lParam) { static BOOL f24Hour, fSuppress ; static HBRUSH hBrushRed ; static int cxClient, cyClient ; HDC hdc ; PAINTSTRUCT ps ; TCHAR szBuffer [2] ; switch (message) { case WM_CREATE: hBrushRed = CreateSolidBrush (RGB (255, 0, 0)) ; SetTimer (hwnd, ID_TIMER, 1000, NULL) ;// fall through case WM_SETTINGCHANGE: GetLocaleInfo (LOCALE_USER_DEFAULT, LOCALE_ITIME, szBuffer, 2) ; f24Hour = (szBuffer[0] == '1') ; GetLocaleInfo (LOCALE_USER_DEFAULT, LOCALE_ITLZERO, szBuffer, 2) ; fSuppress = (szBuffer[0] == '0') ; InvalidateRect (hwnd, NULL, TRUE) ; return 0 ; case WM_SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; return 0 ; case WM_TIMER: InvalidateRect (hwnd, NULL, TRUE) ; return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; SetMapMode (hdc, MM_ISOTROPIC) ; SetWindowExtEx (hdc, 276, 72, NULL) ; SetViewportExtEx (hdc, cxClient, cyClient, NULL) ; SetWindowOrgEx (hdc, 138, 36, NULL) ; SetViewportOrgEx (hdc, cxClient / 2, cyClient / 2, NULL) ; SelectObject (hdc, GetStockObject (NULL_PEN)) ; SelectObject (hdc, hBrushRed) ; DisplayTime (hdc, f24Hour, fSuppress) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: KillTimer (hwnd, ID_TIMER) ; DeleteObject (hBrushRed) ; PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }

DIGCLOCK視窗如圖8-1所示。

雖然,在圖8-1中您看不到時鐘的數字是紅色的。DIGCLOCK的視窗訊息處理程式在處理WM_CREATE訊息處理期間建立了一個紅色的畫刷並在處理WM_DESTROY訊息處理期間清除它。WM_CREATE訊息也為DIGCLOCK設定了一個一秒的計時器,該計時器在處理WM_DESTROY訊息處理期間被終止(待會將討論對GetLocaleInfo的呼叫)。

在收到WM_TIMER訊息後,DIGCLOCK的視窗程序呼叫InvalidateRect簡單地使整個視窗無效。這不是最佳方法,因為每秒整個視窗都要被擦除和重畫,有時會引起顯示器的閃爍。依據目前的時間使視窗需要更新的部分無效是最好的解決方法。然而,在邏輯上這樣做的確很複雜。

在處理WM_TIMER訊息處理期間使視窗無效會迫使所有程式的真正活動轉入WM_PAINT。DIGCLOCK在WM_PAINT訊息一開始將映射方式設定為MM_ISOTROPIC。這樣,DIGCLOCK將使用水平方向和垂直方向相等的軸。這些軸(由SetWindowExtEx呼叫設定)是水平276個單位,垂直72個單位。當然,這些軸定得有點太隨意了,但它們是按照時鐘數位元的大小和間距安排的。

DIGCLOCK將視窗原點設定為(138,36),這是視窗範圍的中心;將視埠原點設定為(cxClient / 2,cyClient / 2)。這意味著時鐘的顯示位於DIGCLOCK顯示區域的中心,但是該DIGCLOCK也可以使用在顯示幕左上角的原點(0, 0)的軸。

然後WM_PAINT將目前畫刷設定為之前建立的紅畫刷,將目前畫筆設定為NULL_PEN, 並呼叫DIGCLOCK中的函式DisplayTime。

取得目前時間

DisplayTime函式開始呼叫Windows函式GetLocalTime,它帶有一個的SYSTEMTIME結構的參數,在WINBASE.H中定義為:

圖8-1 DIGCLOCK的螢幕顯示

typedef struct _SYSTEMTIME { WORD wYear ; WORD wMonth ; WORD wDayOfWeek ; WORD wDay ; WORD wHour ; WORD wMinute ; WORD wSecond ; WORD wMilliseconds ; } SYSTEMTIME, * PSYSTEMTIME ;

很明顯,SYSTEMTIME結構包含日期和時間。月份由1開始遞增(也就是說,一月是1),星期由0開始遞增(星期天是0)。wDay成員是本月目前的日子,也是由1開始遞增的。

SYSTEMTIME主要用於GetLocalTime和GetSystemTime函式。GetSystemTime函式傳回目前的世界時間(Coordinated Universal Time,UTC),大概與英國格林威治時間相同。GetLocalTime函式傳回當地時間,依據電腦所在的時區。這些值的精確度完全決定於使用者所調整的時間精確度以及是否指定了正確的時區。可以雙擊工作列的時間顯示來檢查電腦上的時區設定。 第二十三章 會有一個程式,能夠通過Internet精確地設定時間。

Windows還有SetLocalTime和SetSystemTime函式,以及在/Platform SDK/Windows Base Services/General Library/Time中說明的其他與時間有關的函式。

顯示數字和冒號

如果DIGCLOCK使用一種模擬7段顯示的字體將會簡單一些。否則,它就得使用Polygon函式做所有的工作。

DIGCLOCK中的DisplayDigit函式定義了兩個陣列。fSevenSegment陣列有7個BOOL值,用於從0到9的每個十進位數字。這些值指出了哪一段需要顯示(為1),哪一段不需要顯示(為0)。在這個陣列中,7段由上到下、由左到右排序。7段中的每個段都是一個6邊的多邊形。ptSegment陣列是一個POINT結構的陣列,指出了7個段中每個點的圖形座標。每個數字由下列程式碼畫出:

for (iSeg = 0 ; iSeg < 7 ; iSeg++) if ( fSevenSegment [iNumber][iSeg]) Polygon (hdc, ptSegment [iSeg], 6) ;

類似地(但更簡單),DisplayColon函式在小時與分鐘、分鐘與秒之間畫一個冒號。數字是42個單位寬,冒號是12個單位寬,因此6個數字與2個冒號,總寬度是276個單位,SetWindowExtEx呼叫中使用了這個大小。

回到DisplayTime函式,原點位於最左數字位置的左上角。DisplayTime呼叫DisplayTwoDigits,DisplayTwoDigits呼叫DisplayDigit兩次,並且在每次呼叫OffsetWindowOrgEx後,將視窗原點向右移動42個單位。類似地,DisplayColon函式在畫完冒號後,將視窗原點向右移動12個單位。用這種方法,不管物件出現在視窗內的哪個地方,函式對數字和冒號都使用同樣的座標。

這個程式的其他技巧是以12小時或24小時的格式顯示時間以及當最左邊的小時數字為0時不顯示它。

國際化

儘管像DIGCLOCK這樣顯示時間是非常簡單的,但是要顯示複雜的日期和時間還是要依賴Windows的國際化支援。格式化日期和時間的最簡單的方法是呼叫GetDateFormat和GetTimeFormat函式。這些函式在/Platform SDK/Windows Base Services/General Library/String Manipulation/String Manipulation Reference/String Manipulation Functions中有記載,但是它們在/Platform SDK/Windows Base Services/International Features/National Language Support中進行了說明。這些函式接受SYSTEMTIME結構並且依據使用者在「控制台」的「區域設定」 程式中所做的選擇而將日期和時間格式化。

DIGCLOCK不能使用GetDateFormat函式,因為它只知道顯示數字和冒號,然而,DIGCLOCK應該能夠根據使用者的參數選擇來顯示12小時或24小時的格式,並禁止(或不禁止)開頭的小時數字。您可以從GetLocaleInfo函式中取得這種資訊。雖然GetLocaleInfo在/Platform SDK/Windows Base Services/General Library/String Manipulation/String Manipulation Reference/String Manipulation Functions中有記載,但是這個函式使用的識別字在/Platform SDK/Windows Base Services/International Features/National Language Support/National Language Support Constants中有說明。

DIGCLOCK在處理WM_CREATE訊息時,最初呼叫GetLocaleInfo兩次,第一次使用LOCALE_ITIME識別字(確定使用的是12小時還是24小時格式),然後使用LOCALE_ITLZERO識別字(在小時顯示中禁止前面顯示0)。GetLocaleInfo函式在字串中傳回所有的資訊,但是在大多數情況下把字串轉變為整數並不是非常容易。DIGCLOCK把字串儲存在兩個靜態變數中並把它們傳遞給DisplayTime函式。

如果使用者更改了任何系統設定,則會將WM_SETTINGCHANGE訊息傳送給所有的應用程式。DIGCLOCK通過再次呼叫GetLocaleInfo處理這個訊息。以這種方式,您可以在「控制台」的「區域設定」 程式中進行不同的設定來實驗一下。

在理論上,DIGCLOCK也應該使用LOCALE_STIME識別字呼叫GetLocaleInfo。這會傳回使用者為時間的小時、分鐘和秒等單個部分選擇的字元。因為DIGCLOCK被設定為僅顯示冒號,所以不管選擇了什麼,都會得到冒號。要指出時間是A.M.或P.M.,應用程式可以使用帶有LOCALE_S1159和LOCALE_S2359識別字的GetLocaleInfo函式。這些識別字使程式獲得適合於使用者國家/地區和語言的字串。

我們也可以讓DIGCLOCK處理WM_TIMECHANGE訊息,這樣它將系統時間與日期發生變化的訊息通知應用程式。DIGCLOCK因WM_TIMER訊息而每秒更新一次,實際上沒有必要這樣作,對WM_TIMECHANGE訊息的處理使得每分鐘更新一次的時鐘變得更為合理。

建立類比時鐘

類比時鐘不必關心國際化問題,但是由於圖形所引起的複雜性卻抵消了這種簡化。為了正確地產生時鐘,您需要知道一些三角函數。CLOCK如程式8-4所示。

程式8-4 CLOCK CLOCK.C /*--------------------------------------------------------------------------- CLOCK.C -- Analog Clock Program (c) Charles Petzold, 1998 ---------------------------------------------------------------------------*/ #include <windows.h> #include <math.h> #define ID_TIMER 1 #define TWOPI (2 * 3.14159) LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain ( HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("Clock") ; HWND hwnd; MSG msg; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = NULL ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) { MessageBox ( NULL, TEXT ("Program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow ( szAppName, TEXT ("Analog Clock"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; } void SetIsotropic (HDC hdc, int cxClient, int cyClient) { SetMapMode (hdc, MM_ISOTROPIC) ; SetWindowExtEx (hdc, 1000, 1000, NULL) ; SetViewportExtEx (hdc, cxClient / 2, -cyClient / 2, NULL) ; SetViewportOrgEx (hdc, cxClient / 2, cyClient / 2, NULL) ; } void RotatePoint (POINT pt[], int iNum, int iAngle) { int i ; POINT ptTemp ; for (i = 0 ; i < iNum ; i++) { ptTemp.x = (int) (pt[i].x * cos (TWOPI * iAngle / 360) + pt[i].y * sin (TWOPI * iAngle / 360)) ; ptTemp.y = (int) (pt[i].y * cos (TWOPI * iAngle / 360) - pt[i].x * sin (TWOPI * iAngle / 360)) ; pt[i] = ptTemp ; } } void DrawClock (HDC hdc) { int iAngle ; POINT pt[3] ; for (iAngle = 0 ; iAngle < 360 ; iAngle += 6) { pt[0].x = 0 ; pt[0].y = 900 ; RotatePoint (pt, 1, iAngle) ; pt[2].x = pt[2].y = iAngle % 5 ? 33 : 100 ; pt[0].x - = pt[2].x / 2 ; pt[0].y - = pt[2].y / 2 ; pt[1].x = pt[0].x + pt[2].x ; pt[1].y = pt[0].y + pt[2].y ; SelectObject (hdc, GetStockObject (BLACK_BRUSH)) ; Ellipse (hdc, pt[0].x, pt[0].y, pt[1].x, pt[1].y) ; } } void DrawHands (HDC hdc, SYSTEMTIME * pst, BOOL fChange) { static POINT pt[3][5] ={0, -150, 100, 0, 0, 600, -100, 0, 0, -150, 0, -200, 50, 0, 0, 800, -50, 0, 0, -200, 0, 0, 0, 0, 0, 0, 0, 0, 0, 800 } ; int i, iAngle[3] ; POINT ptTemp[3][5] ; iAngle[0] = (pst->wHour * 30) % 360 + pst->wMinute / 2 ; iAngle[1] = pst->wMinute * 6 ; iAngle[2] = pst->wSecond * 6 ; memcpy (ptTemp, pt, sizeof (pt)) ; for (i = fChange ? 0 : 2 ; i < 3 ; i++) { RotatePoint (ptTemp[i], 5, iAngle[i]) ; Polyline (hdc, ptTemp[i], 5) ; } } LRESULT CALLBACK WndProc ( HWND hwnd, UINT message, WPARAM wParam,LPARAM lParam) { static int cxClient, cyClient ; static SYSTEMTIME stPrevious ; BOOL fChange ; HDC hdc ; PAINTSTRUCT ps ; SYSTEMTIME st ; switch (message) { case WM_CREATE : SetTimer (hwnd, ID_TIMER, 1000, NULL) ; GetLocalTime (&st) ; stPrevious = st ; return 0 ; case WM_SIZE : cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; return 0 ; case WM_TIMER : GetLocalTime (&st) ; fChange = st.wHour ! = stPrevious.wHour || st.wMinute ! = stPrevious.wMinute ; hdc = GetDC (hwnd) ; SetIsotropic (hdc, cxClient, cyClient) ; SelectObject (hdc, GetStockObject (WHITE_PEN)) ; DrawHands (hdc, &stPrevious, fChange) ; SelectObject (hdc, GetStockObject (BLACK_PEN)) ; DrawHands (hdc, &st, TRUE) ; ReleaseDC (hwnd, hdc) ; stPrevious = st ; return 0 ; case WM_PAINT : hdc = BeginPaint (hwnd, &ps) ; SetIsotropic (hdc, cxClient, cyClient) ; DrawClock (hdc) ; DrawHands (hdc, &stPrevious, TRUE) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY : KillTimer (hwnd, ID_TIMER) ; PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }

CLOCK螢幕顯示如圖8-2。

等方向性(isotropic)映射對於這樣的應用來說是理想的,CLOCK.C中的SetIsotropic函式負責設定此模式。在呼叫SetMapMode之後,SetIsotropic將視窗範圍設定為1000,並將視埠範圍設定為顯示區域的一半寬度和顯示區域的負的一半高度。視埠原點被設定為顯示區域的中心。我在第五章中討論過,這將建立一個笛卡兒座標系,其點(0,0)位於顯示區域的中心,在所有方向上的範圍都是1000。

RotatePoint函式是用到三角函數的地方,此函式的三個參數分別是一個或者多個點的陣列、陣列中點的個數以及以度為單位的旋轉角度。函式以原點為中心按順時針方向(這對一個時鐘正合適)旋轉這些點。例如,如果傳給函式的點是(0,100)-即12:00的位置-而角度為90度,那麼該點將被變換為(100,0)-即3:00。它使用下列公式來做到這一點:

RotatePoint函式在繪製時鐘表面的點和錶針時都是有用的,我們將馬上看到這一點。

DrawClock函式繪製60個時鐘表面的點,從頂部(12:00)開始,其中每個點離原點900單位,因此第一個點位於(0,900),此後的每個點按順時針依次增加6度。這些點中的l2個直徑為100個單位;其餘的為33個單位。使用Ellipse函式來畫點。

DrawHands函式繪製時鐘的時針、分針和秒針。定義錶針輪廓(當它們垂直向上時的形狀)的座標存放在一個POINT結構的陣列中。根據時間,這些座標使用RotatePoint函式進行旋轉,並用Windows的Polyline函式進行顯示。注意時針和分針只有當傳遞給DrawHands的bChange參數為TRUE時才被顯示。當程式更新時鐘的錶針時,大多數情況下時針和分針不需要重畫。

現在讓我們將注意力轉到視窗訊息處理程式。在WM_CREATE訊息處理期間,視窗訊息處理程式取得目前時間並將它存放在名為dtPrevious的變數中,這個變數將在以後被用於確定時針或者分針從上次更新以來是否改變過。

第一次繪製時鐘是在第一個WM_PAINT訊息處理期間,這只不過是依次呼叫SetIsotropic、 DrawClock和DrawHands,後者的bChange參數被設定為TRUE。

在WM_TIMER訊息處理期間,WndProc首先取得新的時間並確定是否需要重新繪製時針和分針。如果需要,則使用一個白色畫筆和上一次時間繪製所有的錶針,從而有效地擦除它們。否則,只對秒針使用白色畫筆進行擦除,然後,再使用一個黑色畫筆繪製所有的錶針。

以計時器進行狀態報告

本章的最後一個程式是我在第五章提到過的。它是一個使用GetPixel函式的好例子。

WHATCLR (見程式8-5)顯示了滑鼠游標下目前圖素的RGB顏色。

x' = x * cos (a) + y * sin (a) y' = y * cos (a) - x * sin (a)

圖8-2 CLOCK的螢幕顯示

程式8-5 WHATCLR WHATCLR.C /*-------------------------------------------------------------------------- WHATCLR.C -- Displays Color Under Cursor (c) Charles Petzold, 1998 ---------------------------------------------------------------------------*/ #include <windows.h> #define ID_TIMER 1 void FindWindowSize (int *, int *) ; LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("WhatClr") ; HWND hwnd ; int cxWindow, cyWindow ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) { MessageBox ( NULL, TEXT ("This program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } FindWindowSize (&cxWindow, &cyWindow) ; hwnd = CreateWindow (szAppName, TEXT ("What Color"), WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_BORDER, CW_USEDEFAULT, CW_USEDEFAULT, cxWindow, cyWindow, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; } void FindWindowSize (int * pcxWindow, int * pcyWindow) { HDC hdcScreen ; TEXTMETRIC tm ; hdcScreen = CreateIC (TEXT ("DISPLAY"), NULL, NULL, NULL) ; GetTextMetrics (hdcScreen, &tm) ; DeleteDC (hdcScreen) ; * pcxWindow = 2 * GetSystemMetrics (SM_CXBORDER) + 12 * tm.tmAveCharWidth ; * pcyWindow = 2 * GetSystemMetrics (SM_CYBORDER) + GetSystemMetrics (SM_CYCAPTION) + 2 * tm.tmHeight ; } LRESULT CALLBACK WndProc ( HWND hwnd, UINT message, WPARAM wParam,LPARAM lParam) { static COLORREF cr, crLast ; static HDC hdcScreen ; HDC hdc ; PAINTSTRUCT ps ; POINT pt ; RECT rc ; TCHAR szBuffer [16] ; switch (message) { case WM_CREATE: hdcScreen = CreateDC (TEXT ("DISPLAY"), NULL, NULL, NULL) ; SetTimer (hwnd, ID_TIMER, 100, NULL) ; return 0 ; case WM_TIMER: GetCursorPos (&pt) ; cr = GetPixel (hdcScreen, pt.x, pt.y) ; SetPixel (hdcScreen, pt.x, pt.y, 0) ; if (cr != crLast) { crLast = cr ; InvalidateRect (hwnd, NULL, FALSE) ; } return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; GetClientRect (hwnd, &rc) ; wsprintf (szBuffer, TEXT (" %02X %02X %02X "), GetRValue (cr), GetGValue (cr), GetBValue (cr)) ; DrawText (hdc, szBuffer, -1, &rc, DT_SINGLELINE | DT_CENTER | DT_VCENTER) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: DeleteDC (hdcScreen) ; KillTimer (hwnd, ID_TIMER) ; PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }

WHATCLR在WinMain中做了一點與以往不同的事。因為WHATCLR的視窗只需要顯示十六進位RGB值那麼大,所以它在CreateWindow函式中使用WS_BORDER視窗樣式建立了一個不能改變大小的視窗。要計算視窗的大小,WHATCLR通過先呼叫CreateIC再呼叫GetSystemMetrics以取得用於視訊顯示的裝置內容資訊。計算好的視窗寬度和高度值被傳遞給CreateWindow。

WHATCLR的視窗訊息處理程式在處理WM_CREATE訊息處理期間,呼叫CreateDC建立了用於整個視訊顯示的裝置內容。這個裝置內容在程式的生命週期內都有效。在處理WM_TIMER訊息處理期間,程式取得目前滑鼠游標位置的圖素。在處理WM_PAINT訊息處理期間顯示RGB顏色。

您可能想知道,從CreateDC函式中取得的裝置內容代號是否能讓您在螢幕的任意位置顯示一些東西,而不光只是取得圖素顏色。答案是可以的,一般而言,讓一個應用程式在另一個程式控制的畫面區域上畫圖是不好的,但在某些特殊情況下,這可能會非常有用。