Эта статья впервые была опубликована в 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 кажется всегда работают внутри самого приложения. Учитывая это, здесь представлено одно решение:

  1. Объявите 2 пользовательских события EVENT:TRANSLATESTART и EVENT:TRANSLATESTOP как EVENT:User+1000 и EVENT:USER+2000 соответственно.
  2. Стартуйте новый поток (thread) в вашей главной (main) программе. Убедитесь, что thread имеет окно (и поэтому оператор ACCEPT), и убедитесь что вы сохранили где нибудь номер thread-а.
  3. Напишите код вашего фильтра с нужной логикой трансляции нажатий клавиш. Если вам нужно передать в главное приложение новое нажатие, вычислите equate code новой клавиши. Например, если пользователь нажимает A, а новый код посылаемый в приложение должен быть Z, это число 5AH или 90 десятичное.
  4. Выполните Post(EVENT:TRANSLATESTART + 90) в новый tread (вот почему нужно сохранить номер thread-а).
  5. В цикле ACCEPT нового tread-a, сделайте что то наподобие:
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