WPF 同一窗口內的多線程/多進程 UI(使用 SetParent 嵌入另一個窗口)
發佈於 2018-07-11 13:35 更新於 2018-07-12 11:44
WPF 的 UI 邏輯只在同一個線程中,這是學習 WPF 開發中大家幾乎都會學習到的經驗。如果希望做不同線程的 UI,大家也會想到使用另一個窗口來實現,讓每個窗口擁有自己的 UI 線程。然而,就不能讓同一個窗口內部使用多個 UI 線程嗎?
閱讀本文將收穫一份 Win32 函數 SetParent
及相關函數的使用方法。
WPF 同一個窗口中跨線程訪問 UI 有多種方法:
前者使用的是 WPF 原生方式,做出來的跨線程 UI 可以和原來的 UI 相互重疊遮擋。後者使用的是 Win32 的方式,實際效果非常類似 WindowsFormsHost
,新線程中的 UI 在原來的所有 WPF 控件上面遮擋。另外,後者不止可以是跨線程,還可以跨進程。
準備必要的 Win32 函數
完成基本功能所需的 Win32 函數是非常少的,只有 SetParent
和 MoveWindow
。
[DllImport("user32.dll")] public static extern bool SetParent(IntPtr hWnd, IntPtr hWndNewParent); [DllImport("user32.dll", SetLastError = true)] public static extern bool MoveWindow(IntPtr hWnd, int X, int Y, int nWidth, int nHeight, bool bRepaint);
SetParent
用於指定傳統的窗口父子關係。有多傳統呢?呃……就是 Windows 自誕生以來的那種傳統。在傳統的 Win32 應用程序中,每一個控件都有自己的窗口句柄,它們之間通過 SetParent
進行連接;可以說一個 Button 就是一個窗口。而我們現在使用 SetParent
其實就是在使用傳統 Win32 程序中的控件的機制。
MoveWindow
用於指定窗口相對於其父級的位置,我們使用這個函數來決定新嵌入的窗口在原來界面中的位置。
啓動後臺 UI 線程
啓動一個後臺的 WPF UI 線程網上有不少線程的方法,但大體思路是一樣的。我之前在 如何實現一個可以用 await 異步等待的 Awaiter 一文中寫了一個利用 async
/await
做的更高級的版本。
爲了繼續本文,我將上文中的核心文件抽出來做成了 GitHubGist,訪問 Custom awaiter with background UI thread 下載那三個文件並放入到自己的項目中。
AwaiterInterfaces.cs
爲實現 async/await 機制準備的一些接口,雖然事實上可以不需要,不過加上可以防逗比。DispatcherAsyncOperation.cs
這是我自己實現的自定義 awaiter,可以利用 awaiter 的回調函數機制規避線程同步鎖的使用。UIDispatcher.cs
用於創建後臺 UI 線程的類型,這個文件包含本文需要使用的核心類,使用到了上面兩個文件。
在使用了上面的三個文件的情況下,創建一個後臺 UI 線程並獲得用於執行代碼的 Dispatcher
只需要一句話:
// 傳入的參數是線程的名稱,也可以不用傳。 var dispatcher = await UIDispatcher.RunNewAsync("Background UI");
在得到了後臺 UI 線程 Dispatcher 的情況下,無論做什麼後臺線程的 UI 操作,只需要調用 dispatcher.InvokeAsync
即可。
我們使用下面的句子創建一個後臺線程的窗口並顯示出來:
var backgroundWindow = await dispatcher.InvokeAsync(() => { var window = new Window(); window.SourceInitialized += OnSourceInitialized; window.Show(); return window; });
在代碼中,我們監聽了 SourceInitialized
事件。這是 WPF 窗口剛剛獲得 Windows 窗口句柄的時機,在此事件中,我們可以最早地拿到窗口句柄以便進行 Win32 函數調用。
private void OnSourceInitialized(object sender, EventArgs e) { // 在這裏可以獲取到窗口句柄。 }
嵌入窗口
爲了比較容易寫出嵌入窗口的代碼,我將核心部分代碼貼出來:
class ParentWindow : Window { public ParentWindow() { InitializeComponent(); Loaded += OnLoaded; } private async void OnLoaded(object sender, RoutedEventArgs e) { // 獲取父窗口的窗口句柄。 var hwnd = (HwndSource) PresentationSource.FromVisual(this); _parentHwnd = hwnd; // 在後臺線程創建子窗口。 var dispatcher = await UIDispatcher.RunNewAsync("Background UI"); await dispatcher.InvokeAsync(() => { var window = new Window(); window.SourceInitialized += OnSourceInitialized; window.Show(); }); } private void OnSourceInitialized(object sender, EventArgs e) { var childHandle = new WindowInteropHelper((Window) sender).Handle; SetParent(childHandle, _parentHwnd.Handle); MoveWindow(childHandle, 0, 0, 300, 300, true); } private HwndSource _parentHwnd; [DllImport("user32.dll")] public static extern bool SetParent(IntPtr hWnd, IntPtr hWndNewParent); [DllImport("user32.dll", SetLastError = true)] public static extern bool MoveWindow(IntPtr hWnd, int X, int Y, int nWidth, int nHeight, bool bRepaint); }
具體執行嵌入窗口的是這一段:
private void OnSourceInitialized(object sender, EventArgs e) { var childHandle = new WindowInteropHelper((Window) sender).Handle; SetParent(childHandle, _parentHwnd.Handle); MoveWindow(childHandle, 0, 0, 300, 300, true); }
最終顯示時會將後臺線程的子窗口顯示到父窗口的 (0, 0, 300, 300) 的位置和大小。可以試試在主線程寫一個 Thread.Sleep(5000)
,在卡頓的事件內,你依然可以拖動子窗口的標題欄進行拖拽。
當然,如果你認爲外面那一圈窗口的非客戶區太醜了,使用普通設置窗口屬性的方法去掉即可:
await dispatcher.InvokeAsync(() => { var window = new Window { BorderBrush = Brushes.DodgerBlue, BorderThickness = new Thickness(8), Background = Brushes.Teal, WindowStyle = WindowStyle.None, ResizeMode = ResizeMode.NoResize, Content = new TextBlock { Text = "walterlv.github.io", HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center, Foreground = Brushes.White, FontSize = 24, } }; window.SourceInitialized += OnSourceInitialized; window.Show(); });
本文會經常更新,請閱讀原文: https://walterlv.com/post/embed-win32-window-using-csharp.html ,以避免陳舊錯誤知識的誤導,同時有更好的閱讀體驗。
本作品採用 知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議 進行許可。歡迎轉載、使用、重新發布,但務必保留文章署名 呂毅 (包含鏈接: https://walterlv.com ),不得用於商業目的,基於本文修改後的作品務必以相同的許可發佈。如有任何疑問,請 與我聯繫 ([email protected]) 。