突破Web限制,在 Electron 窗口中嵌入 Windows 桌面应用程序(附源码)

214 阅读4分钟

🎯 核心功能

在 Electron 窗口中嵌入 Windows 桌面应用程序,实现应用程序的"窗口化"管理。

动态效果:

PixPin_2025-11-05_12-06-28.gif

🔧 技术架构

这个项目是一个典型的“混合”应用,由三大部分构成:

  1. Electron 应用层 (JavaScript/HTML):

    • 这是用户交互的界面。
    • 使用 Electron 创建一个主窗口 (main.js)。
    • 主窗口加载一个 HTML 页面 (index.html),该页面提供表单让用户输入要启动的应用程序路径,并通过按钮来创建、管理和销毁嵌入的窗口。
    • 通过 IPC (Inter-Process Communication) 机制与主进程通信。
  2. Node.js 原生插件层 (C++/N-API):

    • 这是项目的核心,用 C++ 编写,编译成 .node 文件供 Node.js 调用。
    • 它暴露了一系列 JavaScript 可调用的函数(如 createEmbeddedWindow, destroyWindow 等),这些函数直接操作 Windows API。
    • 它充当了 Electron 应用与 Windows 操作系统之间的桥梁。
  3. Windows 系统层 (Win32 API):

    • 原生插件最终调用 Windows 提供的底层 API 来实现具体功能,例如创建进程、查找窗口句柄、设置窗口父子关系等。

工作流程简述

  1. 用户在 Electron 界面点击“创建窗口”。
  2. render.js 通过 ipcRenderer.invoke() 发送请求到 main.js
  3. main.js 创建一个新的 BrowserWindow,并加载一个空白的、透明的 blank.html 作为“容器”。
  4. main.js 获取这个新窗口的原生句柄 (Native Window Handle),并通过 IPC 将其连同应用程序路径等参数一起传递给原生插件 BrowserWindowTool.node
  5. 原生插件的 C++ 代码接收到句柄后,开始执行一系列 Win32 API 操作,将目标应用程序的窗口“嵌入”到这个 Electron 窗口里。
  6. 最终,用户看到的是一个 Electron 窗口中运行着另一个独立的桌面应用。

image-20251105105003418.png

二、 核心技术点详解

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 composition
    
    同时,在创建 BrowserWindow 时,明确设置 transparent: falsebackgroundColor: '#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)

这是最核心的逻辑,步骤如下:

  1. 准备父窗口: PrepareParentWindow() 函数检查传入的 Electron 窗口句柄,并修改其样式(如添加 WS_CLIPCHILDREN),为接收子窗口做准备。
  2. 创建容器窗口: CreateContainerWindow() 在 Electron 窗口内部创建一个子窗口(HWND),这个子窗口就是未来用来承载外部应用的“容器”。
  3. 启动外部进程: LaunchProcess() 使用 CreateProcessW() 函数启动用户指定的应用程序(如 notepad.exe)。这里使用 SW_HIDE 参数让进程启动但不显示窗口,避免出现两个窗口。
  4. 查找目标窗口: FindAndEmbedWindow() 是关键。它通过 EnumWindows() 枚举所有顶层窗口,并根据 processId 找到刚刚启动的进程所创建的主窗口句柄。同时,它会过滤掉控制台窗口等非目标窗口。
  5. 窗口父子化: 找到目标窗口句柄后,进行一系列操作:
    • 修改目标窗口的样式:移除 WS_POPUP, WS_CAPTION (标题栏), WS_THICKFRAME (边框),并添加 WS_CHILD,使其变成子窗口。
    • 移除扩展样式中的边框效果。
    • 使用 SetParent(targetWindow, containerWindow) 将外部应用的窗口设置为 Electron 内部容器窗口的子窗口。
    • 调整大小 (SetWindowPos) 并显示 (ShowWindow),使其完美填充容器。
  6. 管理映射: 使用 std::map<std::string, std::shared_ptr<EmbeddedProcess>> processes_ 来存储每个嵌入窗口的信息(ID、进程信息、窗口句柄等),以便后续进行更新、显示/隐藏或销毁操作。

4. 跨进程通信 (IPC)

Electron 的主进程 (main.js) 和渲染进程 (render.js) 之间通过 ipcMain.handleipcRenderer.invoke 进行异步通信。主进程负责调用原生插件,并将结果返回给前端。

参考

把原生窗口嵌入到Electron BrowserWindow内

⚠️ 技术限制

  • 平台限制:仅限于 Windows 系统。
  • 稳定性:依赖于禁用 GPU 加速,可能影响性能和某些图形效
  • 兼容性性: 某些应用程序可能不支持嵌入

仓库地址

⚠️ 目的是演示效果,bug还挺多的,请不要直接用于生产。