Warten, bis Bitmap gezeichnet wurde

  • VB.NET
  • .NET (FX) 4.5–4.8

Es gibt 16 Antworten in diesem Thema. Der letzte Beitrag () ist von ErfinderDesRades.

    Warten, bis Bitmap gezeichnet wurde

    Hallo Leute, ich bin's mal wieder.

    Ich arbeite nach wie vor an meinem Overlay-Programm - und habe mittlerweile auch einige Dinge getauscht, um es soweit funktionsfähig zu machen - zwar noch nicht so wie ich das letztendlich möchte, aber daran arbeite ich dann später - in ruhiger Minute.
    Nun zu meinem Problem (in Kurzform):

    Ich habe ein Programm, das quasi wie folgt arbeitet:

    auf dem Control-Panel wird ein Button geklickt

    Button -> führt Sub aus, das den Wert in einer Spalte um 1 erhöht.

    Hiernach werden kurze Berechnungen ausgeführt, ob der Button noch weiter aktiv werden darf

    Hiernach wird eine Sub gestartet, mit der eine Bitmap vom Panel erstellt und gespeichert werden soll.

    Diese Bitmap wird dann von einem Stream-Programm kontinuierlich eingelesen.

    Funktioniert soweit auch gut. Jetzt habe ich nur das Problem, dass wenn man zu schnell hintereinander klickt, die Bitmaps zwar auch korrekt gezeichnet werden (das letzte überschreibt immer das davor), nur die Streaming-Software bei zu schneller Neuzeichnung bei einem alten Bitmap hängen bleibt (und manchmal auch gar keines findet). Ich vermute, dass bei zu schnellem Neuerstellen der Datei, das Streaming-Programm versucht die Datei neu einzulesen, während diese noch gar nicht erstellt wurde. Bzw. auch, dass wenn das Neuzeichnen Event zu schnell getriggert wird, er anfangen möchte die Datei zu zeichnen, obwohl die vorherige noch gar nicht fertig ist.

    Deshalb nun zu meiner Frage:

    Kann ich irgendwie mit meinem Programm "warten" bis die Bitmap-Zeichnung fertig ist, bevor weiterer Code ausgeführt wird? Ich habe ja gedacht, dass weiterer Code danach erst ausgeführt wird, wenn der komplette Prozess dahinter fertig ist. Aber anscheinend wird nach dem Befehl des Speicherns der Bitmap schon weitergemacht, während das speichern in die Datei noch andauert und unter Windows erstellt wird.

    Ich habe schon gedacht, es ergäbe vllt Sinn, das Zeichnen der Bitmap in einen Backgroundworker auszulagern und bei aktivem BGW die Aktion abzubrechen und neu anzusetzen (also aktuelle Bilder verwerfen, wenn es neue gibt). Doch da ich mit der Bitmapmethode auch GUI-Aktionen ausführe, bekomme ich immer STA-Thread Fehlermeldungen.

    Ic hhabe schon versucht, mich in Threads und dergleichen reinzulesen. Doch im Augenblick raucht mir der Kopf, weil ich nicht ganz verstehe, wer z.B. Invoke ausführen muss, wo der Delegate stehen muss, wann ich invoken muss... etc. Ich bin noch nicht lange dabei, arbeite mich aber immer weiter an die ganzen Thematiken ran. Habt also bitte etwas Nachsicht mit mir :) Danke
    Na, dann würde ich mal sagen, dass Threads eher der falsche Ansatz sind. Du willst ja warten, bis das Ganze fertig ist. Nebenläufigkeit wird aber verwendet, um eben nicht warten zu müssen, bis irgendwas fertig ist. Vielleicht reicht ein kleines, konstantes Zeitpolster. Vielleicht reicht auch eine Prüffunktion, die versucht, das Bitmap zu speichern oder zu laden.
    Dieser Beitrag wurde bereits 5 mal editiert, zuletzt von „VaporiZed“, mal wieder aus Grammatikgründen.

    Aufgrund spontaner Selbsteintrübung sind all meine Glaskugeln beim Hersteller. Lasst mich daher bitte nicht den Spekulatiusbackmodus wechseln.
    ja, an sowas dachte ich eigtl auch schon. Aber ich wollte so etwas wie Thread.sleep(x) nicht verwenden. Dann kam ich auf die Idee mit Ticks zu arbeiten und beim Drücken des Knopfes den Start anzukündigen, los gelaufen werden darf aber erst wenn der Tick-Timer wieder 0 erreicht... Aber ich frage mich, ob man das nicht noch sinnvoller gestalten kann. Sleep wollte ich nicht verwenden, weil es den GUI-Thread ja sperrt. Deshalb war die Tick-Lösung schon besser... aber gibt es da noch was besseres? und auch bei der Tick-Lösung muss ich ja in einem anderen Thread den Befehl für den Marsch geben, oder irre ich mich hier ?
    Und wie wär es, wenn Du die Bitmapfile nicht dem Dienst direkt zur Verfügung stellst und sie erst dann an die Position schiebst, wo der Diest sie erwartet, wenn sie fertig ist?
    Aber das ist jetzt alles Spekulatius, solange ich nix konkretes habe oder gar ein Testprojekt von Dir.
    Dieser Beitrag wurde bereits 5 mal editiert, zuletzt von „VaporiZed“, mal wieder aus Grammatikgründen.

    Aufgrund spontaner Selbsteintrübung sind all meine Glaskugeln beim Hersteller. Lasst mich daher bitte nicht den Spekulatiusbackmodus wechseln.
    ja, die Idee klingt auch nicht schlecht, aber dann würde ich ja unter Umständen nicht nur beim Schreiben sondern auch beim Verschieben/Kopieren der Datei einen Stau verursachen. Ich versuche mal den Code (abgespeckt) hier zu formulieren, um das verständlicher zu machen:

    Das Control Panel hat zum Beispiel den Button für +1:

    VB.NET-Quellcode

    1. Public Class ControlPanel
    2. Dim GameMode as Integer 'Enum für GameMode
    3. ...
    4. Private Sub btnPlus1_Click_1(sender As Object, e As RoutedEventArgs)
    5. If GameMode = Settings.GameMode.Ranked Then
    6. frmRankedPanel.AddWin() ' Füge Win zum Ranked-Panel - und führt noch Hintergrundberechnungen durch und überschreibt Werte im Control
    7. frmRankedPanel.RefreshUI() 'aktualisiere Grafik, um vor dem Speichern in der Bitmap aktuell zu sein
    8. BitMapMaker.CreateBitmap(frmEventPanel) 'erzeuge Bitmap für RP
    9. ElseIf (GameMode = Settings.GameMode.GameModeConstructedEvent) Or (GameMode = Settings.GameMode.GameModeCustom) Or
    10. (GameMode = Settings.GameMode.GameModeTraditionalDraft) Or (GameMode = Settings.GameMode.GameModeRankedDraft) Or
    11. (GameMode = Settings.GameMode.GameModeTraditionalEvent) Or (GameMode = Settings.GameMode.GameModeSealed) Then
    12. frmEventPanel.AddWin() ' Füge Win zum Ranked-Panel - und führt noch Hintergrundberechnungen durch und überschreibt Werte im Control
    13. frmEventPanel.RefreshUI() 'aktualisiere Grafik
    14. BitMapMaker.CreateBitmap(frmEventPanel) 'Erzeuge Bitmap für EP
    15. Else
    16. Dim frmErr As New OK("Missing GameMode", "Select a GameMode before starting a new run.")
    17. frmErr.ShowDialog()
    18. End If
    19. End Sub
    20. End Class



    auf die zwei Panel-Klassen gehe ich mal nicht genauer ein, da sie nicht unbedingt notwendig sind für das Verständis. Es sind jeweils eine Klasse namens RankedPanel und EventPanel mit einigen WPF-Elementen.

    Der Bitmapmaker hat folgenden Aufbau (es gibt ihn jeweils als Konstruktor für jede Klasse):

    VB.NET-Quellcode

    1. Public Shared Sub CreateBitmap(ByRef reference As EventPanel, Optional outPath As String = "")
    2. Try
    3. 'Ausgabepfad erstellen
    4. Dim path As String
    5. If outPath = "" Then
    6. path = My.Settings.BitmapextractionPath
    7. If Not IO.Directory.Exists(path) Then IO.Directory.CreateDirectory(path)
    8. path += "/EventOverlay.png"
    9. Else
    10. path = outPath
    11. End If
    12. Dim DrawVis As DrawingVisual = New DrawingVisual()
    13. Dim rectangles As Rect = VisualTreeHelper.GetDescendantBounds(reference)
    14. Dim drawContext As DrawingContext = DrawVis.RenderOpen()
    15. Using drawContext
    16. Dim conBrush As VisualBrush = New VisualBrush(reference)
    17. drawContext.DrawRectangle(conBrush, Nothing, New Rect(rectangles.Size))
    18. End Using
    19. Dim renderBitmap As RenderTargetBitmap = New RenderTargetBitmap(dpi * reference.ActualWidth, dpi * reference.ActualHeight, dpi * 96, dpi * 96, PixelFormats.Pbgra32) 'dpi ist ein Auflösungskalierungsfaktor
    20. renderBitmap.Render(reference)
    21. Dim encoder As PngBitmapEncoder = New PngBitmapEncoder()
    22. encoder.Frames.Add(BitmapFrame.Create(renderBitmap))
    23. Dim fs As New IO.FileStream(path, IO.FileMode.Create)
    24. encoder.Save(fs)
    25. fs.Dispose()
    26. fs = Nothing
    27. Catch ex As Exception
    28. Debug.WriteLine(ex.Message)
    29. End Try
    30. End Sub​



    Das Problem hierbei ist, dass die Bitmap meist noch geschrieben wird, während der Hauptcode schon durch

    VB.NET-Quellcode

    1. ​BitMapMaker.CreateBitmap(frmEventPanel)


    durch ist. und da hätte ich ganz gerne eine Funktion, um entweder den Button zu deaktivieren bis das Bitmap fertig ist, oder das Zeichnen der Bitmap auf eine Queue zu legen und solange abarbeiten zu lassen, bis die aktuellste Bitmap fertig ist - oder eben den Befehl erst erneut absetzen zu lassen, wenn eine gewisse Zeit verstrichen ist, nur eben ohne das GUI irgendwie zu sperren.

    Aber schon einmal vielen Dank für deine Antworten :) Und auch Ideen hierzu :)
    @PadreSperanza Du kannst doch den Button disablen, bis die Funktion abgearbeitet wurde.
    Dein GameMode schreit mir sehr nach einem Flag-Enum.
    Falls Du diesen Code kopierst, achte auf die C&P-Bremse.
    Jede einzelne Zeile Deines Programms, die Du nicht explizit getestet hast, ist falsch :!:
    Ein guter .NET-Snippetkonverter (der ist verfügbar).
    Programmierfragen über PN / Konversation werden ignoriert!
    ja, den kann ich ohne weiteres disablen. Aber wie mache ich das richtig? Einfach am Anfang ein "isEnabled=False" und am Ende ein "isEnabled=True" bringt nicht den Erfolg, weil der Code ja ausgeführt wird, während unter Umständen die Bitmap noch nicht fertig ist. Weiterhin liefert die Funktion, die in die Bitmap schreibt, keinen Wert zurück, sodass ich das Ergebnis auch nicht abfragen kann.

    Was ein Flag (wenn damit Flanke gemeint ist) ist, weiß ich aus der Elektrotechnik, aber wie kann man sowas in einem Programm einsetzen und wie genau funktioniert das hier? In der E-Technik nutzt man ja den Trigger von steigender oder fallender Flanke. Wie genau implementiert man sowas zum Beispiel im Code? Mit Flags habe ich mich noch gar nicht beschäftigt :/
    Und wenn damit Flanken gemeint sind, bin ich mir nicht sicher, ob das tatsächlich hierzu passt - aber ich lasse mich sehr gerne belehren :)

    PadreSperanza schrieb:

    Aber wie mache ich das richtig?
    So:

    VB.NET-Quellcode

    1. BtnXyz.Enabled = False
    2. ' asynchron etwas tun
    3. BtnXyz.Enabled = True
    Falls Du diesen Code kopierst, achte auf die C&P-Bremse.
    Jede einzelne Zeile Deines Programms, die Du nicht explizit getestet hast, ist falsch :!:
    Ein guter .NET-Snippetkonverter (der ist verfügbar).
    Programmierfragen über PN / Konversation werden ignoriert!
    Quasi-Vollzitat des direkten Vorposts an dieser Stelle entfernt ~VaporiZed

    Okay. Das verstehe ich. Wenn ich nun aber versuche, das asynchron zu machen (zum Beispiel über den Backgroundworker [oder ist der hier auch fehl am Platz?]), dann erhalte ich eine STA-Thread-Meldung - Meines Wissens nach ausgelöst durch

    VB.NET-Quellcode

    1. Dim rectangles As Rect = VisualTreeHelper.GetDescendantBounds(reference)
    (zumindest geht hier bei Einzelschritt der Fehler los).

    Die asynchrone Methode wäre natürlich ideal, weil ich schon gelesen habe, dass man dann mittels "Wait" auf die Abarbeitung warten kann. Bezüglich der STA-Meldung habe ich bereits gelernt, dass ich alle GUI-relevanten Dinge nur im Hauptthread veranstalten darf - somit müsste ich das ja "invoken" und delegieren. Leider habe ich hier noch eine gedankliche Blockade. Aber vermutlich muss ich mir dieses Thema dann doch nochmal zu Gemüte führen :) Danke dir für die Idee

    Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „VaporiZed“ ()

    Falls Du diesen Code kopierst, achte auf die C&P-Bremse.
    Jede einzelne Zeile Deines Programms, die Du nicht explizit getestet hast, ist falsch :!:
    Ein guter .NET-Snippetkonverter (der ist verfügbar).
    Programmierfragen über PN / Konversation werden ignoriert!
    okay, das habe ich soweit verstanden und meinen Code entsprechend angepasst:

    VB.NET-Quellcode

    1. ​btnPlus1.IsEnabled = False
    2. frmEventPanel.AddWin()
    3. frmEventPanel.RefreshUI()
    4. Dim BG As Task(Of Integer) = BitMapMaker.CreateBitmap(frmEventPanel)
    5. Dim waiter As Integer = Await BG
    6. btnPlus1.IsEnabled = True


    wobei ich bei dem BitMapMaker auch anpassen musste:

    VB.NET-Quellcode

    1. ​Public Shared Async Function CreateBitmap(ByVal reference As EventPanel, Optional outPath As String = "") As Task(Of Integer)
    2. Try
    3. Dim path As String
    4. If outPath = "" Then
    5. path = My.Settings.BitmapextractionPath
    6. If Not IO.Directory.Exists(path) Then IO.Directory.CreateDirectory(path)
    7. path += "/EventOverlay.png"
    8. Else
    9. path = outPath
    10. End If
    11. Dim DrawVis As DrawingVisual = New DrawingVisual()
    12. Dim rectangles As Rect = VisualTreeHelper.GetDescendantBounds(reference)
    13. Dim drawContext As DrawingContext = DrawVis.RenderOpen()
    14. Using drawContext
    15. Dim conBrush As VisualBrush = New VisualBrush(reference)
    16. drawContext.DrawRectangle(conBrush, Nothing, New Rect(rectangles.Size))
    17. End Using
    18. Dim renderBitmap As RenderTargetBitmap = New RenderTargetBitmap(dpi * reference.ActualWidth, dpi * reference.ActualHeight, dpi * 96, dpi * 96, PixelFormats.Pbgra32)
    19. renderBitmap.Render(reference)
    20. Dim encoder As PngBitmapEncoder = New PngBitmapEncoder()
    21. encoder.Frames.Add(BitmapFrame.Create(renderBitmap))
    22. Dim fs As New IO.FileStream(path, IO.FileMode.Create)
    23. encoder.Save(fs)
    24. fs.Dispose()
    25. fs = Nothing
    26. Return 1
    27. Catch ex As Exception
    28. Debug.WriteLine(ex.Message)
    29. Return -1
    30. End Try
    31. End Function


    Das funktioniert sehr gut :) Vielen Dank. Aber darf ich - für mein Verständnis - noch eine Frage stellen diesbezüglich?

    Ich muss die Funktion beim btnPlus1 einfügen, da ich ja in dieser Prozedur auf die Abarbeitung der Zeile "BitMapMaker.createBitmap(...)" warten möchte. Um also Await einsetzen du können, muss diese Funktion als Async markiert werden. Das habe ich soweit verstanden. Nun musste ich aber beim BitMapMaker die Funktion auch als Async kennzeichnen, sonst hätte ich keinen "As Task(Of Integer) zurückgeben können". Nur meckert VS nun, dass in der Funktion keine Await-Funktion gegeben ist. Laut Artikel braucht es die auch nicht, weil das dann Synchron abläuft, aber muss das dann wirklich so, oder übersehe ich hier was?

    Oder sollte nur die BitMapMaker.CreateBitmap() als Async gekennzeichnet werden und soll ich dann einfach ab dem DrawingVisual das alles in eine Funktion auslagern und diese asynchron ausführen lassen bis zum Ende der BitMapMaker-Funktion?

    :) Vielen Dank
    1. mach das Async aus CreateBitmap weg

      VB.NET-Quellcode

      1. Public Shared Function CreateBitmap(ByVal reference As EventPanel, Optional outPath As String = "") As Integer

    2. ruf CreateBitmap anders auf, nämlich

      VB.NET-Quellcode

      1. btnPlus1.IsEnabled = False
      2. frmEventPanel.AddWin()
      3. frmEventPanel.RefreshUI()
      4. Dim waiter As Integer = Await Task.Run(Function() BitMapMaker.CreateBitmap(frmEventPanel))
      5. btnPlus1.IsEnabled = True
      Warum die Variable waiter heisst, weisst du hofflich - ich würde einen Integer nicht so nennen.
    Ja, das finde ich einleuchtend. Ich hatte nicht daran gedacht, dass ich das so schreiben kann. Das hilft :)

    ErfinderDesRades schrieb:


    Warum die Variable waiter heisst, weisst du hofflich - ich würde einen Integer nicht so nennen.

    Meinst du, dass ich waiter nicht nutzen solle, weil es nah an einem Schlüsselwort liegt? Oder weil man daraus nicht ablesen kann, dass es ein Integer ist? Ich weiß, dass man Variablen eigentlich einen Typen-Präfix voranstellt, aber da ich das gestern nur schnell noch zum Testen abgegeben habe, habe ich mir da weniger Gedanken drum gemacht

    VB.NET-Quellcode

    1. 'Schande auf mein Haupt

    PadreSperanza schrieb:

    Ich weiß, dass man Variablen eigentlich einen Typen-Präfix voranstellt
    Stimmt garnet!
    Nach MS-Richtlinie soll man sogar überhaupt nie Prefixe verwenden. (das halte ich aber wiederum für Quatsch - manchmal sind sie praktisch).
    Nein, ich weiss einfach nicht, warum das Ergebnis von BitMapMaker.CreateBitmap() numal waiter heissen soll. Es ist doch nur ein Ergebnis, eine Zahl - die kann doch nicht warten.
    Ich weiss aber auch nicht, warum BitMapMaker.CreateBitmap() eine Zahl zurückgibt - und scheinbar nur 1 oder -1.
    Am besten wäre, die Methode gibt überhaupt nix zurück, also mach eine Sub daraus.
    Ach so meinst du das.
    Das waiter hatte ich - wie gesagt -zusammengeknüppelt, um zu sehen, ob das Ergebnis so gemeint gewesen war.
    Die Funktion gibt etwas zurück, weil sie als Funktion etwas zurückgeben soll (deshalb 1 für erfolgreich, -1 für erfolglos). Ich war nun davon ausgegangen, nachdem ich mich auf der oben verlinkten Seite eingelesen hatte, dass immer etwas zurückgegeben werden müsse. So zumindest steht es im Artikel (es sei denn, ich habe ihn falsch verstanden, was ich nicht ausschließen kann). Deshalb hatte ich es so geschrieben.
    Allerdings: das war, bevor ich deinen Vorschlag gesehen hatte, denn du hast mittels "Await Tasks.Run(Function() ...) mir aufgezeigt, dass ich das ändern kann. Dann kann ich daraus auch eine Sub machen. Nach dem oben aufgeführten Beispiel war mir das noch nicht klar. Nach deinem Aufruf braucht es das so nicht mehr (oder ich übersehe hier wieder etwas).

    EDIT
    Wenn ich allerdings daraus eine Sub mache und es aufrufe mittels: Await Task.Run(Function() BitMapMaker(frmEventPanel)), dann bekomme ich von VisualStudio den Vermerk: Der Ausdruck ergibt keinen Wert
    EDIT 2:
    Wenn ich das als reine Sub schreibe und die Funktion nicht mehr Async ist, bekomme ich wieder einen Fehler, da das Element einem anderen Thread gehört.

    Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „PadreSperanza“ ()

    PadreSperanza schrieb:

    weil sie als Funktion etwas zurückgeben soll (deshalb 1 für erfolgreich, -1 für erfolglos).
    Dafür wäre ein Boolean prädestiniert:
    True - erfolgreich,
    False - erfolglos.
    Falls Du diesen Code kopierst, achte auf die C&P-Bremse.
    Jede einzelne Zeile Deines Programms, die Du nicht explizit getestet hast, ist falsch :!:
    Ein guter .NET-Snippetkonverter (der ist verfügbar).
    Programmierfragen über PN / Konversation werden ignoriert!