2

This Windows 11 powershell script is supposed to intercept keyboard shortcut CTRL-SHIFT k and write 'Hotkey pressed!' in the terminal. However it generates this error:

Exception calling "Run" with "1" argument(s): "Object reference not set to an instance of an object."
At C:\Users\desktop-test\hotkey3.ps1:65 char:1
+ [System.Windows.Forms.Application]::Run($syncHash.Window)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : NullReferenceException

Script:


# Define constants
$MOD_CONTROL = 0x0002
$MOD_SHIFT = 0x0004
$VK_K = 0x4B

# Load necessary types
Add-Type -TypeDefinition @"
    using System;
    using System.Runtime.InteropServices;

    public static class Hotkey {
        [DllImport("user32.dll", SetLastError = true)]
        public static extern bool RegisterHotKey(IntPtr hWnd, int id, uint fsModifiers, uint vk);

        [DllImport("user32.dll", SetLastError = true)]
        public static extern bool UnregisterHotKey(IntPtr hWnd, int id);

        public const int WM_HOTKEY = 0x0312;
    }
"@ -Language CSharp

# Create a hidden window for message processing
Add-Type -AssemblyName System.Windows.Forms
$window = New-Object System.Windows.Forms.Form
$window.ShowInTaskbar = $false
$window.WindowState = [System.Windows.Forms.FormWindowState]::Minimized
$window.MinimizeBox = $false
$window.MaximizeBox = $false
$window.Visible = $false

# Register the hotkey
[void][Hotkey]::RegisterHotKey($window.Handle, 1, $MOD_CONTROL -bor $MOD_SHIFT, $VK_K)

# Define the action on hotkey press
$window.Add_KeyDown({
    # Your code here, e.g.:
    Write-Host "Hotkey pressed!"
})

# Unregister the hotkey when the window is closed
$window.Add_FormClosed({
    [void][Hotkey]::UnregisterHotKey($window.Handle, 1)
})

# Display the hidden window
$syncHash = [hashtable]::Synchronized(@{})
# Wait for the WM_HOTKEY message
$window.Add_Load({
    $syncHash.Window = $window
    $window.Activate()
    [void][System.Windows.Forms.Application]::DoEvents()
})

# Run
[System.Windows.Forms.Application]::Run($syncHash.Window)


Command to launch the script assuming the script is in hotkeystack.ps1:

powershell.exe -ExecutionPolicy Bypass -File \Users\supercobra\OneDrive\Desktop\hotkeystack.ps1

I can't find the error. Any help appreciated.

1
  • 1
    I believe the error is happening because $syncHash.Window is null when you are passing it to the function at the end. You did not add anything to the hashtable before that function runs as far as I can see. Try adding $syncHash.Add("Window",$window) to add the $window object to the hashtable before the last line [System.Windows.Forms.Application]::Run($syncHash.Window). That seems to get it to launch for me. However, there seems to be some other issues with it fully recognizing the hotkey presses when I'm running it. Commented Oct 4, 2023 at 21:01

1 Answer 1

1

After a lot of troubleshooting, I think I got something that is working. The importing of the HotKey class did work for me but it took me a bit to realize it was actually failing to register on the keys you specified (CTRL-SHIFT K). To catch this I changed:

# original    
[void][Hotkey]::RegisterHotKey($window.Handle, 1, $MOD_CONTROL -bor $MOD_SHIFT, $VK_K)
# to this
$registered = [bool][Hotkey]::RegisterHotKey($window.Handle, 1,$MOD_CONTROL -bor $MOD_SHIFT, $VK_K)
$errorCode = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error()
if ($registered -eq $false){
    Write-Output "Failed to register HotKey. Error code $errorCode"
    exit
}

Since the RegisterHotKey function returns a bool, we save the return value and if it's false a problem has occurred in registering the hotkey and we exit. I also save the error code which is a win32 error code. Once I saved the error code I discovered that it was failing for me and returning code 1409 which is ERROR_HOTKEY_ALREADY_REGISTERED. I'm not sure if it'll be the same for your system but the hotkey was already registered on mine and was causing it to fail. Changing the $VK_K value to the value for L fixed it, I also tested M (key code values).

