Getting the Keyboard Layout in WPF C#

前端 未结 2 881
自闭症患者
自闭症患者 2021-01-26 13:03

I need to get the Current Windows Keyboard Layout for my WPF application to map each key correctly and handle AZERTY as well as QWERTY and QWERTZ (and so on...)

I notic

相关标签:
2条回答
  • 2021-01-26 13:29

    The following PowerShell1 script declares Get-KeyboardLayoutForPid function which reliably gets the Current Windows Keyboard Layout for any process2.

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

    The Get-KeyboardLayoutForPid function contains a Comment-Based Help placed at the end of its body.
    I hope that its principle is implementable in C# easy.

    The main idea of my approach:

    1. Suppose that the current keyboard layout (CurrentInputLanguage) for a given process is the (user-dependant) default one (DefaultInputLanguage).
    2. Collect keyboard layouts associated with every thread of given process (allLayoutsRaw). Note this trick for a console application: collect keyboard layouts associated with every thread of child conhost process as well.
    3. If there is a keyboard layout different from DefaultInputLanguage in the allLayoutsRaw collection then it's the sought-after one (retLayouts).

    1 Does not work in PowerShell Core (pwsh.exe).

    2 Might fail for explorer process in a language multifarious environment, see an example of failing scenario in the Comment-Based help.

    0 讨论(0)
  • 2021-01-26 13:41

    I am not sure of the ask - whether you want to know the current keyboard layout or you want to set the keyboard layout.

    In both cases, InputLanguageManager should help.

    You can try setting input language manager to appropriate cultureInfo object. This should change the keyboard layout for your WPF application

    InputLanguageManager.Current.CurrentInputLanguage = new CultureInfo("fr-FR");
    
    0 讨论(0)
提交回复
热议问题