• Nie Znaleziono Wyników

Coś takiego cały czas dzieje się z komputerami i jakoś nikt na to nie narzeka.

N/A
N/A
Protected

Academic year: 2021

Share "Coś takiego cały czas dzieje się z komputerami i jakoś nikt na to nie narzeka. "

Copied!
45
0
0

Pełen tekst

(1)

1

APLIKACJE OKIENKOWE

Wyobraź sobie, że gdy w każdy czwartek zwyczajnie zawiązujesz sobie buty, one eksplodują.

Coś takiego cały czas dzieje się z komputerami i jakoś nikt na to nie narzeka.

Jeff Raskin, wywiad dla „Doctor Dobb’s Journal”

Pierwsze komputery osobiste powstały już całkiem dawno temu, bo przy końcu lat siedemdziesiątych ubiegłego stulecia. Prawie od samego początku mogły też wykonywać całkiem współczesne operacje - z edycją dokumentów czy wykorzystaniem sieci włącznie.

A jednak dopiero ostatnia dekada przyczyniła się do niezmiernego upowszechnienia pecetów, a umiejętność ich obsługi stała się powszechna i konieczna. Przyczyn można upatrywać się w szybkim rozwoju Internetu, jednak trudno sobie wyobrazić jego

ekspansję oraz rozpowszechnienie samych komputerów, gdyby ich użytkowanie nie było proste i intuicyjne. Bez łatwych metod komunikacji z programami początkujący

użytkownik byłby bowiem zawsze w trudnej sytuacji.

Po licznych „bojach” stoczonych z konsolą możemy z pewnością stwierdzić, że interfejs tekstowy niekiedy bywa wygodny. Faktycznie oferuje go każdy system operacyjny, a za jego pomocą często można szybciej i efektywniej wykonywać rutynowe zadania - szczególnie, kiedy mamy już pewne doświadczenie w obsłudze danego systemu.

Nie da się jednak ukryć, że całkowity nowicjusz, posadzony przez ekranem z migającym tajemniczo kursorem, może poczuć się, delikatnie mówiąc, lekko zdezorientowany.

Naturalnie mógłby on zajrzeć do stosownych dokumentacji czy też innych źródeł niezbędnych wiadomości, lecz procent użytkowników, którzy rzeczywiście tak czynią, oscyluje chyba gdzieś w granicach błędu statystycznego (jeśli ktokolwiek przeprowadzał kiedykolwiek takie badania) :D Czy to jest jednak tylko ich problem?…

Otóż nie, a właściwie - już nie. Oto bowiem w latach osiemdziesiątych wymyślono nowe sposoby dialogu aplikacji z użytkownikiem, z których najlepszy (dla użytkownika) okazał się interfejs graficzny. Prawdopodobnie zadecydowały tu proste analogie w stosunku do znanych urządzeń, które regulowało się najczęściej przy pomocy różnych przycisków, pokręteł, suwaków czy włączników. Koncepcje te dały się łatwo przenieść w świat

wirtualny i znacznie rozszerzyć, dając w efekcie obecny wygląd interfejsu użytkownika w większości popularnych programów.

Graficzny interfejs użytkownika (ang. graphical user interface - w skrócie GUI) to sposób wymiany informacji między programem a użytkownikiem, oparty na wyświetlaniu interaktywnej grafiki i reakcji na działania, jakie są podejmowane w stosunku do niej.

Istnieje wiele powodów, dla których ten rodzaj interfejsu jest generalnie łatwiejszy w obsłudzie niż rozwiązania oparte na tekście. Nietrudno znaleźć te powody, gdy

porównamy jakąś aplikację konsolową i program wykorzystujący interfejs graficzny.

Warto jednak wymienić te przyczyny - najlepiej w kolejności rosnącego znaczenia:

¾ program konsolowy jest często dla użytkownika „czarną skrzynką” (żeby nie

powiedzieć - czarną magią ;D). O jego przeznaczeniu czy oferowanych przezeń

funkcjach rzadko może się bowiem dowiedzieć z informacji prezentowanych mu na

(2)

ekranie. Są one zwykle tylko wynikami pracy programu lub też prośbami o wprowadzenie potrzebnych danych.

Tymczasem w programach o interfejsie graficznym konieczne są przynajmniej szczątkowe opisy poszczególnych opcji i funkcji, a to samo w sobie daje pewne wskazówki co do prawidłowej obsługi programu.

Nic więc dziwnego, że wielu użytkowników programów uczy się ich obsługi metodą prób i błędów, czyli kolejnego wypróbowywania oferowanych przez nie opcji i obserwacji

efektów tych działań.

¾ elementy interfejsów graficznych są bardziej „namacalne” niż tekstowe polecenia, wpisywane z klawiatury. Łatwiej domyślić się, jak działa przycisk, suwak czy pole tekstowe - czego nie można powiedzieć o komendach konsolowych.

Można więc stwierdzić, że występuje tu podobny efekt jak w przypadku

programowania obiektowego. Coś, co możemy zobaczyć/wyobrazić sobie, jest po prostu łatwiejsze do przyswojenia niż abstrakcyjne koncepcje.

Screen 44. Nawet skomplikowany interfejs graficzny może być prostszy w obsłudze niż aplikacja konsolowa. Kalkulator mógłby oczywiście z powodzeniem działać w trybie tekstowym i oferować

dużą funkcjonalność, np. w postaci obliczania złożonych wyrażeń. Można ją jednak

zaimplementować także w aplikacji z graficznym interfejsem, zaś przyciski i inne elementy okna są z pewnością bardziej intuicyjne niż choćby lista dostępnych funkcji i operacji matematycznych.

¾ graficzne interfejsy użytkownika dają większą swobodę i kontrolę nad przebiegiem programu. O ile w aplikacjach konsolowych funkcjonowanie programu opiera się zazwyczaj na schemacie: pobierz dane Æ pracuj Æ pokaż wynik, o tyle interfejsy graficzne w większości przypadków zostawiają użytkownikowi olbrzymie pole manewru, jeżeli chodzi o podejmowane czynności i ich kolejność. Jest to całkowicie inny model funkcjonowania programu.

GUI jest zatem nie tylko zmianą w powierzchowności aplikacji, ale też fundamentalną różnicą, jeżeli chodzi o jej działanie - zarówno od strony użytkownika, jak i programisty.

Tworzenie programów z interaktywnym interfejsem przebiega więc inaczej niż kodowanie aplikacji konsolowych, działających sekwencyjnie.

Ta druga czynność jest nam już doskonale znana, teraz więc przyszła pora na poznanie

metod tworzenia aplikacji działających w środowiskach graficznych i wykorzystujących

elementy GUI.

(3)

Posiadanie graficznego interfejsu nie oznacza aczkolwiek, że dany program jest w pełni interaktywny. Wiele z aplikacji niewątpliwie graficznych, np. kreatory instalacji, są w rzeczywistości programami sekwencyjnymi. Jednocześnie możliwe jest osiągnięcie interaktywności w środowisku tekstowym, czego najlepszym przykładem jest chyba popularny w czasach DOSa menedżer plików Norton Commander.

Obecnie jednak aplikacje wyposaża się w graficzny interfejs właśnie po to, aby ich obsługa była interaktywna i możliwa na dowolne sposoby. Na tym opierają się dzisiejsze systemy operacyjne.

Zajmiemy się programowaniem aplikacji okienkowych przeznaczonych dla środowiska Windows. Pamiętamy oczywiście, że naszym nadrzędnym celem jest poznanie technik programowania gier; znajomość podstaw tworzenia programów GUI jest jednak niezbędna, by móc korzystać z biblioteki graficznej DirectX, która przecież działa w graficznym środowisku Windows. Umiejętność posługiwania się narzędziami, jakie ten system oferuje, powinna też zaprocentować w bliższej lub dalszej przyszłości i z pewnością okaże się pomocna.

A zatem - zaczynajmy programowanie w Windows!

Wprowadzenie do programowania Windows

Pisanie programów działających w podsystemie GUI w Windows różni się zasadniczo od tworzenia aplikacji konsolowych. Wynika to nie tylko z nowych narzędzi

programistycznych, jakie należy do tego wykorzystać, ale także, czy może przede

wszystkim, z innego modelu funkcjonowania programów okienkowych. Wymaga to nieco innego podejścia do kodowania, myślę jednak, że jest ono nawet łatwiejsze i bardziej sensowne niż dla konsoli.

Na początek naszej przygody z programowaniem Windows poznamy więc ów nowy model działania aplikacji. Później zobaczymy również, jakie instrumenty wspomagające

kodowanie oferuje ten system operacyjny.

Programowanie sterowane zdarzeniami

Elastyczność, jaką wykazują programy z interfejsem graficznym, w zakresie kontroli ich działania przez użytkownika jest niezwykle duża. Można w zasadzie stwierdzić, że dopiero takie aplikacje stają się przydatnymi narzędziami, posłusznymi swoim użytkownikom. Nie narzucają żadnych ścisłych wymogów co do sposobu obsługi, pozostawiając duże pole swobody i ergonomii.

Osiągnięcie takich efektów przy pomocy znanych nam technik programowania byłoby bardzo trudne, a na pewno naciągane - jeżeli nie niemożliwe. Graficzny interfejs aplikacji okienkowych wymaga bowiem zupełnie nowego sposobu kodowania: programowania sterowanego zdarzeniami.

