Mihail Shishkov
Mihail Shishkov

Reputation: 15797

how to get display name of the current selected keyboard layout

I need to display a list of input languages to my users the way the language bar displays them.

For example:

enter image description here

Currently I have

class Program
{
    static void Main(string[] args)
    {
        var langs = InputLanguage.InstalledInputLanguages;

        foreach (InputLanguage lang in langs)
        {
            Console.WriteLine(lang.LayoutName);
        }
    }
}

This prints the following

US
Bulgarian
Bulgarian

As you can see we can't tell the difference between the two "Bulgarians" which one is BGPT and which one is BG only.

Culture is also the same for both. The only difference is the lang.Handle which is of type IntPtr. I suppose I have to P/Invoke some win32 API using the handle to get to that display name.

Any ideas of what it might be?

EDIT:

Executing Get-WinUserLanguageList in powershell displays the following

LanguageTag     : en-US
Autonym         : English (United States)
EnglishName     : English
LocalizedName   : English (United States)
ScriptName      : Latin
InputMethodTips : {0409:00000409}
Spellchecking   : True
Handwriting     : False

LanguageTag     : bg
Autonym         : български
EnglishName     : Bulgarian
LocalizedName   : Bulgarian
ScriptName      : Cyrillic
InputMethodTips : {0402:00040402, 0402:00030402}
Spellchecking   : True
Handwriting     : False

Input method tips is the key here.

0402:00040402 is BG and 0402:00030402 is BGPT

Upvotes: 2

Views: 1123

Answers (2)

DJm00n
DJm00n

Reputation: 1421

This is bug in InputLanguage.LayoutName implementation that was already fixed: https://github.com/dotnet/winforms/pull/8439

You can get the idea from fixed code:

private static string KeyboardLayoutsRegistryPath => @"SYSTEM\CurrentControlSet\Control\Keyboard Layouts";

public string LayoutName
{
    get
    {
        // There is no good way to do this in Windows. GetKeyboardLayoutName does what we want, but only for the
        // current input language; setting and resetting the current input language would generate spurious
        // InputLanguageChanged events.
        // Try to extract needed information manually.
        string layoutName = GetKeyboardLayoutNameForHKL(_handle);

        // https://learn.microsoft.com/windows/win32/intl/using-registry-string-redirection#create-resources-for-keyboard-layout-strings
        using RegistryKey? key = Registry.LocalMachine.OpenSubKey($@"{KeyboardLayoutsRegistryPath}\{layoutName}");
        if (key is not null)
        {
            // Localizable string resource associated with the keyboard layout
            if (key.GetValue("Layout Display Name") is string layoutDisplayName &&
                SHLoadIndirectString(ref layoutDisplayName))
            {
                return layoutDisplayName;
            }

            // Fallback to human-readable name for backward compatibility
            if (key.GetValue("Layout Text") is string layoutText)
            {
                return layoutText;
            }
        }

        return SR.UnknownInputLanguageLayout;
    }
}

internal static string GetKeyboardLayoutNameForHKL(IntPtr hkl)
{
    // According to the GetKeyboardLayout API function docs low word of HKL contains input language.
    int language = PARAM.LOWORD(hkl);

    // High word of HKL contains a device handle to the physical layout of the keyboard but exact format of this
    // handle is not documented. For older keyboard layouts device handle seems contains keyboard layout
    // language which we can use as KLID.
    int device = PARAM.HIWORD(hkl);

    // But for newer keyboard layouts device handle contains layout id if its high nibble is 0xF. This id may be
    // used to search for keyboard layout under registry.
    // NOTE: this logic may break in future versions of Windows since it is not documented.
    if ((device & 0xF000) == 0xF000)
    {
        // Extract layout id from the device handle
        int layoutId = device & 0x0FFF;

        using RegistryKey? key = Registry.LocalMachine.OpenSubKey(KeyboardLayoutsRegistryPath);
        if (key is not null)
        {
            // Match keyboard layout by layout id
            foreach (string subKeyName in key.GetSubKeyNames())
            {
                using RegistryKey? subKey = key.OpenSubKey(subKeyName);
                if (subKey is null)
                {
                    continue;
                }

                if (subKey.GetValue("Layout Id") is not string subKeyLayoutId)
                {
                    continue;
                }

                if (layoutId == Convert.ToInt32(subKeyLayoutId, 16))
                {
                    Debug.Assert(subKeyName.Length == 8, $"unexpected key length in registry: {subKey.Name}");
                    return subKeyName;
                }
            }
        }
    }
    else
    {
        // Keyboard layout language overrides input language, if available. This is crucial in cases when
        // keyboard is installed more than once or under different languages. For example when French keyboard
        // is installed under US input language we need to return French keyboard name.
        if (device != 0)
        {
            language = device;
        }
    }

    return language.ToString("x8");
}

