Using OLE Events with Win32::OLE

By: Jan Dubois


Introduction

In OLE Automation most transactions are initiated by the client: it queries or changes a property or calls a method on the object. In contrast, OLE Events are initiated by the object to inform the client about a status change. That means that the event interface must actually be implemented by the controlling client and not the object.

The Perl implementation of OLE events allows you to provide callback functions which will be executed whenever the object fires an event. The Perl interpreter itself is not reentrant, so this requires a bit of synchronization. This is accomplished by running the Perl program in a single threaded apartment. You can ask the Win32::OLE module to provide this environment by calling:

    use Win32::OLE;
    Win32::OLE->Initialize(Win32::OLE::COINIT_OLEINITIALIZE);

I've added the EVENTS pseudotarget as a more mnemonic shortcut

    use Win32::OLE qw(EVENTS);

which will do the same thing. Remember that this creates a hidden top level window and a message queue for the Perl thread. If the messagequeue is not processed regularly then other programs trying to initiate a DDE conversation may hang. This applies to e.g. InstallShield setup programs and many Shell Associations.


Exploring the Explorer

Let's start with a simple example: I want to find out what kind of events are fired by Microsofts Internet Explorer. I start with standard boilerplate:

    use strict;
    use Win32::OLE qw(EVENTS);
    use Win32::OLE::Variant;
    $|=1;

I'm including Win32::OLE::Variant to get the automatic overloading conversation of Variant objects to strings or numbers. I also set STDOUT to autoflush to see immediately when something happens. Now we can create an Explorer object and make it visible:

    my $ie = Win32::OLE->new('InternetExplorer.Application');
    $ie->{Visible} = 1;

The following statement connects the Event() function to the $ie object. It tells Win32::OLE to attach to a connection point that fires events of the 'DWebBrowserEvents2' interface.

    Win32::OLE->WithEvents($ie, \&Event, 'DWebBrowserEvents2');

You might be wondering why you have to tell Perl (or the Win32::OLE module to be exact) the name of the event interface. COM defines the IProvideClassInfo2 interfaces, which would tell us exactly the name id of the default source of our object. Unfortunately Internet Explorer doesn't implement it (like most other objects don't).

So how does Visual Basic do it then? VB has the advantage of knowing the class (COCLASS) of the object. It can look up the default source for this class in the type library. We can use Win32::OLE->QueryObjectType(). But this method only provides the name of the interface that we are using; it doesn't tell us which COCLASS our object belongs too. I might implement more heuristics to guess at the source interface name, but for the moment we have to specify it explicitly if the object doesn't support IProvideClassInfo2. You'll need to do some spelunking in the type libraries with an OLE viewer. Use the Oleview.exe program included with VC++ or download it from:

    http://www.microsoft.com/com/resources/oleview.asp

After connecting our event callback we now have to wait for some event to happen. We also have to crank the messageloop pump. This is not only necessary to let other applications start DDE conversation but also to get our own events dispatched. In a single threaded apartment COM uses the messageloop to synchronize the object with the client.

    while (1) {
        Win32::OLE->SpinMessageLoop;
        Win32::Sleep(100);
    }

Win32::OLE->SpinMessageLoop() dispatches all pending message to the corresponding windows. We don't want to waste too much CPU time just idle looping so I put a little 100 millisecond sleep statement in there. Next comes our Event() function:

    sub Event {
        my ($obj,$event,@args) = @_;
        print "Event: $event\n";
        for my $arg (@args) {
            my $value = $arg;
            $value = sprintf "[%s %s]", 
		     Win32::OLE->QueryObjectType($value)
                if UNIVERSAL::isa($value, 'Win32::OLE');
            print "  arg: $value\n";
        }
    }

It is called whenever the Explorer fires an event. The first two arguments are the object that fires the event (in this case always $ie) and the name of the event. The remaining arguments are defined by the application and are typically dependent on the specific event type. You can find information about the methods, properties and events of the MS WebBrowser object at the following site:

    http://msdn.microsoft.com/workshop/browser/webbrowser/webbrowser.asp

