Timers and their use in Windows



Timers and their use in Windows

Windows provides timers for you to use. This works roughly as follows:

1. You set a timer, using SetTimer, telling it how often to "tick", in milliseconds. This is a member function of the CWnd class.

2. It goes, "tick, tick, tick" at the specified intervals. Each "tick" is indicated by sending a WM_TIMER message to the window that called SetTimer. (Actually, you can specify another destination for the messages if you wish.)

3. You can respond to that message however you want.

4. When you're done with the time you call KillTimer. That's the end of that timer. It's a memory leak if you don't kill it.

The WM_TIMER message is just placed in the application message queue, so it could take a little time before it gets processed, especially if other applications or threads are competing for CPU time. Therefore, the timing is not accurate enough to support multimedia applications. For example, you can't use it to synchronize text display with people speaking in a video clip. Below 100 milliseconds the timer may not tick completely regularly.

However, you can use it for simple animation, displaying the animated image at its next position once per timer tick. And you can also use it for any other timed task. For example, you could display a "splash screen" for a few seconds and then automatically remove it.

You might have more than one timer. In that case, how would you know which timer sent the WM_TIMER message? Answer: each timer has an ID number, and that number arrives as the wParam with the WM_TIMER message. You specify the ID number in the call to SetTimer, and you pass it to KillTimer.

As an example, we can start with the program from an earlier lecture, which let us drag the Mona Lisa around the screen, and arrange it so the Mona Lisa moves automatically to a new position, randomly chosen, every five seconds, when she is not being dragged. When she is being dragged, the timer is killed and she won't move by herself.

We could set the timer in the view constructor and kill it in the destructor, but instead we add menu items to set and kill the timer. I gave these items ID numbers of IDM_SETTIMER and IDM_KILLTIMER. I put a "Timer" entry on the menu bar and under it the two new items with text "Start" and "Stop". Here are the handlers:

void CDragDemoView::OnSettimer()

{

SetTimer(0,500, NULL); // timer number 0; 500 millisecond ticks;

// NULL means put the message in the application's message queue.

}

void CDragDemoView::OnKilltimer()

{

KillTimer(0); // Kill timer number 0

}

Now go to Class Wizard and map the WM_TIMER message in the view class. When this message is received, we choose a random new location in the view window and move the Mona Lisa there.

void CDragDemoView::OnTimer(UINT nIDEvent)

{

if(m_Carrier.m_dragging)

return; // do nothing while dragging the Mona Lisa

CRect r, rCarrier,target;

GetClientRect(&r); // client rect of the view window

m_Carrier.GetClientRect(&rCarrier);

r.right -= rCarrier.right;

r.bottom -= rCarrier.bottom;

// make x,y a random location in rectangle r. This will

// become the upper left corner of the new Mona Lisa. By

// adjusting the right and bottom we know it will fit on-screen.

target.left = (int)(rand() / (double) RAND_MAX * r.right);

= (int)( rand()/(double) RAND_MAX * r.bottom);

target.right = target.left + rCarrier.right;

target.bottom = + rCarrier.bottom;

m_Carrier.MoveWindow(&target);

}

Since we use rand() we need to call srand(0) in the view class constructor. You should call srand(0) just once per run of a program that uses "random" numbers. It starts the random number generation algorithm by taking some numbers from the system clock's current time.

Note the code for using rand() carefully--it has nothing to do with Windows but you may not have been taught enough fundamentals of C to realize why the code has to look like this. What goes wrong without the cast to (double)? [This problem is the same in Java as in C except for the name of rand().]

To test the program, we should check that you can still drag the Mona Lisa while she's jumping around randomly. You can--if you can catch her in the half second she stands still!

Use of a Timer and Double Buffering for Animation

To give a demonstration of animation, we want to make something move smoothly. The Balls program displays some colored balls bouncing around in a window, like balls rolling on a pool table.

Whenever the user clicks a new ball is created. They are created in varying colors, sizes, and velocities, and when they hit the walls, they bounce off. However, in this simple program, they pass through each other rather than bouncing off each other.

The basic drawing structure of the program is much like the Mandelbrot program used last week to demonstrate double-buffering. You have to use double-buffering to get non-flickering animation of several balls. There will be a memory DC which is updated, for example, 20 times a second. If we imitated the Mandelbrot program, we would put the code in OnIdle like this:

BOOL CBallsApp::OnIdle(LONG lCount)

{ int rval = CWinApp::OnIdle(lCount);

if(rval == 0)

{ /* MFC is done updating command user-interface objects and

deleting temporary graphics objects, so the time is ours! */

CBallsView *p = (CBallsView *) g_pview;

p->m_Update(&p->m_MemDC);

return 1;

}

return 1;

}

