Search code examples
c#.netwinformsvisual-studiovisual-studio-extensions

Bind Mouse Side Buttons to VisualStudio Actions


I try to redirect the XButton 1 and 2 (Side Buttons of the mouse) to specific Visual Studio actions.

When I press the XButton1 I want to compile the project / build it. This action is bound to F6 by default.

When I press the XButton2 I want to switch between code and design view (WinForms). This is bound to F7.

After several attempts using using Visual Studio built-in tools, I created the following script using AutoHotKey:

XButton2:: 
IfWinActive Microsoft Visual Studio
{ 
  Send {F7}

  return 
}


XButton1:: 
IfWinActive Microsoft Visual Studio
{ 
  Send {F6}

  return 
} 

However I am wondering if someone knows a native way of achieving the same with Visual Studio 2015?


Solution

  • Solution

    The main idea is registering a global mouse hook and handling desired mouse event and run a visual studio command. To do so:

    1. Start by creating a Visual Studio Package project.
    1. Register a global mouse hook using SetWindowsHookEx by passing WH_MOUSE_LL and handle desired mouse event, for example WM_XBUTTONDOWN. Perform registration when the solution loads.
    1. Run desired Visual Studio Command using DTE.ExecuteCommand passing suitable command, for example Build.BuildSolution:

      var dte = (EnvDTE.DTE)this.GetService(typeof(EnvDTE.DTE));
      dte.ExecuteCommand("Build.BuildSolution");
      
    1. Don't forget to unhook using UnhookWindowsHookEx when the solution closed.

    Note:

    • To find the command that you need, go to Tools → Options → Environment → KeyBoard and find the command which you need.

    • You will find lots of resources about how to register a global mouse hook like this which I changed a bit and used for test. At the end of the post you can find full source code.

    • In Visual Studio 2013 Add ins are deprecated, so while you can do the same using a Visual Studio Add-in project but it's better to do it using VSPackages.

    Implementaion

    Start by creating a Visual Studio Package project and change the code of package to the code which I post here. Also add the classes which I used for global mouse hook and windows API to the solution.

    Package

    Here is the full code for my Package:

    using Microsoft.VisualStudio.Shell;
    using System;
    using System.Runtime.InteropServices;
    
    [PackageRegistration(UseManagedResourcesOnly = true)]
    [InstalledProductRegistration("#110", "#112", "1.0", IconResourceID = 400)]
    [Guid(GuidList.guidVSPackage1PkgString)]
    [ProvideAutoLoad(Microsoft.VisualStudio.Shell.Interop.UIContextGuids80.SolutionExists)]
    public sealed class VSPackage1Package : Package
    {
        public VSPackage1Package() { }
        EnvDTE.DTE dte;
        protected override void Initialize()
        {
            base.Initialize();
            dte = (EnvDTE.DTE)this.GetService(typeof(EnvDTE.DTE));
            dte.Events.SolutionEvents.Opened += SolutionEvents_Opened;
            dte.Events.SolutionEvents.AfterClosing += SolutionEvents_AfterClosing;
        }
        void SolutionEvents_AfterClosing() { MouseHook.Stop(); }
        void SolutionEvents_Opened()
        {
            MouseHook.Start();
            MouseHook.MouseAction += MouseHook_MouseAction;
        }
        void MouseHook_MouseAction(object sender, EventArgs e)
        {
            dte.ExecuteCommand("Build.BuildSolution");
        }
    }
    

    Windows API messages, structures and methods

    using System;
    using System.Runtime.InteropServices;
    public class Win32
    {
        public delegate IntPtr LowLevelMouseProc(int nCode, IntPtr wParam, IntPtr lParam);
        public const int WH_MOUSE_LL = 14;
        public enum MouseMessages
        {
            WM_LBUTTONDOWN = 0x0201, WM_LBUTTONUP = 0x0202,
            WM_MOUSEMOVE = 0x0200, WM_MOUSEWHEEL = 0x020A,
            WM_RBUTTONDOWN = 0x0204, WM_RBUTTONUP = 0x0205
        }
        [StructLayout(LayoutKind.Sequential)]
        public struct POINT { public int x; public int y; }
        [StructLayout(LayoutKind.Sequential)]
        public struct MSLLHOOKSTRUCT
        {
            public POINT pt;
            public uint mouseData;
            public uint flags;
            public uint time;
            public IntPtr dwExtraInfo;
        }
        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        public static extern IntPtr SetWindowsHookEx(int idHook, LowLevelMouseProc lpfn,
            IntPtr hMod, uint dwThreadId);
        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern bool UnhookWindowsHookEx(IntPtr hhk);
        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        public static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam,
            IntPtr lParam);
        [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        public static extern IntPtr GetModuleHandle(string lpModuleName);
    }
    

    Global Mouse Hook

    Since I don't have XButton in my mouse, I handled WM_RBUTTONDOWN event.

    using System;
    using System.Diagnostics;
    using System.Runtime.InteropServices;
    public static class MouseHook
    {
        public static event EventHandler MouseAction = delegate { };
        private static Win32.LowLevelMouseProc _proc = HookCallback;
        private static IntPtr _hookID = IntPtr.Zero;
        public static void Start() { _hookID = SetHook(_proc); }
        public static void Stop() { Win32.UnhookWindowsHookEx(_hookID); }
        private static IntPtr SetHook(Win32.LowLevelMouseProc proc)
        {
            using (Process curProcess = Process.GetCurrentProcess())
            using (ProcessModule curModule = curProcess.MainModule)
            {
                var handle = Win32.GetModuleHandle(curModule.ModuleName);
                return Win32.SetWindowsHookEx(Win32.WH_MOUSE_LL, proc, handle, 0);
            }
        }
        private static IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
        {
            if (nCode >= 0 && 
                Win32.MouseMessages.WM_RBUTTONDOWN == (Win32.MouseMessages)wParam)
            {
                Win32.MSLLHOOKSTRUCT hookStruct = 
                    (Win32.MSLLHOOKSTRUCT)Marshal.PtrToStructure(lParam, 
                        typeof(Win32.MSLLHOOKSTRUCT));
                MouseAction(null, new EventArgs());
            }
            return Win32.CallNextHookEx(_hookID, nCode, wParam, lParam);
        }
    }