views:

201

answers:

3

A have a C# class which simplifies the handling of global hot keys. This class uses the Win32-API function RegisterHotKey() to register the hot keys.

According to MSDN this function needs an ID value in the range 0x0000 through 0xBFFF when calling from an application and an ID value in the range of 0xC000 through 0xFFFF when calling from a shared DLL. GlobalAddAtom() can be used to get the ID in case of running in a DLL.

To hide this distinction from the user of the class the class itself should decide which ID range is to be used when registering a hot key. Well, and to do this, the class must be able to detect whether its code is running within an application or within a shared DLL.

But how to do this? What is the best C#/.NET way to do this?

+3  A: 

Try this:

bool isDll = this.GetType().Assembly.EntryPoint == null;

MSDN:

Assembly.EntryPoint Property

"Property Value A MethodInfo object that represents the entry point of this assembly. If no entry point is found (for example, the assembly is a DLL), a null reference (Nothing in Visual Basic) is returned. "

Philip Wallace
+3  A: 

It's your class - you know where you're putting it.

If you're not sharing it, then just pick an ID below 0xBFFF and be done with it.

If your class belongs in a DLL that can be shared by multiple applications... Or can simply be shared by code that you don't control and therefore are unable to sort out IDs for... then use GlobalAddAtom() to get an ID (and remember to call GlobalDeleteAtom() after you unregister the hotkey).


Explanation

It's probably worth taking a minute to think about why there are two different ID ranges, and why the API docs recommend using GlobalAddAtom() to obtain an ID in the latter range for shared DLLs. Let's start with the documentation for the parameter to RegisterHotKey():

id
    [in] Specifies the identifier of the hot key. If the hWnd parameter is NULL, then the hot key is associated with the current thread rather than with a particular window. If a hot key already exists with the same hWnd and id parameters, see Remarks for the action taken.

From this, we can surmise that hotkeys are uniquely identified by one of two potential pairs of information: a thread or window handle, and an arbitrary 16-bit number. If you specify a window handle (HWND), then the message is sent to that window; otherwise, it's sent to the thread.

So... If you only register one hotkey for a given window, the ID doesn't really matter1; no one else can register a hotkey for that window, and hotkey events for other windows will post to those windows. Similarly, if you only register one windowless hotkey for a given thread, you'll only get messages for that hotkey. If you control all of the code for your application, you can pick whatever IDs you want for your hotkeys, using whatever technique you want to assign them; no one else will step on them, because you own all the code that would be able to step on them!

But what if you're writing a general-purpose routine that can be called by other code? You can't reliably pick a constant ID then, since the caller could potentially be using that ID already, and if they're also using the same window or thread, you'd end up redefining their hotkey. Or what if (as in your case) you don't know how many hotkeys will be registered until runtime?

You need a way to ensure that the ID you choose at runtime will be one that no one else is using. This is where GlobalAddAtom() comes into play: you pass it a string, and it gives you an ID guaranteed to correspond to that string and no other; this is effectively unique for the system, unless someone else passes the same string - and you can probably come up with a unique string; just use your company name, or your social security number, and a prefix that you increment for each new atom that you need. Or, if you're really paranoid, use a GUID.

The truth behind truths

With that out of the way, let me try to clear up a bit of confusion: Windows doesn't actually care whether or not the code that calls RegisterHotKey() is in a DLL. It can't. Consider the following routine:

void RegisterSuperHappyFunHotKey(HWND hWnd, int id, 
                                 unsigned int fsModifiers, unsigned int vk)
{
   RegisterHotKey(hWnd, id, fsModifiers, vk);
}

This routine does nothing but forward its parameters on to the WinAPI function, none of which identify the calling module. If it lived in a DLL, it would behave the same as if it was compiled into the application itself. There's no reliable way for Windows to tell where the call originates, and the hotkey itself is tied to a window and thread (or a thread alone) either of which could be controlled by code in or out of a DLL. Now of course, you could have app- or library-specific requirements of your own: if your DLL creates a window and sets up a hotkey for it, then you'll want to take care of unregistering that hotkey when you destroy the window... But that's your own concern, to handle as you see fit.

MSDN specifies the two ranges of IDs for one good reason: to encourage you, the DLL author, to avoid stepping on IDs used by an application author. If you're the application author, then the world is your oyster - you control (for the most part) which code gets loaded and executed in your application's process, and can therefore make the decisions as to which IDs you use however you see fit: starting at 0 and incrementing for each new hotkey is perfectly acceptable. But once you venture into the upper range of IDs, you'll have to use GlobalAddAtom() just as if you were a DLL - or you run the risk of colliding with an ID generated in this manner by third-party code loaded from a DLL. It's a... social contract of sorts.

