Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

HotKee macro utility - Assign simple scripts to hotkeys or keywords

0.00/5 (No votes)
18 Aug 2003 1  
Uses Windows hooks to monitor the OS for user-specified hotkeys or keywords, uses Windows messages and mapped memory for IPC.

Sample Image - HotKee.jpg

Introduction

I wrote this utility to allow me to implement keyboard macros from any window. I have several macros in Visual Studio that I like, but I can't use them anywhere else. One example is my code comments. When I go into older code and fix bugs (always someone else's code, mind you), I comment the code like this:

// 08/19/03 AlexR: Should have been checking the return value here...

When I would go into our bug tracking system, I would use the same formatted text to precede my comment there. However, I couldn't use the VS macro there, so I had to type out the date and my username manually (oh, the pain!). This and other reasons prompted me to download some shareware macro programs. I found several that were very good, most of which do even more than this little utility. But by writing my own, I was able to learn quite a bit about Windows hooks and memory mapped files, as well as get a good refresher on using Windows messages as a form of interprocess communication (IPC).

This application allows the user to define a simple script (I refer to it as a Macro) and assign it to one or more hotkeys (I call HotKees) and/or keywords (KeeWords). (I chose the name HotKee because it was unique, if not creative.) For instance, one might define a Macro that evaluates to the text, "ABC Broadcasting Networks, LLC", then create a HotKee for LeftWin-A, and then assign that Macro to that HotKee. Then, whenever the user hits LeftWin-A in a window, the said text is inserted. Likewise, he might define a KeeWord for something like, "abnl" and hook it up to the Macro, in which case whenever he typed "abnl", that text would be replaced with the Macro's text. Confused? Me too. :)

There are three main technologies that are at the intermediate level, which this utility employs: Windows hooks, memory mapped files, and Windows messages (arguably intermediate).

I. Windows hooks

By far the biggest challenge of this project for me was to figure out how hooks worked. There are some great articles here at CodeProject explaining the use of them, and some good info at MSDN as well. The real forehead-slapper was realizing that another process cannot call into my executable using LoadLibrary.

My problem was getting the OS to call my code whenever it got a message, so that I could tell whether the message was one of my user's predefined hotkeys or whether it was the end of a predefined keyword. So I used this code:

HHOOK hKeyboardHook = SetWindowsHookEx (WH_GETMESSAGE, 
                   CMyApp::MessageProc, NULL, 0);

Since I specified 0 for the thread ID, I expected all the threads in the system to call my MessageProc when they got messages. But here's the catch: Calling SetWindowsHookEx () with a 0 for the thread ID instructs every thread to do a LoadLibrary on the DLL you specify, so that they can call into a function in that DLL to preprocess their messages. You cannot load an EXE's code up into your thread's space. So I put the message handler into a DLL and used the following code to get all the threads in the system to load it up:

DWORD CHotKeeDlg::RegisterHook ()
{
  DWORD rc = 0;

  if (IsHookRegistered ()) UnregisterHook ();

  HINSTANCE hinstDLL = LoadLibrary ((LPCTSTR) "HKMSGHND.DLL");

  if (hinstDLL) {

    // "CHotKeeMsgHandlerApp::MessageProc(int,unsigned int,long)"

    HOOKPROC hkProc = 
      (HOOKPROC) GetProcAddress (hinstDLL, (LPCTSTR) 1);     
    ASSERT (hkProc);

    int nHook = WH_GETMESSAGE;
    DWORD dwThreadID = 0;
    if (hkProc) 
       m_pHKData->m_hKeyboardHook = 
         SetWindowsHookEx (nHook, hkProc, 
         hinstDLL, dwThreadID); 
    ASSERT (m_pHKData->m_hKeyboardHook);
  }

  if (!IsHookRegistered ()) rc = GetLastError ();

  return (rc);
}

II. Memory mapped files

Next problem: The main application allows the user to configure all the macros, hotkeys and keywords, but how do I tell each individual instance of the DLL where the data is? My first thought was to save it in a file and send everyone the filename for them to load. However, as you know, disk I/O is one of the biggest slowdowns of any application. Minimizing the amount of disk I/O is usually the first thing done when trying to optimize an application. Add to that the fact that we're looking at anywhere between two and five hundred threads loading up the DLL, so that simply is not an option.