Modele działania programów

Aby dokładnie zrozumieć tę ideę i móc niedługo stosować ją w praktyce, potrzebne są rzecz jasna stosowne wyjaśnienia. Przede wszystkim chciałoby się wiedzieć, na czym polegają różnice w tym sposobie programowania, gdy przyrównamy go do znanego dotychczas sekwencyjnego uruchamiania kodu. Nie od rzeczy byłoby także wskazanie zalet nowego modelu działania aplikacji. To właśnie uczynimy teraz.

Najpierw należałoby więc sprecyzować, co rozumiemy pod pojęciem modelu działania

programu, gdyż stosowaliśmy ten termin już kilkakrotnie i najwyraźniej wydaje się on tu

kluczowy. Mianowicie możemy powiedzieć krótko:

(4)

Model funkcjonowania aplikacji (ang. application behavior model) to, najogólniej mówiąc, pozycja, jaką zajmuje program w stosunku do użytkownika oraz do systemu operacyjnego. Określa on sposób, w jaki kod programu steruje jego działaniem, głównie wprowadzaniem danych wejściowych i wyprowadzaniem wyjściowych.

Wyjaśnienie to może wydawać się dosyć mgliste, ponieważ pojęcie modelu funkcjonowania aplikacji jest jedną z najbardziej fundamentalnych spraw w projektowaniu, kodowaniu, jak również w użytkowaniu wszystkich bez wyjątku

programów. Jednocześnie trudno je rozpatrywać całkiem ogólnie, tak jak tutaj, i dlatego zwykle się tego nie robi; niemniej jednak jest to bardzo ważny aspekt programowania.

Najczęściej wszakże mówi się jedynie o odmianach modelu działania aplikacji, a zatem także i my je poznamy. Nie są one zresztą całkiem dla nas obce, a nawet nowopoznane koncepcje wydadzą się, jak sądzę, dosyć logiczne i rozsądne.

Przyjrzyjmy się więc poszczególnym modelom.

Model sekwencyjny

Najstarszym i najwcześniej przez nas spotkanym w nauce programowania modelem jest model sekwencyjny. Był to w początkach rozwoju komputerów najbardziej oczywisty sposób, w jaki mogły działać ówczesne programy.

Ogólnym założeniem tego modelu jest ustawienie programu w pozycji dialogu z

użytkownikiem. Taki dialog nie jest oczywiście normalną konwersacją, jako że komputery nigdy nie były, nie są i nie będą ani trochę inteligentne. Dlatego też przyjmuje ona formę wywiadu, któremu poddawany jest użytkownik.

Aplikacja zatem „zadaje pytania” i oczekuje na nie odpowiedzi w postaci potrzebnych sobie danych. Prezentuje też wyniki swojej pracy do wglądu użytkownika.

Schemat 36. Sekwencyjny model działania programu. Aplikacja zajmuje tu miejsce nadrzędne w stosunku do użytkownika (stąd jej wielkość na diagramie :D), a system operacyjny jest

pośrednikiem w wymianie informacji (zaobrazowanej strzałkami).

Najważniejsze, że cały przebieg pracy programu jest kontrolowany przez programistę. To on ustala, kiedy należy odczytać dane z klawiatury, wyświetlić coś na ekranie czy

wykonać inne akcje. Wszystko ma tu swój określony porządek i kolejność, na którą użytkownik nie ma wpływu. Właśnie ze względu na ową kolejność model ten nazywamy sekwencyjnym.

Najlepszym przykładem aplikacji, które działają w ten sposób, będą wszystkie napisane dotychczas w tym kursie programy konsolowe; w ogólności tyczy się to w zasadzie

każdego programu konsolowego. Sama natura tego środowiska wymusza pobieranie oraz

pokazywanie informacji w pewnej kolejności, niepozwalającej użytkownikowi na większą

swobodę.

(5)

Do tej grupy możemy też zaliczyć bliższe nam aplikacje, funkcjonujące jako kreatory (ang. wizards), z kreatorami instalacji na czele. Posiadają one wprawdzie interfejs graficzny, ale sposób i porządek ich działania jest ściśle ustalony. Obrazują to nawet kolejne kroki - ekrany, które po kolei pokonuje użytkownik, podając dane i obserwując efekty swoich działań.

Screen 45. Kreatory instalacji (ang. setup wizards) są przykładami programów działających sekwencyjnie. Zawierają wprawdzie elementy interfejsu graficznego właściwe innym aplikacjom,

ale ich działanie jest precyzyjnie ustalone i podzielone na kroki, które należy pokonywać w określonej kolejności.

Poza wspomnianymi kreatorami (które zazwyczaj są tylko częścią większych aplikacji), programy działające sekwencyjnie nie występują zbyt licznie i nie mają poważniejszych zastosowań. Ich niewrażliwość na intencje użytkownika i zatwardziałe trzymanie się ustalonych schematów funkcjonowania sprawiają, że nie można przy ich pomocy swobodnie wykonywać swoich zajęć.

Model zdarzeniowy

Zupełnie inne podejście jest prezentowane w modelu zdarzeniowym, zwanym też programowaniem sterowanym zdarzeniami (ang. event-driven programming).

Model ten opiera się na całkiem odmiennych zasadach niż model sekwencyjny, oferując dzięki nim nieporównywalnie większą elastyczność działania.

Podstawową wytyczną jest tu zmiana roli programu. Nie jest on już ciągiem kolejno podejmowanych kroków, które składają się na wykonywaną przezeń czynność, ale raczej czymś w rodzaju witryny sklepowej, z której użytkownik może wybierać pożądane w danej chwili funkcje.

Dlatego też działanie programu polega na odpowiedniej reakcji na występujące

zdarzenia (ang. events). Tymi zdarzeniami mogą być na przykład wciśnięcia klawiszy,

ruch myszy, zmiana rozmiaru okna, uzyskanie połączenia sieciowego i jeszcze wiele

innych. Program może być informowany o tych zdarzeniach i reagować na nie we

właściwy sobie sposób.

(6)

Najważniejszą cechą tego modelu programowania jest jednak samo wykrywanie zdarzeń. Otóż leży ono całkowicie poza obowiązkami programisty. Nie musi on już organizować czekania na wciśnięcie klawisza czy też wystąpienie innego zdarzenia - wyręcza go w tym system operacyjny. Programista powinien jedynie zapewnić kod reakcji na te zdarzenia, które są ważne dla pisanej przez niego aplikacji.

Schemat 37. Zdarzeniowy model działania programu. Pozycja użytkownika jest tu znacznie ważniejsza niż w modelu sekwencyjnym, gdyż poprzez zdarzenia może on w bardzo dużym stopniu

wpływać na pracę programu. Rola pośrednicząca systemu operacyjnego jest też bardziej rozbudowana.

Większość zdarzeń będzie pochodzić od użytkownika - szczególnie te związane z

urządzeniami wejścia, jak klawiaturą czy myszą. Aplikacja będzie natomiast otrzymywać informacje o nich przez cały swój czas działania, nie zaś tylko wtedy, gdy sama o to poprosi. W reakcji na owe zdarzenia program powinien wykonywać odpowiednie dla siebie czynności i zazwyczaj to właśnie robi. Ponieważ więc zdarzenia są wywoływane przez użytkownika, a aplikacja musi jedynie reagować na nie, więc sposób jej działania jest wówczas prawie całkiem dowolny. To użytkownik decyduje, co program ma w danej chwili robić - a nie on sam.

W modelu zdarzeniowym działanie programu jest podporządkowane przede wszystkim woli użytkownika.

Screen 46 i 47. Przykłady programów wykorzystujących model zdarzeniowy. Ich użytkownicy mogą je kontrolować, wywołując takie zdarzenia jak kliknięcie przycisku lub wybór opcji z menu.

Z początku może to wydawać się niezwykle ograniczające: aby program mógł coś zrobić, musi poczekać na wystąpienie jakiegoś zdarzenia. W istocie jednak wcale nie jest to niedogodnością i można się o tym przekonać, uświadamiając sobie dwa fakty.

Przede wszystkim każdy program powinien być „świadomy” warunków zewnętrznych, które mogą wpływać na jego działanie. W modelu zdarzeniowym to „uświadamianie”

przebiega poprzez informacje o zachodzących zdarzeniach; bez tego aplikacja i tak

(7)

musiałaby w jakiś sposób dowiadywać się o tych zdarzeniach, by móc w ogóle poprawnie fukcjonować.

Po drugie sytuacje, w których należy robić coś niezależnie od zachodzących zdarzeń, należą do względnej rzadkości. Jeżeli nawet jest to konieczne, system operacyjny z pewnością udostępnia sposoby, poprzez które można taki efekt osiągnąć.

A zatem model zdarzeniowy jest najbardziej optymalnym wariantem działania programu, z punktu widzenia zarówno użytkownika (pełna swoboda w korzystaniu z aplikacji), jak i programisty (zautomatyzowane wykrywanie zdarzeń i konieczność jedynie reakcji na nie). Nic więc dziwnego, że obecnie niemal wszystkie porządne programy funkcjonują w zgodzie z tym modelem. W kolejnych rozdziałach my także nauczymy się tworzenia aplikacji działających w ten sposób.

Model czasu rzeczywistego

