跳转至

原生代码与 Electron:C++(Windows)

本教程基于原生代码与 Electron 通用介绍,专注于使用 C++ 和 Win32 API 为 Windows 创建原生插件。为了说明如何在 Electron 应用中嵌入原生 Win32 代码,我们将构建一个基本的原生 Windows GUI(使用 Windows Common Controls),并与 Electron 的 JavaScript 通信。

具体而言,我们将与两个常用的原生 Windows 库集成:

  • comctl32.lib,包含公共控件和用户界面组件。它提供按钮、滚动条、工具栏、状态栏、进度条和树视图等各种 UI 元素。就 Windows 上的 GUI 开发而言,该库非常底层且基础——像 WinUI 或 WPF 这样更现代的框架是更高级的替代方案,但它们需要大量额外的 C++ 和 Windows 版本考量,超出了本教程的实用范围。这样,我们就能避免为多个 Windows 版本构建原生界面的诸多风险!
  • shcore.lib,一个提供高 DPI 感知功能以及其他与 Shell 相关的显示器和 UI 元素管理功能的库。

本教程最适合已经熟悉 Windows 原生 C++ GUI 开发的读者。你应该具备基本窗口类和过程的经验,例如 WNDCLASSEXW 和 WindowProc 函数。你还应该熟悉 Windows 消息循环,它是任何原生应用的核心——我们的代码将使用 GetMessage、TranslateMessage 和 DispatchMessage 来处理消息。最后,我们将使用(但不解释)标准 Win32 控件,例如 WC_EDITW 或 WC_BUTTONW。

[!NOTE] 如果你不熟悉 Windows 上的 C++ GUI 开发,我们推荐 Microsoft 出色的文档和指南,尤其适合初学者。“开始使用 Win32 和 C++” 是一个很好的入门介绍。

要求

与我们的原生代码与 Electron 通用介绍一样,本教程假设你已安装 Node.js 和 npm,以及编译原生代码所需的基本工具。由于本教程讨论编写与 Windows 交互的原生代码,我们建议你在 Windows 上按照本教程操作,并安装 Visual Studio 和“Desktop development with C++ workload”。详细信息,请参阅 Visual Studio 安装说明。

1)创建包

你可以复用我们在原生代码与 Electron教程中创建的包。本教程不会重复其中描述的步骤。首先,让我们设置基本插件文件夹结构:

my-native-win32-addon/
├── binding.gyp
├── include/
│   └── cpp_code.h
├── js/
│   └── index.js
├── package.json
└── src/
    ├── cpp_addon.cc
    └── cpp_code.cc

我们的 package.json 应如下所示:

package.json
{
  "name": "cpp-win32",
  "version": "1.0.0",
  "description": "A demo module that exposes C++ code to Electron",
  "main": "js/index.js",
  "author": "Your Name",
  "scripts": {
    "clean": "rm -rf build_swift && rm -rf build",
    "build-electron": "electron-rebuild",
    "build": "node-gyp configure && node-gyp build"
  },
  "license": "MIT",
  "dependencies": {
    "bindings": "^1.5.0",
    "node-addon-api": "^8.3.0"
  }
}

2)设置构建配置

对于 Windows 专用插件,我们需要修改 binding.gyp 文件以包含 Windows 库并设置适当的编译器标志。简而言之,我们需要做以下三件事:

  1. 我们需要确保插件仅在 Windows 上编译,因为我们将编写平台特定代码。
  2. 我们需要包含 Windows 特定库。在本教程中,我们将针对 comctl32.lib 和 shcore.lib。
  3. 我们需要配置编译器并定义 C++ 宏。
binding.gyp
{
  "targets": [
    {
      "target_name": "cpp_addon",
      "conditions": [
        ['OS=="win"', {
          "sources": [
            "src/cpp_addon.cc",
            "src/cpp_code.cc"
          ],
          "include_dirs": [
            "<!@(node -p \"require('node-addon-api').include\")",
            "include"
          ],
          "libraries": [
            "comctl32.lib",
            "shcore.lib"
          ],
          "dependencies": [
            "<!(node -p \"require('node-addon-api').gyp\")"
          ],
          "msvs_settings": {
            "VCCLCompilerTool": {
              "ExceptionHandling": 1,
              "DebugInformationFormat": "OldStyle",
              "AdditionalOptions": [
                "/FS"
              ]
            },
            "VCLinkerTool": {
              "GenerateDebugInformation": "true"
            }
          },
          "defines": [
            "NODE_ADDON_API_CPP_EXCEPTIONS",
            "WINVER=0x0A00",
            "_WIN32_WINNT=0x0A00"
          ]
        }]
      ]
    }
  ]
}

如果你对此配置的详细信息感兴趣,可以继续阅读;否则,你可以直接复制它们并进入下一步,在那里我们将定义 C++ 接口。

Microsoft Visual Studio 构建配置

msvs_settings 提供 Visual Studio 特定设置。

VCCLCompilerTool 设置

binding.gyp
"VCCLCompilerTool": {
  "ExceptionHandling": 1,
  "DebugInformationFormat": "OldStyle",
  "AdditionalOptions": [
    "/FS"
  ]
}
  • ExceptionHandling: 1:这通过 /EHsc 编译器标志启用 C++ 异常处理。这一点很重要,因为它使编译器能够捕获 C++ 异常,确保发生异常时正确展开栈,并且是 Node-API 在 JavaScript 和 C++ 之间正确处理异常所必需的。
  • DebugInformationFormat: "OldStyle":这指定调试信息的格式,使用更旧、兼容性更好的 PDB(Program Database)格式。它支持各种调试工具的兼容性,并且更适合增量构建。
  • AdditionalOptions: ["/FS"]:这会添加文件序列化标志,强制在编译期间对 PDB 文件进行串行访问。它可以防止在并行构建中,多个编译器进程尝试访问同一 PDB 文件时出现构建错误。

VCLinkerTool 设置

