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?
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.
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.
Related
I want to make an arrow in c#, which goes from A position to B position in 5 seconds for example. I want to put a map image in the form and when i click on a button i want to draw an arrow from A position to B position in an interval of seconds. i have made an arrow when it is in a horizontal position, but when i try to make it oblique it draws me a triangle instead of an arrow and i don't know how to fix it.
here i made an arrow from a position 12 with a width of 300
and i try to make the same with an oblique arrow but when i put different positions it draws me a triangle not an arrow.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Drawing.Drawing2D;
namespace WindowsFormsApp4
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
this.AutoScaleBaseSize = new System.Drawing.Size(5, 13);
this.ClientSize = new System.Drawing.Size(400, 273);
this.Text = "";
this.Resize += new System.EventHandler(this.Form1_Resize);
this.Paint += new System.Windows.Forms.PaintEventHandler(this.Form1_Paint);
}
private void Form1_Paint(object sender, System.Windows.Forms.PaintEventArgs e)
{
Graphics g = e.Graphics;
g.SmoothingMode = SmoothingMode.AntiAlias;
g.FillRectangle(Brushes.White, this.ClientRectangle);
Pen p = new Pen(Color.Black, 5);
p.StartCap = LineCap.Round;
for(int i=1; i<=300;i++)
{
System.Threading.Thread.Sleep(2);
g.DrawLine(p, 12, 30, i, 30);
Cursor.Current = Cursors.Default;
}
p.EndCap = LineCap.ArrowAnchor;
g.DrawLine(p, 12, 30, 310, 30);
p.Dispose();
}
private void Form1_Resize(object sender, System.EventArgs e)
{
Invalidate();
}
}
}
The fundamental problem with your code is that you are doing the entire animation loop inside the Paint event handler. This means that the window is never clear out between each line you draw, so you get all of the copies of the line you're drawing, start to finish, laid on top of each other in the same view.
It is not clear from your question exactly what you expect to see on the screen. However, another potential problem with your code is that the moving end point of the line does not start at the start point of the line, but rather at a point with the same Y coordinate where you want the line to end. This means that the arrow end of the line traverses a horizontal line leading to the final end point, rather than gradually extending from the start point of the line.
There is also the minor point that you seem to be confused about what the DrawLine() method does. You state that the width of your line is 300, but in fact the second argument of the DrawLine() method is just another point. The width of the line is defined by the Pen you use to draw the line. The width of the box containing the line is defined by the start and end point, but in this case is not 300, but rather (at the final length of the line) the difference between your start X coordinate and end X coordinate (i.e. 288).
The fundamental problem described above can be addressed by running a loop outside of the Paint event handler, which updates values that describe the line, and then call Invalidate() so that the Paint event handler can be called to draw just the current state of the animation.
On the assumption that what you really wanted was for a line to extend out from the start point, rather than traverse a horizontal line, the example I show below implements the animation that way as well, in addition to fixing the fundamental issue. I did nothing to change the length or width of the line.
public Form1()
{
InitializeComponent();
this.DoubleBuffered = true;
this.AutoScaleBaseSize = new System.Drawing.Size(5, 13);
this.ClientSize = new System.Drawing.Size(400, 273);
this.Resize += new System.EventHandler(this.Form1_Resize);
this.Paint += new System.Windows.Forms.PaintEventHandler(this.Form1_Paint);
var task = AnimateLine();
}
private readonly Point _lineStart = new Point(12, 30);
private readonly Point _lineFinalEnd = new Point(300, 60);
private const int _animateSteps = 300;
private Point _lineCurrentEnd;
private bool _drawArrow;
private async Task AnimateLine()
{
Size size = new Size(_lineFinalEnd) - new Size(_lineStart);
for (int i = 1; i <= _animateSteps; i++)
{
await Task.Delay(2);
Size currentSize = new Size(
size.Width * i / _animateSteps, size.Height * i / _animateSteps);
_lineCurrentEnd = _lineStart + currentSize;
Invalidate();
}
_drawArrow = true;
Invalidate();
}
private void Form1_Paint(object sender, System.Windows.Forms.PaintEventArgs e)
{
Graphics g = e.Graphics;
g.SmoothingMode = SmoothingMode.AntiAlias;
using (Pen p = new Pen(Color.Black, 5))
{
p.StartCap = LineCap.Round;
p.EndCap = _drawArrow ? LineCap.ArrowAnchor : p.EndCap;
g.DrawLine(p, _lineStart, _lineCurrentEnd);
}
}
Note that the repeated erasing and redrawing of the window would make the window flicker. This is a basic issue with any sort of animation, and the fix is to enable double-buffering for the window, hence the this.DoubleBuffered = true; statement added to the constructor.
Some other points worth mentioning:
The await Task.Delay() call is used so that the loop can yield the UI thread with each iteration of the loop, which allows the UI thread to raise the Paint event, as well as allows any other UI activity to still work during the animation. You can find lots more information about that C# feature in the How and When to use async and await article, and of course by reading the documentation.
Whether you use Thread.Sleep() or Task.Delay(), specifying a delay of 2 ms isn't very useful. The Windows thread scheduler does not schedule threads to that degree of precision. A thread that sleeps for 2 ms could be woken up as much as 50 ms later in the normal case, and even later if the CPU is under heavy load. Nor does a 2 ms delay provide a useful animation frame rate; that would be a 500 Hz refresh rate, which is easily 10x or more faster than the human brain needs in order to perceive a smooth animation.My example above does nothing to try to address this issue, but you should explore implementing the loop slightly differently, such that instead of the number of animation steps, you specify a reasonable animation interval (say, every 50 or 100 ms), make an attempt to delay that interval, but then use a Stopwatch to actually measure what the real delay was and compute the progress within the animation based on the actual time elapsed. This will allow you to have precise control over the total duration of the animation, as well as somewhat precise control over the refresh rate used for the animation.
In my code I retrieve frames from a camera with a pointer to an unmanaged object, make some calculations on it and then I make it visualized on a picturebox control.
Before I go further in this application with all the details, I want to be sure that the base code for this process is good.
In particular I would like to:
- keep execution time minimal and avoid unnecessary operations, such as
copying more images than necessary. I want to keep only essential
operations
- understand if a delay in the calculation process on every frame could have detrimental effects on the way images are shown (i.e. if it is not printed what I expect) or some image is skipped
- prevent more serious errors, such as ones due to memory or thread management, or to image display.
For this purpose, I set up a few experimental lines of code (below), but I’m not able to explain the results of what I found. If you have the executables of OpenCv you can make a try by yourself.
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.Windows.Forms;
using System.Runtime.InteropServices;
using System.Threading;
public partial class FormX : Form
{
private delegate void setImageCallback();
Bitmap _bmp;
Bitmap _bmp_draw;
bool _exit;
double _x;
IntPtr _ImgBuffer;
bool buffercopy;
bool copyBitmap;
bool refresh;
public FormX()
{
InitializeComponent();
_x = 10.1;
// set experimemental parameters
buffercopy = false;
copyBitmap = false;
refresh = true;
}
private void buttonStart_Click(object sender, EventArgs e)
{
Thread camThread = new Thread(new ThreadStart(Cycle));
camThread.Start();
}
private void buttonStop_Click(object sender, EventArgs e)
{
_exit = true;
}
private void Cycle()
{
_ImgBuffer = IntPtr.Zero;
_exit = false;
IntPtr vcap = cvCreateCameraCapture(0);
while (!_exit)
{
IntPtr frame = cvQueryFrame(vcap);
if (buffercopy)
{
UnmanageCopy(frame);
_bmp = SharedBitmap(_ImgBuffer);
}
else
{ _bmp = SharedBitmap(frame); }
// make calculations
int N = 1000000; /*1000000*/
for (int i = 0; i < N; i++)
_x = Math.Sin(0.999999 * _x);
ShowFrame();
}
cvReleaseImage(ref _ImgBuffer);
cvReleaseCapture(ref vcap);
}
private void ShowFrame()
{
if (pbCam.InvokeRequired)
{
this.Invoke(new setImageCallback(ShowFrame));
}
else
{
Pen RectangleDtPen = new Pen(Color.Azure, 3);
if (copyBitmap)
{
if (_bmp_draw != null) _bmp_draw.Dispose();
//_bmp_draw = new Bitmap(_bmp); // deep copy
_bmp_draw = _bmp.Clone(new Rectangle(0, 0, _bmp.Width, _bmp.Height), _bmp.PixelFormat);
}
else
{
_bmp_draw = _bmp; // add reference to the same object
}
Graphics g = Graphics.FromImage(_bmp_draw);
String drawString = _x.ToString();
Font drawFont = new Font("Arial", 56);
SolidBrush drawBrush = new SolidBrush(Color.Red);
PointF drawPoint = new PointF(10.0F, 10.0F);
g.DrawString(drawString, drawFont, drawBrush, drawPoint);
drawPoint = new PointF(10.0F, 300.0F);
g.DrawString(drawString, drawFont, drawBrush, drawPoint);
g.DrawRectangle(RectangleDtPen, 12, 12, 200, 400);
g.Dispose();
pbCam.Image = _bmp_draw;
if (refresh) pbCam.Refresh();
}
}
public void UnmanageCopy(IntPtr f)
{
if (_ImgBuffer == IntPtr.Zero)
_ImgBuffer = cvCloneImage(f);
else
cvCopy(f, _ImgBuffer, IntPtr.Zero);
}
// only works with 3 channel images from camera! (to keep code minimal)
public Bitmap SharedBitmap(IntPtr ipl)
{
// gets unmanaged data from pointer to IplImage:
IntPtr scan0;
int step;
Size size;
OpenCvCall.cvGetRawData(ipl, out scan0, out step, out size);
return new Bitmap(size.Width, size.Height, step, PixelFormat.Format24bppRgb, scan0);
}
// based on older version of OpenCv. Change dll name if different
[DllImport( "opencv_highgui246", CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr cvCreateCameraCapture(int index);
[DllImport("opencv_highgui246", CallingConvention = CallingConvention.Cdecl)]
public static extern void cvReleaseCapture(ref IntPtr capture);
[DllImport("opencv_highgui246", CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr cvQueryFrame(IntPtr capture);
[DllImport("opencv_core246", CallingConvention = CallingConvention.Cdecl)]
public static extern void cvGetRawData(IntPtr arr, out IntPtr data, out int step, out Size roiSize);
[DllImport("opencv_core246", CallingConvention = CallingConvention.Cdecl)]
public static extern void cvCopy(IntPtr src, IntPtr dst, IntPtr mask);
[DllImport("opencv_core246", CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr cvCloneImage(IntPtr src);
[DllImport("opencv_core246", CallingConvention = CallingConvention.Cdecl)]
public static extern void cvReleaseImage(ref IntPtr image);
}
results [dual core 2 Duo T6600 2.2 GHz]:
A. buffercopy = false; copyBitmap = false; refresh = false;
This is the simpler configuration. Each frame is retrieved in turn, operations are made (in the reality they are based on the same frame, here just calculations), then the result of the calculations is printed on top of the image and finally it is displayed on a picturebox.
OpenCv documentation says:
OpenCV 1.x functions cvRetrieveFrame and cv.RetrieveFrame return image
stored inside the video capturing structure. It is not allowed to
modify or release the image! You can copy the frame using
cvCloneImage() and then do whatever you want with the copy.
But this doesn’t prevent us from doing experiments.
If the calculation are not intense (low number of iterations, N), everything is just ok and the fact that we manipulate the image buffer own by the unmanaged frame retriever doesn’t pose a problem here.
The reason is that probably they advise to leave untouched the buffer, in case people would modify its structure (not its values) or do operations asynchronously without realizing it. Now we retrieve frames and modify their content in turn.
If N is increased (N=1000000 or more), when the number of frames per second is not high, for example with artificial light and low exposure, everything seems ok, but after a while the video is lagged and the graphics impressed on it are blinking. With a higher frame rate the blinking appears from the beginning, even when the video is still fluid.
Is this because the mechanism of displaying images on the control (or refreshing or whatever else) is somehow asynchronous and when the picturebox is fetching its buffer of data it is modified in the meanwhile by the camera, deleting the graphics?
Or is there some other reason?
Why is the image lagged in that way, i.e. I would expect that the delay due to calculations only had the effect of skipping the frames received by the camera when the calculation are not done yet, and de facto only reducing the frame rate; or alternatively that all frames are received and the delay due to calculations brings the system to process images gotten minutes before, because the queue of images to process rises over time.
Instead, the observed behavior seems hybrid between the two: there is a delay of a few seconds, but this seems not increased much as the capturing process goes on.
B. buffercopy = true; copyBitmap = false; refresh = false;
Here I make a deep copy of the buffer into a second buffer, following the advice of the OpenCv documentation.
Nothing changes. The second buffer doesn’t change its address in memory during the run.
C. buffercopy = false; copyBitmap = true; refresh = false;
Now the (deep) copy of the bitmap is made allocating every time a new space in memory.
The blinking effect has gone, but the lagging keep arising after a certain time.
D. buffercopy = false; copyBitmap = false; refresh = true;
As before.
Please help me explain these results!
If I may be so frank, it is a bit tedious to understand all the details of your questions, but let me make a few points to help you analyse your results.
In case A, you say you perform calculations directly on the buffer. The documentation says you shouldn't do this, so if you do, you can expect undefined results. OpenCV assumes you won't touch it, so it might do stuff like suddenly delete that part of memory, let some other app process it, etc. It might look like it works, but you can never know for sure, so don't do it *slaps your wrist* In particular, if your processing takes a long time, the camera might overwrite the buffer while you're in the middle of processing it.
The way you should do it is to copy the buffer before doing anything. This will give you a piece of memory that is yours to do with whatever you wish. You can create a Bitmap that refers to this memory, and manually free the memory when you no longer need it.
If your processing rate (frames processed per second) is less than the number of frames captured per second by the camera, you have to expect some frames will be dropped. If you want to show a live view of the processed images, it will lag and there's no simple way around it. If it is vital that your application processes a fluid video (e.g. this might be necessary if you're tracking an object), then consider storing the video to disk so you don't have to process in real-time. You can also consider multithreading to process several frames at once, but the live view would have a latency.
By the way, is there any particular reason why you're not using EmguCV? It has abstractions for the camera and a system that raises an event whenever the camera has captured a new frame. This way, you don't need to continuously call cvQueryFrame on a background thread.
I think that you still have a problem with your UnmanageCopy method in that you only clone the image the first time this is called and you subsequently copy it. I believe that you need to do a cvCloneImage(f) every time as copy performs only a shallow copy, not a deep copy as you seem to think.
I'm trying to make a simple pong game but I encountered this problem, each timer1 tick (with the interval set to 1ms) should move of 1 pixel the white paddle rectangle if the 'S' or 'W' button is pressed, this means theorically that the white rectangle inside my 400px in height picturebox should be moving from y = 0 to y = 400 in 0.4 - 0.8 seconds, but apparently it takes more than 4 full seconds.
I understand that timer tick events may be "skipped" if the cpu is already busy or by processing speed problems but I tried to make a snake game way more complex than these 50 lines of code and the speed of the drawn snake was actually accurate with low-time intervals
Why does it takes that much?
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
pictureBox1.BackColor = Color.Black;
timer1.Interval = 1;
timer1.Start();
}
private void timer1_Tick(object sender, EventArgs e)
{
PongGame.CheckIfMoving();
PongGame.DrawIt(pictureBox1);
}
private void Form1_KeyDown(object sender, KeyEventArgs e)
{
if (e.KeyData == Keys.S)
{ PongGame.movesDown = true; }
if (e.KeyData == Keys.W)
{ PongGame.movesUp = true; }
}
private void Form1_KeyUp(object sender, KeyEventArgs e)
{
if (e.KeyData == Keys.S)
{ PongGame.movesDown = false; }
if (e.KeyData == Keys.W)
{ PongGame.movesUp = false; }
}
}
public class PongGame
{
public static Rectangle paddle1 = new Rectangle(14, 370, 20, 100);
public Rectangle paddle2;
public static bool movesUp;
public static bool movesDown;
public static Graphics Draw;
public static void DrawIt(PictureBox pb1)
{
Draw = pb1.CreateGraphics();
SolidBrush sb = new SolidBrush(Color.White);
Draw.Clear(Color.Black);
Draw.FillRectangle(sb, paddle1);
}
public static void CheckIfMoving()
{
if (movesUp == true)
{
paddle1.Y -= 1;
}
if (movesDown == true)
{
paddle1.Y += 1;
}
}
}
Thanks in advance for the answers (:
First of all, setting the Timer's interval to 1 produces an unnecessary overhead in this case. That's 1000 fps (well, it would be, if you could actually trust the Timer).
In DrawIt:
Draw = pb1.CreateGraphics();
SolidBrush sb = new SolidBrush(Color.White);
This, since DrawIt is called by the Timer's tick, recreates the Graphics, and the Brush, 1000 times, every second. Be careful about what you put in there.
Furtheremore, you shouldn't use the Picturebox's CreateGraphics method. Instead, override its OnPaint method, and call Refresh on it.
You can read more about this on Bob Powell's Website
The timer makes no guarantee that it will fire at the interval that you specify. In fact it can, and does, fire at an interval longer than that which you specify.
Typically the actual interval of a timer will be related to the underlying system and hardware. It's not uncommon for timers on Windows to fire no more frequently than 20Hz.
If you are going to make a timer based game, then use the timer to give your game a pulse, a regular heartbeat. When the timer fires, use the amount of time since the last timer event to update the state. You'll need to use an accurate measure of time. The Stopwatch class should suffice.
You are also using the picture box incorrectly. This is an event driven control. You are expected to paint the scene in a handler for the Paint event of the picture box.
Good afternoon,
over the last few weeks I have been working on a project to create an advanced metronome. the metronome is made up of the following things
a swinging arm
a light flash
a collection of dynamically created user controls that represent beats (4 of them that are either on, accented or off).
a usercontrol that displays an LCD numeric display and calculates the number of milliseconds between beats for the selected BPM (60000/BPM=milliseconds)
the user selects a BPM and presses start and the following happens
the arm swings between two angles at a rate of n milliseconds per sweep
the light flashes at the end of each arm sweep
the indicators are created and they flash in sequence (one at the end of each sweep).
now the problem
the Arm and light flash animation are created in code and added to a story board with repeat forever and auto reverse.
the indicators are created in code and need to fire an event at the end of each Arm sweep animation.
So, what I did after much messing around was create a timer that runs at the same pace as the storyboard.
the problem, over 30 seconds the timer and the storyboard go out of sync and therefore the indicators and the arm sweep are not in time (not good for a metronome!!).
I was trying to catch the completed event of the animations and use that as a trigger to stop and restart the timer, this was all I could come up with to keep the two in perfect sync.
the moving out of sync is caused by the storyboard slipping and the fact that the storyboard is invoked with begin on the line before the timer is invoked with .start, this although microseconds I think means that they start impossibly close but not at exactly the same time.
my question,
when I try to bind to the completed event of the animation it never fires. I was under the impression that completed even fires regardless of autoreverse (i.e in between each iteration). is this not the case?
can anyone think of another (more cunning) way to keep the two things in sync.
lastly, I did look to see if I could fire a method from a storyboard (which would of made my life really easy, however it would appear that this cannot be done).
if there are any suggestions I am not precious, I just want to get this finished!!
final point of interest,
the bpm can be adjusted whilst the metronome is running, this is achieved by calculating the millisecond duration on the fly (mouse down of a button) and scale the storyboard by the difference between the current speed and the new speed. obviously the timer running the indicators has to be changed at the same time (using interval).
code below is from my project so far (not the XAML just the C#)
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media.Animation;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Controls;
using System.Windows.Threading;
namespace MetronomeLibrary
{
public partial class MetronomeLarge
{
private bool Running;
//Speed and time signature
private int _bpm = 60;
private int _beats = 4;
private int _beatUnit = 4;
private int _currentBeat = 1;
private readonly int _baseSpeed = 60000 / 60;
private readonly DispatcherTimer BeatTimer = new DispatcherTimer();
private Storyboard storyboard = new Storyboard();
public MetronomeLarge()
{
InitializeComponent();
NumericDisplay.Value = BPM;
BeatTimer.Tick += new EventHandler(TimerTick);
SetUpAnimation();
SetUpIndicators();
}
public int Beats
{
get
{
return _beats;
}
set
{
_beats = value;
SetUpIndicators();
}
}
public int BPM
{
get
{
return _bpm;
}
set
{
_bpm = value;
//Scale the story board here
SetSpeedRatio();
}
}
public int BeatUnit
{
get
{
return _beatUnit;
}
set
{
_beatUnit = value;
}
}
private void SetSpeedRatio()
{
//divide the new speed (bpm by the old speed to get the new ratio)
float newMilliseconds = (60000 / BPM);
float newRatio = _baseSpeed / newMilliseconds;
storyboard.SetSpeedRatio(newRatio);
//Set the beat timer according to the beattype (standard is quarter beats for one sweep of the metronome
BeatTimer.Interval = TimeSpan.FromMilliseconds(newMilliseconds);
}
private void TimerTick(object sender, EventArgs e)
{
MetronomeBeat(_currentBeat);
_currentBeat++;
if (_currentBeat > Beats)
{
_currentBeat = 1;
}
}
private void MetronomeBeat(int Beat)
{
//turnoff all indicators
TurnOffAllIndicators();
//Find a control by name
MetronomeLargeIndicator theIndicator = (MetronomeLargeIndicator)gridContainer.Children[Beat-1];
//illuminate the control
theIndicator.TurnOn();
theIndicator.PlaySound();
}
private void TurnOffAllIndicators()
{
for (int i = 0; i <= gridContainer.Children.Count-1; i++)
{
MetronomeLargeIndicator theIndicator = (MetronomeLargeIndicator)gridContainer.Children[i];
theIndicator.TurnOff();
}
}
private void SetUpIndicators()
{
gridContainer.Children.Clear();
gridContainer.ColumnDefinitions.Clear();
for (int i = 1; i <= _beats; i++)
{
MetronomeLargeIndicator theNewIndicator = new MetronomeLargeIndicator();
ColumnDefinition newCol = new ColumnDefinition() { Width = GridLength.Auto };
gridContainer.ColumnDefinitions.Add(newCol);
gridContainer.Children.Add(theNewIndicator);
theNewIndicator.Name = "Indicator" + i.ToString();
Grid.SetColumn(theNewIndicator, i - 1);
}
}
private void DisplayOverlay_MouseDown(object sender, MouseButtonEventArgs e)
{
ToggleAnimation();
}
private void ToggleAnimation()
{
if (Running)
{
//stop the animation
((Storyboard)Resources["Storyboard"]).Stop() ;
BeatTimer.Stop();
}
else
{
//start the animation
BeatTimer.Start();
((Storyboard)Resources["Storyboard"]).Begin();
SetSpeedRatio();
}
Running = !Running;
}
private void ButtonIncrement_Click(object sender, RoutedEventArgs e)
{
NumericDisplay.Value++;
BPM = NumericDisplay.Value;
}
private void ButtonDecrement_Click(object sender, RoutedEventArgs e)
{
NumericDisplay.Value--;
BPM = NumericDisplay.Value;
}
private void ButtonIncrement_MouseEnter(object sender, MouseEventArgs e)
{
ImageBrush theBrush = new ImageBrush()
{
ImageSource = new BitmapImage(new
Uri(#"pack://application:,,,/MetronomeLibrary;component/Images/pad-metronome-increment-button-over.png"))
};
ButtonIncrement.Background = theBrush;
}
private void ButtonIncrement_MouseLeave(object sender, MouseEventArgs e)
{
ImageBrush theBrush = new ImageBrush()
{
ImageSource = new BitmapImage(new
Uri(#"pack://application:,,,/MetronomeLibrary;component/Images/pad-metronome-increment-button.png"))
};
ButtonIncrement.Background = theBrush;
}
private void ButtonDecrement_MouseEnter(object sender, MouseEventArgs e)
{
ImageBrush theBrush = new ImageBrush()
{
ImageSource = new BitmapImage(new
Uri(#"pack://application:,,,/MetronomeLibrary;component/Images/pad-metronome-decrement-button-over.png"))
};
ButtonDecrement.Background = theBrush;
}
private void ButtonDecrement_MouseLeave(object sender, MouseEventArgs e)
{
ImageBrush theBrush = new ImageBrush()
{
ImageSource = new BitmapImage(new
Uri(#"pack://application:,,,/MetronomeLibrary;component/Images/pad-metronome-decrement-button.png"))
};
ButtonDecrement.Background = theBrush;
}
private void SweepComplete(object sender, EventArgs e)
{
BeatTimer.Stop();
BeatTimer.Start();
}
private void SetUpAnimation()
{
NameScope.SetNameScope(this, new NameScope());
RegisterName(Arm.Name, Arm);
DoubleAnimation animationRotation = new DoubleAnimation()
{
From = -17,
To = 17,
Duration = new Duration(TimeSpan.FromMilliseconds(NumericDisplay.Milliseconds)),
RepeatBehavior = RepeatBehavior.Forever,
AccelerationRatio = 0.3,
DecelerationRatio = 0.3,
AutoReverse = true,
};
Timeline.SetDesiredFrameRate(animationRotation, 90);
MetronomeFlash.Opacity = 0;
DoubleAnimation opacityAnimation = new DoubleAnimation()
{
From = 1.0,
To = 0.0,
AccelerationRatio = 1,
BeginTime = TimeSpan.FromMilliseconds(NumericDisplay.Milliseconds - 0.5),
Duration = new Duration(TimeSpan.FromMilliseconds(100)),
};
Timeline.SetDesiredFrameRate(opacityAnimation, 10);
storyboard.Duration = new Duration(TimeSpan.FromMilliseconds(NumericDisplay.Milliseconds * 2));
storyboard.RepeatBehavior = RepeatBehavior.Forever;
Storyboard.SetTarget(animationRotation, Arm);
Storyboard.SetTargetProperty(animationRotation, new PropertyPath("(UIElement.RenderTransform).(RotateTransform.Angle)"));
Storyboard.SetTarget(opacityAnimation, MetronomeFlash);
Storyboard.SetTargetProperty(opacityAnimation, new PropertyPath("Opacity"));
storyboard.Children.Add(animationRotation);
storyboard.Children.Add(opacityAnimation);
Resources.Add("Storyboard", storyboard);
}
}
}
This might not be easily implemented with WPF animations. Instead, a good method would be a game loop. A little research should turn up lots of resources about this. The first one that jumped out at me was http://www.nuclex.org/articles/3-basics/5-how-a-game-loop-works.
In your game loop, you would follow one or the other of these basic procedures:
Calculate how much time has elapsed since the last frame.
Move your displays appropriately.
or
Calculate the current time.
Position your displays appropriately.
The advantage of a game loop is that although the timing may drift slightly (depending on what sort of timing you use), all displays will drift by the same amount.
You can prevent clock drift by calculating time by the system clock, which for practical purposes does not drift. Timers do drift, because they do not run by the system clock.
Time sync is a vaster field than you'd think.
I suggest you take a look at Quartz.NET which is renowned for scheduling/timers issues.
Syncing a WPF animation is tricky because Storyboards are not part of the logical tree, therefore you can't bind anything in them.
That's why you can't define dynamic/variable Storyboards in XAML, you have to do it in C# as you did.
I suggest you make 2 Storyboards: one for the tick to the left, the other one to the right.
In between each animation, fire a method to do your calculations/update another part of the UI, but do it in a separate Task so that the timings aren't messed up (a few µs for the calculations make up for quite some time after 30s already!)
Keep in mind that you will need to use Application.Current.Dispatcher from your Task to update the UI.
And lastly, at least set the Task flag TaskCreationOptions.PreferFairness so that Tasks run in the order they were started.
Now since that just gives a hint to the TaskScheduler and doesn't guarantees them to run in order, you may want to use a queueing system instead for full guarantee.
HTH,
Bab.
You could try 2 animations , one for the right swing and one for the left. In the animation complete on each, start the other animation (checking for cancellation flags) and update your indicators (possibly via BeginInvoke on the Dispatcher so you don't interfere with the next animation start.)
I think getting the timer to sync with an animation is difficult - it is a dispatcher based timer which is based on messages - sometimes it can skip a bit of time, ie if you click fast with the mouse a lot I think the animation timer also is dispatcher based, so they will easily get out of sync.
I would suggest to abandon the syncing and let the timer handle it. Can't you let it update a property with notification and let your metronome arm position bind to that?
To get the accelaration/deceleration you just have to use a Sine or Cosine function.
comrades) I've found some interesting behavior of Invalidate method in multithreaded applications. I hope you could help me with a problem...
I experience troubles while trying to invalidate different controls at one time: while they're identical, one succesfully repaints itself, but another - not.
Here is an example: I have a form (MysticForm) with two panels (SlowRenderPanel) on it. Each panel has a timer and with a period of 50ms Invalidate() method is called. In OnPaint method I draw number of current OnPaint call in the centre of panel. But notice that in OnPaint method System.Threading.Thread.Sleep(50) is called to simulate long time draw procedure.
So the problem is that the panel added first repaints itself much more often than another one.
using System;
using System.Drawing;
using System.Windows.Forms;
using System.Runtime.InteropServices;
namespace WindowsFormsApplication1 {
static class Program {
[STAThread]
static void Main() {
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new MysticForm());
}
}
public class MysticForm : Form {
public SlowRenderPanel panel1;
public SlowRenderPanel panel2;
public MysticForm() {
// add 2 panels to the form
Controls.Add(new SlowRenderPanel() { Dock = DockStyle.Left, BackColor = Color.Red, Width = ClientRectangle.Width / 2 });
Controls.Add(new SlowRenderPanel() { Dock = DockStyle.Right, BackColor = Color.Blue, Width = ClientRectangle.Width / 2 });
}
}
public class SlowRenderPanel : Panel {
// synchronized timer
private System.Windows.Forms.Timer timerSafe = null;
// simple timer
private System.Threading.Timer timerUnsafe = null;
// OnPaint call counter
private int counter = 0;
// allows to use one of the above timers
bool useUnsafeTimer = true;
protected override void Dispose(bool disposing) {
// active timer disposal
(useUnsafeTimer ? timerUnsafe as IDisposable : timerSafe as IDisposable).Dispose();
base.Dispose(disposing);
}
public SlowRenderPanel() {
// anti-blink
DoubleBuffered = true;
// large font
Font = new Font(Font.FontFamily, 36);
if (useUnsafeTimer) {
// simple timer. starts in a second. calls Invalidate() with period = 50ms
timerUnsafe = new System.Threading.Timer(state => { Invalidate(); }, null, 1000, 50);
} else {
// safe timer. calls Invalidate() with period = 50ms
timerSafe = new System.Windows.Forms.Timer() { Interval = 50, Enabled = true };
timerSafe.Tick += (sender, e) => { Invalidate(); };
}
}
protected override void OnPaint(PaintEventArgs e) {
string text = counter++.ToString();
// simulate large bitmap drawing
System.Threading.Thread.Sleep(50);
SizeF size = e.Graphics.MeasureString(text, Font);
e.Graphics.DrawString(text, Font, Brushes.Black, new PointF(Width / 2f - size.Width / 2f, Height / 2f - size.Height / 2f));
base.OnPaint(e);
}
}
}
Debug info:
1) Each panel has a bool field useUnsafeTime (set to true by default) which allows using System.Windows.Forms.Timer (false) insted of System.Threading.Timer (true). In the first case (System.Windows.Forms.Timer) everything works fine. Removing System.Threading.Sleep call in OnPaint also makes execution fine.
2) Setting timer interval to 25ms or less prevents second panel repainting at all (while user doesn't resize the form).
3) Using System.Windows.Forms.Timer leads to speed increasement
4) Forcing control to enter synchronization context (Invoke) doesn't make sense. I mean that Invalidate(invalidateChildren = false) is "thread-safe" and could possibly have different behavior in diffenent contexts
5) Nothing interesting found in IL comparison of these two timers... They just use different WinAPI functions to set and remove timers (AddTimerNative, DeleteTimerNative for Threading.Timer; SetTimer, KillTimer for Windows.Forms.Timer), and Windows.Forms.Timer uses NativeWindow's WndProc method for rising Tick event
I use a similar code snippet in my application and unfortunately there is no way of using System.Windows.Forms.Timer) I use long-time multithreaded image rendering of two panels and Invalidate method is called after rendering is completed on each panel...
That would be great if someone could help me to understand what's different happening behind the scenes and how to solve the problem.
P.S. Interesting behavior isn't it?=)
Nice demonstration of what goes wrong when you use members of a control or form on a background thread. Winforms usually catches this but there's a bug in the Invalidate() method code. Change it like this:
timerUnsafe = new System.Threading.Timer(state => { Invalidate(true); }, null, 1000, 50);
to trip the exception.
The other panel is slower because lots of its Invalidate() calls are getting canceled by the paint event. Which is just slow enough to do so. Classic threading race. You cannot call Invalidate() from a worker thread, the synchronous timer is an obvious solution.
Invalidate() invalidates the client area or rectangle ( InvalidateRect() ) and "tells" Windows that next time Windows paints; refresh me, paint me. But it does not cause or invoke a paint message. To force a paint event, you must force windows to paint after an Invalidate call. This is not always needed, but sometimes it's what has to be done.
To force a paint you have to use the Update() function. "Causes the control to redraw the invalidated regions within its client area."
You have to use both in this case.
Edit: A common technique to avoid these kinds of problems is keep all your paint routines and anything related in a single (generally main) thread or timer. The logic can run elsewhere but where the actual paint calls are made should all be in one thread or timer.
This is done in games and 3D simulations.
HTH