After you have to combine it into one string $"{InputLanguage.Culture.DisplayName} - {InputLanguage.LayoutName} keyboard"

Upvotes: 2

Rob
Rob

Reputation: 648

If PowerShell has what you are looking for you could always just get it from there. Add reference to C:\Program Files (x86)\Reference Assemblies\Microsoft\WindowsPowerShell\3.0\System.Management.Automation.dll and C:\Windows\Microsoft.NET\assembly\GAC_MSIL\Microsoft.InternationalSettings.Commands\v4.0_3.0.0.0__31bf3856ad364e35\Microsoft.InternationalSettings.Commands.dll

class Program
{
    static void Main(string[] args)
    {
        System.Management.Automation.PowerShell ps = System.Management.Automation.PowerShell.Create();
        List<Microsoft.InternationalSettings.Commands.WinUserLanguage> userLangList = ps.AddCommand("Get-WinUserLanguageList").Invoke()[0].BaseObject as List<Microsoft.InternationalSettings.Commands.WinUserLanguage>;
        foreach (Microsoft.InternationalSettings.Commands.WinUserLanguage userLang in userLangList)
        {
            Console.WriteLine("{0,-31}{1,-47}", "Antonym", userLang.Autonym);
            Console.WriteLine("{0,-31}{1,-47}", "EnglishName", userLang.EnglishName);
            Console.WriteLine("{0,-31}{1,-47}", "Handwriting", userLang.Handwriting);
            Console.WriteLine("{0,-31}{1,-47}", "InputMethodTips", String.Join(",", userLang.InputMethodTips));
            Console.WriteLine("{0,-31}{1,-47}", "LanguageTag", userLang.LanguageTag);
            Console.WriteLine("{0,-31}{1,-47}", "LocalizedName", userLang.LocalizedName);
            Console.WriteLine("{0,-31}{1,-47}", "ScriptName", userLang.ScriptName);
            Console.WriteLine("{0,-31}{1,-47}", "Spellchecking", userLang.Spellchecking);
            Console.WriteLine();
        }
    }
}

Alternately, Microsoft gets much of this information from the registry, you could do the same:

class Program
{
    static void Main(string[] args)
    {
        (new System.Security.Permissions.RegistryPermission(System.Security.Permissions.PermissionState.Unrestricted)).Assert();

        Microsoft.Win32.RegistryKey rkLanguages = Microsoft.Win32.Registry.CurrentUser.OpenSubKey("Control Panel\\International\\User Profile");
        foreach (string str in rkLanguages.GetSubKeyNames())
        {
            Console.WriteLine(str);
            Microsoft.Win32.RegistryKey rkLang = rkLanguages.OpenSubKey(str);
            foreach (string value in rkLang.GetValueNames())
            {
                if (rkLang.GetValueKind(value) == Microsoft.Win32.RegistryValueKind.DWord)
                {
                    string blah = String.Concat("SYSTEM\\CurrentControlSet\\Control\\Keyboard Layouts\\", value.Split(new char[] { ':' })[1]);
                    Microsoft.Win32.RegistryKey rkKeyboardLayout = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(blah);
                    Console.WriteLine(rkKeyboardLayout.GetValue("Layout Text"));
                }
            }
            Console.WriteLine();
        }

        System.Security.CodeAccessPermission.RevertAssert();
    }
}

Upvotes: 4

Related Questions