Shaddapayaface!

If you've got music playing on your PC and then you lock the desktop, the music keeps playing. Sometimes this is what you want, and sometimes it's not...

I was dogfooding the recent release of a certain media playing software not so long ago, which meant I had stuff playing more of the time than typical. One annoyance that kept bugging me was that I had to remember to pause the music player before locking (Win+L) the desktop, so as not to annoy while I was away from my desk (and to not miss the rest of an album). As far as I can tell, most music players keep on playing under a lock screen, which is the right thing in many cases, but not here... This was an itch I needed to scratch and, just for fun, I thought I'd write it in C (not even C++) to get away from all this new fangled object-oriented language stuff I've been doing recently. (Yes, I know, you can write OO in C, but you know what I mean). Just to make it more interesting, I decided not to use any of the standard C runtime libraries, and top make this application as small as I could without having to resort to assembler. Of course, all this meant that an hour's work took probably a day from start to finish!

Given all that, there are two sections in this blog post: first, the one that actually addresses the problem I set out to address, namely getting the music player to stop playing; the second shorter part covers the tricks to make the application binary and running footprint small.

To begin, we need to detect when a workstation is being locked (and unlocked, of course): WTRegisterSessionNotification is the function to look at. After you call this, the window you designate will be sent messages when login sessions on the PC change state: we're interested in the lock and unlock notifications. The next part is to control the music player: I've been deliberately vague about which player I'm interested in (well, apart from that obvious link above), and that's because the technique I used works with most players. Windows supports a WM_APPCOMMAND message type which, as the name suggests, is used to ferry "application level" messages to and from various applications, such as copy, paste, change volume and, as desired here, pause and play. (Incidentally, these types of message are what the additional keys on "multimedia" keyboards use to perform their functions.) Most modern media players recognise the app command messages for media control and respond appropriately, so all I've got to do is send those messages to the media player. How do I determine which window to send the messages to? A clever search based on window title or class? Nope, I just broadcast to all top level windows that happen to be running... Here's my window procedure:

 LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    switch (msg)
    {
    case WM_CREATE:
        WTSRegisterSessionNotification(hwnd, NOTIFY_FOR_THIS_SESSION);
        /* Make it go very quiet... */
        SetProcessWorkingSetSize( GetCurrentProcess(), (SIZE_T)-1, (SIZE_T)-1 );
        break;
 
    case WM_DESTROY:
        WTSUnRegisterSessionNotification(hwnd);
        break;
 
    case WM_WTSSESSION_CHANGE:
        if (wParam == WTS_SESSION_LOCK || wParam == WTS_SESSION_UNLOCK)
        {
            int start = wParam == WTS_SESSION_UNLOCK;
            PostMessage(HWND_BROADCAST, WM_APPCOMMAND, 0,
                MAKELONG(0, start ? APPCOMMAND_MEDIA_PLAY : APPCOMMAND_MEDIA_PAUSE));
            if(!start)
                PostMessage(HWND_TOPMOST, WM_SYSCOMMAND, SC_MONITORPOWER, 2);
        }
        break;
    }
    return DefWindowProc(hwnd, msg, wParam, lParam);
}

I register for session notifications in the WM_CREATE handler. I also reduce the working set (roughly speaking, working memory) at that point too, to let the system release any memory I might have used for initialisation (and that I won't need to use again) - I suspect that really doesn't make much difference these days, and I've been too lazy to actually measure its impact in any case. For tidiness, I unregister in the destroy handler - probably not necessary since the process is exiting at that point anyway, but I might as well be tidy! You can see in the WM_WTSESSION_CHANGE handler how I create the correct media control message and then broadcast it system wide. (Note that my flag is an int here, not a bool - remember, I said I was writing C, not C++.) In that handler too, when I detect a session lock, I send an additional message, a shut down monitor sys command message: this turns the monitor off, saving a little power. After all, if I'm locking the workstation, it's probably because I'm wandering off, or at least not looking at the screen.

I'm not going to bother to describe how to create the window for which this is the window procedure: that sort of thing is covered in many places, such as the venerable Windows programming books by Charles Petzold (I cut my Windows teeth on the 3.1 version of that book - how old does that make me feel?) - if someone really really does want the whole story, ask in the comments... Rather than have the window cluttering up the desktop, I never actually show it (after all, this application needs no UI to work), but I do create a notification area icon to remind me that it's running, and to also give me a means of exiting the program without resorting to Task manager. Again, that's covered in lots of places so I won't describe it here: just do a search for Shell_NotifyIcon. One thing, however, that is very important and that few people who use notification area icons seem to neglect, is re-registering the icon when Windows Explorer restarts. Explorer doesn't crash often (and, when it does, it's mainly my fault because I've screwed up some Explorer add-in that I'm working on) but it is annoying to lose some notification area icons after a restart. What one should do is handle the taskbar created message: this doesn't have a WM_ constant definition, but instead the message number must be retrieved via RegisterWindowMessage, as in:

 UINT WM_TASKBAR_CREATED;
...
WM_TASKBAR_CREATED = RegisterWindowMessage(_T("TaskbarCreated"));

Then, in the windows procedure, add a default case to check for this message:

     default:
        if (msg == WM_TASKBAR_CREATED)
            /* Do whatever you need to register the icon again */;
        break;

(Because the message isn't a constant, you can't have a case specifically for it, so this is the best one can do.)

Now, on to the second part, making the application small. By default, Visual Studio C(++) projects link with the standard libraries (sometimes known as the CRT). Since I'm not using anything at all from them, I can ignore them. If you look at the CRT source for how the WinMain function you define is called, you'll see that there's a "main" (actually several of them) within the CRT which set up heaps, debug support and a bunch of other things before calling WinMain. None of that is necessary here, apart from calling ExitProcess when my application wants to exit. (Heck, that might not even be necessary but, as with unregistering the session messages, it's probably good to be tidy.) I've replaced all of that with:

 void __cdecl Startup(void)
{
  ExitProcess(Main(GetModuleHandle(NULL)));
}

where Main is my stripped down WinMain which takes only the program HINSTANCE - I don't care about any of the other parameters WinMain normally gets. My Main looks like:

 UINT Main(HINSTANCE hInstance)
{
  MSG msg;
  HWND hwnd = Initialise(hInstance); /* Create my window and notification icon here */
 
  if(!hwnd)
    return 0;
 
  /* Standard message loop */
  while (GetMessage(&msg, NULL, 0, 0))
  {
     TranslateMessage(&msg);
    DispatchMessage(&msg);
  }
 
  /* ... delete notification area icon here ... */
 
  return (UINT)msg.wParam; /* Doesn't really achieve anything, but is the conventional return protocol */
}

Next, you need to change the linker settings to invoke Startup instead of the default CRT initialisation call. After all of that, the binary is all of 9.5KB, about half of which is the notification area icon and version information, which is about as small as you can go these days, and a lot smaller than a typical "hello world" program which does link to the CRT. Completely pointless in these days of multi-GB RAM and TB disks, but it was fun.