前言
Microsoft 说在其自家的 Visual C++ 版本5和6中提供了自动化接口来帮助开发人员扩展该产品,然而不幸的是,这些让人抓狂的接口(所提供的功能)过于简单而又带有一大堆bug,几乎不太可能利用它们来写出功能稍微复杂些的插件。
其实现在已经有部分第三方产品如 Bounds Checker 能够无缝整合到 Visual C++ 的 IDE,这些产品虽非插件但却不受限于自动化接口那些弱爆的功能。这些产品都是已 MFC 扩展 DLL (虽然文件后缀是 .pkg)的形式来实现的,它们的开发人员“偷偷地”使用了 Visual C++ 的一些未曾公开的接口:一个名为 DEVSHL 的扩展库。这个扩展库其实是 Visual C++ 的核心所在。DEVSHL.DLL 是组成 Visual C++ 的其中一个模块,它导出的函数都只有序号(没有名字),所以要使用它的话就得有对应的 .H 和 .LIB 文件才行。至于 Bounds Checker 的开发人员是从微软取得这些文件还是他们自己逆向得出,无从知道。当然了,如果有人乐意把 DEVSHL.H 和 DEVSHL.LIB 发给我,就当我永远欠他这个人情吧。
有些共享软件和商业软件也实现了与 Visual C++ 的整合,而且看起来它们的功能都远非自动化接口所能提供,重点是它们并没有使用微软的任何不为人知的技术。例如 Oz Solomonovich 开发的 WndTabs 插件(点击 这里 和 这里)、本人开发的 WorkspaceEx以及 RadVC。这些插件不仅仅提供了明显复杂的多的功能给用户,同时还能和 Visual C++ 的 UI 完美地融合在一起,而本文的目的正是要揭开实现这种程度的整合的内幕。
本文中偶尔会提到我的 WorkspaceEx 项目,这是一个共享软件,可以从我的 CoDeveloper 网站获得【编辑注:该链接已失效】,虽然我并没有公开它的源代码,但在这篇文章中我会讲解 WorkspaceEx 实现与 Visual C++ 整合所用到的大量技术。
Visual C++ 本身是一个 MFC 程序
这应该不会让人感到意外,要验证这一点只需(用 Dependency Walker )查看 MSDEV.EXE 的依赖模块列表就知道:它是一定会加载 MFC42.DLL 的(原文:It is bound (pun intended) to load MFC42.DLL,说是一语双关,但实在木有看出双关意义在哪,可能是指受限于 MFC 吧)。
实际上,MSDEV.EXE 就是一个典型的大型 MFC 程序的绝佳例子。如果你能一窥其代码实现(等会我们就会这么干)就能发现 Visual C++ 使用了文档/视图的架构,其大部分类都是从 CObject 继承下来的,而且使用了 MFC 核心类如 CString 和 CObList 等等。
你可能会注意到一个有意思的事情是这个 IDE 的很多功能都是用了一些比较“麻烦难搞”的方法实现的,比如 Visual C++ 里用的很多的一个 tab 控件,这些 tab 控件都不是原生的 Windows 公共 tab 控件,它们是专门为 Visual C++ 写的自定义控件类。如果让我来猜为什么这样,我推测那是因为这个 IDE 是在 tab 控件完成或者在 Windows 95 上可用之前写的,或者是这个控件在 Windows NT 3.5 不可用(我个人关于 Windows 的认知是从 NT 4.0 开始的)。
最后一个能揭露 Visual C++ 是一个 MFC 程序的线索是它提供的自定义程序向导(Custom App Wizards)要求开发人员使用 MFC 扩展 DLL 来开发插件以供 IDE 加载。
Package (.PKG) 文件
Visual C++ 由一个可执行文件(MSDEV.EXE)和一些 MFC 扩展 DLL 组成,这些 DLL 大部分是以 .PKG 文件后缀名存放于 BIN\IDE 子目录。而微软在此 IDE 的企业版中以 .PKG 文件的形式发布其附加功能。
虽然 .PKG 文件通常是 MFC 扩展 DLL,但这并非必要条件,作为一个 .PKG 文件,必须导出两个函数:InitPackage() 和 ExitPackage(),这两个函数貌似是如下原型:
DWORD _stdcall InitPackage(HANDLE hInstPkg); // 导出在序号 2
DWORD _stdcall ExitPackage(HANDLE hInstPkg); // 导出在序号 1
Visual Studio 在初始化进程时会加载 .PKG 文件并调用 InitPackage() 函数,.PKG 文件(模块)要返回 1 来表示已成功初始化。同样的,在 IDE 关闭时会调用 ExitPackage()。
传递给这两个函数的参数是 .PKG 文件自己的实例句柄。
为了让那些对使用 MFC 的 DLL 的了解已经生疏的读者理解本文,请允许我来更新一下你的记忆吧。DLL 有两种基本类型:自带 CWinApp 对象的常规 DLL 以及与主调程序共享 CWinApp 的扩展DLL。
作为 MFC 扩展 DLL 来实现的 .PKG 文件有一个显著的优点:它可以访问 Visual C++ 的 CWinApp 对象以及 MFC 对象。
要创建一个自定义 .PKG 文件,最简单的方法就是通过 MFC AppWizard (dll),选择扩展 DLL,然后添加并导出 InitPackage() 和 ExitPackage() 函数,修改项目设置和 .DEF 文件来生成 .PKG 文件后缀(而不是 .DLL)。
如果要在 MFC 扩展 .PKG 模块的 InitPackage() 函数中访问 Visual C++ 的 MFC 对象,只需要调用 AfxGetApp() 并使用其返回的指针来开始你的工作。
还有个很重要的注意事项:这些 MFC 扩展 .PKG 文件必须链接到 release 版(即非 debug 版)MFC 的 DLL 运行库,关于这一事项的进一步说明请看下面的 更好的自动化插件 部分。
Package 文件提供了另外一种途径来开发 Visual C++ 的扩展程序。IDE 会早于加载基于自动化接口实现的插件之前加载 .PKG 文件,而且这些扩展不需要已 COM 实现。不过 .PKG 文件不具有直接访问自动化接口的途径。如果你有兴趣了解的话,其实 WorkspaceEx 同时是 .PKG 扩展和自动化插件。
自动化插件
我敢打赌 95% 的 Visual C++ 插件是从 "DevStudio Addin Wizard" 自动生成的代码开始开发的,这些糟糕到让人恶心的代码从一开始就注定了这些插件是个悲剧的存在。如果你的插件是这样开始的,那么我强烈建议你认真读一遍那 些代码并考虑改造一下。
插件向导生成的代码会编译成一个 COM DLL,这个 DLL 实现了一个由 Visual C++ 实例化的对象。这里有一个有意思的事情是这些代码同时以 MFC 和 ATL 实现,这是一种不太常用的技术。我们马上会有个疑问:为什么?毕竟我们知道其实整个实现都可以很轻易地完全通过 ATL 来实现,在我看来,那样实现更简单(比较可能的原因是在微软写出该向导的时候 ATL 还很“稚嫩”:而这可以直接从它生成的代码中看出来)。
Visual C++ 使用了一种颇具疑问的技术来识别和初始化对象。最主要的问题是 IDE 究竟是如何知道插件对象自己的 CLSID 或 ProgID 从而初始化它的呢?
让我们来想象一下安装和使用新插件的步骤吧。通常来说,用户要打开 工具/自定义 对话框并选择“插件及宏文件(Add-ins and Macro File)”标签。假设插件并不存在于(Visual C++ 安装路径下的)AddIns 子目录,用户就需要自己浏览并找到该插件 DLL 文件。一旦识别成功,Visual Studio 就会加载这个插件然后这个插件就可以立即使用了。
这样来看貌似一切还行(不难理解),唯一不明的就是 IDE 用来检测从 DLL (这个 DLL 在一开始由用户指定的时候甚至还未被注册)实例化的具体对象所用到的神秘魔法。
一个插件可以说只是一个具有唯一 CLSID 的 COM 对象,它必须实现已知的文档化的 IDSAddin 接口才能被 Visual C++ 实例化,这样 Visual C++ 就可以调用该对象所有已知可用的方法,特别是 OnConnection() 和 OnDisconnection()。插件有责任在其OnConnection() 中调回 IDE 的某个接口并传递(通常是)另一个对象的调度接口,该对象实现实际的插件命令集。由于插件对象的 CLSID 是唯一而且不被系统以别的形式注册的,那么 Visual C++ 是怎么知道如何创建它呢?
我个人怀疑是党 Visual C++ 识别到一个可能的插件 DLL 时会尝试加载该 DLL 并且调用 DllRegisterServer(),这是一个在所有进程内 COM 服务器(in-process COM server)已知必须使用的函数。接着插件 DLL 就可以调用系统 API 来更新其注册信息到系统注册表上。我认为 IDE 会监视这种更新动作并且检测正在注册的对象的 CLSID,这样就可以创建这些对象并且从已知的 IDSAddin 接口查询其它接口。
可能这种猜测有点扯淡,我想还有另外已知途径就是 Visual C++ 会加载插件 DLL 的类型库,然后遍历它的 coclass 对象,不过我还是不认为这是 Visual C++ 采用的方法,毕竟一个插件即使没有包含任何 coclass 内容在类型库中也仍可以正常运作。
一个更简洁的架构可能是交由插件本身来将其注册到指定的组件种类中,然后 Visual C++ 就可以遍历该种类中的对象并实例化它们。当然了,这也只是说换做我的话会这么做罢了。
不管怎样,一旦一个插件首次成功加载,Visual C++ 就会将其加入注册表的一个已知插件列表中,这个列表的注册表键名如下:
HKEY_CURRENT_USER\Software\Microsoft\DevStudio\6.0\AddIns
所以只要正确更新该键值下的项目,一个插件完全可以实现自动安装(yonken注:自己安装自己,也就是不需要用户打开 IDE 手动添加插件)。每个插件在上面的 AddIns 项下都会有各自的子键,而且名称是插件的 ProgID,同时它还必须有一个值为 1 的默认键值(这个数字代表的是在 插件与宏文件 标签页上的插件列表对应项的复选框,即启用和关闭插件)。在这个键下面有 3 个必须要有的字符串键值分别是:Description(描述),DisplayName(显示名称)以及 Filename(文件名)。
我的插件的自动安装是这样实现的:在 DllRegisterServer() 执行正常的 COM 注册过程中同时添加必要的注册表项目,这样就可以不必在我的安装程序中再另外写一些代码来做这些事情,这样应该是个较为整洁的方案。安装程序只需要注册插 件 DLL,在下一次运行 Visual C++ 的时候马上就可以使用我的插件了。
不幸的是,DevStudio (即 Visual Studio)向导生成的插件并不会使用 .RGS (注册脚本)文件,而是在直接加入更新注册表的代码。我建议大家还是修改已有的插件代码改用 .RGS 脚本,同时在以后的插件开发中使用我提供的插件向导(下面会有讨论)。记住,已有的功能就是给我们用的,别自己重复发明轮子。
下面就是一个示例 .RGS 脚本,和我的 WorkspaceEx 插件中用到的差不多。第一段执行自动安装,第二段则执行对象注册。
HKCU
{
NoRemove Software
{
NoRemove Microsoft
{
NoRemove DevStudio
{
NoRemove '6.0'
{
NoRemove AddIns
{
ForceRemove 'WorkspaceEx.DSAddin.1' = s '1'
{
val Description = s 'Extends the capabilities of the Workspace window.'
val DisplayName = s 'WorkspaceEx'
val Filename = s '%MODULE%'
}
}DllRegisterServer
}
}
}
}
}
HKCR
{
WorkspaceEx.DSAddin.1 = s 'DSAddin Class'
{
CLSID = s '{4674EF43-FAA0-11D3-84A4-00A0C9E52DCB}'
}
WorkspaceEx.DSAddin = s 'DSAddin Class'
{
CLSID = s '{4674EF43-FAA0-11D3-84A4-00A0C9E52DCB}'
CurVer = s 'WorkspaceEx.DSAddin.1'
}
NoRemove CLSID
{
ForceRemove {4674EF43-FAA0-11D3-84A4-00A0C9E52DCB} = s 'WorkspaceEx'
{
Description = s 'Extends the capabilities of the Workspace window.'
ProgID = s 'WorkspaceEx.DSAddin.1'
VersionIndependentProgID = s 'WorkspaceEx.DSAddin'
InprocServer32 = s '%MODULE%'
{
val ThreadingModel = s 'both'
}
}
}
}
更好的自动化插件
Visual Studio 的插件向导生成的插件是一个常规 MFC DLL,也就是说它有自己的 CWinApp 对象,但是这样就会使得插件不能访问 Visual C++ 的 CWinApp 对象,除非这是个 .PKG 扩展模块。
实际上,要创建一个同时是进程内 COM 服务器(作为一个插件是必须的)又是 MFC 扩展 DLL(得以访问 Visual C++ 内部对象)是完全可能的。事先告诉你目前还没有任何微软的向导可以搞出这样的一个玩意出来,为了改变这一现状,我自己写了一个向导,在本文中有提供下载。
"CoDeveloper's Extension Addin Wizard(CoDeveloper 扩展插件向导)" 的使用非常简单:选择向导,修改插件默认的名字和描述,然后就可以生成代码了。
如果有人想要扩展这个向导来生成 .PKG 文件,或者增加更多的功能,不要客气,我会很乐意合作修改,只要生成的代码不是垃圾就好了。
这个 CoDeveloper 向导会生成一个老的微软向导生成的代码的升级版,最主要的不同点就是目标 DLL 是一个 MFC 扩展 DLL,而且这个 COM 对象是由一个 .RGS 脚本注册,以我之间这些代码更加整洁而且便于维护。
在生成这些将由 MSDEV.EXE 加载的 MFC 扩展 DLL 时还是有一些事情需要注意的:不管是在前面一节中提到的 .PKG 文件还是由 CoDeveloper 向导生成的插件都不能成功链接到 MFC 运行时的 debug 版本 DLL,这是因为 MSDEV.EXE 只会加载 MFC42.DLL,而 debug 版本的 MFC42D.DLL 有着稍微不同的内存结构。
若要避免这些潜在的头痛问题,我建议立即把你的插件项目的 Debug 配置删除,然后重新添加一个基于 Release 配置的版本,最后再手动修改增加 debug 的编译和链接选择,只要记住不要再把那些指示链接到 debug 运行时库的标志即可(而且也不要定义 _DEBUG),这个伪 debug 的编译配置基本上是可以正常使用的了:可以用来调试插件代码,不过不能在调试时跟踪进入运行时库。顺便告诉你,我实在是懒得在 CoDeveloper 向导中加入代码使其生成的项目包含这个伪 debug 配置,因为我曾看了一下那个杯具的通过自动化接口指定新配置的方法,最后也只能自叹:你还是去做别的更重要的事情吧。
当然了,你也可以重新生成 MFC42.DLL,使其带有调试信息,但是我发现在我系统上 MFC 的 makefile 不能正确地重新生成(VC++ SP4)。
菜鸟级插件技巧
你可能会想为啥你丫的这么想要访问 IDE 内部的 MFC 对象呢?为了帮助回答这个问题,我写了另一个插件叫 OpenVC。OpenVC 实现了两个命令(功能)DeleteNCB 和 ShowInnards。
DeleteNCB 和 WorkspaceEx 的 NukeNCB 命令类似,它提供给用户用来自动删除工程中损坏的 .NCB 文件。.NCB 文件时不时就会损坏,而且一旦那样 Visual Studio 的 Intellisense 和 ClassView 功能就会停止工作。以往要解决这个问题只能关闭工程,打开项目文件夹并删除 .NCB 文件,最后再重新打开工程。DeleteNCB 和 NukeNCB 只需点击一下按钮就可以自动完成上面这些操作。DeleteNCB 稍微没有 NukeNCB 好用,因为 Visual C++ 会弹出一个对话框询问是否关闭所有已打开的文件。
要实现 DeleteNCB 的功能必须要解决的问题就是插件本身需要知道 .NCB 文件的路径才能删除它。如果能够知道工程文件(.DSW)的路径自然就够了,因为这两个文件是在同一个文件夹下,然而自动化接口虽然提供了遍历工程中所有 项目的方法但却并没有提供工程本身属性的方法。
DeleteNCB 使用了一个非常漂亮的方法来解决这个问题。简单做一个小小的实验就可以发现工程是以一个继承于 CDocument 的对象实现的,DeleteNCB 只需遍历所有打开的 CDocument 实例直到它发现工程为止,接着就可以关闭工程、删除 .NCB 文件,最后重新打开工程了。
STDMETHODIMP CCommands::DeleteNCB()
{
// 保存所有文件
m_piApplication->ExecuteCommand(_bstr_t("FileSaveAll"));
// 获取 MSDEV CWinApp 对象
// (这就是那个神奇的”魔法“了)
CWinApp* pApp = AfxGetApp();
if (NULL == pApp) return E_FAIL;
// 遍历文档模板,寻找工程文档模板:
POSITION posdt = pApp->GetFirstDocTemplatePosition();
while (NULL != posdt)
{
CDocTemplate* pdt = pApp->GetNextDocTemplate(posdt);
if (0 == strcmp("CProjectWorkspaceDocTemplate",
pdt->GetRuntimeClass()->m_lpszClassName))
{
// 就用找到的第一个(也是唯一一个)文档:
POSITION posdoc = pdt->GetFirstDocPosition();
if (NULL == posdoc) break;
CDocument* pdoc = pdt->GetNextDoc(posdoc);
if (NULL == pdoc) break;
// 记下工程文件路径:
CString strWorkspace = pdoc->GetPathName();
if (0 == strWorkspace.GetLength()) break;
// 关闭工程
// 36633 是一个”魔法数字“(其实是个command ID),代表”文件/关闭工程“
pApp->GetMainWnd()->SendMessage(WM_COMMAND, 36633);
// 生成 NCB 文件名
CString strNCB = strWorkspace.Left(strWorkspace.GetLength()-4);
strNCB += ".ncb";
// 删除 NCB 文件
if (FALSE == ::DeleteFile(strNCB))
AfxMessageBox(CString("Error deleting NCB file:\n\n") + strNCB);
// 重新打开工程
pApp->OpenDocumentFile(strWorkspace);
}
}
return S_OK;
}
好吧,我听见你在嘀咕了:貌似不错,可是你丫的究竟是怎么知道工程类的名字是 CProjectWorkspaceDocTemplate 的?答案是我使用了另一个我写的工具,叫做 ShowInnards。ShowInnards 是 OpenVC 插件的第二个命令,它的用途就是显示 Visual C++ 内部类表。
ShowInnards() 使用了几个技术来检测 Visual C++ 进程中的 MFC 类的 CRuntimeClass 信息,通过使用 ShowInnards,我已经可以识别到超过 200 个类了,有少量缺失的也可以在将来再改进插件来识别到。
ShowInnards() 可以检测到类名、大小、继承关系以及版本号(较少用到),同时还会尝试检测类在内存中的实体结构,也就是说它还会尝试检测一个类的成员变量,这是通过逐个 字节检测一个类在内存中的实体来实现的。通过使用一些巧妙的方法,它可以检测一段字节是否一个指向 CObject 的指针或一个内嵌的从 CObject 继承下来的类,这些都是通过检查潜在对象可能的虚函数表来实现的。如果读到的字节可以认定是 CObject 的虚函数表,那么就调用该对象的 GetRuntimeClass() 方法。
bool CDlgClasses::GetRuntimeInfo(void* pvObj, CRuntimeClass** pprc)
{
CObject* pObj = (CObject*)pvObj;
// 我们想要从潜在的 CObject 指针中获得 MFC 运行时类别信息,
// 但问题是这个指针不一定真的指向 CObject,如果我们盲目地调用 pObj->GetRuntimeClass() 的话,
// 很可能会跑到错误的地方(非代码段)执行代码,甚至会不小心调用了别的虚函数,比如说是一个析构函数。
// 为了避免这种问题,我们在这里要先检查函数的特征,看看它是否真的像是 GetRuntimeClass(),
// 通常是一个 mov 指令然后一个 ret 指令。
//
// 我不能保证这段代码总是能工作,如果不行,那就只好交给你来修复了。
__try // 但愿 SEH 有用,当然如果不必用到它那是更好
{
// 检查地址有效性:
if (FALSE == AfxIsValidAddress(pObj, sizeof(CObject), FALSE))
return false;
// 检查虚函数表的指针的有效性
signature
void** vfptr = (void**)*(void**)pObj;
if (!AfxIsValidAddress(vfptr, sizeof(void*), FALSE))
return false;
// 检查虚函数表的首个内容的有效性
void* pvtf0 = vfptr[0];
if (IsBadCodePtr((FARPROC)pvtf0))
return false;
// 读取该函数开头的内容,判断它是否 mov 和 ret 指令
BYTE arrOpcodes[6];
memcpy(arrOpcodes, pvtf0, 6);
// 准备好了,下面的代码有些难看
// 如果你看不懂那倒可以跳过不管,不必自卑
if (arrOpcodes[0] == 0xFF && arrOpcodes[1] == 0x25) // jmp
{
void** pvAddr = *(void***)&(arrOpcodes[2]);
if (IsBadCodePtr((FARPROC)*pvAddr))
return false;
memcpy(arrOpcodes, *pvAddr, 6);
}
if (arrOpcodes[0] != 0xB8 || arrOpcodes[5] != 0xC3) // mov, ret
return false;
// OK,看起来这大概真的是个 GetRuntimeClass() 函数了。
// 那就认定这是个 CObject 了,下面就获取类的信息了
*pprc = pObj->GetRuntimeClass();
ASSERT(AfxIsValidAddress((*pprc)->m_lpszClassName, sizeof(char*), FALSE));
// 好吧,这里我假设所有的类都以 'C' 开头
ASSERT((*pprc)->m_lpszClassName[0] == 'C');
}
__except (1)
{
return false;
}
return true;
}
这个函数有点风险,不一定 100% 准确。我的代码已经实现了结构化异常处理以免错误调用非代码段内存,不过真正的风险是如果这个函数错误识别了 GetRuntimeClass() 函数(而把一个对象认定为 CObject),那么这个算法调用的就不是我们期望的 GetRuntimeClass 而可能是别的虚函数,比如说是这个对象的虚析构函数。那样的话,这个对象的内部状态就会被改变从而导致严重的错误,但是就 OpenVC 的目的而言,这个函数还是能正确工作的。
好了,现在 ShowInnards 有两种显示模式:报告模式使用了 Chris Maunder grid 控件(使用了 Ken Bertelson grid-tree 扩展)来显示一个被识别的类的报告,对于内部结构也被识别出来的类则会在右侧的 edit 控件中显示该对象的 'C' 风格的结构;图表模式则在一个 CScrollView 中显示所有类的继承关系。很抱歉这里用了一点烂的树形结构,但我想这样应该已经足够了。
了解 VC++ 的内部结构是实现 WorkspaceEx 的许多功能的关键。例如,WorkspaceEx 跟踪并更新 CWorkspaceView 类里面一 个用来存储选中标签序号的成员变量。除非是要修改 IDE 的默认行为,大多数的插件都不需要访问或操作这些结构。其实我还可以讨论更多关于我发现的所有技巧,不过那些都不是非常让人惊叹的东西。 ShowInnards 应该已经提供了足够的信息来帮组你来了解 VC++ 内部的组成了,如果要更进一步,建议你多使用调试器吧。
更多的技巧与提示
钩子和子类化
这是我最常在关于 WorkspaceEx 的问题中看到:究竟它是怎么能如此整洁的整合到 VC++ 的 UI 中的呢?这个问题的答案可以总结为两个词:钩子和窗口子类化。
WorkspaceEx、WndTabs 以及其它插件拦截或子类化了 Visual C++ 创建的窗口以便能在消息发给 IDE 之前进行处理,这些技术是非常强大的,理论上说可以使得一个程序的外观和原本完全不一样。如果你不信,看看 RadVC 吧。除此之外还可以访问并操纵一个程序内部的 MFC 结构,换言之一个插件可以做任何它想做的事情了。
下面让我来以一个例子来演示这究竟是怎么做的吧。WorkspaceEx 需要钩住 CWorkspaceView 这个窗口的消息。为了找到正确的窗口,WorkspaceEx 的内部实现遍历了 MSDEV 进程的所有窗口来进行查找,这个过程大致如下:
BOOL CALLBACK FindWorkspaceProc(HWND hwnd, LPARAM lParam)
{
CWnd* pWnd = CWnd::FromHandle(hwnd);
CString strClass = pWnd->GetRuntimeClass()->m_lpszClassName;
if (strClass == "CWorkspaceView")
g_wndWorkspace.HookWindow(hwnd);
return TRUE;
}
请注意这个函数是如何检测当前窗口的 MFC 类的。这种方法非常容易识别窗口,当然应该还有别的途径来识别窗口(标题、ID、窗口类等等),但这个方法很直观而且能正常工作。
你看到这里用了一个全局变量 g_wndWorkspace,这是一个从 CSubclassWnd 继承下来的类,这个类由 Paul DiLascia 开发,我建议大家使用这个类或类似的类来实现窗口的子类化,这简化了我们的工作。
在某些情况下,使用窗口钩子来拦截窗口消息是非常有用的,例如 WorkspaceEx 中就用了这种技术在 Visual C++ 的选项对话框的 tab 控件中插入了它自己的 tab,首先 WorkspaceEx 会通过使用 WH_CBT 钩子来拦截选项对话框的创建过程,一旦选项对话框创建(通过名字大小和窗口样式来识别),钩子函数就会使用前面提到的方法子类化其窗口。
工具栏
我听过很多人抱怨 VC++ 的自动化接口的一个缺点就是不能指定插件工具栏的名字(VC++ 允许我们创建一个带有几个按钮控件的工具栏,点击按钮执行插件对应的命令)。
这个问题同样可以通过使用窗口 CBT 钩子来解决。在调用 AddCommandBarButton() 之前,安装一个钩子来监视创建标题包含”Toolbar“的过程,那么在工具栏窗口被创建时钩子函数就会被调用,在函数返回之 前,可通过直接修改窗口创建结构体中的窗口名来修改最终出现的工具栏标题。需要注意的是,新的名字必须是和原本的名字同样长度(或更短),也就是说最大只 能8个字符(包括终结符)。这个钩子函数大致如下:
LRESULT CALLBACK FindToolbarProc(int nCode, WPARAM wParam, LPARAM lParam)
{
// 监视窗口创建过程:
if (nCode == HCBT_CREATEWND)
{
CBT_CREATEWND* pcw = (CBT_CREATEWND*)lParam;
// 判断是否在创建工具栏,如是则修改其名字
if (pcw->lpcs->lpszName &&
0 != strstr(pcw->lpcs->lpszName, "Toolbar"))
{
strcpy((char*)pcw->lpcs->lpszName, "NukeNCB");
}
}
return CallNextHookEx(g_hkCBT, nCode, wParam, lParam);
}
关于工具栏还有另外一个问题,VC++ 的文档说那个用来创建工具栏的方法,也就是 AddCommandBarButton(),只能在插件的 OnConnection() 方法中调用,而且还要求此时 OnConnection() 的参数 bFirstTime 的值为 VARIANT_TRUE 才能调用。但是,如果你的插件是自动安装的(前面提到),这个参数在 OnConnection 被调用时永远不会是VARIANT_TRUE,而如果你尝试在 bFirstTime 的值为 VARIANT_FALSE 的时候调用该方法则调用会出现断言失败和错误。
对于这个问题其实有一个简单的方案,我发现 AddCommandBarButton() 实际上在 OnConnection() 方法返回后调用的话并不会失败,即使 bFirstTime 根本不是 VARIANT_TRUE。所以你需要修改你的插件,使其在 OnConnection() 之后创建工具栏,而这可以有多种途径实现。我建议你可以在 OnConnection() 里创建一个隐藏的窗口(或钩住已有的窗口)并在 OnConnection() 返回之前发送一个特殊的消息给该窗口,这样在该消息被处理时 OnConnection() 应该已经返回了,这时插件就可以调用 AddCommandBarButton()。不过,如果你真的决定使用这个技术,那么判断是否已经添加工具栏从而避免多次创建就是作为开发人员的你的责 任了。
结论
就是这样了,至于怎样更好地运用这些技术就只能交给读者自行实践了。