在另一个线程中运行WPF控件

19
我正在使用一个来自某个我没有源代码的库的可视化控件,但更新速度太慢(大约200毫秒),如果同时显示三个这样的控件,则UI响应速度较慢(当它们都在考虑时,我的UI会被卡住大约600毫秒)。
我已经阅读了一些关于TaskSchedulers的帖子,并开始调查Parallel task功能是否可以作为将每个控件在其自己的线程中运行的方法。平台将是多核心,因此我希望利用同时处理的优势。
问题是我甚至不知道该怎么做,也不知道需要注意什么。
有适合在WPF中从主UI线程中分离运行控制的设计模式吗?
具体来说,它是第三方地图控件,当给出新位置或缩放级别时,重绘需要很长时间(约200毫秒)。 最多四个这样的控件以最大4Hz的速率进行更新 - 很明显它们无法跟上... 我已经将WPF控件封装在用户控件中,并需要在其自己的线程中运行每个实例,同时仍然捕捉用户输入(例如鼠标点击)。
更新:当我正在寻找解决方案时,我已经实施了以下内容。我的主(UI)线程产生一个线程,创建一个包含所讨论控件的新窗口,并将它定位到正确的位置(这样它看起来就像是一个普通控件)。
_leftTopThread = new Thread(() =>
{
   _topLeftMap = new MapWindow()
   {
      WindowStartupLocation = WindowStartupLocation.Manual,
      Width = leftLocation.Width,
      Height = leftLocation.Height,
      Left = leftLocation.X,
      Top = leftLocation.Y,
      CommandQueue = _leftMapCommandQueue,
   };

   _topLeftMap.Show();
   System.Windows.Threading.Dispatcher.Run();

});

_leftTopThread.SetApartmentState(ApartmentState.STA);
_leftTopThread.IsBackground = true;
_leftTopThread.Name = "LeftTop";
_leftTopThread.Start();

在这里,CommandQueue 是一个线程安全的 BlockingCollection 队列,用于向地图发送命令(移动位置等)。

  • 问题是:我现在只能够:
  • 由于 System.Windows.Threading.Dispatcher.Run() 的调用而接受用户输入
  • 或者阻塞在 CommandQueue 上,监听主线程发送的命令

我不能旋转等待命令,因为它会占用所有线程的 CPU!
有没有可能同时阻塞并让事件消息泵工作?


所有的WPF控件都必须在UI线程上更新。然而,如果您提供正在使用的控件以及更新/填充它所编写的任何代码的详细信息,我们可能能够提供帮助。 - Cameron Peters
@CameronPeters,你确定不可能有多个“UI线程”吗? - svick
我目前有多个“UI”线程正在运行(感谢Threading.Dispatcher.Run()),但无法阻塞它们等待信号。 - DefenestrationDay
@svick -- 是的,可以有多个UI线程。然而,所有WPF控件都是从System.Windows.Threading.DispatcherObject继承而来,这需要线程亲和性。任何控件只能由创建它的线程更新。后台线程可以完成部分工作,但结果必须分派到创建控件的线程,然后该线程可以进行更新... - Cameron Peters
2个回答

13

我的方法虽然能用,但可能并不是最优雅的方法。

我在XAML中创建了一个窗口,其中包含我的第三方(渲染速度较慢)控件。

public partial class MapWindow : Window
{
    private ConcurrentQueue<MapCommand> _mapCommandQueue;
    private HwndSource _source;

    // ...

}

我的主(UI)线程在一个线程上构建并启动了这个窗口:

_leftTopThread = new Thread(() =>
{
   _topLeftMap = new MapWindow()
   {
      WindowStartupLocation = WindowStartupLocation.Manual,
      CommandQueue = _leftMapCommendQueue,
   };

    _topLeftMap.Show();
    System.Windows.Threading.Dispatcher.Run();

});

_leftTopThread.SetApartmentState(ApartmentState.STA);
_leftTopThread.IsBackground = true;
_leftTopThread.Name = "LeftTop";
_leftTopThread.Start();

在线程初始化后,我获取到该线程中的窗口句柄:

private IntPtr LeftHandMapWindowHandle
{
    get
    {
        if (_leftHandMapWindowHandle == IntPtr.Zero)
        {
            if (!_topLeftMap.Dispatcher.CheckAccess())
            {
                _leftHandMapWindowHandle = (IntPtr)_topLeftMap.Dispatcher.Invoke(
                  new Func<IntPtr>(() => new WindowInteropHelper(_topLeftMap).Handle)
                );
            }
            else
            {
                _leftHandMapWindowHandle = new WindowInteropHelper(_topLeftMap).Handle;
            }
        }
        return _leftHandMapWindowHandle;
    }
}

...将一个命令放入与线程化窗口共享的线程安全队列后:

var command = new MapCommand(MapCommand.CommandType.AircraftLocation, new object[] {RandomLatLon});
_leftMapCommendQueue.Enqueue(command);

我让它知道它可以检查队列:

PostMessage(LeftHandMapWindowHandle, MapWindow.WmCustomCheckForCommandsInQueue, IntPtr.Zero, IntPtr.Zero);
窗口可以接收我的消息,因为它已经钩入了窗口消息。
protected override void OnSourceInitialized(EventArgs e)
{
    base.OnSourceInitialized(e);

    _source = PresentationSource.FromVisual(this) as HwndSource;
    if (_source != null) _source.AddHook(WndProc);
}

...然后可以进行检查:

private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) // 
{
    // Handle messages...
    var result = IntPtr.Zero;

    switch (msg)
    {
        case WmCustomCheckForCommandsInQueue:
            CheckForNewTasks();
            break;

    }
    return result;
}

然后在线程上执行!

private void CheckForNewTasks()
{
    MapCommand newCommand;
    while (_mapCommandQueue.TryDequeue(out newCommand))
    {
        switch (newCommand.Type)
        {
            case MapCommand.CommandType.AircraftLocation:
                SetAircraftLocation((LatLon)newCommand.Arguments[0]);
                break;

            default:
                Console.WriteLine(String.Format("Unknown command '0x{0}'for window", newCommand.Type));
                break;
        }
    }
}

就这么简单.. :)


4
你做得实际上很好。在这里,这种事情很少受到赞赏。 - Boppity Bop
牛逼的技巧!如果“父”窗口被移动或最大化,你该如何处理?那么你需要同时移动“控件”窗口,虽然这可能有点复杂。 - Fabien

7

我也一直在研究这个问题,最相关的信息我在这篇博客文章中找到了(但我还没有测试过):

http://blogs.msdn.com/b/dwayneneed/archive/2007/04/26/multithreaded-ui-hostvisual.aspx

它在UI线程上创建了一个HostVisual,然后启动了一个后台线程,创建了一个MediaElement,并将其放置在VisualTarget中(该VisualTarget指向HostVisual),最后将其全部放入我们的hacky VisualTargetPresentationSource中。这种方法的问题是,显然用户无法与在新线程中运行的控件进行交互。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接