Skip to main content

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

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

本教程基于 原生代码和 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 版本构建原生界面所带来的诸多风险!

    ¥comctl32.lib, which contains common controls and user interface components. It provides various UI elements like buttons, scrollbars, toolbars, status bars, progress bars, and tree views. As far as GUI development on Windows goes, this library is very low-level and basic - more modern frameworks like WinUI or WPF are advanced and alternatives but require a lot more C++ and Windows version considerations than are useful for this tutorial. This way, we can avoid the many perils of building native interfaces for multiple Windows versions!

  • shcore.lib 是一个库,提供高 DPI 感知功能以及其他与 Shell 相关的功能,用于管理显示器和 UI 元素。

    ¥shcore.lib, a library that provides high-DPI awareness functionality and other Shell-related features around managing displays and UI elements.

本教程尤其适合那些已经熟悉 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++ GUI 开发,我们推荐你参考微软优秀的文档和指南,尤其是针对初学者的文档和指南。“开始使用 Win32 和 C++”是一个很好的介绍。

¥[!NOTE] If you're not familiar with C++ GUI development on Windows, we recommend Microsoft's excellent documentation and guides, particular for beginners. "Get Started with Win32 and C++" is a great introduction.

要求

¥Requirements

与我们的 原生代码和 Electron 简介 一样,本教程假设你已安装 Node.js 和 npm,以及编译原生代码所需的基本工具。由于本教程讨论的是编写与 Windows 交互的原生代码,我们建议你在安装了 Visual Studio 和 "使用 C++ 工作负载进行桌面开发" 的 Windows 系统上学习本教程。有关详细信息,请参阅 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.

*

你可以重复使用我们在 原生代码和 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"
}
}

*

对于 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 上编译,因为我们将编写特定于平台的代码。

    ¥We need to ensure our addon is only compiled on Windows, since we'll be writing platform-specific code.

  2. 我们需要包含 Windows 特定的库。在本教程中,我们将针对 comctl32.libshcore.lib

    ¥We need to include the Windows-specific libraries. In our tutorial, we'll be targeting comctl32.lib and shcore.lib.

  3. 我们需要配置编译器并定义 C++ 宏。

    ¥We need to configure the compiler and define C++ macros.

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 特定的设置。

¥msvs_settings provide Visual Studio-specific settings.

VCCLCompilerTool 设置

¥VCCLCompilerTool Settings

binding.gyp
"VCCLCompilerTool": {
"ExceptionHandling": 1,
"DebugInformationFormat": "OldStyle",
"AdditionalOptions": [
"/FS"
]
}
  • ExceptionHandling: 1:这将使用 /EHsc 编译器标志启用 C++ 异常处理。这很重要,因为它使编译器能够捕获 C++ 异常,确保在发生异常时正确展开堆栈,并且是 Node-API 正确处理 JavaScript 和 C++ 之间异常所必需的。

    ¥ExceptionHandling: 1: This enables C++ exception handling with the /EHsc compiler flag. This is important because it enables the compiler to catch C++ exceptions, ensures proper stack unwinding when exceptions occur, and is required for Node-API to properly handle exceptions between JavaScript and C++.

  • DebugInformationFormat: "OldStyle":这指定了调试信息的格式,使用更旧、更兼容的 PDB(程序数据库)格式。这支持与各种调试工具的兼容性,并且与增量构建配合得更好。

    ¥DebugInformationFormat: "OldStyle": This specifies the format of debugging information, using the older, more compatible PDB (Program Database) format. This supports compatibility with various debugging tools and works better with incremental builds.

  • AdditionalOptions: ["/FS"]:这将添加文件序列化标志,强制在编译期间对 PDB 文件进行序列化访问。它可以防止在并行构建中多个编译器进程尝试访问同一个 PDB 文件时出现构建错误。

    ¥AdditionalOptions: ["/FS"]: This adds the File Serialization flag, forcing serialized access to PDB files during compilation. It prevents build errors in parallel builds where multiple compiler processes try to access the same PDB file.

VCLinkerTool 设置

¥VCLinkerTool Settings

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

    ¥GenerateDebugInformation: "true": This tells the linker to include debug information, which allows source-level debugging in tools that use symbols. Most importantly, this will allow us to get human-readable stack traces if the addon crashes.

预处理器宏 (defines):

