Skip to content

Commit

Permalink
Use RegLoadMUIString API instead of the SHLoadIndirectString as recom…
Browse files Browse the repository at this point in the history
…mended at https://learn.microsoft.com/windows/win32/intl/locating-redirected-strings#load-a-language-neutral-registry-value

Move InputLanguage GetKeyboardLayoutNameForHKL method implementation to the internal LayoutId property
  • Loading branch information
DJm00n committed Apr 10, 2023
1 parent 8024efa commit e3f9ca9
Show file tree
Hide file tree
Showing 5 changed files with 128 additions and 102 deletions.
2 changes: 1 addition & 1 deletion src/System.Windows.Forms.Primitives/src/NativeMethods.txt
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,7 @@ RegisterClass
RegisterClipboardFormat
RegisterDragDrop
RegisterWindowMessage
RegLoadMUIString
ReleaseCapture
ReleaseStgMedium
RestoreDC
Expand Down Expand Up @@ -574,7 +575,6 @@ SHGetSpecialFolderLocation
ShowCaret
ShowCursor
SHGetSpecialFolderLocation
SHLoadIndirectString
ShowWindow
SHParseDisplayName
SIATTRIBFLAGS
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Buffers;
using Microsoft.Win32;

namespace Windows.Win32
{
internal static partial class PInvoke
{
/// <inheritdoc cref="RegLoadMUIString(System.Registry.HKEY, string, PWSTR, uint, uint*, uint, string)"/>
public static unsafe bool RegLoadMUIString(RegistryKey key, string keyName, out string localizedValue)
{
uint bytes;
WIN32_ERROR error;
var hkey = (System.Registry.HKEY)key.Handle.DangerousGetHandle();
char[] buffer = ArrayPool<char>.Shared.Rent(MAX_PATH + 1);
fixed (char* pszOutBuf = buffer)
{
error = RegLoadMUIString(hkey, keyName, pszOutBuf, (uint)(buffer.Length * sizeof(char)), &bytes, 0, null);
}

// The buffer is too small. Try again with a larger buffer.
if (error == WIN32_ERROR.ERROR_MORE_DATA)
{
ArrayPool<char>.Shared.Return(buffer);
buffer = ArrayPool<char>.Shared.Rent((int)(bytes / sizeof(char)));
fixed (char* pszOutBuf = buffer)
{
error = RegLoadMUIString(hkey, keyName, pszOutBuf, (uint)(buffer.Length * sizeof(char)), &bytes, 0, null);
}
}

localizedValue = new string(buffer, 0, Math.Max((int)(bytes / sizeof(char)) - 1, 0));
ArrayPool<char>.Shared.Return(buffer);

return HRESULT.HRESULT_FROM_WIN32(error).Succeeded;
}
}
}
156 changes: 61 additions & 95 deletions src/System.Windows.Forms/src/System/Windows/Forms/InputLanguage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public sealed class InputLanguage
/// <summary>
/// The HKL handle.
/// </summary>
private readonly IntPtr _handle;
private readonly nint _handle;

internal InputLanguage(IntPtr handle)
{
Expand Down Expand Up @@ -96,7 +96,7 @@ public static unsafe InputLanguageCollection InstalledInputLanguages
}
}

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

/// <summary>
/// Returns the name of the current keyboard layout as it appears in the Windows
Expand All @@ -106,100 +106,82 @@ 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;
using RegistryKey? key = Registry.LocalMachine.OpenSubKey($@"{s_keyboardLayoutsRegistryPath}\{LayoutId}");
return key.GetMUIString("Layout Display Name", "Layout Text") ?? SR.UnknownInputLanguageLayout;
}
}

