Panel użytkownika
Nazwa użytkownika:
Hasło:
Nie masz jeszcze konta?
Autor: 'Złośliwiec'
Biblioteki C++

Haki

[lekcja]
Enkapsulacja różnych niskopoziomowych operacji w funkcjach Windows API to dla programistów niewątpliwie dobra rzecz. Dzieki temu nie musimy się na przykład grzebać w dokumentacji naszego hardware'u i martwić o to, czy nasz program uruchomi się na innych konfiguracjach sprzętowych (to ostatnie oczywiście nie do końca jest prawdą, ale z pewnością bez WinAPI byłoby pod tym względem dużo, dużo gorzej).

Jednak zdarzają się sytuacje, że ta enkapsulacja jest dla nas niemałą przeszkodą. Czasem chciałoby się dostać do "bebechów" Windowsa i trochę pozmieniać jego działanie. W jednej z poprzednich części tego kursu poznaliśmy takie techniki, jak subclassing i superclassing, pozwalające przede wszystkim dokonać pewnych zmian w kontrolkach. Miały one jednak dość wąskie pole zastosowań i sporo ograniczeń. Na szczęście nie są jedynym, co w tej sprawie może nam przyjść z pomocą.

Haki (ang. hooks) to takie miejsca w mechanizmie komunikatów, w których możemy "podstawić" własną procedurę, przechwytującą te komunikaty, zanim dotrą one do swojej docelowej procedury okna. Owa "własna procedura" może sama podejmować decyzję, co z takim komunikatem zrobić. Przede wszystkim - może go przesłać dalej (do docelowej procedury okna) lub też unicestwić. Może również wyciągać różne informacje z komunikatu i wykorzystywać ich do własnych, cnych lub niecnych, celów :-).

Przykładowe użycie takiego mechanizmu możemy zaobserwować w znanym i (z konieczności) lubianym komunikatorze GG. Jak powszechnie wiadomo, gdy wyjdziemy na kawę, słoneczko na ikonie programu zachodzi za chmurkę. Skąd GG wie, że użytkownik gdzieś polazł? Oczywiście musi założyć globalny hak, monitorujący komunikaty klawiatury i myszy - jeśli przez określony czas nie przyjdzie żaden taki komunikat, GG uznaje, że użytkownik "zaraz wraca". I rzeczywiście, w katalogu z programem znajdziemy plik gghook.dll - niewątpliwie zawiera on procedurę hakową, o której zaraz się dowiemy coś więcej.

Zanim przejdziemy dalej, umówmy się jeszcze, że odtąd dla uproszczenia hakiem będziemy zwali samą procedurę, która przechwytuje "cudze" komunikaty systemowe.

Rodzaje haków

