2

This is a program that I have used with many changes from the old xp day's It's a cmd line program that will change track in media applications(Spotify,vlc,mediaPlayer) just like keyboards with next/previous track buttons.

Currently im using Microsoft natural keyboard that does not have those buttons but have programmable keys that execute this prog.

This all works EXCEPT when Visual Studio 2012/2013 has the focus (Windows 7) (haven't tried other versions), and it works in Sql management studio.

using System;
using System.Runtime.InteropServices;

namespace NxtTrack
{    
class Program
{
    [DllImport("user32.dll")]
    private static extern bool PostMessage(IntPtr hWnd, uint Msg, UIntPtr wParam, IntPtr lParam);
    [DllImport("user32.dll")]
    public static extern void keybd_event(byte vkCode, byte scanCode, int flags, IntPtr extraInfo);

    enum TrackMove
    {
        Previous,Next
    }

    static void Main(string[] args)
    {

        TrackMove trackMove;

        try
        {
            if(args[0].ToLower().Contains("previous"))
                trackMove = TrackMove.Previous;
            else if(args[0].ToLower().Contains("next"))
                trackMove = TrackMove.Next;
            else
            {
                throw new Exception("wrong param");
            }
        }
        catch
        {
            Console.WriteLine("Params needed: Next or Previous");
            return;
        }
        TrackKeys(trackMove);
    }

    private static void TrackKeys(TrackMove trackMove)
    {
        //http://msdn.microsoft.com/en-us/library/dd375731%28v=VS.85%29.aspx

        byte msg = trackMove == TrackMove.Previous ? (byte)0xB1 : (byte)0xB0;
        keybd_event(msg, 0x45, 0, IntPtr.Zero);
    }
}
}
Archlight
  • 2,019
  • 2
  • 21
  • 34

2 Answers2

7

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);
}
Hans Passant
  • 922,412
  • 146
  • 1,693
  • 2,536
-1

The problem is VS has the focus when the message is sent, you can only put this to work with the focus on the correct application.

To work around this put a Thread.Sleep before the call to keybd_event and F5 before the sleep and change the focus to the correct place in this time.

Pedro.The.Kid
  • 1,968
  • 1
  • 14
  • 18
  • This application works without focus. Its just simulating a keyboard event and works with any application having focus, exept when visual studio is the focused application. – Archlight Jan 27 '14 at 16:23
  • Yes because VS ignores messages from the debugged application when its debugging it if you run the application unattached from VS the message will be processed. the same happens when you are in a breakpoint and click or try to move the windows of the attached process. – Pedro.The.Kid Jan 27 '14 at 16:38
  • Ahh but this application is not being run in VS. And just VS having focus NOT running in debug or anything(I accept that VS would intercept this in debug mode) kills this media event. But still the volume buttons work and pause/start hardware buttons. So there must be somthing the hardware buttons are doing that im not doing in software. – Archlight Jan 27 '14 at 17:45