背景:
前段时间开发时遇到一个文件生成到桌面,然后选中桌面图标的功能要求。大家知道选中文件夹下的文件,可以使用 SHOpenFolderAndSelectItems 函数,当时就想,选中桌面图标,应该也是提供了对应的API。结果网上搜了一圈,并没发现对应的函数。
于是我的目光很快转移到 IShellView 这个COM接口上,去MSDN文档一看,果然有SelectItem这个函数。根据经验,八九不离十 就是它了。
但额外的问题来了,如果才能拿到属于桌面的 IShellView 接口呢?兜兜转转,在网上找了一些资料总算把代码整理出来,虽然我也不知道 QueryDesktopShellView 为什么要这样做,但它确实取到了桌面的 IShellView 接口。这正是COM技术的恶心之处,它看起来更规范,而且不需要你理解函数内部实现,但正因如此,很多人都被COM技术弄的云里雾里。
吐槽完了,这种网上找不到现成例子,实现起来也弄不清楚逻辑的代码,我还是记录一下,免得以后忘记了。
实现代码:
#include <iostream>
#include <ShObjIdl_core.h>
#include <comutil.h>
#include <exdisp.h>
#include <winerror.h>
#include <atlcomcli.h>
#include <shlguid.h>
#include <shtypes.h>
#include <vector>
#include <ShlObj_core.h>
//反正这个函数就能取到桌面的 IShellView
bool QueryDesktopShellView(IShellView** pShellView)
{
long hWnd = 0;
_variant_t VarLock;
CComPtr<IShellWindows> pShellWindows;
CComQIPtr<IWebBrowserApp> pWebBrowserApp;
CComQIPtr<IServiceProvider> pServiceProvider;
CComPtr<IShellBrowser> pShellBrowser;
if (NULL == pShellView)
{
return false;
}
if (FAILED(pShellWindows.CoCreateInstance(CLSID_ShellWindows)))
{
return false;
}
if (!pShellWindows || FAILED(pShellWindows->FindWindowSW(&VarLock, &VarLock, SWC_DESKTOP,
&hWnd, SWFO_NEEDDISPATCH, reinterpret_cast<IDispatch**>(&pWebBrowserApp))))
{
return false;
}
if (!pWebBrowserApp || FAILED(pWebBrowserApp->QueryInterface(IID_IServiceProvider,
reinterpret_cast<void**>(&pServiceProvider))))
{
return false;
}
if (!pServiceProvider || FAILED(pServiceProvider->QueryService(SID_STopLevelBrowser, IID_IShellBrowser,
reinterpret_cast<LPVOID*>(&pShellBrowser))))
{
return false;
}
if (!pShellBrowser || FAILED(pShellBrowser->QueryActiveShellView(pShellView)))
{
return false;
}
return true;
}
int main()
{
LPITEMIDLIST pidl = NULL;
std::vector<LPITEMIDLIST> vecItem;
IShellView *pShellView = NULL;
HRESULT hr = S_OK;
CoInitialize(NULL);
if (false == QueryDesktopShellView(&pShellView))
{
return false;
}
pidl = ::ILCreateFromPathW(L"腾讯QQ.lnk");
if (pidl)
{
vecItem.push_back(pidl);
}
pidl = ::ILCreateFromPathW(L"微信.lnk");
if (pidl)
{
vecItem.push_back(pidl);
}
pShellView->SelectItem(NULL, SVSI_DESELECTOTHERS);//取消之前的选中
for (int i = 0; vecItem.size() > i; i++)
{
hr |= pShellView->SelectItem(vecItem[i], SVSI_SELECT);
}
pShellView->UIActivate(SVUIA_ACTIVATE_FOCUS);//设置桌面焦点
for (int i = 0; vecItem.size() > i; i++)
{
::ILFree(vecItem[i]);
}
pShellView->Release();
CoUninitialize();
return 0;
}
代码刚写完的时候,调用了SelectItem函数,桌面却没有任何反应,搞的我有点怀疑人生,第二天回来的时候,我突然醒悟,肯定是桌面没有焦点的原因!于是再上MSDN看了一下文档,加了一句
pShellView->UIActivate(SVUIA_ACTIVATE_FOCUS);
OK, 图标被选中了。
缺陷:
代码写完后测试了几次。测试的代码流程大概是:生成文件到桌面路径,则调用选中桌面图标函数。
但偶尔会出现没选中的情况,我捋了几次项目代码逻辑,没有问题啊。
最后推测原因如下:
- 文件生成到桌面目录后,桌面进程收到目录变化的通知,会重新加载桌面
- SelectItem 肯定是使用进程间通信通知桌面选中图标的
- 由于目录变更通知和我的SelectItem 函数,均存在于不同的进程,那他们内部线程执行的顺序也是不确定的, 如果发送目录变更通知的线程响应不够快,则 explorer.exe 可能先收到 SelectItem 通知,再收到目录变更通知,就会出现没选中的情况。
简单的解决办法是延时一下 SelectItem 的执行,不过我这边不纠结这种问题,所以并没进行额外处理。
另外桌面图标展示的是私有文件夹和公共文件夹,这两个文件夹里面的图标,所以桌面是会有同名文件的。若必须要求准确命中对应的Item, 估计要利用 GetItemObject 函数(不用知道Item数量,直接 i++ 枚举就行了)来查看Item 信息再进行选中。不过我这边暂时没打算做的这么细致,所以还是留给大家研究一下怎么实现吧。