Nie ma jednego uniwersalnego haka, który potrafiłby przechwycić każdy komunikat. Zależnie od potrzeb musimy sobie wybrać odpowiedni rodzaj:
 
  • WH_CALLWNDPROC oraz WH_CALLWNDPROCRET - pozwalają monitorować komunikaty przeznaczone dla konkretnej procedury okna. WH_CALLWNDPROC przechwytuje komunikat przed jego dotarciem do procedury okna, WH_CALLWNDPROCRET - po.
  • WH_CBT - nazwa pochodzi od computer-based training (CBT), czyli komputerowej edukacji, dla którego to rodzaju aplikacji przede wszystkim powstał ten hak. W każdym razie hak WH_CBT pozwala przechwytywać komunikaty przed utworzeniem, zniszczeniem, minimalizacją, maksymalizacją, przeniesieniem, zmianą rozmiaru (uff...) okna, przed wykonaniem polecenia systemowego, przed zdjęciem komunikatu myszy lub klawiatury z kolejki komunikatów, przed ustawieniem fokusa oraz przed synchronizacją kolejki komunikatów. Dzięki temu możemy określić, czy nasza aplikacja "pozwala" na te operacje czy też nie. Teraz już co niektórzy mogą kojarzyć - tak, wszelkiego rodzaju komputerowe testy, quizy i programy egzaminacyjne korzystają z tego, by zablokować biednym studentom dostęp do pomocy naukowych ;-).
  • WH_DEBUG - ten hak wywoływany jest zawsze przed wszystkimi innymi hakami, więc można go użyć do "odpluskwiania" innych haków
  • WH_FOREGROUNDIDLE - hak pozwalający nam wykonywać różne zadania w tle, gdy system wykryje, że pierwszoplanowy wątek nic w danej chwili nie robi
  • WH_GETMESSAGE - pozwala przechwycić komunikat, który właśnie ma być zwrócony przez GetMessage lub PeekMessage (w przeciwieństwie do WH_CALLWNDPROC, który przechwytuje komunikat już po wywołaniu DispatchMessage). To jest właśnie najbardziej zalecany hak do przechwytywania komunikatów związanych z myszą i klawiaturą
  • WH_JOURNALPLAYBACK - ciekawy hak, który umożliwia odtworzenie "nagranych" wcześniej zdarzeń wejściowych (np. ruchu myszy). Jest to hak globalny, dotyczący wszystkich wątków w systemie.
  • WH_JOURNALRECORD - "nagrywa" zdarzenia do późniejszego wykorzystania przez WH_JOURNALPLAYBACK; oczywiście to również jest hak globalny
  • WH_KEYBOARD_LL - przechwytuje niskopoziomowe (low-level) zdarzenia, wywołane przez klawiaturę
  • WH_KEYBOARD - przechwytuje wysokopoziomowe zdarzenia klawiatury (jest to więc wyspecjalizowany wariant haka WH_GETMESSAGE)
  • WH_MOUSE_LL - przechwytuje niskopoziomowe zdarzenia myszy
  • WH_MOUSE - przechwytuje wysokopoziomowe zdarzenia myszy (kolejna wyspecjalizowana wersja WH_GETMESSAGE)
  • WH_MSGFILTER oraz WH_SYSMSGFILTER - dzięki tym hakom możemy monitorować komunikaty pochodzące z menu, pasków przewijania, message box'ów oraz okien dialogowych. Możemy też wykrywać aktywację okna przy pomocy Alt+Tab lub Alt+Esc. Normalnie nie możemy tego wszystkiego zrobić, gdy wykonywana jest jakakolwiek pętla modalna (np. wewnętrzna pętla message box'a). WH_SYSMSGFILTER ma działanie globalne, zaś WH_MSGFILTER dotyczy jedynie aplikacji, która zainstalowała hak
  • wreszcie WH_SHELL został stworzony po to, byśmy w aplikacji shell'owej (jeśli takową piszemy) mogli otrzymywać istotne powiadomienia systemowe. Domyślnie jest to aktywacja aplikacji shell'owej oraz utworzenie/zniszczenie jej głównego okna.

W tym momencie może się jeszcze wydawać, że większość wymienionych rodzajów haków wzajemnie duplikuje swoją funkcjonalność i co za tym idzie, jest to tylko niepotrzebna nadmiarowość wyboru. Zauważmy jednak, że niemal każdy hak działa na nieco innym poziomie w
indowsowego mechanizmu komunikatów. W prostych przypadkach najczęściej nie ma dużej różnicy, jakiego haka użyjemy, jednak docenisz tę różnorodność, gdy tylko spróbujesz napisać coś naprawdę złożonego :-).

Przykład - przechwytywanie klawiszy

Pora zrobić wreszcie coś konkretnego z hakami. Zacznijmy od przechwytywania klawiatury. Normalnie obsługujemy ją przez komunikaty w rodzaju WM_KEYDOWN. Takie komunikaty generuje dla nas system, często po wielu dość skomplikowanych manewrach. W wyniku tych manewrów czasami dostajemy już jakieś zdarzenie wyższego poziomu. I tak na przykład wciśnięcie kombinacji Alt+F4 zamknie nasze okno i nie będziemy mieli szansy obsłużenia tego przez WM_KEYDOWN. Chyba, że założymy odpowiedni hak:
 
C/C++
HHOOK g_MyHook = NULL;
 
Funkcja SetWindowsHookEx założy hak i zwróci do niego uchwyt. Powinniśmy ją wywołać tam, gdzie wykonujemy inicjalizację naszej aplikacji - np. po stworzeniu głównego okna:
 
