首页 > 解决方案 > 来自特定语言键盘的所有可能输入字符的列表,包括死键字符

问题描述

我想获得可以通过键盘输入的所有可能的字符。例如,如果语言是英语(美国),那么可以通过键盘输入的所有字符的列表。这是我的代码(C#):

Enumerable.Range(char.MinValue, char.MaxValue)
                .Select(n => (char)n)
                .Where(c => IsRepresentable(c, 
keyboardLayout));

但它缺少死键字符。是否有任何解决方案可以获取特定键盘的所有字符列表?这里有几个死键字符:ò、Ò、ô、Ô、ß、ä、Ä

标签: c#

解决方案


这不是一个小问题。我相信至少有两种方法可以解决这个问题,并且都需要使用 Windows API,无论是 Win32 还是单独的键盘 DLL。我已经充实了第二个(更简单?)选项,因为第一个选项对我来说似乎非常复杂:

1.查询键盘布局DLL

每个键盘布局都有自己的 DLL,描述产生每个字符所需的键组合。通过调用 DLL 中的方法,您可以查询布局表以获取完整的详细信息。主要好处是您可以保证获得完全准确的组合键列表。第二个好处是确实需要安装键盘布局并将其设置为查询应用程序的默认值。

Windows 注册表将可用布局的完整列表保存在HKLM\SYSTEM\CurrentControlSet\Control\Keyboard Layouts. 您可以在其中找到 DLL 文件名和显示名称的条目。所有键盘布局 DLL 文件都存储在该%SystemRoot%\system32文件夹下。

有关这个复杂主题的更多信息,请参阅Michael S. Kaplan 的博客存档。一个好的起点是%WINDIR%\system32\kbd*.dll帖子。

2. 详尽地尝试你能想象到的每一个组合键

坦率地说,从我的角度来看,这是一个更容易的解决方案。我们可以使用 Win32 API 调用GetKeyboardLayoutMapVirtualKeyToUnicodeEx在其中User32.dll模拟按键组合并查看产生了什么字符。

我们首先尝试所有普通键而不尝试死键前缀:

TestDeadKeyCombinations(Keys.None);

然后我们尝试为我们已经识别的潜在死键添加前缀,例如Keys.Oem1or Keys.Question

for (int deadKey = (int)Keys.OemSemicolon; deadKey <= (int)Keys.Oem102; deadKey++)
{
    TestDeadKeyCombinations((Keys)deadKey);
}

请注意,数字行符号(例如Shift+5)也可以是死键:

for (int deadKey = (int)Keys.D0; deadKey <= (int)Keys.D9; deadKey++)
{
    TestDeadKeyCombinations((Keys)deadKey);
}

然后我们的TestDeadKeyModifiers方法尝试三种死键组合:

  1. 按死键,然后按数字行键 0-9 或 AZ 键:

    for (int key = '0'; key <= 'Z'; key++)
    {
        TestDeadKeyModifiers(deadKey, (Keys)key);
    }
    
  2. 按死键两次:

    TestDeadKeyModifiers(deadKey, deadKey);
    
  3. 按死键,然后按空格键:

    TestDeadKeyModifiers(deadKey, Keys.Space);
    

TestDeadKeyModifiers方法迭代所有的死按键组合,然后是正常按键,同时还尝试 和 的所有变ShiftAltGr

void TestDeadKeyModifiers(Keys deadKey, Keys key)
{
    foreach (var mod1 in _modifierCombinations)
    {
        foreach (var mod2 in _modifierCombinations)
        {
            if (TestKeypress(deadKey, mod1, true))
            {
                TestKeypress(key, mod2, false);
            }
        }
    }
}

我们的TestKeypress方法调用 Win32 API 来模拟按键:

public bool TestKeypress(Keys key, List<Keys> modifiers, bool deadKey)
{
    if (deadKey && key == Keys.None)
    {
        // Try the special case of key on its own (without prior dead key).
        return true;
    }

    byte[] keyboardState = new byte[255];

    foreach (var holdKeyCombination in modifiers)
    {
        keyboardState[(int)holdKeyCombination] = 0x80;
    }

modifiers现在我们已经准备好模拟键盘,并通过设置字节的高位来模拟按住指定的键。接下来,我们使用 将请求的键映射到键盘扫描代码MapVirtualKey。如果我们试图模拟死按键,那么我们设置uMapType2.

    uint virtualKeyCode = (uint)key;

    uint scanCode = MapVirtualKey(virtualKeyCode, (deadKey ? 2U : 0U));

我们请求当前线程的输入语言环境,并且此信息(输入语言和键盘的物理布局)ToUnicodeEx与其他所有信息一起传递给 API 调用:

    IntPtr inputLocaleIdentifier = GetKeyboardLayout(0);

    StringBuilder output = new StringBuilder();

    int result = ToUnicodeEx(virtualKeyCode, scanCode, keyboardState, output,
        (int)5, (uint)0, inputLocaleIdentifier);

最后,我们检查结果,如果我们在输出字符串中有一个字符,则将其视为成功,并将其与用于获取它的组合键一起添加到我们的字符列表中。我们还跟踪方法外的最后一次死键按下,以便我们可以将此函数重用于下一次按键:

    if (result != -1 && output.Length == 1)
    {
        AddSuccess(output[0], key, modifiers);

        _lastDeadKey = Keys.None;
    }
    else if (deadKey && result == -1)
    {
        _lastDeadKeyModifiers = modifiers;
        _lastDeadKey = key;
    }
    else if (!deadKey)
    {
        _lastDeadKey = Keys.None;
    }

    return (result == -1 || output.Length == 1);
}

如果没有产生字符(或多个字符),那么我们忽略结果。例如在United States-International键盘上,按下Shift+6组合键一次作为死键,第二次给我们两个抑扬键^^。我们不想记录这一点,因为Shift+6之后Space给了我们想要的单个抑扬符。

注意:虽然对于某些键盘(例如United States-International),这可能会找到您需要的所有字符,但不能保证这样做。我相当肯定大多数键盘布局将使用附加键作为死键,或作为普通键,或区分左右修饰符,这些都需要满足。

当然,每个 Unicode 字符在技术上都可以通过使用Alt+Numpad键盘组合来通过十进制 Unicode 值检索字符。

完整的 LinqPad 脚本以及用于跟踪生成每个字符的组合键的附加代码在这里:

[DllImport("user32.dll", CharSet = CharSet.Unicode)]
private static extern int ToUnicodeEx(uint virtualKeyCode, uint scanCode,
    byte[] keyboardState,
    [Out, MarshalAs(UnmanagedType.LPWStr, SizeParamIndex = 4)] StringBuilder receivingBuffer,
    int bufferSize, uint flags, IntPtr dwhkl);

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

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


HashSet<char> _typeable = new HashSet<char>();
Dictionary<char, string> _howToObtain = new Dictionary<char, string>();

List<List<Keys>> _modifierCombinations = new List<List<Keys>> {
    new List<Keys>{ },
    new List<Keys>{ Keys.ShiftKey, Keys.LShiftKey },
    new List<Keys>{ Keys.ShiftKey, Keys.RShiftKey },
    new List<Keys>{ Keys.ControlKey,Keys.Menu, Keys.LControlKey, Keys.RMenu },
    new List<Keys>{ Keys.ShiftKey, Keys.LShiftKey,
        Keys.ControlKey, Keys.Menu, Keys.LControlKey, Keys.RMenu }
};

private List<Keys> _modifierKeys = new List<Keys> {
    Keys.ControlKey,
    Keys.ShiftKey,
    Keys.Menu,
    Keys.LShiftKey,
    Keys.RShiftKey,
    Keys.LControlKey,
    Keys.RControlKey,
    Keys.LMenu,
    Keys.RMenu
};

Keys _lastDeadKey = Keys.None;
List<Keys> _lastDeadKeyModifiers;

void Main()
{
    TestDeadKeyCombinations(Keys.None);

    for (int deadKey = (int)Keys.OemSemicolon;
            deadKey <= (int)Keys.Oem102;
            deadKey++)
    {
        TestDeadKeyCombinations((Keys)deadKey);
    }

    for (int deadKey = (int)Keys.D0; deadKey <= (int)Keys.D9; deadKey++)
    {
        TestDeadKeyCombinations((Keys)deadKey);
    }

    // _typeable.OrderBy(x => x).Dump("Typeable characters");

    _howToObtain.OrderBy(x => x.Key).Dump(
        "Characters available through keyboard combinations");

    // Enumerable.Range(32, ((int)char.MaxValue) - 31)
    //  .Select(e => (char)e)
    //  .Except(_typeable)
    //  .OrderBy(x => x)
    //  .Select(x => $"{x} (0x{((int)x).ToString("x4")})")
    //  .Dump("Non-typeable characters");
}

void TestDeadKeyCombinations(Keys deadKey)
{
    for (int key = '0'; key <= 'Z'; key++)
    {
        TestDeadKeyModifiers(deadKey, (Keys)key);
    }

    TestDeadKeyModifiers(deadKey, deadKey);

    TestDeadKeyModifiers(deadKey, Keys.Space);
}

void TestDeadKeyModifiers(Keys deadKey, Keys key)
{
    foreach (var mod1 in _modifierCombinations)
    {
        foreach (var mod2 in _modifierCombinations)
        {
            if (TestKeypress(deadKey, mod1, true))
            {
                TestKeypress(key, mod2, false);
            }
        }
    }
}

public bool TestKeypress(Keys key, List<Keys> modifiers, bool deadKey)
{
    if (deadKey && key == Keys.None)
    {
        // Try the special case of key on its own (without prior dead key).
        return true;
    }

    byte[] keyboardState = new byte[255];

    foreach (var holdKeyCombination in modifiers)
    {
        keyboardState[(int)holdKeyCombination] = 0x80;
    }

    uint virtualKeyCode = (uint)key;

    uint scanCode = MapVirtualKey(virtualKeyCode, (deadKey ? 2U : 0U));
    IntPtr inputLocaleIdentifier = GetKeyboardLayout(0);

    StringBuilder output = new StringBuilder();

    int result = ToUnicodeEx(virtualKeyCode, scanCode, keyboardState, output,
        (int)5, (uint)0, inputLocaleIdentifier);

    if (result != -1 && output.Length == 1)
    {
        AddSuccess(output[0], key, modifiers);

        _lastDeadKey = Keys.None;
    }
    else if (deadKey && result == -1)
    {
        _lastDeadKeyModifiers = modifiers;
        _lastDeadKey = key;
    }
    else if (!deadKey)
    {
        _lastDeadKey = Keys.None;
    }

    return (result == -1 || output.Length == 1);
}

void AddSuccess(char outputChar, Keys key, List<Keys> modifiers)
{
    if (_typeable.Add(outputChar))
    {
        // This is the first time we've been able to produce this character,
        // so store the first (simplest) key combination that produced it
        string hto = string.Empty;

        if (_lastDeadKey != Keys.None)
        {
            hto = ExplainKeyCombo(_lastDeadKey, _lastDeadKeyModifiers);
            hto += " ";
        }

        hto += ExplainKeyCombo(key, modifiers);

        _howToObtain.Add(outputChar, hto);
    }
}

string ExplainKeyCombo(Keys key, List<Keys> modifiers)
{
    string explain = string.Empty;

    if (modifiers.Intersect(
        new List<Keys>{
            Keys.ShiftKey, Keys.LShiftKey
        }).Count() == 2)
    {
        explain += "Shift+";
    }

    if (modifiers.Intersect(
        new List<Keys> {
            Keys.ControlKey, Keys.Menu, Keys.LControlKey, Keys.RMenu
        }).Count() == 4)
    {
        explain += "AltGr+";
    }

    explain += key.ToString();

    return explain;
}

推荐阅读