原文 WPF 同一窗口内的多线程/多进程 UI(使用 SetParent 嵌入另一个窗口)

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,以避免陈旧错误知识的误导,同时有更好的阅读体验。

WPF 同一窗口内的多线程/多进程 UI(使用 SetParent 嵌入另一个窗口)的更多相关文章

  1. WPF 同一窗口内的多线程 UI(VisualTarget)

    WPF 的 UI 逻辑只在同一个线程中,这是学习 WPF 开发中大家几乎都会学习到的经验.如果希望做不同线程的 UI,大家也会想到使用另一个窗口来实现,让每个窗口拥有自己的 UI 线程.然而,就不能让 ...

  2. C#关闭一个窗口的同时打开另一个窗口

    在.net的WinForm程序中,如果是直接起动的Form作为主窗口,那么这个主窗口是不能关闭的,因为它维护了一个Windows消息循环,它一旦关闭了就等于声明整个应用程序结束,所以新打开的窗口也就被 ...

  3. IDEA一个窗口打开多个项目

    首先IDEA没有Eclipse的Workspace的概念,且IDEA推荐是一个窗口对应着一个Project. 然后经过研究你会发现IDEA其实是由一个主进程来维护这些窗口的,所以即使你开了很多个窗口, ...

  4. 拒绝卡顿——在WPF中使用多线程更新UI

    原文:拒绝卡顿--在WPF中使用多线程更新UI 有经验的程序员们都知道:不能在UI线程上进行耗时操作,那样会造成界面卡顿,如下就是一个简单的示例: public partial class MainW ...

  5. C# Wpf异步修改UI,多线程修改UI(二)

    1.使用定时器异步修改 这是相对比较简单的方法 在Wpf中定时器使用DiapatcherTimer,不使用Timer原因: 在一个应用程序中,Timer会重复生成time事件,而DispatcherT ...

  6. WPF里面多线程访问UI线程、主线程的控件

    如果出现以下错误:调用线程无法访问此对象,因为另一个线程拥有该对象. 你就碰到多线程访问UI线程.主线程的控件的问题了. 先占位.

  7. C++程序员面试题目总结(涉及C++基础、多线程多进程、网络编程、数据结构与算法)

     说明:C++程序员面试题目总结(涉及C++基础知识.多线程多进程.TCP/IP网络编程.Linux操作.数据结构与算法) 内容来自作者看过的帖子或者看过的文章,个人整理自互联网,如有侵权,请联系作者 ...

  8. Android多线程更新UI的方式

    Android下,对于耗时的操作要放到子线程中,要不然会残生ANR,本次我们就来学习一下Android多线程更新UI的方式. 首先我们来认识一下anr: anr:application not rep ...

  9. Python多线程多进程那些事儿看这篇就够了~~

    自己以前也写过多线程,发现都是零零碎碎,这篇写写详细点,填一下GIL和Python多线程多进程的坑~ 总结下GIL的坑和python多线程多进程分别应用场景(IO密集.计算密集)以及具体实现的代码模块 ...

随机推荐

  1. 使用DNSCrypt解决Dropbox污染问题

     作者:半点闲 时间:2014-6-27 18:27 博客:blog.csdn.net/cg_i 邮箱:b_dx@sohu.com 背景知识:防火长城(GFW) keyword:DNSCrypt ...

  2. [Node] Setup an Nginx Proxy for a Node.js App

    Learn how to setup an Nginx proxy server that sits in front of a Node.js app. You can use a proxy to ...

  3. 【Codeforces Round #185 (Div. 2) C】The Closest Pair

    [链接] 链接 [题意] 让你构造n个点,去hack一种求最近点对的算法. [题解] 让x相同. 那么那个剪枝就不会起作用了. [错的次数] 在这里输入错的次数 [反思] 在这里输入反思 [代码] # ...

  4. java 封装解析 Json数据。

    import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; im ...

  5. “locktype”enum type 类型重定义问题的解决

    作者:朱金灿 来源:http://blog.csdn.net/clever101 使用ado来连接数据库,结果出现这样一些编译错误: 1>f:\c++pro\iocptser\debug\msa ...

  6. 一次svn数据库的崩溃错误的解决

    作者:朱金灿 来源:. 然后再更新svn数据库,依然出现上面提到的错误.于是又想到新建一个svn数据库,然后将旧库备份出来再导入到新库中,运行命令:svnadmin dump E:\Repositor ...

  7. jquery ajax实现省市二级联动

    今天给大家带来使用jQuery ajax实现的省市联动效果.我们直奔主题,先说下实现思路: 准备数据 这里数据库我使用的是mysql,先看下表格: provience表 city表 这里使用provi ...

  8. 【Lucene4.8教程之三】搜索 2014-06-21 09:53 1532人阅读 评论(0) 收藏

    1.关键类 Lucene的搜索过程中涉及的主要类有以下几个: (1)IndexSearcher:执行search()方法的类 (2)IndexReader:对索引文件进行读操作,并为IndexSear ...

  9. 9个完整android开源app项目

    一.photoup 介绍: photoup 是一款开源的相册类app,主要功能是将本地图片提交到facebook上去,虽然他的功能和facebook的远程服务相关,但是本身是可以被当作一款 相册应用的 ...

  10. auto_create_partition

    #!/usr/bin/env python # -*- encoding: utf8 -*- import calendar import time import os import sys from ...