在指南的第一部分和第二部分,我向大家演示了如何编写上下文菜单扩展。在第三部分,我将燕是一种新的扩展类型,向大家解释如何共享外壳的内存,并且演示如何在ATL之外使用MFC。
第三部分假设你已经知道了外壳扩展的基本知识(在第一部分中解释了),而且你对MFC很熟悉。要注意的是这儿的扩展需要4.71或者更高版本的扩展,所以你必须是运行Windows 98 或 2000,或者在95/NT4 上装有活动桌面(Active Desktop)。
查询信息扩展(The QueryInfo extension)
活动桌面(Active Desktop)引进了一个新特征,如果你的鼠标在特定的对象上悬停的话,工具条提示会显示对象的描述。比如说,在“我的电脑”上悬停,就会出现如下的工具提示:
其它一些对象比如说“网上邻居”和“控制面板”也有相似的提示。我们也可以通过查询信息扩展(QueryInfo extension)为其他一些对象提供我们自己的工具提示。
关于查询信息扩展(QueryInfo extension)的说明是:这是我命名的;我这么称呼他是因为他用了这么一个借口:IQueryInfo 。到现在我可以说,它还没有一个官方名字。我快速的查看了一下1999年十月的MSDN,甚至没有提到这个扩展。 很明确它是一个被支持的扩展,因为微软的Office也为它的文件类型安装了QueryInfo扩展,如下所示:
WinZip 版本8也有一个为压缩文件安装的QueryInfo扩展:
我已经找到的最佳的文档是Dino Esposito的在2000年3月的MSDN杂志上的文章“Enhance Your User's Experience with New Infotip and Icon Overlay Shell Extensions(使用新的信息提示和图标覆盖外壳扩展来增强你的用户的体验)”。
QueryInfo扩展的开始?它能做什么?
这个外壳扩展将是一个快速文本文件查看器——它将显示文件大小和文件的第一行的内容。当用户在一个TXT文件上悬停鼠标的时候,我们的信息将在工具提示(tooltip)上显示。
用AppWizard 开始
运行AppWizard ,做一个新的ATL COM wizard app。我们叫它TxtInfo 。因为我们这次要使用MFC,所以请选中Support MFC 复选框,然后单击完成。我们现在就有了一个空的将会生成DLL的ATL项目,但是我们必须添加自己的外壳扩展COM对象。在ClassView树中,右键单击 TxtInfo classes项,选择New ATL Object。
在ATL Object 向导,第一面板已经选择了Simple Object ,只要单击下一步就行了。在第二面板中,在Short Name 编辑控件中输入TxtInfoShlExt ,然后单击确定(面板中的其它的编辑框将会自动完成)。这就创建了一个类名为CTxtInfoShlExt 的类,它包含了实现一个COM对象的基本代码。我们将向这个类添加我们的代码。
如果你看一下ClassView树的的时候,你将会发现我们有一个从CWinApp派生的CTxtInfoApp类。这个类和全局变量theApp的出现使我们使用MFC成为可能,正如我们编写一个没有ATL的普通MFC DLL一样。
接口实现
以前,在我们的上下文菜单扩展中(context menu extensions),我们实现Explorer实现我们对象的IShellExtInit接口。对外壳扩展来说,还有另一个实现接口,IPersistFile这是一个QueryInfo 扩展使用的接口。为什么会不同?如果你还记得的话,IShellExtInit::Initialize()接收一个IDataObject 指针,通过该指针我们可以枚举被选中的文件。通过IPersistFile扩展只能对单个文件进行操作。因为鼠标不可能在同一时间在两个对象上悬停,所以QueryInfo扩展一次只在一个文件上工作,所以它使用IPersistFile 。
所以我们需要添加IPersistFile到CTxtInfoShlExt 实现的接口列表中。打开TxtInfoShlExt.h,然后添加下面红色的行:
#include <comdef.h>
#include <shlobj.h>
class ATL_NO_VTABLE CTxtInfoShlExt :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CTxtInfoShlExt, &CLSID_TxtInfoShlExt>,
public IDispatchImpl<ITxtInfoShlExt, &IID_ITxtInfoShlExt, &LIBID_TXTINFOLib>,
public IPersistFile
{
BEGIN_COM_MAP(CTxtInfoShlExt)
COM_INTERFACE_ENTRY(ITxtInfoShlExt)
COM_INTERFACE_ENTRY(IDispatch)
COM_INTERFACE_ENTRY(IPersistFile)
END_COM_MAP()
我们还需要一个变量来存储Explorer在我们实现过程中的文件名:
protected:
// ITxtInfoShlExt
CString m_sFilename;
注意的是我们现在可以在任何地方使用一个MFC对象。
如果你查看IPersistFile的文档,你会发现有很多的方法。幸运的是,对本文的扩展,我们仅仅需要实现Load()而忽略其他的。下面是IPersistFile的方法的原型。
public:
// IPersistFile
STDMETHOD(GetClassID)(LPCLSID) { return E_NOTIMPL; }
STDMETHOD(IsDirty)() { return E_NOTIMPL; }
STDMETHOD(Load)(LPCOLESTR, DWORD);
STDMETHOD(Save)(LPCOLESTR, BOOL) { return E_NOTIMPL; }
STDMETHOD(SaveCompleted)(LPCOLESTR) { return E_NOTIMPL; }
STDMETHOD(GetCurFile)(LPOLESTR*) { return E_NOTIMPL; }
除了Load()的任何方法都仅仅返回E_NOTIMPL,表示我们并不实现他。
更加漂亮的是我们的Load()方法非常简单。我们仅需要存储Explorer传给我们的文件的名称。这是鼠标悬停的那个文件。
HRESULT CTxtInfoShlExt::Load ( LPCOLESTR wszFilename, DWORD dwMode )
{
AFX_MANAGE_STATE(AfxGetStaticModuleState()); // init MFC
// Let CString convert the filename to ANSI if necessary.
m_sFilename = wszFilename;
return S_OK;
}
注意函数的第一行。要使MFC工作正常,这行是必需的。因为我们的DLL被非MFC程序装载,每个使用MFC的出口函数必须人工初始化MFC。如果不包括这一行,很多的MFC函数(大多是和资源相关的)将会中断或产生断言。
文件名被保存在m_sFilename以备后用。注意,我使用了CString赋值操作符转换字符串到ANSI的优点,如果这个DLL是作为ANSI建立的话。
创建工具提示文本
在Explorer调用我们的Load()方法之后,它调用QueryInterface()来获得另一个接口:IQueryInfo 。IQueryInfo是一个相当简单的接口,仅有两个方法(实际上我们只是用了其中一个)。再次打开TxtInfoShlExt.h ,添加如下红颜色的行:
class ATL_NO_VTABLE CTxtInfoShlExt :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CTxtInfoShlExt, &CLSID_TxtInfoShlExt>,
public IDispatchImpl<ITxtInfoShlExt, &IID_ITxtInfoShlExt, &LIBID_TXTINFOLib>,
public IPersistFile,
public IQueryInfo
{
BEGIN_COM_MAP(CTxtInfoShlExt)
COM_INTERFACE_ENTRY(ITxtInfoShlExt)
COM_INTERFACE_ENTRY(IDispatch)
COM_INTERFACE_ENTRY(IPersistFile)
COM_INTERFACE_ENTRY(IQueryInfo)
END_COM_MAP()
然后添加IQueryInfo的方法:
// IQueryInfo
STDMETHOD(GetInfoFlags)(DWORD*) { return E_NOTIMPL; }
STDMETHOD(GetInfoTip)(DWORD, LPWSTR*);
GetInfoFlags()方法目前不被应用,我们仅仅返回E_NOTIMPL。 GetInfoTip()是我们返回给Explorer并让它显示在工具条提示上的实现之处。首先是讨厌的模块:
HRESULT CTxtInfoShlExt::GetInfoTip (
DWORD dwFlags,
LPWSTR* ppwszTip )
{
AFX_MANAGE_STATE(AfxGetStaticModuleState()); // init MFC
LPMALLOC pMalloc;
CStdioFile file;
DWORD dwFileSize;
CString sFirstLine;
BOOL bReadLine;
CString sTooltip;
USES_CONVERSION;
再次的,为了初始化MFC,AFX_MANAGE_STATE 首先被调用。这必须是在函数开始就被做,甚至在变量定义之前,因为很多的构造器是调用了MFC函数。
dwFlags 目前未被应用。ppwszTip 是一个指向一个LPWSTR 的指针(Unicode字符串指针),我们设置它指向我们必须分配的缓存。
第一步,我们将试图打开文件。我们知道它的文件名,因为我们早先在Load()函数中将它存储了。
if ( !file.Open ( m_sFilename , CFile::modeRead | CFile::shareDenyWrite ))
return E_FAIL;
现在,由于我们需要使用外壳内存分配器来分配一个缓存,我们需要一个IMalloc 接口指针。通过调用SHGetMalloc()来获得这个指针:
if ( FAILED( SHGetMalloc ( &pMalloc )))
return E_FAIL;
稍候,关于IMalloc 我有更多要说的东西。下一步是获得文件的大小,并读出文件的第一行:
// Get the size of the file.
dwFileSize = file.GetLength();
// Read in the first line from the file.
bReadLine = file.ReadString ( sFirstLine );
bReadLine 通常都是TRUE,除非这个文件是不可访问的或者只有0字节长。下一步是创建工具提示(tooltip)的第一部分,列出了文件的大小:
sTooltip.Format ( _T("File size: %lu"), dwFileSize );
现在,如果我们能够阅读文件的第一行,把它添加到工具提示(tooltip)中。
if ( bReadLine )
{
sTooltip += _T("\n");
sTooltip += sFirstLine;
}
现在,我们已经完成了工具提示。我们需要分配一个缓存。这儿我们使用IMalloc 。由SHGetMalloc()返回的指针是外壳IMalloc接口的一个拷贝。任何使用那个接口分配的内存都存在于外壳的进程空间中,因此外壳可以使用它。更重要的,外壳也可以释放它。所以我们要做的是分配缓存,然后就忘了它吧。外壳在使用完了之后会释放它的。
另外一件需要意思到的是我们返回给外壳的字符串必须是Unicode的。这就是为什么我们在下面的Alloc()调用中计算时乘以sizeof(wchar_t);仅仅分配lstrlen(sToolTip)长度的内存只有必需的内存的一半。
*ppwszTip = (LPWSTR) pMalloc->Alloc ( (1 + lstrlen(sTooltip)) * sizeof(wchar_t) );
if ( NULL == *ppwszTip )
{
pMalloc->Release();
return E_OUTOFMEMORY;
}
// Use the Unicode string copy function to put the tooltip text in the buffer.
wcscpy ( *ppwszTip, T2COLE((LPCTSTR) sTooltip) );
最后一件要做的事是释放我们早先的到的IMalloc 接口。
pMalloc->Release();
return S_OK;
}
这就是所有我们要做的。Explorer获得在*ppwszTip中的字符串,并把它显示在工具提示(tooltip)中。
注册外壳扩展
QueryInfo扩展的注册和上下文菜单扩展稍有不同。我们的扩展在HKEY_CLASSES_ROOT 的子键下注册,它的名字是我们想处理的文件扩展名。在这篇文章里,它是HKCR\.txt 。不过等一等,好像有点奇怪!你可能认为ShellEx 的子键看起来应该像"TooltipHandlers"之类的样子。但是不是!这个键被叫做 "{00021500-0000-0000-C000-000000000046}"。
在这儿,我想微软可能试图越过我们偷偷摸摸做些外壳扩展。如果你看看注册表的话,你会发现其它的ShellEx 的子键的名称也是GUID。上面的GUID正好是IQueryInfo的GUID。
不管怎样,这是是我们的扩展被.TXT文件调用的必须脚本:
HKCR
{
NoRemove .txt
{
NoRemove shellex
{
NoRemove {00021500-0000-0000-C000-000000000046} = s '{F4D78AE1-05AB-11D4-8D3B-444553540000}'
}
}
}
你可以通过复制上面的这段代码,然后改变".txt"为任何你想要的扩展名来使扩展被其他扩展名的文件调用。不幸的是,你不能在*或者 AllFileSystemObjects下注册达到能被所用文件调用的目的。
正如我们在前面的扩展中所讲的一样,在NT/2000下,我们需要添加我们的扩展到“认证的(approved)”扩展列表当中。实现这个过程的代码在 DllRegisterServer()和DllUnregisterServer()函数中。
待续...
在第四部分中,我们将回到上下文菜单中去看看一种新的扩展,拖和扔(drag and drop)处理。我们也将看到更多的MFC的使用。
你可以从下面的网址获得这个和其他文章的最新版本:http://home.inreach.com/mdunn/code/