Практика
Поддержка Alt-кодов в компонентах редактирования текста

:: Меню ::
:: На главную ::
:: FAQ ::
:: Заметки ::
:: Практика ::
:: Win API ::
:: Проекты ::
:: Скачать ::
:: Секреты ::
:: Ссылки ::

:: Сервис ::
:: Написать ::

:: MVP ::

:: RSS ::

Яндекс.Метрика


Delphi уже давно (если быть точным – с версии 2009) поддерживает UNICODE, но, по непонятным для меня причинам, до сих пор (а на пороге уже Delphi XE8) не "научила" компоненты, предназначенные для редактирования текста, воспринимать Alt-коды, вводимые с клавиатуры, хотя из кода это можно сделать без каких либо проблем. Ввод символа ≠ из кода можно реализовать, например, так:

procedure TForm1.Button1Click(Sender: TObject);
begin
   Edit1.Text := Edit1.Text + Chr( 8800 );
end;

Хотелось бы иметь возможность вводить подобные символы с клавиатуры, как, например, это позволяет делать MS Word. В этой статье я хочу показать, как можно исправить это досадное упущение.

В начале несколько слов для тех, кто не знаком с Alt-кодами. ALT-код — код, символы которого вводятся посредством кнопки Alt и последовательности цифр на NUM-паде (небольшом блоке из 17 клавиш, обычно находящемся с правого края клавиатуры). Так, для того, чтобы ввести символ '≠', нужно нажать кнопку Alt, и не отпуская ее нажать на NUM-паде последовательность цифр 8 + 8 + 0 + 0.

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

Мы можем воспользоваться одним из 2 вариантов, а именно WH_KEYBOARD или WH_KEYBOARD_LL. Если кратко описать разницу между ними, то можно сказать так: WH_KEYBOARD срабатывает при попадании в очередь сообщений потока сообщений WM_KEYUP/WM_KEYDOWN, а WH_KEYBOARD_LL срабатывает при низкоуровневых клавиатурных событиях, поступающих от драйвера клавиатуры (процедура обработки WH_KEYBOARD_LL выполняется в контексте приложения, установившего хук).

Рассмотрим оба варианта, и начнем с WH_KEYBOARD.

WH_KEYBOARD

const
  KHF_ALTDOWN_HI: Cardinal = $2000;

function KeyHook( Code: Integer; WParam: WPARAM; lParam: LPARAM ): LRESULT; stdcall;
const
  {$J+}
  CodeStr: string = '';
  {$J-}
var
  Key: Cardinal;
  IsAltDown: Boolean;
  SelPos, SelLen: Integer;
  Str: string;
begin
   if Code = HC_ACTION then
   begin
      if Form2.ActiveControl = Form2.Edit1 then
      begin
         IsAltDown := HiWord( lParam ) and KHF_ALTDOWN_HI = KHF_ALTDOWN_HI;

         if IsAltDown then
         begin
            if HiWord( lParam ) and KF_UP = 0 then
               if WParam in [96..105] then
                  CodeStr := CodeStr + Chr( WParam - 48 );
         end
         else
         begin
            if CodeStr <> '' then
            begin
               SelPos := Form2.Edit1.SelStart + 1;
               SelLen := Form2.Edit1.SelLength;
               Str := Form2.Edit1.Text;

               if SelLen > 0 then
                  Delete( Str, SelPos, SelLen );
               Insert( Chr( StrToInt( CodeStr ) ), Str, SelPos );

               Form2.Edit1.Text := Str;
               Form2.Edit1.SelStart := SelPos;

               CodeStr := '';

               if WParam = VK_MENU then
               begin
                  Result := 1;
                  Exit;
               end;
            end;
         end;
      end;
   end;
   Result := CallNextHookEx( KeyboardHook, Code, wParam, lParam );
end;

