Rozdział 29.
Praca z bibliotekami DLL
W tym rozdziale:
Jawne i niejawne łączenie bibliotek
Standardowe biblioteki DLL w języku C++
Implementowanie własnej funkcji DLLMain ()
Użycie makra AFX_MANAGE_STATE
Przeglądanie zasobów binarnych
Tworzenie systemowego haka klawiatury
Odwoływanie się do globalnych obiektów C++ z różnych egzemplarzy biblioteki DLL
DLL-e rozszerzeń MFC
Eksportowanie całych klas lub części klas z bibliotek DLL
Zagnieżdżone DLL-e rozszerzeń MFC, reprezentujące dokumenty i widoki
Już od najwcześniejszych początków Windows, we wszystkich wersjach i odmianach tego systemu, intensywnie korzystano z funkcji i danych przechowywanych w dynamicznie łączonych bibliotekach (DLL, Dynamie Link Library). Gdy zajrzysz do systemu Windows, praktycznie wszystko jest jakąś formą biblioteki DLL. Czcionki i ikony wyświetlane na ekranie są przechowywane w DLL-ach. Kod potrzebny do wyświetlenia pulpitu Windows i przetwarzania poleceń jest zawarty w DLL-u. Nawet Windows API jest zawarte w bibliotece DLL. W związku z tym w tym rozdziale postaramy się wyjaśnić, czym są dynamicznie łączone biblioteki oraz jak można je wykorzystać w aplikacjach Visual C++.
Ponieważ Visual C++ definiuje dwa typy bibliotek DLL ("zwykłe" i "rozszerzeń"), rozdział jest podzielony na dwie części. W pierwszej z nich poznasz zalety korzystania w Visual C++ ze zwykłych bibliotek DLL oraz sposoby tworzenia ich za pomocą AppWizarda. Dowiesz się także jak zarządzać aktualnym "stanem modułu", dzięki któremu DLL może wywoływać funkcje MFC zawarte we wspólnych bibliotekach DLL MFC. Nauczysz się również jak dynamicznie ładować DLL-e i wywoływać eksportowane w nich funkcje. Pierwszy program demonstracyjny pokazuje zastosowanie haków Windows, na przykładzie procedury haka klawiatury. Drugi program demonstracyjny pokazuje, w jaki sposób można korzystać ze wspólnych obiektów C++ w kilku różnych aplikacjach korzystających z tej samej biblioteki DLL.
W drugiej części rozdziału poznasz biblioteki DLL rozszerzeń MFC oraz sposób ich konstruowania. W ten sposób dowiesz się jak eksportować z biblioteki całe klasy lub tylko specyficzne funkcje składowe danej klasy. Gdy już poznasz podstawy bibliotek rozszerzeń MFC, przejdziemy do dwóch programów demonstracyjnych. Pierwszy z nich pokaże jak w aplikacji MFC można używać zagnieżdżonych bibliotek rozszerzeń MFC. Drugim przykładem będzie biblioteka DLL reprezentująca wzorzec dokumentu, dokument oraz widok potrzebne do stworzenia aplikacji, do przeglądania obrazków, w której cała praca związana z wyświetleniem obrazka sprowadza się do pojedynczego wywołania funkcji z biblioteki DLL.
Podstawy bibliotek DLL
Biblioteki DLL opierają się na koncepcji komunikacji typu klient-serwer. W bibliotece DLL (serwerze) zawarte są funkcje i dane, udostępnione do wykorzystania przez jeden lub więcej klientów. Te klienty to aplikacje lub inne DLL-e. Tak więc w tym rozdziale terminem klient będziemy określać dowolnego klienta biblioteki DLL, zarówno aplikację jak i inną bibliotekę DLL.
Biblioteki dynamiczne i statyczne
Biblioteki DLL są podobne do normalnych, statycznych bibliotek. W przypadku biblioteki statycznej, funkcje i dane są kompilowane do pliku binarnego (zwykle z rozszerzeniem .LIB}. Program łączący (linker) następnie kopiuje te funkcje i dane z biblioteki i łączy je z innymi modułami aplikacji, tworząc ostateczny plik wykonywalny (plik .EXE). Program łączący jest odpowiedzialny także za odwzorowanie funkcji, czyli za to, jak wywołania z innych modułów pliku wykonywalnego są odwzorowywane do wywołań funkcji z biblioteki. Gdy aplikacja łączy się ze statyczną biblioteką, cały ten proces jest nazywany łączeniem statycznym. Ponieważ elementy biblioteki wykorzystywane przez program są kopiowane do pliku wykonywalnego, wraz z tym plikiem nie musi być dostarczana również biblioteka.
W przypadku łączenia dynamicznego, funkcje i dane z biblioteki nie są kopiowane do pliku wykonywalnego. Zamiast tego są tworzone dwa pliki: biblioteka importowa oraz biblioteka DLL. Biblioteka importowa zawiera nazwy i lokalizacje funkcji eksportowanych przez bibliotekę DLL, zaś biblioteka DLL zawiera właściwe funkcje i dane. Aplikacja chcąca wykorzystać eksportowaną funkcję danego DLL-a, łączy się z biblioteką importową. Jednak dzieje się tak w przypadku, gdy aplikacja niejawnie łączy się z biblioteką DLL. Zagadnienia jawnego i niejawnego łączenia z DLL-ami zostaną omówione w następnej sekcji. Ponieważ biblioteka DLL zawiera funkcje i dane potrzebne klientom tej biblioteki, wraz z programem wykonywalnym trzeba rozprowadzać również bibliotekę.
Proces tworzenia DLL-a jest bardzo podobny do procesu tworzenia pliku wykonywalnego. Funkcje biblioteczne są kompilowane do pliku . OBJ (modułu), po czym program łączący tworzy ostateczny plik biblioteki DLL, łącząc razem kod z różnych modułów. Jednak w przypadku bibliotek DLL specjalna opcja linkera (/DLL) informuje program łączący, zamiast pliku EXE ma stworzyć bibliotekę DLL. Program ładujący Windows potrafi rozróżnić plik biblioteki DLL od pliku wykonywalnego aplikacji.
Ładowanie bibliotek DLL
Istnieją dwa sposoby załadowania biblioteki DLL: niejawny i jawny. Klienty ładują bibliotekę niejawnie albo poprzez połączenie się z biblioteką importową DLL-a albo poprzez listę funkcji eksportowanych z DLL-a (zawartą w sekcji IMPORTS pliku .DEF klienta). Lądowanie niejawne jest powszechnym sposobem ładowania bibliotek DLL, a to z tego powodu, że przy takim ładowaniu klient nie musi robić już nic więcej. Programista aplikacji po prostu dołącza potrzebne pliki nagłówkowe, wywołuje żądane funkcje i łączy program z biblioteką DLL. Jednak niejawne łączenie się z DLL-em ma także swoje wady:
Biblioteka DLL musi mieć rozszerzenie pliku .DLL.
Nawet jeśli nie są wywoływane żadne funkcje z DLL-a, aplikacja i tak zawiera pewien narzut związany z ładowaniem biblioteki.
Gdy klient niejawnie ładuje jedną lub kilka bibliotek DLL, są one ładowane do pamięci w momencie ładowania klienta. Aby Windows mógł załadować DLL-a, musi zlokalizować fizyczny plik na dysku. Szukając pliku biblioteki DLL, Windows przeszukuje kartoteki w następującej kolejności:
Bieżącą kartotekę
Kartotekę Windows
Kartotekę systemową Windows
Kartotekę, w której znajduje się klient
Kartoteki wymienione w zmiennej środowiskowej PATH (zgodnie z ich kolejnością)
Mapowane kartoteki sieciowe
Choć łączenie niejawne jest najprostszym sposobem ładowania biblioteki DLL przez klienta, zdarzają się przypadki, gdy bardziej odpowiednie jest łączenie jawne. Łączenie jawne wymaga zażądania w konkretnym momencie załadowania konkretnego pliku przez system Windows. Choć ładowanie jawne wiąże się z koniecznością wykonania nieco większej ilości pracy, jednak posiada kilka zalet w stosunku do ładowania niejawnego:
Plik biblioteki może posiadać dowolne rozszerzenie
Biblioteka DLL jest ładowana tylko, wtedy gdy zażąda tego klient
Nawet w czasie działania programu klient może dokonywać wyboru pomiędzy różnymi wersjami biblioteki.
Jawne ładowanie biblioteki przebiega następująco:
Klient wywołuje funkcję Win32 LoadLibrary (). Ta funkcja zwraca uchwyt biblioteki. Jeśli załadowanie biblioteki się nie powiedzie, funkcja zwraca wartość NULL, zaś w celu poznania przyczyny niepowodzenia należy wywołać funkcję Win32 GetLastError () .
Używając uchwytu zwróconego przez funkcję LoadLibrary () klient wywołuje funkcję Win32 GetProcAddress (), przekazując jej nazwę żądanej funkcji. Funkcja GetProcAddress () zwraca adres funkcji, który może zostać wykorzystany do jej wywołania.
Gdy klient skończy korzystać z DLL-a, powinien wywołać funkcję Win32 Free-LibraryO w celu zwolnienia biblioteki z pamięci oraz związania wszystkich powiązanych z nią zasobów.
Zwykłe biblioteki DLL w Visual C++
Visual C++ definiuje dwa typy bibliotek DLL: biblioteki zwykłe oraz biblioteki rozszerzeń. Zwykłe biblioteki są używane wtedy, gdy eksportowanymi funkcjami są funkcje języka C, klasy C++ lub funkcje składowe klas C++. Nie należy przy tym mylić klas języka C++ z klasami C++ biblioteki MFC. Jeśli biblioteka ma eksportować klasę MFC, powinieneś stworzyć bibliotekę rozszerzeń MFC, jaką omówimy w drugiej części rozdziału. Zalety korzystania z ze zwykłych bibliotek w stosunku do bibliotek rozszerzeń MFC są następujące:
Klient nie musi być aplikacją MFC; musi jedynie być w stanie wywoływać funkcje języka C. Tak więc klientem może być dowolny program, od aplikacji MFC po aplikacje Delphi czy Visual Basica.
Zwykły DLL może używać klas C++ wewnętrznie, a następnie eksponować tylko funkcje pośrednie napisane w C. W ten sposób wszelkie zmiany w tych klasach C++ nie wpłyną na aplikacje korzystające z tej biblioteki DLL.
W Visual C++ zwykłe biblioteki DLL można tworzyć z pomocą AppWizarda. Po wywołaniu AppWizarda wybierz po prostu projekt typu MFC AppWizard (DLL). Pierwsze okno dialogowe kreatora pozwala na określenie, czy chcesz użyć biblioteki MFC oraz w jaki sposób biblioteki MFC mająbyć łączone z twóją biblioteką DLL: statycznie czy dynamicznie.
Podobnie jak w przypadku zalet z korzystania ze zwykłych DLL-i w stosunku do korzystania z DLL-i rozszerzeń MFC, istnieją wady i zalety związane z wyborem sposobu połączenia MFC z tworzoną biblioteką DLL:
Jeśli zwykła biblioteka DLL dynamicznie łączy się z bibliotekami MFC, rozmiar jej pliku będzie dużo mniejszy niż w przypadku zwykłego DLL korzystającego ze statycznego łączenia. Jednak wadą tego rozwiązania jest konieczność dostarczania wraz z całym programem również pliku biblioteki DLL MFC. Oczywiście, ten problem pojawia się tylko, gdy musisz dystrybuować swoją aplikację. Jeśli tworzysz ją dla pojedynczego komputera lub dla środowiska, nad którym masz kontrolę, przestaje to mieć znaczenie.
Jeśli dystrybuujesz swoją bibliotekę DLL w celu użycia przez inną aplikację MFC, zaś ta aplikacja dynamicznie łączy się z bibliotekami MFC, nigdy nie będziesz miał pewności co do wersji MFC, jaką DLL będzie miał do dyspozycji. Jeśli jednak twój DLL będzie statycznie połączony z bibliotekami MFC, nie będziesz musiał się martwić o wersję MFC używaną przez swoją bibliotekę.
Jak widać, zanim zaczniesz pracę z bibliotekami DLL, musisz podjąć kilka decyzji. Zależą one od czynników: czy będziesz miał kontrolę nad środowiskiem, w którym DLL będzie używany, w jakim języku będzie napisana aplikacja wywołująca oraz to, czy wraz ze swoim kodem będziesz dystrybuował biblioteki DLL MFC. Prawdopodobnie będziesz musiał podejmować te decyzje dla każdej tworzonej przez siebie aplikacji i biblioteki DLL.
Wewnętrzne działanie zwykłych bibliotek DLL
Jeśli do stworzenia szkieletu zwykłej biblioteki DLL użyjesz AppWizarda i spojrzysz na wygenerowany kod źródłowy, znajdziesz w nim znajomą klasę: CWinApp.Prawdopodobnie przywykłeś już do tworzenia aplikacji MFC, w których sama aplikacja jest reprezentowana przez obiekt CWinApp. Jednak w przypadku bibliotek DLL utworzonych przez AppWizarda, obiekt cwinApp reprezentuje bibliotekę DLL. Oprócz tego, jeśli wcześniej zajmowałeś się tworzeniem bibliotek DLL, możesz zastanawiać się, gdzie podziała się funkcja DiiMain (}. Przecież w końcu każdy DLL Win32 musi zawierać punkt wejścia DiiMain {). Cóż, AppWizard automatycznie tworzy funkcję DiiMain (), podobnie jak winMainO dla aplikacji Win32. Zaletą posiadania klasy cwinApp jest to, że możesz programować swoją bibliotekę DLL tak jak każdą inną klasę wyprowadzoną z cwinApp. Na przykład, inicjalizacja klasy cwinApp zwykle odbywa się w funkcji initinstance (), możesz przesłonić tę funkcję także w przypadku DLL-a i zawrzeć w niej globalną inicjaliza-cję. Oprócz tego deinicjalizacja biblioteki powinna odbywać się w funkcji Exitinstance () klasy wyprowadzonej z klasy cwinApp DLL-a.
Implementacja własnej funkcji DIIMain()
Choć MFC dostarcza domyślnej implementacji funkcji DllMainO, zaś dokumentacja MFC zaleca przeprowadzanie inicjalizacji w funkcji initinstance () w klasie wyprowadzonej z cwinApp biblioteki DLL, zdarzają się sytuacje, w których konieczne jest zaimplementowanie własnej funkcji DllMainO. Jednym z potencjalnych problemów wynikających z korzystania z dostarczonej przez MFC funkcji DiiMain () w połączeniu z funkcją initinstance o jest to, że ta funkcja initinstance () jest wywoływana tylko, wtedy gdy proces łączy się z biblioteką DLL lub odłącza się od biblioteki DLL. W większości przypadków działa to zupełnie poprawnie, jednak czasem zdarza się, że inicjalizacja jest konieczna także podczas przyłączania lub odłączania wątku. Aby móc obsłużyć taki scenariusz, konieczne może być zaimplementowanie własnej funkcji DiiMain ().
Powodem, dla którego funkcja DllMainO jest automatycznie kompilowana do biblioteki DLL jest to, że kod źródłowy MFC pobiera kod dla biblioteki DLL z pliku Dllmodul.cpp. W rzeczywistości, gdy przejrzysz folder zawierający pliki źródłowe MFC, znajdziesz nie tylko ten plik, ale także zawartą w nim definicję funkcji DllMainO. Oprócz tej funkcji, plik Dllmodul.cpp zawiera także większość kodu używanego do obsługi zwykłych DLL-i. Zalecanym sposobem przesłonięcia lub zmiany tej funkcji jest skopiowanie tego pliku do własnego foldera plików źródłowych projektu biblioteki DLL i dołączenie go do projektu. W tym momencie będziesz mógł dokonywać dowolnych zmian w tym pliku, który zostanie lokalnie skompilowany i połączony z biblioteką DLL.
Kolejne ważne zagadnienie dotyczące zwykłych DLL-i pojawia się wtedy, gdy zwykły DLL musi odwołać się do funkcji MFC. MFC przechowuje pewne wewnętrzne globalne informacje o stanie, odnoszące się do aplikacji lub biblioteki DLL. W związku z tym, jeśli funkcja wyeksportowana w DLL-u spróbuje wywołać funkcję zależną od tych informacji, zostaną użyte niewłaściwe dane. Oto prosty przykład ilustrujący to zagadnienie.
Używając AppWizarda stwórz nowy projekt DLL-a MFC o nazwie DisplayAppName. Nie zapomnij o zdefiniowaniu biblioteki DLL jako zwykłego DLL (jest to domyślne ustawienie AppWizarda dla tworzonych DLL-i). Gdy kreator stworzy pliki źródłowe, w pliku DisplayAppName.h zadeklaruj poniższą funkcję:
void DisplayAppName(); W pliku DisplayAppName. cpp zdefiniują tę funkcję następująco:
void DisplayAppName()
{
CString str = AfxGetAppName(); AfxMessageBox(str);
}
Następnie dopisz tę funkcję do sekcji EKPORTS w pliku DisplayAppName.def. Sekcja EKPORTS powinna wyglądać następująco:
EXPORTS
DisplayAppName
Zbuduj DLL-a i skopiuj go do foldera, w którym Windows będzie mogło go znaleźć. Jeśli nie jesteś pewien, gdzie umieścić plik DLL-a, wróć do sekcji "Lądowanie bibliotek DLL" we wcześniejszym rozdziale. Następnie stwórz projekt aplikacji MFC i nazwij go RegDHTest. Po dołączeniu do któregoś z plików źródłowych pliku nagłówkowego DisplayAppName. h oraz po dołączeniu biblioteki DisplayAppName.lib do projektu, umieść w kodzie wywołanie funkcji DisplayAppName ().
Następnie zbuduj i uruchom aplikację. Zobaczysz, że wyświetloną nazwą jest nazwa pliku .EXE, a nie pliku .DLL. Dzieje się tak ponieważ MFC umieścił dane globalne na stosie reprezentującym bieżący moduł - w tym przypadku aplikację. Aby określić, że chcesz użyć globalnych danych MFC związanych z DLL-em, w swoim kodzie musisz użyć makra AFX_MANAGE_STATE. To makro, jest używane do odkładania lub zdejmowania globalnych danych powiązanych z modułem. Tak więc, na początku każdej eksportowanej funkcji musisz ustawić bieżący stan modułu na stan modułu DLL-a. Nie martw się o przywrócenie właściwego stanu na końcu funkcji, gdyż makro AFX_MANAGE_STATE jest w rzeczywistości rozwijane do obiektu klasy, której destruktor przywraca poprzedni stan w momencie, gdy obiekt znajdzie się poza zakresem widoczności. Oto jak powinieneś zmodyfikować przykładowy kod, aby okno komunikatu wyświetliło nazwę DLL-a, a nie nazwę aplikacji:
void DisplayAppName()
{
AFX_MANAGE_STATE(AfxGetStaticModuleState() ) ; CString str = AfxGetAppName(); AfxMessageBox(str);
}
Dynamiczne ładowanie DLL-i
Jak już wspominaliśmy, gdy następuje ładowanie aplikacji, Windows automatycznie ładuje DLL-e, z którymi aplikacja jest połączona niejawnie. Jednak często zdarza się, że chcesz dynamicznie załadować bibliotekę DLL. Aby załadować bibliotekę w czasie działania programu, wywołaj po prostu funkcję LoadLibrary (). Jak widać z poniższego prototypu, wymaga ona podania tylko jednego argumentu - nazwy pliku:
HINSTANCE LoadLibrary(LPCTSTR IpLibFileName);
Zwracana wartość HINSTANCE jest globalnym uchwytem reprezentującym załadowaną bibliotekę. Ten uchwyt jest używany w procedurze GetProcAddress () zwracającej adres wskazanej funkcji. Zwracany adres jest przechowywany we wskaźniku do funkcji, który może być wywoływany tak samo jak każda inna funkcja. Gdy aplikacja nie potrzebuje już biblioteki, wywołuje funkcję FreeLibrary () zwalniającą bibliotekę z pamięci.
Przykłady sytuacji wymagających dynamicznego ładowania bibliotek DLL
Gdy znasz już podstawy dynamicznego ładowania bibliotek DLL, podamy kilka przykładów sytuacji, w których takie ładowanie może okazać się przydatne.
Programowanie interfejsów funkcji
Interfejs funkcji to abstrakcyjna warstwa pomiędzy funkcją a wywołującym ją procesem. Ma ona na celu odizolowanie procesu wywołującego od wewnętrznych mechanizmów działania wywoływanej funkcji. Załóżmy, że masz aplikację obsługującą kilka protokołów komunikacyjnych. Aplikacja może obsługiwać APPC, NetBIOS oraz nazwane kanały. W jaki sposób aplikacja może obsługiwać wszystkie, zupełnie odmienne komunikacyjne interfejsy API bez konieczności dokonywania specjalnych zmian w głównym module kodu źródłowego?
Jednym ze sposobów jest zaimplementowanie interfejsu funkcji, w którym każdy z komunikacyjnych interfejsów API zostałby umieszczony w osobnej bibliotece DLL. Każda z bibliotek eksportowałaby dokładnie taki sam zestaw funkcji. Na przykład, każda biblioteka mogłaby eksportować ogólną funkcję Read () o jednoznacznie określonej definicji. Dzięki temu funkcja wywołująca mogłaby ładować odpowiednią bibliotekę DLL, a następnie wywoływać jej funkcję Read (), nie zastanawiając się w jaki sposób biblioteka wykonuje swoją pracę wewnętrznie.
Przykładem użycia interfejsu funkcji może być ODBC (omawiane w rozdziale 26). Gdy aplikacja zażąda usługi od Menedżera sterowników ODBC, to żądanie musi zostać przekazane do wskazanego sterownika ODBC. Ponieważ Menedżer sterowników ODBC nie może być przepisywany dla każdego nowego sterownika ODBC, Microsoft osiągnął cel poprzez wymaganie, by każdy sterownik ODBC eksportował określony zestaw funkcji zgodny z opublikowaną specyfikacją. W ten sposób Menedżer sterowników ODBC nie musi interesować się sposobem obsługi żądania przez sterownik oraz nie wymaga specjalnych wersji kodu dla różnych sterowników.
Pisanie aplikacji wielojęzycznych
Kolejnym, częstym przykładem konieczności użycia dynamicznie ładowanych bibliotek DLL jest programowanie wielojęzycznych aplikacji. Istnieje kilka różnych sposobów obsługi więcej niż jednego języka, jednak najpopularniejszym z nich jest użycie bibliotek DLL zawierających jedynie zasoby. Aby obsłużyć kilka języków, wystarczy stworzyć dla każdego z nich osobną bibliotekę DLL. Po stworzeniu biblioteki, są do niej dodawane odpowiednie zasoby (tablice łańcuchów, menu itd.). Ważne jest jednak, aby pamiętać, że w każdej z bibliotek należy zastosować te same identyfikatory zasobów. Na przykład, jeśli identyfikatorem zasobu menu w bibliotece DLL dla wersji angielskiej jest 100, to w wersji hiszpańskiej identyfikatorem tego menu powinno być również 100.
Gdy już to zrobisz, program wywołujący musi zrobić jedynie dwie rzeczy w celu obsługi kilku języków. Musi po prostu załadować odpowiednią bibliotekę i dostarczyć otrzymany uchwyt HINSTANCE funkcji AfxSetResourceHandle (). Od tego momentu, gdy kod aplikacji odwoła się do danego identyfikatora zasobu, zostanie użyty zasób ze wskazanej biblioteki DLL.
Gdy plik nagłówkowy lub biblioteka importowa nie są dostępne
Czasem zdarza się, że aplikacja musi wywołać funkcję zawartą w bibliotece DLL, lecz jej plik nagłówkowy lub biblioteka importowa nie są dostępne. W swojej aplikacji możesz umieścić wywołania eksportowanych funkcji, jednak bez pliku nagłówkowego nie będziesz mógł jej skompilować, zaś bez biblioteki importowej nie będziesz mógł połączyć modułów w plik wykonywalny. Jeśli jednak możesz wygenerować listę funkcji eksportowanych przez bibliotekę DLL (na przykład programem DUMPBIN.EXE) oraz znasz prototyp funkcji, możesz dynamicznie załadować bibliotekę i otrzymać adres funkcji, którą chcesz wywołać. W tym momencie będziesz mógł wywołać żądane funkcje.
Pobieranie zasobów z pliku binarnego
Kolejnym popularnym powodem dynamicznego łączenia bibliotek jest odczytywanie zawartych w nich zasobów. Ponieważ kompilator zasobów kopiuje zasoby do pliku tworzonej aplikacji lub biblioteki DLL, zasoby są zawarte w samym pliku binarnym. Przykładem mogą być różne wskaźniki myszy standardowe dla Windows. Te kursory można odczytać z pliku user32.dll dynamicznie, ładując go w aplikacji, wywołując funkcję LoadCursor () i przekazując identyfikator zasobu dla żądanego kursora.
Przeglądanie zasobów binarnych
Zasoby w plikach binarnych można przeglądać także za pomocą Visual Studia. Po prostu otwórz okno dialogowe File Open i wskaż plik binarny, który zawiera zasoby, jakie chcesz przejrzeć. Gdy wskażesz żądany plik binarny, na rozwijanej liście Open As wybierz pozycję Resources (zasoby). Po otwarciu pliku ujrzysz wszystkie jego zasoby, tak jakbyś otwierał zwykły plik zasobów.
Haki Windows
W tej sekcji dowiesz się czym są haki w Windows oraz jak pisać procedury haków. Haki (ang. hook) są punktami w ścieżce przekazywania komunikatów Windows, w których są wstawiane podprogramy służące do filtrowania określonych typów komunikatów przed przekazaniem ich do celu. Po przechwyceniu komunikatu, można go zmodyfikować, zarejestrować w dzienniku lub po prostu odrzucić. Procedury haków są nazywane/?//ram/. Filtry można podzielić ze względu na rodzaje filtrowanych zdarzeń. Na przykład, hak, jaki stworzymy w tym rozdziale jest nazywany hakiem klawiatury. Gdy do haka jest dołączana funkcja filtra, nazywa się to ustawianiem haka. Jak zapewne się domyślasz, zdarzają się sytuacje, w których jest ustawiany więcej niż jeden hak tego samego typu. Aby poradzić sobie z takimi sytuacjami, Windows rejestruje łańcuch funkcji filtrów. Bardzo ważną właściwością tego łańcucha jest to, że ostatnia dodana funkcja filtra staje się pierwszą funkcją w łańcuchu, w związku z tym jako pierwsza może filtrować otrzymane komunikaty. Taka funkcja może wykonać swoją pracę i przekazać komunikat następnej funkcji w łańcuchu, lecz może także po prostu go odrzucić. Listę funkcji haków obsługiwanych w systemie Windows zawiera tabela 29. l.
Funkcja SetWindowsHookEx()
Aby ustawić hak, konieczne jest wywołanie funkcji SetwindowsHookEx (). Jej prototyp jest następujący:
HHOOK SetWindowsHookEx(int idHook, HOOKPROC Ipfn, HINSTANCE hMod,DWORD dwThreadId)
Pierwszy argument, idHook, reprezentuje stałą, określającą rodzaj ustawianego haka. W systemie Windows można ustawić około 15 różnych typów haków. Wartością użytą w naszym programie demonstracyjnym jest WH_KEYBOARD, wskazująca, że będzie ustawiany hak klawiatury.
Drugi argument, ipfn, jest po prostu wskaźnikiem do funkcji typu HOOKPROC. W przypadku haka klawiatury, funkcja wskazywana w wywołaniu funkcji SetwindowsHookEx () musi mieć poniższy prototyp (samą funkcję omówimy bardziej szczegółowo):
LRESULT CALLBACKKeyboardProc(int code, WPARAM, LPARAM);
Trzeci argument, hMod, jest uchwytem HINSTANCE biblioteki DLL zawierającej funkcję filtra.
Czwarty i ostatni argument funkcji setwindowsHookEx () umożliwia aplikacji wskazanie wątku, dla którego jest ustawiany hak. Na przykład, aplikacja może ustawić hak dla pojedynczego wątku lub dla wszystkich aktywnych wątków. W tym drugim przypadku jako argument dwThreadld należy przekazać wartość NULL.
Funkcja setwindowsHookEx () zwraca wartość typu HHOOK (uchwyt haka). Jeśli nie powiodło się ustawienie haka, funkcja zwraca wartość NULL, zaś przyczynę błędu można poznać wywołując funkcję GetLastError ().
Tabela 29.1. Funkcje haków w Windows
Funkcja
Opis
WH_CALLWNDPROC
Monitoruje wszystkie komunikaty Windows przed przesianiem ich do docelowej procedury okna (patrz WH_CALLWNDPROCRET).
WH_CALLWNDPROCRET
Monitoruje komunikaty już po przetworzeniu ich przez docelową procedurę okna.
WH_CBT
Monitoruje komunikaty używane przez komputerowe aplikacje treningowe.
WH_DEBUG
Pomaga w debuggowaniu innych funkcji haków.
WH_GETMESSAGE
Monitoruje komunikaty Windows przesyłane do kolejki komunikatów.
WH_JOURNALPLAYBACKPrzesyła komunikaty zarejestrowane uprzednio przez procedurę
haka WM_JOURNALRECORD.
WH_JOURNALRECORD
Rejestruje komunikaty wejściowe przesyłane do systemowej kolejki komunikatów.
WH_KEYBOARDMonitoruje aktywność klawiatury
.
WH_KEYBOARD_LL
Monitoruje niskopoziomowe zdarzenia wejściowe klawiatury (tylko w Windows NT).
WH_MOUSE
Monitoruje komunikaty myszy.
WH_MOUSE_LL
Monitoruje niskopoziomowe komunikaty wejściowe myszy (tylko w Windows NT).
WH_MSGFILTER
Monitoruje komunikaty wygenerowane jako zdarzenia wejściowe w oknie dialogowym, oknie komunikatu, menu lub pasku przewijania.
WH_SHELL
Otrzymuje komunikaty powiadamiające o zdarzeniach mających znaczenie dla aplikacji powłoki.
WH_SYSMSGFILTER
Podobnie do WH_MSGFILTER, monitoruje komunikaty wygenerowane jako zdarzenia wejściowe w oknie dialogowym, oknie komunikatu, menu lub pasku przewijania. Jednak procedura WH_SYSMSGFILTER monitoruje te komunikaty dla wszystkich aktywnych aplikacji.
Funkcja UnhookWindowsHookEx()
Aby usunąć hak z łańcucha wywołań funkcji haków, musisz wywołać funkcję Unhook-WindowsHookEx(). Ta funkcja (o podanym poniżej prototypie) otrzymuje po prostu wartość HHOOK otrzymaną w uprzednim wywołaniu funkcji SetwindowsHookEx (). Funkcja UnhookWindowsHookEx (} zwraca wartość logiczną, określającą, czy wywołanie funkcji się powiodło.
BOOL UnhookWindowsHookEx(HHOOK hHook);
Funkcja CallNextHookEx()
Przy ustawianiu funkcji danego typu haka, jest ona wstawiana na początek łańcucha funkcji filtrów dla tego typu. Gdy funkcja filtra wykona swoje zadanie, może wywołać funkcję CallNextHookEx () w celu przekazania komunikatu następnej funkcji filtra w łańcuchu. Zdarzają się jednak przypadki, gdy nie chcesz by przetwarzanie komunikatu było kontynuowane. Na przykład, gdy piszesz hak klawiatury i chcesz zablokować wciśnięcia jakiegoś klawisza, nie powinieneś wywoływać tej funkcji. Jeśli jednak tylko rejestrujesz zdarzenia w dzienniku i chcesz by komunikat trafił do swojego miejsca przeznaczenia, po przetworzeniu komunikatu funkcja haka powinna wywołać funkcję CallNextHookEx (). Jeśli nie będzie innych funkcji filtra dla tego typu haka, Windows dostarczy komunikat do odpowiedniej kolejki komunikatów.
Pisanie biblioteki DLL dla haka klawiatury
Najpierw musisz stworzyć bibliotekę DLL, która będzie zawierała funkcję filtra dla haka klawiatury. W tym celu stwórz projekt o nazwie KeybdHook, wybierając przy tym następujące opcje:
MFC AppWizard (dli) w oknie dialogowym New.
Regular DLL with MFC statically linked przy wyborze rodzaju biblioteki DLL.
Otwórz plik KeybdHook.h i przed deklaracją klasy CKeybdHookApp dopisz poniższe linie. Pierwsza dyrektywa #define stanowi łatwy sposób eksportowania funkcji wywoływanych z użyciem mechanizmu _stdcall. Druga funkcja jest zasadniczą funkcją filtra. Właśnie ona będzie wywoływana dla każdego wciskanego klawisza. Jej argumenty zostaną omówione w dalszej części rozdziału.
#define EXPORTED_DLL_FUNCTION \ _declspec(dllexport) _stdcall
LRESULT EXPORTED_DLL_FUNCTION KbdHookProc (int nCode, WPARAM wParam, LPARAM 1Param);
Następnie otwórz plik KeybdHook. cpp i na jego początku dopisz poniższe linie. Służą one do stworzenia wspólnego segmentu danych dla wszystkich procesów korzystających z tej biblioteki DLL. W ten sposób wartości zdefiniowane wewnątrz tego bloku pragma będą globalne dla wszystkich klientów tej biblioteki DLL. Przy deklarowaniu zmiennych globalnych koniecznie trzeba je zainicjalizować.
#pragma data_seg(".SHARDAT") static HWND ghWndMain = 0; static HHOOK ghKeyHook = NULL ; ttpragma data_seg()
HINSTANCE ghlnstance = 0; HOOKPROC glpfnHookProc = 0;
Po zdefiniowaniu zmiennych globalnych używanych w tym DLL-u, dopisz funkcję filtra pokazaną na listingu 29.1, wywoływaną za każdym razem, gdy Windows wykryje wciśnięcie klawisza. Jak widać, ta funkcja po prostu wychwytuje wciśnięcia klawisza F10.
Wychwycenie wciśnięcia klawisza zastosowane w tej funkcji nie jest skomplikowane. Gdy funkcja filtra wychwyci wciśnięcie właściwego klawisza, możesz przejść do wykonywania dowolnych czynności. Zwróć uwagę, gdy obsłużysz wciśnięcie klawisza i nie chcesz, by Windows przetwarzały je dalej, zamiast wywoływać funkcję caiiNextHookEx() zwróć po prostu wartość TRUE.
Listing 29.1. Funkcja filtra wywoływana za każdym razem, gdy Windows wykryje wciśnięcie _________klawisza na klawiaturze__________________________________
LRESULT EXPORTED_DLL_FUNCTION KbdHookProc (int nCode, WPARAM wParam, LPARAM 1Param)
{
BOOL bHandledKeystroke = FALSE;
if (((DWORD)IParam & 0x40000000) && (HC_ACTION == nCode)) { switch (wParam)
case VK_F10:
AfxMessageBox("Wychwycono klawisz F10!");
bHandledKeystroke = TRUE; break;
}
}
default : break ;
}
}
return (bHandledKeystroke ? TRUE : ::CallNextHookEx (ghKeyHook, nCode, wParam, 1Param));
}
Przyjrzyjmy się argumentom przekazywanym funkcji haka klawiatury.
Jedną z wartości, jaką może zawierać pierwszy argument, nCode, jest stała HC_ACTION. W rzeczywistości, ten argument może mieć tylko dwie wartości: HC_ACTION lub HC_NOREMOVE. W obu przypadkach parametry wParam i IParam zawierają informacje o wciśniętym klawiszu. Jednak w przypadku, gdy nCode ma wartość HC_NOREMOVE, oznacza to, że docelowa aplikacja dla tego komunikatu użyła funkcji PeekMessage () ze znacznikiem PM_NOREMOVE. Dodatkowo, jeśli wartość nCode jest mniejsza niż zero, funkcja filtra nie powinna przetwarzać komunikatu i zamiast tego powinna wywołać funkcję CallNextHookEx ().
Wartość wParam zawiera wirtualny kod dla wciśniętego klawisza, używany do sprawdzania, który klawisz został wciśnięty. Na koniec, parametr IParam zawiera dodatkowe informacje o wciśniętym klawiszu, na przykład takie jak: ilość powtórzeń, czy stan klawisza przed wysłaniem komunikatu.
Biblioteka DLL musi wyeksportować kilka funkcji, aby umożliwić klientowi ustawienie i zwolnienie haka klawiatury. Tak więc najpierw otwórz plik KeybdHook.h i zadeklaruj poniższą funkcję, która będzie używana do instalowania (ustawiania) haka klawiatury:
BOOL EXPORTED_DLL_FUNCTION InstallKeyboardHook (HWND hWnd);
Po zadeklarowaniu tej funkcji, zdefiniuj ją w pliku KeybdHookcpp (tak jak w poniższym przykładowym kodzie). Jeśli wywołanie tej funkcji się powiedzie i hak zostanie ustawiony, zmiennej ghKeyHook zostanie przypisany uchwyt HHOOK otrzymany od funkcji SetwindowsHookEx (). Tak więc pierwszą rzeczą w funkcji InstallKeyboardHook () jest sprawdzenie, czy zmienna ta ma jakąś wartość. Jeśli tak, oznacza to, że hak został ustawiony już wcześniej i funkcja zwraca sterowanie, nie robiąc nic więcej. Jeśli zmienna ghKeyHook nie zawiera żadnej wartości, funkcja próbuje ustawić hak klawiatury. W globalnym segmencie danych zdefiniowałeś już zmienną glpfnHookProc, ale co z ustawieniem zmiennej ghinstance? Ta zmienna będzie ustawiana w funkcji initinstance o biblioteki DLL. Nasza funkcja InstallKeyboardHook () zwraca następnie wartość logiczną oznaczającą sukces lub porażkę w ustawieniu haka.
BOOL EXPORTED_DLL_FUNCTION InstallKeyboardHook (HWND hWnd)
{
BOOL bSuccess = FALSE;
if (!ghKeyHook)
{
ghWndMain = hWnd ;
glpfnHookProc = (HOOKPROC) KbdHookProc ;
bSuccess = (NULL != (ghKeyHook = ::SetWindowsHookEx (WH_KEYBOARD, glpfnHookProc, ghinstance, NULL) )) ;
}
return bSuccess;
}
Po wyeksportowaniu funkcji ustawiającej hak klawiatury musisz także wyeksportować funkcję zwalniającą ten hak. W pliku KeybdHook.h, powyżej deklaracji funkcji
InstallKeyboardHook (), zadeklaruj dodatkową funkcję:
BOOL EXPORTED_DLL_FUNCTION DelnstallKeyboardHook(J;
Po dopisaniu tej deklaracji, przejdź do pliku KeybdHook.cpp i dopisz do niego poniższą funkcję. Zwróć uwagę, że w przypadku pomyślnego wykonania funkcji UnHookWindows-HookEx(), zmienna ghKeyHook jest ponownie ustawiana na NULL. Dzięki temu klient tej biblioteki DLL może w razie potrzeby ponownie wywołać funkcję InstallKeyboardHook () w celu ponownego ustawienia haka.
BOOL EXPORTED_DLL_FUNCTION DelnstallKeyboardHook()
{
if (ghKeyHook) {
if (TRUE == (O != ::UnhookWindowsHookEx(ghKeyHook)))
{ ghKeyHook = NULL;
}
}
return (NULL == ghKeyHook);
}
W tym momencie mamy już cały kod potrzebny do ustawiania i zwalniania haka, więc za pomocą ClassWizarda możemy dodać do obiektu CKeybdHookApp inicjalizacyjno-porządkowe funkcje initinstance ( ) oraz Exitinstance ( ) . Powinny być zdefiniowane następująco:
BOOL CKeybdHookApp: : Initinstance { } {
AFX_MANAGE_STATE (AfxGetStaticModuleState ( ) ) ;
ghlnstance = AfxGetInstanceHandle ( ) ;
return TRUE;
}
int CKeybdHookApp: : Exitlnstance ( )
{
DelnstallKeyboardHook(); return CWinApp::Exitlnstance();
}
Gdy dopiszesz te funkcje, zbuduj bibliotekę DLL i skopiuj ją do systemowego foldera Windows.
Budowanie aplikacji testującej DLL-a haka klawiatury
Po napisaniu funkcji haka, nadszedł czas, aby j ą przetestować. Aby móc to zrobić, za pomocą AppWizarda, stwórz opartą na oknie dialogowym aplikację KeybdHookClient. Po stworzeniu projektu, za pomocą ClassWizarda, stwórz funkcję obsługi kliknięć na przycisku i DOK. W tej funkcji obsługi wywołaj po prostu eksportowaną przez bibliotekę DLL funkcję InstallKeyboardHook (). Twoja funkcja OnOK () powinna wyglądać następująco:
void CKeybdHookClientDlg::OnOK()
{
m_bHookInstalled = InstallKeyboardHook(GetSafeHwnd());
}
Następnie, za pomocą ClassWizarda przesłoń funkcję OnCancel o okna dialogowego. W tej funkcji po prostu zwolnimy hak klawiatury przed zamknięciem okna dialogowego. Gdy skończysz funkcja OnCancel () powinna wyglądać następująco:
void CKeybdHookClientDlg::OnCancel()
{
DelnstallKeyboardHook(); CDialog::OnCancel();
}
Tak jak w przypadku wszystkich bibliotek DLL, wszystko co teraz musisz zrobić to dołączyć do pliku nagłówkowy zawierający deklaracje funkcji, które chcesz wywołać. Na początku pliku KeybdHookClient.cpp dopisz więc poniższą dyrektywę #include. Oczywiście, we własnym projekcie musisz podać także względną ścieżkę dostępu do foldera, w którym znajduje się ten plik.
#include"KeybdHook.h"
Zanim spróbujesz zbudować i uruchomić aplikację, jedyne co pozostało to dołączyć do projektu bibliotekę importową biblioteki DLL, plik KeybdHook.lib.
Testowanie biblioteki DLL haka klawiatury
Aby przetestować hak klawiatury, znajdź jakiś program wykorzystujący klawisz F10. Ponieważ klawisz F10 zwykle jest wykorzystywany w celu dostępu do menu za pomocą klawiatury, korzysta z niego mnóstwo różnych aplikacji. My wybraliśmy standardową aplikację Notatnika, instalowaną we wszystkich egzemplarzach Windows. Aby z jej pomocą sprawdzić działanie haka klawiatury, wykonaj poniższe kroki:
1. Uruchom Notatnik. Kursor znajdzie się w obszarze dokumentu (stanowiącej w rzeczywistości wielowierszową kontrolkę pola edycji).
2. Wciśnij klawisz F10; znajdziesz się w menu Plik.
3. Wciśnij klawisz Esc, wracając z menu do dokumentu.
4. Uruchom aplikację KeybdHookClient i wciśnij klawisz Enter, co spowoduje ustawienie haka klawiatury.
5. Wróć do Notatnika i wciśnij klawisz F10.
6. Tym razem zamiast przejścia do menu Plik, na ekranie pojawi się okno komunikatu informujące o poprawnym wychwyceniu klawisza F10. Ponieważ nasza funkcja haka nie wywołuje wtedy funkcji CallNextHookEx (), wychwycony komunikat nie zostanie przesłany do Notatnika, w związku z czym za pomocą klawisza F10 nie będziesz mógł dostać się do menu tego programu. Innymi słowy, wciśnięcie klawisza zostało odfiltrowane i odrzucone.
7. Zamknij aplikację KeybdHookClient. Spowoduje to zwolnienie haka klawiatury.
8. Wróć do Notatnika i wciśnij klawisz F10. Tym razem klawisz zadziała zgodnie z przeznaczeniem, gdyż wciśnięcie nie zostanie wychwycone przez hak klawiatury.
Rysunek 29. l przedstawia wynik działania haka klawiatury. Zwróć uwagę, mimo iż w momencie wciskania klawisza F10 aktywną aplikacją był Notatnik, funkcja filtra wychwyciła wciśnięcie klawisza i wyświetliła komunikat, nie pozwalając aplikacji Notatnika na przetworzenie go.
Globalne obiekty C++ w bibliotekach DLL
DLL-e Win32 są mapowane w przestrzeń adresową procesu wywołującego, przy czym każde wywołanie DLL-a otrzymuje świeżą kopię swoich danych. Jednak często biblioteki DLL muszą korzystać z danych nie należących do konkretnego wywołania, ale do wszystkich wywołań wspólnie, co wymaga zdefiniowana w bibliotece danych globalnych. Jak widzieliśmy w przykładzie haka klawiatury, dzielenie globalnych danych pomiędzy różne wywołania DLL-a nie jest trudne. Należy po prostu użyć dyrektywy #pragma w celu nazwania segmentu danych, po czy zadeklarować i zainicjalizować dane w tym segmencie. Dzięki temu aplikacja może przechowywać dane bez względu na wywołanie biblioteki DLL. Dane globalne są przydatne do rzeczy takich jak: śledzenie ilości klientów połączonych z biblioteką DLL. Innym przykładem może być serwer bazy danych. Serwer prawdopodobnie posiadałby tabele i listy aktualnie połączonych klientów, blokowanych i buforowanych rekordów, a także uchwytów bazy danych. Jak widać, zadeklarowanie zmiennej globalnej prostego typu (na przykład long czy WORD) może być wykorzystane do śledzenia tego, ile klientów jest połączonych z biblioteką DLL. Niestety, zarządzanie bardziej rozbudowanymi strukturami danych, takimi jak, obiekty C++, wymaga trochę więcej pracy ze strony biblioteki DLL.
Problem związany ze wspomnianą metodą globalizowania obiektów C++ w bibliotece DLL polega na tym, że za każdym razem, gdy proces dołącza się do biblioteki DLL, wywoływany jest konstruktor obiektu. Oto przykład tego problemu:
#pragma data_seg ( " . SHARDAT" )
_declspec (dllexport) CCounter counter; _declspec (dllexport) WORD wCounter = 0;
#pragma data_seg()
class CCounter
{
public :
CCounter () { m_wCounter =0; }
};
W powyższym fragmencie kodu, DLL musi przechowywać ilość aktualnie podłączonych do siebie klientów. Próbują to robić obie linie kodu. Pierwsza linia używa obiektu posiadającego zmienną składową zawierającą ilość podłączonych klientów, podczas gdy druga linia korzysta z prostej zmiennej typu WORD. Załóżmy, że oba liczniki są zwiększane za każdym razem, gdy klient dołącza się do biblioteki i zmniejszane, gdy się od niej odłącza. W powyższym kodzie, zmienna wCounter będzie działać tak, jak tego oczekujemy. Jednak obiekt CCounter nie będzie działał poprawnie. Dzieje się tak, ponieważ za każdym razem, gdy klient podłącza się do tej biblioteki, jest wywoływany konstruktor klasy CCounter, zerujący zawartość licznika w zmiennej składowej.
Jeśli w dokumentacji MSDN poszukasz rozwiązania tego problemu, znajdziesz poniższą radę:
W celu dzielenia [obiektów C++], musisz użyć pliku odwzorowanego w pamięci. Więcej informacji na temat plików odwzorowanych w pamięci znajdziesz w sekcji File Mappingw dokumentacji Win 32 SDK.
Jeśli kiedykolwiek pracowałeś z plikami odwzorowanymi w pamięci, z pewnością zdajesz sobie sprawę, że ta rada nie jest tym, czego szukasz. Choć pliki odwzorowane w pamięci w pewnych zastosowaniach są bardzo użyteczne, jednak użycie ich do dzielenia pojedynczego obiektu przypomina strzelanie z armaty do komara. Na szczęście, istnieje rozsądna alternatywa, wykorzystująca tzw. operator placement new.
Operator placement new
Choć wyczerpujący opis operatora placement new wykracza poza ramy tego rozdziału, jednak spróbujemy opisać go na tyle dokładnie, abyś mógł zorientować się jak możesz go wykorzystać do dzielenia w bibliotekach DLL, globalnych obiektów C++. Obiekty zwykle alokowane są na stercie, na stosie lub w pamięci statycznej. Odbywa się to odpowiednio poprzez alokację dynamiczną, alokację automatyczną oraz alokację statyczną. Jednak czasem występuje potrzeba wskazania dokładnego adresu, pod jakim obiekt powinien zostać utworzony. Na przykład, urządzenie sprzętowe musi wymieniać dane z aplikacją. Osiąga się to poprzez zastosowanie operatora placement new. Oto przykład, w którym obiekt CRegi ster jest alokowany pod określonym, zaszytym w kodzie adresem pamięci:
void* pMemory = (void)OxDEADBEEF;
CRegisters* pRegisters = new (pMemory) CRegisters();
Miejsce alokowane po adresem wskazywanym przez pMemory musi być na tyle duże, aby pomieściło obiekt CRegister, ponadto musi być poprawnie wyrównane w celu pomieszczenia tego obiektu. Zwróć uwagę, że wartość numeryczna wskaźnika pRegisters będzie taka sama jak wskaźnika pMemory. Jednak w tym przypadku wskaźnik pRegisters wskazuje na typ CRegisters, zaś pMemory jest wskaźnikiem do typu void.
Oczywiście, najprawdopodobniej nie będziesz wprost wskazywał adresu, pod jakim ma zostać utworzony obiekt. Jednak możesz zaalokować bufor w pamięci, a następnie przekazać adres tego bufora operatorowi placement new tworzącemu inny obiekt, jaki ma znaleźć się w buforze, podobnie jak w przypadku poniższego segmentu data_seg w swojej bibliotece DLL:
#pragma data_seg(".SHARDAT")
static char IpszMemory[sizeof(CRegisters}];
CRegisters* g_pRegisters =NULL;
int g_nClients = 9;
ttpragma data_seg();
Zwróć uwagę, że w tym momencie mamy w pamięci bufor (IpszMemory) o rozmiarze obiektu CRegisters. Dodatkowo, wskaźnik do obiektu CRegister ma wartość NULL, zaś licznik (g_nciients) jest używany do śledzenia ilości podłączonych klientów. Teraz wszystko co musi zrobić biblioteka DLL, to zaalokować pamięć i skonstruować obiekt CRegister w momencie podłączenia pierwszego klienta, (gdy wskaźnik g_pRegister ma wartość NULL) oraz zwolnić zaalokowaną pamięć w momencie odłączenia ostatniego klienta (gdy licznik g_nciients osiągnie wartość zero). Całą tę procedurę przedstawimy szczegółowo w demonstracyjnym programie GlobalDllObjects.
Program demonstracyjny GlobalDIIObjects
Za pomocą AppWizarda stwórz bibliotekę DLL korzystającą z MFC. Stwórz zwykłą bibliotekę DLL ze statycznie dołączoną biblioteką MFC. Gdy zostaną utworzone pliki projektu, otwórz plik GlobalDIIObjects. h i dopisz poniższy kod tuż przed deklaracją klasy CGlobalDllobjectsApp. Jak już wiesz, te dyrektywy zapewniają mimo iż ten plik nagłówkowy może być dołączony do każdego modułu, makro GLOBALDLLOBJECTS_API zostanie rozwinięte do modyfikatora declspec(dllexport) tylko w module ze zdefiniowanym symbolem GLOBALDLLOBJECTS_EXPORTS.
#ifdef GLOBALDLLOBJECTS_EXPORTS
ftdefine GLOBALDLLOBJECTS_API _ declspec (dllexport }
#else
ttdefine GLOBALDLLOBJECTS_API _ declspec (dllimport )
#endif
Następnie do pliku GlobalDIIObjects. h dopisz deklarację klasy ciest, której obiekt będzie udostępniany globalnie przez bibliotekę DLL (listing 29.2). Operatory new zostały przesłonięte w taki sposób, że obiekty tej klasy nie mogą być tworzone inaczej niż z użyciem operatora placement new. Dodatkowo, zarówno konstruktor jak i destruktor klasy wyświetlają, w momencie wywołania odpowiednie komunikaty. Dzięki temu, podczas testowania biblioteki DLL, będziesz mógł sprawdzić, czy konstruktor i destruktor są wywoływane tylko raz, bez względu na ilość podłączanych i odłączanych klientów tej biblioteki.
Listing 29.2. Deklaracja klasy CTest __________________________________
class GLOBALDLLOBJECTS_API CTest
{
public :
// Jedyne dozwolone konstruowanie obiektu
// odbywa się z użyciem operatora placement
// new.
void* operator new(size_t) { return NULL; }
void* operator new(size_t, void* p) { return p; }
void operator delete (void*) {};
// W konstruktorze i destruktorze są wyświetlane // komunikaty, dzięki czemu możesz się przekonać // że obiekt jest konstruowany tylko wtedy, gdy // podłącza się pierwszy klient i niszcony tylko // wtedy, gdy odłącza się ostatni klient. CTest (} {
: :MessageBox (NULL, "Konstruktor",
"CTest.DLL", MB_OK) ;
}
CTest () {
: :MessageBox (NULL, "Destruktor" ,
"CTest.DLL", MB_OK) ;
}
CTest (const CTest&) ;
};
Dopisz poniższe deklaracje funkcji poniżej deklaracji klasy CTest. Te funkcje pozwalają klientom DLL-a na pobieranie wskaźnika do globalnego obiektu CTest oraz pobranie ilości klientów aktualnie podłączonych do biblioteki.
#define EXPORTED_DLL_FUNCTION \ _declspec(dllexport) _stdcall
void EXPORTED_DLL_FUNCTION GetTestPtr(CTest*& rpTest); int EXPORTED_DLL_FUNCTION GetNbrOfClients();
Następnie otwórz plik GlobalDllObjects.cpp i zaraz po dyrektywie #include "stdaf x .h" dopisz dyrektywę włączającą plik nagłówkowy new.h:
#include
Jak już wspominaliśmy, główny moduł biblioteki będzie musiał mieć zdefiniowany symbol GLOBALDLLOBJECTS_EXPORTS. Tak więc przed dyrektywą włączającą plik GlobalDllOb-jects.h dopisz poniższą dyrektywę #def ine:
#define GLOBALDLLOBJECTS_EXPORT
Teraz dopisz poniższy kod tworzący globalny mutex dla biblioteki DLL. Ten mutex pozwoli bibliotece na synchronizowanie dostępu do globalnych danych, które biblioteka będzie alokować i dealokować.
// blokowanie dostępu między procesami
HANDLE g_hMutex = ::CreateMutex(NULL, FALSE, "CTest");
Następnie dodaj poniższy segment data_seg do pliku GlobalDllObjects.cpp. Jak widać, segment data_seg ma nazwę .SHARDAT i zawiera trzy zmienne globalne. Pierwszą zmienną jest blok pamięci o rozmiarze odpowiadającym rozmiarom obiektu klasy CTest. Drugą zmienną jest wskaźnik do obiektu CTest inicjowany wartością NULL. Na koniec, trzecią i ostatnią zmienną jest licznik śledzący ilość aktualnie podłączonych klientów.
ftpragma data_seg(".SHARDAT")
static char 1IpszMemory[sizeof(CTest)];
CTest* g_pTest = NULL;
int g_nClients = 0;
ftpragma data_seg()
Gdy już zadeklarujesz globalne zmienne, musisz skonstruować obiekt CTest. Jak już wiesz, inicjalizacja zwykłej biblioteki DLL może odbywać się w zmiennej składowej initin-stance (). Tak więc za pomocą ClassWizarda stwórz następującą implementację tej funkcji:
BOOL CGlobalDllObjectsApp::InitInstance()
{
Pierwszą rzeczą, jaką trzeba zrobić w tej funkcji, jest wywołanie funkcji waitForSingie-object () po to, aby kilku klientów jednocześnie dołączających się do biblioteki DLL nie spowodowało dwukrotnego zaalokowania obiektu CTest. W efekcie, dostęp do funkcji InitInstance () biblioteki DLL jest serializowany poprzez nasz mutex.
::WaitForSingleObject(g_hMutex,INFINITE);
Po uzyskaniu mutexu, sprawdzany jest wskaźnik g_pTest. Jeśli nie zawiera żadnej wartości, oznacza to, że bieżący klient jest pierwszym klientem łączącym się z biblioteką DLL:
if (!g_pTest) { ::MessageBox(NULL,
"CGlobalDllObjectsApp::Initlnstance - "
"konstruowanie globalnego CTest",
"CTest.DLL", MB_OK);
Jeśli to faktycznie pierwszy, łączący się klient, tworzymy obiekt CTest pod adresem wskazywanym przez globalną zmienną ipszMemory.
g_pTest = ::new (IpszMemory) CTest; ASSERT(g pTest);
}
Ostatnią rzeczą w funkcji jest zwiększenie globalnej zmiennej g_nCount w celu odzwierciedlenia faktu dołączenia się do biblioteki kolejnego klienta. Wywołując funkcję ReleaseMutex () musimy także zwolnić posiadany mutex:
g_nClients++;
: : ReleaseMutex (g_hMutex) ;
return CWinApp: : Initlnstance ();
}
Po zaimplementowaniu funkcji Initlnstance ( ) , za pomocą ClassWizarda zaimplementuj funkcję Exitinstance ( ) :
int CGlobalDllObjectsApp : : Exitlnstance ( )
{
Także w tym przypadku, przed odwołaniem się do globalnych zmiennych musimy uzyskać globalny mutex zapewniający serializację dostępu:
: :WaitForSingleObject (g_hMutex, INFINITE) ;
Po uzyskaniu mutexu, sprawdzana jest zmienna globalna zawierająca ilość aktualnie podłączonych klientów (g_nClients). Jeśli jej wartość wyniesie zero, pamięć zaalo-kowana dla obiektu CTest jest zwalniana.
if (O == -- g_nClients)
{
MSG msg;
::PeekMessage(&msg, NULL, WM_QUIT, WM_QUIT, PM_REMOVE);
::MessageBox(NULL,
"CGlobalDllObjectsApp::ExitInstance - " "niszczenie globalnego obiektu CTest", "CTest.DLL", MB OK);
Następne linie niszczą globalny obiekt CTest i ustawiają wskaźnik na NULL, jeśli inny proces dołączy się do biblioteki DLL przed usunięciem jej z pamięci, obiekt będzie mógł zostać odtworzony.
delete g_pTest; g_pTest = NULL;
}
Zwolnij i zamknij uchwyt globalnego mutexu używanego do serializacji dostępu do globalnych zmiennych:
: : ReleaseMutex (g_hMutex) ; : : CloseHandle (g_hMutex) ;
return CWinApp: : Exitlnstance ( ) ;
}
Ostatnią zmianą, jakiej musimy dokonać w kodzie biblioteki, jest zaimplementowanie dwóch funkcji, które będą eksportowane przez bibliotekę. Pierwsza z nich, GetTestPtr ( ) , będzie używana do pobierania wskaźnika, do globalnego obiektu CTest ( ) . Druga funkcja, GetNbrOfClients ( ) , będzie zwracała ilość aktualnie podłączonych klientów.
void EXPORTED_DLL_FUNCTION GetTestPtr (CTest*& rpTest)
rpTest = g_pTest;
}
int EXPORTED_DLL_FUNCTION GetNbrOf Client s () { return g_nClients;
}
Gdy już zakończysz tworzenie kodu biblioteki, wciskając kombinację klawiszy Alt+F7, wyświetl okno dialogowe ustawień projektu dla projektu GlobalDllobjects. Na zakładce Link, z rozwijanej listy Settings For, wybierz pozycję Win32 Debug. Następnie wpisz poniższe polecenie na końcu pola edycji Project Options w dolnej części okna dialogowego. Gdy to zrobisz, uczyń to samo dla pozycji Win32 Release na rozwijanej liście Settings For.
/SECTION: .SHARDAT,RWS
To wszystko. Zbuduj DLL-a i skopiuj go do foldera, w którym Windows będzie mógł go znaleźć. Jeśli nie wiesz, gdzie skopiować plik biblioteki DLL, zajrzyj do sekcji "Ładowanie bibliotek DLL" we wcześniejszej części rozdziału.
Użycie funkcji PeekMessage() do usuwania komunikatu WM_QUIT
W funkcji Exitinstance () być może zauważyłeś wywołanie funkcji PeekMessage () tuż przed wywołaniem funkcji MessageBox (). Jest ona wywoływana z kilku powodów. Po pierwsze, aplikacja klienta (GlobalDllObjectsClienf) łączy się z tą biblioteką niejawnie. W związku z tym, wywołanie funkcji Exitinstance () w tej bibliotece oznacza, że aplikacja klienta właśnie kończy działanie. Po drugie, gdy jest zamykane główne okno dialogowe aplikacji MFC opartej na oknie dialogowym, MFC wysyła komunikat WM_QUIT.
Jeśli w kolejce komunikatów oczekuje komunikat WM_QUIT, okno komunikatu nie jest wyświetlane. Jednak wywołanie funkcji PeekMessageO ze znacznikiem PM_REMOVE powoduje usunięcie tego komunikatu z kolejki i pozwala na wyświetlenie okna komunikatu. Oto przykład jak można wywołać funkcję PeekMessage ():
MSG msg;
::PeekMessage(&msg,NULL,
WM QUIT, WM QUIT,PM REMOYE)
Testowa aplikacja GlobalDIIObjectCIient
Gdy już mamy bibliotekę DLL, musimy zbudować testową aplikację sprawdzającą, czy nasza biblioteka działa tak jak tego oczekujemy. Za pomocą AppWizarda stwórz opartą na oknie dialogowym aplikację MFC o nazwie GlobalDIIObjectCIient. Projekt aplikacji i plik wykonywalny znajdziesz także na dołączonej do książki płytce CD-ROM, w folderze Rozdz29\GlobalDllObjectClient. Gdy AppWizard zakończy działanie, otwórz plik Global-DllObjectClientDlg.cpp i dopisz do niego poniższą dyrektywę:
ttinclude "GlobalDllObjects .h"
Dodaj funkcję obsługi komunikatu BN_CLICKED dla przycisku IDOK, w głównym oknie dialogowym programu. Ta funkcja będzie wywoływać funkcję GetTestPtr ( ) biblioteki GlobalDllObjects.dll w celu otrzymania wskaźnika do globalnego obiektu CTest. Dodatkowo będziemy wywoływać także funkcję GetNbrOfClients ( ) w celu pobrania ilości aktualnie podłączonych klientów. Obie otrzymane wartości zostaną wyświetlone w oknie dialogowym. Gdy skończysz, funkcja onOK ( ) powinna wyglądać następująco:
void CGlobalDllObjectClientDlg: : OnOK ( ) {
CTest* pTest;
GetTestPtr (pTest) ;
ASSERT (pTest) ;
if (pTest)
{ CString str;
str. Format ("Adres obiektu CTest = Ox%08x, " "Dołączonych klientów: %ld\n", pTest, GetNbrOfClients () );
AfxMessageBox (str) ;
}
}
Ostatnią rzeczą jaką musisz zrobić jest dodanie biblioteki importowej GlobalDllObjects.lib do tworzonego projektu i zbudowanie aplikacji.
Testowanie biblioteki GlobalDIIObjects.dll
Uruchom kopię programu GlobalDllObjectClient. Powinieneś ujrzeć dwa okna komunikatów. Pierwsze z nich jest wynikiem wywołania metody initinstance () biblioteki DLL, tworzącej globalny obiekt CTest. Drugi komunikat pochodzi od konstruktora tego obiektu. Te dwa okna komunikatów powinieneś ujrzeć tylko wtedy, gdy jest podłączany pierwszy klient biblioteki DLL. Każdy następny klient powinien korzystać z tego samego obiektu CTest utworzonego przy pierwszym podłączaniu się aplikacji. Aby sprawdzić, czy biblioteka działa właśnie w ten sposób, uruchom drugą kopię aplikacji. Tym razem nie powinieneś ujrzeć żadnego okna komunikatu; jednak kliknięcie na przycisku IDOK okna dialogowego powinno spowodować wyświetlenie okna komunikatu zawierającego dokładnie te same dane: adres obiektu CTest biblioteki DLL oraz ilość aktualnie podłączonych klientów. Rysunek 29.2 pokazuje przykład dwóch klientach dołączonych do biblioteki.
Biblioteki DLL rozszerzeń MFC
O ile zwykłe biblioteki DLL są najczęściej wykorzystywane przez klientów nie będących programami MFC, o tyle biblioteki rozszerzeń MFC są używane do eksportowania funkcji i klas zwiększających funkcjonalność biblioteki MFC. Na przykład, załóżmy, że stworzyłeś nowy rodzaj paska narzędzi, wyprowadzony z klasy MFC CToolbar. Aby wyeksportować tę klasę, musisz umieścić ją w bibliotece DLL rozszerzenia MFC. Innym przykładem konieczności użycia biblioteki rozszerzeń MFC może być sytuacja, w której aplikacja jest aplikacją MDI i chcesz reprezentować każdy widok lub logiczną grupę widoków w oddzielnych bibliotekach DLL.
Działanie bibliotek DLL rozszerzeń MFC
Choć zarówno zwykłe biblioteki jak i biblioteki rozszerzeń MFC można tworzyć za pomocą AppWizarda, jednak wy stępuj ą pomiędzy nimi zasadnicze różnice:
Każdy klient biblioteki DLL rozszerzeń MFC musi być programem MFC.
W odróżnieniu od zwykłych DLL-i, biblioteki rozszerzeń MFC nie posiadają obiektu wyprowadzonego z klasy CWinApp.
O ile zwykłe DLL-e zwykle są inicjalizowane w swoich funkcjach initin-
stance () i Exitinstance (), o tyle biblioteki rozszerzeń MFC zawierają w tym celu funkcję DllMain ().
Biblioteki rozszerzeń MFC posiadają nową klasę, CDynLinkLibrary, pozwalającą na eksportowanie informacji CRuntimeClass lub zasobów.
Eksportowanie klas przez biblioteki rozszerzeń MFC
DLL-e rozszerzeń MFC zwykle są implementowane, wtedy gdy wystąpi potrzeba eksportowania klas opartych na MFC. Klasy i ich funkcje są eksportowane na jeden z dwóch sposobów, w zależności od tego, czy jest eksportowana cała klasa, czy tylko podzbiór jej funkcji składowych.
W poprzednich sekcjach opisujących zwykłe biblioteki DLL widziałeś, że w celu wyeksportowania funkcji ze zwykłego DLL-a zwykle używa się pliku .DEF. W przypadku klas C++ (takich jak klasy MFC) lub funkcji składowych jest to o tyle niewygodne, że gdy chcesz użyć do ich wyeksportowania pliku .DEF, musisz podać stworzone przez kompilator udekorowane nazwy klas i funkcji. Na szczęście MFC posiada kilka makr, które to znacznie ułatwiają. Eksportując z biblioteki całą klasę, w deklaracji klasy możesz zastosować makro AFX_EXT_CLASS:
class AFX EXT_CLASS CMyFancyToolbar : public CToolbar
...
Eksportowanie części klasy jest równie łatwe. Zamiast umieszczać makro AFX_EXT_CLASS przed nazwą klasy, umieść je bezpośrednio przed nazwą funkcji, którą chcesz wyeksportować:
class CMyFancyDialog : public CDialog
{
public:
AFX_EXT_CLASS CMyFanceDialog() ; AFX_EXT_CLASS int DoModal(); public:
BOOL Create(LPCTSTR 1pszTemplateName, CWnd* pParentWnd = NULL); BOOL Create(UINT nIDTemplate, CWnd* pParent = NULL);
...
Zwróć uwagę, że w klasie CMyFancyDialog jest eksportowany konstruktor klasy oraz funkcja DoModal (), lecz nie są eksportowane dwie przeciążone wersje funkcji Create (). Oznacza to, że klient może konstruować klasę i wywoływać jej funkcję DoModal (), lecz jeśli będzie posiadał w swoim kodzie wywołanie którejś z przeciążonych funkcji Create (), budowa programu nie powiedzie się, gdyż program łączący nie będzie mógł znaleźć odpowiednich funkcji.
Makro AFX_EXT_CLASS
Jak już wiemy, makro AFX_EXT_CLASS może zostać użyte do eksportowania albo całych klas, albo ich części. Jednak, gdy aplikacja klienta dołącza do siebie plik nagłówkowy zawierający deklaracje klas eksportowanych przez DLL-a, pojawia się problem, gdyż oba moduły informują program łączący, że są odpowiedzialne za eksportowanie klasy. Ten potencjalny problem jest rozwiązywany dzięki sposobowi zdefiniowania makra AFX_EXT_CLASS, które zależy od poniższych definicji preprocesora w projekcie:
Jeśli w projekcie są zdefiniowane symbole _AFXDLL oraz _AFXEXT, makro AFX_EXT_ c LAS s jest rozwijane następująco:
_declspec(dllexport)
Jeśli _AFXEXT nie jest zdefiniowane, makro AFX_EXT_CLASS jest rozwijane następująco w celu importowania klasy:
_declspec(dllimport)
Tak więc, gdy AppWizard tworzy bibliotekę rozszerzeń MFC, definiuje zarówno symbol _AFXDLL jak i _AFXEXT. Za każdym razem, gdy kod źródłowy DLL-a odwołuje się do makra AFX_EXT_CLASS, klasa jest eksportowana.
Użycie zagnieżdżonych bibliotek rozszerzeń MFC
Jak już wspominaliśmy, makro AFX_EXT_CLASS jest używane do eksportowania z biblioteki DLL całych klas lub ich części. Jeśli jednak aplikacja chce użyć zagnieżdżonych bibliotek rozszerzeń MFC, może pojawić się problem. Załóżmy, że masz bibliotekę rozszerzeń MFC ze standardowymi procedurami i klasami, używanymi we wszystkich projektach. Nazwijmy tą bibliotekę biblioteką "standardową". Możesz także posiadać inną bibliotekę rozszerzeń MFC, zawierającą standardowe procedury i klasy dla określonego projektu, którą nazwiemy biblioteką "projektu". Biblioteka DLL projektu prawie na pewno będzie korzystać ze standardowej biblioteki DLL. Oto "standardowy" przykład zagnieżdżonych bibliotek rozszerzeń DLL.
Gdy spróbujesz łączyć bibliotekę projektu, możesz otrzymać komunikaty błędów programu łączącego , gdyż zarówno biblioteka standardowa jak i biblioteka projektu używają tego samego makra i deklarują tę samą klasę, choć w rzeczywistości biblioteka projektu ma importować tę klasę.
Rozwiązanie tego problemu jest proste. Zamiast używać makra AFX_EXT_CLASS, powinieneś użyć makra biorącego po uwagę projekt, który je wykorzystuje. Na przykład, możesz stworzyć standardowe makro o nazwie COMMON_IMPORT_EXPORT i zdefiniować je jak w przykładzie poniżej. Plik nagłówkowy używany do deklarowania klas i funkcji eksportowanych przez standardową bibliotekę używa makra COMMON_IMPORT_EXPORT zamiast makra AFX_EXT_CLASS. Zaletą jest to, że tylko standardowa biblioteka będzie eksportowała swoje klasy. Wszystkie inne moduły będą je importować. Jednym ze sposobów zdefiniowania symbolu _COMMON_DLL dla standardowej biblioteki jest zdefiniowanie go w oknie dialogowym Project Settings dla tej biblioteki.
#ifdef _COMMON_DLL
#define COMMON_IMPORT_EXPORT_dęcisc(dllexport)
#else
ttdefine COMMON_IMPORT_EXPORdeclspec(dllexport)
#endif
Eksportowanie zasobów
Każda aplikacja MFC posiada połączoną listę obiektów CDynLinkLibrary. Jeśli twój kod w aplikacji MFC wymaga, by MFC samo ładowało zasoby, MFC najpierw próbuje załadować żądany zasób z bieżącego modułu. Do zlokalizowania zasobów modułu MFC wykorzystuje funkcję AfxGetResourceHandle (). Jeśli żądany zasób nie może być zlokalizowany, MFC "przechodzi" przez listę obiektów CDynLinkLibrary w celu zlokalizowania zasobu. Aby wskazać domyślny moduł, od którego MFC rozpocznie wyszukiwanie, musisz użyć funkcji AfxSetResourceHandle () w celu określenia uchwytu HINSTANCE modułu.
Pisanie demonstracyjnej biblioteki DLL rozszerzeń MFC zawierającej dokumenty i widoki
Ponieważ żyjemy w czasach opartego na komponentach, wielokrotnie wykorzystywanego oprogramowania, ta demonstracyjna biblioteka rozszerzeń MFC będzie ilustrować sposób, w jaki możemy zawrzeć w bibliotece DLL obsługę modelu dokument-widok. Załóżmy, że napisałeś dokument i widok, w celu obsługi odczytywania obrazków w plikach .JPG. Być może musisz napisać aplikację, która między innymi musi wyświetlać te obrazki. Normalnie stworzyłbyś klasy dokumentu i widoku obsługujące te pliki wewnątrz swojej głównej aplikacji. Jednak co zrobisz, gdy następnego dnia będziesz potrzebował tych samych funkcji obsługi plików JPG w innej aplikacji? Oczywiście dużo lepszym rozwiązaniem byłoby umieszczenie funkcji niezależnych od aplikacji (takich jak przeglądanie plików JPG) w bibliotece DLL. W ten sposób klient MFC mógłby korzystać z tego kodu łącząc się z DLL-em i wywołując odpowiednie funkcje.
Tworzenie biblioteki rozszerzeń MFC dla dokumentu i widoku obrazka
Najpierw stwórz bibliotekę rozszerzeń MFC. Stwórz projekt o nazwie ImageViewer z następującymi ustawieniami:
MFC AppWizard (dli) w oknie dialogowym New.
Jako typ DLL-a wybierz MFC Extension DLL (using shared MFC DLL)
Kodowanie funkcji biblioteki lmageViewer
Po stworzeniu projektu, za pomocą edytora zasobów stwórz także widok formularza IDD_IMAGE_VIEWER. Ten widok formularza posłuży do wyświetlania obrazków. Następnie użyj ClassWizarda do stworzenia klasy cimageviewerview, wyprowadzonej z klasy CFormView i opartej na wzorcu dialogu IDD_IMAGE_VIEWER. W tym programie nie będziemy dokonywać żadnych zmian w klasie dokumentu, za pomocą ClassWizarda stwórz klasę ClmageViewerDoc, wyprowadzoną Z klasy CDocument.
Po stworzeniu w bibliotece DLL klas dokumentu i widoku, otwórz plik lmageViewerView.h i dopisz poniższe dyrektywy tfinclude. Nasz program demonstracyjny do wyświetlania obrazków będzie korzystał z biblioteki ObjectLibrary. Jej demonstracyjna wersja została zawarta na dołączonej do książki płytce CD-ROM. Plik imageobject.h jest głównym plikiem nagłówkowym tej biblioteki.
#inciude "stdafx.h"
#include "ImageViewerView.h"
Po dołączeniu plików nagłówkowych dodaj do klasy cimageviewerview poniższą zmienną składową. Ten obiekt będzie wszystkim czego potrzebujemy do wyświetlania w widoku plików graficznych.
protected: ClmageObject*m_plmageObjec;
Następnie otwórz plik ImageVieV(>erVie\v.cpp i dokonaj poniższych zmian w konstruktorze i destruktorze:
CImageViewerView::CImageViewerView()
:CFormView(CImageViewerView::IDD)
{
m_plmage0bject = NULL;
}
CImageViewerView::~CImageViewerView(
{
delete m_plmage0bject; m_plmage0bject = NULL;
}
Ponieważ celem tego programu jest umieszczenie dokumentów i widoków w bibliotece DLL a nie zajmowanie się tym, co robią dokumenty i widoki, widok będzie po prostu wyświetlał żądany obrazek. W tym celu, za pomocą ClassWizarda stwórz funkcję OnDraw i) i wypełnij j ą poniższym kodem:
void CImageViewerView::OnDraw(CDC* pDC) t
if (!m_plmage0bject) {
CDocument* pDoc = GetDocument(); ASSERT__VALID(pDoc) ;
CString strPathName = pDoc->GetPathName(); ASSERT(0 < strPathName.GetLengthO ))) ; if (O < strPathName.GetLength())
{
m_plmage0bject = new ClmageObject(strPathName);
}
}
i f (m_plmage0bject)
{
m_p!mageObject->SetPalette(pDC) m_p!mageObject->Draw(pDC);
}
}
Umieszczanie dokumentu i widoku obrazka w klasie
Gdy stworzyłeś i zakodowałeś dokument i widok, skoncentrujmy się na ważnej części naszego programu: umieszczeniu dokumentu i widoku w bibliotece DLL. Najpierw stwórz plik ImageYiewer.h. Ten plik będzie bardzo prosty i będzie służył udostępnieniu klasy ClmageViewer Z pojedynczą funkcją, ClmageViewer : : Init ( ) . Właśnie tę funkcję będzie wywoływać aplikacja w celu użycia dokumentu i widoku, zakodowanych w bibliotece DLL.
Plik ImageYiewer.h powinien wyglądać tak jak poniżej. Gdy stworzysz i zapiszesz ten plik, dyrektywą ttinclude dołącz go także do początku pliku ImageViewer.cpp.
#pragma once
class AFX_EXT_CLASS ClmageViewer {
pubiic : BOOL Init () ;
};
Do pliku ImageVie\ver.cpp dopisz także dwie poniższe dyrektywy ttinclude, za dyrektywą dołączającą plik lmageViewer.h\
#include " ImageViewerView . h" #include "ImageViewerDoc .h"
Dopisz funkcję inito, przedstawioną na listingu 29.3. Funkcja ta po prostu pobiera wskaźnik do obiektu aplikacji. Jest on wymagany do wywołania funkcji AddDocTemplate ( } . Następnie funkcja tworzy obiekt CMultiDocTemplate i wywołując funkcję AddDocTemplate ( ) dodaje go do listy wzorców dokumentów obiektu aplikacji.
Listing 29.3. Funkcja Init() ________________________________________
BOOL ClmageYiewer: :Init ( ) { BOOL bSuccess = FALSE;
CWinApp* pApp = Af KGetApp ( ) ; ASSERT(pApp) ; if (pApp) { CMultiDocTemplate* pDocTemplate;
pDocTemplate = new CMultiDocTemplate ( IDR_IMAGEDOCTYPE , RUNTIME_CLASS (CImageViewerDoc) , RUNTIME_CLASS (CMDIChildWnd) , RUNTIME_CLASS (CImageViewerView) ) ;
ASSERT(pDocTemplate); if (pDocTemplate) {
pApp->AddDocTemplate(pDocTemplate);
bSuccess = TRUE;
}
}
return bSuccess;
}
Aby wzorzec dokumentu mógł zostać utworzony, w pliku zasobów biblioteki DLL, musisz utworzyć tablicę łańcuchów i dodać do niej, pozycję dla wzorca dokumentu. Identyfikatorem tej pozycji powinno być IDR_IMAGEDOCTYPE, zaś wartością
"\nImageV\nImageV\nPliki obrazków (*.jpg, *.gif, *.bmp) \n.jpg;.gif;.bmp;\nlmage Viewer.Document\nImageV Document"
Ostatnim krokiem przed zbudowaniem DLL-a jest dołączenie do projektu biblioteki importowej ImageObject. Nie zapomnij, że w celu uruchomienia aplikacji musisz skopiować do odpowiedniego foldera nie tylko tworzoną bibliotekę DLL, ale także bibliotekę ImageObject z dołączonej do książki płytki CD-ROM.
Budowanie testowej aplikacji dla biblioteki DLL dokumentu i widoku
Gdy już mamy bibliotekę DLL rozszerzeń MFC dla naszego dokumentu i widoku dla typów dokumentów, jakie chcemy otwierać, nadszedł czas, aby stworzyć aplikację testującą tę bibliotekę.
Za pomocą AppWizarda stwórz aplikację MDI o nazwie ImageViewerClient, pamiętając by pozostała włączona opcja Document/View. Następnie otwórz plik ltnageVie\verClient.h i przed deklaracją klasy cimageViewerClientApp dopisz poniższą dyrektywę ttindude. Jest to plik nagłówkowy klasy cimageviewer biblioteki DLL; w związku z tym musisz albo wpisać w tej dyrektywie folder, w którym znajduje się plik ImageViewer.h, albo dopisać folder do listy folderów z plikami nagłówkowymi projektu.
#include "..\ImageViewer\ImageViewer.h" Do klasy aplikacji dopisz poniższą zmienną składową:
protected: ClmageViewer* m_p!mageViewer;
Otwórz plik ImageViewerClient.cpp i dopisz poniższą linię do konstruktora klasy obiektu aplikacji:
m_plmageViewer = new ClmageViewer();
Odszukaj funkcję initinstance (). Zastąp jej cały istniejący kod poniższym kodem:
BOOL ClmageYiewerClientApp::Initinstance()
{
Enable3dControls(); LoadStdProfileSettings();
CMainFrame* pMainFrame = new CMainFrame; if (!pMainFrame->LoadFrame(IDR_MAINFRAME)} return FALSE;
pMainFrame->ShowWindow(m_nCmdShow); pMainFrame->UpdateWindow(); m_pMainWnd = pMainFrame;
ASSERT(m_p!mageViewer != NULL); m_pImageViewer->Init();
return TRUE; }
Na koniec, stwórz destruktor klasy cimageViewerciient w celu usunięcia obiektu
m_pImageViewer:
CImageViewerClientApp::~CImageViewerClientApp()
{ delete m_p!mageViewer;
}
Ostatnią rzeczą, jaką musisz zrobić przed przetestowaniem swojej nowej aplikacji jest dodanie do jej projektu, biblioteki importowej dla biblioteki DLL rozszerzeń MFC.
Rysunek 29.3 przedstawia rezultat uruchomienia aplikacji i otwarcia dwóch plików JPG.
Na podstawie tego programu demonstracyjnego możesz łatwo korzystać z możliwości wyświetlania obrazków, dołączając bibliotekę importową Im agę Viewer do swojego projektu i wywołując pojedynczą, eksportowaną przez nią funkcję C++ (cimageViewer:
: Init (}).
Podsumowanie
W rozdziale tym widzieliśmy jak łatwe jest tworzenie zarówno zwykłych bibliotek DLL, jak i bibliotek rozszerzeń MFC. W przypadku zwykłych bibliotek dowiedziałeś się, jak zarządzać bieżącym stanem modułu, dzięki czemu DLL może odwoływać się do biblioteki MFC w postaci wspólnych plików DLL, a także spróbowałeś stworzyć trzy programy demonstracyjne. Jeden z nich odnosił się do tworzenia zwykłej biblioteki DLL (systemowego haka klawiatury). Kolejny przykład pokazywał, w jaki sposób można dzielić obiekty C++ pomiędzy różne wywołania tej samej biblioteki. Jeśli chodzi o biblioteki rozszerzeń MFC, nauczyłeś się eksportowania z biblioteki całych klas lub ich określonych funkcji składowych, a także umieszczania dokumentów i widoków w osobnych bibliotekach DLL. Jak widać, biblioteki DLL są pomocne w rozwiązywaniu codziennych programistycznych problemów, zaś dzięki wsparciu ze strony Visual C++, tworzenie takich bibliotek staje się bardzo proste.
Wyszukiwarka
Podobne podstrony:
Biblioteka dll
Wyłaczenie bibliotek DLL przyśpiesza kompa
Biblioteki DLL
bibliografia i orzecznictwo praca magisterska
Biblioteki statyczne i dynamiczne (DLL)
próbna 29 marca 2014
000805 29
BIBLIOGRAFIA
Automatyka okrętowa – praca kontrolna 2
cmd=hrk praca&serwis=1
więcej podobnych podstron