Хукнемо Windows

 | 21.52

Мой Компьютер, №04 (508), 09.06.2008

Более того, если вы умеете и хотите программировать (а постоянные читатели журнала «Мой компьютер» программировать умеют), вам ничего не стоит облегчить поиск при помощи Windows и для других читателей. Для этого достаточно создать маленькие программки для простой настройки параметров реестра.

Так можно ли назвать операционную систему Windows Vista сыщиком первого класса? Можно, но с натяжкой. По одной простой причине: графические возможности Windows Vista не позволяют выполнять быстрый просмотр результатов поиска. Например, так, как это реализовано в очень простой программе AVSearch.

В общем, каждый читатель должен решить сам, достоин ли механизм поиска Windows Vista того, чтобы доверять ему сыскное дело. Если же после прочтения наших статей вы еще не определились с этим, без сомнения, мировым вопросом, тогда предлагаю вам воспользоваться справкой Windows, чтобы узнать о поиске побольше.

Для этого просто воспользуйтесь командами вида rundll32.exe ndfapi.dll NdfRunDllHelpTopic mshelp://windows/?id=идентификатор (чтобы сразу отобразить нужный раздел справки), либо же самостоятельно выполните поиск разделов. В качестве идентификатора можно указать следующие разделы:

  • 50fdc72f-3274-4a53-ad84-c82c71a320cf — сохранение результатов поиска;
  • 56cdc9d9-597e-4ea5-bd62-24eb8abaafa3 — устранение неполадок при поиске;
  • 58bc79b0-6b79-411d-9e95-629e9904f058 — можно ли включать или отключать индекс поиска Windows;
  • 68dd14f2-a9cb-4134-a076-b8abb011a1f6 — ускорение поиска в Windows при помощи индекса: вопросы и ответы.

    Цікава штука ці хуки. Тільки зважайте на те, що антивіруси з модним проактивним захистом будуть на них реагувати (рис. 1: це «Касперський» про тестову програму, яку ми напишемо в кінці). Тому в своїх програмах та документації до них вказуйте на цю особливість.

    Пам’ятайте, що хуки мають негативний вплив на швидкодію системи, адже хук-процедури викликаються для кожного повідомлення даного типу хуку. Але ж те, що після натиснення клавіші виконалось на одну процедуру більше, ніж раніше, буде для вас непомітно (якщо не написати ресурсоємну процедуру). Якщо ж необхідно виконати щось важке, то раджу запускати це в окремому треді та завершувати процедуру.

    Ланцюжки хуків

    Для кожного типу хуку система має свій список вказівників на хук-процедури. При появі нового повідомлення вона викликає першу процедуру з цього списку, а за передачу повідомлення далі по ланцюжку відповідає сама процедура, викликаючи (чи ні) функцію CallNextHookEx.

    Нова хук-процедура додається завжди.

    Хук-процедури

    В них, власне, і відбувається обробка повідомлень. Вони повинні мати ось таку сигнатуру:

    function HookProc(nCode: integer; wParam: WPARAM; lParam: LPARAM): LRESULT; stdcall;

    (Яка ж це процедура, мовляв, якщо вона функція? Насправді між процедурами і функціями тут нема ніякої різниці: якщо функції, як у випадку stdcall, повертають значення не в стеці, а в регістрі, всі вони — просто адреси в пам’яті. Тому хук-процедури я буду називати саме процедурами і назви процедурних типів доповнювати словом Proc, а не Func.)

    Типова процедура повинна мати три параметри (всі Integer’и, хоча і називаються WPARAM та LPARAM), повертати LRESULT (знову ж таки Integer) і згоду на виклик — stdcall. Якщо цього не дотриматись, то у вас буде висіти все, аж навіть сама система.

    З хуками взагалі треба обережно поводитись, щоб не тиснути потім reset. Подібне було в ДОСі, коли підміняли вектори переривань, але там для зависання досить було одного неправильного біта, а тепер у нас все просто — викликай собі функції, передавай правильні параметри (в чому допоможе компілятор — точніше, він допоможе не передати туди невірні значення).

    nCode містить деяке значення, яке процедура використовує, щоб дізнатись, що робити. Типи цих значень відрізняються: наприклад, для хуку WH_CBT (обробляє зокрема повідомлення для вікон (створити, знищити, згорнути etc)) nCode може приймати: HCBT_CREATEWND, HCBT_DESTROYWND, HCBT_MINMAX, HCBT_MOVESIZE (назви говорять самі за себе).

    Спільне для всіх типів хуків: якщо nCode менше нуля, то процедура повинна негайно повернути значення від CallNextHookEx без обробки повідомлення. wParam та lParam зазвичай містять інформацію про повідомлення, але не завжди.

    А що повертати, якщо nCode більше або рівне нулю? В цьому випадку рекомендується повернути значення, отримане викликом CallNextHookEx, якщо наша процедура не обробила повідомлення (або просто спостерігає за ними), інакше інші програми, які теж встановили такий тип хуку, можуть неправильно працювати (тому що їхні хук-процедури не викликатимуться). Якщо повернути значення, відмінне від нуля, то це заборонить відправку повідомлення решті ланцюжка хук-процедур і віконній процедурі.

    Заготовка матиме такий вигляд:

    function HookProc (nCode: integer; wParam: WPARAM; lParam: LPARAM): LRESULT; stdcall;

    begin

     if nCode < 0 then begin

     Result := CallNextHookEx(hhk, nCode, wParam, lParam);

     exit;

     end;

     // щось робимо

    end;

    Встановлення та видалення

    Для цього існують функції:

    SetWindowsHookEx(idHook: Integer; lpfn: TFNHookProc; hmod: Cardinal; dwThreadID: Cardinal);

    та відповідно

    UnhookWindowsHookEx(hhk: HHOOK);

    SetWindowsHookEx повертає значення типу HHOOK (Cardinal), яке потрібно десь зберегти, щоб викликати потім UnhookWindowsHookEx та CallNextHookEx (передача повідомлення далі по ланцюжку). idHook визначає тип хуку, вони задані константами в модулі Windows, що починаються на «WH», наприклад, WH_CALLWNDPROC (моніторить повідомлення перед відправкою їх віконній процедурі). lpfn — адреса процедури.

    Хук можна встановити глобальний (на всі треди) або на конкретний тред (параметр dwThreadID = 0, якщо на всі). Якщо ми ставимо на тред нашої програми, то все добре, але якщо на тред іншої чи глобальний, то хук-процедура має бути в dll :-).

    Але ж хвилюватись нема чого — в Делфі з динамічними бібліотеками все просто. Чому ж в dll? Справа в тому, що глобальний хук викликається в контексті тої програми, до якої відправлено повідомлення, тобто наша процедура повинна стати частинкою чужої проги. Це можливо, якщо здійснити проеціювання бібліотеки з процедурою в адресний простір процесу (цим займається Windows). Отже, якщо ми ставимо глобальний або тредоспеціалізований хук, причому тред не нашого процесу, то хук-процедура повинна бути в dll, хендл якої передаємо в параметрі hmod, lpfn має містити адресу підпрограми в динамічній бібліотеці. Інакше hmod = 0.

    Оскільки хук-процедура виконується в контексті іншого процесу, то з’являються проблеми зі зв’язком з нашою програмою, але взаємодія між процесами — то вже зовсім інша історія.

    SetWindowsHookEx може повернути 0, якщо сталася помилка, зазвичай таке можливо за неправильними параметрами (перевіряйте, чи справді бібліотека завантажена, а адреса процедури знайдена). Деякі типи хуків можуть бути лише глобальними (WH_JOURNALPLAYBACK, WH_JOURNALRECORD, WH_KEYBOARD_LL, WH_MOUSE_LL, WH_SYSMSGFILTER).

    Все це добре, от тільки чому функція називається SetWindowsHookEx (що це за «Ex»?)? Очевидно, має існувати і просто SetWindowsHook. Так, все правильно, така функція є, тільки вона застаріла (Win16API), тому забудемо про неї.

    Деякі типи хуків

    Оскільки хуків багацько, описувати кожен детально, включаючи призначення параметрів хук-процедур — дуже багато місця треба, та й не довідник пишу ж, одначе. Я прагну показати механізм використання хуків в цілому, а за конкретикою параметрів хук-процедур раджу звернутись до MSDN’у чи ще якого довідника по функціям Win32API (краще до MSDN’у).

    • WH_CALLWNDPROC, WH_CALLWNDPROCRET — дозволяють моніторити (але не змінювати) повідомлення, послані віконній процедурі. Різниця між ними полягає в тому, що перший викликається перед тим, як послати повідомлення, а другий — після того, як віконна процедура його обробила;
    • WH_CBT — хук-процедури цього типу викликаються перед створенням, активацією, знищенням, згортуванням та розгортуванням, пересуванням та зміною розміру вікон і ще в деяких випадках. Тут ми вже можемо міняти повідомлення. За допомогою цього типу хуку можемо реалізувати прив’язку всіх вікон одне до одного та до країв екрану. (Схоплюємо пересування та зміну розміру вікна, порівнюємо його координати з рештою вікон та змінюємо, якщо воно знаходиться близько до якогось іншого вікна);
    • WH_DEBUG — викликається перед викликом іншої хук-процедури. Призначений для відлагодження;
    • WH_FOREGROUNDIDLE — цей тип хуку дозволяє виконувати низькопріоритетні задачі в ті моменти, коли активний тред простоює;
    • WH_GETMESSAGE — цей хук дозволяє стежити (і корегувати!) повідомлення, які повертаються функціями GetMessage та PeekMessage;
    • WH_JOURNALRECORD, WH_JOURNALPLAYBACK — зазвичай WH_JOURNALRECORD використовують щоб відстежити серію подій від миші та клавіатури, аби пізніше їх повторити за допомогою WH_JOURNALPLAYBACK. Очевидно, їх розробили для макросів;
    • WH_KEYBOARD, WH_KEYBOARD_LL, WH_MOUSE, WH_MOUSE_LL — хуки на повідомлення клавіатури та миші. З суфіксом LL — низькорівневі, вони викликаються перед тим, як помістити повідомлення в чергу повідомлень треда, тоді як звичайні — перед поверненням функцій GetMessage та PeekMessage. Ще одна важлива деталь — LL-хуки викликаються в контексті того процесу, який їх встановив, тобто відпадає необхідність пересилати дані між процесами (якщо хук-процедура повинна зв’язатись з програмою, що її встановила). Саме низькорівнеий хук на клавіатуру я і використаю у прикладі. Процедури LL-хуків мають обробити повідомлення за час, менший, ніж вказано в реєстрі в HKEY_CURRENT_USERControl PanelDesktop, параметр LowLevelHooksTimeout (значення в мілісекундах, в моїй системі вказано 5000, що, я вважаю, занадто багато. Ви лишень уявіть: натиснули клавішу, все підвисло на 5 секунд, потім з’явилась літера). Якщо обробка буде довшою, то система просто викличе наступну процедуру з ланцюжка. WH_DEBUG не працює для LL.

    Алгоритм використання механізму хуків:

    • встановити хук (SetWindowsHookEx);
    • зняти хук (UnhookWindowsHookEx);
    • все :-).

    Хук на мишу

    Теорія, звісно, добре, але треба ж і пальці розім’яти. Напишемо невеличку, але корисну (може, хтось поставить до автозавантаження) програму. Вона дозволить кліком миші по краю вікна притулити його до відповідного краю екрану. Думаю, всі колись намагались мишею так пересунути вікно, щоб між ним і екраном не лишилось жодного пікселя. Тепер це не проблема.

    Добре, поїхали. Створимо два проекти, один — звичайна віконна програма, інший — проект DLL. Програма буде встановлювати та знімати хук, який знаходитиметься в бібліотеці. І робити це буде за допомогою функцій start та stop, які експортуватиме бібліотека. При створенні вікна ставимо, знищенні — знімаємо, всього десять рядків дописати.

    procedure TForm4.FormCreate(Sender: TObject);

    // Тут буде адреса процедури старт.

    // Тип TNoParamProc це процедура без параметрів, опишіть вище

    // ось так TNoParamProc — procedure; stdcall;

    var p: TNoParamProc;

    begin

     // Завантажимо бібліотеку. Змінна hlib типу Cardinal глобальна.

     hlib := LoadLibrary(‘mousehook.dll’);

     // Дізнаємось адресу процедури старт.

     p := GetProcAddress(hlib, ‘start’);

     // І стартуємо.

     p;

    end;

    procedure TForm4.FormDestroy(Sender: TObject);

    var p: TNoParamProc;

    begin

     // Знімаємо хук.

     p := GetProcAddress(hlib, ‘stop’);

     p;

     FreeLibrary(hlib);

    end;

    Не забудьте описати процедурний тип та глобальну змінну, вказати правильний шлях до бібліотеки.

    Бібліотека

    Глобальна змінна:

    var hhook: Cardinal;

    Процедури «старт» і «стоп», завдяки яким ми просто ставимо та знімаємо хук, треба експортувати:

    procedure start(); stdcall;

    begin

     hhook := SetWindowsHookEx(WH_MOUSE, @MouseProc, hInstance, 0);

    end;

    procedure stop(); stdcall;

    begin

     UnhookWindowsHookEx(hhook);

    end;

    Хук-процедура

    Розглянемо для типу хуку WH_MOUSE параметри процедури. nCode може приймати такі значення:

    • HC_ACTION — wParam та lParam містять інформацію про повідомлення;
    • HC_NOREMOVE — wParam та lParam містять інформацію про повідомлення, повідомлення миші не були видалені з черги повідомлень вікна.

    wParam містить ідентифікатор повідомлення миші. Може приймати такі значення: WM_LBUTTONDOWN, WM_LBUTTONUP, WM_RBUTTONDOWN, WM_RBUTTONUP, WM_MBUTTONDOWN, WM_MBUTTONUP (натиснення та відпускання лівої, правої та середньої кнопки миші). WM_LBUTTONDBLCLK, WM_RBUTTONDBLCLK, WM_MBUTTONDBLCLK (подвійне клацання кнопками миші), WM_MOUSEMOVE (рух курсору миші), WM_MOUSEWHEEL (обертання колеса миші), є аналогічні названим повідомлення, але з префіксом NC (подія відбулася в неклієнтській області вікна), і деякі інші.

    lParam — вказівник на структуру MOUSEHOOKSTRUCT.

    tagMOUSEHOOKSTRUCT = packed record

     pt: TPoint;

     hwnd: HWND;

     wHitTestCode: UINT;

     dwExtraInfo: DWORD;

     end;

    • pt — координати курсору миші;
    • hwnd — хендл вікна, яке отримає повідомлення;
    • wHitTestCode — місце влучання курсору (границя вікна, кнопка закриття);
    • dwExtraInfo — додаткова інформація, асоційована з повідомленням.

    В таблиці наведу можливі значення параметру wHitTestCode (див. табл.).

    Тепер зрозуміло, як працюють програми, які дозволяють кліком правої кнопки миші мінімізувати будь-яке вікно у трей. Точніше, зрозуміло, як вони дізнаються про те, коли і яке вікно треба згорнути.

    function MouseProc(nCode: Integer; wParam: WPARAM; lParam: LPARAM): LRESULT; stdcall;

    // Вказівник на структуру MOUSEHOOKSTRUCT.

    var p: ^MOUSEHOOKSTRUCT;

     // Тут зберігатимемо хендл вікна.

     hwnd: Cardinal;

     // Його координати, висота й ширина.

     x, y, w, h: Integer;

     // Висота й ширина екрану.

     sw, sh: Integer;

     // Прямокутник для отримання розмірів вікна та екрану.

     rect: TRect;

    begin

     // Початок стандартний.

     if nCode < 0 then begin

     Result := CallNextHookEx(hhook, nCode, wParam, lParam);

     exit;

     end;

     // Виконуватимемо якісь дії, тільки якщо натиснена

     // ліва кнопка в неклієнтській області вікна.

     if (nCode = HC_ACTION) and (wParam = WM_NCLBUTTONDOWN) then begin

     // Це можна закоментувати, зазвичай я використовую цю функцію під час відлагодження хуків.

     Beep();

     // Отримуємо вказівник на структуру та хендл вікна.

     p := Pointer(lParam);

     hwnd := p.hwnd;

     // Дізнаємось координати вікна

     GetWindowRect(hwnd, rect);

     x := rect.Left;

     y := rect.Top;

     w := rect.Right — rect.Left;

     h := rect.Bottom — rect.Top;

     // та розміри екрану.

     SystemParametersInfo(SPI_GETWORKAREA, 0, @rect, 0);

     sw := rect.Right — rect.Left;

     sh := rect.Bottom — rect.Top;

     // В залежності від місця натиснення кнопки миші

     // пересуваємо вікно в те чи інше місце.

     case p.wHitTestCode of

       HTLEFT: SetWindowPos(hwnd, HWND_TOP, 0, y, w, h, SWP_NOSIZE);

       HTTOP: SetWindowPos(hwnd, HWND_TOP, x, 0, w, h, SWP_NOSIZE);

       HTRIGHT: SetWindowPos(hwnd, HWND_TOP, sw — w, y, w, h, SWP_NOSIZE);

       HTBOTTOM: SetWindowPos(hwnd, HWND_TOP, x, sh — h, w, h, SWP_NOSIZE);

     end;

     end;

     // Кінець теж стандартний.

     Result := CallNextHookEx(hhook, nCode, wParam, lParam);

    end;

    Треба тільки підключити модулі Windows (для хуків) та Messages (для повідомлень). Якщо ви знайшли цю програму корисною для себе, то рекомендую додати ще кілька функцій: по кліку на кутку вікна рухати його до відповідного кутка екрану (+4 рядки), по кліку середньою кнопкою по границі центрувати вікно по горизонталі чи вертикалі, по кутку — пересунути вікно до середини екрану. А на праву кнопку можна повісити розгортання (і згортання назад, але це вже складніше) вікна на всю довжину чи ширину. А далі можна замислитись над згортанням вікон в трей, зміною статусу «поверх усіх» і т.д. Якщо підійти до цього серйозно, то можна спробувати навіть заробити на shareware :-).

    В хук-процедурі я використав пару функцій SetWindowPos та SystemParametersInfo. Остання функція ну просто велетенська, вона дає можливість отримувати та встановлювати кілька десятків різних системних параметрів, я за її допомогою дізнавався розміри робочої області екрану (зона не зайнята таскбаром та іншими панелями). SetWindowPos встановлює позицію вікна, що включає в себе координати вікна, його розміри, положення у Z-порядку. В прикладі вікно опиниться поверх всіх за допомогою параметру HWND_TOP, положення вікна задається координатами лівого верхнього кута та довжиною, шириною, останній флаг SWP_NOSIZE вказує, що розміри вікна міняти не слід.

    В програмі я не писав ніяких перевірок на правильність завантаження бібліотеки та знаходження адрес процедур, тому якщо ви вкажете неправильний шлях до бібліотеки чи перекрутите назву процедур, будуть помилки, тож будьте уважні.

    Як бачите, потужним механізмом хуків користуватись дуже просто, головне — знати призначення параметрів хук-процедур (в MSDN все описано детально, от тільки не по-нашому, що втім не повинно вас спинити).

    Ярик УЛАНОВИЧ aka Mahpella

Robo User
Web-droid editor

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *