уважаемые посетители блога, если Вам понравилась, то, пожалуйста, помогите автору с лечением. Подробности тут.

Как я уже отмечал, начиная с Delphi 2009 введена полная поддержка Unicode.  Это замечательно, необходимо и важно. Однако, как ни крути, но не все толком понимают как работать-то? Я сейчас не имею ввиду профессиональных программистов, работающих постоянно с Delphi. Речь идёт о простых любителях, начинающих программистах, которые большую часть информации получают из Сети, в результате чего возникают всякие непредвиденные ситуации. Вот, например, ситуация — надо написать программу, которая будет отлавливать нажатия клавиш клавиатуры и записывать их в лог.  В Интернет есть масса примеров как организовать глобальный хук на клаву и считывать в лог виртуальные и скан-коды клавиш (подобная статья есть и у нас в блоге). Естественно, начинающий программист просто качает готовый пример и пробует транслировать эти самые коды в буковки…а они просто так не транслируются — получаем либо «кракозябры» либо нормальные буквы но не те.  И сидит такой начинающий программист, чешет затылок и другие части тела, не понимая что он делает не так? В лучшем случае лезет в опять в Сеть и с огромной вероятностью натыкается на подобного рода «рецепты-объяснения»:

В событиях OnKeyDown и OnKeyUp, Key является беззнаковым двухбайтовым (Word) значением, которое представляет виртуальную клавишу Windows. Для получания значения символа можно воспользоваться функцией Chr. В событии OnKeyPress параметр Key является значением Char, которое представляет символ ASCII.

Вроде все правильно. НО не для Delphi 2009-2010, при работе с русскими символами, так как во-первых, введена поддержка Unicode, а во-вторых ASCII никогда с роду не содержала русских букв (см.историю разработки кодировок).  И никогда, ни при каких обстоятельствах Вы не сможете перевести просто так виртуальную клавишу в русский символ функцией Chr.

Если не верите — проделайте такой простой эксперимент: откройте Delphi 2009-2010, создайте новое приложение и в событии onKeyUp для главной формы напишите такой код:

ShowMessage(chr(Key))

Теперь запустите приложение, переключитесь на русский язык и нажмите любую клавишу.

Вот результат нажатия русской буквы «Ы»:

S
Спрашивается: Чё за фигня? Где моя буковка «Ы»? И, что самое интересное, я ведь нажимал букву без Shift и CapsLock, а мне чего-то буква в верхнем регистре упала? Давайте разбираться в определениях. Так как без четкого знания определений мы с Вами в этих лабиринтах и хитросплетениях не разберемся никогда.

1. Скан-коды, виртуальные код и коды символов

Итак, с клавиатурой связано как минимум три разновидности кодов: скан-код, символьный код и виртуальный код. И далеко не все начинающие программисты понимают различия между ними.
Самая большая путаница со скан-кодами. Для начала, их существует две разновидности. Есть скан-коды «настоящие» — это то, что получает система прямо от клавиатуры, а есть скан-коды которые выдаются после того, как они будут обработаны в BIOS, которая интерпретирует, например, одновременное нажатие клавиш-модификаторов, а также справляется с текущей таблицей кодов символов и отправляет эти скан-коды в буфер клавиатуры.
Теперь посмотрим, как выглядит скан-код в принципе. ibm-pc-keyboardНа рисунке представлена оригинальная 84-кнопочная клавиатура IBM PC. Вот во времена существования этой клавиатуры и назначались скан-коды клавишам. Причем присваивались они слева-направо, начиная с клавиши Esc. Так клавиша Esc получила скан-код 1, а клавиша 1 — код 2 и т.д. Пока не будем кидаться в дебри дополнительных клавиш, которые появились на современных клавиатурах (там есть свои особенности присвоения скан-кодов), а уясним для себя следующее:

Скан-код — это идентификатор клавиши на клавиатуре и он один в чистом виде никак не может сам по себе описать символ в условиях unicode

Теперь двигаемся дальше. Разберемся с виртуальными кодами.

Виртуальные коды — это то, что использует система (Windows) для идентификации клавиш.

То есть, проведя простую аналогию, можно с большим допущением сказать, что виртуальный код клавиши — это тот же скан-код, но только не для BIOS, а для операционной системы, установленной на компьютере пользователя.

