使用Win32 SDK開發螢幕保護程式
暮鼓集 行走集
1.引言
螢幕保護程式(Screen Saver)的歷史幾乎與視窗作業系統的歷史一樣悠久,設計它的目的是為了保護CRT顯示器使其的使用壽命更長。但隨著技術的進步,新型的CRT顯示器及液晶顯示器已經無須這樣做了,不過螢幕保護程式並未消失,因為其絢麗的畫面成了人們彰顯個性的方式。
現在,逼真的水族箱,浩瀚的太空以及更吸引眼球的螢幕保護程式不段被開發出來。
如果你有興趣開發自己的螢幕保護程式,並且具有Windows程式的開發經驗,請深入閱讀本文,本文將提供使用Win32 Platform SDK開發螢幕保護程式的指引及範例。
2.螢幕保護程式是一種應用程式
螢幕保護程式雖然變化多端,但對開發者而言,它仍然是一種Windows應用程式,雖然螢幕保護程式的擴充套件檔名是scr,但在檔案格式上與exe可執行檔案是完全一樣的。
那麼,是不是隻要是Windows應用程式都可以作為螢幕保護程式呢?
我們做個一個實驗,將Windows自帶的“掃雷”程式檔案winmine.exe改成winmine.scr,接著我們開啟顯示器設定,選擇螢幕保護程式選項卡,可以發現winmine.scr已經被當成一個螢幕保護程式出現在列表裡。我們選中它,看看發生了甚麼--掃雷程式執行起來了!但是,隨後無論我們如何擺弄鍵盤和滑鼠,程式不會象其他螢幕保護程式那樣自動關閉。
所以,雖然螢幕保護程式一種應用程式,但是它與普通應用程式在人機介面上有所不同,我們可以歸納出以下幾點特徵:
作業系統自動執行程式 全螢幕的視窗 使用者輸入即退出 因此,只要我們在一個應用程式中實現這些特徵,那麼這個應用程式即可做為螢幕保護程式。
3.實現螢幕保護程式的基本特徵
3.1 作業系統自動執行程式
這實際上由系統完成,只要使用者將程式設定為系統的螢幕保護程式,那麼當用戶沒有輸入一段時間後,系統就會自動呼叫這個程式。因此,這不需要開發者操心。
3.2. 建立全螢幕的視窗
我們在建立Window時設定其座標及長寬引數即可實現這個目的:
CreateWindow( WNDCLASS_SSFRAME, NULL, WS_POPUP|WS_VISIBLE, 0, 0, GetSystemMetrics( SM_CXSCREEN ), GetSystemMetrics( SM_CYSCREEN ), HWND_DESKTOP, NULL, hInst, NULL);
注意,在這裡函式引數中的父視窗控制代碼傳入的是HWND_DESKTOP。
3.3. 使用者輸入即退出
使用者輸入的主要途徑是通過鍵盤或滑鼠,在Windows系統中,當鍵盤或滑鼠事件發生時,系統會產生一系列的訊息通知應用程式,具體有如下幾個:
WM_LBUTTONDOWN | 按下滑鼠左鍵 |
WM_MBUTTONDOWN | 按下滑鼠中鍵 |
WM_RBUTTONDOWN | 按下滑鼠右鍵 |
WM_MOUSEMOVE | 滑鼠移動 |
WM_KEYDOWN | 按下鍵盤鍵 |
WM_KEYUP | 擡起鍵盤鍵 |
WM_SYSKEYDOWN | 按下鍵盤系統鍵 |
(注: 系統鍵指Windows系統定義的用來啟用選單的F10鍵,以及按下Alt與其它鍵的組合鍵)。
因此,當螢幕保護程式收到這些訊息時,應立刻退出執行,在訊息處理函式新增如下的程式碼:
INT g_nOrigin_X = -1, g_nOrigin_Y = -1;
switch( message )
{
...
case WM_LBUTTONDOWN:
case WM_MBUTTONDOWN:
case WM_RBUTTONDOWN:
case WM_KEYDOWN:
case WM_SYSKEYDOWN:
PostQuitMessage( 0 ); // 退出程式
break;
case WM_MOUSEMOVE:
{
INT nNew_X = LOWORD(lParam);
INT nNew_Y = HIWORD(lParam);
if( g_nOrigin_X == -1 && g_nOrigin_Y == -1 )
{
g_nOrigin_X = nNew_X ;
g_nOrigin_Y = nNew_Y ;
}
else if( g_nOrigin_X != nNew_X && g_nOrigin_Y != nNew_Y )
PostQuitMessage( 0 );
}
...
}
請注意,程式對WM_MOUSEMOVE的訊息的處理有些特殊。
當應用程式開始之行時,系統會將當前的滑鼠座標用WM_MOUSEMOVE訊息通知應用程式。如果採用收到這個訊息就退出處理方法,就會導致程式剛執行就會退出。因此,通過使用兩個全域性變數g_nOrigin_X和g_nOrigin_Y來紀錄下初始化時的滑鼠位置值。當再次收到這一訊息時,通過比較當前滑鼠位置與紀錄的位置值可檢測出滑鼠有沒有被移動過,若檢測到則退出執行。
3.4 實現螢幕保護程式的畫面
這與普通應用程式是完全一樣的,在WM_PAINT裡運用GDI API實現你的想法。在本例中,我們什麼也不做,這就是簡單實現黑屏的效果。
按照上面的方法生成的螢幕保護程式已經有模有樣了。
4.成為真正的螢幕保護程式
儘管上述的應用程式已經很像一個螢幕保護程式了,但是,如果我們深入使用它,就會發現一些問題。
當我們把這個應用程式的副檔名改成scr並且設定它為系統的螢幕保護程式後,再次在控制面板中選擇顯示器設定並進入螢幕保護程式選項卡,發現及時我們沒有“測試”,它卻已經運行了。
這是因為,Windows系統會通過不同的命令列引數,以三種方式執行螢幕保護程式:
方式 | 命令列引數 |
---|---|
正常(由系統自動執行,全螢幕顯示 | /s |
配置(螢幕保護程式選項卡中的設定) | /c |
預覽(顯示在螢幕保護程式選項卡的小視窗中) | /p |
當我們一進入顯示器設定的螢幕保護程式選項卡,系統就會自動在小視窗中預覽當前選中的螢幕保護程式,即在命令列中傳入/p引數運行了程式。而我們的程式中並未處理命令列引數,所以出現了上述的問題。
4.1 處理命令列引數
程式如下
if( __argc > 1 )
{
if( strstr(__argv[1], "/p"))
{
if( __argc == 3 )
{
hFrameWnd = (HWND)atoi( __argv[2] );
GetClientRect( hFrameWnd, &rect );
CreateWindow( "ScreenSaverDraw", NULL,
WS_CHILD|WS_VISIBLE,
rect.left, rect.top,
rect.right, rect.bottom,
hFrameWnd, NULL,
hInst, NULL);
return 0;
}
}
else if(strchr(__argv[1], 'c') )
{
MessageBox( NULL, "This screen saver doesn't have configuration",
"Blank Screen Saver", MB_OK );
// Preview mode
return 0;
}
}
我們重點要處理的是/p和/c引數。
當系統傳入/p引數時,一定會將預覽視窗的控制代碼通過第二個命令列引數一併傳入。因此在程式中,可以通過
hFrameWnd = (HWND)atoi( __argv[2] );
來獲知預覽視窗控制代碼,此後可以在此視窗中顯示預覽介面。
當系統傳入/c引數時,通常我們要建立一個對話方塊,並在其中提供一些設定讓使用者對螢幕保護程式的外觀進行定製而使其更具個性。在本程式中,為了簡化,所以只其彈出一個對話方塊,告知使用者"This screen saver doesn’t have configuration”。
4.2 密碼驗證
如果在設定螢幕保護程式的時候選擇了密碼保護選項,則當螢幕保護程式退出前,應該顯示密碼驗證視窗。
應用程式如何獲知使用者有沒有選擇密碼保護? Windows會通過傳送訊息SCRM_VERIFYPW訊息來通知應用程式來進行保護。
我們可以定製一個對話方塊要求使用者輸入指定的密碼。 也可以通過Windows的Shell API使用共用的密碼介面,這會更加簡單可靠。
處理SCRM_VERIFYPW的程式碼:
case SCRM_VERIFYPW:
if( VerifyPassword(hWnd) )
PostQuitMessage(0);
break;
函式VerifyPassword的程式碼:
BOOL VerifyPassword( HWND hwnd )
{
// Under NT, we return TRUE immediately. This lets the saver quit,
// and the system manages passwords. Under '95, we call VerifyScreenSavePwd.
// This checks the appropriate registry key and, if necessary,
// pops up a verify dialog
OSVERSIONINFO osv;
HINSTANCE hpwdcpl;
typedef BOOL (WINAPI *VERIFYSCREENSAVEPWD)(HWND hwnd);
VERIFYSCREENSAVEPWD VerifyScreenSavePwd;
BOOL bRet;
osv.dwOSVersionInfoSize = sizeof(osv);
GetVersionEx( &osv );
if( osv.dwPlatformId == VER_PLATFORM_WIN32_NT )
return TRUE;
hpwdcpl = LoadLibrary( "PASSWORD.CPL" );
if( hpwdcpl == NULL )
return TRUE;
VerifyScreenSavePwd=(VERIFYSCREENSAVEPWD)GetProcAddress(hpwdcpl,"VerifyScreenSavePwd");
if (VerifyScreenSavePwd==NULL)
{
FreeLibrary( hpwdcpl );
return TRUE;
}
bRet = VerifyScreenSavePwd( hwnd );
FreeLibrary(hpwdcpl);
return bRet;
}
4.3 處理其它訊息
螢幕保護程式還必須處理如下系統訊息
WM_ACTIVATE
WM_ACTIVATEAPP
WM_NCACTIVATE
這是因為我們沒有處理WM_SYSCOMMAND訊息。當我們選擇最大化,最小化按鈕,選擇系統選單時等等,或者是應用程式就會收到這個訊息,
前三個訊息指示當程式視窗被系統或其它應用程式解除活動狀態時,應退出。(注,wParam為FALSE表示收到這是一個DEACTIVATE訊息。WM_SYSCOMMAND是系統通知訊息,wParam標示引數型別,SC_SCREENSAVE和SC_CLOSE表示系統要求執行預定的螢幕保護程式和結束程式,這兩種情況下,也應該結束程式的執行。
程式碼如下:
switch( message )
{
...
case WM_ACTIVATE:
case WM_ACTIVATEAPP:
case WM_NCACTIVATE :
if( wParam == FALSE )
PostQuitMessage( 0 );
break;
case WM_SYSCOMMAND:
if( wParam == SC_SCREENSAVE || wParam == SC_CLOSE )
PostQuitMessage( 0 );
break;
...
}
4.4 隱藏游標
由於螢幕保護程式執行過程中,不需要與使用者互動,因此滑鼠游標沒有必要顯示出來。隱藏滑鼠的游標可以通過處理WM_SETCURSOR訊息來實現。
case WM_SETCURSOR:
SetCursor( NULL );
return FALSE;
5.讓開發更簡單-Screen Saver程式庫
使用上述的方法所開發的應用程式已經具有螢幕保護程式的典型特徵了。我們可以瞭解,任何螢幕保護程式為了實現其共有特徵,必須對鍵盤,滑鼠和部分系統訊息進行相同的處理。
Microsoft考慮到了這一點,它對這些處理過程進行封裝,這就是Screen Saver程式庫scrnsave.lib。
在這個程式庫中,Mircosoft封裝了包括WinMain,註冊Window Class(視窗類),建立訊息迴圈等過程。開發者要做的只是完成名為”ScreenSaverProc”的訊息處理函式。
下面是用Screen Saver程式庫改寫的”全屏”程式的完整程式碼。
#include <windows.h>
#include <scrnsave.h>
LRESULT CALLBACK ScreenSaverProc( HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam )
{
switch( message )
{
case WM_PAINT:
{
PAINTSTRUCT ps;
HDC hdc;
RECT rect;
GetClientRect( hWnd, &rect );
hdc = BeginPaint(hWnd, &ps);
hBrush = CreateSolidBrush( RGB(0,0,0) );
FillRect( hdc, &rect, hBrush );
DeleteObject( hBrush );
EndPaint( hWnd, &ps );
}
break;
case SCRM_VERIFYPW:
ScreenSaverChangePassword( hWnd );
break;
}
defScreenSaverProc(hWnd, message, wParam, lParam);
}
BOOL WINAPI RegisterDialogClasses (HANDLE hInst)
{
return TRUE;
}
BOOL WINAPI ScreenSaverConfigureDialog (HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
switch( message )
{
case WM_INITDIALOG:
return FALSE;
}
return 0;
}
6.結束語
通過閱讀本文,相信讀者已經掌握了螢幕保護程式的開發方法。對於開發一般的螢幕保護程式,我建議使用Screen Saver程式庫進行開發,這樣,開發者可以將精力放在螢幕保護程式的內容的開發上,從而提高開發的效率。