Working with Events
f90VB’s Automation library provides a set of functions and subroutines that allow you to handle events [41] fired by automation objects you are controlling. To handle events, you must follow a set of simple steps:
Create an Event Sink Object.
Connect the Event Sink Object to the ActiveX object’s Events Interface.
Register the Fortran subroutines that will handle the events with the Event Sink Object.
After you have completed these steps, each time the ActiveX object fires an event, the Fortran subroutine that you have registered to handle the event will be executed. Before your Fortran program terminates, you must undo the previous steps in the same order (i.e. un-register the Fortran subroutines that handle the events, then disconnect and destroy the Event Sink Object).
If this seems simple enough, there is one catch to it. Most Fortran programs have linear execution. This means, they are loaded in memory, execute a set of instructions, and then they are unloaded from memory (Figure 6.10).
Programming with events, on the other hand, involves a shift from the linear-programming paradigm. In this case, your program is loaded in memory, and stays loaded responding to events that implement the program functionality until a terminate event is fired. At this point the program terminates and unloads itself from memory. If you have done any Windows programming in Fortran (or any other language for that matter), you are familiar with the event-driven paradigm [42], because this is exactly what a Window does (Figure 6.11). An application Window is nothing more than a loop (the message loop) that retrieves messages and acts accordingly.
A mouse click on the window area, keys pressed on the keyboard, selecting an option from the window’s menu, these are all events that generate messages that are posted by the operating system to the message queue of the application window. Once the application has received a message, it can select to process the message, to dispatch the message to the associated Window class for processing or both. To get a message sent to it, an application calls function GetMessage, which is part of the core functions provided by the operating system (in win32.dll). To dispatch a message to the associated Window class the application calls function DispatchMessage, which is also a core function of the operating system. This reading and dispatching of messages is implemented using what is called an application message loop. A typical application message loop has the following form:
do while( GetMessage (msg, 0, 0, 0) )
iRet = TranslateMessage( msg )
iRet = DispatchMessage( msg )
enddo
The syntax for the GetMessage function is defined as:
logical GetMessage( lpMsg, HWND, wMsgFilterMin, wMsgFilterMax )
In most cases, only the first parameter is actually used to return the message itself. The remaining three parameters are usually passed as NULL or zero.
Conventionally, loop statements monopolize the system until terminated, thus preventing other operations for the duration of the loop. The GetMessage function, however, has the ability to pre-empt the loop operation to yield control to other applications when no messages are available for the current application, or when WM_PAINT or WM_TIMER messages directed to other tasks are available. Thus, it can give other applications their share of CPU time to execute.
For the present, when the application receives an event message (other than WM_QUIT), the message value is passed. First, it goes to the Windows TranslateMessage function for any keystroke translation that may be specific to the application. Then it is passed to the DispatchMessage handler, where the message information is passed to the next appropriate message-handling procedure (back to Windows, either for immediate handling or, indirectly, for forwarding to the exported WndProc procedure of the associated window class that handles the events of the application.
The intention of this section is not to explain Windows programming, so we’ll limit the description of the message loop and how Windows messages are processed [43]. However, programs that handle ActiveX events work in a very similar way to message-driven programs. The main difference is that the mechanics of ActiveX events are more complicated. Sending messages to the main application when events occur is a relatively limited method of communication. The mechanism used by ActiveX was designed to allow effective two-way interactions between Automation servers and their controllers, even when they reside in different computers connected through a network.
In a very simplistic way, the mechanics of ActiveX events is similar to using callback functions. An ActiveX object defines the events it can fire in the form of callback functions, which must be implemented by the controller application. When the controller instantiates the ActiveX object, it tells the object where to find these callback functions. Then, when the ActiveX object fires an event, it effectively calls the callback function implemented by the controller. In practice, however, you will find that the process is much more complicated. Automation servers and their clients do not always reside in the same computer, and the security scheme of Windows assigns processes different and independent levels of access to system resources. Under these circumstances, simple callbacks are out of the question. Indeed the OLE/COM standard implements event handling through special objects called sinks.
In addition to their normal incoming interfaces, COM objects can also expose outgoing interfaces to their clients. Incoming interfaces are implemented on a COM object and receive calls from external clients of the object. On the other hand, outgoing interfaces are implemented on the client's sink object and receive calls from the COM object. The COM object defines the interface it would like to use, but the client provides the implementation of the interface in the form of a sink object that the client creates. The COM object then calls methods of the outgoing interface on the sink object to notify the client of changes, to trigger events in the client, to request something from the client, or, in fact, for any purpose the COM object creator comes up with. Because sink objects created by the client are COM objects themselves, they can be marshaled by COM and will work through network environments.
COM objects that expose outgoing interfaces are usually called connectable objects. COM defines a standard mechanism for connectable objects to expose their outgoing interfaces, and for clients to query the connectable object and find what outgoing interfaces the connectable object exposes. The centerpiece of this standard mechanism is an interface implemented by the connectable object and called IConnectionPointContainer. In general terms the process takes place as follows (see also Figure 6.12):
The client queries for IConnectionPointContainer on the COM object to determine if the object is connectable. If this call is successful, the client holds a pointer to the IConnectionPointContainer interface on the connectable object, and the connectable object reference counter is incremented. Otherwise, the object is not connectable and does not support outgoing interfaces.
If the object is connectable, the client next tries to obtain a pointer to the IConnectionPoint interface on a connection point within the connectable object. There are two methods for obtaining this pointer, both in IConnectionPointContainer; FindConnectionPoint and EnumConnectionPoints. If successful, the connectable object and the client both support the same outgoing interface. The connectable object defines it and calls it while the client implements it. The client can then communicate through the connection point within the connectable object.
The client then calls IConnectionPoint::Advise on the connection point to establish a connection between its sink object interface and the COM object's connection point. After this call, the COM object's connection point holds a pointer to the outgoing interface on the sink object.
The code inside IConnectionPoint::Advise calls QueryInterface on the interface pointer that is passed in, asking for the specific interface identifier to which it connects.
The COM object calls methods on the sink's object interface as needed using the pointer held by its connection point.
When the client is done handling the events triggered by the server, it calls IConnectionPoint::Unadvise to terminate the connection. Then the client calls IConnectionPoint::Release to free its hold on the connection point and, thus, the main connectable object. The client must also call IConnectionPointContainer::Release to free its hold on the main connectable object.
As you see, the process is complicated, and involves the client creating a sink object, which as you are aware by now is not the kind of thing that can be easily done in Fortran. Luckily, f90VB’s Automation library provides the necessary support to make this an extremely easy task. These f90VB capabilities are better explained through an example.
Example 6.5
In this example, you are going to build an automation controller that keeps a log of all the pages you visit while surfing the net with Internet Explorer. The controller works this way; it first checks if there is an instance of Internet Explorer running, in which case it requests a reference to the IE application object. If there isn’t an instance of IE running, it creates a new one. The controller then requests the default event interface of IE and registers a subroutine (OnNavigationComplete) that handles the IE event NavigationComplete2. Fortran subroutine OnNavigationComplete only prints the name of the downloaded URL into the main application window but it can be easily modified to store the output into a log file. The program keeps logging the navigated pages until the user quits Internet Explorer. This is detected through the event OnQuit, which is fired by IE explorer when the user closes the application. Below is the program in its version for Compaq Visual Fortran [44].
program Example65
!Demonstrates event handling using
!the EventSink facility in f90VBAutomation
!Copyright (C) 1999-2000, Canaima Software, Inc.
!All rights reserved
use f90VBDefs
use f90VBVariants
use f90VBAutomation
use EventHandler
use dfwinty, only: T_MSG
use user32, only: GetMessage, DispatchMessage
implicit none
!Variants containing main objects
type(VARIANT)::IE
!Variants used to stored temporal objects and collections
type(VARIANT)::VarTmp,URL
!Event sink handle
integer(HRESULT_KIND)::iRet
type(VARIANT)::IsVisible
type(T_MSG)::mesg
!Initialize Ole
iRet = OleInitialize()
!Get an instance of Internet Explorer
IE = GetActiveOleObject('InternetExplorer.Application', iRet)
if (iRet.ne.S_OK) then
!no instances of IE are running, create one
IE = CreateOleObject('InternetExplorer.Application', iRet)
if (iRet.ne.S_OK) then
print *,'You need to have Internet Explorer installed'
print *,'for this program to work'
goto 1000
endif
!Makes the IE object visible
call PropertyPut(IE,'Visible',VariantCreate(VT_BOOL,.true.))
endif
!Create an f90VBAutomation Event Sink
EventSinkHndl = EventSinkCreate(iRet)
if (iRet.ne.S_OK) then
print *,'There was an error creating the Event Sink'
goto 1000
endif
!Connects the Event Sink to the default IE events interface
call EventSinkConnect(EventSinkHndl, IE, NullGUID(),iRet)
if (iRet.ne.S_OK) then
print *,'Cannot connect to IE events interface'
goto 1000
endif
print *,'Log started...'
!Register Fortran functions for the events we want to handle
call EventSinkRegEvnt(EventSinkHndl, 'OnQuit', loc(OnQuit), iRet)
call EventSinkRegEvnt(EventSinkHndl, 'NavigateComplete2', &
loc(OnNavigationComplete), iRet)
!This loop keeps the program running until IE quits
do while( GetMessage (mesg, 0, 0, 0) )
if (IEUnloaded) exit
iret = DispatchMessage( mesg )
enddo
1000 continue
!the next three statements are not really necessary as IE has
!already quit when we get here. We have no way to stop it
!from doing this
call EventSinkUnregEvnt(EventSinkHndl, 'OnQuit', iRet)
call EventSinkUnregEvnt(EventSinkHndl, 'NavigateComplete2', iRet)
call EventSinkDisconnect(EventSinkHndl)
!destroys the Event Sink object and releases its memory
call EventSinkDestroy(EventSinkHndl)
!Release the instance to IE
call Release(IE)
!Clean up variants
call VariantClear(URL)
call VariantClear(VarTmp)
!Uninitialize Ole
call OLEUninitialize()
stop
end
We put the two subroutines that handle the events into a Fortran module called EventHandler. Here is the code for the module:
module EventHandler
use f90VBDefs
use f90VBBstrings
use f90VBVariants
use f90VBAutomation
implicit none
logical::IEUnloaded = .false.
integer(POINTER_KIND)::EventSinkHndl
contains
function OnNavigationComplete(DispID, nArgs, VarArgList, &
nNamedArgs, DispIDList)
!This function is fired by the event
!NavigationComplete2 of Internet Explorer
!We don't do much here, just show a message on the main application
!console indicating that the event was fired and the URL downloaded
integer(HRESULT_KIND)::OnNavigationComplete
integer(LONG_KIND),intent(in)::DispID
integer(LONG_KIND),intent(in)::nArgs
type(VARIANT),intent(inout)::VarArgList(nArgs)
integer(LONG_KIND),intent(in)::nNamedArgs
integer(LONG_KIND),intent(in)::DispIDList(nNamedArgs)
character(len=1024)::TmpStr
integer(HRESULT_KIND)::iRet
integer(BSTRHNDL_KIND)::BStrHndl
!get the URL of the downloaded page
BstrHndl= VariantToBString(VarArgList(1))
!copy the BStr into a Fortran string for printing
call StrCopy(BstrHndl,TmpStr,iRet)
if (iRet.ge.0) then
print *,'Page downloaded:<',trim(TmpStr),'>'
else
print *,'Page downloaded:<cannot print URL>'
endif
!releases the BStr
call StrFree(BStrHndl)
OnNavigationComplete = S_OK
end function OnNavigationComplete
function OnQuit(DispID, nArgs, VarArgList, nNamedArgs, DispIDList)
!This function is fired by the event OnQuit of Internet Explorer
!We don't do much here, just show a message on the main application
!console indicating that the event was fired
integer(HRESULT_KIND)::OnQuit
integer(LONG_KIND),intent(in)::DispID
integer(LONG_KIND),intent(in)::nArgs
type(VARIANT),intent(inout)::VarArgList(nArgs)
integer(LONG_KIND),intent(in)::nNamedArgs
integer(LONG_KIND),intent(in)::DispIDList(nNamedArgs)
integer(HRESULT_KIND)::iRet
print *,'Quit Internet Explorer'
IEUnloaded = .true.
OnQuit = S_OK
end function OnQuit
end module
Let’s now take a detailed look at the code in this example. The program starts by initializing OLE and creating a new instance of IE if one doesn’t exist. If the program creates a new instance, then it must set its Visible property to True (traditionally new instances of automation servers are not visible). We have seen this last technique before, in Example 6.3:
iRet = OleInitialize()
IE = GetActiveOleObject('InternetExplorer.Application', iRet)
if (iRet.ne.S_OK) then
IE = CreateOleObject('InternetExplorer.Application', iRet)
if (iRet.ne.S_OK) then
print *,'You need to have Internet Explorer installed'
print *,'for this program to work'
goto 1000
endif
call PropertyPut(IE,'Visible',VariantCreate(VT_BOOL,.true.))
endif
Next we create an event sink object through a call to f90VB subroutine EventSinkCreate:
EventSinkHndl = EventSinkCreate(iRet)
if (iRet.ne.S_OK) then
print *,'There was an error creating the Event Sink'
goto 1000
endif
In OLE/COM terminology, an event sink is an object capable of responding to events fired by an ActiveX object (the event source). In languages that understand OLE/COM, like Visual Basic, event sinks are created at compile time and usually the compiler takes care of the details for you. Function EventSinkCreate creates a generic event sink capable of responding to events fired by any ActiveX object. The function returns a handle (i.e. a pointer) that is later used to identify the instance of the event sink object you have just created.
Once you have an event sink, the next step is to connect the object to the automation server:
call EventSinkConnect(EventSinkHndl, IE, NullGUID(),iRet)
if (iRet.ne.S_OK) then
print *,'Cannot connect to IE events interface'
goto 1000
You do this by calling EventSinkConnect. Note that EventSinkConnect has four parameters; the first is the handle of an event sink object (EventSinkHndl in this example), the second is a Variant variable that contains a reference to the automation object (IE) that provides the event source, the third argument is the Interface Identifier (IID) of the automation object’s interface that exposes the events. As we saw before, objects expose their functionality through interfaces, and this also applies to events. Event interfaces are usually called outgoing interfaces, because their member functions are not implemented by the automation server, but must be implemented by the automation controller. In this sense, outgoing interfaces are like a template describing the methods an automation controller must implement in order to support communication and messages sent from the automation server. Automation objects can expose more than one set of events through different interfaces. EventSinkConnect gives you the option of connecting an event sink to any of these interfaces by allowing you to pass the Interface Identifier (remember that IIDs are GUID structures) of the outgoing interface you want to use. If you call EventSinkConnect with a null IID (this is what f90VB function NullGUID returns) then the subroutine just connects the event sink to the first outgoing interface exposed by the object, which is normally the default event interface [45].
The forth argument in EventSinkConnect is used to return an error flag if the function cannot establish a connection to the requested event interface (for example, if the object does not expose events).
At any given time, a single event sink object may be connected to only one outgoing interface. If you need to simultaneously handle the events fired by more than one object (or more than one outgoing interface of the same object), you must create an event sink object for each event source object (or outgoing interface). You can, however, reuse sink objects (i.e. connect the sink object to a different event interface) after you have previously disconnected them from the Automation object.
Once you have the event sink connected to the event interface of the object your Fortran application is controlling, the next step is to register the Fortran functions that will handle specific events fired by the ActiveX server. We do this by calling EventSinkRegEvnt:
call EventSinkRegEvnt(EventSinkHndl, 'OnQuit', loc(OnQuit), iRet)
call EventSinkRegEvnt(EventSinkHndl, 'NavigateComplete2', &
loc(OnNavigationComplete), iRet)
Again, the first argument of EventSinkRegEvnt is the handle of the event sink. The second argument is a character string with the name of the event for which you want to register a Fortran function handler. The third argument is the address of the Fortran function that will handle the event, and the forth argument is the iRet error flag indicator. When you call EventSinkRegEvnt, your are telling the event sink object each time the ActiveX object you are connected to fires this event, execute the Fortran function located at this memory address.
The two calls to EventSinkRegEvnt in Example 6.5 register two Fortran functions (OnNavigationComplete and OnQuit) as the functions to be called when events NavigateComplete2 and OnQuit are fired. Internet Explorer fires event NavigateComplete2 after an URL has been loaded, while event OnQuit is fired when IE quits.
If you take a look at the Fortran functions OnNavigationComplete and OnQuit in module EventHandler, you will notice that they receive exactly the same arguments:
DispID: This is the Dispatch ID of the fired event that resulted in the calling of the Fortran function.
nArgs: This is the number of arguments passed by the event to the Fortran function. Each event in an outgoing interface may pass a different number of arguments.
VarArgList: This is a vector containing the arguments of the event as Variant values. The size of this vector is always nArgs.
nNamedArgs: This is the number of named arguments for the event.
DispIDList: If the event uses named arguments, then this vector contains the DispID of the named arguments [46].
Note also that the function return value is a long integer (i.e. of type HRESULT_KIND).
All Fortran functions that are registered to handle events must have this exact same declaration [47]. To access the parameters provided by the event, you use VarArgList, as demonstrated in function OnNavigationComplete:
function OnNavigationComplete(DispID, nArgs, VarArgList, &
nNamedArgs, DispIDList)
integer(HRESULT_KIND)::OnNavigationComplete
integer(LONG_KIND),intent(in)::DispID
integer(LONG_KIND),intent(in)::nArgs
type(VARIANT),intent(inout)::VarArgList(nArgs)
integer(LONG_KIND),intent(in)::nNamedArgs
integer(LONG_KIND),intent(in)::DispIDList(nNamedArgs)
character(len=1024)::TmpStr
integer(HRESULT_KIND)::iRet
integer(BSTRHNDL_KIND)::BStrHndl
BstrHndl= VariantToBString(VarArgList(1))
call StrCopy(BstrHndl,TmpStr,iRet)
if (iRet.ge.0) then
print *,'Page downloaded:<',trim(TmpStr),'>'
else
print *,'Page downloaded:<cannot print URL>'
endif
call StrFree(BStrHndl)
OnNavigationComplete = S_OK
end function OnNavigationComplete
If you check the documentation of IE you will notice that event NavigateComplete2 is defined as follows:
Sub NavigateComplete2(ByVal pDisp As Object, URL As Variant)
The first parameter is a pointer to the IDispatch interface of the IE instance that fired the event. The second parameter is the URL to which the browser has navigated. We are interested in this last parameter, because we can use it to keep a log of pages visited by IE. Note however that in Fortran function OnNavigationComplete the URL is accessed as the first entry in vector VarArgList:
BstrHndl= VariantToBString(VarArgList(1))
This is because internally ActiveX arguments are always passed in inverse order [48]. So for example, to access pDisp you would use VarArgList(2).
Because all Fortran functions registered to handle events use exactly the same definition, rather than having individual functions to handle each event (as we did in Example 6.5), you could also use a single Fortran function to handle all the events. This technique is explained later in section Centralizing event handling code.
Going back to the main program in Example 6.5, at this point, the program is ready to start processing events. In fact, a Fortran event-handling function will be called in answer to events fired by the ActiveX object immediately after the function is registered for an event.
The next thing the main program in Example 6.5 does is to enter into a Windows message loop:
do while( GetMessage (mesg, 0, 0, 0) )
if (IEUnloaded) exit
iret = DispatchMessage( mesg )
enddo
The loop exits when the value of variable IEUnloaded is set to True or when function GetMessage returns False.
IEUnloaded is changed by function OnQuit, which, as explained earlier, is fired just before IE quits. You then might be rightfully asking why we are using a message loop, rather than a loop like the one below:
do while(.not. IEUnloaded)
enddo
Our suggestion is that you try it. If you edit Example 6.5 and replace the message loop by the loop above, the program will compile and seem to work fine, except that neither OnNavigationComplete nor OnQuit will ever be called [49]. Because OnNavigationComplete is never called, you won’t see the log of URLs loaded by the browser, and because OnQuit is also never called, IEUnloaded value is never set to True. In other words, the program will never terminate. Earlier in this chapter, we hinted about this kind of behavior. Loop statements tend to monopolize the system until terminated, thus preventing other operations in the same thread for the duration of the loop. Because the Fortran functions that handle the events and the main application use the same thread of execution, the loop just wouldn’t give the event-handling functions a chance to execute. We get around this problem easily by using a message loop, because when no messages are available for the current application, function GetMessage pre-empts the loop operation and yields control, which allows the event functions to be executed. We do have to agree that a loop message looks awfully weird is a console-based application, but the examples presented here are all console-based applications because we want to keep them as simple as possible, so the important points about f90VB don’t get confused with the complexities related to creating Windows applications in Fortran. Note that the message loop in Example 6.5 doesn’t do much, it just retrieves messages and dispatches them immediately.
As we explained before, the OnQuit event sets variable IEUnloaded to True, in which case the application exits the message loop. The code executed next is straight forward; first the Fortran functions associated with the event sink are unregistered:
call EventSinkUnregEvnt(EventSinkHndl, 'OnQuit', iRet)
call EventSinkUnregEvnt(EventSinkHndl, 'NavigateComplete2', iRet)
Then the event sink is disconnected:
call EventSinkDisconnect(EventSinkHndl)
Un-registering and disconnecting the sink object are standard procedures that must be executed before terminating any application that uses events. In this particular case, by the time these procedures are executed, Internet Explorer is no longer running, so you could have just destroyed the sink object directly. We included the instructions mainly for illustration purposes.
Finally, the event sink created by the application needs to be destroyed, so its memory is released to the operating system:
call EventSinkDestroy(EventSinkHndl)
The rest of the code in Example 6.5 is standard and has been explained before; we call subroutine Release to release the instance of Internet Explorer in variant variable IE, clean up used variants and BStrings, and un-initialize OLE with a call to subroutine OleUninitialize:
call Release(IE)
call VariantClear(URL)
call VariantClear(VarTmp)
call OLEUninitialize()
Centralizing event handling code
As explained earlier in this chapter, all Fortran subroutines used to handle Automation events have exactly the same return value and arguments definition. This allows you to centralize all the event-related processing into a single function. To illustrate the way this works, let’s create a centralized Fortran event handler function, called GenEventHndlr, in Example 6.5:
function GenEventHndlr(DispID, nArgs, VarArgList, nNamedArgs, DispIDList)
integer(HRESULT_KIND):: GenEventHndlr
integer(LONG_KIND),intent(in)::DispID
integer(LONG_KIND),intent(in)::nArgs
type(VARIANT),intent(inout)::VarArgList(nArgs)
integer(LONG_KIND),intent(in)::nNamedArgs
integer(LONG_KIND),intent(in)::DispIDList(nNamedArgs)
integer(HRESULT_KIND)::iRet
character(len=1024)::TmpStr
integer(BSTRHNDL_KIND)::BStrHndl
select case(DispID)
case(253) !OnQuit event
print *,'Quit Internet Explorer'
IEUnloaded = .true.
case(252) !NavigateComplete2 event
BstrHndl= VariantToBString(VarArgList(1))
call StrCopy(BstrHndl,TmpStr,iRet)
if (iRet.ge.0) then
print *,'Page downloaded:<',trim(TmpStr),'>'
else
print *,'Page downloaded:<cannot print URL>'
endif
call StrFree(BStrHndl)
end select
OnQuit = S_OK
end function GenEventHndlr
Note that function GenEventHndlr contains a select-case statement used to discriminate the passed DispID. You can do this because each event fired by an outgoing interface is identified by a unique Dispatch Identification (DispID) number. The DispID for event OnQuit is 253, and event NavigateComplete2 has a DispID of 252. How can you get the DispID of the events exposed by an Automation object? You can use f90VB subroutine EventSinkEnumEvnts [50] that returns a list of events supported by a connected sink object.
You also need to register function GenEventHndlr as the handler for all the events that the function can handle. GenEventHndlr only handles two events, NavigateComplete2 and OnQuit, so you register the same function for both events:
call EventSinkRegEvnt(EventSinkHndl, 'OnQuit', loc(GenEventHndlr), iRet)
call EventSinkRegEvnt(EventSinkHndl, 'NavigateComplete2', &
loc(GenEventHndlr), iRet)
In the code above, it is important to note that you are passing the same Fortran function (GenEventHndlr) as the handler for both events.
The rest of the code in Example 6.5 stays the same.
Centralizing event handling is a unique feature of f90VB that has many advantages. Because all the code that handles events resides in the same function, it is usually easier to debug. Also, upgrading your application to operate on new events becomes easier, because it does not involve creating new functions.