Перейти к содержанию
Fire Monkey от А до Я
  • 0

Хук на клавиатуру


Вопрос

В декабре я задавал здесь вопрос о борьбе с перехватами нажатий клавиш компонентом TWebBrowser. Продвинутый пользователь Kami посоветовал тогда, раз уж меня интересует только Windows, поставить хук на клавиатуру. Поделился полезной ссылкой. Добавил, что можно еще много чего нагуглить. Что-то действительно нагуглилось - но не в том объеме, чтобы я смог четко понять, как это следует делать. Вопросов много. Куда именно должна быть воткнута функция KeyboardProc? Что в ней должно содержаться, чтобы управление передавалось уже написанному обработчику события FormKeyDown? Многие также упоминают о возникающих проблемах с юникодом, и хорошо было бы понять, как уберечься от них.

Буду очень признателен, если кто-нибудь осветит эту темную для меня материю.

Ссылка на комментарий

Рекомендуемые сообщения

  • 0

Есть старый проект где я реализовал хук на клавиатуру, без использования длл. Проект для дельфи 7, соотв. никакого юникода.
Подробности сейчас не вспомню, помню лишь что я недели две исследовал как это работает.
Могу выслать в личку если интересует.

Ссылка на комментарий
  • 0
4 минуты назад, Barbanel сказал:

Могу выслать в личку если интересует.

Очень обяжете. Можно даже сразу на электронную почту:  vsСОБАКАsusi.ru

Ссылка на комментарий
  • 1
3 часа назад, Вадим Смоленский сказал:

Куда именно должна быть воткнута функция KeyboardProc?

Да куда угодно. Любой модуль, но скорее всего - модуль той формы, на которой лежит веббраузер (его же нужно "обойти"). Там же (например - в конструкторе / деструкторе формы) - регистрация и удаление хука.

3 часа назад, Вадим Смоленский сказал:

Что в ней должно содержаться, чтобы управление передавалось уже написанному обработчику события FormKeyDown?

Ну вот например: https://ru.stackoverflow.com/a/538552/192901  (просмотрел бегло, но криминала нет, по крайней мере - система не помрет, что, кстати, весьма возможно при использовании глобальных хуков).

вместо SendMessage подставить myForm.OnKeyDown(...);

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

 

Ссылка на комментарий
  • 0
9 часов назад, kami сказал:

Ссылка внятная, спасибо. Но пока не помогло. Вот такой конструктор формы у меня:

constructor TForm1.Create(AOwner: TComponent);
begin
 inherited;
 CurrentHook := SetWindowsHookEx(WH_KEYBOARD, @KeyboardProc, HInstance, 0);
end;

На отладчике вижу, что после его выполнения CurrentHook как был равен нулю, так и остался. Одно это уже подозрительно. И в функцию KeyboardProc управление не попадает, какие клавиши ни нажимай.

Кстати, в комментариях по ссылке вот еще что пишут:

Цитата

...ваша ловушка не будет работать для 64-битных процессов (подразумевая, что ваша DLL - 32-битная) - потому что 32-битную DLL нельзя загрузить в 64-битный процесс (и наоборот).

Иными словами, чтобы установить действительно глобальную ловушку, вам нужно написать два приложения и две DLL - на 32 и 64 бита. И устанавливать/удалять их одновременно.

У меня 64. Не в этом ли закавыка? Или же сказанное относится лишь к WH_KEYBOARD_LL ?  Я не смог понять.

Изменено пользователем Вадим Смоленский
Ссылка на комментарий
  • 1
9 часов назад, Вадим Смоленский сказал:

Вот такой конструктор формы у меня:

Было бы неплохо почитать, чем отличается WH_KEYBOARD от WH_KEYBOARD_LL. Принципы действия абсолютно разные.

Цитата

This hook may be called in the context of the thread that installed it. The call is made by sending a message to the thread that installed the hook. Therefore, the thread that installed the hook must have a message loop.

Вполне возможно, что окна браузера находятся в другом потоке. Посему для начала экспериментов вам нужен именно WH_KEYBOARD_LL

 

9 часов назад, Вадим Смоленский сказал:

CurrentHook как был равен нулю, так и остался

Ну так нужно же понимать причину - почему он равен нулю. Обращаемся к первоисточнику.

Цитата

If the function fails, the return value is NULL. To get extended error information, call GetLastError.

Смотрите, что вернется и узнавайте причину. Можно так:

if CurrentHook = 0 then
  RaiseLastOSError.
9 часов назад, Вадим Смоленский сказал:

У меня 64. Не в этом ли закавыка?

Нет. Здесь имеется ввиду не разрядность операционной системы, а именно разрядность процессов, запущенных в ней (в 64-битный процесс может быть загружена только 64-битная dll. В 32бита - 32). Кроме того, _LL хуки не зависят от разрядности процесса, поскольку работают в контексте установившего хук потока, им dll не требуется.

Изменено пользователем kami
Ссылка на комментарий
  • 0
4 часа назад, kami сказал:

Ну так нужно же понимать причину - почему он равен нулю.

