2008-11-03 2 views
4

Mon application a un objet DataGridView et une liste de type MousePos. MousePos est une classe personnalisée qui contient les coordonnées X, Y de la souris (de type "Point") et un compte courant de cette position. J'ai un thread (System.Timers.Timer) qui soulève un événement une fois par seconde, vérifie la position de la souris, ajoute et/ou met à jour le compte de la position de la souris sur cette liste.Comment remplir en toute sécurité avec des données et Refresh() un DataGridView dans une application multithread?

Je voudrais avoir un thread en cours d'exécution similaire (encore une fois, je pense que System.Timers.Timer est un bon choix) qui relancerait un événement une fois par seconde pour actualiser automatiquement le DataGridView afin que l'utilisateur puisse voir les données sur la mise à jour de l'écran. (comme le fait TaskManager.)

Malheureusement, l'appel de la méthode DataGridView.Refresh() entraîne l'arrêt de l'exécution de VS2005 et signale que j'ai rencontré une situation de threads croisés.

Si je comprends bien, j'ai 3 fils maintenant:

  • thread d'interface utilisateur primaire
  • MousePos fil Liste (minuterie)
  • DataGridView de fil de rafraîchissement (minuterie)

Pour voir si je pourrais Refresh() le DataGridView sur le thread primaire, j'ai ajouté un bouton au formulaire qui a appelé DataGridView.Refresh(), mais cela (étrangement) n'a rien fait. J'ai trouvé un sujet qui semblait indiquer que si je définissais DataGridView.DataSource = null et retour à ma liste, cela actualiserait la grille de données. Et en effet, cela a fonctionné, mais seulement à travers le bouton (qui est piloté sur le thread principal.)


Donc, cette question a transformé en deux parter:

  1. réglage est-il à DataGridView.DataSource null et retour à ma liste un moyen acceptable d'actualiser le DataGrid? (Cela me semble inefficace ...)
  2. Comment faire cela en toute sécurité dans un environnement multithread?