С некоторыми из констант, определяющих виртуальный код клавиш Вы можете ознакомиться в модуле windows.pas Delphi. Но, там отсутствуют коды для буквенно-цифровых клавиш, т.к. они совпадают с соответствующими символами из таблицы ASCII.

Теперь, я думаю, становится более-менее ясно почему при нажатии клавиши с буквой «ы» мы в примере получили заглавную букву «S»? Delphi в этом случае сработала идеально правильно  — выдала именно виртуальный код клавиши, как и полагается.

А для того, чтобы получить русскую букву нам необходимо знать третий код — код символа в таблице символов Unicode или, как мы посмотрим ниже, скан-код, виртуальный код и ещё один параметр — состояние клавиатуры.

Для того, чтобы продемонстрировать правильность работы Delphi, немного допишем наш пример.

Создайте такую глобальную переменную:

Ch: word;

Теперь в обработчике события onKeyPress главной формы напишите такой код:

Ch:=Ord(Key);

То есть, таким простым способом мы получим в переменную Ch уже не виртуальный, а код символа из таблицы Unicode, т.к. в переменную Key обработчика onKeyPress поступает уже не код, а символ.

В обработчике onKeyUp добавьте новую строку:

ShowMessage(Chr(Ch));

Запустите приложение и проверьте любую русскую букву. Второй ShowMessage вернет как и полагается верный символ в верной раскладке.

Чтобы убедиться, что в переменную Ch попадает именно Unicode, а не код из какой-то другой таблицы кодировок (например ANSI)  Вам достаточно вспомнить как располагаются символы в этой кодировке (см. историю кодировок) и посмотреть у переменной Ch первое слово. В Delphi это можно сделать, например вот так:

ShowMessage('Код символа = '+IntToStr(Ch)+#10#13+
'HEX представление = '+IntToHex(Ch,4)+#10#13+
'Символ = '+Chr(Ch));

В результате, для полюбившейся мне буквы «ы» получим:

S2Как видите, нет никаких ошибок в работе Delphi, функции ord и chr как работали со времен бородатого Pascal так и продолжают работать и выполнять свое назначение. Появилась только небольшая проблемка — как зная скан-код и виртуальный код клавиши получить код символа и, естественно сам символ из таблицы Unicode?

Самый простой вариант — это составить таблицу соответствия русских символов (строчных или прописных) из Unicode с виртуальными кодами клавиш. Для того, чтобы просто транслировать виртуальный код в букву этого будет вполне достаточно, а вот для полноценной работы — врядли. Необходимо также учитывать одновременное нажатие клавиш-модификаторов и прочие хитросплетения, чтобы верно транслировать большие и маленькие буквы, цифры, точки, запятые и т.д. Но, мы ведь не ищем лёгких путей? И в итоге доберемся до истины без всяких таблиц.

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

2. Работа клавиатурой в Delphi 2010

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

А рассмотрим те возможности, которые предоставляют нам Delphi и Windows для работы с клавиатурой в принципе.

Итак, в начале рассмотрим как можно получить в свое распоряжение и виртуальный код клавиши и скан-код клавиши.  Чтобы получить в распоряжение эти коды воспользуемся функцией MapVirtualKey.

MapVirtualKey переводит виртуальный код клавиши в скан-код или символьное значение, или переводит скан-кода в код виртуальной клавиши.

Синтаксис функции в Delphi:

function MapVirtualKey(uCode, uMapType: cardial): cardinal

uCode — определяет виртуальный код клавиши или скан-кода. Как интерпретируется эта величина зависит от значения параметра uMapType

uMapType — Указывает на то как будет произведен перевод. Значение этого параметра зависит от значения параметра uCode и может принимать следующие значения:

MAPVK_VK_TO_VSC — uCode представляет собой виртуальный код клавиши и переводится в скан-код.  Если это виртуальный код клавиши  (что не делает различия между левыми и правыми клавишами-модификаторами ), то функция вернет значение скан-кода для левой клавиши-модификатора или буквенно-цифровой клавиши. Если перевода невозможен, то функция возвращает 0.

MAPVK_VSC_TO_VK uCode — это скан-код и переводится в виртуальный код клавиши.

MAPVK_VK_TO_CHAR — uCode виртуальный код клавиши и переводится в значение код символа из ASCII без смещение.

MAPVK_VSC_TO_VK_EX — для Windows NT/2000/XP: uCode это скан-код и переводится в виртуальный код клавиши, При этом различаются левые и правые модификаторы.