binding.gyp
"VCLinkerTool": {
  "GenerateDebugInformation": "true"
}
  • GenerateDebugInformation: "true":这会让链接器包含调试信息,从而允许使用符号的工具进行源代码级调试。最重要的是,如果插件崩溃,这将使我们能够获得人类可读的堆栈跟踪。

预处理器宏(defines):

  • NODE_ADDON_API_CPP_EXCEPTIONS:此宏在 Node Addon API 中启用 C++ 异常处理。默认情况下,Node-API 使用返回值错误处理模式,但该定义允许 C++ 包装器抛出和捕获 C++ 异常,从而使代码更符合惯用的 C++ 风格,也更容易使用。
  • WINVER=0x0A00:这定义了代码所针对的最低 Windows 版本。值 0x0A00 对应 Windows 10。设置此值会告诉编译器,代码可以使用 Windows 10 中可用的功能,并且不会尝试保持与早期 Windows 版本的向后兼容性。请确保将其设置为您打算在 Electron 应用中支持的最低 Windows 版本。
  • _WIN32_WINNT=0x0A00 - 与 WINVER 类似,这定义了代码将运行的 Windows NT 内核的最低版本。同样,0x0A00 对应 Windows 10。通常将其设置为与 WINVER 相同的值。

3) 定义 C++ 接口

让我们在 include/cpp_code.h 中定义我们的头文件:

include/cpp_code.h
#pragma once
#include <string>
#include <functional>

namespace cpp_code {

std::string hello_world(const std::string& input);
void hello_gui();

// Callback function types
using TodoCallback = std::function<void(const std::string&)>;

// Callback setters
void setTodoAddedCallback(TodoCallback callback);

} // namespace cpp_code

这个头文件:

  • 包含来自通用教程的基本 hello_world 函数
  • 添加一个 hello_gui 函数以创建 Win32 GUI
  • 定义 Todo 操作(添加)的回调类型。为了使本教程保持相对简短,我们只会实现一个回调。
  • 为这些回调提供设置函数

4) 实现 Win32 GUI 代码

现在,让我们在 src/cpp_code.cc 中实现我们的 Win32 GUI。这是一个较大的文件,因此我们将分节查看它。首先,让我们包含必要的头文件并定义基本结构。

src/cpp_code.cc
#include <windows.h>
#include <windowsx.h>
#include <string>
#include <functional>
#include <chrono>
#include <vector>
#include <commctrl.h>
#include <shellscalingapi.h>
#include <thread>

#pragma comment(lib, "comctl32.lib")
#pragma comment(linker, "\"/manifestdependency:type='win32' \
name='Microsoft.Windows.Common-Controls' version='6.0.0.0' \
processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"")

using TodoCallback = std::function<void(const std::string &)>;

static TodoCallback g_todoAddedCallback;

struct TodoItem
{
  GUID id;
  std::wstring text;
  int64_t date;

  std::string toJson() const
  {
    OLECHAR *guidString;
    StringFromCLSID(id, &guidString);
    std::wstring widGuid(guidString);
    CoTaskMemFree(guidString);

    // Convert wide string to narrow for JSON
    std::string guidStr(widGuid.begin(), widGuid.end());
    std::string textStr(text.begin(), text.end());

    return "{"
           "\"id\":\"" + guidStr + "\","
           "\"text\":\"" + textStr + "\","
           "\"date\":" + std::to_string(date) +
           "}";
  }
};

namespace cpp_code
{
  // More code to follow later...
}

在这一节中:

  • 我们包含必要的 Win32 头文件
  • 我们设置 pragma 注释以链接所需的库
  • 我们定义 Todo 操作的回调变量
  • 我们创建一个 TodoItem 结构体,其中包含一个转换为 JSON 的方法

接下来,让我们实现基本函数和辅助方法:

src/cpp_code.cc
namespace cpp_code
{
  std::string hello_world(const std::string &input)
  {
    return "Hello from C++! You said: " + input;
  }

  void setTodoAddedCallback(TodoCallback callback)
  {
    g_todoAddedCallback = callback;
  }

  // Window procedure function that handles window messages
  // hwnd: Handle to the window
  // uMsg: Message code
  // wParam: Additional message-specific information
  // lParam: Additional message-specific information
  LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

  // Helper function to scale a value based on DPI
  int Scale(int value, UINT dpi)
  {
    return MulDiv(value, dpi, 96); // 96 is the default DPI
  }

  // Helper function to convert SYSTEMTIME to milliseconds since epoch
  int64_t SystemTimeToMillis(const SYSTEMTIME &st)
  {
    FILETIME ft;
    SystemTimeToFileTime(&st, &ft);
    ULARGE_INTEGER uli;
    uli.LowPart = ft.dwLowDateTime;
    uli.HighPart = ft.dwHighDateTime;
    return (uli.QuadPart - 116444736000000000ULL) / 10000;
  }

  // More code to follow later...
}

在这一节中,我们添加了一个函数,允许我们设置已添加 todo 项的回调。我们还添加了两个在与 JavaScript 配合工作时需要的辅助函数:一个根据显示器的 DPI 缩放我们的 UI 元素,另一个将 Windows SYSTEMTIME 转换为自 epoch 以来的毫秒数,这是 JavaScript 记录时间的方式。

现在,让我们进入您来到本教程可能最关心的部分——创建一个 GUI 线程并在屏幕上绘制原生像素。我们将通过向 cpp_code 命名空间添加一个 void hello_gui() 函数来实现这一点。我们需要考虑以下几点:

  • 我们需要为 GUI 创建一个新线程,以避免阻塞 Node.js 事件循环。处理 GUI 事件的 Windows 消息循环在一个无限循环中运行,如果在主线程上运行,会阻止 Node.js 处理其他事件。通过在单独线程上运行 GUI,我们可以让原生 Windows 界面和 Node.js 都保持响应。这种分离还有助于防止可能发生的潜在死锁,例如 GUI 操作需要等待 JavaScript 回调时。对于更简单的 Windows API 交互,您不需要这样做——但由于您需要检查消息循环,因此您需要为 GUI 设置自己的线程。
  • 然后,在我们的线程内,我们需要运行一个消息循环来处理任何 Windows 消息。
  • 我们需要设置 DPI 感知以实现正确的显示缩放。
  • 我们需要注册窗口类、创建窗口并添加各种 UI 控件。

