Laden...

"Allgemeiner Fehler in GDI+" [==> Stream von Bitmap-Objekt unterm Hintern weg geschlossen]

Erstellt von skullsplitter vor 11 Jahren Letzter Beitrag vor 11 Jahren 7.419 Views
S
skullsplitter Themenstarter:in
23 Beiträge seit 2011
vor 11 Jahren
"Allgemeiner Fehler in GDI+" [==> Stream von Bitmap-Objekt unterm Hintern weg geschlossen]

Hallo,

ich weiß, dass der Fehler "Allgemeiner Fehler in GDI+." ein sehr leidiges Thema ist, doch ich weiß leider wirklich nicht mehr weiter...

Ich habe ein Programm geschrieben, welches einen kleinen integrierten Server hat. Der Server dient als Schnittstelle um Bilder von einem Smartphone (Android oder iOS) zu empfangen.

Das Programm verfügt zusätzlich über ein integriertes Snipping Tool (wie das Snipping Tool von Windows).

"Gesnippete" und empfangene Bilder werden in einer PictureBox dargstellt.

Wenn das dargstellte Bild jetzt gespeichert werden soll, wird die Methode mit folgendem Code aufgerufen:


        private void saveAsToolStripMenuItem_Click(object sender, EventArgs e)
        {
            Image saveImg = pbImage.Image;

            SaveFileDialog save = new SaveFileDialog();
            save.Filter = "JPEG|*.jpg|Bitmap|*.bmp|GIF|*.gif|PNG|*.png";
            save.Title = "Save as Image";
            save.ShowDialog();

            if (save.FileName != "")
            {
                ImageFormat format = null;

                switch (save.FilterIndex)
                {
                    case 1:
                        format = ImageFormat.Jpeg;
                        break;
                    case 2:
                        format = ImageFormat.Bmp;
                        break;
                    case 3:
                        format = ImageFormat.Gif;
                        break;
                    case 4:
                        format = ImageFormat.Png;
                        break;
                    default:
                        break;
                }

                saveImg.Save(save.FileName, format);
}

Zuerst speichere ich das Bild von der PictureBox um in ein Image-Objekt, dann folgt der SaveFileDialog und am Ende das Speichern.

Wenn ich ein Bild speichere, welches mit dem Snipping-Tool gemacht wurde, funktioniert der Code ohne Probleme (auch mehrmals hintereinander, zeitverzögert usw).

Wenn ich ein Bild speichere, welches gesendet wurde, kommt der Fehler "Allgemeiner Fehler in GDI+.".

Es spielt dabei keine Rolle, ob ich davor bereits ein Bild gespeichert habe, welches via Snipping-Tool gemacht wurde, oder ein direkt empfangenes Bild speichern möchte....

Beide, das "Snipping-Tool" sowie der Server, verwenden die Selbe Methode, um das Bild in die PictureBox zu laden:


        private delegate void SetImageInPictureBoxCallback(Image img);

        private void setImageOnPictureBox(Image img)
        {
            if (pbImage.InvokeRequired)
            {
                SetImageInPictureBoxCallback s = new SetImageInPictureBoxCallback(setImageOnPictureBox);
                pbImage.Invoke(s, new object[] { img });
                return;
            }

            pbImage.Image = img;
        }

Das Invoke benötige ich, da vom Server ein Event ausgelöst wird, wenn ein Bild ankommt. Der Server läuft in einem eigenen Thread.

Bitte um Hilfe!

Vielen herzlichen Dank!

Grüße,
Stefan

B
357 Beiträge seit 2010
vor 11 Jahren

Was sagt denn der Debugger? Wird das ImageFormat korrekt gesetzt? An welcher Stelle fliegt das Ding weg (Try-Catch hilft da meist gut weiter, zusammen mit StackTrace-Infos). Die GDI+-Fehler hatte ich meist, wenn das Speichern fehlgeschlagen ist. Entweder durch konkurrierende Schreibzugriffe, oder weil ein Programmteil eben noch einen Handle auf das Ding hatte (evtl. die Picturebox?).

S
skullsplitter Themenstarter:in
23 Beiträge seit 2011
vor 11 Jahren

Hallo!

Danke für deine Hilfe 😃

Folgendes steht in der Exception:> Fehlermeldung:

bei System.Drawing.Image.Save(String filename, ImageCodecInfo encoder, EncoderParameters encoderParams)
bei System.Drawing.Image.Save(String filename, ImageFormat format)
bei Test2.saveAsToolStripMenuItem_Click(Object sender, EventArgs e) in
.......

Das Bildformat wird korrekt gesetzt.

Hier noch der Code, der das Empfangen des Bildes regelt:


        private void handleClientConn(object tcpClient)
        {
            TcpClient client = (TcpClient) tcpClient;
            NetworkStream clientStream = client.GetStream();
            Image returnedImg = null;

            byte[] img = new byte[1024];
            
            using (MemoryStream ms = new MemoryStream())
            {
                int read = 0;

                while ((read = clientStream.Read(img, 0, img.Length)) > 0)
                {
                    ms.Write(img, 0, read);
                }

                Image recImg = Bitmap.FromStream(ms, false, false);
                returnedImg = (Bitmap) recImg.Clone();
            }

            clientStream.Close();
            client.Close();

            if (returnedImg != null)
                evImageReceived(this, new ImageReceivedArgs((Bitmap)returnedImg.Clone()));

            returnedImg.Dispose();
        }

Dies wird pro Verbindung in einem eigenen Thread abgehandelt.

Die Exception fliegt genau in dieser Zeile in der im 1. Beitrag geschriebenen Methode:

private void saveAsToolStripMenuItem_Click(object sender, EventArgs e)
        {
                ....
                saveImg.Save(save.FileName, format);
        }

Es muss also, wie du gesagt hast, irgendwie mit konkurrierenden Schreibzugriffen oder einem Programmteil zusammenhängen, welcher noch ein Handle auf das Bild hat.

Ich wüsste nicht, wo die PictureBox nicht einen Handle auf das Bild hat oder wieso es irgendwie zu Rechteproblemen kommen könnte.

Habe beim Debuggen jetzt folgendes noch feststellen können:
Nachdem die oben genannte Exception geflogen ist und ich erneut 2x F5 gedrückt habe, kamen folgende Exceptions:> Fehlermeldung:

AccessViolationException:
Es wurde versucht, im geschützten Speicher zu lesen oder zu schreiben. Dies ist häufig ein Hinweis darauf, dass anderer Speicher beschädigt ist.

StackTrace:
bei System.Drawing.SafeNativeMethods.Gdip.GdipSaveImageToFile(HandleRef image, String filename, Guid& classId, HandleRef encoderParams)
bei System.Drawing.Image.Save(String filename, ImageCodecInfo encoder, EncoderParameters encoderParams)
bei System.Drawing.Image.Save(String filename, ImageFormat format)
bei Test2.saveAsToolStripMenuItem_Click(Object sender, EventArgs e) in .......

InvalidOperationException:
Das Objekt wird bereits an anderer Stelle verwendet.

StackTrace:
bei System.Drawing.Image.Save(String filename, ImageCodecInfo encoder, EncoderParameters encoderParams)
bei System.Drawing.Image.Save(String filename, ImageFormat format)
bei Test2.saveAsToolStripMenuItem_Click(Object sender, EventArgs e) in .......

Gruß
Stefan

Hinweis von gfoidl vor 11 Jahren

Fehlermeldungen bitte mit dem [error[nop][/nop]]-Tag angeben - od. per rot hinterlegtem Button in der Toolbar.

2.891 Beiträge seit 2004
vor 11 Jahren

Das Invoke benötige ich, da vom Server ein Event ausgelöst wird, wenn ein Bild ankommt. Der Server läuft in einem eigenen Thread.

EDIT: So, wie der Code da steht, ist deine setImageOnPictureBox-Methode aber falsch implementiert. Da, was nach dem If-Block vom InvokeRequired steht, muss in einen else-Zweig (und darf bei einem InvokeRequired==true nicht ausgeführt werden). Ich habe das return übersehen. Danke an ujr.

Ist das Verhalten bei allen Bildformaten gleich? Bei bestimmten Operationen muss man z.B. mit dem PNG-Format aufpassen.

S
skullsplitter Themenstarter:in
23 Beiträge seit 2011
vor 11 Jahren

Hallo,

ich habe die setImageOnPictureBox-Methode jetzt durchgestoppt.

Szenario 1: Anderer Thread will neues Bild setzen (Kommentare beachten!)


if (pbImage.InvokeRequired) //1. Aufruf ist true also ausführen
            {
                SetImageInPictureBoxCallback s = new SetImageInPictureBoxCallback(setImageOnPictureBox);
                pbImage.Invoke(s, new object[] { img }); //1.1 diese Methode erneut aufrufen
                return; //nach 2. Aufruf abbrechen
            }

            //2. Aufruf: Bild wird jetzt gesetzt
            pbImage.Image = img;

Szenario 2: GUI-Thread ruft Methode auf


if (pbImage.InvokeRequired) //1. Aufruf ist false also nicht ausführen
            {
                SetImageInPictureBoxCallback s = new SetImageInPictureBoxCallback(setImageOnPictureBox);
                pbImage.Invoke(s, new object[] { img }); 
                return; 
            }

            //1. Aufruf: einfach Bild setzen
            pbImage.Image = img;

Das Setzen des Bildes findet bei einem InvokeRequired = true also nicht direkt statt - oder habe ich jetzt einen Denkfehler drinnen?

Das Verhalten ist bei allen Bildformaten identisch. Snipping-Tool funktioniert ohne Probleme, wenn das Bild geschickt wurde, fliegt die Exception "Allgemeiner Fehler in GDI+.".

Ich habe beim Debuggen jetzt noch folgendes festgestellt:
Nachdem die oben genannte Exception geflogen ist und ich erneut F5 gedrückt habe, kamen folgende Exceptions:> Fehlermeldung:

AccessViolationException:
Es wurde versucht, im geschützten Speicher zu lesen oder zu schreiben. Dies ist häufig ein Hinweis darauf, dass anderer Speicher beschädigt ist.

StackTrace:
bei System.Drawing.SafeNativeMethods.Gdip.GdipSaveImageToFile(HandleRef image, String filename, Guid& classId, HandleRef encoderParams)
bei System.Drawing.Image.Save(String filename, ImageCodecInfo encoder, EncoderParameters encoderParams)
bei System.Drawing.Image.Save(String filename, ImageFormat format)
bei Test2.saveAsToolStripMenuItem_Click(Object sender, EventArgs e) in .......

Nach einem weiteren male F5:> Fehlermeldung:

InvalidOperationException:
Das Objekt wird bereits an anderer Stelle verwendet.

StackTrace:
bei System.Drawing.Image.Save(String filename, ImageCodecInfo encoder, EncoderParameters encoderParams)
bei System.Drawing.Image.Save(String filename, ImageFormat format)
bei Test2.saveAsToolStripMenuItem_Click(Object sender, EventArgs e) in .......

Ich weiß jetzt nicht genau, ob das daran liegt, dass ich einfach nochmals F5 gedrückt habe und er es erneut versucht hat, oder an etwas anderem?

Aber ich tippe jetzt mal darauf, dass es daran liegt, dass ich erneut F5 gedrückt habe.

Danke!

Gruße,
Stefan

Edit:
Habe die setImageOnPictureBox-Methode jetzt auf if(..) else .. umgebaut -> keine Veränderung.

2.891 Beiträge seit 2004
vor 11 Jahren

Also nochmal zum Nachvollziehen: Du hast zwei Threads. Einen Server-Thread und den GUI-Thread. Im Server-Thread bekommst du ein Byte-Array übertragen, machst daraus ein Bild; und übergibst das dann an den GUI-Thread, der das Bild an eine PictureBox hängt und es dann speichert.

Was du noch testen könntest: Übergibt mal statt dem Bild erstmal nur das Byte[] an den GUI-Thread und erstelle das Bitmap dann erst direkt im GUI-Thread.

U
1.688 Beiträge seit 2007
vor 11 Jahren

So, wie der Code da steht, ist deine setImageOnPictureBox-Methode aber falsch implementiert. Da, was nach dem If-Block vom InvokeRequired steht, muss in einen else-Zweig (und darf bei einem InvokeRequired==true nicht ausgeführt werden).

Da steht doch aber ein "return" im if() - insofern ist's doch korrekt.

Versuch mal, nach dem Schreiben der Daten den ms.Position auf 0 zu setzen.

Schon mal versucht, die Daten direkt nach dem Empfang auf die Platte zu schreiben? Handelt es sich um das richtige Image?
Und wäre das nicht sowieso viel einfacher?

S
skullsplitter Themenstarter:in
23 Beiträge seit 2011
vor 11 Jahren

Hallo,

ich glaube auch, dass das mit dem return eigentlich passt, weil in dem von dir geschriebenen Beitrag dN!3L wird es genau so gemacht:


void DoCheapGuiAccess ()
{
   if (ctrl.InvokeRequired) { // Wenn Invoke nötig ist, ...
      // dann rufen wir die Methode selbst per Invoke auf
      ctrl.Invoke (new MethodInvoker (DoCheapGuiAccess));
      return;
   }
   // eigentlicher Zugriff; läuft jetzt auf jeden Fall im GUI-Thread
   ctrl.Text = "Hello World!";
}

Habe meinen Code jetzt vorläufig auf If(..) else abgeändert, wie du gesagt hast.

Also nochmal zum Nachvollziehen: Du hast zwei Threads. Einen Server-Thread und den GUI-Thread. Im Server-Thread bekommst du ein Byte-Array übertragen, machst daraus ein Bild; und übergibst das dann an den GUI-Thread, der das Bild an eine PictureBox hängt und es dann speichert.

Was du noch testen könntest: Übergibt mal statt dem Bild erstmal nur das Byte[] an den GUI-Thread und erstelle das Bitmap dann erst direkt im GUI-Thread.

Ja, 2 Threads.
1x Server (wartet auf Verbindungen)
1x Hauptthread (GUI-Thread)

Wenn ein Client (Smartphone) eine Verbindung aufbaut, kommt nochmals ein Thread dazu, in dem das Bild empfangen und das ImageReceived-Event ausgelöst wird, wenn das Bild komplett empfangen wurde (siehe Methode handleClientConn im 3. Beitrag)
Dieser Client-Connected-Thread wird im Server-Thread gestartet.
Ich habe beim Debuggen nochmals darauf geachtet:
das Speichern/Setzen des Bildes für die PictureBox findet im GUI-Thread statt (pictureBox.Image = ...)

Ich habe das Bild jetzt nur als Byte-Array an den GUI-Thread übergeben und wie du gesagt hast das Bild erst hier erstellt. Leider ergab das keine Änderung. Beim Speichern tritt der Fehler wieder auf. Ich habe beim Debuggen auch darauf geachtet, dass das Erstellen des Bildes im GUI-Thread stattfindet!

Versuche jetzt gleich einmal das Bild direkt zu speichern und setze die Position des MemoryStreams nach dem Schreiben auf 0

Was meinst du mit "handelt es sich um das richtige Image"?

@ujr:
Wie würdest du das leichter realisieren?

Danke 😃

Gruß
Stefan

2.891 Beiträge seit 2004
vor 11 Jahren

Da steht doch aber ein "return" im if() - insofern ist's doch korrekt.

Ja, hast recht - return übersehen. Ich nehme alles zurück und behaupte das Gegenteil. 😉

Ich habe das Bild jetzt nur als Byte-Array an den GUI-Thread übergeben und wie du gesagt hast das Bild erst hier erstellt. Leider ergab das keine Änderung. Beim Speichern tritt der Fehler wieder auf. ?

Hm, das ist seltsam.
Ich denke mal, bevor wir hier nur noch weiter rumraten solltest du mal [Tutorial] Vertrackte Fehler durch Vergleich von echtem Projekt mit minimalem Testprojekt finden.

U
1.688 Beiträge seit 2007
vor 11 Jahren

Was meinst du mit "handelt es sich um das richtige Image"?

Du empfängst Daten und speicherst sie ohne weitere Umwandlung auf HD. Das sollte doch dem originalen Bild entsprechen. Tut es das?
Und mit "leichter" meinte ich genau dies - empfangene Daten werden ohne Umwandlung durch GDI gespeichert.

Hast Du schon mal, nur testweise, das Dispose vom Image aus dem Empfangsthread genommen? Tritt auch dann der Fehler auf?

S
skullsplitter Themenstarter:in
23 Beiträge seit 2011
vor 11 Jahren

Hallo,

ich fange gleich an zu heulen...^^

Ich habe den Code für den Server und was dazu benötigt wird in ein Test-Projekt übernommen. Das Resultat war identisch.

Ich habe die Empfangs-Methode jetzt so umgebaut (in einem kleinen Testprojekt):


        private void handleClientConn(object tcpClient)
        {
            TcpClient client = (TcpClient)tcpClient;
            NetworkStream clientStream = client.GetStream();
            Image returnedImg = null;

            byte[] img = new byte[1024];

            string ip = ((IPEndPoint)client.Client.RemoteEndPoint).Address.ToString();

            MemoryStream ms = new MemoryStream();
            int read = 0;

            while ((read = clientStream.Read(img, 0, img.Length)) > 0)
            {
                ms.Write(img, 0, read);
            }

            Image recImg = Bitmap.FromStream(ms, false, false);

            recImg.Save(@"C:\test1.jpeg"); //wird gespeichert
            returnedImg = (Bitmap)recImg.Clone();

            clientStream.Close();
            client.Close();

            returnedImg.Save(@"C:\test2.jpeg"); //wird gespeichert

            if (returnedImg != null)
                evImageReceived(this, new ImageReceivedArgs((Bitmap)returnedImg.Clone()));

            returnedImg.Dispose();
        }

Seit ich den MemoryStream nicht mehr mit using(...) verwende, funktioniert alles - auch das Speichern über den GUI-Thread. (im Test- sowie Hauptprojekt!)

Das Bild entspricht dem aufgenommenem Bild. Soweit passt das also.

Kann sich das einer erklären?

Habe ein bisschen gegooglet und herausgefunden, dass nach dem Using-Block der MemoryStream auf den GC wartet und solange nicht alles frei gibt.

Ich vermute, dass so der MemoryStream die Operationen geblockt hat... aber wieso hat das so gravierende Auswirkungen auf geclonte Objekte??

Theoretisch könnte ich die Dispose() im Client-Connection-Thread ebenfalls weglassen, weil alle Ressourcen nachdem der Thread beendet wurde, freigegeben werden, oder?

Wenn ich nach dem returnedImg.Dispose(); den MemoryStream mit Close() und dann Dispose() schließe, funktioniert wieder nichts mehr und ich erhalten beim Speichern wieder die GDI+ Fehler...^^

Danke!

Gruß,
Stefan

49.485 Beiträge seit 2005
vor 11 Jahren

Hallo skullsplitter,

Habe ein bisschen gegooglet und herausgefunden, dass nach dem Using-Block der MemoryStream auf den GC wartet und solange nicht alles frei gibt.

using hat mit dem GC quasi nichts zu tun. using sorgt dafür, dass in jedem Fall Dispose aufgerufen wird. Das Aufräumen des Speichers des Objekts erfolgt davon unabhängig durch den GC zu einem beliebigen (späteren) Zeitpunkt, ohne dass darauf gewartet wird.

Ich vermute, dass so der MemoryStream die Operationen geblockt hat... aber wieso hat das so gravierende Auswirkungen auf geclonte Objekte??

Das glaube ich nicht. Aber Bitmap-Objekte (ob nun geklont oder nicht) brauchen ihren Basis-Stream die ganze Lebensdauer über offen. Wenn man ihnen den Stream unter den Hintern weg schließt - und das passiert ja durch using/Dispose - kann es negative Effekte geben.

Theoretisch könnte ich die Dispose() im Client-Connection-Thread ebenfalls weglassen, weil alle Ressourcen nachdem der Thread beendet wurde, freigegeben werden, oder?

Alle Objekte existieren thread-übergreifend. Das Beenden eines Threads alleine bewirkt also nichts. Nur wenn durch das Verlassen der ThreadStart-Methode die letzte (in einer lokalen Variable enthaltene) Referenz auf ein Objekt entfernt wird, hat das Auswirkungen. Der GC kann das Objekt ohne verbleibende Referenz dann wegräumen. Das bedeutet aber nicht, dass er es sofort tut.

herbivore

S
skullsplitter Themenstarter:in
23 Beiträge seit 2011
vor 11 Jahren

Hallo!

Danke für die aufschlussreiche Antwort!

Habe ein bisschen gegooglet und herausgefunden, dass nach dem Using-Block der MemoryStream auf den GC wartet und solange nicht alles frei gibt.
using hat mit dem GC quasi nichts zu tun. using sorgt dafür, dass in jedem Fall Dispose aufgerufen wird. Das Aufräumen des Speichers des Objekts erfolgt davon unabhängig durch den GC zu einem beliebigen (späteren) Zeitpunkt, ohne dass darauf gewartet wird.

Alles klar - dann habe ich mich wohl falsch informiert. 🙁

Ich vermute, dass so der MemoryStream die Operationen geblockt hat... aber wieso hat das so gravierende Auswirkungen auf geclonte Objekte??
Das glaube ich nicht. Aber Bitmap-Objekte (ob nun geklont oder nicht) brauchen ihren Basis-Stream die ganze Lebensdauer über offen. Wenn man ihnen den Stream unter den Hintern weg schließt - und das passiert ja durch using/Dispose - kann es negative Effekte geben.

Das heißt, dass ich soviel clonen und umkopieren kann wie ich will.
Das erhaltene Bild wird sich immer auf seine Basis referenzieren.

Also:
Bild 1 = Bild welches ich erhalten habe
Bild1.Clone() = Basis-Stream bleibt Bild 1
Bild1.Clone().Clone() = Basis-Stream ist Bild1.Clone() oder Bild 1?

Wenn ich jetzt einen Clone dispose, hat das keine Auswirkungen. Ich muss also besonders darauf achten, dass der Basis-Stream erhalten bleibt, dann dürfte es keine Probleme geben.

Theoretisch könnte ich die Dispose() im Client-Connection-Thread ebenfalls weglassen, weil alle Ressourcen nachdem der Thread beendet wurde, freigegeben werden, oder?
Alle Objekte existieren thread-übergreifend. Das Beenden eines Threads alleine bewirkt also nichts. Nur wenn durch das Verlassen der ThreadStart-Methode die letzte (in einer lokalen Variable enthaltene) Referenz auf ein Objekt entfernt wird, hat das Auswirkungen. Der GC kann das Objekt ohne verbleibende Referenz dann wegräumen. Das bedeutet aber nicht, dass er es sofort tut.

Das stimmt allerdings... hätte ich eigentlich selber auch wissen müssen.... X(

Danke!

Gruß
Stefan

49.485 Beiträge seit 2005
vor 11 Jahren

Hallo skullsplitter,

Wenn ich jetzt einen Clone dispose, hat das keine Auswirkungen.

100%ig sicher bin ich nicht, aber ich gehe davon aus, dass

a) bei einem mit Clone geklonten Bitmap-Objekt beide Objekte auf denselben Strom referenzieren, beide Objekte würden sich also den Strom teilen,
b) bei einem Dispose eines Bitmap-Objekts ein Dispose des referenzierten Stroms durchgeführt wird.

