C++?Qt實(shí)現(xiàn)一個(gè)解除文件占用小工具
前言
相信大家或多或少都遇到過想刪除一個(gè)文件,卻提示被占用的情況:
不知道各位都是如何處理的,反正我一直都是用的火絨??。但是作為一名程序員,自己寫一個(gè)小程序?qū)崿F(xiàn)多有意思,是吧。況且為了一個(gè)小工具去安裝一個(gè)殺毒軟件,不是一個(gè)合格的程序員,你們說對(duì)不對(duì)???;谝陨系脑?,最終出現(xiàn)了這篇文章,效果如下,本文所對(duì)應(yīng)的完整代碼已上傳到GitHub,可自行取用~~~
一些可以使用的工具
在正式編碼之前,這里先介紹一些已有的工具,如果想看編碼實(shí)現(xiàn),可以跳過本節(jié)。
火絨等殺毒軟件
這里以火絨自帶的工具為例,使用方式如下所示:
通過火絨自帶的工具,可以看到文件被什么程序占用了,然后進(jìn)行解鎖。
專用工具
Unlocker
、LockHunter
、IObit Unlocker
,由于未實(shí)際使用過,這里不再展開介紹。
任務(wù)管理器
通過Windows 自帶的任務(wù)管理器也可以查詢文件的占用狀態(tài),缺點(diǎn)是無法只解鎖文件,只能關(guān)閉占用的進(jìn)程。
Sysinternals 下的 handle
Sysinternals 是 Windows 平臺(tái)上使用的一個(gè)工具集合,可以監(jiān)控系統(tǒng)的絕大部分文件,磁盤,網(wǎng)絡(luò),進(jìn)程線程,模塊,工具全集可以在微軟官網(wǎng)進(jìn)行下載,這里只講解用于句柄操作的 Handle:
首先在官網(wǎng)進(jìn)行下載,可以發(fā)現(xiàn)包含的文件很簡(jiǎn)單,exe 文件可以直接運(yùn)行:
在這里我們選擇其中的 handle64 即可,首先以管理員身份運(yùn)行終端,然后運(yùn)行以下命令:
handle64 "C:\Users\xxx\Desktop\demo.gif"
然后我們就可以看到上圖所示的占用的程序進(jìn)程號(hào)和對(duì)應(yīng)的文件句柄,之后我們就可以運(yùn)行以下命令去解除占用了,其中 1CE8 和 20392 分別是上述命令獲取到的文件句柄和占用進(jìn)程號(hào):
handle64 -nobanner -c 1CE8 -y -p 20392
自己編碼實(shí)現(xiàn)
以上講解了一些解除文件占用的第三方功能,下面則開始步入正題,從零實(shí)現(xiàn)一個(gè)解除文件占用的小工具。
軟硬件運(yùn)行環(huán)境及工具
- Windows11
- Visual Studio 2022
- Qt5.15.2/QML(用于展示簡(jiǎn)單結(jié)果文本,不了解 Qt 也沒什么影響)
- Inno Setup(用于創(chuàng)建程序的安裝程序)
編碼實(shí)現(xiàn)
首先說明以下程序的整體思路:程序初始判斷是否有傳參,如果無參說明程序是手動(dòng)運(yùn)行,執(zhí)行添加注冊(cè)表實(shí)現(xiàn)右鍵菜單包含解鎖文件
選項(xiàng)的邏輯。如果包含參數(shù),說明程序是通過右鍵菜單運(yùn)行的,根據(jù)傳遞的參數(shù)(即文件路徑)執(zhí)行相應(yīng)的文件解鎖操作。
以下不展示全部代碼,完整代碼可在前言
中的GitHub查看,全部邏輯都在 main.cpp 中。
注冊(cè)表功能實(shí)現(xiàn)
最終效果如下:
結(jié)合上圖和以下代碼即注釋,相關(guān)代碼不難理解,主要步驟如下:
1.添加名為unlockfile
的注冊(cè)鍵,包含兩個(gè)鍵值,一個(gè)默認(rèn)項(xiàng)解鎖文件
對(duì)應(yīng)右鍵菜單顯示的名稱,一個(gè)Icon
設(shè)置為應(yīng)用程序的地址對(duì)應(yīng)右鍵菜單顯示的圖標(biāo)。
2.在unlockfile
下添加名為command
的子鍵,值是程序路徑和 "%1"(對(duì)應(yīng)傳遞的文件路徑參數(shù)用于文件解鎖操作)。
使用注冊(cè)表時(shí)要特別注意文件編碼,字符串類型轉(zhuǎn)換的處理。
QVariant showInfo; string appPath = QCoreApplication::applicationDirPath() .replace(QRegExp("/"), "\\").toStdString() + "\\unlockfile.exe"; if (setRightMenu("unlockfile", "解鎖文件", appPath)) { showInfo = u8"注冊(cè)表添加成功"; } else { showInfo = u8"注冊(cè)表添加失敗, 請(qǐng)確保以管理員身份運(yùn)行"; } QMetaObject::invokeMethod(root, "showInfo", Q_ARG(QVariant, showInfo)); /// <summary> /// 設(shè)置右鍵菜單 /// </summary> /// <param name="strRegKeyKey">注冊(cè)鍵</param> /// <param name="strRegKeyName">注冊(cè)名</param> /// <param name="strApplication">應(yīng)用地址</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; // 創(chuàng)建注冊(cè)表鍵, 對(duì)應(yīng)右鍵菜單項(xiàng) 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; } // 創(chuàng)建注冊(cè)表值, 對(duì)應(yīng)右鍵菜單項(xiàng)顯示的內(nèi)容 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; } // 設(shè)置右鍵菜單圖標(biāo) 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; } // 創(chuàng)建注冊(cè)表子項(xiàng)鍵, 對(duì)應(yīng)點(diǎn)擊右鍵菜單項(xiàng)后的命令項(xiàng) 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; } // 創(chuàng)建注冊(cè)表子項(xiàng)值, 對(duì)應(yīng)點(diǎn)擊右鍵菜單項(xiàng)后的具體執(zhí)行命令 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; }
實(shí)現(xiàn)的效果如下,其中解鎖文件就是我們創(chuàng)建的:
解鎖文件邏輯實(shí)現(xiàn)
這部分邏輯稍微復(fù)雜一些,具體步驟如下:
- 首先執(zhí)行
init()
進(jìn)行初始化的操作,包括加載 Native API 和遍歷系統(tǒng)中所有句柄。 - 調(diào)用
getFileObjectTypeNumber()
獲取文件句柄對(duì)應(yīng)的編號(hào)(句柄有很多種,比如窗口、文件、圖標(biāo)和菜單),經(jīng)測(cè)試,不同系統(tǒng)版本的編號(hào)也有所不同:win11: 40 win10: 37 win7: 28
。 - 遍歷執(zhí)行
init()
得到的系統(tǒng)所有句柄信息,只處理其中類型為文件且不屬于系統(tǒng)進(jìn)程的句柄。 - 對(duì)符合條件的文件句柄去獲取其文件名,如果文件名和傳遞的文件名相同,則關(guān)閉相應(yīng)的句柄即可實(shí)現(xiàn)解鎖文件的效果,同時(shí)獲取占用的進(jìn)程路徑展示給用戶。
特別注意,在 ring3 級(jí)調(diào)用NtQueryObject
會(huì)出現(xiàn)阻塞的情況,因此需要通過開一個(gè)線程增加超時(shí)處理,避免程序卡住。此外,由于是跨進(jìn)程處理句柄,因此需要調(diào)用DuplicateHandle
方法。
/// <summary> /// 查詢對(duì)象信息 /// </summary> /// <param name="lpParam">參數(shù)</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) { // 查找句柄對(duì)象信息并分配內(nèi)存進(jìn)行保存 pObject = (POBJECT_NAME_INFORMATION)HeapAlloc(GetProcessHeap(), 0, MAX_PATH * 2); if (pObject == 0) { HeapFree(GetProcessHeap(), 0, pObject); return; } // NtQueryObject 調(diào)用會(huì)出現(xiàn)阻塞, 啟動(dòng)線程增加超時(shí)處理 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 用于遍歷獲取系統(tǒng)信息 HMODULE hNtDll = LoadLibrary(L"ntdll.dll"); if (hNtDll == NULL) { return false; } NTQUERYSYSTEMINFOMATION NtQuerySystemInformation = (NTQUERYSYSTEMINFOMATION)GetProcAddress(hNtDll, "NtQuerySystemInformation"); if (NtQuerySystemInformation == NULL) { return false; } // 用于獲取操作系統(tǒng)中文件類型句柄對(duì)應(yīng)的對(duì)象類型數(shù)字 nulFileHandle = CreateFile(L"NUL", GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, 0); if (nulFileHandle == NULL) { return false; } // 從 ntdll.dll 中加載 Native API: NtQueryObject 用于獲取句柄對(duì)象信息 NtQueryObject = (PNtQueryObject)GetProcAddress(hNtDll, "NtQueryObject"); // 查找所有的句柄信息并分配內(nèi)存進(jìn)行保存 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> /// 獲取文件類型對(duì)應(yīng)的對(duì)象編號(hào), 經(jīng)測(cè)試 win11: 40 win10: 37 win7: 28, 默認(rèn)返回 win11 下的編碼 /// </summary> /// <returns>文件類型對(duì)應(yīng)的對(duì)象編號(hào)</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> /// 關(guān)閉文件 /// </summary> /// <param name="closeFileName">關(guān)閉的文件名</param> void closeFile(string& closeFileName) { int fileObjectTypeNumber = getFileObjectTypeNumber(); // 遍歷所有的句柄 for (ULONG i = 0; i < pHandleInfo->NumberOfHandles; i++) { PSYSTEM_HANDLE pHandle = (PSYSTEM_HANDLE) & (pHandleInfo->HandleInfo[i]); // 只處理類型為文件且不屬于系統(tǒng)進(jìn)程(id 為 4)的句柄 if (pHandle->ObjectTypeNumber != fileObjectTypeNumber || pHandle->ProcessId == 4 || pHandle->Handle == 0) { continue; } // 打開句柄對(duì)應(yīng)的進(jìn)行并進(jìn)行復(fù)制用于后續(xù)操作 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; } // 根據(jù)句柄獲取文件名 int pid = pHandle->ProcessId; string fileName; getFileName(fileName); if (fileName.find(closeFileName) != -1) { // 獲取占用的進(jìn)程名稱 WCHAR tmpName[MAX_PATH] = {}; DWORD size = MAX_PATH; QueryFullProcessImageName(hProcess, 0, tmpName, &size); wStringToString(processName, tmpName); // 關(guān)閉占用的文件句柄 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; }
界面展示實(shí)現(xiàn)
界面展示這里使用了 Qt 的 QML 進(jìn)行實(shí)現(xiàn),頁面比較簡(jiǎn)單,包含以下兩個(gè)界面。
主界面
主界面只是簡(jiǎn)單展示一下文本,其中文本會(huì)根據(jù)注冊(cè)表添加成功或失敗展示相應(yīng)的信息(在注冊(cè)表功能實(shí)現(xiàn)
部分的代碼開頭可以看到)。
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!" } }
解鎖界面
解鎖界面稍微復(fù)雜一些,通過 Timer 定時(shí)器實(shí)現(xiàn)動(dòng)態(tài)的查找中...
展示,在解鎖文件完成后會(huì)通過showFile
函數(shù)展示占用的進(jìn)程名。
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 } } }
其中設(shè)置進(jìn)程名的代碼操作在 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))); } });
制作安裝程序
最后再介紹如何制作程序的安裝程序,前提是需要先對(duì) Qt 程序進(jìn)行打包(此處省略 500 字),然后就可以使用Inno Setup
工具進(jìn)行制作了,步驟如下:
1.設(shè)置應(yīng)用的名稱版本:
2.設(shè)置應(yīng)用的安裝路徑,同時(shí)允許用戶進(jìn)行自定義:
3.設(shè)置執(zhí)行程序的路徑和根文件夾路徑:
4.之后全部點(diǎn)擊下一步,然后在選擇語言時(shí)按需選擇:
5.然后可以設(shè)置程序的圖標(biāo)和安裝程序輸出路徑,之后全部點(diǎn)擊下一步即可:
6.然后就可以在輸出路徑看到生成的安裝程序:
7.點(diǎn)擊運(yùn)行就是熟悉的程序安裝界面了,按需進(jìn)行選擇后即可使用,同時(shí)需要以管理員身份運(yùn)行:
安裝程序也可以在GitHub中找到,目前只在 win10 和 win11 進(jìn)行了測(cè)試。
總結(jié)
本文講解了如何實(shí)現(xiàn)一個(gè)解除文件占用的小程序,不過還存在很多不完善的地方:
- 注冊(cè)表添加項(xiàng)無法自定義,同時(shí)未提供刪除注冊(cè)表的操作
- 不是列出所有占用項(xiàng)讓用戶選擇進(jìn)行解鎖
- 只測(cè)試了 win10 和 win11 環(huán)境下的運(yùn)行
- 未實(shí)現(xiàn)批量解除文件占用的功能
- ...
以上就是C++ Qt實(shí)現(xiàn)一個(gè)解除文件占用小工具的詳細(xì)內(nèi)容,更多關(guān)于C++ Qt解除文件占用的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
C/C++ Qt 自定義Dialog對(duì)話框組件應(yīng)用案例詳解
有時(shí)候我們需要一次性修改多個(gè)數(shù)據(jù),使用默認(rèn)的模態(tài)對(duì)話框似乎不太夠用,此時(shí)我們需要自己創(chuàng)建一個(gè)自定義對(duì)話框。這篇文章主要介紹了Qt自定義Dialog對(duì)話框組件的應(yīng)用,感興趣的同學(xué)可以學(xué)習(xí)一下2021-11-11stringstream操縱string的方法總結(jié)
下面小編就為大家?guī)硪黄猻tringstream操縱string的方法總結(jié)。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2016-12-12C++實(shí)現(xiàn)棧的操作(push和pop)
這篇文章主要介紹了C++實(shí)現(xiàn)棧的操作(push和pop),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-07-07C++ 自由存儲(chǔ)區(qū)是否等價(jià)于堆你知道嗎
自由存儲(chǔ)是C++中通過new與delete動(dòng)態(tài)分配和釋放對(duì)象的抽象概念,而堆(heap)是C語言和操作系統(tǒng)的術(shù)語,是操作系統(tǒng)維護(hù)的一塊動(dòng)態(tài)分配內(nèi)存2021-08-08c/c++獲取系統(tǒng)時(shí)間函數(shù)的方法示例
這篇文章主要介紹了c/c++獲取系統(tǒng)時(shí)間函數(shù)的方法示例,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-02-02C++中關(guān)于多態(tài)實(shí)現(xiàn)和使用方法
這篇文章主要介紹了C++中關(guān)于多態(tài)實(shí)現(xiàn)和使用方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-07-07