admin管理员组文章数量:1573370
转自:http://yadang418.blog.163/blog/static/2684365620096534257530/
第七章侵入Shell
与所有其它Win32进程一样,Shell也有其自己的内存地址空间,这是其它应用完全不可知的地址空间。为了进入这个地址空间,我们必须传递一定数量的控制点,就象我们正在跨过国家边界一样。在Windows Shell这块陆地上什么是我们感兴趣的呢,它是一个伊甸园吗,它有丰富的金矿吗,它是天堂的宝库吗?不幸,它都不是。进入Shell,只是简单地允许我们编写代码执行在Shell外面不能执行的操作。通过注入代码到一个Win32进程的地址空间,我们能够控制这个程序的行为,能够过滤它的事件,查看消息流,以及强迫它做(或不做)一定的操作。
为了达到这个目的,我们可以采取几种不同的方法。有力的方法是使用某些Windows的特征(或弱点)进入进程的地址空间和子类化它的窗口。此外,有些程序明确地允许外部模块介入,且可以一同工作。此时我们要做的是写一个具有必要接口的模块(一般是一个COM进程内服务器),并且在主模块要求的地方注册它。
第三种方法是让每个进程都在自己的空间中运行,但是建立一个通道,使它们之间可以通讯。你可以想象一个程序合理地影响另一个程序行的情形—或者,一个程序能够做一些使另一个程序能够知道的操作。在这种情况下,有一个潜在的通道连接这些模块—允许探测器知道你可能对文件或文件夹作出的任何改变就使用了这种方式。
在这一章中,我们将给出实现上述三种模块的例子。另外还解释:
Shell怎样感知文件系统的变化
你的事件怎样才能通知到Shell
怎样进入到Shell的地址空间
作为上述结果,怎样改变‘开始’按钮的行为
我们重点使用Win32软件的两个部分:钩子和通知对象。在我们将要研讨的很多关键点上这些机理都是隐含的。
Shell通知事件
你一定已经注意到了,探测器能非常快地感知文件系统的任何变化,周期地刷新当前观察和反映其它应用引起的任何改变。例如,当你打开DOS窗口和探测器窗口时,在两者中选择相同的目录,然后在DOS窗口中建立一个目录,后者将没有任何迟滞地更新显示。
似乎有某件事情告诉探测器已经建立了一个新的文件夹。在这个外壳下,使所有这些成为可能的控件是通知对象。
通知对象
通知对象是同步线程的核心对象,其概念是你建立这样一个对象,并给它赋予某些用以配置事件的属性,然后在其上阻塞线程等待事件的发生。如果你愿意,你可以把通知对象当成专门的事件,在它感觉到文件系统改变时自动获得信号。通过通知对象,你可以控制目录,子树,甚至整个驱动器,以及监视文件和文件夹事件—建立,重命名,删除,属性更改等。
通知对象的用法
Win32 SDK定义了三个操作通知对象的函数,它们是:
FindFirstChangeNotification()
FindNextChangeNotification()
FindCloseChangeNotification()
第一个函数‘建立’新通知对象,最后一个函数删除这个对象。奇怪的是,你不必象对待其它核对象那样使用CloseHandle()来释放通知对象。
前面讲过,在通知对象背后是一个标准的Win32同步对象,但是它已经增加了监视文件系统的特殊行为。在FindFirstChangeNotification()和FindNextChangeNotification()函数的背后有捕捉这个核对象信号状态的秘密任务。在通过调用FindFirstChangeNotification()建立对象时,它是非信号状态的,当它感觉到一个满足滤波条件的活动时,状态改变信号发送给等待线程。为了继续查询事件,必须显式地重置初始状态,这就是FindNextChangeNotification()所要做的。
同步对象包括‘互斥体(mutexes)’,‘信号灯(semaphores)’,‘事件(events)’和‘临界节(critical sections)’等等,在VC++帮助文件中有完备描述。它们有不同的行为,但是基本上都作用于线程的同步过程。从高层观点上考虑,你可以认为它们是线程相遇的控制点。
同步对象有两种状态:信号状态和非信号状态。线程停止在非信号状态,在捕捉到信号状态后继续执行。
建立参数
FindFirstChangeNotification()声明如下:
HANDLE FindFirstChangeNotification(LPCTSTR lpPathName,
BOOL bWatchSubtree,
DWORD dwNotifyFilter);
lpPathName是包含要监视目录名的缓冲指针。bWatchSubtree布尔值指定是否路径中包含子树。dwNotifyFilter使你能设置通知的实际触发规则。通过在dwNotifyFilter上使用可能的组合标志,你能够决定监视哪种类型的文件系统事件。可用的标志是:
标志 | 描述 |
FILE_NOTIFY_CHANGE_FILE_NAME | 文件被建立,删除,移动 |
FILE_NOTIFY_CHANGE_DIR_NAME | 文件夹被建立,删除,移动 |
FILE_NOTIFY_CHANGE_ATTRIBUTES | 文件或文件夹的任何属性改变 |
FILE_NOTIFY_CHANGE_SIZE | 文件或文件夹的尺寸改变,仅当任何缓存写回到磁盘时才有这个感觉。 |
FILE_NOTIFY_CHANGE_LAST_WRITE | 文件或文件夹的最近写入时间改变,仅当任何缓存写回到磁盘时才有这个感觉。 |
FILE_NOTIFY_CHANGE_SECURITY | 文件或文件夹的任何安全描述符改变 |
显然在监视路径时这些事件必然发生。例如,如果你发起一个如下调用:
HANDLE hNotify = FindFirstChangeNotification(__TEXT("c:\\"), TRUE,
FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME |
FILE_NOTIFY_CHANGE_ATTRIBUTES | FILE_NOTIFY_CHANGE_SIZE);
在C驱动器上建立任何新文件,都将唤醒等待这个通知对象的线程。如果在第二个参数中指定FALSE,则仅仅C驱动器根目录下的变化被感觉。调用FindFirstChangeNotification()产生的返回对象是在非信号状态的,意思是,要求使用这个对象同步的线程将停止。
监视目录
现在我们已经知道了怎样建立一个变动通知对象,另一个问题是:这是否就能完全能监视目录活动。实际上不能,就象其它监视活动一样,目录监视需要耐心,因此,你还必须准备捕捉任何时间发生的事件。用软件术语讲,你需要在代码中设置某种循环。每当处理完一个事件后,你还要立即通知准备处理事件的下一次发生或准备处理同时发生的其它事件。FindNextChangeNotification()就是此时要使用的函数。
BOOL FindNextChangeNotification(HANDLE hChangeHandle);
下面是从示例应用中截取的一段代码,显示了函数的典型用法:
//注意线程外设置的逻辑保护.
//这是一段工作线程上摘下来的代码.
while(g_bContinue)
{
// 等待改变发生
WaitForSingleObject(hNotify, INFINITE);
// 改变已经发生,通知主窗口.
// 使之有机会来刷新程序的UI.
// WM_EX_XXX 是应用定义的客户消息.
PostMessage(ci.hWnd, WM_EX_CHANGENOTIFICATION, 0, 0);
// 准备下一次改变到达
FindNextChangeNotification(hNotify);
// NB:
// 在这一点上由hNotify封装的同步对象处于非信号状态,所以当这个线程再次执行
// WaitForSingleObject()时,它将停止,直到新的改变发生和变成信号状态
}
如上所见,在循环内部没有使循环终止的事件。g_bContinue逻辑变量是线程外设置的全程变量,也就是说,这段代码暗示有两个线程:主应用线程和涉及到通知对象的工作线程。
由于这段代码假定在调用FindFirstChangeNotification()后执行,因此在执行了一段后将停止在WaitForSingleObject()的调用上,因为此时的通知对象已经变成非信号状态了。当满足hNotify通知对象条件的事件发生时,对象的状态改变成信号状态,执行继续,并抛出一个客户消息到指定窗口,给它一个刷新用户界面或做进一步处理的机会,然后再一次停止,等待新的事件发生。调用FindNextChangeNotification()之后包含在hNotify中的同步对象的状态又变回到非信号状态。
在处理通知对象时,明智的选择是用不同的工作线程隔离所有等待事件的代码。这样能够避免主程序不确定的阻塞。如果你不想要多线程应用,则应该使用MsgWaitForMultipleObjects()代替WaitForSingleObject()来等待消息或事件。同时设置多个通知对象也是可能的。例如,你可能需要对相同或不同驱动器上的不同目录进行分别监视,如果需要这样做,WaitForMultipleObjects()可以帮助你一起同步化所有通知对象。
停止监视
释放通知对象必须调用FindCloseChangeNotification(),传递的唯一变量是由FindFirstChangeNotification()建立的Handle:
BOOL FindCloseChangeNotification(HANDLE hChangeHandle);
总体示例
让我们看一个示例应用,这个程序概念性的展示探测器在屏幕后面的工作。这个程序让你选择路径和建立监视整个子树的通知对象,所有变动通知的处理都在不同的线程中完成。每一次事件的感觉都有消息抛给应用主窗口。作为示范,我们仅简单地增加一个包含当前时间的行到报告列表观察中。而在实际工作中你可能需要做更多的处理。工作线程接受窗口Handle和监视路径,窗口Handle用于发送消息到应用主窗口,使用用户定义的结构传递数据。程序的用户界面显示如图:
在你单击按钮时,通知对象使用上面调用的属性安装:
FILE_NOTIFY_CHANGE_FILE_NAME,
FILE_NOTIFY_CHANGE_DIR_NAME,
FILE_NOTIFY_CHANGE_ATTRIBUTES,
FILE_NOTIFY_CHANGE_SIZE
下面是需要加入框架的代码:
//数据
HICON g_hIconLarge;
HICON g_hIconSmall;
bool g_bContinue; //在WinMain()中应该设置为FALSE
const int WM_EX_CHANGENOTIFICATION = WM_APP + 1;
//传递给线程的客户数据
struct CUSTOMINFO
{
HWND hWnd;
TCHAR pszDir[MAX_PATH];
};
typedef CUSTOMINFO* LPCUSTOMINFO;
在上面的代码中我们显式地声明了WM_EX_CHANGENOTIFICATION消息常量。一般在定义常量作为Windows消息时,应该使用RegisterWindowMessage()函数,以确保系统唯一的消息号。然而在相关的单个应用中,如果没有广播消息,使用基于WM_APP的显式声明常量是安全的。WM_APP是一个基本常量,它以后的消息常量不能与Windows系统消息冲突。唯一的冒险是可能与来自其它应用的客户消息冲突,这一点在这个例子中是不能发生的。
有一个新处理器要加到APP_DlgProc()中,它在通知对象感觉到变化时被唤醒,你好需要在IDCANCEL处理器上做一点小的改变,用以在程序关闭时终止线程。
BOOL CALLBACK APP_DlgProc(HWND hDlg, UINT uiMsg, WPARAM wParam, LPARAM lParam)
{
switch(uiMsg)
{
case WM_INITDIALOG:
On
break;
case WM_EX_CHANGENOTIFICATION:
UpdateView(hDlg);
break;
case WM_COMMAND:
switch(wParam)
{
case IDOK:
On
return FALSE;
case IDCANCEL:
g_bContinue = false;
EndDialog(hDlg, FALSE);
return FALSE;
}
break;
}
return FALSE;
}
再有,这个处理器对于‘安装通知对象’按钮,仍然调用On
void On
{
TCHAR szDir[MAX_PATH] = {0};
GetDlgItemText(hDlg, IDC_EDIT, szDir, MAX_PATH);
SHInstallNotifier(hDlg, szDir);
}
On
HANDLE SHInstallNotifier(HWND hwndParent, LPCTSTR pszDir)
{
DWORD dwID = 0;
CUSTOMINFO ci;
ZeroMemory(&ci, sizeof(CUSTOMINFO));
ci.hWnd = hwndParent;
lstrcpy(ci.pszDir, pszDir);
// 建立工作线程
g_bContinue = true;
HANDLE hThread = CreateThread(NULL, 0, Notify, &ci, 0, &dwID);
return hThread;
}
Notify()本身存在产生调用FindXXXChangeNotification()函数的地方,并在循环中保持对指定目录树的监视:
DWORD WINAPI Notify(LPVOID lpv)
{
CUSTOMINFO ci;
ci.hWnd = static_cast<LPCUSTOMINFO>(lpv)->hWnd;
lstrcpy(ci.pszDir, static_cast<LPCUSTOMINFO>(lpv)->pszDir);
HANDLE hNotify = FindFirstChangeNotification(ci.pszDir, TRUE,
FILE_NOTIFY_CHANGE_FILE_NAME |
FILE_NOTIFY_CHANGE_DIR_NAME |
FILE_NOTIFY_CHANGE_ATTRIBUTES |
FILE_NOTIFY_CHANGE_SIZE);
if(hNotify == INVALID_HANDLE_VALUE)
{
SPB_SystemMessage(GetLastError());
return 0;
}
while(g_bContinue)
{
WaitForSingleObject(hNotify, INFINITE);
PostMessage(ci.hWnd, WM_EX_CHANGENOTIFICATION, 0, 0);
FindNextChangeNotification(hNotify);
}
FindCloseChangeNotification(hNotify);
return 1;
}
当事件变为信号事件时,WM_EX_CHANGENOTIFICATION类型的消息被发送,引起UpdateView()函数调用:
void UpdateView(HWND hDlg)
{
TCHAR szTime[100] = {0};
HWND hwndList = GetDlgItem(hDlg,IDC_LIST);
GetTimeFormat(LOCALE_SYSTEM_DEFAULT, 0, NULL, NULL, szTime, 100);
AddStringToReportView(hwndList, szTime, 1);
}
你可以看到这段代码使用了AddStringToReportView()函数,这是我们在上一章中开发的发送串到报告观察的函数。其伴随函数是MakeReportView(),在On
void On
{
// 设置图标T/F大/小图标)
SendMessage(hDlg, WM_SETICON, FALSE, reinterpret_cast<LPARAM>(g_hIconSmall));
SendMessage(hDlg, WM_SETICON, TRUE, reinterpret_cast<LPARAM>(g_hIconLarge));
LPTSTR psz[] = {__TEXT("Date and Time"), reinterpret_cast<LPTSTR>(400)};
MakeReportView(GetDlgItem(hDlg, IDC_LIST), psz, 1);
}
要加#include resource.h到源文件的顶部,并编译连接这个应用。运行这个应用之后,你会注意到,如果拷贝文件,你能够获得两个通知,删除可以有三个通知,如果删除所有标志,仅保留FILE_NOTIFY_CHANGE_FILE_NAME,并且重复拷贝操作,通知数被减少到1,因为我们不再对属性和尺寸的变化感兴趣。尽管如此,在删除操作时仍然有两个通知发生。为了查看为什么这样,按住Shift键后试着删除文件—你将发现现在只有一个通知了。这种情况说明,此次删除文件是直接销毁文件而不是保存到‘回收站’中。因此消除了正常文件删除的两个步骤中的一个—拷贝到‘回收站’,然后删除文件。
简单地删除文件在文件被实际删除时产生一个通知。
探测器和通知对象
概略地讲,探测器的行为与这个应用一样:它设置通知对象到当前显示的文件夹上,每次接收到某个事件变动的通知,它都重新装入这个文件夹以响应那些变化。稍微思考一下,你就会认识到通知对象的机理就是为探测器的需要而精确定制的。
探测器不是文件系统的监视例程,它需要知道当前被观察的文件夹中某些东西是否被改变了,以及改变所影响的显示数据:文件和子文件夹名,属性,尺寸,日期,安全等。无论确切的操作如何,重要的是已经发生了某些事情。这个机理似乎在系统与探测器性能方面是一个好的折中。
揭示文件系统的监视例程
就象我们已经看到的,通知对象的最大缺陷是对于实际发生事件所能提供的信息十分贫乏,通知对象就像一个防盗和火灾报警铃:在铃声响时,你不知道是被盗了还是着火了,或者二者都发生了。这个限制使它很难(不是不可能)应用于建立文件系统监视实用程序来使我们知道在整个系统中程序正在处理哪些文件。
以后我们将考虑使用ICopyHook Shell扩展方法解决这个问题。即使这是一个重大进步,但是与我们的最终目标仍然有一定的距离。
关于Windows NT的说明
到目前为止,我们并没有讨论不同的操作系统。你可能认为在Windows95,Windows98和Windows NT之间没有什么重大的差别,但事实上,我们所希望的事情出现在Windows NT4.0以上版本中。Windows NT的Win32 SDK输出和说明了ReadDirectoryChangesW()函数,它有一个类似于FindFirstChangeNotification()的原型,但是有一个很大的差别:它使用活动发生的特殊信息和所涉及的活动者信息填充一个缓冲。
关于ReadDirectoryChangesW()函数和通知对象的更多信息,一般来讲可以在Advanced Windows资料中找到。
SHChangeNotify()函数
当系统变动的事情发生时,探测器本身能够感知到它们(特别是文件的变化),但是还不需要显式地告知程序执行的什么变化。为了使这容易些,Shell API定义了SHChangeNotify()函数,他唯一的目的就是通知探测器,某些系统设置已经被修改。概念上,SHChangeNotify()与通知对象产生相同的效果,但是,它遵从不同的逻辑。因此,一个外部应用可以用来向探测器通报某些它所制造的变化。在响应这个通知中,探测器将刷新用户界面。这是一个明显的例子,它说明了我们前面提到的在应用与Shell之间的‘通道’。
调用SHChangeNotify()函数
这个函数定义在shlobj.h中,下面是它的原型:
void WINAPI SHChangeNotify(LONG wEventId,
UINT uFlags,
LPCVOID dwItem1,
LPCVOID dwItem2);
wEventId参数指定通知系统的事件,它接收一个或多个可能值的集合。最常用的值列表如下:
事件 | 描述 |
SHCNE_ASSOCCHANGED | 一个文件关联的类型已经改变,没有指定具体是哪一个。 |
SHCNE_NETSHARE | 一个本地文件夹正在被共享,这引起图标的变化。dwItem1应包含文件夹名,文件夹名可以是全路径名或PIDL(见下面)。 |
SHCNE_NETUNSHARE | 一个本地文件夹不再被共享。这引起图标改变。dwItem1中包含文件夹名(全路径名或PIDL)。 |
SHCNE_SERVERDISCONNECT | 这台PC已经与服务器断开。dwItem1中包含服务器名。 |
SHCNE_UPDATEDIR | 给出文件夹的内容已经改变,但是这个变化并不影响文件系统。dwItem1中包含文件夹名(全路径名或PIDL)。 |
SHCNE_UPDATEIMAGE | 系统图像列表中的图标已经改变。dwItem1包含图标索引。这引起探测器刷新用户界面,必要时绘制新图标。探测器使用的所有图标都存储在称为‘系统图像列表’的全程结构中或‘探测器内部图标缓存’中。在第四章中已经显示了怎样获得这个图像列表的Handle。 |
SHCNE_UPDATEITEM | 一个非文件夹项已经改变。dwItem1中包含全文件名或PIDL。 |
这个事件列表不是完整的,我们将在后面给出剩余的标志。完整的标志列表可以参考MSDN库。
SHChangeNotify()的另外三个参数受wEventId变量指定的事件标识符影响,dwItem1和dwItem2变量包含事件相关的值。uFlags参数用于表示dwItem1和dwItem2的类型。它可以表示DWORD数(SHCNF_DWORD),PIDL(SHCNF_IDLIST),串(SHNCF_PATH)或指针名(SHCNF_PRINTER)。此外,uFlags还能指出函数是否应该等待通知被处理完。SHCNF_FLUSH常量表示等待;SHCNF_FLUSHNOWAIT则表示不等待,使函数立即返回。
SHChangeNotify()函数的作用
函数SHChangeNotify()是作为通知对象的补充功能而提出的,换句话说,它确实是绝对需要的函数。这个函数努力提供与通知对象相同的功能(尽管使用不同的逻辑),但是,它并不仅仅限于文件系统对象。正象我们在第五章中看到过的,Windows Shell是由文件对象组成,并且绝大多数文件对象都映射到文件系统的物理实体上,但并不总是这样。比如文件对象‘我的计算机’和‘打印机’就没有对应的目录。更进一步,即使你有一个连接到目录的文件夹,它们所包含的项也不一定是文件。也就是说,你可以添加新项(或删除项)到文件夹对文件系统没有任何影响。此时探测器怎样感觉这些变化?
对这个问题有了深层次的了解之后,我们反而困惑了,是否能够设计出监视系统整个范围可能活动的软件程序呢?后面我们将看到,命名空间扩展通过文件夹风格的接口可以用于显示很多东西。例如,Internet客户端SDK有一个示例RegView,在探测器层次观察上加了一个新节点,就象一个普通文件夹一样,其特征是所包含的内容是系统注册表,实际上是一两个文件的内容。探测器或其它工具怎样感觉这里的变化?你可以写一段软件来钩住注册表的活动,但是,如果某人用另一个命名空间扩展替换了RegView,并且做完全不同的操作,怎么办?
只要操作超出了传统文件系统关联的范围,我们就需要改变通知的方式。它不再是探测器本身感觉变化,而是应用发送通知的事情。这就是SHChangeNotify()所设想的方式。某些用于调用SHChangeNotify()而定义的事件可能是多余的,例如,事件SHCNE_CREATE可能没有用—它表示建立一个新文件,但是探测器已经知道了这个事件,回想一下通知对象。反之,如果这个项不是文件系统对象,你就必须调用SHChangeNotify(),使探测器知道这个变化:
SHChangeNotify(SHCNE_CREATE, SHCNF_IDLIST, pidl, NULL);
SHChangeNotify()的其他事件
现在SHChangeNotify()函数的基本概念已经有点清楚了,但是还需要时间来进一步补充说明。下面是全部可以通过wEventId变量传递给函数的事件:
事件 | 描述 |
SHCNE_ATTRIBUTES | 文件或文件夹的属性改变。dwItem1是文件或文件夹名(全路径名或PIDL)。 |
SHCNE_CREATE | 已经建立了一个文件对象。dwItem1是文件对象名。 |
SHCNE_DELETE | 已经删除了一个文件对象。dwItem1是文件对象名。 |
SHCNE_DRIVEADD | 添加了一个驱动器。dwItem1是驱动器的根,有形式:C:\。 |
SHCNE_DRIVEADDGUI | 添加了一个驱动器并且需要一个新窗口。dwItem1是驱动器的根,有形式:C:\。 |
SHCNE_DRIVEREMOVED | 删除了一个驱动器。dwItem1是驱动器的根。 |
SHCNE_FREESPACE | 驱动器上可用空间量变化。dwItem1是驱动器的根,有形式:C:\。 |
SHCNE_MEDIAINSERTED | 存储介质已经插入到驱动器中。dwItem1是驱动器的根,有形式:C:\。 |
SHCNE_MEDIAREMOVED | 存储介质已经从驱动器中删除。dwItem1是驱动器的根,有形式:C:\。 |
SHCNE_MKDIR | 已经建立了一个文件夹。dwItem1是文件对象名。 |
SHCNE_RENAMEFOLDER | 文件夹已经重命名。dwItem1是老文件夹名,dwItem2是新文件夹名。名字可以是全路径名或PIDLs。 |
SHCNE_RENAMEITEM | 重命名了一个文件对象。dwItem1是老文件对象名,dwItem2是新文件对象名。 |
SHCNE_RMDIR | 删除了一个文件对象。dwItem1是文件对象名。 |
使用SHChangeNotify()
在开始写命名空间扩展程序时,SHChangeNotify()函数是非常有用的,因为它使你向探测器隐藏了一个项或文件夹可能不是实际文件系统对象这样一个事实。在第十六章中我们将开发一个命名空间扩展程序,它以窗口本身作为文件夹的内容显示系统中当前存在窗口的信息。通过扩展与全程钩子的组合,程序能够感知任何新窗口的建立,并使用SHCNE_CREATE标志调用SHChangeNotify(),并将使探测器能够有规律地刷新这个客户文件夹的内容。
尽管在第二章中我们已经提到了,在这里我们还是不想论及Windows钩子。你可以参考MSDN库来得到更多信息。
相反,一般的应用很少需要开发SHChangeNotify()服务。动态改变文件关联类型的程序可能需要使用—即,它改变了程序用于处理特殊类文档的信息,这些信息存储在注册表的下面指定的位置:
HKEY_LOCAL_MACHINE
\Software
\Microsoft
\Windows
\CurrentVersion
\Extensions
为了通知探测器更新,你可以调用:
SHChangeNotify(SHCNE_ASSOCCHANGED, 0, NULL, NULL);
入侵Shell存储空间
如果你是一个有经验的Win32程序员,就会知道每一个进程都在它自己的地址空间中运行,并且只有在这个地址空间内,内存地址才有一个一致的值。例如,你不能子类化由另一个进程建立的窗口,因为新窗口过程的地址仅能指向你在另一个地址空间中可以看到它的地方。事实上,SetWindowLong()函数阻止了这种努力,如果你试图这样做,它就返回零。
但是使你的程序代码映射进另一个应用进程的地址空间确实是可能的,这需要几个步骤。微软之所以阻止这样做是因为它产生的潜在错误比使用其它更普通的编程技术要高,然而,访问另一个应用的地址空间是安全的,只要你知道你打算做什么,你必须做什么,以及上面的全部知识。突破进程边界没有阻碍和实质危险的事情。就象使用指针一样—如果不正常使用的话,能够引起Bug。
Shell是一个Win32进程,你可以使用与侵入Notepad应用一样的方法侵入它的内存空间。为什么我们需要侵入Shell呢,这与你进入任何其他Win32或Win16进程的理由是一样的:需要改变(或过滤)一个程序的行为。你是否已经注意到Notepad的拷贝有一种维持某些交叉会话设置的能力。运行Notepad,并打开‘词重叠’模式,这个设置会永久保持,在每次打开时都会恢复。如果你要在Windows95或Windows98下实现这个功能,你就必须客户化Notepad的标准行为。换句话说,你需要使你的代码侵入到它的地址空间中。
在这一章的剩余部分,我们将显示三种进入探测器的地址空间的方法。头一个是传统的SDK技术,如钩子和子类化。第二种探索不为人知的Shell API函数SHLoadInProc()。这两项技术都能在Win32平台上工作,除了WindowsCE。第三种选择仅在4.71以上版才可用,探索探测器与IE共有的特征:浏览辅助对象。
强制进入方式
在认识到不通过菜单就没有办法建立文件夹的时候,我们考虑子类化探测器窗口。我不相信我是唯一的没有找到梦幻组合键的人,我努力添加一个快速建立新文件夹的键盘加速器。即使在包含Windows键盘快捷键列表的知识库文章中也没有提到建立新文件夹的组合键。我不知道别人怎样,我却发现所有这些工作都相当失败:右击(或单击‘文件’菜单),然后选择两项,最后再次单击。
我的目的就是建立一个小应用程序,把它放进‘启动’文件夹,它安装一个系统范围的钩子,用以保持属于一定类的窗口建立轨迹,在这个问题中,这个窗口类是探测器窗口‘ExploreWClass’。
我们使用了Spy++探测存在的窗口栈才找到这个类名字。
一旦获得了探测器窗口的Handle,我们就可以安装键盘钩子到建立这个窗口的特定线程。第二个钩子响应键盘活动,和在键盘组合满足指定的规则时建立文件夹。这个任务可以分解成两个部分:
进入探测器
用与探测器相同的方法建立文件夹
在Win32中,没有太多使你的代码映射进另一个进程地址空间的方法。如果想使你的代码兼容于Windows9x和WindowsNT,则只有一种可能:建立系统范围内的钩子。
为什么使用钩子
即使最终目标不是钩子而是简单地子类化窗口,如果这个窗口属于另一个进程的话,在做这个子类化工作之前,你也必须安装一个钩子。不管你使用什么钩子,问题在于它施加到系统所有线程上的是什么。
如第二章中介绍的概念,使用钩子意思是指定了一个回调函数,当一定的(相关于这个钩子的)事件发生时系统将唤醒这个函数。如果想要监视所有运行中的进程,函数就必须驻留在一个DLL中,因为系统需要将它映射进那些进程中。
进入到探测器内部
我们的程序将寻找正在建立的窗口(特别,寻找探测器窗口)。类型为WH_CBT的钩子过程需要在程序启动时安装以便系统在窗口上执行任何活动(建立,删除,激活等)时触发这个函数:
g_hShellHook = SetWindowsHookEx(WH_CBT, ShellDll_MainHook, g_hThisDll, 0);
这个钩子在退出时必须被删除:
if(g_hShellHook != NULL)
UnhookWindowsHookEx(g_hShellHook);
显然,在整个系统范围内有一个钩子存在会影响到它的性能。任何系统范围内的钩子因为它们的存在都将影响到系统性能。它使系统做附加的工作,这毋庸置疑地等比例缩减系统性能。因此我们建议,系统的钩子要尽可能地小。我这里是最小的一个,它极大地缩减了性能损失的风险。这个钩子的过程如下:
LRESULT CALLBACK ShellDll_MainHook(int nCode, WPARAM wParam, LPARAM lParam)
{
TCHAR szClass[MAX_PATH] = {0};
// 任何钩子过程都有的典型开头
if(nCode < 0)
return CallNextHookEx(g_hShellHook, nCode, wParam, lParam);
// 系统正在建立窗口。注意钩子从这段代码内被唤醒CreateWindow() and CreateWindowEx()。
// 在这一点上,窗口已经存在,HWND是有效的,即使我们仍然在建立过程中间。
if(nCode == HCBT_CREATEWND)
{
// 获得窗口的HWND
HWND hwndExplorer = reinterpret_cast<HWND>(wParam);
//比较'ExploreWClass'和安装键盘钩子
GetClassName(hwndExplorer, szClass, MAX_PATH);
if(!lstrcmpi(szClass, __TEXT("ExploreWClass")))
InstallKeyboardHook(hwndExplorer);
}
return CallNextHookEx(g_hShellHook, nCode, wParam, lParam);
}
每当有窗口建立时都执行这段代码。如果窗口类名与探测器窗口类名匹配(为ExploreWClass),则安装键盘钩子,在这一点上,我们就已经进入到探测器的地址空间了。注意,‘键盘’钩子可以是局部于探测器线程的,它拥有窗口类ExploreWClass,不必在整个系统上钩住键盘活动,因为当我们在建立新文件夹时,输入焦点自然在探测器上(在编写辅助对象那一节我们将进一步说明)。
怎样建立新文件夹
为了使钩子代码映射到一个进程的地址空间,充分的条件是从进程内部唤醒一个系统范围的钩子过程。现在这个问题缩减到要建立一个新文件夹。显然我们希望获得与手动建立习惯相同的操作方式。所以最容易的方法是精确地重复探测器使用‘新建|文件夹’菜单时的操作。
你可能要问为什么不选择采用前面讨论过的方法—也就是说,为什么不使用Shell API,取得当前目录和建立新目录。原因就是在这种情况下那些方法已经失效了。首先,你怎么知道探测器当前显示的是哪一个文件夹?GetCurrentDirectory()返回的名字是不完备的。其次,很多特殊文件夹不允许建立子文件夹,如果这样做将引起麻烦。
我论述了探测器在响应发送到主窗口的WM_COMMAND消息时建立新文件夹的原理。为了便于研究,我写了一段程序,子类化了ExploreWClass窗口,以便在每次处理WM_COMMAND消息时探测它的参数。通过这个方法,我们发现,要请求探测器建立新文件夹,你只需要向这个窗口发送如下消息即可:
PostMessage(hwndExplorer, WM_COMMAND, 29281, 0);
魔力数29281是‘新建|文件夹’菜单项的ID。这是非官方信息,而且它可能在新版本的Shell中被改动。但是,现在,它能与Windows9x和WindowsNT一起工作。如果将来这个数改变,除非Shell的本质结构变化,你只需要简单地找出新的ID号就可以了。这个数从4.00到4.71一直都没有变。
安装了键盘钩子后,Shell可以响应一键建立新文件夹操作。我们选择了F12键—没有什么特殊的原因,可以自由地采用任何其它的键。当键盘钩子过程感觉到F12按下时,它简单地恢复探测器窗口,和发送一个消息。
示例程序
正象说明的那样,示例程序必然分为两个部分:DLL和可执行程序。首先是DLL程序源码,包含了两个钩子,它是基于DLL框架的,我们取名为ExpHook工程(project)。全程变量和函数声明加到ExpHook.h文件中:
/*---------------------------------------------------------------*/
//原型节
/*---------------------------------------------------------------*/
HHOOK g_hShellHook;
HHOOK g_hKeybHook;
HWND g_hwndExplorer;
void InstallKeyboardHook(HWND hwnd);
void APIENTRY ShellDll_Hook();
void APIENTRY ShellDll_Unhook();
LRESULT CALLBACK ShellDll_KeybHook(int nCode, WPARAM wParam, LPARAM lParam);
LRESULT CALLBACK ShellDll_MainHook(int nCode, WPARAM wParam, LPARAM lParam);
自然,原型的实现在ExpHook.cpp,这些函数正好实现了我们讨论过的原理:
//设置钩子来感觉探测器的启动
void APIENTRY ShellDll_Hook()
{
g_hShellHook = SetWindowsHookEx(WH_CBT, ShellDll_MainHook, g_hThisDll, 0);
}
void APIENTRY ShellDll_Unhook()
{
if(g_hKeybHook != NULL)
UnhookWindowsHookEx(g_hKeybHook);
if(g_hShellHook != NULL)
UnhookWindowsHookEx(g_hShellHook);
}
//列表中的ShellDll_MainHook()钩子插入这段代码
LRESULT CALLBACK ShellDll_KeybHook(int nCode, WPARAM wParam, LPARAM lParam)
{
// 任何钩子过程典型的开头
if(nCode < 0)
return CallNextHookEx(g_hKeybHook, nCode, wParam, lParam);
// 一般这段代码在键盘按下和松开时都执行.状态变换信息存储在lParam的最高两位中
//因此,我们仅处理一次键盘操作。
if((lParam & 0x80000000) || (lParam & 0x40000000))
return CallNextHookEx(g_hKeybHook, nCode, wParam, lParam);
if(wParam == VK_F12)
{
//取得探测器窗口Handle和发送消息。
g_hwndExplorer = FindWindow("ExploreWClass", NULL);
PostMessage(g_hwndExplorer, WM_COMMAND, 29281, 0);
}
return CallNextHookEx(g_hKeybHook, nCode, wParam, lParam);
}
//安装键盘钩子
void InstallKeyboardHook(HWND hwnd)
{
g_hwndExplorer = hwnd;
DWORD dwThread = GetWindowThreadProcessId(g_hwndExplorer, NULL);
g_hKeybHook = SetWindowsHookEx(WH_KEYBOARD, ShellDll_KeybHook,
g_hThisDll, dwThread);
}
为了使这个库输出我们需要的函数,还应该把这些行加到.def文件:
EXPORTS
ShellDll_Hook @2
ShellDll_Unhook @3
ShellDll_KeybHook @4
ShellDll_MainHook @5
这就是我们需要的DLL,编译连接之后移到主程序一起,运行后将在托盘通知区域增加一个图标,便于你容易地卸载这个钩子。除了建立图标,主程序本身还包含安装和卸载WH_CBT钩子功能。由于这个应用程序的特性,不象一般的应用程序那样有多少客户需求。首先建立一个基于对话框的应用ExpFold,加一个#include语句,包含DLL函数定义:
/*---------------------------------------------------------------*/
//包含节
/*---------------------------------------------------------------*/
#include "ExpFold.h"
#include "ExpHook.h"
其次需要两个新常量:一是客户消息,当托盘图标被点击时发送的消息,再有就是图标的ID:
// Data
const int WM_MYMESSAGE = WM_APP + 1; //托盘图标消息
const int ICON_ID = 13;
HICON g_hIconLarge;
HICON g_hIconSmall;
HINSTANCE g_hInstance;
新的全程变量用于存储应用实例的Handle,这在后来调用LoadMenu()时是必须的。下面是对WinMain()需要作出的改变:
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevious,LPTSTR lpsz, int iCmd)
{
// 保存全程数据
g_hInstance = hInstance;
g_hIconSmall = static_cast<HICON>(LoadImage(hInstance, "APP_ICON",
IMAGE_ICON, GetSystemMetrics(SM_CXSMICON),
GetSystemMetrics(SM_CXSMICON), 0));
// 建立不可视对话框获得来自图表的消息
HWND hDlg = CreateDialog(hInstance, "DLG_MAIN", NULL, APP_DlgProc);
// 在托盘区域显示图标
TrayIcon(hDlg, NIM_ADD);
// 安装探测器钩子
ShellDll_Hook();
MSG msg;
while(GetMessage(&msg, NULL, 0, 0))
{
if(!IsDialogMessage(hDlg, &msg))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
// 卸载钩子
ShellDll_Unhook();
// 删除图标
TrayIcon(hDlg, NIM_DELETE);
DestroyWindow(hDlg);
DestroyIcon(g_hIconSmall);
return 1;
}
与显示的对话框不一样,这个应用通过调用CreateDialog()而不是DialogBox()建立了一个不可视对话框,对话框过程如下:
BOOL CALLBACK APP_DlgProc(HWND hDlg, UINT uiMsg, WPARAM wParam, LPARAM lParam)
{
switch(uiMsg)
{
case WM_COMMAND:
switch(wParam)
{
case IDCANCEL:
PostQuitMessage(0);
return FALSE;
}
break;
case WM_MYMESSAGE:
if(wParam == ICON_ID)
{
switch(lParam)
{
case WM_RBUTTONUP:
ContextMenu(hDlg);
break;
}
}
break;
}
return FALSE;
}
TrayIcon()函数由WinMain()在对话框设置后调用,它显示一个图标到任务条托盘,退出时要删除它:
//在托盘区域显示图标
BOOL TrayIcon(HWND hWnd, DWORD msg)
{
NOTIFYICONDATA nid;
ZeroMemory(&nid, sizeof(NOTIFYICONDATA));
nid.cbSize = sizeof(NOTIFYICONDATA);
nid.hWnd = hWnd;
nid.uID = ICON_ID;
nid.uFlags = NIF_TIP | NIF_ICON | NIF_MESSAGE;
nid.uCallbackMessage = WM_MYMESSAGE;
nid.hIcon = g_hIconSmall;
lstrcpyn(nid.szTip, __TEXT("Explorer's Hook"), 64);
return Shell_NotifyIcon(msg, &nid);
}
最后当用户点击托盘图标时ContextMenu()函数被调用。为了正常工作,需要加一个IDR_MENU类型的菜单资源到工程(project)中,菜单中应该包含一个项‘关闭’,它的ID是IDCANCEL。
//显示图标的关联菜单
void ContextMenu(HWND hwnd)
{
POINT pt;
GetCursorPos(&pt);
HMENU hmenu = LoadMenu(g_hInstance, MAKEINTRESOURCE(IDR_MENU));
HMENU hmnuPopup = GetSubMenu(hmenu, 0);
SetMenuDefaultItem(hmnuPopup, IDOK, FALSE);
SetForegroundWindow(hwnd);
TrackPopupMenu(hmnuPopup, TPM_LEFTALIGN, pt.x, pt.y, 0, hwnd, NULL);
SetForegroundWindow(hwnd);
DestroyMenu(hmnuPopup);
DestroyMenu(hmenu);
}
程序活动
包含#include "resource.h",并编译程序后,连接库exphook.lib,将获得.exe和.dll两个文件。而后建立可执行文件的快捷方式,把这个快捷方式拷贝到‘启动’文件夹下。
这个程序可以通过右击托盘图标并选择‘关闭’删除。只要它被安装,就能钩住每一个探测器窗口,并在响应线程中建立和安装键盘钩子,这个键盘过程查寻F12并向相应窗口发送消息。
进入Shell存储空间
把外部代码注入到Shell地址空间有两种方法。侵入(我们已经看到的),和邀请(一种友好的方法,仅当我们可以找到一种办法这么做)。前一种方式下,主程序完全不知道它正在运行什么。相反,后一种方法主程序直接控制每一件事情的发生。
Windows Shell正是通过邀请而不是侵入提供了一种方法来进入它的存储空间—Shell API提供了一个经常不受重视的函数SHLoadInProc(),它定义在shlobj.h中,并且有令人惊讶的能力。然而,这个函数的说明资料确是十分贫乏的,根据仅有的资料,你可能会怀疑这个函数是否有想象的能力。正是为了说明它的能力,在这一节我们打算建立一个DLL的例子,这个例子允许我们恢复和置换Windows‘开始’按钮。在开始这个任务之前,更进一的说明是必要的。
SHLoadInProc()函数
在坚固的外壳下,SHLoadInProc()函数将你的模块装入到Shell的地址空间。这实际上是你在上一节中努力要达到的目的。SHLoadInProc()装入模块,然后保留它独自作任何操作。下面是Internet客户端SDK中对它的描述资料:
WINSHELLAPI HRESULT WINAPI SHLoadInProc(REFCLSID rclsid);
在关联的Shell进程内建立一个指定对象的实例,如果成功,返回NOERROR,否则返回OLE定义的错误结果。
rclsid
要建立的对象类的CLSID。
这个资料是绝对正确的,问题是一点也没有提到‘对象类’的结构。哪些接口是必须要实现的,哪些特殊的规则是必须要遵循的,没有特定接口的COM服务器能做什么,如果不要求特定的接口,对象如何启动工作。
所有这些问题在资料中都没有回答,诚实地讲,我们不能理解怎样做才能使这个函数工作。
最小COM对象
SHLoadInProc()函数是把我们的代码引入Shell地址空间最快和最有效的方法,但是,这个代码必须是一个COM对象。然而,为了探索这个函数,我们没有必要建立一个完整的COM对象—只使用部分COM和DLL代码。但是它必须实现COM服务器的规则(因而需要自注册和一个CLSID),而实际上它更象一个老的DLL而不象进程内COM服务器。
怎样建立COM对象
一个进程内COM对象是一个DLL,即,它有一个DllMain()函数。更重要的是,一个COM对象输出四个全程函数,这些函数是由与进程内对象一同工作的任何容器来操作的。它们是:
DllGetClassObject()
DllCanUnloadNow()
DllRegisterServer()
DllUnregisterServer()
后两个是自动注册和注销函数,如果承诺手动注册和注销,可以不用实现这两个函数。我们的COM对象现在就减去这两个函数,此时只有两个全程函数输出:DllGetClassObject()和DllCanUnloadNow()。
DllGetClassObject()函数的作用
任何COM对象的客户必须首先加载包含这个COM对象的库,然后通过DllGetClassObject()函数取得接口指针:
STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID* ppv);
重点是在类对象被装入后,总是立即调用这个函数。换言之,这段代码总能得到执行。更重要的是,它是在Shell的关联空间中执行的。
满足客户的期望
一般,加载类对象的模块调用DllGetClassObject(),请求IClassFactory接口,我们的客户—当前情况下是探测器—期望一个由DllGetClassObject()函数返回的接口指针。由于我们并没有实现这个接口,怎样才能应对这个期望呢?
对我们而言,显式地说明请求的类不可用就足够了,这仅需简单地返回一个适当的错误码:
STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID* ppv)
{
return CLASS_E_CLASSNOTAVAILABLE;
}
上面是一种DllGetClassObject()函数可能的实现,它产生的一个感觉是不支持特定的接口。
使用Shell地址空间
除了返回错误码以外,这个函数还可以同任何驻留在Shell地址空间上的对象一道做任何想要做的工作。在DllGetClassObject()被调用时,我们就已经在Shell的关联空间中了,因此,这将允许我们子类化‘开始’按钮。在花一点时间讨论DllCanUnloadNow()函数之后,我们将很快开始证实这一点。
DllCanUnloadNow()函数的作用
通过DllGetClassObject()加载COM对象的客户模块调用DllCanUnloadNow()函数来保证DLL可以被安全地卸载和释放。探测器周期地执行这个检查,尽管这个周期可能延迟十秒或十分钟。在第十五章的Shell扩展中我们将展开讨论这一点。
如果DllCanUnloadNow()返回S_OK,则宿主DLL将被卸载。如果它总是返回S_FALSE,或DLL没有输出具有这个名字的函数,则只有在主应用程序调用CoUninitialize()函数关闭COM库时,这个DLL库才被释放。因为此时的主应用是探测器,所以调用CoUninitialize()函数之后的一段时间才发生DLL库被释放操作。
COM对象源码
接下来是这个‘假冒’COM对象的最小源代码,用来结合SHLoadInProc()函数,并且作为一个例程的种子,它将逐步成长为‘开始’按钮子类化的应用。在VC++中建立一个新的Win32动态链接库起名为‘Start’(选择‘简单DLL’选项),添加下述代码到startp:
#include "start.h"
HINSTANCE g_hInstance;
BOOL APIENTRY DllMain(HINSTANCE hInstance,
DWORD ul_reason_for_call,
LPVOID lpReserved)
{
g_hInstance = hModule;
return TRUE;
}
/*---------------------------------------------------------------------------*/
// DllGetClassObject
// COM进程内对象住函数
/*---------------------------------------------------------------------------*/
STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID* ppv)
{
// 在这了做一些操作
return CLASS_E_CLASSNOTAVAILABLE;
}
/*---------------------------------------------------------------------------*/
// DllCanUnloadNow
//确认卸载COM库
/*---------------------------------------------------------------------------*/
STDAPI DllCanUnloadNow()
{
return S_OK;
}
start.h头文件包含有上面定义的‘假冒’COM对象的CLSID:
#include <windows.h>
#include <windowsx.h>
#include <objbase.h>
#include <shlobj.h>
DEFINE_GUID(CLSID_NewStart, 0x20051998, 0x0020,0x0005,
0x19, 0x98, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00);
对于库的输出函数,还需要定义start.def文件:
LIBRARY START
EXPORTS
DllCanUnloadNow @1 PRIVATE
DllGetClassObject @2 PRIVATE
作为这一节的结束,这里是一段代码,它使用SHLoadInProc()函数加载COM到探测器地址空间:
void DoGoInsideExplorer()
{
const CLSID clsid = {0x20051998,0x0020,0x0005,
{0x19,0x98,0x00,0x00,0x00,0x00,0x00,0x00}};
SHLoadInProc(clsid);
}
注册COM对象
本质上有两种方法可以注册COM对象:在DllRegisterServer()函数中插入代码,或手动地注册—最好用一个注册脚本。现在让我们关注一下这两种方法。先从最简单的注册脚本开始。
下面是一个脚本文件REG的内容,它由注册表编辑器自动处理。它添加两个注册CLSID的键到HKEY_CLASSES_ROOT的CLSID节点下,并且存储实现COM的可执行文件名。
REGEDIT4
[HKEY_CLASSES_ROOT\CLSID\{20051998-0020-0005-1998-000000000000}]
@= "Start Button"
[HKEY_CLASSES_ROOT\CLSID\{20051998-0020-0005-1998-000000000000}\InProcServer32]
@= "C:\\Chap07\\Source\\Start\\start.dll"
"ThreadingModel" = "Apartment"
当然,一定要保证注册的路径是实际文件所在的路径。实际,一个需要加到CLSID下的键应该具有封装为如下用括号括起来的CLSID名字:
HKEY_CLASSES_ROOT
\CLSID
\{20051998-0020-0005-1998-000000000000}
此外,我们还需要在这个键下添加另一个称为InProcServer32的键,其默认值指向实际服务器的名。值ThreadingModel指示必须的线程模型。要注册这个服务器,需要在探测器上双击这个REG文件,或使用注册编辑器引入它。
正规的方法是把这些内容全部编写进DllRegisterServer()函数中,这要求我们使用Win32注册表API编程。我们在第十章中将说明在Shell4.71以上版中包含了涉及到注册表的新的高层函数集。在这里我们可以使用这些函数,但是,这段代码将只能在4.71或以上版使用。下面的代码使用传统的Win32注册表API:
STDAPI DllRegisterServer()
{
TCHAR szSubKey[MAX_PATH] = {0};
TCHAR szCLSID[MAX_PATH] = {0};
TCHAR szModule[MAX_PATH] = {0};
HKEY hKey;
DWORD dwDisp;
//设置CLSID
lstrcpy(szCLSID, __TEXT("{20051998-0020-0005-1998-000000000000}"));
// 取得模块名
GetModuleFileName(g_hInstance, szModule, MAX_PATH);
// HKCR: CLSID\{...}
wsprintf(szSubKey, __TEXT("CLSID\\%s"), szCLSID);
LRESULT lResult = RegCreateKeyEx(HKEY_CLASSES_ROOT, szSubKey, 0, NULL,
REG_OPTION_NON_VOLATILE, KEY_WRITE, NULL, &hKey, &dwDisp);
if(lResult == NOERROR)
{
TCHAR szData[MAX_PATH] = {0};
wsprintf(szData, __TEXT("Start Button"), szModule);
lResult = RegSetValueEx(hKey, NULL, 0, REG_SZ,
reinterpret_cast<LPBYTE>(szData), lstrlen(szData) + 1);
RegCloseKey(hKey);
}
// HKCR: CLSID\{...}\InProcServer32
wsprintf(szSubKey, __TEXT("CLSID\\%s\\InProcServer32"), szCLSID);
lResult = RegCreateKeyEx(HKEY_CLASSES_ROOT, szSubKey, 0, NULL,
REG_OPTION_NON_VOLATILE, KEY_WRITE, NULL, &hKey, &dwDisp);
if(lResult == NOERROR)
{
lResult = RegSetValueEx(hKey, NULL, 0, REG_SZ,
reinterpret_cast<LPBYTE>(szModule), lstrlen(szModule) + 1);
TCHAR szData[MAX_PATH] = {0};
lstrcpy(szData, __TEXT("Apartment"));
lResult = RegSetValueEx(hKey, __TEXT("ThreadingModel"), 0, REG_SZ,
reinterpret_cast<LPBYTE>(szData), lstrlen(szData) + 1);
RegCloseKey(hKey);
}
return S_OK;
}
COM对象在DEF文件中输出了DllRegisterServer(),可以使用系统实用程序regsvr32.exe来进行注册:
regsvr32.exe <full_server_name>
注销COM对象
REG脚本不允许注销设置,所以要这样做的唯一方法是通过注册表编辑器的帮助手动删除。如果安装了Windows脚本环境(WSH)则可以有另一种方案,写一个VB脚本或Java脚本函数,使用WSH注册表对象来删除键和值。由于使用脚本语言比REG更灵活和通用,因此这种方法在未来将可能成为流行的方法。
说到脚本语言,其价值在于用ATL写的COM对象可以用RGS文件提供注册和注销。RGS脚本并不是注册表编辑器REG文件的增强版。
返回到我们关于API函数的讨论,要使COM对象自己注销,你应该使用下面的编码:
STDAPI DllUnregisterServer()
{
TCHAR szSubKey[MAX_PATH] = {0};
TCHAR szCLSID[MAX_PATH] = {0};
TCHAR szModule[MAX_PATH] = {0};
HKEY hKey;
DWORD dwDisp;
// 设置CLSID
lstrcpy(szCLSID, __TEXT("{20051998-0020-0005-1998-000000000000}"));
// 打开HKCR
LRESULT lResult = RegCreateKeyEx(HKEY_CLASSES_ROOT, "", 0, NULL,
REG_OPTION_NON_VOLATILE, KEY_WRITE, NULL, &hKey, &dwDisp);
if(lResult == NOERROR)
{
wsprintf(szSubKey, __TEXT("CLSID\\%s\\InProcServer32"), szCLSID);
RegDeleteKey(hKey, szSubKey);
wsprintf(szSubKey, __TEXT("CLSID\\%s"), szCLSID);
RegDeleteKey(hKey, szSubKey);
RegCloseKey(hKey);
}
return S_OK;
}
在这个函数中,我们打开HKEY_CLASSES_ROOT,和删除其中的键。RegDeleteKey()在Windows9x和Windows NT下稍微有点差别。前者允许包含子键的键,这种递归删除在NT下不支持,如果给定键不空,这个函数失败。注意‘空’意思是没有子键,而不是他表示的值。由于上述代码首先删除最内部的键,所以在两个平台上都能工作。
输出DllUnregisterServer()的COM对象可以由regsvr32.exe系统实用程序加以注销:
regsvr32.exe /u <full_server_name>
一个崭新的开始按钮
为了说明SHLoadInProc()的能力,我们给出了扩展DllGetClassObject()函数的代码,建立了一个崭新的‘开始’按钮,它具有不同的图像和菜单。我们将通过以下步骤达到这个目标:
取得‘开始’按钮的Handle
置换它的图像
子类化按钮窗口,改变菜单和光标
建立和显示客户化菜单
而后,你就可以控制‘Windows’键和‘Ctrl+Esc’组合键。你也可以限制它们,让它们显示标准的‘开始’菜单,或用新的客户化的菜单连接它们。期望的结果显示如下:
第一件事情是建立在DllGetClassObject()中调用的主函数。这是进入Shell未可知领域的第一步。
STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID* ppv)
{
InstallHandler();
return CLASS_E_CLASSNOTAVAILABLE;
}
/*-------------------------------------------------------*/
// InstallHandler
//置换开始菜单和安装钩子
/*-------------------------------------------------------*/
void InstallHandler()
{
if(g_bInstalled)
{
int irc = MessageBox(HWND_DESKTOP,
__TEXT("The extension is installed. Would you like to uninstall?"),
__TEXT("Start"), MB_ICONQUESTION | MB_YESNO | MB_SETFOREGROUND);
if(irc == IDYES)
UninstallHandler();
return;
}
// 记住是否已经安装了处理器
g_bInstalled = TRUE;
// 设置新的‘开始’按钮
SetNewStartButton(TRUE);
}
在完成以后并需要恢复标准行为时,调用卸载其函数:
void UninstallHandler()
{
// 恢复标准设置
SetNewStartButton(FALSE);
// 处理器卸载
g_bInstalled = FALSE;
}
在探测器调用DllCanUnloadNow()来探索我们的库是否可以卸载时,这个处理器的存在现在就变成了关键因素。在这一节的最后我们要做的则是确保在安装这个处理器期间没有有威胁的事件发生。
STDAPI DllCanUnloadNow()
{
return (g_bInstalled ? S_FALSE : S_OK);
}
给出了这个函数之后,现在我们就可以操作安装和卸载‘开始’按钮的处理器了,现在让我们看一下完成任务所需要的几个步骤。
取得按钮的Handle
因为我们正在变换一个熟知的Windows界面部件,其结果是明显的,但是事实上我们正在做进入Shell地址空间这个工作的最艰难部分。剩余的工作仅简单地是把Win32编程技术应用于某些Shell对象而已。注意,这里真正重要的是我们的最小COM对象(在start.dll中)正工作在与探测器相同的环境中。‘开始’按钮是一个普通的‘Button’类窗口,就象在Spy++中显示的那样:
使用Spy++搜索工具在大量窗口栈中查找这个按钮是比较容易的:只要拖动搜索器到期望的窗口,它将在窗口列表中被选中。这个搜索工具在‘搜索|查找窗口…’菜单下。
如果想要编程恢复子窗口的Handle,应该使用FindWindowEx(),而不是FindWindow(),其差别在于,前者可以指定搜索开始的根窗口。在我们的情况下,‘开始’按钮是任务条的子窗口,在系统中它是Shell_TrayWnd窗口类的唯一窗口。
hwndTray = FindWindowEx(NULL, NULL, "Shell_TrayWnd", NULL);
hwndStart = FindWindowEx(hwndTray, NULL, "Button", NULL);
上面片断首先恢复任务条窗口的Handle,然后恢复类为‘Button’的第一个子窗口。
不管它们的外貌如何,你所看到的任务条上的其他‘按钮’都不是按钮。事实上它们也不是窗口—它们仅仅是类按钮的tab控件。
置换图像
再次观察上面的Spy++截图,你会注意到‘开始’按钮没有标题,也就是说,‘开始’这个词(对非英文版的Windows是被本地化的)是一个bitmap图像。然而在shell32.dll,或explorer.exe,或任何其它系统模块中你都不能找到这个图像的踪迹。这个图像是通过合并Windows标记图和一个资源中的串动态地建立的。二者均被存储在explorer.exe之中。
Windows标记图的ID是143,而‘开始’串在串表中的位置ID是578。
组合图像在内存设备关联中通过拷贝Windows标记图和绘制文字建立。
探测器资源的反向工程
如果查看探测器资源,你将发现,很多各种对话框中流行的图像(例如,‘任务条属性’对话框中显示的)都是动态建立的,以节省存储空间。事实上,explorer.exe文件中仅包含某些元素图像,而不是最终显示的结果图像。
要浏览某个应用的资源,下面是建议德操作步骤:
建立要浏览文件的备份,这是必要的,因为这个文件可能正在使用中。
用VC++打开它,一定要保证在‘打开’时的下拉框中指定‘资源’条件。
在Windows9x下,IDE将警告,不能更新资源。不管它。
在显示出资源树后,很容易就可以把它们保存到不同文件中。只需右击希望保存的资源和选择‘输出…’。这个操作仅仅适用于映射到文件的资源,如Bitmap,图表和光标。以及象AVI文件那样的客户资源。你不能保存对话框模版到文本文件。
开始按钮的风格
‘开始’按钮有BS_BITMAP风格,即,它的表面由图像而不是通常的文字覆盖,(你可以通过在Spy++列表中右击窗口,然后选择‘属性…|风格’来证实这一点)。调用下面函数可以很容易地得到这个图像的Handle:
g_hbmStart = reinterpret_cast<HBITMAP>(SendMessage(hwndStart,
BM_GETIMAGE, IMAGE_BITMAP, 0));
置换这个图像也不太困难。首先是用LoadImage()函数从应用的资源中装载一个新图像,其次SendMessage()函数允许我们把图像赋值给具有BS_BITMAP风格的按钮。lParam参数引用由LoadImage()返回的Handle。
HBITMAP hbm = reinterpret_cast<HBITMAP>(LoadImage(g_hInstance,
MAKEINTRESOURCE(IDB_NEWSTART), IMAGE_BITMAP, 0, 0, LR_DEFAULTSIZE));
SendMessage(hwndStart, BM_SETIMAGE,IMAGE_BITMAP, reinterpret_cast<LPARAM>(hbm));
我们在示例中使用的图像,其ID是IDB_NEWSTART,在resource.h文件中定义:
对这个示例我们选择了一个类似超链的图像,为了简化编码,我们把这个图像放进模块的资源中。这个图像与‘开始’按钮有相同的尺寸(48X16),你可以使用任何你喜欢的图像,但是建议你保持这个尺寸。简单地改变图像不能必然地导致按钮表面的立即刷新,按钮需要重新绘制它的非客户区域来反映我们所作的改变。我们可以通过调用SetWindowPos()函数强制执行这个操作:
SetWindowPos(hwndStart, NULL, 0, 0, 0, 0,
SWP_NOSIZE | SWP_NOZORDER | SWP_NOMOVE | SWP_DRAWFRAME);
为了看到工作的效果,我们需要实现SetNewStartButton()函数,它把我们前面给出的所有代码段穿成串,如下所示:
void SetNewStartButton(BOOL fNew)
{
// 取得‘开始’按钮的handle
HWND hwndTray = FindWindowEx(NULL, NULL, "Shell_TrayWnd", NULL);
HWND hwndStart = FindWindowEx(hwndTray, NULL, "Button", NULL);
// 改变图像
g_hbmStart = NewStartBitmap(hwndStart, fNew);
}
取得按钮的Handle是微不足道的工作,而使用一种方法置换其图像的操作就要求有一点逻辑了。这就是为什么我们将这段代码分离出来组成NewStartBitmap()函数调用的原因:
HBITMAP NewStartBitmap(HWND hwndStart, BOOL fNew)
{
if(!fNew)
{
if(g_hbmStart)
SendMessage(hwndStart, BM_SETIMAGE, IMAGE_BITMAP,
reinterpret_cast<LPARAM>(g_hbmStart));
// 刷新按钮响应变化
SetWindowPos(hwndStart, NULL, 0, 0, 0, 0,
SWP_NOSIZE | SWP_NOZORDER | SWP_NOMOVE | SWP_DRAWFRAME);
return NULL;
}
// 保存当前图像
g_hbmStart = reinterpret_cast<HBITMAP>(SendMessage(hwndStart,
BM_GETIMAGE, IMAGE_BITMAP, 0));
// 装如何设置新图像
HBITMAP hbm = reinterpret_cast<HBITMAP>(LoadImage(g_hInstance,
MAKEINTRESOURCE(IDB_NEWSTART), IMAGE_BITMAP, 0, 0, LR_DEFAULTSIZE));
SendMessage(hwndStart, BM_SETIMAGE, IMAGE_BITMAP,
reinterpret_cast<LPARAM>(hbm));
// 刷新按钮享用变化
SetWindowPos(hwndStart, NULL, 0, 0, 0, 0,
SWP_NOSIZE | SWP_NOZORDER | SWP_NOMOVE | SWP_DRAWFRAME);
return g_hbmStart;
}
现在有了一个需要建立工作 DLL 的全部代码,注册之后,就可以使用 DoGoInsideExplorer() 这样的函数来调用 SHLoadInProc() ,并且可以使这个假冒的 COM 对象进入探测器的地址空间。 评论这张 转发至微博 转发至微博版权声明:本文标题:转:Windows Shell 编程 第七章_1 内容由热心网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:https://www.elefans.com/dongtai/1726266123a1063275.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论