Using Multimedia Keyboard Keys in Your Own Program

WEBINAR: On-demand webcast

How to Boost Database Development Productivity on Linux, Docker, and Kubernetes with Microsoft SQL Server 2017 REGISTER >

Environment: VS.NET (2003), Windows 2000, Windows XP

When I bought my current computer, it came with a multimedia keyboard that has a lot more keys than usual. There are keys for controlling a media player and some browser keys, such as forward, back, and so forth.

The Windows media player can be controlled by these keys, even if it is in the background; this is a very convenient feature. My version of Winamp sadly does not make use of the keys.

When I looked for a MP3/Ogg player with an integrated media library and available source code (so I could change things if neccesary), I stumbled over Musik. This is a Linux/Windows MP3/Ogg player using wxwindows2 for the GUI and sqlite as the database. It is quite fast, at least faster than mediaplayer and musicmatch jukebox, concerning the startup time and media library browsing. It is available at http://musik.sf.net.

Musik also didn't make use of the special keys, which I got accustomed to, so I started to add this feature for Windows. After reading some MSDN, I found out that you have to handle the WM_APPCOMMAND message, which is available to your program code, if you define _WIN32_WINNT=0x0500 (or higher) in your project. WM_APPCOMMAND can be found in winuser.h, if you have the right SDK headers. VS.NET already has these. For VC 6, I am not quite sure.

Your window proc of your window that should handle the WM_APPCOMMAND message has to do something like the following code does:

if(message == WM_APPCOMMAND)
{
  switch(GET_APPCOMMAND_LPARAM(lParam))
  {
  case APPCOMMAND_MEDIA_NEXTTRACK:
    // do something, which skips the track
    return 1;
  case APPCOMMAND_MEDIA_PREVIOUSTRACK:
    // do something...
    return 1;
  case APPCOMMAND_MEDIA_STOP:
    // do something, which stops playing
    return 1;
  case APPCOMMAND_MEDIA_PLAY_PAUSE:
    // toggle between play and pause
    return 1;
  }
}
// handle other messages or call default window proc.
...

This shows how to use the playing control keys, but you can use all other keys, too, if you use the corresponding APPCOMMAND_XXX code. (See MSDN or winuser.h for more.)

Now, by handling the WM_APPCOMMAND, your program will react to these keys, but only if the window with this windows proc is the active one, or one of its parent windows handles the message. If your program is not active, it will not get the message. So, what to do? The answer is that you have to write a DLL containg a shell hook. Windows will call a shell hook with the hook code equal to HSHELL_APPCOMMAND if the active program doesn't handle the WM_APPCOMMAND message. This hook has to be in a DLL because only then can other processes than your own be hooked.

I have written such a hook DLL. The hook code just sends a WM_APPCOMMAND message every time the hook sees the HSHELL_APPCOMMAND code to a window that has installed the hook, by calling the SetMMShellHook(HWND hWnd) function of the hook DLL. To unregister, UnSetMMShellHook() has to be called. Call SetMMShellHook after creating your window and UnSetMMShellHook before it gets destroyed.

// Hook procedure for Shell hook

LRESULT CALLBACK ShellProc(int nCode, WPARAM wParam, LPARAM lParam)
{
  // Do we have to handle this?
  if (nCode == HSHELL_APPCOMMAND)
  {
    // Process the hook if the hNotifyWnd window handle is valid
    if (hNotifyWnd != NULL)
    {
      short AppCommand = GET_APPCOMMAND_LPARAM(lParam);
      switch (AppCommand)
      {
      case APPCOMMAND_MEDIA_NEXTTRACK:
      case APPCOMMAND_MEDIA_PLAY_PAUSE:
      case APPCOMMAND_MEDIA_PREVIOUSTRACK:
      case APPCOMMAND_MEDIA_STOP:
        ::PostMessage(hNotifyWnd,WM_APPCOMMAND,wParam,lParam);
        return 1; // Don't call CallNextHookEx; instead,
                  // return non-zero, because we have handled
                  // the message (see MSDN doc)

      }
    }
  }

  // Call the next handler in the chain
  return CallNextHookEx (hShellHook, nCode, wParam, lParam);
}

This code only delegates some APPCOMMAD messages to the windows. You can add other ones, if you want, or delegate all, by removing the "switch case" and calling PostMessage every time.

Together with the window proc code above, your program reacts to the keys, even if it is in the background, but only if no application that handles the WM_APPCOMMAND is active. Then the keys will be handled by this application.

Because the hook has to be installed for different processes, the hook DLL makes use of a shared segment.

#pragma data_seg(".shared")
HWND  hNotifyWnd = NULL;
HHOOK hShellHook = NULL;    // Handle to the Shell hook
#pragma data_seg( )

This code declares a segment called ".shared". To let this segment really become shared, you have to add a line in the .def file of the DLL. The linker has to learn of the def file by the /DEF: option.

SECTIONS
  .shared read write shared
Note: The .sln and vcproject files are for VS.NET 2003, but you can use them with VS.NET if you use a text editor to change the version to 7.00 in these files.

Have fun!

Downloads

Download source code - 5 Kb


Comments

  • More appcommand compatibility and bug fix.

    Posted by x87bliss on 07/25/2008 02:15am

    My two problems with the code was that it only supported a hard-coded list of AppCommands. Therefore, would need to be recompiled to support newer ones such as "APPCOMMAND_MEDIA_PLAY" The bug was if the hNotifyWnd did not support one of the hard-coded appcommands, it'd fall back to the shellhook, which would send it back to hNotifyWnd. Resulting in an infinite dribble back and forth of the message. I modified the ShellProc function to support ALL appcommands. As well as prevent this dribble action. Just read the comments for details on what I changed and why.

    LRESULT CALLBACK ShellProc(int nCode, WPARAM wParam, LPARAM lParam)
    {
    	// Do we have to handle this message?
    	if (nCode == HSHELL_APPCOMMAND)
    	{
    		// Process the hook if the hNotifyWnd window handle is valid
    		// Also check that this message is not returned from hNotifyWnd
    		if (hNotifyWnd != NULL && hNotifyWnd != (HWND)wParam)
    		{
    			// Do not forward the current wParam (HWND that originally received the appcommand)
    			// instead, use hNotifyWnd, this way if it's sent back again, it will not go through
    			// a second time
    			::PostMessage(hNotifyWnd,WM_APPCOMMAND,(WPARAM)hNotifyWnd,lParam);
    			return 1; // dont call CallNextHookEx, instead return non-zero, because we have handled the message (see MSDN doc)
    		}
    	}
    
    	// Call the next handler in the chain
        return CallNextHookEx (hShellHook, nCode, wParam, lParam);
    }
    

    Reply
  • Thanks!

    Posted by Legacy on 08/15/2003 12:00am

    Originally posted by: John P

    Good stuff to know. Thanks!

    • this.Handle - can't use System.Windows?

      Posted by dzarn on 05/31/2004 10:57pm

      I'd like a service that registers global hotkeys, but to do this I need a window handle (this.Handle). However, in a Windows Services project, I can't use System.Windows. Any ideas?

      Reply
    Reply
Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • As all sorts of data becomes available for storage, analysis and retrieval - so called 'Big Data' - there are potentially huge benefits, but equally huge challenges...
  • The agile organization needs knowledge to act on, quickly and effectively. Though many organizations are clamouring for "Big Data", not nearly as many know what to do with it...
  • Cloud-based integration solutions can be confusing. Adding to the confusion are the multiple ways IT departments can deliver such integration...

Most Popular Programming Stories

More for Developers

RSS Feeds

Thanks for your registration, follow us on our social networks to keep up-to-date