C++ Qt实现一个解除文件占用小工具

 更新时间:2023年09月18日 08:32:34   作者:庄周de蝴蝶  
这篇文章主要为大家详细介绍了如何利用C++ Qt实现一个解除文件占用小工具,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下

前言

相信大家或多或少都遇到过想删除一个文件,却提示被占用的情况:

不知道各位都是如何处理的,反正我一直都是用的火绒😄。但是作为一名程序员,自己写一个小程序实现多有意思,是吧。况且为了一个小工具去安装一个杀毒软件,不是一个合格的程序员,你们说对不对🤔。基于以上的原因,最终出现了这篇文章,效果如下,本文所对应的完整代码已上传到GitHub,可自行取用~~~

一些可以使用的工具

在正式编码之前,这里先介绍一些已有的工具,如果想看编码实现,可以跳过本节。

火绒等杀毒软件

这里以火绒自带的工具为例,使用方式如下所示:

通过火绒自带的工具,可以看到文件被什么程序占用了,然后进行解锁。

专用工具

UnlockerLockHunterIObit Unlocker,由于未实际使用过,这里不再展开介绍。

任务管理器

通过Windows 自带的任务管理器也可以查询文件的占用状态,缺点是无法只解锁文件,只能关闭占用的进程。

Sysinternals 下的 handle

Sysinternals 是 Windows 平台上使用的一个工具集合,可以监控系统的绝大部分文件,磁盘,网络,进程线程,模块,工具全集可以在微软官网进行下载,这里只讲解用于句柄操作的 Handle:

首先在官网进行下载,可以发现包含的文件很简单,exe 文件可以直接运行:

在这里我们选择其中的 handle64 即可,首先以管理员身份运行终端,然后运行以下命令:

handle64 "C:\Users\xxx\Desktop\demo.gif"

然后我们就可以看到上图所示的占用的程序进程号和对应的文件句柄,之后我们就可以运行以下命令去解除占用了,其中 1CE8 和 20392 分别是上述命令获取到的文件句柄和占用进程号:

handle64 -nobanner -c 1CE8 -y -p 20392

自己编码实现

以上讲解了一些解除文件占用的第三方功能,下面则开始步入正题,从零实现一个解除文件占用的小工具。

软硬件运行环境及工具

  • Windows11
  • Visual Studio 2022
  • Qt5.15.2/QML(用于展示简单结果文本,不了解 Qt 也没什么影响)
  • Inno Setup(用于创建程序的安装程序)

编码实现

首先说明以下程序的整体思路:程序初始判断是否有传参,如果无参说明程序是手动运行,执行添加注册表实现右键菜单包含解锁文件选项的逻辑。如果包含参数,说明程序是通过右键菜单运行的,根据传递的参数(即文件路径)执行相应的文件解锁操作。

以下不展示全部代码,完整代码可在前言中的GitHub查看,全部逻辑都在 main.cpp 中。

注册表功能实现

最终效果如下:

结合上图和以下代码即注释,相关代码不难理解,主要步骤如下:

1.添加名为unlockfile的注册键,包含两个键值,一个默认项解锁文件对应右键菜单显示的名称,一个Icon设置为应用程序的地址对应右键菜单显示的图标。

2.在unlockfile下添加名为command的子键,值是程序路径和 "%1"(对应传递的文件路径参数用于文件解锁操作)。

使用注册表时要特别注意文件编码,字符串类型转换的处理。

QVariant showInfo;
string appPath = QCoreApplication::applicationDirPath()
    .replace(QRegExp("/"), "\\").toStdString() + "\\unlockfile.exe";
