Skip to main content

本地代码与 Electron:C++(Windows)

🌐 Native Code and Electron: C++ (Windows)

本教程基于Native Code 和 Electron 的一般介绍,重点介绍如何使用 C++ 和 Win32 API 为 Windows 创建本地插件。为了说明如何在你的 Electron 应用中嵌入本地 Win32 代码,我们将构建一个基本的本地 Windows GUI(使用 Windows 通用控件),并与 Electron 的 JavaScript 进行通信。

🌐 This tutorial builds on the general introduction to Native Code and Electron and focuses on creating a native addon for Windows using C++ and the Win32 API. To illustrate how you can embed native Win32 code in your Electron app, we'll be building a basic native Windows GUI (using the Windows Common Controls) that communicates with Electron's JavaScript.

具体来说,我们将集成两个常用的 Windows 原生库:

🌐 Specifically, we'll be integrating with two commonly used native Windows libraries:

  • comctl32.lib,其中包含常用控件和用户界面组件。它提供各种 UI 元素,如按钮、滚动条、工具栏、状态栏、进度条和树视图。就 Windows 上的 GUI 开发而言,这个库是非常底层和基础的——相比之下,更现代的框架如 WinUI 或 WPF 更加高级,但它们需要更多的 C++ 和 Windows 版本相关考虑,而这些对于本教程来说并不必要。通过这种方式,我们可以避免为多个 Windows 版本构建本地界面时遇到的诸多风险!
  • shcore.lib 是一个库,提供高 DPI 感知功能以及与显示器和 UI 元素管理相关的其他 Shell 功能。

本教程对于那些已经对 Windows 上的原生 C++ GUI 开发有所了解的人最为有用。你应该具备使用基本窗口类和进程的经验,比如 WNDCLASSEXWWindowProc 函数。你还应该熟悉 Windows 消息循环,这是任何原生应用的核心——我们的代码将使用 GetMessageTranslateMessageDispatchMessage 来处理消息。最后,我们将使用(但不会解释)标准 Win32 控件,比如 WC_EDITWWC_BUTTONW

🌐 This tutorial will be most useful to those who already have some familiarity with native C++ GUI development on Windows. You should have experience with basic window classes and procedures, like WNDCLASSEXW and WindowProc functions. You should also be familiar with the Windows message loop, which is the heart of any native application - our code will be using GetMessage, TranslateMessage, and DispatchMessage to handle messages. Lastly, we'll be using (but not explaining) standard Win32 controls like WC_EDITW or WC_BUTTONW.

note

如果你不熟悉 Windows 上的 C++ 图形界面开发,我们推荐微软的优秀文档和指南,尤其适合初学者。《用 Win32 和 C++ 入门》是一个很好的介绍。

要求

🌐 Requirements

就像我们的原生代码和 Electron 总体介绍一样,本教程假设你已经安装了 Node.js 和 npm,以及编译原生代码所需的基本工具。由于本教程讨论的是与 Windows 交互的原生代码编写,我们建议你在 Windows 系统上按照本教程操作,并安装 Visual Studio 以及“使用 C++ 的桌面开发”工作负载。详情请参阅Visual Studio 安装说明

🌐 Just like our general introduction to Native Code and Electron, this tutorial assumes you have Node.js and npm installed, as well as the basic tools necessary for compiling native code. Since this tutorial discusses writing native code that interacts with Windows, we recommend that you follow this tutorial on Windows with both Visual Studio and the "Desktop development with C++ workload" installed. For details, see the Visual Studio Installation instructions.

1) 创建一个软件包

🌐 1) Creating a package

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

🌐 You can re-use the package we created in our Native Code and Electron tutorial. This tutorial will not be repeating the steps described there. Let's first setup our basic addon folder structure:

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

我们的 package.json 应该看起来像这样:

🌐 Our package.json should look like this:

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) 设置构建配置

🌐 2) Setting Up the Build Configuration

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

🌐 For a Windows-specific addon, we need to modify our binding.gyp file to include Windows libraries and set appropriate compiler flags. In short, we need to do the following three things:

  1. 我们需要确保我们的插件仅在 Windows 上编译,因为我们将编写特定于平台的代码。
  2. 我们需要包含 Windows 特定的库。在我们的教程中,我们将以 comctl32.libshcore.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++ 接口。

