第10章 其他常用Windows API编程知识
10.1 快捷方式
快捷方式是Windows提供的一种快速启动程序、打开文件或文件夹的方法,快捷方式的扩展名通常为.lnk,另外还有扩展名为.url的网页快捷方式等。
快捷方式的相关属性包括快捷方式指向的目标文件路径、快捷方式自身路径、起始位置(工作目录)、快捷键、运行方式(常规窗口、最小化或最大化)、备注(描述)、图标和命令行参数等。快捷键属性指的是为目标文件设置一个特定线程热键,按下快捷键时会打开目标文件。起始位置是指目标文件在运行过程中需要的一些工作文件例如配置文件、数据库文件或动态链接库等所在的目录,当程序需要的所有文件都在同一个目录中时,可以设置起始位置为程序所在目录,对于某些有特殊要求的程序,可能需要使用不在同一目录中的其他文件,在这种情况下可以在起始位置属性中指定这些文件所在的目录。默认情况下当鼠标悬停在一个快捷方式上时,会显示目标文件所在目录,用户可以通过备注属性设置为其他自定义字符串。
要创建一个.lnk快捷方式,可以使用COM库接口提供的相关函数,例如下面的自定义函数MyCreateShortcut:
/**************************************************************************
* 函数功能: 通过调用COM库接口函数创建程序快捷方式
* 输入参数的说明:
1. lpszDestFileName参数表示快捷方式指向的目标文件路径,必须指定
2. lpszShortcutFileName参数表示快捷方式的保存路径(扩展名为.lnk),必须指定
3. lpszWorkingDirectory参数表示起始位置(工作目录),如果设置为NULL表示程序所在目录
4. wHotKey参数表示快捷键,设置为0表示不设置快捷键
5. iShowCmd参数表示运行方式,可以设置为SW_SHOWNORMAL、SW_SHOWMINNOACTIVE或SW_SHOWMAXIMIZED
分别表示常规窗口、最小化或最大化,设置为0表示常规窗口
6. lpszDescription参数表示备注(描述),可以设置为NULL
* 注意:该函数需要使用tchar.h、Shlobj.h和strsafe.h头文件
**************************************************************************/
BOOL MyCreateShortcut(LPTSTR lpszDestFileName, LPTSTR lpszShortcutFileName,
LPTSTR lpszWorkingDirectory, WORD wHotKey, int iShowCmd, LPTSTR lpszDescription)
{
HRESULT hr;
if (lpszDestFileName == NULL || lpszShortcutFileName == NULL)
return FALSE;
// 初始化COM库
CoInitializeEx(NULL, 0);
// 创建一个IShellLink对象
IShellLink* pShellLink;
hr = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_SERVER, IID_IShellLink, (LPVOID*)
&pShellLink);
if (FAILED(hr))
return FALSE;
// 使用返回的IShellLink对象中的方法设置快捷方式的属性
// 目标文件路径
pShellLink->SetPath(lpszDestFileName);
// 起始位置(工作目录)
if (!lpszWorkingDirectory)
{
TCHAR szWorkingDirectory[MAX_PATH] = { 0 };
StringCchCopy(szWorkingDirectory, _countof(szWorkingDirectory), lpszDestFileName);
LPTSTR lpsz = _tcsrchr(szWorkingDirectory, TEXT('\\\\'));
*lpsz = TEXT('\\0');
pShellLink->SetWorkingDirectory(szWorkingDirectory);
}
else
{
pShellLink->SetWorkingDirectory(lpszWorkingDirectory);
}
// 快捷键(低字节表示虚拟键码,高字节表示修饰键)
if (wHotKey != 0)
pShellLink->SetHotkey(wHotKey);
// 运行方式
if (!iShowCmd)
pShellLink->SetShowCmd(SW_SHOWNORMAL);
else
pShellLink->SetShowCmd(iShowCmd);
// 备注(描述)
if (lpszDescription != NULL)
pShellLink->SetDescription(lpszDescription);
// 调用IShellLink的父类IUnknown中的QueryInterface方法获取IPersistFile对象
IPersistFile* pPersistFile;
hr = pShellLink->QueryInterface(IID_IPersistFile, (LPVOID*)&pPersistFile);
if (FAILED(hr))
{
pShellLink->Release();
return FALSE;
}
// 使用获取到的IPersistFile对象中的Save方法保存快捷方式到指定位置
pPersistFile->Save(lpszShortcutFileName, TRUE);
// 释放相关对象
pPersistFile->Release();
pShellLink->Release();
// 关闭COM库
CoUninitialize();
return TRUE;
}
10.2 程序开机自动启动
开机自动启动是程序设计中经常用到的技术,前面已经介绍了通过将程序的完整路径写入注册表Run子键的方式来实现开机自动启动,本节再介绍以下几种方法。
(1)把程序的快捷方式写入开机自动启动程序目录。
(2)创建任务计划实现开机自动启动。
(3)创建系统服务实现开机自动启动。
10.2.1 将程序的快捷方式写入开机自动启动程序目录
前面介绍过SHGetKnownFolderPath函数,通过该函数可以获取一些系统常用目录的完整路径,快速启动目录及其对应的GUID如下。
-
快速启动目录(所有用户):82A5EA35-D9CD-47C5-9629-E15D2F714E6E。
-
快速启动目录(当前用户):B97D20BB-F46A-4C97-BA10-5E3608430854。
把程序的快捷方式添加到快速启动目录后,操作系统启动时会自动加载相应的程序,实现开机自动启动。
我们可以调用前面介绍的自定义函数MyCreateShortcut创建快捷方式到快速启动目录,AutoRun_Shortcut.cpp源文件的内容如下:
#include <windows.h>
#include <tchar.h>
#include <Shlobj.h>
#include <strsafe.h>
#include "resource.h"
// 函数声明
INT_PTR CALLBACK DialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam);
BOOL MyCreateShortcut(LPTSTR lpszDestFileName, LPTSTR lpszShortcutFileName,
LPTSTR lpszWorkingDirectory, WORD wHotKey, int iShowCmd, LPTSTR lpszDescription);
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
DialogBoxParam(hInstance, MAKEINTRESOURCE(IDD_MAIN), NULL, DialogProc, NULL);
return 0;
}
INT_PTR CALLBACK DialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
GUID guid = { 0xB97D20BB, 0xF46A, 0x4C97, {0xBA, 0x10, 0x5E, 0x36, 0x08, 0x43, 0x08, 0x54} };
LPTSTR lpStrStartup; // 返回当前用户的开机自动启动程序目录
TCHAR szDestFileName[MAX_PATH] = { 0 }; // 可执行文件完整路径
TCHAR szFileName[MAX_PATH] = { 0 }; // 可执行文件名称
TCHAR szShortcutFileName[MAX_PATH] = { 0 }; // 快捷方式的保存路径
switch (uMsg)
{
case WM_COMMAND:
switch (LOWORD(wParam))
{
case IDC_BTN_OK:
// 获取当前用户的开机自动启动程序目录
SHGetKnownFolderPath(guid, 0, NULL, &lpStrStartup);
// 获取当前进程的可执行文件完整路径
GetModuleFileName(NULL, szDestFileName, _countof(szDestFileName));
// 拼凑快捷方式的保存路径
// 开机自动启动程序目录后面加一个反斜杠
StringCchCopy(szShortcutFileName, _countof(szShortcutFileName), lpStrStartup);
if (szShortcutFileName[_tcslen(szShortcutFileName) – 1] != TEXT('\\\\'))
StringCchCat(szShortcutFileName, _countof(szShortcutFileName), TEXT("\\\\"));
// 可执行文件名称.lnk
StringCchCopy(szFileName, _countof(szFileName), _tcsrchr(szDestFileName, TEXT('\\\\')) + 1);
*(_tcsrchr(szFileName, TEXT('.')) + 1) = TEXT('\\0');
StringCchCat(szFileName, _countof(szFileName), TEXT("lnk"));
// 开机自动启动程序目录\\可执行文件名称.lnk
StringCchCat(szShortcutFileName, _countof(szShortcutFileName), szFileName);
// 调用自定义函数MyCreateShortcut创建快捷方式
MyCreateShortcut(szDestFileName, szShortcutFileName, NULL, 0, 0, NULL);
CoTaskMemFree(lpStrStartup);
break;
case IDCANCEL:
EndDialog(hwndDlg, 0);
break;
}
return TRUE;
}
return FALSE;
}
完整代码参见Chapter10\\AutoRun_Shortcut项目。
10.2.2 创建任务计划实现开机自动启动
通过任务计划可以将任何脚本、程序或文档安排在某个时间运行,单击桌面右下角的开始菜单→ 计算机管理(本地)→系统工具→任务计划程序,或者用鼠标右键单击桌面的此计算机,然后选择管理→计算机管理(本地)→系统工具→任务计划程序,打开任务计划程序,即可创建基本任务或创建任务。通过“创建基本任务”向导可以快速地为常见任务创建计划,如果需要更多高级选项或设置,例如多任务操作或触发器,则可以使用“创建任务”命令。
触发器设置为当前用户登录时,启动指定目录中的程序,即可实现开机自动启动。要编程实现创建任务计划,需要使用COM库接口提供的相关函数。任务计划是Windows提供的一个强大功能,本节不列出相关示例程序源码,示例代码请参考Chapter10\\AutoRun_Task项目,AutoRun_Task项目实现了创建任务计划和删除任务计划两个自定义函数,读者可以直接使用,也可以根据需要进行个性化修改。
10.2.3 创建系统服务实现开机自动启动
系统服务也是一种应用程序类型,它在后台运行,服务程序没有窗口、对话框等用户界面,服务程序可以在本地或通过网络为用户提供一些功能。服务程序可以在系统引导时自动启动,也可以由用户手动启动,即使没有用户登录到系统,服务程序也可以运行。
在Windows操作系统的早期版本中,所有服务都与登录的第一个用户在同一会话中运行,该会话称为会话0,在会话0中一起运行服务和用户应用程序会带来安全风险,因为服务以提升的特权运行,经常会被试图提高自身特权级别的恶意程序利用。在Windows Vista操作系统及更高版本系统中,采用了会话0隔离机制,只有系统进程、服务和驱动程序在会话0中运行,第一个登录的用户连接到会话1,第二个登录的用户连接到会话2,以此类推,服务永远不会与用户的应用程序在同一会话中运行,这就避免了来自用户应用程序的攻击。
服务程序无法向用户应用程序发送消息,用户应用程序也无法向服务程序发送消息,服务程序无法直接显示用户界面例如对话框,但是可以通过使用WTSSendMessage函数在另一个会话中显示消息框,如果需要实现更复杂的用户界面,可以使用CreateProcessAsUser函数在用户会话中创建一个进程。微软公司建议使用客户机-服务器机制,例如远程过程调用RPC或命名管道在服务和用户应用程序之间进行通信。
1.实现一个系统服务管理器
单纯讲解如何创建一个开机自动启动的服务程序无法理解系统服务的工作原理,本节实现一个系统服务管理器程序ServiceManager,该程序的功能与用鼠标右键单击桌面的此计算机,然后选择管理→计算机管理(本地)→服务和应用程序→服务,所打开的系统提供的服务管理程序(services.msc)功能类似。ServiceManager程序界面如原书的图10.1所示。
首先,枚举服务控制管理器数据库中的服务,并显示每个服务的显示名称、服务状态(正在运行、已停止等)、启动类型(手动、自动和禁用等)、服务程序所在的文件路径和服务描述。这里不再详细介绍每个函数的具体用法。
OpenSCManager函数用于建立到指定计算机上的服务控制管理器的连接,并打开指定的服务控制管理器数据库,如果函数执行成功,则返回指定的服务控制管理器数据库的句柄;如果函数执行失败,则返回值为NULL。当不再需要服务控制管理器数据库句柄时可以调用CloseServiceHandle函数关闭服务句柄。
利用服务控制管理器数据库的句柄,可以通过调用EnumServicesStatusEx函数枚举服务控制管理器数据库中的服务,该函数返回一个ENUM_SERVICE_STATUS_PROCESS结构数组,每个数组元素对应着一个服务的信息,包括服务的服务名称、显示名称、当前状态(正在运行、已停止等)和服务的进程ID等。ENUM_SERVICE_STATUS_PROCESS结构的定义如下:
typedef struct _ENUM_SERVICE_STATUS_PROCESS {
LPTSTR lpServiceName; // 服务控制管理器数据库中服务的服务名称
LPTSTR lpDisplayName; // 服务控制程序用来标识服务的显示名称
SERVICE_STATUS_PROCESS ServiceStatusProcess;// 该结构包含服务类型、服务当前状态、服务的进程ID等
} ENUM_SERVICE_STATUS_PROCESS, * LPENUM_SERVICE_STATUS_PROCESS;
ENUM_SERVICE_STATUS_PROCESS结构中的SERVICE_STATUS_PROCESS结构的定义如下:
typedef struct _SERVICE_STATUS_PROCESS {
DWORD dwServiceType; // 服务类型
DWORD dwCurrentState; // 服务的当前状态
DWORD dwControlsAccepted; // 服务可以接受并在其处理函数中处理的控制代码
DWORD dwWin32ExitCode; // 服务用于报告启动或停止时发生的错误的错误代码
DWORD dwServiceSpecificExitCode; // 服务启动或停止时如果发生错误,服务将返回服务的特定错误代码
DWORD dwCheckPoint; // 用于跟踪服务的操作(启动、停止、暂停或继续)进度
DWORD dwWaitHint; // 启动、停止、暂停或继续操作所需的估计时间
DWORD dwProcessId; // 服务的进程ID
DWORD dwServiceFlags; // 服务是否在系统进程中运行
} SERVICE_STATUS_PROCESS, * LPSERVICE_STATUS_PROCESS;
- SERVICE_STATUS_PROCESS结构的dwCurrentState字段表示服务的当前状态,可以是表10.1所示的值之一。
表10.1
| SERVICE_START_PENDING | 0x00000002 | 服务正在启动 |
| SERVICE_RUNNING | 0x00000004 | 服务正在运行 |
| SERVICE_STOP_PENDING | 0x00000003 | 服务正在停止 |
| SERVICE_STOPPED | 0x00000001 | 服务已停止 |
| SERVICE_PAUSE_PENDING | 0x00000006 | 服务正在暂停 |
| SERVICE_PAUSED | 0x00000007 | 服务已暂停 |
| SERVICE_CONTINUE_PENDING | 0x00000005 | 服务正在继续 |
- SERVICE_STATUS_PROCESS结构的dwControlsAccepted字段表示服务可以接受并在其处理函数中处理的控制代码。如果需要控制服务的当前状态,就需要查询该字段的值以确定是否可以停止、暂停等,常用的值如表10.2所示。
表10.2
| SERVICE_ACCEPT_STOP | 0x00000001 | 服务可以停止,此控制代码允许服务接收SERVICE_CONTROL_STOP通知 |
| SERVICE_ACCEPT_PAUSE_CONTINUE | 0x00000002 | 服务可以暂停并继续,此控制代码允许服务接收SERVICE_CONTROL_PAUSE和SERVICE_CONTROL_CONTINUE通知 |
ENUM_SERVICE_STATUS_PROCESS结构不包含服务的启动类型(手动、自动和禁用等),程序可以通过调用OpenService函数打开服务(第1个参数指定为服务控制管理器数据库句柄,第2个参数指定为服务名称),函数执行成功则返回一个服务句柄,函数执行失败则返回值为NULL。当不再需要的时候可以调用CloseServiceHandle函数关闭服务句柄。
有了具体某个服务的句柄,可以通过调用QueryServiceConfig函数查询该服务的配置参数,该函数返回一个QUERY_SERVICE_CONFIG结构,包括服务的启动类型、文件路径等,结构定义如下:
typedef struct _QUERY_SERVICE_CONFIG {
DWORD dwServiceType; // 服务类型
DWORD dwStartType; // 何时启动服务,即启动类型(手动、自动和禁用等)
DWORD dwErrorControl; // 当服务无法启动时采取的措施
LPTSTR lpBinaryPathName; // 服务的文件路径
LPTSTR lpLoadOrderGroup; // 服务所属的加载顺序组的名称
DWORD dwTagId; // 在lpLoadOrderGroup参数指定的组中此服务的唯一ID
LPTSTR lpDependencies; // 服务或加载顺序组的名称的数组指针
LPTSTR lpServiceStartName; // 服务进程在运行时将登录的账户名称
LPTSTR lpDisplayName; // 服务的显示名称
} QUERY_SERVICE_CONFIG, * LPQUERY_SERVICE_CONFIG;
QUERY_SERVICE_CONFIG结构的dwStartType字段表示启动类型,可以是表10.3所示的值之一。
表10.3
| SERVICE_BOOT_START | 0x00000000 | 自动启动(用于驱动程序服务) |
| SERVICE_SYSTEM_START | 0x00000001 | 手动启动(用于驱动程序服务) |
| SERVICE_AUTO_START | 0x00000002 | 自动启动 |
| SERVICE_DEMAND_START | 0x00000003 | 手动启动 |
| SERVICE_DISABLED | 0x00000004 | 禁用 |
如果有部分服务的描述信息未获取,可以通过调用QueryServiceConfig2函数查询服务的其他配置参数,该函数的第2个参数指定为 SERVICE_CONFIG_DESCRIPTION可以获取服务的描述信息(返回一个SERVICE_DESCRIPTION结构),该结构定义如下:
typedef struct _SERVICE_DESCRIPTION {
LPTSTR lpDescription; // 服务的描述字符串
} SERVICE_DESCRIPTION, * LPSERVICE_DESCRIPTION;
列表视图控件的LVS_SORTASCENDING和LVS_SORTDESCENDING样式只可以根据列表项文本进行排序,对于报表视图不能简单地通过指定这两个样式来达到对列表项进行排序的目的,因为这会导致子项不能正确显示。要对报表视图进行排序可以发送LVM_SORTITEMS消息,通过该消息可以根据列表项文本或任何子项进行排序。
对于报表视图,当用户单击列标题时会收到包含LVN_COLUMNCLICK通知码的WM_NOTIFY消息,消息的lParam参数是一个指向NMLISTVIEW结构的指针,NMLISTVIEW.iSubItem字段是列的索引,这时程序可以通过发送LVM_SORTITEMS消息根据所单击的列进行排序。
LVM_SORTITEMS消息的lParam参数是一个指向应用程序定义的比较函数的指针,系统会调用该回调函数对列表视图控件中的所有列表项进行排序,回调函数定义格式如下:
int CALLBACK CompareFunc(LPARAM lParam1, LPARAM lParam2, LPARAM lParamSort);
-
参数lParam1是参与比较的第1个列表项对应的项目数据,参数lParam2是参与比较的第2个列表项对应的项目数据。项目数据是插入列表项时在LVITEM结构的lParam字段中指定的值。
-
lParamSort参数是LVM_SORTITEMS消息的wParam参数传递过来的程序自定义数据。
如果第1项在第2项之前,回调函数应该返回负值;如果第1项在第2项之后,回调函数应该返回正值;如果两项相等,则回调函数应该返回零。
如果要按服务的显示名称进行排序,则插入列表项时应该把显示名称的地址作为项目数据;如果要按服务的当前状态进行排序,则插入列表项时应该把服务的当前状态作为项目数据。本例要实现用户单击服务的显示名称、服务状态和启动类型时根据所单击的列进行排序,因此应该使用一个自定义结构作为项目数据:
typedef struct _ITEMDATA
{
TCHAR m_szServiceName[256]; // 服务名称
TCHAR m_szDisplayName[256]; // 显示名称
DWORD m_dwCurrentState; // 服务状态
DWORD m_dwControlsAccepted; // 控制代码
DWORD m_dwStartType; // 启动类型
}ITEMDATA, * PITEMDATA;
后期需要使用服务名称打开服务,如果需要控制服务的当前状态就要先确定服务的控制代码,因此增加了m_szServiceName和m_dwControlsAccepted两个字段。
当用户单击列标题时,我们处理WM_NOTIFY消息的LVN_COLUMNCLICK通知码,把((LPNMLISTVIEW) lParam)->iSubItem字段作为LVM_SORTITEMS消息的wParam参数,发送LVM_SORTITEMS消息,然后在回调函数CompareFunc中根据所单击的列取出项目数据的相应字段进行排序即可。
至此,ServiceManager程序的界面显示工作已经完成,为了防止程序源代码过长不易阅读,建议读者先阅读并理解Chapter10\\ServiceManager项目,各个菜单功能的实现参见Chapter10\\ServiceManager2项目。
2.控制服务状态
本节将实现启动服务、停止服务、暂停服务和继续服务菜单项的功能。前面我们已经在项目数据中保存了每个服务的当前状态和控制代码,程序应该根据服务的当前状态决定启用还是禁用上述菜单项,同时应该确定服务的控制代码是否支持停止服务、暂停服务和继续服务。本程序在WM_INITMENUPOPUP消息中调用了自定义函数QueryServiceStatusAndConfig,在显示弹出菜单前,获取选中服务的当前状态和配置参数,启用禁用相关菜单项。
QueryServiceStatusEx函数用于获取指定服务的当前状态信息,包括服务类型、当前状态、服务的进程ID等,该函数返回一个SERVICE_STATUS_PROCESS结构(同EnumServicesStatusEx函数返回的ENUM_SERVICE_STATUS_PROCESS结构中的SERVICE_STATUS_PROCESS结构),本程序在调用自定义函数GetServiceList获取服务列表时已经保存了每个服务的当前状态和控制代码,因此不需要额外调用QueryServiceStatusEx函数。
要启动指定的服务可以调用StartService函数:
BOOL WINAPI StartService(
_In_ SC_HANDLE hService, // 服务句柄,必须具有SERVICE_START访问权限
_In_ DWORD dwNumServiceArgs, // lpServiceArgVectors数组中的数组元素个数
_In_opt_ LPCTSTR* lpServiceArgVectors); // 传递给服务的ServiceMain函数的参数数组
如果函数执行成功,则返回值为非零;如果函数执行失败,则返回值为0。
ControlService函数用于向指定的服务发送控制代码,例如停止服务、暂停服务、继续服务:
BOOL WINAPI ControlService(
_In_ SC_HANDLE hService, // 服务句柄
_In_ DWORD dwControl, // 控制代码
_Out_ LPSERVICE_STATUS lpServiceStatus); // 返回最新服务状态信息的SERVICE_STATUS结构
dwControl参数指定控制代码,常用的值如表10.4所示。
表10.4
| SERVICE_CONTROL_STOP | 0x00000001 | 通知服务停止服务,hService句柄须具有SERVICE_STOP访问权限 |
| SERVICE_CONTROL_PAUSE | 0x00000002 | 通知服务暂停服务,hService句柄须具有SERVICE_PAUSE_CONTINUE访问权限 |
| SERVICE_CONTROL_CONTINUE | 0x00000003 | 通知暂停的服务应恢复(继续服务),hService句柄须具有SERVICE_PAUSE_CONTINUE访问权限 |
| SERVICE_CONTROL_SHUTDOWN | 0x00000005 | 通知服务系统正在关闭 |
如果函数执行成功,则返回值为非零;如果函数执行失败,则返回值为0。
对于启动服务,本程序定义了自定义函数StartTheService,对于停止服务、暂停服务、继续服务,程序定义了自定义函数ControlCurrentState。
3.改变服务启动类型
如前所述,调用QueryServiceConfig函数可以查询指定服务的配置参数,该函数返回一个QUERY_SERVICE_CONFIG结构,包括服务的启动类型、文件路径等,调用QueryServiceConfig2函数可以查询指定服务的其他配置参数。与之对应的,要更改指定服务的配置参数可以调用ChangeServiceConfig函数,要更改指定服务的其他配置参数可以调用ChangeServiceConfig2函数。ChangeServiceConfig函数声明如下:
BOOL WINAPI ChangeServiceConfig(
_In_ SC_HANDLE hService, // 服务句柄,必须具有SERVICE_CHANGE_CONFIG访问权限
_In_ DWORD dwServiceType, // 服务类型,如果不需要更改可以设置为SERVICE_NO_CHANGE
_In_ DWORD dwStartType, // 启动类型,如果不需要更改可以设置为SERVICE_NO_CHANGE
_In_ DWORD dwErrorControl, // 错误控制,如果不需要更改可以设置为SERVICE_NO_CHANGE
_In_opt_ LPCTSTR lpBinaryPathName, // 服务的文件路径
_In_opt_ LPCTSTR lpLoadOrderGroup, // 服务所属的加载顺序组的名称
_Out_opt_ LPDWORD lpdwTagId, // 返回在lpLoadOrderGroup参数指定的组中此服务的唯一ID
_In_opt_ LPCTSTR lpDependencies, // 服务或加载顺序组的名称的数组指针
_In_opt_ LPCTSTR lpServiceStartName, // 服务进程在运行时将登录的账户名称
_In_opt_ LPCTSTR lpPassword, // lpServiceStartName参数指定的账户名的密码
_In_opt_ LPCTSTR lpDisplayName); // 服务的显示名称
如果函数执行成功,则返回值为非零;如果函数执行失败,则返回值为0。
对于设为自动启动、设为手动启动、设为已禁用这些菜单项,本程序定义了自定义函数ChangeTheServiceConfig。
4.添加和删除服务
CreateService函数用于创建一个服务对象,并将其添加到服务控制管理器数据库中:
SC_HANDLE WINAPI CreateService(
_In_ SC_HANDLE hSCManager, // SCM数据库句柄,须具有SC_MANAGER_CREATE_SERVICE权限
_In_ LPCTSTR lpServiceName, // 服务名称,最大长度为256个字符
_In_opt_ LPCTSTR lpDisplayName, // 显示名称,最大长度为256个字符
_In_ DWORD dwDesiredAccess, // 服务的访问权限,可以指定为SERVICE_ALL_ACCESS
_In_ DWORD dwServiceType, // 服务类型,通常指定为SERVICE_WIN32_OWN_PROCESS
_In_ DWORD dwStartType, // 启动类型,可以指定为SERVICE_AUTO_START自动启动
_In_ DWORD dwErrorControl, // 错误控制,可以设置为SERVICE_ERROR_NORMAL
_In_opt_ LPCTSTR lpBinaryPathName, // 服务的文件路径,如果路径包含空格则必须使用引号引起来
_In_opt_ LPCTSTR lpLoadOrderGroup, // 服务所属的加载排序组的名称,可为NULL
_Out_opt_ LPDWORD lpdwTagId, // 返回在lpLoadOrderGroup组中此服务的唯一ID,可为NULL
_In_opt_ LPCTSTR lpDependencies, // 服务或加载顺序组的名称的数组指针,可为NULL
_In_opt_ LPCTSTR lpServiceStartName, // 服务进程在运行时将登录的账户名称,可为NULL
_In_opt_ LPCTSTR lpPassword); // lpServiceStartName参数指定的账户名的密码,可为NULL
如果函数执行成功,则返回值是服务的句柄;如果函数执行失败,则返回值为NULL。
要创建一个服务,需要指定文件路径、服务名称、显示名称、启动类型和服务描述等,因此在用户单击添加服务菜单项时,应该调用DialogBoxParam函数弹出一个对话框供用户输入上述信息(见原书的图10.2)。
用户单击“创建服务”按钮,调用自定义函数CreateAService创建服务,如果创建服务成功,则创建服务对话框的窗口过程返回2(EndDialog(hwndDlg, 2)),如果创建服务失败,则返回1(EndDialog(hwndDlg, 1)),然后主程序可以根据创建服务对话框的返回值以显示创建服务成功还是失败。
DeleteService函数用于将指定的服务从服务控制管理器数据库中删除:
BOOL DeleteService(_In_ SC_HANDLE hService); // 服务句柄,必须具有DELETE访问权限
如果指定的服务正在运行,可以通过调用ControlService函数来停止正在运行的服务(SERVICE_CONTROL_STOP控制代码),然后再删除。
关于删除服务菜单项,本程序定义了自定义函数DeleteTheService。
5.编写服务程序
前面多次提到过服务类型的概念,例如EnumServicesStatusEx函数返回的ENUM_SERVICE_STATUS_PROCESS结构中的SERVICE_STATUS_PROCESS结构中的dwServiceType字段,QueryServiceConfig函数返回的QUERY_SERVICE_CONFIG结构中的dwServiceType字段,ChangeServiceConfig函数的dwServiceType参数,CreateService函数的dwServiceType参数等,服务类型可以是表10.5所示的值之一。
一个服务程序可以包含一个或多个服务,使用SERVICE_WIN32_OWN_PROCESS类型创建的服务程序仅包含一个服务,使用SERVICE_WIN32_SHARE_PROCESS类型创建的服务程序可以包含多个服务(多个服务之间可以共享代码)。
表10.5
| SERVICE_KERNEL_DRIVER | 0x00000001 | 驱动程序服务 |
| SERVICE_FILE_SYSTEM_DRIVER | 0x00000002 | 文件系统驱动程序服务 |
| SERVICE_WIN32_OWN_PROCESS | 0x00000010 | 在自己的进程中运行的服务 |
| SERVICE_WIN32_SHARE_PROCESS | 0x00000020 | 与一个或多个其他服务共享一个进程的服务 |
当一个应用程序需要常驻在系统,或者随时为其他应用程序提供服务时,可以使用服务程序,服务程序没有窗口、对话框这些用户界面,因此服务通常被编写为控制台应用程序。服务程序不同于一般的可执行程序,编写服务程序需要遵循一定的规范。
当服务控制管理器(SCM)启动服务程序时,它将等待服务程序调用StartServiceCtrlDispatcher函数,StartServiceCtrlDispatcher函数用于将调用线程连接到服务控制管理器,从而使该线程成为服务控制调度线程:
BOOL WINAPI StartServiceCtrlDispatcher(
_In_ const SERVICE_TABLE_ENTRY* lpServiceTable); // 指向SERVICE_TABLE_ENTRY结构数组的指针
lpServiceTable参数是一个指向SERVICE_TABLE_ENTRY结构数组的指针,每个SERVICE_TABLE_ENTRY结构包含一个服务的服务名称和服务入口点函数ServiceMain的指针,结构数组的最后一个数组元素应以空结构结尾。SERVICE_TABLE_ENTRY结构的定义如下:
typedef struct _SERVICE_TABLE_ENTRY {
LPTSTR lpServiceName; // 服务名称
LPSERVICE_MAIN_FUNCTION lpServiceProc; // 服务入口点函数指针
} SERVICE_TABLE_ENTRY, * LPSERVICE_TABLE_ENTRY;
如果函数执行成功,则返回值为非零;如果函数执行失败,则返回值为0。函数执行成功后会将调用线程连接到服务控制管理器,从而使该线程成为服务控制调度线程,服务控制调度线程不会返回,直到服务进程中所有的服务都停止(SERVICE_STOPPED状态)。
SCM通过命名管道向服务控制调度线程发送控制请求,服务控制调度线程执行以下任务。
(1)创建新线程以在启动新服务时调用对应的服务入口点函数ServiceMain。
(2)调用服务对应的处理函数来处理服务控制请求(每个服务都有一个服务控制处理函数HandlerEx)。
服务入口点函数ServiceMain(也可以是其他名称)的定义格式如下:
VOID WINAPI ServiceMain(_In_ DWORD dwArgc, _In_ LPTSTR* lpszArgv);
lpszArgv参数是一个参数字符串数组,该参数数组是在调用StartService函数启动服务时传递过来的,第一个参数lpszArgv[0]是服务名称,后面是其他参数(lpszArgv[1]~lpszArgv[dwArgc-1]),如果没有参数,则lpszArgv参数为NULL。dwArgc参数表示lpszArgv数组中的参数个数。
当服务控制程序请求启动一个服务时,SCM将启动请求发送到服务控制调度线程,服务控制调度线程会创建一个新线程以执行该服务的ServiceMain函数。ServiceMain函数应执行以下任务。
(1)初始化全局变量。
(2)立即调用RegisterServiceCtrlHandlerEx函数以注册一个服务控制处理函数HandlerEx来处理对该服务的控制请求,该函数返回一个服务状态句柄hServiceStatus。
(3)执行初始化工作。如果初始化代码的执行时间很短(少于1秒),可以直接在ServiceMain函数中执行初始化。如果预计初始化时间将超过1秒,则服务应使用以下初始化技术之一。
-
调用SetServiceStatus函数,该函数需要一个服务状态句柄hServiceStatus和一个SERVICE_STATUS结构,把SERVICE_STATUS.dwCurrentState设置为SERVICE_RUNNING,SERVICE_STATUS.dwControlsAccepted设置为0,然后调用SetServiceStatus函数,这表示服务正在运行,但不接受任何控制请求,这样一来SCM就可以去管理其他服务,而不是一直等待服务初始化完成。建议使用这种初始化方法来提高性能,尤其是对于自动启动服务。
-
把SERVICE_STATUS.dwCurrentState设置为SERVICE_START_PENDING,SERVICE_STATUS. dwControlsAccepted设置为0,然后调用SetServiceStatus函数,这表示服务正在启动中,不接受任何控制请求,启动该服务的程序可以调用QueryServiceStatusEx函数从SCM获取最新的检查点值,并使用该值向用户报告进度。
(4)初始化完成后,调用SetServiceStatus函数将服务状态设置为SERVICE_RUNNING并指定服务可以接受的控制请求。
(5)执行服务任务。
RegisterServiceCtrlHandlerEx函数用于注册一个服务控制处理函数HandlerEx来处理对服务的控制请求:
SERVICE_STATUS_HANDLE WINAPI RegisterServiceCtrlHandlerEx(
_In_ LPCTSTR lpServiceName, // 服务名称
_In_ LPHANDLER_FUNCTION_EX lpHandlerProc, // 服务控制处理函数的指针
_In_opt_ LPVOID lpContext); // 用户自定义数据
lpServiceName参数指定服务名称;lpContext参数指定用户自定义数据,当多个服务共享一个进程时,可以通过该参数确定是哪个服务;lpHandlerProc参数指定服务控制处理函数HandlerEx的指针。如果函数执行成功,则返回值是服务状态句柄;如果函数执行失败,则返回值为0。
服务控制处理函数HandlerEx(也可以是其他名称)的定义格式如下:
DWORD WINAPI HandlerEx(
_In_ DWORD dwControl, // 控制代码
_In_ DWORD dwEventType, // 发生的事件类型
_In_ LPVOID lpEventData, // 事件数据,该数据的格式取决于dwControl和dwEventType参数的值
_In_ LPVOID lpContext); // 从RegisterServiceCtrlHandlerEx函数传递过来的用户自定义数据
服务控制处理函数应该根据传递过来的控制代码执行对应的任务,并调用SetServiceStatus函数以将其新的服务状态报告给SCM,处理完控制请求后函数可以返回NO_ERROR(0)。
接下来编写一个服务程序MyService,控制台程序,MyService.cpp源文件的内容如下:
#include <Windows.h>
#include <tchar.h>
// 服务入口点函数
VOID WINAPI ServiceMain(DWORD dwArgc, LPTSTR* lpszArgv);
// 服务控制处理函数
DWORD WINAPI HandlerEx(DWORD dwControl, DWORD dwEventType, LPVOID lpEventData, LPVOID lpContext);
// 全局变量
TCHAR g_szServiceName[] = TEXT("MyService"); // 服务名称
SERVICE_STATUS_HANDLE g_hServiceStatus; // 服务状态句柄
SERVICE_STATUS g_serviceStatus = { 0 }; // 服务状态结构
int _tmain(int argc, TCHAR* argv[])
{
const SERVICE_TABLE_ENTRY serviceTableEntry[] = { {g_szServiceName, ServiceMain}, {NULL, NULL} };
// 将调用线程连接到SCM,从而使该线程成为服务控制调度线程
StartServiceCtrlDispatcher(serviceTableEntry);
return 0;
}
VOID WINAPI ServiceMain(DWORD dwArgc, LPTSTR* lpszArgv)
{
// 注册一个服务控制处理函数HandlerEx,该函数返回一个服务状态句柄hServiceStatus
g_hServiceStatus = RegisterServiceCtrlHandlerEx(g_szServiceName, HandlerEx, NULL);
g_serviceStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
g_serviceStatus.dwCurrentState = SERVICE_RUNNING;
g_serviceStatus.dwControlsAccepted = 0;
SetServiceStatus(g_hServiceStatus, &g_serviceStatus);
// 初始化工作
Sleep(2000);
g_serviceStatus.dwCurrentState = SERVICE_RUNNING;
g_serviceStatus.dwControlsAccepted =
SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_PAUSE_CONTINUE | SERVICE_ACCEPT_SHUTDOWN;
SetServiceStatus(g_hServiceStatus, &g_serviceStatus);
// 执行服务任务,这里可以是用户想要执行的任何代码
ShellExecute(NULL, TEXT("open"),
TEXT("F:\\\\Source\\\\Windows\\\\Chapter10\\\\HelloWindows7\\\\Debug\\\\HelloWindows.exe"),
NULL, NULL, SW_SHOW);
}
DWORD WINAPI HandlerEx(DWORD dwControl, DWORD dwEventType, LPVOID lpEventData, LPVOID lpContext)
{
switch (dwControl)
{
case SERVICE_CONTROL_SHUTDOWN:
case SERVICE_CONTROL_STOP:
g_serviceStatus.dwCurrentState = SERVICE_STOP_PENDING;
SetServiceStatus(g_hServiceStatus, &g_serviceStatus);
// 可以执行一些清理操作
g_serviceStatus.dwCurrentState = SERVICE_STOPPED;
SetServiceStatus(g_hServiceStatus, &g_serviceStatus);
break;
case SERVICE_CONTROL_PAUSE:
g_serviceStatus.dwCurrentState = SERVICE_PAUSE_PENDING;
SetServiceStatus(g_hServiceStatus, &g_serviceStatus);
g_serviceStatus.dwCurrentState = SERVICE_PAUSED;
SetServiceStatus(g_hServiceStatus, &g_serviceStatus);
break;
case SERVICE_CONTROL_CONTINUE:
g_serviceStatus.dwCurrentState = SERVICE_CONTINUE_PENDING;
SetServiceStatus(g_hServiceStatus, &g_serviceStatus);
g_serviceStatus.dwCurrentState = SERVICE_RUNNING;
SetServiceStatus(g_hServiceStatus, &g_serviceStatus);
break;
}
return NO_ERROR;
}
本例中,服务所执行的代码就是调用ShellExecute函数运行F:\\Source\\Windows\\Chapter10\\HelloWindows7\\ Debug\\HelloWindows.exe程序,因为服务的特性,HelloWindows.exe程序不会显示用户界面,但是启动服务后,就会运行HelloWindows.exe程序,我们可以听到歌声。
按 Ctrl + F5 组合键编译程序,然后打开F:\\ Source\\Windows\\Chapter10\\ServiceManager2\\Debug\\ ServiceManager.exe服务管理程序,单击添加服务菜单项,把MyService服务添加进去(见原书的图10.3)。
添加MyService服务后,该服务程序默认处于停止状态,我们可以用鼠标右键单击该服务程序单击启动服务菜单项,服务启动成功,歌声响起。
按下Ctrl + Alt + Delete组合键选择注销用户,MyService服务程序会正常运行,而且HelloWindows. exe程序也会正常运行,因为HelloWindows.exe程序属于会话0 SYSTEM用户,关机重新启动计算机,MyService服务程序可以开机自动启动。
打开任务管理器,切换到详细信息选项卡,显示进程列表,进程列表是一个列表视图控件,用鼠标右键单击列表视图控件的列标题,然后选择列,勾选会话ID,单击确定按钮,可以看到原书的图10.4所示的界面。
服务程序MyService.exe和HelloWindows.exe程序同属于会话0,同属于SYSTEM用户,HelloWindows.exe的父进程是MyService.exe,MyService.exe的父进程是services.exe。
6.突破会话0隔离通过服务创建用户界面
通常,服务程序被设计为无人看管的无须图形用户界面(GUI)的控制台应用程序,从Windows Vista系统开始,服务无法直接与用户交互,但是某些服务可能偶尔需要与用户进行交互。服务程序可以通过调用WTSSendMessage函数在用户会话中显示一个消息框,如果需要实现更复杂的用户界面可以使用CreateProcessAsUser函数在用户会话中创建一个进程。
调用WTSSendMessage函数在用户会话中显示消息框的示例代码如下:
TCHAR szTitle[] = TEXT("消息标题");
TCHAR szMessage[] = TEXT("消息内容");
DWORD dwSessionId;
DWORD dwResponse;
dwSessionId = WTSGetActiveConsoleSessionId();
WTSSendMessage(WTS_CURRENT_SERVER_HANDLE, dwSessionId, szTitle, sizeof(szTitle),
szMessage, sizeof(szMessage), MB_OK, 0, &dwResponse, TRUE);
WTSGetActiveConsoleSessionId函数用于获取当前连接到物理控制台的会话ID(Session ID),物理控制台是指屏幕、鼠标和键盘,即获取当前登录用户的会话ID:
DWORD WTSGetActiveConsoleSessionId();
如果函数执行成功,则返回当前登录用户的会话ID,通常情况下函数返回值是1;如果当前没有用户登录,则函数返回0xFFFFFFFF。
本节会用到几个以WTS开头的函数,WTS(Windows Terminal Services,Windows终端服务)系列函数可以用于服务层与应用层的交互。
服务程序如果需要在用户会话中创建一个具有用户界面的进程,可以调用CreateProcessAsUser函数:
BOOL CreateProcessAsUser(
_In_opt_ HANDLE hToken, // 访问令牌句柄
_In_opt_ LPCTSTR lpApplicationName,
_Inout_opt_ LPTSTR lpCommandLine,
_In_opt_ LPSECURITY_ATTRIBUTES lpProcessAttributes,
_In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes,
_In_ BOOL bInheritHandles,
_In_ DWORD dwCreationFlags,
_In_opt_ LPVOID lpEnvironment,
_In_opt_ LPCTSTR lpCurrentDirectory,
_In_ LPSTARTUPINFO lpStartupInfo,
_Out_ LPPROCESS_INFORMATION lpProcessInformation);
与CreateProcess函数相比,CreateProcessAsUser函数只是多了一个访问令牌句柄参数hToken。
访问令牌是描述进程或线程的安全环境的一个内核对象,令牌中的信息包括与进程或线程关联的用户账户的ID和特权。当用户登录时,系统通过将用户密码与存储在安全数据库中的信息进行比较来验证用户密码,如果密码通过验证,系统将生成一个访问令牌,在该用户账户中运行的每个进程都有此访问令牌的副本。要通过CreateProcessAsUser 函数在用户会话中创建一个进程,需要通过调用WTSQueryUserToken函数查询当前登录用户的访问令牌,该函数返回一个访问令牌句柄hUserToken,有了访问令牌句柄就可以在服务程序中像调用CreateProcess函数一样创建进程。但是在创建进程前最好做以下两方面的工作(可选操作)。
(1)在用户账户中运行的每个进程最好使用用户访问令牌的副本,可以通过调用DuplicateTokenEx函数复制一份用户访问令牌句柄 hUserTokenDup,用于 CreateProcessAsUser 函数。如果需要,可以调用 SetTokenInformation函数设置hUserTokenDup句柄的访问令牌信息。
(2)每个进程都有一个环境块,默认情况下子进程会继承其父进程的环境变量,服务程序可以通过调用CreateEnvironmentBlock函数获取当前登录用户的环境变量块(包括用户环境变量和系统环境变量),然后把获取到的环境变量块用于CreateProcessAsUser函数的lpEnvironment参数。
下面先介绍WTSQueryUserToken、DuplicateTokenEx和CreateEnvironmentBlock这几个函数的用法。WTSQueryUserToken函数用于查询指定会话ID对应的登录用户的主访问令牌:
BOOL WTSQueryUserToken(
_In_ ULONG SessionId, // 会话ID,可以通过调用WTSGetActiveConsole
// SessionId函数获得
_Out_ PHANDLE phToken); // 返回SessionId参数指定的登录用户的主访问令牌句柄
如果函数执行成功,则返回值为非零值;如果函数执行失败,则返回值为0。为了防止用户令牌泄露,不再需要访问令牌句柄时必须及时调用CloseHandle函数关闭句柄。
DuplicateTokenEx函数用于复制一份访问令牌句柄:
BOOL WINAPI DuplicateTokenEx(
_In_ HANDLE hExistingToken, // 一个现有令牌句柄
_In_ DWORD dwDesiredAccess, // 新令牌句柄的访问权限
_In_opt_ LPSECURITY_ATTRIBUTES lpTokenAttributes, // 指向SECURITY_ATTRIBUTES
// 结构的指针
_In_ SECURITY_IMPERSONATION_LEVEL ImpersonationLevel, // 新令牌的模拟级别
_In_ TOKEN_TYPE TokenType, // 新令牌的令牌类型
_Out_ PHANDLE phNewToken); // 返回新令牌句柄
-
hExistingToken参数指定为一个现有令牌的句柄。
-
dwDesiredAccess参数指定新令牌句柄的访问权限,设置为0表示使用与现有令牌句柄相同的访问权限,设置为MAXIMUM_ALLOWED表示使用对调用者有效的所有访问权限。
-
lpTokenAttributes参数是一个指向SECURITY_ATTRIBUTES结构的指针,该结构为新令牌指定安全描述符,并指定子进程是否可以继承令牌,该参数的用法同其他内核对象,通常可以设置为NULL。
-
ImpersonationLevel参数指定新令牌的模拟级别,该参数是一个SECURITY_IMPERSONATION_LEVEL枚举类型,这里指定为SecurityIdentification。
-
TokenType参数指定新令牌的令牌类型,该参数是一个TOKEN_TYPE枚举类型,这里指定为TokenPrimary,表示新令牌是可以在CreateProcessAsUser函数中使用的主令牌,允许模拟客户端的服务程序创建具有客户端安全环境的进程。
-
phNewToken参数指向接收新令牌句柄变量的指针。
如果函数执行成功,则返回非零值;如果函数执行失败,则返回值为0。不再需要令牌句柄时应该调用CloseHandle函数关闭。
CreateEnvironmentBlock函数用于获取指定用户的环境变量块:
BOOL WINAPI CreateEnvironmentBlock(
_Out_ LPVOID* lpEnvironment, // 返回环境变量块的指针
_In_opt_ HANDLE hToken, // 访问令牌句柄,如果设置为NULL则返回的环境块仅包含系统变量
_In_ BOOL bInherit); // 是否继承当前进程的环境变量
在把返回的环境变量块用于CreateProcessAsUser函数时,CreateProcessAsUser函数的dwCreationFlags参数应该指定CREATE_UNICODE_ENVIRONMENT标志,因为默认情况下lpEnvironment参数指向的环境变量块使用ANSI字符,指定该标志后将使用Unicode字符。CreateProcessAsUser函数返回后,新进程将具有环境变量块的副本,因此可以调用DestroyEnvironmentBlock函数释放环境变量块。
下面封装一个自定义函数CreateUIProcess用于在用户会话中创建一个具有用户界面的进程:
BOOL CreateUIProcess(LPCTSTR lpApplicationName, LPTSTR lpCommandLine)
{
DWORD dwSessionId; // 当前登录用户的会话ID
HANDLE hUserToken = NULL, hUserTokenDup = NULL; // 访问令牌句柄
LPVOID lpEnvironment = NULL;
STARTUPINFO si = { sizeof(STARTUPINFO) };
PROCESS_INFORMATION pi = { 0 };
// 获取当前登录用户的会话ID
dwSessionId = WTSGetActiveConsoleSessionId();
// 查询指定会话ID对应的登录用户的主访问令牌
WTSQueryUserToken(dwSessionId, &hUserToken);
// 复制一份用户访问令牌句柄
DuplicateTokenEx(hUserToken, MAXIMUM_ALLOWED, NULL, SecurityIdentification,
TokenPrimary, &hUserTokenDup);
CloseHandle(hUserToken);
// 获取当前登录用户的环境变量块
CreateEnvironmentBlock(&lpEnvironment, hUserTokenDup, FALSE);
// 创建进程
CreateProcessAsUser(hUserTokenDup, lpApplicationName, lpCommandLine, NULL, NULL, FALSE,
CREATE_UNICODE_ENVIRONMENT, lpEnvironment, NULL, &si, &pi);
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
CloseHandle(hUserTokenDup);
DestroyEnvironmentBlock(lpEnvironment);
return TRUE;
}
把MyService项目的ServiceMain函数中的ShellExecute函数调用改为上面的自定义函数:
TCHAR szCommandLine[MAX_PATH] =
TEXT("F:\\\\Source\\\\Windows\\\\Chapter10\\\\HelloWindows7\\\\Debug\\\\
HelloWindows.exe");
CreateUIProcess(NULL, szCommandLine);
经过测试,启动服务后HelloWindows.exe程序如约而至成功显示了用户界面。此时,打开任务管理器,可以看到原书的图10.5所示的界面。
HelloWindows.exe进程的父进程是MyService.exe,但是HelloWindows.exe属于当前登录的SuperWang用户会话1,这是正确的,因为调用CreateProcessAsUser函数创建进程时使用的就是当前登录用户的访问令牌。如果当前登录用户SuperWang注销,HelloWindows.exe进程一定会结束!当然,重新启动计算机,MyService服务程序可以开机自动启动,HelloWindows.exe进程也会正常显示。
在Windows系统中SYSTEM用户拥有最高权限,以SYSTEM用户身份运行的进程可以完成很多常规情况下无法完成的任务。如果希望HelloWindows.exe进程以SYSTEM用户身份运行,但还是会话1,可以调用OpenProcessToken函数获取与服务进程关联的访问令牌,然后调用DuplicateTokenEx函数复制一份,得到的复制句柄可以调用SetTokenInformation函数设置会话ID为当前登录用户,这样调用CreateProcessAsUser函数创建的子进程就属于SYSTEM用户。例如下面的代码:
BOOL CreateUIProcess(LPCTSTR lpApplicationName, LPTSTR lpCommandLine)
{
DWORD dwSessionId; // 当前登录用户的会话ID
HANDLE hProcessToken = NULL, hProcessTokenDup = NULL; // 访问令牌句柄
LPVOID lpEnvironment = NULL;
STARTUPINFO si = { sizeof(STARTUPINFO) };
PROCESS_INFORMATION pi = { 0 };
// 获取与服务进程关联的访问令牌
OpenProcessToken(GetCurrentProcess(), TOKEN_ALL_ACCESS, &hProcessToken);
// 复制一份访问令牌句柄
DuplicateTokenEx(hProcessToken, MAXIMUM_ALLOWED, NULL, SecurityIdentification,
TokenPrimary, &hProcessTokenDup);
CloseHandle(hProcessToken);
// 设置访问令牌句柄的会话ID为当前登录用户
dwSessionId = WTSGetActiveConsoleSessionId();
SetTokenInformation(hProcessTokenDup, TokenSessionId, &dwSessionId, sizeof(dwSessionId));
// 获取当前登录用户的环境变量块
CreateEnvironmentBlock(&lpEnvironment, hProcessTokenDup, FALSE);
// 创建进程
CreateProcessAsUser(hProcessTokenDup, lpApplicationName, lpCommandLine, NULL, NULL, FALSE,
CREATE_UNICODE_ENVIRONMENT, lpEnvironment, NULL, &si, &pi);
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
CloseHandle(hProcessTokenDup);
DestroyEnvironmentBlock(lpEnvironment);
return TRUE;
}
但是同样HelloWindows.exe进程会随着当前登录用户的注销而终止。会话和用户这两个概念之间比较模糊,如果希望HelloWindows.exe进程运行于SYSTEM用户,会话0,则需要使用CreateProcess或ShellExecute一类的函数,会话0用于系统进程、服务和驱动程序,而不是创建用户界面和用户进行交互。
10.3 用户账户控制
用户账户控制(User Account Control,UAC)是Microsoft在Windows Vista及更高版本操作系统中采用的一种安全控制机制,它要求用户在执行可能会影响计算机运行的操作或执行更改其他用户的设置的操作以前,提供权限或管理员密码,以防止恶意软件在未经许可的情况下在计算机上进行安装或对计算机进行更改。简单地说,就是在运行某些软件的时候,要求用户进行授权才能运行它。
在Windows系统中用户账户按组划分,每个组中可以有多个成员用户,例如:
-
Administrators组;
-
Administrator;
-
管理员用户;
-
Users组;
-
标准用户。
使用微软原版Windows镜像安装Windows时会要求我们输入一个管理员用户名称(作者计算机SuperWang)。
当用户登录到计算机时,系统会为用户创建访问令牌,访问令牌包含授予用户的访问级别的信息,包括安全标识符(SID)和Windows权限,在默认情况下,标准用户和管理员用户(包括Administrator和其他管理员用户)都是以标准用户的访问令牌访问资源并运行应用程序。
用户登录后,系统会为用户创建两个访问令牌:标准用户访问令牌和管理员访问令牌,然后系统使用标准用户访问令牌显示桌面(Eexplorer.exe),Explorer.exe作为父进程,用户启动的其他所有进程都将从该父进程继承访问令牌。除非用户批准应用程序使用管理员访问令牌,否则所有应用程序均以标准用户访问令牌运行。当用户运行需要管理员访问令牌的应用程序时,系统会自动提示用户进行批准,该提示称为提权提示,可以阻止恶意软件在用户不知情的情况下提升权限。
我们来看新装系统以后的用户账户控制默认设置。单击开始,输入gpedit.msc,打开本地组策略编辑器,计算机配置→ Windows设置→安全设置→本地策略→安全选项,滑动至最下方,可以看到原书的图10.6所示的界面。
以管理员批准模式运行所有管理员:这是打开或关闭用户账户控制的设置,用户可以选择“已启用”以打开用户账户控制,或者选择“已禁用”以关闭用户账户控制,更改后需要重启计算机。关闭用户账户控制后,默认情况下管理员用户将使用完全管理权限(管理员访问令牌)运行所有应用程序。
打开控制面板,查看方式选择小图标,单击用户账户 → 更改用户账户控制设置,可以看到原书的图10.7所示的界面。
以上就是用户新装系统后的用户账户控制默认设置。当用户用鼠标右键单击程序,然后选择以管理员身份运行,或者要运行的程序需要管理员权限(管理员访问令牌)时,用户桌面将切换到安全桌面,安全桌面会使用户桌面变暗,并显示一个提权提示对话框,在继续之前用户必须对其进行响应,当用户单击“是”或“否”时,桌面将切换回用户桌面,只有Windows进程才能访问安全桌面。根据要运行的程序是否是Windows内部应用程序以及是否具有数字签名,提权对话框所显示的内容与颜色略有不同。如果当前登录用户是标准用户,系统则会弹出一个对话框要求用户输入管理员密码以提权。
10.3.1 自动提示用户提升权限
用户可以用鼠标右键单击程序,然后选择以管理员身份运行,以取得管理员权限。如果需要在程序运行时自动提示用户提升权限,则可以通过项目属性来设置,打开项目属性→配置属性→链接器→清单文件(见原书的图10.8)。
设置UAC执行级别为requireAdministrator (/level=‘requireAdministrator’)。
也可以通过添加一个清单文件来进行设置,前面已经介绍过清单文件的编写方法。这里以Chapter4\\ProcessList项目为例,ProcessList.exe.manifest文件的内容如下:
<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" >
<assemblyIdentity
version="1.0.0.0"
processorArchitecture="*"
name="CompanyName.ProductName.YourApplication"
type="win32"
/>
<description>Your application description here.</description>
<dependency>
<dependentAssembly>
<assemblyIdentity
type="win32"
name="Microsoft.Windows.Common-Controls"
version="6.0.0.0"
processorArchitecture="*"
publicKeyToken="6595b64144ccf1df"
language="*"
/>
</dependentAssembly>
</dependency>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security>
<requestedPrivileges>
<requestedExecutionLevel
level="requireAdministrator"
uiAccess="false"/>
</requestedPrivileges>
</security>
</trustInfo>
</assembly>
level属性可用的值及含义如表10.6所示。
表10.6
| requireAdministrator | 应用程序必须以管理员权限启动,否则不会运行 |
| highestAvailable | 应用程序以当前用户可以获得的最高权限运行 |
| asInvoker | 应用程序使用与父进程相同的权限运行 |
添加清单文件后,最好在ProcessList.rc资源脚本文件中添加如下语句:
#define MANIFEST_RESOURCE_ID 1
MANIFEST_RESOURCE_ID RT_MANIFEST "ProcessList.exe.manifest"
这表示将清单文件嵌入PE文件的资源中。
按Ctrl + F5组合键编译运行程序,弹出原书的图10.9所示的对话框。
因为VS是以标准用户权限启动的,VS作为父进程,无法启动要求管理员权限的程序,我们可以选择“使用其他凭据重新启动®”,这样一来VS会以管理员权限重新启动。
虽然VS无法启动ProcessList.exe,但是程序已经编译完毕,打开Chapter4\\ProcessList\\Debug可以看到ProcessList.exe程序图标上面有一个盾牌图标,盾牌图标说明该程序需要以提升权限运行。双击运行ProcessList.exe,会弹出提权提示对话框,选择是,即可以管理员权限运行该程序。
用户如果需要以管理员权限运行某个程序,可以用鼠标右键单击程序,然后选择属性,切换到兼容性选项卡,勾选“以管理员身份运行该程序”。上述设置也可以通过写注册表来实现,在HKEY_CURRENT_USER\\Software\\Microsoft\\Windows NT\\CurrentVersion\\AppCompatFlags\\Layers子键下面添加一个键值项,以可执行文件的完整路径为键名,以字符串"RUNASADMIN"为键值,例如下面的代码:
LPCTSTR lpSubKey = TEXT("Software\\\\Microsoft\\\\Windows NT\\\\CurrentVersion\\\\ AppCompatFlags\\\\
Layers");
LPCTSTR lpValueName = TEXT("F:\\\\Source\\\\Windows\\\\Chapter4\\\\ProcessList\\\\ Debug\\\\ProcessList.
exe");
LPTSTR lpData = TEXT("RUNASADMIN");
DWORD dwcbData = (_tcslen(lpData) + 1) * sizeof(TCHAR);
HKEY hKey;
RegCreateKeyEx(HKEY_CURRENT_USER, lpSubKey, 0, NULL, REG_OPTION_NON_VOLATILE,
KEY_WRITE, NULL, &hKey, NULL);
RegSetValueEx(hKey, lpValueName, NULL, REG_SZ, (LPBYTE)lpData, dwcbData);
RegCloseKey(hKey);
10.3.2 利用ShellExecuteEx函数以管理员权限启动程序
ShellExecuteEx函数声明如下:
BOOL ShellExecuteEx(_Inout_ SHELLEXECUTEINFO* pExecInfo);
pExecInfo参数是一个指向SHELLEXECUTEINFO结构的指针,该结构包含要执行的应用程序的信息,结构定义如下:
typedef struct _SHELLEXECUTEINFOA {
DWORD cbSize;
ULONG fMask;
HWND hwnd;
LPCTSTR lpVerb;
LPCTSTR lpFile;
LPCTSTR lpParameters;
LPCTSTR lpDirectory;
int nShow;
HINSTANCE hInstApp;
void* lpIDList;
LPCTSTR lpClass;
HKEY hkeyClass;
DWORD dwHotKey;
union {
HANDLE hIcon;
HANDLE hMonitor;
} DUMMYUNIONNAME;
HANDLE hProcess;
} SHELLEXECUTEINFO, * LPSHELLEXECUTEINFO;
lpVerb字段指定为runas表示以管理员权限启动lpFile字段指定的应用程序,系统会弹出一个提权提示对话框,或者要求用户输入管理员密码以提权(标准用户)。
例如下面的代码:
SHELLEXECUTEINFO sei = { sizeof(sei) };
sei.lpVerb = TEXT("runas");
sei.lpFile = TEXT("F:\\\\Source\\\\Windows\\\\Chapter4\\\\ProcessList\\\\Debug\\\\ProcessList.exe");
sei.nShow = SW_SHOWNORMAL;
DWORD dwStatus;
if (!ShellExecuteEx(&sei))
{
dwStatus = GetLastError();
if (dwStatus == ERROR_CANCELLED)
MessageBox(hwndDlg, TEXT("用户拒绝提升权限"), TEXT("提示"), MB_OK);
else if (dwStatus == ERROR_FILE_NOT_FOUND)
MessageBox(hwndDlg, TEXT("指定的文件没有找到"), TEXT("提示"), MB_OK);
}
10.3.3 绕过UAC提权提示以管理员权限运行
绕过UAC提权提示以管理员权限运行程序应该说是黑客必备的一项技术,方法有很多,但是很多方法在最新的Windows系统中已经失效。在COM中有一个COM提升名字对象(COM Elevation Moniker)技术,该技术允许应用程序以提升的权限激活COM类,然后可以调用CMSTPLUA组件中的ICMLuaUtil接口的ShellExec方法执行任何程序。
要以管理员权限激活COM类,可以使用自定义函数CoCreateInstanceAsAdmin,该函数在MSDN中提供。为CoCreateInstanceAsAdmin函数指定CMSTPLUA组件的CLSID和ICMLuaUtil接口的IID,该函数就可以返回ICMLuaUtil接口的指针,然后可以调用ICMLuaUtil接口的ShellExec方法执行任何程序,需要注意的是ICMLuaUtil接口需要自己定义。
如果在普通的程序中调用以管理员权限激活COM类的自定义函数CoCreateInstanceAsAdmin,还是会触发UAC提权提示,因此要想关闭UAC提权提示,调用代码应该在系统信任程序中执行,例如Taskmgr.exe、CompMgmtLauncher.exe和rundll32.exe,这些程序已经具有管理员权限,我们可以通过DLL注入等技术,将调用代码注入这些系统信任程序的进程空间中。
本节介绍通过rundll32.exe执行调用代码的方法。rundll32.exe位于C:\\Windows\\System32目录下,顾名思义就是运行32位动态链接库,当然也可以运行64位动态链接库,rundll32.exe可以执行动态链接库中的导出函数,命令格式为如下内容:
rundll32.exe DllName FunctionName [Arguments]
DllName指定为需要执行的动态链接库完整路径名(如果DllName参数中存在空格,路径名应该使用""引起来),FunctionName指定为动态链接库中的导出函数名称,Arguments是可选的传递给导出函数的参数。
rundll32.exe要求的导出函数原型如下:
void CALLBACK FunctionName(HWND hwnd, HINSTANCE hInstance, LPSTR lpCmdLine, int nCmdShow);
因此,我们创建一个DLL项目PassUAC,在PassUAC.dll中导出符合rundll32.exe调用要求的导出函数PassUAC,在PassUAC函数中调用CoCreateInstanceAsAdmin函数以管理员权限创建COM对象,返回ICMLuaUtil接口的指针,然后调用ICMLuaUtil接口的ShellExec方法执行任何程序。具体代码参见Chapter10\\PassUAC项目,该项目非常简单,但是这对于没有COM基础的读者来说并不容易理解。
通过rundll32.exe执行导出函数则比较简单,只需要拼凑出命令字符串,然后调用ShellExecute函数执行即可,例如:
TCHAR szRundll32Path[MAX_PATH] = TEXT("C:\\\\Windows\\\\System32\\\\rundll32.exe");
TCHAR szDllPath[MAX_PATH] = TEXT("F:\\\\Source\\\\Windows\\\\Chapter10\\\\PassUAC\\\\ Debug\\\\PassUAC.dll");
TCHAR szExcuteFileName[MAX_PATH] = TEXT("F:\\\\Source\\\\Windows\\\\Chapter10\\\\ProcessList.exe");
TCHAR szParameters[MAX_PATH] = { 0 };
wsprintf(szParameters, TEXT("\\"%s\\" %s \\"%s\\""), szDllPath, TEXT("PassUAC"), szExcuteFileName);
ShellExecute(NULL, TEXT("open"), szRundll32Path, szParameters, NULL, SW_HIDE);
如果需要通过编程方式禁用用户计算机的UAC,可以把HKEY_LOCAL_MACHINE\\ SOFTWARE\\ Microsoft\\Windows\\CurrentVersion\\Policies\\System下的EnableLUA键值项设置为0。
10.4 用户界面特权隔离
在启用用户账户控制的情况下,标准用户和管理员用户均使用标准用户权限,如果GetMd5Test程序以管理员权限运行,那么当用户拖动一个文件到GetMd5Test程序窗口中时,是接收不到WM_DROPFILES消息的。本节介绍强制完整性控制和用户界面特权隔离这两个概念。
强制完整性控制(Mandatory Integrity Control,MIC)是一种控制对安全对象访问的机制,该机制是对自由(自主)访问控制的补充,在对安全对象的自由访问控制列表(DACL)进行访问前需要评估,MIC使用完整性级别和强制策略来评估访问。安全主体和安全对象被分配了完整性级别,完整性级别决定了它们的保护或访问级别,例如,具有低完整性级别的主体无法写入具有中完整性级别的对象,即使该对象的DACL允许进行写访问。Windows定义了四个完整性级别:低、中、高和系统,标准用户获得中,提升用户获得高。打开Process Explorer程序,默认会显示系统中正在运行的进程列表,用鼠标右键单击任何一列的列标题,然后选择Select Columns…→Process Image选项卡→选中“Integrity Level”,可以看到进程列表中有一列是“Integrity”,表示进程的完整性级别。
Windows消息子系统采用了强制完整性控制,从而实现了用户界面特权隔离(User Interface Privilege Isolation,UIPI),UIPI会阻止一个进程向其他完整性级别更高的进程所属的窗口发送消息,以下消息除外:WM_NULL、WM_MOVE、WM_SIZE、WM_GETTEXT、WM_GETTEXTLENGTH、WM_GETHOTKEY、WM_GETICON、WM_RENDERFORMAT、WM_DRAWCLIPBOARD、WM_CHANGECBCHAIN、WM_THEMECHANGED。启用UIPI后还有以下限制:不能通过调用SendMessage/ PostMessage函数向更高权限进程的窗口发送消息,尽管这两个函数会被成功调用,但是实际上消息会被删除;不能使用线程挂钩附加到更高权限的进程;不能使用日志钩子监控更高权限的进程;不能将DLL注入更高权限的进程,等等。但是,低完整性级别的进程并未与其他进程完全隔离,低完整性级别与较高完整性级别进程之间可以使用剪贴板、共享内存、IPC、Socket、COM接口和命名管道等进行通信。
完整性级别和用户账户控制、访问令牌等概念息息相关。在启用用户账户控制的情况下,Explorer. exe进程的完整性级别是中,Explorer.exe作为父进程,用户启动的其他所有进程都将从该父进程继承访问令牌,因此它们的完整性级别也是中。从资源管理器中用鼠标拖放文件到GetMd5Test程序窗口,其实就是Explorer.exe和GetMd5Test.exe两个进程之间的通信,Explorer.exe进程需要向GetMd5Test.exe进程发送WM_DROPFILES消息,要想成功发送该消息,Explorer.exe进程的完整性级别必须低于或等于GetMd5Test.exe进程。
ChangeWindowMessageFilterEx函数用于修改指定窗口的UIPI消息过滤器:
BOOL WINAPI ChangeWindowMessageFilterEx(
_In_ HWND hwnd, // 要修改其UIPI消息过滤器的窗口的句柄
_In_ UINT message, // 允许消息过滤器通过或阻止的消息类型
_In_ DWORD action, // 要执行的操作,通过、阻止或重置
_Inout_opt_ PCHANGEFILTERSTRUCT pChangeFilterStruct); // 指向CHANGEFILTERSTRUCT结构的指针
- action参数用于指定要执行的操作,可以是表10.7中的值之一。
表10.7
| MSGFLT_ALLOW | 允许消息通过UIPI消息过滤器,hwnd窗口能够接收message参数指定的消息,即使消息来自较低权限的进程 |
| MSGFLT_DISALLOW | 如果消息来自较低权限的进程,则阻止要传递到hwnd窗口的message参数指定的消息 |
| MSGFLT_RESET | 将hwnd参数指定的窗口的UIPI消息过滤器重置为默认值 |
- pChangeFilterStruct参数是一个指向CHANGEFILTERSTRUCT结构的指针,用于获取函数调用的扩展结果信息。该结构在WinUser.h头文件中定义如下:
typedef struct tagCHANGEFILTERSTRUCT {
DWORD cbSize; // 结构的大小,以字节为单位
DWORD ExtStatus; // 函数调用的扩展结果信息
} CHANGEFILTERSTRUCT, * PCHANGEFILTERSTRUCT;
还有一个ChangeWindowMessageFilter,修改的是整个进程的UIPI消息过滤器:
BOOL WINAPI ChangeWindowMessageFilter(
_In_ UINT message, // 允许消息过滤器通过或阻止的消息类型
_In_ DWORD dwFlag); // 要执行的操作,添加MSGFLT_ADD或删除MSGFLT_REMOVE
10.5 窗口的查找与枚举
有的加密软件在运行过程中会禁止用户运行一些调试、跟踪或监视软件,以防止自身被破解。要查看系统中是否正在运行敏感软件,可以枚举系统中正在运行的进程列表,还可以枚举所有顶级窗口并获取窗口标题。
重叠窗口和弹出窗口都可以是顶级窗口,顶级窗口的父窗口为桌面窗口,因此可以通过枚举桌面窗口的所有子窗口的方式来实现窗口枚举,例如下面的代码:
// 获取桌面窗口的第一个子窗口。GetDesktopWindow函数用于获取桌面窗口句柄
hwnd = GetWindow(GetDesktopWindow(), GW_CHILD);
while (hwnd != NULL)
{
GetWindowText(hwnd, szTitle, _countof(szTitle)); // 窗口标题
GetClassName(hwnd, szClassName, _countof(szClassName)); // 窗口类名
// 其他操作
// 获取桌面窗口的下一个子窗口
hwnd = GetWindow(hwnd, GW_HWNDNEXT);
}
打开Spy++的窗口列表,可以看到该工具同时枚举了顶级窗口的子窗口,但是上面的代码只能枚举顶级窗口。与FindWindow函数相同,FindWindowEx函数也用于查找具有指定窗口类名和窗口标题的窗口的窗口句柄,但是FindWindowEx函数只查找指定父窗口中的子窗口:
HWND FindWindowEx(
_In_opt_ HWND hWndParent, // 父窗口句柄,设置为NULL表示函数使用桌面窗口作为父窗口
_In_opt_ HWND hWndChildAfter, // 子窗口句柄,函数将从该子窗口以后开始搜索
_In_opt_ LPCTSTR lpszClass, // 窗口类名
_In_opt_ LPCTSTR lpszWindow); // 窗口标题
-
hWndParent参数指定父窗口句柄,设置为NULL表示函数使用桌面窗口作为父窗口。
-
hWndChildAfter参数指定一个子窗口句柄,函数将从该子窗口以后开始查找,如果该参数设置为NULL,则表示从hwndParent的第一个子窗口开始查找。
如果hwndParent和hwndChildAfter参数均指定为NULL,函数将查找所有顶级窗口。
-
lpszClass参数指定窗口类名。
-
lpszWindow参数指定窗口标题。
如果函数执行成功,则返回值是具有指定类和窗口名称的窗口句柄;如果函数执行失败,则返回值为NULL。
上面枚举顶级窗口的代码,在循环中处理完一个顶级窗口句柄以后,可以继续调用下面的代码枚举该窗口中的子窗口:
hwndChild = NULL;
while (hwndChild = FindWindowEx(hwnd, hwndChild, NULL, NULL))
{
// 获取其他窗口的子窗口标题应该发送WM_GETTEXT消息,而不是GetWindowText
SendMessage(hwndChild, WM_GETTEXT, _countof(szTitle), (LPARAM)szTitle);
GetClassName(hwndChild, szClassName, _countof(szClassName));
// 其他操作
}
要枚举窗口,还有一个更简单的函数EnumWindows。调用GetWindow函数时,可能会陷入死循环或引用已被销毁的窗口句柄,所以EnumWindows函数更可靠一些。EnumWindows函数声明如下:
BOOL EnumWindows(
_In_ WNDENUMPROC lpEnumFunc, // 回调函数
_In_ LPARAM lParam); // 传递给回调函数的参数
回调函数的定义格式如下:
BOOL CALLBACK EnumWindowsProc(
_In_ HWND hwnd, // 顶级窗口窗口句柄
_In_ LPARAM lParam); // 传递过来的参数
回调函数可以根据窗口句柄参数hwnd获取有关窗口的各种信息,处理完后应该返回TRUE表示继续枚举,如果想停止枚举可以返回FALSE。
EnumWindows函数只能枚举顶级窗口,要枚举顶级窗口中的子窗口可以使用EnumChildWindows函数:
BOOL EnumChildWindows(
_In_opt_ HWND hWndParent, // 父窗口句柄,设置为NULL则该函数等效于EnumWindows
_In_ WNDENUMPROC lpEnumFunc, // 回调函数,定义格式和用法与EnumWindows函数的回调函数相同
_In_ LPARAM lParam); // 传递给回调函数的参数
10.6 实现任务栏通知区域图标与气泡通知
通知区域是任务栏的一部分,也称为系统托盘或状态区域,在默认情况下,电池电量、网络状态和音量控制都会显示在通知区域。用户可以通过控制面板(查看方式选择小图标)→任务栏和导航→通知区域→选择哪些图标显示在任务栏上,设置通知区域图标的自定义显示。
例如,通知区域中的QQ程序图标,当鼠标悬停在通知区域中的QQ图标上时会显示一个工具提示,用鼠标左键单击时会恢复显示QQ程序面板,用鼠标右键单击时会弹出一个快捷菜单,当好友的消息发送过来时,通知区域图标会显示为好友头像图标并闪动。当鼠标悬停在通知区域中的QQ图标上时显示的工具提示如原书的图10.10所示。
当发生特定事件的时候,还可以显示一个气泡通知以告知用户,气泡通知可以包含图标、标题和文本,如原书的图10.11所示。
程序可以通过调用Shell_NotifyIcon函数在通知区域中添加、修改或删除图标:
BOOL Shell_NotifyIcon(
_In_ DWORD dwMessage, // 要执行的操作
_In_ PNOTIFYICONDATA lpData); // 指向NOTIFYICONDATA结构的指针
- dwMessage参数指定要执行的操作,可以是表10.8中的值之一(常见值)。
表10.8
| NIM_ADD(0x00000000) | 将图标添加到通知区域,通过lpdata的uID或guidItem字段指定图标的ID或GUID |
| NIM_MODIFY(0x00000001) | 修改通知区域中的图标,通过lpdata的uID或guidItem字段指定图标的ID或GUID |
| NIM_DELETE(0x00000002) | 从通知区域中删除图标,通过lpdata的uID或guidItem字段指定图标的ID或GUID |
- lpData参数是一个指向NOTIFYICONDATA结构的指针,该结构的定义如下:
typedef struct _NOTIFYICONDATA {
DWORD cbSize; // 该结构的大小
HWND hWnd; // 用于接收通知区域中图标的通知消息的窗口句柄
UINT uID; // 通知区域中图标的ID,通过hWnd和uID(或guidItem)可以确定一个通知区域图标
UINT uFlags; // 标志,用于指定哪个字段有效
UINT uCallbackMessage; // 自定义消息ID,当在通知区域中的图标上发生鼠标事件时会发送该消息
HICON hIcon; // 要添加、修改或删除的图标的句柄
TCHAR szTip[128]; // 标准工具提示的文本
DWORD dwState;
DWORD dwStateMask;
TCHAR szInfo[256]; // 气球通知的文本
union {
UINT uTimeout;
UINT uVersion;
} DUMMYUNIONNAME;
TCHAR szInfoTitle[64]; // 气球通知的标题
DWORD dwInfoFlags;
GUID guidItem; // 图标的GUID,如果指定了该字段会覆盖uID字段
HICON hBalloonIcon;
} NOTIFYICONDATA, * PNOTIFYICONDATA;
uFlags字段用于指定哪个字段有效,可以是表10.9中的值的组合。
表10.9
| NIF_MESSAGE(0x00000001) | uCallbackMessage字段有效 |
| NIF_ICON(0x00000002) | hIcon字段有效 |
| NIF_TIP(0x00000004) | szTip字段有效 |
| NIF_STATE(0x00000008) | dwState和dwStateMask字段有效 |
| NIF_INFO(0x00000010) | szInfo、szInfoTitle、dwInfoFlags和uTimeout字段有效。要显示气球通知可以通过szInfo字段指定文本,要删除气球通知可以把szInfo字段设置为一个空字符串,要添加通知区域图标而不显示气球通知则不要设置NIF_INFO标志(为了避免打扰用户通常不应该显示气球通知) |
| NIF_GUID(0x00000020) | guidItem字段有效 |
| NIF_REALTIME(0x00000040) | 与NIF_INFO标志结合使用,如果气球通知无法立即显示则将其丢弃,该标志用于实时信息的通知,后期显示这些信息将毫无意义或产生误导 |
当在通知区域中的图标上发生鼠标事件时会发送NOTIFYICONDATA.uCallbackMessage字段指定的自定义消息,当NOTIFYICONDATA.uVersion字段为0或NOTIFYICON_VERSION时,消息的wParam参数是通知区域图标ID,lParam参数是具体的鼠标事件,例如WM_MOUSEMOVE、WM_LBUTTONUP、WM_RBUTTONUP等。
接下来实现一个简单的示例程序NotifyIcon,当用户单击“最小化”按钮时在通知区域中添加一个图标并隐藏程序窗口,当用户单击“关闭”按钮时则从通知区域中删除图标。在自定义消息WM_TRAYMSG中,当用户鼠标左键单击通知区域图标时显示窗口,当用户用鼠标右键单击通知区域图标时弹出一个快捷菜单。NotifyIcon程序没有演示通知区域图标修改、闪动的功能,如果需要,可以创建一个计时器,在计时器消息中调用Shell_NotifyIcon函数并把dwMessage参数设置为NIM_MODIFY,NOTIFYICONDATA.hIcon字段设置为NULL表示不显示图标,设置为其他图标句柄则可以显示其他图标。具体代码参见Chapter10\\NotifyIcon项目。



