keybd_event along with PostMessage win32 not working when Visual Studio has focus (or any application run as admin)

These are the VK_MEDIA_NEXT_TRACK and VK_MEDIA_PREV_TRACK virtual keys. The processing for them is highly convoluted:

  • Whatever program owns the foreground window will retrieve his keystroke from its message queue when it calls GetMessage()
  • The TranslateMessage() call in that program’s message loop translates the keystroke to a WM_APPCOMMAND message, APPCOMMAND_MEDIA_NEXTTRACK or APPCOMMAND_MEDIA_PREVIOUSTRACK and sends it to the child window with the focus
  • The child window won’t use it and passes the message to DefWindowProc()
  • Which passes the message to the parent of the child window. This repeats as often as the child windows are nested, eventually reaching the top-level window
  • When it calls DefWindowProc, Windows then calls a shell hook to trigger a callback in any program that has called SetWindowsHookEx() for the WH_SHELL hook, HSHELL_APPCOMMAND notification. A program like Windows Media Player will use that
  • If no hooks intercept it, it will eventually end up in Explorer as the last hook which then finally performs the operation.

The solution here is to not send a keystroke but move up the previously listed processing chain. It would be nice if it were possible to directly call the shell hook but that feature is not exposed. Next step back is to send the WM_APPCOMMAND message instead.

That requires picking a window to send the message to. That’s a difficult these days due to UAC, a feature called UIPI (User Interface Privilege Isolation) prevents a non-elevated program from sending messages to an elevated one. So you can’t just use the window in the foreground, it may well be an app that’s running with admin privileges. Like Visual Studio, the likely reason why PostMessage and keybd_event() is failing when you tried them.

The trick is to send it to a window that you own. Which you made difficult, you are using a console mode project. That can be worked around. Project + Add Reference, select System.Windows.Forms. Add a new class to your project and paste the code shown below. Replace your keybd_event() call with, say,

  AppCommand.Send(AppCommands.MediaNext);  

I threw in the whole kit-and-caboodle of all the commands you can send. I provided a Cleanup() method to release resources, it is not necessary to call it. The code is compatible with .NET version 2.0 through 4.5.1

using System;
using System.Threading;
using System.Windows.Forms;
using System.Runtime.InteropServices;

public enum AppCommands {
    BrowserBack = 1,
    BrowserForward = 2,
    BrowserRefresh = 3,
    BrowserStop = 4,
    BrowserSearch = 5,
    BrowserFavorite = 6,
    BrowserHome = 7,
    VolumeMute = 8,
    VolumeDown = 9,
    VolumeUp = 10,
    MediaNext = 11,
    MediaPrevious = 12,
    MediaStop = 13,
    MediaPlayPause = 14,
    LaunchMail = 15,
    LaunchMediaSelect = 16,
    LaunchApp1 = 17,
    LaunchApp2 = 18,
    BassDown = 19,
    BassBoost = 20,
    BassUp = 21,
    TrebleUp = 22,
    TrebleDown = 23,
    MicrophoneMute = 24,
    MicrophoneVolumeUp = 25,
    MicrophoneVolumeDown = 26,
    Help = 27,
    Find = 28,
    New = 29,
    Open = 30,
    Close = 31,
    Save = 32,
    Print = 33,
    Undo = 34,
    Redo = 35,
    Copy = 36,
    Cut = 37,
    Paste = 38,
    ReplyToMail = 39,
    ForwardMail = 40,
    SendMail = 41,
    SpellCheck = 42,
    Dictate = 43,
    MicrophoneOnOff = 44,
    CorrectionList = 45,
    MediaPlay = 46,
    MediaPause = 47,
    MediaRecord = 48,
    MediaFastForward = 49,
    MediaRewind = 50,
    MediaChannelUp = 51,
    MediaChannelDown = 52,
    Delete = 53,
    Flip3D = 54
}

public static class AppCommand {
    public static void Send(AppCommands cmd) {
        if (frm == null) Initialize();
        frm.Invoke(new MethodInvoker(() => SendMessage(frm.Handle, WM_APPCOMMAND, frm.Handle, (IntPtr)((int)cmd << 16))));
    }

    private static void Initialize() {
        // Run the message loop on another thread so we're compatible with a console mode app
        var t = new Thread(() => {
            frm = new Form();
            var dummy = frm.Handle; 
            frm.BeginInvoke(new MethodInvoker(() => mre.Set()));
            Application.Run();
        });
        t.SetApartmentState(ApartmentState.STA);
        t.IsBackground = true;
        t.Start();
        mre.WaitOne();
    }
    public static void Cleanup() { 
        if (frm != null) {
            frm.BeginInvoke(new MethodInvoker(() => { 
                frm.Close();
                Application.ExitThread();
                mre.Set(); 
            }));
            mre.WaitOne();
            frm = null;
        }
    }

    private static ManualResetEvent mre = new ManualResetEvent(false);
    private static Form frm;

    // Pinvoke
    private const int WM_APPCOMMAND = 0x319;
    [DllImport("user32.dll")]
    private static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wp, IntPtr lp);
}

Leave a Comment