在下面的代码中,我们还没有添加任何实际控件。我们故意这样做,以便在这里以更小的部分查看我们添加的代码。

src/cpp_code.cc
void hello_gui() {
  // Launch GUI in a separate thread
  std::thread guiThread([]() {
    // Enable Per-Monitor DPI awareness
    SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);

    // Initialize Common Controls
    INITCOMMONCONTROLSEX icex;
    icex.dwSize = sizeof(INITCOMMONCONTROLSEX);
    icex.dwICC = ICC_STANDARD_CLASSES | ICC_WIN95_CLASSES;
    InitCommonControlsEx(&icex);

    // Register window class
    WNDCLASSEXW wc = {};
    wc.cbSize = sizeof(WNDCLASSEXW);
    wc.lpfnWndProc = WindowProc;
    wc.hInstance = GetModuleHandle(nullptr);
    wc.lpszClassName = L"TodoApp";
    RegisterClassExW(&wc);

    // Get the DPI for the monitor
    UINT dpi = GetDpiForSystem();

    // Create window
    HWND hwnd = CreateWindowExW(
      0, L"TodoApp", L"Todo List",
      WS_OVERLAPPEDWINDOW,
      CW_USEDEFAULT, CW_USEDEFAULT,
      Scale(500, dpi), Scale(500, dpi),
      nullptr, nullptr,
      GetModuleHandle(nullptr), nullptr
    );

    if (hwnd == nullptr) {
      return;
    }

    // Controls go here! The window is currently empty,
    // we'll add controls in the next step.

    ShowWindow(hwnd, SW_SHOW);

    // Message loop
    MSG msg = {};
    while (GetMessage(&msg, nullptr, 0, 0)) {
      TranslateMessage(&msg);
      DispatchMessage(&msg);
    }

    // Clean up
    DeleteObject(hFont);
  });

  // Detach the thread so it runs independently
  guiThread.detach();
}

现在我们已经有了线程、窗口和消息循环,可以添加一些控件了。我们在这里做的事情并不是为 Electron 编写 Windows C++ 所独有的——你只需将下面的代码复制并粘贴到 hello_gui() 函数内的 Controls go here! 部分即可。

我们具体要添加按钮、日期选择器和列表。

src/cpp_code.cc
void hello_gui() {
    // ...
    // All the code above "Controls go here!"

    // Create the modern font with DPI-aware size
    HFONT hFont = CreateFontW(
      -Scale(14, dpi),              // Height (scaled)
      0,                            // Width
      0,                            // Escapement
      0,                            // Orientation
      FW_NORMAL,                    // Weight
      FALSE,                        // Italic
      FALSE,                        // Underline
      FALSE,                        // StrikeOut
      DEFAULT_CHARSET,              // CharSet
      OUT_DEFAULT_PRECIS,           // OutPrecision
      CLIP_DEFAULT_PRECIS,          // ClipPrecision
      CLEARTYPE_QUALITY,            // Quality
      DEFAULT_PITCH | FF_DONTCARE,  // Pitch and Family
      L"Segoe UI"                   // Font face name
    );

    // Create input controls with scaled positions and sizes
    HWND hEdit = CreateWindowExW(0, WC_EDITW, L"",
      WS_CHILD | WS_VISIBLE | WS_BORDER | ES_AUTOHSCROLL,
      Scale(10, dpi), Scale(10, dpi),
      Scale(250, dpi), Scale(25, dpi),
      hwnd, (HMENU)1, GetModuleHandle(nullptr), nullptr);
    SendMessageW(hEdit, WM_SETFONT, (WPARAM)hFont, TRUE);

    // Create date picker
    HWND hDatePicker = CreateWindowExW(0, DATETIMEPICK_CLASSW, L"",
      WS_CHILD | WS_VISIBLE | DTS_SHORTDATECENTURYFORMAT,
      Scale(270, dpi), Scale(10, dpi),
      Scale(100, dpi), Scale(25, dpi),
      hwnd, (HMENU)4, GetModuleHandle(nullptr), nullptr);
    SendMessageW(hDatePicker, WM_SETFONT, (WPARAM)hFont, TRUE);

    HWND hButton = CreateWindowExW(0, WC_BUTTONW, L"Add",
      WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON,
      Scale(380, dpi), Scale(10, dpi),
      Scale(50, dpi), Scale(25, dpi),
      hwnd, (HMENU)2, GetModuleHandle(nullptr), nullptr);
    SendMessageW(hButton, WM_SETFONT, (WPARAM)hFont, TRUE);

    HWND hListBox = CreateWindowExW(0, WC_LISTBOXW, L"",
      WS_CHILD | WS_VISIBLE | WS_BORDER | WS_VSCROLL | LBS_NOTIFY,
      Scale(10, dpi), Scale(45, dpi),
      Scale(460, dpi), Scale(400, dpi),
      hwnd, (HMENU)3, GetModuleHandle(nullptr), nullptr);
    SendMessageW(hListBox, WM_SETFONT, (WPARAM)hFont, TRUE);

    // Store menu handle in window's user data
    SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR)hContextMenu);

    // All the code below "Controls go here!"
    // ...
}

现在我们已经有了一个允许用户添加待办事项的用户界面,需要存储它们——并添加一个可能会调用我们的 JavaScript 回调的辅助函数。在 void hello_gui() { ... } 函数正下方,我们将添加以下内容:

src/cpp_code.cc
  // Global vector to store todos
  static std::vector<TodoItem> g_todos;

  void NotifyCallback(const TodoCallback &callback, const std::string &json)
  {
    if (callback)
    {
      callback(json);
      // Process pending messages
      MSG msg;
      while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE))
      {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
      }
    }
  }

