前言

由於目前正在開發一個基於Electron的,模仿瀏覽器的多標籤頁管理機制,想實現BrowserWindow拖出來為原窗口這種效果,因此稍微在網路上搜尋了一下,發現沒有現成的答復。但是這個Issue給了我一些啟發:Embed External Native Windows - electron

研究

最開始的嘗試

使用一些簡單的Tricks實現了Windows環境下Electron中的子窗口嵌入。由於Electron並沒有提供真正的窗口嵌入的API,因此這個方法通過C#調用Win32API,來實現子窗口嵌入。目前只論證了原理上的可行性,至於後續是否有其他的問題和Bug還需後續繼續探索。

先使用WinSpy++找到父窗口的Chrome_RenderWidgetHostHWND,複製下它的句柄(Handle)。

WinSpy++ 演示

再找到子窗口本身(Class: Chrome_WidgetWin_1),複製下它的句柄。

打開Visual Studio,創建一個控制台C#解決方案,在源文件中鍵入以下代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
using System;
using System.Runtime.InteropServices;

class Program
{
[DllImport("user32.dll")]
static extern IntPtr SetParent(IntPtr hWndChild, IntPtr hWndNewParent);

[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool MoveWindow(IntPtr hWnd, int X, int Y, int nWidth, int nHeight, bool bRepaint);

[DllImport("user32.dll")]
static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);

[DllImport("user32.dll")]
static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);

static void Main(string[] args)
{
// 替换为您的父窗口Chrome_RenderWidgetHostHWND句柄
IntPtr parentHandle = (IntPtr)0x002D0C58;
// 替换为您的子窗口Chrome_WidgetWin_1句柄
IntPtr childHandle = (IntPtr)0x001F092A;

// 将子窗口嵌入到父窗口中
SetParent(childHandle, parentHandle);

// 移动子窗口到父窗口的相对位置 (100, 100),并设置大小为 (200, 150)
MoveWindow(childHandle, 100, 100, 200, 150, true);

// 设置子窗口为始终位于顶部
SetWindowPos(childHandle, new IntPtr(-1), 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE | SWP_NOACTIVATE | SWP_NOZORDER);

// 显示子窗口
ShowWindow(childHandle, SW_SHOW);
}

const int SW_SHOW = 5;
const uint SWP_NOMOVE = 0x0002;
const uint SWP_NOSIZE = 0x0001;
const uint SWP_NOACTIVATE = 0x0010;
const uint SWP_NOZORDER = 0x0004;
}

為什麼不將父窗口的句柄也使用父窗口的Chrome_WidgetWin_1呢?經過測試,當父窗口被其他窗口完全遮擋后會導致子窗口無法響應任何Input事件(也可能是我的問題)。

在Electron Fiddle中鍵入以下內容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})

const subWindow = new BrowserWindow({
width: 800,
height: 600,
title: "subWin",
alwaysOnTop: true,
frame: false,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})

// and load the index.html of the app.
mainWindow.loadFile('index.html')
subWindow.loadFile('index.html')

注意子窗口的alwaysOnTop不能設置為false,不然在切換焦點後會導致窗口隱藏。

接下來是效果圖:

这是效果图

獲取父窗口句柄

那麼,問題來了,要用代碼實現獲取父窗口的Chrome_RenderWidgetHostHWND的句柄,該怎麼寫呢?

我們可以嘗試使用EnumChildWindows函數。透過依序將每個子視窗的句柄傳遞給應用程式定義的回呼函數,枚舉屬於指定父視窗的子視窗。EnumChildWindows繼續下去,直到枚舉最後一個子視窗或回呼函數返回FALSE。那麼就可以使用EnumChildWindows獲取父窗口的所有子窗口,然後根據每個子窗口的Class進行篩選,使用GetClassName函数可以檢索指定視窗所屬的類別的名稱。

但是,這裡其實有更好的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 替换为您的父窗口句柄
IntPtr parentWindowHandle = (IntPtr)0x001B0BA6;

// 替换为子窗口句柄
IntPtr childHandle = (IntPtr)0x005309AA;

// 找到父窗口中class为"Chrome_RenderWidgetHostHWND"的子窗口
IntPtr parentHandle = FindWindowEx(parentWindowHandle, IntPtr.Zero, "Chrome_RenderWidgetHostHWND", null);