Dla niektórych programów model zdarzeniowy jest jednak niewystarczający lub nieodpowiedni. Ich natura zmusza bowiem do ciągłej pracy i wykonywania kodu

niezależnie od zachodzących zdarzeń. O takim programach mówimy, iż działają w czasie rzeczywistym (ang. real time).

W praktyce wiele programów wykonuje podczas swej pracy dodatkowe zadania w tle (ang. background tasks), niezależne od zachodzących zdarzeń. Przykładowo, dobre edytory tekstu często dokonują sprawdzania poprawności językowej dokumentów podczas ich edycji. Podobnie niektóre systemy operacyjne przeprowadzają

defragmentację dysków w sposób ciągły, przez cały czas swego działania.

O takich aplikacjach nie możemy jednak powiedzieć, że wykorzystują model czasu rzeczywistego. Jakkolwiek różnica między nim a modelem zdarzeniowym wydaje się płynna, to za programy czasu rzeczywistego można uznać wyłącznie te, dla których czynności wykonywane w sposób ciągły są głównym (a nie pobocznym) celem działania.

Schemat 38. Model działania programu czasu rzeczywistego. Programy tego rodzaju zwykle same dbają o pobieranie odpowiednich danych od systemu operacyjnego; mogą sobie na to pozwolić, gdyż nieprzerwanie wykonują swój kod. Natomiast ich interakacja z użytkownikiem jest zwykle

dość ograniczona.

Całkiem spora liczba aplikacji działa w ten sposób, tyle że zazwyczaj trudno to zauważyć.

Należą do nich bowiem wszelkie programy działające w tle: od sterowników urządzeń, po liczniki czasu połączeń internetowych, firewalle, skanery antywirusowe, menedżery pamięci operacyjnej lub aplikacje dokonujące jakichś skomplikowanch obliczeń

naukowych. Swoją pracę muszą one wykonywać przez cały czas - niezależnie od tego, czy jest to monitoring zewnętrznych urządzeń, procesów systemowych czy też

pracochłonne algorytmy. Koncentrują na tym prawie wszystkie swoje zasoby, choć mogą naturalnie zapewniać jakąś formę kontaktu z użytkownikiem, podobnie jak programy sterowane zdarzeniami.

Zwykle też aplikacje czasu rzeczywistego tworzy się podobnie jak programy w modelu

zdarzeniowym. Uzupełnia się je tylko o pewne dodatkowe procedury, wykonywane przez

(8)

cały czas trwania programu lub też wtedy, gdy nie są odbierane żadne informacje o zdarzeniach.

Screen 48. Programy czasu rzeczywistego mogą działać w tle i wykonywać przez cały czas właściwe sobie czynności. Klient SETI@home dokonuje na przykład analizy informacji zbieranych przez

radioteleskopy w poszukiwaniu sygnałów od inteligentnych cywilizacji pozaziemskich.

Drugą niezwykle ważną (szczególnie dla nas) grupą aplikacji, które wykorzystują ten model funkcjonowania, są gry. Przez cały swój czas działania wykonują one pracę określaną w skrócie jako generowanie klatek, czyli obrazów, które są wyświetlane potem na ekranie komputera. Aby dawały one złudzenie ruchu, muszą zmieniać się wiele razy w ciągu sekundy, zatem na ich tworzenie powinien być przeznaczony cały dostępny grze czas i zasoby systemowe. Tak też faktycznie się dzieje, a generowanie klatek przeprowadza się nieustannie i bez przerwy.

Model czasu rzeczywistego jest więc najbardziej nas, przyszłych programistów gier, interesującym sposobem działania programów. Aby jednak tworzyć aplikacje oparte na tym modelu, trzeba dobrze poznać także programowanie sterowane zdarzeniami, jako że jest ono z nim nierozerwalnie związane. Umiejętność tworzenia aplikacji okienkowych w Windows jest bowiem pierwszym i niezbędnym wymaganiem, jakie jest stawiane przed adeptami programowania gier, działających w tym systemie operacyjnym.

Dalej więc poznamy bliżej ideę programowania sterowanego zdarzeniami i przyjrzymy się, jak jest ona realizowana w praktyce.

Zdarzenia i reakcje na nie

Aby program mógł wykonywać jakiś kod w reakcji na pewne zdarzenie, musi się o tym zdarzeniu dowiedzieć, zidentyfikować jego rodzaj oraz ewentualne dodatkowe informacje, związane z nim. Bez tego nie ma mowy o programowaniu sterowanym zdarzeniami.

W systemach operacyjnych takich jak DOS czy UNIX rozwiązywano ten problem w dość pokrętny sposób. Otóż jeżeli program nie miał działać sekwencyjnie, lecz reagować na niezależne od niego zdarzenia, to musiał nieustannie prowadzić monitorowanie ich potencjalnych źródeł. Musiał więc „nasłuchiwać” w oczekiwaniu na wciśnięcia klawiszy, kliknięcia myszą czy inne wydarzenia i w przypadku ich wystąpienia podejmować odpowiednie akcje. Proces ten odbywał się niezależnie do systemu operacyjnego, który

„nie wtrącał” się w działanie programu.

W dzisiejszych systemach operacyjnych, które są w całości sterowane zdarzeniami, ich

wykrywanie odbywa się już automatycznie i poszczególne programy nie muszą o to dbać.

(9)

Są one aczkolwiek w odpowiedni sposób powiadamiane, gdy zajdzie jakiegokolwiek zdarzenie systemowe.

Jak to się dzieje?… Intensywnie wykorzystywany jest tu mechanizm funkcji zwrotnych (ang. callback functions). Funkcje takie są pisane przez twórcę aplikacji, ale ich

wywoływaniem zajmuje się system operacyjny; robi to, gdy wystąpi jakieś zdarzenie.

Przy uruchamianiu programu funkcje te muszą więc być w jakiś sposób przekazane do systemu, by ten mógł je we właściwym momencie wywoływać. Przypomina to

zamawianie budzenia w hotelu na określoną godzinę: najpierw dzwonimy do recepcji, by zamówić usługę, a potem możemy już spokojnie położyć się do snu. O wyznaczonej godzinie zadzwoni bowiem telefon, którego dźwięk z pewnością wybudzi nas z drzemki.

Podobnie każdy program „zamawia usługę” powiadamiania o zdarzeniach w „recepcji”

systemu operacyjnego. Przekazuje mu przy tym wskaźnik do funkcji, która ma być wywołana, gdy zajdzie jakieś zdarzenie. Gdy istotnie tak się stanie, system operacyjny

„oddzwoni” do programu, wywołując podaną funkcję. Aplikacja może wtedy zareagować na dane zdarzenie.

Rola, jaką w tym procesie odgrywają wskaźniki do funkcji, jest bodaj ich najważniejszym programistycznym zastosowaniem.

Funkcje, które są wywoływane w następstwie zdarzeń, nazywamy procedurami zdarzeniowymi (ang. event procedures).

Rozróżnianie zdarzeń

Informacja o zdarzeniu powinna oczywiście zawierać także dane o jego rodzaju; należy przecież odróżnić zdarzenia pochodzące od klawiatury, myszy, okien, systemu plików czy jeszcze innych kategorii i obiektów. Można to uczynić kilkoma drogami, a wszystkie mają swoje wady i zalety.

Pierwszy z nich zakłada obecność tylko jednej procedury zdarzeniowej, która dostaje informacje o wszystkich występujących zdarzeniach. W takim wypadku konieczne są dodatkowe parametry funkcji, poprzez które przekazywany będzie rodzaj zdarzenia (np.

jako odpowiednia stała wyliczeniowa) oraz ewentualne dane z nim związane. W treści takiej procedury wystąpią zapewne odpowiednie instrukcje warunkowe, dzięki którym podjęte zostaną akcje właściwe danym rodzajom zdarzeń.

Inny wariant zakłada zgrupowanie podobnych zdarzeń w taki sposób, aby o ich

wystąpieniu były informowane oddzielne procedury. Przy tym rozwiązaniu można mieć osobne funkcje reagujące na zdarzenia myszy, osobne dla obsługi klawiatury itp.

Trzeci sposób wiąże się ze specjalnymi procedurami dla każdego zdarzenia. Gdy go wykorzystujemy, o każdym rodzaju zdarzeń (wciśnięcie klawisza, kliknięcie przyciskiem myszy, zakończenie programu itd.) dowiadujemy się poprzez wywołanie unikalnej dla niego procedury zdarzeniowej. Jednemu zdarzeniu odpowiada więc jedna taka procedura.

Jeżeli chodzi o wykorzystanie w praktyce, to stosuje się zwykle pierwszą lub trzecią możliwość. Pojedyncza procedura zdarzeniowa występuje w programowaniu Windows przy użyciu jego API - poznamy ją jeszcze w tym rozdziale. Osobne procedury dla każdego możliwego zdarzenia są natomiast częste w wizualnych środowiskach programistycznych, takich jak C++ Builder czy Delphi.

Fundamenty Windows

Windows jest systemem operacyjnym znanym chyba wszystkim użytkownikom

komputerów i nie tylko. Chociaż wiele osób narzeka na niego z różnych powodów, nie

sposób nie docenić jego roli w rozwoju komputerów. To w zasadzie dzięki Windows trafiły

one pod strzechy.

(10)