MAPVK_VK_TO_VSC_EX — то же самое, что и MAPVK_VK_TO_VSC, но с различием левых и правых модификаторов.

Теперь, воспользуемся этой функцией и попробуем получить значения скан-кодов и виртуальных кодов. Создаем или немного модифицируем наше тестовое приложение. У меня, например, для проверки работы с клавиатурой получилось вот такое приложение:

Приложение для тестов при работе с клавиатуройДля удобства работы можно использовать новый компонент Delphi 2010 с вкладки Touch палитры компонентов.

В Memo будем выводить всю, интересующую нас информацию.

Теперь в обработчике события onKeyUp главной форму пишем:

procedure TForm2.FormKeyUp(Sender: TObject; var Key: Word; Shift: TShiftState);
var VK, SC, C: cardinal;
begin
if b then  //слежение за клавиатурой включено
  begin
    SC := MapVirtualKey(Key, MAPVK_VK_TO_VSC);// получаем скан-код
    VK := MapVirtualKey(SC, MAPVK_VSC_TO_VK);// скан-код переводится в виртуальный код
    C := MapVirtualKey(VK, MAPVK_VK_TO_CHAR);// виртуальный код в код симола по ASCII
    //выводим всё, что только можно по нажатой кнопке
    Memo1.Lines.Add('++++ KeyUp() ++++');
    Memo1.Lines.Add('Вернула значение            ' + IntToStr(Key));
    Memo1.Lines.Add('++++ Значения функции MapVirtualKey() ++++');
    Memo1.Lines.Add('Скан-код клавиши            ' + IntToStr(SC));
    Memo1.Lines.Add('Виртуальный код клаиши      ' + IntToStr(VK));
    Memo1.Lines.Add('Не сдвинутое значение ASCII ' + IntToStr(C));
    Label5.Caption := IntToStr(Key);
    Label7.Caption := Chr(Key);
  end;
end;

А в обработчике события onKeyPress такой код:

procedure TForm2.FormKeyPress(Sender: TObject; var Key: Char);
begin
if b then
  begin
    Ch := Ord(Key);
    Memo1.Lines.Add('++++ Функция ORD() ++++');
    Memo1.Lines.Add('Вернула значение            ' + IntToStr(Ord(Key)));
  end;
end;

Запускаем приложение и проверяем работу функции MapVirtualKey:

Проверка скан-кода и виртуального кода для клавиши в английской раскладкеКак и полагается для английской буквы виртуальный код совпадает с несдвинутым значением кода символа по ASCII и это же значение было возвращено нам в переменной Key процедуры обработки события onKeyUp. При этом обратите внимание на то, какое значение вернула функция ord() — она вернула верный код символа из ASCII или, что-то же самое, из начала таблицы Unicode.

Теперь переключаемся на русскую раскладку и проверяем ту же самую кнопку:

Проверка русской раскладкикак и в первом случае — все значения кодов те же самые за исключением значения из функции ord() она как и полагается вернула значение кода символа с первым байтом равным 4.

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

Смотрим, что нам говорит справка Delphi про функцию MapVirtualKeyEx:

MapVirtualKeyEx функция переводит виртуальный код клавиши в скан-код или символьное значение, или переводит скан-код в код виртуальной клавиши. Функция переводит код, используя язык ввода.

Синтаксис:

function MapVirtualKey(uCode, uMapType, dwhkl: cardial): cardinal

Первые два параметра те же, что и у предыдущей функции. А третий параметр:

dwhkl — Язык ввода, используется для перевода указанного кода. Этот параметр может быть любым идентификатором Язык ввода предварительно возвращенный функцией LoadKeyboardLayout.

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

Первая функция получает значение текущего языка ввода для процесса:

GetKeyBoardLayout(dwLayout: cardinal)

dwLayout — идентификатор процесса для которого необходимо получить значение языка ввода. Если параметр равен нулю, то возвращается значение для текущего процесса.

Проверим работу функции. Пусть при включении режима слежения за клавиатурой (в моей программе за это отвечает кнопка и переменная b: boolean) выводится значение функции GetKeyboardLayout.

procedure TForm2.Button1Click(Sender: TObject);
begin
b := not b;
if b then
  begin
    Label2.Caption := 'Слежение';
    Label9.Caption:=IntToStr(GetKeyboardLayout(0));
  end
else
  Label2.Caption := 'Простой'
end;

