博客 / 詳情

返回

在nodejs中通過c++調用windows api喚起文件/目錄選擇窗口

一、需求

在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服務端在同一機器中的場景

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.