Как передать 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
, но я не могу представить пример, который решает эту проблему. Просьба сообщить.
Ответы
Ответ 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!
}));
}