C/C++
g_MyHook = SetWindowsHookEx( WH_KEYBOARD, & AltF4Proc, NULL, GetThreadId( GetCurrentThread() ) );
 
Tutaj skojarzyliśmy nasz hak tylko z bieżącym wątkiem. Możemy jednak również tworzyć globalne haki, tyle że takie musimy umieszczać wewnątrz DLL.
 
Od razu, żeby nie zapomnieć, zadbamy o usunięcie haka w momencie zakończenia naszej aplikacji:
 
C/C++
case WM_DESTROY:
UnhookWindowsHookEx( g_MyHook );
PostQuitMessage( 0 );
break;
 
Teraz najważniejszy etap zabawy, czyli stworzenie procedury hakowej. Jej deklaracja wygląda u nas tak:
 
C/C++
LRESULT CALLBACK AltF4Proc( int code, WPARAM wParam, LPARAM lParam );
 
Jak widać, jest całkiem podobna do procedury okna i nie jest to przypadkowe podobieństwo. Pełniona przez nią rola to również obsługa przychodzących komunikatów, różni się właściwie tylko momentem, w którym te komunikaty obsługujemy.
 
Jak się zająć komunikatem, który już dotrze do naszej procedury hakowej? Przede wszystkim musimy zbadać wartość parametru code. Jeśli jest ona mniejsza od 0, to powinniśmy zrezygnować z dalszego przetwarzania i od razu wywołać CallNextHookEx() i zwrócić wartość od niej otrzymaną. Jak się być może domyślasz, funkcja CallNextHookEx() woła kolejną procedurę hakową typu WH_KEYBOARD, o ile taka została zainstalowana. Wywoływanie tej funkcji pozwala więc na współdziałanie wielu haków, pochodzących z różnych aplikacji.
 
Jeśli code jest większe lub równe 0, to możemy normalnie obsłużyć komunikat. Załóżmy, że chcemy aby nasza aplikacja po wciśnięciu Alt+F4 nie zamykała się, tylko wyświetlała jakąś wiadomość. Musimy sprawdzić, czy klawisz którego dotyczy komunikat to F4 oraz dodatkowo czy wciśnięto prawy Alt. Parametr wParam zawiera kod wirtualnego klawisza, zaś lParam dodatkowe flagi - nas interesuje bit numer 29, ustawiony gdy naciśnięto Alt. Maska bitowa odpowiadająca tej fladze to 536870912 (czyli 2^29). Czynimy więc następująco:
 
C/C++
LRESULT CALLBACK AltF4Proc( int code, WPARAM wParam, LPARAM lParam )
{
    if( code < 0 ) return CallNextHookEx( 0, code, wParam, lParam );
   
    if( wParam == VK_F4 &&( lParam & 536870912 ) )
    {
        MessageBox( hwnd, "Nie zamkniesz mnie tak łatwo!", NULL, MB_ICONEXCLAMATION );
        return 1;
    }
   
    return CallNextHookEx( 0, code, wParam, lParam );
}
 
Skoro już przekonaliśmy się, że użytkownik nacisnął Alt+F4, to pokazujemy naszą głupią wiadomość i zwracamy wartość niezerową, co oznacza, że obsłużyliśmy komunikat i nie chcemy przekazywać go dalej. To powoduje, że jest on "konsumowany" i że użytkownik nie będzie mógł w ten sposób zamknąć okna (za to nadal może kliknąć przycisk "X").

Przykład - odtwarzanie akcji

Teraz coś jeszcze zabawniejszego. Wykorzystamy parę haków WH_JOURNALRECORD i WH_JOURNALPLAYBACK, by odtworzyć dokładnie czynności, które przed chwilą wykonał użytkownik.
 
Najpierw musimy te czynności zarejestrować, instalując hak WH_JOURNALRECORD. Ponieważ jest to hak globalny, umieścimy kod jego procedury w bibliotece DLL. Nie jest to wprawdzie obowiązkowe dla WH_JOURNALRECORD, ale już dla WH_JOURNALPLAYBACK - owszem, więc żeby mieć wszystko w jednym miejscu, zarówno procedurę dla WH_JOURNALRECORD, jak i dla WH_JOURNALPLAYBACK wrzucimy do DLL.
 
