修改 7-Zip 的文件夹选择对话框
背景
7-Zip 在解压文件时使用的是旧版的文件夹选择对话框 SHBrowseForFolder
,需要逐级展开树状目录,使用起来不是很好。
于是就想在不改动 7-Zip 源程序的情况下,把对话框给换成现代的 IFileDialog
。

旧版对话框 SHBrowseForFolder

新版对话框 IFileDialog
以 7-Zip 24.09 源码为例。
结构初始化部分的代码在 CPP\Windows\Shell.cpp#666
附近,整理后大概如下:
bool BrowseForFolder(LPBROWSEINFO browseInfo, CSysString &resultPath)
{
resultPath.Empty();
NWindows::NCOM::CComInitializer comInitializer;
LPITEMIDLIST itemIDList = ::SHBrowseForFolder(browseInfo);
if (!itemIDList) return false;
CItemIDList itemIDListHolder;
itemIDListHolder.Attach(itemIDList);
return GetPathFromIDList(itemIDList, resultPath);
}
static int CALLBACK BrowseCallbackProc(HWND hwnd, UINT uMsg, LPARAM /* lp */, LPARAM data)
{
switch (uMsg)
{
case BFFM_INITIALIZED:
{
SendMessage(hwnd, BFFM_SETSELECTION, TRUE, data);
break;
}
default:
break;
}
return 0;
}
static bool BrowseForFolder(HWND owner, LPCTSTR title, UINT ulFlags, LPCTSTR initialFolder, CSysString &resultPath)
{
CSysString displayName;
BROWSEINFO browseInfo;
browseInfo.hwndOwner = owner;
browseInfo.pidlRoot = NULL;
browseInfo.pszDisplayName = displayName.GetBuf(MAX_PATH);
browseInfo.lpszTitle = title;
browseInfo.ulFlags = ulFlags;
browseInfo.lpfn = initialFolder ? BrowseCallbackProc : NULL;
browseInfo.lParam = (LPARAM)initialFolder;
return BrowseForFolder(&browseInfo, resultPath);
}
除了要把 SHBrowseForFolder
detour 掉以外,还需要检查下 bi
的回调函数,如果存在的话说明 bi.lParam
里放了当前文件夹的路径。
DLL Proxy :不可行
7z 的提取文件对话框由 7zG.exe 提供,检查了下导入表,发现都是 known dll ,没有什么操作空间。
COMCTL32.dll
comdlg32.dll
GDI32.dll
OLEAUT32.dll
ole32.dll
USER32.dll
ADVAPI32.dll
SHELL32.dll
msvcrt.dll
KERNEL32.dll
7z 插件方法:可行
7zG 会尝试从其目录下的 Codecs
和 Formats
下加载 dll 。
(如果要 patch 7zFM 则麻烦些,因为它只在打开压缩文件时,才会去加载插件。)
因此尝试写一个 dll 去 detour 一下。
static LPITEMIDLIST(WINAPI *Original_SHBrowseForFolderW)(LPBROWSEINFOW lpbi) =
SHBrowseForFolderW;
LPITEMIDLIST WINAPI Detoured_SHBrowseForFolderW(LPBROWSEINFOW lpbi) {
MessageBoxW(NULL, L"Hello", L"World", 0);
return Original_SHBrowseForFolderW(lpbi);
}
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call,
LPVOID lpReserved) {
if (DetourIsHelperProcess()) {
return TRUE;
}
switch (ul_reason_for_call) {
case DLL_PROCESS_ATTACH:
DetourRestoreAfterWith();
DetourTransactionBegin();
DetourUpdateThread(GetCurrentThread());
DetourAttach(&(PVOID &)Original_SHBrowseForFolderW,
(PVOID)Detoured_SHBrowseForFolderW);
DetourTransactionCommit();
break;
}
return TRUE;
}
编译运行,报 access violation 退出了。 仔细一看, 7z 如果发现插件 dll 里没有可用的 codec/format 就会主动卸载 dll 。 因此在 dll 加载时,我们手动 pin 一下自己,防止被卸载。
完整代码如下:
#define WIN32_LEAN_AND_MEAN
#define UNICODE
#define _UNICODE
// clang-format off
#include <windows.h>
#include <detours.h>
#include <shlobj_core.h>
#include <comdef.h>
#include <pathcch.h>
#include <stdexcept>
// clang-format on
// mini unsafe com wrapper
template <typename T>
struct com_ptr {
T *ptr_{nullptr};
com_ptr() = default;
~com_ptr() {
if (ptr_) {
ptr_->Release();
}
}
T *get() const { return ptr_; }
T *operator->() { return ptr_; }
T **operator&() { return &ptr_; }
};
inline void check_hr(HRESULT hr) {
if (FAILED(hr)) {
_com_error err(hr);
#ifndef NDEBUG
MessageBoxW(NULL, err.ErrorMessage(), L"Unexpected error",
MB_ICONERROR | MB_OK);
#endif
throw std::runtime_error("unexpected error");
}
}
static LPITEMIDLIST(WINAPI *Original_SHBrowseForFolderW)(LPBROWSEINFOW lpbi) =
SHBrowseForFolderW;
bool TryGetAvailableFolder(LPCWSTR pszPath, IShellItem **ppsi) {
WCHAR szPath[MAX_PATH];
wcscpy_s(szPath, MAX_PATH, pszPath);
// try to parse current path
auto hr = SHCreateItemFromParsingName(szPath, NULL, IID_PPV_ARGS(ppsi));
if (SUCCEEDED(hr)) {
return true;
}
// try to remove trailing backslash
PathCchRemoveBackslash(szPath, MAX_PATH);
// try to get the parent folder
hr = PathCchRemoveFileSpec(szPath, MAX_PATH);
if (FAILED(hr)) {
return false;
}
hr = SHCreateItemFromParsingName(szPath, NULL, IID_PPV_ARGS(ppsi));
if (SUCCEEDED(hr)) {
return true;
}
return false;
}
LPITEMIDLIST ModernFolderDialog(LPBROWSEINFOW lpbi) {
com_ptr<IFileDialog> pfd;
check_hr(CoCreateInstance(CLSID_FileOpenDialog, NULL, CLSCTX_INPROC_SERVER,
IID_PPV_ARGS(&pfd)));
DWORD dwFlags;
check_hr(pfd->GetOptions(&dwFlags));
dwFlags |= FOS_PICKFOLDERS | FOS_FORCEFILESYSTEM;
check_hr(pfd->SetOptions(dwFlags));
check_hr(pfd->SetTitle(lpbi->lpszTitle));
if (lpbi->lpfn) {
// lParam is LPCWSTR initialFolder
com_ptr<IShellItem> psiFolder;
if (TryGetAvailableFolder((LPCWSTR)lpbi->lParam, &psiFolder)) {
check_hr(pfd->SetFolder(psiFolder.get()));
}
}
auto hr = pfd->Show(lpbi->hwndOwner);
if (hr == HRESULT_FROM_WIN32(ERROR_CANCELLED)) {
return NULL;
}
check_hr(hr);
com_ptr<IShellItem> psi;
check_hr(pfd->GetResult(&psi));
LPITEMIDLIST pidl = NULL;
check_hr(SHGetIDListFromObject(psi.get(), &pidl));
return pidl;
}
LPITEMIDLIST WINAPI Detoured_SHBrowseForFolderW(LPBROWSEINFOW lpbi) {
try {
return ModernFolderDialog(lpbi);
} catch (...) {
return Original_SHBrowseForFolderW(lpbi);
}
}
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call,
LPVOID lpReserved) {
if (DetourIsHelperProcess()) {
return TRUE;
}
switch (ul_reason_for_call) {
case DLL_PROCESS_ATTACH:
// avoid 7zip unloading the DLL
WCHAR moduleName[MAX_PATH];
GetModuleFileNameW(hModule, moduleName, MAX_PATH);
HMODULE hModuleTmp;
GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_PIN, moduleName,
&hModuleTmp);
DetourRestoreAfterWith();
DetourTransactionBegin();
DetourUpdateThread(GetCurrentThread());
DetourAttach(&(PVOID &)Original_SHBrowseForFolderW,
(PVOID)Detoured_SHBrowseForFolderW);
DetourTransactionCommit();
break;
}
return TRUE;
}