/// <summary>
/// Returns the keyboard layout name for the provided <see cref="HKL"/> handle.
/// Returns the
/// <see
/// href="https://learn.microsoft.com/windows/win32/api/winuser/nf-winuser-getkeyboardlayoutnamew">
/// keyboard layout identifier</see> of the current input language.
/// </summary>
/// <param name="hkl">The <see cref="HKL"/> input locale identifier representing the input language.</param>
/// <returns>
/// <c>KLID</c> (Keyboard layout ID) string in the same format as in <see
/// href="https://learn.microsoft.com/windows/win32/api/winuser/nf-winuser-getkeyboardlayoutnamew">GetKeyboardLayoutName</see>
/// (8 hex chars with leading zeros when needed).
/// </returns>
/// <seealso href="https://learn.microsoft.com/windows-hardware/manufacture/desktop/windows-language-pack-default-values">
/// <seealso
/// href="https://learn.microsoft.com/windows-hardware/manufacture/desktop/windows-language-pack-default-values">
/// Keyboard identifiers and input method editors for Windows</seealso>
internal static string GetKeyboardLayoutNameForHKL(IntPtr hkl)
internal string LayoutId
{
// 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)
get
{
// Extract layout id from the device handle
int layoutId = device & 0x0FFF;

using RegistryKey? key = Registry.LocalMachine.OpenSubKey(KeyboardLayoutsRegistryPath);
if (key is not null)
// 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.

// 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 identifier.
int device = PARAM.HIWORD(_handle);

// But for newer keyboard layouts device handle contains special 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)
{
// Match keyboard layout by layout id
foreach (string subKeyName in key.GetSubKeyNames())
{
using RegistryKey? subKey = key.OpenSubKey(subKeyName);
if (subKey is null)
{
continue;
}
// Extract special layout id from the device handle
int layoutId = device & 0x0FFF;

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

if (layoutId == Convert.ToInt32(subKeyLayoutId, 16))
using RegistryKey? key = Registry.LocalMachine.OpenSubKey(s_keyboardLayoutsRegistryPath);
if (key is not null)
{
// Match keyboard layout by layout id
foreach (string subKeyName in key.GetSubKeyNames())
{
Debug.Assert(subKeyName.Length == 8, $"unexpected key length in registry: {subKey.Name}");
return subKeyName;
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.ToUpperInvariant();
}
}
}
}
}
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)
else
{
language = device;
// Use input language only if keyboard layout language is not 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 identifier.
if (device == 0)
{
// According to the GetKeyboardLayout API function docs low word of HKL contains input language.
device = PARAM.LOWORD(_handle);
}
}
}

return language.ToString("x8");
return device.ToString("X8");
}
}

/// <summary>
Expand Down Expand Up @@ -253,22 +235,6 @@ public override bool Equals(object? value)
/// <summary>
/// Hash code for this input language.
/// </summary>
public override int GetHashCode() => unchecked((int)(long)_handle);

internal static unsafe bool SHLoadIndirectString(ref string source)
{
var ppvReserved = (void*)IntPtr.Zero;
Span<char> buffer = stackalloc char[512];
fixed (char* pBuffer = buffer)
{
if (PInvoke.SHLoadIndirectString(source, pBuffer, (uint)buffer.Length, ref ppvReserved) == HRESULT.S_OK)
{
source = buffer.SliceAtFirstNull().ToString();
return true;
}
}

return false;
}
public override int GetHashCode() => (int)_handle;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using Microsoft.Win32;

namespace System.Windows.Forms
{
internal static class RegistryKeyExtensions
{
public static string? GetMUIString(this RegistryKey? key, string keyName, string fallbackKeyName)
{
return key is not null
? PInvoke.RegLoadMUIString(key, keyName, out var localizedValue)
? localizedValue
: key.GetValue(fallbackKeyName) is string value
? value
: null
: null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -111,20 +111,21 @@ public void InputLanguage_GetHashCode_Invoke_RemainsSameAcrossCalls()
Assert.Equal(language.GetHashCode(), language.GetHashCode());
}

public static IEnumerable<object[]> GetKeyboardLayoutNameForHKL_TestData()
public static IEnumerable<object[]> InputLanguageLayoutId_TestData()
{
yield return new object[] { unchecked((nint)0x0000000000000409), "00000409" }; // US
yield return new object[] { unchecked((nint)0x0000000004090409), "00000409" }; // US
yield return new object[] { unchecked((nint)0x00000000040c0409), "0000040c" }; // French
yield return new object[] { unchecked((nint)0x00000000040c0409), "0000040C" }; // French
yield return new object[] { unchecked((nint)0xfffffffff0200409), "00011009" }; // Canadian Multilingual Standard
yield return new object[] { unchecked((nint)0xfffffffff0b42400), "000c0c00" }; // Gothic
yield return new object[] { unchecked((nint)0xfffffffff0b42400), "000C0C00" }; // Gothic
}

[Theory]
[MemberData(nameof(GetKeyboardLayoutNameForHKL_TestData))]
public void InputLanguage_GetKeyboardLayoutNameForHKL_Invoke_ReturnsExpected(IntPtr handle, string keyboardName)
[MemberData(nameof(InputLanguageLayoutId_TestData))]
public void InputLanguage_InputLanguageLayoutId_Expected(IntPtr handle, string layoutId)
{
Assert.Equal(keyboardName, InputLanguage.GetKeyboardLayoutNameForHKL(handle));
var language = new InputLanguage(handle);
Assert.Equal(layoutId, language.LayoutId);
}

private static void VerifyInputLanguage(InputLanguage language)
Expand Down

0 comments on commit e3f9ca9

Please sign in to comment.