if (setRightMenu("unlockfile", "解锁文件", appPath))
{
			showInfo = u8"注册表添加成功";
}
else
{
	showInfo = u8"注册表添加失败, 请确保以管理员身份运行";
}
QMetaObject::invokeMethod(root, "showInfo", Q_ARG(QVariant, showInfo));
/// <summary>
/// 设置右键菜单
/// </summary>
/// <param name="strRegKeyKey">注册键</param>
/// <param name="strRegKeyName">注册名</param>
/// <param name="strApplication">应用地址</param>
/// <returns>是否添加成功</returns>
bool setRightMenu(string strRegKeyKey, string strRegKeyName, string strApplication)
{
	HKEY hresult;
	string strRegKey = "*\\shell\\" + strRegKeyKey;
	string strRegSubkey = strRegKey + "\\command";
	string strApplicationValue = "\"" + strApplication +  "\"" + " \"%1\"";
	DWORD dwPos;
	// 创建注册表键, 对应右键菜单项
	if (RegCreateKeyEx(HKEY_CLASSES_ROOT, stringToWString(strRegKey.c_str()), 0,
		NULL, REG_OPTION_NON_VOLATILE, KEY_CREATE_SUB_KEY | KEY_ALL_ACCESS, NULL, &hresult, &dwPos) != ERROR_SUCCESS)
	{
		RegCloseKey(hresult);
		return false;
	}
	// 创建注册表值, 对应右键菜单项显示的内容
	if (RegSetValueEx(hresult, NULL, 0, REG_SZ, (BYTE*)stringToWString(strRegKeyName.c_str()), (wcslen(stringToWString(strApplicationValue.c_str())) + 1) * sizeof(wchar_t)) != ERROR_SUCCESS)
	{
		RegCloseKey(hresult);
		return false;
	}
	// 设置右键菜单图标
	if (RegSetValueEx(hresult, stringToWString("Icon"), 0, REG_SZ, (BYTE*)stringToWString(strApplication.c_str()), (wcslen(stringToWString(strApplication.c_str())) + 1) * sizeof(wchar_t)) != ERROR_SUCCESS)
	{
		RegCloseKey(hresult);
		return false;
	}
	// 创建注册表子项键, 对应点击右键菜单项后的命令项
	if (RegCreateKeyEx(HKEY_CLASSES_ROOT, stringToWString(strRegSubkey.c_str()), 0, NULL, REG_OPTION_NON_VOLATILE, KEY_CREATE_SUB_KEY | KEY_ALL_ACCESS, NULL, &hresult, &dwPos) != ERROR_SUCCESS)
	{
		RegCloseKey(hresult);
		return false;
	}
	// 创建注册表子项值, 对应点击右键菜单项后的具体执行命令
	if (RegSetValueEx(hresult, NULL, 0, REG_SZ, (BYTE*)stringToWString(strApplicationValue.c_str()), (wcslen(stringToWString(strApplicationValue.c_str())) + 1) * sizeof(wchar_t)) != ERROR_SUCCESS)
	{
		RegCloseKey(hresult);
		return false;
	}
	RegCloseKey(hresult);
	return true;
}

实现的效果如下,其中解锁文件就是我们创建的:

解锁文件逻辑实现

这部分逻辑稍微复杂一些,具体步骤如下:

  • 首先执行init()进行初始化的操作,包括加载 Native API 和遍历系统中所有句柄。
  • 调用getFileObjectTypeNumber()获取文件句柄对应的编号(句柄有很多种,比如窗口、文件、图标和菜单),经测试,不同系统版本的编号也有所不同:win11: 40 win10: 37 win7: 28
  • 遍历执行init()得到的系统所有句柄信息,只处理其中类型为文件且不属于系统进程的句柄。
  • 对符合条件的文件句柄去获取其文件名,如果文件名和传递的文件名相同,则关闭相应的句柄即可实现解锁文件的效果,同时获取占用的进程路径展示给用户。

特别注意,在 ring3 级调用NtQueryObject会出现阻塞的情况,因此需要通过开一个线程增加超时处理,避免程序卡住。此外,由于是跨进程处理句柄,因此需要调用DuplicateHandle方法。