I tried using Add_KeyDown but had issues getting it to work from there.

The last part of the code was causing your error that you posted object reference not set to an instance of an object."

[System.Windows.Forms.Application]::Run($syncHash.Window)

$synch.Window is still null at this point. I'm not exactly sure why the hashtable was added but it works for me just passing the original object $window to the run function.

--

From my understanding after some research, the key to capturing hotkeys is overriding the WndProc function. WndProc ("... is a callback function, which you define in your application, that processes messages sent to a window"). So basically, we override that function and just wait for the WM_HOTKEY system message to be passed and do an action based on that. I used this answer to help in defining the Form class with the override function.

The following is working for me and is set for Control + Shift + L:

# Define constants
# https://learn.microsoft.com/en-us/windows/win32/inputdev/wm-hotkey
$MOD_CONTROL = [uint16]0x0002
$MOD_SHIFT = [UInt16]0x0004

# Control + SHIFT + K - Didn't work for me. Looks like it's already a registed hotkey at least on my system.  
# Not sure if it'll be the same for others.
# Produced an error code 1409 when trying to register. (ERROR_HOTKEY_ALREADY_REGISTERED)
# https://learn.microsoft.com/en-us/windows/win32/debug/system-error-codes--1300-1699-
# $VK_K = [uint16]0x4B (letter "K")

# "L" key - worked for me. Tested "M" as well.
# https://learn.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes
$VK_K = [uint16]0x4C # (Letter "L")


# Load necessary types
Add-Type -TypeDefinition @"
    using System;
    using System.Runtime.InteropServices;

    public static class Hotkey {
        [DllImport("user32.dll", SetLastError = true)]
        public static extern bool RegisterHotKey(IntPtr hWnd, int id, uint fsModifiers, uint vk);

        [DllImport("user32.dll", SetLastError = true)]
        public static extern bool UnregisterHotKey(IntPtr hWnd, int id);

    }
"@ -Language CSharp


# used this as reference for overriding the WndProc function for the form
# to capture hotkeys: https://stackoverflow.com/a/59043639/20473839
# Create the C# derived Form
$assemblies = "System.Windows.Forms", "System.Drawing"
$code = @'
using System;
using System.Windows.Forms;
public class MyForm:Form
{    
    // https://wiki.winehq.org/List_Of_Windows_Messages    
    private static int WM_HOTKEY = 0x0312;

    protected override void WndProc(ref System.Windows.Forms.Message msg)
    {   
        // if hotkey has been used do desired action     
        if (msg.Msg == WM_HOTKEY){
            hotKeyAction();
        }       
        base.WndProc(ref msg);           
    } 
    public void hotKeyAction(){       
        Console.WriteLine("HOTKEY PRESSED");
    }
}
'@
Add-Type -ReferencedAssemblies $assemblies -TypeDefinition $code -Language CSharp

# Create a hidden window for message processing
$window = [Myform] @{}
$window.ShowInTaskbar = $false
$window.WindowState = [System.Windows.Forms.FormWindowState]::Minimized
$window.MinimizeBox = $false
$window.MaximizeBox = $false
$window.Visible = $false


# Register the hotkey
# RegisterHotKey will return false on a failed registration
$registered = [bool][Hotkey]::RegisterHotKey($window.Handle, 1,$MOD_CONTROL -bor $MOD_SHIFT, $VK_K)

# if hot key registration fails, it creates a win32 error. 
# retrieving error reference. https://stackoverflow.com/a/73395877/20473839
$errorCode = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error()
if ($registered -eq $false){
    Write-Output "Failed to register HotKey. Error code $errorCode"
    exit
}

# Unregister the hotkey when the window is closed
$window.Add_FormClosed({
    [void][Hotkey]::UnregisterHotKey($window.Handle, 1)
})

$window.Add_Load({
    $window.Activate()
})

# Run
[System.Windows.Forms.Application]::Run($window)

Some other references I used (one,two, three, four) that I found interesting.

I'm not sure that this is the best way to go about what you want but it does work and is a good enough starting point to improve upon.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.