我们还需要一个将待办事项转换为可显示内容的函数。我们不需要花哨的东西——给定待办事项的名称和一个 SYSTEMTIME 时间戳,我们将返回一个简单的字符串。将它添加在上面的函数正下方:

src/cpp_code.cc
  std::wstring FormatTodoDisplay(const std::wstring &text, const SYSTEMTIME &st)
  {
    wchar_t dateStr[64];
    GetDateFormatW(LOCALE_USER_DEFAULT, DATE_SHORTDATE, &st, nullptr, dateStr, 64);
    return text + L" - " + dateStr;
  }

当用户添加待办事项时,我们希望将控件重置回空状态。为此,在我们刚刚添加的代码下方添加一个辅助函数:

src/cpp_code.cc
  void ResetControls(HWND hwnd)
  {
    HWND hEdit = GetDlgItem(hwnd, 1);
    HWND hDatePicker = GetDlgItem(hwnd, 4);
    HWND hAddButton = GetDlgItem(hwnd, 2);

    // Clear text
    SetWindowTextW(hEdit, L"");

    // Reset date to current
    SYSTEMTIME currentTime;
    GetLocalTime(&currentTime);
    DateTime_SetSystemtime(hDatePicker, GDT_VALID, &currentTime);
  }

然后,我们需要实现窗口过程来处理 Windows 消息。和这里的许多代码一样,这段代码中几乎没有特定于 Electron 的部分——因此,作为一名 Win32 C++ 开发者,你会认出这个函数。唯一独特之处在于,我们可能需要通知 JavaScript 回调关于新增的待办事项。我们之前已经实现了 NotifyCallback() 函数,这里将使用它。在上面的函数下方添加以下代码:

src/cpp_code.cc
  LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
  {
    switch (uMsg)
    {
      case WM_COMMAND:
      {
        HWND hListBox = GetDlgItem(hwnd, 3);
        int cmd = LOWORD(wParam);

        switch (cmd)
        {
          case 2: // Add button
          {
            wchar_t buffer[256];
            GetDlgItemTextW(hwnd, 1, buffer, 256);

            if (wcslen(buffer) > 0)
            {
              SYSTEMTIME st;
              HWND hDatePicker = GetDlgItem(hwnd, 4);
              DateTime_GetSystemtime(hDatePicker, &st);

              TodoItem todo;
              CoCreateGuid(&todo.id);
              todo.text = buffer;
              todo.date = SystemTimeToMillis(st);

              g_todos.push_back(todo);

              std::wstring displayText = FormatTodoDisplay(buffer, st);
              SendMessageW(hListBox, LB_ADDSTRING, 0, (LPARAM)displayText.c_str());

              ResetControls(hwnd);
              NotifyCallback(g_todoAddedCallback, todo.toJson());
            }
            break;
          }
        }
        break;
      }

      case WM_DESTROY:
      {
        PostQuitMessage(0);
        return 0;
      }
    }

    return DefWindowProcW(hwnd, uMsg, wParam, lParam);
  }

现在,我们已经成功实现了 Win32 C++ 代码。其中大部分看起来和感觉起来都应该像你在有或没有 Electron 的情况下编写的代码。在下一步中,我们将构建 C++ 和 JavaScript 之间的桥梁。以下是完整实现:

src/cpp_code.cc
#include <windows.h>
#include <windowsx.h>
#include <string>
#include <functional>
#include <chrono>
#include <vector>
#include <commctrl.h>
#include <shellscalingapi.h>
#include <thread>

#pragma comment(lib, "comctl32.lib")
#pragma comment(linker, "\"/manifestdependency:type='win32' \
name='Microsoft.Windows.Common-Controls' version='6.0.0.0' \
processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"")

using TodoCallback = std::function<void(const std::string &)>;

static TodoCallback g_todoAddedCallback;
static TodoCallback g_todoUpdatedCallback;
static TodoCallback g_todoDeletedCallback;

struct TodoItem
{
  GUID id;
  std::wstring text;
  int64_t date;

  std::string toJson() const
  {
    OLECHAR *guidString;
    StringFromCLSID(id, &guidString);
    std::wstring widGuid(guidString);
    CoTaskMemFree(guidString);

    // Convert wide string to narrow for JSON
    std::string guidStr(widGuid.begin(), widGuid.end());
    std::string textStr(text.begin(), text.end());

    return "{"
           "\"id\":\"" + guidStr + "\","
           "\"text\":\"" + textStr + "\","
           "\"date\":" + std::to_string(date) +
           "}";
  }
};

namespace cpp_code
{

  std::string hello_world(const std::string &input)
  {
    return "Hello from C++! You said: " + input;
  }

  void setTodoAddedCallback(TodoCallback callback)
  {
    g_todoAddedCallback = callback;
  }

  void setTodoUpdatedCallback(TodoCallback callback)
  {
    g_todoUpdatedCallback = callback;
  }

  void setTodoDeletedCallback(TodoCallback callback)
  {
    g_todoDeletedCallback = callback;
  }

  LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

  // Helper function to scale a value based on DPI
  int Scale(int value, UINT dpi)
  {
    return MulDiv(value, dpi, 96); // 96 is the default DPI
  }

  // Helper function to convert SYSTEMTIME to milliseconds since epoch
  int64_t SystemTimeToMillis(const SYSTEMTIME &st)
  {
    FILETIME ft;
    SystemTimeToFileTime(&st, &ft);
    ULARGE_INTEGER uli;
    uli.LowPart = ft.dwLowDateTime;
    uli.HighPart = ft.dwHighDateTime;
    return (uli.QuadPart - 116444736000000000ULL) / 10000;
  }