/// <summary>
/// 查询对象信息
/// </summary>
/// <param name="lpParam">参数</param>
/// <returns>返回值</returns>
DWORD queryObj(LPVOID lpParam)
{
    return NtQueryObject(hCopy, 1, pObject, MAX_PATH * 2, NULL);
}
/// <summary>
/// 获取文件名
/// </summary>
/// <param name="hCopy">文件句柄</param>
/// <param name="hCopy">文件名</param>
void getFileName(string& fileName)
{
    // 查找句柄对象信息并分配内存进行保存
    pObject = (POBJECT_NAME_INFORMATION)HeapAlloc(GetProcessHeap(), 0, MAX_PATH * 2);
    if (pObject == 0)
    {
        HeapFree(GetProcessHeap(), 0, pObject);
        return;
    }
    // NtQueryObject 调用会出现阻塞, 启动线程增加超时处理
    HANDLE hThread = CreateThread(NULL, 0, queryObj, NULL, 0, NULL);
    if (hThread == 0)
    {
        HeapFree(GetProcessHeap(), 0, pObject);
        return;
    }
    DWORD dwSatus = WaitForSingleObject(hThread, 200);
    if (dwSatus == WAIT_TIMEOUT)
    {
        HeapFree(GetProcessHeap(), 0, pObject);
        return;
    }
    // 返回文件名
    if (pObject->NameBuffer != NULL)
    {
        DWORD n = WideCharToMultiByte(CP_OEMCP, NULL, pObject->NameBuffer, -1, NULL, 0, NULL, FALSE);
        char* name = new char[n + 1];
        memset(name, 0, n + 1);
        WideCharToMultiByte(CP_OEMCP, NULL, pObject->NameBuffer, -1, name, n, NULL, FALSE);
        fileName = name;
        delete[] name;
        HeapFree(GetProcessHeap(), 0, pObject);
        return;
    }
    HeapFree(GetProcessHeap(), 0, pObject);
    return;
}
/// <summary>
/// 初始化处理
/// </summary>
/// <returns>是否正常初始化</returns>
bool init()
{
    // 从 ntdll.dll 中加载 Native API: NtQuerySystemInformation 用于遍历获取系统信息
    HMODULE hNtDll = LoadLibrary(L"ntdll.dll");
    if (hNtDll == NULL)
    {
        return false;
    }
    NTQUERYSYSTEMINFOMATION NtQuerySystemInformation = (NTQUERYSYSTEMINFOMATION)GetProcAddress(hNtDll, "NtQuerySystemInformation");
    if (NtQuerySystemInformation == NULL)
    {
        return false;
    }
    // 用于获取操作系统中文件类型句柄对应的对象类型数字
    nulFileHandle = CreateFile(L"NUL", GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, 0);
    if (nulFileHandle == NULL)
    {
        return false;
    }
    // 从 ntdll.dll 中加载 Native API: NtQueryObject 用于获取句柄对象信息
    NtQueryObject = (PNtQueryObject)GetProcAddress(hNtDll, "NtQueryObject");
    // 查找所有的句柄信息并分配内存进行保存
    DWORD nSize = 4096;
    pHandleInfo = (PSYSTEM_HANDLE_INFORMATION)HeapAlloc(GetProcessHeap(), 0, nSize);
    while (NtQuerySystemInformation(SystemHandleInformation, pHandleInfo, nSize, NULL) == STATUS_INFO_LENGTH_MISMATCH)
    {
        HeapFree(GetProcessHeap(), 0, pHandleInfo);
        nSize += 4096;
        pHandleInfo = (PSYSTEM_HANDLE_INFORMATION)HeapAlloc(GetProcessHeap(), 0, nSize);
    }
    if (pHandleInfo == NULL)
    {
        return false;
    }
    return true;
}
/// <summary>
/// 获取文件类型对应的对象编号, 经测试 win11: 40 win10: 37 win7: 28, 默认返回 win11 下的编码
/// </summary>
/// <returns>文件类型对应的对象编号</returns>
int getFileObjectTypeNumber()
{
    // 遍历所有的句柄
    for (ULONG i = 0; i < pHandleInfo->NumberOfHandles; i++)
    {
        PSYSTEM_HANDLE pHandle = (PSYSTEM_HANDLE) & (pHandleInfo->HandleInfo[i]);
        if ((int)GetCurrentProcessId() == pHandle->ProcessId && pHandle->Handle == (USHORT)nulFileHandle)
        {
            return (int)pHandle->ObjectTypeNumber;
        }
    }
    return 40;
}
/// <summary>
/// 关闭文件
/// </summary>
/// <param name="closeFileName">关闭的文件名</param>
void closeFile(string& closeFileName)
{
    int fileObjectTypeNumber = getFileObjectTypeNumber();
    // 遍历所有的句柄
    for (ULONG i = 0; i < pHandleInfo->NumberOfHandles; i++)
    {
        PSYSTEM_HANDLE pHandle = (PSYSTEM_HANDLE) & (pHandleInfo->HandleInfo[i]);
        // 只处理类型为文件且不属于系统进程(id 为 4)的句柄
        if (pHandle->ObjectTypeNumber != fileObjectTypeNumber || pHandle->ProcessId == 4 || pHandle->Handle == 0)
        {
            continue;
        }
        // 打开句柄对应的进行并进行复制用于后续操作
        HANDLE hProcess = OpenProcess(PROCESS_DUP_HANDLE | PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, pHandle->ProcessId);
        if (hProcess == NULL)
        {
            continue;
        }
        hCopy = 0;
        if (!DuplicateHandle(hProcess, (HANDLE)pHandle->Handle, GetCurrentProcess(), &hCopy, MAXIMUM_ALLOWED, FALSE, 0))
        {
            continue;
        }
        // 根据句柄获取文件名
        int pid = pHandle->ProcessId;
        string fileName;
        getFileName(fileName);
        if (fileName.find(closeFileName) != -1)
        {
            // 获取占用的进程名称
            WCHAR tmpName[MAX_PATH] = {};
            DWORD size = MAX_PATH;
            QueryFullProcessImageName(hProcess, 0, tmpName, &size);
            wStringToString(processName, tmpName);
            // 关闭占用的文件句柄
            HANDLE h_tar = NULL;
            if (DuplicateHandle(hProcess, (HANDLE)pHandle->Handle, GetCurrentProcess(), &h_tar, 0, FALSE, DUPLICATE_SAME_ACCESS | DUPLICATE_CLOSE_SOURCE))
            {
                CloseHandle(h_tar);
            }
            CloseHandle(hCopy);
            CloseHandle(hProcess);
            return;
        }
        CloseHandle(hCopy);
        CloseHandle(hProcess);
    }
    HeapFree(GetProcessHeap(), 0, pHandleInfo);
    return;
}