Результат работы программы можно увидеть на рисунке:

идентификатор языка вводаВпервые столкнувшись с работой этой функции у меня возник резонный вопрос «Что мне с этим числом делать?» Ответ нашелся спустя несколько минут. Дело в том, что это число можно представить в несколько иной форме. Запишем вывод идентификатора в немного ином виде — возьмем только младшее слово:

Label9.Caption:=IntToStr(LoWord(GetKeyboardLayout(0)));

Теперь запустите снова программу и посмотрите на результат. Уже более знакомая картинка: русский язык имеет идентификатор 1049, а английский 1033.В HEX-форме эти же значения будут выглядеть как 0419 и 0409 соответственно.

Двигаемся дальше. Вторая функция — LoadKeyboardLayout загружает новый идентификатор языка.

Синтаксис:

LoadKeyboardLayout(pwszKLID: PWideChar; Flags: cardinal);

pwszKLID — указатель на буфер, который определяет имя идентификатора локали для загрузки. Это имя представляет собой строку, состоящую из шестнадцатеричного значения идентификатора языка (младшее слово) и идентификатора устройства (старшее  слово).

Flags может принимать одно из следующих значений:

KLF_ACTIVATE — если указанный идентификатор локали ввода еще не загружен, функция загружает и активирует его для текущего потока.

KLF_NOTELLSHELL предотвращает процедуру ShellProc. Это значение обычно используется, когда приложение загружает несколько идентификаторов языков ввода один за другим.

KLF_REORDER — перемещает указанный идентификатор локали в список идентификаторов локали ввода, делая этот идентификатор активным для текущего потока.Также могут использоваться флаги  KLF_REPLACELANG, KLF_SUBSTITUTE_OK KLF_SETFORPROCESS.

Попробуем применить все вышеизложенное на практике. Пусть по какому-либо событию в нашем приложении меняется идентификатор языка ввода. Я для этого выбрал клик по кнопке:

procedure TForm2.Button2Click(Sender: TObject);
var newHKL: PWideChar;
begin
  hkl:=LoWord(GetKeyboardLayout(0)); //смотрим текущую локаль
  if hkl=1049 then //текущия язык - русский
    begin
      hkl:=1033; //изменяем на английскую
      newHKL:=PChar('0000'+IntToHex(hkl,4));
      LoadKeyboardLayout(newHKL,KLF_ACTIVATE);
      Button2.Caption:='En';
    end
  else
    begin   //текущий язык - английский
      hkl:=1049;
      newHKL:=PChar('0000'+IntToHex(1049,4));
      LoadKeyboardLayout(newHKL,KLF_ACTIVATE);
      Button2.Caption:='Ru';
    end;
  Label9.Caption:=IntToStr(hkl);
  label11.Caption:=IntToHex(hkl,4);
end;

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

Теперь можно, не останавливая слежение за нажатыми кнопками клавиатуры, спокойно менять раскладку на противоположную одним кликом по кнопке.

Двигаемся дальше.  Про получение виртуального и скан-кода разобрались, как получить или сменить идентификатор языка ввода узнали, осталось научиться применять полученные знания для определения русских символов в используемой таблице Unicode.

Как я уже отмечал выше, функция Chr для перевода виртуального кода в русский символ не подходит. Поэтому будем пользоваться тем, что предлагает в наше распоряжение Windows API. А предлагается нам на выбор две функции:

  1. ToAscii
  2. ToUnicode

Есть та же ToAsciiEx и ToUnicodeEx, которые используют в качестве дополнительного параметра текущие локали, но мы их рассматривать не будем, будем так сказать зреть в корень.

Так как нам необходимо научиться определять по скан-коду и виртуальному коду русские символы, то первая функция ToAscii сразу отметается в сторону, остается ToUnicode.

Синтаксис функции в Delphi следующий:

ToUnicode(wVirtKey, wScanCode: cardinal;lpKeyState: TKeyBoardState; pwszBuff: void type; cchBuff: integer; wFlags: Cardinal):integer;

wVirtKey и wScanCode — виртуальный и скан-код клавиши, которая должна быть оттранслирована в символ

lpKeyState — переменная, определяющая состояние клавиатуры в момент трансляции. Представляет собой массив на 256 элементов типа byte.

pwszBuff — указатель на буфер в котором будет хранится символ

cchBuff — размер буфера

wFlags — флаги. Допускается использовать 0.