Summary:

The "shared DLL" bit is a red herring here; if you can know the IDs of all hotkeys registered by your application, then just pick a number below 0xBFFF and use it. If you can't, because your code will be used by multiple callers (as yours is...), then obtain an ID using GlobalAddAtom() and use that.

Recommendation

For these reasons, I recommend that you do use GlobalAddAtom() for the class you're designing, simply because it sounds as though you don't know at this time whether or not you'll be building it into an application of your own design (where you control the IDs being used) or a DLL to be loaded by some other app (where you do not). Don't worry - you're not violating the contract by pretending to be a DLL, so long as you follow the rules set forth for DLL callers.


1ok, so there are a couple of system-defined hotkey IDs that you have to watch out for...

Shog9
Um, what if he is packaging the source?
Tim
Of course, I know where I use it. But it wouldn't be a good design for such a class if the user of this class would have to deal with IDs and would have to think about the range of the IDs. So I want to hide this within the class. Reusability is another point here.
Habi
Ok, I've tried to explain this a bit better. Basically, either you can pick IDs reliably *and know it*, or you can't *and know that you can't* - in the former case, use the low range; in the latter, use `GlobalAddAtom()`. There's never a scenario where you can get away with deciding which range of IDs to use at runtime; if you can't decide ahead of time, you'll always use the high range (and GlobalAddAtom()).
Shog9
@Shog9: Thank you for the detailed (and good) explanation. You say: "Basically, either you can pick IDs reliably and know it, or you can't and know that you can't".But this is wrong.The class handles the IDs internally. If someone uses this class in an appl the class must use IDs <= 0xBFFF. Now someone takes this class and uses it within a shared DLL. And now the class has to use GlobalAddAtom(). The user must change code.Or the designer of the class could look forward and take this already into account by handling booth cases. Or use generally GlobalAddAtom(), but this is against MSDN.
Habi
You can use `GlobalAddAtom()` within your app. You just don't *have* to - if it's your app, you know which IDs in the low range are available, and can simply pick from among those. What MSDN is saying is essentially, "Don't stomp on the IDs reserved for the application if you're not the application", and suggesting `GlobalAddAtom()` as a reliable way of picking IDs that won't be used elsewhere. If you can't guarantee that your code won't be used by an application that uses some other means of picking IDs, then use `GlobalAddAtom()` to pick them.
Shog9
In your specific situation, I would recommend the use of GlobalAddAtom() to pick IDs, simply because it sounds like you plan on making these routines available to other applications in addition to your own. If that's *not* the case - if you'll only be using this in your own apps, and this class will be the *only* code that calls `RegisterHotKey()`, then you can simplify it by simply initializing a static class member to 0 and using it for each new ID (incrementing it afterward so that it represents the next available app-internal ID).
Shog9
FWIW, this pattern is common to several other Windows APIs, of which the most familiar is probably window messages: if you need a general-purpose window message, *and* you control the application, then you can just pick a message ID in the range `WM_APP` through 0xBFFF. Otherwise, you can use `RegisterWindowMessage()` to generate an ID in the range 0xC000 through 0xFFFF from a string. If those ranges look familiar, it's because `RegisterWindowMessage()` simply calls `GlobalAddAtom()` behind the scenes... ;-)
Shog9
And finally... If you're asking yourself how I can be so sure about this, that Windows won't crash and burn if you use an upper-range ID from outside of a DLL... It's because Windows doesn't *know* whether or not you're calling `RegisterHotKey()` from a DLL. Windows *can't* know, because you don't tell it anywhere - it would have to walk the callstack to determine this, and that's extremely unreliable! If it really needed to know, it would require that you tell it by passing in a `HINSTANCE` identifying the DLL (as is the case with SetWindowHookEx()). The ranges are for your own protection...
Shog9
See also: http://blogs.msdn.com/oldnewthing/archive/2008/04/30/8440201.aspx
Shog9
A: 

Completing the Philip answer:

You need to get a reference of the assembly that is calling your function, so the code should be like this:

Assembly assembly = Assembly.GetCallingAssembly();
Boolean isDll = assembly.EntryPoint == null;

Hope this helps.

Ricardo Lacerda Castelo Branco

Ricardo Lacerda Castelo Branco
If this code is in a public method inside a dll, but called from the executable - what will the first line return?
Philip Wallace