Введение

Эта статья показывает, как легко можно написать относительно хорошее и приятное приложение виртуальной клавиатуры. Тут использованы техники программирования с использованием как WinApi так и .NET.

Да, это действительно «ещё одно приложение виртуальной клавиатуры» и конечно, читая эту статью, вы можете подумать: «Эй, парень! У нас есть много приложений виртуальной клавиатуры!». Конечно, вы правы, но … но другие виртуальные клавиатуры не имеют такой функции как подсказка следующей наиболее вероятной буквы, после введенной, и выполнены они не в стиле клавиатуры внешне очень напоминающей мне любимую раскладку моего лэптопа:) Samsung R серии. Да, я могу начать рассказывать, как мне не хватало именно такой вот утилитки, но честно говоря, программа писалась чисто из соображений «поразвлечься»! Результат титанической проделанной работы Вы видите в этой статье.

На рисунке выше сразу же показана работа программы. Тут мы видим 2 зеленых подсветки – это сигнализаторы, которые показывают наиболее вероятные буквы (E, Z) относительно только что введенной (D). Подсвечивается именно так, потому что в словаре имеется слово “Codeproject” (видим содержимое файла словаря “dictEN.txt” на заднем плане). Можно заметить, что подсвечиваются буквы с разной яркостью. Это сделано для того, чтобы показать, какая из букв имеет большую вероятность. Работает всё это с использованием статистического анализа и так называемой Марковской модели, но об этом чуть позже…

 

Предыстория. Так рождалась вселенная

Я люблю читать книги и контент сайтов лежа на спине, управляя компьютером с помощью мыши. Не раз сталкивался с ситуацией, когда лежишь себе и смотришь интересный фильм и тут, вдруг слышишь раздражающий звук «О — Ооо» из программы «QIP». Читаешь сообщение и понимаешь, что тебе таки нужно ответить на него фразой вроде «Да, я сплю!», но чтобы сделать это, нужно поднять своё туловище в вертикальное положение (а ведь так лень…). О Боги, мне даже страшно предположить, что может подумать обо мне читатель, но именно лень натолкнула меня на написание программки виртуальной клавиатуры, а позже и вой этой вот статьи.

 

Использование кода

Проект написан с использованием свободной и действительно замечательной среды разработки программного обеспечения «SharpDevelop 3.0.0.3800». Честно говоря, я делал все возможное, чтобы писать интуитивно понятный и приятный код с достаточным количеством комментариев (ИМХО). Я не применяю «рефакторинг» C-стиля, вроде этого:

  • Было: getSortedIndexesBubblesort1D
  • Переделали: gtStdIdxBblrt1D

Надеюсь, читатель будет чувствовать себя комфортно, читая написанный мной исходный код. Приступим к нему… Главными проблемами, с которыми я столкнулся, были:

  • Разработка интерфейса без каких-либо нежелательных сдвигов между кнопками
  • Создание матрицы Марковской цепи зависимостей
  • Управление фокусом приложения (а именно потеря иди восстановление фокуса в нужный момент)

 

Рисуем интерфейс

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

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
enum KeyType
{
    EMPTY,
    REGULAR,// like A...Z
    LITTLE,// like F1-F12
    TAB,
    SHIFTL,
    SHIFTR,
    CAPSLOCK,
    BACKSPACE,
    SLASH,// like \
    ENTER,
    SPACEBAR,
    ALTL,
    ALTR,
    CTRLL,
    CTRLR
};

 

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

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// loading of the keys' images
Image imgTmp = null;
Image imgTab = Image.FromFile(pathTab);
Image imgAltL = Image.FromFile(pathAltL);
Image imgAltR = Image.FromFile(pathAltR);
Image imgCtrlL = Image.FromFile(pathCtrlL);
Image imgCtrlR = Image.FromFile(pathCtrlR);
Image imgSlash = Image.FromFile(pathSlash);
Image imgEnter = Image.FromFile(pathEnter);
Image imgShiftL = Image.FromFile(pathShiftL);
Image imgShiftR = Image.FromFile(pathShiftR);
Image imgLittle = Image.FromFile(pathLittle);
Image imgRegular = Image.FromFile(pathRegular);
Image imgSpacebar = Image.FromFile(pathSpacebar);
Image imgCapslock = Image.FromFile(pathCapslock);
Image imgBackspace = Image.FromFile(pathBackspace);

 

Алгоритм отрисовки клавиатуры выглядит следующим образом:

  • Отрисовать каждую из кнопок в цикле (не руками же пиксели вычислять:))
  • Написать соответствующий текст на каждой из клавиш.

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

