User-Independent Time out processes in FoxProBy Colin Nicholls This article was first published in the July 1993 issue of Pinnacle's FoxTalk magazine. There are a number of useful things I've always wanted to add to my FoxPro applications that I call "user independent". Like screen savers, or popup reminders, and other things, that are not a consequential effect of normal program flow - they happen independent of the user's current focus in your application. The Main Event:I assume you are familiar with screen savers: after a predefined period of inactivity from the user, the display is blanked. This is a "time out" process. Rather like lighting the fuse on a bomb: In a screen saver or similar time out process, every time the user stops typing, the "fuse" is lit. Unless they extinguish the fuse by pressing a key on the keyboard, after a short time the "bomb" will "explode". If you have ever wanted FoxPro to automatically tell you when it is time to go home, blank the screen for you, or have clients that want an automatic backup of data in their applications, then this article will probably interest you. There are two classes of independent processes that I have identified: those that occur after a set period of inactivity, e.g. a Screen Saver, or an automatic logout - I call these IDLE time out processes; and those that occur at set times, e.g. a "reminder" alert box, calendar alarm, or automatic backup - these are SCHEDULED time out processes. These "independent processes" would normally have to be supplied through some external TSR program loaded prior to running FoxPro. These TSR's can take away valuable conventional memory from your FoxPro applications, and use undesirable key combinations to invoke. Also their user interfaces can be non-standard, making it difficult to integrate with your applications. The FoxPro API and Library Contruction Kit (LCK) provides a way for us to add functions to FoxPro's native syntax. I am going to present here a library containing a few simple functions that, when added to FoxPro's command set, will allow us to program many of the time out and scheduling functions traditionally requiring TSR utilities directly in your FoxPro applications. Event Handling - The Basic Mechanism:The FoxPro interface is often described as "Event - Driven". On the surface, this means that FoxPro does things when you tell it to: by clicking on a menu or pressing a key. Behind the scenes, however, Foxpro is responding to EVENTS, taken in turn from a queue (which is filled with the events as they occur.) Events can be thought of as little packets of information describing discrete things FoxPro needs to respond to. When you press a key, select a menu option, or click the mouse, an event is generated. There are events for menu selections, mouse movements and clicks, keystrokes, and ON KEY LABEL traps. Another type of event is added to the queue when nothing is happening: NULL events. Events are taken from the queue and passed through the "event handler". This is a bit like a game of "pass the parcel", where the event is passed down the line of event handler routines, until one of them processes it. If the event is still in the queue at the end, FoxPro processes it. (e.g. a window CloseEvent results in FoxPro closing a window on the screen.) Each event contains information: what type of event it is, when it occurred, and what the mouse position was, for example. When our library is loaded, another event handler is added to the chain of event handler routines. Our event handler code gets to examine each event before FoxPro does. This is where the core function of our library takes place: we can modify the event data of any given event and turn it into another event - fooling FoxPro into thinking that something else has occurred. For example: that the user pressed a particular key! We make use of this in our applications by ensuring that if the user HAD pressed that key, the desired action would occur. We can't modify any old event that is passed through our handler, of course - we might prevent windows from closing, swallow mouse clicks, or worse! It is safer to take only null events and turn them into key press events. Time Out, Already!All we need to do to implement a time out process is to keep track of how long the periods of inactivity are between the user's keystrokes, mouse-clicks, and other (non-null) events. In the EVENTS library event handler, each non-null event is observed and its time of occurrence noted. Each subsequent null event is examined to see when it happened, and the time since the last non-null event calculated. If the user does not insert a non-null event into the queue (via a key-press, or whatever), then as soon as this calculated value exceeds our desired time out value, the handler mutates the next null event into a key press event of our choosing. It is up to our FoxPro code to attach an appropriate procedure to that key press. Using the EVENTS libraryWhen you execute the command SET LIBRARY TO EVENTS, five functions are added to FoxPro's command set. The first, Events_Ver() is trivial: it returns the version string of the event library. The four other functions in EVENTS.PLB are paired up to manage the two (IDLE and SCHEDULED) types of time out processes. The calling syntax of these functions is as follows:
SetIdle() and SetSched() take three parameters - a numeric key press code <KeyID>, a numeric time out period <Seconds>, and an optional logical parameter, <OnKey>. The <KeyID> parameter indicates which keypress event is to be generated. Some example key codes: Keypress Event Key Codes
Note that the key codes are not the same as those returned by the native Foxpro INKEY() function, but are Key ID codes associated with "KeyPress" events in FoxPro's event queue. Table 1 was obtained from the FP2.0 LCK documentation page 4.4 - 4.8., but shows the hexadecimal values converted to decimal. Be warned that I have had some problems using Alt-Key codes when using ON KEY LABEL traps. I mostly use Ctrl-F5 / Ctrl-F6. Also you may want to avoid F11/F12 in case they aren't available! NB: Appendix A of the FoxPro 2.5 LCK documentation discusses these key codes in more detail. To understand the existence of the <OnKey> parameter, it is important to understand that pressing a key on which an ON KEY LABEL trap has been set will generate an event with different associated data than that which does not have the ON KEY LABEL. This sounds strange, but trust me: if we want to fool FoxPro into thinking that Ctrl-F5 has been pressed, and we have an ON KEY LABEL trap on that key, we need to generate event data that is slightly different from that contained in a normal keypress event. Passing an <OnKey> value of .T. to SetIdle() and SetSched() indicates that an ON KEY LABEL keypress event is to be generated, rather than a normal key press event. SetIdle() and ClrIdle() are used to enable a idle time out process and to disable it respectively. SetSched() and ClrSched() are used to enable a periodic time out process and to disable it respectively. Following is an example of programming each sort of time out function, starting with an idle time out. Idle Time Out: An ExampleThe following example program loads the appropriate event library for your version of FoxPro, and adds a bar "Time Out" to the System menu popup. This is given a hot key shortcut of Ctrl-F5, and the routine IDLER is attached to it. *-----------------------------------------------EV_EX1.PRG: set talk off set procedure to (program()) do case case "2.5" $ version() set library to events25 additive case "2.0" $ version() set library to events20 additive endcase public CTRL_F5 CTRL_F5 = 354 define bar 1 of _msystem prompt "Time Out" ; key Ctrl-F5,"^F5" on selection bar 1 of _msystem do idler =setidle( CTRL_F5, 5 ) clear ? ? "Ctrl-F5 will be pressed every 5 seconds..." ? 'type "? CLRIDLE()" to disable.' return *--------------------------SubRoutine to run after timeout: procedure idler *------------------------------disable Timeout temporarily: =clridle() ?? chr(7) wait window "Timed out! " timeout 0.5 =setidle( CTRL_F5, 5 ) return *------------------------------------------EOF: EV_EX1.PRG: If you run the example (EV_EX1.PRG on the source code disk) and wait for 5 seconds, you will see a WAIT WINDOW message pop up. You can trigger this yourself by pressing Ctrl-F5, or by selecting "Time Out" from the system menu, but if you start typing, the message does not appear - until you pause for more than 5 seconds. Then the event handler we installed will press it for you! To disable the time out routine, type "=clridle() <Return>" at the command window, or alternatively, "set library to <Return>". If you prefer to use an ON KEY LABEL instead of adding a bar to the system menu, replace the lines: define bar 1 of _msystem prompt "Time Out" ; key Ctrl-F5,"^F5" on selection bar 1 of _msystem do idler with: on key label Ctrl-F5 do idler and add a third parameter to the SetIdle() call, thus: =setidle( CTRL_F5, 5, .T. ) This example is a bit silly, I know, but it isn't hard to extend the idea into something a bit more practical. For example, write a screen blanking routine and attach it to your application menu, with a hot key shortcut of F8. Decide on a good time out period for a screen saver (say 1 minute), and include the following lines in your application: set library to events =setidle( 322, 60 ) After 60 seconds of inactivity, the event handler will press the F8 key for you and invoke your screen blanking routine. Or instead of a screen saver, get the event handler to keyboard Ctrl-Q to quit the application after 10 minutes of inactivity from the user as a security measure. Aside: If you are using menu bars / hot key short cuts, ensure your routine can be invoked from inside MODAL READs by re- defining your popup bar in each READ WHEN clause eg: define bar x of mymenu prompt "Time Out routine" (Note that you don't need to include the "KEY <label>" clause. That will remain valid after the re-definition.) ON KEY LABEL traps remain active under modal reads - but you may need to consider any PUSH KEY CLEAR statements, which will affect them. But wait... there's more!You have probably realised that only one timeout action can be active at any one time. For example, if IDLER is a screen saver that kicks in after 30 seconds, then you can not also have a simultaneously active automatic shutdown routine with a 5 minute timeout. You might try to get around this by using a nested call - something like this: *--------------------------SubRoutine to run after timeout: procedure idler *-------------------------------------------save OKL state: push key clear *---------------------------------------------setup action: on key label Ctrl-F6 do shutdown.prg *-------------------------------------set 5 minute timeout: =setidle( CTRL_F6, 300, .T. ) *---------------------------------------------------------: ....( screen saver code ) *--------------------------------------------------restore: pop key *-------------------------------re-enable original timeout: =setidle( CTRL_F5, time_out_period ) return In other words, during the execution of the first idle time out code, a second idle time out action is prepared. This may not always be possible: for example, the first idle time out routine may be generating some non-Null events, which would continually reset the null-event "stopwatch", and the second time out period would never be exceeded. Try it and see... Scheduled Time OutsScheduled time out processes are different from idle time outs in that they are required to occur irrespective of whether or not the user is active or not, and at quite arbitrary times. For example, a reminder message box could be required to pop up at 5:30pm to announce that everyone should log out of the application to allow routine database maintenance. In order to schedule FoxPro routines to run at preset times , we will need a database of scheduled actions. We will need to store: What to do; When to do it; and (in multi-user environments of course) Who to do it to. We will also need a routine to check the database to see if anything is due to happen, and if required, do it! We could use SetIdle() to trigger a routine that does this, but idle time outs only occur when the user has stopped working, so a busy users would never see them. (I have to stop working for 5 minutes before my screen saver takes over, for example.) It is better to have a more regular event handler process - one that doesn't reset its stopwatch when non-null events pass through. EVENTS.PLB contains a further two functions that will allow this: SetSched() and ClrSched():
They are called identically to SetIdle() and ClrIdle(). See "Using the EVENTS library" above for descriptions of each parameter. Avoiding a Nasty Side EffectIn my first version of this library, SetSched() caused the requested key press event to be generated periodically (the period specified by the <Seconds> parameter). An efficient FoxPro routine attached to the key press would then be dispatched to check the database to see if anything needed doing. This worked well - except for one unavoidable side effect. You can observe the effect yourself: You are in a READ, and you select a menu option or press a key with an ON KEY LABEL routine. When the routine returns, the cursor is repositioned at the beginning of the GET field you were in at the time. My clients pointed out that it was very distracting (to say the least) to have the cursor spontaneously reposition itself at the beginning of the GET field they were currently editing - this would happen whenever the event handler pressed the key to trigger the checking routine. I considered it an unacceptable side effect, and decided that in order to avoid this, the key press (and subsequent procedure call) must occur only if there is something scheduled to happen. This meant removing the database schedule check code from the FoxPro code, and moving it into the library. In other words, I needed to make the event handler interrogate the database and decide whether or not to issue the key press event. There is a function in the API called _Execute() that allows FoxPro statements to be executed from a C routine. However, this function is expressly forbidden to be called from an event handler. I determined this to be good advice when I tried to use it anyway. FoxPro code can cause the event handler to be called recursively, which is Not A Good Thing. So I ended up writing the database check routine in native C code, using the API functions. Although this makes the database check as fast as possible (which is good), there are some constraints we have to work with. Firstly, there is no way to open and close databases from an API routine. This means we must indicate to our Event Handler in which work area it may find our database. Secondly, a subset of the structure of the schedule database must be known by the API routine. The fields that are required by our library are:
Note that Errors will be reported if these fields are not in the structure of the database open in the workarea specified by the EV_AREA variable. Also note that events cannot be scheduled more accurately than whole minutes. One reason for this is that to ensure that an event will occur within, say 15 seconds of its scheduled time, the Event Handler must poll the database every 15 seconds. Although the overhead of the Scheduling code is small, I estimate that FoxPro performance will suffer should you attempt to schedule events to the second! I normally use a polling period of about 50 seconds, to ensure that a schedule record will be processed within the duration of the given minute. I have set it to 10 seconds and not noticed any performance degradation at all in interactive use. I'll just check my schedule...The database check code is contained in the internal C function SchRequired(), which is called every polling period. It does the following:
Step 2) sounds clumsy. You may think that a better way to prevent the SchRequired() routine from triggering on a record previously processed would be to delete the record, or otherwise indicate that it had been processed. But what happens in multiuser situations, where one record could be required to be processed by multiple users? The method of storing the timing data in EV_LAST[] is not elegant, but it seems to work. I'll leave the task of developing a better mechanism as an exercise for the student. (I discuss multi-user schedulers in more detail later.) Who pushed your button?The contents of the routine that responds to the key press from the event handler is largely arbitary. However there are some things you will have to do if you want to keep it practical: When the routine is called, the schedule database record pointer will be sitting on the record that instigated the key press. If your routine takes an appreciable amount of time to execute (if requires input from the user for example) then you will want to temporarally disable the Event Handler. Call ClrSched() at the beginning of your routine, and SetSched() again on your way out. Alternatively, PUSH KEY CLEAR / POP KEY or SET SKIP OF BAR would do just as well to disable it. Unless you want the Event Handler to react again next polling period to the same database record, you will want to store the timing data of the record in the EV_LAST array. It follows from the this that the EventHandler() will only see the first schedule record due in a given minute. (EventHandler() will skip records that have fields equivalent to the values in EV_LAST.) Therefore, if there is a possibility of more than one schedule record due in a given minute, your routine will be required to process all matching records. Periodic Time Out: an ExampleA simple example (although not a very practical one) that demonstrates event scheduling is provided in the following program, EV_EX2.PRG: *-----------------------------------------------EV_EX2.PRG: set talk off set procedure to (program()) set clock on *---------------------------------------------Load library: if "2.5" $ version() set library to events25 additive else set library to events20 additive endif * ( loading the library defines ev_last[5] ) *------------------------------------Define some constants: public CTRL_F6, ev_area CTRL_F6 = 355 *------------------------initialise database with 2 events: create table example ( EVENT_MSG C(25), ; SCH_YEAR N(4,0), SCH_MONTH N(2,0), SCH_DAY N(2,0), ; SCH_HOUR N(2,0), SCH_MINUTE N(2,0) ) ev_area = select() sch_year = year(date()) sch_month = month(date()) sch_day = day(date()) sch_hour = val( left( time(),2 ) ) sch_minute = val( substr( time(),4,2 ) ) + 1 event_msg = "This is the first one..." if m.sch_minute = 60 sch_minute = 00 sch_hour = m.sch_hour + 1 endif insert into example from memvar event_msg = "Whoops! Here's # 2..." sch_minute = m.sch_minute + 1 if m.sch_minute = 60 sch_minute = 00 sch_hour = m.sch_hour + 1 endif insert into example from memvar *--------------------------------------Define ON KEY LABEL: on key label Ctrl-F6 do evnttrap *------------------------------------Turn on the scheduler: =setsched( CTRL_F6, 15, .T. ) *---------------------------------------------------------: clear ? ? "Database initialised with 2 events:" list fields sch_hour,sch_minute,event_msg ? "Database polled every 15 seconds." return *---------------------------SubRoutine to run at key press: procedure evnttrap *------------------------------disable polling temporarily: =clrsched() *-------------------------------------update ev_last array: old_area = select() select (ev_area) scatter fields SCH_YEAR,SCH_MONTH,SCH_DAY,; SCH_HOUR,SCH_MINUTE to EV_LAST select (old_area) *--------------------------------------------Process Event: wait window example.event_msg + " : Press any key " *--------------------------------------re-enable scheduler: =setsched( CTRL_F6, 15, .T. ) return *------------------------------------------EOF: EV_EX2.PRG: Run this program and observe what happens. An example event database is created and listed on the screen. If you wait patiently for a couple of minutes you will see the messages pop up shortly after the system clock reaches the time indicated in the event record. These message popups should occur regardless of what database file you may be browsing, what program you are a editing, or what amount of mouse and keyboard activity may be going on. An exception is if you are running a computation-intensive program or a big report or PACK. ON KEY LABEL's typically get acted on in between statements in your program, whereras menu shortcut hot keys are only active during a READ, BROWSE, MIODIFY FILE or similar point in your application. You can observe that the event handler suspends its operation if the work area (EV_AREA) is vacant, or contains a database with an invalid structure, by running EV_EX2.PRG again, and closing the EXAMPLE database after the first event message pops up. If you now "USE example IN (ev_area)", the second scheduled event should pop up. SET LIBRARY TO or CLRSCHED() will unload the event handler. Building a better Event TrapThe program EVNTTRAP used here is not meant to be a practical example. In any real situation, we would need to make EVNTTRAP a little more sophisticated. You will want to add your own fields to the required structure, dependant on the type of processes you wish to trigger. You could make EVNTTRAP automatically back up your applications databases. It could check whether or not it was convenient to do so, and if not, re-schedule the back up process by changing the date and time fields of the schedule record. Some processes I have added to my applications include the ability to popup a message box at a scheduled time; to log out all users from the application at a specified time; and by enhancing the program to be multiuser aware, the ability for users to send messages to each other. Another enhancement is that due to the mechanism used to indicate what events have been processed, all events that are scheduled in a given minute must be processed at the same time, or they will be missed. A more practival EVNTTRAP is shown in the following example - two extra fields have been added to the schedule database: SCH_TYPE (type of process) and SCH_MSG (a message string). Depending on the value of SCH_TYPE, different processes are triggered. You may wish to adapt this to your own requirements: *---------------------------------------------------------- procedure evnttrap *---------------------------------------------------------- external array ev_last private old_area, curr_time private array ev_now[5] =clrsched() old_area = select() select (ev_area) scatter fields SCH_YEAR,SCH_MONTH,SCH_DAY,; SCH_HOUR,SCH_MINUTE to EV_NOW scan rest *---------------------------Search for valid recs: if SCH_YEAR < ev_last[1] loop endif if SCH_YEAR > ev_now[1] exit endif if SCH_MONTH < ev_last[2] loop endif if SCH_MONTH > ev_now[2] exit endif if SCH_DAY < ev_last[3] loop endif if SCH_DAY > ev_now[3] exit endif if SCH_HOUR < ev_last[4] loop endif if SCH_HOUR > ev_now[4] exit endif if SCH_MINUTE <= ev_last[5] loop endif if SCH_MINUTE > ev_now[5] exit endif *-------We are on a record due this instant: do case case SCH_TYPE = 1 do msgbox with SCH_MSG case SCH_TYPE = 2 && Emergency Exit flush quit case SCH_TYPE = 3 && What do you want to do? * your process code endcase endscan =acopy( ev_now, ev_last ) select (old_area) =setsched(393,30,.f.) return *---------------------------------------------EOF: EvntTrap A Networked SchedulerFor a multi-user scheduler, you will have to add some extra fields. For example: SCH_GLOBAL (Logical): indicates valid for all users SCH_USER (Char) : which user it applies to You will have to record the username of each user running the application, in M.ev_user, say, and them apply a filter to the schedule database: set filter to SCH_GLOBAL or SCH_USER=m.ev_user The API database routines in events.c will respect the filter and only create a keypress event when a record appropriate to the current user is due. You will also want a front-end in your application to allow users to add records to the schedule database. But that's another story... Inside the source...The source code of the event library is called EVENTS.C and should be present on the FoxTalk disk. (You can skip this section if you are not interested in some of the details of how the event library works.) When FoxPro executes the statement SET LIBRARY TO EVENTS, the function Ev_Init() is called, which, via the API call _ActivateHandler(), adds our event handling code (contained in the internal function EventHandler()) to the queue of event handling routines. It also creates and initializes the public array ev_last[5]. Events immediately get passed to EventHandler(), but this has very little effect because the timeout functions are initially turned off. (idleEnable is false and schedInterval = 0). All that happens is that the time each successive event arrives is recorded in the variable nonnullLast. The SetIdle() and SetSched() functions merely enable the time out portions of EventHandler by setting idleEnable true and schedInterval to a non-zero value. You may observe that events that are of type CloseEvent are deliberately ignored by the event handler, even though CloseEvents are non-null. (CloseEvents are added to the event queue each time a FoxPro window is closed.) Some problems I had during development of the library were explained when I discovered that CloseEvents appear to have invalid time values in their structure. This was incorrectly setting nonnullLast and causing the Time Out to trigger immediately after I closed a window, either with the mouse, or via a program. (This bug has been fixed and will not be a problem in the FoxPro 2.5 LCK.) However, the EVENTS library avoids the problem in FoxPro 2.0 by ignoring CloseEvents. You knew it was too good to be true...You may expect that once SetSched() and SetIdle() are installed and running, nothing will prevent your routines from being called by the time out processes. Unfortunately, there are limitations in when event handler generated keypresses are visible to FoxPro. For example, if you have a READ that includes a popup GET object, and the popup has been expanded by the user's edit focus, then neither ON KEY LABEL traps or menu shortcut keys will work. But I don't feel too bad about that, because a READ TIMEOUT clause won't take effect in that situation either. In the following table, I have tried to show the circumstances under which FoxPro does not seem to "see" the event handler pressing the key. I've compared the two methods of triggering routines from key presses ( both System menu Shortcut keys, and ON KEY LABEL traps) on both FoxPro platforms, and also compared this with what happens when you press the key interactively.
OK = Fox responds to keypress. nr = No response to keypress Note that FoxPro/Windows behaves significantly differently from the DOS version. This is mainly noticeable when the system menu is active - the Windows version can not see ON KEY LABEL traps or menu shortcut keys! The DOS product will, however, see menu short cut keys. Under FoxPro/DOS, the recommended method of attaching code to key press events is system menu shortcut hot keys. Under FoxPro/Windows, the choice is not so clear. The behaviour of the Windows product may change over time - an update to FoxPro/Win 2.5 is expected to be released soon. Another difference between the two platforms that is not so obvious stems from the fact that Windows is a multi-tasking environment. As a result, timed null events don't happen as regularly in FoxPro/Win as they do in Foxpro/DOS. Indeed, under some circumstances, I'm informed that they don't seem to occur at all. The process that events.c uses depends on timed null events, so performance of the library in FoxPro/Win may vary. It could be said that the Windows version does not have the same need of a library such as EVENTS, as Windows is well-equipped with screen-savers and similar utilities that take the place of TSR's in the DOS environment. Nevertheless, I hope I've shown that this library could still be used to provide many useful features to your applications. Both the source code (EVENTS.C) and the compiled libraries are included on the FoxTalk floppy disk, along with the example programs showing how the library can be used. This code has been compiled into three files: EVENTS20.PLB for FoxPro 2.0; EVENTS25.PLB for FoxPro 2.5 DOS; and EVENTS25.FLL for FoxPro/Windows. The Last EventLastly, credit must go to two people who gave me assistance (over and above that required by professional association and sibling loyalty). My good friend Matt Peirse, the ultimate wall for bouncing ideas off; and my brother Walter Nicholls, who helped me out when the C code threatened to get the better of me... You can get the libraries and source code discussed in this article here in events.zip. |