This function is just for testing which events are actually fired, and what their arguments might contain. So let's just run the program and see what happens. It just brings up an Internet Explorer Window and then sits idle. I manually type ``www.perlmonth.com'' into Exploder and press ENTER. I get 137 lines of output from my Perl program. I'll show you only an abbreviated listing (with commentary) here:

    Event: OnVisible
      arg: 1
    Event: BeforeNavigate2
      arg: [SHDocVw IWebBrowser2]
      arg: http://www.perlmonth.com/
      arg: 0
      arg: Win32::OLE(0.1008) error 0x80070057: 
"Der Parameter stimmt nicht" at ie.pl line 22
    Use of uninitialized value at ie.pl line 22.
      arg: 
      arg: 
      arg: 0

The BeforeNavigate2 event is fired before the specified URL is actually loaded. I'll give a more detailed rundown on the parameters later. It is interesting to see the error message (part of it is in German because I'm running an German Version of NT 4.0 Workstation here). I was a bit baffled why I got the ``Invalid parameter'' error at all. I found that InterNet Explorer actually sends out data in an invalid format (as per Microsoft documentation). I'll add a workaround for that to the next version of Win32::OLE.

    Event: DownloadBegin
    Event: StatusTextChange
      arg: Suchen der Site: www.perlmonth.com
    Event: ProgressChange
      arg: 100
      arg: 10000
    Event: ProgressChange
      arg: 150
      arg: 10000
    Event: StatusTextChange
      arg: Verbindung mit Site 206.112.62.102 herstellen
    Event: StatusTextChange
      arg: Website gefunden. Warten auf Antwort...
    Event: StatusTextChange
      arg: Umleiten zur Site:
http://www.perlmonth.com/index.html?issue=2&id=930604396
    Event: StatusTextChange
      arg: Download beginnen von Site:
http://www.perlmonth.com/index.html?issue=2&id=930604396
    Event: DownloadComplete

We get plenty of messages detailing the progress of the ongoing transaction. The ProgressChange message gives values for the progress bar: 100/10000 means 1% complete etc. The status text shows us that the page actually redirects to another one.

    Event: StatusTextChange
      arg: 
    Event: TitleChange
      arg: http://www.perlmonth.com/index.html?issue=2&id=930604396
    Event: NavigateComplete2
      arg: [SHDocVw IWebBrowser2]
      arg: http://www.perlmonth.com/index.html?issue=2&id=930604396
    Event: ProgressChange
      arg: -1
      arg: 10000
    Event: DownloadComplete
    Event: TitleChange
      arg: Perlmonth, Just use Perl;
    Event: DocumentComplete
      arg: [SHDocVw IWebBrowser2]
      arg: http://www.perlmonth.com/index.html?issue=2&id=930604396

The status line is cleared, the window title is set to the URL and we are told that the navigation is complete. Internet Explorer now interprets the HTML, clears the progress bar and sets the window title to the document TITLE. The DocumentComplete events tells us that it is now safe to process the downloaded document. We'll do that in our next exercise. Let's stop Internet Explorer now.

    Event: OnQuit

The Explorer Window is gone, but our Perl program patiently goes on processing it's message loop and sleeping. We have to kill it, e.g. with Ctrl-C.


A Browsing Companion

We now know the types of events we are getting from IE. Let's actually do something when we get them. Here are the changes to the previous program:

Here is the new program:

    use strict;
    use Fcntl;
    use SDBM_File;
    use Win32::OLE qw(EVENTS);

    my $Quit;
    tie my %adb, 'SDBM_File', 'adb', O_RDWR|O_CREAT, 0744;

    my $ie = Win32::OLE->new('InternetExplorer.Application');
    Win32::OLE->WithEvents($ie, 'BrowserEvents', 'DWebBrowserEvents2');
    $ie->{Visible} = 1;

    until ($Quit) {
        Win32::OLE->SpinMessageLoop;
        Win32::Sleep(100);
    }
    untie %adb;