Если у вас возник вопрос, почему для хранения введенного пользователем кода используется локальная константа, и как это вообще работает, прочтите статью Константы в Delphi, а здесь я повторяться не стану. Теперь рассмотрим параметры процедуры хука:
  • Code – может принимать одно из следующих значений:
    • HC_ACTION – приходит при удалении сообщения из очереди;
    • HC_NOREMOVE – приходит, когда клавиатурное сообщение не удаляется из очереди, потому что приложение вызвало функцию PeekMessage с параметром PM_NOREMOVE. При вызове хука с этим кодом не гарантируется передача действительного состояния клавиатуры;
  • wParam – содержит виртуальный код клавиши (например, VK_F1, VK_RETURN, VK_LEFT);
  • lParam расшифровывается следующим образом:
    • Биты 0-15 содержат количество повторений нажатой клавиши в случае "залипания";
    • Биты 16-23 содержат скан код нажатой клавиши. Это аппаратно зависимый код, который зависит от конкретной клавиатуры;
    • Бит 24 равен 1, если нажатая клавиша является расширенной (функциональной или на цифровой клавиатуре), иначе 0;
    • Биты 25-28 зарезервированы;
    • Бит 29 равен 1, если при нажатии клавиши была нажата клавиша Alt, иначе 0;
    • Бит 30 говорит о состоянии клавиши до отправки сообщения. Бит равен 1, если до этого кнопка отправки сообщения была нажата, иначе 0;
    • Бит 31 говорит о текущем состоянии клавиши. Он равен 1, если клавиша отпускается, иначе 0.
Первое, что нужно определить – нажата ли клавиша Alt. Это можно сделать несколькими способами. Рассмотрим их:

const
  KHF_ALTDOWN_HI: Cardinal = $2000; // Для проверки HiWord( lParam )

function KeyHook( Code: Integer; WParam: WPARAM; lParam: LPARAM ): LRESULT; stdcall;
var
  IsAltDown: Boolean;
  {...}
begin
   if Code = HC_ACTION then
   begin
      if Form2.ActiveControl = Form2.Edit1 then
      begin
         IsAltDown := HiWord( lParam ) and KHF_ALTDOWN_HI = KHF_ALTDOWN_HI;
         {...}
      end;
   end;
   Result := CallNextHookEx( KeyboardHook, Code, wParam, lParam );
end;

// Несколько измененный вариант предыдущего кода
const
  KHF_ALTDOWN: Cardinal = $20000000; // Для проверки lParam

function KeyHook( Code: Integer; WParam: WPARAM; lParam: LPARAM ): LRESULT; stdcall;
var
  IsAltDown: Boolean;
  {...}
begin
   if Code = HC_ACTION then
   begin
      if Form2.ActiveControl = Form2.Edit1 then
      begin
         IsAltDown := lParam and KHF_ALTDOWN = KHF_ALTDOWN;
         {...}
      end;
   end;
   Result := CallNextHookEx( KeyboardHook, Code, wParam, lParam );
end;

var
  IsAltDown: Boolean;
  {...}

function KeyHook( Code: Integer; WParam: WPARAM; lParam: LPARAM ): LRESULT; stdcall;
begin
   if Code = HC_ACTION then
   begin
      if Form2.ActiveControl = Form2.Edit1 then
      begin
         if WParam = VK_MENU then
            IsAltDown := HiWord( lParam ) and KF_UP = 0;
         {...}
      end;
   end;
   Result := CallNextHookEx( KeyboardHook, Code, wParam, lParam );
end;

Если клавиша Alt нажата, запоминает вводимые с NUM-пада цифры, формируя из них строку. При отпускании клавиши Alt заносим символ, соответствующий введенному коду, в текстовое поле в позицию каретки или заменяя выделенный текст. Обратите внимание на то, что для работы 3-его примера переменная IsAltDown должна быть глобальной, в то время как для первых 2-х премеров это не обязательно.

Теперь рассмотрим вариант с WH_KEYBOARD_LL.

WH_KEYBOARD_LL

const
  LLKHF_ALTDOWN: Cardinal = KF_ALTDOWN shr 8;
  LLKHF_UP: Cardinal = KF_UP shr 8;

