Animated GIF's framerate seems lower than expe

2019-08-13 14:18发布

问题:

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?

回答1:

You can only get guesses, I doubt anybody will have much luck getting a repro:

  • timeBeginPeriod() can technically fail, although that is very unusual when you ask for 10 msec, verify that it returns 0.
  • If the image is large then it might just not be able to update fast enough. Or your UI thread is occupied too much with other duties. The pixel format of a gif is a poor match with the pixel format of the video adapter on a modern machine. The conversion is done every time the frame is updated. It is fairly expensive, especially so if you also force the image to be rescaled (i.e. PictureBox.SizeMode != Normal). Use Task Manager to verify that your UI thread is not burning 100% core.
  • You can get a second opinion about the effective timer period by running 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.


回答2:

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.