if (parentHandle == IntPtr.Zero)
{
Console.WriteLine("未找到符合条件的Chrome_RenderWidgetHostHWND!");
return;
}

其實可以直接使用FindWindowEx函數的!

改進一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
using System;
using System.Runtime.InteropServices;
using System.Text;

class Program
{
// 导入Win32 API中的FindWindowEx和GetClassName函数
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
public static extern IntPtr FindWindowEx(IntPtr hwndParent, IntPtr hwndChildAfter, string lpszClass, string lpszWindow);

[DllImport("user32.dll", CharSet = CharSet.Unicode)]
public static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount);

[DllImport("user32.dll", CharSet = CharSet.Unicode)]
public static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount);

static void Main(string[] args)
{
// 假设你已经有了父窗口的句柄
IntPtr parentWindowHandle = new IntPtr(0x0006082E); // 替换成实际的父窗口句柄

// 查找第一个子窗口句柄
IntPtr childWindowHandle = FindChildWindow(parentWindowHandle);
if (childWindowHandle != IntPtr.Zero)
{
Console.WriteLine("Found child window: " + childWindowHandle);
}
else
{
Console.WriteLine("No child window found.");
}
}

// 根据类名和窗口标题查找子窗口
public static IntPtr FindChildWindow(IntPtr parentHandle)
{
return FindChildWindowRecursive(parentHandle);
}

// 递归搜索子窗口
private static IntPtr FindChildWindowRecursive(IntPtr parentHandle)
{
IntPtr childHandle = IntPtr.Zero;
IntPtr currentChildHandle = IntPtr.Zero;


// 查找第一个子窗口
currentChildHandle = FindWindowEx(parentHandle, IntPtr.Zero, null, null);

// 循环查找后续子窗口
while (currentChildHandle != IntPtr.Zero)
{
// 获取当前子窗口的类名
StringBuilder currentClassName = new StringBuilder(256);
GetClassName(currentChildHandle, currentClassName, currentClassName.Capacity);

// 获取当前子窗口的窗口标题
StringBuilder windowTitle = new StringBuilder(256);
GetWindowText(currentChildHandle, windowTitle, windowTitle.Capacity);

Console.WriteLine("found a window!");
Console.WriteLine(currentClassName);

// 检查类名或窗口标题是否包含指定字符串
if (currentClassName.ToString().Contains("RenderWidgetHostHWND") || windowTitle.ToString().Contains("Chrome Legacy Window"))
{
childHandle = currentChildHandle;
break;
}

// 递归搜索当前子窗口的子窗口
childHandle = FindChildWindowRecursive(currentChildHandle);
if (childHandle != IntPtr.Zero)
{
break;
}

// 继续查找下一个子窗口
currentChildHandle = FindWindowEx(parentHandle, currentChildHandle, null, null);
}

return childHandle;
}
}

找不到有效子窗口的問題

只需要在初始化Electron的時候,把BrowserWindow中的選項添加一個backgroundThrottling:false就行了。他決定是否當頁面不在前台顯示時限制動畫和計時器。也正是因為這個選項的默認開啟而導致窗口在被遮擋住後,Chrome_RenderWidgetHostHWND就無法被找到了。我們無法使用FindWindowEx或者EnumChildWindows等方法找到這個子窗口。

思維擴展

不止可以嵌入Electron窗口,所有窗口嵌入到這個Chrome_RenderWidgetHostHWND子窗口後,只需要設置AlwaysOnTop後就可以完全正常的工作了。比如Powerpoint,Chrome等。

嘗試使用koffi

什麼是koffi?Koffi 是一個快速且易於使用的 Node.js C FFI 模組,低開銷和快速效能,並且無需任何操作即可默認支援Electron,再也不要使用ffi-napi了!koffi更好用!

正在更新..


參考

  1. WPF 同一窗口内的多线程/多进程 UI(使用 SetParent 嵌入另一个窗口)
  2. SetParent 函数 (winuser.h)
  3. BrowserWindow - Electron
  4. [Solved] EnumChildWindows doesn’t find all child windows
  5. Koffi