Callbacks

A callback is a Function called by Windows when an event occurs. In the previous modal dialog example, when the OK button is clicked by the user, Windows calls the OkButton() function. PowerBASIC's Dynamic Dialog Tools allows you to create a single callback to handle all events for the dialog, or you can create individual Callback Functions for each control in your dialog. You can even use a combination of the two methods.

 

Control Callback

If you've used Visual Basic, you'll be familiar with the concept of a Control Callback even though it's not called by that name.  A Control Callback is a function that is called when a %WM_COMMAND or %WM_NOTIFY event is generated for a particular control.  In the earlier example, we arranged it so the OkButton() function was called when the OK button was clicked.  Further, when the Cancel button was clicked, the CancelButton() function was called.  A Control Callback function is enabled when you execute a CONTROL ADD statement using the CALL CtlProc option at the end.

CONTROL ADD BUTTON, hDlg, %IDOK, "OK", 34, 32, 40, 14, %BS_DEFAULT OR %WS_TABSTOP CALL OkButton

CONTROL ADD BUTTON, hDlg, %IDCANCEL, "Cancel", 84, 32, 40, 14 CALL CancelButton

Some controls, like text boxes, list boxes, and combo boxes, can generate more than one type of event.  In VB, each separate event on each control is handled by a new function.  For example, if your VB form includes a list box, it may include a Callback Function such as List1_Change() that is called whenever the current selected item changes.  In PowerBASIC, only a single Callback Function is needed for each control.  When an event occurs, the Callback Function just chooses which events to handle, and which events to ignore.  If your PowerBASIC callback wanted to process the Change event for a list box, your code would look like this:

CALLBACK FUNCTION List1() AS LONG
  IF CB.MSG = %WM_COMMAND THEN
    IF CB.CTLMSG = %LBN_SELCHANGE THEN
      ... your code here ...
      FUNCTION = 1
    END IF
  END IF

END FUNCTION

You can use a combination of the CB.MSG and CB.CTLMSG functions to decide exactly which event has occurred. Generally speaking, in a Control Callback, CB.MSG will contain either %WM_COMMAND or %WM_NOTIFY.  The CB.CTLMSG will return the specific message is either of those two categories.  In this example situation, the control notification  %LBN_SELCHANGE is sent to the callback for the list box whenever the item in the list box changes (the user clicks on the new item or uses the keyboard to select a new item).

All of the control and dialog message equates are located in the DDT.INC file. This file is simply a subset of the much larger WIN32API.INC file and is provided only for convenience. Therefore, the use of these two files is mutually exclusive.

If your code processes a message, it should return TRUE (any non-zero value) by setting FUNCTION = number within the Control Callback.  This advises that there is no need to process that message further.  If you return the value FALSE (zero), the message is passed on to your Dialog Callback, if you have one. If the message is still unhandled by your Dialog Callback, the DDT dialog engine itself will handle the message on your behalf.

If your code processes a %WM_NOTIFY message, the return value is generally ignored.  Because of the nature of %WM_NOTIFY messages, they are always directed to both Control callbacks and Dialog callbacks to use as needed.

Prior to version 9.0 of PowerBASIC for Windows, Control Callback Functions received only %WM_COMMAND messages.  Beginning with PB 9.0, %WM_NOTIFY messages are sent as well.  There are many situations where these added messages will prove to be very important.  If your existing callback functions are written with complete error checking (ensuring that CB.MSG = %WM_COMMAND), this minor addition will cause no problems.  It just presents additional information which can be acted upon, or just ignored.  However, if callbacks were written without complete error checking, some ambiguity is possible.  In this case, you should either update your Control Callback code, or suppress %WM_NOTIFY messages with a #MESSAGES COMMAND metastatement.   

When a Control Callback receives a click notification for a control, the callback will receive a %WM_COMMAND message in the CB.MSG variable. A common mistake made by programmers is to fail to test both CB.MSG and CB.CTLMSG parameters before responding to the message.  If the message is truly generated from a click event, CB.CTLMSG will contain %BN_CLICKED.  This simple test ensures that your code responds correctly to notification messages.

CALLBACK FUNCTION OkButton() AS LONG
  IF CB.MSG = %WM_COMMAND AND CB.CTLMSG = %BN_CLICKED THEN
    '...Process the click event here
    FUNCTION = 1
  END IF
END FUNCTION

It pays to be sure you are responding to the correct message in your callback. Subtle bugs can occur if you aren't very careful to notice and recognize unanticipated messages.

It should also be noted that there are ranges of notification messages that individual controls can send to the Control Callback or Dialog Callback.  However, many of these messages are suppressed unless the controls have been initially assigned a "notify" style. For controls that are members of the Button class (CHECKBOX, OPTION, FRAME, etc.), this is the %BS_NOTIFY style. Please refer to the statements for additional information on notification styles for other control types.

 

Dialog callback

If you review the example code in most Windows programming books (particularly the Windows 32-bit SDK), you will see that most of the examples create a single callback for the entire dialog. Each time the user presses a button, a message is sent to this Callback Function. Within this Callback Function, there is often a large SELECT CASE or IF/ELSEIF/THEN structure, designed to pick out the incoming event messages and then process the selected messages.