¥Preprocessor macros (defines):

  • NODE_ADDON_API_CPP_EXCEPTIONS:此宏在 Node 插件 API 中启用 C++ 异常处理。默认情况下,Node-API 使用返回值错误处理模式,但此定义允许 C++ 封装器抛出并捕获 C++ 异常,这使得代码更符合 C++ 的惯用语法,也更易于使用。

    ¥NODE_ADDON_API_CPP_EXCEPTIONS: This macro enables C++ exception handling in the Node Addon API. By default, Node-API uses a return-value error handling pattern, but this define allows the C++ wrapper to throw and catch C++ exceptions, which makes the code more idiomatic C++ and easier to work with.

  • WINVER=0x0A00:这定义了代码所针对的最低 Windows 版本。0x0A00 的值对应于 Windows 10。设置此项可告知编译器代码可以使用 Windows 10 中的可用功能,并且不会尝试保持与早期 Windows 版本的向后兼容性。请确保将其设置为你计划使用 Electron 应用支持的最低 Windows 版本。

    ¥WINVER=0x0A00: This defines the minimum Windows version that the code is targeting. The value 0x0A00 corresponds to Windows 10. Setting this tells the compiler that the code can use features available in Windows 10, and it won't attempt to maintain backward compatibility with earlier Windows versions. Make sure to set this to the lowest version of Windows you intend to support with your Electron app.

  • _WIN32_WINNT=0x0A00 - 与 WINVER 类似,这定义了代码将在其上运行的 Windows NT 内核的最低版本。同样,0x0A00 对应 Windows 10。这通常设置为与 WINVER 相同的值。

    ¥_WIN32_WINNT=0x0A00 - Similar to WINVER, this defines the minimum version of the Windows NT kernel that the code will run on. Again, 0x0A00 corresponds to Windows 10. This is commonly set to the same value as WINVER.

*

让我们在 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 函数

    ¥Includes the basic hello_world function from the general tutorial

  • 添加一个 hello_gui 函数来创建 Win32 GUI

    ¥Adds a hello_gui function to create a Win32 GUI

  • 定义 Todo 操作(add)的回调类型。为了使本教程简洁明了,我们将只实现一个回调函数。

    ¥Defines callback types for Todo operations (add). To keep this tutorial somewhat brief, we'll only be implementing one callback.

  • 为这些回调提供设置函数

    ¥Provides setter functions for these callbacks

*

现在,让我们在 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 头文件

    ¥We include necessary Win32 headers

  • 我们设置了注释来链接到所需的库。

    ¥We set up pragma comments to link against required libraries

  • 我们为 Todo 操作定义回调变量

    ¥We define callback variables for Todo operations

  • 我们创建一个 TodoItem 结构体,并包含一个转换为 JSON 的方法

    ¥We create a TodoItem struct with a method to convert to JSON

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

¥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 设置自己的线程。

    ¥We need to create a new thread for the GUI to avoid blocking the Node.js event loop. The Windows message loop that processes GUI events runs in an infinite loop, which would prevent Node.js from processing other events if run on the main thread. By running the GUI on a separate thread, we allow both the native Windows interface and Node.js to remain responsive. This separation also helps prevent potential deadlocks that could occur if GUI operations needed to wait for JavaScript callbacks. You don't need to do that for simpler Windows API interactions - but since you need to check the message loop, you do need to setup your own thread for GUI.

  • 然后,我们需要在线程中运行一个消息循环来处理所有 Windows 消息。

    ¥Then, within our thread, we need to run a message loop to handle any Windows messages.

  • 我们需要设置 DPI 感知以实现正确的显示缩放。

    ¥We need to setup DPI awareness for proper display scaling.

  • 我们需要注册一个窗口类,创建一个窗口,并添加各种 UI 控件。

    ¥We need to register a window class, create a window, and add various UI controls.

在下面的代码中,我们尚未添加任何实际的控件。我们这样做是为了在这里以较小的部分来查看我们添加的代码。

¥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

*

现在让我们在 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. 添加私有成员来存储回调函数

    ¥Add private members to store callbacks

  2. 创建一个线程安全函数用于跨线程通信

    ¥Create a threadsafe function for cross-thread communication

  3. 添加一个 On 方法来注册 JavaScript 回调函数

    ¥Add an On method to register JavaScript callbacks

  4. 设置将触发 JavaScript 回调的 C++ 回调

    ¥Set up C++ callbacks that will trigger the JavaScript callbacks

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)

*

让我们在 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 = {}
}

*

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

¥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 things we've done here are:

  1. 使用 C++ 创建原生 Windows GUI

    ¥Creating a native Windows GUI from C++

  2. 实现具有添加、编辑和删除功能的待办事项列表应用

    ¥Implementing a Todo list application with Add, Edit, and Delete functionality

  3. C++ 和 JavaScript 之间的双向通信

    ¥Bidirectional communication between C++ and JavaScript

  4. 使用 Win32 控件和 Windows 特定功能

    ¥Using Win32 controls and Windows-specific features

  5. 从 C++ 线程安全地回调到 JavaScript

    ¥Safely calling back into JavaScript from C++ threads

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

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