151 31 13MB
Polish Pages [529] Year 2010
Spis treści Wstęp . ............................................................................................ 9 Rozdział 1. Bardzo krótkie wprowadzenie do projektowania interfejsu aplikacji przy użyciu biblioteki MFC . ............................................................ 13 Tworzenie projektu . ......................................................................................................... 13 Dodawanie kontrolki . ...................................................................................................... 15 Wiązanie metody z komunikatem domyślnym kontrolki . .............................................. 16 IntelliSense . ..................................................................................................................... 17 Wiązanie komunikatów . .................................................................................................. 18 Metoda MessageBox — trochę filozofii MFC ................................................................ 19 Okno Properties: własności i zdarzenia ........................................................................... 21 Wiązanie zmiennej z kontrolką . ....................................................................................... 22 Usuwanie zbędnych kontrolek . ........................................................................................ 24 Analiza kodu aplikacji . .................................................................................................... 24 Blokowanie zamykania okna dialogowego po naciśnięciu klawisza Enter ..................... 25 Więcej kontrolek . ............................................................................................................ 26 Kolory ............................................................................................................................... 29 Użycie kontrolki ActiveX . ............................................................................................... 31
Rozdział 2. Kontrola stanu systemu . ................................................................ 33 Zamykanie i wstrzymywanie systemu Windows . ........................................................... 33 Funkcja ExitWindowsEx (zamykanie lub ponowne uruchamianie systemu Windows) . ................................ 33 Funkcja InitiateSystemShutdown (zamykanie wybranego komputera w sieci) ........ 41 Hibernacja i wstrzymywanie systemu („usypianie”) za pomocą funkcji SetSystemPowerState . ...................................................................... 46 Blokowanie dostępu do komputera . ................................................................................ 49 Odczytywanie informacji o baterii notebooka ................................................................. 50 Kontrola trybu wyświetlania karty graficznej .................................................................. 52 Pobieranie dostępnych trybów pracy karty graficznej . ............................................. 52 Identyfikowanie bieżącego trybu działania karty graficznej . ................................... 56 Zmiana trybu wyświetlania . ...................................................................................... 57
Rozdział 3. Uruchamianie i kontrolowanie aplikacji oraz ich okien . ................... 59 Uruchamianie, zamykanie i zmiana priorytetu aplikacji . ................................................ 59 Uruchamianie aplikacji za pomocą funkcji WinExec . .............................................. 60 Uruchamianie aplikacji za pomocą ShellExecute . .................................................... 62 Przygotowanie e-maila za pomocą ShellExecute . .................................................... 63 Zmiana priorytetu bieżącej aplikacji ......................................................................... 63
4
Visual C++. Gotowe rozwiązania dla programistów Windows Sprawdzenie priorytetu bieżącej aplikacji ....................................................................... 65 Zmiana priorytetu innej aplikacji . ................................................................................... 66 Zamykanie innej aplikacji . .............................................................................................. 67 Uruchamianie aplikacji za pomocą funkcji CreateProcess . ............................................ 68 Wykrywanie zakończenia działania uruchomionej aplikacji . ......................................... 73 Kontrolowanie ilości instancji aplikacji .......................................................................... 74 Uruchamianie aplikacji w Windows Vista ....................................................................... 75 Uruchamianie procesu jako administrator . ...................................................................... 76 Program z tarczą ............................................................................................................... 78 Kontrolowanie własności okien . ...................................................................................... 79 Lista okien ........................................................................................................................ 79 Okno tylko na wierzchu . ................................................................................................. 83 Ukrywanie okna aplikacji . ............................................................................................... 83 Mrugnij do mnie! . ........................................................................................................... 84 Sygnał dźwiękowy . ......................................................................................................... 84 Numery identyfikacyjne procesu i uchwyt okna . ............................................................ 85 Jak zdobyć identyfikator procesu, znając uchwyt okna? . ............................................... 85 Jak zdobyć uchwyt głównego okna, znając identyfikator procesu? . ............................... 86 Kontrolowanie okna innej aplikacji . ................................................................................ 90 Kontrolowanie grupy okien . ............................................................................................ 94 Okna o dowolnym kształcie . ............................................................................................ 98 Okno w kształcie elipsy . ............................................................................................ 99 Łączenie obszarów. Dodanie ikon z paska tytułu . .................................................... 99 Okno z wizjerem . .................................................................................................... 101 Aby przenosić okno, chwytając za dowolny punkt . ............................................... 102
Rozdział 4. Systemy plików, multimedia i inne funkcje WinAPI . ...................... 105
Pliki i system plików (funkcje powłoki) ........................................................................ 105 Odczytywanie ścieżek do katalogów specjalnych . ................................................. 106 Tworzenie skrótu (.lnk) . .......................................................................................... 107 Odczyt i edycja skrótu .lnk . ..................................................................................... 110 Umieszczenie skrótu na pulpicie ............................................................................. 112 Operacje na plikach i katalogach (funkcje WinAPI) . ............................................. 113 Operacje na plikach i katalogach (funkcje powłoki) . ............................................. 114 Operacje na plikach i katalogach w Windows Vista (interfejs IFileOperation) . .... 116 Jak usunąć plik, umieszczając go w koszu? . .......................................................... 118 Operacje na całym katalogu .................................................................................... 119 Odczytywanie wersji pliku .exe i .dll ...................................................................... 120 Jak dodać nazwę dokumentu do listy ostatnio otwartych dokumentów w menu Start? . ............................................................ 124 Odczytywanie informacji o dysku ................................................................................. 125 Odczytywanie danych . .................................................................................................. 125 Testy ............................................................................................................................... 129 Kontrolka MFC .............................................................................................................. 131 Ikona w obszarze powiadamiania (zasobniku) . ............................................................ 137 Funkcja Shell_NotifyIcon . ............................................................................................ 137 Menu kontekstowe ikony . ............................................................................................. 138 „Dymek” ........................................................................................................................ 140 Multimedia (CD-Audio, MCI) . ...................................................................................... 141 Aby wysunąć lub wsunąć tackę w napędzie CD lub DVD . .................................... 141 Wykrywanie wysunięcia płyty z napędu lub umieszczenia jej w napędzie CD lub DVD . ..................................................................................... 143 Sprawdzanie stanu wybranego napędu CD-Audio . ................................................ 143 Jak zbadać, czy w napędzie jest płyta CD-Audio . .................................................. 144 Kontrola napędu CD-Audio .................................................................................... 145
Spis treści
5 Multimedia (pliki dźwiękowe WAVE) .......................................................................... 147 Asynchroniczne odtwarzanie pliku dźwiękowego ......................................................... 147 Jak wykryć obecność karty dźwiękowej ........................................................................ 147 Kontrola poziomu głośności odtwarzania plików dźwiękowych . ................................. 148 Kontrola poziomu głośności CD-Audio ........................................................................ 150 Inne ................................................................................................................................. 150 Pisanie i malowanie na pulpicie .............................................................................. 150 Czy Windows mówi po polsku? .............................................................................. 153 Jak zablokować uruchamiany automatycznie wygaszacz ekranu? ......................... 153 Zmiana tła pulpitu . .................................................................................................. 154
Rozdział 5. Rejestr systemu Windows . .......................................................... 155 Rejestr ............................................................................................................................. 155 Klasa obsługująca operacje na rejestrze .................................................................. 156 Przechowywanie położenia i rozmiaru okna . ......................................................... 162 Automatyczne uruchamianie aplikacji po zalogowaniu się użytkownika ............... 165 Umieszczanie informacji o zainstalowanym programie (aplet Dodaj/Usuń programy) ............................................................................... 169 Gdzie jest katalog z moimi dokumentami? . ........................................................... 176 Dodawanie pozycji do menu kontekstowego związanego z zarejestrowanym typem pliku ............................................................................ 176 Obsługa rejestru i plików INI za pomocą MFC . ........................................................... 180 Przechowywanie położenia i rozmiaru okna w rejestrze (MFC) ............................ 180 Przechowywanie położenia i rozmiaru okna w pliku INI (MFC) ........................... 182 Skrót internetowy (.url) . .......................................................................................... 183
Rozdział 6. Komunikaty Windows . ................................................................. 185 Pętla główna aplikacji . ................................................................................................... 185 Obsługa komunikatów w procedurze okna (MFC) . ...................................................... 187 Reakcja okna lub kontrolki na konkretny typ komunikatu . .......................................... 187 Lista komunikatów odbieranych przez okno ................................................................. 188 Filtrowanie zdarzeń . ...................................................................................................... 191 Przykład odczytywania informacji dostarczanych przez komunikat . ........................... 191 Lista wszystkich komunikatów odbieranych przez okno i jego kontrolki . ................... 193 Wykrycie zmiany trybu pracy karty graficznej ............................................................. 193 Wysyłanie komunikatów . .............................................................................................. 196 Wysyłanie komunikatów. „Symulowanie” zdarzeń . .............................................. 196 Wysłanie komunikatu uruchamiającego wygaszacz ekranu i detekcja włączenia wygaszacza .......................................................................... 197 Wykorzystanie komunikatów do kontroli innej aplikacji na przykładzie Winampa ..... 197 Przykłady reakcji na komunikaty (MFC) ...................................................................... 198 Blokowanie zamknięcia sesji Windows .................................................................. 198 Wykrycie włożenia do napędu lub wysunięcia z niego płyty CD lub DVD; wykrycie podłączenia do gniazda USB lub odłączenia pamięci Flash ................. 199 Przeciąganie plików między aplikacjami ................................................................ 201 Poprawny sposób blokowania zamykania okna dialogowego po naciśnięciu klawisza Enter ............................................................................... 204 Zmiana aktywnego komponentu za pomocą klawisza Enter . ................................. 205 XKill dla Windows . ................................................................................................. 206 Modyfikowanie menu systemowego formy . .......................................................... 208 Haki ................................................................................................................................ 210 Biblioteka DLL z procedurą haka ........................................................................... 211 Rejestrowanie klawiszy naciskanych na klawiaturze . ............................................ 216
6
Visual C++. Gotowe rozwiązania dla programistów Windows
Rozdział 7. Biblioteki DLL . ............................................................................ 217 Funkcje i klasy w bibliotece DLL .................................................................................. 218 Tworzenie regularnej biblioteki DLL — eksport funkcji . ...................................... 218 Statyczne łączenie bibliotek DLL — import funkcji . ............................................. 220 Dynamiczne ładowanie bibliotek DLL — import funkcji . ..................................... 222 Tworzenie biblioteki DLL z rozszerzeniem MFC — eksport funkcji .................... 224 Tworzenie biblioteki DLL z rozszerzeniem MFC — eksport klasy ....................... 224 Statyczne łączenie biblioteki DLL — import klasy . ............................................... 226 Tworzenie biblioteki DLL z rozszerzeniem MFC — eksport klasy. Modyfikacja dla dynamicznie ładowanych bibliotek ........................ 227 Dynamiczne łączenie bibliotek DLL — import klasy ................................................... 228 Powiadamianie biblioteki o jej załadowaniu lub usunięciu z pamięci . ......................... 230 Zasoby w bibliotece DLL . ............................................................................................. 232 Łańcuchy w bibliotece DLL . ......................................................................................... 232 Bitmapa w bibliotece DLL . ........................................................................................... 234 Okno dialogowe w bibliotece DLL . .............................................................................. 237 Tworzenie apletu panelu sterowania wyświetlającego informacje o dyskach .............. 240
Rozdział 8. Automatyzacja i inne technologie bazujące na COM . .................... 249 Technologia COM . ........................................................................................................ 249 Osadzanie obiektów OLE2 . ........................................................................................... 250 Statyczne osadzanie obiektu . ......................................................................................... 251 Kończenie edycji dokumentu. Łączenie menu aplikacji klienckiej i serwera OLE . ............ 252 Wykrywanie niezakończonej edycji podczas zamykania programu . ............................ 254 Inicjowanie edycji osadzonego obiektu z poziomu kodu . ........................................ 255 Dynamiczne osadzanie obiektu . .................................................................................... 256 Automatyzacja . .............................................................................................................. 258 Typ VARIANT i klasa COleVariant ....................................................................... 258 Łączenie z serwerem automatyzacji aplikacji Excel . ............................................. 259 Uruchamianie aplikacji Excel za pośrednictwem mechanizmu automatyzacji ....... 265 Uruchamianie procedur serwera automatyzacji . ..................................................... 266 Eksplorowanie danych w arkuszu kalkulacyjnym . ................................................. 266 Korzystanie z okien dialogowych serwera automatyzacji. Zapisywanie danych w pliku ................................................................................. 268 Zapisywanie danych z wykorzystaniem okna dialogowego aplikacji klienckiej .... 268 Edycja danych w komórkach Excela ....................................................................... 269 Korzystanie z funkcji matematycznych i statystycznych Excela ............................ 271 Konwersja skoroszytu Excela do pliku HTML . ..................................................... 273 Uruchamianie aplikacji Microsoft Word i tworzenie nowego dokumentu lub otwieranie istniejącego ................................................................................... 276 Wywoływanie funkcji Worda na przykładzie sprawdzania pisowni i drukowania . ........................................................................................................ 278 Wstawianie tekstu do bieżącego dokumentu Worda . ............................................. 278 Zapisywanie bieżącego dokumentu Worda . ........................................................... 279 Zaznaczanie i kopiowanie całego tekstu dokumentu Worda do schowka .............. 280 Kopiowanie zawartości dokumentu Worda do komponentu CRichEditCtrl bez użycia schowka (z pominięciem formatowania tekstu) . ................................ 280 Formatowanie zaznaczonego fragmentu tekstu w dokumencie Worda .................. 281 Serwer automatyzacji OLE przeglądarki Internet Explorer . ................................... 282 Własny serwer automatyzacji . ....................................................................................... 284 Projektowanie serwera automatyzacji . .......................................................................... 284 Testowanie serwera automatyzacji . ............................................................................... 287 ActiveX .......................................................................................................................... 289 Korzystanie z kontrolek ActiveX ............................................................................ 289
Spis treści
7
Rozdział 9. Sieci komputerowe . ..................................................................... 293 Struktura sieci komputerowych ..................................................................................... 293 Lista połączeń sieciowych i diagnoza sieci .................................................................... 296 Aktywne połączenia TCP . ............................................................................................. 296 Aktywne gniazda UDP . ................................................................................................. 299 Sprawdzanie konfiguracji interfejsów sieciowych ........................................................ 300 Ping ................................................................................................................................ 302 Sprawdzanie adresu IP hosta (funkcja DnsQuery) ........................................................ 305 Sprawdzanie adresu IP i nazwy hosta (funkcje gethostbyaddr i gethostbyname) ... 307 Odczytywanie adresów MAC z tablicy ARP ................................................................ 311 Tablica ARP — wiązanie wpisów z interfejsem ........................................................... 314 Protokoły TCP i UDP . ................................................................................................... 316 Tworzenie i zamykanie gniazda — klasa bazowa ......................................................... 316 Klasa implementująca serwer TCP . ............................................................................... 317 Klasa implementująca serwer UDP . .............................................................................. 319 Aplikacja działająca jako serwer TCP i UDP ................................................................ 320 Klasa implementująca klienta TCP . .............................................................................. 322 Klasa implementująca klienta UDP . .............................................................................. 324 Aplikacja działająca jako klient TCP i UDP ................................................................. 325 Serwer TCP działający asynchronicznie (funkcja WSAAsyncSelect) . ......................... 327 Serwer TCP — użycie klasy CSocket . .......................................................................... 330 Klient TCP — użycie klasy CSocket . ........................................................................... 334 Inne protokoły sieciowe . ................................................................................................ 336 Protokół FTP (przesyłanie plików) . ............................................................................... 336 Protokół SMTP (poczta elektroniczna) ......................................................................... 343 Inne ................................................................................................................................. 350 Aby pobrać plik z Internetu ..................................................................................... 350 Mapowanie dysków sieciowych .............................................................................. 350
Rozdział 10. Wątki . ........................................................................................ 353 Tworzenie wątków . ....................................................................................................... 353 Tworzenie wątku . .......................................................................................................... 354 Tworzenie wątku roboczego za pomocą MFC .............................................................. 355 Usypianie wątków (funkcja Sleep) . ............................................................................... 357 Czas wykonywania wątków . ......................................................................................... 359 Wstrzymywanie i wznawianie wątków ......................................................................... 361 Kończenie wątku . .......................................................................................................... 362 Funkcja TerminateThread . ............................................................................................ 362 Funkcja ExitThread . ...................................................................................................... 362 Funkcje TerminateProcess i ExitProcess ....................................................................... 363 Priorytety wątków . ........................................................................................................ 364 Priorytety procesu . ........................................................................................................ 365 Statyczna kontrola priorytetów wątków ........................................................................ 369 Dynamiczna kontrola priorytetów wątków .................................................................... 370 Flaga CREATE_SUSPENDED . .................................................................................... 371 Wątek działający z ukrycia . ........................................................................................... 373 Programowanie koligacji . .............................................................................................. 374 Informacja o liczbie procesorów (funkcja GetSystemInfo) . ......................................... 374 Przypisywanie procesu do procesora . ............................................................................ 375 Odczytywanie maski koligacji procesu . ........................................................................ 377 Programowanie koligacji wątku . ................................................................................... 378 Wątki interfejsu użytkownika . ....................................................................................... 380 Tworzenie wątku UI . ............................................................................................... 380 Wykonywanie zadań w tle ...................................................................................... 383 Uwolnienie głównego okna aplikacji ...................................................................... 385
8
Visual C++. Gotowe rozwiązania dla programistów Windows Synchronizacja wątków . ................................................................................................ 386 Wyzwalanie wątków za pomocą zdarzeń ................................................................ 387 Sekcje krytyczne . .................................................................................................... 390 Semafory (zliczanie użycia zasobów) ..................................................................... 393 Muteksy . .................................................................................................................. 398
Rozdział 11. Programowanie współbieżne z OpenMP . ....................................... 403 Blok równoległy . ........................................................................................................... 405 Dynamiczne tworzenie wątków, zmienne środowiskowe i funkcje biblioteczne ......... 407 Zrównoleglenie pętli . ..................................................................................................... 408 Sposoby podziału iteracji między wątki ........................................................................ 417 Redukcja i bloki krytyczne . ........................................................................................... 420 Sekcje, czyli współbieżność zadań ................................................................................ 422 Zmienne prywatne i zmienne wspólne .......................................................................... 427 Synchronizacja wątków . ................................................................................................ 429
Rozdział 12. Biblioteka Threading Building Blocks . .......................................... 431 Instalacja . ....................................................................................................................... 432 Inicjalizacja biblioteki . .................................................................................................. 434 Zrównoleglanie pętli . .................................................................................................... 436 Rozmiar ziarna i podział przestrzeni danych . ............................................................... 441 Pomiar czasu wykonywania kodu . ................................................................................ 443 Równoległa redukcja . .................................................................................................... 444 Łączenie zrównoleglania pętli z redukcją ...................................................................... 446 Równoległe przetwarzanie potoków . ............................................................................ 447 Wykorzystanie parallel_do . ........................................................................................... 451 Własne przestrzenie danych . ......................................................................................... 454 Równoległe sortowanie . ................................................................................................ 457 Równoległe obliczanie prefiksu . ................................................................................... 458 Skalowalne alokatory pamięci . ...................................................................................... 460 Kontenery . ..................................................................................................................... 462 Wykorzystanie concurrent_vector . ................................................................................ 465 Wykorzystanie concurrent_hash_map . .......................................................................... 467 Wzajemne wykluczanie i operacje atomowe ................................................................. 468 Wykorzystanie blokad . .................................................................................................. 470 Łączenie TBB z OpenMP . ............................................................................................. 472 Bezpośrednie korzystanie z planisty .............................................................................. 473 Tworzenie zadań za pomocą metody blokowania . ................................................. 474 Tworzenie zadań za pomocą metody kontynuacji . ................................................. 477
Dodatek A CUDA . ........................................................................................ 481 Skorowidz . ................................................................................. 507
Wstęp Książkę o środowisku programistycznym Visual C++ zacznę od pochwały jego konkurencji. Programowania dla Windows uczyłem się bowiem, używając Borland C++Builder. Dołączona do tego środowiska biblioteka VCL jest niezwykle intuicyjna w użyciu, a przy tym bogata w najróżniejszego rodzaju komponenty, z kolei sam kompilator — szybki i sprawny. Bez wątpienia środowisko C++Builder znakomicie ułatwia pierwsze próby tworzenia aplikacji Windows. Firma Borland przestała już wprawdzie istnieć (w maju tego roku kupiła ją Micro Focus, a kilka miesięcy wcześniej firmę-córkę CodeGear odpowiedzialną za rozwój środowisk programistycznych przejęła firma Embarcadero), ale ja nadal używam C++Buildera 6, jeżeli tylko muszę przygotować coś szybko i jak najmniejszym wysiłkiem. Nie bez znaczenia są również niewielkie wymagania sprzętowe tej wersji środowiska, czego nie można już powiedzieć o nowszych jego wersjach. Jednak o ile C++Builder jest szeroko rozpowszechniony wśród nauczycieli akademickich i ich studentów, to konkurencyjny Microsoft Visual C++ zdecydowanie króluje na rynku w pełni profesjonalnej produkcji oprogramowania. Zaskakujące jest zatem, że ta popularność zupełnie nie idzie w parze z ilością polskojęzycznej literatury na temat środowiska Microsoftu, szczególnie jeżeli porównamy ją z ilością pozycji na temat środowisk C++Builder i Delphi. Stąd pomysł, aby „skonwertować na Visual C++”, a jednocześnie rozwinąć i zaktualizować moją wcześniejszą książkę pt. C++Builder 2006. 222 gotowe rozwiązania. Podstawą tego pomysłu był fakt, że tamta książka, zresztą podobnie jak obecna, w dużej mierze skupiała się nie na samym środowisku, a na funkcjach i technologiach systemu Windows, po które każdy programista aplikacji Windows prędzej czy później musi sięgnąć. Korzystanie z bibliotek komponentów jest intuicyjne i w zasadzie można się go nauczyć samemu. Problem pojawia się natomiast wtedy, gdy chcemy zrobić coś, do czego owe komponenty już nie wystarczają. W tym właśnie momencie ma pomóc Czytelnikowi książka, którą trzyma w ręku. Dokładnie na ten problem natknęliśmy się podczas „konwersji” niektórych projektów C++Builder, w których korzystaliśmy z VCL, do Visual C++. Kontrolki MFC mogą zastąpić komponenty VCL tylko w niewielkim stopniu. W większości przypadków konieczne jest sięgnięcie do oryginalnych funkcji interfejsu programistycznego Windows, czyli WinAPI. Jak zapowiedziałem, to one są głównymi bohaterami tej książki.
10
Visual C++. Gotowe rozwiązania dla programistów Windows
Opis i przykłady praktycznego użycia funkcji WinAPI uzupełniono w niektórych przypadkach opisem funkcji biblioteki MFC, którą zasadniczo można traktować jako obiektowe opakowanie WinAPI. Muszę też wyznać, że w niektórych rozdziałach używamy łańcucha zdefiniowanego w MFC (chodzi o typ CString), aby uprościć kod i ułatwić jego interpretację. Ale WinAPI to nie cała treść książki. W porównaniu do wcześniejszej edycji — dla C++Buildera — pojawiło się w niej sporo nowych rozdziałów. Poza rozdziałem wprowadzającym do budowania interfejsu za pomocą kontrolek MFC oraz rozdziałem dotyczącym komunikatów bardzo ważnym nowym elementem książki jest grupa rozdziałów dotyczących programowania współbieżnego, co wymuszone zostało przez rozwój wielordzeniowych procesorów. Prowadzimy zatem Czytelnika od omówienia wątków, jakie daje nam WinAPI, poprzez OpenMP (technologia zaimplementowana już w Visual C++ 2005) i narzędzia Intela, po wprowadzenie do technologii CUDA, która pozwala wykorzystać procesory na kartach graficznych do dowolnych obliczeń. Wszystkie te technologie mogą być użyte w Visual C++. Ważną technologią związaną z obliczeniami współbieżnymi, która jednak nie jest omawiana w książce, jest MPI. MPI jest jednak przede wszystkim technologią pozwalającą zrównoleglić obliczenia wykonywane na wielu komputerach połączonych w tzw. klaster. Siłą rzeczy nie jest to rozwiązanie zbyt popularne poza wyspecjalizowanymi ośrodkami obliczeniowymi. Innym ważnym powodem, dla którego zdecydowaliśmy się na napisanie tej pozycji, jest dominacja w księgarniach książek dotyczących tworzenia aplikacji dla platformy .NET (w pewnym stopniu mamy też w niej pewien udział), podczas gdy duże programy, takie jak narzędzia biurowe, wszelkiego typu gry czy narzędzia użytkowe, nadal tworzone są dla tradycyjnej dla Windows platformy Win32. Nawet Microsoft Office nadal działa na tej platformie. Chęć uzupełnienia luki w literaturze poświęconej tworzeniu oprogramowania dla platformy Win32 za pomocą narzędzi Microsoft była zatem naszą kolejną motywacją. Choć podczas pisania książki wszyscy autorzy korzystali z Visual C++ 2008 lub Visual C++ 2010, to temat książki uniezależnia ją w zasadzie od konkretnej wersji tego środowiska programistycznego. Większym ograniczeniem jest wersja Windows, jaką dysponujemy i jaką wspiera konkretne środowisko. Jednak z uwagi na przenośność aplikacji stosowane są zazwyczaj te funkcje WinAPI, które obecne są w Windows już od dłuższego czasu, a przynajmniej od wersji Windows 2000 i Windows XP. Dlatego nowym funkcjom Windows Vista i Windows 7 poświęcamy stosunkowo niewiele miejsca. Wspomniany w pierwszym akapicie interfejs programistyczny systemu Windows (ang. Windows Application Programming Interface), określany zwykle jako WinAPI, jest zbiorem funkcji z kilku bibliotek DLL dołączanych do Windows, które zapewniają niemal nieograniczone możliwości: od wyłączenia komputera, jego hibernacji, zmiany rozdzielczości ekranu, poprzez uruchamianie aplikacji i kontrolę ich stanu, aż po zagadnienia związane z operacjami na plikach. Tym tematom poświęcone są rozdziały od 2. do 5. Osobnego potraktowania wymagają technologie, do których obsługi potrzebne są funkcje WinAPI: mechanizm komunikatów Windows, automatyzacja i technologie COM, tworzenie i wykorzystywanie bibliotek DLL oraz programowanie sieciowe z wykorzystaniem biblioteki WinSock. Ich opis znajdzie Czytelnik w rozdziałach od 6. do 9.
Wstęp
11
Wszystkie kolejne rozdziały związane są z programowaniem współbieżnym: od tworzenia i kontroli wątków (rozdział 10.), poprzez OpenMP i TBB (rozdziały 11. i 12.), aż do CUDA (dodatek A). Kilka słów we wstępie należy poświęcić ewolucji WinAPI. Nie oznacza to, że zamierzam opowiadać tutaj o historii rozwoju tego interfejsu programistycznego w kolejnych wersjach systemu Windows. Chodzi mi o bardziej praktyczny aspekt, a mianowicie o to, czy można mieć pewność, że aplikacja napisana i przetestowana w Windows XP lub Windows Vista zadziała w Windows 95 lub Windows 7. Problem zgodności starszej wersji WinAPI z nowszą nie musi powodować większych obaw. Inaczej jest w przypadku odwrotnym. WinAPI w Windows 2000, a potem w Windows XP, Vista i Windows 7 jest coraz bogatsze. Zawiera znacznie więcej funkcji niż w pierwszych wersjach dołączanych do Windows NT i 95. Wiele interesujących funkcji, na przykład cały pakiet kontroli wątków, zostało dodanych w momencie połączenia linii NT/2000 i 95/98/Me, zatem przy każdym użyciu funkcji WinAPI należy sprawdzić, najlepiej w dokumentacji MSDN, od jakiej wersji jest ona dostępna. W tej książce wersje systemu Windows, które wspierają użytą przez nas funkcję, zazwyczaj zostaną podane w przypisach. W większości rozdziałów opisane funkcje pomocnicze zostały napisane w taki sposób, aby mogły być użyte w dowolnym projekcie (korzystają tylko z WinAPI). Przykłady ich użycia są jednak zazwyczaj umieszczone w projektach aplikacji MFC z oknem dialogowym. Nie są to projekty skomplikowane, kładziemy właśnie nacisk na ich prostotę i przejrzystość. Traktujemy je wyłącznie jako ilustracje użycia funkcji WinAPI lub funkcji, które na bazie funkcji WinAPI przygotowaliśmy. W szczególności nie dbamy o tak ważny dla programistów MFC rozdział modelu (danych aplikacji) i widoku (prezentujących dane kontrolek). We wstępie należy chyba również wspomnieć o tym, że Visual C++ oprócz projektowania aplikacji dla platformy Win32 umożliwia także programowanie dla platformy .NET. Jednak uważamy, że bardziej racjonalny w takim przypadku jest wybór języka C#. Niemniej taka możliwość istnieje, choć nie poświęcimy jej w tej książce już ani słowa więcej. Jacek Matulewski
12
Visual C++. Gotowe rozwiązania dla programistów Windows
Rozdział 1.
Bardzo krótkie wprowadzenie do projektowania interfejsu aplikacji przy użyciu biblioteki MFC Choć biblioteka kontrolek MFC nie jest tematem tej książki, będzie się w niej pojawiać wielokrotnie. Wykorzystywać ją będziemy do budowania interfejsów prostych aplikacji ilustrujących omawiane funkcje WinAPI. Stąd to krótkie wprowadzenie, w którym przedstawię przykład aplikacji opartej na oknie dialogowym MFC. Nie ma ono oczywiście aspiracji do wyczerpania tematu, raczej ma pozwolić Czytelnikowi zorientować się, z czym będzie miał do czynienia w dalszych rozdziałach.
Tworzenie projektu Od czego zaczniemy? Proponuję utworzenie projektu prostej aplikacji, której okno pozbawione będzie wszelkich dodatków typu menu, paska narzędzi itp. Do okna aplikacji dodamy tylko jeden przycisk, którego kliknięcie pokaże komunikat o treści „Hello World!”. Wszystko to uzyskamy minimalnym nakładem pracy. 1. Aby utworzyć nowy projekt, naciskamy kombinację klawiszy Ctrl+Shift+N
lub z menu File, New, wybieramy pozycję Project. Następnie: a) W panelu Project types wybieramy Other languages, Visual C++, MFC. b) Wówczas w panelu Templates wybieramy MFC Application.
14
Visual C++. Gotowe rozwiązania dla programistów Windows c) W polu edycyjnym Name wpisujemy nazwę projektu, np. Hello. d) Klikamy OK. 2. Pojawi się kreator aplikacji MFC Application Wizard. Klikamy Next >. 3. W drugim kroku kreatora wybieramy (rysunek 1.1): a) Application type: Dialog based (to ważne ustawienie, proszę go nie przeoczyć!). b) Use of MFC: Use MFC in a static library. c) Zaznaczona niech zostanie opcja Use Unicode libraries.
Rysunek 1.1. Kreator aplikacji MFC w VC++ 2008
4. W trzecim kroku (User Interfaces Features): a) Usuwamy zaznaczenie przy pozycji About box. b) Natomiast pozostawiamy zaznaczone System menu. c) Zaznaczamy także Minimize box i Maximize box. d) Tytuł okna (pole Dialog title) ustawiamy na Hello World!. 5. Klikamy Finish.
Typ aplikacji, jaki wybraliśmy (punkt 3a), ma zasadnicze znaczenie dla wygody programowania. Ponieważ VC++ wyposażony jest tylko w narzędzia wizualne przeznaczone do projektowania okien dialogowych (rysunek 1.2), wybranie innego typu aplikacji pozbawiłoby nas możliwości projektowania interfejsu aplikacji przy użyciu myszy.
Rozdział 1..
+
1.5
Bardzo krótkie wprowadzenie do projektowania interfejsu aplikacji
lllllOo Hello - Mi cro s oftVisua l Stu d io Eile
&dit
! tfil •
Yi�
E.roject
I
,l!uild
tJ li, l'SetFileName(L"cmd.exe"); cp->SetWindowStyle(swNormal); cp->SetPriorities(prIdle); cp->SetDefaultPosition(false); cp->SetLeft(100); cp->SetTop(10); cp->SetWidth(800); cp->SetHeight(600); cp->SetDefaultSize(false);
Rozdział 3. ♦ Uruchamianie i kontrolowanie aplikacji oraz ich okien
73
if(cp->Execute()) { edit1.SetWindowTextW(L"Nazwa pliku "+cp->GetFileName()); CString opis; opis.AppendFormat(L"Uchwyt Procesu %d, Identyfikator procesu %d.",cp´>GetProcessInformation().hProcess,cp->GetProcessInformation().dwProcessId); edit2.SetWindowTextW(opis); CString opis2; opis2.AppendFormat(L"Uchwyt wątku %d, Identyfikator wątku %d.",cp´>GetProcessInformation().hThread,cp->GetProcessInformation().dwThreadId); edit3.SetWindowTextW(opis2);
}
} else MessageBox(L"Uruchomienie aplikacji "+cp->GetFileName()+L" nie powiodlo sie"); delete cp;
Niektóre aplikacje nie reagują na ustawianie pozycji lub rozmiaru okna. Możemy łatwo odgadnąć dlaczego — wystarczy, że w ich konstruktorach znajdują się polecenia ustalające wielkość lub położenie okna. Czytelnicy posiadający komputery wieloprocesorowe, co staje się coraz powszechniejsze, mogą także zwrócić uwagę na dostępną w Windows Vista, XP, 2000 funkcję SetProcessAffinityMask, pozwalającą na wybór procesorów, na których ma działać uruchamiany proces. Więcej informacji na jej temat znajdzie Czytelnik w rozdziale 10.
Wykrywanie zakończenia działania uruchomionej aplikacji Czasem zachodzi potrzeba, aby z poziomu naszej aplikacji uruchomić inną aplikację, mającą wykonać pewne operacje, których zakończenie jest warunkiem kontynuowania działania naszej aplikacji. Aby umożliwić taki sposób uruchamiania aplikacji, do klasy CCreateProcess dodamy metodę ExecuteAndWait, która tworzy proces nowej aplikacji i czeka na jej zakończenie. Zatem w przeciwieństwie do Execute działanie tej metody i powrót do wątku głównego nie nastąpią tuż po inicjacji nowej aplikacji, lecz dopiero po wykryciu jej zakończenia. Aby niepotrzebnie nie powtarzać kodu, do uruchomienia procesu wykorzystamy oczywiście metodę Execute. Do wstrzymania bieżącej aplikacji, aż do zamknięcia uruchomionego procesu, wykorzystujemy funkcję Wait ´ForSingleObject. Jej argumenty to uchwyt procesu, na który czekamy, i czas oczekiwania na sygnał jego zakończenia. 1. Do nagłówka UruchamianieAplikacji.h, a dokładniej do klasy CCreateProcess,
dodajemy deklarację nowej metody publicznej: bool ExecuteAndWait();
2. Do pliku z kodem źródłowym UruchamianieAplikacji.cpp dodajemy definicję
tej metody (listing 3.12).
74
Visual C++. Gotowe rozwiązania dla programistów Windows
Listing 3.12. Metoda ta zakończy się dopiero po zakończeniu uruchomionej przez nią aplikacji bool CCreateProcess::ExecuteAndWait() { bool wynik = Execute(); if(!wynik) return false; WaitForSingleObject(processInformation.hProcess,INFINITE); memset(&processInformation,0,sizeof(PROCESS_INFORMATION)); return wynik; }
Działanie nowej metody zilustrujemy w projekcie testującym w taki sposób, że na czas działania uruchomionej aplikacji okno bieżącej aplikacji będzie minimalizowane. Moim ulubionym programem testującym był zawsze pasjans, jednak ponieważ sposób jego uruchamiania zależy od wersji systemu, do testowania aplikacji użyjemy kalkulatora (plik calc.exe). 1. Do okna dialogowego z poprzedniego projektu dodajemy przycisk z etykietą
Uruchom i minimalizuj do zakończenia. 2. Tworzymy domyślną metodę zdarzeniową nowego przycisku i umieszczamy
w niej kod z listingu 3.13. Listing 3.13. Minimalizujemy bieżące okno do momentu zamknięcia uruchomionej aplikacji void CAppDlg::OnBnClickedButton2() { CCreateProcess * cp = new CCreateProcess(); cp->SetFileName(L"calc.exe"); this->ShowWindow(SW_MINIMIZE); cp->ExecuteAndWait(); this->ShowWindow(SW_SHOWDEFAULT); delete cp; }
Zminimalizowanie okna jest w powyższym przypadku stanem, na który użytkownik nie będzie mógł wpłynąć, dopóki uruchomiony kalkulator nie zostanie zamknięty. Dlaczego? Otóż dlatego, że wątek aplikacji przekazany do metody ExecuteAndWait utkwił tam aż do momentu zakończenia uruchomionego programu i aplikacja nie obsługuje kolejki komunikatów. Można oczywiście uruchomić polecenia z powyższego listingu w osobnym wątku — wówczas oczekiwanie na zamknięcie kalkulatora nie będzie miało wpływu na interakcję okna naszej aplikacji z użytkownikiem. Zależy to jedynie od tego, jakiego zachowania oczekujemy od aplikacji.
Kontrolowanie ilości instancji aplikacji Dość popularnym zagadnieniem związanym z uruchamianiem aplikacji jest ograniczenie ich instancji. Zazwyczaj chodzi o to, żeby po uruchomieniu jednego egzemplarza programu i w trakcie jego działania nie można było uruchomić ich więcej. Można to zrobić na kilka sposobów. Tu skorzystamy z funkcji tworzącej muteks CreateMutex9. 9
Wyprzedzamy w ten sposób zagadnienia omówione w rozdziale 10. Tam pojęcie muteksu zostanie bardziej szczegółowo wyjaśnione i zilustrowane.
Rozdział 3. ♦ Uruchamianie i kontrolowanie aplikacji oraz ich okien
75
Będziemy próbowali utworzyć muteks przy każdym uruchomieniu aplikacji. W przypadku gdy aplikacja jest tworzona po raz pierwszy, utworzenie muteksu powiedzie się. Natomiast w przypadku kolejnej instancji utworzenie muteksu się nie uda. Będzie to wyraźnym sygnałem, że jedna instancja jest już uruchomiona i należy czym prędzej zakończyć działanie następnej. 1. Tworzymy nowy projekt MFC Application z oknem dialogowym. 2. Do metody InitInstance klasy okna dodajemy kod z listingu 3.14. Listing 3.14. Ograniczamy instancję naszej aplikacji do jednej BOOL CAppApp::InitInstance() { HANDLE hMutex=CreateMutex(NULL,TRUE,L"ToPowinnaBycJakasBardzoUnikalnaNazwaMutexu"); bool mutexIstnieje = (GetLastError() == ERROR_ALREADY_EXISTS); if(hMutex != NULL) ReleaseMutex(hMutex); if(mutexIstnieje) { MessageBox(0,L"Zablokowano uruchomienie kolejnej instancji aplikacji",L"",MB_OK); return FALSE; } // reszta kodu }
Kompilujemy kod. Spróbujmy teraz uruchomić dwie instancje aplikacji bez debugowania (Ctrl+F5). Pierwsza próba powiedzie się. Zamiast drugiego okna powinniśmy zobaczyć komunikat o treści „Zablokowano uruchomienie kolejnej instancji aplikacji”.
Uruchamianie aplikacji w Windows Vista W tej książce zwykle unikam funkcji wprowadzonych w systemie Windows Vista. Nie dlatego, że mam do nich jakąś awersję, a po prostu dlatego, że system ten nie stał się na tyle popularny, aby pisanie programów wyłącznie dla niego było opłacalne. A tak właśnie będzie, jeżeli użyjemy jakiejkolwiek funkcji, która pojawiła się w WinAPI tego systemu. Jest jednak mechanizm wprowadzony do Windows Vista, którego nie możemy zignorować. Mam na myśli UAC (ang. User Account Control) — kontrolę konta użytkownika, czyli mechanizm, który zawiesza aplikację korzystającą ze zwiększonych uprawnień do momentu autoryzacji przez użytkownika. A to ostatnie objawia się w irytujących tak wiele osób monitach. Działanie UAC ma ustrzec przed problemami szczególnie te osoby, które na co dzień używają konta administratora.
76
Visual C++. Gotowe rozwiązania dla programistów Windows
Z punktu widzenia programisty pojawia się ciekawe pytanie. W jakim kontekście pojawi się taki monit, jeżeli będziemy uruchamiać aplikację, w szczególności jeżeli będziemy uruchamiać aplikację z poziomu innej aplikacji? Innym objawem tego samego mechanizmu jest dostęp do chronionych obszarów dysku. Jeżeli działamy w koncie zwykłego użytkownika, to próba skopiowania pliku np. do katalogu systemowego C:\WINDOWS nie powiedzie się. Możemy jednak proces kopiowania uruchomić jako administrator. Jak to zrobić z poziomu kodu? To będzie pierwszym zagadnieniem, które zbadamy. Poniższe zagadnienia zostały wstępnie opracowane przez Dawida Urbańskiego na podstawie książki Writing Secure Code For Windows Vista napisanej przez Michaela Howarda i Davida LeBlanca (Microsoft Press, 2007).
Uruchamianie procesu jako administrator Po pierwsze, zwróćmy uwagę, że w Windows XP w menu kontekstowym programów (w Eksploatorze Windows lub wprost na pulpicie) znajduje się pozycja Uruchom jako… pozwalająca uruchomić program jako inny niż bieżący użytkownik. W systemie Vista z menu kontekstowego zniknęło polecenie Uruchom jako…, a zamiast niego pojawiło się polecenie Uruchom jako administrator. Idea jest jednak generalnie taka sama. Z konsoli ta sama funkcjonalność dostępna jest dzięki poleceniu runas. Jeżeli na przykład chcemy uruchomić linię komend z uprawnieniami administratora, wpisujemy polecenie: runas /noprofile /user:Administrator cmd
Przełącznik noprofile powoduje, że nie jest przeprowadzane pełne ładowanie profilu użytkownika, co przyspiesza uruchamianie programu. Analogicznie jeżeli z poziomu linii komend chcemy uruchomić przeglądarkę z podwyższonymi uprawnieniami administratora (np. aby skorzystać ze skanera antywirusowego online) lub wręcz przeciwnie, to z konta jacek o zmniejszonych uprawnieniach wpisujemy: runas /noprofile /user:jacek "C:\Program Files\Internet Explorer\iexplore.exe"
W ten sposób nie można niestety uruchamiać komend interpretera poleceń, więc nie można za runas wstawić np. polecenia copy. Polecenie runas odwołuje się do jednej z funkcji WinAPI: CreateProcessAsUser lub CreateProcessWithLogon. Pierwsza uruchamia proces z tokenem bezpieczeństwa innego użytkownika, a więc także z jego uprawnieniami (zwiększonymi lub zmniejszonymi). Druga natomiast wymaga interaktywnego zalogowania (podania hasła) innego użytkownika przed uruchomieniem nowego procesu. Funkcja CreateProcessAsUser różni się od poznanej już przez nas funkcji CreateProcess tylko jednym (pierwszym) argumentem, który jest uchwytem do tokenu innego użytkownika (por. proces pobierania tokenu dla bieżącego użytkownika opisany w poprzednim rozdziale). My jednak pójdziemy na skróty i nie będziemy korzystać z żadnej z tych funkcji. Zamiast tego uruchomimy proces za pomocą funkcji ShellExecute, w której jednak zastąpimy polecenie open poleceniem runas. To wystarczy, aby proces uzyskał pełne
Rozdział 3. ♦ Uruchamianie i kontrolowanie aplikacji oraz ich okien
77
uprawnienia użytkownika będącego administratorem lub spróbował uzyskać uprawnienia administratora. Wczytajmy projekt rozwijany na początku rozdziału (ten, w którym wykonywany plik wybierany był za pomocą okna dialogowego), a następnie do okna dodajmy trzeci przycisk z opisem Uruchom jako administrator. Stwórzmy jego domyślną metodę zdarzeniową i umieśćmy w niej kod z listingu 3.15. Listing 3.15. Wywołanie funkcji ShellExecute, w której polecenie runas zastąpiło polecenie open void CAppDlg::OnBnClickedButton4() { CString editText; edit1.GetWindowTextW(editText); ShellExecute(this->GetSafeHwnd(),L"runas",editText,L"",L"",SW_NORMAL); }
Wykonanie tej metody spowoduje, że użytkownik mający uprawnienia administratora zobaczy komunikat, który irytuje osoby korzystające z Visty (rysunek 3.4, lewy), a który żąda poświadczenia, że proces może być uruchomiony z pełnymi uprawnieniami. Jeżeli bieżący użytkownik ma mniejsze uprawnienia, mechanizm UAC wyświetli okno zawierające listę użytkowników-administratorów i będzie żądać hasła któregoś z nich (rysunek 3.4, prawy). Okno może być bardziej rozbudowane, gdy komputer jest członkiem domeny — wówczas można użyć poświadczeń konta domenowego, jeżeli komputer wyposażony jest w urządzenia służące do autoryzacji — również one mogą być uwzględnione w oknie komunikatu.
Rysunek 3.4. Komunikat UAC zależy od tego, czy użytkownik jest administratorem, czy użytkownikiem o ograniczonych uprawnieniach
78
Visual C++. Gotowe rozwiązania dla programistów Windows
Program z tarczą Programy, które wymagają uprawnień administratora, powinny być specjalnie oznaczone. Na ich ikonie powinien znajdować się symbol czterokolorowej tarczy (rysunek 3.6). To sygnalizuje użytkownikowi, z jakim programem ma do czynienia, i uprzedza go, że może spodziewać się monitów UAC. Dobrym przykładem takiego programu jest z pewnością instalator, który może wymagać dostępu do zasobów systemowych i katalogów wszystkich użytkowników, oczywiście wszystkie programy dbające o bezpieczeństwo komputera, czy chociażby program, który chce umieścić jakiś plik w katalogu systemowym lub zapis w rejestrze poza kluczem użytkownika. Odpowiednie ustawienie pojawiło się w Visual Studio 2008. W ustawieniach projektu (menu Project, pozycja np. App Properties), w gałęzi Configuration Properties, Linker, Manifest files pojawiły się pozycje odwołujące się do technologii kontroli konta użytkownika (rysunek 3.5). Należy zmienić ustawienie UAC Execution Level z domyślnej wartości asInvoker na requireAdministrator. W ustawieniu Enable User Account Control (UAC) musi być oczywiście zaznaczone Yes. Rysunek 3.5. Uruchomienie aplikacji z takim manifestem będzie wymagać potwierdzenia uprawnień
Po przebudowaniu projektu (F6) do ikony pliku .exe (z podkatalogu Debug) dodana zostanie tarcza (rysunek 3.6). Ale to oczywiście tylko objaw faktu, że program po uruchomieniu będzie żądał uprawnień administratora (bez względu na to, czy będzie je rzeczywiście wykorzystywał). Jeżeli w ten sposób zmodyfikujemy projekt uruchamiający inne procesy, to po jego uruchomieniu pojawi się monit widoczny na rysunku 3.7. Za to program nie będzie wymagał dodatkowych potwierdzeń przy wywoływaniu metody ShellExecute z poleceniem runas.
Rozdział 3. ♦ Uruchamianie i kontrolowanie aplikacji oraz ich okien
79
Rysunek 3.6. Oznaczenie programu, który po uruchomieniu będzie żądał uprawnień administratora
Rysunek 3.7. Monit UAC po uruchomieniu aplikacji żądającej uprawnień administratora w przypadku użytkownika nie mającego takich uprawnień
Kontrolowanie własności okien Lista okien W kolejnym projekcie przygotujemy aplikację, która wyświetla listę okien wraz z informacjami na ich temat. Będzie to okazja, żeby poznać funkcję EnumWindows i jej dość specyficzny, a mimo to dość charakterystyczny dla WinAPI sposób działania10. Polega 10
Na podobnej zasadzie działa również kilka innych funkcji WinAPI z Enum... w nazwie.
80
Visual C++. Gotowe rozwiązania dla programistów Windows
on na konieczności wywołania zdefiniowanej przez nas funkcji zwrotnej (o ściśle określonej deklaracji) tyle razy, ile jest otwartych okien. Za każdym razem do funkcji zwrotnej, w naszym przypadku funkcji o nazwie DodajWiersz, przekazywany jest uchwyt kolejnego okna. Według MSDN deklaracja funkcji zwrotnej powinna być następująca: BOOL CALLBACK EnumWindowsProc(HWND hwnd,LPARAM lParam)
W projekcie MFC jej sygnatura może wyglądać na przykład tak: static int CALLBACK DodajWiersz(HWND uchwyt, LPARAM lParam);
Oczywiście to nie nazwa funkcji jest istotna, a jej argumenty i zwracana wartość. Należy również zauważyć, że zdefiniujemy ją jako metodę klasy okna dialogowego. Jest to możliwe wyłącznie, jeżeli metoda ta będzie statyczna (dlatego dodajemy modyfikator static). Takie metody traktowane są bowiem jak zwykłe funkcje. Działanie funkcji zwrotnej może być dowolne — jest to wielka zaleta tego typu konstrukcji. Funkcja taka może na przykład zmieniać priorytety procesów, zamykać procesy lub choćby minimalizować okna aplikacji. My ograniczymy się tylko do pobierania informacji — zadaniem funkcji DodajWiersz będzie zapełnienie informacjami o oknie i jego procesie kolejnego wiersza w umieszczonej na formie liście, tj. komponencie List Control. To tłumaczy nazwę funkcji. Pośród tych informacji będzie m.in. tytuł okna pobrany za pomocą funkcji GetWindowText, nazwa klasy okna pobrana przy użyciu funkcji GetClassName, jego położenie i wielkość (GetWindowPlacement) i wreszcie identyfikator procesu (GetWindowThreadProcessId11). Pierwszym argumentem wszystkich wymienionych funkcji jest uchwyt do okna. Zostanie on dostarczony do funkcji Dodaj ´Wiersz w jej pierwszym argumencie. Zadba o to funkcja EnumWindows. Pobrane informacje o oknie zapisywane są zazwyczaj w zmiennych lub w strukturach wskazanych przez drugi argument funkcji zwrotnej. My uczynimy podobnie, przekazując tą drogą wskaźnik do komponentu List Control. 1. Tworzymy nowy projekt o nazwie App typu MFC Application z oknem
dialogowym. 2. Do okna dialogowego dodajemy komponent List Control. 3. Własność View nowo dodanego komponentu zmieniamy na Report. 4. Z komponentem wiążemy zmienną o nazwie list1. 5. W pliku nagłówkowym AppDlg.h deklarujemy statyczną metodę DodajWiersz
o sygnaturze: static int CALLBACK DodajWiersz(HWND uchwyt, LPARAM lParam);
6. Metodę tę definiujemy w pliku AppDlg.cpp (listing 3.16).
11
Wszystkie te funkcje obecne są we wszystkich 32-bitowych wersjach Windows, tj. od Windows 95 i Windows NT 3.1.
Rozdział 3. ♦ Uruchamianie i kontrolowanie aplikacji oraz ich okien
81
Listing 3.16. Deklaracja i definicja metody DodajWiersz int CALLBACK CAppDlg::DodajWiersz(HWND uchwyt, LPARAM lParam) { CListCtrl * list = (CListCtrl*)lParam; const int MAX_CHAR_SIZE=128; wchar_t tytul[MAX_CHAR_SIZE]; wchar_t nazwaKlasy[MAX_CHAR_SIZE]; DWORD idProcesu; WINDOWPLACEMENT ustawieniaOkna; ::GetWindowText(uchwyt,tytul,MAX_CHAR_SIZE); ::GetClassName(uchwyt,nazwaKlasy,MAX_CHAR_SIZE); ::GetWindowThreadProcessId(uchwyt,&idProcesu); ::GetWindowPlacement(uchwyt,&ustawieniaOkna); CString stanOkna; switch (ustawieniaOkna.showCmd) { case SW_HIDE: stanOkna.Append(L"ukryte"); break; case SW_MAXIMIZE: stanOkna.Append(L"zmaksymalizowane"); break; case SW_MINIMIZE: case SW_SHOWMINIMIZED: case SW_SHOWMINNOACTIVE : stanOkna.Append(L"zminimalizowane"); break; case SW_SHOWNORMAL: case SW_SHOW: case SW_SHOWNA: case SW_SHOWNOACTIVATE: case SW_RESTORE: stanOkna.Append(L"normalne"); break; } int szerOkna = ustawieniaOkna.rcNormalPosition.right-ustawieniaOkna.rcNormalPosition.left; int wysOkna = ustawieniaOkna.rcNormalPosition.bottom-ustawieniaOkna.rcNormalPosition.top; if (::IsWindowVisible(uchwyt)) { int row = list->GetHeaderCtrl()->GetItemCount() - 1; int index = list->InsertItem(row, tytul); list->SetItem(index, 1, LVIF_TEXT, nazwaKlasy, 0, 0, 0, NULL); CString sIdProcesu; sIdProcesu.AppendFormat(L"%d",idProcesu); list->SetItem(index, 2, LVIF_TEXT, sIdProcesu, 0, 0, 0, NULL); CString sUchwyt; sUchwyt.AppendFormat(L"%d",(int)uchwyt); list->SetItem(index, 3, LVIF_TEXT, sUchwyt, 0, 0, 0, NULL); list->SetItem(index, 4, LVIF_TEXT, stanOkna, 0, 0, 0, NULL); CString pozycja; pozycja.AppendFormat( L"x=%d y=%d",ustawieniaOkna.rcNormalPosition.left,ustawieniaOkna.rcNormalPosition.top); if (stanOkna.Compare(L"zminimalizowane") == 0) list->SetItem(index, 5, LVIF_TEXT, L" ", 0, 0, 0, NULL); else list->SetItem(index, 5, LVIF_TEXT, pozycja, 0, 0, 0, NULL); CString rozmiar;
82
Visual C++. Gotowe rozwiązania dla programistów Windows rozmiar.AppendFormat(L"%d x %d",szerOkna,wysOkna); list->SetItem(index, 6, LVIF_TEXT, rozmiar, 0, 0, 0, NULL); } return 1; }
7. Do metody OnInitDialog klasy okna dialogowego dodajemy kod ustawiający
własności kontrolki List Control z listingu 3.17. Listing 3.17. Tworzymy kolumny w komponencie List Control RECT r; list1.GetWindowRect(&r); int szerListy = r.right - r.left; int szerKolumny = szerListy / 7; list1.InsertColumn(0,L"Tytuł Okna",LVCFMT_LEFT, szerKolumny, 0); list1.InsertColumn(1,L"Nazwa Klasy Okna",LVCFMT_LEFT, szerKolumny, 0); list1.InsertColumn(2,L"ID okna",LVCFMT_LEFT, szerKolumny, 0); list1.InsertColumn(3,L"Uchwyt",LVCFMT_LEFT, szerKolumny, 0); list1.InsertColumn(4,L"Stan Okna",LVCFMT_LEFT, szerKolumny, 0); list1.InsertColumn(5,L"Położenie",LVCFMT_LEFT, szerKolumny, 0); list1.InsertColumn(6,L"Rozmiar",LVCFMT_LEFT, szerKolumny, 0); ::EnumWindows((WNDENUMPROC)DodajWiersz,(LPARAM)&list1);
Następnie kompilujemy projekt. Po jego uruchomieniu powinniśmy zobaczyć listę podobną do przedstawionej na rysunku 3.8. Widoczne są na niej tylko informacje o oknach głównych każdego procesu. W bardzo podobny sposób, korzystając z gotowej już funkcji zwrotnej DodajWiersz, możemy wyświetlić informacje o wszystkich oknach wybranej aplikacji, wywołując funkcję EnumChildWindows. Istnieje także funkcja Enum ´DesktopWindows, która pozwala na wyświetlenie informacji o wszystkich oknach widocznych na pulpicie. Rysunek 3.8. Wygląd aplikacji wyświetlającej listę okien
Rozdział 3. ♦ Uruchamianie i kontrolowanie aplikacji oraz ich okien
83
Okno tylko na wierzchu Spróbujemy zmienić własność okna, tak aby zawsze pozostawało na wierzchu. Do tego celu należy użyć funkcji WinAPI SetWindowPos12. Pierwszym argumentem, jak zwykle w przypadku funkcji dotyczących okien, jest uchwyt okna. Drugim — stała określająca styl okna. Kolejne cztery argumenty określają jego położenie i rozmiar. Wreszcie ostatni argument pozwala na ukrycie okna lub przełączenie flagi, powodującej np. ignorowanie parametrów określających rozmiar i położenie, a uwzględnienie tylko nowego ustawienia stylu. 1. Tworzymy nowy projekt MFC Application z oknem dialogowym. 2. Do okna dialogowego dodajemy pole opcji (komponent Check Box),
którego etykietę ustawiamy na Okno zawsze na wierzchu. 3. Tworzymy jego domyślną metodę zdarzeniową i umieszczamy w niej instrukcję: ::SetWindowPos(this->m_hWnd,checkBox1.GetCheck()?HWND_TOPMOST:HWND_NOTOPMOST, 0,0,0,0,SWP_NOMOVE | SWP_NOSIZE);
lub SetWindowPos(CWnd::FromHandle(checkBox1.GetCheck() ? HWND_TOPMOST:HWND_NOTOPMOST), 0,0,0,0,SWP_NOMOVE | SWP_NOSIZE);
W pierwszej instrukcji korzystamy z funkcji WinAPI, a w drugiej — z jej „opakowania” zdefiniowanego w klasie okna MFC. Ich działanie jest takie samo, z tym że w przypadku metody nie musimy martwić się o uchwyt okna.
Ukrywanie okna aplikacji Teraz ukryjemy okno, korzystając z funkcji ShowWindow (lub z metody o tej samej nazwie w klasie CWnd). Funkcja ta oprócz uchwytu do okna przyjmuje jeden parametr, a mianowicie wartość określającą sposób pokazania okna (por. ostatni argument metod WinExec i ShelExecute). Do okna dialogowego z poprzedniego projektu dodajemy jeszcze jeden komponent Check Box z etykietą Ukryj okno. Tworzymy dla niego domyślną metodę zdarzeniową, w której umieszczamy instrukcję chowającą okno: ::ShowWindow(this->m_hWnd,SW_HIDE);
lub ShowWindow(SW_HIDE);
12
Funkcja SetWindowPos dostępna jest w wersjach od Windows 95 i Windows NT 3.1.
84
Visual C++. Gotowe rozwiązania dla programistów Windows
Mrugnij do mnie! Warto również zwrócić uwagę na funkcję i metodę o nazwie FlashWindow. Jej wywołanie spowoduje mrugnięcie aplikacji na pasku zadań. Metoda ta nadaje się doskonale do powiadomienia użytkownika o ważnym zdarzeniu, ale nie na tyle istotnym, aby np. przywoływać samo okno aplikacji. Dodajemy do formy przycisk, np. z etykietą Mrugnij. Tworzymy jego domyślną metodę zdarzeniową i umieszczamy w niej polecenie: ::FlashWindow(this->m_hWnd,true);
lub FlashWindow(true);
W zależności od systemu efekt może być nieco inny. W Windows XP rejon na pasku aplikacji odpowiadający naszemu programowi zacznie migać (dwukrotnie zmieni kolor na inny i powróci do pierwotnego), po czym kolor pozostanie zmieniony aż do momentu przywołania okna przez użytkownika. Liczbę mrugnięć można zwiększyć, korzystając z funkcji i metody FlashWindowEx. Jeżeli np. chcemy mrugnąć dziesięć razy, powinniśmy użyć instrukcji: FLASHWINFO fwi; fwi.cbSize=sizeof(FLASHWINFO); fwi.hwnd=this->m_hWnd; fwi.dwFlags=FLASHW_ALL; fwi.uCount=10; fwi.dwTimeout=0; ::FlashWindowEx(&fwi);
lub krótszej wersji korzystającej z metody FlashWindowEx(FLASHW_ALL,10,0);
Sygnał dźwiękowy Do mrugnięcia okna możemy dodać dźwięk, stosując funkcję Beep lub MessageBeep. Funkcja Beep13 umożliwia wygenerowanie dźwięku o określonej częstości (w Hz) i czasie trwania (w milisekundach), np. Beep(100,100); lub nawet for (int i=10;i>0;i--) Beep(100*i,100); (o ile ktoś lubi muzykę à la ZX Spectrum). Z kolei funkcja Message ´Beep14 odtwarza jeden z predefiniowanych dźwięków aktualnego schematu dźwiękowego użytkownika. Jej jedynym argumentem jest liczba identyfikująca rodzaj odtwarzanego dźwięku15. Jako stałe określające rodzaj dźwięku wykorzystywane są stałe używane także w funkcji MessageBox do określania rodzaju wyświetlanej ikony, a więc MB_ICONASTERISK, MB_ICONASTERISK, MB_ICONHAND, MB_ICONQUESTION i MB_OK. Przykładem typowego wywołania powyższej funkcji jest „wykrzyknik”, tj. MessageBeep(MB_ICONEXCLAMATION); 13
Dostępna w systemach z serii NT/2000/XP/2003. Funkcja obecna we wszystkich 32-bitowych wersjach systemu. 15 Skojarzone ze stałymi pliki .wav można ustalić w panelu sterowania (ikona Dźwięki i urządzenia audio, zakładka Dźwięki). 14
Rozdział 3. ♦ Uruchamianie i kontrolowanie aplikacji oraz ich okien
85
Numery identyfikacyjne procesu i uchwyt okna Jak zdobyć identyfikator procesu, znając uchwyt okna? Jeżeli otworzymy Menedżera zadań Windows, to na zakładce Procesy zobaczymy listę procesów wraz z ich numerami identyfikacyjnymi (PID), priorytetami, liczbą wątków itp. (widoczne informacje zależą od konfiguracji Menedżera). Numer identyfikacyjny PID jest unikalnym numerem przypisanym do każdego procesu, ale jak widzieliśmy w poprzednich przykładach, nie jest on zbyt przydatny, gdy zamierzamy pobrać informacje o którymś z okien procesu lub ingerować w jego stan. O wiele ważniejszy jest uchwyt do tego okna. W jaki sposób odnaleźć uchwyt okna głównego związanego z procesem o znanym numerze PID? I jak znaleźć numer PID procesu obejmującego okno, którego uchwyt znamy? Okazuje się, że odpowiedź na drugie pytanie jest znacznie łatwiejsza — tym zajmiemy się teraz. Natomiast sposób zdobycia uchwytu głównego okna procesu opiszę w następnym podrozdziale. Aby zdobyć identyfikator bieżącego procesu, możemy użyć bezparametrowej funkcji GetCurrentProcessId. Zwraca ona ten sam numer, który widzimy w Menedżerze zadań Windows. Uchwyt do tego procesu można znaleźć za pomocą funkcji GetCurrentProcess. Aby zdobyć numer procesu o znanym uchwycie, można użyć funkcji GetWindowThreadProcessId16; jej pierwszym argumentem jest uchwyt okna, a drugim wskaźnik do zmiennej, w której będzie zapisany numer PID. Wywołanie funkcji GetWindowThreadProcessId, w której jako argumentu użyjemy uchwytu bieżącego okna, da taki sam rezultat jak w przypadku wywołania funkcji GetCurrentProcessId. Od Windows XP z SP1 dostępna jest także funkcja GetProcessId znajdująca identyfikator procesu, którego uchwyt podajemy jako argument. Uchwyt do bieżącego procesu możemy znaleźć, korzystając z funkcji GetCurrentProcess. Aby się o tym przekonać, można wykonać instrukcje z listingu 3.18. Zostały one umieszczone w domyślnej metodzie zdarzeniowej przycisku MFC, ale oczywiście nie ma to wpływu na ich działanie. Do wyświetlenia numerów PID używamy metody MessageBox. Listing 3.18. We wszystkich trzech przypadkach zobaczymy ten sam numer PID, czyli numer bieżącego procesu void CAppDlg::OnBnClickedButton1() { CString szPid; szPid.AppendFormat(L"%d\n",GetCurrentProcessId()); unsigned long pid ; GetWindowThreadProcessId(this->m_hWnd,&pid); 16
Wszystkie trzy funkcje dostępne są we wszystkich 32-bitowych wersjach Windows.
86
Visual C++. Gotowe rozwiązania dla programistów Windows szPid.AppendFormat(L"%d\n",pid); //od wersji XP z SP1 unsigned long pid2 = GetProcessId(GetCurrentProcess()); szPid.AppendFormat(L"%d\n",pid2); MessageBox(szPid);
}
Jak zdobyć uchwyt głównego okna, znając identyfikator procesu? Jak już wiemy, uzyskanie identyfikatora procesu związanego z danym oknem nie stwarza większych problemów. Gorzej jest z zadaniem odwrotnym. Nie ma prostego sposobu rozwiązania go, ponieważ jeden proces może mieć wiele wątków i okien. Załóżmy jednak, że zależy nam na uchwycie do okna głównego aplikacji17. Wówczas możemy posłużyć się funkcją EnumWindows, za pomocą której wyświetlaliśmy listę okien (pamiętajmy, że EnumWindows wylicza jedynie główne okna aplikacji). Za jej pomocą przeszukamy wszystkie okna główne, porównując identyfikatory procesów każdego z nich z identyfikatorem procesu, którego okna szukamy. Jeżeli identyfikatory się zgadzają — uchwyt okna jest tym, którego szukamy. Algorytm ten zapiszmy w funkcji PobierzUchwytGlownegoOkna, która jako argument przyjmować będzie numer procesu, a zwracać uchwyt do jego głównego okna. Zamkniemy ją w osobnym module, aby łatwiej było ją wykorzystać w kolejnych projektach. Moduł będziemy testować w prostej aplikacji, sprawdzającej uchwyt procesu, którego numer PID użytkownik może podać w polu edycyjnym. Najlepszym źródłem numerów identyfikujących procesy jest oczywiście Menedżer zadań Windows. Aby przetestować funkcjonowanie naszego rozwiązania, dodamy również możliwość sprawdzenia numeru PID związanego z pobranym uchwytem — powinien być ten sam, co podany w argumencie naszej funkcji. Innym testem będzie mrugnięcie odnalezionym oknem. 1. Jak zwykle tworzymy nowy projekt MFC Application z oknem dialogowym. 2. Do okna dialogowego dodajemy dwa pola edycyjne Edit Control oraz dwa
przyciski Button zgodnie ze wzorem na rysunku 3.9 (lewy). 3. Z polami edycyjnymi wiążemy zmienne o nazwach edit1 i edit2. Rysunek 3.9. Efekt kliknięcia przycisku Pobierz uchwyt
17
Oknem głównym jest to okno, które tworzone jest jako pierwsze. Zamknięcie tego okna powoduje zamknięcie całej aplikacji.
Rozdział 3. ♦ Uruchamianie i kontrolowanie aplikacji oraz ich okien
87
4. Do projektu dodajemy dwa pliki: PobieranieUchwytuOkna.h
i PobieranieUchwytuOkna.cpp. 5. W nowym pliku nagłówkowym definiujemy pomocniczą strukturę, za pomocą której będziemy do funkcji zwrotnej, wykorzystywanej przez EnumWindows
(korzystając z jej drugiego argumentu), przekazywać numer PID i odbierać uchwyt szukanego okna (listing 3.19). Deklarujemy też zasadniczą funkcję PobierzUchwytGlownegoOkna. Listing 3.19. Kod pliku nagłówkowego z definicją struktury #include"stdafx.h" struct StrukturaPomocnicza { unsigned long identyfikatorSzukanegoProcesu; HWND uchwytSzukanegoOkna; }; HWND PobierzUchwytGlownegoOkna(unsigned long identyfikatorProcesu);
6. Następnie w pliku PobieranieUchwytuOkna.cpp definiujemy funkcję zwrotną wykorzystywaną w EnumWindows. Nazwiemy ją ZbadajOkno (listing 3.20).
Do tej funkcji będziemy przekazywali wskaźnik (jako drugi argument) do struktury zdefiniowanej w poprzednim punkcie. W ten sposób funkcja ZbadajOkno rozpozna numer PID szukanego okna i będzie mogła zwrócić uchwyt do niego bez angażowania zmiennych globalnych. Listing 3.20. W obrębie jednego procesu może funkcjonować kilka wątków i kilka okien — interesujemy się oknem głównym #include "PobieranieUchwytuOkna.h" int CALLBACK ZbadajOkno(HWND uchwytBadanegoOkna,LPARAM lParam) { StrukturaPomocnicza* wskaznik=(StrukturaPomocnicza*)lParam; unsigned long identyfikatorSzukanegoProcesu=wskaznik->identyfikatorSzukanegoProcesu; unsigned long identyfikatorBadanegoProcesu; GetWindowThreadProcessId(uchwytBadanegoOkna,&identyfikatorBadanegoProcesu); if (identyfikatorSzukanegoProcesu==identyfikatorBadanegoProcesu) { wskaznik->uchwytSzukanegoOkna=uchwytBadanegoOkna; return false; } return true; } HWND PobierzUchwytGlownegoOkna(unsigned long identyfikatorProcesu) { StrukturaPomocnicza daneSzukanegoOkna; daneSzukanegoOkna.identyfikatorSzukanegoProcesu=identyfikatorProcesu; daneSzukanegoOkna.uchwytSzukanegoOkna=0; StrukturaPomocnicza* wskaznik=&daneSzukanegoOkna; EnumWindows((WNDENUMPROC)&ZbadajOkno,(LPARAM)wskaznik); return daneSzukanegoOkna.uchwytSzukanegoOkna; }
88
Visual C++. Gotowe rozwiązania dla programistów Windows 7. Nagłówek PobieranieUchwytuOkna.h „dołączamy” do pliku nagłówkowego
okna dialogowego: #include "PobieranieUchwytuOkna.h"
8. W widoku projektowania okna dialogowego klikamy dwukrotnie przycisk
Pobierz uchwyt, tworząc domyślną metodę zdarzeniową, do której wpisujemy kod z listingu 3.21. Test poprawności pobranego uchwytu polega na pobraniu tytułu okna za pomocą funkcji GetWindowText oraz nazwy jego klasy — za pomocą funkcji GetClassName, a także na mrugnięciu nim za pomocą funkcji FlashWindow. Wszystkie te funkcje przyjmują uchwyt okna jako argument. Pobieranie nazwy okna nie zawsze da dobry rezultat, ponieważ część okien po prostu nie ma nazwy. Efekt kliknięcia powinien być podobny do widocznego na rysunku 3.9 (prawy). Listing 3.21. Testujemy funkcję PobierzUchwytGlownegoOkna void CAppDlg::OnBnClickedButton1() { const int MAX_COUNT=256; unsigned long processId = GetCurrentProcessId(); try { CString processIdStr; edit1.GetWindowTextW(processIdStr); processId=_wtoi(processIdStr); } catch(...) { MessageBox(L"Identyfikator procesu musi być liczbą"); return; } HWND uchwytOkna=PobierzUchwytGlownegoOkna(processId); CString uchwytOknaStr; uchwytOknaStr.AppendFormat(L"%d",(int)uchwytOkna); edit2.SetWindowTextW(uchwytOknaStr); if (uchwytOkna!=0) { wchar_t nazwaOkna[MAX_COUNT]; wchar_t nazwaKlasy[MAX_COUNT]; ::GetWindowText(uchwytOkna,nazwaOkna,MAX_COUNT); ::GetClassName(uchwytOkna,nazwaKlasy,MAX_COUNT); CString opis; opis.AppendFormat(L"Uchwyt okna: %d \nNazwa okna: %s\nKlasa okna: ´%s",uchwytOkna,nazwaOkna,nazwaKlasy); MessageBox(opis); } else MessageBox(L"Proces o podanym numerze PID nie istnieje"); }
9. W podobny sposób tworzymy domyślną metodę zdarzeniową dla przycisku
Pobierz PID. Wpisujemy do niej kod z listingu 3.22.
Rozdział 3. ♦ Uruchamianie i kontrolowanie aplikacji oraz ich okien
89
Listing 3.22. I jeszcze jeden test… void CAppDlg::OnBnClickedButton2() { HWND uchwytOkna= this->GetSafeHwnd(); try { CString tekstOkna; edit2.GetWindowTextW(tekstOkna); uchwytOkna=(HWND)_wtoi(tekstOkna); } catch(...) { MessageBox(L"Uchwyt okna musi być liczbą"); return; } unsigned long processId=0; if (GetWindowThreadProcessId(uchwytOkna,&processId)!=0) { CString szUchwytOkna; szUchwytOkna.AppendFormat(L"%d",processId); edit1.SetWindowTextW(szUchwytOkna); CString wiadomosc; wiadomosc.AppendFormat(L"Identyfikator procesu: %d \nUchwyt Okna: ´%d",processId,(int)PobierzUchwytGlownegoOkna(processId)); MessageBox(wiadomosc); } else MessageBox(L"Podanemu uchwytowi nie odpowiada żadne okno"); }
10. Dla elegancji można podczas uruchamiania aplikacji zapisać w polach
edycyjnych identyfikator bieżącego procesu, odczytany za pomocą funkcji GetCurrentProcessId, i uchwyt do okna this->m_hWnd (listing 3.23). Listing 3.23. Ustalanie PID oraz uchwytu podczas uruchamiania aplikacji BOOL CAppDlg::OnInitDialog() { CString sProcessId; sProcessId.AppendFormat(L"%d",GetCurrentProcessId()); edit1.SetWindowTextW(sProcessId); CString sUchwyt; sUchwyt.AppendFormat(L"%d",GetSafeHwnd()); edit2.SetWindowTextW(sUchwyt); return TRUE;
// return TRUE
unless you set the focus to a control
}
Po uruchomieniu aplikacji należy wpisać do pola edycyjnego jeden z identyfikatorów, jaki możemy znaleźć w Menedżerze zadań Windows, na zakładce Procesy (być może należy dodać kolumnę o nazwie PID).
90
Visual C++. Gotowe rozwiązania dla programistów Windows
Kontrolowanie okna innej aplikacji Jak już wspomniałem we wstępie do tego rozdziału, funkcje WinAPI umożliwiają kontrolę okna każdej uruchomionej przez użytkownika aplikacji z poziomu kodu innej aplikacji. Dotyczy to w szczególności położenia, rozmiaru, stylu, a nawet napisu widocznego na pasku tytułu. Czy jest to naruszenie integralności programu? Oczywiście nie. Użytkownik robi przecież to samo (może poza zmianą nazwy na pasku tytułu), przesuwając myszą okno aplikacji lub zmieniając jego rozmiar18. Przygotujemy aplikację umożliwiającą kontrolowanie kilku własności okna za pomocą funkcji SetWindowText i SetWindowPlacement. Bieżący stan tych własności odczytywać będziemy za pomocą funkcji GetWindowText i GetWindowPlacement19. W celu rozpoznania okna aplikacji skorzystamy z numeru identyfikacyjnego PID procesu aplikacji. Czytelnik może we własnym zakresie rozwinąć aplikację tak, aby wybór możliwy był z listy okien (por. projekt „Lista okien”) lub na podstawie tytułu okna (tj. korzystając z poznanej już funkcji FindWindow). Funkcje GetWindowPlacement i SetWindowPlacement wykorzystują strukturę WINDOW ´PLACEMENT. Struktura ta przechowuje pełną informację o położeniu okna we wszystkich trzech przypadkach, tj. gdy okno jest widoczne na pulpicie, gdy jest zminimalizowane oraz gdy jest zmaksymalizowane. Zajmiemy się jedynie tym pierwszym przypadkiem. Odpowiada mu pole rcNormalPosition, które jest strukturą typu RECT. Zawiera ona cztery pola odpowiadające pozycji czterech krawędzi okna (left, right, top, bottom). Zatem aby odczytać położenie okna, należy wywołać funkcję GetWindowPlacement, podając jako pierwszy argument uchwyt okna, a jako drugi — adres zmiennej typu WINDOWPLACEMENT. Przy modyfikacji geometrii okna innej aplikacji wywołujemy SetWindowPlacement z identycznymi argumentami. Wcześniej należy jednak pamiętać o zainicjowaniu pól length i showCmd. W przypadku pola showCmd wykorzystujemy stałe o nazwach rozpoczynających się od SW_, znane z omawianych wyżej funkcji WinExec, ShellExecute, ShowWindow i CreateProcess. Wykorzystamy SW_RESTORE, która powoduje pokazanie okna, jeżeli jest zminimalizowane, lub przywrócenie do normalnych rozmiarów, jeżeli jest zmaksymalizowane. 1. Tworzymy nowy projekt MFC Application z oknem dialogowym. 2. Interfejs przygotowujemy zgodnie z rysunkiem 3.10. Wszystkie komponenty
poza pierwszym polem edycyjnym i przyciskiem powinny być nieaktywne (właściwość Disabled ustawiona na true). 3. Dodajemy zmienne dla górnego okna edycyjnego (proponuję edit1), dla dolnego pola edycyjnego (edit2) oraz dla suwaków (slider1, slider2, slider3 oraz slider4). 4. Do katalogu bieżącego projektu kopiujemy pliki PobieranieUchwytuOkna.h,
PobieranieUchwytuOkna.cpp z poprzedniego projektu i dodajemy je do naszego projektu (tak aby były widoczne w oknie Solution Explorer). 18
Na podobną kontrolę pozwalają również komunikaty przesyłane do aplikacji, ale o tym w rozdziale 8.
19
Funkcje SetWindowText, GetWindowText, GetWindowPlacement i GetWindowPlacement dostępne są we wszystkich 32-bitowych wersjach Windows.
Rozdział 3. ♦ Uruchamianie i kontrolowanie aplikacji oraz ich okien
91
Rysunek 3.10. Panel kontrolujący okno dowolnej uruchomionej aplikacji
5. Do pliku nagłówkowego okna dialogowego dołączamy plik nagłówkowy
PobieranieUchwytuOkna.h. #include "PobieranieUchwytuOkna.h"
6. Tworzymy domyślną metodę zdarzeniową dla przycisku. Będzie ona sprawdzała
wpisany do pola edycyjnego numer identyfikacyjny procesu (aplikacji) i inicjowała kilka pól prywatnych przechowujących informacje o oknie, które zamierzamy kontrolować: a) W widoku projektowania okna dialogowego klikamy dwukrotnie przycisk
z etykietą Sprawdź, przechodzimy dzięki temu do edytora kodu. b) Utworzony automatycznie szkielet metody uzupełniamy kodem widocznym
na listingu 3.24. Listing 3.24. „Łączenie” z oknem innej aplikacji oraz detekcja jego tytułu, położenia i rozmiaru void CAppDlg::OnBnClickedButton1() { const int MAX_COUNT=256; wchar_t nazwaOkna[MAX_COUNT]; trwaOdczytywanieWlasnosciOkna=true; unsigned long processId=0; try { CString processIdStr; edit1.GetWindowTextW(processIdStr); processId=_wtoi(processIdStr); } catch(...) { MessageBox(L"Identyfikator procesu musi być liczbą"); return; }
92
Visual C++. Gotowe rozwiązania dla programistów Windows uchwytOkna=PobierzUchwytGlownegoOkna(processId); bool prawidlowyPID=(uchwytOkna!=0); slider1.EnableWindow(prawidlowyPID); slider2.EnableWindow(prawidlowyPID); slider3.EnableWindow(prawidlowyPID); slider4.EnableWindow(prawidlowyPID); edit2.EnableWindow(prawidlowyPID); if (prawidlowyPID) { //Odczytywanie tytułu okna ::GetWindowText(uchwytOkna,nazwaOkna,MAX_COUNT); edit2.SetWindowTextW(nazwaOkna); //Odczytywanie położenia okna WINDOWPLACEMENT ustawieniaOkna; ::GetWindowPlacement(uchwytOkna,&ustawieniaOkna); int int int int
x=ustawieniaOkna.rcNormalPosition.left; y=ustawieniaOkna.rcNormalPosition.top; szer=ustawieniaOkna.rcNormalPosition.right-x; wys=ustawieniaOkna.rcNormalPosition.bottom-y;
int szerMonitora = ::GetSystemMetrics(SM_CXSCREEN); int wysMonitora = ::GetSystemMetrics(SM_CYSCREEN); //ujemna pozycja nie jest uwzględniana slider1.SetRangeMax(szerMonitora); slider2.SetRangeMax(wysMonitora); slider3.SetRangeMax(szerMonitora); slider4.SetRangeMax(wysMonitora); slider1.SetPos(szer); slider2.SetPos(wys); slider3.SetPos(x); slider4.SetPos(y); } else MessageBox(L"Proces o podanym numerze PID nie istnieje"); trwaOdczytywanieWlasnosciOkna=false; }
c) Aby powyższa metoda zadziałała, należy zdefiniować dwa pola prywatne klasy okna dialogowego, o nazwach uchwytOkna i trwaOdczytywanieWlasnosciOkna.
To drugie będzie zapobiegało zapętleniu programu przy inicjowaniu pozycji pasków przewijania. HWND uchwytOkna; bool trwaOdczytywanieWlasnosciOkna;
d) Aby zainicjować pola obiektu, do metody OnInitDialog dodajemy kod
z listingu 3.25.
Rozdział 3. ♦ Uruchamianie i kontrolowanie aplikacji oraz ich okien
93
Listing 3.25. Jak zwykle domyślnym numerem PID jest numer bieżącego procesu BOOL CAppDlg::OnInitDialog() { ... uchwytOkna = 0; trwaOdczytywanieWlasnosciOkna = false; CString textOkna; textOkna.AppendFormat(L"%d",GetCurrentProcessId()); edit1.SetWindowTextW(textOkna); return TRUE;
// return TRUE
unless you set the focus to a control
}
7. Teraz czas na zmianę geometrii okna innej aplikacji. Zaznaczamy pierwszy pasek
przewijania, w oknie Properties przechodzimy do zakładki Control Events, odszukujemy komunikat NM_CUSTOMDRAW20 i dodajemy nową metodę obsługującą to zdarzenie (listing 3.26). Listing 3.26. Manipulowanie własnościami okna innej aplikacji void CAppDlg::OnNMCustomdrawSlider1(NMHDR *pNMHDR, LRESULT *pResult) { LPNMCUSTOMDRAW pNMCD = reinterpret_cast(pNMHDR); // TODO: Add your control notification handler code here *pResult = 0; if(uchwytOkna == 0 || trwaOdczytywanieWlasnosciOkna) return ; WINDOWPLACEMENT ustawieniaOkna; ustawieniaOkna.rcNormalPosition.left = slider3.GetPos(); ustawieniaOkna.rcNormalPosition.top = slider4.GetPos(); ustawieniaOkna.rcNormalPosition.right = slider3.GetPos()+slider1.GetPos(); ustawieniaOkna.rcNormalPosition.bottom = slider4.GetPos()+slider2.GetPos(); ustawieniaOkna.showCmd = SW_RESTORE; ustawieniaOkna.length = sizeof(ustawieniaOkna); ::SetWindowPlacement(uchwytOkna,&ustawieniaOkna); }
8. Następnie łączymy komunikat NM_CUSTOMDRAW pozostałych pasków przewijania z metodą OnNMCustomdrawSlider1; zaznaczamy kontrolkę i w liście rozwijanej
przy komunikacie wskazujemy metodę. 9. Zmieniamy tytuł okna. Odpowiedni kod umieśćmy w metodzie zdarzeniowej związanej ze zmianą zawartości drugiego pola edycyjnego (komunikat EN_CHANGE).
Pokazuje ją listing 3.27.
20
Nie korzystamy z komunikatu TRBN_THUMPOSCHANGING, ponieważ ten obsługiwany jest dopiero od systemu Windows Vista (por. opis w rozdziale 1.).
94
Visual C++. Gotowe rozwiązania dla programistów Windows
Listing 3.27. W momencie zmiany zawartości pola edycyjnego zmienia się tytuł okna void CAppDlg::OnEnChangeEdit2() { if(uchwytOkna == 0 || trwaOdczytywanieWlasnosciOkna) return; CString tytulOkna; edit2.GetWindowTextW(tytulOkna); ::SetWindowText(uchwytOkna,tytulOkna); }
Kontrolowanie grupy okien Czasami istnieje potrzeba kontrolowania całej grupy okien. W tym celu można by zastosować funkcje użyte w poprzednim przykładzie, jednak istnieje prostsze rozwiązanie. Biblioteka WinApi dostarcza zestaw funkcji, których zadaniem jest kontrola właściwości (takich jak pozycja czy wymiary) grup okien. Są to BeginDeferWindowPos, DeferWindowPos i EndDeferWindowPos. Zadaniem funkcji BeginDeferWindowPos jest alokacja pamięci dla struktury opisującej pozycję grupy okien. Funkcja zwraca uchwyt do struktury. Gdy mamy już uchwyt do struktury, możemy przystąpić do ustalenia pozycji i rozmiaru wybranych okien. W tym celu wykorzystujemy funkcję DeferWindowPos. Funkcja zwraca uchwyt zaktualizowanej struktury opisującej okna. Ostatnim krokiem jest wywołanie EndDeferWindowPos, która zmienia położenie i rozmiar okien opisanych przez strukturę. Warto dodać, że zmiany dokonywane są równocześnie dla wszystkich okien. 1. Tworzymy nowy projekt MFC Application z oknem dialogowym. 2. Do okna dialogowego dodajemy komponent List Control, pięć komponentów
Static Text, komponent Combo Box, komponent Check Box, cztery komponenty Slider Control, cztery komponenty Edit Control oraz przycisk (rysunek 3.11). 3. Z kontrolkami wiążemy zmienne: list1 (komponent List Control), combo1 (komponent Combo Box), checkBox1 (komponent Check Box), slider1, slider2, slider3, slider4 (dla komponentów Slider Control), edit1, edit2, edit3, edit4 (dla komponentów Edit Control). 4. Własność View kontrolki List Control zmieniamy na Report, a własność Single
Selection na false. 5. Aby dodać elementy do combo1, można we właściwości Data wpisać nowe elementy,
oddzielając je średnikami: „HWND_BOTTOM;HWND_NOTOPMOST; HWND_TOP;HWND_TOPMOST”. 6. Własność Sort kontrolki Combo Box ustawiamy na false. 7. Następnie w klasie okna dialogowego (w pliku nagłówkowym) deklarujemy metodę DodajWiersz, którą wykorzystamy do pobrania informacji o widocznych
oknach i umieszczenia jej w liście (listing 3.28).
Rozdział 3. ♦ Uruchamianie i kontrolowanie aplikacji oraz ich okien Rysunek 3.11. Kontrola wybranej grupy okien
Listing 3.28. Deklaracja funkcji zwrotnej class CKontrolowanieGrupOkienDlg : public CDialog { ... private: static int CALLBACK DodajWiersz(HWND uchwyt, LPARAM lParam); };
8. Jej definicje umieszczamy w pliku .cpp (listing 3.29). Listing 3.29. Pobieramy nazwę i uchwyt widocznych okien int CALLBACK CKontrolowanieGrupOkienDlg::DodajWiersz(HWND uchwyt, LPARAM lParam) { CListCtrl * list = (CListCtrl*)lParam; const int MAX_CHAR_SIZE=128; wchar_t tytul[MAX_CHAR_SIZE]; ::GetWindowText(uchwyt,tytul,MAX_CHAR_SIZE); if (::IsWindowVisible(uchwyt)) { int row = list->GetHeaderCtrl()->GetItemCount() - 1; int index = list->InsertItem(row, tytul); CString sUchwyt; sUchwyt.AppendFormat(L"%d",(int)uchwyt); list->SetItem(index, 1, LVIF_TEXT, sUchwyt, 0, 0, 0, NULL);
95
96
Visual C++. Gotowe rozwiązania dla programistów Windows } return 1; }
9. W metodzie OnInitDialog konfigurujemy kolumny listy, a także określamy
maksymalne wartości dla suwaków i włączamy zaznaczenie dla pola opcji (listing 3.30). Listing 3.30. Komponent Slider Control wyświetla nam dwie kolumny — tekst okna oraz jego
uchwyt BOOL CKontrolowanieGrupOkienDlg::OnInitDialog() { CDialog::OnInitDialog(); // Set the icon for this dialog. The framework does this automatically // when the application's main window is not a dialog SetIcon(m_hIcon, TRUE); // Set big icon SetIcon(m_hIcon, FALSE); // Set small icon RECT r; list1.GetWindowRect(&r); int szerListy = r.right - r.left; int szerKolumny = szerListy / 2; list1.InsertColumn(0,L"Tytuł Okna",LVCFMT_LEFT, szerKolumny, 0); list1.InsertColumn(1,L"Uchwyt Okna",LVCFMT_LEFT, szerKolumny, 0); int szerMonitora = ::GetSystemMetrics(SM_CXSCREEN); int wysMonitora = ::GetSystemMetrics(SM_CYSCREEN); slider1.SetRangeMax(szerMonitora); slider2.SetRangeMax(wysMonitora); slider3.SetRangeMax(szerMonitora); slider4.SetRangeMax(wysMonitora); checkBox1.SetCheck(TRUE); ::EnumWindows((WNDENUMPROC)DodajWiersz,(LPARAM)&list1); return TRUE;
// return TRUE
unless you set the focus to a control
}
10. Pozycja każdego suwaka będzie na bieżąco prezentowana w towarzyszącym
mu polu edycyjnym. Zmiana jego pozycji będzie jednak wpływać na wygląd okna aplikacji wybranej w liście dopiero po naciśnięciu przycisku Wykonaj. Tworzymy zatem domyślne metody zdarzeniowe (zdarzenie NW_CUSTOMDRAW) dla każdego suwaka i umieszczamy w nich kod przygotowany na wzór metody z listingu 3.31. W kolejnych metodach zmieniać się będą tylko numery suwaka i pola edycyjnego.
Rozdział 3. ♦ Uruchamianie i kontrolowanie aplikacji oraz ich okien
97
Listing 3.31. Aktualizacja pozycji suwaka void CKontrolowanieGrupOkienDlg::OnNMCustomdrawSlider1(NMHDR *pNMHDR, LRESULT *pResult) { LPNMCUSTOMDRAW pNMCD = reinterpret_cast(pNMHDR); // TODO: Add your control notification handler code here *pResult = 0; CString wartosc; wartosc.AppendFormat(L"%d",slider1.GetPos()); edit1.SetWindowTextW(wartosc); }
11. Do pliku KontrolowanieOkienDlg.cpp dodajemy zasadniczą funkcję tego projektu, a mianowicie PrzesunOkna (listing 3.32). Jej zadaniem jest zmiana
rozmiaru i położenia wybranej grupy okien. Listing 3.32. Definicja funkcji PrzesunOkna void PrzesunOkna(HWND * okna, int ileOkien, HWND pozycjaOkna, int flagi, int x, ´int y, int szer, int wys) { HDWP s = BeginDeferWindowPos(ileOkien); for(int i=0;iGetWindowInfo(&windowInfo); int szerokoscOkno = windowInfo.rcWindow.right - windowInfo.rcWindow.left; int wysokoscOkno = windowInfo.rcWindow.bottom - windowInfo.rcWindow.top; int wysPaskaTytulu = titleBarInfo.rcTitleBar.bottom; int promien=min(wysokoscOkno,szerokoscOkno); HRGN rgn=CreateEllipticRgn(0,wysPaskaTytulu,promien,promien); HRGN rgnOkno = CreateRectRgn(0,0,szerokoscOkno,wysokoscOkno); CombineRgn(rgn,rgnOkno,rgn,RGN_DIFF); SetWindowRgn(rgn,true); return TRUE;
// return TRUE
unless you set the focus to a control
}
Rysunek 3.14. „Dziura” w formie uzyskana za pomocą funkcji WinAPI
Bardziej finezyjne kształty okien wymagają żmudnego składania regionów. Można ten problem ominąć, składając regiony po jednym punkcie na podstawie obrazu wczytanego z bitmapy. Wówczas projektowanie okna ogranicza się do projektowania jego rysunku za pomocą dowolnego programu graficznego. Realizujący ten pomysł projekt, oparty na oryginalnym kodzie pochodzącym z książki Michaela Flenova, znajduje się w dołączonych do książki materiałach.
102
Visual C++. Gotowe rozwiązania dla programistów Windows
Aby przenosić okno, chwytając za dowolny punkt Po usunięciu paska tytułu pojawia się kłopot z przesuwaniem okna. Po prostu nie ma go za co chwycić. Można wówczas zastosować proste rozwiązanie pozwalające na przeciąganie okna za dowolny punkt okna. W tym celu trzeba obsłużyć trzy komunikaty przychodzące do okna i dotyczące wciśnięcia i zwolnienia lewego przycisku myszy w obrębie okna (WM_LBUTTONDOWN i WM_LBUTTONUP) oraz ruchu myszy (WM_MOUSEMOVE). W momencie wciśnięcia przycisku myszy przestawimy zmienną logiczną (flagę) informującą o aktywnym procesie przeciągania i zapamiętamy pozycję okna i kursora myszy w tym momencie. Zwolnienie przycisku ponownie przestawia flagę. Natomiast w momencie ruchu myszy, o ile flaga jest ustawiona na true, a więc przycisk jest wciśnięty, będziemy zmieniać pozycję okna o przesunięcie myszy względem momentu, w którym wciśnięty był przycisk. Zacznijmy od zdefiniowania trzech prywatnych pól obsługujący proces przeciągania: private: bool czyPrzyciskMyszyWcisniety; POINT polozenieMyszyWMomencieWduszeniaPrzycisku; RECT polozenieOknaWMomencieWduszeniaPrzycisku;
Umieszczamy je w pliku nagłówkowym (np. AppDlg.h) w obrębie klasy okna dialogowego. Flagę czyPrzyciskMyszyWcisniety inicjujemy w metodzie OnInitDialog wartością true. W zasadzie fakt wduszenia przycisku moglibyśmy badać za pomocą funkcji WinAPI, ale skoro i tak musimy zareagować na wciśnięcie zapamiętaniem położeń myszy i okna, utworzenie zmiennej wydało mi się prostszym rozwiązaniem. Aby zrealizować powyższy plan przeciągania okna, wystarczy teraz przejść do podglądu okna i utworzyć metody związane z trzema wymienionymi wyżej komunikatami. Pozwala na to zakładka Messages w oknie Properties przy zaznaczonym całym oknie. W metodach tych wpisujemy kod widoczny na listingu 3.37. Listing 3.37. Metody realizujące przeciąganie okna myszą void CAppDlg::OnLButtonDown(UINT nFlags, CPoint point) { czyPrzyciskMyszyWcisniety=true; GetCursorPos(&polozenieMyszyWMomencieWduszeniaPrzycisku); this->GetWindowRect(&polozenieOknaWMomencieWduszeniaPrzycisku); CDialog::OnLButtonDown(nFlags, point); } void CAppDlg::OnLButtonUp(UINT nFlags, CPoint point) { czyPrzyciskMyszyWcisniety=false; CDialog::OnLButtonUp(nFlags, point); } void CAppDlg::OnMouseMove(UINT nFlags, CPoint point) {
Rozdział 3. ♦ Uruchamianie i kontrolowanie aplikacji oraz ich okien if(czyPrzyciskMyszyWcisniety) { POINT biezacePolozenieMyszy; GetCursorPos(&biezacePolozenieMyszy); POINT przesuniecieMyszy; przesuniecieMyszy.x=biezacePolozenieMyszy. ´x-polozenieMyszyWMomencieWduszeniaPrzycisku.x; przesuniecieMyszy.y=biezacePolozenieMyszy.y´polozenieMyszyWMomencieWduszeniaPrzycisku.y; this->SetWindowPos(0,polozenieOknaWMomencieWduszeniaPrzycisku.left+ ´przesuniecieMyszy.x,polozenieOknaWMomencieWduszeniaPrzycisku. ´top+przesuniecieMyszy.y,0,0,SWP_NOOWNERZORDER | SWP_NOSIZE); //this->RedrawWindow(); } CDialog::OnMouseMove(nFlags, point); }
103
104
Visual C++. Gotowe rozwiązania dla programistów Windows
Rozdział 4.
Systemy plików, multimedia i inne funkcje WinAPI Pliki i system plików (funkcje powłoki) Interfejs użytkownika systemu Windows pozwala na uruchamianie programów, kontrolę plików i katalogów (z funkcjami kosza systemowego włącznie), drukowanie dokumentów, tworzenie skrótów do nich itp. W interfejsie programisty WinAPI tym operacjom odpowiadają tzw. funkcje powłoki, gdzie przez powłokę (ang. shell) rozumie się tę najwyższą warstwę systemu, która odpowiada za komunikację z użytkownikiem1. W ten sposób powłoka przesłania jądro i warstwy, do których użytkownik nie musi sięgać2. Funkcje WinAPI dotyczące powłoki są zazwyczaj prostsze w użyciu i bardziej zautomatyzowane niż ich głębsze odpowiedniki. Najlepszym przykładem jest opisana w poprzednim rozdziale funkcja ShellExecute, która jest znacznie łatwiejsza w użyciu od CreateProcess. Teraz Czytelnik pozna inne funkcje pozwalające na wygodniejsze manipulowanie plikami, w tym m.in. na korzystanie z kosza, operacje na grupach plików i całych katalogach. Omówimy także interfejsy COM należące do powłoki, które pozwalają na tworzenie skrótów, oraz interfejs IFileOperation, który jest dostępny w systemie Windows Vista. 1
Słowo „interfejs” w tym i w poprzednim zdaniu oznacza oczywiście coś innego. W pierwszym przypadku chodzi o GUI (ang. graphic user interface), a więc okna, menu i inne graficzne elementy aplikacji widoczne na ekranie, podczas gdy w drugim mowa o bibliotece funkcji pozwalających na kontrolę systemu Windows. Funkcje powłoki to podzbiór funkcji interfejsu WinAPI, które pozwalają na kontrolę interfejsu GUI.
2
Zob. „Blokowanie dostępu do komputera” w rozdziale 2.
106
Visual C++. Gotowe rozwiązania dla programistów Windows
Odczytywanie ścieżek do katalogów specjalnych Ścieżki do katalogów specjalnych użytkownika (np. katalogu z dokumentami czy pulpitu) można odczytać z rejestru (por. rozdział 5.). Jednak nie jest to sposób zalecany. Przedstawione tutaj rozwiązanie korzystające z funkcji powłoki jest poprawnym sposobem odczytywania ścieżki do tych katalogów.
Do odczytania katalogów specjalnych systemu i profilu użytkownika służy funkcja SHGetSpecialFolderPath3 zdefiniowana w nagłówku shlobj.h. Jej trzeci argument wskazuje interesujący nas katalog. Najbardziej popularne to: CSIDL_PERSONAL (Moje dokumenty), CSIDL_DESKTOP (Pulpit), CSIDL_WINDOWS (C:\Windows) i CSIDL_SYSTEM (C:\ Windows\ System32). Część stałych odpowiadających katalogom definiowanym dla każdego użytkownika ma wersje zawierające _COMMON_. Odnoszą się one do odpowiednich katalogów zawierających elementy dostępne w profilach wszystkich użytkowników (w Windows XP są to podkatalogi katalogu C:\Documents and Settings\All Users, a w Windows Vista są to podkatalogi katalogu C:\Users (Użytkownicy)), np. CSIDL_ ´COMMON_DESKTOPDIRECTORY4. Listing 4.1 zawiera kilka przykładowych funkcji zwracających uzyskane dzięki wywołaniu funkcji SHGetSpecialFolderPath ścieżki do katalogów specjalnych w postaci obiektu CString — łańcucha wygodnego do użycia w aplikacjach korzystających z biblioteki MFC. Listing 4.1. Zbiór funkcji zwracających ścieżki do katalogów specjalnych CString Katalog_Windows() { TCHAR path[MAX_PATH]; SHGetSpecialFolderPath(NULL, path, CSIDL_WINDOWS, FALSE); return CString(path); } CString Katalog_System() { TCHAR path[MAX_PATH]; SHGetSpecialFolderPath(NULL, path, CSIDL_SYSTEM, FALSE); return CString(path); } CString Katalog_MojeDokumenty() { TCHAR path[MAX_PATH]; SHGetSpecialFolderPath(NULL, path, CSIDL_PERSONAL, FALSE); return CString(path); } CString Katalog_AllUsers_Pulpit() { TCHAR path[MAX_PATH]; 3
Funkcja ta działa w każdej wersji systemu Windows, jednak w Windows 95 i NT 4.0 wymaga zainstalowania Internet Explorera 4.0.
4
Wszystkie stałe CSIDL znajdzie Czytelnik w dokumentacji MSDN pod hasłem CSIDL.
Rozdział 4. ♦ Systemy plików, multimediai inne funkcje WinAPI
107
SHGetSpecialFolderPath(NULL, path, CSIDL_COMMON_DESKTOPDIRECTORY, FALSE); return CString(path); } CString Katalog_Pulpit() { TCHAR path[MAX_PATH]; SHGetSpecialFolderPath(NULL, path, CSIDL_DESKTOPDIRECTORY, FALSE); return CString(path); }
Tworzenie skrótu (.lnk) W tym projekcie po raz pierwszy będziemy mieli do czynienia z obiektem zdefiniowanym w systemie Windows i udostępnionym programistom w ramach mechanizmu COM (ang. Component Object Model). Pełniejsze wprowadzenie do COM i związanych z nim technologii znajdzie Czytelnik w rozdziale 4. Tutaj ograniczę się zatem jedynie do omówienia funkcji wykorzystanych w poniższym kodzie (w komentarzu na końcu tego projektu). Zgodnie z zasadami przedstawionymi we wstępie zasadnicze funkcje napisane zostaną w taki sposób, aby mogły być użyte w dowolnym projekcie, lecz w przykładach ich użycia skorzystamy z typów zdefiniowanych w MFC, w szczególności dotyczy to łańcuchów. 1. Tworzymy nowy projekt aplikacji MFC z oknem dialogowym o nazwie PlikSkrotu. 2. Do projektu dodajemy pliki Skrot.h i Skrot.cpp (oczywiście do odpowiednich
gałęzi w drzewie plików projektu widocznym w Solution Explorer). 3. W pliku nagłówkowym Skrot.h definiujemy strukturę pomocniczą CParametrySkrotu, której pola będą przechowywały następujące własności
skrótu: pełną ścieżkę wraz z nazwą pliku, do którego chcemy utworzyć skrót, opis, katalog roboczy, klawisz skrótu (litera, która razem z klawiszami Ctrl i Alt będzie uruchamiała skrót, jeżeli będzie umieszczony na pulpicie lub na pasku szybkiego uruchamiania), ścieżkę do pliku zawierającego ikonę skrótu oraz numer ikony w tym pliku (listing 4.2). Listing 4.2. Zawartość pliku nagłówkowego Skrot.h #pragma once struct CParametrySkrotu { TCHAR sciezkaPliku[MAX_PATH], katalogRoboczy[MAX_PATH], sciezkaIkony[MAX_PATH]; TCHAR argumenty[256], opis[256]; int rodzajOkna, numerIkony; wchar_t klawiszSkrotu; }; BOOL TworzSkrot(LPCTSTR sciezkaLinku, CParametrySkrotu parametrySkrotu);
108
Visual C++. Gotowe rozwiązania dla programistów Windows 4. Listing 4.2 zawiera również deklaracje dwóch funkcji, które zdefiniujemy
w pliku Skrot.cpp, a które służyć będą do tworzenia i odczytywania pliku skrótu. 5. Przechodzimy do edycji pliku Skrot.cpp. Umieszczamy w nim dyrektywę
dołączającą nagłówek shlwapi.h, zawierający deklarację m.in. funkcji służących do operacji na ścieżkach do pliku, w szczególności funkcji PathRemoveFileSpec, która usuwa z pełnej ścieżki do pliku nazwę pliku, a pozostawia jedynie ścieżkę katalogu. Definiujemy w pliku również funkcję tworzącą skrót (listing 4.3). Listing 4.3. Omówienie funkcji znajduje się w komentarzu poniżej #include "stdafx.h" #include "Skrot.h" #include // PathRemoveFileSpec BOOL TworzSkrot(LPCTSTR sciezkaLinku, CParametrySkrotu parametrySkrotu) { CoInitializeEx(NULL, COINIT_MULTITHREADED); IShellLink* pISLink; if (CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_IShellLink, (void**) &pISLink) != S_OK) return FALSE; IPersistFile* pIPFile; pISLink->QueryInterface(IID_IPersistFile,(void**) &pIPFile); //przygotowanie parametrów if (wcscmp(parametrySkrotu.sciezkaPliku,L"")==0) THROW("Brak nazwy pliku, do ´którego ma zostać utworzony skrót"); if (wcscmp(parametrySkrotu.katalogRoboczy,L"")==0) { wcscpy_s(parametrySkrotu.katalogRoboczy, MAX_PATH, parametrySkrotu.sciezkaPliku); PathRemoveFileSpecW(parametrySkrotu.katalogRoboczy);//parametrySkrotu.katalogRoboczy. ´GetBuffer()); } if (parametrySkrotu.rodzajOkna == 0) parametrySkrotu.rodzajOkna = SW_SHOWNORMAL; //nie dopuszczamy SW_HIDE=0 ze względu na taką domyślną inicjację parametrySkrotu.klawiszSkrotu = toupper(parametrySkrotu.klawiszSkrotu); //przygotowanie obiektu pISLink->SetPath(parametrySkrotu.sciezkaPliku); pISLink->SetWorkingDirectory(parametrySkrotu.katalogRoboczy); pISLink->SetArguments(parametrySkrotu.argumenty); if (parametrySkrotu.opis != L"") pISLink->SetDescription(parametrySkrotu.opis); pISLink->SetShowCmd(parametrySkrotu.rodzajOkna); if (parametrySkrotu.sciezkaIkony != L"") pISLink->SetIconLocation(parametry ´Skrotu.sciezkaIkony, parametrySkrotu.numerIkony); if (parametrySkrotu.klawiszSkrotu != NULL) pISLink->SetHotkey(((HOTKEYF_ALT | ´HOTKEYF_CONTROL) Save(sciezkaLinku, FALSE) == S_OK); pISLink->Release();
Rozdział 4. ♦ Systemy plików, multimediai inne funkcje WinAPI
109
CoUninitialize(); return wynik; }
6. Aby przetestować funkcję TworzSkrot, przechodzimy do widoku projektowania
i na podglądzie formy umieszczamy przycisk. Klikając go dwukrotnie, tworzymy domyślną metodę zdarzeniową, w której umieszczamy polecenia z listingu 4.4. W pliku nagłówkowym PlikSkrotuDlg.h należy wcześniej dołączyć nowy nagłówek za pomocą dyrektywy prekompilatora: #include "Skrot.h". Listing 4.4. Tworzymy skrót do bieżącej aplikacji w bieżącym katalogu void CPlikSkrotuDlg::OnBnClickedButton1() { CParametrySkrotu parametrySkrotu; GetModuleFileName(GetModuleHandle(NULL), parametrySkrotu.sciezkaPliku, MAX_PATH); wcscpy_s(parametrySkrotu.katalogRoboczy,MAX_PATH,parametrySkrotu.sciezkaPliku); PathRemoveFileSpec(parametrySkrotu.katalogRoboczy); wcscpy_s(parametrySkrotu.argumenty,260,L""); wcscpy_s(parametrySkrotu.opis,260,AfxGetApp()->m_pszAppName); parametrySkrotu.rodzajOkna = SW_SHOWNORMAL; wcscpy_s(parametrySkrotu.sciezkaIkony,MAX_PATH,parametrySkrotu.sciezkaPliku); parametrySkrotu.numerIkony = 0; parametrySkrotu.klawiszSkrotu = 'y'; TworzSkrot(L"Skrot.lnk", parametrySkrotu); }
Listing 4.3 zawiera zasadniczą funkcję TworzSkrot. W pierwszej linii kodu tej funkcji inicjujemy bibliotekę COM, korzystając z funkcji CoInitializeEx. Zwykle polecenie to umieszcza się w jednej z funkcji inicjujących aplikacji, np. na początku funkcji OnInitDialog, w pliku PlikSkrotuDlg.cpp. My umieściliśmy ją w funkcji TworzSkrot, aby zwrócić uwagę Czytelnika, że powinna być wywołana przed utworzeniem obiektu COM, a poza tym, aby funkcja TworzSkrot była bardziej autonomiczna, co ułatwi jej użycie w projektach Czytelnika. Następnie tworzymy instancję obiektu COM, posługując się funkcją CoCreateInstance z identyfikatorem obiektu CLSID_ShellLink. Funkcja ta zapisuje we wskaźniku typu IShellLink* (nazwa użytej przez nas zmiennej to pISLink) wskaźnik do utworzonego obiektu COM. Typ IShellLink oraz użyty później IPersist ´File to interfejsy, czyli w nomenklaturze technologii COM zbiory funkcji (metod), które podobnie jak klasy mogą dziedziczyć z innych interfejsów (w tym przypadku z IUnknown). Interfejsy te umożliwiają dostęp do metod utworzonego przez nas obiektu COM. Interfejs IShellLink udostępnia metody pozwalające na ustalenie lub odczytanie własności skrótu (pliku z rozszerzeniem .lnk). Natomiast IPersistFile5 zawiera metody Save i Load, które pozwalają zapisać w pliku i odczytać z niego atrybuty ustalone przez interfejs IShellLink. Po zakończeniu korzystania z obiektu należy go jeszcze zwolnić, używając metody Release.
5
Oba interfejsy dostępne są we wszystkich 32-bitowych wersjach Windows, poza Windows NT 3.x.
110
Visual C++. Gotowe rozwiązania dla programistów Windows
Jeżeli skrót o podanej nazwie już istnieje, powyższa funkcja nadpisze go bez pytania o zgodę.
Do wskazania ścieżki pliku, do którego tworzymy skrót, wykorzystujemy metodę IShellLink::SetPath; do wskazania katalogu roboczego — IShellLink::SetWorking ´Directory; do opisu — ISLink::SetDescription itd. Klawisz skrótu (w przypadku plików .lnk obowiązkowa jest kombinacja klawiszy Ctrl+Alt) ustalamy metodą ISLink::SetHotKey. Jej argument to liczba typu Word, w której mniej znaczący bajt zajmuje znak typu char, a w górnym, bardziej znaczącym bajcie zapalamy bity wskazane przez stałe HOTKEYF_ALT i HOTKEYF_CONTROL (czyli w efekcie 00000110). Plik zapisany przez metodę z listingu 4.4 można sprawdzić za pomocą systemowego edytora skrótów (rysunek 4.1).
Rysunek 4.1. Systemowy edytor skrótów. Dziwny opis pliku na lewym rysunku jest domyślnym opisem aplikacji w zasobach projektu; można go oczywiście z łatwością zmienić
Odczyt i edycja skrótu .lnk Zdefiniujemy funkcję CzytajSkrot, która umieści informacje o skrócie w strukturze CParametrySkrotu zdefiniowanej w listingu 4.2. Edycja tej struktury nie powinna sprawić żadnych trudności. Po modyfikacji informacje o skrócie można ponownie zapisać, korzystając z funkcji TworzSkrot. 1. Funkcja CzytajSkrot z listingu 4.5 powinna znaleźć się w pliku Skrot.cpp,
natomiast do pliku nagłówkowego Skrot.h należy dodać jej deklarację. Nie zawiera ona zasadniczo nowych elementów. Jeszcze raz wykorzystujemy obiekt identyfikowany przez stałą CLSID_ShellLink i interfejsy IShellLink oraz IPersistFile.
Rozdział 4. ♦ Systemy plików, multimediai inne funkcje WinAPI Listing 4.5. Definicja funkcji CzytajSkrot w wersji dla Win32 BOOL CzytajSkrot(LPCTSTR sciezkaLinku, CParametrySkrotu& parametrySkrotu) { CoInitializeEx(NULL, COINIT_MULTITHREADED); IShellLink* pISLink; if (CoCreateInstance (CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_IShellLink, (void**) &pISLink) != S_OK) return FALSE; IPersistFile* pIPFile; pISLink->QueryInterface(IID_IPersistFile,(void**) &pIPFile); if (pIPFile->Load(sciezkaLinku, 0) != S_OK) { pISLink->Release(); return FALSE; } TCHAR cstr[MAX_PATH]; WIN32_FIND_DATA informacjeOPliku; //tu nie wykorzystywane pISLink->GetPath(parametrySkrotu.sciezkaPliku, MAX_PATH, &informacjeOPliku, ´SLGP_UNCPRIORITY); pISLink->GetWorkingDirectory(parametrySkrotu.katalogRoboczy , MAX_PATH); pISLink->GetArguments(cstr, MAX_PATH); wcscpy_s(parametrySkrotu.argumenty,260,cstr); pISLink->GetDescription(cstr, MAX_PATH); wcscpy_s(parametrySkrotu.opis,260,cstr); pISLink->GetShowCmd(&(parametrySkrotu.rodzajOkna)); pISLink->GetIconLocation(parametrySkrotu.sciezkaIkony, MAX_PATH, ´&(parametrySkrotu.numerIkony)); WORD klawiszSkrotu; pISLink->GetHotkey(&klawiszSkrotu); parametrySkrotu.klawiszSkrotu = (klawiszSkrotu & 255); pISLink->Release(); }
return TRUE;
2. Aby przetestować funkcję CzytajSkrot, umieszczamy na podglądzie formy
kolejny przycisk i tworzymy jego domyślną metodę zdarzeniową. Umieszczamy w niej polecenia z listingu 4.6. Listing 4.6. Ograniczymy się do zaprezentowania parametrów skrótu w oknie komunikatu void CPlikSkrotuDlg::OnBnClickedButton2() { CParametrySkrotu parametrySkrotu; if (CzytajSkrot(L"Skrot.lnk", parametrySkrotu))
111
112
Visual C++. Gotowe rozwiązania dla programistów Windows { CString temp; temp.Format(L"Informacje o pliku skrótu\nSciezka pliku: %s\nArgumenty: ´%s,\nKatalog roboczy: %s\nOpis: %s\nIkona: %s (nr ikony: %d)\nKlawisz ´skrótu: %c", parametrySkrotu.sciezkaPliku, parametrySkrotu.argumenty, parametrySkrotu.katalogRoboczy, parametrySkrotu.opis, parametrySkrotu.sciezkaIkony, parametrySkrotu.numerIkony, parametrySkrotu.klawiszSkrotu); AfxMessageBox(temp); } }
3. Po uruchomieniu aplikacji możemy kliknąć nowy przycisk. Powinniśmy wówczas
zobaczyć opis skrótu jak na rysunku 4.2. Rysunek 4.2. Odczytane z pliku parametry skrótu
Umieszczenie skrótu na pulpicie Aby umieścić skrót na pulpicie, wystarczy połączyć wiedzę z dwóch pierwszych projektów w rozdziale. Listing 4.7 pokazuje, jak to zrobić. Zupełnie analogicznie wyglądałoby umieszczenie skrótu np. w menu Start. Listing 4.7. Wykorzystujemy funkcję TworzSkrot, wskazując ścieżkę pliku skonstruowaną za pomocą funkcji Katalog_Pulpit void CPlikSkrotuDlg::TworzSkrotNaPulpicie(LPCTSTR sciezkaLinku, CParametrySkrotu ´parametrySkrotu) { CString temp; temp.Format(L"%s\\%s", Katalog_Pulpit(), sciezkaLinku); TworzSkrot(temp, parametrySkrotu); }
Rozdział 4. ♦ Systemy plików, multimediai inne funkcje WinAPI
113
Operacje na plikach i katalogach (funkcje WinAPI) Użytkownik ma do wyboru trzy sposoby, za pomocą których może wykonywać operacje na plikach. Po pierwsze, może wykorzystać standardowe funkcje C++ (ten sposób omówiono niżej). Po drugie, może użyć zbioru funkcji WinAPI (CopyFile, MoveFile, DeleteFile itp.). Te niestety zostały dodane do WinAPI dopiero od Windows 2000, co oczywiście ogranicza przenośność korzystających z nich aplikacji. I po trzecie, użytkownik może użyć funkcji powłoki o nazwie SHFileOperation, która pozwala między innymi na operacje na grupach plików, całych katalogach, czy na przeniesienie pliku do kosza. Ten ostatni sposób zostanie omówiony w następnych projektach. Wybór między pierwszym i drugim sposobem nie jest rozłączny; nie wszystkie operacje da się łatwo wykonać za pomocą funkcji C++. Nie ma na przykład gotowej funkcji pozwalającej na kopiowanie pliku. Do tego koniecznie trzeba użyć funkcji WinAPI, np. CopyFile(L"D:\\TMP\\Log.txt",L"D:\\TMP\\Log.bak",FALSE)
lub bardziej złożonej konstrukcji: if (!CopyFile(L"D:\\TMP\\Log.txt",L"D:\\TMP\\Log.bak",FALSE)) ::MessageBox(NULL,L"Operacja kopiowania nie powiodła się!",L"Błąd ",MB_OK); else ::MessageBox(NULL,L"Kopiowanie zakończone",L"Informacja",MB_OK);
Jak łatwo się domyślić, pierwsze dwa argumenty wskazują nazwę pliku źródłowego i nową nazwę pliku. Natomiast trzeci argument to wartość logiczna określająca, czy możliwe jest nadpisywanie istniejącego pliku. Funkcja ta pozwala kopiować także katalogi razem z zawartością i podkatalogami. Istnieje również funkcja CopyFileEx, pozwalająca na śledzenie postępu kopiowania pliku, który można np. pokazać na pasku postępu. Podobnie działa funkcja MoveFile (także z WinAPI), przenosząca plik (zmieniająca jego położenie w tablicy alokacji plików): MoveFile(L"D:\\TMP\\Log.txt",L"D:\\TMP\\Log_nowy.txt");
Funkcja MoveFileWithProgress pozwala na śledzenie postępu przenoszenia pliku, a także na ustalenie sposobu przenoszenia (np. opóźnienie do momentu ponownego uruchamiania). Ta ostatnia możliwość dostępna jest także w funkcji MoveFileEx. Aby usunąć plik, można skorzystać z funkcji DeleteFile: DeleteFile(L"D:\\TMP\\Log.txt");
Podobnie wygląda sprawa z katalogami. Do tworzenia katalogu można użyć funkcji C++ mkdir lub funkcji WinAPI CreateDirectory. Ta ostatnia dołączona została jednak dopiero w Windows 2000. Do zmiany bieżącego katalogu można użyć funkcji chdir, natomiast do usuwania pustego katalogu — rmdir lub wspomnianej już funkcji DeleteFile. Funkcje te są zadeklarowane w nagłówku dir.h i „od zawsze” należą do standardu C++. Nie należy się jednak obawiać używania ich w 32-bitowych wersjach Windows, gdyż obecnie są one po prostu „nakładkami” na analogiczne funkcje WinAPI. Do pobrania ścieżki bieżącego katalogu można użyć funkcji GetCurrentDirectory.
114
Visual C++. Gotowe rozwiązania dla programistów Windows
Operacje na plikach i katalogach (funkcje powłoki) W poprzednim projekcie użyliśmy niskopoziomowych funkcji interfejsu programistycznego Windows (WinAPI) CopyFile, MoveFile czy DeleteFile do wykonywania podstawowych operacji na plikach. Chciałbym jednak zwrócić uwagę Czytelnika na inny sposób wykonania tych operacji, który może wydawać się z początku nieco bardziej skomplikowany, ale za to daje dodatkowe korzyści i przy wykonywaniu bardziej złożonych operacji okazuje się o wiele prostszy niż korzystanie z funkcji niskopoziomowych. Użyjemy do tego funkcji WinAPI SHFileOperation z biblioteki shell32.dll. Jedną z ich zalet jest to, że dostępne są już od Windows 95 i NT 4.0, czyli we wszystkich 32-bitowych wersjach Windows. Biblioteka ta wykorzystywana jest m.in. przez Eksploratora Windows i dlatego funkcje odwołują się do mechanizmów charakterystycznych dla eksploratora, m.in. kosza systemowego czy katalogów specjalnych. W odróżnieniu od poprzednio użytych funkcji niskopoziomowych funkcja SHFileOperation należy do warstwy powłoki (interfejsu graficznego) i dlatego jej działaniu towarzyszą okna żądające potwierdzenia chęci utworzenia katalogu, ostrzegające przed nadpisaniem pliku itp. 1. Tworzymy nowy projekt aplikacji MFC z oknem dialogowym o nazwie
OperacjeNaPlikach. 2. W pliku nagłówkowym OperacjeNaPlikachDlg.h importujemy potrzebny moduł: #include
3. W tym samym pliku deklarujmy również funkcje składowe: BOOL KopiowaniePliku(HWND uchwyt, LPCTSTR szZrodlo, LPCTSTR szCel); BOOL PrzenoszeniePliku(HWND uchwyt, LPCTSTR szZrodlo, LPCTSTR szCel); BOOL UsuwaniePliku(HWND uchwyt, LPCTSTR szZrodlo);
4. Przejdźmy do pliku źródłowego i zdefiniujmy funkcję pomocniczą OperacjaNaPliku, dzięki której korzystanie z SHFileOperation będzie
łatwiejsze (listing 4.8). Listing 4.8. Funkcja „prywatna” pozwalająca na uniknięcie powtarzania kodu BOOL COperacjeNaPlikachDlg::OperacjaNaPliku(HWND uchwyt, LPCTSTR szZrodlo, ´LPCTSTR szCel, DWORD operacja, DWORD opcje) { if(!PathFileExists(szZrodlo)) // Czy plik źródłowy istnieje? { AfxMessageBox(L"Nie odnaleziono pliku"); return FALSE; } SHFILEOPSTRUCT parametryOperacji; parametryOperacji.hwnd = uchwyt; parametryOperacji.wFunc = operacja; if (szZrodlo != L"") parametryOperacji.pFrom = szZrodlo; else parametryOperacji.pFrom = NULL; if (szCel != L"") parametryOperacji.pTo = szCel;
Rozdział 4. ♦ Systemy plików, multimediai inne funkcje WinAPI
115
else { parametryOperacji.pTo = NULL; parametryOperacji.fFlags = opcje; parametryOperacji.hNameMappings = NULL; parametryOperacji.lpszProgressTitle = NULL; } return (SHFileOperation(¶metryOperacji) == 0); }
5. Wreszcie definiujemy zadeklarowany w pliku nagłówkowym zestaw metod
(zadeklarowaliśmy je w nagłówku) wykonujących konkretne czynności na plikach (listing 4.9). Listing 4.9. Zbiór funkcji „publicznych” BOOL COperacjeNaPlikachDlg::KopiowaniePliku(HWND uchwyt, LPCTSTR szZrodlo, ´LPCTSTR szCel) { TCHAR lpBuffer[MAX_PATH] = {0}; GetFullPathName(szZrodlo, MAX_PATH, lpBuffer, NULL); return OperacjaNaPliku(uchwyt, lpBuffer, szCel, FO_COPY, 0); } BOOL COperacjeNaPlikachDlg::PrzenoszeniePliku(HWND uchwyt, LPCTSTR szZrodlo, ´LPCTSTR szCel) { TCHAR lpBuffer[MAX_PATH] = {0}; GetFullPathName(szZrodlo, MAX_PATH, lpBuffer, NULL); return OperacjaNaPliku(uchwyt, lpBuffer, szCel, FO_MOVE, 0); } BOOL COperacjeNaPlikachDlg::UsuwaniePliku(HWND uchwyt, LPCTSTR szZrodlo) { TCHAR lpBuffer[MAX_PATH] = {0}; GetFullPathName(szZrodlo, MAX_PATH, lpBuffer, NULL); return OperacjaNaPliku(uchwyt, lpBuffer, L"", FO_DELETE, 0); }
6. Listing 4.10 zawiera domyślną metodę zdarzeniową kliknięcia przycisku Button1, która testuje działanie powyższych funkcji. Listing 4.10. Aby poniższy test zadziałał, musimy dysponować dyskiem d:. Można to oczywiście łatwo zmienić void COperacjeNaPlikachDlg::OnBnClickedButton1() { TCHAR path[MAX_PATH] = {0}; GetModuleFileName(GetModuleHandle(NULL), path, MAX_PATH); KopiowaniePliku(m_hWnd, path, L"d:\\Kopia projektu.sln"); PrzenoszeniePliku(m_hWnd, L"d:\\Kopia projektu.sln", L"d:\\Kopia pliku.xml"); UsuwaniePliku(m_hWnd, L"d:\\Kopia pliku.xml"); }
116
Visual C++. Gotowe rozwiązania dla programistów Windows
Operacje na plikach i katalogach w Windows Vista (interfejs IFileOperation) W systemie Windows Vista oddano do użytku interfejs IFileOperation, który zastępuje opisaną wcześniej strukturę SHFILEOPSTRUCT. Nie oznacza to jednak, iż nie możemy już korzystać z tej struktury w nowych wersjach Windows. Interfejs IFileOperation wydaje się jednak wygodniejszy w użyciu. Ułatwia śledzenie postępu wykonywanych operacji oraz zapewnia możliwość wykonywania wielu operacji jednocześnie, a także bardziej szczegółowo informuje o błędach. W celu użycia funkcji udostępnianych przez interfejs IFileOperation należy korzystać z obiektu IShellItem przy określeniu ścieżki do plików i katalogów. Dzięki temu można wykonywać operacje nie tylko na plikach i katalogach, ale również na takich obiektach, jak foldery wirtualne. Przykład wykorzystania omawianego interfejsu przedstawiają poniższe projekty. 1. Tworzymy nowy projekt aplikacji MFC z oknem dialogowym o nazwie
OperacjeNaPlikachIF. 2. Na początku pliku OperacjeNaPlikachIFDlg.cpp umieszczamy trzy dyrektywy: #include #include #include
3. Natomiast w pliku nagłówkowym deklarujemy prywatną funkcję OperacjaNaPlikuIF i definiujemy ją zgodnie z listingiem 4.11. Listing 4.11. Funkcja wykonująca operacje na pliku (kopiowanie, przenoszenie, usuwanie) wykorzystująca interfejs IFileOperation HRESULT COperacjeNaPlikachIFDlg::OperacjaNaPlikuIF(LPCTSTR szZrodlo, LPCTSTR szCel, ´DWORD operacja, DWORD opcje) { if(!PathFileExists(szZrodlo)) // Czy plik źródłowy istnieje? { AfxMessageBox(L"Nie odnaleziono pliku"); return E_POINTER; } CT2W wszZrodlo(szZrodlo); CT2W wszCel(szCel); CString wszNowaNazwa = PathFindFileNameW(wszCel); PathRemoveFileSpec(wszCel); // Tworzenie nowej instancji interfejsu IFileOperation IFileOperation *iFo; HRESULT hr = CoCreateInstance(CLSID_FileOperation, NULL, CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&iFo)); if(!SUCCEEDED(hr)) return hr; iFo->SetOperationFlags(opcje); // Tworzenie obiektów IShellItem
Rozdział 4. ♦ Systemy plików, multimediai inne funkcje WinAPI
117
IShellItem *psiCel = NULL, *psiZrodlo = NULL; SHCreateItemFromParsingName(wszZrodlo, NULL, IID_PPV_ARGS(&psiZrodlo)); if(wszCel != NULL) SHCreateItemFromParsingName(wszCel, NULL, IID_PPV_ARGS(&psiCel)); // Kopiowanie, przenoszenie czy usuwanie? switch(operacja) { case FO_COPY: iFo->CopyItem(psiZrodlo, psiCel, wszNowaNazwa, NULL); break; case FO_MOVE: iFo->MoveItem(psiZrodlo, psiCel, wszNowaNazwa, NULL); break; case FO_DELETE: iFo->DeleteItem(psiZrodlo, NULL); break; } // Potwierdzenie wykonania operacji hr = iFo->PerformOperations(); if(!SUCCEEDED(hr)) return hr; psiZrodlo->Release(); if(psiCel != NULL) psiCel->Release(); // Zwolnienie interfejsu iFo->Release(); }
return hr;
4. Analogicznie jak w poprzednim projekcie deklarujemy i definiujemy publiczne
metody odpowiedzialne za kopiowanie, przenoszenie i usuwanie plików (listing 4.12). Jednakże teraz zamiast zwracać wartość BOOL, zwracamy HRESULT. Listing 4.12. Metody realizujące kopiowanie, przenoszenie i usuwanie plików HRESULT COperacjeNaPlikachIFDlg::KopiowaniePliku(LPCTSTR szZrodlo, LPCTSTR szCel) { TCHAR lpBuffer[MAX_PATH] = {0}; GetFullPathName(szZrodlo, MAX_PATH, lpBuffer, NULL); }
return OperacjaNaPlikuIF(lpBuffer, szCel, FO_COPY, 0);
HRESULT COperacjeNaPlikachIFDlg::PrzenoszeniePliku(LPCTSTR szZrodlo, LPCTSTR szCel) { TCHAR lpBuffer[MAX_PATH] = {0}; GetFullPathName(szZrodlo, MAX_PATH, lpBuffer, NULL); }
return OperacjaNaPlikuIF(lpBuffer, szCel, FO_MOVE, 0);
HRESULT COperacjeNaPlikachIFDlg::UsuwaniePliku(LPCTSTR szZrodlo) { TCHAR lpBuffer[MAX_PATH] = {0}; GetFullPathName(szZrodlo, MAX_PATH, lpBuffer, NULL); }
return OperacjaNaPlikuIF(lpBuffer, NULL, FO_DELETE, 0);
118
Visual C++. Gotowe rozwiązania dla programistów Windows 5. W podglądzie okna umieszczamy przycisk i tworzymy jego domyślną metodę
zdarzeniową, w której umieszczamy wywołanie funkcji z listingu 4.13. Listing 4.13. Na potrzeby naszego przykładu tworzymy plik test.txt, który wykorzystujemy do testowania funkcji opartych na interfejsie IFileOperation CString sciezkaPliku = L"d:\\test.txt"; HANDLE hFile = CreateFileW(sciezkaPliku, GENERIC_READ | GENERIC_WRITE, 0, NULL, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, NULL); CloseHandle(hFile); KopiowaniePliku(sciezkaPliku, L"d:\\test kopia.txt"); PrzenoszeniePliku(L"d:\\test kopia.txt", L"d:\\kopia xml.xml"); UsuwaniePliku(L"d:\\kopia xml.xml");
Czytelnik powinien zauważyć, że w przypadku kopiowania większych plików pojawia się teraz okno dialogowe, charakterystyczne dla systemu Windows Vista, informujące o postępie kopiowania (rysunek 4.3). Jeśli korzystamy ze struktury SHFILEOPSTRUCT, to nie mamy dostępu do własności specyficznych dla Visty. Rysunek 4.3. Kopiowanie elementu za pomocą IFileOperation
Przy usuwaniu pliku (funkcja UsuwaniePliku) pojawi się okno dialogowe z prośbą o potwierdzenie operacji. Jeżeli nie jest ono pożądane, należy w ciele funkcji Usuwanie ´Pliku, w ostatnim argumencie funkcji OperacjaNaPlikuIF, zamiast zera użyć stałej FOF_NOCONFIRMATION (listing 4.14). Poza bardzo szczególnymi sytuacjami nie jest to jednak rozwiązanie godne polecenia. Listing 4.14. Usuwanie pliku bez konieczności potwierdzenia HRESULT COperacjeNaPlikachIFDlg::UsuwaniePlikuBezPotwierdzenia(LPCTSTR szZrodlo) { TCHAR lpBuffer[MAX_PATH] = {0}; GetFullPathName(szZrodlo, MAX_PATH, lpBuffer, NULL); }
return OperacjaNaPlikuIF(lpBuffer, NULL, FO_DELETE, FOF_NOCONFIRMATION);
Jak usunąć plik, umieszczając go w koszu? Listing 4.15 zawiera funkcję, która różni się od funkcji UsuwaniePliku z poprzedniego projektu jednym szczegółem. Użyta została opcja FOF_ALLOWUNDO, która nakazuje przeniesienie pliku do kosza zamiast usunięcia.
Rozdział 4. ♦ Systemy plików, multimediai inne funkcje WinAPI
119
Listing 4.15. Usuwanie pliku z wykorzystaniem mechanizmu powłoki kosza systemowego HRESULT COperacjeNaPlikachIFDlg::UsuwaniePlikuDoKosza(LPCTSTR szZrodlo) { TCHAR lpBuffer[MAX_PATH] = {0}; GetFullPathName(szZrodlo, MAX_PATH, lpBuffer, NULL); return OperacjaNaPlikuIF(lpBuffer, NULL, FO_DELETE, FOF_ALLOWUNDO); }
Teraz przed skasowaniem pliku wyświetlone zostanie okno dialogowe z pytaniem o umieszczenie pliku w koszu. Tej samej stałej można użyć w metodzie OperacjeNaPliku w rozwiązaniu nie korzystającym z interfejsu IFileOperation.
Operacje na całym katalogu Kopiowanie całego katalogu z podkatalogami jest również możliwe i równie łatwe jak kasowanie pliku. W przypadku funkcji WinAPI i funkcji C++, które poznaliśmy wcześniej, programowanie tej operacji jest możliwe, ale wszelkie informacje prezentowane w trakcie użytkownikowi wymagałyby sporej dodatkowej pracy. A gdy korzysta się z funkcji powłoki, wystarczy jedno polecenie. W listingu 4.16 zaprezentowane są funkcje dla Windows Vista, tj. korzystające z metody OperacjeNaPlikuIF, ale analogiczne polecenia można bez problemu przygotować dla wcześniejszej metody OperacjeNaPliku. Listing 4.16. Zestaw funkcji służących do kopiowania, przenoszenia i usuwania katalogów HRESULT COperacjeNaPlikachIFDlg::KopiowanieKatalogu(LPCTSTR szZrodlo, LPCTSTR szCel) { return OperacjaNaPlikuIF(szZrodlo, szCel, FO_COPY, FOF_NOCONFIRMMKDIR); } HRESULT COperacjeNaPlikachIFDlg::PrzenoszenieKatalogu(LPCTSTR szZrodlo, LPCTSTR szCel) { HRESULT wynik = OperacjaNaPlikuIF(szZrodlo, szCel, FO_MOVE, FOF_NOCONFIRMMKDIR); RemoveDirectory(szZrodlo); return wynik; } HRESULT COperacjeNaPlikachIFDlg::UsuwanieKatalogu(LPCTSTR szZrodlo) { return UsuwaniePliku(szZrodlo); } HRESULT COperacjeNaPlikachIFDlg::UsuwanieKataloguDoKosza(LPCTSTR szZrodlo) { return UsuwaniePlikuDoKosza(szZrodlo); }
Aby utworzyć katalog bez dialogu potwierdzenia, należy użyć opcji FOF_NOCONFIRMMKDIR, a nie FOF_NOCONFIRMATION. Ważną opcją jest FOF_FILESONLY. Powoduje ona, że operacje są wykonywane jedynie na plikach pasujących do użytej maski. Wówczas nie zostaną utworzone podkatalogi.
120
Visual C++. Gotowe rozwiązania dla programistów Windows
W przypadku dwóch funkcji usuwających katalog utworzyliśmy tak naprawdę alias do funkcji usuwających pliki. Okazuje się, że działają one równie dobrze dla pojedynczych plików, jak i dla katalogów. Argumentami wszystkich funkcji powinny być oczywiście ścieżki do katalogów, np. KopiowanieKatalogu("d:\\","d:\\Kopia projektu"); UsuwanieKataloguDoKosza("d:\\Kopia projektu");
Jak wspomniałem wcześniej, analogiczne metody można przygotować, korzystając z funkcji z projektu opartego na metodzie OperacjeNaPliku.
Odczytywanie wersji pliku .exe i .dll Pliki wykonywalne .exe oraz pliki bibliotek .dll mogą zawierać informacje o wersji, producencie, prawach autorskich itp.6 O ile zapisywanie tych informacji w Visual Studio jest proste (pozwalają na to ustawienia projektu), o tyle przy ich odczycie trzeba się trochę pomęczyć. 1. Tworzymy nowy projekt aplikacji MFC z oknem dialogowym o nazwie
OdczytywanieWersji. 2. Klikając dwukrotnie plik OdczytywanieWersji.rc w podoknie Solution Explorer,
otwieramy podokno Resource View — zasoby projektu. W gałęzi Version znajduje się pozycja VS_VERSION_INFO [Angielski (Stany Zjednoczone)]. Klikając ją dwukrotnie, otworzymy edytor (rysunek 4.4), który pozwala na zmianę jej wszystkich ustawień.
Rysunek 4.4. Edytor wersji umieszczanej w zasobach aplikacji 6
Windows pozwala na oglądanie tych informacji w oknie właściwości pliku, na zakładce Wersja.
Rozdział 4. ♦ Systemy plików, multimediai inne funkcje WinAPI
121
3. Po powrocie do widoku projektowania umieszczamy na podglądzie formy dwa
pola edycyjne oraz przycisk (rysunek 4.5). Z polami edycyjnymi należy związać zmienne Edit1 i Edit2.
Rysunek 4.5. Informacje wyświetlane przez nasz program oraz w oknie właściwości Windows Vista 4. Własność MultiLine drugiego pola edycyjnego ustawiamy na True. 5. Definiujemy metodę PobierzInformacjeOPliku zgodnie z listingiem 4.17. Listing 4.17. Funkcja odczytująca informacje o wersji ze wskazanego pliku .exe lub .dll CString COdczytywanieWersjiDlg::PobierzInformacjeOPliku(LPCTSTR nazwaPliku) { CString Wynik, temp; DWORD uchwyt, rozmiarBufora; UINT rozmiarWartosci; LPTSTR lpBufor; VS_FIXEDFILEINFO *pInformacjeOPliku; rozmiarBufora = GetFileVersionInfoSize(nazwaPliku, &uchwyt); if (!rozmiarBufora) { AfxMessageBox(L"Brak informacji o wersji pliku"); return NULL; } lpBufor = (LPTSTR)malloc(rozmiarBufora); if (!lpBufor) return NULL; if(!GetFileVersionInfo(nazwaPliku, uchwyt, rozmiarBufora, lpBufor)) { free (lpBufor); return NULL; } if(VerQueryValue(lpBufor, L"\\", (LPVOID *)&pInformacjeOPliku, ´(PUINT)&rozmiarWartosci))
122
Visual C++. Gotowe rozwiązania dla programistów Windows { CString typPliku; switch (pInformacjeOPliku->dwFileType) { case VFT_UNKNOWN: typPliku="Nieznany"; break; case VFT_APP: typPliku="Aplikacja"; break; case VFT_DLL: typPliku="Biblioteka DLL"; break; case VFT_STATIC_LIB: typPliku="Biblioteka ładowana statycznie"; break; case VFT_DRV: switch (pInformacjeOPliku->dwFileSubtype) { case VFT2_UNKNOWN: typPliku="Nieznany rodzaj sterownika"; break; case VFT2_DRV_COMM: typPliku="Sterownik komunikacyjny"; break; case VFT2_DRV_PRINTER: typPliku="Sterownik drukarki"; break; case VFT2_DRV_KEYBOARD: typPliku="Sterownik klawiatury"; break; case VFT2_DRV_LANGUAGE: typPliku="Sterownik języka"; break; case VFT2_DRV_DISPLAY: typPliku="Sterownik karty graficznej"; break; case VFT2_DRV_MOUSE: typPliku="Sterownik myszy"; break; case VFT2_DRV_NETWORK: typPliku="Sterownik karty sieciowej"; break; case VFT2_DRV_SYSTEM: typPliku="Sterownik systemowy"; break; case VFT2_DRV_INSTALLABLE: typPliku="Sterownik do instalacji"; break; case VFT2_DRV_SOUND: typPliku="Sterownik karty dźwiękowej"; break; }; break; case VFT_FONT: switch (pInformacjeOPliku->dwFileSubtype) { case VFT2_UNKNOWN: typPliku="Unknown Font"; break; case VFT2_FONT_RASTER: typPliku="Raster Font"; break; case VFT2_FONT_VECTOR: typPliku="Vector Font"; break; case VFT2_FONT_TRUETYPE: typPliku="Truetype Font"; break; } break; case VFT_VXD: typPliku.Format(L"Virtual Defice Identifier = %04x", ´pInformacjeOPliku->dwFileSubtype); break; } Wynik.Append(L"Typ pliku:"); Wynik.Append(typPliku); Wynik.Append(L"\r\n"); } unsigned short jezyk, stronaKodowa; if (VerQueryValue(lpBufor, L"\\VarFileInfo\\Translation", (LPVOID ´*)&pInformacjeOPliku, (PUINT)&rozmiarWartosci)) { unsigned short* wartosc=(unsigned short*)pInformacjeOPliku; jezyk = *wartosc; stronaKodowa = *(wartosc+1); } else { jezyk = 0; stronaKodowa = 0; } temp.Format(L"Język: %d (0x%x)\r\n", jezyk, jezyk); Wynik.Append(temp); temp.Format(L"Strona kodowa: %d (0x%x)\r\n\r\n", stronaKodowa, stronaKodowa); Wynik.Append(temp);
Rozdział 4. ♦ Systemy plików, multimediai inne funkcje WinAPI
123
const CString InfoStr[12]= {L"Comments", L"InternalName", L"ProductName", ´L"CompanyName", L"LegalCopyright", L"ProductVersion", L"FileDescription", L"LegalTrademarks", ´L"PrivateBuild", L"FileVersion", L"OriginalFilename", L"SpecialBuild"};
}
for(int i=0;i=0?(long)((x)+0.5):(long)((x)-0.5)) #define ROUND1(x) (ROUND(10*x)/10.0) //zaokrąglenie z częścią dziesiętną #define ROUND2(x) (ROUND(100*x)/100.0) //zaokrąglenie z setnymi #define DI_MAX_LENGTH 256 struct DaneODysku { char literaDysku; BOOL czyDyskDostepny; int typDysku; TCHAR typDyskuOpis[DI_MAX_LENGTH]; unsigned __int64 calkowitaPrzestrzen; unsigned __int64 wolnaPrzestrzen; unsigned __int64 zajetaPrzestrzen; double wolnaPrzestrzenUlamek; unsigned char wolnaPrzestrzenProcenty; TCHAR ULONG TCHAR ULONG };
nazwaDysku[DI_MAX_LENGTH]; numerSeryjnyDysku; nazwaFAT[DI_MAX_LENGTH]; maksymalnaDlugoscPlikuLubKatalogu;
ULONG maksymalnaDlugoscSciezki;
BOOL PobierzInformacjeODysku(char literaDysku, DaneODysku& diskInfo);
4. Natomiast do pliku źródłowego InformacjeODysku.cpp dodajemy kod funkcji PobierzInformacjeODysku według listingu 4.20. Listing 4.20. Pełny kod pliku nagłówkowego #include "stdafx.h" #include "InformacjeODysku.h" BOOL PobierzInformacjeODysku(char literaDysku, DaneODysku &diskInfo) { diskInfo.literaDysku = toupper(literaDysku); //Ustalanie wstępnych wartości diskInfo.czyDyskDostepny = TRUE; diskInfo.typDysku = 0; wcscpy_s(diskInfo.typDyskuOpis,L""); diskInfo.calkowitaPrzestrzen = 0; diskInfo.wolnaPrzestrzen = 0; diskInfo.zajetaPrzestrzen = 0; diskInfo.wolnaPrzestrzenUlamek = 0;
Rozdział 4. ♦ Systemy plików, multimediai inne funkcje WinAPI
127
diskInfo.wolnaPrzestrzenProcenty = 0; wcscpy_s(diskInfo.nazwaDysku,L""); diskInfo.numerSeryjnyDysku = 0; wcscpy_s(diskInfo.nazwaFAT,L""); diskInfo.maksymalnaDlugoscPlikuLubKatalogu = 0; diskInfo.maksymalnaDlugoscSciezki = 0; //Ścieżka katalogu głównego na dysku TCHAR katalogGlownyDysku[4]; katalogGlownyDysku[0]=diskInfo.literaDysku; katalogGlownyDysku[1]='\0'; wcscat_s(katalogGlownyDysku,L":\\"); //Typ napędu (drive type) diskInfo.typDysku = GetDriveType(katalogGlownyDysku); switch(diskInfo.typDysku) { case 0: wcscpy_s(diskInfo.typDyskuOpis,L"Napęd nie istnieje"); diskInfo.czyDyskDostepny = FALSE; break; case 1: wcscpy_s(diskInfo.typDyskuOpis,L"Dysk nie jest sformatowany"); diskInfo.czyDyskDostepny = FALSE; break; case DRIVE_REMOVABLE: wcscpy_s(diskInfo.typDyskuOpis,L"Dysk wymienny"); break; case DRIVE_FIXED: wcscpy_s(diskInfo.typDyskuOpis,L"Dysk lokalny"); break; case DRIVE_REMOTE: wcscpy_s(diskInfo.typDyskuOpis,L"Dysk sieciowy"); break; case DRIVE_CDROM: wcscpy_s(diskInfo.typDyskuOpis,L"Płyta CDROM"); break; case DRIVE_RAMDISK: wcscpy_s(diskInfo.typDyskuOpis,L"RAM Drive"); break; default: wcscpy_s(diskInfo.typDyskuOpis,L"Typ dysku nierozpoznany"); break; } //Jeżeli dysk niedostępny, to kończymy if (!diskInfo.czyDyskDostepny) return FALSE; //Ilość wolnego miejsca na dysku (disk free space) //Typy argumentów niezgodne z Win32 SDK BOOL Wynik = ::GetDiskFreeSpaceEx( katalogGlownyDysku, NULL, (ULARGE_INTEGER*)&(diskInfo.calkowitaPrzestrzen), (ULARGE_INTEGER*)&(diskInfo.wolnaPrzestrzen)); diskInfo.zajetaPrzestrzen = diskInfo.calkowitaPrzestrzen diskInfo.wolnaPrzestrzen; if (Wynik && (diskInfo.calkowitaPrzestrzen != 0)) { diskInfo.wolnaPrzestrzenUlamek = diskInfo.wolnaPrzestrzen/ (double)diskInfo.calkowitaPrzestrzen; diskInfo.wolnaPrzestrzenProcenty = (unsigned char)ROUND(100 * diskInfo.wolnaPrzestrzenUlamek); } else {
128
Visual C++. Gotowe rozwiązania dla programistów Windows diskInfo.wolnaPrzestrzenUlamek = 0; diskInfo.wolnaPrzestrzenProcenty = 0; diskInfo.czyDyskDostepny = FALSE; return FALSE; } //Nazwa dysku, typ FAT, numer seryjny (GetVolumeInformation) unsigned long wlasnosciSystemuPlikow; Wynik = GetVolumeInformation(katalogGlownyDysku, diskInfo.nazwaDysku, DI_MAX_LENGTH, &(diskInfo.numerSeryjnyDysku), &(diskInfo.maksymalnaDlugoscPlikuLubKatalogu), &(wlasnosciSystemuPlikow), diskInfo.nazwaFAT, DI_MAX_LENGTH); diskInfo.maksymalnaDlugoscSciezki = MAX_PATH; return Wynik; }
5. Korzystanie z funkcji tego typu jest dość naturalne dla osób posługujących się
na co dzień WinAPI. Osoby programujące w C++ zapewne chętniej widziałyby klasę, która w konstruktorze pobierać będzie literę dysku i po utworzeniu udostępniać będzie informacje o dysku. Bez problemu możemy przekształcić powyższą strukturę w taką klasę, choć kosztem tego, że wszystkie jej pola są publiczne. Wystarczy do pliku nagłówkowego dodać definicję klasy InformacjeODysku widoczną na listingu 4.21. Może bardziej elegancko byłoby uczynić DaneODysku polem nowej klasy, a nie jej klasą bazową, ale na dłuższą metę rozwiązanie takie jest mniej wygodne. Listing 4.21. Dodajemy konstruktor inicjujący klasę udostępniającą informacje o dysku class InformacjeODysku : public DaneODysku { public: InformacjeODysku(char literaDysku='C') { this->literaDysku=literaDysku; PobierzInformacjeODysku(this->literaDysku,*this); } };
Omówię po kolei użyte w funkcji PobierzInformacjeODysku funkcje WinAPI: GetDriveType11 — zwracana przez nią wartość to liczba naturalna określająca typ na-
pędu. Zdefiniowane stałe kodujące poszczególne typy napędów są wymienione i opisane w instrukcji switch, następującej po wywołaniu tej funkcji. Nierozpoznawane
11
Dostępna w Windows 95/98/Me oraz NT/2000/XP.
Rozdział 4. ♦ Systemy plików, multimediai inne funkcje WinAPI
129
pozostawiamy typy stacji dyskietek (3.5" lub 5.25")12. Jedynym argumentem funkcji jest wskaźnik do łańcucha zawierającego ścieżkę do katalogu głównego dysku w badanym napędzie. GetDiskFreeSpaceEx — funkcja dostępna od wersji systemu Windows 95 OSR213, a więc
od wersji, która pozwalała na obsługę dysków większych niż 2 GB14 . Wcześniejsza wersja funkcji GetDiskFreeSpace zwraca niepoprawne wartości dla tak dużych dysków. Pierwszym argumentem, podobnie jak w poprzedniej funkcji i w większości funkcji związanych z dyskami, jest wskaźnik do łańcucha zawierającego ścieżkę katalogu głównego dysku. W kolejnych trzech podawane są wskaźniki do typu ULARGE_INTEGER, w których zapisana zostanie ilość wolnego miejsca dostępnego dla aplikacji wywołującej funkcję, całkowita ilość bajtów dostępna na dysku i całkowita ilość wolnych bajtów. Typ ULARGE_INTEGER jest zdefiniowany w WinAPI jako unia, która w zależności od używanego kompilatora może być parą dwóch liczb 32-bitowych lub jedną liczbą 64-bitową. Visual Studio zawiera typ unsigned __int64, który wykorzystujemy do przechowania zwracanych przez GetDiskFreeSpaceEx wartości, a więc dotyczy nas druga opcja unii. Konieczne jest jednak rzutowanie wskaźników z unsigned __int64 na ULARGE_INTEGER przy wywoływaniu funkcji. GetVolumeInformation15 to funkcja zwracająca informacje o systemie plików na dysku,
tj. nazwę dysku, numer seryjny, maksymalną długość nazwy katalogu lub pliku, typ FAT, informacje o kompresji itp. Dane pobieramy bezpośrednio do elementów struktury DaneODysku. Stała MAX_PATH przechowuje maksymalną długość ścieżki do pliku łącznie z jego nazwą z rozszerzeniem. Zapisujemy tę wartość w polu maksymalnaDlugoscSciezki struktury. Jest ona własnością systemu, a nie dysku, jest więc identyczna dla każdego dysku.
Testy Najprostszy sposób przetestowania powyższej funkcji PobierzInformacjeODysku (ewentualnie klasy InformacjeODysku) polega na umieszczeniu w oknie przycisku, z którego wywołujemy funkcję widoczną na listingu 4.22. Listing 4.22. Funkcja wyświetlająca informacje o dysku void WyswietlInformacje(char literaDysku) { InformacjeODysku informacjeODysku(literaDysku); if(!informacjeODysku.czyDyskDostepny) 12
Można je oczywiście, nieco nieelegancko, rozpoznać, korzystając z objętości dyskietki. W takiej postaci program nie będzie działał w „czystym” Windows 95. Można temu zaradzić, sprawdzając wersję systemu za pomocą GetVersionEx i w tym systemie wywołując starszą wersję funkcji — system i tak nie obsługuje większych dysków. 14 Łatwo sprawdzić, że 2 giga = 2 • 1024 mega = 2 • 1024 • 1024 kilo = 2 • 1024 • 1024 • 1024 = 2147483648; to więcej niż zakres 32-bitowej liczby całkowitej long (ze znakiem). Konieczne więc jest użycie liczb 64-bitowych typu __int64. I tu należy uważać, żeby przypadkowo nie przeprowadzić konwersji na liczby 32-bitowe przez przypisywanie lub operacje na zmiennych. 15 Dostępna w Windows 95/98/Me oraz NT/2000/XP. 13
130
Visual C++. Gotowe rozwiązania dla programistów Windows { MessageBox(NULL,L"Dysk nie jest dostępny",L"Ostrzeżenie",MB_ICONWARNING); return; } CString komunikat; komunikat.Format(L"Informacje o dysku\nNazwa: %s\nTyp dysku: %s\nTyp FAT: ´%s\nWielkość dysku: %.2f GB\nIlość wolnego miejsca: %.0f %%", informacjeODysku.nazwaDysku, informacjeODysku.typDyskuOpis, informacjeODysku.nazwaFAT, informacjeODysku.calkowitaPrzestrzen/1024.0/1024.0/1024.0, 100*informacjeODysku.wolnaPrzestrzenUlamek ); MessageBox(NULL,komunikat,L"Informacja o dysku",MB_OK); }
Wyświetla ona okno z komunikatem zawierającym podstawowe informacje o dysku lub informacje, że dysk jest niedostępny, jeżeli podamy złą literę dysku16. Możemy jednak pójść o krok dalej — przygotujmy projekt okna z listą CListBox (rysunek 4.6) i wyświetlmy na niej informacje o wszystkich dostępnych w komputerze dyskach logicznych, zarówno lokalnych, jak i sieciowych. 1. Przechodzimy do widoku projektowania okna. 2. Na formie umieszczamy kontrolkę CListBox, z którą wiążemy zmienną ListBox1. 3. W metodzie OnInitDialog umieszczamy polecenia z listingu 4.23. Listing 4.23. Sprawdzamy po prostu kolejno wszystkie litery od C: do Z: BOOL CDiskInfoDlg::OnInitDialog() { CDialog::OnInitDialog(); // Set the icon for this dialog. The framework does this automatically // when the application's main window is not a dialog SetIcon(m_hIcon, TRUE); // Set big icon SetIcon(m_hIcon, FALSE); // Set small icon const int BwGB=1024*1024*1024; //ilość bajtów w GB for(char litera = 'c'; litera SetPos(pozycjaPaska); } DaneODysku PanelZInformacjamiODysku::GetDaneODysku() { return daneODysku; } char PanelZInformacjamiODysku::GetLiteraDysku() { return literaDysku; } // PanelZInformacjamiODysku message handlers
7. Pole obiektIstnieje wykorzystaliśmy na końcu metody SetLiteraDysku. Jeżeli
obiekt już powstał, np. gdy jawnie wywołał tę metodę użytkownik, aktualizowana jest pozycja paska postępu. W konstruktorze pozycja nie zostanie ustawiona. Musimy to zrobić po powstaniu obiektu. Wykorzystamy do tego pierwsze odświeżenie kontrolki, tj. pierwsze wywołanie metody związanej z otrzymaniem komunikatu WM_PAINT18. Będzie to o tyle wygodne, że w metodzie tej przygotujemy także dodatkowy opis umieszczony na kontrolce. a) W pliku PanelZInformacjamiODysku.cpp do makr mapujących komunikaty dodajemy wywołanie makra wiążącego komunikat WM_PAINT z metodą OnPaint: BEGIN_MESSAGE_MAP(PanelZInformacjamiODysku, CProgressCtrl) ON_WM_PAINT() END_MESSAGE_MAP()
18
Użycie komunikatu WM_CREATE w systemach wcześniejszych niż Windows Vista może powodować kłopoty, dlatego staram się go unikać.
Rozdział 4. ♦ Systemy plików, multimediai inne funkcje WinAPI
135
b) W pliku nagłówkowym PanelZInformacjamiODysku.h umieszczamy deklarację chronionej metody OnPaint: protected: void OnCreate();
c) Definiujemy tę metodę w pliku źródłowym zgodnie ze wzorem z listingu 4.26. Listing 4.26. Metoda uruchamiana po utworzeniu kontrolki void PanelZInformacjamiODysku::OnPaint() { //ustawianie pozycji paska przy pierwszym uruchamianiu if(!obiektIstnieje) { obiektIstnieje=true; this->SetPos(pozycjaPaska); } CClientDC dc(this); //device context for painting CRect rc, rcUpdate, rcProgressBar; CRgn rgn; GetUpdateRect(rcUpdate); CProgressCtrl::OnPaint(); rgn.CreateRectRgn(rcUpdate.left, rcUpdate.top, rcUpdate.right, rcUpdate.bottom); dc.SelectClipRgn(&rgn); GetClientRect(rc); dc.SetBkMode(TRANSPARENT); dc.SelectClipRgn(NULL); dc.DrawText(opisLewy, -1, rc, DT_SINGLELINE | DT_VCENTER | DT_LEFT); // lewy ´opis paska postępu dc.DrawText(opisPrawy, -1, rc, DT_SINGLELINE | DT_VCENTER | DT_RIGHT); // prawy ´opis paska postępu }
dc.SelectClipRgn(NULL);
8. Kontrolka jest gotowa. Przejdźmy teraz do okna dialogowego, aby wykorzystać
nowy komponent do prezentacji informacji o dyskach. Zacznijmy od usunięcia obecnych w oknie dwóch przycisków i etykiety. 9. Następnie w pliku DiskInfoKomponentDlg.cpp zadeklarujmy dostęp do plików
komponentu, dodając dyrektywę: #include "PanelZInformacjamiODysku.h"
10. Następnie w metodzie OnInitDialog utwórzmy i umieśćmy w oknie panele
(listing 4.27). Po umieszczeniu paneli dostosowujemy wielkość okna, tak żeby wszystkie dyski były widoczne. Listing 4.27. Dynamiczne tworzenie kontrolek CDiskInfoPanel BOOL CDiskInfoKomponentDlg::OnInitDialog() { CDialog::OnInitDialog();
136
Visual C++. Gotowe rozwiązania dla programistów Windows // Set the icon for this dialog. The framework does this automatically // when the application's main window is not a dialog SetIcon(m_hIcon, TRUE); // Set big icon SetIcon(m_hIcon, FALSE); // Set small icon //przygotowanie paneli const int przes = 10, wys = 30; int iloscPaneli = 0; CRect clientRect; GetClientRect(clientRect); PanelZInformacjamiODysku *panel; for(char litera = 'c'; litera Informacje.czyDyskDostepny) { panel->Create(WS_CHILD | WS_VISIBLE, clientRect, this, iloscPaneli); panel->MoveWindow(przes, przes + iloscPaneli * (wys + przes), ´clientRect.Width() - 2*przes, wys); iloscPaneli++; } else delete panel; } // Dopasowanie wysokości okna dialogowego do ilości paneli int newHeight = (iloscPaneli+1) * (wys + przes); SetWindowPos(NULL, clientRect.left, clientRect.bottom, clientRect.Width(), ´newHeight, SWP_SHOWWINDOW); return TRUE;
// return TRUE
unless you set the focus to a control
}
11. Kompilujemy projekt i uruchamiamy aplikację. Zobaczymy formę zapełnioną dynamicznie tworzonymi komponentami CDiskInfoPanel (rysunek 4.8).
Rysunek 4.8. Ostateczna postać aplikacji w Windows XP i Windows Vista
Stworzoną kontrolkę można osadzić na formie w sposób „statyczny”. W tym celu wystarczy przejść do widoku projektowania okna dialogowego projektu, do którego dołączone są pliki InformacjeODysku.h/.cpp oraz pliki kontrolki PanelZInformacjamiODysku.h/.cpp, i umieścić w tym oknie kontrolkę CProgressCtrl. Następnie należy związać z nią zmienną, której domyślny typ CProgressCtrl trzeba zmodyfikować na PanelZInformacjamiODysku.
Rozdział 4. ♦ Systemy plików, multimediai inne funkcje WinAPI
137
Typ ten można zresztą zmienić już po utworzeniu w deklaracji zmiennej w pliku nagłówkowym okna dialogowego. Kontrolka udostępnia własność LiteraDysku, której można przypisać literę dowolnego dysku (ze względu na domyślny argument konstruktora domyślna litera to C).
Ikona w obszarze powiadamiania (zasobniku) Powracamy na chwilę do funkcji powłoki, a dokładnie do jednej z nich, o nazwie Shell_NotifyIcon19. Pozwala ona na kontrolę ikon w obszarze powiadamiania (mowa o tym miejscu przy zegarze, które przy domyślnym ustawieniu pulpitu znajduje się z prawej strony paska zadań).
Funkcja Shell_NotifyIcon Do obsługi ikon umieszczanych w obszarze powiadamiania służy funkcja powłoki Shell_NotifyIcon. Przyjmuje ona dwa argumenty, z których pierwszy może mieć wartości NIM_ADD, NIM_MODIFY lub NIM_DELETE, oznaczające odpowiednio: dodanie, zmianę i usunięcie ikony z obszaru powiadamiania. Drugi to wskaźnik do struktury typu NOTIFYICONDATA zawierającej informacje o ikonie. Zacznijmy od umieszczenia ikony w zasobniku. Nie jest to zadanie trudne. Aby się o tym przekonać, możemy do projektu dodać przycisk i w jego metodzie zdarzeniowej umieścić wyróżnione w listingu polecenia. Klikając przycisk, dodamy do zasobnika ikonę aplikacji (tę, która używana jest w pasku zadań) z podpowiedzią o treści Podpowiedź. 1. Tworzymy nowy projekt aplikacji MFC z oknem dialogowym o nazwie
IkonaWZasobniku. 2. Deklarujemy pole klasy CIkonaWZasobniku, w którym przechowywać będziemy
informację o ikonie: private: NOTIFYICONDATA informacjeOIkonie;
3. Następnie umieszczamy na oknie dialogowym przycisk. Klikając go dwukrotnie,
tworzymy domyślną metodę zdarzeniową i umieszczamy w niej polecenia widoczne na listingu 4.28. Listing 4.28. Najprostsze użycie funkcji Shell_NotifyIcon void CIkonaWZasobnikuDlg::OnBnClickedButton1() { informacjeOIkonie.cbSize = sizeof(informacjeOIkonie); informacjeOIkonie.hWnd = m_hWnd; wcscpy_s(informacjeOIkonie.szTip, L"Podpowiedź"); 19
Dostępna we wszystkich 32-bitowych wersjach Windows, poza NT 3.x.
138
Visual C++. Gotowe rozwiązania dla programistów Windows informacjeOIkonie.hIcon = LoadIcon(AfxGetInstanceHandle(), ´MAKEINTRESOURCE(IDR_MAINFRAME)); informacjeOIkonie.uID = 0; informacjeOIkonie.uFlags = NIF_ICON | NIF_TIP; Shell_NotifyIcon(NIM_ADD, &informacjeOIkonie); }
4. Kładziemy na formie drugi przycisk i umieszczamy w nim polecenie usuwające
ikonę z zasobnika (listing 4.29). Listing 4.29. Usuwanie z zasobnika ikony identyfikowanej przez strukturę informacjeOIkonie void CIkonaWZasobnikuDlg::OnBnClickedButton2() { Shell_NotifyIcon(NIM_DELETE, &informacjeOIkonie); }
Menu kontekstowe ikony Z ikoną mogą wiązać się pewne zdarzenia. W szczególności są to pojedyncze i podwójne kliknięcie lewym przyciskiem myszy czy kliknięcie prawym przyciskiem myszy. Konsekwencją tych zdarzeń jest przesłanie komunikatów do okna aplikacji20. Z kliknięciem prawym przyciskiem myszy zazwyczaj związane jest rozwinięcie menu kontekstowego. Na kliknięcia lewym przyciskiem zareagujemy tylko komunikatami. 1. Zacznijmy od utworzenia menu kontekstowego (kontynuujemy rozbudowę
poprzedniego projektu): a) Przechodzimy do edytora zasobów (podokno Resource View). b) Rozwijamy węzeł przy nazwie IkonaWZasobniku. c) Klikamy prawym przyciskiem myszy nazwę IkonaWZasobniku.rc i z menu
podręcznego wybieramy pozycję Add Resource…. d) Wybieramy menu (rysunek 4.9). Jego identyfikator powinien być równy IDR_MENU1. Rysunek 4.9. Dodajemy menu do naszego projektu
20
Pełny opis obsługi komunikatów Windows zawarto w rozdziale 6.
Rozdział 4. ♦ Systemy plików, multimediai inne funkcje WinAPI
139
e) Projektujemy menu według rysunku 4.10. Rysunek 4.10. Edytor menu Visual Studio
2. Modyfikujemy metodę OnBnClickedButton1 z listingu 4.28 zgodnie z listingiem 4.30. Listing 4.30. Włączenie obsługi komunikatów do funkcji tworzącej ikonę w zasobniku void CIkonaWZasobnikuDlg::OnBnClickedButton1() { informacjeOIkonie.cbSize = sizeof(informacjeOIkonie); informacjeOIkonie.hWnd = m_hWnd; wcscpy_s(informacjeOIkonie.szTip, L"Podpowiedź"); informacjeOIkonie.hIcon = LoadIcon(AfxGetInstanceHandle(), ´MAKEINTRESOURCE(IDR_MAINFRAME)); informacjeOIkonie.uID = 0; informacjeOIkonie.uFlags = NIF_TIP | NIF_ICON | NIF_MESSAGE; informacjeOIkonie.uVersion = NOTIFYICON_VERSION_4; informacjeOIkonie.uCallbackMessage = WM_USER + 1; Shell_NotifyIcon(NIM_ADD, &informacjeOIkonie); }
3. Wiążemy metodę zdarzeniową OnTrayClick z identyfikatorem WM_USER+1, który
jest używany przez system operacyjny do wysyłania powiadomień między ikoną w zasobniku a oknem o identyfikatorze informacjeOIkonie.hWnd. BEGIN_MESSAGE_MAP(CIkonaWZasobnikuDlg, CDialog) ON_WM_PAINT() ON_WM_QUERYDRAGICON() //}}AFX_MSG_MAP ON_BN_CLICKED(IDC_BUTTON1, &CIkonaWZasobnikuDlg::OnBnClickedButton1) ON_BN_CLICKED(IDC_BUTTON2, &CIkonaWZasobnikuDlg::OnBnClickedButton2) ON_MESSAGE(WM_USER + 1, &CIkonaWZasobnikuDlg::OnTrayClick) END_MESSAGE_MAP()
4. Dodajemy metodę onTrayClick i definiujemy ją według listingu 4.31. Listing 4.31. Obsługa komunikatów wysyłanych przez ikonę w zasobniku LRESULT CIkonaWZasobnikuDlg::OnTrayClick(WPARAM wParam, LPARAM lParam) { UINT uMsg = (UINT) lParam; switch (uMsg) { case WM_LBUTTONDBLCLK: AfxMessageBox(L"Dwukrotne kliknięcie"); break;
140
Visual C++. Gotowe rozwiązania dla programistów Windows case WM_LBUTTONDOWN: AfxMessageBox(L"Kliknięcie"); break; case WM_RBUTTONUP: CPoint pt; CMenu trayMenu; GetCursorPos(&pt); trayMenu.LoadMenuW(MAKEINTRESOURCE(IDR_MENU1)); trayMenu.GetSubMenu(0)->TrackPopupMenu(TPM_LEFTALIGN | TPM_RIGHTBUTTON, ´pt.x, pt.y, this); break; } return TRUE; }
Jak wynika z kodu metody OnTrayClick z listingu 4.31, kliknięcia lewym przyciskiem myszy powodują wywołanie metod pokazujących komunikaty, natomiast naciśnięcie prawego klawisza myszy powoduje załadowanie menu z zasobów aplikacji i pokazanie go przy bieżącej pozycji myszy (rysunek 4.11). Rysunek 4.11. Przykładowe menu związane z ikoną aplikacji w zasobniku
„Dymek” W nowszych wersjach Windows z ikonami w obszarze powiadamiania związany może być „dymek” (ang. balloon hint; rysunek 4.12). Łatwo możemy go utworzyć, modyfikując funkcję z listingu 4.28. W tym celu: Rysunek 4.12. „Dymek” to forma powiadamiania o zdarzeniach wymagających uwagi Czytelnika. Zaletą „dymku” jest to, że nie przejmuje „focusu” bieżącej aplikacji
Dodajemy do naszej formy kolejny przycisk. Tworzymy jego domyślną metodę zdarzeniową i definiujemy zgodnie z listingiem 4.32. Listing 4.32. Wywołanie „dymka” związanego z ikoną umieszczoną w zasobniku void CIkonaWZasobnikuDlg::OnBnClickedButton3() { informacjeOIkonie.cbSize = sizeof(informacjeOIkonie); informacjeOIkonie.hWnd = m_hWnd; wcscpy_s(informacjeOIkonie.szInfoTitle, L"Dymek");
Rozdział 4. ♦ Systemy plików, multimediai inne funkcje WinAPI
141
wcscpy_s(informacjeOIkonie.szInfo, L"Informacja umieszczona w dymku"); informacjeOIkonie.hIcon = LoadIcon(AfxGetInstanceHandle(), ´MAKEINTRESOURCE(IDR_MAINFRAME)); informacjeOIkonie.dwInfoFlags = NIIF_INFO; informacjeOIkonie.uID = 0; informacjeOIkonie.uFlags = NIF_INFO | NIF_ICON; Shell_NotifyIcon(NIM_MODIFY, &informacjeOIkonie); }
Metoda jest tak napisana, że pokazuje dymek przy istniejącej ikonie. Jeżeli chcemy pokazać dymek, tworząc jednocześnie ikonę, wystarczy pierwszy argument funkcji Shell_NotifyIcon zmienić na NIM_ADD.
Multimedia (CD-Audio, MCI) Od razu uprzedzam, że podrozdział poświęcony bibliotece MCI (ang. Media Control Interface), a więc podzbiorowi funkcji WinAPI służącemu do obsługi urządzeń multimedialnych, jest daleki od kompletności. Zaletą tej biblioteki jest to, że udostępnia funkcje, które pozwalają sterować wszystkimi urządzeniami multimedialnymi. W poniższych projektach skupimy naszą uwagę na obsłudze napędów CD/DVD, odtwarzaniu muzyki z płyt CD-Audio oraz kontroli poziomu głośności.
Aby wysunąć lub wsunąć tackę w napędzie CD lub DVD Aby otworzyć lub zamknąć domyślny napęd CD-Audio, można użyć poleceń (zadeklarowane są w nagłówku Mmsystem.h): mciSendString(L"set cdaudio door open wait", NULL, 0, 0); mciSendString(L"set cdaudio door closed wait", NULL, 0, 0);
Ten prosty sposób nie pomoże nam jednak, gdy zechcemy wysunąć płytę z innego napędu niż domyślny (czyli tego z najniższą literą w symbolu dysku). Aby rozwiązać ten problem, przygotujemy funkcję KontrolaTackiCD oraz dwie funkcje: OpenCD i CloseCD (listing 4.33). Przygotujemy dla nich moduł plików Multimedia.h/ Multimedia.cpp. Dwie ostatnie funkcje zadeklarujemy w pliku nagłówkowym. Skorzystamy z funkcji mciSendCommand21, która pozwala na przesyłanie do urządzeń multimedialnych poleceń sterujących ich działaniem. Tym razem będzie to polecenie MCI_SET, za pomocą którego można ustawiać niektóre ich parametry. W omawianym przypadku ustawienie będzie dotyczyło położenia tacki.
21
Funkcje mciSendString i mciSendCommand dostępne są we wszystkich 32-bitowych wersjach Windows.
142
Visual C++. Gotowe rozwiązania dla programistów Windows
Listing 4.33. Zawartość pliku Multimedia.cpp. Funkcje OpenCD i CloseCD należy zadeklarować w pliku nagłówkowym #include "stdafx.h" #include "mmsystem.h" #pragma comment(lib, "Winmm.lib") BOOL KontrolaTackiCD(LPCTSTR Drive, BOOL Operacja) { BOOL Wynik = FALSE; MCI_OPEN_PARMS parametry; parametry.dwCallback = 0; //uchwyt okna, do którego mogłyby być wysyłane ´komunikaty powiadamiające o wysunięciu tacki parametry.lpstrDeviceType = L"CDAudio"; parametry.lpstrElementName = Drive; //Symbol dysku w formacie X: //Inicjalizacja urządzenia mciSendCommand(0, MCI_OPEN, MCI_OPEN_ELEMENT | MCI_OPEN_TYPE, (long)¶metry); if (Operacja) //Otwieranie napędu CD-ROM Wynik = (mciSendCommand(parametry.wDeviceID, MCI_SET, MCI_SET_DOOR_OPEN, 0) == 0); else //Zamykanie napędu CD-ROM Wynik = (mciSendCommand(parametry.wDeviceID, MCI_SET, MCI_SET_DOOR_CLOSED, 0)==0); //zwolnienie dostępu do urządzenia mciSendCommand(parametry.wDeviceID, MCI_CLOSE, MCI_NOTIFY, (long)¶metry); return Wynik; } BOOL OpenCD(LPCTSTR Drive) { return KontrolaTackiCD(Drive, TRUE); } BOOL CloseCD(LPCTSTR Drive) { return KontrolaTackiCD(Drive, FALSE); }
Aby uczynić kod bardziej przejrzystym, w powyższych funkcjach pominęliśmy obsługę błędów. Gdybyśmy ją uwzględnili, polecenie np. otwarcia dostępu do urządzenia powinno wyglądać następująco: MCIERROR mciBlad = mciSendCommand(0,MCI_OPEN,MCI_OPEN_ELEMENT | MCI_OPEN_TYPE, ´(long)¶metry); if (mciBlad != 0) { wchar_t opisBledu[MAXERRORLENGTH]; mciGetErrorString(mciBlad, opisBledu, MAXERRORLENGTH); AfxMessageBox(opisBledu); return false; }
Rozdział 4. ♦ Systemy plików, multimediai inne funkcje WinAPI
143
gdzie opisBledu to tablica znaków o długości MAXERRORLENGTH, a mciBlad to zmienna typu long. Użycie funkcji jest bardzo proste, np. OpenCD(L"D:");. Równie proste byłoby przygotowanie aplikacji konsolowej eject korzystającej z poniższej funkcji, a której zadaniem byłoby właśnie wysuwanie (a z parametrem -t wsuwanie) wskazanego napędu. Pozostawiam to Czytelnikowi jako „zadanie domowe”.
Wykrywanie wysunięcia płyty z napędu lub umieszczenia jej w napędzie CD lub DVD Aby wykryć moment wysunięcia płyty z napędu lub umieszczenia jej w napędzie, konieczne jest wykorzystanie mechanizmu komunikatów Windows. Dlatego projekt ten umieszczony został w rozdziale 6.
Sprawdzanie stanu wybranego napędu CD-Audio Kilka kolejnych projektów dotyczy obsługi napędów z płytą CD-Audio. Oznacza to tylko i wyłącznie kontrolę napędu, który umożliwia sterowanie odtwarzaniem muzyki z płyt CD-Audio. Są napędy, które nie pozwalają na taką kontrolę (np. napędy montowane w notebookach), lub takie, które umożliwiają ją tylko w pewnym stopniu. A nawet jeżeli napęd pozwala na taką kontrolę, ale napęd optyczny nie jest połączony odpowiednim przewodem z kartą muzyczną, możemy nic nie usłyszeć. Poza tym warto wiedzieć, że muzykę można odtwarzać także w inny sposób. Przykładem jest Windows Media Player, który odczytuje dane z płyty i odtwarza je, nie korzystając ze służących do tego funkcji napędów, ale za pośrednictwem urządzeń odtwarzania plików dźwiękowych (co obciąża nieco procesor, ale jest niezależne od napędu). Pierwsza z tej serii to funkcja StanCDAudio, widoczna na listingu 4.34. Funkcja ta pozwala określić, w jakim stanie jest napęd CD, a dokładnie, czy zawiera płytę i czy jest ona właśnie odtwarzana (mowa o odtwarzaniu przez napęd, a nie np. przez Windows Media Player). Musimy ponownie wykorzystać funkcję mciSendCommand, która za cenę mniejszej wygody obsługuje wiele typów urządzeń multimedialnych. Tym razem użyjemy polecenia MCI_STATUS i związanej z nim struktury MCI_STATUS_PARMS. Jak zwykle należy pamiętać o otwarciu i zamknięciu dostępu do urządzenia (polecenia MCI_OPEN i MCI_CLOSE). Listing 4.34. Funkcja sprawdzająca stan napędu CD. Należy ją umieścić w pliku Multimedia.cpp i zadeklarować w pliku nagłówkowym unsigned long StanCDAudio(LPCTSTR Drive) { MCI_OPEN_PARMS parametry; parametry.dwCallback = 0; parametry.lpstrDeviceType = L"CDAudio"; parametry.lpstrElementName = Drive; //Literą dysku musi być np. "X:" mciSendCommand(0, MCI_OPEN, MCI_OPEN_ELEMENT | MCI_OPEN_TYPE, (long)¶metry);
144
Visual C++. Gotowe rozwiązania dla programistów Windows MCI_STATUS_PARMS stan; stan.dwItem = MCI_STATUS_MODE; mciSendCommand(parametry.wDeviceID,MCI_STATUS,MCI_WAIT | ´MCI_STATUS_ITEM,(long)&stan); unsigned long wynik=stan.dwReturn; //stan MCI zaczyna się od 524 mciSendCommand(parametry.wDeviceID,MCI_CLOSE,MCI_NOTIFY,(long)¶metry); return wynik; }
Wartość zwracana przez tę funkcję świadczy o stanie napędu (są to liczby od 524 wzwyż). Listing 4.35 zawiera przykład jej użycia, a jednocześnie wymienia ważniejsze zwracane przez funkcję wartości (stałe zdefiniowane są w pliku mmsystem.h). Przed jej testowaniem proszę pamiętać o włożeniu płyty CD-Audio do napędu. Listing 4.35. Przykład wykorzystania funkcji StanCDAudio #include "mmsystem.h" void CMCIDlg::OnBnClickedButton3() { wchar_t katalogNaPlycie[MAX_PATH]; edit1.GetWindowTextW(katalogNaPlycie,MAX_PATH); if(katalogNaPlycie != L"") { unsigned long wynik=StanCDAudio(katalogNaPlycie); switch (wynik) { case MCI_MODE_NOT_READY: MessageBox(L"Napęd nie jest gotowy (brak płyty ´CD-Audio)"); break; case MCI_MODE_PAUSE: MessageBox(L"Odtwarzanie wstrzymane (pauza)"); break; case MCI_MODE_PLAY: MessageBox(L"Trwa odtwarzanie"); break; case MCI_MODE_STOP: MessageBox(L"Odtwarzanie zatrzymane (stop)"); break; case MCI_MODE_OPEN: MessageBox(L"Tacka jest wysunięta"); break; case MCI_MODE_RECORD: MessageBox(L"Trwa zapis na płytę"); break; case MCI_MODE_SEEK: MessageBox(L"Szukanie"); break; default: CString temp; temp.Format(L"Kod błędu: %lu (prawdopodobnie napęd nie jest dyskiem ´optycznym)", wynik); MessageBox(temp); break; } } }
Jak zbadać, czy w napędzie jest płyta CD-Audio Funkcja StanCDAudio pozwala w zasadzie na sprawdzenie, czy mamy do czynienia z płytą CD-Audio. Test taki możemy też wykonać w nieco inny, bardziej skrupulatny sposób. Możemy mianowicie sprawdzić, czy na dysku są dostępne jakieś utwory (ścieżki
Rozdział 4. ♦ Systemy plików, multimediai inne funkcje WinAPI
145
muzyczne). W tym celu przygotujemy funkcję pobierającą symbol napędu i zwracającą wartość logiczną odpowiadającą odnalezieniu płyty z muzyką. Działanie funkcji polega na wielostopniowym teście zaczynającym się od sprawdzenia typu napędu, jego stanu i wreszcie na odnalezieniu ścieżek muzycznych (listing 4.36). Listing 4.36. Jeżeli płyta zawiera ścieżki audio, uznajemy, że jest to płyta CD-Audio bool IsCDAudio(LPCTSTR Drive) { MCI_OPEN_PARMS parametry; parametry.dwCallback = 0; parametry.lpstrDeviceType = L"CDAudio"; parametry.lpstrElementName = Drive; MCIERROR mciBlad = mciSendCommand(0, MCI_OPEN, MCI_OPEN_ELEMENT | ´MCI_OPEN_TYPE,(long)¶metry); if (mciBlad != 0) return false; MCI_STATUS_PARMS stanNapedu; stanNapedu.dwCallback = 0; stanNapedu.dwItem = MCI_CDA_STATUS_TYPE_TRACK; stanNapedu.dwTrack = 1; mciBlad=mciSendCommand(parametry.wDeviceID,MCI_STATUS,MCI_TRACK | ´MCI_STATUS_ITEM,(long)&stanNapedu); if (mciBlad!=0) return false; bool wynik; switch (stanNapedu.dwReturn) { case MCI_CDA_TRACK_AUDIO: wynik = true; break; default: wynik = false; break; } mciSendCommand(parametry.wDeviceID, MCI_CLOSE, MCI_NOTIFY, (long)¶metry); return wynik; }
Kontrola napędu CD-Audio Przejdźmy do zasadniczej funkcji tego podrozdziału, która pozwoli nam rozpoczynać, wstrzymywać, wznawiać i zatrzymywać odtwarzanie płyt CD-Audio. Przypominam, że istnieją napędy, nawet te nowe, które nie wspierają sprzętowego odtwarzania płyt CD-Audio. Najczęściej można je spotkać w notebookach. Żeby uniknąć powtarzania kodu (przy wszystkich tych poleceniach należy otworzyć i zamknąć dostęp do urządzenia), przygotujemy funkcję KontrolaCDAudio, której nie będziemy udostępniać poza plikiem Multimedia.cpp, oraz zbiór czterech funkcji udostępnionych w pliku nagłówkowym, a które będą obsługiwać poszczególne czynności. Odpowiadają im cztery polecenia MCI, a więc: MCI_PLAY, MCI_STOP, MCI_PAUSE i MCI_RESUME. Listing 4.37 przedstawia wszystkie funkcje.
146
Visual C++. Gotowe rozwiązania dla programistów Windows
Listing 4.37. Funkcje pozwalające na odtwarzanie muzyki z płyt CD-Audio bool KontrolaCDAudio(LPCTSTR Drive, ULONG Operacja) { MCI_OPEN_PARMS parametry; parametry.dwCallback = 0; parametry.lpstrDeviceType = L"CDAudio"; parametry.lpstrElementName = Drive; //Literą dysku musi być np. "X:" mciSendCommand(0, MCI_OPEN, MCI_OPEN_ELEMENT | MCI_OPEN_TYPE, (long)¶metry); bool wynik=(mciSendCommand(parametry.wDeviceID, Operacja, 0, 0) == 0); mciSendCommand(parametry.wDeviceID,MCI_CLOSE,MCI_NOTIFY,(long)¶metry); return wynik; } bool PlayCDAudio(LPCTSTR Drive) { return KontrolaCDAudio(Drive, MCI_PLAY); } bool ResumeCDAudio(LPCTSTR Drive) { return KontrolaCDAudio(Drive, MCI_RESUME); } bool PauseCDAudio(LPCTSTR Drive) { if (StanCDAudio(Drive) != 525) return KontrolaCDAudio(Drive, MCI_PAUSE); //gdy odtwarzanie else return ResumeCDAudio(Drive); //gdy zatrzymany } bool StopCDAudio(LPCTSTR Drive) { return KontrolaCDAudio(Drive, MCI_STOP); }
Funkcja Pause jest na tyle sprytna, że sprawdza, czy odtwarzanie nie było wcześniej wstrzymane — jeżeli było, wznawia je za pomocą funkcji Resume. Korzysta w tym celu z funkcji StanCDAudio. Polecam uwadze Czytelnika program CD-Audi, który wykorzystuje i testuje zdefiniowane w tym podrozdziale funkcje. Dostępny jest w dołączonych do książki materiałach.
Rozdział 4. ♦ Systemy plików, multimediai inne funkcje WinAPI
147
Multimedia (pliki dźwiękowe WAVE) Asynchroniczne odtwarzanie pliku dźwiękowego Kolejnym przykładem zastosowania biblioteki MCI jest odtwarzanie różnego typu plików audio i wideo. Jeżeli jednak chcemy odtworzyć pojedynczy plik .wav, należy wykorzystać prostszą w obsłudze funkcję WinAPI PlaySound22 . Potrafi ona odtwarzać pliki synchronicznie i asynchronicznie. W pierwszym przypadku działanie funkcji zakończy się dopiero po zakończeniu odtwarzania, w drugim tuż po jego uruchomieniu, a dźwięk odtwarzany jest w osobnym wątku. Ponadto funkcja pozwala na wskazanie jednego ze zdefiniowanych dźwięków systemowych. W końcu to też są tylko pliki .wav, ale do ich identyfikacji użyć można nazw-aliasów ze schematów dźwiękowych. Dwa przedstawione niżej polecenia odtwarzają asynchronicznie (modyfikator SND_ASYNC) pliki .wav. W pierwszym wskazujemy konkretny plik znajdujący się na dysku, w drugim korzystamy z aliasu, żeby usłyszeć dźwięk odtwarzany przy uruchomieniu systemu. PlaySound(L"c:\\WINDOWS\\MEDIA\\Windows Logon Sound.wav", 0, SND_FILENAME | SND_ASYNC); PlaySound((LPCWSTR)SND_ALIAS_SYSTEMSTART, 0, SND_ALIAS_ID | SND_ASYNC);
Aby wszystko zadziałało, należy zaimportować nagłówek mmsystem.h: #include "mmsystem.h" #pragma comment(lib, "Winmm.lib")
Poza przełącznikami SND_FILENAME i SND_ALIAS do wskazania źródła dźwięku można użyć także SND_RESOURCE. Wówczas pierwszy argument musi zawierać identyfikator zasobu. Warto zwrócić uwagę także na modyfikator SND_LOOP, który spowoduje odtwarzanie dźwięku w pętli, aż do kolejnego wywołania funkcji PlaySound z pierwszym parametrem równym NULL, tj. PlaySound(NULL,0,0).
Jak wykryć obecność karty dźwiękowej Omówiliśmy już odtwarzanie płyt CD-Audio, plików .wav, ale może warto byłoby się upewnić, że w systemie w ogóle zainstalowana jest jakaś karta dźwiękowa. Możemy do tego celu użyć funkcji WinAPI waveoutGetNumDevs zwracającej liczbę zarejestrowanych urządzeń zdolnych do odtwarzania plików .wav. Jeżeli jest przynajmniej jedno, możemy być pewni co do obecności karty dźwiękowej. Oto przykład: if (waveOutGetNumDevs == 0) AfxMessageBox(L"Brak karty dźwiękowej!"); else AfxMessageBox(L"Karta dźwiękowa jest zainstalowana");
22
Dostępna we wszystkich 32-bitowych wersjach Windows.
148
Visual C++. Gotowe rozwiązania dla programistów Windows
Kontrola poziomu głośności odtwarzania plików dźwiękowych Odtwarzanie plików dźwiękowych wiąże się z wykorzystaniem urządzenia, którego obecność badaliśmy w poprzednim projekcie. Urządzenie to wspomaga nie tylko odtwarzanie plików .wav, ale również plików .mp3 oraz ścieżek dźwiękowych filmów .avi. Za pomocą funkcji WinAPI waveoutSetVolume możemy kontrolować poziom głośności dźwięku generowanego przez to urządzenie. Ze względu na to, że funkcja ta przyjmuje czterobajtową liczbę zawierającą poziom głośności lewego i prawego kanału, zdefiniujemy prostszą w użyciu funkcję rozdzielającą te argumenty (listing 4.38). To samo dotyczy odczytywania poziomu głośności, które jest możliwe dzięki funkcji waveoutGetVolume23. Listing 4.38. Funkcje ułatwiające kontrolę głośności kanału WAVE void UstalPoziomGlosnosciWave(USHORT kanalLewy, USHORT kanalPrawy) { ULONG glosnosc = (kanalLewy 24); kanalPrawy = (USHORT)((glosnosc & 0x0000FFFF) >> 8); }
Aby je przetestować: 1. Umieszczamy na formie dwa suwaki CSliderControl. 2. Wiążemy z nimi zmienne slider1 i slider2. 3. Przechodzimy do metody OnInitDialog() i modyfikujemy ją według listingu 4.39. Listing 4.39. Ustawienie własności suwaków BOOL CPlikiDzwiekoweDlg::OnInitDialog() { CDialog::OnInitDialog(); // Set the icon for this dialog. The framework does this automatically // when the application's main window is not a dialog SetIcon(m_hIcon, TRUE); // Set big icon SetIcon(m_hIcon, FALSE); // Set small icon USHORT kanalLewy, kanalPrawy; CzytajPoziomGlosnosciWave(kanalLewy, kanalPrawy);
23
Obie funkcje dostępne są we wszystkich 32-bitowych wersjach Windows.
Rozdział 4. ♦ Systemy plików, multimediai inne funkcje WinAPI
149
slider1.SetRangeMin(0); slider1.SetRangeMax(100); slider1.SetTicFreq(15); slider1.SetPos(kanalLewy); slider2.SetRangeMin(0); slider2.SetRangeMax(100); slider2.SetTicFreq(15); slider2.SetPos(kanalPrawy); }
return TRUE;
// return TRUE
unless you set the focus to a control
4. Przechodzimy do edycji ich własności w celu zmiany pozycji Notify Before
Move na True. 5. Do pierwszego suwaka dodajemy metodę zdarzeniową, związaną z komunikatem NM_CUSTOMDRAW, i umieszczamy w niej polecenie z listingu 4.40. Listing 4.40. Głośność w lewym i prawym kanale kontrolować będziemy suwakami void CPlikiDzwiekoweDlg::OnNMCustomdrawSlider1(NMHDR *pNMHDR, LRESULT *pResult) { LPNMCUSTOMDRAW pNMCD = reinterpret_cast(pNMHDR); // TODO: Add your control notification handler code here *pResult = 0; UstalPoziomGlosnosciWave(slider1.GetPos(), slider2.GetPos()); }
6. Przechodzimy do sekcji mapowania komunikatów i dodajemy w niej polecenie
wyróżnione na listingu 4.41, które wiąże drugi suwak z istniejącą metodą. Listing 4.41. Sekcja mapowania komunikatów BEGIN_MESSAGE_MAP(CPlikiDzwiekoweDlg, CDialog) ON_WM_PAINT() ON_WM_QUERYDRAGICON() //}}AFX_MSG_MAP ON_BN_CLICKED(IDC_BUTTON1, &CPlikiDzwiekoweDlg::OnBnClickedButton1) ON_BN_CLICKED(IDC_BUTTON2, &CPlikiDzwiekoweDlg::OnBnClickedButton2) ON_NOTIFY(NM_CUSTOMDRAW, IDC_SLIDER1, &CPlikiDzwiekoweDlg::OnNMCustomdrawSlider1) ON_NOTIFY(NM_CUSTOMDRAW, IDC_SLIDER2, &CPlikiDzwiekoweDlg::OnNMCustomdrawSlider1) END_MESSAGE_MAP()
Zwykle zamiast ustalania głośności w lewym i prawym kanale udostępnia się użytkownikowi aplikacji komponenty kontrolujące głośność obu kanałów oraz balans. Przygotowanie ich pozostawiam Czytelnikowi, ale uprzedzam, że zadanie to wcale nie jest tak banalne, jak z pozoru może się wydawać.
150
Visual C++. Gotowe rozwiązania dla programistów Windows
Kontrola poziomu głośności CD-Audio Powróćmy jeszcze na chwilę do odtwarzania płyt CD-Audio. Jak kontrolować głośność ich odtwarzania? Służą do tego funkcje auxSetVolume i auxGetVolume, z których korzysta się zupełnie analogicznie jak z funkcji waveoutSetVolume i waveoutGetVolume poznanych w poprzednim projekcie. Wzorem poprzedniego projektu przygotujemy ponownie dwie funkcje ułatwiające kontrolę głośności (listing 4.42), jednak tym razem zwracają one wartości informujące o powodzeniu operacji. Listing 4.42. Nie zawsze możliwa jest kontrola CD-Audio. Może się więc okazać, że poniższe funkcje nie przynoszą żadnych efektów bool UstalPoziomGlosnosciCDAudio(USHORT kanalLewy, USHORT kanalPrawy) { ULONG glosnosc = (kanalLewy 24); kanalPrawy = (USHORT)((glosnosc & 0x0000FFFF) >> 8); return wynik; }
Łatwo się domyślić, że do kontroli głośności urządzenia MIDI służą analogiczne funkcje midiSetVolume i midiGetVolume.
Inne Na koniec chciałbym przedstawić zbiór kilku projektów, które trudno zakwalifikować do jednej z omówionych już kategorii, a więc różne dziwne możliwości, nie zawsze całkiem praktyczne.
Pisanie i malowanie na pulpicie Oto ciekawostka. Znajdziemy okno związane z pulpitem, znajdziemy uchwyt do jego płótna (kontekst wyświetlania) i wykorzystamy go, żeby umieścić na pulpicie dowolny tekst, korzystając z metody TextOut. W tym celu: 1. Tworzymy nowy projekt o nazwie PulpitPisanie. 2. Formę aplikacji projektujemy według wzoru z rysunku 4.13, gdzie prostokąty
pod etykietami koloru tła i koloru czcionki są kontrolkami ActiveX Microsoft Forms Image 2.0.
Rozdział 4. ♦ Systemy plików, multimediai inne funkcje WinAPI
151
3. Z polem edycyjnym wiążemy zmienną Edit1, a z etykietami odpowiednio zmienne Label1 i Label2 (należy pamiętać o zmianie ich ID z ID_STATIC na np. ID_STATIC10). Kontrolkom ActiveX przypisujemy zmienne Image1 i Image2. Rysunek 4.13. Widok projektowanej aplikacji
4. Do klasy CPulpitPisanieDlg dodajemy następujące pola: private: LOGFONT lf; CHOOSECOLOR cc, textColor; CFont newFont; CFont *oldFont;
5. Klikamy dwukrotnie pierwszy przycisk i umieszczamy w nim polecenia
wyróżnione na listingu 4.43. Listing 4.43. Formatujemy czcionkę void CPulpitPisanieDlg::OnBnClickedButton1() { CFontDialog fontDialog;
}
if(fontDialog.DoModal() != IDCANCEL) { fontDialog.GetCurrentFont(&lf); newFont.CreateFontIndirectW(&lf); Label1.SetWindowTextW(lf.lfFaceName); CString temp; temp.Format(L"%d", fontDialog.GetSize()/10); Label2.SetWindowTextW(temp); textColor.rgbResult = fontDialog.GetColor(); Image2.put_BackColor(textColor.rgbResult); }
6. Tworzymy metodę zdarzeniową dla drugiego z przycisków, którą definiujemy
zgodnie z listingiem 4.44. Listing 4.44. Modyfikujemy kolor tła void CPulpitPisanieDlg::OnBnClickedButton2() { CColorDialog colorDialog; if(colorDialog.DoModal() != IDCANCEL)
152
Visual C++. Gotowe rozwiązania dla programistów Windows { cc = colorDialog.m_cc; Image1.put_BackColor(cc.rgbResult); } }
7. Przechodzimy wreszcie do punktu kulminacyjnego i klikamy dwukrotnie w polu
edycyjnym, tworząc w ten sposób domyślną metodę zdarzeniową, w której umieszczamy polecenia z listingu 4.45. Listing 4.45. Napis nie będzie trwały, gdyż w żaden sposób nie zadbaliśmy o jego odświeżanie void CPulpitPisanieDlg::OnEnChangeEdit1() { // TODO: If this is a RICHEDIT control, the control will not // send this notification unless you override the CDialog::OnInitDialog() // function and call CRichEditCtrl().SetEventMask() // with the ENM_CHANGE flag ORed into the mask. CClientDC dc(GetDesktopWindow()); CString temp; oldFont = dc.SelectObject(&newFont); dc.SetBkColor(cc.rgbResult); dc.SetBkMode(OPAQUE); dc.SetTextColor(textColor.rgbResult); Edit1.GetWindowTextW(temp); dc.TextOut(200, 200, temp); dc.SelectObject(oldFont); }
Po uruchomieniu programu możemy wybrać krój i kolor czcionki, kolor tła i wpisać dowolny tekst w polu edycyjnym. Tekst ten pojawi się równocześnie na pulpicie (rysunek 4.14). Czcionka i kolory napisu powinny być zgodne z wybranymi. Rysunek 4.14. Prosta aplikacja służąca do pisania w oknie pulpitu
Rozdział 4. ♦ Systemy plików, multimediai inne funkcje WinAPI
153
Czy Windows mówi po polsku? Dynamiczna lokalizacja programu polega na sprawdzeniu, jaki jest język zainstalowanej wersji Windows, i wyświetlaniu komunikatów w tym języku. Sprawa się upraszcza, jeżeli program operuje tylko dwoma językami: polskim, gdy mamy do czynienia z polską wersją systemu, i angielskim — w każdym innym przypadku. Listing 4.46 pokazuje, jak sprawdzić za pomocą funkcji GetSystemDefaultLangID24, czy mamy do czynienia z polską wersją Windows. Listing 4.46. Sprawdzamy domyślny język zainstalowanego systemu Windows bool CzyJezykPolski() { return (GetSystemDefaultLangID() == 0x0415); }
Jak zablokować uruchamiany automatycznie wygaszacz ekranu? Taka możliwość przydaje się przy projektowaniu aplikacji, które po uruchomieniu dalej pracują samodzielnie (bez konieczności ich kontroli myszą lub klawiaturą), a wynik ich działania jest obserwowany przez użytkownika. Są to na przykład wszelkiego rodzaju odtwarzacze wideo. 1. Na formie umieszczamy kontrolkę CheckBox, z którą wiążemy zmienną checkBox1. 2. Klikamy ją dwukrotnie i w utworzonej w ten sposób domyślnej metodzie
zdarzeniowej umieszczamy polecenia z listingu 4.47. Listing 4.47. Blokujemy uruchamiany automatycznie przez system wygaszacz ekranu void CBlokadaWygaszaczaEkranuDlg::OnBnClickedCheck1() { int aktywny=checkBox1.GetCheck()?0:1; SystemParametersInfo(SPI_SETSCREENSAVEACTIVE,aktywny,NULL,0); }
Swoją drogą warto przejrzeć dokumentację użytej w powyższym kodzie funkcji SystemParametersInfo25. Pozwala ona nie tylko zablokować wygaszacz ekranu czy ustalić czas, po którym system go uruchomi, ale również zmodyfikować wiele innych ustawień powłoki systemu.
24
Dostępna we wszystkich 32-bitowych wersjach Windows.
25
Dostępna we wszystkich 32-bitowych wersjach Windows.
154
Visual C++. Gotowe rozwiązania dla programistów Windows
Zmiana tła pulpitu Przykładem innej sztuczki, która jest możliwa dzięki zmianie ustawień systemu przeprowadzonej za pomocą funkcji SystemParametersInfo, jest zmiana tła pulpitu (czyli tzw. tapety). SystemParametersInfo(SPI_SETDESKWALLPAPER, 0, nazwa pliku.GetBuffer(), SPIF_UPDATEINIFILE | SPIF_SENDWININICHANGE);
Rozbudowany przykład prezentujący sposób użycia tej funkcji znajduje się w dołączonych do książki źródłach. Efekt działania tego projektu przedstawia rysunek 4.15. Rysunek 4.15. Wprawka programistyczna — program do podglądu i zmiany tła pulpitu
Powyższa funkcja nie zadziała, jeżeli uaktywniona jest usługa Active Desktop. Na szczęście dla naszej metody nie stała się ona nigdy zbyt popularna. Jeżeli jednak uprzemy się, aby zmienić tło pulpitu przy uaktywnionej tej usłudze, konieczne jest wykorzystanie obiektu COM, który służy do jej kontroli. Obiekt identyfikowany jest przez stałą TGUID CLSID_ActiveDesktop="{75048700-EF1F-11D0-9888-006097DEACF9}". Natomiast interfejs, który zawiera potrzebną do zmiany tła funkcję SetWallpaper, to IActiveDesktop. Pamiętać jeszcze należy o wywołaniu metody ApplyChanges — i gotowe.
Rozdział 5.
Rejestr systemu Windows Rejestr Rejestr systemu Windows jest rodzajem bazy danych, w której system i aplikacje przechowują swoje ustawienia i dane. Rejestr składa się z kilku kluczy głównych, z których z naszego punktu widzenia interesujące są trzy. Pierwszym jest HKEY_LOCAL_ MACHINE, który przechowuje ustawienia systemowe dotyczące sprzętu, systemu i wszystkich użytkowników. Drugi to HKEY_CURRENT_USER. Jest to w istocie alias do jednego z kluczy, znajdującego się w kluczu głównym HKEY_USERS, którego właścicielem jest aktualnie zalogowany użytkownik. W kluczu tym przechowywane są wszystkie ustawienia użytkownika dotyczące systemu (np. ustawienia interfejsu graficznego) oraz spersonalizowane ustawienia aplikacji1. Trzecim jest HKEY_CLASSES_ROOT, w którym zapisane są informacje o skojarzeniach aplikacji z rozszerzeniami pliku oraz operacjami, jakie na tych plikach można wykonać (por. drugi argument metody ShellExecute omawianej w rozdziale 3.). Rejestr zorganizowany jest na wzór systemu plików. W każdym z kluczy podstawowych można zakładać własne klucze (ang. key), które odpowiadają folderom. W każdym kluczu mogą znajdować się podklucze lub wartości (ang. value). W wartościach przechowane są zwykle łańcuchy (ciągi, typ REG_SZ) lub liczby naturalne (typ REG_DWORD). Inne typy nie będą przez nas wykorzystywane. Aby uniknąć konfliktu w nazewnictwie, przyjmijmy, że dane przechowywane przez wartość będziemy nazywać zawartością. Wykorzystanie rejestru z poziomu aplikacji zaprezentujemy w pięciu najczęściej wykorzystywanych w praktyce przykładach, takich jak: przechowywanie ustawień aplikacji (na przykładzie pozycji i rozmiaru okna), umieszczanie zapisu powodującego automatyczne uruchamianie aplikacji w momencie zalogowania użytkownika, umieszczanie w rejestrze informacji o instalacji odczytywanych przez aplet Dodaj/Usuń programy
1
Alternatywnym do rejestru rozwiązaniem problemu przechowywania konfiguracji aplikacji jest użycie plików INI (o nich w dalszej części rozdziału). Zwykle jest to jednak mniej praktyczne.
156
Visual C++. Gotowe rozwiązania dla programistów Windows
(znajdujący się w panelu sterowania), sprawdzanie ustawień systemowych na przykładzie odczytywania lokalizacji katalogów powłoki, np. katalogu Moje dokumenty, oraz modyfikacja menu systemowych aplikacji. Przygotowane w poniższych przykładach funkcje umieścimy w module Rejestr.h/ Rejestr.cpp, który razem z klasą Rejestr stanie się wygodnym zestawem narzędzi ułatwiającym najczęstsze operacje dotyczące rejestru.
Klasa obsługująca operacje na rejestrze W bibliotece MFC, w klasie CWinApp zaimplementowany został zestaw metod pozwalających na zapisywanie i odczytywanie danych w rejestrze oraz plikach INI. Są to m.in. GetProfileString i WriteProfileString dla odczytu i zapisywania łańcuchów, GetProfileInt i WriteProfileInt dla liczb całkowitych itd. Funkcje te omówiono w dalszej części rozdziału. Będziemy ich używać nawet dla plików INI. W ogólnym przypadku ich użyteczność jest jednak ograniczona wyłącznie do operacji w kluczu HKEY_ ´CURRENT_USER\Software2, w którym powinny być przechowywane ustawienia aplikacji. Uniemożliwia to bardziej zaawansowane operacje na rejestrze. Do niektórych z wymienionych wyżej zadań będziemy potrzebowali bardziej ogólnego narzędzia. Odpowiednie do tego celu funkcje znajdziemy w WinAPI. Dla wygody opakujemy je klasą o nazwie CRejestr. Dobrym wzorem jest klasa TRegistry z biblioteki VCL. Oczywiście nie jest to konieczne. Można z funkcji obsługujących rejestr korzystać bezpośrednio. Jednak ilość przyjmowanych przez te funkcje parametrów jest zazwyczaj spora, a tylko ich niewielka część jest rzeczywiście modyfikowana lub różna od wartości, które można by uznać za domyślne. Aby stworzyć nową klasę CRejestr: 1. Otwórzmy nowy projekt typu MFC Application z oknem dialogowym o nazwie
RejestrSystemuWindows. 2. W podoknie Solution Explorer zaznaczmy projekt (pozycja
RejestrSystemuWindows). Następnie z menu Project wybierzmy Add Class… (Shift+Alt+C). Pojawi się okno, w którym wybieramy pozycję C++ Class i klikamy przycisk z etykietą Add. 3. W kolejnym oknie dialogowym (rysunek 5.1) w polu Class name wpisujemy nazwę klasy CRejestr. W dwóch polach zawierających nazwy plików pojawią
się: Rejestr.h i Rejestr.cpp. Klikamy przycisk Finish. Powstaną w ten sposób dwa pliki zawierające definicję klasy ze zdefiniowanym pustym konstruktorem i destruktorem. 4. Edytując pliki Rejestr.h i Rejestr.cpp, dodajmy do klasy deklaracje pól
i zainicjujmy je w konstruktorze. Plik nagłówkowy widoczny jest na listingu 5.1.
2
Sposób, w jaki tworzony jest klucz dla naszej aplikacji, można podejrzeć w pliku appui3.cpp znajdującym się w katalogu ..\Microsoft Visual Studio 9.0\VC\atlmfc\src\mfc\.
Rozdział 5. ♦ Rejestr systemu Windows
157
Rysunek 5.1. Tworzenie klasy CRejestr
Listing 5.1. Plik nagłówkowy Rejestr.h #pragma once class CRejestr { private: HKEY uchwytKlucza; HKEY kluczGlowny; REGSAM trybDostepu; public: bool wyswietlajKomunikatyOBledach; public: CRejestr(HKEY kluczGlowny,REGSAM trybDostepu,bool wyswietlajKomunikatyOBledach=true); ~CRejestr(void); };
void WyswietlKomunikatBledu(LONG numerBledu) const;
5. W pliku z kodem źródłowym Rejestr.cpp (listing 5.2) po modyfikacji konstruktora
definiujemy również metodę, której będziemy używać do wyświetlania ewentualnych komunikatów o błędach. Listing 5.2. Plik z kodem źródłowym Rejestr.cpp #include "StdAfx.h" #include "Rejestr.h" CRejestr::CRejestr(HKEY kluczGlowny,REGSAM trybDostepu,bool ´wyswietlajKomunikatyOBledach)
158
Visual C++. Gotowe rozwiązania dla programistów Windows :uchwytKlucza(NULL),kluczGlowny(kluczGlowny), trybDostepu(trybDostepu),wyswietlajKomunikatyOBledach(wyswietlajKomunikatyOBledach) { } CRejestr::~CRejestr(void) { } void CRejestr::WyswietlKomunikatBledu(LONG numerBledu) const { if(!wyswietlajKomunikatyOBledach) return; wchar_t* komunikat; FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | ´FORMAT_MESSAGE_FROM_SYSTEM | ´FORMAT_MESSAGE_IGNORE_INSERTS, NULL, numerBledu, 0, (LPWSTR)&komunikat, 0, NULL); MessageBox(NULL,komunikat,L"Błąd podczas operacji na rejestrach", MB_OK | MB_ICONERROR); LocalFree(komunikat); }
6. Zdefiniujmy teraz metody służące do tworzenia, otwierania, zamykania i usuwania
klucza. Przedstawia je listing 5.3. Listing 5.3. Metody klasy CRejestr dotyczące kluczy #pragma region Metody dotyczace kluczy bool CRejestr::UtworzKlucz(const wchar_t* klucz) { LONG numerBledu = RegCreateKeyEx(kluczGlowny,klucz, ´NULL,0,REG_OPTION_NON_VOLATILE, ´trybDostepu,NULL,&uchwytKlucza,NULL); if (numerBledu != ERROR_SUCCESS) WyswietlKomunikatBledu(numerBledu); return numerBledu == ERROR_SUCCESS; } bool CRejestr::OtworzKlucz(const wchar_t* klucz) { if(uchwytKlucza!=NULL) { if (wyswietlajKomunikatyOBledach) MessageBox(NULL, L"Przed otwarciem nowego klucza zamknij klucz otwarty wcześniej", L"Błąd otwarcia klucza",MB_OK); return false; } LONG numerBledu = RegOpenKeyEx(kluczGlowny,klucz,0,trybDostepu,&uchwytKlucza); if (numerBledu != ERROR_SUCCESS) WyswietlKomunikatBledu(numerBledu); return numerBledu == ERROR_SUCCESS; } bool CRejestr::ZamknijKlucz() {
Rozdział 5. ♦ Rejestr systemu Windows LONG numerBledu = if (numerBledu != else uchwytKlucza return numerBledu
159
RegCloseKey(uchwytKlucza); ERROR_SUCCESS) WyswietlKomunikatBledu(numerBledu); = NULL; == ERROR_SUCCESS;
} bool CRejestr::UsunKlucz(const wchar_t* klucz) { LONG numerBledu = RegDeleteKey(kluczGlowny,klucz); if (numerBledu != ERROR_SUCCESS) WyswietlKomunikatBledu(numerBledu); else klucz=NULL; return numerBledu == ERROR_SUCCESS; } #pragma endregion
7. Dodajmy do tego regionu jeszcze dwie metody. Pierwsza sprawdza, czy podany
klucz istnieje w rejestrze. W drugiej, korzystając z tej pierwszej metody, przeciążamy metodę otwierającą klucz w taki sposób, że jeżeli klucza nie ma w rejestrze, jest automatycznie tworzony. Obie metody widoczne są na listingu 5.4. Listing 5.4. Dodatkowe metody dla kluczy bool CRejestr::CzyKluczIstnieje(const wchar_t* klucz) const { HKEY lokalnyUchwytKlucza; bool wynik = RegOpenKeyEx(kluczGlowny,klucz,0,trybDostepu, ´&lokalnyUchwytKlucza) == ERROR_SUCCESS; RegCloseKey(lokalnyUchwytKlucza); return wynik; } bool CRejestr::OtworzKlucz(const wchar_t* klucz,bool utworzJezeliNieIstnieje) { if(CzyKluczIstnieje(klucz)) return OtworzKlucz(klucz); else { if(!utworzJezeliNieIstnieje) return false; else return UtworzKlucz(klucz); } }
8. Warto uzupełnić konstruktor klasy CRejestr o polecenie, które będzie dbało
o zamknięcie klucza w momencie usuwania instancji tej klasy z pamięci (listing 5.5). Listing 5.5. Lepiej myśleć za programistę CRejestr::~CRejestr(void) { if(uchwytKlucza!=NULL) ZamknijKlucz(); }
160
Visual C++. Gotowe rozwiązania dla programistów Windows 9. Kolejny zbiór metod dotyczy operacji na wartościach. Możemy sprawdzić,
czy wartość jest zdefiniowana, odczytać ją, utworzyć (nadpisać) lub usunąć (listing 5.6). Poniższe funkcje składowe umożliwiają jedynie zapisanie i odczytanie dwóch typów zmiennych: liczby całkowitej i łańcucha Unicode. Metody dla innych typów można jednak bez problemu przygotować na ich wzór. Listing 5.6. Metody klasy CRejestr dotyczące wartości #pragma region Metody dotyczace wartosci #include bool CRejestr::CzyWartoscIstnieje(const wchar_t* nazwa) const { DWORD rozmiar = 1024; unsigned char bufor[1024]; LONG numerBledu = RegQueryValueEx(uchwytKlucza,nazwa,NULL,NULL,bufor,&rozmiar); if (numerBledu != ERROR_SUCCESS && numerBledu != ERROR_FILE_NOT_FOUND) WyswietlKomunikatBledu(numerBledu); return numerBledu == ERROR_SUCCESS; } bool CRejestr::UsunWartosc(const wchar_t* nazwa) const { LONG numerBledu = RegDeleteValue(uchwytKlucza,nazwa); if (numerBledu != ERROR_SUCCESS) WyswietlKomunikatBledu(numerBledu); return numerBledu == ERROR_SUCCESS; } int CRejestr::CzytajInt(const wchar_t* nazwa) const { int wartosc; DWORD rozmiar; LONG numerBledu = RegQueryValueEx(uchwytKlucza,nazwa,NULL, ´NULL,(LPBYTE)&wartosc,&rozmiar); if (numerBledu != ERROR_SUCCESS) { WyswietlKomunikatBledu(numerBledu); throw std::exception("Odczyt wartości nie powiódł się"); } return wartosc; } bool CRejestr::ZapiszInt(const wchar_t* nazwa,int wartosc) const { LONG numerBledu = RegSetValueEx(uchwytKlucza,nazwa,0,REG_DWORD,(LPBYTE)&wartosc,sizeof(wartosc)); if (numerBledu != ERROR_SUCCESS) WyswietlKomunikatBledu(numerBledu); return numerBledu == ERROR_SUCCESS; } wchar_t* CRejestr::CzytajString(const wchar_t* nazwa,wchar_t* wartosc) const { DWORD rozmiar = 1024; LONG numerBledu = RegQueryValueEx(uchwytKlucza,nazwa,NULL,NULL, ´(LPBYTE)wartosc,&rozmiar); if (numerBledu != ERROR_SUCCESS)
Rozdział 5. ♦ Rejestr systemu Windows { WyswietlKomunikatBledu(numerBledu); throw std::exception("Odczyt wartości nie powiódł się"); } return wartosc; } bool CRejestr::ZapiszString(const wchar_t* nazwa,const wchar_t* wartosc) const { DWORD rozmiar = (wcslen(wartosc)+1)*sizeof(wchar_t); LONG numerBledu = RegSetValueEx(uchwytKlucza,nazwa,0, ´REG_SZ,(LPBYTE)wartosc,rozmiar); if (numerBledu != ERROR_SUCCESS) WyswietlKomunikatBledu(numerBledu); return numerBledu == ERROR_SUCCESS; } #pragma endregion
10. Po zdefiniowaniu powyższego pokaźnego zbioru metod należy je wszystkie zadeklarować w klasie CRejestr w pliku nagłówkowym Rejestr.h. Prezentuje
to listing 5.7. Listing 5.7. Ostateczna postać pliku nagłówkowego klasy CRejestr #pragma once class CRejestr { private: HKEY uchwytKlucza; HKEY kluczGlowny; REGSAM trybDostepu; public: bool wyswietlajKomunikatyOBledach; public: CRejestr(HKEY kluczGlowny,REGSAM trybDostepu,bool ´wyswietlajKomunikatyOBledach=true); ~CRejestr(void); void WyswietlKomunikatBledu(LONG numerBledu) const; bool bool bool bool bool bool
UtworzKlucz(const wchar_t* klucz); OtworzKlucz(const wchar_t* klucz); ZamknijKlucz(); UsunKlucz(const wchar_t* klucz); CzyKluczIstnieje(const wchar_t* klucz) const; OtworzKlucz(const wchar_t* klucz,bool utworzJezeliNieIstnieje);
bool CzyWartoscIstnieje(const wchar_t* nazwa) const; bool UsunWartosc(const wchar_t* nazwa) const; int CzytajInt(const wchar_t* nazwa) const; bool ZapiszInt(const wchar_t* nazwa,int wartosc) const; wchar_t* CzytajString(const wchar_t* nazwa,wchar_t* wartosc) const; bool ZapiszString(const wchar_t* nazwa,const wchar_t* zawartosc) const; };
161
162
Visual C++. Gotowe rozwiązania dla programistów Windows
Pole prywatne uchwytKlucza klasy CRejestr (typu HKEY) służy do przechowywania uchwytu otwartego klucza. Metody UtworzKlucz i OtworzKlucz zapisują do niego uchwyty stworzonego lub otwartego klucza, natomiast metoda ZamknijKlucz korzysta z jego wartości, co powoduje, że zamyka ostatnio otwarty klucz. Z tego powodu blokowana jest możliwość otwarcia nowego klucza, bez zamknięcia poprzedniego. Jeżeli chcemy jednocześnie otworzyć dwa klucze, musimy powołać do życia dwie instancje klasy CRejestr. Z kolei pole kluczGlowny przechowuje uchwyt do klucza głównego (HKEY_CLASSES_ROOT, HKEY_CURRENT_CONFIG, HKEY_CURRENT_USER HKEY_LOCAL_MACHINE lub HKEY_USERS). Jest to pole prywatne, które może być ustawione jedynie w konstruktorze. Również prywatne jest pole trybDostepu, które przechowuje rodzaj dostępu do klucza głównego. W kolejnych przykładach będziemy korzystać tylko z pełnych uprawnień (stała KEY_ALL_ACCESS), trybu tylko do odczytu (KEY_READ) i tylko do zapisu (KEY_WRITE). Nie będziemy jednak sprawdzać tych uprawnień sami — robią to wywoływane przez nas funkcje WinAPI. Zwróćmy uwagę, że klasa CRejestr nie jest w żaden sposób zależna od biblioteki MFC. Można ją zatem wykorzystywać w każdym kompilatorze mającym dostęp do funkcji WinAPI.
Przechowywanie położenia i rozmiaru okna Przygotujmy funkcje, które będą odczytywać z rejestru i zapisywać w nim położenie i rozmiar okna. Jeżeli ich wywołania w odpowiedni sposób umieścimy w metodach OnInitDialog i OnDestroy klasy okna dialogowego (tę drugą trzeba stworzyć), to po uruchomieniu aplikacji okno pojawi się w miejscu, w którym zostało zamknięte w poprzedniej sesji. Obie funkcje dodamy do modułu Rejestr.h/Rejestr.cpp, aby umożliwić wygodne korzystanie z nich w innych projektach. Funkcje będą zapisywać we wskazanym kluczu rejestru lub odczytywać z niego cztery wartości typu całkowitego (REG_DWORD) o nazwach Left, Top, Width i Height, odpowiadające własnościom formy określającym jej położenie i rozmiar. Pierwszym argumentem obu funkcji będzie uchwyt do okna, drugim — ścieżka do klucza, w którym zostaną zapisane wymienione wcześniej wartości. Nic nie stoi na przeszkodzie, aby użyć tych funkcji dla kilku okien w przypadku aplikacji z wieloma oknami. Należy wówczas pamiętać o podaniu różnych kluczy rejestru. Do automatycznego wykreowania takiego klucza, innego dla każdej formy, można wykorzystać nadany przez programistę tytuł aplikacji oraz nazwę obiektu okna, np. "\\Software\\Helion\\"+AfxGetApp()->m_pszAppName+"\\"+title , gdzie title jest zmienną typu CString i zawiera właściwy tytuł okna odczytany za pomocą instrukcji this->GetWindowText(title);. Proszę zwrócić uwagę, że w nazwie klucza znak ukośnika \ (backslash) został podwojony. Jest tak dlatego, że znak \ używany jest w C++ do rozpoczynania sekwencji znaków specjalnych. Jedną z takich sekwencji jest właśnie \\. 1. Kontynuujemy rozwój projektu utworzonego w poprzednim podrozdziale.
Jest to projekt aplikacji MFC z oknem dialogowym, do którego dodaliśmy moduł Rejestr.h/Rejestr.cpp.
Rozdział 5. ♦ Rejestr systemu Windows
163
2. Do pliku nagłówkowego Rejestr.h dodajemy deklarację dwóch nowych funkcji: bool CzytajPolozenieIWielkoscOknaZRejestru(HWND okno,const wchar_t* klucz); void ZapiszPolozenieIWielkoscOknaWRejestrze(HWND okno,const wchar_t* klucz);
3. Definiujemy je w pliku z kodem źródłowym Rejestr.cpp według wzoru z listingu 5.8. Obie metody korzystają z klasy CRejestr, dzięki czemu ich kod
komentuje się w zasadzie sam. Listing 5.8. Plik nagłówkowy RejestrFunkcje.h #pragma region Przechowywanie położenia i rozmiaru okna bool CzytajPolozenieIWielkoscOknaZRejestru(HWND uchwytOkna,const wchar_t* klucz) { bool wynik=false; int left,top,width,height; CRejestr rejestr(HKEY_CURRENT_USER,KEY_READ,true); if(!rejestr.CzyKluczIstnieje(klucz)) return false; if(rejestr.OtworzKlucz(klucz)) { try { left = rejestr.CzytajInt(L"Left"); top = rejestr.CzytajInt(L"Top"); width = rejestr.CzytajInt(L"Width"); height = rejestr.CzytajInt(L"Height"); MoveWindow(uchwytOkna,left,top,width,height,TRUE); wynik=true; } catch(...) { MessageBox(NULL,L"Odczytanie położenia i wartości okna nie powiodło ´się",L"Błąd podczas odczytu położenia okna z rejestru",MB_OK); wynik=false; } } }
rejestr.ZamknijKlucz();
return wynik;
void ZapiszPolozenieIWielkoscOknaWRejestrze(HWND uchwytOkna,const wchar_t* klucz) { CRejestr rejestr(HKEY_CURRENT_USER,KEY_WRITE,true); if(!rejestr.CzyKluczIstnieje(klucz)) rejestr.UtworzKlucz(klucz); if(rejestr.OtworzKlucz(klucz)) { CRect rect; GetWindowRect(uchwytOkna,&rect); rejestr.ZapiszInt(L"Left",rect.left); rejestr.ZapiszInt(L"Top",rect.top); rejestr.ZapiszInt(L"Width",rect.Width()); rejestr.ZapiszInt(L"Height",rect.Height()); rejestr.ZamknijKlucz(); } } #pragma endregion
164
Visual C++. Gotowe rozwiązania dla programistów Windows 4. Umieścimy teraz wywołania tych funkcji w metodach klasy okna dialogowego.
Zacznijmy od dołączenia pliku nagłówkowego Rejestr.h. W tym celu w pliku RejestrSystemuWindowsDlg.cpp umieśćmy dyrektywę prekompilatora: #include „Rejestr.h”
5. Następnie do istniejącej metody OnInitDialog dodajemy instrukcję próbującą
odczytać wartości ze wskazanego klucza. Pokazuje to listing 5.9, w którym szarym tłem zaznaczone są dodane instrukcje. Brak klucza interpretowany jest jako pierwsze uruchomienie aplikacji. Listing 5.9. Metoda uruchamiana po załadowaniu okna BOOL CRejestrSystemuWinowsDlg::OnInitDialog() { CDialog::OnInitDialog(); // Set the icon for this dialog. The framework does this automatically // when the application's main window is not a dialog SetIcon(m_hIcon, TRUE); // Set big icon SetIcon(m_hIcon, FALSE); // Set small icon if (!CzytajPolozenieIWielkoscOknaZRejestru(this->m_hWnd, L"Software\\Helion\\Przyklad\\Okno")) { MessageBox(L"Pierwsze uruchomienie programu"); } }
return TRUE;
// return TRUE
unless you set the focus to a control
6. Polecenia zapisujące informacje o geometrii okna do klucza rejestru umieścimy w metodzie związanej z komunikatem WM_DESTROY, który wysyłany jest do okna
przed jego usunięciem z pamięci. Aby utworzyć tę metodę, przechodzimy do widoku projektowania okna, zaznaczamy okno i w podoknie Properties, na zakładce Messages odnajdujemy pozycję WM_DESTROY. Z rozwijanej listy przy tej pozycji wybieramy < Add > OnDestroy. Następnie dodajemy do niej kod wyróżniony w listingu 5.10. Listing 5.10. Metoda uruchamiana przed usunięciem okna z pamięci void CRejestrSystemuWinowsDlg::OnDestroy() { CDialog::OnDestroy();
}
ZapiszPolozenieIWielkoscOknaWRejestrze(this->m_hWnd, L"Software\\Helion\\Przyklad\\Okno");
7. Aby móc w pełni testować możliwość przechowywania zarówno położenia, jak i rozmiaru okna, musimy ustawić jego własność Border na Resizing. 8. Teraz wystarczy uruchomić aplikację (pojawi się komunikat informujący
o pierwszym uruchomieniu), zmienić położenie i rozmiar, a następnie zamknąć aplikację. Przy kolejnym uruchomieniu aplikacji okno powinno pojawić się w miejscu, w którym było w momencie zamykania.
Rozdział 5. ♦ Rejestr systemu Windows
165
Aby jeszcze bardziej upewnić się, że wartości zostały zapisane, możemy zajrzeć do rejestru systemowego za pomocą systemowego edytora rejestru regedit.exe (rysunek 5.2).
Rysunek 5.2. Klucz rejestru, w którym program zapisał informacje o położeniu i rozmiarze okna
Informacje o położeniu i rozmiarze okna to oczywiście tylko przykład informacji, jakie można przechowywać w rejestrze. Mogłyby to być m.in. ostatnio otwierane pliki, katalog tymczasowy aplikacji czy opcje skonfigurowane przez użytkownika.
Automatyczne uruchamianie aplikacji po zalogowaniu się użytkownika Automatyczne uruchomienie aplikacji w momencie logowania użytkownika można wymusić na kilka sposobów. Pierwszy z nich to utworzenie skrótu w menu Start/ Autostart danego użytkownika lub wszystkich użytkowników. Jest to jednak sposób, który użytkownik może wykorzystać samodzielnie, posługując się myszą. W przypadku programów bardziej właściwe wydaje się umieszczenie odpowiedniego zapisu w rejestrze, w kluczu Software\Microsoft\Windows\CurrentVersion\Run. Można to zrobić albo w kluczu prywatnym użytkownika (HKEY_CURRENT_USER), albo w kluczu systemowym (HKEY_LOCAL_MACHINE). W tym drugim przypadku aplikacja będzie uruchamiana po zalogowaniu każdego z użytkowników. Należy jednak pamiętać, że aby stworzyć zapis w tym kluczu, użytkownik musi mieć odpowiednio wysokie uprawnienia. Poza kluczem Run dostępne są jeszcze RunOnce i RunOnceEx, które pozwalają na jednorazowe uruchomienie aplikacji przy najbliższym uruchomieniu systemu (potem zapisy są usuwane).
166
Visual C++. Gotowe rozwiązania dla programistów Windows
Można też uruchamiać programy, a ściślej usługi, jeszcze przed pokazaniem ekranu logowania. W tym celu konieczne jest utworzenie klucza o nazwie HKEY_LOCAL_ MACHINE\Software\Microsoft\Windows\ CurrentVersion\RunServices i umieszczenie w nim odpowiedniego wpisu. Więcej informacji znajdzie Czytelnik w artykule INFO: Run, RunOnce, RunServices, RunServicesOnce and Startup w Bazie Wiedzy na stronach firmy Microsoft (ID artykułu: 179365).
Przygotujemy funkcję, która będzie umieszczała odpowiedni zapis w rejestrze. Oprócz niej potrzebna będzie również funkcja, która pozwoli na usunięcie tego zapisu, oraz funkcja sprawdzająca, czy zapis już istnieje. Aby uniknąć powielania kodu związanego z otwieraniem tego samego klucza rejestru, przygotujemy jedną funkcję Konfiguruj ´Autostart, wykonującą zasadnicze czynności związane z rejestrem (niedostępną poza modułem Rejestr.h/Rejestr.cpp), oraz trzy funkcje ułatwiające korzystanie z niej. Oczywiście tylko trzy ostatnie funkcje będą udostępniane przez interfejs modułu. 1. Do pliku z kodem źródłowym Rejestr.cpp dodajemy wewnętrzny typ pomocniczy,
określający wykonywaną na rejestrze czynność: enum CAutostartCzynnosci {acSprawdz=0,acZapisz=1,acUsun=2};
Typ ten zdefiniowany jest w pliku .cpp, a nie w pliku nagłówkowym .h, i dlatego mimo 3 importowania nagłówka pozostanie on dla kompilatora niewidoczny . Jest to typowa dla C++ kontrola zakresu w modułach. Może nie działa w sposób tak wyrafinowany, jak kontrola zakresu w klasach, ale dobrze odgrywa swoją rolę. 2. Następnie definiujemy funkcję KonfigurujAutostart, przedstawioną na listingu 5.11. Listing 5.11. W terminologii obiektowej poniższą funkcję można określić jako prywatną bool KonfigurujAutostart(HKEY kluczGlowny,const wchar_t* nazwa,const wchar_ ´t* sciezkaPliku,CAutostartCzynnosci czynnosc) { const wchar_t klucz[]=L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run"; bool wynik=false; CRejestr rejestr(kluczGlowny,(czynnosc==acSprawdz)?KEY_READ:KEY_WRITE); if(!rejestr.CzyKluczIstnieje(klucz)) { MessageBox(NULL,L"Klucz nie istnieje",L"Błąd podczas operacji na rejestrach ´autostartu",MB_ICONERROR); return false; } if(rejestr.OtworzKlucz(klucz)) { switch (czynnosc) { case acSprawdz: wynik = rejestr.CzyWartoscIstnieje(nazwa); break; case acZapisz: wynik = rejestr.ZapiszString(nazwa,sciezkaPliku); break; 3
Do pliku wynikowego typ ten zostanie dołączony dopiero w trakcie linkowania pliku Plik.cpp, który należy do projektu, ale nie jest kompilowany razem z głównymi plikami. Oczywiście sprawa wyglądałaby zupełnie inaczej, gdybyśmy użyli dyrektywy #include .
Rozdział 5. ♦ Rejestr systemu Windows
167
case acUsun: wynik = rejestr.UsunWartosc(nazwa); break; } rejestr.ZamknijKlucz(); //if (czynnosc!=acSprawdz) wynik=true; } return wynik; }
3. Definiujemy trzy funkcje, które udostępnimy użytkownikowi (listing 5.12). Listing 5.12. Kolejny zestaw funkcji gotowych do użycia w projektach Czytelnika bool CzyIstniejeZapisAutostart(HKEY kluczGlowny,const wchar_t* nazwa) { return KonfigurujAutostart(kluczGlowny,nazwa,L"",acSprawdz); } bool ZapiszAutostart(HKEY kluczGlowny,const wchar_t* nazwa,const wchar_ ´t* sciezkaPliku) { return KonfigurujAutostart(kluczGlowny,nazwa,sciezkaPliku,acZapisz); } bool UsunAutostart(HKEY kluczGlowny,const wchar_t* nazwa) { return KonfigurujAutostart(kluczGlowny,nazwa,L"",acUsun); }
4. Po czym do pliku nagłówkowego RejestrFunkcje.h dodajemy deklaracje
powyższych funkcji: bool bool ´t* bool
CzyIstniejeZapisAutostart(HKEY kluczGlowny,const wchar_t* nazwa); ZapiszAutostart(HKEY kluczGlowny,const wchar_t* nazwa,const wchar_ sciezkaPliku); UsunAutostart(HKEY kluczGlowny,const wchar_t* nazwa);
5. Aby sprawdzić działanie nowej funkcji, umieścimy w oknie dialogowym pole
opcji, którego zaznaczenie będzie decydować o umieszczeniu lub usunięciu z rejestru zapisu o automatycznym uruchomieniu bieżącej aplikacji. W tym celu otwieramy w zasobach podgląd okna dialogowego i na widocznej formie umieszczamy pole opcji CheckBox z etykietą Uruchom automatycznie (własność Caption). 6. Wiążemy nasze pole opcji ze zmienną publiczną o nazwie CheckBox1 (typ CButton). 7. Do metody OnInitDialog (plik RejestrSystemuWindowsDlg.cpp) dodajemy
polecenia sprawdzające, czy w rejestrze istnieje zapis automatycznego uruchamiania aplikacji, i na tej podstawie ustalamy zaznaczenie pola opcji (listing 5.13). Listing 5.13. Metoda OnInitDialog okna dialogowego BOOL CRejestrSystemuWinowsDlg::OnInitDialog() { CDialog::OnInitDialog();
168
Visual C++. Gotowe rozwiązania dla programistów Windows // Set the icon for this dialog. The framework does this automatically // when the application's main window is not a dialog SetIcon(m_hIcon, TRUE); // Set big icon SetIcon(m_hIcon, FALSE); // Set small icon if (!CzytajPolozenieIWielkoscOknaZRejestru(this->m_hWnd,L"Software\\Helion\\ ´Przyklad\\Okno")) { MessageBox(L"Pierwsze uruchomienie programu"); } checkBox1.SetCheck((int)CzyIstniejeZapisAutostart(HKEY_CURRENT_USER, AfxGetAppName())); return TRUE;
// return TRUE
unless you set the focus to a control
}
8. Klikając dwukrotnie podgląd pola opcji w oknie w widoku projektowania, tworzymy domyślną metodę zdarzeniową (zdarzenie BN_CLICKED) i umieszczamy
w niej kod tworzący bądź usuwający zapis z rejestru w zależności od tego, czy pole opcji jest zaznaczone (listing 5.14). Listing 5.14. Metoda uruchamiana po kliknięciu pola opcji void CRejestrSystemuWinowsDlg::OnBnClickedCheck1() { wchar_t sciezkaPliku[MAX_PATH]; GetModuleFileName(NULL,sciezkaPliku,MAX_PATH); if(checkBox1.GetCheck()) ZapiszAutostart(HKEY_CURRENT_USER,AfxGetAppName(),sciezkaPliku); else UsunAutostart(HKEY_CURRENT_USER,AfxGetAppName()); }
9. Po uruchomieniu aplikacji zmieniajmy stan pola opcji, obserwując jednocześnie
w edytorze rejestru zawartość klucza HKEY_CURRENT_USER\Software\ Microsoft\Windows\CurrentVersion\Run (edytor odświeża wyświetlane klucze po naciśnięciu klawisza F5). Po zaznaczeniu pola opcji powinna się w tym kluczu pojawić wartość o nazwie RejestrSystemuWindows typu REG_SZ. Po usunięciu zaznaczenia powinna ona zniknąć. 10. Jeżeli pole opcji jest zaznaczone (wartość jest zapisana w rejestrze), możemy
wylogować bieżącego użytkownika, a następnie zalogować się ponownie. Aplikacja powinna uruchomić się automatycznie. Należy zwrócić uwagę na to, by ścieżka do pliku zapisywana w rejestrze istniała w momencie logowania. Dotyczy to na przykład dysków sieciowych lub dysków wirtualnych, które mogą być mapowane po próbie uruchomienia aplikacji.
Rozdział 5. ♦ Rejestr systemu Windows
169
Umieszczanie informacji o zainstalowanym programie (aplet Dodaj/Usuń programy) Większość aplikacji daje się automatyczne odinstalować za pośrednictwem apletu Dodaj/ Usuń programy z panelu sterowania. Co więcej, w nowszych wersjach systemu Windows aplet ten umożliwia nie tylko odinstalowywanie, ale również modyfikację zainstalowanych składników oraz ich naprawę. Informacje o zainstalowanych aplikacjach, odczytywane przez ten aplet, umieszczane są w rejestrze, w kluczu systemowym HKEY_LOCAL_MACHINE, w podkluczu \SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall. Zadanie umieszczenia tam informacji o aplikacji należy do programu instalacyjnego. Zwykle korzysta się z automatów typu InstallShield, w których cały proces instalacji konfiguruje się za pomocą wygodnych kreatorów. Jednak czasem, np. w przypadku małej, „kompaktowej” aplikacji, informacje potrzebne do jej odinstalowania mogą być umieszczane w rejestrze przez tę samą aplikację. Zapis informacji o instalacji programu na potrzeby apletu Dodaj/Usuń programy wymaga odpowiednich uprawnień. Zwykły użytkownik może nie być w stanie nawet odczytać powyższego klucza. Ponadto w systemie Windows Vista mechanizm UAC może blokować dostęp do tego klucza rejestru nawet administratorom. Program musi żądać odpowiednich uprawnień, co powinno być zapisane w pliku manifestu aplikacji (zob. opis w rozdziale 3., podrozdział „Uruchamianie aplikacji w Windows Vista”). Przygotujemy funkcję, której argumentem będzie struktura zawierająca informacje o instalowanym programie przeznaczone do umieszczenia w kluczu. Struktura pozwoli uniknąć przekazywania kilkunastu argumentów do funkcji i dzięki nazwom pól ułatwi ustalanie właściwych wartości. Ponieważ nie wszystkie pola muszą znaleźć się w kluczu (aplet Dodaj/Usuń programy jest pod tym względem dość elastyczny), przyjmiemy, że pusta wartość pola łańcuchowego i wartość -1 dla liczby sygnalizuje, by informacja nie została zapisana. Nie dotyczy to tylko obowiązkowych wartości DisplayName i UninstallString — jeżeli będą one puste, wygenerowany zostanie wyjątek. Nowe funkcje dodamy do tej samej pary plików Rejestr.h i Rejestr.cpp, do której dodaliśmy poprzednie funkcje związane z rejestrem. Podobnie jak w przykładzie dotyczącym automatycznego uruchamiania programu, tak i teraz utworzymy funkcję pomocniczą KonfigurujDodajUsun, której użytkownik nie będzie używał, a która zapobiega niepotrzebnemu powtarzaniu kodu. Dla użytkownika przeznaczone będą trzy funkcje realizujące poszczególne czynności, a więc sprawdzanie obecności wpisu, zapisywanie danych i ich usuwanie. 1. Przechodzimy do pliku Rejestr.h i dodajemy do niego definicję rekordu
zawierającego wszystkie możliwe wartości, jakie można by zapisać w kluczu przechowującym dane o zainstalowanej aplikacji (listing 5.15). Ponieważ w większości składowymi tego rekordu są łańcuchy, dla wygody wyjątkowo wykorzystałem w ich deklaracji typ MFC CString. Listing 5.15. Struktura CDodajUsunInfo ułatwi przekazywanie dużej ilości parametrów do funkcji struct CDodajUsunInfo { CString //dla wygody korzystam z CString zamiast wchar_t[] DisplayName, DisplayIcon, DisplayVersion,
170
Visual C++. Gotowe rozwiązania dla programistów Windows Contact, Publisher, Comments, URLUpdateInfo, URLInfoAbout, InstallLocation, InstallSource, ProductID, UninstallString, UninstallPath, ModifyPath, Readme, HelpLink, HelpTelephone, RegCompany, RegOwner, LogFile; int VersionMajor, VersionMinor, NoRemove, NoRepair, Language; };
2. Tak jak w poprzednim projekcie, tak i w pliku Rejestr.cpp definiujemy typ
wyliczeniowy, który wykorzystamy do określenia rodzaju operacji na rejestrze. Przekazywany on jest jako argument do funkcji KonfigurujDodajUsun przez trzy funkcje, chciałoby się rzec, publiczne (listing 5.16). Listing 5.16. Należy pamiętać, że część wartości zapisanych w rejestrze zostanie zinterpretowana tylko przez Windows XP i późniejsze wersje Windows. Funkcja zadziała jednak w każdym systemie #pragma region Dodaj/Usun enum CDodajUsunCzynnosci {ducSprawdz=0,ducZapisz=1,ducUsun=2}; bool KonfigurujDodajUsun(const wchar_t* nazwaKlucza,CDodajUsunInfo ´rekord,CDodajUsunCzynnosci czynnosc) { const int rozmiar=1024; wchar_t klucz[rozmiar]=L"SOFTWARE\\Microsoft\\Windows\\ ´CurrentVersion\\Uninstall"; bool wynik=false; CRejestr rejestr(HKEY_LOCAL_MACHINE,KEY_ALL_ACCESS,true); if (!rejestr.CzyKluczIstnieje(klucz)) { MessageBox(NULL,L"W rejestrze nie ma klucza przechowującego informacje ´o instalacji aplikacji",L"Błąd",MB_OK | MB_ICONERROR); return false; } wcscat_s(klucz,rozmiar,L"\\"); wcscat_s(klucz,rozmiar,nazwaKlucza); bool kluczAplikacjiIstnieje=rejestr.CzyKluczIstnieje(klucz); if (czynnosc==ducSprawdz) return kluczAplikacjiIstnieje; if (kluczAplikacjiIstnieje) { switch (czynnosc) { case ducZapisz: wynik=false; break; case ducUsun: wynik=rejestr.UsunKlucz(klucz); } } else { switch (czynnosc) { case ducZapisz: wynik=rejestr.UtworzKlucz(klucz); if (wynik)
break;
Rozdział 5. ♦ Rejestr systemu Windows
171
{ //pola wymagane if (rekord.DisplayName!=L"") ´rejestr.ZapiszString(L"DisplayName",rekord.DisplayName); else throw std::exception("Pole \"DisplayName\" musi zawierać ´nazwę aplikacji."); if (rekord.UninstallString!=L"") rejestr.ZapiszString ´(L"UninstallString",rekord.UninstallString); else throw std::exception("Pole \"UninstallString\" musi zawierać ´ścieżkę do pliku instalatora."); //pola opcjonalne if (rekord.DisplayIcon!=L"") ´rejestr.ZapiszString(L"DisplayIcon",rekord.DisplayIcon); if (rekord.DisplayVersion!=L"") ´rejestr.ZapiszString(L"DisplayVersion",rekord.DisplayVersion); if (rekord.Contact!=L"") ´rejestr.ZapiszString(L"Contact",rekord.Contact); if (rekord.Publisher!=L"") ´rejestr.ZapiszString(L"Publisher",rekord.Publisher); if (rekord.Comments!=L"") ´rejestr.ZapiszString(L"Comments",rekord.Comments); if (rekord.URLUpdateInfo!=L"") ´rejestr.ZapiszString(L"URLUpdateInfo",rekord.URLUpdateInfo); if (rekord.URLInfoAbout!=L"") ´rejestr.ZapiszString(L"URLInfoAbout",rekord.URLInfoAbout); if (rekord.InstallLocation!=L"") ´rejestr.ZapiszString(L"InstallLocation",rekord.InstallLocation); if (rekord.InstallSource!=L"") ´rejestr.ZapiszString(L"InstallSource",rekord.InstallSource); if (rekord.ProductID!=L"") ´rejestr.ZapiszString(L"ProductID",rekord.ProductID); if (rekord.UninstallPath!=L"") ´rejestr.ZapiszString(L"UninstallPath",rekord.UninstallPath); if (rekord.ModifyPath!=L"") ´rejestr.ZapiszString(L"ModifyPath",rekord.ModifyPath); if (rekord.Readme!=L"") ´rejestr.ZapiszString(L"Readme",rekord.Readme); if (rekord.HelpLink!=L"") ´rejestr.ZapiszString(L"HelpLink",rekord.HelpLink); if (rekord.HelpTelephone!=L"") ´rejestr.ZapiszString(L"HelpTelephone",rekord.HelpTelephone); if (rekord.RegCompany!=L"") ´rejestr.ZapiszString(L"RegCompany",rekord.RegCompany); if (rekord.RegOwner!=L"") ´rejestr.ZapiszString(L"RegOwner",rekord.RegOwner); if (rekord.LogFile!=L"") ´rejestr.ZapiszString(L"LogFile",rekord.LogFile); if (rekord.VersionMajor!=-1) ´rejestr.ZapiszInt(L"VersionMajor",rekord.VersionMajor); if (rekord.VersionMinor!=-1) ´rejestr.ZapiszInt(L"VersionMinor",rekord.VersionMinor); if (rekord.NoRemove!=-1) ´rejestr.ZapiszInt(L"NoRemove",rekord.NoRemove); if (rekord.NoRepair!=-1) ´rejestr.ZapiszInt(L"NoRepair",rekord.NoRepair); if (rekord.Language!=-1) ´rejestr.ZapiszInt(L"Language",rekord.Language);
172
Visual C++. Gotowe rozwiązania dla programistów Windows rejestr.ZamknijKlucz(); } break; case ducUsun: wynik=false; break;
} } return wynik;
} #pragma endregion
3. Następnie definiujemy trzy funkcje, które korzystając z powyższej funkcji,
wykonują poszczególne czynności związane z tym kluczem rejestru. Należy je oczywiście zadeklarować w pliku nagłówkowym (listing 5.17). Listing 5.17. Metody, które po udostępnieniu w pliku nagłówkowym będą umożliwiały łatwe tworzenie klucza zawierającego informacje o zainstalowanym programie bool CzyIstniejeZapisDodajUsun(const wchar_t* nazwaKlucza) { CDodajUsunInfo rekord; return KonfigurujDodajUsun(nazwaKlucza,rekord,ducSprawdz); } bool ZapiszDodajUsun(const wchar_t* nazwaKlucza,CDodajUsunInfo rekord) { return KonfigurujDodajUsun(nazwaKlucza,rekord,ducZapisz); } bool UsunDodajUsun(const wchar_t* nazwaKlucza) { CDodajUsunInfo rekord; return KonfigurujDodajUsun(nazwaKlucza,rekord,ducUsun); }
Funkcja KonfigurujDodajUsun rozpoczyna działanie od sprawdzenia, czy w rejestrze istnieje klucz przechowujący informacje o zainstalowanych programach. Jego brak jest jednak raczej nieprawdopodobny. Następnie sprawdzana jest obecność podklucza zawierającego te informacje dla naszej aplikacji. Jeżeli celem wywołania funkcji jest sprawdzenie obecności tego klucza, jej działanie może być już zakończone. W przeciwnym przypadku, jeżeli klucz istnieje, a zadaniem funkcji jest jego skasowanie — jest usuwany. Najciekawszy i największy fragment kodu dotyczy oczywiście sytuacji, w której klucz jeszcze nie istnieje, a zadaniem funkcji jest jego utworzenie. Zauważmy, że powyższe funkcje są dość elastyczne. Mogą być użyte w aplikacji umieszczającej informacje o samej sobie w rejestrze; mogą też być użyte w projektowanym przez nas instalatorze. Poniższy przykład zakłada pierwszą z tych sytuacji. 1. Umieszczamy na oknie dialogowym dwa przyciski. 2. Zaznaczamy pierwszy przycisk. Zmieniamy jego etykietę na np. Dodaj zapis
o zainstalowanym programie. Następnie klikamy go dwukrotnie, aby utworzyć metodę zdarzeniową, którą uzupełniamy zgodnie ze wzorem z listingu 5.18.
Rozdział 5. ♦ Rejestr systemu Windows
173
Listing 5.18. W poniższym przykładzie pomijamy informację o powodzeniu operacji, sygnalizowaną poprzez wartość zwracaną przez zdefiniowane przez nas wyżej funkcje #include void CRejestrSystemuWindowsDlg::OnBnClickedButton1() { wchar_t sciezkaPliku[MAX_PATH]; GetModuleFileName(NULL,sciezkaPliku,MAX_PATH); wchar_t katalogRoboczy[MAX_PATH]; GetCurrentDirectory(MAX_PATH,katalogRoboczy); wchar_t nazwaAplikacji[1024]; wcscpy_s(nazwaAplikacji,1024,AfxGetApp()->m_pszAppName); CDodajUsunInfo strukturaDodajUsun; strukturaDodajUsun.DisplayName="Tester edycji rejestru systemu Windows"; strukturaDodajUsun.DisplayIcon=sciezkaPliku; strukturaDodajUsun.DisplayVersion=L"1.1"; strukturaDodajUsun.Contact=L"[email protected]"; strukturaDodajUsun.Publisher=L"Helion"; strukturaDodajUsun.Comments=L"Aplikacja testująca możliwość edycji rejestru"; strukturaDodajUsun.URLUpdateInfo=L"http://www.fizyka.umk.pl/~jacek/"; strukturaDodajUsun.URLInfoAbout=L"http://helion.pl"; strukturaDodajUsun.InstallLocation=katalogRoboczy; strukturaDodajUsun.InstallSource=katalogRoboczy; strukturaDodajUsun.UninstallString=sciezkaPliku+CString(" /uninstall"); strukturaDodajUsun.UninstallPath=sciezkaPliku+CString(" /uninstall"); strukturaDodajUsun.ModifyPath=sciezkaPliku+CString(" /modify"); strukturaDodajUsun.HelpLink=L"http://www.fizyka.umk.pl/~jacek/"; strukturaDodajUsun.VersionMajor=1; strukturaDodajUsun.VersionMinor=0; strukturaDodajUsun.NoRepair=1; strukturaDodajUsun.Language=-1; //nie zapisywana try { if (!ZapiszDodajUsun(nazwaAplikacji,strukturaDodajUsun)) throw std::exception("Błąd przy zapisywaniu informacji o instalacji"); MessageBox(L"Klucz ("+CString(nazwaAplikacji)+L") z informacją o instalacji ´został utworzony."); } catch(std::exception& exc) { MessageBox(CString(exc.what()), L"Błąd przy zapisywaniu informacji o instalacji",MB_ICONERROR); } }
3. Następnie zaznaczamy przycisk Button2 i zmieniamy jego etykietę na Usuń
zapis o zainstalowanym programie. Tworzymy domyślną metodę zdarzeniową i umieszczamy w niej polecenia z listingu 5.19.
174
Visual C++. Gotowe rozwiązania dla programistów Windows
Listing 5.19. Domyślna metoda zdarzeniowa przycisku z etykietą Usuń zapis void CRejestrSystemuWindowsDlg::OnBnClickedButton2() { if (UsunDodajUsun(AfxGetApp()->m_pszAppName)) MessageBox(L"Klucz z informacją o instalacji programu został usunięty"); else MessageBox(L"Usunięcie klucza z informacją o instalacji nie powiodło się"); }
4. Dostęp do klucza HKEY_LOCAL_MACHINE wymaga wyższych uprawnień. W Windows
Vista oznacza to, że użytkownik musi być administratorem, a aplikacja powinna mieć dostęp do wszystkich jego uprawnień (w sensie kontroli konta użytkownika UAC). Najwygodniej będzie tak zmodyfikować plik manifestu aplikacji, aby automatycznie pobierała odpowiednie uprawnienia. W tym celu w ustawieniach projektu (menu Project, RejestrSystemuWindows Properties…) na zakładce Configuration Properties, Linker, Manifest File ustawiamy pozycję UAC Execution Level na requireAdministrator (por. informacje w rozdziale 3.). Uruchomienie apletu Dodaj/Usuń programy z poziomu aplikacji możliwe jest za pomocą polecenia ShellExecute(NULL,L"open",L"control.exe",L"appwiz.cpl",´L"",SW_SHOW);.
Jeżeli po uruchomieniu programu klikniemy przycisk Dodaj zapis o zainstalowanym programie, do rejestru dodany zostanie klucz zawierający informacje o tym programie zgodnie z zawartością struktury CDodajUsunInfo z listingu 5.15. Obecność tego klucza można oczywiście sprawdzić za pomocą edytora rejestru (rysunek 5.3). Ważniejsze jest jednak, że informacje o aplikacji pojawią się w aplecie Panelu sterowania (rysunek 5.4). Rysunek 5.3. Dodany klucz w edytorze rejestru
Rozdział 5. ♦ Rejestr systemu Windows
175
Rysunek 5.4. Informacje z klucza prezentowane przez aplet Dodaj/Usuń programy w wersji z Windows XP i Windows Vista
176
Visual C++. Gotowe rozwiązania dla programistów Windows
Gdzie jest katalog z moimi dokumentami? Programista może odnaleźć w rejestrze wiele ciekawych informacji o profilu użytkownika i o konfiguracji systemu. Wystarczy tylko wiedzieć, gdzie szukać. W kolejnym przykładzie odczytamy z rejestru pełną ścieżkę katalogów specjalnych użytkownika, w tym domyślnego katalogu dokumentów (zwykle Moje dokumenty). Przygotujemy dwie funkcje (listing 5.20). Pierwsza będzie odczytywała katalog wskazany przez podany przez użytkownika identyfikator. Nazwy identyfikatorów możemy znaleźć w rejestrze użytkownika (tj. w kluczu głównym HKEY_CURRENT_USER), w podkluczu \Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders (rysunek 5.6). Druga funkcja będzie korzystać z tej pierwszej do odczytania ścieżki do katalogu Moje dokumenty użytkownika, kryjącej się w rejestrze pod nazwą Personal. Obie funkcje umieścimy w pliku Rejestr.cpp i zadeklarujemy w pliku nagłówkowym Rejestr.h. Listing 5.20. Argumentem pierwszej funkcji może być nazwa jednej z widocznych na rysunku 5.6 wartości rejestru, zawierających ścieżkę katalogu specjalnego wchar_t* PobierzSciezkeDoKataloguSpecjalnego(const wchar_t* nazwa,wchar_t* katalog) { const wchar_t klucz[]=L"Software\\Microsoft\\Windows\\ ´CurrentVersion\\Explorer\\Shell Folders";
}
CRejestr rejestr(HKEY_CURRENT_USER,KEY_READ,true); if (rejestr.OtworzKlucz(klucz)) { if (rejestr.CzyWartoscIstnieje(nazwa)) rejestr.CzytajString(nazwa,katalog); else throw std::exception("Wartość nie istnieje"); rejestr.ZamknijKlucz(); } else throw std::exception("Klucz nie istnieje lub nie może być otwarty"); return katalog;
wchar_t* PobierzSciezkeDoKataloguMojeDokumenty(wchar_t* katalog) { return PobierzSciezkeDoKataloguSpecjalnego(L"Personal",katalog); }
Jak wiemy z poprzedniego rozdziału, tę samą informację można uzyskać za pomocą funkcji SHGetSpecialFolderPath. Użycie tej funkcji zaleca zresztą obecna w tym kluczu wartość !Do not use this registry key (rysunek 5.5).
Dodawanie pozycji do menu kontekstowego związanego z zarejestrowanym typem pliku Jeżeli chcemy, aby napisany przez nas program pełniący rolę edytora konkretnego formatu plików o ustalonym rozszerzeniu mógł być uruchamiany z menu kontekstowego pliku, np. w Eksploratorze Windows, musimy dodać wpis do rejestru w kluczu
Rozdział 5. ♦ Rejestr systemu Windows
177
Rysunek 5.5. Proszę zwrócić uwagę na pierwszą (poza domyślną) wartość w kluczu
głównym HKEY_CLASSES_ROOT. Dla uproszczenia zajmiemy się przypadkiem, w którym istnieje już klucz dla danego typu plików. Załóżmy dla przykładu, że dodajemy nowe polecenie do menu związanego z plikiem .rtf. Wówczas musimy zacząć od odczytania z klucza HKEY_CLASSES_ROOT\.rtf opisu tego typu pliku. W przypadku plików .rtf może to być np. Word.RTF.8. Jest to nie tylko nazwa kodowa typu plików, ale także nazwa klucza w tym samym kluczu głównym, zawierającego więcej informacji o plikach z tym rozszerzeniem. W szczególności zawiera on podklucz shell, który przechowuje informacje o elementach menu kontekstowego związanego z plikami .rtf (każda pozycja w osobnym podkluczu). Aby stworzyć nową pozycję w tym menu kontekstowym, otwieramy klucz Word.RTF.8\shell, tworzymy w nim podklucz o nazwie np. edit1 i w podkluczu tym zapisujemy wartość domyślną, która będzie jednocześnie etykietą pozycji, widoczną w menu kontekstowym, np. Edytuj w Project1. Następnie musimy stworzyć kolejny podklucz Word.RTF.8\shell\edit2\Command i zapisać w nim polecenie, które ma uruchamiać edytor i wczytywać plik. Zazwyczaj jest to ścieżka do edytora wraz z parametrem "%1", oznaczającym pełną ścieżkę do pliku .rtf, na rzecz którego wywołane zostało menu kontekstowe. Oczywiście zakładamy, że edytor uruchamiany z parametrem będzie „wiedział”, co z podaną w parametrze ścieżką zrobić. Listing 5.21 zawiera kod funkcji RozszerzMenuKontekstoweIstniejacegoTypuPliku realizującej przedstawiony algorytm. Listing 5.21. Funkcja odnajduje właściwy klucz i dopisuje do niego wartość opisującą nową pozycję menu kontekstowego związanego z rozszerzeniem pliku void RozszerzMenuKontekstoweIstniejacegoTypuPliku(const wchar_t* rozszerzenie, ´const wchar_t* polecenie,const wchar_t* opis) { const int DLG_NAZW=256; CRejestr rejestr(HKEY_CLASSES_ROOT,KEY_ALL_ACCESS);
178
Visual C++. Gotowe rozwiązania dla programistów Windows //Odczytywanie nazwy typu pliku wchar_t _rozszerzenie[DLG_NAZW]; if(rozszerzenie[0]!='.') wcscpy_s(_rozszerzenie,DLG_NAZW,L"."); wcscat_s(_rozszerzenie,DLG_NAZW,rozszerzenie); wchar_t nazwa[DLG_NAZW]; if (rejestr.OtworzKlucz(_rozszerzenie)) { rejestr.CzytajString(L"",nazwa); rejestr.ZamknijKlucz(); } else throw std::exception("Rozszerzenie nie jest zarejestrowane"); //Czy polecenie jest już zdefiniowane wchar_t klucz[DLG_NAZW]; wcscpy_s(klucz,DLG_NAZW,nazwa); wcscat_s(klucz,DLG_NAZW,L"\\shell\\"); wcscat_s(klucz,DLG_NAZW,polecenie); if (rejestr.CzyKluczIstnieje(klucz)) throw std::exception("Polecenie jest już ´zdefiniowane"); //Tworzenie klucza dla polecenia if (rejestr.UtworzKlucz(klucz)) { rejestr.ZapiszString(L"",opis); rejestr.ZamknijKlucz(); wcscat_s(klucz,DLG_NAZW,L"\\Command"); if (rejestr.UtworzKlucz(klucz)) { wchar_t sciezkaPliku[MAX_PATH]; GetModuleFileName(NULL,sciezkaPliku,DLG_NAZW); wchar_t polecenie[DLG_NAZW]; wcscpy_s(polecenie,DLG_NAZW,L"\""); wcscat_s(polecenie,DLG_NAZW,sciezkaPliku); wcscat_s(polecenie,DLG_NAZW,L"\" \"%1\""); rejestr.ZapiszString(L"",polecenie); rejestr.ZamknijKlucz(); } else throw std::exception("Utworzenie podklucza Command nie jest możliwe"); } else throw std::exception("Utworzenie klucza nie jest możliwe"); }
Aby sprawdzić działanie tej funkcji, wywołajmy ją w programie, który rozwijaliśmy do tej pory, np. w domyślnej metodzie zdarzeniowej nowego przycisku. Kod tej metody widoczny jest na listingu 5.22. W efekcie w menu kontekstowym plików .rtf pojawi się nowa pozycja Pokaż ścieżkę do pliku (rysunek 5.6). Wybranie jej spowoduje uruchomienie programu. Jednak nasz program nie sprawdza parametrów uruchomienia i nic z podaną w pierwszym argumencie ścieżką do pliku nie zrobi. Aby wyświetlić ścieżkę, dodajmy do metody InitInstance klasy aplikacji (plik RejestrSystemuWindows.cpp) polecenia wyróżnione na listingu 5.234. 4
Dysponując pełną ścieżką do pliku, możemy zrobić z nim cokolwiek, w szczególności wczytać jego zawartość do kontrolki Rich Edit 2.0 Control.
Rozdział 5. ♦ Rejestr systemu Windows
179
Listing 5.22. Tworzenie klucza rejestru powodującego dodanie nowej pozycji w menu kontekstowym plików .rtf void CRejestrSystemuWindowsDlg::OnBnClickedButton5() { try { RozszerzMenuKontekstoweIstniejacegoTypuPliku(L"rtf",L"edit1",L"Pokaż ścieżkę ´do pliku"); } catch(std::exception& exc) { MessageBox(CString(exc.what()),L"Błąd",MB_ICONERROR); } }
Rysunek 5.6. Nowe polecenie w menu kontekstowym plików RTF Listing 5.23. Jeżeli program został uruchomiony z parametrem, to wartość parametru zostanie pokazana za pomocą funkcji MessageBox BOOL CRejestrSystemuWindowsApp::InitInstance() { if(this->m_lpCmdLine[0] != _T('\0')) MessageBox(NULL, CString("Program został uruchomiony z parametrem: ")+this->m_lpCmdLine, L"Informacja",MB_ICONINFORMATION); ...
180
Visual C++. Gotowe rozwiązania dla programistów Windows
Obsługa rejestru i plików INI za pomocą MFC W bibliotece MFC, a dokładniej w klasie CWinApp, dodano obsługę rejestru. Niestety umożliwia ona jedynie zapis i odczyt informacji w podkluczu HKEY_CURRENT_ USER\Software. To wystarcza do przechowywania informacji aplikacji na jej własne potrzeby, ale np. uniemożliwia ingerowanie w ustawienia systemu. Co ciekawe, te same funkcje, które służą do obsługi rejestru, pozwalają także na posługiwanie się plikami INI. Pokażemy to w kolejnych przykładach. Na podstawie poprzedniego przykładu pokazane będzie, jak przechować rozmiar okna i jego położenie w inny sposób, który będzie można później łatwo wykorzystać podczas pracy z plikami INI. Zarówno użycie rejestru, jak i plików INI zilustrujemy na przykładzie, który był już omawiany na początku rozdziału, a mianowicie zapiszemy, a potem odczytamy położenie i rozmiar formy. Dodatkowo wykorzystamy możliwość tworzenia plików INI do przygotowania skrótu internetowego umieszczanego zazwyczaj na pulpicie lub w menu Start.
Przechowywanie położenia i rozmiaru okna w rejestrze (MFC) 1. Stwórz nowy projekt aplikacji MFC z oknem dialogowym. Nazwij aplikację
RejestrMFC. 2. Przejdź do pliku RejestrMFC.cpp, tj. do pliku, w którym zapisana została klasa
aplikacji. 3. W metodzie CRejestrMFCApp::InitInstance znajdziesz polecenie ustalające
nazwę klucza, w którym zapisywane będą ustawienia aplikacji: SetRegistryKey(_T("Local AppWizard-Generated Applications"));
Łańcuch będący argumentem tej metody klasy CWinApp należy zmienić, tak aby odpowiadał nazwie firmy produkującej aplikację, np. SetRegistryKey(_T("Helion"));
MFC samo doda do niego jeszcze nazwę aplikacji. W efekcie klucz, do którego dodawane będą wartości, będzie następujący: HKEY_CURRENT_USER\Software\Helion\RejestrMFC\ RejestrSystemuWindows. Jest on dostępny w klasie aplikacji w polu CWinApp::m_pszRegistryKey. 4. Przejdźmy teraz do pliku RejestrMFCDlg.cpp zawierającego klasę dialogową okna, stwórzmy metodę związaną z komunikatem WM_DESTROY wysyłanym do okna
i umieśćmy w niej kod zapisujący położenie okna w rejestrze (listing 5.24).
Rozdział 5. ♦ Rejestr systemu Windows
181
Listing 5.24. Zapis położenia okna za pomocą metod MFC void CRejestrMFCDlg::OnDestroy() { CDialog::OnDestroy(); CWinApp* aplikacja=AfxGetApp(); CRect rect; this->GetWindowRect(rect); if((aplikacja->WriteProfileInt(L"Okno",L"Left",rect.left) != 0) && (aplikacja->WriteProfileInt(L"Okno",L"Top",rect.top) != 0) && (aplikacja->WriteProfileInt(L"Okno",L"Width",rect.Width()) != 0) && (aplikacja->WriteProfileInt(L"Okno",L"Height",rect.Height()) != 0)) { //MessageBox(L"Położenie i rozmiar okna zostały zapisane"); } else { //MessageBox(L"Zapis położenia i rozmiaru okna nie powiódł się"); } }
5. Natomiast w metodzie CRejestrMFCDlg::OnInitDialog umieszczamy polecenia
odczytujące położenie i rozmiar okna oraz ustawiające okno według nich (listing 5.25). Listing 5.25. Odczyt położenia i rozmiaru okna za pomocą metod MFC BOOL CRejestrMFCDlg::OnInitDialog() { CDialog::OnInitDialog(); // Set the icon for this dialog. The framework does this automatically // when the application's main window is not a dialog SetIcon(m_hIcon, TRUE); // Set big icon SetIcon(m_hIcon, FALSE); // Set small icon CWinApp* aplikacja=AfxGetApp(); int left,top,width,height; left = aplikacja->GetProfileInt(L"Okno",L"Left",-1); top = aplikacja->GetProfileInt(L"Okno",L"Top",-1); width = aplikacja->GetProfileInt(L"Okno",L"Width",-1); height = aplikacja->GetProfileInt(L"Okno",L"Height",-1); if(left != -1 && top != -1 && width != -1 && height != -1) this->MoveWindow(left,top,width,height,TRUE); else MessageBox(L"Pierwsze uruchomienie aplikacji"); return TRUE;
// return TRUE
unless you set the focus to a control
}
6. Pozostaje tylko przetestować nowy kod. W tym celu zmieńmy własność Border okna na Resizing, zmieńmy jego położenie i rozmiar, a następnie zamknijmy
i ponownie uruchommy aplikację.
182
Visual C++. Gotowe rozwiązania dla programistów Windows
Po utworzeniu projektu aplikacji MFC w metodzie inicjującej klasę aplikacji znajduje się wywołanie metody SetRegistryKey, które ustala klucz rejestru. Klucz ten nie jest wówczas tworzony; tworzy je pierwsze wywołanie jednej z metod służących do zapisu danych (WriteProfileInt czy WriteProfileStringW).
Przechowywanie położenia i rozmiaru okna w pliku INI (MFC) Pliki INI są starszym, bo pochodzącym jeszcze z Windows 3.x, mechanizmem przechowywania danych przez aplikacje i system. Nadal mogą być jednak używane; bywają przydatne, szczególnie gdy np. zależy nam na możliwości łatwego przenoszenia plików konfigurujących między komputerami. Pliki INI mają jednopoziomową strukturę. Składają się z kluczy o schemacie nazwa_klucza=wartość, uporządkowanych w grupach. Grupa deklarowana jest w osobnej linii przez podanie nazwy grupy ujętej w nawiasy kwadratowe, np. [Grupa]. Poniżej zostaną zaprezentowane dwa przykłady użycia plików INI. W pierwszym odtworzymy funkcjonalność poprzedniej aplikacji, zmieni się jedynie sposób przechowywania danych. W drugim przygotujemy skrót internetowy (plik .url). Przekształcimy teraz projekt z poprzedniego podrozdziału, w którym położenie i rozmiar okna zapisywane są w rejestrze, w taki sposób, aby dane zapisywane były do pliku INI znajdującego się w katalogu roboczym aplikacji. Dobra wiadomość jest taka, że aby to zadanie zrealizować, należy jedynie zmienić wartość łańcucha przechowywanego w polu CWinApp::m_pszProfileName. Zamiast nazwy klucza powinna teraz zawierać ścieżkę do pliku. To oznacza, że w pliku RejestrMFC.cpp, w metodzie CRejestrMFCApp::InitInstance zamieniamy wywołanie metody SetRegistryKey przez ustalenie wartości pola m_pszProfileName (listing 5.26). I to wystarczy, aby metody CWinApp::WriteProfileInt i CWinApp::WriteProfileStringW zapisywały ustawienia do pliku okno.ini w bieżącym katalogu (o czym decyduje kropka na początku ścieżki do pliku). Listing 5.26. Zmiany w metodzie CRejestrMFCApp::InitInstance (plik RejestrMFC.cpp) konieczne do zmiany rejestru na plik INI BOOL CRejestrMFCApp::InitInstance() { ... // // // // // // //
Standard initialization If you are not using these features and wish to reduce the size of your final executable, you should remove from the following the specific initialization routines you do not need Change the registry key under which our settings are stored TODO: You should modify this string to be something appropriate such as the name of your company or organization
SetRegistryKey(_T("Helion")); this->m_pszProfileName=_tcsdup(L".\\okno.ini"); }
...
Rozdział 5. ♦ Rejestr systemu Windows
183
Należy pamiętać, że domyślnym katalogiem, w którym przechowywane są pliki INI, jest katalog systemu Windows (zazwyczaj C:\Windows), do którego aplikacje bez specjalnych zabiegów nie mają dostępu. Jeżeli chcemy, żeby był to inny katalog, należy podać pełną ścieżkę do pliku.
Skrót internetowy (.url) Skrót internetowy jest typowym plikiem INI, ale o rozszerzeniu .url, zawierającym z góry ustalone klucze. W grupie InternetShortcut można umieścić klucz URL, którego wartością jest adres (łącznie z protokołem) strony WWW lub dokumentu. Kolejny klucz, o nazwie IconFile, zawiera położenie pliku ikony, która jest używana do prezentacji skrótu. W kluczu IconIndex przechowywany jest numer ikony w tym pliku. Oto przykład typowego skrótu internetowego: [InternetShortcut] URL=http://www.fizyka.umk.pl/~jacek/ IconFile=c:\windows\explorer.exe IconIndex=5
1. Tworzymy nowy projekt typu MFC Application o nazwie IniUrlMFC. 2. Przechodzimy do pliku źródłowego zawierającego metody klasy aplikacji, tj. do pliku IniUrlMFC.cpp, i z metody InitInstance usuwamy linię zawierającą wywołanie metody SetRegistryKey. 3. W pliku IniUrlMFCDlg.cpp definiujemy funkcję StworzSkrotInternetowy
(listing 5.27). Należy zwrócić uwagę, że dwa ostatnie argumenty mają ustalone wartości domyślne — nie ma więc konieczności podawania ich przy wywołaniu funkcji. Listing 5.27. Funkcja tworząca pliki INI — skróty internetowe void StworzSkrotInternetowy(CString nazwaPliku,CString adresURL,CString ´plikIkony=L"",int numerIkony=0) { CWinApp* aplikacja = AfxGetApp(); free((void*)aplikacja->m_pszProfileName); aplikacja->m_pszProfileName = _tcsdup(nazwaPliku); aplikacja->WriteProfileString(L"InternetShortcut",L"URL",adresURL); if (!plikIkony.IsEmpty()) { aplikacja->WriteProfileString(L"InternetShortcut",L"IconFile",plikIkony); CString numerIkonyStr; numerIkonyStr.Format(L"%d",numerIkony); aplikacja->WriteProfileString(L"InternetShortcut", ´L"IconIndex",numerIkonyStr); } }
Obowiązkowymi argumentami funkcji są ścieżka do pliku .url i pełny adres URL. Kolejne dwa, które mogą być pominięte, to plik zawierający ikonę oraz jej numer w tym pliku. Druga i trzecia linia powyższej funkcji odpowiedzialne są za zmianę nazwy pliku INI
184
Visual C++. Gotowe rozwiązania dla programistów Windows
zapisanej w polu CWinAPP::m_pszProfileName. Biblioteka MFC domyślnie tworzy nazwę pliku z nazwy projektu. A oto trzy przykłady użycia tej funkcji: StworzSkrotInternetowy(L".\\Jacek Matulewski - Strona domowa.url", L"http://www.phys.uni.torun.pl/~jacek/",NULL,0); StworzSkrotInternetowy(L".\\jm.url",L"http://www.phys.uni.torun.pl/~jacek/", L"c:\\windows\\explorer.exe",5); wchar_t filePath[MAX_PATH]; GetModuleFileName(NULL,filePath,MAX_PATH); StworzSkrotInternetowy(CString(".\\")+AfxGetApp()-> ´m_pszAppName+CString(".url"),CString("file://")+filePath,filePath,0);
Pierwszy z nich to typowe zastosowanie funkcji do utworzenia skrótu do strony WWW, w którym wykorzystywana jest ikona domyślna. Drugi przykład pokazuje, jak można skrótowi nadać własną ikonę. Ciekawy jest ostatni przykład — rezultatem jest utworzenie skrótu do aplikacji, z której funkcja została wywołana. Mimo że jest to skrót internetowy .url, dzięki wykorzystaniu protokołu file:// działa jak typowy skrót .lnk.
Rozdział 6.
Komunikaty Windows Komunikat Windows (ang. Windows message) to mechanizm przekazywania informacji do aplikacji przez system lub inne uruchomione w nim aplikacje (rzecz odbywa się w obrębie jednego systemu). Komunikaty dotyczą szczególnej sytuacji w systemie, związanej bądź z działaniem użytkownika (np. przesunięcie myszy, naciśnięcie klawisza, zamknięcie okna itp.), bądź wynikającej z funkcjonowania systemu (np. odświeżenie okna, zamknięcie sesji Windows itp.). Przekazywana przez komunikat informacja dotyczy przede wszystkim rodzaju zdarzenia, którego komunikat jest skutkiem (identyfikator komunikatu), oraz związanych z tą sytuacją parametrów (może to być położenie myszy lub naciśnięty klawisz).
Pętla główna aplikacji Tworząc od zera aplikację dla platformy Win32, musimy w funkcji zwrotnej WinMain przygotować tzw. pętlę główną odbierającą komunikaty z kolejki komunikatów aplikacji i przesyłającą je do odpowiednich okien kontrolek. Oto przykład takiej pętli umieszczony w funkcji Run wywoływanej z WinMain: WPARAM Run() { //Pętla główna - obsługa komunikatów MSG msg; while(GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } return msg.wParam; }
Za odbiór komunikatów z kolejki odpowiedzialna jest funkcja GetMessage. Jeżeli chcielibyśmy tylko sprawdzić, jaki komunikat jest w kolejce bez usuwania go, możemy wykorzystać funkcję PeekMessage. Obie wypełniają strukturę MSG informacjami o komunikacie. Wartość zwracana przez GetMessage jest niezerowa. Wyjątkiem jest sytuacja, w której
186
Visual C++. Gotowe rozwiązania dla programistów Windows
odebrany komunikat to WM_QUIT (informacja o żądaniu zamknięcia aplikacji). Działanie pętli kończy się zatem w momencie zamykania aplikacji. Wewnątrz pętli wywoływane są dwie funkcje: TranslateMessage i DispatchMessage. Pierwsza odpowiedzialna jest za przekształcanie komunikatów związanych z klawiaturą. Podstawowym komunikatem informującym o wciśnięciu klawisza jest WM_KEYDOWN. Przenoszona jest w nim informacja o naciśniętym lub zwolnionym „fizycznym klawiszu” na klawiaturze. Dzięki funkcji TranslateMessage do kolejki wysyłany jest komunikat WM_CHAR, w którym informacja o znaku jest przechowywana za pomocą łatwiejszego w użyciu kodu UTF-16. Z kolei funkcja DispatchMessage odpowiedzialna jest za wysłanie komunikatu do odpowiedniej procedury okna (ang. window procedure), a więc funkcji wskazanej podczas tworzenia okna (zob. opis funkcji CreateWindow w MSDN), która jest odpowiedzialna za odpowiednie zareagowanie aplikacji na otrzymany komunikat. Zwykle działanie procedury okna ogranicza się do uruchamiania odpowiednich metod związanych z komunikatami (ang. message handlers), tj. zawiera instrukcję switch rozdzielającą sterowanie do poszczególnych funkcji lub metod. Natomiast w aplikacjach przygotowywanych w Visual C++ z użyciem biblioteki MFC, w których pętla główna komunikatów i procedura okna zaszyte są w klasie okna CWnd, wiązanie metod z komunikatami odbywa się za pomocą makr mapujących, które omówione zostały w rozdziale 1. (podrozdział „Wiązanie komunikatów”). Etapy systemowej obsługi komunikatów są następujące: 1. W reakcji na sytuację w systemie lub działanie użytkownika system tworzy
strukturę komunikatu zawierającą uchwyt okna, którego dotyczy komunikat, numer zdarzenia systemowego (w MSDN opisane są stałe identyfikujące komunikaty, nazwy tych stałych zaczynają się zawsze od WM_) oraz dwie liczby przechowujące informację o zdarzeniu wParam i lParam. Znaczenie parametrów wParam i lParam zależy od typu komunikatu i przechowuje informacje specyficzne dla danego komunikatu. 2. Struktura zostaje umieszczona w kolejce komunikatów aplikacji, której okno
jest adresatem przesyłanego komunikatu. Komunikat może być też rozsyłany do wszystkich okien (ang. broadcast). 3. Aplikacja odbiera komunikat i przekazuje go do właściwego okna (zgodnie
z uchwytem w komunikacie). Następnie procedura okna-adresata wywołuje metodę obsługującą ten konkretny komunikat (np. WM_PAINT spowoduje wywołanie metody odświeżającej formę OnPaint). Za obsługę komunikatów przez kontrolki MFC odpowiedzialne są zdefiniowane w ich klasach procedury okna WindowProc, wywoływane przez system lub komponent macierzysty za każdym razem, gdy przysłany zostanie do niego komunikat. Od tego schematu są wyjątki — niektóre komunikaty przekazywane są bezpośrednio do właściwego okna z pominięciem kolejki aplikacji. Garść wstępnych informacji na temat praktycznego wykorzystania mechanizmu komunikatów w aplikacjach korzystających z biblioteki MFC znajdzie Czytelnik w rozdziale 1.
Rozdział 6. ♦ Komunikaty Windows
187
Obsługa komunikatów w procedurze okna (MFC) Reakcja okna lub kontrolki na konkretny typ komunikatu Zacznijmy od najprostszej sytuacji, w której okno reaguje na komunikaty np. związane z myszą. Niech tytuł okna zmieni się, jeżeli użytkownik kliknie lewym przyciskiem myszy, i ponownie, gdy zwolni przycisk. 1. Po utworzeniu projektu o nazwie Komunikaty aplikacji MFC z oknem dialogowym
przejdźmy do widoku projektowania. 2. Zaznaczmy samo okno w podglądzie i przejdźmy do jego własności (podokno
Properties). W przypadku okna oprócz zakładek Properties i Events pojawi się również zakładka Messages1. 3. Odnajdźmy w niej komunikat WM_LBUTTONDOWN i ze związanej z nim rozwijanej
listy wybierzmy OnLButtonDown. 4. Jak już wiemy, spowoduje to dodanie do klasy okna dialogowego metody OnLButtonDown, w której możemy umieścić instrukcje, które mają być wykonane
po wciśnięciu przycisku myszy (przykład widoczny jest na listingu 6.1). Listing 6.1. Zmiana tytułu okna po kliknięciu okna lewym przyciskiem myszy void CKomunikatyDlg::OnLButtonDown(UINT nFlags, CPoint point) { this->SetWindowTextW(L"Lewy przycisk myszy jest wduszony"); CDialog::OnLButtonDown(nFlags, point); }
5. Usunięcie wiązania metody z komunikatem możliwe jest dzięki tej samej
rozwijanej liście (pozycja < Delete> OnLButtonDown). 6. Postępując analogicznie, przygotuj metodę informującą o zwolnieniu lewego
przycisku myszy. Jak zostało wyjaśnione w rozdziale 1., wiązanie metody z komunikatem odbywa się za pośrednictwem makra w pliku .cpp okna dialogowego. Przykład takiego makra mapującego2 widoczny jest na listingu 6.2.
1
Czasem lista komunikatów, która powinna być na niej widoczna, nie pojawia się. Wówczas trzeba kliknąć dowolny komponent na oknie (np. przycisk), a potem ponownie zaznaczyć okno.
2
Puryści językowi wolą określenie „odwzorowującego”.
188
Visual C++. Gotowe rozwiązania dla programistów Windows
Listing 6.2. Wiązanie komunikatów z metodami (por. rozdział 1.) BEGIN_MESSAGE_MAP(CKomunikatyDlg, CDialog) ON_WM_PAINT() ON_WM_QUERYDRAGICON() //}}AFX_MSG_MAP ON_WM_LBUTTONDOWN() ON_WM_LBUTTONUP() END_MESSAGE_MAP()
Lista komunikatów odbieranych przez okno Wszystkie komunikaty, które zostały dostarczone do aplikacji, bez względu na to, czy przechodzą przez kolejkę komunikatów, czy nie, przechodzą przez procedurę okna (ang. window procedure). Chcąc coś z nimi zrobić, musimy nadpisać tę metodę w projektowanej przez nas klasie okna. W takim przypadku musimy koniecznie pamiętać o wywołaniu metody z klasy bazowej, aby miała ona możliwość rozdzielenia komunikatów do poszczególnych komponentów. Pamiętajmy jednak, że większość kontrolek jest przez system traktowana jak osobne okna. W konsekwencji, jeżeli na formie umieścimy kontrolkę List Box (klasa CListBox), to w momencie, gdy mysz znajduje się nad tą kontrolką, komunikaty przesyłane są do niej bezpośrednio, a nie do procedury okna, na której jest umieszczona. Należy również pamiętać, że komunikaty trafiają do aplikacji nawet wówczas, gdy jej okna nie zostały jeszcze w pełni zainicjowane, tj. np. niezainicjowane są jeszcze umieszczone w nich kontrolki, w których chcielibyśmy pokazać użytkownikowi informacje o odebranym komunikacie, a także wtedy, gdy aplikacja kończy swoje działanie i modyfikowanie komponentów też nie jest już możliwe. Dlatego przed umieszczeniem informacji o komunikacie np. w CListBox należy sprawdzić, czy uchwyt okna kontrolki nie jest przypadkiem pusty. 1. W projekcie z poprzedniego podrozdziału, w pliku KomunikatyDlg.h, w klasie CKomunikatyDlg deklarujemy metodę WindowProc nadpisującą odpowiednią
metodę z klasy bazowej. private: virtual LRESULT WindowProc(UINT message, WPARAM wParam,LPARAM lParam);
2. Następnie definiujemy metodę WindowProc zgodnie z listingiem 6.3 w pliku
KomunikatyDlg.cpp, dbając o wywołanie metody bazowej. Ponieważ tymczasową reakcją na odebranie komunikatu jest tylko sygnał dźwiękowy, nie musimy na razie sprawdzać, czy okno jest już zainicjowane. Listing 6.3. Definicja metody pełniącej rolę procedury okna LRESULT CKomunikatyDlg::WindowProc(UINT message, WPARAM wParam,LPARAM lParam) { Beep(100,10); return CDialog::WindowProc(message,wParam,lParam); }
Rozdział 6. ♦ Komunikaty Windows
189
3. Pójdźmy dalej — wyświetlmy komunikaty w liście typu List Box. Umieśćmy ją zatem na oknie, wyłączmy sortowanie (własność Sort ustawmy na false), przełączmy w tryb wielu kolumn (własność Multicolumn ustawmy na true), wyświetlmy poziomy pasek przewijania (własność Horizontal Scroll).
Usuńmy także z okna domyślnie umieszczone tam dwa przyciski i etykietę. 4. Stwórzmy zmienną związaną z listą, o nazwie listBox1. 5. Zmieńmy szerokość kolumny w liście, dodając do metody Ckomunikaty ´Dlg::OnInitDialog instrukcję: listBox1.SetColumnWidth(30);
6. I wreszcie do metody WindowProc dodajmy instrukcję wyświetlającą numery
komunikatów (listing 6.4). Listing 6.4. Bez warunku sprawdzającego, czy uchwyt listy jest różny od NULL, od razu po uruchomieniu aplikacji pojawi się wyjątek związany z próbą wywołania metod nieistniejącego jeszcze komponentu LRESULT CKomunikatyDlg::WindowProc(UINT message, WPARAM wParam,LPARAM lParam) { //Beep(100,10); if(listBox1.m_hWnd!=NULL && message!=WM_CTLCOLOR && message!=WM_CTLCOLORLISTBOX) { wchar_t opis[256]; _itow_s(message,opis,256,10); listBox1.AddString(opis); } return CDialog::WindowProc(message,wParam,lParam); }
7. Komunikatów będzie wiele. Abyśmy je dobrze widzieli, powiększmy listę do
rozmiaru całego okna i umożliwmy kontrolę rozmiaru okna. W tym celu: a) Zmieńmy styl okna na umożliwiający zmianę rozmiaru; w tym celu własność Border okna przestawmy na Resizing. b) Korzystając z komunikatu WM_SIZE, zwiążemy obszar zajmowany przez listę
z obszarem klienta okna: zaznaczamy okno i tworzymy metodę związaną z komunikatem WM_SIZE, w której umieszczamy kod z listingu 6.5. Listing 6.5. Z prawej strony listy zostawiamy nieco miejsca, które przyda się do przeprowadzania opisanych niżej testów void CKomunikatyDlg::OnSize(UINT nType, int cx, int cy) { CDialog::OnSize(nType, cx, cy); if(listBox1.m_hWnd!=NULL) { RECT obszarKlienta; this->GetClientRect(&obszarKlienta); listBox1.SetWindowPos(&wndTop, obszarKlienta.left,
190
Visual C++. Gotowe rozwiązania dla programistów Windows obszarKlienta.top, obszarKlienta.right-obszarKlienta.left-100, obszarKlienta.bottom-obszarKlienta.top, 0); } }
Pierwszym argumentem zdefiniowanej w pierwszym punkcie metody WindowProc jest numer komunikatu. Dwa kolejne argumenty to dodatkowe informacje przekazywane wraz z komunikatem. Ich znaczenie zależy od tego, z jakim komunikatem mamy do czynienia. Metoda WindowProc uruchamiana będzie zawsze, gdy okno odbierze komunikat. Przez „okno” należy rozumieć okno dialogowe MFC. Jeżeli większa część okna zajęta jest przez listę CListBox, to do okna dialogowego nie są wysyłane komunikaty związane np. z myszą i klawiaturą. Trafiają bezpośrednio do procedury okna, które związane jest z listą. Jeżeli po skompilowaniu aplikacji poruszymy myszą tak, aby kursor znalazł się nad oknem, a nie nad listą, lista widoczna na formie szybko zapełni się powiadomieniami o komunikatach o numerze 32 (0x20), tj. WM_SETCURSOR, i 512 (0x200), tj. WM_MOUSEMOVE (rysunek 6.1). Gdy poruszający się kursor znajduje się nad listą, tylko drugi komunikat przesyłany jest do bazowego okna; pierwszy wysyłany jest tylko do okna związanego z listą. Natomiast gdy kursor myszy porusza się w obszarze paska tytułu lub brzegu okna, wówczas przesyłany jest komunikat WM_NCMOUSEMOVE (litery NC od ang. nonclient area) o numerze 160 (0xA0). W metodzie z punktu 6. pomijane są dwa komunikaty, wysyłane w przypadku zmiany zawartości kontrolki CListBox. Bez tego aplikacja zapętliłaby się, wysyłając w reakcji na dodanie łańcucha do listy komunikat, którego odebranie spowodowałoby dodanie kolejnego łańcucha i kolejne wysłanie komunikatu itd. Rysunek 6.1. Lista komunikatów odbieranych przez aplikację
Rozdział 6. ♦ Komunikaty Windows
191
Filtrowanie zdarzeń Zazwyczaj metoda WindowProc zawiera instrukcję wielokrotnego wyboru switch, która pozwala na wygodne filtrowanie komunikatów i wyłanianie tych spośród nich, na które chcemy zareagować. Przykład takiej instrukcji, która spośród wszystkich komunikatów wybiera te najbardziej popularne i umieszcza ich nazwę w liście listBox1, znajduje się na listingu 6.6. Jest to zmodyfikowana metoda z poprzedniego projektu. Listing 6.6. Instrukcja switch filtrująca zdarzenia LRESULT CKomunikatyDlg::WindowProc(UINT message, WPARAM wParam,LPARAM lParam) { if(listBox1.m_hWnd!=NULL) { switch(message) { //Odmalowywanie okna case WM_PAINT: listBox1.AddString(L"WM_PAINT"); break; //Mysz case WM_MOUSEMOVE: listBox1.AddString(L"WM_MOUSEMOVE"); break; case WM_NCMOUSEMOVE: listBox1.AddString(L"WM_NCMOUSEMOVE"); break; //case WM_SETCURSOR: listBox1.AddString(L"WM_SETCURSOR"); break; case WM_LBUTTONDOWN: listBox1.AddString(L"WM_LBUTTONDOWN"); break; case WM_LBUTTONUP: listBox1.AddString(L"WM_LBUTTONUP"); break; case WM_LBUTTONDBLCLK: listBox1.AddString(L"WM_LBUTTONDBLCLK"); break; case WM_RBUTTONDOWN: listBox1.AddString(L"WM_RBUTTONDOWN"); break; case WM_RBUTTONUP: listBox1.AddString(L"WM_RBUTTONUP"); break; case WM_RBUTTONDBLCLK: listBox1.AddString(L"WM_RBUTTONDBLCLK"); break; case WM_MBUTTONDOWN: listBox1.AddString(L"WM_MBUTTONDOWN"); break; case WM_MBUTTONUP: listBox1.AddString(L"WM_MBUTTONUP"); break; case WM_MBUTTONDBLCLK: listBox1.AddString(L"WM_MBUTTONDBLCLK"); break;
} }
}
//Klawiatura case WM_KEYDOWN: listBox1.AddString(L"WM_KEYDOWN"); break; case WM_KEYUP: listBox1.AddString(L"WM_KEYUP"); break; case WM_CHAR: listBox1.AddString(L"WM_CHAR"); break;
return CDialog::WindowProc(message,wParam,lParam);
Jeszcze raz warto poruszać kursorem myszy nad oknem poza listą i nad listą, aby zobaczyć, jakie komunikaty są przesyłane do procedury okna.
Przykład odczytywania informacji dostarczanych przez komunikat Spróbujmy odczytać informacje niesione przez komunikat na przykładzie komunikatów dotyczących ruchu myszy. Posługując się nadal procedurą okna, tj. metodą CKomunikaty ´Dlg::WindowProc, będziemy śledzić położenie myszy, reagując na komunikaty WM_MOUSEMOVE i WM_NCMOUSEMOVE.
192
Visual C++. Gotowe rozwiązania dla programistów Windows 1. Usuńmy z okna kontrolkę typu CListBox. 2. Na formie umieszczamy trzy pola edycyjne zgodnie z rysunkiem 6.2.
Rysunek 6.2. Projekt formy, na której zaprezentujemy przesyłane w komunikatach informacje o ruchu myszy
3. Z wszystkimi polami edycyjnymi wiążemy zmienne (edit1, edit2 i edit3). 4. Modyfikujemy metodę WindowProc zgodnie ze wzorem z listingu 6.7. Listing 6.7. Wyświetlanie informacji o położeniu myszy, odebranych z komunikatów WM_MOUSEMOVE i WM_NCMOUSEMOVE LRESULT CKomunikatyDlg::WindowProc(UINT message, WPARAM wParam,LPARAM lParam) { switch (message) { case WM_MOUSEMOVE: case WM_NCMOUSEMOVE: if (message==WM_MOUSEMOVE) edit1.SetWindowTextW(L"W obrębie obszaru ´użytkownika formy (wsp. okna)"); else edit1.SetWindowTextW(L"Poza obszarem użytkownika formy (wsp. ekranu)"); wchar_t txtLParamLo[256]; _itow_s(LOWORD(lParam),txtLParamLo,256,10); wchar_t txtLParamHi[256]; _itow_s(HIWORD(lParam),txtLParamHi,256,10); edit2.SetWindowTextW(txtLParamLo); edit3.SetWindowTextW(txtLParamHi); break; } }
return CDialog::WindowProc(message,wParam,lParam);
Jeżeli mysz znajduje się wewnątrz obszaru dostępnego dla użytkownika (ang. client area), przekazywane przez komunikat współrzędne położenia myszy to współrzędne względem formy, a dokładniej względem lewego górnego rogu obszaru użytkownika. Poza nim (a więc na brzegu okna, na pasku tytułu) są to współrzędne ekranu. Współrzędne
Rozdział 6. ♦ Komunikaty Windows
193
ekranu i obszaru użytkownika formy można łatwo przetransformować, korzystając z funkcji WinAPI ScreenToClient i ClientToScreen lub metod kontrolek o identycznych nazwach.
Lista wszystkich komunikatów odbieranych przez okno i jego kontrolki Czasem chcemy zareagować np. na kliknięcie myszą lub naciśnięcie klawisza bez względu na to, czy akurat aktywne jest okno, czy jedna z umieszczonych w nim kontrolek. Jest to możliwe, jeżeli w bieżącej klasie nadpiszemy metodę PreTranslate ´Message zdefiniowaną w klasie okna CWnd. Jej zadaniem jest wstępne przekształcanie komunikatów (jeszcze przed posłaniem ich do funkcji TranslateMessage). Informacje o komunikacie znowu ograniczymy do wyświetlenia numerów komunikatu (listing 6.8). Listing 6.8. Wyświetlanie pełnej listy komunikatów BOOL CKomunikatyDlg::PreTranslateMessage(MSG* pMsg) { if(listBox1.m_hWnd!=NULL && pMsg->message!=WM_CTLCOLOR && pMsg´>message!=WM_CTLCOLORLISTBOX) { wchar_t opis[256]; _itow_s(pMsg->message,opis,256,10); listBox1.AddString(opis); } return CDialog::PreTranslateMessage(pMsg); }
Jeżeli chcemy, żeby naciskanie dowolnych klawiszy było sygnalizowane dźwiękiem (jak w ZX Spectrum), możemy zmodyfikować powyższą metodę w sposób pokazany na listingu 6.9. Listing 6.9. Dźwięk towarzyszy każdemu naciśnięciu klawisza przy aktywnym oknie BOOL CKomunikatyDlg::PreTranslateMessage(MSG* pMsg) { if(pMsg->message==WM_KEYDOWN) Beep(100,10); return CDialog::PreTranslateMessage(pMsg); }
Wykrycie zmiany trybu pracy karty graficznej Jest pewna grupa komunikatów, które rozsyłane są do wszystkich aplikacji. Związane są one najczęściej ze zmianą parametrów systemu, np. z wylogowaniem użytkownika lub zmianą rozdzielczości karty graficznej. W przypadku tego ostatniego zdarzenia do
Więcej na: www.ebook4all.pl
194
Visual C++. Gotowe rozwiązania dla programistów Windows
wszystkich okien rozsyłany jest komunikat WM_DISPLAYCHANGE, który — podobnie jak np. WM_MOVE — nie trafia do kolejki komunikatów. 1. Powróćmy do programu rozwijanego w projektach opisanych na końcu
rozdziału 2. 2. Uzupełniamy okno dialogowe o dwa kolejne pola edycyjne (rysunek 6.3) i związujemy z nimi zmienne edit3 i edit4. Rysunek 6.3. Uzupełniony interfejs aplikacji służącej do kontroli pracy karty graficznej
3. Do definicji klasy CTrybyKartyGrafDlg w pliku nagłówkowym TrybyKartyGrafDlg.h dodajemy deklarację metody WindowProc oraz metodę pomocniczą BiezacyCzasIData
(listing 6.10). Listing 6.10. Deklaracja dwóch nowych metod w klasie okna class CTrybyKartyGrafDlg : public CDialog { private: CList * listaTrybowKartyGraf; CString PrzygotujOpisTrybuKartyGraf(int i,DEVMODE* pTrybKartyGraf); bool CzyWinNT(); void TworzListeTrybowKartyGraf(); void BiezacyTrybKartyGraf(); CString BiezacyCzasIData(); virtual LRESULT WindowProc(UINT message,WPARAM wParam,LPARAM lParam); ...
Rozdział 6. ♦ Komunikaty Windows
195
4. W pliku TrybyKartyGrafDlg.cpp definiujemy obie metody (listing 6.11). Listing 6.11. Obsługa komunikatu WM_DISPLAYCHANGE CString CTrybyKartyGrafDlg::BiezacyCzasIData() { SYSTEMTIME data; GetLocalTime(&data); CString s; wchar_t tmp[5]; _itow_s(data.wDay,tmp,5,10); if(data.wDayHandle,VERTRES)
ilość bitów na piksel (ilość kolorów)
GetDeviceCaps(Form1->Canvas->Handle,PLANES)*
częstość odświeżania
GetDeviceCaps(Form1->Canvas->Handle,VREFRESH)
GetDeviceCaps(Form1->Canvas->Handle,BITSPIXEL)
Wysłanie komunikatu uruchamiającego wygaszacz ekranu i detekcja włączenia wygaszacza Wysyłając odpowiedni komunikat, można spowodować uruchomienie przez system wygaszacza ekranu. Wystarczy wykonać polecenie, korzystając z funkcji WinAPI: ::SendMessage(this->m_hWnd,WM_SYSCOMMAND,SC_SCREENSAVE,0);
lub metody okna dialogowego: this->SendMessage(WM_SYSCOMMAND,SC_SCREENSAVE,0);
Aby wykryć włączenie wygaszacza przez system, wystarczy nasłuchiwać komunikatu WM_SYSCOMMAND, np. tworząc metodę komunikatu z okna własności. Należy w niej sprawdzić, czy wParam jest równy SC_SCREENSAVE (listing 6.12). Listing 6.12. Reakcja na komunikat WM_SYSCOMMAND void CKomunikaty2Dlg::OnSysCommand(UINT nID, LPARAM lParam) { if(nID==SC_SCREENSAVE) Beep(100,100); CDialog::OnSysCommand(nID, lParam); }
Wykorzystanie komunikatów do kontroli innej aplikacji na przykładzie Winampa Proszę zwrócić uwagę, że efektem wysłania komunikatu jest wywołanie metody obsługującej komunikat. Mechanizm komunikatów można zatem rozumieć jako niebezpośrednie wywoływanie metod, przy czym mogą to być metody obiektów utworzonych przez zupełnie inne aplikacje. Oznacza to, że mechanizm komunikatów Windows
198
Visual C++. Gotowe rozwiązania dla programistów Windows
umożliwia przygotowanie ciekawego sposobu kontroli aplikacji, czyniąc ją swojego rodzaju serwerem odbierającym komunikaty i reagującym na nie. Inna aplikacja klient może, wysyłając komunikaty, kontrolować ten serwer poprzez wywoływanie metod jego obiektów. Niewiele jest aplikacji realizujących ten schemat, bo też ten „kliencko-serwerowy” tandem ograniczony jest tylko do jednego komputera, w związku z czym nie ma dla niego zbyt wielu zastosowań. Jedną z aplikacji reagujących na komunikaty jest popularny Winamp. Umożliwia on daleko idącą kontrolę swojego działania za pomocą komunikatów4. Każde polecenie ze zbioru widocznego w listingu 6.13 wysyła komunikat do Winampa, którego okno identyfikujemy za pomocą nazwy klasy Winamp v1.x. Wbrew numerowi w nazwie jest to wspólna nazwa klasy wszystkich wersji tej aplikacji. Wysyłany jest komunikat WM_COMMAND. Istotna jest przekazywana przez niego wartość wParam, która może być jedną z liczb określających komendę dla Winampa. Przykładowe wartości to: 40045 (aby rozpocząć odtwarzanie), 40046 (dla pauzy), 40047 (aby zatrzymać odtwarzanie) i 40048 (aby przejść do następnego pliku). Listing 6.13. Polecenia pozwalające na „zdalną” kontrolę Winampa ::SendMessage(::FindWindow(L"Winamp v1.x",NULL),WM_COMMAND,40045,0); //Odtwarzaj ::SendMessage(::FindWindow(L"Winamp v1.x",NULL),WM_COMMAND,40046,0); //Pauza ::SendMessage(::FindWindow(L"Winamp v1.x",NULL),WM_COMMAND,40047,0); //Stop ::SendMessage(::FindWindow(L"Winamp v1.x",NULL),WM_COMMAND,40048,0); //Następny utwór
Przykłady reakcji na komunikaty (MFC) Blokowanie zamknięcia sesji Windows Do tej pory przechwytywaliśmy komunikaty i ewentualnie reagowaliśmy na ich pojawienie się zmianami stanu naszej aplikacji. Obsługa komunikatów może jednak wiązać się także z odesłaniem informacji do nadawcy komunikatu. Doskonałym przykładem jest komunikat WM_QUERYENDSESSION, który jest wysyłany do aplikacji przed zamknięciem systemu (dokładniej przed zamknięciem sesji użytkownika, tj. przed jego wylogowaniem), o ile nie zostało zastosowane wymuszenie zamknięcia (na ten temat zob. „Zamykanie i wstrzymywanie systemu Windows” w rozdziale 2.). Jeżeli chcemy zablokować ten proces, należy w metodzie obsługującej komunikat zwrócić wartość FALSE (0). 1. Na formie umieśćmy kontrolkę pola wyboru (ang. check box) i stwórzmy do niej zmienną o nazwie checkBox1. 2. Za pomocą okna własności stwórzmy metodę obsługującą komunikat WM_QUERYENDSESSION (listing 6.14).
4
Projekt może współpracować zarówno ze starszymi wersjami Winampa, jak i z najnowszą wersją 5. Jednak w odniesieniu do najnowszej wersji Nullsoft zaleca się stosowanie specjalnie przygotowanego interfejsu programistycznego (API), dostępnego w pakiecie SDK.
Rozdział 6. ♦ Komunikaty Windows
199
Listing 6.14. Czy mogę zamknąć system? BOOL CKomunikaty2Dlg::OnQueryEndSession() { if (!CDialog::OnQueryEndSession()) return FALSE; Beep(100,100); return (checkBox1.GetState() & 0x0003)==0; }
Należy się liczyć z faktem, że w systemach z rodziny NT/2000/XP/2003 proces zamykania systemu zostanie zablokowany dopiero w momencie próby zamknięcia bieżącej aplikacji. Wcześniej mogło zostać zamkniętych kilka innych aplikacji. W systemach z rodziny 95/98/Me aplikacje są zamykane dopiero po odebraniu zgody na zamknięcie systemu ze wszystkich działających procesów. W Windows Vista powrócono do tego pomysłu w nieco zmodyfikowanej formie. System wysyła komunikaty do wszystkich aplikacji i wyświetla komunikat informujący o blokowaniu zamknięcia sesji (rysunek 6.4). W zależności od decyzji użytkownika program blokujący zostanie zamknięty lub nie. Rysunek 6.4. Informacja o programie blokującym zamknięcie sesji w Windows Vista
Wykrycie włożenia do napędu lub wysunięcia z niego płyty CD lub DVD; wykrycie podłączenia do gniazda USB lub odłączenia pamięci Flash Kolejnym komunikatem, który nie przechodzi przez kolejkę komunikatów, a wydaje się ciekawy, jest WM_DEVICECHANGE. Jest on rozsyłany między innymi wówczas, gdy w napędzie CD lub DVD zmieniona zostaje płyta lub gdy do gniazda USB podłączone zostanie urządzenie z pamięcią Flash (np. aparat cyfrowy lub pamięć USB).
200
Visual C++. Gotowe rozwiązania dla programistów Windows
Niestety nie znajdziemy tego komunikatu w zakładce Messages we własnościach okna. Musimy albo sami skonfigurować metodę komunikatu, albo skorzystać z procedury okna. Proponuję tym razem użyć tego pierwszego rozwiązania. 1. W pliku nagłówkowym klasy okna dialogowego definiujemy prywatną metodę OnDeviceChange. virtual BOOL OnDeviceChange(UINT nEventType,DWORD_PTR dwData);
2. Importujemy nagłówek dbt.h, w którym zdefiniowane są stałe i struktury
używane w tym komunikacie. #include
3. Definiujemy ją w pliku .cpp zgodnie z listingiem 6.15. Listing 6.15. Metoda z komunikatami odpowiadającymi włożeniu lub wysunięciu płyt CD i DVD BOOL CKomunikaty2Dlg::OnDeviceChange(UINT nEventType,DWORD_PTR dwData) { CString s; switch(nEventType) { case DBT_DEVICEARRIVAL: s.Append(L"Włożony nośnik"); break; case DBT_DEVICEQUERYREMOVE: s.Append(L"Pytanie o możliwość usunięcia ´nośnika"); break; case DBT_DEVICEQUERYREMOVEFAILED: s.Append(L"Prośba o możliwość usunięcia ´nośnika została anulowana"); break; case DBT_DEVICEREMOVEPENDING: s.Append(L"Nośnik zostanie usunięty"); break; case DBT_DEVICEREMOVECOMPLETE: s.Append(L"Usuwanie nośnika zostało ´zakończone"); break; case DBT_DEVICETYPESPECIFIC: s.Append(L"Zdarzenie specyficzne dla danego ´urządzenia"); break; case DBT_CONFIGCHANGED: s.Append(L"Bieżąca konfiguracja urządzenia została ´zmieniona"); break; case DBT_DEVNODES_CHANGED: s.Append(L"Węzeł urządzenia został zmieniony"); break; } listBox1.AddString(s); return TRUE; }
W praktyce przy wkładaniu i wyjmowaniu płyt CD, DVD lub pamięci USB można spodziewać się komunikatu z wartościami parametru wParam (w powyższej metodzie przekazywany jest jako argument nEventType) równymi DBT_DEVICEARRIVAL, DBT_DEVICEREMOVE ´COMPLETE i DBT_DEVNODES_CHANGED. Tego ostatniego w przypadku pamięci USB nawet wiele razy. Bardziej szczegółowe informacje można odczytać z parametru lParam dostępnego w argumencie dwData metody OnDeviceChange. W przypadku włożenia lub usunięcia nośnika zawiera on strukturę DEV_BROADCAST_HDR, z której można odczytać m.in., jaki typ nośnika został włożony lub usunięty. Jeszcze więcej informacji można uzyskać, jeżeli aplikacja zarejestruje się w systemie, korzystając z funkcji Register ´DeviceNotification. Można w ten sposób śledzić, co się dzieje z pamięciami USB, płytami CD, DVD itd., kartami sieciowymi, urządzeniami podłączanymi do USB (mysz, klawiatura, kierownica, dżojstick itp., czyli urządzeniami typu human interface device) i innymi (zob. listę w kluczu rejestru \\HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\DeviceClasses).
Rozdział 6. ♦ Komunikaty Windows
201
Przeciąganie plików między aplikacjami Obsługa mechanizmu drag & drop w przypadku zaangażowania dwóch aplikacji (przenoszenie elementów pomiędzy oknami należącymi do różnych aplikacji) wymaga zareagowania na przesyłane między tymi aplikacjami komunikaty. Najczęściej przenoszenie dotyczy plików i taki przykład przedstawiony jest w tym projekcie. Upuszczenie pliku (zabranego np. z Eksploratora Windows) na formę powoduje wysłanie do aplikacji komunikatu WM_DROPFILES. Dla uproszczenia przyjmijmy, że aplikacja będzie akceptować wszystkie pliki i katalogi bez względu na ich pochodzenie i rodzaj. Możemy wobec tego już w momencie tworzenia okna wywołać funkcję WinAPI DragAcceptFiles5. Jej pierwszym argumentem jest uchwyt okna, który będzie akceptować upuszczanie plików, a drugim wartość informująca o włączeniu (TRUE) lub wyłączeniu (FALSE) akceptacji. W klasie CWnd, a więc także w jej klasie potomnej CDialog zdefiniowana jest metoda DragAcceptFiles z argumentem domyślnym równym 1, czyli TRUE. 1. Tworzymy nowy projekt aplikacji MFC Application. 2. W metodzie OnInitDialog umieszczamy wywołanie metody DragAcceptFiles
(listing 6.16) lub przełączamy własność okna dialogowego Accept Files na true. Listing 6.16. Bieżące okno będzie mogło przyjmować upuszczane na nie pliki BOOL CPrzenoszeniePlikowDlg::OnInitDialog() { CDialog::OnInitDialog(); SetIcon(m_hIcon, TRUE); SetIcon(m_hIcon, FALSE); DragAcceptFiles(TRUE); return TRUE; }
3. Wracamy do podglądu okna. Umieszczamy w nim kontrolkę List Box, z którą wiążemy zmienną o nazwie listBox1. 4. Zaznaczamy okno dialogowe i przechodzimy do jego własności (F4). 5. Na zakładce Messages odnajdujemy komunikat WM_DROPFILES. Tworzymy
związaną z nim metodę (ang. message handler). 6. W tej metodzie musimy umieścić zasadniczą część kodu, która określi reakcję aplikacji na nadejście komunikatu WM_DROPFILES informującego o upuszczeniu
pliku. Tu ograniczymy się do wyświetlenia pełnych ścieżek plików i katalogów w kontrolce List Box (listing 6.17), jednak programista, mając do dyspozycji pełną ścieżkę pliku lub katalogu, może z nimi zrobić, co tylko zechce.
5
Obecna we wszystkich 32-bitowych wersjach Windows.
202
Visual C++. Gotowe rozwiązania dla programistów Windows
Listing 6.17. Metoda uwzględnia możliwość przeciągania wielu plików i katalogów void CPrzenoszeniePlikowDlg::OnDropFiles(HDROP hDropInfo) { Beep(100,100); wchar_t nazwaPliku[MAX_PATH]; int liczbaPlikow=DragQueryFile(hDropInfo,0xFFFFFFFF,NULL,0); //liczba ´zrzuconych plików CString s; listBox1.ResetContent(); //Czyszczenie listy przy każdym upuszczeniu plików s.Append(L"Czas operacji: "); s.Append(BiezacyCzasIData()); listBox1.AddString(s); s.Empty(); s.Append(L"Ilość plików zrzuconych na formę: "); wchar_t txtLiczbaPlikow[5]; _itow_s(liczbaPlikow,txtLiczbaPlikow,5,10); s.Append(txtLiczbaPlikow); listBox1.AddString(s); for(int i=0;imessage==WM_KEYDOWN && pMsg->wParam==VK_RETURN) SendMessage(WM_NEXTDLGCTL,(int)GetAsyncKeyState(VK_SHIFT),FALSE); return CDialog::PreTranslateMessage(pMsg); }
W obu metodach uwzględniona została możliwość naciśnięcia klawisza Shift. Wówczas aktywna staje się poprzednia kontrolka w kolejce.
206
Visual C++. Gotowe rozwiązania dla programistów Windows
XKill dla Windows W rozdziale 3. wspomniałem o funkcjach pozwalających na zamknięcie procesu dowolnej aplikacji. Poniżej przedstawię inny sposób na uzyskanie podobnego efektu — wysłanie do okna aplikacji komunikatu WM_CLOSE, tj. prośby o zamknięcie. Ściślej rzecz biorąc, odebranie tego komunikatu spowoduje zamknięcie okna-adresata komunikatu. Ale jeżeli jest to okno główne, zamknięcie go powoduje również zamknięcie aplikacji. Ten sam komunikat otrzymuje okno, gdy klikamy jego ikonę z krzyżykiem na pasku tytułu. Przygotujemy aplikację, która pozwoli na zamknięcie okna wskazanego przez kliknięcie myszy — odpowiednik znanego z Linuksa programu xkill. Ponieważ kliknięcie okna innej aplikacji nie spowoduje zdarzenia ani nawet wysłania komunikatu do naszej aplikacji, musimy skorzystać z prostej sztuczki. Będziemy śledzić nie zdarzenie kliknięcia myszą, ale utraty „focusu” przez naszą aplikację, co jest oczywiście konsekwencją kliknięcia innego okna. Jeżeli to nastąpi, sprawdzimy pozycję myszy, wykryjemy okno, jakie się w tym miejscu znajduje, i postaramy się o jego uchwyt. Mając uchwyt okna, nie będziemy już mieć problemu z wysłaniem do niego odpowiedniego komunikatu. 1. Tworzymy nowy projekt aplikacji opartej na oknie dialogowym. 2. Tworzymy metodę związaną z komunikatem WM_KILLFOCUS. 3. W metodzie tej umieszczamy polecenia zgodnie z listingiem 6.23. Listing 6.23. Metody użyte do zdobycia uchwytu okna omówione zostały w rozdziale 3. void CWinKillDlg::OnKillFocus(CWnd* pNewWnd) { CDialog::OnKillFocus(pNewWnd); POINT punkt; GetCursorPos(&punkt); HWND uchwytOkna=::WindowFromPoint(punkt); unsigned long identProcesu; wchar_t tytul[256]; ::GetWindowText(uchwytOkna,tytul,256); GetWindowThreadProcessId(uchwytOkna,&identProcesu); CString s=L"Czy zamknąć okno "; s.Append(tytul); s.Append(L"?\nUchwyt okna: "); wchar_t txtUchwyt[256]; _itow_s((int)uchwytOkna,txtUchwyt,256,10); s.Append(txtUchwyt); s.Append(L"\nIdentyfikator procesu: "); wchar_t txtIdentProcesu[256]; _itow_s(identProcesu,txtIdentProcesu,256,10); s.Append(txtIdentProcesu); if (MessageBox(s,L"WinKill",MB_YESNO+MB_ICONQUESTION)==IDYES) { ::SendMessage(uchwytOkna,WM_CLOSE,0,0); //EndDialog(IDOK); } }
Rozdział 6. ♦ Komunikaty Windows
207
4. Aby okno modalne z pytaniem wyświetlane przez metodę MessageBox było widoczne po utracie „focusu”, należy w metodzie OnInitDialog wywołać metodę: ::SetWindowPos(this->m_hWnd,HWND_TOPMOST,0,0,0,0,SWP_NOSIZE);
Działanie aplikacji jest proste. Uruchamiamy ją i klikamy okno, które ma być zamknięte. Zobaczymy pytanie z prośbą o potwierdzenie i jeżeli klikniemy Tak, do wskazanego okna zostanie wysłany komunikat WM_CLOSE. Efekt powinien być taki sam, jak gdyby kliknięta została ikona z krzyżykiem na pasku tytułu wskazanej aplikacji, a więc aplikacja będzie miała okazję na anulowanie zamknięcia, np. jeżeli znajdują się w niej niezapisane dane. Należy jednak być świadomym tego, że aplikacje często tworzą dodatkowe okna, których obecności użytkownik nie musi być świadomy. Weźmy dla przykładu grę pasjans. Istotne jest, czy okno klikniemy na pasku tytułu, czy w jego wnętrzu. Wnętrze jest bowiem osobnym oknem, którego zamknięcie usunie wprawdzie planszę gry, ale nie zamknie właściwego okna aplikacji. Ponadto w tej wersji nasza aplikacja nie pomoże zbytnio, jeżeli z oknem dzieje się coś złego (wówczas zazwyczaj przestaje reagować na komunikaty, co można poznać po braku odświeżania zawartości okna). Oba problemy rozwiązuje użycie funkcji TerminateProcess, którą omówiono w rozdziale 3. Powyższa metoda miałaby wówczas postać widoczną na listingu 6.24. W tym przypadku zamykany będzie proces, nie tylko okno, i nie będzie już możliwości zapisywania niezapisanych danych. Takiego działania spodziewać się jednak należy po odpowiedniku xkill. Listing 6.24. Ta metoda powinna się w zasadzie znaleźć w rozdziale 6. void CWinKillDlg::OnKillFocus(CWnd* pNewWnd) { CDialog::OnKillFocus(pNewWnd); POINT punkt; GetCursorPos(&punkt); HWND uchwytOkna=::WindowFromPoint(punkt); unsigned long identProcesu; wchar_t tytul[256]; ::GetWindowText(uchwytOkna,tytul,256); GetWindowThreadProcessId(uchwytOkna,&identProcesu); CString s=L"Czy zamknąć okno "; s.Append(tytul); s.Append(L"?\nUchwyt okna: "); wchar_t txtUchwyt[256]; _itow_s((int)uchwytOkna,txtUchwyt,256,10); s.Append(txtUchwyt); s.Append(L"\nIdentyfikator procesu: "); wchar_t txtIdentProcesu[256]; _itow_s(identProcesu,txtIdentProcesu,256,10); s.Append(txtIdentProcesu); if (MessageBox(s,L"WinKill",MB_YESNO+MB_ICONQUESTION)==IDYES) { //::SendMessage(uchwytOkna,WM_CLOSE,0,0); HANDLE uchwytProcesu=OpenProcess(PROCESS_TERMINATE,FALSE,identProcesu); if (uchwytProcesu!=0) {
208
Visual C++. Gotowe rozwiązania dla programistów Windows if (!TerminateProcess(uchwytProcesu,0)) { CString s=L"Nie można zabić procesu ("; s.Append(tytul); s.Append(L")"); MessageBox(s); } CloseHandle(uchwytProcesu); } EndDialog(IDOK); } }
Modyfikowanie menu systemowego formy Aby dodać nowe pozycje do menu rozwijanego po kliknięciu ikony formy (rysunek 6.6), nie potrzeba wprawdzie wykorzystywać mechanizmu komunikatów, ale pojawią się one, gdy będziemy chcieli, aby aplikacja reagowała na wybranie nowej pozycji w menu. Rysunek 6.6. Do menu okna dodamy separator i pozycję O...
1. Tworzymy nowy projekt typu MFC Application z oknem dialogowym,
pamiętając, aby w trzecim kroku kreatora zaznaczyć opcję System menu odpowiedzialną za dodanie do okna menu systemowego (rysunek 6.7). 2. Definiujemy stałą globalną const int SE_INFO=100; identyfikującą komunikaty
związane z kliknięciem dodanej do menu pozycji (każda pozycja powinna być identyfikowana przez osobną stałą). 3. Do metody OnInitDialog dodajemy polecenia dodające do menu linię
separatora i nową pozycję z etykietą O… (listing 6.25).
Rozdział 6. ♦ Komunikaty Windows
209
Rysunek 6.7. Konfigurowanie okna dialogowego
Listing 6.25. Pełny kod pliku Unit1.cpp const int SE_INFO=100; BOOL CMenuSystemoweDlg::OnInitDialog() { CDialog::OnInitDialog(); // Set the icon for this dialog. The framework does this automatically // when the application's main window is not a dialog SetIcon(m_hIcon, TRUE); // Set big icon SetIcon(m_hIcon, FALSE); // Set small icon // TODO: Add extra initialization here AppendMenu(::GetSystemMenu(this->m_hWnd,false),MF_SEPARATOR,0,NULL); AppendMenu(::GetSystemMenu(this->m_hWnd,false),MF_STRING,SE_INFO,L"O ..."); return TRUE;
// return TRUE
unless you set the focus to a control
}
4. Możemy skompilować projekt, aby przekonać się, że w menu formy pojawiła
się nowa pozycja. Aby móc reagować na jej wybranie, musimy odbierać komunikat WM_SYSCOMMAND wysłany do formy. Za pomocą zakładki Messages w podoknie Properties tworzymy metodę OnSysCommand związaną z komunikatem WM_SYSCOMMAND. W niej wyławiamy te spośród komunikatów, których parametr wParam (przekazywany do metody jako argument nID) równy jest SE_INFO (listing 6.26).
210
Visual C++. Gotowe rozwiązania dla programistów Windows
Listing 6.26. W przypadku nadpisywania metod związanych z komunikatami należy zawsze pamiętać o wywołaniu metody nadpisywanej void CMenuSystemoweDlg::OnSysCommand(UINT nID, LPARAM lParam) { if(nID==SE_INFO) MessageBox(L"Helion"); CDialog::OnSysCommand(nID, lParam); }
Nowe pozycje w menu okna widoczne są również w menu rozwijanym kliknięciem prawym klawiszem myszy na reprezentacji aplikacji w pasku zadań (rysunek 6.8). Rysunek 6.8. Zmodyfikowane menu okna rozwijane po kliknięciu paska zadań
Haki Jak działają programy przechwytujące i zapisujące wszystkie naciśnięte przez użytkowników komputera klawisze (tzw. keyloggery)? W systemie Windows ich zadaniem jest zazwyczaj monitorowanie komunikatów związanych z klawiaturą. Co to znaczy? Jak wiemy, komunikaty tworzą układ nerwowy systemu Windows, łącząc rdzeń systemu z uruchomionymi w nim aplikacjami. To właśnie komunikaty przekazują aplikacji informacje o tym, że została ona kliknięta myszą lub naciśnięty został jakiś klawisz i w związku z tym system oczekuje od aplikacji jakiejś reakcji. To, co jest dla nas naturalne, np. kliknięcie przycisku widocznego na oknie aplikacji, i co użytkownikowi komputera wydaje się odbywać jedynie w kontekście samej aplikacji, w istocie angażuje system Windows — w końcu mysz i klawiatura nie są podłączone do aplikacji, ale do komputera zarządzanego przez Windows i to właśnie system Windows, a nie sama aplikacja może odczytać stan tych urządzeń. Aplikacja odcięta od strumienia komunikatów staje się głucha i niema9. W szczególności aplikacja przestaje reagować na polecenia odświeżenia okna, co objawia się charakterystyczną „białą plamą” w miejscu interfejsu aplikacji. A w jaki sposób możliwe jest monitorowanie przepływu komunikatów spoza aplikacji, do której komunikaty są skierowane? System Windows udostępnia mechanizm haków (ang. hooks). Pozwala on skojarzyć określony w haku typ komunikatów ze zdefiniowaną 9
Aby taki stan uzyskać, wystarczy nadpisać metodę WndProc okna aplikacji, nie wywołując z niej metody nadpisywanej i pozostawiając ją zupełnie pustą.
Więcej na: www.ebook4all.pl
Rozdział 6. ♦ Komunikaty Windows
211
przez użytkownika metodą. Haki mogą być ustawiane bądź w kontekście konkretnej aplikacji, konkretniej wątku, bądź też globalnie. W tym drugim przypadku funkcja wykonywana będzie po wykryciu każdego komunikatu monitorowanego typu w całej „sieci nerwowej” systemu. Tylko drugi rodzaj haków, haki globalne (ang. global hooks), jest interesujący w kontekście aplikacji „podsłuchujących” klawiaturę. Haki globalne, a dokładniej procedury haka (ang. hook procedure), tj. funkcje uruchamiane po wykryciu interesującego komunikatu, muszą być umieszczone w bibliotece DLL10 ładowanej do przestrzeni adresowej każdej aplikacji, która otrzyma monitorowany typ komunikatu. W ten sposób dowolna aplikacja będzie mogła uruchomić przygotowaną przez nas funkcję. Mechanizm haków nie został oczywiście zaprojektowany przez programistów Microsoft po to, żeby możliwe było szpiegowanie komputerów kontrolowanych przez system Windows. Ich zadania mogą być bardzo różnorodne: od przygotowywania aplikacji instruktażowych, w których czynności użytkownika mogą być śledzone przez program uczący, po programowanie debugerów zintegrowanych ze środowiskami programistycznymi. Nawet pierwsze zadanie, do którego za chwilę wykorzystamy haki, a mianowicie sygnalizowanie naciśnięcia klawisza dźwiękiem, to dobry przykład praktycznego wykorzystania haka. Takie potwierdzenie naciśnięcia klawisza może być bardzo pożyteczne choćby w przypadku osób niepełnosprawnych korzystających z komputera.
Biblioteka DLL z procedurą haka Bibliotekę DLL z procedurą haka, tj. funkcją wywoływaną w momencie wykrycia obecności interesującego nas komunikatu, np. komunikatu związanego z naciśnięciem klawisza na klawiaturze, wyposażymy również w dodatkowe wyeksportowane funkcje, które umożliwią zakładanie i zdejmowanie haka. 1. Tworzymy projekt typu Win32 Project o nazwie Haki. W kreatorze zaznaczamy
pozycję DLL (rysunek 6.9) i klikamy Finish. 2. Projekt składa się z kilku plików — większość z nich zignorujemy, skupiając się na dllmain.cpp. To w nim zdefiniowana została funkcja zwrotna DllMain — odpowiednik funkcji WinMain w plikach .exe. Funkcje te wywoływane są
w momencie ładowania plików .dll i .exe do pamięci. 3. Definiujemy zmienną typu HMODULE i tak modyfikujemy metodę DllMain,
aby w momencie załadowania biblioteki do pamięci inicjowała tę zmienną, zapisując uchwyt do bieżącej biblioteki DLL (listing 6.27). Listing 6.27. Punkt wejściowy biblioteki DLL // dllmain.cpp : Defines the entry point for the DLL application. #include "stdafx.h" HMODULE uchwytDLL = NULL;
10
Biblioteki DLL opisane zostały w następnym rozdziale.
212
Visual C++. Gotowe rozwiązania dla programistów Windows
Rysunek 6.9. Tworzenie projektu biblioteki DLL dla platformy Win32
BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: uchwytDLL = hModule; break; case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: break; } return TRUE; }
4. Uchwyt do biblioteki DLL potrzebny będzie w momencie zakładania haka
— wskazuje bibliotekę DLL, w której zdefiniowana jest procedura haka. Do zakładania haka służy funkcja SetWindowsHookEx. Jej wywołanie oraz wyświetlenie ewentualnych komunikatów to cała zawartość metody UstawHak (listing 6.28). Uchwyt haka zapisywany jest do zmiennej uchwytHaka typu HHOOK — będzie potrzebny w momencie zdejmowania haka. Funkcja SetWindowsHookEx wymaga również procedury haka. Tej jeszcze nie zdefiniowaliśmy, ale możemy ją zadeklarować. Funkcję UstawHak należy wyeksportować z biblioteki DLL. W tym celu do jej sygnatury, przed wskazaniem zwracanego typu dodany został modyfikator extern "C" __declspec(dllexport).
Rozdział 6. ♦ Komunikaty Windows
213
Listing 6.28. Nowe funkcje w bibliotece DLL //deklaracja procedury haka extern "C" __declspec(dllexport) LRESULT CALLBACK KeyboardHookProc(int code,WPARAM ´wParam,LPARAM lParam); //uchwyt do haka HHOOK uchwytHaka = NULL; //funkcja zakładająca hak extern "C" __declspec(dllexport) void __stdcall UstawHak(void) { uchwytHaka = SetWindowsHookEx(WH_KEYBOARD, ´(HOOKPROC)KeyboardHookProc,uchwytDLL,NULL); if (uchwytHaka == NULL) MessageBox(NULL, L"Założenie haka nie powiodło się", ´L"KeyHook",MB_OK | MB_ICONERROR); else MessageBox(NULL, L"Założenie haka udało się", L"KeyHook", MB_OK | ´MB_ICONINFORMATION); }
5. Kolejnym krokiem będzie zdefiniowanie funkcji, która usunie hak (listing 6.29). Wywołuje ona funkcję WinAPI UnhookWindowsHookEx. Listing 6.29. Funkcja zdejmująca hak extern "C" __declspec(dllexport) void __stdcall UsunHak(void) { if (UnhookWindowsHookEx(uchwytHaka)) MessageBox(NULL, L"Usunięcie haka udało ´się", L"KeyHook", MB_OK | MB_ICONINFORMATION); else MessageBox(NULL, L"Usunięcie haka nie powiodło się", L"KeyHook", MB_OK | ´MB_ICONERROR); }
6. Teraz możemy zdefiniować procedurę haka (listing 6.30). Jej zadanie nie będzie
zbyt wyszukane — każdemu naciśnięciu klawisza towarzyszyć będzie dźwięk. Listing 6.30. Serce biblioteki DLL — procedura haka LRESULT CALLBACK KeyboardHookProc(int code,WPARAM wParam,LPARAM lParam) { if (code>=HC_ACTION) { if((lParam & 0x80000000)==0) Beep(150,50); else Beep(50,50); } return CallNextHookEx(uchwytHaka,code,wParam,lParam); }
Ustawienie haka realizowane jest przez wywołanie funkcji WinAPI SetWindowsHookEx, której pierwszym argumentem jest stała identyfikująca typ interesujących nas komunikatów, w naszym przypadku będzie to stała WH_KEYBOARD, drugim jest wskaźnik do zdefiniowanej w punkcie 6. funkcji zahaczonej, natomiast trzeci argument wskazuje uchwyt biblioteki, w której funkcja zahaczona jest umieszczona. W naszym przypadku, w którym funkcja ustawiająca hak i funkcja zahaczona są w tej samej bibliotece, umieścimy w trzecim argumencie uchwyt do bieżącej biblioteki.
214
Visual C++. Gotowe rozwiązania dla programistów Windows
Procedura haka będzie wywoływana, gdy tylko dotkniemy klawiatury. Jej sygnatura jest ściśle określona w dokumentacji WinAPI. Przyjmuje trzy argumenty: code, wParam i lParam. Pierwszy informuje o tym, co funkcja zahaczona powinna zrobić z przechwyconym komunikatem. Jeżeli jej wartość jest mniejsza od zera, komunikat powinien być zignorowany. Możliwe wartości nieujemne to 0 (stała HC_ACTION) lub 3 (HC_NOREMOVE). Informują o wykryciu komunikatu, a różnią się tym, że w drugim przypadku komunikat nie został jeszcze zdjęty z kolejki komunikatów. W przypadku komunikatów związanych z klawiaturą wartość code jest niemal zawsze równa 0. Wyjątkiem jest na przykład Microsoft Word, który w bardziej złożony sposób obsługuje komunikaty klawiaturowe. Pozostałe dwa argumenty funkcji zahaczonej przekazują dane komunikatu (porównaj opis komunikatów WM_KEYDOWN i WM_KEYUP w MSDN). Parametr wParam to kod znaku naciśniętego klawisza, a w bitach lParam umieszczone są dodatkowe informacje o kontekście, w jakim naciśnięty został klawisz. Szczegóły omówię niżej. Funkcja zahaczona będzie wywoływana z poziomu aplikacji, do której ładowana będzie nasza biblioteka, dlatego musi być wyeksportowana (stąd modyfikatory w jej deklaracji). W trakcie naciskania klawisza na klawiaturze system generuje dwa komunikaty. Pierwszy, gdy klawisz zostanie wciśnięty, drugi — gdy jest zwalniany. Widoczny w funkcji warunek sprawdzający wartość 31. bitu parametru lParam powoduje, że w obu przypadkach generowany jest inny dźwięk (służy do tego funkcja WinAPI Beep): wyższy, gdy klawisz jest naciskany, i niższy, gdy zwalniany. Po zareagowaniu na wykrycie komunikatu należy jeszcze wywołać funkcję CallNextHookEx, która powoduje wywołanie następnej funkcji zahaczonej związanej z tym samym typem komunikatów. Dzięki temu są one organizowane w swoiste łańcuchy, z których system wywołuje pierwszy element, a funkcje zahaczone dbają o wywołanie następnych. Przed przetestowaniem funkcji zapisanych w bibliotece zwróćmy jeszcze uwagę na modyfikatory eksportowanych funkcji. Pozornie używamy dwóch modyfikatorów: __stdcall i CALLBACK, ale w istocie drugi jest makrem o wartości równej pierwszemu. Pomijając kwestię przekazywania argumentów, efektem użycia tej konwencji jest dodanie znaku podkreślenia przed nazwą funkcji oraz liczby bajtów, które trzeba zarezerwować dla argumentów po nazwie funkcji i znaku @. Oznacza to, że funkcja UstawHak wyeksportowana będzie jako _UstawHak@0. Tych zmian uniknęlibyśmy, zmieniając modyfikator na __cdecl. Aby przetestować bibliotekę DLL, wykorzystamy program rundll32 obecny w systemie Windows. Jej argumentem jest nazwa biblioteki DLL, a po przecinku nazwa funkcji, którą chcemy uruchomić. W naszym przypadku chodzi oczywiście o funkcję UstawHak, a dokładnie o _UstawHak@0. 1. Skompilujmy projekt, naciskając klawisz F6. 2. Uruchommy wiersz polecenia i przejdźmy do podkatalogu Debug katalogu
projektu. Powinna znajdować się w nim biblioteka Haki.dll. 3. Wpisujemy polecenie rundll32 Haki.dll,_UstawHak@0 (rysunek 6.10).
Po pojawieniu się komunikatu „Założenie haka udało się” nie klikajmy OK, aby uniknąć usunięcia pierwotnej instancji biblioteki DLL z pamięci i tym samym usunięcia haka. Wówczas, gdy naciśniemy klawisze przy aktywnej dowolnej aplikacji, powinniśmy
Rozdział 6. ♦ Komunikaty Windows
215
Rysunek 6.10. Ładowanie biblioteki DLL z procedurą haka
usłyszeć charakterystyczne brzęczenie — znak, że nasz hak działa. Zauważmy, że wykrywane są osobno naciśnięcia wszystkich klawiszy, w tym klawiszy funkcyjnych oraz klawiszy Ctrl, Shift i Alt. Na razie procedura haka jedynie uroczo sobie pobrzękuje. To pozwala nam upewnić się, że jest ona rzeczywiście wykonywana. Warto również dodać do DllMain polecenia pokazujące komunikaty informujące o załadowaniu biblioteki do pamięci i jej usunięciu. Dzięki temu będziemy mogli na własne oczy przekonać się, że biblioteka jest ładowana do przestrzeni adresowej każdej aplikacji, która otrzyma komunikat o naciśnięciu klawisza, tzn. która będzie aktywna, gdy będziemy stukać w klawiaturę.
216
Visual C++. Gotowe rozwiązania dla programistów Windows
Rejestrowanie klawiszy naciskanych na klawiaturze Zmodyfikujemy funkcję zahaczoną, tak żeby zapisywała do pliku wszystkie naciśnięte klawisze. Nie będzie z tym żadnego kłopotu, bo — jak już wiemy — informacja ta przekazywana jest do funkcji w parametrze wParam. Wystarczy zapisać ją do pliku. Najprostsza realizacja tego pomysłu przedstawiona została na listingu 6.31. Listing 6.31. Funkcja haka dla keyloggera #include LRESULT CALLBACK KeyboardHookProc (int code,WPARAM wParam,LPARAM lParam) { if (code>=HC_ACTION) { char kod[5]; _itoa_s((char)wParam,kod,5,10); char lParam_bits[33]; _itoa_s(lParam,lParam_bits,33,2); //zapis do pliku std::ofstream txt("d:\\klawisze.log",std::ios::app); txt OnDblclk(hWnd, lParam1, lParam2); default: break; } return 1; } LONG CDiskInfoApletApp::OnNewInquire(UINT uAppNum, NEWCPLINFO* pInfo) { pInfo->dwSize = sizeof(NEWCPLINFO); pInfo->dwFlags = 0; pInfo->dwHelpContext = 0; pInfo->lData = 0; pInfo->hIcon = LoadIcon(IDI_ICON1); CString sCplName, sCplInfo; sCplName.LoadString(IDS_STRINGNAME); sCplInfo.LoadString(IDS_STRINGINFO); _tcscpy(pInfo->szName, sCplName); _tcscpy(pInfo->szInfo, sCplInfo); _tcscpy(pInfo->szHelpFile, _T("")); return 0; } LONG CDiskInfoApletApp::OnInquire(UINT uAppNum, CPLINFO* pInfo) { pInfo->idIcon = IDI_ICON1; pInfo->idName = IDS_STRINGNAME;
245
246
Visual C++. Gotowe rozwiązania dla programistów Windows pInfo->idInfo = IDS_STRINGINFO; pInfo->lData = 0; return 0; } LONG CDiskInfoApletApp::OnDblclk(HWND hWnd, UINT uAppNum, LONG lData) { dialog.DoModal(); return 0; } LONG CDiskInfoApletApp::OnExit() { return 0; } LONG CDiskInfoApletApp::OnGetCount() { return 1; } LONG CDiskInfoApletApp::OnInit() { return 1; } LONG CDiskInfoApletApp::OnStop(UINT uAppNum, LONG lData) { return 1; }
17. Modyfikujemy plik .def projektu zgodnie z listingiem 7.34. Listing 7.34. Plik definicji biblioteki DLL ; DiskInfoAplet.def : Declares the module parameters for the DLL. LIBRARY
"DiskInfoAplet"
EXPORTS ; Explicit exports can go here CPlApplet
18. Budujemy projekt.
Po zbudowaniu projektu otrzymujemy plik z rozszerzeniem .dll. Aby móc użyć naszej biblioteki jako apletu panelu sterowania, zmieniamy jej rozszerzenie z .dll na .cpl. Następnie plik .cpl kopiujemy do katalogu %windir%\system32 (zapewne C:\Windows\ System32). Podczas otwierania panelu sterowania obecność nowego apletu jest automatycznie wykrywana (to wtedy wywoływana jest funkcja CPlApplet z argumentami CPL_INIT, CPL_GETCOUNT, CPL_INQUIRE, i CPL_NEWINQUIRE). Wykrycie obecności apletu
Rozdział 7. ♦ Biblioteki DLL
247
spowoduje automatyczne umieszczenie jego ikony w panelu sterowania (rysunek 7.12). Kliknięcie tej ikony spowoduje załadowanie biblioteki DLL i pokazanie okna z informacjami o dyskach (rysunek 7.13).
Rysunek 7.12. Aplet informacji o dyskach w panelu sterowania Rysunek 7.13. Okno dialogowe wyświetlające informacje o dyskach
W Windows XP i nowszych w standardowym trybie wyświetlania aplety panelu sterowania podzielone są na kategorie. Aby przypisać nasz aplet do jednej z nich, należy umieścić w kluczu rejestru HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\ Current Version\Control Panel\Extended Properties\{305CA226-D286-468e-B8482B2E8E697B74} 2 wartość DWORD zawierającą numer kategorii. Są to4:
4
Więcej informacji na temat panelu sterowania znajdzie Czytelnik w dokumentacji MSDN pod hasłem Control Panel Items.
248
Visual C++. Gotowe rozwiązania dla programistów Windows 1. Wygląd i kompozycje 2. Drukarki i inne urządzenia 3. Połączenia sieciowe i internetowe 4. Dźwięk, mowa i urządzenia audio 5. Wydajność i konserwacja 6. Data, godzina, język i opcje regionalne 7. Opcje ułatwień dostępu 8. Dodaj lub usuń programy 9. Konta użytkowników 10. Centrum zabezpieczeń (obecne po zainstalowaniu Service Pack 2 systemu
Windows XP). 4294967295 — znak dla panelu sterowania, aby nie dodawał apletu do żadnej kategorii. Nasz aplet najlepiej nadaje się do kategorii 5. Można tę kategorię przypisać apletowi ręcznie, ale oczywiście lepiej byłoby przygotować dla niego stosowny instalator. Informacje niezbędne do realizacji tego zadania znajdzie Czytelnik w rozdziale 5., w którym opisane zostały funkcje służące do edycji rejestru systemu Windows.
Rozdział 8.
Automatyzacja i inne technologie bazujące na COM Technologia COM Technologia COM (ang. Component Object Model) jest fundamentem wielu technologii Windows. Jej zadaniem jest dostarczenie mechanizmu komunikacji między obiektami, a mechanizm ten ma być niezależny od języka programowania, w którym obiekty te są zaimplementowane. Technologia COM bazuje na architekturze klient-serwer. Obiekt serwer COM, udostępniając swoje metody i za ich pomocą, może być w pewnym stopniu kontrolowany przez obiekt będący klientem. Klient może łączyć się z istniejącym w pamięci obiektem lub tworzyć nowe jego instancje. Informacje o metodach udostępnianych przez obiekty COM przechowywane są w interfejsach. Z punktu widzenia systemu instancje interfejsów są tablicami zawierającymi wskaźniki do poszczególnych metod serwera COM (w tym sensie przypominają tablicę funkcji wirtualnych). Wszystkie interfejsy dziedziczą z interfejsu IUnknown, którego trzy metody pozwalają na inicjację interfejsu (metoda AddRef), pobranie informacji o interfejsie (QueryInterface) i jego zwolnienie (Release). Istotną zaletą technologii COM jest szeregowanie (ang. marshalling), które pozwala na przekazywanie informacji o obiektach w sposób niezależny od procesów, w kontekście których zostały utworzone. To właśnie umożliwia komunikację obiektów z różnych aplikacji. Na razie istnieją dwa rozszerzenia technologii COM. Pierwsze z nich to DCOM (ang. distributed COM). Wprowadzone zostało do 32-bitowych systemów Windows w 1996 roku i dodaje do COM możliwość komunikacji między obiektami znajdującymi się na różnych komputerach. W zasadzie nie ma już systemowych bibliotek udostępniających obiekty COM, które nie mogłyby być tworzone i kontrolowane zdalnie. Drugim rozszerzeniem jest COM+, wprowadzające model transakcyjny w komunikacji między
250
Visual C++. Gotowe rozwiązania dla programistów Windows
obiektami (zarówno lokalnie, jak i poprzez sieć). Zapewnia to zwiększenie bezpieczeństwa przeprowadzanych operacji w rozproszonym środowisku, w jakim działają obiekty COM/DCOM. Na bazie COM zbudowane są dwie technologie, które zainteresują nas w szczególny sposób: ActiveX i OLE. ActiveX to microsoftowa wersja komponentów w stylu biblioteki VCL firmy Borland, z tym że jak zwykle w przypadku dobrych pomysłów Microsoftu wprowadzona wprost do systemu operacyjnego. Jest to mechanizm pozwalający na definiowanie klas, które po zarejestrowaniu w systemie mogą być przez programistów wykorzystywane jak komponenty, z których budują oni interfejs aplikacji. Z tej technologii korzystają między innymi środowiska programistyczne Microsoft (zarówno dla platformy Win32, jak i platformy .NET). Obiekty będące instancjami kontrolek ActiveX można wykorzystać także w kodzie HTML na stronach WWW. Druga z technologii to OLE (ang. Object Linking and Embedding — łączenie i osadzanie obiektów). Pozwala na odwoływanie się do obiektów serwerów OLE udostępnianych przez jedną aplikację z poziomu innej aplikacji, posiadającej obiekt pojemnik OLE (ang. OLE container); odgrywa on rolę „kinowego ekranu”, na którym prezentowany jest interfejs obiektu serwera. Właśnie dzięki temu mechanizmowi możliwe jest edytowanie wewnątrz dokumentów Worda osadzonych w nich rysunków za pomocą narzędzi Paint. Edytor może być widoczny zarówno w owym „ekranie” (jest to osadzenie), jak i po uruchomieniu aplikacji udostępniającej serwer w osobnym oknie (łączenie). Szczególnie ciekawą odmianą OLE jest automatyzacja (ang. automation). W tym przypadku serwer udostępnia obiekty, które pozwalają na kontrolę działania całej aplikacji serwera przez obiekt będący klientem. Temu zagadnieniu w odniesieniu do elementów pakietu Microsoft Office poświęcona będzie znaczna część niniejszego rozdziału. Wyżej w owej piramidzie technologii, której podstawą jest COM, znajdują się mechanizmy związane z bazami danych. Mam w szczególności na myśli OLE DB (ang. OLE DataBase) i ADO (ang. ActiveX Data Objects). Od Windows 95 SE są to składniki systemu, które pozwalają na dostęp w scenariuszu klient-serwer do lokalnych i zdalnych baz danych (zwłaszcza do Microsoft Access i Microsoft SQL Server). Technologia COM była już przez nas używana w rozdziale 4., w którym do utworzenia pliku skrótu .lnk wykorzystaliśmy obiekt serwer COM ShellLink. Obiekt ten jest elementem systemu, a informacje o jego metodach zostały zapisane w wykorzystanych przez nas wówczas interfejsach IShellLink i IPersistFile.
Osadzanie obiektów OLE2 OLE (ang. Object Linking and Embedding — łączenie i osadzanie obiektów) jest jedną ze starszych technologii Windows. Pamiętamy, że już w Windows 3.1 możliwe było edytowanie rysunków osadzonych w dokumentach Worda za pomocą systemowego edytora Paintbrush. To była właśnie pierwsza odsłona OLE. Standard OLE ewoluował razem z Windows do wersji 32-bitowej (OLE2), wzbogacając się między innymi o automatyzację (ten mechanizm omówię w następnym podrozdziale).
Rozdział 8. ♦ Automatyzacja i inne technologie bazujące na COM
251
W Visual Studio nie ma zaimplementowanego komponentu pojemnika, który można wykorzystać jako kontrolkę. W zamian za to Application Wizard oferuje możliwość stworzenia aplikacji okienkowej, wspierającej dokumenty złożone. Omówieniu takiej możliwości poświęcimy kilka kolejnych projektów.
Statyczne osadzanie obiektu Aplikacja MFC, wspierająca dokumenty złożone, implementuje obiekt pojemnik OLE, który — używając przywoływanej już metafory — tworzy ekran, na którym prezentowany jest interfejs obiektu serwera OLE. Wybór obiektu serwera możliwy jest z poziomu kodu, co prezentuje niniejsze ćwiczenie. Ale można go także utworzyć już w trakcie działania aplikacji, zarówno za pomocą automatycznie tworzonej pozycji w menu Edit, Insert New Object, jak i ręcznie. Tę ostatnią możliwość pokażemy niżej. 1. Tworzymy nowy projekt typu MFC Application o nazwie OContainer. 2. Po uruchomieniu kreatora: a) Na zakładce Application Type kreatora zaznaczamy pozycję Single Document
oraz Use MFC in a static library. b) W części Compound Document Support wybieramy Container. c) Przechodzimy na zakładkę User Interface Features i zaznaczamy Toolbars: none. d) W obszarze Advanced Features usuwamy zaznaczenie opcji Printing and
Print Preview i klikamy Finish. 3. W pliku nagłówkowym COContainerView.h w klasie COContainerView umieszczamy deklarację prywatnej metody wstawObiekt o widocznej poniżej sygnaturze. Parametr fileName będzie wykorzystany w następnych projektach. void wstawObiekt(CString objName, CString fileName = NULL);
4. W pliku COContainerView.cpp definiujemy metodę klasy COContainerView::wstawObiekt według wzoru z listingu 8.1. Listing 8.1. Metoda wstawiająca obiekt OLE void COContainerView::wstawObiekt(CString objName, CString fileName) { m_pSelection = NULL; BeginWaitCursor(); // Poinformowanie użytkownika o specjalnych okolicznościach COContainerCntrItem* pItem = NULL; try { COContainerDoc* pDoc = GetDocument(); CLSID clsid; ASSERT_VALID(pDoc); pItem = new COContainerCntrItem(pDoc); ASSERT_VALID(pItem); // Obiekt z pliku czy z rejestru
252
Visual C++. Gotowe rozwiązania dla programistów Windows if(fileName == "") { CLSIDFromProgID(objName, &clsid); if (!pItem->CreateNewItem(clsid)) AfxThrowMemoryException(); // wystarczy dowolny wyjątek } else { if (!pItem->CreateFromFile(fileName)) AfxThrowMemoryException(); } ASSERT_VALID(pItem); m_pSelection = pItem; pDoc->UpdateAllViews(NULL);
}
} catch(CException* e) { if (pItem != NULL) { ASSERT_VALID(pItem); pItem->Delete(); } e->ReportError(); e->Delete(); } EndWaitCursor(); // koniec specjalnych okoliczności
5. Na końcu definicji metody COContainerView::OnInitialUpdate wstawiamy niniejsze wywołanie metody wstawObiekt: wstawObiekt(L"Paint.Picture");. 6. Kompilujemy i uruchamiamy aplikację.
Po uruchomieniu aplikacji zobaczymy osadzony obiekt. W menu Edit, Obiekt obraz – mapa bitowa, dostępne są pozycje: Edytuj, Otwórz oraz Konwertuj. Wybór pierwszej z nich powoduje uruchomienie edycji rysunku wewnątrz komponentu (zobaczymy w nim palety kolorów i narzędzia programu Paint). Wybór drugiej powoduje, że następuje edycja rysunku w zewnętrznej instancji programu Paint. Menu tworzone jest na podstawie komend zarejestrowanych w kluczu rejestru związanym z danym obiektem OLE (por. informacje w rozdziale 5., podrozdział „Dodawanie pozycji do menu kontekstowego związanego z zarejestrowanym typem pliku”). W przypadku osadzonego przez nas obiektu zdefiniowane są tylko dwa polecenia: Edytuj i Otwórz, dlatego też wybór opcji Konwertuj umożliwia konwersję tylko na typ obraz – mapa bitowa.
Kończenie edycji dokumentu. Łączenie menu aplikacji klienckiej i serwera OLE Po uruchomieniu aplikacji z poprzedniego projektu możemy za pomocą polecenia Edytuj z menu aplikacji uruchomić tryb edycji obrazu, w której edytor, w naszym przypadku Paint, „wciśnięty” jest w naszą aplikację. Okazuje się jednak, że nie można edycji zakończyć.
Rozdział 8. ♦ Automatyzacja i inne technologie bazujące na COM
253
Aby tego dokonać, należy samodzielnie wywołać metodę Close. Najwygodniej jest dodać do menu File pozycję Zakończ edycję rysunku. Z domyślnej metody zdarzeniowej tej pozycji menu należy wywołać wspomnianą wyżej metodę Close (listing 8.2). Dodatkową zaletą takiego rozwiązania będzie to, że osadzany obiekt umieści w tym menu swoje pozycje. W efekcie oba menu zostaną połączone. 1. Aby dodać pozycję Zakończ edycję rysunku do menu File, należy wykonać
następujące kroki: a) Przechodzimy do Resource View (Ctrl+Shift+E), rozwijamy węzeł przy
pozycji OContainer, następnie przy pozycji OContainer.rc i Menu. b) Klikamy dwukrotnie pozycję IDR_CNTR_INPLACE. Pojawi się wówczas
szkielet menu. c) Klikamy prawym przyciskiem myszy w obrębie pozycji File edytowanego menu
i z rozwiniętego w ten sposób menu kontekstowego wybieramy Insert New. d) Wpisujemy nazwę nowej pozycji, a mianowicie Zakończ edycję rysunku
(rysunek 8.1). Rysunek 8.1. Edytor menu Visual Studio 2008
2. Dodajemy teraz metodę zdarzeniową. W tym celu: a) Z menu kontekstowego nowej pozycji menu wybieramy polecenie Add Event
Handler. b) W kreatorze Event Handler Wizard ustawiamy: pozycję Message type
na Command, pozycję Function Handler Name na OnFileZakoncz i Class list na COContainerView (rysunek 8.2). c) Klikamy przycisk Add and Edit. d) Wstawioną metodę definiujemy zgodnie ze wzorem na listingu 8.2.
254
Visual C++. Gotowe rozwiązania dla programistów Windows
Rysunek 8.2. Kreator obsługi zdarzeń
Listing 8.2. Metoda związana z kliknięciem pozycji Zakończ edycję rysunku w menu File void COContainerView::OnFileZakoncz() { // Kończenie edycji COleClientItem* pActiveItem = GetDocument()->GetInPlaceActiveItem(this); if(pActiveItem != NULL) pActiveItem->Close(); }
Wykrywanie niezakończonej edycji podczas zamykania programu Przed zamknięciem aplikacji klienta uaktywniony edytor obiektu osadzonego w aplikacji powinien być zamknięty. Jeżeli tego nie zrobimy, wprowadzone do stanu obiektu zmiany nie zostaną zachowane. Dlatego przy zamknięciu aplikacji należy sprawdzić, czy działanie obiektu serwera OLE zostało zakończone. W tym celu należy przeciążyć metodę zdarzeniową COContainerDoc::CanCloseFrame, umieszczając w niej polecenia z listingu 8.3. Listing 8.3. Przy próbie zamknięcia aplikacji połączonej z serwerem pojawi się komunikat z przyciskami Tak, Nie i Anuluj BOOL COContainerDoc::CanCloseFrame(CFrameWnd* pFrame) { COleClientItem* pActiveItem = GetInPlaceActiveItem(pFrame); if(pActiveItem != NULL)
Rozdział 8. ♦ Automatyzacja i inne technologie bazujące na COM
255
{ int state = pActiveItem->GetItemState(); if(state == COleClientItem::activeUIState) switch(AfxMessageBox(L"Czy zakończyć edycję?", MB_YESNOCANCEL)) { case IDYES: pActiveItem->Close(); return 1; break; case IDNO: AfxMessageBox(L"Zmiany nie zostaną zachowane"); return 1; case IDCANCEL: return 0; break; } else return 1; } else return 1; }
Metoda COContainerDoc::CanCloseFrame powinna zwrócić wartość różną od zera tylko wtedy, gdy aplikacja może zostać zamknięta. Zamknięciu edytora towarzyszy wywołanie metody COleClientItem::OnDeactivateUI. Możemy ją wykorzystać do wykonania jakiejś czynności, która powinna nastąpić zawsze po zakończeniu edycji. Listing 8.4 ogranicza się do wyświetlenia komunikatu. Listing 8.4. Powiadomienie o zamknięciu edytora OLE void COContainerCntrItem::OnDeactivateUI(BOOL bUndoable) { COleClientItem::OnDeactivateUI(bUndoable); DWORD dwMisc = 0; m_lpObject->GetMiscStatus(GetDrawAspect(), &dwMisc); if (dwMisc & OLEMISC_INSIDEOUT) DoVerb(OLEIVERB_HIDE, NULL); // Wyświetlamy komunikat AfxMessageBox(L"Zamykanie edytora"); }
Inicjowanie edycji osadzonego obiektu z poziomu kodu Ręczne inicjowanie edycji rysunku wymaga wywołania metody DoVerb klasy obiektu OLE z odpowiednią komendą jako argumentem. Lista możliwych komend dostępna jest za pośrednictwem własności ObjectVerbs. Zobaczymy je, uruchamiając narzędzie OLE/COM Object Viewer (oleview.exe), które jest dostarczane wraz z Visual Studio 2008
256
Visual C++. Gotowe rozwiązania dla programistów Windows
(węzeł Verb). W przypadku obiektu Paint do edycji wewnątrz komponentu należy wywołać metodę DoVerb z argumentem OLEIVERB_SHOW, a w osobnym oknie — z argumentem OLEIVERB_OPEN. Jednak zazwyczaj jako argument metody DoVerb wykorzystuje się stałą OLEIVERB_PRIMARY oznaczającą komendę domyślną. Do menu głównego aplikacji pod pozycją File dodajemy polecenia z etykietami Edytuj i Otwórz. Tworzymy ich metody zdarzeniowe (analogicznie jak we wcześniejszym projekcie, z tym jednym wyjątkiem, że dodajemy te pozycje do menu IDR_MAINFRAME) i umieszczamy w nich polecenia zgodnie z listingiem 8.5. Listing 8.5. Wykonywanie operacji zdefiniowanych dla danego serwera OLE void COContainerView::OnFileEdytuj() { m_pSelection->DoVerb(OLEIVERB_SHOW, this); } void COContainerView::OnFileOtworz() { m_pSelection->DoVerb(OLEIVERB_OPEN, this); }
Dynamiczne osadzanie obiektu Aby w trakcie działania aplikacji otworzyć okno służące do osadzania obiektów OLE widoczne na rysunku 8.3, należy wywołać metodę COContainerView::OnInsert ´Object, skojarzoną z pozycją w menu Edit, Insert New Object.... To jednak w praktyce rzadko wykorzystywana możliwość. Zazwyczaj już w trakcie projektowania (ewentualnie na podstawie stanu aplikacji) wiemy, jaki obiekt chcemy umieścić w pojemniku. Wówczas należy wywołać zdefiniowaną w listingu 8.1 metodę wstawObiekt, z parametrami zależnymi od tego, czy zamierzamy osadzić „świeży” obiekt wybranego typu, czy też obiekt zachowany w pliku (np. obraz BMP lub dokument Worda). Metoda ta przyjmuje dwa argumenty. Drugi z nich określa, czy obiekt ma być odczytany z pliku. Argument pierwszy jest łańcuchem określającym nazwę obiektu OLE (te same nazwy, które używane będą w przypadku automatyzacji), a więc np. Word.Document, Excel.Sheet lub Paint.Picture (listing 8.6). Rysunek 8.3. Do pojemnika OLE możemy wstawić dowolny obiekt zarejestrowany w systemie
Rozdział 8. ♦ Automatyzacja i inne technologie bazujące na COM
257
Listing 8.6. Wstawiamy dokument Worda void COContainerView::OnFileWstawdokumentword() { wstawObiekt(L"Word.Document"); }
Z kolei tworząc obiekt na podstawie pliku, w drugim argumencie metody wstawObiekt wskazujemy nazwę pliku (listing 8.7). Listing 8.7. W celu wyboru pliku korzystamy ze standardowego okna dialogowego void COContainerView::OnFileWstawdokumentwordzpliku() { CFileDialog fileDialog(TRUE, 0, 0, 2, L"Dokumenty Worda (*.doc)|*.doc||"); if(fileDialog.DoModal() != IDCANCEL) wstawObiekt(NULL, fileDialog.GetPathName()); else MessageBox(L"Anulowano wstawienie dokumentu"); }
Rysunek 8.4 pokazuje przykład aplikacji z osadzonym dokumentem Microsoft Word. Rysunek 8.4. Aplikacja ze wstawionym dokumentem Worda
258
Visual C++. Gotowe rozwiązania dla programistów Windows
Automatyzacja Automatyzacja (ang. automation) jest jedną z technologii Windows bazujących na COM/DCOM, czyli na standardzie umożliwiającym aplikacjom udostępnianie obiektów innym aplikacjom (lokalnym lub zdalnym). Ściśle rzecz biorąc, automatyzacja jest częścią technologii OLE2. Pozwala na kontrolę aplikacji serwerów zawierających obiekty automatyzacji przez aplikacje klienty (lub sterowniki), które, uzyskując dostęp do obiektów serwerów automatyzacji, uzyskują tym samym kontrolę nad aplikacją serwerem. Innymi słowy, mamy do czynienia z pewną odmianą mechanizmu OLE: obiekt serwer OLE udostępnia metody pozwalające na pełną kontrolę aplikacji serwera1. Należy wspomnieć, że automatyzacja, korzystając z zalet DCOM, umożliwia też kontrolę aplikacji uruchomionych na zdalnych komputerach. Typowymi przykładami serwerów automatyzacji są składniki pakietu Microsoft Office, a typowym ich zastosowaniem — gromadzenie danych wykorzystywanych przez aplikacje w arkuszach Excela oraz ich obróbka za pomocą udostępnionych przez ten serwer funkcji oraz wykorzystywanie dokumentów Worda do tworzenia wydruków. Automatyzacja umożliwia niemal nieograniczony dostęp do komórek i funkcji Excela oraz każdego elementu dokumentu Worda. Właśnie na tych dwóch serwerach automatyzacji skupimy się w poniższych przykładach, pokazując kilka typowych działań, które są z nimi związane. Chciałbym z góry uprzedzić, że poniższe projekty to jedynie wierzchołek góry lodowej zagadnień związanych z automatyzacją. Aby rozwinąć większy projekt, konieczne jest poznanie szczegółowej struktury obiektów serwera automatyzacji oraz ich metod i własności, a to wykracza poza ramy tego rozdziału, którego celem jest jedynie uświadomienie Czytelnikowi, czym jest automatyzacja i jak może ją wykorzystać we własnych projektach. W przypadku Microsoft Office najrzetelniejszym źródłem informacji o serwerach automatyzacji jest dokumentacja MSDN — wystarczy w wyszukiwarce na stronie MSDN wpisać hasło automation. Źródłem informacji o innych serwerach automatyzacji są ich producenci. Tu szczególnie pozytywnie wyróżnia się dokumentacja aplikacji Adobe Acrobat dostępna na stronie WWW.
Typ VARIANT i klasa COleVariant Aby możliwe było korzystanie z automatyzacji, środowisko programistyczne musi uporać się z zasadniczym problemem: w trakcie projektowania aplikacji nie są znane własności i metody obiektu COM, który podłączany jest dopiero w trakcie jej działania. Rozwiązano ten problem, definiując typ VARIANT. Typ ten nie jest niczym innym, jak strukturą składającą się z unii przechowujących dane (lub wskaźnik do danych) oraz pola określającego typ tych danych (wskaźnika). Pole przechowujące informacje o typie nazywa się vt. Typ VARIANT wspiera znaczną liczbę znanych typów, takich jak: double 1
Skupimy się jedynie na korzystaniu z tzw. serwerów zewnątrzprocesowych, tj. kontrolowanych przez inne aplikacje. Alternatywą są serwery wewnątrzprocesowe, które udostępniają obiekty automatyzacji umieszczone w bibliotece DLL ładowanej przez moduł główny aplikacji w obrębie tego samego procesu.
Rozdział 8. ♦ Automatyzacja i inne technologie bazujące na COM
259
(VT_R8), BSTR (VT_BSTR), BOOL (VT_BOOL). Zatem jeśli zmiennej typu VARIANT przypisano wartość typu double, to pole vt ma wartość VT_R8. W celu odczytania jego wartości należy się wówczas odwołać do pola unii o nazwie dblVal (listing 8.8). Listing 8.8. Przykład wykorzystania klasy COleVariant CString temp; COleVariant v; v = L"Helion"; AfxMessageBox(v.bstrVal); v = 2.0; temp.Format(L"%.2lf", v.dblVal); AfxMessageBox(temp); v.ChangeType(VT_BSTR, &v); // zmiana typu AfxMessageBox(v.bstrVal);
Wszędzie tam, gdzie należy przekazać parametr typu VARIANT, możemy skorzystać z klasy COleVariant, która opakowuje ten typ w MFC. Korzystając z klasy COleVariant, otrzymujemy dostęp do pól typu VARIANT. Ponadto klasa ta ułatwia inicjalizację obiektów typu VARIANT. Oznacza to, że w miejscu, w którym należy przekazać parametr tego typu, można użyć odpowiedniego konstruktora klasy COleVariant. Przykłady praktycznego wykorzystania tej klasy przedstawiają poniższe projekty.
Łączenie z serwerem automatyzacji aplikacji Excel Visual Studio 2008, podobnie jak poprzednie jego wersje, dostarcza narzędzie Class Wizard (kreator klasy). To właśnie za jego pomocą wygenerujemy klasy MFC na podstawie bibliotek typów aplikacji Microsoft Office. Interfejsy, metody i własności udostępniane przez serwery automatyzacji są bowiem zawarte w bibliotekach typów. Do ich przeglądania służy narzędzie OLE/COM ObjectViewer (oleview.exe), dostarczane wraz z VS 2008. Przykłady wykorzystania automatyzacji przedstawione w następnych projektach dotyczą głównie Worda i Excela. Jednak w podobny sposób możemy automatyzować inne aplikacje. W tabeli 8.1 przedstawiono nazwy bibliotek typów dla odpowiednich wersji aplikacji z rodziny Microsoft Office oraz domyślne ścieżki dostępu do tych bibliotek. Tabela 8.1. Biblioteki typów dla aplikacji Microsoft Office Nazwa aplikacji
Nazwa biblioteki typu
Microsoft Access 97
Msacc8.olb
Microsoft Binder 97
Msbdr8.olb
Microsoft Excel 97
Excel8.olb
Microsoft Graph 97
Graph8.olb
Microsoft Outlook 97
Msoutl97.olb
Microsoft PowerPoint 97
Msppt8.olb
Microsoft Word 97
Msword8.olb
Domyślna ścieżka dostępu
C:\Program Files\ Microsoft Office\Office
260
Visual C++. Gotowe rozwiązania dla programistów Windows
Tabela 8.1. Biblioteki typów dla aplikacji Microsoft Office — ciąg dalszy Nazwa aplikacji
Nazwa biblioteki typu
Domyślna ścieżka dostępu
Microsoft Office 97
Mso97.dll
C:\Program Files\Common Files\Microsoft Shared\Office
Microsoft Jet Database 3.5
DAO350.dll
C:\Program Files\Common Files\Microsoft Shared\Dao
Microsoft Access 2000
Msacc9.olb
Microsoft Binder 2000
Msbdr9.olb
Microsoft Excel 2000
Excel9.olb
Microsoft Graph 2000
Graph9.olb
Microsoft Outlook 2000
Msoutl9.olb
Microsoft PowerPoint 2000
Msppt9.olb
Microsoft Word 2000
Msword9.olb
Microsoft Office 2000
Mso9.dll
C:\Program Files\Common Files\Microsoft Shared\Office
Microsoft Jet Database 3.51
DAO360.dll
C:\Program Files\Common Files\Microsoft Shared\Dao
Microsoft Access 2002
Msacc.olb
Microsoft Excel 2002
Excel.exe
Microsoft Graph 2002
Graph.exe
Microsoft Outlook 2002
MSOutl.olb
Microsoft PowerPoint 2002
MSPpt.olb
Microsoft Word 2002
MSWord.olb
Microsoft Office 2002
MSO.dll
Microsoft Office Access 2003
Msacc.olb
Microsoft Office Excel 2003
Excel.exe
Microsoft Office Graph 2003
Graph.exe
Microsoft Office Outlook 2003
MSOutl.olb
Microsoft Office PowerPoint 2003
MSPpt.olb
Microsoft Office Word 2003
MSWord.olb
Microsoft Office 2003
MSO.dll
Microsoft Office Access 2007
Msacc.olb
Microsoft Office Excel 2007
Excel.exe
Microsoft Office Graph 2007
Graph.exe
Microsoft Office Outlook 2007
MSOutl.olb
Microsoft Office PowerPoint 2007
MSPpt.olb
Microsoft Office Word 2007
MSWord.olb
Microsoft Office 2007
MSO.dll
C:\Program Files\ Microsoft Office\Office
C:\Program Files\ Microsoft Office\Office10
C:\Program Files\Common Files\Microsoft Shared\Office10
C:\Program Files\Microsoft Office\Office11
C:\Program Files\Common Files\Microsoft Shared\Office11
C:\Program Files\ Microsoft Office\Office12
C:\Program Files\Common Files\Microsoft Shared\Office12
Rozdział 8. ♦ Automatyzacja i inne technologie bazujące na COM
261
Próby rozpocznijmy od połączenia się z działającą aplikacją Excel. Pobierzmy informacje o jej stanie (nazwę bieżącego skoroszytu, arkusza, adres bieżącej komórki i jej zawartość): 1. Tworzymy nowy projekt MFC Application z oknem dialogowym o nazwie Excel1. 2. W obszarze Resource View klikamy Add, Add Class.... Wybieramy pozycję MFC
Class From TypeLib (klasa MFC z biblioteki typów) i klikamy Add (rysunek 8.5). Rysunek 8.5. Add Class Wizard
3. Z rozwijanej listy wszystkich zarejestrowanych w systemie bibliotek typów
wybieramy Microsoft Excel XX.X Object Library, gdzie XX.X określa wersję programu Excel, którą zamierzamy automatyzować. W niniejszym rozdziale używamy wersji 11.0, co odpowiada Microsoft Excel 2003. 4. Z dostępnych po prawej stronie interfejsów wybieramy _Application,
_Workbook, _Worksheet, Range, Workbooks i Worksheets (rysunek 8.6). Po wciśnięciu przycisku Finish stwierdzamy, że do naszego projektu dodane zostały dodatkowe klasy, których metody możemy przejrzeć za pomocą Class View2. 5. W plikach nagłówkowych CApplication.h, CWorkbook.h, CWorkbooks.h,
CWorksheet.h, CWorksheets.h oraz CRange.h usuwamy poniższą linię3: #import "C:\\Program Files\\Microsoft Office\\OFFICE11\\EXCEL.EXE" ´no_namespace
6. Przechodzimy do edycji pliku Excel1Dlg.h, w którym tuż po dyrektywie #pragma once dodajemy polecenia z listingu 8.9.
2
Kreator Class Wizard generuje osobne pliki nagłówkowe dla każdego z wybranych interfejsów, więc w trakcie ich dodawania należy wybrać tylko te, z których będziemy korzystać.
3
Ścieżka do pliku excel.exe może zależeć od parametrów instalacji Microsoft Office.
262
Visual C++. Gotowe rozwiązania dla programistów Windows
Rysunek 8.6. Lista dostępnych interfejsów biblioteki typu Microsoft Excel 11.0
Listing 8.9. Nagłówek pliku Excel1Dlg.h #pragma once #import "C:\\Program Files\\Common Files\\Microsoft Shared\\OFFICE11\\mso.dll" #import "C:\\Program Files\\Microsoft Office\\OFFICE11\\EXCEL.exe" rename("RGB", ´"ExcelRGB") rename("CopyFile", "ExcelCopyFile") rename("DialogBox", ´"ExcelDialogBox") rename_namespace("MSExcel") raw_interfaces_only #include "CApplication.h" #include "CWorkbook.h" #include "CWorksheet.h" #include "CRange.h"
Jeśli w trakcie kompilacji pojawi się błąd z komunikatem „warning C4003: not enough actual parameters for macro 'DialogBoxA'”, to w pliku nagłówkowym CRange.h należy zmienić nazwę funkcji Variant DialogBox() na Variant _DialogBox(). Błąd ten spowodowany jest tym, że MFC Class Wizard w pewnych wypadkach nie radzi sobie z rozwiązywaniem konfliktów nazw między makrami zdefiniowanymi w WinAPI a metodami zawartymi w wygenerowanych klasach. Z tego powodu w listingu 8.9 korzystamy z funkcji rename. 7. W klasie CExcel1Dlg dodajemy metodę zdefiniowaną według listingu 8.10 oraz dwa prywatne pola CApplication oExcel i CWorkbook oBook. Listing 8.10. Pobieranie interfejsu IDispatch uruchomionego serwera automatyzacji LPDISPATCH CExcel1Dlg::GetIDispatch(CString AppName) { CLSID clsid; CLSIDFromProgID(AppName, &clsid);
Rozdział 8. ♦ Automatyzacja i inne technologie bazujące na COM
263
IDispatch *pDisp; IUnknown *pUnk; HRESULT hr = GetActiveObject(clsid, NULL, (IUnknown**)&pUnk); ASSERT(!FAILED(hr)); hr = pUnk->QueryInterface(IID_IDispatch, (void **)&pDisp); ASSERT(!FAILED(hr)); // Zwolnienie niepotrzebnego już interfejsu IUnknown pUnk->Release(); return pDisp; }
8. W konstruktorze klasy CExcel1Dlg umieszczamy polecenie CoInitializeEx(NULL,0);, które zainicjuje bibliotekę COM. 9. Umieszczamy na formie przycisk z etykietą Informacje o aktualnie uruchomionej
aplikacji Excel. 10. Klikamy go dwukrotnie, aby utworzyć domyślną metodę zdarzeniową, w której
umieszczamy polecenia z listingu 8.11. Listing 8.11. Prezentujemy informacje o uruchomionej aplikacji Excel void CExcel1Dlg::OnBnClickedButton1() { CWorksheet oSheet; CRange oRange; LPDISPATCH pDisp; pDisp = GetIDispatch(L"Excel.Application"); oExcel.AttachDispatch(pDisp); oRange = oExcel.get_ActiveCell(); oSheet = oExcel.get_ActiveSheet(); oBook = oExcel.get_ActiveWorkbook(); CString adres = oRange.get_Address(COleVariant((short)TRUE), COleVariant((short)TRUE), 1, COleVariant((short)FALSE), COleVariant((short)FALSE)); VARIANT val = oRange.get_Value2(); MessageBox(L"Nazwa otwartego skoroszytu: " + oBook.get_Name() + L"\nNazwa ´otwartego arkusza: " + oSheet.get_Name() + L"\nAdres aktywnej komórki: " + adres + L"\nWartość ´aktywnej komórki: " + (CString)val); oExcel.ReleaseDispatch(); // Odłączanie od Excela }
264
Visual C++. Gotowe rozwiązania dla programistów Windows
Kompilujemy i uruchamiamy aplikację. Po kliknięciu przycisku powinniśmy zobaczyć komunikat zawierający dane o stanie aplikacji Excel podobne do widocznych na rysunku 8.7.
Rysunek 8.7. Excel 2003 i łącząca się z nim aplikacja kliencka (sterownik automatyzacji)
Po pobraniu informacji należy pamiętać o rozłączeniu z serwerem za pomocą metody Excel.ReleaseDispatch. W przeciwnym wypadku mogą się pojawić problemy z kolejnymi połączeniami — bez rozłączenia aplikacja serwera (niekoniecznie widoczna na pasku zadań) nie zostanie zamknięta. W takiej sytuacji można w Menedżerze zadań Windows, na zakładce Procesy, odnaleźć odpowiedni proces i wymusić jego zakończenie. Komentarza wymaga jeszcze sposób działania metody GetIDispatch z listingu 8.11, zwracającej obiekt IDispatch związany z aktualnie uruchomioną aplikacją (tu: Microsoft Excel). Za pomocą tego interfejsu aplikacja kliencka uzyskuje dostęp do metod i własności udostępnianych przez serwery automatyzacji. Te ostatnie rejestrują się w tabeli ROT (z ang. Running Object Table), korzystając z metody API pod nazwą RegisterActiveObject. Działające instancje takich serwerów mogą być znalezione za pomocą funkcji API GetActiveObject, której wywołanie musi zawierać identyfikator klasy CSLID. CSLID jest globalnym identyfikatorem klasy obiektów COM i znajduje się w kluczu rejestru systemowego: HKEY_LOCAL_MACHINE\SOFTWARE\Classes\CLSID. W ogólności może być uruchomionych kilka instancji aplikacji Microsoft Office. Jednak tak skonstruowana metoda GetIDispatch zwróci interfejs IDispatch pierwszej uruchomionej instancji Excela.
Rozdział 8. ♦ Automatyzacja i inne technologie bazujące na COM
265
Uruchamianie aplikacji Excel za pośrednictwem mechanizmu automatyzacji Aby utworzyć nową instancję obiektu Excel.Application, co prowadzi do uruchomienia nowej kopii aplikacji Excela, należy wykorzystać metodę Excel.CreateDispatch. Po uruchomieniu w taki sposób okno aplikacji serwera pozostanie niewidoczne. Zmienimy to, korzystając z własności Visible obiektu serwera. Ponadto utworzymy nowy, gotowy do edycji zeszyt. Wszystkie te czynności ujęte zostały na listingu 8.12, który prezentuje domyślną metodę zdarzeniową dla przycisku Button2 dodanego do okna w poprzednim projekcie. Listing 8.12. Uruchamianie aplikacji Excel z wykorzystaniem klasy COleException do obsługi wyjątków void CExcel1Dlg::OnBnClickedButton2() { COleException e; CWorkbooks oBooks; CWorksheets oSheets; CWorksheet oSheet; COleVariant covOptional(DISP_E_PARAMNOTFOUND,VT_ERROR); // Parametr opcjonalny if(!oExcel.CreateDispatch(L"Excel.Application", &e)) { e.ReportError(); return; } else { oExcel.put_Visible(TRUE); oExcel.put_UserControl(TRUE); // Dodanie nowego skoroszytu i pobranie pierwszego arkusza oBooks = oExcel.get_Workbooks(); oBook = oBooks.Add(covOptional); oSheets = oBook.get_Worksheets(); oSheet = oSheets.get_Item(COleVariant((short)1)); oExcel.ReleaseDispatch(); } }
W powyższej metodzie wykorzystano specjalny parametr opcjonalny covOptional, który w MFC należy przekazać do metody obiektu serwera, wykorzystując klasę COleVariant. Do obsługi wyjątków wykorzystaliśmy klasę COleException. Spróbujmy usunąć linię zawierającą polecenie CoInitializeEx(NULL, 0); w konstruktorze klasy CExcel1Dlg. W efekcie po ponownym skompilowaniu projektu i wciśnięciu przycisku Button2 powinniśmy uzyskać komunikat przedstawiony na rysunku 8.8.
266
Visual C++. Gotowe rozwiązania dla programistów Windows
Rysunek 8.8. Komunikat o błędzie
Uruchamianie procedur serwera automatyzacji Wywoływanie metod serwera automatyzacji jest podobne do czytania i zmieniania własności jego obiektu. Można dla przykładu zamknąć uruchomioną aplikację Excela, korzystając z jego metody Quit. Umieszczamy na formie przycisk z etykietą Zamknij działającą aplikację Excel. Tworzymy związaną z tym przyciskiem domyślną metodę zdarzeniową i umieszczamy w niej polecenia widoczne na listingu 8.13. Listing 8.13. Zamykamy wcześniej uruchomioną aplikację Excela void CExcel1Dlg::OnBnClickedButton3() { LPDISPATCH pDisp = GetIDispatch(L"Excel.Application"); oExcel.AttachDispatch(pDisp); oExcel.Quit(); oExcel.ReleaseDispatch(); }
Eksplorowanie danych w arkuszu kalkulacyjnym A teraz zajrzyjmy do aktywnej aplikacji Excela i sprawdźmy, jakie zeszyty i arkusze są w niej edytowane. 1. W oknie z poprzedniego projektu dodajemy przycisk (w naszym przypadku powinien to być Button4) oraz drzewo (TreeControl), z którym wiążemy zmienną o nazwie Tree1. 2. W oknie własności kontrolki TreeControl (F4) ustawiamy Has Buttons oraz
Has Lines na True. 3. Tworzymy domyślną metodę zdarzeniową dla przycisku i umieszczamy w niej
polecenia zgodnie ze wzorem z listingu 8.14. Listing 8.14. Prezentacja struktury arkuszy w drzewie void CExcel1Dlg::OnBnClickedButton4() { LPDISPATCH pDisp = GetIDispatch(L"Excel.Application"); oExcel.AttachDispatch(pDisp); CWorkbooks workbooks = oExcel.get_Workbooks();
Rozdział 8. ♦ Automatyzacja i inne technologie bazujące na COM
267
CWorksheets worksheets; CWorkbook workbook; CWorksheet worksheet; Tree1.DeleteAllItems(); for(int i = 1; i Progress1.GetPos(); } catch(CException &c) { c.ReportError(); c.Delete(); } return 0; } void CSerwerAutomatyzacjiDlgAutoProxy::SetPosition(LONG newVal) { AFX_MANAGE_STATE(AfxGetAppModuleState()); try { m_pDialog->Progress1.SetPos(newVal); } catch(CException &c) { c.ReportError(); c.Delete(); } }
6. Kompilujemy projekt (Ctrl+F5). 7. Konieczne jest zarejestrowanie serwera w systemie. Na szczęście jest to proste.
Wystarczy uruchomić aplikację zawierającą serwer automatyzacji z parametrem /regserver lub /register. W naszym przypadku powinniśmy w linii komend wpisać komendę SerwerAutomatyzacji /regserver. Oczywiście powinniśmy wówczas być w podkatalogu, w którym znajduje się skompilowany plik aplikacji. Można również posłużyć się poleceniem Comand Arguments na zakładce Debugging z menu Project, SerwerAutomatyzacji Properties.... Odinstalowanie serwera następuje wówczas, gdy aplikację uruchomimy z parametrem /unregserver lub /unregister4. Do zarejestrowania serwera można również użyć pliku SerwerAutomatyzacji.reg wygenerowanego przez kreator aplikacji MFC Application Wizard. 4
Do zarejestrowania i wyrejestrowania serwera można również użyć polecenia regsrv32 z linii komend.
Rozdział 8. ♦ Automatyzacja i inne technologie bazujące na COM
287
Kreator dodał do projektu moduł DlgProxy.cpp/DlgProxy.h, w którym zdefiniowana została klasa CSerwerAutomatyzacjiDlgProxy. Implementuje ona między innymi interfejs-szablon IDispatch. Do projektu dołączony został również plik SerwerAutomatyzacji.idl. W pliku tym zawarte są informacje o interfejsie Dispinterface, opartym na IDispatch. To właśnie do interfejsu Dispinterface dodawane są informacje o metodach i własnościach serwera automatyzacji. Rejestracja obiektu będącego serwerem automatyzacji (punkt 6.) polega na dodaniu do rejestru HKEY_CLASSES_ROOT\CLSID klucza o nazwie odpowiadającej unikalnemu numerowi GUID obiektu automatyzacji. W naszym przypadku numer ten jest ustalany na podstawie wartości przekazanej do makra IMPLEMENT_OLECREATE2 wywołanego w pliku DlgProxy.cpp.
Testowanie serwera automatyzacji Aby przetestować działanie serwera automatyzacji SerwerAutomatyzacji, utworzymy aplikację klienta (kontrolera) automatyzacji. 1. Tworzymy nowy projekt aplikacji MFC z oknem dialogowym o nazwie
KlientAutomatyzacji. 2. Do projektu dodajemy nową klasę, korzystając z MFC Class From TypeLib,
gdzie z listy rozwijanej wybieramy SerwerAutomatyzacji. Dodajemy jedyny dostępny interfejs, czyli ISerwerAutomatyzacji i klikamy Finish. 3. W wygenerowanym pliku nagłówkowym CSerwerAutomatyzacji.h usuwamy
linię zawierającą: #import no_namespace
4. W pliku nagłówkowym KlientAutomatyzacjiDlg.h dodajemy komendy
z listingu 8.37, a następnie deklarujemy prywatne pole klasy CSerwerAutomatyzacji o nazwie oSerwer. Listing 8.37. Nagłówek pliku KlientSerweraAutomatyzacjiDlg.h #pragma once #import raw_interfaces_only #include "CSerwerAutomatyzacji.h"
5. W widoku projektowania umieszczamy na formie przyciski z etykietami Połącz i Odłącz oraz suwak CSliderCtrl. Wiążemy zmienną Slider1 z kontrolką CSliderCtrl. 6. W konstruktorze klasy CKlientAutomatyzacjiDlg dodajemy polecenie: CoInitializeEx(NULL, 0);
7. Do klasy CKlientAutomatyzacjiDlg dodajemy prywatne pole typu CSerwerAutomatyzacji o nazwie oSerwer. 8. Tworzymy domyślną metodę zdarzeniową pierwszego przycisku i umieszczamy
w niej polecenia widoczne na listingu 8.38.
288
Visual C++. Gotowe rozwiązania dla programistów Windows
Listing 8.38. Uruchamiamy serwer automatyzacji i ustawiamy pasek postępu w pozycji wyjściowej void CKlientAutomatyzacjiDlg::OnBnClickedButton1() { oSerwer.CreateDispatch(L"SerwerAutomatyzacji.Application"); oSerwer.Clear(); }
9. Następnie tworzymy metodę zdarzeniową dla suwaka (TRBN_THUMPOSCHANGING lub NM_CUSTOMDRAW w zależności od zainstalowanego systemu operacyjnego) i umieszczamy w niej polecenie przypisujące własności Position obiektu Serwer
wartość odpowiadającą aktualnej pozycji suwaka (listing 8.39). Należy pamiętać o ustawieniu własności Notify Before Move kontrolki CSliderCtrl na True. Listing 8.39. Metoda zdarzeniowa kontrolki CSliderCtrl void CKlientAutomatyzacjiDlg::OnTRBNThumbPosChangingSlider1(NMHDR *pNMHDR, LRESULT ´*pResult) { // This feature requires Windows Vista or greater. // The symbol _WIN32_WINNT must be >= 0x0600. NMTRBTHUMBPOSCHANGING *pNMTPC = reinterpret_cast(pNMHDR); // TODO: Add your control notification handler code here *pResult = 0; }
oSerwer.SetPosition(Slider1.GetPos());
10. Tworzymy metodę zdarzeniową dla drugiego przycisku, zgodną z listingiem 8.40. Listing 8.40. Odłączamy aplikację kliencką od serwera automatyzacji void CKlientAutomatyzacjiDlg::OnBnClickedButton2() { oSerwer.ReleaseDispatch(); }
Kompilujemy projektowaną aplikację i uruchamiamy ją (F5). Klikamy przycisk Połącz. Uruchomiona zostanie aplikacja serwera automatyzacji przygotowana w poprzednim projekcie (o ile zarejestrowaliśmy ją zgodnie z instrukcjami z tego projektu). Za pomocą paska przewijania w aplikacji kontrolerze możemy kontrolować stan paska postępu w aplikacji serwerze (rysunek 8.20). Rysunek 8.20. Sterownik (z przodu) i uruchomiony przez niego serwer automatyzacji (w tle)
Rozdział 8. ♦ Automatyzacja i inne technologie bazujące na COM
289
ActiveX Nazwa ActiveX wprowadzona została przez Microsoft w miejsce skrótu OCX, znanego wszystkim „starym” programistom korzystającym z Visual Basica, zanim ten został ograniczony do platformy .NET. Oznacza ona bibliotekę komponentów, wykorzystywaną przez środowiska programistyczne Microsoftu. Obecnie mechanizm ActiveX jest elementem systemu Windows. Kontrolki są zatem rejestrowane nie w środowisku programistycznym, ale w samym systemie, co nadaje im uniwersalny charakter — zarejestrowana w systemie kontrolka może być wykorzystywana w niemal każdej platformie programistycznej, a nawet w kodzie HTML. Wadą takiego rozwiązania jest jednak angażowanie systemu do jej kontroli — w przeciwieństwie do kontrolki MFC kontrolka ActiveX nie jest integralną częścią skompilowanej aplikacji i musi być dostarczona niezależnie (co oznacza utrudnienie w dystrybucji aplikacji). ActiveX jako technologia oznacza zatem oparty na standardzie COM mechanizm udostępniania gotowych do wykorzystania klas. Każda zarejestrowana klasa ActiveX posiada swój unikalny w systemie identyfikator (GUID — Globally Unique Identifier) o nazwie CLSID (Class Identifier). Aplikacja klient musi go podać systemowi, aby wskazać na interesującą ją klasę ActiveX, której instancję chciałaby utworzyć.
Korzystanie z kontrolek ActiveX Visual Studio 2008 oferuje bardzo prosty sposób osadzania kontrolek ActiveX w projektach przeznaczonych dla platformy Win32, który sprowadza się do paru kliknięć. 1. Tworzymy nowy projekt aplikacji MFC z oknem dialogowym. 2. W widoku projektowania okna dialogowego prawym przyciskiem myszy
rozwijamy menu kontekstowe projektowanego okna i wybieramy polecenie Insert ActiveX Control... (rysunek 8.21).
Rysunek 8.21. Wstawianie kontrolki ActiveX
290
Visual C++. Gotowe rozwiązania dla programistów Windows 3. W następnym oknie, ukazującym listę wszystkich kontrolek ActiveX
zarejestrowanych w systemie (rysunek 8.22), wybieramy Windows Media Player. Rysunek 8.22. Lista wszystkich zarejestrowanych w systemie kontrolek ActiveX
4. W oknie własności nowo wstawionej kontrolki ActiveX możemy zmienić jej
własność URL, tak aby wskazywała dowolny plik znajdujący się na lokalnym komputerze bądź w Internecie. Wybrany plik zostanie odtworzony przez tę kontrolkę. Innym, prostszym sposobem wybrania pliku jest skorzystanie z okna właściwości Windows Media Player, które wywołuje się przez wciśnięcie kombinacji Shift+F4 (lub menu View, Property Pages) po zaznaczeniu kontrolki. W części Ogólne możemy wybrać odtwarzany plik, układ regulatorów, opcje odtwarzania i ustawienia głośności. Ponadto możemy wymusić odtwarzanie filmu na pełnym ekranie. Po skompilowaniu i uruchomieniu projektu powinniśmy uzyskać efekt podobny do widocznego na rysunku 8.20. Rysunek 8.23. Tu odtwarzam plik .mp3, a na „ekranie” prezentowana jest wizualizacja generowana przez Windows Media Player
Rozdział 8. ♦ Automatyzacja i inne technologie bazujące na COM
291
Aby można było kontrolować kontrolkę ActiveX z poziomu kodu, należy związać z nią zmienną. Wygenerowana zostanie wówczas klasa umieszczona w osobnym module (w przypadku kontrolki Windows Media Player powstaną pliki player.h i player.cpp), zawierająca metody i własności tej kontrolki. Jeśli w wygenerowanym pliku nagłówkowym nie znalazły się definicje metod5, to należy postąpić następująco: 1. Klikamy pozycję menu Project, Add Class.... 2. W kreatorze wybieramy MFC Class From ActiveX Control (rysunek 8.24). Rysunek 8.24. Kreator Class Wizard dla kontrolki ActiveX
3. Z rozwijanej listy wybieramy Windows Media Player, a z listy dostępnych
interfejsów — IWMPPlayer (rysunek 8.25). Rysunek 8.25. Lista interfejsów kontrolki ActiveX Windows Media Player6
5
Dotyczy to w szczególności Windows XP.
6
Lista interfejsów może zależeć od zainstalowanej wersji Windows Media Player oraz od wersji systemu Windows.
292
Visual C++. Gotowe rozwiązania dla programistów Windows 4. Przechodzimy do edycji pliku CWMPPlayer.h i kopiujemy jego zawartość
od linii zawierającej komentarz // Operations do końca pliku. Skopiowane definicje metod wklejamy do pliku player.h.
Rozdział 9.
Sieci komputerowe Struktura sieci komputerowych Model OSI (ang. Open System Interconnection) jest abstrakcyjnym opisem struktury sieci komputerowych. Model ten opisuje wędrówkę danych, począwszy od aplikacji działającej na jednym komputerze, aż do aplikacji uruchomionej na drugim komputerze. Model OSI podzielony jest na siedem warstw, z których każda pełni oddzielną funkcję. Obecnie strukturę sieci komputerowych lepiej oddaje model TCP/IP (inna jego nazwa to model DOD). Model TCP/IP składa się z czterech warstw. Rysunek 9.1 przedstawia porównanie modelu OSI oraz modelu TCP/IP. Rysunek 9.1. Model OSI i model TCP/IP
294
Visual C++. Gotowe rozwiązania dla programistów Windows
Model TCP/IP składa się z następujących warstw: Warstwa aplikacji — łączy w sobie trzy górne warstwy z modelu OSI (aplikacji,
prezentacji, sesji). Warstwa ta jest najbliższa użytkownikowi — w niej pracują takie aplikacje jak przeglądarka WWW, klient FTP. Obejmuje ona szereg protokołów wysokiego poziomu, takich jak: HTPP, FTP, SMTP, POP3, SLL, DNS. Zarówno model OSI, jak i model TCP/IP zawierają warstwę aplikacji — nie należy ich jednak utożsamiać. Warstwa transportowa — zapewnia przepływ danych, a także kontroluje
dostarczenie danych do właściwego procesu. W tej warstwie wprowadza się pojęcie portu sieciowego. Każdy port jest identyfikowany przez podanie numeru portu, będącego liczbą naturalną z przedziału 0 – 65535. Podstawowe protokoły warstwy transportu to TCP i UDP. Oba protokoły zawierają w nagłówku numer portu nadawcy oraz numer portu odbiorcy. Każdy proces, odbierający lub wysyłający dane, rezerwuje port. Żaden inny proces nie może korzystać z określonego numeru portu, jeżeli zostanie on wcześniej zarezerwowany przez inny proces. Tak więc każdy numer portu jest unikatowy w ramach danego protokołu. Właśnie to sprawia, że jesteśmy w stanie określić protokół, do którego należy przekazać aktualnie przetwarzane dane. Warstwa sieciowa — zapewnia łączność między hostami, a także wybór
odpowiedniej drogi dla danych. Podstawowe protokoły tej warstwy to IP, ICMP, ARP, RARP. Warstwa dostępu do sieci — obejmuje warstwę łącza danych oraz fizyczną
z modelu OSI. Jej zadaniem jest przekazywanie danych przez fizyczne połączenia między faktycznymi urządzeniami. Przesyłane pakiety danych przed wysłaniem przekazywane są do niższych warstw sieci, przy czym na każdym poziomie uzupełniane są o dodatkowe dane. Proces ten nosi nazwę enkapsulacji (ang. encapsulation). W niniejszym rozdziale będziemy korzystali z Internet Protocol Helper (IP Helper), a także biblioteki gniazd WinSock. W przypadku IP Helper do projektu będziemy musieli dołączyć bibliotekę importową Iphlpapi.lib. Aby to zrobić, należy przejść do właściwości projektu, a następnie w drzewie opcji znaleźć Configuration Properties, Linker, Input i w polu Additional Dependencies wpisać Iphlpapi.lib1 (rysunek 9.2). Natomiast aby korzystać z biblioteki WinSock, możemy w czasie tworzenia projektu w kroku kreatora z nagłówkiem Advanced Features zaznaczyć pozycję Windows sockets (rysunek 9.3).
1
Dokładny opis dodawania biblioteki importowej do projektu znajduje się w rozdziale 7. „Biblioteki DLL”, w omówieniu projektu „Statyczne łączenie bibliotek DLL — import funkcji”.
Rozdział 9. ♦ Sieci komputerowe
295
Biblioteka Iphlpapi.lib znajduje się w Windows SDK. Jeżeli Visual Studio nie potrafi jej znaleźć podczas operacji łączenia plików, prawdopodobnie katalog z biblioteką nie jest uwzględniany przy poszukiwaniu. Należy wtedy przejść do właściwości projektu, a następnie w drzewie opcji Configuration Properties, Linker, General, w polu Additional Library Directories wybrać katalog, w którym znajduje się Iphlpapi.lib. Więcej informacji na temat dodawania kolejnych katalogów z bibliotekami importowymi można znaleźć w rozdziale 7. „Biblioteki DLL”, a także na stronie http://msdn. microsoft.com/en-us/library/1xhzskbe(VS.80).aspx. Rysunek 9.2. Dodawanie biblioteki importowej Iphlpapi.lib
Rysunek 9.3. Przy tworzeniu projektu możemy zaznaczyć, że chcemy używać biblioteki gniazd
296
Visual C++. Gotowe rozwiązania dla programistów Windows
Lista połączeń sieciowych i diagnoza sieci Aktywne połączenia TCP Protokół TCP/IP jest jednym z najbardziej popularnych protokołów warstwy transportowej. Jest to protokół połączeniowy, co oznacza, że przed wymianą danych nawiązywane jest połączenie między dwoma urządzeniami. Do nawiązania tego połączenia protokół TCP wykorzystuje metody three-way handshake. Nawiązanie połączenia pozwala m.in. zapewnić niezawodność transmisji danych. Wiarygodność transmitowanych danych jest potwierdzana przy wykorzystaniu sum kontrolnych oraz numerów sekwencyjnych pakietów, według których są porządkowane. Dzięki temu, jeżeli jakikolwiek pakiet zaginie w sieci, urządzenie docelowe zgłasza potrzebę jego retransmisji. W pierwszym projekcie skorzystamy z funkcji WinApi GetTcpTable2, aby odczytać aktualną tabelę połączeń bieżącego komputera z innymi komputerami w sieci. Nagłówek tej funkcji jest następujący: DWORD WINAPI GetTcpTable(PMIB_TCPTABLE pTcpTable,PDWORD pdwSize,BOOL bOrder);
Argument pTcpTable jest wskaźnikiem na strukturę MIB_TCPTABLE, która opisuje tablicę połączeń TCP. Argument pdwSize jest wskaźnikiem na zmienną określającą rozmiar tej struktury, a bOrder określa, w jakiej kolejności zostaną ułożone połączenia TCP w odczytanej tablicy. Nasuwa się zapewne pytanie, skąd mamy wiedzieć, jaki jest rozmiar struktury wskazywanej przez pTcpTable, skoro w danej chwili aktywna może być różna liczba połączeń? Właśnie ze względu na to w powyższej funkcji obecny jest parametr pdwSize. Pierwsze wywołanie funkcji GetTcpTable z pierwszym parametrem ustawionym na NULL spowoduje, że zmienna wskazywana drugim parametrem zostanie ustawiona na liczbę bajtów, jaką zajmuje tablica połączeń TCP, a funkcja GetTcpTable zwróci stałą ERROR_INSUFFICIENT_BUFFER. Dopiero wówczas możemy przydzielić odpowiednią ilość pamięci dla struktury i ponownie wywołać funkcję, która już tym razem zapisze do bufora potrzebne nam dane. Zobaczmy, jak wygląda implementacja tego mechanizmu w projekcie MFC. 1. Tworzymy nowy projekt MFC Application z oknem dialogowym. 2. Do projektu dodajemy kontrolkę List Control. 3. Jej własność View zmieniamy na Report. 4. Z kontrolką wiążemy zmienną o nazwie list1. 5. Deklarujemy i definiujemy nową metodę klasy okna dialogowego SprawdzPolaczeniaTCP (listing 9.1).
2
Dostępna od Windows 2000.
Rozdział 9. ♦ Sieci komputerowe
297
Listing 9.1. Sprawdzamy aktywne połączenia TCP. Definicja pochodzi z pliku .cpp zawierającego definicję klasy okna dialogowego. Funkcję trzeba zadeklarować w pliku nagłówkowym w klasie okna void CNetStatDlg::SprawdzPolaczeniaTCP() { MIB_TCPTABLE * tcpTable = NULL; unsigned long rozmiar = 0; in_addr ipAddr; CString lokalny,obcy,stan; wchar_t wchLokalny[128] = {'\0'}, wchObcy[128]={'\0'}; if(GetTcpTable(tcpTable,&rozmiar,true) == ERROR_INSUFFICIENT_BUFFER) { tcpTable = (MIB_TCPTABLE*)malloc(rozmiar); } if(tcpTable != NULL) { if(GetTcpTable(tcpTable,&rozmiar,true) == NO_ERROR) { for(int i=0;idwNumEntries;i++) { int row = list1.GetHeaderCtrl()->GetItemCount() - 1; int index = list1.InsertItem(row,L"TCP"); ipAddr.S_un.S_addr = tcpTable->table[i].dwLocalAddr; mbstowcs(wchLokalny,inet_ntoa(ipAddr),128); lokalny.AppendFormat(L"%s:%d",wchLokalny,tcpTable->table[i].dwLocalPort); list1.SetItem(index, 1, LVIF_TEXT,lokalny, 0, 0, 0, NULL); ipAddr.S_un.S_addr = tcpTable->table[i].dwRemoteAddr; mbstowcs(wchObcy,inet_ntoa(ipAddr),128); obcy.AppendFormat(L"%s:%d",wchObcy,tcpTable->table[i].dwRemotePort); list1.SetItem(index, 2, LVIF_TEXT,obcy ,0, 0, 0, NULL); switch (tcpTable->table[i].dwState) { case MIB_TCP_STATE_CLOSED: stan.Append(L"Zamknięty"); break; case MIB_TCP_STATE_LISTEN: stan.Append(L"Nasłuch"); break; case MIB_TCP_STATE_ESTAB: stan.Append(L"Ustanowiono"); break; case MIB_TCP_STATE_CLOSING: stan.Append(L"Zamykanie"); break; default: stan.Append(L"Inny stan"); break; } list1.SetItem(index, 3, LVIF_TEXT,stan ,0, 0, 0, NULL); lokalny.Empty(); obcy.Empty(); stan.Empty();
}
}
} } free(tcpTable);
298
Visual C++. Gotowe rozwiązania dla programistów Windows 1. Ustalamy kolumny listy, a także wywołujemy metodę SprawdzPolaczeniaTCP. W tym celu do metody OnInitDialog okna dialogowego dodajemy kod
z listingu 9.2. Listing 9.2. Wywołanie metody sprawdzającej aktywne połączenia TCP BOOL CNetStatDlg::OnInitDialog() { CDialog::OnInitDialog(); SetIcon(m_hIcon, TRUE); SetIcon(m_hIcon, FALSE);
// Set big icon // Set small icon
RECT r; list1.GetWindowRect(&r); int szerListy = r.right - r.left; int szerKolumny = szerListy / 4; list1.InsertColumn(0,L"Protokół",LVCFMT_LEFT, szerKolumny, 0); list1.InsertColumn(1,L"Adres lokalny",LVCFMT_LEFT, szerKolumny, 0); list1.InsertColumn(2,L"Adres obcy",LVCFMT_LEFT, szerKolumny, 0); list1.InsertColumn(3,L"Stan",LVCFMT_LEFT, szerKolumny, 0); SprawdzPolaczeniaTCP(); return TRUE; }
1. Do projektu dodajemy bibliotekę importową Iphlpapi.lib zawierającą IP Helper
(zob. komentarz nad rysunkiem 9.2). 8. Kompilujemy i uruchamiamy projekt. Efekt widoczny jest na rysunku 9.4. Rysunek 9.4. Aktywne połączenia TCP
Rozdział 9. ♦ Sieci komputerowe
299
Aby w systemie Windows uzyskać informacje o połączeniach sieciowych, można skorzystać z polecenia netstat.
Aktywne gniazda UDP Protokół UDP jest drugim co do ważności protokołem warstwy transportowej. W przeciwieństwie do TCP jest protokołem bezpołączeniowym, przez co pozbawiony jest mechanizmów weryfikacji wysyłanych i odbieranych danych. Jeżeli jakiekolwiek pakiety zagubią się w sieci, host docelowy nigdy się o tym nie dowie. Zaletą tego protokołu jest jednak to, że jego bezpołączeniowa natura zwiększa szybkość transmisji. Dzięki temu UDP wykorzystuje się wszędzie tam, gdzie liczy się szybkość transmisji, a straty pewnej części pakietów są do zaakceptowania i nie stanowią większego zagrożenia (wideokonferencje, gry sieciowe itp.). W kolejnym projekcie skorzystamy z funkcji GetUdpTable, aby odczytać listę aktualnych połączeń UDP. Wykorzystujemy ją w sposób prawie identyczny jak funkcję GetTcpTable. Funkcje te różni jedynie pierwszy parametr, którym tym razem jest wskaźnik na strukturę MIB_UDPTABLE. W tej strukturze nie ma informacji na temat połączeń UDP, gdyż UDP jest protokołem bezpołączeniowym. Możemy co najwyżej dowiedzieć się o gniazdach UDP zainstalowanych na określonych portach. 1. Otwieramy projekt utworzony w poprzednim przykładzie. 2. Deklarujemy nową metodę klasy okna dialogowego, o nazwie SprawdzGniazdaUDP. 3. Definiujemy metodę SprawdzGniazdaUDP, dodając kod z listingu 9.3. Listing 9.3. Sprawdzamy gniazda UDP, na których „nasłuchuje” komputer void CNetStatDlg::SprawdzGniazdaUDP() { MIB_UDPTABLE * udpTable = NULL; unsigned long rozmiar = 0; in_addr ipAddr; CString lokalny; wchar_t wchLokalny[128]; if(GetUdpTable(udpTable,&rozmiar,true) == ERROR_INSUFFICIENT_BUFFER) { udpTable = (MIB_UDPTABLE*)malloc(rozmiar); } if(udpTable != NULL) { if(GetUdpTable(udpTable,&rozmiar,true) == NO_ERROR) { for(int i=0;idwNumEntries;i++) { int row = list1.GetHeaderCtrl()->GetItemCount() - 1; int index = list1.InsertItem(row,L"UDP"); ipAddr.S_un.S_addr = udpTable->table[i].dwLocalAddr; mbstowcs(wchLokalny,inet_ntoa(ipAddr),128);
300
Visual C++. Gotowe rozwiązania dla programistów Windows lokalny.AppendFormat(L"%s:%d",wchLokalny,udpTable->table[i].dwLocalPort); list1.SetItem(index, 1, LVIF_TEXT,lokalny, 0, 0, 0, NULL); list1.SetItem(index, 2, LVIF_TEXT,L"*.*" ,0, 0, 0, NULL); lokalny.Empty(); } } free(udpTable); } }
1. Do metody OnInitDialog dodajemy wywołanie metody SprawdzGniazdaUDP. 5. Kompilujemy i uruchamiamy projekt. Powinniśmy zobaczyć okno podobne
do przedstawionego na rysunku 9.5 (oczywiście wylistowane połączenia są specyficzne dla każdego komputera i to w danym momencie). Rysunek 9.5. Aktywne połączenia TCP i aktywne gniazda UDP
Sprawdzanie konfiguracji interfejsów sieciowych Użytkownik systemu Windows ma do dyspozycji polecenie ipconfig, które wyświetla konfigurację TCP/IP danego interfejsu sieciowego. W tym projekcie spróbujemy osiągnąć podobną funkcjonalność, wykorzystując WinApi. Aby w systemie Windows odczytać konfigurację interfejsów sieciowych, należy użyć funkcji GetAdaptersInfo o nagłówku: DWORD GetAdaptersInfo(PIP_ADAPTER_INFO pAdapterInfo, PULONG pOutBufLen);
Pierwszy parametr to wskaźnik do struktury IP_ADAPTER_INFO. Drugi to wskaźnik na zmienną określającą rozmiar tej struktury w bajtach. Podobnie jak w przypadku funkcji pobierających listę połączeń: jeżeli rozmiar struktury jest zbyt mały, funkcja zwraca
Rozdział 9. ♦ Sieci komputerowe
301
stałą ERROR_BUFFER_OVERFLOW i zapisuje w zmiennej wskazywanej drugim parametrem wymagany rozmiar struktury. 1. Tworzymy nowy projekt MFC Application z oknem dialogowym. 2. Dodajemy pole edycyjne Edit Control, tak aby pokrywało całą powierzchnię
okna dialogowego. 3. Z nową kontrolką wiążemy zmienną o nazwie edit1. 4. Jej właściwości Multiline i Vertical Scroll ustawiamy na true. 5. Do projektu dołączamy plik nagłówkowy Iphlpapi.h, a także bibliotekę
importową Iphlpapi.lib. 6. Do metody OnInitDialog dodajemy kod z listingu 9.4. Listing 9.4. Sprawdzamy konfigurację interfejsów sieciowych IP_ADAPTER_INFO * pIpInfo = NULL; unsigned long rozmiar; CString opis; wchar_t addr[16] = {'\0'}; wchar_t nazwaAdaptera[256] = {'\0'}; if(GetAdaptersInfo(pIpInfo,&rozmiar) == ERROR_BUFFER_OVERFLOW) { pIpInfo = (IP_ADAPTER_INFO *)malloc(rozmiar); if(pIpInfo == NULL) return TRUE; if(GetAdaptersInfo(pIpInfo,&rozmiar) == NO_ERROR) { while(pIpInfo) { opis.Append(L"++++++++++++++++++++++++++++++\r\n"); memset(addr,0,16*sizeof(wchar_t)); mbstowcs(nazwaAdaptera,pIpInfo->AdapterName,256); opis.AppendFormat(L"Nazwa Adaptera: %s\r\n",nazwaAdaptera); opis.Append(L"Adres MAC: "); for (int i = 0; i < pIpInfo->AddressLength; i++) { if (i == (pIpInfo->AddressLength - 1)) opis.AppendFormat(L"%.2X", (int) pIpInfo->Address[i]); else opis.AppendFormat(L"%.2X-", (int) pIpInfo->Address[i]); } opis.Append(L"\r\n"); memset(addr,0,16*sizeof(wchar_t)); mbstowcs(addr,pIpInfo->IpAddressList.IpAddress.String,16); opis.AppendFormat(L"Adres IP: %s\r\n",addr); memset(addr,0,16*sizeof(wchar_t)); mbstowcs(addr,pIpInfo->IpAddressList.IpMask.String,16); opis.AppendFormat(L"Maska: %s\r\n",addr); memset(addr,0,16*sizeof(wchar_t)); mbstowcs(addr,pIpInfo->GatewayList.IpAddress.String,16); opis.AppendFormat(L"Brama: %s\r\n",addr); if(pIpInfo->DhcpEnabled) { opis.Append(L"DHCP: TAK\r\n");
302
Visual C++. Gotowe rozwiązania dla programistów Windows memset(addr,0,16*sizeof(wchar_t)); mbstowcs(addr,pIpInfo->DhcpServer.IpAddress.String,16); opis.AppendFormat(L"Adres DHCP: %s\r\n",addr); } else opis.Append(L"DHCP: NIE\r\n"); opis.Append(L"\r\n\r\n"); edit1.SetWindowTextW(opis); pIpInfo = pIpInfo->Next; } } free(pIpInfo); }
7. Kompilujemy i uruchamiamy projekt. Powinniśmy zobaczyć okna z ustawieniami
poszczególnych interfejsów sieciowych, podobnie jak na rysunku 9.6. Rysunek 9.6. Konfiguracja interfejsów sieciowych
Informacje o interfejsach sieciowych możemy także odczytać z rejestru systemowego, przeszukując klucz HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Services \\Tcpip\\Parameters\\Interfaces.
Ping Ping jest programem używanym w sieciach komputerowych do diagnozowania połączenia sieciowego. Obecny jest zarówno w systemie Windows, jak i we wszystkich edycjach Linuksa. Wykorzystuje bezpołączeniowy protokół ICMP działający (zdefiniowany) w warstwie sieciowej. Protokół ten wysyła zapytania typu Echo Request, a w odpowiedzi otrzymuje odpowiedź Echo Response. Polecenie ping możemy także wykorzystać do sprawdzenia odległości między hostami (liczonej w ilości przeskoków pomiędzy routerami).
Rozdział 9. ♦ Sieci komputerowe
303
W projekcie zrealizujemy funkcjonalność bardzo podobną do programu ping. Skorzystamy w tym celu z funkcji IcmpCreateFile, która otwiera uchwyt służący do wysłania Echo Request. Natomiast do wysłania zapytania Echo Request i odbioru odpowiedzi Echo Response służy funkcja IcmpSendEcho, której nagłówek jest następujący: DWORD IcmpSendEcho(HANDLE IcmpHandle, IPAddr DestinationAddress, LPVOID ´RequestData, WORD RequestSize, PIP_OPTION_INFORMATION RequestOptions, LPVOID ´ReplyBuffer, DWORD ReplySize, DWORD Timeout);
Parametry tej funkcji to: uchwyt uzyskany wywołaniem IcmpCreateFile; adres hosta docelowego; wskaźnik bufora z danymi, które zostaną wysłane w zapytaniu; rozmiar wysyłanych danych (w bajtach); pakiet IP z ustawionym parametrem TLL3; adres bufora, w którym zostanie zapisana odpowiedź Echo Response
(jej zawartość powinna być identyczna jak dane wysłane do adresata); rozmiar bufora na odebrane dane (w bajtach); limit czasu oczekiwania na odpowiedź.
Funkcja IcmpCloseHandle zamyka uchwyt uzyskany wywołaniem funkcji IcmpCreateFile. 1. Tworzymy nowy projekt MFC Application z oknem dialogowym. 2. Do projektu dodajemy pliki MojPing.h i MojPing.cpp. 3. Do pliku nagłówkowego dodajemy kod z listingu 9.5. Listing 9.5. Deklaracja klasy ze statyczną metodą sprawdzającą połączenie #include "stdafx.h" #include "Icmpapi.h" class PingClass { public: static void SprawdzPing(CEdit & edit,char * addr); };
4. Do pliku z rozszerzeniem .cpp dodajemy definicję statycznej metody SprawdzPing (listing 9.6).
3
TTL (ang. Time to Live) określa czas „życia” pakietu. Parametr ten nie zawsze reprezentuje czas. Przeważnie reprezentuje on liczbę przeskoków, które może wykonać pakiet na swojej drodze. Każdy router zmniejsza wartość tego parametru o 1. Router odrzuca pakiety z TTL ustawionym na 1.
304
Visual C++. Gotowe rozwiązania dla programistów Windows
Listing 9.6. Sprawdzamy połączenie protokołem Echo #include "MojPing.h" void PingClass::SprawdzPing(CEdit & edit,char * addr) { unsigned long ipaddr = INADDR_NONE; HANDLE hIcmp = IcmpCreateFile(); if (hIcmp == INVALID_HANDLE_VALUE) { return ; } char request[32] = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; int replySize = sizeof(ICMP_ECHO_REPLY) + sizeof(request); LPVOID reply = (LPVOID)malloc(replySize); int ret = IcmpSendEcho(hIcmp,inet_addr(addr),request,sizeof(request), ´NULL,reply,replySize,1000); if(ret != 0) { PICMP_ECHO_REPLY pEchoReply = (PICMP_ECHO_REPLY)reply; in_addr replyAddr; replyAddr.s_addr = pEchoReply->Address; char * szAddr = inet_ntoa( replyAddr ); wchar_t szAddrWide[16] = {'\0'}; mbstowcs(szAddrWide,szAddr,16); CString strReq; strReq.AppendFormat(L"Odebrano od %s; Status %ld; Czas %ld ms;",szAddrWide, ´pEchoReply->Status, pEchoReply->RoundTripTime); edit.SetWindowTextW(strReq); }
}
1. Moduł MojPing.h/.cpp dołączamy do pliku okna zawierającego klasę okna
dialogowego za pomocą dyrektywy: #include "MojPing.h"
6. Tworzymy interfejs użytkownika, dodając kontrolki: IP Address Control, Edit
Control, dwa razy Static Text oraz Button zgodnie ze wzorem na rysunku 9.7. Rysunek 9.7. Interfejs programu Ping
Rozdział 9. ♦ Sieci komputerowe
305
7. Właściwość Multiline pola edycyjnego ustawiamy na true. 8. Dodajemy zmienne; proponuję: edit1 dla pola edycyjnego oraz ipAddr dla
kontrolki adresu. 9. Dwukrotnie klikamy przycisk Pinguj i do nowo utworzonej metody dodajemy
kod z listingu 9.7. Listing 9.7. Ping wchar_t ip[16] ; ipAddr.GetWindowTextW(ip,15); char chIp[16] = {'\0'}; wcstombs(chIp,ip,15); PingClass::SprawdzPing(edit1,chIp);
10. Dodajemy bibliotekę importową Iphlpapi.lib (zob. komentarz nad rysunkiem 9.2). 11. Budujemy i uruchamiamy projekt. Efekt widoczny jest na rysunku 9.7. Aby sprawdzić trasę pakietów w sieci IP, możemy skorzystać z polecenia tracert.
Sprawdzanie adresu IP hosta (funkcja DnsQuery) DNS (ang. Domain Name System) to hierarchiczny system serwerów, a także protokół komunikacyjny, który zamienia nazwy przyjazne człowiekowi (mnemoniczne), takie jak helion.pl, na adresy IP (dla helion.pl to 213.186.88.113). Aby sprawdzić adres IP hosta, należy wysłać do serwera DNS zapytanie. Służy do tego funkcja DnsQuery. Jej nagłówek jest następujący: DNS_STATUS WINAPI DnsQuery(PCWSTR lpstrName, WORD wType, DWORD Options, PVOID ´pExtra, PDNS_RECORD *ppQueryResultsSet, PVOID *pReserved);
Parametry tej funkcji to: nazwa hosta, stała reprezentująca typ rekordu DNS, mapa bitowa reprezentująca ustawienia zapytania DNS, trzeci parametr jest zarezerwowany i musi być równy NULL, wskaźnik na wskaźnik na strukturę DNS_RECORD, opisującą rekord DNS.
Aby zwolnić pamięć przydzieloną dla rekordów DNS, wywołujemy funkcję DnsFree. Pierwszy parametr tej funkcji to wskaźnik na dane, które chcemy zwolnić. Drugi określa typ danych wskazywanych przez pierwszy parametr. 1. Tworzymy nowy projekt MFC Application z oknem dialogowym. 2. Interfejs użytkownika budujemy zgodnie z rysunkiem 9.8. Z kontrolkami wiążemy zmienne; proponuję: addrDNS oraz addrHosta dla komponentów IP Address Control i editNazwaHosta dla pola edycyjnego.
306
Visual C++. Gotowe rozwiązania dla programistów Windows
Rysunek 9.8. Wysyłamy zapytanie DNS
3. Właściwość Disabled komponentu editNazwaHosta ustawiamy na true. 4. Do projektu dodajemy pliki DNS.h i DNS.cpp. 5. Do nowego pliku nagłówkowego dodajemy kod z listingu 9.8. Listing 9.8. Deklaracja funkcji sprawdzającej adres IP hosta #include"stdafx.h" #include CString SprawdzNazwe(CString nazwaHosta, char * adresServeraDNS);
6. W pliku .cpp dopisujemy kod z listingu 9.9. Listing 9.9. Funkcja SprawdzNazwe #include "DNS.h" CString SprawdzNazwe(CString nazwaHosta, char * adresServeraDNS) { PDNS_RECORD pDnsRecord; IN_ADDR ipaddr; CString wynik; wynik.Empty(); PIP4_ARRAY pSrvList = (PIP4_ARRAY) LocalAlloc(LPTR,sizeof(IP4_ARRAY)); pSrvList->AddrCount = 1; pSrvList->AddrArray[0] = inet_addr(adresServeraDNS); int odp = DnsQuery(nazwaHosta,DNS_TYPE_A,DNS_QUERY_STANDARD,pSrvList,&pDnsRecord,NULL); if(odp) { MessageBox(0,L"Błąd funkcji DnsQuery",L"",MB_OK); } else { ipaddr.S_un.S_addr = (pDnsRecord->Data.A.IpAddress); char * chAddr = inet_ntoa(ipaddr); int dlugosc = strlen(chAddr); wchar_t wchAddr[16] = {'\0'}; mbstowcs(wchAddr,chAddr,dlugosc); wynik.Append(wchAddr); DnsRecordListFree(pDnsRecord, DnsFreeRecordListDeep);
Rozdział 9. ♦ Sieci komputerowe
}
307
} LocalFree(pSrvList); return wynik;
7. Plik DNS.h „doklejamy” do pliku nagłówkowego okna dialogowego. #include "DNS.h"
8. Dwukrotnie klikamy przycisk z etykietą Sprawdź i do nowo utworzonej metody
dodajemy kod z listingu 9.10. Listing 9.10. Wywołujemy funkcję sprawdzającą adres IP hosta void CProt_DNSDlg::OnBnClickedButton1() { CString host; editNazwaHosta.GetWindowTextW(host); CString adresSerweraDNS; addrDNS.GetWindowTextW(adresSerweraDNS); int dlugosc = adresSerweraDNS.GetLength(); char * chAdresSerweraDNS = new char[dlugosc+1]; chAdresSerweraDNS = (char *)SecureZeroMemory(chAdresSerweraDNS,dlugosc+1); wcstombs(chAdresSerweraDNS,adresSerweraDNS,dlugosc); addrHosta.SetWindowTextW(SprawdzNazwe(host,chAdresSerweraDNS)); }
1. Do projektu dodajemy bibliotekę importową Dnsapi.lib (wymagają jej funkcje DnsQuery i DnsFree). 10. Budujemy i uruchamiamy projekt. Efekt powinien być podobny do widocznego
na rysunku 9.8. Aby sprawdzić szczegółowe informacje odnoszące się do serwerów DNS, w Windows możemy skorzystać z polecenia nslookup.
Sprawdzanie adresu IP i nazwy hosta (funkcje gethostbyaddr i gethostbyname) W systemach z rodziny Windows za obsługę sieci odpowiedzialna jest biblioteka gniazd — Winsock. Obecnie występują dwie wersje tej biblioteki — starsza, bazująca na modelu biblioteki gniazd według standardu Berkeley, oraz nowsza, czyli tzw. Winsock 24. Nowa wersja biblioteki gniazd jest zgodna wstecz, co oznacza, że aplikacje korzystające z Winsock 2 mogą wciąż korzystać z funkcji z pierwszej wersji tej biblioteki. Jednak Winsock 2 zawiera także nowe funkcje. Funkcje te wyróżnia wspólny przedrostek WSA.. (np. WSARecv, WSASocket). Aby wczytać bibliotekę gniazd, należy wywołać funkcję WSAStartup. Jej nagłówek jest następujący: int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData); 4
Biblioteka dostępna od Windows 98.
308
Visual C++. Gotowe rozwiązania dla programistów Windows
Pierwszy parametr określa wersję biblioteki. Jest to wartość typu WORD (16 bitów), gdzie młodszy bajt określa numer główny biblioteki, a starszy bajt — numer poboczny. W celu utworzenia tego parametru można skorzystać z makra MAKEWORD, którego pierwszym parametrem jest młodszy bajt, a drugim — starszy, np. MAKEWORD(2,0). Biblioteka gniazd jest usuwana z pamięci poprzez wywołanie bezparametrowej funkcji WSACleanup. Aby aplikacja MFC mogła korzystać z funkcji biblioteki gniazd, należy w kreatorze tworzenia aplikacji (krok z nagłówkiem Advanced Features) zaznaczyć opcję Windows sockets (rysunek 9.3). W poprzednim przykładzie pokazano, jak za pomocą funkcji DnsQuery można zamienić nazwę hosta na jego adres IP. Do tego celu możemy także użyć funkcji gethostbyname znajdującej się w bibliotece gniazd Winsock. Przed wykorzystaniem funkcji gethostbyname musimy wczytać bibliotekę gniazd Ws2_32.dll do pamięci. Sygnatura funkcji gethostbyname jest następująca: struct hostent* FAR gethostbyname(const char *name);
Parametr funkcji to nazwa hosta. Funkcja zwraca wskaźnik na strukturę hostent, której definicja jest następująca: typedef struct hostent { char FAR *h_name; char FAR FAR **h_aliases; short h_addrtype; short h_length; char FAR FAR **h_addr_list; } HOSTENT, *PHOSTENT, FAR *LPHOSTENT;
Warto zwrócić szczególną uwagę na parametr h_addr_list — jest to lista adresów badanego hosta. Zmiana adresu IP hosta na jego przyjazną nazwę DNS możliwa jest za pomocą funkcji gethostbyaddr. Jej sygnatura jest następująca: struct hostent* FAR gethostbyaddr(const char *addr, int len, int type);
Pierwszy parametr tej funkcji to wskaźnik do bufora, który zawiera adres IP hosta. Drugi parametr to wielkość bufora wyrażona w bajtach. Ostatni parametr to typ adresu. Podobnie jak w przypadku gethostbyname, tak i przed wykorzystaniem funkcji gethostbyname należy wczytać bibliotekę gniazd. Aby przekonwertować adres IP na postać „kropkowaną” (tzn. do formatu, gdzie każdy z czterech oktetów wyrażonych w systemie dziesiętnym jest oddzielony kropką, np. 10.0.0.5), możemy użyć funkcji inet_ntoa. Funkcja ta przyjmuje jeden parametr — strukturę in_addr opisującą adres hosta, a zwraca łańcuch znaków będący adresem IP w formacie „kropkowanym”.
Rozdział 9. ♦ Sieci komputerowe
309
1. Tworzymy nowy projekt MFC Application z oknem dialogowym. 2. Do projektu dodajemy pliki Konwersja.h i Konwersja.cpp. 3. W pliku nagłówkowym umieszczamy definicję klasy, której zadaniem będzie
odczytywanie adresu IP na podstawie podanej nazwy hosta i vice versa. Dodajemy do niego kod z listingu 9.11. Listing 9.11. Deklaracja klasy CKonwersja #include "stdafx.h" class CKonwersja { public: static CString IPToName(CString ip); static CString NameToIP(CString name); };
1. W pliku .cpp umieszczamy natomiast definicję klasy CKonwersja (listing 9.12). Listing 9.12. Definicja klasy CKonwersja #include"Konwersja.h" CString CKonwersja::IPToName(CString ip) { CString wynik; struct in_addr addr; WSADATA wsaData; WORD wersja = MAKEWORD( 2, 0 ); int blad = WSAStartup(wersja, &wsaData ); if(blad != 0) { MessageBox(0,L"Błąd wczytywania biblioteki gniazda",L"",MB_OK); } char szAddr[16] = {'\0'}; wcstombs(szAddr,ip,15); addr.s_addr = inet_addr(szAddr); hostent* pInfoKomputer=gethostbyaddr((char *) &addr, 4, AF_INET); wchar_t wName[128] = {'\0'}; mbstowcs(wName,pInfoKomputer->h_name,127); wynik.AppendFormat(L"%s", wName); WSACleanup(); }
return wynik;
CString CKonwersja::NameToIP(CString name) { CString wynik; struct in_addr addr;
310
Visual C++. Gotowe rozwiązania dla programistów Windows WSADATA wsaData; WORD wersja = MAKEWORD( 2, 0 ); int blad = WSAStartup(wersja, &wsaData ); if(blad != 0) { MessageBox(0,L"Błąd wczytywania biblioteki gniazda",L"",MB_OK); } char szName[128] = {'\0'}; wcstombs(szName,name,127); hostent* pInfoKomputer = gethostbyname(szName); addr.s_addr = *(u_long *) pInfoKomputer->h_addr_list[0]; char * adres = inet_ntoa(addr); wchar_t wAdres[16] = {'\0'}; mbstowcs(wAdres,adres,1024); wynik.AppendFormat(L"%s",wAdres); WSACleanup(); return wynik; }
5. W pliku .cpp zawierającym klasę okna dialogowego dołączamy nagłówek
Konwersja.h: #include "Konwersja.h"
6. Do okna dialogowego dodajemy dwa komponenty Edit Control, dwa komponenty
IP Address Control, dwa komponenty Static Text oraz przycisk (rysunek 9.9). Rysunek 9.9. Sprawdzamy adres i nazwę hosta
7. Komponenty po lewej są wykorzystywane, gdy znamy adres IP hosta,
a chcemy poznać jego nazwę DNS. Wiążemy z nimi zmienne: dla pola edycyjnego — docNazwa, dla komponentu IP Address Control — zrodIP. 8. Komponenty po prawej są wykorzystywane, gdy znamy nazwę DNS hosta,
a chcemy poznać jego adres IP. Dodajemy zmienne dla tych komponentów: dla okna edycyjnego — zrodNazwa, natomiast dla IP Address Control — zmienną o nazwie docIP.
Rozdział 9. ♦ Sieci komputerowe
311
9. Dwukrotnie klikamy przycisk z etykietą Sprawdź i do nowo utworzonej metody
zdarzeniowej dodajemy kod z listingu 9.13. Listing 9.13. Sprawdzamy informacje o hoście void CIP_DNSDlg::OnBnClickedButton1() { CString szIP; zrodIP.GetWindowTextW(szIP); docNazwa.SetWindowTextW(CKonwersja::IPToName(szIP)); CString szName; zrodNazwa.GetWindowTextW(szName); docIP.SetWindowTextW(CKonwersja::NameToIP(szName)); }
10. Dodajemy także wstępne inicjalizacje komponentów. W tym celu do metody OnInitDialog (klasa okna dialogowego) dodajemy kod z listingu 9.14. Listing 9.14. Ustalamy wstępne wartości komponentów zrodIP.SetWindowTextW(L"127.0.0.1"); zrodNazwa.SetWindowTextW(L"localhost");
11. Budujemy i uruchamiamy projekt. Rysunek 9.10 prezentuje przykładową
konwersję adresu IP na nazwę DNS i odwrotnie. Rysunek 9.10. Sprawdzamy adres i nazwę hosta
Odczytywanie adresów MAC z tablicy ARP Protokół ARP jest protokołem komunikacyjnym, którego zadaniem jest znajdowanie tzw. fizycznego adresu hosta (adres MAC dla sieci Ethernet) na podstawie adresu warstwy sieciowej (w szczególności adresu IP). ARP może być stosowany dla różnych typów sieci i nie jest ograniczony tylko do sieci Ethernet. Każdy system operacyjny zarządza tzw. tablicą ARP przechowującą pary: adres IP i adres sprzętowy (MAC). W systemach z rodziny Windows tablicę tę można sprawdzić, korzystając z komendy arp -a.
312
Visual C++. Gotowe rozwiązania dla programistów Windows
W kolejnym projekcie spróbujemy napisać aplikację odczytującą wpisy w tablicy ARP. W tym celu wykorzystamy funkcję GetIpNetTable. 1. Tworzymy nowy projekt MFC Application z oknem dialogowym. 2. Do okna dialogowego dodajemy komponent Edit Control. 3. Właściwość Multiline komponentu ustawiamy na true. 4. Dodajemy także zmienną dla tego komponentu; proponuję edit1. 5. Do projektu dodajemy nowe pliki arp.h i arp.cpp. 6. Do pliku arp.h dodajemy deklarację klasy z listingu 9.15. Listing 9.15. Deklaracja klasy odczytującej wpisy w tablicy ARP #include "stdafx.h" class ArpOperations { public: static void ReadArpTable(CEdit & edit); private: static wchar_t * Type(int type); };
7. Do pliku arp.cpp dodajemy definicję metod ReadArpTable i Type (listing 9.16). Listing 9.16. Metody ReadArpTable i Type #include "arp.h" void ArpOperations::ReadArpTable(CEdit & edit) { MIB_IPNETTABLE * ipNetTable = NULL; ULONG pdwSize = 0; CString text; GetIpNetTable(ipNetTable,&pdwSize,true); ipNetTable = (MIB_IPNETTABLE*)malloc(pdwSize); if(GetIpNetTable(ipNetTable,&pdwSize,true) == NO_ERROR) { for(int i=0;idwNumEntries;i++) { wchar_t * typ = ArpOperations::Type(ipNetTable->table[i].dwType); CString macStr; macStr.AppendFormat(L"%02x-%02x-%02x-%02x-%02x-%02x", ipNetTable->table[i].bPhysAddr[0], ipNetTable->table[i].bPhysAddr[1], ipNetTable->table[i].bPhysAddr[2], ipNetTable->table[i].bPhysAddr[3], ipNetTable->table[i].bPhysAddr[4], ipNetTable->table[i].bPhysAddr[5]); in_addr addr; addr.s_addr = ipNetTable->table[i].dwAddr; char * szAddr = inet_ntoa(addr);
Rozdział 9. ♦ Sieci komputerowe
313
wchar_t szAddrWide[16] = {'\0'}; mbstowcs(szAddrWide,szAddr,16); text.AppendFormat(L"Adres IP:%s\r\nAdres ´fizyczny:%s\r\nTyp:%s\r\n",szAddrWide,macStr,typ); edit.SetWindowTextW(text); } } if(ipNetTable != NULL) { free(ipNetTable); }
} wchar_t * ArpOperations::Type(int type) { switch(type) { case 4: return L"statyczny"; case 3: return L"dynamiczny"; case 2: return L"niepoprawny"; default: return L"inny"; } }
1. Do projektu włączamy plik arp.h. #include "arp.h"
9. Dodajemy bibliotekę importową Iphlpapi.lib (zob. komentarz nad rysunkiem 9.2). 10. W metodzie OnInitDialog okna dialogowego wywołujemy statyczną metodę ReadArpTable. ArpOperations::ReadArpTable(edit1);
11. Budujemy i uruchamiamy projekt. Efekt widoczny jest na rysunku 9.11. Rysunek 9.11. Wpisy w tablicy ARP
314
Visual C++. Gotowe rozwiązania dla programistów Windows
Napisany program prezentuje wpisy w tablicy ARP dla wszystkich interfejsów sieciowych w komputerze. Obecnie komputery posiadają często więcej niż jedną kartę sieciową, tak więc należy sprawdzić, które wpisy odnoszą się do jakiego interfejsu sieciowego. Wykonamy to w następnym projekcie.
Tablica ARP — wiązanie wpisów z interfejsem Aby uzupełnić informację o rozdysponowaniu wpisów ARP, należy poinformować użytkownika o adresach IP poszczególnych interfejsów. Struktura MIB_IPNETTABLE będąca parametrem użytej przez nas funkcji GetIpNetTable zawiera tylko indeks interfejsu, który przecież użytkownikowi nic nie może mówić. Tak więc aby odczytać adresy IP zainstalowanych interfejsów, skorzystamy z funkcji GetIpAddrTable. Wpisy w tablicy ARP powiążemy z interfejsem poprzez indeks interfejsu, który jesteśmy w stanie odczytać zarówno za pomocą GetIpNetTable, jak i GetIpAddrTable. 1. Otwieramy projekt utworzony w poprzednim przykładzie. 2. Do klasy ArpOperationions dodajemy metodę ReadArpTable2, której definicję
przedstawia listing 9.17. Listing 9.17. Tym razem określamy interfejs dla każdego wpisu w tablicy ARP void ArpOperations::ReadArpTable2(CEdit & edit) { MIB_IPNETTABLE * ipNetTable = NULL; ULONG pdwSize = 0; CString text; GetIpNetTable(ipNetTable,&pdwSize,true); ipNetTable = (MIB_IPNETTABLE*)malloc(pdwSize); if(GetIpNetTable(ipNetTable,&pdwSize,true) == NO_ERROR) { MIB_IPADDRTABLE * ipAddrTable = NULL; ULONG pdwSizeAddr = 0; GetIpAddrTable(ipAddrTable,&pdwSizeAddr,true); ipAddrTable = (MIB_IPADDRTABLE *)malloc(pdwSizeAddr); if(GetIpAddrTable(ipAddrTable,&pdwSizeAddr,true) == NO_ERROR) { for(int j=0;jdwNumEntries;j++) { for(int i=0;idwNumEntries;i++) { if(ipAddrTable->table[j].dwIndex == ipNetTable->table[i].dwIndex) { in_addr addr_i; addr_i.s_addr = ipAddrTable->table[j].dwAddr; char * szAddr_i = inet_ntoa(addr_i); wchar_t szAddrWide_i[16] = {'\0'}; mbstowcs(szAddrWide_i,szAddr_i,16); text.AppendFormat(L"Interfejs:%s --´%d\r\n",szAddrWide_i,ipAddrTable->table[j].dwIndex);
Rozdział 9. ♦ Sieci komputerowe
315
wchar_t * typ = ArpOperations::Type(ipNetTable->table[i].dwType); CString macStr; macStr.AppendFormat(L"%02x-%02x-%02x-%02x-%02x-%02x", ipNetTable->table[i].bPhysAddr[0], ipNetTable->table[i].bPhysAddr[1], ipNetTable->table[i].bPhysAddr[2], ipNetTable->table[i].bPhysAddr[3], ipNetTable->table[i].bPhysAddr[4], ipNetTable->table[i].bPhysAddr[5]); in_addr addr; addr.s_addr = ipNetTable->table[i].dwAddr; char * szAddr = inet_ntoa(addr); wchar_t szAddrWide[16] = {'\0'}; mbstowcs(szAddrWide,szAddr,16); text.AppendFormat(L"Adres IP:%s\r\nAdres ´fizyczny:%s\r\nTyp:%s\r\n",szAddrWide,macStr,typ); edit.SetWindowTextW(text); } } } } free(ipAddrTable); } if(ipNetTable != NULL) { free(ipNetTable); } }
3. W OnInitDialog wywołujemy nowo utworzoną metodę: ArpOperations::ReadArpTable2(edit1);
4. Budujemy i uruchamiamy projekt. Przykładowe wpisy w tablicy ARP są
widoczne na rysunku 9.12. Rysunek 9.12. Przykładowe wpisy w tablicy ARP
316
Visual C++. Gotowe rozwiązania dla programistów Windows
Protokoły TCP i UDP Tworzenie i zamykanie gniazda — klasa bazowa Gniazdo (ang. socket) jest abstrakcyjnym, dwukierunkowym punktem końcowym procesu komunikacji. Dwukierunkowość oznacza możliwość odbioru i wysyłania danych. W celu utworzenia gniazda możemy skorzystać albo z funkcji socket z pierwszej wersji biblioteki gniazd, albo z WSASocket z Winsock 2. Obie funkcje zwracają nowo utworzone gniazdo, lecz różnią się przyjmowanymi parametrami. Nagłówek funkcji socket jest następujący: SOCKET socket(int af, int type, int protocol);
Pierwszy parametr określa rodzinę protokołów, drugi typ tworzonego gniazda, ostatni — typ protokołu obsługiwanego przez gniazdo. W celu zamknięcie transmisji danych za pośrednictwem gniazda używa się funkcji shutdown, która przyjmuje dwa parametry: deskryptor gniazda, a także rodzaj operacji, które zostaną zakończone (wysyłanie danych, odbieranie danych lub jedno i drugie). Gdy już nie potrzebujemy gniazda, możemy je zamknąć, wywołując funkcję closesocket. Jej jedyny parametr to deskryptor gniazda. W tym projekcie utworzymy klasę bazową, którą w kolejnych podrozdziałach rozszerzymy o funkcje serwera i klienta TCP/UDP. 1. Tworzymy nowy projekt MFC Application z oknem dialogowym. Pamiętajmy,
aby w kreatorze projektu zaznaczyć opcję Windows sockets (rysunek 9.3). 2. Do projektu dodajemy pliki Socket.h i Socket.cpp. 3. Do pliku nagłówkowego Socket.h dodajemy deklarację klasy bazowej CSocketBase (listing 9.18). Listing 9.18. Bazowa klasy obsługująca gniazda #include "stdafx.h" class CSocketBase { public: void ZainicjujWinsock(const int typ); void ZamknijGniazdo() ; protected : SOCKET mojSocket; };
4. Definicję metod klasy CSocketBase dodajemy do pliku Socket.cpp (listing 9.19). Listing 9.19. Tworzymy i zamykamy gniazdo #include "Socket.h" void CSocketBase::ZainicjujWinsock(const int typ) { WSADATA wsaData;
Rozdział 9. ♦ Sieci komputerowe
317
WORD wersja = MAKEWORD( 2, 0 ); int blad = WSAStartup(wersja, &wsaData ); if(blad != 0) { MessageBox(0,L"Błąd wczytywania biblioteki gniazda",L"",MB_OK); return ; } if((mojSocket = socket( AF_INET, typ, IPPROTO_IP )) == SOCKET_ERROR) { MessageBox(0,L"Błąd tworzenia gniazda",L"",MB_OK); return ; } } void CSocketBase::ZamknijGniazdo() { shutdown(mojSocket,SD_BOTH); closesocket(mojSocket); }
5. Kompilujemy projekt.
Klasa implementująca serwer TCP W modelu klient-serwer serwer jest urządzeniem udostępniającym usługę, podczas gdy klient może wysyłać żądanie w celu uzyskania dostępu do tej usługi. Przykładem serwera może być serwer WWW, który na żądanie klienta odsyła kod HTML strony internetowej. Przeglądarka internetowa pełni rolę klienta. W obecnych czasach granica pomiędzy serwerem a klientem jest często zatarta — urządzenia czy aplikacje pełnią często funkcje zarówno serwera, jak i klienta lub wybór roli zależy np. od kolejności uruchomienia kilku instancji aplikacji na różnych komputerach. Jednakże w projektach opisanych w tej książce podział na funkcje klienta i serwera będzie zachowany, aby ułatwić naukę. Jak wspomniałem wcześniej, protokół TCP jest protokołem połączeniowym, co oznacza, że przed wymianą danych należy uzyskać połączenie między hostami. Serwer jest aplikacją oczekującą na połączenie inicjowane przez klienta. Po utworzeniu gniazda aplikacja serwera musi skojarzyć nowo utworzone gniazdo z adresami, które będą uwzględniane przy oczekiwaniu na połączenie. Służy do tego funkcja bind. Jej sygnatura to: int bind(SOCKET s, const struct sockaddr *name, int namelen);
Pierwszy parametr to deskryptor gniazda. Drugi to wskaźnik na strukturę sockaddr opisującą adresy — jej konkretna struktura zależy od wybranego protokołu (w przypadku protokołu IPv4 należy korzystać z sockaddr_in). Ostatni parametr to rozmiar struktury wskazywanej drugim parametrem. Funkcja listen służy do nasłuchu na nadchodzące połączenia. Pierwszy jej parametr to deskryptor gniazda, a drugi to limit oczekujących połączeń.
318
Visual C++. Gotowe rozwiązania dla programistów Windows
Do akceptowania połączenia służy funkcja accept. Funkcja ta domyślnie działa w trybie blokującym, co oznacza, że w oczekiwaniu na połączenie będzie blokować wątek. Jej nagłówek jest następujący: SOCKET accept(SOCKET s, struct sockaddr *addr, int *addrlen);
Pierwszy parametr funkcji to deskryptor gniazda będącego w trybie nasłuchu. Drugi parametr to wskaźnik do struktury sockaddr. Po zakończeniu funkcji accept struktura ta będzie zawierać adres hosta, z którym zostało nawiązane połączenie. Ostatni parametr to rozmiar struktury. Funkcja zwraca deskryptor gniazda, który można wykorzystać do komunikacji z klientem. W celu odebrania danych od klienta można wykorzystać funkcję recv. Jej nagłówek jest następujący: int recv(SOCKET s, char *buf, int len, int flags);
Pierwszy parametr to deskryptor połączonego gniazda, drugi to wskaźnik na bufor, w którym zostaną umieszczone dane. Trzeci parametr jest długością bufora, a bity czwartego kodują opcje wpływające na działanie funkcji (flagi). Funkcja zwraca ilość odczytanych danych (w bajtach). Rozwiniemy teraz projekt z poprzedniego podrozdziału, przygotowując klasę, która będzie rozszerzała klasę CSocketBase napisaną w poprzednim projekcie poprzez dodanie do niej metody OdbierzDane. Parametry nowej metody to numer portu i referencja do klasy opisującej okno edycyjne, w którym wyświetlimy odebrane dane. 1. Otwieramy pliki Socket.h i Socket.cpp utworzone w poprzednim projekcie. 2. Do pliku nagłówkowego dodajemy deklarację klasy CSocketServerTCP, dziedziczącej po CSocketBase (listing 9.20). Listing 9.20. Klasa CSocketServerTCP class CSocketServerTCP : public CSocketBase { public: void OdbierzDane(const int port, CEdit & edit); };
3. W pliku Socket.cpp definiujemy metodę OdbierzDane, dodając kod z listingu 9.21. Listing 9.21. Odbieramy dane z sieci za pomocą protokołu TCP void CSocketServerTCP::OdbierzDane(const int port, CEdit & edit) { sockaddr_in sin; sockaddr_in klientAdr; int rozmiar = sizeof(klientAdr); char bufor[1024]; wchar_t buforWide[1024]; sin.sin_addr.s_addr = htonl(INADDR_ANY); sin.sin_family = AF_INET; sin.sin_port = htons(port);
Rozdział 9. ♦ Sieci komputerowe
319
if(bind(mojSocket,(const sockaddr*)&sin,sizeof(sin)) == SOCKET_ERROR) { MessageBox(0,L"Błąd funkcji bind",L"",MB_OK); return ; } listen(mojSocket,5); SOCKET klientSocket = accept(mojSocket,(sockaddr*)&klientAdr,&rozmiar); if(klientSocket == INVALID_SOCKET) { MessageBox(0,L"Niepoprawne gniazdo klienta",L"",MB_OK); return; } int n = recv(klientSocket,bufor,sizeof(bufor),0); mbstowcs(buforWide,bufor,n); }
edit.SetWindowTextW(buforWide);
Klasa implementująca serwer UDP Protokół UDP jest protokołem bezpołączeniowym. Z tego powodu kod klasy przygotowanej dla serwera UDP pozbawiony będzie wywołania funkcji listen i accept. Jednakże podobnie jak w przypadku serwera TCP, tak i teraz po utworzeniu gniazda musimy je powiązać z adresami, od których chcemy odebrać dane. Dane odbieramy, używając funkcji recvfrom. Nagłówek tej funkcji jest następujący: int recvfrom(SOCKET s, char *buf, int len, int flags, struct sockaddr *from, ´int *fromlen);
Cztery pierwsze parametry są takie same jak w przypadku recv. Parametr piąty jest opcjonalny i jest to wskaźnik na strukturę sockaddr, który po powrocie recvfrom będzie zawierać adres hosta, od którego odebrano dane. Szósty parametr jest także opcjonalny i wskazuje na ilość bajtów zajmowanych przez strukturę wskazywaną piątym parametrem. 1. Otwieramy pliki Socket.h i Socket.cpp utworzone w poprzednim projekcie. 2. Do pliku nagłówkowego dodajemy deklarację klasy CSocketServerUDP, dziedziczącej po CSocketBase (listing 9.22). Klasa ta, podobnie jak w przypadku serwera TCP, udostępnia tylko jedną nową metodę OdbierzDane (nie licząc oczywiście metod odziedziczonych z CSocketBase) służącą do odbioru danych
z sieci. Listing 9.22. Deklaracja klasy CSocketServerUDP w pliku nagłówkowym class CSocketServerUDP : public CSocketBase { public: void OdbierzDane(const int port, CEdit & edit); };
320
Visual C++. Gotowe rozwiązania dla programistów Windows 3. W pliku Socket.cpp definiujemy metodę OdbierzDane klasy CSocketServerUDP
(listing 9.23). Listing 9.23. Bezpołączeniowy model serwera void CSocketServerUDP::OdbierzDane(const int port, CEdit &edit) { sockaddr_in sin; sockaddr_in klientAdr; int rozmiar = sizeof(klientAdr); char bufor[1024]; wchar_t buforWide[1024]; sin.sin_addr.s_addr = htonl(INADDR_ANY); sin.sin_family = AF_INET; sin.sin_port = htons(port); if(bind(mojSocket,(const sockaddr*)&sin,sizeof(sin)) == SOCKET_ERROR) { MessageBox(0,L"Błąd funkcji bind",L"",MB_OK); return ; } int n = recvfrom(mojSocket,bufor,sizeof(bufor),0, ´(sockaddr*)&klientAdr,&rozmiar); mbstowcs(buforWide,bufor,n); edit.SetWindowTextW(buforWide); }
Aplikacja działająca jako serwer TCP i UDP Przygotujemy teraz nowy projekt aplikacji, która dzięki wykorzystaniu klas z modułu Socket.h/.cpp będzie działać jako serwer TCP lub UDP. 1. Tworzymy nowy projekt MFC Application z oknem dialogowym. 2. Do projektu dodajemy pliki Socket.h i Socket.cpp przygotowane i rozwijane
w trzech poprzednich projektach. 3. Plik Socket.h dołączamy do pliku nagłówkowego okna dialogowego dyrektywą: #include "Socket.h"
4. Do klasy okna dialogowego dodajemy dwa pola: private: CSocketServerTCP serverTCP; CSocketServerUDP serverUDP;
5. Do okna dialogowego dodajemy dwie rozwijane listy Combo Box, trzy
kontrolki Static Text, pole edycyjne Edit Control, a także przycisk Button. Kontrolki ustawiamy zgodnie ze wzorem na rysunku 9.13.
Rozdział 9. ♦ Sieci komputerowe
321
Rysunek 9.13. Przykładowy serwer TCP/UDP
6. Właściwość Sort komponentów Combo Box ustawiamy na false. 7. Właściwości Read Only i Multiline pola edycyjnego ustawiamy na true. 8. Z kontrolkami wiążemy zmienne: comboPort oraz comboTypGniazda dla rozwijanych list, edit1 dla pola edycyjnego oraz buttonStart dla przycisku. 9. W metodzie OnInitDialog klasy okna dialogowego dodajemy instrukcje
umieszczające w rozwijanych listach porty, z których serwer może korzystać, a do drugiej — typy gniazda: połączeniowy TCP i bezpołączeniowy UDP (listing 9.24). Listing 9.24. Ustawiamy numery portów oraz typy gniazda BOOL CSocketNativeServerDlg::OnInitDialog() { CDialog::OnInitDialog(); SetIcon(m_hIcon, TRUE); SetIcon(m_hIcon, FALSE);
// Set big icon // Set small icon
CString gniazdo; for(int i=1025;ih_addr))->s_addr; sin.sin_port = htons(port); connect(mojSocket,(const sockaddr*)&sin,sizeof(sin)); int n = send(mojSocket,dane,dlugosc,0); return n; }
324
Visual C++. Gotowe rozwiązania dla programistów Windows
Klasa implementująca klienta UDP W przypadku klienta UDP nie musimy nawiązywać połączenia z serwerem; nie mamy także pewności, że wysłane dane dotarły do adresata. Wykorzystujemy tu funkcję sendto, której nagłówek wygląda następująco: int sendto(SOCKET s, const char *buf, int len, int flags, const struct sockaddr ´*to, int tolen);
Pierwsze cztery parametry są takie same jak w przypadku funkcji send. Piąty parametr jest opcjonalny i jest wskaźnikiem do struktury, do której funkcja sendto zapisze adres serwera. Szósty parametr, także opcjonalny, to rozmiar struktury wskazywanej piątym parametrem. 1. Otwieramy pliki Socket.h i Socket.cpp utworzone w poprzednich projektach. 2. Do pliku nagłówkowego dodajemy deklarację klasy obsługującej wysyłanie
danych za pomocą protokołu UDP (listing 9.28). Listing 9.28. Deklaracja klasy CSocketClientUDP class CSocketClientUDP : public CSocketBase { public: int Wyslij(const char * serwer, const int port, const char * dane, int dlugosc); };
3. Do pliku z rozszerzeniem .cpp dodajemy definicję metody Wyslij (listing 9.29). Listing 9.29. Metoda Wyslij int CSocketClientUDP::Wyslij(const char *serwer, const int port, const char *dane, ´int dlugosc) { hostent *host; sockaddr_in sin; memset( &sin, 0, sizeof(sin)); host=gethostbyname(serwer); sin.sin_family = AF_INET; sin.sin_addr.s_addr = ((struct in_addr *)(host->h_addr))->s_addr; sin.sin_port = htons(port); int n = sendto(mojSocket,dane,dlugosc,0,(sockaddr*)&sin,sizeof(sin)); return n; }
Rozdział 9. ♦ Sieci komputerowe
325
Aplikacja działająca jako klient TCP i UDP Utworzymy teraz nowy projekt aplikacji pełniącej rolę klienta TCP i UDP. Wykorzystamy w niej moduł Socket.h/.cpp rozwijany w poprzednich projektach. 1. Tworzymy nowy projekt MFC Application z oknem dialogowym. 2. Do projektu dodajemy pliki Socket.h i Socket.cpp. 3. Plik Socket.h dołączamy do projektu #include "Socket.h"
4. Do klasy okna dialogowego dodajemy pola: private: CSocketClientTCP klientTCP; CSocketClientUDP klientUDP;
5. Do okna dialogowego dodajemy komponent IP Address Control, dwie rozwijane
listy Combo Box, trzy kontrolki Static Text, pole edycyjne Edit Control oraz przycisk Button (rysunek 9.14). Rysunek 9.14. Przykładowy klient UDP
6. Właściwość Sort komponentów Combo Box ustawiamy na false. 7. Właściwość Multiline pola edycyjnego ustawiamy na true. 8. Dodajemy zmienne związane z kontrolkami. Proponuję: comboPort i comboTypGniazda dla rozwijanych list Combo Box, addr dla komponentu IP Address Control oraz edit1 dla pola edycyjnego. 9. Do metody OnInitDialog dodajemy kod z listingu 9.30, konfigurujący rozwijane
listy, a konkretnie wypełniający je numerami portów oraz typami połączeń.
326
Visual C++. Gotowe rozwiązania dla programistów Windows
Listing 9.30. Przyjmujemy numery portów 1025 – 4999 BOOL CSocketNativeClientDlg::OnInitDialog() { CDialog::OnInitDialog(); SetIcon(m_hIcon, TRUE); SetIcon(m_hIcon, FALSE); CString gniazdo; for(int i=1025;iGetSafeHwnd()); }
1. Kompilujemy i uruchamiamy projekt. Rysunek 9.15 prezentuje interfejs
użytkownika aplikacji. Rysunek 9.15. Przykładowy klient UDP
Po kliknięciu przycisku Start Asynchroniczny TCP rozpoczynamy nasłuch na określonym porcie (nie musimy określać typu gniazda, gdyż zawsze instalowane jest gniazdo TCP). Serwer działa w trybie asynchronicznym, dzięki czemu interfejs użytkownika nie jest blokowany. Gdy zostanie ustanowione połączenie, serwer wyświetla odpowiedni komunikat w polu edycyjnym i próbuje odebrać dane. Również gdy dane zostaną odebrane, są wyświetlane.
Serwer TCP — użycie klasy CSocket Poprzednie podrozdziały zawierały opis procesu tworzenia klas dla klientów i serwerów w przypadku protokołów TCP i UDP na bazie biblioteki gniazd obecnych w WinAPI. W przypadku aplikacji MFC trud, jaki włożyliśmy w jej projektowanie, nie jest opłacalny (oczywiście poza walorem edukacyjnym) ze względu na klasę CSocket, która jest gotowym „opakowaniem” dla gniazd. Klasa ta upraszcza korzystanie z gniazd sieciowych, udostępniając wiele metod działających bardzo podobnie jak funkcje poznane we wcześniejszych projektach. Tak więc serwer rozpoczyna nasłuch, wywołując metodę Listen, połączenia są akceptowane poprzez wywołanie metody Accept, a dane są odbierane z użyciem metody Receive. Klient łączy się z serwerem, wywołując metodę Connect, a wysyła dane, wywołując metodę Send. Wszystkie te metody — a także wiele innych — pochodzą z klasy CAsyncSocket, z której dziedziczy CSocket.
Rozdział 9. ♦ Sieci komputerowe
331
Nazwa klasy bazowej sugeruje, że w projektowaniu klasy MFC wykorzystano nowszą bibliotekę gniazd Winsock 2. Przypuszczenie to potwierdza fakt, że klasa CSocket wyposażona jest w kilka metod, które są wywoływane, gdy zajdą określone zdarzenia sieciowe. Tak więc mamy np. OnAccept — gdy uzyskiwane jest połączenie z klientem, OnConnect — gdy klient połączy się z serwerem. Oznacza to, że serwer i klient działają w trybie asynchronicznym, nie blokując wątków aplikacji. Z klasy CSocket możemy korzystać na dwa sposoby: możemy skorzystać bezpośrednio z tej klasy, tworząc jej instancję i odpowiednio konfigurując jej stan, lub napisać swoją klasę, dziedziczącą z CSocket. W tym oraz następnym projekcie wykorzystamy obie metody. 1. Tworzymy nowy projekt MFC Application z oknem dialogowym. 2. Do projektu dodajemy pliki Server.h oraz Server.cpp. 3. Do pliku nagłówkowego dodajemy definicję klasy SerwerTCPAsync z listingu 9.37. Listing 9.37. Klasa SerwerTCPAsync, wykorzystująca obiekt CSocket #include "stdafx.h" class SerwerTCPAsync : public CSocket { public: SerwerTCPAsync(CEdit * _edit,int _port) { CSocket::CSocket(); edit = _edit; port = _port; klient = NULL; } ~SerwerTCPAsync() {if ( klient != NULL) delete klient;} void Start(); virtual void OnAccept(int nErrorCode); virtual void OnReceive(int nErrorCode); private: CAsyncSocket * klient; CEdit * edit; int port; };
1. Do pliku z rozszerzeniem .cpp dodajemy definicję klasy SerwerTCPAsync
z listingu 9.38. Listing 9.38. Odbieramy dane z sieci, wykorzystując nadpisane metody klasy MFC CSocket #include "Server.h" void SerwerTCPAsync::Start() { WSADATA wsaData; AfxSocketInit(&wsaData); if(!this->Create(port) )
332
Visual C++. Gotowe rozwiązania dla programistów Windows {
}
MessageBox(0,L"blad",L"",MB_OK); return ;
} if(!Listen()) { MessageBox(0,L"blad",L"",MB_OK); return ; }
void SerwerTCPAsync::OnAccept(int nErrorCode) { klient = new SerwerTCPAsync(edit,port); Accept(*klient); CString opis; edit->GetWindowTextW(opis); opis.Append(L"\r\n Nawiązano połączenie"); edit->SetWindowTextW(opis); __super::OnAccept(nErrorCode); } void SerwerTCPAsync::OnReceive(int nErrorCode) { wchar_t buff[1024] = {'\0'}; int n = Receive(buff,sizeof(buff));
}
CString opis; edit->GetWindowTextW(opis); opis.AppendFormat(L"\r\n Odebrane Dane: %s",buff); edit->SetWindowTextW(opis); __super::OnReceive(nErrorCode);
5. Plik Server.h dołączamy do projektu, dodając dyrektywę #include "Server.h"
do pliku nagłówkowego okna dialogowego. 6. Teraz należy przygotować i „oprogramować” interfejs użytkownika. Do okna
dialogowego dodajemy: pole edycyjne Edit Control, komponent Static Text, rozwijane listy Combo Box oraz przycisk Button (rysunek 9.16). Rysunek 9.16. Serwer TCP korzystający z CSocket
Rozdział 9. ♦ Sieci komputerowe
333
7. Dodajemy zmienne dla nowo dodanych komponentów; proponuję: edit1 dla komponentu Edit Control oraz comboPort dla komponentu Combo Box. 8. Właściwości Read Only i Multiline pola edycyjnego ustawiamy na true. 9. Do klasy okna dialogowego dodajemy nowe pole i deklarację destruktora. virtual ~CCSocketServerDlg(); private: SerwerTCPAsync * serwerTCP;
10. W pliku .cpp okna dialogowego modyfikujemy kod konstruktora oraz dodajemy
definicję destruktora (listing 9.39). Listing 9.39. Konstruktor i destruktor okna dialogowego CCSocketServerDlg::CCSocketServerDlg(CWnd* pParent /*=NULL*/) : CDialog(CCSocketServerDlg::IDD, pParent) { m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME); serwerTCP = NULL; } CCSocketServerDlg::~CCSocketServerDlg() { if(serwerTCP!= NULL) delete serwerTCP; }
11. Do metody OnInitDialog okna dialogowego dodajemy kod z listingu 9.40. Listing 9.40. Serwer TCP może być zainstalowany na porcie z przedziału 1025 – 4999 BOOL CCSocketServerDlg::OnInitDialog() { CDialog::OnInitDialog(); SetIcon(m_hIcon, TRUE); SetIcon(m_hIcon, FALSE); CString tmp; for(int i=1025;iStart(); }
13. Budujemy projekt.
Korzystanie z klasy CSocket niesie ze sobą wszystkie zalety obiektowości. W szczególności możemy przygotować własną klasę rozszerzającą CSocket i realizującą bardziej konkretne zadania. Ten sam efekt można oczywiście uzyskać za pomocą klasy korzystającej z funkcji WinSocket, ale chyba jednak wymaga to nieco większego wysiłku.
Klient TCP — użycie klasy CSocket Tym razem, korzystając z gotowej klasy CSocket z biblioteki MFC, przygotujemy projekt aplikacji pełniącej rolę klienta połączenia TCP. 1. Tworzymy nowy projekt MFC Application z oknem dialogowym. 2. Do projektu dodajemy pliki Klient.h i Klient.cpp. 3. Do pliku Klient.h dodajemy deklarację klasy KlientTCP z listingu 9.42. Listing 9.42. Wysyłamy dane, używając klasy CSocket #include "stdafx.h" class KlientTCP { public: KlientTCP(); void WyslijDane(CEdit & edit,const wchar_t * addr,const int port,const wchar_t * ´dane,const int dlugosc); private: CSocket socket; };
4. Do pliku nagłówkowego dodajemy definicję klasy KlientTCP z listingu 9.43. Listing 9.43. Wysyłamy dane przy użyciu protokołu TCP #include "Klient.h" KlientTCP::KlientTCP() { socket.Create(); } void KlientTCP::WyslijDane(CEdit & edit,const wchar_t * addr,const int port,const ´wchar_t * dane,const int dlugosc) { socket.Connect(addr,port); int n = socket.Send(dane,dlugosc);
Rozdział 9. ♦ Sieci komputerowe
335
CString opis; opis.AppendFormat(L"Wysłano %d bajtów",n); edit.SetWindowTextW(opis); }
5. W pliku nagłówkowym okna dialogowego dołączamy plik Klient.h: #include "Klient.h"
6. Do okna dialogowego dodajemy komponenty IP Address Control i Combo Box,
dwa pola edycyjne, cztery komponenty Static Text oraz przycisk Button zgodnie ze wzorem z rysunku 9.17. Rysunek 9.17. Klient TCP
7. Z kontrolkami wiążemy zmienne editDane i editWiad dla pól edycyjnych, addr dla IP Address Control oraz comboPort dla komponentu Combo Box. 8. Dla editWiad ustawiamy właściwość Read Only na true. 9. W metodzie OnInitDialog dopisujemy kod z listingu 9.44. Listing 9.44. Ustawiamy numery portów w komponencie Combo Box CString tmp; for(int i=1025;iGetFileName())) ´MessageBox(L"Pomyślnie zapisano plik"); } delete fileDialog;
16. Ostatnią czynnością jest przygotowanie kodu odpowiedzialnego za przesyłanie
plików na serwer FTP. Klikamy dwukrotnie przycisk Załaduj Plik i w utworzonej w ten sposób metodzie dodajemy kod z listingu 9.52. Listing 9.52. Ładowanie plików na serwer FTP void CPR_FTPDlg::OnBnClickedButton4() { fileDialog = new CFileDialog(true); if(fileDialog->DoModal() == IDOK) { CString file; file.AppendFormat(L"%s.%s",fileDialog->GetFileTitle(),fileDialog->GetFileExt()); if(ftp.DodajPlik(fileDialog->GetFileName(),file)) MessageBox(L"Pomyślnie ´zapisano plik"); } }
17. Budujemy i uruchamiamy projekt. Dużo serwerów FTP umożliwia logowanie
bez podawania loginu i hasła. Są to np. ftp.netscape.com czy ftp.mozilla.org. Takie serwery przeważnie jednak nie pozwalają na umieszczanie nowych plików na serwerze; możemy je tylko pobierać6.
Protokół SMTP (poczta elektroniczna) Protokół SMTP (ang. Simple Mail Transfer Protocol) służy do przekazywania (w tym wysyłania i odbierania) wiadomości e-mail w Internecie. Jest to prosty, tekstowy protokół, którego usługa korzysta zazwyczaj z portu o numerze 25. Istnieją rozszerzenia protokołu SMTP. Najczęściej spotykane to: SMTP-AUTH — protokół SMTP uzupełniony o mechanizmy uwierzytelniania.
Do autoryzacji używa się dwóch podstawowych metod: AUTH PLAIN i AUTH LOGIN. Każda z nich stosuje kodowanie Base64. ESMTP — dodano w nim komendę EHLO, a także nowe parametry do poleceń
MAIL FROM i RCPT TO. Tabela 9.1 przedstawia podstawowe komendy protokołu SMTP/ESMTP w kolejności, w jakiej powinny być wysyłane. Natomiast listing 9.53 przedstawia przykładową sesję SMTP. Warto ją przeanalizować, zwracając uwagę na kolejność wysyłanych komend. Taką samą kolejność za chwilę zastosujemy w aplikacji wysyłającej wiadomość e-mail. 6
Wiele firm oferuje darmowe serwery FTP, których można użyć do testów przygotowywanej aplikacji.
344
Visual C++. Gotowe rozwiązania dla programistów Windows
Tabela 9.1. Komendy protokołu SMTP HELO/EHLO
Rozpoczyna połączenie z serwerem. Polecenie HELO jest używane dla protokołu SMTP, a EHLO dla ESMTP
MAIL FROM
Identyfikuje nadawcę wiadomości e-mail Identyfikuje odbiorców wiadomości e-mail Informuje serwer o rozpoczęciu przesyłania treści wiadomości e-mail Kończy sesję SMTP/ESMTP
RCPT TO DATA QUIT
Listing 9.53. Przykład sesji SMTP S e r w e r : 220 poczta.interia.pl ESMTP INTERIA.PL K l i e n t : EHLO [poczta.interia.pl] S e r w e r : 250-poczta.interia.pl 250-PIPELINING 250-SIZE 52428800 250-VRFY 250-ETRN 250-STARTTLS 250-AUTH LOGIN PLAIN CRAM-MD5 250-AUTH=LOGIN PLAIN CRAM-MD5 250 8BITMIME K l i e n t : AUTH LOGIN S e r w e r : 334 VXNlcm5hbWU6 K l i e n t : bWFjX3Bhaw== S e r w e r : 334 UGFzc3dvcmQ6 K l i e n t : MzA1Njg1OQ== S e r w e r : 235 Authentication successful K l i e n t : MAIL FROM: S e r w e r : 250 Ok K l i e n t : RCPT TO: S e r w e r : 250 Ok K l i e n t : DATA S e r w e r : 354 End data with . K l i e n t : From: Maciej Pakulski K l i e n t : To: K l i e n t : Subject: Witaj !! K l i e n t : Oto treść wiadomości . S e r w e r : 250 Ok: queued as D8F0225A5B4D K l i e n t : QUIT S e r w e r : 221 Bye
Przygotujemy teraz klasę MailClass, która wyposażona będzie w metodę Wyslij służącą do wysłania wiadomości e-mail za pośrednictwem wskazanego serwera SMTP, a ściślej mówiąc: korzystając z gniazd, metoda ta będzie wysyłać komendy do serwera SMTP. Deklarację i definicję klasy umieścimy w plikach EMail.h i EMail.cpp. 1. Otwieramy plik EMail.h i dodajemy deklarację klasy MailClass z listingu 9.54. Listing 9.54. Deklaracja klasy MailClass #include "stdafx.h" #include "base64.h" class MailClass
Rozdział 9. ♦ Sieci komputerowe
345
{ public: void Wyslij(const char *user, const char *pass,const char *serwer,const char ´*smail,const char *rmail,const char *fuser,const char *topic,const char ´*msg,bool log); private: bool ZainicjujWinsock(const char * serwer); SOCKET mojSocket; };
1. W pliku EMail.cpp umieszczamy definicję metod klasy MailClass (listing 9.55). Listing 9.55. Definicja klasy MailClass #include "EMail.h" void MailClass::Wyslij(const char *user, const char *pass,const char *serwer,const ´char *smail, const char *rmail,const char *fuser,const char *topic,const char ´*msg,bool log) { char daneOdebrane[1024] = {'\0'}; char daneWysylane[1024] = {'\0'}; CString encode,desc; CFileDialog * fileDialog = NULL; Base64 base64; if(log) fileDialog = new CFileDialog(false,L"txt",L"log.txt"); if(!ZainicjujWinsock(serwer)) return; recv(mojSocket,daneOdebrane,1024,0); desc.AppendFormat(L"Serwer: %s",daneOdebrane); sprintf(daneWysylane,"EHLO [%s]\r\n",serwer); send(mojSocket,daneWysylane,strlen(daneWysylane),0); desc.AppendFormat(L"Klient: %s",daneWysylane); ZeroMemory(daneOdebrane,1024); recv(mojSocket,daneOdebrane,1024,0); desc.AppendFormat(L"Serwer: %s",daneOdebrane); ZeroMemory(daneWysylane,1024); sprintf(daneWysylane,"AUTH LOGIN\r\n"); send(mojSocket,daneWysylane,strlen(daneWysylane),0); desc.AppendFormat(L"Klient: %s",daneWysylane); ZeroMemory(daneOdebrane,1024); recv(mojSocket,daneOdebrane,1024,0); desc.AppendFormat(L"Serwer: %s",daneOdebrane); encode = base64.base64_encode((const unsigned char*)user,strlen(user)); ZeroMemory(daneWysylane,1024); wcstombs(daneWysylane,(LPWSTR)encode.GetBuffer(),1024); sprintf(daneWysylane,"%s\r\n", daneWysylane); //wysyłamy zakodowaną nazwę użytkownika send(mojSocket,daneWysylane,strlen(daneWysylane),0); desc.AppendFormat(L"Klient: %s",daneWysylane);
346
Visual C++. Gotowe rozwiązania dla programistów Windows ZeroMemory(daneOdebrane,1024); recv(mojSocket,daneOdebrane,1024,0); desc.AppendFormat(L"Serwer: %s",daneOdebrane); encode = base64.base64_encode((const unsigned char*)pass,strlen(pass)); ZeroMemory(daneWysylane,1024); wcstombs(daneWysylane,(LPWSTR)encode.GetBuffer(),1024); sprintf(daneWysylane,"%s\r\n",daneWysylane ); //wysyłamy zakodowane hasło send(mojSocket,daneWysylane,strlen(daneWysylane),0); desc.AppendFormat(L"Klient: %s",daneWysylane); ZeroMemory(daneOdebrane,1024); recv(mojSocket,daneOdebrane,1024,0); desc.AppendFormat(L"Serwer: %s",daneOdebrane); ZeroMemory(daneWysylane,1024); sprintf(daneWysylane,"MAIL FROM:\r\n",smail); send(mojSocket,daneWysylane,strlen(daneWysylane),0); desc.AppendFormat(L"Klient: %s",daneWysylane); ZeroMemory(daneOdebrane,1024); recv(mojSocket,daneOdebrane,1024,0); desc.AppendFormat(L"Serwer: %s",daneOdebrane); ZeroMemory(daneWysylane,1024); sprintf(daneWysylane,"RCPT TO:\r\n",rmail); send(mojSocket,daneWysylane,strlen(daneWysylane),0); desc.AppendFormat(L"Klient: %s",daneWysylane); ZeroMemory(daneOdebrane,1024); recv(mojSocket,daneOdebrane,1024,0); desc.AppendFormat(L"Serwer: %s",daneOdebrane); ZeroMemory(daneWysylane,1024); sprintf(daneWysylane,"DATA\r\n"); send(mojSocket,daneWysylane,strlen(daneWysylane),0); desc.AppendFormat(L"Klient: %s",daneWysylane); ZeroMemory(daneOdebrane,1024); recv(mojSocket,daneOdebrane,1024,0); desc.AppendFormat(L"Serwer: %s",daneOdebrane); ZeroMemory(daneWysylane,1024); sprintf(daneWysylane,"From: %s \r\n",fuser,smail); send(mojSocket,daneWysylane,strlen(daneWysylane),0); desc.AppendFormat(L"Klient: %s",daneWysylane); ZeroMemory(daneWysylane,1024); sprintf(daneWysylane,"To: \r\n",rmail); send(mojSocket,daneWysylane,strlen(daneWysylane),0); desc.AppendFormat(L"Klient: %s",daneWysylane); ZeroMemory(daneWysylane,1024); sprintf(daneWysylane,"Subject: %s\r\n",topic); send(mojSocket,daneWysylane,strlen(daneWysylane),0); desc.AppendFormat(L"Klient: %s",daneWysylane);
Rozdział 9. ♦ Sieci komputerowe ZeroMemory(daneWysylane,1024); sprintf(daneWysylane,"%s%s",msg,"\r\n.\r\n"); send(mojSocket,daneWysylane,strlen(daneWysylane),0); desc.AppendFormat(L"Klient: %s",daneWysylane); ZeroMemory(daneOdebrane,1024); recv(mojSocket,daneOdebrane,1024,0); desc.AppendFormat(L"Serwer: %s",daneOdebrane); ZeroMemory(daneWysylane,1024); sprintf(daneWysylane,"QUIT\r\n"); send(mojSocket,daneWysylane,strlen(daneWysylane),0); desc.AppendFormat(L"Klient: %s",daneWysylane); ZeroMemory(daneOdebrane,1024); recv(mojSocket,daneOdebrane,1024,0); desc.AppendFormat(L"Serwer: %s",daneOdebrane); closesocket(mojSocket); if(log) { if(fileDialog->DoModal() == IDOK) { CString h = fileDialog->GetFileName(); CFile file; file.Open(fileDialog->GetFileName(), CFile::modeCreate | CFile::modeWrite | CFile::shareExclusive); file.Write(desc,sizeof(wchar_t)*wcslen(desc)); file.Close(); } } } bool MailClass::ZainicjujWinsock(const char * serwer) { WSADATA wsaData; WORD wersja = MAKEWORD( 2, 0 ); int blad = WSAStartup(wersja, &wsaData ); struct hostent *host; struct sockaddr_in sin; memset( &sin, 0, sizeof(sin)); //pobierz adres serwera pocztowego host=gethostbyname(serwer); //zainicjuj socket sin.sin_family = AF_INET; sin.sin_addr.s_addr = ((struct in_addr *)(host->h_addr))->s_addr; sin.sin_port = htons(25); //port smtp-25 mojSocket = socket( AF_INET, SOCK_STREAM, 0 ); //połącz się z serwerem if(connect(mojSocket,(struct sockaddr *) &sin, sizeof(sin))) { return false; } return true; }
347
348
Visual C++. Gotowe rozwiązania dla programistów Windows 3. Kompilujemy projekt, aby sprawdzić poprawność klasy MailClass.
Klasa MailClass implementuje funkcjonalność prostego klienta pocztowego. Realizujemy to na bazie gniazda TCP, które należy utworzyć (tym zajmowaliśmy się wcześniej). Po utworzeniu gniazda możemy przejść do wysłania komend SMTP z odpowiednimi parametrami określającymi nagłówek i treść listu. Warto prześledzić listingi 9.53 i 9.55, porównując kolejność wysyłanych komend. Projekt używa algorytmu Base64, którego implementacja jest oparta na projekcie René Nyffeneggera7. Algorytm ten został zmodyfikowany przez napisanie klasy udostępniającej metody do kodowania i dekodowania ciągu znaków. Implementację algorytmu można znaleźć w dołączonych do książki materiałach (pliki base64.h i base64.cpp). Możemy teraz przygotować aplikację pozwalającą na wysyłanie listów elektronicznych. Wykorzystamy w niej klasę MailClass. Dodatkową funkcjonalnością aplikacji będzie możliwość zapisu komend protokołu SMTP w pliku tekstowym. 1. Tworzymy nowy projekt MFC Application z oknem dialogowym. 2. Do katalogu projektu kopiujemy pliki EMail.h i EMail.cpp i dodajemy je do
projektu. 3. Plik EMail.h dołączamy do pliku nagłówkowego klasy okna dialogowego za
pomocą dyrektywy: #include "EMail.h"
4. Do okna dialogowego dodajemy dziewięć komponentów Static Text, osiem pól
edycyjnych Edit Control, pole opcji Check Box oraz przycisk Button zgodnie ze wzorem z rysunku 9.19. 5. Z kontrolkami wiążemy zmienne. Dla okien dialogowych (w kolejności od góry) proponuję użyć nazw: uzytEdit, hasloEdit, serwerEdit, nadawcaEdit, odbiorcaEdit, nazwaNadEdit, editTemat, wiadEdit, natomiast dla komponentu Check Box — zmiennej o nazwie checkBox1. 6. Klikamy dwukrotnie przycisk Wyślij i w utworzonej metodzie dodajemy kod
z listingu 9.56. Listing 9.56. Wysyłamy e-mail, używając klasy MailClass void CSendMailDlg::OnBnClickedButton1() { ...char szUzyt[128],szHaslo[128],szSerwer[128],szNadawca[128], ´szOdbiorca[128],szNazwaNad[128],szTemat[128],szWiad[1024]; ...CString Uzyt,Haslo,Serwer,Nadawca,Odbiorca,NazwaNad,Temat,Wiad; ...uzytEdit.GetWindowTextW(Uzyt); ...hasloEdit.GetWindowTextW(Haslo); ...serwerEdit.GetWindowTextW(Serwer);
7
Oryginalny kod jest dostępny pod adresem http://www.adp-gmbh.ch/cpp/common/base64.html.
Rozdział 9. ♦ Sieci komputerowe
349
Rysunek 9.19. Interfejs użytkownika aplikacji do wysyłania e-maili
...nadawcaEdit.GetWindowTextW(Nadawca); ...odbiorcaEdit.GetWindowTextW(Odbiorca); ...nazwaNadEdit.GetWindowTextW(NazwaNad); ...editTemat.GetWindowTextW(Temat); ...wiadEdit.GetWindowTextW(Wiad); ...wcstombs(szUzyt,(LPWSTR)Uzyt.GetBuffer(),1024); ...wcstombs(szHaslo,(LPWSTR)Haslo.GetBuffer(),1024); ...wcstombs(szSerwer,(LPWSTR)Serwer.GetBuffer(),1024); ...wcstombs(szNadawca,(LPWSTR)Nadawca.GetBuffer(),1024); ...wcstombs(szOdbiorca,(LPWSTR)Odbiorca.GetBuffer(),1024); ...wcstombs(szNazwaNad,(LPWSTR)NazwaNad.GetBuffer(),1024); ...wcstombs(szTemat,(LPWSTR)Temat.GetBuffer(),1024); ...wcstombs(szWiad,(LPWSTR)Wiad.GetBuffer(),1024); ...MailClass mail; ...mail.Wyslij(szUzyt,szHaslo,szSerwer,szNadawca,szOdbiorca,szNazwaNad,szTemat, ´szWiad,checkBox1.GetCheck()); }
7. Budujemy i uruchamiamy projekt.
Aby wysłać wiadomość e-mail za pomocą naszego programu, musimy do pól edycyjnych wpisać adres serwera SMTP, dane dostępowe do serwera, adres e-mail nadawcy, adres e-mail odbiorcy, nazwę nadawcy, temat wiadomości, jej treść i nacisnąć przycisk
350
Visual C++. Gotowe rozwiązania dla programistów Windows
Wyślij. Dodatkowo, jeżeli chcemy sprawdzić, jak wyglądała sesja SMTP, możemy zaznaczyć pole opcji LOG. Kod sesji SMTP zostanie wtedy zapisany do pliku tekstowego. Przykład takiej sesji widoczny jest na listingu 9.53.
Inne Aby pobrać plik z Internetu Do pobrania pojedynczego pliku z serwera LAN lub z Internetu (np. dodatkowe dane lub pliki aktualizacji) najprościej jest wykorzystać funkcję URLDownloadToFile zadeklarowaną w nagłówku urlmon.h. Listing 9.57 prezentuje przykład jej wywołania. Listing 9.57. Najprostszy sposób pobrania pliku z Internetu #include "urlmon.h" void CInternetAppDlg::OnBnClickedButton1() { if (URLDownloadToFile(NULL, L"http://www.phys.uni.torun.pl/~jacek/img/torun.gif", L"c:\\WINDOWS\\TEMP\\torun.gif", 0, NULL) == S_OK) AfxMessageBox(L"Plik pobrany"); else AfxMessageBox(L"Błąd podczas pobierania pliku"); }
Należy pamiętać o zaimportowaniu nagłówka urlmon.h. Jeżeli zwracana przez funkcję wartość jest równa S_OK, czyli 0, oznacza to, że operacja powiodła się. Warto zwrócić uwagę, że dane nie muszą być pobierane tylko za pomocą protokołu http. Możemy np. użyć protokołu FTP: #include "urlmon.h" void CInternetAppDlg::OnBnClickedButton1() { if (URLDownloadToFile(NULL, L"ftp://ftp.mozilla.org/README", L"c:\\WINDOWS\\TEMP\\readme.txt", 0, NULL) == S_OK) AfxMessageBox(L"Plik pobrany"); else AfxMessageBox(L"Błąd podczas pobierania pliku"); }
Mapowanie dysków sieciowych Ostatni projekt dotyczący sieci poświęcimy funkcjom związanym z mapowaniem dysków sieciowych. Nie dlatego, żeby pośród usług lokalnych sieci LAN mapowanie dysków było szczególnie ważne, ale dlatego, że jest najbardziej reprezentatywne, bo
Rozdział 9. ♦ Sieci komputerowe
351
dostępne są dwie drogi, aby uzyskać odpowiedni efekt. Po pierwsze: możemy wywołaniem funkcji WinAPI WNetConnectionDialog(m_hWnd, RESOURCETYPE_DISK); wyświetlić użytkownikowi okno dialogowe o nazwie Mapowanie dysku sieciowego. Analogicznie okno dialogowe pozwalające na odłączenie dysku wyświetlamy za pomocą polecenia WNetDisconnectDialog(m_hWnd, RESOURCETYPE_DISK);. Ma to oczywiście ograniczony sens — zazwyczaj w programach mapowanie dysku służy konkretnym celom i pozostawianie użytkownikowi dowolności określania litery dysku i zdalnego zasobu zwykle nic nie daje. Dlatego dostępna jest także funkcja WNetAddConnection2, która wykonuje właściwe podłączenie wskazanego dysku sieciowego do wskazanego symbolu dysku. Podać wówczas trzeba także login użytkownika i jego hasło (chodzi o login i hasło na serwerze, który udostępnia dysk). Do odłączania dysku służy funkcja WNetCancelConnection28, w której jako pierwszego argumentu najlepiej użyć lokalnego symbolu zmapowanego dysku. Przykładowy kod realizujący trwałe (tj. zapisane w profilu użytkownika i w ten sposób odnawiane po ponownym jego zalogowaniu) podłączenie dysku sieciowego pokazany jest w listingu 9.58. Listing 9.58. Po kliknięciu przycisku Button3 symbol N: będzie reprezentował katalog domowy użytkownika konta na serwerze. Oczywiście pod warunkiem, że jego hasło na serwerze to „haslo” #include "winnetwk.h" #pragma comment(lib, "mpr.lib") void CInternetAppDlg::OnBnClickedButton3() { NETRESOURCE zasobySieciowe; zasobySieciowe.dwType = RESOURCETYPE_ANY; zasobySieciowe.lpLocalName = L"N:"; //symbol dysku zasobySieciowe.lpRemoteName = L"\\\\serwer\\konto"; //nazwa zdalnego zasobu zasobySieciowe.lpProvider = NULL; if (WNetAddConnection2(&zasobySieciowe, L"haslo", L"konto", ´CONNECT_UPDATE_PROFILE) == NO_ERROR) AfxMessageBox(L"Udane mapowanie dysku sieciowego"); else AfxMessageBox(L"Mapowanie dysku sieciowego nie powiodło się"); }
Podawanie hasła i loginu (drugi i trzeci argument funkcji WNetAddConnect2) nie jest konieczne. Zastąpienie łańcucha przez pusty wskaźnik NULL wskazuje, że wykorzystane mają być login i hasło użyte przy lokalnym logowaniu bieżącego użytkownika. To oczywiście nie zawsze działa prawidłowo.
8
Wszystkie wymienione funkcje dostępne są we wszystkich 32-bitowych wersjach Windows.
352
Visual C++. Gotowe rozwiązania dla programistów Windows
Rozdział 10.
Wątki
Tworzenie wątków Każdy użytkownik systemu Windows doskonale wie, że może uruchamiać kilka aplikacji jednocześnie. Pisząc ten rozdział, mogę swobodnie słuchać radia internetowego i odbierać wiadomości e-mail. Aplikacje dające mi tę możliwość działają jako osobne procesy, definiowane często jako instancje działającego programu. Żeby się o tym przekonać, wystarczy uruchomić menedżera zadań Windows i przejść na zakładkę Procesy. To, co umyka mojej uwadze, to fakt, że tak naprawdę aplikacje te nie działają w tym samym czasie. Moimi odczuciami steruje planista systemu Windows, który wątkom działającym w ramach tych procesów przydziela pewien bardzo mały odstęp czasu, zwany kwantem. Planista, sprytnie przydzielając wątkom kwanty czasu procesora, tworzy wrażenie, że aplikacje pracują równocześnie. Procesy bez wątków są tym, czym komputer bez użytkownika. W trakcie uruchamiania procesu tworzony jest jego wątek główny, który z kolei może uruchamiać następne wątki, a te następne mogą uruchamiać kolejne itd. Jeśli wszystkie wątki zakończą swoje działanie, to proces nie ma już po co istnieć, a planista Windows kończy jego działanie. Wielowątkowość może przyspieszyć działanie aplikacji dzięki wykonywaniu niektórych operacji w tle. W codziennym „życiu komputerowym” doświadczamy wielowątkowości na wielu płaszczyznach. Edytory tekstu na przykład sprawdzają w tle pisownię, w tle może odbywać się kopiowanie plików itd. Windows bardzo wiele czynności wykonuje w tle bez naszej wiedzy: od indeksowania plików, poprzez działanie wszystkich usług, po specjalny wątek zerowania stron, mający na celu czyszczenie pamięci. Z punktu widzenia biblioteki MFC wątki dzielą się na wątki robocze oraz na wątki interfejsu użytkownika (UI). Odróżnia je to, że tylko wątki UI posiadają pętlę komunikatów. Wątki robocze nie mogą tworzyć okien i wysyłać do nich komunikatów. Przebiegają zgodnie z ustalonym algorytmem z ograniczoną możliwością bieżącej kontroli w trakcie działania. Doskonale nadają się do wykonywania zadań nie wymagających interakcji z użytkownikiem. Wątki robocze mogą jednak co jakiś czas np. odczytywać parametry z plików, które będą wpływać na ich działanie. Gdybyśmy na
354
Visual C++. Gotowe rozwiązania dla programistów Windows
przykład projektowali aplikację obsługującą termometr podłączony do komputera, to zadaniem wątku roboczego mogłoby być odczytywanie informacji z tego urządzenia i wykonywanie związanych z tym obliczeń. Wówczas zadania wątku głównego aplikacji ograniczałyby się jedynie do prezentacji wyników.
Tworzenie wątku Przygodę z wątkami rozpoczniemy od utworzenia wątku roboczego za pomocą funkcji WinApi CreateThread oraz funkcji tzw. C runtime (CRT) _beginthread1. Jako pierwszej użyjemy funkcji _beginthread. Argumentami tej funkcji są: start_address, stack_size oraz arglist. Pierwszy z nich określa adres funkcji, która ma być wykonywana przez nowy wątek (funkcja wątku); drugi określa, ile przestrzeni adresowej wątek może wykorzystać na własny stos; trzeci odpowiada za listę argumentów, które zostaną przekazane do funkcji wątku. W niniejszym projekcie zadaniem funkcji wątku będzie wyświetlenie informacji o wersji zainstalowanego dodatku Service Pack. Wykorzystamy do tego strukturę OSVERSIONINFO oraz funkcję GetVersionExW. Tej samej funkcji wątku użyjemy w metodzie tworzącej wątek za pomocą funkcji WinAPI CreateThread2. Jej argumenty są analogiczne do parametrów funkcji _beginthreadex. Różnią się tylko formalnymi nazwami typów (typy WinAPI i odpowiadające im typy C). Z tego powodu konieczne będzie rzutowanie funkcji wątku na typ LPTHREAD_START_ROUTINE. 1. Tworzymy nowy projekt z oknem dialogowym. 2. Dodajemy do formy dwa przyciski i tworzymy ich domyślne metody
zdarzeniowe, które definiujemy według listingu 10.1. Listing 10.1. Ograniczamy się do wyświetlenia wersji zainstalowanego Service Pack #include "process.h" //_beginthread, _beginthreadex ... void FunkcjaWatku(void *param) { OSVERSIONINFO lInfo; ZeroMemory(&lInfo, sizeof(OSVERSIONINFO)); lInfo.dwOSVersionInfoSize = sizeof(OSVERSIONINFO); GetVersionExW(&lInfo); CString temp; temp.Format(L"Wersja Service Pack: %s", lInfo.szCSDVersion); AfxMessageBox(temp); } void CWatekDlg::OnBnClickedButton1() { _beginthread(FunkcjaWatku, 0, NULL); } 1
Dostępne począwszy od Windows 2000.
2
Obecna od Windows 2000.
Rozdział 10. ♦ Wątki
355
void CWatekDlg::OnBnClickedButton2() { CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)FunkcjaWatku, NULL, NULL, NULL); }
Bibliotekę uruchomieniową C (CRT) wyposażono również w funkcję _beginthreadex. Liczne argumenty tej funkcji umożliwiają lepszą kontrolę nad tworzonym wątkiem. Jej dodatkowymi argumentami w stosunku do funkcji _beginthread są: security, initflag oraz thrdaddr. Parametr security jest wskaźnikiem do struktury SECURITY_ ´ATTRIBUTES. Z kolei initflag określa początkowy stan wątku. W szczególności można przekazać tu flagę CREATE_SUSPENDED, której poświęcono jeden z następnych projektów. Ostatni parametr rozszerzonej wersji funkcji, o nazwie thrdaddr, służy do nadania wątkowi identyfikatora. Oczywiście w CRT dostępne są również funkcje, których wywołanie powoduje zakończenie wykonywania wątku3. Są to _endthread oraz _endthreadex. Pierwsza wymusza zakończenie wątku utworzonego przez funkcję _beginthread, a druga — _beginthreadex. Oczywiście najlepszym sposobem zakończenia wątku jest po prostu zakończenie działania funkcji wątku, jednakże ręczne wywołanie tych funkcji umożliwia „awaryjne” przerwanie działania wątku, zapewniając jednocześnie właściwe zwolnienie zasobów zaalokowanych na potrzeby wykonania wątku. Niezależnie od funkcji _endthread oraz _endthreadex wszystkie wątki kończą również działanie, jeżeli nastąpi wywołanie jednej z funkcji C: abort, exit, _exit lub funkcji WinApi ExitProcess.
Tworzenie wątku roboczego za pomocą MFC Do utworzenia wątku roboczego za pomocą MFC służy funkcja AfxBeginThread. Jej pierwszym i najważniejszym parametrem jest funkcja wykonywana przez nowy wątek. Parametr przesłany do tej funkcji podajemy w drugim argumencie. Pozostałe parametry omówimy przy okazji następnych projektów. 1. Tworzymy nowy projekt aplikacji MFC z oknem dialogowym o nazwie
WatkiRobocze. 2. Umieszczamy na formie przycisk z etykietą Utwórz wątek roboczy oraz pole edycyjne, z którym należy związać zmienną Edit1 (należy również upewnić się, że ID kontrolki Edit1 ma wartość IDC_EDIT1). 3. Interfejs aplikacji uzupełniamy, dodając do formy etykietę Koniec pętli przy,
i umieszczamy obok niej następne pole edycyjne (rysunek10.1). 4. Wiążemy zmienną Edit2 z drugim polem edycyjnym (tym znajdującym się
bliżej etykiety). 5. Tworzymy domyślną metodę zdarzeniową do przycisku i uzupełniamy jej kod
zgodnie z listingiem 10.2.
3
Bardziej szczegółowe omówienie mechanizmu kończenia wątków znajduje się w części „Kończenie wątku” niniejszego rozdziału.
356
Visual C++. Gotowe rozwiązania dla programistów Windows
Rysunek 10.1. Widok formy projektowanej aplikacji
Listing 10.2. Uruchamianie wątku roboczego void CWatkiRoboczeDlg::OnBnClickedButton1() { if(Edit2.GetWindowTextLengthW() > 0) { CString temp; // Ilość iteracji Edit2.GetWindowTextW(temp); int its = _wtoi(temp); // Uruchomienie wątku CWinThread *pThread = AfxBeginThread(FunkcjaWatku, (LPVOID)its); } else AfxMessageBox(L"Popraw ilość iteracji"); }
6. Deklarujemy zmienną globalną typu HWND o nazwie hWnd, która przechowywać
będzie uchwyt do okna dialogowego. 7. W metodzie OnInitDialog umieszczamy polecenie kopiujące uchwyt okna do zmiennej globalnej: hWnd = m_hWnd;. 8. Definiujemy funkcję wątku według wzoru z listingu 10.3. Należy pamiętać,
aby umieścić tę definicję przed definicją metody zdarzeniowej przycisku. Listing 10.3. Prosta funkcja wątku, której zadaniem jest wyświetlanie wartości indeksu pętli UINT FunkcjaWatku(LPVOID pParam) { int its = (int)pParam; CString temp; if(its > 1) { for(int i = 1; i its > 1) { for(int i = 1; i its; i++) { temp.Format(L"Indeks pętli = %d", i); SetDlgItemTextW(hWnd, IDC_EDIT1, temp); if(params->sleepTime != NULL) Sleep(params->sleepTime); } } delete params; return 0; // Wywołanie funkcji wątku zakończyło się sukcesem }
Listing 10.6. Do funkcji AfxBeginThread przekazujemy strukturę parametrów void CWatkiRoboczeDlg::OnBnClickedButton1() { if(Edit2.GetWindowTextLengthW() > 0) { CString temp; threadParams *params = new threadParams; // Ilość iteracji Edit2.GetWindowTextW(temp); params->its = _wtoi(temp); params->sleepTime = NULL; // Czas uśpienia wątku if(Edit3.GetWindowTextLengthW() > 0) {
Rozdział 10. ♦ Wątki
359 int sleepTime; CString tempSleepTime; Edit3.GetWindowTextW(tempSleepTime); sleepTime = _wtoi(tempSleepTime); if(sleepTime < 0) AfxThrowUserException(); // Wyjątek można dopasować ´w zależności od potrzeb else params->sleepTime = sleepTime;
} // Uruchomienie wątku CWinThread *pThread = AfxBeginThread(FunkcjaWatku, params); } else AfxMessageBox(L"Popraw ilość iteracji"); }
Po ponownym skompilowaniu aplikacji uruchamiamy wątek roboczy z większym od zera czasem uśpienia. W menedżerze zadań Windows na zakładce Wydajność zauważymy wówczas, że wątek dzieli się czasem procesora z innymi aplikacjami. Nietrudno stwierdzić, iż uśpienie wątku powoduje również wydłużenie czasu potrzebnego na wykonanie jego działania. Ale czy samo wypisywanie liczb całkowitych z zakresu od 1 do zadanej przez użytkownika wartości trwa rzeczywiście dłużej? O tym przekonamy się w następnym projekcie.
Czas wykonywania wątków Po przeanalizowaniu powyższych projektów pojawia się naturalne pytanie o czas faktycznego wykonywania wątków. System Windows oferuje funkcje QueryPerformanceFrequency i QueryPerformanceCount5, dzięki którym zmierzymy czas wykonywania wątków przy wykorzystaniu pomiarów w wysokiej rozdzielczości. 1. Modyfikujemy funkcję wątku zgodnie z listingiem 10.7. Listing 10.7. Obliczamy czas wykonywania wątku, a wynik wyświetlamy na ekranie UINT FunkcjaWatku(LPVOID pParam) { CWatkiRoboczeDlg::threadParams* params = (CWatkiRoboczeDlg::threadParams *) pParam; CString temp; LARGE_INTEGER startTime, endTime, freq; QueryPerformanceCounter(&startTime); // Czas początkowy QueryPerformanceFrequency(&freq); // Częstość if(params->its > 1) { for(int i = 1; i its; i++) { 5
Obie dostępne we wszystkich 32-bitowych wersjach systemu Windows.
360
Visual C++. Gotowe rozwiązania dla programistów Windows temp.Format(L"Indeks pętli = %d", i); SetDlgItemTextW(hWnd, IDC_EDIT1, temp); if(params->sleepTime != NULL) Sleep(params->sleepTime); } } QueryPerformanceCounter(&endTime); // Czas końcowy __int64 totalTime = (endTime.QuadPart - startTime.QuadPart)*1000/freq.QuadPart; ´// Czas wykonania w ms temp.Format(L"Czas wykonywania wątku to: %d ms", totalTime); AfxMessageBox(temp); delete params; return 0; // Wywołanie funkcji wątku zakończyło się sukcesem }
2. Kompilujemy kod źródłowy i uruchamiamy aplikację. Uzyskany wynik
powinien być analogiczny do przedstawionego na rysunku 10.2. Rysunek 10.2. Informacja o czasie potrzebnym na wykonanie wątku
Funkcje QueryPerformanceFrequency i QueryPerformanceCount wymagają krótkiego komentarza. Obie funkcje dostarczają informacji na temat wydajności procesora. Jeśli nasz sprzęt nie obsługuje licznika wysokiej rozdzielczości, funkcja QueryPerformanceFrequency zwróci wartość zero sygnalizującą błąd. Wartość niezerowa oznacza ilość cykli procesora na sekundę (częstotliwość). Z kolei funkcja QueryPerformanceCount zwraca bieżącą wartość licznika wysokiej rozdzielczości. Większość pomiarów w wysokiej rozdzielczości wykonuje się dla krótkotrwałych bloków kodu. Ich scenariusz może być następujący: 1. Wywołujemy funkcję QueryPerformanceCount bezpośrednio przed i po bloku
instrukcji, których czas wykonania zamierzamy zmierzyć.
Rozdział 10. ♦ Wątki
361
2. Odejmujemy od siebie otrzymane wartości, a wynik dzielimy przez częstotliwość otrzymaną z funkcji QueryPerformanceFrequency. W efekcie uzyskamy czas
wykonania zadania w sekundach. 3. W celu wyrażenia tego czasu w milisekundach otrzymany w poprzednim
punkcie wynik mnożymy przez 1000. Warto sprawdzić, jak zmieniają się czasy wykonywania wątku, dla różnych wartości czasu uśpienia wątku. W szczególności, gdy czas ten ustalimy na 0. Pomiary nie korzystające z licznika wysokiej rozdzielczości możemy wykonać, korzystając ze standardowej funkcji time.
Wstrzymywanie i wznawianie wątków Wstrzymywanie wątków realizuje się za pomocą funkcji SuspendThread, a ich wznawianie za pomocą funkcji ResumeThread6. Aby zaprezentować ich działanie: 1. Dodajemy do klasy okna dialogowego prywatne pole uchwytWatku typu HANDLE. 2. Modyfikujemy metodę zdarzeniową przycisku Utwórz wątek roboczy
z listingu 10.5 w następujący sposób: ... CWinThread *pThread = AfxBeginThread(FunkcjaWatku, params); uchwytWatku = pThread->m_hThread; // zachowujemy uchwyt wątku ...
3. Pod przyciskiem Utwórz wątek wstrzymany umieszczamy na formie dwa
przyciski z etykietami Wstrzymaj wątek i Wznów wątek. 4. Tworzymy do obu przycisków metody zdarzeniowe i definiujemy je zgodnie
z listingiem 10.8. Listing 10.8. Kontrolujemy wykonywanie wątku void CWatkiRoboczeDlg::OnBnClickedButton2() { SuspendThread(uchwytWatku); } void CWatkiRoboczeDlg::OnBnClickedButton3() { ResumeThread(uchwytWatku); }
Wewnątrz wątku znajduje się pewien licznik, określający ilość wstrzymań wątku. Wywołanie funkcji SuspendThread na rzecz tego wątku powoduje zwiększenie licznika wstrzymań o 1. Liczba wstrzymań jest ograniczona z góry przez wartość MAXIMUM_ ´SUSPEND_COUNT. Jeśli wątek wstrzymano trzy razy, to zanim zacznie swoje działanie, 6
Obie funkcje dostępne są od Windows 2000.
362
Visual C++. Gotowe rozwiązania dla programistów Windows
należy go wznowić trzy razy za pomocą funkcji ResumeThread. Poprzez wywołanie funkcji SuspendThread dowolny wątek może wstrzymać inny wątek, ale nie może sam siebie wznowić. W naszym przykładzie wznawianie odbywa się z wątku głównego aplikacji.
Kończenie wątku Istnieje kilka sposobów na zakończenie działania wątku. Naturalnym jest taki, że funkcja wątku kończy po prostu swoje działanie i zwraca wartość. Taki sposób zakończenia wątku zaprezentowaliśmy w poprzednich projektach. Teraz natomiast omówimy trzy możliwości wymuszenia zakończenia działania wątku, mianowicie: zakończenie wskazanego wątku przez wywołanie funkcji TerminateThread, zakończenie bieżącego wątku przez wywołanie funkcji ExitThread, zabicie procesu zawierającego wątek — funkcje ExitProcess i TerminateProcess.
Wszystkie te funkcje są obecne w WinAPI od Windows 2000.
Funkcja TerminateThread 1. Umieszczamy na formie przycisk z etykietą Zakończ wątek (TerminateThread). 2. Tworzymy metodę zdarzeniową dla tego przycisku i wstawiamy do niej wywołanie funkcji TerminateThread (listing 10.9). Listing 10.9. W ten sposób możemy zakończyć dowolny wątek void CWatkiRoboczeDlg::OnBnClickedButton4() { TerminateThread(uchwytWatku, -1); }
Argumentem funkcji TerminateThread jest parametr typu DWORD, określający kod wyjścia dla wątku. Za pomocą tej funkcji możemy zniszczyć dowolny wątek, o ile znamy jego uchwyt.
Funkcja ExitThread Postępujemy analogicznie jak w poprzednim projekcie. 1. Dodajemy do formy przycisk i zmieniamy jego etykietę na Zakończ wątek
(ExitThread). 2. W domyślnej metodzie zdarzeniowej przycisku umieszczamy polecenia
z listingu 10.10.
Rozdział 10. ♦ Wątki
363
Listing 10.10. Wywołana w poniższy sposób funkcja ExitThread zakończy główny wątek aplikacji void CWatkiRoboczeDlg::OnBnClickedButton5() { ExitThread(-1); }
Po uruchomieniu aplikacji przekonamy się, że wciśnięcie przycisku Zakończ wątek (ExitThread) spowoduje zakończenie działania wątku odpowiedzialnego za okno dialogowe. Okno zniknie, ale po pewnym czasie naszym oczom ukaże się okno komunikatu informujące o czasie wykonania wątku roboczego. Innymi słowy, zakończyliśmy działanie wątku, ale nie całego procesu (aplikacja nadal działa). System operacyjny zakończy proces dopiero wówczas, gdy wszystkie jego wątki zostaną zakończone. Wymuszenie zakończenia bieżącego wątku można zrealizować przez wywołanie w nim funkcji ExitThread. Należy jednak pamiętać, że aplikacja nie powinna nadużywać funkcji TerminateThread i ExitThread. Tak przerywane wątki nie mają bowiem możliwości zwolnienia zajmowanych przez siebie zasobów, nie mają też żadnej możliwości, aby zapobiec zakończeniu działania lub opóźnić czas zakończenia działania np. w sytuacji, gdy wykonują jakieś krytyczne czynności. Wymuszenie zakończenia wątku można zrealizować przez przekazanie do funkcji wątku sygnału (np. przez ustawienie zmiennych globalnych), że ta powinna zakończyć swoje działanie. W niektórych sytuacjach użycie funkcji TerminateThread i ExitThread jest jednak niezbędne.
Funkcje TerminateProcess i ExitProcess Korzystając z funkcji TerminateProcess i ExitProcess, zniszczymy cały proces, w ramach którego wykonują się wątki. Unikniemy wówczas takiej sytuacji, z jaką spotkaliśmy się w poprzednim podrozdziale, tzn. nie pojawi się już komunikat informujący o czasie wykonania wątku. 1. Dodajemy do formy dwa przyciski: Zakończ wątek (TerminateProcess)
i Zakończ wątek (ExitProcess). 2. Tworzymy i definiujemy domyślne metody zdarzeniowe zgodnie ze wzorem
z listingu 10.11. Listing 10.11. „Zabijamy” procesy void CWatkiRoboczeDlg::OnBnClickedButton6() { // Funkcja GetCurrentProcess zwraca uchwyt do aktualnego procesu TerminateProcess(GetCurrentProcess(), -1); } void CWatkiRoboczeDlg::OnBnClickedButton7() { ExitProcess(-1); }
364
Visual C++. Gotowe rozwiązania dla programistów Windows
Priorytety wątków System Windows planuje wykonywanie wątków według ich priorytetów, które mogą przyjmować wartości od 0 (najniższy priorytet) do 31 (najwyższy priorytet). Należy przy tym zwrócić uwagę na fakt, że system nie planuje przydzielania czasu procesora procesom, a wyłącznie wątkom (każdy proces ma przynajmniej jeden wątek). Procedura przydzielania czasu procesora w systemie Windows działa w taki sposób, że najpierw wykonywane są naprzemiennie wątki o najwyższym priorytecie, a dopiero potem te o niższym. Sytuacja ta nazywana jest wyczerpywaniem. Powoduje, że wątki o niskim priorytecie mogą zostać wykonane po bardzo długim czasie, o ile działa wiele długich zadań o wysokim priorytecie. Taki scenariusz obowiązuje na maszynie jednoprocesorowej. W systemach wieloprocesorowych system operacyjny stara się zająć wszystkie procesory, zatem wątki o priorytecie 31 i 30 mogą działać równolegle. Istnieje jeden szczególny priorytet, do którego nie mamy dostępu. Jest nim poziom 0, któremu odpowiada systemowy wątek zerowania stron. Jest on odpowiedzialny za zerowanie i zwalnianie stron pamięci w systemie. Działa tylko wtedy, gdy aktualnie w systemie nie są wykonywane żadne inne wątki. Priorytet wątku (tzw. priorytet bazowy wątku) zależy od klasy priorytetu procesu, w ramach którego został utworzony, oraz od względnego priorytetu dla wątku. System Windows udostępnia sześć klas priorytetów dla procesów (tabela 10.1) oraz siedem względnych priorytetów dla wątków (tabela 10.2). Ich odpowiednie kombinacje składają się na priorytet wątku, czyli liczbę od 1 do 31 (tabela 10.3). Priorytet nr 0 jest zarezerwowany dla wątku zerowania stron. Nietrudno sobie wyobrazić, że użycie priorytetu czasu rzeczywistego (tabela 10.1) powoduje, że wątki w tym procesie muszą reagować na zdarzenia natychmiast. Może to uniemożliwić m.in. wykonywanie operacji wejścia-wyjścia, ponieważ większość wątków systemowych działa „tylko” z wysokim priorytetem. W szczególności po utworzeniu wątku z tak wysokim priorytetem nasza aplikacja może przestać reagować na sygnały wprowadzane z klawiatury i myszy, przez co może sprawiać wrażenie „zawieszonej”. W celu uruchomienia aplikacji z priorytetem czasu rzeczywistego wymagane jest posiadanie odpowiednich uprawnień, które domyślnie nadawane są administratorom i użytkownikom zaawansowanym. Z kolei uruchamianie wątków z niskim priorytetem doskonale nadaje się do wykonywania operacji w tle. Domyślnym priorytetem procesu i wątku jest poziom normalny. Tabela 10.1. Klasy priorytetów dla procesów Klasa priorytetów
Oznaczenie
Czasu rzeczywistego
REALTIME_PRIORITY_CLASS
Wysoki
HIGH_PRIORITY_CLASS
Powyżej normalnego
ABOVE_NORMAL_PRIORITY_CLASS
Normalny
NORMAL_PRIORITY_CLASS
Poniżej normalnego
BELOW_NORMAL_PRIORITY_CLASS
Niski
IDLE_PRIORITY_CLASS
Rozdział 10. ♦ Wątki
365
Tabela 10.2. Względne priorytety dla wątków od najwyższego do najniższego Względny priorytet dla wątku
Oznaczenie
Krytyczny Najwyższy Powyżej normalnego Normalny Poniżej normalnego Najniższy Bezczynny
THREAD_PRIORITY_TIME_CRITICAL THREAD_PRIORITY_HIGHEST THREAD_PRIORITY_ABOVE_NORMAL THREAD_PRIORITY_NORMAL THREAD_PRIORITY_BELOW_NORMAL THREAD_PRIORITY_LOWEST THREAD_PRIORITY_IDLE
Tabela 10.3. Priorytety bazowe wątków7. Poziomy 17- 21, 27-30 nie są dostępne Klasa priorytetów procesu Względny priorytet wątku Bezczynny Najniższy Poniżej normalnego Normalny Powyżej normalnego Najwyższy Krytyczny
Niski
Poniżej normalnego
Normalny
Powyżej normalnego
Wysoki
Czasu rzeczywistego
1 2
1 4
1 6
1 8
1 11
16 22
3
5
7
9
12
24
4
6
8
10
13
24
5
7
9
11
14
25
6 15
8 15
10 15
12 15
15 15
26 31
Priorytety procesu System Windows umożliwia uruchamianie aplikacji z opisanymi powyżej klasami priorytetów procesów. W tym celu należy wykorzystać polecenie start. Dla przykładu: aby uruchomić edytor rejestru z niskim priorytetem, należy użyć polecenia: start /LOW regedit. Priorytet działającego procesu można zmienić, korzystając z menedżera zadań Windows (rysunek 10.3). Zmianę priorytetu procesu z poziomu kodu umożliwiają dwie funkcje WinAPI:
SetPriorityClass, która zmienia klasę priorytetu, oraz funkcja GetPriorityClass8, zwra-
cająca bieżącą klasę priorytetów procesu9. Korzystając z tych dwóch funkcji, będziemy kontrolować priorytet procesu z poziomu kodu. Natomiast w dwóch następnych podrozdziałach przedstawimy analogiczny sposób zmiany priorytetów wątków.
7 8 9
J. Richter, C. Nasare , Windows via C/C++ Wydanie V, Microsoft Press, 2008. Obie funkcje — podobnie jak inne funkcje dotyczące kontroli wątku — zostały dodane do WinAPI od Windows 2000. Obie funkcje omówione zostały już w rozdziale 3., niniejszy projekt można zatem traktować jako powtórzenie i uzupełnienie przedstawionych tam wiadomości przed przejściem do zmiany priorytetu wątków.
366
Visual C++. Gotowe rozwiązania dla programistów Windows
Rysunek 10.3. Sprawdzamy priorytet procesu
1. Umieszczamy na formie trzy etykiety (rysunek 10.4). Własność Caption
pierwszej z nich zmieniamy na Priorytet bieżącego procesu. 2. Z drugą etykietą wiążemy zmienną Label1. Należy pamiętać o wcześniejszej zmianie jej identyfikatora ID z ID_STATIC na np. ID_STATIC1. Rysunek 10.4. Widok projektowanej aplikacji. Kontrolki związane z priorytetami wątków wykorzystamy w dwóch następnych projektach
3. Tekst wyświetlany przez trzecią etykietę zmieniamy na Ustaw priorytet procesu. 4. Bezpośrednio pod trzecią etykietą umieszczamy listę rozwijaną, z którą wiążemy zmienną ComboBox1. 5. W pliku nagłówkowym WatkiRoboczeDlg.h umieszczamy definicję prywatnej struktury processPriority.
Rozdział 10. ♦ Wątki
367
typedef struct processPriority { DWORD priority; CString priorityName; } processPriority;
6. Przechodzimy do edycji kodu klasy CWatkiRoboczeDlg i modyfikujemy metodę OnInitDialog zgodnie z listingiem 10.12. Listing 10.12. Przystępujemy do zmiany priorytetu procesu BOOL CWatkiRoboczeDlg::OnInitDialog() { CDialog::OnInitDialog(); hWnd = m_hWnd; // Set the icon for this dialog. The framework does this automatically // when the application's main window is not a dialog SetIcon(m_hIcon, TRUE); // Set big icon SetIcon(m_hIcon, FALSE); // Set small icon processPriority processPS[] = { {IDLE_PRIORITY_CLASS, L"IDLE_PRIORITY_CLASS"}, {BELOW_NORMAL_PRIORITY_CLASS, L"BELOW_NORMAL_PRIORITY_CLASS"}, {NORMAL_PRIORITY_CLASS, L"NORMAL_PRIORITY_CLASS"}, {ABOVE_NORMAL_PRIORITY_CLASS, L"ABOVE_NORMAL_PRIORITY_CLASS"}, {HIGH_PRIORITY_CLASS, L"HIGH_PRIORITY_CLASS"}, {REALTIME_PRIORITY_CLASS, L"REALTIME_PRIORITY_CLASS"}, }; // Wypełnianie ComboBox1 i przedstawienie informacji // o priorytecie aktualnego procesu int actualProcessClass = GetPriorityClass(GetCurrentProcess()); for(int i = 0; i < sizeof(processPS)/sizeof(*processPS); i++) { ComboBox1.AddString(processPS[i].priorityName); ComboBox1.SetItemData(i, processPS[i].priority); if(processPS[i].priority == actualProcessClass) Label1.SetWindowTextW(processPS[i].priorityName); } }
return TRUE;
// return TRUE
unless you set the focus to a control
7. Tworzymy metodę zdarzeniową CBN_SELCHANGE dla kontrolki ComboBox1
i umieszczamy w jej definicji polecenia z listingu 10.13. Listing 10.13. Priorytet bieżącego procesu zmieniamy w sposób dynamiczny. Do przekazania kodu błędu używamy funkcji GetLastError. Do przedstawienia informacji o błędzie służy funkcja FormatMessage void CWatkiRoboczeDlg::OnCbnSelchangeCombo1() { int index = ComboBox1.GetCurSel(); if(!SetPriorityClass(GetCurrentProcess(), ComboBox1.GetItemData(index))) { HLOCAL hlocal = NULL; BOOL msg = FormatMessageW( FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS |
368
Visual C++. Gotowe rozwiązania dla programistów Windows FORMAT_MESSAGE_ALLOCATE_BUFFER, NULL, GetLastError(), MAKELANGID(LANG_POLISH, SUBLANG_POLISH_POLAND), (PTSTR)&hlocal, 0, NULL);
} else {
}
}
if(msg && (hlocal != NULL)) MessageBox((PCTSTR)LocalLock(hlocal)); else MessageBox(L"Wystąpił nieznany błąd");
CString temp; ComboBox1.GetLBText(index, temp); Label1.SetWindowTextW(temp);
8. Kompilujemy aplikację i uruchamiamy ją. Efekt zmiany priorytetu procesu
powinien być widoczny w menedżerze zadań, jak pokazano na rysunku 10.5. Rysunek 10.5. Widok aplikacji oraz podgląd priorytetu procesu w menedżerze zadań
Rozdział 10. ♦ Wątki
369
Statyczna kontrola priorytetów wątków 1. Interfejs użytkownika projektujemy według wzoru z rysunku 10.4
(umieszczamy na oknie tylko brakujące kontrolki). 2. Z kontrolką ComboBox, znajdującą się pod etykietą Ustaw priorytet wątku, wiążemy zmienną ComboBox2. 3. Do klasy CWatkiRoboczeDlg (plik WatkiRoboczeDlg.h) dodajemy definicję prywatnej struktury threadPriority: typedef struct threadPriority { int priority; CString priorityName; } threadPriority;
4. W metodzie CWatkiRoboczeDlg::OnInitDialog dodajemy polecenia z listingu 10.14. Listing 10.14. Listę rozwijaną wypełniamy stałymi przypisanymi do priorytetów wątków BOOL CWatkiRoboczeDlg::OnInitDialog() { ... threadPriority threadPS[] = { {THREAD_PRIORITY_ABOVE_NORMAL, L"THREAD_PRIORITY_ABOVE_NORMAL"}, {THREAD_PRIORITY_BELOW_NORMAL, L"THREAD_PRIORITY_BELOW_NORMAL"}, {THREAD_PRIORITY_HIGHEST, L"THREAD_PRIORITY_HIGHEST"}, {THREAD_PRIORITY_IDLE, L"THREAD_PRIORITY_IDLE"}, {THREAD_PRIORITY_LOWEST, L"THREAD_PRIORITY_LOWEST"}, {THREAD_PRIORITY_NORMAL, L"THREAD_PRIORITY_NORMAL"}, {THREAD_PRIORITY_TIME_CRITICAL, L"THREAD_PRIORITY_TIME_CRITICAL"}, }; // Wypełnianie ComboBox2 for(int i = 0; i < sizeof(threadPS)/sizeof(*threadPS); i++) { ComboBox2.AddString(threadPS[i].priorityName); ComboBox2.SetItemData(i, threadPS[i].priority); } return TRUE; // return TRUE unless you set the focus to a control }
5. Metodę zdarzeniową związaną z przyciskiem Uruchom wątek roboczy (listing 10.6)
modyfikujemy według wzoru z listingu 10.15. Listing 10.15. Wykorzystujemy kolejny parametr funkcji AfxBeginThread, jakim jest możliwość ustawienia priorytetu wątku void CWatkiRoboczeDlg::OnBnClickedButton1() { if(Edit2.GetWindowTextLengthW() > 0) { CString temp; int threadPriority = 0; threadParams *params = new threadParams;
370
Visual C++. Gotowe rozwiązania dla programistów Windows // Ilość iteracji Edit2.GetWindowTextW(temp); params->its = _wtoi(temp); params->sleepTime = NULL; // Czas uśpienia wątku if(Edit3.GetWindowTextLengthW() > 0) { int sleepTime; CString tempSleepTime; Edit3.GetWindowTextW(tempSleepTime); sleepTime = _wtoi(tempSleepTime); if(sleepTime < 0) AfxThrowUserException(); else params->sleepTime = sleepTime; } // Priorytet wątku CString tempThreadPriority; int index = ComboBox1.GetCurSel(); if(index != CB_ERR) threadPriority = ComboBox1.GetItemData(index); // Uruchomienie wątku CWinThread *pThread = AfxBeginThread(FunkcjaWatku, params, threadPriority); uchwytWatku = pThread->m_hThread; // Zachowujemy uchwyt wątku } else AfxMessageBox(L"Popraw ilość iteracji"); }
Dynamiczna kontrola priorytetów wątków W poprzednim projekcie nauczyliśmy się uruchamiać wątek z zadanym priorytetem. W tym projekcie spróbujemy natomiast zmieniać priorytety wątków w sposób dynamiczny. Służy do tego funkcja SetThreadPriority. Windows oferuje również funkcję GetThreadPriority, dzięki której dowiemy się, z jakim priorytetem wykonywany jest wątek. 1. Dodajemy do formy przycisk z etykietą Pokaż priorytet wątku. 2. Tworzymy domyślną metodę zdarzeniową dla nowego przycisku, w której
umieszczamy polecenia z listingu 10.16. Listing 10.16. Prezentujemy informacje o priorytecie, z jakim uruchomiony został wątek roboczy void CWatkiRoboczeDlg::OnBnClickedButton8() { int threadPriority = GetThreadPriority(uchwytWatku); CString temp; for(int i = 0; i < ComboBox2.GetCount(); i++)
Rozdział 10. ♦ Wątki
371
{ if(ComboBox2.GetItemData(i) == threadPriority) ComboBox2.GetLBText(i, temp); } AfxMessageBox(L"Priorytet aktualnie uruchomionego wątku to " + temp); }
3. Do rozwijanej listy dodajemy metodę obsługującą zdarzenie CBN_SELCHANGE
(listing 10.17). Listing 10.17. Dynamiczna zmiana priorytetu wątku void CWatkiRoboczeDlg::OnCbnSelchangeCombo2() { DWORD exitCode; CString temp; GetExitCodeThread(uchwytWatku, &exitCode); if(exitCode == STILL_ACTIVE) // Czy wątek zakończył swoje działanie? if(!SetThreadPriority(uchwytWatku,ComboBox2.GetItemData(ComboBox2.GetCurSel()))) { temp.Format(L"Próba zmiany priorytetu wątku nie powiodła się. Kod ´błędu: %d", GetLastError()); AfxMessageBox(temp); } else ; else AfxMessageBox(L"Nie uruchomiono wątku roboczego"); }
W powyższej metodzie zdarzeniowej upewniamy się, że wątek roboczy nie zakończył działania. Wykorzystujemy do tego funkcję GetExitCodeThread, która w zmiennej typu DWORD zapisuje kod wyjścia wątku. Jeśli w momencie wywołania funkcji Get ´ExitCodeThread wątek nie zakończył jeszcze swojego działania, to kod wyjścia ma wartość STILL_ACTIVE. Wywołanie funkcji ExitThread lub TerminateThread powoduje zmianę wartości STILL_ACTIVE na wartość kodu wyjścia przekazaną do jednej z tych funkcji.
Flaga CREATE_SUSPENDED W niektórych sytuacjach wygodnie jest utworzyć wątek wstrzymany, który uruchomiony będzie z opóźnieniem, np. w wyniku interakcji z użytkownikiem. Służy do tego flaga CREATE_SUSPENDED. W nowym projekcie wykorzystamy ten mechanizm do tworzenia wątku umieszczającego napisy w zwykłym notatniku Windows. 1. Tworzymy nowy projekt aplikacji MFC z oknem dialogowym o nazwie
WatkiCreateSuspended. 2. Umieszczamy na formie dwa przyciski. Pierwszy z etykietą Uruchom wątek
wstrzymany, a drugi z opisem Wznów wątek wstrzymany.
372
Visual C++. Gotowe rozwiązania dla programistów Windows 3. Do klasy CWatkiCreateSuspendedDlg dodajemy prywatne pole typu HANDLE o nazwie uchwytWatku. 4. Tworzymy domyślną metodę zdarzeniową do pierwszego przycisku i definiujemy
ją według listingu 10.18. Na tym samym listingu widoczna jest także funkcja wątku. Listing 10.18. Funkcja wątku wstrzymanego umieszcza napis w oknie notatnika Windows. W podobny sposób możemy wysłać napisy do okien innych aplikacji UINT FunkcjaWatku(LPVOID pParam) { HWND hWndP = FindWindow(NULL, L"Bez tytułu - Notatnik"); HWND hWndC = GetWindow(hWndP, GW_CHILD); const wchar_t tekst[] = L"Tekst wstawiony przez wątek wstrzymany"; if(hWndP != NULL) SendMessage(hWndC, WM_SETTEXT, 0, (LPARAM)tekst); else AfxMessageBox(L"Notatnik nie został uruchomiony!"); return 0; } void CWatkiCreateSuspendedDlg::OnBnClickedButton1() { CWinThread *pThread = AfxBeginThread(FunkcjaWatku, NULL, 0, 0, ´CREATE_SUSPENDED); uchwytWatku = pThread->m_hThread; }
5. W metodzie zdarzeniowej drugiego przycisku umieszczamy polecenie wznowienia
działania wątku (a w zasadzie rozpoczęcia jego działania) widoczne na listingu 10.19. Listing 10.19. Z funkcji ResumeThread skorzystaliśmy już w listingu 10.8 void CWatkiCreateSuspendedDlg::OnBnClickedButton2() { ResumeThread(uchwytWatku); }
6. Kompilujemy aplikację i uruchamiamy ją. 7. Uruchamiamy notatnik. 8. Klikamy przycisk Uruchom wątek wstrzymany. Po kliknięciu przycisku Wznów
wątek wstrzymany okno notatnika powinno wyglądać podobnie jak na rysunku 10.6. Może ciekawiej byłoby automatycznie zareagować na uruchomienie notatnika i po wykryciu tego faktu od razu umieścić w jego oknie napis. Taką modyfikację naszej aplikacji zaprezentujemy w następnym podrozdziale.
Rozdział 10. ♦ Wątki
373
Rysunek 10.6. Wysłanie tekstu do notatnika
Wątek działający z ukrycia 1. Metodę CWatkiCreateSuspendedDlg::OnInitDialog modyfikujemy według
wzoru z listingu 10.20. Listing 10.20. Uruchamiamy wątek roboczy wraz z pojawieniem się okna aplikacji BOOL CWatkiCreateSuspendedDlg::OnInitDialog() { CDialog::OnInitDialog(); hWnd = m_hWnd; // Set the icon for this dialog. The framework does this automatically // when the application's main window is not a dialog SetIcon(m_hIcon, TRUE); // Set big icon SetIcon(m_hIcon, FALSE); // Set small icon CWinThread *pThread2 = AfxBeginThread(FunkcjaWatku2, NULL); return TRUE;
// return TRUE
unless you set the focus to a control
}
2. Funkcję wątku należy zdefiniować zgodnie z listingiem 10.21. Listing 10.21. Wątek „polujący” na uruchomienie notatnika jest usypiany, żeby działał, nie korzystając nadmiernie z czasu procesora UINT FunkcjaWatku2(LPVOID pParam) { while(true) { HWND hWndP = FindWindow(NULL, L"Bez tytułu - Notatnik"); if(hWndP != NULL) FunkcjaWatku(NULL); Sleep(100); } return 0; }
3. Kompilujemy aplikację i uruchamiamy ją. W efekcie po otwarciu notatnika
zostaje wstawiony do niego odpowiedni napis.
374
Visual C++. Gotowe rozwiązania dla programistów Windows
Programowanie koligacji Koligacja oznacza przypisywanie procesów i wątków do określonych procesorów. Wyróżniamy miękką i twardą koligację. System Windows, korzystając domyślnie z miękkiej koligacji, stara się uruchamiać wątek na tym procesorze, na którym był ostatnio uruchomiony. Umożliwia to wątkowi ponowne wykorzystanie danych znajdujących się w pamięci podręcznej procesora, co przekłada się na wydajność zadania. W architekturach typu NUMA (ang. Non-Uniform Memory Access — niejednorodny dostęp do pamięci) występuje kilka płyt zawierających określoną liczbę procesorów oraz wydzieloną pamięć. Szczególnie istotne jest wówczas, aby wątki uruchamiane były na procesorach znajdujących się na tej samej płycie. Umożliwia to korzystanie z dedykowanej pamięci i poprawia wydajność. W przeciwnym wypadku efektywność systemu NUMA zmniejsza się w znaczący sposób. Ograniczanie możliwości uruchamiania wątków do określonego procesora lub grupy procesorów nazywa się twardą koligacją. Na przykładzie poniższych projektów przeanalizujemy funkcje dostarczane przez system Windows, a służące do programowania koligacji.
Informacja o liczbie procesorów (funkcja GetSystemInfo) Niniejszy projekt poświęcimy przedstawieniu informacji o liczbie procesorów w naszym komputerze. System Windows w trakcie uruchamiania określa liczbę dostępnych procesorów w danej maszynie. W każdym momencie możemy zapytać system o tę informację. Służy do tego funkcja GetSystemInfo10. 1. Tworzymy nowy projekt aplikacji MFC z oknem dialogowym o nazwie
WatkiKoligacje. 2. Do klasy CWatkiKoligacjeDlg dodajemy prywatne pole typu SYSTEM_INFO
(typ zdefiniowany w WinAPI przechowujący informacje m.in. o architekturze komputera) o nazwie sysInfo. 3. W metodzie CWatkiKoligacjeDlg::OnInitDialog dodajemy polecenie GetSystemInfo(&sysInfo);. 4. Umieszczamy na formie przycisk z etykietą Wyświetl informacje o ilości
procesorów. 5. Tworzymy domyślną metodę zdarzeniową dla przycisku i umieszczamy w niej
komendy z listingu 10.22.
10
Dostępna w Windows 2000 i nowszych wersjach.
Rozdział 10. ♦ Wątki
375
Listing 10.22. Funkcja GetSystemInfo inicjuje wszystkie elementy struktury SYSTEM_INFO. Nasze potrzeby ograniczają się jedynie do jednego elementu void CWatkiKoligacjeDlg::OnBnClickedButton1() { CString temp; temp.Format(L"Ilość procesorów w twoim komputerze = %d", sysInfo.dwNumberOfProcessors); AfxMessageBox(temp); }
6. Kompilujemy aplikację i uruchamiamy ją. Kliknięcie przycisku Wyświetl
informacje o ilości procesorów spowoduje wyświetlenie komunikatu widocznego na rysunku 10.7. Rysunek 10.7. Prezentujemy informacje o liczbie procesorów w systemie
Przypisywanie procesu do procesora Ograniczenie wykonywania procesu do podzbioru procesorów umożliwia funkcja SetProcessAffinityMask11. Funkcję tę parametryzują pola: hProcess i dwProcessAffinityMask. Pierwszy parametr określa uchwyt do procesu, którego koligację pragniemy zmienić. Natomiast drugi oznacza maskę koligacji, która jest mapą bitową reprezentującą podzbiór procesorów. Procesory numerowane są kolejnymi liczbami całkowitymi, zaczynając od 0. Zatem, jeśli proces ma być wykonywany na określonych procesorach, to w masce koligacji należy ustawić wartość odpowiednich bitów na 1. Przykłady masek koligacji przedstawiono w tabeli 10.4. 1. Dodajemy do okna aplikacji z poprzedniego podrozdziału drugi przycisk z etykietą
Przełącz proces na następny procesor. 2. Do klasy CWatkiKoligacjeDlg dodajemy prywatne pole aktualnyProcesor typu int. 3. W metodzie OnInitDialog umieszczamy polecenie inicjujące to pole wartością
równą 1. 4. Tworzymy domyślną metodę zdarzeniową dla nowego przycisku i definiujemy
ją zgodnie z listingiem 10.23.
11
Dostępna od Windows 2000.
376
Visual C++. Gotowe rozwiązania dla programistów Windows
Tabela 10.4. Przykładowe maski koligacji dla maszyny czteroprocesorowej Procesor nr 0
1
Maska koligacji 2
3
Postać heksadecymalna
Postać bitowa
0x1
0001
0x2
0010
X X X
0x3
0011
X
0x4
0100
X
0x5
0101
X
X X X X X
0x6
0110
X
0x8
1000
X X
0x9
1001
X
0xA
1010
X
X
X
0xE
1110
X
X
X
0xF
1111
Listing 10.23. Programowanie koligacji void CWatkiKoligacjeDlg::OnBnClickedButton2() { int iloscProcesorow = sysInfo.dwNumberOfProcessors; DWORD numerProcesora; CString temp; if(aktualnyProcesor >= iloscProcesorow) aktualnyProcesor = 1; else aktualnyProcesor++; numerProcesora = 1 0.01f) 79 printf(”%d = %f != %f \n”, i, goldRes[i], h_output[i]);
Pozostaje już tylko posprzątać po sobie, co robią instrukcje pokazane na listingu A.16. Listing A.16. Zwalnianie pamięci i kończenie wątku 81 82 83 84 85 86 87 88 89
// zwalnianie pamięci free(h_input); free(h_output); free(goldRes); cutilSafeCall(cudaFree(d_input)); cutilSafeCall(cudaFree(d_output)); cudaThreadExit(); cutilExit(argc, argv);
W ten sposób zakończyliśmy implementację tej części programu, która wykonywana jest na CPU. Przejdźmy teraz do najważniejszej części programu, a mianowicie do funkcji jądra, która wykonywana będzie na GPU. Ponownie pierwszą czynnością w funkcji jądra jest wyznaczenie indeksu wątku (linia numer 9 w listingu A.17). Później sprawdzamy, czy nie jesteśmy na krańcu tablicy. Jeżeli tak, to kopiujemy do tablicy wyjściowej odpowiadający mu element tablicy wejściowej i kończymy ten wątek. Wątki nie znajdujące się na krańcach tablicy obliczają sumę elementów oddalonych
500
Visual C++. Gotowe rozwiązania dla programistów Windows
od ich pozycji o co najwyżej wartość przechowywaną w stałej RADIUS, a następnie dzielą ją przez ilość elementów, które sumowały (listing A.17). Listing A.17. Zasadnicze obliczenia wykonywane przez jądro 9 const unsigned int tid = blockIdx.x*blockDim.x + threadIdx.x; 10 11 if( tid < RADIUS | | tid >= BLOCK_SIZE * gridDim.x − RADIUS) { 12 d_output[tid] = d_input[tid]; 13 return; 14 } 15 16 float res = 0; 17 for(int i = −RADIUS; i = BLOCK_SIZE * gridDim.x − RADIUS) { d_output[tid] = sBuf[localId]; return ; }
Analogicznie postępujemy z obliczaniem sumy (listing A.25). Listing A.25. Uśrednianie elementów wektora 26 float res = 0 ; 27 28 for(int i = −RADIUS; i