Pierwsza wersja tego systemu (oznaczona numerem 1.0) została wydana niemal dwadzieścia lat temu - w listopadzie 1985 roku. Dzisiaj jest to już więc zamierzchła prehistoria, a przez kolejne lata doczekaliśmy się wielu nowych wydań tego systemu.

Od samego początku posiadał on jednak graficzny interfejs oparty na oknach oraz wiele innych cech, które będą kluczowe przy tworzeniu aplikacji pracujących na nim.

Screen 49. Menedżer programów w Windows 3.0. Seria 3.x Windows była, obok Windows 95, tą, która przyniosła systemowi największą część z obecnej popularności.

(screen pochodzi z serwisu Nathan’s Toasty Technology)

O dokładnej historii Windows możesz przeczytać w internetowym serwisie Microsoftu.

Tym fundamentom Windows poświęcimy teraz nieco uwagi.

Okna

W Windows najważniejsze są okna; są na tyle ważne, że system wziął od nich nawet swoją nazwę. Chociaż więc pomysł zamknięcią interfejsu użytkownika w takie

prostokątne obszary ekranu pochodzi od MacOS’a, jego popularyzację zawdzięczamy przede wszystkim systemowi z Redmond. Dzisiaj okna występują w każdym graficznym systemie operacyjnym, od Linuxów po QNX.

Intuicyjnie za okno uważamy kolorowy prostokąt „zawierający program”. Ma on obramowanie, kilka przycisków, pasek tytułu oraz ewentualnie inne elementy. Dla systemu pojęcie to jest jednak szersze:

Okno (ang. window) to w systemie Windows dowolny element graficznego interfejsu użytkownika.

Oznacza ono, że za swoiste okna są uważane także przyciski, pola tekstowe, wyboru i

inne kontrolki. Takie podejście może się wydawać dziwne, sztuczne i nielogiczne, jednak

ma uzasadnienie programistyczne, o którym rychło się dowiemy,

(11)

Hierarchia okien

Jeżeli za okno uznamy każdy element GUI, wtedy dostrzeżemy także, że tworzą one hierarchię: pewne okno może być nadrzędnym dla innego, podrzędnego.

Na szczycie tej hierarchii widnieje pulpit - okno, które istnieje przez cały czas działania systemu. Bezpośrednio podległe są mu okna poszczególnych aplikacji (lub inne okna systemowe), zaś dalej hierarchia może sięgać aż do pojedynczych kontrolek (przycisków itd.).

Screen 50 i Schemat 39. Przykładowa hierarchia okien

Dzięki takiemu porządkowi Windows może w prawidłowy sposob kontrolować zachowania okien - począwszy od ich wyświetlania, a kończąc na przekazywaniu doń komunikatów (o czym będziemy mówili niedługo).

Aplikacje i procesy

Żadne okno w systemie Windows nie istnieje jednak samo dla siebie. Zawsze musi być ono związane z jakimś programem, a dokładniej z jego instancją.

Instancją programu (ang. application instance) nazywamy pojedynczy egzemplarz uruchomionej aplikacji.

Uruchomienie programu z pliku wykonywalnego EXE pociąga więc za sobą stworzenie jego instancji. Do niej są następnie „doczepianie” kolejno tworzone przez aplikację okna.

Gdy zaś działanie programu dobiegnie końca, są one wszystkie niszczone.

O tworzeniu i niszczeniu okien powiemy sobie w następnym podrozdziale.

Oprócz tego uruchomiony program egzystuje w pamięci operacyjnej w postaci jednego

(najczęściej) lub kilku procesów (ang. processes). Cechą szczególną procesu w Windows

jest to, iż posiada on wyłączną i własną przestrzeń adresową. Dostęp do tej

(12)

przestrzeni jest zarezerowowany tylko i wyłącznie dla niego - wszelkie inne próby nieuprawnionego odczytu lub zapisu spowodują wyjątek.

Warto też przypomnieć, że Windows jako system 32-bitowy używa płaskiego modelu adresowania pamięci. Każdy proces może więc teoretycznie posiadać cztery gigabajty pamięci operacyjnej do swej wyłącznej dyspozycji. W praktyce zależy to oczywiście od ilości zamontowanej w komputerze pamięci fizycznej oraz wielkości pliku wymiany.

Dynamicznie dołączane biblioteki

Jedną z przyczyn sukcesu Windows jest łatwość obsługi programów pracujących pod kontrolą tego systemu. Każda aplikacja wygląda tu podobnie, posiada zbliżony interfejs użytkownika. Nauka korzystania z nowego programu nie oznacza więc przymusu

poznawania nowych elementów interfejsu, które w ogromnej większości są takie same w każdym programie.

Pamiętajmy jednak, że każdy interfejs użytkownika wymaga odpowiedniego kodu, zajmującego się jego wyświetlaniem, reakcją na kliknięcia i wciśnięcia klawiszy oraz innymi jeszcze aspektami funkcjonowania. GUI występujące w Windows nie jest tu żadnym wyjątkiem, a skoro każdy program okienkowy korzysta z tego interfejsu, musi mieć dostęp do wspomnianego kodu.

Nierozsądne byłoby jednak zakładanie, że każda aplikacja posiada jego własną kopię.

Pomijając już marnotrawstwo miejsca na dysku i w pamięci, które by się z tym wiązało, trzeba zauważyć, że łatwo mogłyby to prowadzić do konfliktów w zakresie wersji

systemu. Najprawdopodobniej należałoby wtedy całkiem zapomnieć o kompatybilności wstecz, a każda aplikacja działaby tylko na właściwej sobie wersji systemu operacyjnego.

Problemy te są bardzo poważne i doczekały się równie poważnego rozwiązania.

Lekarstwem na te bolączki są mianowicie dynamicznie dołączane biblioteki.

Dynamicznie dołączane biblioteki (ang. dynamically linked libraries, w skrócie DLL), zwane też bibliotekami DLL lub po prostu DLL’ami, są skompilowanymi modułami, zawierającymi kod (funkcje, zmienne, klasy itd.), który może być wykorzystywany przez wiele programów jednocześnie. Kod ten istnieje przy tym tylko w jednej kopii - zarówno na dysku, jak i w pamięci operacyjnej.

Biblioteki takie istnieją w postaci plików z rozszerzeniem .dll i są zwykle umieszczone w katalogu systemowym

97

, względnie w folderach wykorzystujących je aplikacji.

Udostępniają one (eksportują) zbiory symboli, które mogą być użyte (zaimportowane) w programach pracujących w Windows.

Z punktu widzenia programisty C++ korzystanie z kodu zawartego w bibliotekach DLL nie różni się wiele od stosowania zasobów Biblioteki Standardowej lub też funkcji w rodzaju system(), getch() czy rand(). Różnica polega na tym, że biblioteki DLL nie są

statycznie dołączane do pliku wykonywalnego aplikacji, lecz linkowane dynamicznie (stąd ich nazwa) w czasie działania programu. W pliku EXE muszą się jedynie znaleźć informacje o nazwach wykorzystywanych bibliotek oraz o symbolach, które są z nich importowane. Dane te są automatycznie zapisywane przez kompilator jako tzw. tabele importu.

Wyodrębnienie kluczowego kodu systemu Windows w postaci bibliotek DLL likwiduje zatem wszystkie dolegliwości związane z jego wykorzystaniem w aplikacjach. Mechanizm dynamicznych bibliotek pozwala ponadto na tworzenie innych, własnych skarbnic kodu, które mogą być współużytkowane przez wiele programów. W takiej postaci istnieje na

97 W Windows 9x jest to \WINDOWS\SYSTEM\, w Windows NT zaś \WINDOWS\SYSTEM32\.

(13)

przykład platforma DirectX czy moduł FMod, które będziemy w przyszłości wykorzystywać, pisząc swoje gry.

Windows API

Kod, który będziemy wykorzystywać w tworzeniu aplikacji okienkowych, nosi ogólną nazwę Windows API. API to skrót od Applications’ Programmed Interface, czyli

„interfejsu programowanego aplikacjami”.

Windows API (czasem zwane Win32API lub po prostu WinAPI) to zbiór funkcji, typów danych i innych zasobów programistycznych, pozwalający tworzyć programy działające w trybie graficznym pod kontrolą systemu Windows.

Nauka pisania programów okienkowych polega w dużej mierze na przyswojeniu sobie umiejętności posługiwania się tą biblioteką. Temu właśnie celowi będą podporządkowane najbliższe rozdziały niniejszego kursu, łącznie z aktualnym.

Na początek aczkolwiek przyjrzymy się WinAPI jako całości i poznamy kilka przydatnych zasad, wspomagających programowanie z użyciem jego zasobów.

Biblioteki DLL

Windows API jest zawarte w bibliotekach DLL. Wraz z kolejnymi wersjami systemu bibliotek tych przybywało - pojawiły się moduły odpowiedzialne za multimedia, komunikację sieciową, Internet i jeszcze wiele innych.

Najważniejsze trzy z nich były jednak obecne od samego początku

98

i to one tworzą zasadniczą część interfejsu programistycznego Windows. Są to:

¾ kernel32.dll - w niej zawarte są funkcje sterujące jądrem (ang. kernel) systemu, zarządzające pamięcią, procesami, wątkami i innymi niskopoziomowymi

