作者:朱志强
本文通过一个快速启动屏幕保护程序的小程序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 something
break;
case WM_LBUTTONUP :
//Do something
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或以下标志的组合:
DON'T_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上下文菜单的菜单项。