04 July 2021

Interacting with Forms (Part 2)

This is the second post in a series about forms. In the first post we discussed the commands to create forms and their window styles. Now we’ll discuss how to interact with forms.

Owner and owned forms
The commands OpenW and Form support the creation of owned forms (windows). They provide the Owner clause to specify the ‘parent’ or owner of the new form. The following example creates an overlapped non-owned window (Win_0) and an overlapped owned window frm.

OpenW 0, 100, 100, 300, 300 : TitleW 0, "Owner"
Form Owner Win_0, frm = "Owned", 150, 170, 300, 200
Do : Sleep : Until Me Is Nothing

There are advantages and disadvantages of using a parent-owner relationship.

  • The owned form (frm) is always displayed on top of the non-owned window (Win_0). It is therefor mostly used for forms that behave as dialog boxes.
  • When one of the windows is activated the other is activated automatically.
  • When the parent window is closed the owned window is closed as well. Not the other way around. When you close form frm the Win_0 form is not closed and Me does not become Nothing. The message loop is not ended. When you close Win_0 the form frm is closed as well and Me will become Nothing.
  • The owned window is not disabled (for input) if the parent is disabled. When a program displays a modal dialog box (a form with Ocx controls that disables all other windows while on screen) all other forms have to be disabled explicitly.

Suppose a program wants to display an About box in a modal dialog box. In GB32 a modal dialog box (form) is simulated by showing an owned form and then disabling input for the other forms using the Disable property. Then, after closing the modal dialog box form the other forms are enabled again using the Enable property. Like this:

OpenW 0, 100, 100, 300, 300 : TitleW 0, "Owner"
Ocx Command cmd = "About", 10, 10, 100, 24
Form Owner Win_0, frm = "Owned", 150, 170, 300, 200
Do
  Sleep
Until Me Is Nothing
Sub cmd_Click
  ' Display frm1 (owned by Win_0) created using the Form-Editor
  LoadForm frm1, Win_0.Left + PixelsToTwipX(50), Win_0.Top + PixelsToTwipY(50)
  Win_0.Disable         ' no input allowed
  frm.Disable           ' no input allowed

Sub frm1_Destroy        ' frm1 is being destroyed
  frm.Enable            ' enable input
  Win_0.Enable          ' enable input
  Win_0.Activate        ' activate

In the form-editor the Owned property is set to True. The form only has one Ocx control; a label with an enlarged font. The LoadForm command uses the current active form as the owner for the About box. The result is this, only the About box is active and enabled:

Screenshot 2021-06-28 121116

The message loop
To interact with forms the application needs a message loop. The most common message loop uses Sleep and ends with Until Me Is Nothing (Do : Sleep : Until Me is Nothing). When all windows are closed Me – the current active form - becomes Nothing and the program will end.
Sleep waits for a queued message; a message that is posted to the window’s message queue. The OS posts only a limited amount of messages, most messages are sent to the window-procedure directly. In general, the posted messages include the input messages for keyboard and mouse, the WM_PAINT and WM_TIMER message.

After Sleep detects a new message it dispatches (relays) the message to the window’s window-procedure. So, in the end all messages (posted or sent) end up in the window procedure of the window. The window procedure is responsible for executing the event subs.

Sleep is not the only command that reads the message queue, others are the (also new) DoEvents, and the older 16-bit GFA-BASIC compatible commands GetEvent, and PeekEvent. Sleep is the preferred command to wait for and handle a message. When the Sleep command executes it first processes all pending (waiting) messages and then puts the program to sleep using WaitMessage(). When a new message arrives Sleep processes all new message(s) before it returns back to the program. Schematically, from left to right Sleep executes:

If any: Handle All Messages + Sleep(0) WaitMessage Handle All
Messages + Sleep(0)
=> return to program (loop)

As part of Handle All Events the Sleep command invokes the Windows API Sleep(0), giving up the remainder of the time slice that the scheduler assigned to the thread (process).

The other command that waits for a message is GetEvent, but this works a bit different. Upon entering the GetEvent handles one event if there is a message present in the queue. Without any pending messages GetEvent invokes WaitMessage() and halts the program. After obtaining a message it continues by processing exactly one event. If more messages are present GetEvent will process them the next time it is executed. Schematically, there are two situations:

Message In queue? Handle One Message => return to program (loop)  
No pending messages? WaitMessage Handle One Message => ret to prog

