🎯 核心功能
在 Electron 窗口中嵌入 Windows 桌面应用程序,实现应用程序的"窗口化"管理。
动态效果:
🔧 技术架构
这个项目是一个典型的“混合”应用,由三大部分构成:
-
Electron 应用层 (JavaScript/HTML):
- 这是用户交互的界面。
- 使用 Electron 创建一个主窗口 (
main.js)。 - 主窗口加载一个 HTML 页面 (
index.html),该页面提供表单让用户输入要启动的应用程序路径,并通过按钮来创建、管理和销毁嵌入的窗口。 - 通过 IPC (Inter-Process Communication) 机制与主进程通信。
-
Node.js 原生插件层 (C++/N-API):
- 这是项目的核心,用 C++ 编写,编译成
.node文件供 Node.js 调用。 - 它暴露了一系列 JavaScript 可调用的函数(如
createEmbeddedWindow,destroyWindow等),这些函数直接操作 Windows API。 - 它充当了 Electron 应用与 Windows 操作系统之间的桥梁。
- 这是项目的核心,用 C++ 编写,编译成
-
Windows 系统层 (Win32 API):
- 原生插件最终调用 Windows 提供的底层 API 来实现具体功能,例如创建进程、查找窗口句柄、设置窗口父子关系等。
工作流程简述:
- 用户在 Electron 界面点击“创建窗口”。
render.js通过ipcRenderer.invoke()发送请求到main.js。main.js创建一个新的BrowserWindow,并加载一个空白的、透明的blank.html作为“容器”。main.js获取这个新窗口的原生句柄 (Native Window Handle),并通过 IPC 将其连同应用程序路径等参数一起传递给原生插件BrowserWindowTool.node。- 原生插件的 C++ 代码接收到句柄后,开始执行一系列 Win32 API 操作,将目标应用程序的窗口“嵌入”到这个 Electron 窗口里。
- 最终,用户看到的是一个 Electron 窗口中运行着另一个独立的桌面应用。
二、 核心技术点详解
1. 禁用 GPU 加速与 DirectComposition
这是该项目成功的关键前提。
- 问题:现代 Electron 默认使用硬件加速和 DirectComposition (D3D) 来渲染 UI,这会将内容绘制在一个独立的 GPU 层上。而 Win32 API 操作的窗口是在 GDI 层。这两个层级是分离的,无法直接嵌套。
- 解决方案:在
main.js中,通过以下命令行开关强制 Electron 回退到传统的 GDI 渲染模式:同时,在创建app.commandLine.appendSwitch('in-process-gpu') app.commandLine.appendSwitch('disable-gpu-sandbox') app.commandLine.appendSwitch('disable-direct-composition') // 👈 关键:禁用 D3D compositionBrowserWindow时,明确设置transparent: false和backgroundColor: '#ffffff',确保没有创建透明或离屏渲染的复杂场景。这样,Electron 窗口就变成了一个标准的、可以被 Win32 API 操作的普通 Windows 窗口。
2. 原生插件 (Native Addon) 与 N-API
- binding.gyp: 这是 node-gyp 的配置文件,告诉编译器如何将 C++ 源码 (
src/main.cc,src/WindowManager.cc) 编译成 Node.js 可以加载的二进制模块 (BrowserWindowTool.node)。 - N-API: 项目使用了 Node.js 的 N-API (
NODE_ADDON_API_ENABLE_MAYBE)。这是一种稳定的 C API,用于构建原生插件。它的好处是向后兼容性好,减少了因 Node.js 版本升级导致插件失效的风险。
3. 窗口嵌入 (Window Embedding) 的实现 (WindowManager.cc)
这是最核心的逻辑,步骤如下:
- 准备父窗口:
PrepareParentWindow()函数检查传入的 Electron 窗口句柄,并修改其样式(如添加WS_CLIPCHILDREN),为接收子窗口做准备。 - 创建容器窗口:
CreateContainerWindow()在 Electron 窗口内部创建一个子窗口(HWND),这个子窗口就是未来用来承载外部应用的“容器”。 - 启动外部进程:
LaunchProcess()使用CreateProcessW()函数启动用户指定的应用程序(如notepad.exe)。这里使用SW_HIDE参数让进程启动但不显示窗口,避免出现两个窗口。 - 查找目标窗口:
FindAndEmbedWindow()是关键。它通过EnumWindows()枚举所有顶层窗口,并根据processId找到刚刚启动的进程所创建的主窗口句柄。同时,它会过滤掉控制台窗口等非目标窗口。 - 窗口父子化: 找到目标窗口句柄后,进行一系列操作:
- 修改目标窗口的样式:移除
WS_POPUP,WS_CAPTION(标题栏),WS_THICKFRAME(边框),并添加WS_CHILD,使其变成子窗口。 - 移除扩展样式中的边框效果。
- 使用
SetParent(targetWindow, containerWindow)将外部应用的窗口设置为 Electron 内部容器窗口的子窗口。 - 调整大小 (
SetWindowPos) 并显示 (ShowWindow),使其完美填充容器。
- 修改目标窗口的样式:移除
- 管理映射: 使用
std::map<std::string, std::shared_ptr<EmbeddedProcess>> processes_来存储每个嵌入窗口的信息(ID、进程信息、窗口句柄等),以便后续进行更新、显示/隐藏或销毁操作。
4. 跨进程通信 (IPC)
Electron 的主进程 (main.js) 和渲染进程 (render.js) 之间通过 ipcMain.handle 和 ipcRenderer.invoke 进行异步通信。主进程负责调用原生插件,并将结果返回给前端。
参考
把原生窗口嵌入到Electron BrowserWindow内
⚠️ 技术限制
- 平台限制:仅限于 Windows 系统。
- 稳定性:依赖于禁用 GPU 加速,可能影响性能和某些图形效
- 兼容性性: 某些应用程序可能不支持嵌入
⚠️ 目的是演示效果,bug还挺多的,请不要直接用于生产。