本文通過一個快速啟動螢幕保護裝置程式的小程式SSLaunch,來介紹應用程式如何向工作列通知區加入表徵圖、如何禁止多個Win32執行個體以及螢幕保護裝置程式的有關內容。
SSLaunch用C語言編寫,用Visual C++ 5.0編譯,是一個基於無強制回應對話方塊的程式,同時禁止多個執行個體,即一次只能有一個執行個體運行。工作列通知區表徵圖在對話方塊初始化時加入,對話方塊響應程式定義的回調訊息,當滑鼠左鍵按下時,彈出一由螢幕保護裝置程式名填充的操作功能表。對話方塊關閉(即程式退出)時刪除工作列通知區表徵圖。如果讀者有興趣可以很容易地把它移植成基於 MFC 的程式。
1、工作列通知區
Windows 95的工作列中有一個通知區, 應用程式可以把一個表徵圖放入其中,以表示操作狀態,並可以有與之相關聯的工具用作說明控制。當滑鼠出現在此表徵圖的矩形邊界內時,向相應的應用程式發送應用程式定義的回調訊息。 應用程式通過發送訊息增加、修改、刪除工作列表徵圖。訊息的發送通過調用函數Shell_NotifyIcon來完成,如果調用成功,則返回TRUE;否則,返回FALSE。Shell_NotifyIcon函數原形如下:
WINSHELLAPI BOOL WINAPI Shell_NotifyIcon(
DWORD dwMessage, // 訊息標識符
PNOTIFYICONDATA pnid // NOTIFYICONDATA 結構
);
訊息標識符可以是 :
NIM_ADD 向工作列通知區加入表徵圖
NIM_DELETE 從工作列通知區刪除表徵圖
NIM_MODIFY 改變工作列通知區表徵圖
NOTIFYICONDATA 結構:
typedef struct _NOTIFYICONDATA {
DWORD cbSize;
HWND hWnd;
UINT uID;
UINT uFlags;
UINT uCallbackMessage;
HICON hIcon;
char szTip[64];
} NOTIFYICONDATA, *PNOTIFYICONDATA;
其中:
cbSize NOTIFYICONDATA 結構大小
hWnd 接收回調訊息視窗控制代碼
uID 工作列通知區表徵圖標識
uFlags 指定該結構中那些成員有效
uCallbackMessage 應用程式定義的回調訊息
hIcon 工作列通知區表徵圖控制代碼
szTip 工作列通知區提示字串
參數uFlags可以是下列值的組合:
NIF_ICON 工作列通知區表徵圖有效
NIF_MESSAGE 應用程式定義的回調訊息有效
NIF_TIP 工作列通知區提示字串有效
a.工作列通知區表徵圖的加入
BOOL SSLaunch_OnInitDialog(HWND hwnd, HWND hwndFocus, LPARAM lParam)
{
// Add an notification icon to the taskbar
NOTIFYCONDATA nid;
NOTIFYICONDATA nid;
nid.cbSize = sizeof(nid);
nid.hWnd = hwnd;
nid.uID = IDI_SSLAUNCH;
nid.uFlags = NIF_MESSAGE|NIF_ICON|NIF_TIP;
nid.uCallbackMessage = WM_SSLAUNCHICONNOTIFY;
nid.hIcon=LoadIcon(GetWindowInstance(hwnd),
KEINTRESOURCE(IDI_SSLAUNCH));
lstrcpyn(nid.szTip,g_szAppName,sizeof(nid.szTip) /sizeof(nid.szTip[0]));
return(Shell_NotifyIcon(NIM_ADD, &nid))
}
b.工作列通知區表徵圖的刪除
應用程式退出時,應該刪除任務通知區上相應的表徵圖:
void SSLaunch_OnDestroy(HWND hwnd)
{
// Remove the notification icon from the taskbar
NOTIFYICONDATA nid;
nid.cbSize = sizeof(nid);
nid.hWnd = hwnd;
nid.uID = IDI_SSLAUNCH;
Shell_NotifyIcon(NIM_DELETE, &nid);
}
c.應用程式定義回調訊息的接收
若為工作列通知區指定了回調訊息,則系統會於滑鼠事件在此地區發生時
嚮應用程式發送此訊息,其中wParam是工作列通知區表徵圖標識,lParam
是滑鼠事件發生後的滑鼠資訊。
void SSLaunch_OnIconNotify(WPARAM wParam, LPARAM lParam)
{
UINT uID = (UINT)wParam;
UINT uMsg = (UINT)lParam;
if(uID == IDI_SSLAUNCH){
switch(uMsg){
case WM_LBUTTONDOWN :
//Do somthing
break;
case WM_LBUTTONUP :
//Do somthing
break;
default :
break;
}
}
}
2.禁止多個Win32執行個體
在討論禁止多個Win32執行個體之前,我們先討論一下WinMain函數。我們知道,任何一個基於GDI的Windows程式以WinMain函數作為入口被系統調用。在Win16中,hPrevInstance指向前一個執行個體的控制代碼,但在Win32中,每一個進程都有一個獨立的4G地址空間,從0到2G屬於進程私人,對其他進程來說是不可見的。所以,在Win32中,hPrevInstance總是為NULL。
int WINAPI WinMain(
HINSTANCE hInstance, // handle to current instance
HINSTANCE hPrevInstance, // handle to previous instance
LPSTR lpCmdLine, // pointer to command line
int nCmdShow // show state of window
);
因而,在Win32下不能通過判斷hPrevInstance是否為NULL來判斷一個程式的另一個執行個體是否存在,要用其他的方法來判斷。
方法一
用FindWindow 函數尋找指定視窗,如果成功,則返回要找的視窗的控制代碼,否則返回NULL,由此可判斷是否有程式的另一個執行個體存在。
的程式碼片段示範如何使用FindWindow函數:
TCHAR szClassName[] = _TEXT("My Wnd Class");
TCHAR szWndName[] = _TEXT("My Wnd");
HWND hWnd = FindWindow(szClassName,szWndName);
if(hWnd){
MessageBox(NULL, _TEXT("Another Instance is already running."), _TEXT("Information"),
MB_OK | MB_ICONINFORMATION);
}
需要注意的是,很可能程式的各個執行個體有不同的視窗名,如果象下面這樣調用FindWindow
HWND hWnd = FindWindow(szClassName,NULL);
則尋找所有的視窗並匹配視窗類別名,如果你能保證你的視窗類別名是唯一的,那麼你可以信賴FindWindow,否則,你需要用更好的方法。
方法二
通過在EXE之間共用資料區段從而共用資料來判斷是否有程式的另一個執行個體存在。
每個EXE或DLL都是由段的集合組成,在Win32程式中,每個段以點(.)開頭。例如,當編譯器是編譯器時,則將所有代碼放入一個叫.text的段、將所有未初始化的資料放入.bss段、將所有初始化的資料放入.data段。
可以給每個段賦予一個或多個屬性(以下為常用的一些段屬性):
READ 段中的資料可讀
WRITE 段中的資料可寫
SHARED 段中的資料可被多個執行個體共用
EXECUTE 段中的資料可被執行
可以用以下指令產生段:
#pragma data_seg("Shared")
static LONG g_lInstanceCount = -1;
#pragma data_seg()
編譯器產生這段代碼時,產生一個新段,並把它所在#pragma data_seg("Shared")指令後的初始化資料放入新段Shared,未初始化的資料放入.bss段。#pragma data_seg()以後的資料放回預設資料區段。
僅告訴編譯器把特定資料放入自己的段內還不足以共用它們,還要告訴連結器在某一特定段內變數要共用。可以在連結時指定這個段的屬性。
/section:Shared,rws
段名 屬性
程式初始化時,例如調用WinMain函數時,調用InterlockedIncrement函數使共用段內變數加1,就可以通過判斷共用段內變數的值來判斷一個程式有幾個執行個體在運行。以下代碼示範了如何判斷一個正在啟動並執行程式執行個體是這個程式的第一個執行個體。
BOOL bIsFirstInstance = (InterlockedIncrement(&g_lInstanceCount) == 0);
if(!bIsFirstInstance){
MessageBox(NULL, _TEXT("Screen Saver Launcher is already running."), g_szAppName,
MB_OK | MB_ICONINFORMATION);
}
使共用段內變數加1,沒使用 g_lInstanceCount ++,而是使用InterlockedIncrement(&g_lInstanceCount),因為InterlockedIncrement函數對變數的訪問進行同步(Synchronize),阻止多個線程同時訪問同一個變數。有關線程同步的內容請參閱有關Win32 SDK的文檔。
禁止多個Win32執行個體的方法很多,如Win32核心對象(Mutex, Semaphore)、全域原子等都可以用來禁止多個Win32執行個體,在這裡我們只簡單地介紹以上兩種方法。
3.Screen Saver Launch:
螢幕保護裝置程式是以scr為副檔名的標準Windows可執行程式。當編輯可用螢幕保護裝置程式的列表時,Control Panel Desktop Applet在Windows啟動目錄(Windows目錄和系統目錄)下尋找副檔名是scr的基於Windows的可執行程式,如果Windows目錄和系統目錄下同時存在相同檔案名稱的螢幕保護裝置程式,則忽略Windows目錄下的那一個。 任何蓄意的搗亂(如將文字檔或是基於DOS的可執行檔副檔名改為scr)Window95都不予理睬,但是將標準Windows可執行程式的副檔名改為scr時,Windows95及NT將不會察覺。這隻是很極端的情況,相信使用者不會採用這種做法來"測試"你的Windows.
標準的基於Win32的螢幕保護裝置程式必須按照嚴格的標準編寫,有關詳細介紹請參閱有關Win32 SDK文檔。這裡需要提到的一點是所有的基於Win32的螢幕保護裝置程式都要求有一個不超過25個字元的說明字串。在螢幕保護裝置程式的資源字串表中,這個說明字串的標識必須是1。
但我們發現在Windows 95下的螢幕保護裝置程式不完全是嚴格按照標準編寫的,當編輯可用螢幕保護裝置程式的列表時,Control Panel Desktop Applet只是簡單地把螢幕保護裝置程式的檔案名稱加入列表,而不是加入上面提及的說明字串。而在Windows NT下,系統嚴格區分標準的和非標準的螢幕保護裝置程式。對於標準的螢幕保護裝置程式,系統取得它的說明字串並將其顯示在螢幕保護裝置程式的列表中;對於非標準的螢幕保護裝置程式,系統只把它的檔案名稱加入列表。
由於Windows 95和Windows NT下螢幕保護裝置程式的列表顯示略有不同,所以這裡分別加以說明。為區別起見,Windows 95下的SSLaunch用SSLaunch95表示,Windows NT下的SSLaunch用SSLaunchNT表示。
SSLaunch95 採用Window 95調用螢幕保護裝置程式的方法,在Windows95的啟動目錄下搜尋螢幕保護裝置程式,把檔案名稱加到工作列通知區表徵圖操作功能表中,單擊滑鼠即可啟動相應的螢幕保護裝置程式。Windows 95把使用者選中的螢幕保護裝置程式名儲存在 System.ini檔案中/boot/SCRNSAVE.EXE 下。SSLaunch95比較系統儲存的使用者選中的螢幕保護裝置程式名和搜尋到的螢幕保護裝置程式名,如果相同,則在工作列通知區表徵圖操作功能表的相應功能表項目設定檢查標誌,以表示這個螢幕保護裝置程式是否是目前使用者選中的。SSLaunch95沒有判斷Windows啟動目錄下的螢幕保護裝置程式是否是真正的螢幕保護裝置程式,因為Windows 95下的Win32不能輕易地判斷一個scr檔案是否是基於GDI的Windows可執行檔(NE 或PE格式)。作者找到了兩個可用於判斷檔案類型的函數:SHGetFileInfo,GetBinaryType。SHGetFileInfo可以判斷出.exe、.com、.bat幾種檔案類型,但認為.scr檔案不是可執行檔;GetBinaryType可以輕易地判斷出檔案類型,但Windows 95不支援,只是簡單地返回ERROR_NOT_IMPLEMENT,而Win32卻支援它。
點擊
SSLaunch95也可以在Windows NT 下運行,不過彈出的操作功能表不能用螢幕保護裝置程式說明字串填充,並且不能判斷scr是否是基於GDI的Windows可執行程式。
下面介紹SSLaunchNT在Windows NT下對scr檔案的判別,以及從scr檔案資源中取得螢幕保護裝置程式描述字串的方法。
a.對scr檔案的判別
Windows NT提供了對GetBinaryType函數的支援,因此,可用此函數判斷一個scr檔案是否是Windows可執行程式,並判斷出它是基於Win16還是 Win32的可執行程式。這一點很重要,因為,對基於Win32的scr檔案,我們在後面要取得它的字串資源中的一個重要訊息,及對螢幕保護裝置程式的描述字串。還應注意的是,lpApplicationName應給出全路徑,否則,它只在進程所在的路徑下尋找檔案,這樣會導致錯誤,從而不能返回在Windows啟動目錄下的.scr檔案的資訊。
BOOL GetBinaryType(
LPCTSTR lpApplicationName,
LPDWORD lpBinaryType
);
GetBinaryType調用成功後,lpBinaryType指向的DWORD返回以下值:
SCS_32BIT_BINARY 基於Win32的應用程式
SCS_DOS_BINARY 基於MS-DOS的應用程式
SCS_OS216_BINARY 基於16位OS/2的應用程式
SCS_PIF_BINARY MS-DOS應用程式的PIF 檔案
SCS_POSIX_BINARY 基於POSIX的應用程式
SCS_WOW_BINARY 基於16位Windows的應用程式
b.從scr檔案字串資源中取得螢幕保護裝置檔案描述字串
當我們判斷出了一個基於Win32的scr檔案後,就可以著手取得它的字串。在Win32中,有一種簡單有效方法:把一個EXE或DLL檔案以資料檔案方式載入,調用LoadLibraryEx函數。
HINSTANCE LoadLibraryEx(
LPCTSTR lpLibFileName, // EXE或DLL檔案名稱
HANDLE hFile, // 保留參數,必須為NULL
DWORD dwFlags // 函數入口標誌
);
dwFlags可以是0或以下標誌的組合:
DONT_RESOLVE_DLL_REFERENCES 系統將DLL映射到進程的地址空間而不調用DllMain函數。
LOAD_LIBRARY_AS_DATAFILE 系統將DLL象一個資料檔案那樣映射到進程的地址空間,而不調用DllMain函數。如果要取得EXE中的資源,也可調用LoadLibraryEx函數把EXE映射到進程地址空間。
LOAD_WITH_ALTERED_SEARCH_PATH 將改變LoadLibraryEx在定位DLL檔案時所採用的方法。
當以LOAD_LIBRARY_AS_DATAFILE的方式調用LoadLibraryEx時,系統只是簡單地建立一個檔案映象對象,把DLL(EXE)映射到本進程的地址空間,並不調用DllMain(WinMain)。如果調用成功,則函數返回一個HINSTANCE,即被映射到本進程地址空間的DLL(EXE)的裝入地址,這樣,就可以調用LoadString函數,從DLL(EXE)檔案的字串資源表中取得指定的字串。
點擊
這裡仍需指出的是,必須判斷LoadString函數調用是否成功,因為有些scr檔案(即使是基於Win32的)也有可能是非標準的(如Windows 95下的大多數scr檔案),如果LoadString調用失敗,則SSLaunchNT用檔案名稱取代scr的描述字串填入SSLaunchNT操作功能表的功能表項目。