2010-06-14 5 views
19

J'ai un thread d'arrière-plan qui génère une série d'objets BitmapImage. Chaque fois que le thread d'arrière-plan finit de générer un bitmap, je voudrais montrer cette image à l'utilisateur. Le problème est de savoir comment passer le BitmapImage du thread d'arrière-plan au thread de l'interface utilisateur.Comment transmettre un bitmapImage d'un thread d'arrière-plan au thread d'interface utilisateur dans WPF?

Ce projet est MVVM, donc mon avis a un élément Image:

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

Mon point de vue-modèle a une propriété GeneratedImage:

private BitmapImage _generatedImage; 

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

Mon point de vue-modèle a également le code crée le fil d'arrière-plan:

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? 
    } 
} 

J'aimerais passer en quelque sorte backgroundThreadImage au thread UI, où il deviendra uiThreadImage, puis définissez GeneratedImage = uiThreadImage pour que la vue puisse être mise à jour. J'ai regardé quelques exemples traitant du WPF Dispatcher, mais je n'arrive pas à trouver un exemple qui résout ce problème. S'il vous plaît donnez votre avis.

Répondre

12

L'exemple suivant utilise le répartiteur pour exécuter un délégué d'action sur le thread d'interface utilisateur. Ceci utilise un modèle synchrone, l'autre Dispatcher.BeginInvoke exécutera le délégué de manière asynchrone.

var backgroundThreadImage = GenerateImage(coordinate); 

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

MISE À JOUR Comme il est indiqué dans les commentaires, ce qui précède seul ne fonctionnera pas comme le BitmapImage n'est pas créé sur le thread d'interface utilisateur. Si vous n'avez pas l'intention de modifier l'image une fois que vous l'avez créée, vous pouvez la geler en utilisant Freezable.Freeze, puis l'affecter à GeneratedImage dans le délégué du répartiteur (l'image BitmapImage devient en lecture seule et donc threadsafe suite au gel). L'autre option consiste à charger l'image dans un MemoryStream sur le thread d'arrière-plan, puis à créer le BitmapImage sur le thread d'interface utilisateur dans le délégué Dispatcher avec ce flux et la propriété StreamSource de BitmapImage.

+0

Ceci est utile, Simon, merci. Mais comment contourner le fait que, lorsque ce processus commence, 'GeneratedImage' n'est pas défini sur une instance d'un objet? – devuxer

+1

@DanM bien sûr je n'ai pas pensé à ça :) Vous pouvez également obtenir un Dispatcher via 'Application.Current.Dispatcher' –

+0

J'ai peur que cela ne le fasse pas non plus. Je reçois "Le thread appelant ne peut pas accéder à cet objet car un thread différent le possède." – devuxer

6

Vous devez faire deux choses:

  1. gel votre BitmapImage de sorte qu'il peut être déplacé vers le thread d'interface utilisateur, puis
  2. Utilisez le Dispatcher pour la transition vers le thread d'interface utilisateur pour définir la GeneratedImage

Vous devez accéder au Dispatcher du thread d'interface utilisateur à partir du thread du générateur. La façon la plus flexible pour le faire est de capturer la valeur Dispatcher.CurrentDispatcher dans le thread principal et passer dans le fil du générateur:

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

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

    ... 

Si vous savez que vous n'utiliserez cela dans une application en cours d'exécution et que le l'application n'aura qu'un thread d'interface utilisateur, vous pouvez simplement appeler Application.Current.Dispatcher pour obtenir le répartiteur actuel. Les inconvénients sont les suivants:

  1. Vous perdez la possibilité d'utiliser votre modèle de vue indépendamment d'un objet Application construit.
  2. Vous ne pouvez avoir qu'un seul thread d'interface utilisateur dans votre application.

Dans le fil de générateur, ajouter un appel à congeler après que l'image est générée, puis utiliser le répartiteur à la transition vers le thread d'interface utilisateur pour définir l'image:

var backgroundThreadImage = GenerateImage(coordinate); 

backgroundThreadImage.Freeze(); 

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

Clarification

Dans le code ci-dessus, il est essentiel que Dispatcher.CurrentDispatcher soit accessible à partir du thread d'interface utilisateur, pas à partir du thread du générateur. Chaque thread a son propre Dispatcher. Si vous appelez Dispatcher.CurrentDispatcher à partir du thread du générateur, vous obtiendrez son Dispatcher au lieu de celui que vous voulez.

En d'autres termes, vous devez faire ceci:

var dispatcher = Dispatcher.CurrentDispatcher; 

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

et non ceci:

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

+1. Merci, Ray. J'aime ton astuce pour passer dans le répartiteur. Le concept Freeze() est également critique, quelque chose dont je sais que je vais avoir besoin à l'avenir. (Comme je l'ai commenté à Simon, libérer le thread de l'interface utilisateur n'était pas vraiment mon objectif dans ce cas - j'essayais simplement de le mettre à jour (restituer) chaque image plutôt que d'attendre que toutes les images soient traitées pour être mises à jour.) – devuxer

+0

Ray, j'ai juste essayé votre idée de passer dans le répartiteur, mais pour une raison quelconque, 'Dispatcher.CurrentDispatcher' n'est pas équivalent à' App.Current.Dispatcher'. Si j'utilise 'App.Current.Dispatcher', mon code fonctionne parfaitement, mais si je passe' Dispatcher.CurrentDispatcher', j'obtiens "Le thread appelant ne peut pas accéder à cet objet car un thread différent le possède." Une idée de pourquoi cela pourrait être? – devuxer

+0

Oui, vous appelez Dispatcher.CurrentDispatcher à partir du thread du générateur et non du thread de l'interface utilisateur. J'ai ajouté une clarification à la réponse pour expliquer comment la réparer. –

0

Dans le fil fond travail avec cours d'eau.

Par exemple, dans le fil de fond:

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

StreamResourceInfo albumArtPlaceholder = Application.GetResourceStream(artUri); 

var _defaultArtPlaceholderStream = albumArtPlaceholder.Stream; 

SendStreamToDispatcher(_defaultArtPlaceholderStream); 

Dans le thread d'interface utilisateur:

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