  void ResetControls(HWND hwnd)
  {
    HWND hEdit = GetDlgItem(hwnd, 1);
    HWND hDatePicker = GetDlgItem(hwnd, 4);
    HWND hAddButton = GetDlgItem(hwnd, 2);

    // Clear text
    SetWindowTextW(hEdit, L"");

    // Reset date to current
    SYSTEMTIME currentTime;
    GetLocalTime(&currentTime);
    DateTime_SetSystemtime(hDatePicker, GDT_VALID, &currentTime);
  }

  void hello_gui() {
    // Launch GUI in a separate thread
    std::thread guiThread([]() {
      // Enable Per-Monitor DPI awareness
      SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);

      // Initialize Common Controls
      INITCOMMONCONTROLSEX icex;
      icex.dwSize = sizeof(INITCOMMONCONTROLSEX);
      icex.dwICC = ICC_STANDARD_CLASSES | ICC_WIN95_CLASSES;
      InitCommonControlsEx(&icex);

      // Register window class
      WNDCLASSEXW wc = {};
      wc.cbSize = sizeof(WNDCLASSEXW);
      wc.lpfnWndProc = WindowProc;
      wc.hInstance = GetModuleHandle(nullptr);
      wc.lpszClassName = L"TodoApp";
      RegisterClassExW(&wc);

      // Get the DPI for the monitor
      UINT dpi = GetDpiForSystem();

      // Create window
      HWND hwnd = CreateWindowExW(
        0, L"TodoApp", L"Todo List",
        WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT, CW_USEDEFAULT,
        Scale(500, dpi), Scale(500, dpi),
        nullptr, nullptr,
        GetModuleHandle(nullptr), nullptr
      );

      if (hwnd == nullptr) {
        return;
      }

      // Create the modern font with DPI-aware size
      HFONT hFont = CreateFontW(
        -Scale(14, dpi),              // Height (scaled)
        0,                            // Width
        0,                            // Escapement
        0,                            // Orientation
        FW_NORMAL,                    // Weight
        FALSE,                        // Italic
        FALSE,                        // Underline
        FALSE,                        // StrikeOut
        DEFAULT_CHARSET,              // CharSet
        OUT_DEFAULT_PRECIS,           // OutPrecision
        CLIP_DEFAULT_PRECIS,          // ClipPrecision
        CLEARTYPE_QUALITY,            // Quality
        DEFAULT_PITCH | FF_DONTCARE,  // Pitch and Family
        L"Segoe UI"                   // Font face name
      );

      // Create input controls with scaled positions and sizes
      HWND hEdit = CreateWindowExW(0, WC_EDITW, L"",
        WS_CHILD | WS_VISIBLE | WS_BORDER | ES_AUTOHSCROLL,
        Scale(10, dpi), Scale(10, dpi),
        Scale(250, dpi), Scale(25, dpi),
        hwnd, (HMENU)1, GetModuleHandle(nullptr), nullptr);
      SendMessageW(hEdit, WM_SETFONT, (WPARAM)hFont, TRUE);

      // Create date picker
      HWND hDatePicker = CreateWindowExW(0, DATETIMEPICK_CLASSW, L"",
        WS_CHILD | WS_VISIBLE | DTS_SHORTDATECENTURYFORMAT,
        Scale(270, dpi), Scale(10, dpi),
        Scale(100, dpi), Scale(25, dpi),
        hwnd, (HMENU)4, GetModuleHandle(nullptr), nullptr);
      SendMessageW(hDatePicker, WM_SETFONT, (WPARAM)hFont, TRUE);

      HWND hButton = CreateWindowExW(0, WC_BUTTONW, L"Add",
        WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON,
        Scale(380, dpi), Scale(10, dpi),
        Scale(50, dpi), Scale(25, dpi),
        hwnd, (HMENU)2, GetModuleHandle(nullptr), nullptr);
      SendMessageW(hButton, WM_SETFONT, (WPARAM)hFont, TRUE);

      HWND hListBox = CreateWindowExW(0, WC_LISTBOXW, L"",
        WS_CHILD | WS_VISIBLE | WS_BORDER | WS_VSCROLL | LBS_NOTIFY,
        Scale(10, dpi), Scale(45, dpi),
        Scale(460, dpi), Scale(400, dpi),
        hwnd, (HMENU)3, GetModuleHandle(nullptr), nullptr);
      SendMessageW(hListBox, WM_SETFONT, (WPARAM)hFont, TRUE);

      ShowWindow(hwnd, SW_SHOW);

      // Message loop
      MSG msg = {};
      while (GetMessage(&msg, nullptr, 0, 0)) {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
      }

      // Clean up
      DeleteObject(hFont);
    });

    // Detach the thread so it runs independently
    guiThread.detach();
  }

  // Global vector to store todos
  static std::vector<TodoItem> g_todos;

  void NotifyCallback(const TodoCallback &callback, const std::string &json)
  {
    if (callback)
    {
      callback(json);
      // Process pending messages
      MSG msg;
      while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE))
      {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
      }
    }
  }

  std::wstring FormatTodoDisplay(const std::wstring &text, const SYSTEMTIME &st)
  {
    wchar_t dateStr[64];
    GetDateFormatW(LOCALE_USER_DEFAULT, DATE_SHORTDATE, &st, nullptr, dateStr, 64);
    return text + L" - " + dateStr;
  }

  LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
  {
    switch (uMsg)
    {
      case WM_COMMAND:
      {
        HWND hListBox = GetDlgItem(hwnd, 3);
        int cmd = LOWORD(wParam);

        switch (cmd)
        {
          case 2: // Add button
          {
            wchar_t buffer[256];
            GetDlgItemTextW(hwnd, 1, buffer, 256);

            if (wcslen(buffer) > 0)
            {
              SYSTEMTIME st;
              HWND hDatePicker = GetDlgItem(hwnd, 4);
              DateTime_GetSystemtime(hDatePicker, &st);

              TodoItem todo;
              CoCreateGuid(&todo.id);
              todo.text = buffer;
              todo.date = SystemTimeToMillis(st);

              g_todos.push_back(todo);

              std::wstring displayText = FormatTodoDisplay(buffer, st);
              SendMessageW(hListBox, LB_ADDSTRING, 0, (LPARAM)displayText.c_str());

              ResetControls(hwnd);
              NotifyCallback(g_todoAddedCallback, todo.toJson());
            }
            break;
          }
        }
        break;
      }

      case WM_DESTROY:
      {
        PostQuitMessage(0);
        return 0;
      }
    }

    return DefWindowProcW(hwnd, uMsg, wParam, lParam);
  }

} // namespace cpp_code