Wenn das beides stimmt, würde das eine Bitmap-Objekt beim Dispose zwangsläufig den Strom des anderen Objekts unter den Hintern weg schließen.

In Bezug auf Punkt b) bin ich mir recht sicher. Punkt a) ist eine Vermutung, weil Clone typischerweise shallow (und nicht deep) implementiert ist.

Der beste Weg, ein Objekt zu klonen, ohne dass es Probleme mit dem Stream gibt, ist ein neues Bitmap-Objekt gleicher Größe und gleichem Pixelformat zu erzeugen und das alte Bild per DrawImage in das neue zu zeichnen.

Der beste Weg, ein Objekt aus einer Bilddatei zu öffnen, ohne dass die Datei geöffnet bleibt, ist den Dateiinhalt in ein Bitmap-Array zu laden (File.GetAllBytes), aus dem Bitmap-Array einen MemoryStream zu erzeugen und aus diesem das Bitmap-Objekt zu erstellen (Image.FromStream; natürlich ohne den MemoryStream zu schließen/disposen, solange das Bitmap-Objekt lebt).

herbivore

S
skullsplitter Themenstarter:in
23 Beiträge seit 2011
vor 11 Jahren

Hallo,

hmh - wenn ich dich jetzt richtig verstanden habe läuft das also so ab:

