修改 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 会尝试从其目录下的 CodecsFormats 下加载 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;
}