2

Some keyboard layouts in Windows (e.g., US-QWERTY), treat right Alt as a regular Alt key, while others (e.g., US International) treat it as AltGr and generate both Ctrl and Alt when it is pressed. The Microsoft Keyboard Layout Creator offers a "Right Alt treated as Ctrl+Alt (also known as AltGr)" options to determine the mode used by a given layout.

Is there a way on Windows to determine programmatically which way the currently active keyboard layout treats right Alt?

The on-screen keyboard in Windows 10 appears to differentiate the two (labeling the key either "Alt" or "AltGr" depending on the layout), but I'm not sure if it's determining that through public APIs, through deeper hooks into the OS, or just by having knowledge of the layouts that ship with Windows.

rkjnsn
  • 913
  • 7
  • 16

2 Answers2

2

VkKeyScanExW function has the following Return Value:

Type: SHORT

If the function succeeds, the low-order byte of the return value contains the virtual-key code and the high-order byte contains the shift state, which can be a combination of the following flag bits.

Return value  Description
 1              Either SHIFT key is pressed.
 2              Either CTRL key is pressed.
 4              Either ALT key is pressed.
 8              The Hankaku key is pressed
16              Reserved (defined by the keyboard layout driver).
32              Reserved (defined by the keyboard layout driver).

If the function finds no key that translates to the passed character code, both the low-order and high-order bytes contain –1.

… For keyboard layouts that use the right-hand ALT key as a shift key (for example, the French keyboard layout), the shift state is represented by the value 6, because the right-hand ALT key is converted internally into CTRL+ALT.

The function translates a character to the corresponding virtual-key code and shift state. The function translates the character using the input language and physical keyboard layout identified by the input locale identifier.

Unfortunately, I don't know an inverse function (translate virtual-key code and shift state to the corresponding character using the input language and physical keyboard layout).
The function Get-KeyboardRightAlt defined in the following PowerShell script 58633725_RightAltFinder.ps1 mimics such an inverse function for particular shift state 6 (which corresponds to RightAlt) and for any installed input locale identifier applying brute-force approach: checks output from VkKeyScanExW for each possible character (Unicode range from U+0020 to U+FFFD).

Function Get-KeyboardRightAlt {
[cmdletbinding()]
Param (
    [parameter(Mandatory=$False)] [int32]$HKL=0,
    [parameter(Mandatory=$False)][switch]$All    # returns all `RightAlt` codes
)
begin {
    $InstalledInputLanguages = [System.Windows.Forms.InputLanguage]::InstalledInputLanguages
    if ( $HKL -eq 0 ) {
        $DefaultInputLanguage = [System.Windows.Forms.InputLanguage]::DefaultInputLanguage
        $HKL = $DefaultInputLanguage.Handle
        Write-Warning ( "input language {0:x8}: system default" -f $HKL )
    } else {
        if ( $HKL -notin $InstalledInputLanguages.Handle ) {
            Write-Warning ( "input language {0:x8}: not installed!" -f $HKL )
        }
    }
    $resultFound = $False
    $result = [PSCustomObject]@{
        VkKey         = '{0:x4}' -f 0
        Unicode       = '{0:x4}' -f 0
        Char          = ''
        RightAltKey   = ''
    }
}
Process {
    Write-Verbose ( "input language {0:x8}: processed" -f $HKL )
    for ( $i   = 0x0020;  # Space (1st "printable" character)
          $i -le 0xFFFD;  # Replacement Character
          $i++ ) {
        if ( $i -ge 0xD800 -and # <Non Private Use High Surrogate, First>
                  #   DB7F      # <Non Private Use High Surrogate, Last>
                  #   DB80      # <Private Use High Surrogate, First>
                  #   DBFF      # <Private Use High Surrogate, Last>
                  #   DC00      # <Low Surrogate, First>
                  #   DFFF      # <Low Surrogate, Last>
                  #   E000      # <Private Use, First>
             $i -le 0xF8FF      # <Private Use, Last>
            ) { continue }      # skip some codepoints
        $VkKey = [Win32Functions.KeyboardScan]::VkKeyScanEx([char]$i, $HKL)
        if ( ( $VkKey -ne -1 ) -and ( ($VkKey -band 0x0600) -eq 0x0600) ) {
            $resultFound = $True
            if ( $All.IsPresent ) {
                $result.VkKey       = '{0:x4}' -f $VkKey
                $result.Unicode     = '{0:x4}' -f $i
                $result.Char        = [char]$i
                $result.RightAltKey = [Win32Functions.KeyboardScan]::
                            VKCodeToUnicodeHKL($VkKey % 0x100, $HKL)
                $result
            } else {
                break
            }
        }
    }
    if ( -not ($All.IsPresent) ) { $resultFound }
}
} # Function Get-KeyboardRightAlt
  # abbreviation HKL (Handle Keyboard Layout) = an input locale identifier