🌐 If you're curious about the details about this config, you can read on - otherwise, feel free to just copy them and move on to the next step, where we define the C++ interface.

Microsoft Visual Studio 构建配置

🌐 Microsoft Visual Studio Build Configurations

msvs_settings 提供 Visual Studio 特定的设置。

VCCLCompilerTool 设置

🌐 VCCLCompilerTool Settings

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

VCLinkerTool 设置

🌐 VCLinkerTool Settings

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

预处理器宏(defines):

🌐 Preprocessor macros (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++ 接口

🌐 3) Defining the C++ Interface

让我们在 include/cpp_code.h 中定义我们的标题:

🌐 Let's define our header in 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

标题:

🌐 This header:

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

4) 实现 Win32 图形用户界面代码

🌐 4) Implementing Win32 GUI Code

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

🌐 Now, let's implement our Win32 GUI in src/cpp_code.cc. This is a larger file, so we'll review it in sections. First, let's include necessary headers and define basic structures.

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...
}

本节内容:

🌐 In this section:

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

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

🌐 Next, let's implement the basic functions and helper methods:

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...
}

在本节中,我们添加了一个函数,允许我们为新增的待办事项设置回调。我们还添加了两个在使用 JavaScript 时需要的辅助函数:一个用于根据显示器的 DPI 缩放我们的 UI 元素,另一个用于将 Windows SYSTEMTIME 转换为自纪元以来的毫秒数,这是 JavaScript 记录时间的方式。

🌐 In this section, we've added a function that allows us to set the callback for an added todo item. We also added two helper functions that we need when working with JavaScript: One to scale our UI elements depending on the display's DPI - and another one to convert a Windows SYSTEMTIME to milliseconds since epoch, which is how JavaScript keeps track of time.

现在,让我们进入你可能来本教程的主要部分——创建一个 GUI 线程并在屏幕上绘制原生像素。我们将通过向我们的 cpp_code 命名空间添加一个 void hello_gui() 函数来实现这一点。有一些注意事项需要我们考虑:

🌐 Now, let's get to the part you probably came to this tutorial for - creating a GUI thread and drawing native pixels on screen. We'll do that by adding a void hello_gui() function to our cpp_code namespace. There are a few considerations we need to make:

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

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

🌐 In the code below, we haven't added any actual controls yet. We're doing that on purpose to look at our added code in smaller portions here.

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! 部分。

🌐 Now that we have a thread, a window, and a message loop, we can add some controls. Nothing we're doing here is unique to writing Windows C++ for Electron - you can simply copy & paste the code below into the Controls go here! section inside our hello_gui() function.

我们专门添加了按钮、日期选择器和列表。

🌐 We're specifically adding buttons, a date picker, and a list.

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() { ... } 函数的正下方,我们将添加以下内容:

🌐 Now that we have a user interface that allows users to add todos, we need to store them - and add a helper function that'll potentially call our JavaScript callback. Right below the void hello_gui() { ... } function, we'll add the following:

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 时间戳,我们将返回一个简单的字符串。把它添加到上面函数的正下方:

🌐 We'll also need a function that turns a todo into something we can display. We don't need anything fancy - given the name of the todo and a SYSTEMTIME timestamp, we'll return a simple string. Add it right below the function above:

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;
}

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

🌐 When a user adds a todo, we want to reset the controls back to an empty state. To do so, add a helper function below the code we just added:

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() 函数,我们将在这里使用它。在上面的函数下面添加这段代码:

🌐 Then, we'll need to implement the window procedure to handle Windows messages. Like a lot of our code here, there is very little specific to Electron in this code - so as a Win32 C++ developer, you'll recognize this function. The only thing that is unique is that we want to potentially notify the JavaScript callback about an added todo. We've previously implemented the NotifyCallback() function, which we will be using here. Add this code right below the function above:

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 之间的桥梁。以下是完整的实现:

🌐 We now have successfully implemented the Win32 C++ code. Most of this should look and feel to you like code you'd write with or without Electron. In the next step, we'll be building the bridge between C++ and JavaScript. Here's the complete implementation:

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 插件桥接

🌐 5) Creating the Node.js Addon Bridge

现在让我们在 src/cpp_addon.cc 中实现我们的 C++ 代码和 Node.js 之间的桥梁。让我们从为我们的插件创建一个基本框架开始:

🌐 Now let's implement the bridge between our C++ code and Node.js in src/cpp_addon.cc. Let's start by creating a basic skeleton for our addon:

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 宏则注册我们的初始化器。

🌐 This is the minimal structure required for a Node.js addon using node-addon-api. The Init function is called when the addon is loaded, and the NODE_API_MODULE macro registers our initializer.

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

🌐 Create a Class to Wrap Our C++ Code

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

🌐 Let's create a class that will wrap our C++ code and expose it to 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。

🌐 This creates a class that inherits from Napi::ObjectWrap, which allows us to wrap our C++ object for use in JavaScript. The Init function sets up the class and exports it to JavaScript.

实现基本功能 - HelloWorld

🌐 Implement Basic Functionality - HelloWorld

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

🌐 Now let's add our first method, the HelloWorld function:

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。

🌐 This adds the HelloWorld method to our class and registers it with DefineClass. The method validates inputs, calls our C++ function, and returns the result to 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 窗口。

🌐 This simple method calls our hello_gui function from the C++ code, which launches the Win32 GUI window in a separate thread.

设置事件系统

🌐 Setting Up the Event System

现在到了复杂的部分——设置事件系统,以便我们的 C++ 代码可以回调到 JavaScript。我们需要:

🌐 Now comes the complex part - setting up the event system so our C++ code can call back to JavaScript. We need to:

  1. 添加私有成员来存储回调函数
  2. 创建一个线程安全函数用于跨线程通信
  3. 添加一个 On 方法来注册 JavaScript 回调
  4. 设置将触发 JavaScript 回调的 C++ 回调
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

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

🌐 Now, let's enhance our constructor to initialize these members:

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

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

🌐 Now let's add the threadsafe function setup to our constructor:

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 回调并使用提供的有效负载进行调用。

🌐 This creates a threadsafe function that allows our C++ code to call JavaScript from any thread. When called, it retrieves the appropriate JavaScript callback and invokes it with the provided payload.

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

🌐 Now let's add the callbacks setup:

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 对象并将其传递给我们的线程安全函数。

🌐 This creates a function that generates callbacks for each event type. The callbacks capture the event type and, when called, create a CallbackData object and pass it to our threadsafe function.

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

🌐 Finally, let's add the On method to allow JavaScript to register callback functions:

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)
});

// ... 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();
}

这允许 JavaScript 为特定事件类型注册回调函数。

🌐 This allows JavaScript to register callbacks for specific event types.

搭建桥接器

🌐 Putting the bridge together

现在我们已经准备好了所有部分。

🌐 Now we have all the pieces in place.

完整实现如下:

🌐 Here's the complete implementation:

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::Object Init(Napi::Env env, Napi::Object exports) {
return CppAddon::Init(env, exports);
}

NODE_API_MODULE(cpp_addon, Init)

6) 创建一个 JavaScript 封装器

🌐 6) Creating a JavaScript Wrapper

让我们在 js/index.js 中通过添加一个 JavaScript 封装器来完成最后的工作。正如我们所看到的,C++ 需要大量样板代码,而这些代码在 JavaScript 中可能更容易或更快编写——你会发现许多生产应用最终会在调用本地代码之前在 JavaScript 中转换数据或请求。例如,我们会将时间戳转换为合适的 JavaScript 日期。

🌐 Let's finish things off by adding a JavaScript wrapper in js/index.js. As we could all see, C++ requires a lot of boilerplate code that might be easier or faster to write in JavaScript - and you will find that many production applications end up transforming data or requests in JavaScript before invoking native code. We, for instance, turn our timestamp into a proper JavaScript date.

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()
}

#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 = {}
}

7)构建和测试插件

🌐 7) Building and Testing the Addon

所有文件就绪后,即可构建插件:

🌐 With all files in place, you can build the addon:

npm run build

结论

🌐 Conclusion

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

🌐 You've now built a complete native Node.js addon for Windows using C++ and the Win32 API. Some of the things we've done here are:

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

这为在你的 Electron 应用中构建更复杂的 Windows 特定功能提供了基础,让你同时拥有两全其美的优势:网页技术的便利与本地代码的强大功能。

🌐 This provides a foundation for building more complex Windows-specific features in your Electron apps, giving you the best of both worlds: the ease of web technologies with the power of native code.

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

🌐 For more information on working with Win32 API, refer to the Microsoft C++, C, and Assembler documentation and the Windows API reference.