Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
455 views
in Technique[技术] by (71.8m points)

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

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);
    }
}
}
See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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);
}

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...