![]() |
|
Published 1998-10-01 Printer-friendly version
Something that you may need to do from time to time within your application is trap keystrokes before the Clarion ACCEPT loop can get them. If you read my previous article, youll already be thinking of sub-classed procedures and trapping the WM_KEYDOWN and WM_KEYUP events before they reach the ACCEPT loop. If youve actually tried it, you've found it doesn't work - the subclassed procedure never gets the WM_KEYDOWN event you expected it to. Why? Well, only a few people in London know the answer to that one, but luckily there is a solution to the problem. Use a hook, or message filter.
According to the Windows API documentation:
"A hook is a point in the Microsoft Windows message-handling mechanism where an application can install a subroutine to monitor the message traffic in the system and process certain types of messages before they reach the target window procedure. This [help] topic describes Windows hooks and explains how to use them in a Windows-based application."
In the great scheme of things, writing message filters isn't that difficult. However, if you've never written one before, things can get complicated rather quickly. There are a couple of important things to remember when writing filters:
You can't debug a message filter that processes keystrokes with the debugger, at least in 16-bit. In 32-bit it's a little more stable, but it makes debugging your filter rather difficult. You can't use Stop or Message either, as pressing a key to close the window generates another keystroke which you filter catches, so displaying another Stop or Message etc.
GET(A,1) MESSAGE(ERRORCODE()) MESSAGE(ERRORCODE())
Spot the problem? The GET() on file A will work fine, and you'll see 0 as the errorcode. When you press Enter to close the message dialog, your filter will do a GET() on file B. If you're not translating the Enter key, it'll fail with an error 33 and the second call to MESSAGE() will show 33, not 0. That's a prime example of your filter procedure affecting your application - I know, it took me 2 days to find it when I wrote my first 16-bit keyboard filter. The solution? Inside your filter procedure, if the GET() on file B fails, then do something you know will work, like GET(B,1) or CLOSE(B);OPEN(B)
So, how do you actually do it? The code I'm going to show is for a 32-bit filter which disables the J,P and I keys on the keyboard. As I mentioned above, it won't catch keys that the user types in a DOS box - but then, I'm a Windows programmer, and what people do in a DOS box is their own business.
One thing you will probably notice is my prototypes and use of the Windows API. I prefer to prototype everything using Longs, and use the ADDRESS() function when I pass parameters. It's just personal preference, and as long as you end up passing the same thing it doesn't quite matter what the prototype is.
The code for the application that installs the filter is quite simple:
PROGRAM
MAP
MODULE('DLL32')
InstallHook
RemoveHook
END
END
FilterWindow WINDOW,AT(,,72,29),GRAY
PROMPT('Sorry, I've just disabled' &|
' your J,P and I keys!'),|
AT(5,1,59,28),USE(?Prompt1)
END
CODE
OPEN(FilterWindow)
InstallHook
ACCEPT
END
CLOSE(FilterWindow)
RemoveHook
After opening a window or application frame, simply call a function in your DLL to install the filter. When you close the application, remove the filter.
The code for the DLL, as you might expect, is a little bit more complicated. The code that actually installs the hook looks like this:
LocalVars GROUP,PRE(Loc)
DLLName CSTRING(20)
DllNamePtr ULONG
KHName CSTRING(50)
KHNamePtr ULONG
MHName CSTRING(50)
MHNamePtr ULONG
ThreadID DWORD
DLLInstance HINSTANCE
END
! Valid Hook types
WH_Min EQUATE(-1)
WH_MsgFilter EQUATE(-1)
WH_JournalRecord EQUATE(0)
WH_JournalPlayback EQUATE(1)
WH_Keyboard EQUATE(2)
WH_GetMessage EQUATE(3)
WH_CallWndProc EQUATE(4)
WH_CBT EQUATE(5)
WH_SysMsgFilter EQUATE(6)
WH_Mouse EQUATE(7)
WH_Hardware EQUATE(8)
WH_Debug EQUATE(9)
WH_Shell EQUATE(10)
InstallHook PROCEDURE
CODE
HV:InstalledOK = False
Loc:DLLName = 'FILTER.DLL'
Loc:DllNamePtr = Address(Loc:DLLName)
Loc:DLLInstance = LoadLibrary( Loc:DllNamePtr )
IF Loc:DLLInstance <> 0
Loc:KHName = 'HookProcedure'
Loc:MHNamePtr = Address(Loc:KHName)
HV:Hook = GetProcAddress(Loc:DLLInstance,Loc:MHNamePtr)
IF HV:Hook <> 0
HV:PrevHook = SetWindowsHookEx(|
WH_Keyboard,|
HV:Hook,|
Loc:DLLInstance,0)
IF HV:PrevHook = 0
HV:InstalledOK = True
END
END
END
The example code above is used to install a system-wide keyboard hook. We first do a LoadLibrary on ourself to get a handle to the DLL.
We then do a GetProcAddress to get the address of the hook procedure. We need to pass both these things when we actually install the hook.
Finally, assuming we've been able to get the information we require, we install the hook. We pass in the hook type, procedure address and the handle to the DLL. Because we are installing a system hook, not an application hook, we pass a 0 as the last parameter to indicate that the hook is associated with all existing threads.
To remove the hook is simplicity itself:
RemoveHook PROCEDURE
CODE
IF HV:InstalledOK = True
x# = UnhookWindowsHookEx( HV:PrevHook )
END
If we installed it correctly then remove it.
The filter procedure contains the code that will be executed for every keypress (except those made inside a DOS box).
One thing to remember is that there are quite a few different types of filter you can install - remember all the different equates in the InstallHook() procedure? Well, although all filter prototypes are the same, the meaning of the variables, how you handle them and the return code from the filter, all depend on what type of filter you installed. So, don't change the type of filter in the install procedure without checking that the parameters mean the same thing. Otherwise your computer will go pear-shaped rather quickly, and youll end up rebooting rather a lot.
HookProcedure FUNCTION(|
Prm:nCode,|
Prm:wparam,|
Prm:lparam)
LocalVars GROUP,PRE(Loc)
ProcessedEvent BYTE
ReturnValue DWORD
END
Key_Up EQUATE(80000000B)
VK_I EQUATE(049H)
VK_J EQUATE(04AH)
VK_P EQUATE(050H)
CODE
IF Prm:nCode < 0
Loc:ProcessedEvent = False
ELSE
! Your code goes in here
! My example code disables keys, so do that here
DO EatKeyStroke
END
IF ~Loc:ProcessedEvent
Loc:ReturnValue = CallNextHookEx(|
HV:PrevHook,|
Prm:nCode,|
Prm:wParam,|
Prm:lParam)
END
RETURN(Loc:ReturnValue)
EatKeyStroke ROUTINE
IF ~Band(Prm:lParam,Key_Up)
IF Prm:wParam = VK_J |
OR Prm:wParam = VK_P |
OR Prm:wParam = VK_I
Loc:ReturnValue = 1
Loc:ProcessedEvent = True
END
END
I actually installed a WH_KEYBOARD filter, and according to the documentation for hooks of type WH_KEYBOARD:
"If nCode is less than zero, the hook procedure must pass the message to the CallNextHookEx function without further processing and should return the value returned by CallNextHookEx"
So, I first determine if I am allowed to process this keystroke. If I am, I then have to determine what I actually want to do with the keystroke. In this example, I am disabling the J, P and I keys, so I first check to see if this is a key press or a key release. Once I've determined that the key is being pressed, I set a flag telling myself that I have processed the event, and I set the return value appropriately:
Again, referring to the Windows API documentation tells me that:
"To prevent Windows from passing the message to the rest of the hook chain or to the target window procedure, the return value must be a nonzero value. To allow Windows to pass the message to the target window procedure, bypassing the remaining procedures in the chain, the return value must be zero."
So, to disable the J, P or I keys all I have to do is return a non-zero value from the filter, and those keys are not passed to a window procedure (Clarion subclassed procedure) or accept loop. If we didn't process the event ourselves, go and pass this keystroke on to the next hook in the chain.
Easy isn't it?
One of the uses of a filter is to translate one key into another. You may want to write a filter that reverses the alphabet, so when the presses A he gets a Z, B to X, C to W, etc. To get your new keystroke into your application, youll probably try PRESS or PRESSKEY. That may or may not work, depending on something that I havent worked out yet. It may be to do with the fact the Clarion itself uses a WH_JOURNALPLAYBACK filter for the PRESS command, which is conflicting with our filter procedure. The end result is that PRESS or PRESSKEY may not work for you, and your new keystroke is stuck in the filter procedure with no way of getting it to your ACCEPT loop.
What is important to know is that PRESS and PRESSKEY always seem to work from within the application itself. With this is mind, heres one possible solution:
ACCEPT
IF EVENT() > EVENT:TRANSLATESTART AND EVENT() < |
EVENT:TRANSLATESTOP
PRESSKEY(EVENT() - EVENT:TRANSLATESTART)
END
CASE EVENT()
OF …
END
END
All weve done here is invent a rather complicated means of informing the application of the new keystroke. There are plenty of other ways to do it (posting a user event to the application and having it query a variable in the filter DLL for the new keycode is another) but the important thing to remember is that theres always a solution somewhere. It may be a lot more work than you wanted, but its there.
Writing a 16-bit filter isn't that much different from writing a 32-bit filter. The names of the Windows API procedures are slightly different (remove the EX from the end), but that's basically it.
Instead of using LoadLibrary and GetProcAddress in the install procedure to get the handle to the DLL, you can use System{PROP:appinstance} as the instance of the DLL and Address(HookProcedure) as the handle to the procedure.
In 16-bit everything works out to be the same, but in 32-bit the numbers are substantially different. Thats why in 32-bit you have to use the Windows API instead to load the DLL and get to the filter procedure.
The example Ive shown here is a very basic example of a keyboard filter. There are many different types of filter, each one suited to a different task. Make sure you use the correct type of filter, and good luck!
16-bit serial communications
Copyright © 1999-2008 by CoveComm Inc. All Rights Reserved. Reproduction in any form without the express written consent of CoveComm Inc., except as described in the subscription agreement, is prohibited.
Clarion Magazine ISSN 1718-9942
One year: $184
(includes all back issues since '99)
Renewals from $134
Two years: $274
Renewals from $224