原生代码与 Electron:C++ (Windows)
本教程建立在 原生代码和 Electron 的通用介绍 的基础上,专注于使用 C++ 和 Win32 API 为 Windows 创建原生插件。为了说明如何将原生的 Win32 代码嵌入到你的 Electron 应用中,我们将构建一个基本的原生 Windows GUI(使用 Windows 公共控件),它将与 Electron 的 JavaScript 进行通信。
具体来说,我们将集成两个常用的原生 Windows 库:
comctl32.lib
,它包含公共控件和用户界面组件。它提供了各种 UI 元素,如按钮、滚动条、工具栏、状态栏、进度条和树形视图。就 Windows 上的 GUI 开发而言,这个库非常底层和基础——像 WinUI 或 WPF 这样的现代框架是更高级的选择,但它们需要更多的 C++ 和 Windows 版本考虑,这对于本教程来说并不实用。这样,我们就可以避免为多个 Windows 版本构建原生界面的许多麻烦!shcore.lib
,这是一个提供高 DPI 感知功能以及其他有关管理显示器和 UI 元素的 Shell 相关功能的库。
本教程对已经熟悉 Windows 上原生 C++ GUI 开发的人来说将最有价值。你应该有使用基本窗口类和过程的经验,例如 WNDCLASSEXW
和 WindowProc
函数。你也应该熟悉 Windows 消息循环,这是任何原生应用程序的核心——我们的代码将使用 GetMessage
、TranslateMessage
和 DispatchMessage
来处理消息。最后,我们将使用(但不解释)标准的 Win32 控件,如 WC_EDITW
或 WC_BUTTONW
。
如果你不熟悉 Windows 上的 C++ GUI 开发,我们推荐微软提供的优秀文档和指南,特别是给初学者的。 《学习 Windows 和 C++ 编程》是一个很好的入门。
要求
与我们 关于原生代码和 Electron 的通用介绍 类似,本教程假设你已安装 Node.js 和 npm,以及编译原生代码所需的基本工具。由于本教程讨论编写与 Windows 交互的原生代码,我们建议你在 Windows 上按照本教程进行操作,并安装 Visual Studio 和“使用 C++ 进行桌面开发”工作负载。有关详细信息,请参阅 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
应该如下所示:
{
"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 库并设置适当的编译器标志。简而言之,我们需要做以下三件事:
- 我们需要确保我们的插件仅在 Windows 上编译,因为我们将编写特定于平台的代码。
- 我们需要包含特定于 Windows 的库。在本教程中,我们将目标设置为
comctl32.lib
和shcore.lib
。 - 我们需要配置编译器并定义 C++ 宏。
{
"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
设置
"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": {
"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
中定义我们的头文件。
#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。 - 定义用于待办事项操作(添加)的回调类型。为了使本教程相对简短,我们将只实现一个回调。
- 提供这些回调的 setter 函数。
4) 实现 Win32 GUI 代码
现在,让我们在 src/cpp_code.cc
中实现我们的 Win32 GUI。这是一个较大的文件,因此我们将分段进行审查。首先,让我们包含必要的头文件并定义基本结构。
#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 注释以链接到所需的库。
- 我们定义待办事项操作的回调变量。
- 我们创建一个
TodoItem
结构体,其中包含一个转换为 JSON 的方法。
接下来,让我们实现基本函数和辅助方法。
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
转换为自 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 控件。
在下面的代码中,我们还没有添加任何实际控件。我们这样做是为了让我们在检查添加的代码时能更小块地进行。
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!
部分。
我们特别添加了按钮、一个日期选择器和一个列表。
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() { ... }
函数的下方,我们将添加以下内容:
// 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
时间戳,我们将返回一个简单的字符串。将它添加到上面的函数下方。
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;
}
当用户添加待办事项时,我们希望将控件重置为空状态。要做到这一点,请在刚添加的代码下方添加一个辅助函数。
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(¤tTime);
DateTime_SetSystemtime(hDatePicker, GDT_VALID, ¤tTime);
}
然后,我们需要实现窗口过程来处理 Windows 消息。与我们这里的许多代码一样,这段代码很少有 Electron 特有的内容——所以作为一名 Win32 C++ 开发者,你会认出这个函数。唯一独特之处在于我们希望可能通知 JavaScript 回调有关已添加待办事项的信息。我们之前已经实现了 NotifyCallback()
函数,我们将在这里使用它。将此代码添加到上面的函数正下方。
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 之间的桥梁。这是完整的实现:
#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(¤tTime);
DateTime_SetSystemtime(hDatePicker, GDT_VALID, ¤tTime);
}
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 之间的桥梁。让我们从创建一个插件的基本骨架开始。
#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。
#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
函数。
// ... 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。
// ... 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。我们需要:
- 添加私有成员来存储回调。
- 创建一个线程安全函数用于跨线程通信。
- 添加一个
On
方法来注册 JavaScript 回调。 - 设置 C++ 回调,这些回调将触发 JavaScript 回调。
// ... 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
现在,让我们增强我们的构造函数来初始化这些成员。
// ... 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
现在让我们将线程安全函数设置添加到我们的构造函数中。
// ... 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 回调并使用提供的有效负载调用它。
现在让我们添加回调设置。
// ... 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 注册回调函数。
// ... 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 为特定事件类型注册回调。
整合桥梁
现在我们已经具备了所有要素。
这是完整的实现:
#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 包装器
让我们通过在 js/index.js
中添加一个 JavaScript 包装器来完成。正如我们所见,C++ 需要大量的样板代码,这些代码在 JavaScript 中可能更容易或更快编写——你会发现许多生产应用程序最终会在调用原生代码之前在 JavaScript 中转换数据或请求。例如,我们将时间戳转换为正确的 JavaScript 日期。
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) 构建和测试插件
在所有文件都就位后,你可以构建插件。
npm run build
结论
你现在已经使用 C++ 和 Win32 API 为 Windows 构建了一个完整的原生 Node.js 插件。我们在这里做的一些事情包括:
- 从 C++ 创建一个原生的 Windows GUI。
- 实现一个具有添加、编辑和删除功能的待办事项列表应用程序。
- C++ 和 JavaScript 之间的双向通信。
- 使用 Win32 控件和特定于 Windows 的功能。
- 从 C++ 线程安全地回调到 JavaScript。
这为在你的 Electron 应用中构建更复杂的特定于 Windows 的功能奠定了基础,让你获得两全其美:Web 技术的便捷性与原生代码的力量。
有关处理 Win32 API 的更多信息,请参阅 Microsoft C++、C 和汇编程序文档以及 Windows API 参考。