Animate Images in DataGridView

2020-05-04 16:14发布

问题:

I wrote the helper class below in order to animate images in a DataGridView, which is not working (images aren't animated).

Before that, I found some sample code on the wen, but they didn't work either.

I want to understand how this works instead of simply shove a piece of code inside my app just because it works. Why does my code not do what it is expected to?

Edit

I discovered the reason why it isn't working. The source DataTable itself don't contain images: they are assigned to DataGridView's Cells elsewhere in the code by its CellFormatting handler method. Since this event also triggers all the time, a fresh image object is always passed, so it keeps always showing the image's frame #1. When I created a new column with native image values stored in it, they animated as desired.

The question now is: is it possible to animate images that are assigned to the .FormattedValue property inside DataGridView's CellFormatting event handler method?

Public Class DataGridViewImageAnimator

    Private WithEvents MyDataGridView As DataGridView

    Public Sub New(dataGridView As DataGridView)

        MyDataGridView = dataGridView

    End Sub

    Private MyAnimatedImages As New Dictionary(Of Point, Image)

    Private Sub ImageAnimator_FrameChanged(sender As Object, e As EventArgs)

        Dim imageCells = MyDataGridView.Rows.Cast(Of DataGridViewRow).SelectMany(
            Function(dgvr) dgvr.Cells.OfType(Of DataGridViewImageCell))

        For Each cell In imageCells

            Dim img = TryCast(cell.FormattedValue, Image)

            If img IsNot Nothing AndAlso MyAnimatedImages.ContainsValue(img) Then

                MyDataGridView.InvalidateCell(cell)

            End If
        Next

    End Sub

    Private Sub MyDataGridView_CellPainting(
            sender As Object,
            e As DataGridViewCellPaintingEventArgs
            ) Handles MyDataGridView.CellPainting

        If e.ColumnIndex >= 0 AndAlso e.RowIndex >= 0 Then

            Dim cell = MyDataGridView(e.ColumnIndex, e.RowIndex)

            Dim drawPoint = MyDataGridView.GetCellDisplayRectangle(e.ColumnIndex, e.RowIndex, True).Location

            Dim pt = New Point(e.ColumnIndex, e.RowIndex)

            Dim cellImg = TryCast(cell.FormattedValue, Image)

            If MyAnimatedImages.ContainsKey(pt) AndAlso Equals(MyAnimatedImages(pt), cellImg) Then
                'If image is already registered as animated, and is still in cell

                ImageAnimator.UpdateFrames()

                e.Graphics.DrawImage(cellImg, drawPoint)

            Else

                If MyAnimatedImages.ContainsKey(pt) Then
                    'If image registered as animated is no longer in cell

                    ImageAnimator.StopAnimate(MyAnimatedImages(pt), AddressOf ImageAnimator_FrameChanged)

                    MyAnimatedImages.Remove(pt)

                End If

                If cellImg IsNot Nothing AndAlso ImageAnimator.CanAnimate(cellImg) Then
                    'If cell contains an image not yet registered as animated

                    MyAnimatedImages(pt) = cellImg

                    ImageAnimator.Animate(MyAnimatedImages(pt), AddressOf ImageAnimator_FrameChanged)

                    ImageAnimator.UpdateFrames()

                    e.Graphics.DrawImage(cellImg, drawPoint)

                End If

            End If

        End If

    End Sub

End Class

回答1:

A custom Column with custom Cells offers some advantages.

All the design logic is confined in one place and it can be selected as a Column template at design time using the DataGridView designer.


The performace is quite good (tested with 200 animated cells) and I didn't notice any flickering.

The animated Gifs can be Stretched or Zoomed as usual, using the designer settings, by code or manually resizing the Rows/Columns.

However, I can't consider it complete, because I couldn't find out a good way to start all the animations using this custom Column class methods or properties.

EDIT:
Added an Extension method to the DataGridView (DataGridView.Animate()).
This allows to hide the invalidating procedure.
After the DataGridView Data Binding is complete, simply call the extension method:

DataGridView1.DataSource = [DataSource]
DataGridView1.Animate()

The Module containing the Extension method:

Imports System.Runtime.CompilerServices

Module DGVExtesions

    <Extension()>
    Public Sub Animate(ByVal AnimatedGrid As DataGridView)
        Try
            For Each row As DataGridViewRow In AnimatedGrid.Rows
                For Each cell As DataGridViewCell In row.Cells.OfType(Of AnimatedDGVColumn.AnimatedCell)()
                    AnimatedGrid.InvalidateCell(cell)
                Next
            Next
        Catch ex As Exception
            Trace.WriteLine("Exception: {0}", ex.Message)
        End Try

    End Sub

End Module

Of course this is still not good enough. Some more research is needed.

This is the custom Animated Column class:

Imports System.ComponentModel
Imports System.Windows.Forms

Public Class AnimatedDGVColumn
    Inherits System.Windows.Forms.DataGridViewColumn

    Private custCellTemplate As AnimatedCell

    Public Sub New()
        Me.custCellTemplate = New AnimatedCell
        Me.custCellTemplate.ImageLayout = DataGridViewImageCellLayout.Zoom
        MyBase.CellTemplate = custCellTemplate
        Me.AutoSizeMode = DataGridViewAutoSizeColumnMode.None
        Me.DefaultCellStyle.Alignment = DataGridViewContentAlignment.MiddleCenter
    End Sub

    <Description("The ImageLayout in the Cells for this Column"), Category("Appearance")> _
    <EditorBrowsable(EditorBrowsableState.Always), Browsable(True)>
    Public Property ImageLayout As DataGridViewImageCellLayout
        Get
            Return Me.custCellTemplate.ImageLayout
        End Get
        Set(ByVal value As DataGridViewImageCellLayout)
            Me.custCellTemplate.ImageLayout = value
        End Set
    End Property

    Public Overloads Property CellTemplate As AnimatedCell
        Get
            Return Me.custCellTemplate
        End Get
        Set(value As AnimatedCell)
            Me.custCellTemplate = value
            MyBase.CellTemplate = value
        End Set
    End Property

    Public Class AnimatedCell
        Inherits System.Windows.Forms.DataGridViewImageCell

        Private Animation As Image
        Private IsAnimating As Boolean

        Public Sub New()
            Me.Animation = Nothing
            Me.IsAnimating = False
        End Sub

        Public Overloads Property ImageLayout() As DataGridViewImageCellLayout
            Get
                Return MyBase.ImageLayout
            End Get
            Set(ByVal value As DataGridViewImageCellLayout)
                MyBase.ImageLayout = value
            End Set
        End Property

        Protected Overrides Sub Paint(graphics As Graphics, clipBounds As Rectangle, cellBounds As Rectangle, rowIndex As Integer, elementState As DataGridViewElementStates, value As Object, formattedValue As Object, errorText As String, cellStyle As DataGridViewCellStyle, advancedBorderStyle As DataGridViewAdvancedBorderStyle, paintParts As DataGridViewPaintParts)
            If (IsDBNull(value)) OrElse (value Is Nothing) Then Return
            If Me.Animation Is Nothing Then
                Me.Animation = CType(formattedValue, Image)
            End If
            Animate()
            ImageAnimator.UpdateFrames()

            MyBase.Paint(graphics, clipBounds, cellBounds, rowIndex, elementState, Nothing, Me.Animation, errorText, cellStyle, advancedBorderStyle, paintParts)
        End Sub

        Private Sub Animate()
            If Me.IsAnimating = True Then Return
            If (Me.Animation IsNot Nothing) AndAlso ImageAnimator.CanAnimate(Me.Animation) = True Then
                ImageAnimator.Animate(Me.Animation, AddressOf Me.RotateFrame)
                Me.IsAnimating = True
            End If
        End Sub

        Private Sub RotateFrame(o As Object, e As EventArgs)
            If Me.RowIndex > -1 Then
                Me.DataGridView.InvalidateCell(Me)
            End If
        End Sub

        Public Overrides Function Clone() As Object
            Dim result As AnimatedCell = New AnimatedCell With {
                        .IsAnimating = False,
                        .Animation = Nothing,
                        .ImageLayout = Me.ImageLayout
                        }
            Return result
        End Function

    End Class

End Class