Merkwürdiges Verhalten beim Zeichnen von Linien auf Bitmap

  • .NET (FX) 4.5–4.8
  • WPF

Es gibt 24 Antworten in diesem Thema. Der letzte Beitrag () ist von kafffee.

    Merkwürdiges Verhalten beim Zeichnen von Linien auf Bitmap

    Hallo zusammen :) ich hoffe mal das ist das richtige Unterforum für dieses Thema, wenn nicht, bitte verschieben...

    ich habe ein Bitmap (einer WaveForm), das in einem Image-Control (entsprechend WinForms-PictureBox), welches auf einem Canvas liegt, angezeigt wird. Dort möchte ich einige verschiedene Linien zeichnen, einmal die Aufnahmeposition, die Playbackposition und eine Position, die man per Klick auf das Image setzen kann. Dabei kann man auch per Button das Image auf dem Canvas nach links oder rechts verschieben.

    Hier mein Code:

    Allgemein:

    Spoiler anzeigen

    VB.NET-Quellcode

    1. Public Function TransformToPixels(Units As Double) As Integer 'diese Funktion wandelt die in WPF gängigen Units in "bitmapfähige" Pixel um
    2. Using g As Graphics = Graphics.FromHwnd(IntPtr.Zero)
    3. Dim retValue As Integer = CInt(((g.DpiX / 96) * Units))
    4. Return CInt(retValue - (retValue * (MainModule.HoleSkalierung - 1)))
    5. End Using
    6. End Function
    7. Public Sub UpdateWaveForm(sender As Object, e As EventArgs) 'Aufruf über Dispatcher des MainWindows
    8. Try
    9. Services.ServiceContainer.GetService(Of IMainWindowService)?.HoleDispatcher().Invoke(Sub() WellenFormZeichnen())
    10. Catch
    11. End Try
    12. End Sub


    Die Properties:

    Spoiler anzeigen

    VB.NET-Quellcode

    1. Private _RecordingXPosWF As Double 'Aufnahmeposition (rote Linie)
    2. Public Property RecordingXPosWF As Double
    3. Get
    4. Return _RecordingXPosWF
    5. End Get
    6. Set(value As Double)
    7. _RecordingXPosWF = value
    8. RaisePropertyChanged()
    9. End Set
    10. End Property
    11. Private _PlayXPosWF As Double 'die Playbackposition (grüne Linie)
    12. Public Property PlayXPosWF As Double
    13. Get
    14. Return _PlayXPosWF
    15. End Get
    16. Set(value As Double)
    17. If value <= RecordingXPosWF Then
    18. _PlayXPosWF = value
    19. PlayBackPosition = TimeSpan.FromSeconds(GetPositionInSeconds(_PlayXPosWF))
    20. RaisePropertyChanged()
    21. Else
    22. If IsPlaying Then
    23. Bass.BASS_ChannelStop(PlayBackChannel)
    24. Else
    25. Dim OKVM = New OKDialogViewModel
    26. OKVM.Meldung = "Du versuchst die Abspielposition auf einen ungültigen Wert festzulegen. Bitte stelle sicher, dass du die Abspielposition links von oder direkt auf dem Aufnahmefortschritt platzierst."
    27. Services.ServiceContainer.GetService(Of IMainWindowService)?.HoleDispatcher().Invoke(Sub() dialogService.ShowModalDialog("", OKVM, Me, True, False, Services.WindowStyle.None, Services.ResizeMode.NoResize, 500, Services.SizeToContent.Height, Services.WindowStartupLocation.CenterOwner, ""))
    28. End If
    29. End If
    30. End Set
    31. End Property
    32. ​Private _ClickedXPosWF As Double 'klickbare Position (gelbe Linie)
    33. Public Property ClickedXPosWF As Double
    34. Get
    35. Return _ClickedXPosWF
    36. End Get
    37. Set(value As Double)
    38. If value <= RecordingXPosWF Then
    39. _ClickedXPosWF = value
    40. MarkerPosition = TimeSpan.FromSeconds(GetPositionInSeconds(value))
    41. RaisePropertyChanged()
    42. Else
    43. Dim OKVM = New OKDialogViewModel
    44. OKVM.Meldung = "Du versuchst den Marker auf einen ungültigen Wert festzulegen. Bitte stelle sicher, dass du die Markerposition links von oder direkt auf dem Aufnahmefortschritt platzierst."
    45. dialogService.ShowModalDialog("", OKVM, Me, True, False, Services.WindowStyle.None, Services.ResizeMode.NoResize, 500, Services.SizeToContent.Height, Services.WindowStartupLocation.CenterOwner, "")
    46. End If
    47. End Set
    48. End Property
    49. Private _Zoom As Integer
    50. Public Property Zoom As Integer 'hier kann man zoomen, so dass die Bitmap eine höhere Auflösung hat dadurch, dass sie einfach breiter gezeichnet wird
    51. Get
    52. Return _Zoom
    53. End Get
    54. Set(value As Integer)
    55. _Zoom = value
    56. RaisePropertyChanged()
    57. UpdateWaveForm(Nothing, Nothing)
    58. End Set
    59. End Property
    60. Private _WFBreite As Double
    61. Public Property WFBreite As Double 'Die Breite des Image-Controls, gebunden an ActualWidth
    62. Get
    63. Return _WFBreite
    64. End Get
    65. Set(value As Double)
    66. _WFBreite = value
    67. RaisePropertyChanged()
    68. End Set
    69. End Property
    70. Private _WFXPosOnCanvas As Double
    71. Public Property WFXPosOnCanvas As Double 'Die X-Position des Image-Controls auf dem Canvas
    72. Get
    73. Return _WFXPosOnCanvas
    74. End Get
    75. Set(value As Double)
    76. _WFXPosOnCanvas = value
    77. RaisePropertyChanged()
    78. End Set
    79. End Property


    Das tatsächliche Zeichnen der Linien und erstellen des Bitmaps:

    Spoiler anzeigen

    VB.NET-Quellcode

    1. Public Sub WellenFormZeichnen()
    2. If WF IsNot Nothing Then
    3. If RecordingXPosWF <> 0 Then
    4. Dim WellenForm As Bitmap = WF.CreateBitmap(CInt(TransformToPixels(WFBreite)) * Zoom, CInt(TransformToPixels(WFHoehe) * MainModule.HoleSkalierung), -1, -1, False) 'Bitmap erzeugen
    5. Dim Grafik As Graphics = Graphics.FromImage(WellenForm)
    6. Dim Stift As System.Drawing.Pen
    7. If (RecordingXPosWF <> Nothing) OrElse (RecordingXPosWF <> 0) Then
    8. Stift = New System.Drawing.Pen(System.Drawing.Color.Red)
    9. Grafik.DrawLine(Stift, CSng(TransformToPixels(RecordingXPosWF) * Zoom), 0, CSng(TransformToPixels(RecordingXPosWF) * Zoom), TransformToPixels(WFHoehe)) 'Aufnahmeposition einzeichnen (rot)
    10. End If
    11. If (ClickedXPosWF <> Nothing) OrElse (ClickedXPosWF <> 0) Then
    12. Stift = New System.Drawing.Pen(System.Drawing.Color.Yellow)
    13. Grafik.DrawLine(Stift, CSng(ClickedXPosWF * Zoom - WFXPosOnCanvas), 0, CSng(ClickedXPosWF * Zoom - WFXPosOnCanvas), TransformToPixels(WFHoehe)) 'klickbare Position enizeichnen (gelb)
    14. End If
    15. If (PlayXPosWF <> Nothing) OrElse (PlayXPosWF <> 0) Then
    16. Stift = New System.Drawing.Pen(System.Drawing.Color.Green)
    17. Grafik.DrawLine(Stift, CSng(PlayXPosWF * Zoom - WFXPosOnCanvas), 0, CSng(PlayXPosWF * Zoom - WFXPosOnCanvas), TransformToPixels(WFHoehe)) 'Playbackposition einzeichnen (grün)
    18. End If
    19. WaveForm = BitmapToImageSource(WellenForm)
    20. Else
    21. WaveForm = BitmapToImageSource(WF.CreateBitmap(CInt(TransformToPixels(WFBreite)) * Zoom, CInt(TransformToPixels(WFHoehe) * MainModule.HoleSkalierung), -1, -1, False))
    22. End If
    23. Else
    24. WaveForm = Nothing
    25. End If
    26. End Sub​


    Wenn die Aufnahme gestoppt wird, wird Folgendes ausgeführt:

    Spoiler anzeigen

    VB.NET-Quellcode

    1. Private Sub RecordingStopped()
    2. Bass.BASS_ChannelStop(RecordingChannel)
    3. Dim Prozent As Double = BytesWritten / MaxTrackLaengeInBytes * 100 'ermitteln der Aufnahmeposition
    4. RecordingXPosWF = TransformToPixels(WFBreite) / 100 * Prozent 'wenn ich hier WFBreite nicht zu Pixel transformiere, bekomme ich augenscheinlich das gleiche Ergebnis, was eigentlich nicht sein kann??
    5. WF.RenderRecording(RenderBuffer, RenderLength)
    6. UpdateWaveForm(Nothing, Nothing)
    7. If Not IsPlaying Then PeakTimer.Stop()
    8. PeakL = 0
    9. PeakR = 0
    10. RecordingOrStoppedIcon = RecordingIcon
    11. HasRecorded = True
    12. IsRecording = False
    13. End Sub


    Nun zu den Problemen:

    (1) Im untersten Spoiler in Zeile 6 scheint das TransformToPixels keinen Effekt zu haben. Denn egal wie ich es mache, die Position scheint immer korrekt gezeichnet zu werden. Eigentlich ja gut, aber in irgendeiner sehr speziellen Situation könnte mir das um die Ohren fliegen... Der Code geht definitv auch an dieser Stelle vorbei, hab es mit Haltepunkt getestet...

    (2) Wenn ich, nachdem RecordingStopped ausgeführt wurde, den Zoom erhöhe und das Image auf dem Canvas verschoben habe, und dann wieder den Zoom verändere, dann werden häufig die gelbe und grüne Linie falsch gezeichnet, was ich nicht verstehe. Siehe angehängte Screenshots. Bei dem fehlerbehafteten Screenshot sind die gelbe und rote Linie auf einmal rechts von der roten Linie, was nicht sein darf, und dich verstehe nicht, wie das zu Stande kommen kann...
    Bilder
    • mit Fehler.PNG

      4,79 kB, 419×377, 54 mal angesehen
    • fehlerfrei.PNG

      7,78 kB, 697×384, 57 mal angesehen
    @kafffee Das ganze ist doch sehr WPF-Lastig, also Thread umdeklarieren und verschieben lassen.
    Zu Snippet 1:

    VB.NET-Quellcode

    1. Using g As Graphics = Graphics.FromHwnd(IntPtr.Zero)
    ist nicht nötig, da g.DpiX seinen Wert über die Laufzeit nicht ändert.
    Also diesen Wert vorab ermitteln und als Property einsetzen, das sollte dann schneller laufen.
    =====
    Zu Snippet 2, alle Setter:
    Rufe RaisePropertyChanged() nur auf, wenn sich der Wert der Property geändert hat, das sollte dann schneller laufen.

    VB.NET-Quellcode

    1. Set(value As Double)
    2. Dim changed = _RecordingXPosWF <> value
    3. _RecordingXPosWF = value
    4. If changed Then
    5. RaisePropertyChanged()
    6. End If
    7. End Set

    =====
    Zu Snippet 3:
    Mach einfach übereal

    VB.NET-Quellcode

    1. Stift = System.Drawing.Pens.Red

    =====
    Zu Deinen eigentlichen Problemen fehlt doch noch etwas Drum herum.
    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!

    kafffee schrieb:

    Was brauchste?
    So viel Code, um Deinen Effekt reproduzieren zu können.
    Mach ein kleines Testprojekt (wenn es geht in WinForm mit Framework), das Deinen Effekt reprofuziert.
    Möglicherweise hilft Dir das sogar schon Deinen Fehler selbst zu finden.
    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!
    @RodFromGermany

    Alles klar ich versuch das mal in WinForms zu reproduzieren. Kann aber dauern...



    Edit:
    wenn es geht in WinForm mit Framework


    Wird halt schwierig das genau so zu machen, alldieweil die ganze Sache mit den TransformToPixels.... siehst du eine Möglichkeit das zu "simulieren" für WinForms, weil das ist glaub ich echt von zentraler Bedeutung in diesem Thread....? Oder kommst du etwa sogar mit einem einfachen WPF-Projekt klar?

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

    kafffee schrieb:

    etwa sogar
    So ist es.
    Ich bin halt ein Dinosaurier. ;)
    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!
    Wenn das Projekt compiliert, schau ich mal rüber.
    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!
    Bin grad am Debuggen des Testprogramms. Jetzt bekomme ich hier, wenn ich einen Haltepunkt setze, die Meldung, dass RecordingXPosWF = NotANumber ist...

    Was ist denn da los?:

    VB.NET-Quellcode

    1. Public Sub WellenFormZeichnen()
    2. If WF IsNot Nothing Then
    3. If RecordingXPosWF <> 0 Then
    4. [...]


    Liegt das daran, dass ich im Setter der Property RecordingXPosWF das RaiseProperty rausgenommen hab? Ich bin bisher davon ausgegangen, dass dies bloss für die Aktualisierung der UI von Bedeutung ist...

    kafffee schrieb:

    Ich bin bisher davon ausgegangen, dass dies bloss für die Aktualisierung der UI von Bedeutung ist...
    Dann musst Du mal das RaiseProperty verfolgen, dorrt kommt normalerweise Dein Code.
    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!

    RodFromGermany schrieb:

    Wenn das Projekt compiliert, schau ich mal rüber.


    Voilà, hab tatsächlich geschafft das Problem nachzustellen. Alles schön in einer einzigen Klasse (MainWindow.xaml.vb), ich denke damit kommst du klar. Die anderen Klassen kannst du denke ich ignorieren.

    github.com/kafffee/WaveFormTest

    Alle anderen Mitleser, wenn ihr wollt auch mal anschauen :)

    In den Zeilen 14, 32 und 133 müsst ihr je nach Aufnahmegerät noch was nachbessern, hab ich aber kommentiert.

    imgWF ist das Image-Control!

    Wer Fragen hat gerne melden :)

    Es werden noch die bass.net.dll und bass.dll benötigt, gibt es hier:

    un4seen.com/
    @kafffee Wenn man aus WF.WAVEFORMDRAWTYPE.Mono => WaveForm.WAVEFORMDRAWTYPE.Mono (usw.) macht, lässt es sich compilieren.
    Wie muss ich das Programm bedienen, um Deinen Effekt zu reproduzieren?
    Und:

    Mach das global: Visual Studio – Empfohlene Einstellungen
    und Du musst niemals woeder Option Strict On schreiben.
    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!

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

    Bei mir ließ es sich wie es ist kompilieren... Komisch...

    Vergiss nicht, in den mir genannten Zeilen ggf. die Werte zu ändern.... Du kannst die Werte von deinem Audioeingang in der Soundsystemsteuerung unter den Eigenschaften des Aufnahmegeräts einsehen. Die Skalierung musst du ebenfalls ggf. ändern, nimm den Wert aus den Windows-Einstellungen geteilt durch 100.

    Um dem Fehler zu erzeugen, klicke links unten um die Aufnahme zu starten. Nach ca. 15 Sekunden wieder stoppen mit einem Klick auf den selben Button.

    Dann erzeugst du die Linien.

    Die rote Linie ist die RecordingPosition. Die gelbe Linie erzeugst du durch einen Linksklick links von der roten Linie, die grüne Linie genauso aber mit Rechtsklick.

    Dann erhöhe den Zoom auf 5 oder so.

    Dann verschiebe die Waveform ein bisschen mit dem Button Vor- oder Zurückspulen.

    Dann schraube wieder am Zoom rum.

    Das sollte den Fehler reproduzieren. Kann sein dass du bisschen rumspielen musst, ich hab noch kein eindeutiges Muster erkennen können.

    kafffee schrieb:

    Dann erzeugst du die Linien.
    Wie?
    Maus-rechts-Links-Klick, habs gefunden.
    ===============
    Der Zoom-Faktor wird falsch oder gar nicht an die Linien-Position übertragen.
    Sollte die Linien-Darstellung nicht dem Canvas überlassen werden?
    =====
    Die Maus-Klick-Position passt nicht zum Zoom.
    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!

    Dieser Beitrag wurde bereits 2 mal editiert, zuletzt von „RodFromGermany“ ()

    RodFromGermany schrieb:

    Der Zoom-Faktor wird falsch oder gar nicht an die Linien-Position übertragen.

    Das müsste an den Settings liegen. Siehe angehängter Screenshot. Wenn in der Combobox '"1-Kanal (statt 2-Kanal) steht, musst du in Zeile 14 StereoOn = False machen. Ausserdem in Zeile 32, wenn du z.B. in den Windowseinstellungen eine Skalierung von 125% hast, dann 1.25 returnen.
    Zeile 133 ist so ausgelegt, dass das Standard-Aufnahmegerät verwendet werden soll. Also musst du ebenfalls in der Soundsystemsteuerung das Gerät als Standardgerät festlegen (über das Kontextmenü des Geräts).
    Und eins noch, das hatte ich übersehen:
    In Zeile 137 musst du als erstes Argument die gleiche Samplingrate des Geräts wie festgelegt übergeben (zu finden in s.Screenshot, also z.B. 44100 wen da steht 44100 Hz).

    RodFromGermany schrieb:

    Sollte die Linien-Darstellung nicht dem Canvas überlassen werden?

    Ja das hatte ich zuerst auch so gemacht, hielt es dann aber für leichter, die Linien direkt ins Bitmap zu zeichnen, weil man müsste sonst beim Vor- und Zurückspulen das jedes Mal neu berechnen...

    Edit:

    Und gleich noch n Bug entdeckt. Wenn du eine gelbe oder grüne Linie erzeugst, die nah dran ist an der roten Linie, kommt meine Meldung, dass du versuchst, den Marker an eine ungültige Position festzulegen. Das darf nicht sein, ich hatte das eigentlich schon getestet und für gut befunden...
    Bilder
    • soundsyscontrol.PNG

      50,34 kB, 1.024×674, 51 mal angesehen

    Dieser Beitrag wurde bereits 2 mal editiert, zuletzt von „kafffee“ ()

    kafffee schrieb:

    weil man müsste sonst beim Vor- und Zurückspulen das jedes Mal neu berechnen...
    Dann musst Du die Position der Linien bei jeder Zoom-Änderung neu berechnen.
    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!
    @kafffee Da passiert zu viel WPF-Hintergrund-Zeugs, wovon ich keine Ahnung habe:
    Z.B. Beim Klicken auf +-Zoom wird wo die Update-Routine aufgerufen?
    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!
    Direkt im Setter von Zoom.

    Wird häufig bei WPF so gemacht, dass man jetzt keinen Eventhandler hat, z.B. wenn du ne ScrollBar hast, wird der Code nicht in einem EventHandler ausgeführt, also statt z.B. Private Sub ScrollBar1_Scrolled(...)Handles ScrollBar1.Scrolled bindet man eine Property vom ScrollBar, also (Value) an eine Property im Code, z.B. Volume und führt dann die anfallenden Arbeiten im Setter aus:

    VB.NET-Quellcode

    1. Private _Volume As Single
    2. Public Property Volume As Single
    3. Get
    4. Return _Volume
    5. End Get
    6. Set(value As Single)
    7. _Volume = value
    8. [zusätzlicher Code]
    9. RaisePropertyChanged 'UI updaten
    10. End Set
    11. End Property
    @kafffee In der Prozedur WellenFormZeichnen(), der Faktor Zoom muss immer auf die Differenz angewedndet werden, kommt zwei Mal vor.

    VB.NET-Quellcode

    1. If (ClickedXPosWF <> Nothing) OrElse (ClickedXPosWF <> 0) Then
    2. Stift = Pens.Yellow
    3. Grafik.DrawLine(Stift, CSng(ClickedXPosWF - Canvas.GetLeft(imgWF)) * Zoom, 0, CSng(ClickedXPosWF - Canvas.GetLeft(imgWF)) * Zoom, TransformToPixels(imgWF.Height))
    4. End If
    Und:
    Hole ist das Loch, Whole ist das Ganze. ;)
    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!