вторник, 5 апреля 2016 г.

Как перекодировать текст с одной раскладки клавиатуры на другую на VB.Net

Собственно суть вопроса в том, как текст, набранный правильно, но при неправильной раскладке клавиатуры перекодировать в нужную раскладку. По сути то же самое, что делает PuntoSwitcher, но только более универсально, для любых раскладок. То что "накопал" по этому вопросу постараюсь предельно просто изложить.

Каждая раскладка имеет собственное уникальное имя. Имя раскладки представляет из себя строку, состоящую из набора цифр. Например для английской раскладки (основной в Windows) это имя - 00000409, для русской - 00000419. С ними мы пока и будем проводить проверку.
Список всех установленных в системе раскладок можно найти в реестре
Code
1
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Keyboard Layouts
Имена вложенных ключей совпадают с именами раскладок. По каждому можно получить дополнительную информацию из параметров, больше всего видимо интересен параметр Layout Text
Кроме того, из параметров ключа
Code
1
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Keyboard Layout\DosKeybCodes
можно получить код для создания экземпляра System.Globalization.CultureInfo, например для вывода на экран имени языка раскладки на этом самом языке, ну и не только разумеется.

Каждая клавиша на клавиатуре имеет свой уникальный код, причем она выдает именно этот код, независимо от раскладок и чего бы то ни было. Клавиатура вообще "не знает" ровным счетом ничего о раскладках или любых других настройках компьютера. Но получив код от клавиатуры, операционная система уже сама интерпретирует его в соответствии с активной раскладкой клавиатуры и таким образом в текстовое поле могут вводиться разные символы, реагируя на одну и ту же клавишу.

Воспользуемся этим обстоятельством и сначала полученный текст перекодируем в коды клавиш с помощью исходной (неправильной раскладки), после чего нам понадобится эти коды перекодировать обратно уже с помощью раскладки правильной.

Нам надо импортировать пространства имен
vb.net
1
2
Imports System.Runtime.InteropServices
Imports System.Text
Для получения кодов клавиш по символам и раскладке нам понадобится следующая функция
vb.net
1
2
3
    <DllImport("user32.dll")> _
    Private Function VkKeyScanEx(ch As Char, dwhkl As IntPtr) As Short
    End Function
Она принимает символ и указатель раскладки. Но у нас раскладки даны не в указателях, а в именах, поэтому для получения указателя по имени нам нужна еще такая функция
vb.net
1
2
3
    <DllImport("user32")>
    Function LoadKeyboardLayout(pwszKLID As String, Flags As UInteger) As IntPtr
    End Function
Первый ее параметр - это как раз имя раскладки, вторму можно передавать ноль.
Еще, для того, чтобы получить символ Юникода из кода клавиши и раскладки нам потребуется импортировать еще одну функцию из библиотеки user32
vb.net
1
2
3
4
5
6
7
8
9
10
    Public Declare Function ToUnicodeEx Lib "user32" (
        wVirtKey As UInteger,
        wScanCode As UInteger,
        lpKeyState As Byte(),
        <Out()>
        <MarshalAs(UnmanagedType.LPWStr, SizeConst:=64)>
        ByVal lpChar As System.Text.StringBuilder,
        cchBuff As Integer,
        wFlags As UInteger,
        dwhkl As IntPtr) As Integer
С импортом пока все. Теперь можно писать основной код. Главная функция, в которой будет происходить вся магия, будет принимать три аргумента: собственно строку для перекодирования, а так же исходную и целевую раскладки в виде указателей.
vb.net
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
    Function ConvertStringByKeyboardLayout(srcStr As String, LayoutFrom As IntPtr, LayoutTo As IntPtr) As String
        Dim result As New StringBuilder
        For Each c As Char In srcStr
            Dim keyScan = VkKeyScanEx(c, LayoutFrom)
            If keyScan = -1 Then
                result.Append(c)
                Continue For
            End If
            Dim keydead = BitConverter.GetBytes(keyScan)(1)
            Dim keyState(255) As Byte
            If keydead And 1 Then
                keyState(16) = 255
            End If
            If keydead And 2 Then
                keyState(17) = 255
            End If
            If keydead And 4 Then
                keyState(18) = 255
            End If
            Dim sbstring As New StringBuilder
            ToUnicodeEx(keyScan, 0, keyState, sbstring, 5, 0, LayoutTo)
            result.Append(sbstring.ToString)
        Next
        Return result.ToString
    End Function