sprawami, które są kluczowe dla funkcjonowania systemu operacyjnego.

¾ user32.dll - odpowiada za graficzny interfejs użytkownika, czyli za okna - ich wyświetlanie i interaktywność.

¾ gdi32.dll - jest to elastyczna biblioteka graficzna, pozwalająca rysować skomplikowane kształty, bitmapy oraz tekst na dowolnym rodzaju urządzeń wyjściowych. Zapoznamy się z nią w rozdziale 3, Windows GDI.

Każda z tych bibliotek eksportuje setki funkcji. Bogactwo to może przyprawić o zawrót głowy, ale wkrótce przekonasz się, że korzystanie z niego jest całkiem proste. Poza tym, jak wiadomo, od przybytku głowa nie boli ;-)

Pliki nagłówkowe

Biblioteki DLL są zasadniczo przeznaczone dla samych programów; z nich czerpią one kod potrzebnych funkcji Windows API. Dla nas, programistów, ważniejsze są

mechanizmy, które pozwalają użyć tychże funkcji w C++.

I tu spotyka nas miła niespodzianka: wykorzystanie WinAPI w aplikacjach pisanych w C++ przebiega bowiem podobnie, jak stosowanie modułów Biblioteki Standardowej.

Wymaga mianowicie dołączenia odpowiedniego pliku nagłówkowego.

Tym plikiem jest windows.h. Dołączając go, otrzymujemy dostęp do wszystkich

podstawowych i części bardziej zaawansowanych funkcji Windows API. Warto przy tym podkreślić, że nawet owe „podstawowe” funkcje pozwalają tworzyć rozbudowane i

skomplikowane aplikacje, a dla programistów chcących pisać głównie gry w DirectX będą one znacznie więcej niż wystarczające.

98 Chociaż nie zawsze były 32-bitowe i ich nazwy nie kończyły się na 32.

(14)

Jeszcze przed dołączeniem (za pomocą dyrektywy #include ) nagłówka windows.h dobrze jest zdefiniować (poprzez #define) makro WIN32_LEAN_AND_MEAN. Wyłączy to niektóre rzadziej używane fragmenty API, zmniejszając rozmiar powstałych plików

wykonywalnych i skracając czas potrzebny na ich zbudowanie. Będę stosował tę sztuczkę we wszystkich programach przykładowych, w których będzie to możliwe.

windows.h wewnętrznie dołącza także wiele innych plików nagłówkowych, z których najważniejszymi są:

¾ windef.h, zawierający definicje typów (głównie strukturalnych) używanych w Windows API

¾ winbase.h, który udostępnia funkcje jądra systemu (z biblioteki kernel32.dll)

¾ winuser.h, odpowiedzialny za interfejs użytkownika (czyli bibliotekę user32.dll)

¾ wingdi.h, udostępniający moduł graficzny GDI (biblioteka gdi32.dll) Oprócz tych nagłówków istnieje także całe mnóstwo rzadziej używanych,

odpowiadających na przykład za programowanie sieciowe (winsock.h) czy też obsługę multimediów (winmm.h). Będziesz je dołączął, jeżeli zechcesz skorzystać z bardziej zaawansowanych możliwości systemu Windows.

O funkcjach Windows API

Większą część wymienionych plików nagłówkowych stanowią prototypy funkcji,

używanych w programach okienkowych. Jest ich przynajmniej kilkaset, zgrupowanych w kilkakanaście zespołów zajmujących się poszczególnymi aspektami systemu

operacyjnego.

Mnogość tych fukcji nie powinna jednak przerażać. Znaczy ona przede wszystkich to, iż Windows API jest niezwykle potężnym narzędziem, które oferuje wiele przydatnych możliwości. Tak naprawdę bardzo niewiele jest czynności, których wykonanie przy pomocy tego ogromnego zbioru jest niemożliwe.

Naturalnie nie zawsze tak było. W ciągu tych kilkunastu lat istnienia systemu Windows jego API cały czas się rozrastało i ulegało poszerzeniu o nowe intrumenty i funkcje. Z czasem wprowadzono lepsze sposoby realizacji tych samych czynności; konsekwencją tego jest częsta obecność dwóch wersji funkcji realizujących to samo zadanie.

Jedna z nich jest wariantem podstawowym (ang. basic), wykonującym swoją pracę w pewien określony, domyślny sposób. Funkcje takie można poznać po ich zwyczajnych nazwach, jak na przykład CreateWindow() (stworzenie okna), ReadFile() (odczytanie danych z pliku), ShellExecute() (uruchomienie/otwarcie jakiegoś obiektu), itp.

Ponadto istnieją też bardziej zaawansowane, rozszerzone (ang. extended) wersje niektórych funkcji. Poznać je można po przyrostku Ex, a także po tym, iż przyjmują one większą ilość danych jako swoje parametry

99

. Pozwalają tym samym ściślej określić sposób wykonania danego zadania. Rozszerzonymi kuzynami poprzednio wymienionych funkcji są więc CreateWindowEx(), ReadFileEx() oraz ShellExecuteEx().

Atoli nie wszystkie funkcje mają swe rozszerzone odpowiedniki - wręcz przeciwnie, większość z nich takowych nie posiada. Jeżeli jednak występują, wówczas zalecane jest używanie właśnie ich. Są to bowiem nowsze wersje funkcji, które mogą wykonywać zlecone sobie zadania nie tylko w bardziej elastyczny, ale też w ogólnie lepszy (czyli wydajniejszy, bezpieczniejszy itp.) sposób. Kiedy więc stajemy przed podobnym wyborem, pamiętajmy, że:

99 Nie musi to od razu oznaczać, że przyjmują one większą liczbę parametrów. Niektóre (jak np.

ShellExecuteEx()) żądają zamiast tego obszernej struktury, przekazanej jako parametr.

(15)

Użycie rozszerzonych funkcji Windows API (z nazwami zakończonymi na Ex) jest pożądane wszędzie tam, gdzie mogą one zastąpić swoje podstawowe wersje.

Podobne, choć bardziej szczegółowe zalecenia występują też w niemal każdym opisie rozszerzonej funkcji w MSDN. Dlatego też w tym kursie powyższa rada będzie

skrupulatnie przestrzegana.

Jest jeszcze jedne przyrostek w nazwie funkcji, który ma specjalne znaczenie - chodzi o Indirect (‘pośrednio’). Funkcje z tym zakończeniem różnią się od swych zwykłych krewniaków tym, że zamiast kilku(nastu) parametrów przyjmują strukturę, zawierającą pola dokładnie odpowiadające tymże parametrom.

Obiektowość symulowana przy pomocy uchwytów

Możliwe, że dziwisz się, dlaczego jest tu mowa tylko o funkcjach WinAPI, a ani słówkiem nie są wspomniane klasy, z których mogłaby składać się ta biblioteka. Czyżby więc nie korzystała ona z dobrodziejstw programowania obiektowego?…

W dużej mierze jest to prawdą. Większość składników Windows API została napisana w języku C, zatem nie może wykorzystywać obiektowych możliwości języka C++. Nie można jednakże powiedzieć, iż jest to biblioteka strukturalna - jej twórców nie zniechęciła bowiem ułomność języka programowania i zdołali z powodzeniem zaimplementować obiektowy projekt w zgoła nieobiektowym środowisku.

Nie da się ukryć, że było to niezbędne. Mnóstwo koncepcji Windows (z oknami na czele) daje się bowiem sensownie przedstawić jedynie za pomocą technik zbliżonych do OOP.

W języku C++ obiekty obsługujemy najczęściej poprzez wskaźniki na nie. Gdy wywołujemy ich metody, używamy składni w rodzaju:

obiekt->metoda (parametry);

Dla kompilatora jest to prawie zwyczajne wywołanie funkcji, tyle że z dodatkowym parametrem, który wewnątrz owej funkcji (metody) jest potem reprezentowany poprzez this. „Prawdziwa” postać powyższej instrukcji mogłaby więc wyglądać tak:

metoda (obiekt, parametry);

Taką też składnię mają wszystkie „metodopodobne” funkcje Windows API, operujące na oknach, plikach, blokach pamięci, procesach czy innych obiektach systemowych.

Istnieje tu jednak pewna różnica. Otóż biblioteka WinAPI nie może sobie pozwolić na udostępnianie programiście wskaźników do swych wewnętrznych struktur danych;

mogłoby to skończyć się błędami o nieprzewidzianych konsekwencjach. Stosuje tu więc inną technikę: obiekty są użyczane koderowi poprzez swoje uchwyty.

Uchwyt (ang. handle) to unikalny liczbowy identyfikator obiektu, za pomocą którego można na tym obiekcie wykonywać operacje udostępniane przez funkcje biblioteczne.

Cała gama funkcji wykorzystuje uchwyty. Niektóre z nich tworzą obiekty i zwracają je w wyniku - tak robi na przykład CreateWindowEx(), tworząca okno. Inne służą do

wykonywania określonych działań na obiektach, a kolejne odpowiadają wreszcie za ich niszczenie i sprzątanie po nich.

Jakkolwiek więc uchwyty nie są wskaźnikami, widać spore podobieństwo między

obydwiema konstrukcjami. Dotyczy ono także konieczności zwalniania obiektów

reprezentowanych przez uchwyty, gdy już nie będą nam potrzebne. Należy używać do

tego odpowiednich funkcji, z których większość poznamy wkrótce.

(16)

Niezwolnienie obiektu poprzez jego uchwyt prowadzi do zjawiska wycieku zasobów (ang. resource leak), które jest przynajmniej tak samo groźne jak wyciek pamięci w przypadku wskaźników.

Ostatnią cechą wspólną z wskaźnikami jest specjalne traktowanie wartości NULL, czyli zera. Jako uchwyt nie reprezentuje ona żadnego obiektu, zatem pełni identyczną rolę, jak pusty wskaźnik.

Typy danych

W nagłówkach Windows API widnieje, oprócz prototypów funkcji, także bardzo wiele deklaracji nowych typów danych. Spora część z nich to struktury, które w programowaniu Windows są używane bardzo często.

Większość jednak jest tylko aliasami na typy podstawowe, głównie na liczbę całkowitą bez znaku. Nadmiarowe nazwy dla takich typów mają jednak swoje uzasadnienie:

pozwalają łatwiej orientować się, jakie jest znaczenie danego typu oraz jaką dokładnie rolę pełni. Jest to szczególnie ważne, gdy nie można definiować własnych klas.

Warto więc przyjrzeć się, w jaki sposób tworzone są nazwy typów w Windows. Zacznijmy najpierw od najbardziej podstawowych, będących głównie prostymi przezwiskami

znanych nam z C++ rodzajów danych.

Otóż w WinAPI posiadają one swoje dodatkowe miana, które tym tylko różnią się od oryginalnych, że są pisane w całości wielkimi literami

100

. Konwencja ta dotyczy zresztą każdego innego typu:

W Windows API typy danych mają nazwy składające się wyłącznie z wielkich liter.

Mamy zatem typy CHAR, FLOAT, VOID czy DOUBLE.

Dodatkowo dla wygody programisty zdefiniowano aliasy na liczby bez znaku:

nazwa właściwy typ opis

BYTE unsigned char bajt (8-bitowa liczba całkowita bez znaku) UINT unsigned int liczba całkowita bez znaku i określonego rozmiaru DWORD unsigned long długa (32-bitowa) liczba całkowita bez znaku

WORD unsigned short krótka (16-bitowa) liczba całkowita bez znaku

Tabela 13. Typy liczb całkowitych bez znaku w Windows API

Istnieje również pokaźny zbiór typów wskaźnikowych, które powstają poprzez dodanie przedrostka P (lub LP) do nazwy typu podstawowego. Jest więc typ PINT, PDWORD, PBYTE i jeszcze mnóstwo innych.

Zaprezentowane tu nazwy typów są także stosowane w bibliotece DirectX, a zatem nie rozstaniemy się z nimi zbyt szybko i warto się do nich przyzwyczaić :) Obficie występują bowiem w dokumentacjach obu bibliotek, a także w innych pomocniczych narzędziach programistycznych dla Windows.