In meinem MemoryStream befindet sich das empfangene Bild.

Ich erzeuge aus dem Stream mein Bild 1 (Image.FromStream()).

Wenn ich jetzt von dem erzeugten Bild 1 ein Clone anfertige (= Bild 2), bleibt die Referenz auf den MemoryStream bestehen.

Bild 2 wird an das Event übergeben und dann an einem anderen Ort im Programm weiterverarbeitet...

Nachdem das Event ausgelöst wurde und abgehandelt, wird Dispose von Bild 2 aufgerufen.

(Dieser gesamte Vorgang (schreiben der empfangenen Daten in den MemoryStream, clonen, Event auslösen) läuft in einem eigenen Thread, welcher die Verbindung zum Client abhandelt - sprich der Code, welcher ausgeführt wird nachdem das Event ausgelöst wurde, wird ebenfalls in diesem Thread ausgeführt)

So sieht der Code derzeit aus (er funktioniert, hatte bisher keine Probleme mehr; die Kommentare beziehen sich auf den 3 Zeilen drüber beschriebenen Vorgang ^^):


.....
            MemoryStream ms = new MemoryStream();
            int read = 0;

            while ((read = clientStream.Read(img, 0, img.Length)) > 0)
            {
                ms.Write(img, 0, read);
            }

            Image recImg = Bitmap.FromStream(ms, false, false); //Bild 1
            returnedImg = (Bitmap)recImg.Clone(); //Bild 2
            
            clientStream.Close();
            client.Close();

            if (returnedImg != null)
                evImageReceived(this, new ImageReceivedArgs(returnedImg));

            returnedImg.Dispose(); //Bild 2

