多线程UI,是winform里面是一件非常简单的事情,然而在WPF里面,想要做到跨线程的UI渲染,可就没那么简单了。
我们知道,在Winform中,我们只需要在多线程里直接new
一个新的窗口就可以实现多线程UI了。这对我们解决UI卡顿的问题有一定的帮助。
今天我们重点来学习一下,在WPF程序中,怎么去实现一个跨线程UI控件。
基本思路
在WPF中,负责线程调度的管理器是Dispatcher
,UI线程必须要在主线程中执行,而且主线程必须是单线程实例。
UI线程实际上是运行在一个消息循环
机制中,有DispatcherFrame
以及消息处理,转发等机制。
所以我们要实现多线程UI,首先需要有一个专门的UI消息处理线程;其次需要将多线程上的UI渲染合并到现有主线程上去,以便可以加载到其他的主线程UI窗体上。
具体实现方法
- 定义UI线程,直接看代码
/// <summary>
/// 管理 DynamicRenderer 线程
/// </summary>
public class DispatcherUIThread : IDisposable
{
/// <summary>
/// 启动新的线程
/// </summary>
public void Start()
{
_syncThreadStartResetEvent = new AutoResetEvent(false);
var dynamicRendererThread = new Thread(InternalRunning)
{
Priority = ThreadPriority.Highest,
IsBackground = true,
};
//设置线程为单线程
dynamicRendererThread.SetApartmentState(ApartmentState.STA);
dynamicRendererThread.Start();
//等待Dispatcher已启动
_syncThreadStartResetEvent.WaitOne();
_syncThreadStartResetEvent.Dispose();
_syncThreadStartResetEvent = null;
}
private AutoResetEvent _syncThreadStartResetEvent;
/// <summary>
/// 运行笔的线程
/// </summary>
public Dispatcher Dispatcher { get; private set; }
[SecurityCritical]
private void InternalRunning()
{
Dispatcher = Dispatcher.CurrentDispatcher;
_syncThreadStartResetEvent.Set();
//开启消息循环
Dispatcher.Run();
}
/// <inheritdoc />
public void Dispose()
{
Close();
Dispatcher = null;
}
/// <summary>
/// 关闭线程
/// </summary>
public void Close()
{
Dispatcher.InvokeShutdown();
}
}
对此段代码有不明白的,请在评论区留言。
- 跨线程UI合并
跨线程UI合并需要用到俩个关键类:HostVisual
和VisualTarget
。
HostVisual
:用于承载多线程UI的子元素宿主,该Visual处于主线程上。
VisualTarget
:用于衔接主线程和多线程UI的边界处理器。
代码如下:
//异步UI容器
public class AsyncUIContainer : UIElement
{
public AsyncUIContainer()
{
Initialize();
}
HostVisual hostVisual;
ContainerVisual Container;
Rect _hitTestRect = Rect.Empty;
/// <summary>
///
/// </summary>
void Initialize()
{
//定义主线程UI宿主
hostVisual = new HostVisual();
//开启UI线程
var thread = new DispatcherUIThread();
thread.Start();
thread.Dispatcher.Invoke(() =>
{
//在新的UI线程中,初始化Visual容器
Container = new ContainerVisual();
//先此线程的UI和主线程UI衔接
new VisualTarget(hostVisual)
{
RootVisual = Container
};
});
//将UI宿主,添加到视觉树
AddVisualChild(hostVisual);
}
//因为是多线程上的UI,所以需要使用该UI所在线程才可调度
public Dispatcher UIDispatcher
{
get
{
return Container.Dispatcher;
}
}
public VisualCollection Children
{
get
{
return Container.Children;
}
}
//重写命中测试,让多线程UI能够响应基础输入
protected override HitTestResult HitTestCore(PointHitTestParameters hitTestParameters)
{
if (_hitTestRect.Contains(hitTestParameters.HitPoint))
{
return new PointHitTestResult(this, hitTestParameters.HitPoint);
}
return base.HitTestCore(hitTestParameters);
}
//重写布局系统,让多线程UI元素能正常布局
protected override Size MeasureCore(Size availableSize)
{
if (availableSize.Height > 0 && availableSize.Width > 0 && !(double.IsInfinity(availableSize.Height) || double.IsInfinity(availableSize.Width)))
{
return availableSize;
}
return new Size(200, 200);
}
//重写布局系统,让多线程UI元素能正常布局
protected override void ArrangeCore(Rect finalRect)
{
base.ArrangeCore(finalRect);
RenderSize = finalRect.Size;
_hitTestRect = finalRect;
}
//指定起Visual的数量
protected override int VisualChildrenCount => 1;
//指定需要渲染的Visual对象
protected override Visual GetVisualChild(int index)
{
return hostVisual;
}
}
基础实现方法就差不多了,大家可以根据自己需要进行魔改。
- 如何使用
多线程的UI控件我们已经写好了,我们只需要将AsyncUIContainer
添加到我们的Xaml
中。
对于其子元素,我们需要在后台代码中添加(想要在Xaml中添加也可以,需要自己实现Xaml扩展代码,让其支持直接在xaml中编辑)。
asyncUIContainer.UIDispatcher.InvokeAsync(() =>
{
var Rectangle = new Rectangle()
{
Fill=Brushes.Red,
Height=400,
Width=400,
};
//这里必须调用Arrange方法,否则界面不会渲染。
//如果使用DrawingVisual,那么需要调用DrawingVisual的RenderOpen()方法,并主动调用渲染的绘制原语才可正常渲染。
//下面是个DrawingVisual的例子
//public void Draw()
//{
// using var drawingContext = RenderOpen();
// drawingContext.DrawGeometry(_fillBrush, null, DrawingGeometry);
//}
rectangle.Arrange(new Rect(0, 0, 400, 400));
asyncUIContainer.Children.Add(rectangle);
});
通过以上就可以显示我们的多线程UI了。
在实际业务中,如果有遇到卡主线程UI的场景,便有用武之地了。
本文会经常更新,请阅读原文: https://huchengv5.gitee.io//post/WPF-%E6%89%8B%E6%8A%8A%E6%89%8B%E6%95%99%E4%BD%A0%E5%86%99%E8%B7%A8%E7%BA%BF%E7%A8%8BUI%E6%8E%A7%E4%BB%B6.html ,以避免陈旧错误知识的误导,同时有更好的阅读体验。
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名胡承(包含链接: https://huchengv5.gitee.io/ ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请 与我联系 。