Hardcore Clarion - Message Filters 101

by Paul Attryde

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, you’ll already be thinking of sub-classed procedures and trapping the WM_KEYDOWN and WM_KEYUP events before they reach the ACCEPT loop. If you’ve 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.

  • The filter procedure must be in a DLL. I know people have successfully written filters that reside in executables (.EXEs) but, if you regard the Windows API as your bible (and you will when writing filters), then strictly speaking it must be in a DLL
  • Filters that trap keystrokes won't trap keys you type in a DOS box, only Windows apps like Word or Notepad.
  • Clarion itself uses a number of filters internally (especially for the PRESS command - more on that later). If you experience problems when developing your filter procedure, you may be conflicting somehow with the Clarion runtime. It’s not happened to me yet, but it’s something to be aware of.
  • Any Clarion function you call inside a filter procedure will affect the rest of the application. For example, assume you have 2 files declared in your dictionary, A and B. You've written a keyboard filter to translate keys into other keys, and inside the filter procedure you do a GET() on file B. If the keystroke you're translating isn't in file B, you'll get an errorcode returned. Assuming that, look at the following Clarion code:
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.

Installing a 32-bit 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.

Removing a 32-bit filter

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

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 you’ll 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?

Translating keystrokes inside a filter

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, you’ll probably try PRESS or PRESSKEY. That may or may not work, depending on something that I haven’t 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, here’s one possible solution:

  1. Declare 2 user events; EVENT:TRANSLATESTART and EVENT:TRANSLATESTOP as EVENT:User+1000 and EVENT:USER+2000 respectively.
  2. Start a new thread in your main program. Make sure the thread has a window (and therefore an ACCEPT statement), and make sure you store the thread number somewhere.
  3. Code your filter with the appropriate logic to determine when to reverse the alphabet. When you need to inform the main application of a new keystroke, work out the equate code of the new key. If the user pressed A (for example) the new keycode to send to the application would be for Z, which is 5AH or 90 decimal.
  4. Post EVENT:TRANSLATESTART+90 to the new thread (this is why you need to store the thread number somewhere).
  5. In the ACCEPT loop of the new thread, do something similar to
  ACCEPT
    IF EVENT() > EVENT:TRANSLATESTART AND EVENT() < |
      EVENT:TRANSLATESTOP
      PRESSKEY(EVENT() - EVENT:TRANSLATESTART)
    END
    CASE EVENT()
    OF …
    END
  END

All we’ve 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 there’s always a solution somewhere. It may be a lot more work than you wanted, but it’s there.

16-bit Considerations

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. That’s why in 32-bit you have to use the Windows API instead to load the DLL and get to the filter procedure.

Summary

The example I’ve 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!

Next time

16-bit serial communications

Download the source

Printer-friendly version

 
 

Search

 

Advanced Search
Topical Index

Related Articles

Subscribe to
ClarionMag

One year: $184

(includes all back issues since '99)

Renewals from $134

Two years: $274

Renewals from $224

More Info

Subscribe Now!

ClarionMag Blog

RSS Feeds

Updates via Email

Enter your Email


Powered by FeedBlitz

Quick Links