Функция возвращает:

в переменную pwszBuff символ.

в result:

-1, если трансляция не удалась. Это может произойти, если вы, например, хотели оттранслировать коды управляющих клавиш.

0, если трансляция символа невозможна для текущего состояния клавиатуры.

1, если в буфер был помещен один символ

2, и более, если в буфер было помещено 2 и более символов.

Теперь проверим как работает функция в условиях Delphi 2010.

Снова возвращаемся к нашему приложению-тестеру и дорабатываем его следующим образом:  будем в событии onKeyUp использовать функцию toUnicode и выводить результат и содержимое буфера в Memo.

Обработчик события onKeyUp дополняем следующим образом:

procedure TForm2.FormKeyUp(Sender: TObject; var Key: Word; Shift: TShiftState);
var
...
  KS: TKeyboardState;
  Ch: Char;
begin
...
  GetKeyboardState(KS); //получили состояние клавиатуры
....
  Memo1.Lines.Add('++++ Преобразование кодов в символ ++++');
  Memo1.Lines.Add('Функция toUnicode вернула значение '+IntToStr(ToUnicode(VK,SC,KS,@Ch,Sizeof(Ch),0)));
  Memo1.Lines.Add('В буфер поступил символ   "'+Ch+'"');
end;

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

Перевод виртуального кода и скан-кода клавиши в символКрасным выделено то, что мы могли получить ДО использования функции toUnicode, используя преобразования через Chr(). Как видите в label вывелась конкретная несуразица. Я специально выбрал русский символ для которого не существует на клавиатуре английского аналога. Если работать с клавиатурой через хуки (о чем мы поговорим в другой раз), то просто так получить значение, которое мы получили в событии onKeyPress, нам тоже не удастся. Всё, что мы имели в действительности — виртуальный код, скан-код и состояние (TKeyboardState) клавиатуры. Для полноты букета, могли бы ещё получить идентификатор языка и т.д. А русский символ — нет.

Используя же ToUnicode мы справились с задачей и верно транслировали вртуальный и скан-код в русскую букву. Причем функция также вернет правильный результат, если вы например включите CapsLock или зажмете Shift.

Так что, если кто-то пробовал или даже сумел написать клавиатурного шпиона в Delphi 7 и теперь недоумевает, что работает не так — просто посмотрите как у вас транслировались коды клавиш в символы. Если через Chr или ToAscii, то меняйте эти функции на toUnicode и дорабатывайте приложения. А мы в следующий раз попробуем написать свое приложение по автоматической смене раскладки, ну или, в крайнем случае, доработаем DLL-ку с функциями чтения данных с клавиатуры.

5 1 голос
Рейтинг статьи
уважаемые посетители блога, если Вам понравилась, то, пожалуйста, помогите автору с лечением. Подробности тут.
Подписаться
Уведомить о
7 Комментарий
Межтекстовые Отзывы
Посмотреть все комментарии
Виталя
Виталя
04/12/2009 18:53

Спасибо большое!! Даже и не подумал, что в D-10 так устроено. С уважением

Sergey
Sergey
25/05/2010 20:25

Подскажите,как правильно составить программу для консольных приложений Delphi 2010,
для правильного отображения русского шрифта.
Что работает на Delphi 7, не работает на Delphi 2010
…….
for i:=1 to length(mes) do
case mes[i] of
‘A’..’n’ : mes[i] := Chr(Ord(mes[i]) — 64);
‘p’..’я’ : mes[i] := Chr(Ord(mes[i]) — 16);
end;
rus := mes;
……..
Это не работает.

InfStudent
InfStudent
29/05/2010 18:06

Покопай в сторону функции CharToOEM.

Евгений
Евгений
16/06/2010 20:40

кто может подсказать почему в Delphi 2009,
функция ToUnicodeEx, не пренимает
var KS : TKeyboardState;
выходит ошыбка что тип PByte и TKeyboardState не сходятса.
Не пойму как исправить, помогите плиз!

archangel.VII.II
archangel.VII.II
16/01/2011 23:42

Подскажите что я должен подставить в переменную      keybd_event( СЮДА ,0,0,0);
у меня по очереди идут клавиши в переменной String напр WASD и вот как string в СЮДА подставить ?

trackback

[…] Delphi 2010. Работа с клавиатурой в условиях Unicode. | Delphi в Interne… к записи Компонент Delphi 2010 для работы с Опциями […]