Theoretisch müsste dann diese Zeile returnedImg.Dispose(); //Bild 2 zur Folge haben, dass der Basisstrom (MemoryStream) geschlossen wird, richtig?

Ich habe das Clone eingefügt, da das ein Versuch von mir war den GDI+ Fehler zu beheben. Wirklich gebraucht wird das rumgeclone in diesem Stück Code eigentlich nicht, oder? Könnte ja gleich das Bild, welches ich mit Image.FromStream() erhalte, beim Auslösen des Events mitgeben.

Nachdem das Event ausgelöst wurde, wird dieser Code ausgeführt:


            Image recImg = (Bitmap)e.ReceivedImage;
 
//clonen des Bildes und Ausgabe an der GUI
            Image pbImg = (Bitmap)recImg.Clone();
            setImageOnPictureBox(pbImg);

//clonen des Bildes und decoden
            Image parseImg = (Bitmap)recImg.Clone();
            ParserResult res = reader.decodeImage((Bitmap)parseImg);

            if (res != null)
                    ..... else.....
//dispose aufrufen für das Bild, welches zum decoden verwendet wurde
//wird nicht mehr benötigt
            parseImg.Dispose(); 

(habe den Code von nicht benötigtem Code befreit, damit es lesbarer ist; daher "...." steht für Code, der nichts mehr mit dem Bild macht)

