您当前的位置: 首页 >  ui

君子居易

暂无认证

  • 0浏览

    0关注

    210博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

WPF UI多线程:HostVisual

君子居易 发布时间:2022-07-09 12:17:26 ,浏览量:0

背景:WPF 线程模型

通常,WPF 中的对象只能从创建它们的线程中访问。有时这种限制会与 UI 线程混淆,但事实并非如此,对象存在于其他线程上是完全可以的。但是通常不可能在一个线程上创建一个对象,然后从另一个线程访问它。在几乎所有情况下,这都会导致 InvalidOperationException,指出“调用线程无法访问此对象,因为不同的线程拥有它”。

可冻结的

当然,这个限制也有例外。一个熟悉的类是 Freezable 类。Freezable 对象可以被冻结,此时它们变为只读,我们解除了单线程限制。冻结的 Freezables 的示例包括从 Brushes 类中获得的标准画笔。这些刷子可以随时在任何线程上使用。

这非常有用,可用于所有图形基元资源(钢笔、画笔、变换等)。您甚至可以从 Freezable 派生出您自己的类型并按照相同的规则进行游戏。

单独的窗口

不幸的是,这种只读限制可能是一个真正的问题。在许多场景中,我们希望在单独的线程上运行单独的 UI 片段。如果这些 UI 片段彼此独立,您可以将它们托管在单独的窗口中,并在单独的线程上运行这些窗口。对于某些场景,这可能是一个合理的解决方案,尤其是单独的线程可以在独立的顶级窗口中运行的场景。这种方法的最大限制是来自一个的图形不能与另一个的图形合成。因此,虽然您可以为另一个线程的 UI 使用子窗口,但系统只会在另一个之上渲染一个。你不能有透明度,你不能将它用作 3D 内容的画笔,你不能在它上面绘图,等等。

主机视觉

如果您的方案不需要交互性(即输入),那么 WPF 提供了另一个选项:HostVisual。此选项利用 WPF 中强大的合成引擎,该引擎已经能够将来自多个线程的渲染图元聚合到一个场景中。工作线程拥有的元素树被渲染到自己的合成目标(称为VisualTarget)中,结果被合成到UI线程拥有的HostVisual中。

问题 #1:在 XAML 中托管视觉对象

第一个要解决的问题是 HostVisual 类派生自 Visual。我不能使用现有的面板(例如边框)来承载此视觉效果。Border 派生自 Decorator,它是具有单个子面板的标准基类。不幸的是,孩子被强类型化为 UIElement。我必须使用不是从 UIElement 派生的 HostVisual。我知道没有内置方法可以将 Visual 作为标准元素之一的子元素(例如边框、网格、画布等)。所以我们自己制作:

[ContentProperty("Child")]
public class VisualWrapper : FrameworkElement
{
    public Visual Child
    {
        get
        {
            return _child;
        }
 
        set
        {
            if (_child != null)
            {
                RemoveVisualChild(_child);
            }
 
            _child = value;
 
            if (_child != null)
            {
                AddVisualChild(_child);
            }
        }
    }
 
    protected override Visual GetVisualChild(int index)
    {
        if (_child != null && index == 0)
        {
            return _child;
        }
        else
        {
            throw new ArgumentOutOfRangeException("index");
        }
    }
 
    protected override int VisualChildrenCount
    {
        get
        {
            return _child != null ? 1 : 0;
        }
    }

    private Visual _child;
}
问题 #2:布局和 Loaded 事件

WPF 提供了一个非常方便的事件,称为“Loaded”。此事件基本上表示元素已完全初始化、测量、排列、渲染并插入演示源(例如窗口)。许多元素使用此事件,包括 MediaElement,但遗憾的是,对于未插入表示源的元素树,不会引发此事件,并且通过 HostVisual/VisualTarget 显示元素树不算在内。所以解决这个问题,我们制作自己的表示源并使用它来根工作线程将拥有的元素树。这立即导致另一个问题:布局在所有元素上暂停,直到由表示源恢复。不幸的是,这样做的官方机制是内部的,所以我们能做的最好的事情就是明确地测量和安排根元素。

public class VisualTargetPresentationSource : PresentationSource
{
    public VisualTargetPresentationSource(HostVisual hostVisual)
    {
        _visualTarget = new VisualTarget(hostVisual);
    }
 
    public override Visual RootVisual
    {
        get
        {
            return _visualTarget.RootVisual;
        }
 
        set
        {
            Visual oldRoot = _visualTarget.RootVisual;
 
            // Set the root visual of the VisualTarget.  This visual will
            // now be used to visually compose the scene.
            _visualTarget.RootVisual = value;
 
            // Tell the PresentationSource that the root visual has
            // changed.  This kicks off a bunch of stuff like the
            // Loaded event.
            RootChanged(oldRoot, value);
 
            // Kickoff layout...
            UIElement rootElement = value as UIElement;
            if (rootElement != null)
            {
                rootElement.Measure(new Size(Double.PositiveInfinity,
                                             Double.PositiveInfinity));
                rootElement.Arrange(new Rect(rootElement.DesiredSize));
            }
        }
    }
 
