Хуки в Windows. Часть третья. Оконные функции
Здравствуйте, программисты и им сочувствующие! После долгого перерыва представляю к вашему вниманию третью статью про механизм хуков в системах MS Windows. В первых двух статьях про хуки речь шла о клавиатурных хуках и хуках на события создания и уничтожения окон. В этой статье мы будем говорить про самое интересное: про хуки на оконные функции. Хуки на оконные функции являются самым мощным и гибким типом хуков, с их помощью можно сделать всё что угодно касающееся пользовательского интерфейса.
Итак, приступим. Сначала вспомним, что мы знаем. Пользовательский интерфейс полностью базируется на окнах. Окно, также его называют «контролом» (что будет правильнее), является фундаментальным понятием в пользовательском интерфейсе (GUI) Windows. Почти все, что вы можете видеть на экране компьютера с операционной системой Windows состоит из окон. Что же такое окно вернее «контрол»? Окно это объект подсистемы GUI обладающей рядом специфических свойств. Кнопка, листбокс, эдит, форма и т.д. это всё окно.
Начнём сначала, как описываются окна? Все контролы описываются с помощью хендлов. Хендлы контролов отличаются от хендлов объектов ядра (файлов, процессов и других системных объектов), в отличие от хендлов объектов ядра, которые индивидуальны для каждого процесса, хендлы контролов общие для всех процессов. Как уже было сказано, окна не являются объектами ядра системы, т.е. по сути, ядро системы об окнах ничего не знает и вся реализация подсистемы GUI находится в пользовательском режиме в библиотеке user32.dll (по большей части).
Вернёмся в теме, каждый контрол имеет два следующих ключевых свойства: оконную функцию и класс (имя). Эти свойства есть любого контрола. Класс задаёт основные свойства контрола, оконная функция обрабатывает сообщения, отправляемые этому окну. Окна взаимодействуют с «окружающим миром» с помощью сообщений. Сообщения, присылаемые окну выстраиваются в очереди. У каждого потока есть очередь сообщений.
В любом GUI приложении как минимум в одном потоке есть что-то напоминающее этот цикл
while (GetMessage(&msg, (HWND) NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
Функция GetMessage производит получение сообщения из очереди (иногда можно встретить вариант и использование функции PeekMessage, но у неё немного другое поведение). Функция TranslateMessage производит конвертирование сообщений связанных с клавиатурой (WM_KEYDOWN и WM_KEYUP) в сообщения типа WM_CHAR. Функция DispatchMessage производит обработку сообщения. Именно эта функция в последствии вызывает функция обработчик этого контрола.
Теперь разберёмся с оконной функцией. Это функция обратного вызова (CALLBACK) задаётся самой программой, при создании класса, она производит обработку сообщений, либо вызывает общую функцию для всех контролов DefWindowProc.
Теперь разберёмся с типами хуков, которые позволят нам отлавливать события связанные с оконными функциями:
Хук типа WH_CALLWNDPROCRET не представляет для нас никакого практического интереса. В этой статье я расскажу про хуки типа WH_CALLWNDPROC и WH_GETMESSAGE.
Хуки WH_CALLWNDPROC
Итак, обработчики этого типа хука вызываются непосредственно перед вызовом оконной функции. Этот тип хуков, как и любой другой тип хуков, позволяет отменить обработку сообщения, не позволяет изменить само сообщение. Прототип функции обработчика такой же, какой и любого другого типа хуков, вот только назначение параметров другое.
LRESULT CALLBACK CallWndProc(
int nCode,
WPARAM wParam,
LPARAM lParam
);
Назначение первого параметра такое же, как у всех хуков, если он равен HC_ACTION, функция должна обработать сообщение, таким образом, каким ей захочется. Если первый параметр меньше нуля, то функция обязана вызвать следующую функцию-обработчик хука (CallNextHookEx). Второй параметр говорит нам какой процесс отправил сообщение, если текущий то не ноль, если другой то ноль. Третий параметр является указателем на структуру CWPSTRUCT.
typedef struct tagCWPSTRUCT {
LPARAM lParam;
WPARAM wParam;
UINT message;
HWND hwnd;
} CWPSTRUCT;
Эта структура является упрощённым вариантом структуры MSG. Тем не менее, как бы ни изменяли её, никакого эффекта мы не заметим.
Теперь приступим к практике. На форуме программистов часто задаются вопросы типа «Как отловить нажатие на кнопку в чужом приложении?», а также куча других похожих вопросов. Все эти вопросы решаются одинаково: создаём хук WH_CALLWNDPROC и отлавливаем сообщения WM_LBUTTONDOWN (либо другие похожие).
Общаться обработчик хука с «сервером» будет, как я писал в предыдущей статье с помощью механизма файлового мэппинга: в общей области памяти будем хранить хендл контрола клики, на котором будем отлавливать, а также будем хранить счётчик нажатий. Синхронизироваться обработчики будут по-прежнему, с помощью мьютексов.
Итак, напишем обработчик, который будем отлавливать клики на указанной «сервером» кнопке.
function CallWndProcHook(CODE, WParam, LParam: DWORD): DWORD; stdcall;
var
_CurrMessage: TCWPStruct;
begin
Result:=CallNextHookEx(HookHandle, CODE, WParam, LParam);
if CODE=HC_ACTION then
begin
_CurrMessage:=PCWPStruct(LParam)^;
if (_CurrMessage.hwnd=HWND(SharedBuffer^)) and
(_CurrMessage.message=WM_MOUSEACTIVATE) and
((_CurrMessage.lParam shr 16)=WM_LBUTTONDOWN) then
begin
WaitForSingleObject(SyncMutexHandle,INFINITE);
//
Inc(DWORD(pointer(DWORD(SharedBuffer)+4)^));
//
ReleaseMutex(SyncMutexHandle);
end;
end;
end;
В первых четырёх байтах общей области памяти будет храниться хендл кнопки, а в следующих четырёх байтах будет храниться переменная-счётчик кликов. Приложение было проверено на кнопках «равно» Калькулятор и кнопке «Пуск» (прежде чем запускать хук необходимо запустить приложение, в котором находится кнопка, так как обработчик должен знать хендл контрола). Экспериментально было выяснено, что чаще всего при клике посылается сообщение WM_MOUSEACTIVATE с флагом uMsg WM_LBUTTONDOWN.
Полный код примера находится в архиве с исходниками прилагающемся к этой статье.
Хуки WH_GETMESSAGE
Хуки этого типа более гибкие, чем хуки типа WH_CALLWNDPROC. Они позволяют помимо удаления сообщения из очереди также изменять само сообщение. В отличие от хука WH_CALLWNDPROC, когда обработчик вызывает непосредственно перед вызовом оконной функции, обработчик хука WH_GETMESSAGE срабатывает при вызове функции GetMessage, это даёт нам полный контроль над сообщением, и мы можем его изменить ещё до того, как приложение узнает о его существовании.
На форуме программистов довольно-таки часто встречаются вопросы типа «Как переназначить клавиши в конкретном окне?». Зная возможности хука WH_GETMESSAGE, можно осуществить почти всё что угодно касающееся пользовательского интерфейса. Итак, напишем хук, который производит перенаправление клавиши «1» на клавишу «2» в блокноте.
function GetMsgProcHook(CODE, WParam, LParam: DWORD): DWORD; stdcall;
var
_msg:PMsg;
begin
if CODE=HC_ACTION then
begin
_msg:=PMsg(LParam);
if (_msg^.hwnd=HWND(SharedBuffer^)) then
if (_msg^.message=WM_KEYDOWN) or (_msg^.message=WM_KEYUP) then
if _msg.wParam=ord('1') then _msg.wParam:=ord('2');
end;
Result:=CallNextHookEx(HookHandle, CODE, WParam, LParam);
end;
Вот, собственно и всё. Принцип его работы элементарен: при получении сообщений WM_KEYDOWN и WM_KEYUP производится изменение поля wParam, если оно содержит виртуальный код переназначаемой клавиши. Если убрать строку с проверкой хендла, то хук будет переназначать клавиши во всей системе.
Перед запуском хука блокнот должен быть запущен. Полный код примера находится в архиве с исходнкиами прилагающемуся к этой статье.
Как видно хук типа WH_GETMESSAGE является самым мощным из всех изученных. К примеру, хук WH_KEYBOARD, по сути, является частным случаем этого хука. Разумеется, абсолютно всего не сделаешь с помощью хуков, к примеру, мы всё равно не сможем осуществить перехват системных таких комбинаций как Ctrl-Alt-Del или Ctrl-Shift-Esc, но это уже совсем другая история (о перехвате этих комбинаций я уже рассказывал в этой статье).
Напоследок надо дать полезный совет. В обработчиках хуков WH_GETMESSAGE и WH_CALLWNDPROC следует минимизировать обращения к функциям SendMessage и PostMessage, а также к функциям, которые сводятся к вызову этих двух функций, так как это может привести к повторному вызову обработчика хука и к бесконечной рекурсии. И ещё один совет: обработчики данного хука срабатывают при каждом обращении к оконным функциям (примерно сотни раз в секунду), поэтому их следует минимизировать, так как чем «тяжелее» обработчик, тем больше тормозов на всей системе.
Вот собственно и всё что я хотел рассказать в третьей статье про хуки.
Спасибо. помогло