Tworzenie DLL omówione jest w odpowiednim artykule, więc nie będziemy się powtarzać. Zaczynamy od niezbędnych deklaracji:

C/C++
#include <vector>
using namespace std;

vector < EVENTMSG >
g_Events;
int g_CurrentEvent;
bool g_PlaybackStopped;
HHOOK g_RecordHook = NULL;
HHOOK g_PlaybackHook = NULL;
DWORD g_StartTime = 0;
HINSTANCE g_hInst = NULL;
 
Wszystkie akcje wykonywane przez użytkownika będziemy przechowywać w wektorze z STL - nazwiemy go g_Events. Zmienna g_CurrentEvent będzie wykorzystana przy odtwarzaniu i będzie przechowywać numer aktualnie odtwarzanego zdarzenia (coś jakby numer klatki w animacji). Zmienną g_PlaybackStopped deklarujemy na wszelki wypadek, by nie przekroczyć zakresu tablicy g_Events. Dalej mamy uchwyty do naszych dwóch haków g_RecordHook i g_PlaybackHook - musimy je pamiętać, by móc usunąć haki, gdy przestaną być potrzebne (jak w poprzednim rozdziale).
 
W zmiennej g_StartTime będziemy przechowywać czas, w którym rozpoczęliśmy nagrywanie / odtwarzanie. Wyjaśnimy później, po co tak robimy. Wreszcie w g_hInst zapamiętamy sobie uchwyt do modułu DLL, który uzyskamy jako parametr z funkcji DllMain, wyglądającej mniej więcej tak:
 