RaiseLastOSError помог установить причину:

System Error.  Code: 1428.
Cannot set nonlocal hook without a module handle

Только не знаю пока, как это трактовать и что с этим делать.

Интересно и то, что с WH_KEYBOARD_LL  хук срабатывает: CurrentHook ненулевой, и по нажатию клавиши управление переходит в KeyboardProc. Но ведь это, как я понял, глобальный хук, он будет перехватывать нажатия у других приложений. А это уже будет лишним.

Ссылка на комментарий
  • 1
15 минут назад, Вадим Смоленский сказал:

Только не знаю пока, как это трактовать и что с этим делать.

по тексту этой ошибки гуглится очень многое.

Основной посыл: для WH_KEYBOARD, если указывать HInstance, то он должен быть инстансом dll. Потому что exe не инжектнется в чужой процесс.

Можно указать вместо HInstance - 0, а последний параметр выставить в TThread.Current.ThreadID, но (повторюсь) я не уверен, что веббраузер работает только в контексте основного потока приложения.

Ссылка на комментарий
  • 0
1 час назад, kami сказал:

Можно указать вместо HInstance - 0, а последний параметр выставить в TThread.Current.ThreadID

Попробовал, и получилось! С веббраузером проблем не наблюдается - даже когда фокус на нем, управление переходит в KeyboardProc.

Осталось последнее: понять, как из KeyboardProc(nCode: integer; wParam: integer; lParam: integer) вызвать FormKeyDown(Sender: TObject; var Key: Word; var KeyChar: Char; Shift: TShiftState). В идеале хотелось бы суметь передать без искажений все три параметра: Key, KeyChar и Shift.

Ссылка на комментарий
  • 0
1 час назад, Вадим Смоленский сказал:

В идеале хотелось бы суметь передать без искажений все три параметра: Key, KeyChar и Shift. 

Так а в чем загвоздка-то?
внутри этого хука доступен сканкод нажатой клавиши. Есть GetAsyncKeyState и много чего еще

Ссылка на комментарий
  • 0
37 минут назад, kami сказал:

Так а в чем загвоздка-то?

Загвоздка в моей дремучести, которую не ликвидировать враз. Нашел описание GetAsyncKeyState, но это несколько не о том, туда надо передавать параметром уже известный код клавиши. А что за снанкод? Где про него можно узнать?

Ссылка на комментарий
  • 0

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

function KeyboardProc(nCode: integer; wParam: integer; lParam: integer ): LongWord; stdcall;
var W: Word;
    C: Char;
    KeyUp : Boolean;
    KeyState: TKeyboardState;
    TheShift: TShiftState;
begin
  if (nCode < 0) then
  begin
    Result := CallNextHookEx(CurrentHook, nCode, wParam, lParam);
    Exit;
  end;
  KeyUp := ((lParam AND (1 shl 31)) <> 0);
  if not KeyUp then
     begin
      W:=wParam;
      C:=Chr(wParam);
      TheShift:=[];
      if GetKeyboardState(KeyState) then
          begin
           if (lParam AND (1 shl 29))<>0 then TheShift:=[ssAlt];
           if (GetKeyState(VK_CONTROL) AND (1 shl 15))<>0 then TheShift:=TheShift+[ssCtrl];
           if (GetKeyState(VK_SHIFT) AND (1 shl 15))<>0 then TheShift:=TheShift+[ssShift];
          end;
      Form1.FormKeyDown(Form1,W,C,TheShift);
     end;
  Result := CallNextHookEx(CurrentHook, nCode, wParam, lparam);
end;

Придирчиво еще не тестировал, но в первом приближении всё работает.

Ссылка на комментарий
  • 0

Выявилась проблемка. Когда FormKeyDown вызывался как обработчик события, в самом его конце выполнялся оператор Key:=0. Это обнуляло нажатие клавиши, гарантировало, что оно не сработает каким-то дополнительным, непредвиденным образом. В новой конфигурации это не работает, я уже столкнулся с досадными побочными эффектами. Нажатие нужно обнулить в обработчике хука KeyboardProc. Но как?

Ссылка на комментарий
  • 0
2 минуты назад, kami сказал:

не вызывать следующий хук

Вы имеете в виду, убрать эту строчку:

Result := CallNextHookEx(CurrentHook, nCode, wParam, lparam);

Я полагал, что "следующий хук" относится к возможному нажатию следующей клавиши, которое хранится в очереди. Разве это не так?

И что присваивать результату в таком случае? Ноль?

Ссылка на комментарий
  • 0
7 минут назад, Вадим Смоленский сказал:

Вы имеете в виду, убрать эту строчку:

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

 

7 минут назад, Вадим Смоленский сказал:

Я полагал, что "следующий хук" относится к возможному нажатию следующей клавиши, которое хранится в очереди. Разве это не так?

А вы считаете, что единственный на всю систему установили хук на клавиатуру ? :))) Их десятки, если не сотни. Тот же WH_KEYBOARD: система будет вызывать последовательно все KeyboardProc в порядке (емнип), обратном времени установки хука. Естественно, если вы скажете системе "можно отдать на обработку следующему хуку".

 

7 минут назад, Вадим Смоленский сказал:

И что присваивать результату в таком случае? Ноль

Внимательно читаем раздел Result в https://msdn.microsoft.com/ru-ru/library/windows/desktop/ms644984(v=vs.85).aspx

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

Изменено пользователем kami
Ссылка на комментарий
  • 0
1 час назад, kami сказал:

Внимательно читаем раздел Result

Такого раздела там нет. Вы, видимо, имели в виду раздел "Return value". В частности, там сказано, что CallNextHookEx нужно вызывать в том случае, если (цитирую) the hook procedure did not process the message. То есть, насколько я понял, если моя процедура FormKeyDown установила Key:=0, то это можно трактовать в том ключе, что message обработан, и тогда CallNextHookEx вызывать необязательно. Так или не так?

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

1 час назад, kami сказал:

Тот же WH_KEYBOARD: система будет вызывать последовательно все KeyboardProc в порядке (емнип), обратном времени установки хука.

Я так и не смог понять разницы между WH_KEYBOARD_LL и WH_KEYBOARD. Мне казалось, что лишь первый вариант относится ко всей системе целиком и проверяется всеми приложениями, где есть хук, в то время как второй ограничен лишь одним-единственным приложением. Всё сложнее?

Ссылка на комментарий
  • 0
2 минуты назад, Вадим Смоленский сказал:

тогда CallNextHookEx вызывать необязательно. Так или не так?

Мало. If the hook procedure processed the message, it may return a nonzero value to prevent the system from passing the message to the rest of the hook chain or the target window procedure.

3 минуты назад, Вадим Смоленский сказал:

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

Не вижу ничего запутанного. В штатной ситуации - вызываете следующий хук. А когда нужно, чтобы клавиша не добралась до адресата - не вызываете.
Или же - вызываете, но Result возвращаете <>0.
Я не знаю, какой из двух вариантов лучше - я давно уже не работал с WinAPI в этом ракурсе. Эксперименты вам всё покажут, там делов на 20 минут.

5 минут назад, Вадим Смоленский сказал:

Я так и не смог понять разницы между WH_KEYBOARD_LL и WH_KEYBOARD.

Разные механизмы работы. Оба - system-wide.

Ссылка на комментарий
  • 0
4 часа назад, kami сказал:

Или же - вызываете, но Result возвращаете <>0.

Это непонятно. Какой именно <>0? Любой кроме ноля? А если, как советуют, присвоить CallNextHookEx(CurrentHook, nCode, wParam, lparam), то это всегда будет ноль, что ли?

Ссылка на комментарий
  • 0
В 15.06.2018 в 19:23, Вадим Смоленский сказал:

Какой именно <>0?

Что может быть непонятно во фразе non-zero value из первоисточника? Скорее всего, возвращаемый результат интерпретируется как BOOL. У любого булеан-типа есть два значения: 0 = False, не 0 = True.

Что там присваивают конкретные компиляторы для значения True - это их проблемы. Сравнение всегда ведется с нулем.

 

В 15.06.2018 в 19:23, Вадим Смоленский сказал:

А если, как советуют, присвоить CallNextHookEx(CurrentHook, nCode, wParam, lparam), то это всегда будет ноль, что ли?

Может да, а может и нет. Откуда вы знаете, какую логику заложили другие разработчики в свои хуки? Это исключительно их дело - считают ли они нужным

Цитата

to prevent the system from passing the message to the rest of the hook chain or the target window procedure.

 

Ссылка на комментарий
  • 0
17 часов назад, kami сказал:

Что может быть непонятно во фразе non-zero value из первоисточника?

Да нет, всё понятно теперь. Я просто упустил из вида, что внутри функции CallNextHookEx еще много всякого разного может происходить и ее выходное значение в данном случае - не самое главное. Кстати, в отладчике посмотрел - оно в штатном режиме действительно получается нулевым.

Так что теперь в тех случаях, когда нажатие клавиши точно обработано моим приложением и я больше не хочу от него никаких сюрпризов, просто присваиваю Result:=1. Всё работает, как часы. Огромное спасибо!

Ссылка на комментарий
  • 0
6 часов назад, Вадим Смоленский сказал:

внутри функции CallNextHookEx еще много всякого разного может происходить

По большому счету там происходит всего одно: система смотрит, кто еще поставил хук такого же типа и вызывает его функцию. И далее и далее, пока не пройдет всех использовавших SetWindowsHookEx.
Повторю - хуков в системе даже "в штатном режиме" установлено великое множество.

Ссылка на комментарий

Присоединяйтесь к обсуждению

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

Гость
Ответить на вопрос...

×   Вставлено с форматированием.   Вставить как обычный текст

  Разрешено использовать не более 75 эмодзи.

×   Ваша ссылка была автоматически встроена.   Отображать как обычную ссылку

×   Ваш предыдущий контент был восстановлен.   Очистить редактор

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

×
×
  • Создать...