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
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