第八章 程序执行器
在Windows95以后,文档的概念得到了进一步增强。这隐含地说,文档不仅仅是ASCII文档,也不仅仅是Word或Excel文件,‘文档’在这里的意思是更广泛的对象,它是系统命名空间的一部分,并且有(或可能有)程序来‘打开’,‘打印’,‘探索’或‘查询’这些文档对象,换句话说,文档是一个程序可以在其上执行操作(命令串,如‘打开’,‘打印’,‘探索’)的项。
这种被执行的能力不再是具有扩展名如.exe,.com,.pif,.bat这一小类文件的特权。自Windows95以后,所有具有关连动词的文件都变得可以执行了。因此,单独的执行程序现在不多了。
程序就是一个文件,运行它们只是你在一个文件上执行的活动。在寻找一种触发外部程序的方法时,我们现在有了一个选择,正确的选择显然是重要的。这些选择经过两个中间阶段已经有了一定的进化,但是基本上还是从一个函数—WinExec()—到另一个函数—ShellExecuteEx()—的迁移。
在这一章中我们将说明启动应用的各种选择。建立进程,打开文档等,特别,我们还解释:
WinExec()和CreateProcess()之间的差异
怎样用ShellExecute()和ShellExecuteEx()取代其它函数
动词,文档和策略
怎样用钩子客户化执行进程
我们还给出代码的示例说明:
怎样探测默认浏览器
怎样运行程序和等待它们终止
怎样显示一个文件的‘属性’对话框
怎样显示‘查找’对话框
怎样防止用户访问某些文件夹或运行某些应用
开始先让我们看一看CreateProcess()比老的WinExec()有多先进。
从WinExec()到CreateProcess()
在Windows3.1下,WinExec()是运行外部程序唯一的方法。它有一个全世界最简单的原型—只要需要瞥一眼就能飞快地记住它。如果想要启动程序,只需要指定它的名字和新窗口属性就可以了:
UINT WinExec(LPCSTR lpCmdLine, UINT uCmdShow);
lpCmdLine参数是一个NULL终止的串指针,指向包含要启动应用的命令行:程序名和它所接受的变量。uCmdShow是一个众所周知的SW_常量,指定结果窗口的属性:图标形式的,最大化的,隐藏的,可视的等。WinExec()的主要缺点是不能探测到新启动的进程是否已经终止,尤其是在很多实际应用要求同步操作时。WinExec()的返回值如果小于32,则是一个错误码,并且是与任务相关的。
在能够找到一种方法建立调用者与被调用者之间的最小通讯级别之前,做这些同步是非正式的,非本质的,有潜在不安全隐患存在的。
外观上,WinExec()在从16位到32位过渡期间已经有了一点变化。而内部,它调用CreateProcess()来辅助执行。后者是我们在建立新进程时要讨论的第一个函数。
WinExec()与CreateProcess()函数的比较
找出WinExec()和CreateProcess()不同能力的一个好办法是比较它们各自的原型。前面已经看到了WinExec()的原型,现在我们来看看CreateProcess()函数的:
BOOL CreateProcess(LPCTSTR lpApplicationName, // 指向可执行模块名的指针
LPTSTR lpCommandLine, // 指向命令行串的指针
LPSECURITY_ATTRIBUTES lpPA, // 指向进程安全属性的指针
LPSECURITY_ATTRIBUTES lpTA, // 指向线程安全属性的指针
BOOL bInheritHandles, // Handle标志
DWORD dwCreationFlags, // 建立标志
LPVOID lpEnvironment, // 指向新环境块的指针
LPCTSTR lpCurrentDir, // 指向当前目录名的指针
LPSTARTUPINFO lpStartupInfo, // 指向STARTUPINFO的指针
LPPROCESS_INFORMATION lpPI // 指向PROCESS_INFORMATION的指针
);
如上所见,这个函数有许多新特征,在MSDN上它是有完善的资料说明的,所以在这里我们不再给出详细解释。下面是测试CreateProcess()函数的最小调用代码:
ZeroMemory(&si, sizeof(STARTUPINFO));
bResult = CreateProcess(NULL, szPrgName, NULL, NULL, TRUE,
NORMAL_PRIORITY_CLASS, NULL, NULL, &si, &pi);
无论怎么看,这个函数都比WinExec()要复杂得多。然而CreateProcess()至少有一个实用的优点。尽管导出一个新进程需要做点工作,如上面代码说明,等待它终止实际上是容易的并且代价也相对低廉。
BOOL WinExecEx(LPCSTR lpCmdLine, UINT uCmdShow)
{
PROCESS_INFORMATION pi;
STARTUPINFO si;
// 建立新进程
ZeroMemory(&si, sizeof(STARTUPINFO));
BOOL b = CreateProcess(NULL, const_cast<LPTSTR>(lpCmdLine), NULL,
NULL, TRUE, NORMAL_PRIORITY_CLASS, NULL, NULL, &si, &pi);
if(!b)
return FALSE;
// 阻塞调用者线程
WaitForSingleObject(pi.hProcess, INFINITE);
return TRUE;
}
上面代码显示了一个在CreateProcess()和WinExec()之间功能的差异。一旦新进程建立,它就阻塞了调用者线程,以等待这个进程Handle变成信号状态。由CreateProcess()通过PROCESS_INFORMATION结构的hPROCESS成员返回的进程Handle是一个核心对象,它仅在Handle本身为NULL时变成信号状态。而进程Handle只有在进程终止时才变成NULL。
如果调用上面函数的程序有一个用户界面,则应该首先最小化和隐藏窗口,这样可以避免窗口绘制时出问题,因为应用被挂起时,它停止响应,和拒绝接收任何消息。如果窗口绘制消息被忽略,则在移动和重叠发生时窗口就不能被适当地重绘。
CreateProcess()是天赐甘露吗
CreateProcess()显然已经解决了WinExec()存在的所有问题。我们能够启动程序,同步它们,和可以对整个启动的过程进行控制。是否还有其它方面的需求呢?从导出应用的角度来看,回答是没有了,但是对于文档要做些什么?更明确地,对于得益于Shell新概念的文档要做些什么?如果你认为CreateProcess()所作的已经包含了这些,虽然正确,但也仅仅是全部内容的一小部分。可执行程序在传统意义上仅是一种类型的文档。Windows9x的演进遵循了以文档为中心的理念,此时动词‘执行’只是“使用”的同义词。一般而言,你以及你的用户正在使用的是所有类型的文档,而不仅仅是程序。
ShellExecute()函数
ShellExecute()函数是WinExec()的一个封装函数,最初出现在Windows3x中,是第一个试图实现Windows的文档为中心概念的第一个例子函数。它已经融进了Windows95和后来的系统中,为兼容而保留了相同的原型。在Win32下,它封装了WinExec()和CreateProcess()这两个函数,前面说过,WinExec()内部也使用CreateProcess()。这个函数的原型如下:
HINSTANCE ShellExecute(HWND hwnd,
LPCTSTR lpOperation,
LPCTSTR lpFile,
LPCTSTR lpParameters,
LPCTSTR lpDirectory,
INT nShowCmd);
它接受比WinExec()更多的变量,但是开始一眼看到它并没有CreateProcess()函数那么有力。尽管如此,ShellExecute()函数实际的差别在于它有处理文件类型关联的能力。也就是说,当你传递的是非可执行文件名时,这个函数扫视注册表,搜索能处理这种类型文档的可执行程序。只要这个处理器存在,这个函数将工作。
在这一章的后面将给出这个重要概念的解释。现在让我们更进一步查看这个函数的原型。
参数 |
描述 |
hwnd |
函数显示消息框的父窗口 |
lpOperation |
表示想要在文件上执行操作的串(见下面) |
lpFile |
函数操作的文件名,它可以是可执行的或文档的。 |
lpParameters |
传递给可执行文件的参数串,如果lpFile是文档文件则忽略。 |
lpDirectory |
可执行文件的工作目录。如果lpFile是文档文件则忽略。 |
nShowCmd |
新建窗口的显示属性——SW_型常量之一,如果lpFile是文档文件,这个标志被忽略。 |
可以在文件上执行的操作(也叫动词)依据文件类型稍有不同。公共操作在下表中列出:
操作 |
应用于 |
Open |
程序,文档,文件夹 |
Explore |
文件夹 |
|
文档 |
Print to |
文档 |
Find |
文件夹 |
注意这里的‘公共操作’,是指由所有文件类型共同支持的动词。还是有点不确定,例如,可以想象一下打印一个可执行文件,此时,‘应用于’列表示命令正常应用的仅仅是文档类型。没有固定和严厉的规则。在给出代码时,没有任何东西能够防止你对可执行文件执行打印操作,或对一定类型恢复信息片的文档执行查找操作。
默认的操作是‘打开’,而且在设置lpOperation变量为NULL时发生打开操作。然而,接受的操作名并不自动说明操作将被执行。函数需要确定执行响应动词的命令行,以及存储在注册表中的信息。进一步讨论这个函数之前,需要更详细的解释‘公共’操作。
打开操作
你可以调用‘打开’来打开文档,运行程序,或解析一个快捷方式的引用。要这样做,可能需要对其它参数指定特殊的值:
ShellExecute(hwnd, __TEXT("open"), __TEXT("c://prg.exe"),
__TEXT("/nologo"), __TEXT("d://"), SW_SHOW);
上面这行代码运行c:/prg.exe,传递/nologo作为命令行,并指定d:/作为它的工作目录。如果被打开的文档不是可执行文档,则lpParameters和lpDirectory参数就被忽略。对于nCmdShow参数而言,相同的情况并不出现。下面代码段显示怎样用注册的处理程序打开一个.txt文档(正常应该是notepad.exe):
ShellExecute(hwnd, __TEXT("open"), __TEXT("c://file.txt"),NULL, NULL, SW_SHOW);
其中重要的是仅当有一个能够处理文档的注册的程序存在,ShellExecute()才能打开这个文档。如果没有程序注册,则函数提示你指定这个程序:
有的文件类型对‘打开’的概念是不明确的。例如,假设有一个VB脚本.vbs文件,当我们讲‘打开’这个文件时,我们的企图是要编辑它的内容还是要运行它呢,在Shell关联下,第二个操作是正确的。
探测操作
探测操作仅能应用于文件夹。它显示给定文件夹的内容,此时的文件夹在探测与打开操作之间有一点微妙的差异,二者都是通过探测器观察文件夹的内容,但是,使用后者,我们获得的是一个新的单独的文件夹窗口,而前者是建立两个并列的窗口:
探测操作对于文件夹自动执行—即‘文件夹’类型的文档。在Windows安装过程中这个设置就被保存了。然而,如果要使探测操作感觉到你的文档,你应该注册它。下面的代码导出图像显示的新窗口:
ShellExecute(hwnd, __TEXT("explore"), __TEXT("c://windows"),NULL, NULL, SW_SHOW);
打印操作
‘打印’操作是指打印文档,但是这依赖于存储在注册表中的对应程序的信息和能打印指定文档的命令行信息。下面一小段代码说明怎样打印文本文件:
ShellExecute(hwnd, __TEXT("print"), __TEXT("c://file.txt"),NULL, NULL, SW_SHOW);
此时,函数搜索处理文本文件的注册程序,并查看它是否支持打印命令。一般这个程序是notepad.exe,命令行形式为:
notepad.exe /p <file_name>
注意,使用 /p是用命令行输出打印命令的普遍支持的方式。
默认情况下打印操作试图使用默认打印机。然而在整个Windows Shell范围内,用户都可以通过拖动文件到打印机图标打印文档。此时shell执行的代码如下:
ShellExecute(NULL, __TEXT("printto"), filename, NULL, NULL, SW_SHOW);
正如你所看见的,还有另一种操作叫‘打印到’。如果文档支持这个操作,注册的命令行就被执行,否则将有一个提示框出现:
与打开和探测操作不同,文件夹并不是与生俱来地支持‘打印’和‘打印到’操作。‘打印到’操作允许你在Shell中使用非默认打印机打印文档。除了文件名之外,还有三个必须传递的附加参数,以及那些在lpParameter中格式化的变量。第一个是出现在‘打印机’文件夹中的打印机名,其他两个是驱动器名和端口名。如果你增加对‘打印到’命令的支持,命令行应该有如下形式:
MyProgram.exe %1 %2 %3 %4
这里第一个参数是文件名,还要考虑一般Windows程序实际使用 /pt 指定打印机端口。换句话说,如果你写一个想要在不同的打印机(连接到不同端口)上打印文档的应用程序,我们建议你用/pt前缀侦测应用命令行上的端口选项:
MyProgram.exe /pt %1 %2 %3 %4
然而这仅仅是习惯而已,不是规则。
打印到端口
要求附加的三个命令行参数是允许系统构建请求打印机的设备关联(DC)。设备关联是WindowsGDI函数发送输出的逻辑界面,对于每一个想要操作的打印机都需要一个特定的DC。打印机DC使用CreateDC() API函数建立。其原型是:
HDC CreateDC(LPCTSTR lpszDriver, // 驱动器名(e.g. wspuni.drv)
LPCTSTR lpszDevice, // 设备名 (e.g. OKIPage 4W)
LPCTSTR lpszOutput, // 没有使用,设置为NULL
CONST DEVMODE* lpInitData); // 打印机数据选项
有趣的是,这个函数维持了Windows3.1下的相同原型,尽管事实上实际仅使用一个变量:lpszDevice。在Win32下打印机通过描述名来识别,并不需要显式地提供驱动器名和端口名的信息。在Windows3.1中,lpszOutput是具有端口名(如LPT1)的串指针。在Win32下这个信息仍然很重要,但没有直接的理由。这个函数使用设备名作为搜索键在注册表中查找实体,以及其它保存的信息。
查找操作
除了上面提到的操作之外,还有另一个完全被资料忽略的操作,这个操作仅由最近的知识库文章略有提及。它就是‘查找’操作,它应用于文件夹。作用是运行‘查找’:‘所有文件’系统对话框,从指定文件夹开始:
这一节的重点是ShellExecute()没有限制lpOperation参数的内容必须是有限的串集合。通过编辑注册表,你可以加入新的动词,并连接到命令行上。在这一章的后面我们将看到这个活动。
一个不应该的资料错误
我们并不是每天都调用ShellExecute(),而且我们也承认,没有仔细地研读资料。在很多情况下,我们都自信的觉得已经完全弄懂了这个函数的作用,随着时间的流逝,我们形成了固有的概念,它就是一个WinExec()函数的高度专门的版本,而且在写一个调用时仅依赖于记忆而不是参考说明资料,因此我们总是给nCmdShow 变量传递SW_SHOW值。
直到有一天,察看了资料后,我们发现nCmdShow应该是0,如果lpFile不是指向可执行文件的话。为什么会这样?我们很奇怪,据判断,SW_SHOW应该是唯一可能的值。
为了验证这个资料,我们把SW_SHOW换成0,但是从那以后,我们仅能导出程序而不再能打开文档了。我们花费了几天时间来认识,究竟发生了什么。而我们测试的越多,我们的系统就变得越慢。
虽然有资料说明,我们传递给nCmdShow的值在目标不是可执行文件时也不被忽略。通过传递0,就等于告诉函数显示一个有SW_HIDE属性的新窗口。在运行了一串测试应用并发现了这一点之后,我们打开了任务管理器,发现有许多Notepad,WScript和MSPaint 的实例存在,但是都是隐藏的。
这是明显的资料说明错。这个错误直到现在MSDN中都没有提到。
动词的更详细资料
到目前为止我们已经讨论过的所有操作(也称为动词)都有一个直觉的实现。例如,要‘打开’程序,意思是调用CreateProcess()。探测甚至打开文件夹通常都是使用特定的命令行标志调用explorer.exe,即使查找对话框,也是由Shell的一个内部函数完成的—文件夹名只不过是一个传递给它的变量。所有这些都是简单而且‘静止’的。
然而,当需要打开一个普通的文档时,事情开始变得复杂了。在这一章的开始我们提到的VB脚本文件,哪一个程序知道它是什么,它是否要求一个特殊的命令行?更重要的是Shell怎样知道这些。这就是动词和文件处理器这对概念起作用的地方。
动词和文件处理器
正像我们前面提到过的那样,动词是一个表示‘动作’的串,程序可以在特殊类型的文件上执行这个动作。程序可以调用文件处理器,因为文件处理器知道怎样处理这种类型的文档。
在文件类型,动词和处理器三者之间的联系可以在系统注册表中看到。在键HKEY_CLASSES_ROOT下:
图中显示了包含所有文件扩展名列表的子树,其中就可能有正确注册的处理器。一个节点—如,.xyz—对于由Shell完全管理的文件类型是不充分的。为了说明这一点,我们考虑VB脚本文件,其扩展名为.VBS。它简单地是一个包含VB脚本源码的ASCII码文件。
在HKEY_CLASSES_ROOT下的.VBS键仅是第一步。在它的默认实体中 ,这个节点包含了一个串,它是指向同一个子树下的另一个键,此时是:
HKEY_CLASSES_ROOT
/VBSFile
这个节点的默认实体定义了探测器作为文件类型描述的串,并且,在它之下是与我们这里讨论相关的节。其中包含了所有定义的动词,以及它们的命令行。对于VB脚本而言(在我的PC上)情形描述如下图所示:
每一个‘动词’键都有一个Command子键,其默认实体指向命令行。正是这个串确定了Shell的实际行为。此时,要打开(或运行)一个VB脚本,Shell就必须使用wscript.exe文件。相反,打印VB脚本只要求调用Notepad,使用习惯的 /p 选择项:
C:/WINDOWS/Notepad.exe /p %1
如我们可能知道的那样,这些动词是特定类型文件关联菜单的主要部件:
这些动词的内容由程序和有经验的终端用户所决定。毕竟还没有告诉你怎样打印一个文档:直到你(或你安装的程序)知道怎样做的方法之后,并且把命令行保存到注册表中,才能理解为什么这样。从Shell的角度观察,整个过程就是它能够在HKEY_CLASSES_ROOT/DocumentType/Shell键下找到打印键,并且有一个command子键。
这里要注意的是‘打印’不是由ShellExecute()处理的键,而是一个与注册表中命令行适当关联的字,是命令行才能够打印指定类型的文档。
执行动词
让我们看一下ShellExecute()实际是怎样处理一个调用的。这可以澄清这个函数的使用方法,和怎样编辑注册表。我们分析的调用代码如下:
ShellExecute(hwnd, __TEXT("OpenWithIE"), __TEXT("file.txt"),NULL, NULL, SW_SHOW);
这里这个动词是相当陌生的,它的调用意图是应用于文本文件。对ShellExecute()函数,这个动词没有问题,它正好遵循正常的过程:
找出这个文档的Shell注册表键的路径
搜索OpenWithIE键
读出命令子键
使用CreateProcess()执行指定的命令行,文档名作为变量传递
首先,假设某人已经建立了如图所示的键:
这个函数在调用命令行后终止:
"C:/PROGRA~1/INTERN~1/iexplore.exe" -nohome %1
此时它用文件名file.txt,替换%1,而后转到CreateProcess():
CreateProcess(NULL, const_cast<LPTSTR>(lpCmdLine), NULL, NULL, TRUE,
NORMAL_PRIORITY_CLASS, NULL, NULL, &si, &pi);
此时lpCmdLine变为:
"C:/PROGRA~1/INTERN~1/iexplore.exe" -nohome file.txt
最后调用结果是IE将以只读模式打开这个文本文件。
静态和动态动词
在注册表中找到的所有键都是静态动词,并且它们是我们可能期望一个类似于ShellExecute()的函数能识别的唯一一类动词。然而,还有一类动态动词,它们是关联菜单项,它们是在可能飞快变化的条件下或依赖于文件在运行时添加上去的。处理静态动词,Shell总是建立新的进程,并从注册表的命令行启动。相反,动态动词由Shell扩展处理,它与Shell存在于同一个进程之中。
获取文件的可执行名
如果你的目的仅仅是简单地打开特定文件,则ShellExecute()函数就是你所需要的,你需要提供的仅仅是想要打开的文件名字。然而,可能还有其它的情况发生,比如,你希望知道当前计算机上注册的处理给定类型文件的确切的程序名。让我们来看一个例子。
浏览器是怎样感觉到它不再是默认浏览器的,你是否曾经怀疑过,这怎么解释呢?概略地说,它们试着读取处理HTML文件所注册的可执行文件名。如果不能找到它们的名字,则实际上你已经改变了默认浏览器的设置。
Windows SDK定义了一个返回注册处理给定类型文件的可执行文件名的函数FindExecutable(),它定义在shellapi.dll中:
HINSTANCE FindExecutable(LPCTSTR lpFile, LPCTSTR lpDirectory, LPTSTR lpResult);
这个原型是很好理解的,它接受一个文档名,如果文档名不是全路径名,还需要一个基目录。如果文件名是完整的(包括驱动器和目录),则lpDirectory就是多余的。通过lpResult返回文件名。
FindExecutable()为给定的文件扩展名搜索注册表,并返回shell/open/command/default实体的内容。
FindExecutable()函数的瑕疵
关于这个函数有几点需要说明:FindExecutable()不是完美的,它至少有一个已知的Bug。从一开始,说明资料就声称返回值大于32 表示成功,我们并没有想到它的实际意义—它是一个HINSTANCE,一个DDE的会话ID或一个随机数什么的。在Win32平台上,我们发现它是一个不甚合理的布尔值。
文件必须存在
FindExecutable()还有另外两个更奇怪的缺陷。头一个与文件名相关:你传递的文件名必须存在,这在资料中并未明确地说明。我们认为这是为了保持向后兼容。由于传递的文件本身不存在就不能恢复相关的可执行文件名是不合理的,函数所探求的仅仅是文件扩展名的信息。如果需要知道默认浏览器的名字,使用*.htm为文件名调用FindExecutable()就应该能知道。为了说明这些不是演绎出来的想法,我们可以回顾SHGetFileInfo()函数(在第四章中)。
消除路径名中的空格
最后,FindExecutable()有一个实在的问题是包含空格的长文件名问题。这是一个明显的,微软完全知道的问题。
在请求FindExecutable()恢复路径名时,它读注册表并返回一个串,问题是,有时这个串包含有命令行变量,而要函数来确定变量的开始就不容易了,应该遵循的好习惯是用引号把文件名括起来,这样函数才能断定最后引号后的是变量:
Default = "c:/my Dir/theApp.exe" /n
如果不这样,函数将截断串以定位可能的命令行变量,因为它总是假设变量从第一个空格开始。因而,如果路径中含有空格,它就会被截断。例如下面的串:
c:/My Dir/theapp.exe
则实际返回的是:
c:/My
我们注意到IE在注册表中注册的是8.3格式的(在HKEY_CLASSES_ROOT/htmlfile/shell/command键下),而不是它的长文件名。很多其它的微软程序都是这样的。这是因为FindExecutable()使用长文件名有问题所致。
无限制地使用长文件名
这个问题很早就已经提出了,然而FindExecutable()仍然没有解决,它仅简单地继承了无限制地使用长文件名导致的复杂性,因此我们再次强调,一定要使用引号封装文件名。
有些MSDN文章指出,可以用空格(ASCII 32)替换返回串的终止符 /0 这个串将被确定。不幸的是这并不总是对的。
微软是正确的,它认为用空格替换返回串的终止符可以使串恢复到初始状态。这是因为存储缓冲实际并没有被改变和清除。下面的函数应该能很好地工作:
HINSTANCE FindExecutableEx(LPCTSTR lpFile, LPCTSTR lpDirectory, LPTSTR lpResult)
{
HINSTANCE hi = FindExecutable(lpFile, lpDirectory, lpResult);
lpResult[lstrlen(lpesult)] = 32; // 32 is the ASCII value of space
return hi;
}
这里有一个小问题,对于这个修改,我们返回的是从注册表中读出的串。而麻烦就是这个串有命令行变量,这是个最初就提到的问题。例如假设shell/command的串是:
c:/My Dir/theapp.exe %1
进一步假设希望的可执行名是处理c:/myFile.xyz文件的可执行程序名。正常情况下FindExecutable()返回:
c:/My
使用FindExecutableEx()函数后返回的串为:
c:/My Dir/theapp.exe c:/myFile.xyz
现在留给你的是要抽取实际的文件名,记住,你不能依靠空格来割裂串,因为可能是带有空格的长文件名,甚至在扩展名中。
适用于FindExecutable()的一种更可靠方法
如果执行老的_splitpath()函数来分离文件名,像上面的串,返回到目录项应该是/My Dir/theapp.exe c:/。
_splitpath()函数总是抽取第一个和最后一个反斜杠之间的串。如果有一个文件名作为变量,则‘:’将总被抽取。因此可以再次在‘:’点上分解这个串。下面就是它的代码:
HINSTANCE FindExecutableEx(LPCTSTR lpFile, LPCTSTR lpDirectory, LPTSTR lpResult)
{
// 这些 _MAX 常量在stdlib.h中定义
TCHAR drive[_MAX_DRIVE];
TCHAR dir[_MAX_DIR];
TCHAR dir1[_MAX_DIR];
TCHAR file[_MAX_FNAME];
TCHAR ext[_MAX_EXT];
HINSTANCE hi = FindExecutable(lpFile, lpDirectory, lpResult);
lpResult[lstrlen(lpResult)] = 32;
_splitpath(lpResult, drive, dir, file, ext);
// 在目录名中搜索 : 并截断这个串
LPTSTR p = strchr(dir, ':');
if(p != NULL)
{
--p;
dir[p - dir] = 0;
// 现在再次分解以获得文件和扩展
_splitpath(dir, NULL, dir1, file, ext);
_makepath(lpResult, drive, dir1, file, ext);
}
return hi;
}
它是否能工作,在命令行上没有东西时它提供了什么,换句话说,下面的串是命令行时,这个函数是OK的:
c:/My Dir/theapp.exe %1 [whatever you want]
但是如果这样,则仍然有问题:
c:/My Dir/theapp.exe [option list] %1
很不幸,这并不是一个通用的选择,我们不能提供100%安全的解决方案—我们一点也不能保证有这样的方案。如果使用长文件名,你应该知道在*解析时很难做到没有缺陷。
有一个选项列表附加在文件扩展名之后,除非其中包含了不正确的长文件名字符,扩展名中包含空格如.exe –p也是完全可以接受的,此外,_splitpath()和_makepath()只是处理串而不检查长文件名的兼容性。
简短截说,截断文件的扩展名到第一个空格是最接近的首选。我们从来也没有看到过在文件扩展名中使用空格的现象。因此最终的FindExecutableEx()函数应该有下面形式:
HINSTANCE FindExecutableEx(LPCTSTR lpFile, LPCTSTR lpDirectory, LPTSTR lpResult)
{
...
// 在目录中搜索 : ,并截断它
LPTSTR p = strchr(dir, ':');
if(p != NULL)
{
--p;
dir[p - dir] = 0;
// 现在再次分解以获得文件和扩展名
_splitpath(dir, NULL, dir1, file, ext);
p = strchr(ext, 32);
ext[p - ext] = 0;
_makepath(lpResult, drive, dir1, file, ext);
}
return hi;
}
FindExecutable()函数的这个Bug在Windows95发布几个月之后就出现了,而且远没有得到解决,继续在Windows98 中存在。
你可能知道。Shell4.71以上版支持一个新库shlwapi.dll,其中有几个处理串和路径名的函数十分有用。这些函数对解决这个Bug是否有帮助,比如PathRemoveArgs()。不幸的是它们不能—我们已经测试过了,它们没有足够的智慧来处理长文件名。
ShellExecute()的技巧和窍门
我们早期提到过的ShellExecute()函数在文件和系统对象上执行操作是非常有用的。除此之外,在与FindExecutable()联和使用时,可以帮助你更快地执行某些有刺激性的任务。下面是一些这方面的例子。
感知默认的浏览器
为了确定机器的默认浏览器,你需要指定一个.htm文件到FindExecutable()函数,一个自包含例程可以快速建立空文件,调用FindExecutable()函数,然后删除这个空文件:
void GetDefaultBrowser(LPTSTR szBrowserName)
{
HFILE h =_lcreat("dummy.htm", 0);
_lclose(h);
FindExecutable("dummy.htm", NULL, szBrowserName);
DeleteFile("dummy.htm");
}
当然,要感觉默认浏览器是IE还是Netscape,你还可以直接检查注册表HKEY_CLASSES_ROOT/.htm键下的Default实体值。如果是htmlfile则浏览器是IE,如果是NetscapeMarkup,则浏览器是Netscape。每个浏览器都在注册表子树中设置自己的不同值。只要改变了这个值,默认浏览器就改变了。
连接到URL
如果你需要知道浏览器名以便连接到远程URL,或查看HTML文件,有一个快速方案:ShellExecute()。
ShellExecute(NULL, NULL, __TEXT("http://www.wrox.com"), NULL, NULL, SW_SHOW);
这个函数本身就执行恢复和导出浏览器的操作(如果有一个安装的)。在文件名有一个前缀‘http’时,ShellExecute()函数在HKEY_CLASSES_ROOT/http/shell/open/command下进行搜索。
发送Email消息
要编程发送e-mail消息,你可以有几个选择:有协作数据对象,消息API,或依赖于其它应用提供的服务如微软的OutLook。我们总是在HTML页上采用这个任务的简化形式,这只需要一个特定的‘mailto’协议连接:
<A href=mailto:desposito@infomedia.it>Dino Esposito</A>
这里,采用ShellExecute(),同样便捷的操作对于Windows程序也是可用的:
ShellExecute(NULL, NULL, __TEXT("mailto:desposito@infomedia.it"),
NULL, NULL, SW_SHOW);
再次需要注册表中的键:
HKEY_CLASSES_ROOT
/mailto
/shell
/open
/command
http 和 mailto是可插入协议的例子—构建于一个进程内COM服务器中,客户URL协议通过访问这个资源过程指导浏览器操作。使用ShellExecute(),你就可以通过任何注册的协议唤醒资源,甚至是象res这样的客户协议。
打印文档
只要允许使用命令行打印某种文档的程序存在,你就可以输出如下命令:
ShellExecute(NULL, __TEXT("print"), szDocName, NULL, NULL, SW_SHOW);
一般的习惯是允许在命令行上使用 /p 选项打印文档,但也有一种习惯—不使用任何选项来表示打印操作。
查找文件和文件夹
如果需要从特定文件夹开始运行‘查找’对话框,这样调用比较容易:
ShellExecute(NULL, __TEXT("find"), szDirName, NULL, NULL, SW_SHOW);
如果指定了一个NULL或空串作为文件夹名,这个对话框将显示准备在C驱动器上开始工作。如果传递一个非零的串,指向一个不存在文件夹,函数将返回错误。
ShellExecute()对CreateProcess()
为了与CreateProcess()函数比较,我们已经足够详细地解释了ShellExecute()。哪一个函数比较好这一点,使用哪一个函数建立进程,这一点是无法确定的(它们有相当多的不同,二者都非常有用)。
第一个要说明的是,ShellExecute()内部调用了CreateProcess(),所以ShellExecute()必然是CreateProcess()函数的一个更小和用法更简单的封装。反之,ShellExecute()在打开和打印文档方面是足够灵活的。没有涉及可用文档类的特定动词。除非你需要使用CreateProcess()建立探索高级属性的进程(Debug模式,环境设置,启动信息等),否则我们建议你总应该选择ShellExecute(),它有一个更简单的文法规则。
为什么应该使用ShellExecute()来运行程序
另一个使天平向ShellExecute()倾斜的论点来自微软新需求方案的建议—这是一本在建立微软Windows98和WindowsNT兼容的应用时需要参考的手册。
微软建议你使用ShellExecute()来运行外部应用,是因为它能确保系统管理员采用的任何策略限制被仔细地检查。系统策略允许管理者确定哪些应用能或不能从Windows启动。ShellExecute()采用黑名单执行这个过程,而CreateProcess()没有。
策略
策略简单地是相关设置集,它们一般保存在系统注册表中。其中最有趣的一个是Shell约束策略,其中所包含的注册表实体使你可以控制‘开始’菜单和探测器的功能。
其中你可以做的是防止Shell在开始菜单中显示‘运行’或‘查找’项。同样,你已可以禁止使用‘控制板’或任务条‘属性’对话框改变设置。下面就让我们看一看怎样做这些设置。
Shell约束策略
Shell约束策略涉及的注册表键是:
HKEY_CURRENT_USER
/Software
/Microsoft
/Windows
/CurrentVersion
/Policies
/Explorer
为了说明其轮廓,你需要建立某些不存在的新实体,它们应有默认设置0或1:
实体 |
描述 |
NoRun |
如果这个实体设置为1,则在开始菜单中隐藏‘运行…’命令 |
NoFind |
如果为1,则在开始菜单中隐藏‘查找’命令 |
NoSetFolders |
如果为1,则在开始菜单中隐藏所有标准的‘设置’命令 |
NoSetTaskbar |
如果为1,则隐藏‘任务条属性’对话框 |
要使更新发生,所有实体都必须是DWORD。在以这种方式删除命令时,改变立即发生,然而用户界面在重新引导之前并不更新。此时如果试着使用一个命令,将会得到下面的信息框:
关于实现策略的注册键信息来源可以在MSDN库的平台SDK范围找到。
扩展ShellExecute()
尽管支持策略设置,ShellExecute()还是有一个难于使用的重大障碍:它不能返回或使你知道新建进程的Handle。也就是说,你不能导出程序并在继续执行之前等待它终止。换句话说ShellExecute()受到了它的16位血统的损害,它仅仅发掘了新的和更有威力的函数CreateProcess()的一个特征子集—WinExec()也支持的子集。
然而在4.0以后版本中引进了一个新函数:ShellExecuteEx()。它有一个Shell函数典型的原型,支持多标志,以及上述所有功能,通过提供对进程同步和PIDLs的支持扩展了ShellExecute()。
ShellExecuteEx()函数
ShellExecuteEx()函数明确地取代了ShellExecute()。它在shellapi.h中声明:
BOOL ShellExecuteEx(LPSHELLEXECUTEINFO lpExecInfo);
SHELLEXECUTEINFO定义如下:
typedef struct _SHELLEXECUTEINFO
{
DWORD cbSize;
ULONG fMask;
HWND hwnd;
LPCTSTR lpVerb;
LPCTSTR lpFile;
LPCTSTR lpParameters;
LPCTSTR lpDirectory;
int nShow;
HINSTANCE hInstApp;
// 可选的成员
LPVOID lpIDList;
LPCSTR lpClass;
HKEY hkeyClass;
DWORD dwHotKey;
HANDLE hIcon;
HANDLE hProcess;
} SHELLEXECUTEINFO, FAR *LPSHELLEXECUTEINFO;
在使用这个结构之前,我们极力建议你把它充填为0,并设置cbSize到结构的实际长度,操作如下:
SHELLEXECUTEINFO sei;
ZeroMemory(&sei, sizeof(SHELLEXECUTEINFO));
sei.cbSize = sizeof(SHELLEXECUTEINFO);
正如声明中的注释所说,结构的成员分成了两组。实际上,头一组使ShellExecuteEx()的功能等价于ShellExecute()。而选项成员组使函数更有力,这正是‘Ex’后缀的由来。
hwnd, lpVerb, lpFile, lpParameters, lpDirectory 和 nShow成员等价于ShellExecute()的参数,这是我们已经看到的。而hInstApp成员则是一个输出缓冲,这将由ShellExecute()的返回值填写。
nShow成员总是表示建立窗口的风格,即使lpFile是一个应用程序,它也仅仅说明应用应该怎样显示。无论lpFile是应用程序还是文档文件,nShow必须总是赋值为SW_型常量,你是知道的,如果设置为0将获得隐藏窗口。
下面是调用ShellExecuteEx()的最简单方法:
SHELLEXECUTEINFO sei;
ZeroMemory(&sei, sizeof(SHELLEXECUTEINFO));
sei.cbSize = sizeof(SHELLEXECUTEINFO);
sei.lpFile = __TEXT("explorer.exe");
sei.nShow = SW_SHOW;
sei.lpVerb = __TEXT("open");
ShellExecuteEx(&sei);
可选成员
没有在ShellExecute()中对应参数的成员之一是fMask。它可以是一个或多个下面值的组合:
标志 |
描述 |
SEE_MASK_CLASSKEY |
应该使用 hkeyClass 成员 |
SEE_MASK_CLASSNAME |
应该使用 lpClass 成员 |
SEE_MASK_CONNECTNETDRV |
lpFile将被解释成UNC(通用命名习惯)格式的文件名 |
SEE_MASK_DOENVSUBST |
任何在lpDirectory和lpFile成员中的环境变量都将被展开,例如,%WINDIR% 打开Windows文件夹 |
SEE_MASK_FLAG_DDEWAIT |
如果函数启动DDE会话,在返回之前等待它终止。 |
SEE_MASK_FLAG_NO_UI |
在错误情况下不显示消息框 |
SEE_MASK_HOTKEY |
应该使用 dwHotkey 成员 |
SEE_MASK_ICON |
应该使用 hIcon 成员 |
SEE_MASK_IDLIST |
强制函数使用lpIDList内容代替lpFile |
SEE_MASK_INVOKEIDLIST |
引起函数使用lpIDList中指定的PIDL。如果这个成员为NULL,则建立一个lpFile的PIDL,并使用这个PIDL。这个标志重载了SEE_MASK_IDLIST |
SEE_MASK_NOCLOSEPROCESS |
用进程Handle设置hProcess成员。lpIDList成员可以包含一个用于代替lpFile的PIDL。hProcess返回导出的HPROCESS类型的新进程handle |
附加的特征
可选字段适用于某些超出ShellExecute()的附加功能。第一点,也是最重要的一点,可以使用PIDLs来运行应用和打开文件夹。下面是打开‘打印机’文件夹的代码:
LPITEMIDLIST pidl;
SHGetSpecialFolderLocation(NULL, CSIDL_PRINTERS, &pidl);
SHELLEXECUTEINFO sei;
ZeroMemory(&sei, sizeof(SHELLEXECUTEINFO));
sei.cbSize = sizeof(SHELLEXECUTEINFO);
sei.nShow = SW_SHOW;
sei.lpIDList = pidl;
sei.fMask = SEE_MASK_INVOKEIDLIST;
sei.lpVerb = __TEXT("open");
ShellExecuteEx(&sei);
如果指定了SEE_MASK_DOENVSUBST标志,则可以在lpFile和lpDirectory中使用任何环境变量。例如,要打开Windows目录,可以表示为%WINDIR%。
最后,我们获得了由ShellExecuteEx()导出的应用的同步能力。在设置了SEE_MASK_NOCLOSEPROCESS位到fMask成员后,新进程的handle将由hProcess成员返回,因此这一行代码:
WaitForSingleObject(sei.hProcess, INFINITE);
将导致调用的应用阻塞,等待另一个应用终止。
显示文件属性对话框
SEE_MASK_INVOKEIDLIST标志是一个重要标志,因为这是ShellExecuteEx()另一个优于ShellExecute()的亮点:它允许函数象执行静态动词那样唤醒动态动词。前面解释过,动态动词是运行时由Shell扩展的关联菜单添加的。其工作方法是:如果ShellExecuteEx()不能在静态动词列表中找到这个动词,它就试图寻找给定文件的关联菜单。这个搜索引出IContextMenu接口指针。然后通过接口暴露的方法唤醒动态动词。
作为这个操作的结论,我们可以很容易地显示文件的属性对话框—与右击文件,然后选择属性显示的对话框相同。这里是一个简单的例子函数:
void ShowFileProperties(LPCTSTR szPathName)
{
SHELLEXECUTEINFO sei;
ZeroMemory(&sei, sizeof(SHELLEXECUTEINFO));
sei.cbSize = sizeof(SHELLEXECUTEINFO);
sei.lpFile = szPathName;
sei.nShow = SW_SHOW;
sei.fMask = SEE_MASK_INVOKEIDLIST;
sei.lpVerb = __TEXT("properties");
ShellExecuteEx(&sei);
}
ShellExecuteEx()函数的返回值
这个函数返回一个布尔值用来描述调用是否成功:TRUE,成功,而FALSE,则失败。GetLastError()和hInstApp中的返回值可以用来获得调用时发生事件的更多信息。
例子:程序执行器
下面的截图显示了一个示例程序Execute的界面,它允许你测试动词,它是一个基于对话框的应用程序。
可以通过键入名字或浏览按钮来选择特定的测试文件。在‘操作’编辑框中应该输入想要在文件上执行的动词名。头两个按钮—ShellExecute 和 ShellExecuteEx—可以分别测试各自的函数。而FindExecutable按钮则返回打开(总是动词‘打开’)指定文件注册的执行程序名,这个名字显示在‘Executable found’编辑框中,而‘Return’中显示FindExecutable()函数的返回码。
实现这个应用的功能,实际上就是提供这四个按钮的处理器操作代码。OnBrowse()是最容易的,代码如下:
void OnBrowse(HWND hDlg)
{
TCHAR szFile[MAX_PATH] = {0};
TCHAR szWinDir[MAX_PATH] = {0};
GetWindowsDirectory(szWinDir, MAX_PATH);
OPENFILENAME ofn;
ZeroMemory(&ofn, sizeof(OPENFILENAME));
ofn.lStructSize = sizeof(OPENFILENAME);
ofn.lpstrFilter = __TEXT("All files/0*.*/0");
ofn.nMaxFile = MAX_PATH;
ofn.lpstrInitialDir = szWinDir;
ofn.lpstrFile = szFile;
if(!GetOpenFileName(&ofn))
return;
else
SetDlgItemText(hDlg, IDC_FILENAME, ofn.lpstrFile);
}
其次是OnShellExecute(),简单地从对话框中抽取文件名和操作,生成ShellExecute()函数的调用,以及显示返回值:
void OnShellExecute(HWND hDlg)
{
TCHAR sFile[MAX_PATH] = {0};
TCHAR sOp[MAX_PATH] = {0};
TCHAR sRC[MAX_PATH] = {0};
GetDlgItemText(hDlg, IDC_FILENAME, sFile, MAX_PATH);
GetDlgItemText(hDlg, IDC_OPERATION, sOp, MAX_PATH);
HINSTANCE h = ShellExecute(NULL, sOp, sFile, NULL, NULL, SW_SHOW);
wsprintf(sRC, __TEXT("%ld"), h);
SetDlgItemText(hDlg, IDC_RETVAL, sRC);
return;
}
第三步是OnShellExecuteEx(),这与上项恰好相同,只是使用了SHELLEXECUTEINFO结构:
void OnShellExecuteEx(HWND hDlg)
{
TCHAR sFile[MAX_PATH] = {0};
TCHAR sOp[MAX_PATH] = {0};
TCHAR sRC[MAX_PATH] = {0};
GetDlgItemText(hDlg, IDC_FILENAME, sFile, MAX_PATH);
GetDlgItemText(hDlg, IDC_OPERATION, sOp, MAX_PATH);
SHELLEXECUTEINFO sei;
ZeroMemory(&sei, sizeof(SHELLEXECUTEINFO));
sei.cbSize = sizeof(SHELLEXECUTEINFO);
sei.lpFile = sFile;
sei.nShow = SW_SHOW;
sei.fMask = SEE_MASK_DOENVSUBST | SEE_MASK_INVOKEIDLIST;
sei.lpVerb = sOp;
DWORD rc = ShellExecuteEx(&sei);
wsprintf(sRC, __TEXT("%ld"), rc);
SetDlgItemText(hDlg, IDC_RETVAL, sRC);
return;
}
最后是OnFindExec(),使用FindExecutableEx()函数,把前面给出的代码在这里汇集到一起:
void OnFindExec(HWND hDlg)
{
TCHAR sFile[MAX_PATH] = {0};
TCHAR sPrg[MAX_PATH] = {0};
TCHAR sRC[MAX_PATH] = {0};
GetDlgItemText(hDlg, IDC_FILENAME, sFile, MAX_PATH);
HINSTANCE h = FindExecutableEx(sFile, NULL, sPrg);
wsprintf(sRC, __TEXT("%ld"), h);
SetDlgItemText(hDlg, IDC_RETVAL, sRC);
SetDlgItemText(hDlg, IDC_EXE, sPrg);
return;
}
添加#includes shlobj.h, commdlg.h 和resource.h到源程序的顶部,并保证连接到comdlg32.h文件,就可以编译和连接这个应用了。下面的截图显示了它所取得的一个GIF文件的‘属性’对话框:
多监视器支持
作为结束关于ShellExecute()和ShellExecuteEx()函数的讨论,我们给出一个Windows98下的一个新特征—多监视器支持。这是一种编程使输出能够扩张到多个监视器的能力。在Windows98下有一令人惊异的函数MonitorFromPoint(),到目前为止我们还不甚了解这个函数。多监视器支持与ShellExecute()之间有什么关系呢?正好,Windows98 版的这个函数支持多监视器。这就是说,任何子进程都与父进程显示在同一个监视器上。然而,这仅仅是默认行为。如果指定了hwnd参数,你就可以重定向新窗口到有这个hwnd参数窗口所在的监视器上。
ShellExecute()函数上的钩子
你有没有听说过IShellExecuteHook接口?就象它的名字一样,其逻辑遵循传统的Windows钩子模型,而实现则要求你写一个进程内COM服务器。接口方法由ShellExecute()和 ShellExecuteEx()两个函数调用,以使用户获得对启动进程这个过程的更多控制。通过使用IShellExecuteHook接口,模块可以解析(客户方法解析)被执行的命令行,和导出正确的程序。
例如,在MS-DOS下,我们有时写一个小的批处理过程,使用短的或容易键入的名字,以此种方法,我们可以快速并容易地运行程序和执行重复的任务。现在,IShellExecuteHook接口给了我们在Windows下做差不多相同工作的能力。实现了IShellExecuteHook接口的模块在ShellExecute()或 ShellExecuteEx()执行文件动词操作时,无论文件类型如何都被唤醒。这个模块夹在操作中间,可以作任何适合的操作,比如:
跟踪(记录到文件)所有由Shell启动的应用
防止非授权访问某些程序或文件夹
实现命名对象—即,映射到特殊程序或活动的关键字
实现IShellExecuteHook接口实际上是相当容易的。不幸的是没有任何资料说明怎样使Shell知道你已经实现了它,关于这一点,我们将在下一节中讨论。
注册IShellExecuteHook处理器
第一,IShellExecuteHook处理器是一个COM服务器,它必须在下面的路径上适当地注册:
HKEY_CLASSES_ROOT
/CLSID
当然只有这些还远远不够,Windows Shell 必须知道这个处理器存在,以及它放在哪儿。由于IShellExecuteHook处理器与浏览器辅助对象没有多少差别,因此我们认为它们的注册方式应该类似,事实证明我们是正确的。辅助对象和Shell执行钩子二者都必须注册在下面的位置:
HKEY_LOCAL_MACHINE
/Software
/Microsoft
/Windows
/CurrentVersion
/Explorer
辅助对象在‘Browser Helper Objects’键名下,而执行钩子在‘ShellExecuteHooks’键名下:
如上图所示,每一个键下可以包含一个CLSID串集合。Shell遍历这个列表,并装入这些服务器。
IShellExecuteHook接口
IShellExecuteHook是我们目前为止所看到的最简单的COM接口之一。它只由一个函数Execute()组成。下面是这个函数的声明:
HRESULT Execute(LPSHELLEXECUTEINFO pei);
SHELLEXECUTEINFO与我们在ShellExecuteEx()中介绍的结构相同。这个函数由系统在新应用或文档被打开之前通过Shell接口唤醒。换言之,这个钩子在运行新应用,或以下面方法执行一个文档的动词时被唤醒:
编程,由ShellExecute()或ShellExecuteEx()。
通过‘运行’对话框。
从探测器双击。
如果通过CreateProcess()或WinExec()运行其它的程序,这个钩子模块不能得到通知。同样,如果通过DOS运行程序或打开文档,或使用任何其它底层技术,也不能唤醒钩子。
利用作为变量传递的结构,Execute()方法接受动词,文件名,变量,目录,以及任何用户传递给ShellExecute()或其它关联函数的数据。
前面已经提到,ShellExecute()和ShellExecuteEx()二者都最终调用CreateProcess()。然而,它们除了取得和传递命令行到CreateProcess()外还要做更多其它的操作,而一开始,它们就处理策略和支持这个钩子。
从钩子返回
钩子返回S_FALSE,则Shell通常可以继续,和建立需要的进程。然而,如果不能继续要求的操作—即,钩子不想要Shell启动进程—则应该返回S_OK。这是因为钩子检查了某些条件,想要防止当前连接的用户运行程序或文档,另一种可能性是钩子代码想要自己运行这个文档。给出一个非标准的优先线程。这要求你自己处理CreateProcess()调用。更重要的是,如果返回S_OK到Shell,我们还需要适当地设置SHELLEXECUTEINFO结构中的hInstApp成员。
“适当设置hInstApp成员”,就是说给它赋一个值,向Shell表示我们的处理成功或失败(使用相关的错误消息)。如果我们自己运行这个应用,则这个值应是新进程的HINSTANCE。如果我们截断处理,则这个值可以是任何大于32的值,以防止Shell显示任何错误消息框。
下面是一个例子,假设我们确定阻塞任何新进程:
HRESULT Execute(LPSHELLEXECUTEINFO lpsei)
{
return S_OK
}
无论我们接收的参数如何,都立即返回S_OK。此时Shell发现hInstApp成员地值是0,解释返回值为错误码。然后适当地显示一个消息框,如下图所示:
写IShellExecuteHook处理器
写COM服务器,活动模版库(ATL)是重要的资源。运行ATL COM应用大师,生成一个Hook名的COM 服务器框架,选择‘简单对象’后,我们就可以在其中添加新类了:
新类名为CShowHook,应该从IShellExecuteHook导出。这个接口要求代码包含shlobj.h文件。由于比直接从IShellExecuteHook导出要好,我们定义一个普通的实现类(以习惯的ATL风格)命名为IShellExecuteHookImpl:
// IShellExecuteHookImpl.h
//
//////////////////////////////////////////////////////////////////////
#include <AtlCom.h>
#include <ShlObj.h>
class ATL_NO_VTABLE IShellExecuteHookImpl : public IShellExecuteHook
{
public:
// IUnknown
STDMETHOD(QueryInterface)(REFIID riid, void** ppvObject) = 0;
_ATL_DEBUG_ADDREF_RELEASE_IMPL(IShellExecuteHookImpl)
// IShellExecuteHook
STDMETHOD(Execute)(LPSHELLEXECUTEINFO lpsei)
{
return S_FALSE;
}
};
实际的CShowHook类声明如下:
#include "resource.h"
#include "comdef.h"
#include "IShellExecuteHookImpl.h"
///////////////////////////////////////////////////////////////////////////
// CShowHook
class ATL_NO_VTABLE CShowHook :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CShowHook, &CLSID_ShowHook>,
public IShellExecuteHookImpl,
public IDispatchImpl<IShowHook, &IID_IShowHook, &LIBID_SHOWLib>
{
public:
CShowHook()
{
}
STDMETHOD(Execute)(LPSHELLEXECUTEINFO lpsei);
DECLARE_REGISTRY_RESOURCEID(IDR_SHOWHOOK)
DECLARE_PROTECT_FINAL_CONSTRUCT()
BEGIN_COM_MAP(CShowHook)
COM_INTERFACE_ENTRY(IShowHook)
COM_INTERFACE_ENTRY(IDispatch)
COM_INTERFACE_ENTRY(IShellExecuteHook)
END_COM_MAP()
// IShowHook
public:
};
现在还缺少这个钩子的实现。我们开始时提出了三个钩子的作用:跟踪,授权和命名。现在让我们看一看这三方面必要的代码:
HRESULT CShowHook::Execute(LPSHELLEXECUTEINFO lpsei)
{
// 跟踪程序/文件的打开操作
TCHAR szTime[50] = {0};
GetTimeFormat(LOCALE_SYSTEM_DEFAULT, 0, NULL, NULL, szTime, 50);
TCHAR szText[1024] = {0};
wsprintf(szText, __TEXT("%s: %s at %s"),lpsei->lpVerb, lpsei->lpFile, szTime);
FILE *f;
f = fopen(__TEXT("c://ShowHook.txt"), __TEXT("a+t"));
fseek(f, 0, SEEK_END);
fprintf(f, __TEXT("%s: %s at %s/n/r"),lpsei->lpVerb, lpsei->lpFile, szTime);
fclose(f);
// 检查快捷方式列表和运行程序
TCHAR szFileName[MAX_PATH] = {0};
GetPrivateProfileString(__TEXT("GoldList"), lpsei->lpFile,
"", szFileName, MAX_PATH, __TEXT("c://showhook.ini"));
if(lstrlen(szFileName))
{
lpsei->hInstApp = reinterpret_cast<HINSTANCE>(WinExec(szFileName,
SW_SHOW));
return S_OK;
}
// 如果名字包含DEBUG,则禁止做任何操作
strlwr(const_cast<LPTSTR>(lpsei->lpFile));
if(strstr(lpsei->lpFile, __TEXT("debug")))
{
lpsei->hInstApp = reinterpret_cast<HINSTANCE>(42);
return S_OK;
}
// 让Shell继续
return S_FALSE;
}
编辑注册表脚本
在分析上面代码之前,需要多做一点事情,以使服务器成为完全自注册服务器。这涉及到对大师给出的注册表脚本代码的补充。加入使Shell执行钩子的特殊信息,把这些信息放在文件的尾部:
HKLM
{
SOFTWARE
{
Microsoft
{
Windows
{
CurrentVersion
{
Explorer
{
ShellExecuteHooks
{
val {4F43D133-2951-11D2-BC00-7CA506C10000} = s ''
}
}
}
}
}
}
}
这个钩子包含在ShowHook.dll中,服务器的注册是自动的。这里我们已经做的是正确地在ShellExecuteHooks键下注册钩子的CLSID。
钩子怎样工作
跟踪就是把使用的动词,活动的文件名,和调用时间保存到磁盘。下面的图中显示了跟踪的结果。注意,在记录中还包含了一个重启动活动的跟踪(SysTray.exe的实例)。
上面的代码还涉及到命名—识别键名列表,然后把它们转换成应用。这个列表保存在根目录下的一个.ini文件中,其典型内容为:
[GoldList]
reg=regedit.exe
tt=notepad.exe
AddNewHardware=control.exe sysdm.cpl,Add New Hardware
左边的字是由钩子识别的并把它转换成右边的命令行。这就允许我们键入AddNewHardware到‘运行’框中。
最后是Execute()函数的授权部分,防止名字中包含‘debug’串的任何文件夹或文件被打开。注意,如果忘记了返回值要大于32,则可能有一个讨厌的错误消息框出现。这正好说明了ShellExecute()实际上是经常由探测器调用的,所以应该在钩子代码中仔细地考量尺寸和效率。
小结
在这一章中我们讨论了大量的基础知识,并且指出了所用方法的一些缺陷。我们从讨论WinExec()函数开始,随后是CreateProcess()函数,最后讨论了ShellExecute()函数。在讨论这些函数的特征和bugs后,我们论及了FindExecutable()函数,它也有几个瑕疵。
总体上,ShellExecuteEx()似乎组合了CreateProcess()和ShellExecute()函数的功能。支持PIDLs和策略,以及钩子能力使ShellExecuteEx()成为‘最好的Windows程序执行器’。这与我们的观点相同,我们解释了:
ShellExecute()和FindExecutable()的特征与Bugs。
为什么Windows98资料推荐使用ShellExecute()/ShellExecuteEx()而不
是CreateProcess(),
ShellExecuteEx()在那些方面扩展了ShellExecute()的功能。
怎样使用钩子扩展ShellExecuteEx()。