Кстати, метод DrawKeyboard(…) использует многомерный массив keyboardStructure, который сделан из элементов KeyType. Содержимое данного массива:

 

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
PiliKeyboard.MainForm.KeyType[][] keyboardStructure =
{
new KeyType[] {
        KeyType.LITTLE, KeyType.LITTLE, KeyType.LITTLE, KeyType.LITTLE,
        KeyType.LITTLE, KeyType.LITTLE, KeyType.LITTLE, KeyType.LITTLE,
        KeyType.LITTLE, KeyType.LITTLE, KeyType.LITTLE, KeyType.LITTLE,
        KeyType.LITTLE, KeyType.LITTLE, KeyType.LITTLE, KeyType.LITTLE,
        KeyType.LITTLE },
 
new KeyType[] {
        KeyType.REGULAR, KeyType.REGULAR, KeyType.REGULAR, KeyType.REGULAR,
        KeyType.REGULAR, KeyType.REGULAR, KeyType.REGULAR, KeyType.REGULAR,
        KeyType.REGULAR, KeyType.REGULAR, KeyType.REGULAR, KeyType.REGULAR,
        KeyType.REGULAR, KeyType.BACKSPACE, KeyType.REGULAR },
 
new KeyType[] {
        KeyType.TAB, KeyType.REGULAR, KeyType.REGULAR, KeyType.REGULAR,
        KeyType.REGULAR, KeyType.REGULAR, KeyType.REGULAR, KeyType.REGULAR,
        KeyType.REGULAR, KeyType.REGULAR, KeyType.REGULAR, KeyType.REGULAR,
        KeyType.REGULAR, KeyType.SLASH, KeyType.REGULAR },
 
new KeyType[] {
        KeyType.CAPSLOCK, KeyType.REGULAR, KeyType.REGULAR, KeyType.REGULAR,
        KeyType.REGULAR, KeyType.REGULAR, KeyType.REGULAR, KeyType.REGULAR,
        KeyType.REGULAR, KeyType.REGULAR, KeyType.REGULAR, KeyType.REGULAR,
        KeyType.ENTER, KeyType.REGULAR },
 
new KeyType[] {
        KeyType.SHIFTL, KeyType.REGULAR, KeyType.REGULAR, KeyType.REGULAR,
        KeyType.REGULAR, KeyType.REGULAR, KeyType.REGULAR, KeyType.REGULAR,
        KeyType.REGULAR, KeyType.REGULAR, KeyType.REGULAR, KeyType.SHIFTR,
        KeyType.REGULAR },
 
new KeyType[] {
        KeyType.REGULAR, KeyType.CTRLL, KeyType.REGULAR, KeyType.ALTL,
        KeyType.SPACEBAR, KeyType.ALTR, KeyType.REGULAR, KeyType.CTRLR,
        KeyType.LITTLE, KeyType.LITTLE, KeyType.LITTLE},
 
new KeyType[] {
        KeyType.LITTLE, KeyType.LITTLE, KeyType.LITTLE}
};

 

Тут я «бегал» по клавиатуре, слева направо заполняя порядок следования клавиш в многоразмерный массив.

 

Создание матрицы Марковских связей

В этой части также не было трудностей. Может быть из-за того что метод я разработал много лет назад, когда писал диплом в университете. Идея тривиальна. Первое, что необходимо сделать, это выяснить сколько раз каждый из символов (буква) встречается после исследуемой (буквы). Этой рутиной занимается метод getProbabilityMatrix(…). В результате, мы получим матрицу относительных частот (сколько раз встречается символ после исследуемого символа).

 

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
private static float[,] getProbabilityMatrix
    (string sDictionaryPath, string sInterestSymbols)
{
    long symbolsNum = 0;
 
    float[,] probabilityMatrix = 
    new float[sInterestSymbols.Length, sInterestSymbols.Length];
 
    probabilityMatrix = new float[sInterestSymbols.Length, sInterestSymbols.Length];
 
    // reading all the dictionary into the buffer
    string sDict = "";
 
    using (TextReader tr = new StreamReader(sDictionaryPath, Encoding.GetEncoding(1251)))
    {
        sDict = tr.ReadToEnd();
    }
 
    // making all the symbols from dictionary as lower cased
    sDict.ToUpper();
 
    for (int s = 0; s < sInterestSymbols.Length; s++)
    {
        for (int i = 0; i < sDict.Length - 1; i++)
        {
            if ((sDict[i].ToString().ToUpper() == 
            sInterestSymbols[s].ToString().ToUpper()))
            {
                for (int j = 0; j < sInterestSymbols.Length; j++)
                {
                    if (sDict[i + 1].ToString().ToUpper() == 
                sInterestSymbols[j].ToString().ToUpper())
                    {
                        probabilityMatrix[s, j] += 1;
                    }
                }
                symbolsNum++;
            }
        }
    }
 
    return probabilityMatrix;
}

 