Niemal wszystkie pozostałe typy, których nazwy biorą się od znaczenia w programach, są aliasami na typ DWORD, czyli 32-bitową liczbę całkowitę bez znaku. Wśród nich poczesne miejsce zajmują wszlkiego typu uchwyty; kilka najważniejszych, które spotkasz

najwcześniej i najczęściej, wymienia poniższa tabelka:

100 Pewnym wyjątkiem od tej reguły jest typ BOOL, będący aliasem na int, a nie na bool. Powód takiego nazewnictwo stanowi chyba jedną z najbardziej tajemniczych zagadek Wszechświata ;D

(17)

typ uchwyt do… uwagi

HANDLE — uniwersalny uchwyt do czegokolwiek

HWND okna jednoznacznie identyfikuje każde okno w systemie HINSTANCE instancji programu program otrzymuje go od systemu w funkcji WinMain()

HDC kontekstu

urządzenia można na nim wykonywać operacje graficzne z modułu Windows GDI

HMENU menu reprezentuje pasek menu okna (jeżeli jest)

Tabela 14. Podstawowe typy uchwytów w Windows API

Jak łatwo zauważyć, wszystkie typy uchwytów mają nazwy zaczynające się na H.

Pełną listę typów danych występujących w WinAPI wraz z opisami znajdziesz rzecz jasna w MSDN.

Dokumentacja

Względnie dobra znajomość nazw typów pojawiających się w Windows API jest bardzo przydatna, gdy chcemy korzystać z dokumentacji tej biblioteki. Ta dokumentacja jest bowiem najlepszym źródłem wiedzy o WinAPI.

Z początku miała ona formę pojedynczej publikacji Win32 Programmers’ Reference (zarówno w postaci papierowej, jak i elektronicznej) i traktowała wyłącznie o

podstawowych i średniozaawansowanych aspektach programowania Windows. Dzisiaj jako Platform SDK jest ona częścią MSDN.

To cenne źródło informacji jest domyślnie instalowane wraz z Visual Studio .NET, zatem z pewnością posiadasz już do niego dostęp (bardzo możliwe, że korzystałeś z niego już wcześniej w tym kursie). Teraz będzie ono dla ciebie szczególnie przydatne.

Najczęstszym powodem, dla którego będziesz doń sięgać, jest poznanie zasady działania i użycia konkretnej funkcji. Potrzebne opisy łatwo znaleźć przy pomocy Indeksu; można też po prostu umieścić kursor w oknie kodu na nazwie interesującej funkcji i wcisnąć F1.

Dokumentacja poszczególnych funkcji ma też tę zaletę, iż posiada jednolitą strukturę w każdym przypadku.

Screen 51. Opis funkcji Sleep() w MSDN. Jest to chyba najprostsza funkcja Windows API; powoduje ona wstrzymanie działania programu na podaną liczbę milisekund.

(18)

Opis funkcji w MSDN składa się więc kolejno z:

¾ krótkiego wprowadzenia, przedstawiającego ogólnikowo sposób działania funkcji

¾ prototypu, z którego można dowiedzieć o liczbie, nazwach oraz typach parametrów funkcji oraz o typie zwracanej przezeń wartości

¾ dokładnego opisu znaczenia każdego parametru, w kolejności ich występowania w deklaracji

Na początku każdego opisu w nawiasach kwadratowych widnieje zwykle oznaczenie jego roli. in oznacza, że dany parametr jest wejściową daną dla funkcji; out - że poprzez niego zwracana jest jakaś wartość wyjściowo; retval (tylko razem z out) - że owa wartość jest jednocześnię tą, którą funkcją zwraca w „normalny” sposób.

¾ informacji o wartości zwracanej przez funkcję. Jest tu podane, kiedy rezultat może być uznany za poprawny, a kiedy powinien być potraktowany jako błąd.

¾ dodatkowych uwag (Remarks) co do działania oraz stosowania funkcji

¾ przykładowego kodu, ilustrującego użycie funkcji

¾ wymaganiach systemowych, które muszą być spełnione, by można było

skorzystać z funkcji. Jest tam także informacja, w której bibliotece funkcja jest zawarta i w jakim pliku nagłówkowym widnieje jej deklaracja.

¾ odsyłaczy do pokrewnych tematów

Ten standard dokumentacji okazał się na tyle dobry, że jest wykorzystywany niezwykle szeroko - także w projektach niezwiązanych nijak z Windows API, Microsoftem, C++, ani nawet z systemem Windows. Nic w tym dziwnego, gdyż z punktu widzenia programisty jest on bardzo wygodnym rozwiązaniem. Przekonasz się o tym sam, gdy sam zaczniesz intensywnie wykorzystywać MSDN w praktyce koderskiej.

***

Ten podrozdział był dość wyczerpującym wprowadzeniem w programowanie aplikacji okienkowych w Windows. Postarałem się wyjaśnić w nim wszystkie ważniejsze aspekty Windows i jego API, abyś dokładnie wiedział, w co się pakujesz ;D

W następnym podrozdziale przejdziemy wreszcie do właściwego kodowania i napiszemy swoje pierwsze prawdziwe aplikacje dla Windows.

Pierwsze kroki

Gdy znamy już z grubsza całą programistyczną otoczkę Windows, czas wreszcie spróbować tworzenia aplikacji dla tego systemu. W tym podrozdziale napiszemy dwa takie programy: pierwszy pokaże jedynie prosty komunikat, ale za to w drugim stworzymy swoje pierwsze pełnowartościowe okno!

Jak najszybciej rozpocznijmy zatem właściwe programowanie dla środowiska Windows.

Najprostsza aplikacja

Od początku kursu napisałeś już pewnie całe mnóstwo programów, więc nieobca jest ci czynność uruchomienia IDE i stworzenia nowego projektu. Tym razem jednak muszą w niej zajść niewielkie zmiany.

Zmieni się nam mianowicie rodzaj projektu, który mamy zamiar stworzyć. Porzucamy

przecież programy konsolowe, a chcemy kreować aplikacje okienkowe, działające w

trybie graficznym. W opcjach projektu, na zakładce Application Settings, w pozycji

Application type wybieramy zatem wariant Windows application:

(19)

Screen 52. Opcje projektu aplikacji okienkowej

Jako że tradycyjnie zaznaczamy także pole Empty project, po kliknięciu przycisku Finish nie zobaczymy żadnych widocznych różnic w stosunku do projektów programów

konsolowych. Nasza nowa aplikacja okienkowa jest więc na razie całkowicie pusta.

