前言 由於目前正在開發一個基於Electron的,模仿瀏覽器的多標籤頁管理機制,想實現BrowserWindow拖出來為原窗口這種效果,因此稍微在網路上搜尋了一下,發現沒有現成的答復。但是這個Issue給了我一些啟發:Embed External Native Windows - electron 。
研究 最開始的嘗試 使用一些簡單的Tricks實現了Windows環境下Electron中的子窗口嵌入。由於Electron並沒有提供真正的窗口嵌入的API,因此這個方法通過C#調用Win32API,來實現子窗口嵌入。目前只論證了原理上的可行性,至於後續是否有其他的問題和Bug還需後續繼續探索。
先使用WinSpy++找到父窗口的Chrome_RenderWidgetHostHWND
,複製下它的句柄(Handle)。
再找到子窗口本身(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 ) { IntPtr parentHandle = (IntPtr)0x002D0C58 ; IntPtr childHandle = (IntPtr)0x001F092A ; SetParent(childHandle, parentHandle); 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' ) } }) 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 ; 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 { [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
更好用!
正在更新..
參考
WPF 同一窗口内的多线程/多进程 UI(使用 SetParent 嵌入另一个窗口)
SetParent 函数 (winuser.h)
BrowserWindow - Electron
[Solved] EnumChildWindows doesn’t find all child windows
Koffi