Hier nochmals der Code, der für das Setzen des Bildes in der PictureBox zuständig ist:


        private delegate void SetImageInPictureBoxCallback(Image img);

        private void setImageOnPictureBox(Image img)
        {
            if (pbImage.InvokeRequired)
            {
                SetImageInPictureBoxCallback s = new SetImageInPictureBoxCallback(setImageOnPictureBox);
                pbImage.Invoke(s, new object[] { img });
                return;
            }

            pbImage.Image = img;
        }

Ich clone im oben gezeigten Code (2. gezeigter Code) die Bilder aus folgendem Grund:
Wenn ich das Bild in die PictureBox setze und gleich danach an den "reader" übergebe, bekomme ich die Meldung, dass das Objekt derzeit wo anders verwendet wird.

Daher clone ich zweimal - das Clonen hat dazu geführt, dass diese Meldung nicht mehr gekommen ist. In PictureBox.Image Property steht, dass man Bilder, die zweimal eingesetzt werden, clonen sollen (siehe Punkt Remarks - Note)

If you want to use the same image in multiple PictureBox controls, create a clone of the image for each PictureBox. Accessing the same image from multiple controls causes an exception to occur.

Dies bezieht sich zwar auf den Einsatz des selben Bildes in mehreren PictureBoxen, allerdings habe ich mir gedacht, dass das sicherlich nicht nur für weitere PictureBoxen gilt, sondern auch für eventuelle weitere Verarbeitungsprozesse. Es hat auch Wirkung gezeigt und das Problem behoben.