Поясню, что здесь происходит. Поскольку встроенные механизмы операционной системы обрабатывают сигналы с клавиатуры по одному, у нас есть инструменты для обработки отдельных символов, а не всей строки целиком. Таким образом, полученную строку мы обходим посимвольно в цикле, перекодируем каждый символ и добавляем результат в объект StringBuilder под названием result. Сначала нам надо получить код символа и мы для этой цели применяем VkKeyScanEx, передавая ей символ и исходную раскладку клавиатуры. Функция может возвратить либо код символа, либо -1. Последнее означает, что такого символа в данной раскладке нет. Мы проверяем keyScan на равенство -1 и далее можно поступить по-разному, я просто добавляю символ без обработки в выходную коллекцию и перехожу к следующей итерации. Надо сказать, что у такого подхода есть недостатки, поскольку неизвестно, был ли символ перекодирован, а кроме того, многие символы есть в разных раскладках(например знаки препинания) но расположены они в них на разных клавишах, а это может привести к некорректной замене символа. Можно сделать так, чтобы в этом случае выбрасывалось исключение или возвращалась пустая строка. Можно добавить в функцию еще один параметр, который будет определять, как функция должна вести себя в подобных случаях. Тут уже кому как удобнее.
Относительно переменной keyDead. Здесь опять-таки нужно небольшое пояснение. Код любой клавиши на клавиатуре - это один байт. Этого вполне достаточно, поскольку на любой клавиатуре клавиш чуть больше ста, а байт дает 256 комбинаций. Тем не менее скан клавиши имеет тип Short, а не байт и занимает такое число два байта вместо одного. При этом первый байт - это собственно код клавиши, а второй - это флаги клавиш-модификаторов. И в этом втором байте первый бит, установленный в 1 означает, что нажата клавиша Shift (в принципе нас в основном это и интересует, поскольку от этого зависит регистр символа). Второй - Ctrl. Третий - Alt. Четвертый - Hankaku(что-то нужное для ввода символов азиатских алфавитов). Остальные зарезервированы и из назначение зависит от конкретных драйверов.
Таким образом переменная keydead - это как раз байт, с интересующими нас модификаторами.
Байтовый массив keyState служит практически тем же целям, что и keydead только уже для перевода кода клавиши в в символ юникода в новой раскладке. В этом массиве 256 элементов, каждый соответствует конкретной клавише, по коду клавиши находится индекс элемента. Если клавиша зажата, то соответствующий ей байт будет равен 255, в противном случае - 0. Нам нужно указать состояние Shift, Ctrl и Alt, то есть инициировать элементы 16, 17 и 18 соответственно, но только если соответсвующие флажки установлены в keydead.
Далее создаем StringBuilder для получения результата и вызываем ToUnicodeEx, который передает результат в этот StringBuilder, а из него символ записывается в result.

Теперь у нас есть функция для перекодирования строк, но она не очень удобна, поскольку требует передавать указатели на раскладки. Немного упростим вызов, создав функцию, которая будет загружать раскладки по имени.
vb.net
1
2
3
4
5
    Function ConvertStringByKeyboardLayoutName(srcStr As String, strLayoutFrom As String, strLayoutTo As String) As String
        Dim layoutFrom = LoadKeyboardLayout(strLayoutFrom, 0)
        Dim layoutTo = LoadKeyboardLayout(strLayoutTo, 0)
        Return ConvertStringByKeyboardLayout(srcStr, layoutFrom, layoutTo)
    End Function
Ну и теперь можно создать две готовые функции для перекодирования с русской раскладки на английскую и обратно. Зная имена раскладок это сделать совсем несложно.
vb.net
1
2
3
4
5
6
7
    Function ConvertEnToRuByKBL(srcStr As String) As String
        Return ConvertStringByKeyboardLayoutName(srcStr, "00000409", "00000419")
    End Function
 
    Function ConvertRuToEnByKBL(srcStr As String) As String
        Return ConvertStringByKeyboardLayoutName(srcStr, "00000419", "00000409")
    End Function
Вот таким кодом теперь можно протестировать перевод с английской на русскую раскладку
vb.net
1
2
        Console.WriteLine(ConvertEnToRuByKBL(Console.ReadLine()))
        Console.ReadKey()
В дополнение еще следует сказать о том, как можно получить имя активной раскладки клавиатуры.
vb.net
1
2
3
4
5
6
7
8
9
10
11
12
13
    <DllImport("user32")>
    Function GetKeyboardLayoutName(<Out> pwszKLID As StringBuilder) As Boolean
    End Function
 
    Function GetKeyboardLayoutName() As String
        Dim sb As New StringBuilder
        Dim result = GetKeyboardLayoutName(sb)
        If result Then
            Return sb.ToString
        Else
            Return ""
        End If
    End Function
Вторая функция - просто удобная оболочка для первой. Она возвращает имя текущей раскладки. Правда в консольном приложении она будет возвращать имя раскладки, в которой было запущено приложение, так что в консольном приложении ее лучше не использовать.
Можно так же сразу загрузить текущую раскладку с помощью
vb.net
1
    Public Declare Function GetKeyboardLayout Lib "user32" (ByVal idThread As UInteger) As IntPtr
Она принимает ид потока, для активного потока надо передать 0. Но в консольном приложении результат будет тем же, что и для предыдущей функции.
Есть еще функция, возвращающая все активные раскладки системы.
vb.net
1
2
3
    <DllImport("user32.dll")>
    Function GetKeyboardLayoutList(nBuff As Integer, <Out> lpList As IntPtr()) As UInteger
    End Function
Примерный вариант использования
vb.net
1
2
3
4
        Dim len = GetKeyboardLayoutList(0, Nothing)
        Dim lll(len - 1) As IntPtr
        GetKeyboardLayoutList(len, lll)
        Array.ForEach(lll, AddressOf Console.WriteLine)
PS
Функции, определенные с помощью атрибута DllImport (а не с помощью Declare) должны быть статическими, следовательно в таком виде, как они представлены здесь из можно использовать только в модуле. В классе они должны объявляться как Shared.

Вот собственно и все.

Комментариев нет :

Отправить комментарий