The global variable g_pview should be declared outside of any class (in any file that has #include CBallsView at the top); it should be initialized to this in the view class constructor. This is the same as in the Mandelbrot application, whose entire source code is available ( here or from the main course web page.)

The trouble with putting the m_Update call in OnIdle is that we don't know how often it will be called. We can actually produce a pretty decent program by just making a guess about how many milliseconds it will be between calls to m_Update and hard-coding that guess. But then, if we run the program on a machine which is faster or slower, the balls will move correspondingly faster or slower, which in some applications would not be acceptable.

And even on the same machine, we can't make the speeds changeable without using a time.

Therefore we want to set a timer and call m_Update every time it ticks. We create a member variable (in the view class) m_Tick for the number of milliseconds per tick, and initialize it to 50 (which means 20 ticks per second). We set the timer in OnCreate

with the call SetTimer(1) and kill it in OnDestroy with the call KillTimer(1). To create these handlers, map the messages WM_CREATE and WM_DESTROY using Class Wizard.

We map the WM_TIMER message and put the call to m_Update in the OnTimer handler:

void CBallsView::OnTimer(UINT nIDEvent)

{

m_Update(&m_MemDC);

CView::OnTimer(nIDEvent);

}

Balls to Animate

Now we need something to animate. Create a new class ball

class ball

{

public:

ball();

virtual ~ball();

public:

int x; // coordinates of center of the ball

int y;

int color; // index into some array

int r; // radius of the ball

int p; // x coordinate of velocity, pixels per second

int q; // y coordinate of velocity

};

This class contains the data required to update a ball when the timer ticks. We need the velocity information to update the position. The velocity remains the same unless the ball bounces off a wall, in which case one of the velocities changes sign. If the ball hits a corner both velocities change sign. Before we get to m_Update, we need to initialize some balls.

Add an array of 50 balls to the view class. Call it m_Balls. Probably we should add it to the document class since this array is the real data of the application, but the code is simpler if we put it in the view class, and we want to focus on the animation ideas here rather than on the document-view architecture.

A member variable m_nBalls will keep track of the actual number of balls. New balls will be initialized on mouse clicks--more precisely, in OnLButtonDown. The position of the mouse will be the position of the new ball. What should we take for the radius and velocity and color? That is up to our creativity. We will write a function CreateBall and use it like this:

void CBallsView::OnLButtonDown(UINT nFlags, CPoint point)

{

CreateBall(point.x,point.y);

CView::OnLButtonDown(nFlags, point);

// InvalidateRect(NULL); Don't do this

// because we are using double-buffering.

}

Here's a sample CreateBall you could use:

#define NCOLORS 4

#define NRADII 3

static int radii[NRADII] ={ 10, 20, 30} ;

void CBallsView::CreateBall(int x, int y)

/* create a new ball with the specified center.

The speed of the ball will depend on x,

but its direction will be random.

Colors and radii will cycle. Since NCOLORS

and NRADII are relatively prime, they'll cycle

more or less independently.

*/

{ double t;

if(m_nballs == 49 )

return; /* can't create more */

balls[m_nballs].x = x;

balls[m_nballs].y = y;

balls[m_nballs].r = radii[m_counter % NRADII];

balls[m_nballs].color = m_counter % NCOLORS;

balls[m_nballs].p = balls[m_nballs].q = 0;

/* Make t be a random number between 0 and 2 pi */

t = 6.283 * rand()/ (double) (RAND_MAX-1);

/* Give it a speed sufficient to enable it to reach the left edge

in one second, if it moved left, but direct its velocity

randomly. That velocity is x pixels per second. */

balls[m_nballs].p = (int)( x * cos(t));

balls[m_nballs].q = (int)( x * sin(t));

++m_counter;

++m_nballs;

}

The Actual Animation

void CBallsView::m_Update(CDC *pDC)

{ int i;

/* First clear the whole hdc */

CRect r;

int temp;

int width = m_width;

int height = m_height;

r.left = = 0;

r.right = m_width;

r.bottom = m_height;

CBrush BlackBrush;

BlackBrush.CreateStockObject(BLACK_BRUSH);

pDC->FillRect(&r, &BlackBrush);

/* That should turn the window black, overwriting all old images */

for(i=0;iSelectObject(m_brushes[balls[i].color]);

pDC->Ellipse(

balls[i].x-balls[i].r, balls[i].y - balls[i].r,

balls[i].x + balls[i].r, balls[i].y + balls[i].r

);

}

InvalidateRect(NULL,0);

}

As a final touch:

BOOL CBallsView::OnEraseBkgnd(CDC* pDC)

/* This prevents blinking when the window is resized. */

{

return FALSE;

}

Now the program is complete.

Ping Pong

This is offered as a challenge to my hard-working and talented students. If you successfully write this program come and show it to me in office hours. It is not an assignment because it will probably take you longer than a normal homework assignment. You will not get a grade for it but only the pleasure of learning and the direct praise of the professor.

This program features a small ball which bounces around the view window. It bounces off the top, bottom, and left sides of the view rectangle. At the right side, there is a "paddle" which can move vertically (and only vertically). It is shown as a thin rectangle about an inch tall. (You could make the paddle size adjustable and the speed of the ball adjustable by menu items.) This rectangle is thought of as an "edge view" of a ping-pong paddle. The user can drag the paddle up and down the edge of the window with the mouse. If she manages to get it in position properly, the ball will bounce off it and continue in play. If not, then the ball will hit the right side of the window and the volley is over. The user gets a point each time she successfully hits the ball. You could display the current score somewhere if you like. (SetWindowText could be used to put it in the title bar.) It will be enough to reset the score to zero after each volley--the point of the exercise is not score-keeping. You might want to use the Beep function for sound effects when the ball bounces.

To move the ball, I recommend the approach with MoveWindow which was demonstrated with the Mona Lisa in class. Of course, this time the positions are not random, but are calculated more or less as in the balls program, according to the ball's current velocity. You could also use a memoryDC and just write the ball at the right location into the memoryDC each time it moves. Either one should work.

The ball always travels in straight lines and bounces off the paddle just like off the wall--the paddle has no velocity and there is no way to put "spin" on the ball. That would probably require the multimedia timing functions. You could measure the time between two successive WM_MOUSEMOVE messages to calculate the paddle's velocity , but it probably would not be accurate enough. You can't use WM_TIMER for serious video game programming.

................
................

In order to avoid copyright disputes, this page is only a partial summary.

Google Online Preview   Download