Kompilator wie aczkolwiek, że ma tutaj do czynienia z programem funkcjonującym w trybie GUI.

Zmianę podsystemu z GUI na konsolę lub odwrotnie można przeprowadzić, wyświetlając właściwości projektu (pozycja Properties z menu podręcznego w Solution Explorer), przechodząc do sekcji Linker|System i wybierając odpowiednią pozycję na liście w polu SubSystem (Windows /SUBSYSTEM:WINDOWS dla programów okienkowych lub Console /SUBSYSTEM:CONSOLE dla aplikacji konsolowych).

Trzeba jednakże pamiętać, że oba rodzaje projektów wymagają innych funkcji startowych: dla konsoli jest to main(), a dla GUI WinMain(). Brak właściwej funkcji objawi się natomiast błędem linkera.

Dodajmy teraz do projektu nowy moduł main.cpp i wpiszmy do niego ten oto kod:

// MsgBox - okno komunikatu

#define WIN32_LEAN_AND_MEAN

#include <windows.h>

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpszCmdLine, int nCmdShow)

{

MessageBox (NULL, "Oto nasz pierwszy program w Windows!", "Komunikat", NULL);

return 0;

}

Nie jest on ani specjalnie długi, ani szczególnie zawiły, gdyż jest to listing bodaj

najprostszej możliwej aplikacji dla Windows. Wykonywane przezeń za danie także nie jest

bardzo złożone - pokazuje ona bowiem poniższe okno z komunikatem:

(20)

Screen 53. Okno prezentujące pewien komunikat

Znika ono zaraz po kliknięciu przycisku OK, a to kończy również cały program. Nie zmienia to jednak faktu, że oto wyświetliliśmy na ekranie swoje pierwsze (na razie skromne) okno, żegnając się jednocześnie z czarno-białą konsolą; nie było tu po niej najmniejszego śladu. Możemy zatem z absolutną pewnością stwierdzić, iż napisany przez nas program jest rzeczywiście aplikacją okienkową!

Pokrzepieni tą motywującą wiadomością możemy teraz przystąpić do oględzin kodu naszego krótkiego programu.

Niezbędny nagłówek

Listing rozpoczyna się dwoma dyrektywami dla preprocesora:

#define WIN32_LEAN_AND_MEAN

#include <windows.h>

Z tej pary konieczna jest oczywiście fraza #include. Powoduje ona dołączenie do kodu pliku nagłówkowego windows.h, zawierającego (bezpośrednio lub pośrednio) deklaracje niemal wszystkich symboli składających się na Windows API.

Plik ten jest więc dość spory, a rzadko mamy okazję skorzystać choćby z większości określonych tam funkcji, typów i stałych. Niezależnie od tego nazwy wszystkich tych funkcji będą jednak włączone do tabeli importu wynikowego pliku EXE, zajmując w nim nieco miejsca.

Zapobiegamy temu w pewnym stopniu, stosując drugą dyrektywę. Jak widać definiuje ona makro WIN32_LEAN_AND_MEAN, nie wiążąc z nim żadnej konkretnej wartości.

Makro to ma aczkolwiek swoją rolę, którą dobrze oddaje jego nazwa - w swobodnym tłumaczeniu: „Windows chudy i skąpy”. Zdefiniowanie go powoduje mianowicie wyłączenie kilku rzadko używanych mechanizmów WinAPI, przez co skompilowany program staje się mniejszy.

Dyrektywa #define dla tego makra musi się koniecznie znaleźć przed #include,

dołączającym windows.h. W tymże nagłówku umieszczony jest bowiem szereg instrukcji

#if i #ifdef , które uzależniają kompilację pewnych jego fragmentów od tego, czy omawiane makro nie zostało wcześniej zdefiniowane. Powinno więc ono być określone zanim jeszcze preprocesor zajmie się przetwarzaniem nagłówka windows.h - i tak też dzieje się w naszym kodzie.

Musisz wiedzieć, że z tego użyteczego makra możesz korzystać w zdecydowanej większości zwykłych programów okienkowych, a także w aplikacjach wykorzystujących biblioteki DirectX. Będzie ono występować również w przykładowych programach.

Funkcja WinMain()

Dalszą i zdecydowanie największą część programu zajmuje funkcja WinMain(); jest to

jednocześnie jedyna procedura w naszej aplikacji. Musi więc pełnić w niej rolę wyjątkową

i tak jest w istocie: oto bowiem punkt startowy i końcowy dla programu. WinMain() jest

zatem windowsowym odpowiednikiem funkcji main().

(21)

Łatwo też zauważyć, że postać tej funkcji jest znacznie bardziej skomplikowana niż main(). Prototyp wygląda bowiem następująco:

int WINAPI WinMain(HINSTANCE hInstance,

HINSTANCE hPrevInstance,

LPSTR lpszCmdLine,

int nCmdShow);

Analizując go od początku, widzimy, że funkcja zwraca wartość typu int. Ten sam typ może zwracać także funkcja main() (chociaż nie musi

101

), a zwany jest kodem wyjścia.

Informuje on podmiot uruchamiający nasz program o skutkach jego wykonania. Przyjęła się tu konwencja, że 0 oznacza wykonanie bez przeszkód, natomiast inna wartość jest sygnałem jakiegoś błędu.

To dokładnie odwrotnie niż w przypadku funkcji Windows API, które będziemy sami wywoływać (WinMain() wywołuje bowiem system operacyjny). Tam zerowy rezultat jest sygnałem błędu - dzięki temu możliwe jest wykorzystanie tej wartości w charakterze warunku instrukcji if , zgadza to się także z zasadą, iż pusty uchwyt (uchwyty są często zwracane przez funkcje WinAPI) ma numer 0 .

Do pobierania kodu błędu służy zaś oddzielna funkcja GetLastError(), o której powiemy sobie w swoim czasie (ewentualnie sam sobie o niej poczytasz we właściwym źródle :D).

Kolejną częścią prototypu jest tajemnicza fraza WINAPI, o której pewnie nikt nie ma pojęcia, do czego służy ;) W rzeczywistości jest to proste makro zdefiniowane jako:

#define WINAPI __stdcall

Zastępuje ono słowo kluczowe __stdcall, oznaczające konwencję wywołania funkcji WinMain(). Sposób stdcall jest standardową drogą do wywołania tej funkcji, a także wszystkich procedur całego Windows API (o czym wszak nie trzeba wiedzieć, aby móc je poprawnie stosować). Makro WINAPI (czasem zastępowane przez APIENTRY) jest więc koniecznym i niezbędnym składnikiem sygnatury WinMain(), którym aczkolwiek nie potrzeba się szczególnie przejmować :)

Z pewnością najbardziej interesujące są parametry funkcji WinMain(), prezentujące się w bardzo słusznej liczbie czterech sztuk. Nie wszystkie są równie istotne i przydatne, a znaczenie każdego opisuje poniższa tabelka:

typ nazwa opis

HINSTANCE hInstance

hInstance to uchwyt instancji naszego programu. Jest to więc liczba jednoznacznie identyfikująca uruchomiony

egzemplarz aplikacji. To ogromnie ważna i niezwykle przydatna wartość, wymagana przy wywołaniach wielu funkcji Windows API i tak fundamentalnych czynnościach jak np. tworzenie okna. Warto zatem zapisać ją w miejscu,

z którego będzie dostępna w całej aplikacji (choćby wzmiennej globalnej lub statycznym polu klasy).

HINSTANCE hPrevInstance Parametr hPrevInstance jest już wyłącznie reliktem przeszłości, pochodzącym z czasów 16-bitowego systemu

Windows. Wówczas zawierał on uchwyt do ewentualnej

101 Mówiąc ściśle, to jednak musi :) Ogromna większość kompilatorów akceptuje oczywiście funkcję main() ze zwracanym typem void, ale Standard C++ głosi, że jedyną przenośną jej wersją jest int main(int argc, char* argv[]);. Ponieważ jednak nie zajmujemy się już konsolą, możemy nie rozstrząsać dalej tego problemu i skoncentrować się raczej na funkcji WinMain().

(22)

typ nazwa opis

poprzedniej instancji uruchamianego programu. Obecnie jest on zawsze uchwytem pustym, a więc zawiera wartość

NULL, czyli zero.

Gdy chcemy wykryć wielokrotne uruchamianie naszej aplikacji, musimy zatem posłużyć się innymi mechanizmem. Najczęściej przegląda się listę procesów aktywnych w systemie lub też szuka innego egzemplarza

głównego okna aplikacji przy pomocy funkcji FindWindow().

[ L]PSTR lpszCmdLine

Są to argumenty wiersza poleceń, podane mu podczas jego uruchamiania. Ten sposób podawania danych jest już

Windows rzadko stosowany, gdyż wymaga albo uruchomienia konsoli, albo utworzenia skrótu do programu. Niemniej znajomość parametrów, z jakimi

wywołano program bywa przydatna; lpszCmdLine przechowuje je jako łańcuch znaków w stylu C, który

można przypisać do zmiennej typu std::string i operować na nim wedle potrzeb.

Zauważmy, że jest to jeden ciąg znaków. Funkcja main() zwykła bowiem rozbijać go na pojedyncze parametry oddzielone spacją lub ujęte w cudzysłowy. Podobnego

