Эта статья впервые была опубликована в ClarionOnline ( http://www.clariononline.com ) в volume 2 issue 3 (Октябрь1999)
Фильтры сообщений 101
Иногда возникает необходимость перехвата нажатий на клавиши до цикла ACCEPT Clarion. Если вы читали одну из моих предыдущих статей, то вы уже, наверное, думали об sub-class процедурах и перехвате событий WM_KEYDOWN и WM_KEYUP до того как они дойдут до цикла ACCEPT. Если вы пытались это сделать, то обнаружили, что это не работает - subclas процедура никогда не получает события WM_KEYDOWN, которое вы ожидали. Почему? Хорошо, только несколько человек в Лондоне знают ответ, но к счастью есть решение этой проблемы. Это использывание hook-а, или фильтра сообщений.
В соответствии с документацией на Windows API:
"Hook это точка механизме обработки событий Windows где приложение может установить подпрограмму для отслеживания сообщений в системе и обработать некоторые типы сообщений до того как они достигнут целевой процудуры окна. Этот [help] раздел описывает Windows hooks и объясняет как использовать их в приложениях Windows."
В великой схеме вещей, написание фильтра сообщений не очень сложно. Однако, если вы никогда не писали их раньше, это может представлять некоторую сложность. Есть пара важных вещей, которые нужно помнить при написании фильтра:
Get(A,1) Message(Errorcode()) Message(Errorcode())
Видите проблему? GET() в файл A прекрастно работает, и вы получите errorcode=0. Когда вы нажмете <Enter> для закрытия окна сообщения, ваш фильтр сделает GET() в файл B. если вы не транслируете клавишу Enter, то оно свалится с кодом ошибки 33 и второй вызов MESSAGE() покажет 33, а не 0. Это первый пример как процедура фильтра действует на приложение - я знаю, это заняло у меня 2 дня поиска, когда я писал свой первый 16-разрядный клавиатурный фильтр. Решение? Внутри процедуры фильтра, если GET() в файл B неудачен, то делайте что либо, что вы знаете будет работать, наподобие GET(B,1) или CLOSE(B);OPEN(B).
Итак, как вы на самом деле можете это сделать? Код, который я хочу показать, это 32-разрядный фильтр, который запрещает буквы клавиатуры J,P и I. Как я заметил выше, он не будет перехватывать клавиши, которые пользователь нажимает в окне DOS - но, я программист Windows, и что люди делают в окне DOS это их личное дело.
Еще одна вещь, которую вы вероятно заметили, это мои прототипы и использование Windows API. Я предпочитаю прототипировать все используя тип Long, и использую функцию ADDRESS() для передачи параметров. Это только мое персональное предпочтение, и так как в конце концов передается то же самое, то не важно при помощи какого прототипа это достигается.
Код приложения, которое устанавливает фильтр довольно прост:
Program
Map
Module('DLL32')
InstallHook()
RemoveHook()
End
End
FilterWindow WINDOW,AT(,,72,29),GRAY
Prompt('Извините, я только запрещаю клавиши J,P и I'),AT(5,1,59,28),USE(?Prompt1)
END
Code
Open(FilterWindow)
InstallHook
Accept
End
Close(FilterWindow)
RemoveHook
После открытия окна или application frame, просто вызовите функцию вашего DLL для установки фильтра. Закрытие приложения удаляет фильтр.
Установка 32-разрядного фильтра
Код в DLL, как вы могли ожидать, немного сложнее. Он выглядит наподобие этого:
LocalVars Group,Pre(Loc)
DLLName CString(20)
DllNamePtr ULong
KHName CString(50)
KHNamePtr Ulong
MHName CString(50)
MHNamePtr Ulong
ThreadID DWord
DLLInstance hInstance
End
! Valid Hook types
WH_Min Equate(-1)
WH_MsgFilter Equate(-1)
WH_JournalRecord Equate(0)
WH_JournalPlayback Equate(1)
WH_Keyboard Equate(2)
WH_GetMessage Equate(3)
WH_CallWndProc Equate(4)
WH_CBT Equate(5)
WH_SysMsgFilter Equate(6)
WH_Mouse Equate(7)
WH_Hardware Equate(8)
WH_Debug Equate(9)
WH_Shell Equate(10)
InstallHook Procedure
Code
HV:InstalledOK = False
Loc:DLLName = 'FILTER.DLL'
Loc:DllNamePtr = Address(Loc:DLLName)
Loc:DLLInstance = LoadLibrary( Loc:DllNamePtr )
If Loc:DLLInstance <> 0
Loc:KHName = 'HookProcedure'
Loc:MHNamePtr = Address(Loc:KHName)
HV:Hook = GetProcAddress(Loc:DLLInstance,Loc:MHNamePtr)
If HV:Hook <> 0
HV:PrevHook = SetWindowsHookEx(WH_Keyboard,HV:Hook,Loc:DLLInstance,0)
If HV:PrevHook <> 0
HV:InstalledOK = True
End
End
End
Показанный выше пример кода используется для установки общесистемного (system-wide) перехватчика клавиатуры. Сначала мы выполняем LoadLibrary на смого себя, чтобы получить хендл DLL.
Затем выполняется GetProcAddress для получения адреса перехватчика клавиатуры. Эти две вещи нам нужны для передачи, когда мы будем устанавливать свой перехватчик.
Наконец, предполагая что мы имеем нужную информацию, устанавливаем свой hook. При этом мы передаем тип перехватчика, адрес процедуры и хендл DLL. Так как мы устанавливаем системный hook, а не hook приложения, мы передаем в последнем параметре 0 для индикации того что hook ассоциируется со всеми существующими потоками (threads).
Снятие 32-разрядного фильтра
Удаление перехватчика это сама простота:
RemoveHook Procedure Code If HV:InstalledOK = True x# = UnhookWindowsHookEx( HV:PrevHook ) End
Если hook установлен корректно, то удаляем его.
Процедура фильтра
Процедура фильтра содержит код, который выполняется при каждом нажатии на клавишу (кроме нажатий в окне DOS).
Надо знать что имеется несколько разных типов фильтров, которые вы можете установить - помните все equates в процедуре InstallHook()? Да, прототипы фильтров одинаковы, но смысл переменных, как их обрабатывать, и код возврата из процедуры фильтра: все зависит от типа устанавливаемого фильтра. Поэтому, не изменяйте тип фильтра без проверки того, что параметры означают то же самое. Иначе ваш компьютер быстро станет грушевидным, и вы, скорее всего, будете перезагружаться.
HookProcedure Function(Prm:nCode, Prm:wparam, Prm:lparam)
LocalVars Group,Pre(Loc)
ProcessedEvent Byte
ReturnValue DWord
End
Key_Up Equate(10000000000000000000000000000000B)
VK_I Equate(049H)
VK_J Equate(04AH)
VK_P Equate(050H)
Code
If Prm:nCode < 0
Loc:ProcessedEvent = False
Else
! Здесь будет ваш код
Do EatKeyStroke ! Мой пример запрещает клавиши
End
If ~Loc:ProcessedEvent
Loc:ReturnValue = CallNextHookEx(HV:PrevHook,Prm:nCode,Prm:wParam,Prm:lParam)
End
Return( Loc:ReturnValue )
EatKeyStroke Routine
If ~Band(Prm:lParam,Key_Up)
If Prm:wParam = VK_J OR Prm:wParam = VK_P OR Prm:wParam = VK_I
Loc:ReturnValue = 1
Loc:ProcessedEvent = True
End
End
Я действительно установил фильтр WH_KEYBOARD, в соответствии с документацией на фильтры тира WH_KEYBOARD:
"Если nCode меньше нуля, hook procedure должна передать сообщение в функцию CallNextHookEx без последующей обработки и должна возвратить значение, возвращаемое из CallNextHookEx"
Итак, сначала я определил, можно ли обрабатывать нажатие. Если можно, то далее я делаю то что я действительно хочу делать с нажатием. В этом примере я запрещаю клавиши J,P и I, поэтому сначала я проверяю что это: нажатие или отпускание клавиши.
После того как я определил, что это нажатие нужной клавиши, я просто устанавливаю флаг, сообщающий мне что я обработал событие, и соответственно устанавливаю возвращаемое значение:
Снова, документация на Windows API гласит:
"Для предотвращения Windows от передачи сообщения по цепочке hook-ов или в процедуру целевого окна, возвращаемое значение должно быть не равно нулю. Чтобы разрешить Windows передавать сообщение в процедуру целевого окна, пропустив остальные процедуры в цепочке, возвращаемое значение должно быть равно нулю."
Поэтому, для запрещения клавиш J, P и I, все что я должен сделать это возвратить ненулевое значение из процедуры фильтра, и эти клавиши не будут переданы в процедуру окна (subclass процедуру Clarion) или в цикл Accept(). Если мы не обрабатываем событие сами, то нужно передать это нажатие в следующий hook в цепочки.
Просто, не правла ли?
Трансляция нажатий внутри фильтра
Одним из применений фильтра является трансляция одних клавиш в другие. Вы можете захотеть написать фильтр, который реверсирует алфавит, так что когда вы нажимаете A получаете Z; B в X; C в W и т.д . Для того чтобы поместить новые нажатия в ваше приложение, вы, вероятно, попытаетесь использовать PRESS или PRESSKEY. Это может работать или не работать, в зависимости от того, что я еще не вычислил.
Это может быть сделано используя то, что Clarion сам использует фильтр WH_JOURNALPLAYBACK для команды PRESS, которая конфликтует с нашей процедурой. В результате этого PRESS или PRESSKEY могут у вас не работать, и ваше новое нажатие "застрянет" в процедуре фильтра не дойдя до цикла ACCEPT.
Важно знать что PRESS и PRESSKEY кажется всегда работают внутри самого приложения. Учитывая это, здесь представлено одно решение:
Accept
If Event() > Event:TranslateStart And Event() < Event:TranslateStop
Presskey(Event() - Event:TranslateStart)
End
Case Event()
Of ….
End
End
Все что мы здесь сделали - это выдумали довольно сложный способ передачи в приложение новой клавиши. Есть множество других путей сделать это (передать пользовательское событие в приложение и have it query a variable in the filter DLL for the new keycode is another), но важно помнить что всегда есть какое-то решение. Это может быть более трудоемко, чем вы ожидали, но оно есть.
Рассмотрение 16-разрядного варианта
Написание 16-разрядного фильтра не сильно отличается от написания 32-разрядного. Имена процедур Windows API слегка отличаются (удалите символы EX из конца), и это в основном все.
Вместо использования LoadLibrary и GetProcAddress в процедуре установки для получения хендла DLL, можно использовать System{PROP:appinstance} как экземпляр DLL и Address(HookProcedure) как хендл процедуры.
В 16-разрядах все вычисляется также, но 32-разрядные числа сильно отличаются. Вот почему в 32-разрядах вы должны использовать Windows API вместо загрузки DLL и получения процедуры фильтра.
Выводы
Пример, который я здесь представил, является очень общим примером клавиатурного фильтра. Имеется множество различных типов фильтров, каждое из которых подходит для своей задачи. Убедитесь что вы используете правильный тип фильтра, и всего наилучшего!
Back to my home page. Or send me mail at paula@attglobal.net
(c) 1999 Paul Attryde