C programmers are usually quite familiar with this concept, and often resort to using "Message Cracker" functions to separate their event handling code into a set of independent functions.  On the other hand, PowerBASIC's DDT takes much of this drudgery away.  By permitting separate callbacks for each CONTROL ADD statement, you become free to enclose your event handling code in separate functions, just like a C programmer may do, but without the confusing macros C programmers are often forced to use.

DDT gives the programmer the choice of either using a single callback to handle all dialog and control events, or writing a callback for each (or any) specific control. If you intentionally omit a callback for a particular control, the programmer has the choice of handling messages for that control within the dialog Callback Function, or ignoring them altogether.

In addition to handling control messages within the dialog callback, this Callback Function also provides a way to handle events that concern the actual dialog box itself.  For example, handling a %WM_PAINT message, or notification that the dialog was minimized, etc.

A Dialog Callback function is enabled when you execute a DIALOG SHOW statement using the CALL DlgProc option.

DIALOG SHOW MODELESS hDlg CALL DlgProc TO lResult&

or:

DIALOG SHOW MODAL hDlg CALL DlgProc TO lResult&

These two lines of code specify that dialog related event messages should be directed to the Callback Function DlgProc().  If we rewrote the earlier DDT example to use a single Dialog Callback instead of individual Control Callback Functions, the function might look something like this:

CALLBACK FUNCTION DlgProc()
  SELECT CASE CB.MSG
    CASE %WM_COMMAND
      IF CB.CTLMSG = %BN_CLICKED THEN
        IF CB.CTL = %IDOK THEN
          DIALOG END CB.HNDL, 1
          FUNCTION = 1
        ELSEIF CB.CTL = %IDCANCEL THEN
          DIALOG END CB.HNDL, 0
          FUNCTION = 1
        END IF
      END IF
  END SELECT
END FUNCTION

To complete this stage of modifications, you would also remove the "CALL OkButton" and "CALL CancelButton" parameters from the CONTROL ADD lines.  Once changed, this modified code produces the identical behavior of the original example with only a single Callback Function.

This simple example only scrapes the surface of what can be achieved in a Dialog Callback Function.  For example, by intercepting a %WM_ERASEBKGND message, you could draw onto the dialog client area, producing colorful dialogs with ease.

 

Callback Return Values

Callback functions always return a long integer result.  The primary purpose of this return value is to tell the PowerBASIC DDT engine and the Windows operating system whether your Callback Function has processed this particular message.  If you return the value TRUE (any non-zero value), you are asserting that the message was processed and no further handling is needed.  If you return the value FALSE (zero), the PowerBASIC DDT engine will manage the message for you, using the default message procedures in Windows.  If you do not specify a return value in the function, PowerBASIC chooses the value FALSE (zero) for you.

The term "process a message" may have many meanings.  If it's a simple notification of a change in focus or style, which has no impact on your program, you may decide to consider it processed, yet do nothing.  In other cases, your reaction could be quite complex and involved.  As the programmer, that's your decision to make.  But, regardless of your reaction, you should consider a message "processed" (returning a true value) whenever no further handling of the message (by DDT or Windows) is needed.

In some cases, especially when dealing with Common Controls and custom controls, you may be required to return a second result value through a special Windows data area named DWL_MSGRESULT.  When you complete a Callback Function, PowerBASIC automatically copies any non-zero return value to DWL_MSGRESULT, if you haven't done so already.  Therefore, it's generally safe to ignore this requirement in your code.

In most cases, when you process a message, you'll return a generic value for TRUE, such as:  FUNCTION = 1.  However, some messages require that you return a special value for TRUE, such as a graphical brush handle.  As long as the value is non-zero, you can return it in the normal manner (with FUNCTION=n), since any non-zero value automatically implies that the message was processed.

That said, there are a few unique messages which may require special handling.  Luckily, they're rare, but some just "break all the rules" listed above.  For example, you might find one which requires a zero result, even when you have processed the message.  You may find another which requires the return value be different from DWL_MSGRESULT.  For these very special cases, you can simply specify two return values:

FUNCTION = 1, BrushHandle&

In this form, the first numeric expression specifies the value to be returned from the Callback Function.  The second numeric expression tells the value to be assigned to DWL_MSGRESULT.  When you use this double parameter assignment, the results are absolute.  PowerBASIC assumes you have processed the message, regardless of the values given. PowerBASIC makes no other assumptions of any kind about these values. A double parameter function assignment is only allowed in a Callback Function.

Previous versions of PowerBASIC did not offer a double parameter form of function return.  This caused some difficulty with a few Windows messages which required a special return value of zero. If you return a value of zero (0) with the single parameter form, it implies the message was not processed at all by the Callback. This issue is totally circumvented by the double parameter form.

 

See Also

Dynamic Dialog Tools (DDT)

Creating a Dialog

Adding Controls to the Dialog

Modal vs. Modeless

Controls

Control Styles

Dialog Styles

Menus