界面展示实现

界面展示这里使用了 Qt 的 QML 进行实现,页面比较简单,包含以下两个界面。

主界面

主界面只是简单展示一下文本,其中文本会根据注册表添加成功或失败展示相应的信息(在注册表功能实现部分的代码开头可以看到)。

import QtQuick 2.9
import QtQuick.Window 2.2
Window {
    id: w
    visible: true
    width: 320
    height: 120
    title: "unlockfile"
    function showInfo(infoText) {
        info.text = infoText
    }
    Text {
        id: info
        anchors.fill: parent
        horizontalAlignment: Text.AlignHCenter
        verticalAlignment: Text.AlignVCenter
        text: "Enjoy!"
    }
}

解锁界面

解锁界面稍微复杂一些,通过 Timer 定时器实现动态的查找中...展示,在解锁文件完成后会通过showFile函数展示占用的进程名。

import QtQuick 2.9
import QtQuick.Window 2.2
Window {
    id: w
    visible: true
    width: 480
    height: 200
    title: "unlockfile"
    property bool run: true
    property int count: 0
    function showFile(fileText) {
        file.text = fileText
        run = false
    }
    Text {
        id: file
        anchors.fill: parent
        horizontalAlignment: Text.AlignHCenter
        verticalAlignment: Text.AlignVCenter
        text: "查找中"
    }
    Timer {
        interval: 1000
        running: run
        repeat: true
        onTriggered: {
            let str = ""
            for (let i = 0; i < count; i++) {
                str += "."
            }
            file.text = "查找中" + str
            count = (count + 1) % 4
        }
    }
}

其中设置进程名的代码操作在 main.cpp 文件中:

