Подтвердить что ты не робот

Как передать BitmapImage из фонового потока в поток пользовательского интерфейса в WPF?

У меня есть фоновый поток, который генерирует серию объектов BitmapImage. Каждый раз, когда фоновый поток заканчивает создание растрового изображения, я хотел бы показать это растровое изображение для пользователя. Проблема заключается в том, как передать BitmapImage из фонового потока в поток пользовательского интерфейса.

Это проект 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 в поток пользовательского интерфейса, где он станет uiThreadImage, а затем установите GeneratedImage = uiThreadImage, чтобы представление могло обновляться. Я рассмотрел некоторые примеры работы с WPF Dispatcher, но я не могу представить пример, который решает эту проблему. Просьба сообщить.

4b9b3361

Ответ 1

Следующее использует диспетчер для выполнения делегата Action в потоке пользовательского интерфейса. Это использует синхронную модель, альтернативный Dispatcher.BeginInvoke будет выполнять делегат асинхронно.

var backgroundThreadImage = GenerateImage(coordinate);

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

UPDATE Как обсуждалось в комментариях, вышесказанное не будет работать, поскольку BitmapImage не создается в потоке пользовательского интерфейса. Если вы не намерены изменять изображение после его создания, вы можете его заморозить, используя Freezable.Freeze, а затем назначить GeneratedImage в делегате диспетчера (BitmapImage становится доступным только для чтения и, следовательно, потокобезопасным в результате Freeze). Другой вариант - загрузить изображение в MemoryStream в фоновом потоке, а затем создать BitmapImage в потоке пользовательского интерфейса в делете диспетчера с этим потоком и свойство StreamSource BitmapImage.

Ответ 2

Вам нужно сделать две вещи:

  • Заморозьте BitmapImage, чтобы его можно было перемещать в поток пользовательского интерфейса, затем
  • Используйте диспетчер для перехода к потоку пользовательского интерфейса, чтобы установить GeneratedImage

Вам понадобится доступ к Диспетчеру потока пользовательского интерфейса из потока генератора. Самый гибкий способ сделать это - захватить значение Dispatcher.CurrentDispatcher в основном потоке и передать его в поток генератора:

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

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

  ...

Если вы знаете, что используете это только в запущенном приложении и что приложение будет иметь только один поток пользовательского интерфейса, вы можете просто вызвать Application.Current.Dispatcher, чтобы получить текущего диспетчера. Недостатки:

  • Вы теряете возможность использовать свою модель просмотра независимо от созданного объекта приложения.
  • В вашем приложении может быть только один поток пользовательского интерфейса.

В потоке генератора добавьте вызов Freeze после создания изображения, затем используйте Dispatcher для перехода к потоку пользовательского интерфейса, чтобы установить изображение:

var backgroundThreadImage = GenerateImage(coordinate);

backgroundThreadImage.Freeze();

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

Разъяснение

В приведенном выше коде крайне важно, чтобы Dispatcher.CurrentDispatcher был доступен из потока пользовательского интерфейса, а не из потока генератора. У каждого потока есть свой Диспетчер. Если вы вызовете Dispatcher.CurrentDispatcher из потока генератора, вы получите его диспетчер вместо того, который вам нужен.

Другими словами, вы должны сделать это:

  var dispatcher = Dispatcher.CurrentDispatcher;

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

а не это:

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

Ответ 3

В фоновом потоке работают потоки.

Например, в фоновом потоке:

var artUri = new Uri("MyProject;component/../Images/artwork.placeholder.png", UriKind.Relative);

StreamResourceInfo albumArtPlaceholder = Application.GetResourceStream(artUri);

var _defaultArtPlaceholderStream = albumArtPlaceholder.Stream;

SendStreamToDispatcher(_defaultArtPlaceholderStream);

В потоке пользовательского интерфейса:

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!
     })); 
}