有的时候,软件开发是创造新的东西,不过更常见的是把现有的东西组合到一起。今天的难题就属于后一种。
给定一个窗口句柄,你可以判定:(1)是否是一个资源管理器窗口,如果是,那么(2)它正在显示哪个文件夹,而且(3)当前焦点在哪一项上。
这其实不是一件难事。你只需把许多小碎片拼凑起来就可以。
一切从 ShellWindows 对象开始,它代表所有打开的外壳窗口。你可以使用 Item 属性遍历它们。用 C++ 来做这事看起来有点笨拙,因为 ShellWindows 对象就是为像 JScript 或者 Visual Basic 这样的脚本语言的使用所设计的。
IShellWindows *psw;
if (SUCCEEDED(CoCreateInstance(CLSID_ShellWindows, NULL, CLSCTX_ALL,
IID_IShellWindows, (void**)&psw))) {
VARIANT v;
V_VT(&v) = VT_I4;
IDispatch *pdisp;
BOOL fFound = FALSE;
for (V_I4(&v) = 0; !fFound && psw->Item(v, &pdisp) == S_OK;
V_I4(&v)++) {
...
pdisp->Release();
}
psw->Release();
}
对于每一个元素,我们可以询问其窗口句柄以判断是否是我们需要的。
IWebBrowserApp *pwba;
if (SUCCEEDED(pdisp->QueryInterface(IID_IWebBrowserApp, (void**)&pwba))) {
HWND hwndWBA;
if (SUCCEEDED(pwba->get_HWND((LONG_PTR*)&hwndWBA)) &&
hwndWBA == hwndFind) {
fFound = TRUE;
...
}
pwba->Release();
}
不错,现在我们已经通过各元素的 IWebBrowserApp 接口找到了目标,接下来需要得到顶级外壳浏览器对象(top shell browser)。这可以通过查询 SID_STopLevelBrowser 服务并请求 IShellBrowser 接口来完成。
IServiceProvider *psp;
if (SUCCEEDED(pwba->QueryInterface(IID_IServiceProvider, (void**)&psp))) {
IShellBrowser *psb;
if (SUCCEEDED(psp->QueryService(SID_STopLevelBrowser,
IID_IShellBrowser, (void**)&psb))) {
...
psb->Release();
}
psp->Release();
}
我们再使用 QueryActiveShellView 方法从 IShellBrowser 接口请求当前的外壳视图(shell view)。
IShellView *psv;
if (SUCCEEDED(psb->QueryActiveShellView(&psv))) {
...
psv->Release();
}
当然,我们真正想要的其实是 IFolderView 接口,它是一个自动化对象,囊括了所有的好东西。
IFolderView *pfv;
if (SUCCEEDED(psv->QueryInterface(IID_IFolderView,
(void**)&pfv))) {
...
pfv->Release();
}
挺好。想从视图里得到些什么呢,正被浏览的 IShellFolder 的位置怎么样?做这件事情,我们需要使用 IPersistFolder2::GetCurFolder。GetFolder 方法可以使我们访问到外壳文件夹(shell folder),我们要从那里请求 IPersistFolder2。(大部分情况下你需要 IShellFolder 接口,因为大多数的酷玩意儿都在那儿。)
IPersistFolder2 *ppf2;
if (SUCCEEDED(pfv->GetFolder(IID_IPersistFolder2,
(void**)&ppf2))) {
LPITEMIDLIST pidlFolder;
if (SUCCEEDED(ppf2->GetCurFolder(&pidlFolder))) {
...
CoTaskMemFree(pidlFolder);
}
ppf2->Release();
}
我们把 pidl 转换为用于显示的路径。
if (!SHGetPathFromIDList(pidlFolder, g_szPath)) {
lstrcpyn(g_szPath, TEXT("<not a directory>"), MAX_PATH);
}
...
用我们已经得到的这些可以干什么呢?哦,对了,来看一下当前的焦点在什么上吧。
int iFocus;
if (SUCCEEDED(pfv->GetFocusedItem(&iFocus))) {
...
}
要把聚焦项的名字显示出来,我们需要该项的 pidl 和 IShellFolder。(看看,我告诉过你好东西就在 IShellFolder 那儿)。项可以通过 Item 方法得到。
LPITEMIDLIST pidlItem;
if (SUCCEEDED(pfv->Item(iFocus, &pidlItem))) {
...
CoTaskMemFree(pidlItem);
}
(如果想得到选中项的列表,我们可以使用 Items 方法,需要传入 SVGIO_SELECTION。)
得到 pidl 之后,还需要 IShellFolder:
IShellFolder *psf;
if (SUCCEEDED(ppf2->QueryInterface(IID_IShellFolder,
(void**)&psf))) {
...
psf->Release();
}
然后我们用这两样东西以及 GetDisplayNameOf 方法的辅助来获取项的显示名字。
STRRET str;
if (SUCCEEDED(psf->GetDisplayNameOf(pidlItem, SHGDN_INFOLDER,
&str))) {
...
}
我们可以使用辅助函数 StrRetToBuf 把那个乖僻的 STRRET 结构转换为令人生厌的字符串缓冲区。(怪异的 STRRET 结构的历史容以后再说。)
StrRetToBuf(&str, pidlItem, g_szItem, MAX_PATH);
好,下面我们把所有这些都组合起来。代码看起来相当丑陋,因为我把所有东西都放到了一个大函数里而没有把它们分离为子函数。在“现实生活”中,我会把它们分为小的辅助函数,那样更便于管理。
打开 scratch(译者注:原作者的一个测试用程序原型) 程序,添加一下函数:
#include <shlobj.h>
#include <exdisp.h>
TCHAR g_szPath[MAX_PATH];
TCHAR g_szItem[MAX_PATH];
void CALLBACK RecalcText(HWND hwnd, UINT, UINT_PTR, DWORD)
{
HWND hwndFind = GetForegroundWindow();
g_szPath[0] = TEXT('/0');
g_szItem[0] = TEXT('/0');
IShellWindows *psw;
if (SUCCEEDED(CoCreateInstance(CLSID_ShellWindows, NULL, CLSCTX_ALL,
IID_IShellWindows, (void**)&psw))) {
VARIANT v;
V_VT(&v) = VT_I4;
IDispatch *pdisp;
BOOL fFound = FALSE;
for (V_I4(&v) = 0; !fFound && psw->Item(v, &pdisp) == S_OK; V_I4(&v)++) {
IWebBrowserApp *pwba;
if (SUCCEEDED(pdisp->QueryInterface(IID_IWebBrowserApp, (void**)&pwba))) {
HWND hwndWBA;
if (SUCCEEDED(pwba->get_HWND((LONG_PTR*)&hwndWBA)) &&
hwndWBA == hwndFind) {
fFound = TRUE;
IServiceProvider *psp;
if (SUCCEEDED(pwba->QueryInterface(IID_IServiceProvider, (void**)&psp))) {
IShellBrowser *psb;
if (SUCCEEDED(psp->QueryService(SID_STopLevelBrowser,
IID_IShellBrowser, (void**)&psb))) {
IShellView *psv;
if (SUCCEEDED(psb->QueryActiveShellView(&psv))) {
IFolderView *pfv;
if (SUCCEEDED(psv->QueryInterface(IID_IFolderView,
(void**)&pfv))) {
IPersistFolder2 *ppf2;
if (SUCCEEDED(pfv->GetFolder(IID_IPersistFolder2,
(void**)&ppf2))) {
LPITEMIDLIST pidlFolder;
if (SUCCEEDED(ppf2->GetCurFolder(&pidlFolder))) {
if (!SHGetPathFromIDList(pidlFolder, g_szPath)) {
lstrcpyn(g_szPath, TEXT("<not a directory>"), MAX_PATH);
}
int iFocus;
if (SUCCEEDED(pfv->GetFocusedItem(&iFocus))) {
LPITEMIDLIST pidlItem;
if (SUCCEEDED(pfv->Item(iFocus, &pidlItem))) {
IShellFolder *psf;
if (SUCCEEDED(ppf2->QueryInterface(IID_IShellFolder,
(void**)&psf))) {
STRRET str;
if (SUCCEEDED(psf->GetDisplayNameOf(pidlItem,
SHGDN_INFOLDER,
&str))) {
StrRetToBuf(&str, pidlItem, g_szItem, MAX_PATH);
}
psf->Release();
}
CoTaskMemFree(pidlItem);
}
}
CoTaskMemFree(pidlFolder);
}
ppf2->Release();
}
pfv->Release();
}
psv->Release();
}
psb->Release();
}
psp->Release();
}
}
pwba->Release();
}
pdisp->Release();
}
psw->Release();
}
InvalidateRect(hwnd, NULL, TRUE);
}
剩下的就是定期调用此函数并打印结果。
BOOL
OnCreate(HWND hwnd, LPCREATESTRUCT lpcs)
{
SetTimer(hwnd, 1, 1000, RecalcText);
return TRUE;
}
void
PaintContent(HWND hwnd, PAINTSTRUCT *pps)
{
TextOut(pps->hdc, 0, 0, g_szPath, lstrlen(g_szPath));
TextOut(pps->hdc, 0, 20, g_szItem, lstrlen(g_szItem));
}
现在已经准备就绪了。运行程序,把它放到一边,然后启动资源管理器并观察程序,会发现它可以跟踪你所在的文件夹和焦点所在的项。
我希望我已经证实了我的观点:通常,你所需的东西的所有片断已经存在了,你只不过是需要指出怎样把它们拼在一块。注意,每一小片都不大。只是你必须能意识到可以用一种有趣的方法把它们放到一起。
练习:修改程序,使它得到文件夹并将其切换到详细信息视图