Using IOLEControl to intercept Alt-keys and Determine when events are frozen
Add proper mnemonic processing support to VB controls.
This article demonstrates how to replace VB's rather useless implementation of the IOLEControl interface with something you can actually use to get an event when an Alt-key is pressed. It also demonstrates how to use the code to receive EventsFrozen notifications which can be useful if you want to change a controls behaviour when the user has pressed pause in the IDE.
The IOLEControl interface is one of many which help interface an ActiveX control with its parent. This particular interface has something of a grab bag of features: it controls keyboard mnemonic (Alt-key) handling, notifying controls when ambient properties change and notifies controls when their container can or can't receive events.
VB's built-in implementation of this interface is half-hearted. The only feature that's implemented properly is the notification of ambient property changes through the AmbientChanged event. Mnemonic processing is broken: although you can specify a series of keys which can activate the control through the AccessKeys property, the only thing that happens when one of these keys is pressed is that the control gets focus if it didn't before. This allows you to create controls which behave like a label control, but not ones which behave like (for example) a Toolbar, when each button could have its own alt-key mnemonic. The EventsFrozen event notification is not directly implemented (although it appears that the AmbientChanged event fires with a property name of UIDead when this occurs, which might be a suitable alternative).
Using The Code
There are two samples in the download. The first is a minimal control demonstrating how to support multiple Alt- key mnemonics, which resembles the keyboard interface for something like a toolbar control. The second demonstrates how to use a control to determine when events are frozen.
1. Processing Mnemonics
To process mnemonics you need to do the following:
Once this is done, you're ready to start adding new mnemonics. The pcMnemonics class provides methods to add shortcuts keys based on either the a string character (useful if your control has labels which include the ampersand prefix) or the virtual key code. There are two corresponding remove methods and also methods for enumerating the keys which have been set up. Refer to the demonstration to see these methods in action.
Note that if you want to change which mnemonics are set up at run-time, you may need to force the container to acknowledge that you want a change. In VB you can do this by modifying the AccessKeys property. I chose to simply toggle the value of this property between a space and an empty string, which does the trick:
Private Sub updateMnemonics() If (UserControl.AccessKeys = "") Then UserControl.AccessKeys = " " Else UserControl.AccessKeys = "" End If End Sub
2. Events Frozen Notifications
Setting up an Events Frozen notification is done in a very similar to the last example, except for the last two stages you implement and override the FreezeEvents method instead:
Private m_ptrFreezeEventsOrig As Long Friend Function FreezeEvents(bFreeze As Long) As Long ' Process event here. If bFreeze = 0 then ' events are unfrozen, otherwise they are ' frozen. End Function Private Sub UserControl_Initialize() Dim iOleCtl As IOleControl Set iOleCtl = Me m_ptrFreezeEventsOrig = ReplaceVTableEntry( _ ObjPtr(iOleCtl), IDX_FreezeEvents, _ AddressOf mIOleControl.IOleControl_FreezeEvents, _ ObjPtr(Me) _ ) End Sub Private Sub UserControl_Terminate() Dim iOleCtl As IOleControl Set iOleCtl = Me ReplaceVTableEntry _ ObjPtr(iOleCtl), IDX_FreezeEvents, _ m_ptrFreezeEventsOrig End Sub
How It Works
This technique is a well-known hack to work around limitations of COM objects which don't provide the facilities you want, and relies on the way COM objects are laid out physically in memory. Since this is specified in the COM contract, you know that all COM objects
Overriding COM Object Methods
A COM object in memory is laid out as a sequence of pointers to the functions to be called. This sequence is called the vTable (or Virtual Table). Each object instance contains a memory pointer to this table (an object pointer in memory is actually just a pointer to this). So if we can get the pointer to the vtable, then in theory we can replace any of the original functions with our own versions.
This is achieved in the code using the ReplaceVTableEntry method. Basically all this needs to do is to locate the table entry to replace, and put a new pointer in its place then return. My version of the code additionally allows you to associate another pointer with this change. I use this additional pointer to store an un-referenced instance of the owning control, so I can later get a control instance and call it.
To locate the table entry to replace, we first note that on a 32-bit process all memory addresses are 4 bytes long. The vTable is laid out sequentially, with each method following the other in sequence. So the table entry to replace can be identified by its index in the interface. You can see the layout of an IOLEControl interface by looking at its definition in the PlatformSDK (you will find this in the file OCIdl.Idl). However, note that as with all COM objects, the object must implement IUnknown, and hence the first three members must be the three members of the IUnknown interface:
Therefore the first entry of a COM interface will appear at position 4, following the pointer to the Release method.
A similar technique is used in the IOLEInPlaceActiveObject TabCatch sample, except in that code the entire vTable is replaced, rather than just individual methods.