    protected override CompositionTarget GetCompositionTargetCore()
    {
        return _visualTarget;
    }
 
    public override bool IsDisposed
    {
        get
        {
            // We don't support disposing this object.
            return false;
        }
    }
 
    private VisualTarget _visualTarget;
}
后台线程

在 C# 中创建线程很容易。需要注意的一个技巧是,您必须将线程标记为“后台”线程,否则只要这些线程处于活动状态,应用程序就会继续运行。还要记住,WPF 的某些部分要求为 COM 的“单线程单元”初始化其线程。所有这一切都很容易做到,稍后您将看到这段代码:

Thread thread = new Thread(/*…*/);

thread.ApartmentState = ApartmentState.STA;

thread.IsBackground = true;
thread.Start(/*…*/);
演示

该演示将是一个显示 3 部电影的网格,每部电影都在不同的后台线程上呈现。该演示非常简单,但希望能够传达要点。从 UI 线程到工作线程当然可以进行通信,并且考虑到所有线程都有自己的调度程序,这实际上非常容易,但是这个演示没有做到这一点。有兴趣的读者应该查阅 Dispatcher.BeginInvoke 的文档。

XAML

XAML 简单地定义了一个包含 3 列的网格,并在每列中放置了一个 VisualWrapper。这是我们将从代码中放置 HostVisual 的地方。构建一个在另一个线程上自动显示其内容的类会很酷,但是我们必须更改解析器以支持切换线程,因为创建对象的线程是拥有该对象的线程。无论如何,简单的 XAML 是:


  
    
      
      
      
    
    
    
    
  
编码

代码也很简单。对于 3 个播放器中的每一个,它在 UI 线程上创建一个 HostVisual,然后启动一个后台线程,创建一个 MediaElement,将其放入一个 VisualTarget(它指向 HostVisual),并将其全部放入我们的 hacky VisualTargetPresentationSource。

public partial class Window1 : System.Windows.Window
{
    public Window1()
    {
        InitializeComponent();
    }
 
    private void OnLoaded(object sender, RoutedEventArgs e)
    {
        Player1.Child = CreateMediaElementOnWorkerThread();
        Player2.Child = CreateMediaElementOnWorkerThread();
        Player3.Child = CreateMediaElementOnWorkerThread();
    }
 
    private HostVisual CreateMediaElementOnWorkerThread()
    {
        // Create the HostVisual that will "contain" the VisualTarget
        // on the worker thread.
        HostVisual hostVisual = new HostVisual();
 
        // Spin up a worker thread, and pass it the HostVisual that it
        // should be part of.
        Thread thread = new Thread(new ParameterizedThreadStart(MediaWorkerThread));
        thread.ApartmentState = ApartmentState.STA;
        thread.IsBackground = true;
        thread.Start(hostVisual);
 
        // Wait for the worker thread to spin up and create the VisualTarget.
        s_event.WaitOne();
 
        return hostVisual;
    }
 
    private FrameworkElement CreateMediaElement()
    {
        // Create a MediaElement, and give it some video content.
        MediaElement mediaElement = new MediaElement();
        mediaElement.BeginInit();
        mediaElement.Source = new Uri("http://download.microsoft.com/download/2/C/4/2C433161-F56C-4BAB-BBC5-B8C6F240AFCC/SL_0410_448x256_300kb_2passCBR.wmv?amp;clcid=0x409");
        mediaElement.Width = 200;
        mediaElement.Height = 100;
        mediaElement.EndInit();
 
        return mediaElement;
    }
 
    private void MediaWorkerThread(object arg)
    {
        // Create the VisualTargetPresentationSource and then signal the
        // calling thread, so that it can continue without waiting for us.
        HostVisual hostVisual = (HostVisual)arg;
        VisualTargetPresentationSource visualTargetPS = new VisualTargetPresentationSource(hostVisual);
        s_event.Set();
 
        // Create a MediaElement and use it as the root visual for the
        // VisualTarget.
        visualTargetPS.RootVisual = CreateMediaElement();
 
        // Run a dispatcher for this worker thread.  This is the central
        // processing loop for WPF.
        System.Windows.Threading.Dispatcher.Run();
    }
 
    private static AutoResetEvent s_event = new AutoResetEvent(false);
}
概括

在这个演示中,我展示了如何使用 HostVisual 和 VisualTarget 类来组合来自不同线程的 UI 片段。有一些限制:即工作线程拥有的 UI 不接收输入事件。在此过程中,我们还必须解决一些烦恼,但事实证明这些烦恼相当小。

关注
打赏
1660814979
查看更多评论
立即登录/注册

微信扫码登录

0.0361s