C/C++
BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved )
{
    g_hInst = hModule;
   
    switch( ul_reason_for_call )
    {
    case DLL_PROCESS_ATTACH:
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
   
    return TRUE;
}
 
Jeśli nie wyjaśniliśmy sobie tego w którymś z pozostałych odcinków kursu WinAPI, to zróbmy to teraz: uchwyty typu HMODULE i HINSTANCE są praktycznie tożsame, dlatego możemy bez obaw przypisać jeden do drugiego, tak jak to czynimy powyżej. Uchwyt taki będzie nam potrzebny do założenia haków.
 
Teraz musimy napisać procedury hakowe. Zaczniemy oczywiście od procedury "nagrywającej" - u nas będzie się ona nazywać RecordProc:
 
C/C++
DLL_API LRESULT CALLBACK RecordProc( int code, WPARAM wParam, LPARAM lParam )
{
    if( code < 0 ) return CallNextHookEx( 0, code, wParam, lParam );
   
    if( code == HC_ACTION )
    {
        EVENTMSG * msg =( EVENTMSG * ) lParam;
        msg->time -= g_StartTime;
        g_Events.push_back( * msg );
        return 0;
    }
   
    return CallNextHookEx( 0, code, wParam, lParam );
}
 
Działanie makra DLL_API jest opisane w odcinku o DLL, więc nie musimy tutaj tego tłumaczyć. Zajmijmy się ważniejszymi w tej chwili sprawami. Generalnie procedura dla haka "nagrywającego" jest wołana przede wszystkim wtedy, gdy z kolejki komunikatów zdejmowany jest jakiś komunikat. Wtedy parametr code jest równy HC_ACTION, a lParam zawiera wskaźnik do struktury typu EVENTMSG, która opisuje ten komunikat. Struktura ta zawiera między innymi pole time, które oznacza czas wysłania opisywanego przez strukturę komunikatu. Jest to czas bezwględny, więc nic nam po nim - odtwarzanie może równie dobrze mieć miejsce kilka godzin po nagraniu, albo... kilka godzin wcześniej, bo czas jest tutaj liczony w "tyknięciach" zegara systemowego od momentu uruchomienia systemu, a więc gdybyśmy chcieli odtworzyć zdarzenia zapisane w takiej strukturze po zresetowaniu komputera, to mogłoby się okazać, że zostały one nagrane "w przyszłości". Dlatego lepiej jest liczyć czas względem rozpoczęcia nagrywania i dlatego też odejmujemy wartość zmiennej g_StartTime (w momencie rozpoczęcia nagrywania, a także odtwarzania, musimy tę zmienną ustawić na aktualny czas systemowy). Po tej operacji po prostu dodajemy strukturę wskazywaną przez lParam do wektora i zwracamy dowolną wartość (jest w tym przypadku ignorowana). Poza tymi szczegółami procedura RecordProc działa podobnie jak wszystkie inne procedury hakowe - te ogólne zasady ich bytowania już omówiliśmy wcześniej.
 
Warto wiedzieć, że oprócz HC_ACTION procedura "nagrywająca" może zostać wywołana również z innymi parametrami. Tutaj je ignorujemy dla uproszczenia, jednak jeśli chcemy napisać w 100% poprawną procedurę hakową, powinniśmy je oczywiście uwzględnić - po detale odsyłam do dokumentacji Platform SDK.

Teraz pora na drugą procedurę - "odtwarzającą":
 
C/C++
DLL_API LRESULT CALLBACK PlaybackProc( int code, WPARAM wParam, LPARAM lParam )
{
    if( code < 0 ) return CallNextHookEx( 0, code, wParam, lParam );
   
    if( code == HC_GETNEXT )
    {
       
        EVENTMSG * msg =( EVENTMSG * ) lParam;
        msg->hwnd = g_Events[ g_CurrentEvent ].hwnd;
        msg->message = g_Events[ g_CurrentEvent ].message;
        msg->paramH = g_Events[ g_CurrentEvent ].paramH;
        msg->paramL = g_Events[ g_CurrentEvent ].paramL;
        msg->time = g_StartTime + g_Events[ g_CurrentEvent ].time;
       
        DWORD delta = msg->time - GetTickCount();
       
        if( delta > 0 )
             return delta;
        else
             return 0;
       
    }
    else if( code == HC_SKIP )
    {
        if( !g_PlaybackStopped )
             g_CurrentEvent++;
       
        if( g_CurrentEvent >= g_Events.size() )
        {
            g_CurrentEvent = 0;
            g_StartTime = GetTickCount();
            g_PlaybackStopped = false;
            return 0;
        }
    }
   
    return 0;
}
 
Tutaj mamy do obsłużenia co najmniej dwie sytuacje. Pierwsza zachodzi wtedy, gdy parametr code jest równy HC_SKIP i wtedy musimy, zgodnie z dokumentacją, "przygotować następny komunikat do kopiowania". W naszym przypadku oznacza to po prostu zwiększenie licznika g_CurrentEvent. Dodatkowo sprawdzamy, czy przypadkiem nie skończyły się nam komunikaty, zapamiętane w wektorze g_Events. Jeśli tak, to zerujemy licznik, co oznacza, że odtwarzamy wszystko od początku. W ten sposób odtwarzanie będzie trwało w nieskończoność - a przynajmniej do momentu, gdy użytkownik się zniecierpliwi i coś naciśnie :-).
 
Nie jest jednak obojętne, co takiego naciśnie. Klawiatura i mysz są bowiem "wyłączone" w trakcie odtwarzania. Aktywne są jedynie dwie dobrze znane użytkownikom Windows kombinacje specjalne: Ctrl+Esc i Ctrl+Alt+Del. Obie powodują natychmiastowe przerwanie odtwarzania (a także nagrywania) poprzez usunięcie odpowiedniego hooka. Tak więc żadnego dodatkowego kodu przerywającego nie musimy pisać.
 
Drugą akcją, którą musimy wykonać, jest skopiowanie następnej "klatki animacji" (komunikatu do odtworzenia) z tablicy do struktury wskazywanej przez lParam (code == HC_GETNEXT). Znów musimy "poprawić" pole z czasem. Jak pamiętamy, jest on względny, a system operuje w tym przypadku na czasach bezwględnych. Tak więc aby system się nie pogubił, dodajemy mu czas rozpoczęcia odtwarzania (tak - odtwarzania, a nie nagrywania!) do czasu względnego z tablicy.
 
