www.pudn.com > ObjectListViewDemo.zip > AnimatedGifRenderer.cs
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Threading;
using System.Windows.Forms;
using BrightIdeasSoftware;
namespace ObjectListViewDemo
{
///
/// This renderer displays an animated gif.
/// The aspect for this column must be a string that contains path to a file that contains an animated gif.
///
public class AnimatedGifRenderer : ImageRenderer
{
const int PropertyTagTypeShort = 3;
const int PropertyTagTypeLong = 4;
const int PropertyTagFrameDelay = 0x5100;
const int PropertyTagLoopCount = 0x5101;
private class AnimationState
{
///
/// Create an AnimationState in a quiet state
///
public AnimationState()
{
this.currentFrame = 0;
this.frameCount = 1;
this.imageDuration = new List();
this.image = null;
this.fileName = String.Empty;
}
///
/// Create an animation state for the given image, which may or may not
/// be an animation
///
/// The image to be rendered
public AnimationState(Image image) : this()
{
this.InitFromImage(image);
}
///
/// Create an animation state by loading an image from a file
///
/// File containing the image
public AnimationState(string fileName) : this()
{
this.fileName = fileName;
try {
this.InitFromImage(Image.FromFile(fileName));
} catch (FileNotFoundException) {
} catch (OutOfMemoryException) {
}
}
private void InitFromImage(Image image) {
this.image = image;
if (this.image == null)
return;
// Does the image contain an animation?
if (!(new List(this.image.FrameDimensionsList)).Contains(FrameDimension.Time.Guid))
return;
this.frameCount = this.image.GetFrameCount(FrameDimension.Time);
// Find the delay between each frame. The delays are stored an array of
// 4-byte ints. Each int is the number of 1/100th of a second that should elapsed
// before the frame expires
foreach (PropertyItem pi in this.image.PropertyItems) {
if (pi.Id == PropertyTagFrameDelay) {
for (int i = 0; i < pi.Len; i += 4) {
//TODO: There must be a better way to convert 4-bytes to an int
int delay = (pi.Value[i + 3] << 24) + (pi.Value[i + 2] << 16) + (pi.Value[i + 1] << 8) + pi.Value[i];
this.imageDuration.Add(delay * 10); // store delays as milliseconds
}
break;
}
}
// There should be as many frame durations as frames
Debug.Assert(this.imageDuration.Count == this.frameCount);
}
public bool IsValid {
get {
return this.image != null;
}
}
public bool IsAnimation {
get {
return this.frameCount > 1;
}
}
public string fileName;
public int currentFrame;
public long currentFrameExpiresAt;
public Image image;
public List imageDuration;
public int frameCount;
}
///
/// Create a renderer that will draw animated gifs into a column.
///
/// The one renderer draws all the animated gifs in the column. The state required to draw the gifs
/// is stored in the Tag of the associate subitem.
public AnimatedGifRenderer()
{
this.isPaused = false;
this.tickler = new System.Threading.Timer(new TimerCallback(this.OnTimer), null, 1000, Timeout.Infinite);
this.stopwatch = Stopwatch.StartNew();
}
///
/// Should the animations in this renderer be paused?
///
public bool Paused
{
get { return isPaused; }
set { isPaused = value; }
}
delegate void OnTimerCallback(Object state);
///
/// This is the method that is invoked by the timer. It basically switches control to the listview thread.
///
/// not used
public void OnTimer(Object state)
{
if (this.ListView == null || this.Paused) {
this.tickler.Change(1000, Timeout.Infinite);
} else {
if (this.ListView.InvokeRequired) {
this.ListView.Invoke(new OnTimerCallback(this.OnTimer), new object[] { state });
} else {
this.OnTimerInThread();
}
}
}
///
/// This is the OnTimer callback, but invoked in the same thread as the creator of the ListView.
/// This method can use all of ListViews methods without creating a CrossThread exception.
///
public void OnTimerInThread()
{
// If this listview has been destroyed, we can't do anything
if (this.ListView.IsDisposed)
return;
// If we're not in Detail view, we can't do anything at the moment, but we still reschedule
// the tickler because the view may change later.
if (this.ListView.View != System.Windows.Forms.View.Details) {
this.tickler.Change(1000, Timeout.Infinite);
return;
}
long elapsedMilliseconds = this.stopwatch.ElapsedMilliseconds;
int subItemIndex = this.Column.Index;
long nextCheckAt = elapsedMilliseconds + 1000; // wait at most one second before checking again
Rectangle updateRect = new Rectangle(); // what part of the view must be updated to draw the changed gifs?
// Run through all the subitems in the view for our column, and for each one that
// has a gif attached to it, see if the frame needs updating.
foreach (ListViewItem lvi in this.ListView.Items) {
// Get the gif state from the subitem. If there isn't a gif state, skip this row.
ListViewItem.ListViewSubItem lvsi = lvi.SubItems[subItemIndex];
AnimationState state = lvsi.Tag as AnimationState;
if (state == null || !state.IsValid || !state.IsAnimation)
continue;
// Has this frame of the animation expired?
if (elapsedMilliseconds >= state.currentFrameExpiresAt) {
state.currentFrame = (state.currentFrame + 1) % state.frameCount;
state.currentFrameExpiresAt = elapsedMilliseconds + state.imageDuration[state.currentFrame];
state.image.SelectActiveFrame(FrameDimension.Time, state.currentFrame);
// Track the area of the view that needs to be redrawn to show the changed images
if (updateRect.IsEmpty)
updateRect = lvsi.Bounds;
else
updateRect = Rectangle.Union(updateRect, lvsi.Bounds);
}
// Remember the minimum time at which a frame is next due to change
nextCheckAt = Math.Min(nextCheckAt, state.currentFrameExpiresAt);
}
if (!updateRect.IsEmpty)
this.ListView.Invalidate(updateRect);
this.tickler.Change(nextCheckAt - elapsedMilliseconds, Timeout.Infinite);
}
///
/// Calculate the image that should be drawn.
/// If the image is an animation, we setup an object to track its state.
///
/// Return the image that should be displayed
override protected Image GetImageFromAspect()
{
Image image = base.GetImageFromAspect();
if (image == null)
return null;
// If the tag is set but set to something we don't expect, we don't setup a state
if (this.OLVSubItem.Tag != null && !(this.OLVSubItem.Tag is AnimationState))
return image;
// If we don't have a gif state, make one. If we do, check that it's for the right file
AnimationState state = null;
if (this.OLVSubItem.Tag == null)
this.OLVSubItem.Tag = state = new AnimationState(image);
else {
state = (AnimationState)this.OLVSubItem.Tag;
}
// If we couldn't decode the file, don't show anything
if (!state.IsValid)
return null;
// After all that, finally return the image we want to show
return state.image;
}
private System.Threading.Timer tickler;
private Stopwatch stopwatch;
private bool isPaused;
}
}