Method Two

The first method for setting the timer causes WM_TIMER messages to be sent to the normal window procedure. With this second method, you can direct Windows to send the timer messages to another function within your program.

The function that will receive these timer messages is termed a ”call-back“ function. This is a function within your program that is called by Windows. You tell Windows the address of this function (well, not really the address of the function, but we'll get to that), and Windows later calls the function. This should sound familiar because a program's window procedure is really a type of call-back function. You tell Windows the address of the function when registering the window class, and Windows calls the function when sending messages to the program. However, call-back functions that are not window procedures must be handled a little differently.

SetTimer is not the only Windows function that uses a call-back function. The CreateDialog and DialogBox functions (discussed in Chapter 10) use call-back functions to process messages in a dialog box; several Windows functions (EnumChildWindows, EnumFonts, EnumObjects, EnumProps, and EnumWindows) pass enumerated information to call-back functions; and several less commonly used functions (GrayString, LineDDA, SetResourceHandler, and SetWindowsHook) also require call-back functions. Call-back functions are often a major hang-up for beginning Windows programmers. Some strange things are involved. I'm first going to tell you how to use call-back functions, and then I'll tell you the reasons for what you're doing.

Like a window procedure, a call-back function must be defined as FAR PASCAL because it is called by Windows from outside the code segment of the program. The parameters to the call-back function and the value returned from the call-back function depend on the purpose of the call-back function. In the case of the call-back function associated with the timer, the input parameters are the same as the input parameters to a window procedure. The timer call-back function returns a WORD value to Windows.

Let's name the call-back function TimerProc. (You can name it anything you like.) It will process only WM_TIMER messages.

WORD FAR PASCAL TimerProc (HWND hwnd, WORD message, WORD wParam, LONG lParam)

{

[process WM_TIMER messages]

return 0 ;

}

The hwnd input parameter is the handle to the window specified when you call SetTimer. Windows will send only WM_TIMER messages to TimerProc, so a message will always equal WM_TIMER. The wParam value is the timer ID, and the lParam value can be ignored. (It is set to the address of the function.)

Just as you must include your regular window procedure in the EXPORTS section of the module definition (.DEF) file, you must also include the names of any call-back functions within your program. When using a call-back function named TimerProc, your module definition file contains the following lines:

EXPORTS WndProc

TimerProc

As I noted earlier, the first method for setting a timer requires a SetTimer call that looks like this:

SetTimer (hwnd, 1, wMsecInterval, NULL) ;

When you use a call-back function to process WM_TIMER messages, the fourth parameter to SetTimer is instead the far address of the call-back function.

But not really.

Now listen carefully: The far address that you must pass to Windows as the fourth parameter of the SetTimer call is not the address of the function within the program. It is instead a far address obtained from the Windows function MakeProcInstance. To use MakeProcInstance, first define a variable that is a far pointer to a function. You can use the WINDOWS.H identifier FARPROC for this definition:

FARPROC lpfnTimerProc ;

In WinMain (or any other section of your program that is executed only once for each instance), call MakeProcInstance. The parameters to MakeProcInstance are the address of TimerProc and the value of hInstance. MakeProcInstance returns a far pointer that you save in lpfnTimerProc:

lpfnTimerProc = MakeProcInstance (TimerProc, hInstance) ;

You can now use this lpfnTimerProc value when you call SetTimer:

SetTimer (hwnd, 1, wMsecInterval, lpfnTimerProc) ;

You're done. Now that's not too bad, is it?

Well, you may say, ”This is so weird that I'll never use this method for setting a timer. I'll use the first method, where I don't have to bother with call-back functions.“ That's fine. But you're going to be forced to deal with call-back functions, EXPORTS, and MakeProcInstance when we start discussing dialog boxes. You can't do a dialog box without them. So you can pay your dues now, or you can pay them later.

A sample program

Let's look at some sample code so you can see how this stuff fits together. Then we'll explore MakeProcInstance some more. The BEEPER2 program, shown in Figure 5-5, is functionally the same as BEEPER1 except that Windows sends the timer messages to TimerProc rather than WndProc. To tell the C compiler that TimerProc is a function so that we can use the name of the function when calling MakeProcInstance, we declare TimerProc at the top of the program along with WndProc. Notice that the program calls MakeProcInstance for each instance.

I mentioned above that the lParam value passed to TimerProc is the address of the TimerProc function. Not exactly. It is actually the value of lpfnTimerProc returned from MakeProcInstance, if you ever need to use it.

BEEPER2.MAK

#-----------------------

# BEEPER2.MAK make file

#-----------------------

beeper2.exe : beeper2.obj beeper2.def

link beeper2, /align:16, NUL, /nod slibcew libw, beeper2

rc beeper2.exe

beeper2.obj : beeper2.c

cl -c -Gsw -Ow -W2 -Zp beeper2.c

BEEPER2.C

/*----------------------------------------

BEEPER2.C -- Timer Demo Program No. 2

(c) Charles Petzold, 1990

----------------------------------------*/

#include <windows.h>

#define ID_TIMER 1

long FAR PASCAL WndProc (HWND, WORD, WORD, LONG) ;

WORD FAR PASCAL TimerProc (HWND, WORD, WORD, LONG) ;

int PASCAL WinMain (HANDLE hInstance, HANDLE hPrevInstance,

LPSTR lpszCmdLine, int nCmdShow)

{

static char szAppName[] = "Beeper2" ;

FARPROC lpfnTimerProc ;

HWND hwnd ;

MSG msg ;

WNDCLASS wndclass ;

if (!hPrevInstance)

{

wndclass.style = CS_HREDRAW | CS_VREDRAW ;

wndclass.lpfnWndProc = WndProc ;

wndclass.cbClsExtra = 0 ;

wndclass.cbWndExtra = 0 ;

wndclass.hInstance = hInstance ;

wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ;

wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ;

wndclass.hbrBackground = GetStockObject (WHITE_BRUSH) ;

wndclass.lpszMenuName = NULL ;

wndclass.lpszClassName = szAppName ;

RegisterClass (&wndclass) ;

}

hwnd = CreateWindow (szAppName, "Beeper2 Timer Demo",

WS_OVERLAPPEDWINDOW,

CW_USEDEFAULT, CW_USEDEFAULT,

CW_USEDEFAULT, CW_USEDEFAULT,

NULL, NULL, hInstance, NULL) ;

lpfnTimerProc = MakeProcInstance (TimerProc, hInstance) ;

while (!SetTimer (hwnd, ID_TIMER, 1000, lpfnTimerProc))

if (IDCANCEL == MessageBox (hwnd,

"Too many clocks or timers!", szAppName,

MB_ICONEXCLAMATION | MB_RETRYCANCEL))

return FALSE ;

ShowWindow (hwnd, nCmdShow) ;

UpdateWindow (hwnd) ;

while (GetMessage (&msg, NULL, 0, 0))

{

TranslateMessage (&msg) ;

DispatchMessage (&msg) ;

}

return msg.wParam ;

}

long FAR PASCAL WndProc (HWND hwnd, WORD message, WORD wParam, LONG lParam)

{

switch (message)

{

case WM_DESTROY :

KillTimer (hwnd, ID_TIMER) ;

PostQuitMessage (0) ;

return 0 ;

}

return DefWindowProc (hwnd, message, wParam, lParam) ;

}

WORD FAR PASCAL TimerProc (HWND hwnd, WORD message, WORD wParam, LONG lParam)

{

static BOOL fFlipFlop = FALSE ;

HBRUSH hBrush ;

HDC hdc ;

RECT rc ;

MessageBeep (0) ;

fFlipFlop = !fFlipFlop ;

GetClientRect (hwnd, &rc) ;

hdc = GetDC (hwnd) ;

hBrush = CreateSolidBrush (fFlipFlop ? RGB(255,0,0) : RGB(0,0,255)) ;

FillRect (hdc, &rc, hBrush) ;

ReleaseDC (hwnd, hdc) ;

DeleteObject (hBrush) ;

return 0 ;

}

BEEPER2.DEF

;------------------------------------

; BEEPER2.DEF module definition file

;------------------------------------

NAME BEEPER2

DESCRIPTION 'Timer Demo Program No. 2 (c) Charles Petzold, 1990'

EXETYPE WINDOWS

STUB 'WINSTUB.EXE'

CODE PRELOAD MOVEABLE DISCARDABLE

DATA PRELOAD MOVEABLE MULTIPLE

HEAPSIZE 1024

STACKSIZE 8192

EXPORTS WndProc

TimerProc

Proper handling of call-back functions

Let's summarize the three requirements for call-back functions. Any function within your program that is called by Windows must be handled as follows:

1.The function must be defined as FAR PASCAL.

2.The function must be included in the EXPORTS section of the module definition (.DEF) file.

3.The address of the function that you give to Windows must be the return value from a MakeProcInstance call. (This third rule does not apply to window procedures that are passed to Windows as part of a window's class structure in a RegisterClass call. Windows itself handles the MakeProcInstance requirement in this case.)

As you'll discover in Chapter 7, ”Memory Management,“ these three requirements are closely related to each other. They are part of the overhead necessary for Windows to run several instances of the same program using the same code segment, where each instance must have its own data segment. Windows also requires this overhead to move the code segment and the data segments around in memory. Here are the practical results of the three requirements:

1.When you define a function as FAR and you compile with the -Gw (Windows) flag, the compiler inserts special prolog and epilog code in the function. In assembly language, the prolog code sets the value of the AX register equal to the DS (data segment) register by using the PUSH DS and POP AX instructions. It then saves the value of DS (with PUSH DS) and sets DS equal to AX by using a MOV DS, AX instruction. The epilog code at the end of the function pops the original value of DS off the stack.

2.If the FAR function is also exported (that is, if the function is listed in the EXPORTS section of the module definition file), Windows replaces the PUSH DS and POP AX instructions at the top of the function prolog with NOP (no operation) instructions when the code segment is loaded into memory. With this change the function sets DS from the value of AX. But what is the value of AX on entry to the function? Well, if you didn't do anything else, the value of AX would be indeterminate. So would the operation of your program.

3.MakeProcInstance creates a small piece of code elsewhere in Windows called a ”thunk.“ The far address returned from MakeProcInstance is the address of this thunk. The thunk loads the segment address of the data segment in AX and branches to the function. The function prolog then loads DS from AX. Perfect.

Note that MakeProcInstance requires hInstance as a parameter. You must use MakeProcInstance to create a different thunk for each instance because each instance has its own data segment. For a particular call-back function, the thunks for each instance all branch to the same function address (because all instances use the same code segment), but each thunk sets AX to a different data segment—the data segment for that instance. When Windows moves a data segment in memory, it must change the thunk for that instance so that the thunk sets AX to the new data segment address. The thunk itself is always in an unmoveable area of memory.

(To further complicate this matter, any reference in your program to a FAR function—such as the address passed to the MakeProcInstance call and the address that the thunk branches to—is not even the address of the function within your program. Another small routine sits between the thunk and the actual function. This routine loads the code segment into memory if it has not yet been loaded or if it has been discarded from memory. But let's forget about this for now and come back to it in Chapter 7. The last thing I want to do here is make this subject sound as complex as it actually is.)

These requirements imply that you should never call exported far functions directly from within your program. For instance, you might want to simulate a timer message with the following statement:

TimerProc (hwnd, WM_TIMER, 1, 0L) ; // WRONG !!!

It looks OK, but don't do it! The prolog of TimerProc will set DS equal to AX, but the value of AX could be anything, and it very likely is not the segment address of the program's data segment. If you need to directly call an exported function from within your program, use the far pointer returned from MakeProcInstance:

(*lpfnTimerProc) (hwnd, WM_TIMER, 1, 0L) ; // RIGHT

This code calls the thunk, and the thunk sets AX equal to the correct data segment before branching to the function.