Wenn ich jetzt allerdings am Ende wieder das Dispose des geclonten Bildes aufrufe, müsste der bereits dispose vom Basisstream (MemoryStream) aufrufen, richtig?

Dann ist dieser Codeabschnitt fertig und es wird zurück in den Code gesprungen, indem das Event ausgelöst wurde. (1. gezeigter Code in diesem Beitrag).

Darauf folgt der Aufruf von Dispose vom 1. geclonten Bild (=Bild 2). Zu diesem Zeitpunkt dürft der Basisstream (MemoryStream) garnicht mehr existieren, da bereits die Kopie von der Kopie der Kopie Dispose aufgerufen hat, wenn ich dich richtig verstanden habe, oder? ...^^

Oder habe ich da jetzt etwas falsch verstanden?

In Image.Dispose Method steht, dass die von diesem Bild verwendeten Ressourcen wieder für weitere Zwecke freigegeben werden, wenn die Dispose-Methode aufgerufen wird. Könnte es also auch sein, dass vom Basisstream nicht die Dispose-Methode aufgerufen wird?

Calling the Dispose method allows the resources used by this Image to be reallocated for other purposes.

Ich habe jetzt in Image.FromStream-Methode (Stream) noch nachgelesen, dass der Stream, welche verwendet wird um das Bild zu erzeugen, immer offen sein muss (daher kam auch der Fehler von mir... wie bereits in der Thread-Überschrift steht).

