C# 动画GIF的帧率似乎低于预期
我有一个winforms应用程序,上面有一个gif,让用户知道进程暂停的情况 问题是它的运行速度比其他应用程序慢得多,比如chrome、InternetExplorer 我在PictureBox和Label上尝试过gif,但结果速度是一样的。经过一点研究,我得到了传奇人物@Hans Passant的问题和答案,但不幸的是,应用他建议的样板代码并没有任何区别 下面是简单的复制代码:C# 动画GIF的帧率似乎低于预期,c#,winforms,time,animated-gif,C#,Winforms,Time,Animated Gif,我有一个winforms应用程序,上面有一个gif,让用户知道进程暂停的情况 问题是它的运行速度比其他应用程序慢得多,比如chrome、InternetExplorer 我在PictureBox和Label上尝试过gif,但结果速度是一样的。经过一点研究,我得到了传奇人物@Hans Passant的问题和答案,但不幸的是,应用他建议的样板代码并没有任何区别 下面是简单的复制代码: public partial class Form1 : Form { public Form1 ()
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。在应用此代码时,我还需要注意其他几点吗?您只能猜测,我怀疑任何人都不会有机会获得复制: timeBeginPeriod在技术上可能会失败,尽管当您请求10毫秒时这是非常不寻常的,请验证它是否返回0。 如果图像很大,那么它可能无法足够快地更新。或者您的UI线程被其他任务占用太多。gif的像素格式与现代机器上视频适配器的像素格式不匹配。每次更新帧时都会进行转换。这相当昂贵,尤其是如果您还强制重新缩放图像,即PictureBox.SizeMode!=典型的使用任务管理器验证您的UI线程没有100%烧掉内核。 通过从提升的命令提示符运行powercfg/energy,您可以获得关于有效计时器周期的第二个意见。在应用程序运行时执行此操作。它将滚动一分钟,然后生成一个HTML文件,您可以使用浏览器查看该文件。在平台计时器分辨率:计时器请求堆栈标题下报告,请求的周期值应为10000。请注意,其他进程或驱动程序可能也发出了请求。
你只能猜测,我怀疑任何人都不会有多大的运气得到重考: timeBeginPeriod在技术上可能会失败,尽管当您请求10毫秒时这是非常不寻常的,请验证它是否返回0。 如果图像很大,那么它可能无法足够快地更新。或者您的UI线程被其他任务占用太多。gif的像素格式与现代机器上视频适配器的像素格式不匹配。每次更新帧时都会进行转换。这相当昂贵,尤其是如果您还强制重新缩放图像,即PictureBox.SizeMode!=典型的使用任务管理器验证您的UI线程没有100%烧掉内核。 通过从提升的命令提示符运行powercfg/energy,您可以获得关于有效计时器周期的第二个意见。在应用程序运行时执行此操作。它将滚动一分钟,然后生成一个HTML文件,您可以使用浏览器查看该文件。在平台计时器分辨率:计时器请求堆栈标题下报告,请求的周期值应为10000。请注意,其他进程或驱动程序可能也发出了请求。 更新2021-04-02 PictureBox以低帧速设置动画的根本原因是,它在场景后面使用ImageAnimator类,该类仅以20 FPS的速度设置动画。它有一个硬编码的50ms线程。请在其工作线程方法中睡眠: 在编写下面的代码时,我最初没有访问Windows窗体源代码的权限,但现在我可能会将PictureBox子类化,并使其使用不同的ImageAnimator实现来减少线程。Sleep到一个合理的值。ImageAnimator是一个密封类,因此您必须将代码复制到一个新类中,并在PictureBox中引用该类,以获得更平滑的动画 原始答案: PictureBox是一个相当重的控件,我建议使用类似Panel的东西来放置动画GIF。此外,我还了解到PictureBox的内部动画计时器分辨率较低,这意味着选择更新间隔update 2021-04-02 PictureBox以低帧速设置动画的根本原因是,它在场景后面使用ImageAnimator类,该类仅以20 FPS的速度设置动画。它有一个硬编码的50ms线程。请在其工作线程方法中睡眠: 在编写下面的代码时,我最初没有访问Windows窗体源代码的权限,但现在我可能会将PictureBox子类化,并使其使用不同的ImageAnimator实现来减少线程。Sleep到一个合理的值。ImageAnimator是一个密封类,因此您必须将代码复制到一个新类中,并在PictureBox中引用该类,以获得更平滑的动画 原始答案:
PictureBox是一个相当重的控件,我建议使用类似Panel的东西来放置动画GIF。此外,我还了解到PictureBox的内部动画计时器的分辨率很低,这意味着选择一个更新间隔,为什么要将其设置为0精度
给我一个应用程序资源。你不能简单地用一些gif编辑器修改原始gif吗?只需使其变慢,例如,每秒钟移除一帧并延长剩余帧的持续时间。重复,直到结果令人满意。我尝试了不同的精度值,0-10-1000:没有任何区别。现在我在问题中也把它设回了10。@Sinatr,我已经通过属性->图像->导入添加了gif。我不认为它需要这样一个痛苦有一个真正的速度效果的gif无论如何。也许操作系统的问题。您编译此文件的具体.net框架是什么?您在哪个操作系统上运行它?为什么要将其设置为0精度?在我看来,此gif就像一个应用程序资源。你不能简单地用一些gif编辑器修改原始gif吗?只需使其变慢,例如,每秒钟移除一帧并延长剩余帧的持续时间。重复,直到结果令人满意。我尝试了不同的精度值,0-10-1000:没有任何区别。现在我在问题中也把它设回了10。@Sinatr,我已经通过属性->图像->导入添加了gif。我不认为它需要这样一个痛苦有一个真正的速度效果的gif无论如何。也许操作系统的问题。你编译这篇文章的确切的.net框架是什么?你在哪个操作系统上运行它?工作正常,但它会闪烁很多,这会破坏它。@我的原始答案现在已经有5年多的历史了,因此.net和底层操作系统很可能自编写以来都发生了变化;然而,我刚刚看了PictureBox的源代码,并用一些可能对您有用的更多信息更新了上面的答案。不幸的是,该源代码在2015年就不可用了。虽然有效,但它闪烁了很多次,破坏了它。@我的原始答案现在已经有5年多的历史了,所以很可能.NET和底层操作系统自编写以来都发生了变化;然而,我刚刚看了PictureBox的源代码,并用一些可能对您有用的更多信息更新了上面的答案。不幸的是,该来源在2015年还不可用。
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);
}
}