Animierte GIFs in PictureBox anhalten für Hover-Effekt

  • VB.NET

Es gibt 72 Antworten in diesem Thema. Der letzte Beitrag () ist von RodFromGermany.

    @-Franky-

    Soweit, so gut. Nur wo packe ich den Code aus Form1_Load und Form1_Closing rein? Eine PictureBox bietet diese Ereignisse nicht...
    Einfach jeweils eine Methode in der Klasse erstellen und dann extern über die Form aufrufen oder geht das eleganter?

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

    kafffee schrieb:

    Projekt öffnen
    Welches Projekt?
    Form_Load in die Property GIFPath.Hab ich doch schon geschrieben.
    Form_Closing => Dispose()
    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

    Hab ein Windows Forms Projekt gemacht mit einem Benutzersteuerelement und einer Form zum Testen. Wenn alles läuft wollte ich die Form löschen, den Projekt Typ auf Klassenbibliothek ändern, das Ganze zu einer DLL kompilieren und feddich wäre mein eigenes Steuerelement.
    Ich glaub wir reden aneinander vorbei... Mein letzter Post bezieht sich aber auf ein anderes Projekt : Ein Test projekt zum Ausprobieren einer vererbten PictureBox

    EDIT: Achso jetzt hab ich verstanden wie du meinst alles klar :)

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

    kafffee schrieb:

    Ich glaub wir reden aneinander vorbei...
    Da müssen wir mal hart an uns arbeiten.
    Poste zunächst Deinen relevanten Code, insbesondere auch den Designer-Code, sonst raten wir hier, was Du sonst noch für Controls drinne hast.
    Um Dein UseerControl effektiv testen zu können zieh Dire kein Control auf die GUI sondern erstell in einer Button_Click in einem Using-Block Dein Control:

    VB.NET-Quellcode

    1. Using ggg = New AnimGifButton
    2. ggg.Show()
    3. End Using
    Auch wenn er da nix anzeigt, muss er wenigstens durchlaufen.
    Wenn das dann läuft, zueh eine Instanz auf die GUI.
    Vorher nicht.
    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:

    Poste zunächst Deinen relevanten Code, insbesondere auch den Designer-Code, sonst raten wir hier, was Du sonst noch für Controls drinne hast.


    Könnte ich jetzt machen, aber ich denke das ist Tinnef, ums mal mit deinen Worten auszudrücken. Ich hab das Vererben der PictureBox jetzt als Weg gewählt, der sogar noch besser ist für meine Zwecke. Wenn jetzt nicht noch irgendwas Unvorhergesehenes passiert, bleib ich auch dabei. Ich hab zwar vor paar Tagen schon erfolgreich ein kleines Control auf diese Weise gemacht, aber mit diesen merkwürdigen Fehlern lass ich lieber erstmal die Finger davon. Der Screenshot den ich gepostet hab war ja nicht das Einzige. U.A. hat der Code Editor auf einmal angefangen, meinen Code zu löschen, so dass ich dachte, die Entf-Taste auf meiner Tastatur hätte sich verklemmt. Also manchmal echt... ?(
    Wenns dich interessiert kann ich dir den DesignerCode ja trotzdem mal posten. Gib Bescheid! :)

    kafffee schrieb:

    aber mit diesen merkwürdigen Fehlern
    Die würden wir beheben.
    Ein Screenshot ist da nicht aussagekräftig, da nicht zu erkennen ist, wo der Fehler auftritt.
    Jou. Poste Code und Designercode, ggf. eine ResX, so vorhanden.
    Alles zippen und anhängen.
    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:

    Soweit, so gut. Nur wo packe ich den Code aus Form1_Load und Form1_Closing rein?

    Nun ja, es ist eine ganz normale Klasse die von PictureBox erbt. Sich also so verhält wie eine PictureBox. Jede Klasse besitzt eine Sub New bzw. Finalize. Für Deinen Fall "Form_Load", könntest Du die ganz normale Eigenschaft Image des Controls verwenden um dem Control eine GIF zuzuweisen (zur Entwurfs- oder zur Laufzeit). Nachteil, sobald Du das Programm startest, läuft auch die GIF in dem Control los. Das möchtest aber nicht. Hier würde sich zB. eine neue Property z.B. GifImage anbieten. Die neue Property steht Dir dann, nach einmaligen Starten des Programmes, dann auch zur Entwurfs- sowie zur Laufzeit zur Verfügung. Im Setter kannst ja dann das machen, was Du vorher im Form_Load gemacht hast (GIF in eine Variable speichern, Anzahl der Frames ermitteln usw). Um das Dispose der GIF musst Dich selber innerhalb der Klasse kümmern. Zum einen, falls Du dem Control eine neue GIF zuweist (muss die alte GIF vorher disposed werden bevor Du die neue GIF in eine Variable speicherst, kann man ja mit IsNothing prüfen) und spätesten in der Sub Finalize (Form_Closing).
    Mfg -Franky-
    @RodFromGermany
    Ist in Arbeit, bzw. ich probier das erstmal selber hinzukriegen...

    @-Franky-
    Ich probier den ganzen Tag schon dran rum, aber nichts wird besser, egal was mir einfällt...

    1. Problem:
    Beim Starten des Programms soll mein AnimGifButton ja bei der Variable Start die Wiedergabe anhalten. Es hält aber immer bei einem scheinbar zufälligen Frame an. Im MouseEnter-Event hingegen funktioniert das tadellos... Hab schon versucht den Code aus dem Setter der Property GIFPath in die Sub New() zu kopieren, das macht aber auch keinen Unterschied...
    2. Problem:
    Ich möchte, dass die Property StartFrame bei einem Klick auf das Control geändert wird. So weit, so gut. Wenn ich nun zur Laufzeit auf das Control klicke, ändert sich der StartFrame nicht, erst beim zweiten Klick ändert er sich und ich verstehe nicht warum. Hintergrund ist, dass das in diesem Fall ein Play/Pause-Button werden soll, der sobald draufgeklickt wird, logischerweise von der Play- in die Pause-Optik und umgekehrt geändert werden soll...

    Hier meine Klasse:

    VB.NET-Quellcode

    1. ​Public Class AnimGifButton
    2. Inherits PictureBox
    3. Private animGif As Bitmap
    4. Private animFrameCount As Integer
    5. Private animCurrentFrame As Integer
    6. Private Image As String = Me.GIFPath
    7. Private Start As Integer = Me.StartFrame
    8. Private StartGIF As Boolean = True
    9. Private Sub AnimGifButton_Click(sender As Object, e As EventArgs) Handles Me.Click
    10. Me.StartFrame = 1
    11. End Sub
    12. Public Property GIFPath As String
    13. Get
    14. Return Image
    15. End Get
    16. Set(value As String)
    17. Image = value
    18. If Image <> "" Then
    19. animCurrentFrame = StartFrame 'Initialisierung
    20. 'Dim imgAdd As Image = My.Resources.Add
    21. 'imgAdd.Save("C:\Users\Alpha\Desktop\20210307-1\EQTest\EQTest\bin\Debug\Add.gif", Imaging.ImageFormat.Gif)
    22. animGif = New Bitmap(Image)
    23. animFrameCount = animGif.GetFrameCount(New FrameDimension(animGif.FrameDimensionsList(0)))
    24. ImageAnimator.Animate(animGif, New EventHandler(AddressOf Me.Animate))
    25. End If
    26. StartGIF = True
    27. End Set
    28. End Property
    29. Public Property StartFrame As Integer
    30. Get
    31. Return Start
    32. End Get
    33. Set(value As Integer)
    34. Start = value
    35. ImageAnimator.Animate(animGif, New EventHandler(AddressOf Me.Animate))
    36. End Set
    37. End Property
    38. Private Sub AnimGifButton_Paint(sender As Object, e As PaintEventArgs) Handles Me.Paint
    39. If Image <> "" Then
    40. ImageAnimator.UpdateFrames()
    41. animCurrentFrame += 1
    42. If animCurrentFrame > animFrameCount Then
    43. ImageAnimator.StopAnimate(animGif, New EventHandler(AddressOf Me.Animate))
    44. animCurrentFrame = Start
    45. End If
    46. Using bmpFrame As New Bitmap(animGif, animGif.Width, animGif.Height)
    47. e.Graphics.SmoothingMode = Drawing2D.SmoothingMode.HighQuality
    48. e.Graphics.InterpolationMode = Drawing2D.InterpolationMode.HighQualityBilinear
    49. e.Graphics.Clear(Me.BackColor)
    50. Dim dblRatio As Double = Math.Min(Convert.ToDouble(Me.Width) / animGif.Width,
    51. Convert.ToDouble(Me.Height) / animGif.Height)
    52. e.Graphics.DrawImage(bmpFrame, 0, 0, Convert.ToSingle(animGif.Width * dblRatio),
    53. Convert.ToSingle(animGif.Height * dblRatio))
    54. End Using
    55. End If
    56. End Sub
    57. Private Sub AnimGifButton_MouseEnter(sender As Object, e As EventArgs) Handles Me.MouseEnter
    58. If Image <> "" Then
    59. ImageAnimator.Animate(animGif, New EventHandler(AddressOf Me.Animate))
    60. End If
    61. End Sub
    62. Private Sub Animate(ByVal o As Object, ByVal e As EventArgs)
    63. If Image <> "" Then
    64. Me.Invalidate()
    65. End If
    66. End Sub
    67. Private Sub AnimGifButton_Disposed(sender As Object, e As EventArgs) Handles Me.Disposed
    68. If Image <> "" Then
    69. ImageAnimator.StopAnimate(animGif, New EventHandler(AddressOf Me.Animate))
    70. animGif.Dispose()
    71. End If
    72. End Sub
    73. Public Sub New()
    74. End Sub
    75. End Class
    @kafffee Wenn GifPath (=Image) nicht leer ist, dann dispose das vorhandene Bild bevor Du das neue setzt.
    Bei Animate(...) kannst Du ohne Test Me.Invalidate() aufrufen, dort wird derselbe Test noch mal gemacht.
    Den leeren parameterlosen Konstruktor kannst Du auch weglassen, der wird vom Framework eh generiert.
    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 GifPath (=Image) nicht leer ist, dann dispose das vorhandene Bild bevor Du das neue setzt.

    Hab ich gemacht:

    VB.NET-Quellcode

    1. If animGif IsNot Nothing Then
    2. animGif.Dispose()
    3. End If


    Der leere Konstruktor ist da bloss aus Testzwecken noch drin...

    Also dass das beim Laden der Form immer bei einem scheinbar zufälligen Frame anhält, ist mir ein Rätsel. Kann das sein, dass wenn die Form noch nicht ganz geladen ist, das ein oder andere Paint-Event "geschluckt" wird? Wie wärs wenn man das irgendwie in einen parallelen Thread packt? Oder übersehe ich da was?
    Wenn ich den Pfad im Designer setze, hat animFrameCount den Wert 1, sollte 18 sein.
    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:

    Bei mir funktioniert das.
    Ich merke grade, dass meine GIF ein anderes Format hat, da kommt tatsächlich 1 raus, der IrfanView zeigt 18. ;(
    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
    Auf die schnelle zusammen gezimmert (ohne check ob man hier und da noch was ändern könnte). Damit solltest zurecht kommen bzw kannst Du an Deine Bedürfnisse anpassen.
    Spoiler anzeigen

    VB.NET-Quellcode

    1. Option Strict On
    2. Option Explicit On
    3. Public Class Form1
    4. Private Sub Form1_Load(sender As Object, e As EventArgs) Handles Me.Load
    5. AnimGifButton1.GifStartIndex = 7
    6. AnimGifButton1.GifImage = My.Resources.animake
    7. End Sub
    8. End Class
    9. Public Class AnimGifButton
    10. Inherits PictureBox
    11. Private Const PropertyFrameDelay As Integer = &H5100
    12. Private m_FrameCount As Integer = -1
    13. Private m_StartIndex As Integer = 0
    14. Private m_CurrIndex As Integer = 0
    15. Private m_Bitmap As Bitmap = Nothing
    16. Private m_FrameTimes As Byte()
    17. Private m_tmr As New Timer
    18. Public Sub New()
    19. AddHandler m_tmr.Tick, AddressOf Timer_Tick
    20. End Sub
    21. Protected Overrides Sub Finalize()
    22. m_tmr.Stop()
    23. If IsNothing(m_Bitmap) = False Then
    24. m_Bitmap.Dispose()
    25. End If
    26. RemoveHandler m_tmr.Tick, AddressOf Timer_Tick
    27. MyBase.Finalize()
    28. End Sub
    29. Public Property GifStartIndex As Integer
    30. Set(value As Integer)
    31. m_StartIndex = value
    32. End Set
    33. Get
    34. Return m_StartIndex
    35. End Get
    36. End Property
    37. Public Property GifImage As Bitmap
    38. Set(value As Bitmap)
    39. If IsNothing(m_Bitmap) = False Then
    40. m_Bitmap.Dispose()
    41. End If
    42. If IsNothing(value) = False Then
    43. m_tmr.Stop()
    44. m_Bitmap = value
    45. m_FrameCount = m_Bitmap.GetFrameCount(Imaging.FrameDimension.Time)
    46. m_FrameTimes = m_Bitmap.GetPropertyItem(PropertyFrameDelay).Value
    47. If m_StartIndex < 0 Then m_StartIndex = 0
    48. If m_FrameCount <> -1 Then If m_StartIndex > m_FrameCount Then m_StartIndex = m_FrameCount
    49. m_CurrIndex = m_StartIndex
    50. m_tmr.Interval = BitConverter.ToInt32(m_FrameTimes, (m_StartIndex * 4) Mod m_FrameTimes.Length) * 10
    51. m_Bitmap.SelectActiveFrame(Imaging.FrameDimension.Time, m_StartIndex)
    52. Me.Image = New Bitmap(m_Bitmap)
    53. End If
    54. End Set
    55. Get
    56. Return m_Bitmap
    57. End Get
    58. End Property
    59. Private Sub Timer_Tick(ByVal sender As Object, ByVal e As EventArgs)
    60. m_CurrIndex = (m_CurrIndex + 1) Mod m_FrameCount
    61. m_Bitmap.SelectActiveFrame(Imaging.FrameDimension.Time, m_CurrIndex)
    62. m_tmr.Interval = BitConverter.ToInt32(m_FrameTimes, (m_CurrIndex * 4) Mod m_FrameTimes.Length) * 10
    63. If IsNothing(Me.Image) = False Then Me.Image.Dispose()
    64. Me.Image = New Bitmap(m_Bitmap)
    65. If m_CurrIndex = m_StartIndex Then
    66. m_tmr.Stop()
    67. m_CurrIndex = m_StartIndex
    68. End If
    69. End Sub
    70. Private Sub AnimGifButton_MouseEnter(sender As Object, e As EventArgs) Handles Me.MouseEnter
    71. m_tmr.Start()
    72. End Sub
    73. End Class

    Mfg -Franky-
    @kafffee @-Franky- OK, blöde GIF.
    Baut mal diesen Test in den Setter ein, bevor die Image-Property, bevor m_Bitmap gesetzt wird:

    VB.NET-Quellcode

    1. If Not ImageAnimator.CanAnimate(value) Then
    2. Return
    3. End If
    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!

    -Franky- schrieb:

    Auf die schnelle zusammen gezimmert


    Haha du bist echt krass. Auf die Schnelle? Da hätt ich Tage für gebraucht wahrscheinlich...
    Spass beiseite:
    Ich hab die _Tick folgendermassen geändert:

    VB.NET-Quellcode

    1. Private MusicIsPlaying = False
    2. Private Sub Timer_Tick(ByVal sender As Object, ByVal e As EventArgs)
    3. If MusicIsPlaying = False Then
    4. m_CurrIndex = (m_CurrIndex + 1) Mod m_FrameCount
    5. Else
    6. m_CurrIndex = (m_CurrIndex - 1) Mod m_FrameCount
    7. End If
    8. If m_CurrIndex < 1 Then m_CurrIndex = m_FrameCount
    9. m_Bitmap.SelectActiveFrame(Imaging.FrameDimension.Time, m_CurrIndex)
    10. m_tmr.Interval = BitConverter.ToInt32(m_FrameTimes, (m_CurrIndex * 4) Mod m_FrameTimes.Length) * 10
    11. If IsNothing(Me.Image) = False Then Me.Image.Dispose()
    12. Me.Image = New Bitmap(m_Bitmap)
    13. If m_CurrIndex = m_StartIndex Then
    14. m_tmr.Stop()
    15. m_CurrIndex = m_StartIndex
    16. If MusicIsPlaying = True Then
    17. MusicIsPlaying = False
    18. Else
    19. MusicIsPlaying = True
    20. End If
    21. End If
    22. End Sub


    Hintergrund: Das soll wie gesagt ein Play/Pause-Button werden, ich verwende nun als Auslöser statt _MouseEnter das _Click-Event. Bei jedem Klick soll zwischen einem Play und Pause-Frame abgewechselt werden. D.h. das Ausgangsframe ist ein Play-Bild, das andere ein Pause-Bild. Bei einem Klick soll dann hochgezählt werden, beim nächsten Klick runtergezählt, so dass die Animation quasi rückwärts läuft. Das funktioniert so 1-3 Mal, dann kommt der Fehler: System.Runtime.InteropServices.ExternalException: "Allgemeiner Fehler in GDI+"

    Weisst du an was das liegt?
    @kafffee Setz einen Haltepunkt drauf. Wahrscheinlich war der Index negativ.
    Machst Du

    VB.NET-Quellcode

    1. If MusicIsPlaying Then
    2. m_CurrIndex = (m_CurrIndex + m_FrameCount - 1) Mod m_FrameCount
    3. Else
    4. m_CurrIndex = (m_CurrIndex + 1) Mod m_FrameCount
    5. End If

    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“ ()

    @-Franky-
    @RodFromGermany

    Alls gut habs jetzt so hingekriegt

    VB.NET-Quellcode

    1. Private Sub Timer_Tick(ByVal sender As Object, ByVal e As EventArgs)
    2. If MusicIsPlaying = False Then
    3. m_CurrIndex = (m_CurrIndex + 1) Mod m_FrameCount
    4. Else
    5. m_CurrIndex = (m_CurrIndex - 1) Mod m_FrameCount
    6. End If
    7. If m_CurrIndex < 0 Then m_CurrIndex = m_FrameCount - 1
    8. m_Bitmap.SelectActiveFrame(Imaging.FrameDimension.Time, m_CurrIndex)
    9. m_tmr.Interval = BitConverter.ToInt32(m_FrameTimes, (m_CurrIndex * 4) Mod m_FrameTimes.Length) * 10
    10. If IsNothing(Me.Image) = False Then Me.Image.Dispose()
    11. Me.Image = New Bitmap(m_Bitmap)
    12. If MusicIsPlaying = True Then
    13. If m_CurrIndex = m_StopIndex Then
    14. m_tmr.Stop()
    15. 'm_CurrIndex = m_StartIndex
    16. End If
    17. Else
    18. If m_CurrIndex = m_StartIndex Then
    19. m_tmr.Stop()
    20. End If
    21. End If
    22. End Sub
    23. Private Sub AnimGifButton_Click(sender As Object, e As EventArgs) Handles Me.Click
    24. m_tmr.Start()
    25. If MusicIsPlaying = True Then
    26. MusicIsPlaying = False
    27. Else
    28. MusicIsPlaying = True
    29. End If
    30. End Sub


    Einziges Manko: Beim ersten Klick ab Programmstart wird zuerst der ganze "Cycle" abgespielt und nicht bloss von m_StartIndex zu m_Stopindex. Ist bestimmt nur ein ganz kleines Problem, aber ic komm nicht drauf...

    Ich hab auch noch zwei Verständnisfragen:

    Warum das:

    VB.NET-Quellcode

    1. Public Sub New()
    2. AddHandler m_tmr.Tick, AddressOf Timer_Tick
    3. End Sub


    Warum nennst du das Ereignis nicht gleich m_tmr_Tick?

    Und warum:

    Protected Overrides Sub Finalize()

    und nicht einfach Private Sub Finalize()?

    Soll keine Kritik sein ich würde einfach nur gerne den Code verstehen.

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

    kafffee schrieb:

    Protected Overrides Sub Finalize()
    Weil das in der Basisklasse deklariert ist und vom Framework der in der "höchsten" Klasse aufgerufen werden soll.
    Du musst dann innerhalb des Finalizers die Basisklasse aufrufen, um deren Objekte ebenfalls zu zerstören:

    VB.NET-Quellcode

    1. Protected Overrides Sub Finalize()
    2. ' etwas tun
    3. MyNase.Finalizer()
    4. End Sub
    Wenn nix zu zerstören ist, kannst Du die Sub auch weglassen. Wenn sie da ist, dann muss die Basisklasse aufgerufen werden, um sicherzustellen, dass sie aufgeräumt wird!
    m_tmr_Tick: (private) Namen sind Schall und Rauch.
    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!