在WPF中,如何将BitmapImage从后台线程传递到UI线程?

25

我有一个后台线程,会生成一系列的BitmapImage对象。每当后台线程完成生成一个位图时,我想向用户展示这个位图。问题在于如何将BitmapImage从后台线程传递到UI线程。

这是一个MVVM项目,所以我的视图有一个Image元素:

<Image Source="{Binding GeneratedImage}" />

我的视图模型有一个名为 GeneratedImage 的属性:

private BitmapImage _generatedImage;

public BitmapImage GeneratedImage
{
    get { return _generatedImage; }
    set
    {
        if (value == _generatedImage) return;
        _generatedImage= value;
        RaisePropertyChanged("GeneratedImage");
    }
}

我的视图模型也有创建后台线程的代码:

public void InitiateGenerateImages(List<Coordinate> coordinates)
{
    ThreadStart generatorThreadStarter = delegate { GenerateImages(coordinates); };
    var generatorThread = new Thread(generatorThreadStarter);
    generatorThread.ApartmentState = ApartmentState.STA;
    generatorThread.IsBackground = true;
    generatorThread.Start();
}

private void GenerateImages(List<Coordinate> coordinates)
{
    foreach (var coordinate in coordinates)
    {
        var backgroundThreadImage = GenerateImage(coordinate);
        // I'm stuck here...how do I pass this to the UI thread?
    }
}

我希望以某种方式将backgroundThreadImage传递到UI线程,在那里它将成为uiThreadImage,然后设置GeneratedImage = uiThreadImage,以便视图可以更新。我查看了一些涉及WPF Dispatcher的示例,但似乎没有例子解决此问题。请给予建议。

3个回答

13
使用调度程序在UI线程上执行Action委托的示例。这是同步模型,替代方案Dispatcher.BeginInvoke将异步执行委托。
var backgroundThreadImage = GenerateImage(coordinate);

GeneratedImage.Dispatcher.Invoke(
        DispatcherPriority.Normal,
        new Action(() =>
            {
                GeneratedImage = backgroundThreadImage;
            }));

更新 如评论中所讨论的那样,仅上述代码是行不通的,因为BitmapImage没有在UI线程上创建。如果您创建图像后没有修改它的意图,则可以使用Freezable.Freeze将其冻结,然后在调度程序委托中将其分配给GeneratedImage(由于Freeze使BitmapImage变为只读,因此线程安全)。另一个选项是在后台线程中将图像加载到MemoryStream中,然后使用该流和BitmapImage的StreamSource属性在调度程序委托中在UI线程上创建BitmapImage。


这很有帮助,Simon,谢谢。但是当这个过程开始时,GeneratedImage尚未设置为对象的实例,我该如何解决这个问题? - devuxer
1
@DanM 当然我没有考虑到那个 :) 你也可以通过 Application.Current.Dispatcher 获取 Dispatcher。 - Simon Fox
我恐怕这也不行。我收到了“调用线程无法访问此对象,因为其他线程拥有它”的错误信息。 - devuxer
看起来你将不得不在 UI 线程上创建 BitmapImage...将 GenerateImage 的调用移到派发器正在在 UI 线程上执行的委托中。 - Simon Fox
1
哦,讽刺啊。在我的代码中,我一直在冻结StreamGeometries,但是直到我执行了这个操作之后才发现为什么位图没有被绘制:bitmap.Freeze();Dispatcher.Invoke(() => { myImage.Source = bitmap; }); - Stonetip
显示剩余2条评论

11

你需要做两件事情:

  1. 冻结你的BitmapImage,以便可以将其移动到UI线程,然后
  2. 使用Dispatcher在UI线程中设置GeneratedImage

你需要从生成器线程访问UI线程的Dispatcher。 最灵活的方法是在主线程中捕获Dispatcher.CurrentDispatcher的值并将其传递到生成器线程中:

public void InitiateGenerateImages(List<Coordinate> coordinates)   
{
  var dispatcher = Dispatcher.CurrentDispatcher;

  var generatorThreadStarter = new ThreadStart(() =>
     GenerateImages(coordinates, dispatcher));

  ...

如果你知道你将只在运行的应用程序中使用该代码,并且该应用程序将只有一个UI线程,那么你可以直接调用Application.Current.Dispatcher来获取当前的调度程序。其缺点是:

  1. 你失去了独立于构造的应用程序对象使用你的视图模型的能力。
  2. 你的应用程序只能有一个UI线程。

在生成器线程中,在生成图像后添加一个Freeze调用,然后使用Dispatcher转换到UI线程来设置图像:

var backgroundThreadImage = GenerateImage(coordinate);

backgroundThreadImage.Freeze();

dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(() =>
{
   GeneratedImage = backgroundThreadImage;
}));

澄清:

在上述代码中,从UI线程而不是生成器线程访问Dispatcher.CurrentDispatcher非常关键。每个线程都有自己的Dispatcher。如果你从生成器线程调用Dispatcher.CurrentDispatcher,你将得到它的Dispatcher而不是你想要的那个。

换句话说,你必须这样做:

  var dispatcher = Dispatcher.CurrentDispatcher;

  var generatorThreadStarter = new ThreadStart(() =>
     GenerateImages(coordinates, dispatcher));

而不是这个:

  var generatorThreadStarter = new ThreadStart(() =>
     GenerateImages(coordinates, Dispatcher.CurrentDispatcher));

+1。谢谢,Ray。我喜欢你传递调度程序的提示。Freeze()概念也很关键,我知道将来会用到它。(正如我对Simon评论的那样,在这种情况下,释放UI线程并不是我的目标--我只是想让它更新(渲染)每个图像,而不是等待所有图像处理完后再更新。) - devuxer
Ray,我刚刚尝试了你传递调度程序的想法,但是由于某种原因,Dispatcher.CurrentDispatcherApp.Current.Dispatcher不等同。如果我使用App.Current.Dispatcher,我的代码可以完美运行,但是如果我传递Dispatcher.CurrentDispatcher,我会得到“调用线程无法访问此对象,因为不同的线程拥有它”的错误。你知道这可能是为什么吗? - devuxer
是的,你正在从生成器线程而不是UI线程调用Dispatcher.CurrentDispatcher。我已经在答案中添加了一个澄清来解释如何修复它。 - Ray Burns

0
在后台线程中使用流进行操作。例如,在后台线程中:
var artUri = new Uri("MyProject;component/../Images/artwork.placeholder.png", UriKind.Relative);

StreamResourceInfo albumArtPlaceholder = Application.GetResourceStream(artUri);

var _defaultArtPlaceholderStream = albumArtPlaceholder.Stream;

SendStreamToDispatcher(_defaultArtPlaceholderStream);

在UI线程中:

void SendStreamToDispatcher(Stream imgStream)
{
     dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(() =>
     {
        var imageToDisplay = new BitmapImage();
        imageToDisplay.SetSource(imgStream);

        //Use you bitmap image obtained from a background thread as you wish!
     })); 
}

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