Voici le code que j'ai écrit jusqu'à présent (C# /. Net 2,0)

public partial class Form1 : Form 
{ 
    private static List<MousePos> mousePositionList = new List<MousePos>(); 
    private static System.Timers.Timer mouseCheck = new System.Timers.Timer(1000); 
    private static System.Timers.Timer refreshWindow = new System.Timers.Timer(1000); 

    public Form1() 
    { 
     InitializeComponent(); 
     mousePositionList.Add(new MousePos()); // ANSWER! Must have at least 1 entry before binding to DataSource 
     dataGridView1.DataSource = mousePositionList; 
     mouseCheck.Elapsed += new System.Timers.ElapsedEventHandler(mouseCheck_Elapsed); 
     mouseCheck.Start(); 
     refreshWindow.Elapsed += new System.Timers.ElapsedEventHandler(refreshWindow_Elapsed); 
     refreshWindow.Start(); 
    } 

    public void mouseCheck_Elapsed(object source, EventArgs e) 
    { 
     Point mPnt = Control.MousePosition; 
     MousePos mPos = mousePositionList.Find(ByPoint(mPnt)); 
     if (mPos == null) { mousePositionList.Add(new MousePos(mPnt)); } 
     else { mPos.Count++; } 
    } 

    public void refreshWindow_Elapsed(object source, EventArgs e) 
    { 
     //dataGridView1.DataSource = null;    // Old way 
     //dataGridView1.DataSource = mousePositionList; // Old way 
     dataGridView1.Invalidate();      // <= ANSWER!! 
    } 

    private static Predicate<MousePos> ByPoint(Point pnt) 
    { 
     return delegate(MousePos mPos) { return (mPos.Pnt == pnt); }; 
    } 
} 

public class MousePos 
{ 
    private Point position = new Point(); 
    private int count = 1; 

    public Point Pnt { get { return position; } } 
    public int X { get { return position.X; } set { position.X = value; } } 
    public int Y { get { return position.Y; } set { position.Y = value; } } 
    public int Count { get { return count; } set { count = value; } } 

    public MousePos() { } 
    public MousePos(Point mouse) { position = mouse; } 
} 
+0

Je suppose que mettais [C#] dans le sujet est verboten, hein? – Pretzel

Répondre

5

MISE À JOUR! - Je partiellement compris la réponse à partie # 1 dans le livre "Pro 2.0 .NET Windows Forms et commandes des clients en C#"

J'avais initialement pensé que Refresh() n'a pas été faire quelque chose et que je devais appeler la méthode Invalidate(), pour dire à Windows de repeindre mon contrôle à ses loisirs. (qui est généralement tout de suite, mais si vous avez besoin d'une garantie pour le repeindre maintenant, puis de suivre avec un appel immédiat à la méthode Update().)

dataGridView1.Invalidate(); 

Mais, il se trouve que le Refresh() méthode est simplement un alias pour:

dataGridView1.Invalidate(true); 
    dataGridView1.Update();    // <== forces immediate redraw 

Le seul petit problème que j'ai trouvé c'était que s'il n'y avait pas de données dans la dataGridView, aucune quantité d'invalidation n'actualiserait le contrôle. J'ai dû réaffecter la source de données. Ensuite, cela a bien fonctionné après cela. Mais seulement pour la quantité de lignes (ou d'éléments dans ma liste) - Si de nouveaux éléments ont été ajoutés, le DataGridView ne serait pas au courant qu'il y avait plus de lignes à afficher. Il semble donc que lors de la liaison d'une source de données (List ou Table) à la source de données, dataGridView compte les éléments (lignes) puis les définit en interne et ne vérifie jamais s'il y a de nouvelles lignes/éléments ou lignes/éléments supprimés. C'est pourquoi la réaffectation répétée de la source de données fonctionnait auparavant.

Maintenant, pour comprendre comment mettre à jour le nombre de lignes à afficher dans dataGridView sans avoir à re-lier la source de données ... amusant, amusant, amusant! :-)


Après avoir fait quelques recherches, je pense avoir ma réponse à partie # 2 de ma question (aka multi-threading sécurité.):

Plutôt que d'utiliser System.Timers .Timer, j'ai trouvé que je devrais utiliser System.Windows.Forms.Timer à la place. L'événement se produit de sorte que la méthode utilisée dans le rappel se produise automatiquement sur le thread principal.

Pas de problèmes d'inter-threading!

La déclaration ressemble à ceci:

private static System.Windows.Forms.Timer refreshWindow2; 
refreshWindow2 = new Timer(); 
refreshWindow2.Interval = 1000; 
refreshWindow2.Tick += new EventHandler(refreshWindow2_Tick); 
refreshWindow2.Start(); 

Et la méthode est comme ceci:

private void refreshWindow2_Tick(object sender, EventArgs e) 
{ 
    dataGridView1.Invalidate(); 
} 
+2

Un moyen plus élégant de réinitialiser la DataSource consiste à utiliser un BindingContext et à l'actualiser à la place. Cela signifie que vous n'avez pas besoin de le réinitialiser à zéro à chaque fois. En termes de mise à jour automatique, je pense que vous avez besoin d'une source de données qui implémente INotifyPropertyChanged. – Quibblesome

5

Vous devez mettre à jour la grille sur le thread principal de l'interface utilisateur, comme tous les autres contrôles. Voir control.Invoke ou Control.BeginInvoke.

3

On dirait que vous avez votre réponse là! Juste dans cawse vous êtes curieux de savoir comment faire des appels cross-thread à ui: Tous les contrôles ont une méthode Invoke() (ou BEginInvoke() - au cas où vous voulez faire les choses de manière asynchrone), cela est utilisé pour appeler tout méthode sur le contrôle dans le contexte du thread UI principal. Donc, si vous allez appeler votre datagridview d'un autre fil que vous devrez effectuer les opérations suivantes:

public void refreshWindow_Elapsed(object source, EventArgs e) 
{ 

    // we use anonymous delgate here as it saves us declaring a named delegate in our class 
    // however, as c# type inference sometimes need a bit of 'help' we need to cast it 
    // to an instance of MethodInvoker 
    dataGridView1.Invoke((MethodInvoker)delegate() { dataGridView1.Invalidate(); }); 
} 
Questions connexes