I found a solution in the form of mapped memory. Here is a technology I had never used before, so I was excited about learning it. Basically the way it works is, I ask the OS for a bit of publicly accessible memory, tell it how big I want it, and give it a unique name. The OS responds with a memory address and I can simply write out to that address all the data I want, up to the size I specified. Here's the code:

 CMemFile mf;
 CArchive ar (&mf, CArchive::store);
 m_pHKData->Serialize (ar);
 ar.Close ();
 DWORD dwLength = mf.GetLength ();
 m_hMap = CreateFileMapping ((HANDLE) 0xFFFFFFFF,
        NULL, 
        PAGE_READWRITE, 
        0x0, dwLength + 4, // Extra 4 bytes for the buffer size

        HK_SHARED_MEMORY_FILENAME);
 ASSERT (m_hMap != NULL);
 if (m_hMap) {
 
  m_pMapBase = (BYTE *) MapViewOfFile (m_hMap, 
                      FILE_MAP_WRITE, 0, 0, 0);
  ASSERT (m_pMapBase);
  if (m_pMapBase) {
   memcpy (m_pMapBase, &dwLength, sizeof (dwLength));
   mf.SeekToBegin ();
   mf.Read (m_pMapBase + sizeof (dwLength), dwLength);
  }
 }

Now, all that's left is to tell all those threads that there is memory to be read. This brings us to our next section...

III. Windows messages

There are many forms of IPC that are both easy and efficient. However, in this particular case, I have a single application that is going to be talking to hundreds of other threads on the same machine. The obvious solution in my mind was using user-defined messages. The Windows API has a nice function called UINT RegisterWindowMessage (LPCSTR sMsgName) which "defines a new window message that is guaranteed to be unique throughout the system". This ensures that no other process is going to call Send/PostMessage with our message number. If no message number for the specified sMsgName exists, a new one is registered by the OS and returned. If one already exists, that number is returned. This is very handy, since we need all the instances of the DLL and the main application to send and receive the same message number. The main application simply makes the first call before setting the hook. Once the hook is set, each thread will initialize its own instance of the DLL and those instances will make the call to RegisterWindowMessage, getting the same message number that the main application has. Now we're all speaking the same language!

I didn't want to go message-crazy, so I only registered a single message. The message number indicates that the message is a communication between the HotKee main application and the instances of the DLL. I used the WPARAM to specify the message type, so that the receiving code will know what to do with the message. If any data goes along with the message, it can be passed along inside the LPARAM. The main area I use this is in the main application when I instruct all the instances that new data needs to be read from the memory file. Here's the call:

::PostMessage (HWND_BROADCAST, m_nCommMessage, 
            WPARAM_HK_RELOAD_DATA, (LPARAM) m_hWnd);

Here I'm telling the DLL instances to reload their data from the mapped memory. I use the HWND_BROADCAST constant so that the API will post the message to every message queue in the system. This ensures that every instance of the DLL that has a parent thread with a message pump gets the message. (We're not actually interested in any other threads, since some window always has the focus to receive our HotKees or KeeWords.) I'm also passing along, the main window's handle to the DLL code. This allows the DLL instances to respond to only the main window instead of broadcasting their responses to the entire system, thereby flooding it with thousands of messages.

On the DLL side, messages are sent to both itself and to its parent window to accomplish much of what it does. Why are messages sent to itself? Because, as the DLL is filtering the messages for its parent window, instead of stopping the message pump to execute macros, it simply posts another message on the queue to do the work. This allows any other messages to be processed (like WM_PAINT, etc.) in the meantime. If the message needs to be removed from the queue, that's easy enough: we just set the hwnd and the message number of the message to 0 and pass it along.

Messages are sent from the DLL to its parent window when a Macro is evaluated to some text which needs to be put into the window. Each character of the text is sent to the window as a message for the window to process, as it normally would. I discovered the SendInput() function long after writing the program (today, as a matter of fact), which would probably be a better solution to mimic character messages to a window. If anyone is interested in trying it, I'd like to know how it works out!

Installing and Running the Binaries

Here is a list of files included in the zip file:

  • HotKee.exe - The main application.
  • Hkmsghnd.dll - The DLL containing the message filtering code.
  • HotKee.hlp - Help file (limited, but informative).
  • HotKee.cnt - Contents file for help file... not required.

Simply put all these files into one directory and run the executable. No other modification to your system should be required.

Conclusion

The Windows API is a great tool that allows you to tap much of the power of the operating system on which your applications run. I hope you find this utility both useful and educational. It is wide open for expansion (as most free utilities are!), so if you do make some modifications you're willing to share, let me know!

Above all remember, "Use your powers for good!"

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here