if ( $null -eq  ( 'Win32Functions.KeyboardScan' -as [type]) ) {
    Add-Type -Name KeyboardScan -Namespace Win32Functions -MemberDefinition @'

    [DllImport("user32.dll", CharSet = CharSet.Unicode)]
    public static extern short VkKeyScanEx(char ch, IntPtr dwhkl);

    [DllImport("user32.dll")]
    public static extern uint MapVirtualKeyEx(uint uCode, uint uMapType, IntPtr dwhkl);

    [DllImport("user32.dll")]
    static extern int GetKeyNameText(int lParam, 
        [Out] System.Text.StringBuilder lpString,
        int nSize);

    [DllImport("user32.dll")]
    static extern int ToUnicodeEx( 
        uint wVirtKey, uint wScanCode, byte [] lpKeyState, 
        [Out, MarshalAs(UnmanagedType.LPWStr)] System.Text.StringBuilder pwszBuff, 
        int cchBuff, uint wFlags, IntPtr dwhkl);

    [DllImport("user32.dll")]
    public static extern bool GetKeyboardState(byte [] lpKeyState);

    [DllImport("user32.dll")]
    static extern uint MapVirtualKey(uint uCode, uint uMapType);

    [DllImport("user32.dll")]
    public static extern IntPtr GetKeyboardLayout(uint idThread);

    public static string VKCodeToUnicodeHKL(uint VKCode, IntPtr HKL)
    {
        System.Text.StringBuilder sbString = new System.Text.StringBuilder();
        byte[] bKeyState  = new byte[255];
        bool bKeyStateStatus = GetKeyboardState(bKeyState);
        if (!bKeyStateStatus) 
            return "";
        uint lScanCode = MapVirtualKey(VKCode,0);
        uint xflags = 4; // If bit 2 is set, keyboard state is not changed (Windows 10, version 1607 and newer)

        ToUnicodeEx(VKCode, lScanCode, bKeyState, sbString, (int)5, xflags, HKL);
        return sbString.ToString();
    }

    // ↓ for the present: unsuccessful
    public static string VKCodeToUnicodeHKLRightAlt(uint VKCode, IntPtr HKL)
    {
        System.Text.StringBuilder sbString = new System.Text.StringBuilder();
        byte[] bKeyState  = new byte[255];
        // bool bKeyStateStatus = GetKeyboardState(bKeyState);
        // if (!bKeyStateStatus) 
        //    return "";
        bKeyState[118] = 128; // LeftCtrl
        bKeyState[120] = 128; // LeftAlt
        bKeyState[121] = 128; // RightAlt
        uint lScanCode = MapVirtualKey(VKCode,0);
        uint xflags = 4; // If bit 2 is set, keyboard state is not changed (Windows 10, version 1607 and newer)

        ToUnicodeEx(VKCode, lScanCode, bKeyState, sbString, (int)5, xflags, HKL);
        return sbString.ToString();
    // ↑ for the present: unsuccessful
    }
'@
}

if ( -not ('System.Windows.Forms.InputLanguage' -as [type]) ) {
    Add-Type -AssemblyName System.Windows.Forms
}

# EOF 58633725_RightAltFinder.ps1

Output. The function returns either

  • boolean value (true if supplied keyboard layout supports RightAlt functionality, false otherwise), or
  • (with -all switch) an object containing full RightAlt+? list with following properties:
    • VkKey       - (used for debugging),
    • Unicode     - codepoint of resultant character,
    • Char        - resultant character itself,
    • RightAltKey - character pressed while holding RightAlt.

Of course, sometimes a resultant character in some layout can be a deadkey, like diaeresis (¨) in the Russian example below.