Because GetEvent handles only one message a time it is able to place GFA-BASIC 16 compatible message-info in the MENU() -array. An application can respond to the values of the MENU() array inside the GetEvent-message loop. The GetEvent command does not invoke Sleep(0). With multiple messages pending the GetEvent loop will run until all messages are handled without giving up time, which is not a real problem. After all, the Sleep command does not call Sleep(0) before it handled all pending messages.

PeekEvent is like GetEvent: it fills the MENU() array, but it doesn’t wait for a message, it returns immediately to the program.

Handle One Message (if any) => return to program (loop)

Because PeekEvent is usually placed in a (message) loop, it is likely that the CPU will be overloaded and will be running at 100%. You will need some mechanism to give up some processor time, for instance by using the Sleep(ms) API or the GB32 command Sleep ms inside the message loop.

DoEvents is a bit like PeekEvent: it doesn’t wait for a message. However, rather than processing only one message at a time, DoEvents handles all pending messages in a row before it returns to the program.

Handle All Messages + Sleep(0) => return to program

This does not mean that DoEvents - when used in a loop - will let the processor run at 100%. Before returning to the program DoEvents invokes Sleep(0) to give up CPU time to other processes. DoEvents is often used in a time-consuming loop to keep the GUI responsive.

All these commands Sleep, GetEvent, PeekEvent, and DoEvents handle the keyboard navigation between Ocx controls on the form. However before the keyboard navigation is applied GB32 invokes the Screen_Preview event sub if it is present. This event sub allows the program to intercept queued keyboard messages (WM_KEYFIRST – WM_KEYLAST) to respond to a key and/or cancel the message if it wants to.

Sub Screen_KeyPreview(hWnd%, uMsg%, wParam%, lParam%, Cancel?)

Set the Cancel? parameter to True when a keyboard message is handled and you don’t want to pass it on to the Ocx-control keyboard-navigator or to the window-procedure.

Event Subs
All message retrieval commands, including GetEvent and PeekEvent, execute event subs. (To be precise, the window procedure invokes the event subs.) Using event subs is the preferred way to respond to a message taking away the need to distinguish between posted and sent messages as in previous versions of GFA-BASIC. To handle a form-message simply add an event sub to the program. Note that when a form is being created it executes event subs while the form’s object variable isn’t initialized yet. In particular, the _Paint and _Resize events are invoked without having access to the form’s variable. This behavior is discussed in the previous blog post: Forms, the base of an app.

The _MessageProc sub event
A powerful event sub is the _MessageProc sub event. If defined, it is called for each and every message that is relayed to the window-procedure, including the posted messages.

' Called for all messages
Sub Win_0_MessageProc(hWnd%,Mess%,wParam%,lParam%,retval%,ValidRet?)

You need to know what to do if you handle a message in the _MessageProc event sub. This knowledge is acquired through the Windows SDK (now available on line). For the sent messages the program needs to specify a return value in the by reference variable retval% and set the ValidRet? variable to True. This is necessary; if the program sets ValidRet? the message is considered handled and any event subs that would otherwise be invoked will not be executed. When ValidRet? is set the form’s window procedure will return to the caller (Windows OS) immediately. To handle a posted message – one that doesn’t require a return value - you needn’t set retval%, but to prevent further handling you should set ValidRet? though.

The _Message event sub is for posted messages
The _Message event sub is intended to process queued (posted) messages only. However, the _Message event sub is called for all messages (like the _MessageProc event sub) except for WM_PAINT and WM_ERASEBKGND (!?). Because of its syntax – it misses retval% and ValidRet? - it can only be used to process posted messages, the messages that don’t require a return value:

' Process posted messages only
Sub Win_0_Message(hWnd%, Mess%, wParam%, lParam%)

Even when a program handles a message in the _Message event sub the event sub for that message is invoked as well. For instance, if WM_LBUTTONDOWN is handled in the _Message event sub and the _Click event sub is defined as well, the _Click event is also executed. If both _Message and _MessageProc subs are used both subs are invoked, first the _Message event sub followed by the _MessageProc. If a program responds to a (posted) message in the _Message event sub, it shouldn’t process it in _MessageProc again, or in some other event sub.
The practical use of the _Message event sub is limited, it is intended to port GB16 code that uses the MENU()-array or _hWnd, _Mess, _wParam, _lParam to GFA-BASIC 32. Because the _Message event sub is called from the window-procedure a program can handle all posted messages when the message loop uses Sleep or DoEvents.

No comments:

Post a Comment