Sławomir Osiak Zasady Programowania w Windows Wprowadzenie. Dla kogo jest ta książka przeznaczona i jak należy ją czytać? Programować w Windows nie jest łatwo. Nawet ci, którzy od wielu lat zajmują się programowaniem zawodowo, mają trudności w przejściu na nowy system. Nie dziwi więc, że w pełni zaawansowane podręczniki programowania w Windows liczą najczęściej grubo ponad tysiąc stron oraz że dostępne są tylko w języku angielskim. Każdy, kto myśli poważnie o przygotowywaniu aplikacji Windows, będzie musiał, prędzej czy później, po jedną z takich pozycji sięgnąć. Niemniej jednak wcześniej dobrze jest się do tak poważne lektury przygotować, zapoznając się z podstawowymi ideami nowego środowiska oraz różnicami, jakie występują między tradycyjnym programowaniem a programowaniem w Windows. Przybliżenie powyższych zagadnień stanowi cel tej książki. Jest więc ona przeznaczona dla wszystkich tych osób, które albo rozpoczynają, albo niedawno rozpoczęły tworzyć aplikacje Windows - niezależnie od tego, czy są one przeznaczone dla wersji 3.1, czy 95 - ale nie czują się jeszcze w tej dziedzinie specjalistami. Prosty język i praktyczne podejście do tematu sprawiają, że do kręgu odbiorców publikacji zaliczyć można nie tylko zawodowych programistów, ale także uczniów i studentów poznających informatykę w szkołach lub na uczelniach oraz osoby traktujące programowanie jedynie jako hobby. Jestem przekonany, że wszyscy oni po przeczytaniu tej pozycji będą znać sposób funkcjonowania systemu Windows, że zostaną wyczuleni na bardzo specyficzną filozofię "okien", tak bardzo różniącą się od tego wszystkiego, do czego IBM PC przyzwyczaił nas wcześniej. Ponieważ zadaniem tej książki jest przede wszystkim objaśnienie uniwersalnych zasad tworzenia aplikacji Windows, niezależnych od konkretnego pakietu czy nawet języka programowania, podczas podejmowania decyzji co do rodzaju używanego środowiska pracy wybór padł na pakiet "Turbo Pascal for Windows" firmy Borland. Pakiet ten, jak mało który, łączy prostotę z profesjonalnością i wysokimi walorami dydaktycznymi. Należy jednak pamiętać o tym, że większość informacji podanych w książce ma charakter uniwersalny i dlatego może znaleźć zastosowanie także podczas pracy w dowolnym innym środowisku programowania. Zawarte w tej książce informacje dotyczące osobliwości programowania w Windows są omawiane na przykładzie prawie pięćdziesięciu konkretnych aplikacji (działających zarówno w środowisku 3.1, jak i 95). Tekst źródłowy podanych przykładów i ich wersje skompilowane znajdują się na dołączonej do książki dyskietce (informacje na jej temat podane są w dalszej części wprowadzenia). Dzięki temu unika się konieczności mozolnego wprowadzania instrukcji programowania oraz ryzyka przypadkowego popełnienia błędu. Wymagania stawiane Czytelnikowi są następujące: - umiejętność obsługi systemu Windows w wersji 3.1 (3.11 ) lub 95, - znajomość języka programowania Pascal, - opanowanie podstawowych reguł programowania obiektowego. Nie jest natomiast konieczna znajmość zasad obsługi poszczególnych programów wchodzących w skład pakietu "Turbo Pascal for Windows". Ponieważ kolejne fragmenty książki wykorzystują w wysokim stopniu informacje przekazane we wcześniejszych rozdziałach, książkę należy czytać sekwencyjnie. Oczywiście po zakończeniu czytania całej książki jak najbardziej możliwy jest powrót do jej wybranych fragmentów. W szybkim odnalezieniu potrzebnych informacji pomoże z pewnością dokładny spis treści oraz obszerny skorowidz. Poszczególne hasła skorowidza są uporządkowane w kolejności alfabetycznej ich najbardziej znaczących fragmentów, co oznacza a przykład, że funkcję "API" "TextOut" należy szukać jako hasło "«TextOut», funkcja API", zaś metodę "TApplication.InitMainWindow" - jako "«InitMainWindow» metoda obiektu «TApplication»". Omówienie treści. Oprócz wprowadzenia książka zawiera 9 rozdziałów i skorowidz. Rozdział 1, "Dlaczego Windows?", ma charakter wprowadzający i jest próbą spojrzenia na system Windows oczami użytkownika. Można tu więc znaleźć zarówno krótki opis rozwoju tego systemu, od chwili jego powstania aż do dnia dzisiejszego, jak i odpowiedź na pytanie, które zalety środowiska Windows zadecydowały o jego tak ogromnej popularności oraz jakie istotne nowości wnosi on w stosunku do systemu operacyjnego DOS. Rozdział 2, "Podstawowe zasady programowania", zawiera krótkie omówienie najważniejszych idei programowania w Windows, które tak bardzo odróżniają je od przygotowywania aplikacji działających w środowisku DOS-a. Mowa jest tu więc o sposobie realizowania wielozadaniowości i związanej z nią obsłudze komunikatów, o zarządzaniu oknami, bibliotekach Windows udostępniających tak zwane funkcje "API" oraz zasadach korzystania z zasobów aplikacji, zarówno programowych, jak i sprzętowych. Rozdział 2 kończy si omówieniem najważniejszych pakietów przeznaczonych do tworzenia aplikacji Windows. W rozdziale 3, "Pierwsza aplikacja Windows", rozpoczyna się właściwe programowanie. Pierwszy program jest oczywiście bardzo prosty, ale ma już wszelkie atrybuty aplikacji Windows: okno z paskiem tytułowym, system menu, którego obsługę ułatwia możliwość stosowania skrótów klawiszowych (ang. "hotkeys"), oraz ikonę. Program ten potrafi już także przetwarzać niektóre komunikaty Windows, dzięki czemu wykrywa naciśnięcie przycisku myszy oraz wybór dwóch poleceń menu - wszystkie te zdarzenia powodują wyświtlanie prostych okien dialogowych. Jednym z najważniejszych zadań każdego programu jest wyświetlanie danych wyjściowych. Zagadnienie to stanowi temat rozdziału 4, "Wyświetlanie tekstu i rysunków". Rozdział ten zawiera nie tylko omówienie najważniejszych funkcji graficznych systemu Windows, ale także objaśnienie różnic, jakie istnieją między sposobem wyświetlania informacji w środowiskach DOS-a oraz Windows. Z rozdziału 4 można się między innymi dowiedzieć, co to jest kontekst wyświetlania i dlaczego pełni on w Windows tak ogromną rol, jakie niebezpieczeństwa wiążą się ze zmianą palety kolorów, z czego wynika konieczność odtwarzania zawartości ekranu oraz jakie znaczenie ma tak zwany prostokąt nieważny i prostokąt obcinający. Najbardziej przekonującą wizytówką Windows są jego okna. Ale jakie są ich rodzaje, jakimi określa się je atrybutami i w jaki sposób komunikują się ze sobą? Co to jest specyfikacja "MDI" oraz czym okno aplikacji różni się od okna dokumentu? W jaki sposób można korzystać z pasków przewijania w celu umożliwienia wyświetlenia danych, które nie mieszczą się w całości w obszarze roboczym okna? Odpowiedzi na te i na wiele innych pytań dotyczących okien Windows znaleźć można w rozdziale 5, "Zarządzanie oknai w aplikacjach Windows". Cóż warte by były aplikacje Windows bez okien dialogowych i występujących na nich elementów sterujących, takich jak przyciski, pola edycji, listy, pola wyboru i przełączniki? Sposobom opisywania okien dialogowych w pliku z opisem zasobów, ich tworzeniu i obsłudze, a także metodom wymiany danych między aplikacją a jej oknami dialogowymi poświęcony jest rozdział 6, "Obsługa okien dialogowych". Ostatni fragment tego rozdziału zawiera krótki opis dwóch standardowych okien dialogowych zdefiniowanych w paiecie "Turbo Pascal for Windows", pozwalających na wprowadzanie tekstu oraz dokonanie wyboru pliku. Wprawdzie w środowisku Windows wymiana informacji między programami a użytkownikiem dokonuje się najczęściej w oparciu o okna dialogowe, menu oraz paski narzędzi, niekiedy jednak zachodzi potrzeba samodzielnego rozpoznania przez aplikację naciśnięcia klawisza lub operacji wykonanej za pomocą myszy, takiej jak na przykład podwójne kliknięcie. Interfejs graficzny użytkownika nie jest też w stanie zastąpić zegara systemowego, umożliwiającego wykonywanie przez aplikację określonych działań w pewnych staych odstępach czasu, takich jak chociażby wyświetlanie bieżącej godziny. O tym, jak zadania te realizuje się programowo, traktuje rozdział 7, "Wczytywanie danych: klawiatura, mysz i zegar". Rozdział 8, "Korzystanie z plików i drukowanie", poświęcony jest zagadnieniom wykorzystania plików. Wprawdzie operacje na plikach wykonuje się w systemie Windows za pomocą tych samych funkcji i procedur, które stosuje się w środowisku DOS-a, niemniej jednak także i w tym przypadku konieczne jest dostosowanie się do pewnych specyficznych wymogów stawianych przez środowisko wielozadaniowe, jakim jest przecież Windows. Szczególnie dotkliwe utrudnienia napotyka programista podczas przygotowywania funkcj umożliwiających wykonywanie wydruków, których w pełni profesjonalna realizacja jest zadaniem niełatwym - być może właśnie dlatego zagadnienie to jest lekceważone w większości podręczników programowania w Windows. W rozdziale 8 znaleźć można objaśnienie sposobu drukowania zarówno tekstu, jak i rysunków. W rozdziale 9, ostatnim, "Standardowe aplikacje pakietu TPW", prezentowany jest sposób skorzystania z dwóch standardowych okien aplikacji zdefiniowanych w pakiecie "TPW". Rozdział ten może posłużyć albo jako materiał ułatwiający powtórzenie i utrwalenie zdobytych wcześniej informacji, albo jako bodziec do rozwinięcia podanych przykładów w celu utworzenia własnych aplikacji. Uwagi dotyczące dyskietki. Do książki jest dołączona dyskietka zawierająca tekst źródłowy i wynikowy wszystkich omawianych wersji aplikacji Windows. Zdecydowana większość plików nosi nazwy rozpoczynające się od liter "PAW" (program "Pierwsza aplikacja Windows") lub "MDI" (program "Okna dokumentów"), za którymi następują cyfry zgodne z odpowiadającymi im podrozdziałami książki. Na przykład plik "PAW423.PAS" zawiera tekst źródłowy programu, który został przygotowany do podrozdziału 4.2.3, "Wyrównywanie tekstu", włącznie, zaś plk "PAW423.EXE" - wersję wynikową tego programu. Numerów pozbawione są tylko te programy, które występują w wersjach pojedynczych. Są to: "PAL" (4.5.2, "Odtwarzanie kolorów"), "EDIT" (9.1, "Edycja tekstu ASCII") i "FILE" (9.2, "Edycja plików ASCII"). Oprócz tego na dyskietce znajdują się pliki z rozszerzeniem "RC" i "RES", zawierające wersję źródłową i wynikową opisu zasobów przygotowywanych aplikacji, pliki "PAW_M.*" i "MDI_M.*" z definicjami stałych określających wartości identyfikatorów poleceń menu, a także plik tekstowy "TEKST.TXT", usprawniający korzystanie z końcowych wersji programu "Okna dokumentów" ("MDI*.PAS"). Wszystkie programy zamieszczone na dyskietce można uruchomić zarówno w środowisku Windows 3.1, jak i Windows 95, bez konieczności wykonywania jakichkolwiek działań wstępnych. Niemniej jednak polecane jest wcześniejsze utworzenie na dysku odrębnego katalogu i przekopiowanie do niego plików z dyskietki. Wiele przyjemności w poznawaniu tajników nowego środowiska życzy Sławomir Osiak Poznań, dn. 15.11.96 Rozdział 1. Dlaczego Windows? Jeszcze nie tak dawno wielu malkontentów było przekonanych, że Windows to w gruncie rzeczy nic więcej niż ładnie wyglądajaca zabawka, która, owszem, jest w stanie zainteresować hobbystów komputerowych, ale w poważnych zastosowaniach długo się jeszcze w Polsce nie przyjmie. Tymczasem rzeczywistość okazała się zgoła inna. Trudno dzisiaj w naszym kraju znaleźć taki komputer, na którym nie byłby zainstalowany system Windows, czy to w wersji 3.1, czy 95. Nie brak też licznych aplikacji Windows w języku plskim, będacych zarówno spolszczonymi wersjami programów zachodnich, jak i całkowicie rodzimymi produktami. Co spowodowało, że podobnie jak na całym świecie, również i u nas środowisko Windows zyskało tak ogromną popularność? Ksiażkę tę rozpoczynamy od próby znalezienia odpowiedzi na to właśnie pytanie. 1.1. Od początków do dnia dzisiejszego. 1.1.1. Trochę historii. Wszystko zaczęło się kilkanaście lat temu, w listopadzie 1983 roku, kiedy to firma Microsoft zapowiedziała utworzenie pakietu programowego o nazwie "Windows". Jego pierwsza wersja przeznaczona do sprzedaży, oznaczona numerem "1.01", pojawiła się dwa lata później, by w przeciągu dwóch następnych lat doczekać się kilku uaktualnień, mających między innymi na celu dostarczenie dodatkowych programów obsługi monitorów i drukarek. W listopadzie 1987 roku została skierowana do sprzedaży wersja "Windows 2.0", w której wprowadzono kilka zmian do interfejsu użytkownika, tak aby był on zgodny z mającym się ukazać w następnym roku systemem "OS/2". W praktyce oznaczało to przede wszystkim pojawienie się możliwości kaskadowego układania okien, czyli takiego, w którym okna na siebie zachodzą (ang. "cascade"), a nie tylko - jak poprzednio - znajdują się jedno obok drugiego (ang. "tile"). Windows 2.0 oferował także wygodniejszy sposób krzystania z klawiatury i myszy, szczególnie w zakresie obsługi list menu i okien dialogowych. Następnym ważnym momentem w historii Windows było pojawienie się (krótko po Windows 2.0) systemu oznaczonego nazwą "Windows/386", który dzięki korzystaniu z trybu wirtualnego mikroprocesora 386 umożliwiał równoczesne wykonywanie i przedstawianie za pomocą okien wielu programów DOS-a wykonujących działania bezpośrednio na sprzęcie. W celu ujednolicenia oznaczeń nazwę Windows 2.1 zamieniono wówczas na "Windows/286". Prawdziwą furorę środowisko Windows zaczęło jednak robić dopiero po pojawieniu się w maju 1990 roku wersji "3.0", która nie tylko skupiła w sobie osiągnięcia dwóch poprzednich wersji, /286 oraz /386, ale także zawierała wiele nowych rozwiązań, takich jak pismo proporcjonalne, trójwymiarowe cieniowanie czy kolorowe ikony. Całkowicie została odnowiona postać programów użytkowych wchodzących w skład pakietu. Ponadto specjalistom z firmy Microsoft udało się zapewnić Windows i jego aplikacjom dostęp do 1 megabajtów pamięci, a w przypadku dysponowania procesorem 386 lub lepszym - możliwość korzystania z wirtualnej przestrzeni adresowej o wielkości czterokrotnie większej niż fizycznie zainstalowana pamięć w systemie. 1.1.2. Stan obecny. Pakiet Windows 3 został sprzedany w liczbie ponad 500 tysięcy egzemplarzy w przeciągu pierwszych sześciu tygodni od momentu pojawienia się na rynku, co znacznie przewyższyło wcześniej spotykane tego rodzaju rekordy. Ale firma Microsoft pracowała już wówczas nad następną wersją swego przeboju - systemem "Windows 3.1". Jego wprowadzenie w 1992 roku nie stało się już wprawdzie tak wielkim wydarzeniem, jak miało to miejsce w przypadku wersji poprzednich, niemniej jednak spotkało się z bardzo życzliwym pzyjęciem ze strony użytkowników, którzy od razu docenili polepszenie szaty graficznej oraz modyfikację programów użytkowych pakietu. Szczególnie duże znaczenie miało dokonanie istotnych zmian w programie zarządzającym plikami, na którego funkcjonowanie padały wcześniej liczne skargi. Pewne rozwinięcie Windows 3.1 stanowi "Windows 3.11 dla grup roboczych", system umożliwiający tworzenie prostych lokalnych sieci komputerowych, nie wymagających serwera, a przez to bardziej atrakcyjnych cenowo od najpopularniejszej w Polsce sieci "Novell Netware". Oprócz wprowadzenia dodatkowych aplikacji sieciowych do systemu, w Windows 3.11 został nieznacznie poprawiony interfejs użytkownika pozostałych programów pakietu, szczególnie poprzez dodanie pasków narzędzi. W roku 1994 byliśmy świadkami kolejnego ważnego wydarzenia w historii Windows, jakim było pojawienie się na rynku systemu "Windows NT". Jest to pełen, 32-bitowy system operacyjny o dużo bogatszych funkcjach niż Windows 3.1, między innymi umożliwiający pracę w sieci. Jednak ze względu na wysokie wymagania sprzętowe jego sprzedaż następuje dość powoli. Nie bez znaczenia jest tu też z pewnością fakt, że zdecydowana większość użytkowników systemu Windows nie potrzebuje tak rozbudowanej wersji jak NT. Ogromny rozgłos towarzyszył pojawieniu się we wrześniu 1995 roku najmłodszego dziecka firmy Microsoft, systemu "Windows 95". Jako rozwinięcie wersji 3.1, ale jednocześnie nie tak bardzo rozbudowane jak NT, środowisko to jest przeznaczone dla szerokiego kręgu odbiorców. Jego główną zaletą jest wyższy poziom aplikacji użytkowych wchodzących w skład pakietu oraz pojawienie się programów ukierunkowanych na korzystanie z sieci komputerowych, w tym także coraz bardziej popularnej sieci Internet. Ponadto uepszeniu uległa realizacja wielozadaniowości, jak również wprowadzona została możliwość tak zwanej wielowątkowej realizacji programów, czyli współbieżnego wykonywania kilku modułów tego samego programu. System Windows 95 nie jest jednak niestety pozbawionych istotnych wad. Należą do nich przede wszystkim znacznie zwiększone wymagania sprzętowe, dużo wyższe niż podawane w oficjalnych komunikatach firmy Microsoft, oraz znaczna liczba błędów i niedociągnięć. Oba te mankamenty sprawiły, że nowy system przyjmowany jest przez użytkowników z dużą rezerwą. Nie brak nawet takich, którzy zrezygnowali z Windows 95 już po jego zainstalowaniu, po to, by wrócić do wersji poprzedniej, 3.1 lub 3.11 . Czy wersja Windows 95 będzie ostatnia? Z pewnością nie, chociaż trudno w tej chwili dokładnie prognozować dalszy rozwój tego systemu. Pewnego rodzaju konkurencję dla niego może stanowić zapowiadany coraz częściej rozwój prostych komputerów sieciowych, służących do połączenia się z globalną siecią teleinformatyczną i korzystających z dostępnego w niej oprogramowania. W każdym razie nikt dzisiaj z pewnością nie odważy się już zaprzeczyć, że system Windows stał się najpopularniejszym środowiskiem grafiznym komputerów działających w oparciu o "MS-DOS". Od momentu pojawienia się na rynku w 1985 roku został on sprzedany w wielu milionach egzemplarzy na całym świecie, żeby nie wspomnieć o znacznie większej, choć nie do kóńca znanej, liczbie jego kopii pirackich. Jedno jest pewne: Windows wyznaczył nowe kierunki rozwoju komputerów "IBM PC", od którego nie ma już odwrotu. 1.1.3. Programy działające w środowisku Windows. W ślad za tak szybkim rozwojem Windows rośnie także liczba jego aplikacji, czyli programów w pełni do niego przystosowanych; liczba ta obecnie wyraża się już w tysiącach. Do najbardziej znanych spośród aplikacji Windows należą: edytor tekstów "Microsoft Word for Windows", arkusz kalkulacyjny "Microsoft Excel", pakiet graficzny firmy Corel, baza danych "Paradox for Windows" firmy Borland, program "DTP PageMaker" firmy Aldus. Nie ma już praktycznie takiej dziedziny programowania, w której nie można byznaleźć przynajmniej jednej aplikacji Windows wysokiej klasy. Obecnie sytuacja w naszym kraju praktycznie nie odbiega od sytuacji panującej w najwyżej rozwiniętych krajach świata. Gwałtownie rośnie liczba spolszczonych wersji programów zachodnich, coraz więcej powstaje też aplikacji tworzonych przez polskich informatyków. Produkty rodzime nie są wprawdzie tak rozbudowane, jak ich odpowiedniki amerykańskie, nad którymi pracują przecież ogromne sztaby specjalistów, ale są za to tańsze, a przede wszystkim - w pełni dostosowane do wymagań polskich użytkowników. Jeeli zaś chodzi o ich możliwości, to okazują się one wystarczające dla zdecydowanej większości zastosowań. Mimo iż system Windows został stworzony przede wszystkim w celu umożliwienia wykonywania programów napisanych specjalnie do niego, to jednak umożliwia on też korzystanie z programów działających w środowisku DOS-a. Oczywiście programy takie nie wykorzystują wielu udogodnień, jakie oferuje swym użytkownikom Windows, ale w większości sytuacji mogą być przedstawiane w postaci okna i wykonywane równocześnie z aplikacjami Windows. W środowisku Windows 95 istnieje nawet możliwość przydzielenia programom DS-a prostego paska narzędzi oraz korzystania przy ich obsłudze z myszy. W czasach, kiedy Windows działał na komputerach 286 (niekiedy, aczkolwiek niezmiernie rzadko, zdarza się to również i dzisiaj) istotny był podział programów DOS-a na dwie grupy, z których pierwsza zawierała tak zwane dobre, druga zaś - złe aplikacje. Nazewnictwo to nie odnosiło się do jakości programów, lecz do sposobu, w jaki korzystały one ze sprzętu komputera. Dobrymi aplikacjami były te, które w celu wczytywania danych z klawiatury i wyświetlania ich na ekranie monitora stosowały przerwania progamowe "DOS-a" i "BIOS-a", natomiast złymi - takie, które przesyłały dane bezpośrednio do monitora, wykorzystując grafikę albo obsługując przerwania sprzętowe klawiatury. Dla systemu Windows zainstalowanego na komputerze z procesorem 386 lub lepszym rodzaj programu DOS-a nie ma żadnego znaczenia - Windows potrafi każdy program traktować na tych samych zasadach, co swe własne aplikacje. Jeżeli natomiast korzysta on z procesora 286, to nie ma możliwości przydzielenia okna złym aplikacjom i równoczesnego wykonywania ich z innymi programami. 1.2. Zalety Windows. 1.2.1. Graficzny interfejs użytkownika. Wszystkie aplikacje Windows mają taki sam wygląd i identyczny sposób obsługi, zwany graficznym interfejsem użytkownika ("GUI"), dzięki czemu łatwiej jest je poznawać i stosować niż tradycyjne programy DOS-a. Kiedy już wiemy, jak obsługiwać jedną aplikację Windows, jesteśmy w stanie łatwo nauczyć się korzystać z innej. Do innych zalet graficznego interfejsu użytkownika należą: - bogate możliwości korzystania z myszy, co pozwala na wprowadzanie danych za pomocą menu oraz takich obiektów graficznych, jak ikony, przyciski czy paski przewijania; - wyświetlanie danych w trybie graficznym, który umożliwia przekazywanie w przystępny sposób znacznie większej ilości informacji niż tryb tekstowy, a ponadto pozwala na wyświetlanie "WYSIWYG", czyli takie, w którym tekst i rysunki widoczne na ekranie wyglądają, tak jak po ich wydrukowaniu; - atrakcyjność wizualna środowiska, przejawiająca się w efektownej grafice oraz interesującym zestawieniu kolorów. Korzyści wynikające ze stosowania graficznego interfejsu użytkownika zostały także dostrzeżone przez producentów innego sprzętu komputerowego. Jeszcze zanim powstał system Windows, zastosowane w nim zasady zostały wykorzystane przez firmę Apple, najpierw na komputerze Lisa, a następnie - Macintosh, wprowadzonym na rynek w styczniu 1984 roku. Jeśli popularne "jabłuszko" stanowi silną konkurencję dla "IBM-a", to nie z powodu rozwiązań sprzętowych, lecz dzięki bardzo łatwemu i atrakcyjnemu sposobowi obługi . Inne firmy także nie pozostały w tyle. Dzisiaj graficzny interfejs użytkownika stosowany jest między innymi w komputerach Amiga, Atari, a także na sprzęcie z zainstalowanym Unixem. Mimo iż poszczególne środowiska graficzne różnią się co do szczegółów, to jednak podstawowe zasady ich funkcjonowania są takie same. Dla milionów użytkowników komputerów na całym świecie są one czymś tak oczywistym, że nie mogą oni pojąć, jak to jest możliwe, iż wielu właścicieli PC nadal zadowala się samym tylko DOS-em. 1.2.2. Wiele programów jednocześnie. Twórcy systemu operacyjnego DOS nie przewidzieli w zasadzie jednoczesnego wykorzystywania więcej niż jednego programu. Wprawdzie programiści znaleźli na to radę, przygotowując programy rezydentne, takie jak SideKick, ale korzystanie z nich związane było zawsze z dużą liczbą ograniczeń i pułapek, nie wspominając już o trudnościach występujących podczas pisania tych programów. Niemniej jednak ich ogromna popularność dowiodła, że ze strony użytkowników PC istnieje duże zapotrzebowanie na wielozadaniowoć. Środowisko Windows umożliwia jednoczesne uruchomienie wielu programów, z których każdy zajmuje na ekranie prostokątne okno. Użytkownik może w bardzo łatwy sposób uaktywniać kolejno różne programy i przekazywać między nimi dane. Windows pozwala nawet na jednoczesne uruchamianie wielu programów DOS-a, a także - na wielokrotne uruchomienie pojedynczego programu, o ile on sam tego nie zabrania. 1.2.3. Koniec problemów z pamięcią. Użytkownicy korzystający z DOS-a doskonale znają problemy związane z korzystaniem z większej ilości pamięci. W momencie projektowania pierwszego komputera "IBM PC" 640 kilobajtów pamięci wydawało się ilością ogromną i w zupełności wystarczającą do wszystkich ówczesnych zastosowań. W miarę upływu czasu rósł jednak apetyt programów na pamięć, w związku z czym konieczne stawało się instalowanie coraz większych jej ilości. Dzisiaj 16 MB RAM-u należy już do standardowego wyposażenia komputera. Niestety krzystanie z pamięci o adresach znajdujących się powyżej progu 640 kB wymaga stosowania specjalnych technik, z których większość programów DOS-a nie potrafi korzystać. W efekcie często zdarza się, że wykorzystywana jest tylko mała część dostępnej pamięci operacyjnej. To dziwaczne ograniczenie pamięci wynikające z architektury PC zostało w skuteczny sposób wyeliminowane w systemie Windows. Dzięki wykorzystaniu rozkazów trybu chronionego mikroprocesorów 80286 i 80386 Windows oraz jego aplikacje uzyskały dostęp do 16 megabajtów pamięci zainstalowanej w systemie. Dalsze możliwości powstały w momencie pojawienia się procesorów 486 i Pentium. Oprócz tego wprowadzenie samodzielnego zarządzania pamięcią przez system Windows pozwoliło zorganizować pamięć pomocniczą na dyku. Dzięki temu komputery wyposażone w procesor 386 lub lepszy mają możliwość korzystania z pamięci wirtualnej, której wielkość może być wielokrotnie większa niż fizycznie zainstalowana pamięć w systemie. Ale to jeszcze nie wszystko. Windows zawiera w sobie wiele mechanizmów wpływających na efektywne wykorzystywanie pamięci. System ten potrafi usuwać z niej fragmenty programów, a następnie ponownie ładować je z plików "EXE". Ponadto wszystkie uruchomione kopie tego samego programu dzielą w pamięci wspólny kod, mając jedynie oddzielne bloki danych. Co więcej, aplikacje Windows mogą korzystać w czasie działania z procedur umieszczonych w innych plikach, tak zwanych bibliotekach dynamicznych ("DLL"). Sa system Windows składa się prawie wyłącznie z tego rodzaju bibliotek. Jest rzeczą zrozumiałą, że uruchamianie, wykonywanie i kończenie programów w środowisku wielozadaniowym powoduje powstawanie zjawiska fragmentacji pamięci. Lecz Windows potrafi sobie świetnie poradzić także i z tym problemem. Poprzez odpowiednie przenoszenie bloków programów i danych łączy ze sobą wolne obszary pamięci, umożliwiając w ten sposób ich pełne wykorzystywanie. 1.2.4. Obsługa urządzeń zewnętrznych. Nawet jeśli system Windows uruchamia się z poziomu DOS-a tak samo, jak każdy inny program użytkowy (ma to miejsce w przypadku stosowania wersji 3.1), to jednak w trakcie działania przejmuje on na siebie wiele zadań systemu operacyjnego. Także i wtedy, gdy nie jest pełnym systemem operacyjnym, gdyż odpowiedzialność za zarządzanie zasobami komputera dzieli z DOS-em, to właśnie on zajmuje się obsługą programów, pamięci, klawiatury, myszy, monitora, drukarki i portów szeregowych, pozwalając DOS-owi ograiczyć się w zasadzie do zarządzania systemem plików. Łatwość korzystania z aplikacji Windows wynika w dużej mierze z braku konieczności dostosowywania ich do aktualnej konfiguracji systemu komputerowego. Dzięki temu instalacja programów przebiega w prosty sposób, a jeśli zdarzają się jakiekolwiek problemy związane z niezgodnością sprzętową, to dotyczą one wyłącznie systemu Windows 95 i wynikają niestety z błędów popełnionych przez programistów firmy Microsoft. W wersji 3.1 tego rodzaju problemy nigdy się nie pojawiają. Możliwe jest przygotowanie aplikacji Windows, która mimo iż składać się będzie z jednego tylko pliku "EXE", to jednak działać będzie bezbłędnie na każdym komputerze PC, niezależnie od jego konfiguracji. W przypadku programów DOS-owych korzystających z grafiki lub z drukarki rozwiązanie takie jest nie do pomyślenia. Tam plikowi "EXE" towarzyszy zazwyczaj cała plejada programów obsługi różnych typów urządzeń, która w praktyce i tak nierzadko okazuje się niewystarczająca. 1.2.5. Programy użytkowe pakietu. System Windows to nie tylko system operacyjny lub nakładka na niego; to także zestaw programów narzędziowych oraz użytkowych, które wprawdzie nie dorównują sprzedawanym oddzielnie wysoko wyspecjalizowanym aplikacjom, ale w wielu zastosowaniach okazują się całkiem wystarczające. W wersji 3.1 najważniejszym z nich jest oczywiście "Menedżer programów", umożliwiający uruchamianie i przekazywanie sterowania między poszczególnymi programami. Bez tej aplikacji nie byłoby Windows 3.1 . Ale system ten to przcież także program zarządzający plikami "Menedżer plików", procesor tekstu "Write", program graficzny "Paintbrush", program komunikacyjny "Terminal", a ponadto terminarz, kartoteka, kalkulator, zegar, uwielbiany przez początkujących użytkowników komputerów pasjans oraz kilka innych programów. W Windows 3.11 dochodzą do tego programy sieciowe, szczególnie program obsługi poczty elektronicznej "Mail", program do planowania prac "Schedule+", a także aplikacja "Remote Access", umożliwiająca połączenie się z siecia lokalną z odległego miejsca. Jeszcze lepsze aplikacje udostępnia Windows 95. Wśród nich w pierwszej kolejności należy wymienić program zarządzający plikami "Eksplorator" oraz ulepszone: procesor tekstu "WinPad", program graficzny "Paint" i program komunikacyjny "HyperTerminal". Do tego dochodzą aplikacje związane z pracą sieciową: "Exchange", obsługująca system poczty elektronicznej, oraz "Dial-Up Networking", umożliwiająca uzyskanie połączenia modemowego z serwerami różnych sieci rozległych i lokalnych, takich jak Internet, Th Microsoft Network, Windows 3.11, Windows NT czy Windows 95. Jeżeli więc procesor tekstu potrzebny jest jedynie po to, by od czasu do czasu napisać list, to nie ma sensu kupować "Worda dla Windows"; "Write", a tym bardziej "WinPad" okażą się w tym przypadku zupełnie wystarczające. Na tej samej zasadzie "HyperTerminal", czy nawet "Terminal" w sposób zadowalający wykona zadanie modemowego przesłania pliku. A zatem komputer z zainstalowanym systemem Windows jest już w pełni przygotowany do wykonywania wielu działań, jeszcze zanim dokona się zakupu dodatkowych prgramów. Ale są też i inne korzyści wynikające z istnienia programów wchodzących w skład Windows. W przeszłości często zdarzało się, że osoby nie związane bliżej z informatyką miały trudności z przyswojeniem sobie choćby podstawowych poleceń DOS-a, takich jak "COPY" czy "CD". Tymczasem system Windows nie wymaga od swych użytkowników zapamiętywania jakichkolwiek poleceń - wszystkie operacje mogą być w łatwy i przyjemny sposób wywołane za pomocą list menu. Innym bardzo istotnym elementem systemu przyjaznym dlaużytkownika jest fakt przetłumaczenia go na wielu języków, w tym także na polski, co dla osób nie znających angielskiego ma pierwszorzędne znaczenie. Rozdział 2. Podstawowe zasady programowania. Dlaczego system Windows zyskał sobie opinię środowiska łatwego i przyjemnego dla użytkowników, ale jednocześnie dziwacznego i trudnego dla programistów? Dlaczego cała dotychczasowa wiedza i doświadczenie zdobyte podczas wieloletniego programowania okazują się niewystarczające wobec wymagań nowego systemu? Na czym polega przesyłanie i obsługa komunikatów oraz w jaki sposób zarządza się oknami Windows? Co to sa funkcje "API", jaką rolę pełnią zasoby aplikacji Windows i jakie pakiety umożliwiają ich twrzenie? W poprzednim rozdziale patrzyliśmy na Windows głównie oczami użytkownika, teraz spojrzymy na niego jako programiści. 2.1. Jednak warto... Na postawione wyżej pytania odpowiedzieć można w prosty sposób: przecież ogromne możliwości Windows, związane głównie z wielozadaniowością i graficznym interfejsem użytkownika, nie powstają same z siebie; do ich uzyskania przyczynić się musi także programista. Jednak jego wysiłek zostanie bez wątpienia sowicie wynagrodzony; doprowadzi on do powstania programu, który z całą pewnością bez korzystania z Windows nigdy by się nie narodził. Samo przygotowanie obsługi programu i jego szaty graficznej wiązaoby się w innych warunkach z tak ogromnym nakładem pracy, iż zdecydowana większość programistów zrezygnowałaby z niego, zadowalając się mniej ambitnymi rozwiązaniami. Są też takie dziedziny programowania, które dzięki możliwości korzystania z procedur systemowych zawartych w bibliotekach Windows charakteryzują się większą prostotą niż w przypadku DOS-a. Należy do nich z całą pewnością transmisja szeregowa. Aby napisać program komunikacyjny będący aplikacją Windows, nie trzeba już ani zagłębiać się w tajniki funkcjonowania sprzętu, programować samodzielnie "UART" i sterownik przerwań, ani też wydawać pieniędzy na zakup dodatkowego pakietu; wystarczy skorzystać z pocedur Windows służących do obsługi portu szeregowego. Zanim zaczniemy zajmować się poszczególnymi zagadnieniami związanymi z programowaniem w Windows, sensowne wydaje się poznanie podstawowych zasad jego funkcjonowania, na razie bez wdawania się w szczegóły związane z przygotowywaniem aplikacji. Być może uda nam się w ten sposób znaleźć dokładniejsze odpowiedzi na zawarte we wstępie tego rozdziału pytania. 2.2. Obsługa komunikatów. Większość programów działających w środowisku DOS-a wykonuje swoje zadania w sposób sekwencyjny, co oznacza, że stopniowo przechodzą one kolejne etapy pracy, aż do momentu zakończenia swego działania. Każda procedura wywoływana jest z określonego miejsca, do którego po pewnym czasie program wraca. Oczekiwanie na wprowadzenie danych przez użytkownika, zakończenie wykonywania obliczeń lub upłynięcie określonego czasu nie stanowi żadnego problemu - przecież wykonywany program jest jedynym korzystającymz zasobów komputera. W Windows sytuacja wygląda zupełnie inaczej. Tutaj program stanowi zbiór procedur reagujących na zajście określonych zdarzeń w systemie. Zdarzeniami takimi są na przykład: naciśnięcie klawisza klawiatury, przesunięcie wskaźnika myszy, upłynięcie określonego czasu czy też zmiana wielkości okna - o tym wszystkim program dowiaduje się za pomocą komunikatów, które otrzymuje od Windows. Po zakończeniu wykonywania działań będących reakcją na zajście danego zdarzenia aplikacja musi natychmiast zwrócić sterwanie systemowi, gdyż w przeciwnym wypadku nie miałby on możliwości przekazywania kolejnych komunikatów zarówno jej samej, jak i też innym działającym aktualnie aplikacjom. Zrozumienie tej właściwości Windows stanowi klucz do właściwego programowania w środowisku graficznego interfejsu użytkownika. Na początku konieczne jest jednak przełamanie pewnych przyzwyczajeń nabytych podczas pisania programów działających w środowisku DOS-a. Na przykład, jeżeli aplikacja Windows oczekuje na wprowadzenie danych przez użytkownika, to nie może ona w tym celu skorzystać z żadnej ze standardowych procedur wejściowych. Zamiast tego powinna oddać sterowanie Windows i czekać, aż system am powiadomi ją o zajściu odpowiedniego zdarzenia. Dodatkową trudnością dla programisty jest fakt, że procedura odbierająca komunikaty znajduje się w zupełnie innym miejscu programu niż to, w którym zostało przerwane jego wykonywanie. Procedura ta, zwana procedurą oknową (ang. "window procedura"), reaguje na wszystkie komunikaty otrzymywane od systemu i przeznaczone dla danego okna (nowoczesne pakiety służące do tworzenia aplikacji Windows, takie jak "Turbo Pascal for Windows", implementują samodzielnie procedurę oknową, pozwalając programiście ograiczyć się do napisania procedur odbierających konkretne komunikaty - więcej na ten temat w rozdziale następnym, "Pierwsza aplikacja Windows"). Nie należy przez to rozumieć, że zadaniem każdej aplikacji jest przetwarzanie wszystkich otrzymywanych przez nią komunikatów. Owszem, możliwość taka istnieje, jednak w zdecydowanej większości przypadków wystarcza wywołanie funkcji Windows "DefWindowProc", reagującej na komunikaty w sposób standardowy. W skrajnym przypadku możliwe jest napisanie procedury oknowej, która sama zajmowałaby się tylko jednym komunikatem - sygnalizującym wybór polecenia menu kończącego działanie programu. Jeżeli jednak aplkacja, ma wykonywać jakieś użyteczne funkcje, to musi przetwarzać samodzielnie więcej komunikatów, przynajmniej te, które umożliwiają użytkownikowi pełne korzystanie z list menu oraz wprowadzanie danych i wyświetlanie wyników programu. Z obsługą komunikatów wiąże się zasada funkcjonowania wielozadaniowości w systemie Windows. Większość tradycyjnych systemów wielozadaniowych przekazuje sterowanie poszczególnym programom w oparciu o podział czasu. Oznacza to, że każdy program wykonywany jest przez określony czas, po upłynięciu którego zostaje on przerwany, przy czym zasoby komputera przejmuje program następny w kolejce. Rozwiązanie to ma tę zaletę, że nigdy nie dochodzi do zablokowania systemu przez jedno zadanie. Windows nie przerywa wykonywania programów. To one same zobowiązane są przekazywać mu odpowiednio często sterowanie. Wiąże się z tym oczywiście ogromna odpowiedzialność programisty za funkcjonowanie nie tylko jego własnej aplikacji, ale także wszystkich pozostałych. Nie można dopuścić do powstania sytuacji, w której jeden program zablokowałby na czas swego wykonywania cały system. Dlatego sterowanie powinno się oddawać nie tylko po zakończeniu obsługi każdego zdarzenia, ale także zawsze wtedy, gdy wkonywanie jakiejś operacji, na przykład wyświetlanie grafiki, trwa zbyt długo; tego rodzaju zadania należy dzielić na mniejsze kroki i wykonywać etapami. 2.3. Zarządzanie oknami. Słowo "windows", oznaczające "okna", stało się nie tylko nazwą systemu, ale także jego najbardziej przekonującą wizytówką. Okna służą nie tylko do wymiany danych między programem a użytkownikiem, odpowiadając pod tym względem całemu ekranowi monitora w przypadku programów DOS-owych, ale także do reprezentowania aplikacji - każdy program działa tak długo, jak długo otwarte jest jego główne okno. Każde okno składa się co najmniej z ramki umożliwiającej zmianę jego wielkości (ang. "sizing border"), paska tytułu (ang. "caption bar") oraz wnętrza zwanego obszarem roboczym, a czasami także powierzchnią użytkową lub obszarem klienta (ang. "client area"). Ponadto z reguły zawiera ono pasek menu (ang. "menu bar"), przycisk menu systemowego (ang. "system menu box"), przycisk zwijający lub minimalizacji (ang. "minimize box"), przycisk rozwijający lub maksymalizacji (ang. "maximize box"), paski przewiania (ang. "scroll bar") oraz ewentualnie przycisk zamknięcia (w Windows 95). Pewien szczególny typ okien stanowią okna dialogowe (ang. "dialog box"), które zawierają na swej powierzchni określoną liczbę elementów sterujących (ang. "child window control"), umożliwiających wprowadzanie danych w wygodny, zgodny z określonym standardem, sposób. Elementy te przyjmują postać przycisków (ang. "push button"), przełączników (ang. "radio button"), pól wyboru (ang. "check box"), pól statycznych (ang. "stalic"), ikon (ang. "icon"), pól edycji (ang. "edit text"), list (ang. "list box"), asków przewijania (ang. "scroll bar") i pól kombinowanych (ang. "combo box"), będących połączeniem pola edycji z listą W obsłudze okien system Windows świadczy wprost nieocenioną pomoc, wykonując samodzielnie takie zadania, jak otwieranie, zamykanie, przesuwanie oraz zmienianie wielkości okien, w tym zwijanie ich do postaci ikony i odtwarzanie ich poprzedniej postaci, ponadto - wybór poleceń menu czy też wprowadzanie danych za pomocą elementów sterujących okien dialogowych. Napisanie programu, który sam wykonywałby wszystkie te funkcje, byłoby niezmiernie trudne, w praktyce nieopłacalne. Z punktu widzenia programisty najistotniejszą informacją dotyczącą okien jest stwierdzenie, że z każdym z nich związana jest wspomniana już wcześniej procedura oknowa. To właśnie ona wywoływana jest w momencie zajścia dowolnego zdarzenia związanego z odpowiadającym jej oknem, ponosząc w ten sposób odpowiedzialność za właściwe przetwarzanie komunikatów przez aplikację. Dla programistów przyzwyczajonych do pisania programów działających pod DOS-em jest to z pewnością pomysł nowy. Jako rzecz normalną taktują oni fakt wywoływania przez program procedur systemu operacyjnego, na przykład w celu otwarcia pliku. Tutaj ma jednak miejsce zjawisko dokładnie odwrotne - to właśnie system wywołuje procedury programu. Jest to jednak podstawowa zasada działania Windows. Z technicznego punktu widzenia sytuacja wygląda następująco: W momencie rozpoczęcia wykonywania aplikacji system Windows tworzy dla niej kolejkę, w której następnie gromadzone są komunikaty dla wszystkich tworzonych przez nią okien. Zadaniem aplikacji jest pobieranie tych komunikatów z kolejki i wysyłanie ich - na podstawie zawartej w nich informacji - do odpowiednich procedur oknowych (w nowoczesnych pakietach służących do tworzenia aplikacji Windows za wykonywanie tej czynności odpowiedzialne są bblioteki podprogramów - więcej na ten temat w rozdziale następnym). Ponadto istnieją też takie komunikaty, choć stanowią one mniejszość, które są przesyłane bezpośrednio do procedur oknowych, z ominięciem kolejki komunikatów. 2.4. Biblioteki Windows. 2.4.1. Co to są funkcje "API"? Aplikacje Windows mogą korzystać z ponad 600 różnych funkcji systemowych, tak zwanych funkcji "API" (ang. "Application Programming Interface"), zapisanych w trzech plikach: - "KERNEL.EXE" ("KRNL286.EXE" w trybie standardowym, "KRNL386.EXE" w trybie rozszerzonym 386) - zarządzanie programami, pamięcią i pozostałymi zasobami komputera oraz współpraca z DOS-em; - "USER.EXE" - zarządzanie graficznym interfejsem użytkownika; - "GDI.EXE" - wyświetlanie i drukowanie grafiki. Zapamiętanie składni wszystkich funkcji "API" jest niezwykle mało prawdopodobne, stąd też w praktyce podczas pisania aplikacji Windows konieczne okazuje się korzystanie z podręcznika lub z pomocy ekranowej. Niemniej jednak znacznie łatwiej jest nauczyć się korzystać z bibliotek Windows niż... próbować je napisać samemu. Każda z funkcji Windows ma opisową, łatwą do zrozumienia nazwę, tworzoną za pomocą dużych i małych liter. Jako przykład można podać procedurę "GetClientRect", służącą do odczytania współrzędnych obszaru roboczego, "TextOut", wyświetlającą tekst, czy też "MessageBox", otwierającą okno informacyjne. Prototypy wszystkich funkcji Windows, a także związane z nimi definicje stałych i deklaracje typów, zgromadzone są w pliku "WINDOWS.H", dołączanym z reguły do każdego pakietu programowania. Z funkcji systemowych korzystać mogą tylko aplikacje Windows, nie jest natomiast możliwe stosowanie ich przez programy działające pod DOS-em. Można więc powiedzieć, że Windows stawia przed programistą warunek "wszystko albo nic". Wynika to stąd, że w aplikacji Windows wszystko jest ze sobą powiązane - jeśli chce się utworzyć pewną grafikę na ekranie monitora, potrzebne jest do tego celu okno, to zaś wymaga obsługi komunikatów, a w konsekwencji - przygotowania pełnowartościowej aplikacji. 2.4.2. Wyświetlanie tekstu i rysunków. Jedna z wyżej wymienionych bibliotek Windows, "GDI" (ang. "Graphics Device Interface"), opisuje rozbudowany język programowania grafiki, który pozwala na łatwe wyświetlanie i drukowanie rysunków oraz sformatowanego tekstu. Aplikacje Windows nie uzyskują bezpośredniego dostępu do sprzętu, takiego jak monitor czy drukarka, lecz działają w połączeniu z dowolnym ich typem, dla którego istnieje zainstalowany w systemie program obsługi. Program nie musi określać, jakiego rodzaju urządzenie zostało połączoe z komputerem. Dzięki temu generowanie rysunków jest w systemie Windows dużo łatwiejsze niż w programach DOS-a. Z drugiej strony, w pierwszej chwili nieco skomplikowane wydać się może wyprowadzanie tekstu. Odczucie takie jest jednak jak najbardziej zrozumiałe - w Windows tekst powstaje przecież w trybie graficznym, który umożliwia dowolny wybór czcionki, jej stylu, koloru i wyrównania, podczas gdy w programach DOS-owych każdy łańcuch wyprowadzany jest w identyczny sposób, za pomocą tego samego zestawu znaków. Możliwość realizowania zróżnicowanych projektów graficznych powstała dzięki zastosowaniu tak zwanych narzędzi rysujących, do których należą, czcionki, pióra i pędzle. To one określają szerokość i kolor prowadzonych linii, wzór wypełniania figur czy wreszcie rodzaj pisma, włączając w to wszystkie jego elementy charakterystyczne, takie jak pochylenie czy pogrubienie. 2.4.3. Dynamiczne łączenie. Procedury biblioteczne programów działających pod DOS-em dołączane są do nich na etapie konsolidacji (ang. "linking") i zajmują miejsce w plikach "EXE". Natomiast procedury obsługujące przerwania programowe, na przykład przerwania DOS-a 21 h, rezydują na stałe w pamięci RAM, czekając na wywołanie przez działający aktualnie program. W Windows nie zastosowano żadnego z wymienionych wyżej rozwiązań. Funkcjonuje tu tak zwane dynamiczne łączenie, które polega na tym, że procedury systemowe łączone są z programem w czasie jego wykonywania. Podejście takie ma kilka istotnych zalet. Po pierwsze, funkcje Windows nie zajmują miejsca w plikach "EXE", a przez to także w pamięci operacyjnej, lecz są ładowane dopiero wtedy, gdy wywołuje je jedna z wykonywanych aplikacji. Po drugie, uaktualnienie procedur systemowych Windows nie pociąga za sbą konieczności ponownej kompilacji programu. Aby się o tym przekonać, wystarczy uruchomić dowolną aplikację Windows, zarówno w wersji 3.1 tego systemu, jak i 95. Zupełnie inaczej sytuacja wygląda w przypadku DOS-a, gdzie określona wersja procedur bibliotecznych na stałe związana jest z programem. I wreszcie po trzecie, dynamiczne łączenie umożliwia jednoczesne korzystanie z tych samych procedur przez wiele aplikacji. Biblioteki dynamiczne mają na ogół rozszerzenie "DLL", choć istnieją od tej zasady odstępstwa. Wystarczy tu choćby wspomnieć o trzech plikach Windows, "KERNEL", "USER" i "GDI", które mimo swego rozszerzenia "EXE" nie są, plikami wykonywalnymi, lecz bibliotekami dynamicznymi. Ale nie tylko funkcje systemowe Windows mogą być łączone dynamicznie z jego aplikacjami - z udogodnienia tego są w stanie korzystać także wszystkie inne procedury. Tworzenie własnych bibliotek dynamicznych opłaca się szczególniewówczas, gdy te same funkcje wykorzystywane są przez większą liczbę programów. 2.5. Korzystanie z zasobów programu. 2.5.1. Pliki z opisem zasobów. Do przygotowania programu działającego pod DOS-em wystarczy napisanie kodu źródłowego, który jest odpowiedzialny nie tylko za operacje wewnętrzne wykonywane przez ten program, na przykład obliczenia czy działania na plikach, ale także za organizowanie wymiany danych z użytkownikiem. W Windows sytuacja wygląda inaczej. Tutaj każda bardziej rozbudowana aplikacja zawiera oprócz kodu źródłowego także opis zasobów (ang. "resources"), czyli takich jej elementów, jak menu, ikony czy okna dialogowe. Obiektó tych z reguły nie definiuje się poprzez bezpośrednie wprowadzanie tekstu, lecz dzięki korzystaniu ze specjalnych programów, zwanych edytorami zasobów. W efekcie powstaje najczęściej plik tekstowy o rozszerzeniu nazwy "RC" lub jego wersja skompilowana w postaci pliku z rozszerzeniem "RES". Mimo iż rozszerzenie nazwy aplikacji Windows jest takie samo, jak programów działających pod DOS-em, to jednak ich format, zwany nowym formatem plików wykonywalnych (ang. "New Executable file format"), jest nieco inny. W nagłówku zawierają one bowiem dodatkowo tablicę odwołań do dołączanych dynamicznie funkcji systemowych oraz tablicę zasobów, natomiast w części końcowej - opis zasobów. 2.5.2. Uchwyty. Osoby programujące w DOS-ie przyzwyczajone są do tego, że otwieranym plikom przydzielane są uchwyty (ang. "handle"), umożliwiające późniejsze wykonywanie operacji na tych plikach. Ale korzystanie z uchwytów w odniesieniu do niemalże wszystkich zasobów programu jest z pewnością rozwiązaniem nowym. A właśnie ono zostało zastosowane w aplikacjach Windows - uchwyty, będące liczbami całkowitymi dodatnimi, pełnią tu kluczową rolę. Jednym z najważniejszych uchwytów jest uchwyt okna, umożliwiający wykonywanie takich operacji, jak zmiana jego wielkości, położenia czy wyglądu. Z grafiką nierozerwalnie związane jest pojęcie uchwytu urządzenia, niezbędnego do określenia obiektu, w którym tekst lub rysunki mają się pojawiać; obiektem tym może być na przykład jedno z okien lub drukarka. Ale możliwości stosowania uchwytów jest znacznie więcej niż te, które zostały wymienione. Pojawiają się one przy okazji korzystania z menu i ikon, oken dialogowych i ich elementów sterujących, narzędzi rysujących, a nawet bloków pamięci. Uchwyty towarzyszą więc programiście Windows przez cały czas pisania aplikacji. 2.6. Pakiety do tworzenia aplikacji Windows. Jeszcze nie tak dawno, kiedy programiści mieli do swej dyspozycji jedynie pakiet Windows "Software Development Kit" ("SDK") firmy Microsoft, programowanie w Windows było bardzo trudne. Być może właśnie dlatego pierwsze aplikacje powstawały dość wolno. Wymagały one od programisty doskonałej znajomości funkcjonowania systemu, a najdrobniejszy nawet błąd karany był zazwyczaj przerwaniem działania programu i wyświetleniem komunikatu o błędzie aplikacji (ang. "Application Error"). Dzisiaj sytuacja zmieniła się bardzo na korzyść. Powstały pakiety, które zajmują się szczegółami związanymi z tworzeniem aplikacji Windows, pozwalając skupić się programiście na ich funkcjach użytkowych. Jako przykłady takich pakietów można podać "Turbo Pascal for Windows" oraz "Turbo C++ for Windows" firmy Borland, oba z biblioteką "ObjectWindows", która definiuje między innymi obiekty reprezentujące aplikacje, okna oraz okna dialogowe i ich elementy sterujące. Wspomnieć wypada jeszcze o pakietach, w których nazwach występuje słowo wizualny, czyli o "Visual Basic" oraz "Visual C" firmy Microsoft. Oba te pakiety prezentują całkowicie nowe jakościowo podejście do programowania. Cała struktura aplikacji tworzona jest w nich automatycznie przez środowisko pakietu i pozostaje niewidoczna dla programisty, ograniczając jego zadanie do napisania procedur reagujących na zajście poszczególnych zdarzeń. Jak najbardziej trafne jest więc określenie pakietu "Visual Basi" zaproponowane przez jego twórców: "programowanie sterowane zdarzeniami" (ang. "event driven programming"). Tworzenie procedur jest tam nierozerwalnie związane z projektowaniem poszczególnych obiektów programu, takich jak okna dialogowe; oba te procesy w praktyce na ogół występują jednocześnie. Z drugiej strony, pakiety wizualne nie pozwalają na zastosowanie wszystkich tych mechanizmów, które udostępniają programistom pakiety w rodzaju "Turbo Pascal for Windows". Uniemożliwiają one na przykład otrzymywanie niektórych komunikatów generowanych przez system. Jeśli chodzi o bardzo popularny, szczególnie wśród programistów-amatorów, "Visual Basic", to jego istotną, wadą jest również to, że stosuje on język programowania Basic, który ze względu na niski stopień strukturalności znacznie ustępuje ascalowi i C. Ponadto pakiet ten nie generuje bezpośrednio kodu wynikowego, lecz pseudokod, do którego trzeba dołączać specjalny plik pomocniczy i który działa znacznie wolniej niż typowe programy. W sumie więc pakiet ten można polecić w przypadku przygotowywania prostych, nieskomplikowanych aplikacji, kiedy zależy nam na czasie. W celu utworzenia profesjonalnego programu trzeba skorzystać z jednego z bardziej zaawansowanych pakietów. Z dużym oddźwiękiem w środowisku informatycznym spotkało się pewne wydarzenie, które miało miejsce w 1995 roku na hannoverskich targach "CeBit". Mowa tu o zaprezentowaniu przez firmę Borland kolejnej wersji środowiska Pascala, o nazwie "Delphi", już dużo wcześniej reklamowanej jako "Visual Basic Killer". Określenie to wydaje się jak najbardziej słuszne, gdyż pakiet "Delphi" ma wszystkie zalety środowiska wizualnego, a ponadto umożliwia programowanie w Pascalu. Jego dodatkową zaletą jest to, że udostpnia on też bibliotekę procedur zarządzających bazami danych. Który pakiet należy wybrać? Z punktu widzenia dydaktyki najlepszym językiem jest Pascal, zaś środowisko "Turbo Pascal for Windows" łączy prostotę z uniwersalnością, jest wygodne w obsłudze i pozwala zaobserwować procesy zachodzące w Windows. Dlatego właśnie z niego korzystać będziemy w dalszej części książki. Z drugiej strony, trzeba pamiętać o tym, że większość profesjonalnych aplikacji Windows powstaje w języku "C++". Ponieważ jednak oba te języki są do siebie bardzo podobne, ewentualne przejście jednego na drugi nie powinno nikomu sprawiać żadnych kłopotów, szczególnie jeśli odbywać się ono będzie w ramach dwóch różnych pakietów firmy Borland. Rozdział 3. Pierwsza aplikacja Windows. Teraz, kiedy zaprzyjaźniliśmy się już z Windows, kiedy z jednej strony poznaliśmy jego kaprysy, z drugiej zaś zalety, możemy zastosować zdobytą wiedzę w praktyce, to znaczy napisać pierwszy program. Pomimo swej prostoty program ten stanowić będzie już pełnowartościową aplikację Windows, reprezentowaną przez własną ikonę, pozwalającą na korzystanie z list menu i skrótów klawiszowych (ang. "hotkeys"), wyświetlającą komunikaty, reagującą na naciśnięcie przycisku myszy, a nawet wymagającą potwierdzenia hęci zakończenia jej działania. Napisanie i uruchomienie tej aplikacji pozwoli poznać w praktyce sposób realizacji podstawowych wymagań środowiska Windows: zarządzanie oknami, przetwarzanie komunikatów i korzystanie z zasobów, takich jak menu czy ikona. 3.1. Torbo Pascal for Windows. 3.1.1. Najważniejsze programy pakietu. Pakiet "Turbo Pascal for Windows" (w skrócie "TPW"), z którego korzystać będziemy od tego momentu, stanowi istotną pomoc w programowaniu w Windows. Najważniejsze programy środowiska "TPW" są następujące: - "TPW.EXE" - zintegrowane środowisko uruchamiania programów (ang. "integrated development environment"), czyli edytor tekstu w połączeniu z kompilatorem; - "TDW.EXE" - rozbudowany debugger, pozwalający śledzić wykonywanie programu także na poziomie instrukcji asemblera oraz udostępniający zaawansowany system ustawiania punktów przerwań; - "WORKSHOP.EXE" - edytor zasobów, czyli program umożliwiający tworzenie plików zawierających opis zasobów programowych, takich jak listy menu, okna dialogowe czy ikony. 3.1.2. Biblioteki podprogramów. Przejmując wykonywanie większości standardowych zadań związanych z zarządzaniem aplikacją, "TPW" pozwala programiście zaoszczędzić wiele cennego czasu oraz tworzyć bardziej niezawodne programy. Jedną z największych zalet tego pakietu jest pobieranie komunikatów systemu i przekazywanie ich odpowiednim procedurom, dzięki czemu odpada konieczność samodzielnego pisania procedur oknowych (zob. podrozdział 2.2, "Obsluga komunikatów"). W skład pakietu "TPW" wchodzą zarówno moduły umożliwiające korzystanie z bibliotek systemowych "API", jak i te, które związane są z biblioteką "ObjectWindows". Do pierwszej grupy należą: - "WinTypes" - typy i stałe "APl", - "WinProcs" - funkcje i procedury "API". Natomiast biblioteka "ObjectWindows" reprezentowana jest przez: - "WObjects" - wszystkie podstawowe typy, stałe, zmienne, funkcje i procedury "ObjectWindows", - "StdWnds" - typy standardowych okien "TDlgWindow" i "TFileWindow", - "StdDlgs" - typy standardowych okien dialogowych "TFileDialog" i "TlnputDialog", - "BWCC" - obsługa "ozdobnych" elementów sterujących okien dialogowych, tworzonych w tak zwanym stylu firmy Borland (ang. "Borland Windows Custom Controls"). Ponadto można korzystać z biblioteki standardowej "Windows" (ang. "runtime Iibrary"), zawierającej funkcje i procedury zadeklarowane w czterech modułach: - "System" - podstawowe zmienne, funkcje i procedury "TPW", między innymi zarządzające pamięcią dynamiczną oraz wykonujące obsługę plików, - "WinCrt" - zarządzanie oknem i klawiaturą w bardzo prostych aplikacjach nie tworzących rysunków, - "WinDos" - korzystanie z przerwań programowych DOS-a, - "Strings" - operacje na łańcuchach znakowych zakończonych zerem (stosowanych w języku programowania "C"). Uzyskane dzięki pakietowi "TPW" uproszczenie kodu źródłowego programu jest wprost uderzające. Wystarczy porównać postać najprostszej aplikacji, przedstawionej w następnym podrozdziale, z jej odpowiednikiem przygotowanym za pomocą pakietu "SDK" firmy Microsoft. Wymaga on napisania ponad dwudziestu instrukcji języka "C", wykonujących działania na kilkunastu zmiennych i niewiele mniejszej liczbie stałych oraz zawierających wywołania dwunastu różnych funkcji, z których jedna ma jedenaście parametrów! 3.2. Aplikacja i jej główne okno. 3.2.1. Obiekt aplikacji. Jakże prosty wydaje się w tej sytuacji program przedstawiony niżej, który też stanowi aplikację "Windows"! Po jej uruchomieniu zostaje otwarte okno mające przycisk menu systemowego oraz przycisk minimalizacji i maksymalizacji. Możliwa jest zatem zmiana wielkości i położenia okna, zmniejszenie go do postaci ikony oraz powiększenie do powierzchni całego ekranu, jak również przekazywanie sterowania pomiędzy nim a oknami innych działających w danym momencie programów. Uwaga: Aby umożliwić wyświetlanie polskich liter w edytorze "TPW", należy najpierw wybrać polecenie menu "Options" w nim "Preferences", a następnie w liście "Font" zamienić standardową czcionkę "BorlandTE, 09" na inną, zawierającą polskie litery, na przykład "Courier, 10" lub "Fixedsys, 09". {******************************************************************} { } {Moja pierwsza aplikacja Windows } {******************************************************************} PROGRAM PAW; USES WObjects; VAR Aplikacja: TApplication; {Blok główny programu} BEGIN Aplikacja.Init('Pierwsza aplikacja Windows'); Aplikacja.Run; Aplikacja.Done; END. Uzyskanie tak prostej postaci programu możliwe było dzięki wykorzystaniu reprezentującego go obiektu "TApplication" z biblioteki "ObjectWindows". Blok główny zawiera wywołania trzech metod tego obiektu, z których pierwsza, będąca konstruktorem, służy do jego inicjowania, druga, najważniejsza, odpowiedzialna jest za uruchomienie programu oraz zarządzanie komunikatami, zaś ostatnia - zwalnia obiekt. Parametr przekazany funkcji "Init" określa nazwę programu, z której korzysta on wewnętrznie. Zadaniem obiektu "TApplication" jest też utworzenie czterech zmiennych globalnych: - "Hlnstance" - przydzielony przez Windows uchwyt aktualnego egzemplarza aplikacji; - "HPrevInstance" - uchwyt poprzednio uruchomionego egzemplarza tej samej aplikacji lub 0, jeśli została ona uruchomiona po raz pierwszy; - "CmdLine" - parametry przekazane programowi w wierszu wywołania (można je przekazać między innymi za pomocą polecenia "Plik" w nim "Uruchom" Menedżera programów systemu Windows 3.1 ); - "CmdShow" - wartość określająca początkowy sposób wyświetlenia okna, na przykład w postaci ikony, na całym ekranie itp. Rys. 3.1. Okno pierwszej aplikacji Windows jest wprawdzie niezmiernie proste, ale ma już menu systemowe oraz przyciski minimalizacji i maksymalizacji. 3.2.2. Obiekt okna. Następnym krokiem na drodze ku tworzeniu coraz lepszych aplikacji Windows będzie umieszczenie tytułu w głównym oknie napisanego przez nas programu. W tym celu skorzystać będziemy musieli z innego obiektu biblioteki "ObjectWindows", "TWindow", z którego wywodzą się wszystkie typy reprezentujące okna aplikacji. Ponieważ obiekt "TWindow" tworzony jest w metodzie "TApplication.InitMainWindow", konieczne staje się przykrycie tej metody, a przez to - utworzenie typu potomnego w stosunku do "TApplication".Zmodyfikowany program (oprócz początkowego komentarza oraz bloku głównego, który nie uległ zmianie) przyjmuje obecnie postać następującą: PROGRAM PAW; USES WObjects; TYPE {Obiekt reprezentujący aplikację} TAplikacja = OBJECT(TApplication) PROCEDURE InitMainWmdow; VIRTUAL; END; VAR Aplikacja: TAplikacja; {Utworzenie okna głównego aplikacji} PROCEDURE TAplikacja.InitMainWindow; BEGIN MainWindow := New(PWindow, Init(NIL, 'Pierwsza aplikacja Windows')); END; "PWindow" jest typem wskazującym na "TWindow"; w pakiecie "TPW" regułę stanowi rozpoczynanie nazw obiektów od dużej litery T, zaś typów wskazujących na obiekty - od dużej litery P. Konstruktor "Init" obiektu "TWindow" ma dwa parametry, z których pierwszy jest wskaźnikiem do obiektu reprezentującego okno nadrzędne (ponieważ takie nie istnieje, przekazujemy tu wartość zerową "NIL"), drugi zaś oznacza tytuł okna. Zmienna "MainWindow" jest polem obiektu "TApplication" (a więc także obiektu "TAplikacja"), wskazującym na główne okno aplikacji. Jeśli chodzi o obiekt "TWindow", to zawiera on wiele pól, z których zdecydowanie najważniejszym i wielokrotnie w każdym programie wykorzystywanym jest "HWindow", będące uchwytem okna i absolutnie niezbędne przy wywołaniach funkcji "API". Rys. 3.2. To okno aplikacji wyróżnia się już swoją nazwą. 3.2.3. Wielkość i położenie okna. Standardowo system Windows sam decyduje o wielkości i położeniu otwieranych okien aplikacji; kolejne okna przesuwane są coraz bardziej w dół i na prawo i mają coraz mniejsze rozmiary. Ponieważ sytuacja taka jest mało wygodna, wszystkie profesjonalne aplikacje przejmują zadanie określenia współrzędnych swego okna głównego. Współrzędne te wpisuje się w konstruktorze okna do następujących pól struktury o nazwie "Attr": - "X", "Y"- współrzędne lewego górnego rogu okna; - "W", "H" - szerokość i wysokość okna, łącznie z wszystkimi jego elementami, takimi jak ramka, pasek tytułu czy pasek menu. W standardowym trybie wymiarowania jednostką długości jest piksel. Początek układu współrzędnych znajduje się w lewym górnym rogu ekranu i jest oznaczony parą liczb (0;0). A zatem, aby w przygotowywanej przez nas aplikacji wykorzystać podane wyżej informacje, trzeba w pierwszej kolejności utworzyć obiekt potomny w stosunku do obiektu głównego okna "TWindow", nadając mu na przykład nazwę "TOkno": {Obiekt reprezentujący okno główne aplikacji} POkno = ^TOkno TOkno = OBJECT(Window) CONSTRUCTOR Init(AParent: PWIndowsObject; ATitle: PChair); END; Następnie konieczne jest zdefiniowanie konstruktora "TOkno.Init", którego zawartość może być następująca: {Konstruktor obiektu reprezentującego okno główne} CONSTRUCTOR TOkno.Init; BEGIN TWindow.Init(NIL, ATitle); WITH Attr DO BEGIN X := 0; Y := 0; W := 480; H := 240; END; END; Ostatnią konieczną do wykonania zmianą w naszej aplikacji jest zastąpienie w metodzie "TAplikacja.InitMainWindow" obiektu "PWindow" obiektem "POkno": PROCEDURE TAplikacja.InitMainWindow; BEGIN MainWindow := New(POkno, Init(NIL, 'Pierwsza aplikacja Windows')); END; 3.3. Komunikaty. 3.3.1. Przetwarzanie komunikatów. Wiemy już, że funkcjonowanie aplikacji Windows nie byłoby możliwe bez właściwego przetwarzania nieustannie przekazywanych jej przez system komunikatów. Jak widać z zamieszczonych wyżej przykładów, to, co stanowiło niegdyś utrapienie programistów mających do swej dyspozycji jedynie funkcje "API", zostało skutecznie przejęte przez bibliotekę "ObjectWindows". To właśnie dzięki niej odpada konieczność tworzenia pętli pobierającej komunikaty, procedur oknowych oraz funkcji reagujących na niektóre zdarzena zachodzące w systemie. Nie zwalnia nas to jednak z obowiązku przetwarzania wszystkich tych komunikatów , na które program nie może reagować w sposób standardowy. Chodzi tu przede wszystkim o komunikaty informujące aplikację o wyborze poleceń menu oraz wprowadzaniu danych za pomocą elementów okien dialogowych bądź też bezpośrednio - za pośrednictwem klawiatury lub myszy, jak również komunikaty umożliwiające wyświetlanie grafiki. Lecz także i tu "ObjectWindows" okazuje się bardzo pomocny, gdyż wywołuje automatycznie metody owiązane z odpowiadającymi im zdarzeniami. Powiązania tego dokonuje się poprzez przydzielanie poszczególnym metodom indeksów komunikatów, na które mają one odpowiadać. Indeksy te składają się z dwóch członów, z których pierwszy oznacza zakres obejmujący komunikat, natomiast drugi - jego identyfikator. Jeżeli więc na przykład komunikaty związane z oknami (ang. "window message") rozpoczynają się od "wm_First", natomiast stała "wm_LButtonDown" oznacza zdarzenie polegające na naciśnięciu lewego przycisku myszy, to indeksem tego zdarzenia będzie wm_First + wm_LButtonDown". A zatem następujące zmodyfikowanie obiektu "TOkno": Tokno = OBJECT(TWindow) ... PROCEDURE WMLButtonDown(VAR Msg : TMessage); VIRTUAL wm_First+wm_LButtonDown; END; spowoduje, że metoda "TOkno.WMLButtonDown" będzie automatycznie wywoływana w momencie naciśnięcia przez użytkownika lewego przycisku myszy (gdy jej wskaźnik znajdować się będzie w obszarze roboczym okna "TOkno"). W tradycyjnym programie, utworzonym bez pomocy "ObjectWindows", konieczne byłoby tu samodzielne napisanie procedury oknowej, która odbierałaby wszystkie komunikaty i jeśli jeden z nich informowałby o naciśnięciu lewego przycisku myszy - wywoływałaby procedurę "WMLButtonDown". Procedura ta może mieć następującą postać: {Reakcja na naciśnięcle lewego przycisku myszy} PROCEDURE TOkno.WMLButtonDown; BEGIN MessageBox(HWindow, 'Został naciśnięty lewy przycisk myszy.', 'Komunikat', mb_Iconlnformation OR mb_ OK); END; Funkcja "MessageBox" znajduje się w bibliotece "API" i umożliwia wyświetlenie okna informacyjnego. Zawiera ona cztery parametry: - uchwyt okna nadrzędnego, czyli tego, w którym okno informacyjne ma zostać otwarte, - tekst, który ma się w oknie pojawić, - tytuł okna, - rodzaj przycisku i ikony, które mają zostać wyświetlone (w tym przypadku zostanie wyświetlona ikona z literą "i" oraz przycisk "OK". W zmodyfikowanym programie nie można też zapomnieć o uzupełnieniu spisu modułów, z których korzysta aplikacja, o "WinTypes" (stałe "wm_*", "mb_*") oraz "WinProcs" (funkcja "MessageBox"): USES WObjects., WinTypes, WinProcs; Rys.3.3. Każdorazowo po otrzymaniu komunikatu o naciśnięciu lewego przycisku myszy aplikacja otwiera proste okno informacyjne. 3.3.2. Strukturta komunikatu. Do tej pory pomijaliśmy milczeniem znaczenie parametru "Msg" procedury "WMLButtonDown", aby w ten sposób nie utrudniać zrozumienia istoty przekazywania komunikatów. Parametr ten, określony typem "TMessage", ma jednak ogromne znaczenie, przekazuje bowiem dokładne informacje na temat zaistniałego zdarzenia, często wykorzystywane przez otrzymującą go metodę. Poszczególne pola struktury "TMessage" są następujące: - "Receiver" - uchwyt okna, do którego skierowany jest komunikat, - "Message" - identyfikator komunikatu, np. "wm_LButtonDown", - "WParam" i "LParam" ("WParamLo", "WParamHi", "LParamLo", "LParamHi") - szczegółowe dane, których znaczenie uzależnione jest od rodzaju zdarzenia, - "Result" ("ResultLo", "ResultHi") - pole do wpisania ewentualnego wyniku przetworzenia komunikatu przez metodę obiektu okna. Spróbujmy obecnie wykorzystać wartości zapisane w strukturze "TMessage" dla potrzeb naszego programu. Komunikaty związane z naciskaniem przycisków myszy zawierają w polu "LParamLo" współrzędną "X" oraz w polu "LParamHi" współrzędną "Y" kursora myszy w momencie naciśnięcia jej przycisku. Wobec tego możliwe jest dokonanie takiej zmiany w metodzie "TOkno.WMLButtonDown", aby obecnie dodatkowo informowała ona o miejscu położenia myszy w momencie naciśnięcia jej lewego przycisku: PROCEDURE TOkno.WMLButtonDown; VAR X, Y : STRING; Napis: ARRAY[0..255] OF Char; BEGIN Str(Msg.LParamLo : 5, X); Str(Mag.LParamHi : 5, Y); StrPCopy(Napis, 'Został naciśnięty lewy przycisk myszy.' + Chr(13) + 'Współrzędna X: ' + X + Chr(13) + 'Współrzędna Y: ' + Y); MessageBox(HWindow, Napis, 'Komunikat', mb_IconInformation OR mb_OK); END; Rys. 3.4. W kolejnej wersji programu okno informacyjne zawiera współrzędne określające położenie wskaźnika myszy w momencie naciśnięcia jej przycisku. Warto w tym miejscu zauważyć, że funkcje związane z Windows korzystają z łańcuchów zakończonych zerem, to znaczy opisanych typem "PChar" lub "ARRAY[0..n] OF Char", gdzie "n" oznacza maksymalną długość łańcucha. Kopiowania łańcucha typu "STRING" do postaci "PChar" dokonuje wywoływana w procedurze "TOkno.WMLButtonDown" funkcja "StrPCopy". Ponieważ znajduje się ona w module "Strings", należy dołączyć jego nazwę do pozostałych modułów w klauzuli "USES": USES WObjects, WinTypes, WinProcs, Strings; Komunikat wyświetlony po naciśnięciu lewego przycisku myszy w obszarze roboczym okna naszego programu został pokazany na rysunku 3.4. 3.4. System menu. 3.4.1. Tworzenie menu. W ten sposób doszliśmy do miejsca, w którym dalsze rozwijanie programu jedynie w oparciu o jego kod źródłowy wydaje się bezcelowe - cóż bowiem warta jest aplikacja Windows bez menu? To zaś, jako jeden z zasobów, definiowane jest w odrębnym pliku. Podobnie jak pozostałe moduły programu, również i zasoby przechowywane mogą być zarówno w wersji źródłowej (pliki z rozszerzeniem "RC"), jak i skompilowanej (głównie pliki z rozszerzeniem "RES"). Wyróżniają się one jednak tym spośród innych elementów aplikaji, że można je utworzyć od razu w postaci wynikowej, z pominięciem postaci tekstowej. Do wykonania tego zadania służy edytor zasobów, który jest jednocześnie ich kompilatorem; w "Turbo Pascalu for Windows" nosi on nazwę "Workshop". Podczas gdy w przypadku niektórych zasobów, takich jak mapy bitowe, korzystanie z edytora zasobów jest w praktyce niezbędne, to menu można bez większych trudności opisać w postaci źródłowej za pomocą dowolnego edytora tekstu, by potem skompilować je programem "Workshop". Dokładniej rzecz biorac, należy w takim przypadku wczytać plik z rozszerzeniem "RC" (polecenie "File" w nim "Open project"), a następnie zapamiętać go pod tą samą nazwą, ale z rozszerzeniem "RES (polecenie "File" w nim "Save file as). "Workshop" dokona wówczas automatycznie kompilacji. Skoncentrujmy się teraz na postaci źródłowej menu, które chcemy wprowadzić do naszego programu. Może ono wyglądać następująco (z poszczególnych poleceń tak zdefiniowanego menu korzystać będziemy w dalszej części książki): /*PAW.RC: Zasoby programu PAW*/ #include "paw_m.pas" PAW.MENU BEGIN POPUP "&Plik" BEGIN MENUITEM "&Nowy", cm_Nowy MENUITEM "&Otwórz", cm_Otworz MENUITEM "&Zachowaj", cm_Zachowaj MENUITEM SEPARATOR MENUITEM "&Koniec", cm_Koniec END POPUP "&GDI" BEGIN MENUITEM "&Tekst", cm_Tekst MENUITEM "P&owieść", cm_Poczatek MENUITEM SEPARATOR MENUITEM "&Rysunek", cm_Rysunek MENUITEM "&Paleta", cm_Paleta MENUITEM "&Fraktal", cm_Fraktal END POPUP "&Okno" BEGIN MENUITEM "&Nakładane", cm_Nakladane MENUITEM "&Rozwijalne", cm_Rozwijalne MENUITEM SEPARATOR MENUITEM "&Dialogowe", cm_Dialogowe MENUITEM "&TInputDialog", cm_Input MENUITEM "T&FileDialog", cm_File END POPUP "&Pomoc" BEGIN MENUITEM "&Informacja", cm_Informacja END END Jak widać, składnia tekstu zawartego w pliku "PAW.RC" przypomina zarówno "Pascal", jak i "C". Nie jest to jednak żaden z tych języków programowania, lecz specjalny język służący do definiowania zasobów. Bez nadmiernego wgłębiania się w jego szczegóły zauważymy jedynie, że: - wiersz /* ... */jest komentarzem; - "#include «paw_m.pas»"poleca wczytanie pliku o podanej nazwie; - wiersz "PAW MENU" określa nazwę zasobu i jego rodzaj; - słowo "POPUP" oznacza rozwijalny element menu (listę menu), zaś "MENUITEM" -końcowy (polecenie menu); - znak "&" określa literę, która ma być w poleceniu menu podkreślona, umożliwiając jego wybór poprzez naciśnięcie pojedynczego klawisza; - słowa zaczynające się od "cm_" są identyfikatorami poleceń, zdefiniowanymi w pliku "PAW_M.PAS" i określającymi metody, które mają być wywoływane w następstwie wyboru odpowiadających im poleceń (więcej na ten temat niżej oraz w podrozdziale 3.4.3, "Obsluga menu"); - słowo "SEPARATOR" oznacza poziomą kreską rozdzielającą dwie grupy poleceń znajdu jące się obok siebie na jednej liście. Zawartość pliku "PAW_M.PAS" została przedstawiona niżej. Oczywiście można by zrezygnować z jego tworzenia, podając w opisie menu bezpośrednio wartości liczbowe poszczególnych poleceń. Rozwiązanie takie byłoby jednak dużo mniej czytelne i w znacznie większym stopniu sprzyjałoby popełnianiu błędów w czasie programowania, szczególnie w przypadku modyfikacji menu. {PAW_M.PAS: Identyfikatory poleceń menu} UNIT PAW_M; INTERFACE CONST cm_Nowy = 11; cm_Otworz = 12; cm_Zachowaj = 13; cm_Koniec = 14; cm_Tekst = 21; cm_Rysunek = 22; cm_Paleta = 23; cm_Fraktal = 24; cm_Nakladane = 31; cm_Rozwijalne = 32; cm_Dialogowe = 33; cm_Informacja = 41; IMPLEMENTATION END. 3.4.2. Edytor zasobów. Menu można także zdefiniować za pomocą edytora zasobów "Workshop" (w tej książce korzystać będziemy z jego wersji 1.02). Dla jednych sposób ten będzie stanowić ułatwienie, gdyż zwalnia z konieczności pamiętania poleceń języka opisu zasobów. Innym wyda się uciążliwy, ze względu na konieczność obsługi dużej liczby elementów sterujących okna dialogowego i brak możliwości skorzystania z udogodnień towarzyszących edycji tekstu. Wybór metody należeć będzie oczywiście do samego programisty. Rys. 3.5. Menu programu można też zdefiniować za pomocą edytora zasobów. Aby rozpocząć edycję menu, trzeba najpierw otworzyć plik "PAW.RC" (lub ewentualnie "PAW.RES", ale to drugie rozwiązanie nie jest korzystne, gdyż nie zapewnia dysponowania aktualną postacią pliku źródłowego z opisem zasobów), korzystając z polecenia "File" w nim "Open Project". Następnie konieczne jest podwójne kliknięcie nazwy zasobu ("PAW") albo zaznaczenie go i wybór polecenia "Resource" w nim "Edit". Spowoduje to otwarcie okna dialogowego, którego najważniejsze elementy są następujące: - "Item text" - nazwa elementu menu, - "Item id" - identyfikator elementu menu, - "Item type" - rodzaj elementu menu: lista ("Pop-up"), polecenie ("Menu item") lub pozioma kreska oddzielająca ("Separator"). Z prawej strony okna znajduje się lista menu, w takiej samej postaci, w jakiej pojawi się ona w aplikacji, a poniżej - opis menu, przypominający zawartość pliku "PAW.RC". Wprowadzania nowych elementów menu dokonuje się za pomocą poleceń menu "Menu" lub odpowiadających ich klawiszy. Na przykład naciśnięcie klawisza "INSERT" spowoduje wstawienie nowego polecenia bezpośrednio pod elementem aktualnie wskazywanym. Jeśli ktoś woli wprowadzić zmiany bezpośrednio w pliku tekstowym, nie musi opuszczać edytora zasobów. Wystarczy w tym celu wybrać polecenie "Resource" w nim "Edit as text". 3.4.3. Obsługa menu. Aby użytkownik mógł korzystać z menu, należy je jeszcze dołączyć do programu. W tym celu bezpośrednio za jego nazwą trzeba umieścić dyrektywę kompilatora {$R PAW.RES} oraz uzupełnić klauzulę "USES" o moduł z identyfikatorami poleceń menu, "PAW_M": USES WObjects, WinTypes, WinProcs, Strings, PAW_M; Ponadto konieczne jest zmodyfikowanie konstruktora obiektu głównego okna, "TOkno.Init", tak aby miało w nim miejsce ładowanie zasobu menu do pamięci. W tym celu trzeba wywołać funkcję "API" "LoadMenu", która ma dwa parametry: uchwyt egzemplarza aplikacji i nazwę zasobu menu (podaną w pliku "PAW.RC" przed słowem "MENU"). Rezultatem tej funkcji jest uchwyt menu (lub zero, jeśli ładowanie nie zakończyło się sukcesem), który należy wpisać do pola "Attr.Menu" obiektu "TOkno". Jego konstruktor powinien zaem obecnie wyglądać następująco: CONSTRUCTOR TOkno.Init; BEGIN TWindow.Init(NIL, ATitle); WITH Attr DO BEGIN ... Menu := LoadMenu(HInstance, 'PAW'); END; END; Oczywiście z samego faktu dołączenia menu do programu odnosi się niewiele korzyści, jeśli nie umożliwia ono jednocześnie wywoływania poszczególnych funkcji aplikacji. Aby cel ten osiągnąć, należy utworzyć metody okna zawierającego menu, przydzielając im indeksy będące sumą stałej "cm_First", oznaczającej zakres komunikatów menu (ang. "command messages"), oraz identyfikatorów poszczególnych poleceń; dzięki temu metody te będą automatycznie wywoływane w momencie wyboru odpowiadających im pozycji menu.Jeżeli chcemy na przykład, aby program reagował na wybór polecenia "Pomoc" w nim "Informacja", to powinniśmy utworzyć metodę okna głównego zaopatrzoną w indeks "cm_First+cm_Informacja": TOkno = OBJECT(TWindow) ... PROCEDURE Informacja(VAR Msg: TMessage); VIRTUAL cm_First+cm_Informacja; END Treść tej metody może być na razie następująca: {Reakcja na wybór polecenia menu Pomoc|Informacja} PROCEDURE TOkno.Informacja; BEGIN MessageBox(HWindow, 'Moja pierwsza aplikacja Windows', 'Informacja' , mb_IconInformatlon OR mb_OK); END; Rys. 3.6. Przy niewielkim nakładzie pracy aplikacja została wyposażona w system menu. Rys.3.7. Wybór polecenia "Pomoc" w nim "Informacja" powoduje wyświetlenie przez program odpowiedniego okna informacyjnego. Znajdujące się wyżej rysunki przedstawiają okno główne programu po dołączeniu do niego menu, z rozwiniętym menu "Plik", oraz okno dialogowe wyświetlone w wyniku wyboru polecenia menu "Pomoc" w nim "Informacja" (położenie tego okna zostało zmienione w celu zmniejszenia rozmiarów rysunku). 3.5. Skróty klawiszowe. 3.5.1. Zmiany w plików z opisem zasobów. Większość aplikacji umożliwia stosowanie tak zwanych skrótów klawiszowych, zwanych także klawiszami szybkiego wywołania (ang. "hotkeys"), odpowiadających zazwyczaj często wykorzystywanym poleceniom menu. Skróty takie noszą w aplikacjach Windows nazwę "akceleratorów" (ang. "accelerators") i są tworzone w podobny sposób jak menu, to znaczy są definiowane w pliku z zasobami oraz dołączane do programu w trakcie jego wykonywania. A zatem, aby umożliwić korzystanie ze skrótów klawiszowych w naszej aplikacji, musimy w pierwszej kolejności wprowadzić następujące zmiany w pliku "PAW.RC" (a następnie ponownie skompilować go przez zachowanie w formacie z rozszerzeniem "RES"): - obok nazw poleceń menu umieścić nazwy odpowiadających im skrótów klawiszowych, - dołączyć opis tablicy akceleratorów. Poniżej widać uzupełnienie opisu menu przygotowywanej aplikacji. Znaki sterujące "\t", pojawiające się za nazwami poszczególnych poleceń, powodują, że wszystkie nazwy skrótów klawiszowych będą wyświetlane w menu jedna pod drugą: PAW MENU BEGIN POPUP "&Plik" BEGIN MENUITEM "&Nowy\tCtrl+N", cm_Nowy MENUITEM "&Otwórz\tCtrl+0", cm_Otworz MENUITEM "&Zachowaj\tCtrl+S", cm_Zachowaj ... END POPUP "&GDI" BEGIN MENUITEM "&Tekst\tF7", cm_Tekst MENUITEM "P&owieść\tShift+F7", cm_Poczatek ... MENUITEM "&Rysunek\tF8", cm_Rysunek MENUITEM "&Paleta\tShift+F8", cm_Paleta MENUITEM "&Fraktal\tCtrl+F8", cm_Fraktal END ... POPUP "Po&moc" BEGIN MENUITEM "&Informacja\tCtrl+I", cm_Informacja END END Opis tablicy akceleratorów odpowiadających zdefiniowanym wyżej poleceniom menu powinien wyglądać następująco: PAW ACCELERATORS BEGIN "^N", cm_Nowy "^O", cm_Otworz "^S", cm_Zachowaj VK_F7, cm_Tekst, VIRTKEY VK_F7, cm_Poczatek, VIRTKEY, SHIFT VK_F8, cm_Rysunek, VIRTKEY VK_F8, cm_Paleta, VIRTKEY, SHIFT VK_F8, cm_Fraktal, VIRTKEY, CONTROL "^I", cm_Informacja END Poszczególne elementy opisu mają następujące znaczenie: - wiersz "PAW ACCELERATORS" określa nazwę zasobu i jego rodzaj; - "^N" oznacza kombinację klawiszy "CTRL+N"; - "VK_F7" jest jednym z tak zwanych kodów klawiszy wirtualnych, reprezentującym klawisz "F7"; - słowo "VIRTKEY" informuje kompilator zasobów o tym, że na początku wiersza znajduje się kod klawisza wirtualnego; - słowo "SHIFT" w połączeniu ze stałą "VK_F8" opisuje kombinację klawiszy "SHIFT+F8", zaś słowo "CONTROL" w połączeniu z tą stałą - kombinację klawiszy "CTRL+F8"; - słowa zaczynające się od "cm_" są, identyfikatorami poleceń menu, które mają być wybierane w następstwie naciskania odpowiadających im skrótów klawiszowych. 3.5.2. Edytor zasobów. Tablica akceleratorów daje się także zdefiniować za pomocą edytora zasobów "Workshop". Sposób postępowania jest tu analogiczny do tworzenia systemu menu. Po otwarciu pliku "PAW.RC" oraz zainicjowaniu edycji zasobu "ACCELERATORS" w nim "PAW" zostaje wyświetlone okno dialogowe, którego najważniejsze elementy są następujące: - "Command" - identyfikator elementu menu odpowiadającego danemu skrótowi klawiszowemu, - "Key" - opis klawisza, - "Key type" - rodzaj opisu klawisza: wartość "ASCII" znaku wprowadzanego przez klawisz ("Ascii") lub kod klawisza wirtualnego ("Virtual key"), - "Modifiers" - ewentualne dodatkowe klawisze, które muszą być naciśnięte razem z klawiszem opisanym w polach "Key" i "Key type". Wprowadzania nowych elementów tablicy akceleratorów dokonuje się przez naciśnięcie klawisza "INSERT". Opis klawisza można albo wpisać samodzielnie, albo, lepiej, za pomocą polecenia "Accelerator" w nim "Key value", pozwalającego zdefiniować klawisz lub kombinację klawiszy bezpośrednio przez ich naciśnięcie. Wybór polecenia "Resource" w nim "Edit as text" umożliwia, podobnie jak ma to miejsce podczas definiowania menu, edycję postaci tekstowej tablicy akceleratorów. Rys. 3.8. Tablicę akceleratorów można także zdefiniować za pomocą, edytora zasobów. 3.5.3. Ładowanie tablicy akceleratorów przez program. Aby móc korzystać z tablicy akceleratorów zdefiniowanej w pliku z zasobami, należy ją załadować do programu. Funkcja, z której się w tym celu korzysta, nosi nazwę "LoadAccelerators" i jest wywoływana w metodzie "Initlnstance" obiektu aplikacji (wynika stąd, że skróty klawiszowe są związane z całą aplikacją, a nie tylko z wybranym oknem). Funkcja "LoadAccelerators" ma dwa parametry: uchwyt egzemplarza aplikacji oraz nazwę tablicy akceleratorów. Jej rezultatem jest uchwyt tej tablicy (lub zero, jeśli adowanie nie zakończyło się sukcesem), który należy wpisać do pola "HAccTable" obiektu "TApplication". W związku z powyższym w przygotowywanej przez nas aplikacji musimy przykryć metodę "TApplication.Initlnstance": TAplikacja = OBJECT(TAplication) PROCEDURE InitInstance; VIRTUAL; ... END Treść metody "Initlnstance" powinna wyglądać następująco: {Zainicjowanie egzemplarza aplikacji} PROCEDURE TAplikacja.Initlnstance; BEGIN TApplication.Initlnstance; HAccTable := LoadAccelerators(Hlnstance, 'PAW'); END; Po wprowadzeniu do programu wszystkich podanych wyżej zmian można już korzystać ze zdefiniowanej tablicy akceleratorów. Ponieważ jednak na razie większość poleceń menu nie ma odpowiadających im procedur, jedyną kombinacją klawiszy, jaka przekonuje nas o prawidłowym działaniu aplikacji, jest "CTRL+I". Jej naciśnięcie powoduje wyświetlenie okna dialogowego "Informacja", identycznie jak po wyborze polecenia o takiej samej nazwie, zawartego w menu "Pomoc". Niemniej jednak nazwy wszystkich zdefiniowanychskrótów klawiszowych są już widoczne obok odpowiadających im poleceń menu. Rys. 3.9. Obok nazw poleceń menu przygotowywanej aplikacji pojawiły się nazwy odpowiadających im skrótów klawiszowych. 3.6. Ikona aplikacji. 3.6.1. Projektowanie ikony. Każda "porządna" aplikacja ma przynajmniej jedną własną ikonę, odpowiadającą jej oknu głównemu. Ikony stanowią kolejny rodzaj zasobów po omówionych wcześniej: menu i tablicy akceleratorów. Mimo iż można je definiować wprowadzając bezpośrednio ich opis do pliku źródłowego z opisem zasobów, to jednak znacznie wygodniejsze wydaje się korzystanie z edytora zasobów "Workshop", który tym razem przekształca się w "prawdziwy" program graficzny. Po otwarciu pliku "PAW.RC" i ewentualnym wskazaniu zasobu "ACCELERATORS" w nim "PAW" (aby nowy zasób został dopisany na koniec listy) należy wybrać polecenie "Resource" w nim "New" oraz określić typ zasobu jako "ICON". Na pytanie programu, czy nowy zasób ma zostać utworzony w wersji źródłowej, czy binarnej, dobrze jest odpowiedzieć przez wskazanie pierwszej możliwości, gdyż wówczas opis ikony zostanie dołączony do listy zasobów pliku "PAW.RC", zapewniając jego aktualność; po zdecydowaniu się na wersę binarną opis ten zostałby umieszczony w osobnym pliku z rozszerzeniem "ICO". Jeśli chodzi o zaproponowaną przez program wielkość ikony (32x32) i liczbę jej kolorów (16), to odpowiadają one standardowemu rozwiązaniu i nie wymagają zmian. Właściwe okno umożliwiające zaprojektowanie ikony zawiera dwa jej obrazy, z których jeden, prawy, wyświetlany jest zawsze w wielkości oryginalnej, natomiast lewy można powiększać lub pomniejszać za pomocą poleceń "Zoom in" i "Zoom aut" z menu "View"; rysunki można nanosić na obu obrazach. Dostępny pasek narzędzi rysowania obejmuje między innymi strzałkę do zaznaczania fragmentów ikony, gumkę, pióro, pędzel, aerozol, wałek malarski (do wypełniania zamkniętych konturów) oraz narzędzia do tworzenia linii i prostych figur geometrycznych, a także do wprowadzania tekstu. Zmianę śladu pozostawianego przez pióro, pędzel i aerozol można uzyskać po wyborze poleceń "Brush shape", "Airbrush shape" i "Pen style" z menu "Options". Natomiast menu "Text" pozwala na wybór rodzaju i wielkości czcionkioraz określenie sposobu wyrównywania tekstu w stosunku do miejsca położenia kursora. Rysować można zarówno za pomocą lewego, jak prawego przycisku myszy, przy czym w pierwszym przypadku program stosuje kolor podstawowy, w drugim zaś - kolor tła. Ustalenia obu tych kolorów dokonuje się przez kliknięcie odpowiednio za pomocą lewego lub prawego przycisku myszy w wybranych polach palety kolorów. Fragmenty ikony narysowane kolorem "Transparent" będą przezroczyste (na początku cała ikona jest przezroczysta), natomiast narysowane kolorem "Inverted" - wyświetlane w kolorach odwróconych w stsunku do kolorów tła (podłoża). W trakcie projektowania ikony można w każdej chwili przetestować wyniki swej pracy, wybierając polecenie "Icon" w nim "Test". W wyniku wykonania tej czynności edytor zasobów zasymuluje takie zachowanie się ikony, jakie będzie miało miejsce w rzeczywistych sytuacjach. Osoby dociekliwe mogą też wybrać polecenie "Resource" w nim "Edit as text" lub po zakończeniu projektowaniu ikony otworzyć w dowolnym edytorze tekstu plik "PAW.RC", aby zobaczyć, w jaki sposób obraz graficzny ikony został zakodowany w pstaci ciągu liczb oznaczających kolory kolejnych pikseli. Rys. 3.10. Nawet prosta ikona (paw - to skrót od "Pierwsza aplikacja Windows") stanowi pewnego rodzaju wizerunek przygotowywanej aplikacji. 3.6.2. Zmiany w programie. Z ładowaniem ikony wiąże się pojęcie tak zwanej "klasy okna", czyli zestawu wartości określających niektóre atrybuty okna, takie jak jego tła, rodzaj ikony czy postać wskaźnika myszy. Zmiany standardowej postaci tych wartości można dokonać w metodzie "GetWindowClass" obiektu reprezentującego okno, poprzez odpowiednie ustawienie pól przekazywanej jej w postaci parametru struktury typu "TWndClass". Uchwyt zasobu opisującego ikonę wpisuje się do pola o nazwie "hIcon". Uchwyt ten można uzyskać w postacirezultatu funkcji "LoadIcon", która, analogicznie do omawianych wcześniej funkcji "LoadMenu" i "LoadAccelerators", wymaga podania dwóch parametrów: uchwytu egzemplarza aplikacji i nazwy zasobu z ikoną. Konieczne do wykonania w naszym programie zmiany polegają więc na przykryciu metody "GetWindowClass" obiektu okna głównego. Jej deklaracja oraz zawartość powinny wyglądać następująco: Tokno = OBJECT(TWindow) ... PROCEDURE GetWindowClass(VAR AWndClass: TWndClass); VIRTUAL; ... END; {Zdefiniowanie klasy okna} PROCEDURE TOkno.GetWindowClass; BEGIN TWindow.GetWindowClass(AWndClass); AWndClass.hIcon := LoadIcon (Hlnstance, 'PAW'); END; Rys. 3.11. Na pulpicie Windows (na rysunku pokazano wersję 3.1) wszystkie aplikacje reprezentowane są w postaci swoich ikon. 3.7. Koniec działania programu. Zakończenie działania programu może nastąpić na skutek wyboru polecenia "Zamknij" ("Close") ze standardowego menu systemowego, którego obsługę przeprowadza Windows. Oprócz tego przyjęło się, że każdy program dostarcza własnego polecenia o nazwie "Koniec" ("Exit") lub podobnej, znajdującego się w końcowej części menu "Plik" ("File"). Źle świadczy o aplikacji Windows, jeśli oba wymienione polecenia obsługuje ona w inny sposób. Zadaniem programisty jest więc zapewnienie właściwej obsługi obu poleceń. Orócz tego spoczywa na nim odpowiedzialność za to, aby program przed zakończeniem swego działania umożliwił użytkownikowi zapamiętanie zmienionych danych lub anulowanie być może przypadkowo podjętej decyzji. W momencie wyboru polecenia "Zamknij" z menu systemowego system Windows wysyła do okna głównego komunikat "wm_Close", informujący je o tym, że ma ono zostać zamknięte. W efekcie wywoływane są, metody "CanClose" aplikacji, okna głównego oraz wszystkich jego okien wewnętrznych. Tylko wówczas, gdy wszystkie te metody zwrócą wartość "True", dochodzi do wysłania komunikatów "wm_Destroy" oraz "wm_Quit", prowadzących do zakończenia działania aplikacji. Wynika stąd ważny wniosek: jakiekolwiek działania, które mają być wykonane przed zamknięciem programu, muszą zostać umieszczone w metodzie "CanClose". Jeżeli chcemy na przykład, aby nasza aplikacja wymagała potwierdzenia wyboru polecenia "Zamknij", to powinniśmy przykryć metodę "CanClose" obiektu "Tokno": TOkno = OBJECT(TWindow) ... FUNCTION CanClose: Boolean; VIRTUAL; ... END; Funkcja "TOkno.CanClose" może, podobnie jak inne metody okna "TOkno", posłużyć się funkcją "MessageBox", która pozwala nie tylko na wyświetlanie komunikatów, ale także na zadawanie pytań. Uzyskanie wyniku "IdYes" oznacza, że użytkownik nacisnął przycisk "Tak"; w przeciwnym wypadku zostaje zwrócona wartość "IdNo": {Reakcja na próbę zakończenia programu} FUNCTION TOkno.CanClose; BEGIN IF MessageBOX(Hwindow, 'Zakończyć program?', 'Koniec', mb_IconQuestion OR mb_YesNo) = IdYes THEN CanClose := True ELSE CanClose := False; END; Obecnie pozostaje jeszcze do utworzenia procedura reagująca na wybór polecenia "Koniec". Jej treść jest bardzo prosta, gdyż może się ona ograniczyć do wywołania funkcji "CloseWindow". Funkcja ta jest metodą abstrakcyjnego obiektu "TWindowsObject", z którego wywodzi się między innymi "TWindow", i wykonuje identyczne działania z tymi, jakie mają miejsce po wyborze polecenia "Close" z menu systemowego: TOkno = OBJECT(TWindow) ... PROCEDURE Koniec(VAR Msg: TMessage); VIRTUAL cm_First+cm_Koniec; ... END; {Reakcja na wybór polecenia menu Plik|Koniec} PROCEDURE TOkno.Koniec; BEGIN CloseWindow; END; Na rysunku poniżej pokazano okno, które jest wyświetlane po wyborze jednego z poleceń menu kończących działanie aplikacji: Rys. 3.12. Po wyborze polecenia "Zamknij" z menu systemowego lub "Koniec" z menu "Plik" program wyświetla okno weryfikacyjne. Rozdział 4. Wyświetlanie tekstu i rysunków. W poprzednim rozdziale napisaliśmy pierwszą, aplikację Windows. Ma już ona ikonę, menu i tablicę tak zwanych akceleratorów, potrafi reagować na niektóre zdarzenia i wyświetlać komunikaty. W obecnym rozdziale uzupełnimy ją o funkcje graficzne, wyświetlające na ekranie monitora tekst i rysunki. Będziemy przy tym zwracać szczególną uwagę na te elementy programowania w Windows, które odróżniają je od przygotowywania programów działajacych w środowisku DOS-a. Będzie więc mowa o tym, jaką funkcję pełni kotekst wyświetlania, jakie udogodnienia stwarzają dla programisty logiczne czcionki, narzędzia rysujące (pióra i pędzle) i palety kolorów, dlaczego konieczne jest odtwarzanie zawartości okna, jak się tę czynność wykonuje i jakie musi ona uwzględnić sytuacje wyjątkowe, a także - w jaki sposób należy reagować na zmiany rozmiarów okna. 4.1. Kontekst urządzenia i wyświetlania. Jak pamiętamy, w systemie Windows do wyprowadzania danych służą funkcje "GDI", przy czym korzystanie z nich jest możliwe tylko za pośrednictwem tak zwanego kontekstu urządzenia. Kontekst ten stanowi wewnętrzną strukturę danych związaną z danym obiektem wyjściowym, takim jak drukarka, ploter, pamięć czy jedno z okien ekranu monitora. W ostatnim przypadku kontekst urządzenia przyjmuje nazwę kontekstu wyświetlania. Przed rozpoczęciem tworzenia grafiki (zarówno rysunków, jak i tekstu) konieczne jest uzyskanie uchwytu kontekstu urządzenia, zaś po zakończeniu jej tworzenia - zwolnienie go. Wykonanie tej ostatniej czynności jest o tyle istotne, że Windows zezwala na jednoczesne istnienie co najwyżej pięciu różnych uchwytów kontekstów urządzeń. Niewłaściwy sposób programowania mógłby zatem spowodować. zablokowanie niektórych aplikacji. Po zwolnieniu uchwytu nie jest możliwe dalsze korzystanie z funkcji graficznych. W dalszej części tego rozdziału zajmować się będziemy wyłącznie kierowaniem danych na ekran, potraktujemy zatem kontekst wyświetlania jako pewną szczególną odmianę kontekstu urządzenia. Jednym z dwóch sposobów uzyskania jego uchwytu, typu "HDC", jest wywołanie funkcji "API" "GetDC". Wymaga ona podania tylko jednego parametru - uchwytu okna, w którym ma być wyświetlana grafika. Do zwolnienia uchwytu kontekstu wyświetlania służy między innymi funkcja "ReleaseDC", będąca odpowiednikiem funkcji "GetDC" mająca dwa parametry: uchwyt okna oraz zwalniany uchwyt kontekstu. Ważne jest, że obie wymienione funkcje można umieścić w dowolnym miejscu programu. Typowa metoda obiektu reprezentującego okno aplikacji, która tworzy grafikę, wygląda zatem następująco: VAR HKontWysw := HDC; BEGIN HKontWysw := GetDC(HWindow); ... Tworzenie grafiki ReleaseDC(HWindow, HKontWysw); END; 4.2. Wyświetlanie tekstu. 4.2.1. Podstawowe wiadomości. Każdy program działający pod DOS-em dysponuje całym ekranem, który ma zawsze określoną liczbę wierszy i kolumn. Również i rodzaj stosowanej czcionki (ang. "font") w zasadzie się nie zmienia. W Windows sytuacja wygląda zupełnie inaczej. Wielkość okna, z którego korzysta działająca w tym systemie aplikacja, z reguły nie jest ustalona i może w dowolnym momencie ulec zmianie. Jeśli zaś chodzi o czcionkę, to panuje tu tak ogromna dowolność, wyrażająca się choćby w kroju pisma, zestawie znaków, ich wielkości czy stosowaniu efektów specjalnych, że trudno nawet podać dokładną liczbę wszystkich dostępnych kombinacji. Do tego dochodzi możliwość dowolnego wyboru koloru tekstu i tła. Wszystko to powoduje, że z jednaj strony aplikacje Windows są pod względem wizualnym bez porównania bardziej atrakcyjne od swych odpowiedników działających pod DOS-em, z drugiej jednak strony wyświelanie tekstu staje się w nich trudniejsze. Nie można jednak zapominać o ogromnej pomocy udzielanej przez Windows, którą stanowi uzyskanie pełnej niezależności od sprzętu. Programista nie musi więc ustalać rodzaju zainstalowanej karty graficznej i uzależniać od niej sposobu wyświetlania tekstu. Nawet określanie kolorów może następować w sposób zupełnie ogólny - system sam wybiera kolor fizyczny (możliwy do przedstawienia na ekranie) najbardziej zbliżony do koloru logicznego określonego w programie. Na początek ograniczymy się do stosowania standardowej czcionki systemowej oraz poznamy tylko jedną, najczęściej wywoływaną, funkcję służącą do wyprowadzania tekstu. Nosi ona nazwę "TextOut" i ma następujące parametry: - uchwyt kontekstu wyświetlania, - współrzędne logiczne "X" i "Y" lewego górnego rogu pierwszego znaku, - łańcuch (w formacie "PChar") oraz jego długość (liczba znaków). Początek współrzędnych logicznych znajduje się w lewym górnym rogu okna, a jednostka logiczna odpowiada w standardowym trybie odwzorowania jednemu pikselowi. Oczywiście w systemie Windows trudno jest operować pojęciem wiersza lub kolumny, gdyż przecież znaki mogą mieć różną wielkość. Nawet w obrębie tej samej czcionki szerokość znaków może się zmieniać - mówimy wówczas o piśmie proporcjonalnym (tworzy je między innymi czcionka systemowa). Wykorzystując podane informacje możemy rozwinąć nasz program o funkcję reagującą na wybór polecenia "GDI" w nim "Tekst". Naturalnie powinna ona należeć do obiektu reprezentującego okno główne aplikacji: TOkno = OBJECT(TWindow) ... PROCEDURE Tekst(VAR Msg: TMessage); VIRTUAL cm_First+cm_Tekst; END; {Reakcja na wybór polecenia GDI|Tekst} PROCEDURE TOkno.Tekst; VAR HKontWysw: HDC; Napis : PChar; BEGIN HKontWysw := GetDC(HWindow); Napis := 'To jest przykładowy tekst.'; TextOut(HKontWysw, 100, 100, Napis, StrLen(Napis)); ReleaseDC(HWindow, HKontWysw); END; Rys. 4.1. Po wyborze polecenia "GDI" w nim "Tekst" program wyświetla krótki tekst. 4.2.2. Wybór czcionki. Jeśli tekst wyświetlany na ekranie monitora ma być tworzony za pomocą innej czcionki niż systemowa, najpierw trzeba utworzyć nową "czcionkę logiczną", a następnie przydzielić ją kontekstowi wyświetlania. Przez czcionkę logiczną rozumiemy pewną abstrakcyjną czcionkę, opisaną w sposób niezależny od sprzętu, której Windows przydziela najbardziej zbliżoną do niej czcionkę fizyczną spośród wszystkich zainstalowanych w systemie. Po zakończeniu korzystania z czcionki logicznej powinno się ją usunąć, aby ni zajmowała niepotrzebnie miejsca w pamięci. Do atrybutów dobrego programowania należy także odtworzenie czcionki oryginalnej. Należy przy tym uważać, aby nie usunąć czcionki, która jest aktualnie przydzielona do kontekstu wyświetlania, gdyż grozi to niechybnie pojawieniem się błędu programu, nawet jego zawieszeniem. Do zdefiniowania nowej czcionki logicznej najlepiej użyć funkcji "CreateFontlndirect", której parametrem jest zmienna typu "TLogFont", opisująca poszczególne atrybuty tworzonej czcionki. Najważniejsze pola tej struktury są następujące: - "lfHeight" - wysokość czcionki wyrażona w jednostkach logicznych, którymi w standardowym trybie odwzorowania są piksele; wartość pola "lfHeight" większa od zera oznacza całkowitą wysokość czcionki, łącznie z tak zwanym wewnętrznym odstępem między wierszami (ang. "internat leading"), czyli odległością między najwyższym punktem osiąganym przez czcionkę, na przykład w literze "Ä", a górną granicą dużej litery; natomiast wartość mniejsza od zera, stosowana z reguły przez procesory tekstu, oznacza ysokość czcionki bez wewnętrznego odstępu; podana tu wartość nie oznacza w żadnym wypadku wysokości punktowej czcionki, czyli wyrażonej w punktach drukarskich (1 punkt drukarski = 1 /72 cala w przybliżeniu 0,35 mm); - "lfWeight" - grubość czcionki; mimo iż pole to może przyjmować wiele różnych wartości, z reguły ograniczają się one do stałych "fw_Normal" i "fw_Bold" (pogrubienie); - "lfItalic" - wartość tego pola różna od zera oznacza kursywę; - "lfUnderline" - wartość tego pola różna od zera oznacza podkreślenie; - "lfFaceName" - nazwa czcionki; można tu wpisać nazwę dowolnej czcionki zainstalowanej w systemie, na przykład "Times New Roman CE", "Arial CE", "Courier New CE", "Script" lub "Symbol". Pozostałe pola struktury "TLogFont" mają znacznie mniejsze znaczenie praktyczne, w związku z czym na obecnym etapie nauki programowania w Windows należy im przypisać wartości standardowe, określone w zamieszczonym niżej przykładzie. Samo utworzenie czcionki nie wystarcza, aby można było z niej korzystać. Trzeba jeszcze wywołać funkcję "SelectObject", która przydziela czcionkę do kontekstu wyświetlania. Funkcja ta ma dwa parametry, oba będące uchwytami: jeden - kontekstu wyświetlania, drugi - wybieranego obiektu, którym może być między innymi czcionka. Jeśli działanie funkcji "SelectObject" zakończyło się poprawnie, jej wartością jest uchwyt zastępowanego obiektu. W przeciwnym wypadku, na przykład wtedy, gdy przekazany parametr ie określa prawidłowo utworzonej czcionki, wartość ta wynosi zero. Zadaniem funkcji "DeleteObject" jest usunięcie obiektu o przekazanym jej w postaci parametru uuchwycie. Wykorzystując podane wyżej objaśnienia, możemy obecnie dokonać takich zmian w procedurze odpowiadającej poleceniu menu "GDI" w nim "Tekst" naszej aplikacji, aby wyświetlała ona tekst za pomocą kilku różnych czcionek: PROCEDURE TOkno.Tekst; VAR HKontWysw: HDC; Napis: PChar; OpisCzcionki: TLongFont; Czcionka1, Czcionka2, Czcionka3, StaraCzcionka: HFont; BEGIN HKontWysw := GetDC(HWindow); {Czcionka systemowa} Napis := 'Czcionka systemowa'; TextOut(HKontWysw, 40, 40, Napis, StrLen(Napis)); {Nowa czcionka 1} WITH OpisCzcionki DO BEGIN lfHeight := -16; lfWidth := 0; lfEscapement := 0; lfOrientation := 0; lfWeight := fw_Normal; lfItalic := 1; lfUnderline := 1; lfStrikeOut := 0; lfCharSet := ANSI_CharSet; lfOutPrecision := Out_Default_Precis; lfClipPrecision := Clip_Default_Precis; lfQuality := Default_Quality; lfPitchAndFamily := Default_Pitch; StrCopy(@lfFaceName, 'Times New Roman CE'); END; Czcionka := CreateFontIndirect(OpisCzcionki); StaraCzcionka := SelectObject(HKontWysw, Czcionka1); Napis := 'Times New Roman CE 16, kursywa i podkreślenie'; TextOut(HKontWysw, 40, 70, Napis, StrLen(Napis)); {Nowa Czcionka 2} WITH OpisCzcionki DO BEGIN lfHeight := -18; lfWeight := fw_Bold; lfItalic := 0; lfUnderline := 0; StrCopy(@lfFaceName, 'Arial CE'); END; Czcionka2 := CreateFontIndirect(OpisCzcionki); SelectObject(HKontWysw, Czcionka2); Napis := 'Arial CE 18, pogrubienie'; TextOut(HKontWysw, 40, 100, Napis, StrLen(Napis)); {Nowa czcionka 3} WITH OpisCzcionki DO BEGIN lfHeight := -14; lfWeight := fw_Normal; lfItalic := 0; lfUnderline := 0; StrCopy(@lfFaceName, 'Courier New CE'); END; Czcionka3 := CreateFontIndirect(OpisCzcionki); SelectObject(HKontWysw, Czcionka3); Napis := 'Courier New CE 14'; TextOut(HKontWysw, 40, 130, Napis, StrLen(Napis)); SelectObject(HKontWysw, StaraCzcionka); DeleteObject(Czcionka1); DeleteObject(Czcionka2); DeleteObject(Czcionka3); ReleaseDC(HWindow, HKontWysw); END; Rys. 4.2. W wyniku wprowadzenia czcionek logicznych wyświetlany tekst może mieć wiele różnych postaci. 4.2.3. Wyrównywanie tekstu. Standardowo tekst wyrównywany jest do lewej strony. Ale przecież wiemy, że każdy procesor tekstu pozwala także wyrównywać akapity do prawej strony oraz centrować je. Czy wszystkie niezbędne obliczenia trzeba wykonywać osobiście? Nie, wystarczy skorzystać z funkcji "SetTextAlign", która ma dwa parametry: uchwyt kontekstu wyświetlania oraz wartość określającą sposób wyrównywania tekstu. Wartość ta powinna być sumą logiczną wybranych elementów z dwóch grup stałych: "ta_Center", "ta_Left", "ta_Right", oreślających wyrównywanie poziome, oraz "ta_BaseLine", "ta_Bottom", "ta_Top", określających wyrównywanie pionowe (zastosowanie stałej "ta_BaseLine" powoduje wyrównywanie do tak zwanej linii bazowej, czyli linii określającej dolne położenie dużych liter). Wartością funkcji "SetTextAlign" jest stała określająca zastępowany sposób wyrównywania tekstu. Podobnie jak podczas definiowania czcionek logicznych, również i tutaj do zasad dobrego programowania należy przywrócenie standardowych ustawień systemu. Jeżeli tekst ma być wyśrodkowywany lub wyrównywany do prawej strony, nie można zapomnieć o odpowiednim zmodyfikowaniu współrzędnych będących parametrami funkcji "TextOut", tak aby wskazywały one na odpowiedni punkt, na przykład na prawy dolny róg tekstu w przypadku wyrównywania tekstu do prawej strony i do dołu. Korzystając z powyższych wskazówek możemy obecnie tak zmodyfikować naszą aplikację, aby pierwszy z wyświetlanych przez nią wierszy został wyrównany do dolnej granicy tekstu, drugi - do lewego marginesu, trzeci - wyśrodkowany, zaś czwarty - wyrównany do prawego marginesu, przy czym marginesy określone są współrzędnymi "X" wynoszącymi 40 oraz 440. Konieczne do wykonania zmiany są następujące: PROCEDURE TOkno.Tekst; VAR ... StareWyrown: Word; BEGIN ... {Czcionka systemowa} StareWyrown := SetTextAlign(HKontWysw, ta_Center OR ta_Bottom); Napis := 'Czcionka systemowa'; TextOut(HKontWysw, 240, 40, Napis, StrLen(Napis)); {Nowa czcionka 1} ... SetTextAlign(HkontWysw, ta_Left OR ta_Top); Napis := 'Times New Roman CE 16, kursywa i podkreślenie'; TextOut(HKontWysw, 40, 70, Napis, StrLen(Napis)); {Nowa czcionka 2} ... SetTextAlign(HKontWysw, ta_Center OR ta_Top); Napis := 'Arial CE 18, pogrubienie'; TextOut(HKontWysw, 240, 100, Napis, StrLen(Napis)); {Nowa czcionka 3} ... SetTextAlign(HKontWysw, ta_Right OR ta_Top); Napis := 'Courier New CE 14'; TextOut(HKontWysw, 440, 130, Napis, StrLen(Napis)); SetTextAlign(HKontWysw, StareWyrown); ... END; Na widocznym niżej rysunku linie określające sposób wyrównania tekstu zostały dorysowane "ręcznie" w celu lepszego zobrazowania działania funkcji "SetTextAlign". Rys. 4.3. Można określić sposób, w jaki Windows ma wyrównywać wyświetlany na ekranie tekst. 4.2.4. Zmiana koloru tekstu i tła. Bogactwo kolorów stanowi jedną z głównych atrakcji Windows. Wprawdzie tekst jest standardowo wyświetlany zawsze w kolorze czarnym na białym tle (chyba że inaczej został zdefiniowany kolor tła okna), ale te ustawienia systemu można w prosty sposób zmienić. W tym celu wystarczy skorzystać z funkcji "SetTextColor" i "SetBkColor". Obie one mają identyczne parametry: uchwyt kontekstu wyświetlania oraz liczbę określającą kolor. Liczba ta jest typu "TColorRef" (typ równoznaczny z "Longint") i może być tworzona i interpretowana na trzy różne sposoby, w zależności od wartości jej pierwszego, najbardziej znaczącego bajtu. Może ona na przykład wskazywać na konkretny element bieżącej palety kolorów, podobnie jak ma to miejsce podczas programowania w DOS-ie (do zgadnienia tego wrócimy w dalszej części tekstu). Niemniej jednak, najwygodniejszy sposób określenia koloru polega na podaniu tak zwanej "wartości RGB", czyli liczby, która w pierwszym bajcie zawiera wartość 0, zaś w trzech następnych stopień intensywności trzech barw podstawowych: czerwonej, zielonej i niebieskiej. Wszystkie te trzy składniki koloru mogą przyjmować wartości w zakresie od 0 do 255, przy czym im większa wartość, tym wyższy stopień intensywności barwy. Wartości "RGB" można wprawdzie wprowadzać samodzielnie, ale dużo wygodniejsze i cztelniejsze jest wywołanie funkcji (dokładniej: makra) "RGB", która pobiera w postaci parametrów poszczególne składowe koloru, zwraca zaś potrzebną liczbę typu "TColorRef". Tak więc na przykład "RGB"("255", "255", "255") oznacza kolor biały, "RGB"("0", "0", "0") czarny, zaś "RGB"("255", "0", "0") czerwony. Jeśli podczas wyprowadzania tekstu kolor opisany przez wartość "RGB" nie istnieje w bieżącej systemowej (fizycznej) palecie kolorów, czyli zestawie kolorów, które mogą być w danej chwili wyświetlane na ekranie monitora, Windows zastępuje go kolorem najbardziej do niego zbliżonym. Wynikiem obu omawianych funkcji, zarówno "SetTextColor", jak i "SetBkColor", jest wartość "RGB" zastępowanego koloru, czyli odpowiednio - koloru tekstu oraz koloru tła. Dzięki temu można w prosty sposób odtworzyć oryginalne ustawienia kontekstu wyświetlania. Wykorzystując podane infortnacje możemy obecnie dokonać takich zmian w naszej aplikacji, aby wyświetlany przez nią tekst prezentowany był w następujący sposób: 1. wiersz - kolory standardowe, to znaczy kolor czarny na białym tle, 2. wiersz - kolor czerwony na białym tle, 3. wiersz - kolor żółty na niebieskim tle, 4. wiersz - kolor biały na czarnym tle. Procedura "TOkno.Tekst" przybiera teraz taką postać: PROCEDURE TOkno.Tekst; VAR StaryKolorTekstu, StaryKolorTla: TColorRef; BEGIN ... {Nowa czcionka 1} ... StaryKolorTekstu := SetTextColor(HKontWysw, RGB(255, 0, 0)); Napis := 'Times New Roman CE 16, kursywa i podkreślenie'; TextOut(HKontWysw, 40, 70, Napis, StrLen(Napis)); {Nowa czcionka 2} ... SetTextColor(HKontWysw, RGB(255, 255, 0); StaryKolorTla := SetBkColor(HKontWysw, RGB(0, 0, 255)); Napis := 'Arial CE 18, pogrubienie'; TextOut(HKontWysw, 240, 100, Napis, StrLen(Napis)); {Nowa czcionka 3} ... SetTextColor(HKontWysw, RGB(255, 255, 255)); SetBkColor(HKontWysw, RGB(0, 0, 0)); Napis := 'Courier New CE 14'; TextOut(HKontWysw, 440, 130, Napis, StrLen(Napis)); SetTextColor(HKontWysw, StaryKolorTekstu); SetBkColor(HKontWysw, StaryKolorTla); ... END; Rys. 4.4. Tekst staje się atrakcyjniejszy, jeśli jest wyświetlany w różnych kolorach. 4.3. Tworzenie rysunków. 4.3.1. Rysowanie punktów i figur geometrycznych. Odpowiednikiem czcionek w zakresie tworzenia rysunków są "pióra" (ang. "pen") i "pędzle" (ang. "brush"), określające odpowiednio sposób nanoszenia konturów i wypełniania wnętrza figur. Pełna dowolność w zakresie definiowania tych narzędzi, włączając w to rodzaj stosowanych przez nie kolorów, powoduje, że Windows oferuje nieograniczone wprost możliwości w zakresie tworzenia grafiki. Dodatkową zaletą funkcji "GDI" jest ich pełna niezależność od sprzętu, o której była już mowa w podrozdziale 4.2, "Wyśwetlanie tekstu". Na razie ograniczymy się do stosowania standardowego pióra i pędzla, skupiając się na poznaniu kilku podstawowych funkcji rysujących. Pierwsza z nich, "LineTo", służy do rysowania linii prostej. Parametry tej funkcji określają jedynie współrzędne punktu końcowego, natomiast punkt początkowy pokrywa się z tak zwaną "bieżącą pozycją". Zmiana tej pozycji następuje w wyniku wywołania "LineTo" lub innej funkcji "API", "MoveTo", która poza tym nie wykonuje żadnego innego zadania. Prostokąt można narysować za pomocą funkcji "Rectangle", wymagającej określenia współrzędnych lewego górnego oraz prawego dolnego rogu. Natomiast do utworzenia okręgu lub elipsy położonej wewnątrz opisanego w taki sposób prostokąta służy funkcja "Ellipse". Jeśli zachodzi potrzeba zaznaczenia pojedynczego piksela, to do tego celu należy wykorzystać funkcję "SetPixel". Różni się jednak ona tym od funkcji poprzednio wymienionych, że nie stosuje standardowego koloru, lecz wymaga jego określenia w postac ostatniego parametru. Oczywiście pierwszym parametrem wszystkich funkcji "GDI", zarówno służących do wyprowadzania tekstu, jak i tworzenia rysunków, jest uchwyt kontekstu wyświetlania. Wobec powyższego, możemy obecnie uzupełnić naszą aplikację o metodę obiektu okna głównego wykonywaną w następstwie wyboru polecenia "GDI" w nim "Rysunek", tak aby rysowała ona kilka prostych figur geometrycznych, w tym kwadrat, którego górny lewy róg jest koloru czarnego, natomiast kolejne punkty stają się coraz bardziej czerwone w miarę posuwania się na prawo oraz coraz bardziej niebieskie w miarę posuwania się w dół (na monitorach pracujących w trybie 256 kolorów pozwala to zaobserwować sposób zasępowania przez Windows kolorów logicznych kolorami fizycznymi): TOkno = OBJECT(TWindow) ... PROCEDURE Rysunek(VAR Msg: TMessage); VIRTUAL cm_First+cm_Rysunek; ... END; {Reakcja na wybór polecenia menu GDI|Rysunek} PROCEDURE TOkno.Rysunek; VAR HKontWysw: HDC; X, Y: Integer; BEGIN HKontWysw := GetDC(HWindow); FOR X := 20 TO 83 DO FOR Y := 70 TO 133 DO SetPixel(HKontWysw, X, Y, RGB(4*(X-20), 0, 4*(Y-70))); MoveTo(HKontWysw, 125, 100); LineTo(HKontWysw, 150, 50); LineTo(HKontWysw, 175, 150); LineTo(HKontWysw, 200, 100); Rectangle(HKontWysw, 225, 50, 325, 150); Ellipse(HKontWysw, 350, 50, 450, 150); ReleaseDC(HWindow, HKontWysw); END; Rys. 4.5. W wyniku wyboru polecenia "GDI" w nim "Rysunek" powstaje kilka rysunków - powyżej widać ekran monitora pracującego w trybie 256 kolorów. 4.3.2. Zmiana konturów figur. Standardowo Windows rysuje kontury figur za pomocą czarnej, ciągłej, pojedynczej linii. Jeżeli ustawienie to nie odpowiada potrzebom programu, trzeba utworzyć "pióro logiczne". Można by to zrobić podobnie, jak podczas tworzenia czcionki logicznej, to znaczy poprzez wypełnienie pól pewnej struktury ("TLogPen"), a następnie przekazanie jej odpowiedniej funkcji ("CreatePenlndirect"). Ponieważ jednak struktura ta w przypadku pióra logicznego jest bardzo prosta, bardziej opłaca się wywołać od razu funkcj "CreatePen", której można przekazać bezpośrednio poszczególne wartości opisujące pióro, bez konieczności tworzenia wspomnianej struktury. Wartości te są następujące: - rodzaj linii rysowanej przez pióro; najczęściej pojawiającymi się w tym miejscu wartościami są: "ps_Solid" (linia ciągła), "ps_Dash" (linia kreskowana) oraz "ps_Dot" (linia kropkowana); - szerokość linii, wyrażona w jednostkach logicznych, którymi w standardowym trybie odwzorowania są piksele; - kolor linii, wyrażony za pomocą liczby typu "TColorRef". Jak łatwo się domyślić, rezultatem funkcji "CreatePen" jest uchwyt zastępowanego pióra, co można później wykorzystać do odtworzenia standardowego ustawienia kontekstu wyświetlania. Nie trzeba też chyba dodawać, że zdefiniowane pióro należy przydzielić temu kontekstowi przez wywołanie funkcji "SelectObject" oraz że po zakończeniu korzystania z niego konieczne jest jego usunięcie z pamięci za pomocą funkcji "DeleteObject". Wykorzystując podane wyżej informacje możemy obecnie tak przekształcić postać procedury "TOkno.Rysunek", aby kontury trzech rysowanych figur (nie licząc kwadratu utworzonego z pojedynczych pikseli) miały niestandardową postać: PROCEDURE TOkno.Rysunek; VAR ... Pioro1, Pioro2, Pioro3, StarePioro: HPen; BEGIN HKontWysw := GetDC(HWindow); ... {Figura 1} Pioro1 := CreatePen(ps_Solid, 2, RGB(255, 0, 0)); StarePioro := SelectObject(HKontWysw, Pioro1); MoveTo(HKontWysw, 125, 100); LineTo(HKontWysw, 150, 50); LineTo(HKontWysw, 175, 150); LineTo(HKontWysw, 200, 100); {Figura 2} Pioro2 := CreatePen(ps_Dash, 1, RGB(0, 128, 0)); SelectObject(HKontWysw, Pioro2); Rectangle(HKontWysw, 225, 50, 325, 150); {Figura 3} Pioro3 := CreatePen(ps_Dot, 1, RGB(0, 0, 255)); SelectObject(HKontWysw, Pioro3); Ellipse(HKontWysw, 350, 50, 450, 150); SelectObject(HKontWysw, StarePioro); DeleteObject(Pioro1); DeleteObject(Pioro2); DeleteObject(Pioro3); ReleaseDC(HWindow, HKontWys); END; Rys. 4.6. Kontury figur zostały narysowane w różnych kolorach i za pomocą trzech różnych linii. 4.3.3. Wypełnianie figur. Do wypełniania rysowanych figur Windows używa standardowo koloru białego. Nie jest to jednak jedyne rozwiązanie, jakie system ten daje do dyspozycji programiście. W dziedzinie definiowania "pędzli logicznych" - to one bowiem określają wygląd wnętrza figury - możliwości systemu są niemalże nieograniczone. Pozwala on nawet na wypełnianie powierzchni okien wzorem utworzonym ze zdefiniowanych wcześniej map bitowych (obrazów graficznych). Jednakże omówienie tej metody pominiemy, ograniczając się w dalsze części tekstu do stosowania prostszych rozwiązań. Pierwsze z nich polega na wypełnieniu figury kolorem stałym. W tym celu najlepiej skorzystać z funkcji "CreateSolidBrush", pobierającej tylko jeden parametr - liczbę typu "TColorRef", opisującą kolor pędzla. To lapidarne wyjaśnienie nie jest jednak tak oczywiste, jak by się to na pierwszy rzut oka wydawało. Ze stosowaniem kolorów przez pędzle logiczne wiąże się bowiem pewne ważne zagadnienie. Jeśli kolor określany przez wartość "RGB" nie istnieje w systemowej palecie kolorów, to podczas stosowania czcionki lub pióra Windows zastępuje go najbardziej zbliżonym do niego kolorem fizycznym. Inaczej ma się rzecz w przypadku korzystania z pędzla. Tutaj system czyni użytek z pewnej techniki graficznej, zwanej z języka angielskiego "dithering", która polega na wyświetlaniu pikseli w różnych kolorach w celu wywołania złudzenia powstania nowej barwy. Jeśli więc monitor nie jest w stanie w danej chwli wyświetlić odcienia koloru czerwonego określonego na przykład wartością "RGB"("224", "0", "0"), to Windows wyświetla w odpowiedniej proporcji punkty "RGB"("255", "0", "0") oraz na przykład "RGB"("192", "0", "0"). Najczęściej zjawisko takie jest jak najbardziej pożądane, czasami jednak wywołuje negatywne efekty. Wówczas funkcję "RGB" należy zastąpić funkcją "PaletteRGB". Jej parametrami są wprawdzie także stopnie intensywności poszczególnych barw podstawowych, ale w tworzonej liczbie formatu "TColorRef" wpisuje ona do pierwszego bajtu wartość 2, co stanowi sygnał dla systemu, że nie ma stosować techniki dithering, lecz odszukać odpowiedni, najbliższy kolor w palecie systemowej. Powyższe rozważania znalazły swoje odbicie w widocznych niżej zmianach procedury "TOkno.Rysunek". Szczególnie interesujące jest w niej porównanie sposobu wypełnienia figur 5 i 6 (dolnych środkowych). Mimo iż w obu przypadkach użyto pędzla utworzonego przez podanie tych samych stopni intensywności barw podstawowych, to jednak figura 5 została wypełniona kolorem mieszanym, natomiast 6 - czystym. Warto też zwrócić uwagę na kolejne zastosowanie funkcji "DeleteObject", która tym razem została użyta do usnięcia pędzli logicznych: PROCEDURE TOkno.Rysunek; VAR ... Pedzel4, Pedzel5, Pedzel6, Pedzel7, StaryPedzel : HBrush; BEGIN HKontWysw := GetDC(HWindow); ... {Figura 1} ... MoveTo(HKontWysw, 125, 100); LineTo(HKontWysw, 150, 50); LineTo(HKontWysw, 175, 150); LineTo(HKontWysw, 200, 100); {Figura 2} ... Rectangle(HKontWysw, 225, 25, 325, 75); {Figura 3} ... Ellipse(HKontWysw, 350, 25, 450, 75); SelectObject(HKontWysw, StarePioro); DeleteObject(Pioro1); DeleteObject(Pioro2); DeleteObject(Pioro3); {Figura 4} Pedzel4 := CreateSolidBrush(RGB(0, 255, 0)); StaryPedzel := SelectObject(HKontWysw, Pedzel4); Rectangle(HKontWysw, 225, 100, 275, 175); {Figura 5} Pedzel5 := CreateSolidBrush(RGB(224, 0, 0)); SelectObject(HKontWysw, 285, 100, 335, 175); {Figura 6} Pedzel6 := CreateSolidBrush(PaletteRGB(224, 0, 0)); SelectObject(HKontWysw, Pedzel6); Rectangle(HKontWysw, 345, 100, 395, 175); {Figura 7} Pedzel7 := CreateHatchBrush(hs_DiagCross, RGB(0, 0, 255)); SelectObject(HKontWysw, Pedzel7); Rectangle(HKontWysw, 405, 100, 455, 175); SelectObject(HKontWysw, StaryPedzel); DeleteObject(Pedzel4); DeleteObject(Pedzel5); DeleteObject(Pedzel6); DeleteObject(Pedzel7); ReleaseDC(HWindow, HKontWysw); END; Rys. 4.7. To tylko niektóre przykłady spośród praktycznie nieograniczonych możliwości wypełniania figur. 4.4. Zmiana palety kolorów. Pomimo zawrotnego tempa rozwoju sprzętu komputerowego obecne komputery osobiste pracują jeszcze często w trybie graficznym umożliwiającym jednoczesne wyświetlanie "tylko" 256, a czasami nawet 16 kolorów. Słowo "tylko" znalazło się w cudzysłowie, gdyż nie tak odległe są czasy, kiedy możliwość zastosowania palety składającej się z 256 barw stanowiło niemożliwe do zrealizowania marzenie wielu użytkowników komputerów. Jeśli jednak wziąć pod uwagę liczbę możliwych kombinacji kolorów podstawowych, to dochdzi się do wniosku, że standardowa postać takiej palety, reprezentującej pełne spektrum barw, okazuje się niewystarczająca do przedstawienia takich obrazów, jak na przykład zawierająca wiele odcieni zieleni roślinność czy malowniczy błękit nieba. Jeżeli jednak obraz taki ma zostać wiernie przedstawiony, to nie pozostaje nic innego, jak zmienić paletę systemową, zastępując w niej dotychczasowe kolory kolorami potrzebnymi do wykonania rysunku. W tym celu trzeba najpierw zdefiniować "paletę logiczną" i przypisać ją bieżącemu kontekstowi wyświetlania (analogicznie do zmiany czcionki, pióra i pędzla), a następnie dokonać jej "realizacji", to znaczy umieszczenia jej kolorów w palecie systemowej. Palety logiczne, podobnie jak wszystkie inne narzędzia logiczne "GDI", są całkowicie niezależne od sprzętu. Oznacza to na przykład, że zdefiniowana paleta logiczna może zawierać więcej kolorów, niż jest w stanie pomieścić bieżąca paleta fizyczna. Wówczas jednak niektórym kolorom fizycznym będą odpowiadać dwa lub więcej zbliżonych do siebie kolorów logicznych. Ponieważ kolory te są umieszczane w palecie fizycznej w kolejności ich występowania w palecie logicznej, wynika stąd, że powinno się je definiwać w kolejności od najbardziej do najmniej ważnego. Należy też wziąć pod uwagę fakt, że pewną liczbę kolorów palety fizycznej Windows rezerwuje dla własnych potrzeb, związanych z wyświetlaniem elementów standardowych okien - kolory te nie mogą zostać zastąpione innymi. Może się też zdarzyć, że z powodu ograniczeń bieżącego trybu graficznego nie wszystkie kolory logiczne mogą znaleźć swe dokładne odpowiedniki fizyczne. Wówczas reprezentowane są one w sposób przybliżony. Do zdefiniowania palety logicznej służy funkcja "CreatePalette". Pobiera ona w postaci parametru strukturę typu "TLogPalette", która ma następujące pola: - "palVersion" - liczbę określająca numer wersji Windows; dla wersji Windows 3.0 lub późniejszej powinna ona przyjąć wartość $300; - "palNumEntries" - liczbę kolorów zdefiniowanych w palecie logicznej; - "palPalEntry" - tablicę struktur typu "TPaletteEntry", opisującą poszczególne kolory palety. Dla każdego koloru trzeba wypełnić poszczególne pola struktury "TPaletteEntry". Są one następujące: - "peRed", "peGreen", "peBlue" - stopnie intensywności trzech barw podstawowych, które mogą przyjmować wartości w zakresie od 0 do 255; - "peFlags" - sposób późniejszego stosowania koloru, określony w standardowej palecie logicznej liczbą 0. Ze stosowaniem struktury "TLogPalette" wiąże się niebezpieczeństwo popełnienia pewnego błędu. Otóż jej pole "palPalEntry" zostało zdefiniowane jako "ARRAY[0..0] OF TPaIetteEntry", a więc standardowo jest ono w stanie pomieścić informacje dotyczące zaledwie jednego koloru. Ponieważ jednak zazwyczaj chcemy utworzyć paletę zawierającą znacznie więcej barw, przed ich zdefiniowaniem trzeba zarezerwować na tyle duży blok pamięci, żeby był on w stanie pomieścić wszystkie niezbędne struktury "TPaletteEntry" łącznie z polami "palVersion" i "palNumEntries" i przyporządkować jego adres wskaźnikowi do struktury "TLogPalette". Nie można oczywiście zapomnieć o konieczności zwolnienia tego bloku po usunięciu palety za pomocą funkcji "DeleteObject". Do rezerwowania pamięci służy w środowisku "TPW" funkcja "GetMem", która pobiera dwa parametry: adres tworzonego bloku pamięci oraz jego wielkość wyrażoną w bajtach. Do obliczenia wielkości struktury najlepiej skorzystać z funkcji "SizeOf", której należy podać w postaci parametru nazwę typu. Natomiast zwolnienie zajętego bloku można uzyskać przez wywołanie procedury standardowej "FreeMem", mającej takie same parametry jak "GetMem". Przyporządkowanie zdefiniowanej palety logicznej kontekstowi wyświetlania nastąpi po wywołaniu funkcji "SelectPalette", która oprócz tradycyjnych parametrów w postaci uchwytu kontekstu i palety wymaga podania jeszcze jednej dodatkowej liczby. Wartość tej liczby ma poważne konsekwencje praktyczne, określa ona bowiem, czy utworzona paleta ma być zawsze paletą drugoplanową (wartość "True"), czy nie (wartość "False"). Przekazanie wartości "False" spowoduje, że paleta będzie pierwszoplanowa zawsze wtedy i tylko wtedy), gdy okno, którego ona dotyczy, będzie aktywne. Priorytet palety ma istotne znaczenie podczas dokonywania jej realizacji. Wynika to stąd, że w pierwszej kolejności w palecie systemowej umieszczane są zawsze kolory palety pierwszoplanowej, a dopiero potem palet drugoplanowych. Jeżeli więc zależy nam na tym, aby zdefiniowane kolory były wyświetlane na ekranie monitora, to ostatniemu parametrowi funkcji "SelectPalette" powinniśmy przypisać wartość "False". Rezultatem funkcji "SelectPalette" jest - stanowi to jakby nie było standard funkcji "GDI" - uchwyt zastępowanego narzędzia, którym jest w tym przypadku poprzednio wykorzystywana paleta logiczna. Fakt ten można wykorzystać do przywrócenia standardowego ustawienia kontekstu wyświetlania. Aby zrealizować paletę, należy wywołać funkcję "RealizePalette", przekazując jej w postaci parametru uchwyt kontekstu wyświetlania. Wykonanie tej operacji jest absolutnie niezbędne do tego, aby kolory zdefiniowanej palety pojawiły się w palecie systemowej. Co się zaś tyczy stosowania tych kolorów, to można się na nie powoływać wszędzie tam, gdzie funkcja "GDI" oczekuje podania liczby typu "TColorRef", czyli zamiennie w stosunku do funkcji "RGB" i "PaletteRGB". Tym razem należy zamiast nich zastosować funkcję "Palettelndex", podając jej w postaci parametru kolejny numer pozycji palety. Wszystkie podane informacje zostały wykorzystane w praktyce w postaci procedury "TOkno.PaletaKolorow" przygotowywanej przez nas aplikacji, reagującej na wybór polecenia menu "GDI" w nim "Paleta". Procedura ta tworzy paletę logiczną zawierającą 16 kolorów będących kolejnymi odcieniami szarości. Jeżeli komputer pracuje w trybie graficznym 256 kolorów, to bez utworzenia takiej palety można wyświetlić jedynie 4 różne odcienie (ta sama uwaga dotyczy także innych kolorów, takich jak czerwony czy żółty). Dklaracja i zawartość metody "PaletaKolorow" wyglądają następująco: TOkno = OBJECT(TWindow) ... PROCEDURE PaletaKolorow(VAR Msg: TMessage); VIRTUAL cm_First+cm_Paleta; ... END; {Reakcja na wybór polecenia menu GDI|Paleta} PROCEDURE TOkno.PaletaKolorow; CONST LiczKol = 16; VAR HKontWysw: HDC; AdrOpisuPal: ^TLogPalette; Paleta, StaraPaleta: HPalette; NrKol: Integer; StaryPedzel, P: HBrush; Pedzle: ARRAY[0..LiczKol-1] OF HBrush; BEGIN HKontWysw := GetDC(HWindow); GetMem(AdrOpisuPal, SizeOf(TLogPalette) + SizeOf(TPaletteEntry) * LiczKol); WITH AdrOpisuPal^ DO BEGIN palVersion := $0300; palNumEntries := LiczKol; FOR NrKol := 0 TO LiczKol-1 DO BEGIN palPalEntry[NrKol].peRed := 16*(NrKol+1) -1; palPalEntry[NrKol].peGreen := 16*(NrKol+1) -1; palPalEntry[NrKol].peBlue :+ 16*(NrKol+1) -1; palPalEntry[NrKol].peFlags := 0; END; END; Paleta := CreatePalette(AdrOpisuPal^); StaraPaleta := SelectPalette(HKontWysw, Paleta, False); RealizePalette(HKontWysw); FOR NrKol := 0 TO LiczKol-1 DO BEGIN Pedzle[NrKol] := CreateSolidBrush(PaletteIndex(NrKol)); P := SelectObject(HKontWysw, Pedzle[NrKol]); IF NrKol = 0 THEN StaryPedzel := P; Rectangle(HKontWysw, 25*(NrKol+1), 50, 25*(NrKol+2), 150; END; SelectObject(HKontWysw, StaryPedzel); FOR NrKol := 0 TO LiczKol-1 DO DeleteObject(Pedzle[NrKol]); SelectPalette(HKontWysw, StaraPaleta, False); RealizePalette(HKontWysw); DeleteObject(Paleta); FreeMem(AdrOpisuPal, sizeof(TLogPalette) + sizeof(TPaletteEntry) * LiczKol); ReleaseDC(HWindow, HKontWysw); END; Rys. 4.8. Po wyborze polecenia "GDI" w nim "Paleta" program wyświetla 16 odcieni szarości. 4.5. Odtwarzanie zawartości okna. 4.5.1. Powtórne wyświetlanie grafiki. Wiele rodzajów pisma, różnorodność funkcji graficznych i bogactwo kolorów to nie jedyne wyzwania, jakie stoją przed programistą przystępującym do wyświetlania grafiki w aplikacji Windows. Jego kolega przygotowujący program działający w środowisku DOSa może być pewien, że wyprowadzone przez niego dane nie znikną w pewnym momencie z ekranu monitora, lecz zostaną tam tak długo, aż sam ich nie usunie. Nawet jeśli użytkownik uruchomi w pewnym momencie program rezydentny ("TSR"), to można oczekiwać, że prgram ten odtworzy zajęte przez siebie fragmenty ekranu. W inny sposób trzeba pisać aplikacje Windows, gdyż najczęściej muszą one same odtwarzać zawartość swojego okna. Istnieją wprawdzie takie sytuacje, w których wyręcza je w tym system (należy do nich zmiana położenia kursora i ikony, często także - zamykanie menu oraz okna dialogowego), ale w większości przypadków (między innymi po odsłonięciu fragmentu okna, zmianie jego wielkości oraz przesunięciu wskaźnika paska przewijania) to właśnie aplikacja odpowiedzialna jest za ponowne wyświetlenie grafiki. Ay się o tym przekonać, wystarczy uruchomić napisany program, wybrać jedno z poleceń menu "GDI", na przykład "Tekst", a następnie nieznacznie zmienić wielkość okna - znajdujący się w nim tekst natychmiast zniknie. Na szczęście za każdym razem, kiedy konieczne staje się odświeżenie zawartości okna lub którejś jego części, Windows przesyła aplikacji komunikat "wm_Paint", doprowadzając w ten sposób do wywołania funkcji "TWindow.Paint". Tak więc umieszczenie fragmentów programu tworzących grafikę w obrębie tej funkcji prowadzi automatycznie do zapobiegnięcia znikaniu zawartości okna. Dodatkową, istotną zaletą wynikającą z takiego rozwiązania jest fakt, że każdorazowo przed wyświetlaniem grafiki przez funkcję "Paint" system czyści zawartość całego okna (dzieje się tak przy standardowych ustawieniach programu). Operacja taka nie jest natomiast wykonywana automatycznie wtedy, gdy tworzenie grafiki ma miejsce w dowolnym innym miejscu aplikacji. Również i o tym łatwo się przekonać, wywołując w przygotowanej przez nas aplikacji kolejno dwa różne polecenia menu "GDI". Oczywiście takie ozwiązanie programowe jest absolutnie nie do przyjęcia. Można by wprawdzie zawartość okna czyścić samodzielnie, ale stanowiłoby to dla programisty niepotrzebne utrudnienie. Istnieje jeszcze jedna korzyść, jaką odnosi się ze stosowania funkcji "Paint". Jest nią brak konieczności uzyskiwania i zwalniania uchwytu kontekstu wyświetlania. Czynności te są bowiem wykonywane przez "ObjectWindows", odpowiednio bezpośrednio przed i po wywołaniu metody "Paint". Warto też wiedzieć, że do tego celu wykorzystywane są funkcje "API" "BeginPaint" i "EndPaint", które można wywołać tylko po otrzymaniu komunikatu "wm_Paint". Kontekst wyświetlania jest przekazywany metodzie "Paint" w postai parametru "PaintDC". W celu wykorzystania podanych wyżej informacji do ulepszenia naszego programu konieczne jest dokonanie w nim następujących zmian: - przykrycie procedury "TWindow.Paint" i przeniesienie do niej dotychczasowej treści metod "TOkno.Tekst", "TOkno.Rysunek" oraz "TOkno.PaletaKolorow"; - usunięcie deklaracji zmiennej "HKontWysw" i wywołań funkcji "GetDC" i "ReleaseDC" oraz zastosowanie zmiennej "PaintDC" zamiast "HKontWysw" jako uchwytu kontekstu wyświetlania; - spowodowanie, aby po wyborze dowolnego z poleceń menu "GDI" została wywołana procedura "TOkno.Paint"; - wprowadzenie zmiennej sterującej "Wysw", która określałaby, czy i jakie zostało wybrane polecenie menu "GDI"; zmienna ta powinna być "zerowana" w konstruktorze okna głównego ("TOkno.Init"), ustawiana w metodach "TOkno.Tekst", "TOkno.Rysunek" i "TOkno.PaletaKolorów" oraz sprawdzana w "TOkno.Paint". Wywołanie funkcji "Paint" następuje w wyniku zastosowania funkcji "API" "InvalidateRect", która między innymi powoduje przesłanie komunikatu "wm_Paint". Funkcja ta miała następujące parametry: - uchwyt okna; - wskaźnik do struktury "TRect" (typu "PRect") opisującej prostokąt, który ma zostać dołączony do tak zwanego obszaru nieważnego, czyli takiego, w którym ma zostać wyświetlona grafika (więcej na ten temat w dalszej części książki); jeśli parametr ten ma wartość "NIL", unieważniane jest całe okno (fakt ten wykorzystamy w naszym programie); - parametr określający, czy funkcja "BeginPaint" (wywoływana automatycznie przez "ObjectWindows" przed funkcją "Paint") ma wyczyścić tło obszaru nieważnego; jeśli tak, należy przekazać wartość "True", w przeciwnym wypadku - "False". Wszystkie omówione zmiany zostały przedstawione niżej w postaci tekstu źródłowego programu: TOkno + OBJECT(TWindow) Wysw: (Nic, WTekst, WRysunek, WPaleta); ... PROCEDURE Paint(PaintDC: HDC; VAR PaintInfo: TPaintStruct); VIRTUAL; ... END; CONSTRUCTOR TOkno.Init, BEGIN ... Wysw := Nic; ... END; PROCEDURE TOkno.Tekst; BEGIN Wysw := WTekst; InvalidateRect(HWindow, NIL, True); END; PROCEDURE TOkno.Rysunek; BEGIN Wysw := WRysunek; InvalidateRect(HWindow, NIL, True); END; PROCEDURE TOkno.PaletaKolorow; BEGIN Wysw := WPaleta; InvalidateRect(HWindow, NIL, True); END; {Wyświetlanie zawartości okna głównego} PROCEDURE TOkno.Paint; {Reakcja na wykonanie funkcji TOkno.Tekst} PROCEDURE Paint_Tekst(PaintDC: HDC; VAR PaintInfo: TPaintStruct); VAR ... BEGIN {Czcionka systemowa} StareWyrown := SetTextAlign(PaintDC, ta_Center OR ta_Bottom); ... TextOut(PaintDC, 240, 40, Napis, StrLen(Napis)); {Nowa czcionka 1} ... StaraCzcionka := SelectObject(PaintDC, Czcionka1); SetTextAlign(PaintDC, ta_Left OR ta_Top); StaryKolorTekstu := SetTextColor(PaintDC, RGB(255, 0, 0)); ... TextOut(PaintDC, 40, 70, Napis, StrLen(Napis)); {Nowa czcionka 2} ... SelectObject(PaintDC, Czcionka2); SetTextAlign(PaintDC, ta_Center OR ta_Top); SetTextColor(PaintDC, RGB(255, 255, 0)); StaryKolorTla := SetBkColor(PaintDC, RGB(0, 0, 255)); ... TextOut(PaintDC, 240, 100, Napis, StrLen(Napis)); {Nowa czcionka 3} ... SelectObject(PaintDC, Czcionka3); SetTextAlign(PaintDC, ta_Right OR ta_Top); SetTextColor(PaintDC, RGB(255, 255, 255)); SetBkColor(PaintDC, RGB(0, 0, 0)); ... TextOut(PaintDC, 440, 130, Napis, StrLen(Napis)); SetTextColor(PaintDC, StaryKolorTekstu); SetBkColor(PaintDC, StaryKolorTla); SetTextAlign(PaintDC, StareWyrown); SelectObject(PaintDC, StaraCzcionka); ... END; {Reakcja na wykonanie funkcji TOkno.Rysunek} PROCEDURE Paint_Rysunek(PaintDC: HDC; VAR PaintInfo: TPaintStruct); VAR ... BEGIN {Piksele} FOR X := 20 TO 83 DO FOR Y := 70 TO 133 DO SetPixel(PaintDC, X, Y, RGB(4*(X-20), 0, 4*(Y-70))); {Figura 1} ... StarePioro := SelectObject(PaintDC, Pioro1); MoveTo(PaintDC, 125, 100); LineTo(PaintDC, 150, 50); LineTo(PaintDC, 175, 150); LineTo(PaintDC, 200, 100); {Figura 2} ... SelectObject(PaintDC, Pioro2); Rectangle(PaintDC, 225, 25, 325, 75); {Figura 3} ... SelectObject(PaintDC, Pioro3); Ellipse(PaintDC, 350, 25, 450, 75); SelectObject(PaintDC, StarePioro); ... {Figura 4} ... StaryPedzel := SelectObject(PaintDC, Pedzel4); Rectangle(PaintDC, 225, 100, 275, 175); {Figura 5} ... SelectObject(PaintDC, Pedzel5); Rectangle(PaintDC, 285, 100, 335, 175); {Figura 6} ... SelectObject(PaintDC, Pedzel6); Rectangle(PaintDC, 345, 100, 395, 175); {Figura 7} ... SelectObject(PaintDC, Pedzel7); Rectangle(PaintDC, 405, 100, 455, 175); SelectObject(PaintDC, StaryPedzel); ... END; {Reakcja na wykonanie funkcji TOkno.Paleta} PROCEDURE Paint_Paleta(PaintDC: HDC; VAR PaintInfo: TPaintStruct); CONST ... VAR ... BEGIN ... StaraPaleta := SelectPalette(PaintDC, Paleta, False); RealizePalette(PaintDC); FOR NrKol := 0 TO LiczKol-1 DO BEGIN ... P := SelectObject(PaintDC, Pedzle[NrKol]); ... Rectangle(PaintDC, 25*(NrKol+1), 50, 25*(NrKol+2), 150); END; SelectObject(PaintDC, StaryPedzel); ... SelectPalette(PaintDC, StaraPaleta, False); RealizePalette(PaintDC); ... END; {Blok główny procedury TOkno.Paint} BEGIN IF Wysw = WTekst THEN Paint_Tekst(PaintDC, PaintInfo) ELSE IF Wysw = WRysunek THEN Paint_Rysunek(PaintDC, PaintInfo) ELSE IF Wysw = WPaleta THEN Paint_Paleta(PaintDC, PaintInfo); END; 4.5.2. Odtwarzanie kolorów. Nie tylko tekst i rysunki wyświetlane w oknach aplikacji Windows wymagają odnawiania; czynność tę należy także wykonywać w stosunku do kolorów. Omawiając sposób utworzenia palety logicznej wspomniałem o tym, że tylko jedna spośród wszystkich istniejących w systemie palet logicznych jest pierwszoplanowa, czyli wyróżniona priorytetem w dostępie do palety systemowej. Jeżeli więc okno naszej aplikacji traci stan aktywny na rzecz innego okna, które realizuje swoją, paletę logiczną, to może się zdarzyć, ż wyświetlane przez nas kolory stają się nagle zupełnie inne. Póki okno pozostaje nieaktywne, nie ma to większego znaczenia, ale po jego ponownym uaktywnieniu przywrócenie oryginalnych kolorów staje się absolutną koniecznością. Zanim przejdziemy do rozwiązania tego problemu, skupimy się przez moment na pewnej bardzo prostej aplikacji przykładowej. Jej jedynym zadaniem jest przechwycenie komunikatu informującego o naciśnięciu lewego przycisku myszy i zareagowanie na niego w postaci utworzenia palety logicznej zawierającej 256 różnych odcieni zieleni, zaprezentowania jej, a następnie usunięcia. Przeanalizowanie tekstu źródłowego tej aplikacji można potraktować jako powtórkę przyswojonych do tej pory wiadomości, ale głównym celem umieszczenia jej tutaj było umożliwienie praktycznego zapoznania się z równoczesnym funkcjonowaniem kilku palet logicznych, a zwłaszcza z pułapkami, jakie czyhają w takiej sytuacji na programistę. W tym celu należy uruchomić oba przygotowane przez nas programy, "Pierwszą aplikację Windows" oraz "Paletę kolorów", po czym w pierwszym z nich wybrać polecenie "GDI" w nm "Paleta", a w drugim - nacisnąć lewy przycisk myszy. Równocześnie z pojawieniem się zielonego paska w oknie "Palety kolorów" kolor niemalże wszystkich elementów rysunku "Pierwszej aplikacji Windows" staje się zielony i pozostaje takim także wtedy, gdy okno tego programu uzyskuje stan aktywny (w wyniku działania użytkownika). Oczywiście rozwiązanie programowe, które prowadzi do powstania takiej sytuacji, jest nie do zaakceptowania. Pełny tekst nowej aplikacji wygląda następująco: {****************************************************************} { } {Paleta kolorów } {****************************************************************} PROGRAM Paleta; USES WObjects, WinTypes, WinProcs; TYPE {Obiekt reprezentujący aplikację} TAplikacja = OBJECT(TApplication) PROCEDURE InitMainWindow; VIRTUAL; END; {Obiekt reprezentujący okno główne aplikacji} POkno = ^TOkno; TOkno = OBJECT(TWindow) CONSTRUCTOR Init(AParent: PWIndowsObject; Atitle: PChar); PROCEDURE WMLButtonDown(VAR Msg: TMessage); VIRTUAL wm_First+wm_LButtonDown; END; VAR Aplikacja: TAplikacja; {Utworzenie okna głównego aplikacji} PROCEDURE TAplikacja.InitMainWindow; BEGIN MAINWindow := New(POkno, Init(NIL, 'Paleta kolorow')); END; {Konstruktor obiektu reprezentującego okno główne} CONSTRUCTOR TOkno.Init; BEGIN TWindow.Init(NIL, ATitle); WITH Attr DO BEGIN X := 0; Y :+ 240; W := 320; H := 120; END; END; {Reakcja na naciśnięcie lewego przycisku myszy} PROCEDURE TOkno.WMLButtonDown; CONST LiczKol = 256; VAR HKontWysw: HDC; AdrOpisuPal: ^TLogPalette; NrKol, Y: Integer; BEGIN HKontWysw := GetDC(HWindow); GetMem(AdrOpisuPal, SizeOF(TLogPalette) + SizeOF(TPaletteEntry) * LiczKol); WITH AdrOpisuPal^ DO BEGIN palVersion := $0300; PalNumEntries := LiczKol; FOR NrKol := 0 TO LiczKol-1 DO BEGIN palPalEntry[NrKol].peRed := 0; palPalEntry[NrKol].peGreen := 255-NrKol; palPalEntry[NrKol].peBlue :=0; palPalEntry[NrKol].peFlags := 0; END; END; Paleta := CreatePalette(AdrOpisuPal^); StaraPaleta := SelectPalette(HKontWysw, Paleta, False); RealizePalette(HKontWysw); FOR NrKol := 0 TO LiczKol-1 DO FOR Y := 40 TO 44 DO SetPixel(HKontWysw, 25+NrKol, Y, PaletteIndex(NrKol)); SelectPalette(HKontWysw, StaraPaleta, False); RealizePalette(HKontWysw); DeleteObject(Paleta); FreeMem(AdrOpisuPal, sizeof(TLogPalette) + sizeof(TPaletteEntry) * LiczKol); ReleaseDC(HWindow, HKontWysw); END; {Blok główny programu} BEGIN Aplikacja.Init('Pierwsza aplikacja Windows'); Aplikacja.Run; Aplikacja.Done; END. Aby zapobiec opisanemu negatywnemu sposobowi zachowania się przygotowywanej przez nas aplikacji, należy spowodować, aby fragment funkcji "Paint" dotyczący palety wykonywała ona ponownie także i wtedy, gdy w czasie wyświetlania odcieni szarości jakiś inny program dokonał zmian w palecie systemowej. Na szczęście o każdej zmianie tej palety informuje nas Windows, przekazując komunikat "wm_PaletteChanged". Wystarczy więc przypisać mu nową metodę okna głównego, która ustawiałaby pewną zmienną sterującą -nazwijmy ją "ZmPal" - informującą o tym, że paleta systemowa uległa modyfikacji. Ponieważ jednak komunikat "wm_PaletteChanged" przekazywany jest także wtedy, gdy sprawcą tej modyfikacji była nasza własna aplikacja, przed ustawieniem zmiennej "ZmPal" należy sprawdzić uchwyt okna, które dokonało zmiany, przekazywany w postaci parametru "Msg.WParam". Ponadto nie ma sensu ustawiać zmiennej "ZmPal" wtedy, gdy aktualnie nie są wyświetlane odcienie szarości. Jeżeli zmienna "ZmPal" została ustawiona, to wywołanie funkcji "Paint" powinno nastąpić natychmiast po ponownym uzyskaniu przez nasze okno stanu aktywności. Również i o tym zdarzeniu informuje nas Windows, przekazując komunikat "wm_SetFocus" (następuje to wtedy, gdy okno uzyskuje możliwość przechwytywania komunikatów informujących o naciskaniu klawiszy). Należy więc przypisać mu kolejną procedurę, generującą komunikat wywołujący funkcję "Paint" oraz zerującą zmienną "ZmPal". Nie można też zapomnieć konieczności zadeklarowania tej zmiennej oraz wyzerowania jej w konstruktorze obiektu okna głównego, "TOkno.Init". A oto wszystkie niezbędne uzupełnienia tekstu źródłowego programu: TOkno = OBJECT(TWindow) ... ZmPal : Boolean; ... PROCEDURE OknoAktywne(VAR Msg: TMessage); VIRTUAL wm_First+wm_SetFocus; PROCEDURE ZmianaPalety(VAR Msg: TMessage); VIRTUAL wm_First+wm_PaletteChanged; ... END; CONSTRUCTOR TOkno.Init; BEGIN ... ZmPal := False; ... END; {Reakcja na uaktywnienie okna} PROCEDURE TOkno.OknoAktywne; BEGIN IF ZmPal + True THEN BEGIN ZmPal := False; InvalidateRect(HWindow, NIL, True); END END; {Reakcja na zmianę palety systemowej} PROCEDURE TOkno.ZmianaPalety; BEGIN IF (Msg.WParam <> Hwindow) AND (Wysw = WPaleta) THEN ZmPal := True; END; 4.5.3. Prostokąt nieważny i obcinający. Metoda "Paint" okna "TOkno" wywoływana jest nie tylko z parametrem "PaintDC", ale także ze zmienną "PaintInfo", stanowiącą strukturę danych "TPaintStruct". Struktura ta zawiera kilka pól, z których najważniejsze, o nazwie "rcPaint", określa lewy górny i prawy dolny róg wspomnianego już wcześniej "prostokąta nieważnego" (ang. "invalid rectangle"). Parametr "rcPaint" jest strukturą typu "TRect", zawierającą cztery pola: "Left", "Top", "Right", "Bottom". Prostokąt nieważny opisuje obszar okna, którego awartość powinna ulec odtworzeniu. Często bowiem się zdarza, że nie jest konieczne ponowne wyświetlanie grafiki na powierzchni całego okna, lecz tylko na jego małym fragmencie. Prostokąt nieważny przekazany procedurze "Paint" jest jednocześnie "prostokątem obcinającym" (ang. "clipping rectangle"), to znaczy takim, poza granicami którego Windows nie wyświetla żadnej grafiki, także wtedy, gdy program kieruje tam swoje dane. Warto też pamiętać o tym, że w ogólnym przypadku mówimy o obszarze nieważnym (ang. "invalid region"), nazywanym także obszarem uaktualniania (ang. "update region") oraz o obszarze obcinającym (ang. "clipping region"). Obszary te mogą mieć kształt inny niżprostokątny. Zmiana obu rodzajów obszarów może nastąpić zarówno w wyniku działań użytkownika, na przykład po odsłonięciu fragmentu okna, jak i też przez wywołanie odpowiedniej funkcji przez samą aplikację. Drugi z wymienionych przypadków ma miejsce wtedy, gdy program chce wyświetlić grafikę. Wówczas jego zadaniem jest dołączenie fragmentu okna, w którym grafika ta ma się pojawić, zarówno do obszaru obcinającego (aby umożliwić jej wyświetlenie), jak i też nieważnego (w celu powiadomienia procedury "Paint" o położniu obszaru, którego zawartość wymaga odświeżenia). Ponieważ zmiana obszaru nieważnego pociąga za sobą automatycznie zmianę obszaru obcinającego, wystarczy unieważnienie odpowiedniego fragmentu okna (lub całego obszaru roboczego). Zadania tego dokonujemy w naszej aplikacji za pomocą funkcji "InvalidateRect". Mimo iż korzystanie z przekazanych przez Windows współrzędnych prostokąta nieważnego nie jest wymagane z punktu widzenia prawidłowości działania programu, to jednak niekiedy pozwala ono na jego znaczne przyspieszenie. Sytuacja taka ma miejsce wtedy, gdy wyświetlanie grafiki wiąże się z koniecznością wykonywania bardzo dużej liczby skomplikowanych obliczeń. Aby się o tym przekonać, rozwińmy naszą aplikację o procedurę wyświetlającą w wyniku wyboru polecenia "GDI" w nim "Fraktal" pewien fraktal. Zmiany, które trzeba w tym celu dokonać w tekście źródłowym programu, przedstawiają się następująco: TOkno = OBJECT(TWindow) Wysw: (Nic, WTekst, WRysunek, WPaleta, WFraktal); ... PROCEDURE Fraktal(VAR Msg: TMessage); VIRTUAL cm_First=cm_Fraktal; ... END; PROCEDURE TOkno.Paint; ... {Reakcja na wykonanie funkcji TOkno.Fraktal} PROCEDURE Paint_Fraktal(PaintDC: HDC; VAR PaintInfo: TPaintStruct); CONST Xl = 110; Xp = 360; Yg = 20; Yd = 170; VAR X, Y, I: Integer; V, V0, V1, W, W0: Real; BEGIN FOR X := Xl TO Xp DO BEGIN FOR Y := Yg TO Yd DO BEGIN I := 0; V := 0; W := 0; V0 := (2.65*(X-Xl)+1.5*(X-Xp))/(Xp-Xl); W0 := -2*((Y-Yg)+(Y-Yd))/(Yd-Yg); REPEAT I := I+1; V1 := V; V := Sqr(V)+Sqr(W)-V0; W := 2*V1*W-W0; UNTIL (Sqr(V)+Sqr(W) > 100) OR (I >= 10); IF (I = 10) THEN SetPixel(PaintDC, X, Y, RGB(0, 0, 0)); ELSE IF (I < 50) THEN IF Odd(I) THEN SetPixel(paintDC, X, Y, RGB(255, 0, 0); ELSE IF (Odd(X) AND Odd(Y)) THEN SetPixel(PaintDC, X, Y, RGB(0, 0, 255)); END; END; END; {Blok główny procedury TOkno.Paint} BEGIN IF Wysw = WTekst THEN Paint_Tekst(PaintDC, PaintInfo) ELSE IF Wysw = WRysunek THEN Paint_Rysunek(PaintDC, PaintInfo) ELSE IF Wysw = WPaleta THEN Paint_Paleta(PaintDC, PaintInfo) ELSE IF Wysw = WFraktal THEN Paint_Fraktal(PaintDC, PaintInfo); END; {Reakcja na wybór polecenia menu GDI|Fraktal} PROCEDURE TOkno.Fraktal; BEGIN Wysw := WFraktal; InvalidateRect(HWindow, NIL, True); END; Rys. 4.9. Jak można oczekiwać z nazwy, wywołanie funkcji "GDI" w niej "Fraktal" prowadzi do wyświetlenia pewnego interesującego fraktala. Nie wdając się w szczegóły dotyczące działania algorytmu tworzącego fraktal, zauważmy jedynie, że obiekt ten zajmuje powierzchnię w kształcie prostokąta o współrzędnych lewego górnego rogu (110; 20) i prawego dolnego rogu (360; 170) - wartości te przypisane są czterem stałym na początku procedury "Paint_Fraktal". Zapełnienie tego stosunkowo małego wycinka ekranu zajmuje niespodziewanie dużo czasu, na komputerze 486 DX/100 - 7 sekund, przy czym zdecydowana większość tego czasu poświęcana jest na wykoanie obliczeń związanych z utworzeniem fraktala, a nie na jego wyświetlenie. Pomimo tego faktu każdorazowo, kiedy w wyniku przesunięcia okna dowolnego programu działającego w Windows odsłaniany jest fragment okna naszej aplikacji, cały rysunek tworzony jest od nowa, co powoduje bezruch reszty systemu. Dzieje się tak także i wtedy, gdy osłonięty fragment obejmuje choćby jeden piksel fraktala, a nawet, gdy leży całkowicie poza nim. Trzeba przyznać, że jest to w stanie wyprowadzić z równowagi nawet najbardziej cierpliwego użytkownika komputera. Wszystkich zainteresowanych zachęam gorąco do samodzielnego wykonania opisanego "doświadczenia". Z podanego przykładu widać dobitnie, że konieczne staje się ograniczenie wyświetlania fraktala do powierzchni prostokąta nieważnego (obcinającego). Staje się to możliwe po odczytaniu wartości zapisanych w polu "rcPaint" parametru "Paintlnfo" metody "TOkno.Paint" i porównaniu ich z współrzędnymi określającymi powierzchnię zajmowaną przez fraktal. W celu zwiększenia przejrzystości procedury "Paint Fraktal" dodatkowo zostały zdefiniowane dwie funkcje pomocnicze, obliczające wartość minimalną i maksymalą dwóch liczb: PROCEDURE Paint_Fraktal(PaintDC: HDC; VAR PaintInfo: TPaintStruct); CONST ... VAR Xmin, Xmax, Ymin, Ymax: Integer; ... FUNCTION Min(A, B: Integer); Integer; BEGIN IF A < B THEN Min := A ELSE Min := B; END; FUNCTION Max(A, B: Integer): Integer; BEGIN IF A > B THEN Max := B; END; BEGIN WITH PaintInfo.rcOaint DO BEGIN Xmin := Max(Xl, left); Xmax :=(Xp, right); Ymin := Max(Yg, top); Ymax := Min(Yd, bottom); END; FOR X := Xmin TO Xmax DO BEGIN FOR Y := Ymin TO Ymax DO BEGIN ... END; END; END; Na zakończenie tego podrozdziału zauważmy, że istnieje jeszcze jeden sposób przyspieszenia wyświetlania grafiki, prostszy, ale za to mniej dokładny. Polega on na wywołaniu funkcji "RectVisible", która określa, czy przekazany jej w postaci parametru prostokąt "R" ma część wspólną z obszarem obcinającym. Jeśli tak, zwracana jest wartość "True", w przeciwnym wypadku - "False". Prostokąt "R" należy tak zdefiniować, aby obejmował on całą tworzoną grafikę lub jej wybrany element. Do tego celu najlepiej wyorzystać funkcje "API" "SetRect", której przekazuje się współrzędne lewego górnego i prawego dolnego rogu prostokąta. Dzięki takiemu rozwiązaniu program może tworzyć ponownie tylko te elementy rysunku, które przynajmniej częściowo znalazły się w obszarze obcinającym. 4.6. Sprawdzanie wielkości obszaru roboczego. Do utrudnień związanych z wyświetlaniem grafiki w środowisku Windows należy nie tylko potrzeba odtwarzania zawartości okna, ale także konieczność uwzględniania bieżącej wielkości obszaru roboczego. Programista przygotowujący aplikację Windows musi się bowiem liczyć z tym, że korzystający z niej użytkownik może w każdej chwili zmienić rozmiary jej okna (wykonanie tej czynności można wprawdzie programowo uniemożliwić, ale czyni się tak tylko w sytuacjach wyjątkowych). Ponadto większość aplikacji nie utala początkowego położenia i wielkości swoich okien, lecz odtwarza wartości zapamiętane podczas ostatniego zakończenia ich działania. Oba podane fakty przemawiają za tym, żeby każdorazowo przed wyświetlaniem grafiki sprawdzać wielkość obszaru roboczego okna. W tym celu należy skorzystać z funkcji "API" "GetClientRect", która pobiera w postaci parametrów uchwyt okna oraz zmienną typu "TRect", umożliwiającą zapisanie jego współrzędnych. W zasadzie interesująca jest jedynie wartość dwóch ostatnich pól struktury "TRect", "Right" i "Bottom", gdyż lewy górny wierzchołek obszaru roboczego ma zawsze współrzędne (0; 0). Po wywołaniu funkcji "GetClientRect" trzeba oczywiście wykorzystać uzyskane w ten sposób wartości w celu odpowiedniego ukształtowania wyświetlanej grafiki. Rodzaj wybranego rozwiązania zależy od konkretnej aplikacji; może on polegać zarówno na zmodyfikowaniu ilości i wielkości wyświetlanego tekstu i rysunków, jak i na zmianie ich rozmieszczenia na powierzchni okna. Ponowne wyświetlenie grafiki na skutek zmiany wielkości okna dokonanej przez użytkownika nie wymaga wykonywania żadnych dodatkowych działań w programie, gdyż system generuje w takiej sytuacji komunikat "wm_Paint" (co, jak pamiętamy, powoduje wywołanie funkcji "TWindow.Paint"), unieważniając jednocześnie cały obszar roboczy okna. W celu zilustrowania podanych informacji przykładem rozwińmy naszą aplikację o realizację polecenia menu "GDI" w nim "Powieść", polegającą na wyświetleniu pierwszego akapitu słynnej powieści Andrzeja Szczypiorskiego, "Początek". Aby tekst tego akapitu został wyświetlony wewnątrz prostokąta o podanych wymiarach, skorzystamy z kolejnej, nie stosowanej wcześniej funkcji "GDI", "DrawText". Podobnie jak odpowiadająca jej funkcja "TextOut", oczekuje ona podania kontekstu wyświetlania, tekstu (typu "PChar" oraz jego długości (wartość tego parametru, która wynosi -1, poleca wyświetlenie całego przekazanego łańcucha tekstowego, aż do napotkania końcowego zera). Jednak zamiast współrzędnych lewego górnego rogu tekstu tym razem należy określić prostokąt, w którym tekst ten ma zostać wyświetlony. Ponadto istnieje jeszcze jeden parametr funkcji "DrawText", umożliwiający określenie sposobu, w jaki tekst ma zostać wyświetlony. Parametr ten powinien być sumą logiczną odpowiednich predefiniowanych stałych, rozpoczynających się od przedrostka "dt_". Na przykład, stała "dt_Left" oznacza, że tekst ma zostać wyrównany do lewej strony, natomiast "dt_WordBreak", że w chili dojścia do prawego boku prostokąta ograniczającego wyświetlany tekst ma być przełamywany do następnego wiersza. Na początek załóżmy, że okno główne naszej aplikacji ma zawsze rozmiary 480 na 320 pikseli: TOkno = OBJECT(TWindow) Wysw: (Nic, WTekst, WPoczatek, WRysunek, WPaleta,, WFraktal); ... PROCEDURE Poczatek(VAR msg: TMessage); VIRTUAL cm_First=cm_Poczatek; ... END; PROCEDURE TOkno.Paint; ... {Reakcja na wykonanie funkcji TOkno.Poczatek} PROCEDURE Paint_Poczatek(PaintDC: HDC; VAR PaintInfo: TPanitStruct); VAR Obszar: TRect; Tekst : ARRAY[0..1024] OF Char; BEGIN SetRect(Obszar, 20, 16, 452, 178); WITH Obszar DO Rectangle(PaintDC, Left-3, Top-1, Right+3, Bottom+1); StrCopy(Tekst, 'W pokoju panował półmrok, ponieważ '); StrCat(Tekst, 'sędzia był miłośnikiem półmroku. Jego '); StrCat(Tekst, 'myśli, zwykle niedokończone i mgliste, '); StrCat(Tekst, 'niechętnie wpadały w pułapkę światła. '); StrCat(Tekst, 'Wszystko na świecie jest ciemne '); StrCat(Tekst, 'i niejasne, a sędzia lubił zgłębiać świat, '); StrCat)Tekst, 'przeto siadywał zwykle w kącie ogromnego '); StrCat(Tekst, 'salonu, w fotelu na biegunach, z głową '); StrCat(Tekst, 'odchyloną ku tyłowi, tak aby myśli '); StrCat(Tekst, 'kołatały się łagodnie w rytm fotela, '); StrCat(Tekst, 'wprawianego w ruch lekkim dotykiem stopy, '); StrCat(Tekst, 'raz lewej, raz prawej. Na stopach miał '); StrCat(Tekst, 'pantofle filcowe do kostki, zapinane na '); StrCat(Tekst, 'metalową haftkę. Haftki błyszczały '); StrCat(Tekst, 'niebieskawo na tle dywanu, gdy padało na '); StrCat(Tekst, 'nie światło lampy, ocienionej abażurem.'); DrawText(PaintDC, Tekst, -1, Obszar, dt_Left OR dt_WordBreak); END; ... {Blok główny procedury TOkno.Paint} BEGIN IF Wysw = WTekst THEN Paint_Tekst(PaintDC, PaintInfo) ELSE IF Wysw = WPoczatek THEN Paint_Poczatek(PaintDC, PaintInfo) ELSE IF Wysw = WRysunek THEN Paint_Rysunek(PaintDC, PaintInfo) ELSE IF Wysw = WPaleta THEN Paint_Paleta(PaintDC, PaintInfo) ELSE IF Wysw = WFraktal THEN Paint_Fraktal(PaintDC, PaintInfo); END; {Reakcja na wybór polecenia GDI|Powieść} PROCEDURE TOkno.Poczatek BEGIN Wysw := WPoczatek; BEGIN Wysw := WPoczatek; InvalidateRect(HWindow, NIL, True); END; Rys.4.10. Wybór polecenia "GDI" w nim "Powieść" powoduje wyświetlenie początkowego akapitu słynnej powieści. Wybór polecenia "GDI" w nim "Powieść" tak zmodyfikowanej aplikacji prowadzi wprawdzie do prawidłowego wyświetlenia tekstu, ale tylko wówczas, kiedy wielkość okna aplikacji pozostaje niezmieniona. Jeżeli rozmiary prostokąta ograniczającego tekst mają być dostosowywane do aktualnej wielkości okna, konieczne jest wprowadzenie pewnej modyfikacji do procedury "Paint_Poczatek": {Reakcja na wykonanie funkcji TOkno.Poczatek} PROCEDURE Paint_Poczatek(PaintDC: HDC; VAR PaintInfo: TPaintStruct); VAR ... BEGIN GetClientRect(HWindow, Obszar); WITH Obszar DO BEGIN SetRect(Obszar, 20, 16, Right-20, Bottom-16); Rectangle(PaintDC, Left-3, Top-1, Right+3, Bottom+1); END; ... END; Rys. 4.11. Zmiana wielkości okna pociąga za sobą automatyczną zmianę ułożenia tekstu. Rozdział 5. Zarządzanie oknami w aplikacjach Windows. Oprócz okna głównego i okien informacyjnych, do których się na razie ograniczaliśmy, aplikacje Windows pozwalają na ogół na otwarcie znacznie większej liczby okien, do których należą zarówno te w tradycyjnym rozumieniu tego słowa, czyli służące do wyświetlania tekstu i rysunków, jak i okna dialogowe. Oknami są nawet elementy sterujące okien dialogowych oraz przyciski znajdujące się w paskach narzędzi! Na poczatek zajmiemy się podstawowymi typami okien, do których należą tak zwane okna niezależne, rerezentujące aplikacje i jej główne moduły, oraz okna dokumentów. Te ostatnie służą najczęściej do redagowania zawartości plików ze sformatowanym tekstem, rysunkami czy arkuszami kalkulacyjnymi i są zarządzane zgodnie z tak zwaną, specyfikacją "MDI". Na zakończenie rozdziału będzie mowa o tym, jak można sterować zawartością okna za pomocą pasków przewijania. 5.1. Okna niezależne. 5.1.1. Podstawowe wiadomości. "Okna niezależne" (ang. "overlapped windows") charakteryzują się tym, że są wyświetlane na ekranie w całkowitej niezależności od siebie. Oznacza to, że ich położenie i wielkość jest określana we współrzędnych ekranowych oraz, że przesunięcie lub zmiana wielkości jednego z nich nie wpływa na postać pozostałych. Jeśli okno niezależne zostaje zmniejszone do ikony, to zostaje ona umieszczona bezpośrednio na pulpicie Windows. Istotne jest też to, że dowolne okno niezależne można w dowolnej chwili uaktywnć bez konieczności zamykania pozostałych. Tę ostatnią właściwość mają zresztą także tak zwane okna zależne (w tym okna dokumentów), w przeciwieństwie do zdecydowanej większości okien dialogowych, którymi będziemy się zajmować w następnym rozdziale. Istnieją dwa główne rodzaje okien niezależnych: "nakładane" (ang. "overlapped" lub "overlapped formal") i rozwijalne (ang. "popup"). Zarówno jedne, jak i drugie, mają z reguły pasek tytułowy oraz przycisk menu systemowego, ale podczas, gdy w oknach rozwijalnych aktywne są jedynie polecenia "Przesuń", "Zamknij" i "Przelącz na", to okna nakładane pozwalają dodatkowo na zmianę swej wielkości, a także zmniejszenie ich do postaci ikony i rozwinięcie do powierzchni całego ekranu. W ślad za tym idzie obecnść poleceń "Przywróć", "Rozmiar", "Do ikony" i "Pelny ekran", a także odpowiednich przycisków znajdujących się w prawej części paska tytułu oraz podwójnej ramki umożliwiającej zwiększanie i zmniejszanie okna (ramka okien rozwijalnych jest pojedyncza). Ponadto okna nakładane mogą mieć pasek menu. Najbardziej typowym przykładem tego typu okien jest okno główne aplikacji. Jak więc widać, okna nakładane są w wysokim stopniu samodzielne. Przykładem aplikacji, która czyni z nich użytek, jest "Microsoft Visual Basic". Jedno z okien nakładanych służy tam do wyświetlenia listy modułów przygotowywanego programu, drugie umożliwia zaprojektowanie tak zwanej formy, czy postaci okna odpowiadającego temu modułowi, a trzecie pozwala wprowadzić jego kod źródłowy. Okna nakładane okazują się więc przydatne wszędzie tam, gdzie program udostępnia kilka różnych, nie podporządkowanych sbie funkcji. Co się natomiast tyczy okien rozwijalnych, to są one stosowane dużo rzadziej i pojawiają się z reguły w celu wyświetlenia informacji, na przykład tekstu pomocy. Wszystkie okna za wyjątkiem okna głównego aplikacji mają swoje "okno nadrzędne", od którego są w dość znacznym stopniu uzależnione. W praktyce oznacza to na przykład, że zamknięcie danego okna powoduje także zamknięcie wszystkich jego okien podrzędnych; analogiczna uwaga dotyczy odtwarzania okna z postaci ikony. Struktura okien ma także istotny wpływ na drogę pokonywaną przez dotyczące ich komunikaty. Niektóre z nich przekazywane są najpierw do docelowego okna, a jeśli nie zostaną przez nie przetworzone, poruszają, się ścieżką prowadzącą do kolejnych okien nadrzędnych; inne natomiast trafiają najpierw do okna głównego, a dopiero potem, jeśli nie zostaną przez nie przyjęte, przechodzą stopniowo na coraz niższy poziom. 5.1.2. Tworzenie okien nakładanych i rozwijalnych. Obecnie możemy przystąpić do wykorzystania podanych informacji w celu uzupełnienia naszego programu o możliwości wyświetlania obu omówionych rodzajów okien. Aby tego dokonać, w pierwszej kolejności należy rozbudować definicję obiektu okna głównego o metody reagujące na wybór poleceń "Okno" w nim "Nakladane" i "Okno" w nim "Rozwijalne": TOkno = OBJECT(TWindow) ... PROCEDURE Nakladane(VAR Msg: TMessage); VIRTUAL cm_First+cm_Nakladane; PROCEDURE Rozwijalne(VAR Msg: TMessage); VIRTUAL cm_First+cm_Rozwijalne; ... END; Mimo iż moglibyśmy już przystąpić do wpisywania zawartości nowych metod, to jednak sensowne wydaje się wcześniejsze zdefiniowanie dwóch dodatkowych obiektów, reprezentujących otwierane okna podrzędne i będących potomkami obiektu "TWindow". Wykonanie tej operacji nie jest wprawdzie konieczne, ale zaniechanie jej uniemożliwiłoby wyświetlenie jakiejkolwiek grafiki w nowych oknach. Do tego potrzebne jest przecież przykrycie ich metod "TWindow.Paint": {Obiekt okna nakładanego} POknoNakladane = ^TOknoNakladane; TOknoNakladane = OBJECT(TWindow) PROCEDURE Paint(PaintDC: HDC; VAR PaintInfo: TPaintStruct); VIRTUAL; END; {Obiekt okna rozwijalnego} POknoRozwijalne = ^TOknoRozwijalne; TOknoRozwijalne = OBJECT(TWindow) PROCEDURE Paint(PaintDC: HDC; VAR PaintInfo: TPaintINfo: TPaintStruct); VIRTUAL; END; Jak pamiętamy, pierwszy parametr konstruktora "Init" obiektu "TWindow" jest wskaźnikiem do obiektu reprezentującego okno nadrzędne. Ponieważ oknem takim w stosunku do obu tworzonych okien jest okno główne aplikacji, parametr ten powinien przyjąć postać "@Self", oznaczającą zawsze w wykonywanej metodzie wskaźnik do egzemplarza obiektu, który ją wywołał. W naszym przypadku będzie to więc potrzebny wskaźnik do egzemplarza obiektu reprezentującego okno główne. Tworzenia okien podrzędnych najlepiej dokonywać za pomocą metody "TApplication.MakeWindow", której główna zaleta polega na tym, że przed utworzeniem nowego elementu aplikacji (poprzez wywołanie funkcji "TWindow.Create") sprawdza ona jego poprawność (za pomocą funkcji "TApplication.ValidWindow"). Przed wywołaniem metody "MakeWindow", mającej tylko jeden parametr, a mianowicie wskaźnik do obiektu reprezentującego otwierane okno, konieczne jest ustawienie pól rekordu "Attr" tego obiektu, określających tyl, czyli rodzaj okna (pole "Style"), oraz jego położenie (pola "X", "Y" i wielkość (pola "W", "H"). Styl okna definiuje się za pomocą stałych rozpoczynających się od przedrostka "ws_" ; na przykład "ws_OverlappedWindow" oznacza okno nakładane, "ws_PopupWindow" - okno rozwijalne, zaś "ws_Caption" - istnienie paska tytułu: {Reakcja na wybór polecenia menu Okno|Nakładane} PROCEDURE TOkno.Nakladane; VAR OknoNakladane : PWindow; BEGIN OknoNakladane := New(POknoNakladane, Init(@Self, 'Okno nakładane')); WITH OknoNakladane := New(POknoNakladane^.Attr DO BEGIN Style := Style OR ws_OverlappedWindow OR ws_Caption; X := 50; Y := 50; W := 400; H := 200; END; Aplikacja.MakeWindow(OknoNakladane); END; {Reakcja na wybór polecenia menu Okno|Rozwijalne); PROCEDURE TOkno.Rozwijalne; VAR OknoRozwijalne : PWindow; BEGIN OknoRozwijalne := New(POknoRozwijalne, Init(@Self, 'okno rozwijalne')); WITH OknoRozwijalne^.Attr DO BEGIN Style := Style OR ws_PopupWindow OR ws_Caption; X := 72; Y := 72; W := 400; H := 200; END; Aplikacja.MakeWindow(OknoRozwijalne); END; Ostatnim koniecznym uzupełnieniem programu jest napisanie metod "Paint" obu nowych okien, które mogą ograniczać się jedynie do wyświetlenia prostego tekstu: {Wyświetlanie zawartości okna nakładanego} PROCEDURE TOknoNakladane.Paint; VAR Napis: PChar; BEGIN Napis := 'Ten tekst został wyświetlony w oknie nakładanym.'; TextOut(PaintDC, 10, 10, Napis, StrLen(Napis)); END; {Wyświetlanie zawartości okna rozwijalnego} PROCEDURE TOknoRozwijalne.Paint; VAR Napis: PChar; BEGIN Napis := 'Temn tekst został yświetlony w oknie rozwijalnym.'; TextOut(PaintDC, 10, 10, Napis, StrLen(Napis)); END; Rys. 5.1. Okno nakładane wyświetlane przez aplikację jest bardzo proste, ale ma wszystkie niezbędne atrybuty. Rys. 5.2. Okna rozwijalne, w przeciwieństwie do okien nakładanych, nie pozwalają na zmianę swej wielkości. 5.1.3. Komunikowanie się okien. Jak łatwo zauważyć, oba rodzaje okien tworzonych przez naszą aplikację pozostają, na ekranie tak długo, aż zostaną zamknięte przez użytkownika. A zatem powtórny wybór polecenia "Nakładane" lub "Rozwijalne" z menu "Okno" spowoduje wyświetlenie w tym samym miejscu kolejnego, takiego samego okna, bez zamknięcia jego poprzedniego egzemplarza. Łatwo się o tym przekonać, przesuwając nowo utworzone okno w dowolnym kierunku. Ponieważ najczęściej sytuacja taka jest niepożądana, należy zastosować mechanizm zaamiętywania informacji o oknach niezależnych, które są, aktualnie otwarte. Informacja taka powinna być przy tym dostępna w obiekcie okna głównego, gdyż właśnie w jego metodach, "TOkno.Nakladane" i "TOkno.Rozwijalne", podejmowana jest decyzja o otwieraniu okien podrzędnych. Nie ulega wątpliwości, że w tym celu trzeba wprowadzić zmienne sterujące pamiętające informację o tym, które z okien tworzonych przez aplikację są w danej chwili otwarte. Oczywiste jest też to, że zmienne te powinny być zerowane w konstruktorze okna głównego, "TOkno.Init", oraz ustawiane w procedurach "TOkno.Nakladane" i "TOkno.Rozwijalne". Zastanowienia wymaga jednak udzielenie odpowiedzi na pytanie o sposób uchwycenia momentu zamknięcia danego okna, gdyż czynność ta, zainicjowana wyborem polecenia "Zamknij" z menu systemowego, jest wykonywana w całości przez Windows, bez uczestnictwa aplikacji. W celu rozwiązania tego problemu najlepiej wykorzystać fakt, że zamknięciu każdego okna towarzyszy zawsze wywołanie destruktora "Done" reprezentującego go obiektu. Wystarczy więc przykryć tę metodę w obiektach "TOknoNakladane" i "Tokno "Rozwjane", uzupełniając ją o polecenia powodujące ustawienie wspomnianych wcześniej zmiennych sterujących. W tym miejscu pojawia się jednak kolejne pytanie, dotyczące sposobu uzyskania dostępu do tych zmiennych - przecież należą one do innego obiektu, obiektu okna głównego! Można by wprawdzie zadeklarować je jako zmienne globalne, ale dużo bardziej eleganckim i uniwersalnym rozwiązaniem jest zastosowanie techniki, która leży u podstaw programowania w Windows - techniki przesyłania komunikatów. Komunikaty mogą bowiem pochodzić nie tylko od systemu, ale mogą być także samodzielnie generowane przez poszczególne okna aplikacji, umożliwiając w ten sposób ich wzajemne porozumiewanie się. Do przesyłania komunikatów służą dwie funkcje "API", "PostMessage" i "SendMessage". Obie one mają takie same parametry i działają niemalże identycznie, z tym że wykonywanie pierwszej z nich kończy się natychmiast po umieszczeniu komunikatu w kolejce, natomiast drugiej dopiero po jego przetworzeniu. Parametry obu funkcji są następujące: - uchwyt okna, do którego komunikat jest adresowany; przekazanie wartości $FFFF spowoduje, że wysyłany komunikat trafi do wszystkich okien aplikacji, z których oczywiście tylko niektóre mogą go przetworzyć; z udogodnienia tego korzysta się bardzo często, gdyż zwalnia ono z konieczności pamiętania wartości uchwytu potrzebnego okna; - liczba całkowita określająca rodzaj komunikatu; przy określaniu wartości tego parametru należy wziąć pod uwagę fakt, że bardzo dużo liczb jest już zarezerwowanych dla komunikatów przetwarzanych wewnętrznie przez system lub przez bibliotekę "ObjectWindows"; aby więc uniknąć konfliktu z wykorzystywanymi wartościami, należy w tym miejscu podać liczbę równą co najmniej stałej "wm_User", która oznacza paczątek obszaru zarezerwowanego dla komunikatów przesyłanych między oknami; - wartość pól "WParam" i "LParam" struktury typu "TMessage", przekazywanej procedurze przetwarzającej komunikat; dzięki obu tym parametrom okno wysyłające komunikat może przekazać jego odbiorcy dodatkowe informacje; na przykład w naszej aplikacji parametr "WParam" określa nadawcę komunikatu (wartość 1 oznacza obiekt okna nakładanego, zaś 2 - obiekt okna rozwijalnego). Zestawmy więc wszystkie konieczne modyfikacje naszej aplikacji. Należy w niej: - w obiekcie okna głównego "TOkno" wprowadzić zmienne sterujące "OtwOknoNakl" i "OtwOknoRozw", wstępnie zerując je w konstruktorze "Init" tego okna; - przykryć destruktory "Done" obiektów "TOknoNakladane" i "TOknoRozwijalne", rozszerzając je o wysłanie komunikatu "wm_User", odpowiednio z parametrem "WParam" równym 1 lub 2; - utworzyć metodę "KomunikatWewn" obiektu okna głównego, odbierającą komunikat "wm_User" i w zależności od jego nadawcy zerującą jedną ze zmiennych sterujących; - zmodyfikować procedury "TOkno.Nakladane" i "TOkno.Rozwijalne" w taki sposób, aby sprawdzały one wartość zmiennych sterujących i otwierały okna podrzędne tylko wtedy, gdy w danym momencie nie są one otwarte; w przeciwnym wypadku mają wyświetlać odpowiedni komunikat; otwarciu okna powinno towarzyszyć ustawienie odpowiadającej mu zmiennej sterującej. TOkno = OBJECT(TWindow) ... OtwOknoNakl, OtwOknoRozw: Boolean; ... PROCEDURE KomunikatWewn(VAR Msg: TMessage); VIRTUAL wm_User; ... END; TOknoNakladane = OBJECT(TWindow) DESTRUCTOR Done; VIRTUAL; ... END; TOknoRozwijalne = OBJECT(TWindow) DESTRUCTOR Done; VIRTUAL; ... END; CONSTRUCTOR TOkno.Init; BEGIN ... OtwOknoNakl := False; OtwOknoRozw := False; ... END; {Reakcja na przesłanie komunikatu wewnętrznego aplikacji} PROCEDURE TOkno.KomunikatWewn; BEGIN IF Msg.WParam = 1 THEN OtwOknoNakl := False ELSE IF Msg.WParam = 2 THEN OtwOknoRozw := False; END; PROCEDURE TOkno.Nakladane; VAR ... BEGIN IF OtwOknoNakl = False THEN BEGIN OtwOknoNakl := True; ... END ELSE MessageBox(HWindow, 'Okno nakładane jest już otwarte.', 'Informacja', mb_IconInformation OR mb_OK); END; PROCEDURE TOkno.Rozwijalne; VAR ... BEGIN IF OtwOknoRozw = False THEN BEGIN OtwOknoRozw := True; ... END ELSE Message(HWindow, 'Okno rozwijalne jest już otwarte.' 'Informacja', mb_IconInformation OR mb_OK); END; {Destruktor okna nakładanego} DESTRUCTOR TOknoNakladane.Done; BEGIN TWindow.Done; PostMessage($FFFF, wm_User, 1, 0); END; {Destruktor okna rozwijalnego} DESTRUCTOR TOknoRozwijalne.Done; BEGIN TWindow.Done; PostMessage($FFFF, wm_User, 2, 0); END; Rys. 5.3. Kolejna wersja przygotowywanej aplikacji pozwala na otwarcie tylko pojedynczych egzemplarzy obu rodzajów okien podrzędnych. 5.2. Okna dokumentów. 5.2.1. Co to jest specyfikacja "MDI"? Oprócz okien niezależnych w aplikacjach Windows występują także okna zależne (ang. "child windows"), czyli takie, które są niejako na stałe osadzone w swoich oknach nadrzędnych. Położenie okien zależnych wyrażane jest we współrzędnych ich okien nadrzędnych, w konsekwencji czego dowolne przesunięcie okna nadrzędnego pociąga za sobą identyczną zmianę położenia wszystkich jego okien podrzędnych. Okno zależne nigdy nie może się znaleźć poza granicami swojego okna nadrzędnego; nawet jeżeli użytkownik zmnejszył je do ikony, ikona ta zostaje umieszczona w obszarze roboczym okna nadrzędnego. Co się tyczy wyglądu okien zależnych, to przypominają one niemalże do złudzenia okna nakładane. Oznacza to między innymi, że okna zależne mają z reguły pasek tytułowy, przycisk menu systemowego, minimalizacji i maksymalizacji oraz że pozwalają na zmianę swej wielkości (oczywiście w granicach okna nadrzędnego). Jednak w przeciwieństwie do okien nakładanych nie mogą mieć paska menu. Okna zależne występują najczęściej pod postacią "okien dokumentów" (ang. "document windows"), w których jest umieszczana zawartość redagowanych obiektów, zazwyczaj plików, takich jak teksty, rysunki, arkusze kalkulacyjne czy moduły przygotowywanego programu. Istnieje bardzo wiele aplikacji Windows umożliwiających pracę z oknami dokumentów - należy do nich między innymi "TPW", "Word, Excel", a nawet "Menedżer programów" i "Menedżer plików". Specyfikację określającą sposób zarządzania oknami dokumentó określa się skrótem "MDI" (ang. "Multiple Document Interface"). Zestawmy jej najważniejsze zasady: - okno dokumentu "MDI" może zostać zmniejszone do ikony (w Windows 95 do paska z ikoną i tytułem okna), przy czym ikona taka pojawia się w dolnej części obszaru roboczego okna aplikacji; kolejne ikony układane są od strony lewej do prawej; - okno dokumentu "MDI" może zostać także zmaksymalizowane; wówczas jego pasek tytułu znika, a nazwa dokumentu, będąca zazwyczaj nazwą pliku, pojawia się za nazwą aplikacji w pasku tytułu jej okna głównego; jednocześnie pierwszym elementem paska menu staje się przycisk menu systemowego dokumentu, zaś ostatnim, dosuniętym do prawego brzegu okna - przycisk przywrócenia poprzednich rozmiarów okna dokumentu (w Windows 95 - przycisk zamknięcia okna); - menu systemowe okna dokumentu jest bardzo podobne do menu systemowego okna głównego aplikacji, z tym, że zamiast polecenia "Przełącz na" występuje polecenie "Następne okno"; również i działanie stosowanych w obu przypadkach skrótów klawiszowych niewiele się od siebie różni: "ALT+F4" zamyka okno aplikacji, zaś "CTRL+F4" - okno dokumentu; kombinacji "CTRL+ESC", umożliwiającej przełączanie się między aplikacjami, odpowiada kombinacja "CTRL+F6", pozwalająca poruszać się między oknami dokumentów; natomast otwarcia obu menu systemowych dokonuje się odpowiednio za pomocą "ALT+SPACE" i "ALT+-"; - jeśli aplikacja obsługuje kilka różnych typów okien dokumentów (na przykład różne rodzaje zasobów w edytorze zasobów "Workshop", tekst i arkusz kalkulacyjny w "Microsoft Works" czy arkusz i wykres w "Microsoft Excel"), menu powinno umożliwiać wykonywanie działań związanych z danym typem okna dokumentu; ponadto, jeśli nie jest otwarte żadne okno dokumentu, menu powinno być ograniczone tylko do tych poleceń, które umożliwiają otwarcie nowego okna; - menu aplikacji powinno zawierać pozycję o nazwie "Okno" ("Window"), umieszczoną bezpośrednio na lewo od polecenia "Pomoc" ("Help") lub na końcu paska menu, jeśli polecenie "Pomoc" nie istnieje; zadaniem menu "Okno" jest przede wszystkim umożliwienie odpowiedniego rozmieszczenia okien dokumentów na powierzchni obszaru roboczego okna aplikacji ["Kaskada" ("Cascade"), "Sąsiadująco" ("Tile")], równomierne ułożenie ikon ["Uporządkuj ikony" ("Arrange Icons")] oraz ewentualnie zamknięcie wszystkich otwarych okien dokumentów ["Zamknij wszystkie" ("Close all)"]; ponadto w menu "Okno" powinna się znajdować lista wszystkich otwartych okien dokumentów, umożliwiająca szybkie przejście do dowolnego z nich. 5.2.2. Tworzenie okna głównego "MDI". Realizacją niemalże wszystkich wymienionych wyżej zaleceń specyfikacji "MDI" (za wyjątkiem zmieniania paska menu) zajmuje się system Windows (począwszy od wersji 3.0) oraz funkcje i procedury biblioteki "ObjectWindows". Napisanie aplikacji "MDI" jest więc niemal tak proste, jak napisanie odpowiadającej jej "zwykłej" aplikacji. Różnice dzielące oba te rodzaje programów są następujące: - obiekt okna głównego aplikacji powinien być typu "TMDIWindow", a nie, jak dotychczas, "TWindow"; ponieważ typ ten może reprezentować tylko okno główne, jego konstruktor "Init" nie ma parametru określającego uchwyt okna nadrzędnego; z drugiej strony oczekuje on przekazania mu uchwytu menu, co wynika z przyjętego założenia, że każde okno główne aplikacji "MDI" musi mieć pasek menu; w związku z tym konieczne staje się przeniesienie wywołania funkcji "LoadMenu" z konstruktora okna do procedury go wywoującej, czyli "TAplikacja.InitMainWindow"; - w definicji zasobu menu aplikacji należy umieścić listę menu, na końcu której system ma umieszczać nazwy wszystkich otwartych okien dokumentów; zgodnie ze specyfikacją "MDI" pozycja taka powinna nosić nazwę "Okno" ("Window"); oprócz tego trzeba poinformować Windows o tym, która jest to lista menu, wpisując jej numer w konstruktorze obiektu okna głównego do jego pola "ChildMenuPos"; numeracja pozycji paska menu rozpoczyna się od zera i biegnie od strony lewej do prawej. Przykładowa aplikacja "MDI", prosta, ale wyposażona w tablicę akceleratorów i własną ikonę, może na przykład wyglądać następująco: {*******************************************************} { } {Okna dokumentów (MDI) } { } {*******************************************************} PROGRAM MDI; {$R MDI.RES} USES WObjects, WinTypes, WinProcs, Strings, MDI_M; TYPE {Obiekt reprezentujący aplikację} TAplikacja = OBJECT(TApplication) PROCEDURE InitInstance; VIRTUAL; PROCEDURE InitMainWindow; VIRTUAL; END; {Obiekt reprezentujący okno główne aplikacji} POknoMDI = ^TOknoMDI; TOknoMDI = OBJECT(TMDIWindow) CONSTRUCTOR Init(ATitle: PChar; AMenu: HMenu); PROCEDURE GetWindowClass(VAR AWndClass: TWndClass); VIRTUAL; END; VAR Aplikacja: TAplikacja; {Zainicjowanie egzemplarza aplikacji} PROCEDURE TAplikacja.InitInstance; BEGIN Tapplication := LoadAccelerators(HInstance, 'MDI'); END; {Utworzenie okna głównego aplikacji} PROCEDURE TAplikacja.InitMainWindow; BEGIN MainWindow := New(POknoMDI, Init('Okna dokumentów', LoadMenu(HInstance, 'Mdi'))); END; {Konstruktor obiektu reprezentującego okno główwne} CONSTRUCTOR TOknoMDI.Init; BEGIN TMDIWindow.Init(ATitle, AMenu); WITH Attr DO BEGIN X := 0; Y := 0; W := 480; H := 320; END; ChildMenuPos := 2; END; {Zdefiniowanie klasy okna} PROCEDURE TOknoMDI.GetWindowClass; BEGIN TMDIWindow.GetWindowClass(AWndClass); AWndClass.hIcon := LoadIcon(HInstance, 'MDI'); END; {Blok główny programu} BEGIN Aplikacja.Init('Okna dokumentów'); Aplikacja.Run; Aplikacja.Done; END. Oprócz tekstu źródłowego programu trzeba też wprowadzić opis jego zasobów. W podanym niżej przykładzie, poza identyfikatorami zdefiniowanymi w nowo utworzonym pliku "MDI_M.PAS", zastosowano także kilka stałych predefiniowanych w bibliotece "ObjectWindows". Należą do nich między innymi: "cm_FileSave", "cm_MDIFileOpen", "cm_CreateChild", "cm_Arrangelcons", "cm_EditUndo", "cm_Exit" oraz kilka innych. Niektóre z nich przypisane są odpowiadającym im procedurom lub funkcjom. I tak, wybór na przykład polecenia menu o identyfikatorze "cm_Exit" powoduje wywołanie funkcji "CloseWindow", czyli tej samej, która jest wykonywana w wyniku wyboru polecenia "Zamknij" z menu systemowego, co pozwala uniknąć konieczności samodzielnego definiowania procedury przypisanej poleceniu menu "Plik" w nim "Koniec". Podobnie, identyfikator "cm_CreateChild" przypisany jest funkcji odpowiedzialnej za utworzenie nowego okna dokumentu, "cm_CascadeChildren" - ułożenia wszystkich otwartych okien w kskadę itd. Podana tu zasada nie dotyczy niestety wszystkich zastosowanych identyfikatorów. Niektóre z nich, na przykład grupa stałych rozpoczynających się od przedrostka "cm_Edit", nie są skojarzone z żadnymi istniejącymi funkcjami czy procedurami. Niemniej jednak zastosowanie również i takich identyfikatorów jest warte polecenia, gdyż czyni ono program czytelniejszym oraz zwalnia programistę z konieczności samodzielnego zdefiniowania stałych: /*MDI.RC - Zasoby programu MDI*/ #include "c:\tpw\owl\wobjects.h" #include "mdi_m.pas" MDI MENU BEGIN POPUP "&Plik" BEGIN MENUITEM "&Nowy\tCtrl+N", cm_CreateChild MENUITEM "&Otwórz\tCtrl+O", cm_MDIFileOpen MENUITEM "&Zachowaj\tCtrl+S", cm_FileSave MANUITEM "Zachowaj &jako", cm_FileSaveAs MANUITEM SEPARATOR MANUITEM "&Drukuj\tCtrl+P", cm_Drukuj MANUITEM "&Ustawienia drukarki", cm_UstawDruk MANUITEM SEPARATOR MANUITEM "&Koniec", cm_Exit END POPUP "&Edycja" BEGIN MANUITEM "&Cofnij\tAlt+BS", cm_EditUndo MANUITEM SEPARATOR MANUITEM "&Wyt&nij\tShift+Del", cm_EditCut MANUITEM "&Kopiuj\tCtrl+Ins", cm_EditCopy MANUITEM "&Wklej\tShift+Ins", cm_EditPaste MANUITEM "&Usuń\tDel", cm_EditClear END POPUP "&Okno" BEGIN MANUITEM "&Kaskada\tShift+F5", cm_CascadeChildren MANUITEM "&Sąsiadująco\tShift+F4", cm_TileChildren MANUITEM "&Uporządkuj ikony", cm_ArragleIcons MANUITEM "&Zamknij wszystkie", cm_CloseChildren END POPUP "Po&moc" BEGIN MANUITEM "&Informacja\tAlt+I", cm_Informacja END END MDI Accelerators BEGIN "^N", cm_CreateChild, ASCII "^O", cm_MDIFileOpen, ASCII "^S", cmFileSave, ASCII "^P", cm_Drukuj, ASCII VK_BACK, cm_EditUndo, ASCII, ALT VK_DELETE, cmEditCut, VIRTKEY, SHIFT VK_INSERT, cm_EditCopy, VIRTKEY, CONTROL VK_INSERT, cm_EditPaste, VIRTKEY, SHIFT VK_DELETE, cm_EditClear, VIRTKEY VK_F4, cm_TileChildren, VIRTKEY, SHIFT VK_F5, cm_CascadeChildren, VIRTKEY, SHIFT "i", cm_Informacja, ASCII, ALT END MDI ICON BEGIN ... END Dzięki zastosowaniu predefiniowanych identyfikatorów objętość pliku "MDI_M.PAS" mogła ulec znacznemu zmniejszeniu: {MDI_M.PAS: Identyfikatory poleceń menu} UNIT MDI_M; INTERFACE CONST cm_Drukuj = 11; cm_UstawDruk = 12; cm_Informacja = 21; IMPLEMENTATION END. Rys. 5.4. Windows przydziela wszystkim oknom dokumentów tę samą nazwę i automatycznie nimi zarządza. Rys. 5.5. Zdefiniowanie ikony dla przygotowywanej aplikacji ułatwia rozpoznanie jej pliku - na rysunku ekran systemu Windows 95. 5.2.3. Tworzenie okien dokumentów. Wprawdzie przygotowana przez nas aplikacja "MDI" wykonuje na oknach dokumentów wszelkie niezbędne operacje sterujące, niemniej jednak znacznym jej mankamentem jest to, że wszystkie okna mają taką samą, mało atrakcyjną nazwę. Aby ten stan rzeczy zmienić, trzeba przykryć metodę "InitChild" obiektu "TOknoMDI" okna głównego aplikacji, gdyż ona właśnie jest odpowiedzialna za tworzenie kolejnych okien dokumentów. Obiekty reprezentujące te okna mogą być typu "TWindow", a wskaźniki do nich powinny być zwracne w postaci wartości funkcji "InitChild". Rys. 5.6. Często okna dokumentów są układane kaskadowo - tak zadanie to wykonuje system Windows 3.1, ... Rys. 5.7. ... a tak Windows 95. Jeżeli okna dokumentów mają być numerowane, w obiekcie "TOknoMDI" trzeba utworzyć zmienną pamiętającą ostatnio wykorzystany numer. Zmienna ta, nosząca w naszej aplikacji nazwę "NrOknaDok", powinna być zerowana w konstruktorze okna głównego, zwiększana zaś (przy zastosowaniu najprostszego rozwiązania) w metodzie "TOknoMDI.InitChild", każdorazowo podczas tworzenia nowego okna. Rys. 5.8. Tym razem okna ułożono sąsiadująco, a w pierwszym z nich otwarto menu systemowe - zadanie to wykonano zarówno w Windows 3.1, ... Rys. 5.9. ... jak i w Windows 95. Wiele aplikacji Windows, wśród nich "Word" oraz "Excel", automatycznie otwiera pierwsze okno dokumentu natychmiast po ich uruchomieniu. Aby funkcję tę wbudować do przygotowywanej aplikacji, należy skorzystać z jeszcze jednej, nie stosowanej dotychczas, metody obiektów reprezentujących okna, "SetupWindow". Jej zadaniem jest, mówiąc najogólniej, wypełnienie obszaru roboczego tworzonego okna. Na przykład metoda "SetupWindow" obiektu "TMDIWindow" tworzy standardowo tak zwane okno obsługi aplikacji "MDI" którym jest obiekt typu "TMDlClient", reprezentujący obszar roboczy okna głównego i biorący udział w zarządzaniu oknami dokumentów. Istnienie okna obsługi jest jednak w prostych aplikacjach "MDI" niewidoczne dla programisty. Obecnie musimy więc rozszerzyć treść procedury "SetupWindow" o wywołanie metody "CreateChild", odpowiedzialnej za utworzenie okna dokumentu. Warto zauważyć, że procedura "CreateChild" wywołuje z kolei omówioną wcześniej metodę "InitChild". W przygotowywanej aplikacji "Okna dokumentów" należy więc dokonać następujących zmian: TOknoMDI = OBJECT(TMDIWindow) NrOknaDok: Integer; ... PROCEDURE SetupWindow; VIRTUAL; FUNCTION InitChild: PWindowsObject; VIRTUAL; ... END; CONSTRUCTOR TOknoMDI.Init; BEGIN ... NrOknaDok := 0; END; {Utworzenie pierwszego okna dokumentu} PROCEDURE TOknoMDI.SetupWindow; BEGIN TMDIWindow.SetupWindow; CreateChild; END; {Utworzenie okna dokumentu} FUNCTION TOknoMDI.InitChild; VAR Numer: STRING; Tytul: ARRAY[0..255] OF Char; BEGIN NROknaDok := NrOknaDok + 1; Str(NrOknaDok, Numer); StrPCopy(Tytul, 'Okno dokumentu' + Number); InitChild := New(PWindow, Init(@Self, Tytul)); END; Rys. 5.10. Każde z otwartych okien dokumentów może zostać zmaksymalizowane, to znaczy powiększone do powierzchni obszaru roboczego okna głównego. 5.2.4.Obsługa komunikatów w aplikacjach "MDI". Obsługa komunikatów w aplikacjach "MDI" przebiega niemal tak samo jak w zwykłych aplikacjach. Jedynymi osobliwościami aplikacji "MDI" jest to, że: - komunikaty informujące o wyborze polecenia menu są kierowane w pierwszej kolejności do aktywnego okna dokumentu, a dopiero potem, jeśli nie zostaną przez nie przetworzone, do okna głównego aplikacji; wynika stąd, że procedury reagujące na wybór poszczególnych poleceń menu można tworzyć zarówno w obiekcie reprezentującym okno główne (na przykład polecenie "Pomoc" w nim "Informacja"), jak i w obiektach okien dokumentów (na przykład polecenie "Plik" w nim "Zachowaj"); - wszystkie okna dokumentów są reprezentowane przez obiekty tego samego typu, co oznacza, że ich obsługą, w tym także wykonywaniem poleceń menu, zajmują się te same procedury; aby więc każda procedura obiektu okna dokumentu wiedziała, w odniesieniu do którego okna ma wykonać określone działanie, obiekt ten powinien zawierać pola umożliwiające zapamiętanie wszystkich istotnych informacji dotyczących redagowanego dokumentu, takich jak otwartego pliku, miejsce wyświetlanego tekstu, położenie kursora it. (wykorzystuje się tu fakt, że dla każdego okna dokumentu tworzony jest oddzielny egzemplarz obiektu). W przygotowywanej przez nas aplikacji "MDI" obiekt "TOknoDok", potomny w stosunku do "TWindow" i reprezentujący okna dokumentów, nie zawiera dodatkowych pól, gdyż na tym etapie pracy nie są one jeszcze potrzebne. Niemniej jednak już teraz w celu identyfikacji okna wykorzystamy pole "Attr.Title", do którego na etapie tworzenia okna wpisywany jest automatycznie jego tytuł. Nasza aplikacja reaguje na wybór dwóch poleceń menu: "Pomoc" w nim "Informacja" oraz "Plik" w nim "Zachowaj". Obsługą pierwszego z nich zajmuje się procedura "Informacja" obiektu okna głównego, drugiego zaś - procedura "Zachowaj" obiektu okna dokumentu. Oprócz tego zamknięcie każdego okna dokumentu poprzedzane jest wyświetleniem okna weryfikacyjnego. Wykorzystujemy tu omówione wcześniej udogodnienie Windows, polegające na wywołaniu funkcji "CanClose" wszystkich obiektów reprezentujących okna, które ają zostać zamknięte. Zwróćmy uwagę na fakt, że zakończenie działania aplikacji wiąże się z koniecznością zamknięcia wszystkich otwartych okien dokumentów, w wyniku czego wspomniane okno weryfikacyjne zostaje wyświetlone kolejno przez każde z tych okien; jak łatwo zauważyć, odpowiada to zachowaniu się profesjonalnych aplikacji Windows. Poniżej odnotowano wszystkie zmiany, których trzeba dokonać w aplikacji "MDI": TOknoMDI = OBJECT(TMDIWindow) ... PROCEDURE Informacja(VAR Msg: TMessage); VIRTUAL cm_First+cm_Informacja; END; {Obiekt reprezentujący okno dokumentu} POknoDok = ^TOknoDok; TOknoDok = OBJECT(TWindow) FUNCTION CanClose: Boolean; VIRTUAL; PROCEDURE Zachowaj(VAR Msg: Tmessage); VIRTUAL cm_First+cm_FileSave; END; FUNCTION TOknoMDI.InitChild; VAR ... BEGIN ...InitChild := New(POknoDok, Init(@Self, Tytul)); END; {Reakcja na wybór polecenia menu Pomoc|Informacja} PROCEDURE TOknoMDI.Informacja; BEGIN MessageBox(HWindow, 'Okna dokumentów (MDI)', 'Informacja', mb_IconInformation OR mb_OK); END; {Reakcja na próbę zakończenia programu lub zamknięcia okna dokumentu} FUNCTION TOknoDok.CanClose; BEGIN IF MessageBox(HWindow, 'Zamknąć okno?', Attr.Title, mb_IconQuestion OR mb_YesNo) = IdYes THEN CanClose := True ELSE CanClose := False; END; {Reakcja na wybór polecenia menu Plik|Zachowaj} PROCEDURE TOknoDok.Zachowaj; BEGIN MessageBox(HWindow, 'Dokument został zachowany.', Attr.Title, mb_IconInformation OR mb_OK); END; Rys. 5.11. Zamknięcie każdego okna dokumentu poprzedzone jest wyświetleniem okna weryfikacyjnego. 5.3. Paski przewijania w oknach. 5.3.1. Tworzenie pasków przewijania. Dość rzadko się zdarza, aby cały dokument mieścił się w swoim oknie. Jeżeli jednak chcemy zapewnić użytkownikowi możliwość zapoznania się z jego pełną zawartością, musimy zastosować paski przewijania, zwane też niekiedy suwakami. My jednak tego ostatniego pojęcia używać będziemy w dalszej części książki w odniesieniu do prostokątnego pola znajdującego się na powierzchni pasków i określającego położenie wyświetlanego fragmentu dokumentu. Paski przewijania, zarówno poziomy, jak i pionowy, tworzy się na etapie powstawania okna, w jego konstruktorze "Init". W tym celu należy najpierw zmodyfikować wartość pola "Attr", określającego styl okna, przez rozszerzenie go o stałe "ws_VScroll" (pasek pionowy) oraz/lub "ws_HScroll" (pasek poziomy). Następnie trzeba utworzyć obiekt typu "TScroller", reprezentujący oba paski, i wpisać wskaźnik do niego do pola "Scroller" obiektu okna. Konstruktor "Init" obiektu "TScroller" ma pięć parametrów: - wskaźnik do obiektu okna, w którym mają zostać umieszczone paski przewijania; - liczbę pikseli, o jaką ma zostać przesunięty obraz w wyniku naciśnięcia pola ze strzałką w pasku poziomym oraz pionowym - wartości te zostają wpisane odpowiednio do pól "XUnit" i "YUnit" obiektu "TScroller"; - maksymalną liczbę możliwych jednostkowych przesunięć obrazu w kierunku poziomym i pionowym - te wartości zostają wpisane odpowiednio do pól "XRange" i "YRange" obiektu "TScroller". Z podanych informacji wynika, że w wyniku zastosowania pasków przewijania rozmiary wyświetlanego obrazu magą zostać powiększone w stosunku do rozmiarów obszaru roboczego o "XUnit * XRange" w kierunku poziomym oraz "YUnit * YRange" w kierunku pionowym. Informacje dotyczące pasków przewijania wykorzystamy w przygotowywanej przez nas aplikacji "MDI", rozszerzając ją o realizację polecenia menu "Plik" w nim "Otwórz", które polegać będzie na wyświetleniu w trzech kolorach podstawowych grupy zmniejszających się prostokątów o wspólnym środku. Wprawdzie działanie to nie jest związane z operacjami na plikach, co sugerowałaby nazwa menu, niemniej jednak powoduje zapełnienie obszaru roboczego okna dokumentu, co w profesjonalnych aplikacjach ma właśnie miejse w wyniku wywołania polecenia "Otwórz" z menu "Plik". Przy okazji warto zauważyć pewną bardzo ważną dla programisty zaletę systemu Windows: wyświetlana grafika może być kierowana poza fizyczne granice obszaru roboczego. Jak więc widać kolejny już raz, obiekty, na które powołują się aplikacje Windows, mają rzeczywiście charakter logiczny, nie związany ze stanem konkretnego urządzenia fizycznego. Stanowi to z pewnością ogromny krok naprzód w stosunku do tworzenia programów działających w środowisku DOS-a. Ponieważ nie cały utworzony rysunek mieści się na powierzchni obszaru roboczego, zachodzi potrzeba utworzenia pasków przewijania. Ich parametry określane podczas wywołania konstruktora "TScroller.Init" zostały na razie obliczone tak, aby umożliwiały wyświetlenie całego rysunku przy nie zmienionej wielkości obszaru roboczego okna dokumentu (szerokość obszaru roboczego + 5 * 40 w przybliżeniu 560, wysokość obszaru roboczego + 6 * 40 w przybliżeniu 370). Lepsze rozwiązanie zostanie zaprezentowane w koljnym podrozdziale książki . ToknoDok = OBJECT(TWindow) Wysw: Boolean; CONSTRUCTOR Init(AParent: PWindowsObject; ATitle: PChar); ... PROCEDURE Paint(PaintDC: HDC; VAR PaintInfo: TPaintStruct); VIRTUAL; PROCEDURE Otworz(VAR Msg: TMessage); VIRTUAL cm_First+cmMDIFileOpen; ... END; {Konstruktor obiektu reprezentującego okno dokumentu} CONSTRUCTOR TOknoDok.Init; BEGIN TWindow.Init(AParent, ATitle); Attr.Style := Atte.Style OR ws_VScroll OR ws_HScroll; Scroller := New(PScroller, Init(@Self, 40, 40, 5, 6)); Wysw := False END; {Wyświetlanie zawartości okna dokumentu} PROCEDURE TOknoDok.Paint; VAR Pioro: ARRAY[1..3] OF HPen I, J, K: Integer; BEGIN IF Wysw = True THEN BEGIN Pioro[1] := CreatePen(ps_Solid, 1, RGB(255, 0, 0)); Pioro[2] := CreatePen(ps_Solid, 1, RGB(0, 255, 0)); Pioro[3] := CreatePen(ps_Solid, 1, RGB(0, 0, 255); StarePioro := SelectObject(PaintDC, Pioro[1]); FOR I := 0 TO 5 DO FOR J := 1 TO 3 DO BEGIN SelectObject)PaintDC, Pioro[J]); K := 3*I+J; Rectangle(PaintDC, K*10, K*10, 560-K*10, 370-K*10); END; SelectObject(PaintDC, StarePioro); FOR J := 1 TO 3 DO DeleteObject(Pioro[J]); END; END; {Reakcja na wybór polecenia menu Plik|Otwórz} PROCEDURE TOknoDok.Otworz; BEGIN Wysw := True InvalidateRect(HWindow, NIL, True); END; Rys. 5.12. Ten rysunek nie mieści się wprawdzie w obszarze roboczym, ale za pomocą pasków przewijania można obejrzeć dowolny jego fragment. 5.3.2. Paski przewijania a wielkość okna. Stałe określenie wartości pól "XUnit", "YUnit", "XRange" i "YRange" ma dość istotną wadę, polegającą na tym, że dostępny zakres współrzędnych obszaru roboczego zmienia się wraz z dokonywaniem przez użytkownika zmian rozmiarów okna dokumentu. Łatwo jest się o tym przekonać, zwiększając maksymalnie wielkość okna dokumentu, w odniesieniu do którego zostało wykonane polecenie "Plik" w nim "Otwórz", a następnie przewijając oba paski do końca. Spowoduje to nadmierne przesunięcie obrazu w górę i w lewo i osłoni niepotrzebnie białe pole w prawym dolnym rogu. Aby tej sytuacji zapobiec, parametry pasków przewijania należy modyfikować każdorazowo po zmianie wielkości obszaru roboczego okna, w którym się one znajdują. O zajściu takiego zdarzenia Windows informuje zainteresowane okno przez przesłanie mu komunikatu "wm_Size", który z kolei powoduje wywołanie procedury "WMSize" z biblioteki "ObjectWindows". Wystarczy więc procedurę tę przykryć, uzupełniając ją o wywołanie metody "SetRange" obiektu "TScroller", która przydziela wartości swoich parametrów polom XRange" i "YRange". Nie można przy tym pominąć wywołania oryginalnej procedury "TWindow.WMSize", gdyż spowodowałoby to błędną obsługę okien dokumentów. W profesjonalnych aplikacjach zmiana wartości parametrów pasków przewijania następuje także w wyniku zwiększenia lub zmniejszenia łącznej powierzchni zajmowanej przez zawartość bieżącego dokumentu. Wówczas przydatna może się także okazać funkcja "SetUnits", która umożliwia zmianę wielkości jednostkowego przesunięcia wyświetlanego fragmentu dokumentu, czyli wykonuje analogiczne działania co "SetRange", ale w stosunku do pól "XUnit" i "YUnit". W przedstawionej niżej modyfikacji naszego programu zostało dla uproszczenia poczynione założenie, że wielkość wyświetlanej grafiki nie zmienia się i jest określona stałymi "SzerObr" oraz "WysObr"; w rzeczywistych zastosowaniach najczęściej sytuacja wygląda odmiennie. Warto też zauważyć, że kiedy okno dokumentu zostaje zwiększone w takim stopniu, iż jest w stanie pomieścić cały rysunek, funkcja "SetRange" jest wywoływana z parametrami równymi zeru, co powoduje zanik obu pasków przewijania. Jeżeli naomiast tylko jeden z wymiarów okna jest równy lub większy od odpowiadającego mu wymiaru rysunku, zostaje usunięty jeden pasek. Żadna z wymienionych sytuacji nie wywiera jednak wpływu na styl okna, a także nie usuwa obiektów typu "TScroller", w związku z czym po odpowiednio dużym zmniejszeniu okna jeden lub oba paski natychmiast ponownie pojawiają się na ekranie: TOknoDok = OBJECT(TWindow) ... PROCEDURE WMSize(VAR Msg: TMessage); VIRTUAL wm_First+wm_Size; ... END; CONSTRUCTOR TOknoDok.Init; BEGIN ... Scroller := New(PScroller, Init(@Self, 40, 40, 0, 0)); ... END; {Reakcja na zmianę wielkości okna dokumentu} PROCEDURE TOknoDok.WMSize; CONST SzerObr = 560; WysObr = 370; VAR ObszRob: TRect; DodSzer, DodWys: Integer; ZakresX, ZakresY: Integer; BEGIN TWindow.WMSize(Msg); GetClientRect(HWindow, ObszRob); DodSzer := SzerObr-ObszRob.Right; ZakresX := DodSzer DIV Scroller^.XUnit; IF (DodSzer MOD Scroller^.Unit) > 0 THEN ZakresX := ZakresX+1; DodWys := WysObr-ObszRob.Bottom; ZakresY := DodWys DIV Scroller^.YUnit; IF (DodWysw MOD Scroller^.YUnit) > 0 THEN ZakresY := ZakresY+1; Scroller^.SetRange(ZakresX, ZakresY); END; Rys. 5.13. Ustawienie suwaków pasków przewijania w końcowych położeniach spowodowało wyświetlenie prawego dolnego fragmentu rysunku. Rozdział 6. Obsługa okien dialogowych. Jednym z najważniejszych zagadnień dotyczących programowania w Windows jest tworzenie i obsługa okien dialogowych. Okna te pojawiaja się w większości aplikacji znacznie częściej niż inne okna podrzędne i umożliwiają wymianę danych między programem a użytkownikiem przez stosowanie elementów sterujących, takich jak przyciski, listy czy pola edycji. W tym rozdziale zostaną omówione rodzaje stosowanych okien dialogowych oraz występujących w nich elementów sterujacych, sposób ich opisu w plikach z zasobai oraz metody obsługi w programie. W końcowej części rozdziału zajmiemy się dwoma standardowymi oknami dialogowymi udostępnianymi przez bibliotekę "ObjectWindows" pakietu "TPW": prostym oknem do wprowadzania tekstu oraz oknem umożliwiającym podanie nazwy pliku, który ma zostać otwarty lub zapisany na dysku. 6.1. Podstawowe wiadomości. 6.1.1. Rodzaje okien dialogowych. W programach działających w systemie DOS wprowadzanie danych wejściowych przez użytkownika odbywa się często bezpośrednio na ekranie monitora. Inaczej zachowują się aplikacje Windows, które w tym celu korzystają z okien dialogowych, zawierających pewną liczbę standardowych elementów sterujących. Otwarcie okna dialogowego następuje zazwyczaj w wyniku wyboru polecenia menu; wszystkie polecenia, których wykonanie związane jest z wyświetleniem takiego okna, powinny być wyróżnione wielokropkiem. Istnieją dwa rodzaje okien dialogowych: niezależne i zależne. Najczęściej spotyka się "zależne okna dialogowe" (ang. "model dialog box"), które charakteryzują się tym, że w czasie ich wyświetlania nie jest możliwe uaktywnianie pozostałych okien aplikacji. Okna takie służą zazwyczaj do wprowadzania informacji warunkujących wykonanie przez program określonego działania, takiego jak otwarcie pliku czy określenie parametrów wydruku dokumentu. Zamknięcie zależnego okna dialogowego następuje w wyniku nacinięcia przez użytkownika jednego z przycisków, na przykład lub "Anuluj" ("Cancel"). Pewien szczególny typ zależnych okien dialogowych stanowią "systemowe zależne okna dialogowe" (ang. "system modal dialog box"), które nie pozwalają nawet na uaktywnianie innych aplikacji. Inny rodzaj okien stanowią "niezależne okna dialogowe" (ang. "modeless dialog box"), które mogą działać równolegle z pozostałymi oknami aplikacji. Są one jednak spotykane w aplikacjach Windows dużo rzadziej niż okna zależne. Przykładem okna niezależnego jest okno umożliwiające znalezienie tekstu w programie "Word dla Windows", które może pozostawać otwarte w czasie wpisywania tekstu, a nawet korzystania z innych okien dialogowych. Niezależne okna dialogowe stosuje się wtedy, gdy istnieje duże prawdoodobieństwo, że użytkownik będzie chciał je wykorzystać w danym czasie wielokrotnie. W dalszej części tego rozdziału zajmować się będziemy wyłącznie zależnymi oknami dialogowymi. 6.1.2. Elementy sterujące okien dialogowych. Sens korzystania z okien dialogowych związany jest z istnieniem w ich obszarze roboczym tak zwanych elementów sterujących, traktowanych przez system Windows jako szczególnego rodzaju okna zależne. Najważniejszymi tego typu elementami są "przyciski" (ang. "push button"), których odpowiednikami w oknach aplikacji są polecenia menu. Zazwyczaj jeden z przycisków nosi nazwę "Anuluj" ("Cancel") i umożliwia zamknięcie okna dialogowego bez wywierania jakiegokolwiek wpływu na dalsze działanie programu. Wyborwi tego przycisku odpowiada naciśnięcie klawisza "ESC". Oprócz tego, prawie zawsze istnieje tak zwany "przycisk opcjonalny" (ang. "default push button"), który jest wybierany po naciśnięciu przez użytkownika klawisza "ENTER" w chwili, gdy nie był aktywny żaden inny przycisk (na przykład podczas wpisywania tekstu). Przycisk opcjonalny nosi najczęściej (nie zawsze!) nazwę "OK" i powoduje zaakceptowanie wszystkich wartości wprowadzonych w oknie dialogowym, a następnie zamknięcie go. Z punktu widzenia programowania w Windows przyciskami są także przełączniki i pola wyboru. "Przelączniki" (ang. "radio button"), zwane też polami przełączającymi, umożliwiają wybór jednego spośród kilku oferowanych wariantów. Przełączniki są oznaczone w oknach dialogowych kółkami i nigdy nie występują pojedynczo, lecz zawsze w grupach. Ich działanie charakteryzuje się tym, że wszystkie elementy danej grupy wzajemnie się wykluczają, to znaczy, że w dowolnej chwili dokładnie jeden z nich musi być aktyny. Zaznaczenie jednego z przełączników powoduje więc automatyczne skasowanie wszystkich innych należących do tej samej grupy. Element aktywny jest wyróżniony znajdującą się w kółku kropką. Nieco inaczej niż przełączniki zachowują się "pola wyboru" (ang. "check box"), zwane także polami opcji lub opcjami i oznaczane kwadratami. Mogą one występować samodzielnie, a ponadto każde z nich może być aktywne lub nie, niezależnie od stanu pozostałych elementów znajdujących się w tej samej grupie. Zaznaczenie lub skasowanie jednego z pól wyboru nie wpływa więc na zmianę stanu pozostałych pól należących do tej samej grupy. Wszystkie aktywne pola wyboru są oznaczone umieszczonym w kwadracie znakie "x". Wizualne powiązanie pewnej liczby elementów sterujących, najczęściej przełączników lub pól wyboru; jest możliwe dzięki "polom grup elementów" (ang. "group box"), które przyjmują postać prostokąta ze znajdującym się na jego górnym boku tekstem. Mimo iż pól grup elementów nie można naciskać za pomocą myszy, to jednak przez Windows są one traktowane jako pewien szczególny rodzaj przełącznika. "Pola edycji" (ang. "edit box"), zwane inaczej polami tekstowymi lub redakcyjnymi, służą do wprowadzania i redagowania tekstu. Istnieje wiele różnych odmian pól edycji: jedno- i wielowierszowe, nie dokonujące żadnej konwersji wpisywanych znaków, zamieniające litery duże na małe lub wyświetlające tylko jeden rodzaj znaku w celu utajnienia wprowadzanego hasła. Wielowierszowym polom edycji towarzyszą zazwyczaj "pionowe paski przewijania" (ang. "vertical scroll bar"); w bardziej zaawansowanych zastosowaiach mogą się także pojawić "poziome paski przewijania" (ang. "horizontal scroll bar"). "Pola statyczne" (ang. "stalic") umożliwiają wyświetlanie napisów stanowiących nazwy innych elementów sterujących, ikon, a także pustych lub wypełnionych prostokątów. Najczęściej spotyka się je bezpośrednio przed polami edycji, gdzie określają, jaki rodzaj tekstu ma zostać wprowadzony. "Listy" (ang. "list box"), zwane również polami listowymi, pozwalają użytkownikowi na wybór jednego spośród pewnej liczby elementów. Elementy te przyjmują najczęściej postać tekstu, ale mogą to być także rysunki. Jeśli nie wszystkie pozycje listy mogą być jednocześnie wyświetlone na ekranie, pojawiają się pionowe paski przewijania. "Pola kombinowane" (ang. "combo box") stanowią połączenie listy z polem edycji lub polem statycznym. Właściwie to powinny się one nazywać "polami łączonymi", ale w języku polskim przyjęło się słowo odpowiadające terminowi angielskiemu pod względem brzmieniowym, a nie znaczeniowym (zauważmy, że takie samo zjawisko nastąpiło w odniesieniu do angielskiego słowa "icon", którego polskim odpowiednikiem jest piktogram, gdy tymczasem powszechnie stosuje się słowo "ikona"). Rys. 6.1. Jednolity wygląd elementów sterujących okien dialogowych znacznie ułatwia obsługę aplikacji Windows. Po tej krótkiej dygresji dotyczącej zagadnień językowych wróćmy jednak do omawiania pól kombinowanych. Istnieją ich trzy rodzaje: - "listy rozwijalne" (ang. "drop down list box"), stanowiące połączenie listy z polem statycznym; normalnie w polu statycznym widoczny jest tylko wybrany element listy, a ona sama pozostaje ukryta (istnieje tu analogia do rozwijalnej listy menu); rozwinięcie listy następuje dopiero po naciśnięciu przycisku ze strzałką, znajdującego się z prawej strony pola statycznego; ze względu na swoje podobieństwo do zwykłych list, listy rozwijalne traktuje się często jako odmianę list, a nie pól kombinowanych; - "pola kombinowane proste" (ang. "simple combo box"), będące połączeniem zwykłej listy z polem edycji; potrzebną wartość można albo wybrać z listy, albo wprowadzić samodzielnie za pomocą pola edycji, przy czym wprowadzony tekst nie musi być zgodny z żadnym z wyświetlanych elementów listy; niemniej jednak, Windows uaktywnia w każdej chwili ten element, którego początek nazwy jest zgodny z wprowadzonym w polu edycji; - "pola kombinowane rozwijalne" (ang. "drop down combo box"), stanowiące również połączenie listy z polem edycji, ale różniące się od pól kombinowanych prostych tym, że, podobnie jak w przypadku list rozwijalnych, normalnie wyświetlany jest tylko wprowadzony tekst, zaś rozwinięcie listy następuje po naciśnięciu przycisku ze strzałką, znajdującego się z prawej strony pola edycji. Oprócz wszystkich wymienionych elementów sterujących w polach edycji mogą także występować samodzielnie "paski przewijania". Sytuacja taka ma jednak miejsce bardzo rzadko - wtedy, gdy nie wszystkie elementy mieszczą się jednocześnie na powierzchni okna. Jeszcze innym rodzajem elementów sterujących są "elementy użytkownika" (ang. "custom control", czyli elementy zdefiniowane samodzielnie przez programistę. Na rysunku 6.1 przedstawiono postać omówionych elementów sterujących. Należy zwrócić uwagę na fakt, że rysunek ten nie stanowi kopii okna dialogowego żadnej rzeczywistej aplikacji, gdyż jest na nim jednocześnie aktywnych kilka elementów (świadczy o tym ich zmieniony kolor i widoczny kursor). 6.1.3. Zależności występujące między elementami sterującymi. Wszystkie elementy sterujące znajdujące się w oknie dialogowym są zawsze uporządkowane w pewnej "kolejności". Kolejność ta decyduje o tym, który element zostanie wyróżniony (ang. "get focus") po kolejnym naciśnięciu klawisza "TAB". Do elementu wyróżnionego kierowane są wszystkie znaki wprowadzane za pomocą klawiatury. Nie każdy jednak element okna dialogowego może stać się aktywny. Jaki bowiem sens miałoby wykonanie takiej operacji na przykład w stosunku do pola statycznego? Dlatego też dla każdego elementu sterującego określa się, czy ma mu zostać przydzielone tak zwane "miejsce tabulacji" (ang. "tab stop"), czyli czy będzie miał możliwość stania się elementem aktywnym w wyniku naciśnięcia klawisza "TAB" lub kliknięcia myszą. Jeśli pewna liczba elementów sterujących, takich jak przełączniki czy pola wyboru, jest ze sobą ściśle powiązana, to łączy się ją w "grupę elementów" (ang. "group"), której nie należy mylić z omawianymi wcześniej polami grup elementów. Grupy elementów pełnią dwie funkcje. Po pierwsze, korzysta się z nich po to, by określić, które przełączniki znajdujące się w oknie dialogowym wzajemnie się wykluczają. Po drugie zaś, umożliwiają one korzystanie z klawiszy ze strzałkami - klawisze "strzałka w lewo" i strzałka w górę" powodują uaktywnienie poprzedniego elementu sterującego należącego do tej samej grupy, natomiast klawisze "strzałka w prawo" i "strzałka w dół" - elementu następnego. 6.2. Opis okna dialogowego w pliku z zasobami. 6.2.1. Tekst opisujący okno dialogowe. Okna dialogowe, wraz ze wszystkimi znajdującymi się w nich elementami sterującymi opisuje się w plikach z zasobami (w przeciwieństwie do innych okien, które można utworzyć bezpośrednio w programie). Wprawdzie w tym celu korzysta się zazwyczaj z edytorów zasobów, niemniej jednak również i one wymagają posiadania przez programistę pewnej wiedzy dotyczącej sposobu definiowania okien i ich elementów. Dlatego zanim przystąpimy do zapoznawania się z odpowiednimi funkcjami programu "Workshop", przyjrzymy sę bliżej postaci źródłowej opisu okna dialogowego. Podejście takie ma jeszcze dwie inny zalety. Po pierwsze, zmiany parametrów elementów okien dialogowych można często wprowadzić łatwiej przez bezpośrednie odwołanie się do postaci źródłowej ich opisu. Po drugie, podane tu informacje będą także pomocne w zrozumieniu istoty zarządzania oknami dialogowymi i ich elementami sterującymi, co na tym etapie nauki programowania w Windows jest rzeczą najważniejszą. Pierwszy wiersz opisu zaczyna się, podobnie jak w tekstach definiujących wszystkie inne zasoby, od nazwy zasobu, czyli w tym przypadku okna dialogowego. Po nazwie następuje słowo "DIALOG" oraz cztery liczby określające współrzędne globalne, a więc odnoszące się do całego ekranu, lewego górnego rogu okna, a także jego szerokość i wysokość. Nowością jest to, że wszystkie wymienione liczby wyraża się w jednostkach innych niż te, do których przywykliśmy (do tej pory współrzędne wyrażaliśmy zawsze w pikselach). Jednostkę szerokości, stosowaną zarówno przy opisie samego okna, jak i jego elementów sterujących, stanowi tym razem 1/4 średniej szerokości litery czcionki systemowej, zaś jednostkę wysokości - 1/8 wysokości tej czcionki. Rozwiązanie takie zostało podyktowane chęcią uzyskania niezależności od sprzętu. Okna dialogowe służą bowiem wpierwszej kolejności do wyświetlania tekstu, a więc ich wielkość powinna być dostosowana do wielkości stosowanej aktualnie czcionki. Na marginesie warto zauważyć, że ponieważ wysokość czcionki systemowej jest najczęściej dwukrotnie większa od szerokości, obie jednostki stosowane w opisie okien dialogowych są w zasadzie takie same. W drugim wierszu opisu okna dialogowego wpisuje się słowo "STYLE", a za nim - styl okna, wyrażony w postaci sumy logicznej stałych, takich samych lub podobnych do tych, których używa się podczas tworzenia zwykłych okien. Styl standardowego niezależnego okna dialogowego wyrażają stałe "DS_MODALFRAME" \ "WS_POPUP" \ "WS_CAPTION" \ "WS_SYSMENU". Ich znaczenie jest następujące: "DS_MODALFRAME" opisuje ramkę typową dla omawianego rodzaju okna, "WS_CAPTION oznacza istnienie paska tytułu, "WS_POPUP" stwierza, że okno dialogowe jest oknem rozwijalnym (a nie oknem nakładanym lub zależnym), zaś "WS_SYSMENU" - oznacza istnienie menu systemowego. Jeśli styl okna opisany jest między innymi stałą "WS_CAPTION", to kolejny wiersz opisu zaczyna się od słowa "CAPTION" i określa tytuł okna. W dalszej kolejności może się także ewentualnie pojawić wiersz rozpoczynający się od słowa "FONT" i określający, jaka czcionka i jakiej wielkości ma być stosowana do wyświetlenia okna. Warto pamiętać, że w oknach dialogowych systemu Windows 3.1 na ogół korzysta się ze standardowej czcionki systemowej, która nie wymaga wprowadzania polecenia "FONT". Pozostała część opisu zawiera definicje poszczególnych elementów sterujących, umieszczone między słowami "BEGIN" i "END". Kolejność opisywania elementów w pliku z zasobami odpowiada ich kolejności w oknie dialogowym. Najbardziej ogólny opis pojedynczego elementu sterującego wygląda w taki sposób: CONTROL "tekst" , ident, "klasa", styl, x, y, szer, wys na przykład CONTROL "COM &1", id_Com1, "BUTTON", BS_AUTORADIOBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 16, 20, 32, 12 Znaczenie użytych symboli jest następujące: - "CONTROL" - słowo oznaczające początek definicji elementu sterującego; - "tekst" - nazwa elementu sterującego, czyli związany z nim tekst, który ma być wyświetlony w oknie dialogowym, na przykład słowo "OK", będące nazwą przycisku; znak tę pełni podobną funkcję jak w definicjach menu i oznacza, że dostęp do danego elementu sterującego można uzyskać przez naciśnięcie kombinacji klawisza "ALT" ze znakiem występującym za "&"; - "ident" - liczba będąca identyfikatorem elementu; elementom, do których nie zachodzi potrzeba odwoływania się w programie, na przykład elementom statycznym, przydziela się zazwyczaj identyfikatory równe -1, natomiast wszystkim pozostałym - liczby większe od zera, zazwyczaj zdefiniowane w postaci stałych w odrębnym pliku; należy też pamiętać o tym, że wartości 1 i 2 są zarezerwowane w Windows dla przycisków "OK" i "Anuluj"; zastosowanie się do tej reguły spowoduje, że w wyniku naciśnięcia jednego ztych przycisków system sam zamknie okno dialogowego, a ponadto możliwe będzie skorzystanie z tak zwanego transferu parametrów, wykonywanego przez procedury biblioteki "ObjectWindows" (zagadnienie to zostanie wyjaśnione w dalszej części książki); - "klasa" - łańcuch znaków opisujący klasę (typ) elementu sterującego; najważniejsze klasy elementów sterujących to: - "BUTTON"- przyciski, przełączniki, pola wyboru i pola grup elementów; - "EDIT"-pola edycji; - "STATIC" - pola statyczne; - "LISTBOX"- listy; - "COMBOBOX"- listy rozwijalne, pola kombinowane proste i rozwijalne; - "styl" - liczba opisująca styl elementu, czyli jego rodzaj i wygląd; liczbę tę podaje się w postaci sumy logicznej stałych, z których niektóre, rozpoczynające się od liter "WS_", można stosować do opisu wszystkich elementów sterujących (a także normalnych okien), pozostałe zaś - tylko do wybranych klas; o tym, z jaką klasą związana jest dana stała, świadczą jej początkowe litery, na przykład "BS" - "button style", "CBS" - "combo box style" itp.; następujące stałe zasługują na szczególną uwagę: - "BS_PUSHBUTTON", "BS_DEFPUSHBUTTON", "BS_RADIOBUTTON", "BS_CHECKBOX", "BS_GROUPBOX" - element klasy "BUTTON" jest przyciskiem (zwykłym), przyciskiem opcjonalnym, przełącznikiem, polem wyboru lub polem grupy elementów; "BS_AUTORADIOBUTTON", "BS_AUTOCHECKBOX" - element klasy "BUTTON" jest automatycznym przełącznikiem lub automatycznym polem wyboru; elementy te tym różnią się od swych zaprezentowanych wcześniej odpowiedników, że system Windows sam je obsługuje, czyli zawsze zmienia ich stan po ich naciśnięciu przez użytkownika; ta funkcja systemu stanowi istotne ułatwienie dla programisty, z związku z czym wszędzie tam, gdzie obsługa przełączników lub pól wyboru ma charakter standardowy, powinno się je definiować jao elementy automatyczne; - "CBS_SIMPLE", "CBS_DROPDOWN", "CBS_DROPDOWNLIST" - element klasy "COMBOBOX"jest polem kombinowanym prostym, polem kombinowanym rozwijalnym lub listą rozwijalną; - "WS_CHILD" - element sterujący jest oknem zależnym (jest to oczywiście słuszne w odniesieniu do wszystkich elementów sterujących okien dialogowych); - "WS_VISIBLE" - element sterujący staje się widoczny na ekranie natychmiast po otwarciu okna dialogowego; elementy, które nie mają tego stylu, trzeba wyświetlać samodzielnie za pomocą jednej z funkcji "API"; - "WS_TABSTOP" - z elementem sterującym jest związane miejsce tabulacji, można więc do niego uzyskać dostęp za pomocą klawisza "TAB" lub myszy; - "WS_GROUP" - element sterujący jest pierwszym z pewnej grupy elementów; ostatnim elementem tej grupy jest ostatni element okna dialogowego (występujący za elementem rozpoczynającym grupę), który nie ma stylu "WS_GROUP"; - "WS_VSCROLL", "WS_HSCROLL" - element sterujący ma suwak pionowy lub poziomy; styl ten dotyczy list i pól edycji; - "x", "y" - współrzędne lewego górnego rogu elementu; - "szer", "wys" - szerokość i wysokość elementu. W odniesieniu do niektórych elementów sterujących można zastosować uproszczony opis, mający postać następującą: TYP "tekst", ident, x, y, szer, wys na przykład DEFPUSHBUTTON "OK", id_Ok, 236, 68, 40, 14 Jak widać, w podanych wyżej wzorcach brakuje słowa "CONTROL" oraz elementów opisu "klasa" i "styl", które zostały zastąpione jedną z predefiniowanych stałych (oznaczoną wyżej symbolem "TYP"), opisujących klasę i styl niektórych standardowych typów elementów sterujących. I tak, na przykład, "PUSHBUTTON" oznacza standardowy przycisk, czyli element klasy , "BUTTON", opisany stylem "BS_PUSHBUTTON" | "WS_CHILD" | "WS_VISIBLE" | "WS_TABSTOP", "DEFPUSHBUTTON" - standardowy przycisk opcjonalny, zaś "LTEXT" pole statyczne zawierające tekst, który ma zostać wyrównany do lewej strony. Niestety nie istnieją predefiniowane stałe oznaczające automatyczne przyciski i pola wyboru. W odniesieniu do tych elementów sterujących, które nie mają nazwy, głównie pól edycji, list i pól kombinowanych, można zastosować jeszcze prostszy wzorzec, w którym dodatkowo opuszcza się element opisu "tekst": TYP ident, x, y, szer, wys na przykład EDITTEXT id_Polacz, 56, 114, 40, 12 Jeśli predefiniowana stała nie opisuje dokładnie potrzebnego elementu sterującego, to za liczbami określającymi jego szerokość i wysokość można podać stałą uzupełniającą opis stylu. Z możliwości tej warto na przykład skorzystać w stosunku do stałej "LISTBOX", oznaczającej listę z pionowym paskiem przewijania, ale (co dziwne) nie powiązaną z miejscem tabulacji. Aby więc zdefiniować listę, która może stać się aktywna, należy na końcu jej opisu dołączyć słowo "WS_TABSTOP" (poprzedzone przecinkiem), na rzykład: LISTBOX id_Szybkosc, 64, 20, 49, 48, WS_TABSTOP Po tak obfitej porcji informacji nadszedł czas na ich zastosowanie w praktyce. Tym razem rozbudujemy plik "PAW.RC", zawierający definicje zasobów naszej aplikacji, o opis okna dialogowego służącego do określenia wartości parametrów transmisji szeregowej (widoczne niżej wyrównanie tekstu zostało wykonane jedynie w celu ułatwienia jego zrozumienia, ale nie jest ono oczywiście konieczne do prawidłowej interpretacji opisu okna dialogowego przez kompilator zasobów): PAW DIALOG 20, 20, 288,134 STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU CAPTION "Parametry połączenia" BEGIN CONTROL "Numer portu", -1, "BUTTON", BS_GROUPBOX | WS_CHILD | WS_VISIBLE | WS_GROUP, 8, 8, 48, 64 CONTROL "COM &1", id_Com1, "BUTTON", BS_AUTORADIOBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 16, 20, 32, 12 CONTROL "COM &2", id_Com2, "BUTTON", BS_AUTORADIOBUTTON | WS_CHILD, | WS_VISIBLE | WS_TABSTOP, 16, 32, 32, 12 CONTROL "COM &3", id_Com3, "BUTTON", BS_AUTORADIOBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 16, 44, 32, 12 CONTROL "COM &4", id_Com4, "BUTTON", BS_AUTORADIOBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 16, 56; 32, 12 LTEXT "&Szybkość:", -1, 64, 10, 48, 8 LISTBOX id_Szybkosc, 64, 20, 49, 48 WS_TABSTOP CONTROL "Bity danych", -1, "BUTTON", BS_GROUPBOX | WS_CHILD | WS_VISIBLE | WS_GROUP, 120, 8, 48, 40 CONTROL "&7 bitów", id_Dane7, "BUTTON", BS_AUTORADIOBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 128, 20, 32, 12 CONTROL "&8 bitów", id_Dane8, "BUTTON", BS_AUTORADIOBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 128, 32, 32, 12 CONTROL "Bity stopu" , -1, "BUTTON", BS_GROUPBOX | WS_CHILD | WS_VISIBLE | WS_GROUP, 176, 8, 48, 40 CONTROL "1 bi&t", id_Stop1, "BUTTON", BS_AUTORADIOBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 184, 20, 32, 12 CONTROL "2 bit&y", id_Stop2, "BUTTON", BS_AUTORADIOBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 184, 32, 32, 12 CONTROL "Parzystość", -1, "BUTTON", BS_GROUPBOX | WS_CHILD | WS_VISIBLE | WS_GROUP, 232, 8, 48, 52 CONTROL "&Brak", id_ParzBrak, "BUTTON", BS_AUTORADIOBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 240, 20, 32, 12 CONTROL "&Even", id_ParzEven, "BUTTON", BS_AUTORADIOBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 240, 32, 32, 12 CONTROL "&Odd", id_ParzOdd, "BUTTON" , BS_AUTORADIOBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 240, 44, 32, 12 CONTROL "Sterowanie", -1, "BUTTON", BS_GROUPBOX | WS_CHILD | WS_VISIBLE | WS_GROUP, 120, 56, 104, 28 CONTROL "RTS/&CTS", id_SterRtsCts, "BUTTON", BS_AUTOCHECKBOX | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 128, 68, 40, 12 CONTROL "&XON/XOFF", id_SterXonXoff, "BUTTON", BS_AUTOCHECKBOX | WS_CHILD | WS_VISIBLE |WS_TABSTOP, 176, 68, 44, 12 LTEXT "&Inicjalizacja:", -1, 8, 94, 44, 8 EDITTEXT id_Inicjal, 56, 92, 112, 12, ES_AUTOHSCROLL LTEXT "&Połączenie:", -1, 8, 116, 44, 8 EDITTEXT id_Rozlacz, 156, 114, 68, 12, ES_AUTOHSCROLL DEFPUSHBUTTON "OK", id_Ok, 236, 68, 40, 14 PUSHBUTTON "Anuluj", id_Cancel, 236, 90, 40, 14 PUSHBUTTON "Przywróć", id_Przywroc, 236, 112, 40, 14 Dla zwiększenia przejrzystości programu w zamieszczonym opisie zostały zastosowane stałe oznaczające identyfikatory poszczególnych elementów sterujących. Ich definicje powinny się znaleźć w pliku "PAW_M.PAS": id_Com1 = 101; id_Com2 = 102; id_Com3 = 103; id_Com4 = 104; Id_Szybkosc = 105; id_Dane7 = 106; id_Dane8 = 107; id_Stop1 = 108; id_Stop2 = 109; id_ParzBrak = 110; id_ParzEven = 111; id_ParzOdd = 112; id_SterRtsCts = 113; id_SterXonXoff = 114; id_Inicjal = 115; id_Polacz = 116; id_Rozlacz = 117; id_Ok = 1; id_Cancel = 2; id_Przywroc = 118; 6.2.2. Edytor zasobów. Przy tworzeniu opisów okien dialogowych i ich elementów sterujących bardzo pomocne okazują się edytory zasobów, w tym opisywany już w tej książce kilkakrotnie edytor "Workshop". Aby utworzyć opis okna dialogowego podobny do przedstawionego w poprzednim podrozdziale, należy najpierw otworzyć plik "PAW.RC", a następnie wybrać polecenie "Resource" w nim "New" oraz określić typ zasobu jako "DIALOG". Spowoduje to otwarcie okna o nazwie "Dialog:Dialog_1", które obok definiowanego okna dialogowego o początowej nazwie "Dialog_1" zawiera także trzy okna pomocnicze: okno narzędzi ("Tools"), okno wyrównywania ("Alignment") oraz okno tytułu ("Caption"). Najważniejszym z wymienionych okien jest okno narzędzi, które między innymi zawiera przyciski umożliwiające umieszczanie na powierzchni okna kolejnych elementów sterujących. Bezpośrednio po utworzeniu mają one postać standardową, ale w krótkim czasie można je dostosować do własnych potrzeb. Nazwę elementu, podobnie jak i nazwę całego okna dialogowego, najłatwiej zmienić poprzez wpisanie jej do okna tytułu. Natomiast zmiany położenia i wielkości elementu sterującego lub okna dokonuje się w wyniku umiszczenia wskaźnika myszy na jego powierzchni lub ramce oraz przeciągnięcia jej w odpowiednim kierunku, czyli w wyniku wykonania działania takiego samego, jak podczas przesuwania i zmiany wielkości okna w dowolnej aplikacji Windows. Przed przystąpieniem do wykonywania każdej z opisanych czynności należy zwrócić uwagę na to, czy modyfikowany obiekt jest aktualnie wybrany, co można rozpoznać po otaczającej go podwójnej szarej ramce; jeśli nie, konieczne będzie jego kliknięcie. W odniesieniu do niektórych elementów sterujących konieczne jest dodatkowo albo ich podwójne kliknięcie, albo zaznaczenie i wybór polecenia menu "Control" w nim "Style". Obie te czynności prowadzą do otwarcia okna dialogowego o nazwie zawierającej nazwę klasy utworzonego obiektu, na przykład "Button style", i pozwalającego na jego dokładny opis. Skorzystanie z tego okna stanowi na przykład jedyną możliwość utworzenia automatycznego przełącznika (należy wówczas zaznaczyć przełącznik "Auto radio butto") lub automatycznego pola wyboru (przełącznik "Auto check box"). Rys. 6.2. W oknie dialogowym "Button style" można dokładnie określić styl elementu klasy "BUTTON". Można też skorzystać z okna "Window style", pozwalającego na dokładne określenie stylu całego okna dialogowego. Niemniej jednak podczas definiowania standardowych niezależnych okien dialogowych czynność tę wykonuje się bardzo rzadko i to tylko wtedy, gdy zachodzi konieczność zmiany czcionki, której system ma użyć do wyświetlenia tekstu w definiowanym oknie. W tym celu można albo wpisać nazwę czcionki i jej wielkość do odpowiednich pól okna "Window style", albo wybrać ją przez naciśnięcie przycisku "onts"; należy przy tym pamiętać, że standardowo w oknach dialogowych stosowana jest czcionka systemowa o wielkości 10. Pewnego rodzaju ciekawostkę stanowi fakt, iż okno dialogowe może mieć także własny pasek menu (widać to po polu edycji "Menu"), ale z możliwości tej praktycznie nigdy się nie korzysta. Rys. 6.3. Okno dialogowe "Window style" pozwala między innymi na wybór czcionki, której system ma użyć do wyświetlenia definiowanego okna dialogowego. Rys. 6.4. Wprowadzenie opisu zamieszczonego w poprzednim podrozdziale książki pozwala zapoznać się w programie "Workshop" z wyglądem tworzonego okna dialogowego. Często zdarza się, że pewna liczba elementów powinna być w pewien sposób wyrównana. Wtedy najlepiej jest je najpierw wszystkie zaznaczyć przez klikanie ich przy naciśniętym klawiszu "SHIFT", a następnie skorzystać z okna "Alignment". Rozwiązanie alternatywne stanowi wybór polecenia menu "Align" w nim "Align" i zaznaczenie odpowiednich przełączników w oknie dialogowym "Align controls". Na przykład, zaznaczenie pola "Left sides" (pierwszy przycisk w oknie "Alignment") spowoduje, że wszystkie zaznaczon elementy sterujące będą rozpoczynać się od pionowej linii określonej przez element położony najbardziej na lewo, zaznaczenie "Space eqaually" - że zostanie wyrównana odległość dzieląca poszczególne zaznaczone elementy, zaś "Center in dialog" (trzeci przycisk) - że zostaną one umieszczone dokładnie na środku okna dialogowego w kierunku poziomym lub pionowym. Po utworzeniu wszystkich potrzebnych elementów sterujących należy w oknie narzędzi nacisnąć przycisk oznaczony dwoma strzałkami skierowanymi w przeciwnym kierunku lub wybrać polecenie menu "Options" w nim "Set tabs", aby zainicjować operację ustalania położenia miejsc tabulacji (opisanych stylem "WS_TABSTOP"). Wszystkie elementy, które są z takimi miejscami powiązane, edytor wyróżnia przez umieszczenie wokół nich szarej ramki. Kliknięcie danego elementu powoduje umieszczenie w nim lub usunięcie z nigo miejsca tabulacji. Rys. 6.5. Miejsca tabulacji definiowanego okna dialogowego zostały już rozmieszczone. Kolejny etap pracy stanowi utworzenie grup elementów (określanych stylem "WS_GROUP"). W celu jego rozpoczęcia należy najpierw w oknie narzędzi nacisnąć przycisk "G" lub wybrać polecenie menu "Options" w nim "Set groups", a następnie spowodować metodą kliknięć, aby wszystkie te elementy sterujące, które mają rozpoczynać kolejne grupy, były otoczone szarą ramką. Rys. 6.6. Tym razem edytor zasobów pokazuje utworzone grupy elementów sterujących. Na zakończenie definiowania okna dialogowego konieczne jest ustawienie prawidłowej kolejności jego elementów sterujących. W tym celu trzeba najpierw nacisnąć przycisk 1, 2 lub wybrać polecenie menu "Options" w nim "Set groups", a następnie kliknąć w odpowiedniej kolejności poszczególne elementy. Rys. 6.7. Kolejność elementów sterujących łatwo rozpoznać po umieszczonych na nich liczbach. Wszystkie opisane wyżej czynności można też wykonać przez odpowiednią modyfikację wersji źródłowej opisu okna dialogowego - w tym celu wystarczy wybrać polecenie "Resource" w nim "Edit as text". Można też po zakończeniu projektowania okna otworzyć plik "PAW.RC" za pomocą dowolnego edytora tekstu, a następnie ponownie go skompilować, korzystając z edytora zasobów. W trakcie projektowania okna dialogowego edytor zasobów pozwala w każdej chwili przetestować wyniki wykonanej pracy, udostępniając do tego celu przycisk "Test" oraz odpowiadające mu polecenie menu "Options" w nim "Test dialog". W wyniku skorzystania z jednego z tych elementów programu zasymuluje on takie zachowanie się okna dialogowego, jakie będzie miało miejsce w rzeczywistych sytuacjach. 6.3. Obsługa okna dialogowego. 6.3.1. Wyświetlanie okien dialogowych. Zdefiniowanie okna dialogowego w pliku z zasobami stanowi być może najważniejszą, ale bynajmniej nie jedyną czynność, którą trzeba wykonać po to, by można je było udostępnić użytkownikowi - do tego niezbędne jest jeszcze odpowiednie zmodyfikowanie tekstu źródłowego przygotowywanej aplikacji Windows. Na początek zajmiemy się samym wyświetleniem przygotowanego okna, pomijając na razie zagadnienia związane z wymianą wartości elementów sterujących między programem a oknem oraz przetwarzaniem przez nie ddatkowych komunikatów. Jeśli korzysta się z biblioteki "ObjectWindows", dla każdego okna dialogowego trzeba utworzyć obiekt "TDialog" lub potomny w stosunku do niego. Konstruktor obiektu "TDialog" ma dwa parametry: wskaźnik do obiektu reprezentującego okno nadrzędne oraz nazwę okna dialogowego (podaną w pliku "PAW.RC" przed słowem "DIALOG"). Otwarcia okna dialogowego dokonuje się za pomocą metody "TApplication.ExecDialog", która działa analogicznie do "MakeWindow". Parametrem tej funkcji jest wskaźnik do obiektu reprezentującego okno, natomiast wartością - liczba całkowita przekazana przez dialog, będąca najczęściej identyfikatorem przycisku, którego naciśnięcie przez użytkownika spowodowało zamknięcie okna. Jeśli przyciskiem tym jest "OK" lub "Anuluj", okno zamykane jest automatycznie przez Windows. Aby w przygotowywanej przez nas aplikacji "PAW" wybór polecenia menu "Okno" w nim "Dialogowe" powodował wyświetlenie okna dialogowego "Parametry połączenia", trzeba w niej dokonać następujących zmian: TOkno = OBJECT(TWindow) ... PROCEDURE Dialogowe(VAR Msg: TMessage); VIRTUAL cm_First+cm_Dialogowe; ... END; {Reakcja na wybór polecenia menu Okno|Dialogowe} PROCEDURE TOkno.Dialogowe; VAR OknoDialogowe: PDialog; Komunikat: PChar; BEGIN OknoDialogowe := New(PDialog, Init(@Self, 'PAW')); IF Aplikacja.ExecDialog(OknoDialogowe) = id_OK THEN Komunikat := 'Został naciśnięty przycisk "OK".' ELSE Komunikat := 'Zosrtał naciśnięty przycisk "Anuluj".'; MessageBox(HWindow, Komunikat, 'Parametry połączenia', mb_IconInformation OR mb_OK); END; Rys.6.8. W wyniku wyboru polecenia menu "Okno" w nim "Dialogowe" program wyświetla okno dialogowe "Parametry połączenia". Zamknięcie okna dialogowego "Parametry połączenia" następuje zarówno w wyniku naciśnięcia przycisku "Anuluj", jak i "OK" - program zawsze potwierdza wykonaną czynność, wyświetlając odpowiednie okno informacyjne. Rys. 6.9. Program potwierdza naciśnięcie między innymi przycisku "OK". 6.3.2. Wymiana wartości elementów sterujących. Wyświetlanemu w naszej aplikacji oknu dialogowemu nie można wprawdzie niczego zarzucić pod względem wyglądu i obsługi, ale jego mankamentem jest brak wymiany wartości elementów sterujących między nim a programem. W konsekwencji wprowadzone za pomocą okna wartości parametrów nie są zapamiętywane i nie mają żadnego wpływu na dalsze działanie aplikacji - dzieje się tak, jakby za każdym razem użytkownik naciskał przycisk "Anuluj". Aby tę sytuację zmienić, trzeba rozbudować tekst przygotowywanego przez nas programu o elementy umożliwiające przekazywanie obiektowi okna bieżących wartości elementów sterujących przed jego wyświetleniem oraz odczytywanie i zapamiętywanie tych wartości po naciśnięciu przycisku "OK". Zadanie to można wykonać na dwa sposoby - w obecnym podrozdziale poznamy pierwszy z nich, a w następnym drugi. Sposób, który omówię teraz, nosi nazwę "transferu wartości" elementów sterujących okien dialogowych i wykorzystuje w dużym stopniu pomoc procedur biblioteki "ObjectWindows". Aby z niego skorzystać, należy w pierwszej kolejności utworzyć rekord pozwalający pamiętać wartości wszystkich elementów sterujących okna dialogowego. Zmienna typu tego rekordu, zwana buforem transferu, powinna stanowić jedno z pól obiektu okna, które wywołuje okno dialogowe, i być początkowo ustawiana w konstruktorze tego okna. Kolejność pól rekordu transferu musi odpowiadać kolejności elementów sterujących okna. Typ i wartości poszczególnych pól powinny być następujące: -przełączniki i pola wyboru (w tym także automatyczne) - liczba naturalna (typu "Word") przyjmująca najczęściej jedną z dwóch wartości: "bf_Checked" (element zaznaczony) lub "bf_Unchecked" (element nie zaznaczony); Obsługa okien dialogowych 145 - pola edycji - tablica znaków o liczbie elementów o 1 większej niż maksymalna długość tekstu, który może zostać wpisany do danego pola; - listy zwykłe i rozwijalne - wskaźnik do kolekcji łańcuchów oraz liczba będąca numerem wybranego elementu (licząc od zera); - pola kombinowane zwykłe i rozwijalne - wskaźnik do kolekcji łańcuchów oraz tablica znaków (tak jak w polach edycji). Jeśli chodzi o wspomnianą wyżej "kolekcję łańcuchów", to jest ona obiektem typu "TStrCollection", pozwalającym na przechowywanie zmiennej liczby łańcuchów tekstowych. Konstruktor tego obiektu ma dwa parametry będące liczbami całkowitymi, z których pierwsza określa początkową liczbę elementów kolekcji, druga zaś - wartość, o jaką ma się zwiększyć liczba elementów kolekcji wtedy, gdy liczba dotychczasowa okazuje się niewystarczająca. Kolejne łańcuchy znakowe dopisuje się do kolekcji za pomocą procedur "Insert", która oczekuje w postaci parametru wskaźnika do nowego łańcucha. Jeśli łańcuch ten jest podany w postaci stałej, należy dodatkowo zastosować funkcję "StrNew", tworzącą jego kopię na stosie i zwracającą wskaźnik do niej. Oprócz utworzenia bufora transferu należy jeszcze wprowadzić do programu dwa inne uzupełnienia, oba w miejscu bezpośrednio poprzedzającym wyświetlenie okna dialogowego za pomocą funkcji "ExecDialog": - utworzenie specjalnych obiektów, reprezentujących wszystkie te elementy okna dialogowego, które uczestniczą w przekazywaniu wartości, czyli są reprezentowane w buforze transferu, oraz - przyporządkowanie wskaźnika do tego bufora polu "TransferBuffer" obiektu okna dialogowego. Jeśli chodzi o typy obiektów reprezentujących poszczególne elementy sterujące okien dialogowych, to są one zdefiniowane w bibliotece "ObjectWindows". Do najważniejszych spośród nich należą: - "TRadioButton" - przełączniki, także automatyczne; - "TCheckBox" - pola wyboru, także automatyczne; - "TEdit" - pola edycji; - "TListBox" - listy zwykłe; - TComboBox - listy rozwijalne, pola kombinowane zwykłe i rozwijalne; Podczas tworzenia wymienionych obiektów korzysta się z konstruktora "InitResource" , który oczekuje podania co najmniej dwóch parametrów: wskaźnika do okna dialogowego oraz identyfikatora elementu sterującego, który ma dany obiekt reprezentować. Ponadto, jeśli element sterujący umożliwia wprowadzenie tekstu (na przykład pola edycji), konieczne jest podanie jeszcze jednego parametru, określającego maksymalną dozwoloną długość tego tekstu. Warto w tym miejscu zwrócić uwagę na to, że z omawianych obiektów (a także kilku innych, na przykład "TButton" czy "TStatic") można korzystać nie tylko w połączeniu z oknami dialogowymi, ale także po to, by utworzyć samodzielne elementy sterujące, występujące w innych oknach, na przykład nakładanych lub zależnych, i nie zdefiniowane w pliku z opisem zasobów. W takiej sytuacji nie korzysta się z konstruktora "InitResource", lecz z konstruktora "Init", który nie wymaga określenia identyfikatora elemenu sterującego, umożliwia natomiast podanie jego położenia, wymiarów i stylu. Niemniej jednak, zagadnieniem tym, jako mniej ważnym, nie będziemy się w tej książce dokładnie zajmować. Dokonanie w programie wszystkich opisanych wyżej zmian spowoduje, że: - wartości pamiętane w buforze transferu będą przekazywane automatycznie odpowiadającym im elementom sterującym okna dialogowego bezpośrednio przed jego otwarciem oraz - wartości elementów sterujących okna dialogowego będą zapamiętywane automatycznie w odpowiadających im polach bufora transferu po zamknięciu okna za pomocą przycisku "OK". Obecnie możemy przystąpić do modyfikowania przygotowywanej przez nas aplikacji "PAW". W pierwszej kolejności musimy zdefiniować typ rekordowy "TBuforDlg", pozwalający na utworzenie bufora transferu. Dla zwiększenia przejrzystości programu poszczególnym grupom przełączników i pól wyboru przyporządkujemy wspólne pola, będące tablicami liczb naturalnych: {Rekord z wartościami elementów sterujących okna dialogowego} TBuforDlg = RECORD NrPortu: ARRAY[1..4] OF Word; ListaSzybk: PStrCollection; Szybkosc: Integer; BityDanych: ARRAY[1..2] OF Word; BityStopu: ARRAY[1..2] OF Word; BityParz: ARRAY[1..3] OF Word; RtsCts: Word; XonXoff: Word; Inicjal: ARRAY[0..40] OF Char; Polacz: ARRAY[0..20] OF Char; Rozlacz: ARRAY[0..30] OF Char; END; Kolejny krok stanowi zadeklarowanie bufora transferu "BuforDlg" w definicji obiektu "TOkno", reprezentującego okno główne, oraz uzupełnienie konstruktora "Init" tego obiektu o wypełnienie poszczególnych pól bufora standardowymi wartościami elementów sterujących okna "Parametry połączenia". W tym celu konieczne jest także utworzenie kolekcji mieszczącej łańcuchy znakowe, będące kolejnymi elementami listy "Szybkość": TOkno = OBJECT(TWindow) ... BuforDlg: TBuforDlg; ... END; CONSTRUCTOR TOkno.Init; BEGIN ... WITH BuforDlg DO BEGIN NrPortu[1] := bf_Unchecked; NrPortu[2] := bf_Unchecked; NrPortu[3] := bf_Unchecked; NrPortu[4] := bf_Checked; ListaSzybk := New(PStrCollection, Init(1, 1)); ListaSzybk^.Insert(StrNew('75')); ListaSzybk^.Insert(StrNew('110')); ListaSzybk^.Insert(StrNew('150')); ListaSzybk^.Insert(StrNew('300')); ListaSzybk^.Insert(StrNew('600')); ListaSzybk^.Insert(StrNew('1200')); ListaSzybk^.Insert(StrNew('2400')); ListaSzybk^.Insert(StrNew('4800')); ListaSzybk^.Insert(StrNew('9600')); ListaSzybk^.Insert(StrNew('14400')); ListaSzybk^.Insert(StrNew('19200')); ListaSzybk^.Insert(StrNew('28800')); ListaSzybk^.Insert(StrNew('38400')); ListaSzybk^.Insert(StrNew('57600')); ListaSzybk^.Insert(StrNew('115200')); Szybkosc := 9; BityDanych[1] := bf_Unchecked; BityDanych[2] := bf_Checked; BityStopu[1] := bf_Checked; BityStopu[2] := bf_Unchecked; BityParz[1] := bf_Checked; BityParz[2] := bf_Unchecked; BityParz[3] := bf_Unchecked; RtsCts := bf_Checked; XonXoff := bf_Unchecked; StrCopy(Inicjal, 'AT &F &C1 &D2 E1'); StrCopy(Polacz, 'AT DP'); StrCopy(Rozlacz, '~+++~AT H0'); END; END; Ostatnią zmianą, którą trzeba dokonać w programie, jest odpowiednie zmodyfikowanie metody "TOkno.Dialogowe", reagującej na wywołanie polecenia menu "Okno" w nim "Dialogowe". W celu uproszczenia tekstu programu skorzystamy tu z faktu, że funkcja "New", z której korzystaliśmy już wielokrotnie, może także występować w postaci procedury. Zmienia się wtedy charakter pierwszego parametru, który nie jest już typem, lecz wskaźnikiem do tworzonego obiektu: PROCEDURE T.OknoDialogowe; VAR OknoDialogowe: PDialog; Komunikat: PChar; Com1, Com2, Com3, Com4: PRadioButton; Szybkosc: PListBox; Dane7, Dane8: PRadioButton; Stop1, Stop2: PRadioButton; ParzBrak, ParzEven, ParzOdd: PRadioButton; SterRtsCts, SterXonXoff: PCheckBox; Inicjal, Polacz, Rozlacz: Pedit; BEGIN OknoDialogowe := New(PDialog, Init(@Self, 'PAW')); New(Com1, InitResource(OknoDialogowe, id_Com1)); New(Com2, InitResource(OknoDialogowe, id_Com2)); New(Com3, InitResource(OknoDialogowe, id_Com3)); New(Com4, InitResource(OknoDialogowe, id_Com4)); New(Szybkosc, InitResource(OknoDialogowe, id_Szybkosc)); New(Dane7, InitResource(OknoDialogowe, id_Dane7)); New(Dane8, InitResource(OknoDialogowe, id_Dane8)); New(Stop1, InitResource(OknoDialogowe, id_Stop1)); New(Stop2, InitResource(OknoDialogowe, id_Stop2)); New(ParzBrak, InitResource(OknoDialogowe, id_ParzBrak)); New(ParzEven, InitResource(OknoDialogowe, id_ParzEven)); New (ParzOdd, InitResource (OknoDialogowe, id_ParzOdd)); New(SterRtsCts, InitResource(OknoDialogowe, id_SterRtsCts)); New(SterXonXoff, InitResource(OknoDialogowe, id_SterXonXoff); New(Inicjal, InitResource(OknoDialogowe, rd_Inicjal, SizeOf(BuforDlg.Inicjal))); New(Polacz, lnitResource(OknoDialogowe, id_Polacz, SizeOf(BuforDlg.Polacz))); New(Rozlacz, InitResource(OknoDialogowe, id_Rozlacz, SizeOf(BuforDlg Rozlacz))); OknoDialogowe^.TransferBuffer: @BuforDlg; Aplikacja.ExecDialog(OknoDialogowe); END; Rys. 6.10. W oknie dialogowym ustawione zostały już wartości elementów sterujących. 6.3.3. Tworzenie obiektów reprezentujących okna dialogowe. Jak widać na przykładzie naszej aplikacji, większość czynności związanych z obsługą okien dialogowych wykonuje sam system Windows, dzięki czemu nie ma potrzeby pisania własnych procedur umożliwiających na przykład naciskanie przełączników i pól wyboru (pod warunkiem, że zostały one zdefiniowane jako automatyczne), wprowadzanie tekstu do pól edycji czy wybieranie elementów list. Ponadto, jeśli został naciśnięty przycisk "OK" lub "Anuluj", automatycznie wykonywane jest zamknięcie okna (chyba że w plik z opisem zasobów podanym przyciskom zostały przydzielone inne identyfikatory niż 1 i 2). Mimo to czasami zdarzają się sytuacje, w których konieczne okazuje się przetwarzanie przez procedury programu komunikatów generowanych przez elementy sterujące okna dialogowego. Ma to miejsce na przykład wtedy, gdy użytkownik oczekuje, iż wykonanie pewnego działania, na przykład naciśnięcie przycisku lub wybór elementu listy, wpłynie na wartości pozostałych elementów sterujących okna. Z sytuacją taką mamy również do czynienia w przygotowywanej przez nas aplikacji -naciśnięcie przycisku "Przywróć" powinno spowodować przywrócenie wartości standardowych wszystkich elementów sterujących okna dialogowego "Parametry połączenia". Dobrze by też było, aby ustawienie określonej liczby bitów danych, stopu lub parzystości powodowało automatyczne dostosowanie liczby pozostałych rodzajów bitów, tak aby ich suma wynosiła 9 (czyli 10 razem z bitem startu), co jest warunkiem przeprowadzenia prawiłowej transmisji szeregowej. Aby te cele zrealizować, konieczne jest utworzenie obiektu potomnego w stosunku do "TDialog", który by reprezentował omawiane okno dialogowe. Obiekt ten, nazwany "TOknoDialogowe", powinien zawierać metody reagujące na naciśnięcie przycisku "Przywróć" oraz przełączników z grup "Bity danych", "Bity stopu" i "Parzystość". Powiązania tych metod z odpowiadającymi im komunikatami dokonuje się podobnie, jak podczas kojarzenia procedur z poleceniami menu, to znaczy poprzez przydzielenie im odpowiednich indesów. Jedyna różnica między obu czynnościami polega na tym, że tym razem zamiast stałej "cm_First" stosuje się stałą "id_First", oznaczającą początek zakresu komunikatów pochodzących od elementów sterujących okien dialogowych. Utworzenie obiektu reprezentującego okno dialogowe poddaje w wątpliwość sens stosowania transferu wartości jego elementów sterujących, opisanego w poprzednim podrozdziale. Sposób ten nie jest bowiem pozbawiony wad - dla każdego elementu trzeba przecież utworzyć oddzielny obiekt, a i struktura samego bufora transferu nie jest zbyt prosta. Dodatkowy mankament stanowi fakt, że listy występujące w oknie dialogowym trzeba zapełniać elementami w procedurze nie należącej do obiektu reprezentującego to okno co nie wydaje się najlepszym rozwiązaniem z punktu widzenia budowy programu. Dużo lepiej jest przecież wykonywać wszystkie czynności związane z obsługą okna dialogowego w obrębie jego obiektu. Wszystkie te argumenty przemawiają za tym, żeby ustawianie wartości elementów sterujących okna dialogowego "Parametry połączenia" przenieść do obiektu "TOknoDialogowe". Powstaje jednak w tym miejscu pytanie, w jakiej procedurze czynność ta powinna być wykonana - musi to z pewnością nastąpić na etapie tworzenia okna, jeszcze przed jego wyświetleniem. Dlatego najlepszym rozwiązaniem jest skorzystanie z metody "SetupWindow", która (jak wiadomo z fragmentu książki poświęconemu oknom dokumentów) istniejewe wszystkich obiektach standardowych reprezentujących okna i służy do wypełnienia ich obszaru roboczego. Aby jednak metoda "Setup" "wiedziała", jakie wartości ma przydzielić poszczególnym elementom sterującym, musi mieć ona dostęp do bufora "TBuforDlg", w którym wartości te są przechowywane. Dlatego niezbędne jest uzupełnienie listy parametrów konstruktora "Init" okna dialogowego o wskaźnik do tego bufora, a ponadto utworzenie pola, w którym mógłby on zostać zapamiętany, tak aby mogła z niego korzystać metoda "Setup". Nie można też zapomnieć o utworzeniu metody reagującej na naciśnięcie przycisku "OK",gdyż obecnie program sam ponosi odpowiedzialność za zapamiętanie zmienionych wartości elementów sterujących. Jednocześnie uprościć możemy postać bufora "TBuforDlg", usuwając z niego kolekcję łańcuchów znakowych będących elementami listy "Szybkość" oraz przeznaczając dla każdej z grup przełączników po jednym polu całkowitym (zapamiętana tu wartość będzie oznaczać identyfikator zaznaczonego przełącznika). W konsekwencji jeszcze większemu uproszczeniu ulega obecnie postać konstruktora okna głównego, "TOkno.Init". Wszystkie podane informacje znalazły swoje odzwierciedlenie w zamieszczonych niżej nowych fragmentach aplikacji "PAW" (nie jest to pełny tekst źródłowy aplikacji, jego dokończenie zostanie podane w następnym podrozdziale): PBuforDlg = ^TBuforDlg; TBuforDlg = RECORD NrPortu: Integer; Szybkosc: Integer; BityDanych: Integer; BityStopu: Integer; BityParz: Integer; RtsCts: Word; XonXoff: Word; Inicjal: ARRAY[0..40] OF Char; Polacz: ARRAY[0..20] OF Char; Rozlacz: ARRAY[0..30] OF Char; END; {Obiekt okna dialogowego} POknoDialogowe = ^TOknoDialogowe; TOknoDialogowe = OBJECT(TDialog) BuforDlg: PBuforDlg; CONSTRUCTOR Init(AParent: PWIndowsObjects; ATitle: PChar; ABuforDlg: PBuforDlg); PROCEDURE SetupWindow; VIRTUAL; PROCEDURE OK(VAR Msg: TMessage); VIRTUAL id_First+id_Ok; PROCEDURE Przywroc(VAR Msg: TMessage); VIRTUAL id_First+id_Przywroc PROCEDURE Dane7(VAR Msg: TMessage); VIRTUAL id_First+id_Dane7; PROCEDURE Dane8(VAR Msg: TMessage); VIRTUAL id_First+id_Dane8; PROCEDURE Stop1(VAR Msg: TMessage); VIRTUAL id_First+id_Stop1; PROCEDURE Stop2(VAR Msg: TMessage); VIRTUAL id_First+id_Stop2; PROCEDURE ParzBrak(VAR Msg: TMessage); VIRTUAL id_First+id_ParzBrak; PROCEDURE ParzEven(VAR Msg: TMessage); VIRTUAL id_First+id_ParzEven; PROCEDURE ParzOdd(VAR Msg: TMessage); VIRTUAL id_First+id_ParzOdd;END; CONSTRUCTOR TOkno.Init; BEGIN ... WITH BuforDlg DO BEGIN NrPortu := id_Com4; Szybkosc := 9; BityDanych := id_Dane8; BityStopu := id_Stop1; BityParz := id_ParzBrak; ... END; END; PROCEDURE TOkno.Dialogowe; VAR ... BEGIN OknoDialogowe := New(POknoDialogowe, Init(@Self, 'PAW', @BuforDlg)); ... END; 6.3.4. Przetwarzanie komunikatów w oknie dialogowym. Teraz pozostała jeszcze do napisania treść poszczególnych metod obiektu "TOknoDialogowe", których działanie sprowadza się głównie do ustawiania i odczytywania wartości poszczególnych elementów sterujących okna dialogowego "Parametry połączenia". Aby ten cel zrealizować, będziemy musieli skorzystać z kilku funkcji "API" (podczas korzystania z opisanego wcześniej bufora transferu wywoływanie tych funkcji przejmuje na siebie biblioteka "ObjectWindows"). Wśród nich na szczególną uwagę zasługuje funkcja "SendDlgItemMessage", która jest podobna do opisanej wcześniej funkcji "SendMessage", ale tym się od niej różni, że służy wyłącznie do przesyłania komunikatów do elementów sterujących okien dialogowych. Ponadto nie wymaga określenia uchwytu okna docelowego, lecz oczekuje podania uchwytu okna dialogowego oraz identyfikatora elementu sterującego - wartości te przekazuje się w postaci dwóch pierwszych parametrów. Co się tyczy pozostałych trzech parametró, to są one takie same, jak w funkcji "SendMessage", czyli określają identyfikator komunikatu oraz wartość pól "WParam" i "LParam" struktury typu "TMessage". Z każdym elementem sterującym jest związana duża liczba różnych komunikatów, nakazujących mu wykonanie określonych czynności. Identyfikatory tych komunikatów są pamiętane w bibliotece "ObjectWindows" w postaci stałych, które rozpoczynają się od przedrostków pochodzących od początkowych liter nazwy elementu oraz ewentualnie słowa "message". Tak więc stałe rozpoczynające się od "em_" oznaczają komunikaty pól edycji (skrót od "edit message"), od "lb_" - komunikaty list (skrót od "list box") itd. Podczas przesyłania komunikatów do elementów sterujących bardzo często istotna jest informacja zwrotna przekazywana przez te elementy oknu dialogowemu. Niektóre komunikaty nie "polecają" wręcz elementowi sterującemu wykonania żadnego innego działania, jak tylko udzielenie odpowiedzi na pewne pytanie, na przykład - jaki tekst został wprowadzony do pola edycji. Tego rodzaju informacja najczęściej przekazywana jest w postaci wartości zwracanej przez funkcję "SendDlgItemMessage", a niekiedy także za pośednictwem parametrów "wParam" i "lParam". A oto kilka przykładów komunikatów elementów sterujących (skorzystamy z nich w przygotowywanej przez nas aplikacji): - "em_LimitText" - komunikat określający maksymalną liczbę znaków, na których wpisanie ma zezwolić pole edycji; liczbę tę przekazuje się w postaci parametru "wParam"; - "lb_AddString" - komunikat "nakazujący" liście dopisanie kolejnego elementu, będącego łańcuchem znakowym, do którego wskaźnik przekazuje się w postaci parametru "lParam"; - lb_SetCurSel" - komunikat "nakazujący" liście wybór elementu o numerze określonym parametrem "wParam" (pierwszy element jest oznaczony numerem 0); - "lb_GetCurSel" - komunikat "pytający" listę o numer aktualnie wybranego elementu; numer ten jest zwracany w postaci wartości funkcji "SendDlgItemMessage". Mimo iż zarządzanie elementami sterującymi przebiega zawsze w oparciu o wysyłane do nich komunikaty, to jednak nie zawsze zachodzi potrzeba skorzystania z funkcji "SendDlgItemMessage". Biblioteka "API" zawiera bowiem kilka innych, wysoko wyspecjalizowanych funkcji, które służą do przesyłania elementom sterującym pewnych określonych komunikatów i odznaczają się przyjazną dla programisty postacią Elementem wspólnym wszystkich tych funkcji jest to, że ich dwa pierwsze parametry są zawsze takie same jakw funkcji "SendDlgItemMessage", czyli określają uchwyt okna dialogowego i identyfikator elementu sterującego. Jednym z takich podprogramów jest na przykład funkcja "CheckRadioButton", która w grupie przełączników zaznacza jeden z nich i kasuje wszystkie pozostałe. Drugi, trzeci i czwarty parametr określają kolejno: identyfikator pierwszego i ostatniego przełącznika należącego do danej grupy oraz identyfikator przełącznika, który ma zostać zaznaczony. Gdyby nie istniała funkcja "CheckRadioButton", należałoby wielokrotnie wywoływać funkcję "SendDlgItemMessage" w celu przesłania dla każdego przełącznika odpowidniego komunikatu (oznaczonego stałą "bm_SetCheck"). Do oznaczania oraz kasowania pól wyboru służy funkcja "CheckDlgButton". Stan, który ma przyjąć pole o podanym identyfikatorze, określa trzeci parametr tej funkcji, przyjmujący najczęściej wartość "bf_Checked" lub "bf_Unchecked". Jeżeli natomiast zachodzi potrzeba sprawdzenia stanu przełącznika lub pola wyboru, należy skorzystać z funkcji "IsDIgButtonChecked", która ma tylko dwa parametry i zwraca wartość także określoną jedną ze stałych rozpoczynających się od przedrostka "bf_". Wpisywania i odczytywania tekstu z pól edycji dokonuje się za pomocą funkcji "SetDlgItemText" i "GetDlgItemText". Obie one oczekują podania w postaci trzeciego parametru wskaźnika do bufora znakowego, który zawiera tekst lub do którego tekst z pola edycji ma zostać wpisany. Ponadto funkcja "GetDIgItemText" wymaga przekazania jeszcze jednego, czwartego parametru, określającego maksymalną długość tekstu, jaka ma zostać przekopiowana do bufora. Przyjęcie takiego rozwiązania zostało podyktowane troską oto, by nie doszło do przepełnienia zbyt małego bufora, co prędzej czy później doprowadziłoby niechybnie do zawieszenia się programu. Po omówieniu wszystkich tych informacji możemy się pokusić o uzupełnienie podanego w poprzednim podrozdziale tekstu źródłowego aplikacji "PAW", to znaczy o napisanie treści poszczególnych metod obiektu "TOknoDialogowe". Jedynym zadaniem konstruktora tego obiektu jest, oprócz wywołania konstruktora obiektu "TDialog", zapamiętanie adresu bufora "BuforDlg", w którym przechowywane są wartości elementów sterujących okna: {Konstruktor okna dialogowego} CONSTRUCTOR TOknoDialogowe.Init; BEGIN TDialog.Init(AParent, ATitle); BuforDlg := ABuforDlg; END; Dużo bardziej obszerna jest procedura "SetupWindow", która ponosi odpowiedzialność za wstawienie elementów listy "Szybkość" oraz ustawienie wartości wszystkich elementów sterujących na podstawie zawartości bufora "BuforDlg". Podobne zadanie wykonuje też metoda reagująca na naciśnięcie przycisku "Przywróć", z tym że nie musi już ona wstawiać elementów listy "Szybkość", a ponadto elementom sterującym przekazuje wartości standardowe, a nie te, które zostały zapamiętane w buforze "BuforDlg": {Zainicjowanie okna dialogowego} PROCEDURE TOknoDialogowe.SetupWindow; CONST ListaSzybk: ARRAY[0..14] OF PChar = ('75', '110', '150', '300', '600', '1200', '2400', '4800', '9600', '14400', '19200', '28800', '38400', '57600', '115200'); VAR NrElem: Integer; BEGIN TDialog.SetupWindow; CheckRadioButton(HWindow, id_Com1, id_Com4, BuforDlg^.NrPortu ); FOR NrElem := 0 TO 14 DO SendDlgItemMessage(HWindow, id_Szybkosc, lb_AddString, 0, LongInt(ListaSzybk[NrElem])); SendDlgItemMessage(HWindow, id_Szybkosc, lb_SetCurSel, BuforDlg^.Szybkosc,0); CheckRadioButton(HWindow, id_Dane7, id_Dane8, BuforDlg^.BityDanych); CheckRadioButton(HWindow, id_Stop1, id_Stop2, BuforDlg^.BityStopu); CheckRadioButton(HWindow, id_ParzBrak, id_ParzOdd, BuforDlg^.BityParz); CheckDlgButton(HWindow, id_SterRtsCts, BuforDlg^.RtsCts); CheckDlgButton(HWindow, id_SterXonXoff, BuforDlg^.XonXoff); SendDlgItemMessage(HWindow, id_Inicjal, em_LimitText, SizeOf(BuforDlg^.Inicjal)-1, 0); SetDlgItemText(HWindow, id_Inicjal, BuforDlg^.Inicjal); SendDlgItemMessage(HWindow, id_Polacz, em_LimitText, SizeOf(BuforDlg^.Polacz)-1, 0); SetDlgItemText(HWindow, id_Polacz, BuforDlg^.Polacz); SendDlgItemMessage(HWindow, id_Rozlacz, em_LimitText, SizeOf(BuforDlg^.Rozlacz)-1, 0); SetDlgItemText(HWindow, id_Rozlacz, BuforDlg^.Rozlacz); END; {Reakcja na naciśnięcie przycisku Przywróć} PROCEDURE TOknoDialogowe.Przywroc; BEGIN CheckRadioButton(HWindow, id_Com1, id_Com4, id_Com4); SendDlgItemMessage(HWindow, id_Szybkosc, lb_SetCurSel, 9, 0); CheckRadioButton(HWindow, id_Dane7, id_Dane8, id_Dane8); CheckRadioButton(HWindow, id_Stop1, id_Stop2, id_Stop1); CheckRadioButton(HWindow, id_ParzBrak, id_OarzOdd, id_ParzBrak); CheckDlgButton(HWindow, id_SterRtsCts, bf_Checked); CheckDlgButton(HWindow, id_SterXonXoff, bf_Unchecked); SetDlgItemText(HWindow, id_Inicjal, 'AT &F &C1 &D2 E1'); SetDlgItemText(HWindow, id_Polacz, 'AT DP'); SetDlgItemText(HWindow, id_Rozlacz, '~+++~AT H0'); END; Działanie w pewnym sensie odwrotne do procedury "SetupWindow" wykonuje metoda reagująca na naciśnięcie przycisku "OK". Jej zadaniem jest odczytanie stanu poszczególnych elementów sterujących i zapamiętanie go w buforze "BuforDlg": {Reakcja na naciśnięcie przycisku OK} PROCEDURE TOknoDialogowe.OK; VAR Ident: Word; BEGIN FOR Ident := id_Com1 TO id_Com4 DO IF IsDlgButtonChecked(HWindow, Ident) = bf_Checked THEN BuforDlg^.NrPortu := Ident; Bufor^.Szybkosc := SendDlgItemMessage(HWindow, id_Szybkosc, lb_GetCurSel, 0, 0); FOR Ident := id_Dane7 TO id_Dane8 DO IF IsDlgButtonChecked(HWindow, Ident) = bf_Checked THEN BuforDlg^.BityDanych := Ident; FOR Ident := id_Stop1 TO id_Stop2 DO IF IsDlgButtonChecked(HWindow, Ident) = bf_Checked THEN BuforDlg^.BityStopu := Ident; FOR Ident := id_ParzBrak TO id_ParzOdd DO IF IsDlgButtonChecked(HWindow, Ident) = bf_Checked THEN BuforDlg^.BityParz := Ident; Bufor^.RtsCts := IsDlgButtonChecked(HWindow, id_SterRtsCts); BuforDlg^.XonXoff := IsDlgButtonChecked(HWindow, id_SterXonXoff); GetDlgItemText(HWindow, id_Inicjal, BuforDlg^.Inicjal, SizeOf(BuforDlg^.Inicjal)); GetDlgItemText(HWindow, id_Polacz, BuforDlg^.Polacz, SizeOf(BuforDlg^.Polacz)); GetDlgItemText(HWindow, id_Rozlacz, BuforDlg^.Rozlacz, SizeOf(BuforDlg^.Rozlacz)); TDialog.Ok(Msg); END; Ostatnią grupę metod obiektu "TOknoDialogowe" stanowią procedury reagujące na zaznaczanie przełączników z grup służących do określenia formatu przesyłanych danych: {Reakcja na naciśnięcie przełącznika Bity danych|7 bitów} PROCEDURE TOknoDialogowe.Dane7; BEGIN IF (IsDlgButtonChecked(HWindow, id_Stop1) = bf_Checked) AND (IsDlgButtonChecked(HWindow, id_ParzBrak) = bf_Checked) THEN CheckRadioButton(HWindow, id_ParzBrak, id_ParzOdd, id_ParzEven); END; {Reakcja na naciśnięcie przełącznika Bity danych|8 bitów} PROCEDURE TOknoDialogowe.Dane8; BEGIN CheckRadioButton(HWindow, id_Stop1, id_Stop2, id_Stop1); CheckRadioButton(HWindow, id_ParzBrak, id_ParzOdd, id_ParzBrak); END; {Reakcja na naciśnięcie przełącznika Bity stopu|1 bit} PROCEDURE TOknoDialogowe.Stop1; BEGIN IF IsDlgButtonChecked(HWindow, id_Dane8) = bf_Checked THEN CheckRadioButton(HWindow, id_ParzBrak, id_ParzOdd, id_ParzBrak) ELSE IF IsDlgButtonChecked(HWindow, id_ParzBrak) = bf_Checked THEN CheckRadioButton(HWindow, id_ParzBrak, id_ParzOdd, id_ParzEven); END; {Reakcja na naciśnięcie przełącznika Bity stopu|2 bity} PROCEDURE TOknoDialogowe.Stop2; BEGIN CheckRadioButton(HWindow, id_Dane7, id_Dane8, id_Dane7); CheckRadioButton(HWindow, id_ParzBrak, id_ParzOdd, id_ParzBrak); END; {Reakcja na naciśnięcie przełącznika Parzystość|Brak} PROCEDURE TOknoDialogowe.ParzBrak; BEGIN CheckRadioButton(HWindow, id_Dane7, id_Dane8, id_Dane8); CheckRadioButton(HWindow, id_Stop1, id_Stop2, id_Stop1); END; {Reakcja na naciśnięcie przełącznika Parzystość|Even} PROCEDURE TOknoDialogowe.ParzEven; BEGIN CheckRadioButton(HWindow, id_Dane7, id_Dane8, id_Dane7); CheckRadioButton(HWindow, id_Stop1, id_Stop2, id_Stop1); END; {Reakcja na naciśnięcie przełącznika Parzystość|Odd} PROCEDURE TOknoDialogowe.ParzOdd; BEGIN CheckRadioButton(HWindow, id_Dane7, id_Dane8, id_Dane7); CheckRadioButton(HWindow, id_Stop1, id_Stop2, id_Stop1); END; 6.4. Standardowe okna dialogowe pakietu "TPW". 6.4.1. Okno do wczytania tekstu. Nie zawsze wyświetlenie okna dialogowego wymaga samodzielnego opisania go w pliku z zasobami i utworzenia reprezentującego go obiektu. Biblioteka "ObjectWindows" zawiera bowiem kilka standardowych okien dialogowych, służących do wprowadzenia tekstu oraz wyboru pliku. Pierwsze z nich jest reprezentowane przez obiekt "TlnputDialog" oraz jeden z zasobów zapisanych w module "StdDlgs" (w pliku "STDDLGS.RES"). Konstruktor obiektu "TlnputDialog" wymaga podania następujących parametrów: wskaźnika do okna nadrzędnego, tytułu okna dialogowego, komunikatu, który ma zostać w tym oknie wyświetlony, bufora przeznaczonego do przechowania wprowadzanego tekstu oraz liczby określającej jego maksymalną długość. Aby umożliwić w naszym programie wyświetlenie okna służącego do wczytania tekstu, dokonamy w nim następujących zmian: - uzupełnimy klauzulę "USES" o moduł "StdDlgs", zawierający obiekt "TlnputDialog"; - rozbudujemy obiekt "TOkno" o metodę reagującą na wybór polecenia "Okno" w nim "TlnputDialog"; - utworzymy tekst nowej metody, która wczytywać będzie za pomocą okna dialogowego tekst i potwierdzać jego postać w przypadku naciśnięcia przycisku "OK". USES WObjects, WinTypes, WinProcs, Strings, StdDlgs, PAW_M; TOkno = OBJECT(TWindow) ... PROCEDURE InputDlg(VAR Msg: TMessage); VIRTUAL cm_First+cm_Input; ... END; {Reakcja na wybór polecenia menu Okno|TInputDialog} PROCEDURE TOkno.InputDlg; VAR OknoDialogowe: PInputDialog; Bufor: ARRAY[0..64] OF Char; BEGIN StrPCopy(Bufor, 'MS Windows'); OknoDialogowe := New(PInputDialog, Init(@Self, 'Okno standardowe TInputDialog', 'Tekst:', Bufor, 64)); IF Aplikacja.ExecDialog(OknoDialogowe) = id_OK THEN MessageBox(HWindow, Bufor, 'Wprowadzony tekst', mb_IconInformation OR mb_OK); END; Rys. 6.11. Często wystarczy skorzystać ze standardowego okna służącego do wprowadzania tekstu. Rys. 6.12. Program "PAW" potwierdza każdorazowo wczytanie wprowadzonego tekstu. 6.4.2. Okna do wczytania nazwy pliku. Znacznie bardziej rozbudowane niż okno służące do wprowadzania tekstu są okna umożliwiające określenie nazwy pliku przeznaczonego do odczytu lub zapisu danych. Są one reprezentowane przez obiekt "TFileDialog" oraz zasoby zapisane w module "StdDlgs". Konstruktor obiektu "TFileDialog" wymaga podania następujących parametrów: - wskaźnika do okna nadrzędnego; - tytułu okna dialogowego, który może przyjąć tylko jedną z dwóch postaci, wykorzystujących definicje zasobów z modułu "StdDlgs": "PChar(sd_FileOpen)" (oznaczającą tekst "File Open"), jeśli plik ma zostać otwarty do odczytu, lub "PChar(sd_FileSave") ("= File Save As") w przeciwnym wypadku; - bufora przeznaczonego do przechowania wprowadzonej nazwy pliku wraz z pełną ścieżką, określającą jego położenie w systemie komputerowym; bufor ten powinien być tablicą pozwalającą na pomieszczenie "fsPathName" znaków (stała "fsPathName" jest zdefiniowana w module "WinDos" pakietu "TPW"). Okna obiektu "TFileDialog" nadają się świetnie do stosowania z poleceniami "Otwórz" i "Zachowaj jako" z menu "Plik". Jedyną ich wadą jest to, że wszystkie wyświetlane w nich teksty są w języku angielskim. Problem ten można łatwo rozwiązać przez odpowiednie zmodyfikowanie ich oryginalnego opisu, znajdującego się w pliku "STDDLGS.RES", i zapamiętanie go w innym, własnym pliku z opisem zasobów. Zmiany, których musimy dokonać w aplikacji "PAW" po to, aby umożliwić wyświetlenie w niej okna pozwalającego określić nazwę pliku przeznaczonego do otwarcia, są podobne do opisanych w poprzednim podrozdziale. Polegają one na: - uzupełnieniu klauzuli "USES" o moduł "WinDos", który zawiera definicję stałej "fsPathName"; - rozbudowaniu obiektu "TOkno" o metodę reagującą na wybór polecenia "Okno" w nim "TFileDialog"; - utworzeniu tekstu nowej metody, która wczytywać będzie za pomocą okna dialogowego nazwę pliku i potwierdzać jego postać w przypadku naciśnięcia przycisku "OK". USES WObjects, WinTypes, WinProcs, WinDos, Strings, StdDlgs, PAW_M; TOkno = OBJECT(TWindow) ... END; {Reakcja na wybór polecenia menu Okno|TFileDialog} PROCEDURE TOkno.FileDlg; VAR OknoDialogowe: PFileDialog; NazwaPliku: ARRAY[0..fsPathName] OF Char; BEGIN StrCopy(NazwaPliku, '*.*'); OknoDialogowe := New(PFileDialog, Init(@Self,PChar(sd_FileOpen), NazwaPliku)); IF Aplikacja.ExecDialog(OknoDialogowe) = id_OK THEN MessageBox(HWindow, NazwaPliku, 'Wybrana nazwa pliku', mb_IconInformation OR mb_OK); END; Rys. 6.13. Skorzystanie ze standardowego okna służącego do wyboru nazwy pliku pozwala programiście zaoszczędzić wiele pracy. Rys. 6.14. Program potwierdza wczytaną nazwę pliku wraz z całą ścieżką określającą jego położenie na dysku. Rozdział 7. Wczytywanie danych: klawiatura, mysz i zegar. Wprawdzie większość danych wejściowych wprowadzana jest w aplikacjach Windows za pośrednictwem wygodnych w obsłudze elementów interfejsu graficznego, takich jak okna dialogowe i ich elementy sterujące, paski narzędzi czy system menu, ale istnieją także sytuacje (na przykład przygotowywanie edytora tekstu), w których zachodzi konieczność samodzielnego wczytywania danych wprowadzanych za pomocą klawiatury czy myszy. W obecnym rozdziale zajmiemy się zarówno obsługą zdarzeń powstających w wyniku użycia ych dwóch urządzeń zewnętrznych, jak i generowanych przez zegar, który także może być źródłem danych wejściowych. Dzięki jego stosowaniu możliwe jest wykonywanie w aplikacjach Windows w stałych odstępach czasu pewnych czynności, takich jak wyświetlanie bieżącej godziny. 7.1. Klawiatura. 7.1.1. Komunikaty dotyczące klawiszy. Zgodnie z ogólną filozofią funkcjonowania środowiska Windows naciśnięcie lub zwolnienie klawisza klawiatury powoduje wygenerowanie odpowiedniego komunikatu. Komunikat ten kierowany jest do tego spośród wszystkich okien otwartych w systemie, do którego kierowane są dane wprowadzane za pomocą klawiatury, czyli tego, które jako ostatnie zostało wyróżnione (focus). Oknem tym jest albo okno aktywne (oznaczone podświetlonym paskiem tytułu lub samym tytułem, jeśli jest to ikona), albo jedno z jego okien porzędnych (na przykład jeden z elementów sterujących okna dialogowego). W tej samej chwili dane wprowadzane za pomocą klawiatury mogą być kierowane co najwyżej do jednego okna. Jako ciekawostkę można podać fakt, że istnieją sytuacje, w których nie ma żadnego takiego okna - wówczas naciśnięcie klawisza klawiatury powoduje wygenerowanie tak zwanego komunikatu systemowego, obsługiwanego, zgodnie ze swoją nazwą, przez sam system Windows. Komunikaty systemowe generowane są także wtedy, gdy została naciśnięta kombinacja dowolnego klawisza z klawiszem "ALT", gdyż w środowisku indows tego rodzaju kombinacje są zarezerwowane dla obsługi interfejsu graficznego, na przykład do wywoływania poleceń menu. Oczywiście aplikacja może przechwytywać komunikaty systemowe, ale postępowanie takie nie jest zalecane. Po naciśnięciu klawisza generowany jest komunikat "wm_KeyDown" (lub komunikat systemowy "wm_SysKeyDown"), natomiast po jego zwolnieniu - "wm_KeyUp" (lub "wm_SysKeyUp"). Warto zauważyć, że liczba wysłanych komunikatów "wm_KeyDown" może być większa niż liczba komunikatów "wm_KeyUp"; sytuacja taka ma miejsce wtedy, gdy dany klawisz przytrzymywany jest przez dłuższy czas. Kod naciśniętego lub zwolnionego klawisza przekazywany jest w polu "WParam" struktury "TMessage" (będącej parametrem procedury obsługującej w systemie "TPW" każde zdarzenie). Kody poszczególnych klawiszy mają charakter wirtualny, to znaczy niezależny od konkretnego typu klawiatury. Stałe określające kody klawiszy są zdefiniowane w module "WinTypes" i rozpoczynają się od znaków "vk_" (skrót od ang. wyrażenia "virtual key" = klawisz wirtualny), na przykład: "vk_Return", "vk_Escape", "vk_Space", "vk_Pror" ("PGUP"), "vk_Next" ("PGDN"), "vk_Home", "vk_End", "vk_Left", "vk_Right", "vk_Up", vk_Down", "vk F1", ..., "vk F16" i wiele innych. Jeśli chodzi o pole "LParam" parametru typu "TMessage", to zawiera ono wiele informacji, z których jednak istotna jest jedynie liczba powtórzeń naciśnięć klawisza, pamiętana w mniej znaczącym słowie, czyli w słowie zajmującym bity od 0 do 15 (parametr "LParamLo"). Liczba ta jest różna od 1 wtedy, gdy użytkownik naciska przez pewien czas określony klawisz, a program nie nadąża z obsługą wszystkich generowanych w ten sposób pojedynczych komunikatów "wm_KeyDown" (lub "wm_SysKeyDown"). W takiej sytuacjikolejne pojedyncze komunikaty łączone są w jeden, o odpowiednio zwiększonej liczbie powtórzeń. Co się tyczy pozostałych informacji zawartych w polu "Lparam", to mają one dużo mniejsze znaczenie (w większości są nadmiarowe) i w praktyce prawie nigdy się z nich nie korzysta. Istotne jest to, że żadne z pól struktury "TMessage" nie informuje o stanie, w jakim znajdowały się klawisze takie, jak "SHIFT" czy "CTRL", w czasie naciśnięcia lub zwolnienia klawisza, który wygenerował odpowiedni komunikat. Sprawdzenia stanu wybranych klawiszy (w chwili zajścia zdarzenia opisanego otrzymanym komunikatem) trzeba dokonać samodzielnie, przez wywołanie funkcji "API" "GetKeyState". Należy jednak pamiętać o tym, że funkcję tę można wywołać tylko po otrzymaniu komunikatu informującego o prowadzeniu danych za pomocą klawiatury - nie można z niej korzystać w dowolnym miejscu programu w celu uzyskania informacji o bieżącym stanie klawiatury. Funkcji "GetKeyState" trzeba przekazać w postaci parametru kod wirtualny klawisza, którego stan nas interesuje. Zwracana wartość jest typu całkowitego i zawiera dwa istotne bity. Podczas badania stanu klawiszy takich jak "SHIFT" korzysta się z bitu najstarszego; jeśli jest on równy 1, dany klawisz był wciśnięty, w przeciwnym wypadku - zwolniony. Natomiast w przypadku sprawdzania stanu klawiszy dwustanowych, takich jak "CAPSLOCK", użyteczny jest bit najmłodszy; jego wartość 1 oznacza, że dany klawiszbył włączony (został naciśnięty nieparzystą liczbę razy), wartość 0 sygnalizuje wyłączenie klawisza. Podane informacje wykorzystamy teraz w praktyce, modyfikując napisany przez nas wcześniej program "MDI" o możliwość przewijania zawartości okien dokumentów (zmiany położenia suwaków w paskach przewijania) za pomocą klawiszy ze strzałkami (przewinięcie o wartość jednostkową) oraz klawiszy: "HOME" (przewinięcie do lewej strony obrazu), "END" (przewinięcie do prawej strony), "PGUP" (przewinięcie na początek) i "PGDN" (przewinięcie na koniec obrazu). Założymy przy tym, że w momencie naciskania jednego zwymienionych klawiszy zwolniony powinien być zarówno klawisz "SHIFT", jak i "CTRL". W tym celu skorzystamy z dwóch metod obiektu typu "TScroller", reprezentującego paski przewijania, "ScrollBy" i "ScrollTo". Obie te metody mają po dwa parametry, z których pierwszy określa odpowiednio wielkość przesunięcia lub docelowe położenie suwaka w pasku przewijania poziomego, drugi zaś określa w taki sam sposób docelowe położenie suwaka w pasku przewijania pionowego. Wartości obu parametrów wyrażone są w jednostkach równych wartościom przewinięcia jednostkowego, zapisanych w polach "XUnit" i YUnit" (zobacz podrozdział 5.3.1, "Utworzenie pasków przewijania"). Aby w programie "MDI" zrealizować opisane zadanie, konieczne jest zdefiniowanie metody "WMKeyDown" obiektu "TOknoDok", reprezentującego okno dokumentu, i powiązanie jej z komunikatem "wm_KeyDown". Metoda ta powinna sprawdzać zarówno stan klawiszy modyfikujących "SHIFT" i "CTRL", jak i kod wirtualny naciśniętego klawisza i w przypadku zajścia istotnego zdarzenia dokonać odpowiedniego przesunięcia zawartości okna dokumentu: TOknoDok = OBJEECT(TWindow) ... PROCEDURE WMKeyDown(VAR Msg: TMessage); VIRTUAL wm_First+wm_KeyDown; ... END; {Reakcja na naciśnięcie niektórych klawiszy klawiatury} PROCEDURE TOknoDok.WMKeyDown; BEGIN IF (GetKeyState(vk_Shift) >= 0) AND (GetKeyState(vk_Control) >= 0) THEN WITH Scroller^ DO CASE Msg.WParam OF vk_Left: ScrollBy(-1, 0); vk_Right: ScrollBy(1, 0); vk_Up: ScrollBy(0, -1); vk_Down: ScrolBy(0, 1); vk_Home: ScrollTo(0, YPos); vk_End: ScrollTo(XRange, YPos); vk_Prior: ScrollTo(XPos, 0); {PgUp} vk_Next: ScrollTo(XPos, YRange); {PgDn} END; END; Rys.7.1. W zmodyfikowanym programie wyświetlany obraz graficzny daje się przesuwać także za pomocą klawiszy kierunkowych. 7.1.2. Wyświetlanie kursora (wskaźnika klawiatury). Wpisywaniu tekstu towarzyszy zawsze wyświetlanie znaku graficznego określającego miejsce, w którym zostanie umieszczony następny wprowadzony za pomocą klawiatury znak. W systemie DOS znak ten nazywa się powszechnie kursorem. W środowisku Windows obok pojęcia "kursor" istnieje kilka innych określeń: kursor tekstowy, wskaźnik klawiatury, punkt wprowadzania tekstu, karetka. Ta niejednolitość słownictwa wynika stąd, że w Windows powszechnie stosowana jest mysz, której wskaźnik niekiedy także nazywany jet kursorem. Warto zresztą pamiętać o tym, że w literaturze angielskojęzycznej słowa "cursor" zasadniczo używa się właśnie w odniesieniu do wskaźnika myszy, podczas gdy na określenie wskaźnika klawiatury stosuje się słowo "carat". My jednak pozostaniemy wierni dawnym przyzwyczajeniom (z systemu DOS) i w dalszej części książki wskaźnik klawiatury nazywać będziemy tradycyjnie kursorem. W systemie Windows może w danej chwili istnieć tylko jeden kursor. W związku z tym wskaźnik ten powinno się tworzyć i wyświetlać tylko wtedy, gdy do danego okna został skierowany strumień danych wprowadzanych za pomocą klawiatury (gdy okno to zostało wyróżnione) i usuwać natychmiast po utracie przez okno strumienia danych. Oba te zdarzenia sygnalizowane są przez wysłanie do okna komunikatów "wm_SetFocus" oraz "wm_KillFocus". Do obsługi kursora stosuje się głównie następujące funkcje "API": - "CreateCaret" - tworzy kursor; ma cztery parametry, z których pierwszy zawiera uchwyt okna, drugi określa, czy kursor ma być wyświetlany w kolorze czarnym (wartość 0), czy szarym (1), zaś pozostałe dwa ustalają szerokość i wysokość kursora; kursor może przyjmować postać poziomej lub pionowej kreski lub prostokącika, ale w przypadku stosowania czcionki o zmiennej szerokości znaków powinno się ze zrozumiałych względów ograniczyć do tworzenia kursorów w kształcie pionowej kreski (o szerokości równej ); funkcję "CreateCaret" należy wywoływać każdorazowo po otrzymaniu komunikatu "wm_SetFocus"; - "DestroyCaret" - niszczy kursor, to znaczy usuwa go z systemu; ponieważ w systemie może istnieć co najwyżej jeden kursor, procedura "DestroyCaret" nie ma żadnego parametru; powinna ona być wywoływana każdorazowo po otrzymaniu komunikatu "wm_KillFocus", po wcześniejszym wywołaniu procedury "HideCaret"; - "ShowCaret" - wyświetla na ekranie kursor; ma jeden parametr: uchwyt okna, w którym został utworzony kursor za pomocą procedury "CreateCaret"; procedura "ShowCaret" powinna być wywoływana po utworzeniu i ustaleniu położenia kursora ("ShowCaretPos"), a także w celu wznowienia wyświetlania kursora po wcześniejszym ukryciu go za pomocą procedury "HideCaret"; - "HideCaret" - działa przeciwnie do procedury "ShowCaret", to znaczy powoduje, że kursor staje się niewidoczny; jedyny parametr określa uchwyt okna; kursor musi być zawsze ukryty podczas tworzenia tekstu lub rysunków w oknie; ponieważ funkcje "BeginPaint" i "EndPaint" wywołują procedury "HideCaret" i "ShowCaret" automatycznie, w funkcji "Paint" obiektów reprezentujących okna nie ma potrzeby samodzielnego wykonywania tej operacji; - "ShowCaretPos" - ustala pozycję kursora w oknie; ma dwa parametry, określające współrzędne logiczne (względem powierzchni okna) "x" i "y" lewego górnego rogu znaku graficznego reprezentującego kursor; standardowo współrzędne te określa się w pikselach, względem lewego górnego rogu okna. W celu dostosowania wielkości kursora do rozmiarów znaków czcionki stosowanej w danym oknie warto skorzystać z funkcji "API" "GetTextMetrics". Ma ona dwa parametry, z których pierwszy określa uchwyt okna, drugi zaś jest strukturą typu "TTextMetric", zawierającą wiele pól opisujących dokładnie postać danej czcionki. Do pól tych należy między innymi: "tmAveCharWidth" i "tmMaxCharWidth" (przeciętna i maksymalna szerokość znaków) oraz "tmHeight" (wysokość znaków). Aby w oknach dokumentów naszej aplikacji "MDI" był wyświetlany kursor w kształcie prostokąta o rozmiarach pokrywających się z rozmiarami znaków czcionki systemowej ("System_Fixed_Font"), konieczne jest dokonanie następujących zmian: - utworzenie pól "SzerZn" i "WysZn" w obiekcie "TOknoDok", reprezentującym okna dokumentów; - obliczenie w konstruktorze obiektu "TOknoDok" szerokości i wysokości pojedynczego znaku czcionki systemowej i zapamiętanie otrzymanych wartości w zmiennych "SzerZn" i "WysZn"; aby tego dokonać, trzeba wcześniej wybrać czcionkę systemową dla kontekstu wyświetlania danego okna przez wywołanie funkcji "SelectObject"; w celu uzyskania uchwytu czcionki systemowej nie jest konieczne korzystanie z funkcji "CreateFont" lub "CreateFontlndirect" - wystarczy zastosować funkcję "GetStockObject", pozwalającą uyskiwać uchwyty predefiniowanych czcionek, piór oraz pędzli; należy pamiętać o tym, że w przypadku uzyskiwania uchwytu obiektu logicznego za pomocą funkcji "GetStockObject" nie wolno usuwać tego obiektu przy użyciu funkcji "DeleteObject"; - utworzenie kursora oraz określenie jego pozycji i wyświetlenie go po uzyskaniu dostępu do klawiatury (po otrzymaniu komunikatu "wm_SetFocus", to znaczy w funkcji "TOknoDok.WMSetFocus") oraz zakończenie wyświetlania kursora i usunięcie go z systemu po utraceniu dostępu do klawiatury (po uzyskaniu komunikatu "wm_KillFocus", czyli w funkcji "TOknoDok.WMKilIFocus"). TOknoDok = OBJECT(TWindow) ... SzerZn, WysZn: Integer; ... PROCEDURE WMSetFocus(VAR Msg: TMessage); VIRTUAL wm_First+wm_SetFocus; PROCEDURE WMKillFocus(VAR Msg: TMessage); VIRTUAL wm_First+wm_KillFocus; ... END; CONSTRUCTOR TOknoDok.Init; VAR WymTekst: TTextMetric; KontWysw: HDC; BEGIN ... KontWysw := GetDC(HWindow); SelectObject(KontWysw, GetStockObject(SYSTEM_FIXED_FONT)); GetTextMetrics(KontWysw, WymTekst); SzerZn := WymTekst.tmAveCharWidth; WysZn := WymTekst.tmHeight; ReleaseDC(HWindow, KontWysw); ... END; {Reakcja na uzyskanie dostępu do klawiatury} PROCEDURE TOknoDok.WMSetFocus; BEGIN CreateCaret(HWindow, 0, SzerZn, WysZn); SetCaretPos(0, 0); ShowCaret(HWindow); END; {Reakcja na utracenie dostępu do klawiatury} PROCEDURE TOknoDok.WMKillFocus; BEGIN HideCaret(HWindow); DestroyCaret; END; Rys. 7.2. W kolejnej wersji programu "MDI" okna dokumentów zostały wzbogacone o kursor (wskaźnik klawiatury). 7.1.3. Komunikaty dotyczące znaków. Wprawdzie wszystkie znaki wprowadzane za pomocą, klawiatury można by wczytywać w funkcjach wywoływanych po uzyskaniu komunikatów "wm_KeyDown", ewentualnie "wm_KeyUp", ale postępowanie takie byłoby dość uciążliwe. Trzeba bowiem pamiętać o tym, że istnieje wiele różnych sposobów ułożenia znaków na klawiaturach stosowanych w poszczególnych krajach - naciśnięcie tego samego klawisza może w jednym systemie oznaczać na przykład wprowadzenie litery "Y", w innym zaś litery "Z". Poza tym wiele znaków wprowadanych jest przez naciśnięcie co najmniej dwóch klawiszy - duża litera "A", to "SHIFT+A", "ą "- "ALTPR+A", zaś "Ą" - zestawienie aż trzech klawiszy: "ALTPR, SHIFT i A". W związku z tym, aby ułatwić wczytywanie znaków alfanumerycznych, system Windows generuje oprócz omówionych wcześniej komunikatów dotyczących klawiszy komunikat "wm_Char" (a także mające mniejsze znaczenie komunikaty "wm_SysChar", "wm_DeadChar wm_SysDeadChar"), który informuje o wczytaniu znaku. Tak więc wprowadzenie przez użytkownika na przykład dużej litery "A" powoduje wygenerowanie sekwencji następujących komunikatów: "wm_KeyDown" ("SHIFT"), "wm_KeyDown" ("A"), "wm_Char" ("A"), "wm_KeyUp" ("A"),"wm_KeyUp" ("SHIFT"). W polu "WParam" struktury "TMessage", przekazywanej funkcji odbierającej komunikat "wm_Char", podawany jest kod "ASCII" wprowadzonego znaku. Ponieważ kod ten uwzględnia już stan klawiszy "SHIFT", "CAPSLOCK", "NUMLOCK" i innych, nie ma potrzeby sprawdzania ich za pomocą funkcji "GetKeyState". W niektórych, rzadkich, sytuacjach trzeba uwzględnić rodzaj stosowanego zestawu znaków. W systemie Windows stosowane są bowiem dwa zestawy: jeden z nich został zapożyczony z systemu DOS i nosi nazwę "OEM", drugi zaś stanowi standard Windows i określany jest skrótem "ANSI". Oba te zestawy są identyczne w zakresie od 32 do 127, różnią się natomiast co do znaczenia kodów o wartościach większych niż 127. W zestawie "ANSI" brakuje na przykład znaków grafiki blokowej, stosowanych powszechnie w programach ziałających w DOS-ie do rysowania różnego rodzaju ramek. Trzeba też pamiętać o tym, że istnieje wiele wariantów obu zestawów znaków, umożliwiających wyświetlanie znaków narodowych, takich jak polskie "ą", "ć" czy "ż". Znaki te są umieszczone w górnej połowie kodów, przy czym poza środowiskiem Windows nie doszło jeszcze do ustalenia jednego powszechnie obowiązującego standardu. Wystarczy w tym miejscu wspomnieć o kilku sposobach kodowania polskich liter, z których najważniejszymi są: stosowany przez Windows, "Latin 2" oraz "Mazovia". Wspomniane zagadnienianabierają jednak znaczenia dopiero wtedy, gdy tekst zapisany w aplikacji Windows ma być stosowany w środowisku DOS-a. Jeśli chodzi o pole "LParam" struktury "TMessage" towarzyszącej przesłaniu komunikatu "wm_Char", to jest ono równe polu "LParam" opisującemu ostatni komunikat dotyczący klawiszy (zazwyczaj "wm_KeyDown"), który spowodował wygenerowanie danego znaku. Jedyną istotną informacją zapisaną w tym polu jest, tak jak w przypadku komunikatów "wm_KeyDown" i "wm_KeyUp", liczba powtórzeń (liczba wprowadzonych identycznych znaków), zapisana w mniej znaczącym 16-bitowym słowie. Przekazane informacje są już w zasadzie wystarczające do samodzielnego utworzenia prostego edytora tekstu. Wprawdzie w prostszych zastosowaniach można skorzystać z wielowierszowego pola edycji, występującego niekiedy także w oknach dialogowych (lub standardowego okna aplikacji pakietu "TPW", opisanego w rozdziale 9), ale nie jest ono w stanie zastąpić bardziej skomplikowanych zastosowań, związanych z realizowaniem nietypowych zadań. Nasz edytor pozwala na wprowadzenie 25 wierszy zawierających maksymalnie po 80 znaków i reaguje na naciśnięcie następujących klawiszy sterujących: klawisze ze strzałkami (zmiana położenia kursora o jeden znak lub jeden wiersz - o ile jest to możliwe), "HOME" i "END" - przeniesienie kursora na początek lub na koniec bieżącego wiersza, "PGUP" i "PGDN" - przeniesienie kursora do pierwszego lub do ostatniego wiersza (bez zmiany bieżącej kolumny). Ponadto możliwe jest skorzystanie z klawiszy "ENTER" oraz"BS" ("BACKSPACE"), których działanie jest zgodne z ogólnie obowiązującymi zasadami. Warto też zwrócić uwagę na fakt, że jeśli jest konieczne, zmianie położenia kursora towarzyszy odpowiednie przewinięcie tekstu, czyli przewinięcie go o taką najmniejszą odległość, która gwarantuje widoczność kursora (zmiana położenia suwaka w odpowiednim pasku przewijania). W aplikacji "MDI" konieczne jest dokonanie następujących zmian: - wprowadzenie w obiekcie "TOknoDok" zmiennych "PozXKurs" i "PozYKurs" umożliwiających pamiętanie położenia kursora (numer kolumny i wiersza); zmienne te powinny być zadeklarowane w definicji obiektu okna dokumentu, zerowane w jego konstruktorze oraz odpowiednio zmieniane każdorazowo po wczytaniu dowolnego znaku (komunikat "wm_Char") lub jednego z klawiszy sterujących położeniem kursora (komunikat "wm_KeyDown"); ponadto należy zmienić parametry funkcji "SetCaretPos" wywoływanej w metodzie "WMSetFocu", tak aby uwzględniały one bieżące położenie kursora; - utworzenie w obiekcie okna dokumentu bufora służącego do zapamiętywania znaków wprowadzanych za pomocą klawiatury; bufor ten powinien być wypełniony spacjami w konstruktorze tego obiektu oraz uaktualniany każdorazowo po wprowadzeniu nowego znaku lub naciśnięciu klawisza "BS"; - zdefiniowanie metody "WMChar" obiektu "TOknoDok", reagującej na wprowadzanie znaków za pomocą klawiatury, czyli odbierającej komunikaty "wm_Char"; procedura ta powinna uaktualniać zawartość bufora "Bufor", wyświetlać jego zmienioną zawartość, zmieniać położenie kursora oraz w razie potrzeby przewijać zawartość okna; - zmodyfikowanie funkcji "WMKeyDown", odbierającej komunikat "wm_KeyDown", tak aby dokonywała ona zmiany położenia kursora i w razie potrzeby przewijała tekst w odpowiednim kierunku; - usunięcie zmiennej "Wysw", która określała w poprzednich wersjach programu moment rozpoczęcia wyświetlania figury złożonej z grupy prostokątów o wspólnym środku, a obecnie nie jest już potrzebna; - zmodyfikowanie funkcji "Paint", tak aby zamiast wyświetlania wspomnianych prostokątów prezentowała zawartość bufora "Bufor". W celu uproszczenia tekstu źródłowego aplikacji i zwiększenia jego czytelności wprowadzono dwie procedury lokalne obiektu "TOknoDok": - procedura "PrzewinOkno" ma za zadanie takie przewinięcie okna, aby był w niej widoczny kursor; - procedura "ZmienPozKurs" zmienia położenie kursora po uwzględnieniu bieżącego położenia suwaków obu pasków przewijania. Podczas analizowania programu warto zwrócić uwagę na sposób definiowania współrzędnych lokalnych przekazywanych w postaci parametrów funkcjom "TextOut" oraz "SetCaretPos" podczas stosowania pasków przewijania. Tylko w obrębie funkcji "Paint" współrzędne te określa się względem wirtualnego początku układu współrzędnych, to znaczy względem lewego górnego rogu obszaru zawierającego tekst, który może także znajdować się poza ekranem (jeśli obraz został przewinięty na prawo lub w dół). We wszystkich pozotałych przypadkach, to znaczy w funkcjach "WMSetFocus", "WMKeyDown" i "WMChar", współrzędne określa się względem lewego górnego rogu obszaru wyświetlanego aktualnie w oknie. Warto także zwrócić uwagę na to, że na początku metody "ZmienPozKurs" obiektu "TOknoDok" dokonywane jest sprawdzenie, czy wartość pola "Scroller" tego obiektu jest różna od zera. Jak wiadomo, pole "Scroller" przechowuje wskaźnik do obiektu "TScroller" reprezentującego paski przewijania okna i staje się równe zeru, gdy paski te ulegają likwidacji. Sytuacja taka ma miejsce między innymi po zamknięciu okna przez użytkownika, na przykład za pomocą kombinacji klawiszy "CTRL+F4" lub skorzystaniu z odpowieniego przycisku w pasku tytułu. Tymczasem już po zamknięciu okna system Windows wysyła komunikat "wm_SetFocus", powodując w naszym przypadku wywołanie metody "TOknoDok.WMSetFocus". Ponieważ metoda ta wywołuje z kolei procedurę "ZmienPozKurs", oczywista staje się przyczyna dokonywania wspomnianego na początku tego akapitu sprawdzenia. Gdybyśmy nie sprawdzili wartości pola "Scroller", spowodowalibyśmy wyświetlenie przez Windows komunikatu o wykonaniu przez aplikację "MDI" nieprawidłowej instrukcji i atomatyczne zakończenie jej działania. Jak więc mieliśmy okazję się przekonać, nieraz programowanie w Windows wcale nie jest takie łatwe... CONST MaxNrWrsz = 24; MaxNrKol = 79; TOknoDok = OBJECT(TWindow) SzerZn, WysZn, PozXKurs, PozYKurs: Integer; Bufor: ARRAY[0..MaxNrWrsz] OF ARRAY[0..MaxNrKol] OF Char; ... PROCEDURE WMChar(VAR Msg: TMessage); VIRTUAL wm_First+wm_Char; ... PRIVATE PROCEDURE PrzewinOkno; PROCEDURE ZmienPozKurs; END; CONSTRUCTOR TOknoDok.Init; VAR ... X, Y: Integer; BEGIN ... PozXKurs := 0; PozYKurs := 0; FOR Y := 0 TO MaxNrWrsz DO FOR X := 0 TO MaxNrKol DO Bufor [Y] [X] := ' '; END; PROCEDURE TOknoDok.Paint; VAR Y: Integer; BEGIN SelectObject(PaintDC, GetStockObject(SYSTEM_FIXED_FONT)); FOR Y := 0 TO MaxNrWrsz DO TextOut(PaintDC, 0, Y*WysZn, @Bufor[Y], MaxNrKol+1); END; PROCEDURE TOknoDok.WMSize; VAR SzerObr, WysObr: Integer; ... BEGIN ... SzerObr := SzerZn * (MaxNrKol + 1); WysObr := WysZn * (MaxNrWrsz + 1); ... END; PROCEDURE TOknoDok.WMSetFocus; BEGIN ... ZmienPozKurs; ... END; PROCEDURE TOknoDok.WMKeyDown; BEGIN IF (GetKeyState(vk_Shift) >= 0) AND (GetKeyState(vk_Control) >= 0) THEN BEGIN Case Msg.WParam OF vk_Left: IF PozXKurs > 0 THEN PozXKurs := PozXKurs - 1; vk_Right: IF PozXKurs < MaxNrKol THEN PozXKurs := PozXkurs + 1; vk_Up: IF PozYKurs > 0 THEN PozYKurs := PozYKurs - 1; vk_Down: IF PozYKurs < MaxNrWrsz THEN PozYKurs := PozYKurs + 1; vk_Home: PozXKurs := 0; vk_End: PozXKurs := MaxNrKol; vk_Prior: PozYKurs := 0; {PgUp} vk_Next: PozYKurs := MaxNrWrsz; {PgDn} END; PrzewinOkno; ZmienPozKurs END; END; {Reakcja na wprowadzenie znaku za pomocą klawiatury} PROCEDURE TOknoDok.WMChar; CONST BS = 8; Tab = 9; CR = 13; Space = 32; VAR KontWysw: HDC; NrZn, X: Integer; BEGIN FOR NrZn := 1 TO Msg.LParamLo DO CASE Msg>WParam OF BS: IF PozXKurs > 0 THEN BEGIN PozXKurs := PozXKurs - 1; FOR X := PozXKurs TO MaxNrKol - 1 DO Bufor[PozYKurs] [X] := Bufor[PozYKurs] [X+1]; Bufor[PozYKurs] [MaxNrKol] := ' '; HideCaret(HWindow); KontWysw := GetDC(HWindow); SelectObject(KontWysw, GetStockObject(SYSTEM_FIXED_FONT)); WITH Scroller^ DO TextOut(KontWysw, PozXKurs*SzerZn - XPos*XUnit, PozYKurs*WysZn - YPos*YUnit, @Bufor[PozYKurs] [PozXKurs], MaxNrKol-PozXKurs+1); ShowCaret(HWindow); ReleaseDC(HWindow, KontWysw); END; CR: BEGIN PozXKurs := 0; PozYKurs := PozYKurs + 1; IF PozYKurs = MaxNrWrsz + 1 THEN PozYKurs := 0; END; ELSE IF Msg.WParam >= Space THEN BEGIN Bufor[PozYKurs] [PozXKurs] := Char(Msg.WParam); HideCaret(HWindow); KontWysw := GetDC(HWindow); SelectObject(KontWysw, GetStockObject(SYSTEM_FIXED_FONT)); WITH Scroller^ DO TextOut(KontWysw, PozXKurs*SzerZn - XPos*XUnit, PozYKurs*WysZn - YPos*YUnit, @Bufor[PozYKurs] [PozXKurs], 1); ShowCaret(HWindow); ReleaseDC(HWindow, KontWysw); PozXKurs := PozXKurs + 1; IF PozXKurs = MaxNrKol + 1 THEN BEGIN PozXKurs := 0; PozYKurs := PozYKurs + 1; IF PozYKurs = MaxNrWrsz + 1 THEN PozYKurs := 0; END; END; END; PrzewinOkno; ZmienPozKurs; END; {Przewinięcie okna, tak aby widoczny był kursor} PROCEDURE TOknoDok.PrzewinOkno; VAR ObszRob: TRect; PrzesX, PrzesY: Integer; BEGIN GetClientRect(HWindow, ObszRob); PrzesX := 0; PrzesY := 0; Rys. 7.3. Prosty edytor tekstu zawarty w aplikacji "MDI" pozwala na wprowadzenie tekstu zawierającego maksymalnie 25 wierszy po 80 znaków. WITH Scroller^ DO BEGIN WHILE PozXKurs*SzerZn - (XPos+PrzesX)*XUnit < 0 DO PrzesX := PrzesX - 1; WHILE (PosXKurs+1)*SzerZn - (XPos+PrzesX)*XUnit > ObszRob.Right DO PrzesX := PrzesX + 1; WHILE PosYKurs*WysZn - (YPos+PrzesY)*YUnit < 0 DO PrzesY := PrzesY - 1; WHILE (PosYKurs+1)*WysZn - (YPos+PrzesY)*YUnit > ObszRob.Bottom DO PrzesY := PrzesY + 1; ScrollBy(PrzesX, PrzesY); END; END; {Zmiana położenia kursora} PROCEDURE TOknoDok.ZmienPozKurs; BEGIN IF Scroller <> 0 THEN WITH Scroller^ DO SetCaretPos(PozXKurs*SzerZn - XPos*XUnit, PozYKurs*WysZn - YPos*YUnit); END; Rys. 7.4. Zmiana położenia kursora pociąga za sobą odpowiednie przewinięcie obrazu. 7.2. Mysz. 7.2.1. Wyświetlanie wskaźnika myszy. Wprawdzie teoretycznie mysz należy do tak zwanych urządzeń opcjonalnych Windows, co oznacza, że nie musi ona być podłączona do komputera, niemniej jednak dzisiaj już nikt chyba sobie nie wyobraża obsługi aplikacji Windows tylko za pomocą klawiatury. O obecności myszy w systemie świadczy jej wskaźnik, zwany także niekiedy kursorem (zgodnie z pierwowzorem angielskim, "cursor"). Wskaźnik myszy jest prawie cały czas widoczny na ekranie i przyjmuje różne postaci, w zależności od miejsca położenia, czyli nnymi słowy, od funkcji, której wykonanie w danej chwili umożliwia. Jeśli znajduje się na pasku tytułu, pasku menu, pasku przewijania lub w obszarze roboczym okna, ma standardowo kształt ukośnej strzałki skierowanej w lewo i ku górze; umieszczenie wskaźnika na ramce okna sprawia, że przekształca się on w strzałkę o dwóch grotach, wskazujących na możliwe kierunki zmiany rozmiarów okna. Każda aplikacja Windows ma możliwość zmiany standardowych ustawień i dowolnego zdefiniowania kształtu wskaźnika myszy. Ponieważ jednak kształt ten musi być w razie potrzeby natychmiast modyfikowany, potrzebny jest komunikat, który by informował aplikację o każdej zmianie położenia wskaźnika. Komunikat taki nosi nazwę "wm_SetCursor". Jest on standardowo wysyłany do okna aktywnego, chyba że któreś z okien systemu Windows przejęło całkowitą obsługę myszy za pomocą funkcji "SetCapture" (zagadnieniem tym jako mniej ważnym, nie będziemy się w tej książce bliżej zajmować). Najważniejszą informację przekazywaną przez komunikat "wm_SetCursor" stanowi oczywiście określenie bieżącego miejsca zajmowanego przez wskaźnik myszy. Miejsce to nie jest jednak opisywane za pomocą współrzędnych, których interpretacja byłaby w programie dość uciążliwa, lecz przez opisanie fragmentu okna, w którym aktualnie wskaźnik się mieści. W tym celu w mniej znaczącym słowie pola "LParam" struktury "Msg" ("LParamLo") komunikat "wm_SetCursor" przekazuje jedną ze stałych zdefiniowanych w module "WnTypes" i rozpoczynających się od liter "ht". I tak, na przykład, przekazanie wartości "htClient" oznacza, że wskaźnik myszy został umieszczony w obszarze roboczym okna, "htCaption" - na pasku menu, "htTopRight" - w prawym dolnym rogu okna, "htVScroll" - na pasku przewijania pionowego, "htZoom" - na przycisku maksymalizacji itp. Zazwyczaj aplikacja zmienia postać wskaźnika myszy tylko wtedy, gdy mieści się on w obrębie obszaru roboczego okna. We wszystkich pozostałych przypadkach zdefiniowanie postaci wskaźnika najlepiej jest pozostawić samemu systemowi Windows, wywołując metodę "DefWndProc" obiektu "TWindow", która z kolei wywołuje funkcję "API" "DefWindowProc". Funkcja ta przetwarza w sposób standardowy wszystkie te komunikaty, które nie zostały przejęte przez aplikację i ma tylko jeden parametr - strukturę "Msg" typu "TMssage". Jeżeli jednak zachodzi potrzeba zmiany kształtu kursora, należy skorzystać z innej funkcji "API", "SetCursor". Oczekuje ona podania jej w postaci parametru uchwytu zasobu określającego graficzną postać nowego kursora, przy czym może to być albo jeden z predefiniowanych kursorów systemu Windows, albo zdefiniowany samodzielnie w pliku z zasobami, podobnie jak ikona aplikacji, okna dialogowe czy system menu. Oczywiście pierwszy z wymienionych sposobów jest prostszy w realizacji i w większości przypadkó okazuje się wystarczający. Uchwyt kursora uzyskuje się za pomocą funkcji "API" "LoadCursor". Wartości, które należy przekazać tej funkcji w postaci parametrów, zależą od sposobu definiowania nowego kursora. Jeśli jest to jeden z predefiniowanych kursorów Windows, pierwszy parametr powinien przyjąć wartość 0, drugi zaś określać rodzaj kursora za pomocą jednej ze stałych zdefiniowanych w module "WinTypes" i rozpoczynających się od znaków "idc_" . Do stałych tych należą między innymi: "idc_Arrow" (standardowa strzałka), "idc_Cros" (krzyżyk wykorzystywany przez wiele programów graficznych podczas rysowania figur geometrycznych), "idc_IBeam" (wskaźnik w postaci litery "I", stosowany w edytorach tekstu) oraz "idc_Wait" (klepsydra, sygnalizująca wykonywanie przez program pewnej długotrwałej czynności). Aby w oknach dokumentów naszej aplikacji "MDI" był wyświetlany wskaźnik myszy w kształcie litery "I", wystarczy zdefiniować procedurę "WMSetCursor", przetwarzającą komunikat "wm_SetCursor". Procedura ta powinna przydzielać wskaźnikowi predefiniowaną postać opisaną stałą "idc_IBeam" zawsze wtedy, gdy znajduje się on w obrębie obszaru roboczego, zaś we wszystkich pozostałych przypadkach - wywoływać funkcję "DefWndProc". Rys. 7.5. Umieszczenie wskaźnika myszy w obszarze roboczym okna powoduje zmianę jego kształtu. TOkno = OBJECT(TWindow) ... PROCEDURE WMSetCursor(VAR Msg: TMessage); VIRTUAL wm_First + wm_SetCursor; ... END; {Określanie kształtu wskaźnika myszy} PROCEDURE TOknoDok.WMSetCursor; BEGIN IF Msg>LParamLo = htClient THEN SetCursor(LoadCursor(0, idc_IBeam)) ELSE DefWndProc(Msg); END; 7.2.2. Komunikaty dotyczące myszy. Nietrudno się domyślić, że o wszelkiego rodzaju czynnościach wykonywanych za pomocą myszy system Windows informuje aplikacje za pomocą odpowiednich komunikatów. Do najważniejszych z nich należą: - "wm_MouseMove" - nastąpiła zmiana położenia wskaźnika myszy; - "wm_LButtonDown", "wm_RButtonDown", "wm_MButtonDown" - został naciśnięty lewy, prawy lub środkowy przycisk myszy; - "wm_LButtonUp", "wm_RButtonUp", "wm_MButtonUp" - został zwolniony lewy, prawy lub środkowy przycisk myszy; - "wm_LButtonDblClk", "wm_RButtonDblClk", "wm_MButtonDblClk" - w operacji podwójnego kliknięcia został naciśnięty po raz drugi lewy, prawy lub środkowy przycisk myszy (na przykład podczas podwójnego kliknięcia za pomocą lewego przycisku wysyłane są kolejno następujące komunikaty: "wm_LButtonDown", "wm_LButtonUp" i "wm_LButtonDblClk"). We wszystkich opisanych komunikatach pole "WParam" struktury "TMessage" zawiera informację o stanie klawiatury i myszy w momencie zajścia danego zdarzenia. Informacja ta jest przekazywana w postaci sumy logicznej stałych, zdefiniowanych w module "WinTypes" i rozpoczynających się od znaków "mk_". Mają one następującą postać: "mk_Control", "mk_Shift" (naciśnięty był klawisz "CTRL" lub "SHIFT"), "mk_LButton", "mk_RButton", "mk_MButton" (naciśnięty był lewy, prawy lub środkowy przycisk myszy). Jeśli chodzi o pole "LParam", to zawiera ono współrzędne wskaźnika myszy w momencie wygenerowania komunikatu, standardowo wyrażone w pikselach względem lewego górnego rogu obszaru roboczego okna. Mniej znaczące słowo określa współrzędną "x", bardziej znaczące - współrzędną "y". Warto w tym miejscu zwrócić uwagę na fakt, że jeżeli aplikacja Windows nie nadąża z obsługą każdego pojedynczego komunikatu "wm_MouseMove, są one łączone w jeden, informujący o końcowym położeniu wskaźnika myszy. Oprócz wymienionych wyżej komunikatów istnieją, także komunikaty informujące o zdarzeniach wywołanych przez mysz wprawdzie w obrębie okna, ale poza jego obszarem roboczym. Komunikaty te odpowiadają komunikatom systemowym klawiatury i rzadko bywają obsługiwane przez aplikacje Windows. Ich nazwy rozpoczynają się od znaków "wm_nc" (od angielskiego wyrażenia "window message - non-client area", co można przetłumaczyć jako "komunikat okna - miejsce poza obszarem roboczym"). Pole "WParam" określa w tym przpadku miejsce wystąpienia zdarzenia, pole "LParam" położenie wskaźnika, ale wyrażone we współrzędnych ekranowych. Obecnie wykorzystamy powyższe informacje w celu takiego zmodyfikowania aplikacji "MDI", aby: - przy naciśniętym lewym przycisku myszy można było zaznaczyć fragment okna w którym mają zostać usunięte wszystkie znaki (czyli który ma zostać wypełniony spacjami), ale tylko pod warunkiem, że przycisk myszy został naciśnięty w obszarze roboczym okna; - kliknięcie za pomocą prawego przycisku myszy powodowało wyświetlenie napisu "Programowanie w Windows"; napis ten powinien rozpoczynać się w miejscu wskazywanym przez wskaźnik myszy lub w miejscu odpowiednio przesuniętym w lewo, jeśli cały napis nie zmieściłby się w redagowanym tekście (maksymalna długość wiersza wynosi 80 znaków). W tym celu konieczne jest dokonanie w programie następujących zmian: - wprowadzenie w obiekcie "TOknoDok", reprezentującym okno dokumentu, pól "Obszar" i "Blok" typu "TRect", tak aby możliwe było pamiętanie współrzędnych zaznaczonego fragmentu tekstu; w zmiennej "Obszar" współrzędne wyrażane są w pikselach względem lewego górnego rogu obszaru roboczego okna dokumentu, podczas gdy w zmiennej "TRect" określają one numery pierwszej i ostatniej kolumny oraz pierwszego i ostatniego wiersza tekstu, liczone względem lewego górnego rogu całkowitego obszaru okna (włączając w o obszar w danej chwili niewidoczny na skutek przesunięcia suwaków w paskach przewijania); z obu zmiennych korzystają omówione niżej procedury "WMLButtonDown", "WMLButtonUp" oraz "WMMouseMove" obiektu "TOknoDok"; - wprowadzenie pola "NacisnLKlawMyszy" obiektu "TOkno", umożliwiającego zapamiętanie, czy lewy przycisk myszy został naciśnięty w obszarze roboczym okna dokumentu; polu temu powinna być przypisywana wartość "False" w konstruktorze okna oraz każdorazowo po zwolnieniu lewego przycisku myszy w obszarze roboczym okna (metoda "WMLButtonDown"), natomiast wartość "True" - po naciśnięciu tego przycisku w obszarze roboczym ("WMLButtonUp"); instrukcje powodujące zmianę zaznaczanego obszaru i zakończenie jego aznaczania należy wykonywać w metodach "WMMouseMove" i "WMLButtonUp" tylko wtedy, gdy wartość zmiennej "NacisnLKlawMyszy" jest równa "True"; jeśli natomiast wartość tego pola wynosi "True" po uzyskaniu komunikatu "wm_LButtonDown", oznacza to, że przycisk myszy został zwolniony poza obszarem roboczym, w związku z czym trzeba skasować krawędzie poprzednio zaznaczonego bloku; - zdefiniowanie metody "WMLButtonDown" obiektu "TOknoDok", która by zapamiętywała współrzędne wskaźnika myszy, przypisując je lewemu górnemu rogowi zaznaczanego obszaru, oraz rozpoczynała wyświetlanie prostokąta zaznaczającego; - zdefiniowanie metody "WMMouseMove", która by zapamiętywała współrzędne wskaźnika myszy, traktując je jako bieżące współrzędne prawego dolnego rogu zaznaczanego obszaru, a także modyfikowała rozmiar wyświetlanego prostokąta zaznaczającego; - wprowadzenie metody "WMLButtonUp", usuwającej z ekranu prostokąt zaznaczający, zamieniającej w buforze tekstowym wszystkie znaki zawarte w zaznaczonym obszarze na spacje oraz wyświetlającej te spacje w odpowiednim miejscu ekranu; - utworzenie metody "WMRButtonUp", obliczającej miejsce położenia tekstu "Programowanie w Windows" i wyświetlającej go. Podczas analizowania tekstu źródłowego programu warto zwrócić uwagę na jego następujące elementy: - ponieważ współrzędne wskaźnika myszy mają charakter ciągły, to znaczy mogą zmieniać się co wartość jednostkową, natomiast zaznaczony blok musi obejmować pełne znaki, w metodach "WMLButtonDown", "WMLButtonUp" i "WMRButtonUp" konieczne jest odpowiednie zmodyfikowanie współrzędnych przekazanych w polu "LParam" struktury "TMessage" (wykorzystuje się do tego celu operatory arytmetyczne "MOD" i "DIV"); - prostokąt zaznaczający rysowany jest za pomocą funkcji "API" "DrawFocusRect", która rysuje prostokąt wykonując operację bitową "XOR" dla każdego z jego pikseli; dzięki temu powtórne narysowanie w tym samym miejscu prostokąta o niezmienionych wymiarach powoduje skasowanie prostokąta poprzedniego; - podczas wyświetlania tekstu w metodzie "WMLButtonUp" trzeba podać współrzędne wyrażone względem lewego górnego rogu wyświetlanego obszaru roboczego okna, a nie względem lewego górnego rogu pełnego wirtualnego obszaru okna; postępuje się więc tu tak samo, jak na przykład podczas obsługi zdarzenia polegającego na naciśnięciu klawisza, inaczej jednak niż w metodzie "Paint". TOkno = OBJECT(TWindow) ... Obszar, Blok: TRect; NacisLKlawMyszy: Boolean; ... PROCEDURE WMLButtonDown(VAR Msg: TMessage); VIRTUAL wm_First + wm_LButtonDown; PROCEDURE WMLButtonUp(VAR Msg: TMessage); VIRTUAL wm_First + wm_LButtonUp; PROCEDURE WMMouseMove(VAR Msg: TMessage); VIRTUAL wm_First + wm_MouseMove; PROCEDURE WMRButtonUp(VAR Msg: TMessage); VIRTUAL wm_First + wm_RButtonUp; ... END; CONSTRUCTOR TOknoDok.Init; VAR ... BEGIN ... NacisLKlawMyszy := False; END; {Reakcja na naciśnięcie lewego przycisku myszy} PROCEDURE TOknoDok.WMLButtonDown; VAR KontWysw := GetDC(HWindow); IF NacisLKlawMyszy THEN DrawFocusRect(KontWysw, Obszar); WITH Msg, Scroller^ DO BEGIN WHILE (LParamLo + XPos*XUnit) MOD SzerZn <> 0 DO LParamLo := - 1; WHILE (LParamHi + YPos*YUnit) MOD WysZn <> 0 DO LParamHi := - 1; SetRect(Obszar, LParamLo, LParamHi, LParamLo, LParamHi); Blok.Left := (LParamLo + XPos*XUnit) DIV SzerZn; Blok.Right := Blok.Left; Blok.Top := (LParamHi + YPos*YUnit DIV WysZn; Blok.Bottom := Blok.Top; END; DrawFocusRect(KontWysw, Obszar); ReleaseDC(HWindow, KontWysw); NacisLKlawMyszy := True; END; {Reakcja na zwolnienie lewego przycisku myszy} PROCEDURE TOknoDok.WMLButtonUp VAR KontWysw: HDC; X, Y: Integer; BEGIN IF NacisLKlawMyszy THEN BEGIN KontWysw := GetDC(HWindow); DrawFocusRect(KontWysw, Obszar); IF (Blok.Left = Blok.Right) AND (Blok.Top = Blok.Bottom) THEN BEGIN PozXKurs := Blok.Left; PozYKurs := Blok.Bottom; ZmienPozKurs; END ELSE BEGIN HideCaret(HWindow); SelectObject(KontWysw, GetStockObject(SYSTEM_FIXED_FONT)); WITH Blok DO BEGIN FOR Y := Top TO Bottom DO BEGIN FOR X := Left TO Right DO Bufor [Y] [X] := ' '; WITH Scroller^ DO TextOut(KontWysw, Obszar.Left, Y*WysZn - YPos*YUnit, @Bufor [Y] [Left], Right-Left+1); END; END; ShowCaret(HWindow); END; ReleaseDC(HWindow, KontWysw); NacisLKlawMyszy := False; END; END; {Reakcja na przesunięcie wskaźnika myszy} PROCEDURE TOknoDok.WMMouseMove; VAR KontWysw: HDC; BEGIN IF NacisnLKlawMyszy AND (Msg.WParam AND mk_LButton <> 0) THEN BEGIN KontWysw := GetDC(HWindow); DrawFocusRect(KontWysw, Obszar); WITH Msg, Scroller^ DO BEGIN WHILE (LParamLo + XPos*XUnit) MOD SzerZn <> 0 DO LParamLo := LParamLo + 1; WHILE (LParamHi + YPos*YUnit) MOD WysZn <> 0 DO LParamHi := LParamHi + 1; Obszar.Right := LParamLo; Obszar.Bottom := LParamHi; Blok.Bottom := (LParamHi + XPos*XUnit) DIV SzerZn - 1; Blok.Bottom := (LParamHi + YPos*YUnit) DIV WysZn -1; END; DrawFocusRect(KontWysw, Obszar); ReleaseDC(HWindow, KontWysw); END; END; {Reakcja na zwolnienie prawego przycisku myszy} PROCEDURE TOknoDok.WMRButtonUp; VAR KontWysw: HDC; Napis: PChar; X, Y: Integer; BEGIN KontWysw := GetDC(HWindow); HideCaret(HWindow); SelectObject(KontWysw, GetStockObject(SYSTEM_FIXED_FONT); Napis := 'Programowanie w Windows'; WITH Scroller^ DO BEGIN X := (Msg.LParamLo + XPos*XUnit) DIV SzerZn; Y := (Msg.LParamHi + YPos*YUnit) DIV WysZn; END; IF X + StrLen(Napis) > MaxNrKol + 1 THEN X := MaxNrKol - StrLen(Napis) + 1; StrCopy(Bufor [Y]+X, Napis, StrLen(Napis)-1); Bufor [Y] [X+StrLen(Napis)-1] := ' '; WITH Scroller^ DO TextOut(KontWysw, X*SzerZn - XPos*XUnit, Y*WysZn - YPos*YUnit, Napis, StrLen(Napis)); ShowCaret(HWindow); ReleaseDC(HWindow, KontWysw); END; Rys. 7.6. Przy naciśniętym lewym przycisku myszy można zaznaczyć dowolny obszar okna. Rys. 7.7. Po zwolnieniu lewego przycisku myszy wszystkie znaki znajdujące się w zaznaczonym obszarze zostają zastąpione spacjami. Rys. 7.8. Kliknięcie w dowolnym miejscu obszaru roboczego okna dokumentu powoduje umieszczenie w tym miejscu napisu "Programowanie w Windows". 7.3. Zegar. Kolejnym, oprócz klawiatury i myszy, źródłem danych wejściowych jest w aplikacjach Windows zegar. Mimo iż w pierwszej chwili może się on wydawać mało użytecznym urządzeniem, to jednak wiele aplikacji nie mogło by wręcz bez niego istnieć. Do typowych przykładów zastosowań zegara należą między innymi: wyświetlanie bieżącego czasu, systematyczne uaktualnianie wyświetlanych informacji o programie lub systemie, na przykład o ilości wolnych zasobów, dzielenie długotrwałych operacji na mniejsze fragmenty wcelu umożliwienia przekazania sterowania innym działającym aplikacjom Windows, automatyczne zachowywanie redagowanego dokumentu co pewien z góry ustalony czas, tworzenie animacji w grach komputerowych czy wreszcie odczytywanie danych z buforów wejściowych portów szeregowych i równoległych. Jak więc widać, zegar pełni w systemie Windows bardzo ważną rolę. Działa on jednak na nieco innych zasadach niż przerwanie sprzętowe 08h, wykorzystywane w programach DOS-a. Zegar Windows nie generuje bowiem przerwań, lecz jedynie zgłasza komunikat o nazwie "wm_Timer", działając na takich samych zasadach, jak klawiatura i mysz. Wynika stąd wniosek, że aplikacja Windows nie może polegać na zegarze jako na urządzeniu umożliwiającym wykonywanie operacji dokładnie z pewną ustaloną częstotliwością Jeżeli jakichś powodów zostaje na dłużej zablokowany w systemie odbiór komunikatów, może zakłócić to w istotny sposób przebieg jego działania (każdy kolejny komunikat "wm_Timer" powoduje usunięcie poprzedniego, w związku z czym aplikacja otrzymuje tylko jeden komunikat końcowy). Aby się o tym przekonać, wystarczy uruchomić program "Zegar" (Windows 3.1 ) lub ikonę "DatalGodzina" (Windows 95), a następnie umieścić wskaźnik myszy na pasku tytułowym okna oraz nacisnąć lewy przycisk myszy. Przez cały czas jegoprzytrzymywania wyświetlana godzina nie będzie ulegać zmianie - jej aktualizacja nastąpi dopiero po zwolnieniu przycisku. Aby aplikacja mogła otrzymywać komunikat "wm_Timer", wcześniej trzeba utworzyć (zdefiniować) zegar przez wywołanie funkcji "API" "SetTimer". Miejsce wywołania tej funkcji jest wprawdzie dowolne, ale najlepiej, jeśli następuje bezpośrednio przed rozpoczęciem korzystania z zegara. Jeśli urządzenie to ma towarzyszyć oknu przez cały czas jego istnienia, najodpowiedniejszym miejscem wykonania funkcji "SetTimer" wydaje się metoda "SetupWindow", stosowana do ustawienia parametrów nowo tworzonego okna. Funkcja "SetTimer" ma cztery parametry. Pierwszy z nich musi być równy uchwytowi okna, z którym zegar ma zostać powiązany, ewentualnie zeru, jeśli żadne takie okno nie istnieje. Drugi parametr może być dowolną liczbą całkowitą dodatnią; liczba ta stanowi identyfikator zegara i umożliwia jego późniejsze rozpoznanie w przypadku, gdy zdefiniowano większą liczbę zegarów. Najważniejszy jest parametr trzeci, określający długość taktu zegara, czyli odcinek czasu, co jaki dane okno uzyskiwać będzie komunika "wm_Timer". Czas ten wyraża się w milisekundach, ale ponieważ system Windows korzysta ze standardowego zegara DOS-a, którego częstotliwość taktowania wynosi 18,2 razy na sekundę, podana wartość zaokrąglana jest zawsze do wielokrotności liczby 54,945 ms (= 1 sek / 18,2). Jeśli chodzi o czwarty parametr funkcji "SetTimer", to jego wartość należy w środowisku "TPW" ustawić zawsze na zero. Jak więc widać, zegar ma w środowisku Windows charakter wirtualny i stanowi pewnego rodzaju zasób. Ponieważ jednak liczba zasobów jest ograniczona, może się zdarzyć, że utworzenie zegara nie jest możliwe. W przypadku wystąpienia takiej sytuacji funkcja "SetTimer" zwraca wartość równą zeru. Można wówczas albo wyświetlić odpowiedni komunikat i zakończyć wykonywanie aplikacji lub jednego z jej modułów, albo czekać na zwolnienie zegara w jednej z innych aplikacji (lub okien). Aby zapobiec powstawaniu opisanej sytuacji, natychmiast po zakończeniu korzystania z zegara należy go z systemu usunąć. Robi się to za pomocą funkcji "API" "KillTimer", która ma dwa parametry: uchwyt okna oraz identyfikator zegara; wartości obu tych parametrów muszą być takie same, jakie zostały podane w wywołaniu funkcji "SetTimer". Jeżeli zegar potrzebny jest oknu przez cały czas jego istnienia, najlepszym miejscem pozbycia się go jest metoda obiektu "TWindowsObject" (przodka obiektów reprezentująych okna, okna dialogowe i ich elementy sterujace) o nazwie "WMDestroy". Metoda ta przetwarza komunikat "wm_Destroy", informujący okno o tym, że za chwilę zostanie ono usunięte. Jeżeli zegar ma być wykorzystywany we wszystkich oknach podrzędnych okna głównego (na przykład w aplikacjach "MDI"), wystarczy utworzyć go jednorazowo, w oknie głównym. Następnie procedura okna głównego przetwarzająca komunikat "wm_Timer" może informować o tym fakcie wszystkie okna podrzędne. Do tego celu najlepiej wykorzystać metodę "ForEach" obiektu "TWindowsObject", która dla każdego okna podrzędnego wywołuje pewną określoną funkcję, przekazując jej w postaci parametru wskaźnik do kolejnego okna.Funkcję, która ma być wywoływana, określa się przez podanie wskaźnika do niej jako parametru funkcji "ForEach". Stosowanie opisanego mechanizmu ilustruje przykład wzajemnego współdziałania poszczególnych okien należących do tej samej aplikacji. Obecnie możemy wykorzystać podane informacje w celu ulepszenia utworzonej przez nas aplikacji "MDI". Dokonana zmiana spowoduje, że w prawym dolnym rogu okna każdego dokumentu wyświetlany będzie prostokąt z bieżącą godziną W tym celu konieczne jest dokonanie w tekście programu następujących modyfikacji: - uzupełnienie metody "SetupWindow" obiektu "TOknoMDI", reprezentującego okno główne aplikacji, o zdefiniowanie zegara za pomocą funkcji "SetTimer"; wartość trzeciego parametru powinna być równa 1000, tak aby komunikat "wm_Timer" generowany był co 1 sekundę (dokładniej: co 989 ms); - przykrycie metody "TOknoMDI.WMDestroy" w celu uzupełnienia jej o wywołanie funkcji "KillTimer", usuwającej zegar z systemu; - zadeklarowanie pola "IdZegara" obiektu "TOknoMDl"; pole to powinno być ustawiane w metodzie "TOknoMDI.SetupWindow" oraz przyjmować wartość różną od zera wtedy, gdy udało się utworzyć zegar, i wartość równą zeru w przypadku przeciwnym; dzięki takiemu rozwiązaniu metoda "TOknoDok.WMDestroy" uzyskuje informację o tym, czy ma wywołać funkcję "KillTimer"; - utworzenie metody "TOknoMDl.WMTimer", odbierającej komunikaty "wm_Timer" i wywołującej w następstwie każdego z nich funkcję "TOknoDok.Zegar" (opisaną niżej) kolejno dla wszystkich istniejących okien dokumentów; - zdefiniowanie metody "Zegar" obiektu "TOknoDok", reprezentującego okno dokumentu; metoda ta mogłaby ograniczać się do wywołania funkcji "API" "InvalidateRect", wyzwalającej wywołanie metody "Paint", ale spowodowałoby to unieważnienie całej powierzchni okna, a w konsekwencji - niepotrzebne wyświetlanie wszystkich danych co sekundę; dużo lepiej jest więc najpierw obliczyć współrzędne obszaru, w którym wyświetlany jest zegar, a dopiero potem wywołać funkcję "InvalidateRect", unieważniając tylko ten oszar; - uzupełnienie metody "TOknoDok.Paint" o wyświetlenie bieżącego czasu; do tego celu najlepiej jest wykorzystać procedurę standardową "GetTime", zdefiniowaną w module "WinDos" i zwracającą w postaci parametrów liczby określające bieżącą godzinę, minutę i sekundę; możliwość wywołania funkcji "GetTime" wymaga oczywiście odpowiedniego uzupełnienia klauzuli "USES". Warto zauważyć, że w naszej aplikacji "MDI" prostokącik z bieżącą godziną dostosowuje swoje położenie do zmiany wielkości okna (znajduje się zawsze w prawym dolnym rogu), ale nie do zmian położenia suwaków w paskach przewijania. Przyjęcie takiego rozwiązania wynika z faktu, że obsługa pasków przewijania dokonywana jest w większości przez procedury biblioteki "ObjectWindows". W związku z tym umieszczanie zegara zawsze w prawym dolnym rogu okna dokumentu wymagałoby konieczności dokonania dalszych zmia w tekście źródłowym programu, co niepotrzebnie by go skomplikowało, a nie wniosło przy tym istotnych korzyści z punktu widzenia dydaktycznego. USES WObjects, WinTypes, WinProcs, WinDos, Strings, MDI_M; TOknoMDI = OBJECT(TMDIWindow) ... IdZegara: Integer; ... PROCEDURE WMDestroy(VAR Msg: TMessage); VIRTUAL wm_First+wm_Destroy; PROCEDURE WMTimer(VAR Msg: TMessage); VIRTUAL wm_First+wm_Timer; ... END; TOknoDok = OBJECT(TWindow) ... PROCEDURE Zegar; ... END; PROCEDURE TOknoMDI.SetupWindow; VAR Result: Integer; BEGIN ... IdZegara := 1; Result := IdRetry; WHILE (SetTimer(HWindow, IdZegara, 1000, NIL) = 0) AND (Result = IdRetry) DO Result := MessageBox(HWindow, 'Nie można ustawić zegara!', 'MDI', mb_RetryCancel); IF Result = IdCancel THEN IdZegara := 0; END; {Reakcja na otrzymanie komunikatu wm_Destroy} PROCEDURE TOknoMDI.WMDestroy; BEGIN IF IdZegara <> 0 THEN KillTimer(HWindow, 0); TMDIWindow.WMDestroy(msg); END; {Reakcja okna głównego na otrzymanie komunikatu od zegara} PROCEDURE TOknoMDI.WMTimer; PROCEDURE ZegarDok(OknoDok: POknoDok); FAR; BEGIN OknoDok^.Zegar; END; BEGIN ForEach(@ZegarDok); END; PROCEDURE TOknoDo.Paint; VAR ... ObszarZeg: TRect; G, M, S, S100: Word; Godz, Min, Sek: STRING[2]; Napis: ARRAY[0..8] OF Char; BEGIN ... GetClientRect(HWindow, ObszarZeg); WITH ObszZeg DO BEGIN Left := Right - (8 * SzerZn + 4); Top := Bottom - (WysZn + 2); Rectangle(PaintDC, Left, Top, Right, Bottom); GetTime(G, M, S, S100); Str(G:2, Godz); Str(M:2, Min); Str(S:2, Sek); StrPCopy(Napis, Godz + ':' + Min + ':' + Sek); IF Napis[0] = ' ' THEN Napis[0] := '0'; IF Napis[3] = ' ' THEN Napis[3] := '0'; IF Napis[6] = ' ' THEN Napis[6] := '0'; TextOut(PaintDC, Lrft+2, Top+1, Napis, 8); END; END; {Reakcja okna dokumentu na otrzymanie komunikatu od zegara} PROCEDURE TOknoDok.Zegar; VAR ObszZeg: TRect; BEGIN GetClientRect(HWindow, ObszZeg); WITH ObszZeg, Scroller^ DO BEGIN Right := Right - Xpos * Xunit; Left := Right - (8 * SzeZn + 4); Bottom := Bottom - Ypos * Yunit; Top := Bottom - (WysZn + 2); END; InvalidateRect(HWindow, @ObszZeg, False); END; Rys. 7.9. W prawym dolnym rogu okna dokumentu wyświetlany jest prostokącik z bieżącą godziną. Rys. 7.10. Dzięki wzajemnemu współdziałaniu okien aplikacji możliwe jest wykorzystanie jednego zegara do wyświetlania godziny we wszystkich oknach dokumentów. Rozdział 8. Korzystanie z plików i drukowanie. Dzisiaj trudno sobie wyobrazić jakikolwiek program, niezależnie od tego, czy działa on w środowisku DOS-a, czy Windows, bez funkcji umożliwiających otwieranie i zapisywanie dokumentów oraz drukowanie ich zawartości - w aplikacjach Windows funkcje te występują, standardowo w menu "Plik". W obecnym, przedostatnim, rozdziale książki zastanowimy się, w jaki sposób są one w środowisku Windows realizowane. Na szczególną uwagę zasługują zasady rządzące wykonywaniem wydruków - nie są one bowiem często opisyane w podręcznikach, a należą do najtrudniejszych elementów programowania w Windows. 8.1. Wykonywanie operacji na plikach. Operacje na plikach wykonuje się z reguły za pomocą tych samych funkcji i procedur, z których korzysta się w programach działających w środowisku systemu DOS. W języku programowania "Pascal" należą do nich między innymi: "Assign" (skojarzenie zmiennej plikowej z plikiem), "Reset" (otwarcie istniejącego pliku do odczytu), "Rewrite" (ewentualne utworzenie pliku oraz otwarcie go do zapisu), "ReadLn" (odczytanie kolejnego wiersza z pliku tekstowego) i "WriteLn" (zapisanie kolejnego wiersza do pliku teksowego), "Eof" (sprawdzenie, czy osiągnięto koniec pliku). W czasie wykonywania procedur "Reset" i "Rewrite" powinna być koniecznie włączona dyrektywa "$I", aby za pomocą funkcji "IOResult" aplikacja mogła samodzielnie wykryć ewentualne błędy powstałe podczas tworzenia lub otwierania pliku i w odpowiedni sposób na nie zareagować. System Windows udostępnia tylko jedną funkcję "API" związaną z wykonywaniem operacji na plikach, o nazwie "OpenFile", ale ponieważ zwraca ona uchwyt pliku stosowany w systemie DOS, może być wykorzystywana jedynie w połączeniu z funkcjami niskiego poziomu, takimi jak "_lread" czy "_lwrite". Funkcja "OpenFile" umożliwia, wbrew swojej nazwie, nie tylko otwarcie wybranego pliku, ale także jego utworzenie, skasowanie, odszukanie czy uzyskanie o nim informacji. Rodzaj wykonywanej operacji określa się prze przypisanie odpowiedniej wartości drugiemu parametrowi (pierwszy parametr zawiera nazwę pliku), dokonując wyboru spośród predefiniowanych stałych rozpoczynających się od stałych "of_", takich jak "of_Create", "of_Delete", "of_Exist", "of_Read", "of_ReadWrite", "of_Search" czy "of_Write". Tym, co odróżnia tekst źródłowy aplikacji Windows od tekstu źródłowego programów DOS-a, jest przede wszystkim jego inna struktura, wynikająca z konieczności wykonywania wszystkich operacji w środowisku Windows w odpowiedzi na uzyskiwanie określonych komunikatów. Zamieszczone niżej uzupełnienia i modyfikacje przygotowywanej przez nas aplikacji "MDI" mają na celu pokazanie możliwego sposobu realizacji interfejsu umożliwiającego korzystanie z poleceń "Nowy", "Otwórz", "Zachowaj" ("Zapisz") i "Zachowaj ako" ("Zapisz jako"), występujących w menu "Plik" praktycznie każdej aplikacji Windows. Dla zwiększenia przejrzystości tekstu źródłowego założono, że polecenie "Otwórz" nie powoduje otwarcia nowego okna dokumentu, lecz jedynie umożliwia użytkownikowi umieszczenie zawartości wybranego pliku w oknie bieżącym. Tak więc w celu wykonania działania odpowiadającego wywołaniu polecenia "Otwórz" typowej aplikacji Windows należy w naszym programie wybrać kolejno polecenia "Nowy" oraz "Otwórz". Innym krokiem poczynionym w celu uproszczenia tekstu programu jest skorzystanie ze standardowego okna dalogowego "TFileDialog", zdefiniowanego w module "StdDlgs"; mimo iż okno to zawiera teksty w języku angielskim oraz odznacza się dość spartańską postacią, w pełni wystarcza do określenia nazwy katalogu i pliku. Konieczne do wykonania zmiany w programie są następujące: - uzupełnienie klauzuli "USES" o moduł "StdDlgs"; - zwiększenie rozmiaru bufora "Bufor", tak aby w każdym jego wierszu można było zapamiętać liczbę znaków o jeden większą od liczby kolumn redagowanego dokumentu (czyli 81 ); zmiana taka umożliwia bezpośrednią wymianę danych między buforem a plikiem (za pomocą funkcji "ReadLn" i "WriteLn"); - utworzenie pola "NazwaPliku" obiektu "TOknoDok", umożliwiającego zapamiętanie nazwy redagowanego pliku (wraz z pełną ścieżką); pole to powinno być inicjalizowane przez wpisanie do niego łańcucha pustego w konstruktorze obiektu "TOknoDok", modyfikowane po wczytaniu nazwy pliku w metodzie prywatnej "TOknoDok.CzytNazPliku" (wywoływanej podczas otwierania i zachowywania dokumentu) oraz wykorzystywane w metodach "Otworz", "Zachowaj" i "ZachowajJako" obiektu "TOknoDok", czyli po wyborze z menu "Plik" poeceń o odpowiadających tym metodom nazwach; - wprowadzenie pola "PlikZmodyf" obiektu "TOknoDok", pozwalającego pamiętać, czy w zawartości bieżącego dokumentu zostały dokonane zmiany; polu temu powinna być przypisywana wartość logiczna "False" w konstruktorze obiektu "TOknoDok" oraz bezpośrednio po wczytaniu nowego dokumentu ("TOknoDok.Otworz") lub zapisaniu bieżącego dokumentu na dysku ("TOknoDok.Zapisz" i "TOknoDok.ZapiszJako"); wartość "True" należy przypisać polu "PlikZmodyf" po wczytaniu dowolnego znaku ("TOkno.WMChar"); bieżąca wartość tgo pola powinna być sprawdzana każdorazowo przed wyświetleniem okna dialogowego zawierającego pytanie, czy bieżący dokument ma zostać zapisany w pliku ("TOknoDok.CanClose" i "TOknoDok.Otworz"); - zmodyfikowanie metody "TOknoDok.Otworz", tak aby w razie potrzeby (gdy redagowany tekst został zmodyfikowany) umożliwiała ona zachowanie bieżącego dokumentu oraz aby pozwalała wczytać do bieżącego okna dokumentu zawartość pliku o nazwie podanej za pośrednictwem okna dialogowego; - utworzenie metody "TOknoDok.ZachowajJako", reagującej na wybór polecenia "Zachowaj jako" z menu "Plik" i umożliwiającej zapisanie dokumentu w pliku o nazwie wprowadzonej za pomocą okna dialogowego; - zmodyfikowanie metody "TOknoDok.Zachowaj", tak aby wtedy, gdy nazwa pliku została już podana, zapisywała ona w nim bieżący dokument, w przeciwnym zaś wypadku wykonywała działanie identyczne z działaniem metody "ZachowajJako"; - zdefiniowanie funkcji prywatnej "CzytNazPliku" obiektu "TOknoDok", wyświetlającej okno dialogowe do wprowadzenia nazwy pliku, który ma zostać otwarty lub zapisany; - utworzenie funkcji prywatnych "WczytajPlik" i "ZapiszPlik" obiektu "TOknoDok", z których pierwsza powinna wczytywać do bufora "Bufor" zawartość pliku o nazwie pamiętanej w polu "NazwaPliku", druga zaś wykonywać działanie odwrotne, to znaczy zapisywać zawartość bufora do tego pliku; - zmodyfikowanie metody "TOknoDok.CanClose", tak aby okno weryfikacyjne było wyświetlane tylko wtedy, gdy bieżący dokument został zmodyfikowany od czasu jego wczytania z pliku lub ostatniego zachowania, to znaczy kiedy wartość zmiennej "PlikZmodyf" jest równa "True", oraz aby to okno dialogowe nie tylko zawierało pytanie, czy okno dokumentu ma zostać zamknięte, ale także umożliwiało zapisanie dokumentu na dysku. Podczas analizy tekstu źródłowego aplikacji "MDI" warto zwrócić uwagę na to, że: - zwiększenie wartości zmiennej "Y" funkcji "TOknoDok.WczytajPlik" o 1 dokonywane jest za pomocą procedury "Inc"; przypomnijmy, że zapis "Inc(X)" jest równoznaczny z zapisem "X := X + 1", natomiast "Inc(X, N)" odpowiada instrukcji "X := X + N"; - stała "mb_YesNoCancel", zastosowana w wywołaniach funkcji "MessageBox" występujących w metodach "CanClose" i "Otworz" obiektu "TOknoDok", umożliwia wyświetlenie standardowego okna informacyjnego zawierającego trzy przyciski. "Tak", "Nie" i "Anuluj". USES WObjects, WinTypes, WinProcs, WinDos, Strings, StdDlgs, MDI_M; TOknoDok = OBJECT(TWindow) ... Bufor: ARRAY[0..MaxNrWrsz] OF ARRAY[0..MaxNrKol+1] OF Char; ... NazwaPliku: ARRAY[0..fsPathName] OF Char; PlikZmodyf: Boolean; ... PROCEDURE ZachowajJako(VAR Msg: TMessage); VIRTUAL cm_First+cm_FileSaveAs; PRIVATE ... FUNCTION CzytNazPliku(RodzOper: Word) ; Boolean; FUNCTION WczytajPlik: Boolean; FUNCTION ZapiszPlik: Boolean; END; CONSTRUCTOR TOknoDok.Init; VAR ... BEGIN ... StrCopy(NazwaPliku, ' '); PlikZmodyf := False; END; FUNCTION TOknoDok.CanClose; VAR Wybor: Integer; Msg: TMessage; BEGIN IF PlikZmodyf THEN BEGIN Wybor := MessageBox(HWindow, 'Zapisać dokument?', Attr.Title, mb_IconQuestion OR mb_YesNoCancel); IF Wybor = IdYes THEN Zachowaj(Msg); END ELSE Wybor := IdNo; IF Wybor <> IdCancel THEN CanClose := True ELSE CanClose := False; END; PROCEDURE TOknoDok.WMChar; CONST ... Var ... BEGIN ... PlikZmodyf := True; END; PROCEDURE TOknoDok.Otworz; VAR Wybor: Integer; BEGIN IF PlikZmodyf THEN BEGIN Wybor := MessageBox(HWindow, 'Zapisać dokument?', Attr.Title, mb_IconQuestion OR mb_YesNoCancel); IF Wybor = IdYes THEN Zachowaj(Msg); END ELSE Wybor := IdNo; IF Wybor <> IdCancel THEN IF CzytNazPliku(sd_FileOpen) THEN IF WczytajPlik THEN SetWindowText(HWindow, NazwaPliku); END; PROCEDURE TOknoDok.Zachowaj; BEGIN IF NazwaPliku[0] = #0 THEN CzytNazPliku(sd_FileSave); IF NazwaPliku[0] <> #0 THEN IF ZapiszPlik THEN SetWindowText(HWindow, NazwaPliku); END; {Reakcja na wybór polecenia menu Plik|Zachowaj jako} PROCEDURE TOknoDok.ZachowajJako; BEGIN IF CzytNazPliku(sd_FileSave) THEN IF ZapiszPlik THEN SetWindowText(HWindow, NazwaPliku); END; {Wczytanie nazwy pliku, który ma zostać otwarty lub zachowany} FUNCTION TOknoDok.CzytNazPliku; VAR OknoDialogowe: PFileDialog; Nazwa: ARRAY[0..fsPathName] OF Char; BEGIN STRCopy(Nazwa, '*.txt'); OknoDialogowe := New(PFileDialog, Init(@Self, PChar(RodzOper), Nazwa)); IF Aplikacja.ExecDialog(OknoDialogowe) = id_OK THEN BEGIN StrCopy(NazwaPliku, Nazwa); CzytNazPliku := True; END ELSE CzytNazPliku := False; END; {Wczytanie zawartości pliku do bufora} FUNCTION TOknoDok.WczytajPlik; VAR Plik: TEXT; X, Y: Integer; BEGIN Assign(Plik, NazwaPliku); {$I-} Reset(Plik); {$I+} IFIOResult = 0 THEN BEGIN FOR Y: 0 TO MaxNrWrsz DO FOR X := 0 TO MaxNrKol DO Bufor [Y] [X] := ' '; Y := 0; WHILE NOT Eof(Plik) AND (Y <= MaxNrWrsz) DO BEGIN ReadLn(Plik, Bufor [Y]); Inc(Y); END; Close(Plik); PlikZmodyf := False; WczytajPlik := True; InvalidateRect(HWindow, NIL, True); END ELSE Begin MessageBox(HWindow, 'Błąd odczytu pliku', Attr.Title, mb_IconExclamation OR mb_OK); WczytajPlik := False; END; END; {Zapisanie zawartości bufora do pliku} FUNCTION TOknoDok.ZapiszPlik; VAR Plik: TEXT; X, Y: Integer; BEGIN Assign(Plik, NazwaPliku); [$I-} Rewrite(Plik); {$I+} IF IOResult = 0 THEN BEGIN FOR Y := ) TO MaxNrWrsz DO BEGIN Bufor [Y] [MaxNrKol+1] := #0; WriteLn(Plik, Bufor [Y]); END; Close(Plik); PlikZmodyf := False; ZapiszPlik := True; END ELSE BEGIN MessageBox(HWindow, 'Błąd zapisu pliku', Attr.Title, mb_IconExclamation OR mb_OK); ZapiszPlik := False; END; END; Rys. 8.1. Po wyborze polecenia "Otwórz" z menu "Plik" program pozwala umieścić w oknie dokumentu zawartość wybranego pliku tekstowego. Rys. 8.2. Zredagowany dokument można zapisać w pliku po wyborze polecenia "Zachowaj" lub "Zachowaj jako" z menu "Plik". Rys. 8.3. Błąd powstały podczas odczytywania zawartości pliku sygnalizowany jest wyświetleniem okna informacyjnego. Rys. 8.4. Jeżeli dokument został zmodyfikowany, przed zamknięciem okna lub wczytaniem zawartości innego pliku program wyświetla okno weryfikacyjne. 8.2. Drukowanie. 8.2.1. Uzyskiwanie kontekstu drukowania. Jednym z pozytywnych skutków daleko idącej wirtualizacji operacji wykonywanych w środowisku Windows jest ogromne podobieństwo drukowania do wyświetlania danych na ekranie. W obu przypadkach należy uzyskać kontekst urządzenia (w odniesieniu do ekranu zwany najczęściej kontekstem wyświetlania) oraz można korzystać z funkcji "GDI" (omówionych w rozdziale 4, "Wyświetlanie tekstu i rysunków"), takich jak "TextOut" czy "Rectangle". Z drugiej jednak strony, między obsługą monitora i drukarki istnieją dość zasadnicze różnice, które wynikają głównie z następujących faktów: - w systemie komputerowym istnieje zawsze jeden monitor (nawet, jeśli fizycznie podłączonych jest więcej, to każdy z nich wyświetla ten sam obraz), ale może występować większa liczba drukarek; - w przeciwieństwie do parametrów ekranu, których jest niewiele (rozdzielczość i liczba wyświetlanych kolorów) i które mają z góry ustalone wartości, liczba parametrów drukarki jest z reguły bardzo duża (rozdzielczość, format i orientacja papieru, liczba kopii, sposób drukowania grafiki itp.), a poza tym każdy z nich można dowolnie ustawić bezpośrednio przed rozpoczęciem wydruku; - podczas gdy ekran może być dzielony przez wiele aplikacji, z których każda korzysta z przydzielonego jej okna, zadania zlecone drukarce przez poszczególne aplikacje muszą być od siebie oddzielone i wykonywane kolejno, a nie jednocześnie; - drukarki są dużo wolniejsze od monitorów, w związku z czym system musi zapewnić możliwość wznowienia pracy z aplikacją przed zakończeniem wydruku; - wyświetlenie nowych informacji na ekranie następuje poprzez skasowanie informacji poprzednich; działania takiego nie można oczywiście wykonać na drukarce - w tym przypadku jedynym rozwiązaniem jest wysunięcie bieżącej strony (kartki) papieru; - nie można oczekiwać od drukarki, że będzie ona drukowała dane w kolejności ich powstawania na ekranie, dlatego trzeba najpierw zdefiniować całą stronę, a dopiero potem można rozpocząć jej drukowanie. Wszystko to powoduje, że w pełni profesjonalna implementacja funkcji drukowania (łącznie z możliwością wyboru drukarki i ustawieniem wszystkich parametrów drukowania) jest zadaniem bardzo trudnym i znacznie wykraczającym poza zasięg tej książki. Dlatego w jej dalszej części zajmiemy się jedynie wydrukiem wykonanym za pomocą drukarki domyślnej z zastosowaniem jej standardowych parametrów. Przytoczone fakty znajdują swoje odbicie już w sposobie uzyskiwania kontekstu drukowania. O ile do uzyskania kontekstu wyświetlania wystarczało wywołanie funkcji "GetDC" połączone z przekazaniem jej w postaci parametru uchwytu okna (w metodzie "TWindow.Paint" wykonywanie nawet tego działania nie było konieczne), przed utworzeniem kontekstu drukowania należy uzyskać dokładne informacje dotyczące wybranej drukarki, do których należą: nazwa drukarki, nazwa jej programu obsługi (zwanego czasami sterowniiem lub, w slangu informatycznym, drajwerem) oraz oznaczenie portu, do którego została ona podłączona (na przykład "LTP1" ). Wymienione informacje zgromadzone są w pliku konfiguracyjnym Windows o nazwie "WIN.INI". Jak wiadomo, plik ten podzielony jest na sekcje, które są poprzedzane nagłówkami, wyróżniającymi się spośród pozostałego tekstu tym, że występują w nawiasach kwadratowych. Każda z pozycji sekcji zajmuje cały wiersz i rozpoczyna się do słowa kluczowego, za którym następuje znak równości oraz jedno lub więcej słów oddzielonych od siebie przecinkami. Informacje o drukarkach występują w dwóch sekcjach. Pierwsza z nih, o nazwie "windows", składa się z kilku pozycji poświęconych systemowi Windows, z których jedna rozpoczyna się od słowa kluczowego "device" i zawiera dane dotyczące drukarki domyślnej. Druga sekcja nosi nazwę "devices" i poświęcona jest wyłącznie drukarkom, a raczej ich programom obsługi, z których korzysta komputer. Struktura pozycji opisujących drukarki w obu wymienionych sekcjach różni się od siebie. W sekcji windows przyjmuje ona postać: "device=nazwa_drukarki,nazwa_programu_obsługi,oznaczenie_portu", na przykład "device=Epson Stylus COLOR IIs,EPMJ5C,LPT1:". Postać poszczególnych pozycji sekcji "devices" jest następująca: "nazwa_drukarki=nazwa_programu_obslugi,oznaczenie_portu", na przykład "IBM Proprinter II=PROPRINT,LPT1:". Aby do informacji zgromadzonych w wybranej pozycji pliku "WIN.INI" uzyskać dostęp w aplikacji Windows, należy skorzystać z funkcji "API" "GetProfileString". Ma ona pięć parametrów, z których pierwsze dwa są łańcuchami opisującymi daną pozycję przez podanie nagłówka sekcji (bez nawiasów kwadratowych) i łańcucha ze słowem kluczowym. Parametr czwarty powinien zawierać wskaźnik do bufora, w którym funkcja ma umieścić tekst potrzebnej pozycji, natomiast parametr piąty - długość tego bufora. Jeżeli pozycj opisanej dwoma pierwszymi parametrami nie ma w pliku "WIN.INI", funkcja "GetProfileString" zwraca łańcuch standardowy, przekazany jej w postaci trzeciego parametru; parametr ten może być łańcuchem pustym, ale nie może przyjmować wartości "NIL". Kontekst drukowania (podobnie, jak kontekst związany z każdym innym urządzeniem, na przykład platerem) tworzy się za pomocą funkcji "API" "CreateDC", która oczekuje przekazania jej w postaci pierwszych trzech parametrów następujących informacji: nazwy programu obsługi, nazwy drukarki (urządzenia) oraz oznaczenia portu. Parametrowi czwartemu należy przypisać wartość "NIL" wtedy, gdy program obsługi ma dokonać inicjalizacji drukarki z wykorzystaniem parametrów ustawionych w "Panelu sterowania". W przeciwnym wypadku parametr ten powinien wskazywać na strukturę "TDevMode", zawierającą w poszczególnych polach wartości określające potrzebne parametry pracy drukarki, takie jak jej rozdzielczość czy wielkość i orientację kartki papieru (bieżące ustawienia drukarki można uzyskać za pomocą funkcji "API" o nazwie "ExtDeviceMde"). Jeżeli działanie funkcji "CreateDC" zakończyło się pomyślnie, zwraca ona wartość określającą kontekst drukowania; w razie wystąpienia błędu zwracana wartość jest równa zeru. Aby z łańcucha znaków zwracanego przez funkcję "GetProfileString" (w postaci czwartego parametru) można było wyodrębnić poszczególne elementy oddzielone od siebie przecinkami, najlepiej skorzystać z funkcji "Pascala" "StrScan", zawartej w module "Strings". Funkcja ta przeszukuje przekazany jej w postaci pierwszego parametru łańcuch znakowy w celu odszukania pierwszego wystąpienia znaku określonego jako drugi parametr (w tym przypadku - przecinka). Wartością funkcji "StrScan" jest wskaźnik do miejscaokreślającego położenie szukanego znaku, jeśli został on odnaleziony, lub 0 - w przeciwnym wypadku. Mimo iż z reguły interpretacja danych zawartych w pliku "WIN.INI" przebiega prawidłowo, to jednak nie można wykluczyć, że uległ on z jakichś powodów uszkodzeniu, w wyniku czego któreś z wywołań funkcji "StrScan", względnie wywołanie funkcji "CreateDC", nie kończy się pomyślnie. W takiej sytuacji utworzenie kontekstu drukowania nie jest oczywiście możliwe i wykonywanie bieżącej funkcji musi ulec zakończeniu. Ponieważ jednak tego rodzaju sytuacja awaryjna może być traktowana jako pewnego rodzaju wyjątk, w celu zachowania odpowiedniej struktury tekstu źródłowego wygodnie jest skorzystać z procedury standardowej "Exit", powodującej natychmiastowe zakończenie wykonywania bieżącego bloku instrukcji, którym może być także cała procedura lub funkcja. Po zakończeniu drukowania nie można oczywiście zapomnieć o zwolnieniu (usunięciu) kontekstu drukowania. Dokonuje się tego za pomocą funkcji "API" "DeleteDC", czyli inaczej niż w przypadku zwalniania kontekstu wyświetlania, gdzie, jak wiadomo, stosuje się funkcję "ReleaseDC". Funkcja "DeleteDC" ma tylko jeden parametr, którym jest usuwany kontekst drukowania. Obecnie możemy przystąpić do ulepszania postaci naszej aplikacji "MDI", tak aby w wyniku wyboru polecenia "Drukuj" z menu "Plik" wyświetlała ona okno weryfikujące zawierające informacje o drukarce domyślnej, a także pytanie, czy wydruk ma być rzeczywiście wykonany. W razie udzielenia przez użytkownika odpowiedzi twierdzącej (naciśnięcia przycisku "Tak") nasza aplikacja na razie ograniczać się będzie do utworzenia oraz zwolnienia kontekstu drukowania (samo drukowanie zrealizujemy w następnym podrozdzale). W tym celu konieczne jest dokonanie następujących zmian w tekście źródłowym programu: - utworzenie metody "TOknoDok.Drukuj", reagującej na wybór polecenia "Drukuj" z menu "Plik" przez wywołanie metody prywatnej obiektu "UtworzKontDruk"; - zdefiniowanie metody "TOknoDok.UtworzKontDruk", której zadaniem jest utworzenie kontekstu drukarki domyślnej i wyświetlenie wspomnianego weryfikacyjnego okna dialogowego. TOknoDok = OBJECT(TWindow) ... PROCEDURE Drukuj(VAR Msg: Tmessage); VIRTUAL cm_First+cm_Drukuj; PRIVATE ... FUNCTION UtworzKontDruk: HDC; END; {Reakcja na wybór polecenia menu Plik|Drukuj} PROCEDURE TOknoDok.Drukuj; VAR KontDruk: HDC; BEGIN KontDruk := UtworzKontDruk; IF KontDruk = 0 THEN Exit; DeleteDC(KontDruk); END; {Utworzenie kontekstu drukarki} FUNCTION TOknoDok.UtworzKontDruk; VAR Wiersz: ARRAY[0..80] OF Char; NazwaDruk, NazwaSter, NazwaPortu: PChar Napis: ARRAY[0..255] OF Char; BEGIN UtworzKontDruk := 0; GetProfileString('windows', 'device', ' ', Wiersz, SizeOf(Wiersz)); NazwaDruk := Wiersz; NazwaSter := StrScan(NazwaDruk, ', '); IF NazwaSter = NIL THEN Exit; NazwaSter^ := #0; Inc(NazwaSter); NazwaPortu := StrScan(NazwaSter, ','); IF NazwaPortu = NIL THEN Exit; NazwaPortu^ := # 0; Inc(NazwaPortu); StrCopy(Napis, 'Drukarka:' + #9); StrCat(Napis, NazwaDruk); StrCopy(Napis+StrLen(Napis), #13 + 'Program obsługi:' + #9); StrCat(Napis, NazwaSter); StrPCopy(Napis+StrLen(Napis), #13 + 'Port' + #9#9); StrCat(Napis, NazwaPortu); StrPCopy(Napis+StrLen(Napis), #13#13 + 'Drukować?'); IF MessageBox(HWindow, Napis, 'Drukuj', mb_IconQuestion OR mb_YesNo) = IdYes THEN UtworzKontDruk := CreateDC(NazwaSter, NazwaDruk, NazwaPortu, NIL); END; Rys. 8.5. Wybór polecenia "Drukuj" z menu "Plik" powoduje wyświetlenie okna weryfikacyjnego z informacjami dotyczącymi drukarki, która zostanie użyta do drukowania. 8.2.2. Przebieg drukowania. Aż do wersji 3.0 włącznie moduł "GDI" systemu Windows udostępniał tylko jedną dodatkową funkcję, o nazwie "Escape", w celu umożliwienia programistom zaspokojenia wszystkich zwiększonych (w stosunku do monitora) wymagań drukarek. Funkcja ta, obecnie uzupełniona o dodatkowe funkcje "API", ze względu na kompatybilność istnieje nadal w swej niezmienionej postaci. Jak łatwo się domyślić, ma ona wiele różnych podfunkcji, których rodzaj określa się za pomocą jednego z jej parametrów. Podfunkcje "Escape", pdobnie jak wszystkie inne funkcje "GDI", są niezależne od sprzętu. Dzięki temu programista zwolniony jest z obowiązku uwzględniania wielu różnych sekwencji sterujących pracą drukarek, występujących powszechnie w programach działających w środowisku DOS-a. Funkcji "Escape" należy przekazać pięć parametrów, z których stały charakter mają tylko dwa pierwsze, określające kolejno kontekst drukowania oraz kod wywoływanej podfunkcji. Znaczenie pozostałych trzech parametrów zależy od rodzaju podfunkcji, niemniej jednak zawsze drugi z nich, "InData", jest wskaźnikiem do struktury zawierającej dane wejściowe odczytywane przez daną podfunkcję, natomiast trzeci, "OutData" - wskaźnikiem do bufora, umożliwiającego zapisanie danych przez nią zwracanych. Parametr pirwszy, "Count", określa liczbę bajtów danych parametru "InData". Jeżeli dana podfunkcja nie oczekuje przekazania jej danych wejściowych, parametrowi "InData" przypisuje się wartość "NIL", a parametrowi "Count" wartość 1 ; brak danych wyjściowych wiąże się z przypisaniem wartości "NIL" parametrowi "OutData". Wartość funkcji "Escape" oznacza sposób zakończenia jej działania. Jeśli jest większa od zera, działanie funkcji zakończyło się sukcesem, jeśli mniejsza od zera, wystąpił błąd. Wartość równa zeru oznacza, że wybrana podfunkcja nie została w danym programie obsługi drukarki zaimplementowana. Większość programów obsługi uwzględnia bowiem tylko niektóre spośród wszystkich podfunkcji "Escape", co jednak w niczym nie ogranicza możliwości pracy programisty, gdyż z reguły korzysta się tylko z ich niewielkigo podzbioru. W wersji 3.1 Windows zostały wprowadzone nowe funkcje "API" do obsługi drukarek, ale większość z nich ma swoje odpowiedniki w postaci podfunkcji "Escape" i z reguły nosi takie same nazwy jak ich pierwowzór. Dodatkowe funkcje charakteryzują się różną liczbą parametrów. Ich elementem wspólnym jest to, że pierwszy (niekiedy jedyny) parametr określa zawsze kontekst drukowania. Jak wobec tego przebiega wykonywanie typowego wydruku? Rozpoczyna się ono zawsze od wywołania przez aplikację funkcji "StartDoc", która informuje program obsługi drukarki o zainicjowaniu nowego zadania drukowania oraz uruchamia "Menedżer wydruku", chyba że został on już uruchomiony wcześniej. Oprócz kontekstu drukowania, funkcja "StartDoc" oczekuje przekazania jej jeszcze parametru będącego strukturą typu "TDocInfo". Do pierwszych dwóch pól tej struktury należy wpisać jej wielkość wyrażoną w bajtach ("cbSize") oraz nazwę zadania drukowania, pojawiającą się w oknie "Menedżera wydruku" ("lpszDocName"). Za pomocą trzeciego pola, "lpszOutput", określa się, czy wydruk ma być wykonany w podanym kontekście drukowania, czy skierowany do pliku; w pierwszym przypadku wpisuje się wartość "NIL", w dugim podaje nazwę odpowiedniego pliku. Rozwiązaniem alternatywnym w stosunku do korzystania z funkcji "StartDoc" jest wywołanie podfunkcji "Escape" o takiej samej nazwie. Tworzenie każdej nowej strony wydruku powinno być poprzedzone wywołaniem funkcji "StartPage" (brak odpowiednika w podfunkcjach "Escape"), przygotowującej program obsługi drukarki do odbioru danych. Następnie można generować tekst i rysunki, które mają się znaleźć na danej stronie wydruku, korzystając z tych samych funkcji "GDI", których używa się do wyświetlania zawartości ekranu. Tym razem funkcje te jednak działają inaczej: zamiast natychmiastowego przesyłania danych do drukarki umieszczają je w opowiednim formacie w pliku roboczym na dysku. Po zakończeniu definiowania całej strony wydruku należy wywołać funkcję "EndPage" lub odpowiadającą jej podfunkcję "Escape" o nazwie "NewFrame". Dopiero w tym momencie rozpoczyna się właściwe drukowanie, w czasie którego program obsługi drukarki zamienia instrukcje zawarte w pliku roboczym na sekwencje sterujące pracą drukarki. Sekwencjami tymi są albo proste instrukcje z następującymi po nich ciągami zer i jedynek, które określają postać graficzną wydruku, albo instrukcje "PostScriptowe". Ostatni eap realizacji funkcji "EndPage" stanowi zawsze dokonanie wysuwu bieżącej strony (kartki) papieru. Dzięki temu, że drukowanie obsługiwane jest nie przez samą aplikację Windows, lecz przez "Menedżer wydruku" oraz program obsługi drukarki, do generowania kolejnej strony wydruku (inicjowanego wywołaniem funkcji "StartPage") można przystąpić natychmiast po zakończeniu definiowania poprzedniej, czyli po wywołaniu funkcji "EndPage". W ten sposób użytkownik może wznowić pracę z aplikacją zaraz po utworzeniu wszystkich plików roboczych, w czasie, gdy jeszcze trwa drukowanie. Zresztą w chwili przekazania adania drukowania "Menedżerowi wydruku" zaczyna ono niejako żyć własnym życiem - można je anulować, dowolnie opóźniać w czasie, drukować na przemian strony należące do różnych dokumentów itp. Ostatnią operacją wykonywaną przez aplikację podczas drukowania dokumentu jest wywołanie funkcji "EndDoc" (lub podfunkcji "Escape" o takiej samej nazwie), która informuje program obsługi drukarki o zakończeniu zadania drukowania rozpoczętego funkcją "StartDoc". Istnieje także możliwość przerwania zadania wraz ze skasowaniem wszystkich jego danych wyjściowych, czyli tekstu i rysunków wygenerowanych od czasu ostatniego wywołania funkcji "StartDoc". Operację tę przeprowadza się za pomocą funkcji "AbortDoc". Na uwagę zasługuje także funkcja "SetAbortProc", która umożliwia wskazanie zdefiniowanej w aplikacji procedury zezwalającej użytkownikowi na przerwanie zadania drukowania w czasie wysyłania danych do drukarki. Procedurę tę określa się przez przekazanie jejadresu (utworzonego za pomocą funkcji "API" "MakeProcInstance") w postaci drugiego parametru funkcji "SetAbortProc". Zarówno "AbortDoc", jak i "SetAbortProc" mają swoje odpowiedniki w postaci podfunkcji "Escape" o takich samych nazwach. Oprócz "StartDoc" i "SetAbortProc" każda inna spośród omawianych funkcji sterujących pracą drukarki ma tylko jeden parametr, a mianowicie kontekst drukowania. Ponieważ wykonanie tych funkcji nie zawsze musi zakończyć się sukcesem, powinno się sprawdzać zwracane przez nie wartości, biorąc pod uwagę fakt, że poprawne wykonanie funkcji "StartDoc", "StartPage" i "SetAbortProc" powoduje zwrócenie wartości dodatniej, podczas gdy w wyniku prawidłowego wykonania funkcji "EndPage", "EndDoc" i "AbortDoc" zwraana jest wartość większa lub równa zeru. Należy jednak pamiętać o tym, że żadna z omówionych funkcji sterujących pracą drukarki nie wykrywa takich sytuacji nieprawidłowych, jak niewłączenie drukarki czy brak papieru - wystąpienie tego rodzaju błędów sygnalizowane jest dopiero przez program obsługi. Po tym dość obszernym wprowadzeniu nadeszła pora na wykorzystanie przynajmniej niektórych spośród powyższych informacji w praktyce. W tym celu rozbudujemy w taki sposób przygotowywaną przez nas aplikację "MDI", aby jej metoda "TOknoDok.Drukuj" drukowała zawartość bieżącego okna dokumentu (zawartość bufora "Bufor"). W celu uproszczenia tekstu źródłowego sprawdzać będziemy tylko poprawność wykonania funkcji "StartDoc". Oprócz rozszerzenia procedury "Drukuj" konieczne jest też uzupełnienie dyrektywy "UES" o wczytanie modułu Win31, w którym znajduje się definicja typu "TDocInfo": USES WObjects, WinTypes, WinProcs, WinDos, Win31, Strings, StdDlgs, MDI_M; PROCEDURE TOknoDok.Drukuj; CONST MargLewy = 10; MargGorny = 10; Odstep = 20; VAR ... DocInfo: TDocInfo; Y; Integer; BEGIN ... WITH DocInfo DO BEGIN cbSize := SizeOF(TDocInfo); lpszDocName := 'Programowanie w Windows'; lpszOutput := NIL; END; IF StarDoc(KontDruk, DocInfo) > 0 THEN BEGIN StartPage(KontDruk); FOR Y := 0 TO MaxNrWrsz DO TextOut(KontDruk, MargLewy, MargGorny+Y*Odstep, @Bufor [Y], MaxNrKol+1); EndPage(KontDruk); EndDoc(KontDruk); END ELSE MEssageBox(HWindow, 'Błąd drukowania', Attr.Title, mb_IconExclamation OR mb_OK); ... END; Rys. 8.6. "Menedżer wydruku" wyświetla nazwę zadania drukowania określoną w wywołaniu funkcji "StartDoc". Rozdział 9. Standardowe aplikacje pakietu TPW. W module "StdWnds" pakietu "TPW" zdefiniowane sa obiekty reprezentujące okna dwóch prostych aplikacji, "TEdit" i "TFile". Obie one umożliwiają przeprowadzanie edycji tekstu, przy czym druga pozwala dodatkowo na wykonywanie operacji na plikach. Mimo iż wadą tych aplikacji jest ich mały stopień zaawansowania oraz wyświetlanie tekstów w języku angielskim, to jednak z pewnością warto przeanalizować ich tekst źródłowy, aby w ten sposób utrwalić swoją znajomość podstaw programowania w Windows. Nic także ne stoi na przeszkodzie w wykorzystaniu tego tekstu w celu przygotowania samodzielnych, bardziej rozbudowanych aplikacji. 9.1. Edycja tekstu ASCII. Zarówno "TEdit", jak i "TFile" są obiektami reprezentującymi okna dwóch prostych edytorów tekstu. Umożliwiają więc one wykonywanie prostych operacji edycyjnych przeprowadzanych za pomocą klawiatury i myszy, a także udostępniają menu "Edit" ("Edycja") i "Search" ("Szukaj"). Menu te mają wygląd typowy dla aplikacji Windows i zawierają polecenia pozwalające między innymi na kopiowanie i wklejanie bloków tekstowych czy szukanie i zastępowanie wybranych fragmentów tekstu. Poniżej przedstawiono tekst źródowy bardzo prostej aplikacji korzystającej z obiektu "TEdit". Definicja tego obiektu, podobnie jak obiektu "TFile", znajduje się w module "StdWnds" (w pliku "STDWNDS.PAS", mieszczącym się w podkatalogu "OWL" katalogu pakietu "TPW"): Rys. 9.1. Okno programu do edycji tekstu "ASCII" udostępnia dwa menu: "Edit" i "Search". {*******************************************************************} { } {Edycja tekstu ASCII } { } {*******************************************************************} PROGRAM Edycja1; USES WObjects, WinTypes, WinProcs, StdWnds; TYPE {Obiekt reprezentujący aplikację} TAplikacja = OBJECT(TApplication) PROCEDURE InitInstance; VIRTUAL; PROCEDURE InitMainWindow; VIRTUAL; END; {Obiekt reprezentujący okno główne aplikacji} POknoEdycji = ^TOknoEdycji; TOknoEdycji = OBJECT(TEditWindow) CONSTRUCTOR Init(AParent: PWindowObject; ATitle: PChar); END; VAR Aplikacja: TAplikacja; {Zainicjowanie egzemplarza aplikacji} PROCEDURE TAplikacja.InitInstance; BEGIN TApplication.InitInstance; HAccTable := LoadAccelerators(HInstance, 'EdiutCommands'); END; {Utworzenie okna głównego aplikacji} PROCEDURE TAplikacja.InitMainWindow; BEGIN MainWindow := New(POknoEdycji, Init(NIL, 'Edycja tekstu ASCII')); END; {Konstruktor obiektu reprezentującego okno główne} CONSTRUCTOR TOknoEdycji.Init; BEGIN TEditWindow.Init(NIL, ATitle); Attr.Menu := LoadMenu(HInstance, 'EditCommands'); END; {Blok główny programu} BEGIN Aplikacja.Init('Aplikacja'); Aplikacja.Run; Aplikacja.Done; END. Rys. 9.2. Okno dialogowe "Replace" umożliwia zastąpienie fragmentów tekstu. 9.2. Edycja plików ASCII. Obiekt "TFile" stanowi rozszerzenie obiektu "TEdit" i udostępnia dodatkowo menu "File" ("Plik"), które zawiera polecenia pozwalające na wykonywanie typowych operacji na plikach, takich jak ich otwieranie czy zachowywanie. Poniżej przedstawiono tekst źródłowy praktycznie najprostszej z możliwych aplikacji wykorzystujących obiekt "TFile": {***************************************************************} { } {Edycja plików ASCII } { } {***************************************************************} PROGRAM Edycja2; USES WObjects, WinTypes, WinProcs, StdWnds; TYPE {Obiekt reprezentujący aplikację} TAplikacja = OBJECT(TApplication) PROCEDURE InitInstance; VIRTUAL; PROCEDURE InitMainWindow; VIRTUAL; END; {Obiekt reprezentujący okno główne aplikacji} POknoPliku = ^TOknoPliku TOknoPliku = OBJECT(TFileWindow) CONSTRUCTOR Init(AParent: PWindowsObject; ATitle: PChar); END; VAR Aplikacja: TAplikacja; {Zainicjowanie egzemplarza aplikacji} PROCEDURE TAplikacja.InitInstance; BEGIN TApplication.InitInstance; HAccTable := LoadAccelerators(HInstance, 'FileCommands'); END; Rys. 9.3. Dzięki menu "File" można edytować zawartość dowolnego pliku tekstowego - tym razem wybrano plik "AUTOEXEC.BAT". Rys. 9.4. Jeśli nastąpiła próba zakończenia działania programu, a zmodyfikowany plik nie został zachowany, dochodzi do wyświetlenia okna weryfikacyjnego. {Utworzenie okna głównego aplikacji} PROCEDURE TAplikacja.InitMainWindow; BEGIN MainWindow := New(POknoPliku, Init(NIL, 'Edycja pliku ASCII'); END; {Konstruktor obiektu reprezentującego okno główne} CONSTRUCTOR TOknoPliku.Init; BEGIN TFileWindow.Init(NIL, ATitle, NIL); Attr.Menu := LoadMenu(HInstance, 'FileCommands'); END; {Blok główny programu} BEGIN Aplikacja.Init('Aplikacja'); Aplikacja.Run; Aplikacja.Done; END. W ten sposób dotarliśmy do końca tej książki. Zawarte w niej informacje okażą, się najprawdopodobniej wystarczające dla tych czytelników, którzy sa hobbystami komputerowymi i piszą programy jedynie dla własnych potrzeb. Również i ci, którzy podstawy programowania w Windows chcieli poznać jedynie po to, by wzbogacić swoją wiedzę z dziedziny informatyki lub po prostu zaspokoić własną ciekawość, nie będą zapewne musieli sięgać po dodatkowe publikacje. W innej sytuacji są programiści pragnący pisać profesjonalne aplikacje Windows - znaleźli się oni dopiero na początku drogi prowadzącej do osiągnięcia zamierzonego celu. Jestem jednak przekonany, że po poznaniu i zrozumieniu zasad programowania w Windows zawartych w tej książce nie napotkają oni trudności w przyswajaniu wiedzy zawartej w dużo bardziej zaawansowanych, najczęściej angielskojęzycznych, wielotomowych publikacjach, oraz że ich studiowanie stanie się dla nich źródłem przyjemności i satysfakcji, a nie zniechęcenia, stresów i nie kończących się problemów.