头图

一、需求

在windows中运行网页+nodejs服务时,
在网页端请求nodejs接口,
唤起文件/文件夹选择窗口,
将选择的文件/目录实际路径显示在网页中
(非C:/fakepath)

二、流程图

    sequenceDiagram
    participant B as 浏览器
    participant S as nodejs server
    participant C as c++编译的exe程序
    B->>S: 发送http请求
    activate S
    activate B
    S->>C: child_process唤起
    deactivate S
    activate C
    note right of C: 等待用户选择文件or目录
    C-->>S: 返回选择
    deactivate C
    activate S
    note right of S: JSON.parse解析结果
    S-->>B : 响应结果给浏览器
    deactivate S
    deactivate B

三、C++实现

#include <windows.h>
#include <shobjidl.h> // 包含 IFileDialog 接口所需的头文件
#include <iostream>
#include <string>

std::string WStringToUTF8(const std::wstring& wstr) {
    if (wstr.empty()) return std::string();
    
    int size_needed = WideCharToMultiByte(CP_UTF8, 0, &wstr[0], (int)wstr.size(), NULL, 0, NULL, NULL);
    std::string strTo(size_needed, 0);
    WideCharToMultiByte(CP_UTF8, 0, &wstr[0], (int)wstr.size(), &strTo[0], size_needed, NULL, NULL);
    return strTo;
}

// 将路径中的 '\' 替换为 '\\\\'
std::string EscapeBackslashes(const std::string& path) {
    std::string escapedPath;
    for (char ch : path) {
        if (ch == '\\') {
            escapedPath += "\\\\\\\\"; // 替换为四个反斜杠
        } else {
            escapedPath += ch;
        }
    }
    return escapedPath;
}

void OpenFileDialog() {
    // 初始化 COM 库
    CoInitialize(nullptr);

    // 创建 IFileDialog 对象
    IFileDialog* pFileDialog = nullptr;
    HRESULT hr = CoCreateInstance(CLSID_FileOpenDialog, nullptr, CLSCTX_ALL, IID_IFileDialog, reinterpret_cast<void**>(&pFileDialog));
    if (SUCCEEDED(hr)) {
        // 显示文件对话框
        hr = pFileDialog->Show(nullptr);
        if (SUCCEEDED(hr)) {
            IShellItem* pItem;
            hr = pFileDialog->GetResult(&pItem);
            if (SUCCEEDED(hr)) {
                // 获取选定文件的路径
                LPWSTR pszName;
                hr = pItem->GetDisplayName(SIGDN_FILESYSPATH, &pszName);
                if (SUCCEEDED(hr)) {
                    std::wstring filePath(pszName);
                    CoTaskMemFree(pszName); // 释放内存
                    pItem->Release(); // 释放 IShellItem 对象
                    pFileDialog->Release(); // 释放 IFileDialog 对象
                    CoUninitialize(); // 释放 COM 库

                    std::string filePathStr = WStringToUTF8(filePath);
                    filePathStr = EscapeBackslashes(filePathStr);

                    std::cout << "{\"selectedFile\":\"" << filePathStr << "\"}" << std::endl;
                    return;
                }
                pItem->Release();
            }
        }
        pFileDialog->Release(); // 释放 IFileDialog 对象
    }

    CoUninitialize(); // 释放 COM 库
    std::cout << "{\"selectedFile\":\"\"}" << std::endl;
}

void SelectFolderDialog() {
    // 初始化 COM 库
    CoInitialize(nullptr);

    // 创建 IFileDialog 对象
    IFileDialog* pFileDialog = nullptr;
    HRESULT hr = CoCreateInstance(CLSID_FileOpenDialog, nullptr, CLSCTX_ALL, IID_IFileDialog, reinterpret_cast<void**>(&pFileDialog));
    if (SUCCEEDED(hr)) {
        // 设置对话框为文件夹选择模式
        pFileDialog->SetOptions(FOS_PICKFOLDERS | FOS_FORCEFILESYSTEM);

        // 显示对话框
        hr = pFileDialog->Show(nullptr);
        if (SUCCEEDED(hr)) {
            IShellItem* pItem;
            hr = pFileDialog->GetResult(&pItem);
            if (SUCCEEDED(hr)) {
                // 获取选定文件夹的路径
                LPWSTR pszName;
                hr = pItem->GetDisplayName(SIGDN_FILESYSPATH, &pszName);
                if (SUCCEEDED(hr)) {
                    std::wstring folderPath(pszName);
                    CoTaskMemFree(pszName); // 释放内存
                    pItem->Release(); // 释放 IShellItem 对象
                    pFileDialog->Release(); // 释放 IFileDialog 对象
                    CoUninitialize(); // 释放 COM 库

                    std::string folderPathStr = WStringToUTF8(folderPath);
                    folderPathStr = EscapeBackslashes(folderPathStr);

                    std::cout << "{\"selectedFolder\":\"" << folderPathStr << "\"}" << std::endl;
                    return;
                }
                pItem->Release();
            }
        }
        pFileDialog->Release(); // 释放 IFileDialog 对象
    }
    
    CoUninitialize(); // 释放 COM 库
    std::cout << "{\"selectedFolder\":\"\"}" << std::endl;
}

int main(int argc, char *argv[]) {
    if (argc > 1 && std::string(argv[1]) == "selectFile") {
        OpenFileDialog();
    } else if (argc > 1 && std::string(argv[1]) == "selectFolder") {
        SelectFolderDialog();
    } else {
        std::cout << "{}" << std::endl;
    }
    return 0;
}

四、使用MINGW64编译

g++ dialog.cpp -o dialog.exe -lole32 -lshell32 -luuid

如果此步骤报错,需要先在MINGW64中安装c++编译环境

pacman -Syu
pacman -S mingw-w64-x86_64-toolchain

五、调用方式

const { spawn } = require('child_process');

// 执行 dialog.exe
const child = spawn('./path/to/dialog.exe', ['selectFile']);
// 或者选择目录(文件夹)
// const child = spawn('./dialog.exe', ['selectFolder']);

// 处理标准输出
child.stdout.on('data', (data) => {
    console.log(`输出: ${data}`);
});

// 处理标准错误输出
child.stderr.on('data', (data) => {
    console.error(`错误: ${data}`);
});

// 处理进程结束
child.on('close', (code) => {
    console.log(`子进程退出,代码: ${code}`);
});

六、局限

只能用于前端和nodejs服务端在同一机器中的场景


643104191
2.4k 声望993 粉丝