【问题标题】:Animated GIF's framerate seems lower than expected动画 GIF 的帧率似乎低于预期
【发布时间】:2016-03-17 09:33:05
【问题描述】:

我有一个winforms 应用程序,上面有一个 gif,可以让用户了解停止进程。

问题在于它的播放速度比在其他应用程序(chrome、Internet Explorer)上看起来要慢得多。

我在PictureBoxLabel 上尝试过gif,但结果速度是一样的。然后经过一点研究,我遇到了this 问题和传奇@Hans Passant 的答案,但不幸的是,应用他建议的样板代码并没有任何区别。

下面是简单的重现代码:

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 );
}

如果需要,还有设计器代码:

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

两个 gif 以相同的速度播放,但低于实际 gif。在应用此代码时,我还有什么需要注意的吗?

【问题讨论】:

  • 你为什么将其设置为 0 精度?
  • 这个 gif 在我看来就像一个应用程序资源。您不能使用一些 gif 编辑器简单地修改原始 gif 吗?只需让它变慢,例如通过删除每一秒帧并延长剩下的每一帧的持续时间。重复直到结果令人满意。
  • 我尝试了不同的精度值,0 - 10 - 1000 :) 但是没有任何区别。现在我也在问题中将其设置回 10。
  • @Sinatr,我已通过properties->image->import 添加了 gif。无论如何,我认为拥有 gif 的真正速度效果并不需要如此痛苦。
  • 也许操作系统确实很重要。你编译它的确切 .net 框架是什么,你在哪个操作系统上运行它?

标签: c# winforms time animated-gif


【解决方案1】:

你只能猜测,我怀疑有人会很幸运地得到复制:

  • timeBeginPeriod() 在技术上可能会失败,尽管当您要求 10 毫秒时这是非常不寻常的,但请验证它是否返回 0。
  • 如果图像很大,那么它可能无法足够快地更新。或者您的 UI 线程忙于其他职责。 gif 的像素格式与现代机器上视频适配器的像素格式不匹配。每次更新帧时都会完成转换。它相当昂贵,尤其是如果您还强制重新缩放图像(即 PictureBox.SizeMode != Normal)。使用任务管理器验证您的 UI 线程没有燃烧 100% 内核。
  • 您可以通过在提升的命令提示符下运行powercfg /energy 来获得有关有效计时器周期的第二个意见。在您的应用程序运行时执行此操作。它会运行一分钟,然后生成一个 HTML 文件,您可以使用浏览器查看该文件。在“Platform Timer Resolution:Timer Request Stack”标题下报告,Requested Period 值应为 10000。请注意其他进程或驱动程序也可能已发出请求。

【讨论】:

    【解决方案2】:

    2021-04-02 更新

    PictureBox 以低帧速率制作动画的根本原因是它在幕后使用了 ImageAnimator 类,而该类仅以 20 FPS 进行动画处理。它在这里的工作线程方法中有一个硬编码的 50ms Thread.Sleep()

    https://referencesource.microsoft.com/#System.Drawing/commonui/System/Drawing/ImageAnimator.cs,333

    在编写以下内容时,我最初无法访问 Windows 窗体源,但现在我可能会继承 PictureBox 并使其使用不同的 ImageAnimator 实现以将 Thread.Sleep() 减少到合理的价值。 (ImageAnimator 是一个密封类,因此您必须将代码复制到一个新类中并在您的 PictureBox 中引用它以获得更流畅的动画。)

    原答案:

    PictureBox 是一个相当重量级的控件,我建议使用类似Panel 的东西来代替您的动画 GIF。此外,我了解到PictureBox 的内部动画计时器分辨率较低,这意味着选择

    相反,您可以自己控制绘画和动画。这使用了 PInvoke,因为它利用了一些内核计时器方法。示例代码如下:

    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);
        }
    }
    

    注意:我们将计时器调用上的Period 设置为100000,因为如果您将其设置为0(表示一次性计时),即使您随后调用@,它也只会触发一次987654337@.

    计时器仍然不适合超精确的计时,但这仍然应该比PictureBox 提供更快的更新。

    【讨论】:

    • 有效,但闪烁很多,这会破坏它。
    • @EpicSpeedy 我的原始答案现在已经超过 5 年了,所以很可能 .NET 和底层操作系统自编写以来都发生了变化;)但是,我看过@刚刚 987654339@ 源代码,我已经更新了上面的答案,提供了一些可能对您有用的更多信息。不幸的是,该来源在 2015 年不可用。
    猜你喜欢
    • 2014-10-12
    • 2021-02-04
    • 1970-01-01
    • 1970-01-01
    • 2015-01-04
    • 1970-01-01
    • 1970-01-01
    • 2010-10-09
    • 1970-01-01
    相关资源
    最近更新 更多