The $Quit flag will be set by the OnQuit event when the browser closes. This will terminate our message loop and terminate the Perl program as well. %adb is our address book. It is implemented as a tied hash. This way it will remember all old addresses from session to session.

I used a different way to specify our event callback. Instead of the reference to a function I used a normal string. This is interpreted as the name of a package. Whenever the $ie object wants to fire an event Win32::OLE will now look into the ``BrowserEvents'' namespace if a function with this name exists. When it exists, then this function will be called with $ie as the first parameter followed by the event specific parameters. If the function is not found then the event is ignored.

    package BrowserEvents;
    use Win32::OLE qw(in);
    use Win32::OLE::Variant;

I'll shortly need the Win32::OLE::in() function, so I'll import the name into the ``BrowserEvents'' namespace. I also want Win32::OLE::Variant objects to be automatically translated to strings and numbers, so I load that package too.

    sub BeforeNavigate2 {
        my $self = shift;
        my ($browser,$url,$flags,$frame,$postdata,$headers,$cancel) = @_;
        if ($url =~ m'http://[^/]*java'i) {
            $cancel->Put(1);
            $browser->Navigate("http://www.perlmonth.com";);
        }
    }

>From the URL given above we know that the BeforeNavigate2 event has the following VB prototype:

    Private Sub object_BeforeNavigate2(
		ByVal pDisp As Object,
    		ByVal URL As String, 
		ByVal Flags As Long, 
		ByVal TargetFrameName As String,
    		PostData As Variant, 
		ByVal Headers As String, 
		Cancel As Boolean
    )

The interesting thing about the Cancel parameter is that it is being passed by reference and not by value. That means we can change the variable using the Win32::OLE::Variant methods and the changed value will be passed back to the object. So whenever the new URL contains the word 'Java' we will just cancel the navigation. For good measure we issue a Navigate method call to actually return to the PerlMonth web site instead.

    sub DocumentComplete {
        my $self = shift;
        Win32::OLE->Option(Warn => 0);
        my $document = $self->Document;
        Win32::OLE->Option(Warn => 1);
        return if Win32::OLE->LastError;

        foreach my $link (in $document->links) {
            next unless $link->HREF =~ /^mailto:(.*)/;
            $adb{$link->innerText} = $1;
        }
    }

The ``redirection'' in BeforeNavigate2 above is the reason that the browser doesn't always have a valid Document property in the DocumentComplete event. That's why I disable the warnings when trying to access the Document and return when I got an error.

We then cycle through all links in the current documents, ignoring all non-mailto links. Every mailto link is now stored in our address book, with the link text used as the key.

    sub OnQuit { $Quit = 1 }

The OnQuit sets the flag that our message loop above is allowed to take a rest.


Checking our Address Book

Now that we have collected all these email addresses, we also need a way to utilize this address collection. Here is a short program that either dumps the complete address book or lets you do a regexp search through it:

    use strict;
    use Fcntl;
    use SDBM_File;

    tie my %adb, 'SDBM_File', 'adb', O_RDONLY, 0744;
    if (@ARGV) {
        foreach my $arg (@ARGV) {
            my @names = grep /$arg/i, sort keys %adb;
            printf "%-30s %s\n", $_, $adb{$_} foreach @names;
        }
    }
    else {
        printf "%-30s %s\n", $_, $adb{$_} foreach sort keys %adb;
    }
    untie %adb;

After browsing a few pages on www.perlmonth.com I get the following list from ``perl -w adblist.pl perlmonth''

    baiju@perlmonth.com        baiju@perlmonth.com
?subject=PerlMonth
    feedback@perlmonth.com     feedback@perlmonth.com
?subject=PerlMonth Feedback
    info@perlmonth.com         info@perlmonth.com
?subject=PerlMonth Info
    kevin@perlmonth.com        kevin@perlmonth.com
?subject=PerlMonth
    suggestions@perlmonth.com  suggestions@perlmonth.com
?subject=PerlMonth Suggestions


The End

This article was just a brief practical overview about what is already possible with Perl and OLE events. If you have any suggestions for improvements to the functionality or interface, please let me know. I think at least the following areas should be enhanced: