目录
1 概述
本文基于Qt利用Windows的Shell编程实现文件和目录的右键菜单。
2 定义
Windows Shell编程需要引用头文件:
#include <Shlobj.h>
#include <shlwapi.h>
#include <windows.h>
这里定义两个类ContextMenu和ContextMenuHelper
2.1 ContextMenu
该类定义了上下文菜单调用接口,定义如下:
class ContextMenu
{
public:
static void show(QStringList const& fileNames,
void *handle, QPoint const& p);
};
该类利用下面的辅助类ContextMenuHelper实现功能。
2.2 ContextMenuHelper
该类是一个辅助类型定义如下:
struct ContextMenuHelper
{
static ContextMenuHelper* Instatnce();//获取单例对象
~ContextMenuHelper();
void showContextMenu(QStringList const& fileNames, void* handle, int x, int y);
private:
ContextMenuHelper();
LPSHELLFOLDER getParentFolder(QString const& filePath, bool isRoot = false);
bool getChildParent(ShellItem::Ptr & item, QString &childPath, QString const& filePath);
LPSHELLFOLDER getSpecialFolder(int idFolder);
bool isRootPath(QString const& path);
QString strToString(LPITEMIDLIST pidl, STRRET *str);
QString toWindowsPath(QString const& linuxPath);
QString toLinuxPath(QString const& windowsPath);
IShellFolder* pDesktop = nullptr;
};
如上所示ContextMenuHelper是一个单例类,公共接口showContextMenu实现右键上下文菜单。
后面讲述两个类的实现。
3 实现
3.1 ContextMenu
该类实现比较简单,直接调用ContextMenuHelper的showContextMenu,如下所示。
void ContextMenu::show(QStringList const& fileNames,
void *handle, QPoint const& p)
{
ContextMenuHelper::Instatnce()->showContextMenu(fileNames, handle, p.x(), p.y());
}
3.2 ContextMenuHelper
重点是辅助类实现。
3.2.1 对象构建与销毁
ContextMenuHelper::ContextMenuHelper()
{
SHGetDesktopFolder(&pDesktop);
}
ContextMenuHelper::~ContextMenuHelper()
{
if(pDesktop)
pDesktop->Release();
}
对象的构造函数定义为private,没法直接实现构造,需要实现静态函数Instatnce来完成对象构建。
3.2.2 单例函数
ContextMenuHelper* Instatnce()
{
static ContextMenuHelper helper;
return &helper;
}
这样一个单例类构建完成。
3.2.3 showContextMenu
实现如下:
void ContextMenuHelper::showContextMenu(QStringList const& fileNames, void* handle, int x, int y)
{
if(fileNames.isEmpty())
return;
QString parentPath = toWindowsPath(QFileInfo(fileNames[0]).path());/*将Qt路径分割符/转换为\*/
LPSHELLFOLDER pParentFolder = getParentFolder(parentPath, isRootPath(fileNames[0]));
if(!pParentFolder)
return;
LPITEMIDLIST* pidlFiles = (LPITEMIDLIST *)malloc(sizeof(LPITEMIDLIST) * fileNames.size());
{
LPENUMIDLIST pEnum;
HRESULT hr = pParentFolder->EnumObjects(0, SHCONTF_FOLDERS | SHCONTF_NONFOLDERS, &pEnum);
if(SUCCEEDED(hr))
{
LPITEMIDLIST pidl;
STRRET str;
int index = 0;
while(pEnum->Next(1, &pidl, 0) == S_OK)
{
pParentFolder->GetDisplayNameOf(pidl, SHGDN_FORPARSING, &str);
QString fileName = toLinuxPath(strToString(pidl, &str));
if(!fileNames.contains(fileName))
CoTaskMemFree(pidl);
else
{
if(index < fileNames.size())
pidlFiles[index++] = pidl;
}
}
}
}
IContextMenu *pcm;
HRESULT hr = pParentFolder->GetUIObjectOf(0,
fileNames.size(), (PCUITEMID_CHILD_ARRAY)pidlFiles,
IID_IContextMenu, 0, (LPVOID*)&pcm);
if(SUCCEEDED(hr))
{
HMENU hPopup = CreatePopupMenu();
if(hPopup)
{
hr = pcm->QueryContextMenu(hPopup, 0, 1, 0x7fff, CMF_NORMAL);
if(SUCCEEDED(hr))
{
UINT idCmd = TrackPopupMenu(hPopup,
TPM_LEFTALIGN | TPM_RETURNCMD | TPM_RIGHTBUTTON,
x, y, 0, (HWND)handle, 0);
if(idCmd && (idCmd != (UINT)-1))
{
CMINVOKECOMMANDINFO cmi;
cmi.cbSize = sizeof(CMINVOKECOMMANDINFO);
cmi.fMask = 0;
cmi.hwnd = 0;
cmi.lpVerb = (LPCSTR)(INT_PTR)(idCmd - 1);
cmi.lpParameters = NULL;
cmi.lpDirectory = NULL;
cmi.nShow = SW_SHOWNORMAL;
cmi.dwHotKey = 0;
cmi.hIcon = NULL;
pcm->InvokeCommand(&cmi);
}
}
DestroyMenu(hPopup);
}
}
for(int i = 0; i < fileNames.size(); i++)
ILFree(pidlFiles[i]);
free(pidlFiles);
pParentFolder->Release();
}
该接口支持显示同一目录下的多为文件和目录,代码流程如下:
- 获取文件/目录的父文件夹对象pParentFolder(LPSHELLFOLDER).
- 由pParentFolder遍历出要对应目录的子对象数组pidlFiles(LPITEMIDLIST).
- pParentFolder获取pidlFiles的上下文菜单接口pcm(IContextMenu).
- 创建弹出菜单hPopup,并填充菜单项
- 弹出菜单后
- 释放分配资源
3.2.4 getParentFolder
获取文件路径filePath的父文件夹对象,实现如下:
LPSHELLFOLDER ContextMenuHelper::getParentFolder(QString const& filePath, bool isRoot = false)
{
LPSHELLFOLDER pDrives = getSpecialFolder(CSIDL_DRIVES);//获取我的电脑文件夹对象
ShellItem::Ptr item;
if(pDrives)
{
LPENUMIDLIST pEnum;
HRESULT hr = pDrives->EnumObjects(0, SHCONTF_FOLDERS, &pEnum);
if (SUCCEEDED(hr))
{
LPITEMIDLIST pidl;
STRRET str;
QString childPath;
while(pEnum->Next(1, &pidl, 0) == S_OK)
{
pDrives->GetDisplayNameOf(pidl, SHGDN_FORPARSING, &str);
childPath = strToString(pidl, &str);
if(isRootPath(childPath) && filePath.startsWith(childPath))//这里childPath是C:\\,D:\\,E:\\
{
item = ShellItem::Ptr(new ShellItem());
item->pidlRel = pidl;
pDrives->AddRef();
item->pParentFolder = pDrives;
break;
}
}
//递归找到filePath所在文件夹
while(true)
{
if(childPath == filePath)
break;
if(!getChildParent(item, childPath, filePath))
break;
}
pEnum->Release();
if(item)
{
if(isRoot)
return item->pParentFolder;
LPSHELLFOLDER pParentFolder = 0;
item->pParentFolder->BindToObject(item->pidlRel,
0, IID_IShellFolder, (LPVOID*)&pParentFolder);
return pParentFolder;
}
}
}
return 0;
}
3.2.5 getChildParent
在文件目录下查找文件filePath, 实现如下:
bool ContextMenuHelper::getChildParent(ShellItem::Ptr & item, QString &childPath, QString const& filePath)
{
if(!item)
return false;
LPSHELLFOLDER pParentFolder = NULL;
HRESULT hr = item->pParentFolder->BindToObject(item->pidlRel, 0, IID_IShellFolder,
(LPVOID*)&pParentFolder);
if(SUCCEEDED(hr))
{
LPENUMIDLIST pEnum;
hr = pParentFolder->EnumObjects(NULL, SHCONTF_FOLDERS, &pEnum);
if(SUCCEEDED(hr))
{
LPITEMIDLIST pidl;
DWORD dwFetched = 1;
STRRET str;
while(S_OK == (pEnum->Next(1, &pidl, &dwFetched)) && dwFetched)
{
pParentFolder->GetDisplayNameOf(pidl, SHGDN_FORPARSING, &str);
childPath = strToString(pidl, &str);
int index = filePath.indexOf(childPath);
if(index >= 0)
{
if(filePath == childPath || filePath.at(childPath.size()) == '\\')
{
ShellItem::Ptr newItem(new ShellItem());
newItem->pidlRel = pidl;
pParentFolder->AddRef();
newItem->pParentFolder = pParentFolder;
item = newItem;
pEnum->Release();
pParentFolder->Release();
return true;
}
}
}
pEnum->Release();
}
pParentFolder->Release();
}
return false;
}
3.2.6 getSpecialFolder
获取指定文件对应的LPSHELLFOLDER对象,在本文使用的是CSIDL_DRIVES(驱动器对象),就是我电脑对象。
LPSHELLFOLDER ContextMenuHelper::getSpecialFolder(int idFolder)
{
LPITEMIDLIST pidl;
LPSHELLFOLDER psf = NULL;
HRESULT hr = SHGetSpecialFolderLocation(0, idFolder, &pidl);
if (SUCCEEDED(hr))
{
pDesktop->BindToObject(pidl, NULL, IID_IShellFolder, (LPVOID *)&psf);
CoTaskMemFree(pidl);
}
return psf;
}
3.2.7 其它函数
其它函数实现如下:
bool ContextMenuHelper::isRootPath(QString const& path)
{
//for dirver D: D:/
//for virtual driver, the path's len is larger than 3
return path.size() > 1 && path.size() <= 3;
}
QString ContextMenuHelper::strToString(LPITEMIDLIST pidl, STRRET *str)
{
LPTSTR pszText;
QString text;
HRESULT hr = StrRetToStr(str, pidl, &pszText);
if (SUCCEEDED(hr))
{
text = QString::fromStdWString(pszText);
CoTaskMemFree(pszText);
}
return text;
}
QString ContextMenuHelper::toWindowsPath(QString const& linuxPath)
{
QString windowsPath = linuxPath;
windowsPath.replace("/", "\\");
return windowsPath;
}
QString ContextMenuHelper::toLinuxPath(QString const& windowsPath)
{
QString linuxPath = windowsPath;
linuxPath.replace("\\", "/");
return linuxPath;
}
4 使用
实现中用到HWND对象,需要在Qt中窗口对象中调用。
从QWidget派生一个类型Widget。
4.1 Widget定义
Widget定义如下:
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
private slots:
void customContextMenu(QPoint const& point);
private:
Ui::Widget *ui;
};
4.2 Widget实现
实现如下:
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
setContextMenuPolicy(Qt::CustomContextMenu);
connect(this, SIGNAL(customContextMenuRequested(QPoint)),
this, SLOT(customContextMenu(QPoint)));
}
Widget::~Widget()
{
delete ui;
}
void Widget::customContextMenu(QPoint const&)
{
//winId()返回的Windows的HWND类型窗口对象。
ContextMenu::show(QStringList() << "D:/", (void *)winId(), QCursor::pos());
}
如上所示在窗口中单击右键显示右键上下文菜单.