Kolejnym krokiem jest zwrócenie odpowiedniej wartości. Jeśli będzie ona większa od 0, system zostanie "uśpiony" na tę właśnie ilość czasu. Dzięki temu działania będą odtworzone w dokładnie takim tempie, w jakim zostały nagrane. Żeby obliczyć właściwy interwał czasowy, odejmujemy aktualny czas (pobrany za pomocą funkcji GetTickCount()) od czasu jaki właśnie zapisaliśmy w strukturze.
W systemie Vista występują problemy z odczekiwaniem żądanego czasu. Prawdopodobnie można to obejść, wołając Sleep(), a z procedury "odtwarzającej" zwracając zawsze 0. Nie sprawdziłem tego jednak, więc nie podpisuję się pod tym rozwiązaniem :-).
 Aby nasze procedury (umieszczone w DLL) były dostępne dla zewnętrznej aplikacji (którą też zaraz sobie napiszemy), musimy je wyeksportować. Albo nasze dwa haki założymy bezpośrednio z DLL, co będzie dużo wygodniejsze. W tym celu stworzymy sobie interfejs w postaci takich oto dwóch funkcji:

C/C++
DLL_API void StartRecording( void )
{
    g_StartTime = GetTickCount();
    g_RecordHook = SetWindowsHookEx( WH_JOURNALRECORD, RecordProc, g_hInst, 0 );
}

DLL_API void StartPlayback( void )
{
    g_CurrentEvent = 0;
    g_StartTime = GetTickCount();
    g_PlaybackStopped = false;
    UnhookWindowsHookEx( g_RecordHook );
    g_PlaybackHook = SetWindowsHookEx( WH_JOURNALPLAYBACK, PlaybackProc, g_hInst, 0 );
}
 
Zakładanie haków w DLL różni się od zakładania ich w "zwykłej" aplikacji praktycznie tylko tym, że musimy pamiętać, by przekazać uchwyt do modułu DLL. Już go sobie zapamiętaliśmy w zmiennej g_hInst, więc nie mamy z tym problemu. Obie funkcje ustawiają zmienną g_StartTime na aktualny czas systemowy - zgodnie z tym, co już wcześniej powiedzieliśmy. Dodatkowo StartPlayback zeruje licznik g_CurrentEvent oraz usuwa hak "nagrywający".
 
Jedyne, co musimy teraz wyeksportować z DLL, to nasz interfejs - funkcje StartRecording i StartPlayback:
 
C/C++
extern "C"
{
    DLL_API void StartPlayback( void );
    DLL_API void StartRecording( void );
}
 
Teraz przechodzimy do napisania aplikacji, która skorzysta z tego DLL. Najpierw kilka globalnych zmiennych:
 
C/C++
typedef void( * VOIDFN )( void );

VOIDFN g_Play = NULL;
VOIDFN g_Record = NULL;
HINSTANCE g_hInstDLL = NULL;
 
Wskaźniki g_Play i g_Record będą nam potrzebne, aby zapamiętać w nich adresy dwóch funkcji z DLL, które przed chwilą stworzyliśmy. Ładujemy więc tego DLL-a (załóżmy, że nazwaliśmy go journal.dll i wrzuciliśmy go do katalogu, gdzie jest projekt naszej aplikacji) i te funkcje:
 
C/C++
HINSTANCE hInstDLL = LoadLibrary( "journal.dll" );
g_Play =( VOIDFN ) GetProcAddress( hInstDLL, "StartPlayback" );
g_Record =( VOIDFN ) GetProcAddress( hInstDLL, "StartRecording" );
 
Pozostaje już tylko wywołać obydwie funkcje w odpowiednich momentach. Załóżmy, że nagrywanie chcemy rozpocząć klawiszem F2, a odtwarzanie - F3:

C/C++
ase WM_KEYDOWN:
{
    if( wParam == VK_F2 )
    {
        g_Record();
    }
    else if( wParam == VK_F3 )
    {
        g_Play();
    }
}
break;
 
Po wciśnięciu F2 możemy sobie teraz wykonać parę ruchów myszą albo powciskać jakieś klawisze (wszystko jedno w jakim programie - hak jest przecież globalny), po czym naciskając F3 sprawimy, że to wszystko zostanie powtórzone. Oczywiście nie zawsze dokładnie - jeśli zamkniemy okno myszą podczas nagrywania, to podczas odtwarzania mysz kliknie już na czymś innym :-).
 