. D:\PShell\SO\58633725_RightAltFinder.ps1 # activate the function

Get-KeyboardRightAlt -HKL 0x04090405       # US keyboard
False
Get-KeyboardRightAlt -HKL 0xf0330419       # Russian - Mnemonic
True
Get-KeyboardRightAlt -HKL 0xf0330419 -all  # Russian - Mnemonic
VkKey Unicode Char RightAltKey
----- ------- ---- -----------
06c0  00a8       ¨ ъ          
06de  2019       ’ ь          
0638  20bd       ₽ 8

Another problem is determining currently active keyboard layout. The following PowerShell script declares Get-KeyboardLayoutForPid function which reliably gets the Current Windows Keyboard Layout for any process (described in this my answer):

if ( $null -eq ('Win32Functions.KeyboardLayout' -as [type]) ) {
    Add-Type -MemberDefinition @'
        [DllImport("user32.dll")] 
        public static extern IntPtr GetKeyboardLayout(uint thread);
'@ -Name KeyboardLayout -Namespace Win32Functions
}

Function Get-KeyboardLayoutForPid {
    [cmdletbinding()]
    Param (
        [parameter(Mandatory=$False, ValueFromPipeline=$False)]
        [int]$Id = $PID,
        # used formerly for debugging
        [parameter(Mandatory=$False, DontShow=$True)]
        [switch]$Raw
    )

    $InstalledInputLanguages = [System.Windows.Forms.InputLanguage]::InstalledInputLanguages
    $CurrentInputLanguage    = [System.Windows.Forms.InputLanguage]::DefaultInputLanguage # CurrentInputLanguage
    $CurrentInputLanguageHKL = $CurrentInputLanguage.Handle # just an assumption
    ### Write-Verbose ('CurrentInputLanguage: {0}, 0x{1:X8} ({2}), {3}' -f $CurrentInputLanguage.Culture, ($CurrentInputLanguageHKL -band 0xffffffff), $CurrentInputLanguageHKL, $CurrentInputLanguage.LayoutName)

    Function GetRealLayoutName ( [IntPtr]$HKL ) {
    $regBase = 'Registry::' + 
                'HKLM\SYSTEM\CurrentControlSet\Control\Keyboard Layouts'
    $LayoutHex = '{0:x8}' -f ($hkl -band 0xFFFFFFFF)
    if ( ($hkl -band 0xFFFFFFFF) -lt 0 ) {
        $auxKeyb = Get-ChildItem -Path $regBase | 
            Where-Object { 
            $_.Property -contains 'Layout Id' -and 
                (Get-ItemPropertyValue -Path "Registry::$($_.Name)" `
                    -Name 'Layout Id' `
                    -ErrorAction SilentlyContinue
                ) -eq $LayoutHex.Substring(2,2).PadLeft(4,'0')
        } | Select-Object -ExpandProperty PSChildName
    } else {
        $auxKeyb = $LayoutHex.Substring(0,4).PadLeft(8,'0')
    }
    $KbdLayoutName  = Get-ItemPropertyValue -Path (
        Join-Path -Path $regBase -ChildPath $auxKeyb
    ) -ErrorAction SilentlyContinue -Name 'Layout Text'
    $KbdLayoutName
    # Another option: grab localized string from 'Layout Display Name'
    } # Function GetRealLayoutName

    Function  GetKbdLayoutForPid {
    Param (
        [parameter(Mandatory=$True, ValueFromPipeline=$False)]
        [int]$Id,
        [parameter(Mandatory=$False, DontShow=$True)]
        [string]$Parent = ''
    ) 
    $Processes = Get-Process -Id $Id
    $Weirds  = @('powershell_ise','explorer') # not implemented yet

    $allLayouts = foreach ( $Proces in $Processes ) {
        $LayoutsExtra = [ordered]@{}
        $auxKLIDs = @( for ( $i=0; $i -lt $Proces.Threads.Count; $i++ ) {
            $thread = $Proces.Threads[$i]
            ## The return value is the input locale identifier for the thread:
            $LayoutInt = [Win32Functions.KeyboardLayout]::GetKeyboardLayout( $thread.Id )
            $LayoutsExtra[$LayoutInt] = $thread.Id
            if ($Raw.IsPresent) {
                $auxIndicator = if ($LayoutInt -notin ($CurrentInputLanguageHKL, [System.IntPtr]::Zero)) { '#' } else { '' }
                Write-Verbose ('thread {0,6} {1,8} {2,1} {3,8} {4,8}' -f $i, 
                (('{0:x8}' -f ($LayoutInt -band 0xffffffff)) -replace '00000000'), 
                $auxIndicator,
                ('{0:x}' -f $thread.Id), 
                $thread.Id)
            }
        })
        Write-Verbose ('{0,6} ({1,6}) {2}: {3}' -f $Proces.Id, $Parent, 
            $Proces.ProcessName, (($LayoutsExtra.Keys | 
                Select-Object -Property @{ N='Handl';E={('{0:x8}' -f ($_ -band 0xffffffff))}} | 
                Select-Object -ExpandProperty Handl) -join ', '))
        foreach ( $auxHandle in $LayoutsExtra.Keys ) {
            $InstalledInputLanguages | Where-Object { $_.Handle -eq $auxHandle }
        }
        $ConHost = Get-WmiObject Win32_Process -Filter "Name = 'conhost.exe'"
        $isConsoleApp = $ConHost | Where-Object { $_.ParentProcessId -eq $Proces.Id }
        if ( $null -ne $isConsoleApp ) {
            GetKbdLayoutForPid -Id ($isConsoleApp.ProcessId) -Parent ($Proces.Id -as [string])
        }
    }
    if ( $null -eq $allLayouts ) {
        # Write-Verbose ('{0,6} ({1,6}) {2}: {3}' -f $Proces.Id, $Parent, $Proces.ProcessName, '')
    } else {
        $allLayouts
    }
    } # GetKbdLayoutForPid

    $allLayoutsRaw = GetKbdLayoutForPid -Id $Id
    if ( $null -ne $allLayoutsRaw ) {
        if ( ([bool]$PSBoundParameters['Raw']) ) {
            $allLayoutsRaw
        } else {
            $retLayouts = $allLayoutsRaw | 
                Sort-Object -Property Handle -Unique | 
                Where-Object { $_.Handle -ne $CurrentInputLanguageHKL }
            if ( $null -eq $retLayouts ) { $retLayouts = $CurrentInputLanguage }
            $RealLayoutName = $retLayouts.Handle | 
                ForEach-Object { GetRealLayoutName -HKL $_ }
            $ProcessAux = Get-Process -Id $Id
            $retLayouts | Add-Member -MemberType NoteProperty -Name 'ProcessId' -Value "$Id"
            $retLayouts | Add-Member -MemberType NoteProperty -Name 'ProcessName' -Value ($ProcessAux | Select-Object -ExpandProperty ProcessName )
            # $retLayouts | Add-Member -MemberType NoteProperty -Name 'WindowTitle' -Value ($ProcessAux | Select-Object -ExpandProperty MainWindowTitle )
            $retLayouts | Add-Member -MemberType NoteProperty -Name 'RealLayoutName' -Value ($RealLayoutName -join ';')
            $retLayouts
        }
    }
<#

.Synopsis
Get the current Windows Keyboard Layout for a process.

.Description
Gets the current Windows Keyboard Layout for a process. Identify the process
using -Id parameter.

.Parameter Id
A process Id, e.g.
- Id property of System.Diagnostics.Process instance (Get-Process), or
- ProcessId property (an instance of the Win32_Process WMI class), or 
- PID property from "TaskList.exe /FO CSV", …

.Parameter Raw
Parameter -Raw is used merely for debugging. 

.Example
Get-KeyboardLayoutForPid

This example shows output for the current process (-Id $PID). 
Note that properties RealLayoutName and LayoutName could differ (the latter is wrong; a bug in [System.Windows.Forms.InputLanguage] implementation?)

ProcessId      : 2528
ProcessName    : powershell
RealLayoutName : United States-International
Culture        : cs-CZ
Handle         : -268368891
LayoutName     : US

.Example
. D:\PShell\tests\Get-KeyboardLayoutForPid.ps1  # activate the function

Get-Process -Name * | 
    ForEach-Object { Get-KeyboardLayoutForPid -Id $_.Id -Verbose }

This example shows output for each currently running process, unfortunately
even (likely unusable) info about utility/service processes. 

The output itself can be empty for most processes, but the verbose stream
shows (hopefully worthwhile) info where current keboard layout is held.

Note different placement of the current keboard layout ID:
- console application      (cmd, powershell, ubuntu): conhost
- combined GUI/console app (powershell_ise)         : the app itself
- classic  GUI apps        (notepad, notepad++, …)  : the app itself
- advanced GUI apps        (iexplore)               : Id ≘ tab
- "modern" GUI apps        (MicrosoftEdge*)         : Id ≟ tab (unclear)
- combined GUI/service app (explorer)               : indiscernible
- etc…                     (this list is incomplete).

For instance, iexplore.exe creates a separate process for each open window 
or tab, so their identifying and assigning input languages is an easy task.

On the other side, explorer.exe creates the only process, regardless of 
open visible window(s), so they are indistinguishable by techniques used here…

.Example
gps -Name explorer | % { Get-KeyboardLayoutForPid -Id $_.Id } | ft -au

This example shows where the function could fail in a language multifarious environment:

ProcessId ProcessName RealLayoutName Culture     Handle LayoutName 
--------- ----------- -------------- -------     ------ ---------- 
5344      explorer    Greek (220);US el-GR   -266992632 Greek (220)
5344      explorer    Greek (220);US cs-CZ     67699717 US         

- scenario: 
  open three different file explorer windows and set their input languages
  as follows (their order does not matter):
    - 1st window: let default input language   (e.g. Czech, in my case),
    - 2nd window: set different input language (e.g. US English),
    - 3rd window: set different input language (e.g. Greek).
- output:
  an array (and note that default input language window isn't listed).

.Inputs
No object can be piped to the function. Use -Id pameter instead,
named or positional.

.Outputs
[System.Windows.Forms.InputLanguage] extended as follows:
                                     note the <…> placeholder

Get-KeyboardLayoutForPid | Get-Member -MemberType Properties

   TypeName: System.Windows.Forms.InputLanguage

Name           MemberType   Definition
----           ----------   ----------
ProcessId      NoteProperty string ProcessId=<…>
ProcessName    NoteProperty System.String ProcessName=powershell
RealLayoutName NoteProperty string RealLayoutName=<…>
Culture        Property     cultureinfo Culture {get;}
Handle         Property     System.IntPtr Handle {get;}
LayoutName     Property     string LayoutName {get;}

.Notes
To add the `Get-KeyboardLayoutForPid` function to the current scope,
run the script using `.` dot sourcing operator, e.g. as 
. D:\PShell\tests\Get-KeyboardLayoutForPid.ps1

Auhor:     https://stackoverflow.com/users/3439404/josefz
Created:   2019-11-24
Revisions:  

.Link

.Component
P/Invoke

<##>

} # Function Get-KeyboardLayoutForPid

if ( -not ('System.Windows.Forms.InputLanguage' -as [type]) ) {
    Add-Type -AssemblyName System.Windows.Forms
}

# EOF Get-KeyboardLayoutForPid.ps1
JosefZ
  • 28,460
  • 5
  • 44
  • 83
1

Afaik there is no such API that can give you information directly.

As you already found AltGr is actually Ctrl+Alt in Windows. You can query that indirectly by making calls to a ToUnicodeEx for each scan code in range 0x00..0x7f with lpKeyState[VK_MENU] = 0x80 and lpKeyState[VK_CONTROL] = 0x80 (pressed state) to determine if keyboard layout have at least one mapped character key with AltGr modifier.

See Getting all you can out of a keyboard layout, Part #5 by Michael S. Kaplan for more info.

Or you can try to go with a hard way. You can open keyboard layout dll file and parse it manually and inspect KBDTABLES.fLocaleFlags for a KLLF_ALTGR flag. Here is its definition from kbd.h header from Windows SDK:

/*
 * Attributes such as AltGr, LRM_RLM, ShiftLock are stored in the the low word
 * of fLocaleFlags (layout specific) or in gdwKeyboardAttributes (all layouts)
 */
#define KLLF_ALTGR       0x0001

Here is list of keyboards with AltGr flag set that are shipped with Windows.

DJm00n
  • 1,083
  • 5
  • 18