5) 创建 Node.js 插件桥接

现在,让我们在 src/cpp_addon.cc 中实现 C++ 代码与 Node.js 之间的桥接。首先,为我们的插件创建一个基本骨架:

src/cpp_addon.cc
#include <napi.h>
#include <string>
#include "cpp_code.h"

Napi::Object Init(Napi::Env env, Napi::Object exports) {
    // We'll add code here later
    return exports;
}

NODE_API_MODULE(cpp_addon, Init)

这是使用 node-addon-api 的 Node.js 插件所需的最小结构。Init 函数在插件加载时被调用,NODE_API_MODULE 宏用于注册我们的初始化函数。

创建一个类来封装我们的 C++ 代码

让我们创建一个类,用于封装我们的 C++ 代码并将其暴露给 JavaScript:

src/cpp_addon.cc
#include <napi.h>
#include <string>
#include "cpp_code.h"

class CppAddon : public Napi::ObjectWrap<CppAddon> {
public:
    static Napi::Object Init(Napi::Env env, Napi::Object exports) {
        Napi::Function func = DefineClass(env, "CppWin32Addon", {
            // We'll add methods here later
        });

        Napi::FunctionReference* constructor = new Napi::FunctionReference();
        *constructor = Napi::Persistent(func);
        env.SetInstanceData(constructor);

        exports.Set("CppWin32Addon", func);
        return exports;
    }

    CppAddon(const Napi::CallbackInfo& info)
        : Napi::ObjectWrap<CppAddon>(info) {
        // Constructor logic will go here
    }

private:
    // Will add private members and methods later
};

Napi::Object Init(Napi::Env env, Napi::Object exports) {
    return CppAddon::Init(env, exports);
}

NODE_API_MODULE(cpp_addon, Init)

这会创建一个继承自 Napi::ObjectWrap 的类,使我们能够封装 C++ 对象以便在 JavaScript 中使用。Init 函数会设置该类并将其导出到 JavaScript。

实现基本功能 - HelloWorld

现在,让我们添加第一个方法,即 HelloWorld 函数:

src/cpp_addon.cc
// ... previous code

class CppAddon : public Napi::ObjectWrap<CppAddon> {
public:
    static Napi::Object Init(Napi::Env env, Napi::Object exports) {
        Napi::Function func = DefineClass(env, "CppWin32Addon", {
            InstanceMethod("helloWorld", &CppAddon::HelloWorld),
        });

        // ... rest of Init function
    }

    CppAddon(const Napi::CallbackInfo& info)
        : Napi::ObjectWrap<CppAddon>(info) {
        // Constructor logic will go here
    }

private:
    Napi::Value HelloWorld(const Napi::CallbackInfo& info) {
        Napi::Env env = info.Env();

        if (info.Length() < 1 || !info[0].IsString()) {
            Napi::TypeError::New(env, "Expected string argument").ThrowAsJavaScriptException();
            return env.Null();
        }

        std::string input = info[0].As<Napi::String>();
        std::string result = cpp_code::hello_world(input);

        return Napi::String::New(env, result);
    }
};

// ... rest of the file

这会将 HelloWorld 方法添加到我们的类中,并通过 DefineClass 进行注册。该方法会验证输入,调用我们的 C++ 函数,并将结果返回给 JavaScript。

src/cpp_addon.cc
// ... previous code

class CppAddon : public Napi::ObjectWrap<CppAddon> {
public:
    static Napi::Object Init(Napi::Env env, Napi::Object exports) {
        Napi::Function func = DefineClass(env, "CppWin32Addon", {
            InstanceMethod("helloWorld", &CppAddon::HelloWorld),
            InstanceMethod("helloGui", &CppAddon::HelloGui),
        });

        // ... rest of Init function
    }

    // ... constructor

private:
    // ... HelloWorld method

    void HelloGui(const Napi::CallbackInfo& info) {
        cpp_code::hello_gui();
    }
};

// ... rest of the file

这个简单的方法会调用 C++ 代码中的 hello_gui 函数,该函数会在单独的线程中启动 Win32 GUI 窗口。

设置事件系统

接下来是复杂的部分——设置事件系统,以便我们的 C++ 代码能够回调 JavaScript。我们需要:

  1. 添加私有成员以存储回调
  2. 创建一个线程安全函数,用于跨线程通信
  3. 添加一个 On 方法以注册 JavaScript 回调
  4. 设置 C++ 回调,以触发 JavaScript 回调
src/cpp_addon.cc
// ... previous code

class CppAddon : public Napi::ObjectWrap<CppAddon> {
public:
    // ... previous public methods

private:
    Napi::Env env_;
    Napi::ObjectReference emitter;
    Napi::ObjectReference callbacks;
    napi_threadsafe_function tsfn_;

    // ... existing private methods
};

// ... rest of the file

现在,让我们增强构造函数以初始化这些成员:

src/cpp_addon.cc
// ... previous code

class CppAddon : public Napi::ObjectWrap<CppAddon> {
public:
    // CallbackData struct to pass data between threads
    struct CallbackData {
        std::string eventType;
        std::string payload;
        CppAddon* addon;
    };

    CppAddon(const Napi::CallbackInfo& info)
        : Napi::ObjectWrap<CppAddon>(info)
        , env_(info.Env())
        , emitter(Napi::Persistent(Napi::Object::New(info.Env())))
        , callbacks(Napi::Persistent(Napi::Object::New(info.Env())))
        , tsfn_(nullptr) {

        // We'll add threadsafe function setup here in the next step
    }

    // Add destructor to clean up
    ~CppAddon() {
        if (tsfn_ != nullptr) {
            napi_release_threadsafe_function(tsfn_, napi_tsfn_release);
            tsfn_ = nullptr;
        }
    }

    // ... rest of the class
};

// ... rest of the file

现在,让我们将线程安全函数的设置添加到构造函数中:

src/cpp_addon.cc
// ... existing constructor code
CppAddon(const Napi::CallbackInfo& info)
    : Napi::ObjectWrap<CppAddon>(info)
    , env_(info.Env())
    , emitter(Napi::Persistent(Napi::Object::New(info.Env())))
    , callbacks(Napi::Persistent(Napi::Object::New(info.Env())))
    , tsfn_(nullptr) {

    napi_status status = napi_create_threadsafe_function(
        env_,
        nullptr,
        nullptr,
        Napi::String::New(env_, "CppCallback"),
        0,
        1,
        nullptr,
        nullptr,
        this,
        [](napi_env env, napi_value js_callback, void* context, void* data) {
            auto* callbackData = static_cast<CallbackData*>(data);
            if (!callbackData) return;

            Napi::Env napi_env(env);
            Napi::HandleScope scope(napi_env);

            auto addon = static_cast<CppAddon*>(context);
            if (!addon) {
                delete callbackData;
                return;
            }

            try {
                auto callback = addon->callbacks.Value().Get(callbackData->eventType).As<Napi::Function>();
                if (callback.IsFunction()) {
                    callback.Call(addon->emitter.Value(), {Napi::String::New(napi_env, callbackData->payload)});
                }
            } catch (...) {}

            delete callbackData;
        },
        &tsfn_
    );

    if (status != napi_ok) {
        Napi::Error::New(env_, "Failed to create threadsafe function").ThrowAsJavaScriptException();
        return;
    }

    // We'll add callback setup in the next step
}

这创建了一个线程安全函数,允许我们的 C++ 代码从任意线程调用 JavaScript。被调用时,它会获取相应的 JavaScript 回调,并使用提供的负载调用它。

现在让我们添加回调设置:

src/cpp_addon.cc
// ... existing constructor code after threadsafe function setup

// Set up the callbacks here
auto makeCallback = [this](const std::string& eventType) {
    return [this, eventType](const std::string& payload) {
        if (tsfn_ != nullptr) {
            auto* data = new CallbackData{
                eventType,
                payload,
                this
            };
            napi_call_threadsafe_function(tsfn_, data, napi_tsfn_blocking);
        }
    };
};

cpp_code::setTodoAddedCallback(makeCallback("todoAdded"));

这创建了一个函数,用于为每种事件类型生成回调。这些回调会捕获事件类型,并在被调用时创建一个 CallbackData 对象,然后将其传递给我们的线程安全函数。

最后,让我们添加 On 方法,以便 JavaScript 可以注册回调函数:

src/cpp_addon.cc
// ... in the class definition, add On to DefineClass
static Napi::Object Init(Napi::Env env, Napi::Object exports) {
    Napi::Function func = DefineClass(env, "CppWin32Addon", {
        InstanceMethod("helloWorld", &CppAddon::HelloWorld),
        InstanceMethod("helloGui", &CppAddon::HelloGui),
        InstanceMethod("on", &CppAddon::On),
        InstanceMethod("destroy", &CppAddon::Destroy)
    });

    // ... rest of Init function
}

// ... and add the implementation in the private section
Napi::Value On(const Napi::CallbackInfo& info) {
    Napi::Env env = info.Env();

    if (info.Length() < 2 || !info[0].IsString() || !info[1].IsFunction()) {
        Napi::TypeError::New(env, "Expected (string, function) arguments").ThrowAsJavaScriptException();
        return env.Undefined();
    }

    callbacks.Value().Set(info[0].As<Napi::String>(), info[1].As<Napi::Function>());
    return env.Undefined();
}

Napi::Value Destroy(const Napi::CallbackInfo& info) {
    callbacks.Reset();
    emitter.Reset();

    if (tsfn_ != nullptr) {
        napi_release_threadsafe_function(tsfn_, napi_tsfn_abort);
        tsfn_ = nullptr;
    }

    return info.Env().Undefined();
}

这允许 JavaScript 为特定事件类型注册回调。Destroy 方法会释放所有持久引用并中止线程安全函数,必须在应用退出前调用它,以防止进程挂起。

将桥接组装起来

现在我们已经具备了所有组件。

以下是完整实现:

src/cpp_addon.cc
#include <napi.h>
#include <string>
#include "cpp_code.h"

class CppAddon : public Napi::ObjectWrap<CppAddon> {
public:
    static Napi::Object Init(Napi::Env env, Napi::Object exports) {
        Napi::Function func = DefineClass(env, "CppWin32Addon", {
            InstanceMethod("helloWorld", &CppAddon::HelloWorld),
            InstanceMethod("helloGui", &CppAddon::HelloGui),
            InstanceMethod("on", &CppAddon::On)
        });

        Napi::FunctionReference* constructor = new Napi::FunctionReference();
        *constructor = Napi::Persistent(func);
        env.SetInstanceData(constructor);

        exports.Set("CppWin32Addon", func);
        return exports;
    }

    struct CallbackData {
        std::string eventType;
        std::string payload;
        CppAddon* addon;
    };