Przydałaby nam się jeszcze jakaś reakcja na zakończenie odtwarzania. Wprawdzie haki - jak już wspomnieliśmy - usuną się automatycznie, ale w pewnych sytuacjach chcielibyśmy wiedzieć, że użytkownik przerwał odtwarzanie, np. jeśli musimy schować jakiś element GUI, na którym wyświetlaliśmy czas odtwarzania.
 
Otóż jeśli odtwarzanie (lub nagrywanie) zostanie przerwane, np. poprzez wciśnięcie Ctrl+Esc, to dostaniemy komunikat WM_CANCELJOURNAL. Jest z nim jednak pewien problem - nie jest skierowany do konkretnej procedury okna, jego parametr hwnd wynosi NULL. Tak więc jeśli umieścimy jego obsługę w procedurze okna, tak jak jesteśmy przyzwyczajeni, to kod ten po prostu się nie wywoła. Są dwa sposoby, aby temu zaradzić. Możemy albo przechwycić ten jeden komunikat "na dziko", czyli bezpośrednio w pętli komunikatów, albo... założyć następny hak, który to uczyni (WH_GETMESSAGE). To pierwsze rozwiązanie jest oczywiście prostsze, ale nie można z niego skorzystać, jeśli nie mamy w naszym programie własnej głównej pętli. Załóżmy, że nasz programik przykładowy taką posiada i możemy napisać tak:
 
C/C++
while( GetMessage( & msg, NULL, 0, 0 ) )
{
    if( msg.message == WM_CANCELJOURNAL )
    {
        MessageBox( hwnd, "Koniec zabawy!", "Info", MB_ICONINFORMATION );
    }
   
   
    TranslateMessage( & msg );
    DispatchMessage( & msg );
}
 
Istotne jest, że "dobraliśmy się" do komunikatu, zanim trafił on do DispatchMessage, skąd by już nie wyszedł ;-). Teraz gdy wciśniemy Ctrl+Esc albo Ctrl+Alt+Del podczas działania jednego z naszych dwóch haków, uraczeni zostaniemy miłym komunikatem. 
 
Zauważmy, że nie ma możliwości zwolnienia wykorzystanej przez nas biblioteki DLL. Jej licznik referencji jest bowiem zwiększany dla każdej aplikacji działającej w systemie! Dzieje się tak dlatego, że użyliśmy haka globalnego. Zawołanie FreeLibrary() nic nie da, bo możemy z tej funkcji skorzystać tylko dla naszego własnego procesu. Biblioteka zostanie zwolniona dopiero po zamknięciu wszystkich niejawnie z nią połączonych procesów albo... wtedy, gdy jakimś cudem wszystkie te procesy zwolnią ją przez FreeLibrary(). Oczywiście cudów nie ma, więc wszystkie te aplikacje muszą wiedzieć o istnieniu naszego DLL, co w praktyce oznacza, że musieliśmy je sami napisać ;-).
 
Programik, który sobie napisaliśmy, może służyć nie tylko do głupich żartów. Na pewno znajdziesz niejedno nużące zadanie, podczas którego trzeba wiele razy powtarzać te same czynności. Po paru drobnych ulepszeniach nasz program wykona je za nas :-).
 
Omówiliśmy dokładniej tylko dwa rodzaje haków, ale to powinno w zupełności wystarczyć. Wszystkie inne rodzaje obsłguje się w bardzo podobny sposób. Każdy ma swoje specyficzne zastosowanie, które zapewne znajdziesz, gdy będziesz go potrzebować :-). Oczywiście, każdy niesie za sobą również mnóstwo potencjalnych (i także specyficznych) problemów - pamiętajmy, że używając haków grzebiemy dość głęboko w systemie, a więc musimy najpierw dokładnie wiedzieć, co robimy - ale to już wszystko jest materiałem na podręcznik, a nie króki kurs ;-).
Poprzedni dokument Następny dokument
DLL MDI