Следующей задачей является сортировка этих данных, чтобы получить что-то наподобие этого: «После буквы ‘а’ у нас могут появляться буквы ‘б’, ‘р’, ‘м’ и так далее». Вся эта работа выполняется в методе getSequenceMatrix(…).

 

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
26
27
28
29
30
31
32
33
34
//method to get sequential matrix
public static string[,] getSequenceMatrix(string sDictionaryPath, string sInterestSymbols)
{
    if (!File.Exists(sDictionaryPath))
        return null;
 
    float[,] probabilityMaxtrix = 
        new float[sInterestSymbols.Length, sInterestSymbols.Length];
    string[,] sequenceMatrix = 
        new string[sInterestSymbols.Length, sInterestSymbols.Length];
 
    float[] dataTmp = new float[sInterestSymbols.Length];
    int[] indxsTmp = new int[sInterestSymbols.Length];
 
    probabilityMaxtrix = getProbabilityMatrix(sDictionaryPath, sInterestSymbols);
 
    for (int r = 0; r < sInterestSymbols.Length; r++)
    {
        for (int i = 0; i < sInterestSymbols.Length; i++)
        {
            dataTmp[i] = probabilityMaxtrix[r, i];
        }
 
        getSortedIndexesBubblesort1D(ref dataTmp, ref indxsTmp);
 
        for (int c = 0; c < sInterestSymbols.Length; c++)
        {
            sequenceMatrix[r, c] = sInterestSymbols
        [indxsTmp[sInterestSymbols.Length - c - 1]].ToString().ToUpper();
        }
    }
 
    return sequenceMatrix;
}

 

Используя этот метод, мы видим, что первый параметр ввода – путь к словарю. Давайте рассмотрим, что же представляют собой словари. Каким должен быть идеальный словарь? Это должен быть достаточно большой набор часто используемых слов. Выполнив это условие, мы будем получать более высокую точность определения следующей буквы после введенной. В любое время, мы можем добавить слово в «базу данных», просто открыв «*.txt» файл. из директории «./Словари/». Эти файлы являются пользовательскими и могут включать любые виды сокращений, если необходимо. Для того, чтобы определить количество подсвечиваемых символов была определена переменная «numOfSuppKeysToShow». Значение по умолчанию равно числу «2″, но не составит особых трудностей установить любое другое значение.

 

 Проблемы, связанные с получением и потерей фокуса программы

Смысл проблемы в том, что во время нажатия курсора мыши по виртуальной клавиатуре наше приложение перехватывает фокус и в результате, сообщение о нажатии клавиши отправляется ему же самому. Для решения этой проблемы я решил схитрить. Пока курсор мыши движется над расфокусированной программой виртуальной клавиатуры, мы получаем дескриптор активного окна. Потом, когда осуществляется выбор желаемой клавиши, мы уже знаем, куда отправлять её код. Благодаря этому «костылю» программа работает замечательно.

 

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
26
27
28
// method to get handle of the target window while the PiliKeyboard inactive
private void MainForm_MouseMove(object sender, MouseEventArgs e)
{
    // when we move cursor under the PiliKeyboard 
    // we are getting pointer to the target window
    IntPtr IntPtrTmp = CWinApi.GetForegroundWindow();
 
    if ((IntPtrTmp != hWndSource) && (IntPtrTmp.ToInt32() != 0))
    {
        hWndTarget = IntPtrTmp;
    }
 
    // updating input language var
    lCurrentLanguage = GetKeyboardLayoutLang();
 
    switch (lCurrentLanguage)
    {
        case LANG.UK:
            sCurrentLanguage = UK;
            break;
        case LANG.RU:
            sCurrentLanguage = RU;
            break;
        case LANG.EN:
            sCurrentLanguage = EN;
            break;
    }
}

 

Настройка

Идея настройки графического интерфейса программы заключается в том, что пользователь имеет возможность самостоятельно изменить стиль путем создания нового или же изменением существующего набора изображений для кнопок. Имеются некоторые ограничения! Размеры для кнопок должна быть такими же, как и у оригинала. Ниже представлен пример немного сумасшедшего и не совсем удачного интерфейса:

 

 

Заключительная часть

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

 

Загрузка

Скачать демонстрационный проект

Скачать исходный код проекта

Поделиться в соц. сетях