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.
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.
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:
We want to store all email addresses in an address book. Whenever a
document has been loaded we'll look at all links and store mailto
links in a tied hash.
When the domain part of the URL contains the word 'Java' we don't want to see the page. Instead we'll load http://www.perlmonth.com instead.
Our program should terminate normally when the browser is closed.
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.
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
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:
The message loop should not be polling but should actually wait until it
gets send a 'Quit' event, e.g. via
Win32::OLE->QuitMessageLoop.
There should be better heuristics support to automatically deduct the name of the event source interface.
Some objects don't seem to fire events at all under Win32::OLE. I get the feeling that they need to be embedded in a full blown OLE control container to work correctly. I have no idea yet how difficult it will be to support this.