目录
1 概述
本文描述了在Qt下利用Windows API实现U盘及光盘弹出操作。
2 实现
通过查资料U盘弹出操作需要使用Windows API函数CM_Request_Device_Eject,该函数原型为:
CMAPI CONFIGRET CM_Request_Device_Eject(
[in] DEVINST dnDevInst,
[out, optional] PPNP_VETO_TYPE pVetoType,
[out, optional] LPWSTR pszVetoName,
[in] ULONG ulNameLength,
[in] ULONG ulFlags
);
实现关键是如何将U盘的驱动器盘符转化为DEVINST类型设备句柄。
需要引用的Windows API头文件
#include <windows.h>
#include <winioctl.h>
#include <setupapi.h>
#include <initguid.h>
#include <newdev.h>
#include <cfgmgr32.h>
#include <regstr.h>
#include <combaseapi.h>
#include <shlobj.h>
2.1 函数定义
定义下面5个函数实现磁盘弹出:
- GetDiskNumber 通过驱动器名称返回磁盘号
- GetDriverType 通过驱动器名称返回类型
- GetDrivesDevInstByDiskNumber 通过驱动器号和类型返回磁盘设备号
- IsRemovable 通过设备号判断磁盘是否可移出
- RemoveDisk 通过驱动器号和类型弹出磁盘
- EjectDisk 通过驱动器名称弹出磁盘
2.2 函数实现
2.2.1 GetDiskNumber
long GetDiskNumber(QString const& dirverPath, bool isDevicePath = false)
{
long diskNumber = -1;
QString volumeAccessPath = isDevicePath ? dirverPath : QString("\\\\.\\%1").arg(dirverPath.toUpper());
HANDLE hVolume = CreateFile(volumeAccessPath.toStdWString().c_str(), 0,
FILE_SHARE_READ | FILE_SHARE_WRITE, 0, OPEN_EXISTING, 0, 0);
if(hVolume == INVALID_HANDLE_VALUE)
return diskNumber;
STORAGE_DEVICE_NUMBER sdn;
DWORD dwBytesReturned = 0;
long res = DeviceIoControl(hVolume, IOCTL_STORAGE_GET_DEVICE_NUMBER,
nullptr, 0, &sdn, sizeof(sdn), &dwBytesReturned, nullptr);
if (res)
diskNumber = sdn.DeviceNumber;
CloseHandle(hVolume);
return diskNumber;
}
该函数通过CreateFile打开一个文件句柄,在这个文件句柄通过DeviceIoControl获取设备号。传入参数dirverPath是驱动器名称,例如D:
2.2.2 GetDriverType
UINT GetDriverType(QString const& dirverPath)
{
QString rootPath = QString("%1\\").arg(dirverPath.toUpper());
return GetDriveType(rootPath.toStdWString().c_str());
}
该函数调用GetDriveType返回设备类型,传入参数dirverPath是驱动器名称,例如D:
2.2.3 GetDrivesDevInstByDiskNumber
DEFINE_GUID(GUID_DEVINTERFACE_DISK, 0x53f56307L, 0xb6bf, 0x11d0, 0x94, 0xf2, 0x00, 0xa0, 0xc9, 0x1e, 0xfb, 0x8b );
DEFINE_GUID(GUID_DEVINTERFACE_CDROM, 0x53f56308L, 0xb6bf, 0x11d0, 0x94, 0xf2, 0x00, 0xa0, 0xc9, 0x1e, 0xfb, 0x8b );
#define MAX_BUF_SIZE 1024
DEVINST GetDrivesDevInstByDiskNumber(long diskNumber, long driverType)
{
if(driverType != DRIVE_REMOVABLE
&& driverType != DRIVE_FIXED
&& driverType != DRIVE_CDROM)
return 0;
const GUID* guid;
if(driverType == DRIVE_CDROM)
guid = &GUID_DEVINTERFACE_CDROM;
else
guid = &GUID_DEVINTERFACE_DISK;
HDEVINFO hDevInfo = SetupDiGetClassDevs(guid, nullptr, nullptr, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE);
if(hDevInfo == INVALID_HANDLE_VALUE)
return 0;
DWORD dwIndex = 0;
SP_DEVICE_INTERFACE_DATA devInterfaceData = {};
devInterfaceData.cbSize = sizeof(SP_DEVICE_INTERFACE_DATA);
while(true)
{
BOOL bRet = SetupDiEnumDeviceInterfaces(hDevInfo, nullptr, guid, dwIndex, &devInterfaceData);
if(!bRet)
break;
SP_DEVICE_INTERFACE_DATA spdid;
DWORD dwSize;
spdid.cbSize = sizeof(spdid);
SetupDiEnumInterfaceDevice(hDevInfo, nullptr, guid, dwIndex, &spdid);
SetupDiGetDeviceInterfaceDetail(hDevInfo, &spdid, nullptr, 0, &dwSize, nullptr);
BYTE buf[MAX_BUF_SIZE];
PSP_DEVICE_INTERFACE_DETAIL_DATA pspdidd = (PSP_DEVICE_INTERFACE_DETAIL_DATA)buf;
if(dwSize != 0 && dwSize <= sizeof(buf)) {
pspdidd->cbSize = sizeof(*pspdidd);
SP_DEVINFO_DATA spdd;
ZeroMemory((PVOID)&spdd, sizeof(spdd));
spdd.cbSize = sizeof(spdd);
long res = SetupDiGetDeviceInterfaceDetail(hDevInfo, &spdid, pspdidd, dwSize, &dwSize, &spdd);
if(res)
{
if(GetDiskNumber(QString::fromStdWString(pspdidd->DevicePath), true) == diskNumber)
{
SetupDiDestroyDeviceInfoList(hDevInfo);
return spdd.DevInst;
}
}
}
dwIndex++;
}
SetupDiDestroyDeviceInfoList(hDevInfo);
return 0;
}
该函数流程如下:
- 通过SetupDiGetClassDevs获取设备信息hDevInfo
- 通过SetupDiEnumDeviceInterfaces和hDevInfo遍历磁盘设备
- 通过SetupDiGetDeviceInterfaceDetail获取设备信息pspdidd和spdd
- 通过GetDiskNumber和pspdidd->DevicePath获取设备号并与传入的diskNumber比较.
- 如果匹配到返回spdd.DevInst
2.2.4 IsRemovable
bool IsRemovable(DEVINST devInst)
{
ULONG status = 0;
ULONG problemNumber = 0;
long res = CM_Get_DevNode_Status(&status, &problemNumber, devInst, 0);
return (res == CR_SUCCESS && (status & DN_REMOVABLE) != 0);
}
该函通过CM_Get_DevNode_Status和设备ID返回设备是否是可移出的
2.2.5 RemoveDisk
#define DELAY_TIME 200 //ms
bool RemoveDisk(long diskNumber, long driverType, int tryCount = 3)
{
if(diskNumber < 0)
return false;
DEVINST devInst = GetDrivesDevInstByDiskNumber(diskNumber, driverType);
if(!devInst)
return false;
long res = CM_Get_Parent(&devInst, devInst, 0);// disk's parent, e.g the usb device, sata port
if(res != CR_SUCCESS)
return false;
bool isRemovable = IsRemovable(devInst);
WCHAR vetoName[MAX_PATH];
PNP_VETO_TYPE vetoType = PNP_VetoTypeUnknown;
for(int i = 0; i < tryCount; i++)
{
vetoName[0] = 0;
if(isRemovable)
res = CM_Request_Device_Eject(devInst, &vetoType, vetoName, sizeof(vetoName), 0);
else
res = CM_Query_And_Remove_SubTree(devInst, &vetoType, vetoName, sizeof(vetoName), 0);
bool bSuccess = (res == CR_SUCCESS && vetoType == PNP_VetoTypeUnknown);
if(bSuccess)
return true;
Sleep(DELAY_TIME);
}
return false;
}
该函数流程如下:
- 根据diskNumber和diskNumber返回设备号devInst
- 返回devInst设备的父设备号,这里设备号是磁盘逻辑设备号,其父设备号是USB或SATA物理设备,弹出设备需要物理设备号。
- 最后调用CM_Request_Device_Eject弹出设备,默认重试3次。
2.2.6 EjectDisk
bool EjectDisk(QString const& dirverPath)
{
long dirverType(GetDriverType(dirverPath));
long diskNumber(GetDiskNumber(dirverPath));
return RemoveDisk(diskNumber, dirverType);
}
3 使用
EjectDisk("F:");
4 总结
函数EjectDisk是阻塞调用,如果U盘正在占用情况下该函数调用花费时间比较长,解决方案是将EjectDisk放入到线程中调用,后续文章Qt线程另一种使用方式会讲述如何将阻塞调用放入线程中。