QThreadPool::globalInstance()->start([=]() {
	string fileName = gbkToUTF8(argv[1]).substr(3);
	if (init())
	{
		closeFile(fileName);
        string info = u8"解锁成功, 占用程序: " + processName;
        QMetaObject::invokeMethod(root, "showFile",
                                  Q_ARG(QVariant, QString::fromStdString(info)));
    }
});

制作安装程序

最后再介绍如何制作程序的安装程序,前提是需要先对 Qt 程序进行打包(此处省略 500 字),然后就可以使用Inno Setup工具进行制作了,步骤如下:

1.设置应用的名称版本:

2.设置应用的安装路径,同时允许用户进行自定义:

3.设置执行程序的路径和根文件夹路径:

4.之后全部点击下一步,然后在选择语言时按需选择:

5.然后可以设置程序的图标和安装程序输出路径,之后全部点击下一步即可:

6.然后就可以在输出路径看到生成的安装程序:

7.点击运行就是熟悉的程序安装界面了,按需进行选择后即可使用,同时需要以管理员身份运行:

安装程序也可以在GitHub中找到,目前只在 win10 和 win11 进行了测试。

总结

本文讲解了如何实现一个解除文件占用的小程序,不过还存在很多不完善的地方:

  • 注册表添加项无法自定义,同时未提供删除注册表的操作
  • 不是列出所有占用项让用户选择进行解锁
  • 只测试了 win10 和 win11 环境下的运行
  • 未实现批量解除文件占用的功能
  • ...

以上就是C++ Qt实现一个解除文件占用小工具的详细内容,更多关于C++ Qt解除文件占用的资料请关注脚本之家其它相关文章!

相关文章

  • C++-操作符重载、并实现复数类详解

    C++-操作符重载、并实现复数类详解

    这篇文章主要介绍了C++-操作符重载、并实现复数类,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-03-03
  • 浅谈C++函数声明后面加throw()的作用(必看)

    浅谈C++函数声明后面加throw()的作用(必看)

    下面小编就为大家带来一篇浅谈C++函数声明后面加throw()的作用(必看)。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-01-01
  • Cocos2d-x中获取系统时间和随机数实例

    Cocos2d-x中获取系统时间和随机数实例

    这篇文章主要介绍了Cocos2d-x中获取系统时间和随机数实例,本文代码含有大量注释来讲解获取系统时间和随机数的方法,需要的朋友可以参考下
    2014-09-09
  • C++中用new创建二维数组和指针数组实例代码

    C++中用new创建二维数组和指针数组实例代码

    这篇文章主要介绍了C++中用new创建二维数组和指针数组实例代码,非常不错,具有参考借鉴价值,需要的朋友参考下
    2017-03-03
  • 解析C++中不能重载为友元函数的四个运算符

    解析C++中不能重载为友元函数的四个运算符

    以下是对C++中不能重载为友元函数的四个运算符进行了详细的分析介绍,需要的朋友可以过来参考下
    2013-07-07
  • VSCode配置C语言环境的方法

    VSCode配置C语言环境的方法

    这篇文章主要介绍了VSCode配置C语言环境的方法,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-04-04
  • 简述C++中虚拟函数的内存分配机制

    简述C++中虚拟函数的内存分配机制

    这篇文章主要介绍了简述C++中虚拟函数的内存分配机制,帮助大家更好的理解和学习c++,感兴趣的朋友可以了解下
    2020-08-08
  • c++禁止函数的传值调用的方法

    c++禁止函数的传值调用的方法

    这篇文章主要介绍了c++禁止函数的传值调用的方法,帮助大家更好的理解和学习,感兴趣的朋友可以了解下
    2020-08-08
  • 关于C++一些特性的探究

    关于C++一些特性的探究

    下面小编就为大家带来一篇关于C++一些特性的探究。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2016-06-06
  • 详解C++语言中std::array的神奇用法

    详解C++语言中std::array的神奇用法

    本文的代码都在C++17环境下编译运行。当前主流的g++版本已经能支持C++17标准,但是很多版本(如gcc 7.3)的C++17特性不是默认打开的,需要手工添加编译选项-std=c++17,具体内容详情跟随小编一起学习吧
    2021-05-05

最新评论