function LLKeyHook( Code: Integer; WParam: wParam; Msg: PKbdDllHookStrukt ): Longint; stdcall;
const
  {$J+}
  CodeStr: Cardinal = 0;
  {$J-}
var
  Key: Cardinal;
  IsAltDown: Boolean;
  SelPos, SelLen: Integer;
  Str: string;
begin
   case Code of
      HC_ACTION: begin
         if Form2.ActiveControl = Form2.Edit1 then
         begin
            // LLKHF_ALTDOWN - Кнопка ALT нажата
            // 0 - Кнопка ALT отжата
            IsAltDown := Msg.flags and LLKHF_ALTDOWN = LLKHF_ALTDOWN;

            if IsAltDown then
            begin
               if Msg.flags and LLKHF_UP = 0 then
               begin
                  if Msg^.vkCode in [96..105] then
                     CodeStr := CodeStr * 10 + Msg^.vkCode - 96;
               end;
            end
            else
            begin
               if CodeStr > 0 then
               begin
                  SelPos := Form2.Edit1.SelStart + 1;
                  SelLen := Form2.Edit1.SelLength;
                  Str := Form2.Edit1.Text;

                  if SelLen > 0 then
                     Delete( Str, SelPos, SelLen );
                  Insert( Chr( CodeStr ), Str, SelPos );

                  Form2.Edit1.Text := Str;
                  Form2.Edit1.SelStart := SelPos;
               end;

               CodeStr := 0;

               if Msg^.vkCode in [VK_LMENU, VK_RMENU] then
               begin
                  keybd_event( VK_MENU, 0, 0, 0 );
                  keybd_event( VK_MENU, 0, KEYEVENTF_KEYUP, 0 );
                  Result := 1;
                  Exit;
               end;
            end;
         end;
      end;
   end;
   Result := CallNextHookEx( LLKeybHook, Code, WParam, Longint( Msg ) );
end;

Рассмотрим параметры структуры TKbdDllHookStrukt:
  • vkCode – виртуальный код клавиши;
  • scanCode – скан-код нажатой клавиши. Это аппаратно зависимый код, который зависит от конкретной клавиатуры;
  • flags - расшифровывается следующим образом:
    • Бит 0 равен 1, если нажатая клавиша является расширенной (функциональной или на цифровой клавиатуре), иначе 0;
    • Бит 1 равен 1, если сообщение пришло от Low Level Integrity процесса, иначе 0;
    • Биты 2 и 3 зарезервированы;
    • Бит 4 равен 1, если это внедренное (injected) событие, иначе 0;
    • Бит 5 равен 1, если при нажатии клавиши была нажата клавиша Alt, иначе 0;
    • Бит 6 зарезервирован;
    • Биты 7 говорит о текущем состоянии клавиши. Он равен 1, если клавиша отпускается, иначе 0.
  • time – время отправки сообщения;
  • dwExtraInfo – дополнительная информация, связанная с сообщением.
Принцип действия аналогичен примеру на WH_KEYBOARD, за исключением того, что для запоминания введенного числового кода используется не строка, а числовая переменная. Какой именно способ использовать, зависит от ваших личных предпочтений.

Полные исходные коды находятся в примерах к данной статье. В примерах поддержка Alt-кодов показана на примере компонента TEdit, однако этот метод можно применить к любым другим компонентам редактирования текста.

P.S.
На основе данных примеров реализованы 2 компонента, скачать которые вы можете в разделе Мои компоненты.

P.P.S.
Я был приятно удивлен, узнав, что разработчики наконец-то добавили стандартным компонентам поддержку Alt-кодов. Причем узнал совершенно случайно, наткнувшись в Embarcadero's Quality Portal на пост Composing keys via Alt-code to enter in an FMX TEdit control is not accepted. Ради интереса протестировал это на том, что в данный момент было под рукой - Delphi 10 Seattle Update 1, все прекрасно работает, пользуйтесь на здоровье (правда пока только в VCL)!

.: Пример к данной статье :.


При использовании материала - ссылка на сайт обязательна