HomePage Delphi Library
Windows API calls are just calls to dynamic link libraries
Back in the early days of Windows, PCs had much less memory available than today.
The first version of Windows required only 256k of memory and two floppy drives...
amazingly little by today's standards. The designers of windows had to fit a lot
of code and resources into that 256k of memory, and still leave room for multiple
programs to run.
This feat was achieved by allowing multiple programs to share commonly used code
and resources. The vehicle used to provide this magic is the dynamic link library.
Functions and resources contained in dynamic link libraries can be shared by
multiple applications. The code and resources contained in a dynamic link
library are generally not loaded until needed, and can be freed when not in
use.
Most of the Windows system is comprised of dynamic link libraries. Usually dynamic
link libraries have the extension of ".DLL" , but this is not a requirement.
An example would be in the heart of Windows itself : GDI.EXE, GDI32.DLL, USER.EXE,
USER32.DLL, KERNEL.EXE, KERNEL32.DLL, printer drivers (.drv), system drivers(.drv),
and some fonts(.fon).
An example of a resource only dynamic link library is contained in the MORICONS.DLL
file that ships with Windows. It contains a variety of extra icons for Windows users
to brighten their desktops with.
Some of the other advantages of placing code and resources in a dynamic link library
are:
Disk space is saved if multiple applications use a dynamic link library. This can
sometimes make a drastic difference in the cost of deploying your applications
since you can place common functions and resources in a single .DLL file, reducing
the size of your executables.
The ability to upgrade and replace parts of the program without shipping a whole
new executable.
The ability to load and execute different resources and code based on some specific
criteria available only at runtime. A good example would be " PlugIn "
applets that can be dynamically loaded by your application at runtime, without
knowing what " PlugIns " might be available at compile time.
This can make your program automatically extendible with after-market products.
Loading a dynamic link library
Your compiler most likely includes interface files to most of the Windows system
functions. You simply link to the interface files, and any dynamic link libraries
that are used by your program are automatically loaded when your program is started.
In 16 Bit Windows, the actual function code is not added to your program, but
rather a pointer to the module name of the dynamic link library and the name of
the function (or its exported index number) is added.
In Win32, the DLL is mapped into the process' address space. The dynamic link
library's file must exist at runtime or an error will occur.
If your compiler does not include an interface to the function or dynamic link
library you want to use, you are going to have to declare a function prototype,
and a linking interface, to let your compiler know about the function you are
dynamically linking to. The instructions to do this should be included with your
compiler's documentation.
A short example of a function prototype in Pascal for a dynamic link library
would be:
{$IFDEF Win32}
function MessageBeep(BeepType: Integer):Bool stdcall; external 'USER32';
{$ELSE}
procedure MessageBeep(BeepType: Integer); far; external 'USER';
{$ENDIF}
This prototype tells the compiler to link to a function named " MessageBeep "
located in USER32.DLL (if compiling under 32 bits), or a procedure named " MessageBeep "
located in USER.EXE (if compiling under 16 bits). The compiler is also made aware
that " MessageBeep " takes a integer size parameter, and uses the " stdcall "
passing convention (if compiling under 32 bits).
When declaring an interface to a dynamic link library, you must be careful to
specify the way that the function expects to have its parameters and return value
passed, and who will maintain the stack. There are various calling techniques used
including: PASCAL, CDECL, STDCALL, and FASTCALL .
In 16 bit Windows, most functions are exported using the PASCAL calling convention.
In 32 bit Windows, STDCALL is preferred since this should be compatible w/most
other languages.
Most API functions are declared with " C " prototypes. If you are wanting
to port an interface to a Pascal based language, and you are not familiar with " C ",
your best bet is to get Alain Tadros's technical paper --
"Using Borland's Delphi and C++ Together" .
It is available from Borland's World Wide Web site, Compuserve, and the Borland
BBS system and has some excellent additions by Eric Uber .
Dynamic loading at runtime
Loading a dynamic link library at runtime allows you to load dynamic link libraries
that are not known at compile time, or load a library for only a short time.
Printer drivers are a good example: You don't know what printer drivers will be
available on a users system until runtime, and you would only want to load a printer
driver during the printing phase of your program.
One of the advantages to loading libraries dynamically is that you don't use up
resources (memory) until you actually load the library and the function you wish
to call.
You load a dynamic link library at runtime by calling the Windows API function
LoadLibrary() . Then you can search for a given function within the library
using the Windows API function GetProcAddress() . Once you have the address of
the function you want, you can call it from your code.
One thing to remember: Always call the Windows API function FreeLibrary() when
you are done using the dynamic link library.
Example:
procedure Win31LoadUserAndCallBeepFn;
var
DllHandle: THandle;
BeepFn: procedure(BeepType: Integer);
begin
DllHandle := LoadLibrary('USER');
if DllHandle >= 32 then begin
@BeepFn := GetProcAddress(DllHandle, 'MessageBeep');
if @BeepFn <> nil then
BeepFn(-1);
FreeLibrary(DllHandle);
end;
end;
procedure Win32LoadUserAndCallBeepFn;
var
DllHandle: THandle;
BeepFn: function(BeepType: Integer): Bool stdcall;
begin
DllHandle := LoadLibrary('USER32');
if DllHandle <> 0 then begin
@BeepFn := GetProcAddress(DllHandle, 'MessageBeep');
if @BeepFn <> nil then
BeepFn($FFFFFFFF);
FreeLibrary(DllHandle);
end;
end;
Here we have two code snippets showing the differences between loading a library
under 16 and 32 bit Windows. In this case, we must mark the function we are calling
as using the " stdcall " calling convention under 32 bit Windows, where
under 16 bit windows the " pascal " calling convention is assumed. Note
that the 16 bit version of MessageBeep() is a procedure, and the 32 bit version
is a function.
The LoadLibrary() function takes a null-terminated string that names the library
file to be loaded. If the string does not contain a path, Windows searches for the
library in this order: the current directory; the Windows directory; the Windows
system directory; the directory containing the executable file for the current
task; the directories listed in the PATH environment variable; and finally, the
list of directories mapped in a network. If no extension is used, the extension
of " .DLL " is assumed. We check the value of the handle returned by
LoadLibrary() for an error.
Under 16 bit Windows, the return value will be less than 32 if an error occurs,
and under 32 bit Windows, the return value will be 0 if an error occurs. We then
call the GetProcAddress() function to get the address of the MessageBeep()
function. The spelling of the function name is critical, and can be case sensitive
under different environments. If GetProcAddress() successfully returns the address
of the function, then we can safely call the function. Finally we unload the
library to free up memory.
Resource only dynamic link libraries
As mentioned, dynamic link libraries can hold resources as well. To use resources
contained in a dynamic link library, you simply make a call to LoadLibrary() ,
then you can load resources from the library using one of the Windows API resource
functions such as LoadBitmap() , passing the handle to the dynamic link library
that you received from the LoadLibrary() call. Just don't forget to free your
resources and the library when you are done.
You can use the snipit from the above section " Loading dynamic link libraries
at runtime " to load a dynamic link library, and then load a resource from the
library.
Example:
LoadBitmap(DllHandle,'SOMEBMP');
Stack usage and dynamic link libraries
When you call a function in a dynamic link library, the library generally uses the
stack of the calling application. Applications running under 16 Bit Windows had much
less stack space available (<64k) than what is available under 32 bit Windows
(1 MB). Since functions in a dynamic link library can call functions in other
dynamic link libraries as well as making callbacks, you should always thoroughly
test your program to make sure you have setup adequate stack space.
Callback functions
Callback functions are a really wonderful feature of the Windows environment.
Callback functions allow you to send the " instanced " address of a
function in your application to a function contained in a dynamic link library.
The dynamic link library function can then call back to the function in your
application to pass back information.
A good example of a Windows API function that utilizes callbacks is the EnumFonts()
function. The EnumFonts() function enumerates fonts available to a given device
context. For each font enumerated, the EnumFonts() function will call back to a
function in your application, passing back information about that font. This
process continues as long as their are more fonts to enumerate, or until your
function returns zero, meaning that you wish to stop further enumerations.
Most Windows API functions that accept a callback function as a parameter will
also accept an lpData parameter. This lpData parameter is used for " application
defined data ". You are free to define what this parameter is used for. The
lpData parameter is usually passed to the dynamic link library as a longint, and
rarely as a far pointer. It is likely you will want to send something besides a
longint. You can always typecast a pointer to a data structure in your application
to a longint when calling dynamically linked functions, and typecast the longint
passed back to your callback function to a pointer so your may access your data
structure.
As mentioned, when your callback function is called by the Windows API function,
the lpData parameter you originally passed to the Windows API function will be
passed back to your callback function for your use. This avoids having to declare
a global variable in your program. A good example of how this works would be if
you were to create a program containing a dialog box to show all the available
fonts in a drop-down listbox. When you process the WM_INITDIALOG message, you
call the EnumFonts() function, passing the instanced address of your callback
function that will process the font information.
Your callback function will perform the work to add each font's name to the
listbox. The only problem: your callback function does not have a handle to the
listbox. You could create a global variable to hold the listbox handle, but
instead, you should pass the handle of the listbox to your callback function
through the lpData parameter, and let the dynamically linked function pass the
handle of the listbox back to your callback function.
While it is always nice to keep global variables to a minimum, having the ability
to pass user defined data to the callback function becomes critically important
when you create callback functions inside a 16-bit dynamic link library.
Generally, under 16-bit Windows, a dynamic link library has only one data segment.
Since multiple applications can use a single dynamic link library, and, since a
16-bit dynamic Link Library has a single data segment, any global variables
defined in the dynamic link library exist in memory only once.
If you create a callback function in a dynamic link library, and use a global
variable to store data used during the callback, you run the risk of data
corruption. Consider the following scenario: You have written several applications
that use your font dialog box. Rather than include your font dialog in each
application, you opt to store the dialog in a dynamic link library.
Suppose that you use a global variable in the dynamic link library to store the
listbox handle for your callback function. Now suppose that more than one
application loads the dialog box at the same time. Each "copy" of your
font dialog box contains a different handle to it's listbox. Clearly, using a
global variable to hold the listbox handle will not work since the listbox handle
will be different for each dialog, and the global variable used to hold this
handle will get trashed. The way around this, is to pass the listbox handle to
the EnumFonts() function using the lpData parameter. This is not a problem,
when writing 32-bit dynamic link libraries for use under Windows 95 and
Windows NT , as under both environments, dynamic link libraries have a separate
data area for each application that loads them.
None the less, it is still a good practice to always keep global variables to
a minimum.
The Hook functions
A " hook " function is a callback function that can be inserted in the
Windows message system so an application can access the message stream before
other processing of the message takes place. Often times, there will be other
hooks already installed in the messaging system, so there will be times you will
need to call the next hook in the " Hook Chain " to allow the other
hooks in the chain to access the messages as well.
When you install a hook function using the Windows API function SetWindowsHookEx(),
you will receive a 32 bit handle to your installed hook function. You will use
this handle to both remove the hook when you are done via the Windows API function
UnHookWindowsHookEx() , and when calling the next hook in the " Hook Chain " via
the Windows API function CallNextHookEx() . We have chosen to use the Windows " Ex "
hook functions since we want to be able to port to 32 bit Windows.
System wide hooks require that your hook filter function reside in a dynamic link
library. The filter function for an application specific hook may reside either
in the application or a dynamic link library.
Different types of Windows Hooks:
Windows defines the following types of hook functions:
WH_CALLWNDPROC - A window procedure hook called whenever the SendMessage()
function is called.
WH_CBT - A computer based training hook called before activating, creating,
destroying, minimizing, maximizing, moving, or sizing a window; before completing
a system command; before removing a mouse or keyboard event; before setting
input focus; and before synchronizing the system message queue.
WH_DEBUG -A debugging hook called before any other filter installed by the
SetWindowsHookEx() function is called.
WH_GETMESSAGE - A message hook called whenever the GetMessage() function
has retrieved a message from an application queue.
WH_HARDWARE - A nonstandard hardware message hook called whenever the
application calls the GetMessage() or PeekMessage() function and there is
a hardware event (other than a mouse or keyboard event) to process.
WH_JOURNALRECORD - A journaling record hook called when the system removes
messages from the system message queue.
WH_JOURNALPLAYBACK - A journaling playback hook used to insert mouse and
keyboard messages into the system message queue.
WH_KEYBOARD - A keyboard hook called whenever the application calls the
GetMessage() or PeekMessage() function and there is a WM_KEYUP or
WM_KEYDOWN keyboard message to process.
WH_MOUSE - A mouse message hook called whenever the application calls the
GetMessage() or PeekMessage() function and there is a mouse message to
process.
WH_MSGFILTER - An application message hook called after a dialog box, message
box, or menu has retrieved a message, but before the message isprocessed.
WH_SHELL - A shell application hook called when notification messages from
the system have been made.
WH_SYSMSGFILTER - A system wide message hook called after a dialog box, message
box, or menu has retrieved a message, but before the message is processed.
Note: Debugging Windows hooks can be very difficult. The documentation has much
to be desired, so before you make the jump, always consult the Microsoft Knowledge
Base for any additional information regarding the hook you are designing for. If
your hook hangs the system, pressing CTRL+ESC or CTRL+AlT+DEL should unhook
any system wide hooks that are in effect.
Data storage in dynamic link libraries
Under 16 bit Windows, a dynamic link library is only loaded once no matter how
many applications request the library to be loaded, generally contains a single
data segment, and all global variables in the dynamic link library exist across
all applications that load the dynamic link library. If any process causes
corruption in the data segment of the dynamic link library, other applications
using the dynamic link library can be affected. Many argue that sharing data
between applications through a 16 bit dynamic link library is considered a very
bad practice.
Under Windows 95 , the code for a dynamic link library is only loaded once no
matter how many applications request the library to be loaded, and the link to a
running application gets its own data storage area. This provides some additional
crash protection to other applications, since any corrupted global variables in a
dynamic link library caused by one application will not adversely affect other
applications using that dynamic link library.
Under Windows NT , a separate copy of both the dynamic link library's data and
code is linked to the calling application. If we think about how the DLL is loaded,
we can still realize a memory saving when we talk about physical memory usage.
Each process does indeed gets its own copy of the DLL and the DLL data. However,
it obtains this through a memory mapping of the DLL. That is, the DLL code is
mapped to each processes address space. In physical memory (as seen by the system)
the DLL is loaded only once.
Instanced addresses and thunking
As already mentioned, 16-bit dynamic link libraries have only one data segment.
When you call a function in a dynamic link library, the data segment must be
switched from your application's data segment to that of the dynamic link library's
data segment, and back again after the function returns. If the dynamically linked
function happens to make a callback to a function in your application, the data
segment must be switched back to your application's data segment during the callback,
and back again to the dynamic link library's data segment when your function returns.
When making a call to a dynamically linked function, the fixup code to switch the
data segment is transparent. In the case of a callback, additional magic is needed
by your application. The magic is made possible by using the Windows API function
MakeProcInstance() to receive an " instance thunk " to your callback
procedure. This " instance thunk " is the glue that holds your callback
function and your data segment together during the callback. The procedure address
passed to the MakeProcInstance() function needs to be marked with both the " far "
and " exported " attributes. After the dynamic link library is through
using your callback function, and returns, you should always call the Windows API
FreeProcInstance() function to free the instance thunk.
Using the MakeProcInstance() function is not necessary in 32 bit applications,
or any dynamic link library (16 or 32 bit). It is, however, safe to use under these
conditions, so any code you have that uses these functions can be safely ported
to dynamic link libraries and 32 bit versions of Windows.
The example:
We will now present an example of using Windows API functions by creating an
application that serves the useful function of hooking into the Windows messaging
system, and recording any keyboard and mouse input to be used for later playback.
We will call our macro recording application " Hookit! ". Our " Hookit! "
application is a great starting place for a full featured Windows macro recorder,
reminiscent of the early days of DOS and my favorite " Sidekick's Sidekick ",
Borland's highly successful TSR program " SuperKey "! It's worth noting
that unlike 16 bit Windows, neither Windows 95 or Windows NT shipped with a
macro recording feature.
Since " Hookit! " will use system wide Windows hook functions for
JournalRecord and JournalPlayback , we will also create a Windows dynamic link
library along the way! Both the application and the dynamic link library will
perform callbacks.
Pascal was originally designed to be a teaching language, thus the code example
presented was written in Pascal , and can easily be ported to other languages such
as " C ". Note that the code cannot be ported to Visual Basic, since,
as of this writing, Visual Basic does not support creating dynamic link libraries
or callback functions.
The code presented may also be directly ported between 16 and 32 bit compilers,
and will successfully compile with Borland's Turbo Pascal for Windows 1.5, Borland
Pascal for Windows 7.0 / 7.01, Delphi 1.0 / 1.02, and Delphi 2.0 . To make it easy
to port to other languages, no Pascal or Delphi specific features are used. The
code also serves as a model for creating a " traditional " Windows program
in Delphi , and utilizing a dialog box template for the main window of a program.
Although " Hookit! " uses features designed to run on Windows 3.1 or above,
the example code can be adapted to run on earlier versions of Windows ( 2.x and 3.0 ).
The Journal Hooks: JournalRecord and JournalPlayback
The Journal hook functions provide a easy way to record keyboard and mouse events
on a system-wide basis, and play the events back at some later date.
You can install a JournalRecord or JournalPlayback hook callback function by
calling the Windows API function SetWindowsHookEx() , and passing the address of
your hook callback function. An instanced address is not needed, as both the
JournalRecord and JournalPlayback callback functions are system wide hooks,
and must reside in a dynamic link library. Calling SetWindowHookEx() will
return a 32 bit handle to your hook callback function for you to identify yourself
to other hook functions already in the " Hook Chain ", and to remove
your hook from the chain when you are done recording.
When you call SetWindowsHookEx() with the address of your JournalRecord callback
function, Windows will return immediately, and the recording begins. Your JournalRecord
callback function will get called with a code indicating the following conditions:
There is a keyboard
or mouse event to record.
The system is entering
a modal state.
The system leaving
a modal state.
The system wants you
to call the next hook in the hook chain.
If there is a keyboard or mouse event, the code will equal " HC_ACTION ",
and you are free to record the event. A pointer to an EVENTMSG is passed to you
in the lParam parameter of your JournalRecord callback function. The system time
that the event was originally fired is contained in time parameter of the EVENTMSG
structure. For playback purposes, you will need to make a copy of the EVENTMSG
and change the time in the copy to reflect the net time into the recording, since
you will need to synchronize the playback of the message to the system time at
playback. This is done by getting the system time when you start the recording,
and subtracting it from the message time.
If the system enters a modal state, the code will equal " HC_SYSMODALON ",
indicating something bad has happened to the system, and you should temporarily
stop recording, and call the next message hook in the " Hook Chain ",
so hooks further down the chain will know what is going on. When the system
returns from a modal state, the code will equal " HC_SYSMODALOFF" ,
and you may resume recording.
Finally, if the code is less than zero, the system is asking you to call the next
hook in the chain, and you should do so without further processing. When you are
through recording, you can simply call the Windows API function
UnHookWindowsHookEx() passing back the 32 bit handle given to you when you
originally called the SetWindowsHookEx() function, and Windows will remove your
hook callback function from the " Hook Chain ".
When you are ready to playback your recording, you will call SetWindowsHookEx()
again, passing the address of your JournalPlayback callback function. Windows
will return immediately, and the playback begins. During playback, normal mouse
and keyboard input is automatically disabled by the system.
Your JournalPlayback callback function will get called with a code indicating one
the following conditions:
HC_SKIP - You should retrieve the next message. If there are no more messages
to play, then you may safely call the Windows API function UnHookWindowsHookEx() ,
passing the handle to your hook function to end the playback.
HC_GETNEXT - You should play the current message.
HC_SYSMODALON - The system is entering a modal state. This indicates something
bad has happened. You should call the next hook in the hook chain, so other hooks
will know something is up.
HC_SYSMODALOFF - The system leaving a modal state, and Windows has unhooked
your JournalPlayback callback procedure right out from under you. You are done.
As your last act, you should call the next hook in the hook chain, so other
hooks will know they are hosed as well.
Code < 0 - The system wants you to call the next hook in the hook chain
without further processing.
You should go ahead and retrieve the first message before you call the SetWindowsHookEx()
function, since the system will ask you to play the first message before requesting
the next message. You will also need to get the system time, since you will need
to " fix up " the playback time of each of your recorded messages to synchronize
with the system time at playback.
Windows may ask you to play the same message more than once. The first time Windows
asks you to play the current message, your JournalPlayback callback function
should return the difference between the current time and the time the message is
scheduled to play.
If the difference between the current time and the time the message is scheduled
to play is negative, your JournalPlayback callback function should return zero.
The JournalPlayback callback function must also return zero if the same message
is requested to be played more than once.
How HOOKIT! works
Our application ( HOOKIT.EXE ) will contain four buttons:
Start Recording
Stop Recording
Playback
Done
and one callback function:
PlaybackFinished()
Our dynamic link library ( HOOKLIB.DLL ) will contain three functions that our
calling application will use:
StartRecording()
StopRecording()
Playback()
and two hook functions that Windows will use:
JournalRecordProc()
JournalPlaybackProc()
HOOKIT! application logic:
Allow the program to start only if the version of Windows is equal or greater
than Window's 3.1.
We will need to supply a callback function to be called whenever a macro's playback
has finished, since the Playback() function will return immediately, and our
application will continue to execute during playback. We will need to call
MakeProcInstance() to get the instanced address of our callback function
PlaybackFinished() to pass to the Playback() function. Since we cannot call
FreeProcInstance() in the middle of the callback, we must declare a global
variable to hold the instanced address of PlaybackFinished() on program startup,
and free it on exit from the program.
Create a main window from a Dialog Box template.
Upon creation of our main window, we will enable the " Start Recording " and " Done "
button. We will disable the "Stop Recording " and " Playback " button,
since we have no recording to stop or playback.
When the " Done " button is selected we will free the instanced address
of our callback function PlaybackFinished() and exit the program.
When the " Start Recording " button is selected, we will disable the "
Start Recording " and " Done " button, since we don't want to allow
more than one recording at a time, and we don't want to allow the user to quit in
the middle of recording.
We will enable the " Stop Recording " button and call our StartRecording() function.
If the StartRecoding() function returns an error, we will announce the error to
the user, and reset the buttons to their default state before the " Start
Recording " button was pressed.
When the " End Recoding " button is pressed, We will call the StopRecording()
function passing it the filename to store the macro in. Then we will enable both the " Done "
and " StartRecording " buttons, since we can now allow the user to quit,
and we want the user to have the option to record or rerecord the session. We
will enable " Playback " button only if anything has successfully been
recorded.
When the " Playback " button is selected, we will disable all of our buttons,
and call the Playback() function, passing it the filename of the macro to play, the
instanced address of our callback function PlaybackFinished() , and a handle to
our main window to be passed back to us as application defined data. When the
macro playback is done, our hook function will callback to our program's
PlaybackFinished() function, letting us know it has finished, and passing us back
the handle to our main window as application data. We must do this since the
Playback() function will return immediately, causing our program to continue to
run during the macro's playback.
This will allow us to know when it safe to enable our program's buttons again.
If the Playback() function returns an error, we will reset the buttons to their
default state before the " Playback " button was pressed.
When our callback function PlaybackFininshed() is called, we can enable the
" Start Recording ", " Playback " and " Done " buttons.
HOOKLIB dynamic link library logic:
If an error occurs during the call to StartRecord() , or, we are already recording
or playing, we will return zero without further processing.
During the recording process, we will save keyboard and mouse events to an array.
For the sake of simplicity, we will limit the number of recorded events to what
will fit in a 64k memory block.
When StopRecording() is called, if any events have been recorded, we will write
the recorded events to the disk for later playback. We will then unhook our
JournalRecordProc() from the hook chain. We will return an error code of -1 if
nothing is currently recording or -2 if there was trouble unhooking from the " Hook Chain ".
Otherwise we will return the number of messages recorded.
When the Playback() function is called, we will start the playback. If an error
occurs or their is nothing to playback, we will return zero without further
processing.
When the playback is finished, we will callback to the application announcing we
are done playing back the macro.
Since the SetWindowsHookEx() function does not allow us an appdata parameter
for application defined data, we must declare several global variables for our
hook callback functions, and avoid letting more than one application invoke
either the StartRecord() and Playback() functions at any given time under
Windows 3.1.
We will define a global pointer called " PMsgBuff "that will point to
an array of EventMsg structures to record to and playback from. Upon library
startup, we will initialize this pointer to nil. Only when we are recording or
playing a macro will this pointer actually point to a memory block, otherwise it
will be nil. This will give us a way to determine whether or not we are in the
process of recording or playing a macro.
We will also need to define global variables for:
" TheHook "- A 32 bit handle to our hook proc.
" StartTime "- The starting time of the recording or playback.
" MsgCount "- The total number of messages recorded.
" CurrentMsg "- The current message playing.
" ReportDelayTime "- If we should report a delay time.
" SysModalOn "- If the system is currently in a " modal " state.
" cbPlaybackFinishedProc "- The instanced address of the application's
PlaybackFinished() callback function.
" cbAppData "- The user defined application data parameter passed to our
Playback() function.
Got-Ya's!
JournalRecordProc() is incorrectly documented as receiving a MSG structure.
JournalRecordProc() actually receives the same EVENTMSG structure that is
documented by JournalPlaybackProc() .
The JournalRecordProc() and JournalPlaybackProc() hook procedures do not
provide a user defined lpData parameter, so you must use global variables in
your dynamic link library.
Mouse events recorded with JournalRecord may not play back correctly if the
display resolution has changed, or a window's position has changed.
Keyboard events recorded with JournalRecord on a Windows system other than Windows
NT will not play back correctly under Windows NT . Under Windows 3.1, the keyboard
repeat count is stored in the ParamH parameter of the EVENTMSG structure. Under
Windows NT , this parameter is always 1. The size of the EVENTMSG structure
also changes under Win32.
Your JournalPlaybackProc may be called many times with the HC_GETNEXT message.
The system expects you to continue providing the same event to play until you
receive a HC_SKIP message.
Be sure to return a non-zero delay time only once for each unique event you process
in your JournalPlaybackProc , else Windows NT may hang due to timing differences.
If your JournalPlaybackProc() gets called with a code of HC_GETNEXT more than
once for the same event, return zero from your JournalPlaybackProc .
Interactive debugging of a journal hook cannot be done on a single machine. A
Windows NT or Win32 application has an advantage that the system will send a
WM_CANCELJOURNAL message to all applications when the system pulls the hook out
from under the application when the user presses CTRL+ESC or CTRL+ALT+DEL.
" HOOKIT! " handles this event gracefully.
The SetWindowsHookEx() function will return immediately. This can be a problem
if your program needs to know when the playback has finished.
Since you will most likely use the Windows API function GetTickCount() for
calculating messaging times, you always run the risk that the system time will
wrap if windows has not been restarted in the last 49 days. If you account for
the fact that not all compilers support unsigned 32 bit integers, the system time
will appear to go negative sometime into the 24th day.
The " handle " passed back from SetWindowsHookEx() is 32 bits wide,
even in 16 bit environments.
You may need to set your compiler's link buffer to compile to disk when building
dynamic link libraries. If the file image is only created in memory, it will not
be available to the program that uses it.
JournalRecordProc() does not get along well with the new Windows 95 " StartKey "
contained on some new keyboards.
Source code for HOOKLIB.DLL
Note: Save the source as HOOKLIB.PAS for Pascal or HOOKLIB.DPR for Delphi .
{$C FIXED PRELOAD PERMANENT}
library HOOKLIB;
{$IFDEF Win32}
uses
Windows;
type
TwMsg = Longint;
TwParam = Longint;
TlParam = Longint;
{$ELSE}
uses
{$IFDEF VER15}
WinTypes, WinProcs, Win31;
{$ELSE}
{$IFDEF VER70}
WinTypes, WinProcs, Win31;
{$ELSE}
WinTypes, WinProcs;
{$ENDIF}
{$ENDIF}
type
TwMsg = Word;
TwParam = Word;
TlParam = Longint;
{$ENDIF}
const
MAXMSG = 6500;
type
PEventMsg = ^TEventMsg;
TMsgBuff = Array[0..MAXMSG]
of TEventMsg;
TcbPlaybackFinishedProc
= Procedure(AppData: Longint)
{$IFDEF Win32} stdcall; {$ELSE} ; {$ENDIF}
var
PMsgBuff: ^TMsgBuff;
TheHook: HHook;
StartTime: Longint;
MsgCount: Longint;
CurrentMsg: Longint;
ReportDelayTime: Bool;
SysModalOn: Bool;
cbPlaybackFinishedProc:
TcbPlaybackFinishedProc;
cbAppData: Longint;
{ ************************* }
{ function JournalRecordProc(Code: Integer; }
{ wParam: TwParam; }
{ lParam: TlParam): Longint; }
{ Parameters: action to perform and message data. }
{ Returns: zero unless code < 0, in which case return the result }
{ from CallNextHookEx(). }
{ ************************* }
function JournalRecordProc(Code:
Integer;
wParam: TwParam;
lParam: TlParam): Longint
{$IFDEF Win32} stdcall; {$ELSE} ; export; {$ENDIF}
begin
JournalRecordProc := 0;
case Code of
HC_ACTION: begin
if SysModalOn then exit;
if MsgCount > MAXMSG
then exit;
{record the message}
PMsgBuff^[MsgCount]
:= PEventMsg(lParam)^;
{set the delta time of
the message}
Dec(PMsgBuff^[MsgCount].Time,StartTime);
Inc(MsgCount);
exit;
end;
HC_SYSMODALON: begin
SysModalOn := True;
CallNextHookEx(TheHook,
Code, wParam, lParam);
exit;
end;
HC_SYSMODALOFF: begin
SysModalOn := False;
CallNextHookEx(TheHook,
Code, wParam, lParam);
exit;
end;
end;
if code < 0 then
JournalRecordProc := CallNextHookEx(TheHook,
Code,
wParam,
lParam);
end;
{ ************************* }
{ function StartRecording: Integer;}
{ Parameters: none. }
{ Returns: non zero if successful. }
{ ************************* }
function StartRecording: Integer
{$IFDEF
Win32} stdcall; {$ELSE} ; export; {$ENDIF}
begin
StartRecording := 0;
if pMsgBuff <> nil
then exit;
GetMem(PMsgBuff, Sizeof(TMsgBuff));
if PMsgBuff = nil then exit;
SysModalOn := False;
MsgCount := 0;
StartTime := GetTickCount;
TheHook := SetWindowsHookEx(WH_JOURNALRECORD,
JournalRecordProc,
hInstance,
0);
if TheHook <> 0 then
begin
StartRecording := 1;
exit;
end else begin
FreeMem(PMsgBuff, Sizeof(TMsgBuff));
PMsgBuff := nil;
end;
end;
{ ************************* }
{ function StopRecording(lpFileName: PChar): Integer; }
{ Parameters: pointer to filename to save to. }
{ Returns: number of records written. }
{ -1 if not recording. }
{ -2 unable to unhook. }
{ ************************* }
function StopRecording(lpFileName:
PChar): Longint
{$IFDEF
Win32} stdcall; {$ELSE} ; export; {$ENDIF}
var TheFile: File;
begin
if PMsgBuff = nil then begin
StopRecording := -1;
exit;
end;
if UnHookWindowsHookEx(TheHook)
= False then begin
StopRecording := -2;
exit;
end;
TheHook := 0;
if MsgCount > 0 then
begin
Assign(TheFile, lpFileName);
{$I-}
Rewrite(TheFile, Sizeof(TEventMsg));
{$I+}
if IOResult <> 0
then begin
FreeMem(PMsgBuff, Sizeof(TMsgBuff));
PMsgBuff := nil;
StopRecording := 0;
exit;
end;
{$I-}
Blockwrite(TheFile, PMsgBuff^,
MsgCount);
{$I+}
if IOResult <> 0
then begin
FreeMem(PMsgBuff, Sizeof(TMsgBuff));
PMsgBuff := nil;
StopRecording := 0;
{$I-}
Close(TheFile);
{$I+}
if IOResult <>
0 then exit;
exit;
end;
{$I-}
Close(TheFile);
{$I+}
if IOResult <> 0
then begin
FreeMem(PMsgBuff, Sizeof(TMsgBuff));
PMsgBuff := nil;
StopRecording := 0;
exit;
end;
end;
FreeMem(PMsgBuff, Sizeof(TMsgBuff));
PMsgBuff := nil;
StopRecording := MsgCount;
end;
{ ************************* }
{ function JournalPlaybackProc(Code: Integer; }
{ wParam: TwParam; }
{ lParam: TlParam): Longint; }
{ Parameters: action to perform and message data. }
{ Returns: if Code < 0, returns the result from CallNextHookEx(), }
{ otherwise returns the requested time to wait to fire }
{ the next event or zero. }
{ ************************* }
function JournalPlaybackProc(Code:
Integer;
wParam: TwParam;
lParam: TlParam): Longint
{$IFDEF Win32} stdcall; {$ELSE} ; export; {$ENDIF}
var
TimeToFire: Longint;
begin
JournalPlaybackProc := 0;
case Code of
HC_SKIP: begin
{get the next message}
Inc(CurrentMsg);
ReportDelayTime := True;
{are we finished?}
if CurrentMsg >=
(MsgCount-1) then
if TheHook <>
0 then
if UnHookWindowsHookEx(TheHook)
= True then begin
TheHook := 0;
FreeMem(PMsgBuff,
Sizeof(TMsgBuff));
PMsgBuff := nil;
{callback to the
application announcing we are finished}
cbPlaybackFinishedProc(cbAppData);
end;
exit;
end;
HC_GETNEXT: begin
{play the current message}
PEventMsg(lParam)^ :=
PMsgBuff^[CurrentMsg];
PEventMsg(lParam)^.Time
:= StartTime + PMsgBuff^[CurrentMsg].Time;
{if first time this message
has played - report the delay time}
if ReportDelayTime then
begin
ReportDelayTime :=
False;
TimeToFire := PEventMsg(lParam)^.Time
- GetTickCount;
if TimeToFire >
0 then JournalPlaybackProc := TimeToFire;
end;
exit;
end;
HC_SYSMODALON:begin
{something is wrong}
SysModalOn := True;
CallNextHookEx(TheHook,
Code, wParam, lParam);
exit;
end;
HC_SYSMODALOFF:begin
{we have been hosed by
the system - our hook has been pulled!}
SysModalOn := False;
TheHook := 0;
FreeMem(PMsgBuff, Sizeof(TMsgBuff));
PMsgBuff := nil;
{callback to the application
announcing we are finished}
cbPlaybackFinishedProc(cbAppData);
CallNextHookEx(TheHook,
Code, wParam, lParam);
exit;
end;
end;
If code < 0 then
JournalPlaybackProc :=
CallNextHookEx(TheHook,
Code,
wParam,
lParam);
end;
{ ************************* }
{ function Playback(lpFileName: PChar; }
{ EndPlayProc: TcbPlaybackFinishedProc; }
{ AppData: Longint): Integer; }
{ Parameters: pointer to filename to play. }
{ application's EndPlay callback function. }
{ application's defined data. }
{ Returns: non zero if successful. }
{ ************************* }
function Playback(lpFileName:
PChar;
EndPlayProc:
TcbPlaybackFinishedProc;
AppData:
Longint): Integer
{$IFDEF
Win32} stdcall; {$ELSE} ; export; {$ENDIF}
var
TheFile: File;
begin
Playback := 0;
If PMsgBuff <> nil
then exit;
GetMem(PMsgBuff, Sizeof(TMsgBuff));
If PMsgBuff = nil then exit;
Assign(TheFile, lpFileName);
{$I-}
Reset(TheFile, Sizeof(TEventMsg));
{$I+}
if IOResult <> 0 then
begin
FreeMem(PMsgBuff, Sizeof(TMsgBuff));
PMsgBuff := nil;
exit;
end;
{$I-}
MsgCount := FileSize(TheFile);
{$I+}
if IOResult <> 0 then
begin
FreeMem(PMsgBuff, Sizeof(TMsgBuff));
PMsgBuff := nil;
{$I-}
Close(TheFile);
{$I+}
if IOResult <> 0
then exit;
exit;
end;
if MsgCount = 0 then begin
FreeMem(PMsgBuff, Sizeof(TMsgBuff));
PMsgBuff := nil;
{$I-}
Close(TheFile);
{$I+}
if IOResult <> 0
then exit;
exit;
end;
{$I-}
Blockread(TheFile, PMsgBuff^,
MsgCount);
{$I+}
if IOResult <> 0 then
begin
FreeMem(PMsgBuff, Sizeof(TMsgBuff));
PMsgBuff := nil;
{$I-}
Close(TheFile);
{$I+}
if IOResult <> 0
then exit;
exit;
end;
{$I-}
Close(TheFile);
{$I+}
if IOResult <> 0 then
begin
FreeMem(PMsgBuff, Sizeof(TMsgBuff));
PMsgBuff := nil;
exit;
end;
CurrentMsg := 0;
ReportDelayTime := True;
SysModalOn := False;
{save the application's callback
procedure}
cbPlaybackFinishedProc :=
EndPlayProc;
{save the application's defined
data parameter}
cbAppData := AppData;
StartTime := GetTickCount;
TheHook := SetWindowsHookEx(WH_JOURNALPLAYBACK,
JournalPlayBackProc,
hInstance,
0);
if TheHook = 0 then begin
FreeMem(PMsgBuff, Sizeof(TMsgBuff));
PMsgBuff := nil;
exit;
end;
Playback := 1;
end;
exports
JournalRecordProc index
1 name 'JOURNALRECORDPROC' resident,
StartRecording index 2 name
'STARTRECORDING' resident,
StopRecording index 3 name
'STOPRECORDING' resident,
JournalPlayBackProc index
4 name 'JOURNALPLAYBACKPROC' resident,
Playback index 5 name 'PLAYBACK'
resident;
begin
PMsgBuff := nil;
end.
Resource statements to build:
HOOKIT16.RES and HOOKIT32.RES
Note: Save the source as HOOKIT16.RC for 16 bit environments
or HOOKIT32.RC for 32 bit environments. Use the Borland Resource
Command Line Compiler BRCC.EXE to compile a 16 bit resource file and
BRCC32.EXE to compile a 32 bit resource file.
HOOKIT DIALOG 15, 15, 63, 60
STYLE WS_OVERLAPPED | WS_VISIBLE | WS_CAPTION | WS_MINIMIZEBOX
CLASS "HOOKITDIALOGCLASS"
CAPTION "HookIt!"
BEGIN
CONTROL "Start Recording",
101, "BUTTON", BS_PUSHBUTTON | WS_CHILD | WS_VISIBLE
| WS_GROUP | WS_TABSTOP, 0, 0, 63, 15
CONTROL "Stop Recording",
102, "BUTTON", BS_PUSHBUTTON | WS_CHILD | WS_VISIBLE
| WS_GROUP | WS_TABSTOP, 0, 15, 63, 15
CONTROL "PlayBack",
103, "BUTTON", BS_PUSHBUTTON | WS_CHILD | WS_VISIBLE
| WS_GROUP | WS_TABSTOP, 0, 30, 63, 15
CONTROL "Done!",
1, "BUTTON", BS_PUSHBUTTON | WS_CHILD | WS_VISIBLE |
WS_GROUP | WS_TABSTOP, 0, 45, 63, 15
END
Source code for HOOKIT.EXE
Note: Save the source as HOOKIT.PAS for Pascal or HOOKIT.DPR for Delphi .
program HookIt;
{$D HookIt!}
{$C MOVEABLE PRELOAD PERMANENT}
{$IFDEF Win32}
{$R HOOKIT32.RES}
uses
Windows, Messages;
type
TwMsg = Longint;
TwParam = Longint;
TlParam = Longint;
{$ELSE}
{$R HOOKIT16.RES}
uses
{$IFDEF VER15}
WinTypes, WinProcs, Win31;
{$ELSE}
{$IFDEF VER70}
WinTypes, WinProcs, Win31;
{$ELSE}
WinTypes, WinProcs, Messages;
{$ENDIF}
{$ENDIF}
type
TwMsg = Word;
TwParam = Word;
TlParam = Longint;
{$ENDIF}
type
TWinVersion = record
WinMajor : Byte;
WinMinor : Byte;
DosMajor : Byte;
DosMinor : Byte;
end;
TcbPlaybackFinishedProc = Procedure(AppData: Longint)
{$IFDEF Win32} stdcall; {$ELSE} ; {$ENDIF}
const
APPNAME = 'HookIt!';
CLASSNAME ='HOOKITDIALOGCLASS';
ID_BTN_START_RECORDING =
101;
ID_BTN_STOP_RECORDING =
102;
ID_BTN_PLAYBACK = 103;
ID_BTN_DONE = IDOK;
FILENAME = 'HOOKIT.MAC';
var
PlaybackFinishedProc :TcbPlaybackFinishedProc;
function StartRecording: Integer
{$IFDEF
Win32} stdcall; {$ELSE} ; far; {$ENDIF}
external
'HOOKLIB' index 2;
function StopRecording(lpFileName:
PChar): Integer
{$IFDEF
Win32} stdcall; {$ELSE} ; far; {$ENDIF}
external
'HOOKLIB' index 3;
function Playback(lpFileName:
PChar;
EndPlayProc:
TcbPlaybackFinishedProc;
AppData:
Longint): Integer
{$IFDEF Win32}
stdcall; {$ELSE} ; far; {$ENDIF}
external
'HOOKLIB' index 5;
procedure PlaybackFinished(AppData:
Longint)
{$IFDEF Win32} stdcall; {$ELSE} ; export; {$ENDIF}
begin
EnableWindow(GetDlgItem(hWnd(AppData),
ID_BTN_START_RECORDING), True);
EnableWindow(GetDlgItem(hWnd(AppData),
ID_BTN_STOP_RECORDING), False);
EnableWindow(GetDlgItem(hWnd(AppData),
ID_BTN_PLAYBACK), True);
EnableWindow(GetDlgItem(hWnd(AppData),
ID_BTN_DONE), True);
SetFocus(GetDlgItem(hWnd(AppData),
ID_BTN_PLAYBACK));
end;
function HookitDialogProc(Dialog:
HWnd;
Msg: TwMsg;
WParam: TwParam;
LParam: TlParam): Longbool
{$IFDEF
Win32} stdcall; {$ELSE} ; export; {$ENDIF}
begin
HookitDialogProc := True;
{do any default class handling
here for HookItDlg}
HookitDialogProc := Longbool(DefDlgProc(Dialog,
Msg, WParam, LParam));
end;
function MainDlgProc(Dialog: HWnd;
Msg:TwMsg;
WParam:TwParam;
LParam:TlParam): Bool
{$IFDEF Win32} stdcall; {$ELSE} ; export; {$ENDIF}
begin
MainDlgProc := True;
case Msg Of
WM_INITDIALOG: begin
EnableWindow(GetDlgItem(Dialog,
ID_BTN_START_RECORDING), True);
EnableWindow(GetDlgItem(Dialog,
ID_BTN_STOP_RECORDING), False);
EnableWindow(GetDlgItem(Dialog,
ID_BTN_PLAYBACK), False);
EnableWindow(GetDlgItem(Dialog,
ID_BTN_DONE), True);
exit;
end;
WM_COMMAND: begin
case WParam of
ID_BTN_START_RECORDING:
begin
EnableWindow(GetDlgItem(Dialog,
ID_BTN_STOP_RECORDING), True);
SetFocus(GetDlgItem(Dialog,
ID_BTN_STOP_RECORDING));
EnableWindow(GetDlgItem(Dialog,
ID_BTN_START_RECORDING), False);
EnableWindow(GetDlgItem(Dialog,
ID_BTN_PLAYBACK), False);
EnableWindow(GetDlgItem(Dialog,
ID_BTN_DONE), False);
if StartRecording
= 0 then begin
EnableWindow(GetDlgItem(Dialog,
ID_BTN_START_RECORDING), True);
SetFocus(GetDlgItem(Dialog,
ID_BTN_START_RECORDING));
EnableWindow(GetDlgItem(Dialog,
ID_BTN_STOP_RECORDING), False);
EnableWindow(GetDlgItem(Dialog,
ID_BTN_PLAYBACK), False);
EnableWindow(GetDlgItem(Dialog,
ID_BTN_DONE), True);
Messagebox(Dialog,
'Unable
to start recording!',
APPNAME,
MB_OK);
end;
exit;
end;
ID_BTN_STOP_RECORDING:
begin
if StopRecording(FILENAME)
> 0 then begin
EnableWindow(GetDlgItem(Dialog,
ID_BTN_START_RECORDING), True);
EnableWindow(GetDlgItem(Dialog,
ID_BTN_PLAYBACK), True);
SetFocus(GetDlgItem(Dialog,
ID_BTN_PLAYBACK));
end else begin
EnableWindow(GetDlgItem(Dialog,
ID_BTN_START_RECORDING), True);
SetFocus(GetDlgItem(Dialog,
ID_BTN_START_RECORDING));
EnableWindow(GetDlgItem(Dialog,
ID_BTN_PLAYBACK), False);
end;
EnableWindow(GetDlgItem(Dialog,
ID_BTN_STOP_RECORDING), False);
EnableWindow(GetDlgItem(Dialog,
ID_BTN_DONE), True);
exit;
end;
ID_BTN_PLAYBACK: begin
EnableWindow(GetDlgItem(Dialog,
ID_BTN_START_RECORDING), False);
EnableWindow(GetDlgItem(Dialog,
ID_BTN_STOP_RECORDING), False);
EnableWindow(GetDlgItem(Dialog,
ID_BTN_PLAYBACK), False);
EnableWindow(GetDlgItem(Dialog,
ID_BTN_DONE), False);
if PlayBack(FILENAME,
PlaybackFinishedProc, Dialog) = 0 then begin
EnableWindow(GetDlgItem(Dialog,
ID_BTN_START_RECORDING), True);
EnableWindow(GetDlgItem(Dialog,
ID_BTN_STOP_RECORDING), False);
EnableWindow(GetDlgItem(Dialog,
ID_BTN_PLAYBACK), True);
EnableWindow(GetDlgItem(Dialog,
ID_BTN_DONE), True);
SetFocus(GetDlgItem(hWnd(Dialog),
ID_BTN_PLAYBACK));
end;
exit;
end;
ID_BTN_DONE: begin
EndDialog(Dialog,
ID_BTN_DONE);
exit;
end;
end; {wParam}
end; {WM_COMMAND}
WM_CLOSE: begin
FreeProcInstance(@PlaybackFinishedProc);
EndDialog(Dialog, IDOK);
exit;
end;
end;
MainDlgProc := False;
end;
procedure Init;
var
WindowClass: TWndClass;
WinVer: TWinVersion;
begin
Longint(WinVer) := GetVersion;
if ((WinVer.WinMajor <
3) OR
((WinVer.WinMajor =
3) AND
(WinVer.WinMinor <
10)) ) then begin
Messagebox(0,
'Microsoft
Windows 3.10 or greater required!',
APPNAME,
MB_OK);
halt;
end;
@PlaybackFinishedProc :=
MakeProcInstance(@PlaybackFinished, hInstance);
If @PlaybackFinishedProc
= nil then begin
Messagebox(0,
'Cannot create
instance thunk!',
APPNAME,
MB_OK);
halt;
end;
if FindWindow(CLASSNAME,
APPNAME) <> 0 then begin
Messagebox(0,
'Multiple Sessions
not allowed',
APPNAME,
MB_OK);
halt;
end else begin
WindowClass.Style
:= CS_BYTEALIGNWINDOW;
WindowClass.lpfnWndProc
:= @HookItDialogProc;
WindowClass.cbClsExtra
:= 0;
WindowClass.cbWndExtra
:= DLGWINDOWEXTRA;
WindowClass.hInstance
:= hInstance;
WindowClass.hIcon
:= LoadIcon(0, IDI_APPLICATION);
WindowClass.hCursor
:= LoadCursor(0, IDC_ARROW);
WindowClass.hbrBackground
:= GetStockObject(WHITE_BRUSH);
WindowClass.lpszMenuName
:= nil;
WindowClass.lpszClassName
:= CLASSNAME;
if not Bool(RegisterClass(WindowClass))
then begin
Messagebox(0,
'RegisterClass
Failed!',
APPNAME,
MB_OK);
halt;
end;
end;
end;
procedure MyWinMain;
var
WindowProc:TFarProc;
begin
WindowProc:=MakeProcInstance(@MainDlgProc,
hInstance);
DialogBox(hInstance, 'HOOKIT',
0, WindowProc);
FreeProcInstance(WindowProc);
end;
{WinMain}
begin
Init;
MyWinMain;
end.
Conclusion:
While writing Windows applications gets easier as development tools get more
sophisticated, there will be times when you must "bite the bullet" and a interface
to new functions that are not handled by your current tools.
Remember... before you explore unchartered paths, check available resources for
documentation that may help to make your journey a pleasant and rewarding
experience.
|
Copyright (c) HelpMakers, 1997 - 99 |