Hinweise
Der Stream muss für die Lebensdauer von Image geöffnet bleiben.

Der Stream wird auf 0 (null) zurückgesetzt, wenn diese Methode in der Folge mit demselben Stream aufgerufen wird.

Ich habe auch gesehen, dass es für die Klasse Bitmap einen Konstruktor gibt, der ein Objekt des Types Image erwartet. (siehe Bitmap Constructor (Image)). Erzeugt dieser Konstruktor ein neues Bitmap Objekt nach der von dir beschriebenen Methode:

Der beste Weg, ein Objekt zu klonen, ohne dass es Probleme mit dem Stream gibt, ist ein neues Bitmap-Objekt gleicher Größe und gleichem Pixelformat zu erzeugen und das alte Bild per DrawImage in das neue zu zeichnen.

oder wird das Originalbild in einem Attribute gespeichert und es wird im Endeffekt immer auf das Originalbild zugegriffen?

Konnte das nicht herausfinden. 😦

Es gibt auch einen Konstruktor, der einen Stream erwartet (siehe Bitmap Constructor (Stream)). Ist dieser äquivalent zu Bitmap.FromStream()?

Vielen, vielen herzlichen Dank 😃

Immer schön, wenn man mehr ins Detail erfährt 😃

Danke!

Grüße,
Stefan

49.485 Beiträge seit 2005
vor 11 Jahren

Hallo skullsplitter,

sprich der Code, welcher ausgeführt wird nachdem das Event ausgelöst wurde, wird ebenfalls in diesem Thread ausgeführt

nein, der Schluss stimmt so nicht. In welchen Thread ein Event abonniert wird, ist vollkommen egal. Ein EventHandler läuft ohne weiteres Zutun immer in dem Thread, der den Event auslöst/feuert. Nur auf letzteres kommt es also an.

Immer schön, wenn man mehr ins Detail erfährt

Ich weiß nicht, man kann es auch übertreiben. Die Antworten auf die meisten deiner Fragen ergeben sich ohnehin aus dem weiter oben breits gesagten. Auf andere Fragen, z.B. ob es einen funktionalen Unterschied zwischen new Bitmap (Image) und Bitmap.Clone gibt, kann ich keine verbindliche Antwort geben, ohne selber erst im Detail nachzuforschen. Und schließlich interpretierst du an manchen Stellen etwas hinein, was gar nicht da steht, z.B. ist mit "to be reallocated for other purposes" gemeint, dass freigegebener Speicher für anderen Zwecke neu vergeben werden kann, nicht dass die Ressourcen, speziell der Strom, nach dem Dispose noch irgendwie weitergenutzt werden kann. Im Gegenteil, "dispose" heißt "zerstören" und genau das passiert.

herbivore

S
skullsplitter Themenstarter:in
23 Beiträge seit 2011
vor 11 Jahren

sprich der Code, welcher ausgeführt wird nachdem das Event ausgelöst wurde, wird ebenfalls in diesem Thread ausgeführt
nein, der Schluss stimmt so nicht. In welchen Thread ein Event abonniert wird, ist vollkommen egal. Ein EventHandler läuft ohne weiteres Zutun immer in dem Thread, der den Event auslöst/feuert. Nur auf letzteres kommt es also an.

Da hast du mich glaube ich falsch verstanden oder ich mich falsch ausgedrückt.

Ich habe damit genau das gemeint, was du in anderen Worten gesagt hast.

Immer schön, wenn man mehr ins Detail erfährt
Ich weiß nicht, man kann es auch übertreiben. Die Antworten auf die meisten deiner Fragen ergeben sich ohnehin aus dem weiter oben breits gesagten. Auf andere Fragen, z.B. ob es einen funktionalen Unterschied zwischen new Bitmap (Image) und Bitmap.Clone gibt, kann ich keine verbindliche Antwort geben, ohne selber erst im Detail nachzuforschen. Und schließlich interpretierst du an manchen Stellen etwas hinein, was gar nicht da steht, z.B. ist mit "to be reallocated for other purposes" gemeint, dass freigegebener Speicher für anderen Zwecke neu vergeben werden kann, nicht dass die Ressourcen, speziell der Strom, nach dem Dispose noch irgendwie weitergenutzt werden kann. Im Gegenteil, "dispose" heißt "zerstören" und genau das passiert.

herbivore

Alles klar. 😁

Naja - damit wäre das Thema eh erledigt.