Reputation: 7352
I have a winforms
application, which has a gif on it for letting users know about stalling processes.
The problem is it plays much slower than it seems on other applications (chrome, internet explorer).
I have tried the gif on PictureBox
and Label
but resulting speed is same. Then after a little research I've come accross this question and the answer of legendary @Hans Passant, but unfortunately applying the boilerplate code suggested by him didn't make any difference.
Below is the simple reproducing code:
public partial class Form1 : Form
{
public Form1 ()
{
InitializeComponent();
timeBeginPeriod(timerAccuracy);
}
protected override void OnFormClosed ( FormClosedEventArgs e )
{
timeEndPeriod(timerAccuracy);
base.OnFormClosed(e);
}
// Pinvoke:
private const int timerAccuracy = 10;
[System.Runtime.InteropServices.DllImport("winmm.dll")]
private static extern int timeBeginPeriod ( int msec );
[System.Runtime.InteropServices.DllImport("winmm.dll")]
public static extern int timeEndPeriod ( int msec );
}
And the designer code if needed:
partial class Form1
{
private System.ComponentModel.IContainer components = null;
protected override void Dispose ( bool disposing )
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
private void InitializeComponent ()
{
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(Form1));
this.pictureBox1 = new System.Windows.Forms.PictureBox();
this.label1 = new System.Windows.Forms.Label();
((System.ComponentModel.ISupportInitialize)(this.pictureBox1)).BeginInit();
this.SuspendLayout();
//
// pictureBox1
//
this.pictureBox1.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
this.pictureBox1.Image = ((System.Drawing.Image)(resources.GetObject("pictureBox1.Image")));
this.pictureBox1.Location = new System.Drawing.Point(8, 9);
this.pictureBox1.Name = "pictureBox1";
this.pictureBox1.Size = new System.Drawing.Size(166, 119);
this.pictureBox1.SizeMode = System.Windows.Forms.PictureBoxSizeMode.CenterImage;
this.pictureBox1.TabIndex = 0;
this.pictureBox1.TabStop = false;
//
// label1
//
this.label1.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
this.label1.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
this.label1.Image = ((System.Drawing.Image)(resources.GetObject("label1.Image")));
this.label1.Location = new System.Drawing.Point(180, 9);
this.label1.Name = "label1";
this.label1.Size = new System.Drawing.Size(158, 119);
this.label1.TabIndex = 1;
//
// Form1
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(346, 134);
this.Controls.Add(this.label1);
this.Controls.Add(this.pictureBox1);
this.Name = "Form1";
this.Text = "Form1";
((System.ComponentModel.ISupportInitialize)(this.pictureBox1)).EndInit();
this.ResumeLayout(false);
}
#endregion
private System.Windows.Forms.PictureBox pictureBox1;
private System.Windows.Forms.Label label1;
}
Both gifs play at same speed, but lower than the actual gif. Is there any other points that I should be aware of while applying this code?
Upvotes: 1
Views: 3549
Reputation: 7304
Update 2021-04-02
The underlying reason behind PictureBox
animating at a low framerate is because it uses the ImageAnimator
class behind the scenes which only ever animates at 20 FPS. It has a hard-coded 50ms Thread.Sleep()
in its worker thread method here:
https://referencesource.microsoft.com/#System.Drawing/commonui/System/Drawing/ImageAnimator.cs,333
I didn't have access to the Windows Forms source originally when the below was written, but now I'd probably subclass PictureBox
and make it use a different ImageAnimator
implementation to reduce that Thread.Sleep()
to a reasonable value. (ImageAnimator
is a sealed class so you'd have to copy the code into a new class and reference that in your PictureBox
instead for smoother animations.)
Original Answer:
PictureBox
is quite a heavyweight control and I'd recommend using something like Panel
to house your animated GIF instead. In addition, I've read that PictureBox
's internal animation timer is low resolution, meaning selecting an update interval of <100ms results in it rounding up to a 100ms update.
Instead you can control the painting and animating yourself. This uses PInvoke because it utilises some kernel timer methods. Example code is below:
using System;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
using System.Windows.Forms;
...
public partial class Form1 : Form
{
[DllImport("kernel32.dll")]
static extern bool CreateTimerQueueTimer(out IntPtr phNewTimer,
IntPtr TimerQueue, WaitOrTimerDelegate Callback, IntPtr Parameter,
uint DueTime, uint Period, uint Flags);
[DllImport("kernel32.dll")]
static extern bool ChangeTimerQueueTimer(IntPtr TimerQueue, IntPtr Timer,
uint DueTime, uint Period);
[DllImport("kernel32.dll")]
static extern bool DeleteTimerQueueTimer(IntPtr TimerQueue,
IntPtr Timer, IntPtr CompletionEvent);
public delegate void WaitOrTimerDelegate(IntPtr lpParameter,
bool TimerOrWaitFired);
// Holds a reference to the function to be called when the timer
// fires
public static WaitOrTimerDelegate UpdateFn;
public enum ExecuteFlags
{
/// <summary>
/// The callback function is queued to an I/O worker thread. This flag should be used if the function should be executed in a thread that waits in an alertable state.
/// The callback function is queued as an APC. Be sure to address reentrancy issues if the function performs an alertable wait operation.
/// </summary>
WT_EXECUTEINIOTHREAD = 0x00000001,
};
private Image gif;
private int frameCount = -1;
private UInt32[] frameIntervals;
private int currentFrame = 0;
private static object locker = new object();
private IntPtr timerPtr;
public Form1()
{
InitializeComponent();
// Attempt to reduce flicker - all control painting must be
// done in overridden paint methods
this.SetStyle(ControlStyles.AllPaintingInWmPaint |
ControlStyles.OptimizedDoubleBuffer, true);
// Set the timer callback
UpdateFn = new WaitOrTimerDelegate(UpdateFrame);
}
private void Form1_Load(object sender, EventArgs e)
{
// Replace this with whatever image you're animating
gif = (Image)Properties.Resources.SomeAnimatedGif;
// How many frames of animation are there in total?
frameCount = gif.GetFrameCount(FrameDimension.Time);
// Retrieve the frame time property
PropertyItem propItem = gif.GetPropertyItem(20736);
int propIndex = 0;
frameIntervals = new UInt32[frameCount];
// Each frame can have a different timing - retrieve each of them
for (int i = 0; i < frameCount; i++)
{
// NB: intervals are given in hundredths of a second, so need
// multiplying to match the timer's millisecond interval
frameIntervals[i] = BitConverter.ToUInt32(propItem.Value,
propIndex) * 10;
// Point to the next interval stored in this property
propIndex += 4;
}
// Show the first frame of the animation
ShowFrame();
// Start the animation. We use a TimerQueueTimer which has better
// resolution than Windows Forms' default one. It should be used
// instead of the multimedia timer, which has been deprecated
CreateTimerQueueTimer(out this.timerPtr, IntPtr.Zero, UpdateFn,
IntPtr.Zero, frameIntervals[0], 100000,
(uint)ExecuteFlags.WT_EXECUTEINIOTHREAD);
}
private void UpdateFrame(IntPtr lpParam, bool timerOrWaitFired)
{
// The timer has elapsed
// Update the number of the frame to show next
currentFrame = (currentFrame + 1) % frameCount;
// Paint the frame to the panel
ShowFrame();
// Re-start the timer after updating its interval to that of
// the new frame
ChangeTimerQueueTimer(IntPtr.Zero, this.timerPtr,
frameIntervals[currentFrame], 100000);
}
private void ShowFrame()
{
// We need to use a lock as we cannot update the GIF at the
// same time as it's being drawn
lock (locker)
{
gif.SelectActiveFrame(FrameDimension.Time, currentFrame);
}
this.panel1.Invalidate();
}
private void panel1_Paint(object sender, PaintEventArgs e)
{
base.OnPaint(e);
lock (locker)
{
e.Graphics.DrawImage(gif, panel1.ClientRectangle);
}
}
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
DeleteTimerQueueTimer(IntPtr.Zero, timerPtr, IntPtr.Zero);
}
}
Note: we set the Period
on the timer calls to 100000
because if you set it to 0
(to indicate a one-off timing), it will only fire once, even if you subsequently call ChangeTimerQueueTimer
.
The timers are still not suitable for super-accurate timings, but this should still give you a faster update than would otherwise have been possible with PictureBox
.
Upvotes: 2
Reputation: 941397
You can only get guesses, I doubt anybody will have much luck getting a repro:
powercfg /energy
from an elevated command prompt. Do so while your app is running. It will trundle for a minute and then generate an HTML file that you can look at with your browser. Reported under the "Platform Timer Resolution:Timer Request Stack" heading, the Requested Period value should be 10000. Beware that other processes or drivers might also have made requests.Upvotes: 4