rozbicia można zresztą w prosty sposób dokonać samodzielnie na tekście podanym w lpszCmdLine.

int nCmdShow

Parametr ten określa sposób wyświetlenia głównego okna aplikacji. Jest on najczęściej ustawiany we właściwościach skrótu do programu i może przyjmować

wartość równą jednej z kilku stałych, które poznamy w następnym rozdziale. Parametrem nCmdShow można więc sugerować się przy wyświetlaniu okna programu, ale nie

jest to obowiązkowe (choć wskazane).

Tabela 15. Parametry funkcji WinMain()

Widać z niej, że z nie wszystkich parametrów będziemy zawsze korzystać (z jednego nawet nigdy). W takich wypadkach warto skorzystać z możliwości, jaką oferuje C++, tzn.

pominięcia nazwy niewykorzystywanego parametru. Skrócimy w ten sposób nagłówek funkcji WinMain().

Okno komunikatu i funkcja MessageBox()

W bloku WinMain(), a więc we właściwych instrukcjach składających się na nasz program, dokonujemy jedynego wywołania funkcji Windows API. Jest nią funkcja

MessageBox(), której należy bez wątpienia przyjrzeć się bliżej. Uczyńmy to w tej chwili.

Prototyp

Nagłówek tej funkcji jawi się następująco:

int MessageBox(HWND hWindow,

LPCTSTR lpText,

LPCTSTR lpCaption,

UINT uFlags);

(23)

Można zauważyć, że przyjmuje ona cztery parametry, których przeznaczenie zwyczajowo zaprezentujemy w odpowiedniej tabelce:

typ nazwa opis

HWND hWindow

W parametrze tym podajemy uchwyt do okna nadrzędnego względem okna komunikatu. Zwykle używa się do tego aktywnego okna aplikacji, lecz można także użyć NULL (np.

wtedy, gdy nie stworzyliśmy jeszcze własnego okna) - wówczas komunikat nie podlega żadnemu oknu.

LPCTSTR

102

lpText

Tekst komunikatu, który ma być wyświetlony. Jest to stały łańcuch znaków w stylu C, który może być podany dosłownie lub np. odczytany ze zmiennej typu std::string przy pomocy

jej metody c_str().

LPCTSTR lpCaption

Tutaj podajemy tytuł okna, którym będzie opatrzony nasz komunikat; jest to taki sam łańcuch znaków jak sam tekst

wiadomości. W tym parametrze można również wstawić wskaźnik pusty (o wartości NULL, czyli zero), a wtedy zostanie

użyty domyślny (i najczęściej nieadekwatny) tytuł "Error" .

UINT uFlags

Są to dodatkowe parametry okna komunikatu, które określają m.in. zestaw dostępnych przycisków, wyrównanie tekstu, ewentualną ikonkę itd. Zostaną one omówione w dalszej

części paragrafu. Ten parametr może także przyjąć wartość zerową, a wówczas w oknie komunikatu pojawi się jedynie

przycisk OK.

Tabela 16. Parametry funkcji MessageBox()

Wynika z niej, iż pierwszy i ostatni parametr niniejszej funkcji może zostać pominięty poprzez wpisanie doń zera (NULL). Wtedy też pokazane okno jest najprostszym

możliwym w Windows sposobem na przekazanie użytkownikowi jakiejś informacji.

Wiadomość taką może on jedynie zaakceptować, wciskając przycisk OK - tak też dzieje się w naszej przykładowej aplikacji MsgBox.

Na tym jednakże nie kończą się możliwości funkcji MessageBox(). Reszta ukrywa się bowiem w jej czwartym parametrze - czas więc przyjrzeć się niektórym z tych opcji.

Opcje okna komunikatu

Ostatni parametr funkcji MessageBox(), nazwany tutaj uFlags, odpowiada za kilka aspektów wyglądu oraz zachowania pokazywanego okna komunikatu. Można w nim mianowicie ustawić:

¾ rodzaj przycisków, jakie pojawią się w oknie

¾ domyślny przycisk (jeżeli do wyboru jest kilka)

¾ ikonkę, jaką ma być opatrzony komunikat

¾ modalność okna komunikatu

¾ sposób wyświetlania (wyrównanie) tekstu zawiadomienia

¾ parametry samego okna komunikatu

Bogactwo opcji jest zatem spore i aż dziw bierze, w jaki sposób mogą się one „zmieścić”

w jednej liczbie całkowitej bez znaku. Jest to jednak możliwe, ponieważ każdej z tych opcji przypisano stałą (której nazwa rozpoczyna się od MB_) o odpowiedniej wartości,

102 Typ LPCTSTR jest wskaźnikiem do ciągu znaków, czyli zasadniczo napisem w stylu C. Może on być jednak zarówno tekstem zapisanym znakami ANSI (typu char), jak i znakami Unicode (typu wchar_t). To, na który typ LPCTSTR jest aliasem, zostaje ustalone podczas kompilacji: gdy jest zdefiniowane makro UNICODE, wtedy staje się on typem const wchar_t*, w przeciwnym wypadku - const char*.

(24)

mającej w zapisie binarnym tylko jedną jedynkę na właściwym sobie miejscu. Dzięki temu poszczególne opcje (tzw. flagi) można „składać” w całość, posługując się do tego operatorem alternatywy bitowej |

103

. W identyczny sposób jest to rozwiązane w innych funkcjach Windows API (i nie tylko), w których jest to konieczne.

Mechanizm ten nazywamy kombinacją flag bitowych, a jest on szerzej opisany w Dodatku B, Reprezentacja danych w pamięci.

Przykładowe użycie tego rozwiązania wygląda choćby tak:

MessageBox (NULL, "To jest komunikat", "Okno komunikatu", MB_OK | MB_ICONINFORMATION);

Użyto w nim dwóch możliwych flag opcji: jedna określa zestaw dostępnych przycisków, a druga ikonkę widoczną w oknie komunikatu. Rzeczone okno wygląda zaś mniej więcej w ten sposób:

Screen 54. Przykładowe okno komunikatu z przyciskiem OK i ikonką informacyjną

Oddane do dyspozycji programisty flagi dzielą się zaś, jak to wykazaliśmy na początku, na kilka grup.

Do (naj)częściej używanych należą zapewne opcje określające zestaw przycisków, które pojawią się w wyświetlonym oknie komunikatu. Domyślnie zbiór ten składa się wyłącznie z przycisku OK, ale dopuszczalnych wariantów jest nieco więcej, co obrazuje poniższa tabelka:

flaga przyciski uwagi MB_OK OK Użytkownik może wyłącznie przeczytać

komunikat i zamknąć go, klikając w OK. Jest to domyślne ustawienie.

MB_OKCANCEL OK, Anuluj Daje prawo wyboru zaakceptowania lub odrzucenia działań zaproponowanych przez

program.

MB_RETRYCANCEL

Ponów próbę, Anuluj

Zestaw wyświetlany zwykle wtedy, gdy jakaś operacja (np. odczyt z wymiennego dysku) nie

powiodła się, ale można spróbować przeprowadzić ją ponownie.

MB_ABORTRETRYIGNORE

Przerwij, Ponów próbę, Zignoruj

Bardziej elastyczny wariant poprzedniego rozwiązania, stosowany przy złożonych procesach, z których pewne etapy mogą się nie powieść. Pozwala on użytkownikowi nie tylko na ponowną próbę lub zakończenie całego procesu,

lecz także zignorowanie błędu.

103 Dopuszczalne jest także użycie operatora dodawania, czyli plusa - w przypadku potęg dwójki, a takimi wartościami są właśnie flagi, będzie on miał takie samo działanie jak alternatywa bitowa. Nie jest on jednak zalecany, jako że jego podstawowe przeznaczenie jest zupełnie inne.

Cytaty

Powiązane dokumenty

Dość niezwykły pomysł nieprawego zdobycia pieniędzy okazuje się jeszcze bardziej za- wikłany w chwili, kiedy ojciec młodego żołnierza unosi się honorem i nie chce

Kształcąc się w kierunku zarządza- nia w ochronie zdrowia, należy więc stale poszukiwać możliwości doskonalenia.. Młodzi Menedżerowie Me- dycyny to organizacja, która

Normą w całej Polsce stał się obraz chylącego się ku upadkowi pu- blicznego szpitala, który oddaje „najlepsze” procedury prywatnej firmie robiącej kokosy na jego terenie..

Nie jest to zresztą jedyny paradoks, inny odnosi się do tego, że część podmiotu sama nie jest możliwa do reprezentowania, a przecież, jak już wiemy, podmiot wyłania

Kandydaci na prezydenta (z jednym wyjątkiem) prześcigali się w przekonywaniu swoich potencjal- nych wyborców, że najlepszym gwarantem ich bez- pieczeństwa zdrowotnego jest

Podpisując umowę na budowę gazociągu bałtyckiego, niemiecki koncern chemiczny BASF i zajmujący się między innymi sprzedażą detalicznym odbiorcom gazu EON zyskały

Zdrowa micha z krewetkami 26 zł na półmisku: krewetki, bajgle pełnoziarniste, kremowy serek śmietankowy, kiełki rzodkiewki, pomidorki koktajlowe, ogórek, papryka, mix

Grupa II – kolekcja Centrum Sztuki Współczesnej Znaki Czasu (Toruń).