C# ListView aus zweitem Thread befüllen

AP Nova

Commander
Registriert
Juni 2008
Beiträge
2.256
Ich habe gerade das Problem, dass ich einer ListView Bilder bereitstellen will, die möglichst nur bei Bedarf (wenn sie auch gezeichnet werden müssen) geladen werden, aber das ohne das gesamte Interface zu blockieren.
Daher muss zum Laden der Bilder (und Skalieren auf die passende Größe) ein zweiter Thread her. Grundsätzlich hatte ich dafür mittlerweile alles Notwendige eingebaut, allerdings wird mir bei e.Graphics.DrawImage im folgenden Beispiel "ArgumentException" gesagt, wozu ich überhaupt keine Erklärung habe.

Code:
void DrawMethod(DrawListViewItemEventArgs e)
        {
            Image abc = Image.FromFile(gültigerBildPfad);
            e.Graphics.DrawImage(abc, e.Bounds.Location); //"Argument Exception"
        }

void DrawLoader()
        {
            while (true)
            {
                DrawDelegate dd = new DrawDelegate(DrawMethod);
                while (queue.Count > 0) //queue enthält ListViewDrawItemEventArgs-Objekte, die genau so von der ListView erzeugt wurden
                {
                    float scale = 1;
                    DrawListViewItemEventArgs DLVIEA;
                    lock (this)
                    {
                        DLVIEA = (DrawListViewItemEventArgs)queue.Dequeue();
                    }
                    if (!ImageLoad[DLVIEA.ItemIndex]) //ImageLoad ist ein bool-Array, welches speichert, ob das Bild bereits geladen wurde
                        Img[DLVIEA.ItemIndex] = Image.FromFile(ImagePaths[DLVIEA.ItemIndex]);
                    if (!ImageScaled[DLVIEA.ItemIndex]) //ImageScaled ist ein bool-Array, welches speichert, ob das Bild bereits passend skaliert wurde
                    { //Der folgende Abschnitt ist egal, da er lediglich für die Skalierung des Bildes sorgt
                        if (Img[DLVIEA.ItemIndex].Width >= Img[DLVIEA.ItemIndex].Height && Img[DLVIEA.ItemIndex].Width > ImageSize)
                        {
                            scale = (float)Img[DLVIEA.ItemIndex].Width / (float)ImageSize;
                        }
                        else
                            if (Img[DLVIEA.ItemIndex].Height > ImageSize)
                            {
                                scale = (float)Img[DLVIEA.ItemIndex].Height / (float)ImageSize;
                            }
                        Img[DLVIEA.ItemIndex] = new Bitmap(Img[DLVIEA.ItemIndex], (int)(Img[DLVIEA.ItemIndex].Width / scale), (int)(Img[DLVIEA.ItemIndex].Height / scale));
                    }
                    lv.Invoke(dd, DLVIEA); //An dieser Stelle wird die obere Methode aufgerufen
                }
                Thread.Sleep(100); //Überprüft alle 100 ms, ob etwas neues geladen werden muss. Ich habe das so gelöst, weil es deutlich einfacher als eine richtige Synchronisation ist und praktisch keine Ressourcen verbraucht
            }
Weiß jemand, wo hier das Problem ist, das diese ArgumentException verursacht? Der Code der oberen Methode klappt definitiv, nur nicht, wenn ich ihn über den Delegaten aus dem zweiten Thread aufrufe.

Alternativ: Wie könnte man sonst realisieren, dass die Bilder in einem zweiten Thread geladen werden und jedes fertig geladene Bild sofort gezeichnet wird? Das Kriterium ist hier auf jeden Fall, dass das Interface dauerhaft nutzbar bleibt.

Danke für eure Hilfe
 
Hmm die Methodik mit der Endlosschleife und dem Sleep find ich recht, naja, unschön.
Wonach entscheidest du denn ob ein Bild geladen werden muss oder nicht?
Wie wäre es denn wenn du ne eigene ListView implementierst und dort einen Mechanismus einbaust der die Listview informiert sobald neue Bilder vorhanden sind.
Das könnte man dann geschmeidig über Events regeln. Deshalb aber nochmal die Konkrete Frage: Wonach entscheidest du ob neue Bilder geladen müssen?
 
Grundsätzlich läuft das so ab: Bei einer bestimmten Situation (z.B. Auswählen eines anderen Ordners), werden die darin befindlichen Dateien erst einmal nur aufgelistet. Da lasse ich dann alles, was mit "jpg", "png" oder "gif" endet, raussuchen. Auf Basis dieser Dateien wird dann ein neues Image-Array erstellt und in passender Größe auch die ImageLoad- und ImageScaled-Arrays. Wenn die ListView nun ein Item zeichnet, gehört dazu auch ein Bild. Das heißt die ListView muss irgendwem sagen, dass sie dieses Bild (bezeichnet durch den ItemIndex) benötigt, woraufhin diese Sache das Bild irgendwie bereitstellen muss.

Im konkreten Fall löse ich das so, dass alle neu zu zeichnenden Items in die Queue gepackt werden bzw. repräsentiv für die Items die ListViewDrawItemEventArgs. Der zweite Thread mit der Endlosschleife und dem Sleep(100) fragt entsprechend alle 100 ms ab, ob wieder etwas in der Queue ist, falls das so ist, wird überprüft ob das Bild bereits geladen, passend skaliert ist usw. Letztendlich wird das ListViewDrawItemEventArgs an den Delegaten weitergereicht, der das Zeichnen in die Wege leitet. Lässt man den ganzen Kram mit dem Laden der Datei (was 100%-ig klappt) außen vor, sieht das also so aus:
ListView will Item neu zeichnen -> ListViewDrawItemEventArgs in queue -> Zweiter Thread findet die EventArgs in der queue, verarbeitet sie -> per Delegat an DrawMethod, damit diese aus dem Ursprungsthread auf die ListView zeichnen kann.

Wenn ich deine Frage richtig verstehe, entscheide ich also durch vorhandene Objekte in "queue", dass wieder Bilder geladen bzw. Items gezeichnet werden müssen.
 
Ok dann würde ich dir folgendes Vorschlagen: Anscheind hast du ja schon einen Mechanismus welcher die Queue befüllt. Aufbauend auf diesem Mechanismus tauscht du die Queue jetzt gegen ObservableCollection<T> aus und hängst dich an einer guten Stelle an deren Event für CollectionChanged ran. Diese Event behält Informationen für dich bereit die dir Sagen wie sich die Collection geändert hat(Hinzufügen,Löschen, Update). Für dich ist hier nur das Hinzufügen interessant. Wenn ich das richtig sehe brauchst du jetzt keinen Thread mehr der aller 100ms fragt ob neue Daten da sind, sondern DU propagierst die einfach raus, sobald neue da sind.
 
Das ist zwar eine "sauberere" Lösung für die Sache mit dem Timer, den zweiten Thread brauche ich trotzdem noch, damit das Interface nicht ruckelt, wenn die Bilder geladen werden.
Die Alternative wäre es, die Bilder im Thread des Interface zu laden, was aber teilweise Ruckler bedeutet, weshalb ich gerne davon wegkäme.

Glücklicherweise kam ich gerade auf eine Idee, woran es liegen könnte. Ich benutze den folgenden Code um die Ereignisse in die Warteschlange zu legen, dabei scheint das Graphics-Objekt zerstört zu werden, wenn die Methode verlassen wird und das nächste Item eingereiht wird.
Code:
void lv_DrawItem(object sender, DrawListViewItemEventArgs e)
        {
            lock(this)
            {
            queue.Enqueue(e);
            }
            Image abc = Image.FromFile("D:\\Bilder\\avatar428387_1.jpg");
            //e.Graphics.DrawImage(abc, e.Bounds.Location);
        }
Als Ersatz werde ich versuchen, statt "e.Graphics" zu verwenden eine "normale" Graphics-Variable der ListView zu erzeugen und damit zu arbeiten. Ich werde gleich noch sagen, ob das geklappt hat.

edit: So klappt es tatsächlich. Der Fehler war also, dass e.Graphics anscheinend unnutzbar wurde.
 
Zurück
Oben