    CppAddon(const Napi::CallbackInfo& info)
        : Napi::ObjectWrap<CppAddon>(info)
        , env_(info.Env())
        , emitter(Napi::Persistent(Napi::Object::New(info.Env())))
        , callbacks(Napi::Persistent(Napi::Object::New(info.Env())))
        , tsfn_(nullptr) {

        napi_status status = napi_create_threadsafe_function(
            env_,
            nullptr,
            nullptr,
            Napi::String::New(env_, "CppCallback"),
            0,
            1,
            nullptr,
            nullptr,
            this,
            [](napi_env env, napi_value js_callback, void* context, void* data) {
                auto* callbackData = static_cast<CallbackData*>(data);
                if (!callbackData) return;

                Napi::Env napi_env(env);
                Napi::HandleScope scope(napi_env);

                auto addon = static_cast<CppAddon*>(context);
                if (!addon) {
                    delete callbackData;
                    return;
                }

                try {
                    auto callback = addon->callbacks.Value().Get(callbackData->eventType).As<Napi::Function>();
                    if (callback.IsFunction()) {
                        callback.Call(addon->emitter.Value(), {Napi::String::New(napi_env, callbackData->payload)});
                    }
                } catch (...) {}

                delete callbackData;
            },
            &tsfn_
        );

        if (status != napi_ok) {
            Napi::Error::New(env_, "Failed to create threadsafe function").ThrowAsJavaScriptException();
            return;
        }

        // Set up the callbacks here
        auto makeCallback = [this](const std::string& eventType) {
            return [this, eventType](const std::string& payload) {
                if (tsfn_ != nullptr) {
                    auto* data = new CallbackData{
                        eventType,
                        payload,
                        this
                    };
                    napi_call_threadsafe_function(tsfn_, data, napi_tsfn_blocking);
                }
            };
        };

        cpp_code::setTodoAddedCallback(makeCallback("todoAdded"));
    }

    ~CppAddon() {
        if (tsfn_ != nullptr) {
            napi_release_threadsafe_function(tsfn_, napi_tsfn_release);
            tsfn_ = nullptr;
        }
    }

private:
    Napi::Env env_;
    Napi::ObjectReference emitter;
    Napi::ObjectReference callbacks;
    napi_threadsafe_function tsfn_;

    Napi::Value HelloWorld(const Napi::CallbackInfo& info) {
        Napi::Env env = info.Env();

        if (info.Length() < 1 || !info[0].IsString()) {
            Napi::TypeError::New(env, "Expected string argument").ThrowAsJavaScriptException();
            return env.Null();
        }

        std::string input = info[0].As<Napi::String>();
        std::string result = cpp_code::hello_world(input);

        return Napi::String::New(env, result);
    }

    void HelloGui(const Napi::CallbackInfo& info) {
        cpp_code::hello_gui();
    }

    Napi::Value On(const Napi::CallbackInfo& info) {
        Napi::Env env = info.Env();

        if (info.Length() < 2 || !info[0].IsString() || !info[1].IsFunction()) {
            Napi::TypeError::New(env, "Expected (string, function) arguments").ThrowAsJavaScriptException();
            return env.Undefined();
        }

        callbacks.Value().Set(info[0].As<Napi::String>(), info[1].As<Napi::Function>());
        return env.Undefined();
    }

    Napi::Value Destroy(const Napi::CallbackInfo& info) {
        callbacks.Reset();
        emitter.Reset();

        if (tsfn_ != nullptr) {
            napi_release_threadsafe_function(tsfn_, napi_tsfn_abort);
            tsfn_ = nullptr;
        }

        return info.Env().Undefined();
    }
};

Napi::Object Init(Napi::Env env, Napi::Object exports) {
    return CppAddon::Init(env, exports);
}

NODE_API_MODULE(cpp_addon, Init)

6) 创建 JavaScript 包装器

让我们通过在 js/index.js 中添加一个 JavaScript 包装器来收尾。正如大家所见,C++ 需要大量样板代码,而这些代码用 JavaScript 编写可能更简单或更快——你会发现许多生产环境应用最终都会在调用原生代码之前,用 JavaScript 转换数据或请求。例如,我们会将时间戳转换为正确的 JavaScript 日期。

js/index.js
const EventEmitter = require('events')

class CppWin32Addon extends EventEmitter {
  constructor() {
    super()

    if (process.platform !== 'win32') {
      throw new Error('This module is only available on Windows')
    }

    const native = require('bindings')('cpp_addon')
    this.addon = new native.CppWin32Addon();

    this.addon.on('todoAdded', (payload) => {
      this.emit('todoAdded', this.#parse(payload))
    });

    this.addon.on('todoUpdated', (payload) => {
      this.emit('todoUpdated', this.#parse(payload))
    });

    this.addon.on('todoDeleted', (payload) => {
      this.emit('todoDeleted', this.#parse(payload))
    });
  }

  helloWorld(input = "") {
    return this.addon.helloWorld(input)
  }

  helloGui() {
    this.addon.helloGui()
  }

  destroy() {
    this.addon.destroy()
  }

  #parse(payload) {
    const parsed = JSON.parse(payload)

    return { ...parsed, date: new Date(parsed.date) }
  }
}

if (process.platform === 'win32') {
  module.exports = new CppWin32Addon()
} else {
  module.exports = {}
}

[!IMPORTANT] 在应用退出之前必须调用 destroy()(例如在 will-quit 或 before-quit 事件处理程序中)。否则,对回调和线程安全函数的持久引用会阻止原生插件的析构函数运行,导致 Electron 在退出时挂起。

7) 构建和测试插件

在所有文件就位后,你可以构建插件:

npm run build

结论

现在,你已经使用 C++ 和 Win32 API 为 Windows 构建了一个完整的原生 Node.js 插件。我们在这里完成的一些事情包括:

  1. 使用 C++ 创建原生 Windows GUI
  2. 实现一个具有添加、编辑和删除功能的待办事项列表应用
  3. 在 C++ 和 JavaScript 之间进行双向通信
  4. 使用 Win32 控件和 Windows 特定功能
  5. 从 C++ 线程安全地回调到 JavaScript

这为在 Electron 应用中构建更复杂的 Windows 特定功能奠定了基础,让你兼得两者之长:Web 技术的易用性与原生代码的强大能力。

有关使用 Win32 API 的更多信息,请参阅 Microsoft C++、C 和汇编器文档 和 Windows API 参考。

本页原文 Markdown:在 AtomGit 查看·内容源自开源项目 el/electron