Add-Ins are a very powerful means of extending Enterprise Architect's built-in functionality. However, in some cases you have a hard time navigating down to deeply nested items of your add-in menu in order to trigger their associated functions.
Wouldn't it come in handy to have the possibility of defining an arbitrary number of keyboard shortcuts that invoke custom functionalities of your add-in? Of course, it would and this tutorial presents how this can be achieved.
Firstly, I'm going to explain which technical challenges existed and how they were tackled in order to get the solution running properly. This might be especially useful for add-in developers that want to grasp the concepts and toy with the notion of extending the feature with some cool stuff. In case you just want to get a hands-on guide on how to setup custom hotkeys for your add-in, feel free to skip this section. Finally, you'll find the used C# source files attached to this tutorial.
Registering hotkeys for global usage
Before the add-in is able to react to pressed keyboard combinations, we first have to register them somewhere so that the operating system, which in fact processes incoming keyboard actions, knows who should be notified in case something is pressed on the keyboard. For this purpose, Windows provides a native interface located in user32.dll. For the task of (un-)registering hotkeys, two methods are of interest to us:
- bool RegisterHotKey(IntPtr hWnd, int id, int fsModifiers, int vk)
- bool UnregisterHotKey(IntPtr hWnd, int id)
You can see that in each case a pointer to a specific window handle is needed for (un-)registration, i.e. global hotkeys can only be registered to existing windows. Note that it doesn't play a role if that window is currently active or not. Since the registration's scope is globally, there must only be one window associated with a specific hotkey at a time implying that registrations can fail if that hotkey combination is already taken by another tool. Keep that in mind in case the functionality is not invoked properly.
In a nutshell, we need a window that has the following properties:
- It should be a descendant of System.Windows.Forms.IWin32Window to serve for hotkey registration
- It needs to be extendable in a way that we can process incoming window processing events
But back to Enterprise Architect scope:
The first idea coming to my mind was getting the window handle of the Enterprise Architect application itself. Recently, Geert Bellekens shared his way of achieving this with the community (Link to forum topic). Unfortunately, it cannot be used for registering hotkeys due to point 2 of the preceding list, because at least I didn't find any possibility to override the window processing event handler for the Enterprise Architect main window class.
Thus, the approach taken here is to create a custom System.Windows.Forms.Form which has all the needed properties by default. Due to its purpose of hotkey processing, it is made invisible to the user. The class is called InvisibleHotkeyForm in the above diagram. It has proven useful to create and show the form in the EA_Connect() event handler of the add-in's main class.
In order to finally register hotkeys, a list of hotkey definitions needs to passed to the InvisibleHotkeyForm constructor. Each hotkey definition is an instance of the Hotkey class, consisting of three properties:
- Key is the character being pressed within the combination, e.g. A in Ctrl+Shift+A. It is represented by the enumeration System.Windows.Forms.Keys
- Modifiers are the modifiers within the combination, e.g. Ctrl+Shift in Ctrl+Shift+A. It is represented by a disjunction of bitvalues of the enumeration Modifiers, that contains a bit pattern for each modifier
- Handler is a C# delegate that holds a function pointer to the user-defined function which should be invoked when pressing the hotkey
After instantiating and showing the InvisibleHotkeyForm, the existing classes take care of the rest of the work.
Getting notified of pressed key combinations and handling them
Having created an instance of InvisibleHotkeyForm, the Windows API has registered it to receive window processing events. The class System.Windows.Forms.NativeWindow, which is a base class of System.Windows.Forms.Form, declares the method void WndProc(ref Message m), which is invoked by the operating system, when a window processing event has occurred. The overriden implementation of it in InvisibleHotkeyForm is:
Since there exist more window processing event types besides hotkey events, we have to check for that first. After that we ask each hotkey in the list, if it's associated with the pressed hotkey combination, which is encoded in the LParam property of the window processing event message. If there is a match with one of our defined hotkeys, we invoke its delegate function that contains the custom behavior. Note that currently, the delegate type neither has parameters nor a return type, since it should only be an entry point for triggering further add-in behavior. However, this could be customized with minor efforts.
Dealing with multiple running instances of the Enterprise Architect application
Because of the fact that we deal with global hotkeys that are restricted to be registered to only one window, the whole approach fails as soon as we open up two instances of the Enterprise Architect application. The reason is that each instance is running in its own process and therefore the add-in is loaded once for each instance, which means we have also two instances of InvisibleHotkeyForm that try to register the same set of hotkeys. This of course succeeds only with the first Enterprise Architect application instance.
In order to tackle this problem, I assumed that the user always works in only one Enterprise Architect instance, which implies that its main window is active only for that time. In this manner, each add-in instance has to be aware, if its Enterprise Architect instance's main window is currently active or not.
Upon this information the add-in can decide, if:
- it has to register its hotkeys as soon as its Enterprise Architect window gets focused by the user
- it has to unregister its hotkeys as soon as its Enterprise Architect window looses focus
Thus, the hotkeys are only registered for the focused Enterprise Architect instance and the registration in the operating system never fails.
Technically, this is achieved by first capturing the process id of the Enterprise Architect instance during startup. Next, a System.ComponentModel.BackgroundWorker is used for periodically polling the process id of the process whose window handle is currently focused by the user. Finally, we have to compare these process ids and determine, whether the Enterprise Architect instance got focus or lost focus in order to register or unregister the hotkeys.
The implementation of the DoWork event handler of the background worker performing this task is:
In the function implementation's first line, the process id of the process with active window is retrieved by invoking ActiveProcess.GetActiveProcess(). This function again makes use of two native methods of the user32.dll, namely
How can I use this feature in my add-in?
First, you have to copy the folder src/GlobalHotkeys of the attached archive to your project and possibly adjust the namespace names of its classes according to your project structure.
An example for the integration of the hotkey feature into your add-in is given below. For modularity reasons, I created a static class that encapsulates all hotkey definitions together with their handler functions. Note that during the definition of the modifiers for the hotkey you have to use the bitwise OR operator and not the logical OR operator to combine different modifiers. In addition, it's theoretically also possible to define single characters as a hotkey, i.e. no modifiers are needed (Modifiers.NoMod) then to activate it.
This tutorial provided the knowledge and practical instructions to enrich Enterprise Architect add-ins with the possibility to define and react to custom hotkeys. In addition to that, it should bring the fact home to add-in developers that searching for solutions beyond the .NET layer (=native Windows interfaces) highly extends the space of possibilities. Finally, I want to encourage the readers of this tutorial to give me feedback in any shape to improve its quality.