1

I am currently stuck at a really strange problem with GDI and timers.

First the Code:

class Graph : UserControl {
  private System.Threading.Timer timer;
  private int refreshRate = 25;              //Hz (redrawings per second)
  private float offsetX = 0;                 //X offset for moving graph

  public Graph() {
    timer = new System.Threading.Timer(timerTick);
  }

  private void timerTick(object data) {
    offsetX -= 1 / refreshRate;
    this.Invalidate();
  }

  public void timerStart() {
    timer.Change(0, 1000 / refreshRate);
  }

  private void onPaint(object sender, PaintEventArgs e) {
    //350 lines of code for drawing the graph
    //Here the offsetX will be used to move the graph
  }
}

I am trying here to move a painted graph in a specific time to 1 "graph unit" to the left. So i use a timer which will change the offset in little steps, so it will be a smooth moving (thats the refreshRate for).

At the first view this code worked, but later i found following problem: If i am using a refreshRate of 1 (1Hz) it will just fine move my graph in 1 step 1 (graph unit) to the left. If i am increasing the refreshRate my movement will slow done. At 20 FPS its slighly slow, at 200 FPS its really slow..

So here is what i tried:

  1. I used Refresh or Update instead of Invalidate

  2. I used a normal Thread (with Sleep) instead of the timer

Both code changes didnt changed the result..

Beside the movement with the timer I also can move the graph with my mouse and if the timer is running i can still smoothly move the graph with my mouse. So its not a peformance problem..

I thought of a problem in the painting queue, because I am refreshing faster than the painting is done? (But why can I sill move the graph smoothly with my mouse?!)

So i need a little help here. Thanks

Marcel
  • 312
  • 4
  • 8
  • 6
    `offsetX -= 1 / refreshRate;` Is that the actual code? Because this is doing an integer division and can only result in 1 or 0... – Thomas Sep 21 '11 at 20:31
  • no, thats the actual code: `graphOffset.X -= step / (float)refreshRate;` where graphOffset is PointF, step is float and refreshRate is int.. i also thought about this problem (or a rounding problem somewhere in my code) – Marcel Sep 21 '11 at 20:41
  • Threading timers and UIs don't normally mix. Is Invalidate safe to call from a non UI thread? – tcarvin Sep 21 '11 at 20:43
  • @tcarvin: Threading timers and the UI work just fine if you use `this.InvokeRequired` and `this.Invoke`. That's what `System.Timers.Timer` does. Of course, `System.Timers.Timer` also squashes exceptions, which in my opinion makes it unsuitable for serious work. – Jim Mischel Sep 22 '11 at 00:25
  • @JimMischel - But the code above doesn't use this.InvokeRequired...I was more asking if Control.Invalidate is safe to call from a non-UI thread without the Invoke. – tcarvin Sep 22 '11 at 12:05

2 Answers2

1

At 20 FPS its slighly slow, at 200 FPS its really slow..

There is a fundamental problem here; to get a refresh rate of 200fps you would need to repaint every 5ms. This will never happen. No matter what you set your timer's interval to it's resolution is limited to about 10-15ms. So your best possible frame rate is about 66-100fps, and that's assuming that your drawing code takes zero time, which of course it does not.

On top of that, you are using a System.Threading.Timer which does not perform it's callbacks on the UI thread, so calling Invalidate() from there is probably not even safe. You don't typically use a System.Threading.Timer for UI code.

You may want to try something like the so called Multi-Media Timer shown here, which uses the CPU's high performance timer and gives a resolution of about 1ms.

Community
  • 1
  • 1
Ed S.
  • 122,712
  • 22
  • 185
  • 265
  • +1 for noting the timer resolution. Whereas it's true that `System.Threading.Timer` executes on a pool thread, it's easy enough to marshal the `Invalidate` call to the UI thread. Just use `Form.Invoke`. He'll have to do the same thing if he uses the multimedia timer. MM timers have been "deprecated" in favor of the native thread pool timers, which will give a resolution of 1 ms. I implemented a wrapper for them at http://www.informit.com/guides/content.aspx?g=dotnet&seqNum=817. – Jim Mischel Sep 22 '11 at 00:18
0

You may try to change the problem slightly.

Since you do not know how long your painting will take, you cannot make an assumption about that.

What you do know, is the amount of time you want the transition to happen, let's say 30 seconds, something like this:

private MAX_TRANSITION_TIME = 30; //seconds

At this point you can also track the amount of time it has elapsed since the start of your operation, by recording when your operation has started, let's say

private DateTime _startMoment;

Now what you can do is to make sure you have the right routine at hand and you calculate your position based on the difference between the startMoment and now

var elapsedTime = DateTime.Now.Subtract(_startMoment).Milliseconds;
var elapsedPercent = elapsedTime / MAX_TRANSITION_TIME * 1000.0 ;

From now on what you can do is to paint accordingly to your percent of time elapsed.

So after your OnPaint is done you should be able to refresh it. If you use one UI timer, you can do this:

 private void onPaint(object sender, PaintEventArgs e) {
       Timer1.Enabled = false;

     //350 lines of (adjusted)code go here

     If (ElapsedPercent<1)
     {
         Timer1.Enabled=True;
     }
     else
     {
         // you may need to perform the last draw setting the ElapsedPercent 
         // to 1. This is because it is very unlikely that your 
         // last call will happen at the very last millisecond
     }
 }

Where Timer1 has to be the Timer control.

In the Interval event you just write

 _startMoment = DateTime.Now();
 this.Invalidate();

Hope this helps

mhttk
  • 1,688
  • 1
  • 16
  • 29
  • As I am reading the answers I am not sure if I understand the Invalidate function correct. Let me explain how my think my code schould work: Lets take my problem from above and use a thread instead of a timer. As I am starting my thread I am starting to decrease my offset every X milliseconds. If i am calling the Invalidate function I am informing the main thread to repaint my graph. So I thought the painting would proceed in the main thread, but actually its repainting where i am using Invalidate? So calculation the execution time will then work. But I will reconsider another solution. – Marcel Sep 21 '11 at 21:27
  • Well, the UI thread will take its time to paint whatever it needs to. After that time can do another paint operation. Your approach would work if and only if the time between calls is higher (or equal) than the time the UI takes to paint. – mhttk Sep 21 '11 at 21:43
  • I am now using 2 different threads, 1 for decreasing my offset and 1 for refreshing the graph. Works fine. – Marcel Sep 21 '11 at 21:55
  • What FPS can you achieve with your strategy? i.e. what is the maximum value before it starts lagging? – mhttk Sep 21 '11 at 21:58
  • @Marcel: See http://msdn.microsoft.com/en-us/library/598t492a.aspx for information about `Invalidate`. In particular, "Calling the Invalidate method does not force a synchronous paint;". `Invalidate` just invalidates the region and Windows will get around to painting it in its own good time. If you want to force a synchronous paint, you have to call `Update` immediately after `Invalidate`. I would not suggest doing that, though. – Jim Mischel Sep 22 '11 at 00:23