PROGRAMOWANIE WINDOWS Charles Petzold Tytuł oryginału amerykańskiego: Programming Windows^ 5th ed. Tłumaczenie: Bogdan Kamiński (rozdz. 1-5), Piotr Czarnocki (rozdz. 6-9), Marcin Moskwa (rozdz. 10-12), Ewa Hadała-Mikołajczuk (rozdz. 13-15), Andrzej Miron (rozdz. 16-18), Piotr Kresak (rozdz. 19-23) Copyright 1999 by Microsoft Corporation Oryginal English language edition Copyńght 1999 by Petzold, Charles (1999). All rights published by arrangement with the originaI publisher, Microsoft Press, a division of Microsoft Corporation, Redmond, Washington, USA. Edycja polska: Wydawnictwo RM, Warszawa 1999 Wydawnictwo RM, 00-987 Warszawa 4, skr. pocztowa 144 e-mail: rm@rm.com.pl WWW: http: / /www.rm.com.pl All rights reserved. No part of this book may be reproduced or transmitted in any form or by any means, electronic or mechanical, including photocopying, recording or by any information storage retrieval system, without permission from the publisher. Żadna częœć tej pracy nie może być powielana i rozpowszechniana, w jakiejkolwiek formie i w jakikolwiek sposób (elektroniczny, mechaniczny) włšcznie z fotokopiowaniem, nagrywa- niem na taœmy lub przy użyciu innych systemów, bez pisemnej zgody wydawcy. Printed in Poland The Microsoft Windows logo is a trademark of Microsoft Corporation Wszystkie nazwy handlowe i towarów występujšce w niniejszej publikacji sš znakami towa- rowymi zastrzeżonymi lub nazwami zastrzeżonymi odpowiednich firm odnoœnych właœcicieli. Wydawnictwo RM dołożyło wszelkich starań, aby zapewnić najwyższš jakoœć tej ksišżce. Jednakże nikomu nie udziela żadnej rękojmi ani gwarancji. Wydawnictwo RM nie jest w żadnym przypadku odpowiedzialne za jakškolwiek szkodę (łšcznie ze szkodami wynikłymi z tytułu utraty zysków zwišzanych z prowadzeniem przedsiębiorstwa, przerw w działalnoœci przedsiębiorstwa lub utraty informacji gospodarczej) będšcej następstwem korzystania z informacji zawartych w niniejszej publikacji, nawet jeœli Wydawnictwo RM zostało powiado- mione o możliwoœci wystšpienia szkód. ISBN 83-87216-90-9 Redaktor prowadz,acy: Mirosława Szymańska Redakcja: joanna Cierkońska, Elżbieta Kaczak-Koœciańczuk, Irmina Wala-Pęgierska Korekta: Maria Najder Projekt graficzny okładki według oryginału: Grażyna Jędrzejec Skład: Robert Górski Drcck i oprawa: READ ME Drukarnia w Łodzi Wydanie II 10987654321 Spis treœci Częœć I. Podstawy 3 Rozdział 1: Zaczynamy 3 Œrodowisko Windows 4 Historia Windows 4 Cechy Windows 5 Łšczenie dynamiczne ... Opcje programowania Windows 9 API i modele pamięci .... Opcje języka programowania 10 Œrodowisko programistyczne 11 Dokumentacja API 12 Pierwszy program windowsowy... 12 Tryb znakowy... 12 Odpowiednik windowsowy 13 Pliki nagłówkowe 14 Punkt startowy programu 15 Funkcja MessageBox 16 Kompilowanie, łšczenie i uruchamianie 17 Rozdział 2: Wprowadzenie do unikodu 19 Krótka historia zestawów znaków 19 Standardy amerykańskie... 20 Inne języki 21 Rozszerzenie ASCII 22 Zestaw znaków dwubajtowych 24 Ratunek w unikodzie 25 Szerokie znaki i C 26 Typ danych char 26 Szersze znaki 27 Funkcje biblioteki znaków szerokich 2g Utrzymywanie pojedynczego Ÿródła 2g Szerokie znaki i Windows 30 Typy Plików nagłówkowych Windows 31 Wywołania funkcji Windows 32 Funkcje obsługi łańcuchów znaków w Windows 33 Użycie printf w Windows 34 Formatowanie okna komunikatu 36 Wielojęzycznoœć i ta ksišżka 37 VI Programowanie Windows Rozdział 3: Windows i komunikaty... 39 Własne okno 39 Przeglšd architektury 39 Program HELLOWIN 41 Myœlenie globalne 44 Rejestracja klasy okna 49 Tworzenie okna 54 Wyœwietlanie okna 56 Pętla komunikatów 56 Procedura okna... 58 Przetwarzanie komunikatów 59 Odtwarzanie pliku dŸwiękowego 59 Komunikat WM PAINT 60 Komunikat WM DESTROY 62 Przeszkody w programowaniu windowsowym 62 Nie wywołuj mnie, ja wywołam ciebie 62 Komunikaty kolejkowane i niekolejkowane 64 Szybkie wejœcie i wyjœcie 65 Rozdział 4: Wyœwietlanie tekstu 67 Malowanie i odœwieżanie 68 Komunikat WM PAINT 68 Prostokšty zatwierdzone i unieważnione 69 Wprowadzenie do GDI 70 Kontekst urzšdzenia 70 Otrzymywanie uchwytu kontekstu urzšdzenia. Sposób pierwszy 71 Struktura informacji o malowaniu 72 Otrzymywanie uchwytu kontekstu urzšdzenia. Sposób drugi 74 Funkcja TextOut - szczegóły 75 Czcionka systemowa 76 Rozmiary znaku 77 Rozmiary tekstu - szczegóły 78 Formatowanie tekstu 79 Składanie wszystkiego w całoœć 80 Procedura okna w SYSMETS1.C 86 Brak miejsca 88 Wielkoœć obszaru roboczego 88 Paski przewijania 89 Zakres i pozycja paska przewijania 91 Komunikaty paska przewijania 92 Przewijanie SYSMETS 94 Dostosowanie programu do malowania 98 Poprawianie przewijania 99 Spis treœci VII Funkcje informacyjne pasków przewijania 99 Jak przewijać minimalnieţ 101 Nowy SYSMETS 102 Nie lubię myszy 108 Rozdział 5: Podstawy grafiki 111 Struktura GDI . 111 Filozofia GDI 111 Wywolania funkcji GDI 113 Podstawowe elementy GDI 114 Pozostale elementy GDI 115 Kontekst urzšdzenia 116 Pobieranie uchwytu kontekstu urzšdzenia 116 Pobieranie informacji o kontekœcie urzšdzenia 118 Program DEVCAPS1 118 Rozmiar urzšdzenia 121 Informacje o kolorach 127 Atrybuty kontekstu urzšdzenia 129 Zapamiętywanie kontekstu urzšdzenia 130 Rysowanie punktów i linii 131 Podœwietlanie pikseli 132 Linie proste 132 Funkcje obramowania 137 Krzywe sklejane Bóziera 142 Używanie piór standardowych 147 Tworzenie, wybieranie i usuwanie piór 148 Zapełnianie przerw 151 Tryby rysowania 151 Rysowanie wypełnionych obszarów 153 Funkcja Polygon i tryb wypelniania wielokšta 154 Malowanie wnętrza 158 Tryby odwzorowania GDI 160 Współrzędne urzšdzenia i wspólrzędne logiczne 162 Systemy wspólrzędnych urzšdzenia 162 Widok i okno 163 Praca w trybie MM TEXT 165 Metryczne tryby odwwzorowania 168 "Modyfikowalne" tryby odwzorowania . 171 Program WHATSIZE 176 Prostokšty, regiony i obcinanie 179 Praca z prostokštami 180 Losowo kreœlone prostokšty 181 Tworzenie i malowanie regionów 185 VIII Programowanie Windows Obcinanie za pomocš prostokštów i regionów 186 Program CLOVER 187 Rozdział 6: Klawiatura 193 Podstawowe informacje 193 Ignorowanie klawiatury 193 Kto ma fokus 194 Kolejki i synchronizacja 195 Klawisze a znaW 196 Komunikaty klawiaturowe 196 Klawisze systemowe i niesystemowe 197 Kody klawiszy wirtualnych 198 Informacja przekazywana przez IParam 202 Stany klawisza [Shift] 204 Korzystanie z komunikatów zwišzanych z naciskaniem klawiszy 205 Wzbogacanie SYSMETS o obsługę klawiatury 206 Komunikaty znakowe 213 Cztery komunikaty znakowe 214 Kolejnoœć komunikatów 215 Naciskane klawisze a znaki 216 Komunikaty martwych znaków 217 Komunikaty klawiaturowe a zestawy znaków 218 Program KEYVIEW1 218 Problem z klawiaturami narodowymi 223 Zestawy znaków a czcionki 225 A co z unikodem? 236 TrueType i duże czcionw 237 Karetka (a nie kursor) 244 Funkcje karetki 244 Program TYPER 245 Rozdział 7: Mysz 253 Podstawy 254 Kilka szybkich definicji 254 Komunikaty myszy dla obszaru roboczego 255 Proste przetwarzanie komunikatów myszy: przykład 257 Obsługa klawiszy specjalnych 260 Kliknięcia dwukrotne 262 Komunikaty myszy nie zwišzane z obszarem roboczym 263 Komunikat testu na trafienie obrzeża okna 264 Komunikaty generujšce komunikaty 265 Testowanie trafienia w twoich programach 266 Przykład hipotetyczny 266 Spis treœci IX Przykładowy program 267 Emulacja myszy za pomocš klawiatury 270 Dodanie interfejsu klawiatury do programu CHECKER 271 Wykorzystanie okien potomnych do testowania trafienia 275 Okna potomne w programie CHECKER 276 Okna potomne a klawiatura 280 Przechwytywanie myszy 286 Blokowanie prostokšta 286 Przechwycenie : 289 Program BLOKOUT2 290 Kółko myszy 293 Co dalej? 300 Rozdział 8: Zegar 301 Podstawowe informacje o zegarze 302 System i zegar 302 Komunikaty zegarowe nie sš asynchroniczne 303 Zegar: trzy metody wykorzystania 304 Metoda pierwsza 304 Metoda druga 307 Metoda trzecia 310 Wykorzystanie zegara 310 Zegar cyfrowy 311 Pobieranie aktualnego czasu 315 Wyœwietlanie cyfr i dwukropków 316 Być œwiatowcem 316 Zegar analogowy 317 Zegar a raporty 322 Rozdział 9: Kontrolki okna potomnego 327 Klasa przycisków 329 Tworzenie okien potomnych 332 Komunikacja między kontrolkami a oknem nadrzędnym 334 Komunikacja pomiędzy oknem nadrzędnym a kontrolkami 335 Przyciski klasyczne (naciskane) 336 Pola wyboru 336 Przyciski opcji 338 Pola grupy 338 Modyfikacja tekstu przycisku 338 Przyciski widoczne i dostępne 339 Przyciski i fokus 339 Kontrolki a kolory 340 Kolory systemowe 341 X Programowanie Windows Kolory przycisku 342 Komunikat WM CTLCOLORBTN 343 Przyciski rysowane przez program 344 Klasa statyczna 350 Klasa paska przewijania 351 Program COLORS1 353 Automatyczny interfejs klawiatury 359 Zakładanie podklasy okna 360 Kolorowanie tła 360 Kolorowanie pasków przewijania i tekstów statycznych 361 Klasa edycji 362 Siyle klasy edycji 365 Kody powiadamiania kontrolki edycji 366 Posługiwanie się kontrolkš edycji 366 Komunikaty wysyłane do kontrolki edycji 366 Klasa pola listy 367 Style pola listy 368 Umieszczanie łańcuchów w polu listy 369 Zaznaczanie i pobieranie elementów listy 370 Komunikaty wysłane przez pole listy : 371 Prosta aplikacja z polem listy 372 Lista plików 376 Program HEAD dla Windows 377 Rozdział 10: Menu i inne zasoby 383 Ikony, kursory, cišgi znaków i zasoby użytkownika 384 Dodawanie ikony do programu 384 Uzyskiwanie uchwytu ikon .389 Używanie ikon w programie 391 Używanie własnych kursorów 392 Cišgi znaków jako zasoby 393 Zasoby użytkownika 395 Menu 402 Założenia menu 402 Struktura menu 403 Definiowanie menu 403 Odwoływanie się do menu w programie 404 Menu i komunikaty 405 Program przykładowy 407 Konwenanse w menu 412 Definiowanie menu -trudniejszy sposób 413 Ruchome menu rozwijane (menu podręczne) 414 Używanie menu systemowego 419 Zmienianie menu 422 Spis treœci XI Inne polecenia menu 422 Niestandardowe podejœcie do menu 423 Klawisze skrótu 427 Dlaczego należy używać klawiszy skrótu 428 Niektóre zasady przypisywania skrótów 428 Tabela skrótów 429 Wczytywanie tabeli skrótów 429 Tłumaczenie naciœnięć klawiszy 429 Odbieranie komunikatów klawiatury 430 Program POPPAD z menu i klawiszami skrótu 431 Udostępnianie elementów menu 437 Przetwarzanie opcji menu 437 Rozdział 11: Okna dialogowe 441 Modalne okna dialogowe 442 Tworzenie okna dialogowego About 442 Okno dialogowe i jego szablon 446 Procedura okna dialogowego 448 Wyœwietlanie okna dialogowego 449 Wariacje na temat 450 Bardziej złożone okna dialogowe 454 Praca z kontrolkami w oknie dialogowym 460 Przyciski OK i Cancel 462 Unikanie zmiennych globalnych 464 Grupy i miejsca przejœcia tabulatorem 465 Rysowanie w oknie dialogowym 466 Używanie innych funkcji z oknami dialogowymi 467 Definiowanie własnych kontrolek 467 Niemodalne okna dialogowe 474 Różnice między modalnymi i niemodalnymi oknami dialogowymi 475 Program COLORS w nowej wersji 477 Program HEXCALC: okno zwykłe czy dialogowe? 481 Standardowe okna dialogowe 489 Nowa wersja programu POPPAD 489 Operacje wejœcia/wyjœcia na plikach Unicode 509 Zmiana czcionki 509 Wyszukiwanie i zastępowanie 510 Program Windows zawierajšcy tylko jedno wywołanie funkcji 510 Rozdział 12: Schowek 513 Proste zastosowanie Schowka 513 Standardowe formaty danych Schowka 513 Przydzielanie pamięci 515 Przenoszenie tekstu do Schowka 517 XII Programowanie Windows Pobieranie tekstu ze Schowka 518 Otwieranie i zamykanie Schowka 519 Schowek i Unicode 520 Poza standardowymi zastosowaniami Schowka 525 Używanie kilku elementów danych 525 OpóŸnione przenoszenie 526 Własne formaty danych 528 Tworzenie podglšdu Schowka 530 Łańcuch podglšdu Schowka 530 Funkcje i komunikaty podglšdu Schowka 531 Prosty podglšd Schowka 533 Częœć II: Grafika 537 Rozdział 13: Drukowanie 539 Podstawy drukowania 540 Drukowanie a buforowanie 540 Kontekst urzšdzenia drukujšcego 544 Rozszerzony program DEVCAPS 546 Wywołanie PrinterProperties 555 Sprawdzanie cechy BitBlt 556 Najprostszy program drukujšcy 557 Drukowanie grafiki i tekstu 558 Drukowanie konturu 561 Anulowanie wydruku i procedura Abort 562 Jak Windows korzysta z AbortProc 564 Implementacja procedury AbortProc 564 Tworzenie okna dialogowego Printing 567 Drukowanie w programach POPPAD 571 Rozdział 14: Bitmapy i BitBlty 579 Podstawowe wiadomoœci o bitmapach 579 Skšd się biorš bitmapy? 580 Rozmiary bitmap 581 Kolor i bitmapy 582 Urzšdzenia rzeczywiste 582 Obsługa bitmap w GDI 585 Transfer bloków bitowych 586 Prosty BitBlt 586 Rozcišganie bitmap 590 Tryb StretchBlt 593 Operacje rastrowe 593 Blt desenia 596 r-Ľ Spis treœci XIII Obiekt bitmapy GDI 598 Tworzenie DDB 599 Bity bitmapy 601 Kontekst urzšdzenia pamięciowego 602 Wczytywanie bitmap z zasobów 603 Format bitmapy monochromatycznej 607 Bitmapowe pędzle 610 Rysowanie na bitmapach 612 Bitmapa cieniowa 616 Wykorzystanie bitmap w menu 621 Nieprostokštne obrazy bitmapowe 633 Prosta animacja 639 Bitmapy poza oknem 642 Rozdział 15: Bitmapa niezależna od urzšdzeń 653 Format pliku DIB 653 DIB w stylu OS/2 654 Do góry nogami! 657 Bity pikseli DIB 658 Rozszerzony format DIB Windows 659 Realia 662 Kompresja DIB 663 Maskowanie kolorów 666 Nagłówek wersji 4 669 Nagłówek wersji 5 673 Wyœwietlanie informacji DIB 674 Wyœwietlanie i drukowanie 681 Penetracja wnętrza DIB 681 Piksel na piksel 684 Postawiony na głowie œwiat DIB 693 Wyœwietlanie sekwencyjne 701 Rozcišganie i dopasowywanie 708 Konwersja kolorów, palety oraz szybkoœć działania 718 Połšczenie DIB i DDB 719 Tworzenie DDB z DIB 719 Od DDB do DIB 726 Sekcja DIB 727 Więcej o odmiennoœci sekcji DIB 735 Opcja odwzorowania pliku 736 Podsumowanie 737 Rozdział 16: Palette Manager 739 Stosowanie palet 739 Sprzęt wideo 740 XIV Programowanie Windows Wyœwietlanie odcieni szaroœci 741 Komunikaty palety 748 Indeks palety 749 Zapytanie o możliwoœć obsługi palety 752 Paleta systemowa 753 Inne funkcje palety 754 Problem działań rastrowych 754 Przyglšdajšc się palecie systemowej 755 Animacja palety 764 Podskakujšca piłka 765 Animacja jednej pozycji palety 773 Aplikacje inżynierskie 777 Palety i obrazy rzeczywiste 782 Palety i upakowane DIB 782 Paleta wielozadaniowa 792 Paleta półtonowa 798 Indeksowanie palety kolorów 802 Palety i obiekty bitmap 808 Palety i sekcje DIB 813 Biblioteka obsługi DIB 818 Struktura DIBSTRUCT 819 Funkcje informacyjne 821 Odczytywanie i zapisywanie pikseli 827 Tworzenie i konwersja sekcji DIB 831 Plik nagłówkowy DIBHELP i makra 843 Program DIBBLE 845 Palety proste; palety optymalne 868 Konwersja formatów 881 Rozdział 17: Tekst i czcionki 887 Proste wyœwietlanie tekstu 887 Funkcje rysowania tekstu 887 Atrybuty kontekstu urzšdzenia dla tekstu 890 Czcionki zapasowe 891 Czcionki - informacje podstawowe 892 Typy czcionek 892 Czcionki TrueType 893 Atrybuty czy style? 894 Rozmiar punktowy 895 Interlinia i odstępy między znakami 895 Problem logicznego cala 895 Czcionka logiczna 897 Tworzenie i wybieranie czcionki logicznej 897 Program PICKFONT 898 Spis treœci XV Struktura czcionki logicznej 912 Algorytm odwzorowania czcionek 917 Uzyskiwanie informacji o czcionce 918 Zestawy znaków i Unicode 919 System EZFONT 921 Obracanie czcionki 929 Wyliczanie czcionek 931 Funkcje wyliczajšce 931 Okno dialogowe ChooseFont 932 Formatowanie akapitu 940 Proste formatowanie tekstu 941 Praca z akapitami 942 Podglšd wydruku 950 Trochę œmiesznych i fantazyjnych efektów 961 Œcieżka GDI 961 Pisaki o rozszerzonych możliwoœciach 962 Cztery przykładowe programy 966 Rozdział 18: Metapliki 973 Stary format metapliku 974 Proste zastosowanie metaplików przechowywanych w pamięci 974 Zapisywanie metaplików na dysku 977 Stary format metapliku i Schowek 978 Rozszerzone metapliki 982 Procedura podstawowa 983 Zajrzyjmy do œrodka 986 Metapliki i obiekty GDI 993 Metapliki i bitmapy 998 Wyliczanie metapliku 1001 Osadzanie obrazów 1008 Podglšd metapliku rozszerzonego i wydruki 1012 Wyœwietlanie szczegółów w metaplikach 1021 Skalowanie i proporcje rozmiarów 1031 Tryby odwzorowania i metapliki 1033 Odwzorowanie i odiwarzanie 1035 Częœć III: Zagadnienia zaawansowane 1041 Rozdział 19: Interfejs wielodokumentowy 1043 Koncepcja MDI 1043 Elementy MDI 1043 Obsfuga MDI 1045 Przykładowa implementacja MDI 1047 XVI Programowanie Windows Trzy menu 1057 Inicjacja programu 1058 Tworzenie okien potomnych 1059 Przetwarzanie komunikatu Więcej okien 1060 Okna potomne, okna dokumentów 1061 Sprzštanie 1063 Rozdział 20: Wielozadaniowoœć i wielowštkowoœţ 1065 Historia wielozadaniowoœci 1066 Wielozadaniowoœć w systemie DOS? 1066 Wielozadaniowoœć bez wywtaszczania 1066 Menedżer prezentacji i szeregowa kolejka komunikatów 1068 Rozwišzanie wielowštkowe 1068 Architektura wielowštkowa 1069 Ucišżliwoœć wštków 1070 Przewaga Windows 1071 Nowoœć! Ulepszona formuła! Dostępne z wštkami! 1072 Wielowštkowoœć w Windows 1072 Losowe prostokšty raz jeszcze 1073 Zadanie z konkursu programistycznego Microsoftu 1076 Rozwišzanie wielowštkowe 1082 Jakieœ problemy? 1090 Zalety snu 1091 Synchronizacja wštków 1092 Sekcja krytyczna 1092 Sygnalizowanie zdarzeń 1094 Program BIGJOB1 1094 Obiekt zdarzenia 1099 Lokalna pamięć wštku (TLS) 1103 Rozdział 21: Biblioteki dynamiczne 1107 Podstawowe informacje o bibliotekach 1107 Biblioteka - temat-rzeka 1108 Przykładowy DLL 1109 Punkt wejœcia i wyjœcia biblioteki 1112 Program testowy 1113 Biblioteki dynamiczne a pamięć wspólna 1116 Program STRPROG 1121 Współużytkowanie danych przez instancje programu STRPROG 1126 Różne tematy zwišzane z bibliotekami dynamicznymi 1126 Dynamiczna konsolidacja bez importu 1127 Biblioteki z samymi zasobami 1128 Spis treœci XVII Rozdział 22: DŸwięk i muzyka 1133 Windows i multimedia 1133 Urzšdzenia systemów multimedialnych 1133 Przeglšd API 1134 Eksplorujemy MCI - program TESTMCI 1135 MCITEXT a CD audio 1140 DŸwięk wave 1143 DŸwięk a kształt fali . 1144 Modulacja kodowo-impulsowa 1145 Częstotliwoœć próbkowania 1146 Rozmiar próbki 1147 Programowe generowanie sygnałów sinusoidalnych 1148 Cyfrowy odtwarzacz dŸwięku 1157 Rozwišzanie alternatywne z zastosowaniem MCI 1168 Rozwišzanie ze znakowymi poleceniami MCI 1176 Format pliku wave 1180 Eksperymenty z syntezš addytywnš 1181 Budzenie za pomocš dŸwięków wave 1190 Muzyka MIDI 1198 Działanie MIDI 1198 Zmiana programu 1200 Kanały MIDI 1200 Komunikaty MIDI 1201 Wprowadzenie do sekwencji MIDI 1203 Symulowanie syntezatora MIDI z klawiatury PC 1210 Automat perkusyjny MIDI 1225 Multimedialne funkcje zwišzane z czasem 1245 Operacje na plikach typu RIFF 1248 Rozdział 23: Smak Internetu 1253 Gniazda Windows 1253 Gniazda a TCP/IP 1253 Sieciowe usługi zwišzane z czasem 1254 Program NETTIME 1255 Winlnet i FTP 1267 Przeglšd FTP API 1268 Program demonstrujšcy œcišganie plików z serwera.FTP 1269 Indeks 1281 auto ra Aktualne informacje na temat tej ksišżki, ze sprostowaniami ewentualnych błę- dów i nowymi wydrukami kodów włšcznie, znajdziesz w moim oœrodku WWW pod adresem www.cpetzold.com. Na tematy zwišzane z ksišżkš możesz napisać do mnie na adres charles@cpetzold.com. Postaram się odpowiedzieć na prostsze pytania, chociaż niczego nie mogę obiecać. Jestem zwykle bardzo zajęty, a mój kot nie chce się uczyć Windows API. Chciałbym podziękować wszystkim pracownikom wydawnictwa Microsoft Press za wielkš pracę włożonš w tę ksišżkę. Sšdzę, że to wydanie, upamiętniajšce dzie- sištš rocznicę pierwszego wydania Programowania Windows, jest najlepsze. Słowa wdzięcznoœci należš się także tym osobom z Microsoftu (włšcznie z niektórymi pierwszymi projektantami Windows), którzy służyli mi pomocš przy wczeœniej- szych wydaniach ksišżki i tam zostali wymienieni. Pragnę też podziękować rodzinie i przyjaciołom, a w szczególnoœci tym (wiesz, o kim mówię!), którzy umożliwili powstanie tej ksišżki. Właœnie wam jš dedykuję. Charles Petzold 5 paŸdziernika 1998 roku Częœć I o s aw Rozdział 2 Z z nam ac Ta ksišżka pokazuje, jak możesz tworzyć programy, które działajš w œrodowisku Windows 98, Windows NT 4.0 i Windows NT 5.0 Microsoftu. Takie programy sš pisane w języku programowania C i wykorzystujš rodzimy interfejs programo- _ _ wania aplikacji Windows (ang. application programming interface, API). Jak dalej wyjaœnię, nie jest to jedyny sposób tworzenia programów, które działajš w tym œrodowisku. Jednak bardzo ważne jest zrozumienie API Windows, niezależnie od tego, w czym ostatecznie będziesz pisał swój kod. jak zapewne wiesz, Windows 98 jest najnowszym wcieleniem graficznego syste- mu operacyjnego, który w rzeczywistoœci stał się standardem dla komputerów osobistych kompatybilnych z IBM-PC wykorzystujšcych 32-bitowe mikroproce- sory Intela, jak 486 oraz Pentium. Windows NT to bardziej rozbudowana, prze- mysłowa wersja Windows, która działa zarówno na komputerach kompatybil- nych z PC, jak też na niektórych RISC-owych stacjach roboczych. Po tę ksišżkę możesz sięgnšć, jeœli spełniasz trzy podstawowe warunki. Po pierw- sze, powinieneœ znać Windows z perspektywy użytkownika. Nie masz co liczyć na napisanie aplikacji dla Windows, jeœli nie poznałeœ interfejsu użytkownika. Z tego powodu sugeruję, abyœ wykonywał swoje prace prograniistyczne (oraz inne prace) na komputerze opartym na Windows, używajšc aplikacji działajšcych w tym œrodowisku. Po drugie, powinieneœ znać C. Jeżeli nie znasz, programowanie Windows praw- dopodobnie nie jest najlepszš okazjš do studiowania tego języka. Radzę ci, byœ uczył się C w œrodowisku znakowym, oferowanym w oknie wiersza poleceń MS- DOS-a w Windows. Programowanie Windows czasem wykorzystuje takie aspekty C, których nie sposób pokazać w programowaniu znakowym. W takich wypad- kach omówię je bardziej szczegółowo. Zazwyczaj jednak wystarcza znajomoœć języka, w szczególnoœci struktur i wskaŸników C. Pewne obycie ze standardowš bibliotekš C jest przydatne, ale niekonieczne. Po trzecie, powinieneœ zainstalować na swoim komputerze 32-bitowy kompila- tor C i œrodowisko projektowe odpowiednie do pisania programów Windows. Przyjmuję, że używasz Visual C++ 6.0 Microsoftu, który możesz nabyć osobno lub jako częœć pakietu Visual Studio 6.0. I to już wszystko. Zakładam, że nie masz żadnego doœwiadczenia w programo- waniu w œrodowisku graficznym, takim jak Windows. Częœć I: Podstawy Œrodowisko Windows Windows wymaga wprowadzenia. Łatwo zapominamy, jak bardzo pod jego wpły- wem zmienił się sposób obsługi komputerów osobistych. Windows pokonał wy- boistš drogę wczesnych lat dziecięcych i zdobył mocnš pozycję na rynku. Historia Windows Wkrótce po wprowadzeniu jesieniš 1981 roku komputerów IBM PC stało się ja- sne, że dominujšcym systemem operacyjnym dla pecetów (i zgodnych z nimi) będzie MS-DOS, czyli w pełnym brzmieniu Microsoft Disk Operating System (dys- kowy system operacyjny Microsoftu). MS-DOS był minimalnym systemem ope- racyjnym. Użytkownikom umożliwiał dostęp (za poœrednictwem interfejsu wier- sza poleceń) do takich poleceń, jak DIR i TYPE, oraz ładowanie do pamięci pro- gramów aplikacji w celu ich wykonania. Programistom aplikacji oferował niewiele więcej ponad zestaw wywołań funkcji do wykonywania operacji plikowego wej- œcia-wyjœcia (I/O). Przy innych zadaniach - w szczególnoœci przy wyœwietlaniu na ekranie tekstu i (czasami) grafiki - aplikacje wykorzystywały bezpoœredni do- stęp do sprzętu. Na skutek ograniczeń pamięciowych i sprzętowych skomplikowane œrodowisko graficzne z trudem przenikało do małych komputerów. Apple Computer zaofe- rował alternatywę w stosunku do œrodowiska znakowego, wprowadzajšc w stycz- niu 1983 roku swój nieudany komputer Lisa, a w styczniu 1984 roku ustanowił standard dla œrodowiska graficznego za pomocš modelu Macintosh. Pomimo malejšcego znaczenia na rynku, cišgle stanowi on wzorzec, z którym porówny- wane sš inne œrodowiska graficzne. Wszystkie œrodowiska graficzne, z Macinto- shem i Windows włšcznie, korzystajš z wyników pionierskich prac prowadzo- nych w połowie lat siedemdziesištych w oœrodku badawczo-rozwojowym Xero- xa - Palo Alto Research Center (PARC). W listopadzie 1983 roku (po Lisie, ale przed Macintoshem) firma Microsoft Cor- poration zapowiedziała system Windows. Pojawił się on na rynku dwa lata póŸ- niej, w listopadzie 1985 roku. Przez następne dwa lata Windows 1.0 był stopnio- wo uzupełniany o dodatkowe sterowniki adapterów graficznych i drukarek w celu spełnienia wymagań rynku międzynarodowego. Windows w wersji 2.0 pojawił się w listopadzie 1987. Sporo zmieniono w inter- fejsie użytkownika. Przede wszystkim stało się możliwe zachodzenie na siebie okien, a nie tylko ustawianie ich sšsiadujšco, jak w wersji Windows 1.0. Windows 2.0 zawierał też rozszerzenia interfejsu klawiatury i myszy, ułatwiajšce posługi- wanie się menu i oknami dialogowymi. Ówczesny Windows wymagał tylko mikroprocesora Intel 8086 lub 8088 pracujš- cego w trybie rzeczywistym, który dawał dostęp do 1 megabajta (MB) pamięci operacyjnej. Windows/386 (który pojawił się wkrótce po wersji 2.0) wykorzysty- wał już tryb wirtualny mikroprocesorów 386 Intela do uruchamiania w oknach wielu programów dosowych, majšcych bezpoœredni dostęp do zasobów sprzęto- wych. Dla zachowania zgodnoœci nazw przemianowano wtedy Windows 2.1 na Windows/286. Rozdział 1: Zaczynamy Windows 3.0 został przedstawiony 22 maja 1990 roku. Łšczył on w jeden pro- dukt wczeœniejsze wersje Windows/286 i Windows/386. Dużš zmianš w Win- dows 3.0 było wykorzystanie 16-bitowego trybu chronionego mikroprocesorów Intel 286, 386 i 486. Umożliwiło to samemu systemowi i jego aplikacjom dostęp do 16 megabajtów pamięci. Całkowicie zmieniono jšdro systemu uruchamiajšce programy i zarzšdzajšce plikami. Windows 3.0 był pierwszš wersjš Windows, która zyskała szersze zastosowanie w domu i biurze. Każda historia Windows musi zawierać wzmiankę o OS/2, systemie alternatyw- nym dla DOS-a i Windows, który został opracowany przez Microsoft we współ- pracy z IBM-em. OS/2 w wersji 1.0 (działajšcy tylko w trybie znakowym) praco- wał na procesorach 286 (i póŸniejszych) Intela, a pojawił się pod koniec 1987 roku. Interfejs graficzny o nazwie Presentation Manager (PM) pojawił się dopiero w wersji 1.1 OS/2, w paŸdzierniku 1988 roku. Pierwotnie miał on stanowić wersję Win- dows pracujšcš w trybie chronionym, ale interfejs API został zmieniony do tego stopnia, że producenci oprogramowania nie byli w stanie obsłużyć jednoczeœnie obu platform. Konflikt pomiędzy IBM-em i Microsoftem osišgnšł punkt kulminacyjny we wrze- œniu 1990 roku i drogi obu firm rozeszły się. IBM przejšł OS/2, a Microsoft skon- centrował się na Windows. Chociaż OS/2 miewa jeszcze zagorzałych zwolenni- ków, nigdy nie osišgnšł popularnoœci choćby zbliżonej do Windows. Wersja 3.1 Windows Microsoftu pojawiła się w kwietniu 1992 roku. Najważniej- szymi ulepszeniami były: TrueType (skalowalne czcionki Windows), multimedia (dŸwięk i muzyka), OLE (Object Linking and Embedding) i standaryzacja podsta- wowych okien dialogowych. Windows 3.1 działał tylko w trybie chronionym i wy- magał procesora 286 lub 386 oraz co najmniej 1 MB pamięci. Windows NT, wprowadzony w lipcu 1993 roku, był pierwszš wersjš Windows umożliwiajšcš korzystanie z trybu 32-bitowego procesorów Intela 386, 486 i Pen- tium. Programy uruchamiane pod Windows NT majš dostęp do 32-bitowej pła- skiej przestrzeni adresowej i używajš zestawu 32-bitowych instrukcji. (O prze- strzeni adresowej powiem więcej w dalszej częœci tego rozdziału). Windows NT został zaprojektowany jako system przenoœny na procesory nie-Intelowskie i działa na wielu RISC-owych stacjach roboczych. Windows 95 został wprowadzony w sierpniu 1995 roku. Podobnie jak Windows NT, Windows 95 także umożliwia 32-bitowy tryb programowania procesorów Intela 386 i póŸniejszych. Chociaż nie posiada niektórych cech Windows NT, ta- kich jak wysoki poziom bezpieczeństwa i przenoœnoœć na maszyny RISC-owe, ma jednak mniejsze wymagania sprzętowe. Windows 98 pojawił się w czerwcu 1998 roku; zawarte w nim ulepszenia zwięk- szajš wydajnoœć oraz umożliwiajš lepszš współpracę ze sprzętem i większš inte- grację z Internetem i WWW. Cechy Windows Windows 98 i Windows NT sš 32-bitowymi graficznymi systemami operacyjny- mi, które cechuje wielozadaniowoœć z wywłaszczaniem (ang. preemptive multita- sking) i wielowštkowoœć (ang. multithreading). Windows ma graficzny interfejs Częœć I: Podstawy użytkownika (ang. graphical user interface, GUI), czasem nazywany interfejsem wizualnym lub okienkowym œrodowiskiem graficznym. Podstawy GUI stworzo- no w połowie lat siedemdziesištych w oœrodku PARC Xeroxa na komputerach Alto i Star w œrodowisku typu SmallTalk. Te prace uzyskały póŸniej większe zna- czenie i zostały spopularyzowane przez Apple Computer i Microsoft. Chociaż niegdyœ wydawało się to dosyć kontrowersyjne, obecnie całkiem oczywiste jest że GUI to - jak uważa Charles Simonyi z Microsoftu - "najdonioœlejszy consen- sus" przemysłu komputerów osobistych. Wszystkie GUI używajš grafiki do wyœwietlania rastrowego. Grafika pozwala na lepsze wykorzystanie obszaru ekranu, zapewnia ciekawe œrodowisko wizualne dla przenoszenia informacji i możliwoœć uzyskania WYSIWYG (ang. zvhat you see is what you get - otrzymasz to, co widzisz), czyli pełnej zgodnoœci wyglšdu do- kumentu na ekranie z jego wydrukiem. Dawniej na ekranie można było jedynie wyœwietlić tekst wprowadzany przez użytkownika z klawiatury. W nowym typie interfejsu ekran staje się dla użyt- kownika urzšdzeniem wejœciowym. Na ekranie pokazywane sš różne obiekty graficzne w formie ikon i urzšdzenia wejœciowe, jak przyciski i paski przewija- nia. Posługujšc się klawiaturš (lub urzšdzeniami wskazujšcymi typu mysz) użyt- kownik może bezpoœrednio manipulować tymi obiektami na ekranie. Obiekty graficzne mogš być przecišgane, przyciski - przyciskane, a suwaki pasków prze- wijania - przesuwane. Interakcja między użytkownikiem i programem staje się bliższa. Zamiast jedno- stronnego przesyłania informacji z klawiatury do programu w celu wyœwietlenia na ekranie, użytkownik bezpoœrednio oddziałuje na obiekty na ekranie, a te od- powiadajš na jego polecenia i ukierunkowujš kolejne kroki. Nikt nie lubi spędzać długich godzin na nauce obsługi komputera lub nowego programu. Windows wychodzi nam naprzeciw, gdyż wszystkie aplikacje majš zbliżony wyglšd na ekranie i opierajš swoje działanie na podobnych zasadach. Program zajmuje zwykle na ekranie prostokštny obszar, zwany oknem. Każde okno jest identyfikowane przez pasek tytułu. Większoœć funkcji programu jest do- stępna w jego menu. Użytkownik może zobaczyć informację, która nie mieœci się na ekranie, wykorzystujšc paski przewijania. Niektóre pozycje menu wywołujš okna dialogowe, w których użytkownik może wprowadzić dodatkowe informa- cje. Jedno z okien dialogowych (używane do otwierania pliku) można znaleŸć w każdym większym programie. To okno dialogowe wyglšda tak samo (albo pra- wie tak samo) we wszystkich programach i prawie zawsze jest wywoływane przez tę samš opcję menu. Ponieważ wiesz już, jak używać programów Windows, łatwo możesz nauczyć się dalszych funkcji. Menu i okna dialogowe pozwalajš użytkownikowi ekspery- mentować z nowym programem i odkrywać jego możliwoœci. Większoœć progra- mów Windows może być obsługiwana na dwa sposoby: z klawiatury i za pomo- cš myszy. Chociaż większoœć funkcji programów jest dostępna z klawiatury, to posługiwanie się myszš jest zdecydowanie wygodniejsze. Z perspektywy programisty spójny interfejs użytkownika wynika z użycia wbu- dowanych procedur do konstrukcji menu i okien dialogowych. Wszystkie menu Rozdział 1: Zaczynamy 7 obsługuje się za pomocš klawiatury i myszy w ten sam sposób, ponieważ to Windows, a nie aplikacja odpowiada za ich działanie. Aby ułatwić jednoczesne korzystanie z wielu programów i wymianę informacji między nimi, Windows oferuje wielozadaniowoœć. Wiele programów może działać i wyœwietlać wyniki w tym samym czasie. Każdy program posiada na ekranie swoje okno. Użytkownik może przesuwać okna po ekranie, zmieniać ich rozmia- ry, przełšczać się między różnymi programami i przenosić dane z jednego pro- gramu do drugiego. Ponieważ te okna przypominajš trochę papiery rozłożone na pulpicie biurka (zanim, oczywiœcie, biurka zostały opanowane przez kompu- tery), Windows czasem używa metafory "pulpit" (ang. desktop) dla widoku wie- lu programów na ekranie. Wczeœniejsze wersje Windows używały tzw. wielozadaniowoœci bez wywłaszcza- nia (ang. nonpreemptive multitasking). Oznaczało to, że Windows nie używał ze- gara systemowego do dzielenia czasu procesora między uruchomione programy. Musiały one dobrowolnie oddać sterowanie, aby inne programy mogły działać. W Windows NT i Windows 98 występuje wielozadaniowoœć z wywłaszczaniem i programy mogš same dzielić się na wiele wštków, które wykonujš się prawie jednoczeœnie. System operacyjny nie może realizować wielozadaniowoœci, jeœli nie zarzšdza pamięciš. W miarę jak uruchamiane sš nowe programy, a stare się kończš, pa- mięć ulega fragmentacji. System musi konsolidować wolnš pamięć. Wymaga to przemieszczania bloków kodu i danych w pamięci. Nawet Windows 1.0, pracujšcy na procesorze 8088, potrafił w ten sposób zarzš- dzać pamięciš. Majšc na uwadze ograniczenia trybu rzeczywistego, możemy uznać to za majstersztyk autorów systemu. Tak oto ograniczenie pamięci archi- tektury PC do 640 kilobajtów (KB) zostało skutecznie przełamane bez dokłada- nia dodatkowej pamięci. Ale Microsoft poszedł dalej: Windows 2.0 dawał aplika- cjom dostęp do pamięci rozszerzonej (EMS), a Windows 3.0 pracował w trybie chronionym dajšc aplikacjom dostęp do 16 MB pamięci rozszerzonej. Windows NT i Windows 98, jako 32-bitowe systemy operacyjne z płaskš przestrzeniš adre- sowš, przełamały te ograniczenia. Programy pracujšce w Windows mogš współużytkować procedury, które znaj- dujš się w innych plikach nazywanych bibliotekami dynamicznymi (ang. dyna- miclink libraries, DLL). Windows zawiera mechanizm łšczšcy procedury w DLL- ach z programem podczas jego pracy. Sam system zasadniczo jest także zbiorem takich bibliotek. Windows to interfejs graficzny i programy windowsowe mogš w pełni wykorzy- stywać grafikę i sformatowany tekst na ekranie i w drukarce. Interfejs graficzny nie tylko jest bardziej atrakcyjny, ale może dostarczyć użytkownikowi więcej in- formacji. Programy pisane w œrodowisku Windows nie majš bezpoœredniego dostępu do kart graficznych i drukarek. W zamian Windows zawiera język programowania grafiki, nazywany GDI (ang. Graphics Display Interface), pozwalajšcy na łatwe wyœwietlanie grafiki i sformatowanego tekstu. Windows wirtualizuje urzšdze- nia wyœwietlajšce. Program powinien działać na każdej karcie graficznej i każdej 8 Częœć I: Podstawy drukarce, dla których dostępne sš sterowniki Windows. Nie potrzebuje sam okre- œlać rodzaju sprzętu dołšczonego do systemu. Stworzenie dla pecetów interfejsu graficznego, niezależnego od urzšdzeń, nie było łatwym zadaniem dla projektantów Windows. Z założenia PC był systemem o otwartej architekturze. Wielu niezależnych wytwórców opracowywało więc urzšdzenia peryferyjne. Chociaż powstało kilka standardów, konwencjonalne pro- gramy MS-DOS-a musiały indywidualnie obsługiwać wiele różnych konfiguracji sprzętowych. Najczęœciej dotyczyło to programów do obróbki tekstu, sprzeda- wanych z kilkoma dyskietkami małych plików, z których każdy służył do obsłu- gi konkretnego typu drukarki. Programy windowsowe nie wymagajš takich ste- rowników, ponieważ stanowiš one częœć systemu operacyjnego. Łšczenie dynamiczne Zasada pracy Windows oparta jest na koncepcji łšczenia dynamicznego (ang. dynamic linking). Windows dostarcza mnóstwa wywołań funkcji, które aplikacja może wykorzystać przy realizacji interfejsu użytkownika i wyœwietlaniu tekstu i grafiki. Funkcje te zostały umieszczone w dynamicznych bibliotekach, inaczej DLL-ach. Sš to pliki o rozszerzeniach .DLL lub czasami .EXE, przeważnie znaj- dujšce się w podkatalogu \WINDOWS\SYSTEM Windows 98 oraz podkatalo- gach \WINNT\SYSTEM i \WINNT\SYSTEM32 Windows NT. Dawniej duża częœć Windows była zrealizowana w postaci zaledwie trzech bi- bliotek dynamicznych. Reprezentowały one trzy podstawowe podsystemy Win- dows, czyli Kernel, User i GDI. Iloœć podsystemów wzrosła w ostatnich wersjach Windows, ale większoœć funkcji, których używa typowy program, znajduje się nadal w tych trzech modułach. Kernel (aktualnie zrealizowany w 16-bitowym KRNL386.EXE i 32-bitowym KERNEL32.DLL) obsługuje wszystkie funkcje jšdra systemu operacyjnego, jak zarzšdzanie pamięciš, plikowe I/O i uruchamianie zadań. User (zrealizowany w 16-bitowym USER.EXE i 32-bitowym USER32.DLL) obsługuje interfejs użytkownika i całš logikę okien. GDI (zrealizowany w 16-bi- towym GDI.EXE i 32-bitowym GDI32.DLL) jest interfejsem urzšdzeń graficznych, pozwalajšcym programowi wyœwietlić tekst i grafikę na ekranie lub wydruko- wać na drukarce. Windows 98 oferuje aplikacjom kilka tysięcy funkcji do wykorzystania. Każda funkcja posiada opisowš nazwę, na przykład CreateWindozu. Ta funkcja (jak mo- żesz się spodziewać) tworzy okno twojego programu. Wszystkie funkcje Windows, których może używać aplikacja, sš zadeklarowane w plikach nagłówkowych. W swoim programie będziesz korzystał z wywołań funkcji Windows praktycz- nie w ten sam sposób, w jaki posługujesz się funkcjami bibliotecznymi C, takimi jak strlen. Podstawowš różnicš jest to, że kod maszynowy dla funkcji bibliotecz- nych C jest włšczony do kodu twojego programu, podczas gdy kod funkcji Win- dows jest umieszczony na zewnštrz twojego programu, w DLL-ach. Kiedy uruchamiasz program windowsowy, łšczy się on z Windows poprzez proces zwany łšczeniem dynamicznym (ang. dynamic linking). Plik EXE w Windows za- wiera odnoœniki do różnych bibliotek dynamicznych i do znajdujšcych się w nich funkcji wykorzystywanych w programie. Gdy program jest ładowany do pamię- Rozdział 1: Zaczynamy 9 ci, odwołania programu sš odwzorowane na wejœcia funkcji w bibliotekach, któ- re sš w miarę potrzeb ładowane do pamięci. Kiedy łšczysz program, by stworzyć plik wykonywalny, musisz połšczyć go ze specjalnymi bibliotekami importowymi, dostarczanymi razem ze œrodowiskiem programistycznym. Te biblioteki importowe zawierajš nazwy bibliotek dynamicz- nych i informację o wszystkich wywołaniach funkcji Windows. Konsolidator (ina- czej linker) używa tej informacji do zbudowania tabeli w pliku EXE, którš Win- dows wykorzystuje do odwzorowania wywołań na funkcje Windows podczas ła- dowania programu. Opcje programowania Windows Ta ksišżka zawiera dużo przykładowych programów, które pozwolš zilustrować różne techniki programowania w Windows. Sš one napisane w C i używajš czystego API Windows. Jest to klasyczny sposób programowania w Windows. Tak pisaliœmy pro- gramy dla Windows 1.0 w 1985 roku, i tak można to robić również dzisiaj. API i modele pamięci Z punktu widzenia programisty system operacyjny jest definiowany przez swoje API. Obejmuje ono wszystkie wywołania funkcji, które może wykorzystywać apli- kacja oraz definicje powišzanych typów danych i struktur. W œrodowisku Windows API wymusza pewnš architekturę programu, którš wyjaœnimy w następnych roz- działach. W zasadzie API Windows niewiele się zmieniło od czasów Windows 1.0. Dla pro- gramisty, który ma do czynienia z Windows 98, kod Ÿródłowy programów dla Windows 1.0 będzie wyglšdał znajomo. API zostało tylko znacznie rozszerzone. Windows 1.0 zawierał mniej niż 450 funkcji; obecnie sš ich tysišce. Największš zmianę w API Windows i jego składni spowodowało przejœcie z ar- chitektury 16-bitowej na 32-bitowš. Wersje Windows od 1.0 do 3.1 posługiwały się pamięciš segmentowš trybu 16-bitowego procesorów Intel 8086, 8088 i 286. Tryb ten jest w dalszym cišgu używany dla zapewnienia kompatybilnoœci w 32- bitowych procesorach Intela, poczšwszy od 386. Wielkoœć rejestru procesora w tym trybie wynosiła 16 bitów, więc typ danych int w C zawierał także 16 bitów. W segmentowym modelu pamięci adresy pamięci składały się z dwóch częœci: 16-bitowego wskaŸnika segmentu (ang. segment pointer) i 16-bitowego wskaŸni- ka przesunięcia (ang. offset pointer). Z perspektywy programisty było to bardzo skomplikowane i wymagało rozróżniania wskaŸników long lub far (zawierajšcych segment i przesunięcie adresu) i wskaŸników short lub near, zawierajšcych jedy- nie przesunięcie adresu (segment adresu był domyœlny). Już w Windows NT i Windows 95 skorzystano z 32-bitowego płaskiego modelu pamięci, używajšc 32-bitowych trybów procesorów Intela 386, 486 i Pentium. Typ danych int w C został rozszerzony do 32 bitów. Programy napisane dla 32-bito- wych wersji Windows używajš prostych, 32-bitowych wartoœci wskaŸników, które adresujš płaskš, liniowš przestrzeń adresowš. 10 Częœć I: Podstawy API dla 16 bitowych wersji Windows (od 1.0 do 3.1) jest znane teraz jako Winló. API dla 32-bitowych wersji Windows (Windows 95, Windows 98 i wszystkich wersji Windows NT) nazywane jest Win32. Przy przejœciu z Winl6 do Win32 wiele funkcji pozostało takich samych, ale niektóre musiały być rozszerzone lub zmie- nione. Na przykład współrzędne graficzne zmieniły się z 16-bitowych w Win16 na 32-bitowe w Win32. Ponadto niektóre funkcje Win16 zwracały dwie współ- rzędne, spakowane w 32-bitowš liczbę całkowitš. Takie rozwišzanie było niemoż- liwe w Win32, więc dodano nowe funkcje, które działały nieco inaczej. Wszystkie 32-bitowe wersje Windows zawierajš zarówno API Win16 zapewniajš- ce kompatybilnoœć ze starymi aplikacjami, jak i API Win32 do uruchamiania no- wych aplikacji. Ciekawe, że cały ten proces działa inaczej w Windows NT niż w Windows 95 i Windows 98. W Windows NT wywołania funkcji Win16 przechodzš przez warstwę translacji i sš zamieniane na wywołania funkcji Win32, obsługiwa- nych przez system operacyjny. W Windows 95 i Windows 98 proces przebiega od- wrotnie: wywołania funkcji Win32 przechodzš przez warstwę translacji i sš zamie- niane na wywołania funkcji Win16, obsługiwanych przez system operacyjny. Przez pewien czas istniały dwa różne (przynajmniej z nazwy) zestawy API Win- dows. Win32s, gdzie "s" oznaczało podzbiór (ang. subset) pozwalało programi- œcie na pisanie 32-bitowych aplikacji dla Windows 3.1. To API zawierało tylko 32-bitowe wersje funkcji już zrealizowanych w Win16. API Windows 95 było na- zywane przez pewien czas Win32c, gdzie "c" oznaczało kompatybilnoœć (ang. compatibility), ale to okreœlenie przestało być używane. Teraz przyjmuje się, że zarówno Windows NT, jak i Windows 98 umożliwiajš ko- rzystanie z API Win32. Jednoczeœnie każdy z tych systemów operacyjnych ma pewne cechy własne niedostępne w drugim. Ponieważ jednak przeważajš funk- cje wspólne, możliwe jest pisanie programów, które działajš w obu systemach. Uważa się, że te dwa produkty stanš się kiedyœ jednym systemem operacyjnym. Opcje języka programowania Używanie C i czystego API nie jest wprawdzie jedynym sposobem pisania pro- gramów Windows 98, pozwala jednak na najwszechstronniejsze i najefektywniej- sze wykorzystanie cech tego systemu. Programy wykonywalne sš względnie małe i nie wymagajš do pracy zewnętrznych bibliotek (poza DLL-ami samych Win- dows, oczywiœcie). A co najważniejsze, poznanie API umożliwi ci głębsze zrozu- mienie budowy wewnętrznej Windows, niezależnie od tego, jak ostatecznie bę- dziesz pisał aplikacje dla tego systemu. Sšdzę, że nauka klasycznego programowania Windows jest ważna dla każdego pro- gramisty, nie zmuszam cię jednak do stosowania C i API w każdej aplikacji Win- dows. Wielu programistów - szczególnie opracowujšcych programy do wewnętrz- nego użytku firmy lub programujšcych hobbystycznnie w domu - docenia prosto- tę takich œrodowisk projektowych, jak Visual Basic Microsoftu czy Delphi Borlanda (które jest obiektowym dialektem Pascala). Te œrodowiska pozwalajš programiœcie skupić uwagę na interfejsie użytkownika i dołšczajš kod z obiektami interfejsu użyt- kownika. Jeœli chcesz nauczyć się Visual Basica, możesz skorzystać z innych ksiš- żek wydawnictwa Microsoft Press, na przykład Learn Visual Basic Now (1996), na- pisanej przez Michaela Halvorsona. Rozdział 1: Zaczynamy 11 Wœród zawodowych programistów - zwłaszcza piszšcych aplikacje handlowe - szczególnie popularnš alternatywš stał się w ostatnich latach Visual C++ Micro- softu z bibliotekš klas MFC (Microsoft Foundation Class Library). MFC hermetyzu- je wiele trudniejszych zagadnień programowania Windows w zbiorze klas C++. Podręcznikiem MFC jest ksišżka Jeffa Prosise'a Programming Windows with MFC Second Edition (Microsoft Press, 1999). Rosnšca popularnoœć Internetu i WWW spowodowała ostatnio duże zainteresowa- nie językiem Java firmy Sun Microsystems. Jest to język niezależny od procesora, inspirowany przez C++ i zawierajšcy pakiet narzędziowy do pisania aplikacji gra- ficznych, które mogš działać w wielu systemach operacyjnych. Dobrš ksišżkš Mi- crosoft Press o J++ Microsoftu (œrodowisku projektowym Javy Microsoftu) jest Pro- gramming Visual j++ 6.0 (1999), napisana przez Stephena R. Davisa. Trudno jest wskazać ten jedyny, właœciwy sposób pisania aplikacji dla Windows. Prawdopodobnie sam rodzaj pisanej aplikacji okreœli narzędzia. Ale poznanie API umożliwi ci wglšd w pracę Windows, która nie zależy od używanej przez ciebie metody kodowania. Windows to złożony system; umieszczenie warstwy opro- gramowania ponad API nie eliminuje złożonoœci - pozwala tylko na jej ukrycie. Prędzej czy póŸniej ta złożonoœć da znać o sobie. Znajomoœć API ułatwi ci przy- wrócenie poprawnego działania aplikacji. Każda dodatkowa warstwa oprogramowania ponad API ogranicza cię do podzbioru jego pełnej funkcjonalnoœci. Na przykład możesz stwierdzić, że Visual Basic jest idealny dla twojej aplikacji, z wyjštkiem tego, że nie pozwala na kilka istotnych prac pomocniczych. W tym wypadku powinieneœ użyć wywołań czystego API. API definiuje wszechœwiat, w którym istniejemy jako programiœci Windows. Nic nie otworzy przed tobš takich możliwoœci, jak bezpoœrednie wykorzystanie API. Szczególnie problematyczne jest zastosowanie MFC. Chociaż bardzo upraszcza wykonywanie wielu zadań (takich jak OLE), zwykle musiałem męczyć się z inny- mi jego cechami (takimi jak architektura dokument/widok), aby zmusić je do dzia- łania po mojej myœli. MFC nie stało się panaceum dla programowania Windows, chociaż wiele osób miało nadzieję, że tak będzie. Mało kto uzna je za przykład do- brego projektu obiektowego. Programiœci MFC, chcšc zrozumieć, co się dzieje w używanych przez nich klasach, zmuszeni sš często zaglšdać do kodu Ÿródłowego. Zrozumienie kodu Ÿródłowego to jedna z korzyœci nauczenia się API Windows. Œrodowisko programistyczne W tej ksišżce przyjšłem, że używasz Visual C++ 6.0 Microsoftu, który jest do- stępny w wersji Standard, Professional i Enterprise. Tańsza wersja Standard wy- starczy do uruchomienia programów z tej ksišżki. Visual C++ jest także częœciš Visual Studio 6.0. Pakiet Visual C++ Microsoftu zawiera nie tylko kompilator C i inne narzędzia i pliki konieczne do kompilowania i łšczenia programów Windows. Zawiera także Visual C++ Developer Studio - œrodowisko, w którym możesz prowadzić edycję swojego kodu Ÿródłowego, z łatwoœciš tworzyć takie zasoby, jak ikony i okna dia- logowe oraz kompilować, uruchamiać i testować swoje programy. 12 Częœć I: Podstawy Jeœli używasz Visual C++ 5.0, powinieneœ pobrać nowe wersje plików nagłówko- wych i bibliotek importowych dla Windows 98 i Windows NT 5.0. Sš one dostęp- ne na stronie WWW Microsoftu. Połšcz się z http://www.microsoft.com/msdn/ i wybierz Downloads, a potem Platform SDK (Software Development Kit). Będziesz miał możliwoœć pobrać i zainstalować zaktualizowane pliki w dowolnie wybra- nym katalogu. Aby zmusić Microsoft Developer Studio do szukania plików w tym katalogu, wybierz Options z menu Tools i przejdŸ na kartę Directories. Czšstka msdn w adresie URL strony Microsoftu oznacza "Microsoft Developer Network". Jest to program, który zapewnia projektantom często aktualizowane CD-ROM-y z informacjami, potrzebnymi w zaawansowanych pracach programi- stycznych. Prawdopodobnie będziesz chciał zaprenumerować MSDN, aby nie œcišgać tych informacji ze strony Microsoftu. Dokumentacja API Ta ksišżka nie zastępuje oficjalnej dokumentacji API Windows. Dokumentacja ta nie jest już dostępna w formie drukowanej; teraz występuje tylko na CD-ROM- ach i w Internecie. Instalujšc Visual C++ 6.0, uzyskasz system pomocy bezpoœredniej, zawierajšcy do- kumentację API. Możesz zaktualizować tę dokumentację, prenumerujšc MSDN lub używajšc sieciowego systemu pomocy bezpoœredniej Microsoftu. Zacznij od połš- czenia się z http://www.microsoft.com/msdn/ i wybierz MSDN Library Online. W Visual C++ 6.0 wybierz Contents z menu Help, aby wywołać okno MSDN. Do- kumentacja API ma strukturę drzewa. ZnajdŸ częœć oznaczonš etykietš Platform SDK. Cała dokumentacja cytowana w ksišżce pochodzi z tej częœci. Lokalizacja dokumentacji jest opisana za pomocš kolejnych poziomów oddzielonych ukoœni- kami, poczynajšc od poziomu Platform SDK. (Wiem, że Platform SDK jest bardzo małš częœciš całoœci wiedzy w MSDN, ale zapewniam, że jest to podstawa progra- mowania Windows). Na przykład, szukajšc dokumentacji na temat używania myszy, zobacz /Platform SDK/User Interface Services/Llser Input/Mouse Input. Jak wspomniałem wczeœniej, większoœć Windows jest podzielona na podsyste- my: Kernel, User i GDI. Opis funkcji Kernela znajdziesz w /Platform SDK/Win- dows Base Services, opis funkcji podsystemu User w /Platform SDK/User Interface Services, a GDI jest opisane w /Platform SDK/Graphics and Multimedia Services/GDI. Pierwszy program windowsowy Już czas zaczšć pisać programy. Rozpoczniemy od obejrzenia bardzo krótkiego programu Windows i porównania go z programem trybu znakowego. Pomoże nam to oswoić się ze œrodowiskiem programistycznym i przejœć przez proces two- rzenia i kompilacji programu. Tryb znakowy Ulubionš ksišżkš programistów jest The C Programming Language (Prentice Hall, 1978 i 1988); polskie tłumaczenia kolejnych wydań to: Język C, WNT 1988 oraz Rozdział 1: Zaczynamy 13 język ANSI C, WNT 1997, napisana przez Briana W. Kernighana i Dennisa M. Ritchiego, często nazywana w skrócie K&R. Rozdział 1 tej ksišżki zaczyna się pro- gramem w C, który wyœwietla słowa "hello, world". Oto program, jaki znajduje się na stronie 6 pierwszego wydania The C Program- ming Language: main () ł printf ("hello, world\n") ; Dawniej programiœci C używali funkcji bibliotecznych, takich jak printf, bez uprzedniego ich deklarowania. Ale teraz mamy lata dziewięćdziesište i chcemy, żeby nasze kompilatory miały szansę oznaczania błędów w naszym kodzie. Oto poprawiony kod z drugiego wydania K&R: ipinclude main () l printf ("hello, world\n") ; l Ten program nie jest tak mały, jak się wydaje. Na pewno skompiluje się i będzie dobrze działał, ale w dzisiejszych czasach wielu programistów woli jawnie okre- œlić wartoœć zwracanš przez funkcję main, bo ANSI C okreœla, że zwraca ona wartoœć: #include int main () ( printf ("hello, world\n") ; return 0 ; Moglibyœmy jeszcze wydłużyć ten program, włšczajšc argumenty funkcji main, ale zostawmy go takim - z instrukcjš include, punktem startowy'programu, wy- wołaniem funkcji bibliotecznej i instrukcjš return. Odpowiednik windowsowy Odpowiednik windowsowy programu "hello, world" zawiera dokładnie takie same składniki, jak wersja znakowa. Posiada instrukcję include, punkt startowy programu, wywołanie funkcji i instrukcję powrotu. Oto ten program: ``/* HelloMsg.c -- Wyœwietla "Hello, Windows 98!" w oknie komunikatów (c) Charles Petzold, 1998 */ #include int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) 14 Częœć I: Podstawy MessageBox (NULL, TEXT ("Hello, Windows 98!"),TEXT ("NelloMsg"), 0) ; return 0 ; Zanim zacznę omawiać ten program, przeœledŸmy proces tworzenia programu w Visual C++ Developer Studio. Najpierw z menu File wybierz opcję New. W oknie dialogowym New wybierz kartę Projects. Na tej karcie wybierz opcję Win32 Application, a w polu Location wprowadŸ podkatalog. W polu Project Name wpisz nazwę projektu, którš w tym przypadku jest HelloMsg. Będzie to także nazwa podkatalogu dla katalogu wpi- sanego w polu Location. Pole opcji Create New Workspace powinno być zazna- czone. W częœei Platforms powinno być wybrane Win32. Naciœnij OK. Pojawi się okno dialogowe Win32 Application - Step 1 Of 1. Wskaż, że chcesz utworzyć Empty Project i wciœnij przycisk Finish. Wybierz ponownie New z menu File. W oknie dialogowym New zaznacz kartę Files. Wybierz C++ Source Files. Powinno być zaznaczone pole opcji Add to Pro- ject i wskazane HelloMsg. Wpisz HelloMsg.c w polu File Name. Wybierz OK. Teraz możesz wpisać podany powyżej plik HELLOMSG.C. Albo możesz wybrać menu Insert i opcję File As Text, aby skopiować zawartoœć tego pliku z towarzy- szšcego ksišżce CD-ROM-u. Strukturalnie HELLOMSG.C jest identyczny z programem "hello, world" K&R. Plik nagłówkowy STDIO.H został zastšpiony przez WINDOWS.H, punkt starto- wy programu main przez WinMain, a funkcja biblioteki wykonawczej C printf przez funkcję MessageBox API Windows. Jednak program zawiera też dużo nowych rzeczy, włšcznie z dziwnie wyglšdajšcymi identyfikatorami pisanymi wielkimi literami. Omówimy wszystko po kolei. Pliki nagłówkowe HELLOMSG.C zaczyna się instrukcjš preprocesora, którš znajdziesz na poczšt- ku w zasadzie każdego programu Windows pisanego w C: include WINDOWS.H jest głównym plikiem nagłówkowym, który włšcza inne pliki na- główkowe Windows, a niektóre z nich włšczajš jeszcze inne pliki nagłówkowe. Podstawowe i najważniejsze z nich to: ů WINDEF.H podstawowe definicje typów ů WINNT.H definicje typów dla unikodu ů WINBASE.H funkcje Kernela ů WINUSER.H funkcje interfejsu użytkownika ů WINGDI.H funkcje interfejsu urzšdzeń graficznych. Te pliki nagłówkowe definiujš wszystkie typy danych, wywołania funkcji, struk- tury danych i stałe Windows. Sš one istotnš częœciš dokumentacji Windows. Do przeszukiwania ich możesz użyć opcji Find In Files z menu Edit w Visual C++ Rozdział 1: Zaczynamy 15 Developer Studio. Możesz także otworzyć te pliki nagłówkowe w Developer Stu- dio i przeszukać je ręcznie. Punkt startowy programu Tak jak punktem startowym programu w C jest funkcja main, tak punktem star- towym programu windowsowego jest funkcja WinMain, która zawsze wyglšda w ten sposób: int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance. PSTR szCmdLine, int iCmdShow) Punkt startowy jest opisany w /Platform SDIC/ţIser Interface Services/Windowing/ Windows/Window Reference/Window Functions. Jest on zadeklarowany w WINBA- SE.H w następujšcy sposób (zachowane zostało łamanie linii): int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd ): Zauważ, że zrobiłem kilka drobnych zmian w HELLOMSG.C. Trzeci parametr jest zdefiniowany jako LPSTR w WINBASE.H, a ja zdefiniowałem go jako PSTR. Oba te typy danych sš zdefiniowane w WINNT.H jako wskaŸniki do łańcucha znaków. Przedrostek LP oznacza "daleki wskaŸnik" (ang. long pointer) i jest za- szłoœciš z 16 bitowych Windows. Zmieniłem także nazwy dwóch parametrów z deklaracji WinMain; wiele progra- mów Windows używa do nazywania zmiennych tzw. notacji węgierskiej. W tym schemacie nazwy zmiennych sš poprzedzane krótkim przedrostkiem oznaczajš- cym typ zmiennej. Szerzej omówię tę koncepcję w rozdziale 3. Na razie zapamię- taj, że przedrostek i reprezentuje int, a sz oznacza "łańcuch znaków zakończony zerem" (ang. string terminated with a zero). Funkcja WinMain jest zadeklarowana jako funkcja zwracajšca int. Identyfikator WINAPI jest zdefiniowany w WINDEEH jako: ildefine WINAPI ţstdcall Ta instrukcja okreœla konwencję wywoływania funkcji, zwišzanš z rodzajem kodu maszynowego, generowanego w celu umieszczenia na stosie argumentów wy- wołania funkcji. Większoœć wywołań funkcji Windows jest zadeklarowana jako WINAPI. Pierwszy parametr WinMain jest czasem nazywany uchwytem realizacji (ang. instance handle). W programowaniu Windows uchwyt to po prostu liczba, której aplikacja używa do identyfikacji czegoœ. W tym wypadku uchwyt jednoznacznie identyfikuje program. Jest on wymagany jako argument przy niektórych innych wywołaniach funkcji Windows. Gdy we wczeœniejszych wersjach Windows uru- chamiałeœ jednoczeœnie kilka kopii danego programu, tworzyłeœ wiele realizacji (ang. multiple instances) programu. Wszystkie realizacje tej samej aplikacji korzy- stajš z tego samego kodu i pamięci tylko do odczytu (zwykle takich zasobów, jak 16 Częœć I: Podstawy menu i wzorce okien dialogowych). Program jest w stanie okreœlić, czy działajš jego inne realizacje przez sprawdzenie parametru hPrevInstance (ang. prevoins in- stance-poprzednia realizacja). Może wtedy opuœcić częœć działań pomocniczych i pobrać dane z poprzedniej realizacji do własnego obszaru danych. W 32-bitowej wersji Windows zrezygnowano z tej koncepcji. Drugi parametr WinMain ma zawsze wartoœć NULL (zdefiniowanš jako 0). Trzecim parametrem WinMain jest wiersz poleceń użyty do uruchomienia pro- gramu. Niektóre aplikacje Windows wykorzystujš go do ładowania do pamięci plików w czasie uruchamiania programu. Czwarty parametr WinMain wskazuje, w jakiej postaci program powinien się zgłaszać po uruchomieniu w oknie stan- dardowym - czy maksymalnie wypełniajšcym ekran, czy też w postaci zmini- malizowanej do ikony na pasku listy zadań. Zastosowanie tego parametru omó- wimy w rozdziale 3. Funkcja MessageBox Funkcja MessageBox ma służyć do wyœwietlania krótkich komunikatów. Małe okno wyœwietlane przez MessageBox jest nazywane oknem dialogowym, chociaż nie jest ono aż tak uniwersalne. Pierwszym argumentem MessageBox jest zwykle uchwyt okna. Co to oznacza, do- wiemy się w rozdziale 3. Drugim argumentem jest tekst, który ukazuje się w oknie komunikatu, a trzecim argumentem -napis, który występuje na pasku tytułu tego okna. W HELLMSG.C każdy z tych napisów jest dołšczony w postaci makra TEXT. Zwykle nie musisz tak robić, ale umieszczenie wszystkich napisów w makrach TEXT pomoże ci przy ewentualnej konwersji programu do zestawu znaków uni- kod. Omówię to bardziej szczegółowo w rozdziale 2. Czwartym argumentem MessageBox może być połšczenie stałych, zaczynajšce się od przedrostka MB-, a zdefiniowane w WINUSER.H. Możesz wybierać jednš stałš z pierwszego zestawu, by wskazać, jakie przyciski powinny ukazać się w oknie dialogowym: ţtdefine MB_OK 0x00000000L ttdefine MB_OKCANCEL 0x00000001L ţidefine MBţABORTRETRYIGNORE 0x00000002L ţidefine MB YESNOCANCEL 0x00000003L ţţdefine MB_YESNO 0x00000004L ţţdefine MB RETRYCANCEL 0x00000005L Jeœli w HELLOMSG ustawisz czwarty argument na 0, pojawi się tylko przycisk OK. Możesz użyć operatora OR ( I ) z C do połšczenia jednej z wymienionych powyżej stałych ze stałš wskazujšcš na przycisk domyœlny: ţţdefine MB_DEFBUTTON1 0x00000000L ttdefine MB_DEFBUTTON2 0x00000100L tkdefine MB_DEFBUTTON3 0x00000200L ţţdefine MBţDEFBUTTON4 0x00000300L Możesz także użyć stałej, która wskazuje na wystšpienie ikony w oknie dialogo- wym: Rozdział 1: Zaczynamy 4ldefine MBţICONHAND 0x00000010L ţţdefine MB_ICONOUESTION 0x00000020L ţldefine MB_ICONEXCLAMATION 0x00000030L ildefine MB ICONASTERISK 0x00000040L Niektóre z tych ikon majš alternatywne nazwy: ildefine MB_ICONWARNING MB_ICONEXCLAMATION ţldefine MB_ICONERROR MB_ICONHAND ţţdefine MB ICONINFORMATION MB_ICONASTERISK ţţdefine MB ICONSTOP MBţICONHAND Jest jeszcze kilka innych stałych MB , ale sprawdŸ je samodzielnie w pliku na- główkowym lub w dokumentacji w /Platform SDK/Llser Interface Services/Windo- zving/Dialog Boxes/Dialog Box Reference/Dialog Box Funetions. W tym programie funkcja MessageBox zwraca wartoœć 1; bardziej poprawne jest stwierdzenie, że zwraca IDOK, które jest zdefiniowane w WINUSER.H jako równe 1. Zależnie od obecnoœci w oknie dialogowym innych przycisków, funkcja Mes- sageBox może także zwracać IDYES, IDNO, IDCANCEL, IDABORT, IDRETRY albo IDIGNORE. Czy ten mały program Windows jest rzeczywiœcie odpowiednikiem programu "hello, world" z K&R? Możesz mieć wštpliwoœci, ponieważ funkcja MessageBox nie ma możliwoœci takiego formatowania tekstu, jak funkcja printf w "hello, world". Ale w następnym rozdziale dowiesz się, jak napisać wersję MessageBox, która wykonuje formatowanie w stylu printf Kompilowanie, łšczenie i uruchamianie Gdy już będziesz gotowy do skompilowania HELLOMSG, możesz wybrać Build Hellomsg.exe z menu Build albo nacisnšć [F7], albo wybrać ikonę Build z paska narzędzi Build. (Możesz zobaczyć tę ikonę w menu Build. Jeœli pasek narzędzi Build nie jest aktualnie wyœwietlany, wybierz Customize z menu Tools, a następ- nie kartę Toolbars. Wybierz Build albo Build MiniBar). Możesz także wybrać Execute Hellomsg.exe z menu Build albo [Ctrl+F5], lub klik- nšć na ikonę Execute Program (wyglšda jak czerwony wykrzyknik) z paska na- rzędzi Build. Pokaże się okno z pytaniem, czy chcesz skompilować program. Podczas fazy kompilacji, kompilator tworzy zazwyczaj plik OBJ (obiektowy) z pli- ku Ÿródłowego C. Podczas fazy łšczenia konsolidator łšczy pliki OBJ z plikami LIB (bibliotekami), aby stworzyć plik EXE (wykonywalny). Możesz zobaczyć li- stę plików bibliotecznych, wybierajšc Settings z karty Project i klikajšc kartę Link. Zobaczysz między innymi KERNEL32.LIB, USER32.LIB i GDI32.LIB. Sš to biblio- teki importowe dla trzech głównych podsystemów Windows. Zawierajš one na- zwy bibliotek dynamicznych i informacje odnoœników, umieszczane w pliku EXE. Windows używa tej informacji do odwzorowania wywołań programu na funkcje w bibliotekach dynamicznych KERNEL32.LIB, USER32.LIB i GDI32.LIB. W Visual C++ Developer Studio możesz kompilować i łšczyć programy w róż- nych konfiguracjach. Domyœlnie sš one nazwane Debug i Release. Pliki wykony- walne sš umieszczone w podkatalogach o nazwach konfiguracji. W konfiguracji Debug do pliku EXE zostaje dodana informacja, która umożliwia poprawianie programu i œledzenie kodu Ÿródłowego programu podczas wykonywania. 18 Częœć I: Podstawy Jeœli wolisz posługiwać się wierszem poleceń, dołšczony CD-ROM zawiera pliki MAK (make) dla wszystkich przykładowych programów. (Możesz zmusić Deve- loper Studio do tworzenia plików MAK, wybierajšc Options z menu Tools i za- znaczajšc odpowiednie pole opcji na karcie Build). Będziesz musiał uruchomić VCVARS32.BAT, znajdujšcy się w podkatalogu BIN w Developer Studio, aby ustawić zmienne œrodowiskowe. Aby uruchomić pliki make z wiersza poleceń, przejdŸ do katalogu HELLOMSG i wpisz: NMAKE /f HelloMsg.mak CFG="HelloMsg - Win32 Debug" albo NMAKE /f HelloMsg.mak CFG="HelloMsg - Win32 Debug" Następnie możesz uruchomić z wiersza poleceń plik EXE, piszšc: DEBUG\HELLOMSG lub RELEASE\HELLOMSG W plikach projektów na dołšczonym do ksišżki CD-ROM-ie wprowadziłem jed- nš zmianę do domyœlnej konfiguracji Debug. W oknie dialogowym Project Set- tings, po wybraniu karty C/C++, w polu Preprocessor Definitions, zdefiniowa- łem identyfikator UNICODE. Znacznie więcej na ten temat powiem w następ- nym rozdziale. Rozdział 2 Wr ' owa z eme o unik 0 u W pierwszym rozdziale obiecałem opowiedzieć o wszystkich aspektach języka C, z którymi mogłeœ nie spotkać się podczas tradycyjnego programowania w try- bie znakowym, a które sš ważne w Windows. Zestawy znaków szerokich i uni- kod (ang. Unicode) niemal na pewno kwalifikujš się do tego. Mówišc prosto, unikod jest rozszerzeniem kodowania znaków ASCII. Zamiast 7 bitów, używanych do reprezentowania wszystkich znaków w "czystym" ASCII, albo 8 bitów na znak, co jest dzisiejszym standardem w komputerach, unikod używa do oznaczenia znaku 16 bitów. Pozwala to zakodować wszystkie litery, ideogramy i inne symbole, które występujš we wszystkich rodzajach pism na œwiecie i mogš być przedmiotem komunikacji komputerowej. W zamierzeniach, unikod ma za zadanie uzupełniać ASCII i przy odrobinie szczęœcia ostatecznie go zastšpić. Ponieważ ASCII jest jednym z najpopularniejszych standardów kom- puterowych, będzie to niełatwe zadanie. Wpływ unikodu widać we wszystkich aspektach szeroko rozumianego przemy- shz komputerowego, ale chyba najbardziej w systemach operacyjnych i językach programowania. Pod tym względem jesteœmy prawie w połowie drogi. Windows NT umożliwia korzystanie wyłšcznie z unikodu. (Niestety, w Windows 98 uni- kod ma rolę jedynie wspomagajšcš). Język programowania C, sformalizowany przez ANSI, umożliwia korzystanie ze znaków szerokich i w ten sposób wspo- maga unikod, co omówię bardziej szczegółowo dalej. Oczywiœcie, jak zwykle, my, jako programiœci, musimy poradzić sobie z tš brud- nš robotš. Próbowałem to nieco ułatwić piszšc przykładowe programy w tej ksišż- ce tak, aby były gotowe do użycia unikodu. Co to dokładnie znaczy, okaże się w dalszej częœci rozdziału. Krótka historia zestawów znaków Nie wiadomo, kiedy ludzie zaczęli mówić, ale pismo pojawiło się około szeœciu tysięcy lat temu. Pierwsze pisma były z natury obrazkowe. Alfabety - gdzie poje- dyncze litery odpowiadajš dŸwiękom mówionym - pojawiły się dopiero około trzech tysięcy lat temu. Przez długie wieki tradycyjne języki zupełnie wystarczały naszym przodkom do komunikowania się; dopiero kilku dziewiętnastowiecznych 20 Częœć I: Podstawy wynalazców zapragnęło ezegoœ więcej. Samuel F. B. Morse, pracujšc nad telegra- fem w Iatach 1838-1854, stworzył kod do jego obsługi. Każdej literze alfabetu od- powiadała seria krótkich i długich impulsów (kropek i kresek). Nie było rozróżnie- nia między wielkimi i małymi literami, aIe liczby i znaki dodatkowe miały swoje własne kody. Kod Morse'a nie był pierwszym pismem, reprezentowanym przez coœ innego niż rysowane lub drukowane figury. W latach 1821-1824 młody Louis Braille, zainspi- rowany przez wojskowy system nocnego czytania i pisania wiadomoœci, stworzył z myœlš o niewidomych kod składajšcy się z wypukłych kropek na papierze. Język Braille'a jest właœciwie 6-bitowym kodem, który zawiera litery, typowe zestawie- nia liter, często powtarzajšce się słowa i znaki przestankowe. Specjalny kod uniko- wy (ang. escape code) oznacza, że następny kod litery powinien być interpretowa- ny jako wielka litera. Specjalny kod przełšczajšcy powoduje, że następne kody zna- ków powinny być interpretowane jako cyfry. Kody teleksowe, z kodem Baudota (od nazwiska francuskiego inżyniera, który zmarł w 1903 roku) i kodem znanym jako CCITT #2 (ustandaryzowanym w 1931 roku) włšcznie, były kodami 5-bitowymi i zawierały przełšczniki literowe i cy- frowe. Standardy amerykańskie Pierwsze komputerowe kody znakowe rozwinęły się z kodowania stosowanego na kartach Holleritha ("nie zginać, nie gnieœć, nie uszkadzać"), wymyœlonych przez Hermana Holleritha i pierwszy raz użytych przy spisie Iudnoœci w Stanach Zjed- noczonych w 1890 roku. Ten 6-bitowy kod znakowy, znany jako BCDIC (ang. Bi- nary-Coded Decima2 Interehange Code - kodowany dwójkowo kod dziesiętny do wymiany informacji ) został rozszerzony do 8-bitowego EBCDIC w latach szeœć- dziesištych i zadomowił się tylko na dużych komputerach IBM i nigdzie więcej. Prace nad ASCII (ang. American Standard Code for Information Interchange) rozpo- częły się pod koniec Iat pięćdziesištych i trwały do 1967 roku. W międzyczasie wywišzała się dyskusja o tym, czy kod powinien mieć szerokoœć 6, 7 czy 8 bitów. Niezawodnoœć wymagała rezygnacji ze znaków przełšczajšcych, więc ASCII nie mogło być kodem 6-bitowym. Wersję 8-bitowš odrzucono z powodu wysokich kosztów (wtedy bity były bardzo drogie). Wersja ostateczna kodu miała 26 ma- łych liter, 26 wielkich liter, 10 cyfr, 32 symbole, 33 kody kontrolne i puste miejsce, aż do 128 znaków. Oficjalnym dokumentem opisujšcym ASCII jest obecnie Coded Character Sets - 7-Bit American National Standard Code for Information Interchange (7-Bit ASCII), opublikowany w ANSI X3.4-1986 przez American National Stan- dards Institute. Rysunek 2-1 pokazuję (po raz tysięczny) ASCII, w formie bardzo podobnej do zdefiniowanej w dokumencie ANSI. Rozdział 2: Wprowadzenie do unikodu 21 0- l- 2- 3- 4- 5- 6- 7- -0 NUL DLE SP 0 @ P ' p -1 SOH DCl ! 1 A Q a q -2 STX DC2 " 2 B R b r -3 ETX DC3 # 3 C S c s -4 EOT DC4 $ 4 D T d t -5 ENQ NAK % 5 E U e u -6 ACK SYN & 6 F V f v -7 BEL ETB ' 7 G W g w -8 BS CAN ( 8 H X h x -9 HT EM ) 9 I Y I y -A LF SUB * . J Z j z -B VT ESC + , K [ k -C FF FS , < L \ 1 -D CR GS - - M ] m } -E SO RS > N ^ n ~ -F SI US / ? O o DEL Rysunek 2-1. Zestaw znaków ASCII Wiele dobrych rzeczy można powiedzieć o ASCII. Na przykład to, że 26 kodów liter występuje bez przerw (co nie zawsze jest prawdš w przypadku EBCDIC). Wielkie litery mogš być zamienione na małe (i na odwrót) poprzez odwrócenie jednego bitu. Kody dla 10 cyfr sš łatwe do okreœlenia z wartoœci cyfr (w BCDIC kod znaku "0" następował po kodzie znaku "9"!). Ale najcenniejsze jest to, że ASCII to niezawodny standard. Żaden inny standard ! nie jest tak powszechny ani tak zakorzeniony w naszych klawiaturach, kartach graficznych, sprzęcie, drukarkach, plikach z czcionkami, systemach operacyjnych i i Internecie. Inne języki Największe ograniczenie ASCII zostało odnotowane już w pierwszym wyrazie nazwy: "american". ASCII jest standardem amerykańskim i nie może sprostać potrzebom innych krajów, nawet anglojęzycznych. Gdzie, na przykład, jest bry- tyjski symbol funta (^)? Język angielski, oparty na alfabecie łacińskim (rzymskim), charakteryzuje się tym, że niewiele słów wymaga liter ze znakami diakrytycznymi. Nawet tam, gdzie akcenty być powinny, na przykład cooperate albo resume, pisownia bez użycia znaków diakrytycznych jest całkowicie akceptowalna. Jednak na południu i północy Stanów Zjednoczonych i za Atlantykiem jest wiele krajów i języków, gdzie znaki diakrytyczne występujš znacznie częœciej. Znaki akcentu pomogły zaadaptować alfabet łaciński do brzmienia tych języków. Pod- różujšc dalej, na wschód albo południe Europy, natrafisz na takie języki, jak grecki, hebrajski, arabski i rosyjski (który używa cyrylicy), które w ogółe nie używajš alfabetu łacińskiego. A jeœli będziesz przemieszczał się jeszcze dalej na wschód, 22 Częœć I: Podstawy odkryjesz chińskie ideogramy Han, które zostały przejęte także w Japonii i Ko- rei. Historia ASCII od roku 1967 jest głównie historiš prób przełamania jego ograni- czeń i uczynienia go odpowiednim także dla języków innych niż amerykańska odmiana angielskiego. Na przykład w roku 1967 ISO (International Standards Or- ganisation) zalecała używanie odmiany ASCII z kodami 0x40, Ox5B, Ox5C, Ox5D, Ox7B, Ox7C i Ox7D "zarezerwowanymi do użytku narodowego" i kodami Ox5E, 0x60 i Ox7e oznaczonymi jako "mogš być użyte dla innych symboli graficznych wtedy, gdy konieczne jest 8, 9 lub 10 pozycji do użytku narodowego". Nie jest to na pew- no najlepszy sposób intemacjonalizacji, ponieważ nie gwarantuje znaczeń tych kodów. To posunięcie pokazało jednak, jak zdesperowani ludzie mogš kodować symbole, niezbędne w różnych językach. Rozszerzenie ASCII W czasie, gdy tworzono pierwsze małe komputery, 8-bitowy bajt był już stan- dardem. Jeœli więc do przechowywania znaków był używany bajt, można było wykorzystać 128 dodatkowych znaków do wsparcia ASCII. Gdy w 1981 roku został zaprezentowany oryginalny IBM PC, jego karta graficzna miała zakodo- wany w ROM-ie 256-znakowy zestaw znaków, który sam szybko został ważnš częœciš standardu IBM. Oryginalny rozszerzony zestaw znaków IBM zawierał kilka znaków akcentowa- nych i małe litery alfabetu greckiego (użyteczne w notacji matematycznej), jak również trochę znaków pseudograficznych (kształtów i linii). Dodatkowe znaki zostały także przypisane kodom znaków kontrolnych ASCII, ponieważ większoœć z tych kodów była niepotrzebna. Ten rozszerzony zestaw znaków IBM był wypalany w niezliczonych ROM-ach na kartach graficznych i w drukarkach, a liczne aplikacje stosowały go do uatrak- cyjnienia swojego wyglšdu w trybie znakowym. Nadal jednak nie zawierał zna- ków akcentowanych języków Europy Zachodniej, które używały alfabetu łaciń- skiego, a co więcej -był nieodpowiedni dla Windows. Windows nie potrzebował już znaków z rysunkami, ponieważ był systemem całkowicie graficznym. W Windows 1.0 (przedstawionym w listopadzie 1985), Microsoft nie zrezygnował całkiem z rozszerzonego zestawu znaków IBM, ale też go nie eksponował. Podsta- wowy zestaw znaków Windows nazwano zestawem znaków ANSI, ponieważ opierał się na szkicu standardu ANSI i ISO, który póŸniej został oficjalnie ogłoszo- ny w ANSI/ISO 8859-1-1987, American National Standard for Information Processing- 8-Bit Single-Byte Coded Graphic Character Sets-Part 1: Latin Alphabet No 1. Jest on znany także pod prostszš nazwš "Latin 1". Oryginalnš wersję zestawu znaków ANSI, wydrukowanš w Programmer's Refe- rence dla Windows 1.0, pokazuje rysunek 2-2. Rozdział 2: Wprowadzenie do unikodu 23 0- 1- 2- 3- 4- 5- 6- 7-' 8- 9- A- B- C- D- E- F- -0 ţ ţ 0 @ P ' p ţ ţ ř a D „ -1ţţ ! 1 A Qa q ţţI ń ElIsl„ ń -2 ţ 0 " 2 B R b r ţ ţ › 2 Ž ™ „ ” -3 0 0 # 3 C S c s ţ ţ ^ 3 Ž Ó „ ó -4 ţ ţ $ 4 D T d t ţ ţ ţx ' Ž ™ „ ” -5 ţ ţ % 5 E U e u ţ ţ  p Ž ™ „ ” -6 ţ ţ & 6 F V f v ţ ţ ţ 9[ ţE ™ ţ ” -7 0 ţ ' 7 G W g w -8 ţ ţ ( 8 H X h x ţ ţ " , E ţ e -9 ţ ţ ) 9 I Y I y ţ ţ ^ i E š e  -A ţ ţ * . J Z j z ţ ţ a ř E LJ e  -B ţ ţ + , K [ k { 0 0 Ž Ż E š e  -C ţ ţ , < L \ 1 I ţ 0 ţ 1/a I š i  -Dţ0 - -M ] m } ţţ- '/zI Y i y -E ţ 0 . > N ^ n ~ ţ 0 ^ 3/a I h i -F ţ ţ / ? O - o DEL 0 ţ ţ ţ I á i y Rysunek 2-2. Zestaw znaków ANSI w Windows (oparty na ANSI/ISO 8859-1) Puste prostokšty oznaczajš kody, dla których nie zdefiniowano znaków. Podobnie był zdefiniowany ANSI/ISO 8859-1. ANSI/ISO 8859-1 pokazuje tylko znaki gra- ficzne, a nie znaki kontrolne (więc nie definiuje DEL). Ponadto kod OxAO zdefinio- wano jako spację nierozdzielajšcš (co oznacza, że nie może być używana do łama- nia linii podczas formatowania) i kod OxAD jako łšcznik opcjonalny (co oznacza, że jest używany do oznaczenia miejsca dzielenia wyrazu podczas łamania linii i tylko wtedy powinien być wyœwietlany). ANSI/ISO 8859-1 definiuje kody: OxD7 jako ! symbol mnożenia (*) i OxF7 jako symbol dzielenia (=). Niektóre czcionki w Win- dows definiujš także pewne znaki z zakresu od 0x80 do Ox9F, ale nie sš one częœciš standardu ANSI/ISO 8859-1. W wersji 3.3 MS-DOS-a (udostępnionej w kwietniu 1987 roku) przyjęto takie roz- ; wišzanie stron kodowych dla komputerów IBM PC, które zostało przeniesione do Windows. Strona kodowa okreœla przyporzšdkowanie kodów znaków do znaków. Oryginalny zestaw znaków IBM występuje pod dwoma nazwami: strona kodowa 437 albo MS-DOS Latin US. Strona kodowa 850 to MS-DOS Latin l, gdzie częœć znaków pseudografiki zastšpiono dodatkowymi literami akcentowanymi (ale nie jest ona zgodna ze standardem ISO/ANSI Latin 1, pokazanym na rysunku 2-2). Dla innych języków zostały zdefiniowane dodatkowe strony kodowe. Pierwszych ,, 128 kodów jest zawsze takie same; kolejne 128 kodów zależy od języka, dla które- go strona została zdefiniowana. Jeœli użytkownik pracujšcy na PC wyposażonym w MD-DOS ustawi na klawiatu- rze, karcie graficznej i drukarce pewnš stronę kodowš, a potem będzie pisał i dru- kował dokumenty na PC, wszystko będzie dobrze działać, gdyż całoœć jest spójna. Kłopotów należy się spodziewać wtedy, gdy użytkownik spróbuje wymienić do- ; kumenty z innym użytkownikiem, który korzysta innej strony kodowej. Kodom I 24 Częœć I: Podstawy znaków zostanš przyporzšdkowane złe znaki. Próbowano rozwišzać ten problem, zapamiętujšc informację o stronie kodowej razem z dokumentem, ale wymagało to sporej pracy przy konwertowaniu stron kodowych. Poczštkowo strony kodowe uzupełniały tylko alfabet łaciński o dodatkowe znaki ţpoza znakami nie akcentowanymi). Ostatecznie stworzono strony kodowe, gdzie wyższe 128 znaków zawierało pełne alfabety niełacińskie, takie jak hebrajski, grec- ki i cyrylica. Taka różnorodnoœć stron kodowych powoduje, oczywiœcie, zamiesza- nie; jeœli kilka akcentowanych liter będzie Ÿle wyœwietlanych, to nic się nie stanie - gorzej, jeœli cały tekst zmieni się w bezładnš, niezrozumiałš mieszaninę. Strony kodowe mnożyły się mimo wszystkich swoich wad. Strona kodowa MS- DOS 855 z cyrylicš nie jest ekwiwalentem strony 1251 Windows dla cyrylicy ani też strony 10007 dla cyrylicy w komputerach Macintosh. Strony kodowe w każ- dym œrodowisku sš modyfikacjami standardowych zestawów znaków œrodowi- ska. IBM OS/2 dodatkowo umożliwia korzystanie ze stron kodowych EBCDIC. Ale poczekaj. Będzie jeszcze gorzej. Zestaw znaków dwubajtowych Dotychczas przyglšdaliœmy się zestawom zawierajšcym 256 znaków. Ale sym- boli ideograficznych chińskich, japońskich i koreańskich jest ponad 21000. Jak można pomieœcić te języki, zachowujšc cišgle jakšœ zgodnoœć z ASCII? Rozwišzaniem (jeœli to właœciwe słowo) jest zestaw znaków dwubajtowych (ang. double-byte character set, DBCS). DBCS zaczyna się 256 kodami, zupełnie jak ASCII. Podobnie jak we wszystkich stronach kodowych, pierwsze 128 kodów to ASCII. Jednak niektóre z kodów powyżej 128 majš zawsze jeszcze dodatkowy bajt. Dwa bajty razem (nazywane odpowiednio bajtem poczštkowym i bajtem końcowym) definiujš pojedynczy znak, będšcy zwykle złożonym ideogramem. Chociaż Chiny, Japonia i Korea wspólnie korzystajš z wielu takich samych ide- ogramów, to przecież rozwinęły różne języki i często te same ideogramy ozna- czajš dla nich trzy różne rzeczy. Windows umożliwia korzystanie z czterech róż- nych zestawów znaków dwubajtowych: strony kodowej 932 (japońska), 936 (chiń- ska uproszczona), 949 (koreańska) i 950 (chińska tradycyjna). Korzystanie z DBCS jest moŸliwe tylko w wersjach Windows przeznaczonych dla tych krajów. W dwubajtowym zestawie znaków nie przysparzajš kłopotów znaki reprezento- wane przez 2 bajty. Natomiast niedogodne jest to, że częœć znaków (a zwłaszcza znaki ASCII) jest reprezentowana przez 1 bajt. Powoduje to różne problemy pro- gramistyczne. Na przykład liczba znaków w łańcuchu znaków nie może być okre- œlona na podstawie wielkoœci łańcucha znaków w bajtach. Trzeba wykonać ana- lizę łańcucha znaków, aby okreœlić jego długoœć, a każdy bajt musi być zbadany, czy nie jest bajtem poczštkowym znaku dwubajtowego. Jeœli masz wskaŸnik do znaku gdzieœ w œrodku łańcucha znaków DBCS, jaki jest adres poprzedniego znaku w łańcuchu znaków? Jedyne rozwišzanie to analiza łańcucha znaków od poczštku, aż do wskaŸnika! Rozdział 2: Wprowadzenie do unikodu 25 Ratunek w unikodzie Zasadniczym problemem jest to, że 256 kodów 8-bitowych to za mało, aby od- wzorować znaki wszystkich potrzebnych języków. Poprzednie rozwišzania ze stronami kodowymi i DBCS-em sš i niewystarczajšce, i niezgrabne. Jak uporać się z tym problemem? Jako programiœci zdobyliœmy pewne doœwiadczenie na tym polu. Jeœli mamy zbyt dużo elementów, by przedstawić je za pomocš wartoœci 8-bitowych, próbujemy zastosować szersze wartoœci - być może 16-bitowe. Właœnie na tym polega bar- dzo prosta koncepcja unikodu. Zamiast mieszania różnych kodów 256-znakowych lub znaków dwubajtowych i jednobajtowych w zestawie znaków dwubajtowych, unikod jest jednolitym systemem 16-bitowym, który pozwala na przedstawienie 65536 znaków. Jest to zasób wystarczajšcy do zapisania wszystkich znaków i ide- ogramów ze wszystkich pisanych języków œwiata, a także znaków matematycz- nych, symboli i zbiorów ornamentów. Istotne jest zrozumienie różnicy między unikodem i DBCS. Mówi się (szczegól- nie w kontekœcie języka programowania C), że unikod używa "znaków szero- kich". Każdy znak w unikodzie ma szerokoœć 16 bitów, zamiast 8 bitów. Warto- œci 8-bitowe nie majš żadnego znaczenia w urukodzie. W odróżnieniu od tego, w dwubajtowym zestawie znaków cišgle mamy do czynienia z wartoœciami 8-bito- wymi. Niektóre bajty same definiujš znaki, a niektóre wskazujš na inny bajt, ko- nieczny do pełnej definicji znaku. O ile praca z łańcuch znakówami w DBCS jest skomplikowana, o tyle praca z tekstem w unikodzie przypomina już pracę z normalnym tekstem. Prawdopo- dobnie będziesz musiał nauczyć się, że pierwszych 128 znaków unikodu (o ko- dach 16-bitowych od 0x0000 do 0x007F) to znaki ASCII, a następnych 128 zna- ków unikodu (kody od 0x0080 do 0x00FF) to rozszerzenie ASCII - ISO 8859-1. Różne bloki znaków wewnštrz unikodu oparte sš na istniejšcych standardach. Upraszcza to wykonywanie konwersji. Alfabet grecki używa kodów od 0x0370 do 0x03FF, cyrylica - od 0x0400 do 0x04FF, armeński - od 0x0530 do 0x058F, a hebrajski - od 0x0590 do 0x05FE Ideogramy chińskie, japońskie i koreańskie (okre- œlane zbiorczo jako CJK) zajmujš kody od 0x3000 do Ox9FFF. Najlepsza cecha unikodu to jeden zestaw znaków. Nie ma tam żadnej dwuznacz- noœci. unikod powstał dzięki współpracy praktycznie wszystkich ważniejszych firm przemysłu komputerów osobistych i jest identyczny ze standardem ISO 10646-1. Najważniejszš pozycjš o unikodzie jest The Unicode Standard, Uersion 2.0 (Addison Wesley, 1996), nadzwyczajna ksišżka, która ujawnia bogactwo i roz- maitoœć pisanych języków œwiata w taki sposób, w jaki niewiele innych publika- cji było to w stanie zrobić. Dodatkowo ksišżka zawiera racjonalne uzasadnienie i szczegóły opracowania unikodu. Czy unikod ma jakieœ wady? Oczywiœcie. Łańcuchy znaków w unikodzie zajmu- jš dwa razy więcej pamięci niż łańcuch znaków ASCII. (Chociaż kompresja pli- ków pomaga trochę zmniejszyć różnicę w zajmowanej przestrzeni dyskowej). Ale być może największš wadš jest to, że unikod nadal nie znajduje zastosowa- nia. Jako programiœci możemy to zmienić. 26 Częœć I: Podstawy Szerokie znaki i C Idea 16-bitowych znaków może przyprawić o dreszcze niejednego programistę piszšcego w C. Dla takiego programisty œwiętoœciš jest szerokoœć char równa baj- towi. Niewielu programistów jest œwiadomych, że ANSI/ISO 9899-1990, Ameri- can National Standard for Programmming Languages - C (znany także jako "ANSI C") dopuszcza korzystanie ze znaków zajmujšcych więcej niż jeden bajt; w tym celu zaproponowano pojęcie znaków szerokich (ang. wide characters). Te znaki sze- rokie współistniejš z normalnymi, dobrze znanymi znakami. ANSI C umożliwia też korzystanie z zestawów znaków wielobajtowych, ofero- wanych przez chińskie, japońskie i koreańskie wersje Windows. Jednak te zesta- wy znaków wielobajtowych sš traktowane jak łańcuchy z wartoœci jednobajto- wych, w których kilka znaków zmienia znaczenie następujšcych po nich znaków. Zestawy znaków wielobajtowych majš wpływ na funkcje bibliotek wykonawczych C. W odróżnieniu od tego, wszystkie znaki szerokie sš jednorodne i wymagajš nowej wersji kompilatora. Znaki szerokie nie muszš być znakami unikodu. Urukod jest tylko jednym z moż- liwych sposobów kodowania znaków szerokich. Jednak ponieważ w tej ksišżce koncentrujemy się na Windows, a nie abstrakcyjnej realizacji C, będę mówił o sze- rokich znakach i unikodzie jako o synonimach. Typ danych char Przypuszczalnie wszyscy dobrze znamy definiowanie i przechowywanie znaków i ich łańcuchów w naszych programach C przy zastosowaniu typu danych char. Aby lepiej zrozumieć, jak C obshxguje znaki szerokie, przypomnimy sobie defini- cję zwykłych znaków, jaka może wystšpić w programie Win32. Następujšca instrukcja definiuje i inicjuje zmiennš zawierajšcš pojedynczy znak: char c = 'A' , Zmienna c wymaga 1 bajtu pamięci i będzie zainicjowana wartoœciš szesnastko- wš 0x41, która jest kodem ASCII dla litery A. Możesz zdefiniować wskaŸnik do łańcucha znaków: char Ponieważ Windows jest 32-bitowym systemem operacyjnym, zmienna wskaŸni- kowa p wymaga 4 bajtów pamięci. Możesz też zainicjować wskaŸnik do łańcu- cha znaków: char * p = "Hello!" , Zmienna p wymaga 4 bajtów pamięci, jak przedtem. Łańcuch znaków jest zapi- sany w pamięci statycznej i zajmuje 7 bajtów - 6 bajtów łańcucha oraz kończšce go 0. Możesz zdefiniować tablicę znaków: char aC107 ; W tym przypadku kompilator rezerwuje dla tablicy 10 bajtów pamięci. Wyrażenie si- zeof (a) zwróci wartoœć 10. Jeœli tablica jest globalna (czyli zdefiniowana poza jakškol- wiek funkcjš), możesz inicjować tablicę znaków, używajšc następujšcej instrukcji: Rozdział 2: Wprowadzenie do unikodu 27 char a[] = "Hello!" , Jeœli definiujesz tę tablicę jako zmiennš lokalnš funkcji, musi być ona zdefinio- wana jako zmienna static, jak tu: static char a[] = "Hello!" W obu powyższych przypadkach łańcuch znaków jest wprowadzany do pamię- ci statycznej programu z dodanym na końcu 0, więc wymaga 7 bajtów pamięci. Szersze znaki Ani unikod, ani znaki szerokie nie zmieniajš znaczenia typu danych char w C. Nadal char oznacza 1 bajt pamięci, a sizeof (char) cišgle zwraca 1. Teoretycznie bajt w C może być większy niż 8 bitów, ale dla większoœci z nas bajt (i chnr) ma zawsze 8 bitów. Znaki szerokie w C oparte sš na typie danych zuchayt, który jest zdefiniowany w kilku plikach nagłówkowych, łšcznie z WCHAIZ.H, jako: typedef unsigned short wchar t ; Tak więc typ danych wchar t jest taki sam jak liczba całkowita bez znaku: ma 16 bitów szerokoœci. Aby zdefiniować zmiennš zawierajšcš pojedynczy szeroki znak, użyj następujš- cej instrukcji: wchar t c = 'A' , Zmienna c jest dwubajtowš ţ,artoœciš 0x0041, która reprezentuje w unikodzie li- terę A. (Ponieważ w mikroprocesorach Intela zapamiętywanie wartoœci wielobaj- towych rozpoczyna się od najmniej znaczšcych bajtów, to rzeczywista kolejnoœć bajtów w pamięci jest następujšca: 0x41 i 0x00. Pamiętaj o tym, jeœli sprawdzasz w pamięci tekst unikodu). Możesz też zdefiniować zainicjowany wskaŸnik do łańcucha znaków szerokich: wchar t * p = L"Hello!" Zwróć uwagę na wielkie L (od ang. long) bezpoœrednio przed pierwszym zna- kiem cudzysłowu. Wskazuje ono kompilatorowi, że łańcuch znaków ma być za- pisany z szerokimi znakami, z których każdy zajmuje 2 bajty. Zmienna wskaŸni- kowa p wymaga jak zwykle 4 bajtów pamięci, ale łańcuch znaków wymaga 14 bajtów - po 2 bajty dla każdego znaku z 2 bajtami zer na końcu. Podobnie możesz zdefiniować tablicę znakóţţ szerokich: static wchar t a[] = L"Hello!" Łańcuch znaków ponownie wymaga 14 bajtów pamięci, więc sizeof (a) powinno zwrócić wartoœć 14. Możesz za pomocš indeksów tablicy uzyskać indywidualne znaki. Wartoœć a(1] to szeroki znak 'e' albo 0x0065. Chociaż wyglšda to jak literówka, L poprzedzajšce otwarcie cudzysłowu jest bar- dzo ważne i nie może być między tymi znakami żadnego odstępu. Tylko z tym L kompilator będzie wiedział, że chcesz przechować łańcuch znaków z 2 bajtami na znak. PóŸniej, kiedy będziemy spotykać łańcuchy składajšce się ze znaków szerokich, ponownie napotkasz L przed pierwszym znakiem cudzysłowu. Na szczęœcie kompilator C najczęœciej zgłosi ostrzeżenie lub komunikat o błędzie, jeœli zapomnisz dołšczyć L. 2g Częœć I: Podstawy Możesz także użyć przedrostka L przed stałš znakowš składajšcš się z pojedyn- czego znaku, jak poniżej, wskazujšc, że powinien być interpretowany jako szero- ki znak. wcharţt c = L'A' , Ale zwykle nie jest to konieczne. Kompilator C powinien rozszerzyć ten znak o zero. Funkcje biblioteki znaków szerokich Wiemy, jak znaleŸć długoœć łańcucha znaków. Na przykład, jeœli zdefiniowali- œmy wskaŸnik do niego: char * pc = "Hello!" , możemy wywołać iLength = strlen (pc) ; Zmienna iLength uzyska wartoœć 6 równš liczbie znaków w łańcuchu. Doskonale! Teraz spróbujmy zdefiniować wskaŸnik do łańcucha znaków szero- kich: wchar t * pw = L"Hello!" , I wywołajmy ponownie strlen: iLength = strlen (pw) : Zaczynajš się kłopoty. Najpierw kompilator C wyœwietla komunikat ostrzegaw- czy, prawdopodobnie coœ w stylu 'function' . incompatible types - from 'unsigned short *' to 'const char *' Oznacza to, że funkcja strlen jest zadeklarowana jako pobierajšca wskaŸnik do char, a otrzymała wskaŸnik do unsigned int. Możesz wprawdzie skompilować i uru- chomić program, ale stwierdzisz, że iLength jest ustawiona na 1. Dlaczego? Oto 16-bitowe wartoœci dla 6 znaków łańcuch znakówu "Hello!": 0x0048 0x0065 0x006C 0x006C 0x006F 0x0021 Mikroprocesor Intela umieszcza je w pamięci następujšco: 48 00 65 00 6C 00 6C 00 6F 00 21 00 Funkcja strlen próbujšc znaleŸć długoœć łańcucha znaków, liczy pierwszy bajt jako znak, ale zakłada, że drugi to bajt zerowy oznaczajšcy koniec łańcucha. To małe ćwiczenie jasno pokazuje różnice między samym językiem C i funkcjami biblioteki wykonawczej. Kompilator interpretuje łańcuch znaków L"Hello!" jako zbiór wartoœci 16-bitowych typu short lnt i zapisuje w tablicy wchar t. Kompila- tor obsługuje też indeksowanie tablicy i operator sizeof, więc działajš one właœci- wie. Ale funkcje biblioteki wykonawczej, takie jak strlen, sš dodawane podczas łšczenia. Te funkcje oczekujš łańcuchów zawierajšcych znaki jednobajtowe. Gdy dostanš łańcuchy z szerokimi znakami, nie działajš tak, jakbyœmy chcieli. Powiesz, że to nic wielkiego. Teraz każda funkcja biblioteki C musi być przepisa- na, aby przyjmowała znaki szerokie. Tak naprawdę, nie każda. Tylko te funkcje, które majš argumenty w postaci łańcuchów znaków. I nie musisz ich przepisy- wać. To już zostało zrobione. Rozdział 2: Wpţowadzenie do unikodu Wersja funkcji strlen do obsługi znaków szerokich nazywa się wcslen (ang. wide- character string length) i jest zadeklarowana zarówno w STRING.H (gdzie znaj- duje się deklaracja strlen), jak też i w WCHAR.H. Funkcja strlen jest zadeklarowa- na następujšco: size t cdecl strlen (const char *) : a oto deklaracja funkcji wcslen: sizę t cdecl wcslen (const wchar t *) : Teraz wiemy, że jeœli chcemy poznać długoœć łańcucha składajšcego się z zna- ków szerokich, musimy wywołać iLength = wcslen (pw) : Funkcja zwraca 6, liczbę znaków w łańcuchu. Zapamiętaj, że długoœć łańcucha (w znakach) nie zmienia się przy przejœciu na znaki szerokie - zmienia się tylko liczba bajtów. Wszystkie twoje ulubione funkcje biblioteki wykonawczej C, które pobierajš ar- gumenty w postaci łańcuchów znaków, majš swoje wersje dla znaków szerokich. Na przykład wprintf to wersja funkcji printf dla znaków szerokich. Te funkcje sš zadeklarowane zarówno w WCHAR.H, jak też w pliku nagłówkowym, w któ- rym deklarowane sš zwykłe funkcje. Utrzymywanie pojedynczego Ÿródła Oczywiœcie, stosowanie unikodu ma pewne wady. Pierwszš i najważniejszš jest to, że każdy łańcuch znaków w programie potrzebuje dwukrotnie więcej miej- sca. W dodatku, możesz zauważyć, że funkcje obsługi znaków szerokich z bi- blioteki wykonawczej sš większe niż zwykłe funkcje. Z tego powodu możesz dšżyć do tworzenia dwóch wersji programu -jednej z łańcuchami znaków ASCII i drugiej - w unikodzie. Najlepszym rozwišzaniem byłby pojedynczy plik kodu Ÿródłowego, który mógłbyœ kompilować z łańcuchami znaków w ASCII albo w unikodzie. Stanowi to pewien problem, ponieważ funkcje biblioteki wykonawczej majš różne nazwy, różnie definiujesz znaki i kłopot sprawia poprzedzanie stałych łańcuchów znaków dużym L. Jednym z rozwišzań może być użycie pliku nagłówkowego TCHAR.H, dołšczo- nego do Visual C++ Microsoftu. Ten plik nagłówkowy nie jest częœciš standardu ANSI C, więc każda funkcja i makrodefinicja jest w nim poprzedzona podkreœle- niem. TCHAR.H dostarcza zestawu altematywnych nazw dla standardowych funkcji biblioteki wykonawczej, wymagajšcych parametrów w postaci łańcuchów (np. tprintf i tcslen). Czasami okreœlamy je jako "ogólne" nazwy funkcji, ponie- ' waż mogš odnosić się zarówno do unikodowej, jak też i do nie unikodowej wer- sji funkcji. Jeżeli identyfikator o nazwie LTMCODE jest zdefiniowany, a plik nagłówkowy TCHAR.H włšczony do twojego programu, tcslen jest zdefiniowana jako wcslen: lldefine tcslen wcslen Jeżeli LTMCODE nie jest zdefiniowany, tcslen jest definiowana jako strlen: tldefine tcslen strlen 30 Częœć I: Podstawy I tak dalej. TCHAR.H rozwišzuje też problem dwóch typów danych znakowych za pomocš nowego typu danych o nazwie TCHAR. Jeżeli identyfikator UNI- CODE jest zdefiniowany, TCHAR jest definiowany jako wchar t: typedef wchar t TCHAR ; W przeciwnym przypadku TCHAR to po prostu char: typedef char TCHAR ; Teraz nadszedł czas rozwišzania problemu L ze stałš łańcuchowš. Jeœli identyfi- kator IJNICODE jest zdefiniowany, definiowane jest także następujšce makro o nazwie T: ţţdefine T(x) Lţţiţx To jest doœć zawiła składnia, ale zgodna ze standardem ANSI C dla preprocesora C. Ta para znaków hash jest nazywana "wstawieniem leksemu" (ang. token paste) i powoduje dodanie L do parametru makra. Jeœli więc parametrem makra jest "Hel- lo!", L##x oznacza L"Hello!". Jeżeli identyfikator LTNICODE nie jest zdefiniowany, makro T jest zdefinio- wane po prostu w ten sposób: iţdefine T(x) x Niezależnie od tego, dwa inne makra sš też zdefiniowane jako T: ţţdefine T(x) T(x) łţdefine TEXT(x) T(x) Którego z nich użyjesz dla swojego programu konsoli Win32, zależy od tego, czy chcesz być bardziej zwięzły, czy też rozgadany. Zasadniczo musisz zdefiniować swój łańcuch znaków wewnštrz makra T albo TEXT w następujšcy sposób: TEXT ("Hello!") Dzięki temu, jeœli zdefiniowany jest identyfikator UNICODE, łańcuch interpre- towany jest jako składajšcy się ze znaków szerokich, a w przeciwnym razie, jako składajšcy się ze znaków 8-bitowych. Szerokie znaki i Windows Windows NT całkowicie korzysta z unikodu. Oznacza to, że wewnętrznie uży- wa łańcuchów składajšcych się ze znaków 16-bitowych. Ponieważ większoœć pro- gramów jeszcze nie używa 16-bitowych znaków, Windows NT musi często kon- wertować łańcuchy znaków przy przekazywaniu ich z lub do systemu. Pod Win- dows NT można uruchamiać programy łańcuch znakówane dla ASCII, unikodu albo dla mieszaniny ASCII i unikodu. W tym celu Windows NT zawiera oddziel- ne funkcje API, które akceptujš 8-bitowe lub 16-bitowe łańcuch znakówy. (Nie- długo zobaczymy, jak to działa). Windows 98 znacznie mniej korzysta z unikodu niż Windows NT. Tylko niektóre funkcje Windows 98 umożliwiajš korzystanie z łańcuchów znaków szerokich. (Funkcje te sš wymienione w artykule Q125671 Microsoft Knowledge Base; jest tam także MessageBox). Jeœli chcesz rozprowadzać pojedynczy plik EXE, który ma się uruchamiać w œrodowisku Windows NT i Windows 98, nie powinien on opie- rać się na unikodzie, bo nie uruchomi się w œrodowisku Windows 98; dokładniej, program nie powinien wywoływać unikodowych wersji funkcji Windows. Jeœli Rozdział 2: Wprowadzenie do unikodu 31 jednak chcesz być przygotowany na stworzenie wersji unikodowej twojego pro- gramu, prawdopodobnie powinieneœ utworzyć pojedyncze Ÿródło, które może być skompilowane albo dla ASCII, albo dla unikodu. Programy w tej ksišżcţ sš łańcuch znakówane właœnie w ten sposób. Typy plików nagłówkowych Windows Jak już widziałeœ w pierwszym rozdziale, program Windows włšcza plik nagłów- kowy WINDOWS.H. Ten plik włšcza wiele innych plików nagłówkowych, mię- dzy innymi WINDEF.H, który zawiera wiele typów podstawowych, używanych w Windows, i który z kolei włšcza WINNT.H. Plik WINNT.H umożliwia korzy- stanie z unikodu. WINNT.H rozpoczyna się od włšczenia pliku nagłówkowego C - CTYPE.H, któ- ry jest jednym z wielu plików zawierajšcych definicję wchar t. WINNT.H defi- niuje nowe typy danych, zwane CHAR i WCHAR: typedef char CHAR ; typedef wchar t WCHAR ; // szerokie znaki CHAR i WCHAR sš typami danych zalecanymi do użytku w programach Win- dows, gdy musisz zdefiniować znak 8-bitowy lub 16-bitowy. Komentarz nastę- pujšcy po definicji WCHAR jest sugerowanym przedrostkiem notacji węgierskiej: zmienna oparta na typie WCHAR może być poprzedzona literami zuc, aby za- znaczyć, że jest to szeroki znak. Plik nagłówkowy WINNT.H definiuje dalszych szeœć typów danych, których możesz używać jako wskaŸników do 8-bitowych łańcuch znakówów i cztery typy danych, których możesz używać jako wskaŸników do 8-bitowych łańcuch zna- kówów const. Trochę skondensowałem prawdziwe instrukcje z pliku nagłówko- wego, aby je tutaj przedstawić: typedef CHAR * PCHAR, * LPCH, * PCH, * NPSTR, *LPSTR, * PSTR ; typedef CONST CHAR * LPCCH, * PCCH, * LPCSTR, * PCSTR ; Przedrostki N i L oznaczajš "near" i "long" i miały znaczenie w 16-bitowych Windows z powodu różnych wielkoœci wskaŸników. W Win32 nie ma różnicy między wskaŸnikami bliskimi i dalekimi. Podobnie WINNT.H definiuje szeœć typów danych, których możesz używać jako wskaŸników do 16-bitowych łańcuchów znaków, i cztery typy danych, których możesz używać jako wskaŸników do 16-bitowych łańcuchów const: typedef WCHAR * PWCHAR, * LPWCH, * PWCH, * NPWSTR, *LPWSTR, * PWSTR ; typedef CONST WCHAR * LPCWCH, * PCWCH, * LPCWSTR, * PCWSTR ; Mamy więc typ danych CHAR (8-bitowy char) i WCHAR (16-bitowy wchar t) oraz wskaŸniki do CHAR i WCHAR. Podobnie jak TCHAR.H, WINNT.H definiuje TCHAR jako ogólny typ danych znakowych. Jeœli jest zdefiniowany identyfika- tor UNICODE (bez podkreœlenia), TCHAR i wskaŸniki do TCHAR sš oparte na WCHAR i wskaŸnikach do WCHAR; jeœli identyfikator UNICODE nie jest zdefi- niowany, TCHAR i wskaŸniki do TCHAR sš oparte na char i wskaŸnikach do char: ţţifdef UNICODE typedef WCHAR TCHAR, * PTCHAR ; typedef LPWSTR LPTCH, PTCH, PTSTR, LPTSTR ; 32 Częœć I: Podstawy typedef LPCWSTR LPCTSTR ; #else typedef char TCHAR, * PTCHAR ; typedef LPSTR LPTCH, PTCH, PTSTR, LPTSTR ; typedef LPCSTR LPCTSTR ; ţiendi f Oba pliki nagłówkowe, WINNT.H i WCHAR.H, chroniš przed redefiniowaniem typu danych TCHAR, jeœli został on już zdefiniowany przez jeden z nich. Jednak jeœli używasz różnych plików nagłówkowych w twoim programie, powinieneœ włšczyć w pierwszej kolejnoœci plik WINDOWS.H. Plik nagłówkowy ţ.H definiuje także makro, które dopisuje L przed pierw- szym znakiem cudzysłowu w łańcuchu znaków. Jeœli identyfikator UNICODE zo- stał zdefiniowany, makro zwane TEXT jest zdefiniowane następujšco: 4idefine TEXT(quote) Lţţquote Jeœli identyfikator UNICODE nie został zdefiniowany, makro TEXT wyglšda następujšco: ţidefine TEXT(quote) quote Niezależnie od tego, makro TEXT jest zdefiniowane następujšco: ţţdefine TEXT(qoute) TEXT(quote) Definicja ta przypomina definicję makra TEXT w TCHAR.H, tyle że nie ma tu- taj tego kłopotliwego podkreœlenia. W tej ksišżce używam makra w wersji TEXT. Te definicje pozwalajš mieszać łańcuchy znaków ASCII i unikodu w jednym pro- gramie lub łańcuch znakówaćjeden program, który może być skompilowany dla ASCII albo dla unikodu. Jeœli chcesz jawnie definiować 8-bitowe zmienne znako- we lub łańcuchy znaków, użyj CHAR, PCHAR (albo innego) i łańcuchów zna- ków w cudzysłowach. Jeœli chcesz definiować 16-bitowe zmienne znakowe lub łańcuchy znaków, użyj WCHAR, PWCHAR i dopisz L przed cudzysłowami. Przy zmiennych, które majš być 8-bitowe albo 16-bitowe, zależnie od definicji identy- fikatora UNICODE, użyj TCHAR, PTCHAR i makra TEXT. Wywołania funkcji Windows W 16-bitowych wersjach Windows, poczynajšc od Windows 1.0, a kończšc na Win- dows 3.1, funkcja MessageBox była umieszczona w dołšczanej dynamicznie biblio- tece USER.EXE. W pliku nagłówkowym WINDOWS.H, dołšczonym do SDK dla Windows 3.1, funkcja MessageBox była zdefiniowana następujšco: int WINAPI MessageBox (HWND, LPCSTR, LPCSTR, UINT) ; Zauważ, że drugi i trzeci argument funkcji sš wskaŸnikami do stałych łańcuchów znaków (ang. constant character strings). Gdy program Winl6 jest kompilowany i łš- czony, Windows pozostawia wywołanie MessageBox nie powišzane. Tabela w pli- ku EXE programu pozwala Windows na dynamiczne połšczenie wywołania w pro- gramie z funkcjš MessageBox znajdujšcš się w bibliotece USER. 32-bitowe wersje Windows (to znaczy wszystkie wersje Windows NT oraz Win- dows 95 lub Windows 98) zawierajš zarówno USER.EXE, co gwarantuje kompa- tybilnoœć z 16-bitowymi programami, jak i bibliotekę dynamicznš, zwanš USER32.DLL, która zawiera punkty startowe dla 32-bitowych wersji funkcji pod- systemu USER, włšczajšc w to MessageBox. Rozdział 2: Wprowadzenie do unikodu 33 I tu jest klucz do obsługi unikodu w Windows. W USER32.DLL nie ma punktu wejœcia dla 32-bitowej funkcji MessageBox. Zamiast tego sš dwa inne punkty wej- œcia: jeden dla funkcji MessageBoxA (wersja ASCII) i drugi dla funkcji Message- BoxW (wersja dla znaków szerokich). Każda funkcja Win32, która wymaga łań- cucha znaków jako argumentu, ma dwa punkty startowe w systemie operacyj- nym! Na szczęœcie nie musisz stale o tym pamiętać. Możesz zwyczajnie używać MessageBox w swoich programach. Podobnie jak plik nagłówkowy TCHAR, róż- ne pliki nagłówkowe Windows wykonujš konieczne sztuczki. Oto jak funkcja MessageBoxA zdefiniowana jest w WINUSER.H. Zwróć uwagę na podobieństwo do wczeœniejszej definicji MessageBox. WINUSERAPI int WINAPI MessageBoxA (HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType) ; A oto MessageBoxW: WINUSERAPI int WINAPI MessageBoxW (HWND hWnd, LPCWSTR lpText, LPCWSTR lpCaption, UINT uType) ; Zauważ, że drugi i trzeci parametr funkcji MessageBoxW sš wskaŸnikami do łań- cucha składajšcego się ze znaków szerokch. Możesz używać niezależnie tunkcji MessageBoxA i MessageBoxW w swoich pro- gramach Windows, jeœli musisz mieć dostęp do funkcji ASCII i do funkcji ze zna- kami szerokimi. Ale większoœć programistów będzie dalej używać MessageBox, co jest równoważne MessageBoxA lub MessageBoxW, zależnie od tego, czy zdefi- niowano UNICODE. Oto doœć prosty kod w WINUSER.H, który wykonuje tę sztuczkę: ţţifdef UNICODE ţţdefine MessageBox MessageBoxW ţţel se ţţdefine MessageBox MessageBoxA ţţendif Tak więc wszystkie wywołania funkcji MessageBox, które znajdujš się w twoim programie, będš naprawdę wywołaniami funkcji MessageBoxW, jeœli jest zdefinio- wany identyfikator UNICODE, lub funkcji MessageBoxA, jeœli nie jest zdefinio- wany. Gdy uruchamiasz program, Windows łšczy różne wywołania funkcji w twoim programie z punktami startowymi w różnych bibliotekach dynamicznych Win- dows. Jednak, z kilkoma wyjštkami, większoœć funkcji Windows w wersjach uni- kodu nie jest obecna w Windows 98. Te funkcje majš punkty startowe, ale za- zwyczaj zwracajš one kod błędu. Do aplikacji należy uporanie się z tym błędem. Funkcje obsługi łańcuchów znaków w Windows Miałem już okazję nadmienić, że kompilator C Microsoftu zawiera zarówno wersje ogólne, jak i wersje dla znaków szerokich tych wszystkich funkcji biblioteki wy- konawczej, które wymagajš łańcuchów znaków jako argumentów. Windows zaœ powiela niektóre z nich. Jako przykład podaję spis funkcji zdefiniowanych w Win- dows, które obliczajš długoœć, kopiujš, łšczš i porównujš łańcuchy znaków: 34 Częœć I: Podstawy ILength = lstrlen (pString) ; pString = lstrcpy (pStringl, pString2) ; pString = lstrcpyn (pStringl, pString2, iCount) ; pString = lstrcat (pStringl, pString2) ; iComp = lstrcmp (pStringl, pString2) ; iComp = lstrcmpi (pStringl, pString2) ; Działajš one podobnie, jak ich odpowiedniki z biblioteki C. Jeœli zdefiniowany jest identyfikator UNICODE, akceptujš one łańcuchy znaków szerokich, a jeœli nie - zwykłe łańcuchy znaków. Wersja funkcji IstrlenW dla znaków szerokich jest zrealizowana w Windows 98. , Użycie printf w Windows Zdarza się, że programiœci, którzy wyroœli na modelu programowania znakowe- go w C w wierszu poleceń, nadmiernie lubiš funkcję printf. To nie przypadek, że w programie "hello, world!" Kernighana i Ritchiego występuje właœnie printf, mimo że można było posłużyć się prostszš funkcjš, takš jak puts. Każdy wie, że rozszerzanie programu "hello, world!" może ewentualnie wymagać formatowa- nego wyjœcia tekstowego funkcji printf, więc równie dobrze można wprowadzić jš już na poczštku. Zła wiadomoœć: nie możesz używać printf w programie Windows. Chociaż mo- żesz stosować większoœć biblioteki wykonawczej C w programach Windows (wie- lu programistów woli używać funkcji zarzšdzania pamięciš i plikowego I/O z C, niż ich odpowiedników z Windows), w Windows nie istnieje pojęcie standardo- wego wejœcia i standardowego wyjœcia. W programie Windows możesz używać fprintf, ale nie printf. Dobra wiadomoœć: nadal możesz wyœwietlać tekst, używajšc sprintf i innych funk- cji z jej rodziny. Działajš one jak printf, z tš różnicš, że zapisujš formatowane wyjœcie do bufora znaków, który jest dostarczany jako pierwszy argument. Na- stępnie możesz robić co chcesz z tym łańcuchem znaków (na przykład przekazać go do MessageBox). Jeżeli nigdy nie miałeœ okazji używać sprintf (tak jak ja, kiedy zaczynałem pro- gramowanie w Windows), podam tu krótki opis. Przypomnij sobie, że funkcja printf jest zadeklarowana następujšco: int printf (const char * szFormat, ...) ; Pierwszym argumentem jest formatujšcy łańcuch znaków, a po nim następuje pewna liczba argumentów różnych typów, odpowiadajšcych formatujšcemu łań- cuchowi znaków. Funkcja sprintf jest zdefiniowana następujšco: int sprintf (char * szBuffer, const char * szFormat, ...) Pierwszym argumentem jest bufor znaków; po nim formatujšcy łańcuch znaków. Zamiast wypisywać sformatowany wynik na standardowe wyjœcie, sprintf wpi- suje go do bufora szBuffer. Funkcja zwraca długoœć łańcucha znaków. Przy pro- gramowaniu w trybie znakowym instrukcja printf ("Suma %i i %i wynosi %i", 5, 3, 5+3) ; jest funkcjonalnie równoważna T Rozdział 2: Wprowadzenie do unikodu 35 char szBuffer C100) ; sprintf (szBuffer, "Suma %i i %i wynosi %i", 5, 3, 5+3) ; puts (szBuffer) ; W Windows możesz używać MessageBox zamiast puts do wyœwietlenia rezulta- tów. Niemal każdy programista natknšł się na Ÿle działajšcš funkcję printf. Może być ona przyczynš zawieszenia się programu, gdy cišg formatujšcy nie odpowiada kolejnoœci zmiennych, które majš być sformatowane. W przypadku sprintf będziesz musiał troszczyć się nie tylko o to, ale jeszcze o innš rzecz: bufor znaków musi mieć dostatecznš wielkoœć, aby zmieœcić wynik. Niestandardowa funkcja Micro- softu, snprintf, rozwišzuje ten problem, za pomocš argumentu, który okreœla wiel- koœć bufora w znakach. Odmianš sprintf jest funkcja vsprintf, która ma tylko trzy argumenty. Funkcja vsprintf jest używana do zrealizowania własnych funkcji, które muszš wykony- wać, podobne do printf, formatowanie zmiennej liczby argumentów. Pierwsze dwa. argumenty vsprintf sš takie same jak w sprintf bufor znaków do przechowywa- nia wyniku i formatujšcy łańcuch znaków. Trzeci argument jest wskaŸnikiem do tablicy argumentów, które majš być sformatowane. W praktyce ten wskaŸnik wišże zmienne, które sš przechowywane na stosie podczas przygotowywania wywołania funkcji. Makra va_list, v„ start i v„ end (zdefiniowane w STDARG.H) pomagajš obsłużyć ten wskaŸnik do stosu. Program SCRNSIZE, przedstawiony na końcu tego rozdziału, pokazuje, jak używać tych makr. Funkcja sprintf może być łańcuch znakówana z wykorzystaniem logiki vsprintf w następujšcy sposób: int sprintf (char * szBuffer, const char * szFormat, ...) ( int iReturn ; vaţlist pArgs ; va_start (pArgs, szFormat) ; iReturn = vsprintf (szBuffer, szFormat, pArgs) ; vaţend (pArgs) ; return iReturn ; Makro v„ start ustawia pArţ, by wskazywał na następnš zmiennš na stosie, po- wyżej argumentu szFormat. Tak wiele dawniejszych programów Windows używało sprintf i vspintf, że Micro- soft dodał do API Windows dwie podobne funkcje. Funkcje Windows wsprintf i zuvsprintf sš funkcjonalnymi odpowiednikami sprir2tf i vsprintf, z wyjštkiem tego, że nie obsługujš formatowania liczb zmiennoprzecinkowych. Oczywiœcie, wraz z pojawieniem się znaków szerokich, urosła liczba funkcji sprintf, tworzšc dziwnš mieszaninę nazw funkcji. Oto tabela, która pokazuje wszystkie funkcje sprintf zawarte w bibliotece wykonawczej C Microsoftu i w Windows. 36 Częœć I: Podstawy Funkcje ASCII dla znaków szerokich ogólne Zmienna liczba argumentów Wersja standardowa sprintf swprintf stprintf Wersja o maksy- snprintf sriwprintf sntprintf malnej długoœci Wersja Windows wsprintfA wsprintf4V wsprintf WskaŸnik do tablicy argumentów Wersja standardowa vsprintf vswprintf vstprintf Wersja o maksy- vsnprintf vsnwprintf vsntprintf malnej długoœci Wersja Windows wvsprintfA wvsprintfYV wvsprintf W wersjach funkcji sprintf obshxgujšcych znaki szerokie, bufor znaków jest też zde- finiowany jako łańcuch znaków szerokich. W wersjach wszystkich tych funkcji do obsługi znaków szerokich, formatujšcy łańcuch znaków też musi być złożony ze znaków szerokich. Jednak musisz być pewny, że wszystkie łańcuchy znaków, któ- re przekazujesz do tych funkcji, sš także złożone ze znaków szerokich. Formatowanie okna komunikatu Program SCRNSIZE z rysunku 2-3 pokazuje, jak zrealizować funkcję MessageBox- Printf, która pobiera zmiennš liczbę argumentów i formatuje je tak jak printf. SCRPISIZE.C /* SCRNSIZE.C - Wyœwietla w oknie komunikatów rozmiar ekranu (c) Charles Petzold. 1998 */ #include ifinclude #include int CDECL MessageBoxPrintf (TCHAR * szCaption, TCHAR * szFormat, ...) l TCHAR szBuffer [1024] ; vaţlist pArgList ; // Makro va start (zdefiniowane w STDARG.H) jest zwykle równoważne: // pArgList = (char *) &szFormat + sizeof (szFormat) ; r Rozdział 2: Wprowadzenie do unikodu 37 vaţstart (pArgList, szFormat) ; // Ostatni argument wvsprintf wskazuje na argumenty vsntprintf (szBuffer, sizeof (szBuffer) / sizeof (TCHAR), szFormat, pArgList) ; /! makro vaţend tylko czyţci pArgList, bez wyraŸnej przyczyny va ęnd (pArgList) ; return MessageBox (NULL, szBuffer, szCaption, 0) ; int WINAPI WinMain (HINSTANCE hlnstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow> int cxScreen, cyScreen ; cxScreen = GetSystemMetrics (SM CXSCREEN) ; cyScreen = GetSystemMetrics (SM CYSCREEN) : MessageBoxPrintf (TEXT ("ScrnSize"), TEXT ("The screen is ţi pixels wide by ii pixels high."), cxScreen, cyScreen) ; return 0 ; Rysunek 2-3. Program SCRNSIZE Program wyœwietla szerokoœć i wysokoœć ekranu w pikselach, posługujšc się in- formaejami zwróconymi przez funkeję GetSystemMetrics. Funkeja GetSystemMe- trics służy do pobierania informacji o rozmiarach różnych obiektów w Windows. W rozdziale 4 pokażę, jak używać funkcji GetSystemMetrics, aby wyœwietlić i prze- wijać wiele linii tekstu w oknie Windows. Wielojęzycznoœć i ta ksišżka Przygotowanie twojego programu Windows na rynek międzynarodowy nie ogra- nicza się jedynie do użycia unikodu. Zagadnienie wielojęzycznoœci nie mieœci się w tematyce tej ksišżki, ale jest dokładnie opisane w ksišżce Nadine Kano Develo- ping International Software for Windows 95 and Windozus NT (Microsoft Press, 1995). Nasza ksišżka ogranicza się do pokazywania programów, które mogš być skom- pilowane ze zdefiniowanym identyfikatorem UNICODE albo bez niego. Wyma- ga to wprowadzenia TCHAR we wszystkich definicjach znaków i łańcuchów zna- ków, używania makra TEXT dla literałów (stałych) łańcuchów znaków i rozróż- niania bajtów i znaków. Na przykład zobacz wywołanie vsntprintf w SCRNSI- ZE. Drugi argument jest wielkoœciš bufora w znakach. Zazwyczaj użyłbyœ size- of(szBvffer). Ale jeœli bufor ma znaki szerokie, nie będzie to wielkoœć bufora w znakach, ale wielkoœć bufora w bajtach. Musisz podzielić to przez sizeof(TCHAR). Zwykle w Visual C++ Developer Studio możesz skompilować program w dwóch różnych konfiguracjach: Debug i Release. Aby rzecz ułatwić, dla przykładowych programów z tej ksišżki zmodyfikowałem konfigurację Debug tak, że definio- 38 Częœć I: Podstawy wany jest identyfikator UNICODE. W tych programach, które używajš funkcji bibliotecznych C wymagajšcych argumentów w postaci łańcucha znaków, w kon- figuracji Debug definiowany jest także identyfikator UNICODE. (Aby zobaczyć, jak to jest zrobione, wybierz Settings z menu Project i kliknij kartę C/C++). Tak oto programy mogš być w prosty sposób powtórnie kompilowane i dołšczane do testów. Wszystkie programy w tej ksišżce -niezależnie, czy skompilowane dla unikodu, czy nie - uruchamiajš się w Windows NT. Z kilkoma wyjštkami programy skom- pilowane dla unikodu z tej ksišżki nie uruchomiš się w œrodowisku Windows 98, ale wersje nieunikodowe będš działać. Programy w tym i w pierwszym rozdzia- le stanowiš te nieliczne wyjštki. MessageBoxW jest jednš z niewielu funkcji Win- dows dla znaków szerokich, które działajš w œrodowisku Windows 98. Jeœli za- stšpisz funkcję vsntprintf w SCRNSIZE.C funkcjš Windows wprintf (będziesz mu- siał także pozbyć się drugiego argumentu funkcji), wersja unikodowa SCRNSI- ZE.C nie uruchomi się w œrodowisku Windows 98, ponieważ Windows 98 nie umożliwia korzystania z wprintfW. Jak zobaczymy dalej w tej ksišżce (zwłaszcza w rozdziale 6, który omawia uży- cie klawiatury), nie jest łatwo napisać program, który obsługuje dwubajtowe ze- stawy znaków dla dalekowschodnich wersji Windows. Nasza ksišżka nie poka- zuje, jak to zrobić, i dlatego niektóre nieunikodowe wersje programów z tej ksišżki nie będš dobrze działać z dalekowschodnimi wersjami Windows. Właœnie dlate- go unikod jest tak ważny dla przyszłoœci programowania. Unikod pozwala pro- gramom na łatwiejsze przekraczanie barier językowych. Rozdział 3 ' ' nikat w ţ komu s Wţ n o Przykładowe programy z pierwszych dwóch rozdziałów odwołujš się do funkcji MessageBox, aby dostarczyć użytkownikowi wyjœcia tekstowego. Funkcja Message- Box tworzy "okno". W Windows słowo "okno" ma dokładnie okreœlone znaczenie. Okno to prostokštny obszar na ekranie, który odbiera polecenia użytkownika i wy- œwietla dane wyjœciowe w formie tekstu i grafiki. Funkcja MessageBox tworzy okno, ale jest to okno specjalne, o ograniczonej ela- stycznoœci. Okno komunikatu zawiera pasek tytułu z przyciskiem zamknięcia, opcjonalnie ikonę, jednš albo więcej linii tekstu oraz do czterech przycisków. Ale ikony i przyciski muszš być wybrane z małego zbioru, który udostępnia Win- dows. Funkcja MessageBox na pewno jest użyteczna, ale nie zajedziemy z niš daleko. W oknie komunikatów nie możemy wyœwietlać grafiki i nie możemy też dodać menu. Dlatego nadszedł już czas, aby utworzyć własne okno. Własne okno Tworzenie okna jest tak łatwe, jak wywołanie funkcji CreateWindow. No, może nie całkiem. Chociaż funkcja tworzenia okna naprawdę nazywa się Cre- ateWindow i możesz znaleŸć jej dokumentację w /Platform SDK/Llser Interface Seroi- ces/Windowing/Windows/Window Reference/Window Functions, to okazuje się, że pierw- szym jej argumentem jest coœ, co okreœlone jest jako nazwa klasy okna, a ta klasa okna jest połšczona z czymœ, co nazywa się procedurš okna. Zanim spróbujemy wywołać CreateWindow, przyda nam się trochę dodatkowych informacji. Przeglšd architektury Kiedy programujesz w Windows, zajmujesz się pewnym rodzajem programowa- nia obiektowego. Najbardziej oczywistym obiektem - z którym będziesz najczę- œciej pracował w Windows, od którego pochodzi nazwa Windows, któremu na- wet nadaje się cechy antropomorficzne, który może nawet pojawiać się w twoich snach -jest obiekt znany jako okno (ang. window). Najczęœciej spotykasz się z oknami aplikacji, które ozdabiajš twój pulpit. Zawie- rajš one pasek tytułu pokazujšcy nazwę programu, menu, być może także pasek narzędzi i pasek przewijania. Innym rodzajem okna jest okno dialogowe, które może nie mieć paska tytułu. 40 Częœć I: Podstawy Mniej oczywiste sš różne rodzaje zwykłych przycisków, pól opcji, pól wyboru, pól listy, pasków przewijania i pól tekstowych, które ozdabiajš powierzchnię okien dialogowych. Każdy z tych małych obiektów wizualnych jest oknem. Sš one na- zywane "oknami potomnymi" albo "oknami kontrolki", albo "kontrolkami okna potomnego". Użytkownik widzi te okna jako obiekty na ekranie i oddziałuje na nie bezpoœred- nio, używajšc klawiatury lub myszy. Co interesujšce, punkt widzenia programi- sty jest tu identyczny z punktem widzenia użytkownika. Okno odbiera informa- cje wejœciowe użytkownika w formie "komunikatów". Okno porozumiewa się z innymi oknami także za pomocš komunikatów. Zrozumienie istoty komunika- tów jest ważnš częœciš nauki programowania w Windows. Oto przykład komunikatów Windows: jak wiesz, większoœć programów Windows ma okno aplikacji o zmiennych rozmiarach. Oznacza to, że możesz uchwycić myszš ramkę okna i zmienić jego wielkoœć. Najczęœciej program odpowie na to zmianš zawartoœci okna. Możesz sšdzić (i będziesz miał rację), że to Windows, a nie aplikacja, obsługuje cały kod zwišzany ze zmianš wielkoœci okna przez użyt- kownika. Jednak aplikacja "wie", że wielkoœć okna została zmieniona, ponieważ potrafi zmienić format swojego wyœwietlania. Skšd aplikacja wie, że użytkownik zmienił rozmiary okna? Programista przyzwy- czajony do konwencjonalnego programowania w trybie znakowym nie miał ni- gdy do czynienia z żadnym mechanizmem systemu operacyjnego, który przeno- siłby informację tego rodzaju do użytkownika. Uzyskanie odpowiedzi na to py- tanie ma zasadnicze znaczenie dla zrozumienia architektury Windows. Kiedy użytkownik zmienia wielkoœć okna, Windows wysyła do programu komunikat z nowymi rozmiarami okna. Program może wtedy dostosować zawartoœć okna do jego nowej wielkoœci. Windows wysyła komunikat do programu". Mam nadzieję, że nie przeczytałeœ tego " bez zastanowienia. Co to oznacza? Mówimy przecież o kodzie programu, a nie o sys- temie telegraficznym. Jak system operacyjny może wysłać komunikat do programu? Kiedy mówię, że Windows wysyła komunikat do programu, mam na myœli to, że Windows wywołuje funkcję znajdujšcš się w twoim programie i będšcš zasadni- czš częœciš kodu tego programu. Parametry tej funkcji opisujš konkretny komu- nikat wysyłany przez Windows i odbierany przez twój program. Ta funkcja w two- im programie jest nazywana procedurš okna (ang. window procedure). Niewštpliwie jesteœ przyzwyczajony do idei programu, który wywołuje funkcje systemu operacyjnego. Na przykład może to być program otwierajšcy pliki dys- kowe. Ale możesz być zaskoczony tym, że to system operacyjny wywołuje funk- cje programu. Jednak właœnie to stanowi podstawę architektury Windows. Każde okno, które jest tworzone przez program, ma powišzanš procedurę okna. Ta procedura to funkcja, która może znajdować się albo w samym programie, albo w dołšczanej dynamicznie bibliotece. Windows wysyła komunikat do okna, wy- wohzjšc procedurę okna. Procedura okna wykonuje pewne przetwarzanie zwiš- zane z komunikatem i zwraca sterowanie do Windows. Mówišc dokładniej, okno zawsze jest tworzone w oparciu o klasę okna (ang. win- dow class). Klasa okna okreœla procedurę okna, która przetwarza komunikaty skie- T Rozdział 3: Windows i komunikaty 41 rowane do okna. Stosowanie klasy okna pozwala na oparcie wielu okien na tej samej klasie okna i na użycie tej samej procedury okna. Na przykład wszystkie przyciski we wszystkich programach Windows odwołujš się do tej samej klasy okna. Ta klasa okna jest powišzana z procedurš okna, znajdujšcš się w bibliotece dynamicznej Windows, przetwarzajšcš komunikaty do wszystkich okien przyci- sków. Obiekt w programowaniu obiektowym to połšczenie kodu i danych. Obiektem jest okno. Kod to procedura okna. Dane to informacja zachowana przez procedurę okna i przez Windows dla każdego okna i każdej klasy okna, które istniejš w systemie. Procedura okna przetwarza komunikaty skierowane do okna. Bardzo często te komunikaty informujš okno o poleceniach użytkownika wprowadzanych z kla- wiatury albo myszš. Na przykład stšd okno przycisku wie, że zostało "kliknię- te". Inne komunikaty mówiš oknu, że jego wielkoœć uległa zmianie lub że po- wierzchnia okna wymaga ponownego narysowania (odœwieżenia). Kiedy program windowsowy rozpoczyna działanie, Windows tworzy kolejkę komunikatów (ang. message queue) dla tego programu. Sš w niej przechowywane komunikaty do wszystkich okien utworzonych przez program. Aplikacja Win- dows zawiera krótki fragment kodu zwany pętlš komunikatów (ang. message loop), który pobiera komunikaty z kolejki i przesyła je do odpowiedniej procedury okna. Inne komunikaty sš wysyłane bezpoœrednio do procedury okna, bez umieszcza- nia ich w kolejce komunikatów. Jeœli zaczynasz już przysypiać œlęczšc nad tym nadmiernie abstrakcyjnym opisem architektury Windows, to najwyższa pora na przyjrzenie się, jak okno, klasa okna, procedura okna, kolejka komunikatów, pętla komunikatów i komunikaty okna współdziałajš ze sobš w prawdziwym programie. Program HELLOWIN Utworzenie okna wymaga zarejestrowania najpierw klasy okna, co z kolei nie jest możliwe bez procedury okna, która przetwarza komunikaty kierowane do okna. Potrzebne sš pewne dodatkowe elementy, które występujš w niemal każdym programie Windows. HELLOWIN, pokazany na rysunku 3-1, jest prostym pro- gramem pokazujšcym większoœć tych dodatkowych elementów. HELLOWIN.C /* HELLOWIN.C -- Wyœwietla napis "Hello. Windows 98!" w obszarze roboczym okna (c) Charles Petzold, 1998 */ llinclude LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) - 42 Częœć I: Podstawy (cišg dalszy ze str. 41) ( static TCHAR szAppName[] = TEXT ("HelloWin") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CS VREDRAW ; ! wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITEţBRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; ' if (!RegisterClass (&wndclass)) MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MBţICONERROR) ; return 0 ; ( hwnd = CreateWindow (szAppName, // nazwa klasy okna TEXT ("The Hello Program"), // naglówek okna WS_OVERLAPPEDWINDOW, // styl okna CW_USEDEFAULT, // poczštkowa pozycja x CW_USEDEFAULT, // poczdtkowa pozycja y CW_USEDEFAULT, // poczdtkowa wielkoœć x CW_USEDEFAULT, // poczštkowa wielkoœć y NULL, // uchwyt okna nadrzędnego NULL, // uchwyt menu okna hInstance, // uchwyt kopii programu NULL) ; // parametry tworzenia ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) TranslateMessage (&msg) ; DispatchMessage (&msg) ; ) return msg.wParam ; LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ( HDC hdc ; PAINTSTRUCT ps ; RECT rect ; switch (message) ( case WM_CREATE: PlaySound (TEXT ("hellowin.wav"), NULL, SNDţFILENAME ţ SND ASYNC) ; return 0 ; Rozdział 3: Windows i komunikaty 43 , case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; GetClientRect (hwnd, &rect) ; DrawText (hdc, TEXT ("Hello, Windows 98!"), -1, &rect, DT_SINGLELINE ţ DT CENTER ţ DT VCENTER) ; I EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: PostOuitMessage (0) ; return 0 ; l return DefWindowProc (hwnd, message, wParam, lParam) ; J Rysunek 3-1. Program HELLOWIN Ten program tworzy standardowe okno aplikacji, takie jak na rysunku 3-2, i wy- œwietla napis "Hello, Windows 98" poœrodku okna. Jeżeli masz zainstalowanš kartę dŸwiękowš, możesz także usłyszeć, jak mówię ten tekst. Rysunek 3-2. Okno programu HELLOWIN Kilka ostrzeżeń: jeżeli używasz Microsoft Visual C++ do stworzenia nowego pro- jektu dla tego programu, musisz dodać kilka rzeczy do bibliotek wynikowych używanych przez konsolidator. Wybierz Settings z menu Project i przejdŸ na kartę Link. Wybierz General z listy Category i dodaj WINMM.LIB (Windows multime- dia) do pola tekstowego Object/Library Modules. Musisz to zrobić, ponieważ HEL- LOWIN używa wywołań funkcji multimedialnych, a biblioteka multimedialna nie jest domyœinie dołšczana do projektu. Jeœli tego nie zrobisz, otrzymasz od konsolidatora komunikat o błędzie, informujšcy, że nie ma funkcji PlaySound. 44 Częœć I: Podstawy HELLOWIN używa pliku HELLOWIN.WAV, który znajduje się w katalogu HEL- LOWIN na CD-ROM-ie dołšczonym do tej ksišżki. Gdy uruchamiasz HELLO- WIN.EXE, domyœlnym katalogiem musi być HELLOWIN. Tak się dzieje, gdy uru- chamiasz program z wnętrza Visual C++, nawet jeœli pliki wykonawcze znajdujš się w podkatalogu RELEASE lub DEBUG katalogu HELLOWIN. Myœlenie globalne Większoœć HELLOWIN.C stanowiš elementy dodatkowe, znajdujšce się praktycz- nie w każdym programie Windows. Nikt jednak nie uczy się na pamięć ich składni; najczęœciej programista Windows zaczyna pisanie nowego programu od skopio- wania istniejšcego i wprowadzenia w nim odpowiednich zmian. Możesz do tego celu wykorzystywać programy z ksišżkowego CD-ROM-u. Wspominałem, że HELLOWIN wyœwietla napis na œrodku swojego okna. To nie- zupełnie prawda. W rzeczywistoœci tekst jest wyœwietlany na œrodku obszaru roboczego (ang. client area), który na rysunku 3-2 jest wielkš białš płaszczyznš zamkniętš wewnštrz paska tytułu i ramek ograniczajšcych rozmiar okna. To roz- różnienie jest bardzo ważne; obszar roboczy to obszar okna, w którym program może swobodnie rysować i dostarczać użytkownikowi wizualnych danych. Zadziwiajšce, ile funkcjonalnoœci zawiera ten program zaledwie w 80 liniach kodu. Możesz chwycić myszš pasek tytułu i przesunšć okno w inne miejsce ekranu. Możesz chwycić ramkę okna i zmienić jego wielkoœć. Kiedy okno zmienia swoje rozmiary, program automatycznie przesuwa napis na œrodek swojego obszaru roboczego. Możesz kliknšć przycisk maksymalizacji i okno HELLOWIN wypełni cały ekran. Możesz kliknšć przycisk minimalizacji i usunšć je z ekranu. Możesz wywołać wszystkie opcje z menu systemowego (mała ikona w lewym rogu pa- ska tytuhx). Możesz też zamknšć okno kończšc działanie programu przez wy- branie opcji Zamknij z menu systemowego, przez kliknięcie przycisku zamknię- cia znajdujšcego się w prawym rogu paska tytułu lub przez dwukrotne kliknię- cie ikony menu systemowego. Będziemy analizować ten program szczegółowo w dalszej częœci rozdziału. Jed- nak najpierw spójrzmy bardziej globalnie. HELLOWIN.C zawiera funkcję WinMain, podobnie jak programy przykładowe w dwóch pierwszych rozdzialach, ale zawiera też drugš funkcję o nazwie Wnd- Proc. To właœnie procedura okna. (W rozmowach między programistami Windows jest ona nazywana "win prock"). Zauważ, że w HELLOWIN.C nie ma kodu, któ- ry wywołuje WndProc. Jednak w WinMain znajduje się odniesienie do WndProc, ponieważ funkcja jest zadeklarowana na poczštku programu. Wywoływanie funkcji Windows HELLOWIN wywołuje co najmniej 18 funkcji Windows. Oto ich lista, wraz z krót- kim opisem (w kolejnoœci występowania w programie): ů LoadIcon - wczytuje do pamięci ikonę używanš przez program. ů LoadCursor - wczytuje do pamięci kursor myszy używany przez program. ů GetStockObject - pobiera obiekt graficzny, w tym przypadku pędzel używany do malowania tła okna. T Rozdział 3: Windows i komunikaty 45 ů RegisterClass - rejestruje klasę okna dla okna programu. ů MessageBox - wyœwietla okno komunikatu. ů CreateWindow - tworzy okno na podstawie klasy okna. ů ShowWindow - wyœwietla okno na ekranie. ů UpdateWindow - wymusza aktualizację (odœwieżenie) zawartoœci okna. ů GetMessage - pobiera komunikat z kolejki komunikatów. ů TranslateMessage - tłumaczy niektóre komunikaty wysyłane z klawiatury. ů DispatchMessage - wysyła komunikat do procedury okna. ů PlaySound - odtwarza plik dŸwiękowy. ů BeginPaint - rozpoczyna blok malowania okna. ů GetClientRect - pobiera rozmiary obszaru okna roboczego. ů DrawText - wyœwietla napis. ů EndPaint - kończy blok malowania okna. ů PostQuitMessage - wstawia do kolejki komunikat "quit" o zakończeniu pro- gramu. ů DefWindowProc - wykonuje domyœlne przetwarzanie komunikatów. Te funkcje sš opisane w dokumentacji Platform SDK i zadeklarowane w różnych plikach nagłówkowych, przeważnie w WINUSER.H. Identyfikatory pisane wielkimi literami Zauważyłeœ pewnie, że w HELLOWIN.C znajdujš się identyfikatory pisane wiel- kimi literami. Wszystkie sš zdefiniowane w plikach nagłówkowych Windows. Częœć z nich zawiera dwu- lub trzyliterowy przedrostek, po którym występuje podkreœlenie: CS HREDRAW DT VCENTER SND FILENAME CS-VREDRAW IDC ARROW WM CREATE CW USEDEFAULT IDI aPPLICATION WM DESTROY DT CENTER MB ICONERROR WM PAINT DT SINGLELINE SND ŽSYNC WS OVERLAPPEDWINDOW Sš to po prostu stałe liczbowe. Przedrostek wskazuje ogólnš kategorię, do której należy stała, jak pokazuje to tabela: Przedrostek Stała CS opcja stylu klasy (ang. Class Style) CW opcja tworzenia okna (ang. Create Window) DT opcja rysowania napisów (ang. Draw Text) IDI identyfikator (liczbowy) ikony (ang. ID for an Icon) IDC identyfikator (liczbowy) kursora (ang. ID for a Cursor) 46 Częœć I: Podstawy Przedrostek Stała MB opcja okna komunikatów (ang. Message Box) SND opcja dŸwięku (ang. SouND) WM komunikat okna (ang. Window Message) WS styl okna (ang. Window Style) Programujšc w Windows, zazwyczaj nie musisz pamiętać wartoœci liczbowej tych stałych, gdyż prawie każda stała liczbowa ma identyfikator zdefiniowany w pli- kach nagłówkowych. Nowe typy danych Niektóre identyfikatory stosowane w HELLOWIN.C to nowe typy danych, które zostały także zdefiniowane w plikach nagłówkowych Windows przy użyciu in- strukcji typedef lub #define. Wprowadzono je, aby uproœcić przenoszenie progra- mów z pierwotnego sytemu 16-bitowego do planowanych systemów operacyj- nych opartych na technologii 32-bitowej. Nie działało to tak sprawnie, jak ocze- kiwano, ale sama idea była prawidłowa. Czasem te nazwy nowych typów danych to po prostu wygodne skróty. Na przy- kład typ danych UINT, używany w drugim parametrze WndProc, zastępuje po prostu unsigned int, który w Windows 98 jest wartoœciš 32-bitowš. Typ danych PSTR, występujšcy w trzecim parametrze WinMain, to wskaŸnik do zwykłego (nie szerokiego) napisu, czyli char *. Inne typy sš mniej oczywiste. Na przykład trzeci i czwarty parametr WndProc sš zdefiniowane odpowiednio jako WPARAM i LPARAM. Wyjaœnienie tych nazw wymaga sięgnięcia do historii. Kiedy Windows był systemem 16-bitowym, trzeci parametr WndProc był zdefiniowany jako WORD, co oznaczało 16-bitowš liczbę całkowitš unsigned short, a czwarty parametr był zdefiniowany jako LONG, co ozna- czało 32-bitowš liczbę całkowitš long ze znakiem. Było to powodem umieszczenia przedrostków "W" i "L" przed słowem "PARAM". Jednak w 32-bitowej wersji Windows WPARAM jest zdefiniowany jako UINT, a LPARAM jest zdefiniowany jako LONG (co w dalszym cišgu oznacza typ danych long z C), więc oba parame- try procedury okna sš wartoœciami 32-bitowymi. Ponieważ typ danych WORD jest cišgle jeszcze zdefiniowany w Windows 98 jako 16-bitowa liczba całkowita unsi- gned short, więc umieszczenie "W" przed "PARAM" tworzy mylšcš nazwę. Funkcja WndProc zwraca wartoœć typu LRESULT. Jest ona zdefiniowana jako LONG. Funkcja WinMain jest typu WINAPI ( jak każde wywołanie funkcji Windows zdefi- niowane w pliku nagłówkowym), a funkcja WndProc jest typu CALLBACK. Oba te identyfikatory sš zdefiniowane jako stdcall, co wskazuje na specjalnš sekwencję wywołania funkcji, która występuje między samym Windows i twojš aplikacjš. HELLOWIN używa też czterech struktur danych (które omówię w dalszej częœci rozdziału), zdefiniowanych w pliku nagłówkowym Windows. Te struktury da- nych pokazuje poniższa tabela: Rozdział 3: Windows i komunikaty 47 Struktura Znaczenie MSG struktura komunikatu (ang. Message) WNDCLASS struktura klasy okna (ang. Window class structure) PAIN'TSTRUCT struktura malowania (ang. Paint structure) RECT struktura prostokšta (ang. Rectangle structure) Pierwsze dwie struktury danych sš używane w WinMain do definicji dwóch zmiennych strukturalnych o nazwach msg i wndclass. Pozostałe sš używane w Wnd- Proc do definicji dwóch zmiennych strukturalnych o nazwach ps i rect. Otrrymywanie uchwytu Na koniec mamy jeszcze trzy identyfikatory pisane wielkimi literami dla różnych rodzajów uchwytów: Identyfikator Znaczenie HINSTANCE uchwyt realizacji (kopii) - sam program (ang. Handle to an instance) HWND uchwyt okna (ang. Handle to a window) HDC uchwyt kontekstu urzšdzenia (ang. Handle to a device con- text) Uchwyty sš stosowane w Windows bardzo często. Zanim doczytasz do końca ten rozdział, spotkasz jeszcze HICON (uchwyt ikony - ang. Handle to an ICOIţ, HCURSOR (uchwyt kursora myszy - ang. Handle to a mouse CURSOR) i HBRUSH (uchwyt pędzla - ang. Handle to a graphics BRUSH). Uchwyt to po prostu liczba (zwykle 32-bitowa), która wskazuje obiekt. Uchwyty w Windows sš podobne do uchwytów plików używanych przy konwencjonal- nym programowaniu w C albo MS-DOS-ie. Program niernal zawsze otrzymuje uchwyt przez wywołanie funkcji Windows. Program stosuje uchwyt w innych funkcjach Windows, aby wskazać obiekt. Dla programu rzeczywista wartoœć uchwytu jest nieistotna, ale moduł Windows, który dał uchwyt twojemu progra- mowi, wie, jak go użyć do wskazania konkretnego obiektu. Notacja węgierska Być może zauważyłeœ także, że niektóre zmienne w HELLOWIN.C majš osobli- wie wyglšdajšce nazwy. Jednym z przykładów jest napis szCmdLine, przekazy- wany do WinMain. Wielu programistów Windows stosuje konwencję nazewniczš znanš jako "nota- cja węgierska" dla uczczenia legendarnego programisty Microsoftu, Charlesa Symonyi. Mówišc prosto, nazwa zmiennej zaczyna się małš literš lub literami, 48 Częœć I: Podstawy które oznaczajš typ danych zmiennej. Na przykład przedrostek sz w szCmdLine oznacza "łańcuch znaków zakończony zerem" (ang. string terminated by zero). Przedrostek h w hlnstance i w hPrevlnstance oznacza "uchwyt" (ang. handle); przed- rostek i w iCmdShow oznacza "liczbę całkowitš" (ang. integer). Ostatnie dwa pa- rametry WndProc także używajš notacji węgierskiej, chociaż, jak wyjaœniłem wcze- œniej, wParam powinien być nazwany uiParam (ui od ang. unsigned integer - licz- ba całkowita bez znaku). Ponieważ jednak te dwa parametry sš zdefiniowane z użyciem typów danych WPARAM i LPARAM, zdecydowałem się pozostawić ich tradycyjne nazwy. Nadajšc nazwę zmiennym strukturalnym, możesz użyć pisanej małymi literami pełnej nazwy struktury (albo skrótu tej nazwy) jako przedrostka albo jako całej nazwy zmiennej. Na przykład w funkcji WinMain w HELLOWIN.C zmienna msg oznacza strukturę typu MSG; wndclass jest strukturš typu WNDCLASS. W funk- cji WndProc ps jest strukturš typu PAINTSTRUCT, a rect jest strukturš typu RECT. Notacja węgierska pomaga unikać błędów w kodzie, zanim stanš się trudne do wykrycia. Ponieważ nazwa zmiennej opisuje i przeznaczenie zmiennej, i typ da- nych, trudniej zrobić błędy, polegajšce na nieodpowiednich typach danych. Przedrostki nazw zmiennych, stosowane w tej ksišżce, pokazuje poniższa tabela: Przedrostek Typ danych char lub WCHAR lub TCHAR by BYTE (unsigned char) n short int x, y int (stosowany przy współrzędnych x i y) cx, cy int (stosowany jako długoœć x lub y); c oznacza liczbę (ang. count) b lub f BOOL (int); f oznacza znacznik (ang. flag) w WORD (unsigned short) LONG (long) dw DWORD (unsigned long) fn funkcja łańcuch znaków (ang. string) sz łańcuch znaków zakończony znakiem 0 uchwyt (ang. handle) p wskaŸnik (ang. pointer) Rozdział 3: Windows i komunikaty 49 Rejestracja klasy okna Okno zawsze jest tworzone na podstawie klasy okna. Klasa okna identyfikuje procedurę okna, która przetwarza komunikaty wysyłane do okna. Na podstawie pojedynczej klasy okna może być utworzone więcej niż jedno okno. Na przykład wszystkie okna przycisków - włšcznie z przyciskami, polami opcji i polami wyboru - sš utworzone na podstawie tej samej klasy okna. Klasa okna definiuje procedurę okna i kilka innych charakterystyk okien, które sš tworzone na podstawie tej klasy. Kiedy tworzysz okno, definiujesz dodatkowš charaktery- stykę okna, który jest dla niego unikatowa. Zanim utworzysz okno aplikacji, musisz zarejestrować klasę okna przez wywo- łanie RegisterClass. Ta funkcja wymaga pojedynczego parametru, który jest wskaŸ- nikiem do struktury typu WNDCLASS. Ta struktura zawiera dwa pola, które sš wskaŸnikami do napisów, więc struktura jest zdefiniowana w pliku nagłówko- wym WINUSER.H na dwa różne sposoby. Oto wersja ASCII - WNDCLASSA: typedef struct tagWNDCLASSA ( UINT style ; WNDPROC lpfnWndProc ; int cbClsExtra ; int cbWndExtra ; HINSTANCE hInstance : HICON hIcon : HCURSOR hCursor ; HBRUSH hbrBackground LPCSTR lpszMenuName LPCSTR lpszClassName 1 WNDCLASSA, * PWNDCLASSA, NEAR * NPWNDCLASSA, FAR * LPWNDCLASSA ; Zwróć uwagę na zastosowanie notacji węgierskiej: przedrostek Ipfn oznacza "dale- ki wskaŸnik do funkcji" (ang. long pointer to a function). (Przypominam, że w Win32 API nie ma żadnej różnicy między dalekimi i bliskimi wskaŸnikami. To pozosta- łoœć z 16-bitowego Windows). Przedrostek cb oznacza "licznik bajtów" (ang. count of bytes) i jest często stosowany dla zmiennej, która oznacza rozmiar w bajtach. Przed- rostek h oznacza "uchwyt" (ang. handle), a hbr to "uchwyt pędzla" (ang. handle to a brush). Przedrostek Ipsz oznacza "daleki wskaŸnik do łańcucha znaków zakończo- nego zeremř (ang. long pointer to a string terminated with zero). Wersja struktury dla unikodu jest zdefiniowana następujšco: typedef struct tagWNDCLASSW ( UINT style ; WNDPROC lpfnWndProc ; int cbClsExtra ; int cbWndExtra ; HINSTANCE hInstance ; HICON hIcon ; HCURSOR hCursor ; HBRUSH hbrBackground ; LPCWSTR lpszMenuName ; LPCWSTR lpszClassName ; ) WNDCLASSW, * PWNDCLASSW, NEAR * NPWNDCLASSW, FAR * LPWNDCLASSW ; 50 Częœć I: Podstawy Jedyna różnica to dwa ostatnie pola, zdefiniowane jako wskaŸniki do stałych łań- cuchów znaków szerokich, a nie wskaŸniki do stałych łańcuchów znaków ASCII. Po zdefiniowaniu przez WINUSEIZ.H struktur WNDCLASSA i WNDCLASSW (i wskaŸników do struktur), plik nagłówkowy definiuje WNDCLASS i wskaŸni- ki do WNDCLASS (niektóre z nich majš zapewnić wstecznš zgodnoœć) na pod- stawie definicji identyfikatora UMCODE: llifdef UNICODE typedef WNDCLASSW WNDCLASS ; typedef PWNDCLASSW PWNDCLASS ; typedef NPWNDCLASSW NPWNDCLASS typedef LPWNDCLASSW LPWNDCLASS llelse typedef WNDCLASSA WNDCLASS ; typedef PWNDCLASSA PWNDCLASS ; typedef NPWNDCLASSA NPWNDCLASS typedef LPWNDCLASSA LPWNDCLASS llendi f Przedstawiajšc kolejne struktury w tej ksišżce, pokazuję funkcjonalnie równoważ- nš definicję struktury, którš dla WNDCLASS jest: typedef struct ( UINT style ; WNDPROC lpfnWndProc ; int cbClsExtra ; int cbWndExtra ; HINSTANCE hInstance ; HICON hIcon ; HCURSOR hCursor ; HBRUSH hbrBackground LPCTSTR lpszMenuName LPCTSTR lpszClassName ) WNDCLASS, * PWNDCLASS ; Nie będę męczył cię różnymi definicjami wskaŸników. Nie ma żadnego powodu, by zaœmiecać swój kod typami zmiennych zaczynajšcymi się od LP i NP. W WinMain zdefiniuj strukturę typu WNDCLASS w następujšcy sposób: WNDCLASS wndclass ; Następnie zainicjuj 10 pól struktury i wywołaj RegisterClass. Dwa najważniejsze pola w strukturze WNDCLASS to pole drugie i ostatnie. Dru- gie pole (IpfnWndProc) jest adresem procedury okna, używanej przez wszystkie okna utworzone na podstawie tej klasy. W HELLOWIN.C procedura okna to WndProc. Ostatnie pole zawiera nazwę tekstowš klasy okna. Może to być cokol- wiek. W programach, które tworzš tylko jedno okno, w polu nazwy klasy okna zwykle wpisywana jest nazwa programu. Poniżej omówimy inne pola, które opisujš pewne właœciwoœci klasy okna. Przyj- rzyjmy się kolejno każdemu polu struktury WNDCLASS. Instrukcj a: wndclass.style = CS HREDRAW ţ CSţVREDRAW ; Rozdział 3: Windows i komunikaty 51 łšczy dwa 32-bitowe identyfikatory "stylów klas" za pomocš bitowego operato- ra OR z C. Plik nagłówkowy WINUSER.H definiuje cały zestaw identyfikatorów z przedrostkiem CS: iţdefine CS_VREDRAW 0x0001 iţdefine CS_HREDRAW 0x0002 #define CS_KEYCVTWINDOW 0x0004 iţdefine CS_DBLCLKS 0x0008 ţţdefine CS_OWNDC 0x0020 ţţdefine CS_CLASSDC 0x0040 ţţdefine CS_PARENTDC 0x0080 #define CSţNOKEYCVT 0x0100 ţtdefine CS_NOCLOSE 0x0200 ţţdefine CSţSAVEBITS 0x0800 ţţdefine CS_BYTEALIGNCLIENT 0x1000 iţdefine CS_BYTEALIGNWINDOW 0x2000 #define CS_GLOBALCLASS 0x4000 ţţdefine CSţIME 0x00010000 Identyfikatory definiowane w ten sposób sš często zwane znacznikami bitowy- mi, ponieważ każdy identyfikator ustawia pojedynczy bit w wartoœci złożonej. Najczęœciej używane sš tylko niektóre z tych stylów klasy. Dwa identyfikatory, użyte w HELLOWIN, oznaczajš, że wszystkie okna stworzone na podstawie tej klasy muszš być odœwieżone, jeœli tylko zmieni się wielkoœć okna w poziomie (CS HREDRAW) albo pionie (CS VREDRAW). Jeœli zmienisz rozmiar okna pro- gramu HELLOWIN, zobaczysz, że łańcuch znaków tekstu jest odœwieżany tak, aby znalazł się na œrodku nowego okna. Te dwa identyfikatory odpowiadajš za ten proces. Niedługo zobaczymy, w jaki sposób procedura okna jest informowa- na o zmianie wielkoœci okna. Drugie pole struktury WNDCLASS jest inicjowane przez instrukcję: wndclass.lpfnWndProc = WndProc : Ustawia to procedurę okna dla tej klasy okna na WndProc, która jest drugš funk- cjš w HELLOWIN.C. Ta procedura okna będzie przetwarzała wszystkie komuni- katy przeznaczone dla wszystkich okien utworzonych na podstawie tej klasy okna. W C, gdy używasz nazwy funkcji w takiej instrukcji, tak naprawdę posługujesz się wskaŸnikiem do tej funkcji. Następne dwa pola służš do rezerwacji dodatkowego miejsca w strukturze klasy i strukturze okna, którym Windows zarzšdza wewnętrznie. wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; Program może użyć tej dodatkowej przestrzeni do własnych celów. Program HELLOWIN nie wykorzystuje tej możliwoœci, więc ustawia wartoœć 0. W innym przypadku, jak wskazuje notacja węgierska, w polu powinna być ustawiana "licz- ba bajtów". (Użyję pola cbWndExtra w programie CHECKER3, w rozdziale 7). Następne pole jest po prostu uchwytem kopii programu (ten uchwyt jest jednym z parametrów WinMain): wndclass.hInstance = hInstance : Instrukcja: wndclass.hIcon = LoadIcon (NULL, IDI aPPLICATION) : 52 Częœć I: Podstawy okreœla ikonę dla wszystkich okien stworzonych na podstawie klasy okna. Ta ikona jest małš bitmapš, która symbolizuje program. Gdy program jest uruchomiony, ikona ta jest widoczna na pasku zadań Windows i z lewej strony paska tytułu aplikacji. PóŸniej dowiesz się, jak tworzyć własne ikony dla swoich programów Windows. Na razie będziemy po prostu używali predefiniowanej ikony. Aby otrzymać uchwyt predefiniowanej ikony, wywołaj Loadlcon z pierwszym argumentem ustawionym na NULL. Gdy ładujesz swoje własne ikony, przecho- wywane na dysku w pliku EXE programu, ten argument powinien mieć wartoœć hlnstance, czyli uchwytu kopii programu. Drugi argument identyfikuje ikonę. Dla predefiniowanych ikon ten argument jest identyfikatorem zaczynajšcym się od przedrostka IDI ("identyfikator dla ikony") zdefiniowanym w WINUSER.H. Ikona IDI APPLICATION jest po prostu małym obrazkiem okna. Funkcja Loadlcon zwraca uchwyt tej ikony. Nie przejmuj się wartoœciš tego uchwytu. Po prostu użyj go do ustawienia wartoœci pola hlcon. To pole jest zdefiniowane w strukturze WNDCLASS jako pole typu HICON, co oznacza "uchwyt ikony" (ang. handle to an icon). Instrukcja: wndclass.hCursor = LoadCursor (NULL, IDC ARROW) ; jest podobna do poprzedniej. Funkcja Loadlcon ładuje predefiniowany kursor myszy, znany jako IDC ARROW, i zwraca uchwyt tego kursora. Ten uchwyt jest przyporzšdkowany polu hCursor struktury WNDCLASS. Gdy kursor myszy znaj- duje się nad obszarem roboczym okna utworzonego na podstawie tej klasy, kur- sor staje się małš strzałkš. Następne pole okreœla kolor tła obszaru roboczego okna, utworzonego na pod- stawie tej klasy. Przedrostek hbr pola hbrBackground oznacza "uchwyt pędzla" (ang. handle to a brush). Pędzel jest okreœleniem, które w grafice komputerowej ozna- cza kolorowy wzór z pikseli, używany do wypełniania obszaru. Windows ma wiele standardowych ("szablonowych" - ang. stock) pędzli. Funkcja GetStockO- bject, pokazana tutaj, zwraca uchwyt białego pędzla: wndclass.hbrBackground = GetStockObject (WHITEţBRUSH) ; Oznacza to, że pole obszaru roboczego będzie białe, co jest najczęœciej stosowa- ne. Następne pole okreœla menu klasy okna. HELLOWIN nie ma menu aplikacji, więc ustawiamy to pole na NULL: wndclass.lpszMenuName = NULL ; Wreszcie musimy nadać klasie nazwę. Dla małego programu może to być po pro- stu nazwa programu, która tu jest łańcuchem znaków "HelloWin", przechowy- wanym w zmierinej szAppName. wndclass.lpszClassName = szAppName ; Ten łańcuch znaków jest złożony ze znaków ASCII albo znaków unikodu, w za- leżnoœci od tego, czy zdefiniowany jest identyfikator UNICODE. Gdy wszystkie 10 pól struktury zostanie zainicjowanych, HELLOWIN rejestruje klasę okna, wywohzjšc RegisterClass. Jedynym argumentem tej funkcji jest wskaŸnik do struktury WNDCLASS. Właœciwie jest to funkcja RegisterClassA, która pobie- Rozdział 3: Windows i komunikatv 53 ra wskaŸnik do struktury WNDCLASSA, i funkcja RegisterClassW, która pobiera wskaŸnik do struktury WNDCLASSW. To, której funkcji użyje program do reje- strowania klasy okna, zależy od tego, czy komunikaty wysyłane do okna będš zawierały tekst ASCII, czy tekst w unikodzie. Tutaj pojawia się problem: jeœli skompilowałeœ program ze zdefiniowanym iden- tyfikatorem UNICODE, twój program wywoła RegisterClassW. Wszystko będzie dobrze, jeœli uruchamiasz program pod Windows NT. Ale jeœli uruchamiasz pro- gram pod Windows 98, okaże się, że funkcja RegisterClassW tak nie jest napraw- dę zrealizowana. Istnieje punkt wejœcia dla tej funkcji, ale zwraca ona zero przy wywołaniu, co oznacza błšd. Dzięki temu program w unikodzie, uruchomiony pod Windows 98, może poinformować użytkownika o problemie i zakończyć dzia- łanie. Większoœć programów z tej ksišżki obsługuje wywołanie funkcji RegisterC- lass następujšco: if (!RegisterClass (&wndclass)) ( MessageBox (NULL, TEXT ("Ten program wymaga Windows NT!"), szAppName, MB ICONERROR) ; return 0 : Funkcja MessageBoxW działa prawidłowo, ponieważ jest to jedna z niewielu funkcji unikodowych zrealizowanych w Windows 98. Oczywiœcie ten fragment kodu zakłada, że RegisterClass nie zawiedzie z innych po- wodów, jak na przykład wartoœć NULL pola lpfnWndProc struktury WNDCLASS. W takich przypadkach funkcja GetLastError pomoże okreœlić przyczynę błędu. Jest to funkcja ogólnego zastosowania w Windows, która otrzymuje rozszerzonš infor- mację o błędzie, kiedy wywołanie funkcji zawiedzie. Dokumentacja różnych funkcji wskazuje, czy możesz stosować GetLastError, aby otrzymać tę informację. W przy- padku niepowodzenia RegisterClassW w Windows 98, GetLastError zwraca 120. Mo- żesz sprawdzić w WINERROR.H, że wartoœć 120 odpowiada identyfikatorowi ER- ROR CALL NOT IMPLEMENTED. Możesz też poszukać znaczenia błędu w /Plat- form SDK/Windows Base Sermces/Debugging and Error Handling/Error Codes/System Errors - Numerical Order. Niektórzy programiœci Windows lubiš sprawdzać wartoœć zwracanš przez każdš funkcję, aby upewnić się, czy nie wystšpił błšd. Na pewno ma to jakiœ sens: jestem pewny, że pamiętasz o zasadzie, by zawsze sprawdzać wystšpienie błędu, gdy przydzielasz (alokujesz) pamięć. Ale wiele funkcji Windows alokuje jakšœ pamięć. Na przykład RegisterClass potrzebuje pamięci, aby przechować informację o klasie okna. Powinieneœ więc bezwzględnie sprawdzać tę funkcję. Ale z drugiej strony, jeœli RegisterClass zawiedzie, ponieważ nie może zaalokować potrzebnej pamięci, Windows prawdopodobnie już się zawiesił. W przykładowych programach w tej ksišżce wykonuję tylko minimalne sprawdza- nie błędów. Robię tak nie dlatego, że nie doceniam idei sprawdzania błędów; po prostu nie chcę zaciemniać tego, co programy powinny pokazywać. Na koniec pewna uwaga dotyczšca historii: w niektórych przykładowych pro- 54 Częœć I: Podstawy gramach Windows możesz spotkać następujšcy kod w WinMain: if (!hPrevInstance) ( wndclass.cbStyle = CS HREDRAW ţ CS VREDRAW ; [inne inicjacje wndclass] RegisterClass (&wndclass) ; Dajš tu o sobie znać stare upodobania. W 16-bitowej wersji Windows, gdy urucha- miałeœ nowš kopię programu przykładowego, który już działał, parametr hPrevln- stance w WinMain powinien być uchwytem realizacji poprzedniej kopii programu. Aby zaoszczędzić pamięć, dwie lub więcej kopii mogły używać tej samej klasy okna. Dlatego klasa okna była rejestrowana tylko wtedy, jeœli hPrevlnstance miało war- toœć NULL, wskazujšc, że żadne inne kopie programu nie działajš. W 32-bitowej wersji Windows hPrevlnstance zawsze ma wartoœć NULL. Ten kod bę- dzie cišgle działał właœciwie, ale sprawdzanie hPrevlnstance nie jest już konieczne. Tworzenie okna Klasa okna definiuje ogólnš charakterystykę okna, pozwalajšc na wykorzystanie jej do tworzenia wielu różnych okien. Gdy pójdziesz dalej i utworzysz okno, wywołujšc CreateWindow, możesz podać bardziej szczegółowš informację o oknie. Poczštkujšcy programiœci Windows czasami gubiš się w rozróżnieniach między klasš okna, oknem i tym, że wszystkich charakterystyk okna nie można okreœlić jednoczeœnie. W rzeczywistoœci dzielenie informacji w ten sposób jest całkiem wy- godne. Na przykład wszystkie okna przycisków sš tworzone na podstawie tej sa- mej klasy okna. Procedura okna powišzana z tš klasš okna znajduje się wewnštrz samego Windows. Jest odpowiedzialna za przetwarzanie informacji wejœciowej z klawiatury i myszy do przycisku oraz definiuje wyglšd przycisku na ekranie. Wszystkie przyciski działajš pod tym względem tak samo. Ale nie wszystkie sš takie same. Niemal na pewno majš różne rozmiary, inne położenie na ekranie i różne napisy. Te ostatnie cechy sš częœciš definicji okna, a nie definicji klasy okna. Podczas gdy informacja przekazywana do funkcji RegisterClass jest okreœlona w strukturze danych, informacja przekazywana do funkcji CreateWindow jest okre- œlana jako oddzielne argumenty funkcji. Oto pełne wywołanie CreateWindow z HELLOWIN.C wraz z komentarzami identyfikujšcymi poszczególne pola: hwnd = CreateWindow (szAppName, // nazwa klasy okna TEXT ("The Nello Program")"// naglówek okna WS_OVERLAPPEDWINDOW, // styl okna CW_USEDEFAULT, // poczdtkowa pozycja x CW_USEDEFAULT, // poczštkowa pozycja y CW_USEDEFAULT, // poczdtkowy rozmiar x CW_USEDEFAULT, // poczdtkowy rozmiar y NULL, // uchwyt okna nadrzędnego NULL, // uchwyt menu okna hInstance, // uchwyt kopii programu NULL) ; // parametry tworzenia W tym miejscu nie będę się martwił, czy w rzeczywistoœci jest to funkcja Create- Rozdział 3: Windows i komunikaty 55 WindowA czy też funkcja CreateWindowW; pierwsza traktowałaby dwa pierwsze parametry jako ASCII, druga - jako urukod. Argument oznaczony jako "nazwa klasy okna" to szAppName, zawierajšcy łań- cuch znaków "HelloWin"- nazwę klasy okna, którš program właœnie zarejestro- wał. W ten sposób tworzone okno jest wišzane z klasš okna. Okno tworzone przez ten program to zwykłe okno, które może nakładać się na inne. Będzie miało: pasek tytuhx, przycisk menu systemowego z lewej strony pa- ska tytuhz, grubš ramkę ustalajšcš rozmiary okna i przyciski minimalizacji, mak- symalizacji i zamknięcia z prawej strony paska tytułu. Jest to standardowy styl okien i ma nazwę WS OVERLAPPEDWINDOW, która występuje jako parametr "styl okna" w CreateWindow. Jeœli zajrzysz do WINUSER.H, zobaczysz, że ten styl jest kombinacjš kilku znaczników bitowych: #define WS OVERLAPPEDWINDOW (WS_OVERLAPPED ţ \ WS_CAPTION ţ \ WS_SYSMENU ţ \ WS_THICKFRAME ţ \ WS_MINIMIZEBOX ţ \ WS MAXIMIZEBOX) "Nagłówek" to tekst, który pojawi się na pasku tytułu okna. Argumenty oznaczone jako "poczštkowa pozycja x" i "poczštkowa pozycja y' okreœlajš poczštkowe położenie lewego górnego rogu okna względem lewego górnego rogu ekranu. Podstawiajšc za nie identyfikator CW USEDEFAULT zga- dzamy się, aby Windows użył domyœinego położenia dla zasłoniętego okna. (CW USEDEFAULT jest zdefiniowany jako 0x80000000). Domyœlnie Windows umieszcza tworzone okna ze skokowo zmienianym przesunięciem w lewo i w dół w stosunku do lewego górnego rogu ekranu. Podobnie argumenty "poczštkowy rozmiar x" i "poczštkowy rozmiar y' okreœlajš poczštkowš szerokoœć i wysokoœć okna. Identyfikator CW LTSEDEFAULT ponownie wskazuje, że chcemy, by Win- dows użył domyœlnego rozmiaru okna. Argument oznaczony "uchwyt okna nadrzędnego" jest ustawiany na NULL, kiedy tworzysz okno "najwyższego poziomu", takie jak okno aplikacji. Zwykle jeœli mię- dzy dwoma oknami istnieje zależnoœć okno nadrzędne - okno potomne, okno po- tomne zawsze pojawia się na powierzchni okna nadrzędnego. Okno aplikacji uka- zuje się na powierzchni okna pulpitu, ale nie potrzebujesz znajdować uchwytu okna pulpitu przy wywoływaniu CreateWindow. "Uchwyt menu okna" też jest ustawiony na NLTLL, ponieważ okno nie ma żadnego menu. "Uchwyt kopii programu" jest ustawiony na uehwyt realizacji (kopii), prze- kazywany do programu jako parametr WinMain. Końcowy wskaŸnik "parametry tworzenia" jest ustawiony na NULL. Możesz wykorzystać ten parametr do wskaza- nia pewnych danych, do których chciaţyœ się póŸniej odnieœć w swoim programie. Wywołanie CreateWindow zwraca uchwyt do tworzonego okna. Ten uchwyt zapi- sujemy w zmiennej hwnd, która jest zdefiniowana jako zmienna typu HWND ("uchwyt okna"). Każde olaţo w Windows ma uchwyt. Twój program też używa uchwytu przy odnoszeniu się do okna. Wiele funkcji Windows wymaga jako ar- gumentu hwnd, więc Windows wie, do.ktbrego okna stosuje się dana funkcja. 56 Częœć I: Podstawy Jeœli program tworzy dużo okien, każde ma inny uchwyt. Uchwyt okna jest jed- nym z najważniejszych uchwytów, którymi posługuje się program w Windows. Wyœwietlanie okna Po powrocie z wywołania CreateWindow, okno jest tworzone wewnętrznie przez Windows. Zasadniczo oznacza to, że Windows przydzielił blok pamięci do prze- chowywania wszystkich informacji o oknie, które okreœliłeœ w wywołaniu Create- Window, oraz kilku innych informacji, które póŸniej odnajdzie na podstawie uchwytu okna. Jednak okno nie pojawia się jeszcze na ekranie. Do tego potrzebne sš jeszcze dwa wywołania. Pierwsze to: ShowWindow (hwnd, iCmdShow) ; Pierwszy argument jest uchwytem okna właœnie utworzonego przez CreateWin- dow. Drugi argument to wartoœć iCmdShow, przekazana jako parametr do Win- Main. Okreœla ona, w jakiej formie okno pojawi się pierwszy raz na ekranie: jako normalne, pomniejszone czy zmaksymalizowane. Użytkownik prawdopodobnie okreœlił to podczas dodawania programu do menu Start. Wartoœciš, jakš otrzy- masz od WinMain i przekażesz do ShowWindow, może być: SW SHOWNORMAL, jeœli okno jest wyœwietlane normalnie, SW SHOWMAXIMIZED, jeœli okno jest zmaksymalizowane i SW SHOWMINNOACTIVE, jeœli okno jest wyœwietlone tylko na pasku zadań. Funkcja ShowWindow umieszcza okno na ekranie. Jeœli drugim argumentem Show- Window jest SW SHOWNORMAL, to obszar roboczy okna jest czyszczony pędz- lem tła, okreœlonym w klasie okna. Wywołanie funkcji: UpdateWindow (hwnd) ; powoduje wypełnienie przez program obszaru roboczego okna. Jest to realizo- wane przez wysłanie do procedury okna (czyli funkcji WndProc w HELLOWIN.C) komunikatu WM PAINT. Wkrótce zobaczymy, co WndProc robi z tym komuni- katem. Pętla komunikatów Po wywołaniu UpdateWindow okno jest już widoczne na ekranie. Program musi teraz być gotowy do odbierania informacji, które użytkownik wprowadza za po- mocš klawiatury i myszy. Dla każdego programu aktualnie działajšcego w sys- temie Windows obsługuje kolejkę komunikatów. Kiedy wystšpi zdarzenie wej- œciowe, Windows tłumaczy je na komunikat, który umieszcza w kolejce komuni- katów programu. Program odzyskuje te komunikaty z kolejki komunikatów wykonujšc blok kodu, nazywany pętlš komunikatów: while (GetMessage (&msg, NULL, 0. 0)) ( TranslateMessage (&msg) ; DispatchMessage (&msg) ; Rozdział 3: Windows i komunikaty 57 Zmienna msg jest strukturš typu MSG, zdefiniowanš następujšco w pliku nagłów- kowym WINUSER.H: typedef struct tagMSG ( HWND hwnd ; UINT message ; WPARAM wParam ; LPARAM lParam ; DWORD time ; POINT pt ; ) MSG, * PMSG ; Typ danych POINT to jeszcze inna struktura, zdefiniowana w pliku nagłówko- wym WINDEF.H: typedef struct tagPOINT ( LONG x ; LONG y ; 1 POINT, * PPOINT; Wywołanie GetMessage, które rozpoczyna pętlę komunikatów, powoduje pobra- nie komunikatu z kolejki komunikatów: GetMessage (&msg, NULL, 0, 0) To wywołanie przekazuje do Windows wskaŸnik do struktury MSG o nazwie msg. Drugi, trzeci i czwarty argument sš ustawiane na NULL albo 0, wskazujšc, że program żšda wszystkich komunikatów wysyłanych do wszystkich okien przez niego utworzonych. Windows wypełnia pola struktury komunikatu następnym komunikatem z kolejki. Polami tej struktury sš: ů hwnd - uchwyt okna, do którego skierowany jest komunikat. W programie HELLOWIN jest on równy wartoœci hwnd zwracanej przez CreateWindow, po- nieważ program ma tylko jedno okno. ů message - identyfikator komunikatu. Jest to liczba, która identyfikuje komuni- kat. Dla każdego komunikatu istnieje odpowiedni identyfikator zdefiniowa- ny w plikach nagłówkowych Windows (w większoœci w WINUSER.H), który zaczyna się przedrostkiem WM ("window message" - komunikat okna). Na przykład jeœli umieœcisz wskaŸnik myszy w obszarze roboczym HELLOWIN i naciœniesz lewy przycisk myszy, Windows umieœci w kolejce komunikat z po- lem message równym WM LBUTTONDOWN, co odpowiada wartoœci 0x0201. ů wParam - 32-bitowy "parametr komunikatu", którego znaczenie i wartoœć za- leżš od konkretnego komunikatu. ů lParam - inny 32-bitowy parametr komunikatu, zależny od rodzaju komuni- katu. ů time - czas okreœlajšcy moment umieszczenia komunikatu w kolejce. ů pt - współrzędne myszy w chwili umieszczania komunikatu w kolejce. Jeżeli pole message komunikatu uzyskanego z kolejki komunikatów jest czymœ innym niż WM-QUIT (które jest równe 0x0012), to GetMessage zwraca wartoœć niezerowš. Komunikat WM-QUIT powoduje, że GetMessage zwraca 0. 58 Częœć I: Podstawy Instrukcja: TranslateMessage (&msg) ; przekazuje strukturę msg z powrotem do Windows w celu pewnego przekształ- cenia komunikatów klawiatury. (Szerzej omówię to w rozdziale 6). Instrukcja DispatchMessage (&msg) ; zwraca strukturę msg do Windows. Windows wysyła wtedy komunikat do wła- œciwej procedury okna w celu przetwarzania. Oznacza to, że Windows wywołu- je procedurę okna. W HELLOWIN tš procedurš okna jest WndProc. Po przetwo- rzeniu komunikatu w WndProc sterowanie wraca do Windows, który cišgle jesz- cze obsługuje wywołanie DispatchMessage. Kiedy Windows powróci do HELLO- WIN po wywołaniu DispatchMessage, pętla komunikatów wykona następne wy- wołanie funkcji GetMessage. Procedura okna Wszystko, co do tej pory opisywałem, to w rzeczywistoœci koszty ogólne. Klasa okna została zarejestrowana, okno wyœwietlone na ekranie, a program kręci się w pętli komunikatów, by pobierać kolejne komunikaty z kolejki. Prawdziwe działanie odbywa się w procedurze okna. To ona decyduje, co okno wyœwietla w obszarze roboczym i jak odpowiada na poczynania użytkownika. W HELLOWIN procedurš okna jest funkcja o nazwie WndProc. Procedura okna może mieć dowolnš nazwę (oczywiœcie o ile nie koliduje ona z innymi nazwa- mi). Program Windows może zawierać więcej niż jednš procedurę okna. Proce- dura okna jest zawsze powišzana z konkretnš klasš okna, którš zarejestrowałeœ, wywohxjšc RegisterClass. Funkcja CreateWindow tworzy okno na podstawie kon- kretnej klasy okna. Na podstawie tej samej klasy okna można utworzyć więcej niż jedno okno. Procedura okna jest zawsze zdefiniowana w ten sposób: LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam. LPARAM lParam) Cztery parametry procedury okna sš identyczne z pierwszymi czterema polami struktury MSG. Pierwszy parametr to hwnd, uchwyt okna otrzymujšcego komu- nikat. Jest to ten sam uchwyt, który zwróciła funkcja CreateWindow. Dla progra- mu takiego jak HELLOWIN, który tworzy tylko jedno okno, jest to jedyny uchwyt okna, o którym wie program. Jeœli program tworzy wiele okien na podstawie tej samej klasy okna (i stšd tej samej procedury okna), hwnd identyfikuje konkretne okno otrzymujšce komunikat. Drugi parametr jest taki sam jak pole message w strukturze MSG. Jest to liczba iden- tyfikujšca komunikat. Ostatnie dwa parametry to 32-bitowe parametry komunika- tu, które dostarczajš dodatkowych informacji o komunikacie. Ich znaczenie zależy od typu komunika#u. Czasem parametr komunikatu stanowiš dwie umieszczone razem wartoœci 16-bitowe, a czasem wskaŸnik do napisu lub struktury danych. Zasadniczo programy nie wywołuj,š bezpoœrednio procedury okna. Procedura okna jest niemal zawsze wywoływana przez Windows. Program może poœred- nio wywołać swojš procedurę okna przez wywołanie funkcji o nazwie SendMes- sage, którš omówimy w dalszych rozdziałach. Rozdział 3: Windows i komunikaty 59 Przetwarzanie komunikatów Każdy komunikat docierajšcy do procedury okna jest identyfikowany przez licz- bę, która stanowi dla niej parametr komunikatu. Plik nagłówkowy Windows WIN- USER.H definiuje identyfikatory zaczynajšce się od przedrostka WM, dla każde- go rodzaju komunikatu. Zasadniczo programiœci Windows używajš konstrukcji switch i case do okreœle- nia, jaki komunikat otrzymała procedura okna i jak go odpowiednio przetwo- rzyć. Gdy procedura okna przetworzy komunikat, powinna zwrócić 0. Wszyst- kie komunikaty, które procedura okno pozostawi nie przetworzone, muszš być przekazane do funkcji Windows o nazwie Def 4VindowProc. Wartoœć zwracana przez Def4VindowProc musi być też zwrócona przez procedurę okna. W HELLOWIN procedura WndProc wybiera do przetwarzania tylko trzy komu- nikaty: WM CREATE, WM PAINT i 4VM-DESTROY. Procedura okna jest zbu- dowana następujšco: switch (iMsg) ( case WM_CREATE : [przetwarzanie komunikatu WM CREATE] return 0 ; case WM_PAINT : [przetwarzanie komunikatu WM_PAINT] return 0 : case WM_DESTROY : [przetwarzanie komunikatu WM_DESTROY] return 0 ; ) return DefWindowProc (hwnd, iMsg, wParam, lParam) ; Ważne jest wywołanie DefWindowProc dla domyœlnego przetwarzania wszystkich komunikatów, którymi nie zajmuje się twoja procedura okna. W przeciwnym przypadku nie wystšpiš niektóre typowe działania, np. zakończenie programu. Odtwarzanie pliku dŸwiękowego Pierwszym komunikatem, który odbiera procedura okna - i pierwszym, który wybiera do przetwarzania-jest WM CREATE. WndProc otrzymuje ten komuni- kat, gdy Windows przetwarza funkcję CreateWindow w WinMain. To znaczy, że kiedy HELLOWIN wywołuje CreateWindow, Windows robi to, co musi robić, a na- stępnie wywołuje WndProc z pierwszym argumentem ustawionym na uchwyt okna, a drugim argumentem ustawionym na WMţCREATE (o wartoœci 1). Wnd- Proc przetwarza komunikat WM CREATE i zwraca sterowanie z powrotem do Windows. Windows wtedy może powrócić z wywołania CreateWindow do HEL- LOWIN, by kontynuować przetwarzanie z WinMain. Często procedura okna wykonuje jednorazowš inicjację okna podczas przetwa- rzania WM-CREATE. HELLOWIN, przetwarzajšc ten komunikat, odtwarza plik dŸwiękowy o nazwie HELLOWIN.WAV. Robi to za pomocš prostej funkcji Play- Sound, opisanej w /Platform SDK/Graphics and Multimedia Services/Multimedia Au- dio/Waveform Audio i udokumentowanej w /Platform SDK/Graphics and Multime- dia Services/Multimedia Reference/Multimedia Functions. w 60 Częœć i: Podstawy Pierwszym argumentem PlaySound jest nazwa pliku dŸwiękowego. (Może to być także nazwa aliasu dŸwięku zdefiniowana w częœci DŸwięk w Panelu sterowa- nia lub w zasobach programu). Drugi argument jest używany tylko wtedy, jeœli pliki dŸwiękowe stanowiš zasób programu. Trzeci argument okreœla szereg opcji. W tym przypadku wskazałem, że pierwszy argument jest nazwš pliku i że dŸwięk ma być grany asynchronicznie. Oznacza to, że powrót z funkcji PIaySound nastš- pi zaraz po rozpoczęciu odtwarzania pliku, bez czekania na jego zakończenie. W ten sposób program może kontynuować swojš inicjację. WndProc, po zakończeniu przetwarzania komunikatu WMţCREATE, zwraca 0. Komunikat WM PAINT Drugim komunikatem przetwarzanym przez WndProc jest WM PAINT. Ten ko- munikat jest bardzo ważny w programowaniu Windows. Informuje program, że częœć lub cały obszar roboczy okna stał się "nieważny" i musi być "zaktualizo- wany" (odœwieżony), co oznacza powtórne narysowanie lub "namalowanie". W jaki sposób obszar roboczy staje się nieważny? Kiedy okno jest tworzone po raz pierwszy, cały obszar roboczy jest nieważny, ponieważ program nie naryso- wał niczego w oknie. Pierwszy komunikat WMţPAINT (które zwykle występu- je, gdy program wywołuje UpdateWindow w WinMain) poleca procedurze okna narysować coœ w obszarze roboczym. Kiedy zmienisz wielkoœć okna HELLOWIN, obszar roboczy staje się nieważny. Przypomnij sobie, że pole style w strukturze wndclass HELLOWIN ma ustawione znaczniki CS_HREDRAW i CS_VREDRAW. Oznacza to dla Windows unieważ- nienie całego okna, kiedy jego rozmiar ulega zmianie. Procedura okna otrzymuje wtedy komunikat WM PAINT. Jeœli zminimalizujesz HELLOWIN, a następnie przywrócisz oknu poprzednie rozmiary, Windows nie zachowa zawartoœci obszaru roboczego. W œrodowisku graficznym wymagałoby to zachowania dużej iloœci danych. Zamiast tego Win- dows unieważnia okno. Procedura okna otrzymuje komunikat WM PAINT i przywraca zawartoœć okna. Kiedy przesuniesz okna na ekranie tak, że będš na siebie zachodziły, Windows nie zachowa obszaru zasłoniętego przez inne okno. Kiedy ten obszar zostanie póŸniej odsłonięty, jest zaznaczany jako nieważny. Procedura okna otrzymuje komunikat WM PAINT w celu odtworzenia zawartoœci okna. Przetwarzanie 4VMţPAINT niemal zawsze zaczyna się wywołaniem BeginPaint: hdc = BeginPaint (hwnd, &ps) ; a kończy wywołaniem EndPaint: EndPaint (hwnd, &ps) ; W obu przypadkach pierwszy argument jest uchwytem okna programu, a drugi wskaŸ,nikiem do struktury typu PAINTSTRUCT. Struktura ta zawiera pewne in- formacje, które procedura okna może wykorzystać przy malowaniu obszaru ro- boczego. Omówię pola tej struktury w następnym rozdziale, a teraz będziemy po prostu wykorzystywać je w funkcjach BeginPaint i EndPaint. Rozdział 3: Windows i komunikaty 61 Podczas wywołania BeginPaint Windows czyœci tło obszaru roboczego, jeœli nie zostało to jeszcze zrobione. W tym celu wykorzystuje pędzel okreœlony w polu hbrBackground struktury WNDCLASS, używanej do rejestracji klasy okna. W przy- padku HELLOWIN, jest to standardowy biały pędzel, co oznacza, że Windows wyciera tło okna, malujšc je na biało. Wywołanie BeginPaint zatwierdza cały ob- szar roboczy i zwraca uchwyt kontekstu urzšdzenia (ang. handle to a device con- text). Kontekst urzšdzenia odnosi się do fizycznego urzšdzenia wyjœciowego (jak ekran) i jego programu obsługi. Uchwyt kontekstu urzšdzenia jest ci potrzebny, aby wyœwietlić tekst i grafikę w obszarze roboczym okna. Używajšc uchwytu kontekstu urzšdzenia zwróconego przez BeginPaint, nie możesz rysować poza obszarem roboczym, choc'byœ nie wiem jak próbował. Wywołanie EndPaint zwal- nia uchwyt kontekstu urzšdzenia i nie można z niego więcej korzystać. Jeœli procedura okna nie przetwarza komunikatu WM_PAINT (co zdarza się bar- dzo rzadko), to komunikat ten musi być przekazany do Def lNindowProc. Def 4Vin- dowProc po prostu wywołuje BeginPaint i EndPaint, w następstwie czego obszar roboczy jest zatwierdzany. WndProc po zakończeniu wywołania BeginPaint wywołuje z kolei GetClient-Rect: GetClientRect (hwnd, &rect) ; Pierwszy argument jest uchwytem okna programu. Drugi argument jest wskaŸ- nikiem do struktury prostokšta typu RECT. Ta struktura zawiera cztery pola LONG o nazwach left, top, right i bottom. Funkcja GetClientRect ustawia w tych czterech polach rozmiary obszaru roboczego okna. Pola left i top sš zawsze usta- wiane na 0. W zwišzku z tym pola right i bottom okreœlajš w pikselach odpowied- nio szerokoœć i wysokoœć obszaru roboczego. WndProc nie analizuje struktury RECT, przekazuje tylko wskaŸnik do niej w czwar- tym argumencie DrawText: DrawText (hdc. TEXT ("Hello, Windows 98!"), -1, &rect, DT SINGLELINE ţ DT CENTER ţ DT VCENTER) ; DrawText - jak wskazuje nazwa - rysuje napisy. W zwišzku z tym pierwszym argumentem jest uchwyt kontekstu urzšdzenia, zwrócony przez BeginPaint. Dru- gim argumentem jest tekst do narysowania, a trzeci argument ma wartoœć -1, co oznacza, że łańcuch znaków tekstu jest zakończony znakiem zerowym. Ostatnim argumentem DrawText jest szereg znaczników bitowych zdefiniowanych w WINUSER.H. (Ponieważ DrawText wyœwietla dane wyjœciowe, to wydaje się być funkcjš GDI, w rzeczywistoœci jest częœciš modułu User, gdyż jest funkcjš ry- sujšcš doœć wysokiego poziomu. Funkcja ta jest udokumentowana w /Platform SDK/Graphics and Multimedia Services/GDI/Fonts and Text). Znaczniki wskazujš, że tekst powinien być wyœwietlony jako pojedyncza linia wypoœrodkowana w pio- " nie i w poziomie wewnštrz prostokšta okreœlonego przez czwarty argument. Wy- wołanie tej funkcji powoduje wyœwietlenie napisu "Hello, Windows 98!" na œrodku obszaru roboczego. Jeœli obszar roboczy staje się nieważny ( jak przy zmianie rozmiarów okna), Wnd- Proc otrzymuje nowy komunikat WM-PAINT. WndProc uzyskuje aktualny roz- miar okna, wywołujšc GetClientRect, i wyœwietla ponownie tekst na œrodku okna. 62 Częœć I: Podstawy Komunikat WM DESTROY Komunikat WM DESTROY także jest ważnym komunikatem. Oznacza on, że Windows niszczy okno na polecenie użytkownika. Komunikat ten jest wynikiem kliknięcia przez użytkownika przycisku Zamknij albo wybrania Zamknij z menu systemowego programu. (W dalszej częœci rozdziału dokładniej omówię genero- wanie komunikatu WM DESTROY przez system). HELLOWIN odpowiada na komunikat WM DESTROY standardowo: PostOuitMessage (0) ; Funkcja ta wstawia komunikat WM QUIT do kolejki komunikatów programu. Wspominałem wczeœniej, że GetMessage zwraca wartoœć różnš od zera dla każde- go komunikatu innego niż WM-QUIT, który pobiera z kolejki komunikatów. Gdy GetMessage pobierze komunikat WMţQUIT, zwróci 0. Spowoduje to, że WinMain opuœci pętlę komunikatów. Wtedy program wykona następujšcš instrukcję: return msg.wParam ; Pole wParam struktury jest wartoœciš przekazanš funkcji PostQuitMessage (zazwy- czaj 0). Instrukcja return powoduje wyjœcie z WinMain i zakończenie programu. Przeszkody w programowaniu windowsowym Nawet z moimi objaœnieniami kodu HELLOWIN, struktura i działanie progra- mu prawdopodobnie sš nadal doœć tajemnicze. W krótkim programie C, napisa- nym dla trybu znakowego, cały program mógł być zawarty w funkcji main. Funkcja WinMain w HELLOWIN zawiera tylko częœć programu odpowiedzialnš za zare- jestrowanie klasy okna, utworzenie okna, pobieranie i przesyłanie komunikatów z kolejki komunikatów Właœciwa częœć programu zawarta jest w procedurze okna. W HELLOWIN nie wykonuje ona zbyt wiele - WndProc po prostu odgrywa plik dŸwiękowy i wy- œwietla napis w oknie. Ale w dalszych rozdziałach dowiesz się, że prawie wszyst- ko, co robi program windowsowy, jest odpowiedziš na komunikaty przychodzš- ce do procedury okna. Musisz się do tego przyzwyczaić, zanim zaczniesz pisać programy dla Windows. Nie wywołuj mnie, ja wywołam ciebie Programiœci sš dobrze obeznani z ideš wywoływania funkcji systemu operacyj- nego w celu zrobienia czegoœ. Na przykład programiœci C używajš funkcji fopen do otwarcia pliku. Funkcja ta jest zrealizowana za pomocš wywołania funkcji sys- temu operacyjnego, która otwiera plik. Nie ma problemu. Ale Windows jest inny. Chociaż posiada kilka tysięcy wywołań funkcji, wywohx- je również funkcje z twojego programu, zwłaszcza procedurę okna, którš nazwa- liœmy WndProc. Procedura okna jest powišzana z klasš okna, którš program za- rejestrował wywołujšc RegisterClass. Okno, utworzone na podstawie danej klasy okna, używa procedury do przetwarzania wszystkich komunikatów skierowa- nych do niego. Windows wysyła komunikat do okna przez wywołanie procedu- ry okna. - Rozdział 3: Windows i komunikaty 63 Windows wywołuje WndProc, gdy okno jest tworzone pierwszy raz. Windows wywołuje WndProc, gdy okno jest ostatecznie niszczone. Windows wywołuje WndProc, gdy okno zmienia rozmiary, jest przesuwane lub minimalizowane. Windows wywołuje WndProc, gdy użytkownik klika przyciskiem myszy w ob- szarze okna. Windows wywołuje WndProc, gdy znaki sš wprowadzane z klawia- tury. Windows wywołuje WndProc, gdy pozycja zostanie wybrana z menu. Win- dows wywołuje WndProc w trakcie korzystania z paska przewijania. Windows wywołuje WndProc, aby oznajmić, że trzeba odœwieżyć obszar roboczy okna. Wszystkie te wywołania WndProc majš formę komunikatów. Główna częœć więk- szoœci programów windowsowych zajmuje się ich obsługš. Komunikaty, które Windows wysyła do programu, sš zazwyczaj identyfikowane przez nazwy, któ- re zaczynajš się przedrostkiem WM i sš zdefiniowane w pliku nagłówkowym WINUSER.H. W rzeczywistoœci idea procedury wewnštrz programu, która jest wywoływana spoza programu, nie jest całkiem nieznana w programowaniu w trybie znako- wym. Funkcja signal w C może przechwycić przerwanie od [Ctrl+C] lub inne przerwania systemu operacyjnego. Stare programy, pisane dla MS-DOS-a, często przechwytywały przerwania sprzętowe. Ale w Windows ta koncepcja rozcišga się na wszystko. Wszystko, co może dziać się z oknem, jest przekazywane do procedury okna w formie komunikatu. Proce- dura okna odpowiada w pewien sposób na ten komunikat lub przekazuje go do DefWindowProc w celu domyœlnego przetworzenia. Parametry procedury okna, wParam i lParam, nie sš używane w HELLOWIN, lecz tylko'przekazywane do DefWindowProc. Te parametry dostarczajš procedurze okna dodatkowych informacji o komunikacie. Ich znaczenie zależy od komunikatu. Popatrzmy na przykład. Ilekroć obszar roboczy okna zmienia rozmiary, Windows wywołuje procedurę okna dla tego okna. Parametr hwnd procedury okna jest uchwy- tem okna zmieniajšcego rozmiary. (Pamiętaj, że jedna procedura okna może obsługi- wać komunikaty dla wielu okien, które zostały utworzone na podstawie tej samej kla- sy okna. Parametr hzvnd mówi procedurze okna, które okno otrzymuje komunikat). Parametr message wynosi WMţSIZE. Parametr wParam dla komunikatu WMţSlZE ma wartoœć SIZEţRESTOREI7, SIZEţMINávffZED, SIZEţMA?mvIIZED, SlZEţMAXSHOW lub SIZEţMA7ţţlDE (zdefiniowane w pliku nagłówkowym WIIţlUSER.H jako liczby od 0 do 4). Oznacza to, że parametr wParam wskazuje, ezy okno wróciło do poprzed- niej wielkoœci, zostało zininimalizowane, zmaksymalizowane albo ukryte. Parametr lParam podaje riowy rozmiar okna. Nowa szerokoœć (wartoœć 16-bito- wa) i nowa wysokoœć (wartoœć 16-bitowa) sš połšczone razem w 32-bitowy 1Pa- ram. Plik nagłówkowy WINDERH definiuje kilka poręcznych makr, które pomo- gš wyłuskać te dwie wartoœci z lParam. Zrobimy to w następnym rozdziale. Czasem komunikaty generujš inne komunikaty jako wynik przetwarzania Def 4Vin- dowProc. Załóżmy, że uruchomiłeœ HELLOWIN i kliknšłeœ przycisk Zamknij lub wybrałeœ Zamknij z menu systemowego, używajšc klawiatury lub myszy. DeţVin- dowProc przetworzy te dane wejœciowe. Gdy wykryje, że wybrałeœ opcję Zamknij, wyœle komunikat WM SYSCOMMAND do procedury okna. WndProc przekaże ten komunikat do DefWindowProc. DefWindowProc odpowie przez wysłanie ko- w-w 64 Częœć I: Podstawy munikatu WM-CLOSE do procedury okna. WndProc ponownie przekaże ten ko- munikat do Def 4VindowProc. DefTNindowProc odpowie na komunikat WM-CLOSE wywołaniem DestroyWindow. DestroyWindow spowoduje, że Windows wyœle ko- munikat WM DESTROY do procedury okna. W końcu WndProc odpowie na ten komunikat wywołaniem PostQuitMessage; wówczas w kolejce komunikatów zo- stanie umieszczony komunikat WM-QUIT. Ten komunikat powoduje zakończe- nie pętli komunikatów w WinMain i zamknięcie programu. Komunikaty kolejkowane i niekolejkowane Mówiłem o wysyłaniu przez Windows komunikatu do okna, co oznacza, że Win- dows wywołuje procedurę okna. Ale program Windows ma także pętlę komuni- katów, która pobiera komunikaty z kolejki komunikatów przez wywołanie Get- Message i przekazuje do procedury okna przez wywołanie DispatchMessage. Tak więc, czy Windows odpytuje kolejkę w celu pobrania wiadomoœci (tak jak zwykły program trybu znakowego pobiera dane z klawiatury), a następnie prze- kazuje te komunikaty do jakiegoœ miejsca programu? Czy też otrzymuje komuni- katy bezpoœrednio spoza programu? I tak, i tak. Komunikaty mogš być kolejkowane lub niekolejkowane. Komunikaty kolejkowane to te, które Windows umieszcza w kolejce komunikatów programu. W pętli ko- munikatów programu sš one pobierane i przekazywane do procedury okna. Ko- munikaty niekolejkowane sš wynikiem bezpoœredniego wywoływania procedu- ry okna przez Windows. Mówi się, że komunikaty kolejkowane sš "wstawiane" do kolejki komunikatów, a komunikaty niekolejkowane sš "wysyłane" do proce- dury okna. Procedura okna otrzymuje wszystkie komunikaty - kolejkowane i nie- kolejkowane - przychodzšce do okna. Procedura okna jest "centrum komunika- tów" okna. Komunikaty kolejkowane to głównie te, które przychodzš od użytkownika w formie: naciœnięć klawiszy (komunikaty, takie jak: WM KEYDOWN i WM KEY- UP), znaków będšcych rezultatem naciœnięć klawiszy (WM-CHAR), ruchu my- szy (WMţMOUSEMOVE) i kliknięć przycisków myszy (WM LBUTTONDOWN). Komunikaty kolejkowane zawierajš także komunikat zegara (WM TIMER), ko- munikat odœwieżania (WM-PAINT) i komunikat wyjœcia (WM QUIT). Komunikaty niekolejkowane to wszystkie pozostałe. Sš one najczęœciej rezulta- tem wywoływania pewnych funkcji Windows. Na przykład, gdy WinMain wy- woła CreateWindow, Windows tworzy okno i podczas tego procesu wysyła proce- durze okna komunikat WM-CREATE. Gdy WinMain wywoła funkcję ShowWin- dow, Windows wysyła procedurze okna komunikaty WM SIZE i 4VM SHOW- WINDOW. Gdy WinMain woła UpdateWindow, Windows wysyła procedurze okna komunikat WM PAINT. Komunikaty kolejkowane, oznaczajšce wejœcie z myszy lub klawiatury, mogš zaowocować także komunikatami niekolejkowanymi. Na przykład, gdy wybierzesz pozycję menu używajšc myszy albo klawiatury, ko- munikat myszy albo klawiatury jest kolejkowany, ale ostateczny komunikat WM COMMAND, oznaczajšcy, że wybrano pozycję menu, jest niekolejkowany. Ten proces jest doœć złożony, ale na szczęœcie większoœć komplikacji to problem Windows, a nie naszego programu. Z perspektywy procedury okna komunikaty Rozdział 3: Windows i komunikaty 65 te przychodzš w sposób uporzšdkowany i zsynchronizowany. Procedura okna może zrobić coœ z tymi komunikatami albo je zignorować. Gdy mówię, że komunikaty przychodzš w uporzšdkowany i zsynchronizowany sposób, mam na myœli, że komunikaty te nie zachowujš się jak przerwania sprzę- towe. Podczas przetwarzania jednego komunikatu w procedurze okna program nie zostanie nagle przerywany przez przyjœcie innego komunikatu. Pomimo że programy Windows mogš mieć wiele wštków wykonania, kolejka komunikatów każdego wštku obsługuje tylko komunikaty dla okien, których procedury okien sš wykonywane w tym wštku. Innymi słowy, pętla komunika- tów i procedura okna nie działajš jednoczeœnie. Gdy pętla komuńikatów pobie- rze komunikat ze swojej kolejki komunikatów i wywoła funkcję DispatchMessage, aby przesłać komunikat do procedury okna, to powrót z DispatchMessage nie na- stšpi, dopóki procedura okna nie zwróci kontroli z powrotem do Windows. Jednak procedura okna może wywołać funkcję, która wyœle jej inny komunikat, a wtedy będzie musiała zakończyć przetwarzanie drugiego komunikatu, zanim ta funkcja zwróci sterowanie i procedura okna zacznie przetwarzać oryginalny komunikat. Na przykład, kiedy procedura okna wywołuje UpdateWindow, Win- dows wywołuje procedurę okna z komunikatem WM PAINT. Kiedy procedura okna zakończy przetwarzanie komunikatu WM PAINT, wywołanie UpdateWin- dow powinno zwrócić sterowanie z powrotem do procedury okna. Oznacza to, że procedura okna musi być wielowejœciowa. W większoœci przypad- ków nie stanowi to problemu, ale powinieneœ być tego œwiadomy. Na przykład załóżmy, że ustawiłeœ zmiennš statycznš w procedurze okna w czasie przetwa- rzania komunikatu i wtedy wywołałeœ funkcję Windows. Czy jesteœ przekonany, że po powrocie z tej funkcji zmienna ma cišgle takš samš wartoœć? Nie zawsze - nie, jeœli konkretna funkcja Windows, którš wywołałeœ, utworzyła inny komuni- kat i procedura okna zmieniła wartoœć zmiennej podczas przetwarzania drugie- go komunikatu. To jest powodem, dla którego różne rodzaje optymalizacji kom- pilatora muszš być wyłšczane podczas kompilacji programów windowsowych. W wielu przypadkach procedura okna musi zachowywać informację, którš otrzy- mała w jednym komunikacie i użyć jej przy przetwarzaniu innego komunikatu. Ta informacja musi być zachowana w zmiennej zdefiniowanej jako static w pro- cedurze okna lub w zmiennych globalnych. Oczywiœcie, będziesz to lepiej rozumiał po przeczytaniu dalszych rozdziałów, w któ- rych procedury okna zostanš rozszerzone o przetwarzanie dalszych komunikatów. Szybkie wejœcie i wyjœcie Windows 98 i Windows NT to œrodowiska wielozadaniowe z wywłaszczaniem. Oznacza to, że gdy jeden program działa zbyt długo, Windows pozwala użyt- kownikowi przekazać sterowanie innemu programowi. Jest to cenna właœciwoœć, będšca głównš zaletš obecnej wersji Windoivs w stosunku do dawnej wersji 16- bitowej. Jednak na skutek budowy Windows ta wielozadaniowoœć z wywłaszczaniem nie zawsze działa tak, jakbyœmy tego oczekiwali. Załóżmy, że twój program prze- 66 Częœć I: Podstawy twarza konkretny komunikat w cišgu minuty lub dwóch. Użytkownik może prze- łšczyć się do innego programu, ale nie może nic zrobić z twoim programem. Nie może przesunšć okna twojego programu, zmienić jego rozmiarów, zminimalizo- wać lub zamknšć - zupełnie ruc nie może zrobić. Dzieje się tak, ponieważ twoja procedura okna jest odpowiedzialna za te funkcje, a przecież teraz jest zajęta wykonywaniem długotrwałej pracy. Można tego nie zauważyć, jeœli procedura okna wykonuje własne operacje przesuwania i zmiany rozmiarów, ale tak się dzieje w rzeczywistoœci. Pracę tę wykonuje Def4VindowProc, która musi się znaleŸć w twojej procedurze okna. Jeżeli twój program musi wykonać długotrwałš pracę przy przetwarzaniu kon- kretnego komunikatu, istniejš lepsze sposoby wykonania tego, które opiszę w roz- dziale 20. Nawet w systemie wielozadaniowym z wywłaszczaniem nie powinno się pozostawiać "zamrożonego" okna na ekranie, ponieważ przeszkadza to użyt- kownikom. Jest to tak dokuczliwe jak błędy, nietypowe zachowanie i niepełne pliki pomocy. Umożliwiaj użytkownikowi przerywanie i szybki powrót ze wszyst- kich komunikatów. Rozdział 4 W 'wi tlanie tekstu s e W poprzednim rozdziale omawialiœmy działanie prostego programu Windows 98, który wyœwietla pojedynczš linię tekstu na œrodku swojego okna lub - do- kładniej mówišc-na œrodku swojego obszaru roboczego. Dowiedzieliœmy się, że obszar roboczy jest tš częœciš całego okna aplikacji, która nie obejmuje paska ty- tułu, ramki okna oraz opcjonalnie paska menu, paska stanu i pasków przewija- nia, (jeœli występujš). Krótko mówišc, obszar roboczy to ta częœć okna, w której program może swobodnie rysować i dostarczać informacji wizualnej użytkowni- kowi. Z obszarem roboczym swojego programu możesz zrobić niemal wszystko - wszystko, o ile nie założysz, że będzie on miał konkretny rozmiar lub że rozmiar pozostanie stały w czasie działania programu. Jeœli nie jesteœ przyzwyczajony do pisania programów w œrodowisku graficznym, te warunki mogš stanowić dla cie- bie wstrzšs. Nie możesz myœleć kategoriami ustalonej liczby 80 znaków w linii. Twój program musi dzielić ekran z innymi programami Windows. To użytkow- nik Windows kontroluje rozmieszczenie okien programów na ekranie. Chociaż jest możliwe, aby programista utworzył okno ustalonego rozmiaru (stosowne dla kalkulatorów lub podobnych narzędzi), użytkownicy zwykle mogš zmieniać roz- miar okien aplikacji. Twój program musi reagować na zadany rozmiar okna i ro- bić z nim coœ rozsšdnego. I tu pojawiajš się dwie skrajnoœci. Jednym razem twój program może mieć ob- szar roboczy tak mały, że ledwie się w nim zmieœci "Hello", a innym razem może być uruchomiony na wielkim ekranie z dużš rozdzielczoœciš i wtedy ma tak duży obszar roboczy, że zmieszczš się dwie strony tekstu i zostanie jeszcze dużo wol- nego miejsca. Inteligentne uwzględnienie obu sytuacji stanowi ważnš częœć pro- gramowania Windows. W tym rozdziale nauczymy się, jak program może wyœwietlić na powierzchni swojego obszaru roboczego coœ bardziej skomplikowanego, niż to, co pokazywa- liœmy w poprzednim rozdziale. Kiedy program wyœwietla tekst albo grafikę w swoim obszarze roboczym, często mówimy, że maluje swój obszar roboczy. Ten rozdział poœwięcony jest nauce malowania. Chociaż Windows zawiera wiele funkcji wyœwietlajšcych GDI, w tym rozdziale zajmuję się tylko wyœwietlaniem prostych linii tekstu. Ignoruję też dostępne w Windows różne kroje i wielkoœci czcionek, używajšc tylko domyœlnej czcionki systemowej. Naprawdę nie jest to ograniczeniem, chociaż może na takie wyglš- dać. Problemy, które napotkamy i rozwišżemy w tym rozdziale, dotyczš całoœci programowania w Windows. Gdy wyœwietlasz łšcznie tekst i grafikę, wielkoœć znaków domyœlnej czcionki Windows często okreœla rozmiary grafiki. 68 Częœć I: Podstawy Ten rozdział tylko pozornie jest poœwięcony wyłšcznie nauce malowania, w rze- czywistoœci uczy podstaw programowania niezależnego od konkretnego urzš- dzenia. Programy windowsowe nie zakładajš wielkoœci obszaru roboczego czy nawet wielkoœci znaków tekstu. Muszš wykorzystać własnoœci Windows do uzy- skania informacji o œrodowisku, w którym działajš. Malowanie i odœwieżanie W œrodowiskach znakowych programy mogš zasadniczo pisać w każdym miej- scu ekranu. To, co program umieszcza na ekranie, pozostaje tam i nie znika nie- spodziewanie. Program może wtedy pozbyć się danych potrzebnych do odtwo- rzenia zawartoœci ekranu. W Windows możesz rysować tekst i grafikę tylko w obszarze roboczym swojego okna i nie masz pewnoœci, że to, co wyœwietliłeœ, pozostanie tam aż do chwili, gdy twój program wyœwietli coœ nowego. Na przykład użytkownik może przesunšć okno innego programu na ekranie tak, że częœciowo zakryje okno twojej aplikacji. Win- dows nie próbuje zapamiętać zawartoœci obszaru twojego okna, które jest zakry- wane przez inny program. Kiedy ten program zostanie usunięty, Windows popro- si, żeby twój program odœwieżył tę częœć swojego obszaru roboczego. Windows jest systemem sterowanym komunikatami. Zawiadamia aplikacje o róż- nych zdarzeniach, wstawiajšc komunikaty do kolejek komunikatów tych aplika- cji albo wysyłajšc komunikat do odpowiedniej procedury okna. Wysyłajšc komu- nikat WM PAINT, Windows zawiadamia procedurę okna, że częœć obszaru ro- boczego okna potrzebuje odœwieżenia. Komunikat WM PAINT Większoœć programów Windows podczas inicjacji w WinMain, tuż przed wejœciem w pętlę komunikatów, wywołuje funkcję UpdateWindow. Windows ma wtedy oka- zję wysłać do procedury okna swój pierwszy komunikat WM PAINT. Ten komu- nikat zawiadamia procedurę okna, że musi być namalowany obszar roboczy. Od tej chwili procedura okna powinna być gotowa w każdej chwili do przetwarza- nia dodatkowych komunikatów WM PAINT i w miarę potrzeb ponownego na- malowania całego obszaru roboczego okna. Procedura okna otrzymuje komuni- kat WM PAINT, gdy wystšpi jedno z następujšcych zdarzeń: ů Użytkownik odsłoni uprzednio niewidoczny obszar okna. ů Użytkownik zmieni wielkoœć okna (jeœli styl klasy okna ma ustawione bity CS HREDRAW i CW VREDRAW). ů Program użyje funkcji ScrollWindow albo ScroIIDC do przewinięcia częœci swo- jego obszaru roboczego. ů Program użyje funkcji InvalidatelZect albo InvalidateRgn, by jawnie wygenero- wać komunikat WM PAINT. W niektórych przypadkach, kiedy częœć obszaru roboczego jest chwilowo prze- słonięta, Windows próbuje zapamiętać ten fragment ekranu i póŸniej go odtwo- rzyć. Nie zawsze się to udaje. Windows może czasami wysyłać komunikat WM PAINT, jeœli: Rozdział 4: Wyœwietlanie tekstu s9 ů Windows usunie okno dialogowe lub okno komunikatów, które zasłaniało częœć okna, ů menu zostało rozwinięte, a następnie zwinięte, ů została wyœwietlona wskazówka. W kilka przypadkach Windows zawsze zapamiętuje zasłonięty fragment ekra- nu, a następnie przywraca go. Ma to miejsce, gdy: ů przez obszar roboczy jest przesuwany kursor myszy, ů przez obszar roboczy jest przecišgana ikona. Praca z komunikatem WM PAINT wymaga, żebyœ zmienił swój sposób myœle- nia o wyœwietlaniu na ekranie. Program powinien być tak skonstruowany, by gromadził wszystkie informacje konieczne do namalowania obszaru roboczego, ale malował tylko "na żšdanie" - gdy Windows wyœle procedurze okna komuni- kat WM PAINT. Jeœli twój program wymaga aktualizacji obszaru roboczego, może zmusić Windows do wygenerowania komunikatu WM PAINT. Może się to wy- dawać okrężnš drogš wyœwietlenia czegoœ na ekranie, ale zyska na tym przej- rzystoœć programu. Prostokšty zatwierdzone i unieważnione Chociaż procedura okna powinna być gotowa do odœwieżenia całego obszaru ro- boczego za każdym razem, gdy otrzymuje komunikat WM PAINT, najczęœciej wystarczy odœwieżyć mniejszy obszar, zwykle prostokštny, wewnštrz obszaru roboczego. Dzieje się tak wtedy, gdy okno dialogowe zakrywa częœć obszaru ro- boczego. Odœwieżenia wymaga wówczas prostokštny obszar odsłonięty po za- mknięciu okna dialogowego. Obszar ten jest nazywany regionem unieważnionym lub regionem aktualizacji. Obecnoœć regionu unieważnionego w obszarze roboczym stanowi dla Windows sygnał, aby umieœcić w kolejce komunikatów aplikacji komunikat WM PAINT. Twoja procedura okna otrzyma komunikat WM PAINT tylko wtedy, gdy częœć twojego obszaru roboczego jest unieważniona. Windows przechowuje dla własnych potrzeb "strukturę informacji o malowaniuř dla każdego okna. Struktura ta zawiera, między innymi, współrzędne najmniejsze- go prostokšta, który obejmuje unieważniony region. Nazywa się go prostokštem unieważnionym. Jeœli inny region obszaru roboczego zostanie unieważniony, zanim procedura okna przetworzy komunikat WMţPAINT, to Windows obliczy nowy re- gion unieważniony (i nowy prostokšt unieważniony), zawierajšcy oba te obszary i za- pamięta zaktualizowanš informację w strukturze informacji o malowaniu. Natomiast nie umieœci kolejnego komunikatu WMţPAINT w kolejce komunikatów. Procedura okna może unieważniać prostokšt w swoim własnym obszarze robo- czym, wywołujšc InvalidateRect. Jeœli kolejka komunikatów zawiera już komuni- kat WM PAINT, Windows oblicza nowy prostokšt unieważniony. W przeciwnym razie umieszcza komunikat WMţPAINT w kolejce komunikatów. Procedura okna może otrzymać współrzędne unieważnionego prostokšta, kiedy otrzyma komu- nikat WMţPAINT (jak zobaczymy w dalszej częœci rozdziału). Może też je uzy- skać w dowolnej chwili, wywohxjšc GetUpd.ţteRect. w- Częœć I: Podstawy Po wywołaniu przez procedurę okna funkcji BeginPaint podczas przetwarzania komunikatu WMţPAINT, zatwierdzany jest cały obszar roboczy. Program może też zatwierdzić każdy obszar prostokštny wewnštrz obszaru roboczego, wywo- łujšc funkcję validateftect. Jeœli to wywołanie spowoduje zatwierdzenie całego unieważnionego obszaru, wtedy wszystkie znajdujšce się w kolejce komunikaty WM-PAINT sš z niej usuwane. Wprowadzenie do GDI Aby pomalować obszar roboczy swojego okna, użyj funkcji GDI Windows (ang. Graphics Device Interface - interfejs urzšdzenia graficznego). Windows dostarcza wielu funkcji GDI wyœwietlajšcych tekst w obszarze roboczym okna. W poprzed- nim rozdziale poznaliœmy funkcję DrawText, ale najczęœciej używana jest funkcja TextOut: TextOut (hdc, x, y, psText, iLength) ; TextOut wyœwietla łańcuch znaków w obszarze roboczym okna. Argument psText jest wskaŸnikiem do tego łańcucha znaków, a iLength jest jego długoœciš. Argu- menty x i y okreœlajš pozycję poczštku tekstu w obszarze roboczym. (Wkrótce podam więcej szczegółów). Argument hdc jest uchwytem kontekstu urzšdzenia i ważnš częœciš GDI. W rzeczywistoœci każda funkcja GDI wymaga tego uchwytu jako swojego pierwszego argumentu. Kontekst urzšdzenia Przypomnij sobie, że uchwyt to po prostu liczba, która pozwala systemowi roz- poznać obiekt. Otrzymujesz uchwyt od Windows i wykorzystujesz go w innych funkcjach. Uchwyt kontekstu urzšdzenia jest wydanym przez Windows paszpor- tem do funkcji GDI. Dysponujšc tym uchwytem, możesz swobodnie malować w swoim obszarze roboczym i upiększać go, kierujšc się swoim gustem. Kontekst urzšdzenia (nazywany także po prostu DC - ang. device context) w rze- czywistoœci jest strukturš danych pamiętanš przez GDI. Kontekst urzšdzenia jest zwišzany z konkretnym urzšdzeniem wyjœciowym, jak ekran monitora lub drukarka. W przypadku ekranu kontekst urzšdzerńa jest zwykle powišzany z kon- kretnym oknem na ekranie. Niektóre wartoœci kontekstu urzšdzenia sš atrybutami graficznymi. Atrybuty te okreœlajš szczegółowo sposób działania funkcji wyœwietlajšcych GDI. Na przy- kład dla funkcji TextOut atrybuty kontekstu urzšdzenia okreœlajš kolor tekstu, kolor tła, sposób odwzorowania współrzędnych x i y funkcji na obszar roboczy okna oraz rodzaj czcionki używanej przez Windows do wyœwietlania tekstu. Aby program mógł coœ namalować, musi najpierw otrzymać uchwyt kontekstu urzšdzenia. Gdy uzyskasz ten uchwyt, Windows wypełni wewnętrznš strukturę kontekstu urzšdzenia domyœlnymi wartoœciami atrybutów. Jak zobaczysz w dal- szej częœci rozdziału, możesz zmienić te domyœlne wartoœci przez wywołanie róż- nych funkcji GDI. Inne funkcje GDI pozwolš ci otrzymać aktualne wartoœci tych atrybutów. Sš oczywiœcie jeszcze inne funkcje GDI, które pozwalajš faktycznie malować w obszarze roboczym okna. T'ţ Rozdział 4: Wyœwietlanie tekstu 71 Gdy program skończy malować swój obszar roboczy, powinien zwolnić uchwyt kontekstu urzšdzenia. Zwolniony uchwyt staje się nieważny i nie może być wię- cej używany. Program powinien otrzymać i zwolnić uchwyt podczas przetwa- rzania pojedynczego komunikatu. Z wyjštkiem kontekstu urzšdzenia utworzo- nego wywołaniem CreateDC (funkcji tej nie omawiam w tym rozdziale), nie po- winieneœ przetrzymywać uchwytu kontekstu urzšdzenia pomiędzy jednym a dru- gim komunikatem. Kiedy aplikacje Windows przygotowujš się do malowania na ekranie, najczę- œciej używajš dwóch sposobów otrzymania uchwytu kontekstu urzšdzenia. Otrzymywanie uchwytu kontekstu urzšdzenia. Sposób pierwszy Możesz użyć tej metody, gdy przetwarzasz komunikat WNfţPAINT. Głównš rolę odgrywajš tu dwie funkcje: BeginPaint i EndPaint. Obie te funkcje wymagajš uchwytu okna, który jest przekazywany do procedury okna jako argument, i ad- resu zmiennej struktury typu PAINTSTRUCT, która jest zdefiniowana w pliku nagłówkowym WINUSER.H. Programujšcy w Windows zwykle nazywajš tę zmiennš struktury ps (ang. paint structure) i definiujš w procedurze okna nastę- pujšco: PAINTSTRUCT ps ; Podczas przetwarzania komunikatu WMţPAINT procedura okna wywołuje naj- pierw BeginPaint. Funkcja BeginPaint przede wszystkim sprawia, że tło obszaru unieważnionego zostaje wyczyszczone i przygotowane do malowania. Funkcja ta wypełnia także pola struktury ps. Wartoœciš zwracanš przez BeginPaint jest uchwyt kontekstu urzšdzenia. Jest on zwykle przechowywany w zmiennej o na- zwie hdc. Możesz jš zdefiniować w swojej procedurze okna w następujšcy spo- sób: HDC hdc ; Typ danych HDC jest zdefiniowany jako 32-bitowa liczba całkowita bez znaku. Program może potem używać funkcji GDI, takich jak TextOut, które wymagajš uchwytu kontekstu urzšdzenia. Wywołanie EndPaint zwalnia uchwyt kontekstu urzšdzenia. Zazwyczaj przetwarzanie komunikatu WMţPAINT wyglšda tak: case WM_PAINT: hdc = BeginPaint (hwnd. &ps) ; [wywolania funkcji GDI] EndPaint (hwnd. &ps) ; return 0 : Podczas przetwarzania komunikatu WMţPAINT procedura okna musi wywoły- wać zawsze parę funkcji: BeginPaint i EndPaint. Jeœli procedura okna nie przetwarza komunikatu WMţPAINT, musi go przekazać do funkcji DefWindowProc, która jest domyœlnš procedurš okna w Windows. Def tNindowProc przetwarza komunikat WMţPAINT w następujšcy sposób: case WM_PAINT: BeginPaint (hwnd, &ps) EndPaint (hwnd, &ps) ; return 0 : w- Częœć I: Podstawy Sekwencja wywołań BeginPaint i EndPaint, bez wywoływania w międzyczasie innych funkcji GDI, zatwierdza poprzednio unieważniony region. Ale nie rób tego w ten sposób: case WMţPAINT: return 0 ; // LE !!1 Windows umieszcza komunikat WMţPAINT w kolejce komunikatów, ponieważ częœć obszaru roboczego jest unieważniona. Dopóki nie wywołasz BeginPaint i End- Paint (lub validateRect), Windows nie zatwierdzi tego obszaru. Natomiast wyœle ci następny komunikat WM PAINT i jeszcze jeden, aż do skutku. Struktura informacji o malowaniu Wspominałem wczeœniej o strukturze informacji o malowaniu, którš Windows zakłada dla każdego okna. To właœnie jest PAINTSTRUCT. Struktura ta jest zde- finiowana następujšco: typedef struct tagPAINTSTRUCT ( HDC hdc ; BOOL fErase ; RECT rcPaint ; BOOL fRestore ; BOOL fIncUpdate ; BYTE rgbReservedC327 ; ) PAINTSTRUCT ; Windows wypełnia pola tej struktury podczas wywoływania przez twój program funkcji BeginPaint. Twój program może używać tylko trzech pierwszych pól. Reszta jest do dyspozycji Windows. Pole hdc jest uchwytem kontekstu urzšdzenia. Przy nadmiarowoœci właœciwej dla Windows wartoœciš zwracanš przez BeginPaint jest także uchwyt kontekstu urzšdzenia. W większoœci przypadków fErase będzie miało wartoœć FALSE (0), co oznacza, że Windows już wyczyœcił tło unieważnio- nego prostokšta. Zostało to zrobione wczeœniej, w funkcji BeginPaint. (Gdybyœ chciał sam czyœcić tło w swojej procedurze okna, możesz przetwarzać komuni- kat WM ĘRASEBKGND). Windows czyœci tło za pomocš pędzla okreœlonego w polu hbrBackground struktury WNDCLASS, służšcej do rejestracji klasy okna w WinMain. Wiele programów Windows używa zazwyczaj białego pędzla do tła okna. Okreœlenie pędzla odbywa się, gdy program ustawia pola struktury klasy okna za pomocš instrukcji: wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE BRUSH) ; Jednak jeœli twój program unieważnia prostokšt obszaru roboczego przez wywo- łanie funkcji InvalidateRect, wówczas ostatni argument funkcji okreœla, czy chcesz wyczyœcić tło. Jeœli tym argumentem jest FALSE (to jest 0), Windows nie wyczy- œci tła i po wywołaniu BeginPaint pole fErase struktury PAINTSTRUCT będzie miało wartoœć TRUE (niezerowš). Pole rcPaint struktury PAINTSTRUCT jest strukturš typu RECT. Jak już wiesz z rozdziału 3, struktura RECT definiuje prostokšt poprzez cztery pola o nazwach left, top, right i bottom. Pole rcPaint struktury PAINTSTRUCT definiuje granice unieważnionego prostokšta, jak widać na rysunku 4-1. Wartoœci te sš wyrażone w pikselach względem lewego górnego rogu obszaru roboczego. Unieważniony prostokšt jest obszarem, który powinieneœ odœwieżyć. Rozdział 4: Wyœwietlanie tekstu 73 Góra Dól Rysunek 4-1. Granice unieważnionego prostokšta Prostokšt rcPaint struktury PAINTSTRUCT jest nie tylko prostokštem unieważ- nionym; jest także prostokštem obcinania (ang. clipping rectangle). Oznacza to, że Windows ogranicza malowanie tylko do tego prostokšta obcinajšcego. Dokład- niej: jeœli unieważniony region nie jest prostokštny, to Windows ogranicza malo- wanie do wnętrza tego regionu. Aby malować poza aktualizowanym prostokštem podczas przetwarzania komu- nikatu WM PAINT, możesz wywołać następujšcš funkcję InvalidateRect (hwnd, NULL, TRUE) ; przed wywołaniem BeginPaint. To unieważni cały obszar roboczy i spowoduje, że BeginPaint wyczyœci tło. Wartoœć FALSE w ostatnim argumencie spowoduje pozostawienie tła. Cokolwiek tam było, pozostanie. Najwygodniejszym rozwišzaniem dla programu Windows jest po prostu odœwie- żanie całego obszaru roboczego za każdym razem, gdy nadejdzie komunikat WMţPAINT, niezależnie od zawartoœci struktury rcPaint. Na przykład jeœli częœć obrazu wyœwietlanego w obszarze roboczym zawiera koło, ale tylko częœć koła znalazła się w obszarze unieważnionym, nie ma sensu rysowanie tylko unieważ- nionej częœci koła. Narysuj całe koło. Gdy używasz uchwytu kontekstu urzšdze- nia zwróconego przez BeginPaint, Windows i tak nie będzie malował poza ob- szarem prostokšta rcPaint. Podczas przetwarzania komunikatu WMţPAINT w programie HELLOWIN z roz- działu 2 nie martwiliœmy się o unieważnione prostokšty. Jeœli obszar, gdzie tekst był wyœwietlany, znajdował się w unieważnionym prostokšcie, DrawText odœwie- żał go. Jeœli nie, to Windows w trakcie przetwarzania funkcji DrawText zauważał, że nie ma nic do wyœwietlenia na ekranie. Ale ustalenie tego zabiera trochę cza- 0 Lewy Prawy 74 Częœć I: Podstawy su. Programista dbajšcy o wydajnoœć i szybkoœć (a to dotyczy, mam nadzieję, nas wszystkich) podczas przetwarzania komunikatu WM-PAINT będzie chciał wy- korzystać informację o unieważnionym prostokšcie, aby uniknšć niepotrzebnych wywołań GDI. Jest to szczególnie ważne, jeœli malowanie wymaga użycia plików dyskowych, takich jak bitmapy. Otrzymywanie uchwytu kontekstu urzšdzenia. Sposób drugi Chociaż najlepiej jest tak zbudować program, aby podczas przetwarzania komu- nikatu WM-PAINT móc odœwieżać cały obszar roboczy, to czasem użyteczne jest też malowanie częœci obszaru roboczego podczas przetwarzania innych komuni- katów. Uchwyt kontekstu urzšdzenia może być ci potrzebny także do innych celów, takich jak pobieranie informacji o kontekœcie urzšdzenia. Aby otrzymać uchwyt kontekstu urzšdzenia obszaru roboczego okna, wywołaj GetDC, dzięki czemu otrzymasz uchwyt i ReleaseDC, gdy już nie jest ci potrzebny: hdc = GetDC (hwnd) ; [wywołania funkcji GDI] ReleaseDC (hwnd, hdc) ; Podobnie jak BeginPaint i EndPaint, funkcje GetDC i ReleaseDC powinny być wy- woływane parami. Jeœli wywołasz GetDC podczas przetwarzania komunikatu, po- winieneœ wywołać ReleaseDC, zanim opuœcisz procedurę okna. Nie wywołuj GetDC w jednym komunikacie, a ReleaseDC w innym. W przeciwieństwie do uchwytu kontekstu urzšdzenia zwracanego przez Begin- Paint, uchwyt kontekstu urzšdzenia zwracany przez GetDC ma prostokšt obci- nania równy całemu obszarowi roboczemu. Możesz malować w każdym miejscu obszaru roboczego, nie tylko w unieważnionym prostokšcie (jeœli rzeczywiœcie jest tam unieważniony prostokšt). W odróżnieniu od BeginPaint, GetDC nie za- twierdza żadnych unieważnionych obszarów. Jeœli musisz zatwierdzić cały ob- szar roboczy, możesz wywołać ValidateRect (hwnd, NULL) ; Z zasady należy używać wywołań GetDC i ReleaseDC w odpowiedzi na komuni- katy klawiatury (jak w edytorze tekstu) lub na komunikaty myszy (jak w progra- mie do rysowania). Pozwala to programowi na rysowanie w obszarze roboczym w odpowiedzi na polecenia użytkownika z klawiatury lub myszy, bez celowego unieważniania częœci obszaru roboczego w celu wygenerowania komunikatu WM PAINT. Jednak jeœli nawet malujesz podczas komunikatów innych niż WMţPAINT, twój program musi nadal przechowywać wystarczajšce informacje niezbędne do odœwieżenia ekranu, na wypadek gdy otrzyma komunikat WMţPA- INT. Funkcjš podobnš do GetDC jest GetWindowDC. Podczas gdy GetDC zwraca uchwyt kontekstu urzšdzenia do rysowania w obszarze roboczym okna, GetWindowDC zwraca uchwyt kontekstu urzšdzenia, który pozwala rysować w całym oknie. Na przykład twój program może używać uchwytu kontekstu urzšdzenia zwrócone- go przez GetWindowDC do malowania na pasku tytułu okna. Jednak wtedy twój program będzie musiał przetwarzać również komunikaty WMţNCPAINT (ma- lowanie poza obszarem roboczym - ang. nonclient paint). Rozdział 4: Wyœwietlanie tekstu 75 Funkcja TextOut - szczegóły TextOut jest najczęœciej używanš funkcjš GDI do wyœwietlania tekstu. Jej skład- nia to: TextOut (hdc, x, y, psText, iLength) ; Omówimy jš teraz bardziej szczegółowo. Pierwszym argumentem jest uchwyt kontekstu urzšdzenia - to wartoœć hdc zwró- cona przez GetDC albo wartoœć hdc zwrócona przez BeginPaint podczas przetwa- rzania komunikatu WM PAINT. Atrybuty kontekstu urzšdzenia sterujš sposobem wyœwietlania tekstu. Na przy- kład jeden z atrybutów okreœla kolor znaków. Domyœlnym kolorem (co przyjmu- jemy z radoœciš!) jest czamy. Domyœlny kontekst urzšdzenia sugeruje białe tło napisu. Gdy program wyœwietla tekst na ekranie, Windows używa tego koloru tła do wypełnienia prostokštnego obszaru otaczajšcego każdy znak, nazywane- go ramkš znaku (ang. character box). Kolor tła napisów nie jest identyczny z tłem okreœlanym przy definicji klasy okna. Tło w klasie okna jest pędzlem (wzorcem, który nie musi mieć czystego koloru) używanym przez Windows do czyszczenia obszaru roboczego. Nie jest on czę- œciš struktury kontekstu urzšdzenia. Gdy definiujesz strukturę klasy okna, więk- szoœć aplikacji Windows używa WHITE BRUSH - tak więc domyœlny kolor tła napisu w domyœlnym kontekœcie urzšdzenia jest taki sam jak kolor pędzla uży- wanego przez Windows do czyszczenia tła obszaru roboczego. Argument psText jest wskaŸnikiem do łańcucha znaków, a iLength jest liczbš zna- ków w łańcuchu. Jeœli psText wskazuje na łańcuch znaków w unikodzie, to liczba bajtów w łańcuchu jest równa podwojonej wartoœci iLength. Łańcuch znaków nie powinien zawierać żadnych znaków sterujšcych ASCII, takich jak powrót karetki, wysunięcie wiersza, tabulacja lub znak cofania. Windows wyœwietla te znaki ste- rujšce jako ramki lub wypełnione prostokšty. TextOut nie rozpoznaje bajtu zero- wego (lub liczby całkowitej zero w unikodzie) jako znacznika końca łańcucha zna- ków. Funkcja używa argumentu iLength do okreœlenia długoœci łańcucha znaków. Argumenty x i y w TextOut okreœlajš położenie punktu poczštkowego napisu w ob- szarze roboczym. Wartoœć x jest położeniem poziomym, a wartoœć y - pionowym. Gómy lewy róg pierwszego znaku jest umieszczony w punkcie o współrzędnych (x, y). W domyœlnym kontekœcie urzšdzenia poczštkiem układu współrzędnych (miejscem, w którym x i y sš równe 0) jest lewy górny róg obszaru roboczego. Jeœli używasz wartoœci zerowych dla x i y w TextOut, napis będzie się rozcišgał od le- wego górnego rogu obszaru roboczego. Gdy przeczytasz dokumentację funkcji rysujšcych GDI, takich jak TextOut, stwier- dzisz, że współrzędne przekazywane do funkcji sš zwykle okreœlane jako współ- rzędne logiczne. Co to dokładnie oznacza, wyjaœnimy bardziej szczegółowo w roz- dziale 5. Teraz wystarczy nam wiedza, że Windows ma wiele trybów odwzoro- wania, które okreœlajš sposób, w jaki współrzędne logiczne podawane w funk- cjach rysujšcych GDI sš tłumaczone na fizyczne współrzędne ekranu. Tryb od- wzorowania jest definiowany w kontekœcie urzšdzenia. Domyœlny tryb odwzo- rowania jest nazywany MMţTEXT (stosujšc identyfikator zdefiniowany w pliku Częœć I: Podstawy nagłówkowym WINGDI.H). W trybie odwzorowania MM-TEXT jednostki logicz- ne sš takie same jak jednostki fizyczne (tj. piksele) i względem lewego górnego rogu obszaru roboczego. Wartoœci x rosnš w miarę przesuwania się w prawš stronę obszaru roboczego, a wartoœci y rosnš w miarę przesuwania się w dół obszaru roboczego (zobacz rysunek 4-2). System współrzędnych MM-TEXT jest identyczny z systemem współrzędnych, którego używa Windows do definiowania prosto- kšta unieważnionego w strukturze PAINTSTRUCT. (Jednak nie jest tak dobrze przy innych trybach odwzorowania). Rysunek 4-2. Współrzędne x i y w trybie odwzorowania MM TEXT Kontekst urzšdzenia definiuje też region obcinania. Jak mówiliœmy, domyœlnym regionem obcinania dla uchwytu kontekstu urzšdzenia otrzymanego od GetDC jest cały obszar roboczy, a dla uchwytu kontekstu urzšdzenia otrzymanego od BeginPaint - obszar unieważniony. Gdy wywołasz TextOut, Windows nie wyœwietli tej częœci łańcucha znaków, która leży poza regionem obcinania. Jeœli znak znaj- duje się na granicy regionu obcinania, Windows wyœwietli tylko częœć znaku znaj- dujšcš się wewnštrz regionu. Wyœwietlanie na zewnštrz obszaru roboczego okna nie jest wcale proste, więc nie martw się, że zrobisz to przez nieuwagę. Czcionka systemowa Kontekst urzšdzenia definiuje też czcionkę, której Windows używa, gdy wywo- łujesz TextOut w celu wyœwietlenia tekstu. Domyœlna czcionka nosi nazwę czcionki systemowej lub (używajšc identyfikatora z pliku nagłówkowego WINGDI.H) SYSTEM FONT. Czcionki systemowej Windows używa domyœlnie do napisów na paskach tytułów, w menu i oknach dialogowych. W poczštkowych wersjach Windows czcionkš systemowš była czcionka niepro- porcjonalna (ang. fixed-pitch font), co oznacza, że wszystkie znaki majš tę samš Rozdział 4: Wyœwietlanie tekstu 77 szerokoœć, zupełnie jak w maszynie do pisania. Jednak poczšwszy od Windows 3.0, czcionkš systemowš została czcionka proporcjonalna (ang. variable-pitch font), co oznacza, że różne znaki majš różnš szerokoœć. Na przykład "W" jest szersze niż "i". Badania wykazały, że teksty wyœwietlone czcionkš proporcjonalnš sš bardziej czytelne od tekstów wyœwietlanych czcionkš nieproporcjonalnš. Ma to prawdopodobnie zwišzek z mniejszymi odległoœciami między znakami, co po- zwala oczom i umysłowi łatwiej wyłowić całe słowa niż pojedyncze znaki. Jak możesz sobie wyobrazić, przejœcie na czcionkę proporcjonalnš wymagało całko- witego przerobienia kodu Windows i zmusiło programistów do poznania nowych metod pracy z tekstem. Czcionka systemowa jest czcionkš rastrowš, co oznacza, że znaki sš zdefiniowa- ne w postaci matrycy pikseli. (W rozdziale 17 poznamy czcionki TrueType, które sš zdefiniowane w postaci skalowalnych konturów). Wielkoœć znaków czcionki systemowej jest w pewnym stopniu oparta na rozmiarach ekranu. Została tak zaprojektowana, aby na ekranie mieœciło się co najmniej 25 linii po 80 znaków tekstu każda. Rozmiary znaku Żeby wyœwietlić wiele linii tekstu, używajšc funkcji TextOut, musisz znać roz- miary znaków czcionki. Możesz rozmieœcić kolejne wiersze tekstu na podstawie wysokoœci znaku, a kolejne kolumny w obszarze roboczym na podstawie œred- niej szerokoœci znaków. Jaka jest wysokoœć i przeciętna szerokoœć znaków czcionki systemowej? Nie po- wiem ci tego. Raczej nie mogę powiedzieć. A naprawdę, to mógłbym powiedzieć, ale pewnie Ÿle. Wszystko zależy od wielkoœci ekranu mierzonej w pikselach. Win- dows wymaga minimalnej rozdzielczoœci ekranu 640 na 480 pikseli, lecz wielu użytkowników woli rozdzielczoœć 800 na 600 lub 1024 na 768. W dodatku przy tych dużych rozdzielczoœciach Windows pozwala użytkownikowi na wybór spo- œród różnej wielkoœci czcionek systemowych. Podobnie jak program może okreœlić rozmiary (lub "metrykę") interfejsu użytkow- nika przez wywołanie funkcji GetSystemMetrics, tak może okreœlić też rozmiary czcionki przez wywołanie GetTextMetrics. GetTextMetrics wymaga uchwytu kontek- stu urzšdzenia, ponieważ zwraca informację o czcionce aktualnie wybieranej w kon- tekœcie urzšdzenia. Windows kopiuje różne wartoœci metryki tekstu do struktury typu TEXTMETRIC zdefiniowanej w WINGDI.H. Struktura TEXTMETRIC zawie- ra 20 pól, ale jesteœmy zainteresowani tylko pierwszymi siedmioma: typedef struct tagTEXTMETRIC f LONG tmHeight ; LONG tmAscent ; LONG tmDescent ; LONG tmInternalLeading ; LONG tmExternalLeading ; LONG tmAveCharWidth ; LONG tmMaxCharWidth ; [inne pola struktury] 1 TEXTMETRIC, * PTEXTMETRIC ; Częœć I: Podstawy Wartoœci tych pól sš wyrażone w jednostkach, które zależš od trybu odwzoro- wania wybranego dla kontekstu urzšdzenia. W domyœlnym kontekœcie urzšdze- rua trybem odwzorowania jest MM TEXT, więc jednostkš rozmiaru sš piksele. Zanim sięgniesz po funkcję GetTextMetrics, najpierw musisz zdefiniować zmien- nš strukturalnš, zwykle nazywanš tm: TEXTMETRIC tm ; Jeœli musisz okreœlić metrykę tekstu, możesz dostać uchwyt kontekstu urzšdze- nia i wywołać GetTextMetrics: hdc = GetDC (hwnd) ; GetTextMetrics (hdc, &tm) ; ReleaseDC (hwnd, hdc) ; Możesz wtedy sprawdzić wartoœci w strukturze matrycy tekstu i prawdopodob- nie zachować kilka z nich w celu ponownego wykorzystania. Rozmiary tekstu - szczegóły Struktura TEXTMETRIC dostarcza różnych informacji o czcionce wybranej w kon- tekœcie urzšdzenia. Jednak pionowy rozmiar czcionki jest zdefiniowany tylko w pięciu polach struktury, z których cztery sš pokazane na rysunku 4-3. tmlnternalLeading tmAscent tmHeight Linia bazowa tmDescent Rysunek 4-3. Cztery wartoœci definiujšce pionowe rozmiary znaku czcionki Najważniejszš wartoœciš jest tmHeight, która jest sumš tmAscent i tmDescent. Te dwie wartoœci okreœlajš maksymalny rozmiar znaku w pionie, odpowiednio powyżej i poniżej linii bazowej. Termin leading (odstęp międzywierszowy) odno- si się do przestrzeni, którš drukarka zostawia pomiędzy liniami tekstu. W struk- turze TEXTMETRIC odstęp wewnętrzny jest włšczony do tmAscent (i stšd do tmHeight) i często obejmuje znaki akcentu. Pole TmlnternalLeading może być usta- wione na 0, a wtedy litery akcentowane sš nieco mniejsze, aby zmieœcić nad nimi znak akcentu. Rozdział 4: Wyœwietlanie tekstu 79 Struktura TEXTMETRIC zawiera też pole o nazwie tmExternalLeading, które nie jest zawarte w wartoœci tmHeight. Jest to odstęp, który projektant czcionki propo- nuje dać między kolejnymi wierszami wyœwietlanego tekstu. Możesz zaakcepto- wać lub odrzucić tę sugestię. W czcionce systemowej, którš spotkałem ostatnio, tmExternalLeading był zerowy, więc nie pokazałem go na rysunku 4-3. (Mimo że obiecywałem nie poruszać tematu rozmiarów czcionki systemowej, rysunek 4-3 przestawia domyœlnš czcionkę systemowš Windows dla rozdzielczoœci ekranu 640 na 480). Struktura TEXTMETRIC zawiera dwa pola opisujšce szerokoœć znaku: pole tmA- veCharWidth jest œredniš ważonš małych liter, a tmMaxCharWidth jest szerokoœciš najszerszego znaku w czcionce. Dla czcionki nieproporcjonalnej te wartoœci sš identyczne. (Dla czcionki przedstawionej na rysunku 4-3 tymi wartoœciami sš odpowiednio 7 i 14). Przykładowe programy w tym rozdziale wymagajš innej szerokoœci znaku - œred- niej szerokoœci wielkich liter. Możesz okreœlić jš doœć dokładnie jako 150% warto- œci tmAveCharWidth. Musisz zdawać sobie sprawę, że rozmiary czcionki systemowej zależš od wiel- koœci ekranu w pikselach podczas działania Windows, ale w niektórych przypad- kach użytkownik może też wybrać wielkoœć czcionki systemowej. Windows do- starcza interfejsu graficznego niezależnego od urzšdzeń, ale musisz mu pomóc. Nie pisz swoich programów Windows, zakładajšc konkretnš wielkoœć znaków. Nie zapisuj na sztywno w programie żadnych wartoœci. Użyj funkcji GetTextMe- trics, aby uzyskać tę informację. Formatowanie tekstu Ponieważ rozmiary czcionki systemowej nie zmieniajš się podczas sesji Windows, wystarczy, że tylko raz wywołasz GetTextMetrics w swoim programie. Dobrym miejscem do tego celu jest przetwarzanie komunikatu WM CIZEATE w procedu- rze okna. Komunikat WMţCREATE jest pierwszym komunikatem, który otrzy- muje procedura okna. Windows wywohzje twojš procedurę okna z komunika- tem WMţCREATE, kiedy wywołujesz CreateWindow w WinMain. Załóżmy, że piszesz program windowsowy, który wyœwietla kilka linii tekstu biegnšcych w dół obszaru roboczego. Będziesz chciał poznać szerokoœć i wyso- koœć znaku. Wewnštrz procedury okna możesz zdefiniować dwie zmienne: do przechowywania œredniej szerokoœci znaku (cxChar) i całkowitej wysokoœci zna- ku (cyChar): static int cxChar, cyChar ; Przedrostek c, dodawany do nazw zmiennych; oznacza "licznik", czyli tutaj licz- bę pikseli. W połšczeniu z x lub y przedrostek ten powoduje oznaczenie zmien- nej jako szerokoœci albo wysokoœci. Zmienne sš zdefiniowane jako static, ponie- waż sš potrzebne, gdy procedura okna przetwarza inne komunikaty, takie jak WMţPAINT. Możesz także zdefiniować te zmienne globalnie, poza funkcjš. Oto kod WM-CIZEATE, który pobiera szerokoœć i wysokoœć znaku czcionki sys- temowej: 80 Częœć I: Podstawy case WM_CREATE: hdc = GetDC (hwnd) : GetTextMetrics (hdc, &tm) ; cxChar = tm.tmAveCharWidth ; cyChar = tm.tmHeight + tm.tmExternalLeading ; ReleaseDC (hwnd, hdc) ; return 0 : Zauważ, że włšczyłem pole tmExternalLeading do obliczania cyChar. Nawet jeœli to pole w czcionce systemowej jest równe 0, powinno być uwzględnione. Jeœli kiedykolwiek będzie niezerowe, umożliwi tworzenie bardziej czytelnych odstę- pów. Każda następna linia tekstu jest wyœwietlana o cyChar pikseli niżej. Często będziesz musiał także wyœwietlać sformatowane liczby i proste napisy. Jak już mówiłem w rozdziale 2, nie możesz użyć w tym celu tradycyjnego narzę- dzia (czyli popularnej funkcji printţ, ale możesz sięgnšć po sprintf lub jej win- dowsowš wersję - wsprintf. Te funkcje działajš dokładnie tak samo jak printf, z wy- jštkiem tego, że zachowujš sformatowany tekst w buforze. Możesz następnie użyć TextOut, aby wyœwietlić napis na ekranie. Wartoœciš zwróconš przez sprintf lub wsprintf jest dhxgoœć łańcucha znaków. Możesz przekazać tę wartoœć do TextDut jako argument iLength. Następujšcy kod pokazuje typowš kombinację wsprintf i TextOut: int iLength ; TCHAR szBuffer [40] ; [ inne linie programu ] iLength = wsprintf (szBuffer, TEXT ("Suma %i i %i wynosi %i"), iA. iB. iA + iB) : TextOut (hdc, x, y, szBuffer, iLength) ; W tak prostym przykładzie możemy obyć się bez definicji iLength i połšczyć te dwie instrukcje w jednš: TextOut (hdc, x, y, szBuffer, wsprintf (szBuffer, TEXT ("Suma %i i %i wynosi %i"), iA, iB, iA + iB)) ; Nie wyglšda to zbyt ładnie, ale działa. Składanie wszystkiego w całoœć Teraz mamy już wszystko co potrzeba, by napisać prosty program wyœwietlajšcy wiele linii tekstu na ekranie. Wiemy, jak uzyskać uchwyt kontekstu urzšdzenia podczas komunikatu WM_PAINT, jak użyć funkcji TextOut i jak rozmieœcić tekst na podstawie wymiarów pojedynczego znaku. Pozostaje nam jedynie wyœwietlić coœinteresujšcego. W poprzednim rozdziale widzieliœmy ciekawš informację udostępnianš przez funkcję GetSystemMetrics. Funkcja zwraca dane o różnych elementach graficznych w Windows, takich jak ikony, kursory, paski tytuhx i paski przewijania. Ich wy- miary zmieniajš się wraz ze zmianš karty graficznej i jej sterownika. GetSystem- Metrics jest tš ważnš funkcjš, która umożliwia twojemu programowi wyœwietla- nie niezależne od urzšdzenia. Rozdział 4: Wyœwietlanie tekstu 81 Funkcja wymaga pojedynczego argumentu nazywanego indeksem. Indeks jest jed- nym z 75 identyfikatorów całkowitych, zdefiniowanych w pliku nagłówkowym Windows. (Liczba identyfikatorów wzrastała z każdš wersjš Windows; dokumen- tacja programisty Windows 1.0 wymieniała ich tylko 26). GetSystemMetrics zwraca liczbę całkowitš, zwykle rozmiar elementu okreœlonego w argumencie. Napiszemy program, który wyœwietla informacje udostępniane przez wywoła- nie GetSystemMetrics w prostym formacie jednego elementu w linii. Ułatwimy sobie pracę, jeœli utworzymy plik nagłówkowy, który zdefiniuje tablicę struktur zawie- rajšcš zarówno identyfikatory pliku nagłówkowego Windows dla indeksów Get- SystemMetrics, jak też i tekst, który chcemy wyœwietlać dla każdej zwracanej war- toœci. Ten plik nagłówkowy o nazwie SYSMETS.H jest pokazany na rysunku 4-4. SYSMETS.H /* SYSMETS.H - Struktura wyœwietlajšca informacje o elementach graficznych Windows */ define NUMLINES ((int) (sizeof sysmetrics / sizeof sysmetrics C07)) struct ( int iIndex : TCHAR * szLabel TCHAR * szDesc : l sysmetrics [] = ( SM CXSCREEN, TEXT ("SM CXSCREEN"), TEXT ("Screen width in pixels"), SM_CYSCREEN, TEXT ("SM CYSCREEN"), TEXT ("Screen height in pixels"), SM_CXVSCROLL, TEXT ("SM CXVSCROLL"), TEXT ("Vertical scroll width"), SM_CYHSCROLL, TEXT ("SM_CYHSCROLL"), TEXT ("Horizontal scroll height"), SM_CYCAPTION, TEXT ("SM CYCAPTION"). TEXT ("Caption bar height"), SM_CXBORDER, TEXT ("SM_CXBORDER"), TEXT ("Window border width"), SM_CYBORDER, TEXT ("SM_CYBORDER"), TEXT ("Window border height"), SM_CXFIXEDFRAME, TEXT ("SM_CXFIXEDFRAME"), TEXT ("Dialog window frame width"), SM_CYFIXEDFRAME, TEXT ("SM_CYFIXEDFRAME"), TEXT ("Dialog window frame height"), SM_CYVTHUMB, TEXT ("SM CYVTHUMB"), TEXT ("Vertical scroll thumb height"), SM_CXHTHUMB, TEXT ("SM_CXHTHUMB"), TEXT ("Horizontal scroll thumb width"), SM CXICON, TEXT ("SM CXICON"), TEXT ("Icon width"), SM CYICON, TEXT ("SM CYICON"). 82 Częœć I: Podstawy (cišg dalszy ze strony 81) TEXT ("Icon height"), SM CXCURSOR, TEXT ("SM CXCURSOR"), TEXT ("Cursor width"), SM CYCURSOR, TEXT ("SM CYCURSOR"), TEXT ("Cursor height"), SM CYMENU, TEXT ("SM CYMENU"), TEXT ("Menu b-ar height"), SM CXFULLSCREEN, TEXT ("SM_CXFULLSCREEN"), TEXT ("Full screen client area width"), SM CYFULLSCREEN, TEXT ("SM_CYFULLSCREEN"), T^XT ("Full screen client area height"), SM CYKANJIWINDOW, TEXT ("SM CYKANJIWINDOW"), TEXT ("Kanji window height"), SM MOUSEPRESENT, TEXT ("SM MOUSEPRESENT"), TEXT ("Mouse present flag"), SM CYVSCROLL, TEXT ("SM CYVSCROLL"), TEXT ("Vertical scroll arrow hei9ht"), SM CXHSCROLL, TEXT ("SM CXHSCROLL"), TEXT ("Horizontal scroll arrow width"), SM DEBUG, TEXT ("SM_DEBUG"), TEXT ("Debug version flag"), SMţSWAPBUTTON. TEXT ("SM SWAPBUTTON"), TEXT ("Mouse buttons swapped fla9"), SM CXMIN, TEXT ("SM_CXMIN"), TEXT ("Minimum window width"), SM CYMIN, TEXT ("SM_CYMIN"), TEXT ("Minimum window height"), SM CXSIZE, TEXT ("SM_CXSIZE"), TEXT ("Min/Max/Close button width"), SM CYSIZE, TEXT ("SM CYSIZE"), TEXT ("Min/Max/Close button height"), SM CXSIZEFRAME, TEXT ("SM_CXSIZEFRAME"), TEXT ("Window sizing frame width"), SM CYSIZEFRAME, TEXT ("SM_CYSIZEFRAME"), TEXT ("Window sizing frame height"), SMţCXMINTRACK, TEXT ("SM_CXMINTRACK"), TEXT ("Minimum window tracking width"), SMţCYMINTRACK, TEXT ("SM_CYMINTRACK"), TEXT ("Minimum window tracking height"), SM CXDOUBLECLK, TEXT ("SM_CXDOUBLECLK"), TEXT ("Double click x tolerance"), SM CYDOUBLECLK, TEXT ("SM_CYDOUBLECLK"), TEXT ("Double click y tolerance"), SM CXICONSPACING, TEXT ("SM_CXICONSPACING"), TEXT ("Horizontal icon spacing"), SMţCYICONSPACING, TEXT ("SM CYICONSPACING"), TEXT ("Vertical icon spacing"), SM MENUDROPALIGNMENT, TEXT ("SM_MENUDROPALIGNMENT"), TEXT ("Left or right menu drop"), SM PENWINDOWS, TEXT ("SMţPENWINDOWS"), TEXT ("Pen extensions installed"), SM DBCSENABLED, TEXT ("SM_DBCSENABLED"), TEXT ("Double-Byte Char Set enabled"), SMţCMOUSEBUTTONS, TEXT ("SM_CMOUSEBUTTONS"), TEXT ("Number of mouse buttons"), SMţSECURE, TEXT ("SMţSECURE"), Rozdział 4: Wyœwietlanie tekstu 83 TEXT ("Security present flag"), SM CXEDGE, TEXT ("SM_CXEDGE"), TEXT ("3-D border width"), SM CYEDGE, TEXT ("SM_CYEDGE"), TEXT ("3-D border height"), SM_CXMINSPACING, TEXT ("SM_CXMINSPACING"), TEXT ("Minimized window spacing width"), SM_CYMINSPACING, TEXT ("SM_CYMINSPACING"), TEXT ("Minimized window spacing height"), SM CXSMICON, TEXT ("SM_CXSMICON"), TEXT ("Small icon width"), SM CYSMICON, TEXT ("SM_CYSMICON"), TEXT ("Small icon height"), SM CYSMCAPTION, TEXT ("SM_CYSMCAPTION"), TEXT ("Small caption height"), SM CXSMSIZE, TEXT ("SM_CXSMSIZE"), TEXT ("Small caption button width"), SM CYSMSIZE, TEXT ("SM_CYSMSIZE"), TEXT ("Small caption button height"), SMţCXMENUSIZE, TEXT ("SMţCXMENUSIZE"), TEXT ("Menu bar button width"), SMţCYMENUSIZE, TEXT ("SM CYMENUSIZE"), TEXT ("Menu bar button height"), SM ŽRRANGE, TEXT ("SM ARRANGE"), TEXT ("How minimized windows arranged"), SM CXMINIMIZED, TEXT ("SM_CXMINIMIZED"), TEXT ("Minimized window width"), SM_CYMINIMIZED, TEXT ("SMţCYMINIMIZED"), TEXT ("Minimized window height"), SM CXMAXTRACK, TEXT ("SM_CXMAXTRACK"), TEXT ("Maximum draggable width"), SM_CYMAXTRACK, TEXT ("SM_CYMAXTRACK"), TEXT ("Maximum draggable height"), SM CXMAXIMIZED, TEXT ("SM_CXMAXIMIZED"), TEXT ("Width of maximized window"), SM CYMAXIMIZED, TEXT ("SM CYMAXIMIZED"), TEXT ("Height of maximized window"), SM_NETWORK, TEXT ("SMţNETWORK"), TEXT ("Network present flag"), SM CLEANBOOT, TEXT ("SM CLEANBOOT"), TEXT ("How system was booted"), SM CXDRAG, TEXT ("SM_CXDRAG"), TEXT ("Avoid drag x tolerance"), SM CYDRAG, TEXT ("SM_CYDRAG"), TEXT ("Avoid drag y tolerance"), SM_SHOWSOUNDS, TEXT ("SMţSHOWSOUNDS"), TEXT ("Present sounds visually"), SM_CXMENUCHECK, TEXT ("SM CXMENUCHECK"), TEXT ("Menu check-mark width"), SM CYMENUCHECK, TEXT ("SM CYMENUCHECK"), TEXT ("Menu check-mark height"), SM_SLOWMACHINE, TEXT ("SM_SLOWMACHINE"), TEXT ("Slow processor flag"), SM_MIDEASTENABLED, TEXT ("SM_MIDEASTENABLED"), TEXT ("Hebrew and Arabic enabled flag"), SM_MOUSEWHEELPRESENT, TEXT ("SM MOUSEWHEELPRESENT"), TEXT ("Mouse wheel present flag"), SM XVIRTUALSCREEN, TEXT ("SMţXVIRTUALSCREEN"), 84 Częœć I: Podstawy (cišg dalszy ze strony 83) TEXT ("Virtual screen x origin"), SM_YVIRTUALSCREEN, TEXT ("SM YVIRTUALSCREEN"), TEXT ("Virtual screen y origin"), SM_CXVIRTUALSCREEN, TEXT ("SM_CXVIRTUALSCREEN"), TEXT ("Virtual screen width"), SM_CYVIRTUALSCREEN, TEXT ("SM_CYVIRTUALSCREEN"), TEXT ("Virtual screen height"), SM_CMONITORS, TEXT ("SM_CMONITORS"), TEXT ("Number of monitors"), SM_SAMEDISPLAYFORMAT, TEXT ("SMţSAMEDISPLAYFORMAT"), TEXT ("Same color format flag") Rysunek 4-4. Plik SYSMETS.H Program, który wyœwietla tę informację, nazywa się SYSMETSl. Plik kodu Ÿró- dłowego SYSMETSl.C pokazany jest na rysunku 4-5. Większoœć kodu powinna wyglšdać teraz znajomo. Kod WinMain jest rzeczywiœcie identyczny z kodem w HELLOWIN, a sporo kodu z WndProc zostało już omówione. SYSMETSI.C /* SYSMETSl.C - Program nr 1 wyœwietlajšcy wymiary elementów graficznych (c) Charles Petzold, 1998 */ ţţinclude iţinclude "sysmets.h" LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) static TCHAR szAppNameC7 = TEXT ("SysMetsl") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CS VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION> ; wndclass.hCursor = LoadCursor (NULL, IDC ŽRROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITEţBRUSH) ; wndclass.lpszMenuName = NULL ; j wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) ( Rozdział 4: Wyœwietlanie tekstu 85 MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MBţICONERROR) ; return 0 ; ) hwnd = CreateWindow (szAppName, TEXT ("Get System Merics No. 1"), WS_OVERLAPPEDWINDOW, ! CWţUSEDEFAULT, CWţUSEDEFAULT, CWţUSEDEFAULT, CW USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) ( TranslateMessage (&msg) ; DispatchMessage (&msg) ; ) i return msg.wParam ; 1 LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) static int cxChar, cxCaps, cyChar ; HDC hdc ; int i ; PAINTSTRUCT ps ; TCHAR szBuffer C10] ; TEXTMETRIC tm ; switch (message) case WM_CREATE: hdc = GetDC (hwnd) ; I GetTextMetrics (hdc, &tm) ; cxChar = tm.tmAveCharWidth ; cxCaps = (tm.tmPitchAndFamily & 1 ? 3 : 2) * cxChar / 2 ; cyChar = tm.tmHeight + tm.tmExternalLeading ; ReleaseDC (hwnd, hdc) ; return 0 ; case WM_PAINT : hdc = BeginPaint (hwnd, &ps) ; for (i = 0 ; i < NUMLINES ; i++) ( TextOut (hdc, 0, cyChar * i, sysmetrics[i].szLabel, lstrlen (sysmetricsCi].szLabel)) ; ( TextOut (hdc, 22 * cxCaps, cyChar * i, sysmetrics[i].szDesc, lstrlen (sysmetrics[i].szDesc)) ; SetTextAlign (hdc, TAţRIGHT ţ TŽ TOP) ; 86 Częœć I: Podstawy i (cišg dalszy ze strony 85) TextOut (hdc, 22 * cxCaps + 40 * cxChar, cyChar * i, szBuffer, wsprintf (szBuffer, TEXT ("ţ5d"), GetSystemMetrics (sysmetricsCi].iIndex))) ; SetTextAlign (hdc, TA LEFT ţ TA TOP) ; ) EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY : PostOuitMessage (0) ; return 0 ; 1 return DefWindowProc (hwnd, message, wParam, lParam) ; ) Rysunek 4-5. Plik SYSMETSl.C IZysunek 4-6 pokazuje SYSMETS1 działajšcy na standardowej karcie VGA. Jak wyru- ka z pierwszych dwóch linii obszaru roboczego programu, szerokoœć ekranu wynosi 640 pikseli, a wysokoœć - 480 pikseli. Te dwie wartoœci, jak też wiele innych pokazy- wanych przez program, mogš być różne dla różnych rozdzielczoœci ekranu. CXSCREEN Screen width in pixels 640 CYSCREEN Screen height in pixels 400 CXVSCROLL Vertical scroll width 16 CYHSCROLL Horizontal scroll height 16 CYCAPTION Caption bar height 19 CXBORDER Windowborderwidth 1 CYBORDER Window border height 1 CXFIXEDFRAME Dialog window frame width 3 CYFIXEDFRAME Dialog window frame helght 3 CYVTHUMB Vertical scrall thumb height 16 CXHTHUMB Harizontal scroll thumb width 16 CXICON Icon width 32 CYICON Icon height 32 CXCURSOR Cursorwidih 32 CYCURSOR Cursorheight 32 CYMENU Menu bar height 19 CXFULLSCREEN Full screen client area width 640 CYFULLSCREEN Full screen client area height 433 CYKANJIWINDOW Kanji window height 0 MOUSEPRESENT Mousepresentflag 1 CWSCROLL Vertical sttoll arrow height 16 CXHSCROLL Harizontal saoll arrowwidth 16 DEBUG Debug version flag 0 SWAPBUTTON Mouse bţttons swapped flag 0 CXMIN Minimumwindowwidth 112 CYMIN Minimum window height 27 Rysunek 4-6. Ekran programu SYSMETS1 Procedura okna w SYSMETSl.C W programie SYSMETSl.C procedura okna WndProc przetwarza trzy komunikaty: WM CREATE, WM-PAINT i WM DESTROY. Komunikat WM DESTROY jest prze- twarzany tak samo, jak w programie HELLOWIN z rozdziału 3. Rozdział 4: Wyœwietlanie tekstu 87 Komunikat WM CREATE jest pierwszym komunikatem, który otrzymuje proce- dura okna. Windows generuje ten komunikat, gdy funkcja CreateWindow tworzy okno. Podczas przetwarzania komunikatu WMţCREATE program SYSMETSI uzyskuje kontekst urzšdzenia dla okna przez wywołanie GetDC i otrzymuje roz- miary tekstu dla domyœlnej czcionki systemowej przez wywołanie GetTextMetrics. SYSMETS1 zachowuje œredniš szerokoœć znaku w cxChar, a całkowitš wysokoœć znaku (włšcznie z odstępem między wierszami) - w cyChar. SYSMETS1 zachowuje też œredniš szerokoœć wielkich liter w zmiennej statycznej cxCaps. Dla czcionki nieproporcjonalnej cxCaps powinno być równe cxChar. Dla czcionki proporcjonalnej cxCaps równa się 150% wartoœci cxChar. Mniej znaczšcy bit pola tmPitchAndFamily w strukturze TEXTMETRIC ma wartoœć 1 dla czcionki proporcjonalnej i 0 dla czcionki nieproporcjonalnej. SYSMETS1 używa tego bitu do obliczania cxCaps na podstawie cxChar: cxCaps = (tm.tmPitchAndFamily & 1 ? 3 : 2) * cxChar / 2 ; SYSMETS1 maluje całe okno podczas przetwarzania komunikatu WM PAINT. Jak zwykle procedura okna najpierw uzyskuje uchwyt do kontekstu urzšdzenia przez wywołanie BeginPaint. Pętla instrukcji for przeglšda wszystkie linie struk- tury sysmetrics zdefiniowanej w SYSMETS.H. W wyniku trzech wywołań funkcji TextOut sš wyœwietlane trzy kolumny tekstu. W każdym przypadku trzeci argu- ment TextOut (czyli pozycja poczštkowa y) jest ustawiany jako cyChar * i Ten argument okreœla w pikselach położenie górnej krawędzi napisu względem górnej krawędzi obszaru roboczego. Pierwsza instrukcja TextOut wyœwietla wielkimi literami identyfikator w pierw- szej z trzech kolumn. Drugi argument TextOut przyjmuje wartoœć 0, aby rozpo- czšć wyœwietlanie tekstu od lewej krawędzi obszaru roboczego. Tekst jest uzy- skiwany z pola szLabel struktury sysmetrics. Do obliczenia dhzgoœci napisu wyko- rzystujemy funkcję lstrlen. Otrzymanš wartoœć wstawiamy jako ostatni argument TextOut. Druga instrukcja TextOut wyœwietla opisy wartoœci danych systemowych. Te opisy pochodzš z pola szDesc struktury sysmetrics. W tym przypadku drugi argument TextOut jest ustawiany na: 22 * cxCaps Najdłuższy identyfikator wyœwietlany w pierwszej kolumnie ma 20 znaków, więc druga kolumna musi rozpoczynać się przynajmniej 20 x cxCaps na prawo od po- czštku pierwszej kolumny tekstu. Ja stosuję 22, aby dodać pewien odstęp mię- dzy kolumnami. Trzecia instrukcja TextOut wyœwietla wartoœć liczbowš otrzymanš od funkcji Get- SystemMetrics. Czcionka proporcjonalna utrudnia formatowanie kolumn liczb wyrównanych do prawej strony. Na szczęœcie we wszystkich używanych obec- nie czcionkach proporcjonalnych cyfry od 0 do 9 majš tę samš szerokoœć. W prze- ciwnym razie wyœwietlanie kolumn liczbowych byłoby koszmarem. Jednak sze- rokoœć cyfr jest większa niż szerokoœć odstępu. Liczby mogš mieć jednš lub wię- cej cyfr, więc różne liczby mogš rozpoczynać się w różnych miejscach na osi po- ziomej. T'ţ' gg Częœć I: Podstawy Czyż nie byłoby proœciej, gdybyœmy mogli wyœwietlać kolumnę liczb wyrówna- nych do prawej strony, okreœlajšc położenie końca liczb, zamiast ich poczštku? Pozwala nam na to funkcja SetTextAlign. Po wywołaniu przez SYSMETSI SetTextAlign (hdc, TAţRIGHT ţ TŽ TOP) ; Windows powinien interpretować współrzędne przekazywane do kolejnych funk- cji TextOut jako prawy górny róg tekstu, a nie lewy górny. Funkcja TextOut do wyœwietlania kolumny liczb ma swój drugi argument usta- wiony na: 22 * cxCaps + 40 * cxChar Wartoœć 40 x cxChar uwzględnia szerokoœć drugiej i trzeciej kolumny. Po funkcji TextOut kolejne wywołanie SetTextAlign przywraca normalne pozycjonowanie dla następnego przejœcia pętli. Brak miejsca Z programem SYSMETSI jest jeden niewiellei kłopot: dopóki nie masz olbrzymiego ekranu o wysokiej rozdzielczoœci, nie zobaczysz wielu linii z listy danych syste- mowych. Jeœli zawęzisz okno, możesz nawet nie zobaczyć wartoœci. SYSMETSI nie jest œwiadomy tego problemu. W przeciwnym razie mógłby wy- œwietlać okno komunikatu mówišce "Przepraszam!". Program nie wie, jak duży jest jego obszar roboczy. Rozpoczyna wyœwietlanie na górze okna i liczy na to, że Windows obetnie wszystko, co wyjdzie poza dolnš krawędŸ obszaru roboczego. Oczywiœcie, to nie tak powinno działać. Naszym pierwszym zadaniem jest więc okreœlenie, jaka częœć wyœwietlanego tekstu zmieœci się w obszarze roboczym. Wielkoœć obszaru roboczego Jeœli poeksperymentujesz z istniejšcymi aplikacjami Windows, stwierdzisz, że rozmiary okna mogš się zmieniać w dużym zakresie. Jeœli okno jest maksymali- zowane, obszar roboczy zajmuje prawie cały ekran. Rozmiary maksymalnego obszaru roboczego sš w rzeczywistoœci dostępne przez wywołanie GetSystemMe- trics z argumentami SM CXFULLSCREEN i SM CYFULLSCREEN (zakładajšc, że okno ma tylko pasek tytułu i żadnego menu). Minimalny rozmiar okna może być tak niewielki, że obszar roboczy przestanie istnieć. W poprzednim rozdziale używaliœmy funkcji GetClientftect do okreœlenia rozmiaru obszaru roboczego. Nie ma w tym nic złego, tyle tylko że wywoływanie jej za każdym razem, gdy potrzebna ci jest ta informacja, jest nieefektywne. Znacznie lepszš metodš okreœlenia rozmiarów okna roboczego jest przetwarzanie komu- nikatu WMţSIZE wewnštrz własnej procedury okna. Windows wysyła komuni- kat WMţSIZE do procedury okna za każdym razem, gdy rozmiar okna się zmie- nia. Zmienna lParam, przekazywana do procedury okna, w mniej znaczšcym sło- wie zawiera szerokoœć obszaru roboczego, zaœ w bardziej znaczšcym słowie -jego wysokoœć. Aby zachować te rozmiary, musisz zdefiniować w swojej procedurze okna dwie zmienne statyczne: static int cxClient, cyClient : Rozdział 4: Wyœwietlanie tekstu 89 Podobnie jak cxChar i cyChar, te zmienne sš zdefiniowane jako statyczne, ponie- waż sš ustawiane podczas przetwarzania jednego komunikatu i używane pod- czas przetwarzania innego. Możesz wykorzystać sposób WM SIZE, jak tu: case WM_SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; return 0 ; Możesz ujrzeć taki kod praktycznie w każdym programie Windows. LOWORD i HIWORD sš makrami zdefiniowanymi w pliku nagłówkowym WINDEF.H Win- dows. Jeœli jesteœ ciekawy, to definicje tych makr wyglšdajš następujšco: define LOWORD(1) ((WORD)(1)) define HIWORD(1) ((WORD)(((DWORD)(1) Ż 16) & OxFFFF)) Obydwa makra zwracajš wartoœci WORD - to znaczy 16 bitowe liczby całkowite bez znaku (ang. unsigned short integer) - w zakresie od 0 do OxFFFF. Najczęœciej będziesz zapamiętywał te wartoœci w postaci 32-bitowych liczb całkowitych ze znakiem. Nie powoduje to żadnych problemów z konwersjš, a ułatwia użycie takich wartoœci w póŸniejszych obliczeniach. W wielu programach Windows po WM SIZE może następować komunikat WM-PAINT. Dlaczego? Ponieważ definiujšc klasę okna okreœlamy styl klasy jako CS HREDRAW ţ CS VREDRAW Ten styl klasy mówi, żeby Windows wymuszał odœwieżanie, jeœli zmieni się po- ziomy albo pionowy rozmiar okna. Możesz obliczyć liczbę pełnych linii tekstu możliwych do wyœwietlenia wewnštrz obszaru roboczego za pomocš wzoru: cyClient / cyChar Możesz otrzymać 0, jeœli wysokoœć obszaru roboczego jest zbyt mała, by wyœwie- tlić pełny znak. Podobnie, przybliżona liczba małych liter możliwych do wyœwie- tlenia w poziomie, wewnštrz obszaru roboczego, jest równa cxClient / cxChar Jeœli okreœlisz cxChar i cyChar podczas komunikatu WM CREATE, nie martw się o dzielenie przez 0 w tych obliczeniach. Twoja procedura okna otrzymuje komu- nikat WMţCREATE, kiedy WinMain wywołuje CreateWindow. Pierwszy komuni- kat WM SIZE przychodzi trochę póŸniej, kiedy WinMain wywołuje ShowWindow, a w tym momencie cxChar i cyChar majš już przypisane wartoœci dodatnie, różne od zera. Poznanie rozmiarów obszaru roboczego okna jest pierwszym krokiem do rozwiš- zania problemu przemieszczania tekstu w takim obszarze roboczym, który nie jest dostatecznie duży, by zmieœcić wszystko. Jeœli znasz inne aplikacje Windows, które majš podobne wymagania, prawdopodobnie już wiesz, czego potrzebuje- my: to tu jest miejsce na cudowny wynalazek - paski przewijania. Paski przewijania Paski przewijania to jedna z najlepszych własnoœci graficznego interfejsu użyt- kownika. Sš one łatwe w użyciu i umożliwiajš doskonałe wizualne sprzężenie Częœć I: Podstawy zwrotne. Możesz użyć paska przewijania przy wyœwietlaniu wszystkiego - tek- stu, grafiki, arkusza kalkulacyjnego, rekordów bazy danych, obrazków, stron WWW - co wymaga więcej miejsca, niż jest dostępne w obszarze roboczym okna. Paski przewijania sš umieszczane pionowo (do przesuwania w górę i w dół) lub poziomo (do przesuwania w lewo i w prawo). Możesz klikać myszš strzałki na obu końcach paska przewijania lub obszar pomiędzy strzałkami. Suwak wędru- je wzdłuż paska przewijania, wskazujšc przybliżone położenie wyœwietlanej częœci materiału w stosunku do całego dokumentu. Możesz także przecišgnšć suwak do okreœlonego położenia za pomocš myszy. Rysunek 4-7 pokazuje zalecany spo- sób korzystania z pionowego paska przewijania przy wyœwietlaniu tekstu. Kliknij tu, by przewinšć jednš linię do góry (zawartoœć okna przesuwa się w dól) Przesuń suwak, by znaleŸć się mniej więcej tam, gdzie chcesz Kliknij tu, by przewinšć jednš linię w dól (zawartoœć okna przesuwa się do góry) Rysunek 4-7. Pionowy pasek przewijania Programiœci majš czasem problemy z terminologiš przewijania, ponieważ ich spojrzenie jest różne od spojrzenia użytkownika. Użytkownik, który przewija w dół, chce zobaczyć dalszš (niższš) częœć dokumentu; jednak w rzeczywistoœci program podnosi dokument w stosunku do okna widocznego na ekranie. Doku- mentacja Windows i identyfikatory w pliku nagłówkowym sš oparte na spojrze- niu użytkownika: przewijanie w górę oznacza poruszanie się ku poczštkowi do- kumentu; przewijanie w dół oznacza poruszanie się ku końcowi dokumentu. Bardzo łatwo jest dołšczyć poziomy albo pionowy pasek przewijania do okna aplikacji. Musisz tylko wstawić identyfikator stylu okna (WS) paska pionowego WS VSCROLL lub paska poziomego WS HSCROLL, lub oba, w trzecim argu- mencie CreateWindow. Paski przewijania okreœlone w funkcji CreateWindow sš umieszczane zawsze po prawej stronie lub na dole obszaru roboczego okna i zaj- mujš całš długoœć lub szerokoœć tego obszaru. Obszar roboczy nie obejmuje pa- ska przewijania. Szerokoœć pionowego paska przewijania i wysokaœć poziornego paska przewijania sš stałe dla konkretnego sterownika graficznego i rozdzielczo- œci ekranu. Jeœli potrzebujesz tych wartoœci, możesz je uzyskać (jak mogłeœ za- uważyć) wywołujšc GetSystemMetrics. Rozdział 4: Wyœwietlanie tekstu 91 Windows wykonuje przetwarzanie wszystkich komunikatów myszy dotyczšcych pasków przewijania. Jednak paski przewijania nie zawierajš interfejsu klawiatu- ry. jeœli chcesz za pomocš klawiszy ze strzałkami powielić niektóre funkcje pa- sków przewijania, musisz sam napisać odpowiedni kod (zrobimy to w kolejnej wersji programu SYSMETS, w dalszych rozdziałach). Zakres i pozycja paska przewijania Z każdym paskiem przewijania sš zwišzane "zakres" i "pozycja". Zakres paska przewijania to para liczb całkowitych przedstawiajšcych minimalnš i maksymalnš wartoœć zwišzanš z paskiem przewijania. Pozycja to położenie suwaka w tym zakresie. Gdy suwak jest na górze (lub na lewym skraju) paska przewijania, po- zycja suwaka odpowiada minimalnej wartoœci zakresu. Gdy suwak jest na dole (lub na prawym skraju), pozycja suwaka odpowiada maksymalnej wartoœci za- kresu. Domyœlny zakres paska przewijania wynosi od 0 (góra lub lewy skraj) do 100 (dół lub prawy skraj), ale łatwo jest zmienić taki zakres na coœ bardziej dogodne- go dla programu: SetScrollRange (hwnd, iBar, iMin, iMax, bRedraw) : Argument iBar przyjmuje wartoœć SB VERT albo SB HORZ. Parametry iMin i iMax wyznaczajš nowš minimalnš i maksymalnš wartoœć zakresu. Możesz ustawić bRedraw na wartoœć TRUE, jeœli chcesz, by Windows przerysował pasek przewi- jania na podstawie nowego zakresu. (Jeœli po wywołaniu SetScrolLRange będziesz chciał wywołać inne funkcje wpływajšce na wyglšd paska przewijania, prawdo- podobnie będziesz chciał ustawić bRedraw na FALSE, aby uniknšć niepotrzebne- go odœwieżania). Położenie suwaka zawsze okreœla liczba całkowita. Na przykład pasek przewija- nia z zakresem od 0 do 4 ma pięć położeń suwaka, jak pokazuje to rysunek 4-8. Pozycja 0 Pozycja 1 Pozycja 2 Pozycja 3 Porycja 4 Rysunek 4-8. Paski przewijania z pięcioma położeniami suwaka Pozycja 0 Pozycja 1 Pozycja 2 Pozycja 3 Pozycţa a 92 Częœć I: Podstawy Możesz użyć SetScrolIPos do ustawienia nowego położenia suwaka na pasku prze- wijania: SetScrollPos (hwnd, iBar, iPos, bRedraw) ; Argument iPos jest nowym położeniem i musi mieœcić się między iMin i iMax. Windows dostarcza podobnych funkcji (GetScrollftange i GetScroIlPos), umożliwia- jšcych uzyskanie aktualnego zakresu i pozycji paska przewijania. Kiedy używasz w swoim programie pasków przewijania, dzielisz z Windows odpowiedzialnoœć za ich obsługę i aktualizację pozycji ich suwaków. Windows odpowiada za następujšce funkcje pasków przewijania: ů obsługę komunikatów myszy przeznaczonš dla pasków przewijania, ů sygnalizację reakcji paska przewijania na kliknięcie myszš, co objawia się migotaniem z odwróceniem kolorów, ů przemieszczanie suwaka w miarę jak użytkownik przecišga go w obrębie paska przewijania, ů wysyłanie komunikatów paska przewijania do odpowiedniej procedury okna. Twój program odpowiada za następujšce funkcje: ů inicjację zakresu i pozycji paska przewijania, ů przetwarzanie komunikatów paska przewijania skierowanych do procedury okna, ů aktualizację pozycji suwaka paska przewijania, ů zmianę zawartoœci obszaru roboczego w odpowiedzi na zmiany na pasku przewijania. Najlepiej będzie, jak przyjrzymy się teraz fragmentowi kodu. Komunikaty paska przewijania Windows wysyła procedurze okna komunikaty WM VSCROLL (pionowe prze- wijanie) i WMţHSCROLL (poziome przewijanie), kiedy klikamy myszš pasek przewijania albo przecišgamy suwak. Każde działanie myszy na pasku przewi- jania generuje co najmniej dwa komunikaty: pierwszy, gdy przycisk myszy zo- stał wciœnięty, i drugi, gdy został zwolniony. Jak wszystkim komunikatom, również WM VSCROLL i WMţHSCROLL towa- rzyszš parametry komunikatu wParam i IParam. W komunikatach generowanych przez paski przewijania utworzone jako częœć twojego okna możesz pominšć parametr IParam. Jest on używany tylko dla pasków przewijania tworzonych jako okna potomne, zwykle w oknach dialogowych. Parametr komunikatu wParam można podzielić na bardziej i mniej znaczšce sło- wo. Mniej znaczšce słowo wParam jest liczbš, która wskazuje, co mysz robi z pa- skiem przewijania. Ta liczba jest nazywana kodem powiadomienia (ang. notifica- tion code). Kody powiadomienia majš wartoœci zdefiniowane przez identyfikato- ry zaczynajšce się od przedrostka SB (ang. scroll bar - pasek przewijania). Oto kody powiadomienia zdefiniowane w WINLlSER.H: Rozdział 4: Wyœwietlanie tekstu 93 ţţdefine SB_LINEUP 0 ţţdefine SB_LINELEFT 0 ţldefine SB_LINEDOWN 1 Ildefine SB_LINERIGHT 1 Ildefine SB_PAGEUP 2 ţldefine SB_PAGELEFT 2 Ildefine SB_PAGEDOWN 3 ţţdefine SB_PAGERIGHT 3 ţţdefine SB_THUMBPOSITION 4 ţţdefine SB_THUMBTRACK 5 ţIdefine SB_TOP 6 lldefine SB_LEFT 6 Ildefine SB_BOTTOM 7 ţldefine SB_RIGHT 7 Ildefine SBţENDSCROLL 8 Możesz używać identyfikatorów zawierajšcych słowa LEFT i RIGHT dla poziomych pasków przewijania i identyfikatorów ze słowami UP, DOWN, TOP i BOTTOM dla pionowych pasków przewijania. Kody powiadomienia powišzane z klikaniem myszš różnych obszarów paska przewijania pokazane sš na rysunku 4-9. Wciœnięty: SB LINEUP lub SB LINELEFT Zwolniony: SB ENDSCROLL Wciœnięty: SB PAGEUP lub SB PAGELEFT Zwolniony: SB ENDSCROLL Wciœnięty: SB THUMBTRACK - Zwolniony: SB THUMBPOSITION Wciœnięty: SB PAGEDOWN lub SB PAGERIGHT Zwolniony: SB ENDSCROLL Wciœnięty: SB LINEDOWN lub SB LINERIGHT Zwolniony: SB ENDSCROLL ţ""'ţ~~ ł ... Rysunek 4-9. Identyfikatory wartoœci wParam komunikatów paska przewijania Jeœli przytrzymasz naciœnięty przycisk myszy na różnych częœciach paska prze- wijania, twój program może otrzymać wiele komunikatów. Kiedy przycisk my- szy zostanie zwolniony, otrzymasz komunikat z kodem powiadomienia SB END- SCROLL. Zasadniczo możesz ignorować komurukaty z tym kodem. Windows nie zmienia pozycji suwaka na pasku przewijania. Robi to twoja aplikacja, wywołu- jšc SetScrolIPos. Jeœli umieœcisz kursor myszy nad suwakiem paska przewijania i naciœniesz przy- cisk myszy, możesz przesuwać suwak. Generuje to komunikaty paska przewija- nia z kodami powiadomienia SB THUMBTRACK i SB THUMBPOSITION. Jeœli 94 Częœć I: Podstawy mniej znaczšcym słowem wParam jest SB THUMBTRACK, to bardziej znaczšce słowo wParam rejestruje bieżšcš pozycję przecišganego suwaka paska przewija- nia. Ta pozycja znajduje się pomiędzy minimalnš i maksymalnš wartoœciš zakre- su paska przewijania. Kiedy mniej znaczšcym słowem wParam jest SB THUMB- POSITION, wtedy bardziej znaczšce słowo wParam zawiera końcowš pozycję suwaka paska przewijania po zwolnieniu przycisku myszy. Przy innych działa- niach paska przewijania bardziej znaczšce słowo wParam należy pominšć. Gdy przecišgasz myszš suwak paska przewijania i twój program otrzyma ko- munikat SB THUMBTRACK, Windows w celu zapewnienia sprzężenia zwrot- nego przemieœci odpowiednio suwak. Jeœli jednak nie przetworzysz komunika- tów SB THUMBTRACK albo SB THUMBPOSITION wywołujšc SetScrollPos, su- wak powróci do swojej poprzedniej pozycji, gdy tylko zwolnisz przycisk myszy. Program może przetwarzać którykolwiek z komunikatów: SB T'HUMBTRACK albo SB THUMBPOSITION, ale zwykle nie przetwarza obydwóch. Jeœli przetwarzasz komunikat Sá THUMBTRACK, powinieneœ przesuwać zawartoœć swojego obsza- ru roboczego wtedy, gdy użytkownik przecišga suwak. Jeœli w zamian przetwa- rzasz komunikat SB THUMBPOSITION, będziesz przesuwał zawartoœć obszaru roboczego tylko wtedy, gdy użytkownik zatrzyma przecišganie suwaka. Bardziej pożšdane (ale trudniejsze) jest przetwarzanie komunikatu SB THUMBTRACK; dla pewnych typów danych twój program może mieć zbyt mało czasu na obsługę tych komunikatów. Jak mogłeœ zauważyć, plik nagłówkowy WINUSER.H zawiera kody powiado- mienia SB TOP, SB BOTTOM, SB LEFT i SB RIGHT, wskazujšce, że pasek prze- wijania został przemieszczony do swojej minimalnej lub maksymalnej pozycji. Jednak nigdy nie otrzymasz tych kodów powiadomienia dla paska przewijania utworzonego jako częœć twojego okna aplikacji. Chociaż nie zdarza się to często, to jednak możliwe jest używanie wartoœci 32-bito- wych dla zakresu paska przewijania. Bardziej znaczšce słowo wParam, które jest tylko 16-bitowš wartoœciš, nie może właœciwie wskazywać pozycji dla działań SB THUMBTRACK i SB THUMBPOSTITON. W tym przypadku musisz użyć funk- cji GetScrolllnfo (opisanej w dalszej częœci rozdziahx), by uzyskać tę informację. Przewijanie SYSMETS Doœć objaœnień. Nadszedł czas na praktyczne zastosowanie nabytych wiadomo- œci. Zaczniemy od czegoœ łatwego, a potrzebnego - od przewijania pionowego. Przewijanie poziome może zaczekać. Kod programu SYSMETS2 jest pokazany na rysunku 4-10. Prawdopodobnie jest on najprostszš realizacjš paska przewija- nia, jakš chciałbyœ mieć w aplikacji. SYSMETS2.C /* SYSMETS2.C - Program nr 2 wyœwietlajdcy wymiary elementów graficznych (c) Charles Petzold, 1998 */ Rozdział 4: Wyœwietlanie tekstu 95 ţţinclude ţţinclude "sysmets.h" LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) static TCHAR szAppName[] = TEXT ("SysMets2") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CS VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; ' wndclass.hCursor = LoadCursor (NULL, IDCţARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITEţBRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MBţICONERROR) ; ) hwnd = CreateWindow (szAppName, TEXT ("Get System Metrics No. 2"), WS_OVERLAPPEDWINDOW ţ WSţVSCROLL, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CWţUSEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) TranslateMessage (&msg) ; DispatchMessage (&msg) ; ) return msg.wParam ; LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ( static int cxChar, cxCaps, cyChar, cyClient, iVscrollPos ; HDC hdc ; int i, y ; PAINTSTRUCT ps ; TCHAR szBuffer[10] ; TEXTMETRIC tm ; switch (message) gs Częœć I: Podstawy (cišg dalszy ze strony 95) t ' case WM_CREATE: hdc = GetDC (hwnd) ; GetTextMetrics (hdc, &tm) ; cxChar = tm.tmAveCharWidth ; cxCaps = (tm.tmPitchAndFamily & 1 ? 3 : 2) * cxChar / 2 ; cyChar = tm.tmHeight + tm.tmExternalLeadin9 ; ReleaseDC (hwnd, hdc) ; SetScrollRange (hwnd, SB VERT, 0, NUMLINES - 1, FALSE> ; SetScrollPos (hwnd, SB VERT, iVscrollPos, TRUE) ; return 0 ; case WM_SIZE: cyClient = HIWORD (lParam) ; return 0 ; case WM VSCROLL: switch (LOWORD (wParam)) ( case SBţLINEUP: iVscrollPos -= 1 ; break ; case SB_LINEDOWN: iVscrollPos += 1 ; break ; case SB_PAGEUP: iVscrollPos -= cyClient / cyChar ; break ; case SBţPAGEDOWN: iVscrollPos += cyClient / cyChar ; break ; case SB THUMBPOSITION: iVscrollPos = HIWORD (wParam) ; break ; default : break ; 1 iV.scrollPos = max (0, min (iVscrollPos, NUMLINES - 1)) ; if (iVscrollPos != GetScrollPos (hwnd. SB VERT)) ( SetScrollPos (hwnd, SB_VERT, iVscrollPos, TRUE) ; InvalidateRect (hwnd, NULL, TRUE) ; ) return 0 ; case WMţPAINT: Rozdział 4: Wyœwietlanie tekstu hdc = BeginPaint (hwnd, &ps) ; for (i = 0 ; i < NUMLINES ; i++) i y = cyChar * (i - iVscrollPos) TextOut (hdc, 0, y, sysmetricsCi].szLabel, lstrlen (sysmetrics[i].szLabel)) : TextOut (hdc, 22 * cxCaps, y, sysmetricsCi].szDesc, lstrlen (sysmetricsCi].szDesc)) ; SetTextAlign (hdc, TAţRIGHT ţ TA TOP) ; TextOut (hdc, 22 * cxCaps + 40 * cxChar, y, szBuffer, wsprintf (szBuffer, TEXT ("%5d"), GetSystemMetrics (sysmetrics[i].iIndex))) ; SetTextAlign (hdc. TAţLEFT ţ TŽ TOP) ; ) EndPaint (hwnd, &ps) ; return 0 ; case WMţDESTROY: PostOuitMessage (0) ; return 0 ; 1 return DefWindowProc (hwnd, message, wParam, lParam) ; Rysunek 4ů10. Program SYSMETS2 Nowe wywołanie CreateWindow dodaje do okna pionowy pasek przewijania przez włšczenie WS VSCROLL w trzecim argumencie stylu okna : WS OVERLAPPEDWINDOW ţ WS USCROLL Komunikat WMţCREATE przetwarzany w procedurze okna WndProc ma dwie dodatkowe linie okreœlajšce zakres i poczštkowš pozycję pionowego paska prze- wijania: SetScrollRange (hwnd, SB_VERT, 0, NUMLINES - 1, FALSE) ; SetScrollPos (hwnd, SBţVERT, iVscrollPos, TRUE) ; Tablica struktury sysmetrics ma NUMLINES linii tekstu, więc zakres paska prze- wijania jest równy przedżiałowi od 0 do NUMLINES - 1. Każda pozycja paska przewijania odpowiada linii tekstu wyœwietlanej na górze obszaru roboczego. Jeœli suwak paska przewijania jest w pozycji 0, na górze obszaru roboczego jest umiesz- czana pierwsza linia. Dla pozycji większych od zera wyœwietlane sš tam inne li- nie. Kiedy pozycjš jest NUMLINES - 1, na górze obszaru roboczego wyœwietlana jest ostatnia linia tekstu. Aby pomóc w przetwarzaniu komunikatu 4VMţVSCROLL, wewnštrz procedury okna została zdefiniowana zmienna statyczna o nazwie iVscroIlPos. Zawiera ona bieżšcš pozycję suwaka paska przewijania. W przypadku komunikatów SB LI- NEUP i SB LINEDOWN, wystarczy jedynie zmienić pozycję przewijania o 1. W przypadku SB PAGEUP i SB PAGEDOWN musimy przemieœcić tekst o za- s 98 Częœć I: Podstawy wartoœć jednego ekranu lub o cyClient podzielone przez cyChar linii. Dla komu- nikatu SB THUMBPOSITION nowš pozycjš suwaka jest bardziej znaczšce sło- wo wParam. Komunikaty SB ENDSCROLL i SB THUMBTRACK sš pomijane. Gdy program oblicza nowš wartoœć iVscrollPos na podstawie typu otrzymanego komunikatu WMţVSCROLL, stosujšc makra min i max upewnia się, że znajduje się ona cišgle pomiędzy wartoœciš minimalnš i maksymalnš zakresu paska prze- wijania. Następnie program porównuje wartoœć iVscrollPos z poprzednim poło- żeniem, uzyskanym przez wywołanie GetScroIlPos. Jeœli zmieniła się pozycja prze- wijania, jest ona aktualizowana przez wywołanie SetScroIlPos, natomiast wywo- łanie InvalidateRect unieważnia całe okno. Funkcja InvalidateRect generuje komunikat WMţPAINT. Kiedy oryginalny pro- gram SYSMETS1 przetwarzał komunikat WMţPAINT, współrzędna y każdej li- n była obliczana następujšco: cyChar W SYSMETS2 wzór został zmodyfikowany: cyChar * (i - iVscrollPos) Pętla cišgle wyœwietla NUMLINES linii tekstu, ale dla wartoœci iVscrollPos róż- nych od zera ta wartoœć jest ujemna. W rzeczywistoœci program wyœwietla wcze- œniejsze linie tekstu powyżej i poza obszarem roboczym. Oczywiœcie Windows nie pozwala tym liniom ukazać się na ekranie, więc wszystko wyglšda ładnie i przyjemnie. Mówiłem, że zaczniemy od prostego przykładu. Jednak ten kod jest mało sku- teczny. Poprawimy go niebawem, ale najpierw omówimy odœwieżanie obszaru roboczego po przetworzeniu komunikatu WM VSCROLL. Dostosowanie programu do malowania Procedura okna w SYSMETS2 nie odœwieża bezpoœrednio obszaru roboczego po przetworzeniu komunikatu paska przewijania. W zamian wywołuje funkcję In- validateRect, unieważniajšc obszar roboczy. Na skutek tego Windows umieszcza komunikat 4VMţPAINT w kolejce komunikatów. Najlepiej, jeœli program windowsowy wykonuje całe malowanie obszaru robo- czego w odpowiedzi na komunikat WMţPAINT. Ponieważ program po otrzy- maniu komunikatu WMţPAINT powinien być zawsze zdolny do odœwieżania całego obszaru roboczego okna, malowanie w odpowiedzi na inny komunikat prawdopodobnie spowoduje włšczenie kodu, będšcego kopiš funkcjonalnš WM PAINT. Możesz buntować się, słyszšc takie stwierdzenia. Zapewne wydaje ci się, że pro- ponuję okrężnš drogę. W poczštkach Windows programiœci z trudem przyjmo- wali ten poglšd, tak różny od programowania w trybie znakowym. Jak wspo- mniałem wczeœniej, istnieje wiele sytuacji, gdy twój program musi zareagować na klawiaturę lub mysz, rysujšc coœ natychmiast. Jest to niewštpliwie wygodne i skuteczne, ale w wielu przypadkach po prostu niepotrzebne. Jeœli opanujesz sztu- kę gromadzenia wszystkich informacji potrzebnych do malowania w odpowie- dzi na komunikat WMţPAINT, będziesz zadowolony z wyników. Rozdział 4: Wyœwietlanie tekstu 99 Jak pokazuje SYSMETS2, przy przetwarzaniu innych komunikatów niż WMţPA- INT program często okreœla, że musi ponownie namalować konkretny obszar ekranu. Pomaga wtedy InvalidateRect. Możesz używać tej funkcji do unieważnia- nia okreœlonego prostokšta obszaru roboczego lub całego obszaru roboczego. W niektórych aplikacjach proste zaznaczenie obszarów okna jako unieważnionych w celu generacji komunikatów WM PAINT może nie dawać zadowalajšcych rezul- tatów. Gdy wywohxjesz InvalidateRect, Windows umieszcza komunikat WMţPAINT w kolejce komunikatów i procedura okna ostatecznie go przetworzy. Jednak Win- dows traktuje WM PAINT jako komunikat o niskim priorytecie, więc jeœli w syste- mie dzieje się dużo innych rzeczy, może chwilę potrwać, zanim twoja procedura okna otrzyma komunikat WM PAIN'T. Każdy z nas widział na ekranie puste białe "dziu- ry" pojawiajšce się po zamknięciu okna dialogowego, wtedy gdy program czekał na odœwieżenie swojego okna. Jeœli chcesz natychmiast aktualizować unieważniony obszar, możesz bezpoœred- nio po InvalidateRect wywołać funkcję UpdateWindow: UpdateWindow (hwnd) ; Jeœli jakaœ częœć obszaru roboczego została unieważniona, UpdateWindow powo- duje natychmiastowe wywołanie procedury okna z komunikatem WM PAINT. (UpdateWindow nie wywoła procedury okna, jeœli cały obszar roboczy jest zatwier- dzony). W tym przypadku komunikat WM PAINT ominie kolejkę komunikatów. Procedura okna zostanie wywołana bezpoœrednio przez Windows. Kiedy proce- dura okna zakończy odœwieżanie, funkcja UpdateWindow zwróci sterowanie do kodu, który jš wywołał. Zauważ, że UpdateWindow jest funkcjš używanš także przez WinMain do genero- wania pierwszego komunikatu WM PAINT. Kiedy okno jest tworzone pierwszy raz, cały obszar roboczy jest unieważniony. UpdateWindow wywołuje procedurę okna, aby namalowała obszar roboczy. Poprawianie przewijania SYSMETS2 działa dobrze, ale jest mało efektywny i nie zasługuje na naœladowa- nie w innych programach. Wkrótce przedstawię nowš wersję, która koryguje jego braki. Być może najbardziej interesujšce jest to, że nowa wersja nie używa żadnej z czterech omawianych dotychczas funkcji paska przewijania. W zamian używa nowych funkcji, właœciwych tylko dla Win32 API. F'unkcje informacyjne pasków przewijania Dokumentacja paska przewijania (w /Platform SDK/Liser Interface Serices/Controls/ Scroll Bars) utrzymuje, że funkcje SetScroIlRange, SetScrollPos, GetScroIlRange i Get- ScrollPos sš przestarzałe. Nie jest to do końca prawdš. Chociaż te funkcje pojawi- ły się już w Windows 1.0, zostały przystosowane do obsługi 32-bitowych argu- mentów Win32 API. Cišgle doskonale działajš i prawdopodobnie pozostanš w użyciu. Ponadto sš one na tyle proste, że nie przestraszš poczštkujšcego pro- gramisty. Dlatego cišgle odwołuję się do nich w tej ksišżce. w 100 Częœć I: Podstawy W Win32 API wprowadzono dwie funkcje pasków przewijania, o nazwach Set- ScrolIlnfo i GetScrolllnfo. Zastępujš one całkowicie poprzednie funkcje i dodajš dwie nowe, ważne cechy. Pierwsza cecha dotyczy rozmiaru suwaka paska przewijania. Jak mogłeœ zauwa- żyć, rozmiar suwaka w programie SYSMETS2 był stały. Jednak w niektórych apli- kacjach Windows rozmiar suwaka jest proporcjonalny do iloœci dokumentu wy- œwietlanego w oknie. Ta wyœwietlana iloœć jest znana jako "wielkoœć strony". Można jš okreœlić arytmetycznie: Wielkoœć suwaka ţ Wielkoœć strony ţ Iloœć wyœwietlanego dokumentu Długoœć przewijania Zakres Całkowita wielkoœć dokumentu Możesz użyć SetScrolllnfo do ustawienia wielkoœci strony (i stšd wielkoœci suwa- ka), jak zobaczymy to niebawem w programie SYSMETS3. Funkcja GetScrolllnfo dodaje drugš ważnš cechę lub raczej koryguje brak w aktu- alnym API. Załóżmy, że chcesz używać zakresu, który ma 65536 lub więcej jed- nostek. Dawniej, w czasach 16-bitowych Windows, nie było to możliwe. Oczywi- œcie w Win32 funkcje sš zdefiniowane jako przyjmujšce 32-bitowe argumenty i tak naprawdę działajš. (Pamiętaj, że jeœli używasz tak wielkiego zakresu, liczba rzeczywistych fizycznych pozycji suwaka jest jeszcze ograniczona przez rozmiar paska przewijania podawany w pikselach). Jednak gdy otrzymasz komunikat WM VSCROLL albo WM HSCROLL z kodem powiadomienia SB THUMB- TRACK albo SB THUMBPOSITION, dostaniesz tylko 16 bitów wskazujšcych aktualnš pozycję suwaka. Funkcja GetScrołllnfo pozwala otrzymywać rzeczywi- œcie wartoœć 32-bitowš. Składnia funkcji SetScrolllnfo i GetScrolllnfo jest następujšca: SetScrollInfo (hwnd, iBar, &si, bRedraw) ; GetScrollInfo (hwnd, iBar, &si) ; Argumentem iBar jest SB VERT albo SB HORZ, jak w innych funkcjach paska prze- wijania. Może nim być także SB CTL dla kontroli paska przewijania. Ostatriim argu- mentem SetScrolllnfo może być TRUE lub FALSE, zależnie od tego, czy chcesz, żeby Windows przerysowywał pasek przewijania uwzględniajšc nowš informację. Trzecim argumentem obu funkcji jest struktura SCROLLINFO, zdefiniowana tak: typedef struct tagSCROLLINFO ( UINT cbSize ; // ustaw na sizeof (SCROLLINFO) UINT fMask ; // wartoœć ţustawiana lub otrzymywana int nMin ; // minimalna wartoœć zakresu int nMax ; // maksymalna wartoœć zakresu UINT nPage ; // wielkoœć strony int nPos ; // aktualne położenie int nTrackPos ; // aktualne położenie œledzenia ) SCROLLINFO, * PSCROLLINFO ; W swoim programie możesz zdefiniować strukturę typu SCROLLINFO w taki sposób: Rozdział 4: Wyœwietlanie tekstu 101 SCROLLINFO si ; Przed wywołaniem SetScrolllnfo lub GetScrolllnfo musisz wpisać do pola cbSize wielkoœć struktury: si.cbSize = sizeof (si) ; albo si.cbSize = sizeof (SCROLLINFO) ; Gdy poznasz lepiej Windows, znajdziesz wiele innych struktur, których pierw- sze pole wskazuje w ten sposób rozmiar struktury. To pole pozwoli przyszłym wersjom Windows rozszerzyć strukturę i dodać nowe własnoœci, a mimo to po- zostać zgodnym z wczeœniej kompilowanymi programami. Ustaw pole fMask na jeden lub więcej znaczników zaczynajšcych się przedrost- kiem SIF. Możesz łšczyć te znaczniki funkcjš OR ( I ) z C (alternatywy bitowej). Kiedy z funkcjš SetScrollInfo będziesz używał znacznika SIF RANGE, musisz ustawić pola nMin i nMax na odpowiedni zakres paska przewijania. Kiedy bę- dziesz używał tego znacznika z funkcjš GetScrolllnfo, przy powrocie z wywoła- nia funkcji pola nMin i nMax będš zawierały aktualny zakres. Znacznik SIF POS działa podobnie. Kiedy jest używany z funkcjš SetScrolllnfo, musisz ustawić pole nPos struktury na odpowiednie położenie. Używajšc znacz- nika SIF POS z GetScrolllnfo, otrzymasz aktualne położenie. Znacznik SIF PAGE pozwala ustawić i otrzymać wielkoœć strony. Ustaw nPage na właœciwš wielkoœć strony przy wywołaniu funkcji SetScrolllnfo. GetScrolllnfo ze znacz- nikiem SIF PAGE pozwala uzyskać aktualnš wielkoœć strony. Nie używaj tego znacz- nika, jeœli nie chcesz proporcjonalnego suwaka paska przewijania. Używaj znacznika SIF TRACKPOS tylko z GetScrolllnfo podczas przetwarzania ko- munikatów WM VSCROLL albo WM HSCROLL z kodem powiadomienia SB_THUMBTRACK albo SB THUMBPOSTTION. Po powrocie z tej funkcji pole nTrackPos struktury SCROLLINFO wskazuje aktualne 32-bitowe położenie suwaka. Używaj znacznika SIF DISABLENOSCROLL tylko razem z funkcjš SetScrolllnfo. Jeœli ten znacznik jest okreœlony, a nowe argumenty paska przewijania powinny uczynić go niewidocznym, to pasek przewijania zostan,ie wyłšczony. (Wyjaœnię to niebawem). Znacznik SIF ALL jest połšczeniem znaczników SIF RANGE, SIF POS, SIF PA- GE i SIF_TRACKPOS. Ułatwia ustawianie argumentów paska przewijania pod- czas komunikatu WMţSIZE. (Znacznik SIF TRACKPOS jest ignorowany, jeœli zo- stanie okreœlony w funkcji SetScrolllnfo). Jest także bardzo wygodny przy prze- twarzaniu komunikatu paska przewijania. Jak przewijać minimalnie? W SYSMETS2 zakres przewijania jest ustawiony na wartoœć minimalnš 0 i mak- symalnš NUMLINES - 1. Kiedy pasek przewijania jest w położeniu 0, pierwsza linia informacji znajduje się na górze obszaru roboczego; kiedy pasek przewija- nia jest w położeniu NUMLINES - 1, ostatnia linia znajduje się na górze obszaru roboczego, a inne linie sš niewidoczne. 102 Częœć I: Podstawy Możesz powiedzieć, że SYSMETS2 przewija zbyt daleko. Wystarczyłoby, żeby prze- wijał tylko do momentu, gdy ostatnia linia informacji ukaże się na dole obszaru ro- boczego, a nie na górze. Możemy zrobić kilka zmian w SYSMETS2, aby to osišgnšć. Zamiast ustawiać zakres paska przewijania podczas przetwarzania komunikatu WM CREATE, możemy zaczekać, aż otrzymamy komunikat WM SIZE: iVscrollMax = max (0, NUMLINES - cyClient / cyChar) ; SetScrollRange (hwnd, SB VERT, 0, iVscrollMax, TRUE) ; Załóżmy, że NUMLINES równa się 75, a dla konkretnego okna wartoœć cyClient dzielona przez cyChar równa się 50. Inaczej mówišc, mamy 75 linii informacji, ale tylko 50 możemy wyœwietlić w obszarze roboczym. Stosujšc pokazane powy- żej dwie linie kodu, zakres jest ustawiany na minimum równe 0 i maksimum równe 25. Kiedy położenie paska przewijania będzie równe 0, program wyœwietli linie od 0 do 49. Kiedy położenie paska przewijania będzie równe 1, program wyœwietli linie od 1 do 50; a kiedy położenie paska przewijania będzie równe 25 (maksy- malne), program wyœwietli linie od 25 do 74. Oczywiœcie musimy wprowadzić też zmiany w innych częœciach programu, ale jest to całkowicie wykonalne. Jednš z przyjemnych cech nowych funkcji paska przewijania jest to, że jeœli uży- wasz rozmiaru strony paska przewijania, wiele z powyższych działań jest wyko- nywane za ciebie. Używajšc struktury SCROLLINFO i funkcji SetScrolllnfo, mo- żesz wykorzystać następujšcy kod: si.cbSize = sizeof (SCROLLINFO) ; si.cbMask = SIF RANGE ţ SIFţPAGE ; si.nMin = 0 ; si.nMax = NUMLINES - 1 ; si.nPage = cyClient / cyChar ; SetScrollInfo (hwnd, SBţVERT, &si, TRUE) ; Kiedy to zrobisz, Windows ograniczy maksymalne położenie paska przewijania nie do si.nMax, ale do si.nMax - si.nPage + 1. Powtórzmy wczeœniejsze założenia: NUMLINES równa się 75 (więc si.nMax równa się 74), a si.nPage równa się 50. Oznacza to, że maksymalne położenie paska przewijania jest ograniczone do 74 - 50 + 1, czyli 25. To dokładnie to, czego chcemy. Co się stanie, jeœli rozmiar strony będzie tak duży jak zakres paska przewijania, czyli jeœli w naszym przykładzie nPage wynosi 75 albo więcej? Windows zwykle ukrywa pasek przewijania, ponieważ nie jest już potrzebny. Jeœli nie chcesz, by pasek przewijania był ukrywany, to podczas wywoływania SetScrolllnfo użyj SIF DISABLENOSCROLL .Wtedy Windows tylko wyłšczy pasek przewijania, za- miast go ukryć. Nowy SYSMETS SYSMETS3 - końcowa wersja naszego programu SYSMETS z tego rozdziału - widnieje na rysunku 4-11. Ta wersja używa funkcji SetScrolllnfo i GetScrolllnfo, dodaje poziomy pasek przewijania w lewo i w prawo i bardziej efektywnie od- œwieża obszar roboczy. Rozdział 4: Wyœwietlanie tekstu 103 SYSMETS3.C , /* SYSMETS3.C - Program nr 3 wyœwietlajšcy wymiary elementów graficznych (c) Charles Petzold, 1998 */ 4ţinclude ţţinclude "sysmets.h" LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) i static TCHAR szAppNameC] = TEXT ("SysMets3") ; HWND hwnd ; j MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CS VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDCţARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITEţBRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) MessageBox (NULL, TEXT ("Program requires Windows NT!"), szAppName, MBţICONERROR) ; return 0 ; 1 hwnd = CreateWindow (szAppName, TEXT ("Get System Metrics No. 3"), WS OVERLAPPEDWINDOW ţ WS_VSCROLL ţ WS-HSCROLL, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CWţUSEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) TranslateMessage (&msg) ; DispatchMessage (&msg) ; I return msg.wParam ; l LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) 104 Częœć I: Podstawy (cišg dalszy ze strony 103) static int cxChar, cxCaps, cyChar, cxClient, cyClient, iMaxWidth ; HDC hdc ; int i, x, y, iVertPos, iHorzPos, iPaintBeg, iPaintEnd ; PAINTSTRUCT ps ; SCROLLINFO si ; TCHAR szBuffer[10] ; TEXTMETRIC tm ; switch (message) ( case WM_CREATE: hdc = GetDC (hwnd) ; GetTextMetrics (hdc, &tm) ; cxChar = tm.tmAveCharWidth ; cxCaps = (tm.tmPitchAndFamily & 1 ? 3 : 2) * cxChar / 2 ; cyChar = tm.tmHeight + tm.tmExternalLeading ; ReleaseDC (hwnd, hdc) ; // Zapamiętaj szerokoœć trzech kolumn iMaxWidth = 40 * cxChar + 22 * cxCaps ; return 0 ; case WM_SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; // Ustaw zakres pionowego paska przewijania i rozmiar strony si.cbSize = sizeof (si) ; si.fMask = SIFţRANGE ţ SIFţPAGE ; si.nMin = 0 ; si.nMax = NUMLINES - 1 ; si.nPage = cyClient / cyChar ; SetScrollInfo (hwnd, SB VERT, &si, TRUE) ; // Ustaw zakres poziomego paska przewijania i rozmiar strony si.cbSize = sizeof (si) ; si.fMask = SIFţRANGE ţ SIFţPAGE ; si.nMin = 0 ; si.nMax = 2 + iMaxWidth / cxChar ; si.nPage = cxClient / cxChar ; SetScrollInfo (hwnd, SB HORZ, &si, TRUE) ; return 0 : case WM VSCROLL: // Pobierz informację o pionowym pasku przewijania si.cbSize = sizeof (si) ; si.fMask = SIF_ALL ; GetScrollInfo (hwnd, SB VERT, &si) ; Rozdział 4: Wyœwietlanie tekstu 105 // Zapamiętaj polożenie dla póŸniejszych porównań iVertPos = si.nPos ; switch (LOWORD (wParam)) case SB_TOP: si.nPos = si.nMin ; break ; case SB_BOTTOM: si.nPos = si.nMax ; break ; case SB_LINEUP: si.nPos -= 1 ; break ; case SB_LINEDOWN: si.nPos += 1 ; break ; case SBţPAGEUP: si.nPos -= si.nPage ; break ; case SB_PAGEDOWN: si nPos += si.nPage ; break ; case SB_THUMBTRACK: si.nPos = si.nTrackPos ; break ; i default: break ; i // Ustaw polożenie i pobierz je. Na skutek korekty // dokonywanej przez Windows może być różne od ustawianego. si.fMask = SIF_POS ; ( SetScrollInfo (hwnd, SBţVERT, &si, TRUE) ; GetScrollInfo (hwnd, SB VERT, &si) ; // Jeœli polożenie zmienione, przewiń i odœwież okno if (si.nPos != iVertPos) ( ScrollWindow (hwnd, 0, cyChar * (iVertPos - si.nPos), NULL, NULL) ; ! UpdateWindow (hwnd) ; 1 i return 0 ; case WM HSCROLL: // Pobierz informację o poziomym pasku przewijania 106 Częœć I: Podstawy (cišg dalszy ze strony 105) si.cbSize = sizeof (si) ; ' si.fMask = SIF ALL ; // Zapamiętaj położenie dla póŸniejszych porównań GetScrollInfo (hwnd, SB HORZ, &si) ; iHorzPos = si.nPos ; switch (LOWORD (wParam)) ( case SB_LINELEFT: si.nPos -= 1 ; break ; case SB_LINERIGHT: si.nPos += 1 ; break ; case SB_PAGELEFT: si nPos -= si.nPage ; break ; case SB_PAGERIGHT: si.nPos += si.nPage ; break ; case SB_THUMBPOSITION: si.nPos = si.nTrackPos ; break ; default : break ; ) // Ustaw polożenie i pobierz je. Na skutek korekty // dokonywanej przez Windows może być różne od ustawianego. si.fMask = SIF_POS ; SetScrollInfo (hwnd, SB HORZ, &si, TRUE) ; GetScrollInfo (hwnd, SB HORZ, &si) ; // Jeœli polożenie zmienione, przewiń i odœwież okno if (si.nPos != iHorzPos) ( ScrollWindow (hwnd, cxChar * (iHorzPos - si.nPos), 0, NULL, NULL) ; ? return 0 ; case WM_PAINT : hdc = BeginPaint (hwnd, &ps) ; // Pobierz położenie pionowego paska przewijania si.cbSize = sizeof (si) ; Rozdział 4: Wyœwietlanie tekstu 107 si.fMask = SIF_POS ; GetScrollInfo (hwnd, SB VERT, &si) ; iVertPos = si.nPos ; // Pobierz polożenie poziomego paska przewijania GetScrollInfo (hwnd, SB HORZ, &si) ; iHorzPos = si.nPos ; // ZnajdŸ ograniczenia malowania iPaintBeg = max (0, iVertPos + ps.rcPaint.top / cyChar) ; iPaintEnd = min (NUMLINES - 1, iVertPos + ps.rcPaint.bottom / cyChar) ; for (i = iPaintBeg ; i <= iPaintEnd ; i++) ( x = cxChar * (1 - iHorzPos) ; y = cyChar * (i - iVertPos) ; TextOut (hdc, x, y, sysmetricsCi].szLabel, lstrlen (sysmetricsCi].szLabel)) ; TextOut (hdc, x + 22 * cxCaps, y, sysmetricsCi].szDesc, lstrlen (sysmetricsCi].szDesc)) ; SetTextAlign (hdc, TAţRIGHT ţ TŽ TOP) ; TextOut (hdc, x + 22 * cxCaps + 40 * cxChar, y, szBuffer, wsprintf (szBuffer, TEXT ("%5d"), GetSystemMetrics (sysmetrics[i].iIndex))) ; SetTextAlign (hdc, TAţLEFT ţ TŽ TOP) ; EndPaint (hwnd, &ps) ; return 0 ; case WM DESTROY : PostOuitMessage (0) ; return 0 ; ) return DefWindowProc (hwnd, message, wParam, lParam) ; 1 Rysunek 4-11. Program SYSMETS3 Ta wersja programu przekazuje Windows zadanie zapamiętywania informacji paska przewijania i większoœć zadań sprawdzania granic. Na poczštku przetwa- rzania komunikatów WM VSCROLL i WM HSCROLL program otrzymuje całš informację paska przewijania, okreœla położenie na podstawie kodu powiadomie- nia, a następnie ustawia je wywołujšc SetScrolllnfo. Następnie wywołuje funkcję GetScrolllnfo. Jeœli położenie z wywołania SetScrolllnfo było poza zakresem, zo- stało skorygowane przez Windows, a poprawna wartoœć jest zwracana przez funk- cję GetScrolllnfo. 108 Częœć I: Podstawy SYSMETS3 używa funkcji ScrollWindow do przewijania informacji w obszarze roboczym okna, a nie do odœwieżania okna. Chociaż funkcja ta jest raczej złożo- na (w najnowszej wersji Windows została zastšpiona przez jeszcze bardziej zło- żonš funkcję ScroIlWindowEx), SYSMETS3 posługuje się niš w doœć prosty spo- sób. Drugi argument funkcji podaje wielkoœć przewijania obszaru roboczego w po- ziomie (w pikselach), a trzeci argument - wielkoœć przewijania obszaru robocze- go w pionie. Ostatnie dwa argumenty ScrollWindow sš ustawione na NULL. Oznacza to, że cały obszar roboczy ma być przewijany. Windows automatycznie unieważnia prosto- kštny region obszaru roboczego, który został "odkryty" w wyniku przewijania. Powoduje to wygenerowanie komunikatu 4VMţPAINT. Funkcja InvalidateRect nie jest już potrzebna. Zauważ, że ScrolIWindow nie jest funkcjš GDI, ponieważ nie wymaga uchwytu kontekstu urzšdzenia. Jest to jedna z niewielu funkcji Windows nie należšcych do GDI, które zmieniajš wyglšd obszaru roboczego okna. Wyjšt- kowo, ale jednoczeœnie dogodnie, jest udokumentowana razem z funkcjami pa- ska przewijania. WM HSCROLL przetwarza przerwania kodu powiadomienia SB THUMBPO- SITION i ignoruje SB THUMBTRACK. Jeœli więc użytkownik przecišga suwak poziomego paska przewijania, program nie powinien przewijać zawartoœci okna w poziomie, dopóki użytkownik nie zwolni przycisku myszy. Strategia WM VSCROLL jest inna: tu program reaguje na komunikaty SB THUMBTRACK i ignoruje SB THUMBPOSITTON. Program będzie więc prze- wijać zawartoœć ekranu w pionie w odpowiedzi na przecišganie przez użytkow- nika suwaka po pionowym pasku przewijania. Jest to bardzo wygodne, ale uwa- żaj: użytkownicy, którzy wykryjš, że twój program przewija zawartoœć ekranu wraz z ruchem suwaka po pasku przewijania, będš goršczkowo szarpać suwak tam i z powrotem, próbujšc rzucić program na kolana. Na szczęœcie, dzisiejsze szybkie pecety najprawdopodobniej przeżyjš tę próbę wytrzymałoœci. Ale prze- testuj swój kod na wolniejszym komputerze i zastanów się, czy nie wprowadzić argumentu SB SLOWMACHINE w GetSystemMetrics, na wypadek, gdyby pro- gram miał pracować na powolnych maszynach. Jedyny sposób przyspieszenia przetwarzania WM PAINT jest przedstawiony w SYSMETS3: kod WM PAINT decyduje, które linie znajdujš się wewnštrz unie- ważnionego prostokšta i tylko te linie przepisuje. Kod jest oczywiœcie bardziej złożony, ale za to znacznie szybszy. Nie lubię myszy W dawnych czasach większoœć użytkowników Windows nie doceniała korzyœci płynšcych z używania myszy i naprawdę sam Windows (i wiele programów windowsowych) nie wymagał myszy. Chociaż komputery osobiste bez myszy podzieliły los monitorów monochromatycznych i drukarek mozaikowych, cišgle zalecane jest pisanie programów, które pozwalajš powielać działania myszy na klawiaturze. Jest to szczególnie ważne dla tak podstawowych funkcji programu, jak paski przewijania, ponieważ nasze klawiatury dysponujš bogatym zestawem klawiszy nawigacyjnych, które zapewniajš alternatywę dla myszy. ; Rozdział 4: Wyœwietlanie tekstu 109 W dalszych rozdziałach nauczysz się używać klawiatury i dodawać interfejs kla- wiatury do programu. Pewnie zauważyłeœ, że SYSMETS3 przetwarza komuni- kat WM VSCIZOLL, jeœli kod powiadomienia jest równy SB TOP i SB BOTTOM. Wspominałem wczeœniej, że procedura okna nie otrzymuje tych komunikatów od pasków przewijania, więc na razie ten kod jest zbyteczny. Kiedy powrócimy do tego programu w następnych rozdziałach, zrozumiesz powód włšczenia tych działań. Rozdział 5 Po staw r fi ki a Ta częœć systemu Windows, która odpowiada za wyœwietlanie obrazu na moni- torach i drukarkach, jest znana jako GDI. Jak możesz sobie wyobrazić, jest to bar- dzo ważny podsystem Windows. Wykorzystujš go nie tylko aplikacje, ale i sam system do wyœwietlania elementów interfejsu użytkownika, takich jak: menu, paski przewijania, ikony i kursory myszy. Niestety, pełny opis GDI wymaga całej ksišżki. To nie jest ta ksišżka. W tym roz- dziale zapoznam cię z podstawami rysowania linii i wypełnionych obszarów. Wystarczy to, abyœ spokojnie mógł przejœć do następnych rozdziałów, gdzie zo- baczymy, jak GDI umożliwia korzystanie z bitmap, metaplików i sformatowane- go tekstu. Struktura GDI Z perspektywy programisty GDI składa się z setek wywołań funkcji i powišza- nych z nimi typów danych, makr i struktur. Ale zanim zaczniemy przyglšdać się szczegółom tych funkcji, popatrzmy na ogólnš strukturę GDI. Filozofia GDI Grafika w Windows 98 i Windows NT jest obsługiwana głównie przez funkcje eksportowane z biblioteki dynamicznej GDI32.DLL. W Windows 98 biblioteka ta korzysta z 16-bitowej biblioteki GDI.EXE. W Windows NT GDI.EXE jest wyko- rzystywana tylko przez programy 16-bitowe. Biblioteki dynamiczne wywohxjš procedury w sterownikach kart graficznych i w sterownikach zainstalowanych drukarek. Sterownik karty graficznej porozumie- wa się z układami na karcie, a sterownik drukarki konwertuje komendy GDI na kody (lub komendy) rozumiane przez danš drukarkę. Oczywiœcie, różne karty graficzne i drukarki wymagajš różnych sterowników urzšdzeń. Do komputerów kompatybilnych z PC można podłšczyć wiele różnych kart gra- ficznych. Jednym z głównych zadań GDI jest umożliwienie tworzenia grafiki nie- zależnie od konkretnych urzšdzeń. Programy Windows powinny bez problemów działać na różnych graficznych urzšdzeniach wyjœciowych, które majš sterowni- ki do Windows. GDI osišga ten cel poprzez dostarczanie właœciwoœci, które izo- lujš twoje programy od szczegółowych charakterystyk różnych urzšdzeń wyjœcio- wych. Częœć I: Podstawy Œwiat graficznych urzšdzeń wyjœciowych jest podzielony na dwie szerokie gru- py: urzšdzenia rastrowe i urzšdzenia wektorowe. Większoœć urzšdzeń wyjœcio- wych PC to urzšdzenia rastrowe, co oznacza, że przedstawiajš one obraz jako prostokštny wzór z kropek: W tej kategorii mieszczš się karty graficzne, drukar- ki igłowe i laserowe. Urzšdzeniami wektorowymi, które rysjš obraz za pomocš linii, sš dzisiaj prawie wyłšcznie plotery. Tradycyjne oprogramowanie grafiki (na jakie możesz się jeszcze natknšć w star- szych ksišżkach) jest oparte wyłšcznie na wektorach. Oznacza to, że program używajšcy wektorowego systemu graficznego znajduje się na wysokim poziomie abstrakcji w stosunku do sprzętu. Urzšdzenie wyjœciowe używa pikseli do repre- zentacji grafiki, ale program nie porozumiewa się z interfejsem przy użyciu pik- seli. Oczywiœcie możesz używać GDI Windows jako systemu rysowania wekto- rowego wysokiego poziomu, ale możesz także używać go na niskim poziomie abstrakcji i operować pikselami. Pod tym względem GDI ma się tak do tradycyjnych interfejsów graficznych, jak język C do innych języków programowania. C jest dobrze znany z wysokiego poziomu przenoœnoœci do różnych systemów operacyjnych i œrodowisk. Słynie też z tego, że pozwala programistom na używanie niskopoziomowych funkcji systemowych, co jest zazwyczaj niemożliwe w innych językach programowania wysokiego poziomu. Tak jak o C mówi się czasem, że jest "asemblerem wysokie- go poziomu", tak możesz uważać GDI za interfejs wysokiego poziomu dla sprzętu graficznego. Jak już wiesz, domyœlny układ współrzędnych Windows jest oparty na pikselach. Większoœć tradycyjnych języków graficznych używa "wirtualnego" systemu współTzędnych z osiš poziomš i pionowš o wielkoœci (na przykład) od 0 do 32767. Chociaż niektóre języki graficzne nie pozwalajš na używanie współrzędnych pik- selowych, GDI Windows umożliwia użycie dowolnego z tych systemów (jak i kilku innych, opartych na wielkoœciach fizycznych). Możesz używać systemu współ- rzędnych wirtualnych, aby trzymać swój program z dala od sprzętu, lub używać systemu współrzędnych urzšdzenia i działać blisko sprzętu. Niektórzy programiœci myœlš, że jeœli pracujesz z pikselami, to straciłeœ niezależ- noœć sprzętowš. W poprzednim rozdziale zobaczyliœmy już, że nie musi tak być. Rzecz w tym, aby używać pikseli w sposób niezależny od urzšdzenia. W tym celu język interfejsu graficznego musi zapewnić programowi wiedzę o charakte- rystyce sprzętu, aby program mógł wykonać niezbędne modyfikacje. Na przy- kład w programie SYSMETS do rozmieszczenia tekstu na ekranie użyliœmy wy- miarów standardowego znaku w czcionce systemowej wyrażonych w pikselach. Podejœcie to pozwala programom na dopasowanie się do różnych kart graficz- nych, majšcych różne rozdzielczoœci, wielkoœci tekstu i współczynniki kształtu obrazu. W tym rozdziale poznasz także inne metody okreœlenia wielkoœci ekra- nu. Dawniej wielu użytkowników uruchamiało Windows na ekranie monochroma- tycznym. Jeszcze niedawno użytkownicy laptopów ograniczeni byli do odcieni szaroœci. Z tego powodu GDI zostało tak skonstruowane, abyœ mógł pisać pro- gramy bez poœwięcania specjalnej uwagi kolorom - oznacza to, że Windows po- Rozdział 5: Podstawy grafiki 113 trafi zamieniać kolory na odcienie szaroœci. Nawet dzisiejsze karty graficzne uży- wane z Windows 98 majš różne możliwoœci pokazywania kolorów (16 kolorów, 256 kolorów, "high color" lub "true color"). Pomimo że drukarki atramentowe umożliwiły tanie drukowanie w kolorze, wielu użytkowników nadal woli uży- wać czarno-białych drukarek laserowych, dajšcych wydruki wysokiej jakoœci. Można używać tych urzšdzeń nie zdajšc sobie sprawy z ich możliwoœci, ale pro- gram jest w stanie dowiedzieć się, ile kolorów ma dane urzšdzenie wyjœciowe i wykorzystywać wszystkie możliwoœci sprzętu. Oczywiœcie, tak jak mógłbyœ napisać program, który miałby małe problemy z prze- noœnoœciš, gdyby był uruchamiany na innych komputerach, tak mógłbyœ niechcšcy wprowadzić zależnoœć od urzšdzenia w swoim programie windowsowym. Jest to cena, jakš płacisz za niecałkowite odizolowanie od sprzętu. GDI jest, ogólnie mówišc, systemem statycznego wyœwietlania z niewielkim tylko wsparciem ani- macji. Pomimo że możesz przesuwać obiekty graficzne po ekranie, to jeœli masz pisać wyrafinowane animacje dla gier, powinieneœ zagłębić się w Microsoft Di- rectX, który oferuje to, czego potrzebujesz. Wywołania funkcji GDI Kilkaset funkcji, składajšcych się na GDI, można podzielić na kilka obszernych grup: ů Funkcje, które pobierajš (lub tworzš) i zwalniajš (lub niszczš) kontekst urzšdzenia. Jak już mówiłem w poprzednich rozdziałach, musisz mieć uchwyt kontekstu urzšdzenia, aby móc rysować. Funkcje BeginPaint i EndPaint (chociaż sš funk- cjami modułu USER, a nie GDI) pozwalajš rysować podczas komunikatu WM PAINT, a funkcje GetDC i ReleaseDC - podczas innych komunikatów. Niedługo zobaczymy kilka innych funkcji dotyczšcych kontekstów urzšdzeń. ů Funkcje, które pozyskujš informacje o kontekœcie urzšdzenia. W programie SYSMETS z rozdziału 4 użyliœmy funkcji GetTextMetrics, aby otrzymać informacje o roz- miarach czcionki aktualnie wybranej w kontekœcie urzšdzenia. W dalszej czę- œci rozdziału obejrzymy program DEVCAPSl, który pobiera inne, bardziej ogólne informacje o kontekœcie urzšdzenia. ů Funkcje, które coœ rysujš. Teraz, gdy wstęp mamy już za sobš, zajmiemy się naprawdę ważnymi rzeczami. W ostatnim rozdziale użyliœmy funkcji TextOut do wyœwietlenia tekstu w obszarze roboczym okna. Jak zobaczymy, inne funk- cje GDI pozwolš nam rysować linie i obszary wypełnione. W rozdziałach 15 i 16 zobaczymy, jak można rysować obrazy bitmapowe. ů Funkcje, które ustawiajš i pobierajš atrybuty kontekstu urzšdzenia. Atrybut kon- tekstu urzšdzenia okreœla różne szczegóły pracy funkcji rysujšcych. Na przy- kład możesz użyć SetTextColor do okreœlenia koloru każdego tekstu, który wyœwietlasz za pomocš TextOut albo innej funkcji wyjœcia tekstowego. W pro- gramach SYSMETS, w rozdziale 4, używamy SetTextAlign do przekazania GDI, że poczštkowš pozycjš napisu w funkcji TextOut powinna być prawa strona napisu, a nie domyœlna lewa. Wszystkie atrybuty kontekstu urzšdzenia majš domyœlne wartoœci, które sš ustawiane w momencie otrzymywania kontek- stu urzšdzenia. Dla wszystkich funkcji ustawiajšcych (Set) istniejš funkcje Częœć I: Podstawy pobierajšce (Get), które pozwalajš uzyskać aktualne atrybuty kontekstu urzš- dzenia. Funkcje, które pracujš z obiektami GDI. W tym miejscu GDI mamy nieco bałaga- nu. Najpierw przykład: domyœlnie wszystkie linie rysowane z wykorzystaniem GDI sš cišgłe i standardowej gruboœci. Chcesz narysować grubszš linię lub linię składajšcš się z szeregu kropek lub kresek. Gruboœć linii i taki styl linii nie sš atrybutami kontekstu urzšdzenia. Natomiast należš one do charakte- rystyki pióra logicznego. Możesz myœleć o piórze jako o wišzce atrybutów. Możesz utworzyć pióro logiczne, podajšc jego charakterystykę w funkcjach CreatePen, CreatePenlndirect lub ExtCreatePen. Chociaż te funkcje sš traktowa- ne jako częœć GDI, to w odróżnieniu od funkcji GDI nie wymagajš uchwytu kontekstu urzšdzenia. Zwracajš one uchwyt pióra logicznego. Aby użyć tego pióra, "wybierasz" uchwyt pióra w kontekœcie urzšdzenia. Pióro wybrane w kontekœcie urzšdzenia staje się atrybutem kontekstu urzšdzenia. Od tej chwi- li wszystkie linie sš rysowane tym piórem. PóŸnieţ usuwasz pióro z kontekstu urzšdzenia i ruszczysz. Niszczenie pióra jest konieczne, ponieważ definicja pióra zajmuje zaalokowanš przestrzeń pamięci. Oprócz pióra, używasz też obiektów GDI służšcych do tworzenia pędzli wypełniajšcych zamknięte ob- szary, a także obiektów do tworzenia czcionek, bitmap i wielu innych. Podstawowe elementy GDI Rodzaje grafiki, które wyœwietlasz na ekranie lub drukujesz na drukarce, mogš być podzielone na kilka kategorii, nazywanych elementami prostymi (ang. primi- tives): ů Linie i krzywe. Linie stanowiš podstawę każdego systemu grafiki wektorowej. GDI umożliwia korzystanie z linii prostych, prostokštów, elips (włšcznie z podzbiorem elips znanym jako koła), "łuków", które sš częœciš krzywizny na obwodzie elipsy i krzywych sklejanych Beziera. O tym wszystkim powiemy jeszcze w tym rozdziale. Jeœli chcesz rysować różnego rodzaju krzywe, rno- żesz je rysować jako linie łamane, które sš szeregiem bardzo krótkich linii definiujšcych krzywe. GDI rysuje linie, używajšc pióra aktualnie wybranego w kontekœcie urzšdzenia. ů Wypełnione obszary. Jeœli szereg linii albo krzywych zamyka pewien obszar, możesz go wypełnić za pomocš obiektu zwanego pędzlem. Ten pędzel może być jednokolorowy, może to być wzór (szereg linii poziomych, pionowych albo biegnšcych po przekštnej) lub obraz bitmapowy (ang. bitmapped image), po- wielany pionowo lub poziomo we wnętrzu obszaru. ů Bitmapa. Bitmapa to prostokštna tablica bitów odpowiadajšcych pikselom na ekranie. Jest ona podstawowym narzędziem grafiki rastrowej. Bitmapy sš używane głównie do wyœwietlania złożonych (często rzeczywistych) obrazów na ekranie lub drukarce. Służš też do wyœwietlania małych obrazów, które muszš być narysowane bardzo szybko, takich jak ikony, kursory myszy i przy- ciski na pasku narz dzi. GDI umożliwia korzystanie z dwóch rodzajów bit- map - starej (chociaz cišgle użytecznej) bitmapy "zależnej od urzšdzenia", która jest obiektem GDI, i nowszej (poczšwszy od Windows 3.0), bitmapy Rozdział 5: Podstawy grafiki 115 "niezależnej od urzšdzenia" (ang. device independent bitmap, DIB), którš moż- na przechowywać w plikach dyskowych. Bitmapy będę omawiał w rozdzia- łach 14 i 15. Tekst. Tekst nie ma tak dokładnej definicji matematycznej, jak inne elementy grafiki komputerowej; natomiast wywodzi się z kilkusetletniej tradycji typo- grafii, która przez wielu, nie tylko typografów, jest uważana za sztukę. Z tego powodu tekst często stanowi najbardziej złożonš częœć każdego systemu gra- fiki komputerowej, ale też (przyjmujšc za normę umiejętnoœć czytania i pisa- nia) - najważniejszš. Struktury danych używane do definiowania czcionek GDI i do uzyskiwania informacji o czcionce należš do największych w Windows. Poczšwszy od Windows 3.1, GDI umożliwia korzystanie z czcionek TrueTy- pe, które sš tworzone na podstawie wypełnionych konturów i mogš być prze- twarzane innymi funkcjami GDI. W Windows 98 nadal można korzystać ze starszych czcionek, tworzonych na podstawie bitmap, ze względu na potrze- bę zachowania kompatybilnoœci i małe wymagania pamięciowe. Czcionki będę omawiał w rozdziale 17. Pozostałe elementy GDI Pozostałe elementy GDI nie dajš się sklasyfikować tak łatwo. Sš to: ů Tryby odwzorowania i transformacje. Chociaż domyœlnymi jednostkami przy rysowaniu sš piksele, nie musisz się do nich ograniczać. Tryby odwzorowa- nia GDI umożliwiajš rysowanie w calach (a raczej w ułamkach cali), milime- trach lub innych jednostkach. W dodatku Windows NT umożliwia korzysta- nie z tradycyjnej "transformacji œwiata" w postaci macierzy 3x3, która pozwala na deformacje i obroty obiektów graficznych. Ta opcja nie występuje w Win- dows 98. ů Metapliki. Metaplik jest zbiorem poleceń GDI przechowywanych w formie bi- narnej. Metapliki sš głównie używane do przekazywania przez Schowek gra- fiki wektorowej. Metapliki omówię w rozdziale 18. ů Regiony. Region jest złożonym obszarem dowolnej figury, zazwyczaj definio- wanym jako logiczna kombinacja prostszych regionów. Bardziej skompliko- wane regiony mogš być przechowywane przez GDI jako serie linii rastrowych pochodzšcych z oryginalnej definicji regionu. Możesz używać regionów do tworzenia zarysów, wypełniania i obcinania. ů Œcieżki. Œcieżka jest zbiorem prostych i krzywych, przechowywanych we- wnętrznie w GDI. Œcieżki mogš być używane do rysowania, wypełniania i ob- cinania. Œcieżki mogš być także przekształcane w regiony. ů Obcinanie. Rysowanie może być ograniczone do pewnego fragmentu obszaru roboczego. Nazywa się to obcinaniem. Obszar obcinajšcy może być zarówno prostokštny, jak i o dowolnym innym kształcie. Zazwyczaj jest okreœlony przez region lub œcieżkę. ů Palety. Użycie własnych palet jest zazwyczaj potrzebne tylko przy trybach gra- ficznych wyœwietlajšcych 256 kolorów. Windows rezerwuje 20 z tych kolorów na użytek systemu. Możesz zmieniać pozostałe 236 kolorów, aby lepiej wy- 116 Częœć I: Podstawy i œwietlić zdjęcia i obrazy, przechowywane jako bitmapy. Palety omówię w roz- dziale 16. ů Drukowanie. Pomimo że rozdział ten ogranicza się do wyœwietlania, prawie wszystko, czego się tu nauczysz, może być zastosowane przy drukowaniu. Drukowanie omówię w rozdziale 13. Kontekst urzšdzenia Zanim zaczniemy rysować, przyjrzyjmy się kontekstowi urzšdzenia dokładniej, niż zrobiliœmy to w rozdziale 4. Gdy chcesz rysować na graficznym urzšdzeniu wyjœciowym, takim jak ekran lub drukarka, musisz najpierw pobrać uchwyt kontekstu urzšdzenia (ang. device con- text - DC). Dajšc twojemu programowi ten uchwyt, Windows pozwala mu na używanie tego urzšdzenia. Następnie przekazujesz ten uchwyt jako argument funkcji GDI, aby powiedzieć Windows o urzšdzeniu, na którym chcesz rysować. Kontekst urzšdzenia zawiera wiele atrybutów, które okreœlajš sposób współdzia- łania funkcji GDI z urzšdzeniem. Te atrybuty pozwalajš funkcjom GDI na po- bieranie tylko kilku argumentów, takich jak współrzędne poczštkowe. Funk- cje GDI nie wymagajš argumentów dla wszystkich elementów, których Windows potrzebuje do wyœwietlenia obiektu w kontekœcie urzšdzenia. Na przykład gdy wywołujesz TextOut, musisz podać tylko uchwyt kontekstu urzšdzenia, współ- rzędne poczštku, napis i jego długoœć. Nie musisz podawać czcionki, koloru tek- stu, koloru tła za tekstem albo odstępów między znakami. Wszystkie te atrybuty sš częœciš kontekstu urzšdzenia. Gdy chcesz zmienić któryœ z tych atrybutów, musisz wywołać funkcję, która to zrobi. Następne wywołania TextOut w tym kon- tekœcie urzšdzenia będš już posługiwały się nowymi atrybutami. Pobieranie uchwytu kontekstu urzšdzenia Istnieje kilka metod pobierania uchwytu kontekstu urzšdzenia w Windows. Jeœli podczas przetwarzania jakiegoœ komunikatu pobierasz uchwyt kontekstu urzš- dzenia dla ekranu, powinieneœ zwolnić go, zanim opuœcisz procedurę okna. Po zwolnieniu uchwytu nie wolno już z niego korzystać. Dla uchwytu kontekstu urzšdzenia drukarki te zasady nie sš już tak œcisłe. Jak już wspomniałem, druko- wanie omówimy w rozdziale 13. Najbardziej popularnš metodš uzyskania uchwytu kontekstu urzšdzenia i zwol- nienia go jest wywołanie BeginPaint i EndPaint podczas przetwarzania komuni- katu WM PAINT: hdc = BeginPaint (hwnd, &ps) ; Cinne linie programu7 EndPaint (hwnd, &ps) ; Zmienna ps jest strukturš typu PAINTSTRUCT. Pole hdc tej struktury jest tym samym uchwytem kontekstu urzšdzenia, który zwraca BeginPaint. Struktura PAINTSTRUCT zawiera także strukturę RECT (prostokšt), zwanš rcPaint, która definiuje prostokšt obejmujšcy unieważnionš częœć obszaru roboczego okna. Rozdział 5: Podstawy grafiki 117 Używajšc uchwytu kontekstu urzšdzenia, pobranego od BeginPaint, możesz ry- sować tylko w tej częœci. Wywołanie BeginPaint także zatwierdza tę częœć. Programy Windows mogš także pobrać uchwyt kontekstu urzšdzenia podczas przetwarzania innych komunikatów niż 4VMţPAINT: hdc = GetDC (hwnd) ; finne linie programu] ReleaseDC (hwnd, hdc) ; Ten kontekst urzšdzenia odnosi się do obszaru roboczego okna, którego uchwy- tem jest hwnd. Podstawowš różnicš między tymi wywołaniami a użyciem kom- binacji BeginPaint i EndPaint jest to, że używajšc uchwytu zwróconego przez GetDC, możesz rysować na całym obszarze roboczym okna. Jednak GetDC i ReleaseDC nie zatwierdzajš żadnych, być może unieważnionych, częœci obszaru roboczego okna. Program Windows może także pobrać uchwyt kontekstu urzšdzenia, który do- tyczy całego okna, a nie tylko jego obszaru roboczego: hdc = GetWindowDC (hwnd) ; Cinne linie programu] ReleaseDC (hwnd, hdc) ; Ten kontekst urzšdzenia zawiera, oprócz obszaru roboczego okna, także pasek tytułu okna, menu, paski przewijania i ramki. Programy aplikacji rzadko używa- jš funkcji GetWindowDC. jeœli chcesz eksperymentować, powinieneœ przechwycić komunikat WM NCPAINT (ang. nonclient paint), który umożliwia rysowanie poza obszarem roboczym okna. Wywołania BeginPaint, GetDC i GetWindowDC dostarczajš kontekstu urzšdzenia powišzanego z konkretnym oknem wyœwietlonym na ekranie. Dużo bardziej ogólnš funkcjš jest CreateDC: hdc = CreateDC (pszDriver, pszDevice, psz0utput, pData) ; [inne linie programu] DeleteDC (hdc) ; Na przykład możesz uzyskać kontekst urzšdzenia dla całego ekranu przez wy- wołanie hdc = CreateDC (TEXT ("DISPLAY"), NULL, NULL, NULL) ; W zasadzie pisanie poza obszarem swojego okna jest niegrzeczne, ale za to wy- godne przy nietypowych aplikacjach. (Chociaż nie jest to udokumentowane, możesz uzyskać kontekst urzšdzenia dla całego ekranu przez wywołanie GetDC z argumentem NULL). W rozdziale 13 będziemy używać funkcji CreateDC do uzyskiwania uchwytu do kontekstu urzšdzenia drukarki. Czasami potrzebujesz tylko kilku informacji o kontekœcie urzšdzenia i nie wyko- nujesz żadnego rysowania. W tych przypadkach możesz otrzymywać uchwyt kontekstu informacji" wywołujšc funkcję CreatelC. Argumenty sš takie same jak " dla funkcji CreateDC. Na przykład hdc = CreateIC (TEXT ("DISPLAY"), NULL, NULL, NULL) ; Nie możesz pisać do urzšdzenia, używajšc tego uchwytu kontekstu informacji. Kiedy pracujesz z bitmapami, czasami może ci się przydać "kontekst urzšdzenia pamięci": T""ţ 118 Częœć I: Podstawy i hdcMem = CreateCompatibleDC (hdc) ; [inne linie programu] DeleteDC (hdcMem) ; Możesz wybrać bitmapę w kontekœcie urzšdzenia pamięci i użyć funkcji GDI do rysowania na bitmapie. Będę omawiał te techniki w rozdziale 14. Wspominałem wczeœniej, że metaplik jest zbiorem ţ.vywołań funkcji GDI, zako- dowanym w formie dwójkowej. Możesz utworzyć metaplik przez uzyskanie kon- tekstu urzšdzenia metapliku: hdcMeta = CreateMetaFile (pszFilename) [inne linie programul hmf = CloseMetaFile (hdcMeta) ; W czasie, gdy kontekst urzšdzenia metapliku jest ważny, każde wywołanie GDI za pomocš hdcMeta nie będzie wyœwietlone, ale stanie się częœciš metapliku. Kie- dy wywołasz CloseMetaFile, uchwyt kontekstu urzšdzenia stanie się nieważny. Funkcja zwraca uchwyt do metapliku (hmţ. Metapliki będę omawiał w rozdzia- le 18. Pobieranie informacji o kontekœcie urzšdzenia Kontekst urzšdzenia zazwyczaj odnosi się do fizycznego urzšdzenia, takiego jak ekran lub drukarka. Często musisz uzyskać informacje o tym urzšdzeniu, takie jak wielkoœć ekranu (zarówno w pikselach, jak i w jednostkach fizycznych) oraz jego możliwoœci pokazywania kolorów. Możesz pobrać te informacje, wywołujšc funkcję GetDeviceCaps (ang. get device capabilities): iValue = GetDeviceCaps (hdc, iIndex) ; Argument ilndex jest jednym z 29 identyfikatorów, zdefiniowanych w pliku na- główkowym WINGDI.H. Na przykład wartoœć HORZRES, przekazana jako iIn- dex, powoduje, że GetDeviceCaps zwraca szerokoœć urzšdzenia w pikselach; ar- gument VERTRES powoduje zwrot wysokoœci urzšdzenia w pikselach. Jeœli hdc jest uchwytem do kontekstu ekranu, będzie to ta sama informacja, którš zwraca GetSystemMetrics. Jeœli hdc jest uchwytem kontekstu drukarki, GetDeviceCaps zwra- ca wysokoœć i szerokoœć (w pikselach) obszaru dostępnego do drukowania. Możesz także użyć GetDeviceCaps do okreœlenia możliwoœci urzšdzenia w zakre- sie przetwarzania różnych typów grafiki. Zazwyczaj nie jest to ważne przy ob- słudze ekranów, ale nabiera znaczenia podczas pracy z drukarkami. Na przy- kład większoœć ploterów nie potrafi rysować obrazów rastrowych, a GetDevice- Caps może ci to powiedzieć. Program DEVCAPSi Program DEVCAPSl, pokazany na rysunku 5-1, wyœwietla wybrane (ale nie wszystkie) informacje, udostępniane przy użyciu kontekstu ekranu przez funk- cję GetDeviceCaps. Rozdział 5: Podstawy grafiki 119 DEVCAPSl.C /* DEVCAPSl.C - Program nr 1 wyœwietlajdcy możliwoœci urzšdzenia (c) Charles Petzold, 1998 */ ţţinclude ţţdefine NUMLINES ((int) (sizeof devcaps / sizeof devcaps CO])) struct ( int iIndex ; TCHAR * szLabel ; TCHAR * szDesc ; 1 devcaps [] = f HORZSIZE, TEXT ("HORZSIZE"), TEXT ("Width in millimeters:"), VERTSIZE, TEXT ("VERTSIZE"), TEXT ("Height in millimeters:"), HORZRES, TEXT ("HORZRES"), TEXT ("Width in pixels:"), VERTRES, TEXT ("VERTRES"), TEXT ("Height in raster lines:"), BITSPIXEL, TEXT ("BITSPIXEL"), TEXT ("Color bits per pixel:"), PLANES, TEXT ("PLANES"), TEXT ("Number of color planes:"), NUMBRUSHES, TEXT ("NUMBRUSHES"), TEXT ("Number of device brushes:"), NUMPENS, TEXT ("NUMPENS"), TEXT ("Number of device pens:"), NUMMARKERS, TEXT ("NUMMARKERS"), TEXT ("Number of device markers:"). NUMFONTS, TEXT ("NUMFONTS"), TEXT ("Number of device fonts:"), NUMCOLORS, TEXT ("NUMCOLORS"), TEXT ("Number of device colors:"), PDEVICESIZE, TEXT ("PDEVICESIZE"), TEXT ("Size of device structure:"), ASPECTX, TEXT ("ASPECTX"), TEXT ("Relative width of pixel:"), ASPECTY, TEXT ("ASPECTY"), TEXT ("Relative height of pixel:"), ASPECTXY, TEXT ("ASPECTXY"), TEXT ("Relative dia9onal of pixel:"), LOGPIXELSX, TEXT ("LOGPIXELSX"), TEXT ("Horizontal dots per inch:"), LOGPIXELSY, TEXT ("LOGPIXELSY"), TEXT ("Vertical dots per inch:"), SIZEPALETTE, TEXT ("SIZEPALETTE"), TEXT ("Number of palette entries:"), NUMRESERVED, TEXT ("NUMRESERVED"), TEXT ("Reserved palette entries:"), COLORRES, TEXT ("COLORRES"), TEXT ("Actual color resolution:") LRESULT CALLBACK WndProc (HWND, UINT. WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInsta.nce, PSTR szCmdLine, int iCmdShow) static TCHAR szAppNameC] = TEXT ("DevCapsl") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CS VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITEţBRUSH) wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; Tţ 120 Częœć I: Podstawy (cišg dalszy ze strony 119) i if (!RegisterClass (&wndclass)) MessageBox (NULL, TEXT ("This program requires Windows NT!"). szAppName, MBţICONERROR) ; return 0 ; ) ,. hwnd = CreateWindow (szAppName, TEXT ("Device Capabilities"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW USEDEFAULT, CW_USEDEFAULT, CW USEDEFAULT, NULL, NULL, hInstance, NULL) ; ' ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessa9e (&msg, NULL, 0, 0)) TranslateMessage (&msg) ; DispatchMessage (&msg) ; ! return msg.wParam ; , 1 LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) I static int cxChar, cxCaps, cyChar ; TCHAR szBufferClO] ; HDC hdc ; int i ; PAINTSTRUCT ps ; TEXTMETRIC tm ; switch (message) case WM_CREATE: hdc = GetDC (hwnd) ; GetTextMetrics (hdc, &tm) ; cxChar = tm.tmAveCharWidth ; cxCaps = (tm.tmPitchAndFamily & 1 ? 3 : 2) * cxChar / 2 ; cyChar = tm.tmHeight + tm.tmExternalLeading ; ReleaseDC (hwnd, hdc) ; return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; for (i = 0 ; i < NUMLINES ; i++) ( TextOut (hdc, 0, cyChar * i, devcapsCi].szLabel, , lstrlen (devcapsCi].szLabel)) ; TextOut (hdc, 14 * cxCaps, cyChar * i, devcapsCi].szDesc, lstrlen CdevcapsCi].szDesc)) ; ; Rozdział 5: Podstawy grafiki 121 SetTextAlign (hdc, TA_RIGHT ţ TŽ TOP) ; TextOut (hdc, 14 * cxCaps + 35 * cxChar, cyChar * i, szBuffer, wsprintf (szBuffer, TEXT ("%5d"), GetDeviceCaps (hdc, devcaps[i].iIndex))) ; SetTextAlign (hdc, TAţLEFT ţ TA TOP) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: PostOuitMessage (0) ; return 0 ; ) return DefWindowProc (hwnd, message, wParam, lParam) ; Rysunek 5-1. Program DEVCAPS1 Jak widzisz, program ten jest doœć podobny do programu SYSMETSI z rozdziału 4. Aby nie wydłużać kodu, pominšłem paski przewijania, ponieważ wiedziałem, że wyœwietlana informacja zmieœci się na ekranie. Wyniki dla karty VGA przy rozdzielczoœci 640 na 480 (w 256 kolorach) sš pokazane na rysunku 5-2. HORZSIZE Width in millimeters: 169 VERTSIZE Height in miIISmeters: 127 HORZRES Width in pbcels: 640 VERTRES Helght in raster lines: -080 BITSPI?CEL Color bfts per pixel: 8 pUlNES Number of color planes: 1 NUMBRUSHES Number al device brushes: -1 NUMPENS Number of device pens: 16 NUMMARKERS Number of device markers: g NUMFONTS Number of device fonts: 0 NUMCOLORS Number of devlce colors: 2g PDEVICESIZE Size of device structure: 1112 ŽSPECTX Relat'rve width of pixel: 36 llSPECTY Relat'rve height of pixel: 36 ASPECTXY Relative diagonal af pixel: 51 LOGPI7ţLSX Horizontal dots per inch: 96 `LOGPIXELSY Vertical dots per inch: 96 SI2EPALETTE Number of paletle entries: 256 NUMRESERVED Reserved paletle entries: 2g COLORRES Adual color resolutlon: 18 Rysunek 5-2. Wyniki DEVCAPS1 dla karty VGA przy rozdzielczoœci 640 na 480 w 256 kolorach Rozmiar urzšdzenia Przypuœćmy, że chcesz narysować kwadrat, którego boki majš 1 cal długoœci. Aby to zrobić, albo ty (programista), albo Windows (system operacyjny), musicie wie- dzieć, ile pikseli odpowiada 1 calowi na ekranie. Funkcja GetDeviceCaps pozwala na pobranie informacji odnoœnie fizycznej wielkoœci urzšdzenia wyjœciowego, nie- zależnie od tego, czy jest to ekran, czy drukarka. Częœć I: Podstawy Wyœwietlacze i drukarki sš dwoma różnymi urzšdzeniami. Ale prawdopodob- nie najmniej zwraca się uwagę na to, co znaczy słowo rozdzielczoœć (ang. resolu- tion) w połšczeniu z tymi urzšdzeniami. W drukarkach najczęœciej oznaczamy rozdzielczoœć w punktach na cal. Na przykład większoœć drukarek laserowych ma rozdzielczoœć 300 Iub 600 punktów na cal. Natomiast rozdzielczoœć ekranu jest całkowitš liczbš pikseli w poziomie i w pionie, na przykład 1024 na 768. Więk- szoœć ludzi nie wie, jakš całkowitš liczbę pikseli mogš wydrukować ich drukarki w poziomie i w pionie na kartce papieru albo ile pikseli na cal ma ich ekran. W tej ksišżce będę używał słowa "rozdzielczoœć" tylko na oznaczenie pewnej licz- by pikseli na jednostkę miary, zasadniczo cal. Będę używał wyrażenia "wielkoœć w pikselach" lub "rozmiar w pikselach" do wskazania całkowitej liczby pikseli, które urzšdzenie wyœwietla w poziomie lub w pionie. "Wielkoœć metryczna" lub rozmiar metryczny" jest wielkoœciš obszaru ekranu urzšdzenia w calach lub " w milimetrach. (Dla strony drukarki nie jest to całkowity rozmiar papieru, ale tylko obszar przeznaczony do druku). Podzielenie wielkoœci w pikselach przez wiel- koœć metrycznš daje rozdzielczoœć. Większoœć monitorów używanych obecnie z Windows ma ekran, którego szero- koœć jest o 33% większa od wysokoœci. Daje to proporcję wymiarów jak 1,33:1 albo (jak się najczęœciej pisze) 4:3. Proporcja ta sięga czasów, kiedy Thomas Edison wynalazł kino. Pozostała standardowš proporcjš wymiarów ekranów kinowych, aż do momentu pojawienia się kin szerokoekranowych w 1953 roku. Wymiary ekranu odbiomików telewizyjnych też zachowujš proporcję 4:3. Jednak twoje aplikacje Windows nie powinny zakładać, że ekran ma proporcję wymiarów 4:3. Pracujšcy nad przetwarzaniem tekstów czasami preferujš ekra- ny, których wymiary odpowiadajš proporcjom kartki papieru. Najczęstszš alter- natywš ekranu o proporcji wymiarów 4:3 jest ekran o proporcji 3:4, czyli stan- dardowy ekran obrócony o 90 stopni. Jeœli pozioma rozdzielczoœć urzšdzenia równa jest rozdzielczoœci pionowej, mó- wimy, że urzšdzenie ma kwadratowe piksele. Obecnie większoœć monitorów używanych z Windows ma kwadratowe piksele, aIe nie zawsze tak bywa. (Two- ja aplikacja też nie powinna zakładać, że ekran ma kwadratowe piksele). Kiedy wprowadzono Windows, standardowymi kartami graficznymi były: karta kolo- rowa (CGA) IBM z obszarem o rozmiarach 640 na 200 pikseii, karta EGA z obsza- rem o rozmiarach 640 na 350 pikseli i karta Hercules z obszarem o rozmiarach 720 na 348 pikseli. Wszystkie te karty graficzne współpracowały z monitorami, których proporcje wymiarów ekranu wynosiły 4:3, aIe liczba pikseli w poziomie i w pionie nie odpowiadała tej proporcji. Użytkownik Windows może łatwo okreœlić rozmiary ekranu w pikselach. Uru- chom aplet Ekran w Panelu sterowania i wybierz kartę Ustawienia. W polu z ety- kietš Obszar ekranu zobaczysz prawdopodobnie jeden z poniższych rozmiarów w pikselach: ů 640 na 480 ů 800 na 600 ů 1024 na 768 ů 1280 na 1024 ů 1600 na 1200 Rozdział 5: Podstawy grafiki 123 Wszystkie majš proporcje rozmiarów 4:3. (Wszystkie - z wyjštkiem rozmiaru 1280 na 1024. Należy go traktować raczej jako dokuczliwš anomalię niż coœ o więk- szym znaczeniu. Jak widzimy, wszystkie te rozmiary w pikselach rozważane w połšczeniu z monitorem o proporcji rozmiarów ekranu 4:3 dajš kwadratowe piksele). Aplikacja Windows może otrzymać rozmiary ekranu w pikselach od GetSystem- Metrics z argumentami SM CXSCREEN i SM CYSCREEN. jak zauważyłeœ w pro- gramie DEVCAPSl, program może otrzymać te same wartoœci od GetDeviceCaps z argumentami HORZRES (rozdzielczoœć pozioma) i VERTRES (rozdzielczoœć pionowa). Tutaj słowo "rozdzielczoœć" oznacza raczej rozmiar w pikselach niż liczbę pikseli na jednostkę miary. To była prosta częœć rozważań o rozmiarze urzšdzenia. A teraz rzecz się kompli- kuje. Pierwsze dwie możliwoœci urzšdzenia, HORZSIZE i VERTSIZE, sš udokumen- towane jako "szerokoœć ekranu fizycznego w milimetrach" i "wysokoœć ekranu fizycznego w milimetrach" (w /Platform SDK/Graphics and Multimedia Services/ GDI/Device Contexts/Device Context Reference/Device Context Functions/GetDevice- Caps). Ta definicja wyglšda prosto, dopóki nie zaczniesz się zastanawiać, co tak naprawdę z niej wynika. Na przykład, skšd Windows zna rzeczywisty rozmiar monitora? A co będzie, jeœli masz laptop (w którym karta graficzna mogłaby znać œcisłe fizyczne rozmiary ekranu) i przyłšczysz do niego zewnętrzny monitor? A jeœli przyłšczysz do swojego PC projektor wideo? W 16-bitowych wersjach Windows (i w Windows NT), Windows używa standar- dowej wielkoœci ekranu dla wartoœci HORZSIZE i VERTSIZE. Jednak poczšwszy od Windows 95, wartoœci HORZSIZE i VERTSIZE sš wyprowadzane z wartoœci HORZRES, VERTRES, LOGPIXELSX i LOGPIXELSY. Przyjrzyjmy się jak to działa. Kiedy będziesz używał apletu Ekran w Panelu sterowania do wybierania roz- miarów ekranu w pikselach, możesz też wybrać rozmiar swojej czcionki syste- mowej. Opcja ta się przydaje, gdy czcionka, używana przy wyœwietlaczu 640 na 480, okazuje się za mała do czytania, gdy przejdziesz na rozmiar 1024 na 768 i więcej. Powinieneœ wybrać większš czcionkę systemowš. Wielkoœci czcionek sys- temowych sš opisane na karcie Ustawienia apletu Ekran jako Małe czcionki i Duże czcionki. W tradycyjnej typografii wielkoœć znaku czcionki jest okreœlana w punktach. Punkt odpowiada około 1/72 cala i w typografii komputerowej jest najczęœciej definio- wany jako dokładnie 1/72 cala. W tQorii wielkoœć czcionki w punktach jest odległoœciš między wierzchołkiem górnym znaków takich jak d, l, t, a wierzchołkiem dolnym znaków, takich jak j, p, q lub y (nie bioršc pod uwagę znaków akcentów). Na przykład w czcionce 10- punktowej odległoœć ta powinna mieć 10/72 cala. W kategoriach struktury TEXT METRIC wielkoœć czcionki w punktach jest równa wartoœci pola tmHeight minus wartoœć pola tmlnternalLeading, jak pokazano na rysunku 5-3. (Ten sam rysunek przytoczyłem w rozdziale 4 jako rystinek 4-3). 124 Częœć I: Podstawy tmlnternalLeading tmAscent tmHeight Linia bazowa tmDescent Rysunek 5-3. Mała czcionka i pola TEXTMETRIC W typografii wielkoœć czcionki w punktach nie zawsze dokładnie odpowiada realnej wielkoœci znaków czcionki. Projektant czcionki może zrobić znaki trochę większe lub mniejsze, niż wskazuje na to wielkoœć w punktach. W końcu projek- towanie czcionek jest sztukš, a nie naukš. Pole tmHeight struktury TEXTMETRIC oznacza, w jaki sposób linie tekstu będš oddzielane na ekranie lub drukarce. Odstępy między liniami mogš być także mierzone w punktach. Na przykład 12-punktowy odstęp oznacza, że linie bazo- we kolejnych wierszy tekstu powinny być odsunięte o 12/72 (albo 1/6) cala. Za- pewne nie będziesz chciał używać 10-punktowego odstępu dla 10-punktowej czcionki, ponieważ linie tekstu mogłyby się dotykać. Ta ksišżka jest wydrukowana czcionkš 10-punktowš i z 12-punktowym odstę- pem. Czcionka 10-punktowa uchodzi za dostatecznie wygodnš w czytaniu. Czcionki mniejsze nie nadajš się do dłuższego czytania. Systemowa czcionka Windows - niezależnie od tego, czy jest "małš czcionkš" czy "dużš czcionkš", i niezależnie od rozmiaru ekranu, który wybrałeœ -jest uwa- żana za czcionkę 10-punktowš z 12-punktowym odstępem. Wiem, że wyglšda to dziwnie. Dlaczego nazywa się czcionkę systemowš "małš czcionkš" lub "dużš czcionkš", jeœli obie majš tę samš wielkoœć? Oto wyjaœnienie: gdy wybierzesz małš czcionkę lub dużš czcionkę w aplecie Ekran w Panelu sterowania, tak naprawdę to ustawiasz przyjmowanš rozdzielczoœć ekra- nu w punktach na cal. Gdy wybierzesz małš czcionkę, mówisz, że chcesz, aby Windows przyjšł, że wyœwietlacz ma rozdzielczoœć 96 punktów na cal. Gdy wy- bierasz dużš czcionkę, chcesz, aby Windows przyjšł, że wyœwietlacz ma rozdziel- czoœć 120 punktów na cal. Spójrz jeszcze raz na rysunek 5-3. To jest mała czcionka, oparta na rozdzielczoœci wyœwietlacza 96 punktów na cal. Powiedziałem, że jest to czcionka 10-punkto- Rozdział 5: Podstawy grafiki 125 wa. Dziesięć punktów to 10/72 cala, co, jeœli pomnożysz przez 96 punktów na cal, da (około) trzynastu pikseli. To jest tmHeight minus tmlnternalLeading. Od- stęp linii,jest 12-punktowy (lub 12/72 cala), co pomnożone przez 96 punktów na cal daje 16 pikseli. To jest tmHeight. Rysunek 5-4 pokazuje dużš czcionkę. Jest ona oparta na rozdzielczoœci 120 punktów na cal. Jeszcze raz, jest to 10-punktowa czcionka, a 10/72 razy 120 punktów na cal rów- na się 16 pikselom (jeœli zaokršglisz w dół), co oznacza tmHeight minus tmInternalLe- ading. 12-punktowy odstęp linii odpowiada 20 pikselom, co daje tmHeight. (Jak już mówiłem w rozdziale 4, a przypomnę jeszcze raz: pokazuję rzeczywiste wymiary, abyœ mógł zrozumieć, jak to działa. Nie umieszczaj tych liczb w swoim programie). 1 tr"ExtemalLeading tmAscent ţ ,ţ. tmHeight - Linia bazowa tmDescent - RySunek 5-4. Duża czcionka i pola FONTMETRIC W programie Windows możesz używać funkcji GetDeviceCaps, aby uzyskać roz- dzielczoœć w punktach na cal, którš użytkownik wybrał w aplecie Ekran w Pane- lu sterowania. Aby pobrać te wartoœţi - które teoretycznie mogš być różne, jeœli , wyœwietlacz nie ma kwadratowych pikseli - użyj znaczników LOGPIXELSX i LOGPIXELSY. Nazwa LOGPIXELS to piksele logiczne (ang. logical pixels), co po prostu oznacza: nierzeczywista rozdzielczoœć w pikselach na cal. Możliwoœci urzšdzenia, które otrzymujesz od GetDeviceCaps z indeksami HORZ- SIZE i VERTSIZE sš okreœlane (jak nadmieniłem wczeœniej) odpowiednio jako szerokoœć ekranu fizycznego w milimetrach i wysokoœć ekranu fizycznego w mi- limetrach. Powinny być jednak nazywane szerokoœciš logicznš i wysokoœciš lo- 1. gicznš, ponieważ ich wartoœci pochodzš od wartoœci HORZRES, VERTRES, LOG- PIXELSX i LOGPIXELSY. Oto odpowiednie wzory: 25 4 x rozdzielczoœć w poţiomie (pikseli) Rozmiar poziomy (mm) = ) X pikseli logicznych (punktów na cal ţ5 ţ x rozaţzielczoœć w pionie (pikseli) Rozmiar pionowy (mm) = unktów na cal) , Y pikseli logicznych (p Tţ 126 Częœć I: Podstawy Stała 25,4 jest konieczna do zamiany cali na milimetry. Wszystko to wyglšda dziwnie. Przecież twój ekran można łatwo zmierzyć linijkš i podać jego rozmiary w milimetrach (co najmniej w przybliżeniu). Ale Windows 98 nie akceptuje takich pomiarów. Oblicza natomiast wielkoœć ekranu w milime- trach na podstawie rozmiaru ekranu w pikselach i rozdzielczoœci czcionki syste- mowej, wybranych przez użytkownika. Zmień rozmiar swojego ekranu w pikse- lach i według funkcji GetDeviceCaps zmieni się jego wielkoœć metryczna. Czy to ma sens? Ma to więcej sensu niż ci się wydaje. Załóżmy, że masz monitor 17-calowy. Rze- czywista wielkoœć ekranu to prawdopodobnie 12 cali na 9 cali. Załóżmy, że uru- chomiłeœ Windows z minimalnymi wymaganymi rozmiarami ekranu: 640 na 480 pikseli. Oznacza to, że rzeczywista rozdzielczoœć to 53 punkty na cal. Czcionka 10-punktowa - doskonale czytelna na papierze - na ekranie będzie miała tylko 7 pikseli wysokoœci. Taka czcionka byłaby brzydka i prawie nieczytelna. (Spytaj tych, którzy pamiętajš, jak pracował Windows na starej karcie graficznej CGA). Teraz podłšcz do swojego PC projektor wideo. Powiedzmy, że ekran projekcyjny ma 4 stopy szerokoœci i 3 stopy wysokoœci. Ten sam rozmiar, 640 na 480 pikseli, daje teraz rozdzielczoœć 13 punktów na cal. To œmieszne wyœwietlać 10-punkto- wš czcionkę w takich warunkach. Czcionka 10-punktowa powinna być czytelna na ekranie, ponieważ jest na pew- no czytelna na wydruku. Dlatego czcionka 10-punktowa staje się tak ważnym punktem odniesienia. Gdy aplikacja Windows ma zagwarantowane, że 10-punk- towa czcionka ekranowa jest przeciętnej wielkoœci, może wtedy wyœwietlać mniej- szš (ale cišgle jeszcze czytelnš) czcionkę 8-punktţowš i czcionkę większš niż 10 punktów. Więc ma to sens, że rozdzielczoœć ekranu (w punktach na cal) wynika z wielkoœci w pikselach czcionki 10-punktowej. Definiujšc wartoœci HORZSIZE i VERTSIZE w Windows NT, zastosowano starsze podejœcie. Jest ono spójne z 16-bitowymi wersjami Windows. Wartoœci HORZRES i VERTRES nadal wskazujš liczbę pikseli w poziomie i w pionie (oczywiœcie), a LOG- PIXELSX i LOGPIXELSY sš powišzane z czcionkš, którš wybierasz, ustawiajšc rozdzielczoœć ekranu w aplecie Ekran w Panelu sterowania. Podobnie jak w Win- dows 98, typowe wartoœci LOGPIXELSX i LOGPIXELSY wynoszš 96 i 120 punk- tów na cal, w zależnoœci od tego, czy wybrałeœ małš, czy też dużš czcionkę. Różnica w Windows NT polega na tym, że wartoœci HORZSIZE i VERTSIZE sš ustalone w ten sposób, by wskazywały standardowš wielkoœć monitora. Dla więk- szoœci kart graficznych wartoœci HORZSIZE i VERTSIZE będš wynosiły odpo- wiednio 320 i 240 milimetrów. Te wartoœci sš takie same, niezależnie od tego, jaki wybierzesz rozmiar ekranu w pikselach. Dlatego sš one niezgodne z wartoœcia- mi otrzymanymi od GetDeviceCaps z indeksami HORZRES, VERTRES, LOGPI- rţ:. XELSX i LOGPIXELSY. Jednak zawsze możesz obliczyć wartoœci HORZSIZE i VERTSIZE, jakie otrzymałbyœ w Windows 98, używajšc pokazanych wczeœniej , wzorów. A jeœli twój program potrzebuje rzeczywistych fizycznych rozmiarów ekranu? Wtedy najlepiej jest utworzyć okno dialogowe, w którym użytkownik poda te wartoœci. Rozdział 5: Podstawy gţafiki Na koniec przedstawimy jeszcze trzy inne wartoœci GetDeviceCaps zwišzane z roz- miarami ekranu. Wartoœci ASPECTX, ASPECTY i ASPECTXY to względna sze- rokoœć, wysokoœć i przekštna rozmiaru każdego piksela, zaokršglone do najbliż- szej wartoœci całkowitej. Dla kwadratowych pikseli wartoœci ASPECTX i ASPEC- TY sš takie same. A jak pamiętasz z twierdzenia Pitagorasa, wartoœć ASPECTXY równa się pierwiastkowi kwadratowemu z sumy kwadratów wartoœci ASPECTX i ASPECTY. Informacje o kolorach Wyœwietlanie czamych i białych kolorów wymaga tylko jednego bitu pamięci na piksel. Wyœwietlacze kolorowe wymagajš wielu bitów na piksel. Im więcej bitów, tym więcej kolorów; albo dokładniej - liczba niepowtarzajšcych się kolorów wy- œwietlanych równoczeœnie jest równa 2 do potęgi równej liczbie bitów na piksel. W trybie full color używa się 24 bitów na piksel - 8 bitów dla czerwonego, 8 bi- tów dla zielonego i 8 bitów dla niebieskiego. Czerwony, zielony i niebieski sš znane jako addytywne barwy podstawowe. Mieszanina tych trzech podstawowych barw pozwala tworzyć wiele innych kolorów, co możesz sprawdzić patrzšc na ekran przez szkło powiększajšce. W trybie high color jest używanych 16 bitów na piksel - 5 bitów dla czerwonego, 6 bitów dla zielonego i 5 dla niebieskiego. Dla zielonej barwy podstawowej po- trzeba więcej bitów, ponieważ oko ludzkie jest bardziej wrażliwe na odmiany zielonego, niż na inne barwy podstawowe. Karta graficzna, która wyœwietla 256 kolorów, wymaga 8 bitów na piksel. Jednak te 8-bitowe wartoœci sš pozycjami w tabeli palety, która definiuje prawdziwe kolory. Omówię to dokładnie w rozdziale 16. W końcu, karta graficzna wyœwietlajšca 16 kolorów wymaga 4 bitów na piksel. Te 16 kolorów zazwyczaj obejmuje ciemne i jasne wersje czerwonego, zielonego, niebieskiego, turkusowego, karmiţsynowego, żółtego, dwu odcieni szarego, czar- nego i białego. Te 16 kolorów pochodzi ze starych kart IBM CGA. Tylko w niektórych dziwnych zadaniach programistycznych potrzebna jest wie- dza o sposobie organizacji pamięci na karcie graficznej; zawsze możesz uzyskać te informacje, wywołujšc funkcję GetDeviceCaps. Pamięć karty graficznej może być zorganizowana albo z użyciem cišgłych bitów kolorów dla każdego piksela, albo z każdym bitem kolorów w oddzielnej płaszczyŸnie pamięci. Następujšce wywołanie zwraca liczbę plaszczyzn kolorów: iPlanes = GetDeviceCaps (hdc, PLANES) : A to wywoł'anie Ÿwraca liczbę bitów koloru na piksel: iBitsPixel = GetDeviceCaps (hdc, BITSPIXEL) : š jedno z tych wywołań zwróci wartoœć 1. Liczba kolorów, które mog być równo- , czeœnie pokazane na ekranie, może być obliczona za pomocš wzoru: iColors = 1 Ž (iPlanes * iBitsPixel) : Ta wartoœe może być albo taka sama, albo różna od liczby kolorów pobranej z argumentem NUMCOLORS: iColors = GetDeviceCaps (hdc. NUMCOLORS) : T 128 Częœć I: Podstawy Wspominałem, że karty graficzne z 256 kolorami używajš palet kolorów. W tym przypadku GetDeviceCaps z indeksem NUMCOLORS zwraca liczbę kolorów za- rezerwowanych przez Windows, czyli 20. Pozostałe 236 kolorów można zmie- niać w programie windowsowym, korzystajšc z menedżera palety. Dla trybów high color i full color funkcja GetDeviceCaps z indeksem NUMCOLORS najczę- œciej zwróci -1, co oznacza, że funkcja ta nie dostarczy ci potrzebnych informacji. Lepiej zastosuj wzór iColors, pokazany wczeœniej, który odwohzje się do wartoœci PLANES i BITSPIXEL. W trakcie wywołań funkcji GDI najczęœciej używasz wartoœci COLORREF (która jest po prostu 32-bitowš długš liczba całkowitš bez znaku) do oznaczenia pew- nego koloru. Wartoœć COLORREF, często nazywana kolorem RGB, okreœla kolor, podajšc stopień nasilenia barwy czerwonej, zielonej i niebieskiej. Te 32 bity war- toœci COLORREF sš ustawiane tak, jak pokazano na rysunku 5-5. 31 . . 24: 23 . . . 16: 15 , . . 8 : 7 '.. . . . ; 0 ': 0 Blue Green Red Rysunek 5-5. 32-bitowa wartoœć COLORREF Zauważ, że najbardziej znaczšce 8 bitów to zera, i że każda barwa podstawowa jest okreœlona jako wartoœć 8-bitowa. Teoretycznie wartoœć COLORREF może odnosić się do 224, czyli około 16 milionów kolorów. Plik nagłówkowy Windows WINGDI.H zawiera kilka makr obsługujšcych war- toœci koloru RGB. Makro RGB pobiera trzy argumenty, przedstawiajšce wartoœci czerwonš, zielonš i niebieskš, i składa je w liczbę całkowitš bez znaku: define RGB(r,g,b) ((COLORREF)(((BYTE)(r) ţ \ ((WORD)((BYTE)(g)) Ž 8)) ţ \ (((DWORD)(BYTE)(b)) Ž 16))). Zwróć uwagę na kolejnoœć tych trzech argumentów: czerwony, zielony i niebie- ski. Tak więc wartoœć RGB (255. 255, 0) to wartoœć 0x0000FFFF albo kolor żółty - kombinacja czerwonego i zielonego. Gdy wszystkie trzy argumenty sš równe 0, to otrzymamy kolor czamy; kiedy wszyst- kie argumenty sš ustawione na 255, otrzymamy kolor biały. Makra GetRvalue, GetGvalue, i GetBvalue wycišgajš wartoœci barw podstawowych z wartoœci CO- LORREF. Te makra przydajš się, gdy używasz funkcji Windows, które zwracajš do programu wartoœci koloru RGB. Na kartach graficznych w trybach 16 lub 256 kolorów Windows może stosować symulowanie (ang. dithering) do zasymulowania większej iloœci kolorów, niż te, które urzšdzenie może wyœwietlić. Symulacja posługuje się małymi wzorcami , które zawierajš piksele różnych kolorów. Możesz okreœlić czysty kolor najbliższy danemu kolorowi, wywołujšc GetNearestColor: crPureColor = GetNearestColor (hdc, crColor) ; Rozdział 5: Podstawy grafiki Œ Atrybuty kontekstu urzšdzenia ; Jak już wspomniałem, Windows używa kontekstu urzšdzenia do przechowywa- nia atrybutów, które okreœlajš, w jaki sposób funkcje GDI działajš na ekranie. Na przykład, gdy wyœwietlasz jakiœ tekst za pomocš funkcji TextOut, nie musisz po- dawać koloru tekstu ani czcionki. Windows używa kontekstu urzšdzenia do po- brania tej informacji. Gdy program pobiera uchwyt kontekstu urzšdzenia, Windows nadaje wszyst- kim atrybutom wartoœci domyœlne. (W następnej częœci zobaczymy, jak zmienić to zachowanie). Następujšca tabela zawiera atrybuty kontekstu urzšdzenia w Win- dows 98, ich wartoœci domyœlne i funkcje do zmiany lub pobrania wartoœci atry- butów. Atrybut kontekstu Wartoœć Funkcja Funkcja urzšdzenia domyœlna zmieniajšca wartoœć zwracajšca wartoœć Tryb odwzorowania MM TEXT SetMapMode GetMapMode (ang. mapping mode) Poczštek okna (0, 0) SetWindowOrgEx GetWindowOrgEx j (ang. window origin) OffsetWindowOrgEx Poczštek widoku (0, 0) SetViewportOrgEx GetViewportOrgEx (ang. viewport origin) OffsetViewportOrgEx Rozcišgłoœć okna (1, 1) SetWindowExtEx GetWindowExtEx (ang. window extents) SetMapMode ScaleWindowExtEx Rozcišgłoœć widoku (1, 1) SetViewportExtEx GetViewportExtEx (ang. viewport extents) SetMapMode ScaleViewportExtEx Pióro (ang. pen) BLACK PEN SelectObject SelectObject Pędzel (ang. brush) WHITE BRUSH SelectObject SelectObject Czcionka (ang. font) SYSTEM FONT SelectObject SelectObject Bitmapa Brak SelectObject SelectObject (ang. bitmap) Pozycja bieżšca (0, 0) MoveToEx GetCurrentPositionEx (ang. current position) LineTo ! PolylineTo PolyBezierTo Tryb tła OPAQUE SetBkMode GetBkMode (ang. background mode) Kolor tła Biały SetBkColor GetBkColor (ang. backgroud color) Kolor tekstu Czarny SetTextColor GetTextColor iţ. (ang. text color) Tryb rysowania R2ţCOPYPEN SetROP2 GetROP2 (ang. drawing mode) i Tryb rozcišgania BLACKONWHITE SetStretchBltMode GetStretchBitMode bitmapy (ang. stretching mode) Tryb wypełniania ALTERNATE SetPolyFillMode GetPolyFillMode wielokštów ; (ang. polygon fill mode) 130 Częœć Iţ Podstawy Atrybut kontekstu Wartoœć Funkcja Funkcja urzšdzenia domyœlna zmieniajšca wartoœć zwracajšca wartoœć Odstęp między 0 SetTextCharacterExtra GetTextCharacterExtra znakami (ang. inter- character spacing) Poczštek pędzla (0, 0) SetBrushOrgEx GetBrushOrgEx (ang. brush origin) Region obcinania Brak SelectObject GetClipBox (ang. clipping region) SelectClipRgn IntersectCIipRgn OffsetClipRgn ExcludeClipRect SelectCIipPath Zapamiętywanie kontekstu urzšdzenia Zwykle, gdy wywołujesz GetDC albo BeginPaint, Windows daje ci kontekst urzš- dzenia z domyœlnymi wartoœciami atrybutów. Wszystkie zmiany, które wprowa- dzisz do atrybutów, przestanš obowišzywać, gdy kontekst urzšdzenia zostanie zwolniony wywołaniem ReleaseDC albo EndPaint. Jeżeli twój program wymaga niestandardowych atrybutów kontekstu urzšdzenia, musisz inicjować kontekst urzšdzenia za każdym razem, gdy pobierasz nowy uchwyt kontekstu urzšdze- nia: case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; [inicjuj atrybuty kontekstu urzddzenia] Cpomaluj obszar roboczy okna] ţ EndPaint (hwnd, &ps) ; return 0 ; Takie rozwišzanie zazwyczaj wystarcza, byłoby jednak lepiej, gdybyœ mógł za- pamiętać zmiany atrybutów, a następnie odtworzyć je przy kolejnym wywoła- niu GetDC albo BeginPaint. Możesz to uczynić, włšczajšc flagę CS OWNDC jako częœć stylu klasy okna, gdy rejestrujesz klasę okna: wndclass.style = CS HREDRAW ţ CSţVREDRAW ţ CS OWNDC ; Teraz każde okno, które utworzysz na podstawie tej klasy okna, będzie miało swój własny kontekst urzšdzenia, który będzie istniał nawet wtedy, kiedy okno zosta- nie zruszczone. Gdy używasz stylu CS'OWNDC, musisz inicjować atrybuty kon- tekstu urzšdzenia tylko raz, na przykład podczas przetwarzania komunikatu WMţCREATE: case WM_CREATE: hdc = GetDC (hwnd) ; Cinicjuj atrybuty kontekstu urzddzenia] ReleaseDC (hwnd, hdc) : Atrybuty te zachowajš ważnoœć do czasu, aż je zmienisz. Styl CS OWNDC wpływa jedynie na kontekst urzšdzenia uzyskany od GetDC i BeginPaint, i nie jest zwišzany z kontekstem urzšdzenia uzyskanym od innych funkcji (takich jak GetWindowDC). Zastosowanie CS OWNDC wymagało nieste- Rozdział 5: Podstawy grafiki 131 ty dodatkowej pamięci; obecnie może polepszyć wydajnoœć w niektórych aplika- cjach Windows NT intensywnie wykorzystujšcych grafikę. Nawet jeœli używasz CS OWNDC, powinieneœ nadal zwalniać uchwyt kontekstu urzšdzenia przed opuszczeniem procedury okna. W niektórych przypadkach chciałbyœ zmienić różne atrybuty kontekstu urzšdze- nia, wykonać malowanie, używajšc zmienionych atrybutów, i powrócić do ory- ginalnego kontekstu urzšdzenia. By uproœcić ten proces, zapamiętaj stan kontek- stu urzšdzenia, wywołujšc: idSaved = SaveDC (hdc) ; Teraz możesz zmienić niektóre atrybuty. Kiedy będziesz chciał przywrócić kon- tekst urzšdzenia do stanu przed wywołaniem SaveDC, zastosuj: RestoreDC (hdc, idSaved) ; Możesz wielokrotnie wywoływać SaveDC, zanim wywołasz RestoreDC. Jednak większoœć programistów używa SaveDC i RestoreDC w inny sposób, tak samo jak instrukcji PUSH i POP w asemblerze. Kiedy wywołasz SaveDC, nie po- trzebujesz zapamiętywać zwracanej wartoœci: SaveDC (hdc) ; Możesz następnie zmieniać niektóre atrybuty i wywołać znowu SaveDC. Aby przywrócić kontekst urzšdzenia sprzed ostatniego stanu, napisz: RestoreDC (hdc, -1) ; To przywróci kontekst urzšdzenia do stanu zapamiętanego przez ostatniš funk- cję SaveDC. Rysowanie punktów i linii W pierwszym rozdziale omawialiœmy, w jaki sposób GDI Windows używa ste- ; rowników urzšdzeń dla graficznych urzšdzeń wyjœciowych dołšczonych do two- jego komputera. Teoretycznie sterowniki urzšdzeń graficznych potrzebujš do rysowania jedynie funkcji SetPixel i GetPixel. Cała reszta może być obsłużona przez procedury wysokiego poziomu zrealizowane w module GDI. Na przykład ryso- wanie linii wymaga jedynie, żeby GDI wywoływał wielokrotnie procedurę SetPi- ` xel, korygujšc odpowiednio współrzędne x i y. W rzeczywistoœci, możesz zrobić niemal każdy potrzebny ci rysunek, korzysta- jšc tylko z funkcji SetPixel i GetPixel. Na bazie tych funkcji możesz też zaprojekto- wać przyjemny i logiczny system programowania graficznego. Jedynym proble- mem pozostaje wydajnoœć. Funkcja, która jest oddalona o szereg wywołań od każ- dej funkcji SetPixel, będzie nadzwyczaj powolna. Z punktu widzenia systemu gra- ficznego o wiele bardziej efektywne jest rysowanie linii i wykonywanie innych zło- żonych operacji graficznych na poziomie sterownika urzšdzenia, który ma swój własny optymalizowany kod do wykonywania tych operacji. Ponadto niektóre karty I zawierajš koprocesory graficzne pozwalajšce sprzętowi na samodzielne rysowa- nie. 132 CzQœć I: Podstawv Podœwietlanie pikseli Chociaż GPI zawiera funkcje SetPixel i GetPixel, zwykle nie sš one używane. W tej ksišżce funkcja SetPixel jest używana tylko w programie CONNECT w rozdziale 7, a funkcja GetPixel tylko w programie WHATCLR w rozdziale 8. Jednak cišgle stanowiš dobre wprowadzenie do analizy grafiki. Funkcja SetPixel umieszcza piksel konkretnego koloru w okreœlonych współrzęd- nych x i y: SetPixel (hdc, x, y, crColor) ; Jak w każdej funkcji rysujšcej, pierwszy argument jest uchwytem do kontekstu urzšdzenia. Drugi i trzeci argument wskazuje współrzędne pozycji. Najczęœciej uzyskasz kontekst urzšdzenia dla obszaru roboczego swojego okna, a x i y będš współrzędnymi względem jego lewego górnego rogu. Końcowy argument typu COLORREF okreœla kolor. Jeœli kolor okreœlony w funkcji nie może być zrealizo- wany na ekranie, funkcja ustawia najbliższy czysty, niesymulowany (ang. nondi- thered) kolor i zwraca jego wartoœć. Funkcja GetPixel zwraca kolor piksela o okreœlonych współrzędnych: crColor = GetPixel (hdc, x, y) ; Linie proste Windows może rysować linie proste, linie eliptyczne (linie zakrzywione na ob- wodzie elipsy) i krzywe sklejane Beziera. Windows 98 umożliwia korzystanie z siedmiu funkcji rysujšcych linie: ů LineTo - rysuje linię prostš. ů Polyline i PolylineTo - rysuje szereg połšczonych linii prostych. ů PolyPolyline - rysuje wiele linii ţamanych. ů Arc - rysuje linie eliptyczne. ů PolyBezier i PolyBezierTo - rysuje krzywe sklejane Beziera. Windows NT umożliwia korzystanie z trzech dodatkowych funkcji rysujšcych linie: ů ArcTo i AngIeArc - rysujš linie eliptyczne. ů PolyDraw - rysuje szereg połšczonych linii prostych i krzywych sklejanych Beziera. W Windows 98 nie można korzystać z tych trzech funkcji. W dalszej częœci rozdziału omówię kilka funkcji, które nie tylko rysujš linie, ale także wypełniajš zamknięte obszary wewnštrz rysowanej figury. Tymi funkcjami sš: ů Rectangle - rysuje prostokšt. ů Ellipse - rysuje elipsę. ů RoundRect - rysuje prostokšt z zaokršglonymi rogami. ů Pie - rysuje częœć elipsy, który wyglšda jak kawałek tortu. ů Chord - rysuje częœć elipsy odciętš przez strunę. Rozdział 5: Podstawy grafiki 133 Pięć atrybutów kontekstu urzšdzenia ma wpływ na pojawienie się linii, którš rysujesz, za pomocš tych funkcji: bieżšca pozycja pióra (tylko dla LineTo, Polyli- neTo, PolyBezierTo i ArcTo), pióro, tryb tła, kolor tła i tryb rysowania. Aby narysować linię prostš, musisz wywołać dwie funkcje. Pierwsza funkcja okreœla punkt poczštkowy linii, a druga - punkt końcowy linii: MoveToEx (hdc, xBeg, yBeg, NULL) ; LineTo (hdc, xEnd, yEnd) ; MoveToEx w rzeczywistoœci niczego nie rysuje; ustawia jedynie atrybut kontek- stu urzšdzenia o nazwie "pozycja bieżšca"(ang. current position). Funkcja LineTo rysuje następnie linię prostš od pozycji bieżšcej do pozycji okreœlonej w funkcji LineTo. Pozycja bieżšca jest po prostu punktem poczštkowym dla paru innych funkcji GDI. W domyœlnym kontekœcie urzšdzenia pozycja bieżšca jest poczšt- i kowo ustawiana na punkt (0, 0). Jeœli wywołasz funkcję LineTo bez wczeœniejsze- go ustawienia pozycji bieżšcej, narysuje ona linię zaczynajšcš się w lewym gór- nym rogu obszaru roboczego. Krótki rys historyczny: w 16 bitowych wersjach Windows funkcjš ustawiajšcš po- zycję bieżšcš była MoveTo. Ta funkcja miała tylko trzy argumenty - uchwyt kon- tekstu urzšdzenia i współrzędne x i y. Funkcja ta zwracała poprzedniš pozycję bie- żšcš jako dwie wartoœci 16-bitowe spakowane w 32-bitowš liczbę całkowitš bez ; znaku. Jednak w 32-bitowych wersjach Windows współrzędne sš wartoœciami 32- bitowymi. Ponieważ 32-bitowe wersje C nie definiujš 64-bitowego typu danych, MoveTo nie może już dłużej pokazywać poprzedniej pozycji bieżšcej w wartoœci zwracanej. Pomimo że wartoœć zwracana przez MoveTo nie znalazła zastosowania w programowaniu, potrzebna była nowa funkcja i jest to MoveToEx. Ostatnim argumentem MoveToEx jest wskaŸ.nik do struktury POINT. Przy powro- i cie z funkcji pola x i y struktury POINT będš zawierały poprzedniš pozycję bieżš- cš. Jeœli nie potrzebujesz tej informacji (a tak zazwyczaj będzie), możesz po prostu przekazać NšiţL jako ostatni argument, tak jak w powyższym przykładzie. A teraz problem: pomimo że wartoœci współrzędnych w Windows 98 wyglšdajš na 32-bitowe, używane jest tylko mniej znaczšce 16 bitów. Wartoœci współrzęd- nych w rzeczywistoœci mieszczš się w zakresie od -32768 do 32767. W Windows NT sš używane pełne wartoœci 32-bitowe. š Jeżeli kiedykolwiek będziesz potrzebował pozycji bieżšcej, będziesz mógł j uzy- skać przez wywołanie: GetCurrentPositionEx (hdc, &pt) ; gdzie pt jest strukturš POINT. Następujšcy kod rysuje siatkę linii w obszarze roboczym okna, w odstępach co 100 pikseli, zaczynajšc od lewego górnego rogu. Zmienna hwnd oznacza uchwyt okna, hdc jest uchwytem kontekstu urzšdzenia, a x i y sš liczbami całkowitymi: GetClientRect (hwnd, &rect) ; l for (x = 0 ; x < rect.right ; x+= 100) ( MoveToEx (hdc, x, 0. NULL) ; LineTo (hdc, x, rect.bottom) ; ; y += 100) for (y = 0 ; y < rect.bottom 134 Częœć I: Podstawy MoveToEx (hdc, 0, y, NULL) ; LineTo (hdc, rect.right, y) ; Chociaż używanie dwóch funkcji do narysowania pojedynczej linii wydaje się przykre, pozycja bieżšca przydaje się, gdy chcesz narysować serię połšczonych linii. Na przykład możesz zdefiniować tablicę 5 punktów (10 wartoœci), które opisujš zarys prostokšta: POINT apt[5] = ( 100, 100, 200, 100, 200, 200, 100, 200, 100, 100 1 ; Zauważ, że ostatni punkt jest taki sam jak pierwszy. Teraz wystarczy tylko wy- wołać MoveToEx dla pierwszego punktu i LineTo dla kolejnych punktów: MoveToEx (hdc, apt[0].x, apt[0].y, NULL) ; for (i = 1 ; i < 5 ; i++) LineTo (hdc, apt[i].x, apt[i].y) ; Ponieważ LineTo rysuje od pozycji bieżšcej do (ale nie włšczajšc) punktu z funk- cji LineTo, żadne punkty nie zostanš narysowane podwójnie w tym kodzie. Po- wtórne rysowanie punktów nie jest problemem na ekranie, może jednak nieład- nie wyglšdać na ploterze albo w niektórych trybach rysowania, które omówię w tym rozdziale. Gdy masz tablicę punktów, które chcesz połšczyć liniami, możesz narysować je proœciej, używajšc funkcji Polyline. Ta instrukcja rysuje taki sam prostokšt jak kod pokazany powyżej: Polyline (hdc, apt, 5) ; Ostatnim argumentem jest liczba punktów. Możemy także okreœlić tę wartoœć jako sizeof (apt)/ sizeof (POINT). Działanie Polyline jest identyczne jak poczštkowego MoveToEx i następujšcych po nim funkcji LineTo. Jednak Polyline nie używa, ani nie zmienia pozycji bieżšcej. Funkcja PolylineTo jest trochę inna. Używa pozycji bieżšcej jako punktu poczštkowego i ustawia pozycję bieżšcš na koniec ostatniej narysowanej linii. Poniższy kod rysuje prostokšt znany nam z poprzednich przy- kładów: MoveToEx (hdc, apt[0].x, aptCO].y, NULL) ; PolylineTo (hdc, apt + 1, 4) ; Chociaż możesz używać Polyline i PolylineTo do rysowania tylko kilku linii, tunk- cje te sš najbardziej użyteczne, gdy musisz narysować złożonš krzywš. Uzyskasz jš, układajšc setki albo nawet tysišce bardzo krótkich odcinków. Jeżeli sš one wystarczajšco krótkie i jest ich wystarczajšco dużo - razem wyglšdajš jak krzy- wa. Przypuœćmy, że musisz narysować sinusoidę. Program SINEWAVE z rysun- ku 5-6 pokazuje, jak to zrobić. SINEWAVE.C /* SINEWAVE.C - Sinusoida z użyciem Polyline (c) Charles Petzold, 1998 */ Ilinclude ilinclude Rozdział 5: Podstawy gţafiki 135 ţţdefine NUM 1000 ţidefine TWOPI (2 * 3.14159) LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) ( static TCHAR szAppNameCJ = TEXT ("SineWave") : HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CS VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI ŽPPLICATION) ; ! wndclass.hCursor = LoadCursor (NULL, IDC ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITEţBRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) i MessageBox (NULL, TEXT ("Program requires Windows NT!"), szAppName, MBţICONERROR) : return 0 ; hwnd = CreateWindow (szAppName. TEXT ("Sine Wave Using Polyline"). WS_OVERLAPPEDWINDOW, 1 CW_USEDEFAULT, CW USEDEFAULT, CW_USEDEFAULT, CW USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&ms9, NULL, 0, 0)) i TranslateMessage (&msg) ; DispatchMessage (&msg) ; i return msg.wParam ; 1 LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) static int cxClient, cyClient ; , HDC hdc ; int i : PAINTSTRUCT ps ; POINT apt [NUM] ; switch (message) ( case WM_SIZE: cxClient = LOWORD (lParam) ; 136 Częœć I: Podstawy cyClient = HIWORD (lParam) ; return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; MoveToEx (hdc, 0, cyClient / 2, NULL) ; LineTo (hdc, cxClient, cyClient / 2) ; for ( i = 0 ; i < NUM ; i++) ( aptCi].x = i * cxClient / NUM ; aptCi].y = (int) (cyClient / 2 * (1 - sin (TWOPI * i / NUM))) ; 1 Polyline (hdc, apt, NUM) ; return 0 ; case WMţDESTROY: PostOuitMessage (0) ; return 0 ; l return DefWindowProc (hwnd, message, wParam, lParam) ; 1 Rysunek 5-6. Program SINEWAVE ' Program zawiera tablicę 1000 struktur POINT. W czasie, gdy pętla for jest zwięk- szana od 0 do 999, pola x struktury POINT sš ustawiane na zwiększanie wartoœci od 0 do cxClient. Program ustawia w polach y struktury POI I' wartoœci krzywej sinusoidalnej dla każdego przebiegu i odpowiednio powiększa, tak aby wypełniły obszar roboczy. Cała krzywa jest rysowana podczas jednego wywołania Polyline. Ponieważ funkcja Polyline jest realizowana na poziomie sterownika urzšdzenia, jest tysišc razy szybsza niż wywoływanie LineTo. Wyruki widać na rysunku 5-7. a Rysunek 5-7. Efekt działania programu SINEWAVE Rozdział 5: Podstawy grafiki Funkcje obramowania Chcę teraz omówić funkcję Arc, rysujšcš krzywe eliptyczne. Jednak nie ma sensu omawiać Arc bez wczeœniejszego omówienia funkcji Ellipse, a nie ma sensu oma- wiać Ellipse bez wczeœniejszego omówienia funkcji Rectangle. A jeœli już omówię Ellipse i Rectangle, mogę omówić także RoundRect, Chord i Pie. Problem stanowi to, że funkcje Rectangle, Ellipse, RoundRect, Chord i Pie nie sš w œci- słym znaczeniu słowa funkcjami rysujšcymi. Tak, rysujš one linie, ale także za- malowujš wybranym pędzlem zamknięty obszar. Domyœlnie ten pędzel jest bia- ły, więc gdy zaczniesz z nimi eksperymentować, możesz nawet nie zauważyć, że funkcje te oprócz rysowania linii robiš coœ więcej. Funkcje te powinny się właœci- wie znaleŸć w podrozdziale "Rysowanie wypełnionych obszarów", ale omówię je już teraz. Funkcje wymienione powyżej majš wspólnš cechę - pracujš na zasadzie prosto- kštnego obramowania (ang. bounding box). Definiujesz współrzędne prostokšt- nego obramowania, w którym znajduje się obiekt, a Windows rysuje obiekt we- wnštrz tego obramowania. Najprostsza z tych funkcji rysuje prostokšt: Rectangle (hdc, xLeft. yTop, xRight, yBottom) ; Punkt (xLeft, yTop) jest lewym górnym wierzchołkiem prostokšta, a (xRight, yBot- tom) prawym dolnym wierzchołkiem. Figura narysowana za pomocš funkcji Rec- tangle pokazana jest na rysunku 5-8. Boki prostokšta sš zawsze równoległe do krawędzi ekranu. xLeft xRight - yTop - yBottom Rysunek 5-8. Figura narysowana za pomocš funkcji Rectangle Programiœci, którzy zajmowali się już grafikš, znajš problem błędów zgubionego piksela. Niektóre systemy programowania grafiki rysujš figury obejmujšce pra- we i dolne współrzędne, a niektóre rysujš figury do tych współrzędnych, ale bez nich. Windows używa tego drugiego podejœcia, zaraz przedstawimy to proœciej. Rozważmy wywołanie funkcji Rectangle (hdc, l, 1, 5, 4) ; Wspomniałem wczeœniej, że Windows rysuje figurę w obramowaniu. WyobraŸ sobie, że ekran jest siatkš, która w każdym oczku mieœci jeden piksel. Wyimagi- nowane obramowanie jest rysowane na siatce, a prostokšt - we wnętrzu tego obramowania. Oto jak będzie wyglšdał rysunek: 138 Częœć I: Podstawy 0 1 2 3 4 Obszar rozdzielajšcy prostokšt od góry i lewej strony od obszaru roboczego ma szerokoœć 1 piksela. Jak wspominałem wczeœniej, Rectangle nie jest tylko funkcjš rysujšcš linie. GDI wypełnia także wnętrze obszaru. Ponieważ domyœlnie obszar wypełniany jest białym kolorem, to nie od razu jest oczywiste, że GDI go maluje. Gdy już wiesz, jak narysować prostokšt, bez trudu poradzisz sobie z elipsš, po- nieważ używa ona takich samych argumentów: Ellipse (hdc, xLeft, yTop, xRight, yBottom) ; Figura narysowana przy użyciu funkcji Ellipse jest pokazana (razem z wyimagi- nowanym obramowaniem) na rysunku 5-9. xLeff xRight i - yTop ţţţţţţţ-yBottom Rysunek 5-9. Figura narysowana za pomocš funkcji Ellipse Funkcja rysujšca prostokšt z zaokršglonymi wierzchołkami używa takiego sa- mego obramowania, jak funkcje Rectangle i Ellipse, ale wymaga jeszcze dwóch argumentów. RoundRect (hdc, xLeft, yTop, xRight, yBottom, xCornerEllipse, yCornerEllipse) ; Figura narysowana za pomocš tej funkcji pokazana jest na rysunku 5-10. xLeft xRight yTop - i , yBottom - - -, , r yCornerEllipse xComerEllipse Rysunek 5-10. Figura narysowana za pomocš funkcji RoundRect Rozdział 5: Podstawy grafiki 139 f Do narysowania zaokršglonych wierzchołków Windows używa małej elipsy. Sze- rokoœciš tej elipsy jest xCornerEllipse, a wysokoœciš-yCornerEllipse. Windows dzieli tę małš elipsę na ćwiartki i każdš z nich wstawia w odpowiedni wierzchołek. Zaokršglenie wierzchołka jest bardziej wyraŸne dla większych wartoœci xCorner- Ellipse i yCornerEllipse. Jeœli xCornerEllipse jest równe różnicy xLeft i xRight, a yCor- nerEllipse - różnicy yTop i yBottom, to funkcja RoundRect narysuje elipsę. Zaokršglony prostokšt z rysunku 5-10 został narysowany za pomocš elipsy o wymiarach obliczonych według wzoru: xCornerEllipse = (xRight - xLeft) / 4 ; yCornerEllipse = (yBottom - yTop) / 4 ; Jest to łatwe podejœcie, ale i wynik nie zachwyca, ponieważ zaokršglenie rogów jest bardziej wyraŸne przy dhxższym brzegu. Aby to poprawić, powinieneœ wy- równać wymiary xCornerEllipse i yCornerEllipse. Funkcje Arc, Chord i Pie pobierajš takie same argumenty: Arc (hdc, xLeft, yTop, xRight, yBottom, xStart, yStart. xEnd, yEnd) : Chord (hdc, xLeft, yTop. xRight, yBottom, xStart, yStart, xEnd. yEnd) ; Pie (hdc, xLeft, yTop, xRight, yBottom, xStart, yStart, xEnd, yEnd) ; Linia narysowana za pomocš funkcji Arc jest pokazana na rysunku 5-11; figury narysowane za pomocš funkcji Chord i Pie pokazane sš na rysunkach 5-12 i 5-13. Windows posługuje się wyimaginowanš liniš łšczšcš (xStart, yStart) ze œrodkiem elipsy. W miejscu, gdzie linia przecina elipsę, Windows rysuje hzk po obwodzie elipsy w kierunku przeciwnym do ruchu wskazówek zegara. Windows używa także wyimaginowanej linii do połšczenia (xEnd, yEnd) ze œrodkiem elipsy. W miej- scu, w którym ta linia przecina elipsę, Windows kończy rysować łuk. xStart xLett I xRight I yStart- .. -! - yTop 9 xEnd , ' : .; yEnd- . ....-rţ,-ţ..........-yBottom Rysunek 5-11. Linia narysowana za pomocš funkcji Arc xStart xLett I xRight I yStart- .., -yTop xEnd , ' .i ţ yEnd - ....... ţ..-..,ţ:r::.... - yBottom Rysunek 5-12. Figura narysowana za pomocš funkcji Chord 140 Częœć I: Podstawy xStart xLeft I xRight ( yStart- ţ-ţţ,-yTop xEnd , ; .Y yEnd , ' ţ.ţ-ţ.-ţ..,..r.-ţţţţ.- -yBottom Rysunek 5-13. Figura narysowana za pomocš funkcji Pie W przypadku funkcji Arc Windows kończy w tym miejscu działanie, ponieważ kreœli łuk i nie wypełnia żadnego obszaru. W przypadku funkcji Chord Windows łšczy końce łuku. W przypadku funkcji Pie Windows łšczy każdy z końców łuku ze œrodkiem elipsy. Wnętrze figur narysowanych za pomocš funkcji Chord i Pie jest wypełniane za pomocš aktualnego pędzla. Takie zastosowanie poczštkowego i końcowego położenia w funkcjach Arc, Chord i Pie może wydawać ci się dziwne. Dlaczego nie podawać po prostu poczštko- wego i końcowego punktu na obwodzie elipsy? Jest to możliwe, ale wtedy sam musiałbyœ wyliczyć ich dokładne położenie. Metoda zaproponowana w Windows nie wymaga takiej precyzji. Program LINEDEMO z rysunku 5-14 rysuje prostokšt, elipsę, prostokšt z zaokrš- glonymi wierzchołkami i dwie linie, ale w innej kolejnoœci. Program pokazuje, że funkcje definiujšce zamknięte obszary naprawdę je wypełniajš, ponieważ linie sš schowane za elipsš. Rezultat działania programu pokazany jest na rysunku 5-15. LINEDEMO.C /* LINEDEMO.C - Program demonstrujšcy rysowanie linii (c) Charles Petzold, 1998 */ ţţinclude LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) static TCHAR szAppName[] = TEXT ("LineDemo") HWND hwnd ; MS6 msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CS VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI ŽPPLICATION) ; Rozdział 5: Podstawy gţafiki 141 wndclass.hCursor = LoadCursor (NULL, IDC ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITEţBRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) ( MessageBox (NULL, TEXT ("Program requires Windows NT!"), szAppName, MBţICONERROR) ; ) hwnd = CreateWindow (szAppName, TEXT ("Line Demonstration"), WS_OIIERLAPPEDWINDOW, CW_USEDEFAULT, CW USEDEFAULT, CW_USEDEFAULT, CW USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) - ( TranslateMessage (&msg) ; DispatchMessage (&msg) ; 1 return msg.wParam ; 1 LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ( static int cxClient, cyClient ; HDC hdc ; PAINTSTRUCT ps ; switch (message) ( case WM_SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; Rectangle (hdc, cxClient / 8, cyClient / 8, 7 * cxClient / 8, 7 * cyClient / 8) ; MoveToEx (hdc, 0, 0, NULL) ; LineTo (hdc, cxClient, cyClient) ; I MoveToEx (hdc, 0, cyClient, NULL) ; LineTo (hdc, cxClienv, 0) ; j Ellipse (hdc, cxClient / 8, cyClient / 8, 7 * cxClient / 8, 7 * cyClient / 8) ; RoundRect (hdc, cxClient / 4, cyClient / 4, 3 * cxClient / 4, 3 * cyClient / 4, 142 Częœó I: Podstawy (cišg dalszy ze strony 141) cxClient / 4, cyClient / 4) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: PostOuitMessage (0) ; return 0 ; ) return DefWindowProc (hwnd, message, wParam, lParam) ; Rysunek 5-14. Program LINEDEMO Rysunek 5-15. Ekran programu LINEDEMO Krzywe sklejane Beziera Krzywe sklejane nazywane sš w żargonie programistów "splajnami" (ang. spli- ne), co oznacza elastyczny kawałek drewna, gumy lub metalu (znany też jako krzywik), stosowany do rysowania krzywych na papierze. Na przykład jeœli masz kilka punktów wykresu i chcesz narysować pomiędzy nimi krzywš (w celu in- terpolacji lub ekstrapolacji), najpierw zaznaczasz punkty na papierze milimetro- wym, następnie przykładasz krzywik do tych punktów i za pomocš ołówka ry- sujesz krzywš przebiegajšcš łagodnie przez podane punkty. Oczywiœcie, krzywe sklejane sš teraz opisane wzorami matematycznymi. Jest wiele rodzajów takich wzorów, ale krzywe sklejane Beziera zdobyły największš popu- larnoœć w programowaniu grafiki komputerowej. Stosunkowo niedawno zostały dodane do narzędzi graficznych dostępnych z poziomu systemu operacyjnego, a pochodzš z nieoczekiwanego Ÿródła: w latach szeœćdziesištych fabryka samocho- /* Rozdział 5: Podstawy grafiki 143 dów Renault przestawiała się z ręcznego projektowania karoserii samochodów (z wykorzystaniem modelowania z gliny) na projektowanie wspomagane kom- puterowo. Potrzebne były narzędzia matematyczne; wówczas Pierre Bózier opra- cował zestaw wzorów, które okazały się bardzo przydatne. Od tego czasu dwuwymiarowa postać krzywych sklejanych Beziera stała się naj- bardziej przydatnš krzywš (po linii prostej i elipsie) w grafice komputerowej. W PostScripcie krzywe Beziera sš używane do kreœlenia wszystkich hzków, na- wet linii eliptycznych. Krzywe Beziera sš też stosowane do definicji znaków czcio- nek postscriptowych. (TrueType używa prostszej i szybszej formy krzywych). Pojedyncza dwuwymiarowa krzywa Bóziera jest zdefiniowana przez cztery punk- ty - dwa punkty końcowe i dwa punkty kontrolne. Końce krzywej sš zakotwi- czane w dwóch punktach końcowych. Punkty kontrolne działajš jak magnesy, odcišgajšc krzywš od linii prostej łšczšcej dwu punkty końcowe. Najlepiej poka- zuje to program interakcyjny o nazwie BEZIER, przedstawiony na rysunku 5-16. BEZIER.C BEZIER.C - Pro9ram demonstrujdcy krzywe Bţziera (c) Charles Petzold, 1998 */ ţţinclude LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) static TCHAR szAppNameC] = TEXT ("Bezier") ; HWND hwnd ; MS6 msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CSţVREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) wndclass.hbrBackground = (HBRUSH) 6etStockObject (WHITE BRUSH) wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) ( MessageBox (NULL, TEXT ("Program requires Windows NT!"), szAppName, MBţICONERROR) ; return 0 ; hwnd = CreateWindow (szAppName, TEXT ("Bezier Splines"), WS OVERLAPPEDWINDOW, T 144 Częœć I: Podstawy I (cišg dalszy ze strony 244) CW_USEDEFAULT, CW USEDEFAULT, I CW_USEDEFAULT, CW USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) ( TranslateMessage (&msg) ; DispatchMessage (&msg) ; ) return msg.wParam ; ) void DrawBezier (HDC hdc, POINT aptCJ) PolyBezier (hdc, apt, 4) ; MoveToEx (hdc, aptCO7.x, aptC07.y, NULL) ; LineTo (hdc, aptCl].x, aptCl7.y) ; MoveToEx (hdc, aptC2J.x, aptC2J.y, NULL) ; LineTo (hdc, aptC37.x, aptC3J.y) ; ) LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) static POINT aptC47 ; HDC hdc ; int cxClient, cyClient ; PAINTSTRUCT ps ; switch (message) ( case WM_SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; aptCO].x = cxClient / 4 ; aptCO].y = cyClient / 2 ; aptClJ.x = cxClient / 2 ; aptCl7.y = cyClient / 4 ; aptC27.x = cxClient / 2 ; aptC2J.y = 3 * cyClient / 4 ; aptC3J.x = 3 * cxClient / 4 ; aptC3J.y = cyClient / 2 ; return 0 ; case WM_LBUTTONDOWN: case WM_RBUTTONDOWN: case WM_MOUSEMOVE: if (wParam & MK LBUTTON II wParam & MKţRBUTTON) Rozdział 5: Podstawy grafiki 145 hdc = GetDC (hwnd) ; SelectObject (hdc, GetStockObject (WHITEţPEN)) ; DrawBezier (hdc, apt) ; if (wParam & MK LBUTTON) ( apt[1].x = LOWORD (lParam) ; apt[lJ.y = HIWORD (lParam) ; if (wParam & MKţRBUTTON) ( aptC2].x = LOWORD (lParam) ; apt[2J.y = HIWORD (lParam) ; ) SelectObject (hdc, GetStockObject (BLACKţPEN)) ; DrawBezier (hdc, apt) ; ReleaseDC (hwnd, hdc) ; } return 0 ; case WM_PAINT: InvalidateRect (hwnd, NULL, TRUE) ; hdc = BeginPaint (hwnd, &ps) ; DrawBezier (hdc, apt) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: PostOuitMessage (0) ; return 0 ; return DefWindowProc (hwnd, message, wParam, lParam) ; Rysunek 5-16. Program BEZIEIZ Ponieważ program zawiera obsługę myszy, które to zagadnienie będziemy po- ruszać dopiero w rozdziale 7, nie będę omawiał teraz jego działania. Wykorzy- staj program do eksperymentów z krzywymi Bćziera. W tym programie dwa punkty końcowe sš ustawiane w połowie wysokoœci obszaru roboczego i w jed- nej czwartej i trzech czwartych jego szerokoœci. Dwa punkty kontrolne można prze- suwać, pierwszy przez wciœnięcie lewego przycisku myszy i jej przemieszczanie, drugi przez wciœnięcie prawego przycisku myszy i jej przemieszczanie. IZysunek 5-17 pokazuje typowy wyglšd ekranu. Oprócz samej krzywej Beziera, program rysuje też linię prostš od pierwszego punktu kontrolnego do pierwszego punktu końcowego (nazywanego też punk- tem poczštkowym) z lewej strony ekranu i od drugiego punktu kontrolnego do punktu końcowego z prawej strony ekranu. 146 Częœć I: Podstawy Krzywe Bóziera sš użyteczne w projektowaniu wspomaganym komputerowo z po- wodu pewnych specjalnych cech. Po pierwsze, przy nawet niewielkim doœwiad- czeniu, możesz prosto ukształtować krzywš w sposób zbliżony do pożšdanego. Rysunek 5-17. Ekran programu BEZIER Po drugie, przebieg krzywej Bóziera jest dokładnie wyznaczony. Niektóre krzy- we nie przechodzš przez żaden z punktów wyznaczajšcych krzywš. Krzywa Beziera jest zawsze zakotwiczona w dwóch punktach końcowych (jest to jedno z założeń potrzebnych do wyprowadzenia wzorów Bóziera). Niektóre krzywe majš specjalne punkty, w których krzywa gwałtownie ucieka w nieskończonoœć. W projektowaniu komputerowym jest to dosyć niepożšdane. Krzywa Bóziera ni- gdy tego nie robi; zawsze jest ograniczona przez czworokšt (zwany "powłokš wypukłš"), utworzony przez połšczenie punktów końcowych i kontrolnych. Trzeciš cennš właœciwoœciš krzywych Bóziera jest zwišzek między punktami końco- wymi i kontrolnymi. Krzywa ta jest zawsze styczna, i to w tym samym kienznku, do odcinka łšczšcego punkt poczštkowy z pierwszym punktem kontrolnym (widać to na przykładzie programu BEZIER). Krzywa ta jest także styczna, w tym samym kie- nmku, do odcinka łšczšcego drugi punkt kontrolny z punktem końcowym. Sš to dwa dodatkowe założenia używane przy wyprowadzaniu wzorów Beziera. Po czwarte, krzywa Beziera jest najczęœciej po prostu ładna. Wiem, że jest to kry- terium subiektywne, ale nie tylko ja tak myœlę. Przed 32-bitowymi wersjami Windows trzeba było kreœlić krzywe Beziera za po- mocš funkcji Polyline. Potrzebna do tego była znajomoœć równań parametrycz- nych krzywej Bóziera. Punktem poczštkowym jest (xo, yo), a punktem końcowym (x3, y3). Dwoma punktami kontrolnymi sš (x1, yl) i (x2, y2). Krzywa jest rysowana dla wartoœci t od 0 do 1: x(t) = (1 - t)3 x + 3t (1 - t)2 xl + 3tz (1 - t) x + t' x y(t) = (1 - t)3 yo + 3t (1 - t)z y, + 3tz (1 - t) yz + t3 y3 Rozdział 5: Podstawy grafiki 147 Programujšc w Windows 98 nie musisz znać tych wzorów. Aby narysować jednš lub kilka połšczonych krzywych Beziera, wywołaj po prostu: PolyBezier (hdc, apt, iCount) ; lub Poly6ezierTo (hdc, apt, iCount) ; W obu przypadkach apt to tablica struktur POINT. W funkcji PolyBezier pierwsze cztery punkty oznaczajš odpowiednio punkt poczštkowy, pierwszy punkt kon- trolny, drugi punkt kontrolny i punkt końcowy pierwszej krzywej Bóziera. Każ- da następna krzywa wymaga już tylko trzech dodatkowych punktów, ponieważ punkt poczštkowy drugiej krzywej Beziera jest taki sam jak punkt końcowy pierw- szej krzywej, i tak dalej. Argument iCount zawsze wynosi jeden plus potrojona liczba połšczonych krzywych, które rysujesz. Funkcja PolyBezierTo używa pozycji bieżšcej jako punktu poczštkowego. I pierw- sza, i każda następna krzywa Beziera wymaga tylko trzech punktów. Przy po- wrocie z funkcji pozycja bieżšca pokrywa się z ostatnim punktem końcowym. Jedna uwaga: gdy rysujesz serię połšczonych krzywych Beziera, miejsce łšczenia będzie gładkie tylko wtedy, jeœli drugi punkt kontrolny pierwszej krzywej Bezie- ra, punkt końcowy pierwszej krzywej (który jest także punktem poczštkowym drugiej krzywej Beziera) i pierwszy punkt kontrolny drugiej krzywej sš współli- niowe, czyli leżš na tej samej prostej. Używanie piór standardowych Gdy wywohxjesz dowolnš z przedstawionych powyżej funkcji rysujšcych linie, Windows kreœli linię "piórem", które jest aktualnie wybrane w kontekœcie urzš- dzenia. Pióro okreœla kolor linii, jej szerokoœć i styl, który może być cišgły, krop- kowany lub przerywany. Domyœlne pióro kontekstu urzšdzenia zwane jest BLACK PEN. Pióro to rysuje cišgłš czarnš linię o gruboœci jednego piksela. BLACK PEN jest jednym z trzech standardowych piór, które występuje w Win- dows. Pozostałe dwa to WHTTE PEN i NULL PEN. NULL PEN jest piórem, które nie rysuje. Możesz także tworzyć swoje własne pióra. W programach Windows sięgasz po pióro, podajšc jego uchwyt. Plik nagłówko- wy Windows WINDEF.H definiuje typ HPEN - uchwyt pióra. Możesz zdefinio- wać zmiennš (na przykład hPen) w ten sposób: HPEN hPen ; Wywohzjšc GetStockObject pobierasz uchwyt jednego ze standardowych piór . Przypuœćmy, że chcesz użyć pióra zwanego WHI'TE-PEN. Uchwyt pióra otrzy- masz w następujšcy sposób: hPen = GetStockObject (WHITEţPEN) ; Teraz musisz "wybrać" to pióro w kontekœcie urzšdzenia. SelectObject (hdc, hPen) ; Teraz białe pióro jest piórem aktualnym. Po tym wywołaniu wszystkie linie, któ- re narysujesz, będš używały pióra WHITĘ PEN do czasu, aż nie wybierzesz in- nego pióra w kontekœcie urzšdzenia lub nie zwolnisz uchwytu kontekstu urzš- dzenia. 148 Częœć I: Podstawy Zamiast definiować zmiennš hPen, możesz połšczyć wywołania GetStockObject i SelectObject w jednš instrukcję: SelectObject (hdc, GetStockObject (WHITEţPEN)) ; Jeœli chcesz znowu używać BLACK PEN, możesz pobrać uchwyt tego standar- dowego obiektu i wybrać go w kontekœcie urzšdzenia, stosujšc tylko jednš in- strukcję: SelectObject (hdc, GetStockObject (BLACK PEN)) : SelectObject zwraca uchwyt pióra, które było poprzednio wybrane w kontekœcie urzšdzenia. Jeœli weŸmiesz nowy kontekst urzšdzenia i wywołasz hPen = SelectObject (hdc, GetStockobject (WHITEţPEN)) ; aktualnym piórem w kontekœcie urzšdzenia stanie się WHITE PEN, a zmienna hPen będzie zawierała uchwyt pióraBLACK PEN. Możesz potem wybrać BLACK PEN w kontekœcie urzšdzenia, wywołujšc SelectObject (hdc, hPen) ; Tworzenie, wybieranie i usuwanie piór Pióra zdefiniowane jako obiekty standardowe na pewno sš wygodne, niosš jed- nak ze sobš ograniczenie: masz do dyspozycji tylko pióro czarne, białe lub żad- ne. Jeœli potrzebujesz czegoœ więcej, musisz utworzyć własne pióra. Oto ogólna procedura: tworzysz "pióro logiczne" będšce tylko opisem pióra , używajšc funkcji CreatePen lub CreatePenlndirect. Te funkcje zwracajš uchwyt pióra logicznego. Wybierasz pióro w kontekœcie urzšdzenia przez wywołanie SelectO- bject. Możesz wtedy tym nowym piórem rysować linie. W danej chwili w kontek- œcie urzšdzenia może być wybrane tylko jedno pióro. Po zwolnieniu kontekstu urzšdzenia (lub po wybraniu w kontekœcie urzšdzenia innego pióra) możesz usu- nšć utworzone pióro logiczne, wywołujšc DeleteObject. Gdy to zrobisz, uchwyt pióra nie jest już ważny. Pióro logiczne jest "obiektem GDI", jednym z szeœciu obiektów GDI, które pro- gram może utworzyć. Pozostałe pięć to pędzle, bitmapy, regiony, czcionki i pale- ty. Poza paletami, każdy z tych obiektów jest wybierany w kontekœcie urzšdze- nia za pomocš SelectObject. Stosowanie takich obiektów GDI jak pióra podlega trzem zasadom: ů powinieneœ usuwać wszystkie obiekty GDI, które ewentualnie utworzyłeœ; ů nie usuwaj obiektów GDI, dopóki sš one wybrane w ważnym kontekœcie urzš- dzenia; ů nie usuwaj standardowych obiektów. Nie sš to wygórowane wymagania, ale czasami mogš być kłopotliwe. Omówimy kilka przykładów, żebyœ zrozumiał, o co tu chodzi. Ogólna składnia funkcji CreatePen wyglšda następujšco: hPen = CreatePen (iPenStyle, iWidth, crColor) : Argument iPenStyle okreœla, czy pióro rysuje linię cišgłš, czy linię przerywanš, składajšcš się z kropek lub kresek. Argumentem może być jeden z następujšcych identyfikatorów, zdefiniowanych w WINGDI.H. Rozdział 5: Podstawy grafiki 149 Rysunek 5-18 pokazuje rodzaj linii tworzony przez każdy z tych stylów. PS SOLID PS DASH - - - - - PS DOT -------- PS DASHDOT ------- PS DASHDOTDOT ------- PS NULL PS INSIDEFRAME Rysunek 5-18. Siedem stylów pióra Dla stylów PS SOLID, PS NULL i PS INSIDEFRAME argument iWidth jest gru- boœciš pióra. Wartoœć iWidth równa 0 powoduje, że Windows stosuje pióro o gru- boœci jednego piksela. Standardowe pióra też majš gruboœć 1 piksela. Jeœli dla kropkowego lub kreskowego stylu pióra okreœlisz gruboœć większš niż 1, to Win- dows użyje stylu cišgłego. Argument crColor to wartoœć COLORREF okreœlajšca kolor pióra. Dla wszystkich stylów piór z wyjštkiem PS INSIDEFRAME, kiedy wybierzesz pióro w kontek- œcie urzšdzenia, Windows zamienia kolor na najbliższy czysty kolor, który jest dostępny w kontekœcie urzšdzenia. PS INSIDEFRAME jest jedynym stylem pió- ra, który pozwala na używanie symulowanego koloru, i to tylko przy gruboœci pióra większej niż 1. Styl PS INSIDEFRAME ujawnia jeszcze jednš dziwnš właœciwoœć, gdy używa się go z funkcjami tworzšcymi wypełnione obszary. Przy wszystkich stylach piór, poza PS INSIDEFRAME, jeœli pióro używane do tworzenia obrysu ma gruboœć większš niż 1 piksel, œrodek pióra znajduje się na brzegu, więc częœć linii może wystawać poza obramowanie. Przy stylu PS INSIDEFRAME cała linia jest ryso- wana wewnštrz obramowania. Możesz także utworzyć pióro przez zainicjowanie struktury LOGPEN ("pióro logicz- ne") i wywołanie CreatePenlndirect. Jeœli program używa wielu różnych piór, które irucjujesz w kodzie Ÿródłowym, ta metoda jest prawdopodobnie bardziej efektywna. Aby użyć CreatePenlndirect, musisz zdefiniować najpierw strukturę typu LOGPEN: LOGPEN logpen ; Ta struktura ma trzy składniki: IopnStyle (liczba całkowita bez znaku, czyli UINT) jest stylem pióra, IopnWidth (struktura POIN'T) jest gruboœciš pióra w jednostkach logicznych, a IopnColor (COLORREF) jest kolorem pióra. Windows używa tylko pola x struktury lopnWidth do ustawiania gruboœci pióra; pole y jest ignorowane. Aby utworzyć pióro, należy przekazać adres struktury do CreatePenlndirect: hPen = CreatePenIndirect (&logpen) ; Zauważ, że funkcje CreatePen i CreatePenlndirect nie wymagajš uchwytu kontek- stu urzšdzenia. Te funkcje tworzš pióra Iogiczne, które nie majš powišzania z kon- tekstem urzšdzenia dopóty, dopóki nie wywołasz SelectObject. Możesz używać tego samego pióra logicznego do wielu różnych urzšdzeń, takich jak ekran lub drukarka. 150 Częœć I: Podstawy Oto metoda tworzenia, wybierania i usuwania piór. Załóżmy, że twój program używa trzech piór: pióra czamego o szerokoœci 1, czerwonego o szerokoœci 3 i czar- nego dajšcego linię kropkowanš. Najpierw zdefiniuj zmienne statyczne przecho- wujšce uchwyty tych trzech piór: static HPEN hPenl, hPen2, hPen3 ; Podczas przetwarzania WMţCREATE, utwórz te trzy pióra: hPenl = CreatePen (PS_SOLID, 1, 0) ; hPen2 = CreatePen (PS_SOLID, 3, RGB (255, 0, 0)) ; hPen3 = CreatePen (PSţDOT, 0, 0) ; Podczas przetwarzania WMţPAINT (lub wtedy, gdy będziesz miał prawidłowy uchwyt kontekstu urzšdzenia) wybierz jedno z tych piór w kontekœcie urzšdze- nia i rysuj nim: SelectObject (hdc, hPen2) ; C funkcje rysujdce linie ] SelectObject (hdc, hPenl) ; C funkcje rysujdce linie ] Podczas przetwarzania WMţDESTROY, usuń te trzy pióra, które wczeœniej utwo- rzyłeœ: Delete0bject (hPenl) ; Delete0bject (hPen2) ; ; Delete0bject (hPen3) ; Jest to najprostsza metoda tworzenia, wybierania i usuwania piór, ale oczywiœcie twój program musi wiedzieć, jakich piór będzie potrzebował. Możesz też two- rzyć pióra podczas przetwarzania komunikatu WMţPAINT i usuwać je po wy- wołaniu EndPaint. (Możesz usunšć je przed wywołaniem EndPaint, ale musisz ! uważać, aby nie usunšć pióra aktualnie wybranego w kontekœcie urzšdzenia). Możesz tworzyć pióra "w locie" i połšczyć wywołania CreatePen i SelectObject ' w jednš instrukcję: SelectObject (hdc, CreatePen (PS DASH, 0, RGB (255, 0, 0))) ; Teraz, rysujšc linie, będziesz używał czerwonego pióra kreskowanego. Gdy skoń- czysz rysować przerywane czerwone linie, możesz usunšć pióro. Aha! Jak moż- na usunšć pióro, gdy nie zachowałeœ uchwytu pióra? Przypomnij sobie, że Selec- tObject zwraca uchwyt pióra, które było poprzednio wybrane w kontekœcie urzš- dzenia. Oznacza to, że możesz skasować to pióro, wybierajšc standardowe pióro BLACK PEN w kontekœcie urzšdzenia i usuwajšc wartoœć zwróconš przez Select- Object: Delete0bject (SelectObject (hdc, GetStockObject (BLACK PEN))) : Oto inny sposób. Gdy wybierasz pióro w nowo utworzonym kontekœcie urzš- dzenia, zapisz uchwyt pióra zwracany przez SelectObject: hPen = SelectObject (hdc, CreatePen (PSţDASH, 0, RGB (255, 0, 0))) ; Czym jest hPen? Jeżeli jest to pierwsze wywołanie SelectObject od pobrania kon- tekstu urzšdzenia, hPen jest uchwytem standardowego obiektu BLACK PEN. Możesz teraz wybrać to pióro w kontekœcie urzšdzenia i skasować pióro utwo- rzone przez siebie (uchwyt zwrócony z drugiego wywołania SelectObject) za po- ! mocš jednej instrukcji: Rozdział 5: Podstawy grafiki 151 Delete0bject (SelectObject (hdc, hPen)) ; Jeœli masz uchwyt do pióra, możesz pobrać wartoœci pól struktury LOGPEN, wywołujšc GetObject: GetObject (hPen, sizeof (LOGPEN), (LPVOID) &logpen) ; Jeœli chcesz otrzymać uchwyt pióra, które jest aktualnie wybrane w kontekœcie urzšdzenia, wywołaj: hPen = GetCurrentObject (hdc, OBJţPEN) ; Innš funkcję tworzšcš pióro, ExtCreatePen, omówię w rozdziale 17. Zapełnianie przerw Używanie piór kropkowanych i przerywanych nasuwa pytanie: co się dzieje z prze- rwami pomiędzy kropkami i kreskami? A co chciałbyœ, żeby się działo? Kolor przerw zależy od dwóch atrybutów kontekstu urzšdzenia - trybu i koloru tła. Domyœlnym trybem tła jest OPAQUE, co oznacza, że Windows wypełnia prze- rwy kolorem tła, który domyœlnie jest biały. Jest to zgodne z kolorem pędzla WHIT'E BRUSH, którego wiele programów używa w klasie okna do czyszczenia tła okna. Możesz zmienić kolor tła, którego Windows używa do wypełniania przerw, wy- wołujšc SetBkColor (hdc, crColor) ; Podobnie jak to było z argumentem crColor używanym jako kolor pióra, Windows zamienia ten kolor tła na najbliższy czysty kolor. Możesz pobrać aktualny kolor tła, zdefiniowany w kontekœcie urzšdzenia, wywołujšc GetBkColor. Możesz także zapobiec wypełnianiu przerw poprzez zmianę trybu tła na TRANS- PARENT: SetBkMode (hdc, TRANSPARENT) ; Windows będzie wtedy ignorował kolor tła i nie wypełni przerw. Możesz pobrać aktualny tryb tła (czyli TftANSPARENT albo OPAQUE), wywołujšc GetBkMode. Tryby rysowania Na wyglšd linii na ekranie wpływa także tryb rysowariia zdefiniowany w kon- tekœcie urzšdzenia. WyobraŸ sobie rysowanie linii, której kolor zależy nie tylko od koloru pióra, ale także od koloru obszaru ekranu, na którym linia jest rysowa- na. WyobraŸ sobie, że mógłbyœ się posłużyć tym samym piórem do rysowania czarnych linii na białym tle i białych linii na czarnym tle, ruc nie wiedzšc o aktu- alnym kolorze tła. Czy taka właœciwoœć może być przydatna? Jest ona możliwa dzięki trybom rysowania. Kiedy Windows używa pióra do rysowania linii, w rzeczywistoœci wykonuje bi- towš operację logicznš na pikselach pióra i pikselach wyœwietlonych w docelo- wym miejscu ekranu, gdzie piksele okreœlajš kolor pióra i kolor powierzchni ekra- nu. Wykonywanie bitowej operacji logicznej na pikselach jest nazywane działa- niem rastrowym (ang. raster operation, ROP). Ponieważ rysowanie linii wymaga tylko dwóch wzorców pikseli (pfksela pióra i piksela docelowego), operacja lo- 152 Częœć I: Podstawy giczna jest nazywana "dwójkowym działaniem rastrowym" (ang. binary raster operation, ftOP2). Windows definiuje 16 kodów ROP2, wskazujšcych jak łšczone sš piksele pióra i docelowe. W domyœlnym kontekœcie urzšdzenia trybem ryso- wania jest R2 COPYPEN, co oznacza, że Windows po prostu kopiuje piksele pióra na piksele docelowe, jakbyœ rysował zwykłym piórem. Jednak istnieje jeszcze 15 innych kodów ROP2. Skšd wzięło się te 16 różnych kodów ROP2? Dla uproszczenia załóżmy, że mamy system monochromatyczny używajšcy 1 bita na piksel. Kolor docelowy (kolor obszaru roboczego okna) może być czarny (wtedy piksel ma wartoœć 0) lub biały (piksel o wartoœci 1). Pióro też może być czame albo białe. Mamy wtedy cztery kombinacje użycia czarnego albo białego pióra do rysowania na czarnym albo białym tle: białe pióro na białym tle, białe pióro na czarnym tle, czarne pióro na białym tle i czame pióro na czarnym tle. Jaki będzie kolor docelowy po narysowaniu lirui piórem? Jednš z możliwoœci sta- nowi rysowanie linii na czarno, niezależnie od koloru pióra lub tła. Ten tryb ryso- warua jest wskazywany przez kod R2ţBLACK. Inna możliwoœć: linia jest czarna z wyjštkiem przypadku, gdy pióro i tło sš czarne, wtedy linia jest rysowana na bia- ło. Chociaż może to być dziwne, ale Windows ma nazwę dla tego trybu. Jest to tryb rysowania R2 NOTMERGEPEN. Windows wykonuje operację alternatywy bitowej (OR) na pikselach tła i pikselach pióra, a następnie neguje wynik. Poniższa tabela zawiera wszystkie 16 kodów trybów rysowania ROP2. Tabela wskazuje jak kolory pióra (P) i docelowy (D) sš ostatecznie łšczone. W kolumnie o nazwie "Operacja logiczna" używam notacji C do pokazania działań wykony- wanych na pikselach. Pióro (P): 1 1 0 0 Operacja Tryb Tło (D): 1 0 1 0 logiczna rysowania Wyniki: 0 0 0 0 0 R2ţBLACK 0 0 0 1 ~(P i D) R2ţNOTMERGEPEN 0 0 1 0 ~P & D R2ţMASKNOTPEN 0 0 1 1 ~P R2 NOTCOPYPEN 0 1 0 0 P & ~D R2 MASKPENNOT 0101 ~D R2ţNOT 0 1 1 0 P ^ D R2 XORPEN 0 1 1 1 ~(P & D) R2ţNOTMASKPEN 1 0 0 0 P & D R2 MASKPEN 1 0 0 1 ~(P ^ D) R2 NOTXORPEN 1010 D R2ţNOP 1 0 1 1 ~P i D R2ţMERGENOTPEN 1 1 0 0 P R2 COPYPEN (domyœlny) 1 1 0 1 P ; ~D R2ţMERGEPENNOT 1 1 1 0 P ; D R2ţMERGEPEN 1 1 1 1 1 lZ2ţWHITE Rozdział 5: Podstawy grafiki 153 Możesz ustawić nowy tryb rysowania dla kontekstu urzšdzenia przez wywołanie SetROP2 (hdc, iDrawMode) ; Argument iDrawMode jest jednš z wartoœci wymienionych w tabeli w kolumnie "Tryb rysowania". Informację o aktualnym trybie rysowania możesz uzyskać wy- wołujšc funkcję: iDrawMode = GetROP2 (hdc) ; Domyœlnym kontekstem urzšdzenia jest R2 COPYPEN, czyli proste przeniesie- nie koloru pióra na docelowe miejsce. Tryb ft2 NOTCOPYPEN rysuje na biało, jeœli pióro ma kolor czarny i na czamo, jeœli pióro ma kolor biały. Tryb R2.ţBLACK zawsze rysuje na czarno, niezależnie od koloru pióra albo tła. Podobnie, tryb R2 WHITE zawsze rysuje na biało. Tryb R2 NOP oznacza "brak działania" - pozostawia tło niezmienione. Omawialiœmy tryby rysowania na przykładzie systemu monochromatycznego. Jednak najczęœciej mamy do czynienia z systemami kolorowymi. W systemach kolorowych Windows wykonuje operacje bitowe trybu rysowania dla każdego bitu koloru pióra i docelowego piksela i ponownie używa 16 kodów ROP2 opisa- nych w poprzedniej tabeli. Okreœlajšc kolor linii tryb rysowania R2 NOT zawsze odwraca kolor tła, nie zważajšc na kolor pióra. Na przykład na turkusowym tle będzie rysowana karmazynowa linia. Tryb R2 NOT zawsze daje widoczne pió- ro, z wyjštkiem rysowania na ciemnoszarym tle. Zastosowanie trybu rysowania R2-NOT pokażę w programach BLOKOUT w rozdziale 7. Rysowanie wypełnionych obszarów Następnym krokiem po rysowaniu linii jest wypełnianie zamkniętych obszarów. W poniższej tabeli wymieniam siedem funkcji, które shzżš Windows do rysowa- nia figur wypełnionych razem z obramoţ.vaniem. Funkcja Figura Rectangle Prostokšt z ostrymi narożnikami Ellipse Elipsa RoundRect Prostokšt z zaokršglonymi narożnikami Chord Łuk po obwodzie elipsy z końcami połšczonymi cięciwš Pie Klin - łuk po obwodzie elipsy z końcami połšczonymi ze œrodkiem elipsy Polygon Wielokšt PolyPolygon Kilka wielokštów Windows rysuje kontur figury piórem aktualnie wybranym w kontekœcie urzš- dzenia. Podczas rysowania konturu używany jest aktualny tryb tła, kolor tła i tryb rysowania, podobnie jak podczas rysowania linii. Wszystko, czego nauczyliœmy się o liniach, dotyczy konturów figur. Figura jest wypełniana pędzlem aktualnie wybranym w kontekœcie urzšdzenia. Domyœlnie jest to standardowy pędzel o nazwie WHITE BRUSH, co oznacza, że 154 Częœć I: Podstawy wnętrze będzie pomalowane na biało. Windows definiuje szeœć standardowych pędzli: WHITE BRUSH, LTGRAY_BRUSH, GRAY BRUSH, DKGRAY BRUSH, BLACK BRUSH i NULL BRUSH (lub HOLLOW BRUSH). Możesz wybrać je- den ze standardowych pędzli w kontekœcie urzšdzenia w ten sam sposób, jak wybierasz standardowe pióro. Windows definiuje HBRUSH jako uchwyt pędzla, więc możesz najpierw zdefiniować zmiennš dla uchwytu pędzla: HBRUSH hBrush ; Możesz otrzymać uchwyt pędzla GRAY BRUSH przez wywołanie funkcji Get- StockObject: hBrush = GetStockObject (GRAY BRUSH) ; Możesz wybrać go w kontekœcie urzšdzenia przez wywołanie funkcji SelectObject: SelectObject (hdc, hBrush) ; Jeœli teraz będziesz rysował jednš z wymienionych powyżej figur, jej wnętrze będziesz szare. Aby narysować figurę bez konturu, wybierz NULL PEN w kontekœcie urzšdze- nia: SelectObject (hdc, GetStockObject (NULLţPEN)) ; Jeœli chcesz narysować kontur figury bez wypełniania wnętrza, wybierz NULL BRUSH w kontekœcie urzšdzenia: SelectObject (hdc, GetStockobject (NULLţBRUSH) ; Możesz także utworzyć zmodyfikowany pędzel, podobnie jak tworzyłeœ zmody- fikowane pióra. Omówimy to niebawem. Funkcja Polygon i tryb wypełniania wielokšta Omówiliœmy już pierwsze pięć funkcji wypełniajšcych obszary. Wielokšt jest szó- stš funkcjš, która rysuje kontur i wypełniania figurę. Wywołanie funkcji jest po- dobne do wywołania funkcji Polyline: Polygon (hdc, apt, iCount) ; Argument apt jest tablicš struktur POINT, a iCount jest liczbš punktów. Jeœli ostatni punkt w tej tablicy jest różny od pierwszego punktu, Windows dodaje innš linię, łšczšcš ostatni punkt z pierwszym. (To się nie zdarza przy funkcji Polyline). Funkcja PolyPolygon wyglšda następujšco: PolyPolygon (hdc, apt, aiCounts, iPolyCount) ; Funkcja rysuje kilka wielokštów. Liczba rysowanych wielokštów jest podana w ostatnim argumencie. Dla każdego wielokšta tablica aiCounts podaje liczbę punktów w wielokšcie. Tablica apt zawiera wszystkie punkty dla wszystkich wie- lokštów. Oprócz zwracanej wartoœci, PolyPolygon jest równoważne funkcjonalnie z następujšcym kodem: for (i = 0. iAccum = 0 ; i < iPolyCount ; i++) ( Polygon (hdc, apt + iAccum, aiCounts[i7) ; iAccum += aiCounts[i] ; Obie funkcje, Polygon i PolyPolygon, powodujš, że Windows maluje ograniczony obszar aktualnym pędzlem, zdefiniowanym w kontekœcie urzšdzenia. Sposób Rozdział 5: Podstawy grafiki . 155 wypełniania wnętrza zależy od trybu wypełniania wielokšta, który możesz usta- wić za pomocš funkcji SetPolyFillMode: SetPolyFillMode (hdc, iMode) ; Domyœlnym trybem wypełniania wielokšta jest ALTERNATE, ale możesz ustawić tryb WINDING. Różnica między nimi pokazana jest na rysunku 5-19. Rysunek 5-19. Figury rysowane za pomocš dwóch trybów wypełniania wielokš- tów: ALTERNATE (po lewej) i WINDING (po prawej) Poczštkowo różnica między trybami ALTERNATE i WINDING wydaje się oczy- wista. W trybie ALTERNATE możesz wyobrazić sobie linię narysowanš od punktu w zamkniętym obszarze do nieskończonoœci. Zamknięty obszar jest wypełniany tylko wtedy, jeœli wyobrażona linia przecina nieparzystš iloœć linii ograniczajš- cych. Dlatego ramiona gwiazdy sš wypełnione, a œrodek nie. Przykład pięcioramiennej gwiazdy sprawia, że tryb WINDING wyglšda znacz- nie proœciej niż w rzeczywistoœci. Gdy rysujesz pojedynczy wielokšt, w większo- œci przypadków tryb WINDING spowoduje wypełnienie wszystkich zamkniętych obszarów wielokšta. Ale istniejš wyjštki. Aby okreœlić, czy zamknięty obszar jest wypełniany w trybie WINDINGD, po- nownie wyobraŸ sobie linię narysowanš od punktu w zamkniętym obszarze do ! nieskończonoœci. Jeœli wyobrażona linia przecina nieparzystš iloœć razy linie ograniczajšce, obszar jest wypełniany, podobnie jak w trybie ALTERNATE. Jeœli wyobrażona linia przecina linie ograniczajšce parzystš iloœć razy, obszar może być wypełniony lub nie. Obszar będzie wypełniony, jeœli iloœć linii ograniczajš- cych biegnšcych w jednym kierunku (względem wyobrażonej linii) nie jest rów- na liczbie linii ograniczajšcych biegnšcych w innym kierunku. Jako przykład rozważmy obiekt pokazany na rysunku 5-20. Strzałki wskazujš ' kierunek rysowania linii. Oba tryby powinny wypełnić trzy zamknięte obszary w kształcie litery L, oznaczone numerami od 1 do 3. Dwa małe obszary wewnętrz- ne, oznaczone 4 i 5, nie będš wypełnione w trybie ALTERNATE. Ale w trybie WINDING obszar 5 zostanie wypełniony, ponieważ musisz przekroczyć dwie linie biegnšce w tym samym kierunku, by wydostać się z wnętrza obszaru na zewnštrz figury. Obszar 4 nie zostanie wypełniony. Musisz znowu przekroczyć dwie linie, , ale biegnšce w przeciwnych kierunkach. 156 Częœć I: Podstawy Jeœli wštpisz, że Windows potrafi to zrobić, program ALTWIND na rysunku 5-21 pokazuje, że jednak potrafi. Rysunek 5-20. Figura, w której tryb WINDING nie wypełnia wszystkich we- wnętrznych obszarów ALTWIND.C /* ALTWIND.C - Tryby wypelniania ALTERNATE i WINDING (c) Charles Petzold, 1998 */ ţkinclude LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) ( static TCHAR szAppName[J = TEXT ("AltWind") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ( CS VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC ŽRROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITEţBRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) MessageBox (NULL, TEXT ("Program requires Windows NT!"),. szAppName, MBţICONERROR) Rozdział 5: Podstawy grafiki 157 return 0 ; 1 hwnd = CreateWindow (szAppName, TEXT ("Alternate and Winding Fill Modes"). WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CWţUSEDEFAULT, NULL, NULL, hInstance, NULL) : ShowWindow (hwnd, iCmdShow) : UpdateWindow (hwnd) : while (GetMessage (&msg, NULL, 0, 0)) ,F', TranslateMessage (&msg) ; DispatchMessage (&msg) ; ) return msg.wParam ; 1 LRESULT CALLBACK WndProc (HWND hwnd, UINT message. WPARAM wParam, LPARAM lParam) ( static POINT aptFigure C10] = ( 10,70, 50.70, 50,10, 90,10, 90,50, 30,50, 30,90, 70,90, 70,30, 10,30 ): static int cxClient, cyClient ; HDC hdc : "'.F int i ; PAINTSTRUCT ps : POINT aptClO] : switch (message) ( case WM_SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) : SelectObject (hdc, GetStockObject (GRAYţBRUSH)) ; for (i = 0 ; i < 10 ; i++) apt[i].x = cxClient * aptFigureCi].x / 200 ; aptCi].y = cyClient * aptFigure[i].y / 100 ; SetPolyFillMode (hdc. ALTERNATE) : Polygon (hdc, apt, 10) ; for (i = 0 ; i < 10 ; i++) ( apt[i].x += cxClient / 2 ; SetPolyFillMode (hdc, WINDING) ; Polygon (hdc, apt, 10) : EndPaint (hwnd, &ps) : return 0 : T 158 Częœć I: Podstawy (cišg dalszy ze strony 157) case WMţDESTROY: PostOuitMessage (0) ; return 0 ; ) return DefWindowProc (hwnd, message, wParam, lParam) ; Rysunek 5-21. Program ALTWIND Współrzędne figur - przeskalowane do obszaru 100 na 100 jednostek - sš prze- chowywane w tablicy aptFigure. Te współrzędne sš skalowane na podstawie sze- rokoœci i wysokoœci obszaru roboczego. Program rysuje figurę dwa razy, najpierw używajšc trybu wypełniania ALTERNATE, a następnie używajšc trybu wypeł- niania WáVDING. Wyniki pokazane sš na rysunku 5-22. Rysunek 5-22. Ekran z wynikami programu ALTWIND Malowanie wnętrza Wnętrza figur Rectangle, RoundRect, Ellipse, Chord, Pie, Polygon i PolyPolygon wy- pełniane sš aktualnym pędzlem (zwanym także czasem "wzorcem" - ang. pat- tern), wybranym w kontekœcie urzšdzenia. Pędzel jest małš bitmapš o wielkoœci 8 na 8 pikseli, której wzór jest powtarzany poziomo i pionowo aż do wypełnienia obszaru. Gdy Windows wykorzystuje symulowanie do wyœwietlenia większej liczby ko- lorów, niż ta normalnie dostępna na ekranie, używa pędzla o tym kolorze. Na wyœwietlaczu monochromatycznym Windows używa symulowania do stworze- nia 64 różnych odcieni szaroœci z czarnych i białych pikseli. Dokładniej, Windows może stworzyć 64 różne monochromatyczne pędzle. Jeœli pędzel jest czarny , Rozdział 5: Podstawy grafiki 159 wszystkie bity w bitmapie 8 na 8 sš ustawione na 0. Jeden bit z 64 ustawiony na 1 (czyli biały) daje pierwszy odcień szaroœci, dwa bity sš białe przy drugim od- cieniu szaroœci i tak dalej, aż wszystkie bity bitmapy 8 na 8 będš równe 1 i wtedy otrzymamy czystš biel. W 16-kolorowym lub 256-kolorowym systemie grafiki symulowane kolory sš także pędzlami, a Windows może wyœwietlić znacznie więcej kolorów niż normalnie jest dostępne. Windows ma pięć funkcji pozwalajšcych tworzyć pędzle logiczne. Możesz po- tem wybrać taki pędzel w kontekœcie urzšdzenia za pomocš SelectObject. Tak jak pióra logiczne, tak pędzle logiczne sš obiektami GDI. Każdy pędzel, który stwo- rzysz, musi być usunięty, ale nie można go usunšć, dopóki jest wybrany w kon- tekœcie urzšdzenia. Oto funkcja tworzšca pędzel logiczny: hBrush = CreateSolidBrush (crColor) ; Słowo Solid w tej funkcji nie oznacza, że pędzel ma czysty kolor. Gdy wybierzesz ten pędzel w kontekœcie urzšdzenia, Windows może stworzyć bitmapę z symu- lowanymi kolorami i używać jej jako pędzla. Możesz także stworzyć pędzel ze wzorami z poziomych, pionowych lub ukoœnych linii. Pędzle z tymi stylami sš najczęœciej używane do kolorowania wnętrz shxp- ków na wykresach i do kreœlenia na ploterach. Funkcjš tworzšcš pędzel ze wzo- rem jest hBrush = CreateHatchBrush (iHatchStyle, crColor) : Argument iHatchStyle okreœla wyglšd wzoru. Rysunek 5-23 pokazuje szeœć do- stępnych stałych stylów wzorów. HS HORIZONTAL HS BDIAGONAL HS VERTICAL IIIIIII HS CROSS HS FDIAGONAL \\ţ HS DIAGCROSS Rysunek 5-23. Szeœć stylów pędzli wypełniajšcych Argument crColor funkcji CreateHatchBrush okreœla kolor linii wzoru. Gdy wybie- rzesz ten pędzel w kontekœcie urzšdzenia, Windows przekształci ten kolor na najbliższy czysty kolor dostępny na ekranie. Obszar pomiędzy liniami wzoru jest kolorowany na podstawie aktualnego trybu tła i koloru tła. Jeżeli trybem tła jest OPAQUE, kolor tła (także zamieniany na czysty kolor) używany jest do wypeł- niania przestrzeni między liniami. Jeżeli trybem tła jest TRANSPARENT, Win- dows rysuje linie wzoru bez wypełniania obszaru pomiędzy nimi. Możesz tworzyć swoje własne pędzle także na podstawie bitmap, używajšc Cre- atePatternBrush i CreateDIBPatternBrushPt. Pišta funkcja do tworzenia pędzli logicznych obejmuje poprzednie cztery funkcje: hBrush = CreateBrushIndirect (&logbrush) : Zmierma logbrush jest strukturš typu LOGBRUSH ("pędzel logiczny" - ang. logi- cal brush). Trzy pola tej struktury sš pokazane poniżej. Wartoœć pola IbStyle okre- œla, w jaki sposób interpretowane będš dwa pozostałe pola: 160 Częœć I: Podstawy lbStyle (UINT) lbColor (COLORREF) lbHatch (LONG) BS SOLID kolor pędzla pomijane BS HOLLOW pomijane pomijane BS HATCHED kolor linii wzoru wzór wypełnienia BS PATTERN pomijane uchwyt bitmapy BS DIBPATTERNPT pomijane wskaŸnik do DIB Wczeœniej używaliœmy: SelectObject do wybrania pióra logicznego w kontekœcie urzšdzenia, DeleteObject do usunięcia pióra logicznego i GetObject do pobrania informacji o piórze logicznym. Możesz używać tych samych trzech funkcji w sto- sunku do pędzli. Gdy już masz uchwyt do pędzla, możesz wybrać ten pędzel w kontekœcie urzšdzenia, wywołujšc SelectObject: SelectObject (hdc, hBrush) ; PóŸniej możesz skasować stworzony pędzel, stosujšc funkcję DeleteObject: Delete0bject (hBrush) ; Nie usuwaj pędzla, jeżeli jest on wybrany w kontekœcie urzšdzenia. Jeœli chcesz pobrać informacje o pędzlu, wywołaj GetObject , GetObject (hBrush, sizeof (LOGBRUSH), (LPVOID) &logbrush) ; gdzie logbrush jest strukturš typu LOGBRUSH. Tryby odwzorowania GDI Aż do tego momentu wszystkie przykładowe programy wykonywały rysunki wy- mierzane w pikselach względem lewego górnego rogu obszaru roboczego. Takie jest ustawienie domyœine, ale masz jeszcze inne możliwoœci. Atrybuty kontekstu urzš- dzera, które dotyczš całego rysunku wykonywanego w obszarze roboczym, sš na- zywane trybami odwzorowania (ang. mapping mode). Cztery inne atrybuty kontek- stu urzšdzenia - poczštek okna (ang. window origin), poczštek widoku (ang. viewport origin), rozcišgłoœć okna (ang. window extents) i rozcišgłoœć widoku (ang. viewport extents) - sš œciœle powišzane z atrybutami trybów odwzorowania. Większoœć funkcji rysujšcych GDI wymaga wartoœci współrzędnych lub wielko- œci. Na przykład funkcja TextOut: TextOut (hdc, x, y, psText, iLength) ; Argumenty x i y wskazujš poczštkowe położenie tekstu. Argument x jest położe- niem na osi poziomej, a argument y jest położeniem na osi pionowej. Często za- pisujemy to w formie (x, y). W TextOut, jak praktycznie we wszystkich funkcjach GDI, te wartoœci współrzęd- nych sš jednostkami logicznymi (ang. logical units). Windows musi thunaczyć jed- nostki logiczne na jednostki urzšdzenia (ang. device units) albo na piksele. To thz- maczenie zależy od trybu odwzorowania, poczštku okna i widoku oraz od roz- cišgłoœci okna i widoku. Tryb odwzorowania sugeruje też orientację osi x i y; to znaczy decyduje, czy wartoœci x wzrastajš w lewš czy w prawš stronę ekranu, oraz czy wartoœci y wzrastajš w górę czy w dół ekranu. 4 161 Rozdział 5: Podstawy grafiki Windows definiuje osiem trybów odwzorowania. Sš one wymienione w poniż- ů szej tabeli, wedhxg identyfikatorów zdefiniowanych w WINGDI.H. Tryb Jednostki Kierunek wzrostu wartoœci odwzorowania logiczne na osi x na osi y MM TEXT piksel w prawo w dół MM LOMETRIC 0,1 mm w prawo w górę MM_HIMETRIC 0,01 mm w prawo w górę MM LOENGLISH 0,01 cala w prawo w górę MM_HIENGLISH 0,001 cala w prawo w górę MM TWIPS 1/1440 cala w prawo w górę MM ISOTROPIC dowolny (x = y) do wyboru do wyboru MM ŽNISOTROPIC dowolny (x !=y) do wyboru do wyboru Słowa METRIC i ENGLISH odnoszš się do popularnych systemów miar; LO i HI oznaczajš "niska" (low) i "wysoka" (high) i odnoszš się do precyzji. "Twip" sta- nowi zbitkę słownš oznaczajšcš "dwudziestš częœć punktu (ang. twentieth of a point). Wspomniałem wczeœniej, że punkt jest jednostkš miary w typograf, rów- nš w przybliżeniu 1 /72 cala, ale w programowaniu graficznym często przyjmuje się, że jest równy dokładnie 1/72 cala. Twip to 1/20 punktu, czyli 1/1440 cala. "Isotropic" i "anisotropic" sš rzeczywistymi słowami, oznaczajšcymi odpowied- nio "identyczny we wszystkich kierunkach" i "nieizotropowy'. Możesz ustawić tryb odwzorowania, używajšc SetMapMode (hdc, iMapMode) ; gdzie iMapMode jest jednym z oœmiu identyfikatorów trybu odwzorowania. Mo- żesz uzyskać aktualny tryb odwzorowania przez wywołanie iMapMode = GetMapMode (hdc) : Domyœlnym trybem odwzorowania jest MM TEXT. W tym trybie odwzorowa- nia jednostki logiczne sš takie same jak jednostki fizyczne, co pozwala nam (lub zmusza nas, zależnie od spojrzenia) do pracy bezpoœrednio w pikselach. W wy- wołaniu TextOut, które wyglšda następujšco: TextOut (hdc. S, 16, TEXT ("Hello"), 5) : tekst zaczyna się 8 pikseli od lewego brzegu i 16 pikseli od góry obszaru robo- czego. Jeœli tryb odwzorowania jest ustawiony na MM LOENGLISH: SetMapMode (hdc, MMţLOENGLISH) ; jednostki logiczne sš okreœlone w setnych częœciach cala. Teraz wywołanie TextOut może wyglšdać następujšco: TextOut (hdc. 50, -100. TEXT ("Hello"). 5) : Tekst zaczyna się pół cala od lewego brzegu i 1 cal od góry obszaru roboczego. (Powód stosowania wartoœci ujemnej dla współrzędnej y stanie się jasny, gdy wkrótce szczegółowo omówię tryby odwzorowania). Inne tryby odwzorowania pozwalajš programom okreœlać współrzędne w milimetrach, punktach lub na dowolnie wyskalowanych osiach. ţT 162 Częœć I: Podstawy I Jeœli lubisz pracować z pikselami, nie potrzebujesz używać innych trybów od- wzorowania z wyjštkiem domyœlnego trybu MMţTEXT. Jeœli musisz wyœwietlać ! obraz w calach albo milimetrach, możesz otrzymać potrzebnš informację od Get- DeviceCaps i zrobić własne skalowanie. Inne tryby odwzorowania sš po prostu wygodnym sposobem uniknięcia własnego skalowania. Chociaż współrzędne, które okreœlasz w funkcjach GDI, sš wartoœciami 32-bito- wymi, to tylko Windows NT może posługiwać się wszystkimi 32 bitami. W Win- , dows 98 współrzędne sš ograniczone do 16 bitów i mogš należeć tylko do zakre- i su od -32768 do 32767. Niektóre funkcje Windows, które używajš współrzędnych ' dla punktu startowego i końcowego prostokšta, wymagajš też, aby szerokoœć i wy- sokoœć prostokšta miały co najwyżej wartoœć 32767. Współrzędne urzšdzenia i współrzędne logiczne Możesz zapytać: jeœli użyję trybu odwzorowania MMţLOENGLISH, czy zacznę otrzymywać komurukaty WMţSIZE z wymiarami w setnych cala? Absolutnie nie. Windows w dalszym cišgu używa współrzędnych urzšdzenia we wszystkich komunikatach (takich jak WM MOVE, WMţSIZE i 4VMţMOUSEMOVE), we wszystkich funkcjach nie należšcych do GDI i nawet w kilku funkcjach GDI. Po- myœl o tym w następujšcy sposób: tryb odwzorowania jest atrybutem kontekstu urzšdzenia, więc należy go uwzględnić tylko wtedy, gdy używasz funkcji GDI, które wymagajš uchwytu kontekstu urzšdzenia jako jednego z argumentów. Get- SystemMetrics nie jest funkcjš GDI, więc w dalszym cišgu zwraca wymiary w jed- nostkach urzšdzenia, którymi sš piksele. I chociaż GetDeviceCaps jest funkcjš GDI wymagajšcš uchwytu kontekstu urzšdzenia, Windows w dalszym cišgu zwraca współrzędne w jednostkach urzšdzenia dla indeksów HORZRES i VERTRES p , onieważ jednym z zadań tej funkcji jest dostarczanie programowi wielkoœci urzš- dzenia w pikselach. Jednak wartoœci w strukturze TEXTMETRIC, zwracane przez funkcję GetTextMe- trics, sš wyrażone w jednostkach logicznych. Jeœli w czasie wywołania tej funkcji trybem odwzorowania jest MMţLOENGLISH, GetTextMetrics podaje szerokoœć i wysokoœć znaku w setnych cala. Ułatwisz sobie pracę, jeœli przed wywołaniem GetTextMetrics w celu uzyskania informacji o wysokoœci i szerokoœci znaku, usta- wisz ten sam tryb odwzorowania, w którym będziesz rysował tekst na podsta- wie otrzymanych wielkoœci. Systemy współrzędnych urzšdzenia Windows przelicza współrzędne logiczne podane w funkcjach GDI na współrzęd- ne urzšdzenia. Zanim omówimy system współrzędnych logicznych używany w różnych trybach odwzorowania, przyjrzyjmy się różnym systemom współrzęd- nych urzšdzenia, które Windows definiuje dla ekranu. Chociaż pracujemy prze- ważnie w obszarze roboczym naszego okna, Windows używa nieraz dwóch róż- nych systemów współrzędnych urzšdzera. We wszystkich systemach współrzęd- nych urzšdzenia, jednostki sš wyrażane w pikselach. Wartoœci na osi poziomej x wzrastajš od lewej strony do prawej, a wartoœci na osi pionowej y wzrastajš od góry do dołu., Rozdział 5: Podstawy grafiki 163 Kiedy korzystamy z całego ekranu, pracujemy w warunkach współrzędnych ekra- nu. Lewy górny róg ekranu to punkt (0, 0). Współrzędne ekranu sš używane w ko- munikacie WM MOVE (z wyjštkiem okien potomnych) i w następujšcych funk- cjach Windows: CreateWindow i MoveWindow (z wyjštkiem okien potomnych), GetMessagePos, GetCursorPos, SetCursorPos, GetWindowRect i WindowFromPoint. (To nie jest peina lista). Sš to funkcje ogólne, nie powišzane z żadnym oknem (tak jak dwie funkcje kursora) albo funkcje, które muszš przesunšć lub znaleŸć okno na podstawie wskazanego miejsca na ekranie. Jeœli wywołasz CreateDC z argu- mentem DISPLAY, aby otrzymać kontekst urzšdzenia dla całego ekranu, współ- rzędne logiczne w wywołaniach GDI będš domyœlnie odwzorowane na współ- rzędne ekranu. Współrzędne całego okna odnoszš się do całego okna programu, włšcznie z pa- skiem tytułu, menu, paskami przewijania i krawędziami okna. W przypadku wspólnego okna aplikacji punkt (0, 0) odpowiada lewemu górnemu rogowi okna. Współrzędne całego okna rzadko występujš w Windows, ale jeœli uzyskasz od kontekstu urzšdzenia GetWindowDC, współrzędne logiczne w funkcjach GDI będš domyœlnie odwzorowane na współrzędne całego okna. Trzeci system współrzędnych urzšdzenia - z którym najczęœciej się spotykamy - używa współrzędnych obszaru roboczego. Punkt (0, 0) to lewy górny róg obsza- ru roboczego. Kiedy otrzymasz kontekst urzšdzenia w wyniku wywołania funk- cji GetDC albo BeginPaint, współrzędne logiczne w funkcjach GDI będš domyœl- nie tłumaczone na współrzędne obszaru roboczego. Możesz zmienić współrzędne obszaru roboczego na współrzędne ekranu i od- wrotnie, używajšc funkcji ClientToScreen i ScreenToClient. Możesz też otrzymać położenie i rozmiar całego okna w warunkach współrzędnych ekranu, używajšc funkcji GetWindowRect. Te trzy funkcje dostarczajš doœć informacji, aby można było przeliczać współrzędne z jednego systemu współrzędnych urzšdzenia na inny. Widok i okno Tryb odwzorowania definiuje sposób, w jaki Windows przelicza współrzędne lo- giczne, które sš okreœlone w funkcjach GDI, na współrzędne urzšdzenia. Konkret- ny system współrzędnych urzšdzenia zależy z kolei od funkcji użytej do uzyska- nia kontekstu urzšdzenia. Aby kontynuować dyskusję o trybach odwzorowania, musimy wprowadzić dodatkowe terminy: tryb odwzorowania definiuje przekształ- canie "okna" (współrzędnych logicznych) na "widok" (współrzędne urzšdzenia). Użycie tych dwóch terminów jest niezbyt trafne. W innych systemach interfej- sów graficznych "widok" (ang. viewport) to zazwyczaj region obcinania. Nato- miast w Windows termin "okno" (ang. window) ma bardzo specyficzne znacze- nie (obszar zajmowany na ekranie przez program). Będziemy więc musieli odło- żyć na bok dotychczasowe nawyki. Widok jest okreœlany w układzie współrzędnych urzšdzenia (w pikselach). Naj- częœciej widok oznacza to samo co obszar roboczy, ale może też odnosić się do współrzędnych całego okna lub współrzędnych ekranu, jeœli otrzymałeœ kontekst T 164 Częœć I: podstawy urzšdzenia od GetWindowDC albo CreateDC. Punkt (0, 0) to lewy górny róg ob- szaru roboczego (lub całego okna czy ekranu). Wartoœci x rosnš w prawo, a war- toœci y - w dół. Okno opisujemy w układzie współrzędnych logicznych, które mogš być piksela- mi, milimetrami, calami lub innš dowolnie wybranš jednostkš. Współrzędne lo- giczne okna okreœlasz w funkcjach rysujšcych GDI. Ale w rzeczywistoœci widok i okno to tylko konstrukcje matematyczne. We wszyst- kich trybach odwzorowania Windows thzmaczy współrzędne okna (logiczne) na współrzędne widoku (urzšdzenia) za pomocš dwóch wzorów: xViewport = (xWindow - xWinOrg) x xViewExt + xViewOrg xWinExt yVżewport = (yWindow - yWinOrg) x yViewExt + yViewOrg yWinExt gdzie (xWindow, yWindow) to przekształcany punkt logiczny, a (xViewport, yView- port) to wynik przekształcenia we wspołrzędnych urzšdzenia, najprawdopodob- niej współrzędnych obszaru roboczego. W tych wzorach występujš dwa punkty, które okreœlajš "poczštek" okna i wido- ku. Punkt (xWinOrg, yWinOrg) jest poczštkiem okna we współrzędnych logicz- nych; punkt (xViewOrg, yViewOrg) jest poczštkiem widoku we współrzędnych urzšdzenia. Domyœlnie te dwa punkty sš ustawiane na (0, 0), ale można to zmie- nić. Wzory sugerujš, że punkt logiczny (xWinOrg, yVlţnOrg) zawsze jest przekształ- cany na punkt urzšdzenia (xViewOrg, yViewOrg). Jeœli poczštki okna i widoku zachowujš swoje wartoœci domyœlne (0, 0), wzory upraszczajš się: xViewport = xWindow xxViewExt xWinExt yViewport = yWindow x yViewExt yWinExt Wzory zawierajš też dwa punkty okreœlajšce rozcišgłoœć: punkt (xWinExt, yWinExt) jest rozcišgłoœciš okna we współrzędnych logicznych; (xViewExt, yViewExt) jest roz- cišgłoœciš widoku we współrzędnych urzšdzenia. W większoœci trybów odwzoro- wania rozcišgłoœć wynika z trybu odwzorowania i nie może być zmieniona. Roz- cišgłoœć sama w sobie nic nie oznacza, ale stosunek rozcišgłoœci widoku do rozciš- głoœci okna jest współczynikiem skalujšcym, niezbędnym do zamiany jednostek logicznych na jednostki urzšdzenia. Na przykład kiedy ustawisz tryb odwzorowania MM-LOENGLISH, Windows ustawi xViewExt na pewnš liczbę pikseli, a xWinExt będzie długoœciš tych pikseli w setnych cala. Stosunek okreœla liczbę pikseli na setne częœci cala. Ze względu na wydajnoœć współczynnik skalowania jest okreœlany jako stosunek liczb całko- wityeh, a nie zmiennoprzecinkowych. Rozdział 5: Podstawy grafiki 165 Rozcišgłoœci mogš być ujemne. Oznacza to, że wartoœci na osi logicznej x nie musz korueczrue wzrastać w prawo, zaœ wartoœci na osi logicznej y nie muszš koniecz- nie wzrastać w dół. Windows może też przekształcać współrzędne widoku (urzšdzenia) na współ- ; rzędne okna (logiczne): X xWinExt + xWinOrg xWindow = (xViewport - xViewOrg) xViewExt yWinExt yWindow = (yViewport - yViewOrg) x + yWinOrg yViewExt Windows dostarcza dwóch funkcji, które pozwalajš przechodzić w programie pomiędzy punktami urzšdzenia i punktami logicznymi. Następujšca funkcja za- mienia punkty urzšdzenia na punkty logiczne: DPtoLP (hdc, pPoints, iNumber) ; Zmienna pPoints jest wskaŸnikiem do tablicy struktur POINT, a iNumber jest licz- bš punktów, które majš być przekształcone. Na przykład ta funkcja przyda się do zamiany rozmiaru okna roboczego otrzymanego od GetClientRect (który za- wsze jest wyrażony w jednostkach urzšdzenia) na współrzędne logiczne: GetClientRect (hwnd, &rect) : DPtoLP (hdc, (PPOINT) &rect, 2) : Ta funkcja żamienia punkty logiczne na punkty urzšdzenia: LPtoDP (hdc, pPoints, iNumber) ; Praca w trybie MM_TEXT W trybie odwzorowania MM TEXT sš następujšce domyœlne punkty poczštko- we i rozcišgłoœci: Poczštek okna: (0, 0) Może być zmieniany Poczštek widoku: (0, 0) Może być zmieniany Rozcišgłoœć okna: (1, 1) Nie może być zmieniana Rozcišgłoœć widoku: (1, 1) Nie może być zmieniana Stosunek rozcišgłoœci widoku do rozcišgłoœci okna wynosi 1, więc nie trzeba wykonywać skalowania współrzędnych logicznych na współrzędne urzšdzenia. Podane poprzednio wzory zamieniajšce współrzędne okna na współrzędne wi- doku znacznie się upraszczajš: xViewport = xWindow - xWinOrg + xViewOrg yViewport = yWindow - yWinOrg + yViewOrg To jest tryb odwzorowania nazywany tekstowym, ponieważ ze względu na orien- tację osi jest najbardziej odpowiedni dla tekstu. W większoœci języków tekst jest czytany od lewej do prawej i od góry do dołu, a MM TEXT właœnie w taki spo- sób definiuje wzrost wartoœci na osiach: T 166 Częœć I: Podstawy +x +y Windows dostarcza funkcji SetViewportOrgEx i SetWindowOrgEx, które umożliwiajš zmianę położenia poczštku widoku i okna. Te funkcje powodujš przesuwanie osi tak, że punkt logiczny (0, 0) nie pokrywa się już z lewym górnym rogiem. Zwy- kle będziesz stosował albo SetViewportOrgEx, albo SetWindowOrgEx, ale nigdy - obu jednoczeœnie. Oto jak działajš te funkcje: jeœli zmienisz poczštek widoku na (xViewOrg, yView- Org), punkt logiczny (0, 0) będzie przekształcony na punkt urzšdzenia (xViewOrg, yViewOrg). Jeœli zmienisz poczštek okna na (xWinOrg, yWinOrg), punkt logiczny (xWinOrg, yWinOrg) zostanie przekształcony na punkt urzšdzenia (0, 0), który jest lewym górnym rogiem. Bez względu na zmiany poczštków okna i widoku, punkt urzšdzenia (0, 0) jest zawsze lewym górnym rogiem obszaru roboczego. Załóżmy, że twój obszar roboczy ma cxClient pikseli szerokoœci i cyClient pikseli wysokoœci. Jeœli chcesz zdefiniować punkt logiczny (0, 0) na œrodku obszaru ro- boczego, możesz to zrobić przez wywołanie SetViewportOrgEx (hdc, cxClient / 2, cyClient / 2, NULL) ; Argumenty SetViewportOrgEx wyrażane sš zawsze w jednostkach urzšdzenia. Punkt logiczny (0, 0) będzie teraz przekształcony na punkt urzšdzenia (cxClient / 2, cyClient / 2). Teraz możesz używać swojego obszaru roboczego tak, jakby miał poniższy układ współrzędnych: x +x Logiczna oœ x ma zakres od -cxClient/2 do +cxClientj2, a logiczna oœ y - od -cy- Client/2 do +cyClient/2. Prawy dolny róg obszaru roboczego jest punktem logicz- nym (cxClient/2, cyClient/2). Jeœli chcesz wyœwietlić tekst, zaczynajšc od lewego górnego rogu obszaru roboczego, który jest punktem urzšdzenia (0, 0), musisz użyć ujemnych współrzędnych: TextOut (hdc, -cxClient / 2, -cyClient / 2, "Hello", 5) ; Takie same rezultaty, jak przy zastosowaniu SetViewportOrgEx, możesz osišgnšć z SetWindowOrgEx: SetWindowOrgEx (hdc, -cxClient / 2, -cyClient / 2, NULL) ; Argumenty SetWindowOrgEx wyrażane sš zawsze w jednostkach logicznych. Po tym wywołaniu punkt logiczny (cxClient / 2, cyClient / 2) jest przekształcany na punkt urzšdzenia (0, 0), czyli lewy górny róg obszaru roboczego. Prawdopodobnie nie chcesz (dopóki nie wiesz, co się stanie) użyć razem wywo- łań obu funkcji: SetViewportOrgEx (hdc, cxClient / 2, cyClient / 2, NULL) ; Rozdział 5: Podstawy grafiki 167 I ; ; SetWindowOrgEx (hdc, -cxClient / 2, -cyClient / 2. NULL) ; Oznacza to, że punkt logiczny (-cxClient/2, -cyClient/2) jest przekształcany na punkt urzšdzenia (cxClientj2,; cyClient/2), dajšc w wyniku następujšcy system współrzęd- nych: I -x Możesz uzyskać aktualne poczštki widoku i okna za pomocš tych funkcji: GetViewportOrgEx (hdc, &pt) ; GetWindowOrgEx (hdc, &pt) : gdzie pt jest strukturš POINT. GetViewportOrgEx zwraca wartoœci we współrzęd- nych urzšdzenia; natomiast GetWindowOrgEx zwraca wartoœci we współrzędnych logicznych. Możesz zmieniać poczštek widoku albo poczštek okna, by przesunšć obraz wy- œwietlany w obszarze roboczym twojego okna - na przykład w odpowiedzi na manipulację przez użytkowruka paskiem przewijania. W programie SYSMETS2 z rozdziahz 4 używamy wartoœci iVscrollPos (pozycji bieżšcej pionowego paska przewijania) w celu dostosowania współrzędnej y ekranu: case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; for.(i = 0 ; i < NUMLINES ; i++) ( y = cyChar * (i - iVscrollPos) ; [wyœwietl tekst7 1 EndPaint (hwnd, &ps) ; return 0 ; Możemy osišgnšć ten sam wynik, używajšc SetWindowOrgEx: case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; SetWindowOrgEx (hdc, 0, cyChar * iVscrollPos) ; for (i = 0 ; i < NUMLINES ; i++) ( y = cyChar * i ; [wyœwietl tekst] ) EndPaint (hwnd, &ps) ; return 0 ; Teraz obliczenie współrzędnej y dla funkcji TextOut nie wymaga wartoœci iVscrol- lPos. Oznacza to, że możesz umieœcić wywołania wyœwietlajšce tekst w oddziel- nej funkcji i nie musisz przekazywać do niej wartoœci iVscrollPos, ponieważ przez zmianę poczštku okna ekran został już dostosowywany. Jeœli pracowałeœ już z prostokštnym układem współrzędnych (kartezjańskim), przesunięcie punktu logicznego (0, 0) na œrodek obszaru roboczego, jak zrobili- 168 Częœć I: Podstawy œmy wczeœniej, może wyglšdać rozsšdnie. Jednak jest mały problem z trybem odwzorowania MM-TEXT. Zwykle w układzie współrzędnych kartezjańskich wartoœci na osi y wţrastajš do góry, podczas gdy MM TEXT definiuje, że wzra- stajš w dół. W tym spnsie MM TEXT jest trochę nietypowy, a pięć kolejnych try- bów odwzorowania robi to już normalnie. Metryczne tryby odwzorowania Windows zawiera pięć trybów odwzorowania, które wyrażajš współrzędne lo- giczne w jednostkach fizycznych. Ponieważ współrzędne logiczne na osiach x i y sš wyskalowane w tych samych jednostkach fizycznych, w tych trybach odwzo- rowania rysowanie kół i kwadratów jest łatwe (nawet na urzšdzeniu, które nie ma kwadratowych pikseli). W poniższej tabeli umieœciłem pięć metrycznych trybów odwzorowania, w ko- lejnoœci ich precyzji. Dla porównania dwie prawe kolumny pokazujš wielkoœć jednostek logicznych w calach i milimetrach. Tryb Jednostki Cale Milimetry odwzorowania logiczne MM LOENGLISH 0,01 cala 0,01 0,254 MM LOMETRIC 0,1 mm 0,00394 0,1 MM HIENGLISH 0,001 cala 0,001 0,0254 MM TWIPS 1/1440 cala 0,000694 0,0176 MM HIMETRIC 0,01 mm 0,000394 0,01 W tym trybie sš następujšce domyœlne poczštki okna i widoku oraz rozcišgłoœci: Poczštek okna: (0, 0) Może być zmieniany Poczštek widoku: (0, 0) Może być zmieniany Rozcišgłoœć okna: (?, ?) Nie może być zmieniana Rozcišgłoœć widoku: (?, ?) Nie może być zmieniana Znaki zapytania wskazujš, że rozcišgłoœci okna i widoku zależš od trybu odwzo- rowania i rozdzielczoœci urzšdzenia. Jak wspominałem wczeœniej, rozcišgłoœci jako takie nie sš ważne, dopóki nie wyrażajš stosunku wartoœci. Oto przypomnienie wzorów przekształceń: xViewport = (xWindow - xWinOrg) X xViewExt + xViewOrg xWinExt yViewport = (yWindow - yWinOrg) x yViewExt+ yViewOrg yWinExt Rozdział 5: Podstawy grafiki 169 Na przykład d1a MM LOENGLISH Windows oblicza następujšce rozcišgłoœci: xViewExt ţ liczba pikseli w poziomie, przypadajšca na 0,01 cala. xWinExt -yViewExtţ njemna liczba pikseli w pionie, przypadajšca na 0,01 cala yWinExt Aby ustawić rozcišgłoœci, Windows używa informacji dostępnych z GetDeviceCaps. Jednak wyglšda to nieco inaczej w Windows 98 niż w Windows NT. Najpierw przyjrzyjmy się, jak to się dzieje w Windows 98: załóżmy, że w aplecie Ekran z Panelu sterowania wybrałeœ czcionkę systemowš 96 dpi. GetDeviceCaps zwróci wartoœć 96 dla obu indeksów: LOGPIXELSX i LOGPIXELSY. Windows używa tych wartoœci dla rozcišgłoœci widoku i ustawia następujšce rozcišgłoœci widoku i okna: Tryb odwzorowania Rozcišgłoœć widoku (x, y) Rozeišgłoœć okna (x, y) MM LOMETRIC (96, 96) (254, -254) MM HIMETRIC (96, 96) (2540, -2540) MM LOENGLISH (96, %) (100, -100) MM HIENGLISH (96, 96) (1000, -1000) MM TWIPS (96, 96) (1440, -1440) Stšd dla MM LOENGLISH 96 dzielone przez 100 jest liczbš pikseli w 0,01 cala. Dla MMţLOMETRIC 96 dzielone przez 254 jest liczbš pikseli w 0,1 milimetra. Windows NT inaczej ustawia rozcišgłoœć widoku i okna (spójnie z wczeœniejszy- mi, 16-bitowymi wersjami Windows). Rozcišgłoœć widoku oparta jest na rozmia- rach ekranu w pikselach. Jest to informacja otrzymywana od GetDeviceCaps przy użyciu indeksów HORZRES i VERTRES. Rozcišgłoœć okna oparta jest na zakła- danym rozmiarze ekranu, który jest zwracany przez GetDeviceCaps, kiedy uży- jesz indeksów HORZSIZE i VERTSIZE. Jak wspominałem wczeœniej, te wartoœci zwykle wynoszš 320 i 240 milimetrów. Jeœli ustawiłeœ swój ekran na rozdzielczoœć 1024 na 768 pikseli, Windows NT poinformuje o następujšcych wartoœciach roz- cišgłoœci widoku i okna: -r .s, ţţ.,"ţţrr,wania Rozciaţłoœć widoku (x, y) Rozcišgłoœć okna (x, y) MM LOMETRIC (1024, -768) (3200, 2400) MM_HIMETRIC (1024, -768) (32000, 24000) MM LOENGLISH (1024, -768) (1260, 945) MM HIENGLISH (1024, -768) (i2598, 9449) o ma. -7681 (18142, 13606) 1 ţ0 Częœć I: Podstawy Te rozcišgłoœci okna przedstawiajš liczbę logicznych jednostek zajmujšcych peł- nš szerokoœć i wysokoœć ekranu. Ekran szerokoœci 320 milimetrów zawiera też 1260 jednostek MMţLOENGLISH lub ma 12,6 cala (320 dzielone przez 25,4 mili- metra na cal). Ujemne znaki na poczštku rozcišgłoœci y zmieniajš orientację osi. Dla tych pięciu trybów odwzorowania, wartoœci y wzrastajš w górę urzšdzenia. Jednak zauważ , że domyœlne poczštki okna i widoku sš równe (0, 0). Konsekwencje sš interesujš- ce. Gdy po raz pierwszy zmieniasz tryb na jeden z pięciu powyższych, system współrzędnych wyglšda następujšco: +x Jedynym sposobem wyœwietlenia czegoœ w obszarze roboczym jest użycie ujem- nych wartoœci y. Na przykład ten kod SetMapMode (hdc, MM_LOENGLISH) ; TextOut (hdc, 100, -100, "Hello", 5) ; wyœwietla tekst w odległoœci 1 cala od górnej krawędzi i od lewej krawędzi ob- szaru roboczego. Prawdopodobnie będziesz chciał uniknšć tak niewygodnego układu. Jedno z roz- wišzań polega na umieszczeniu punktu logicznego (0, 0) w lewym dolnym rogu obszaru roboczego. Zakładajšc, że cyClient jest wysokoœciš obszaru roboczego w pikselach , możesz to zrobić przez wywołanie: SetViewportOrgEx (hdc, 0, cyClient, NULL) ; Teraz układ współrzędnych wyglšda tak: +y +x Jest to prawa górna ćwiartka układu współrzędnych prostokštnych. Ewentualnie możesz umieœcić punkt logiczny (0, 0) w œrodku obszaru robocze- go: SetViewportOrgEx (hdc, cxClient / 2, cyClient / 2, NULL) Wtedy system współrzędnych wyglšda następujšco: +y +x y Rozdział 5: Podstawy grafiki Mamy teraz cztery cwiartki kartezjańskiego układu współrzędnych z jednako- wymi jednostkami logicznymi na osiach x i y, wyrażonymi w calach, milimetrach lub twipsach. Możesz też użyć funkcji SetWindowOrgEx do zmiany położenia punktu logiczne- go (0, 0), ale to zadanie jest trochę trudniejsze, ponieważ argumenty SetWindo- wOrgEx muszš być wyrażone we współrzędnych logicznych. Najpierw musisz zamienić (cxClient, cyClient) na współrzędne logiczne, używajšc funkcji DPtoLP. Zakładajšc, że zmienna pt jest strukturš typu POINT, poniższy kod umieszcza punkt logiczny (0, 0) w œrodku obszaru roboczego: pt.x = cxClient ; pt.y = cyClient : DptoLP (hdc, &pt, 1) ; SetWindowOrgEx (hdc, -pt.x / 2, -pt.y / 2. NULL) ; "Modyfikowalne" tryby odwzorowania Dwa pozostałe tryby odwzorowania to MM ISOTROPIC i MM ŽNISOTROPIC. Sš to jedyne dwa tryby odwzorowania, w których Windows pozwala zmienić rozcišgłoœć widoku i okna, co oznacza, że możesz zmienić skalowanie używane przez Windows do przeliczania współrzędnych logicznych i współrzędnych urzš- ; dzenia. Słowo "izotropowy" oznacza "jednakowy we wszystkich kierunkach" "anizotropowy" ma znaczenie przeciwne - "niejednakowy". Podobnie jak poka- zane wczeœniej metryczne tryby odwzorowania, MM ISOTROPIC używa jedna- kowo skalowanych osi. Jednostki logiczne na osi x majš te same rozmiary fizycz- ne, co jednostki logiczne na osi y. Jest to bardzo wygodne wtedy, kiedy musisz tworzyć obrazy, które będš zachowywać poprawne proporcje, bez względu na proporcje ekranu. Różnica między MM ISOTROPIC i metrycznymi trybami odwzorowania polega na tym, że w MM ISOTROPIC możesz kontrolować fizyczny rozmiar jednostki logicznej. Jeœli chcesz, możesz dostosowywać rozmiar jednostki logicznej na pod- stawie obszaru roboczego. To pozwala rysować obrazy, które odpowiednio zmniej- szajšc się lub powiększajšc, zawsze znajdujš się we wnętrzu obszaru roboczego. Program dwóch zegarów z rozdziału 8 korzysta z obrazu izotropowego. Jeœli zmienisz wielkoœć okna, odpowiednio zmienia się wielkoœć zegarów. Program Windows może całkowicie obsługiwać zmianę wielkoœci obrazu, dosto- sowujšc rozcišgłoœci okna i widoku. Program może wtedy używać tych samych jednostek logicznych w funkcjach rysujšcych, bez względu na wielkoœć okna. Czasami MM TEXT i metryczne tryby odwzorowania sš nazywane "w pełni wymuszonymi" (ang. fully constrained) trybami odwzorowania. Oznacza to, że nie możesz zmienić rozcišgłoœci okna i widoku oraz sposobu skalowania przez Windows współrzędnych logicznych na współrzędne urzšdzenia. MMţISOTRO- PIC jest "częœciowo wymuszonym" (ang. partly constrained) trybem odwzorowa- nia. Windows pozwala zmienić rozcišgłoœć okna i widoku, ale dostosowuje je tak, że jednostki logiczne x i y majš te same rozmiary fizyczne. Tryb odwzorowania MM-AMSOTROPIC jest trybem "niewymuszonym" (ang. unconstrained). Możesz zmienić rozcišgłoœci okna i widoku, a Windows nie dostosowuje wartoœci. 172 Częœć I: Podstawy Tryb odwzorowania MM ISOTROPIC Tryb odwzorowania MMţISOTROPIC jest idealny dla dowolnie skalowanych osi, zachowujšcych jednakowe jednostki logiczne. Prostokšty, których logiczna sze- rokoœć i wysokoœć sš sobie równe, zawsze sš wyœwietlane jako kwadraty, a elip- sy, których logiczna szerokoœć jest równa wysokoœci, sš wyœwietlane jako koła. Gdy pierwszy raz ustawisz tryb odwzorowania na MM_ISOTIZOPIC, Windows użyje takich samych rozcišgłoœci okna i widoku, jak dla trybu MM_LOMETIZIC. (Jednak nie polegaj na tej własnoœci). iZóżnica sprowadza się do tego, że możesz zmienić rozcišgłoœci na bardziej odpowiednie, wywołujšc SetWindowExtEx i Set- ViewportExtEx. Windows dostosuje wtedy rozcišgłoœci tak, aby jednostki logicz- ne na obu osiach przedstawiały jednakowe odległoœci fizyczne. Najczęœciej przy wywołaniu funkcji SetWindowExtEx możesz użyć argumentów zawierajšcych wymaganš wielkoœć logicznš okna logicznego, a przy wywołaniu SetViewportExtEx argumentów zawierajšcych rzeczywistš wysokoœć i szerokoœć obszaru roboczego. Kiedy Windows będzie dostosowywał te rozcišgłoœci, musi umieœcić okno logiczne w granicach fizycznego widoku, co może spowodować, że częœć obszaru roboczego znajdzie się poza oknem logicznym. Aby bardziej efektywnie wykorzystać przestrzeń w obszarze roboczym, powinieneœ wywołać SetWindowExtEx przed wywołaniem SetViewportExtEx. Na przykład załóżmy, że chcesz uzyskać tradycyjnš cwiartkę systemu współrzęd- nych, gdzie (0, 0) znajduje się w lewym dolnym rogu obszaru roboczego, a lo- giczna szerokoœć i wysokoœć mieœci się w zakresie od 0 do 32767. Chcesz, żeby jednostki na osiach x i y rniały te same rozmiary fizyczne. Oto co musisz zrobić: SetMapMode (hdc, MM ISOTROPIC) ; SetWindowExtEx (hdc, 32767, 32767. NULL) ; SetViewportExtEx (hdc, cxClient, -cyClient, NULL) ; SetViewportOrgEx (hdc, 0. cyClient, NULL) ; Jeœli wtedy uzyskasz rozcišgłoœci okna i widoku, używajšc funkcji GetWindowE- xtEx i GetViewportExtEx, stwierdzisz, że różniš się one od wartoœci, które okreœli- łeœ. Windows dostosowywał rozcišgłoœci na podstawie proporcji ekranu, aby jed- nostki logiczne na obu osiach miały te same rozmiary fizyczne. Jeœli szerokoœć obszaru roboczego jest większa ruż wysokoœć (w jednostkach fizycz- nych), Windows dostosowuje rozcišgłoœć x tak, że okno logiczne jest węższe niż ob- szar roboczy widoku. Okno logiczne znajdzie się po lewej stronie obszaru roboczego: 32;767 - +y +x 32,767 Tak naprawdę Windows 98 nie pozwoli wyœwietlić niczego po prawej stronie obszaru roboczego, który jest ograniczony do współrzędnych 16-bitowych. Win- dows NT używa współrzędnych w pełni 32-bitowych i może wyœwietlić coœ po prawej stronie. Rozdział 5: Podstawy grafiki 173 Jeœli wysokoœć obszaru roboczego jest większa niż szerokoœć (w jednostkach fi- zycznych), Windows dostosowuje rozcišgłoœć y. Okno logiczne będzie umiesz- czone na dole obszaru roboczego: 32,767 +y +x 32,767 Windows 98 nie pozwoli niczego wyœwietlić na górze obszaru roboczego. Jeœli chcesz, żeby okno logiczne było umieszczane zawsze w lewej górnej częœci obszaru roboczego, musisz zmienić kod na następujšcy: SetMapMode (MM_ISOTROPIC) ; SetWindowExtEx (hdc, 32767, 32767, NULL) ; SetViewportExtEx (hdc, cxClient, -cyClient, NULL) ; SetWindowOrgEx (hdc, 0, 32767, NULL> ; W wywołaniu SetWindowOrgEx żšdamy, aby punkt logiczny (0, 32767) był prze- liczany na punkt urzšdzenia (0, 0). Teraz, jeœli wysokoœć obszaru roboczego jest większa od szerokoœci, współrzędne sš umieszczone następujšco: 32,767 +y +x 32,767 Dla programu zegara możesz użyć czterech ćwiartek kartezjańskiego systemu współrzędnych z osiami dowolnie skalowanymi w czterech kierunkach, w któ- rym punkt logiczny (0, 0) znajduje się na œrodku obszaru roboczego. Jeœli chcesz, aby każda z osi miała na przykład zakres od 0 do 1000, wykorzystaj poniższy kod: SetMapMode (hdc, MM ISOTROPIC) ; SetWindowExtEx (hdc, 1000, 1000, NULL) ; SetViewportExtEx (hdc, cxClient / 2, -cyClient / 2, NULL) SetViewportOrgEx (hdc, cxClient / 2, cyClient / 2, NULL) ; Jeœli obszar roboczy ma większš szerokoœć niż wysokoœć, współrzędne logiczne wyglšdajš następujšco: 174 Częœć I: Podstawy +y x +x Współrzędne logiczne sš także umieszczane na œrodku, jeœli wysokoœć obszaru roboczego jest większa od szerokoœci: +y -x +x Pamiętaj, że rozcišgłoœć okna lub widoku nie ma nic wspólnego z obcinaniem (ang. clipping). Gdy wywołujesz funkcje GDI, możesz nadal używać wartoœci lo- gicznych x i y mniejszych niż -1000 i większych niż +1000. Zależnie od kształtu obszaru roboczego te punkty mogš, ale nie muszš być widoczne. W trybie MM ISOTROPIC możesz sprawić, że jednostki logiczne będš większe od pikseli. Przypuœćmy, że potrzebujesz trybu odwzorowania z punktem (0, 0) w lewym górnym rogu ekranu i wartoœciami y rosnšcymi w dół (tak jak w trybie MMţTEXT), ale z jednostkami logicznymi w szesnastych częœciach cala. Oto je- den ze sposobów osišgnięcia tego: SetMapMode (hdc, MMţISOTROPIC) ; SetWindowExtEx (hdc. 16, 16, NULL) ; SetViewportExtEx (hdc, GetDeviceCaps (hdc, LOGPIXELSX), GetDeviceCaps (hdc, LOGPIXELSY), NULL) ; Argumenty funkcji SetWindowExtEx wskazujš liczbę jednostek logicznych w jed- nym calu. Argumenty funkcji SetViewportExtEx oznaczajš liczbę jednostek fizycz- nych (pikseli) w jednym calu. Jednak to podejœcie nie jest zgodne z metrycznymi trybami odwzorowania w Win- dows NT. Tryby te używajš wielkoœci w pikselach i wielkoœci metrycznej ekranu. Aby być w zgodzie z metrycznymi trybami odwzorowania, możesz użyć nastę- pujšcego kodu: SetMapMode (hdc, MMţISOTROPIC) ; SetWindowExtEx (hdc, 160 * GetDeviceCaps (hdc, HORZSIZE) / 254, 160 * GetDeviceCaps (hdc, VERTSIZE) / 254, NULL) ; SetViewportExtEx (hdc, GetDeviceCaps (hdc, HORZRES), GetDeviceCaps (hdc, VERTRES), NULL) ; W tym kodzie rozcišgłoœci widoku przyjmujš rozmiary całego ekranu w pikselach. Rozcišgłoœci okna przyjmujš rozmiary ekranu w jednostkach wyskalowanych co 1 /16 cala. Jeœli pracujemy z liczbami zmiennoprzecinkowymi, powinniœmy najpierw Rozdział 5: Podstawy grafiki 175 zamienić milimetry na cale (dzielšc wymiary przez 25,4), a potem przekształcić cale do szesnastych częœci cala, mnożšc przez 16. Ponieważ jednak pracujemy na licz- bach całkowitych, musimy pomnożyć przez 160 i podzielić przez 254. Oczywiœcie, taki układ współrzędnych sprawi, że jednostki logiczne będš więk- sze od jednostek fizycznych. Wszystko, co będziesz rysował na urzšdzeniu, bę- dzie miało wartoœci współrzędnych przekształcone na wielokrotnoœci 1/16 cala. Nie możesz narysować dwóch równoległych linii w odległoœci 1/32 cala od sie- bie, ponieważ wymagałoby to ułamkowych wartoœci współrzędnych. MM ANISOTROPIC: rozcišganie obrazu do zadanych rozmiarów Gdy ustawiasz rozcišgłoœci okna i widoku w trybie odwzorowania MMţISOTRO- PIC, Windows dopasowuje te wartoœci tak, aby logiczne jednostki na obu osiach miały te same wymiary fizyczne. W trybie odwzorowania MM ANISOTROPIC, Windows nie wprowadza żadnych poprawek do ustawionych przez ciebie war- toœci. Oznacza to, że MM ANISOTROPIC niekoniecznie zachowuje prawidłowe proporcje figury. Jednym ze sposobów używania MMţANISOTROPIC jest posiadanie arbitralne- go ustawienia współrzędnych dla obszaru roboczego, jak robiliœmy to w przy- padku MM ISOTROPIC. Następujšcy kod ustawia punkt (0, 0) w lewym dolnym rogu obszaru roboczego, z osiami x i y majšcymi zakres od 0 do 32767. SetMapMode (hdc, MM ANISOTROPIC) ; SetWindowExtEx (hdc, 32767, 32767, NULL) ; SetViewportExtEx (hdc, cxClient, -cyClient, NULL) ; SetViewportOrgEx (hdc, 0, cyClient, NULL) ; W trybie MMţISOTROPIC podobny kod powodował, że częœć obszaru robocze- go okna leżała poza zasięgiem osi. W trybie MMţANISOTROPIC prawy górny róg obszaru roboczego będzie zawsze punktem (32767, 32767), niezależnie od jego rzeczywistych wymiarów. Jeœli obszar ten nie jest kwadratowy, jednostki logicz- ne x i y będš miały różne rozmiary fizyczne. W poprzedniej częœci, omawiajšc tryb odwzorowania MM ISOTROPIC, wyjaœni- łem, jak można narysować okršgły zegar w obszarze roboczym, w którym war- toœci x i y należałyby do zakresu od -1000 do 1000. W trybie MMţANISOTROPIC możesz zrobić coœ podobnego: SetMapMode (hdc, MM ANISOTROPIC) ; SetWindowExtEx (hdc, 1000, 1000, NULL) ; SetViewportExtEx (hdc, cxClient / 2, -cyClient / 2, NULL) ; SetViewportOrgEx (hdc, cxClient / 2, cyClient / 2, NULL) ; Głównš różnicš w trybie MM ANISOTROPIC jest to, że zegar miałby kształt elip- sy, a nie koła. Innym sposobem używania MM ANISOTROPIC jest ustawienie jednostek x i y na stałe, ale różne od siebie wartoœci. Na przykład, jeœli masz program, który wyœwietla tylko tekst, możesz ustawić współrzędne o małej rozdzielczoœci, opar- te na wysokoœci i szerokoœci pojedynczego znaku. SetMapMode (hdc, MM ANISOTROPIC) ; SetWindowExtEx (hdc, 1, 1. NULL) ; SetViewportExtEx (hdc, cxChar, cyChar, NULL) ; 17g Częœć I: Podstawy Przyjšłem, oczywiœcie, że cxChar i cyChar sš szerokoœciš i wysokoœciš znaku czcion- ki. Teraz możesz podawać współrzędne w konwencji wierszy i kolumn znaków. Na przykład następujšca instrukcja wyœwietli napis trzy znaki na lewo i dwa wiersze od góry obszaru roboczego: TextOut (hdc, 3, 2, TEXT ("Hello">, 5) ; Najlepiej używać przy tym czcionki o stałej szerokoœci, tak jak w programie WHATSIZE, który za chwilę się pojawi. Gdy pierwszy raz ustawiasz tryb odwzorowania MM ŽNISOTROPIC, zawsze pobiera on rozcišgłoœć z poprzednio ustawionego trybu odwzorowania. Może to być bardzo wygodne. Można myœleć o MM ANISOTROPIC jako o trybie, który "odblokowuje" rozcišgłoœć; oznacza to, że pozwala na zmianę rozcišgłoœci innych , całkiem ograniczonych trybów odwzorowania. Przypuœćmy, że chciałbyœ użyć trybu MM LOENGLISH, aby uzyskać jednostkę logicznš równš 0,01 cala. Ale nie chcesz, aby wartoœci na osi y zwiększały się podczas przesuwania się w górę ekranu - wolisz orientację MM TEXT, gdzie wartoœci y zwiększajš się podczas przesuwania do dohz. Oto kod: SIZE size ; [inne linie programu] SetMapMode (hdc, MM_LOENGLISH) ; SetMapMode (hdc, MM_ANISOTROPIC) ; GetViewportExtEx (hdc, &size) ; SetViewportExtEx (hdc, size.cx, -size.cy, NULL) ; Najpierw ustawiamy tryb odwzorowania na MM LOENGLISH. Potem uwalnia- my rozcišgłoœci poprzez ustawienie trybu MM ŽNISOTROPIC. Funkcja GetView- portExtEx pobiera rozcišgłoœci do struktury SIZE. Następnie wywołujemy SetView- portExtEx z tymi rozcišgłoœciami, ustawiajšc tylko rozcišgłoœć y na ujemnš. Program WHATSIZE Z historii Windows: pierwszy artykuł o tym, jak programować pod Windows, uka- zał się w "Microsoft Systems Journal" w grudniu 1986. Program przykładowy z tego artykułu nazywał się WSZ (ang. what size) i wyœwietlał wielkoœć obszaru roboczego w pikselach, calach i milimetrach. Uproszczonš wersjš tego programu jest WHATSIZE, pokazany na rysunku 5-24. Pokazuje on wymiary obszaru robo- czego okna w pięciu metrycznych trybach odwzorowania. WHATSIZE.C /* WHATSIZE.C - Jakie sd wymiary okna? (c) Charles Petzold, 1998 */ include LRESULT CALLBACK WndProc (HWND, UINT. WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) Rozdział 5: Podstawy grafiki 177 static TCHAR szAppName[] = TEXT ("WhatSize") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CSţVREDRAW; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC ŽRROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITEţBRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MBţICONERROR) ; return 0 ; t hwnd = CreateWindow (szAppName, TEXT ("What Size is the Window?"), WS_OVERLAPPEDWINDOW, CW USEDEFAULT, CW USEDEFAULT, CW_USEDEFAULT, CW USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) TranslateMessage (&msg) ; DispatchMessage (&msg) ; ) return msg.wParam ; void Show (HWND hwnd, HDC hdc, int xText, int yText, int iMapMode, TCHAR * szMapMode) ( TCHAR szBuffer [60] ; RECT rect ; SaveDC (hdc) ; SetMapMode (hdc, iMapMode) ; GetClientRect (hwnd, &rect) ; DPtoLP (hdc, (PPOINT) &rect, 2) ; RestoreDC (hdc, -1) ; ' TextOut (hdc, xText, yText, szBuffer, i wsprintf (szBuffer, TEXT ("%-20s %7d %7d %7d %7d"), szMapMode, rect.left, rect.right, rect.top, rect.bottom)) ; LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ( i 178 Częœć I: Podstawy (cišg dalszy ze strony 177) static TCHAR szHeading [] = TEXT ("Mapping Mode Left Right Top Bottom") ; static TCHAR szUndLine [] = TEXT ( static int cxChar, cyChar ; HDC hdc ; PAINTSTRUCT ps ; ' TEXTMETRIC tm ; I switch (message) ( case WM_CREATE: hdc = GetDC (hwnd) ; SelectObject (hdc, GetStockObject (SYSTEM FIXED FONT)) ; GetTextMetrics (hdc, &tm> ; cxChar = tm.tmAveCharWidth ; cyChar = tm.tmHeight + tm.tmExternalLeading ; ReleaseDC (hwnd, hdc) ; return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; SelectObject (hdc, GetStockObject (SYSTEM FIXEDţFONT)) ; SetMapMode (hdc, MM ŽNISOTROPIC) ; SetWindowExtEx (hdc, 1, 1, NULL) ; SetViewportExtEx (hdc, cxChar, cyChar, NULL) ; TextOut (hdc, 1, 1, szHeading, lstrlen (szHeading)) ; TextOut (hdc, 1, 2, szUndLine, lstrlen (szUndLine)) ; Show (hwnd, hdc, l, 3, MM TEXT, TEXT ("TEXT (pixels)")) ; Show (hwnd, hdc, 1, 4, MM_LOMETRIC, TEXT ("LOMETRIC (.1 mm)")) : Show (hwnd, hdc, 1, 5, MM_HIMETRIC, TEXT ("HIMETRIC (.Ol mm)")) ; Show (hwnd, hdc, 1, 6, MM_LOENGLISH, TEXT ("LOENGLISH (.O1 in)")) ; Show (hwnd, hdc, 1, 7, MM_HIENGLISH, TEXT ("HIENGLISH (.001 in)")) ; Show (hwnd, hdc, l, 8, MM TWIPS, TEXT ("TWIPS (1/1440 in)")) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: PostOuitMessage (0) ; return 0 ; l return DefWindowProc (hwnd, message, wParam, lParam) ; ) Rysunek 5-24. Program WHATSIZE Aby ułatwić wyœwietlanie informacji podczas wywoływania funkcji TextOut, WHATSIZE używa czcionki o stałej szerokoœci znaków (nieproporcjonalnej). Prze- łšczanie na czcionkę o stałej szerokoœci (która była domyœlnš czcionkš aż do Win- dows 3.0) jest realizowane prostš instrukcjš: Rozdział 5: Podstawy grafiki 179 SelectObject (hdc, GetStockObject (SYSTEMţFIXEDţFONT)) ; Sš to te same dwie funkcje, których używaliœmy do wybierania standardowych piór i pędzli. WHATSIZE używa także trybu odwzorowania MM ANISOTRO- PIC z logicznymi jednostkami ustawionymi na wielkoœć znaku, tak jak pokaza- łem wczeœniej. Gdy WHATSIZE musi pobrać wielkoœć obszaru roboczego dla jednego z szeœciu trybów odwzorowania, zachowuje aktualny kontekst urzšdzenia, ustawia nowy tryb odwzorowania, pobiera współrzędne obszaru roboczego, zamienia je na współrzędne logiczne, a potem przywraca oryginalny tryb odwzorowania, za- nim wyœwietli informacje. Ten kod zawarty jest w funkcji Show programu WHAT SIZE: SaveDC (hdc) ; SetMapMode (hdc, iMapMode) ; GetClientRect (hwnd, &rect) ; DptoLP (hdc, (PPOINT) &rect, 2) ; RestoreDC (hdc. -1) ; Na rysunku 5-25 pokazany jest typowy ekran programu WHATSIZE. Happing Hoae LeFt Right Top Ootton TEXT (pixels) 0 640 0 433 LOlIETRIC [.1 nA) 0 ió93 0 -1146 HIHETRIC (.01 nn) 0 16933 B -11456 LOEHCLISH (.01 in) 0 667 0 -451 HIENCLISH (.001 in) 0 66ó7 0 -451A THIPS [1/144P in) 0 9600 B -6495 Rysunek 5-25. Typowy ekran programu WHATSIZE Prostokšty, regiony i obcinanie Windows zawiera kilka dodatkowych funkcji, które pracujš ze strukturami RECT (prostokšt) i regionami. Region jest obszarem ekranu, który jest połšczeniem pro- stokštów, wielokštów i elips. 180 Częœć I: Podstawy Praca z prostokštami Te trzy funkcje wymagajš wskaŸnika do struktury prostokšta: FillRect (hdc, &rect, hBrush) ; FrameRect (hdc, &rect, hBrush) ; InvertRect (hdc, &rect) ; Parametr rect w tych funkcjach jest strukturš typu RECT z czterema polami: left, top, right i bottom. Współrzędne w tej strukturze sš traktowane jako współrzędne logiczne. FiIlRect wypełnia prostokšt danym pędzlem (ale nie włšcza współrzędnych pra- wej strony i dołu). Funkcja ta nie wymaga, abyœ najpierw wybrał pędzel w kon- tekœcie urzšdzenia. FrameRect używa pędzla do narysowania prostokštnego obramowania, ale nie wypełnia prostokšta. Używanie pędzla do rysowania obramowania może wyda- wać się trochę dziwne, ponieważ funkcje, które dotychczas widziałeœ (takie jak Rectangle), rysujš krawędzie, korzystajšc z aktualnego pióra. FrameRect pozwala na narysowanie prostokštnego obramowania, które nie musi być wykonane czystym kolorem. To obramowanie ma szerokoœć jednej jednostki logicznej. Jeœli jednostki logiczne sš większe niż jednostki urzšdzenia, obramo- wanie będzie miało szerokoœć dwóch lub więcej pikseli. InvertRect odwraca kolor wszystkich pikseli w prostokšcie, zmieniajšc jedynki na zera i odwrotnie. Ta funkcja zmienia biały obszar w czarny, czamy obszar w bia- ły, a zielony w karmazynowy. Windows zawiera także dziewięć funkcji, które pozwalajš na prostš i przyjemnš manipulację strukturami RECT. Na przykład, do ustawienia czterech pól struk- tury RECT na pewne wartoœci, normalnie użyłbyœ kodu wyglšdajšcego mniej więcej tak: rect.left = xLeft : rect.top = xTop ; rect.right = xRight ; rect.bottom = xBottom ; Używajšc SetRect, możesz jednak osišgnšć ten sam rezultat w jednej linii: SetRect (&rect, xLeft, yTop, xRight, yBottom) ; Pozostałe osiem funkcji także mogš się przydać, gdy będziesz chciał zrobić jednš z następujšcych rzeczy: ů Przesunšć prostokšt o pewnš liczbę jednostek wzdhxż osi x i y: OffsetRect (&rect, x, y) ů Zwiększyć lub zmniejszyć wielkoœć prostokšta: InflateRect (&rect, x, y) ; ů Ustawić pola prostokšta na równe 0: SetRectEmpty (&rect) ; ů Skopiować jeden prostokšt do drugiego: CopyRect (&DestRect, &SrcRect) ; ů Pobrać częœć wspólnš tych dwóch prostokštów: IntersectRect (&DestRect, &SrcRectl, &SrcRect2) Rozdział 5: Podstawy grafiki 181 Pobrać sumę tych dwóch prostokštów: UnionRect (&DestRect, &SrcRectl, &SrcRect2) ů Sprawdzić, czy prostokšt jest pusty: bEmpty = IsRectEmpty (&rect) ; ů Sprawdzić, czy pewien punkt zawiera się w prostokšcie: bInRect = PtInRect (&rect, point) ; W większoœci przypadków kod odpowiadajšcy tym funkcjom będzie prosty. Na przykład możesz zduplikować funkcję CopyRect, kopiujšc strukturę pole po polu za pomocš instrukcji: DestRect = SrcRect ; Losowo kreœlone prostokšty Zabawnym programem w dowolnym systemie graficznym jest ten, który działa w nieskończonoœć, rysujšc po prostu hipnotyzujšcš serię obrazków o losowo dobranych rozmiarach i kolorze. Możesz stworzyć taki program w Windows, ale nie jest to proste. Mam nadzieję, że rozumiesz, iż nie można po prostu umieœcić pętli while(TRUE) w komunikacie WMţPAINT. Oczywiœcie, będzie to działać, ale program nie pozwoli samemu sobie na odbiór innych komunikatów. Tak więc nie może być zakończony ani zminimalizowany. Jednym z dopuszczalnych rozwišzań jest poproszenie Windows o wysyłanie ko- munikatu WM TIMER do funkcji okna (omówię zegar w rozdziale 8). Przy każ- dym komunikacie WMţTIMER, pobierz kontekst urzšdzenia wywołujšc funkcję GetDC, narysuj losowy prostokšt, a potem zwolnij kontekst urzšdzenia, używa- jšc ReleaseDC. Ale wtedy program nie jest tak atrakcyjny, ponieważ nie rysuje pro- stokštów tak szybko, jak to tylko możliwe. Musi czekać na każdy komunikat WMţTIMER, który jest uzależniony od dokładnoœci zegara systemowego. Mija dużo "martwego czasu", gdy wszystkie kolejki komunikatów sš puste i Win- dows tylko czeka na wejœcie z klawiatury lub myszy. Czy nie dałoby się przejšć sterowania w tym martwym czasie i rysować prostokštów, oddajšc sterowanie tylko wtedy, gdy do kolejki komunikatów zostanie dodany nowy komunikat? Jest to jedno z zadań funkcji PeekMessage. Oto przykład wywołania PeekMessage: PeekMessage (&msg, NULL, 0, 0, PM REMOVE) ; Pierwsze cztery parametry (wskaŸnik do struktury MSG, uchwyt okna i dwie wartoœci okreœlajšce zakres komunikatów) sš takie same jak te w funkcji GetMes- sage. Ustawiajšc drugi, trzeci i czwarty parametr na NULL lub 0, chcemy, by funk- cja PeekMessage zwracała wszystkie komunikaty dla wszystkich okien w progra- mie. Ostatni parametr PeekMessage jest ustawiony na PM REMOVE, jeœli trzeba usunšć komunikat z kolejki komunikatów. Możesz ustawić go na PM NOREMO- VE, jeœli komunikat ma pozostać. Oto dlaczego PeekMessage ma w nazwie "zerk- nij" (ang. peek), a nie "pobierz" (ang. get) - pozwala ona na sprawdzenie następ- nego komunikatu z kolejki komunikatów bez usuwania go. GetMessage nie zwraca sterowania do programu, dopóki nie pobierze komunika- tu z kolejki. Ale PeekMessage wraca natychmiast niezależnie od tego, czy komuni- kat jest, czy rue. Gdy jest jakiœ komunikat w kolejce, wartoœciš zwracanš przez 1 g2 Częœć I: Podstawy PeekMessage jest TIZUE (wartoœć niezerowa) i komunikat ten może być normalnie przetworzony. Gdy nie ma komunikatu w kolejce, PeekMessage zwraca FALSE (0). Pozwala to na zmianę normalnej pętli programu, która wyglšda mniej więcej tak: while (GetMessage (&msg, NULL, 0, 0)) ( TranslateMessage (&msg) ; DispatchMessage (&msg) ; ) return msg.wParam ; na alternatywnš pętlę komunikatów, która wyglšda tak: while (TRUE) ( if (PeekMessage (&msg, NULL, 0, 0, PM REMOVE)) f if (msg.message == WM OUIT) break ; TranslateMessage (&msg) ; DispatchMessage (&msg) ; ) else ( Cinne 7inie programu, które coœ robiţ] ) return msg.wParam ; Zwróć uwagę, że komunikat WM QUIT jest sprawdzany jawnie. Nie musisz tego robić w normalnej pętli komunikatów, ponieważ GetMessage zwraca FLASE (0), gdy otrzyma komunikat WMţQUIT. Ale PeekMessage używa zwróconej wartoœci do oznaczenia, czy pobrała jakiœ komunikat, więc sprawdzanie WM QUIT jest konieczne. Jeœli wartoœciš zwróconš przez PeekMessage jest TRUE, to komunikat jest prze- twarzany normalnie. Jeœli tš wartoœciš jest FALSE, program może coœ zrobić (na przykład wyœwietlić jeszcze jeden losowy prostokšt), zanim odda sterowanie do Windows. (Dokumentacja Windows podaje, że nie możesz używać PeekMessage do usunię- cia komunikatu WM PAINT z kolejki. Nie stanowi to problemu. Przecież Get- Message też nie usuwa komunikatu WMţPAINT z kolejki. Jedynym sposobem usunięcia WMţPAINT jest zatwierdzenie unieważnionych częœci obszaru robo- czego okna, co możesz zrobić używajšc funkcji validateRect, ValidatelZgn lub pary BeginPaint i EndPaint. Jeœli tradycyjnie przetwarzasz komunikat WM PAINT po otrzymaniu go od PeekMessage z kolejki komunikatów, nie będzie problemów. Nie możesz jednak użyć następujšcego kodu do wyczyszczenia kolejki ze wszystkich komunikatów: while (PeekMessage (&msg, NULL, 0, 0, PM REMOVE)) ; Ta instrukcja usuwa i odrzuca wszystkie komunikaty z kolejki, oprócz WMţPA- INT. Jeœli w kolejce jest komunikat WMţPAINT, pętla będzie wykonywana w nie- skończonoœć). Funkcja PeekMessage miała większe znaczenie we wczeœniejszych wersjach Win- dows niż w Windows 98. A to dlatego, że Windows używał wielozadaniowoœci Rozdział 5: Podstawy grafiki 183 bez wywłaszczania (którš omówię w rozdziale 20). Program Terminal używał pętli PeekMessage do czekania na dane przychodzšce z portu komunikacyjnego. Pro- gram Menedżer wydruku używał tej techruki do drukowania, a i aplikacje Win- dows, które coœ drukowały, też zazwyczaj używały pętli PeekMessage. W przy- padku wielozadaniowoœci z wywłaszczaniem, obecnej w Windows 98, program może tworzyć wiele wštków wykonania, jak zobaczymy w rozdziale 20. Wyposażeni tylko w funkcję PeekMessage możemy jednak napisać program, któ- ry będzie bezustannie wyœwietlać losowe prostokšty. Program ten, nazwany RANDRECT, jest pokazany na rysunku 5-26. RANDRECT.C /* RANDRECT.C - Wyœwietlanie losowych prostokdtów (c) Charles Petzold, 1998 */ ţţinclude ţţinclude // aby mieć dostęp do funkcji rand LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; void DraaiRectangle (HWND) ; int cxClient, cyClient ; int WiNAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevinstance, PSTR szCmdLine, int iCmdShow) static T(;HAR szAppName[] = TEXT ("RandRect") ; HWfdD hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CSţVREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) ( MessageBox (NULL, TEXT ("This program requires Windows NT!">, szAppName, MB ICONERROR) ; return 0 hwnd = CreateWindow (szAppName, TEXT ("Random rectangle"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CWţUSEDEFAULT, NULL, NULL, hInstance, NULL) ; l 184 Częœć I: Podstawy (cišg dalszy ze strony 183) ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd)'; while (TRUE) if (PeekMessage (&ms9, NULL, 0, 0, PMţREMOVE)) ( if (msg.message = WM OUIT) break ; TranslateMessage (&msg) ; DispatchMessage (&msg) ; else DrawRectangle (hwnd) ; return msg.wParam ; ) i LRESULT CALLBACK WndProc (HWND hwnd, UINT iMsg, WPARAM wParam, LPARAM lParam) switch (iMsg) case WM_SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; return 0 ; case WM_DESTROY: PostOuitMessage (0) ; return 0 ; ) return DefWindowProc (hwnd, iMsg, wParam, lParam) ; void DrawRectangle (HWND hwnd) HBRUSH hBrush ; HDC hdc ; RECT rect ; if (cxClient == 0 ţţ cyClient == 0) return ; SetRect (&rect, rand () % cxClient, rand () % cyClient, rand (> % cxClient, rand () % cyClient) : hBrush = CreateSolidBrush ( RGB (rand () % 256, rand () % 256, rand () % 256)) ; hdc = GetDC (hwnd) ; FillRect (hdc, &rect, hBrush) ; ( ReleaseDC (hwnd, hdc) ; Delete0bject (hBrush) ; ) Rysunek 5-26. Program RANDRECT Rozdział 5: Podstawy grafiki 185 Ten program działa tak szybko na obecnych komputerach, że już nie wyglšda jak seria losowych prostokštów! Wykorzystuje on funkcje SetRect i FillRect, o których mówiłem wczeœniej, opierajšc współrzędne prostokštów i kolor cišgłego pędzla na losowych wartoœciach dostarczanych przez funkcję C - rand. W rozdziale 20 pokażę jeszcze jednš wersję tego programu, która używa wielu wštków wyko- nania. Tworzenie i malowanie regionów Region to opis obszaru ekranu, który jest kombinacjš prostokštów, wielokštów i elips. Możesz używać regionów do rysowania i obcinania. Używasz regionu do obcinania (czyli ograniczenia rysowania do pewnej częœci obszaru roboczego) po- przez wybranie go w kontekœcie urzšdzenia. Tak jak pióra i pędzle, regiony sš obiektami GDI. Powinieneœ usunšć wszystkie tworzone regiony, używajšc funk- cji DeleteObject. Gdy tworzysz region, Windows zwraca uchwyt regionu. Ten uchwyt ma typ HRGN. Najprostszym regionem jest prostokšt. Możesz stworzyć prostokštny re- gion na jeden z dwóch sposobów: hRgn = CreateRectRgn (xLeft, yTop, xRight, yBottom) ; lub hRgn = CreateRectRgnIndirect (&rect) ; Możesz tworzyć także regiony o kształcie elipsy, używajšc hRgn = CreateEllipticRgn (xleft, yTop, xRight, yBottom) ; lub hRgn = CreateEllipticRgnIndirect (&rect) ; , CreateRoundRectRgn tworzy prostokštny region z zaokršglonymi wierzchołkami. Tworzenie regionu o kształcie wielokšta jest podobne do używania funkcji Poly- gon: hRgn = CreatePolygonRgn (&point, iCount, iPolyFillMode) ; Parametr point jest tablicš struktur typu POINT, iCount jest iloœciš punktów, a iPolyFiliMode wynosi albo ALTERNATE, albo WINDING. Możesz stworzyć tak- że dużo wielokštnych regionów, używajšc CreatePolyPolygonRgn. I I co z tego, powiesz? Co wyróżnia regiony? Oto funkcja, która daje takš potęgę regionom: iRgnType = CombineRgn (hDestRgn, hSrcRgnl, hSrcRgn2, iCombine) ; Ta funkcja łšczy dwa Ÿródłowe regiony (hDrcRgnl i hSrcRgn2) i powoduje, że uchwyt regionu docelowego odnosi się do tego połšczonego regionu. Wszystkie trzy uchwyty regionów muszš być ważne, ale region opisywany poprzednio przez hDestRgn zostanie usunięty (gdy używasz tej funkcji, możesz uczynić hDestRgn uchwytem do małego, prostokštnego regionu). Parametr iCombine opisuje, w jaki sposób majš być łšczone regiony hSrcRgnl i hSrc- Rgn2: 186 Częœć I: Podstawy Wartoœć iCombine Nowy regioxţ RGN AND Częœć wspólna dwóch regionów Ÿródłowych RGN OR Suma obu regionów Ÿródłowych RGN XOR Suma obu regionów Ÿródłowych, z wyłšczeniem częœci wspólnej RGN DIFF Całoœć hSrcRgnl nie należšca do hSrcRgn2 RGN COPY Całoœć hSrcRgnl (ignoruje hSrcRgn2) Wartoœci iRgnType, zwracane prze CombineRgn, mogš być następujšce: NULLRE- GION, co oznacza pusty region; SIMPLEREGION, co oznacza zwykły prostokšt, elipsę lub wielokšt; COMPLEXREGION, oznaczajšcy połšczenie prostokštów, elips lub wielokštów; lub ERROR, co oznacza, że wystšpił błšd. Gdy już masz uchwyt regionu, możesz używać go z czterema funkejami rysujš- cymi: FillRgn (hdc, hRgn, hBrush) ; FrameRgn (hde, hRgn, hBrush, xFrame, yFrame) ; InvertRgn (hde, hRgn) ; PaintRgn (hdc, hRgn) ; Funkcje FillRgn, FramePgn i InvertRgn sš podobne do funkcji FiIlRect, FrameRect i InvertRect. Parametry xFrame i yFrame funkeji FrameRgn sš szerokoœciš i wysoko- œciš logicznš obramowania, które ma być narysowane wokół regionu. Funkcja PaintRgn wypełnia region, używajšc pędzla aktualnie wybranego w kontekœcie urzšdzenia. Wszystkie te funkcje przyjmujš, że region jest zdefiniowany we współ- rzędnych logicznych. Gdy skończysz działania na regionie, możesz go usunšć, używajšc tej samej funk- cji, która usuwa inne obiekty GDI: Delete0bject (hRgn) ; Obcinanie za pomocš prostokštów i regionów Regiony mogš służyć także do obcinania. Funkcja InvalidateRect unieważnia pro- stokštny obszar ekranu i generuje komunikat WMţPAINT. Na przykład możesz użyć funkcji InvalidateRect do wyczyszczenia obszaru roboczego okna i wygene- rowania komunikatu WM PAINT: InvalidateRect (hwnd, NULL, TRUE) ; Możesz pobrać współrzędne unieważnionego prostokšta, wywołujšc GetUpdate- Rect i możesz zatwierdzić prostokšt obszaru roboczego, używajšc funkcji tţalida- teRect. Gdy otrzymasz komunikat WMţPAINT, współrzędne tego unieważnio- nego prostokšta sš dostępne w strukturze PAINTSTRUCT, którš wypełnia funk- cja BeginPaint. Ten unieważniony prostokšt wyznacza także region obcinania. Nie możesz malować poza regionem obcinania. Windows ma dwie funkeje podobne do InvalidateRect i validateRect, które działa- jš na regionach zamiast na prostokštach: InvalidateRgn (hwnd, hRgn, bErase) ; Rozdział 5: Podstawy grafiki 187 ValidateRgn (hwnd, hRgn) ; Gdy otrzymasz komunikat WMţPAINT w następstwie unieważnienia regionu, region obcinania wcale nie musi być prostokštny. Możesz stworzyć własny region obcinania, wybierajšc region w kontekœcie urzš- dzenia albo wywołujšc SelectObject (hdc, hRgn) ; albo SelectClipRgn (hdc, hRgn) ; Region obcinania należy podać w jednostkach urzšdzenia. GDI tworzy kopię regionu obcinania, więc możesz usunšć region po wybraniu go w kontekœcie urzšdzenia. Windows dysponuje kilkoma funkcjami do mani- pulowania regionem obcinania, takimi jak ExcIudeClipRect do wyłšczania prosto- kšta z regionu obcinania, IntersectClipRect do tworzenia nowego regionu obcina- nia, który stanowić będzie częœć wspólnš poprzedniego regionu i prostokšta, i OffsetClipRgn do przesuwania regionu obcinania do innej częœci obszaru roboczego. Program CLOVER Program CLOVER tworzy region z czterech elips, wybiera ten region w kontek- œcie urzšdzenia, a potem rysuje serię linii, które wychodzš ze œrodka obszaru roboczego okna. Linie te sš widoczne tylko w obszarze zdefiniowanym przez region. Wynik jest pokazany na rysunku 5-28. Chcšc narysować to konwencjonalnie, musisz obliczyć punkt końcowy każdej li- n, opierajšc się na wzorze na obwód elipsy. Używajšc złożonego regionu obci- nania, możesz rysować linie i pozwolić Windows na okreœlanie punktów końco- wych. Program CLOVER jest pokazany na rysunku 5-27. CLOVER.C /* CLOVER.C - Program rysujdcy koniczynkę z użyciem regionów (c) Charles Petzold, 1998 */ ttinclude ilinclude ildefine TWO PI (2.0 * 3.14159) LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) static TCHAR szAppNameC7 = TEXT ("Clover") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CSţHREDRŽW ţ CS VREDRAW ; 188 Częœć I: Podstawy (cišg dalszy ze strony 187) wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_ţAPPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITEţBRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MBţICONERROR) ; return 0 ; ' hwnd = CreateWindow (szAppName, TEXT ("Draw a Clover"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CWţUSEDEFAULT, CWţUSEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) ( TranslateMessage (&msg) ; DispatchMessage (&msg) ; return msg.wParam ; LRESULT CALLBACK WndProc (HWND hwnd, UINT iMsg, WPARAM wParam, LPARAM lParam) static HRGN hRgnClip ; static int cxClient, cyClient ; double fAngle, fRadius ; HCURSOR hCursor ; HDC hdc ; HRGN hRgnTemp[6] ; , int i ; PAINTSTRUCT ps ; switch (iMsg) case WM_SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; ;, hCursor = SetCursor (LoadCursor (NULL, IDC WAIT)) ; r ShowCursor (TRUE) ; if (hRgnClip) Delete0bject (hRgnClip) ; hRgnTemp[0] = CreateEllipticR9n (0, cyClient / 3, cxClient / 2, 2 * cyClient / 3) ; Rozdział 5: Podstawy grafiki 189 hRgnTemp[1] = CreateEllipticRgn (cxClient / 2, cyClient / 3, cxClient, 2 * cyClient / 3) ; hRgnTemp[27 = CreateEllipticRgn (cxClient / 3, 0, 2 * cxClient / 3, cyClient / 2) ; hRgnTemp[3] = CreateEllipticRgn (cxClient / 3, cyClient / 2, 2 * cxClient / 3, cyClient) : hRgnTemp[4] = CreateRectRgn (0, 0, 1, 1) : hRgnTemp[57 = CreateRectRgn (0, 0, 1, 1) : hRgnClip = CreateRectRgn (0, 0. 1, 1) ; CombineRgn (hRgnTemp[4], hRgnTemp[0], hRgnTemp[1], RGN_OR) ; CombineRgn (hRgnTemp[5], hRgnTemp[27, hRgnTemp[3], RGN_OR) : CombineRgn (hRgnClip, hRgnTemp[4], hRgnTemp[5], RGN XOR) ; for (i = 0 : i < 6 ; i++) Delete0bject (hRgnTemp[i]) ; SetCursor (hCursor) ; ShowCursor (FALSE) ; return 0 : case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; SetViewportOrgEx (hdc, cxClient / 2, cyClient / 2, NULL) ; SelectClipRgn (hdc, hRgnClip) ; fRadius = ţhypot (cxClient / 2.0, cyClient / 2.0) ; for (fAngle = 0.0 ; fAngle < TWOţPI ; fAngle += TWOţPI / 360) ( MoveToEx (hdc, 0, 0, NULL) ; LineTo (hdc, (int) ( fRadius * cos (fAngle) + 0.5), (int) (-fRadius * sin (fAngle) + 0.5)) ; 1 EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: Delete0bject (hRgnClip) ; PostOuitMessage (0) ; return 0 ; ) ` return DefWindowProc (hwnd, iMsg, wParam, lParam) ; t Rysunek 5-27. Program CLOVEft 190 Częœć I: Podstawy Rysunek 5-28. Wynik działania programu CLOVER z użyciem złożonego regionu obcinania Ponieważ regiony zawsze używajš współrzędnych urzšdzenia, program CLO- VER musi odtwarzać region z każdym komunikatem WM SIZE. Kiedyœ kompu- ter pracujšcy pod Windows potrzebował kilku sekund na narysowanie takiej fi- gury. Dzisiejsze szybkie komputery rysujš takš figurę w mgnieniu oka. CLOVER zaczyna od stworzenia czterech eliptycznych regionów, które sš prze- chowywane w pierwszych czterech elementach tablicy hRgnTemp. Potem program , tworzy trzy "atrapy" regionów: hRgnTemp C47 = CreateRectRgn (0, 0, l, 1) ; hRgnTemp C5] = CreateRectRgn (0, 0, 1, 1) ; hRgnClip = CreateRectRgn (0, 0, l, 1) ; Dwa eliptyczne regiony po lewej i prawej stronie obszaru roboczego sš łšczone: CombineRgn (hRgnTemp [4], hRgnTemp CO], hRgnTemp C1], RGN OR) ; Podobnie, dwa eliptyczne regiony na górze i dole obszaru roboczego sš łšczone: ' CombineRgn (hRgnTemp C57, hRgnTemp C27, hRgnTemp C37, RGNţOR) ; Na koniec te dwa połšczone regiony sš łšczone w hIZgnClip: CombineRgn (hRgnClip, hRgnTemp C47, hRgnTemp C57, RGNţXOR) ; Identyfikator RGN XOR jesf używany do wyłšczenia nakładajšcych się obsza- rów wynikowych regionów. Na koniec usuwamy szeœć regionów pomocniczych : for (i = 0 ; i < 6 ; i++) Delete0bject (hRgnTemp Ci]) ; Przetwarzanie WM PAINT jest proste, zważywszy na wyniki. Poczštek widoku ustawiamy na œrodek obszaru roboczego (aby wygodniej było rysować linie), a re- gion utworzony podczas komunikatu WM SIZE wybieramy jako region obcina- nia kontekstu urzšdzenia: 191 Rozdział 5: Podstawy grafiki T SetViewportOrg (hdc, xClient / 2, yClient / 2) ; SelectClipRgn (hdc, hRgnClip) ; Teraz pozostaje nam jedynie narysować linie - 360 linii co 1 stopień. Długoœć każdej linii jest okreœlona przez zmiennš fRadius, która jest odległoœciš od œrodka do rogu obszaru roboczego: fRadius = hypot (xClient / 2.0, yClient / 2.0) ; for (fAngle = 0.0 ; fAngle < TWOţPI ; fAngle += TWOţPI / 360) I I MoveToEx (hdc, 0, 0, NULL) ; LineTo (hdc, (int) ( fRadius * cos (fAngle) + 0.5), I (int) (-fRadius * sin (fAngle) + 0.5)) ; ) Podczas przetwarzania komunikatu WM DESTROY region jest usuwany: Delete0bject (hRgnClip) ; To jeszcze nie koniec programowania grafiki w tej ksišżce. Rozdział 13 omawia drukowanie, rozdziały 14 i 15 - bitmapy, rozdział 17 - poœwięcony jest tekstowi ' i czcionkom, a rozdział 18 - metaplikom. t I Rozdział 6 Klawiatura W systemie Windows 98 klawiatura i mysz sš dwoma podstawowymi urzšdze- niami wejœciowymi, a ich funkcje częœciowo się nakładajš. W najnowszych apli- kacjach - w porównaniu z tymi, które powstały kilka lat temu - zdecydowanie częœciej wykorzystywana jest mysz. Producenci pewnych typów programów, na przykład gier, przeglšdarek sieciowych oraz programów graficznych i muzycz- nych, przyzwyczaili nas do tego, że mogš one być obshzgiwane wyłšcznie za jej pomocš. Jednak nawet w takich przypadkach pozbawienie komputera klawiatu- ry miałoby katastrofalne skutki. W porównaniu z innymi składnikami komputerów osobistych, klawiatura ma bar- dzo długš historię: za jej protoplastę można uznać pierwszš maszynę do pisania wyprodukowanš w roku 1874 w fabryce Remingtona. Pierwsi programiœci po- sługiwali się klawiaturš do robienia otworów w kartach Holleritha. Ich następcy używali jej jako urzšdzenia wejœciowego prostych terminali podłšczonych bez- poœrednio do komputerów mainframe. Wkładem komputerów PC w rozwój kla- wiatury sš dodatkowe klawisze funkcyjne, wydzielone klawisze kursora oraz (zwykle) klawiatura numeryczna. Mimo ewolucji wyglšdu klawiatury, zasady po- sługiwania się niš pozostały niezmienione. Podstawowe informacje Prawdopodobnie domyœlasz się już, w jaki sposób system Windows pobiera dane z klawiatury: sš one dostarczane do okna programu w postaci komunikatów. W rzeczy samej: gdy pierwszy raz zetknšłeœ się z komunikatami, klawiatura wy- korzystana została jako przykładowe Ÿródło informacji programu. W systemie Windows istnieje osiem różnych komunikatów odpowiadajšcych róż- nym zdarzeniom zwišzanym z klawiaturš. Może ci się wydawać, że to doœć dużo, jednak (jak wkrótce się przekonasz) twój program może ignorować przynajmniej połowę z nich. W zwišzku z tym podstawowe znaczenie ma wiedza o tym, które z komunikatów sš w danej sytuacji ważne, a które nie. Ignorowanie klawiatury Chociaż w większoœci przypadków klawiatura stanowi podstawowe Ÿródło da- nych programu, nie jest konieczne, aby program odpowiadał na każdy jej komu- nikat. System Windows potrafi większoœć tych komunikatów obsłużyć samodziel- nie. 194 Częœć Iţ Podstawy Na przykład możesz zwykle ignorować naciœnięte kombinacje klawiszy odpowia- dajšcych funkcjom systemowym. Ogólnie mówišc, sš to kombinacje zawierajšce klawisz [Alt]. Takimi komunikatami nie musisz się zajmować, ponieważ Windows zinterpretuje je, a następnie poinformuje program o efektach ich działania. (Oczy- wiœcie, jeżeli to konieczne, program może sam monitorować naciœnięcia odpo- wiednich klawiszy). Kombinacje klawiszy, które wywołujš polecenia znajdujšce się w menu programu, przetwarzane sš przez procedurę okna, jednak zwykle przekazywane sš do funkcji DefVVindowProc, gdzie ma miejsce ich standardowe przetwarzanie. Koniec końców, procedura okna otrzyma informację o wybraniu odpowiedniego polecenia w menu - nie ma potrzeby, aby interesowała się ona czymkolwiek więcej. (Poleceniami menu zajmiemy się w rozdziale 10). Większoœć programów Windows korzysta ze skrótów klawiaturowych, aby wy- wołać najczęœciej wykorzystywane opcje menu. W skład skrótu wchodzi zwykle klawisz [Ctrl] oraz któryœ z klawiszy funkcyjnych lub litera (na przykład, w więk- szoœci aplikacji naciœnięcie [Ctrl+S] spowoduje zapisanie na dysku aktualnego pliku). Skróty klawiaturowe definiowane sš w zasobach programu łšcznie z jego opcjami menu (więcej informacji na ten temat znajdziesz w rozdziale 10). System Windows dokonuje konwersji skrótów klawiaturowych na komunikaty odpowia- dajšce poszczególnym poleceniom menu. Nie musisz robić tego sam. Okna dialogowe również mogš być obsługiwane za pomocš klawiatury, nie jest jednak konieczne, aby program monitorował w tym celu jej stan. Naciskane kla- wisze sš przechwytywane i odpowiednio interpretowane przez system Windows, który z kolei wysyła do aplikacji komunikaty będšce wynikiem naciskania kla- wi.szy. W oknach dialogowych mogš znajdować się kontrolki edycji pozwalajšce na wprowadzanie tekstu. Sš to zwykle niewielkie ramki, w których użytkownik może wpi.sać łańcuchy znaków. System Windows obsługuje całš logikę sterujšca kontrolkš i udostępnia aplikacji gotowš jej zawartoœć. Więcej informacji na temat okien dialogowych znajdziesz w rozdziale 11. Kontrolki edycji nie muszš być ograniczane tylko do jednej linii tekstu. Nie mu- szš również być umieszczane wyłšcznie w oknach dialogowych. Wieloliniowe kontrolki edycji mogš pełnić w twoich programach funkcję prostych edytorów tekstu. (Zostało to pokazane w programach POPPAD w rozdziale 9, 10, 11 oraz 13). System Windows udostępnia również ciekawszš kontrolkę, rich-text, dzięki której możesz edytować i wyœwietlać sformatowane teksty. (Zobacz /Platform SDK/ User Interface Services/Controls/IZich Edit Controls). Piszšc złożone aplikacje Windows, przekonasz się, że można wykorzystać kontro- lki okien potomnych do wstępnego przetwarzania danych pochodzšcych zarów- no z klawiatury, jak i od myszy, a które następnie przekazywane sš do okien nad- rzędnych. Po zgromadzeniu wystarczajšcej liczby takich kontrolek, nigdy więcej nie będziesz musiał zajmować się komunikatami zwišzanymi z klawiaturš. Kto ma fokus Podobnie jak każdy składnik komputera, także klawiatura musi służyć wszyst- kim aplikacjom działajšcym pod kontrolš systemu Windows. Niektóre aplikacje mogš mieć więcej niż jedno okno, a co za tym idzie, klawiatura jest dzielona po- między wszystkie. Rozdział 6: Klawiatura 195 jak sobie przypominasz, struktura MSG, wykorzystywana przez program do po- bierania komunikatów z kolejki, zawiera pole hwnd. Jest ono wykorzystywane do przechowywania uchwytu okna, dla którego dany komunikat jest przezna- czony. Funkcja DispatchMessage znajdujšca się wewnštrz pętli przetwarzajšcej komunikaty ma za zadanie przesłanie komunikatu do tego okna. Gdy użytkow- nik naciœnie jakikolwiek klawisz, tylko jedna procedura okna otrzyma zwišzany z tym komunikat, przy czym będzie on zawierał uchwyt okna odbierajšcego. O oknie, które odbiera komunikat zwišzany z klawiaturš, mówimy, że ma fokus (ang. focus). Pojęcie fokus jest œciœle zwišzane z pojęciem okna aktywnego. Okno majšce fokus jest albo oknem aktywnym, albo jednym z jego okien zależnych - czyli oknem podrzędnym aktywnego okna, oknem podrzędnym okna podrzęd- nego aktywnego okna i tak dalej. Zwykle nie ma żadnych problemów z identyfikacjš okna aktywnego. Jest to za- wsze okno najwyższego poziomu - to znaczy jego uchwyt do okna nadrzędnego jest pusty. Jeżeli okno aktywne posiada pasek tytułu, zostaje on podœwietlony. Jeżeli zamiast paska ma ramkę okna dialogowego (najczęœciej spotykana w oknach dialogowych), Windows spowoduje jej zaznaczenie. Jeżeli natomiast aktywne okno jest zminimalizowane, system wyœwietli odpowiadajšcy mu na pasku zadań przy- cisk jako wciœnięty. Jeżeli okno aktywne posiada okna podrzędne, fokus może otrzymać albo to okno, albo jedno z jego okien podrzędnych. Najczęœciej występujšce okna podrzędne to takie kontrolki jak przyciski, przyciski opcji, pola wyboru, paski przewijania, pola edycji oraz listy pojawiajšce się w oknach dialogowych. Okna podrzędne nigdy nie sš same oknami aktywnymi. Okno podrzędne może mieć fokus tylko wtedy, gdy jego oknem nadrzędnym jest okno aktywne. Ogólnie rzecz bioršc, kontrolki podrzędne informujš, że otrzymały fokus, wyœwietlajšc dookoła siebie prostokšt narysowany liniš przerywanš. Może się zdarzyć, że żadne okno nie ma fokusu. Sytuacja taka ma miejsce, gdy zminimalizujesz wszystkie okna. System Windows w dalszym cišgu będzie wy- syłał do okna aktywnego komunikaty zwišzane z klawiaturš, będš one jednak miały innš postać niż komunikaty wysyłane do okna, które nie zostało zminima- lizowane. Procedura okna może okreœlić, czy jej okno ma fokus, przechwytujšc komunika- ty WM SETFOCUS oraz WM KILLFOCUS. Otrzymanie komunikatu WM-SET FOCUS oznacza, że okno ma fokus, natomiast WM-KILLFOCUS - że inne okno otrzymało fokus. Znacznie więcej na ten temat napiszę w dalszej częœci tego roz- działu. Kolejki i synchronizacja Gdy użytkownik naciska i zwalnia klawisze, system Windows oraz program ob- sługi klawiatury dokonujš konwersji sprzętowych kodów klawiszy na odpowied- nio sformatowane komunikaty. Jednakże nie sš one natychmiast umieszczane w kolejce komunikatów aplikacji. Zamiast tego Windows umieszcza je w czymœ, co nosi nazwę systemowej kolejki komunikatów (ang. system message gueue). Jest to kolejka przechowujšca jeden komunikat, wykorzystywana przez Windows w 196 Częœć Iţ Podstawy celu wstępnego przechowania danych wejœciowych pochodzšcych z klawiatury lub myszy. System pobierze z tej kolejki następny komunikat i umieœci go w ko- lejce komunikatów aplikacji tylko i wyłšcznie wtedy, kiedy aplikacja zakończy przetwarzanie poprzedniego komunikatu. Powodem takiego dwuetapowego przetwarzania - umieszczenie komunikatu najpierw w kolejce systemowej, a następnie w kolejce aplikacji - jest synchroni- zacja. Jak już się wczeœniej dowiedzieliœmy, komunikat zwišzany z klawiaturš przesłany zostanie do okna, które ma fokus. W pewnych sytuacjach może się zdarzyć, że użytkownik jest w stanie szybciej naciskać klawisze, niż aplikacja jest w stanie je przetworzyć, a częœć z nich może spowodować zmianę okna majšce- go fokus. Kolejne komunikaty powinny więc zostać wysłane do zupełnie innego okna. Jednak nie będzie to możliwe, jeżeli komunika te zostan zaadresowa- ne", zanim znajdš się w kolejce aplikacji. Klawisze a znaki Komunikat informujšcy o zdarzeniu zwišzanym z klawiaturš, który aplikacja odbiera od systemu Windows, pozwala na rozróżnienie pomiędzy klawiszami a znakami. Ma ono swoje Ÿródło w dwóch sposobach widzenia klawiatury. Po pierwsze, klawiatura może być traktowana jako zestaw klawiszy. Znajduje się na niej tylko jeden klawisz (A]. Zdarzeniem jest zarówno jego naciœnięcie, jak i zwolnienie. Jednak klawiatura jest także urzšdzeniem wejœciowym, które gene- ruje zarówno znaki wyœwietlane na ekranie, jak i znaki kontrolne. Klawisz [A] może wygenerować wiele różnych znaków w zależnoœci od stanu takich klawi- szy specjalnych, jak [Ctrl], [Shift] czy też [Caps Lock]. Normalnie, po naciœnięciu [A] na ekranie pojawi się mała litera "a". Jeżeli naciœnięty został klawisz [Shift] lub włšczony [Caps Lock], wyœwietlonym znakiem będzie duża litera "A". Z kolei, je- żeli naciœnięty zostanie klawisz [Ctrl], wygenerowana zostanie kombinacja [Ctrl+A] (ma ona co prawda swoje znaczenie w kodzie ASCII, natomiast w aplikacji może pelnić role skrótu klawiaturowego lub pozostawać niewykorzystana). W wielu kla- wiaturach naciœnięcie klawisza [A] może zostać poprzedzone naciœnięciem [Shift], [Ctrl] lub [Alt] albo dowolnej ich kombinacji. Kombinacje te mogš generować małš lub dużš literę ze znakiem akcentu, na przykład „, „, „, ţ1, Ž lub A. W przypadku naciœnięcia kombinacji klawiszy generujšcej znak możliwy do wyœwietlenia, system Windows wysyła do programu komunikat zarówno odpo- wiadajšcy klawiszowi, jak i znakowi. Pewne klawisze nie generujš znaków. Sš to: klawisz [Shift], klawisze funkcyjne, klawisze kursora oraz takie klawisze spe- cjalne, jak [Insert] albo [Delete]. Jeżeli naciœnięty zostanie którykolwiek z nich, system wygeneruje jedynie komunikat odpowiadajšcy temu klawiszowi. Komunikaty klawiaturowe Gdy naciskasz dowolny klawisz, system Windows umieszcza w kolejce okna majšcego fokus komunikat WM_KEYDOWN lub WMţSYSKEYDOWN. Gdy na- tomiast zwolnisz klawisz - w kolejce umieszczany jest komunikat WM KEYUP lub WM SYSKEYUP Rozdział 6: Klawiatura Naciœnięcie klawisza Zwolnienie klawisza Klawisze niesystemowe WM KEYDOWN WM ICEYUP Klawisze systemowe WMţSYSKEYDOWN WM SYSKEYUP Zwykle komunikaty odpowiadajšce naciœnięciu i zwolnieniu klawisza (ang. up and down messages) występujš parami. Jeżeli jednak naciœniesz jakiœ klawisz i przy- trzymasz naciœnięty tak długo, aż system zacznie go powtarzać, do procedury okna trafi seria komunikatów WM KEYDOWN (lub WMţSYSKEYDOWN) oraz jeden WM KEYUP (lub WM SYSKEYUP). Podobnie jak wszystkie komunikaty umieszczane w kolejce, tak również komunikaty klawiaturowe dysponujš infor- macjš o czasie ich wygenerowania. Może ona zostać pobrana za pomocš funkcji GetMessageTime. Klawisze systemowe i niesystemowe SYS" w nazwach komunikatów WM_SYSKEYDOWN oraz WM SYSKEYUP " pochodzi od słowa "system" i wskazuje te klawisze, które sš ważniejsze dla Win- dows niż dla aplikacji. Komunikaty te generowane sš zwykle wtedy, kiedy naci- œnięty zostanie klawisz [Alt] łšcznie z jakimœ innym klawiszem. Umożliwiajš one wywołanie opcji z menu programu lub z menu systemowego, można im również przypisać takie funkcje, jak zmiana okna aktywnego ([Alt+Tab] lub [Alt+Esc]). Kombinacje te mogš także pełnić rolę akceleratorów menu systemowego ([Alt] w połšczeniu z jednym z klawiszy systemowych; na przykład [Alt+F4] zamyka aplikację). Większoœć programów zwykle ignoruje otrzymywane komunikaty WM_SYSKEYDOWN oraz WMţSYSKEYUP, przekazujšc je do standardowej pro- cedury okna DefGVindowProc. Ponieważ wszystkie operacje zwišzane z kombina- cjami klawiszy z [Alt] obsługiwane sš przez system Windows, nie istnieje potrzeba ich przechwytywania. Prawdopodobnie procedura okna otrzyma inne komuni- katy będšce efektem naciœnięcia takich kombinacji klawiszy (na przykład wybór opcji z menu). Jeżeli jednak chcesz wyposażyć procedurę okna w kod przechwy- tujšcy komunikaty odpowiadajšce naciœnięciu klawiszy systemowych (zrobimy to w znajdujšcych się w dalszej częœci tego rozdziału programach KEYVIEW1 oraz KEYVIEW2), pamiętaj, aby po ich przetworzeniu przekazać je do funkcji DefWin- dowProc. Dzięki temu system Windows będzie w stanie poprawnie je obsłużyć. Mimo wszystko powinieneœ jednak przez chwilę zastanowić się nad tym. Nie- mal wszystko, co dotyczy twego programu, najpierw przechodzi przez procedu- rę okna. System Windows może obsłużyć komunikat tylko wtedy, gdy zostanie on przekazany do Def 4VindowProc. Jeżeli, na przykład, do pisanej przez siebie pro- cedury okna dodasz poniższe linie case WM_SYSKEYDOWN: case WM_SYSKEYUP: case WMţSYSCHAR: return 0; wszystkie czynnoœci uruchamiane za pomocš kombinacji [Alt+klawisz] będš za- blokowane dopóty, dopóki okno będzie miało fokus. (Komunikatem WM SY- Częœć I: Podstawy SCHAR zajmę się w dalszej częœci tego rozdziału). Dotyczy to [Alt+Tab], [Alt+Esc] oraz operacji zwišzanych z menu. Chociaż nie sšdzę, byœ kiedykolwiek zechciał napisać taki fragment kodu, mam nadzieję, że pozwoli ci to uœwiadomić sobie siłę procedury okna. Komunikaty WM KEYDOWN oraz WM KEYUP sš wysyłane po naciœnięciu i zwolnieniu klawisza bez [Alt]. Napisany przez ciebie program może je prze- chwycić lub nie. Z punktu widzenia systemu Windows nie majš one żadnego zna- czenia. We wszystkich czterech wspomnianych komunikatach jako wParam przekazywany jest kod klawisza wirtualnego (ang. virtual key code) identyfikujšcy naciœnięty bšdŸ zwolniony klawisz, natomiast jako IParam - inne dane zwišzane z klawiszem. Kody klawiszy wirtualnych Z komunikatami WM KEYDOWN, WM KEYUP, WM SYSKEYDOWN oraz WMţSYSKEYUP zwišzany jest kod klawisza wirtualnego, który przekazywany jest za pomocš parametru wParam. Pozwala on na zidentyfikowanie klawisza, który został naciœnięty lub zwolniony. Ach, ten wszechobecny wyraz "wirtualny". Nie lubisz go? Zwykle okreœla coœ , co występuje raczej w myœlach, a nie w œwiecie rzeczywistym. Jednak tylko we- terani programowania dla DOS-a z wykorzystaniem asemblera sš w stanie zro- zumieć, dlaczego kody klawiszy przetwarzane przez Windows sš traktowane ra- czej jako wirtualne, a nie jako rzeczywiste. Dla tych staroœwieckich programistów prawdziwe kody klawiszy generowane sš przez układy elektroniczne klawiatury. W dokumentacji systemu Windows okre- œlane sš one jako scan-codes (ang.). W komputerach kompatybilnych z IBM, ko- dowi 16 odpowiada klawisz Q, kodowi 17 - W, 18 - E, 19 - R, 20 - T, 21 - Y i tak dalej. Czy zauważyłeœ jakšœ prawidłowoœć? Kody te odpowiadajš fizycznemu położeniu poszczególnych klawiszy na klawiaturze. Jednakże programujšcy w Windows uważali, że kody te sš zbyt zależne od sprzętu. Dlatego też zaczęli traktować klawiaturę w oderwaniu od sprzętu, definiujšc tzw. kody klawiszy wir- tualnych. Częœć z nich nie może zostać wygenerowana na klawiaturach zgodnych z IBM. Mogš się one jednak pojawić, jeżeli do komputera podłšczona została kla- wiatura innego producenta. Nazwy większoœci kodów klawiszy wirtualnych rozpoczynajš się od VKţ i sš zdefiniowane w pliku nagłówkowym WIMTSER.H. Zostały one przedstawione, łšcznie z odpowiadajšcymi im wartoœciami (w systemie dziesiętnym i szesnast- kowym) w poniższych tabelach. Tabele te podajš również klawisz klawiatury IBM odpowiadajšcy danemu kodowi oraz informacje o tym, czy klawisze te sš wy- magane do poprawnej pracy systemu Windows. Kody uporzšdkowane sš we- dług wartoœci numerycznej. Oto pierwsze cztery kody klawiszy wirtualnych zwišzanych z przyciskami my- szy: Rozdział 6: Klawiatma i Wartoœć Wartoœć Identyfikator Wymagany? Klawiatura ' IBM ! dziesiętna szesnastkowa z WINUSER.H 1 Oi VK LBUTTON Lewy przycisk my- szy 2 02 VICţRBUTTON Prawy przycisk my- szy 3 03 VK CANCEL r [Ctrl+Break] 4 04 VK MBUTTON Œrodkowy przycisk myszy Przedstawione powyżej kody przycisków myszy nigdy nie pojawiš się w komu- ; I nikatach zwišzanych z klawiaturš. Znajdziesz je natomiast w komunikatach myszy, którymi zajmiemy się w następnym rozdziale. Kod VK CANCEL jest je- dynym kodem klawisza wirtualnego, który odpowiada naciœnięciu kombinacji dwóch klawiszy ([Ctrl+Break]). Nie jest on wykorzystywany przez aplikacje Win- dows. Kilka z przedstawionych poniżej klawiszy - [Backspace], [Tab], [Enter], [Esc] oraz [Spacja] - często pojawia się w programach Windows. Jednakże programy te, ob- ; sługujšc zdarzenia zwišzane z naciskaniem klawiszy, zwykle poshzgujš się ko- I munikatami znakowymi (a nie komunikatami klawiaturowymi). Wartoœć Wartoœć Identyfikator Wymagany? Klawiatura dziesiętna szesnastkowa z WINUSER.H IBM , g Og VK BACK r [Backspace] 9 09 VT TAB r [Tab] 12 OC VK CLEAR Klawisz [5] z kla- wiatury numerycz- nej przy wyłšczo- nym [Num Lock] 13 OD VICţRETURN r [Enter] (przynajmniej jeden) 16 10 VK SHIFT r [Stft] (przynajmniej jeden) 17 11 VK CONTROL r [Ctrl] (przynajmniej ! jeden) 18 12 VICţMENU r [Alt] (przynajmniej jeden) 19 13 VK_PAUSE [Pause] 20 14 VK CAPITAL r [Caps Lock] 27 1B VICţESCAPE r [Esc] 32 20 VK SPACE r [Spacja] Programy Windows nie muszš monitorować aktualnego stanu klawiszy [Shift], [Ctrl] oraz [Alt]. 200 Częœć I: Podstawy Pierwsze osiem z przedstawionych poniżej wirtualnych kodów klawiszy jest praw- dopodobnie najczęœciej wykorzystywanymi kodami obok VK INSERT oraz VICţDELETE. Wartoœć Wartoœć Identyfikator Wymagany? Klawiatura dziesiętna szesnastkowa z WINUSEIt.H IBM 33 21 VK_PRIOR r [Page Up) 34 22 VK_NEXT r [Page Down] 35 23 VK END r [End) 36 24 VK HOME r [Home] 37 25 VK LEFT - [1 38 26 VK UP r [. 39 27 VK RIGHT r - - [1 40 28 VK DOWN r ţ[. - [1 41 29 VK SELECT 42 2A VK PRINT 43 2B VK EXECUTE 44 2C VK SNAPSHOT [Print Screenl 45 2D VK INSERT r [Insert] 46 2E VK DELETE r [Delete] 47 2F VK HELP Zwróć uwagę na to, że w wielu przypadkach tak się nieszczęœliwie złożyło, że nazwy kodów (na przykład VK PRIOR czy też VICţNEXT) znacznie różniš się od nazw odpowiadajšcych im klawiszy, jak również nie sš spójne z identyfikato- rami wykorzystanymi w paskach przewijania. Klawisz [Print Screen] jest zwykle ignorowany przez większoœć aplikacji Windows. Na jego naciœnięcie odpowiada sam system umieszczajšc w Schowku aktualny obraz ekranu w postaci mapy bi- narnej. Kody VICţSELECT, VICţPRINT, VK EXECUTE oraz VK HELP odpowia- dajš klawiszom, które prawdopodobnie nigdy nie pojawiš się na jakiejkolwiek klawiaturze, z którš będziesz miał do czynienia. System Windows posiada również kody klawiszy wirtualnych odpowiadajšce literom oraz cyfrom znajdujšcym się w podstawowej częœci klawiatury. (Klawia- tura numeryczna obsługiwana jest osobno). Wartoœć Wartoœć Identyfikator Wymagany? Klawiatura dziesiętna szesnastkowa z WINUSER.H IBM 48 - 57 30 - 39 Brak r Cyfry od 0 do 9 z podstawowej częœci klawiatury 65 - 90 41 - 5A Brak r Litery od A do Z Zwróć uwagę, że w tym wypadku kody klawiszy wirtualnych sš identyczne z ko- dami ASCII odpowiadajšcymi danym kławiszom. Programy Windows prawie Rozdział 6: Klawiatura 201 I ! rugdy nie posługujš się tymi kodami klawiszy wirtualnych, posługujš się nato- i miast komunikatami znakowymi. I Przedstawione niżej kody generowane sš przez klawiatury Microsoft Natural Keyboard (klawiatura naturalna Microsoftu) oraz z nimi kompatybilne: Wartoœć Wartoœć Identyfikator Wymagany? Klawiatura dziesiętna szesnastkowa z WINUSER.H IBM gl 5B VK_LWIN Lewy klawisz [WindowsJ 92 5C VK RWIN Prawy klawisz [Windows] 93 5D VIC-APPS Klawisz aplikacji Klawisze VK LWIN oraz VICţIZWáţ1 wykorzystywane sš przez Windows do otwo- rzenia menu Start lub (w starszych wersjach) do uruchomienia Menedżera za- dań. Naciœnięte jednoczeœnie pozwalajš na zalogowanie się lub wylogowanie z sys- temu (jedynie w Microsoft Windows NT) albo na zalogowanie się lub wylogo- wanie z sieci (w Windows for Workgroups). Z kolei klawisz aplikacji może zo- stać wykorzystany do wyœwietlenia informacji pomocniczych lub skrótów. Przedstawione niżej kody odpowiadajš klawiszom znajdujšcym się na klawiatu- rze numerycznej (o ile jest obecna): Wartoœć Wartoœć Identyfikator Wymagany? Klawiatura dziesiętna szesnastkowa z WINUSER.H IBM 96 - 105 60 - 69 VK_NUMPADO Klawisze od 0 do 9 do VK NUMPAD9 z klawiatury numerycznej przy L.. właczonym [Num á Lock] 6A VK MULTIPLY (*] z klawiatury 106 - numerycznej 107 6B VK ADD [+] z klawiatury numerycznej 108 6C VK SEPARATOR 109 6D VK SUBTRAC'T [-] z klawiatury numerycznej 110 6E VK DECIMAL [.] z klawiatury numerycznej 111 6F VK DIVIDE [/] z klawiatury numerycznej Na koniec wreszcie, niemal wszystkie klawiatury posiadajš 12 klawiszy funkcyj- nych. System Windows wymaga obecnoœci jedynie 10, posiada jednak identyfi- katory aż dla 24. Powtórzę jeszcze raz: programy używajš klawiszy funkcyjnych jako akceleratorów klawiaturowych, nie ma więc potrzeby, abyœ kiedykolwiek musiał sięgać po kody przedstawione w poniższej tabeli: 202 ' Częœć I: Podstawy Wartoœć Wartoœć Identyfikator Wymagany? Klawiatura dziesiętna szesnastkowa z WINUSER.H IBM 112 - 121 70 - 79 VIC-Fl do VIC-F10 x Klawisze funkcyjne od [Fl] do [F10] 122 - 135 7A - 87 VIC-Fll do VIC F24 Klawisze funkcyjne od [Fll] do [F24] 144 90 VK NUMLOCK [Num Lock] 145 91 VK SCROLL [Scroll Lock] W systemie Windows zdefiniowane zostały również inne kody klawiszy wirtu- alnych, sš one jednak wykorzystywane we współpracy z pewnymi niestandar- dowymi klawiaturami lub z klawiszami, które znaleŸć można głównie na kla- wiaturach terminali komputerów mainframe. Pełna lista dostępna jest w /Plat- form SDK/Llser Interface Services/Llser Input/Virtual-Key Codes. informacja przekazywana przez IParam W czterech komunikatach klawiaturowych (WM KEYDOWN, WM_KEYUP, WM SYSKEYDOWN oraz WMţSYSKEYUP) za pomocš parametru wParam prze- kazywany jest kod klawisza wirtualnego, natomiast IParam zawiera inne infor- macje użyteczne w okreœleniu znaczenia odebranego komunikatu. 32-bitowa wartoœć lParam podzielona została na szeœć pól przedstawionych na rysunku 6-1. Flaga klawisza rozszerzonego 31 , 30 : 29 28 : 27 , 26 25 ; 24 : 23 : . . . . .:16 ; 15 : , . . ., L T I Kod kontekstu 16 bţtowy Stan poprzedniego klawisza ţ licznik powtórzeń Stan przejœciowy 8-bitowy kod OEM Rysunek 6-1. Szeœć pól parametru lParam w komunikacie zwišzanym z klawiaturš Licznik powtórzeń Wartoœć licznika powtórzeń okreœla, ilu naciœnięciom klawisza odpowiada odebra- ny komunikat. W większoœci przypadków będzie on zawierał wartoœć 1. Jeżeli jed- nak użytkownik nacisnšł i przytrzymuje wybrany klawisz, a procedura okna nie jest wystarczajšco szybka, aby przetworzyć komunikaty nadchodzšce z szybkoœciš powtarzania klawisza (którš możesz ustawić, korzystajšc z ap]etu Klawiatura znaj- dujšcego się w Panelu sterowania), system Windows łšczy kilka komunikatów WMţKEYDOWN lub WMţSYSKEYDOWN w jeden komunikat i odpowiednio zwiększa wartoœć licznika powtórzeń. W przypadku komunikatów WMţKEYUP oraz WMţSYSKEYUP licznik powtórzeń zawsze zawiera wartoœć 1. Ponieważ wartoœć licznika powtórzeń większa niż 1 mówi, że szybkoœć powtarza- nia klawisza jest większa niż ta, którš program jest w stanie obshxżyć, może oka- zać się, że chcesz ignorować zawartoœć licznika przy przetwarzaniu komunika- Rozdziałó: Klawiatura 203 tów klawiaturowych. Niemal każdy zetknšł się już ze zjawiskiem przeskakiwa- nia zawartoœci okna edytora tekstu lub arkusza kalkulacyjnego podczas jego prze- wijania. Było to spowodowane nagromadzeniem w jednym komunikacie kilku "naciœnięć" klawisza. Możesz uniknšć tego problemu w swoich aplikacjach, je- żeli twój program będzie ignorować licznik powtórzeń w sytuacji, gdy potrzebu- je dużo czasu na przetworzenie każdego naciœniętego klawisza. Jednak w innych przypadkach zawartoœć licznika powtórzeń powinna być uwzględniana. Sšdzę więc, że w każdym przypadku powinieneœ sprawdzić, które rozwišzanie będzie lepsze. Kod OEM Kod OEM jest generowany przez układ elektroniczny klawiatury. Z pewnoœciš zaawansowani w latach programiœci korzystajšcy z asemblera rozpoznajš go jako wartoœć zwracanš przez odpowiedniš usługę ROM BIOS komputerów kompaty- bilnych z PC. (OEM jest skrótem pochodzšcym od Original Equipment Manufac- turer, co w naszym kontekœcie jest synonimem "zgodny ze standardem IBM ). Informacja znajdujšca się w tym polu nie jest już do niczego potrzebna. Progra- my przeznaczone do pracy pod kontrolš systemu Windows niemal zawsze mogš ignorować kod OEM, z wyjštkiem sytuacji, gdy ich poprawne działanie jest za- leżne od fizycznego układu klawiatury. Ma to na przykład miejsce w przypadku znajdujšcego się w rozdziale 22 programu KBMIDI. Flaga klawisza z klawiatury rozszerzonej Flaga klawisza z klawiatury rozszerzonej ma wartoœć 1, jeżeli naciœnięty został jeden z klawiszy znajdujšcych się w grupie klawiszy dodatkowych rozszerzonej klawiatury IBM. (Klawiatura rozszerzona składa się ze 101 lub 102 klawiszy. Kla- wisze funkcyjne znajdujš się w jej górnej częœci. Klawisze strzałek sš wydzielone z bloku numerycznego, jednak znajdujš się w nim ich duplikaty). Flaga ma war- toœć l, gdy użytkownik nacisnšł klawisze [Alt] lub [CtrlJ znajdujšce się z prawej strony klawiatury, klawisz strzałki (łšcznie z [Insert] oraz [Delete]) spoza klawia- tury numerycznej, ukoœnik ([/]), klawisz (Enter] klawiatury numerycznej lub [Num Lock]. Ogólnie rzecz bioršc, programy Windows ignorujš stan flagi klawi- sza z klawiatury rozszerzonej. Kod kontekstu Kod kontekstu ma wartoœć 1, jeżeli klawisz [Alt] został zwolniony w trakcie ge- nerowania kombinacji klawiszy. Bit ten będzie zawsze miał wartoœć 1, gdy ode- brany zostanie komunikat WM SYSKEYUP lub WMţSYSKEYDOWN, natomiast dla komunikatu WM ICEYUP lub WM KEYDOWN jest ustawiony na 0 z dwo- ma wyjštkami: ů Jeżeli okno aktywne jest zminimalizowane, to nie ma fokusu wejœciowego. Wszelkie naciskane klawisze generujš komunikaty WM SYSKEYUP oraz WM SYSKEYDOWN. Jeżeli klawisz [AltJ nie został naciœnięty, kod kontekstu ma wartoœć 0. Windows posługuje się komunikatami WM SYSKEYUP oraz WM SYSKEYDOWN, aby zminimalizowane okno nie prżetwarzało informa- cji zwišzanych z naciskanymi klawiszami. wţ- 204 Częœć i: Podstawy ů W klawiaturach z układem charakterystycznym dla narodowych znaków diak- trycznych, pewne znaki generowane sš za pomocš kombinacji klawiszy [Shift], [Ctrl] lub [Alt] z innym klawiszem. W tym przypadku pole Kod kontekstu ma wartoœć 1, jednak mimo to program nie ma do czynienia z systemowym komunikatem klawiaturowym. Poprzedni stan klawisza Flaga ta ma wartoœć 0, jeżeli klawisz poprzednio nie był naciœnięty, natomiast 1 - w przeciwnym wypadku. Jeżeli odebrany został komunikat WM_KEYUP lub 4VM-SYSKEYUP, stan poprzedniego klawisza zawsze będzie równy 1. Natomiast w przypadku WM-KEYDOWN oraz WMţSYSKEYDOWN flaga ta może mieć zarówno wartoœć 0, jak i 1. Wartoœć 1 oznacza, że program ma do czynienia z ko- lejnymi komunikatami wygenerowanymi na skutek automatycznego powtarza- nia przytrzymanego klawisza. Stan przejœciowy Flaga ta ma wartoœć 0, jeżeli klawisz jest naciskany, 0 natomiast -jeżeli jest zwal- niany. Wartoœć 0 występuje razem z komunikatami WM_KEYDOWN oraz WM-SYSKEYDOWN, a wartoœć 1 - z WM-KEYUP oraz WM SYSKEYUP. Stany klawisza (Shift] W trakcie przetwarzania komunikatów klawiaturowych może okazać się istotne, czy naciœnięty został którykolwiek z klawiszy majšcych wpływ na "wielkoœć" generowanej litery ([Shift], [Ctrl] lub [AltJ) lub klawiszy przełšczajšcych ([Caps LockJ, [Num Lock] lub [Scroll LockJ). Informację tę możesz pobrać, wywołujšc funkcję GetKeyState. Na przykład: iState = GetKeyState (VK SHIFT) ; Zmienna iState przyjmie wartoœć ujemnš (to znaczy z ustawionym bardziej zna- czšcym bitem), jeżeli klawisz (ShiftJ jest naciœnięty. Wartoœć zwrócona przez iState = GetKeyState (VK CAPITAL) ; będzie miała ustawiony mniej znaczšcy bit, jeżeli [Caps Lock] został włšczony. Wartoœć tego bitu odpowiada lampce kontrolnej znajdujšcej się na klawiaturze. Ogólnie rzecz bioršc, funkcjš GetKeyState będziesz posługiwać się, podajšc jako jej parametr kody VK SHIFT, VK CONTROL oraz VK MENU (co oznacza, że naciœnięty jest klawisz [AltJ). Jeżeli natomiast chcesz sprawdzić, czy naciœnięty jest prawy czy lewy klawisz [ShiftJ, [Ctrl] lub [Alt], możesz skorzystać z kodów VK LSHIFT, VK RSHIFT, VK LCONTROL, VK RCONTROL, VK LMENU lub VK RMENU. Identyfikatory te mogš być wykorzystane wyłšcznie jako parame- try funkcji GetKeyState oraz GetAsyncKeyState (funkcja ta jest opisana poniżej). Posługujšc się wirtualnymi kodami klawiszy VIC-LBUTTON, VK RBUTTON oraz VICţMBUTTON możesz również dowiedzieć się, jaki jest aktualny stan przyci- sków myszy. Jednakże większoœć programów, w których z jakichœ powodów in- formacja ta ma znaczenie, pobiera jš w inny sposób - sprawdzajšc stan klawiszy, gdy odebrany zostanie komunikat myszy. W rzeczywistoœci informacje o naci- Rozdział 6: Klawiatura 205 œniętych klawiszach zawarte sš już w samym komunikacie. Przekonasz się o tym w następnym rozdziale. Korzystajšc z funkcji GetKeţyState, powinieneœ zachować szczególnš ostrożnoœć. Tak naprawdę nie odzwierciedla ona aktualnego stanu klawiatury. Zamiast tego mo- żesz dowiedzieć się, jaki on był do chwili odebrania aktualnego komunikatu. W większoœci przypadków jest to dokładnie ta informacja, której potrzebujesz. Je- żeli chcesz dowiedzieć się, czy użytkownik nacisnšł kombinację klawiszy [Shift+Tab], możesz wywołać funkcje GetKeyState z parametrem VIC-SHIFT w trakcie przetwarzania komunikatu WM ICEYDOWN wywołanego naciœnięciem klawisza [Tab]. Jeżeli funkcja zwróci wartoœć ujemnš, będzie to oznaczało, że klawisz [Shift] został naciœnięty przed klawiszem [Tab]. W tym przypadku nie ma znaczenia, czy został on już zwolniony w czasie, gdy przetwarzasz odebrany komunikat. Wystar- czy, że wiesz, że był on już wciœnięty, kiedy naciœnięty został klawisz [Tab]. Funkcja GetKeyState nie umożliwia pobierania informacji o klawiaturze w ode- rwaniu od normalnego przetwarzania komunikatów wygenerowanych w odpo- wiedzi na naciœnięcie klawisza. Możesz na przykład czuć pokusę zatrzymania działania programu w procedurze okna do momentu, gdy użytkownik naciœnie klawisz [Fl]: while (GetKeyState (UK F1) >= 0) ; // LE !!! Nigdy tego nie rób! Jest to niezawodny sposób zawieszenia programu (o ile oczy- wiœcie przed rozpoczęciem wykonywania pętli nie został odebrany komunikat WM KEYDOWN spowodowany naciœnięciem klawisza [F1]). Jeżeli naprawdę jesteœ zainteresowany aktualnym stanem klawiatury, powinieneœ posłużyć się funkcjš GetAsyncKeyState. Korzystanie z komunikatów zwišzanych z naciskaniem klawiszy Program Windows otrzymuje informacje o każdym naciœniętym klawiszu. Jest to bardzo użyteczne. Jednakże większoœć programów ignoruje prawie wszystkie takie komunikaty. WMţSYSKEYDOWN oraz WM SYSKEYUP przeznaczone sš dla funkcji systemowych i nie ma powodu, aby program się nimi interesował. Jeżeli zdecydujesz się na przechwycenie WMţKEYDOWN, nie ma powodu, abyœ przechwytywał również WMţKEYUP. Ogólnie rzecz bioršc, programy pracujšce pod kontrolš systemu Windows po- sługujš się komunikatem WMţKEYDOWN, aby dowiedzieć się o naciœnięciu kla- wisza, który nie powoduje wyœwietlenia jakiegokolwiek znaku. Mimo że można dowiedzieć się czegoœ o wpisywanym tekœcie (znakach) na podstawie komuni- katów klawiaturowych oraz informacji o stanie klawiszy specjalnych wpływajš- cych na wielkoœć liter, nie rób tego. Z pewnoœciš będziesz miał problemy z kla- wiaturami typowymi dla innych języków niż angielski. Jeżeli na przykład odbie- rzesz komunikat WM KEYDOWN, w którym wartoœć parametru lParam wynosi 0x33, zorientujesz się, że naciœnięty został klawisz z cyfrš "3". Jak dotšd - cał- kiem dobrze. Jeżeli poshzżysz się funkcjš GetKeyState i dowiesz się, że naciœniety został klawisz [Shift], możesz założyć, że użytkownikowi chodziło o znak funta ("#"). Nie musi to być prawdš. Brytyjczycy poshzgujš się innym symbolem: ^. 1ţ"" 206 Częœć I: Podstawy Komunikat WMţKEYDOWN wydaje się najbardziej użyteczny, gdy zaintereso- wany jesteœ klawiszami kursora, funkcyjnymi, [Insert] lub [Delete]. Jednakże [In- sert], [Delete] oraz klawisze funkcyjne często wykorzystywane sš jako skróty (ak- celeratory) opcji menu programu: Ponieważ system Windows dokonuje ich kon- wersji na komunikaty zwišzane z odpowiadajšcymi ixn opcjami menu, nie ma potrzeby, abyœ przechwytywał komunikaty klawiaturowe odpowiadajšce tym klawiszom. W programach przeznaczonych do pracy pod kontrolš systemu MS-DOS bardzo typowe było korzystanie z klawiszy funkcyjnych w połšczeniu z [Shift], [Alt] oraz [Ctrl]. Coœ takiego możliwe jest również do zrealizowania w aplikacjach prze- znaczonych dla Windows. (Rzeczywiœcie, Microsoft Word bardzo często korzy- sta z klawiszy funkcyjnych jako skrótów dla poleceń znajdujšcych się w menu), nie jest to jednak zalecane. Jeżeli chcesz wykorzystać klawisze funkcyjne, powin- ny one powielać polecenia menu. Jedno z założeń systemu Windows polega na tym, że użytkownik otrzymuje prosty w obsłudze interfejs, który nie wymaga zapamiętywania złożonych poleceń ani sięgania do dokumentacji. Tak więc, wszystko sprowadza się do jednego: w większoœci przypadków będziesz przechwytywał komunikat WM KEYDOWN, aby obshzżyć klawisze kursora lub od czasu do czasu wykonać jakšœ operację po naciœnięciu [Insert] lub [Delete]. Korzystajšc z tych klawiszy, możesz sprawdzać stan [Shift] oraz [Ctrl] za pomo- cš funkcji GetKeyState. Programy pracujšce w œrodowisku Windows często ko- rzystajš z kombinacji klawisza [Shift] i jednego z klawiszy kursora, aby rozsze- rzyć możliwoœci zaznaczania (na przykład) w edytorach tekstu. Z kolei klawisz [Ctrl] używany jest do zmiany znaczenia klawiszy kursora. Na przykład jedno- czesne naciœnięcie [Ctrl] oraz strzałki w prawo może oznaczać przesunięcie kur- sora o jedno słowo w prawo. Najlepszš metodš okreœlenia sposobu, w jaki klawiatura ma działać w twojej aplikacji, jest podpatrzenie, jak jest ona wykorzystywana w innych popularnych programach. Jeżeli jej zachowanie nie będzie ci się z jakiegoœ powodu podobało, możesz je dowolnie zmieruć. Musisz jednak pamiętać, że nietypowe rozwišzania utrudniš przyszłym użytkownikom przyswojenie twego programu. Wzbogacanie SYSMETS o obsługę klawiatury Trzy wersje programu SYSMETS przedstawione w rozdziale 4 zostały napisane bez jakiejkolwiek wiedzy o klawiaturze. Przewijanie tekstu możliwe było wyłšcz- nie za pomocš myszy i pasków przewijania. Teraz, gdy już wiemy, w jaki sposób przetwarzać komunikaty klawiaturowe, dodajmy do programu obshxgę klawia- tury. Mamy w nim trochę pracy dla klawiszy kursora. Posłużymy się większo- œciš z nich ([Home], [End], [Page Up], [Page Down], [T], [ţL]) tak, że pozwolš na pionowe przewijanie tekstu. Klawisze strzałek przesuwajšce kursor w prawo i w lewo mogš zostać wykorzystane do mniej istotnego przewijania tekstu w pozio- mie. Najczęœciej spotykana metoda wzbogacenia programu o obshxgę klawiatury pole- ga na dodaniu do jego procedury okna kodu obsługi komunikatu WM ICEYDOWN, który powieli operacje wykonywane w ramach obshzgi WM VSCROLL oraz Rozdział 6: Klawiatura VMţHSCROLL. Nie jest to jednak najmšdrzejsze rozwišzanie. Jeżeli kiedykol- wiek będziemy chcieli zmodyfikować reakcję programu na przewijanie za pomocš pasków przewijania, dokładnie to samo będziemy musieli zrobić z obsługš ko- munikatu WM KEYDOWN. Czy nie byłoby znacznie lepiej przetłumaczyć każdy z komunikatów WM KEY DOWN na jego odpowiednik w WM VSCROLL lub WMţHSCROLL? Mogliby- œmy na przykład przekonać WndProc, że otrzymuje komunikaty zwišzane z pa- skiem przewijania, wysyłajšc odpowiedni komunikat do procedury okna. System Windows udostępnia odpowiednie narzędzie. Jest to funkcja o nazwie SendMessage. Ma ona takie same parametry jak te, które przekazywane sš do pro- cedury okna: SendMessage (hwnd, message, wParam, lParam) ; Gdy wywołasz SendMessage, system Windows wywoła funkcję tego okna, które- go uchwyt podany został za pomocš hwnd, przekazujšc jej cztery argumenty funk- cji. Gdy procedura okna zakończy przetwarzanie komunikatu, sterowanie zosta- nie zwrócone do następnego polecenia znajdujšcego się po wywołaniu SendMes- sage. Procedura okna, do której wysyłasz komunikat, może być tš samš funkcjš okna, funkcjš innego okna lub nawet funkcjš okna innej aplikacji. Oto w jaki sposób powinniœmy posłużyć się funkcjš SendMessage w celu prze- tworzenia komunikatu WMţKEYDOWN w programie SYSMETS: case WM KEYDOWN switch (wParam) ( case UK_HOME: SendMessage break; (hwnd, WM USCROLL, SB TOP, 0) ; case UK_END: SendMessage (hwnd, WM VSCROLL, SB BOTTOM, 0) ; break; case VK_PRIOR: SendMessage (hwnd, WM USCROLL, SBţPAGEUP, 0) ; break; I tak dalej. Tak wyglšda ogólna struktura. Naszym celem było dodanie obsługi klawiatury do pasków przewijania, i zrobiliœmy to. Sprawiliœmy, że naciskanie klawiszy kursora powiela czynnoœci paska przewijania, wysyłajšc do procedury okna odpowiednie komunikaty. Teraz już wiesz, dlaczego w programie SY SMETS3 dodałem obsługę SB TOP oraz SB BOTTOM w komunikacie WMţVSCROLL. Nie była ona wtedy wykorzystana, jednak teraz poshxgujš się niš klawisze [Home] oraz [End]. W programie SYSMETS4, przedstawionym na rysunku 6-4, wprowadzone zostały opisane przed chwilš zmiany. Aby skompi- lować program, będziesz potrzebował pliku SYSMETS.H z rozdziału 4. 208 Częœć I: Podstawy SYSMETS4.C /* SYSMETS4.C - Pro9ram nr 4 wyœwietlajdcy wymiary elementów graficznych (c) Charles Petzold, 1998 */ ţţinclude ţţinclude "sysmets.h" LRESULT CAţLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) static TCHAR szAppName[] = TEXT ("SysMets4") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CS VREDRAW ; wndclass.lpfnWndProc = WndProc ; i wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI ŽPPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC ŽRROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITEţBRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) ( MessageBox (NULL, TEXT ("Program requires Windows NT!"), szAppName, MBţICONERROR) ; return 0 ; ) hwnd = CreateWindow (szAppName, TEXT ("Get System Metrics No. 4"), WS OVERLAPPEDWINDOW ţ WS_USCROLL ţ WSţHSCROLL, CW_USEDEFAULT, CW USEDEFAULT, CW_USEDEFAULT, CW USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) TranslateMessage (&msg) ; DispatchMessage (&msg) ; return msg.wParam ; LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ( Rozdział 6: Klawiatma 209 static int cxChar, cxCaps, cyChar, cxClient, cyClient, iMaxWidth ; HDC hdc ; int i, x, y, iVertPos, iHorzPos, iPaintBeg, iPaintEnd ; PAINTSTRUCT ps ; SCROLLINFO si ; TCHAR szBuffer[10] ; TEXTMETRIC tm ; switch (message) ( case WM_CREATE: hdc = GetDC (hwnd) ; GetTextMetrics (hdc, &tm) ; cxChar = tm.tmAveCharWidth ; cxCaps = (tm.tmPitchAndFamily & 1 ? 3 : 2) * cxChar / 2 ; cyChar = tm.tmHeight + tm.tmExternalLeading ; ReleaseDC (hwnd, hdc) ; // Zapamiętaj szerokoœć trzech kolumn iMaxWidth = 40 * cxChar + 22 * cxCaps ; return 0 ; case WM_SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; // Ustaw zakres pionowego paska przesuwu oraz wielkoœć strony si.cbSize = sizeof (si) ; si.fMask = SIFţRANGE ţ SIF PAGE ; si.nMin = 0 ; si.nMax = NUMLINES - 1 ; si.nPage = cyClient / cyChar ; SetScrollInfo (hwnd, SBţVERT, &si, TRUE) ; // Ustaw zakres poziomego paska przesuwu oraz // wielkoœć strony si.cbSize = sizeof (si) ; si.fMask = SIFţRANGE ţ SIF PAGE ; si.nMin = 0 ; si.nMax = 2 + iMaxWidth / cr,Char ; si.nPa9e = cxClient / cxChar ; SetScrollInfo (hwnd, SB HORZ, &si, TRUE) ; return 0 ; case WM_VSCROLL: // Pobierz wszystkie dane o przesunięciu w pionie si.cbSize = sizeof (si) ; si.fMask = SIF_ALL ; GetScrollInfo (hwnd, SB VERT, &si) ; // Zapamiętaj polożenia dla póŸniejszego porównania iVertPos = si.nPos ; 210 Częœć I: Podstawy (cišg dalszy ze strony 209) switch (LOWORD (wParam)) t case SB_TOP: si nPos = si.nMin ; break ; case SB_BOTTOM: si.nPos = si.nMax ; break ; case SB_LINEUP: si nPos -= 1 ; break ; case SB_LINEDOWN: si nPos += 1 ; break ; case SB_PAGEUP: si nPos -= si.nPage ; break ; case SB_PAGEOOWN: si nPos += si.nPage ; break ; case SB_THUMBTRACK: si.nPos = si.nTrackPos ; break ; default: break ; I // Ustaw polożenie, a następnie przywróć je. Na skutek ustawień // Windows może to nie być taka sama wartoœć, jaka zostala ustawiona. si.fMask = SIF_POS ; SetScrollInfo (hwnd, SB_VERT, &si, TRUE) ; GetScrollInfo (hwnd, SBţVERT, &si) ; // Jeżeli położenie uleglo zmianie, przesuń zawartoœć i odœwież i if (si.nPos != iVertPos) I ScrollWindow (hwnd, 0, cyChar * (iVertPos - si.nPos), NULL, NULL) ; UpdateWindow (hwnd) ; 1 return 0 ; I case WM_HSCROLL: // Pobierz wszystkie dane o przesunięciu w poziomie si.cbSize = sizeof (si) ; si.fMask = SIF ALL ; i Rozdział 6: Klawiatura 211 I // Zapamiętaj położenia dla póŸniejszego porównania GetScrollInfo (hwnd, SB HORZ, &si) ; iHorzPos = si.nPos ; switch (LOWORD (wParam)) I case SB_LINELEFT: 1 si.nPos -= 1 ; break ; case SB_LINERIGHT: j si.nPos += 1 ; ! break ; case SB_PAGELEFT: si.nPos -= si.nPage ; break ; case SB_PAGERIGHT: si.nPos += si.nPage ; break ; case SB_THUMBPOSITION: ; si.nPos = si.nTrackPos ; break ; j , default: i break ; ) i // Ustaw polożenie, a następnie przywróć je. Na skutek ustawień // Windows może to nie być taka sama wartoœć, jaka została ustawiona. si.fMask = SIF_POS ; SetScrollInfo (hwnd, SB_HORZ, &si, TRUE) ; GetScrollInfo (hwnd, SB HORZ, &si) ; // Jeżeli polożenie uleglo zmianie, przesuń zawartoœć okna if (si.nPos != iHorzPos) i ScrollWindow (hwnd, cxChar * (iHorzPos - si.nPos), 0, NULL, NULL) ; return 0 ; case WM KEYDOWN: switch (wParam) ( case UK_HOME: SendMessage (hwnd, WM USCROLL, SB TOP, 0) ; break ; case VK_END: SendMessage (hwnd, WM USCROLL, SBţBOTTOM, 0) ; break ; 212 Częœć I: Podstawy (cišg dalszy ze strony 211) case VK_PRIOR: SendMessage (hwnd, WM VSCROLL, SBţPAGEUP, 0) ; break ; case VK_NEXT: SendMessage (hwnd, WM VSCROLL, SBţPAGEDOWN, 0) ; break ; case VK_UP: SendMessage (hwnd, WM VSCROLL, SBţLINEUP, 0) ; break ; case VK_DOWN: SendMessage (hwnd, WM VSCROLL, SBţLINEDOWN, 0) ; break ; case VK_LEFT: SendMessage (hwnd, WM HSCROLL, SBţPAGEUP, 0) ; break ; case VK_RIGHT: SendMessage (hwnd, WM HSCROLL, SBţPAGEDOWN, 0) ; break ; ) return 0 : case WMţPAINT: hdc = BeginPaint (hwnd, &ps) ; // Pobierz polożenie pionowego paska przewijania si.cbSize = sizeof (si) ; si.fMask = SIFţPOS ; GetScrollInfo (hwnd, SB VERT, &si) ; iVertPos = si.nPos ; // Pobierz polożenie poziomego paska przewijania GetScrollInfo (hwnd, SB HORZ, &si) ; iHorzPos = si.nPos ; // Okreœl ograniczenia w rysowaniu iPaintBeg = max (0, iVertPos + ps.rcPaint.top / cyChar) ; iPaintEnd = min (NUMLINES - 1, iVertPos + ps.rcPaint.bottom / cyChar) ; for (i = iPaintBeg ; i <= iPaintEnd ; i++) ( x = cxChar * (1 - iHorzPos) ; y = cyChar * (i - iVertPos) ; TextOut (hdc, x, y, sysmetricsCi].szLabel, lstrlen (sysmetricsCi].szLabel)) ; - Rozdzial 6: Klawiatura 213 TextOut (hdc, x + 22 * cxCaps, y, sysmetrics[iJ.szDesc, lstrlen (sysmetrics[i7.szDesc)) ; l SetTextAlign (hdc, TAţRIGHT ţ TŽ TOP) ; TextOut (hdc, x + 22 * cxCaps + 40 * cxChar, y, szBuffer, wsprintf (szBuffer, TEXT ("%5d"), GetSystemMetrics (sysmetrics[iJ.iIndex))) ; SetTextAlign (hdc, TAţLEFT ţ TŽ TOP) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: PostOuitMessage (0) ; return 0 ; return DefWindowProc (hwnd, messa9e, wParam, lParam) ; Rysunek 6-2. Program SYSMETS4 Komunikaty znakowe Wczeœniej w tym rozdziale przedstawiłem pomysł konwersji komunikatów kla- wiszowych (ang. keystroke messages) na znakowe, polegajšcy na wzięciu pod uwagę stanu klawiszy specjanych. Ostrzegłem cię, że posługiwanie się tylko tš informa- i cjš to zbyt mało: powinieneœ również wzišć pod uwagę język i układ klawiatury. Z tego powodu nie powinieneœ dokonywać konwersji komunikatów klawiszo- ! wych na odpowiadajšce im znaki. Zrobi to za ciebie system Windows. Z pewno- œciš widziałeœ już przedstawiony poniżej fragment kodu: while (GetMessage (&msg, NULL, 0, 0)) TranslateMessage (&Msg) ; DispatchMessage (&msg) ; ) ';ł Jest to typowa pętla przetwarzajšca komunikaty, która pojawia się w funkcji Win- Main. Zadaniem GetMessage jest pobranie kolejnego komunikatu z kolejki i urniesz- czenie towarzyszšcych mu informacji w strukturze msg. Z kolei funkcja Dispatch- Message wywołuje odpowiedniš procedurę okna w celu przetworzenia tego ko- munikatu. i Pomiędzy tymi funkcjami wywoływana jest funkcja TranslateMessage. ej zadaniem , J jest dokonanie konwersji komunikatu klawiszowego na komunikat znakowy. Je- i żeli komunikatem klawiszowym jest WM ICEYDOWN lub WM SYSKEYDOWN, a kombinacja naciœniętych ktawiszy odpowiada literze, to TranslateMessage umie- œci komunikat znakowy w kolejce. Będzie to następny komunikat, który zostanie pobrany przez funkeję GetMessage po komunikacie klawiszowym. Częœć I: Podstawy 214 Cztery komunikaty znakowe Istniejš cztery komunikaty znakowe: Znaki Znaki martwe Znaki niesystemowe WM CHAR WM DEADCHAR Znaki systemowe WM SYSCHAR WM SYSDEADCHAR Komunikaty WM CHAR i WM DEADCHAR zwišzane sš z WM KEYDOWN, natomiast WM SYSCHAR oraz WM SYSDEADCHAR - z WM SYSKEYDOWN. (O tym, czym sš tzw. martwe znaki - za chwilę). A teraz dobra wiadomoœć: twój program powinien przechwytywać wyłšcznie ko- munikat WM CHAR, ignorujšc pozostałe trzy. Parametr lParam towarzyszšcy każ- demu z czterech komunikatów wymienionych przed chwilš ma dokładnie takš samš zawartoœć jak lParam, towarzyszšcy komunikatowi klawiszowemu, który spowo- dował wygenerowanie komunikatu znakowego. Jednakże wParam nie przechowuje wirtualnego kodu klawisza. Natomiast zawiera kod znaku ANSI lub unikodu. Przedstawione przed chwilš komunikaty znakowe sš pierwszymi, które odpowia- dajš za dostarczanie tekstu do procedury okna. Nie sš jednak jedyne. Zupełnie inne komunikaty towarzyszš łańcuchom tekstowym zakończonym zerem (ang. zero-ter- minated text string). W jaki sposób funkcja może się zorientować, czy ma do czynie- nia z 8-bitowym znakiem ANSI czy też z 16-bitowym znakiem unikodu To pro- ste: dowolna procedura okna zwišzana z klasš okna zarejestrowanš z wykorzysta- niem Re isterClassA (wersja ANSI klasy RegisterClass) otrzyma komunikat, który g ry š zawiera kod ANSI. Z kolei komunikaty przekazywane do procedu okna zwi za- nej z klasš RegisterClassW ("długa' wersja RegisterClass) otrzymajš znaki unikodu. Jeżeli pisany przez ciebie program rejestruje okno klasy RegisterClass, a w trakcie jego kompilacji zdefiniowany został identyfikator UNICODE, klasa ta będzie od- powiadała RegisterClassW, a RegisterClass w przeciwnym wypadku. O ile jawnie nie wykorzystujesz funkcji ANSI oraz unikodu a także proced y y okna, kod znaku towarzyszšcy komunikatowi WM CHAR (jak również inn m komunikatom znakowym) to (TCHAR) wParam Możliwa jest sytuacja, że jedna i ta sama procedura okna wykorzystywana jest w dwóch klasach okna, przy czym jedna z rch jest zarejestrowana jako Register- ClassA, druga natomiast - jako RegisterClassW. W takiej sytuacji procedura okna może odebrać komunikaty znakowe zawierajšce zarówno znaki ANSI, jak i uni- kodu. W takiej sytuacji, gdy potrzebujesz choćby niewielkiej pomocy, wewnštr'z procedury okna możesz wywołać: fUnicode = IsWindowUnicode (hwnd) : Jeżeli procedura okna została wywołana na rzecz okna hwnd, które powinno otrzy- mywać komunikaty unikodu, zmienna f Unicode będzie zawierać wartoœć TRUE. Oznacza to, że okno zbudowane zostało w oparciu o klasę okna RegisterClassW. Rozdział 6: Klawiatura 215 Kolejnoœć komunikatów Ponieważ komunikaty znakowe generowane sš przez funkcję TranslateMessage na podstawie komunikatów WM KEYDOWN oraz WM SYSKEYDOWN, sš one dostarczane do procedury okna na przemian z komunikatami klawiszowymi. Je- żeli na przykład klawisz [Caps Lock) nie został naciœnięty, naciœnięcie i zwolnie- nie klawisza [A] spowoduje wygenerowanie następujšcej sekwencji komunika- tów: Komunikat Klawisz lub kod WM KEYDOWN Wirtualny kod klawisza [A] (0x41) WM CHAR Kod znaku "a" (0x61) WM KEYUP Wirtualny kod klawisza [A] (0x41) Jeżeli natomiast zamierzasz wpisać dużš ]iterę "A", naciskajšc klawisz [Shift], następnie k]awisz [A], zwalniajšc [A] i wreszcie zwalniajšc [Shift], procedura okna otrzyma pięć komunikatów: Komunikat Klawisz lub kod WM KEYDOWN Wirtualny kod klawisza [Shift] (0x10) WM KEYDOWN Wirtualny kod klawisza [A] (0x41) WM CHAR Kod znaku "a" (0x41) WM KEYUP Wirtualny kod klawisza [A] (0x41) i WM KEYUP Wirtualny kod klawisza [Shift] (0x10) I K]awisz [Shift] jako taki nie powoduje wygenerowania komunikatu znakowego. Jeże]i przytrzymasz naciœnięty klawisz [A] tak długo, że rozpocznie się auto- matyczne powtarzanie, każdemu komunikatowi WMţKEYDOWN odpowiadać będzie jeden komunikat znakowy: Ž Komunikat Klawisz lub kod WM KEYDOWN Wirtualny kod klawisza [A] (0x41) WM CHAR Kod znaku "a" (0x61) WM KEYDOWN Wirtualny kod klawisza [A] (0x41) WMţCHAR Kod znaku "a" (0x61) WM KEYDOWN Wirtualny kod klawisza [A] (0x41) WM CHAR Kod znaku "a" (0x62) ( WM KEYDOWN Wirtualny kod klawisza [A] (0x41) WM CHAR Kod znaku "a" (0x61) WM KEYUP Wirtualnv kod klawisza [A1 (0x41) Częœć I: Podstawy 216 I Jeżeli wartoœć Jicznika powtórzeń częœci komunikatów będzie różna od 1, odpo- wiadajšce im komunikaty WM CHAR będš miały takš samš wartoœć licznika. Jednoczesne naciœnięcie klawisza [Ctrl] i dowolnego klawisza litery powoduje wygenerowanie znaków sterujšcych ASCII, których kody należš do przedziału od 0x01 ((Ctrl+A]) do OxlA ((Ctrl+Z]). Częœć z nich może zostać wygenerowana również przez klawisze pokazane w poniższej tabeli: Klawisz Kod znaku Powielany przez Kody sterujšce ANSI C [Backspace] 0x08 [Ctrl+H] \b [Tab] 0x09 [Ctrl+I] \t [Ctrl+Enter] OxOA [Ctrl+J] \n [Enter] OxOD [Ctrl+M] \r [Esc] OxlB [Ctrl+[] W ostatniej kolumnie po prawej stronie przedstawione zostały kody sterujšce ANSI C odpowiadajšce kodom odpowiednich kJawiszy. Od czasu do czasu programy Windows posługujš się kombinacjš klawisza [Ctrl] oraz jakiejœ litery jako skrótem do polecenia znajdujšcego się w menu (zajmiemy się tym w rozdziale 10). W tym przypadku klawisz danej litery nie jest tłumaczo- ny na komunikat znakowy. Naciskane klawisze a znaki Podstawowa reguła przetwarzania komunikatów klawiszowych to: jeżeli chcesz wczytać dane wprowadzane z kJawiatury, powinieneœ przechwycić komunikat WM_CHAR. Jeżeli natomiast zamierzasz odczytać naciœnięcia kJawiszy kurso- ra, funkcyjnych, [Delete], [Insert], [Shift], [Ctrl] czy też [Alt] - przechwyć WM_KEYDOWN. A co z klawiszem [Tab]? A [Enter], [Backspace] czy też [Esc]? Tradycyjnie, jak zostało to przedstawione w poprzedniej tabeli, klawisze te generujš kody steru- jšce ASCII. Jednakże w systemie Windows generujš one również wirtualne kody klawiszy. Powinniœmy więc przetwarzać je w WM CHAR czy też w WM KEY- DOWN? Po rozważeniu wszystkich za i przeciw (oraz po przejrzeniu wielu programów, które napisałem) doszedłem do wniosku, że do tej pory traktowałem klawisze [Tab], [Enter], [Backspace] oraz [Esc] jako kody sterujšce, a nie jako klawisze wir- tualne. Dlatego też pisana przeze mnie obsługa komunikatu WM CHAR wyglš- da zwykle następujšco: case WM CHAR [inne 7inie programuJ switch (wParam) ( case '\b': // CBackspace7 [inne linie programu7 break ; Rozdział 6: Klawiatura 217 case '\t': // CTab7 [inne linie programuJ break ; case '\n': // nowa linia [inne linie programuJ break ; f case `\r': // powrót karetki [inne linie programuJ break ; default: // kody znaków [inne linie programuJ break ; ) return 0 ; Komunikaty martwych znaków Programy pisane do pracy w systemie Windows zwykle mogš ignorować komu- nikaty WM DEADCHAR oraz WM SYSDEADCHAR, jednak programiœci mu- szš wiedzieć, czym sš martwe znaki (ang. dead characters) oraz w jaki sposób mogš być wykorzystane. Niestandardowe klawiatury (inne niż stosowana w USA) posiadajš pewne kla- wisze przeznaczone do dodawania znaków diaktrycznych do liter. Noszš one nazwę martwych klawiszy (ang. dead keys), ponieważ same nie sš w stanie gene- rować jakichkolwiek znaków. Na przykład po zainstalowaniu klawiatury niemiec- kiej klawisz oznaczony [+\=] na klawiaturze amerykańskiej staje się martwym klawiszem pozwalajšcym na dodanie akcentu ('). Gdy użytkownik naciœnie taki klawisz, do procedury okna wysłany zostanie ko- munikat WM DEADCHAR, przy czym jego parametr wParam będzie zawierał znak kodu ANSI lub unikodu dla tego znaku diaktrycznego. Gdy następnie użyt- kownik naciœnie klawisz ze znakiem, który może być użyty z tym znakiem diak- trycznym (na przykład klawisz [A]), do procedury okna wysłany zostanie komu- nikat WM-CHAR, z parametrem wParam zawierajšcym kod ANSI znaku "a" z od- owiednim akcentem. p Podsumowujšc: program nie musi obshxgiwać komunikatu WM DEADCHAR, ponieważ następujšcy po nim WM CHAR przeœle do niego wszelkie niezbędne informacje. Co więcej, system Windows posiada wbudowanš obsługę błędów: jeżeli martwy klawisz poprzedził znak, z którym nie może zostać wykorzystany (na przykład "s"), do procedury okna wysłane zostanš dwa komunikaty WM CHAR. Pierwszy z nich zawierać będzie kod znaku diaktrycznego - taki sam, który odebrany został w ramach obsługi komunikatu WM DEADCHAR, oraz WM CHAR z parametrem wParam równym wartoœci kodu ASCII dla zna- ku "s". Oczywiœcie, najlepszš metodš zorientowania się, jak to wszystko działa, jest ob- serwowanie komunikatów kršżšcych w "żywej" aplikacji. Najpierw powinieneœ zainstalować obsługę jakiejœ klawiatury narodowej (na przykład niemieckiej). 2ţg Częœć I: Podstawy I Możesz to zrobić otwierajšc w Panelu sterowania aplet Klawiatura, a następnie kartę Język. Potem powinieneœ już uruchomić tylko aplikację, która pozwoli ci obejrzeć wszystkie odbierane komunikaty zwišzane z klawiaturš. W tym celu możesz posłużyć się programem KEYVIEWI, który wkrótce zaprezentuję. Komunikaty klawiaturowe a zestawy znaków Wszystkie programy, których kody Ÿródłowe przedstawione zostanš do końca tego rozdziału, majš pewnš niewielkš wadę: nie będš działały poprawnie we wszystkich wersjach Windows. Nie jest to wada, którš celowo wprowadziłem do kodu. Co więcej, możliwe jest, że nigdy się z niš nie zetkniesz. Problemy - wa- ham się nazwać je błędami - pojawiš się tylko wtedy, gdy będziesz przełšczał się pomiędzy kilkoma różnymi układami klawiatury i językami oraz gdy programy będš uruchamiane pod wersjami Windows przeznaczonymi na rynki Dalekiego Wschodu, w których wykorzystywane sš wielobajtowe kody znaków. Wszystkie przedstawione programy będš pracować znacznie lepiej, gdy zostanš skompilowane dla unikodu a uruchomione w Windows NT. Ta obietnica, którš złożyłem w rozdziale 2, pokazuje, dlaczego unikod jest tak ważny, gdy program ma być tłumaczony na wiele języków. Program KEYVIEW1 Pierwszš czynnoœciš, którš musisz wykonać, aby zrozumieć, w jaki sposób dzia- łajš klawiatury przeznaczone dla różnych języków, jest przyjrzenie się komuni- katom odbieranym przez procedurę okna. Może tu pomóc program KEYVIEW1 przedstawiony na rysunku 6-3. W swoim obszarze roboczym wyœwietla on wszyst- kie informacje, które system Windows wysyła do procedury okna w ramach oœmiu różnych komunikatów klawiaturowych. KEYUIEWl.C /* KEYVIEWl.C - Wyœwietla komunikaty klawiaturowe i znakowe (c) Charles Petzold, 1998 */ ţţinclude LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) : int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) static TCHAR szAppName[] = TEXT ("KeyViewl") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CS VREDRAW : wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; Rozdział 6: Klawiatura 219 wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC ŽRROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITEţBRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; , if (!RegisterClass (&wndclass)) ( MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MBţICONERROR) ; ,ţrr n . ' } hwnd = CreateWindow (szAppName, TEXT ("Keyboard Message Viewer ţţ1"), WS OVERLAPPEDWINDOW, ' CW_USEDEFAULT, CWţUSEDEFAULT, CWţUSEDEFAULT, CWţUSEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)> i f TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; } LRESULT CALLBACK WndProc (HWND hwnd, UINT messa9e, WPARAM wParam, LPARAM lParam) ( static int cxClientMax, cyClientMax, cxClient, cyClient, cxChar, cyChar ; static int cLinesMax, cLines ; static PMSG pmsg ; static RECT rectScroll ; static TCHAR szTopC] = TEXT ("Message Key Char ") TEXT ("Repeat Scan Ext ALT Prev Tran") ; static TCHAR szUndC] = TEXT (" ţ _ ") TEXT (" ţ ţ ţ ţ ţ") ; static TCHAR * szFormatC2] = ( TEXT ("%-13s %3d %-15s%c%6u %4d %3s %3s %4s %4s"), TEXT ("%-13s Ox%04X%ls%c %6u %4d %3s %3s %4s %4s") } ; static TCHAR * szYes = TEXT ("Yes") ; static TCHAR * szNo = TEXT ("No") ; static TCHAR * szDown = TEXT ("Down") ; static TCHAR * szUp = TEXT ("Up") ; . a: static TCHAR * szMessage C] = ( TEXT ("WM_KEYDOWN"), TEXT ("WM_KEYUP"), TEXT ("WM_CHAR"), TEXT ("WM_DEADCHAR"), TEXT ("WM_SYSKEYDOWN"), TEXT ("WM_SYSKEYUP"), TEXT ("WM SYSCHAR"),. TEXT ("WM SYSDEADCHAR") } ; HDC hdc ; int i, iType : PAINTSTRUCT ps ; 220 Częœć I: Podstawy (cišg dalszy ze strony 219) . TCHAR szBuffer[128], szKeyName [32] ; ! TEXTMETRIC tm ; switch (message) i case WM_CREATE: case WM DISPLAYCHANGE: // Wyznacz maksymalny obszar roboczy cxClientMax = GetSystemMetrics (SM CXMAXIMIZED) ; cyClientMax = GetSystemMetrics (SM CYMAXIMIZED) ; // Pobierz wielkoœć znaku dla czcionki // o stalej szerokoœci hdc = GetDC (hwnd) ; SelectObject (hdc, GetStockObject (SYSTEMţFIXEDţFONT)) ; GetTextMetrics (hdc, &tm) ; cxChar = tm.tmAveCharWidth ; cyChar = tm.tmHeight ; ReleaseDC (hwnd, hdc) ; // Przydziel pamięć na wyœwietlane linie if (pmsg) free (pmsg) ; cLinesMax = cyClientMax / cyChar ; pmsg = malloc (cLinesMax * sizeof (MSG)) ; cLines = 0 ; // kontynuuj case WM_SIZE: if (messa9e = WMţSIZE) ( cxClient = LOWORD (lParam) : cyClient = HIWORD (lParam) ; ) // Wyznacz przesuwany prostokdt rectScroll.left = 0 ; rectScroll.right = cxClient ; rectScroll.top = cyChar ; rectScroll.bottom = cyChar * (cyClient / cyChar) ; InvalidateRect (hwnd, NULL, TRUE) ; return 0 ; case WM_KEYDOWN: case WM_KEYUP: case WM_CHAR: case WM_DEADCHAR: case WM_SYSKEYDOWN.: case WM SYSKEYUP: Rozdział 6: Klawiatura 221 case WM SYSCHAR: case WM SYSDEADCHAR: // Przebuduj tablicę przechowujšcš dane for (i = cLinesMax - 1 ; i > 0 ; i-) pmsgCi] = pmsgCi - 1] ; // Przechowaj nowy komunikat pmsgCO].hwnd = hwnd ; pmsgCO].message = message ; pmsgCO].wParam = wParam ; pmsgCO].lParam = lParam ; cLines = min (cLines + 1, cLinesMax) ; // Przewijaj w górę i ScrollWindow (hwnd, 0, -cyChar, &rectScroll, &rectScroll) ; break ; // tzn. wywolaj DefWindowProc, aby // przetworzyć pozostale komunikaty case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; i'. SelectObject (hdc, GetStockObject (SYSTEM FIXED-FONT)) ; SetBkMode (hdc, TRANSPARENT) ; TextOut (hdc, 0, 0, szTop, lstrlen (szTop)) ; 3. TextOut (hdc, 0, 0, szUnd, lstrlen (szUnd)) ; for (i = 0 : i < min (cLines, cyClient / cyChar - 1) ; i++) i iType = pmsgCi].message = WM_CHAR ţţ i pmsgCi].message = WM_SYSCHAR ţţ pmsgCi].message == WM_DEADCHAR ţ pmsgCi].message = WM-SYSDEADCHAR ; GetKeyNameText (pmsgCi].lParam, szKeyName, sizeof (szKeyName) / sizeof (TCHAR)) ; TextOut (hdc, 0, (cyClient / cyChar - 1 - i) * cyChar, szBuffer, wsprintf (szBuffer, szFormat CiType], szMessage CpmsgCi].message - WM-KEYFIRST], pmsgCi].wParam, (PTSTR) (iType ? TEXT (" ") : szKeyName), (TCHAR) (iType ? pmsgCi].wParam : ' '), LOWORD (pmsgCi].lParam), HIWORD (pmsgCi].lParam) ţ OxFF, 0x01000000 & pmsg[i].lParam ? szYes . szNo, 0x20000000 & pmsgCi].lParam ? szYes . szNo, 0x40000000 & pmsgCi].lParam ? szDown : szUp, 0x80000000 & pmsgCi].lParam ? szUp . szDown)) ; 4d. 3. EndPaint (hwnd, &ps) ; return 0 ; s ' i 222 Częœć I: Podstawy (cišg dalszy ze strony 221) case WM_DESTROY: PostOuitMessage (0) ; return 0 ; 1 return DefWindowProc (hwnd, message, wParam, lParam) ; Rysunek 6-3. Program KEYVIEW1 Program KEYVIEW1 wyœwietla informacje towarzyszšce każdemu naciœniętemu klawiszowi oraz komunikatowi znakowemu, który odebrany zostanie przez pro- cedurę okna. Dane te przechowywane sš w tablicy strukiur MSG. Jej rozmiar wy- znaczany jest na podstawie wielkoœci zmaksymalizowanego okna oraz czcionki systemowej o stałej szerokoœci. Jeżeli w trakcie działania programu użytkownik zmodyfikuje rozdzielczoœć ekranu (w tym przypadku KEYVIEWI odbierze ko- munikat WMţDISPLAYCHANGE) rozmiar tablicy zostanie ponownie wyznaczo- ny. Przydzielajšc tablicy pamięć, KEYVIEW1 posługuje się standardowš funkcjš języka C malloc. Na rysunku 6-4 została przedstawiona zawartoœć okna programu KEYVIEW1 0 wpisaniu słowa "Windows". W pierwszej kolumnie znajdujš się komunikaty kla- wiaturowe. Druga kolumna zawiera wirtualny kod naciœniętego klawisza oraz jego nazwę. Została ona pobrana za pomocš funkcji GetKeyNameText. Trzecia ko- lumna (o nazwie "Char") zawiera zapisany w systemie szesnastkowym kod zna- ku przesłany w komunikacie znakowym oraz sam znak. Pozostałe szeœć kolumn wyœwietla zawartoœć szeœciu pól parametru IParam. Char ReDeat Scan Ext ALT Pręv Tran 76 Right ShiFt 1 SN No No Up Dawn 87 W 1 17 No Ho Up Down OxAAS7 W 1 17 Ho Hv Up Dvvm B7 W 1 17 No No Down Up 16 Right ShiFt 1 54 Ho No Down Up 73 I 1 23 Ho Nv Up Down OxAA69 1 1 23 N0 No Up Down 73 I 1 23 No No Dvwn Up 7A H 1 A9 Ho No Up Down BxAA6E n 1 49 No Ho Up Doun 7A N 1 49 Ho No Down up 6A D 1 32 Mo No Up Doun OxAA6& G 1 32 Mo Ho Up Doan 6A D 1 32 Mo Mo Down Up 79 A 1 z4 No No up Doan BxAA6F o 1 2ţ Ho Ho Up Doan 79 0 1 24 Ho Ho DoWn up A7 W 1 17 Mo No Up Doţ BxAA77 W 1 17 No No Up Down B7 W 1 17 No No Down Up 8S S 1 8t Ho No Up Doan BxAA73 s 1 3Y Ho Ho Up Daan ea s 1 37 Ho Ho Dovn Up Rysunek 6-4. Okno programu KEYVIEWI Aby ułatwić wyœwietlanie informacji w postaci kolumn, w programie KEYVIEW1 wykorzystana została czcionka o stałej szerokoœci. Jak wiemy z poprzedniego roz- dziahz, wymagało to wywołania funkcji GetStockObject oraz SelectObject: SelectObject (hdc, GetStockObject (SYSTEMţFIXED FONT)) ; Rozdziałó: Klawiatura 223 W górnej częœci obszaru roboczego okna programu KEYVIEW1 wyœwietla nagłó- wek zawierajšcy nazwy wykorzystywanych przez niego dziewięciu kolumn. Po- szczególne nazwy sš podkreœlone. Chociaż możliwe jest stworzenie czcionki pod- kreœlonej, zdecydowałem się tu na inne rozwišzanie. Zdefiniowałem dwa napisy o nazwach szTop (zawierajšcy tekst) oraz szUnd (zawierajšcy podkreœlenie), a na- stępnie wyœwietliłem oba w tym samym miejscu w górnej częœci okna w trakcie obsługi komunikatu WM PAINT. Zwykle system Windows, wyœwietlajšc tekst w jednym miejscu, usuwa jednoczeœnie to, co się tam znajdowało poprzednio. Spowodowałoby to, że napis szUnd wyœwietlany jako drugi zamazałby pierwszy (szTop). Aby temu zapobiec, należy przełšczyć kontekst urzšdzenia w tryb prze- zroczysty (ang. transparent): SetBkMode (hdc, TRANSPARENT) ; Taka metoda podkreœlania jest możliwa wyłšcznie wtedy, kiedy wykorzystywa- na jest czcionka o stałej szerokoœci. Jeżeli tak by nie było, mogłoby się okazać, że znaki użyte do podkreœlania majš innš szerokoœć, niż użyte do wyœwietlenia na- główków kolumn. Problem z klawiaturami narodowymi Jeżeli korzystasz z amerykańskiej wersji systemu Windows, możesz zainstalować różne układy klawiatury, dzięki czemu będziesz mógł pisać, posługujšc się zna- kami charakterystycznymi dla innych języków. Możesz to zrobić posługujšc się znajdujšcym się w Panelu sterowania apletem Klawiatura. Wybierz kartę Język, a następnie kliknij przycisk Dodaj. Jeżeli chcesz zobaczyć, w jaki sposób obsługi- wane sš martwe klawisze, zainstaluj klawiaturę niemieckš. Zajmę się również układem rosyjskim oraz greckim, dlatego też możesz zainstalować je od razu. Jeżeli układy te nie sš dostępne, konieczne może okazać się zainstalowanie obsługi wielu języków. W tym celu uruchom również aplet Dodaj/Usuń programy w Panelu sterowania i otwórz kartę Instalator Windows. Upewnij się, czy pole wyboru Ob- sługa wielu języków jest zaznaczone. Prawdopodobnie po dokonaniu zmian ko- nieczne będzie poshzżenie się dyskiem instalacyjnym Windows. Po zainstalowaniu innych układów klawiatury w prawym rogu paska zadań pojawi się niebieski prostokšt z dwuliterowym kodem. Będzie to "En", jeżeli do- myœlnym układem klawiatury jest układ angielski. Po kliknięciu ikony zostanie wyœwietlone menu zawierajšce listę zainstalowanych układów klawiatury. Aby zmienić układ, kliknij po prostu nowš nazwę. Zmiana ta będzie miała wpływ wy- łšcznie na aktywny program. Możesz teraz rozpoczšć eksperymenty. Najpierw skompiluj program KEYVIEW bez zdefiniowanego symbolu UNICODE. (Na CD-ROM-ie dołšczonym do tej ksišżki wersja programu nie posługujšca się unikodem znajduje się w katalogu RELEASE). Uruchom program w wersji amerykańskiej systemu Windows, a na- stępnie wpisz "abcde". Komunikaty WM CHAR niosš dokładnie takie informa- cje, jakich oczekiwałeœ: kody ASCII 0x61, 0x62, 0x63 i 0x64 oraz znaki a, b, c, d oraz e. Teraz, gdy program KEYVIEWI cišgle działa, zmień układ klawiatury na niemiec- ki. Naciœnij klawisz [=], a następnie samogłoskę (a, e, i, o lub u). Naciœnięcie [=] 224 Częœć I: Podstawy spowoduje wygenerowanie komunikatu WMţDEADCHAR, a każdej z samogło- sek komunikat WMţCHAR z kodem (odpowiednio) OxEI, OxE9, OxED, OxF3, OxFA i znaki "„", "ó", "i", "ó" oraz "". Oto w jaki sposób działajš martwe klawisze. Gdy program KEYVIEWI cišgle działa, przełšcz układ klawiatury na grecki. Wpisz "abcde". Co otrzymałeœ? Pojawiły się komunikaty WMţCHAR z kodami OxEl, OxE2, OxF8. OxE4, OxE5 oraz ze znakami "„", "„", "ţ", "„" oraz "„". Wydaje się, że coœ nie jest tak, jak powinno być. Czy nie powinieneœ otrzymać liter alfa- betu greckiego? Włšcz teraz rosyjski układ klawiatury i ponownie wpisz "abcde". Otrzymałeœ teraz komunikaty WMţCHAR z kodami odpowiednio OxF4, OxE8, OxE2 oraz OxF3 oraz znaki "”", "e", "ń", "„' oraz "ó". I znowu coœ jest Ÿle. Powinieneœ przecież otrzy- mać znaki z cyrylicy. Problem polega na tym, że przełšczyłeœ układ klawiatury tak, aby generowane były inne kody znaków, nie przełšczyłeœ natomiast GDI tak, aby mogły one zo- stać poprawnie zinterpretowane. Jeżeli jesteœ odważny i dysponujesz jakimœ niepotrzebnym zapasowym kompu- terem oraz posiadasz Universal Subscription to Microsoft Developer Network (MSDN), możesz zainstalować na przykład greckš wersję systemu Windows. Możesz również zainstalować te same cztery układy klawiatury (angielski, grec- ki, niemiecki oraz rosyjski). Uruchom teraz program KEYVIEWl. Uaktywnij an- gielski układ klawiatury i wpisz "abcde". Otrzymasz kody ASCII 0x61, 0x62, 0x63, 0x64 i 0x65 oraz znaki a, b, c, d i e. (Możesz odetchnšć: wyglšda na to, że kody ASCII działajš nawet w greckiej wersji systemu). Posługujšc się w dalszym cišgu greckš wersjš systemu, uaktywnij teraz grecki układ klawiatury i ponownie wpisz "abcde". W oknie programu KEYVIEW1 pojawi się seria komunikatów 4VMţCHAR z kodami OxEI, OxE2, OxF8, OxE4 oraz OxE5. Jak widać, sš to takie same kody, które pojawiły się w angielskiej wersji systemu z zainstalowanym greckim układem klawiatury. jednak tym razem wy- œwietlone zostanš litery a, á, yr, 8, E. Sš to oczywiœcie małe litery alfabetu greckie- go: alfa beta, psi, delta oraz epsilon. (A co się stało z gamma? Cóż, jeżeli miałbyœ zainstalowanš greckš wersję systemu Windows z greckim układem klawiatury, prawdopodobnie na klawiszach miałbyœ również greckie symbole liter. Tak się złożyło, że klawiszem odpowiadajšcym angielskiej literze c jest właœnie psi. Gam- ma znajduje się w tym miejscu, gdzie na klawiaturze angielskiej jest g. Pełny grecki układ klawiatury znajdziesz na stronie 587 ksišżki Nadine Kano Developing In- ternational Software for Windows 95 and Windows NT. Korzystajšc w dalszym cišgu z programu KEVIEWI uruchomionego w greckiej wersji Windows, przełšcz układ klawiatury na niemiecki. Naciœnij klawisz [=], a następnie [a], [e], [i], [o], oraz [u]. W oknie programu pojawi się seria komunika- tów WMţCHAR z kodami znaków odpowiednio OxEl, OxE9, OxED, OxF3 oraz OxFA. Jak widać, sš to takie same kody jak te, które pojawiły się w angielskiej wersji Windows z zainstalowanym niemieckim układem klawiatury. Jednakże wy- œwietlone zostanš znaki a, t, v, cs , a nie poprawne „, e, i, ó oraz . Zmień teraz układ klawiatury na rosyjski, a następnie wpisz "abcde". Otrzymasz kody znaków OxF4, OxE8, OxFl, OxE2 oraz OxF3 czyli takie same, jakie pojawiły rlţţ.lţ:ţt C. /(lnmiAflll'9 225 się w angielskiej wersji systemu z zainstalowanym rosyjskim układem klawiatu- ry. Jednakże wyœwietlone zostanš znaki i, 8, p, á i a. Możesz również zainstalować rosyjskš wersję systemu Windows. Jak prawdopo- dobnie już się domyœlasz, angielski i rosyjski układ klawiatury będš działać po- prawnie, ale nie niemiecki i grecki. Teraz, jeżeli jesteœ odważny do szaleństwa, możesz zainstalować japońskš wersję Windows i ponownie uruchomić program KEYVIEWI. Jeżeli posługujesz się kla- wiaturš amerykańskš, możesz bez problemów wprowadzać tekst angielski: wszystko wydaje się działać poprawnie. Jeżeli jednak uruchomisz układ klawia- tury niemiecki, grecki czy też rosyjski, a następnie powtórzysz powyższe ćwi- czenia, wprowadzane przez ciebie znaki będš wyœwietlane jako kropki. Jeżeli z ko- lei wpiszesz duże litery - niezależnie od tego, czy wpisujesz litery niemieckie, greckie czy też rosyjskie -na ekranie pojawiš się znaki katakana, alfabetu japoń- skiego wykorzystywanego do zapisywania słów innych języków. Możesz się w ten sposób całkiem nieŸle zabawić. Wersje systemu Windows przeznaczone do sprzedaży na Dalekim Wschodzie zawierajš program narzędziowy o nazwie Input Method Editor (edytor metod wejœciowych, w skrócie IME). Dzięki niemu możesz posługiwać się normalnš kla- wiaturš, aby wprowadzić znaki ideograficzne będšce znakami złożonymi wyko- rzystywanyxni w Chinach, Japonii oraz w Korei. Mówišc krótko: po wpisaniu kombinacji liter w osobnym oknie pojawia się symbol złożony. Jeżeli teraz naci- œniesz klawisz [Enter], do aktywnej aplikacji przesłane zostanš odpowiednie kody znaków (to znaczy, do KEYVIEW1). KEYVIEW zareaguje w sposób niemal cał- kowicie pozbawiony sensu - komunikaty WM CHAR będš przenosiły znaki o kodach powyżej 128, jednak same znaki sš pozbawione jakiegokolwiek znacze- nia. (Znacznie więcej informacji o wykorzystaniu IME znajdziesz we wspomnia- nej już ksišżce Nadine Kano). Tak więc, widzieliœmy już kilka sposobów, w jaki KEYVIEW1 wyœwietla niepopraw- ne znaki - gdy w angielskiej wersji Windows uaktywniony zostanie rosyjski lub grecki układ klawiatury, w greckiej wersji Windows z rosyjskim lub niemieckim układem klawiatury oraz w rosyjskiej wersji Windows z układem niemieckim, ro- syjskim lub greckim. Widzieliœmy również, jakie pojawiajš się błędy, gdy wyko- rzystany zostanie Input Method Editor w japońskiej wersji systemu Windows. Zestawy znaków a czcionki Tak naprawdę, problemy występujšce w programie KEYVIEW1 dotyczš czcion- ki. Czcionka, która wykorzystywana jest do wyœwietlania znaków na ekranie, nie jest zgodna z kodami znaków otrzymywanymi przez program z klawiatury. Zaj- mijmy się więc teraz czcionkami. Więcej szczegółów na ten temat znajdziesz w rozdziale 17, jednak już teraz mogę stwierdzić, że system Windows obsługuje trzy rodzaje czcionek - czcionki bit- mapowe, czcionki wektorowe oraz (poczšwszy od Windows 3.1) czcionki True- Type. Czcionki wektorowe sš już niemal nieużywane. Poszczególne znaki składane były z linii prostych, nie potrafiły jednak definiować wypełnianych obszarów. Czcionki T 226 Częœć Iţ Podstawy TrueType sš czcionkami skalowalnymi. W rzeczywistoœci defini a znaku zawie- " ra pewne "wskazówki dotyczšce zaokršgleń, dzięki którym możliwe jest unik- ruęcie sytuacji prowadzšcych do zamazania tekstu. To właœnie dzięki czcionkom TrueType o systemie Windows można powiedzieć, że jest WYSIWYG (ang. what you see is what you get - otrzymasz to, co widzisz) co znaczy, że tekst wyœwietla- ny na ekranie nie ulegnie zmianie po wydrukowaniu. W przypadku czcionek bitmapowych, każdy znak definiowany jest za pomocš tablicy bitów, które odpowiadajš poszczególnym punktom, z których składa się wyœwietlana litera. Czcionki bitmapowe mogš być powiększane, jednak spowo- duje to, że ich krawędzie będš postrzępione. Czcionki te sš zwykle projektowane tak, aby były łatwo czytelne na ekranie. Dlatego też system Windows posługuje się nimi wyœwietlajšc teksty w menu, paskach tytułu, na przyciskach oraz w oknach dialogowych. Czcionka bitmapowa, którš pobierasz w domyœlnym kontekœcie urzšdzenia, nosi nazwę czcionki systemowej. Jej uchwyt możesz pobrać, wywołujšc funkcję Get- StockObject i podajšc jej jako parametr identyfikator SYSTEM FONT. Program KEYVIEW1 posługuje się wersjš czcionki systemowej o stałej szerokoœci, której identyfikator to SYSTEM FIXED_FONT. Ten sam rezultat osišgniesz wywołujšc funkcję GetStockObject i podajšc jej parametr OEM FIXED FONT. Opisane przed chwilš trzy rodzaje czcionek noszš nazwy, odpowiednio: System, FixedSys oraz Terminal. Program może posłużyć się takš nazwš, aby okreœlić czcionkę, wywołujšc funkcję CreateFont lub CreateFontlndirect. Te trzy rodzaje czcio- nek przechowywane sš w dwóch zestawach po trzy pliki każdy w podkatalogu FONTS należšcym do katalogu, w którym zainstalowany został system Windows. To, który zestaw plików jest wykorzystywany, zależy od tego, czy zdecydowałeœ się na wyœwietlanie w systemie dużych czy też małych znaków (aplet Ekran w Pa- nelu sterowania), to znaczy, czy chcesz, aby Windows zakładał, że rozdzielczoœć ekranu to 96 dpi lub 120 dpi. Wszystko, co przed chwilš napisałem, podsumo- wuje poniższa tabela: Identyfikator Nazwa Plik Plik GetStockObject małej czcionki dużej czcionki SYSTEM FONT System VGASYS.FON 85145Y5.FON SYSTEM FIXED FONT FixedSys VGAFIX.FON 8514FIX.FON OEM FIXED FONT Terminal VGAOEM.FON 85140EM.FON Znajdujšce się w nazwach plików "VGA" jest skrótem pochodzšcym od Video Graphics Array. Jest to karta graficzna wprowadzona na rynek przez IBM w 1987 roku. Jako pierwsza karta IBM oferowała ona rozdzielczoœć 640 na 480 pikseli. Jeżeli w znajdujšcym się w Panelu sterowania aplecie Ekran wybierzesz Małe czcionki (co spowoduje, że system Windows założy, że rozdzielczoœć ekranu wynosi 96 dpi), wykorzystane zostanš pliki, które w nazwie posiadajš "VGA". Jeżeli z kolei wybierzesz Duże czcionki (co spowoduje, że Windows założy, że rozdzielczoœć ekranu wynosi 120 dpi) - system poshzży się plikami, których na- Rozdziałó: Klawiatura zwy rozpoczynajš się od "8514". 8514 była kolejnš kartš graficznš IBM (powsta- ła również w 1987 roku), umożliwiała pracę z rozdzielczoœciš 1024 na 768. System Windows nie chce, abyœ miał jakikolwiek kontakt z tymi plikami. Majš one ustawiony atrybut System oraz Hidden, co sprawi, że posługujšc się Eksplo- ratorem plików i wyœwietlajšc zawartoœć katalogu FONTS, w ogóle ich nie zoba- czysz. Będzie tak nawet wtedy, gdy zdecydujesz się na wyœwietlanie zarówno plików ukrytych, jak i systemowych. Możesz natomiast posłużyć się poleceniem ZnajdŸ z menu Narzędzia. Jeżeli wyszukasz wszystkie pliki pasujšce do wzorca *.FON i dwukrotnie klikniesz nazwę, będziesz mógł zobaczyć, jak wyglšda czcion- ka systemowa. W przypadku wielu standardowych kontrolek oraz kontrolek zdefiniowanych przez użytkownika, system Windows nie posługuje się czcionkš systemowš. Zamiast tego wykorzystywana jest czcionka o nazwie MS Sans Serif (MS to skrót od Microsoft). Jest to również czcionka bitmapowa. W pliku (o nazwie SSERIFE.FON) zdefinio- wane zostały czcionki dla rozdzielczoœci 96 dpi o rozmiarach (w punktach) 8, 10, 12, 14, 18 oraz 24.. Możesz je pobrać za pomocš funkcji GetStockObject oraz identy- fikatora DEFAULT GUI FONT. To, z jakiego rozmiaru korzysta system, zależy od rozdzielczoœci okreœlonej za pomocš apletu Ekran w Panelu sterowania. Jak dotšd, wspomniałem o czterech identyfikatorach, które możesz przekazać jako parametry funkcji GetStockObject, jeœli chcesz otrzymać czcionkę wykorzystywa- nš w danym kontekœcie urzšdzenia. Istniejš również trzy inne: ANSI FI- XED FONT, ANSI_VAR_FONT oraz DEVICE DEFAULT FONT. Chcšc przybli- żyć się do problemu klawiatury i wyœwietlanych znaków, przyjrzyjmy się wszyst- kim czcionkom w Windows. Umożliwia to program o nazwie STOKFONT. Zo- stał on przedstawiony na rysunku 6-5. STOKFONT.C /* STOKFONT.C - Zasoby Windows - Czcionki (c) Charles Petzold, 1998 */ ţţinclude LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) static TCHAR szAppName[] = TEXT ("StokFont") : HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CS VREDRAW ; wndclass.lpfnWndProc = WndProc : wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI ŽPPLICATION) wndclass.hCursor = LoadCursor (NULL, IDC ŽRRDW) ; 228 Częœć Iţ Podstawy (cišg dalszy ze strony 227) wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) MessageBox (NULL, TEXT ("Program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; hwnd = CreateWindow (szAppName, TEXT ("Stock Fonts"), WS OVERLAPPEDWINDOW ţ WSţVSCROLL, CW_USEDEFAULT, CW USEDEFAULT, CW USEDEFAULT, CW USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) f TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ( static struct ( int idStockFont ; TCHAR * szStockFont ; } stockfont [] = ( OEM_FIXED_FONT TEXT ("OEM_FIXED_FONT"), ANSI_FIXEDţFONT, TEXT ("ANSI_FIXEDţFONT"), ANSI_VARţFONT, TEXT ("ANSI_VARţFONT"), SYSTEMţFONT, TEXT ("SYSTEM_FONT"), DEVICE_DEFAULT FONT, TEXT ("DEVICE_DEFAULT_FONT"), SYSTEMţFIXEDţFONT, TEXT ("SYSTEM FIXED_FONT"), DEFAULTţGUIţFONT, TEXT ("DEFAULT GUIţFONT") } ; static int iFont, cFonts = sizeof stockfont / sizeof stockfont[0] ; HDC hdc ; int i, x, y, cxGrid, cyGrid ; PAINTSTRUCT ps ; TCHAR szFaceName [LF FACESIZE], szBuffer [LF FACESIZE + 64] ; TEXTMETRIC tm ; switch (message) t case WM_CREATE: SetScrollRange (hwnd, SBţVERT, 0, cFonts - 1, TRUE) ; return 0 ; Rozdział 6: Klawiatura I I case WM DISPLAYCHANGE: 1 InvalidateRect (hwnd, NULL, TRUE) : 4 return 0 ; case WM USCROLL: switch (LOWORD (wParam)) ( case SB_TOP: iFont = 0 ; break ; case SB_BOTTOM: iFont = cFonts - 1 ; break ; case SB_LINEUP: case SB_PAGEUP: iFont -= 1 ; break ; case SB_LINEDOWN: case SB_PAGEDOWN: iFont += 1 ; break ; case SB THUMBPOSITION: iFont = HIWORD (wParam) ; break ; - iFont = max (0, min (cFonts - 1, iFont)) ; SetScrollPos (hwnd, SB VERT, iFont, TRUE) ; - InvalidateRect (hwnd, NULL, TRUE) ; return o ; case WM KEYDOWN: switch (wParam) ( case VK_HOME: SendMessage (hwnd, WM VSCROLL, SB_TOP, 0) ; break : case VK_END: SendMessage (hwnd, WM-VSCROLL, SB-BOTTOM, 0) ; break ; i. case VK-PRIOR: case VK_LEFT: case UK_UP: SendMessage (hwnd, WM-USCROLL, SBţLINEUP, 0) : break ; 1;,... case VK_NEXT: case VK_RIGHT: case VK-DOWN: SendMessage (hwnd, WM-VSCROLL, SB-PAGEDOWN, 0) ; break ; 1 i:.. return 0 ; case WM-PAINT: hdc = BeginPaint (hwnd, &ps) ; i SelectObject (hdc, GetStockObject (stockfont[iFont].idStockFont)) ; GetTextFace (hdc, LF_FACESIZE, szFaceName) : GetTextMetrics (hdc, &tm) : cxGrid = max (3 * tm.tmAveCharWidth, 2 * tm.tmMaxCharWidth) ; cyGrid = tm.tmHeight + 3 ; TextOut (hdc, 0, 0, szBuffer, wsprintf (szBuffer, TEXT (" %s: Face Name = %s, CharSet = %i"), stockfont[iFont].szStockFont, szFaceName, tm.tmCharSet)) ; , SetTextAlign (hdc, TŽ TOP ţ TŽ CENTER) ; // linie poziome i pionowe for (i = 0 ; i < 17 ; i++) MoveToEx (hdc, (i + 2) * cxGrid, 2 * cyGrid, NULL) ; LineTo (hdc, (i + 2) * cxGrid, 19 * cyGrid) ; 230 Częœć Iţ Podstawy i (cišg dalszy ze strony 229) MoveToEx (hdc, cxGrid, (i + 3) * cyGrid, NULL) ; LineTo (hdc, 18 * cxGrid, (i + 3) * cyGrid) ; // nagłówki poziome i pionowe for (i = 0 ; i < 16 ; i++) i TextOut (hdc, (2 * i + 5) * cxGrid / 2, 2 * cyGrid + 2, szBuffer, wsprintf (szBuffer, TEXT ("%X-"), i)) ; TextOut (hdc, 3 * cxGrid / 2, (i + 3) * cyGrid + 2, szBuffer, wsprintf (szBuffer, TEXT ("-%X"), i)) ; ? // znaki for (y = 0 ; y < 16 ; y++) for (x = 0 ; x < 16 ; x++) i f TextOut (hdc, (2 * x + 5) * cxGrid / 2, (y + 3) * cyGrid + Z, szBuffer, wsprintf (szBuffer, TEXT ("%c"), 16 * x + y)) ; EndPaint (hwnd, &ps) ; return 0 ; case WM DESTROY: PostOuitMessage (0) ; return 0 ; ; return DefWindowProc (hwnd, message, wParam, lParam) ; 1 Rysunek 6-5. Program STOKFONT Jak widać program jest bardzo prosty. Dzięki niemu, posługujšc się paskiem prze- wijania oraz klawiszami kursora, możesz wybrać i wyœwietlić jednš z siedmiu czcionek systemowych. Program tworzy siatkę, w której wyœwietla 256 znaków wybranej czcionki. W nagłówkach u góry i po lewej stronie siatki znajdujš się wartoœci szesnastkowe okreœlajšce kody poszczególnych znaków. i W częœci obszaru roboczego program STOKFONT wyœwietla identyfikator, któ- ry został wykorzystany do pobrania czcionki za pomocš funkcji GetStockObject. ; Pojawia się tam również nazwa czcionki otrzymana za pomocš GetTextFace oraz ' I zawartoœć pola tmCharSet struktury TEXTMETRIC. Te informacje identyfikujšce czcionkę majš szczególne znaczenie dla zrozumienia, w jaki sposób uzyskano w systemie Windows możliwoœć wyœwietlania interfejsu użytkownika w wielu ; różnychjęzykach. Jeżeli uruchomisz program STOKFONT pod kontrolš angielskiej wersji Windows, pierwszy wyœwietlony ekran pokaże czcionkę pobranš za pomocš identyfikato- ra OEM FIXED FONT przekazanego do funkcji GetStockObject. Zawartoœć okna j programu przedstawiona została na rysunku 6-6. ; Rozdział 6: Klawiatura 231 Rysunek &6. Czcionka OEM FIXED FONT w angielskiej wersji systemu Windows W powyższym zestawie (podobnie jak we wszystkich zestawach, które zostanš przedstawione w tym rozdziale) częœć znaków to znaki ASCII. Pamiętaj jednak, że ASCII jest kodem 7-bitowym, który definiuje znaki możliwe do wyœwietlenia o kodach od 0x20 do Ox7E. W czasach, gdy firma IBM wyprodukowała swój pierw- szy komputer PC, kody 8-bitowe były już w pełni akceptowane, dzięki czemu również do kodowania poszczególnych znaków można było wykorzystać 8 bi- tów. W tej sytuacji IBM postanowił rozszerzyć standardowy zestaw znaków ASCII o znaki semigraficzne (pozwalajšce na rysowanie linii i bloków), litery z akcen- tem, litery greckie, symbole matematyczne i tym podobne. Wiele z programów pracujšcych pod kontrolš systemu MS-DOS wykorzystywało znaki semigrafiki do komponowania swoich interfejsów użytkownika. Częœć z tych programów po- sługiwała się dodatkowymi znakami również w tworzonych przez siebie plikach. Ten szczególny zestaw znaków sprawiał pewne kłopoty pierwszym programi- stom Windows. Z jednej strony, znaki semigraficzne nie były już potrzebne, po- nieważ Windows w całoœci jest systemem graficznym. Te 48 znaków mogłoby zostać wykorzystane na dodatkowe litery akcentowane, które sš bardzo potrzeb- ne na przykład w krajach Europy wschodniej. Z drugiej strony zestaw znaków zdefiniowany przez IBM stał się standardem, którego w żaden sposób nie można w całoœci ignorować. Dlatego też programiœci Windows zdecydowali, że w nowym systemie zestaw znaków IBM będzie w dalszym cišgu obecny, jednak nie będzie podstawowym zestawem. Jest obecnie wykorzystywany głównie przez aplikacje MS-DOS dzia- łajšce w oknie. Aplikacje pisane specjalnie dla Windows już się nim nie posługu- jš. Wraz z upływem lat traci on coraz bardziej na znaczeniu. Mimo to w dalszym cišgu, o ile jest to niezbędne, możesz z niego korzystać. W tym kontekœcie "OEM znaczy po prostu "IBM". (Powinieneœ wiedzieć, że inne wersje Windows niż angielska nie muszš udostęp- niać dokładnie takiego samego zestawu znaków OEM. Każdy kraj ma swój wła- sny zestaw. Jest to temat sam w œobie, jednak nie będę nim się zajmować w tej ksišżce). 232 Częœć I: Podstawy Ponieważ okazało się, że zestaw znaków IBM nie spełnia wymogów systemu Windows, wybrany został inny zestaw. Nosi on nazwę zestawu znaków ANSI. Mimo że jego nazwa odwohzje się do American National Standards Institute (Ame- rykański Narodowy Instytut Normalizacji) nie jest on objęty normš ISO (Interna- tional Standards Organization, Miedzynarodowa Organizacja Normalizacyjna), czyli normš 8859. Jest on również znany jako: Latin 1, Wester European lub wreszcie Code Page 1252. Na rysunku 6-7 przedstawiona została jedna wersja zestawu znaków ANSI - czcionka systemowa w angielskiej wersji systemu Windows. Rysunek 6-7. Czcionka SYSTEM FONT w angielskiej wersji systemu Windows Gruba pionowa linia oznacza te kody, którym nie zostały przypisane znaki. Zwróć uwagę, że i tym razem znaki o kodach od 0x20 do Ox7E to znaki ASCII. Również kody sterujšce ASCII (0x00 do OxlF oraz Ox7F) nie majš reprezentacji graficznej. Dokładnie tak powinno być. Z powodu kodów od OxCO do OxFF zestaw znaków ANSI ma istotne znaczenie dla wersji Windows pracujšcych w innych językach. Kody te odpowiadajš 64 znakom, które pojawiajš się w językach krajów Europy wschodniej. Znak o ko- dzie OxAO, który wyglšda jak spacja, jest spacjš nierozdzielajšcš (ang. nonbreaking space) tak, jak spacja w "WW II". Ze względu na obecnoœć znaków o kodach od 0x80 do Ox9F powiedziałem, że jest to "jedna wersja" zestawu znaków ANSI. Czcionka systemowa o stałej szero- koœci zawiera jedynie dwa takie znaki, co zostało przedstawione na rysunku 6-8. SYSTEM FONT: Face Name = System. CharSet = 0 Rozdział 6: Klawiatura 233 Rysunek 6-8. Czcionka SYSTEM FIXED FONT w angielskiej wersji systemu Win- dows W unikodzie kody od 0x0000 do 0x007F majš to samo znaczenie, co w ASCII, kody 0x0080 do 0x009F powielajš znaki o kodach od 0x0000 do 0x001F, natomiast kody 0x00A0 do 0x00FF sš odpowiednikiem zestawu znaków ANSI z Windows. Gdy korzystasz z niemieckiej wersji systemu Windows, to posługujšc się identy- fikatorami SYSTEM FONT oraz SYST'EM FIXED FONT przekazywanymi jako parametr funkcji GetStockObject, otrzymasz te same zestawy znaków ANSI. Jest to prawdš również w wersjach przeznaczonych na rynki innych krajów Europy wschodniej. Zestaw znaków ANSI został bowiem tak zaprojektowany, aby za- wierać wszystkie znaki wykorzystywane przez tamtejsze języki. Gdy jednak posługujesz się greckš wersjš Windows, domyœlny zestaw znaków nie jest taki sam. Wykorzystywana w tej wersji czcionka SYSTEM FONT przed- stawiona została na rysunku 6-9. SYSTEM FIXED FONT: Facv Namv = Fixvdsys, CharSvt = U 234 Częœć I: Podstawy Rysunek 6-9. Czcionka SYSTEM FONT w greckiej wersji systemu Windows Czcionka SYSTEM FIXED FONT zawiera takie same znaki. Zwróć uwagę na kody od OxCO do OxFF. Odpowiadajš one dużym i małym literom alfabetu grec- kiego. Domyœlny zestaw znaków wykorzystywany w rosyjskiej wersji systemu Windows przedstawiony został na rysunku 6-10. Rysunek 6-10. Czcionka SYSTEM FONT w rosyjskiej wersji systemu Windows Ponownie zwróć uwagę, że duże i małe litery cyrylicy majš kody od OxCO do OxFF. Na kolejnym rysunku, 6-11, przedstawiona została czcionka SYSTEM FONT uży- wana w japońskiej wersji Windows. Znaki o kodach od OxA5 do OxDF należš do alfabetu katakana. Rozdział 6: Klawiatura 235 Rysunek 6ů11. Czcionka SYSTEM FONT w japońskiej wersji systemu Windows Japońska czcionka systemowa różni się od tych, które przedstawione zostały poprzednio, ponieważ jest to w zasadzie dwubajtowy zestaw znaków, nazywa- ny Shift-JIS QIS jest skrótem od japanese Industrial Standard). Większoœć kodów od 0x81 do Ox9F oraz od OcEO do OxFF to po prostu pierwszy bajt kodu dwubaj- towego. Drugi bajt zawiera się zwykle w grarucach od 0x40 do OxFC. (Pełna ta- blica kodów znajduje się w dodatku A wspomnianej ksišżki Nadine Kano). Tak więc teraz już wiemy, gdzie leży problem z KEYVIEWI: jeżeli zainstalowa- łeœ grecki układ klawiatury i piszesz "abcde", to - niezależnie od zainstalowanej wersji systemu Windows - system generuje komunikaty WM CHAR z kodami OxEl, OxE2, OxFB, OxE2 oraz OxE5. Jednak kody te odpowiadajš znakom a, á, yr, s i s jedynie wtedy, kiedy korzystasz z greckiej wersji Windows z zainstalowanš greckš czcionkš systemowš. Jeżeli zainstalowałeœ rosyjski układ klawiatury i piszesz "abcde", to - niezależ- nie od zainstalowanej wersji Windows - system generuje komunikaty WMţCHAR z kodami OxF4, OxE8, OxFl, OxE2 oraz OxF3. Jednak kody te odpowiadajš znakom cţ, H, c, s i y jedynie wtedy, kiedy korzystasz z rosyjskiej wersji Windows, w której czcionkš systemowš jest cyrylica. Jeżeli masz zainstalowany niemiecki układ klawiatury i naciœniesz klawisz [=] (lub klawisz znajdujšcy się na tej samej pozycji), a następnie [a], [e], [i], [o] lub [u], to - niezależnie od zainstalowanej wersji systemu Windows - system gene- ruje komunikaty WM CHAR z kodami OxEI, OxE9, OxED, OxF3 oraz OxFA. Jedy- nie w przypadku, gdy korzystasz z wersji amerykańskiej lub zachodnioeuropej- skiej systemu, czyli z takiej, która dysponuje zachodnioeuropejskš czcionkš sys- temowš, kody te będš odpowiadały znakom ţ, ó, i, ó oraz . Jeżeli masz zainstalowany angielski układ klawiatury, możesz naciskać dowolne klawisze, a system Windows wygeneruje komunikaty WMţCHAR z kodami, które odpowiadajš tym klawiszom. T 236 Częœć Iţ Podstawy A co z unikodem? W rozdziale 2 wspomniałem, że korzystanie z unikodu wspomaga pisanie apli- kacji, które majš działać w różnych językach. Spróbujmy skompilować program KEYVIEWI ze zdefiniowanym symbolem UMCODE, a następnie uruchomić go w różnych wersjach Windows NT. (W dołšczonym do tej ksišżki CD-ROM-ie wersja unikodowa programu KEYVIEWI znajduje się w katalogu DEBUG). Jeżeli w trakcie kompilacji programu zdefiniowany jest symbol UNICODE, klasa okna "KeyViewl" zostaje zarejestrowana jako RegisterClassW, a nie jako Register- ClassA. Oznacza to, że wszelkie komunikaty, które docierajš do WndProc, i za- wierajš znaki lub łańcuchy znaków, korzystajš z kodów 16-bitowych, a nie z 8- bitowych. W szczególnoœci komunikat WM CHAR będzie posiadał 16-bitowy kod znaku, a nie 8-bitowy. Uruchom wersję unikodowš programu KEYVIEWI pod kontrolš angielskiej wersji Windows NT. Zakładam, że zainstalowałeœ przynajmniej trzy inne układy kla- wiatury, z którymi do tej pory eksperymentowaliœmy - niemiecki, grecki oraz rosyjski. W angielskiej wersji NT z aktywnym angielskim lub niemieckim układem kla- wiatury, unikodowa wersja programu wydaje się działać dokładnie w taki sam sposób, jak wersja skompilowana bez symbolu UMCODE. Będzie ona odbierała takie same kody znaków (wszystkie będš miały wartoœć OxFF lub mniejszš) i bę- dzie wyœwietlała te same poprawne znaki. Dzieje się tak, ponieważ w Windows pierwsze 256 znaków urukodu jest identyczne ze znakami ANSI. Uaktywnij teraz grecki układ klawiatury, a następnie wpisz "abcde". Komunika- ty WM CHAR będš zawierały kody unikodu o wartoœciach 0x03B1, 0x03B2, 0x03C8, 0x03B4 oraz 0x03B5. Zwróć uwagę, że po raz pierwszy pojawiły się kody znaków o wartoœciach większych niż OxFF Odpowiadajš one greckim literom a, á, ţr, 8 i E. Jednakże wszystkie one zostały wyœwietlone w postaci wypełnionych prostokštów! Stało się tak, ponieważ SYSTEM FIXED FONT posiada jedynie 256 znaków. Uaktywnij teraz rosyjski układ klawiatury i ponownie wpisz "abcde". Program KEYVIEWI wyœwietli komunikaty WMţCHAR, którym towarzyszš kody 0x0444, 0x0438, 0x0441, 0x0432 oraz 0x0443. Odpowiadajš one literom cyrylicy cţ, H, c, s oraz y. Jednakże po raz kolejny zamiast znaków wyœwietlone zostały wypełnio- ne prostokšty. Mówišc w skrócie: podczas gdy wersja KEYVIEWI skompilowana bez symbolu UNICODE wyœwietlała niepoprawne znaki, wersja unikodowa tego samego pro- gramu wyœwietla wypełnione prostokšty informujšc w ten sposób, że aktualna czcionka nie zawiera odpowiedniego znaku. Waham się, czy można powiedzieć, że wersja unikodowa KEVIEW1 jest "udoskonaleniem" w porównaniu z wersjš nieunikodowš, ale wyglšda na to, że tak. Ta druga wyœwietla znaki, które nie sš poprawne, pierwsza natomiast - nie. Różnice pomiędzy wersjš zwykłš KEYVIEWI a wersjš Unicode sš dwojakiego rodzaju. Rozdziałó: Kiawiatura 237 Po pierwsze, komunikatowi WM-CHAR towarzyszy 16-bitowy kod znaku, a nie 8-bitowy. Kod 8-bitowy w wersji nieunikodowej mógł mieć różne znaczenia w za- leżnoœci od zainstalowanej klawiatury. Na przykład kod OxEI mógł oznaczać „, jeżeli aktywny był niemiecki układ klawiatury, 8 - w przypadku układu grec- kiego oraz 6 dla układu rosyjskiego. Natomiast w wersji unikodowej 16-bitowy kod znaku jest całkowicie jednoznaczny. Znakowi „ odpowiada kod 0x00E1, zna- kowi a - kod 0x03B1 a znakowi 6 - kod 0x0431. Po drugie, funkcja unikodowaTextOutW wyœwietla znaki na podstawie ich 16- bitowego kodu, kodu 8-bitowego, jak ma to miejsce w wersji nieunikodowej tej funkcji. Ponieważ 16-bitowe kody znaków sš całkowicie jednoznaczne, GDI jest w stanie okreœlić, czy czcionka obowišzujšca w danym kontekœcie urzšdzenia moŸe wyœwietlić żšdany znak. Uruchomienie wersji unikodowej programu KEYVIEWI w amerykańskiej wersji systemu Windows NT jest nieco zwodnicze, ponieważ wydaje się, że GDI wy- œwietla jedynie znaki o kodach od 0x0000 do 0x00FF, pomijajšc pozostałe. Dzieje się tak ze względu na mapowanie jeden do jednego, jakie ma miejsce pomiędzy kodami znaków a 256 znakami czcionki systemowej. Jeżeli jednak zainstalujesz Windows NT w wersji greckiej lub rosyjskiej stwier- dzisz, że tak nie jest. Na przykład po instalacji greckiej wersji systemu zarówno angielski układ klawiatury, jak i układ niemiecki, grecki czy też rosyjski będš generować takie same kody znaków unikodu jak wersja amerykańska. Pomimo to wersja grecka systemu nie będzie wyœwietlać znaków alfabetu niemieckiego i rosyjskiego, ponieważ nie sš one dostępne w zainstalowanej czcionce systemo- wej. Podobnie rosyjska wersja Windows NT nie wyœwietli akcentowanych zna- ków języka niemieckiego oraz liter greckich, ponieważ nie sš one dostępne w ro- syjskiej czcionce systemowej. Najbardziej dramatyczna zmiana zajdzie po uruchomieniu programu KEYVIEW1 w wersji unikodowej pod kontrolš japońskiego Windows NT. Jeżeli posługujšc się IME, wprowadzisz znaki alfabetu japońskiego, zostanš one wyœwietlone po- prawnie. Jedynym problemem będzie formatowanie: ponieważ znaki tego alfa- betu sš często bardzo złożone, będš dwukrotnie szersze niż znaki alfabetu an- gielskiego. TrueType i duże czcionki Czcionki bitmapowe, którymi posługiwaliœmy się (za wyjštkiem czcionek dostar- czanych łšcznie z japońskš wersjš Windows) mogš zawierać do 256 znaków. Można się było tego spodziewać, ponieważ format wykorzystywanego pliku powstał we wczesnej fazie rozwoju Windows, kiedy kody znaków były 8-bito- we. Z tego powodu za każdym razem, gdy posłużysz się identyfikatorem SYS- TEM FONT lub SYSTEM FIXED FONT zawsze znajdzie się jakiœ język, którego znaki nie będš wyœwietlane poprawnie. (Japońska czcionka systemowa różni się od właœnie opisanej, ponieważ jest dwubajtowym zestawem znaków; większoœć znaków jest przechowywana w plikach TrueType Collection z rozszerzeniem nazwy .TCC). 238 Częœć i: Podstawy Czcionki TrueType mogš zawierać więcej niż 256 znaków. Nie wszystkie takie czcionki zawierajš więcej znaków, jednak te, które znajdziesz w Windows 98 lub w Windows NT - tak. Lub inaczej: zawierajš więcej znaków, jeżeli zainstalowa- łeœ obsługę wielu języków. W aplecie Dodaj\Usuń programy w Panelu sterowa- nia otwórz kartę Instalator Windows i upewnij się, że pole wyboru Obsługa wie- lu języków jest zaznaczone. Obsługa ta pozwala na poshzgiwanie się pięcioma zestawami znaków: języków bałtyckich, Europy œrodkowej, cyrylicš, greckim oraz tureckim. Pierwszy z tych zestawów wykorzystywany jest przez wersję estoń- skš, łotewskš oraz litewskš. Zestaw znaków dla Europy œrodkowej - przez wer- sję albańskš, czeskš, chorwackš, węgierskš, polskš, rumuńskš, słowackš oraz sło- weńskš. Cyrylica natomiast jest używana przez wersję bułgarskš, białoruskš, ro- syjskš, serbskš oraz ukraińskš. Czcionki TrueType dostarczane łšcznie z Windows 98 wspierajš wymienione właœnie pięć zestawów znaków oraz zestaw zachodnioeuropejski (ANSI), który wykorzystywany jest domyœlnie przez wszystkie inne języki z wyjštkiem języ- ków Dalekiego Wschodu (chiński, japoński oraz koreański). Czcionki TrueType zawierajšce wiele innych zestawów znaków często okreœlane sšjako "duże czcion- ki" (ang. big fonts). W tym przypadku słowo "duże" odnosi się raczej do bogac- twa znaków, a nie do rozmiaru pliku. Z zalet dużych czcionek możesz skorzystać również w programach nieunikodo- wych. Oznacza to, że możesz się nimi posłużyć, wyœwietlajšc znaki pochodzšce z różnych alfabetów. W tym jednak celu musisz posłużyć się czymœ więcej, niż tylko prostym wywołaniem funkcji GetStockObject. Funkcje CreateFont oraz CreateFontlndirect pozwalajš na stworzenie czcionki lo- gicznej. Działajš one podobnie do funkcji CreatePen oraz CreateBrush, dzięki któ- rym możesz stworzyć odpowiednio pióro lub pędzel. Funkcja CreateFont ma 14 argumentów, dzięki którym możesz dokładnie opisać czcionkę, którš chciałbyœ otrzymać. Z kolei funkcja CreateFontlndirect dysponuje tylko jednym argumen- tem, jest to jednak wskaŸnik do struktury LOGFONT, która zawiera 14 pól odpo- wiadajšcych poszczególnym argumentom CreateFont. Dokładniej tymi funkcjami zajmę się w rozdziale 17. W tej chwili posłużymy się funkcjš CreateFont, korzy- stajšc jednak tylko z kilku jej argumentów. Pozostałe mogš być ustawione na zero. Jeżeli potrzebujesz czcionki o stałej szerokoœci (takiej używaliœmy w programie KEYVIEWI), jako trzynasty argument funkcji należy podać FIXED PITCH. Jeże- li potrzebujesz czcionki zawierajšcej inny niż domyœlny zestaw znaków, jako dziewišty argument podaj identyfikator zestawu znaków (ang. character set ID). Może to być jedna z wartoœci zdefiniowanych w WINGDI.H. Do każdej z nich dodałem komentarz pozwalajšcy na okreœlenie strony kodowej zwišzanej z da- nym zestawem: ţţdefine ANSI_CHARSET 0 // 1252 Latin 1 (ANSI) iţdefine DEFAULT_CHARSET 1 define SYMBOL_CHARSET 2 iţdefine MAC_CHARSET 77 define SHIFTJIS_CHARSET 128 // 932 (DBCS, japoński) iţdefine HANGEUL_CHARSET 129 // 932 (DBCS, koreański) 4ţdefine HANGUL_CHARSET 129 // " " ţţdefine JOHAB CHARSET 130 // 1361 (DBCS, koreański) Rozdziałó: Klawiatura 239 lldefine G82312_CHARSET 134 // 936 (DBCS, uproszczony chiński) lldefine CHINESEBIG5_CHARSET 136 // 950 (DBCS, tradycyjny chiński) lldefine GREEK_CHARSET 161 // 1253 grecki lldefine TURKISH_CHARSET 162 // 1254 Latin 5 (turecki) lldefine VIETNAMESE_CHARSET 163 // 1258 wietnamski Ipdefine HEBREW_CHARSET 177 // 1255 hebrajski lldefine ARABIC_CHARSET 178 // 1256 arabski lldefine BALTIC_CHARSET 186 // 1257 baltycki 4ldefine RUSSIAN_CHARSET 204 // 1251 cyrylica (słowiańska) 4ldefine THAI_CHARSET 222 // 874 tajski lldefine EASTEUROPE_CHARSET 238 // 1250 Latin 2 (Europa centralna) lldefine OEM CHARSET 255 // zależne od kra,iu Dlaczego w systemie Windows istniejš dwie różne liczby - identyfikator zesta- wu znaków oraz identyfikator strony kodowej (ang. code page ID) - na okreœlenie tego samego zestawu znaków? Jest to jedno z tych denerwujšcych dziwactw Windows. ţZwróć uwagę, że identyfikator zestawu znaków może być przecho- wany w jednym bajcie -jest to rozmiar pola zestawu znaków w strukturze LOG- FONT. (Dawno, dawno temu, w czasach Windows 1.0, dostępnej pamięci było tak mało, że należało się liczyć z każdym bajtem). Zwróć również uwagę, że w wielu krajach używa się wielu różnych stron kodowych MS-DOS, ale tylko jeden identyfikator zestawu znaków - OEM CHARSET - jest wykorzystywany do okre- œlenia zestawu znaków MS-DOS. Z pewnoœciš również zwrócisz uwagę na to, że wartoœci zestawu znaków zga- dzajš się z wartoœciami wyœwietlanymi w kolumnie "CharSet" w oknie progra- mu STOKFONT. W amerykańskiej wersji Windows widzieliœmy, że czcionki miały identyfikatory 0 (ANSI CHARSET) oraz 255 (OEM CHARSET). Identyfikator 161 (GREEK CHARSET) pojawił się w greckiej wersji systemu, 205 (RUSSIAN CHAR- SET) - w wersji rosyjskiej, a 128 (SHIFTJIS CHARSET) - w wersji japońskiej. W przedstawionym powyżej fragmencie pliku WINGDI.H skrót DBCS oznacza dwubajtowy zestaw znaków (ang. double-byte character set). Wykorzystywany jest on w dalekowschodnich wersjach Windows. Pozostałe wersje tego systemu nie majš czcionek DBCS, dlatego też nie możesz korzystać z tych identyfikatorów. Funkcja CreateFont zwraca wartoœć typu HFONT - uchwyt czcionki logicznej. Możesz przypisać go aktualnemu kontekstowi urzšdzenia, posługujšc się funk- cjš SelectObject. Konieczne może się również okazać usunięcie czcionki. W tym celu należy wywołać funkcję DeleteObject. Kolejnš rzeczš zwišzanš z dużymi czcionkami jest komunikat WM-INPUTLANG- CHANGE. Za każdym razem, kiedy zmienisz układ klawiatury, posługujšc się menu wyœwietlanym po kliknięciu ikony znajdujšcej się w pasku zadań, system Windows wysyła ten komunikat do procedury okna. Zwišzany z nim parametr wParam przechowuje identyfikator zestawu znaków nowego układu klawiatury. Przedstawiony na rysunku 6-12 program KEYVIEW2 pozwala na zmianę wyko- rzystywanej czcionki za każdym razem, gdy zmieni się układ klawiatury. 240 Częœć I: Podstawy KEYVIEW2.C /* KEYVIEW2.C - Wyœwietla komunikaty klawiaturowe i znakowe (c) Charles Petzold, 1998 */ ţţinclude LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) i static TCHAR szAppName[] = TEXT ("KeyView2") : HWND hwnd ; MSG msg ; WNDCLASS wndclass : wndclass.style = CS_HREDRAW ţ CS VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance : wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC ŽRROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) ( MessageBox (NULL, TEXT ("This program requires Windows NT!"). szAppName, MBţICONERROR) ; return 0 ; hwnd = CreateWindow (szAppName, TEXT ("Keyboard Message Viewer ţţ2"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW USEDEFAULT, CW_USEDEFAULT, CW USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd. iCmdShow) : UpdateWindow (hwnd) : while (GetMessage (&msg, NULL, 0, 0)) ( TranslateMessage (&msg) ; DispatchMessage (&ms9) ; i return msg.wParam ; LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ( static DWORD dwCharSet = DEFAULT CHARSET ; static int cxClientMax, cyClientMax, cxClient, cyClient, cxChar, cyChar ; Rozcţiat 6: Klawiatura 241 static int cLinesMax, cLines ; static PMSG pmsg ; static RECT rectScroll ; static TCHAR szTop[] = TEXT ("Message Key Char ") TEXT ("Repeat Scan Ext ALT Prev Tran") ; static TCHAR szUnd[] = TEXT (" ţ ") TEXT (" ţ ţ ţ ţ ") ; static TCHAR * szFormat[2] = ( TEXT ("%-13s %3d %-15s%c%6u %4d %3s %3s %4s %4s"), TEXT ("%-13s Ox%04X%ls%c %6u %4d %3s %3s %4s %4s") } ; static TCHAR * szYes = TEXT ("Yes") ; static TCHAR * szNo = TEXT ("No"J ; static TCHAR * szDown = TEXT ("Down") ; static TCHAR * szUp = TEXT ("Up") ; static TCHAR * szMessage [] = t TEXT ("WM_KEYDOWN"), TEXT ("WM_KEYUP"), TEXT ("WM_CHAR"), TEXT ("WM_DEADCHAR"), TEXT ("WM_SYSKEYDOWN"), TEXT ("WM_SYSKEYUP"), TEXT ("WMţSYSCHAR"), TEXT ("WM SYSDEADCHAR") } ; HDC hdc ; int i, iType ; PAINTSTRUCT ps ; TCHAR szBuffer[128], szKeyName [32] ; TEXTMETRIC tm ; switch (message) ( case WM_INPUTLANGCHANGE: dwCharSet = wParam ; case WM_CREATE: case WMţDISPLAYCHANGE: // kontynuuj // Wyznacz maksymalny obszar roboczy cxClientMax = GetSystemMetrics (SM CXMAXIMIZED) ; cyClientMax = GetSystemMetrics (SM CYMAXIMIZED) ; // Pobierz wielkoœć znaku dla czcionki o stalej szerokoœci hdc = GetDC (hwnd) ; SelectObject (hdc, CreateFont (0, 0, 0, 0, 0, 0, 0, 0, dwCharSet, 0, 0, 0, FIXEDţPITCH, NULL)) ; GetTextMetrics (hdc, &tm) ; cxChar = tm.tmAveCharWidth ; cyChar = tm.tmHeight ; Delete0bject (SelectObject (hdc, GetStockObject (SYSTEM FONT))) ; ReleaseDC (hwnd, hdc) ; // Przydziel pamięć na wyœwietlane linie if (pmsg) free (pms9) ; 242 Częœć I: Podstawy (cišg dalszy ze strony 241) cLinesMax = cyClientMax / cyChar ; pmsg = malloc (cLinesMax * sizeof (MSG)) ; cLines = 0 ; // kontynuuj case WM_SIZE: if (message = WMţSIZE) ( cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; ) // Wyznacz przesuwany prostokšt rectScroll.left = 0 ; rectScroll.right = cxClient ; rectScroll.top = cyChar ; rectScroll.bottom = cyChar * (cyClient / cyChar) ; InvalidateRect (hwnd, NULL, TRUE) ; if (message = WM INPUTLANGCHANGE) return TRUE ; return 0 ; case WM_KEYDOWN: case WM_KEYUP: case WM_CHAR: case WM_DEADCHAR: case WM_SYSKEYDOWN: case WM_SYSKEYUP: case WM SYSCHAR: case WM SYSDEADCHAR: // Przebuduj tablicę przechowujšcš dane for (i = cLinesMax - 1 ; i > 0 ; i-) ( pmsg[i7 = pmsgCi - 17 ; ) // Przechowaj nowy komunikat pmsgC07.hwnd = hwnd ; pmsgC07.message = message ; pmsgCO].wParam = wParam ; pmsgC07.lParam = lParam ; cLines = min (cLines + 1, cLinesMax) ; // Przewijaj w górę ScrollWindow (hwnd, 0, -cyChar, &rectScroll, &rectScroll) ; break ; l/ tzn. wywołaj DefWindowProc, aby // przetworzyć pozostale komunikaty Rozdział 6: Klawiatura 243 1 case WM_PAINT: ( hdc = BeginPaint (hwnd, &ps) ; SelectObject (hdc, CreateFont (0, 0, 0, 0, 0, 0, 0, 0, dwCharSet, 0, 0, 0, FIXEDţPITCH, NULL)) ; SetBkMode (hdc, TRANSPARENT) ; ! TextOut (hdc, 0, 0, szTop, lstrlen (szTop)) ; TextOut (hdc, 0, 0, szUnd, lstrlen (szUnd)) ; for (i = 0 ; i < min (cLines, cyClient / cyChar - 1) ; i++) ( iType = pmsg[i].message == WM_CHAR ţţ pmsg[i].message == WM_SYSCHAR ţ pmsg[i].message == WM_DEADCHAR pmsg[i].message = WMţSYSDEADCHAR ; GetKeyNameText (pmsg[i].lParam. szKeyName, sizeof (szKeyName) / sizeof (TCHAR)) ; TextOut (hdc, 0, (cyClient / cyChar - 1 - i) * cyChar, szBuffer, wsprintf (szBuffer, szFormat [iType], szMessage [pmsg[i].message - WM KEYFIRST], pmsgCi].wParam, (PTSTR) (iType ? TEXT (" ") : szKeyName), (TCHAR) (iType ? pmsg[i].wParam : ' '), LOWORD (pmsg[i].lParam), HIWORD (pmsg[i].lParam) & OxFF, 0x01000000 & pmsg[i].lParam ? szYes . szNo, 0x20000000 & pmsgCi].lParam ? szYes . szNo, 0x40000000 & pmsgCi].lParam ? szDown : szUp, 0x80000000 & pmsgCi].lParam ? szUp . szDown)) ; ) Delete0bject (SelectObject (hdc, GetStockObject (SYSTEM FONT))> ; EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: PostOuitMessage (0) ; return 0 ; l return DefWindowProc (hwnd, message, wParam, lParam) ; Rysunek 6-12. Program KEYVIEW2 Zwróć uwagę, że program KEYVIEW2 czyœci ekran i ponownie alokuje pamięć dla danych za każdym razem, gdy zmieni się wykorzystywany układ klawiatu- ry. Istniejš dwa powody takiego zachowania: po pierwsze, zmiana języka kla- wiatury może spowodować zmianę wielkoœci znaków czcionki. Dlatego też na- leży ponownie wyznaczyć przestrzeń zajmowanš przez pewne zmienne. Po dru- gie, program KEYVIEW2 nie bierze pod uwagę identyfikatora zestawu znaków, gdy odbiera kornunikat znakowy. Dlatego też po zmianie języka klawiatury pro- gram KEYVIEW2 odtwarza zawartoœć obszaru roboczego, wszystkie znaki zo- stanš wyœwietlone nowš czcionkš. 244 Częœć I: Podstawy Więcej informacji na temat czcionek i zestawu znaków umieœciłem w rozdziale 17. Jeżeli chcesz dowiedzieć się czegoœ więcej na temat zagadnień zwišzanych z aplikacjami wielojęzykowymi, możesz przeczytać dokument /Platform SDK/Win- dows Base Services/Internatioonal Features, jednak znacznie więcej szczegółów znaj- dziesz w /Platform SDK/4Vindows Base Serviees/General Library/String Manipulation. Karetka (a nie kursor) Gdy wprowadzasz do programu tekst, to w miejscu, gdzie ma się pojawić na- stępny znak, system umieszcza niewielki znak podkreœlenia, pionowš linię lub prostokšt. Prawdopodobnie przyzwyczaiłeœ się już, że jest to "kursor". Jednak piszšc programy przeznaczone do pracy w Windows, będziesz musiał pozbyć się tego przyzwyczajenia. Znak ten w Windows nosi bowiem nazwę karetki. Sło- wo "kursor" zostało zarezerwowane dla niewielkiego obrazka, który oznacza po- łożenie myszy. Funkcje karetki Istnieje pięć funkcji œciœle zwišzanych z karetkš: ů CreateCaret Tworzy karetkę zwišzanš z danym oknem. ů SetCaretPos Ustawia pozycję karetki w oknie. ů ShowCaret Wyœwietla karetkę. ů HideCaret Ukrywa karetkę. ů DestroyCaret Niszczy karetkę. Istniejš również funkcje pozwalajšce na odczytanie aktualnej pozycji karetki (Get- CaretPos) oraz ustawienie częstotliwoœci jej migotania (GetCaretBlinkTime oraz Set- CaretBlinkTime). W systemie Windows karetka jest zwykle poziomš kreseczkš lub prostokštem o wielkoœci znaku albo pionowš kreseczkš o wysokoœci znaku. Wykorzystanie po- ziomej linii jako karetki jest zalecane, gdy posługujesz się czcionkš proporcjonal- nš takš, jak na przykład domyœlna czcionka systemowa Windows. Ponieważ po- szczególne znaki czcionki proporcjonalnej nie majš takiej samej szerokoœci, po- zioma linia lub prostokšt nie mogš mieć rozmiaru jednego znaku. jeżeli w swoim programie zamierzasz poshxżyć się karetkš, nie powinieneœ po prostu jej stworzyć w ramach obshxgi komunikatu YVMţCREATE, a usunšć w ra- mach obsługi WMţDESTROY, ponieważ kolejka jest w stanie wspierać tylko jed- nš karetkę. Dlatego też, jeżeli w swoim programie masz kilka okien, muszš one dzielić jednš karetkę. Wbrew pozorom, nie jest to aż tak duże ograniczenie, jakby mogło się na pierw- szy rzut oka wydawać. Jeżeli się nad tym zastanowisz, okaże się, że wyœwietla- nie karetki ma sens tylko wtedy, kiedy okno ma fokus. Rzeczywiœcie, obecnoœć karetki jest jednym z tych efektów wizualnych, które pozwalajš użytkownikowi zorientować się, gdzie może on wprowadzać tekst do programu. Ponieważ w danej chwili tylko jedno okno może otrzymać fokus, nie ma powodu, aby różne okna wyœwietlały w tej samej chwili swoje migajšce karetki. Rozdział s: Klawiaiura 245 Program może okreœlić, czy ma fokus, przechwytujšc Kornunikaty WM-SETFO- CUS oraz WM-KILLFOCUS. Zgodnie z tym, co sugerujš ich nazwy, procedura okna otrzymuje komunikat WM-SETFOCUS, gdy jej okno otrzymuje fokus, a WM-KILLFOCUS - gdy je traci. Komunikaty te zawsze występujš parami: pro- cedura okna zawsze najpierw otrzyma WM-SETFOCUS, a następnie WM-KILL- FOCUS, przy czym w trakcie "życia" okna, liczba tych pierwszych komunika- tów jest zawsze taka sama, jak liczba drugich. Podstawowa zasada korzystania z karetki jest bardzo prosta: w trakcie obsługi komunikatu WM-SETFOCUS procedura okna tworzy karetkę, posługujšc się w tym celu funkcjš CreateCaret, natomiast gdy otrzyma komunikat tNM-KILL- FOCUS, kasuje jš za pomocš DestroyCaret. Istnieje również kilka innych reguł: utworzona karetka nie jest widoczna. Dlate- go też po wywołaniu CreateCaret program musi wywołać ShowCaret. Dopiero wtedy karetka zostanie wyœwietlona. Ponadto, zanim coœ narysujesz w oknie ! w ramach obsługi innego komunikatu niż WM-PAINT, musisz uicryć karetkę za pomocš funkcji HideCaret. Po zakończeniu rysowania, program powinien ponow- nie wywołać funkcję ShowCaret. Skutki działania funkcji HideCaret sumujš się. Oznacza to, że po kilku wywołaniach tej funkcji musisz dokładnie tyle samo razy wywołać ShowCaret. Dopiero wtedy karetka ponownie pojawi się w oknie. Program TYPER Przedstawiony na rysunku 6-13 program TYPER zawiera niemal wszystko, cze- go nauczyliœmy się w tym rozdziale. Możesz traktować go jako niesłychanie pry- mitywny edytor tekstu. W oknie programu możesz wpisywać tekst, przesuwać kursor (to znaczy karetkę), korzystajšc z klawiszy kursora (a może z "klawiszy karetki"?), jak również kasować zawartoœć całego okna, naciskajšc klawisz [Esc]. Zawartoœć okna kasowana jest również wtedy, gdy zmienisz jego wielkoœć lub uaktywnisz inny układ klawiatury. Nie ma jakiegokolwiek przewijania tekstu , wyszukiwania i zamiany, w żaden sposób nie możesz zapisać wprowadzonego tekstu do pliku, sprawdzić poprawnoœci ortograficznej. Mimo tych kilku "drob- nych niedocišgnięć" program funkcjonuje poprawnie. TYPER.C /* TYPER.C - Program do pisania tekstu (c) Charles Petzold, 1998 */ include define BUFFER(x,y) *(pBuffer + y * cxBuffer + x) LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) static TCHAR szAppName[] = TEXT (".Typer") ; HWND hwnd : 246 Częœć I: Podstawy I (cišg dalszy ze strony 245) i MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CS VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) ( MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MB ICONERROR) ; return 0 ; } hwnd = CreateWindow (szAppName, TEXT ("Typing Program"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CWţUSEDEFAULT, CW USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) ( TranslateMessage (&msg) ; DispatchMessage (&msg) ; ) return msg.wParam ; ) LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ( static DWORD dwCharSet = DEFAULT_CHARSET ; static int cxChar, cyChar, cxClient, cyClient, cxBuffer, cyBuffer, xCaret, yCaret ; static TCHAR * pBuffer = NULL ; HDC hdc ; int x, y, i ; PAINTSTRUCT ps ; TEXTMETRIC tm ; switch (message) ( case WM_INPUTLANGCHANGE: dwCharSet = wParam ; // kontynuuj case WM_CREATE: hdc = GetDC (hwnd) ; Rozdział 6: Klawiatura 247 SelectObject (hdc, CreateFont (0, 0, 0, 0, 0, 0, 0, 0, dwCharSet, 0, 0, 0, FIXED PITCH, NULL)) ; GetTextMetrics (hdc, &tm) ; cxChar = tm.tmAveCharWidth ; cyChar = tm.tmHeight ; Delete0bject (SelectObject (hdc, GetStockObject (SYSTEMţFONT))) ; ReleaseDC (hwnd, hdc) ; // kontynuuj case WM_SIZE: // pobierz rozmiar okna w pikselach if (message == WM SIZE) cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; I:: 1 // wyznacz rozmiar okna w znakach ; f. cxBuffer = max (l, cxClient / cxChar) ; cyBuffer = max (i, cyClient / cyChar) ; // przydziel pamięć na bufor i wykasuj jš if (pBuffer != NULL) free (pBuffer) ; pBuffer = (TCHAR *) malloc (cxBuffer * cyBuffer * sizeof (TCHAR)) ; for (y = 0 ; y < cyBuffer ; y++) for (x = 0 ; x < cxBuffer ; x++) I BUFFER(x,y) = ' ' , i:' // ustaw karetkę w lewym górnym narożniku 1 xCaret = 0 ; yCaret = 0 ; if (hwnd == GetFocus ()) ( SetCaretPos (xCaret * cxChar, yCaret * cyChar) ; InvalidateRect (hwnd, NULL, TRUE) ; return 0 ; case WM SETFOCUS: // stwórz i wyœwietl karetkę CreateCaret (hwnd, NULL, cxChar, cyChar) ; SetCaretPos (xCaret * cxChar, yCaret * cyChar) ; ShowCaret (hwnd) ; return 0 ; i case WMţKILLFOCUS: // ukryj i usuń karetkę HideCaret (hwnd) ; ( DestroyCaret () ; return 0 ; 248 Częœć I: Podstawy (cišg dalszy ze strony 247) case WM KEYDOWN: switch (wParam) t case VK_HOME: xCaret = 0 : break ; case UK_END: xCaret = cxBuffer - 1 ; break ; case UK_PRIOR: yCaret = 0 ; break ; case VK_NEXT: yCaret = cyBuffer - 1 ; break ; case UK_LEFT: xCaret = max (xCaret - l, 0) break ; I case UKţRIGHT: xCaret = min (xCaret + 1, cxBuffer - 1) ; break : i case VK_UP: ! yCaret = max (yCaret - i, 0) : break ; i case VK_DOWN: Caret = min (yCaret + 1, cyBuffer - 1) ; y break ; ! case VK_DELETE: for (x = xCaret ; x < cxBuffer - 1 ; x++) I BUFFER (x, yCaret) = BUFFER (x + l, yCaret) ; I BUFFER (cxBuffer - 1, yCaret) = ' ' . HideCaret (hwnd) ; hdc = GetDC (hwnd) ; SelectObject (hdc, CreateFont (0, 0, 0, 0, 0, 0, 0, 0, dwCharSet, 0, 0, 0, FIXEDţPITCH, NULL)) ; TextOut (hdc, xCaret * cxChar, yCaret * cyChar. & BUFFER (xCaret, yCaret). cxBuffer - xCaret) ; Delete0bject (SelectObject (hdc, GetStockObject (SYSTEMţFONT))) ; ReleaseDC (hwnd, hdc) ; ShowCaret (hwnd) ; break ; ! Rozdział 6: Ktawiatura 249 ł 1 SetCaretPos (xCaret * cxChar, yCaret * cyChar) ; return 0 ; ,. case WM CHAR: for (i = 0 ; i < (int) LOWORD (lParam) ; i++) switch (wParam) ! case '\b': // backspace if (xCaret > 0) xCaret- , SendMessage (hwnd, WMţKEYDOWN, VK DELETE, 1) ; I break ; case '\t': // tab do ( SendMessage (hwnd, WM CHAR, ' , 1) ; while (xCaret % 8 != 0) : break ; j case '\n': // przejœcie do nowej linii if (++yCaret = cyBuffer) yCaret = 0 : break ; case '\r': // powrót karetki xCaret = 0 : if (++yCaret = cyBuffer) yCaret = 0 : break ; I case '\x1B': /! escape for (y = 0 ; y < cyBuffer ; y++) for (x = 0 : x < cxBuffer ; x++) i BUFFER (x, y) = ' ' , xCaret = 0 ; yCaret = 0 ; InvalidateRect (hwnd, NULL, FALSE) ; break : default: // kody znaków BUFFER (xCaret, yCaret) = (TCHAR) wParam ; . HideCaret (hwnd) ; hdc = GetDC (hwnd) ; i SelectObject (hdc, CreateFont (0, 0, 0, 0, 0, 0, 0, 0, dwCharSet, 0, 0, 0, FIXED PITCH, NULL)) ; TextOut (hdc, xCaret * cxChar, yCaret * cyChar, & BUFFER (xCaret, yCaret), 1) ; t 250 Częœć I: Podstawy , (cišg dalszy ze strony 249) Delete0bject ( SelectObject (hdc, GetStockObject (SYSTEM FONT))) ; ReleaseDC (hwnd, hdc) ; ShowCaret (hwnd) ; ! i if (++xCaret = cxBuffer) I xCaret = 0 ; if (++yCaret == cyBuffer) yCaret = 0 ; ) break ; ) SetCaretPos (xCaret * cxChar, yCaret * cyChar) ; return 0 ; case b!M_PAINT: hdc = BeginPaint (hwnd, &ps) ; r SelectObject (hdc, CreateFont (0, 0, 0, 0, 0, 0, 0, 0, dwCharSet, 0, 0, 0, FIXED PITCN, NULL)) ; for (y = 0 ; y < cyBuffer ; y++) TextOut (hdc, 0, y * cyChar, & BUFFER(O,y), cxBuffer) ; Delete0bject (SelectObject (hdc, GetStockObject (SYSTEh1 FONT))) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: j PostOuitMessage (0) ; return 0 ; ) I return DefWindowProc (hwnd, message, wParam, lParam) ; ; ) Rysunek 6-13. Program TYPER W celu zapewnienia maksymalnej prostoty, program TYPER posługuje się czcionkš o stałej szerokoœci. Napisanie edytora posługujšcego się czcionkš o zmiennej sze- rokoœci, jak możesz się spodziewać, jest zadaniem znacznie bardziej skompliko- wanym. Program pobiera kontekst urzšdzenia w kilku miejscach: w trakcie ob- sługi komunikatu WMţCREATE, WMţKEYDOWN, WMţCHAR oraz WM PA- INT. Za każdym razem wywołuje funkcje GetStockObject oraz SelectObject, wy- I bierajšc czcionkę o stałej szerokoœci z aktualnym zestawem znaków. i I W trakcie obsługi komunikatu YVMţSIZE, TYPER wyznacza wysokoœć oraz sze- j rokoœć okna wyrażonš w znakach, a następnie zapamiętuje je w zmiennych cxBuffer oraz cyBuffer. Z kolei posługujšc się funkcjš malloc, alokuje bufor o takiej wielko- œci, aby mógł pomieœcić wszystkie znaki, które użytkownik wpisze w oknie. Zwróć Rozdział 6: Klawiatura uwagę, że rozmiar bufora jest iloczynem cxBuffer, cyBuffer oraz sizeof(TCHAR). Ten ostatru człon może przyjmować wartoœć 1 lub 2, w zależnoœci od tego, czy pro- gram skompilowany został do posługiwania się 8-bitowymi kodami znaków, czy też kodami tzrtikodu. Zmienne xCaret oraz yCaret przechowujš wyrażonš w znakach pozycję karetki. W trakcie obsługi komtmikatu WM-SETFOCUS, program TYPER wywołuje funk- cję CreateCaret w celu stworzenia karetki o wielkoœci jednego znaku. Następnie wywołuje SetCaretPos, co powoduje ustawienie pozycji karetki, oraz ShowCaret dzięki czemu zostaje ona wyœwietlona. Z kolei, w trakcie obshxgi WM KILLFO- CUS, TYPER wywołuje HideCaret, a następnie DestroyCaret. W trakcie przetwarzania komunikatu WM-KEYDOWN brane sš pod uwagę głów- nie klawisze kursora. Naciœnięcie [Home] lub [End] spowoduje umieszczenie karetki na poczštku lub na końcu linii, [Page Up] oraz [Page Down] przenoszš karetkę do górnej lub dolnej częœci okna. Klawisze strzałek działajš dokładnie w taki sposób, jak mógłbyœ tego oczekiwać. Jeżeli naciœnięty zostanie klawisz [De- lete], TYPER musi przenieœć wszystkie znaki znajdujšce się za następnš pozycjš karetki aż do końca linii, a następnie za ostatnim znakiem wyœwietlić spację. W ramach przetwarzania komunikatu WMţCHAR uwzględniane sš również kla- wisze [Backspace], [Tab], przejœcie do nowej linii ([Ctrl+Enter]), [Enter], [Esc] oraz klawisze poszczególnych znaków. Zwróć uwagę, że w ramach obsługi WM CHAR uwzględniam zawartoœć licznika powtórzeń przekazywanego za pomocš frag- mentu parametru lParam (założyłem oczywiœcie, że każdy wpisany przez użyt- kownika znak jest ważny). Nie robię tego jednak w trakcie obsługi WM KEY DOWN (uniemożliwiatn w ten sposób nieumyœlne przewijanie zawartoœci okna). Przetwarzanie klawiszy [Backspace] oraz [Tab] zostało uproszczone dzięki wy- korzystaniu funkcji SendMessage. Pierwszy z tych klawiszy emulowany jest za pomocš [Delete], drugi natomiast - serii spacji. Jak wspomniałem wczeœniej, program powinien ukryć karetkę, gdy w oknie od- bywa się rysowanie w ramach obsługi innego komunikatu niż WM PAINT. Pro- gram T1'PER realizuje to w trakcie przetwarzania WM KEYDOWN wygenero- wanego naciœnięciem klawisza [Delete] oraz w trakcie przetwarzania WM CHAR odpowiadajšcego wprowadzanym przez użytkownika znakom. W obu przypad- kach modyfikowana jest zawartoœć bufora, a następnie wyœwietlany jest nowy znak lub znaki. Mimo że TYPER w trakcie przełšczania pomiędzy zestawami znaków posługuje się tš samš logikš, co KEYVIEW2, nie będzie on działał do końca poprawnie dla języków dalekowschodruch. Dzieje się tak, ponieważ nie jest on w żaden sposób przygotowany do obsługi znaków o podwójnej szerokoœci. Zwišzanym z tym problemom poœwięcony został rozdział 17, w którym przedstawiłem więcej szcze- gółów na temat czcionek oraz wyjœcia tekstowego. Rozdział 7 Mysz jest urzšdzeniem wskazujšcym. Ma co najmniej jeden przycisk (zazwyczaj dwa lub trzy). Wyszła zwycięsko z konfrontacji z innymi wejœciowymi urzšdze- niami wskazujšcymi, takimi jak ekrany dotykowe czy pióra œwietlne. Mysz wraz ze swoimi odmianami, takimi jak trackball zwykle stosowany w komputerach przenoœnych, stała się jedynym urzšdzeniem wejœciowym, któremu udało się na dobre zadomowić na rynku komputerów PC. Jednak na samym poczštku perspektywy myszy wcale nie wyglšdały zbyt dobrze. Twórcy wczesnych wersji systemu Windows czuli, że nie mogš wymagać od użyt- kowników kupienia nowego urzšdzenia po to tylko, aby mogli oni korzystać z ich produktu. Dlatego też uznali mysz za urzšdzenie dodatkowe i umożliwili wyko- nywanie wszelkich operacji za pomocš klawiatury. (Jeżeli na przykład przeczytasz teksty podpowiedzi dotyczšcych Kalkulatora, zorientujesz się, w jaki sposób po- szczególnym jego klawiszom i funkcjom przypisane zostały odpowiedniki z kla- wiatury). Niezależru dostawcy oprogramowania również zachowali się w ten sam sposób: każda z funkcji ich pro,gramów mogła zostać wykonana z klawiatury. Wcze- œniejsze wydania tej ksišżki również propagowały podobne podejœcie. Teoretycznie, najnowsze wersje systemu Windows wymagajš obecnoœci myszy. A przynajmniej taka informacja widnieje na opakowaniu. Możesz jednak odłš- czyć mysz od komputera, a system i tak wystartuje poprawnie (pojawi się jedy- nie komunikat, że mysz nie została podłšczona). Jednak korzystanie z systemu bez pomocy myszy przypomina granie na pianinie palcami nóg (przynajmniej na poczštku), nic nie stoi jednak na przeszkodzie, abyœ w ten sposób pracował. Dlatego też cišgle jestem zwolennikiem umieszczania w programach odpowied- ników klawiaturowych wszystkich funkcji dostępnych za poœrednictwem myszy. UŸytkownicy, którzy szczególnie sprawnie poshxgujš się klawiaturš, zwykle nie lubiš zbyt często odrywać od niej ršk. Przypuszczam również, że każdemu z nas wielokrotnie zdarzyło się już "zgubić" kursor myszy na zagraconym biurku lub mieć zdecydowanie zbyt mało miejsca na podkładce, aby sprawnie operować myszš. Dodanie do programu możliwoœci uruchamiania funkcji programu za pomocš klawiatury nie wymaga zbyt dużo wysiłku, a może znacznie ułatwić życie tym użytkownikom, którzy wolš ten sposób komunikacji z komputerem. Podczas gdy klawiatura wykorzystywana jest zwykle do wprowadzania i maru- pulowania danymi, mysz pozwala na łatwe rysowanie oraz przetwarzanie obiek- tów graficznych. Dlatego też większoœć programów przykładowych, które znaj- dziesz w tym rozdziale, ma coœ wspólnego z grafikš. Będziesz przy tym korzy- stał w nich z tego, czego nauczyłeœ się w rozdziale 5. 254 Częœć Iţ Podstawy Podstawy W Windows 98 możesz posługiwać się myszš z jednym, dwoma lub trzema przy- ciskami. Zamiast myszš możesz również poshxgiwać się joystickiem lub piórem œwietlnym. We wczeœniejszych wersjach Windows, aby nie dyskryminować tych, którzy mieli mysz z jednym przyciskiem, unikano posługiwania się drugim i trze- cim. Jednakże wraz z upływem czasu obecnoœć drugiego przycisku stała się stan- dardem, nie ma więc już żadnej podstawy do jego unikania. Obecnie standardo- wo umożliwia on wyœwietlenie na ekranie "menu kontekstowego". Pojawia się ono w oknie poza standardowym paskiem menu lub w zwišzku z przecišganiem (którym zajmiemy się wkrótce). Jednakże programy nie powinny zakładać, że drugi przycisk myszy będzie istniał w każdej sytuacji. Teoretycznie możesz sprawdzić, czy mysz jest obecna, posługujšc się znajomš już funkcjš GetSystemMetrics: fMouse = GetSystemMetrics (SM MOUSEPRESENT) ; Zmienna ţlVlouse przyjmie wartoœć TRUE (niezerowš), jeżeli mysz została zain- stalowana, natomiast 0 - w przeciwnym wypadku. Jednak w Windows 98 funk- cja ta zawsze zwraca wartoœć TRUE, niezależnie od tego, czy mysz jest podłšczo- na, czy nie. W Windows NT funkcja działa poprawnie. Jeżeli chcesz się dowiedzieć, ile przycisków ma mysz, wykonaj: cButtons = GetSystemMetrics (SM CMOUSEBUTTONS) ; Również ta funkcja powinna zwrócić wartoœć 0, jeżeli mysz nie została zainstalo- wana. Jednak w Windows 98 zostanie w tej sytuacji zwrócona wartoœć 2. Ci z użytkowników, którzy posługujš się lewš rękš, mogš zmienić funkcje przy- pisane poszczególnym przyciskom myszy, korzystajšc z możliwoœci apletu Mysz znajdujšcego się w Panelu sterowania. Mimo że aplikacja może sprawdzić, czy przyciski zostały przełšczone, wywołujšc funkcję GetSystemMetrics z parametrem SM SWAPBUTTON, tak naprawdę nie jest to konieczne. Przycisk, który użytkow- nik naciska palcem wskazujšcym, zawsze traktowany jest jako lewy nawet, jeżeli fizycznie znajduje się po prawej stronie myszy. Jeżeli jednak piszesz program œle- dzšcy wykorzystywanie myszy, konieczne może okazać się narysowanie na ekra- nie jej obrazu i w tej sytuacji informacja o przełšczeniu przycisków może okazać się potrzebna. Użytkownik może również zmieniać parametry pracy myszy posługujšc się aple- tem Mysz znajdujšcym się w Panelu sterowariia. Z poziomu aplikacji ten sam efekt możesz osišgnšć, wywołujšc funkcję SystemParameterslnfo. Kilka szybkich definicji Gdy użytkownik w Windows poruszy myszš, system przemieœci wyœwietlanš na ekranie niewielkš bitmapę. Nosi ona nazwę kursora myszy. W jego obrębie znaj- duje się ostrze (ang. hot spot). Od tej pory, zawsze kiedy będę mówił o położeniu kursora myszy na ekranie, będę miał na myœli położenie tego punktu. Windows pozwala programom posługiwać się kilkoma predefiniowanymi kur- sorami myszy. Najbardziej typowym jest pochylona strzałka o nazwie IDC AR- Rozdział 7: Mysz 255 ROW (identyfikator ten, podobnie jak i pozostałe, zdefiniowany został w pliku WINLTSER.H). Punkt aktywny umieszczony został na końcu jej grota. Kursor IDC CROSS (posłużymy się nim w zaprezentowanym dalej programie BLOK- OUT) ma punkt aktywny w miejscu przecięcia prostopadłych linii. Z kolei kur- sor IDC WAIT ma kształt klepsydry; jest zwykle wykorzystywany przez program, gdy chce on poinformować użytkownika, że jest czymœ zajęty. Programiœci mogš również definiować swoje własne kursory. Jak to zrobić, dowiesz się w rozdziale 10. Domyœlny kursor okna deklarowany jest w strukturze klasy okna: wndclass.hCwrsor = LoadCursor (NULL, IDC ARROW) : A oto terminy okreœlajšce czynnoœci, które możesz wykonać za pomocš myszy: ů Kliknięcie - naciskanie i zwalnianie przycisku myszy. ů Dwukrotne kliknięcie - dwukrotne naciœnięcie i zwolnienie przycisku myszy w krótkim odstępie czasu. ů Cišgnięcie - poruszanie myszš z wciœniętym jednym z przycisków. W przypadku myszy z trzema przyciskami, przyciski noszš nazwę lewy, œrodko- wy oraz prawy. Zwišzane z nimi identyfikatory, zdefiniowane w pliku nagłówko- wym, to LBUTTON, MBUTTON oraz RBUTTON. Mysz dwuprzyciskowa posiada jedynie przycisk lewy i prawy, natomiast jednoprzyciskowa - wyłšcznie lewy. Komunikaty myszy dla obszaru roboczego W poprzednim rozdziale zobaczyłeœ, w jaki sposób system Windows wysyła ko- munikaty klawiaturowe wyłšcznie do okna, które ma fokus wejœciowy. Komuni- katy myszy zachowujš się odmiennie: procedura okna otrzymuje takie komuni- katy za każdym razem, gdy kursor myszy znajdzie się nad obszarem okna lub gdy zostanie w nim kliknięty którykolwiek z przycisków. Dzieje się to nawet wtedy, gdy okno nie jest aktywne ani nie ma fokusu. W systemie Windows zde- finiowano 21 komunikatów zwišzanych z myszš. Jednak 11 z nich nie dotyczy obszaru roboczego. Noszš one nazwę "komunikatów nie zwišzanych z obsza- rem roboczym" (ang. nonclient-area messages), a aplikacje Windows zwykle je igno- rujš. Gdy kursor myszy przesuwa się nad obszarem roboczym, do procedury okna wy- syłany jest komunikat WM MOUSEMOVE. Jeżeli we wnętrzu obszaru robocze- go zostanie naciœnięty lub zwolniony przycisk myszy, procedura okna otrzyma jeden z komunikatów przedstawioţych w poniższej tabeli: Przycisk Naciœnięty Zwolniony Naciœnięty (drugie kliknięcie) Lewy WM LBUTTONDOWN 4VMţLBUTTONUP WM LBUTTONDBLCLK Œrodkowy WMţMBUTTONDOWN WM MBUTTONUP WM MBUTTONDBLCLK Prawv WMţRBUTTONDOWN WM RBUTTONUP WMţRBUTTONDBLCLK 2ţ Częœć i: Podstawy i Procedura okna otrzyma komunikaty MBUTTON tylko wtedy, gdy wykorzysty- wana jest mysz z trzema przyciskami, natomiast RBUTTON - gdy wykorzysty- wana jest mysz z dwoma przyciskami. Do procedury okna zostanie wysłany ko- munikat DBLCLK (podwójne kliknięcie) tylko wtedy, gdy klasa okna została zde- finiowana w sposób umożliwiajšcy ich odbieranie (zobacz podrozdział "Kliknię- cia dwukrotne"). Do wszystkich wymienionych wyżej komunikatów pozycja bieżšca myszy jest przekazywana za pomocš parametrtz lParam. Młodsze jego słowo zawiera współ- rzędnš x, natomiast starsze - współrzędnš y, przy czym punktem odniesienia dla obu jest lewy górny róg ekranu. Obie wartoœci możesz pobrać, posługujšc się ma- krami LOWORD oraz HIWORD: x g LOWORD flParam) ; y = HIWORD (lParam) ; Wartoœć parametru wParam zawiera informacje o stanie przycisków myszy oraz klawiszy [Shift] i [Ctrl]. Stan poszczególnych bitów tego parametru możesz spraw- dzić, korzystajšc z makr zdefiniowanych w pliku nagłówkowym WINUSER.H. Prefiks "MK" pochodzi od mouse key (przycisk myszy}. MICţLBUTTON Naciœnięty jest lewy przycisk myszy MK MBUTfON Naciœnięty jest œrodkowy przycisk myszy MIfţRBUTTON Naciœnięty jest prawy przycisk myszy MK SHIFT Naciœnięty jest klawisz [Shiftl MK CONTROL Naciœnięty jest klawisz [CtrII Na przykład, jeżeli odbierzesz komunikat WM LBUTTONDOWN, a wparam & MKţSHIFT ma wartoœć TRUE, oznacza to, że gdy użytkownik przycisnšł lewy przycisk my- szy, naciœnięty był również klawisz [Shift]. W trakcie przesuwania kursora myszy nad obszarem roboczym okna, system Win- dows nie generuje komunikatu WMţMOUSEMOVE dla każdego możliwego po- łożenia kursora myszy. To, ile twój program odbierze komunikatów WM MO- USEMOVE, zależy od elektroniki samej myszy oraz od prędkoœci, z jakš proce- dura okna jest w stanie przetwarzać komunikaty zwišzane z myszš. Innymi sło- wy, Windows nie zapełnia kolejki nie przetworzonymi komunikatami WM MO- USEMOVE. O tym, jaka jest rzeczywista częstotliwoœć pojawiania się komunika- tów, przekonasz się po uruchomieniu programu CONNECT opisanego poniżej. Jeżeli klikniesz lewym przyciskiem myszy w obszarze roboczym okna, które nie jest aktywne, Windows uaktywni je, a następnie przeœle do jego procedury komu- nikat WMţLBUTTONDOWN. Dlatego też za każdym razem, gdy twój program odbierze komunikat WMţLBUTTONDOWN, możesz œmiało założyć, że jego okno jest aktywne. Może się jednak tak zdarzyć, że do okna zostanie wysłany komuni- kat WMţLBUTTONUP, aIe nie zostanie poprzedzony komunikatem WM LBUT TONDOWN. Stanie się tak, gdy Iewy przycisk myszy zostanie naciœnięty nad jed- nym oknem, a zwolniony nad innym. Podobnie, procedura okna może odebrać ko- munikat tNM-LBUTTONDOWN bez odpowiadajšcego mu komunikatu 257 R^zdział 7: Mysz WMţLBUTTONUP, jeżeli przycisk zostanie zwolniony po przesunięciu myszy do innego okno. ' Sš dwa odstępstwa od tej reguły: ů Procedura okna może "przechwycić mysz". Będzie wtedy otrzymywała zwiš- zane z niš komunikaty, nawet jeżeli kursor znajdzie się nad obszarem robo- czym innego okna. O tym, w jaki sposób przechwycić mysz, dowiesz się nie- co dalej. Jeżeli na ekranie wyœwietlane jest systemowe modalne okno komunikatu (ang. . system modal message box) lub systemowe modalne okno dialogowe (ang. sys- tem modal dialog box), żaden inny program nie może odbierać komunikatów myszy. W obu przypadkach nie jest również możliwe przełšczenie do innego okna. Przykładem systemowego modalnego okna dialogowego może być to, które pojawia się, gdy zamierzasz zakończyć pracę komputera. Proste przetwarzanie komunikatów myszy: przykład Przedstawiony na rysunku 7-1 program CONNECT przetwarza kilka prostych komunikatów zwišzanych z myszš. Pozwala ci się także zorientować, w jaki spo- sób system Windows przesyła te komunikaty do twoich programów. CONNECT.C /* CONNECT.C - Demo myszy - ldczenie punktów liniami (c) Charles Petzold. 1998 */ llinclude 4ldefine MAXPOINTS 1000 LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) : int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) static TCHAR szAppNameC7 = TEXT ("Connect") : HWND hwnd : MSG msg : WNDCLASS wndclass : wndclass.style = CS HREDRAW ţ CS VREDRAW : wndclass.lpfnWndProc = WndProc : wndclass.cbClsExtra = 0 : wndclass.cbWndExtra = 0 : wndclass.hInstance = hInstance : wndclass.hIcon = LoadIcon (NULL IDI ŽPPLICATION) : wndclass.hCursor = LoadCursor (NULL, IDC ARROW) : wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITEţBRUSH) wndclass.lpszMenuName = NULL : wndclass.lpszClassName = szAppName : if (!RegisterClass (&wndclass)) i 258 ' Częœć I: Podstawy (cišg dalszy ze strony 257) MessageBox (NULL, TEXT ("Program requires Windows NT!"), szAppName, MBţICONERROR) ; I i hwnd = CreateWindow (szAppName, TEXT ("Connect-the-Points Mouse Demo"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; i while (GetMessage (&msg, NULL, 0, 0)) TranslateMessage (&msg) ; DispatchMessage (&msg) ; return msg.wParam ; I i LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) static POINT ptCMAXPCINTS] ; static int iCount ; HDC hdc ; int i, j : PAINTSTRUCT ps ; switch (message) ( case WM_LBUTTONDOWN: iCount = 0 ; ' InvalidateRect (hwnd, NULL, TRUE) ; return 0 ; case WM_MOUSEMOVE: if (wParam & MK LBUTTON && iCount < 1000) ( pt[iCount ].x = LOWORD (lParam) ; pt[iCount++],y = HIWORD (lParam) ; hdc = GetDC (hwnd) ; SetPixel (hdc, LOWORD (lParam), HIWORD (lParam), 0) ; ReleaseDC (hwnd, hdc) ; , return 0 ; case WMţLBUTTONUP: InvalidateRect (hwnd, NULL, FALSE) ; return 0 ; , case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; Rozdział l: Mysz SetCursor (LoadCursor (NULL, IDC WAIT)) : ShowCursor (TRUE) ; for (i = 0 ; i < iCount - 1 ; i++) for (j = i + 1 ; j < iCount ; j++) .x tCi].y, NULL) ; MoveToEx (hdc, ptCi] , p LineTo (hdc, ptCj].x, ptCj].y) : ShowCursor (FALSE) ; SetCursor (LoadCursor (NULL, IDC ŽRROW)) : EndPaint (hwnd. &ps) : return 0 ; case WM DESTROY: PostOuitMessage (0) ; return 0 ; i return DefWindowProc (hwnd, message, wParam, lParam) : ) Rysunek 7-1. Program CONNECT Program CONNECT przetwarza trzy komunikaty myszy: WM LBUTTONDOWN - CONNECT usuwa zawartoœć obszaru roboczego. WM MOUSEMOVE - jeżeli naciœnięty jest równoczeœnie lewy przycisk myszy, CONNECT rysuje w obszarze roboczym czarnš kropkę oraz zapamiętuje jej współ- rzędne. WMţLBUTTONUP - program CONNECT rysuje połšczenia pomiędzy wszyst- kimi kropkami zaznaczonymi w obszarze roboczym. Czasem uda się w ten spo- sób stw orzyć ładny wzór (zobacz rysunek 7-2). Rysunek 7-2. Zawartoœć okna programu CONNECT 260 c:ęœć I: Podstawy ; ; Posługiwanie się programem CONNECT jest bardzo proste. Umieœć kursor my- szy nad obszarem roboczym, naciœnij jej lewy przycisk, przesuń w dowolny spo- sób jej kursor, a następnie zwolnij przycisk. Program działa najlepiej, jeżeli ma połšczyć kilka kropek położonych na krzywej. Program CONNECT posługuje się trzema funkcjami GDI, które opisałem w roz- dziale 5: SetPixel rysuje czarny punkt po każdym odebraniu komunikatu 4VM-MO- I USEMOVE, o ile oczywiœcie naciœnięty był lewy przycisk myszy. (Jeżeli korzy- stasz z morutora pracujšcego w trybie o dużej rozdzielczoœci, punkty te mogš być niemal niewidoczne). Z kolei rysowanie linii wymaga wywołania funkcji Move- ToEx oraz LineTo. Jeżeli trzymajšc naciœnięty lewy przycisk myszy, przesuniesz jej kursor poza ob- szar roboczy, CONNECT nie będzie mógł połšczyć punktów, ponieważ nie od- bierze komunikatów WM-LBUTTONUP Gdy przesuniesz kursor myszy ponow- nie nad obszar roboczy, a następnie naciœniesz ponownie lewy przycisk, zawar- toœć obszaru roboczego zostanie usunięta. Jeżeli mimo przesunięcia kursora my- szy poza obszar roboczy chcesz kontynuować rysowanie, naciœnij lewy przycisk myszy, a następnie przesuń kursor nad obszar roboczy. Program CONNECT jest w stanie zapamiętać informacje o położeniu co najwy- żej tysišca punktów. Jeżeli założymy, że liczba punktów wynosi P, to liczba linii , które muszš zostać narysowane, wynosi P x (P - 1) / 2. Jeżeli okaże się, że użyt- kownik zaznaczył tysišc punktów, konieczne będzie narysowanie około 500 000 linii, co może zajšć minutę lub dwie, zależnie od tego, jakim komputerem dyspo- nujesz. Ponieważ Windows 98 jest systemem wielozadaniowym, możesz w tym czasie pracować z innymi programami. Natomiast nie możesz zrobić w nim nic innego (na przykład przesunšć okno lub zmienić jego rozmiar). W rozdziale 20 zajmiemy się metodami radzenia sobie z problemami tego typu. Ponieważ programowi CONNECT narysowanie wszystkich linii może zajšć tro- chę czasu, to w trakcie obsługi komunikatu WM_PAINT zmierńa kształt kursora na klepsydrę, a po zakończeniu obsługi - przywraca kształt poczštkowy. Wyma- ga to dwukrotnego wywołania funkcji SetCursor dla dwóch standardowych kur- sorów. CONNECT również dwukrotnie wywohzje funkcję ShowCursor: raz z pa- rametrem TRUE, a za drugim razem - z parametrem FALSE. Tymi wywołaniami bardziej szczegółowo zajmę się w dalszej częœci tego rozdziału pt. "Emulacja my- szy za pomocš klawiatury . Czasem na okreœlenie tego, co robi program po odebraniu informacji o przemiesz- czeniu kursora myszy, używa się słowa "œledzenie". Nie oznacza to oczywiœcie, że w procedurze okna program wchodzi w pętlę i usiłuje wywnioskować, gdzie w danym momencie znajduje się wskaŸnik myszy. Zamiast tego procedura okna przetwarza komunikaty myszy tak szybko, jak to tylko możliwe, a następnie prze- kazuje sterowanie do systemu Windows. Obsługa klawisry specjalnych Gdy program CONNECT odbierze komurukat WMţMOUSEMOVE, wykonuje bi- narnš operację AND na zawartoœci wParam oraz MK LBUTTON. Pozwala mu to zorientować się, czy naciœnięty został lewy przycisk myszy. Parametru wParam Rozdział 7: Mysz 261 można użyć również wtedy, gdy chcesz przekonać się, czy naciœnięty został kla- wisz [Shift] albo [CtrI]. Możesz przy tym poshxżyć się następujšcš konstrukcjš: if (wParam & MK SHIFT) ( if (wParam & MK CONTROL) ( Cklawisze CShiftJ i CCtrlJ sš naciœnięteJ ) else ( Cnaciœnięty jest klawisz CShiftJJ ) else ( if (wParam & MKţCONTROL) ( [naciœnięty jest klawisz CCtrlJJ 1 else { ani CCtrlJJ Cnie jest naciţnięty ani CShiftJ, Jeżeli w swoich programach zamierzasz poshxgiwać się zarówno lewym, jak i pra- wym przyciskiem myszy, a ponadto chcesz, aby mogli z tych programów rów- nież korzystać ci, którzy poshzgujš się myszš z jednym przyciskiem, powinieneœ tak napisać program, aby jednoczesne naciœnięcie klawisza [Shift] oraz lewego przycisku myszy było równoznaczne z naciœnięciem prawego przycisku myszy. Kod obsługujšcy komunikaty myszy mógłby wtedy wyglšdać tak: case WMţLBUTTONDOWN: if (!(wParam & MK SHIFT)) ( Cobsluga Iewego przycisku myszyJ return 0 ; // Kontynuacja przetwarzania case WM_RBUTTONDOWN: Cobsługa prawego przycisku myszy] return 0 ; Opisana w rozdziale 6 funkcja Windows GetKeyState może również zwrócić in- formacje dotyczšce przycisków myszy oraz klawiszy [Shift[ i [Ctrl], jeżeli jako jej parametr podany zostanie jeden z następujšcych wirtualnych kodów klawiszy: VK_LBUTTON, VK RBUTTON, VK MBUTTON, VK SHIFT oraz VK CON- TROL. Przycisk lub klawisz jest naciœnięty, jeżeli wartoœć zwrócona przez Get- KeyState jest ujemna. Ponieważ funkcja ta zwraca informację o stanie przycisków i klawiszy dotyczšcš aktualnie przetwarzanego komunikatu, jest ona doœć do- brze z nim zsynchronizowana. Jak z pewnoœciš pamiętasz, nie można korzystać z funkcji GetKeyState w odniesieniu do klawiszy, które dopiero majš być wciœnię- te. Nie można również czekać w ten sposób na naciœnięcie przycisku. Dlatego nigdy nie powinieneœ pisać takich poleceń jak to: while (GetKeyState (VKţLBUTTON) >= 0) ; // LE !!! 262 Częœć I: Podstawy Funkcja GetKeyState tylko wtedy zwróci informacje o naciœniętym przycisku, je- żeli został on naciœnięty już wczeœniej. Kliknięcia dwukrotne Kliknięcie dwukrotne to dwa kliknięcia rozdzielone krótkš przerwš. Aby dwa kliknięcia mogły być ze sobš połšczone, muszš nastšpić w niezbyt odległych od siebie obszarach (domyœlnie, między jednym kliknięciem a drugim kursor my- szy może przesunšć się o około jednš szerokoœć znaku czcionki systemowej i o pół jego wysokoœci) oraz w niezbyt długim czasie noszšcym nazwę szybkoœci dwu- krotnego kliknięcia (ang. double-click speed). Szybkoœć tš możesz modyfikować poshxgujšc się apletem Mysz znajdujšcym się w Panelu sterowania. Jeżeli chcesz, aby twoja procedura okna otrzymywała komunikaty zwišzane z dwukrotnym kliknięciem, to zanim zarejestrujesz klasę okna za pomocš funk- cji IZegisterClass, w polu stylu struktury opisujšcej klasę musisz podać identyfika- tor CS DBLCLKS: wndclass.style = CS HREDRAW ţ CSţVREDRAW ţ CSţDBLCLKS ; Jeżeli natomiast nie włšczysz identyfikatora CS DBLCLKS, a użytkownik dwu- krotnie kliknie lewym przyciskiem myszy, procedura okna otrzyma następujšce komunikaty: WMţLBUTTONDOWN WM LBUTTONUP 4VMţLBUTTONDOWN WM LBUTTONUP Może się również okazać, że pomiędzy wymienionymi powyżej komunikatami procedura okna odbierze również inne komunikaty. Jeżeli zamierzasz zaimple- mentować swojš własnš obsługę dwukrotnego kliknięcia, za pomocš funkcji Get- MessageTime możesz pobrać czas, na podstawie którego wyznaczysz odstęp po- między kliknięciami. Funkcjš tš dokładniej zajmiemy się w rozdziale 8. Jeżeli do stylu rejestrowanej klasy okna włšczysz identyfikator CS_DBLCLKS , w przypadku pojawienia się dwukrotnego kliknięcia procedura okna odbierze na- stępujšcš serię komunikatów: WMţLBUTTONDOWN WMţLBUTTONUP WMţLBUTTONDBLCLK WMţLBUTTONUP Komunikat WM LBUTTONDBLCLK zastšpił po prostu WMţLBUTTONDOWN z poprzedniego przykładu. Obsługa dwukrotnego kliknięcia jest znacznie łatwiejsza, jeżeli pierwsze kliknię- cie wykonuje tę samš operację co kliknięcie jednokrotne. Drugie kliknięcie (ko- munikat 4VMţLBUTTONDBLCLK) odpowiedzialne jest wtedy za operację do- datkowš w porównaniu do pierwszego kliknięcia. Zwróć na przykład uwagę, w jaki sposób mysz wykorzystywana jest do wybierania plików z listy w Eksplo- Rozdział 7: Mysz 263 ratorze Windows. Pojedyncze kliknięcie zaznacza plik. Powoduje to odwrócenie kolorów liter i tła jego nazwy. Z kolei dwukrotne kliknięcie wykonuje dwie ope- racje: w odpowiedzi na komunikat odpowiadajšcy kliknięciu plik zostaje zazna- czony, podczas gdy drugi komunikat, tym razem odpowiadajšcy dwukrotnemu kliknięciu, powoduje otworzenie zaznaczonego pliku. To bardzo proste. Nieste- ty, wszystko się komplikuje, jeżeli przy podwójnym kliknięciu nie wykonuje tej samej czynnoœci, co jednokrotne kliknięcie. Komunikaty myszy nie zwišzane z obszarem roboczym Omówione do tej pory 10 komunikatów myszy pojawia się, gdy jej kursor znaj- duje się wewnštrz obszaru roboczego okna. Jeżeli natomiast kursor znajduje się poza obszarem roboczym, ale w dalszym cišgu w oknie, system Windows wysy- ła do procedury okna odmienny zestaw komunikatów. Obszary te to: pasek tytu- łu, menu oraz paski przewijania. Zwykle nie musisz zajmować się komunikatami spoza obszaru roboczego. Po pro- stu musisz przekazać je do Def4VindowProc, a całš pracę wykona za ciebie Win- dows. W tym sensie komunikaty te podobne sš do komunikatów klawiaturowych WM-SYSKEYDOWN, WM SYSKEYUP oraz WM-SYSCHAR. Komunikaty myszy nie zwišzane z obszarem roboczym sš niemal dokładnym odpowiednikiem komunikatów zwišzanych z tym obszarem. Ich identyfikatory zawierajš litery "NC" (ang. nonclient), co oznacza, że nie dotyczš one obszaru roboczego. Jeżeli kursor myszy przesuwany jest poza obszarem roboczym, do procedury okna wysłany zostaje komunikat WM NCMOUSEMOVE. Przyciski myszy generujš komunikaty przedstawione w poniższej tabeli: Przycisk Naciœnięty Zwolniony Naciœnięty (drugie kliknięcie) Lewy WM IVCLBUTTONDOWN WMţNCLBUTTONUP WMţNCLBUTTONDBLCLK Œrodkowy WMţNCMBUTTONDOWN WM NCMBUTTONUP WMţNCMBUTTONDBLCLK Prawy WM NCRBUTTONDOWN WM NCRBUTTONUP WM NCRBUTTONDBLCLK Parametry wParam oraz lParam przekazywane razem z komunikatami myszy nie dotyczšcymi obszaru roboczego różniš się jednak od wykorzystywanych przez komunikaty zwišzane z tym obszarem. Parametr wParam wykorzystywany jest do identyfikacji obszaru, w którym naciœnięty został przycisk lub przesunięty kur- sor myszy. Identyfikatory poszczególnych obszarów rozpoczynajš się od liter "HT" (ang. hit-test - test na trafienie) i zostały zdefiniowane w pliku nagłówko- wym WINUSER.H. Młodsze słowo parametru lParam wykorzystywane jest do przekazywania współ- rzędnej x kursora, natomiast starsze - współrzędnej y. Jednakże sš to współrzęd- ne ekranowe, a nie współrzędne obszaru roboczego, jak miało to miejsce w przy- - Częœć i: Podstawy padku komunikatów zwišzanych z obszarem roboczym. W przypadku współ- rzędnych ekranowych, w lewym górnym rogu ekranu zarówno współrzędna x, jak i y majš wartoœć 0. Wartoœć współrzędnej x zwiększa się w miarę przesuwa- nia się w prawo, natomiast współrzędnej y - w miarę przesuwania się w dół. (Zobacz rysunek 7-3). Możliwe jest przekształcanie współrzędnych ekranowych na współrzędne obszaru roboczego i odwrotnie za pomocš dwóch funkcji systemu Windows: ScreenToClient (hwnd, &pt) ; ClientToScreen (hwnd, &pt) ; Parametr pt jest strukturš POINT. Obie funkcje przekształcajš wartoœci przeka- zywane za pomocš tej struktury bez zachowywania ich poprzednich wartoœci. Zwróć uwagę, że jeżeli punkt znajduje się poza obszarem roboczym lub po jego lewej stronie, współrzędne obszaru roboczego będš miały wartoœci ujemne. Wţędne ekranu v Rysunek 7-3. Współrzędne ekranowe oraz współrzędne obszaru roboczego Komunikat testu na trafienie obrzeża okna Jeżeli jeszcze się nie zgubiłeœ, to wiesz, że do tej pory przedstawiłem już 20 z 21 komunikatów zwišzanych z myszš. Ostatnim z nich jest WMţNCHITTEST. Jego nazwa pochodzi od angielskiego nonclient hit test (test na trafienie obrzeża okna). Komunikat ten poprzedza każdy inny komunikat myszy dotyczšcy zarówno ob- szaru roboczego, jak i obrzeża okna. Za pomocš parametru lParam przekazywa- ne sš ekranowe współrzędne x i y kursora myszy. Natomiast wParam nie jest wy- korzystywany. Rozdział 7: klysz 265 Większoœć aplikacji przekazuje ten komurukat do DeţWndowProc. Następnie Win- dows wykorzystuje go do wygenerowania wszystkich pozostałych komunikatów zwišzanych z myszš. W przypadku komunikatów myszy spoza obszaru robo- czego wartoœć zwrócona przez funkcję DeţtNindowProc przekazywana jest do pro- cedury okna jako parametr wParam. Może to być dowolna wartoœć parametru wPa- ram towarzyszšca komunikatom myszy nie zwišzanym z obszarem roboczym lub jedna z następujšych: HTCLIENT Obszar roboczy HTNOWHEftE Żadne okno HTTRANSPARENT Okno przykryte przez inne HTERROR Def iNindowProc generuje krótki sygnał dŸwiękowy Jeżeli po przetworzeniu komunikatu WM NCHITTEST funkcja DeftNindowProc zwróci wartoœć HTCLlENT, system Windows dokonuje konwersji ze współrzęd- nych ekranowych na współrzędne obszaru roboczego, a następnie generuje ko- munikat myszy. Jeżeli pamiętasz jeszcze, w jaki sposób zablokowaliœmy wszystkie systemowe hxnkcje klawiatury, to zapewne się zastanawiasz, czy można zrobić coœ podob- nego z komunikatami myszy. Oczywiœcie! Jeżeli dodasz do procedury okna na- stępujšce linie: case WM NCHITTEST: return (LRESULT) HTNOWHERE ; spowodujesz, że w twoim oknie zostanš zablokowane wszystkie komunikaty myszy zarówno zwišzane z obszarem roboczym, jak i z nim nie zwišzane. Kiedy mysz znajdzie się nad obszarem okna, włšczajšc w to ikonę menu systemowego, przyciski modyfikujšce wielkoœć okna oraz ikonę zamykajšcš okno, przyciski my- szy nie będš po prostu działać. Komunikaty generujšce komunikaty System Windows posługuje się komunikatem WM NCHITTEST do wygenerowa- nia pozostałych komunikatów myszy. Pomysł tworzera jednych komunikatów na podstawie innych jest w tym systemie doœć powszechnie stosowany. Rozważmy pewien przykład. Jak z pewnoœciš wiesz, dwukrotne kliknięcie ikony menu syste- mowego powoduje zamknięcie okna. Dwukrotne kliknięcie generuje serię komu- nikatów WMţNCHTITEST. Ponieważ kursor myszy znajduje się nad ikonš menu systemowego, funkcja Def 4VindowProc zwraca wartoœć HTSYSMENU, a Windows umieszcza w kolejce komunikat WMţNCLBUTTONDBLCLK, przy czym jego pa- rametrowi wParam nadana zostaje wartoœć HTSYSMENU. Procedura okna przekazuje zwykle ten komunikat do DefWindowProc. Gdy z ko- lei DeftNindowProc odbierze komunikat WM NCLBUTTONDBLCLK z parame- trem wParam równym HTSYSMENU, umieszcza w kolejce komunikat WM SY- SCOMMAND z wParam równym SC CLOSE. (Ten sam komunikat WM SY SCOMMAND generowany jest również wtedy, gdy użytkownik wybierze pole- cenie Zamknij z menu systemowego). Po raz kolejny procedura okna przekazuje otrzymany komunikat do Def4VindowProc. Z kolei DefWindowProc w odpowiedzi wysyła do funkcji okna komunikat WM CLOSE. 266 Częœć I: Podstawy R Jeżeli przed zamknięciem program wymaga potwierdzenia użytkownika, to w procedurze okna powinien zostać przechwycony komunikat WM_CLOSE. W przeciwnym bowiem razie trafi on do funkcji Def tNindowPrac, która w odpo- wiedzi wywoła fixnkcję DestroyWindow. Zadaruem tej funkcji jest wysłanie do pro- cedury okna komunikatu WM DESTROY, gdzie zwykle zostaje on obsłużony w następujšcy sposób: case WM_DESTROY: PostOuitMessage (0) ; return 0 ; Funkcja PostQuitMessage powoduje, że Windows umieszcza w kolejce komuni- kat WMţQUIT. Nie dotrze on nigdy do procedury okna, ponieważ po jego po- braniu funkcja GetMessage zwróci wartoœć 0, co spowoduje zakończenie wykony- wania pętli obshzgi komunikatów programu. Testowanie trafienia w twoich programach Wczeœniej wspomniałem, w jaki sposób Eksplorator Windows reaguje na kliknięcia oraz na dwukrotne kliknięcia. Zwykle program (lub kontrolka listy wykorzysty- wana przez Eksplorator Windows) musi dokładnie okreœlić, który plik lub kata- log został wskazany przez użytkownika. Operacja ta nosi miano testowania trafienia (ang. hit-testing). Dokładnie w ten sam sposób, w jaki DefWindowProc po otrzymaniu komunikatu WM_NCHITTEST sprawdza, który element okna został trafiony, procedura okna musi okreœlić, który element wewnštrz jej obszaru roboczego został trafiony. Dlatego też procedura okna musi wykonać pewne obliczenia, posługujšc się współrzędnymi x i y kur- sora przekazanymi za pomocš parametru lParam. Przykład hipotetyczny Oto przykład. Załóżmy, że twój program ma wyœwietlić w kilku kolumnach listę alfabetycznie posortowanych nazw plików. Zwykle powinieneœ skorzystać z wi- doku listy, ponieważ potrafi on wykonać za ciebie całš pracę zwišzanš z testo- waniem trafienia. Załóżmy jednak, że z jakiegoœ powodu nie jest to możliwe. Musisz zrobić to sam. Załóżmy również, że posortowane alfabetycznie nazwy plików przechowywane sš w postaci tablicy wskaŸników do napisów o nazwie szFileNames. Załóżmy ponadto, że lista plików wyœwietlana jest od górnej krawędzi obszaru roboczego, który ma wielkoœć zapamiętanš w cxClient oraz cyClient. Poszczegól- ne kolumny majš szerokoœć cxColWidth punktów, natomiast znaki wykorzysty- wanej przez nas czcionki majš wysokoœć cyChar punktów. Liczba plików, które możesz wyœwietlić w jednej kolumnie, wynosi: iNumInCol = cyClient / cxColWidth ; Gdy program otrzymuje komurukat wywołany kliknięciem myszš, współrzędne jej kursora cxMouse oraz cyMouse możesz pobrać z parametru IParam. Następnie, poshzgujšc się przedstawionym niżej wyrażeniem, możesz wyznaczyć kolumnę, którš wskazał użytkownik: Rozdział 7: Mysz iColumn = cxMouse /cxColWidth ; Położenie nazwy pliku względem poczštku kolumny to iFromTop = cyMouse /cyChar ; Możesz teraz wyznaczyć indeks w tablicy szFileNames. iIndex = iColumn * iNumCol + iFromTop ; Jeżeli wartoœć iIndex jest większa niż liczba nazw plików przechowywanych w ta- blicy, oznacza to, że użytkownik kliknšł pusty obszar okna. W wielu przypadkach testowanie trafienia jest bardziej złożone, niż w przedstawio- nym przykładzie. Gdy wyœwietlasz rysunek składajšcy się z wielu częœci, musisz okreœlić położenie każdej z nich. Testowanie trafienia polega bowiem na przekształ- ceniu współrzędnych na interesujšcy cię obiekt. Zadanie to może okazać się szcze- gólnie trudne w przypadku edytorów tekstu posługujšcych się czcionkš o zmiennej szerokoœci, ponieważ należy cofnšć się i znaleŸć pozycję znaku w napisie. Przykładowy program Przedstawiony na rysunku 7-4 program CHECKEIZI demonstruje prosty test na trafienie. Dzieli on obszar roboczy na 25 prostokštów tworzšcych tablicę o wy- miarach 5 na 5. Jeżeli klikniesz myszš dowolny z prostokštów, zostanie w nim wpisany znak X. Jeżeli klikniesz go po raz kolejny, znak ten zostanie usunięty. CHECKERl.C /* CHECKERl.C - Test trafień myszy, wersja 1 (c) Charles Petzold, 1998 */ ilinclude 4idefine DIVISIONS 5 LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) static TCHAR szAppName[] = TEXT ("Checkerl") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CS llREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL IDC ŽRROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITEţBRUSH) wndclass.lpszMenuName = NULL ; wnfclass.lpszClassName = szAppName ; Częœć t: Podstawy (cišg dalszy ze strony 267) if (!RegisterClass (&wndclass)) , MessageBox (NULL, TEXT ("Program requires Windows NT!"), szAppName, MBţICONERROR) ; J hwnd = CreateWindow (szAppName, TEXT ("Checkerl Mouse Hit-Vest Demo"), ; WS_OVERLAPPEDWINDOW, ; CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) , TranslateMessage (&msg) ; DispatchMessage (&msg) ; 1 return msg.wParam ; LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ( static BOOL fState[DIVISIONS](DIVISIONS] ; static int cxBlock, cyBlock ; HDC hdc ; int x, y ; PAINTSTRUCT ps ; RECT rect ; switch (message) case WM_SIZE : cxBlock = LOWORD (lParam) / DIVISIONS ; cyBlock = HIWORD (lParam) / DIVISIONS ; return 0 ; case WM LBUTTONDOWN : x = LOWORD (lParam) / cxBlock ; y = HIWORD (lParam) l cyBlock ; if (x < DIVISIONS && y < DIVISIONS) ( fState [x][y] ^= 1 ; rect.left = x * cxBlock ; rect.top = y * cyBlock ; rect.right = (x + 1) * cxBlock ; rect.bottom = (y + 1) * cyBlock ; InvalidateRect (hwnd, &rect, FALSE) ; Rozdział 7: Mysz 269 else MessageBeep (0) ; return 0 ; case WM_PAINT : hdc = BeginPaint (hwnd, &ps) ; for (x = 0 ; x < DIVISIONS ; x++) for (y = 0 ; y < DIVISIONS ; y++) f Rectangle (hdc, x * cxBlock, y * cyBlock, (x + 1) * cxBlock, (y + 1) * cyBlock) ; if (fState [x][y]) 1 MoveToEx (hdc, x * cxBlock, y * cyBlock, NULL) ; LineTo (hdc, (x+1) * cxBlock. (y+1) * cyBlock) ; MoveToEx (hdc. x * cxBlock, (y+1) * cyBlock, NULL) ; LineTo (hdc, (x+1) * cxBlock, y * cyBlock) ; l EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY : PostOuitMessage (0) ; return 0 ; ) return DefWindowProc (hwnd, message, wParam, lParam) ; Rysunek 7-4. Program CHECKER1 Na rysunku 7-5 przedstawione zostało okno programu CHECKERl. Wszystkie prostokšty narysowane przez program majš takie same wymiary. Szerokoœć i wy- sokoœć przechowywane sš w zmiennych cxBlock oraz cyBlock. Ich wartoœci wy- znaczane sš za każdym razem, gdy zmienia się wielkoœć okna. W ramach obsłu- gi komunikatu WMţLBUTTONDOWN do okreœlenia, który z prostokštów zo- stał wskazany, wykorzystywane sš współrzędne kursora myszy. Następnie zmie- niany jest stan prostokšta przechowywany w tablicy fState. Na koniec zostaje unie- ważniony obszar roboczy, co z kolei powoduje wygenerowanie komunikatu WM PAINT. 270 Częœć I: Podstawy Rysunek 7-5. Okno programu CHECKERI Jeżeli okaże się, że wysokoœć lub szerokoœć okna nie jest podzielna przez 5, nie- wielki pasek po lewej stronie lub u dołu nie zostanie pokryty przez żaden z pro- stokštów. Kliknięcie jednego z tych obszarów traktowane jest jako błšd i sygnali- zowane za pomocš MessageBeep. Gdy CHECKER1 otrzyma komunikat WM PAINT, odœwieży cały obszar robo- czy, posługujšc się funkcjš GDI ftectangle. Jeżeli flaga fState odpowiadajšca dane- mu prostokštowi jest ustawiona, program rysuje dwie przekštne prostokšta, wy- wohzjšc funkcje MoveToEx oraz LineTo. W trakcie obsługi komunikatu WMţPA- INT program CHECKER1 nie sprawdza, czy dany prostokšt znalazł się w unie- ważnionym obszarze roboczym. Jedna z możliwych metod sprawdzania polega na budowaniu struktury RECT dla każdego prostokšta (można w tym celu wy- korzystać kod zbliżony do tego, który pojawił się w obsłudze komunikatu WMţLBUTTONDOWN). Następnie, wywołujšc funkcję Intersetftect, należałoby sprawdzić, czy dany prostokšt ma częœć wspólnš z unieważnionym obszarem (dostępny jako ps.rcPaint). Emulacja myszy za pomocš klawiatury Do poshzgiwania się programem CHECKER1 niezbędna jest mysz. Wkrótce uzu- pełnimy go o interfejs klawiatury tak, jak zrobiliœmy to w programie SYSMETS opisanym w rozdziale 6. Ponieważ jednak tym razem mysz wykorzystywana jest do wskazywania obiektów, konieczne będzie zatroszczenie się o wyœwietlanie i przemieszczanie kursora myszy. Nawet jeżeli mysz nie jest zainstalowana, Windows w dalszym cišgu może wy- œwietlać jej kursor. Jest z nim zwišzane coœ, co w tym systemie nosi nazwę "licz- nika wyœwietleń" (ang. display count). Jeżeli mysz jest zainstalowana, jego poczšt- kowš wartoœciš jest 0, w przeciwnym wypadku jest to -1. Kursor myszy wyœwie- tla się tylko wtedy, gdy wartoœć liczruka nie jest ujemna. Licznik możesz zwięk- szyć, wywołujšc: Rozdział 7: Mysz 271 ShowCursor (TRUE) ; natomiast zmniejszyć za pomocš: ShowCursor (FALSE) ; Zanim wywołasz funkcję ShowCursor, nie musisz sprawdzać, czy mysz jest zain- stalowana. Jeżeli niezależnie od jej obecnoœci chcesz wyœwietlić kursor, po prostu zwiększ wartoœć licznika, wywołujšc ShowCursor. Teraz zmniejszenie jego warto- œci spowoduje ukrycie kursora, jeœli mysz nie jest zainstalowana. A jeœli mysz była obecna w systemie, kursor pozostanie na ekranie System Windows zarzšdza informacjš o aktualnym położeniu kursora nawet wtedy, gdy mysz nie została zainstalowana. Może nawet wyœwietlić kursor w do- wolnym punkcie ekranu. Pozostanie on tam tak dhzgo, jak dhzgo jawnie go nie przeniesiesz. Położenie kursora możesz pobrać, wywołujšc: GetCursorPos (&pt) gdzie pt jest strukturš POINT. W jej polach umieszczone zostajš współrzędne x i y myszy. Pozycję kursora możesz ustawić, wywohxjšc SetCursorPos (x, y) ; W obu przypadkach x i y to współrzędne ekranowe. (Powinno to być jasne, po- nieważ funkcja nie wymaga podania parametru hwnd). Jak już wspomniałem, możesz konwertować współrzędne ekranowe na współrzędne obszaru robocze- go i vice versa, wywołujšc funkcję ScreenToClient oraz ClientToScreen. Jeżeli zdecydujesz się wywołać funkcję GetCursorPos w trakcie obsługiwania komu- nikatu myszy, może się okazać, że otrzymane w ten sposób współrzędne różniš się nieco od tych, które przekazane zostały za pomocš parametru lParam. Dzieje się tak, ponieważ funkcja GetCursorPos zwraca aktualne współrzędne, natomiast IParam prze- chowuje współrzędne kursora z momentu wygenerowania komunikatu. Prawdopodobnie chcesz, aby napisany przez ciebie interfejs klawiatury pozwo- lił na przemieszczanie kursora myszy za pomocš klawiszy strzałek, natomiast naciœnięcie klawiszy [Spacja] lub [Enter] symulowało przyciœnięcie przycisku my- szy. Na pewno nie chcesz, aby kursor myszy przemieszczał się o jeden punkt za każdym naciœnięciem klawisza. Zmuszałoby to użytkownika do długiego przy- trzymania klawisza. Jeżeli mimo wszystko zdecydowałeœ, że w zaimplementowanym przez ciebie interfejsie klawiatury kursor będzie się przemieszczał co jeden punkt, zrób to przynajmniej tak, aby na poczštku jego ruch był wolny, a póŸniej ulegał przy- œpieszeniu. Z pewnoœciš pamiętasz, że parametr IParam towarzyszšcy komuni- katowi WMţKEYDOWN zawiera informację o tym, czy komunikat ten został wy- generowany na skutek automatycznego powtarzania klawisza. Jest to idealne za- stosowanie tej informacji. Dodanie interfejsu klawiatury do programu CHECKER Przedstawiony na rysunku 7-6 program CHECKER2 jest kopiš wczeœniejszego CHECKERI, z tš jednak różnicš, że został w nim zaimplementowany interfejs klawiatury. Do przemieszczania kursora pomiędzy prostokštami możesz posłu- żyć się klawiszami kursora. Naciœnięcie klawisza [Home] spowoduje przeniesie- 272 CzęœĆ I: Podstawy nie kursora do górnego prawego prostokšta, natomiast klawisza [End] - do dol- nego lewego. Naciœnięcie klawisza [Spacja] lub [Enter] powoduje zaznaczenie wskazywanego aktualnie prostokšta. CHECKER2.C /* CHECKER2.C - Test trafień myszy, wersja 2 (c) Charles Petzold, 1998 */ ţţinclude ţţdefine DIVISIONS 5 LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine. int iCmdShow) static TCHAR szAppNameC] = TEXT ("Checker2") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CS VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC ARROW) ; wndcla.ss.hbrBackground = (HBRUSH) GetStockObjecv (WHITE BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) ( MessageBox (NULL, TEXT ("Program requires Windows NT!"), szAppName, MBţICONERROR) ; return 0 ; j ) hwnd = CreateWindow (szAppName, TEXT ("Checker2 Mouse Hit-Test Demo"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CWţUSEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) TranslateMessage (&msg) ; DispatchMessage (&msg) ; ; Rozdział 7: Mvsz 273 ; return msg.wParam ; i LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParcm, LPARAM lParam) I ? static BOOL fStateCDIVISIONS]CDIVISIONS] ; static int cxBlock, cyBlock ; HDC hdc ; int x, y ; PAINTSTRUCT ps ; POINT point ; RECT rect ; switch (message) i f case WM_SIZE : cxBlock = LOWORD (lParam) / DIVISIONS ; cyBlock = HIWORD (lParam) / DIVISIONS ; return 0 ; case WM_SETFOCUS : ShowCursor (TRUE) ; return 0 ; I i case WM_KILLFOCUS : ShowCursor (FALSE) ; . return 0 ; case WM_KEYDOWN : GetCursorPos (&point) , ScreenToClient (hwnd, &point) ; x = max (0, min (DIVISIONS - 1, point.x / cxBlock)) ; y = max (0, min (DIVISIONS - 1, point.y / cyBlock)) ; switch (wParam) ( case VK UP : break ; case VK_DOWN : y++ ; break ; case VK_LEFT : x-- break ; case VK_RIGHT : x++ ; break ; case VK_HOME : x = y = 0 ; break ; case UK_END : x = y = DIVISIONS - 1 ; 274 Częœć I: Podstawy (cišg dalszy ze strony 273) break ; case UK_RETURN : case VK_SPACE : SendMessage (hwnd, WM_LBUTTONDOWN, MK LBUTTON, MAKELONG (x * cxBlock, y * cyálock)) ; break ; ) x = (x + DIVISIONS) % DIVISIONS ; y = (y + DIVISIONS) % DIVISIONS ; point.x = x * cxálock + cxálock / 2 ; point.y = y * cyálock + cyBlock / 2 ; ClientToScreen (hwnd, &point) ; SetCursorPos (point.x, point.y) ; return 0 ; case WM_LBUTTONDOWN : x = LOWORD (lParam) / cxálock ; y = HIWORD (lParam) / cyálock ; if (x < DIVISIONS && y < DIVISIONS) ( fState[xţCy] ^= 1 ; rect.left = x * cxálock ; rect.top = y * cyálock ; rect.right = (x + 1) * cxálock ; rect.bottom = (y + 1) * cyBlock ; InvalidateRect (hwnd, &rect, FALSE) ; ) else Messageáeep (0) ; return 0 ; case WM_PAINT : hdc = BeginPaint (hwnd, &ps) ; for (x = 0 ; x < DIUISIONS ; x++) for (y = 0 ; y < DIVISIONS ; y++) ( Rectangle (hdc, x * cxBlock, y * cyBlock, (x + 1) * cxálock, (y + 1) * cyBlock) ; if (fState Cx]Cy]) ( MoveToEx (hdc, x *cxBlock, y *cyBlock, NULL) ; LineTo (hdc, (x+1)*cxálock, (y+1)*cyBlock) ; MoveToEx (hdc, x *cxBlock, (y+1)*cyBlock, NULL) ; LineTo (hdc, (x+1)*cxálock, y *cyBlock) ; ) ) EndPaint (hwnd, &ps) ; return 0 ; Rozdział 7: MVsz 275 case WM_DESTROY : PostOuitMessage (0) ; return 0 ; l return DefWindowProc (hwnd, message, wParam, lParam) ; Rysunek 7-6. Program CHECKER2 Obsługa komunikatu WM-KEYDOWN w programie CHECKER2 obejmuje okre- œlenie pozycji kursora za pomocš funkcji GetCursorPos, konwersję współrzędnych ekranowych na współrzędne obszaru roboczego za pomocš ScreenToClient oraz podzielenie każdej z nich przez szerokoœć i wysokoœć prostokšta. W ten sposób otrzymuje się pozycje w tablicy o rozmiarach 5 na 5. Ponieważ w momencie klik- nięcia kursor nie musi znajdować się nad obszarem roboczym, konieczne jest prze- filtrowanie otrzymanych współrzędnych przez makra min i max, co spowoduje, że rezultat będzie zawierał się w przedziale od 0 do 4. Jeżeli naciœnięty został któryœ z klawiszy sterujšcych ruchem kursora, współrzędne x oraz y zostajš odpowiednio zwiększone lub zmniejszone. Jeżeli natomiast naci- œnięta została [Spacja] lub [Enter], CHECKER2 wysyła sam do siebie komunikat WMţLBUTTONDOWN. Jest to metoda bardzo podobna do tej, która została za- stosowana w programie SYSMETS z rozdziału 6. Obsługa komunikatu WM KEY DOWN kończy się wyznaczeniem współrzędnych obszaru roboczego wskazujš- cych na œrodek prostokšta, przekonwertowaniu ich na współrzędne ekranowe za pomocš funkcji ClientToScreen oraz na ustawieniu kursora myszy za pomocš Set- CursorPos. Wykorzystanie okien potomnych do testowania trafienia Częœć programów (na przykład Windows Paint) dzieli obszar roboczy na kilka mniejszych obszarów. Po lewej stronie okna Painta umieszczone zostały ikony oznaczajšce dostępne narzędzie, natomiast u dołu - paleta kolorów. Gdy program ten sprawdza, czy któryœ z tych obszarów nie został trafiony, musi wzišć pod uwagę fragment obszaru roboczego, zanim spróbuje okreœlić, który właœciwie ele- ment użytkowruk miał na myœli. A może wcale tak nie jest? W rzeczywistoœci Paint upraszcza sobie zarówno rysowa- nie, jak i sprawdzanie trafionego obszaru, przez wykorzystanie okien potomnych. Pozwalajš one na podział całego obszaru roboczego na kilka mniejszych prostokšt- nych obszarów. Każde z okien potomnych ma swój własny uchwyt, procedurę okna oraz obszar roboczy. Każda z procedur odbiera tylko te komunikaty, które dotyczš jej okna. W komunikatach myszy parametr IParam zawiera współrzędne kursora względem lewego górnego rogu obszaru roboczego okna potomnego, a nie wzglę- dem okna nadrzędnego (które w przypadku Painta jest głównym oknem programu). Takie wykorzystanie okien potomnych ułatwia modularyzację programu. Jeżeli okna te oparte zostały o inne klasy, każde z nich może posługiwać się swojš wła- snš procedurš okna. Okna różnych klas mogš również mieć inne tła oraz różne kursory. W rozdziale 9 zapoznamy się z kontrolkami okien potomnych, które sš predefiniowanymi oknami przyjmujšcymi postać pasków przewijania, przycisków 276 Czţœć I: Podstawy lub pól edycji. Jednak teraz zajmijmy się tym, w jaki sposób możemy wykorzy- stać okna potomne w programie CHECKER. Okna potomne w programie CHECKER Na rysunku 7-7 przedstawiony został program CHECKER3. Ta wersja tworzy 25 okien potomnych, których zadaniem jest reagowanie na komunikaty myszy. Nie został w nim zaimplementowany interfejs klawiatury, jednak opierajšc się na tym, co przedstawiłem w jego poprzedniej wersji, z łatwoœciš możesz zrobić to sam. CHECKER3.C /* CHECKER3.C - Test trafień myszy, wersja 3 (c) Charles Petzold, 1998 */ ţţinclude ţţdefine DIVISIONS 5 LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; LRESULT CALLBACK ChildWndProc (HWND, UINT, WPARAM, LPARAM) ; TCHAR szChildClassC] = TEXT ("Checker3 Child") ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) static TCHAR szAppName[7 = TEXT ("Checker3") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CSţVREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITEţBRUSH) wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) ( MessageBox (NULL, TEXT ("Program requires Windows NT!"), szAppName, MB-ICONERROR) ; return 0 wndclass.lpfnWndProc = ChildWndProc ; wndclass.cbWndExtra = sizeof (long) ; wndclass.hIcon = NULL ; wndclass.lpszClassName = szChildClass ; Roţzial 7: Mysz 277 ł RegisterClass (&wndclass) ; hwnd = CreateWindow (szAppName, TEXT ("Checker3 Mouse Hit-Test Demo"), WS_OVERLAPPEDWINDOW, CW USEDEFAULT, CW USEDEFAULT, CW_USEDEFAULT, CW USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) f TranslateMessage (&msg) ; DispatchMessage (&msg) ; i return msg.wParam ; I LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) i i static HWND hwndChildCDIVISIONS]CDIVISIONS] ; int cxBtock, cyBlock, x, y ; I: switch (message) case WM_CREATE : for (x = 0 ; x < DIVISIONS ; x++) i for (y = 0 ; y < DIVISIONS ; y++) hwndChild[x][y] = CreateWindow (szChildClass, NULL, WS_CHILDWINDOW ţ WS VISIBLE, 0, 0, 0, 0, hwnd, (HMENU) (y Ž 8 ţ x), (HINSTANCE) GetWindowLong (hwnd, GWL HINSTAtdCE), NULL) ; ! return 0 ; case WM_SIZE : cxBlock = LOWORD (lParam) / DIVISIONS ; cyBlock = HIWORD (lParam) / DIVISIONS ; for (x = 0 ; x < DIVISIONS ; x++) for (y = 0 ; y < DIVISIONS ; y++) MoveWindow (hwndChildCx][y], x * cxBlock, y * cyBlock, cxBlock, cyBlock, TRUE) ; return 0 ; case WM_LBUTTONDOWN : ( MessageBeep (0) ; I return 0 ; case WM_DESTROY : PostOuitMessage (0) ; ' return 0 ; , return DefWindowProc (hwnd, message, wParam, lParam) ; ) 278 Częœć I: Podstawy i (cišg dalszy ze strony 277) i LRESULT CALLBACK ChildWndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) HDC hdc ; PAINTSTRUCT ps ; RECT rect ; switch (message) ( case WM_CREATE : SetWindowLong (hwnd, 0, 0) ; // flaga włdczenia/wyldczenia return 0 ; case WM_LBUTTONDOWN : SetWindowLong (hwnd, 0, 1 ^ GetWindowLong (hwnd, 0)) ; InvalidateRect (hwnd, NULL, FALSE) ; return 0 ; case WM_PAINT : hdc = BeginPaint (hwnd, &ps) ; GetClientRect (hwnd, &rect) ; Rectangle (hdc, 0, 0, rect.right, rect.bottom) ; if (GetWindowLong (hwnd, 0)) MoveToEx (hdc, 0, 0, NULL) ; LineTo (hdc, rect.right, rect.bottom) ; MoveToEx (hdc, 0, rect.bottom, NULL) LineTo (hdc, rect.right, 0) ; EndPaint (hwnd, &ps) ; return 0 ; ) return OefWindowProc (hwnd, message, wParam, lParam) ; Rysunek 7-7. Program CHECKER3 Program CHECKER3 posiada dwie funkcje okna o nazwach WndProc oraz Chil- dWndProc. Pierwsza z nich w dalszym cišgu wykorzystywana jest przez główne okno programu (okno nadrzędne). Z kolei ChildWndProc jest funkcjš okna dla 25 okien potomnych. Obie funkcj,e muszš zostać zdefiniowane jako CALLBACK. Ponieważ procedura okna zwišzana jest z jednš, szczególnš strukturš klasy okna, którš rejestrujesz za pomocš funkcji RegisterClass, program CHECKER3 wymaga zarejestrowania dwóch klas. Klasa pierwszego okna przeznaczona jest dla okna głównego i nosi nazwę "Checker3". Natomiast nazwa drugiej klasy to "Chec- ker3 Child". Wybrane nazwy nie muszš spełniać jakichœ istotnych kryteriów. CHECKER3 rejestruje obie klasy okna w funkcji WinMain. Po zarejestrowaniu pierwszej, wykorzystuje tę samš strukturę wndclass w trakcie rejestracji drugiej. Wszystkie jej pola, za wyjštkiem czterech przedstawionych niżej, nie sš modyfi- kowane: 6 Rozdział 7: Mysz ; ů Polu lpfnWndProc przypisywana jest procedura okna potomnego ChiIdWnd- ' Proc. ů Polu cbWndExtra zostaje nadana wartoœć 4, a dokładniej siezeof(long). Zawar- toœć tego pola nakazuje systemowi Windows zarezerwowanie dodatkowych 4 bajtów na wewnętrzne dane, dostępne dla każdego okna, które zostanie stwo- rzone na podstawie tej klasy. Nie będš to jednak wspólne dane okien: każde z nich może przechowywać w tym obszarze swoje własne informacje. ů Pole hlcon zostaje ustawione na NULL, ponieważ takie okna potomne, jakie wykorzystane zostały w programie CHECKER3, nie potrzebujš ikon. ů Polu pszClassName nadany zostaje łańcuch "Checker3 Child" będšcy nazwš klasy. Funkcja CreateWindow na podstawie klasy Checker3 tworzy okno główne progra- mu wywołane w WinMain. Ten fragment programu niczym nie różni się od po- przednich. Jednak gdy funkcja WinProc otrzyma komunikat WM CREATE, wy- wołuje CreateWindow 25 razy, aby w oparciu o klasę Checker3 Child, utworzyć 25 okien potomnych. W poniższej tabeli porównane zostały parametry przeka- zywane do funkcji CreateWindow wywoływanej w WinMain oraz w WndProc. Parametr Okno główne Okno potomne klasa okna "Checker3" "Checker3 Child" nagłówek okna "Checker3 .... NULL styl okna WM OVERLAPPEDWINDOW WS CHILDWINDOW I WS VISIBLE położenie w pionie CW USEDEFAULT 0 położenie w poziomie CW USEDEFAULT 0 ' szerokoœć CW USEDEFAULT 0 wysokoœć CW USEDEFAULT 0 ! uchwyt okna nadrzędnego NULL hwnd uchwyt menu/ NULL (HMENU)(y Ž 8 I x) identyfikator potomka uchwyt realizacji hInstance (HINSTANCE) GetWindowLong (hwnd, GWL HINSTANCE) dodatkowe parametry NULL NULL Zwykle okna potomne wymagajš podania parametrów okreœlajšcych ich wiel- koœć oraz położenie. Jednak w programie CHECKER sš one odpowiednio roz- mieszczane w oknie nadrzędnym w dalszej częœci funkcji WndProc. W przypad- ku okna głównego uchwyt okna nadrzędnego ma wartoœć NULL, ponieważ wła- œnie to okno jest oknem nadrzędnym. Z kolei parametr ten jest obowišzkowy w przypadku wywołania funkcji CreateWindow tworzšcej okno potomne. Okno główne nie ma menu, dlatego odpowiadajšcy mu parametr ma wartoœć NULL. W przypadku okien potomnych ten sam parametr nosi nazwę "identyfi- kator potomka" (ang. child ID) lub "identyfikator okna potomnego" (ang. child window ID). Jest to liczba, dzięki której możliwa jest jednoznaczna identyfikacja okna potomnego. Identyfikator ten ma większe znaczenie, gdy pracujesz z kon- . 280 Częœć I. Podstawy trolkami umieszczonymi w oknie dialogowym, o czym przekonasz się w rozdziale 11. W programie CHECKER3 zdecydowałem, że identyfikatorem tym będzie liczba wynikajšca ze współrzędnych x i y, które okreœlajš położenie każdego okna w ta- blicy o 5 wierszach i 5 kolumnach w oknie głównym. Funkcja CreateWindow wymaga podamia uchwytu instancji (realizacji). W funkcji Win- Main uchwyt ten jest bardzo łatwo dostępny, ponieważ jest jej parametrem. Gdy z kolei tworzone jest okno potomne, CHECKEIZ3 musi posłużyć się funkcjš GetWindowLong, która umożliwia pobranie wartoœci hlnstance ze struktury, którš Windows przecho- wuje dla każdego okna. (Zamiast wy'woływania GetWindowLong mógłbym zdefinio- wać po prostu zmiennš globalnš i korzystać bezpoœrednio z jej zawartoœci). Każde z okien potomnych ma swój własny uchwyt, który przechowywany jest w tablicy hwndChild. Gdy funkcja WndProc odbierze komurukat WMţSIZE, dla każdego z 25 okien potomnych wywołana zostanie funkcja MoveWindow. Para- metry tej funkcji pozwalajš na okreœlenie lewego górnego rogu okna potomnego względem obszaru roboczego okna głównego, a także jego wysokoœci i szeroko- œci. Możesz również okreœlić, czy zawartoœć okna potomnego powinna zostać od- œwieżona. Przyjrzyjmy się teraz ChildWndProc - funkcji okna potomnego. Jest ona odpowie- dzialna za przetwarzanie komunikatów docierajšcych do każdego z 25 okien potomnych. Przekazywany do niej parametr hwnd jest uchwytem okna potom- nego, które odebrało komunikat. Gdy ChildWndProc przetwarza komunikat WMţCREATE (co odbywa się 25 razy, ponieważ tyle jest okien potomnych), wy- wołuje funkcję SetWindowWord, umieszczajšc w obszarze danych okna wartoœć 0. (Jak z pewnoœciš pamiętasz, zarezerwowaliœmy ten obszar, korzystajšc z pola cbWndExtra struktury definiujšcej klasę okna). Funkcja ChildWndProc poshxguje się tš wartoœciš, aby przechować aktualny stan prostokšta (zaznaczony lub nie). Po kliknięciu okna potomnego, w ramach obshzgi komunikatu WMţLBUTTON- DOWN zmieniana jest po prostu wartoœć tej flagi (z 0 na 1 lub z 1 na 0), a następ- nie unieważniony zostaje cały obszar okna. Obshzga komunikatu WM PAINT jest tym razem trywialna, ponieważ wielkoœć prostokšta, który należy narysować, jest dokładnie taka sama jak wielkoœć obszaru roboczego. Ponieważ zarówno plik z kodem Ÿródłowym, jak i z programem sš większe niż w przypadku programu CHECKERl, nie będę nawet próbował przekonać cię, że CHECKER3 jest "prostszy". Zwróć jednak uwagę, że nie musimy już wykony- wać żadnych testów na trafienie! Jeżeli okno potomne w programie CHECKER3 otrzyma jakikolwiek komunikat WM LBUTTONDOWN, oznacza to, że zostało wybrane i nic więcej nie musi ono wiedzieć. Okna potomne a klawiatura Wydaje się, że dodanie do CHECKER3 interfejsu klawiatury będzie ostatniš lo- gicznš rzeczš, którš będziemy mogli zrobić. Jednak tym razem powinniœmy po- dejœć do problemu trochę inaczej. W programie CHECKER2 położenie kursora myszy mówiło, które okno zostanie zaznaczone, gdy naciœnięty zostanie klawisz [Spacja]. Ponieważ korzystamy z okien potomnych, możemy w pewnym stopniu wzorować się na funkcjonowaniu okien dialogowych. Jeżeli okno potomne w oknie Rozdzial 7: Mysz 281 dialogowym ma fokus, sygnalizuje to za pomocš migajšcej karetki lub naryso- wanego wokół siebie przerywanego prostokšta. Nie zamierzam oczywiœcie powielać tu wszystkich funkcji okien dialogowych, które oczywiœcie sš już "zaszyte" gdzieœ w systemie. Zamierzam po prostu z grub- sza symulować w aplikacji zachowanie się okna dialogowego. Gdy będę wyja- œniał, w jaki sposób należy to zrobić, odkryjesz, że okno główne oraz okna po- tomne powinny wspólnie obsługiwać klawiaturę. Okno potomne powinno nary- sować lub usunšć zaznaczenie, jeżeli naciœnięty został klawisz [Spacja] lub [En- ter]. Z kolei okno główne powinno przenosić fokus z jednego okna potomnego do następnego, gdy naciœnięty zostanie któryœ z klawiszy kursora. Czynnoţci te ulegnš jednak pewnej komplikacji, ponieważ kliknięcie okna potomnego spowo- duje, że fokus otrzyma nie ono, ale okno główne. Program CHECKER4 przedstawiony został na rysunku 7-8. CHECKER4.C /* CHECKER4.C - Test trafień myszy, wersja 4 (c) Charles Petzold, 1998 */ ţţinclude 4idefine DIVISIONS 5 LRESULT CALLBACK WndProc (HWND, UINT. WPARAM, LPARAM) ; LRESULT CALLBACK ChildWndProc (HWND, UINT, WPARAM, LPARAM) ; int idFocus = 0 ; TCHAR szChildClassCJ = TEXT ("Checker4 Child") ; int WINAPI WinMain (HINSTANCE hInstance, KINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) static TCHAR szAppNameCJ = TEXT ("Checker4") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ( CS VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDCţARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE-BRUSH) wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) ( MessageBox (NULL, TEXT ("Program requires Windows NT!"), szAppName, MB-ICONERROR) ; 282 Częœć I: Podstawy (cišg dalszy ze strony 281) return 0 i wndclass.lpfnWndProc = ChildWndProc ; wndclass.cbWndExtra = sizeof (long) ; wndclass.hIcon = NULL ; wndclass.lpszClassName = szChildClass ; RegisterClass (&wndclass) ; i hwnd = CreateWindow (szAppName, TEXT ("Checker4 Mouse Hit-Test Demo"), WS_OVERLAPPEDWINDOW, CW USEDEFAULT, CW USEDEFAULT, I - - CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) ( TranslateMessage (&msg) ; DispatchMessage (&msg) ; i return msg.wParam ; LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) static HWND hwndChildCDIVISIONS][DIVISIONS] ; int cxBlock, cyBlock, x, y ; , switch (message) case WM_CREATE : for (x = 0 ; x < DIVISIONS ; x++) for (y = 0 ; y < DIVISIONS ; y++) hwndChild[x]Cy] = CreateWindow (szChildClass, NULL, WS_CHILDWINDOW ţ WS-VISIBLE, 0, 0, 0, 0, ' hwnd, (HMENU) (y Ž 8 ţ x), (HINSTANCE) GetWindowLong (hwnd, GWL-HINSTANCE), NULL) ; return 0 ; case WM_SIZE : cxBlock = LOWORD (lParam) / DIVISIONS ; - cyBlock = HIWORD (lParam) / DIVISIONS ; for (x = 0 ; x < DIVISIONS ; x++) for (y = 0 ; y < DIVISIONS ; y++) MoveWindow (hwndChild[x][y], x * cxBlock, y * cyBlock, cxBlock, cyBlock, TRUE) ; return 0 ; case WM-LBUTTONDOWN : Rozdział 7: Mysz 283 MessageBeep (0) ; , return 0 ; // Po odebraniu komunikatu SETţFOCUS przenieœ fokus // do okna potomnego case WM_SETFOCUS: SetFocus (GetDlgItem (hwnd, idFocus)) ; return 0 ; // Po odebraniu komunikatu WM_KEYDOWN, możliwe że należy // zmienić okno, które ma fokus case WM_KEYDOWN: x = idFocus & OxFF ; y = idFocus Ż 8 ; i switch (wParam) case UK_UP: y-- , break ; ' case UK_DOWN: y++ ; break ; case VK LEFT: x-- , break ; case VK RIGHT: x++ ; break ; case VK HOME: x = y = 0 ; break ; case VKţEND: x = y = DIVISIONS - 1 ; break ; default: return 0 ; ) x = (x, + DIVISIONS) ţ6 DIVISIONS ; y = (y + DIVISIONS) ţ DIVISIONS ; idFocus = y Ž 8 ţ x ; 1 SetFocus (GetDlgItem (hwnd, idFocus)) ; return 0 ; case WM_DESTROY : PostOuitMessage (0) ; return 0 ; t, return DefWindowProc (hwnd, message, wParam, lParam) ; LRESULT CALLBACK ChildWndProc (HWND hwnd, UINT message œţ WPARAM wParam, LPARAM lParam) HDC hdc ; PAINTSTRUCT ps ; RECT rect ; switch (message) case WM_CREATE : SetWindowLong (hwnd, 0, 0) ; // flaga wlšczona/wyldczona return 0 ; "''jr:, , case WM_KEYDOWN: i,: // Wyœlij większoœć komunikatów klawiaturowych // do okna 9lównego 284 Częœć 1: Podstawy (cišg dalszy ze strony 283) if (wParam != VK RETURN && wParam != VKţSPACE) SendMessage (GetParent (hwnd), message, wParam, lParam) ; return 0 ; ) I // Jeżeli naciœnięto [Enter] lub spację - kontynuacja // i zaznaczenie lub odznaczenie prostokšta case WM_LBUTTONDOWN : SetWindowLong (hwnd, 0, i ^ GetWindowLong (hwnd, 0)) ; SetFocus (hwnd) ; InvalidateRect (hwnd, NULL, FALSE) ; return 0 ; // Komunikaty zwidzane z fokusem - unieważnienie obszaru // roboczego wymuszajdce ponowne wyœwietlenie case WM_SETFOCUS: idFocus = GetWindowLong (hwnd, GWLţID) ; // kontynuacja case WM KILLFOCUS: InvalidateRect (hwnd, NULL, TRUE) ; return 0 ; case WM_PAINT : hdc = BeginPaint (hwnd. &ps) ; GetClientRect (hwnd, &rect) ; Rectangle (hdc, 0, 0, rect.right, rect.bottom) ; // Narysuj znak "x" if (GetWindowLong (hwnd, 0)) ( MoveToEx (hdc, 0, 0, NULL) ; LineTo (hdc, rect.right, rect.bottom) ; MoveToEx (hdc, 0, rect.bottom, NULL) ; LineTo (hdc, rect.right, 0) ; // Narysuj prostokdt oznaczajdcy fokus if (hwnd ţ GetFocus ()) i rect.left += rect.right / 10 ; rect.right -= rect.left ; rect.top += rect.bottom / 10 ; rect.bottom -= rect.top ; SelectObject (hdc, GetStockObject (NULL_BRUSH)) ; SelectObject (hd.c, CreatePen (PS DASH, 0, 0)) ; Rectangle (hdc, rect.left, rect.top, rect.right, rect.bottom) ; Oelete0bject (Sel.ectObject (hdc, GetStockObject (BLACKţPEN))) ; Rozdziai 7: Mysz 285 EndPaint (hwnd, &ps) ; return 0 ; ? return DefWindowProc (hwnd, message, wParam, lParam) ; Rysunek 7-8. Program CHECKER4 Jak pamiętasz, każde z okien potomnych ma swój własny unikatowy identyfika- tor, który został zdefiniowany podczas tworzenia okna za pomocš funkcji Create- Window. W naszym programie jest on zwišzany ze współrzędnymi prostokšta x i y. Dysponujšc uchwytem okna, program może pobrać jego identyfikator za po- mocš: idChild = GetWindowLong (hwndChild, GWLţID) ; Poruższa funkcja wykonuje dokładnie tę samš operację: idChild = GetDlgCtrlID (hwndChild) ; Jak sugeruje nazwa funkcji, jej podstawowym zadaniem jest praca z oknami dia- logowymi i znajdujšcymi się w nich kontrolkami. Jeżeli znasz uchwyt okna głów- nego oraz identyfikator okna potomnego, z łatwoœciš możesz wyznaczyć uchwyt okna potomnego: hwndChild = GetDlgItem (hwndParent, idChild) ; Zmienna globalna idFocus wykorzystywana jest przez program CHECKER4 do przechowywania identyfikatora tego okna potomnego, które aktualnie ma fokus. Jak wspomniałem wczeœniej, kliknięcie myszš okna potomnego nie powoduje automatycznie, że otrzyma fokus. Dlatego też okno główne powinno w takiej sy- tuacji obshzżyć komunikat WMţSETFOCUS, wywohxjšc SetFocus (GetDlgItem (hwnd, idFocus)) ; co spowoduje ustawienie fokusu wejœciowego na wybranym oknie potomnym. Funkcja ChildWndProc przetwarza zarówno komunikat WM SETFOCUS, jak i WMţKILLFOCUS. W przypadku pojawienia się tego pierwszego, w zmiennej globalnej idFocus zapamiętany zostaje identyfikator okna potomnego, które otrzy- mało ten komunikat. Następnie niezależnie od tego, który komunikat został ode- brany, obszar roboczy okna zostaje unieważniony, co z kolei powoduje wygene- rowanie komunikatu WMţPAINT. Jeżeli w trakcie obsługi tego komunikatu oka- że się, że odrysowywane okno ma fokus, do narysowania prostokšta wykorzy- stywane jest pióro PS DASH. Funkcja ChiIdWndProc przetwarza również komunikat WMţKEYDOWN. Jeżeli nie został on spowodowany naciœnięciem [Spacji] ani [Enter], zostaje odesłany do okna głównego. W przeciwnym wypadku wykonywane sš takie same czyn- noœci jak po odebraniu komunikatu WMţLBUTTONDOWN. Przetwarzanie naciskanych klawiszy kursora przekazane zostało do okna głów- nego. Podobnie jak miało to miejsce w CHECKER2, program pobiera współrzęd- ne x i y tego okna potomnego, które ma fokus, a następnie przenosi fokus w spo- sób zależny od naciœniętego klawisza, korzystajšc przy tym z funkcji SetFocus. 286 Częœć I: Podstawy Przechwytywanie myszy Zwykle procedura okna otrzymuje komurukaty myszy tylko wtedy, gdy jej kur- sor znajduje się nad obszarem okna. Jednak w pewnych sytuacjach może się oka- zać, że program musi również otrzymywać te komunikaty, nawet jeżeli mysz zna- lazła się poza oknem. W takiej sytuacji możliwe jest przechwycenie (ang. capture) myszy. Nie bój się, to nie będzie bolało. Blokowanie prostokšta Poniższy przykład pomoże ci zrozumieć, dlaczego w pewnych sytuacjach prze- chwytywanie myszy może okazać się niezbędne. Popatrz na przedstawiony na rysunku 7-9 program BLOKOUTI. Na pierwszy rzut oka może się wydawać, że wszystko jest w jak najlepszym porzšdku. Ma on jednak pewnš "wadę". BLOKOUTl.C /* BLOKOUTl.C - Program demonstrujdcy użycie przycisków myszy (c) Charles Petzold, 1998 */ ţţinclude LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) static TCHAR szAppName[] = TEXT ("BlokOutl") HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CSţVREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITEţBRUSH) wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) t ! MessageBox (NULL, TEXT ("Program requires Windows NT!"), ! szAppName, MBţICONERROR) ; return 0 : ) hwnd = CreateWindow (szAppName, TEXT ("Mouse Button Demo"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW USEDEFAULT, CW USEDEFAULT, Rozdział 7: Mysz NULL, NULL, hInstance, NULL) ; i ShowWindow (hwnd, kCmdShow) ; ;, UpdateWindow (hwnd) ; ; while (GetMessage (&msg, NULL, 0, 0)) ( TranslateMessage (&msg) ; DispatchMessage (&msg) ; ? return msg.wParam ; void DrawBoxOutline (HWND hwnd, POINT ptBeg, POINT ptEnd) Ii HDC hdc ; hdc = GetDC (hwnd) ; Ij SetROP2 (hdc, R2_NOT) ; SelectObject (hdc, GetStockObject (NULLţBRUSH)) ; I.: Rectangle (hdc, ptBeg.x, ptBeg.y, ptEnd.x, ptEnd.y) ; ReleaseDC (hwnd, hdc) ; LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) i static BOOL fBlocking, fValidBox ; static POINT ptBeg, ptEnd, ptBoxBeg, ptBoxEnd ; HDC hdc ; PAINTSTRUCT ps ; switch (message) ( case WM_LBUTTONDOWN : ptBeg.x = ptEnd.x = LOWORD (lParam) ; ptBeg.y = ptEnd.y = HIWORD (lParam) ; DrawBoxOutline (hwnd, ptBeg, ptEnd) : i i SetCursor (LoadCursor (NULL, IDC CROSS)) ; fBlocking = TRUE ; return 0 : case WM_MOUSEMOVE : if (fBlocking) ^ SetCursor (LoadCursor (NULL, IDC CROSS)) ; DrawBoxOutline (hwnd, ptBeg, ptEnd) ; ptEnd.x = LOWORD (lParam) ; ptEnd.y = HIWORD (lParam) ; DrawBoxOutline (hwnd, ptBeg, ptEnd) ; return 0 ; , 288 Częœć I: Podstawy (cišg dalszy ze strony 287) case WM_LBUTTONUP : if (fBlocking) i DrawBoxOutline (hwnd, ptBeg, ptEnd) ; ptBoxBeg = ptBeg ; ptBoxEnd.x = LOWORD (lParam) ; ptBoxEnd.y = HIWORD (lParam) ; SetCursor (LoadCursor (NULL, IDC ARROW)) ; fBlocking = FALSE ; fValidBox = TRUE ; InvalidateRect (hwnd, NULL, TRUE) ; J return 0 ; f case WM_CHAR : if (fBlocking & (wParam ţ '\x1B')) // tzn. [Esc7 ( DrawBoxOutline (hwnd, ptBeg, ptEnd) ; r SetCursor (LoadCursor (NULL, IDC ARROW)) ; fBlocking = FALSE ; ) return 0 ; case WM_PAINT : hdc = BeginPaint (hwnd, &ps) ; ; if (fValidBox) i SelectObject (hdc, GetStockObject (BLACK BRUSH)) ; Rectangle (hdc, ptBoxBeg.x, ptBoxBeg.y, ptBoxEnd.x, ptBoxEnd.y) ; ) if (fBlocking) SetROP2 (hdc, R2_NOT) ; ! SelectObject (hdc, GetStockObject (NULL_BRUSH)) ; Rectangle (hdc, ptBeg.x, ptBeg.y, ptEnd.x, ptEnd.y) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY : PostOuitMessage (0) ; , return 0 ; return DefWindowProc (hwnd, message, wParam, lParam) ; Rysunek 7-9. Program BLOKOLTTI Rozdział 7: Mysz 289 Program ten pokazuje coœ, co może zostać zaimplementowane w dowolnym pro- gramie graficznym. Zabawę możesz zaczšć od naciœnięcia lewego przycisku myszy, aby zaznaczyć wierzchołek prostokšta. Przesuń następnie mysz. Pomię- dzy zaznaczonym punktem a aktualnym położeniem kursora naszkicowany zo- stał prostokšt. Po zwolnieniu przycisku myszy prostokšt zostanie wypełniony. , Na rysunku 7-10 przedstawiony został jeden gotowy prostokšt oraz jeden w trakcie 1 tworzenia. Rysunek 7-10. Okno programu BLOKOUT1 W czym problem? Spróbuj zrobić coœ takiego: naciœnij lewy przycisk myszy nad obszarem roboczym okna programu BLOKOUTI, a następnie przesuń jej kursor poza krawędŸ okna. Program nie otrzymuje już komunikatów WM MOUSEMOVE. A teraz zwolnij przycisk. BLOKOUT1 nie odebrał również komunikatu WMţLBUTTONUP, po- nieważ kursor znalazł się poza obszarem roboczym. A teraz przenieœ kursor po- nownie do wnętrza okna. Procedura okna w dalszym cišgu myœli, że przycisk jest naciœnięty. To niedobrze. Program nie wie, o co chodzi. Przechwycenie Program BLOKOUTI pokazał jednš z typowych funkcji, jednak implementujšcy jš kod ma wadę. Jest to jednak ten rodzaj problemu, do którego rozwišzania wymy- œlono przechwytywanie myszy. Jeżeli użytkownik przesuwa kursor myszy, to nie powinno mieć znaczenia, że znalazł on się chwilowo poza obszarem okna. Program powinien stale kontrolować ruch myszy. Przechwycenie myszy nie jest czynnoœciš zbyt skomplikowanš. Wystarczy wy- wołać jedynie SetCapture (hwnd) ; Od tego momentu Windows będzie przesyłał wszystkie komunikaty zwišzane z myszš do procedury tego okna, którego uchwyt przekazany został jako para- metr funkcji, niezależnie od tego, czy kursor myszy będzie znajdował się nad ob- 290 Częœć I: Podstawy szarem roboczym, czy nie. Parametr lParam w dalszym cišgu będzie przechowy- wał pozycję kursora we współrzędnych obszaru roboczego. Jednakże, jeżeli kur- sor myszy znajdzie się nad obszarem roboczym lub po lewej jego stronie, jedna ze współrzędnych (albo obie) będzie miała wartoœć ujemnš. Gdy chcesz uwolnić kursor myszy, wystarczy wywołać funkcję ReleaseCapture () ; Od tego momentu przywrócone zostanie normalne przekazywanie komunikatów myszy. W 32-bitowej wersji Windows przechwytywanie myszy jest nieco bardziej restryk- cyjne niż we wczeœniejszych wersjach tego systemu. Szczególnie jeżeli mysz zo- stała przechwycona, lecz jej przycisk nie jest właœnie wciœnięty, a kursor znajdzie się nad obszarem innego okna, wtedy właœnie to okno będzie otrzymywało ko- munikat. Jest to zabieg konieczny, aby nie blokować całego systemu, kiedy je- den program przechwyci mysz i jej nie zwolni. Aby uniknšć problemu, twoje programy powinny przechwytywać mysz tylko wte- dy, gdy nad ich obszarem roboczym został wciœnięty jeden z przycisków. Mysz powinna zostać zwolniona wraz ze zwolnieniem przycisku. Program BLOKOUT2 Na rysunku 7-11 przedstawiony został program BLOKOUT2 ilustrujšcy przechwy- tywanie myszy. BLOKOUT2.C /* BLOKOUT2.C - Program demonstrujšcy przyciski i przechwytywanie myszy (c) Charles Petzold, 1998 */ ţţinclude LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) static TCHAR szAppName[] = TEXT ("Blok0ut2") HWND hwnd ; MSG msg ; WNDCLASS wndclass : wndclass.style = CS HREDRAW ţ CS-VREDRAW ; wndclass.lpfnWndProc" = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITEţBRUSH) wndclass.lpszMenuName = NULL ; Rozdział 7: Mysz 291 wndclass.lpszClassName = szAppName : if (!Re9isterClass (&wndclass)) ( MessageBox (NULL. TEXT ("Program requires Windows NT!"), szAppName, MBţICONERROR) : return 0 : ) hwnd = CreateWindow (szAppName, TEXT ("Mouse Button & Capture Demo"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW USEDEFAULT, CW_USEDEFAULT, CW USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) : UpdateWindow (hwnd) : while (GetMessage (&ms9, NULL, 0, 0)) TranslateMessage (&msg) ; DispatchMessage (&msg) ; return msg.wParam ; void DrawBoxOutling (HWND hwnd, POINT ptBeg, POINT ptEnd) HDC hdc : hdc = GetDC (hwnd) ; SetROP2 (hdc, R2 NOT) : SelectObject (hdc, GetStockObject (NULLţBRUSH)> : Rectangle (hdc, ptBeg.x, ptBeg.y, ptEnd.x, ptEnd.y) : ReleaseDC (hwnd, hdc) ; LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ( static BOOL fBlocking, fUalidBox : static POINT ptBeg, ptEnd, ptBoxBeg, ptBoxEnd ; HDC hdc ; PAINTSTRUCT ps ; switch (message) case WM_LBUTTONDOWN : ptBeg.x = ptEnd.x = LOWORD (lParam) ; ptBeg.y = ptEnd.y = HIWORD (lParam) ; DrawBoxOutline (hwnd, ptBeg. ptEnd) : SetCapture (hwnd) : SetCursor (LoadCursor (NULL, IDC CROSS)) : fBlocking = TRUE ; 292 Częœć Iţ Podstawy (cišg dalszy ze strony 291) return 0 ; case WM_MOUSEMOVE : if (fBlocking) SetCursor (LoadCursor (NULL, IDC CROSS)) ; : DrawBoxOutline (hwnd, ptBeg, ptEnd) ; ptEnd.x = LOWORD (lParam) ; ptEnd.y = HIWORD (lParam) ; DrawBoxOutline (hwnd, ptBeg. ptEnd) ; return 0 ; case WM_LBUTTONUP : : if (fBlocking) DrawBoxOutline (hwnd, ptBeg, ptEnd) ; ptBoxBeg = ptBeg ; ptBoxEnd.x = LOWORD (lParam) ; ptBoxEnd.y = HIWORD (lParam) ; ReleaseCapture () ; SetCursor (LoadCursor (NULL, IDC ARROW)) ; fBlocking = FALSE ; fUalidBox = TRUE ; InvalidateRect (hwnd, NULL, TRUE) ; return 0 ; case WM_CHAR : if (fBlocking & (wParam = '\x1B')) // tzn. [Esc] DrawBoxOutline (hwnd, ptBeg, ptEnd) ; ReleaseCapture () ; SetCursor (LoadCursor (NULL, IDC ARROW)) ; i fBlocking = FALSE ; ) i return 0 ; case WM_PAINT : hdc = BeginPaint (hwnd, &ps) ; ; f if (fValidBox) ( SelectObject (hdc, GetStockObject (BLACK BRUSH)) ; Rectangle (hdc, ptBoxBeg.x, ptBoxBeg.y, ptBoxEnd.x, ptBoxEnd.y) ; Rozdzial 7: Mysz 293 if (fBlocking) ( SetROP2 (hdc, R2_NOT) ; SelectObject (hdc, GetStockObject (NULL_BRUSH)) ; Rectangle (hdc, ptBeg.x, ptBeg.y, ptEnd.x, ptEnd.y) EndPaint (hwnd, &ps) ; return 0 : case WM DESTROY : PostOuitMessage (0) ; return 0 ; 1 return DefWindowProc (hwnd, message, wParam, lParam) ; Rysunek 7-11. Program BLOKOUT2 Program BLOKOUT2 różni się od BLOKOUT1 zaledwie kilkoma liniami kodu: wywołaniem SetCapture w ramach obsługi komunikatu WM LBUTTONDOWN oraz wywołaniami ReleaseCapture w ramach obsługi WM LBUTTONUP. Możesz teraz wypróbować wprowadzone zmiany. Zmniejsz okno tak, aby nie zajmowa- ło całego ekranu i rozpocznij kreœlenie prostokšta w obszarze roboczym, a na- stępnie przemieœć kursor poza ten obszar, obojętne do góry czy do dołu, i zwol- nij przycisk myszy. Program będzie teraz miał wszelkie niezbędne informacje do narysowania poprawnego prostokšta. Po prostu powiększ okno i sprawdŸ. Przechwytywanie myszy nie jest zarezerwowane wyłšcznie dla jakichœ dziwnych aplikacji. Powinieneœ to robić zawsze, gdy musisz œledzić komunikaty WM MO- USEMOVE po naciœnięciu przycisku myszy w obszarze roboczym. Przechwyty- wanie możesz zakończyć dopiero wtedy, gdy przycisk zostanie zwolniony. Nie doœć, że dzięki temu pisane przez ciebie programy będš prostsze, to jeszcze speł- niš wymagania użytkownika. Kółko myszy Pewnego dnia moja matka, nieœwiadomie parafrazujšc Emersona, powiedziała: "Wymyœl lepszš pułapkę na myszy, a cały œwiat wydepcze œcieżkę do twoich drzwi". Oczywiœcie, w naszych czasach większe znaczenie miałoby wynalezie- nie lepszej myszy. Funkcja Microsoft IntelliMouse, będšca rozszerzeniem możliwoœci zwykłej my- szy, zmaterializowała się w postaci niewielkiego kółka umieszczonego pomiędzy dwoma przyciskami. Jeżeli poruszysz je, zadziała on w taki sam sposób, jak œrod- kowy przycisk myszy. Jeżeli natomiast obrócisz je palcem wskazujšcym, wyge- nerowany zostanie specjalny komunikat WMţMOUSEWHEEL. Programy, które potrafiš go wykorzystać, w odpowiedzi przewijajš lub powiększajš wyœwietla- ny przez siebie dokument. Na pierwszy rzut oka ta innowacja może się wyda- wać rukomu niepotrzebnš sztuczkš, muszę jednak przyznać, że bardzo szybko przyzwyczaiłem się do nowego sposobu przewijania zawartoœci dokumentu w programie Microsoft Word lub w Internet Explorer. 294 Częœć I: Podstawy Nie zamierzam tu nawet przymierzać się do dyskusji o wszelkich możliwych zale- tach kółka. Zamiast tego po prostu pokażę ci, w jaki sposób możesz je wykorzystać do przewijania danych w obszarze roboczym w takim programie jak SYSMETS4. Na rysunku 7-12 przedstawiona została ostateczna wersja tego programu. SYSMETS.C /* SYSMETS.C - Ostateczna wersja programu wyœwietlajšcego wymiary elementów graficznych (c) Charles Petzold, 1998 */ ţţinclude ţţinclude "sysmets.h" LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) static TCHAR szAppName[] = TEXT ("SysMets") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass : wndclass.style = CS_HREDRAW ţ CS VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE-BRUSH) wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) i MessageBox (NULL, TEXT ("Program requires Windows NT!"), szAppName, MBţICONERROR) ; return 0 ; hwnd = CreateWindow (szAppName, TEXT ("Get System Metrics"), WS_OVERLAPPEDWINDOW ţ WS_VSCROLL ţ WS HSCROLL, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) i TranslateMessage (&msg) ; DispatchMessage (&msg) ; Rozdzial 7: Mysz 295 1 return msg.wParam ; 1 LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ( static int cxChar, cxCaps, cyChar. cxClient, cyClient, iMaxWidth ; static int iDeltaPerLine, iAccumDelta ; // obslugd kólka myszy HDC hdc ; int i, x, y, iVertPos, iHorzPos, iPaintBeg, iPaintEnd ; PAINTSTRUCT ps ; SCROLLINFO si ; TCHAR szBuffer[10] ; TEXTMETRIC tm ; ULONG ulScrollLines ; // obsluga kólka myszy switch (message) ( case WM_CREATE: hdc = GetDC (hwnd) ; GetTextMetrics (hdc, &tm) ; cxChar = tm.tmAveCharWidth ; cxCaps = (tm.tmPitchAndFamily & 1 ? 3 : 2) * cxChar / 2 ; cyChar = tm.tmHeight + tm.tmExternalLeadin9 ; ReleaseDC (hwnd, hdc) ; // Zapamiętaj szerokoœć trzech kolumn iMaxWidth = 40 * cxChar + 22 * cxCaps ; // Kontynuacja - pobranie informacji o kółku CdSe WM_SETTINGCHANGE: SystemParametersInfo (SPI GETWHEELSCROLLLINES, 0, &ulScrollLines, 0) ; i // ulScrollLines zwykle ma wartoœć 3 lub 0 (gdy bez przewijania) // WHEEL DELTA jest równe 120, dlatego iDeltaPerLine bedzie wynosilo 40 if (ulScrollLines) iDeltaPerLine = WHEEL DELTA / ulScrollLines ; else iDeltaPerLine = 0 ; return 0 ; case WM_SIZE: j cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; ' ,. // Ustaw zakres pionowego paska przewijania oraz // wielkoœć strony si.cbSize = sizeof (si> ; si.fMask = SIF RANGE ţ SIFţPAGE ; si.nMin = 0 : 4, si.nMax = NUMLINES - 1 ; si.nPage = cyClient / cyChar ; 296 Częœć I: Podstawy (cišg dalszy ze strony 295) SetScrollInfo (hwnd, SB VERT, &si, TRUE) ; // Ustaw zakres poziomego paska przewijania oraz wielkoœć strony si.cbSize = sizeof (si) ; si.fMask = SIF RANGE ţ SIFţPAGE ; si.nMin = 0 ; si.nMax = 2 + iMaxWidth / cxChar ; si.nPage = cxClient / cxChar ; SetScrollInfo (hwnd, SB HORZ, &si, TRUE) ; return 0 ; case WM_VSCROLL: // Pobierz wszystkie dane o przesunięciu w pionie si.cbSize = sizeof (si) ; si.fMask = SIF_ALL ; GetScrollInfo (hwnd, SB VERT, &si) ; // Zapamiętaj polożenia dla póŸniejszego porównania iVertPos = si.nPos ; t switch (LOWORD (wParam)) ( case SB_TOP: ' si.nPos = si.nMin ; break ; ,. case SB_BOTTOM: si.nPos = si.nMax ; break ; case SB_LINEUP: ` f si.nPos -= 1 ; break ; case SB_LINEDOWN: ' si nPos += 1 ; i break ; i i case SB_PAGEUP: " si.nPos -= si.nPage ; break ; case SB_PAGEDOWN: si.nPos += si.nPage ; break ; case SB_THUMBTRACK: si nPos = si.nTrackPos ; break ; default: break ; J Rozdział7: Mysz // Ustaw położenie, a następnie przywróć je. Na skutek ustawień // Windows może to nie być taka sama wartoœć, jaka została ustawiona. si.fMask = SIF_POS ; SetScrollInfo (hwnd, SB_VERT, &si, TRUE) ; GetScrollInfo (hwnd, SB VERT. &si) ; // Jeżeli położenie uległo zmianie, przesuń zawartoœć i odœwież if (si.nPos != iVertPos) i ScrollWindow (hwnd, 0, cyChar * (iVertPos - si.nPos), NULL, NULL) ; UpdateWindow (hwnd) ; ) return 0 ; case WM HSCROLL: // Pobierz wszystkie dane o przesunięciu w poziomie ; si.cbSize = sizeof (si) ; si.fMask = SIF ALL ; i, // Zapamiętaj położenia, aby póŸniej porównać GetScrollInfo (hwnd, SB HORZ, &si) ; "' iHorzPos = si.nPos ; switch (LOWORD (wParam)) case SB_LINELEFT: si nPos -= 1 ; break ; case SB_LINERIGHT: si.nPos += i : break ; ţi case SB_PAGELEFT: si.nPos -= si.nPage ; break ; case SBţPAGERIGHT: si.nPos += si.nPage ; break ; case SB_THUMBPOSITION: si nPos = si.nTrackPos ; break ; default: break ; l // Ustaw położenie. a następnie przywróć je. Na skutek ustawień // Windows może to nie być taka sama wartoœć, jaka została ustawiona. si.fMask = SIF_POS : SetScrollInfo (hwnd, SB HORZ. &si, TRUE) ; GetScrollInfo (hwnd, SB HORZ, &si) ; I 298 Częœć I: Podstawy (cišg dalszy ze strony 297) i // Jeżeli polożenie uleglo zmianie, przesuń zawartoœć okna I if (si.nPos != iHorzPos) ( ScrollWindow (hwnd, cxChar * (iNorzPos - si.nPos), 0, NULL, NULL) ; ) return 0 ; case WM KEYDOWN : switch (wParam) t case VK_HOME : SendMessage (hwnd, WM VSCROLL, SB TOP, 0) ; break ; case VK_END : SendMessage (hwnd, WM VSCROLL, SB BOTTOM, 0) ; break ; case VK_PRIOR : SendMessage (hwnd, WM VSCROLL, SBţPAGEUP, 0) ; break ; case VK_NEXT : SendMessage (hwnd, WM VSCROLL, SBţPAGEDOWN, 0) ; break ; case VK_UP : SendMessage (hwnd, WM VSCROLL, SBţLINEUP, 0) ; break ; case VK_DOWN : SendMessage (hwnd, WM VSCROLL, SBţLINEDOWN, 0) ; break ; case VK_LEFT : SendMessage (hwnd, WM HSCROLL, SBţPAGEUP, 0) ; break ; case VK_RIGHT : SendMessage (hwnd, WM HSCROLL, SBţPAGEDOWN, 0) : break ; J return 0 ; case WM_MOUSEWHEEL: if (iDeltaPerLine = 0) break ; iAccumDelta += (short) HIWORD (wParam) ; // 120 lub -120 while (iAccumDelta >= iDeltaPerLine) f SendMessage (hwnd, WM_VSCROLL, SBţLINEUP, 0) ; iAccumDelta -= iDeltaPerLine ; Rozdział l: Mysz 299 ) while (iAccumDelta <= -iDeltaPerLine) ( SendMessage (hwnd, WM_VSCROLL, SBţLINEDOWN, 0) ; iAccumDelta += iDeltaPerLine ; ) return 0 ; case WM_PAINT : hdc = BeginPaint (hwnd, &ps) ; // Pobierz położenie pionowego paska przewijania si.cbSize = sizeof (si) ; si.fMask = SIF_POS ; GetScrollInfo (hwnd, SBţVERT, &si) ; iVertPos = si.nPos ; // Pobierz położenie poziomego paska przewijania GetScrollInfo (hwnd, SB HORZ. &si) ; iHorzPos = si.nPos ; // Okreœl ograniczenia rysowania iPaintBe9 = max (0, iVertPos + ps.rcPaint.top / cyChar) ; iPaintEnd = min (NUMLINES - 1, iVertPos + ps.rcPaint.bottom / cyChar) ; for (i = iPaintBeg ; i <= iPaintEnd ; i++) ( x = cxChar * (1 - iHorzPos) ; y = cyChar * (i - iVertPos) ; TextOut (hdc, x, y, I sysmetricsCi].szLabel, lstrlen (sysmetricsCiJ.szLabel)) ; ' TextOut (hdc, x + 22 * cxCaps, y, sysmetricsCiJ.szDesc, lstrlen (sysmetricsCi].szDesc)) ; SetTextAlign (hdc, TAţRIGHT ţ TŽ TOP) ; TextOut (hdc, x + 22 * cxCaps + 40 * cxChar, y, szBuffer, wsprintf (szBuffer, TEXT ("%5d"), GetSystemMetrics (sysmetrics[i].iIndex))) ; SetTextAlign (hdc, TAţLEFT ţ TŽ TOP) ; EndPaint (hwnd, &ps) : return 0 ; case WM DESTROY : PostOuitMessage (0) ; 300 Częœć I: Podstawy return 0 ; ) return DefWindowProc (hwnd, message, wParam, lParam) ; Rysunek 7-12. Program SYSMETS Obracanie kółka wymusza na systemie wysłanie komunikatu WM-MOUSEWHEEL do tego okna, które aktualnie ma fokus (a nie do okna, nad którym znajduje się kursor myszy). Jak zwykle, parametr IParam przechowuje informację o położe- niu kursora myszy. Jednak współrzędne te podane sš względem lewego górnego rogu ekranu, a nie obszaru roboczego. Również jak zwykle młodsze słowo wPa- ram przechowuje informację o stanie przycisków myszy oraz klawiszy [Shift] i [Ctrl] . Jednak w starszym słowie parametru wParam pojawiła się nowa informacja. Jest to wartoœć "delta", która obecnie przyjmuje jednš z dwóch wartoœci: 120 lub -120 za- leżnie od tego, czy kółko obracane jest do przodu (to znaczy w kierunku kabla myszy), czy do tyłu. Wartoœci 120 i -120 oznaczajš, że dokument jest przewijany odpowiednio o trzy linie do góry lub do dołu. Być może kolejne wersje będš za- pewniały dokładniejsze stopniowanie, a co za tym idzie wartoœć delta wynoszš- ca (na przykład) 40 i -40 będzie oznaczała przewijanie o jeden wiersz. Aby program był wystarczajšco ogólny, w ramach obsługi komunikatów 4VM-CREATE oraz WM SETTINGCHANGE wywoływana jest funkcja System- Parameterslnfo z parametrem SI GETWHEELSCROLLLINES. Otrzymana w ten sposób wartoœć wskazuje, o ile linii należy przewinšć dokument dla delty wyno- szšcej WHEEL DELTA (wartoœć ta została zdefiniowana w pliku nagłówkowym WINUSER.H). WHEEL DELTA ma wartoœć 120, a funkcja SystemParameterslnfo domyœlnie zwróci 3. Tak więc delta odpowiadajšca przewinięciu dokumentu o jed- nš linię wynosi 40. Wartoœć ta przechowywana jest w iDeltaPerLine. W ramach obsługi komunikatu WM MOUSEWHEEL, program SYSMETS doda- je wartoœć delty do zmiennej statycznej iAccumDelta. Następnie, jeżeli wartoœć iAccumDelta jest większa lub równa iDeltaPerLine (albo mniejsza lub równa -iDel- taPerLine), wygenerowany zostaje komunikat WM VSCROLL z parametrem SB LINEUP lub SB LINEDOWN. Po każdym wysłaniu tego komunikatu, iAc- cumDelta jest zmniejszana (albo zwiększana) o iDeltaPerLine. Co dalej? W zasadzie pozostało nam jeszcze omówienie tworzenia i dostosowywania do swoich potrzeb kursorów myszy. Zajmę się tym, a także wstępem do zasobów Windows w rozdziale 10. Rozdział 8 Ze a r W systemie Microsoft Windows zegar (ang. timer) jest urzšdzeniem wejœciowym, okresowo informujšcym aplikację o upłynięciu zadanego przedziahz czasu. Za- danie programu jest proste: musi poinformować zegar, jaki okres ma odmierzać. W odpowiedzi program będzie otrzymywał komunikaty WM TIMER sygnalizu- jšce zakończenie każdego okresu. Na pierwszy rzut oka może wydawać się, że zegar jest urzšdzeniem wejœciowym o mniejszym znaczeniu, niż klawiatura czy mysz. W większoœci aplikacji tak jest w istocie. Istniejš jednak i takie aplikacje, które bardzo intensywnie korzystajš z zegara. I nie sš to tylko aplikacje zwišzane z wyœwietlaniem aktualnego czasu, jak na przykład zegar wyœwietlany na pasku zadań czy też dwa programy, które napiszemy w tym rozdziale. Poniżej przedstawione zostały inne zegary wyko- rzystywane przez Windows, przy czym czgœć z nich nie jest zbyt typowa. ů Wielozadaniowoœć. Chociaż Windows 98 jest systemem wielozadaniowym z wy- właszczaniem (ang. preemptive multitasking environment), w pewnych sytuacjach program powinien zwrócić sterowanie do systemu po obsłużeniu komunika- tu tak szybko, jak to tylko możliwe. Jeżeli program musi wykonać bardzo zło- żone i długotrwałe obliczenia, może podzielić zadanie na kilka mniejszych eta- pów i wykonywać je kolejno po odebraniu komunikatu WM TIMER. (Na ten temat będę miał znacznie więcej do powiedzenia w rozdziale 20). ů Sterowanie odœwieżaniem raportów. Program może poshxżyć się zegarem do od- œwieżania wyœwietlanych, stale zmieniajšcych się danych, na przykład infor- macji o aktualnym stanie zasobów systemu lub postępach jakiegoœ zadania. ů Implementacja funkcji "automatycznego zapisu". Zegar może być wykorzystany do okresowego wymuszenia zapisu w pliku dyskowym rezultatów pracy użyt- kownika. ů Zakończenie pracy demonstracyjnej wersji programu. Pewne demonstracyjne wersje programów zostały zaprojektowane w ten sposób, aby zakończyć swojš pra- cę, powiedzmy, po 30 minutach od jej rozpoczęcia. W tym wypadku zegar systemowy wykorzystywany jest do wygenerowania impulsu kończšcego działanie. ů Równomierne przemieszczanie się. W wielu grach konieczne jest, aby obiekty gra- ficzne poruszały się z pewnš okreœlonš prędkoœciš. Tymczasem może okazać się, że jest ona zależna od szybkoœci maszyny, na której program został uru- chomiony. Aby zabezpieczyć się przed podobnymi problemami, wykorzysty- wany jest zegar. 302 Częœć I: Podstawy ů Multimedia. Programy odtwarzajšce płyty CD lub pliki dŸwiękowe często mogš działać w tle. Program może poshxżyć się zegarem do okresowego sprawdza- nia, jaka częœć utworu została już odegrana, i wyœwietlania tych informacji na ekranie. Innš metodš myœlenia o zegarze jest traktowanie go jako gwarancji, że kiedyœ w przyszłoœci program ponownie otrzyma sterowanie. Program zwykle nie wie, kiedy nadejdzie komunikat. Podstawowe informacje o zegarze Przydzielenie zegara aplikacji następuje po wywołaniu funkcji SetTimer. Jako jej parametr podaje się liczbę całkowitš bez znaku, której zadaniem jest okreœlenie, jak dhzgi odcinek czasu ma być odmierzany. Może ona przyjmować wartoœci (teo- retycznie) od 1 milisekundy do 4 294 967 295 milisekund (czyli blisko 50 dni). Wartoœć ta okreœla więc częstotliwoœć, z jakš do twego programu nadchodzić będš komunikaty WMţTIMER. Podanie na przykład odcinka czasu równego 1000 mi- lisekund spowoduje, że WM T'IMER będzie wysyłany co 1 sekundę. Gdy program nie potrzebuje już zegara, powinien wywołać funkcję KillTimer. Mo- żesz oczywiœcie zaprogramować zegar, który wyœle tylko jeden impuls: wystar- czy po prostu wywołać KillTimer w ramach obsługi komurukatu WM TIMER. Wywołanie KillTimer powoduje również, że z kolejki usunięte zostanš wszystkie nieobsłużone komunikaty WM TIMER. Dlatego też masz pewnoœć, że po jej wywołaniu aplikacja nie otrzyma już żadnego komunikatu WM TIMER. System i zegar Zegar dostępny w Windows jest stosunkowo prostym rozszerzeniem możliwo- œci, które ma każdy komputer PC dzięki obecnoœci na jego płycie głównej pew- nych układów elektronicznych. W dawnych czasach, zanim na œwiecie pojawił się system Windows, programiœci piszšc swoje programy przeznaczone dla MS- DOS-a mogli zaimplementować zegar, przechwytujšc "przerwanie zegarowe" (ang. timer tick). Generowane było ono co 54,925 milisekundy, czyli około 18,2 razy na sekundę. Działo się tak, ponieważ zegar oryginalnego IBM PC o często- tliwoœci 4,772720 MHz dzielony był przez 2'8. Aplikacje Windows nie przechwytujš przerwań BIOS-u. Za ich przechwytywa- nie odpowiedzialny jest system Windows. Dla każdego z utworzonych zegarów Windows przechowuje pewien licznik, którego wartoœć jest zmniejszana za każ- dym razem, gdy przechwycone zostanie prżerwanie zegarowe. Gdy zawartoœć licznika osišgnie wartoœć 0, generowany jest komunikat WM TIMER, wstawia- ny do kolejki odpowiedniej aplikacji, a następnie przywracana jest poczštkowa wartoœć licznika. Ponieważ aplikacja Windows otrzymuje komunikaty WMţTIMER za poœrednic- twem zwykłej kolejki, nie musisz się martwić, że program zostanie niespodzie- wanie przerwany przez jego pojawienie się. Pod tym względem zegar podobny jest do klawiatury i myszy: sterownik obsługuje asynchroniczne przerwania sprzę- towe, a następnie przekształca je w odpowiednio zbudowane komunikaty. Rozdział 8: Zegar 303 W Windows 98 zegar ma takš samš rozdzielczoœć, wynoszšcš 55 milisekund, co wykorzystywane przez niego przerwanie sprzętowe zegara PC. Natomiast w Win- dows NT rozdzielczoœć zegara wynosi około 10 milisekund. Aplikacje Windows nie mogš otrzymywać komunikatu WMţTIMER częœciej, niż wynika to z rozdzielczoœci zegara. W Windows 98 jest to około 18,2 razy na sekun- dę, natomiast w Windows NT - 100 razy na sekundę. Dlatego też podana przez ciebie wartoœć parametru funkcji SetTimer zaokršglana jest przez system do naj- bliższej całkowitej liczby impulsów zegara. Na przykład, podanie czasu 1000 mili- sekund spowoduje jego podzielenie przez 54,925, w wyniku czego otrzyma się 18,207 impulsów zegara. Wartoœć ta zostanie zaokršglona w dół do 18, co spowoduje, że odmierzany będzie czas 989 milisekund. Jeżeli natomiast podany zostanie okres krótszy niż 55 milisekund, komunikat generowany jest dla każdego impulsu. Komunikaty zegarowe nie sš asynchroniczne Ponieważ zegar działa w oparciu o przerwanie sprzętowe, częœć programistów sšdzi, że ich aplikacje mogš zostać przerwane w dowolnym momencie w celu obsługi komunikatu WM TTMER. Jednakże komunikat WMţTIMER nie jest asynchroniczny. Za każdym razem , gdy się pojawi, zostaje umieszczony w zwykłej kolejce komunikatów i dostarczony do aplikacji razem z innymi. Dlatego też, jeżeli w wywołaniu SetTimer podasz okres wynoszšcy 1000 milisekund, program nie ma gwarancji, że komunikaty pojawiać się będš co 1 sekundę lub nawet (jak wspomniałem wczeœniej) co 989 milisekund. Jeżeli aplikacja będzie zajęta dhxżej niż jednš sekundę, w tym czasie nie otrzyma żadnego komunikatu WM TIMER. Będziesz mógł się o tym przeko- nać, korzystajšc z programów przedstawionych w tym rozdziale. W rzeczy sa- mej, system Windows traktuje komunikat WM-TIMER podobnie jak WM-PAINT. Oba majš niski priorytet i program odbierze je dopiero wtedy, gdy jego kolejka komunikatów nie będzie zawierała żadnych innych. Komunikat WMţTIMER jest podobny do WM-PAINT pod jeszcze innym wzglę- dem. System Windows nie zawraca sobie głowy umieszczaniem w kolejce wielu komunikatów WMţTIMER. Zamiast tego sš one łšczone w jeden. Dlatego też aplikacja nigdy nie odbierze dwóch kolejnych komunikatów WMţTIMER nawet wtedy, jeżeli pojawiš się one w bardzo krótkim okresie czasu. Co gorsza, aplika- cja nie jest w stanie okreœlić, ile komunikatów zostało zgubionych na skutek ta- kiego traktowania. Z tego powodu programy zegarowe nie mogš wyznaczać czasu na podstawie odebranych przez siebie komunikatów WM-TIMER. Komunikaty te mogš jedy- nie informować, że wyœwietlany czas powinien zostać zmieniony. W tym rozdziale napiszemy dwie aplikacje zegarowe, które będš odœwieżane co sekundę i dopie- ro wtedy dowiesz się, w jaki sposób zostało to zrealizowane. Dla jasnoœci, o zegarze będę mówił w taki sposób, jakby "komunikat WM-TIMER był co sekundę". Powinieneœ jednak pamiętać, że komunikat to nie przerwanie, może pojawić się trochę póŸniej. 304 CzęœĆ l. Podstawy Zegar: trzy metody wykorzystania Jeżeli pisany przez ciebie program będzie korzystał z zegara przez cały czas swojej pracy, funkcję SetTimer prawdopodobnie wywołasz w ramach obshzgi komuni- katu WM CREATE, a KillTimer - w ramach WM DESTROY. Zegarem możesz posługiwać się na jeden z trzech sposobów, w zależnoœci od argumentów SetTi- mer. Metoda pierwsza Metoda ta, najłatwiejsza, powoduje, że Windows wysyła komunikat WM TIMER do zwykłej procedury okna aplikacji. W tym przypadku wywołanie SetTimer wy- glšda następujšco: SetTimer (hwnd, 1, uiMsecInterval, NULL) ; Pierwszy parametr to uchwyt okna, którego procedura będzie otrzymywała ko- munikaty WM TIMER. Drugi parametr to identyfikator zegara. Musi on być liczbš różnš od zera. W tym prżykładzie arbitralnie zdecydowałem, że będzie on rów- ny jeden. Trzecim parametrem funkcji jest 32 bitowa liczba całkowita bez znaku, która okreœla w milisekundach długoœć odmierzanego odcinka czasu. Jeżeli po- dasz 60 000, komunikat WMţTIMER będzie wysyłany do aplikacji co minutę. W dowolnej chwili, nawet w trakcie przetwarzania komunikatu WM T'IMER, możesz zakończyć wysyłanie komunikatów. Wystarczy wywołać funkcję KillTimer (hwnd, 1) ; Drugi argument to ten sam identyfikator zegara, który podany został w wywo- łaniu funkcji SetTimer. Powszechnie uważa się, że dobrym zwyczajem jest kaso- wanie wszystkich uruchomionych zegarów w ramach obsługi komunikatu WM DESTROY. Gdy procedura okna odbierze komunikat WM TIMER, towarzyszšcy mu para- metr wParam przechowuje identyfikator zegara (w naszym przypadku będzie to 1), a lParam - 0. Jeżeli musisz wykorzystać więcej niż jeden zegar, nadaj każdemu z nich odrębny identyfikator. Dzięki temu wartoœć wParam pozwoli ci rozróżnić komumikaty WM-TiMER nadchodzšce do procedury okna. Jeżeli chcesz, aby twój program był łatwiejszy w "czytaniu", definiujšc poszczególne identyfikatory, mo- żesz poshzżyć się wyrażeniem #define: lldefine TIMER SEC 1 lldefine TIMER MIN 2 Oba zegary możesz teraz ustawić w następujšcy sposób: SetTimer (hwnd, TIMER SEC, 1000, NULL) ; SetTimer (hwnd, TIMER MIN, 60000, NULL) ; Natomiast obsługa WM TIMER w funkcji okna będzie wyglšdała jakoœ tak: case WM_TIMER: switch (wParam) f case TIMERţSEC: Cprzetwarzanie raz na sekundę) break ; Rflzdzisł 8: Zeţr ţ 305 case TIMER_MIN: [przetwarzanie raz na minutęJ break ; ) return 0 ; Jeżeli chcesz, aby istniejšcy zegar zaczšł odmierzać inne odcinki czasu, możesz po prostu jeszcze raz wywołać SetTimer i podać innš wartoœć czasu. Możesz to na przykład wykorzystać w programie zegara, który dysponuje opcjš, pozwalajšca na ukrywanie bšdŸ wyœwietlanie sekund. Wystarczy przełšczać okres pomiędzy 1000 a 60 000 milisekund. Na rysunku 8-1 przedstawiony został program posługujšcy się zegarem. Program ten, noszšcy nazwę BEEPERl, tak ustawia zegar, aby odmierzał jednosekundowe odcinki czasu. Po odebraniu komunikatu WMţTIllţfER zmienia on kolor obszaru ro- boczego na niebieski lub czerwony, a następnie, za pomocš funkcji MessageBeep, ge- neruje sygnał dŸwiękowy. (Chociaż MessageBeep często jest wykorzystywana łšcznie z MessageBox, tak naprawdę jest funkcjš, która może być stosowana w dowolnej sy- tuacji. Jeżeli komputer wyposażony jest w kartę dŸwiękowš, do funkcji możesz prze- kazać jednš z wartoœci MBţICON, które zwykle przekazywane sš do MessageBox. Dzięki temu mogš być generowane różne sygnały dŸwiękowe ustawiane przez użyt- kownika za pomocš apletu DŸwięki znajdujšcego się w Panelu sterowania). BEEPERI ustawia zegar w procedurze okna w trakcie obsługi komunikatu WM CREATE. W ramach obsługi WMţTIMER, program wywołuje MessageBeep, zmienia na przeciwnš wartoœć Bagi ţFlipFlop, a następnie unieważnia cały ob- szar roboczy, dzięki czemu wygenerowany zostaje komunikat WMţPAINT. Z kolei w trakcie jego obsługi BEEPER1 za pomocš funkcji GetClientRect pobiera struktu- rę RECT przechowujšcš rozmiar okna. Na koniec dzięki wywołaniu FillRect, ob- szar roboczy zostaje wypełniony odpowiednim kolorem. 8E^P^RI.C /* BEEPERl.C - Program demonstrujšcy zegar, wersja 1 (c) Charles Petzold, 1998 */ ipinclude iţdefine ID TIMER 1 LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) static TCHAR szAppNameC] = TEXT ("Beeperl") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CS VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; 306 Częœć I: Podstawy (cišg dalszy ze strony 305) wndclass.cbWndExtra = 0 : wndclass.hInstance = hInstance : wndclass.hIcon = LoadIcon (NULL, IDI ŽPPLICATION) : wndclass:hCursor = LoadCrsor (NULL, IDC ARROW) : wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE BRUSH) : wndclass.lpszMenuName = NULL : wndclass.lpszClassName = szAppName : if (!RegisterClass (&wndclass)) ( MessageBox (NULL, TEXT ("Program requires Windows NT!"), szAppName, MBţICONERROR) : return 0 : ) hwnd = CreateWindow (szAppName, TEXT ("Beeperl Timer Demo"). WS OVERLAPPEDWINDOW, CW USEDEFAULT, CW USEDEFAULT, CW_USEDEFAULT, CW USEDEFAULT, NULL, NULL, hInstance, NULL) : ShowWindow (hwnd, iCmdShow) : UpdateWindow (hwnd) : while (GetMessage (&msg, NULL, 0, 0)) ( TranslateMessage (&msg) : DispatchMessage (&msg) : ) return msg.wParam : 1 LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ! static BOOL fFlipFlop = FALSE ; HBRUSH hBrush : HDC hdc : PAINTSTRUCT ps : RECT rc : , switch (message) ( case WM CREATE: SetTimer (hwnd, ID TIMER, 1000, NULL) : return 0 : case WM TIMER : MessageBeep (-1) : fFlipFlop = !fFlipFlop InvalidateRect (hwnd, NULL, FALSE) : return 0 : [ case WMţPAINT : hdc = BeginPaint (hwnd, &ps) : GetClientRect (hwnd, &rc) : Rozdział 8: Zegar 307 hBrush = CreateSolidBrush (fFlipFlop ? RGB(255,0,0) : RGB(0,0,255)) ; FillRect (hdc, &rc, hBrush) ; EndPaint (hwnd, &ps) Delete0bject (hBrush) return 0 ; case WM_DESTROY : KillTimer (hwnd, ID_TIMER) ; PostOuitMessage (0) ; return 0 ; return DefWindowProc (hwnd, message, wParam. lParam) ; Rysunek 8-1. Program BEEPER1 Ponieważ BEEPERI głoœno obwieszcza odebranie każdego komunikatu WMţTI- MER, ciekawym doœwiadczeniem może okazać się wykonanie w trakcie jego pracy jakiejœ innej czynnoœci. Z pewnoœciš zorientujesz się, że komunikaty nie sš odbie- rane zbyt regularnie. Oto pewien odkrywczy ekspeţyment, który możesz przeprowadzić: najpierw uruchom aplet Ekran znajdujšcy się w Panelu sterowania i kliknij kartę Efekty. Upewnij się, że pole wyboru Pokazuj zawartoœć okna podczas przecišgania nie jest zaznaczone. Spróbuj teraz zmieruć wielkoœć okna programu BEEPERl. Spo- woduje to, że program wejdzie w modalnš pętlę komurukatów (ang. modal mes- sage loop). Polega ona na tym, że system Windows "przechwytuje" wszystkie komunikaty przez swojš wewnętrznš pętlę i uniemożliwia przerwanie zmiany wielkoœci okna lub jego przemieszczania przez jakikolwiek komunikat. Większoœć z nich jest po prostu usuwana z kolejki. Dlatego też BEEPER1 w trakcie tej opera- cji przestaje generować sygnały dŸwiękowe. Po zakończeniu przesuwania zauwa- żysz, że BEEPER1 nie otrzymał tych wszystkich komunikatów, które nadeszły w trakcie operacji. Jeżeli z kolei pole Pokazuj zawartoœć okna podczas przecišgania jest zaznaczone, modalna pętla komunikatów Windows usiłuje przekazać do procedury okna przy- najmniej częœć komunikatów, które w przeciwnym razie zostałyby utracone. Cza- sem działa to poprawnie, a czasem nie. Metoda druga Pierwsza metoda ustawiania zegara powoduje, że komunikat WM TIMER wy- syłany jest do procedury okna. Korzystajšc natomiast z drugiej metody, możesz sprawić, że system Windows wyœle go do wybranej innej funkcji twego progra- mu. Funkcja, która odbiera komunikaty zegara, okreœlana jest jako call-back. Jest ona po prostu w twoim programie wywoływana przez Windows. Wystarczy, abyœ podał systemowi jej adres. Brzmi to znajomo: przecież funkcja okna jest rodza- jem funkcji call-back! Jej adres podajesz systemowi w trakcie rejestracji klasy okna, dlatego jest póŸniej wywoływana za każdym razem, gdy powinna obshzżyć jakiœ komunikat. 308 Częœć 1: Po„stawy SetTimer nie jest jedynš funkcjš Windows, która korzysta z tego sposobu wywo- ływania. Z podobnej metody korzystajš również funkcje CreateDialog oraz Dia- logBox (przedstawię je w rozdziale 11) do przetwarzania komunikatów dociera- jšcych do okna dialogowego. Wiele funkcji Windows (EnumChildWindow, Enum- Fonts, EnumObjects, EnumProps oraz EnumWindow) przekazuje pobierane przez siebie informacje do funkcji call-back. W ten sam sposób działa również kilka in- nych mniej znanych funkcji: GrayString, LineDDA oraz WindowHookEx. Podobnie jak funkcja okna, również funkcja call-back musi zostać zadeklarowa- ! na jako CALLBACK, ponieważ jest wywoływana przez Windows spoza przestrze- ru kodu programu. Parametry funkcji oraz zwracana przez niš wartoœć zależš od jej przeznaczenia. W przypadku funkcji wywoływanej przez zegar, parametry sš ! takie same jak funkcji okna, chociaż zdefiniowane w nieco inny sposób. Nie po- winna ona również zwracać żadnej wartoœci. Nazwijmy naszš funkcję TimerProc (możesz wybrać dowolnš innš nazwę, która nie koliduje z już istniejšcš).Będzie ona obsługiwała wyłšcznie komunikaty WM_TIMER: VOID CALLBACK TimerProc (HWND hwnd, UINT message, UINT iTimerID, DWORD dwTime) Cobsiuga komunikatu WM TIMER7 Parametr hwnd ma takš samš wartoœć jak hwnd przekazywane do SetTimer. Po- nieważ system Windows będzie przekazywał do TimerProc jedynie komunikaty WM TIMER, parametr message zawsze będzie miał wartoœć WM TIMER. iTimer- ID jest identyfikatorem zegara, natomiast dwTime jest wartoœciš kompatybilnš z wartoœciš zwracanš przez funkcję GetTickCount. Jest to liczba milisekund, które upłynęły od czasu uruchomienia systemu. Jak widzieliœmy, pierwsza metoda uruchamiania zegara wyglšda następujšco: SetTimer (hwnd, iTimerID, iMsecInterval, NULL) ; Jeżeli do obsługi komunikatów WM TTMER posługujesz się funkcjš call-back, czwartym argumentem SetTimer jest jej adres: SetTimer (hwnd, iTimerID, iMsecInterval, TimerProc) ; Przyjrzyjmy się teraz przykładowi, abyœ mógł się przekonać, jak to wszystko wyglšda razem. Przedstawiony na rysunku 8-2 program BEEPER2 pod wzglę- dem funkcjonalnym niczym nie różni się od programu BEEPERl. Jedynie kornu- nikat WMţTIMER trafia do TimerProc, a nie do WndProc. Zwróć uwagę, że Timer- Proc jest zadeklarowana na samym poczštku programu, razem z WndProc. BEEPERI.C /* BEEPERl.C - Pro9ram demonstrujdcy zegar, wersja 2 (c) Charles Petzold, 1998 */ ilinclude ildefine ID TIMER Rozdziai 8: Zegar 309 i LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; VOID CALLBACK TimerProc (HWND, UINT, UINT, DWORD ) ; I int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) ( static TCHAR szAppNameC] = ("Beeper2") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CS UREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; i. if (!RegisterClass (&wndclass)) ( MessageBox (NULL, TEXT ("Program requires Windows NT!"), szAppName, MBţICONERROR) ; return 0 ; hwnd = CreateWindow (szAppName, TEXT ("Beeper2 Timer Demo"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CWţUSEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULţL, 0, 0)) ( TranslateMessage (&ms9) ; DispatchMessage (&msg) ; ;.; ) return msg.wParam ; ) LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ( ţ, switch (message) f case WM_CREATE: SetTimer (hwnd, ID TIMER, 1000, TimerProc) ; return 0 ; case WM_DESTROY: KillTimer (hwnd, ID_TIMER) ; PostOuitMessage (0) ; return 0 ; ) 310 Częœć I: Podstawy (cišg dalszy ze strony 309) return DefWindowProc (hwnd, message, wParam, lParam) ; VOID CALLBACK TimerProc (HWND hwnd, UINT message, UINT iTimerID, DWORD dwTime) ( static BOOL fFlipFlop = FALSE ; HBRUSH hBrush ; HDC hdc : RECT rc ; MessageBeep (-1) : fFlipFlop = !fFlipFlop ; GetClientRect (hwnd, &rc) ; hdc = GetDC (hwnd) : hBrush = CreateSolidBrush (fFlipFlop ? RGB(255,0,0) : RGB(0,0,255)) : FillRect (h-dc, &rc, hBrush) : ReleaseDC (hwnd, hdc) ; Delete0bject (hBrush) : Rysunek -2. Program BEEPER2 Metoda trzecia Trzecia metoda ustawiania zegara podobna jest do drugiej, jednak w tym przy- padku parametr hwnd przekazywany do funkcji SetTimer ma wartoœć NULL, a dru- gi parametr (identyfikator zegara) jest ignorowany. Powoduje to, że funkcja zwraca identyfikator zegara. iTimerId = SetTimer (NULL, 0, wMsecInterval, TimerProc) : Zwracana przez funkcję SetTimer wartoœć iTimerId będzie równa 0 tylko w tych nielicznych przypadkach, gdy zegar nie jest dostępny. Pierwszym parametrem funkcji KilITimer (uchwyt okna) również powinien być NULL. Musisz wykorzystać zwrócony przez SetTimer identyfikator zegara: KillTimer (NULL, iTimerID) ; Przekazywany do funkcji TimerProc parametr hwnd również będzie miał wartoœć NULL. Ten sposób ustawiania zegara nie jest zbyt często stosowany. Możesz po- służyć się nim, gdy w swoim programie musisz uruchomić wiele zegarów odmie- rzajšcych różne czasy, a nie chcesz œledzić przydzielanych im identyfikatorów. Teraz, skoro już wiesz, w jaki sposób możesz ustawiać zegar, nadeszła pora, aby napisać kilka użytecznych aplikacji. Wykorzystanie zegara Najczęœciej zegar wykorzystywany jest w aplikacjach wyœwietlajšcych aktualny czas. Napiszemy dwa takie programy; w oknie pierwszego programu czas bę- dzie przedstawiany cyfrowo, w oknie drugiego natomiast - analogowo. Rozdział 8: Zegar 311 i Zegar cyfrowy Program DIGCLOCK przedstawiony na rysunku 8-3 wyœwietla aktualnš godzi- ; nę, symulujšc wyœwietlacze siedmiosegmentowe. DIGCLOCK. c i /* i DIGCLOCK.c - Zegar cyfrowy (c) Charles Petzold, 1998 */ 1 ; ţţinclude j 4ţdefine ID TIMER 1 LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; 1 i int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) ( I, i:. . a static TCHAR szAppNameC] = TEXT ("DigClock") ; HWND hwnd ; MSG msg ; ,.,, i:. WNDCLASS wndclass ; ,,., wndclass.style = CS-HREDRAW ţ CS VREDRAW ; ' i wndclass.lpfnWndProc = WndProc ; ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC ARROW) ; i wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE-BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) MessageBox (NULL, TEXT ("Program requires Windows NT!"), szAppName, MB-ICONERROR) ; ) hwnd = CreateWindow (szAppName, TEXT ("Digital Clock"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW USEDEFAULT, CW_USEDEFAULT, CW USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; ,''. UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) TranslateMessage (&msg) ; DispatchMessage (&msg) ; 312 Czţœţ t: P„dţawy (cišg datszy ze strony 322) 1 return msg.wParam ; 1 void DisplayDigit (HDC hdc, int iNumber) ( static 800L fSevenSegment [107[77 = ( 1, I, 1, 0, 1. 1, 1, // 0 0, 0, 1, 0, 0, 1, 0, // 1 1, 0, 1, 1. 1, 0. 1. /l 2 1, 0, I, 1, 0, 1, 1, // 3 0, 1, 1. 1, 0, l. 0, // 4 1, 1. 0, 1, 0. 1, 1, l/ 5 1, 1, 0, 1, 1, 1, 1, // 6 1, 0, 1, 0, 0, 1, 0. /! 7 1, l, 1, 1, 1. 1, 1, // 8 1, 1, 1, i, 0, 1, i ) ; /l 9 static POINT ptSegment C77C67 = ( 7, 6, 11, 2, 31, 2, 35, 6, 31. 10, il. 10, 6, 7, 10, 11, 10. 31, 6, 35. 2. 31, 2, 11, 36, 7, 40, il, 40, 31, 36, 35, 32. 31, 32, il, 7. 36, 11, 32, 31. 32,. 35. 36, 31, 40, 11, 40, 6. 37, 10, 41, 10, 61, 6, 65, 2, 61. 2, 4I, 36, 37, 40, 41, 40, 61, 36, 65, 32, 61, 32, 41, 7, 66. 11, 62, 31, 62, 35, 66, 31. 70. 11, 70 ) ; int iSeg ; for (iSeg = 0 ; iSeg < 7 ;. iSeg++) if (fSevenSegment CiNumber7CiSeg]) Polygon (hdc, ptSegment [iSeg7, 6) ; ) void DisplayTwoDigits (HDC hdc, int iNumber, BOOL fSuppress) ( if (!fSuppress ţţ (iNumber / 10 != 0)) Displa.y0igit (hdc, iNumber / 10) ; OffsetWindowOrgEx (hdc, -42, 0. NULL) ; DisplayDigit (hdc, iNumber % 10) ; OffsetWindowOrgEx (hdc, -42, 0, NULL) ; ) void DisplayColon (HDC hdc) ( POINT ptColon [2.][47 = ( 2. 21, 6, 17, 10, 21, 6,. 25, 2, 51, 6, 47, 10, 51, 6, 55 1 ; Polygon (hdc. ptColon [07, 4) ; Polygon (hdc, ptColon [17, 4) ; ) OffsetWindowOrgEx (hdc, -12, 0, NULL) ; void DisplayTime (HDC hdc, BOOL f24Hour, BOOL fSuppress) ( SYSTEMTIME st ; Rozdziar 8: Zegar 3t3 GetLocalTime (&st) ; if (f24Hour) DisplayTwoDigits (hdc, st.wHour, fSuppress) ; else DisplayTwoDigits (hdc, (st.wHour %= 12) ? st.wHour : 12, fSuppress) ; DisplayColon (hdc) ; DisplayTwoOigits (hdc, st.wMinute, FALSE) ; DisplayColon (hdc) DisplayTwoDigits (hdc, st.wSecond, FALS.E) ; l LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) I':'. static BOOL f24Hour, fSuppress : i static HBRUSH hBrushRed ; static int cxClient, cyClient ; HDC hdc ; PAINTSTRUCT ps ; TCHAR szBuffer [2] ; switch (message) case WM_CREATE: hBrushRed = CreateSolidBrush (RGB (255, 0, 0)) ; SetTimer fhwnd, ID TIMER, 1000, NULL) ; i // kontynuacja case WM_SETTINGCHANGE: GetLocaleInfo (LOCALE_USER_DEFAULT, LOCALEţITIME, szBuffer, 2) ; f24Hour = (szBuffer[0] = '1') ; GetLocaleInfo (LOCALE_USER DEFAULT, LOCALEţITLZERO, szBuffer, 2) ; fSu.ppress = (szBuffer[0] = '0') : InvalidateRect (hwnd, NULL, TRUE) ; return 0 ; case WM_SIZE: cxClient = LOWORD (lParam) ; I,'. cyClient = HIWORD (lParam) ; retu:rn 0 : case WM TIMER: InvalidateRect (hwnd, NULL, TRUE) ; return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) : SetMapMode (hdc. MMţISOTROPIC) ; SetWindowExtEx (hdc, 216, 72, NULL) ; SetViewportExtEx (hdc, cxClient, cyClient, NULL) ; SetWindowOrgEx (hdc, 138, 36, NULL) ; SetViewportOrgEx (hdc, cxClient / 2, cyClient / 2, NULL) ; 314 Częţć I: (cišg dalszy ze strony 313) SelectObject (hdc, GetStockObject (NULLţPEN)) ; SelectObject (hdc, hBrushRed) ; DisplayTime (hdc, f24Hour, fSuppress) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: KillTimer (hwnd, ID_TIMER) ; Delete0bject (hBrushRed) ; PostOuitMessage (0) ; ' return 0 ; l return DefWindowProc (hwnd, message, wParam, lParam) ; Rysunek 8-3. Program DIGCLOCK Okno programu DIGCLOCK przedstawione zostało na rysunku 8-4. e nr^ aAs a Rysunek 8-4. Okno programu DIGCLOCK Chociaż na rysunku tego nie widać, poszczególne cyfry sš czerwone. W ramach obsługi komunikatu WM-CREATE procedura okna programu tworzy czerwony pędzel, natomiast usuwa go - w ramach obsługi WM DESTROY. Również w WM CREATE ustawiany jest zegar generujšcy komunikaty WM TIIVIER co jed- nš sekundę. Jak łatwo się domyœlić, jest on usuwany w WM DESTROY. (Wywo- łaniem funkcji GetLocallnfo zajmę się wkrótce). Po odebraniu komunikatu WM TIMER, procedura okna programu DIGCLOCK unieważnia cały obszar okna za pomocš funkcji InvalidateRectangle. Nie jest to naj- lepsze z możliwych rozwišzań, ponieważ oznacza ono, że co sekundę całe okno będzie odœwieżane, co w pewnych okolicznoœciach może doprowadzić do migo- tania obrazu. Zdecydowanie najlepszym rozwišzaniem byłoby unieważnienie tylko tych fragmentów obszaru roboczego okna, które wymagajš zmiany ze wzglę- du na upływajšcy czas. Jednakże algorytm, który należałoby zaimplementować, jest doœć złożony. Unieważnianie okna w ramach obsługi komurukatu 4VMţTIMER powoduje, że cała jego logika umieszezona jest w obshxdze WMţPAINT. Obsługę tego komu- Rozdział 8: Zegar 315 mikatu program DIGCLOCK rozpoczyna od ustawienia trybu odwzorowania (ang. mapping mode) na MM ISOTROPIC. Dlatego też może on używać arbitralnie wy- branej skali osi, takiej samej zarówno w pionie, jak i w poziomie. Osie te (usta- wione za pomocš funkcji SetWindowExtEx) podzielone sš w poziomie na 276 jed- nostek, a w pionie - na 72. Oczywiœcie, osie te wyglšdajš całkiem dowolnie, jed- nak ich punktem odniesienia sš odstępy, jakie powinny być zachowane pomię- dzy poszczególnymi cyframi zegara. Jako punkt poczštkowy okna (138, 36) wybrany został jego œrodek. Z kolei punkt poczštkowy widoku (ang. viewport) to (cxClient/2, cyClient/2). Oznacza to, że ze- gar wyœwietlany będzie w centrum obszaru roboczego programu DIGCLOCK, jednak sam program może posługiwać się osiami, których punkt poczštkowy (0, 0) znajduje się w lewym górnym rogu ekranu. Kolejnš czynnoœciš wykonywanš w ramach obshzgi komunikatu WM PAINT jest przypisanie aktualnemu pędzlowi tego, który został stworzony wczeœniej (czer- wony). Natomiast jako pióro wybierane jest NULL PEN. Kolejnym krokiem jest wywołanie funkcji DispIayTime. Pobieranie aktualnego czasu Funkcja DispIayTime rozpoczyna się od wywołania funkcji Windows GetLocaITi- me. Jedynym jej parametrem jest struktura SYSTEMTIME zdefiniowana w pliku nagłówkowym WINBASE.H w następujšcy sposób: typedef struct ţSYSTEMTIME f WORD wYear; WORD wMonth; WORD wDayOfWeek; WORD wDay; WORD wHour; WORD wMinute; WORD wSecond; WORD wMilliseconds; ) SYSTEMTIME, *PSYSTEMTIME ; Jak można było się spodziewać, struktura SYSTEMTIME zawiera zarówno pola służšce do przechowywania czasu, jak i pola przechowujšce datę. Miesišce nu- merowane sš od 1 (styczeń ma numer 1), natomiast dni tygodnia - od 0 (niedzie- la to 0). Pole wDay przechowuje informację o aktualnym dniu miesišca, który rów- nież numerowany jest od l. Struktura SYSTEMTIME wykorzystywana jest głównie przez funkcje GetLocalTi- me oraz GetSystemTime. Funkcja GetSystemTime zwraca uniwersalny czas skoor- dynowany (ang. Universal Coordinated Time, UTC), który z grubsza odpowiada œredniemu czasowi Greenwich. Z kolei funkcja GetLocalTime zwraca czas lokalny wyznaczony na podstawie strefy czasowej komputera. Dokładnoœć obu funkcji zależy wyłšcznie od starannoœci użytkownika, który powinien zadbać o poprawne ustawienie czasu oraz zdefiniowanie strefy czasowej. Aktualnš strefę czasowš możesz sprawdzić, dwukrotnie klikajšc zegar znajdujšcy się na pasku zadań. Pro- 316 Częœe J: Podstawy gram umożliwiajšcy ustawienie dokładnego czasu na podstawie informacji œciš- gniętych przez Internet przedstawiony został w rozdziale 23. System Windows udostępnia również funkcje SetLocalTime oraz SetSystemTime. Te i inne podobne funkcje opisane zostały w /Piatform SDK/Windows Base Servi- ces/General Library/Time. Wyœwietlanie cyfr i dwukropków Program DIGCLOCK mógłby być znacznie prostszy, gdybyœmy wykorzystali czcionkę symulujšcš wyœwietlacz siedmiosegmentowy. Niestety, program wszyst- ko musi zrobić sam, posługujšc się funkcjš Polygon. Funkcja DisplayDigit programu DIGCLOCK definiuje dwie tablice. Pierwsza z nich, fSevenSegment, zawiera siedem wartoœci typu BOOL dla każdej z 10 cyfr od 0 do 9. Wartoœci te okreœlajš, które segmenty wyœwietlacza sš zapalone (wartoœć 1), a które zgaszone (wartoœć 0). Każdy z siedmiu segmentów wyœwietlany jest jako szeœcio- kšt. Druga tablica programu, ptSegment, przechowuje struktury POINT okreœla- jšce współrzędne graficzne każdego z siedmiu segmentów. Dlatego też każda cyfra rysowana jest w następujšcy sposób: for (i5eg = 0 ; iSeg < 7 ; iSeg++) if (fSevenSegment [iNumber][iSeg]) Polygon (hdc, ptSegment [iSeg], 6) ; Podobnie (chociaż znacznie proœciej) funkcja DisplayColon wyœwietla znak dwu- kropka oddzielajšcy minuty i sekundy. Poszczególne cyfry majš szerokoœć 42 jed- nostek, natomiast dwukropek - 6. Dlatego też 6 cyfr i 2 dwukropki zajmujš ra- zem 276 jednostek. Jest to wartoœć, która została podana w wywołaniu funkcji SetWindowExtEx. W momencie wejœcia do funkcji DisplayTime punkt poczštkowy znajduje się w le- wym górnym narożniku miejsca przeznaczonego na lewš skrajnš cyfrę. Display- Time wywohţje funkcję DispIayTwoDigits, która z kolei dwa razy wywohzje Di- splayDigit, przy czym za każdym razem wywołana zostaje również funkcja Offset- WindowOrgEx przesuwajšca poczštek układu współrzędnych o 42 jednostki w pra- wo. Podobnie, funkcja DisplayColon przesuwa poczštek układu o 12 jednostek w prawo. Dzięki temu DisplayTime, wyœwietlajšc każdš cyfrę, może zawsze posłu- giwać się tymi samymi współrzędnymi, niezależnie od tego, gdzie obiekt ma się pojawić w oknie. , Inne sztuczki w kodzie polegajš na uwzględnieniu wyœwietlania czasu w forma- cie 12- i 24-godzinnym oraz na pomijaniu pierwszej cyfry, jeżeli jest ona zerem. Byó œwiatowcem Chociaż wyœwietlanie czasu w sposób proponowany w DIGCLOCK wydaje się odporne na błędy, w bardziej zaawansowanych programach prezentujšcych datę lub czas powixţieneœ polegać na ustawieniach międzynarodowych systemu Win- dows. Najprostszym sposobem sformatowania daty i czasu jest skorzystanie z funk- cji GetDateFormat oraz GetTimeFormat. Zostały one opisane w rnlatform SDL/Win- dows Base Services/General Library/String Manipulation Reference/String Manipulation Functions. Jako parametr wejœciowy pobierajš one strukturę SYSTEMTlME, a na- Rozdział á: Zegar 317 stępnie formatujš odpowiednio datę i czas na podstawie ustawień dokonanych za Y,' pomocš apletu Ustawienia regionalne znajdujšcego się w Panelu sterowania. Program DIGCLOCK nie może posłużyć się funkcjš GetDateFormat, ponieważ wie jedynie, w jaki sposób należy wyœwietlać cyfry oraz dwukropki. Mimo wszystko jednak powinien respektować ustawienia użytkownika dotyczšce wyœwie ania czasu w formacie 12- lub 24-godzinnym, jak również ukrywanie (lub nie) pierw- szej cyfry, jeżeli jest ona zerem. Wszelkie niezbędne po temu informacje możesz otrzymać, wywołujšc funkcję GetLocalelnfo. Mimo że została ona udokumentowa- na w /Platform SDK/Windows Base Seroices/General Library/String Manipulation/String Manipulation Reference/String Manipulation Functions, identyfikatory, z których bę- dziesz korzystał, przedstawione zostały w /Platform SDK/Windows Base Services/In- ternational Features/National Language Support/Nationale Language Support Constants. Poczštkowo program DIGCLOCK wywołuje dwukrotnie funkcję GetLocalelnfo w ramach obsługi komunikatu WMţCREATE. Za pierwszym razem z identyfikato- rem LOCALE TIME (aby okreœlić, czy ma być wykorzystany format 12- czy też 24-godzinny). Po raz drugi natomiast z identyfikatorem LOCALE ITLZERO (co pozwala na sprawdzenie, czy majš być wyœwietlane wiodšce zera). Funkcja Ge- tLocaleInfo zwraca wszelkie informacje w postaci napisów, jednak jeżeli to koniecz- ne z łatwoœciš można je przekonwertować na liczby całkowite. Otrzymane za po- mocš tej funkcji wartoœci program DIGCLOCK przechowuje w zmiennych sta- tycznych, które sš następnie przekazywane do funkcji DispIayTime. Jeżeli użytkownik zmodyfikuje jakikolwiek parametr systemu, rozesłany zostaje do wszystkich aplikacji komunikat WMţSETTINGCHANGE. W ramach jego ob- sługi program DIGCLOCK ponownie wywołuje GetLocalelnfo. Dzięki temu mo- żesz eksperymentować z różnymi ustawieniami za pomocš apletu Ustawienia regionalne znajdujšcego się w Panelu sterowania. Teoretycznie prograrn DIGCLOCK powinien również wywoływać funkcję GetLo- calelnfo z parametrem LOCALE STIME. Spowoduje to, że funkcja zwróci zdefi- niowane przez użytkownika separatory godzin, minut, sekund oraz dziesištych częœci sekundy. Ponieważ jednak DIGCLOCK został napisany w ten sposób, aby wyœwietlać jedynie dwukropki, zostanš one wykorzystane w charakterze sepa- ratorów, nawet jeżeli użytkownik zdecyduje się na coœ innego. Chcšc okreœlić, czy czas dotyczy przedpołudnia czy też wieczora, aplikacja może wywołać GetLoca- leInfo z identyfikatorem LOCALE 51159 lub LOCALE 52359. Zwrócone zostanš napisy odpowiednie dla kraju i języka użytkownika. Moglibyœmy również wyposażyć program DIGCLOCK w obsługę komunikatu WMţTIMECHANGE. Jest on wysyłany do aplikacji, aby jš poinformować o zmia- nie daty systemowej lub czasu. Nie jest to jednak konieczne, ponieważ program jest uaktualniany co sekundę na skutek odebrania komunikatu WM TIMER. Prze- twarzanie WM TTMECHANGE miałoby większy sens w przypadku zegara, któ- ry byłby aktualizowany co minutę. Zegar analogowy Zegar analogowy nie musi troszczyć się o jakiekolwiek ustawienia międzynaro- dowe, jednak złożonoœć jego gratiki z powodzenţem rekompensuje te niewielkie 318 Częœć I: Podstawy ; oszczędnoœci. Jeżeli chcesz sobie z nim poradzić, musisz orientować się w trygo- nometr. Na rysunku 8-5 przedstawiony został program CLOCK. I CLOCK.C /* CLOCK.C - Zegar analogowy (c) Charles Petzold, 1998 */ ţţinclude ţţinclude ţţdefine ID_TIMER 1 ţţdefine TWOPI (2 * 3.14159) LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) static TCHAR szAppName[ţ = TEXT ("Clock") ; HWND hwnd; MSG msg; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CS UREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = NULL ; wndclass.hCursor = LoadCursor (NULL, IDC ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE-BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) ( MessageBox (NULL, TEXT ("Program requires Windows NT!"), szAppName, MBţICONERROR) ; return 0 ; J hwnd = CreateWindow (szAppName, TEXT ("Analog Clock"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW USEDEFAULT, CW_USEDEFAULT, CW USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) TranslateMessage (&msg) ; DispatchMessage (&msg) ; } Rozdział 8: Zegar 319 return msg.wParam ; void SetIsotropic (HDC hdc, int cxClient, int cyClient) ( SetMapMode (hdc, MMţISOTROPIC) ; SetWindowExtEx (hdc, 1000, 1000, NULL) ; SetViewportExtEx (hdc, cxClient / 2, -cyClient / 2, NULL) ; SetViewportOrgEx (hdc, cxClient / 2, cyClient / 2, NULL) ; 1 void RotatePoint (POINT ptC7, int iNum, int iAngle) ( int i ; POINT ptTemp ; for (i = 0 ; i < iNum ; i++) l ptTemp.x = (int) (ptCi].x * cos (TWOPI * iAngle / 360) + ptCi7.y * sin (TWOPI * iAngle / 360)) ; ptTemp.y = (int) (pt[i7.y * cos (TWOPI * iAngle / 360) - ptCi].x * sin (TWOPI * iAngle / 360)) ; ptCi7 = ptTemp ; void DrawClock (HDC hdc) int iAngle ; POINT ptC37 ; for (iAngle = 0 ; iAngle < 360 ; iAngle += 6) i ţ;, ptC07.x = 0 ; ptC07.y = 900 : RotatePoint (pt, l, iAngle) ; ptC27.x = ptC27.y = iAngle % 5 ? 33 : 100 ; ptC07.x -= ptC27.x / 2 ; ptC07.y -= ptC2].y / 2 ; ptCll.x = ptCOl.x + ptC27.x : ptCl7.y = ptC07.y + ptC27.y : SelectObject (hdc, 6etStockObject (BLACK BRUSH)) ; Ellipse (hdc, ptC07.x, ptC07.y, ptCl7.x, ptCl7.y) : void DrawHands (HDC hdc, SYSTEMTIME * pst, BOOL fChange) ( static POINT ptC37C5] = i 0, -150, 100, 0, 0, 600, -100, 0, 0, -150, 0, -200, 50, 0, 0, 800. -50, 0, 0, -200, 0, 0, 0, 0. 0, 0, 0, 0, 0, 800 ) ; 320 Czţœć t: Podstawy (cišg dalszy ze strony 3l9) int i, iAngle[3J ; POINT ptTemp[3J[5J ; iAngl.e[OJ = (pst->wHour.* 30) % 360 + pst->wMinute / 2 ; iAngle[1] = pst->wMinute * 6 ; iAngle[2] = pst->wSecond. * 6 ; memcpy (ptTemp, pt, sizeof (pt)) : for (i = fChange ? 0 : 2 ; i < 3 ; i++) f RotatePoint (ptTemp[iJ, 5, iAngle[iJ) ; Polyline (hdc, ptTemp[iJ, 5) ; ) LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ( static int cxClient, cyClient ; static SYSTEMTIME stPrevious ; BOOL fChange ; HDC hdc ; PAINTSTRUCT ps ; SYSTEMTIME st ; switch (message) f case WM_CREAFE : SetTimer (hwnd, ID_TIMER, 1000, NULL) ; GetLocalTime (ţst) ; stPrevious = st ; return 0 ; case WM_SIZE : cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; return 0 ; case WM_TIMER : GetLocalTime (&st) ; fChange = st.wHour != stPrevious.wHour st.wMinute != stPrevious.wMinute ; hdc = GetDC (hwnd) ; SetIsotropic (hdc, cxClient, cyClient) ; SelectObject (hdc, GetStockObject (WHITEţPEN)) ; DrawHands (hdc, &stPrevious, fCha.nge) ; ' SelectObject (hdc, GetStockObject (BLACKţPEN)) ; DrawHands (hdc, &st, TRUE) ; ReleaseDC (hwnd, hdc) ; 321 stPrevious = st ; return 0 ; case WMţPAINT : hdc = BeginPaint (hwnd, &ps) ; SetIsotropic (hdc, cxClient, cyClient) ; DrawClock (hdc) ; DrawHands (hdc, &stPrevious. TRUE) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY : KillTimer (hwnd, ID_TIMER) ; PostOuitMessage (0) ; return 0 ; ) return DefWindowProc (hwnd, message, wParam, lParam) ; Rysunetc &5. Program CLOCK Okno programu CLOCK przedstawione zostało na rysunku 8-6. , ' 'ů ,ů . . . . . . ů ů . , . . . . ů . . . . . . . ů . . . . . . .ů. ů. . . .ů. . Rysuţek 8-ţ. Okno programu CLOCK Kolejny raz okazuje sig, że dla takiej aplikaeji idealny jest tryb odwzorowania izo- tropicznego. Za jego ustawienie odpowiedzialna jest funkcja Setlsotroţic znajdu- jšca się w CLOCK.C. Po wywołaniu SetMapMode funkcja ta ustawia wielkoœć okna na 100, natomiast wielkoœć widoku na połowę wysokoœci obszaru roboczego oraz ujemnš (pomnożonš razy -I) połowę wysokoœci obszaru roboczego. Punkt po- czštkowy medium ustawiony zostaje w œrodku obszaru roboezego. Jak napisa- łem w rozdziale 5, spowoduje to utworzenie kartezjańskiego układu współrzęd- nych, którego punkt (0,0) znajduje się w œrodku obszaru roboczego, a każda oœ ma długoœć 1000 w obu kierunkach. Trygonometria dochodzi do głosu w funkeji RotatePoint. Jej trzy parametry po- zwalajš na przekazanie do tablicy punktów liczby znajdujšcych się w niej ele- mentów oraz kšta, o który majš one zostać obrócone. Obrót następuje zgodnie 322 Czţœć i: Podstawv z ruchem wskazówek zegara (jak to zwykle bywa we wszystkich porzšdnych ze- garach) wokół œrodka. Na przykład, jeżeli przekazywany do funkcji punkt ma współrzędne (0,100) - czyli, znajduje się na pozycji godziny 12:00 - a kšt obrotu wynosi 90 stopni, jego współrzędne zostanš przekształcone na (100,0) - czyli na odpowiednik godziny 3:00. Funkcja posługuje się następujšcymi formułami: x'=x*cos(a)+y*sin(a) y'=y*cos(a)+x*sin(a) Funkcja RotatePoint jest użyteczna nie tylko w trakcie rysowania tarczy zegara, ale, jak za chwilę się przekonamy, również jego wskazówek. Funkcja DrawClock odpowiedzialna jest za wyœwietlenie 60 kropek, rozpoczyna- jšc od tej, która znajduje się najwyżej (odpowiada godzinie 12:00). Każda z nich znajduje się w odległoœci 900 jednostek od œrodka układu współrzędnych - pierw- sza znajduje się w punkcie (0, 900). Każda kolejna jest przesuwana o 6 stopni zgodnie z ruchem wskazówek zegara. Dwanaœcie kropek odpowiadajšcych go- dzinom ma œrednicę 100 jednostek, natomiast pozostałe - 33 jednostki. Poszcze- gólne kropki rysowane sš za pomocš funkcji Ellipse. Furikcja DrawHands rysuje trzy wskazówki zegara: wskazujšcš godziny, minuty oraz sekundy. Współrzędne każdej z nich, odpowiadajšce pozycji pionowej, prze- chowywane sš w tablicy struktur POINT. W zależnoœci od aktualnego czasu współrzędne te obracane sš o odpowiedni kšt za pomocš funkcji RotatePoint, a na- stępnie wyœwietlane przez funkcję Windows Polyline. Zwróć uwagę, że wskazówka minutowa oraz godzinna wyœwietlane sš tylko wtedy, gdy parametr bChange funk- cji DrawHands ma wartoœć TRUE. W trakcie odœwieżania położenia te dwie wska- zówki w większoœci przypadków nie zostanš przesunięte. Zwróćmy teraz uwagę na funkcję okna. W ramach obsługi komunikatu WM-CRE- ATE funkcja ta pobiera aktualny czas i umieszcza go w zmiennej o nazwie dtPre- vious. Zostanie ona póŸniej wykorzystana do okreœlenia, czy poprzednio zmieni- ła się godzina lub minuta. Po raz pierwszy zegar jest rysowany za poœrednictwem komunikatu WM-PAINT. Wystarczy po prostu wywołać funkcje Setlsotropic, DrawClock oraz DrawHands. W ramach obsługi komunikatu WM-TiMER najpierw pobierany jest aktualny czas , a następnie program sprawdza, czy wskazówki godzin i minut wymagajš od- œwieżenia. Jeżeli tak, za pomocš białego pióra rysowane sš wszystkie wskazów- ki na podstawie poprzedniej wartoœci czasu. Dzięki temu prostemu zabiegowi zostajš one usunięte. W przeciwnym wypadku w ten sam sposób rysowany jest tylko sekundnik. Następnie rysowane sš wszystkie wskazówki, tym razem jed- nak z wykorzystaniem czarnego pióra. Zegar a raporty Ostatni program w tym rozdziale poœwięcony będzie czemuœ, o czym wspomnia- łem w rozdziale 5. Jest to jedyne godne uwagi zastosowanie funkcji GetPixel, któ- re znalazłem. Przedstawiony na rysunku 8-7 program WHATCLR wyœwietla informację o skła- dowych koloru punktu, który znajduje się akurat pod kursorem myszy. Rozdział 8: Zegar 323 r WHATCLR.C /* WHATCLR.C - Kod koloru pod kursorem myszy (c) Charles Petzold, 1998 */ ţţinclude ţţdefine ID TIMER 1 ' (... i void FindWindowSize (int * int *) ; LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) ( static TCHAR szAppName[] = TEXT ("WhatClr") ; HWND hwnd : int cxWindow, cyWindow ; MSG ms9 ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CS UREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULC, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITEţBRUSH> ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) ( MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MBţICONERROR) ; FindWindowSize (&cxWindow, &cyWindow) ; hwnd = CreateWindow (szAppName, TEXT ("What Color"), WS_OVERLAPPED ţ WS_CAPTION ţ WS-SYSMENU ţ WS BORDER, CW_USEDEFAULT, CW_USEDEFAULT, cxWindow, cyWindow, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) : UpdateWindow (hwnd) : while (GetMessage (&msg, NULL, 0, 0)) ( TranslateMessage (&msg) ; DispatchMessage (&msg) ; . return msg.wParam ; 324 Częœć I: Podstawy (cišg dalszy ze strony 323) ) void FindWindowSize (int * pcxWindow, int * pcyWindow) ( HDC hdcScreen ; TEXTMETRIC tm ; hdcScreen = CreateIC (TEXT ("DISPLAY"), NULL, NULL, NULL) ; GetTextMetrics (hdcScreen, &tm) ; DeleteDC (hdcScreen) ; * pcxWindow = 2 * GetSystemMetrics (SM_CXBORDER) + 12 * tm.tmAveCharWidth ; * pcyWindow = 2 * GetSystemMetrics (SM CYBORDER) + GetSystemMetrics (SM CYCAPTION) + 2 * tm.tmHeight ; ) LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ( static COLORREF cr, crLast ; static HDC hdcScreen ; HDC hdc ; PAINTSTRUCT ps ; POINT pt ; RECT rc ; TCHAR szBuffer [16] ; switch (message) i case WM_CREATE: hdcScreen = CreateDC (TEXT ("DISPLAY"), NULL, NULL, NULL) ; SetTimer (hwnd, ID TIMER, 100, NULL) ; return 0 ; case WM_TIMER: GetCursorPos (&pt) ; cr = GetPixel (hdcScreen, pt.x, pt.y) ; SetPixel (hdcScreen, pt.x, pt.y, 0); if (cr != crLast) ( crLast = cr ; InvalidateRect (hwnd, NULL, FALSE) ; ) return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; GetClientRect (hwnd, &rc) ; wsprintf (szBuffer, TEXT (" %02X %02X %02X "), GetRValue (cr), GetGValue (cr), GetBValue (cr)) ; DrawText (hdc, szBuffer, -i, &rc, DTţSINGLELINE ţ DT CENTER ţ DTţVCENTER) ; Rozdział 8: Zegar 325 EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: DeleteDC (hdcScreen) ; KillTimer (hwnd, ID TIMER) ; PostOuitMessage (0) ; return 0 : ) return DefWindowProc (hwnd, message, wParam, lParam) ; Rysunek &7. Program WHATCLR Wykonujšc funkcję WinMain, program WHATCLR zachowuje się w sposób nieco odmienny niż inne programy. Ponieważ jego okno musi być tylko tak duże, aby pomieœcić szesnastkowš wartoœć RGB, to posługujšc się stylem WS BORDER w funkcji CreateWindow, tworzy okno, którego wielkoœci nie da się zmienić. Chcšc wyznaczyć rozmiar okna, WHATCLR pobiera informacyjny kontekst urzšdzenia (ang. information device context) wywohxjšc CreatelC, a następnie GetSystemMetrics. Obliczona w ten sposób wysokoœć i szerokoœć przekazywana jest do funkcji Cre- ateWindow. Procedura okna programu WHATCLR w ramach obshzgi komunikatu WMţCRE- ATE tworzy kontekst urzšdzenia dla całego ekranu, wywołujšc CreateDC. Kon- tekst ten przechowywany jest przez cały czas pracy programu. Z kolei w ramach obsługi komunikatu WM TIMER, pobierany jest kolor punktu, nad którym znaj- duje się kursor myszy. Informacja o nim wyœwietlana jest w ramach WMţPAINT. Prawdopodobnie zastanawiasz się, czy uchwyt kontekstu urzšdzenia pobrany za pomocš funkcji CreateDC może zostać wykorzystany nie tylko do pobierania ko- loru punktu, lecz na przykład do wyœwietlenia czegoœ. OdpowiedŸ brzmi: tak. , Ogólnie rzecz bioršc uważa się, że "grzeczne" aplikacje nie powinny tego robić niemniej jednak, w niektórych przypadkach, jest to konieczne. Rozdział 9 i kna r Ik o Ko n t o otomne o Przypomruj sobie programy CHECKER zamieszczone w rozdziale 7. Wyœwietlały one siatkę prostokštów. Gdy któryœ z nich został kliknięty, program wyœwietlał w nim znak X. Po ponownym kliknięciu - znak znikał. Chociaż CHECKER1 oraz CHECKER2 posługiwały się wyłšcznie oknem głównym, to już w programie CHEC- KER3 każdy prostokšt zrealizowany został jako osobne okno potomne. Każde z nich obsługiwane było przez własnš procedurę okna o nazwie ChildProc. Gdybyœmy tylko chcieli, moglibyœmy umożliwić funkcji ChiIdProc wysyłanie ko- munikatów do okna nadrzędnego (do funkcji WndProc) za każdym razem, gdy prostokšt został zaznaczony lub odznaczony. Oto sposób: procedura okna potom- nego może okreœlić uchwyt okna nadrzędnego, wywołujšc funkcję GetParent, hwndParent = GetParent (hwnd) ; gdzie hwnd jest uchwytem okna potomnego. Teraz bez żadnych problemów można już wysłać komunikat do procedury okna nadrzędnego: SendMessage (hwndParent, messa9e, wParam, lParam) ; Jakš wartoœć można przekazać jako message? Cóż, może to być dowolna wartoœć liczbowa, pod jednym wszakże warunkiem: musi być ona większa od WM USER. Tylko w takim przypadku zdefiniowany przez ciebie komunikat nie będzie koli- dował z żadnym standardowym komunikatem WM-. Mogłoby się okazać, że jako wParam należałoby przekazać uchwyt okna potomnego. Z kolei lParam mogłoby mieć wartoœć 1, jeżeli okno potomne było zaznaczone, a 0 w przeciwnym wy- padku. Oczywiœcie, nie sš to jedyne możliwoœci. W rezultacie spowoduje to utworzenie kontrolki okna potomnego (ang. child win- dow control). Okno to obsługuje komunikaty myszy i klawiatury oraz informuje okno nadrzędne o każdorazowej zmianie swego stanu. W ten sposób okno po- tomne staje się urzšdzeniem wejœciowym okna nadrzędnego. Sš z nim zwišzane pewne funkcje oraz wyglšd, potrafi reagować na wprowadzane przez użytkow- nika dane, jak również może informować inne okna, gdy przytrafi się mu coœ ważnego. Chociaż możesz tworzyć swoje własne kontrolki okna potomnego, warto jednak skorzystać z wielu klas okien (i procedur okien), które już zostały zdefiniowane. Pisany przez ciebie program może zawierać wszystkie kontrolki, które widziałeœ już w innych aplikacjach. Mogš to być przyciski, pola wyboru, edycji, listy, pola 328 Częœć i. Podstawy kombo, napisy czy paski przewijania. Jeżeli na przykład chcesz umieœcić w rogu pisanego przez siebie arkusza kalkulacyjnego przycisk Przelicz, możesz stworzyć go za pomocš jednego wywołania funkcji CreateWindow. Nie musisz troszczyć się 0 obsługę komunikatów myszy ani sposób wyœwietlania przycisku, gdy zostanie on kliknięty. Wszystko to zrobi za ciebie Windows. Jedyne, czym musisz się za- jšć, to przechwycenie i obsłużenie komunikatu WMţCOMMAND. Dzięki niemu przycisk informuje procedurę okna, że jego stan uległ zmianie. Czyż nie jest to proste? Kontrolki okna potomnego zwykle występujš w oknach dialogowych. Jak dowiesz się z rozdziału 11, ich położenie i wielkoœć opisywane sš za pomocš szablonu okna dialogowego zapisanego w zasobach programu. Możesz jednak także po- służyć się predefiniowanymi kontrolkami, umieszczajšc je w obszarze roboczym zwykłego okna. Każdš z nich tworzysz za pomocš funkcji CreateiNindow, podajšc położenie i wielkoœć w wywołaniu funkcji MoveWindow. hrocedura okna nadrzęd- nego wysyła komunikaty do kontrolki okna potomnego, a ta z kolei odsyła ko- munikaty z powrotem do procedury okna nadrzędnego. Aby utworzyć zwykłe okno aplikacji, najpierw należy zdefiniować opisujšcš je klasę, a następnie zarejestrować jš za pomocš funkcji ftegisterClass. Dopiero wte- dy możesz otworzyć okno, wywołujšc funkcję CreateWindow. Jeżeli jednak chcesz stworzyć jednš z predefiniowanych kontrolek, nie musisz rejestrować jej klasy okna. Ta klasa już istnieje i ma swojš nazwę. Wystarczy podać jš jako parametr CreateWindow. Inny parametr tej funkcji, okreœlajšcy styl okna, pozwala na do- kładniejszy opis wyglšdu i funkcii kontrolki okna potomnego. System Windows dysponuje procedurami okien, które odpowiedzialne sš za obsługę komunika- tów zwišzanych z kontrolkami. Stosowanie kontrolek bezpoœrednio na powierzchni okna wymaga wykonania kilku czynnoœci niższego poziomu niż te, które sš wymagane, gdy kontrolki sš umieszczane w oknach dialogowych. Dzieje się tak, ponieważ menedżer okna dialogowego tworzy dodatkowš warstwę izolujšcš program od samych kontro- lek. Na przykład, wkrótce zorientujesz się, że kontrolki umieszczane na powierzch- ni okna nie majš funkcji, która przenosiłaby fokus z jednej kontrolki do drugiej po naciœnięciu [Tab] lub jednego z klawiszy kursora. Kontrolka okna potomnego może oczywiœcie otrzymać fokus, jednak gdy to już nastšpi, zwróci go dobrowolnie do okna nadrzędnego. Jest to problem, z którym będziemy musieli sobie pora- dzić w tym rozdziale. W dokumentacji przeznaczonej dla programistów Windows kontrolki okien potom- nych omawiane sš w dwóch miejscach. Po pierwsze, najprostsze standardowe kon- trolki wykorzystywane w niezliczonych oknach dialogowych opisane zostały w / Platform SDK/llser Interface Services/Controls. Sš to przyciski (łšcznie z polami wy- boru i przyciskami opcji), pola edycji (pozwalajšce na wprowadzenie i edycję jed- nej lub kilku linii tekstu), kontrolki statyczne (na przykład etykiety pól), paski prze- wijania, listy oraz pola kombi. Za wyjštkiem tej ostatniej, kontrolki obecne sš w sys- temie już od jego wersji 1.0. W tej częœci dokumentacji opisana została również kontrolka typu rich edit, która zbliżona jest do pola edycji z tš jednak różrucš, że pozwala na edycję tekstu sformatowanego, w którym wykorzystane zostały różne czcionki. Znajdziesz tam również informacje o paskach narzędzi. Rozdział 9: KoMrolki okna potomnego 329 System Windows udostępnia również zestaw bardziej specjalizowanych kontrolek, które na przekór nazywane sš kontrolkami typowymi (ang. common controls). Zo- stały one opisane w /Platform SDK/I.Iser Interface Seroices/Shell and Common Con- trols/Common Controls. Nie będę ich opisywał w tym rozdziale, będš one się jednak pojawiały w wielu miejscach tej ksišżki. Ten fragment dokumentacji Windows jest szczególnie użyteczny, ponieważ możesz odnaleŸć w nim to, co wydaje ci się inte- resujšce w innych aplikacjach i co chciałbyœ przenieœć do swojego programu. Klasa przycisków Naszš przygodę z przyciskami Windows zaczniemy od programu BTNLOOK (skrót od ang. button look - przeglšd przycisków). Program ten tworzy 10 przyci- sków, po jednym z każdego stylu. BTNLOOK.C /* BTNLOOK.C - Program Button Look (c) Charles Petzold, 1998 */ iiinclude struct f int iStyle ; TCHAR * szText ; 1 buttonC] = ( BS_PUSHBUTTON, TEXT ("PUSHBUTTON"), BS_DEFPUSHBUTTON, TEXT ("DEFPUSHBUTTON"), BS_CHECKBOX, TEXT ("CHECKBOX"), BS_AUTOCHECKBOX, TEXT ("AUTOCHECKBOX"), BS_RADIOBUTTON, TEXT ("RADIOBUTTON"), BS_3STATE, TEXT ("3STATE"), BS_AUTO3STATE, TEXT ("AUTO3STATE"), BS_GROUPBOX, TEXT ("GROUPBOX"), BS_AUTORADIOBUTTON, TEXT ("AUTORADIO"), BS OWNERDRAW, TEXT ("OWNERDRAW") 4ţdefine NUM (sizeof button / sizeof button[0]) LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) static TCHAR szAppNameC] = TEXT ("BtnLook") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS HREDRAW ţ CS VREDRAW ; 330 Częœć I, Podstawy (cišg dalszy ze strony 329) wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = NULL ; ů wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) t MessdgeBox (NULL, TEXT ("Program requires Windows NT!"), szAppName, MB ICONERROR) ; return 0 ; ) hwnd = CreateWindow (szAppName, TEXT ("Button Look"), WS OVERLAPPEDWINDOW, CW_USEDEFAULT, CW USEDEFAULT, CW_USEDEFAULT, CW USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) ( TranslateMessage (&msg) ; DispatchMessage (&msg) ; ) return msg.wParam ; LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) i static HWND hwndButton[NUM] ; static RECT rect ; static TCHAR szTop[] = TEXT ("message wParam lParam"), szUnd[] = TEXT (" szFormat[] = TEXT ("%-16s%04X-%04X %04X-%04X"), szBuffer[50] ; static int cxChar, cyChar ; HDC hdc ; PAINTSTRUCT ps ; int i ; switch (message) f case WM CREATE : cxChar = LOWORD (GetDialogBaseUnits ()) ; cyChar = HIWORD. (GetDialogBaseUnits ()) ; for ( i = 0 ; i < NUM ; i++) hwndButton[i] = CreateWindow ( TEXT("button"), button[i].szText, WSţCHILD ţ WS VISIBLE ţ button[i].iStyle, Rozdziaţ 9: Kontrolki okna potomnego 331 cxChar, cyChar * (1 + 2 * i), 20 * cxChar, 7 * cyChar / 4, hwnd, (HMENU) i, ((LPCREATESTRUCT) lParam)->hInstance, NULL) ; return 0 ; case WM_SIZE : rect.left = 24 * cxChar ; rect.top - 2 * cyChar ; rect.right = LOWORD (lParam) ; rect.bottom = HIWORD (lParam) ; return 0 ; case WM-PAINT : InvalidateRect (hwnd, &rect, TRUE) ; hdc = BeginPaint (hwnd, &ps) ; ' SelectObject (hdc, GetStockObject (SYSTEMţFIXEDţFONT)) ; SetBkMode (hdc, TRANSPARENT) ; TextOut (hdc, 24 * cxChar, cyChar, szTop, lstrlen (szTop)) ; TextOut (hdc, 24 * cxChar, cyChar, szUnd, lstrlen (szUnd)) ; EndPaint (hwnd, &ps) ; return 0 ; case WM DRAWITEM : case WM COMMAND : ScrollWindow (hwnd, 0, -cyChar, &rect, &rect) ; hdc = GetDC (hwnd) ; SelectObject (hdc, GetStockObject (SYSTEM-FIXEDţFONT)) : TextOut (hdc, 24 * cxChar, cyChar * (rect.bottom / cyChar - 1), szBuffer, wsprintf (szBuffer, szFormat, message = WM DRAWITEM ? TEXT ("WM DRAWITEM") : TEXT ("WM COMMAND"), HIWORD (wParam), LOWORD (wParam), HIWORD (lParam), LOWORD (lParam))) ; ReleaseDC (hwnd, hdc) ; ValidateRect (hwnd, &rect) ; break ; case WM DESTROY : PostauitMessage (0) ; return 0 ; l return DefWindowProc (hwnd, message, wParam, lParam) ; Rysunek 9-1. Program BT'NLOOK ů Po kliknięciu każdy z przycisków wysyła do procedury okna nadrzędnego ko- munikat WM COMMAND. W lewej częœci okna, jak zostało to przedstawione na rysunku 9-2, wyœwietlona zostaje zawartoœć parametrów wParam oraz lParam towarzyszšcych temu komurukatowi. 332 Czţœć I: Podstawy Przycisk typu BS OWNERDRAW narysowany został w tym oknie jedynie w po- staci szarego tła, ponieważ za jego wyœwietlanie odpowiedzialny jest program. Potrzebę wyœwietlenia przycisk ten sygnalizuje za pomocš komunikatu WM-DRAWITEM, przy czym jako lParam przekazywany jest wskaŸnik do struk- tury typu DRAWITEMSTRUCT. Również ten komunikat został wyœwietlony przez program BTNLOOK. Więcej szczegółów na temat przycisków rysowanych przez program znajdzie się w dalszej częœci tego rozdziahz. essaae VParan lParaţ t.... ........... ţ'+ţ. N!1 DBAVITEM IIAI-IIA9 IAl2-FC38 ţţţ....,.......,.ţ WM COlDVIND AAAB-AIAA IAA1-8118 ţ COMlAIND IIAB-1181 IIP7-1118 W11-COMIWlHD 1001-1112 A111-AllC VM COlkINND AAAB-A183 AAA1-AllE iM C0FBlBND lAAA-IAA4 A111-N21 VM COIklBND 110ţ1185 1111-1122 ţ,,.... WN_COIVIAHD IAlB-IIB6 AIB1-8124 YM_COMMBND AAlO-IAA8 AOAt-A128 5M_DBBVITE" IAIţABl9 1112-F811 VN_DBBVITEM OAAB-1119 1112-FBCI VM_DBlVITEM IAAA-AIB9 AAl2-FAA4 YN COMMfIND IIAI-IAB9 IID1-Q128 Rysunek 9-2. Okno programu BTNLOOK Tworzenie okien potomnych W programie BTNLOOK zdefiniowana została struktura o nazwie button, w któ- rej przechowywane sš informacje o stylu oraz opis każdego z 10 typów przyci- sków zdefiniowanych w systemie. Wszystkie identyfikatory stylów rozpoczyna- jš się od liter BS- (ang. button style - styl przycisku). Wszystkie 10 okien potom- nych tworzone jest w pętli for w ramach obshzgi komunikatu WM CREATE w funkcji WndProc. W wywołaniu CreateWindow podawane sš następujšce para- metry: Nazwa klasy TEXT "button" Tekst okna button[1].szText Styl okna WS_CHILD ţ WSţVISIBLE ţ buttonCi].iStyle Wspólrzędna x cxChar Wspólrzędna y cyChar * (1 + 2 * i) Szerokoœć 20 * xChar Wysokoœć 7 * yChar / 4 Okno nadrzędne hwnd Identyfikator okna potomnego (HMENU) i Uchwyt instancji ((LPCREATESTRUCT) lParam) -> hInstance Dodatkowe parametry NULL , Rozdział 9: Kontrolki okna potomnego 333 Parametr będšcy nazwš klasy jest już zdefiniowany. Jako styl okna podawane sš wartoœci WS CHILD, WS VISIBLE oraz jeden z 10 stylów zwišzanych z poszcze- gólnymi rodzajami przycisków (BS PUSHBUTTON, BS DEFPUSHBUTTON i tak dalej), które przechowywane sš w strukturze przycisku. Parametr okreœlajšcy tekst okna (w przypadku zwykłego okna zostaje on wyœwietlony na pasku tytułu) to tekst, który będzie wyœwietlany dla każdego z przycisków. W tym przypadku posłużyłem się po prostu tekstem identyfikujšcym typ przycisku. Parametry x oraz y okreœlajš współrzędne lewego górnego rogu okna potomne- go względem lewego górnego narożnika okna nadrzędnego. Z kolei szerokoœć i wysokoœć definiujš odpowiednie rozmiary przycisku. Zwróć uwagę, że w celu pobrania wysokoœci i szerokoœci znaków czcionki domyœlnej posługuję się funk- cjš GetDialogBaseUnits. Jest to funkcja wykorzystywana przez okno dialogowe do okreœlenia wymiarów tekstu. Zwraca ona wartoœć 32-bitowš: w jej młodszym sło- wie zapisana jest szerokoœć, w starszym natomiast - wysokoœć. Chociaż funkcja GetDialogBaseUnits zwraca w przybliżeniu takš sama wartoœć jak GetTextMetrics, jest znacznie łatwiejsza w użyciu i zapewnia większš spójnoœć kontrolek w oknie dialogowym. Parametr okreœlajšcy identyfikator okna powinien być unikatowy dla każdego z okien potomnych. Dzięki niemu procedura okna może okreœlić, która kontrolka odpowiedzialna jest za wygenerowanie komunikatu WM COMMAND. Zwróć uwagę, że identyfikator okna przekazywany jest do funkcji CreateWindow w miej- scu wykorzystywanym zwykle do okreœlania menu programu. Dlatego też musi być rzutowany na typ HMENU. Uchwyt instancji przekazywany do funkcji CreateWindow na pierwszy rzut oka może wyglšdać doœć dziwnie. Korzystamy jednak z tego, że parametr IParam komunikatu WM-CREATE jest wskaŸnikiem do struktury typu CREATESTRUCT (ang. creation structure - struktura tworzenia), a jednš z jego składowych jest hIn- stance. Dlatego też rzutujemy lParam na wskaŸnik do CREATESTRUCT, a następ- nie pobieramy hlnstance. (Częœć programów pisanych do pracy pod kontrolš systemu Windows przecho- wuje uchwyt instancji w zmiennej globalnej nazywanej zwykle hlnst. W funkcji WinMain wystarczy w tym przypadku napisać po prostu: hInst = hInstance : zanim stworzone zostanie główne okno aplikacji. W programie CHECKER3 z roz- działu 7 do pobrania uchwytu używaliœmy funkcji GetWindowLong: GetWindowLong (hwnd, GW HINSTANCE) Każda z wymienionych wyżej metod jest poprawna). Po wywołaniu funkcji CreateWindow nie musimy już nic robić z oknami potom- nymi. Całš pracę wykona za nas znajdujšca się w systemie Windows procedura okna przycisku. (Wyjštkiem od tej zasady jest przycisk typu BS OWNERDRAW; jak juŸ niedługo będziesz miał okazję się przekonać, wymaga on narysowania przez program). Gdy program będzie kończył pracę, wraz z oknem nadrzędnym usunięte zostanš również wszystkie okna potomne. 334 Częœć I: Podstawy Komunikacja między kontrolkami a oknem nadrzędnym Po uruchomieniu programu BTNLOOK z lewej strony obszaru roboczego zoba- czysz różne rodzaje przycisków. Jak już wspomniałem wczeœniej, jeżeli klikniesz któryœ z nich, przycisk wyœle do okna nadrzędnego komunikat WMţCOMMAND. Procedura okna programu BTNLOOK obsługuje go i wyœwietla zawartoœć zwiš- zanych z nim parametrów wParam i IParam. Oto co one oznaczajš: LOWORD(wParam) Identyfikator okna potomnego HIWORD(wParam) Kod powiadomienia IParam Uchwyt okna potomnego Jeżeli dokonujesz konwersji programu napisanego dla 16-bitowej wersji systemu Windows, uważaj, gdyż zmienił się sposób wykorzystania parametrów komuni- katu (został dostosowany do 32-bitowych uchwytów). Identyfikator okna potomnego to wartoœć przekazana'jako parametr funkcji Cre- ateWindow w trakcie tworzenia okna. W programie BTNLOOK 10 oknom potom- nym przypisane zostały identyfikatory od 0 do 9. Z kolei uchwyt okna potomne- go to wartoœć zwracana przez funkcję CreateWindow po utworzeniu kontrolki. Kod powiadomienia umożliwia dokładniejsze okreœlenie, jakie zdarzenie spowo- dowało wygenerowanie odebranego komunikatu. Możliwe wartoœci kodów po- wiadomienia zdefiniowane zostały w plikach nagłówkowych Windows: Kod powiadomienia przycisku Wartoœć BN CLICKED 0 BN PAINT 1 BN HILITE lub BN PUSHED 2 BN UNHILITE lub BN UNPUSHED 3 BN DISABLE 4 BN DOUBLECLICKED lub BN DBLCLK 5 BN SETFOCUS 6 BN KILLFOCUS 7 W rzeczywistoœci rugdy nie zetkniesz się z większoœciš tych wartoœci. Kody o war- toœciach od 1 do 4 wykorzystywane były przez nieistniejšcy już przycisk typu BS USERBUTTON (został zastšpiony przez BS OWNERDRAW, posługujšcy się odmiennym mechanizmem powiadamiariia). Kody 6 oraz 7 wysyłane sš wyłšcz- nie wtedy, gdy do stylu definiujšcego przycisk dodano BS NOTIFY. Z kolei kod powiadomienia 5 wysyłany jest jedynie przez przyciski BS RADIOBUTTON, BS AUTORADIOBUTTON oraz BS OWNERDRAW lub przez inne przyciski, je- żeli ich styl zawiera BS NOTIFY. Z pewnoœciš zwrócisz uwagę, że kliknięcie przycisku myszš powoduje naryso- wanie wokół niego przerywanej linii. Oznacza to, że przycisk otrzymał fokus. Od tej chwili wszelkie dane wprowadzane z klawiatury będš kierowane do niego, a nie do okna nadrzędnego. Mimo to ten przycisk będzie ignorował naciskanie wszelkich klawiszy z wyjštkiem spacji, której naciœnięcie daje taki sam efekt jak kliknięcie myszš. Rozdział 9: Kontrolki okna potomnego 335 Komunikacja pomiędzy oknem nadrzędnym a kontrolkami Chociaż program BTNLOOK nie pozwala tego zaobserwować, również okno nadrzędne może komunikować się z kontrolkš okna potomnego za pomocš wie- lu typów komunikatów o nazwach rozpoczynajšcych się od WM. Dodatkowo, w pliku WINUSER.H zdefiniowane zostało osiem komunikatów charakterystycz- nych dla przycisków. Nazwa każdego z nich rozpoczyna się od liter BM (ang. button message - komunikat przycisku). Komunikaty te przedstawione zostały w poniższej tabeli: Komunikat przycisku Wartoœć BM_GETCHECK 0x00F0 BM SETCHECK 0x00F1 BM GETSTATE 0x00F2 BM_SETSTATE 0x00F3 BM SETSTYLE 0x00F4 BM CLICK 0x00F5 BM GETIMAGE 0x00F6 BM SETIMAGE 0x00F7 Komunikaty BM GETCHECK oraz BM SETCHECK wysyłane sš przez okno nadrzędne do okna potomnego w celu pobrania lub ustawienia stanu pola wy- boru lub przycisku opcji. Komunikaty BM GETSTATE oraz BM SETSTATE od- noszš się do normalnego, "wciœniętego" przycisku, gdy klikasz go myszš lub naciskasz klawisz [Spacja]. Sposób działania tych komunikatów poznamy, przy- glšdajšc się każdemu z typów przycisków. Komunikat BM SETSTYLE pozwala natomiast na zmianę stylu przycisku po jego utworzeniu. Każde okno potomne ma swój uchwyt oraz unikatowy identyfikator. Jeżeli znasz jeden z nich, możesz jednoznacznie okreœlić drugi. Jeżeli dysponujesz uchwytem okna potomnego, za pomocš poniższej funkcji możesz pobrać jego identyfikator: id = GetWindowLong (hwndChild, GWţID ) : Funkcja ta (łšcznie z SetWindowLang) wykorzystana była w przedstawionym w roz- dziale 7 programie CHECKER3. Posłużyliœmy się niš, korzystajšc z danych umieszczonych w specjalnym obszarze okna rezerwowanym w trakcie rejestracji jego klasy. Obszar, do którego masz dostęp za pomocš identyfikatora GW ID, rezerwowany jest przez system w trakcie tworzenia okna. Aby pobrać identyfi- kator okna, możesz również posłużyć się funkcjš: id = GetDlgCtrlID (hwndChild) : Chociaż będšce częœciš nazwy funkcji "Dlg" pochodzi od okna dialogowego, tak naprawdę jest to funkcja ogólnego zastosowania. Znajšc identyfikator okna potomnego możesz pobrać jego uchwyt. W tym celu powinieneœ posłużyć się następujšcš funkcjš: hwndChild = GetDlgItem (hwndParent, id) ; 336 Częœć I: Podstawy Przyciski klasyczne (naciskane) ' Pierwsze dwa przyciski pokazane w oknie programu BTNLOOK to tak zwane przy- ciski klasyczne (to znaczy takie, które można naciskać). Przycisk klasyczny jest pro- stokštem z tekstem, tym samym, który podany został jako parametr funkcji Create- Window. Prostokšt ten zajmuje całš wysokoœć i szerokoœć podanš za pomocš funk- cji CreateWindow lub MoveWindow. Tekst umieszczony jest w jego centralnej częœci. Przycisków klasycznych używa się zwykle wtedy, gdy konieczne jest natychmia- stowe wykonanie jakiejœ czynnoœci bez zapamiętywania stanu przycisku. Istniejš dwa typy takich przycisków: BS PUSHBUTTON oraz BS DEFPUSHBUTTON. DEF" w nazwie BS DEFPUSHBUTTON pochodzi od słowa "default" (domyœl- " ny). Gdy przyciski te sš wykorzystywane w oknie dialogowym, działajš w spo- sób nieco odmienny. Jeżeli jednak zostanš wykorzystane jako kontrolki okien potomnych - zachowujš się dokładnie tak samo, mimo że BS DEFPUSHBUTTON ma grubszš obwódkę. Przycisk klasyczny wyglšda najlepiej wtedy, gdy jego wysokoœć jest równa 7/4 wysokoœci znajdujšcego się w nim tekstu (taka wykorzystana została w progra- mie BTNLOOK). Z kolei długoœć przycisku musi być równa co najmniej długoœci wyœwietlanego tekstu powiększonej o dwa znaki. Jeżeli użytkownik umieœci nad przyciskiem kursor myszy, a następnie przyciœnie jeden z jej przycisków, odniesie wrażenie, że przycisk zapada się. Zwolnienie przycisku myszy powoduje przywrócenie poczštkowego wyglšdu i wysyłanie do okna nadrzędnego komunikatu WM COMMAND z kodem powiadomienia BN CLICKED. Tak jak w przypadku innych typów przycisków, przekazanie fo- kusu do przycisku klasycznego powoduje, że wokół jego tekstu zostaje naryso- wana przerywana ramka, a naciœnięcie w tym momencie klawisza [Spacja] po- woduje analogicznš reakcję, jak kliknięcie myszš. Zmiany stanu przycisku możesz symulować, wysyłajšc do niego komunikat BM SETSTATE. Wykonanie poniższego wyrażenia spowoduje, że przycisk zo- stanie "naciœnięty": SendMessage (hwndButton, BM SETSTATE, 1, 0) ; Z kolei to polecenie: SendMessage (hwndButton, BM SETSTATE, 0, 0) : spowoduje, że powróci on do swego normalnego stanu. Uchwyt okna hwndButton jest tš samš wartoœciš, która zwrócona została przez funkcję CreateWindow. Do przycisku klasycznego możesz również wysłać komunikat BM GETSTATE. W odpowiedzi na niego kontrolka zwróci swój aktualny stan: TRUE, jeżeli przy- cisk jest naciœnięty, FALSE - w przeciwnym wypadku. Większoœć aplikacji nie potrzebuje tej informacji. Komunikaty te nie sš wykorzystywane również dlate- go, że przyciski klasyczne nie przechowujš informacji o swoim stanie. Pola wyboru Pole wyboru jest prostokštnš ramkš obok której, zwykle po prawej stronie, znaj- duje się tekst (jeżeli w trakcie tworzenia przycisku włšczysz styl BS LEFTTEXT, Rozdział 9: Kontrolki okna potomnego 337 ł tekst pojawi się po lewej stronie ramki; z tego stylu prawdopodobnie będziesz korzystał łšcŸnie z BS RIGHT, co pozwoli na wyrównanie tekstu do prawej stro- ny). Pola wyboru wykorzystywane sš w aplikacji zwykle do ustawiania opcji. Funkcjonujš podobnie do przełšczników: jednokrotne kliknięcie pola wyboru powoduje, że w jego wnętrzu pojawia się znacznik, natomiast drugie kliknięcie i usuwa go. ' Dwa najczęœciej wykorzystywane pola wyboru to: BS CHECKBOX oraz BS AU- ' TOCHECKBOX. Gdy korzystasz z tego pierwszego, po jego kliknięciu we wnę- trzu pola musisz osobiœcie umieœcić znacznik, wysyłajšc komunikat BM SET CHECK. Gdy parametr wParam równa się 1, znacznik zostanie wstawiony, jeœli natomiast równa się 0, znacznik zniknie. Aktualny stan pola wyboru możesz sprawdzić, wysyłajšc do niego komunikat BM GETCHECK. W ramach obsługi komunikatu WMţCOMMAND możesz przełšczać znacznik, posługujšc się po- niższym wyrażeniem: SendMessage ((HWND) lParam, BMţSETCHECK, (WPARAM) !SendMessage ((HWND) lParam, BM GETCHECK, 0, 0), 0) ; Zwróć uwagę na operator ! znajdujšcy się przed drugim wywołaniem funkcji SendMessage. Wartoœć parametru lFaram jest uchwytem okna potomnego, który do procedury okna trafia wraz z komunikatem WM COMMAND. Jeżeli póŸniej będziesz potrzebował informacji o stanie pola wyboru, wyœlij do niego kolejny komunikat BM GETCHECK. Możesz również pobrać jego aktualny stan i prze- chować go w zmiennej pomocniczej. Pole wyboru możesz również zainicjować, wysyłajšc do niego komunikat BM SETCHECK: SendMessage (hwndButton, BM SETCHECH, l, 0) ; W przypadku pola wyboru typu BS AUTOCHECKBOX, sama kontrolka odpo- wiedzialna jest za wyœwietlenie i ukrycie swego znacznika. Dlatego też procedu- ra okna może ignorować zwišzane z niš komunikaty WM-COMMAND. Jeżeli kiedykolwiek będziesz potrzebował informacji o aktualnym stanie tej kontrolki, możesz posłużyć się następujšcym wyrażeniem: iCheck = (int) SendMessage(hwndButton, BM GETCHECK, 0, 0) ; Zmienna iCheck ma wartoœć TRUE (lub niezerowš), jeżeli przycisk został zazna- czony, a FALSE (czyli zero) - w przeciwnym wypadku. Pozostałe dwa typy pola wyboru to: BS 3STATE oraz BS AUTO3STATE. Jak su- gerujš ich nazwy, mogš się one znajdować w trzech stanach. W trzecim z nieh, oznaczanym za pomocš szarego koloru tła, kontrolka znajdzie się wtedy, gdy wysłany zostanie do niej komunikat BM SETCHECK, a jako wParam przekazana zostanie wartoœć 2. Szary kolor informuje użytkownika, że w polu tym nie doko- nano żadnego wyboru lub że wybór nie ma znaczenia. Wywołujšc funkcję CreateWindow, należy podać prostokšt okreœlajšcy położenie okna potomnego. Pole wyboru pojawia się na ekranie dosunięte do jego lewej krawędzi i w œrodku jego wysokoœci. Kliknięcie w dowolnym miejscu tego pro- stokštnego obszaru powoduje, że do okna nadrzędnego wysłany zostaje komu- nikat WMţCOMMAND. Najmniejsza możliwa wysokoœć pola wyboru to jeden znak. Z kolei minimalna szerokoœć równa jest długoœci tekstu kontrolki powięk- szonej o dwa znaki. 338 Częœć I: Podstawy Przyciski opcji Przyciski opcji działajš podobnie jak znajdujšce się w niektórych radioodbiorni- kach samochodowych zestawy przycisków umożliwiajšce zaprogramowanie ulu- bionych stacji, przy czym w danym momencie naciœnięty może być tylko jeden z nich. W oknach dialogowych grupy przycisków opcji wykorzystywane sš zwykle do wybrania jednej ze wzajemnię wykluczajšcych się opcji. W przeciwieństwie do pól wyboru, przyciski opcji nie działajš jak przełšczniki. Oznacza to, że jeżeli klikniesz taki przycisk po raz drugi, jego stan się nie zmieni. Pod względem wyglšdu przyciski opcji podobne sš do pól wyboru z wyjštkiem tego, że zamiast kwadratu wykorzystany został niewielki okršg. Jeżeli w jego wnętrzu znajduje się duża, czarna kropka oznacza to, że przycisk został zazna- czony. Przyciski opcji mogš należeć do jednego z dwóch typów: BS RADIOBUT TON lub BS AUTORADIOBUTTON. Ten drugi jednak może być wykorzystywany wyłšcznie w oknach dialogowych. Jeżeli procedura okna odbierze zwišzany z przyciskiem opcji komunikat WM COMMAND, powinieneœ spowodować wyœwietlenie jego znacznika, wy- syłajšc komunikat BM SETCHECK z parametrem wParam równym 1: SendMessage (hwndButton, BM SETCHECK, 1, 0) ; Należy również usunšć znacznik ze wszystkich innych przycisków opcji należš- cych do tej samej grupy. Możesz to zrobić, wysyłajšc do każdego z nich komuni- kat BM SETCHECK z parametrem wParam równym 0: SendMessage (hwndButtţn, BM SETCHECK, 0, 0) ; Pola grupy Pole grupy, tworzone za pomocš stylu BS GROUPBOX, jest w klasie przycisków pewnego rodzaju odszczepieńcem. Ani nie reaguje na komunikaty zwišzane z kla- wiaturš i myszš, ani nie wysyła komunikatu WMţCOMMAND. Pole grupy to po prostu prostokštna ramka z tekstem w górnej częœci. Pola te sš zwykle wyko- rzystywane do łšczenia ze sobš innych typów przycisków. Modyfikacja tekstu przycisku Tekst przycisku (lub dowolnego innego okna) możesz zmodyfikować, posługu- jšc się funkcjš SetWindowText: SetWindowText (hwnd, pszString) ; gdzie hwnd to uchwyt okna, którego tekst ma zostać zmieniony; a pszString jest wskaŸnikiem do zakończonego zerem łańcucha znaków. W przypadku zwykłe- go okna tekst ten wyœwietlany jest na pasku tytułu. Natomiast w przypadku kon- trolek, wyœwietlany jest razem z przyciskiem. Możliwe jest również pobranie aktualnego tekstu okna: iLength = GetWindowText (hwnd, pszBuffer, iMaxLength) ; Parametr iMaxLength okreœla największš liczbę znaków, która może być skopio- wana do bufora pszBufţ'er. Funkcja zwraca długoœć skopiowanego łańcucha. Moż- liwe jest również wczeœniejsze ustalenie tej długoœci. Rozdział 9: Kontrolki okna potoranego 339 i. Wystarczy w tym celu wykonać następujšce wyrażenie: ! iLength = GetWindowTextLength (hwnd) ; Przyciski widoczne i dostępne Okno potomne może odbierać komunikaty generowane przez klawiaturę i mysz ' tylko wtedy, gdy jest jednoczeœnie widoczne (to znaczy wyœwietlone) oraz do- stępne. Jeżeli okno to jest widoczne, ale nie jest dostępne, jego tekst wyœwietlany jest kolorem szarym, a nie w czarnym. Jeżeli w trakcie tworzenia okna potomnego za pomocš funkcji CreateWindow do jego stylu nie zostanie dołšczony identyfikator WS VISIBLE, okno to nie będzie widoczne aż do momentu, gdy wywołana zostanie funkcja ShowWindow: ShowWindow (hwndChild, SWţSHOWNORMAL) ; Jeżeli natomiast w klasie okna podany zostanie identyfikator WS VISIBLE, wy- i wołanie tej funkcji nie będzie konieczne. Funkcja ShowWindow umożliwia rów- nież ukrycie okna. W tym celu wystarczy wywołać: i ShowWindow (hwndChild, SW HIDE) ; Jeżeli chcesz się przekonać, czy okno jest w danej chwili widoczne, wywołaj IsWindowWisible (hwndChild) ; Kolejnš operacjš, jakš możesz wykonywać na oknie potomnym, jest jego udostęp- nianie lub blokowanie. Domyœlnie każde okno jest dostępne. W każdej chwili możesz je zablokować, posługujšc się w tym celu wyrażeniem EnableWindow (hwndChild, FALSE) ; W przypadku kontrolek przycisków wykonanie tego wyrażenia spowoduje, że tekst przycisku zostanie wyœwietlony kolorem szarym, a sam przycisk przesta- nie reagować na jakiekolwiek komunikaty generowane przez klawiaturę lub mysz. Jest to najlepszy sposób poinformowania użytkownika, że zwišzana z przyciskiem funkcja lub opcja jest w danej chwili niedostępna. Przycisk może zostać ponownie udostępniony po wykonaniu: EnableWindow (hwndChild, TRUE) ; Jeżeli chcesz się przekonać, czy w danej chwili okno jest dostępne, możesz wy- wołać: IsWindowEnabled (hwndChild) ; Przyciski i fokus Jak już wspomniałem wczeœniej, przyciski naciskane, pola wyboru, przyciski opcji jak również przyciski rysowane przez program otrzymujš fokus po kliknięciu my- szš. Sama kontrolka informuje o tym fakcie, rysujšc wokół siebie prostokšt prze- rywanš liniš. Gdy fokus przeniesiony zostaje do kontrolki, traci go okno nadrzęd- ne; wszelkie komunikaty generowane przez klawiaturę będš od tej chwili prze- kazywane do kontrolki. Jednakże kontrolka okna potomnego reaguje jedynie na naciskanie klawisza [Spacja], który działa analogicznie do przycisku myszy. Po- jawia się więc poważny problem: program traci kontrolę nad klawiaturš. Zobacz- my, co można z tym zrobić. o Częœć I: Podstawy Wspomniałem w rozdziale 6, że gdy system Windows przenosi fokus z jednego okna (na przykład z okna nadrzędnego) do innego (na przykład do okna potom- nego), najpierw wysyła komunikat WMţKILLFOCUS do tego pierwszego. Za pomocš parametru wParam przekazywany jest uchwyt tego okna, które ma wła- œnie otrzymać fokus. Następnie Windows przesyła komunikat WM SETFOCUS do okna, które ma otrzymać fokus. Tym razem parametr wParam przechowuje uchwyt okna tracšcego fokus. (W obu przypadkach parametr wParam może mieć wartoœć NULL, co oznacza, że żadne okno nie ma fokusu, lub że żadne go nie otrzymuje). Za pomocš komunikatu WM KILLFOCUS, okno nadrzędne może uniemożliwić oknom potomnym otrzymanie fokusu. Załóżmy, że w tablicy hwndChild przecho- wywane sš uchwyty wszystkich okien potomnych. (Zostały tam umieszczone w trakcie wywoływania funkcji CreateWindow, za pomocš której kontrolki te zostały utworzone). NUM okreœla liczbę okien potomnych. case WMţKILLFOCUS: for (i = 0 ; i < NUM ; i++) if (hwndChild [iţ == (HWND) wParam) ( SetFocus (hwnd) ; break : ) return 0 ; W powyższym przykładzie okno nadrzędne, które odkryje, że traci fokus na rzecz jednego ze swoich okien potomnych, może otrzymać go ponownie, wywohzjšc funkcję SetFocus. Porużej przedstawiona została prostsza (jednak rzadziej spotykana) metoda osiš- gnięcia tego samego celu: case WM_KILLFOCUS : if (hwnd --- GetParent ((HWND) wParam)) SetFocus (hwnd) ; return 0 ; Obie metody majš jednak pewnš wadę. Uniemożliwiajš mianowicie przyciskom reagowanie na naciskanie klawisza [Spacja]. Dzieje się tak, ponieważ przyciski te nigdy nie otrzymujš fokusu. Znacznie lepszym rozwišzaniem byłoby pozosta- wienie przyciskowi fokusu i umożliwienie użytkownikowi przejœcia do następ- nej kontrolki za pomocš klawisza [Tab]. Na pierwszy rzut oka wydaje się to nie- możliwe, jednak wkrótce pokażę ci, w jaki sposób można tego dokonać, stosujšc metodę podklasy okna (ang. window subclassing). Poczekaj po prostu, aż w dal- szej częœci tego rozdziału zajmiemy się programem COLORSl. Kontrolki a kolory Jak widać na rysunku 9-2, wyœwietlanie wielu przycisków nie wypadło zbyt do- brze. Przyciski naciskane wyglšdajš całkiem dobrze, jednak wokół pozostałych narysowane zostały szare prostokšty, których nie powinno tam po prostu być. Stało się tak, ponieważ przyciski zaprojektowane zostały do pracy w oknach dia- logowych, a w Windows 98 okna te majš szare tło. Z kolei nasze okno ma kolor biały, ponieważ tak zdefiniowaliœmy to w opisujšcej go klasie: Rozdział 9: Konlrolki okna potomnego 341 wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITEţBRUSH) ; Zrobiliœmy to, ponieważ często w obszarze roboczym wyœwietlamy tekst, do ry- sowania którego GDI korzysta z domyœlnego koloru oraz tła zdefiniowanego w danym kontekœcie urzšdzenia: sš one zawsze czarne i białe. Jeżeli chcemy, aby przyciski wyglšdały trochę lepiej, mamy do wyboru dwie możliwoœci: albo zmo- dyfikujemy kolor obszaru roboczego tak, aby zgadzał się z kolorem tła przyci- sków, albo zmienimy to tło na białe. Jednak zanim zaczniesz, musisz zrozumieć, co to sš kolory systemowe (ang. sys- tem colors) Windows. Kolory systemowe W systemie Windows zdefiniowano 29 kolorów wykorzystywanych do rysowa- nia poszczególnych elementów graficznego interfejsu użytkownika. Poszczegól- ne kolory możesz pobierać Iub modyfikować, posługujšc się funkcjami GetSysColor oraz SetSysColors. Sam kolor okreœlany jest za pomocš identyfikatorów, które zo- stały zdefiniowane w plikach nagłówkowych. Jeżeli posłużysz się funkcjš SetSys- Colors, kolor zostanie zmodyfikowany tylko dla trwajšcej właœnie sesji Windows. Częœć kolorów możesz zmienić, posługujšc się apletem Ekran znajdujšcym się w Pa- nelu sterowania. W systemie Windows NT informacje o kolorach przechowywane sš w Rejestrze, natomiast w Windows 98 - w pliku WIN.IM. W obu przypadkach wykorzystane zostało 29 identyfikatorów kolorów (sš one różne od identyfikato- rów GetSysColor oraz SetSysColors). Po każdym identyfikatorze występuje sekwen- cja trzech liczb oznaczajšca intensywnoœć barwy czerwonej, zielonej i niebieskiej tworzšcych dany kolor. Wartoœci poszczególnych barw mogš należeć do przedzia- łu od 0 do 255. W poniższej tabeli przedstawione zostały definicje 29 kolorów sys- temowych, jak również odpowiadajšce im identyfikatory funkcji GetSysColor oraz SetSysColors i słowa kluczowe WIN.IM. Zawartoœć tablicy uporzšdkowana zosta- ła według wartoœci stałych COLORJ poczynajšc od 0 a kończšc na 28. Identyfikator Klucz Rejestru lub Domyœlna wartoœć GetSysCoIor oraz SetSysColors identyfikator RGB WIN.INI COLOR SCROLLBAR Scrollbar CO-CO-CO COLOR BACKGROUND Background 00-80-80 COLOR ACTIVECAPT'ION ActiveTitle 00-00-80 COLOR INACTIVECAPTION InactiveTitle 80-80-80 COLOR MENU Menu CO-CO-CO COLOR WINDOW Window FF-FF-FF COLOR WINDOWFRAME WindowFrame 00-00-00 COLOR MENUTEXT MenuText CO-CO-CO COLOR_WINDOWTEXT WindowText 00-00-00 COLOR CAPTIONTEXT ţ TitleText FF-FF-FF COLOR ACTIVEBORDER ActiveBorder CO-CO-CO COLOR INACTIVEBORDER InactiveBorder CO-CO-CO 342 Częœć I: Podstawy Identyfikator Klucz Rejestru lub Domyœlna wartoœć GetSysColor oraz SetSysColors identyfikator RGB WIN.INI COLOR APPWORKSPACE AppWorkspace 80-80-80 COLOR HIGHLIGHT Highlight 00-00-80 COLOR HIGHLIGHTTEXT HighlightText FF-FF-FF COLOR BTNFACE ButtonFace CO-CO-CO COLOR BTNSHADOW ButtonShadow 80-80-80 COLOR GRAYTEXT GrayText 80-80-80 COLOR BTNTEXT ButtonText 00-00-00 COLOR INACTIVECAPTIONTEXT InactiveTitleText CO-CO-CO COLOR BTNHIGHLIGHT ButtonHighlight FF-FF-FF COLOR 3DDKSHADOW ButtonDarkShadow 00-00-00 COLOR 3DLIGHT ButtonLight CO-CO-CO COLOR INFOTEXT InfoText 00-00-00 COLOR INFOBK InfoWindow FF-FF-FF [bez identyfikatora, użyj wartoœci 25] ButtonAlternateFace B8-B4-B8 COLOR HOTLIGHT HotTrackingColor 00-00-FF COLOR GRADIENTACTIVECAPTION GradientActiveTitle 00-00-80 COLOR GRADIENTINACTIVECAPTION GradientInactiveTitle 80-80-80 Domyœlna wartoœć każdego z 29 kolorów okreœlana jest przez program obsługi karty graficznej i może zmieniać się dla różnych komputerów. A teraz zła wiadomoœć: chociaż identyfikatory wielu kolorów wydajš się dokład- nie mówić, czego dotyczš (na przykład COLOR BACKGROUND okreœla kolor pulpitu w tle wszystkich okien), posługiwanie się kolorami systemowymi we wczeœniejszych wersjach Windows było doœć chaotyczne. Dawno, dawno temu interfejs graficzny systemu był o wiele prostszy niż obecnie. Zanim pojawił się Windows 3.0, zdefiniowane było jedynie 13 z powyższych kolorów. Kolejne iden- tyfikatory i kolory pojawiały się wraz z nowymi, coraz bardziej złożonymi kon- trolkami. Kolory przycisku Zasygnalizowany powyżej problem szczególnie dobrze widać na podstawie przy- cisków. COLOR BTNFACE wykorzystywany jest jako kolor górnej powierzchni przycisków naciskanych oraz jako kolor tła pozostałych. (Jest to również kolor systemowy wykorzystywany do wyœwietlania tła okien dialogowych oraz okien komunikatów). COLOR BTNSHADOW używany jest do zaznaczania cienia wzdłuż prawej i dolnej krawędzi przycisku, wewnętrznych linii prostokšta pola wyboru oraz kółek przycisków opcji. W przypadku przycisków naciskanych COLOIRBTN'TEXT wykorzystywany jest do wyœwietlenia tekstu. Pozostałe przy- ciski korzystajš w tym celu z COLOR WINDOWTEXT. Pozostałe kolory sš rów- nież wykorzystywane do wyœwietlania innych częœci przycisków. Rozdział 9: Kontrolki okna potomnego 343 Jeżeli więc chcesz wyœwietlić przycisk w obszarze roboczym tak, aby kolory nie gryzły się, jedynš metodš jest modyfikacja kolorów systemowych. Na poczštek ' możesz użyć koloru COLOR BTNFACE do wypełnienia tła obszaru roboczego okna. Definiujšc klasę okna napisz: wndclass.hbrBackground = (HBRUSH) (COLORţBTNFACE + 1) ; SprawdŸ, jak działa teraz program BTNLOOK. System Windowţ, jeżeli pole hbr- Background struktury WNDCLASS ma zbyt małš wartoœć, rozumie, że okreœla ona kolor systemowy, a nie uchwyt. System wymaga, aby podana w tym polu war- toœć została powiększona o jeden. Jedynym powodem jest zabezpieczenie się przed pojawieniem się tu wartoœci NULL. Jeżeli okaże się, że w trakcie wykonywania programu kolor systemowy uległ modyfikacji, obszar roboczy zostanie unieważ- niony, a do jego ponownego wyœwietlenia wykorzystana zostanie nowa wartoœć ' koloru COLOR BTNFACE. Teraz jednak pojawił się inny problem. W trakcie wyœwietlania napisów za pomocš funkcji TextOut jako kolor tła wykorzystywa- na jest wartoœć zdefiniowana w aktualnym kontekœcie urzšdzenia (co powodu- je, że wymazane zostaje tło znajdujšce się w miejscu, w którym ma się pojawić tekst). Podobnie wybierany jest kolor samego tekstu. Wartoœci domyœlne to biały (tło) oraz czarny (tekst), niezależnie od kolorów systemowych oraz od zdefinio- wanego hbrBackground. Dlatego też musisz posłużyć się funkcjami SetTextColor oraz SetBkColor, dzięki którym będziesz mógł zmodyfikować odpowiednio ko- nor tekstu i jego tła. Pamiętaj jednak, że możesz to zrobić dopiero po pobraniu uchwytu kontekstu urzšdzenia: SetBkColor (hdc, GetSysColor (COLORţBTNFACE)) ; SetTextColor (hdc, GetSysColor (COLOR WINDOWTEXT)) ; Dopiero teraz kolory tła obszaru roboczego, tła tekstu oraz przycisków sš odpo- wiednio dobrane. Jeżeli jednak użytkownik zdecyduje się na zmianę kolorów sys- temowych w czasie, gdy pisany przez ciebie program działa, będziesz zmuszony do ponownego zmodyfikowania koloru tekstu i jego tła. Możesz to zrobić, poshx- gujšc się następujšcym kodem: case WM_SYSCOLORCHANGE: InvalidateRect (hwnd, NULL, TRUE) ; break; Komunikat WM CTLCOLORBTN Wiesz już, w jaki sposób można zsynchronizować kolor obszaru roboczego i wy- œwietlanego w nim tekstu z kolorem tła przycisku. A czy można zmienić kolory przycisków na takie, jakie wybraliœmy dla całego programu? Cóż, teoretycznie można, ale praktycznie - nie. Wiadomo, że nie będziesz mógł się w tym celu po- służyć funkcjš SetSysColors: jej działanie spowoduje modyfikacje kolorów wszyst- kich programów. Nie jest to chyba coœ, z czego użytkownik byłby zadowolony. Znacznie lepszym sposobem (przynajmniej teoretycznie) jest obsługa komunika- tu 4VMţCTLCOLORBTN. Przycisk wysyła ten komunikat do okna nadrzędnego, gdy narysowany ma zostać obszar roboczy okna potomnego. Okno nadrţędne może go wykorzystać do podmieniania kolorów, których kontrolka użyje do ry- sowania. (W 16-bitowych wersjach Windows wszystkie kontrolki posługiwały się 344 Częœó I: Podstawy i komunikatem WMţCTLCOLOR. Natomiast w nowych wersjach każda kontrol- ka ma swój własny komunikat). Gdy do procedury okna nadrzędnego dotrze komunikat WMţCTLCOLORBTN, w towarzyszšcym mu parametrze wParam przekazywany jest uchwyt kontekstu urzš- dzenia przycisku, natomiast w lParam - uchwyt okna przycisku. Oznacza to, że w momencie, kiedy procedura okna rozpoczyna obsługę tego komunikatu. przycisk ! kontrolki pobrał już swój kontekst urzšdzenia. W ramach obsługi komunikatu: i ů możesz ustawić kolor tekstu, posługujšc się funkcjš SetTextColor, ů możesz ustawić kolor tła, posługujšc się funkcjš SetBkColor, ů zwracasz do okna potomnego uchwyt przeznaczonego dla niego pędzla. Teoretycznie okno potomne posługuje się pędzlem do rysowania tła. Jesteœ od- powiedzialny za usunięcie pędzla, gdy tylko nie będzie już potrzebny. A oto pewien problem zwišzany z komunikatem WM CTLCOLORBTN: komu- nikat ten jest wysyłany do okna nadrzędnego jedynie przez przyciski naciskane oraz rysowane przez program. Jednakże tylko te drugie korzystajš z pędzla przy- gotowanego przez procedurę okna i rysujš za jego pomocš tło. Nie jest to zbyt użyteczne, ponieważ za narysowanie takiego przycisku i tak odpowiedzialny jest program. W dalszej częœci tego rozdziału zajmiemy się komunikatami podobnymi do 4VMţCTLCOLORBTN wysyłanymi jednak przez inne kontrolki. Przyciski rysowane przez program Jeżeli chcesz przejšć pełnš kontrolę nad wyglšdem przycisku, a jednoczeœnie nie chcesz zajmować się docierajšcymi do niego komunikatami klawiatury i myszy, możesz posłużyć się przyciskiem typu BS OWNERDRAW. Zostało to zademon- strowane w programie OWNDRAW przedstawionym na rysunku 9-3. OWNDRAW.C /* OWNDRAW.C - Owner-Draw - przykład przycisków rysowanych przez program (c) Charles Petzold, 1998 */ ţţinclude ţţdefine ID_SMALLER 1 lţdefine IDţLARGER 2 ţţdefine BTN WIDTH (8 * cxChar) ţţdefine BTN HEIGHT (4 * cyChar) LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; HINSTANCE hInst ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) Rozdział 9: Kontrolki okna potomnego 345 static TCHAR szAppNameC] = TEXT ("OwnOraw") ; MSG msg ; HWND hwnd ; WNDCLASS wndclass ; Y . ,I hInst = hInstance ; wndclass.style = CS_HREDRAW ţ CS VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) : wndclass.hCursor = LoadCursor (NULL, IDC ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITEţBRUSH) ; wndclass.lpszMenuName = szAppName : wndclass.lpszClassName = szAppName : if (!RegisterClass (&wndclass)) MessageBox (NULL, TEXT ("Program requires Windows NT!"), szAppName, MBţICONERROR) ; hwnd = CreateWindow (szAppName, TEXT ("Owner-Draw Button Demo"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW USEDEFAULT, CW_USEDEFAULT, CW USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWi.ndow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) ( TranslateMessage (&msg) ; DispatchMessage (&msg) ; ) return msg.wParam : ' void Triangle (HDC hdc, POINT ptC]) f SelectObject (hdc, GetStockObject (BLACK BRUSH)) ; Polygon (hdc, Pt, 3) : i SelectObject (hdc, GetStockObject (WHITEţBRUSH)) ; LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ( static HWND hwndSmaller, hwndLarger ; static int cxClient, cyClient, cxChar, cyChar ; int cx, cy ; LPDRAWITEMSTRUCT pdis ; POINT ptC3] ; RECT rc ; 346 Częœć I: Podstawy (cišg dalszy ze strony 345) switch (message) case WM_CREATE : . cxChar = LOWORD (GetOialogBaseUnits ()) ; cyChar = HIWORD (GetDialogBaseUnits ()) ; i // Stwórz przyciski rysowane przez program hwndSmaller = CreateWindow (TEXT ("button"), TEXT (""), I WS_CHILD ţ WS VISIBLE ţ BS_OWNERDRAW, 0, 0, BTN WIDTH, BTN_HEIGHT, hwnd, (HMENU) ID SMALLER, hInst, NULL) ; hwndLarger = CreateWindow (TEXT ("button"), TEXT (""), , WS_CHILD ţ WS_VISIBLE ţ BS_OWNERDRAW, 0, 0, BTN_WIDTH, BTN_HEIGHT, hwnd,ţ(HMENU) ID LARGER, hInst, NULL) ; return 0 ; case WM_SIZE : cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; i // Przenieœ przyciski do nowego œrodka ; I MoveWindow (hwndSmaller, cxClient / 2 - 3 * BTN WIDTH / 2, cyClient / 2 - BTN_HEIGHT / 2, BTN WIDTH, BTNţHEIGHT, TRUE) ; I MoveWindow (hwndLarger, cxClient / 2 + BTN_WIDTH / 2, ` cyClient / 2 - BTN HEIGHT / 2, BTN WIDTH, BTN HEIGHT, TRUE) ; return 0 ; case WM_COMMAND : GetWindowRect (hwnd, &rc) ; // Powiększ lub zmniejsz okno 0 10% , switch (wParam) ; case ID_SMALLER : rc.left += cxClient / 20 ; i rc.right -= cxClient / 20 ; rc.top += cyClient / 20 ; rc.bottom -= cyClient / 20 ; ; break ; case ID_LARGER : r rc left -= cxClient / 20 ; rc.right += cxClient / 20 ; rc.top -= cyClient / 20 ; rc.bottom += cyClient / 20 ; break ; ; Rozdział 9: Kontrolki okna potomnego 347 MoveWindow (hwnd, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, TRUE) ; ' return 0 ; case WM DRAWITEM : pdis = (LPDRAWITEMSTRUCT) lParam ; I , // Wypełnij obszar kolorem bialym, a następnie // wykreœl czarnţ ramkę FillRect (pdis->hDC, &pdis->rcItem, (HBRUSH) GetStockObject (WHITE BRUSH)) : FrameRect (pdis->hDC, &pdis->rcItem, (HBRUSH) GetStockObject (BLACK BRUSH)) ; i;, ' // Rysuj trójkšty skierowane na zwendtrz oraz // do œrodka cx = pdis->rcItem.right - pdis->rcItem.left ; cy = pdis->rcItem.bottom - pdis->rcItem.top . switch (pdis->CtIID) case ID_SMALLER : ptCO].x = 3 * cx / 8 ; ptCO].y = 1 * cy / 8 ; ptCl].x = 5 * cx / 8 ; ptCl].y = 1 * cy / B ; ptC2].x = 4 * cx / 8 : PtC2J.y = 3 * cy / 8 : Triangle (pdis->hDC, pt) ; ptCOJ.x = 7 * cx / 8 ; PtCOI.y = 3 * cy / 8 : ptCl].x = 7 * cx / 8 ; ptCl].y = 5 * cy / 8 ; ptC27.x = 5 * cx / 8 : PtC2].y = 4 * cy / 8 : Triangle (pdis->hDC, pt) : ptCO].x = 5 * cx / 8 ; ptCO].y = 7 * cy / 8 ; ptClJ.x = 3 * cx / 8 : PtCll.y = 7 * cy / 8 : ptC2J.x = 4 * cx / S ; ptC2].y = 5 * cy / 8 : Triangle (pdis->hDC, pt) ; ptCO].x = 1 * cx / 8 ; ptCO].y = 5 * cy / 8 ; ptCl].x = 1 * cx / 8 ; pt[1].y = 3 * cy / 8 ; ptC2J.x = 3 * cx / 8 : PtC2J.y = 4 * cy / 8 ; Triangle (pdis->hDC, pt) : break ; case ID_LARGER : _ ptCO].x = 5 * cx / 8 ; ptCO].y = 3 * cy / 8 : ptCl].x = 3 * cx / 8 ; ptCl].y = 3 * cy / 8 ; ptC2J.x = 4 * cx / 8 ; ptC2J.y = 1 * cy / 8 ; Triangle (pdis->hDC, pt) ; ',ţ. ptCO].x = 5 * cx / 8 : PtCO].y = 5 * cy / 8 ; 348 Częœć I: Podstawy (cišg dalszy ze strony 347) ptClJ.x = 5 * cx / 8 ; ptCl].y = 3 * cy / 8 ; ptC2J.x = 7 * cx / 8 ; PtC2J.y = 4 * cy / 8 ; Triangle (pdis->hDC, pt) ; ptCOJ.x = 3 * cx / 8 ; ptCOJ.y = 5 * cy / 8 ; ptClJ.x = 5 * cx / 8 ; ptClJ.y = 5 * cy / 8 ; ptC27.x = 4 * cx / 8 : PtC2J.y = 7 * cy / 8 ; Triangle (pdis->hDC, pt) ; ptCOJ.x = 3 * cx / 8 ; ptCOJ.y = 3 * cy / 8 ptClJ.x = 3 * cx / 8 ; ptCl].y = 5 * cy / 8 ; ptC2].x = 1 * cx / 8 ; ptC2].y = 4 * cy / 8 ; Triangle (pdis->hDC, pt) ; break ; 1 // Odwrócenie prostokdta, jeżeli przycisk zostal wybrany if (pdis->itemState & ODS_SELECTED) InvertRect (pdis->hDC, &pdis->rcItem) ; // Narysuj wokół przycisku ramkę symbolizujdcd fokus // jeœli przycisk ma fokus if (pdis->itemState & ODSţFOCUS) ( pdis->rcItem.left += cx / 16 ; pdis->rcItem.top += cy / 16 ; pdis->rcItem.right -= cx / 16 ; pdis->rcItem.bottom -= cy / 16 DrawFocusRect (pdis->hDC, &pdis->rcItem) ; ) return 0 ; case WM_DESTROY : PostOuitMessage (0> ; return 0 ; ) return DefWindowProc (hwnd, message, wParam, lParam) ; Rysunek 9-3. Program O4VNDRAW Program ten wyœwietla dwa przyciski na œrodku obszaru roboczego okna, jak zo- stało to przedstawione na rysunku 9-4. Przycisk znajdujšcy się po lewej stronie składa się z czterech trójkštów skierowanych do wnętrza przycisku. Kliknięcie tego przycisku spowoduje zmniejszenie okna o 10 procent. Z kolei przycisk z pra- wej strony składa się z czterech trójkštów zwróconych na zewnštrz. Jego kliknię- cie spowoduje powiększenie okna o 10 procent. Jeżeli na przycisku zamierzasz narysować tylko ikonę lub bitmapę, możesz po- służyć się stylem BS ICON lub BS BTTMAP, a sam obrazek ustawić za pomocš Rozdział 9: Kontrolki okna potomnego 349 komunikatu BM SETIMAGE. Przyciski BS OWNERDRAW pozwalajš jednakże na całkowitš dowolnoœć rysowania. 1ů1 ^ Rysunek 9-4. Okno programu OWNDRAW W trakcie obshzgi komunikatu WMţCREATE program OWNDRAW tworzy dwa przyciski BS OWNERDRAW. Ich szerokoœć równa jest oœmiokrotnej szerokoœci znaków czcionki systemowej, wysokoœć natomiast - czterokrotnej wysokoœci zna- ków czcionki systemowej. (Jeżeli do narysowania przycisku posługujesz się pre- definiowanš bitmapš, powinieneœ wiedzieć, że na obrazie generowanym przez kartę VGA przycisk ten będzie miał wymiary 64 na 64 piksele). Stworzone w ten sposób przyciski nie sš jeszcze umieszczone w odpowiednim miejscu. Dlatego też w ramach obshxgi komunikatu WM SIZE, program OWNDRAW umieszcza je, za pomocš funkcji MoveWindow, w centrum obszaru roboczego. Kliknięcie któregokolwiek z przycisków powoduje wygenerowanie komunikatu tNM-COMMAND. W ramach jego obsługi program OWNDRAW wywohţje funk- cję GetWindowRect, aby zapamiętać aktualne położenie i wielkoœć okna (a nie tyl- ko obszaru roboczego) w zmiennej typu RECT. Położenie okreœlane jest wzglę- dem ekranu. Następnie, zależnie od tego, czy kliknięty został lewy czy prawy przycisk, program OWNDRAW modyfikuje zawartoœć pól struktury RECT. Na koniec wreszcie, okno zostaje przesunięte z jednoczesnš zmianš wielkoœci dzięki wywołaniu funkcji MoveWindow. Powoduje to wygenerowanie kolejnego komu- nikatu WM-SIZE, dzięki czemu przyciski ponownie zostanš umieszczone w cen- trum obszaru roboczego. Gdyby program zajmował się tylko i wyłšcznie opisanymi przed chwilš komuni- katami, przyciski nie byłyby widoczne. Jeżeli przycisk stworzony został za pomo- cš stylu BS OWNERDRAW, za każdym razem, gdy konieczne jest jego odrysowa- nie, wysyła do okna nadrzędnego komunikat WM-DRAWITEM. Następuje to w chwili utworzenia przycisku, gdy zostanie naciœnięty lub zwolniony, gdy otrzymuje lub traci fokus oraz zawsze, gdy konieczne jest jego odœwieżenie. Parametr lParam towarzyszšcy komunikatowi WM-DRAWITEM jest wskaŸnilciem do struktury typu DRAWITEMSTRUCT. Program OWNDRAW przechowuje ten wskaŸnik w zmiennej pdis. Struktura DftAWITEMSTRUCT zawiera wszystkie 350 Częœć I: Podstawy dane potrzebne programowi do narysowania przycisku. (Ta sama struktura wy- korzystywana jest również przez pola listy i menu rysowane przez program). Z punktu widzenia programu, najważniejszymi polami struktury sš hDC (kon- tekst urzšdzenia przycisku), rcltem (struktura RECT definiujšca wielkoœć przyci- sku), CtIID (identyfikator okna kontrolki) oraz itemState (pole to okreœla, czy przy- cisk jest naciœnięty oraz czy ma fokus). Program OWNDRAW rozpoczyna obshxgę komunikatu WM_DRAWITEM od wywołania funkcji FiIlRect w celu zamalowania przycisku kolorem białym, a na- stępnie FrameRect, która rysuje czarnš ramkę dookoła przycisku. Każdy z czte- rech trójkštów rysowany jest przez program za pomocš funkcji Polygon. Jeżeli komunikat wygenerowany został na skutek naciskania przycisku, ustawiony będzie jeden z bitów pola itemState struktury DRAWITEMSTRUCT. Jego stan możesz sprawdzić, posługujšc się stałš ODS SELECTED. Jeżeli bit jest ustawio- ny, OWNDRAW odwraca kolory przycisku za pomocš funkcji InvertRect. Jeżeli przycisk ma fokus, ustawiony będzie bit ODS FOCUS pola itemState. W tym przy- padku program rysuje we wnętrzu przycisku przerywanš ramkę za pomocš funk- cji DrawFocusRect. Jeszcze tylko jedno ostrzeżenie skierowane do wszystkich tych, którzy posługujš się przyciskami rysowanymi przez program: system Windows pobiera dla ciebie kontekst urzšdzenia zwišzany z przyciskiem i umieszcza go w jednym z pól struk- tury DRAWITEMSTRUCT. Kontekst ten powinieneœ zostawić dokładnie w takim samym stanie, w jakim go zastałeœ. Wszelkie obiekty GDI, które zostały dla niego wybrane, powinny zostać zwolnione. Powinieneœ również uważać, rysujšc ram- kę przycisku, aby nie pojawiła się ona na zewnštrz. Klasa statyczna Statyczne okna kontrolek potomnych tworzymy używajšc nazwy "static" w kla- sie okna przekazywanej do funkcji CreateWindow. Sš to bardzo "spokojne" okna potomne. Nie korzystajš z żadnych informacji generowanych przez mysz lub kla- wiaturę, nie wysyłajš też komurukatów WM COMMAND do procedury okna nadrzędnego. Jeœli umieœcisz kursor myszy nad statycznym oknem potomnym lub klikniesz je, przechwyci ono komunikat WM NCHITTEST i zwróci do systemu wartoœć HTTRANSPARENT. Windows przekazuje wówczas dokładnie ten sam komuni- kat WMţNCHITTEST do okna, które znajduje się pod kontrolkš. Zwykle oknem tym będzie okno nadrzędne. Również to okno przekazuje ten komunikat do funkcji Def tNindowProc, gdzie poddawany jest on konwersji na jeden z komunikatów myszy obszaru roboczego. Pierwsze szeœć statycznych stylów okna rysuje po prostu w obszarze roboczym okna potomnego prostokšt lub ramkę. Style statyczne "RECT" (lewa kolumna poniżej) sš wypełnionymi prostokštami. Natomiast style "FRAME" (prawa ko- lumna poniżej) - to po prostu kontury prostokšta niczym nie wypełnione. Rozdział 9: Kontrolki oknţ potomnego 351 SS BLACKRECT SS BLACKFRAME SS GRAYRECT SS GRAYFRAME SS WHITERECT SS WHITEFRAME Słowa "BLACK", "GRAY" oraz "WHITE" nie oznaczajš, że wykorzystywanymi kolorami będš odpowiednio czarny, szary i biały. Wykorzystane raczej zostanš kolory systemowe: Kolor statyczny Kolor systemowy BLACK COLOR 3DDKSHADOW GIZAy COLOR BTNSHADOW WHITE COLOR BTNHIGHLIGHT W przypadku tych stylów parametr funkcji CreateWindow okreœlajšcy tekst okna jest ignorowany. Lewy górny róg ramki znajduje się w punkcie o współrzędnych x i y względem okna nadrzędnego. Jeżeli posłużysz się stylem SS ETCHEDHORZ, SS_ETCHEDVERT lub SS ETCHEDFRAME stworzona zostanie biała lub szara cieniowana ramka. W skład klas statycznych wchodzš również trzy style tekstowe: SS LEFT, SS RI- GHT oraz SS CENTER. Tworzš one tekst odpowiednio dosunięty do lewej kra- wędzi, prawej krawędzi lub wyœrodkowany. Napis, który ma się pojawić w kon- trolce, przekazywany jest do funkcji CreateWindow jako tekst okna. Możliwa jest póŸniejsza jego modyfikacja za pomocš SetWindowText. Aby wyœwietlić ten tekst, procedura okna kontrolki statycznej wywołuje DrawText, przekazujšc jej parametry DT WORDBREAK, DT NOCLIP oraz DT EXPANDTABS. Powoduje to, że tekst ten jest zawijany w ramach prostokšta kontrolki. Jako kolor tła kontrolki wykorzystywany jest zwykle COLOR BTNFACE, nato- miast sam tekst wyœwietlany jest za pomocš COLOR WINDOWTEXT. Jeżeli chcesz zmodyfikować któryœ z tych kolorów, powinieneœ obsłużyć komunikat WM CTLCOLORSTATIC. Następnie, aby zmodyfikować kolor tekstu, wywołaj funkcję SetTextColor . Jeżeli chcesz zmienić kolor tła, wywołaj SetBkColor. Pamię- taj również, aby zwrócić uchwyt pędzla tła. Jak to się robi, zobaczysz za chwilę w programie COLORSl. Na koniec wreszcie, w skład klasy statycznej wchodzš również style SS ICON oraz SS USERITEM. Nie majš one jednak żadnego znaczenia, gdyż wykorzysty- wane sš jako kontrolki okna potomnego. Dlatego też powrócimy do nich przy okazji okien dialogowych. Klasa paska przewijania Gdy w rozdziale 4 po raz pierwszy pojawił się problem pasków przewijania, przedstawiłem pewne różnice między paskami przewijania okna a kontrolkami pasków przewijania. Program SYSMETS posługuje się paskami przewijania okna, które umieszczane sš wzdłuż jego prawej i dolnej krawędzi. Aby się pojawiły, 352 Częœć I: Podstawy wystarczy do funkcji tworzšcej okno dołšczyć jeden lub oba identyfikatory stylu: WS VSCROLL oraz WS HSCROLL. W tej chwili gotowi już jesteœmy do użycia kontrolek pasków przewijania, które - będšc oknami potomnymi - mogš poja- wić się w dowolnym miejscu obszaru roboczego okna nadrzędnego. Kontrolki tego typu tworzy się za pomocš klasy "scrollbar" oraz jednego z dwóch stylów: SBS VERT lub SBS HORZ. W przeciwieństwie do kontrolek przycisków (oraz kontrolek edycji i list, którymi zajmiemy się póŸniej), kontrolki paska przewijania nie wysyłajš do okna nadrzęd- nego komunikatu WM COMMAND. Zamiast tego generujš WM-VSCROLL lub WM-HSCROLL. Działajš więc dokładnie w taki sam sposób, jak paski przewija- nia okna. Obshzgujšc komunikaty zwišzane z paskami przewijania, możesz okre- œlić, czy dotyczš one pasków przewijania okna czy też kontrolek pasków przewi- jania, sprawdzajšc wartoœć przekazanš za pomocš parametru lParam. W pierw- szym przypadku będzie on miał wartoœć 0, w drugim natomiast - będzie zawie- rał uchwyt okna kontrolki przewijania. Co do wParam - w obu przypadkach star- sze i młodsze słowo tego parametru majš takie same znaczenie. Mimo że paski przewijania okna majš stałš szerokoœć, paski przewijania kontro- lki wypełniajš cała powierzchnię prostokšta podanego w wywołaniu funkcji Cre- ateWindow (lub póŸniej funkcji MoveWindow). Możesz więc tworzyć paski zarów- no dhzgie i cienkie, jak i krótkie i grube. Jeżeli chcesz stworzyć kontrolki paska przewijania o takich samych wymiarach jak paski przewijania okna, do pobrania wysokoœci paska poziomego możesz posłużyć się funkcjš GetSystemMetrics: GetSystemMetrics (SM CYHSCROLL) ; natomiast do pobrania szerokoœci pionowego paska: GetSystemMetrics (SMţCXVSCROLL) ; Identyfikatory stylu paska przewijania okna SBS LEFTALIGN, SBS_RIGHTA- LIGN, SBS TOPALIGN oraz SBS_BOTTOMALIGN pozwalajš na stworzenie pa- sków o standardowych wymiarach. Niestety tak się składa, że działajš one jedy- nie wtedy, gdy pasek umieszczony jest w oknie dialogowym. Zarówno zakres, jak i pozycję kontrolki paska przewijania możesz ustawiać za pomocš tych samych funkcji, z których korzystałeœ w przypadku pasków prze- wijania okna: SetScrollRange (hwndScroll, SB_CTL, iMin, iMax, bRedraw) SetScrollPos (hwndScroll, SB_CTL, iPos, bRedraw) ; SetScrollInfo (hwndScroll, SB CTL, &si, bRedraw) ; Jedyna różnica polega na tym, że paski przewijania okna posługujš się uchwy- tem okna nadrzędnego, natomiast jako drugi parametr funkcji przekazujš SB VER lub SB HORZ. - Najzabawniejsze jest to, że nie jest już wykorzystywany kolor systemowy o na- zwie COLOR SCROLLBAR. Do rysowania suwaka oraz przycisków znajdujšcych się na końcach paska używane sš kolory COLOR BTNFACE, COLOR BTNHI- GHLIGH, COLOR BTNSHADOW, COLOR BTNTEXT (mała strzałka), CO- LOR BTNSHADOW oraz COLOR BTNLIGHT. Z kolei kolor obszaru paska znaj- Rozdział 9: Kontrolki okna potomnego 353 dujšcy się pomiędzy przyciskami tworzony jest na podstawie kolorów CO- LOR BTNFACE oraz COLOR BTNHIGHLIGHT. Jeżeli w procedurze okna obsłużysz komunikat WM CTLCOLORSCROLLBAR, będziesz mógł zwrócić uchwyt pędzla, który zostanie wykorzystany do naryso- wania tego obszaru. Zróbmy więc to. Program COLORS1 Przedstawiony na rysunku 9-5 program COLORSI pozwoli nam dokładniej przyj- rzeć się kilku ciekawym zastosowaniom pasków przewijania i kontrolek statycz- nych oraz dokładniej zapoznać się z wykorzystaniem kolorów systemowych. W lewej częœci okna tworzonego przez program wyœwietlane sš trzy paski przewi- jania. Noszš one nazwy "Czerwony", "Zielony oraz "NiebieslW . Przewijanie suwaka któregokolwiek z pasków zmienia kolor tła obszaru roboczego, który po- wstaje ze złożenia tych trzech składowych. Liczbowa wartoœć każdej z nich wy- œwietlana jest pod odpowiednim paskiem przewijania. COLORSl.C /* COLORSl.C - Sterowanie kolorami za pomocd pasków przewijania (c) Charles Petzold, 1998 */ ţpinclude LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; LRESULT CALLBACK ScrollProc (HWND, UINT, WPARAM, LPARAM) : int idFocus ; WNDPROC OldScrollC37 : int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) static TCHAR szAppNameC7 = TEXT ("Colorsl") : HWND hwnd : MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CS UREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI ŽPPLICATION) wndclass.hCursor = LoadCursor (NULL, IDC ARROW) : wndclass.hbrBackground = CreateSolidBrush (0) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) ( 354 Częœć Iţ Podstawy (cišg dalszy ze strony 353) MessageBox (NULL, TEXT ("Program requires Windows NT!"), szAppName, MBţICONERROR) ; return 0 ; } hwnd = CreateWindow (szAppName, TEXT ("Color Scroll"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd. iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) ( TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; } LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ( static COLORREF crPrim[3] = t RGB (255, 0, 0), RGB (0, 255, 0), RGB (0, 0, 255) } ; static HBRUSH hBrush[37, hBrushStatic ; static HWND hwndScroll[3] hwndLabel[3], hwndValue[3], hwndRect ; static int color[3], cyChar ; static RECT rcColor ; static TCHAR * szColorLabel[] = t TEXT ("Red"), TEXT ("Green"), TEXT ("Blue") } ; HINSTANCE hInstance ; int i, cxClient, cyClient ; TCHAR szBuffer[107 ; switch (message) ( case WM_CREATE : hInstance = (HINSTANCE) GetWindowLong (hwnd, GWL HINSTANCE) ; // Stwórz biały prostokšt, na tle którego wyœwietlane będd // paski przewijania. Identyfikator okna potomnego to 9. hwndRect = CreateWindow (TEXT ("static"), NULL, WS_CHILD ţ WSţVISIBLE ţ SS WHITERECT, 0, 0, 0, 0, hwnd, (HMENU) 9, hInstance, NULL) ; for (i = 0 ; i < 3 ; i++) ( // Identyfikatory pasków przewijania to 0, 1 oraz 2. // Każdy z pasków ma zakres od 0 do 255. hwndScroll[i] = CreateWindow (TEXT ("scrollbar"), NULL, WS_CHILD ţ WS_VISIBLE WSţTABSTOP ţ SBSţVERT, Rozdział 9: Kontrolki okna potomnego 355 o, o, o, o, hwnd, (HMENU) i, hInstance, NULL) ; SetScrollRange (hwndScroll[i], SB_CTL, 0, 255, FALSE) ; SetScrollPos (hwndScroll[i], SB CTL, 0, FALSE) ; // Trzy etykiety z nazwami kolorów majš identyfikatory 3, 4, i 5. // Wyœwietlane przez nie teksty to "Czerwony", "Zielony" i "Niebieski" hwndLabel Ci] = CreateWindow (TEXT ("static"), szColorLabelCi], WS_CHILD ţ WSţVISIBLE ţ SS CENTER, 0, 0, 0, 0, hwnd, (HMENU) (i + 3), hInstance, NULL) ; // Trzy kontrolki wyœwietlajdce wartoœć danego koloru majš // identyfikatory 6, 7 oraz B.Poczatkowo wyœwietlany tekst ,, // to "0". hwndValue Ci] = CreateWindow (TEXT ("static"), TEXT ("0"), WS_CHILD ţ WS VISIBLE ţ SS CENTER, 0, 0, 0, 0, hwnd, (HMENU) (i + 6), hInstance, NULL) ; OldScrollCi] = (WNDPROC) SetWindowLong (hwndScrollCi], GWL WNDPROC, (LONG) ScrollProc) ; hBrushCi] = CreateSolidBrush (crPrim[i]) ; hBrushStatic = CreateSolidBrush ( GetSysColor (COLOR BTNHIGHLIGHT)) ; cyChar = HIWORD (GetDialogBaseUnits ()) ; return 0 ; case WM_SIZE : cxClient = LOWORD (lParam) ; ' i. cyClient = HIWORD (lParam) ; SetRect (&rcColor, cxClient / 2, 0, cxQlient, cyClient) ; MoveWindow (hwndRect, 0, 0, cxClient / 2, cyClient, TRUE) ; for (i = 0 ; i < 3 ; i++) MoveWindow (hwndScrollCi], (2 * i + i) * cxClient / 14, 2 * cyChar, cxClient / 14, cyClient - 4 * cyChar, TRUE) ; ;; MoveWindow (hwndLabelCi], (4 x i + 1) * cxClient / 28, cyChar / 2, cxClient / 7, cyChar, TRUE) ; MoveWindow (hwndValue[i], (4 * i + 1).* cxClient / 28, cyClient - 3 * cyChar / 2, cxClient / 7, cyChar, TRUE) ; 356 Częœć I, Podstawy (cišg dalszy ze strony 355) 1 SetFocus (hwnd) ; return 0 ; case WM SETFOCUS : SetFocus (hwndScroll[idFocus]) ; return 0 ; case WM VSCROLL : i = GetWindowLong ((HWND) lParam, GWLţID) ; switch (LOWORD (wParam)) f case SB_PAGEDOWN : color[i] += 15 ; // przejdŸ dalej case SB_LINEDOWN : color[i] = min (255, color[i] + 1) ; break ; case SB_PAGEUP : color[i] -= 15 ; // przejdŸ dalej case SB_LINEUP : color[i] = max (0, color[i] - 1) ; break ; case SB_TOP : color[i] = 0 ; break ; case SB_BOTTOM : color[i] = 255 ; break ; case SB_THUMBPOSITION : case SB_THUMBTRACK : color[i] = HIWORD (wParam) ; break ; default : break ; ) SetScrollPos (hwndScroll[i], SB CTL, color[i], TRUE) ; wsprintf (szBuffer, TEXT ("ţ6i"), color[i]) ; SetWindowText (hwndValue[i], szBuffer) ; Delete0bject ((HBRUSH) SetClassLong (hwnd, GCLţHBRBACKGROUND, (LONG) CreateSolidBrush (RGB (color[0], color[1], color[2])))) ; InvalidateRect (hwnd, &rcColor, TRUE) ; return 0 ; case WM CTLCOLORSCROLLBAR : i = GetWindowLong ((HWND) lParam, GWLţID) ; return (LRESULT) hBrush[i] ; Rozdzial 9: Kontrolki okna potomnego 357 case WM_CTLCOLORSTATIC : i = GetWindowLong ((HWND) lParam, GWL ID) ; if (i >= 3 && i <= 8) // statyczne kontrolki tekstowe ' ( i i. SetTextColor ((HDC) wParam, crPrimCi % 3]) ; SetBkColor ((HDC) wParam, GetSysColor (COLOR BTNHIGHLIGHT)); return (LRESULT) hBrushStatic ; break ; case WM_SYSCOLORCHANGE : Delete0bject (hBrushStatic) ; hBrushStatic = CreateSolidBrush (GetSysColor (COLOR BTNHIGHLIGHT)) ; return 0 ; case WM_DESTROY : Delete0bject ((HBRUSH) SetClassLong (hwnd, GCL_HBRBACKGROUND, (LONG) GetStockObject (WHITE BRUSH))) ; for (i = 0 ; i < 3 ; i++) Delete0bject (hBrushCi]) ; i Delete0bject (hBrushStatic> : PostOuitMessage (0) ; return 0 ; l return DefWindowProc (hwnd, message, wParam, lParam) ; i LRESULT CALLBACK ScrollProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) i int id = GetWindowLong (hwnd, GWLţID) ; switch (message) case WM_KEYDOWN : if (wParam --- VK_TAB) SetFocus (GetDlgItem (GetParent (hwnd), (id + (GetKeyState (UKţSHIFT) < 0 ? 2 : 1)) % 3)) ; break ; case WM_SETFOCUS : idFocus = id ; break ; ) return CallWindowProc (OldScroll[id], hwnd, message. wParam, lParam) ; ) Rysunek 9-5. Program COLORSI Program COLORSI zaprzšgł do pracy swoje 10 okien potomnych: 3 paski prze- wijania, 6 tekstów statycznych oraz 1 statyczny prostokšt. Program obsługuje komunikat 4VMţCTLCOLORSCROLLBAR, dzięki któremu może zmieniać kolor wnętrza pasków przewijania sterujšcych natężeniem poszczególnych kolorów 358 Częœć I: Podstawy składowych. Natomiast komunikat WMţCTLCOLORSTATIVC, pozwala progra- mowi nadać odpowiedni kolor tekstom statycznym. Paski przewijania możesz przewijać, posługujšc się zarówno myszš, jak i klawia- turš. Program COLORS1 możesz wykorzystać do wyboru kolorów - mniej lub bardziej atrakcyjnych, którymi będziesz poshzgiwał się w pisanych przez siebie aplikacjach. Okno programu COLORS1 znajduje się na rysunku 9-6. Ze wzglę- dów technicznych przedstawione zostało w odcieniach szaroœci. Rysunek 9-6. Okno programu COLORS1 Program COLORSI nie obshzguje komunikatów 4VM-PAINT. Po prostu cała praca zwišzana z kolorami wykonywana jest w oknach potomnych. Kolor prawej częœci okna programu jest kolorem tła okna. Statyczne okno potomne SS WHITERECT zajmuje lewš częœć okna. Paski przewijania to kontrolki okien potomnych utworzone z wykorzystaniem stylu SBS VERT. Znajdujš się one w ob- szarze zajmowanym przez kontrolkę SS WHITERECT. Pozostałe szeœć kontro- lek statycznych jest typu SS CENTER (tekst wyœrodkowany). Za ich pomocš wyœwietlane sš nazwy poszczególnych kolorów i ich aktualne wartoœci. Program COLORSI w funkcji WinMain najpierw tworzy swoje zwykłe okna, a następnie 10 okien potomnych. Posługuje się w tym celu funkcjš CreateWindow. Okna sta- tyczne SS WHITERECT oraz SS_CENTER należš do klasy "static"; paski prze- wijania należš do klasy "scrollbar" Przekazywane do funkcji CreateWindow parametry x, y oraz wysokoœć i szerokoœć poczštkowo majš wartoœci 0, ponieważ zależš od wielkoœci obszaru roboczego okna programu, który w momencie tworzenia kontrolek nie jest jeszcze znany. Gdy do procedury okna dotrze komunikat WMţSIZE, poszczególne kontrolki przesuwane sš na właœciwe miejsca za pomocš funkcji MoveWindow. Dlatego też, za każdym razem, gdy zmienisz wielkoœć okna programu, proporcjonalnie zmo- dyfikowana zostanie wielkoœć pasków przewijania. Gdy procedura okna WndProc odbierze komunikat WMţVSCROLL, ze starszego słowa parametru lParam może pobrać ona uchwyt okna potomnego, które spo- Rozdział 9: Kontrolki okna potomnego 359 wodowało wygenerowanie komunikatu. Posługujšc się funkcjš GetWindowWord, można pobrać jego identyfikator: i := GetWindowLong ((HWND) lParam, GWLţID) ; Poszczególne paski przewijania majš identyfikatory równe 0, 1 oraz 2, dzięki cze- mu procedura okna jest w stanie okreœlić, który wygenerował komunikat. Ponieważ w momencie tworzenia poszczególnych okien potomnych odpowia- dajšce im uchwyty zapamiętywane były przez program w tablicach, funkcja WndProc może obsługiwać komunikaty pasków przewijania oraz ustawiać nowe położenie ich suwaków za pomocš SetScrollPos: SetScrollPos (hwndScrolCi], SB CTL, color[i], TRUE) ; WndProc zajmuje się także odpowiedniš modyfikacjš tekstu wyœwietlanego przez okna potomne znajdujšce się pod paskami przewijania: wsprintf (szBuffer, TEXT ("%1"), colorCi]) ; SetWindowText (hwndValue[i], szBuffer) ; Automatyczny interfejs klawiatury Kontrolki pasków przewijania mogš również reagować na naciskanie klawiszy. Zawartoœć poniższej tablicy pokazuje, w jaki sposób poszczególne klawisze kur- sora tłumaczone sš na komunikaty paska przewijania: Klawisz kursora Wartoœć wParam komunikatu paska przewijania [Home] SB TOP [End] SB BOTTOM [Page Up] SB PAGEUP [Page Down] SB PAGEDOWN [F] lub [T] SB LINEUP [ţ] lub [ţL] SB LINEDOWN Tak naprawdę komunikaty SB TOP oraz SB BOTTOM mogš być generowane wyłšcznie za pomocš klawiatury. Jeżeli chcesz, aby kontrolka paska przewijania otrzymała fokus po kliknięciu myszš, musisz podać identyfikator WS TABSTOP jako parametr klasy w wywołaniu CreateWindow. Migotanie szarego prostokšta na suwaku sygnalizuje, że pasek otrzymał fokus. Jeżeli jednak chcesz dodać do pasków przewijania kompletnš obsługę klawiatu- ry, musisz się trochę napracować. Po pierwsze, procedura okna WndProc musi jawnie przekazać fokus do paska przewijania. Może to zrobić za pomocš komu- nikatu WM SETFOCUS, który dociera do okna nadrzędnego za każdym razem, gdy otrzymuje ono fokus. W ramach jego obsługi, procedura okna powinna prze- kazać fokus wybranemu paskowi. SetFocus (hwndScroll[idFocus]) ; Musisz jednak zaimplementować także jakiœ sposób przenoszenia fokusa pomię- dzy jednym paskiem przewijania a kolejnym. Prawdopodobnie zdecydujesz się w tym celu wykorzystać klawisz [Tab]. To zadanie jest już bardziej skompliko- 360 Częœć I: Podstawy wane, ponieważ od chwili otrzymania fokusu pasek przewijania obsługuje wszel- kie komunikaty zwišzane z klawiaturš. Obsługa ta ogranicza się jednak wyłšcz- nie do klawiszy kursora, klawisz [Tab] jest najzwyczajniej ignorowany. Rozwiš- zanie tego problemu kryje się w technice zwanej zakładaniem podklasy okna (ang. window subclassing). Posłużymy się niš, aby w programie COLORS1 uzyskać prze- chodzenie z jednego paska do kolejnego za pomocš klawisza [Tab]. Zakładanie podklasy okna Procedura okna kontrolki paska przewijania "zaszyta" jest gdzieœ we wnętrzu systemu Windows. Jednakże poshxgujšc się funkcjš GetWindowLong wywoływa- nš z identyfikatorem GWL WNDPROC, możesz pobrać jej adres. Co więcej, za pomocš funkcji SetWindowLong możesz zdefiniować nowš funkcję kontrolki! Tech- nika ta, okreœlana mianem zakładania podklasy okna, daje programiœcie ogrom- ne możliwoœci. Pozwala na podłšczenie się do istniejšcych procedur okna, obsłu- gę wybranych komunikatów, a następnie na przekazanie pozostałych do starej procedury okna. Procedura okna, która w programie COLORSI odpowiada za wstępnš obsługę komunikatów pasków przewijania, nazywa się ScrollProc. Znajduje się ona na sa- mym końcu tekstu programu COLORSl. Ponieważ jest funkcjš wywoływanš przez system Windows, musi być zdefiniowana jako CALLBACK. Program COLORSI posługuje się funkcjš SetWindowLong, aby dla każdego z trzech pasków przewijania ustawić nowš procedurę okna i jednoczeœnie pobrać adres dotychczasowej: OldScroll[i] = (WNDPROC) SetWindowLong (hwndScroll[i], GWL WNDPROC, (LONG) ScrollProc)) ; Od tego momentu wszystkie komunikaty wysyłane przez system Windows do pasków przewijania przetwarzane sš przez funkcję ScrollProc. Nie ma to oczywi- œcie żadnego wpływu na paski w innych programach. Zadaniem funkcji jest prze- niesienie fokusu do następnego lub poprzedniego okna, jeżeli dotrze do niej ko- munikat zwišzany z naciœnięciem klawisza [Tab] lub [Shift+Tab]. Na koniec, za pomocš CalIWindowProc wywoływana jest standardowa procedura okna paska przewijania. Kolorowanie tła Gdy program COLORSI definiuje klasę swego okna, do wypełniania tła obszaru roboczego stosuje jednolicie czarny pędzel: wndclass.hbrBackground = CreateSolidBrush (0) ; Gdy użytkownik modyfikuje ustawienia pasków przewijania, program musi za- reagować na to, tworzšc nowy pędzel i wstawiajšc go do klasy okna. Dokładnie w taki sam sposób, jak pobieraliœmy i ustawialiœmy procedurę okna pasków za pomocš funkcji GetWindowLong oraz SetWindowLong, możemy również pobrać i ustawić uchwyt pędzla. Tym razem jednak musimy posłużyć się funkcjami Get- WindowWord oraz SetWindowWord. Rozdział 9: Kontrolki okna potomnego 361 Posługujšc się poniższym wyrażeniem, możesz usunšć uprzednio używany przez klasę pędzel i jednoczeœnie przypisać jej nowy: Delete0bject ((HBRUSH) SetClassLong (hwnd, GCL HBRBACKGROUND, (LONG) CreateSolidBrush (RGB (colorCO], colorCl7. colorC2])))) ; Następnym razem, gdy konieczne będzie ponowne wyœwietlenie tła okna, sys- tem Windows posłuży się nowym pędzlem. Aby z kolei zmusić system do od- œwieżenia tła, należy unieważnić prawš częœć obszaru roboczego: InvalidateRect (hwnd, &rcColor, TRUE) ; Wartoœć TRUE (niezerowa) podana jako trzeci parametr funkcji oznacza, że przed odœwieżeniem tło ma zostać wymazane. Funkcja InvalidateRect zmusza Windows do umieszczenia komunikatu WM PA- INT w kolejce komunikatów przekazywanych do procedury okna programu. Po- nieważ WMţPAINT posiada stosunkowo niski priorytet, nie zostanie obshxżony natychmiast, jeżeli za pomocš myszy lub klawiatury przesuwać będziesz suwaki pasków. Jeżeli jednak zależy ci na natychmiastowym odœwieżeniu okna po zmianie kolorów, możesz dodać następujšce wyrażenie: UpdateWindow (hwnd) ; tuż po funkcji InvalidateRect. Może to jednak sprawić, że program wolniej będzie reagował na mysz i klawiaturę. Procedura okna WndProc programu COLORS1 nie obsługuje bezpoœrednio ko- munikatu WMţPAINT, tylko przekazuje go do Def 4VindowProc. Domyœlna obshx- ga tego komunikatu oferowana przez Windows obejmuje po prostu wywołanie funkcji BeginPaint oraz EndPaint, co zatwierdza okno. Ponieważ w wywołaniu InvalidateRect nakazaliœmy wymazanie tła, funkcja BeginPaint powoduje, że wy- generowany zostaje komunikat WM ERASEBACKGROLTND (czyszczenie tła). Również on jest ignorowany przez procedurę okna WndProc. Jego obsługę przej- muje więc Windows, usuwajšc zawartoœć pulpitu za pomocš pędzla zdefiniowa- nego w klasie okna. Do dobrych manier programistycznych należy posprzštanie po programie, za- nim zakończy on swojš pracę. Dlatego też, w ramach obsługi komunikatu WM DESTROY, zostaje jeszcze raz wywołana funkcja DeleteOhject: Delete0bject ((HBRUSH) SetClassLong (hwnd, GCL_HBRBACKGROUND, (LONG) GetStockObject (WHITE BRUSH))) ; Kolorowanie pasków przewijania i tekstów statycznych W programie COLORSI wnętrza pasków przewijania oraz teksty w szeœciu po- lach tekstów statycznych sš pokolorowane na czerwono, zielono oraz niebiesko. Kolorowanie pasków przewijania zrealizowane zostało w ramach obsługi komu- nikatu WMţCTLCOLORSCROLLBAR. W funkcji WndProc zdefiniujmy statycznš tablicę, w której przechowywać będzie- my uchwyty pędzli: static HBRUSH hBrush [3] ; 362 Częœć I: Podstawy W ramach obsługi komunikatu WMţCREATE, możemy stworzyć potrzebne nam pędzle: for ( I = 0 ; I < 3 ; I++) hBrushCI] = CreateSolidBrush (crPrim CI]) ; gdzie crPrim to tablica zawierajšca wartoœci RGB trzech podstawowych kolorów. W ramach obsługi komunikatu WMţCTLCOLORSCROLLBAR procedura okna zwraca jeden z utworzonych w ten sposób pędzli: case WM CTLCOLORSCROLLBAR: i = GetWindowLong ((HWND) lParam, GWLţID) ; return (LRESULT) hBrush Ci] ; Przed zakończeniem działania programu, w ramach obsługi komunikatu WM DE- STROY, wszystkie pędzle muszš zostać usunięte: for ( i = 0 ; i < 3 ; i++) Delete0bject (hBrush Ci]) ; Pola tekstów statycznych kolorowane sš w analogiczny sposób. W ramach ob- sługi komunikatu WMţCTLCOLORSTATIC wywoływana jest funkcja SetTextCo- lor. Kolor tła tekstu ustawiany jest za pomocš SetBkColor wywoływanej z para- metrem COLOR BTNHIGHLIGHT. Powoduje to, że kolor ten będzie taki sam jak kolor ramki statycznej, wewnštrz której umieszczone zostały zarówno same teksty, jak i paski przewijania. W przypadku tekstów statycznych kolor tła poja- wia się jedynie pod każdym wyœwietlanym znakiem, a nie w całym zajmowa- nym przez kontrolkę prostokšcie. Aby sobie z tym poradzić, procedura okna musi zwrócić również uchwyt pędzla koloru COLOR BTNHIGHLIGHT. Pędzel ten nosi nazwę hBrushStatic i tworzony jest w ramach obsługi komunikatu WM-CREATE. Oczywiœcie, obshzgujšc komunikat WM DESTROY, należy go usunšć. Tworzšc w ramach obsługi WM-CREATE pędzel oparty o kolor COLOR BTN- HIGHLIGHT i używajšc go aż do końca pracy programu, napotkamy na niewielki problem. Jeżeli w międzyczasie zmodyfikowany zostanie kolor COLOR BTNHI- GHLIGHT, zmieni się zarówno kolor ramki statycznej, jak i kolor tła pod tekstem, jednak całe tło kontrolki nie pozostanie zmienione. Aby sobie z tym poradzić, program COLORSI obsługuje również komunikat WM-SYSCOLORCHANGE, zmieniajšc po prostu pędzel hBrush. Klasa edycji Klasa edycji jest pod pewnymi względami jednš z najprostszych klas, a pod in- nymi -jednš z najbardziej skomplikowanych. Gdy za pomocš funkcji CreateWin- dow tworzysz okno potomne posługujšc się klasš "edit", definiujesz prostokšt o współrzędnych x i y lewego górnego rogu oraz danej wysokoœci i szerokoœci. W jego wnętrzu będzie możliwa edycja tekstu. Gdy okno potomne ma fokus, mo- żesz wpisywać tekst, przesuwać kursor, zaznaczać jego fragmenty posługujšc się myszš lub klawiszami kursora i klawiszem [Shift], usuwać zaznaczony tekst do Schowka, naciskajšc [Ctrl+X], kopiować za pomocš [Ctrl+C] lub wreszcie wsta- wiać do kontrolki tekst ze Schowka, naciskajšc [Ctrl+V]. Rozdziaf 9: Kontrolki okna potomnego 363 Jednowierszowe pole wprowadzania danych jest przykładem wykorzystania naj- prostszej kontrolki edycji. Zostało przedstawione w programie POPPAD1 na ry- sunku 9-7. Podobnie, jak miało to miejsce z innymi przykładami, również POPPADI - w miarę poznawania przez nas nowych zagadnień - zostanie wzbogacony o menu, okna dialogowe (pozwalajšce na zapis i wczytanie pliku) oraz drukowanie. Osta- teczna jego wersja będzie prostym, ale pełnym edytorem. Będziesz zaskoczony, jak duże będš jego możliwoœci w porównaniu z kodem, który musiałeœ napisać. POPPADl.C /* POPPADl.C - Prosty edytor poslugujacy się potomnym oknem edycji (c) Charles Petzold, 1998 */ iţinclude ţţdefine IDţEDIT 1 LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM); TCHAR szAppNameC] = TEXT ("PopPadl") ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hRrevInstance, PSTR szCmdLine, int iCmdShow) HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CS-VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE-BRUSH) wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) ( MessageBox (NULL, TEXT ("Program requires Windows NT!"), szAppName, MBţICONERROR) ; return 0 ; ) hwnd = CreateWindow (szAppName, szAppName, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; 364 Częœć 1: Podstawy (cišg dalszy ze strony 363) while (GetMessage (&msg, NULL, 0, 0)) ( TranslateMessage (&msg) ; DispatchMessage (&msg) ; 1 return msg.wParam ; ) LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ( static HWND hwndEdit ; switch (message) ( case WM_CREATE : hwndEdit = CreateWindow (TEXT ("edit"), NULL, WSţCHILD ţ WS_VISIBLE ( WS_HSCROLL ţ WS VSCROLL WS_BORDER ( ES_LEFT ţ ES_MULTILINE ES_AUTOHSCROLL ţ ES_AUTOVSCROLL, 0, 0, 0. 0, hwnd, (HMENU) ID_EDIT, ((LPCREATESTRUCT) lParam) -> hInstance, NULL) ; return 0 ; case WM_SETFOCUS : SetFocus (hwndEdit) ; return 0 ; case WM_SIZE : MoveWindow (hwndEdit, 0, 0, LOWORD (lParam), HIWORD (lParam), TRUE) ; return 0 ; case WM_COMMAND : if (LOWORD (wParam) == ID EDIT) if (HIWORD (wParam) = EN_ERRSPACE HIWORD (wParam) = EN MAXTEXT) MessageBox (hwnd, TEXT ("Edit control out of space."), szAppName, MB OK ţ MB ICONSTOP) ; return 0 ; case WM_DESTROY : PostOuitMessage (0) ; return 0 ; ) return DefWindowProc (hwnd, message, wParam, lParam) ; Rysunek 9-7. Program POPPADI POPPAD1 to edytor wielowierszowy (pozbawiony jakichkolwiek operacji wejœcia/ wyjœcia), którego kod ma mniej niż 100 linii! (Pewnš wadš tego programu jest to, że predefiniowana wieloliniowa kontrolka edycji ograniczona jest do 30 000 zna- ków). Jak możesz się zorientować, POPPAD1 niewiele robi sam. 4Viększoœć pra- cy bierze na siebie kontrolka edycji. Dzięki temu dowiedziałeœ się, do czego jest ona zdolna bez żadnej pomocy ze strony programu. Rozdział 9: Kontrolki okna potomnego 365 Style klasy edycji Jak wspomniałem już wczeœniej, kontrolkę edycji tworzysz, podajšc w wywoła- niu funkcji CreateWindow klasę "edit". Stylem okna powinien być WS CHILD plus ewentualnie kilka innych. Podobnie, jak miało to miejsce w przypadku kontrolek statycznych, tekst wyœwietlany przez kontrolkę edycji może być dosunięty do lewej strony, do prawej lub wyœrodkowany. Odpowiedni sposób wyrównywania po- daje się jako styl: ES LEFT, ES RIGHT lub ES CENTER. Domyœlnie kontrolka edycji pozwala na wyœwietlenie jednej linii tekstu. Jeżeli jednak tworzšc kontrolkę poshxżysz się stylem ES MULTILINE, będzie ona mo- gła wyœwietlać tekst składajšcy się z wielu linii. W przypadku jednoliniowej kon- trolki edycji możesz w niej wprowadzać tekst jedynie do chwili, gdy zajmie on cały prostokšt kontrolki. Jeœli chcesz, aby zawartoœć kontrolki była automatycz- nie przesuwana, wystarczy, jeżeli przy jej tworzeniu poshzżysz się stylem ES AU- TOHSCROLL. Natomiast w przypadku kontrolki wieloliniowej, jeżeli nie uży- jesz stylu ES AUTOHSCROLL, tekst, który zawiera więcej znaków i nie może zostać wyœwietlony w jednej linii, zostanie zawinięty do następnej. Dopiero jed- nak naciœnięcie klawisza [Enter] sprawi, że rozpoczęty zostanie nowy wiersz. Kontrolki wieloliniowe mogš również być wyposażone w pionowe paski prze- wijania; jeżeli jesteœ zainteresowany ich obecnoœciš, użyj stylu ES AUTO- VSCROLL. Jest bardzo prawdopodobne, że po odblokowaniu opcji przewijania chciałbyœ dodać do kontrolek paski przewijania. Możesz w tym celu posłużyć się znanymi już sobie identyfikatorami stylu WS HSCROLL oraz WS VSCROLL. Domyœlnie kontrolka edycji nie posiada ramki. Możesz jš dodać za pomocš stylu WS BOR- DER. Po zaznaczeniu bloku tekstu w kontrolce, system Windows wyœwietla go w ne- gatywie. Jeżeli jednak kontrolka straci fokus, zaznaczony tekst nie będzie już podœwietlany. jeœli chcesz, aby blok tekstu był podœwietlany niezależnie od tego, czy kontrolka ma fokus, czy nie - użyj stylu ES NOHIDESEL. Gdy program POPPAD1 tworzy własnš kontrolkę edycji, jej styl okreœlany jest w wywołaniu funkcji CreateWindow: WS CHILD ţ WS_VISIBLE ţ WS_HSCROLL ţ WS_VSCROLL WS_BORDER ţ ES_LEFT ţ ES_MULTILINE ES AUTOHSCROLL ţ ES AUTOVSCROLL Wymiary kontrolki edycji okreœlane sš w programie POPPAD1 w dalszej kolej- noœci. W ramach obsługi komurukatu WM SIZE wywoływana jest funkcja Move- Window z takimi parametrami, aby wielkoœć kontrolki edycji pokrywała się z wiel- koœciš okna nadrzędnego: MoveWindow (hwndEdit, 0, 0, LOWORD (lParam), HIWORD (lParam), TRUE) ; W przypadku kontrolek jednoliniowych, ich rozmiar musi być co najmniej rów- ny wysokoœci znaku. Jeżeli kontrolka edycji ma ramkę (a zwykle tak jest), to po- winna być 1,5 raza większa od znaku. 366 Częœć I: Podstawy Kody powiadamiania kontrolki edycji Kontrolki edycji wysyłajš do procedury okna nadrzędnego komunikat 4VMţCOM- MAND. Znaczenie parametrów wParam oraz lParam jest takie samo, jak w przy- padku przycisków: LOWORD(wParam) Identyfikator okna potomnego. HIWORD(wParam) Kod powiadomienia. lParam Uchwyt okna potomnego. Poniżej przedstawione zostały kody powiadomienia: EN SETFOCUS Kontrolka edycji otrzymała fokus. EN KILLFOCUS Kontrolka edycji utraciła fokus. EN CHANGE Zawartoœć kontrolki edycji zmieni się. EN UPDATE Zawartoœć kontrolki edycji zmieniła się. EN ERRSPACE W kontrolce edycji nie ma już miejsca. IN hZAX'TEXT W kontrolce edycji zabrakło miejsca w trakcie wstawiania. EN HSCROLL Kliknięty został poziomy pasek przewijania kontrolki. EN VSCROLL Kliknięty został pionowy pasek przewijania kontrolki. Program POPPADI obshxguje wyłšcznie kody powiadamiania EN ERRSPACE oraz EN MAXTEXT. Jeżeli którykolwiek z nich się pojawi, wyœwietlone zostanie odpowiednie okno komunikatu. Posługiwanie się kontrolkš edycji Jeżeli w oknie głównym umieszczone jest kilka kontrolek edycji, to przekazywa- nie pomiędzy nimi fokusu wymaga wprowadzenia podklasy okna. Możesz to zro- bić w podobny sposób jak w programie COLORSl, czyli przechwytujšc komuni- katy odpowiadajšce naciskanym klawiszom [Tab] oraz [Shift+Tab]. (Inny przy- kład tworzenia podklasy okna przedstawiony zostanie w programie HEAD w dalszej częœci tego rozdziału). To, w jaki sposób będziesz obsługiwał klawisz [En- ter], zależy wyłšcznie od ciebie. Może on działać tak samo, jak [Tab] lub sygnali- zować, że wszystkie kontrolki edycji zostały już wypełnione. Jeżeli do kontrolki edycji chcesz wstawić tekst, możesz posłużyć się funkcjš Set- WindowText. Z kolei pobieranie tekstu możliwe jest dzięki funkcji GetWindowTe- xtLength oraz GetWindowText. Z funkcjami tymi zetkniesz się w kolejnych wer- sjach programu POPPAD. Komunikaty wysyłane do kontrolki edycji Ze względu na dużš liczbę komunikatów, które możesz wysyłać za pomocš funkcji SendMessage do kontrolki edycji, zajmę się tu tylko częœciš z nich. Oto krótki prze- glšd tych komunikatów. Poniższe komunikaty pozwalajš na kopiowanie, wycinanie oraz usuwanie aktu- alnie zaznaczonego tekstu. Użytkownik może zaznaczyć tekst, posługujšc się myszš lub kombinacjš klawiszy kursora i klawisza [Shift]. Gdy kontrolka zawie- ra zaznaczony tekst, możesz do niej wysłać trzy komunikaty: Rozdział 9: Kontrolki okna potomnego 367 SendMessage (hwndEdit, WM_CUT, 0, 0) ; SendMessage (hwndEdit, WM_COPY, 0, 0) ; SendMessage (hwndEdit, WM CLEAR, 0, 0) ; Wysłanie komunikatu WM CUT spowoduje, że aktualnie zaznaczony tekst zo- stanie usunięty z kontrolki i umieszczony w Schowku. WM-COPY kopiuje za- znaczony tekst do Schowka. Natomiast WM-CLEAIZ - usuwa zaznaczony tekst, nie kopiujšc go jednoczeœnie do Schowka. Do kontrolki edycji na aktualnej pozycji kursora możesz również wstawić tekst znajdujšcy się w Schowku: SendMessage (hwndEdit, WMţPASTE, 0, 0) ; Położenie zaznaczonego bloku tekstu możesz pobrać wysyłajšc do kontrolki ko- munikat: SendMessage (hwndEdit, EM GETSEL, (WPARAM) &iStart, (LPARAM) &iEnd) ; Położenie końca bloku to położenie ostatniego znajdujšcego się w nim znaku plus 1. Możesz zaznaczyć tekst znajdujšcy się w kontrolce: SendMessage (hwndEdit, EM SETSEL, iStart, iEnd) ; Możesz również zastšpić aktualnie zaznaczony blok tekstu innym: SendMessage (hwndEdit, EMţREPLACESEL, 0, (LPARAM) szString) ; Jeœli korzystasz z kontrolki wieloliniowej, możesz sprawdzić, ile ma linii tekstu: iCount = SendMessage (hwndEdit, EM GETLINECOUNT, 0. 0) ; Dla każdej z linii tekstu możesz wyznaczyć jej przesunięcie względem poczštku bufora edytowanego tekstu: i0ffset = SendMessage (hwndEdit, EM LINEINDEX, iLine, 0) ; Poszczególne linie numerowane sš od 0. Podanie wartoœci iLine równej -1 spo- woduje, że zwrócone zostanie przesunięcie kursora względem poczštku bufora edytowanego tekstu. Długoœć linii możesz pobrać za pomocš: iLength = SendMessage (hwndEdit, EM LINELENGTH, iLine, 0) ; Linię możesz również skopiować do bufora: iLength = SendMessage (hwndEdit, EM GETLINE, iLine (LPARAM) szBuffer) ; Klasa pola listy Ostatnia predefiniowana kontrolka okna potomnego, którš zajmę się w tym roz- dziale, to pole listy. Pole listy jest zbiorem łańcuchów znaków wyœwietlanych na ekranie w postaci listy zamkniętej w prostokštnym obszarze. Program może za- równo dodawać do niej nowe łańcuchy, jak i usuwać już istniejšce, wysyłajšc odpowiednie komurukaty do procedury okna listy. Z kolei kontrolka pola listy wysyła do procedury okna nadrzędnego komunikat WM COMMAND za każ- dym razem, gdy zmienił się zaznaczony element listy. Na jego podstawie okno nadrzędne może okreœlić, który element został zaznaczony. Pole listy może pozwalać na zaznaczanie jednego elementu lub wielu. Ten drugi typ pozwala użytkownikowi zaznaczyć więcej niż jeden element znajdujšcy się na liœcie. Gdy pole listy ma fokus, wokół elementu wyœwietlana jest przerywana 368 Częœć 1: Podstawy ramka - jest to tzw. kursor. Kursor ten nie oznacza jednak elementu zaznaczone- go. Element zaznaczony jest identyfikowany za pomocš podœwietlenia, które po- woduje, że jestţon przedstawiany w negatywie. W polu listy z możliwoœciš wyboru tylko jednego elementu, użytkownik może zaznaczyć element, na którym znajduje się kursor, naciskajšc klawisz spacji. Kla- wisze sterujšce ruchem kursora przemieszczajš zarówno sam kursor, jak i zazna- czenie elementu. Pozwalajš również na przewijanie zawartoœci listy. Klawisze [Page Up] oraz [Page Down] również przewijajš zawartoœć listy przesuwajšc kursor, jed- nak w tym wypadku zaznaczony element pozostaje niezmieniony. Naciœnięcie któ- regokolwiek klawisza z literš powoduje przeniesienie kursora do pierwszego (lub kolejnego) elementu, który rozpoczyna się od tej litery. Element może również zo- stać zaznaczony przez kliknięcie lub dwukrotne kliknięcie go myszš. W polu listy z możliwoœciš wyboru wielu elementów, naciskanie klawisza spacji powoduje przełšczanie stanu wybrania elementu wskazywanego przez kursor. (Jeżeli element jest już zaznaczony, zostanie odznaczony). Naciœnięcie klawisza kursora powoduje odznaczenie wszystkich elementów i przeniesienie zarówno kursora, jak i zaznaczenia, dokładnie tak jak w liœcie z możliwoœciš wyboru jed- nego elementu. Jednakże korzystajšc z klawisza [Ctrl] oraz klawiszy kursora, można przemieszczać kursor bez przemieszczania zaznaczenia. Kliknięcie lub dwukrotne kliknięcie elementu listy z możliwoœciš wielokrotnego wyboru powoduje odznaczenie wszystkich uprzednio zaznaczonych elementów, a następnie zaznaczenie klikniętego. Jeżeli jednak w trakcie kliknięcia naciœnięty będzie klawisz [Shift], zmieni się stan jedynie klikniętego elementu, a pozosta- łych pozostanie niezmieniony. Style pola listy Pole listy tworzysz, podajšc klasę "listbox" w wywołaniu funkcji CreateWindow, a jako styl okna - WS CHILD. Posłużenie się jednak takim domyœlnym stylem pola listy spowoduje, że nie będzie ono wysyłało komunikatów WMţCOMMAND do procedury okna nadrzędnego, co oznacza, że program będzie musiał sam za- troszczyć się o pobieranie informacji o zaznaczonych elementach listy (za pomo- cš wysyłanych do niej komunikatów). Dlatego też w trakcie tworzenia pola listy niemal zawsze dodawany jest jej styl LBS NOTIFY, dzięki któremu okno nad- rzędne będzie otrzymywało komunikaty WM COMMAND. Jeżeli chcesz, aby lista wyœwietlała elementy posortowane, poshzż się stylem LBS SORT. Domyœlnie listy wyboru pozwalajš ma zaznaczenie tylko jednego elementu. Li- sty pozwalajšce na zaznaczanie wielu elementów pojawiajš się stosunkowo rzad- ko. Jeżeli jednak chcesz stworzyć takš listę, użyj stylu LBS MULTIPLESEL. Zwy- kle lista odœwieża siebie, gdy zostanie do niej dodany nowy element. Możesz to uniemożliwić, jeżeli w trakcie tworzenia kontrolki nadasz jej styl LBS NORE- DRAW. Jeżeli jednak z jakichœ powodów nie możesz tego zrobić, powinieneœ po- służyć się komunikatem WM SETREDRAW, którym zajmę się nieco póŸniej. Domyœlnie procedura okna pola wyboru wyœwietla jedynie listę elementów bez jakiejkolwiek ramki. Możesz jš dodać, jeżeli posłużysz się stylem WS BORDER. Rozdział 9: Kontţolki okna potomnego 369 Z kolei, aby dodać pionowy pasek przewijania, dzięki któremu będziesz mógł przewijać zawartoœć listy, posługujšc się myszš, użyj stylu WS VSCROLL. W jednym z plików nagłówkowych Windows zdefiniowany został identyfikator LBS DEFAULT, który ma wartoœć: (LBSţNOTIFY ţ LBS SORT ţ LBS VSCROLL ţ WSţBORDER) Możesz również posłużyć się identyfikatorami WS SIZEBOX oraz WS CAPTION, jednak spowoduje to, że użytkownik będzie mógł zmieniać wielkoœć oraz poło- żenie kontrolki w oknie głównym. Szerokoœć listy powinna być zawsze tak dobrana, aby uwzględniała długoœć naj- dłuższego wyœwietlanego w niej łańcucha oraz ewentualnš obecnoœć paska prze- wijania. Szerokoœć paska możesz pobrać w następujšcy sposób: GetSystemMetrics (SM CXUSCROLL) ; Z kolei wysokoœć pola listy możesz wyznaczyć, mnożšc przez siebie wysokoœć znaku przez liczbę elementów, które chcesz wyœwietlić jednorazowo. Umieszczanie łańcuchów w polu listy Kolejnym krokiem po stworzeniu pola listy jest umieszczenie w nim łańcuchów tekstu. Możesz to zrobić, wysyłajšc do pola komunikat za pomocš funkcji Send- Message. Łańcuchy identyfikowane sš w polu za pomocš numeru, przy czym temu, który znajduje się na samym poczštku listy, przypisany jest numer 0. W zamiesz- czonych poniżej przykładach hwndList jest uchwytem kontrolki okna potomne- go, a ilndex to numer łańcucha. Jak zawsze w przypadku, gdy za pomocš funkcji SendMessage przesyłasz łańcuch znaków, za pomocš parametru lParam przeka- zywany jest wskaŸnik do łańcucha. W większoœci przedstawionych poniżej przykładów, jeżeli procedura okna nie dysponuje wystarczajšcš iloœciš wolnej pamięci, aby dodać do listy kolejny łań- cuch, funkcja SendMessage zwróci kod błędu LB ĘRRSPACE (majšcy wartoœć -2). Jeżeli wystšpi inny błšd, SendMessage zwróci LB ĘRR (-1). jeżeli natomiast wszyst- ko się powiedzie, otrzymasz kod LB OK. (0). Oba wymienione powyżej błędy mogš z łatwoœciš zostać wychwycone, jeżeli sprawdzisz, czy wartoœć zwracana przez SendMessage jest mniejsza od 0. Jeżeli w trakcie tworzenia pola listy posłużyłeœ się stylem LBS SORT (lub jeżeli umieszczasz w polu łańcuchy w takiej kolejnoœci, w jakiej majš być póŸniej wy- œwietlane), najprostszš metodš wypełnienia pola jest wysłanie do niego serii ko- munikatów LB ADDSTRING: SendMessage (hwndList, LB ADDSTRING, 0, (LPARAM) szString) : Jeżeli natomiast nie posłużyłeœ się stylem LBS SORT, do pola listy możesz wsta- wiać łańcuchy, podajšc pozycję, na której majš się one znaleŸć. W tym celu sko- rzystasz z komunikatu LB INSERTSTRING: SendMessage (hwndList, LBţINSERTSTRING, iIndex, (LPARAM) szString) ; Jeżeli na przykład, jako parametr ilndex podasz 4, wstawiany łańcuch znajdzie się na pozycji o numerze 4, czyli zostanie wyœwietlony w polu jako pišty (pamię- taj, że łańcuchy numerowane sš od zera). Jeżeli natomiast jako ilndex podasz wartoœć -1, łańcuch zostanie dodany na końcu listy. Komunikatem LB INSERT 370 Częœć I: Podstawy STRING możesz posłużyć się również wtedy, gdy używasz listy LBS SORTED. Jednak w tym przypadku po wstawieniu łańcucha zawartoœć listy nie zostanie ponownie posortowana. (Nowe łańcuchy możesz wstawiać do listy również za pomocš komunikatu LB DIR, którym szczegółowo zajmę się pod koniec tego rozdziahz). Jeżeli chcesz usunšć z listy łańcuch, możesz posłużyć się komunikatem LB DE- LETESTRING, podajšc jako jego parametr położenie łańcucha: SendMessage (hwndList, LBţDELETESTRING, iIndex, 0) ; Całš zawartoœć pola listy można usunšć, wysyłajšc do niego komunikat LB RE- SETCONTENT: SendMessage (hwndList, LB RESETCONTENT, 0, 0) ; Za każdym razem, gdy do pola listy zostanie dodany nowy łańcuch lub usunięty już istniejšcy, procedura okna pola odœwieża jego zawartoœć. Jeżeli tak się zda- rzy, że będziesz chciał dodać lub usunšć z listy wiele łańcuchów, możesz tym- czasowo zabronić odœwieżania kontrolki, kasujšc jednš z jej flag: SendMessage (hwndList, WM SETREDRAW, FALSE, 0) ; Po zakończeniu operacji na łańcuchach, powinieneœ ponownie ustawić flagę: SendMessage (hwndList, WM SETREDRAW, TRUE, 0) ; Jeżeli w trakcie tworzenia pola listy zostanie wykorzystany styl LBS NOREDRAW, domyœlnie pole nie będzie odœwieżało swojej zawartoœci. Zaznaczanie i pobieranie elementów listy Wszystkie przedstawione poniżej wywołania funkcji SendMessage zwracajš jakšœ wartoœć. Jeżeli wystšpi jakikolwiek błšd, będzie to LB ERR (zdefiniowany jako -1). Po umieszczeniu w polu listy kilku elementów, możesz potrzebować informacji, ile ich tam jest: iCount = SendMessage (hwndList, LB GETCOUNT, 0, 0) ; Niektóre z przedstawionych dalej wywołań zachowujš się różnie w zależnoœci od tego, czy pole listy umożliwia zaznaczenie wielu elementów, czy tylko jedne- go. Zajmijmy się najpierw tym drugim przypadkiem. Zwykle program pozwala użytkownikowi na zaznaczenie na liœcie interesujšce- go go elementu. Jeżeli jednak chcesz zaznaczyć jakiœ element tak, aby był trakto- wany jako domyœlny, możesz zrobić to w następujšcy sposób: SendMessage (hwndList, LBţSETCURSEL, iIndex, 0) ; Jeżeli jako parametr lParam tego komunikatu podasz wartoœć -1, odznaczone zo- stanš wszystkie elementy. Możesz również zaznaczyć element pola listy, dysponujšc jedynie kilkoma jego pierwszymi znakami: iIndex = SendMessage (hwndList, LB_SELECTSTRING, iIndex, (LPARAM) szSearchString) ; Parametr ilndex przekazywany jako lParam w wywołaniu funkcji SendMessage, okre- œla położenie elementu, od którego rozpocznie się wyszukiwanie łańcucha o po- czštkowych znakach przekazanych za pomocš szSearchString. Jeżeli jako ilndex podana zostanie wartoœć -l, wyszukiwanie rozpocznie się od poczštku pola listy. Rozdział 9: Kontrolki okna potomnego 371 Jako rezultat funkcja SendMessage zwraca położenie zaznaczonego łańcucha lub LB ĘRR, jeżeli żaden z nich nie pasował do wzorca podanego w szSearchString. Gdy odbierzesz komunikat 4VM-COMMAND zwišzany z polem listy (lub w każ- dym innym momencie), możesz sprawdzić, który element jest zaznaczony. W tym celu powinieneœ posłużyć się komunikatem LB GETCURSEL: iIndex = SendMessage (hwndList, LB GETCURSEL, 0, 0) ; Jeżeli zwrócona zostanie wartoœć LB_ERR oznacza to, że żaden element nie zo- stał zaznaczony. Jeżeli to konieczne, możesz okreœlić długoœć dowolnego łańcucha umieszczone- go w polu listy: iLength = SendMessage (hwndList, LB GETTEXTLEN, iIndex, 0) ; a następnie skopiować go do bufora: iLength = SendMessage (hwndList, LB_GETTEXT, iIndex, (LPARAM) szBuffer) ; W obu przypadkach wartoœć iLength zwrócona przez funkcję SendMessage okre- œla długoœć łańcucha. Wykorzystywany w drugim przypadku bufor szBuffer po- winien być wystarczajšco długi, aby pomieœcić pobierany łańcuch wraz z koń- czšcym go znakiem NULL. W przypadku list umożliwiajšcych zaznaczanie wielu elementów, nie możesz posługiwać się komunikatami LB SETCURSEL, LB GETCURSEL oraz LB SE- LECTSTRING. Zamiast nich, aby zaznaczyć wybrany łańcuch bez wpływu na stan pozostałych, możesz użyć komunikatu LB SETSEL: SendMessage (hwndList, LB SETSEL, wParam, iIndex) ; Jeżeli parametr wParam ma wartoœć niezerowš, element zostanie zaznaczony, w przeciwnym zaœ wypadku - odznaczony. Jeżeli lParam ma wartoœć -1, zostanš zaznaczone lub odznaczone wszystkie elementy. Możesz również sprawdzić, czy wybrany łańcuch jest zaznaczony: iSelect = SendMessage (hwndList, LB GETSEL, iIndex, 0) ; gdzie iSelect ma wartoœć niezerowš, jeżeli wybrany łańcuch jest zaznaczony, ze- rowš natomiast w przeciwnym wypadku. Komunikaty wysłane przez pole listy Gdy użytkownik kliknie pole listy myszš, otrzymuje ona fokus. Z kolei okno nadxzędne może wymusić przekazanie fokusa do pola listy, wywołujšc funkcję: SetFocus (hwndList) ; Gdy pole listy otrzyma fokus, elementy pola listy można wybierać za pomocš klawiszy kursora, liter oraz spacji. Kontrolka pola listy wysyła do swego okna nadrzędnego komunikaty WMţCOM- MAND. Znaczenie towarzyszšcych im parametrów wParam oraz lParam jest ta- kie samo, jak w przypadku przycisków i kontrolek edycji: LOWORD(wParam) Identyfikator okna potomnego HIWORD(wParam) Kod powiadomienia IParam Uchwyt okna potomnego 372 Częœć I: Podstawy Kody powiadamiania majš następujšce wartoœci: LBN ERRSPACE -2 LBN SELCHANGE 1 LBN DBLCLK 2 LBN SELCANCEL 3 LBN SETFOCUS 4 LBN KILLFOCUS 5 Pole listy wysyła do okna nadrzędnego komunikaty LBN SELCHANGE oraz LBN DBLCLK, gdy styl okna pola listy zawiera flagę LBS NOTIFY. Kod LBN ERRSPACE oznacza, że kontrolka nie dysponuje wystarczajšcš iloœciš miejsca. Kod LBN SELCHANGE jest wysyłany, gdy zmienił się zaznaczony łań- cuch; komunikaty te pojawiajš się, gdy użytkownik przesuwa na liœcie podœwie- tlenie, przełšcza zaznaczenie za pomocš spacji lub klika wybrany element my- szš. Kod LBN DBLCLK oznacza, że dany łańcuch został dwukrotnie kliknięty myszš. (Wartoœci kodów powiadomienia LBN SELCHANGE oraz LBN DBLCLK odpowiadajš liczbie kliknięć). Zależnie od aplikacji, możesz zdecydować się na wykorzystanie LBN SELCHAN- GE, LBN DBLCLK lub obu jednoczeœnie. Do programu będzie docierało wiele komunikatów LBN SELCHANGE, natomiast LBN DBLCLK pojawi się tylko wtedy, gdy użytkownik dwukrotnie kliknie myszš wybrany element. Jeżeli pisa- ny przez ciebie program wykorzystuje w jakiœ sposób dwukrotne kliknięcie, bę- dziesz musiał przygotować dla niego jakiœ odpowiednik dostępny z klawiatury. Prosta aplikacja z polem listy Teraz, gdy już wiesz, w jaki sposób możesz stworzyć pole listy, wypełnić je da- nymi, obsługiwać zwišzane z nim komunikaty oraz pobierać znajdujšce się w nim napisy, nadszedł czas, aby napisać jakšœ aplikację. Przedstawiony na rysunku 9- 8 program ENVIRON posługuje się umieszczonym w obszarze klienta polem li- sty do wyœwietlenia zdefiniowanych w systemie zmiennych œrodowiskowych (ta- kich jak PATH czy WINDIR). Po wybraniu zmiennej, w górnej częœci okna wy- œwietlana jest jej zawartoœć. ENUIRON.C /* ENVIRON.C - Pole listy ze zmiennymi œrodowiskowymi (c) Charles Petzold, 1998 */ ţţinclude ţţdefine ID LIST 1 ţţdefine ID TEXT 2 LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; Rozdział 9: Kontrolki okna potomnego 373 int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) static TCHAR szAppName[] = TEXT ("Environ") ; HWND hwnd : i:'. MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CS VREDRAW ; ii wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; '' wndclass.cbWndExtra = 0 ; "' Ii wndclass.hInstance = hInstance ; !' ţ:: wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) (COLOR WINDOW + 1) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) MessageBox (NULL, TEXT ("Program requires Windows NT!"), szAppName, MBţICONERROR) ; return 0 ; ,. hwnd = CreateWindow (szAppName, TEXT ("Environment List Box"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; 4. ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) TranslateMessage (&msg) ; DispatchMessage (&msg) ; ! ) return msg.wParam ; void FillListBox (HWND hwndList) int iLength ; TCHAR * pVarBlock, * pUarBeg, * pVarEnd, * pVarName ; pVarBlock = GetEnvironmentStrings () ; // Pobierz wskaŸnik do bloku // zmiennych œrodowiskowychţ while (*pVarBlock) ( if (*pVarBlock != =') // Pomiń nazwy zmiennych rozpoczynajdce // się od '- ( pVarBeg = pVarBlock ; // Poczdtek nazwy zmiennej while (*pVarBlock++ != =') ; // Przeglddaj aż do napotkania '- pVarEnd = pVarBlock - 1 ; // Wskazuje na znak '- 374 Częœć I: Podstawy (cišg dalszy ze strony 373) iLength = pVarEnd - pVarBeg ; // Dlugoœć zmiennej nazwy // Przydziel pamięć operacyjnš na nazwę zmiennej z // uwzględnieniem kończšcego jš znaku NULL. // Skopiuj nazwę zmiennej i dolšcz do niej zero. pVarName = calloc (iLength + l, sizeof (TCHAR)) ; CopyMemory (pVarName, pVarBeg, iLength * sizeof (TCHAR)) ; pVarName[iLength] = '\0' , // Umieœć nazwę zmiennej w polu listy, a następnie // zwolnij pamięć przydzielonš zmiennej. SendMessage (hwndList, LB ADDSTRING, 0, (LPARAM) pVarName) ; free (pVarName) ; while (*pVarBlock++ (= '\0') ; // Przeglšdaj aż do napotkania // kończšcego znaku 0 1 FreeEnvironmentStrings (pVarBlock) ; LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ( static HWND hwndList, hwndText ; int iIndex, iLength, cxChar, cyChar ; TCHAR * pVarName, * pVarValue ; switch (message) ( case WM_CREATE : cxChar = LOWORD (GetOialogBaseUnits ()) ; cyChar = HIWORD (GetDialogBaseUnits ()) ; // Stwórz pole listy i statyczne pola tekstowe. hwndList = CreateWindow (TEXT ("listbox"), NULL, WS_CHILD ţ WSţVISIBLE ţ LBS STANDARD, cxChar, cyChar * 3, cxChar * 16 + GetSystemMetrics (SM CXUSCROLL), cyChar * 5, hwnd, (HMENU) ID LIST, (HINSTANCE) GetWindowLong (hwnd, GWL HINSTANCE), NULL) ; hwndText = CreateWindow (TEXT ("static"), NULL, WS_CHILD ţ WSţVISIBLE ( SSţLEFT, cxChar, cyChar, GetSystemMetrics (SM_CXSCREEN), cyChar, hwnd, (HMENU) ID TEXT, (HINSTANCE) GetWindowLong (hwnd, GWL HINSTANCE), NULL) ; FillListBox (hwndList) ; return 0 ; case WM SETFOCUS : Rozdział 9: Kontrolki okna potomnego 375 SetFocus (hwndList) ; return 0 ; case WM_COMMAND : if (LOWORD (wParam) == IDţLIST && HIWORD (wParam) == LBNţSELCHANGE) ( // Pobierz aktualnie zaznaczony lańcuch. iIndex = SendMessage (hwndList, LB_GETCURSEL, 0, 0) ; iLength = SendMessage (hwndList, LB_GETTEXTLEN, iIndex, 0) + 1 ; pVarName = calloc (iLength, sizeof (TCHAR)) ; SendMessage (hwndList, LB 6ETTEXT, iIndex, (LPARAM) pVarName) ; // Pobierz zmiennš œrodowiskowš. iLength = GetEnvironmentVariable (pVarName, NULL, 0) pVarValue = calloc (iLength, sizeof (TCHAR)) ; GetEnvironmentVariable (pVarName, pVarValue, iLength) // Pokaż jš w oknie. SetWindowText (hwndText, pVarValue) ; free (pVarName) ; free (pVarValue) ; ) return 0 ; case WM_DESTROY : PostOuitMessage (0) : return 0 ; ) return DefWindowProc (hwnd, message, wParam, lParam) ; Rysunek 9-8. Program ENVIRON Program ENVIRON tworzy dwa okna potomne: pole listy LBS STANDARD oraz okno statyczne SS LEFT (tekst wyrównywany lewostronnie). Program pobiera wskaŸnik do obszaru pamięci zawierajšcego zmienne œrodowiskowe oraz ich wartoœci za pomocš funkcji GetEnvironmentStrings. Następnie blok ten jest prze- twarzany za poœrednictwem funkcji FillIListbox. Do wstawienia poszczególnych łańcuchów do listy wykorzystywany jest komunikat LB ADDSTRING wysyłany do procedury okna. Gdy posługujesz się programem ENVIRON, zmienne œrodowiskowe możesz wybierać, posługujšc się klawiaturš bšdŸ mysżš. Za każdym razem, gdy zazna- czony zostanie nowy element, do okna nadrzędnego wysyłany jest komunikat WMţCOMMAND. Gdy z kolei funkcja WndProc odbierze WM COMMAND sprawdza, jaka jest wartoœć młodszego słowa parametru wParam. Jeżeli jest to ID LIST (identyfikator kontrolki pola listy), a starsze słowo wParam przechowu- je kod powiadamiania LBN SELCHANGE, program pobiera położenie na liœcie zaznaczonego łańcucha oraz sam łańcuch - nazwę zmiennej œrodowiskowej - korzystajšc w tym celu z komunikatu LB GETTEXT. Następnie, za pomocš funkcji C GetEnvironmentvariable, program ENVIRON pobiera wartoœć zmiennej i korzy- stajšc z SetWindowText przekazuje jš do kontrolki statycznej. 376 Częœć I: Podstawy Lista plików Najsmaczniejszy kšsek zostawiłem sobie na koniec: LB DIR to pole listy o naj- większych możliwoœciach. Przedstawione poniżej wywołanie funkcji wypełnia pole listy plikami znajdujšcymi się w danym katalogu, wyœwietlajšc opcjonalnie również nazwy podkatalogów: SendMessage (hwndList, LB DIR, iAttr. (LPARAM) szFileSpec) ; Posługiwanie się kodami atrybutów Parametr iAttr jest kodem atrybutu. Jego mniej znaczšcy bajt zawiera kod atry- butu pliku, który może być kombinacjš wartoœci przedstawionych w poniższej tabeli: iAttr Wartoœć Atrybut DDL READWRITE 0x0000 Zwykły plik DDL READONLY 0x0001 Plik tylko do odczytu DDL HIDDEN 0x0002 Plik ukryty DDL SYSTEM 0x0004 Plik systemowy DDL_DIRECTORY 0x0010 Podkatalog DDL_ARCHIVE 0x0020 Plik z ustawionym atrybutem archi- wizacji Kolejny starszy bajt wParam pozwala na bardziej szczegółowš kontrolę elemen- tów wstawianych do listy: iAttr Wartoœć Opcja DDL_DRIVES 0x4000 Wyœwietlanie liter dysków DDL EXCLUSIVE 0x8000 Przeszukiwanie wykluczajšce Przedrostek "DDL" jest skrótem od ang. dialog directory list (lista plików w oknie dialogowym). Gdy wartoœć DDL READWRTTE podana zostanie jako parametr iAttr komuni- katu LB DIR, na liœcie umieszczone zostanš zwykłe pliki, pliki przeznaczone wyłšcznie do odczytu oraz te, które majš ustawiony atrybut archiwizacji. Jeżeli z kolei, wartoœciš tš będzie DDL DIRECTORY, oprócz wymienionych przed chwi- la plików, na liœcie znajdš się również nazwy podkatalogów umieszczone w na- wiasach kwadratowych. Wartoœć DDL DRIVES I DDL DIRECTORY rozszerzy listę o litery odpowiadajšce dostępnym w systemie stacjom dysków, przy czym zostanš one ujęte w znaki większoœci i mniejszoœci. Ustawienie najbardziej znaczšcego bitu parametru iAttr spowoduje, że nie będš wyœwietlane wszystkie zwykłe pliki, ale tylko te, których flagi zostały jawnie po- dane. Jeżeli na przykład piszesz program tworzšcy kopie bezpieczeństwa plików, może okazać się, że chcesz wyœwietlić tylko te pliki, które zostały zmodyfikowane Rozdział 9: Kontrolki okna potomnego od czasu ostatniej archiwizacji. Pliki takie majš ustawiony atrybut archiwizacji, powinieneœ więc posłużyć się atrybutem DDL EXCLUSIVE I DDL ARCHIVE. Porzšdkowanie listy plików Parametr lParam jest wskaŸnikiem do maski okreœlajšcej, jakie pliki będš wybie- rane. Może to być na przykład "* *". Maska ta nie ma wpływu na podkatalogi , które mogš ewentualnie pojawić się w polu listy. Prawdopodobnie w stosunku do list zawierajšcych pliki będziesz używał komu- nikatu LBS SORT. Spowoduje to, że w pierwszej kolejnoœci na liœcie wyœwietlo- ny zostanie zbiór plików pasujšcych do podanej maski, następnie (opcjonalnie) podkatalogi. Pierwszym wyœwietlonym podkatalogiem będzie [..] Dzięki obecnoœci tego katalogu, użytkownik może przejœć o jeden poziom wyżej w drzewie katalogów. (Nazwa ta nie pojawi się, jeżeli wyœwietlasz podkatalogi głównego katalogu dysku). Następnie wyœwietlane sš podkatalogi w następujš- cej postaci: [PODKATALOG] Na koniec na liœcie pojawiajš się łańcuchy symbolizujšce dostępne w systemie napędy dysków. Majš one następujšcš postać: [-A-] Program HEAD dla Windows Dobrze znany z systemu Unix program narzędziowy head umożliwia wyœwietle- nie poczštkowych linii pliku. Posłużmy się teraz polem listy, piszšc podobny program przeznaczony do pracy w systemie Windows. Aplikacja przedstawiona na rysunku 9-9 wyœwietla w polu listy wszystkie pliki oraz podkatalogi znajdu- jšce się w wybranym katalogu. Chcšc wybrać plik do wyœwietlenia, możesz dwu- krotnie kliknšć go myszš lub nacisnšć [Enter], gdy jego nazwa będzie podœwie- tlona. Posługujšc się dowolnš z tych metod możesz również przejœć do, innego podkatalogu. Program umożliwia wyœwietlenie pierwszych 8 KB wybranego pliku w prawej częœci swego okna. HEAD.C /* HEAD.C - Wyœwietla poczdtek (nagłówek) pliku (c) Charles Petzold, 1998 */ ţţinclude ţţdefine ID_LIST 4ţdefine ID TEXT ţţdefine MAXREAD 8192 ţţdefine DIRATTR (DDL_READWRITE ţ DDL READONLY ţ DDL_HIDDEN ţ DDLţSYSTEM ţ \ DDL_DIRECTORY ţ DDL ARCHIVE ţ DDL DRIVES) ţţdefine DTFLAGS (DT WORDBREAK ţ DT EXPANDTABS ţ .DT NOCLIP ţ DTţNOPREFIX) Częœć I: Podstawy i (cišg dalszy ze strony 377) LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; I LRESULT CALLBACK ListProc (HWND, UINT, WPARAM, LPARAM) ; WNDPROC OldList ; I int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) f static TCHAR szAppName[] = TEXT ("head") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CSţUREDRAW wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI ŽPPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) (COLOR BTNFACE + 1) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) ( MessageBox (NULL, TEXT ("Program requires Windows NT!"), szAppName, MBţICONERROR) ; return 0 ; 1 hwnd = CreateWindow (szAppName, TEXT ("head">, WS_OVERLAPPEDWINDOW ţ WS_CLIPCHILDREN, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CWţUSEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) ( TranslateMessage (&msg) ; DispatchMessage (&msg) ; ) return ms9.wParam ; LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ( static BOOL bValidFile ; static BYTE buffer[MAXREAD] ; static HWND hwndList, hwndText static RECT rect ; static TCHAR szFile[MAXţPATH + 1] ; HANDLE hFile ; Rozdział 9: Kontrolki okna potomnego HDC hdc ; int i, cxChar, cyChar ; PAINTSTRUCT ps ; TCHAR szBuffer[MAX PATH + 1] ; switch (message) i i, case WM_CREATE : cxChar = LOWORD (GetDialogBaseUnits ()) ; cyChar = HIWORD (GetDialogBaseUnits ()) ; rect.left = 20 * cxChar ; rect.top - 3 * cyChar ; i hwndList = CreateWindow (TEXT ("listbox"), NULL, ! WS_CHILDWINDOW ţ WS VISIBLE ţ LBSţSTANDARD, cxChar, cyChar * 3, ' cxChar * 13 + GetSystemMetrics (SM CXVSCROLL), cyChar * 10, hwnd (HMENU) ID LIST, (HINSTANCE) GetWindowLong (hwnd, GWLţHINSTANCE), NULL) ; I GetCurrent0irectory (MAXţPATH + 1, szBuffer) ; ţi hwndText = CreateWindow (TEXT ("static"), szBuffer, WS CHILDWINDOW ţ WS_VISIBLE ţ SSţLEFT, cxChar, cyChar, cxChar * MAXţPATH, cyChar, hwnd, (HMENU) ID TEXT, (HINSTANCE) GetWindowLong (hwnd, GWLţHINSTANCE), NULL) ; ţ I''. i OldList = (WNDPROC) SetWindowLong (hwndList, GWL WNDPROC, (LPARAM) ListProc) ; SendMessage (hwndList, LB_DIR, DIRATTR, (LPARAM) TEXT ("*.*")) ; return 0 ; case WM_SIZE : rect.right = LOWORD (lParam) ; rect.bottom = HIWORD (lParam) ; return 0 ; case WM_SETFOCUS : SetFocus (hwndList) ; return 0 ; ;i:'. case WM_COMMAND : if (LOWORD (wParam) == ID LIST && HIWORD (wParam) = LBN DBLCLK) if (LB ERR == (i = SendMessage (hwndList, LB GETCURSEL, 0, 0))) ; _ break ; SendMessage (hwndList, LB GETTEXT, i, (LPARAM) szBuffer) ; if (INVALID_HANDLE_VALUE != (hFile = CreateFile (szBuffer, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN EXISTING, 0, NULL))) ţ 4"I Ž.,; ( = 380 Częœć I: Podstawy (cišg dalszy ze strony 379) i CloseHandle (hFile) ; bValidFile = TRUE ; lstrcpy (szFile, szBuffer) ; GetCurrentDirectory (MAXţPATH + 1, szBuffer) ; if (szBuffer [lstrlen (szBuffer) - 1] != '\\') lstrcat (szBuffer, TEXT ("\\")) ; SetWindowText (hwndText, lstrcat (szBuffer, szFile)) ; ) else ( bValidFile = FALSE ; szBuffer [lstrlen (szBuffer) - 1] = '\0' , // Jeżeli próba ustawienia katalogu nie powiodla się, // może jest to próba zmiany napędu. Spróbujmy. if (!SetCurrentDirectory (szBuffer + 1)) i szBuffer [3] = . szBuffer [4] = '\0', SetCurrentDirectory (szBuffer + 2) ; ) // Pobierz nowd nazwę katalogu i wypelnij pole listy. GetCurrentDirectory (MAXţPATH + 1, szBuffer) ; SetWindowText (hwndText, szBuffer) ; SendMessage (hwndList, LB_RESETCONTENT, 0, 0) ; SendMessage (hwndList, LB_DIR, DIRATTR, (LPARAM) TEXT ("*.*")) ; ) InvalidateRect (hwnd, NULL, TRUE) ; 1 return 0 ; case WM_PAINT : if (!bValidFile) . break ; if (INVALID_HANDLE_VALUE = (hFile = CreateFile (szFile, GENERICţREAD, FILEţSHARE READ, NULL, OPENţEXISTING, 0, NULL))) ( bValidFile = FALSE ; break ; ReadFile (hFile, buffer, MAXREAD, &i, NULL) ; CloseHandle (hFile) ; // Zmienna i równa 'est liczbie ba'tów umieszczon ch w buforze. J J y // Pobierz kontekst urzadzenia, aby wyœwietlić tekst. hdc = BeginPaint (hwnd, &ps) ; SelectObject (hdc, GetStockObject (SYSTEM_FIXED_FONT)) ; SetTextColor (hdc, GetSysColor (COLORţBTNTEXT)) ; Rozdział 9: Kontrolki okna potomnego 381 SetBkColor (hdc, GetSysColor (COLORţBTNFACE)) ; // Zalóżmy, że jest to plik ASCII DrawTextA (hdc, buffer, i, &rect, DTFLAGS) ; EndPaint (hwnd, &ps) ; return 0 : case WM DESTROY : PostOuitMessage (0) ; return 0 ; ) return DefWindowProc (hwnd, message, wParam, lParam) : LRESULT CALLBACK ListProc (HWND hwnd, UINT message WPARAM wParam, LPARAM lParam) if (message == WM_KEYDOWN && wParam = VK RETURN) SendMessage (GetParent (hwnd), WM COMMAND, MAKELONG (1, LBN DBLCLK), (LPARAM) hwnd) : return CallWindowProc (OldList, hwnd, messa9e, wParam, lParam) ; Rysunek 9-9. Program HEAD W programie ENVIRON, po wybraniu nazwy zmiennej œrodowiskowej - nieza- leżnie od tego, czy zrobiliœmy to za pomocš myszy czy klawiatury - wyœwietla- na była jej zawartoœć. Gdybyœmy jednak użyli tej samej metody w programie HEAD, działałby on zbyt wolno, ponieważ cišgle musiałby otwierać i zamykać pliki w miarę, jak w polu listy zmieniałbyœ wybrany element. Dlatego też do wy- œwietlenia zawartoœci pliku konieczne jest dwukrotne kliknięcie jego nazwy. Po- woduje to pewne problemy: ponieważ kontrolka pola listy nie ma automatycz- nego interfejsu klawiatury, który odpowiadałby dwukrotnemu kliknięciu myszš. Jak wiemy, wszędzie gdzie to tylko możliwe, należy takie odpowiedniki wpro- wadzać. ftozwišzanie? Trzeba oczywiœcie utworzyć podklasę okna. Procedura okna pod- klasy pola listy została w programie HEAD nazwana ListProc. Jej zadaniem jest przechwycenie komunikatu WM-KEYDOWN z parametrem wParam równym VK IZETURN i wysłanie w tym momencie do okna nadrzędnego komunikatu WM COMMAND z kodem powiadamiania LBN DBLCLK. W funkcji WndProc w ramach. obsługi komunikatu WM COMMAND do sprawdzenia, czy z listy wybrana została poprawna nazwa pliku, wykorzystana została funkcja CreateFi- le. Jeżeli jej wykonanie zakończyło się niepowodzeniem, wybrana nazwa nie jest nazwš pliku, z dużym prawdopodobieństwem można więc powiedzieć, że jest to katalog. W tej sytuacji program HEAD wywołuje funkcję SetCurrentDirectory, której zadaniem jest zmiana aktualnego katalogu. Jeżeli wywołanie tej funkcji zakończyło się błędem, program zakłada, że użytkowruk wybrał z listy literę któ- rejœ ze stacji dysków. Zmiana aktualnej stacji dysków również wymaga wywoła- nia funkcji SetCurrentDirectory, jednak w tym przypadku należy usunšć znaki 382 Częœć Iţ Podstawy myœlnika znajdujšce się po obu stronach litery i dodać dwukropek. Następnie do pola listy wysyłany jest komunikat LB_RESETCONTENT, co powoduje usunię- cie całej jej zawartoœci. Kolejnym wysyłanym komunikatem jest LB DIR, który z kolei powoduje, że lista zostaje wypełniona plikami znajdujšcymi się w nowym podkatalogu. W ramach obsługi komunikatu WMţPAINT, za pomocš funkcji CreateFile otwie- rany jest wybrany przez użytkownika plik. Program otrzymuje w ten sposób uchwyt do pliku, który następnie może zostać przekazany do funkcji ReadFile i CIoseHandle. I teraz, po raz pierwszy w tym rozdziale, spotykamy się z problemem unikodu. Prawdopodobnie w jakimœ inny, idealnym œwiecie, zawartoœć plików jest auto- matycznie rozpoznawana przez system operacyjny, który potrafi przekształcić znaki ASCII na unikod lub odwrotnie. Jednak w naszym programie funkcja Read- File czyta po prostu plik bajt po bajcie nie dokonujšc jakiejkolwiek konwersji. Oznacza to, że funkcja DrawTextA (w pliku wykonywalnym skompilowanym bez zdefiniowanego identyfikatora UNICODE) będzie traktowała tekstjako ASCII, a DrawTextW (w wersji Unicode) -jako unikod. Tak więc, program powinien spróbować okreœlić, czy ma do czynienia z plikiem ASCII czy też w unikodzie, a następnie, zależnie od sytuacji, powinien wywołać DrawTextA lub DrawTextW. Jednak w programie HEAD zdecydowałem się na znacznie prostsze rozwišzanie: niezależnie od sytuacji wywoływana jest funkcja DrawTextA. Rozdział 10 ; ' ' zaso Większoœć programów Microsoft Windows zawiera modyfikowalnš ikonę, którš Windows wyœwietla w lewym górnym rogu paska tytułu okna aplikacji. System wyœwietla tę ikonę także wtedy, gdy program jest umieszczony: w menu Start, na pasku zadań u dołu ekranu, w Exploratorze Windows lub jako skrót na pulpi- cie. Niektóre aplikacje - najczęœciej narzędzia graficzne do rysowania takie jak Windows Paint-wykorzystujš modyfikowalne kursory, reprezentujšce różne ope- racje wykonywane w programie. Wiele programów Windows używa menu, okien dialogowych i pasków przewijania, które sš podstawowymi elementami interfej- su użytkownika w tym systemie. Ikony, kursory, menu i okna dialogowe sš ze sobš zwišzane. Każdy z tych ele- mentów jest odmianš zasobów Windows. Zasoby sš danymi i często przechowu- je się je w pliku .EXE programu, w częœci nie zawierajšcej uruchamialnych da- nych aplikacji. Innymi słowy, zasoby nie sš adresowane bezpoœrednio przez zmien- ne w kodzie programu. Zamiast tego Windows dostarcza funkcje, które jawnie lub niejawnie wczytujš zasoby programu do pamięci i udostępniajš je do użyt- ku. Dwie z tych funkcji już poznaliœmy. Sš to Loadlcon i LoadCursor, które pojawi- ły się w przykładowych programach, w przypisaniach definiujšcych strukturę klas okien programu. Funkcje te wczytywały binarnš ikonę lub kursor z systemu Win- dows i zwracały uchwyty do nich. W tym rozdziale zaczniemy tworzenie swoich własnych ikon, które będziemy wczytywać z pliku .EXE programu. Podręcznik ten omawia następujšce zasoby: ů ikony ů kursory ů cišgi znakowe ů zasoby użytkownika ů menu ů skróty klawiatury ů okna dialogowe ů bitmapy. Pierwsze szeœć zasobów.przedstawimy w tym rozdziale. Okna dialogowe opisu- jemy w rozdziale 11, a bitmapy - w rozdziale 14. 384 Częœć I: Podstawy i 1 Ikony, kursory, cišgi znaków i zasoby użytkownika Jednš z korzyœci płynšcych z używania zasobów jest to, że wiele komponentów aplikacji można włšczyć do pliku .EXE programu. Bez koncepcji zasobów plik binarny, taki jak obraz ikony, musiałby prawdopodobnie rezydować w oddziel- nym pliku, który byłby wczytywany do pamięci przez plik .EXE, lub ikona mu- siałaby zostać zadeklarowana jako tablica bajtów (co mogłoby utrudnić wizuali- zację rzeczywistego jej obrazu). Będšc jednym z zasobów, ikona jest przechowy- wana jako oddzielny edytowalny plik w komputerze projektanta, ale w trakcie procesu budowania aplikacji zostaje włšczona do pliku .EXE. Dodawanie ikony do programu Dodawanie zasobów do programu wymaga użycia pewnych dodatkowych funk- cji Visual C++ Developer Studio. W przypadku ikon używa się programu Image Editor (nazywanego także Graphics Editor) do narysowania obrazu ikony. Obraz jest zachowywany w pliku ikony o rozszerzeniu .ICO. Developer Studio generuje także skrypt zasobów (plik z rozszerzerum .RC, nazywany czasami plikiem defini- cji zasobów), który zawiera listę wszystkich elementów programu i plik nagłów- kowy (RESOURCE.H), pozwalajšcy programowi odwołać się do zasobów. Teraz, gdy wiesz, w jaki sposób te pliki współdziałajš ze sobš, zaczniemy two- rzenie nowego projektu, który nazwiemy ICONDEMO. Jak zwykle w Developers Studio rozpoczruj od wybrania New z menu File, wybierz kartę Projects i zaznacz Win32 Application. W polu Project Name wpisz ICONDEMO i kliknij OK. Deve- lopers Studio utworzy teraz pięć plików, które używane będš do zarzšdzania prze- strzeniš roboczš i projektem. Należš do ruch pliki tekstowe: ICONDEMO.DSW, ICONDEMO.DSP i ICONDEMO.MAK (jeœli zaznaczyłeœ Export makefile when saving project file na karcie Build w oknie dialogowym Options wyœwietlanym po wybraniu Options z menu Tools). Teraz, jak zwykle, wpiszemy kod Ÿródłowy C. Wybierz New z menu File, następnie kartę Files i klilazij C++ Source File. W polu File Name wpisz ICONDEMO.C i klik- nij OK. Developers Studio utworzy dzięki temu pusty plik ICONDEMO.C. Wpisz program pokazany na rysunku 10-1 lub za pomocš opcji Copy As Text skopiuj kod Ÿródłowy z załšczonego CD-ROM-u. ICONDEMO.C /* ICONDEMO.C - Program demonstrujšcy ikony (c) Charles Petzold, 1998 */ ţţinclude ţţinclude "resource.h" LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; Rozdział 10: Menu i inne zasoby 385 int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) ( TCHAR szAppNameC7 = TEXT ("IconDemo") ; HWND hwnd : MSG msg : WNDCLASS wndclass : wndclass.style = CS HREDRAW ţ CS VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; '.;I wndclass.cbWndExtra = 0 : wndclass.hInstance = hInstance ţ wndclass.hIcon = LoadIcon (hInstance, MAKEINTRESOURCE (IDIţICON)) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) : wndclass.hbrBackground = GetStockObject (WHITEţBRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) ( MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MBţICONERROR) ; return 0 : ) hwnd = CreateWindow (szAppName, TEXT ("Icon Demo"), WS OVERLAPPEDWINDOW, CW_USEDEFAULT, CW USEDEFAULT, CW_USEDEFAULT, CW USEDEFAULT, NULL, NULL, hInstance, NULL) : ;ł ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) : 'i while (GetMessage (&msg, NULL, 0, 0)) i TranslateMessage (&msg) ; DispatchMessage (&msg) : l return msg.wParam ; ) LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) i static HICON hIcon ; static int cxIcon, cyIcon, cxClient, cyClient ; HDC hdc : HINSTANCE hInstance ; PAINTSTRUCT ps ; int x, y : ' switch (message) case WM_CREATE : hInstance = ((LPCREATESTRUCT) lParam)->hInstance ; hIcon = LoadIcon (hInstance, MAKEINTRESOURCE (IDIţICON)) : cxIcon = GetSystemMetrics (SM CXICON) : 386 Częœć iů Podstawy (cišg dalszy ze strony 385) cyIcon = GetSystemMetrics (SM_CYICON) ; return 0 : case WM_SIZE : cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; return 0 : case WM_PAINT : hdc = BeginPaint (hwnd, &ps) ; for (y = 0 ; y < cyClient ; y += cyIcon) for (x = 0 ; x < cxClient ; x += cxIcon) DrawIcon (hdc, x, y, hIcon) ; EndPaint (hwnd, &ps) ; return 0 ; case WM DESTROY : PostQuitMessage (0) ; return 0 ; ) return DefWindowProc (hwnd, message, wParam, lParam) ; Rysunek 10-1. Program ICONDEMO Jeœli spróbujesz skompilować ten program, wywołasz błšd, ponieważ plik RE- SOURCE.H, dołšczony na poczštku kodu, jeszcze nie istnieje. Nie będziesz two- rzyć go jednak bezpoœrednio - pozwolisz, aby Developer Studio utworzyło go za ciebie. Wykonasz to dodajšc skrypt zasobów do projektu. Wybierz New z menu File, na- stępnie kartę Files, kliknij Resource Script i wpisz ICONDEMO w pole File Name. Klilalij OK. Developer Studio utworzył dwa kolejne pliki tekstowe: ICONDEMO.RC (skrypt zasobów) i RESOURCE.H (plik nagłówkowy, który umożliwi plikowi z kodem Ÿródłowym C i skryptowi zasobów odwoływanie się do tych samych zde- finiowanych identyfikatorów). Nie próbuj sam edytować tych dwóch plików - po- zwól, aby zrobił to Developer Studio. Jeżeli chcesz przejrzeć skrypt zasobów i plik RESOURCE.H bez używania Developer Studio, umieœć je w Notatniku. Nie zmie- niaj tych plików, chyba że dobrze wiesz, co robisz. Zapamiętaj, że pakiet Develo- per Studio zachowa nowe wersje tych plików tylko wtedy, gdy zostanš one jawnie do niego skierowane lub gdy projekt zostanie przebudowany. Skrypt zasobów jest plikiem tekstowym. Zawiera reprezentacje tekstowe tych zasobów, które mogš być wyrażone przez tekst, takie jak menu i okna dialogo- we. W skrypcie zasobów znajdujš się także odwołania do plików binarnych, któ- re zawierajš zasoby nietekstowe, takie jak ikony i kursory myszy użytkownika. Teraz, gdy istnieje plik RESOURCE.H, ponownie spróbuj skompilować ICONDE- MO. Tym razem uzyskasz komunikat o błędzie mówišcy, że IDI ICON nie jest zdefiniowany. Identyfikator ten pojawia się po raz pierwszy w instrukcji: Rozdział 10: Menu i inne zasoby wndclass.hIcon = LoadIcon (hInstance, MAKEINTRESOURCE (IDIţICON)) : Powyższa instrukcja w ICONDEMO zastšpiła następujšcš, z poprzednich pro- gramów w tej ksišżce: Wndclass.hIcon = LoadIcon (NULL, IDI ŽPPLICATION) ; Zmiana ta jest uzasadniona, ponieważ do tej pory używaliœmy dla aplikacji stan- dardowej ikony, a teraz chcemy użyć własnej. Utwórzmy więc jš. W oknie File View w Developer Studio zobaczysz dwa pliki - ICONDEMO.C i ICONDEMO.RC. Gdy klikniesz ICONDEMO.C, będziesz mógł edytować kod Ÿródłowy. Natomiast gdy klikniesz ICONDEMO.RC, będziesz mógł dodać do pliku nowe zasoby i edytować już istniejšce. Chcšc dodać ikonę, wy- bierz Resource z menu Insert. Kliknij zasób, który chcesz dodać, w tym wypadku Icon, a następnie przycisk New. Wyœwietlona została pusta ikona 32 x 32 piksele, którš można teraz pokoloro- wać. W tym celu możesz wykorzystać ruchome paski narzędzi z zestawem na- rzędzi malarskich i paletš dostępnych kolorów. BšdŸ œwiadom tego, że pasek narzędzi z kolorami zawiera dwie opcje, które nie sš dokładnie kolorami. Czasa- mi okreœla się je jako ekran i negatyw ekranu. Jeœli piksel zostanie pokolorowany ekranem", w rzeczywistoœci będzie przezroczysty. Dowolne tło, na którym zo- " stanie wyœwietlona ikona, będzie widoczne przez takie piksele. Pozwala to two- rzyć ikony, które wyglšdajš na niekwadratowe. Zanim zajdziesz zbyt daleko, kliknij dwukrotnie obszar otaczajšcy ikonę. Otwar- te zostanie okno dialogowe Icon Properties, pozwalajšce zmienić identyfikator ikony i nazwę jej pliku. Developer Studio ustawi prawdopodobnie identyfikator na IDI ICONI. Zmień go na IDI ICON, ponieważ w taki sposób do ikony odwo- łuje się program ICONDEMO. (Przedrostek IDI to skrót od "identyfikator iko- ny"). Zmień także nazwę pliku na ICONDEMO.ICO. Teraz wybierz wyróżniajšcy się kolor (na przykład czerwony) i narysuj na ikonie dużš literę B. Nie musi być ona tak ładna, jak na rysunku 10-2. Rysunek 10-2. W ten sposób wyœwietlana jest w Developer Studio standardowa (32x32) ikona ICONDEMO Program powinien teraz zostać bez problemu skompilowany i uruchomiony. Developer Studio umieœcił nowy wiersz w skrypcie zasobów ICONDEMO.RC, wišżšcy plik ikony (ICONDEMO.ICO) z identyfikatorem (IDI ICON). Plik na- 388 Częœć I: Podstawy główkowy RESOURCE.H zawiera definicję identyfikatora IDI ICON. (Wkrótce przyjrzymy się temu bliżej). Developer Studio kompiluje zasoby używajšc kompilatora zasobów RC.EXE. Tek- stowy skrypt zasobów jest konwertowany do formy binarnej - pliku o rozszerze- niu .RES. Następnie skompilowany plik zasobów zostaje połšczony z plikami OBJ i LIB w kroku LINK. W ten sposób zasoby dodawane sš do końcowego pliku .EXE. Gdy uruchomisz ICONDEMO, ikona programu zostanie wyœwietlona w lewym górnym rogu paska tytułu i na pasku zadań. Jeœli umieœcisz program w menu Start lub jako skrót na pulpicie, ikona pojawi się tam także. ICONDEMO dodatkowo wyœwietla tę ikonę w obszarze roboczym. Używajšc in- strukcji: hIcon = LoadIcon (hInstance, MAKEINTRESOURCE (IDIţICON)) ; program uzyskuje uchwyt ikony. Natomiast stosujšc instrukcje: cxIcon = GetSystemMetrics (SM CXICON) ; cyIcon = GetSystemMetrics (SM CYICON) ; uzyskuje rozmiar ikony. Następnie może wyœwietlić ikonę wielokrotnym wywo- łaniem: DrawIcon (hdc, x, y, hIcon) ; gdzie x i y sš współrzędnymi punktu, w którym umieszczany jest górny lewy róg ikony. Dla większoœci obecnie używanych kart graficznych funlccja GetSystemMetrics z in- deksami SM C7QCON i SM CYICON podaje informację, że rozmiar ikony wynosi 32 x 32 piksele. Taki rozmiar ma ikona, którš utworzyliœmy w Developer Studio. Jest to także rozmiar ikony pojawiajšcej się na pulpicie i wyœwietlanej w obszarze robo- czym programu ICONDEMO. Nie jest to jednak rozmiar ikony wyœwietlanej na pa- sku tytułu programu i na pasku zadań. Rozmiar mniejszej ikony można uzyskać za pomocš GetSystemMetrics z indeksami SM CXSMSIZE i SM CYSMSIZE. (Pierwsze SM oznacza system metryczny, dnxgie - mała (ang. small)). Dla większoœci obecnie użyţ'anych kart graficznych rozmiar małej ikony wynosi 16 x 16 pi.kseli. Może to być problemem. Windows, redukujšc rozmiar ikony z 32 x 32 do 16 x 16, eliminuje zbędne wiersze i kolumny pikseli. W przypadku złożonych obrazów ikon może to powodować zniekształcenia. Z tego powodu najlepiej jest utworzyć spe- cjalnš ikonę o rozmiarze 16 x 16 dla obrazów, które przy zmniejszaniu mogš zostać zniekształcone. Nad obrazem ikony w Developer Studio znajduje się pole listy roz- wijanej, nazwane Device. Po jego prawej stronie znajduje się przycisk, którego klik- nięcie otwiera okno dialogowe New Icon. Wybierz Small (16x16). Teraz możesz narysować dodatkowš ikonę; na przykład takš jak pokazana na rysunku 10-3. .. -:. i "ţ ţ^f d dţi. f-t rv Rysunek 10-3. W ten sposób wyœwietlana jest w Developer Studio mała (16x16) ikona ICONDEMO Rozdział 10: Menu i inne zasoby Nie musisz robić nic więcej w programie. Drugi obraz ikony jest przechowywa- ny w tym samym pliku ICONDEMO.ICO i odwohxje się do niego ten sam iden- tyfikator IM ICON. Windows automatycznie użyje mniejszej ikony, gdy będzie ona bardziej odpowiednia, na przykład na pasku tytułu lub pasku zadań. Duża ikona jest używana do wyœwietlania skrótu na pulpicie i gdy program wywohxje Drawlcon do udekorowania obszaru roboczego. Teraz, gdy poznaliœmy częœć praktycznš, przyjrzyjmy się bliżej temu, co dzieje się za kulisami. Uzyskiwanie uchwytu ikon Jeœli przyjrzysz się plikom ICONDEMO.RC i RESOURCE.H, zobaczysz grupę elementów wygenerowanych przez Developer Studio, które pomagajš w zarzš- dzaniu tymi plikami. Podczas kompilacji skryptu zasobów ważnych jest jednak tylko kilka wierszy. Na rysunku 10-4 pokazane sš zawierajšce je fragmenty pli- ków ICONDEMO.RC i RESOURCE.H. ICONDEMO.RC (fragmenty) //Microsoft Developer Studio generated resource script. ţţinclude "resource.h" ţţinclude "afxres.h" ///////////////////////////////////////////////////////////////////////////// //Icon IDI ICON ICON DISCARDABLE "icondemo.ico" RESOURCE.H (fragmenty) // Microsoft Developer Studio generated include file. // Used by IconDemo.rc ţţdefine IDIţICON 101 Rysunek 10-4. Fragmenty plików ICONDEMO.RC i RESOURCE.H Na rysunku 10-4 pokazane sš pliki ICONDEMO.RC i RESOURCE.H, które wy- glšdajš tak, jakby zostały utworzone zwykłym edytorem tekstów, czyli tak, jak to robili programiœci w latach 80. Jedynš różnicš jest obecnoœć AFXRES.H -pliku nagłówkowego zawierajšcego wiele typowych identyfikatorów używanych przez Developer Studio podczas tworzenia maszynowo generowanych projektów MFC. W tej ksišżce nie będziemy korzystać z tego pliku. Poniższy wiersz, znajdujšcy się w ICONDEMO.RC: IDI_ICON DISCARDABLE "icondemo.ico" jest instrukcjš ICON skryptu zasobów. Ikona ma identyfikator numeryczny IDI ICON równy 101. Dodane przez Developer Studio słowo kluczowe DISCAR- DABLE oznacza, że Windows może w razie potrzeby usunšć ikonę z pamięci, aby uzyskać dodatkowš przestrzeń. System ten może zawsze póŸniej jeszcze raz 390 Częœć I: Podstawy ţ wczytać ikonę bez ingerencji ze strony programu. Atrybut DISCARDABLE jest domyœlny i nie trzeba go wpisywać. Developer Studio umieszcza nazwę pliku w cu- dzysłowie na wypadek, gdyby nazwa lub œcieżka katalogu zawierała spacje. Zasoby, po skompilowaniu i zachowaniu w ICONDEMO.RES oraz dołšczeniu ich przez konsolidator do ICONDEMO.EXE, identyfikowane sš przez typ zaso- bów (w tym wypadku RT ICON) i przez identyfikator (IDI ICON lub 101). Pro- gram może uzyskać uchwyt ikony wywołujšc funkcję Loadlcon: hIcon = LoadIcon (hInstance, MAKEINTRESOURCE (IDIţICON)) ; Należy zauważyć, iż ICONDEMO wywołuje tę funkcję w dwóch miejscach: pod- czas definiowania klasy okna, aby uzyskać ikonę programu, oraz w procedurze okna, aby uzyskać uchwyt ikony do wyœwietlenia w obszarze roboczym. Load1- con zwraca wartoœć typu HICON - uchwyt do ikony. Pierwszym argumentem Loadlcon jest uchwyt wskazujšcy, z którego pliku pobie- rane sš zasoby. Użycie hlnstance oznacza, że zasoby pochodzš z pliku EXE pro- gramu. Drugi argument Loadlcon jest zdefiniowany jako wskaŸnik do cišgu zna- ków. Jak wkrótce zobaczysz, zasoby można identyfikować przez cišgi znaków zamiast przez identyfikatory numeryczne. Makro MAKEINTRESOURCE (make an integer into a resource string - zamień liczbę całkowitš na cišg znaków zasobu) tworzy wskaŸnik z liczby: ţidefine MAKEINTRESOURCE(i> (LPTSTR) ((DWORD) ((WORD) (i))) Funkcja Loadlcon wie, że gdy bardziej znaczšce słowo drugiego argumentu rów- ne jest 0, mniej znaczšce słowo jest identyfikatorem numerycznym ikony. Identy- fikator ikony musi być wartoœciš 16-bitowš. Przykładowe programy prezentowane wczeœniej w tej ksišżce używały predefi- niowanych ikon: LoadIcon (NULL, IDI ŽPPLICATION) ; Windows wie, że to ikona predefiniowana, ponieważ parametr hlnstance jest usta- wiony na NULL. IDI APPLICATION jest zdefiniowany także w WINUSER.H za pomocš MAKEINTRESOURCE: ţidefine IDI ŽPPLICATION MAKEINTRESOURCE(32512) Drugi argument funkcji Loadlcon podsuwa intrygujšce pytanie: czy identyfikator ikony może być cišgiem znaków? Tak, dzieje się to w następujšcy sposób: na li- œcie plików projektu ICONDEMO w Developer Studio zaznacz ICONDEMO.RC. Zobaczysz strukturę drzewiastš, która składa się z następujšcych gałęzi: na gó- rze IconDemo Resources, następnie typ zasobów Icon i na końcu ikona IDI ICON. Jeœli klikniesz prawym przyciskiem myszy identyfikator ikony i wybierzesz z me- nu opcję Properties, będziesz mógł zmieruć identyfikator. Umieszczajšc nazwę w cudzysłowie możesz zmienić identyfikator numeryczny na cišg znaków. Jest to preferowana przeze mnie metoda okreœlania nazw zasobów i będę jej używać w pozostałej częœci ksišżki. Wolę używać nazw tekstowych dla ikon (i częœci innych zasobów), ponieważ nazwa może być nazwš programu. Załóżmy, że program nazwany jest MYPROG. Jeœli użyjesz okna dialogowego Icon Properties, aby wprowadzić MyProg jako identyfikator ikony (za pomocš cudzysłowów), skrypt zasobów będzie zawierać następujšcš instrukcję: Rozdział 10: Menu i inne zasoby MYPROG ICON DISCARDABLE mypro9.ico RESOURCE.H nie będzie zawierać instrukcji #define, która wskazuje, że MYPROG jest identyfikatorem numerycznym. Zamiast tego skrypt zasobów założy, że MYPROG to identyfikator tekstowy. W programie w C używa się funkcji Loadlcon, aby uzyskać uchwyt ikony. Zapewne przypominasz sobie, że istnieje zmienna typu string, zawierajšca nazwę progra- mu: static TCHAR szAppName C] = TEXT ("MyProg") : Oznacza to, że program może wczytać ikonę za pomocš instrukcji: hIcon = LoadIcon (hInstance, szAppName) : która wyglšda o wiele bardziej przejrzyœcie niż makro MŽKEINTRESOURCE. Jeœli jednak wolisz używać liczb zamiast nazw, możesz je zastosować zamiast korzystać z identyfikatorów i cišgów znaków. W oknie dialogowym Icon Proper- ties wprowadŸ w pole ID liczbę. W skrypcie zasobów znajdzie się instrukcja ICON podobna do poniższej: 125 ICON DISCARDABLE myprog.ico Do ikony można odnieœć się dwiema metodami. Oto najprostsza: hIcon = LoadIcon (hInstance, MAKEINTRESOURCE (125)) : Ta jest jednak mniej przejrzysta: hIcon = LoadIcon (hInstance, TEXT ("11125")) : Windows rozpoznaje znak # jako przedrostek liczby w formacie ASCII. Używanie ikon w programie Mimo że Windows dostarcza kilka sposobów wskazywania ikon w programie, wiele programów robi to tylko podczas definiowania klasy okna za pomocš struk- tury WNDCLASS i funkcji RegisterClass. Jak widzieliœmy, ta metoda działa do- brze, w szczególnoœci gdy plik ikony zawiera zarówno standardowy, jak i zmniej- szony obraz ikony. Windows wybiera najbardziej odpowiedni rozmiar obrazu ikony, gdy zachodzi potrzeba jej wyœwietlenia. Dostępna jest rozszerzona wersja RegisterClass, zwana RegisterClassEx, używajš- ca struktury WNDCLASSEX. WNDCLASSEX ma dwa dodatkowe pola: cbSize i hlconSm. Pole cbSize wskazuje rozmiar struktury WNDCLASSEX, a hIconSm musi być ustawione na uchwyt małej ikony. Z tego powodu w strukturze WNDCLAS- SEX ustawia się dwa uchwyty ikon zwišzane z dwoma plikami ikon - jednym dla ikony standardowej, a drugim - dla zmniejszonej. Czy jest to potrzebne? Cóż, nie jest. Jak widzieliœmy, Windows poprawnie uzyskuje obrazy ikon o właœciwych rozmiarach z pojedynczego pliku ikon. Funkcja RegisterClassEx wydaje się być pozbawiona inteligencji RegisterClass. Jeœli pole hlconSm wskazuje na plik ikon zawierajšcy kilka obrazów, użyty zostanie tylko pierwszy. Może to być obraz o standardowym rozmiarze, który będzie następnie zmniejszony. RegisterClassEx została prawdopodobnie zaprojektowana do użytku z kilkoma plikami ikon, z któ- rych każdy zawiera tylko jeden rozmiar obrazu ikony. Ponieważ teraz istnieje możliwoœć umieszczenia kilku rozmiarów ikon w jednym pliku, zalecam używa- nie WNDCLASS i RegisterClass. 392 Częœć I: Podstawy Gdybyœ chciał póŸniej zmienić dynamicznie ikonę programu po jego uruchomie- niu, możesz zrobić to używajšc SetClassLong. Jeœli miałbyœ na przykład drugi plik ikony zwišzany z identyfikatorem IDI ALTTCON, możesz przejœć na tę ikonę uży- wajšc instrukcji: SetClassLong (hwnd, GCL HICON, LoadIcon (hInstance, MAKEINTRESOURCE (IDI ŽLTICON))) Jeœli nie chcesz zachowywać uchwytu ikony programu, a zamiast tego użyć funkcji Drawlcon, by jš gdzieœ wyœwietlić, możesz uzyskać uchwyt wykorzystujšc GetC- lassLong, na przykład: DrawIcon (hdc, x, y, GetClassLong (hwnd, GCL HICON)) ; W niektórych fragmentach dokumentacji Windows Loadlcon jest okreœlana jako anachroniczna i zamiast niej zaleca się używanie Loadlmage. (Loadlcon jest udo- kumentowana w /Platform SDK/Llser Interface Services/Resources/Icons, a Loadlma- ge w /Platform SDK/Llser Interface Services/Resources/Resources). Loadlmage rzeczy- wiœcie jest bardziej elastyczna, ale ustępuje Loadlcon pod względem łatwoœci uży- cia. Zwróć uwagę, że w ICONDEMO Loadlcon jest wywoływana dwa razy dla tej samej ikony. Nie stanowi to problemu i nie wymaga użycia dodatkowej pamięci. Loadlcon jest jednš z kilku funkcji, które uzyskujš uchwyt, ale nie wymagajš jego usunięcia. W rzeczywistoœci dostępna jest funkcja Destroylcon, ale używa się jej w połšczeniu z funkcjami Createlcon, Createlconlndirect i CreatelconFromResource. Pozwalajš one programowi dynamicznie tworzyć obraz ikony w sposób algoryt- miczny. Używanie własnych kursorów Używanie własnych kursorów w programach podobne jest do używania własnych ikon, z wyjštkiem tego, że większoœć programistów uznaje kursory dostarczane przez Windows za odpowiednie do większoœci zastosowań. Własne kursory sš monochromatyczne, i majš rozmiar 32 x 32 piksele. Kursor w Developer Studio tworzy się w ten sam sposób co ikonę (wybierajšc Resource z menu Insert i na- stępnie Cursor), ale nie należy zapominać o zdefiniowaniu goršcego punktu. Własny kursor ustawia się w definicji klasy za pomocš poniższej instrukcji: wndclass.hCursor = LoadCursor (hInstance, MAKEINTRESOURCE (IDC CURSOR)) ; lub następujšcej, jeœli jest zdefiniowany za pomocš nazwy tekstowej: wndclass.hCursor = LoadCursor (hInstance, szCursor) ; Za każdym razem, gdy wskaŸnik myszy zostanie umieszczony nad oknem opartym na tej klasie, wyœwietlony będzie kursor zwišzany z IDI CURSOR lub szCursor. Jeżeli używasz okien potomnych, możesz zechcieć wyœwietlać różne kursory w zależnoœci od tego, które okno znajduje się pod wskaŸnikiem. Jeœli program definiuje klasy okna dla tych okien potomnych, możesz uzyskać różne kursory dla każdej klasy, ustawiajšc odpowiednio pole hCursor dla każdej z nich. jeżeli używasz predefiniowanych kontrolek okien potomnych, możesz zmienić pole hCursor klasy okna wykorzystujšc: SetClassLong (hwndChild, GCL HCURSOR, LoadCursor, TEXT, ("childcursor")); Rozdział 10: Menu i inne zasoby 393 Jeœli dzielisz obszar roboczy na mniejsze obszary logiczne bez użycia okien po- tomnych, możesz użyć SetCursor, aby zmienić kursor myszy: SetCursor (hCursor) ; SetCursor powinieneœ wywoływać podczas przetwarzania komunikatu WM MO- USEMOVE. W przeciwnym przypadku Windows użyje kursora okreœlonego w klasie okna, aby przerysować wskaŸnik w trakcie ruchu myszš. Dokumenta- cja mówi, że SetCursor działa szybko, jeœli kursor nie jest zmieniany. Cišgi znaków jako zasoby Tworzenie zasobów z cišgami znaków może się wydawać dziwne - przecież bez problemów bezpoœrednio w kodzie Ÿródłowym łšczy się zwykłe cišgi znaków zdefiniowane jako zmienne. Zasoby z cišgami znaków służš przede wszystkim do łatwego tłumaczenia pro- gramów na inne języki. W dalszej częœci tego rozdziału dowiesz się, że menu i okna dialogowe także sš elementami skryptu zasobów. Jeœli użyjesz zasobów z cišga- mi znaków zamiast umieszczać cišgi bezpoœrednio w kodzie Ÿródłowym, cały tekst używany przez program będzie znajdował się w jednym pliku - skrypcie zasobów. Jeżeli tekst z tego skryptu zostanie przetłumaczony na inny język, je- dynš czynnoœciš potrzebnš do utworzenia odpowiedniej wersji językowej pro- gramu będzie ponowne jego konsolidowanie. Metoda ta jest o wiele bezpiecz- niejsza niż wykonywanie zmian w kodzie Ÿródłowym. (Jednak oprócz następne- go przykładowego programu nie będę używać tabeli cišgów znakowych w żad- nym innym programie w tej ksišżce, ponieważ powodujš one, że kod staje się skomplikowany i mniej przejrzysty). Tabelę cišgów znaków tworzy się wybierajšc IZesource z menu Insert i następnie String Table. Cišgi znaków pokazywane sš na liœcie po prawej stronie ekranu. Zaznacz cišg klikajšc go dwukrotnie. Dla każdego możesz okreœlić identyfikator i treœć. W skrypcie zasobów cišgi znaków pojawiajš się jako wielowierszowa instrukcja, podobna do poruższej: STRINGTABLE DISCARDABLE BEGIN IDS STRINGl, "character string 1" IDS_STRING2. "character string 2" [inne definicje cidgów znaków] END Jeœli programowałbyœ w Windows parę lat temu i tworzył tabelę cišgów znaków ręcznie za pomocš edytora tekstów (co, jak zapewne się domyœlasz, było łatwiej- sze niż tworzenie jej w Developer Studio), zastšpiłbyœ nawiasy klamrowe instruk- cjami BEGIN i END. Skrypt zasobów może zawierać kilka tabel cišgów znaków, ale każdy identyfika- tor musi unikatowo identyfikować tylko jeden z nich. Każdy cišg znaków musi być umieszczony w jednym wierszu i może mieć maksymalnie 4097 znaków. Jako tabulatorów i znaków końca wiersza używaj \t i \n. Te znaki kontrolne rozpo- znawane sš przez funkcje DrawText i Message.Box. 394 Częœć I: Podstawy Ro; Program może użyć wywołania LoadString, aby skopiować cišg znaków z zaso- Za bów do bufora w segmencie danych programu: LoadString (hInstance, id, szBuffer, iMaxLength) ; Argument id odnosi się do numeru identyfikatora, który poprzedza każdy cišg znaków w skrypcie zasobów. szBuf fer jest wskaŸnikiem do tablicy znaków, w której umieszczany jest cišg znaków, a iMaxLength - maksymalnš liczbš znaków do prze- transferowania do szBufţ'er. Funkcja zwraca liczbę znaków w cišgu. Identyfikatory cišgów znaków poprzedzajšce każdy z nich sš zwykle identyfi- katorami makro, zdefiniowanymi w pliku nagłówkowym. Wielu programistów Windows używa przedrostka IDSţ dla identyfikatorów cišgów znaków. Czasa- mi podczas wyœwietlania takiego cišgu trzeba osadzić w nim nazwę pliku lub inne informacje. W takim przypadku możesz umieœcić w cišgu znaki formatujš- ce języka C i użyć go jako formatujšcy cišg znaków w funkcji wsprintf. Cały tekst zasobów, włšczajšc w to tekst w tabeli cišgów znaków, przechowywa- ny jest w skompilowanym pliku zasobów i w końcowym pliku .EXE w formacie Unicode. Funkcja LoadStringW wczytuje bezpoœrednio tekst Unicode. Natomiast funkcja LoadStringA (jedyna taka funkcja dostępna w Windows 98) wykonuje konwersję tekstu z Unicode na lokalnš stronę kodowš. Przyjrzyjmy się przykładowi funkcji używajšcemu trzech cišgów znaków do wy- œwietlenia trzech komunikatów o błędach w oknie komunikatu. Jak widzisz, plik nagłówkowy RESOURCE.H zawiera trzy identyfikatory dla tych komunikatów. define IDS_FILENOTFOUND 1 itdefine IDS_FILETOOBIG 2 iţdefine IDSţFILEREADONLY 3 Natomiast w skrypcie zasobów znajduje się następujšca tabela cišgów znaków: STRINGTABLE BEGIN IDS_FILENOTFOUND, "File %s not found." IDS_FILETOOBIG, "File %s too large to edit." IDS_FILEREADONLY, "File %s is read-only." END Kod Ÿródłowy w C dołšcza ten plik nagłówkowy i definiuje funkcję wyœwietlajš- cš okno komunikatu. (Zakłada także, że szAppName jest zmiennš globalnš, za- wierajšcš nazwę programu). OkMessage (HWND hwnd, int iErrorNumber, TCHAR *szFileName) TCHAR szFormat [40] ; TCHAR szBuffer [60] ; LoadString (hInst, iErrorNumber, szFormat, 40) ; wsprintf (szBuffer, szFormat, szFileName) : return MessageBox (hwnd, szBuffer, szAppName, MB OK ţ MBţICONEXCLAMATION) ; Aby wyœwietlić okno komunikatu z komunikatem "File not found", program wywołuje: OkMessage (hwnd, IDSţFILENOTFOUND, szFileName) ; Rozdział 10: Nlenu i inne zasoby 395 Zasoby użytkownika Windows definiuje także zasoby własne (ang. custom resource) nazywane także zasobami użytkownika (ang. user-defined resource) (przy czym użytkownikiem je- steœ ty jako programista, a nie szczęœliwa osoba, która otrzymuje twój program do użytku). Zasoby użytkownika sš pomocne przy dołšczaniu różnych danych do pliku .EXE i do uzyskiwania dostępu do tych danych wewnštrz programu. Dane mogš być dowolnego formatu. Funkcje Windows używane przez program do uzyskania dostępu do zasobów użytkownika powodujš, że Windows wczy- tuje dane do pamięci i zwraca do nich wskaŸnik. Z takimi danymi możesz zro- bić, co tylko chcesz. Prawdopodobnie będziesz korzystać z nich przy przecho- wywaniu i udostępnianiu różnych informacji, co jest wygodniejsze niż uzyski- wanie danych z plików zewnętrznych za pomocš funkcji wejœcia. Załóżmy, że masz plik nazwany BINDATA.BIN, zawierajšcy zestaw danych po- trzebnych do wyœwietlenia czegoœ na ekranie. Plik może być w dowolnie wybra- nym formacie. Jeœli w projekcie MYPROG masz skrypt zasobów MYPROG.RC, możesz utworzyć w Developer Studio zasób użytkownika wybierajšc Resource z menu Insert i naciskajšc przycisk Custom. Wpisz nazwę, pod jakš zasób będzie znany, na przykład BIN'TYPE. Developer Studio utworzy nazwę zasobu (w tym wypadku IDR BIIţTTYPEI) i wyœwietli okno pozwalajšce na wprowadzenie da- nych binarnych. Nie musisz jednak tego robić. Kliknij nazwę IDR BINTYPE1 prawym przyciskiem myszy i wybierz Properties. Następnie wprowadŸ nazwę pliku, na przykład BINDATA.BIN. W skrypcie zasobów znajdzie się instrukcja podobna do poniższej: IDR BINTYPE1 BINTYPE BINDATA.BIN Jest ona podobna do instrukcji ICON w ICONDEMO z wyjštkiem tego, że typ zasobów BIN'I'YPE został właœnie przez nas utworzony. Podobnie jak w przy- padku ikon, masz możliwoœć jako nazwy zasobu używać nazw tekstowych za- miast identyfikatorów numerycznych. ; Gdy skompilujesz i skonsolidujesz program, cały plik BINDATA.BIN zostanie do- łšczony do pliku MYPROG.EXE. Podczas inicjacji programu (na przykład podczas przetwarzania komunikatu WMţCREATE) możesz uzyskać uchwyt tego zasobu: hResource = LoadResource (hInstance, FindResource (hInstance. TEXT ("BINTYPE"). MAKEINTRESOURCE (IDR BINTYPE1))) Zmienna hResource jest zdefiniowana za pomocš typu HGLOBAL, będšcego uchwytem bloku pamięci. Mimo swojej nazwy, LoadResource w rzeczywistoœci nie wczytuje zasobu do pamięci. Funkcje LoadResource i FindResource użyte razem w powyższy sposób sš dla zasobów użytkownika odpowiednikami funkcji Lo- adlcon i LoadCursor. W rzeczywistoœci to te ostatnie używajš funkcji LoadResource i FindResource. Gdy potrzebny jest dostęp do tekstu, wywołaj LockResource: pData = LockResource (hResource) ; 396 Częœć I: Podstawy LockResource wczytuje zasób do pamięci (jeœli nie był wczytany wczeœniej) i zwraca wskaŸnik do niego. Gdy zakończysz operację na zasobie, możesz usunšć go z pa- mięci: FreeResource (hResource) ; Zasoby usuwane sš z pamięci także podczas zamykania programu, nawet jeżeli nie wywołasz Freeftesource. Przyjrzyjmy się przykładowemu programowi używajšcemu trzech zasobów - ikony, tabeli cišgów znaków i zasobu użytkownika. Pokazany na rysunku 10-5 program POEPOEM wyœwietla w swoim obszarze roboczym tekst wiersza "An- nabel Lee" Edgara Allana Poe. Zasobem użytkownika jest plik POEPOEM.TXT, zawierajšcy tekst wiersza. Plik tekstowy zakończony jest odwrotnym ukoœnikiem POEPOEM.C /* POEPOEM.C - Program demonstrujdcy zasoby użytkownika (c) Charles Petzold, 1998 */ ţţinclude ţţinclude "resource.h" LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; HINSTANCE hInst ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) TCHAR szAppName C16], szCaption C64], szErrMsg C64] ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; LoadString (hInstance, IDS APPNAME, szAppName, sizeof (szAppName) / sizeof (TCHAR)) ; LoadString (hInstance, IDS CAPTION, szCaption, sizeof (szCaption) / sizeof (TCHAR)) ; wndclass.style = CS_HREDRAW ţ CSţllREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (hInstance, szAppName) ; wndclass.hCursor = LoadCursor (NULL, IDC ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITEţBRUSH) wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) ( j 397 Rozdział 10: Menu i inne zasoby LoadStringA (hInstance, IDS ŽPPNAME, (char *) szAppName, sizeof (szAppName)) ; LoadStringA (hInstance, IDSţERRMSG, (char *) szErrMsg, sizeof (szErrMsg)) ; MessageBoxA (NULL, (char *) szErrMsg, (char *) szAppName, MBţICONERROR) ; return 0 ; 1 hwnd = CreateWindow (szAppName, szCaption, WS_OVERLAPPEDWINDOW ţ WS_CLIPCHILDREN, CW_USEDEFAULT, CW USEDEFAULT, CW_USEDEFAULT, CW USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) ( TranslateMessage (&msg) ; DispatchMessage (&msg) ; ) return msg.wParam ; ? LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ( static char * pText ; static HGLOBAL hResource ; static HWND hScroll ; static int iPosition, cxChar, cyChar, cyClient, iNumLines, xScroll ; HDC hdc ; PAINTSTRUCT ps ; RECT rect ; TEXTMETRIC tm ; switch (message) ( case WM_CREATE : hdc = GetDC (hwnd) ; GetTextMetrics (hdc, &tm) ; cxChar = tm.tmAveCharWidth ; cyChar = tm.tmHeight + tm.tmExternalLeading ; ReleaseDC (hwnd, hdc) ; xScroll = GetSystemMetrics (SM CXVSCROLL) ; hScroll = CreateWindow (TEXT ("scrollbar"), NULL, WS_CHILD ţ WS VISIBLE ţ SBSţVERT, 0, 0, 0, 0, hwnd, (HMENU) 1, hInst, NULL) ; hResource = LoadResource (hInst, FindResource (hInst, TEXT ("AnnabelLee"), TEXT ("TEXT"))) ; 398 Częœć I: (cišg dalszy ze strony 397) pText = (char *) LockResource (hResource) ; iNumLines = 0 ; while (*pText != '\\' && *pText != '\0') ( if (*pText = '\n') iNumLines ++ ; pText = AnsiNext (pText) ; ) *pText = '\0' SetScrollRange (hScroll, SB CTL, 0, iNumLines, FALSE) ; SetScrollPos (hScroll, SB CTL, 0, FALSE) ; return 0 ; case WM_SIZE : MoveWindow (hScroll, LOWORD (lParam) - xScroll, 0, xScroll, cyClient = HIWORD (lParam), TRUE) ; SetFocus (hwnd) ; return 0 ; case WM_SETFOCUS : SetFocus (hScroll) ; return 0 ; case WM USCROLL : switch (wParam) f case SB_TOP : iPosition = 0 ; break ; case SB_BOTTOM : iPosition = iNumLines ; _ break ; case SB_LINEUP : iPosition -= 1 ; break ; case SB_LINEDOWN : iPosition += 1 ; break ; case SB_PAGEUP : iPosition -= cyClient / cyChar ; break ; case SB_PAGEDOWN : iPosition += cyClient / cyChar ; break ; case SB_THUMBPOSITION : iPosition = LOWORD (lParam) ; break ; ) iPosition = max (0, min (iPosition, iNumLines)) ; if (iPosition != GetScrollPos (hScroll, SB CTL)) ( SetScrollPos (hScroll, SB_CTL, iPosition, TRUE) ; InvalidateRect (hwnd, NULL, TRUE) ; l Rozdział 10: Menu i inne zasoby 399 return 0 ; case WM_PAINT : hdc = BeginPaint (hwnd, &ps) ; i,.. i'.: pText = (char *) LockResource (hResource) ; GetClientRect (hwnd, &rect) ; rect.left += cxChar ; i': rect.top += cyChar * (1 - iPosition) ; DrawTextA (hdc, pText, -1, &rect, DTţEXTERNALLEADING) ; EndPaint (hwnd, &ps) ; return 0 ; case WM DESTROY : FreeResource (hResource) ; PostOuitMessage (0) ; return 0 ; I: return DefWindowProc (hwnd, message, wParam, lParam) ; POEPOEM.RC (fragmenty) //Microsoft Developer Studio generated resource script. ţţinclude "resource.h" 4ţinclude "afxres.h" /////////////////////////////////////////////////////////////////////////// // TEXT ANNABELLEE TEXT DISCARDABLE "poepoem.txt" /////////////////////////////////////////////////////////////////////////// // Icon POEPOEM ICON DISCARDABLE "poepoem.ico" ,k ////////////////////////////////////////////////////////ţ////////////////// // String Table STRINGTABLE DISCARDABLE BEGIN IDS_APPNAME "PoePoem" IDS_CAPTION """Annabel Lee"" by Edgar Allan Poe" IDSţERRMSG "This program requires Windows NT!" END ,.. RESOURCE.H (fragmenty) // Microsoft Developer Studio generated include file. // Used by PoePoem.rc 4idefine IDS_APPNAME 1 4ţdefine IDS_CAPTION 2 lidefine IDS ERRMSG g 400 Częœć I: Podstawy (cišg dalszy ze strony 397) POEPOEM.TXT It was many and many a year ago, In a kingdom by the sea, That a maiden there lived whom you may know By the name of Annabel Lee; And this maiden she lived with no other thought Than to love and be loved by me. I was a child and she was a child In this kingdom by the sea, But we loved with a love that was more than love - I and my Annabel Lee - With a love that the winged seraphs of Heaven Coveted her and me. And this was the reason that, long ago, In this kingdom by the sea, A wind blew out of a cloud, chilling My beautiful Annabel Lee; So that her highborn kinsmen came And bore her away from me, To shut her up in a sepulchre In this kingdom by the sea. The angels, not half so happy in Heaven, Went envying her and me - Yes! that was the reason (as all men know, In this kingdom by the sea) That the wind came out of the cloud by night, Chilling and killing my Annabel Lee. But our love it was stronger by far than the love Of those who were older than we - Of many far wiser than we - And neither the angels in Heaven above Nor the demons down under the sea Can ever dissever my soul from the soul Of the beautiful Annabel Lee: For the moon never beams, without bringing me dreams Of the beautiful Annabel Lee; And the stars never rise, but I feel the bright eyes Of the beautiful Annabel Lee: And so, all the night-tide, I lie down by the side Of my darling - my darling - my life and my bride, In her sepulchre there by the sea - In her tomb by the soundin9 sea. [May, 1849] Rozdział 10: Menu i inne zasoby 401 POEPOEM.ICO Rysunek 10-5. Program POEPOEM, zawierajšcy ikonę i zasób użytkownika W skrypcie zasobów POEPOEM.RC zasób użytkownika ma nadany typ TEXT i nazwę tekstowš "ArmabelLee". ANNABELLEE TEXT POEPOEM.TXT Podczas przetwarzania komunikatu WMţCREATE w WndProc uchwyt zasobu uzyskiwany jest przy użyciu FindResource i LoadResource. Zasób jest blokowany przy użyciu LockResource, a mała podprocedura zastępuje na końcu pliku odwrotny ukoœnik (\) zerem (na potrzeby funkcji DrawText używanej póŸniej podczas prze- twarzania komunikatu WM PAINT). Zwróć uwagę, że użyta została kontrolka paska przewijania okna potomnego, a nie rodzica (nadrzędnego). Ma ona automatyczny interfejs klawiatury, więc w PO- EPOEM niepotrzebne jest przetwarzanie komunikatu KEYDOWN. POEPOEM używa także trzech cišgów znaków, których identyfikatory zdefinio- wane sš w pliku nagłówkowym RESOURCE.H. Cišgi IDS APPNAME i IDS CAP- TION wczytywane sš do pamięci na poczštku programu za pomocš LoadString: LoadString (hInstance, IDS APPNAME, szAppName, sizeof (szAppName) / sizeof (TCHAR)) ; LoadString (hInstance, IDS CAPTION, szCaption, sizeof (szCaption) sizeof (TCHAR)) ; Zwróć uwagę, że wywołania te poprzedzajš RegisterClass. Jeœli uruchomisż wer- sję Unicode programu POEPOEM w Windows 98, wywołania tych dwóch funk- cji nie powiodš się. Chociaż LoadStringA jest bardziej złożona od LoadStringW (ponieważ LoadStringA konwertuje cišg znaków z Unicode na ANSI, a LoadStringW wczytuje go bezpoœrednio), ta druga nie jest dostępna w Windows 98. Oznacza to, że jeżeli funkcja RegisterCIassW nie zadziała w Windows 98, funkcja Message- BoxW (która jest dostarczana w tym systemie) nie może używać cišgów znaków wczytanych do programu przy użyciu LoadStringW. Z tego powodu program wczytuje IDS APPNAME i IDS ERRMSG za pomocš LoadStringA i wyœwietla zwykłe okno komunikatu za pomocš MessageBoxA: if (!RegisterClass (&wndclass)) f LoadStringA (hInstance, IDS ŽPPNAME, (char *) szAppName, sizeof (szAppName)) ; LoadStringA (hlnstance, IDSţERRMSG, (char *) szErrMsg, sizeof (szErrMsg)) ; 402 Częœć I: Podstawy MessageBoxA (NULL, (char *) szErrMsg, (char *) szAppName, MBţICONERROR) ; return 0 ; Zwróć uwagę na zamianę zmiennych TCHAR na wskaŸniki char. Majšc wszystkie używane w POEPOEM cišgi znaków zdefiniowane jako zaso- by, tłumaczowi jest o wiele łatwiej przekonwertować go na innš wersję języko- wš. Oczywiœcie do jego zadań należy także przethxmaczenie tekstu wiersza "An- nabel Lee", co przypuszczalnie jest znacznie trudniejszym zadaniem. Menu Czy pamiętasz skecz Monty Pythona o sklepie z serami? Wyglšdał on mniej wię- cej tak: Pewien mężczyzna idzie do sklepu z serami i prosi o okreœlony rodzaj sera. W sklepie nie ma takiego. Prosi o inny rodzaj, potem o następny, potem o jesz- cze inny (tak wylicza kolejne 40, z których każdy jest bardzo osobliwy) i cišgle słyszy odpowiedŸ "Nie, nie, nie, nie...:'. Cała scena kończy się strzelaninš. Sytuacji takiej można byłoby zapobiec używajšc.menu. Menu to lista dostępnych opcji. Mówi głodnemu klientowi, co może zaserwować kuchnia, a w programie Windows - jakie operacje może wykonać aplikacja. Menu jest prawdopodobnie najważniejszš częœciš standardowego interfejsu użyt- kownika oferowanego przez programy Windowsowe, a dołšczanie menu do apli- kacji jest relatywnie łatwym aspektem programowania w Windows. Menu defi- niuje się w Developer Studio. Każdemu wybieralnemu elementowi menu nada- wany jest unikatowy numer ID. Nazwę menu możesz okreœlić w strukturze kla- sy okna. Gdy użytkownik wybiera element menu, Windows wysyła do progra- mu komunikat WM COMMAND, zawierajšcy numer ID. Po omówieniu menu zakończę ten rozdział opisem klawiszy skrótów, a dokład- niej ich kombinacjami używanymi do duplikowania funkcji menu. Założenia menu Pasek menu okna wyœwietlany jest pod paskiem tytuhx. Nazywa się go czasem głównym menu programu lub menu najwyższego poziomu. Elementy menu naj- wyższego poziomu zawierajš zwykle menu rozwijane, nazywane również pod- menu. Można definiować także kilkupoziomowe, zagnieżdżone podmenu, czyli element menu rozwijanego może otwierać kolejne menu rozwijane. Czasami jego elementy otwierajš okna dialogowe, wyœwietlajšce dodatkowe informacje. (Okna dialogowe zostanš omówione w następnym rozdziale). Większoœć okien nadrzęd- nych ma po lewej stronie paska tytułu małš ikonę programu. Ikona ta otwiera menu systemowe, które także jest menu rozwijanym. Elementy menu rozwijanych mogš być zaznaczane, co oznacza narysowanie przez Windows małego znacznika po lewej stronie nazwy elementu. Znaczniki pozwa- lajš użytkownikowi wybierać różne opcje programu z poziomu menu. Opcje te mogš, ale nie muszš wykluczać się nawzajem. Elementy menu najwyższego po- ziomu nie mogš być zaznaczane. Rozdział 10: Menu i inne zasoby 403 Elementy menu najwyższego poziomu i menu rozwijanych mogš być dostępne, niedostępne lub szare. Czasami zamiast okreœleń dostępne i niedostępne używa się aktywne i nieaktywne. Elementy menu oznaczone jako dostępne lub niedo- stępne dla użytkownika wyglšdajš tak samo, ale nazwa elementu szarego jest wyœwietlana, oczywiœcie, na szaro. Z punktu widzenia użytkownika elementy dostępne, niedostępne i szare mogš być wybierane (podœwietlane). Oznacza to, że użytkownik może kliknšć myszš niedostępny element menu, przesunšć na niego pasek podœwietlenia lub naci- snšć klawisz z jego literš. Jednak z punktu widzenia programu elementy dostęp- ne, niedostępne i szare funkcjonujš odmiennie. Windows wysyła do programu komunikat WMţCOMMAND tylko dla dostępnych elementów menu. Elemen- tów niedostępnych i szarych używa się dla opcji, które nie sš aktualnie popraw- ne. Jeœli chcesz poinformować użytkownika, że element menu nie jest poprawny, wyœwietl go na szaro. Struktura menu Podczas tworzenia lub modyfikowania menu w programie dobrze jest traktować menu najwyższego poziomu i menu rozwijane jako oddzielne menu. Menu naj- wyższego poziomu, wszystkie jego podmenu oraz menu systemowe (które także jest menu rozwijanym) majš własne uchwyty. Każdy element menu jest zdefiniowany przez trzy cechy. Pierwszš jest to, co pojawia się w menu. Może to być cišg tekstowy albo bitmapa. Drugš cechš jest albo numer ID przesyłany przez Windows do programu w komunikacie 4VMţCOMMAND, albo uchwyt menu rozwijanego, które system wyœwietla po wybraniu tego elementu. Trzecia cecha opisuje atrybuty elementu menu m.in. okreœlajšc, czy jest on dostępny, niedostępny czy szary. Definiowanie menu Aby użyć Developer Studio w celu dodania menu do skryptu zasobów progra- mu, wybierz Resource z menu Insert i następnie Menu (prawdopodobnie sam się już tego domyœliłeœ). Możesz teraz interaktywnie zdefiniować menu. Każdy jego element ma zwišzane z nim okno dialogowe Item Properties, zawierajšce cišg tekstowy. Jeœli zaznaczone jest pole Pop-up, element otwiera menu rozwijane i nie jest z nim zwišzany żaden identyfikator. W przeciwnym wypadku element ge- neruje komunikat WMţCOMMAND z okreœlonym numerem ID. Te dwa ro- dzaje elementów menu pojawiajš się w skrypcie zasobów odpowiednio jako POPUP i MENUITEM. Wpisujšc tekst dla elementu menu, możesz wstawić znak & (ampersand), aby wskazać, że następujšcy po nim znak ma być podkreœlony, gdy Windows wy- œwietli menu. Podkreœlony znak jest stosowany przez system, gdy użytkownik wybiera element menu za pomocš klawisza [Alt]. Jeieli zaznaczysz opcję Grayed w oknie dialogowym Menu Items ProperHes, ele- ment nie wygeneruje komunikatu WMţCOMMAND, będzie nieaktywny, a jego tekst szary. Jeœli wybierzesz opcję Inactive, element menu będzie nieaktywny i nie wygeneruje komunikatu WM COMMAND, ale jego tekst będzie wyœwietlany nor- 404 Częœć I: Podstawy malnie. Opcja Checked umieszcza znacznik obok elementu menu. Opcja Separator powoduje wyœwietlenie poziomego paska separujšcego w menu rozwijanym. W cišgu znaków dla elementów menu rozwijanych można używać znaku tabu- lacji kolumnowej \t. Tekst następujšcy po \t jest umieszczany w nowej kolumnie oddalonej w prawo o tyle, aby pomieœcić najdłuższy cišg tekstowy w pierwszej kolumnie menu rozwijanego. Zobaczymy, jak to działa, gdy będziemy omawiać klawisze skrótów na końcu tego rozdziału. Znak \a w cišgu znaków wyrównuje do prawej następujšcy po nim tekst. Wartoœci ID sš liczbami, które Windows przesyła do procedury okna w komuni- katach menu. Identyfikatory w menu powinny być unikatowe. Zgodnie z przyję- tš konwencjš, używam identyfikatorów zaczynajšcych się od liter IDM (identyfi- kator menu). Odwoływanie się do menu w programie Większoœć aplikacji Windows ma tylko jedno menu w skrypcie zasobów. Możesz nadać mu nazwę tekstowš, odpowiadajšcš nazwie programu. Programiœci czę- sto używajš takiej samej nazwy programu, jak nazwa menu, aby ten sam cišg znaków mógł zostać użyty także w klasie okna i nazwie ikony programu. Pro- gram może następnie odwołać się do menu w definicji klasy okna: wndclass.lpszMenuName = szAppName ; Odwoływanie się do menu w klasie okna jest najczęœciej używanš metodš pobie- rania zasobów menu, ale nie jedynš. Aplikacja Windows może wczytać zasoby menu do pamięci za pomocš funkcji LoadMenu, podobnej do wczeœniej opisanych funkcji Loadlcon i LoadCursor. LoadMenu zwraca uchwyt menu. Jeœli użyjesz nazwy dla menu w skrypcie zasobów, instrukcja ta będzie wyglšdać podobnie do poniższej: hMenu = LoadMenu (hInstance, TEXT ("MyMenu")); Jeżeli natomiast użyjesz liczby, wywołanie LoadMenu przyjmie następujšcš po- stać: hMenu = LoadMenu (hInstance, MAKEINTRESOURCE (IDţMENU)) : Uchwyt menu można podać jako dziewišty parametr funkcji CreateWindow: hwnd = CreateWindow (TEXT ("MyClas"), TEXT ("Window Caption"), WS OVERLAPPEDWINDOW, CW USEDDEFAULT, CW USEDDEFAULT, CW USEDDEFAULT, CW USEDDEFAULT, NULL, hMenu, hInstance, NULL) ; W tym wypadku menu podane w CreateWindow zastępuje dowolne menu wpro- wadzone w klasie okna. Menu w klasie okna można traktować jako menu do- myœlne dla okien opartych na tej klasie, jeœli dziewišty parametr CreateWindow jest równy NLTLL. Dzięki temu można używać różnych menu dla kilku okien opartych na tej samej klasie okna. Można uzyskać także nazwę okna równš NULL w klasie okna oraz uchwyt menu równy NULL w wywołaniu CreateWindow i dopiero po utworzeniu okna przypi- sać menu : SetMenu (hWnd, hMenu) ; Rozdział 10: Menu i inne zasoby 405 Forma powyższego typu pozwala dynamicznie zmieniać menu okna. Przykład takiej operacji zobaczysz w programie NOPOPUPS, pokazanym w dalszej częœci rozdziału. Dowolne menu dołšczane do okna jest usuwane wraz z usunięciem okna. Menu, które nie sš dołšczone do żadnego okna, należy usuwać samodzielnie za pomo- cš wywołania DestroyMenu przed zakończeniem działania programu. Menu i komunikaty Gdy użytkownik wybierze element menu, Windows wysyła zwykle do procedu- ry okna kilka komunikatów. Najczęœciej jednak program ignoruje większoœć z nich i przekazuje je po prostu do DefYVindowProc. Jednym z takich komunikatów jest WM ITEMMENU z następujšcymi parametrami: wParam: Uchwyt menu głównego lParam: 0 Wartoœciš wParam jest uchwyt głównego menu, nawet jeœli użytkownik wybiera element menu systemowego. Programy Windows generalnie ignorujš komuni- kat WM-ITEMMENU. Mimo że komunikat ten jest generowany i daje możliwoœć zmiany menu przed wybraniem elementu, wszelkie modyfikacje menu najwyż- j szego poziomu w tym momencie wprowadzałyby użytkownika w zakłopotanie. Program odbiera także komunikaty WM MENUSELECT. Sš one generowane, gdy użytkownik przesuwa kursor myszy po menu. Komunikaty te okazujš się po- mocne przy implementowaniu paska stanu, zawierajšcego pełny opis opcji menu. Komunikatowi WM MENUSELECT towarzyszš następujšce parametry: LOWORD (wParam): Wybrany element: identyfikator menu lub indeks menu rozwijanego HIWORD (wParam): Flagi wyboru lParam: Uchwyt menu zawierajšcego wybrany element WM_MENUSELECT jest komunikatem œledzšcym menu. Wartoœć wParam mówi, który element menu jest aktualnie zaznaczony (podœwietlony). Flagi wyboru w bardziej znaczšcym słowie wParam mogš być kombinacjš następujšcych sta- łych: MF GRAYED, MF DISABLED, MF CHECKED, MF BITMAP, MF POPUP, MF HELP, MF SYSMENU i MF MOUSESELECT. Parametru WM MENUSE- LECT możesz użyć, gdy chcesz zmieniać coœ w obszarze klienta okna na podsta- wie ruchów podœwietlenia po elementach menu. Większoœć programów menu przekazuje ten komunikat do Def4VindowProc. Gdy Windows jest gotowy do wyœwietlenia menu rozwijanego, wysyła do proce- dury okna komunikat WM-INITMENUPOPUP z następujšcymi parametrami: wParam: Uchwyt menu rozwijanego LOWORD (IParam): Indeks menu rozwijanego HIWORD (IParam): 1 dla menu systemowego, 0 - w przeciwnym wypadku Komunikat ten jest ważny, jeœli chcesz udostępnić lub wyłšczyć elementy menu rozwijanego przed jego wyœwietleniem. Załóżmy, że program może kopiować tekst ze Schowka za pomocš polecenia Wklej z menu rozwijanego. Gdy otrzy- 406 Częœć I: Podstawy masz komunikat WM INITMENUPOPUP dla tego menu rozwijanego, powinie- neœ sprawdzić, czy w Schowku znajduje się tekst. Jeœli Schowek jest pusty, powi- nieneœ przyszarzyć w menu element Wklej. Przykład takiej operacji zobaczysz w programie POPPAD pod koniec tego rozdziału. Najważniejszym komunikatem menu jest WM COMMAND. Wskazuje on, że użytkownik wybrał dostępny element z menu okna. Jak pamiętasz z rozdziału 8, komunikaty WM COMMAND generowane sš także przez kontrolki okna potom- nego. Jeżeli używasz takich samych identyfikatorów dla menu i kontrolek okien potomnych, ich komunikaty możesz rozróżnić sprawdzajšc wartoœć lParam, któ- ra dla elementu menu wynosi 0. Menu Kontrolki LOWORD (wParam) ID menu ID kontrolki HIWORD (wParam) 0 Kod powiadomienia LParam 0 Uchwyt okna potomnego Komunikat WMţSYSCOMMAND jest podobny do WM COMMAND z wyjšt- kiem tego, że WM SYSCOMMAND sygnalizuje wybranie elementu menu syste- mowego: wParam: Identyfikator menu lParam: 0 Jeœli jednak WM SYSCOMMAND jest rezultatem kliknięcia myszš, LOWORD (lParam) i HIWORD (lParam) zawierajš współrzędne x i y położenia kursora my- szy. Dla WMţSYSCOMMAND parametr ID menu wskazuje, który element menu sys- temowego został wybrany. Dla predefiniowanych elementów tego menu dolne cztery bity powinny być zamaskowane za pomocš iloczynu (AND) z OxFFFO. Uzy- skana wartoœć będzie jednš z następujšcych: SC SIZE, SC MOVE, SC MINIMI- ZE, SC MAXIMIZE, SC NEXTWINDOW, SC PREVWINDOW, SC CLOSE, SC VSCROLL, SC HSCROLL, SC ARRANGE, SC RESTORE i SC TASKLIST. Dodatkowo wParam może być równy SC MOUSEMENU lub SC KEYMENU. jeœli dodasz elementy do menu systemowego, mniej znaczšce słowo wParam bę- dzie zdefiniowanym przez ciebie identyfikatorem menu. Chcšc uruknšć konflik- tów z predefiniowanymi identyfikatorami menu, używaj wartoœci poniżej 0xF000. Komunikaty WM SYSCOMMAND należy przekazywać do Def 4VindowProc. Je- żeli tego nie zrobisz, efektywnie wyłšczysz polecenia menu systemowego. Ostatnim komunikatem, któremu się przyjrzymy, jest WM MENUCHAR. W rze- czywistoœci nie jest on komunikatem menu. Windows wysyła go do procedury okna w jednym z dwóch przypadków: jeœli użytkownik naciœnie [Alt] i klawisz znaku nie odpowiadajšcy żadnemu elementowi menu lub jeœli użytkownik naci- œnie klawisz znaku nie odpowiadajšcy żadnemu elementowi menu, gdy wyœwie- tlone jest menu rozwijane. Komunikatowi WM MENUCHAR towarzyszš nastę- pujšce parametry: Rozdział 10: Menu i inne zasoby 407 LOWORD (wParam): Kod znaku (ASCII lub Unicode) HIWORD (wParam): Kod wyboru lParam: Uchwyt menu Kod wyboru przybiera wartoœci: ů 0 Menu rozwijane nie jest wyœwietlone ů MF POPUP Menu rozwijane jest wyœwietlone ů MF SYSMENU Wyœwietlone jest rozwijane menu systemowe Programy Windowsowe przekazujš zwykle ten komunikat do funkcji Def 4Vindow- Proc, która zwraca do Windows 0, co powoduje, że system generuje sygnał dŸwię- kowy. Komunikatu WM MENUCHAR użyjemy w programie GRAFMENU po- kazanym w rozdziale 14. Program przykładowy ;,::.. Przyjrzyjmy się prostemu przykładowi. Program MENUDEMO, pokazany na rysunku 10-6, ma pięć elementów w menu głównym: File, Edit, Background, Ti- mer i Help. Każdy z tych elementów otwiera menu rozwijane. MENUDEMO wykonuje najprostsze i najczęœciej używane typy operacji na menu, włšczajšc w to przechwytywanie komunikatów WM COMMAND i sprawdzanie mniej znaczš- cego słowa wParam. MENUDEMO.C ,* MENUDEMO.C - Oemonstracja menu (c) Charles Petzold, 1998 *ţ i; . ţţinclude include "resource.h" 4fdefine ID TIMER i LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; ' „ TCHAR szAppNameC] = TEXT ("MenuOemo") : int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) ( i,:. HWND hwnd ; MSG msg : WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CS VREDRAW ; wndclass.lpfnWndProc = WndProc : wndclass.cbClsExtra = 0 : r ,; wndclass.cbWndExtra ř 0 : wndclass.hInstance = hInstance ţ wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) : wndclass.hCursor = LoadOursor (NULL IDC ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE-BRUSH) ; 408 Częœć I: Podstawy (cišg dalszy ze strony 407) wndclass.lpszMenuName = szAppName ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) ( MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MBţICONERROR) ; return 0 ; ) hwnd = CreateWindow (szAppName, TEXT ("Menu Demonstration"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW USEDEFAULT. CW_USEDEFAULT, CW USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) ( TranslateMessage (&msg) ; DispatchMessage (&msg) ; ) return msg.wParam ; LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ( static int idColor (5] = ( WHITE_BRUSH, LTGRAY_BRUSH, GRAY BRUSH, DKGRAY BRUSH. BLACK BRUSH ) ; static int iSelection = IDMţBKGND WHITE ; HMENU hMenu ; switch (message) ( case WM_COMMAND: hMenu = GetMenu (hwnd) ; switch (LOWORD (wParam)) ( case IDM_FILE_NEW: case IDM_FILE_OPEN: case IDM_FILE_SAVE: case IDMţFILE_SAVE_AS: MessageBeep (0) ; return 0 ; case IDM_APPţEXIT: SendMessage (hwnd, WM CLOSE, 0, 0) ; return 0 ; case IDM_EDIT_UNDO: case IDM_EDIT_CUT: case IDM_EDIT_COPY: case IDMţEDITţPASTE: Rozdział 10: Menu i inne zasoby case IDMţEDIT_CLEAR: MessageBeep (0) ; ' return 0 ; case IDM_BKGND_WHITE: // Uwaga: W poniższym kodzie case IDM_BKGND_LTGRAY: // założono, że identyfikatory case IDM_BKGND_GRAY: // od IDM_WHITE do IDM_BLACK case IDM BKGND DKGRAY: // sd kolejnymi liczbami całkowitymi case IDM BKGND BLACK: // o ustalonym porzddku. CheckMenuItem (hMenu, iSelection, MF UNCHECKED) ; iSelection = LOWORD (wParam) ; CheckMenuItem (hMenu, iSelection, MFţCHECKED) ; SetClassLong (hwnd, GCLţHBRBACKGROUND, (LONG) GetStockObject ' (idColor [LOWORD (wParam) - IDM BKGND WHITE])) ; InvalidateRect (hwnd, NULL, TRUE) ; return 0 ; case IDM_TIMERţSTART: if (SetTimer (hwnd, ID TIMER, 1000, NULL)) , EnableMenuItem (hMenu, IDM TIMER START, MF_GRAYED) ; EnableMenuItem (hMenu, IDM TIMER STOP, MFţENABLED) ; 1 l return 0 ; case IDM_TIMER_STOP: KillTimer (hwnd, ID_TIMER) ; EnableMenuItem (hMenu, IDM TIMER START, MF ENABLED) ; EnableMenuItem (hMenu, IDM TIMERţSTOP, MF GRAYED) ; return 0 ; case IDM_APP_HELP: MessageBox (hwnd, TEXT ("Help not yet implemented!"), szAppName, MBţICONEXCLAMATION ţ MB OK) ; return 0 ; case IDM APP_ABOUT: MessageBox (hwnd, TEXT ("Menu Demonstration Program\n") TEXT ("(c) Charles Petzold, 1998"), szAppName, MBţICONINFORMATION ţ MB OK) ; return 0 ; i. t break ; case WM TIMER: MessageBeep .(0) ; return 0 ; case WM DESTROY: PostOuitMessage (0) ; return 0 ; ) return DefWindowProc (hwnd, message, wParam, lParam) ; 3. 410 Częœć I: Podstawy (cišg dalszy ze strony 409) MENUDEMO.RC (fragmenty) //Microsoft Developer Studio generated resource script. ţţinclude "resource.h" ţţinclude "afxres.h" /////////////////////////////////////////////////////////////////////////// // Menu MENUDEMO MENU DISCARDABLE BEGIN POPUP "&File" BEGIN MENUITEM "&New", ID_MENUITEM40020 MENUITEM "&Open", IDM_FILE_OPEN MENUITEM "&Save", IDM_FILE SAVE MENUITEM "Save &As... , IDMţFILĘ SAVE AS MENUITEM SEPARATOR MENUITEM "E&xit", IDM ŽPPţEXIT END POPUP "&Edit" BEGIN MENUITEM "&Undo", IDMţEDIT UNDO MENUITEM SEPARATOR MENUITEM "C&ut", IDM_EDIT_CUT MENUITEM "&Copy", IDM_EDIT_COPY MENUITEM "&Paste", IDM_EDIT PASTE MENUITEM "De&lete", IDMţEDIT CLEAR END POPUP "&Background" BEGIN MENUITEM "&White", IDM_BKGND_WHITE, CHECKED MENUITEM "&Light Gray", IDM_BKGND_LTGRAY MENUITEM "&Gray", IDM_BKGND_GRAY MENUITEM "&Dark Gray", IDM_BKGND_DKGRAY MENUITEM "&Black", IDMţBKGNDţBLACK END POPUP "&Timer" BEGIN MENUITEM "&Start", IDM TIMER_START MENUITEM "S&top", IDM TIMERţSTOP, GRAYED END POPUP "&Help" BEGIN MENUITEM "&Help... , IDM APP HELP MENUITEM "&About MenuDemo... , IDM APP ABOUT END END RESOURCE.H (fragmenty) // Microsoft Developer Studio generated include file. // Used by MenuDemo.rc Rozdział 10: INenu i inne zasoby 411 ţţdefine IDM_FILE_NEW 40001 ; define IDM_FILE_OPEN 40002 4idefine IDM_FILE_SAVE 40003 ildefine IDM_FILE_SAVE ŽS 40004 ildefine IDM_APP_EXIT 40005 ildefine IDM_EDIT UNDO 40006 ildefine IDM_EDIT CUT 40007 ildefine IDM_EDIT_COPY 40008 ildefine IDM_EDIT_PASTE 40009 ildefine IDM_EDIT_CLEAR 40010 ildefine IDM_BKGND_WHITE 40011 ţţdefine IDM_BKGND_LTGRAY 40012 ţţdefine IDM_BKGND_GRAY 40013 4ldefine IDM_BKGND_DKGRAY 40014 4idefine IDM_BKGND_BLACK 40015 ţţdefine IDM_TIMER_START 40016 ildefine IDM TIMER_STOP 40017 define IDM_APP_HELP 40018 ildefine IDM APP ABOUT 40019 Rysunek 10-6. Program MENUDEMO Skrypt zasobów MENUDEMO.RC powinien pomóc ci w zrozumieniu koncepcji definiowania menu. Nazwš tekstowš menu jest MenuDemo. Większoœć elemen- ; tów ma podkreœlonš literę, co oznacza, że musisz przed niš wpisać znak & (am- persand). Instrukcja MENUITEM SEPARATOR jest wynikiem zaznaczenia pola Separator w oknie dialogowym Menu Item Properties. Zwróć uwagę na to, że ' jeden z elementów menu ma zaznaczonš opcję Checked, a inny - Grayed. Pięć elementów menu rozwijanego Background powinno być wprowadzone w kolej- noœci pokazanej na rysunku, aby zapewnić ustawienie identyfikatorów w porzšd- ku numerycznym, na którym opiera się program. Wszystkie identyfikatory ele- mentów menu zdefiniowane sš w pliku RESOURCE.H. Dla większoœci elementów menu rozwijanych File i Edit program MENUDEMO, gdy odbierze komunikat WM COMMAND, po prostu generuje sygnał dŸwięko- wy. Menu rozwijane Background zawiera pięć kolorów, których MENUDEMO może użyć do pokolorowania tła. W skrypcie zasobów MENUDEMO.R element White (z identyfikatorem menu równym IDM BKGND WHITE) jest oznaczony jako CHECKED, co powoduje wyœwietlenie przy nim znacznika. W MENUDE- MO.C wartoœć poczštkowa iSelection jest ustawiana na IDM BKGND WHITE. Pięć kolorów w menu rozwijanym Background wyklucza się wzajemnie. Gdy ME- NUDEMO.C odbierze komunikat WM_COMMAND, w którym wParam jest jed- nym z pięciu elementów menu Background, musi usunšć znacznik z poprzednio wybranego koloru i wyœwietlić go przy nowym. Aby to zrobić, najpierw uzysku- je uchwyt swojego menu: hMenu = GetMenu (hwnd) ; Funkcja CheckMenultem używana jest do usunięcia znacznika znajdujšcego się obok aktualnie wybranego elementu: CheckMenuItem (hMenu, iSelection, MF UNCHECKED) ; Wartoœć iSelection jest ustawiana na wartoœć wParam, a przy nowym kolorze wy- œwietlany jest znacznik: 412 Częœć I: Podstawy Rozc iSelection = wParam ; CheckMenultem (hMenu, iSelection, MF_CHECKED) ; Kolor tla w klasie olma jest teraz zastępowany nowym, a obszar roboczy okna- odœwieżany. Windows zamalowuje okno przy użyciu nowego koloru. Menu rozwijane zawiera dwie opcje: Start i Stop. Poczštkowo opcja Stop jest sza- ra (co ustalone zostało w definicji menu w skrypcie zasobów). Gdy wybierzesz De opcję Start, MENUDEMO spróbuje wlšczyć zegar. Jeœli czynnoœć ta powiedzie się, przyszarzana jest opcja Start, a opcja Stop - ustawiana jako aktywna: EnableMenuItem (hMenu, IDM TIMER START, MF GRAYED) ; EnableMenuItem (hMenu, IDM TIMERţSTOP, MFţENABLED) ; Po otrzymaniu komunikatu WM COMMAND z wParam równym IDM TI- MER STOP, MENUDEMO usuwa zegar, aktywuje opcję Start i przyszarza opcję Stop: EnableMenuItem (hMenu, IDM TIMERţSTART, MFţENABLED) ; EnableMenuItem (hMenu, IDM TIMER STOP, MFţGRAYED) ; Zwróć uwagę na to, że MENUDEMO nie może odebrać komunikatu WMţCOM- MAND z wParam równym IDM TIMER START w trakcie działania zegara. Ana- logicznie nie może odebrać komunikatu WMţCOMMAND z wParam równym IDM TIMER STOP, gdy zegar stoi. Gdy MENUDEMO odbierze komunikat WMţCOMMAND z parametrem wPa- ram równym IDM APP ABOUT lub IMţAPP HELP, wyœwietla okng komunikatu (w następnym rozdziale zastšpimy je oknem dialogowym). ţ hMei Gdy MEDUDEMO odbierze komunikat WM COMMAND z parametrem wPa- hMer ram równym IDM_APP_EXIT, wysyła do siebie komunikat WM CLOSE. Jest to ten sam komunikat, który DefWindowProc kieruje do procedury okna, gdy odbie- AppE rze komunikat WM SYSCOMMAND z wParam równym SC CLOSE. Omówimy AppE to bardziej szczegółowo w przykładzie z programem POPPAD2, pokazanym pod Appe A koniec tego rozdziału. ppe Appe Konwenanse w menu Appe Format menu rozwijanych File i Edit w MENUDEMO jest podobny do stosowa- hMen nych w innych programach Windows. Jednym z celów w Windows jest dostar- czanie użytkownikowi rozpoznawalnego interfejsu, nie wymagajšcego uczenia Appeţ się od podstaw w każdym programie. Niewštpliwie pomaga w tym tworzenie Apper menu File i Edit, wyglšdajšcych tak samo w każdym programie Windows, i uży- Apper wanie tych samych liter do wybierania opcji za pomocš klawisza [Alt]. Apper Oprócz File i Edit pozostałe menu będš prawdopodobnie różne w różnych apli- Apper kacjach Windows. Projektujšc menu powinieneœ przyglšdać się istniejšcym pro- Appen gramom tego systemu i starać się uzyskać spójnoœć. Oczywiœcie, jeœli uważasz, że programy te sš niepoprawne i wiesz, jak zrobić coœ lepiej, nikt cię przed tym hMenu nie powstrzyma. Należy pamiętać również o tym, że korygowanie menu wyma- Appen ga tylko zmiany skryptu zasobów, a nie kodu programu. Elementy menu można Appen przemieszczać póŸniej bez większych problemów. Appen Appenţ Appem Rozdział 10: Menu i inne zasoby 413 Mimo że menu programu może zawierać instrukcje MENUITEM w najwyższym poziomie, stosuje się je rzadko, ponieważ zbyt łatwo można je wybrać przez po- myłkę. Jeżeli wprowadzisz taki element, użyj znaku zapytania po cišgu teksto- wym, aby wskazać, że nie otwiera on menu rozwijanego. Definiowanie menu - trudniejszy sposób Definiowanie menu w skrypcie zasobów programu to zwykle najłatwiejszy spo- sób dodania menu do okna, ale nie jedyny. Możesz poradzić sobie bez skryptu zasobów i utworzyć menu w całoœci wewnštrz programu, używajšc dwóch funkcji: CreateMenu i AppendMenu. Po zakończeniu definiowania menu, możesz przesłać uchwyt menu do CreateMenu i użyć SetMenu, aby uaktywnić menu okna. ;; Robi się to w następujšcy sposób (CreateMenu zwraca po prostu uchwyt do no- ;; I wego menu): 'i hMenu = CreateMenu () ; Menu jest poczštkowo puste. AppendMenu wstawia do niego elementy. Musisz uzyskać różne uchwyty menu najwyższego poziomu i menu rozwijanych. Menu rozwijane sš konstruowane oddzielnie - ich uchwyty wstawia się potem do menu najwyższego poziomu. Kod pokazany na rysunku 10-7 tworzy menu podanš metodš - w efekcie uzyskujemy takie samo menu jak w programie MENUDE- MO. Dla uproszczenia kod używa cišgów znaków ASCII. ( hMenu = CreateMenu () ; hMenuPopup = CreateMenu () ; AppendMenu..(hMenuPopup, MF_STRING, IDM_FILE_NEW, "&New") ; AppendMenu (hMenuPopup. MF_STRING, IDM_FILE_OPEN, "&Open...") : AppendMenu (hMenuPopup, MF_STRING, IDM_FILE_SAVE. "&Save") ; '!i AppendMenu (hMenuPopup. MF_STRING, IDMţFILEţSAVE ŽS, "&Save &As...") : AppendMenu (hMenuPopup, MF_SEPARATOR, 0, NULL) ; AppendMenu (hMenuPopup, MF STRING, IDM ŽPP EXIT, "E&xit") ; AppendMenu (hMenuPopup, MFţPOPUP, hMenuPopup, "&File") ; 1 hMenuPopup = CreateMenu () ; AppendMenu (hMenuPopup, MF_STRING, IDMţEDIT UNDO, "&Undo") : L AppendMenu (hMenuPopup, MF_SEPARATOR, 0, NULL) ; AppendMenu (hMenuPopup, MF_STRING, IDM_EDIT_CUT, "&Cu&t") ; AppendMenu (hMenuPopup, MF_STRING, IDM_EDIT_COPY, "&Copy") ; AppendMenu (hMenuPopup, MF_STRING, IDM_EDIT_PASTE, "&Paste") ; AppendMenu (hMenuPopup, MFţSTRING, IDM EDIT CLEAR, "De&lete") ; AppendMenu (hMenuPopup, MFţPOPUP, hMenuPopup, "&Edit"> : hMenuPopup = CreateMenu () ; AppendMenu (hMenuPopup, MF_STRINGţ MFţCHECKED, IDM_BKGND_WHITE, "&White") ; AppendMenu (hMenuPopup, MF_STRING, IDM_BKGND_LTGRAY, "&Light Gray") : AppendMenu (hMenuPopup, MF_STRING, IDM_BKGND_GRAY, "&Gray") ; AppendMenu (hMenuPopup, MF_STRING, IDM_BKGND_DKGRAY, "&Dark Gray") : AppendMenu (hMenuPopup, MFţSTRING, IDM BKGNDţBLACK, "&Black") ; 414 Częœć Iţ Podstawy i ci g dalszy ze strorc 413) y AppendMenu (hMenuPopcp, MF-POPUP, hMenuPopup, "&Background") ; hMenuPopup = CreateMenu () ; AppendMenu (hMenuPopup, MF_STRING, IDM_TIMER_START, "&Start") ; AppendP1enu (hMenuPopup, MF-STRING ţ MF GRAYED, IDM TIMER STOP, "S&top") ; AppendMenu (hMenuPopup, MF POPUP, hMenuPopup, "&Timer") ; hMenuPopup = CreateMenu () ; AppendMenu (hMenuPopup, MF_STRING, IDM_HELP_HELP, "&Help") ; AppendMenu (hMenuPopup, MF STRING, IDM APP ABOUT, "&About MenuDemo.. ") ; AppendMenu (hMenuPopup, MF-POPUP, hMenuPopup, "&Help") ; Rysunek 10-7. Kod C tworzy bez użycia skryptu zasobów takie samo menu jak w programie MEţTUDEMO Prawdopodobnie zgodzisz się,,że menu uzyskiwane za pomocš skrypiu zasobów jest bardziej przejrzyste i łatwiejsze do utworzenia. Nie zalecam tworzenia menu w ten sposób, jedynie pokazuję, jak można to zrobić. Oczywiœcie można byłoby uproœcić ten kod, używajšc tablic ze strukturami, które zawierajš wszystkie cišgi znakowe elementów menu, identyfikatory i parametry. Jeœli jednak chciałbyœ tak zrobić, rów- nie dobrze możesz skorzystać z trzeciej metody tworzenia menu dostarczanej przez Windows. Funkcja LoadMenulndirect pobiera wskaŸni.k do struktury typu MENU- TTEMTEMPLATE i zwraca uchwyt menu. Funkcja ta używana jest w Windows do konstruowania nowego menu po wczytaniu standardowego menu ze skryptu zaso- bów. Jeżeli masz odwagę, możesz spróbować to wykonać. Ruchome menu rozwijane (menu podręczne) Menu rozwijanych możesz używać także bez menu najwyższego poziomu, na przykład utworzyć menu rozwijane wyœwietlane w dowolnym miejscu ekranu. Jednš z metod jest wyœwietlanie menu rozwijanego w odpowiedzi na kliknięcie prawym przyciskiem myszy. Program POPMENU przedstawiony na rysunku 10- 8 pokazuje, jak to zrobić. POPMENU.C /* POPMENU.C - Demonstracja menu rozwijanego (c) Charles Petzold, 1998 llinclude 4lirţclude "resource.h" */ LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; Rozdział 10: Menu i inne zasoby 415 HINSTANCE hInst ; TCHAR szAppNameC7 = TEXT ("PopMenu") : int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) ( HWND hwnd ; MSG msg : WNDCLASS wndclass : wndclass.style = CS_HREDRAW ţ CS VREDRAW : wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL. szAppName) : wndclass.hCursor = LoadCursor (NULL, IDC ŽRROW) wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITEţBRUSH) : wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName : if (!RegisterClass (&wndclass)) ( MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MBţICONERROR) ; return 0 ; hInst = hInstance : hwnd = CreateWindow (szAppName, TEXT ("Popup Menu Demonstration"), WS_OVERLAPPEDWINDOW CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) : while (GetMessage (&msg, NULL, 0, 0)) ( TranslateMessage (&msg) : DispatchMessage (&msg) : ) return msg.wParam ; LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ( static HMENU hMenu ; static int idColor C57 = f WHITE_BRUSH, LTGRAY BRUSH GRAY BRUSH, DKGRAY_BRUSH, BLACK BRUSH ) : static int iSelection ţ IDM BKGND WHITE ; POINT point ; switch (message) t case WM CREATE: 416 Częœć h Podstawy (cišg dalszy ze strony 415) hMenu = LoadMenu (hInst, szAppName) ; hMenu = GetSubMenu (hMenu, 0) ; return 0 ; case WM RBUTTONUP: point.x = LOWORD (lParam) ; point.y = HIWORD (lParam) ; ClientToScreen (hwnd, &point) ; TrackPopupMenu (hMenu, TPM_RIGHTBUTTON, point.x, point.y, 0, hwnd, NULL) ; return 0 ; case WM COMMAND: switch (LOWORD (wParam)) ( case IDM_FILE_NEW: case IDM_FILE_OPEN: case IDMţFILEţSAVE: case IDMţFILE_SAVE AS: case IDMţEDIT UNDO: case IDM_EDIT CUT: case IDM_EDITţCOPY: case IDM_EDIT_PASTE: case IDMţEDIT_CLEAR: MessageBeep (0) ; return 0 ; case IDM_BKGND WHITE: // Uwaga: W poniższym kodzie case IDM BKGND_LTGRAY: // założono, że identyfikatory case IDM_BKGND_GRAY: // od IDM_WHITE do IDM_BLACK case IDM_BKGND_DKGRAY: // sš kolejnymi liczbami całkowitymi case IDM BKGND BLACK: // o ustalonym porzddku. CheckMenuItem (hMenu, iSelection, MF UNCHECKED) ; iSelection = LOWORD (wParam) ; CheckMenuItem (hMenu, iSelection, MF CHECKED) ; SetClassLong (hwnd, GCL HBRBACKGROUND, (LONG) GetStockObject (idColor CLOWORD (wParam) - IDMţBKGND WHITE))) ; InvalidateRect (hwnd, NULL, TRUE) ; return 0 ; case IDM APP_ABOUT: MessageBox (hwnd, TEXT ("Popup Menu Demonstration Program\n") TEXT ("(c) Charles Petzold, 1998"), szAppName, MBţICONINFORMATION ţ MB OK) ; return 0 ; case IDM_APPţEXIT: SendMessage (hwnd, WM CLOSE, 0, 0) ; return 0 ; case IDM APP HELP: Rozdział 10: Menu i inne zasoby 417 MessageBox (hwnd, TEXT ("Help not yet implemented!"), szAppName, MB-ICONEXCLAMATION ţ MB OK) ; return 0 : break ; case WM_DESTROY: PostOuitMessage (0) ; i return 0 : ) return DefWindowProc (hwnd, message, wParam, lParam) ; 1 POPMENU.RC (fragmenty) //Microsoft Developer Studio generated resource script. i..: ţţinclude "resource.h" include "afxres.h" i: ////////////////////////////////////////////////////////////////////// // Menu POPMENU MENU DISCARDABLE BEGIN POPUP "MyMenu" BEGIN POPUP "&File" I BEGIN IDM_FILE_NEW MENUITEM "&New". ` MENUITEM "&Open", IDM_FILE OPEN MENUITEM "&Save", IDM_FILE SAVE MENUITEM "Save &As", IDM-FILE SAVE AS MENUITEM SEPARATOR MENUITEM "E&xit", IDM APP EXIT END POPUP "&Edit" BEGIN IDM EDIT UNDO MENUITEM "&Undo". - MENUITEM SEPARATOR MENUITEM "Cu&t", IDM_EDIT_CUT MENUITEM "&Copy". IDM_EDIT_COPY MENUITEM "&Paste", IDM EDIT PASTE MENUITEM "De&lete", IDM EDIT CLEAR END POPUP "&Background" BEGIN MENUITEM "&White" IDM_BKGND_WHITE, CHECKED MENUITEM "&Light Gray". IDM_BKGND_LTGRAY MENUITEM "&Gray". IDM_BKGND_GRAY MENUITEM "&Dark Gray", IDM_BKGND DKGRAY MENUITEM "&Black", IDM-BKGND BLACK END POPUP "&Help" BEGIN MENUITEM "&Help...", IDM APP_HELP MENUITEM "&About PopMenu... , IDM APP ABOUT r. ţ. 418 Częœć I: Podstawy (cišg dalszy ze strony 417) i END END END RESOURCE.H (fragmenty) // Microsoft Developer Studio generated include file. // Used by PopMenu.rc ţţdefine IDM_FILE_NEW 40001 ţţdefine IDM_FILE_OPEN 40002 ţţdefine IDM_FILE_SAVE 40003 tţdefine IDM_FILEţSAVE ŽS 40004 ţţdefine IDM_APP_EXIT 40005 ţţdefine IDM_EDIT_UNDO 40006 ţţdefine IDM_EDIT CUT 40007 ţţdefine IDM_EDIT_COPY 40008 lldefine IDM_EDIT_PASTE 40009 4idefine IDM_EDIT_CLEAR 40010 4tdefine IDM_BKGND_WHITE 40011 ţfdefine IDM_BKGND_LTGRAY 40012 ţţdefine IDM_BKGND_GRAY 40013 ţIdefine IDM_BKGND_DKGRAY 40014 ţţdefine IDM_BKGND_BLACK 40015 ţţdefine IDM_APP HELP 40016 ţţdefine IDM APP ABOUT 40017 Rysunek 10-8. Program POPMENU Skrypt zasobów POPMENU.RC definiuje menu podobne do tego w MENUDE- MO.RC. Różniš się tym, że menu najwyższego poziomu zawiera tylko jeden ele- ment - menu rozwijane nazwane MyMenu, które otwiera opcje File, Edit, Back- ground i Help. Te cztery opcje można umieœcić na pionowej liœcie, zamiast w menu głównym na liœcie poziomej. Po pojawieniu się komunikatu WMţCREATE w WndProc POPMENU dostaje uchwyt pierwszego menu rozwijanego (zwanego MyMenu): hMenu = LoadMenu (hInst, szAppName) ; hMenu = GetSubMenu (hMenu, 0) ; Po pojawieniu się komunikatu WM_RBUTTONUP POPMENU dostaje pozycję wskaŸnika myszy, konwertuje jš na współrzędne ekranu i przekazuje je do Track- PopupMenu: point.x = LOWORD (lParam) ; point.y = HIWORD (lParam) ; ClientToScreen (hwnd, &point) ; TrackPopupMenu (hMenu, TPMţRIGHTBUTTON, point.x, point.y, 0, hwnd, NULL) ; Następnie Windows wyœwietla menu rozwijane z elementami File, Edit, Back- ground i Help. Wybranie jednej z tych opcji powoduje, że zagnieżdżone menu pojawiajš się po prawej stronie. Menu funkcjonuje tak samo, jak zwykłe menu. Rozdział 10: Menu i inne zasoby Jeœli chcesz użyć tego samego menu w TrackPopupMenu i jako menu głównego, będziesz miał trudnoœci, ponieważ funkcja ta wymaga uchwytu menu rozwija- nego. Sposób obejœcia tego problemu podany jest w artykule ID Q99806 Micro- soft Knowledge Base. Używanie menu systemowego Okna nadrzędne, utworzone za pomocš stylu zawierajšcego WS SYSMENU, wy- œwietlajš pole menu po lewej stronie paska tytułu. Jeżeli chcesz, możesz zmody- fikować to menu, dodajšc własne polecenia. Kilka lat temu programiœci Windows umieszczali często w menu systemowym opcję About. Ponieważ dzisiaj modyfi- kowanie menu systemowego nie jest już tak powszechne, używana wówczas me- toda może teraz posłużyć jako nieelegancki sposób dołšczenia menu do krótkie- go programu bez definiowania go w skrypcie zasobów. Należy tylko przestrze- gać jednego ograniczenia: numery identyfikatorów używane dla poleceń menu systemowego powinny być niższe od 0xF000. W przeciwnym wypadku spowo- dujš konflikty z identyfikatorami używanymi przez Windows dla poleceń zwy- kłego menu. Należy także pamiętać, że podczas przetwarzania komunikatów WM SYSCOMMAND dla tych poleceń trzeba skierować pozostałe komunikaty WM SYSCOMMAND do DefWindowProc. Jeœli tego nie zrobisz, efektywnie wy- łšczysz wszystkie standardowe opcje w menu systemowym. Program POORMENU (menu dla ubogich - ang. Poor Person's Menu), pokazany na rysunku 10-9, dołšcza pasek separatora i trzy polecenia do menu systemowe- go. Ostatnie z tych poleceń usuwa dodatki. POORMENU.C /* POORMENU.C - Menu systemowe dla ubogich (c) Charles Petzold, 1998 */ include 4idefine IDM_SYS_ABOUT 1 ţ define IDM_SYS_HELP 2 ţidefine IDM SYS-REMOVE 3 LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; static TCHAR szAppName[7 = TEXT ("PoorMenu") ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) HMENU hMenu ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS HREDRAW ţ CS VREDRAW ; 420 Częœć I: Podstauţy (cišg dalszy ze strony 419) wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) ( MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MB ICONERROR) ; return 0 ; l hwnd = CreateWindow (szAppName, TEXT ("The Poor-Person's Menu"), WS OVERLAPPEDWINDOW, CW_USEDEFAULT, CW USEDEFAULT, CW_USEDEFAULT, CW USEDEFAULT, NULL, NULL, hInstance, NULL) ; hMenu = GetSystemMenu (hwnd, FALSE) ; AppendMenu (hMenu, MF_SEPARATOR, 0, NULL) ; AppendMenu (hMenu, MF STRING, IDM SYS_ABOUT, TEXT ("About...")) ; AppendMenu (hMenu, MF STRING, IDM SYS_HELP, TEXT ("Help...")) ; AppendMenu (hMenu, MF STRING, IDM SYS REMOVE, TEXT ("Remove Additions")) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) f TranslateMessage (&msg) ; DispatchMessage (&msg) ; ) return msg.wParam ; l LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) f switch (message) case WM SYSCOMMAND: switch (LOWORD (wParam)) ( case IDM SYS_ABOUT: MessageBox (hwnd, TEXT ("A Poor-Person's Menu Program\n") TEXT ("(c) Charles Petzold, 1998"), szAppName, MB OK ţ MB ICONINFORMATION) ; return 0 ; case IDM SYS_HELP: MessageBox (hwnd, TEXT ("Help not yet implemented!"), Rozdział 10: Menu i inne zasoby 421 return 0 ; szAppName, MBţOK ţ MBţICONEXCLAMATION) ; case IDM_SYSţREMOVE: GetSystemMenu (hwnd, TRUE) : return 0 ; ) break ; case WM DESTROY: PostOuitMessage (0) ; return 0 ; ) return DefWindowProc (hwnd, message, wParam, lParam) ; Rysunek 10-9. Program POORMENU Na poczštku POORMENU.C zdefiniowane sš trzy identyfikatory menu: ţţdefine IDM ŽBOUT 1 define IDM HELP 2 define IDMţREMOVE 3 Po utworzeniu okna programu POORMENU uzyskuje uchwyt menu systemo- wego: hMenu = GetSystemMenu (hwnd, FALSE) ; Gdy wywołujesz funkcję GetSystemMenu po raz pierwszy, powinieneœ ustawić jej drugi parametr na FALSE, aby umożliwić modyfikację menu. Menu konstruowane jest za pomocš czterech wywołań AppendMenu: AppendMenu (hMenu, MF_SEPARATOR, 0, NULL) ; AppendMenu (hMenu, MFţSTRING, IDM SYS ABOUT, TEXT ("About...")) : AppendMenu (hMenu, MF_STRING, IDMţSYS HELP, TEXT ("Help...")) : AppendMenu (hMenu, MFţSTRING, IDMţSYSţREMOVE, TEXT ("Remove Additions...")) : Pierwsze wywołanie dodaje pasek separatora. Wybranie elementu Remove Ad- ditions powoduje, że POORMENU usuwa dodatkowe elementy przez wywoła- nie GetSystemMenu z drugim parametrem ustawionym na TRUE: GetSystemMenu (hwnd, TRUE) ; Standardowe menu systemowe zawiera opcje: Przywróć, Przenieœ, Rozmiar, Mi- nimalizuj, Maksymalizuj i Zamknij. Generujš one komunikaty WM SYSCOM- MAND z parametrem wParam równym: SC IZESTORE, SC MOVE, SC SIZE, SC MINIMUM, SC MAXIMUM i SC CLOSE. Mimo że programy Windows zwykle tego nie robiš, możesz sam przetworzyć te komunikaty, zamiast przesy- łać je do Def INindowsProc. Możesz także wyłšczyć lub usunšć niektóre standar- dowe opcje menu, używajšc metod opisanych poniżej. Dokumentacja Windows zawiera także standardowe sposoby modyfikacji menu systemowego. Używajš one identyfikatorów SC NEXTWINDOW, SC_PREVWINDOW, SC VSCROLL, SC HSCROLL i SC ARRANGE. Polecenia te mogš okazać się użyteczne w nie- których aplikacjach. 422 Częœć I: Podstawy Zmienianie menu Widzieliœmy już, jak funkcja AppendMenu używana jest do definiowania menu wewnštrz programu i dołšczania elementów do menu systemowego. Przed po- jawieniem się Windows 3.0 zadanie to trzeba było wykonywać za pomocš funk- cji ChangeMenu. Miała ona tak wiele zastosowań, że była jednš z najbardziej zło- żonych w tym systemie (przynajmniej wtedy). Teraz istniejš funkcje jeszcze bar- dziej skomplikowane, a ChangeMenu została zastšpiona pięcioma nowszymi: ů AppendMenu - dodaje nowy element na końcu menu ů DeleteMenu - usuwa istniejšcy element z menu i niszczy go ů InsertMenu - wstawia nowy element do menu ů ModifiţMenu - zmienia istniejšcy element menu ů RemoveMenu - usuwa istniejšcy element z menu. Różruca między DeleteMenu i RemoveMenu staje się ważna, jeœli element jest menu rozwijanym. DeleteMenu niszczy menu rozwijane - a RemoveMenu nie. Inne polecenia menu W tej częœci poznasz kilka dodatkowych funkcji użytecznych przy pracy z menu. Gdy zmieniasz element menu najwyższego poziomu, modyfikacja nie zostanie pokazana aż do momentu ponownego wyœwietlenia paska menu przez Windows. Odœwieżenie tego paska można wymusić wywołaniem: DrawMenuBar (hwnd) ; Zwróć uwagę na to, że argumentem DrawMenuBar jest uchwyt okna, a nie uchwyt menu. Uchwyt menu rozwijanego można uzyskać przy użyciu: hMenuPopup = GetSubMenu (hMenu. iPosition) ; gdzie iPosition jest indeksem (zaczynajšcym się od 0) menu rozwijanego wewnštrz menu najwyższego poziomu, wskazywanego przez hMenu. Następnie można go zastosować z innymi funkcjami (takimi jak AppendMenu). Aktualnš liczbę elementów menu głównego lub rozwijanego można uzyskać wykorzystujšc: iCount = GetMenuItemCount (hMenu) ; Identyfikator elementu menu rozwijanego można uzyskać przy użyciu: id = GetMenuID (hMenuPopup, iPosition) ; gdzie iPosition jest pozycjš (zaczynajšcš się od 0) elementu wewnštrz menu roz- wijanego. W MENUDEMO widziałeœ, jak można zaznaczyć i usunšć zaznaczenie elementu menu rozwijanego stosujšc: CheckMenuItem (hMenu, id, iCheck) ; W MENUDEMO hMenu był uchwytem do menu najwyższego poziomu, id - iden- tyfikatorem menu, a wartoœć iCheck wynosiła MF CHECKED lub MF UNCHEC- KED. Jeœli hMenu jest uchwytem menu rozwijanego, parametr id może być in- Rozdziai 10: Menu i inne zasoby 423 deksem pozycji, a nie identyfikatorem menu. Jeżeli indeks jest bardziej potrzeb- ny, należy umieœcić MF BYPOSTTION jako trzeci argument: CheckMenuItem (hMenu. iPosition, MF CHECKED ţ MF BYPOSITION) : Funkcja EnableMenultem działa podobnie do CheckMenultem, z wyjštkiem tego że trzecim argumentem może być MF ENABLED, MF DISABLED lub MF GRAY ED. Jeœli użyjesz EnableMenuItem na elemencie menu najwyższego poziomu, otwie- rajšcym menu rozwijane, musisz zastosować także MF BYPOSIOTION jako trzeci argument, ponieważ taki element nie ma identyfikatora. Przykład użycia Enable- MenuItem zobaczymy w programie POPPAD2, pokazanym w dalszej częœci tego rozdziału. HiliteMenultem jest podobna do CheckMenultem i EnableMenultem, ale wykorzystuje MF HILITE i MF UNHILITE. Podœwietlenie to inwersja, której Windows używa, gdy poruszasz się po elementach. Zwykle nie ma potrzeby sto- sowania HiliteMenultem. Co jeszcze można zrobić z menu? Zapomniałeœ, jakiego cišgu tekstowego użyłeœ w menu? Możesz odœwieżyć pamięć wywołaniem: iCharCount = GetMenuString (hMenu, id, pString, iMaxCount, iFlag) : iFlag przyjmuje wartoœć albo MF BYCOMMAND (gdzie id jest identyfikatorem menu), albo MF BYPOSITION (gdzie id jest indeksem pozycji). Funkcja kopiuje znaki do pString aż do indeksu iMaxCount i zwraca liczbę skopiowanych zna- ków. Jeœli chciałbyœ sprawdzić aktualne parametry elementu menu, możesz wpisać: iFlags = GetMenuState (hMenu, id. Flag) : Ponownie iFlag jest równe albo MF BYCOMMAND, albo MF BYPOSITION. Pa- rametr iFlags jest kombinacjš wszystkich ustawionych aktualnie flag. Można to sprawdzić, wykonujšc testy z identyfikatorami MF DISABLED, MF GRAYED, MF CHECKED, MF MENUBREAK, MF MENUBARBREAK i MF SEPARATOR. W tym momencie możesz mieć już dosyć menu. Ucieszy cię więc wiadomoœć, że jeżeli nie potrzebujesz już w programie menu, masz możliwoœć zniszczenia go: DestroyMenu (hMenu) : Funkcja ta unieważnia uchwyt menu. Niestandardowe podejœcie do menu Zejdziemy teraz nieco w bok z przetartej œcieżki. Czy zamiast tworzyć w progra- mie menu rozwijane, można utworzyć kilka menu najwyższego poziomu i prze- łšczać się między nimi za pomocš wywołania SetMenu? Takie menu przypomina dawnego Lotusa 1-2-3, pracujšcego w trybie tekstowym. Program NOPOPUPS, pokazany na rysunku 10-10, demonstruje, jak to zrobić. Zawiera on elementy File i Edit podobne do używanych przez MENUDEMO, ale wyœwietla je jako przełš- czane menu najwyższego poziomu. I: Podstawy ţ Rozd 424 NOPOPUPS.C /* NOPOPUPS.C - Demonstruje zagnieżdżone menu nierozwijane (c) Charles Petzold, 1998 */ ţţinclude ţţinclude "resource.h" LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) : int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) static TCHAR szAppNameC7 = TEXT ("NoPopUps") : HWND hwnd : MSG msg : WNDCLASS wndclass : wndclass.style = CS HREDRAW ţ CS VREDRAW : wndclass.lpfnWndProc = WndProc : wndclass.cbClsExtra = 0 : wndclass.cbWndExtra = 0 : wndclass.hInstance = hInstance : wndclass.hIcon = LoadIcon (NULL, IDI ŽPPLICATION) : wndclass.hCursor = LoadCursor (NULL, IDC ŽRROW) : wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE BRUSH) : wndclass.lpszMenuName = NULL : wndclass.lpszClassName = szAppName : if (!RegisterClass (&wndclass)) ( MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MBţICONERROR) : return 0 : ) hwnd = CreateWindow (szAppName, TEXT ("No-Popup Nested Menu Demonstration"), WS OVERLAPPEDWINDOW. CW USEDEFAULT, CW USEDEFAULT, CW USEDEFAULT, CW USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) : UpdateWindow (hwnd) : while (GetMessage (&msg, NULL, 0, 0)) ( TranslateMessage (&msg) : DispatchMessage (&msg) : ) return msg.wParam ; ) LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ( static HMENU hMenuMain, hMenuEdit, hMenuFile : Rozdział 10: Menu i inne zasoby 425 HINSTANCE hInstance ; switch (message) ( case WM_CREATE: hInstance = (HINSTANCE) GetWindowLong (hwnd, GWL HINSTANCE) ; hMenuMain = LoadMenu (hInstance, TEXT ("MenuMain")) ; I hMenuFile = LoadMenu (hInstance, TEXT ("MenuFile")) ; hMenuEdit = LoadMenu (hInstance, TEXT ("MenuEdit")) : Iţ SetMenu (hwnd, hMenuMain) ; return 0 : ţ,i. case WM COMMAND: ; switch (LOWORD (wParam)) ,i case IDM_MAIN: SetMenu (hwnd, hMenuMain) ; return 0 ; '' I case IDM_FILE: I SetMenu (hwnd, hMenuFile) ; return 0 : case IDM_EDIT: f SetMenu (hwnd, hMenuEdit) ; return 0 ; V case IDM_FILE_NEW: case IDM_FILE_OPEN: i case IDM_FILE_SAVE: case IDM_FILE_SAUE_AS: case IDM_EDIT_UNDO: case IDM_EDIT_CUT: case IDM_EDIT_COPY: C case IDM_EDIT_PASTE: case IDMţEDIT_CLEAR: MessageBeep (0) ; return 0 ; ) break : case WM DESTROY: SetMenu (hwnd, hMenuMain) ; DestroyMenu (hMenuFile) : DestroyMenu (hMenuEdit) ; PostOuitMessage (0) ; return 0 ; return DefWindowProc (hwnd, message, wParam, lParam) ; 1 NOPOPUPS.RC (f ragmenty) //Microsoft Developer Studio generated resource script. 426 Czţœć 1: Podstawy (cišg dalszy ze strony 425) llinclude "resource.h" 4linclude "afxres.h" /////////////////////////////////////////////////////////////////////////// // Menu MENUMAIN MENU DISCARDABLE BEGIN MENUITEM "MAIN:", 0, INACTIVE MENUITEM "&File... IDM_FILE MENUITEM "bEdit... , IDM_EDIT END MENUFILE MENU DISCARDABLE BEGIN MENUITEM "FILE:", 0, INACTIVE MENUITEM "&New", IDM_FILE_NEW MENUITEM "&Open... , IDM_FILE_OPEN MENUITEM "&Save", IDM_FILE_SAVE MENUITEM "Save &As", IDM_FILE_SAVE_AS MENUITEM "(&Main)", IDM_MAIN END MENUEDIT MENU DISCARDABLE BEGIN MENUITEM "EDIT:" 0, INACTIVE MENUITEM "&Undo", IDM_EDIT_UNDO MENUITEM "Cu&t", IDM_EDIT_CUT MENUITEM "&Copy", IDM_EDIT_COPY MENUITEM "&Paste", IDM_EDIT_PASTE MENUITEM "De&lete", IDM_EDIT_CLEAR MENUITEM "(&Main)", IDM_MAIN END RESOURCE.H (fragmenty) // Microsoft Developer Studio generated include file. // Used by NoPopups.rc ţţdefine IDM_FILE 40001 4ţdefine IDM_EDIT 40002 ţţdefine IDM_FILE_NEW 40003 ţţdefine IDM_FILE_OPEN 40004 ţţdefine IDM_FILE_SAVE 40005 ţţdefine IDM_FILE_SAVE AS 40006 4ldefine IDM_MAIN 40007 ltdefine IDM_EDIT_UNDO 40008 4ţdefine IDM_EDIT_CUT 40009 4tdefine IDM_EDIT_COPY 40010 4ţdefine IDM_EDIT PASTE 40011 ţţdefine IDMţEDIT CLEAR 40012 Rysunek 10ů10. Program NOPOPUPS W Microsoft Developer Studio tworzysz trzy menu zamiast jednego. Polecenie Resource z menu Insert musisz wybrać trzy razy. Każde menu ma innš nazwę Rozdziai 10: Menu i inne zasoby 427 tekstowš. Gdy procedura okna przetwarza komunikat WMţCREATE, Windows wczytuje wszystkie trzy zasoby menu do pamięci: hMenuMain = LoadMenu (hInstance, TEXT ("MenuMain")) ; hMenuMain = LoadMenu (hInstance, TEXT ("MenuFile")) : hMenuMain = LoadMenu (hInstance, TEXT ("MenuEdit")) ; Poczštkowo program wyœwietla menu główne: SetMenu (hwnd, hMenuMain) ; Menu główne wyœwietla trzy opcje używajšc cišgów znaków "MAIN:", "File..." i "Edit...". Ponieważ element "MAIN:" jest niedostępny, nie sš wysyłane dla nie- go do procedury okna komunikaty WM-COMMAND. Menu File i Edit zaczyna- jš się od elementów "FILE:" i "EDIT:", wskazujšcych, że sš to podmenu. Ostat- nim elementem w tych menu jest cišg znaków "(Main)" - opcja ta shxży do po- wrotu do menu głównego. Przełšczanie między trzema menu jest zrealizowane w prosty sposób: case WM COMMAND : switch (wParam) t case IDM MAIN : SetMenu (hwnd, hMenuMain) ; return 0 ; case IDMţFILE : SetMenu (hwnd, hMenuFile) return 0 : case IDMţEDIT : SetMenu (hwnd, hMenuEdit) ; return 0 ; [pozostaie wiersze programuJ ) break : Po otrzymaniu komunikatu WM-DESTROY NOPOPUPS ustawia menu progra- mu na menu Main i ruszczy wywołaniami DestroyMenu menu File i Edit. Menu Main jest usuwane automatycznie, gdy niszczone jest okno. Klawisze skrótu Klawisze skrótu to kombinacje klawiszy generujšce komunikaty WM COMMAND (lub w niektórych przypadkach WM-SYSCOMMAND). Najczęœciej programy uży- wajš klawiszy skrótu do duplikowania typowych poleceń menu, ale mogš wyko- nywać także funkcje niedostępne w nim. Niektóre programy Windows majš na przykład menu Edycja zawierajšce opcje Usuń lub Wyczyœć. Opcjom tym przypi- sywany jest standardowo klawisz skrótu [Delete]. Użytkownik może wybrać opcję Wyczyœć z menu, naciskajšc kombinację jej znaku z klawiszem [Alt] lub po prostu naciskajšc klawisz [Delete]. Gdy procedura okna odbierze komunikat WM COM- MAND, nie musi sprawdzać, czy użyto klawisza skrótu, czy menu. 428 Częœć I: Podstawy Dlaczego należy używać klawiszy skrótu Można zadać pytanie: dlaczego powinno używać się klawiszy skrótu? Dlaczego nie można po prostu przechwycić komunikatów WMţKEYDOWN lub WMţCHAR i samemu zduplikować funkcje menu? Jakie ma to zalety? W aplikacji zawierajšcej jedno okno można oczywiœcie przechwycić komunikaty klawiatury, ale użycie kla- wiszy skrótu ma jednš zaletę - nie musisz duplikować logiki działania menu i skrótu. W aplikacjach z kilkoma oknami i z kilkoma procedurami okien klawisze skrótu stajš się bardzo ważne. Jak wiesz, Windows wysyła komunikaty klawiatury do procedury okna majšcego aktualnie fokus. Jednak w przypadku klawiszy skrótu Windows wysyła komunikat WMţCOMMAND do procedury okna, której uchwyt jest okreœlony w funkcji TranslateAccelerator. W ogólnym przypadku będzie to głów- ne okno programu (to samo, które ma menu), co oznacza, że logika działania kla- wiszy skrótu nie musi być duplikowana w każdej procedurze okna. Ta zaleta staje się szczególnie ważna, jeœli używasz niemodalnych okien dialogo- wych (omawianych w następnym rozdziale) lub okien potomnych w obszarze roboczym okna głównego. Jeżeli okreœlony klawisz skrótu jest zdefiniowany do przemieszczania się między oknami, tylko jedna procedura okna musi zawierać odpowiedniš logikę działania. Okna potomne nie odbierajš komunikatów 4VMţCOMMAND od klawiszy skrótu. Niektóre zasady przypisywania skrótów Teoretycznie możesz zdefiniować klawisz skrótu dla dowolnej kombinacji klawi- sza znaku lub wirtualnego z klawiszem [Shift], [Ctrl] lub [Alt]. Musisz jednak starać się uzyskać spójnoœć z innymi ap]ikacjami i uniknšć nakładania się skró- tów z innymi stosowanymi w Windows. Powinieneœ unikać stosowania k]awi- szy [Tab], [Enter], [Esc] i [Spacja] jako klawiszy skrótu, ponieważ sš one często używane do funkcji systemowych. Klawisze skrótu stosuje się najczęœciej do elementów menu Edycja programu. Za- lecane skróty dla e]ementów tego menu zmieniły się w wersji Windows 3.1 w sto- sunku do Windows 3.0, więc powszechnie przyjęło się dostarczać zarówno stare jak i nowe skróty, pokazane w następujšcej tabeli: Funkcja Stary skrót Nowy skrót Cofnij [Alt+Backspace] [Ctrl+Z] Wytnij [Shift+Delete] [Ctrl+X] Kopiuj [Ctrl+Ins] [Ctrl+C] Wklej [Shift+Ins] [Ctrl+V] Usuń lub Wyczyœć [Delete] [Delete] Innym powszechnie stosowanym skrótem jest klawisz funkcyjny [F1], który wy- wołuje pomoc. Unikaj używania klawiszy [F4], [F5] i [F6], ponieważ używane sš często w programach z interfejsem wielodokumentowym (Multiple Document In- terface, MDI), który omówiono w rozdziale 19. Rozdział 10: Menu i inne zasoby 429 Tabela skrótów Tabelę skrótów możesz zdefiniować w Developer Studio. Aby ułatwić wczyty- wanie tabeli skrótów do programu, nadaj jej takš samš nazwę tekstowš jak pro- gramowi (oraz menu i ikonie). Każdy skrót ma identyfikator i kombinację klawiszy, które możesz zdefiniować w oknie dialogowym Accel Properties. jeœli wczeœniej zdefiniowałeœ menu, jego identyfikatory będš dostępne na liœcie rozwijanej, więc nie trzeba ich ponownie wpisywać. Skróty mogš być albo kodami klawiszy wirtualnych, albo znakami ASCII w kom- binacji z klawiszami [Shift], [Ctrl] i [Alt]. Jeżeli klawisz znaku ASCII ma być na- ciskany z [Ctrl], możesz to okreœlić wpisujšc ^ przed literš. Kody klawiszy wir- tualnych można pobrać z listy rozwijanej. Jeœli definiujesz klawisz skrótu dla elementu menu, powinieneœ dołšczyć kombi- nację klawiszy do tekstu tego elementu. Znak tabulacji (\t) oddziela tekst od skró- tu, wyrównujšc skróty w drugiej kolumnie. Zapisujšc skróty w menu, używaj nazw Ctrl, Shift lub Alt, po nich znaku plus i klawisza, na przykład Shift+F6 lub Ctrl+F6. Wczytywanie tabeli skrótów Aby wczytać tabelę skrótów i uzyskać do niej uchwyt wewnštrz programu, moż- na wykorzystać funkcję LoadAccelerators. Jest ona podobna do instrukcji Loadlcon, LoadCursor i LoadMenu. Najpierw zdefiniuj uchwyt tabeli skrótów typu HANDLE: HANDLE hAccel : Następnie wczytaj tabelę skrótów: hAccel = LoadAccelerators (hInstance, TEXT ("MyAccelerators")) ; Tak jak w wypadku ikon, kursorów i menu, możesz podać liczbę jako nazwę ta- beli skrótów, a następnie użyć jej w instrukcji LoadAccelerators z makrem MAKE- INTRESOURCE lub zamkniętš w cudzysłowach i poprzedzonš znakiem #. Tłumaczenie naciœnięć klawiszy Zajmiemy się teraz trzema wierszami kodu wspólnymi dla wszystkich progra- mów Windows, które utworzyliœmy do tej pory w tej ksišżce. Kod ten jest stan- dardowš pętlš komunikatu: while (GetMessage (&ms9, NULL, 0, 0)) . TranslateMessage (&mœg) ; DispatchMessage (&msg) ; Zmieniamy go następujšco w celu użycia z tabelš skrótów: while (GetMessage (&msg, NULL, 0, 0)) f if (!TranslateAccelerator (hwnd, hAccel, &msg)) ( 430 Częœć I: Podstawy TranslateMessage (&msg) DispatchMessa9e (&msg) Funkcja TranslateAccelerator okreœla, czy komunikat przechowywany w struktu- rze msg jest komunikatem klawiatury. Jeżeli tak, funkcja wyszukuje jego odpo- wiednik w tabeli skrótów, której uchwytem jest hAccel. Jeżeli go znajdzie, wywo- łuje procedurę okna, którego uchwytem jest hwnd. Jeœli identyfikator klawisza skrótu odpowiada elementowi menu systemowego, komunikatem jest WM SY- SCOMMAND. W przeciwnym przypadku jest to WM COMMAND. Funkcja TransIateAccelerator zwraca wartoœć niezerowš, jeżeli komunikat został przethxmaczony (i przesłany do procedury okna), lub 0 w przypadku przeciw- nym. Jeœli zwrócona zostanie wartoœć niezerowa, nie powinno się wywoływać TranslateMessage i DispatchMessage, tylko powrócić do wywołania GetMessage. Parametr hwnd w funkcji TransIateMessage wyglšda trochę tak, jakby był nie na miejscu, ponieważ nie wymaga go żadna z pozostałych funkcji w pętli komuni- katu. Dodatkowo struktura komunikatu (zmienna strukturalna msg) zawiera ele- ment nazwany hwnd, który także jest uchwytem okna. Funkcja ta różni się od innych, ponieważ pola struktury msg wypełniane sš wy- wołaniem GetMessage. Jeœli drugim parametrem GetMessage jest NULL, funkcja pobiera komunikaty od wszystkich okien aplikacji. Gdy GetMessage zwraca war- toœć, element hwnd struktury msg jest uchwytem okna, które odbierze komuni- kat. Jeżeli jednak funkcja TranslateAccelerator przekonwertuje komunikat klawia- tury na komunikat WMţCOMMAND lub WM SYSCOMMAND, zastšpi uchwyt okna msg.hwnd uchwytem podanym jako pierwszy parametr funkcji. W ten spo- sób Windows wysyła wszystkie klawisze skrótu do tej samej procedury okna, nawet jeœli inne okno aplikacji ma aktualnie fokus. TranslateAccelerator nie kon- wertuje komunikatów klawiatury, gdy fokus ma modalne okno dialogowe lub okno komunikatu, ponieważ komunikaty tych okien nie przechodzš przez pętle komunikatów programu. W niektórych przypadkach, kiedy fokus ma inne okno programu (na przykład niemodalne okno dialogowe), możesz nie chcieć, aby komunikaty klawiszy skró- tu były konwertowane. W następnym rozdziale dowiesz się, co zrobić w takiej sytuacji. Odbieranie komunikatów klawiatury Gdy skrót klawiaturowy odpowiada elementowi menu systemowego, funkcja TranslateAccelerator wysyła do procedury okna komunikat WMţSYSCOMMAND. W przeciwnym wypadku funkcja wysyła procedurze okna komunikat WM COM- MAND. W poniższej tabeli pokazane sš rodzaje komunikatów WMţCOMMAND, jakie można otrzymać dla klawiszy skrótu, poleceń menu i kontrolek okien po- tomnych: Rozdział 10: Menu i inne zasobv 431 Skrót Menu Kontrolka LOWORD (wParam) Identyfikator skrótu Identyfikator menu Identyfikator kontrolki HIWORD (wParam) 1 0 Kod powiadomienia lParam 0 0 Uchwyt okna potomnego Jeœli klawisz skrótu odpowiada elementowi menu, procedura okna otrzymuje także komunikaty WM INITMENU, WM INITMENUPOPUP i WMţITEMSE- LECT, czyli takie, jakby została wybrana opcja menu. Programy zwykle udostę - p I niajš i wyłšczajš opcje menu podczas przetwarzania komunikatu WM INITME- NUPOPUP, więc używajšc klawiszy skrótu, nie tracisz tych ustawień. Jeżeli kla- wisz skrótu odpowiada niedostępnemu lub szaremu elementowi menu, funkcja TranslateAccelerator nie wysyła procedurze okna komunikatów WMţCOMMAND i WM SYSCOMMAND. Jeœli aktywne okno jest zminimalizowane, TranslateAccelerator wysyła procedu- ; rze okna komunikaty WMţSYSCOMMAND dla skrótów odpowiadajšcych do- stępnym elementom menu systemowego, ale nie wysyła komunikatów ; i WM COMMAND. TranslateAccelerator kieruje także do procedury okna komuni- katy WM COMMAND dla klawiszy skrótu, które nie odpowiadajš żadnym ele- II mentom menu. Program POPPAD z menu i klawiszami skrótu W rozdziale 9 utworzyliœmy program POPPADl, który używał kontrolki edycji okna potomnego do utworzenia prostego notatnika. W tym rozdziale dodamy do niego menu File oraz Edit i nazwiemy go POPPAD2. Wszystkie elementy menu Edit będš funkcjonalne - elementy menu File ukończymy w rozdziale 11, a funk- cję Print w rozdziale 13. POPPAD2 pokazany jest na rysunku 10-11. POPPAD2.C /* POPPAD2.C - Prosty notatnik, wersja 2 (zawiera menu) (c) Charles Petzold, 1998 */ ţţinclude lţinclude "resource.h" ţţdefine IDţEDIT 1 LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM); TCHAR szAppNameC] = TEXT ("PopPad2") ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, ' PSTR szCmdLine, int iCmdShow) HACCEL hAccel ; HWND hwnd ; 432 Częœć I: Podstawy (cišg dalszy ze strony 431) MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CS VREDRAW ; wndclass.lpfnWndProc = WndProc : wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (hInstance, szAppName) ; wndclass.hCursor = LoadCursor (NULL, IDC ŽRROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITEţBRUSH) ; wndclass.lpszMenuName = szAppName ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) ( MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MBţICONERROR) ; return 0 ; ) hwnd = CreateWindow (szAppName, szAppName, WS_OVERLAPPEDWINDOW, GetSystemMetrics (SM_CXSCREEN) / 4, GetSystemMetrics (SM_CYSCREEN) / 4, GetSystemMetrics (SM CXSCREEN) / 2, GetSystemMetrics (SM CYSCREEN) 1 2, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; hAccel = LoadAccelerators (hInstance, szAppName) ; while (GetMessage (&msg, NULL, 0, 0)) ( if (!TranslateAccelerator (hwnd, hAccel, &msg)) ( TranslateMessage (&msg) ; DispatchMessage (&msg) ; ) ) return msg.wParam ; ) AskConfirmation (HWND hwnd) ( return MessageBox (hwnd, TEXT ("Really want to close PopPad2?"), szAppName, MB YESNO ţ MBţICONOUESTION) ; ) LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ( static HWND hwndEdit ; int iSelect, iEnable ; switch (message) Rozdział 10: Menu i inne zasoby 433 case WM_CREATE: hwndEdit = CreateWindow (TEXT ("edit"), NULL, WS_CHILD ţ WS_VISIBLE ţ WS_HSCROLL ţ WS VSCROLL ţ WS_BORDER ţ ES_LEFT ţ ES_MULTILINE ţ f:. ES_AUTOHSCROLL ţ ES_AUTOVSCROLL, 0, 0, 0, 0, hwnd, (HMENU) ID_EDIT, ((LPCREATESTRUCT) lParam)->hInstance, NULL) ; return 0 ; case WM_SETFOCUS: SetFocus (hwndEdit) ; return 0 ; case WM SIZE: MoveWindow (hwndEdit, 0, 0, LOWORD (lParam), HIWORD (lParam), TRUE) ; return 0 ; case WM_INITMENUPOPUP: if (lParam = 1) ( EnableMenuItem ((HMENU) wParam, IDM_EDIT_UNDO, SendMessage (hwndEdit, EMţCANUNDO, 0, 0) ? MF ENABLED : MF GRAYED) ; EnableMenuItem ((HMENU) wParam, IDM_EDIT_PASTE, IsClipboardFormatAvailable (CF TEXT) ? MFţENABLED : MF GRAYED) ; iSelect = SendMessage (hwndEdit, EM GETSEL, 0, 0) ; if (HIWORD (iSelect) == LOWORD (iSelect)) iEnable = MF GRAYED ; else iEnable = MFţENABLED ; ' EnableMenuItem ((HMENU) wParam, IDM_EDIT_CUT, iEnable) ; EnableMenuItem ((HMENU) wParam, IDM_EDIT_COPY, iEnable) ; EnableMenuItem ((HMENU) wParam, IDM EDITţCLEAR, iEnable) : return 0 ; 1 break ; case WM_COMMAND: if (lParam) ( if (LOWORD (lParam) = ID_EDIT && (HIWORD (wParam) == EN_ERRSPACE ţţ HIWORD (wParam) = EN MAXTEXT)) MessageBox (hwnd, TEXT ("Edit control out of space."), szAppName, MB OK ţ MBţICONSTOP) : return 0 ; ) else switch (LOWORD (wParam)) f case IDM_FILE_NEW: case IDM_FILE_OPEN: case IDMţFILEţSAVE: 434 Częœć I: Podstawy (cišg dalszy ze strony 433) case IDM_FILE_SAVE_AS: case IDM_FILE_PRINT: MessageBeep (0) ; return 0 ; case IDM_APP_EXIT: SendMessa9e (hwnd, WMţCLOSE, 0, 0) ; return 0 ; case IDM_EDIT UNDO: SendMessage (hwndEdit, WM UNDO, 0, 0) ; return 0 ; case IDM_EDIT_CUT: SendMessage (hwndEdit, WM CUT, 0, 0) ; return 0 ; case IDM_EDIT COPY: SendMessage (hwndEdit, WM COPY, 0, 0) ; return 0 ; case IDM_EDIT_PASTE: SendMessage (hwndEdit, WM PASTE, 0, 0) ; return 0 ; case IDM_EDIT_CLEAR: SendMessage (hwndEdit, WM CLEAR, 0, 0) ; return 0 ; case IDM_EDIT SELECT_ALL: SendMessage (hwndEdit, EM SETSEL, 0, -1) ; return 0 ; case IDM HELP_HELP: MessageBox (hwnd, TEXT ("Help not yet implemented!"), szAppName, MBţOK ţ MBţICONEXCLAMATION) ; return 0 ; case IDM APP_ABOUT: MessageBox (hwnd, TEXT ("POPPAD2 (c) Charles Petzold, 1998"), szAppName, MB OK ţ MBţICONINFORMATION) ; return 0 ; break ; case WM_CLOSE: if (IDYES == AskConfirmation (hwnd)) DestroyWindow (hwnd) ; return 0 ; case WM_OUERYENDSESSION: if (IDYES - AskConfirmation (hwnd)) return 1 ; else return 0 ; Rozdział 10: Menu i inne zasoby 435 case WM_DESTROY: PostOuitMessage (0> ; return 0 : ) return DefWindowProc (hwnd, message, wParam, lParam) ; 1 POPPAD2.RC (fragmenty) //Microsoft Dev-eloper Studio generated resource script. i.; I.., ţţinclude "resource.h" i, ţţinclude "afxres.h" ;;i ,, //////////////////////////////////////////////l//////////////////////////// // Icon POPPAD2 ICON DISCARDABLE "poppad2.ico" /////////////////////////////////////////////////////////////////////////// // Menu POPPAD2 MENU DISCARDABLE BEGIN POPUP "&File" BEGIN , MENUITEM "&New", IDM_FILE_NEW MENUITEM "&Open... , IDM_FILE_OPEN MENUITEM "&Save", IDM_FILE_SAVE MENUITEM "Save &As... , IDMţFILEţSAVE AS MENUITEM SEPARATOR MENUITEM "&Print", IDMţFILEţPRINT MENUITEM SEPARATOR MENUITEM "E&xit", IDM APPţEXIT END POPUP "&Edit" BEGIN MENUITEM "&Undo\tCtrl+Z", IDM EDIT UNDO MENUITEM SEPARATOR MENUITEM "Cu&t\tCtrl+X", IDM_EDIT_CUT MENUITEM "&Copy\tCtrl+C", IDM_EDIT_COPY MENUITEM "&Paste\tCtrl+V", IDM_EDIT PASTE MENUITEM "De&lete\tDel", IDMţEDIT CLEAR ' MENUITEM SEPARATOR MENUITEM "&Select All", IDMţEDITţSELECT ALL END POPUP "&Help" BEGIN MENUITEM "&Help... IDM HELP_HELP MENUITEM "&About PopPad2... , IDM ŽPP ABOUT END END /////////////////////////////////////////////////////////////////////////// // Accelerator 436 Częœć I: Podstawy (cišg dalszy ze strony 435) POPPAD2 ACCELERATORS DISCARDABLE BEGIN VKţBACK, IDM_EDIT_UNDO, VIRTKEY, ALT, NOINVERT VK_DELETE, IDM_EDIT_CLEAR, VIRTKEY, NOINVERT UK_DELETE, IDM_EDIT_CUT, VIRTKEY, SHIFT, NOINVERT VK_Fl, IDM_HELP_HELP, VIRTKEY, NOINVERT VK_INSERT, IDM_EDITţCOPY, VIRTKEY, CONTROL, NOINVERT VKţINSERT, IDM_EDIT_PASTE, VIRTKEY, SHIFT, NOINVERT "^C", IDM EDIT_COPY, ASCII, NOINVERT "^V", IDM_EDIT_PASTE, ASCII, NOINVERT "^X", IDM EDIT CUT, ASCII, NOINVERT "^Z", IDMţEDIT UNDO, ASCII, NOINVERT END RESOURCE.H (fragmenty) // Microsoft Developer Studio generated include file. // Used by PopPad2.rc define IDM_FILE_NEW 40001 ildefine IDM_FILE_OPEN 40002 ildefine IDMţFILE_SAVE 40003 ildefine IDMţFILE_SAVE ŽS 40004 ildefine IDMţFILE_PRINT 40005 iţdefine IDM_APP_EXIT 40006 ildefine IDM_EDIT_UNDO 40007 ildefine IDM_EDIT_CUT 40008 ildefine IDM_EDIT_COPY 40009 ţţdefine IDM_EDIT_PASTE 40010 ţţdefine IDM_EDIT_CLEAR 40011 ildefine IDM EDITţSELECT ALL 40012 ţţdefine IDM HELP_HELP 40013 ildefine IDM ŽPP ŽBOUT 40014 POPPAD2.ICO :..:: .:::...... Rysunek 10-11. Program POPPAD2 Skrypt zasobów POPPAD2.RC zawiera menu i tabelę skrótów. Zwróć uwagę na to, że wszystkie skróty wprowadzone sš w cišgach tekstowych elementów menu rozwijanego Edit po znaku tabulacji (\t). Rozdział 10: Menu i inne zasoby 437 Udostępnianie elementów menu Głównym zadaniem procedury okna jest udostępnianie i przyszarzanie opcji w menu Edit podczas przetwarzamia komunikatu WMţINITMENUPOPUP Naj- pierw program sprawdza, czy menu rozwijane Edit ma być wyœwietlone. Ponie- waż indeks pozycji (zaczynajšcy się od 0) w menu Edit wynosi 1, iParam też bę- dzie równy 1, jeœli to menu ma być wyœwietlone. Aby okreœlić, czy opcja Undo może zostać udostępniona, POPPAD2 wysyła ko- munikat EM CANUNDO do kontrolki edycji. Wywołanie SendMessage zwraca wartoœć niezerowš, jeżeli kontrolka edycji może wykonać operację cofnięcia, dzięki czemu opcja jest udostępniana. W przeciwnym wypadku opcja jest szara: EnableMenuItem (wParam, IDMţUNDO, SendMessage (hwndEdit, EM CANUNDO, 0, 0) ? MF ENABLED : MF GRAYED) ; Opcja Paste powinna być udostępniana tylko wtedy, gdy Schowek zawiera tekst. Możemy to sprawdzić wywołaniem IsClipboardFormatAvailable z identyfikatorem CF TEXT: EnableMenuItem (wParam, IDMţPASTE, IsClipboardFormatAvailable (CF TEXT) ? MFţENABLED : MF GRAYED) ; Opcje Cut, Copy i Delete powinny być udostępniane tylko wtedy, gdy tekst w kon- trolce edycji został zaznaczony. Wysłanie kontrolce edycji komunikatu EM GET SEL powoduje zwrócenie liczby całkowitej, zawierajšcej tę informację: iSelect = SendMessage (hwndEdit, EMţGETSEL, 0, 0) ; Mniej znaczšce słowo iSelect jest pozycjš pierwszego zaznaczonego znaku. Bar- dziej znaczšce słowo iSelect jest pozycjš znaku występujšcego za zaznaczeniem. jeœli obydwa słowa sš równe, tekst nie zostaje zaznaczony: if (NIWORD (iSelect) == LOWORD (iSelect)) iEnable = MF GRAYED ; else iEnable = MF ENABLED ; Wartoœć iEnable jest wtedy używana do opcji Cut, Copy i Delete: EnableMenuItem (wParam, IDM_CUT, iEnable) ; EnableMenuItem (wParam, IDM_COPY, iEnable) ; EnableMenuItem (wParam, IDMţDEL, iEnable) ; Przetwarzanie opcji menu Gdybyœmy nie użyli kontrolki okna potomnego edycji w POPPAD2, stanęliby- œmy przed problemami zwišzanymi z właœciwš implementacjš opcji Undo, Cut, Copy, Paste, Clear i Select All w menu Edit. Kontrolka edycji ułatwia ten proces, ponieważ rzadko przesyłamy jej komunikat o jednej z poniższych opcji: case IDM UNDO : SendMessa9e (hwndEdit, WM UNDO, 0, 0) ; return 0 ; 438 Częœć I: Podstawy case IDM_CUT : SendMessage (hwndEdit, WM CUT, 0, 0) ; return 0 ; case IDM_COPY : SendMessage (hwndEdit, WM COPY, 0, 0) : return 0 : case IDM_PASTE : SendMessage (hwndEdit, WMţPASTE, 0, 0) ; return 0 ; case IDM_DEL : SendMessage (hwndEdit, WM DEL, 0, 0) ; return 0 ; case IDM_SELALL : SendMessage (hwndEdit, EMţSETSEL, 0, -1) ; return 0 ; Zwróć uwagę na to, że moglibyœmy jeszcze bardziej uproœcić ten kod ustawiajšc wartoœci na przykład IDM UNDO, IDM CUT na równe wartoœciom odpowia- dajšcych im komunikatów okien WM LTNDO, WM CUT i tak dalej. Opcja About w menu rozwijanym File wyœwietla proste okno komunikatu: case IDM_ABOUT : MessageBox (hwnd, TEXT ("POPPAD2 (c) Charles Petzold, 1998"), szAppName, MB OK ţ MBţICONINFORMATION) ; return 0 : W następnym rozdziale zastšpimy je oknem dialogowym. Okno komunikatu jest także wyœwietlane, gdy wybierzesz opcję Help z tego samego menu lub naciœniesz klawisz skrótu [Fl]: case IDMţEXIT : SendMessage (hwnd, WM CLOSE, 0, 0) ; return 0 ; Dokładnie to samo robi procedura DefiNindowProc, gdy odbierze komunikat WM SYSCOMMAND z parametrem wParam równym SC CLOSE. W poprzednich programach nie przetwarzaliœmy WM CLOSE w procedurze okna, ponieważ kierowaliœmy go do procedury DefWindowProc, która po odebra- niu tego komunikatu wywołuje po prostu funkcję DestroyWindow. Zamiast jed- nak przesyłać WM CLOSE do Def 4VindowProc, program POPPAD2 przetwarza ten komunikat. (Teraz nie jest to istotne, ale stanie się bardzo ważne w rozdziale 11, w którym program POPPAD będzie mógł edytować pliki). case WM CLOSE : if (IDYES == AskConfirmation (hwnd)) DestroyWindow (hwnd) ; return 0 ; AskConfirmation jest funkcjš programu POPPAD2, wyœwietlajšcš okno komuni- katu z pro sš o potwierdzenie polecenia zamknięcia programu: Rozdział 10: Menu i inne zasoby 439 AskConfirmation (HWND hwnd) ( return MessageBox (hwnd, TEXT ("Really want to close Poppad2?"), szAppName, MBţYESNO ţ MBţICONOUESTION) ; Okno komunikatu (również funkcja AskConfirmation) zwraca IDYES, jeœli naci- œnięty zostanie przycisk Tak. Tylko w takim wypadku program POPPAD2 wy- wołuje funkcję DestroyWindow. W przeciwnym razie program działa dalej. Jeżeli chcesz uzyskać potwierdzenie przed wyjœciem z programu, musisz także przetworzyć komunikat WM-QUERYENDSESSION. Windows wysyła go każdej procedurze okna, gdy użytkownik zażšda zamknięcia systemu Windows. Jeœli którakolwiek procedura okna zwróci 0 w odpowiedzi na ten komunikat, sesja Windows nie jest zamykana. Komunikat WMţQUERYENDSESSION obsługuje- my w następujšcy sposób: case WM OUERYENDSESSION : if (IDYES == AskConfirmation (hwnd)) return 1 ; else return 0 ; Komunikaty WMţCLOSE i WMţQUERYENDSESSION sš jedynymi, które mu- sisz przetworzyć, jeœli chcesz, aby użytkownik potwierdził wyjœcie z programu. Z tego powodu opcja menu Exit w programie POPPAD2 wysyła procedurze okna komunikat WMţCLOSE - w ten sposób unikamy trzeciego potwierdzania. Jeżeli przetwarzasz komunikaty WMţQUERYENDSESSION, może zainteresować cię także komunikat WMţENDSESSION. Windows wysyła go do każdej proce- dury okna, która uprzednio odebrała komunikat WMţQUERYENDSESSION. Parametr wParam komunikatu WM ENDSESSION jest równy 0, jeœli sesja Win- dows nie jest zamykana, ponieważ inny program zwrócił 0 w odpowiedzi na komunikat WMţQUERYENDSESSION. Komunikat WMţENDSESSION odpowia- da na pytanie: powiedziałem systemowi Windows, że może mnie zamknšć, ale czy zostałem zamknięty? Mimo że w menu File programu POPPAD2 umieœciliœmy standardowe opcje New, Open, Save i Save As, nie majš one teraz żadnych funkcji. Aby przetworzyć te polecenia, potrzebne sš okna dialogowe. Teraz jesteœ gotów, aby je poznać. Rozdział 11 kn ' a a o owe Okna dialogowe najczęœciej sš wykorzystywane do uzyskiwania dodatkowych danych od użytkownika i można nimi łatwo zarzšdzać za pomocš menu. Pro- gramista wskazuje, że element menu otwiera okno dialogowe, umieszczajšc trzy kropki (...) za jego nazwš. Okno dialogowe zawiera zwykle różne kontrolki okna potomnego. Rozmiar i po- łożenie tych kontrolek okreœlone sš w szablonie okna dialogowego w pliku skryptu zasobów. Mimo że programista może sam zdefiniować szablon okna dialogowe- go, obecnie sš one zwykle projektowane interaktywnie w Visual C++ Developer Studio. Developer Studio automatycznie generuje szablon okna dialogowego. Gdy program wywołuje okno dialogowe oparte na szablonie, Microsoft Windows 98 jest odpowiedzialny za utworzenie go i jego kontrolek oraz za dostarczenie procedurze okna komunikatów okna dialogowego, włšczajšc w to wszystkie ko- munikaty klawiatury i myszy. Kod wewnętrzny Windows, który się tym zajmu- je, jest czasem okreœlany jako menedżer okien dialogowych. Wiele komuriikatów przetwarzanych przez procedurę okna dialogowego z sys- temu Windows jest także przekazywane do funkcji w twoim programie, którš okreœla się nazwš procedura okna dialogowego lub procedura dialogowa. Proce- dura okna dialogowego podobna jest do zwykłej procedury okna, ale z pewnymi istotnymi różnicami. Zwykle będziesz używać jej jedynie do inicjowania kontro- lek podczas tworzenia okna dialogowego, przetwarzania komunikatów od kon- trolek okna potomnego i zamykania okna dialogowego. Procedury okien dialo- gowych najczęœciej nie przetwarzajš bezpoœrednio komunikatów 4VMţPAINT, klawiatury i myszy. Rozdział dotyczšcy okien dialogowych powinien być bardzo obszerny, ponieważ dotyczy kontrolek okien potomnych. Jednak kontrolki te omówiliœmy już w roz- dziale 9. Gdy używasz kontrolek okien potomnych w oknach dialogowych, me- nedżer okien dialogowych Windows przejmuje wiele obowišzków, o których mó- wiliœmy w rozdziale 9. W szczególnoœci podczas pracy z oknami dialogowymi zrukajš problemy zwišzane z przekazywaniem fokusu wejœciowego do pasków przewijania, których doœwiadczyliœmy w programie COLORSl. Windows zajmuje się całš logikš potrzebnš do przemieszczania fokusu po kontrolkach okna dialo- gowego. Dołšczenie okna dialogowego do programu jest jednak nieco bardziej skompli- kowane od dołšczania ikony czy menu. Rozpoczniemy od prostego okna dialo- gowego, abyœ mógł odnaleŸć podobieństwa pomiędzy tymi elementami. 442 Częœć I: Podstawy Modalne okna dialogowe Okna dialogowe mogš być modalne lub niemodalne. Modalne okno dialogowe jest stosowane najczęœciej. Gdy program wyœwietla takie okno, użytkownik nie może przełšczyć się z niego do innego okna programu. Musi najpierw zamknšć okno dialogowe klikajšc przycisk OK lub Anuluj (Cancel). Podczas wyœwietlania modalnego okna dialogowego użytkownik może jednak przełšczyć się do inne- go programu. Niektóre okna dialogowe (nazywane systemowymi) nie pozwalajš nawet na to. Okna modalne systemowe muszš zostać zamknięte, aby użytkow- nik mógł zrobić cokolwiek innego w Windows. Tworzenie okna dialogowego About Nawet jeœli program Windows nie wymaga danych wejœciowych od użytkowni- ka, często ma okno dialogowe wywoływane z menu opcjš About. To okno dialo- gowe wyœwietla nazwę i ikonę programu, informację o prawach autorskich, przy- cisk oznaczony OK, a czasami także inne informacje (na przykład numer telefo- nu pomocy technicznej). Pierwszy program, któremu się przyjrzymy, nie robi nic innego poza wyœwietleniem okna dialogowego About. Program ABOUT1 został pokazany na rysunku 11-1. ABOUTl.C /* ABOUTl.C - Demonstracja okien dialogowych nr 1 (c) Charles Petzold, 1998 */ ţtinclude ţţinclude "resource.h" LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; BOOL CALLBACK AboutDlgProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) static TCHAR szAppName[7 = TEXT ("Aboutl") ; MSG msg ; HWND hwnd ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CS VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (hInstance, szAppName) ; wndclass.hCursor = LoadCursor (NULL, IDC ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITEţBRUSH) wndclass.lpszMenuName = szAppName ; Rozdział 11: Okna dialogowe 443 wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; hwnd = CreateWindow (szAppName, TEXT ("About Box Demo Program"), WS_OUERLAPPEDWINDOW, CWţUSEDEFAULT, CW USEDEFAULT, CW_USEDEFAULT, CW USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) ( TranslateMessage (&msg) ; DispatchMessage (&msg) ; 1 return msg.wParam ; ) LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) 5 static HINSTANCE hInstance ; switch (message) case WM_CREATE : hInstance = ((LPCREATESTRUCT) lParam)->hInstance ; return 0 ; case WM COMMAND : switch (LOWORD (wParam)) ( case IDM_APP_ABOUT : DialogBox (hInstance, TEXT ("AboutBox"), hwnd, AboutOlgProc) ; break ; 1 return 0 ; case WMţDESTROY : PostOuitMessage (0) ; return 0 ; w return DefWindowProc (hwnd, message, wParam, lParam) ; ; ) BOOL CALLBACK AboutDlgProc (HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam) ( switch (message) Częœć I: Podstawy (cišg dalszy ze strony 443) ( case WMţINITDIALOG : return TRUE ; case WM COMMAND : switch (LOWORD (wParam)) ( case IDOK : case IDCANCEL : EndDialog (hDlg, 0) ; return TRUE ; break ; return FALSE ; ABOUTl.RC (fragmenty) //Microsoft Developer Studio generated resource script. ltinclude "resource.h" ltinclude "afxres.h" /////////////////////////////////////////////////////////////////////////// // Dialog ABOUTBOX DIALOG DISCARDABLE 32, 32, 180, 102 STYLE DS_MODALFRAME ţ WSţPOPUP FONT 8, "MS Sans Serif" BEGIN DEFPUSHBUTTON "OK",IDOK,66,81,50,14 ICON "ABOUT1",IDC_STATIC,7,7,21,20 CTEXT "Aboutl",IDC_STATIC.40,12,100,8 CTEXT "About Box Demo Program",IDC_STATIC,7,40,166,8 CTEXT "(c> Charles Petzold, 1998",IDCţSTATIC,7,52,166,8 END /////////////////////////////////////////////////////////////////////////// // Menu ABOUT1 MENU DISCARDABLE BEGIN POPUP "&Help" BEGIN MENUITEM "&About Aboutl... , IDM ŽPP ABOUT END END /////////////////////////////////////////////////////////////////////////// // Icon ABOUT1 ICON DISCARDABLE "Aboutl.ico" Rozdział 11: Okna dialogowe 445 iţ RESOURCE.H (fragmenty) // Microsoft Developer Studio generated include file. // Used by Aboutl.rc ţi ţpdefine IDM_APP_ABOUT 40001 i ţfdefine IDCţSTATIC 3 . ABOUT.ICO Rysunek 11-1. Program ABOUTI iv.. Ikonę i menu w tym programie tworzysz w taki sam sposób, jak w ostatnim roz- dziale. Zarówno ikona, jak i menu majš identyfikatory tekstowe "Aboutl . Menu , ma jednš opcję, która generuje komunikat WM COMMAND z identyfikatorem IDM APP ABOUT. Dzięki temu program wyœwietla okno dialogowe pokazane na rysunku 11-2. Rysunek 11-2. Okno dialogowe programu ABOUT1 446 Częœć Iţ Podstawy Okno dialogowe i jego szablon Chcšc dodać okno dialogowe do aplikacji w Visual C++ Developer Studio, nale- ży wybrać opcję Resource z menu Insert, a następnie Dialog Box. Zostanie wtedy wyœwietlone okno dialogowe z paskiem tytułu, etykietš ("Dialog") oraz przyci- skami OK i Cancel. Pasek narzędzi Controls pozwala umieœcić w oknie dialogo- wym różne kontrolki. Developer Studio nadaje oknu dialogowemu standardowy identyfikator IDD DIALOGI. Możesz kliknšć tę nazwę (lub okno dialogowe) prawym przyci- skiem myszy i wybrać Properties z menu. Dla potrzeb tego programu zmień iden- tyfikator na "AboutBox" (w cudzysłowie). Aby było one zgodne z utworzonym przeze mnie, zmień wartoœci pól X Pos i Y Pos na 32. Wskazujš one, gdzie zosta- nie wyœwietlone okno dialogowe, w odniesieniu do lewego górnego rogu obsza- ru roboczego okna programu. (Współrzędne okna dialogowego będziemy wkrótce omawiać dokładniej). Teraz, wcišż będšc w oknie dialogowym Properties, wybierz kartę Styles. Usuń za- znaczenie z pola Title Bar, ponieważ nasze okno dialogowe nie będzie mieć paska tytułu. Następnie kliknij przycisk Zamknij w oknie dialogowym Properties. Nadszedł czas, aby zaprojektować wyglšd okna dialogowego. Nie będzie nam potrzebny przycisk Cancel, więc klikruj go i naciœnij klawisz [Delete]. Kliknij przy- cisk OK i przejdŸ na dół okna Developer Studio. Znajdziesz tam pasek narzędzi z małš bitmapš, która pozwala wyœrodkować w poziomie kontrolkę w oknie. Naciœnij ten przycisk. Chcemy, aby w oknie dialogowym pojawiła się ikona programu, naciœnij więc przycisk Pictures na ruchomym pasku narzędzi Controls. Przesuń mysz nad okno dialogowe, naciœnij lewy przycisk i rozcišgnij prostokšt. W tym miejscu pojawi się ikona. Naciœnij nad tym prostokštem prawy przycisk myszy i wybierz z menu opcję Properties. Pozostaw identyfikator ustawiony na IDC STATIC. Zostanie on zdefiniowany w pliku RESOURCE.H jako -1. Wartoœć ta jest używana dla wszyst- kich identyfikatorów, do których nie odwołuje się program w C. Zmień Type na Icon. Możesz wpisać nazwę ikony programu w polu Image lub, jeœli już jš utwo- rzyłeœ, możesz wybrać nazwę z pola listy rozwijanej. Aby wprowadzić trzy napisy w oknie dialogowym, wybierz Static Text z paska narzędzi Controls i ustal położenie tekstu w oknie dialogowym. Klinij prawym przyciskiem myszy kontrolkę i wybierz z menu opcję Properties. Wpisz w polu Caption okna Properties tekst, który ma się pojawić w oknie dialogowym. Wy- bierz kartę Styles i zaznacz Center w polu Align Text. Po dodaniu napisów może okazać się potrzebne powiększenie okna dialogowe- go. Zaznacz je i przecišgnij obramowanie. Możesz także zaznaczać i zmieniać roz- miar kontrolek. Często łatwiej jest wykorzystać w tym celu klawisze strzałek. Użyte pojedynczo przesuwajš kontrolkę, a z klawiszem [Shift] powalajš zmie- niać rozmiar. Współrzędne i rozmiar zaznaczonej kontrolki sš pokazane w dol- nym lewym rogu okna Developer Studio. Jeżeli dokończysz aplikację i przyjrzysz się póŸniej plikowi ABOUTl.RC, zoba- czysz wygenerowany przez Developer Studio szablon okna dialogowego. Zapro- jektowane przeze mnie okno dialogowe miało następujšcy szablon: Rozdział 11: Okna dialogowe 447 ABOUTBOX DIALOG DISCARDABLE 32, 32, 180, 100 STYLE DS_MODALFRAME ţ WSţPOPUP FONT 8, "MS Sans Serif" BEGIN DEFPUSHBUTTON "OK",IDOK,66,80,50,14 ICON "ABOUT1",IDC_STATIC,7,7,21,20 CTEXT "Aboutl",IDCţSTATIC,40,12,100,8 CTEXT "About Box Demo Program",IDC STATIC,7,40,166,8 CTEXT "(c) Charles Petzold, 1998",IDC STATIC,7,52,166,8 END Pierwszy wiersz nadaje oknu dialogowemu nazwę (w tym przypadku ABOUT BOX). Tak jak z innymi zasobami zamiast niej można użyć numeru. Po nazwie następujš słowa kluczowe DIALOG, DISCARDABLE i cztery liczby. Pierwsze dwie sš współrzędnymi x i y lewego górnego rogu okna dialogowego w odnie- sieniu do obszaru roboczego okna nadrzędnego. Pozostałe dwie liczby to szero- koœć i wysokoœć okna dialogowego. Współrzędne i rozmiar nie sš podawane w pikselach. Opierajš się na specjalnym systemie jednostek, używanym tylko w szablonach okien dialogowych. Liczby te liczš się z rozmiarem czcionki zastosowanej w oknie dialogowym (w tym przy- padku 8-punktowa czcionka MS Sans Serif). Współrzędna x i szerokoœć wyrażo- ne sš w jednostkach równych 1/4 œredniej szerokoœci znaku, natomiast współ- rzędna y i wysokoœć - w jednostkach równych 1 /8 wysokoœci znaku. Z tego po- wodu lewy górny róg tego okna dialogowego znajduje się 5 znaków od lewej kra- wędzi obszaru roboczego okna nadrzędnego i 2-1/2 znaku od górnej jego kra- wędzi. Okno dialogowe jest szerokie na 40 znaków i wysokie na 10. Taki system pozwala używać współrzędnych i rozmiarów, które umożliwiajš zachowanie ogólnego wyglšdu i wymiaru niezależnie od rozdzielczoœci ekranu i wybranej czcionki. Ponieważ znaki majš zwykle wysokoœć około dwóch razy większš od szerokoœci wymiaru, na osiach x i y sš prawie takie same. Instrukcja STYLE w szablonie jest podobna do pola Style lub wywołania Create- Window. WS_POPUP i DS MODALFRAME sš zwykle używane do modalnych okien dialogowych, ale nie tylko, o czym napiszemy póŸniej. Pomiędzy instrukcjami BEGlN i END (lub nawiasami, jeœli preferujesz je, sam pro- jektujšc okno dialogowe) definiuje się kontrolki okna potomnego, które pojawiš się w oknie dialogowym. Nasze okno używa trzech typów kontrolek okna po- tomnego: DEFPUSHBUTTON (domyœlny przycisk polecenia), ICON (ikona) i CTEXT (tekst wyœrodkowany). Format tych instrukcji jest następujšcy: control-type "text" id, xPos, yPos, xWidth, yHeight, iStyle Wartoœć iStyle na końcu instrukcji jest opcjonalna. Okreœla dodatkowe style okna, które używajš identyfţkatorów zdefiniowanych w plikach nagłówkowych Windows. Identyfikatory DEFPUSHBUTTON, ICON i CTEXT używane sš tylko w oknach dialogowych. Skracajš one dostęp do poszczególnych klas i stylów okna. CTEXT wskazuje na przykład, że klasa okna potomnego jest statyczna, a styl równy: WS CHILD ţ SS CENTER ţ WS VISIBLE ţ WSţGROUP Identyfikatora WS GROUP użyliœmy po raz pierwszy, ale style okna WS CHILD, SS_CENTER i WS VISIBLE stosowaliœmy już podczas tworzenia kontrolek okna potomnego w programie COLORSI w rozdziale 9. 448 Częœć i: Podstawy I Dla ikony pole "text" jest nazwš zasobu ikony programu, która także została zdefiniowana w skrypcie zasobów ABOUTI. Pole "text" przycisku polecenia za- wiera tekst, który pojawi się na przycisku. Tekst ten odpowiada drugiemu argu- mentowi podawanemu w wywołaniu CreateWindow, gdy tworzysz kontrolkę okna potomnego w programie. Pole id jest wartoœciš, której okno potomne używa do identyfikacji, gdy wysyła komunikaty (zwykle WM-COMMAND) do okna nadrzędnego. Oknem nadrzęd- nym kontrolki okna potomnego jest okno dialogowe, które przesyła komunikaty dalej do procedury okna w Windows. Procedura okna wysyła te komunikaty także do procedury okna dialogowego, którš dołšczasz do programu. Wartoœci id od- powiadajš identyfikatorom okien potomnych używanych w funkcji CreateWindow (zobacz rozdział 9). Ponieważ kontrollti ikony i tekstu nie wysyłajš komunika- tów do okna nadrzędnego, ich wartoœci id sš ustawione na IDC_STATIC, która jest zdefiniowana w RESOURCE.H jako -1. Wartoœciš identyfikatora przycisku polecenia jest IDOK, zdefiniowana w WINUSER.H jako 1. Kolejne cztery liczby ustalajš położenie kontrolki okna potomnego (względem górnego lewego rogu obszaru roboczego okna dialogowego) oraz jej rozmiar. Po- łożenie i rozmiar wyrażone sš w jednostkach równych jednej czwartej œredniej szerokoœci czcionki i jednej ósmej jej wysokoœci. Wartoœci xWidth i yHeight sš igno- rowane w przypadku instrukcji ICON. Instrukcja DEFPUSHBUTTON w szablonie okna dialogowego dołšcza dodatko- wo styl WS GROUP oprócz stylu wymuszanego przez słowo kluczowe DEFPU- SHBUTTON. Podam więcej informacji o WS GROUP (i zwišzanym z nim stylu WS TABSTOP), gdy będę omawiał drugš wersję tego programu - ABOUT2. Procedura okna dialogowego Procedura okna dialogowego w programie obsługuje komunikaty okna dialogo- wego. Mimo że jest ona podobna do zwykłej procedury okna, w rzeczywistoœci nie jest takš procedurš. Procedura okna dla okna dialogowego znajduje się we- wnštrz systemu Windows. Wywołuje ona procedurę okna dialogowego z komu- nikatami, które otrzyrna. Poniżej pokazana jest procedura okna dialogowego z pro- gramu ABOUTl: BOOL CALLBACK AboutDlgProc (HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam) r switch (message) ( case WMţINITDIALO6 return TRUE case WM COMMAND : switch (LOWORD (wParam)) ( case IDOK : case IDCANCEL : EndDialog (hDlg, 0) ; return TRUE : Rozdział 11: Okna dialogowe break ; ) return FALSE ; 449 Parametry tej funkcji sš takie same jak w przypadku zwykłej procedury okna. Tak ak ona, procedura okna dialogowego musi być zdefiniowana jako funkcja CALLBACK. Mimo że zastosowałem hDlg jako uchwyt okna dialogowego, mo- żesz zamiast niego użyć równie dobrze hwnd. Przyjrzyjmy się różnicom pomię- dzy tš funkcjš a procedurš okna: ů Procedura okna zwraca typ LRESULT, a procedura okna dialogowego - typ BOOL, który zdefiniowany jest w plikach nagłówkowych Windows jako int. ů Procedura okna wywołuje funkcję Def 4VindowProc, jeœli nie przetwarza okre- œlonych komunikatów, a procedura okna dialogowego zwraca wartoœć TRUE (niezerowš), jeœli przetwarza komunikat, i FALSE (0), jeœli tego nie robi. ů Procedura okna dialogowego nie potrzebuje przetwarzać komunikatów WM PAINT ani WM DESTROY i nie odbiera komunikatu WM CREATE. Zamiast tego wykonuje inicjację podczas specjalnego komunikatu WM INIT DIALOG. Komunikat WM INITDIALOG to pierwszy komunikat, który odbiera procedura okna dialogowego. Jest on wysyłany tylko do procedur okien dialogowych. Jeœli rocedura okna dialogowego zwróci wartoœć'TRUE, Windows ustawia fokus wej- œ iowy na pierwszš w oknie dialogowym kontrolkę okna potomnego, która ma styl WS TABSTOP (styl ten omówię przy okazji programu ABOUT2). W tym oknie dialogowym pierwszš kontrolkš, która ma taki styl, jest przycisk polecenia. Al- ternatywnie podczas przetwarzania komunikatu WM INITDIALOG procedura okna dialogowego może użyć funkcji SetFocus, aby ustawić fokus w oknie dialo- gowym na jednej z kontrolek okna potomnego, i zwrócić wartoœć FALSE. Nasze okno dialogowe przetwarza jeszcze tylko jeden komunikat - WM COM- MAND. Jest to komunikat, który przycisk polecenia wysyła do swojego okna nadrzędnego, gdy zostanie kliknięty przyciskiem myszy lub naciœnięty będzie kla- wisz [Spacja}, a przycisk ma fokus wejœciowy. Identyfikator kontrolki (który usta- wiliœmy na IDOK w szablonie okna dialogowego) jest mniej znaczšcym słowem lParam. Dla tego komunikatu procedura okna dialogowego wywołuje funkcję End- Dialo , które nakazuje Windows, aby zruszczył okno dialogowe. Dla pozostałych komunikatów nasza procedura okna dialogowego zwraca wartoœć FALSE, która mówi procedurze okna dialogowego wewnštrz Windows, że nasza procedura nie rzetworzyła komunikatu. Komunikaty dla modalnych okien dialogowych nie przechodzš przez kolejkę komunikatów programu, więc nie musisz martwić się o wpływ skrótów klawiaturowych wewnštrz okna dialogowego. Wyœwietlanie okna dialogowego Podczas przetwarzania komunikatu WM CREATE w WndProc program ABOUT1 otrzymuje uchwyt egzemplarza programu i zachowuje go w zmiennej statycz- nej: hInstance = ((LPCREATESTRUCT) lParam)->hInstance ; 450 Częœć I: Podstawy Program ABOUTI sprawdza, czy mniej znaczšce słowo w parametrze wParam komunikatów WMţCOMMAND jest równe IDM_APP ABOUT. Jeœli tak jest, wywołuje funkcję DialogBox: DialogBox (hInstance, TEXT ("AboutBox"), hwnd, AboutDlgProc) ; Funkcja ta wymaga egzemplarza uchwytu (zapisanego podczas przetwarzania komunikatu WMţCREATE), nazwy okna dialogowego (zdefiniowanej w skryp- cie zasobów), okna nadrzędnego (główne okno programu) dla okna dialogowe- go i adresu procedury okna dialogowego. Jeœli użyjesz identyfikatora numerycz- nego zamiast nazwy szablonu okna dialogowego, możesz go przekonwertować na tekst, używajšc makra MAKEINTRESOURCE. Wybranie z menu opcji About Aboutl powoduje wyœwietlenie okna dialogowe- go pokazanego na rysunku 11-2. Okno to możesz zamknšć klikajšc myszš przy- cisk OK, naciskajšc klawisz spacji lub [Enter]. Dla dowolnego okna dialogowego , które zawiera domyœlny przycisk polecenia, po naciœnięciu klawisza [Enter] lub spacji, Windows wysyła do okna dialogowego komunikat WM_COMMAND z mniej znaczšcym słowem parametru wParam równym identyfikatorowi przyci- sku polecenia. W tym przypadku identyfikatorem jest IDOK. Okno dialogowe możesz zamknšć również naciskajšc klawisz [Esc]. W tym przypadku Windows wysyła komunikat 4VMţCOMMAND z identyfikatorem równym IDCANCEL. Funkcja DialogBox, którš wywołujesz, aby wyœwietlić okno dialogowe, nie zwra- ca sterowania do WndProc dopóki okno dialogowe nie zostanie zamknięte. War- toœć zwracana przez DialogBox jest drugim parametrem przekazywanym do funk- cji EndDialog wywoływanej w procedurze okna dialogowego. (Wartoœć ta nie jest używana w programie ABOUTl, ale jest w ABOUT2). Funkcja WndProc może wtedy zwrócić kontrolę systemowi Windows. Nawet jeœli okno dialogowe zostanie wyœwietlone, funkcja WndProc może odbie- rać komuriikaty. Masz możliwoœć wysłania komunikatów do WndProc z pozio- mu procedury okna dialogowego. Główne okno programu ABOUT1 jest oknem nadrzędnym okna dialogowego, więc wywołanie SendMessage w procedurze Abo- utDlgProc zaczynałoby się w następujšcy sposób: SendMessage (GetParent (hDlg), Wariacje na temat Mimo że edytor okien dialogowych i inne edytory zasobów w Visual C++ Develo- per Studio powodujš, że zaglšdanie do skryptu zasobów staje się niepotrzebne, nadal poznanie jego składni może okazać się użyteczne. Szczególnie w przypadku szablonów okien dialogowych znajomoœć składrii pozwala lepiej zorientować się w możliwoœciach i ograniczeniach okien dialogowych. Możesz również ręcznie utworzyć szablon okna dialogowego, jeżeli zechcesz w nim umieœcić coœ, czego nie da się tam wstawić w inny sposób (jak na przykład w programie HEXCALC, który pokazany jest w dalszej częœci tego rodziału). Kompilator i składnia skryptu za- sobów sš udokumentowane w pliku /Platform SDK/Windows Programming Guide- lines/Platform SDK Tools/Compiling/Llsing the IZesource Compiler. Styl okna dialogowego okreœlany w oknie dialogowym Properties w Developers Studio jest tłumaczony na wiersz STYLE w szablonie okna dialogowego. W przy- Rozdziaf 11: Okna dialogowe 451 padku programu ABOUT1 użyliœmy stylu, który jest stosowany najczęœciej do modalnych okien dialogowych: STYLE WS_POPUP ţ DS_MODALFRAME Możesz jednak poeksperymentować także z innymi stylami. Niektóre okna dia- logowe majš pasek tytułu, który okreœla cel okna i pozwala użytkownikowi prze- mieszczać je po ekranie za pomocš myszy. Umożliwia to styl WS CAPTION. Gdy użyjesz tego stylu, współrzędne x i y okreœlone w instrukcji DIALOG sš współ- rzędnymi obszaru roboczego okna dialogowego w odniesieniu do lewego górne- go rogu obszaru roboczego okna nadrzędnego. Pasek tytułu wyœwietlany jest nad współrzędnš y. Jeœli masz pasek tytułu, możesz umieœcić na nim tekst używajšc instrukcji CAP- TION, umieszczonej po instrukcji STYLE w szablonie okna dialogowego: CAPTION "Dialog Box Caption" lub podczas przetwarzania komunikatu WM INITDIALOG w procedurze okna dialogowego wykorzystać: SetWindowText (hDlg, TEXT ("Dialog Box Caption")) : Jeżeli użyjesz stylu WS CAPTION, możesz dodać także pole menu systemowe- go za pomocš stylu WS SYSMENU. Styl ten pozwala użytkownikowi wybrać plecenia Przenieœ i Zamknij z menu systemowego. Wybranie Resizing z pola listy Border w oknie dialogowym Properties (co odpo- wiada stylowi WS THICKFRAME) pozwala użytkownikowi zmienić rozmiar okna dialogowego, chociaż jest to bardzo rzadko stosowane. Jeœli chcesz coœ jesz- cze bardziej nietypowego, możesz dodać także pole maksymalizacji do stylu okna dialogowego. Do okna dialogowego możesz dodać nawet menu. Wtedy jego szablon będzie zawierać instrukcję: MENU menu-name Argument jest albo nazwš menu, albo numerem menu w skrypcie zasobów. Menu sš prawie niespotykane w modalnych oknach dialogowych. Jeœli zastosujesz je, upewnij się, że identyfikatory w menu i w kontrolkach okna dialogowego sš uni- katowe, a jeœli tak nie jest, że duplikujš te same funkcje. Instrukcja FONT pozwala ustawić w oknie dialogowym czcionkę innš niż syste- mowa. Dawniej było to rzadko spotykane w oknach dialogowych, ale ostatnio wiele się zmieniło. Developer Studio dla wszystkich okien dialogowych, które two- rzysz, wybiera domyœlnie 8-punktowš czcionkę MS Sans Serif. Program Windows może nabrać unikatowego wyglšdu dostarczajšc specjalne czcionki w oknach dia- logowych i innych danych wyjœciowych. Mimo że procedura okna dialogowego zwykle znajduje się wewnštrz tego syste- mu, możesz użyć jednej ze swoich procedur okna, aby przetworzyć komunikaty okna dialogowego. Chcšc to zrobić, podaj nazwę klasy okna w szablonie okna dialogowego: CLASS "class-name" Wišże się to z kilkoma trudnoœciami, ale omówię to rozwišzanie przy okazji pro- gramu HEXCALC pokazanego w dalszej częœci tego rozdziału. 452 Częœć Iţ Podstawy Gdy wywołujesz funkcję DialogBox, podajšc nazwę szablonu okna dialogowego , Windows ma prawie wszystko, czego potrzebuje, aby wyœwietlić okno dialogo- we wywołaniem funkcji CreateWindow. System ten dostaje współrzędne, rozmiar, styl i tytuł okna oraz menu z szablonu okna dialogowego, a egzemplarz uchwy- tu i uchwyt okna nadrzędnego z argumentów funkcji DialogBox. Jedynš dodat- kowš informacjš, której potrzebuje system, jest klasa okna (zakładajšc, że nie zo- stała okreœlona w szablonie okna dialogowego). Windows rejestruje specjalnš klasę okna dla okien dialogowych. Procedura okna dla tej klasy ma dostęp do adresu twojej procedury okna dialogowego (który podajesz w wywołaniu DialogBox), więc może informować program o komunikatach odbieranych przez okno dialogowe. Oczywiœcie, możesz utworzyć własne okno dialogowe i zarzšdzać nim. Jednak zastosowanie DialogBox jest znacznie łatwiejszym rozwišzaniem. Czasami potrzebujesz skorzystać z zalet menedżera okien dialogowych w Win- dows, ale nie chcesz (lub nie możesz) definiować szablonu okna dialogowego w skrypcie zasobów. Chcesz na przykład, aby program po uruchomieniu dyna- micznie utworzył okno dialogowe. Służy do tego funkcja DialogBoxlndirect, która używa struktur danych do zdefiniowania szablonu. W szablonie okna dialogowego z programu ABOUTI została użyta skrótowa notacja CTEXT, ICON i DEFPUSHBUTTON, aby zdefiniować trzy rodzaje kon- trolek okna potomnego umieszczane w oknie dialogowym. Istnieje kilka innych kontrolek, które możesz dodać w ten sposób. Każdy ich rodzaj odpowiada okre- œlonej predefiniowanej klasie i stylom okna. W poniżej tabeli pokazane sš odpo- wiednie klasy i style okna dla niektórych typowych kontrolek: Typ kontrolki Klasa okna Styl okna PUSHBUTTON button BS PUSHBUTTON I WS TABSTOP DEFPUSHBUTTON button BS DEFPUSHBUTTON I WS_TABSTOP CHECKBOX button BS CHECKBOX I WS TABSTOP RADIOBUTTON button BS RADIOBUTTON I WS TABSTOP GROilPBOX button BS GROUPBOX I WS TABSTOP LTEXT static SS LEFT I WS GROUP CTEXT static SS CENTER I WS GROUP RTEXT static SS RIGHT I WS GROUP ICON static SS ICON EDIT'TEXT edit ES LEFT I WS BORDER i WS_TABSTOP SCROLLBAR scrollbar SBS HORZ LISTBOX listbox LBS NOTIFY I WS BORDER I WS_VSCROLL COMBOBOX combobox CBS SIMPLE i WS TABSTOP Kompilator zasobów jest jedynym programem, który rozumie takš skróconš no- tację. Dodatkowo, oprócz stylów okna pokazanych powyżej, każda z kontrolek ma styl: WSţCHILD ţ WS VISIBLE Rozdział 11: Okna dialogowe 453 Dla wszystkich tych kontrolek, z wyjštkiem EDITTEXT, SCROLLBAR, LISTBOX i COMBOBOX, instrukcja ma następujšcy format: control-type "text" id, xPos, yPos, xWidth, yHeight, iStyle a dla kontrolek EDITTEXT, SCROLLBAR, LISTBOX i COMBOBOX: control-type id, xPos, yPos, xWidth, yHeight, iStyle czyli nie zawiera pola tekstowego. W obydwu instrukcjach parametr iStyle jest opcjonalny. W rozdziale 9 omawiałem zasady okreœlania szerokoœci i wysokoœć predefinio- wanych kontrolek okna potomnego. Pamiętaj, że rozmiary w szablonach okien dialogowych podawane sš w jednostkach równych jednej czwartej œredniej sze- rokoœci czcionki i jednej ósmej jej wysokoœci. Pole iStyle instrukcji kontrolek jest opcjonalne. Pozwala dołšczyć inne identyfi- katory stylu okna. Jeœli potrzebowałbyœ na przykład pola wyboru z tekstem po lewej stronie pola, mógłbyœ napisać: CHECKBOX "text", id, xPos, yPos, xWidth, yHeight, BSţLEFTTEXT Zwróć uwagę na to, że typ EDTTTEXT kontrolki zawiera ramkę. Jeżeli chcesz utwo- rzyć kontrolkę edycji okna potomnego bez ramki, możesz napisać: EDITTEXT id, xPos, yPos, xWidth, yHeight, NOT WS BORDER Kompilator zasobów rozpoznaje także ogólnš instrukcję kontrolki, która wyglš- da następujšco: CONTROL "text", id, "class", iStyle, xPos, yPos, xWidth, yHeight Instrukcja ta pozwala utworzyć dowolny typ kontrolki okna potomnego przez okreœlenie klasy okna i pełnego jego stylu. Zamiast używać na przykład: PUSHBUTTON "OK", IDOK, 10, 20, 32, 14 mógłbyœ zastosować: CONTROL "OK", IDOK, "button", WSţCHILD ţ WS VISIBLE BSţPUSHBUTTON ţ WS TABSTOP, 10, 20, 32, 14 Gdy skrypt zasobów jest kompilowany, te dwie instrukcje sš kodowane dokładnie tak samo w plikach RES i EXE. W Developer Studio takš instrukcję tworzy się wy- bierajšc opcję Custom Control z paska narzędzi Controls. Z programiu ABOUT3, pokazanego na rysunku 11-5, dowiesz się, jak możesz wykorzystać tę instrukcję do utworzenia kontrolki, której klasa okna jest zdefiniowana w programie. Gdy używasz instrukcji CONTROL w szablonie okna dialogowego, nie musisz dołšczać stylów WS CHILD i WS VISIBLE. Windows automatycznie dołšcza je, gdy tworzy okna potomne. Format instrukcji CONTROL wyjaœnia również, co robi menedżer okien dialogowych Windows, gdy tworzy okno dialogowe. Naj- pierw, jak już pisałem, tworzy okno potomne, którego oknem nadrzędnym jest uchwyt okna przekazany do funkcji DialogBox. Następnie dla każdej kontrolki w szablonie okna dialogowego menedżer okien dialogowych tworzy okno po- tomne. Oknem nadrzędnym każdej z tych kontrolek jest okno dialogowe. Instruk- cja CONTROL, pokazana powyżej, jest thxmaczona na wywołanie CreateWindow wyglšdajšce jak poniższe: hCtrl = CreateWindow (TEXT ("button"), TEXT ("OK"), WS CHILD ţ WS VISIBLE ţ WS TABSTOP ţ BSţPUSHBUTTON, 454 Częœć I: Podstawy 10 * cxChar / 4, 20 * cyChar / 8, 32 * cxChar / 4, 14 * cyChar / 8, hDlg, IDOK, hInstance, NULL) ; gdzie cxChar i cyChar to szerokoœć i wysokoœć czcionki okna dialogowego w pik- selach. Parametr hDig jest zwracany z wywołania CreateWindow, które tworzy okno dialogowe. Parametr hlnstance jest uzyskiwany z oryginalnego wywołania Dia- logBox. Bardziej złożone okna dialogowe Proste okno dialogowe z programu ABOUT1 demonstrowało podstawy zwišza- ne z tworzeniem i wyœwietlaniem. Teraz spróbujemy wykonać okna nieco bar- dziej skomplikowane. Program ABOUT2, pokazany na rysunku 11-3, przedsta- wia, jak zarzšdzać kontrolkami (w tym przypadku przyciskami opcji) wewnštrz procedury okna dialogowego i jak wypełnić obszar roboczy okna dialogowego. ABOUT2.C /* ABDUT2.C - Demonstracja okien dialogowych nr 2 (c) Charles Petzold, 1998 */ include iţinclude "resource.h" LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; BOOL CALLBACK AboutDlgProc (HWND, UINT, WPARAM, LPARAM) ; int iCurrentColor = IDC_BLACK, iCurrentFigure = IDCţRECT ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) static TCHAR szAppNameC] = TEXT ("About2") MSG msg ; HWND hwnd ; WNDCLASS wndclass : wndclass.style = CS_HREDRAW ţ CSţVREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndcl„ss.hInstance = hInstance ; wndclass.hIcon = LoadIcon (hInstance, szAppName) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) wndclass.lpszMenuName = szAppName ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) ( MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MB ICONERROR) ; Rozdział 11: Okna dialogowe 455 return 0 ; ) hwnd = CreateWindow (szAppName, TEXT ("About Box Demo Program"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW USEDEFAULT, CW_USEDEFAULT, CWţUSEDEFAULT, NULL, NULL, hInstance. NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) : while (GetMessage (&msg, NULL, 0, 0)) TranslateMessage (&msg) : DispatchMessage (&msg) : return msg.wParam ; , void PaintWindow (HWND hwnd, int iColor, int iFi9ure) ( static COLORREF crColorC8] = t RGB ( 0, 0. 0). RGB ( 0, 0, 255), RGB ( 0, 255, 0), RGB ( 0, 255, 255). RGB (255, 0, 0), RGB (255. 0, 255), RGB (255, 255, 0), RGB (255, 255, 255) 1 : HBRUSH hBrush ; HDC hdc ; RECT rect ; hdc = GetDC (hwnd) ; GetClientRect (hwnd, &rect) : hBrush = CreateSolidBrush (crColor[iColor - IDC BLACK]) : hBrush = (HBRUSH) SelectObject (hdc, hBrush) ; if (iFigure = IDCţRECT) Rectangle (hdc, rect.left, rect.top, rect.right, rect.bottom) ; else Ellipse (hdc. rect.left, rect.top, rect.right. rect.bottom) ; Delete0bject (SelectObject (hdc, hBrush)) ; ReleaseDC (hwnd, hdc) : void PaintTheBlock (HWND hCtrl, int iColor, int iFigure) InvalidateRect (hCtrl, NULL, TRUE) ; UpdateWindow (hCtrl) ; PaintWindow (hCtrl, iColor, iFigure) ; il.. LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) static HINSTANCE hInstance ; PAINTSTRUCT ps ; switch (message) 456 Częœć I: Podstawy (cišg dalszy ze strony 455) t case WM_CREATE: hInstance = ((LPCREATESTRUCT) lParam)->hInstance ; return 0 ; case WM COMMAND: switch (LOWORD (wParam)) ( case IDM_APP_ABOUT: if (DialogBox (hInstance, TEXT ("AboutBox"), hwnd, AboutDlgProc)) InvalidateRect (hwnd, NULL, TRUE) ; return 0 ; ) break ; case WM_PAINT: BeginPaint (hwnd, &ps) ; EndPaint (hwnd, &ps) ; PaintWindow (hwnd, iCurrentColor, iCurrentFigure) ; return 0 ; case WM DESTROY: PostOuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; 1 BOOL CALLBACK AboutDlgProc (HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam) f static HWND hCtrlBlock ; static int iColor, iFigure ; switch (message) ( case WM_INITDIALOG: iColor = iCurrentColor ; iFigure = iCurrentFigure ; CheckRadioButton (hDlg, IDC_BLACK, IDC_WHITE, iColor) ; CheckRadioButton (hDlg, IDCţRECT, IDCţELLIPSE. iFigure) ; hCtrlBlock = GetDlgItem (hDlg, IDCţPAINT) ; SetFocus (GetOlgItem (hDlg, iColor)) ; return FALSE ; case WM COMMAND: switch (LOWORD (wParam)) f case IDOK: iCurrentColor = iColor ; iCurrentFigure = iFigure ; EndDialog (hDlg, TRUE) ; return TRUE ; Rozdział 11: Okna dialogowe 457 case IDCANCEL: EndDialog (hDlg, FALSE) ; return TRUE ; case IDC_BLACK: case IDC_RED: case IDC_GREEN: case IDC_YELLOW: , case IDC_BLUE: case IDC_MAGENTA: case IDC_CYAN: case IDC_WHITE: iColor = LOWORD (wParam) ; CheckRadioButton (hDlg, IDC BLACK, IDC_WHITE, LOWORD (wParam)) ; PaintTheBlock (hCtrlBlock, iColor, iFigure) ; return TRUE ; case IDC_RECT: case IDCţELLIPSE: iFigure = LOWORD (wParam) ţ CheckRadioButton (hDlg, IDC RECT, IDC ELLIPSE, LOWORD (wParam)) ; PaintTheBlock (hCtrlBlock, iColor, iFigure) ; return TRUE ; } break ; case WM PAINT: PaintTheBlock (hCtrlBlock, iColor, iFigure) ; break ; J return FALSE ; ) ABOUT2.RC (fragmenty) //Microsoft Developer Studio generated resource script. ţţinclude "resource.h" ţţinclude "afxres.h" /////////////////////////////////////////////////////////////////////////// // Dialo9 ABOUTBOX DIALOG DISCARDABLE 32 32, 200, 234 STYLE DS MODALFRAME ţ WSţPOPUP ţ WS CAPTION FONT 8, "MS Sans Serif" BEGIN ICON "ABOUT2",IDC_STATIC,7,7.20 20 CTEXT "About2",IDCţSTATIC,57,12,86,8 CTEXT "About Box Demo Program" IDCţSTATIC,7,40,186,8 LTEXT "",IDCţPAINT,ll4,67,72,72 GROUPBOX "&Color",IDC_STATIC,7,60,84,143 RADIOBUTTON "&Black",IDC_BLACK,l6,76,64,8,WS GROUP ţ WS TABSTOP RADIOBUTTON "B&lue",IDC_BLUE,l6,92,64,8 RADIOBUTTON "&Green",IDC_GREEN,l6,108,64,8 RADIOBUTTON "Cya&n",IDC_CYAN,l6,124,64,8 RADIOBUTTON "&Red",IDC_RED,16,140,64,8 RADIOBUTTON "&Magenta",IDC MAGENTA,l6,156.64,8 458 Częœć i: Podstawy RADIOBUTTON "&Yellow",IDC_YELLOW,l6,172,64,8 RADIOBUTTON "&White",IDC_WHITE,l6,188,64,8 GROUPBOX "&Figure",IDC_STATIC,l09,156,84,46,WS GROUP RADIOBUTTON "Rec&tangle",IDC_RECT,ll6,172,65,8,WS GROUP ţ WS_TABSTOP RADIOBUTTON "&Ellipse",IDC_ELLIPSE,ll6,188,64,8 DEFPUSHBUTTON "OK",IDOK,35,212,50,14,WS_GROUP PUSHBUTTON "Cancel",IDCANCEL,ll3,212,50,14,WSţGROUP END /////////////////////////////////////////////////////////////////////////// // Icon ABOUT2 ICON DISCARDABLE "About2.ico" /////////////////////////////////////////////////////////////////////////// // Menu ABOUT2 MENU DISCARDABLE BEGIN POPUP "&Help" BEGIN MENUITEM "&About", IDM APP ABOUT END END RESOURCE.H (fragmenty) // Microsoft Developer Studio generated include file. // Used by About2.rc ţţdefine IDC_BLACK 1000 ţţdefine IDC_BLUE 1001 ţţdefine IDC_GREEN 1002 ţţdefine IDC_CYAN 1003 ţţdefine IDC_RED 1004 ţţdefine IDC_MAGENTA 1005 ţţdefine IDC_YELLOW 1006 ţţdefine IDC_WHITE 1007 ţţdefine IDC_RECT 1008 ţţdefine IDC_ELLIPSE 1009 4ţdefine IDC_PAINT 1010 ţţdefine IDM_APP_ABOUT 40001 ţţdefine IDC STATIC -1 ABOUT2.ICO Rysunek 11-3. Program ABOLTT2 Rozdział 11: Okna dialogowe 459 Okno dialogowe About w programie ABOUT2 ma dwie grupy przycisków opcji. Jedna grupa jest używana do wybrania koloru, a druga do wybrania prostokšta lub elipsy. Prostokšt lub elipsa sš wyœwietlane we wnętrzu okna dialogowego i wypełniane aktualnie zaznaczonym kolorem. Jeœli naciœniesz przycisk OK, okno dialogowe jest zamykane, a procedura okna rysuje wybranš figurę we własnym obszarze roboczym. Jeżeli przyciœniesz Cancel, obszar roboczy głównego okna pozostaje niezmieniony. Okno dialogowe jest pokazane na rysunku 11-4. Dla przy- cisków poleceń korzysta ono z predefiniowanych identyfikatorów IDOK i IDCAN- CEL, a każdy z przycisków opcji ma swój własny identyfikator, zaczynajšcy się od liter IDC. Identyfikatory te sš zdefiniowane w pliku RESOURCE.H. Rysunek 11-4. Okno dialogowe programu ABOUT2 Gdy będziesz tworzyć przyciski poleceń w oknie dialogowym ABOUT2, wstaw je w takiej kolejnoœci jak na rysunku. Zapewni to, że Developer Studio zdefiniuje w takiej samej kolejnoœci identyfikatory, z czego korzysta program. Usuń rów- nież zaznaczenie opcji Auto dla każdego z przycisków opcji. Przycisk Auto Ra- dio Button wymaga mniej kodu, ale jest bardziej złożony. Nadaj przyciskom iden- tyfikatory podane w pokazanym wczeœniej pliku ABOUT2.RC. Zaznacz opcję Group w oknie dialogowym Properties dla przycisków OK i Can- cel, dla pola grupy Figure oraz dla pierwszych przycisków opcji (Black i Rectan- gle) w każdej grupie. Następnie zaznacz pole opcji Tab Stop dla przycisków Black i Rectangle. Gdy ustawisz rozmiar i położenie wszystkich kontrolek okna dialogowego, wy- bierz opcję Tab Order z menu Layout. Kliknij każdš z kontrolek w kolejnoœci po- kazanej w skrypcie zasobów ABOUT2.RC. 460 Częœć I: Podstawy Praca z kontrolkami w oknie dialogowym Z rozdziału 9 dowiedziałeœ się, że większoœć kontrolek okien potomnych wysyła komunikat WM COMMAND do okna nadrzędnego (wyjštkiem sš kontrolki pa- sków przewijania). Wiesz już także, że okno nadrzędne może modyfikować kon- trolki okien potomnych (na przykład zaznaczać lub usuwać zaznaczenie przyci- sków opcji lub wyboru) wysyłajšc do nich komunikaty. W podobny sposób moż- na zmieniać kontrolki w procedurze okna dialogowego. Jeœli masz na przykład przyciski opcji, możesz zaznaczać je i usuwać zaznaczenia wysyłajšc do nich komunikaty. Windows dostarcza jednak kilku ułatwień, jeœli chodzi o pracę z kon- trolkami w oknach dialogowych. Przyjrzyjmy się metodzie komunikacji proce- dury okna dialogowego i kontrolek okien potomnych. Szablon okna dialogowego programu ABOUT2 jest pokazany w skrypcie zaso- bów ABOUT2.RC na rysunku 11-3. Kontrolka GROUPBOX jest po prostu ramkš z tytułem (Color lub Figure), która otacza każdš grupę przycisków opcji. Osiem przycisków opcji w pierwszej grupie wzajemnie się wyklucza, podobnie jak dwa przyciski w drugiej. Gdy jeden z przycisków opcji zostanie kliknięty myszš (lub jeœli naciœnięty został klawisz [Spacja], gdy jeden z przycisków opcji ma fokus wejœciowy), okno po- tomne wyœle swojemu oknu nadrzędnemu mniej znaczšce słowo parametru wPa- ram, ustawione na identyfikator kontrolki. Bardziej znaczšce słowo wParam jest kodem powiadomienia, a lParam - uchwytem okna kontrolki. Dla przycisku opcji kodem powiadomienia jest zawsze BN CLICKED, równy 0. Procedura okna dia- logowego w Windows przesyła wtedy komunikat WMţCOMMAND do proce- dury okna dialogowego w programie ABOUT2.C. Gdy procedura okna dialogo- wego odbierze komunikat WMţCOMMAND od jednego z przycisków poleceń, umieszcza przy nim znacznik i wyłšcza zaznaczenie przy wszystkich pozosta- łych przyciskach w grupie. Jak być może pamiętasz z rozdziału 9, zaznaczanie i usuwanie zaznaczenia przy- cisku opcji wymaga wysłania do kontrolki okna potomnego komunikatu BM CHECK. Aby włšczyć zaznaczenie przycisku opcji, używa się: SendMessage (hwndCtrl, BM SETCHECK, 1 , 0) ; Chcšc usunšć zaznaczenie, wykorzystuje się: SendMessage (hwndCtrl, BM SETCHECK, 0 , 0) ; Parametr hwndCtrl jest uchwytem okna kontrolki przycisku. Metoda ta może sprawić jednak nieco problemów w procedurze okna dialogo- wego, ponieważ nie znasz uchwytów okna wszystkich przycisków opcji. Znasz jedynie ten uchwyt, od którego obierasz komunikat. Na szczęœcie, Windows do- starcza funkcję, która pozwala uzyskać uchwyt kontrolki okna dialogowego przy zastosowaniu uchwytu okna dialogowego i identyfikatora kontrolki: hwndCtrl = GetDlgItem (hDlg, id) ; Można także uzyskać wartoœć identyfikatora kontrolki za pomocš uchwytu okna używajšc funkcji: Id = GetWindowLong (hwndCtrl, GWL ID) ; ale rzadko jest to potrzebne. Rozdział 11: Okna dialogowe 461 Zawróć uwagę na to, że w pliku nagłówkowym ABOUT2.H, pokazanym na ry- sunku 11-3, wartoœci identyfikatorów od IDCţBLACK do IDC WHITE dla oœmiu kolorów sš kolejnymi liczbami. Takie uporzšdkowanie pomaga przy przetwarza- niu komunikatów WM COMMAND odbieranych od przycisków opcji. Przy pierwszej próbie zaznaczenia i usunięcia zaznaczenia przycisków opcji można wypróbować następujšce rozwišzanie w procedurze okna dialogowego: static int iColor ; [inne linie programu] case WM COMMAND: switch (LOWORD (wParam)) ( Cpozostaţe wiersze programu] case IDC_BLACK: . case IDC_RED: case IDC_GREEN: case IDC_YELLOW: case IDC_BLUE: case IDC_MAGENTA: case IDC_CYAN: case IDC_WHITE: iColor = LOWORD (wParam) ; for (i = IDCţBLACK, i <= IDC_WHITE, i++) SendMessage (GetDlgItem (hDlg, i), BM SETCHECK, i == LOWORD (wParam), 0) ; return TRUE ; [inne linie programu] Takie rozwišzanie działa poprawnie. Zapisujesz nowš wartoœć koloru w iColor i tworzysz pętlę, która przechodzi przez wartoœci identyfikatorów dla wszystkich oœmiu kolorów. Pobierasz uchwyt okna dla każdego z tych oœmiu przycisków opcji i używasz funkcji SendMessage, aby wysłać każdemu uchwytowi komunikat BM SETCHECK. Wartoœć parametru wParam tego komunikatu jest ustawiana na 1 tylko dla przycisku, który wysłał komunikat WM COMMAND do procedury okna dialogowego. Pierwszym ułatwieniem jest specjalna procedura okna dialogowego SendDlgltem- Message: SendDlgItemMessage (hDlg, id, iMsg, wParam, Lparam) ; co odpowiada instrukcji: SendMessage (GetDlgItem (hDlg, id), id. wParam, lParam> ; Pętla wyglšdałaby wtedy następujšco: for (i = IDC BLACK, 1 <= IDC WHITE, i++) SendDlgItemMessage (hDlg, i, BMţSETCHECK, i == LWORD (wParam), 0) ; Takie rozwišzanie jest nieco lepsze, ale prawdziwe usprawnienie można uzyskać stosujšc funkcję CheckRadioButton: CheckRadioButton (hDlg, idFirst, idLast, idCheck) ; Funkcja ta wyłšcza zaznaczenie dla wszystkich kontrolek przycisków opeji z iden- tyfikatorami od idFirst do idLast z wyjštkiem przycisku opcji z identyfikatorem idCheck, który jest zaznaczany. Identyfikatory muszš być kolejnymi liczbami. Te- raz możemy całkowicie zrezygnować z pętli i napisać: 462 Częœć I: Podstawy CheckRadioButton (hDlg, IDCţBLACK, IDC WHITE, LOWORD (wParam)) ; W ten sposób jest to zrobione w procedurze okna dialogowego w programie ABOUT2. Podobne ułatwienie dostępne jest przy pracy z polami wyboru. Jeœli utworzysz kontrolkę CHECKBOX w oknie dialogowym, możesz wystawić i usunšć znacz- nik używajšc funkcji: CheckDlgButton (hDlg, idCheckbox, iCheck) ; Jeżeli parametr iCheck jest ustawiony na 1, przycisk jest zaznaczany; jeœli na 0, zaznaczenie przycisku jest usuwane. Stan pola wyboru w oknie dialogowym można okreœlić korzystajšc z funkcji: iCheck = IsDlgButtonChecked (hDlg, idCheckBox) ; Możesz albo zachować bieżšcy stan zaznaczenia przycisku opcji w zmiennej sta- tycznej wewnštrz procedury okna dialogowego, albo wykonać operację podob- nš do poniższej, aby przełšczyć stan przycisku w trakcie komunikatu WM COM- MAND: CheckDlgButton (hDlg, idCheckbox, !IsDlgButtonChecked (hDlg, idCheckbox)) ; Jeżeli zdefiniujesz kontrolkę BS AUTOCHECKBOX, nie musisz w ogóle przetwa- rzać komunikatu WM COMMAND. Możesz po prostu sprawdzić bieżšcy stan przycisku używajšc funkcji IsDIgButtonChecked przed opuszczeniem okna dialo- gowego. Jeœli jednak użyjesz stylu BS AUTORADIOBUTTON, funkcja IsDIgBut- tonChecked może okazać się niewystarczajšca, ponieważ musiałbyœ jš wywoływać dla każdego przycisku opcji dotšd, aż funkcja zwróci wartoœć TRUE. Zamiast tego lepiej jest jednak przechwytywać komunikaty WM COMMAND, œledzšc, który przycisk został naciœnięty. Przyciski OK i Cancel Program ABOUT2 ma dwa przyciski oznaczone OK i Cancel. W szablonie okna dialogowego ABOUT2.RC przycisk OK ma identyfikator IDOK (zdefiniowany w pliku WINUSER.H jako 1), a przycisk Cancel-identyfikator ODCANCEL (zde- finiowany jako 2). Przycisk OK jest przyciskiem domyœlnym: DEFPUSHBUTTON "OK", IDOK, 35, 212, 50, 14 PUSHBUTTON "Cancel", IDCANCEL, 113, 212, 50, 14 Takie uporzšdkowanie jest typowe dla przycisków OK i Cancel w oknach dialo- gowych. Ustawienie przycisku OK jako domyœlnego pomaga współpracować z in- terfejsem klawiatury. Oto powód: zwykle okno dialogowe zamyka się kliknię- ciem jednego z tych przycisków myszš lub przez naciœnięcie klawisza spacji, gdy jeden z nich ma fokus wejœciowy. Jednak procedura okna dialogowego generuje komunikat WMţCOMMAND także wtedy, gdy użytkownik naciœnie [Enter], nie- zależnie od tego, która kontrolka ma fokus wejœciowy. Słowo LOWORD parame- tru wParam jest ustawiane na wartoœć identyfikatora domyœlnego przycisku po- lecenia w oknie dialogowym, chyba że inny taki przycisk ma akurat fokus wej- œciowy. W naszym przypadku słowo LOWORD parametru wParam jest ustawia- ne na identyfikator przycisku polecenia, który ma fokus wejœciowy. Jeœli żaden Rozdział 11: Okna dialogowe 463 z przycisków polecenia w oknie dialogowym nie jest przyciskiem domyœlnym, to Windows wysyła procedurze okna dialogowego komunikat WM COMMAND ze słowem LOWORD parametru wParam równym IDOK. Jeżeli użytkownik na- ciœnie klawisz [Esc] lub [Ctrl+Break], Windows wysyła procedurze okna dialo- gowego komunikat WM COMMAND ze słowem LOWORD parametru wParam równym IDCANCEL. Nie musisz więc dołšczać specjalnej logiki klawiatury do procedury okna dialogowego, ponieważ naciœnięcia klawiszy, które zwykle za- mykajš okno dialogowe, sš tłumaczone przez ten system na komunikaty WM COMMAND dla tych dwóch przycisków poleceń. Funkcja AboutDIgProc obshxguje te dwa komunikaty WM COMMAND wywołu- jšc EndDialog: switch (LWORD (wParam)) ( case IDOK: iCurrentColor = iColor iCurrentFigure = iFigure EndDialog (hDl9, TRUE) ; return TRUE : case IDCANCEL: EndDialog (hDlg, FALSE) ; return TRUE : Procedura okna programu ABOUT2 używa zmiennych globalnych iCurrentColor i iCurrentFigure podczas rysowania prostokšta lub elipsy w obszarze roboczym programu. Procedura AboutDIgProc wykorzystuje statyczne zmienne lokalne iColor i iFigure podczas rysowania figury wewnštrz okna dialogowego. Zwróć uwagę na różne wartoœci drugiego parametru funkcji EndDialog. Jest to wartoœć przekazywana jako wartoœć zwracana przez oryginalnš funkcję Dialog- Box w procedurze WndProc: case IDM_ABOUT: if (DialogBox (hInstance, TEXT ("AboutBox"), hwnd, AboutDlgProc)) InvalidateRect (hwnd, NULL, TRUE) ; return 0 ; Jeœli DialogBox zwróci wartoœć TRUE (niezerowš), co oznacza, że naciœnięty zo- stał przycisk OK, obszar roboczy głównego okna musi zostać odœwieżony nowš figurš i kolorem. Figura i kolor zapisywane sš w zmiennych globalnych iCurrent- Color i CurrentFigure przez procedurę AboutDIgProc, gdy odbierze ona komuni- kat WMţCOMMAND z mniej znaczšcym słowem parametru wParam równym IDOK. Jeżeli DialogBox zwróci wartoœć FALSE, główne okno dalej używa poprzed- nich ustawień iCurrentColor i iCurrentFigure. Wartoœci TRUE i FALSE sš zwykle stosowane w wywołaniach EndDialog, aby zasygnalizować procedurze głównego okna, że użytkownik zamknšł okno dia- logowe przyciskiem OK lub Cancel. Argument kierowany do funkcji EndDialog jest jednak typu int, podobnie jak wartoœć zwracana przez funkcję DialogBox, więc można przekazać więcej informacji niż tylko TRUE lub FALSE. 464 Częœć I: Podstawy Unikanie zmiennych globalnych Zastosowanie zmiennych globalnych w programie ABOUT2 może ci przeszka- dzać. Niektórzy programiœci (włšcznie ze mnš) wolš ograniczać użycie zmien- nych globalnych do niezbędnego minimum. Zdefiniowanie zmiennych iCurrent- Color i iCurrentFigure w programie ABOUT2 jako globalnych z pewnoœciš jest uzasadnione, ponieważ muszš one być wykorzystane zarówno w zwykłej proce- durze okna, jak i w procedurze okna dialogowego. Jeœli jednak program zawiera wiele okien dialogowych, z których każde może modyfikować wartoœci kilku zmiennych, można w ten sposób łatwo zwiększyć liczbę potrzebnych zmiennych globalnych, powodujšc pomyłki. W zwišzku z tym lepiej jest skojarzyć każde okno dialogowe w programie ze struk- turš danych zawierajšcš wszystkie zmienne, które może ono modyfikować. Struk- tury takie można zdefiniować za pomocš instrukcji typedef. Na przykład w pro- gramie ABOUT2 mógłbyœ zdefiniować następujšcš strukturę zwišzanš z oknem dialogowym About: typedef struct ( int iColor, iFigure ; ) ABOUTBOXţDATA ; W procedurze WndProc należy zdefiniować i zainicjować zmiennš statycznš opartš na tej strukturze: static ABOUTBOX DATA ad = ( IDC BLACK, IDCţRECT ) ; Również w WndProc należy zastšpić wszystkie wystšpienia iCurrentColor oraz iCurrentFigure przez ad.iColor i ad.iFigure. Wywołujšc okno dialogowe użyj DialogBoxParam zamiast DialogBox. Pišty argument tej funkcji może być dowolnš 32-bitowš wartoœciš. Zwykle jest on ustawiany na wskaŸnik do struktury, w tym przypadku ABOUTBOX DATA w procedurze WndProc: case IDM ABOUT: if (DialogBoxParam (hInstance, TEXT ("AboutBox"), hwnd, AboutDlgProc, &ad)) InvalidateRect (hwnd, NULL, TRUE) ; return 0 ; Oto klucz do tej metody: ostatni argument funkcji DialogBoxParam jest przekazy- wany do procedury okna dialogowego jako parametr lParam w komunikacie WM_INITDIALOG. Procedura okna dialogowego powinna mieć dwie zmienne statyczne (strukturę i wskaŸ.nik do niej) oparte na strukturze ABOUTBOX DATA: static ABOUTBOXţDATA ad. * pad ; Powyższa definicja zastępuje w procedurze AboutDlgProc definicje iColor i iFigu- re. Po otrzymaniu komunikatu WMţINITDIALOG procedura okna dialogowego ustawia wartoœci tych dwóch zmiennych na podstawie parametru lParam: pad = (ABOUTBOX_DATA *) lParam ; ad = * pad ; W pierwszej instrukcji zmienna pad jest ustawiana na wskaŸnik lParam. Oznacza to, że pad wskazuje w istocie na strukturę ABOUTBOX DATA, zdefiniowanš Rozdział 11: Okna dialogowe 465 w procedurze WndProc. Druga instrukcja wykonuje kopiowanie pole po polu struktury z WndProc do lokalnej struktury w DlgProc. Teraz w całej procedurze AboutDlgProc należy zastšpić zmienne iFigure i iColor przez ad.iColor i ad.iFigure z wyjštkiem kodu uruchamianego, gdy użytkownik naciœnie przycisk OK. W tym przypadku należy skopiować zawartoœć struktury lokalnej z powrotem do struktury w WndProc: case IDOK : * pad = ad ; EndDialog (hDlg. TRUE) ; return TRUE : Grupy i miejsca przejœcia tabulatorem W rozdziale 9 używaliœmy podklas okna, aby umożliwić w programie COLORSI przechodzenie z jednego paska przewijania do drugiego przez naciœnięcie kla- wisza [Tab]. W oknie dialogowym tworzenie podklas okna nie jest potrzebne - Windows zajmuje się logikš wymaganš do przenoszenia fokusu wejœciowego z jednej kontrolki do drugiej. Musisz jednak wspomóc system, używajšc stylów okna WS TABSTOP i WS GROUP w szablonie okna dialogowego. Dla wszyst- kich kontrolek, które majš być dostępne przy użyciu klawisza [Tab], musisz do- łšczyć WS TABSTOP do stylu okna. Jeœli odwołasz się do tabeli przedstawionej wczeœniej w tym rozdziale, zauwa- żysz, że wiele kontrolek dołšcza styl WS TABSTOP domyœlnie, natomiast pozo- stałe tego nie robiš. Ogólnie rzecz bioršc, kontrolki, które nie dołšczajš domyœl- nie stylu WS TABSTOP (w szczególnoœci kontrolki statyczne), nie powinny otrzy- mywać fokusu wejœciowego, ponieważ i tak nie mogš nic z nim zrobić. Jeżeli nie ustawisz fokusu wejœciowego na okreœlonš kontrolkę w oknie dialogowym pod- czas przetwarzania komunikatu WM INITDIALOG, Windows ustawia fokus wejœciowy na pierwszš kontrolkę w oknie dialogowym, która ma styl WM TAB- STOP. Drugim interfejsem klawiatury, który system ten dołšcza do okna dialogowego, sš klawisze strzałek. Ten interfejs ma szczególne znaczenie w przypadku przyci- sków opcji. Gdy użyjesz klawisza [Tab], aby przejœć do aktualnie zaznaczonego w grupie przycisku opcji, potrzebujesz klawiszy strzałek, które pozwoliłyby prze- nieœć fokus wejœciowy na inny przycisk opcji w tej grupie. Efekt ten uzyskasz wykorzystujšc styl WS GROUP okna. Dla grup kontrolek okreœlonych w szablo- nie okna dialogowego Windows użyje klawiszy strzałek do przemieszczania fo- kusu wejœciowego od pierwszej kontrolki, która ma styl WS GROUP, aż do na- stępnej majšcej taki styl. Z ostatniej kontrolki w oknie dialogowym system ten przeniesie fokus na pierwszš kontrolkę, jeœli będzie to konieczne. Domyœlnie kontrolki LTEXT, CTEXT, RTEXT i ICON majš styl WS GROUP, któ- ry oznacza koniec grupy. Często musisz dodawać styl WS GROUP także do in- nych typów kontrolek. Przyjrzyj się szablonowi okna dialogowego w pliku ABOUT2.RC. Czterema kon- trolkami, które majš styl WS_TABSTOP, sš pierwsze przyciski opcji w obu gru- pach (ustawione przez użytkownika) i dwa przyciski poleceń (domyœlnie). Gdy 466 Częœć I: Podstawy wywołasz okno dialogowe po raz pierwszy, pomiędzy tymi kontrolkami będziesz mógł się przemieszczać klawiszem [Tab]. Wewnštrz każdej grupy przycisków opcji używasz klawiszy strzałek, aby prze- nieœć fokus wejœciowy i znacznik. Styl WS GROUP majš na przykład pierwszy przycisk opcji (Black) w grupie Color i pole grupy Figure. Oznacza to, że możesz wykorzystać klawisze strzałek, aby przemieszczać fokus od przycisku opcji Black aż do pola grupy Figure. Styl WS GROUP ma także pierwszy przycisk opcji (Rec- tangle) w polu grupy Figure i przycisk DEFPUSHBUTTON, więc możesz użyć klawiszy strzałek do przemieszczania się pomiędzy dwoma przyciskami w tej grupie: Rectangle i Elipse. Obydwa przyciski poleceń majš styl WS GROUP, dzięki czemu klawisze strzałek nie spełniajš żadnej funkcji, gdy przyciski te majš fokus wejœciowy. Podczas wykonywania programu ABOUT2 menedżer okien dialogowych Win- dows czyni magiczne rzeczy z dwiema grupami przycisków opcji. Jak należy oczekiwać, klawisze strzałek wewnštrz grupy przycisków opcji przenoszš fokus wejœciowy i wysyłajš komunikat WM COMMAND do procedury okna dialogo- wego. Kiedy jednak zmienisz w grupie zaznaczony przycisk opcji, Windows przy- pisze nowo zaznaczonemu styl WS_TABSTOP. Następnym razem, gdy przejdziesz do tej grupy klawiszem [Tab], fokus wejœciowy zostanie ustawiony na zaznaczo- ny przycisk opcji. Znak & w polu tekstowym powoduje, że litera po nim występujšca jest podkre- œlana, i dodaje kolejny interfejs klawiatury. Możesz przenieœć fokus wejœciowy do dowolnego z przycisków opcji naciskajšc podkreœlonš literę. Naciskajšc [C] (dla pola grupy Color) lub [F] (dla pola grupy Figure) możesz przenieœć fokus wejœciowy do aktualnie zaznaczonego w tej grupie przycisku opcji. Mimo że programiœci zwykle pozwalajš menedżerowi okien dialogowych zajšć się wszystkim, Windows dostarcza dwie funkcje, które umożliwiajš odnalezie- nie następnego lub poprzedniego miejsca przejœcia tabulatorem. Sš to funkcje: hwndCtrl = GetNextDlgTabltem (hDlg, hwndCtrl, bPrevious) ; oraz hwndCtrl = GetNextDlgGroupItem (hDlg, hwndCtrl, bPrevious) : jeœli hPrevious ma wartoœć TRUE, funkcja zwraca poprzednie miejsce przejœcia tabulatorem, a jeżeli FALSE - następne miejsce przejœcia tabulatorem lub grupę. Rysowanie w oknie dialogowym Program ABOUT2 wykonuje także coœ nietypowego - rysuje w oknie dialogowym. Przyjrzyjmy się, jak to działa. W szablonie okna dialogowego, w pliku ABOUT2.RC zdefiniowana jest pusta kontrolka tekstu, majšca położenie i rozmiar obszaru, któ- ry chcemy zamalować: LTEXT "" IDCţPAINT, 114, 67, 72, 72 Obszar ten ma szerokoœć równš osiemnastu znakom i wysokoœć wynoszšcš dzie- więć znaków. Ponieważ kontrolka nie zawiera tekstu, wszystko, co robi proce- dura okna dla klasy statycznej, to usunięcie tła, w przypadku gdy kontrolka okna potomnego ma być przemalowana. Rozdział 11: Okna dialogowe 467 Jeœli zmieni się bieżšcy kolor lub figura albo okno dialogowe odbierze komuni- kat WMţPAINT, procedura okna dialogowego wywołuje funkcję PaintTheBlock, która zdefiniowana jest w pliku ABOUT2.C: PaintTheBlock (hCtlrBlock, iColor, iFigure) : W procedurze AboutDlgProc podczas przetwarzania komunikatu WMţIMTDIALOG ustawiony został uchwyt okna hCtrBlock: hCtrlBlock = GetDlgBlock (hDlg, IDDţPAINT) ; Oto funkcja PaintTheBlock: void PaintTheBlock (HWND hCtrl, int iColor, int iFigure) ( InvalidateRect (hCtrl, NULL, TRUE) ; UpdateWindow (hCtrl) ; PaintWindow (hCtrl, iColor, iFigure) ; Legalizuje ona kontrolkę okna potomnego, generuje komunikat WM PAINT dla kontrolki procedury okna, a następnie wywołuje innš funkcję z pliku ABOUT2, nazwanš PaintWindow. Funkcja PaintWindow pobiera uchwyt kontekstu urzšdzenia dla hCtrl i rysuje wybranš figurę, wypełniajšc jš odpowiednim kolorem. Rozmiar kontrolki okna potomnego jest uzyskiwany od funkcji GetClientRect. Mimo że szablon okna dia- logowego definiuje rozmiar kontrolki w znakach, funkcja GetClientRect pobiera rozmiar w pikselach. Możesz użyć również funkcji MapDialogRect, aby przekon- wertować współrzędne znakowe w obszarze roboczym na piksele. W rzeczywistoœci nie zamalowujemy obszanx roboczego okna dialogowego - tylko obszar roboczy kontrolki okna potomnego. Za każdym razem, gdy okno dialo- gowe odbierze komunikat WM PAINT, kontrolka okna potomnego jest legalizo- wana i odœwieżana. Następnie jest ona zamalowywana. Używanie innych funkcji z oknami dialogowymi Większoœć funkcji, które wykorzystujemy z oknami potomnymi, stosujemy także z kontrolkami okna dialogowego. Jeœli chcesz być złoœliwy, możesz użyć metody MoveWindow do przesuwania kontrolek po oknie dialogowym i zmusić użytkow- nika do œcigania ich kursorem myszy. Czasami trzeba dynamicznie włšczać i wyłšczać okreœlone kontrolki w oknie dia- logowym w zależnoœci od ustawień innych kontrolek. Poniższe wywołanie: EnableWindow (hwndCtrl, bEnable) : włšcza kontrolkę, gdy bEnable ma wartoœć TRUE (niezerowš), i wyłšcza jš, gdy bEnable jest równy FALSE (0). Gdy kontrolka jest wyłšczona, nie odbiera danych wejœciowych z klawiatury ani myszy. Nie wyłšczaj tej, która ma fokus wejœcio- wy. Definiowanie własnych kontrolek Mimo że Windows bierze na siebie dużš częœć odpowiedzialnoœci zwišzanej z za- rzšdzaniem oknami dialogowymi i kontrolkami okien potomnych, istniejš różne metody powalajšce wprowadzić do tego procesu własny kod. Widzieliœmy już 468 Częœć I: Podstawy metodę, która umożliwia zamalowanie obszaru okna dialogowego. Możesz użyć także podklas (omawianych w rozdziale 9), aby zmienić działanie kontrolek okien potomnych. Możesz także definiować własne kontrolki okien potomnych i stosować je w oknie dialogowym. Załóżmy, że nie odpowiadajš ci prostokštne przyciski poleceń i wo- lałbyœ stosować przyciski eliptyczne. Możesz tego dokonać rejestrujšc klasę okna i wykorzystujšc własnš procedurę okna do przetwarzania komunikatów od no- wej kontrolki okna potomnego. Następnie tę klasę okna musisz wprowadzić w Developer Studio w oknie dialogowym Properties, zwišzanym z nowš kontrol- kš. Spowoduje to umieszczenie instrukcji CONTROL w szablonie okna dialogo- wego. Program ABOUT3, pokazany na rysunku 11-5, wykonuje opisane czynno- œci. ABOUT3.C /* ABOUT3.C - Demonstracja okien dialogowych nr 3 (c) Charles Petzold, 1998 */ ţţinclude ţţinclude "resource.h" LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; BOOL CALLBACK AboutDlgProc (HWND, UINT, WPARAM, LPARAM) ; LRESULT CALLBACK EllipPushWndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) static TCHAR szAppName[J = TEXT ("About3") ; MSG msg ; HWND hwnd ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CS UREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (hInstance, szAppName) ; wndclass.hCursor = LoadCursor (NULL, IDC ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITEţBRUSH) wndclass.lpszMenuName = szAppName ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) ( MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MBţICONERROR) ; return 0 ; wndclass.style = CS HREDRAW ţ CSţVREDRAW ; 469 Rozdział 11: Okna dialogowe wndclass.lpfnWndProc = EllipPushWndProc ; wndclass.cbClsExtra = 0 : wndclass.cbWndExtra = 0 : wndclass.hInstance = hInstance ; wndclass.hIcon = NULL ; wndclass.hCursor = LoadCursor (NULL, IDCţARROW) : wndclass.hbrBackground = (HBRUSH) (COLORţBTNFACE + 1) : wndclass.lpszMenuName = NULL ţ wndclass.lpszClassName = TEXT ("EllipPush") : RegisterClass (&wndclass) : hwnd = CreateWindow (szAppName. TEXT ("About Box Demo Program"). WS OVERLAPPEDWINDOW. CW USEDEFAULT, CW USEDEFAULT, CW USEDEFAULT. CW USEDEFAULT, NULL, NULL, hInstance, NULL) : ShowWindow (hwnd, iCmdShow) : UpdateWindow (hwnd) : while (GetMessage (&msg, NULL, 0. 0)) l TranslateMessa9e (&msg) : DispatchMessage (&msg) : return msg.wParam ; 1 LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ( static HINSTANCE hInstance : switch (message) ( case WM CREATE : hInstance = ((LPCREATESTRUCT) lParam)->hlnstance : return 0 ; case WM COMMAND : switch (LOWORD (wParam)) ( case IDM ŽPP ABOUT : DialogBox (hInstance, TEXT ("AboutBox"), hwnd. AboutDlgProc) : return 0 : ) break : case WM DESTROY : PostOuitMessage (0) : return 0 ; ) return DefWindowProc (hwnd, message, wParam. lParam) : ) BOOL CALLBACK AboutDlgProc (HWND hDlg. UINT message, WPARAM wParam, LPARAM lParam) 470 Częœć I: Podstawy (cišg dalszy ze strony 469) switch (message) ( case WM INITDIALOG : return TRUE ; case WM COMMAND : switch (LOWORD (wParam)) ( case IDOK : EndDialog (hDlg, 0) ; return TRUE ; ) break ; ) return FALSE ; ) LRESULT CALLBACK EllipPushWndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ( TCHAR szText(40] ; HBRUSH hBrush ; ' HDC hdc ; PAINTSTRUCT ps ; RECT rect ; switch (message) ( case WM_PAINT : GetClientRect (hwnd, &rect) ; GetWindowText (hwnd, szText, sizeof (szText)) ; hdc = BeginPaint (hwnd, &ps) ; hBrush = CreateSolidBrush (GetSysColor (COLOR WINDOW)) ; hBrush = (HBRUSH) SelectObject (hdc, hBrush) ; SetBkColor (hdc, GetSysColor (COLOR_WINDOW)) ; SetTextColor (hdc, GetSysColor (COLOR WINDOWTEXT)) ; Ellipse (hdc, rect.left, rect.top, rect.right, rect.bottom) ; DrawText (hdc, szText, -1, &rect, DTţSINGLELINE ţ DT CENTER ţ DT UCENTER) ; Delete0bject (SelectObject (hdc, hBrush)) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_KEYUP : if (wParam != UKţSPACE) break ; // nie powiodło się case WM LBUTTONUP : SendMessage (GetParent (hwnd), WM_COMMAND, GetWindowLong (hwnd, GWLţIDJ, (LPARAM) hwnd) ; return 0 ; Rozdział 11: Okna dialogowe 471 1 return DefWindowProc (hwnd, message, wParam, lParam) ; ABOUT3.RC (fragmenty) //Microsoft Developer Studio generated resource script. ţnclude "resource.h" ţţinclude "afxres.h" /////////////////////////////////////////////////////////////////////////// // Dialog ABOUTBOX DIALOG DISCARDABLE 32, 32, 180, 100 STYLE DS_MODALFRAME ţ WSţPOPUP FONT 8, "MS Sans Serif" BEGIN CONTROL "OK",IDOK,"EllipPush",WS_GROUP ţ WS TABSTOP,73,79,32,14 ICON "ABOUT3",IDC_STATIC,7,7,20,20 CTEXT "About3",IDC_STATIC,40,12,100,8 CTEXT "About Box Demo Program",IDC_STATIC,7,40,166,8 CTEXT "(c) Charles Petzold, 1998",IDC STATIC,7,52,166,8 END /////////////////////////////////////////////////////////////////////////// // Menu ABOUT3 MENU DISCARDABLE BEGIN POPUP "&Help" BEGIN MENUITEM "&About About3... , IDM APP ABOUT END END /////////////////////////////////////////////////////////////////////////// // Icon ABOUT3 ICON DISCARDABLE "iconl.ico" RESOURCE.H (fragmenty) // Microsoft Developer Studio generated include file. // Used by About3.rc 4ţdefine IDM_APP_ABOUT 40001 ţţdefine IDCţSTATIC -1 Częœć I: Podstawy (cišg dalszy ze strony 471) i I ABOUT3.ICO ! Rysunek 11-5. Program ABOUT3 Klasa okna, którš będziemy rejestrować, nazywa się EllipPush (eliptyczny przy- cisk polecenia). W edytorze okien dialogowych w Developer Studio usuń oby- dwa przyciski OK i Cancel. Aby dodać kontrolkę opartš na klasie okna EllipPush, wybierz Custom Control z paska narzędzi Controls. W oknie dialogowym Pro- perties tej kontrolki wpisz w pole Class nazwę: EllipPush. Zamiast instrukcji DE- FPUSHBUTTON, pojawiajšcej się zwykle w szablonie okna dialogowego, zoba- czysz instrukcję CONTROL z nowš klasš okna: CONTROL "OK" IDOK, "EllipPush", TABGRP, 64, 60, 32, 14 Menedżer okien dialogowych używa tej klasy okna w wywołaniu CreateWindow podczas tworzenia kontrolki okna potomnego w oknie dialogowym. Program ABOUT3.C rejestruje klasę okna EllipPush w funkcji WinMain: wndclass.style = CS_HREDRAW ţ CS VREDRAW ; wndclass.lpfnWndProc = EllipPushWndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = NULL ; wndclass.hCursor = LoadCursor (NULL, IDC ARROW) ; wndclass.hbrBackground = (HBRUSH) (COLOR WINDOW + 1) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = TEXT ("EllipPush") ; RegisterClass (&wndclass) ; Klasa okna podaje, że procedurš okna jest EIIipPushWndProc, znajdujšca się w pliku ABOUT3.C. Procedura okna EIIipPushWndProc przetwarza tylko komunikaty: WM PAINT, WMţKEYLTP i 4VMţLBUTTONUP. Po odebraniu komunikatu WMţPAINT pobiera rozmiar swojego okna za pomocš funkcji GetClientRect i za pomocš funkcji Ge- tWindowText tekst, który pojawia się na przycisku. Używa funkcji Ellipse i Draw- Text systemu Windows, aby narysować elipsę i wyœwietlić tekst. Rozdział 11: Okna dialogowe 473 Przetwarzanie komunikatów WM KEYUP i WM LBUTTONUP jest proste: case WMţKEYUP : if (wParam != VKţSPACE) break ; // nie powiodło się case WM_LBUTTONUP : SendMessage (GetParent (hwnd), WM_COMMAND, GetWindowLong (hwnd, GWLţID), (LPARAM) hwnd) ; return 0 ; Procedura okna pobiera uchwyt swojego okna nadrzędnego (okna dialogowego) używajšc funkcji GetParent i wysyła komunikat WM COMMAND z parametrem wParam równym identyfikatorowi kontrolki. Identyfikator jest uzyskiwany po zastosowaniu funkcji GetWindowLong. Procedura okna dialogowego przesyła na- stępnie ten komunikat do procedury okna dialogowego w programie ABOUT3. IZezultatem jest własny przycisk polecenia, pokazany na rysunku 11-6. Tę samš metodę można wykorzystać, aby uzyskać inne niestandardowe kontrolki w oknie dialogowym. Rysunek 11-6. Niestandardowy przycisk polecenia utworzony przez program ABOUT3 Czy to już wszystko? Niezupełnie. Procedura EllipPushButtonWndProc jest jedynie niezbędnym minimum, jeœli chodzi o logikę zwišzanš z zarzšdzaniem kontrolkš okna potomnego. Nasz przycisk na przykład nie zmienia się, gdy jest naciskany. Aby odwrócić kolory wnętrza przycisku polecenia, procedura okna musiałaby prze- twarzać komunikaty WM KEYDOWN (od spacji) i 4VMţLBUTTONDOWN. Po- winna też œledzić ruchy myszy podczas komunikatu WM LBUTTONDOWN i przy- wracać normalne kolory przycisku, jeżeli kursor myszy wyjdzie poza obszar ro- boczy okna potomnego, a lewy przycisk myszy będzie nadal naciœnięty. Jedynie w przypadku zwolnienia tego przycisku, gdy jej kursor znajduje się nad przyci- skiem polecenia, procedura okna powinna wysyłać do swojego okna nadrzędne- go komunikat WM COMMAND. 474 Częœć I: Podstawy Procedura EIlipPushWndProc nie przetwarza komunikatów WM ENABLE. Jak wspominałem wczeœniej, procedura okna dialogowego może wyłšczyć okno uży- wajšc funkcji EnableWindow. Okno potomne powinno w takim przypadku być szare, aby wskazać, że jest niedostępne i nie może odbierać komunikatów, a nie wyœwietlać tekst na czarno. Jeżeli procedura okna kontrolki okna potomnego ma przechowywać dane, które różniš się dla każdego z utworzonych okien, może to zrobić, wykorzystujšc do- datniš wartoœć cbWndExtra w strukturze klasy okna. Zastosowanie takiej warto- œci rezerwuje w wewnętrznej strukturze okna przestrzeń, do której dostęp moż- na uzyskać funkcjami SetWindowLong i GetWindowLong. Niemodalne okna dialogowe Na poczštku tego rozdziału napisałem, że okna dialogowe mogš być modalne albo niemodalne. Do tej pory przyglšdaliœmy się modalnym oknom dialogowym, które stosuje się najczęœciej. Modalne okna dialogowe (z wyjštkiem modalnych systemowych okien dialogowych) pozwalajš użytkownikowi przełšczać się po- między oknem dialogowym i innymi programami. Użytkownik nie może jednak przejœć do innego okna w programie, dopóki wyœwietlane modalne okno dialo- gowe nie zostanie usunięte. Niemodalne okna dialogowe pozwalajš użytkowni- kowi przełšczać się między oknem dialogowym i oknem, które je utworzyło, oraz między oknem dialogowym i innymi programami. Niemodalne okno dialogowe jest z tego powodu bardziej podobne do zwykłych okien tworzonych przez pro- gram. Niemodalne okna dialogowe stosuje się, gdy wyœwietlanie okna dialogowego przez pewien czas może być wygodne dla użytkownika. Procesory tekstów uży- wajš na przykład niemodalnych okien dialogowych dla opcji Odszukaj i Zamień. Gdyby okno dialogowe Odszukaj było modalne, użytkownik musiałby wybrać z menu opcję Odszukaj, wprowadzić tekst do odszukania, zamknšć okno dialo- gowe, aby powrócić do dokumentu, i powtórzyć cały ten proces w celu odnale- zienia kolejnego wystšpienia tego samego tekstu. Pozwolenie użytkownikowi na przełšczanie się między dokumentem i oknem dialogowym jest rozwišzaniem o wiele bardziej wygodnym. Jak widziałeœ, modalne okna dialogowe tworzone sš przy użyciu funkcji Dialog- Box. Funkcja ta zwraca wartoœć dopiero po zniszczeniu okna dialogowego. Zwra- cana wartoœć jest okreœlona przez drugi parametr funkcji EndDialog; której uży- wa się wewnštrz procedury okna dialogowego do jego zamknięcia. Niemodalne okna dialogowego tworzone sš za pomocš funkcji CreateDialog, która ma takie same parametry jak DialogBox: hDlgModeless = CreateDialog (hInstance, szTemplate, HwndParent, DialogProc) ; Różni je to, że funkcja CreateDialog zwraca natychmiast uchwyt okna dialogowe- go. Zwykle uchwyt ten przechowywany jest w zmiennej globalnej. Mimo że sposób wykorzystania funkcji DialogBox do utworzenia modalnych okien dialogowych i CreateDialog do niemodalnych jest podobny, możesz je odróżnić Rozdział 11: Okna dialogowe 475 pamiętajšc o tym, że niemodalne okna dialogowe sš podobne do zwykłych. Funk- ! cja CreateDialog powinna przypominać ci funkcję CreateWindow, która tworzy zwykłe okna. Różnice migdzy modalnymi i niemodalnymi oknami dialogowymi Praca z niemodalnymi oknami dialogowymi jest podobna do pracy z modalny- mi oknami dialogowymi, ale istniejš też cztery istotne różnice. Po pierwsze, niemodalne okna dialogowe majš zwykle pasek tytułu i pole menu systemowego. Opcje te sš ustawiane jako domyœlne podczas tworzenia okna dia- logowego w Developer Studio. W szablonie okna dialogowego instrukcja STYLE dla niemodalnego okna dialogowego wyglšdałaby podobnie do poniższej: STYLE WSţPOPUP ţ WS CAPTION ţ WSţSYSMENU ţ WSţVISIBLE Pasek tytułu i menu systemowe pozwalajš użytkownikowi przemieœcić niemo- dalne okno dialogowe do innego obszaru ekranu za pomocš myszy albo klawia- tury. W przypadku modalnych okien dialogowych zwykle nie dostarcza się pa- ska tytułu i menu systemowego, ponieważ użytkownik i tak nie może nic zrobić z oknem leżšcym pod spodem. Po drugie, zwróć uwagę na to, że w naszej przykładowej instrukcji znalazł się styl WS VISIBLE. W Developer Studio zaznacz ten styl na karcie More Styles w oknie Dialog Properties. Jeżeli tego nie zrobisz, musisz wywołać funkcję Show- Window zaraz po wywołaniu CreateDialog: hDlgModeless = CreateDialog ( . . . ) ; ShowWindow (hOlgModeless, SWţSHOW) ; Jeœli nie dołšczysz stylu WS VISIBLE ani nie wywołasz ShowWindow, niemodal- ł.;., . ne okno dialogowe nie zostanie wyœwietlone. Programiœci znajšcy się tylko na modalnych oknach dialogowych często o tym zapominajš i napotykajš problemy przy pierwszej próbie utworzenia niemodalnego okna dialogowego. Po trzecie, w przeciwieństwie do modalnych i okien komunikatów, niemodalne okna dialogowe przechodzš przez kolejkę komunikatów programu. Trzeba jš zmo- dyfikować, aby komunikaty docierały do procedury okna dialogowego. Oto spo- sób: gdy używasz funkcji CreateDialog do utworzenia niemodalnego okna dialo- gowego, powinieneœ zachować w zmiennej globalnej (na przykład hDlgModeless) uchwyt okna dialogowego zwrócony przez to wywołanie. Zmień pętle komuni- katu tak, aby wyglšdała jak poniższa: while (GetMessage (&msg, NULL, 0, 0)) ( if (hDlgModeless == 0 ţţ !IsDialogMessage (hDlgModeless, &msg)) TranslateMessage (&msg) ; DispatchMessage (&msg) ; Jeżeli komunikat jest przeznaczony dla niemodalnego okna dialogowego, to funk- cja IsDialogMessage wysyła go do procedury okna dialogowego i zwraca wartoœć TRUE (niezerowš). W przeciwnym wypadku zwraca wartoœć FALSE (0). Funkcje TranslateMessage i DispatchMessage powinny być wywoływane tylko wtedy, gdy 476 Częœć I: Podstawy hDlgModeless wynosi 0 lub gdy komunikat nie jest przeznaczony dla okna dialo- gowego. Jeœli używasz klawiszy skrótu w oknie programu, pętla komunikatu będzie wyglšdać następujšco: while (GetMessage (&msg, NULL, 0. 0)) ( if (hDlgModeless == 0 ţţ !IsOialogMessage (hDlgModeless, &msg)) t if (!TranslateAccelerator (hwnd, hAccel, &msg)) ( TranslateMessage (&msg) ; DispatchMessage (&msg) ; Ponieważ zmienne globalne sš inicjowane na 0, również zmienna hDIgModeless będzie równa 0 do momentu utworzenia okna dialogowego, co gwarantuje, że funkcja IsDialogMessge nie zostanie wywołana z niewłaœciwym uchwytem okna. Musisz uwzględnić pewne, opisane poniżej, niebezpieczeństwa zwišzane z nisz- czeniem niemodalnego okna dialogowego. Zmienna hDlgModeless może być używana także przez inne częœci programu jako test istnienia niemodalnego okna dialogowego. Inne okna w programie mogłyby na przykład wysyłać komunikaty do okna dialogowego tylko wtedy, gdy zmien- na hDlgModeless miałaby wartoœć innš niż 0. Wreszcie po czwarte, w celu zamknięcia niemodalnego okna dialogowego stosuj funkcję DestroyWindow zamiast EndDialog, a wywołujšc DestroyDialog ustaw zmien- nš globalnš hDIgModeless na NULL. Użytkownik zamyka zwykle niemodalne okno dialogowe, wybierajšc opcję Za- mkruj z menu systemowego. Mimo że opcja ta jest włšczona, procedura okna dia- logowego wewnštrz Windows nie przetwarza komunikatu WM CLOSE. Musisz zrobić to sam w procedurze okna dialogowego: case WM CLOSE : DestroyWindow (hOlg) HDlgModeless = NULL break ; Zwróć uwagę na różnicę między ńastępujšcymi dwoma uchwytami okien: hDlg funkcji DestroyWindow jest parametrem przekazywanym do procedury okna dia- logowego, a DlgModeless jest zmiennš globalnš zwracanš przez funkcję Create- Dialog - wartoœć tej zmiennej sprawdzasz w pętli komunikatu. Możesz także pozwolić użytkownikowi na zamykanie niemodalnego okna dia- logowego za pomocš przycisków poleceń. Użyj takiej samej logiki jak dla komu- rukatu WMţCLOSE. Wszystkie informacje, które okno dialogowe musi "zwró- cić" do swojego okna nadrzędnego, mogš być przechowane w zmiennych glo- balnych. Jeœli wolisz nie stosować zmiennych globalnych, możesz utworzyć nie- modalne okno dialogowe za pomocš funkcji CreateDialogParam i przekazać je do wskaŸruka struktury, co opisano wczeœniej. Rozdział 11: Okna dialogowe Program COLORS w nowej wersji Program COLORSI, opisany w rozdziale 9, tworzył dziewięć okien potomnych, aby wyœwietlić trzy paski przewijania i szeœć elementów tekstowych. Wtedy był on najbardziej złożonym programem, który utworzyliœmy. Przekonwertowanie programu COLORS1 tak, aby korzystał z niemodalnych okien dialogowych, po- woduje, że staje się on, a w szczególnoœci jego funkcja 4VndProc, niewiarygodnie prosty. Nowa wersja tego programu COLORS2 pokazana jest na rysunku 11-7. COLORS2.C /* COLORS2.C - Wersja z niemodalnymi oknami dialo9owymi (c) Charles Petzold, 1998 */ ţpinclude LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; BOOL CALLBACK ColorScrDlg (HWND, UINT, WPARAM, LPARAM) ; HWND hDlgModeless : int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) ( = TEXT ("Colors2") ; static TCHAR szAppNameCJ HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CS VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = CreateSolidBrush (OL) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) ( MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MBţICONERROR) ; return 0 ; hwnd = CreateWindow (szAppName, TEXT ("Color Scroll"), WS_OVERLAPPEDWINDOW ţ WS_CLIPCHILDREN, CW USEDEFAULT, CW USEDEFAULT, CW USEDEFAULT, CW USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; Częœć i: Podstawy (cišg dalszy ze strony 477) UpdateWindow (hwnd) ; hDlgModeless = CreateDialog (hInstance, TEXT ("ColorScrDlg"), hwnd, ColorScrDlg) ; while (GetMessage (&msg, NULL, 0, 0)) ( if (hOlgModeless = 0 ţţ !IsDialogMessage (hDlgModeless, &msg)) ( TranslateMessage (&msg) ; DispatchMessage (&msg) ; 1 return msg.wParam ; LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ( switch (message) ( case WM_DESTROY : Delete0bject ((HGDIOBJ) SetClassLong (hwnd, GCL_HBRBACKGROUND, (LONG) GetStockObject (WHITE BRUSH))) ; PostOuitMessage (0) ; return 0 ; return DefWindowProc (hwnd, message, wParam, lParam) ; ) BOOL CALLBACK ColorScrDlg (HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam) ( static int iColor[3] ; HWND hwndParent, hCtrl ; int iCtrlID, iIndex ; switch (message) ( case WMţINITDIALOG : for (iCtrlID = 10 ; iCtrlID < 13 ; iCtrlID++) ( hCtrl = GetDlgItem (hDlg, iCtrlID) ; SetScrollRange (hCtrl, SB_CTL, 0, 255, FALSE) ; SetScrollPos (hCtrl, SB CTL, 0, FALSE) ; ) return TRUE ; case WM_USCROLL : hCtrl = (HWND) lParam ; iCtrlID = GetWindowLong (hCtrl, GWLţID) ; iIndex = iCtrlID - 10 ; hwndParent = GetParent (hDlg) ; switch (LOWORD (wParam)) ( case SBţPAGEDOWN : Rozdział 11: Okna dialogowe iColorCiIndex7 += 15 ; // nie powiodlo się case SB_LINEDOWN : iColorCiIndex7 = min (255, iColor[iIndex7 + 1) ; break ; case SB_PAGEUP : iColor[iIndex7 -= 15 ; // nie powiodlo się case SB_LINEUP : iColorCiIndex7 = max (0, iColorCiIndex7 - 1) ; break ; case SB_TOP : iColorCiIndex7 = 0 ; break ; case SB_BOTTOM : iColor[iIndex] = 255 ; break ; case SB_THUMBPOSITION : case SB_THUMBTRACK : iColor[iIndex7 = HIWORD (wParam) ; break ; default : return FALSE ; iColor[iIndex7, TRUE) ; SetScrollPos (hCtrl, SB CTL, SetDlgItemInt (hDlg, iCtrlID + 3, iColorCiIndex7, FALSE) ; Delete0bject ((HGDIOBJ) SetClassLong (hwndParent, GCL HBRBACKGROUND, (LONG) CreateSolidBrush ( RGB (iColorC07, iColorCl7, iColorC27)))) ; InvalidateRect (hwndParent, NULL, TRUE) ; return TRUE ; ` l.. :. ) return FALSE ; ) COLORS2 . RC ( fragrţenty ) //Microsoft Developer Studio generated resource script. 4kinclude "resource.h" ţţinclude "afxres.h" /////////////////////////////////////////////////////////////////////////// // Dialo9 COLORSCRDLG DIALOG DISCARDABLE 16, 16 120, 141 STYLE DS_MODALFRAME ţ WSţPOPUP ţ WS VISIBLE ţ WSţCAPTION CAPTION "Color Scroll Scrollbars" FONT 8, "MS Sans Serif" BEGIN CTEXT "&Red",IDC_STATIC,8,8,24 8,NOT WS GROUP SCROLLBAR 10,8,20 24,100 SBS VERT ţ WS TABSTOP CTEXT "0",13,8,124,24,8,N0T WS_GROUP CTEXT "&Green",IDCţSTATIC,48,8,24,8,N0T WSţGROUP ;:,.; SCROLLBAR 11,48,20,24,100,S8S_VERT ţ WS TABSTOP CTEXT "0",14,48,124,24,8,N0T WSţGROUP ' CTEXT "&Blue",IDC_STATIC,89,8,24,8,N0T WSţGROUP SCROLLBAR 12,89,20,24,100,S8S VERT ţ WS TABSTOP vi; i ` . 480 Częœć Iţ Podstawy (cišg dalszy ze strony 479) CTEXT "0",15,89,124,24,8,N0T WS GROUP END RESOURCE.H (fragmenty) // Microsoft Developer Studio generated include file. // Used by Colors2.rc lldefine IDCţSTATIC Rysunek 11-7. Program COLORS2 Mimo że oryginalny program COLORS1 wyœwietlał paski przewijania, których rozmiar opierał się na rozmiarze okna, nowa wersja zachowuje stałš ich długoœć wewnštrz niemodalnego okna dialogowego, co widać na rysunku 11-8. Tworzšc szablon okna dialogowego, użyj identyfikatorów 10, 11 i 12 dla pasków przewijania oraz 13, 14 i 15 dla trzech statycznych pól tekstowych wyœwietlajšcych bieżšce wartoœci pasków przewijania. Nadaj każdemu paskowi styl WS TABSTOP, ale usuń styl WS GROUP ze wszystkich szeœciu statycznych pól tekstowych. Rysunek 11-8. Ekran programu COLORS2 Niemodahie okno dialogowe tworzone jest w programie COLORS2 w funkcji WinMain, występujšcej po wywołaniu ShowWindow dla głównego okna progra- mu. Zwróć uwagę, że na styl okna głównego składa się styl WS CLIPCHILDREN, który pozwala programowi przemalować główne okno bez czyszczenia okna dia- logowego. Uchwyt okna dialogowego zwracany przez funkcję CreateDialog jest zachowywany w zmiennej globalnej hDlgModeless, sprawdzanej w pętli komunikatu, co oma- wiałem wczeœniej. W tym programie nie ma jednak potrzeby przechowywania uchwytu w zmiennej globahiej ani sprawdzania jej wartoœci przed wywołaniem IsDialogMessage. Pętla komunikatu mogłaby być napisana w następujšcy sposób: Rozdział 11: Okna dialogowe 481 while (GetMessage (&msg, NULL, 0, 0)) ( if (!IsDialogMessage (hDlgModeless, &msg)) ( TranslateMessage (&msg) ; DispatchMessage (&msg) ; Ponieważ okno dialogowe jest tworzone, zanim program wejdzie do pętli komu- nikatu i nie jest niszczone przed zamknięciem programu, wartoœć zmiennej hDlg- Modeless zawsze będzie właœciwa. Przedstawionš logikę dołšczyłem na wypadek, gdybyœ chciał dodać do procedury okna dialogowego kod niszczšcy okno dialo- gowe: case WMţCLOSE : DestroyWindow (hDlg) : HDlgModeless = NULL ; break ; W oryginalnym programie COLORS1 funkcja SetWindowText ustawiała wartoœci trzech etykiet numerycznych po przekonwertowaniu liczb całkowitych na tekst za pomocš funkcji wsprintf. Kod wyglšdał następujšco: wsprintf (szBuffer, TEXT ("%i"), color[i]) : SetWindowText (hwndValue[i], szBuffer) ; Wartoœć i była numerem identyfikatora aktualnie przetwarzanego paska przewi- jania, a hwndValue - tablicš zawierajšcš uchwyty okien dla trzech statycznych tek- stowych okien potomnych, które wyœwietlajš wartoœci numeryczne kolorów. Nowa wersja używa funkcji SetDlgItemInt, aby ustawić wszystkie pola wszelkich okien potomnych na liczby: SetDlgItemInt (hDlg, iCtrlID + 3, color CiCtrIID], FALSE) ; Mimo że funkcja SetDIgItemlnt i towarzyszšca jej funkcja GetDlgItemlnt sš naj- częœciej używane z kontrolkami edycji, można je także zastosować do ustawienia pól tekstowych innych kontrolek, takich jak statyczne kontrolki tekstu. Zmienna iCtrlID jest numerem identyfikatora paska przewijania - zwiększenie tego numeru o trzy konwertuje go na identyfikator odpowiedniej etykiety numerycznej. Trze- cim argumentem jest wartoœć koloru. Czwarty argument wskazuje, czy wartoœć trzeciego argumentu ma być traktowana jako signed (jeœli czwarty argument równy jest TRUE), czy jako unsigned (jeżeli czwarty argument równy jest FALSE). W tym programie wartoœci mieszczš się w zakresie od 0 do 255, więc czwarty argument nie ma znaczenia. Konwertujšc COLORS1 na COLORS2, przekazywaliœmy coraz więcej zadań sys- temowi Windows. Wczeœniejsza wersja wywoływała funkcję CreateWindow 10 razy, nowa wywohzje raz funkcję CreateWindow i raz funkcję CreateDialog. Jeœli myœlisz jednak, że zredukowaliœmy wywołania funkcji CreateWindow do minimum, mo- żesz zdziwić się oglšdajšc następny program. Program HEXCALC: okno zwykłe czy dialogowe? Program HEXCALC, pokazany na rysunku 11-9, może być przykładem lenistwa w programowaniu. Nie wywołuje ani razu funkcji CreateWindow, nie przetwarza 482 Częœć I: Podstawy komunikatów WM-PAINT, nie pobiera kontekstu urzšdzenia i nie przetwarza komunikatów myszy, a mimo to udaje mu się utworzyć dziesięciofunkcyjny kal- kulator z pełnym interfejsem klawiatury i myszy w niespelna 150 liniach kodu. Kalkulator pokazany jest na rysunku 11-10. HEXCALC.C /* HEXCALC.C - Kalkulator szesnastkowy (c) Charles Petzold, 1998 */ ţţinclude LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) ( static TCHAR szAppNameC] = TEXT ("HexCalc") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CSţVREDRAW; LRI wndclass.lpfnWndProc = WndProc ; ( wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = DLGWINDOWEXTRA ; // Uwaga! wndclass.hInstance = hInstance ; wndclass.hIcon =.LoadIcon (hInstance, szAppName) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) (COLOR_BTNFACE + 1) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) ( MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MB ICONERROR) ; return 0 ; l hwnd = CreateDialog (hInstance, szAppName, 0, NULL) ; ShowWindow (hwnd, iCmdShow) ; while (GetMessage (&msg, NULL, 0, 0)) ( TranslateMessage (&msg) ; DispatchMessage (&msg) ; ) return msg.wParam ; 7 void ShowNumber (HWND hwnd, UINT iNumber) ( Rozdział 11: Okna dialogowe 483 TCHAR szBufferC207 ; wsprintf (szBuffer, TEXT ("%X"), iNumber) ; SetDlgItemText (hwnd, VKţESCAPE, szBuffer) ; DWORD CalcIt (UINT iFirstNum, int i0peration, UINT iNum) switch (i0peration) 'I case '- . return iNum ; case '+': return iFirstNum + iNum ; ' case '- . return iFirstNum - iNum ; case '*': return iFirstNum * iNum ; case '&': return iFirstNum & iNum ; case 'ţ': return iFirstNum ţ iNum ; case '^': return iFirstNum ^ iNum ; case '<': return iFirstNum Ž iNum ; case '>': return iFirstNum Ż iNum ; 4 case '/': return iNum ? iFirstNum / iNum: MAXDWORD ; , case '%': return iNum ? iFirstNum % iNum: MAXDWORD ; j default : return 0 ; } } LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) i'; static BOOL bNewNumber = TRUE ; static int i0peration = - , l '! I static UINT iNumber, iFirstNum ; ,. J HWND hButton ; , switch (message) f case WM_KEYDOWN: // lewa strzałka --> backspace if (wParam != VK LEFT) break ; wParam = VK BACK ; // ten przypadek zostawiamy nieopracowany case WM_CHAR: if ((wParam = (WPARAM) CharUpper ((TCHAR *) wParam)) = VK RETURN) wParam = - , if (hButton = GetDlgItem (hwnd, wParam)) ( SendMessage (hButton, BM-SETSTATE, l, 0) ; Sleep (100) ; SendMessage (hButton, BM SETSTATE, 0, 0) ; } else MessageBeep (0) ; break ; } y // ten przypadek zostawiamy nieopracowan case WM_COMMAND: SetFocus (hwnd) ; Częœć I' Podstawy (cišg dalszy ze strony 483) if (LOWORD (wparam) == VK_BACK) // backspace ShowNumber (hwnd, iNumber /= 16) ; else if (LOWORD (wparam) = VKţESCAPE) // escape ShowNumber (hwnd, iNumber = 0) ; else if (isxdigit (LOWORD (wparam))) // cyfra szesnastkowa ( if (bNewNumber) ( iFirstNum = iNumber ; iNumber = 0 ; ) bNewNumber = FALSE ; if (iNumber <= MAXDWORD Ż 4) ShowNumber (hwnd, iNumber = 16 * iNumber + wparam - (isdigit (wparam) ? '0': 'A' - 10)) ; else MessageBeep (0) ; 1 else // działanie ( if (!bNewNumber) ShowNumber (hwnd. iNumber = CalcIt (iFirstNum, i0peration, iNumber)) ; bNewNumber = TRUE ; i0peration = LOWORD (wparam) ; ) return 0 ; case WM DESTROY: PostOuitMessage (0) ; return 0 ; ) return DefWindowproc (hwnd, message, wparam, lParam) ; ? HEXCALC.RC (fragmenty) } //Microsoft Developer Studio generated resource script. Hf Ilinclude "resource.h" Itinclude "afxres.h" /////////////////////////////////////////////////////////////////////////// // Icon HEXCALC ICON DISCARDABLE "HexCalc.ico" /////////////////////////////////////////////////////////////////////////// Ilinclude "hexcalc.dlg" Rozdział 11: Okna dialogowe 485 HEXCALC.DLG /* */ HEXCALC.DLG dialog script HexCalc DIALOG -1, -1, 102, 122 STYLE WS OVERLAPPED ţ WS CAPTION ţ WS SYSMENU ţ WS MINIMIZEBOX CLASS "HexCalc" I.' CAPTION "Hex Calculator" Ii'. PUSHBUTTON "D", 68, 8, 24, 14, 14 PUSHBUTTON "A", 65, 8, 40, 14, 14 PUSHBUTTON "7", 55, 8. 56, 14, 14 PUSHBUTTON "4", 52, 8, 72. 14, 14 PUSHBUTTON "1", 49, 8, 88, 14, 14 PUSHBUTTON "0", 48, 8, 104, 14, 14 `' PUSHBUTTON "0", 27. 26, 4, 50, 14 PUSHBUTTON "E", 69. 26, 24, 14, 14 PUSHBUTTON "B", 66, 26, 40, 14, 14 PUSHBUTTON "8", 56, 26, 56, 14, 14 PUSHBUTTON "5", 53, 26, 72, 14, 14 PUSHBUTTON "2", 50, 26, 88, 14, 14 PUSHBUTTON "Back", 8, 26, 104, 32, 14 PUSHBUTTON "C", 67, 44, 40, 14, 14 PUSHBUTTON "F", 70, 44, 24, 14, 14 PUSHBUTTON "9", 57, 44, 56, 14, 14 PUSHBUTTON "6", 54, 44, 72, 14, 14 PUSHBUTTON "3", 51, 44, 88, 14, 14 PUSHBUTTON "+", 43, 62, 24, 14, 14 PUSHBUTTON "- 45, 62, 40, 14, 14 PUSHBUTTON "*", 42, 62, 56, 14, 14 PUSHBUTTON "/", 4J, 62, 72, 14, 14 PUSHBUTTON "%", 37, 62, 88, 14, 14 PUSHBUTTON "Equals", 61, 62, 104, 32, 14 PUSHBUTTON "&&". 38, 80, 24, 14, 14 PUSHBUTTON "ţ", 124, 80, 40, 14, 14 PUSHBUTTON "^" 94, 80, 56, 14, 14 PUSHBUTTON "<", 60, 80, 72, 14, 14 PUSHBUTTON ">", 62, 80, 88, 14, 14 ; HEXCALC.ICO F ....... .....: Rysunek 11-9. Program HEXCALC 486 Częœć I: Podstawy Rysunek 11-10. Ekran programu HEXCALC HEXCALC jest zwykłym kalkulatorem o stałej notacji. Do operacji używa notacji C. Działa z 32-bitowymi liczbami typu unsigned int i wykonuje dodawanie, odej- mowanie, mnożenie, dzielenie z resztš, bitowe operacje AND, OR i XOR oraz przesunięcia bitów w lewo i w prawo. Dzielenie przez 0 powoduje ustawienie wyniku na FFFFFFFF. W programie HEXCALC możesz używać myszy lub klawiatury. Rozpoczynasz od "wyklikania" lub wpisania pierwszej liczby (do oœmiu cyfr szesnastkowych), potem podajesz typ operacji i drugš liczbę. Następnie możesz wyœwietlić wynik klikajšc przycisk Equals lub naciskajšc klawisz [=] albo [Enter]. Chcšc poprawić wpisanš liczbę, użyj, przycisku Back lub klawisza [Backspace] albo [E-]. Kliknij pole "wyœwietlacza lub naciœnij klawisz [Esc], aby usunšć aktualnie wprowa- dzonš liczbę. Program HEXCALC jest nietypowy pod tym względem, że okno wyœwietlone na ekranie wyglšda jak hybryda zwykłego i niemodalnego okna dialogowego. Z jed- nej strony, wszystkie komunikaty programu HEXCALC sš przetwarzane w tunkcji nazwanej WndProc, która wyglšda jak procedura zwykłego okna. Funkcja ta zwra- ca wartoœć typu long, przetwarza komunikat WMţDESTROY i wywołuje proce- durę DefYVindowProc zupełnie jak procedura zwykłego okna. Z drugiej strony, okno jest tworzone w procedurze WinMain wywołaniem CreateDialog, które używa sza- blonu okna dialogowego zdefiniowanego w pliku HEXCALC.DLG. Jakie okno stosuje więc program HEXCALC: zwykłe czy dialogowe niemodalne? Najprostsza odpowiedŸ brzmi: okno dialogowe także jest zwykłym oknem. Win- dows używa zazwyczaj swojej wewnętrznej procedury do przetwarzania komu- nikatów wysyłanych do okna dialogowego. Następnie system wysyła te komu- nikaty do procedury okna dialogowego wewnštrz programu, która tworzy okna dialogowe. W programie HEXCALC zmuszamy Windows do użycia szablonu okna dialogowego, ale komunikaty wysyłane do tego okna przetwarzamy sami. Szablon okna dialogowego wymaga jednak czegoœ, czego nie można dodać w edy- torze okien dialogowych w Developer Studio. Z tego powodu szablon okna dia- logowego umieszczony jest w pliku HEXCALC.DLG, który, jak być może się Rozdział 11: Okna dialogowe domyœlasz (i masz rację), został wpisany ręcznie. Do każdego projektu można dodać plik tekstowy wybierajšc opcję New z menu File, następnie kartę Files i za- znaczajšc Text File z listy typów plików. Taki plik, zawierajšcy dodatkowe defi- nicje zasobów, musi zostać dołšczony w skrypcie zasobów. Z menu View, wy- bierz Resource Includes. Spowoduje to wyœwietlenie okna dialogowego. W polu edycji Compile-time, wpisz: ilinclude "hexcalc.dlg" Wiersz ten zostanie wstawiony do skryptu zasobów HEXCALC.RC, co widać było na rysunku. Bliższe przyjrzenie się szablonowi okna dialogowego w pliku HEXCALC.DLG pomaga zrozumieć, jak program HEXCALC używa własnej procedury okna z oknem dialogowym. Poczštek szablonu okna dialogowego wyglšda następujš- co: HexCalc DIALOG -1, -l, 102, 122 STYLE WS OVERLAPPED ţ WS CAPTION ţ WS SYSMENU ţ WS MINIMIZEBOX CLASS "HexCalc" CAPTION "Hex Calculator" Zwróć uwagę na identyfikatory, takie jak WS OVERLAPPED i WS MINIMIZE- BOX, których normalnie moglibyœmy użyć do utworzenia zwykłego okna za po- mocš wywołania CreateWindow. Instrukcja CLASS stanowi podstawowš różnicę między tym oknem dialogowym a oknami, które tworzyliœmy do tej pory (wła- œnie tego nie pozwala nam wpisać edytor okien dialogowych w Developer Stu- dio). Gdybyœmy ominęli tę instrukcję w szablonie okna dialogowego, Windows zarejestrowałby klasę okna dialogowego i użył własnej procedury okna do prze- tworzenia komunikatów wysyłanych do okna dialogowego. Instrukcja CLASS mówi systemowi Windows, aby wysyłał komunikaty gdzie indziej - w naszym przypadku do procedury okna okreœlonej w klasie okna HexCalc. Klasa okna HexCalc jest rejestrowana w funkcji WinMain programu HEXCALC, podobnie jak klasa zwykłego okna. Zwróć jednak uwagę na istotnš różnicę: pole cbWndExtra struktury WNDCLASS jest ustawiane na DLGWINDOWEXTRA. Jest to ważne w przypadku procedur okien dialogowych, które same się rejestrujš. Po zarejestrowaniu klasy okna funkcja WinMain wywołuje funkcję CreateDialog: hwnd = CreateDialog (hInstance, szAppName, 0, NULL) ; Drugim argumentem (cišg znaków "HexCalc") jest nazwa szablonu okna dialo- gowego. Trzeci argument, zwykle będšcy uchwytem okna nadrzędnego, jest usta- wiony na 0, ponieważ nasze okno nie ma okna nadrzędnego. Ostatni argument, zwykle adres procedury okna dialogowego, nie jest potrzebny, ponieważ Win- dows nie będzie przetwarzać komunikatów i z tego powodu nie może ich wy- słać do procedury okna dialogowego. Takie wywołanie CreateDialog w połšczeniu z szablonem okna dialogowego jest tłumaczone przez Windows na podobne do poniższego wywołanie CreateWindow: hwnd = CreateWindow (TEXT ("HexCalc") TEXT ("Hex Calculator"), WS_OVERLAPPED ţ WS_CAPTION ţ WSţSYSMENU ţ WS MINIMIZEBOX, CW_USEDEFAULT, CW_USEDEFAULT, 102 * 4 / cxChar, 122 * 8 / cyChar, NULL, NULL, hInstance, NULL> : 488 Częœó I: Podstawy w którym zmienne cxChar i cyChar sš szerokoœciš i wysokoœciš czcionki okna dia- logowego. Zyskujemy znaczne korzyœci pozwalajšc Windows wykonać takie wywołanie CreateWindow: system ten nie poprzestanie na utworzeniu jednego okna, ale wy- woła funkcję CreateWindow także dla wszystkich 29 kontrolek okien potomnych przycisków poleceń, które zdefiniowane sš w szablonie okna dialogowego. Wszystkie te kontrolki wysyłajš komunikaty WS COMMAND do procedury okna dla okna nadrzędnego, którš jest właœnie WndProc. Jest to doskonała technika tworzenia okien, które majš zawierać kolekcję okien potomnych. Oto inny sposób ograniczenia kodu programu HEXCALC do minimum: możesz zauważyć, że program HEXCALC nie zawiera pliku nagłówkowego, który zwy- kle jest wymagany do zdefiniowania wszystkich kontrolek okien potomnych w szablonie okna dialogowego. Możemy obyć się bez tego pliku, ponieważ nu- mery identyfikatorów wszystkich kontrolek przycisków poleceń ustawiane sš na kod ASCII tekstu pojawiajšcego się w kontrolce. Oznacza to, że procedura Wnd- Proc może traktować komunikaty WM COMMAND i WM CHAR w podobny sposób. W obu przypadkach mniej znaczšce słowo parametru wParam jest ko- dem ASCII tekstu przycisku. Oczywiœcie nadal należy przetworzyć komunikaty klawiatury. Procedura Wnd- Proc przechwytuje komunikaty WMţKEYDOWN, aby przetłumaczyć klawisz [ţ] na [Backspace]. Podczas przetwarzania komunikatów WM CHAR procedura WndProc konwertuje, jeœli potrzeba, kod znaku na kod wielkiej litery i kod klawi- sza [Enter] na kod klawisza [Esc]. Wywołanie GetDlgltem sprawdza poprawnoœć komunikatu WM CHAR. Jeœli funkcja GetDlgltem zwróci 0, będzie to oznaczało, że znak klawiatury nie jest jed- nym z numerów identyfikatorów zdefiniowanych w szablonie okna dialogowe- go. Jeœli jednak znak jest ustawiony na jeden z identyfikatorów, odwracane będš kolory odpowiedniego przycisku polecenia przez wysłanie do niego pary komu- nikatów BM SETSTATE: if (hButton = GetDlgItem (hwnd, wParam)) ( SendMessage (hButton, BMţSETSTATE, 1, 0) : Sleep (100) ; SendMessage (hButton, BM SETSTATE, 0, 0) ; Dzięki temu uzyskujemy w programie funkcjonalny interfejs klawiatury przy minimalnym wysiłku. Funkcja Sleep zatrzymuje wykonywanie programu na 100 milisekund. Zapobiega dzięki temu klikaniu przycisków tak szybko, że pozosta- je to niezauważone. Gdy procedura WndProc przetwarza komunikaty WMţCOMMAND, zawsze usta- wia fokus wejœciowy na okno nadrzędne: case WM_COMMAND : SetFocus (hwnd) : Inaczej fokus byłby za każdym razem ustawiany na przycisk polecenia, który został kliknięty myszš. Rozdział 11: Okna dialogowe 489 Standardowe okna dialogowe Jednym z podstawowych celów pierwszej wersji 4Vindows było promowanie stan- dardowego interfejsu użytkownika. Dosyć szybko dokonała się standaryzacja typowych elementów menu. Prawie wszyscy producenci oprogramowania za- adaptowali sekwencję [Alt+P(lik)+O(twórz)] do otwierania plików. Jednak okna dialogowe do tego służšce były często niepodobne. Poczšwszy od wersji 3.1, udostępniono rozwišzanie tego problemu. Rozszerze- nie to nazwane zostało bibliotekš standardowych okien dialogowych (ang. dialog boxes library). Biblioteka ta składa się z kilku funkcji, które wywołujš standardo- we okna dialogowe do otwierania i zapisywania plików, wyszukiwania i zastę- powania, wybierania czcionek (okna te zademonstruję w tym rozdziale) oraz dru- kowania (które przedstawię w rozdziale 13). Aby skorzystać z tych funkcji, inicjuje się pola struktury i przesyła wskaŸnik do struktury do funkcji w bibliotece standardowych okien dialogowych. Funkcja ta tworzy i wyœwietla okno dialogowe. Gdy użytkownik zamknie to okno, wywo- łana funkcja zwraca kontrolę do programu - informacje uzyskasz ze struktury, którš przekazałeœ do tej funkcji. Do każdego kodu Ÿródłowego w języku C, który korzysta z bliblioteki standar- dowych okien dialogowych, musisz dołšczyć plik nagłówkowy COMMDLG.H. Standardowe okna dialogowe udokumentowane sš w pliku /Platform SDK/LIser Interface Services/Llser Input/Common Dialog Box Library. Nowa wersja programu POPPAD Gdy w rozdziale 10 dodawaliœmy menu do programu POPPAD, pozostawiliœmy kilka niezaimplementowanych opcji. Teraz możemy do tego programu dodać lo- gikę zwišzanš z otwieraniem, wczytywaniem i zapisywaniem edytowanych pli- ków. Dołšczymy także możliwoœć wyboru czcionki oraz wyszukiwania i zastę- owania tekstu. p Pliki składajšce się na program POPPAD3 pokazane sš na rysunku 11-11. POPPAD.C /* POPPAD.C - Prosty notatnik (c) Charles Petzold, 1998 */ 4nclude ilinclude 4iinclude "resource.h" 4ldefine EDITID 1 4ldefine UNTITLED TEXT ("(untitled)") LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; BOOL CALLBACK AboutDlgProc (HWND, UINT. WPARAM, LPARAM) ; 490 Częœć I: Podstawy (cišg dalszy ze strony 489) // Funkcje w POPFILE.C void PopFileInitialize (HWND) ; BOOL PopFileOpenDlg (HWND, PTSTR, PTSTR) ; BOOL PopFileSaveDlg (HWND, PTSTR, PTSTR) ; BOOL PopFileRead (HWND, PTSTR) ; BOOL PopFileWrite (HWND, PTSTR) ; // Funkcje w POPFIND.C HWND PopFindFindOlg (HWND) ; HWND PopFindReplaceDlg (HWND) ; BOOL PopFindFindText (HWND, int *, LPFINDREPLACE) ; BOOL PopFindReplaceText (HWND, int * LPFINDREPLACE) ; BOOL PopFindNextText (HWND, int *) ; BOOL PopFindValidFind (void) ; // Funkcje w POPFONT.C void PopFontInitialize (HWND) ; BOOL PopFontChooseFont (HWND) ; void PopFontSetFont (HWND) ; void PopFontDeinitialize (void) ; // Funkcje w POPPRNT.C BOOL PopPrntPrintFile (HINSTANCE, HWND, HWND, PTSTR) ; // Zmienne globalne static HWND hDlgModeless ; static TCHAR szAppName[] = TEXT ("PopPad") ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) ( MSG msg ; HWND hwnd ; HACCEL hAccel ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CSţVREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (hInstance, szAppName) ; wndclass.hCursor = LoadCursor (NULL, IDC ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITEţBRUSH) ; wndclass.lpszMenuName = szAppName ; wndclass.lpszClassName = szAppName ; if (!Re9isterClass (&wndclass)) ( MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MB ICONERROR) ; Rozdział 11: Okna dialogowe return 0 ; } hwnd = CreateWindow (szAppName, NULL, WS_OVERLAPPEDWINDOW, , CW_USEDEFAULT, CW USEDEFAULT, CW_USEDEFAULT, CW USEDEFAULT, NULL, NULL, hInstance, szCmdLine) ; i ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; hAccel = LoadAccelerators (hInstance, szAppName) ; while (GetMessage (&msg, NULL, 0, 0)) ( if (hDlgModeless == NULL ţţ !IsDialogMessage (hDlgModeless, &msg)) if (!TranslateAccelerator (hwnd, hAccel, &msg)) TranslateMessage (&msg) ; DispatchMessage (&msg) ; } ţ...:, } return msg.wParam ; } ţ void DoCaption (HWND hwnd, TCHAR * szTitleName) TCHAR szCaptionC64 + MAXţPATH] ; wsprintf (szCaption, TEXT ("ţs - %s")-, szAppName, szTitleNameCO] ? szTitleName : UNTITLED) ; SetWindowText (hwnd, szCaption) ; void OkMessage (HWND hwnd. TCHAR * szMessage, TCHAR * szTitleName) ( TCHAR szBufferC64 + MAX PATH7 ; wsprintf (szBuffer, szMessage, szTitleNameCO] ? szTitleName : UNTITLED) ; ţ. . MessageBox (hwnd, szBuffer, szAppName, MB OK ţ MB ICONEXCLAMATION) ; short AskAboutSave (HWND hwnd, TCHAR * szTitleName) ( TCHAR szBufferCó4 + MAX PATH] ; int iReturn ; wsprintf (szBuffer, TEXT ("Save current changes in ţs?"), szTitleNameCO7 ? szTitleName : UNTITLED) ; iReturn = MessageBox (hwnd, szBuffer, szAppName, MB YESNOCANCEL ţ MBţICONOUESTION) ; ` 492 Częœć I: Podstawy (cišg dalszy ze strony 491) if (iReturn = IDYES) if (!SendMessage (hwnd, WM COMMAND, IDMţFILEţSAVE, 0)) iReturn = IDCANCEL ; return iReturn ; LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ( static BOOL bNeedSave = FALSE ; static HINSTANCE hInst ; static HWND hwndEdit ; static int i0ffset ; static TCHAR szFileNameCMAX_PATH], szTitleName[MAXţPATH] ; static UINT messageFindReplace ; int iSelBeg, iSelEnd, iEnable ; LPFINDREPLACE pfr ; switch (message) ( case WM_CREATE: hInst = ((LPCREATESTRUCT) lParam) -> hInstance ; // Utwórz kontrolkę edycji okna potomnego hwndEdit = CreateWindow (TEXT ("edit"), NULL, WS_CHILD ţ WS_VISIBLE ( WS HSCROLL ţ WS USCROLL WS_BORDER ţ ES_LEFT ţ ES_MULTILINE ESţNOHIDESEL ţ ES AUTOHSCROLL ţ ES AUTOVSCROLL, 0, 0, 0, 0, hwnd, (HMENU) EDITID, hInst, NULL) ; SendMessage (hwndEdit, EMţLIMITTEXT, 32000, OL) ; // Zainicjuj typowe ustawienia okna dialogowego PopFileInitialize (hwnd) ; PopFontInitialize (hwndEdit) ; messageFindReplace = RegisterWindowMessage (FINDMSGSTRING) ; DoCaption (hwnd, szTitleName) ; return 0 ; case WM_SETFOCUS: SetFocus (hwndEdit) ; return 0 ; case WM_SIZE: MoveWindow (hwndOdit, 0, 0, LOWORD (lParam), HIWORD (lParam), TRUE) ; return 0 ; case WM INITMENUPOPUP: switch (lParam) ( case 1: // menu Edit Rozdział 11: Okna dialogowe 493 // Wlšcz Undo, jeœli kontrolka edycji na to pozwala i EnableMenuItem ((HMENU> wParam, IDM_EDIT UNDO, , SendMessage (hwndEdit, EM_CANUNDO, 0, OL) ? MF ENABLED : MF GRAYED) ; ! // Wldcz Paste, jeœli w Schowku jest tekst i EnableMenuItem ((HMENU) wParam, IDM_EDIT_PASTE, I IsClipboardFormatAvailable (CF TEXT) ? i MF ENABLED : MF GRAYED) ; // Wlšcz Cut, Copy i Del, jeœli tekst jest zaznaczony SendMessage (hwndEdit, EM GETSEL, (WPARAM) &iSelBeg, (LPARAM) &iSelEnd) ; iEnable = iSelBeg != iSelEnd ? MF ENABLED : MF GRAYED ; EnableMenuItem ((HMENU) wParam, IDMţEDIT_CUT, iEnable) ; EnableMenuItem ((HMENU) wParam, IDM_EDIT_COPY, iEnable) ; EnableMenuItem ((HMENU) wParam, IDM EDIT CLEAR, iEnable) ; break ; case 2: // menu Search // Wlšcz Find, Next i Replace, jeœli niemodalne okna // dialogowe nie sš jeszcze aktywne iEnable = hDlgModeless == NULL ? MFţENABLED : MFţGRAYED ; EnableMenuItem ((HMENU) wParam, IDM_SEARCH_FIND, iEnable) ; EnableMenuItem ((HMENU) wParam, IDM_SEARCH_NEXT, iEnable) ; EnableMenuItem ((HMENU) wParam, IDM SEARCH REPLACE, iEnable) ; break ; return 0 ; case WM COMMAND: // Komunikaty od kontrolki edycji if (lParam && LOWORD (wParam) == EDITID) ( switch (HIWORD (wParam)) ( case EN_UPDATE : bNeedSave = TRUE ; return 0 ; case EN_ERRSPACE : case ENţMAXTEXT : MessageBox (hwnd, TEXT ("Edit control out of space."), szAppName, MB OK i MBţICONSTOP) ; return 0 ; ) break ; ) 4g4 Częœć I: Podstawy (cišg dalszy ze strony 493) switch (LOWORD (wParam)) t // Komunikaty od menu File case IDM_FILE_NEW: if (bNeedSave && IDCANCEL = AskAboutSave (hwnd, szTitleName)) return 0 ; SetWindowText (hwndEdit, TEXT ("\0")) ; szFileName[07 - '\0' , szTitleName[07 = '\0' DoCaption (hwnd, szTitleName) ; bNeedSave = FALSE ; return 0 ; case IDM_FILE_OPEN: if (bNeedSave && IDCANCEL = AskAboutSave (hwnd, szTitleName)) return 0 ; if (PopFileOpenDlg (hwnd, szFileName. szTitleName)) if (!PopFileRead (hwndEdit, szFileName)) ( OkMessage (hwnd. TEXT ("Could not read file %s!"). szTitleName) ; szFileName[07 - '\0' . szTitleName[07 = '\0' , ) l DoCaption (hwnd, szTitleName) ; bNeedSave = FALSE : return 0 ; case IDM_FILE_SAUE: if (szFileName[07) ( if (PopFileWrite (hwndEdit, szFileName)) ( bNeedSave = FALSE ; return 1 ; 1 else ( OkMessage (hwnd. TEXT ("Could not write file %s"), szTitleName) ; return 0 ; 1 // ten przypadek zostawiamy nieopracowany case IDM_FILE SAVE AS: if (PopFileSaveDlg (hwnd, szFileName, szTitleName)) ( DoCaption (hwnd. szTitleName) ; Rozdział 11: Okna dialogowe 495 i ţ.,ţ if (PopFileWrite (hwndEdit, szFileName)) i I ....ţ bNeedSave = FALSE ; ţ::.:-;:. return 1 : t I:::. else i OkMessage (hwnd, TEXT ("Could not write file %s"), ' ţ 1 ';: szTitleName) ; ` return 0 ; i,. ) return 0 ; case IDM_FILE_PRINT: if (!PopPrntPrintFile (hInst, hwnd, hwndEdit, szTitleName)) OkMessage (hwnd, TEXT ("Could not print file %s"), szTitleName) ; return 0 ; `.. ,. case IDM_APP_EXIT: i. k SendMessage (hwnd, WM CLOSE, 0, 0) ; return 0 ; // Komunikaty od menu Edit case IDM_EDIT_UNDO: SendMessage (hwndEdit, WM UNDO, 0, 0) ; return 0 ; case IDM_EDIT_CUT: SendMessage (hwndEdit, WM CUT, 0, 0) ; return 0 ; case IDM_EDIT COPY: SendMessage (hwndEdit, WM COPY, 0, 0) ; '' return 0 ; case IDM_EDITţPASTE: SendMessage (hwndEdit, WMţPASTE, 0, 0) ; return 0 ; case IDM_EDIT CLEAR: SendMessage (hwndEdit, WM CLEAR, 0. 0) ; return 0 ; case IDM_EDITţSELECT_ALL: SendMessage (hwndEdit, EM SETSEL, 0, -1) ; return 0 ; // Komunikaty od menu Search case IDM_SEARCH FIND: SendMessage (hwndEdit, EM_GETSEL, 0, (LPARAM) &i0ffset) ; hDlgModeless = PopFindFindDlg (hwnd) ; return 0 ; case IDM SEARCHţNEXT: 496 Częœć I: Podstawy (cišg dalszy ze strony 495) SendMessage (hwndEdit, EM GETSEL, 0, (LPARAM) &i0ffset) ; if (PopFindValidFind ()) PopFindNextText (hwndEdit, &i0ffset) ; else hDlgModeless = PopFindFindDlg (hwnd) ; return 0 ; case IDM_SEARCHţREPLACE: SendMessage (hwndEdit, EM_GETSEL, 0, (LPARAM) &i0ffset) ; hDlgModeless = PopFindReplaceDlg (hwnd) ; return 0 ; case IDM_FORMAT_FONT: if (PopFontChooseFont (hwnd)) PopFontSetFont (hwndEdit) ; return 0 ; // Komunikaty od menu Help case IDM_HELP: OkMessage (hwnd, TEXT ("Help not yet implemented!"), TEXT ("\0")) ; return 0 ; case IDM_APP_ABOUT: DialogBox (hInst, TEXT ("AboutBox"), hwnd, AboutDlgProc) ; return 0 ; 1 break ; case WM_CLOSE: if (!bNeedSave ţţ IDCANCEL != AskAboutSave (hwnd, szTitleName)) DestroyWindow (hwnd) ; return 0 ; case WM_OUERYENDSESSION : if (!bNeedSave ţţ IDCANCEL != AskAboutSave (hwnd, szTitleName)) return 1 ; return 0 ; case WM_DESTROY: PopFontDeinitialize () ; PostOuitMessage (0) ; return 0 ; default: // Przetwarzanie komunikatów "odszukaj-zastšp" if (message == messageFindReplace) ( pfr = (LPFINDREPLACE) lParam ; Rozdział 11: Okna dialogowe if (pfr->Flags & FR DIALOGTERM) hOlgModeless = NULL ; if (pfr->Flags & FR_FINDNEXT) if (!PopFindFindText (hwndEdit, &i0ffset, pfr)) OkMessage (hwnd, TEXT ("Text not found!"), TEXT ("\0")) ; ;'..; if (pfr->Flags & FR_REPLACE ţţ pfr->Flags & FRţREPLACEALL) if (!PopFindReplaceText (hwndEdit, &i0ffset, pfr)) OkMessage (hwnd, TEXT ("Text not found!"), TEXT ("\0")) ; if (pfr->Fla9s & FR_REPLACEALL) while (PopFindReplaceText (hwndEdit, &i0ffset, pfr)) ; i . .,. return 0 ; 1 break ; I:: return DefWindowProc (hwnd, message, wParam, lParam) ; i t ; BOOL CALLBACK AboutDlgProc (HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam) t switch (message) case WM_INITDIALOG: return TRUE ; case WM COMMAND: switch (LOWORD (wParam)) ( case IDOK: EndDialog (hDlg, 0) ; return TRUE ; break ; return FALSE ; POPFILE.C /* POPFILE.C - Funkcje obslugi plików notatnika */ ţţinclude ţţinclude static OPENFILENAME ofn ; void PopFileInitialize (HWND hwnd) ( static TCHAR szFilter[] = TEXT ("Text Files (*.TXT)\0*.txt\0") \ TEXT ("ASCII Files (*.ASC)\0*.asc\0") \ 498 Częœć I: Podstawy (cišg dalszy ze strony 497) TEXT ("All FileS (*.*)\0*.*\0\0") ; ofn.lStructSize = sizeof (OPENFILENAME) ; ofn.hwndOwner = hwnd ; ofn.hInstance = NULL ; ofn.lpstrFilter = szFilter ; ofn.lpstrCustomFilter,= NULL ; ofn.nMaxCustFilter = 0 ; ofn.nFilterIndex = 0 ; ofn.lpstrFile = NULL ; // Ustawiane w funkcjach Open i Close ofn.nMaxFile = MAXţPATH ; ofn.lpstrFileTitle = NULL ; // Ustawiane w funkcjach Open i Close ofn.nMaxFileTitle = MAX_PATH ; ' ofn.lpstrInitialDir = NULL ; ofn.lpstrTitle = NULL ; ofn.Flags = 0 : // Ustawiane w funkcjach Open i Close ofn.nFileOffset = 0 ; ofn.nFileExtension = 0 : ofn.lpstrOefExt = TEXT ("txt") ; ofn.lCustOata = OL ; ofn.lpfnHook = NULL ; ofn.lpTemplateName = NULL ; ) BOOL PopFileOpenDlg (HWND hwnd, PTSTR pstrFileName, PTSTR pstrTitleName) ( ofn.hwndOwner = hwnd ; ofn.lpstrFile = pstrFileName ; ofn.lpstrFileTitle = pstrTitleName ; ofn.Flags = OFN HIDEREADONLY ţ OFN CREATEPROMPT ; 1 return GetOpenFileName (&ofn) ; BOOL PopFileSaveDlg (HWND hwnd, PTSTR pstrFileName, PTSTR pstrTitleName) ofn.hwndOwner = hwnd ; ofn.lpstrFile = pstrFileName ; ofn.lpstrFileTitle = pstrTitleName ofn.Flags = OFN OVERWRITEPROMPT ; ) return GetSaveFileName (&ofn) ; BOOL PopFileRead (HWND hwndEdit, PTSTR pstrFileName) ( BYTE bySwap ; DWORD dwBytesRead ; HANDLE hFile ; int i, iFileLength, iUniTest ; PBYTE pBuffer, pText, pConv ; // Otwarcie pliku. if (INVALID_HANDLE VALUE = (hFile = CreateFile (pstrFileName, GENERICţREAD, FILEţSHAREţREAD, NULL, OPENţEXISTING, 0, NULL))) ii Rozdział 11: Okna dialogowe ţ return FALSE ; // Pobranie rozmiaru pliku w bajtach i przydzielenie pamięci do odczytu. // Dodawane sd dwa dodatkowe bajty na wypadek zakończenia zerem. i j iFileLength = GetFileSize (hFile, NULL) ; pBuffer = malloc (iFileLength + 2) ; /! Odczytanie pliku i umieszczenie na końcu zer. ReadFile (hFile, pBuffer, iFileLength, &dwBytesRead, NULL) ; CloseHandle (hFile) ; pBuffer[iFileLength] = '\0' pBuffer[iFileLength + 1] = '\0' , // Sprawdzenie, czy tekst jest w formacie Unicode iUniTest = IS TEXT UNICODEţSIGNATURE ţ IS TEXT UNICODEţREVERSEţSIGNATURE ; if (IsTextUnicode (pBuffer, iFileLength, &iUniTest)) pText = pBuffer + 2 ; ' iFileLength -= 2 ; if (iUniTest & IS TEXT UNICODEţREVERSE SIGNATURE) { for (i = 0 ; i < iFileLength / 2 ; i++) t bySwap = ((BYTE *) pText) [2 * i] ; ((BYTE *) pText) [2 * 1] = ((BYTE *) pText)"[2 * i + 1] ; ((BYTE *) pText) [2 * i + 1] = bySwap ; ) ) // Przydzielenie pamięci na ewentualny przekonwertowany cidg znaków pConv = malloc (iFileLength + 2) ; // Jeœli kontrolka edycji nie jest typu Unicode, przekonwertuj // odpowiednio tekst (np. na znaki dwubajtowe). ţţifndef UNICODE WideCharToMultiByte (CP ACP, 0, (PWSTR) pText, -1, pConv, iFileLength + 2, NULL, NULL) ; // Jeœli kontrolka edycji jest typu Unicode, po prostu skopiuj tekst 4ie1 se lstrcpy ((PTSTR) pConv, (PTSTR) pText) ; ţţendi f l else // plik nie jest formatu Unicode pText = pBuffer ; // Przydziel pamięć na konwertowany cišg znaków. pConv = malloc (2 * iFileLength + 2) ; 500 Częœć I: Podstawy (cišg dalszy ze strony 499) // Jeœli kontrolka edycji jest typu Unicode, przekonwertuj tekst ASCII. ţţifdef UNICODE MultiByteToWideChar (CP ACP, 0, pText, -1, (PTSTR) pConv, iFileLength + 1) : // Jeœli tak nie jest, po prostu skopiuj bufor llelse lstrcpy ((PTSTR) pConv, (PTSTR) pText) ; ilendi f ) SetWindowText (hwndEdit, (PTSTR) pConv) ; free (pBuffer) ; free (pConv) ; return TRUE ; ) BOOL PopFileWrite (HWND hwndEdit, PTSTR pstrFileName) ( DWORD dwBytesWritten ; HANDLE hFile ; int iLength ; PTSTR pstrBuffer ; WORD wByteOrderMark = OxFEFF ; // Otwórz plik, w razie potrzeby tworzšc nowy if (INVALID_HANDLE_VALUE == (hFile = CreateFile (pstrFileName, GENERIC_WRITE, 0, NULL, CREATE ALWAYS, 0, NULL))) return FALSE ; // Pobierz liczbę znaków w kontrolce edycji i przydziel pamięć // dla nich. iLength = GetWindowTextLength (hwndEdit) ; pstrBuffer = (PTSTR) malloc ((iLength + 1) * sizeof (TCHAR)) ; if (!pstrBuffer) ( CloseHandle (hFile) ; return FALSE ; 1 // Jeœli kontrolka edycji zwróci tekst Unicode, zapisz // w pliku znacznik porzddku bajtów. ţţifdef UNICODE WriteFile (hFile, &wByteOrderMark, 2, &dwBytesWritten, NULL) ; ilendi f // Pobierz bufor edycji i zapisz go do pliku. Rozdział 11: Okna dialogowe 501 ) GetWindowText (hwndEdit, pstrBuffer, iLength + 1) ; WriteFile (hFile, pstrBuffer, iLength * sizeof (TCHAR), &dwBytesWritten, NULL) ; if ((iLength * sizeof (TCHAR)) != (int) dwBytesWritten) CloseHandle (hFile) ; free (pstrBuffer) ; return FALSE ; CloseHandle (hFile) ; free (pstrBuffer) ; return TRUE ; POPFIND.C /* POPFIND.C - Funkcje wyszukiwania i zastępowania notatnika */ ţţinclude ţţinclude ţţinclude // Dla ţtcsstr (strstr dla Unicode i nie-Unicode) ţţdefine MAX STRING LEN 256 static TCHAR szFindText CMAXţSTRING_LEN] ; static TCHAR szReplText [MAXţSTRINGţLEN] ; HWND PopFindFindDlg (HWND hwnd) static FINDREPLACE fr ; // Dla okna niemodalnego musi być statyczna!!! fr.lStructSize = sizeof (FINDREPLACE) ; fr.hwndOwner = hwnd ; fr.hInstance = NULL ; fr.Flags = FR_HIDEUPDOWN ţ FRţHIDEMATCHCASE ţ FRţHIDEWHOLEWORD ; fr.lpstrFindWhat = szFindText ; fr.lpstrReplaceWith = NULL ; fr.wFindWhatLen = MAX STRINGţLEN ; fr.wReplaceWithLen = 0 ; fr.lCustOata = 0 ; fr.lpfnHook = NULL ; fr.lpTemplateN„me = NULL ; return FindText (&fr) ; ) HWND PopFindReplaceDlg (HWND hwnd) static FINDREPLACE fr ; // Dla okna niemodalnego musi być statyczna!!! fr.lStructSize = sizeof (FINDREPLACE) ; fr.hwndOwner = hwnd ; fr.hInstance = NULL ; 502 Częœć I: Podstawy (cišg dalszy ze strony 501) fr.Flags = FR_HIDEUPDOWN ţ FR HIDEMATCHCASE ţ FR HIDEWHOLEWORD ; fr.lpstrFindWhat = szFindText ; fr.lpstrReplaceWith = szReplText ; fr.wFindWhatLen = MAX STRING_LEN ; fr.wReplaceWithLen = MAX STRINGţLEN ; fr.lCustData = 0 ; fr.lpfnHook = NULL ; fr.lpTemplateName = NULL ; ) return ReplaceText (&fr) ; BOOL PopFindFindText (HWND hwndEdit, int * piSearchOffset, LPFINDREPLACE pfr) int iLength, iPos ; PTSTR pstrDoc, pstrPos ; // Wczytanie edytowane9o dokumentu iLength = GetWindowTextLength (hwndEdit) ; if (NULL --- (pstrDoc = (PTSTR) malloc ((iLength + 1) * sizeof (TCHAR)))) return FALSE ; GetWindowText (hwndEdit, pstrDoc, iLength + 1) ; // Wyszukanie w dokumencie szukanego cišgu znaków pstrPos = _tcsstr (pstr0oc + * piSearchOffset, pfr->lpstrFindWhat) ; free (pstrDoc) ; // Zwrócenie blędu, jeœli cišg znaków nie zostanie znaleziony if (pstrPos == NULL> return FALSE ; // Okreœlenie pozycji w dokumencie i ustawienie nowego przesunięcia iPos = pstrPos - pstr0oc ; * piSearchOffset = iPos + lstrlen (pfr->lpstrFindWhat) ; // Zaznaczenie odszukanego cišgu znaków SendMessage (hwndEdit, EM_SETSEL, iPos, * piSearchOffset) ; SendMessage (hwndEdit, EM SCROLLCARET, 0, 0) ; return TRUE ; BOOL PopFindNextText (HWND hwndEdit, int * piSearchOffset) ( FINDREPLACE fr ; fr.lpstrFindWhat = szFindText ; return PopFindFindText (hwndEdit, piSearchOffset, &fr) ; Rozdziaţ 11: Okna dialogowe 503 ? !,I BOOL PopFindReplaceText (HWND hwndEdit. int * piSearchOffset, LPFINDREPLACE pfr) ( // Odszukanie tekstu if (!PopFindFindText (hwndEdit, piSearchOffset, pfr)) return FALSE ; I l // Zastšpienie go ; SendMessage (hwndEdit, EM REPLACESEL. 0, (LPARAM) pfr->lpstrReplaceWith) ; return TRUE ) BOOL PopFindValidFind (void) ( return * szFindText != '\0' , 1 POPFONT.C /* POPFONT.C - Funkcje obslugi czcionek notatnika */ ţţinclude ţţinclude static LOGFONT logfont ; static HFONT hF.ont ; ! BOOL PopFontChooseFont (HWND hwnd) { CHOOSEFONT cf ; cf.lStructSize = sizeof (CHOOSEFONT) ; cf.hwndOwner = hwnd ; cf.hDC = NULL ; cf.lpLogFont = &logfont ; cf.iPointSize = 0 ; cf.Flags = CFţINITTOLOGFONTSTRUCT ţ CF SCREENFONTS ţ CF EFFECTS ; cf.rgbColors = 0 ; cf.lCustData = 0 ; cf.lpfnHook = NULL ; cf.lpTemplateName = NULL ; cf.hInstance = NULL ; cf.lpszStyle = NULL ; cf.nFontType = 0 ; // Zwracane przez ChooseFont cf.nSizeMin = 0 ; cf.nSizeMax = 0 ; return ChooseFont (&cf) ; void PopFontInitialize (HWND hwndEdit) 504 Częœć I: Podstawy (cišg dalszy ze strony 503) t GetObject (GetStockObject (SYSTEMţFONT), sizeof (LOGFONT), (PTSTR) &logfont) ; hFont = CreateFontIndirect (&logfont) ; SendMessage (hwndEdit, WMţSETFONT, (WPARAM) hFont, 0) ; ) void PopFontSetFont (HWND hwndEdit) ( HFONT hFontNew ; RECT rect ; hFontNew = CreateFontIndirect (&logfont) ; SendMessage (hwndEdit, WM SETFONT, (WPARAM) hFontNew, 0) ; Delete0bject (hFont) ; hFont = hFontNew ; GetClientRect (hwndEdit, &rect) ; InvalidateRect (hwndEdit, &rect, TRUE) ; ) void PopFontDeinitialize (void) f Delete0bject (hFont) ; ) POPPRNTO.C /* POPPRNTO.C - Funkcje drukowania notatnika (uproszczona wersja) */ include BOOL PopPrntPrintFile (HINSTANCE hInst, HWND hwnd, HWND hwndEdit, PTSTR pstrTitleName) { return FALSE ; ) POPPAD.RC (fragmenty) //Microsoft Developer Studio generated resource script. include "resource.h" ţţinclude "afxres.h" /////////////////////////////////////////////////////////////////////////// // Dialog ABOUTBOX DIALOG DISCARDABLE 32, 32, 180, 100 STYLE DS_MODALFRAME ţ WSţPOPUP FONT 8, "MS Sans Serif" BEGIN DEFPUSHBUTTON "OK",IDOK,66,80,50,14 Rozdział 11: Okna dialogowe 505 ICON "POPPAD",IDC_STATIC,7,7,20,20 CTEXT "PopPad",IDCţSTATIC,40,12,100,8 CTEXT "Popup Editor for Windows",IDC_STATIC,7,40,166,8 CTEXT "(c) Charles Petzold, 1998",IDC_STATIC,7,52,166,8 END !i PRINlDLGBOX DIALOG DISCARDABLE 32, 32, 186, 95 STYLE DS MODALFRAME ţ WSţPOPUP ţ WSţVISIBLE ţ WS CAPTION ţ WSţSYSMENU CAPTION "PopPad" FONT 8, "MS Sans Serif" BEGIN PUSHBUTTON "Cancel",IDCANCEL,67,74,50,14 CTEXT "Sending",IDC_STATIC,8,8,172,8 CTEXT " ,IDCţFILENAME,8,28,172,8 CTEXT "to print spooler.",IDCţSTATIC,8,48,172,8 END /////////////////////////////////////////////////////////////////////////// // Menu POPPAD MENU DISCARDABLE BEGIN POPUP "&File" BEGIN MENUITEM "&New\tCtrl+N", IDM_FILE_NEW MENUITEM "&Open...\tCtrl+0", IDM_FILE_OPEN MENUITEM "&Save\tCtr1+S", IDM_FILE_SAVE MENUITEM "Save &As... , IDMţFILEţSAVE ŽS MENUITEM SEPARATOR MENUITEM "&Print\tCtrl+p", IDMţFILEţPRINT MENUITEM SEPARATOR MENUITEM "E&xit", IDM APP EXIT END POPUP "&Edit" BEGIN MENUITEM "&Undo\tCtrl+Z", IDM EDIT UNDO MENUITEM SEPARATOR MENUITEM "Cu&t\tCtrl+X", IDM EDIT_CUT MENUITEM "&Copy\tCtrl+C", IDM_EDIT_COPY MENUITEM "&Paste\tCtrl+V", IDM_EDIT_PASTE MENUITEM "De&lete\tDel", IDM EDIT CLEAR MENUITEM SEPARATOR MENUITEM "&Select All", IDM EDIT SELECT ŽLL END POPUP "&Search" BEGTN MENUITEM "&Find...\tCtrl+F", IDM_SEARCH FIND MENUITEM "Find &Next\tF3", IDM_SEARCH NEXT MENUITEM "&Replace...\tCtrl+R", IDMţSEARCHţREPLACE END POPUP "F&ormat" BEGIN MENUITEM "&Font... , IDM FORMATţFONT END POPUP "&Help" BEGIN MENUITEM "&Help", IDM HELP MENUITEM "&About PopPad... , IDM ŽPP ABOUT 506 Częœć I: Podstawy (cišg dalszy ze strony 505) END END /////////////////////////////////////////////////////////////////////////// // Accelerator POPPAD ACCELERATORS DISCARDABLE BEGIN VK_BACK, IDM_EDIT_UNDO, VIRTKEY, ALT, NOINVERT VK_DELETE, IDM_EDIT_CLEAR, VIRTKEY, NOINVERT VK_DELETE, IDM_EDIT_CUT, VIRTKEY, SHIFT, NOINVERT VK_F1, IDM_HELP, VIRTKEY, NOINVERT UK_F3, IDM_SEARCH_NEXT, VIRTKEY, NOINVERT VK_INSERT, IDM_EDIT_COPY, VIRTKEY, CONTROL, NOINVERT VK_INSERT, IDM_EDIT_PASTE, VIRTKEY, SHIFT, NOINVERT "^C", IDM_EDIT_COPY, ASCII, NOINVERT "^F", IDM_SEARCH_FIND, ASCII, NOINVERT "^N", IDM_FILE_NEW, ASCII, NOINVERT "^0", IDM_FILE_OPEN, ASCII, NOINVERT "^P", IDM_FILE_PRINT, ASCII, NOINVERT "^R", IDM_SEARCH_REPLACE, ASCII, NOINVERT "^S", IDM_FILE_SAVE, ASCII, NOINVERT "^V", IDM_EDIT_PASTE, ASCII, NOINVERT "^X", IDM_EDIT CUT, ASCII, NOINVERT "^Z", IDM EDIT UNDO, ASCII, NOINVERT END /////////////////////////////////////////////////////////////////////////// // Icon POPPAD ICON DISCARDABLE "poppad.ico" RESOURCE.H (fragmenty) // Microsoft Developer Studio generated include file. // Used by poppad.rc ţţdefine IDCţFILENAME 1000 ţţdefine IDM_FILE NEW 40001 ţţdefine IDM_FILĘ OPEN 40002 ţţdefine IDM_FILE_SAVE 40003 ţţdefine IDM_FILE_SAVE ŽS 40004 ţţdefine IDM_FILE_PRINT 40005 ţţdefine.IDM_APP_EXIT 40006 ţţdefine IDM_EDIT_UNDO 40007 ţţdefine IDM_EDIT_CUT 40008 ţţdefine IDM_EDIT COPY 40009 ţţdefine IDM_EDIT_PASTE 40010 ţţdefine IDM_EDIT_CLEAR 40011 ţţdefine IDM_EDIT_SELECT_ALL 40012 ţţdefine IDM_SEARCH_FIND 40013 4ţdefine IDM_SEARCH_NEXT 40014 ţţdefine IDM_SEARCH_REPLACE 40015 ţţdefine IDM_FORMAT FONT 40016 ţţdefine IDM HELP 40017 ţţdefine IDM APP ABOUT 40018 Rozdział 11: Okna dialogowe 507 POPPAD.ICO .. ...... Rysunek 11-11. Program POPPAD3 Chcšc uniknšć duplikowania kodu Ÿródłowego w rozdziale, dodałem do menu w pliku POPPAD.RC drukowanie i kilka innych funkcji. Plik POPPAD.C zawiera podstawowy kod programu, POPFILE.C - kod zwišza- ny z wyœwietlaniem okien dialogowych Otwórz i Zapisz jako oraz procedury wej- œcia/wyjœcia, POPFIND.C - logikę wyszukiwania i zastępowania oraz POPFONT - logikę wyboru czcionki. Plik POPPIZNTO.C nie zawiera zbyt wiele - w rozdzia- le 13 zostanie zastšpiony przez plik POPPIZNT.C, tworzšc końcowš wersję pro- gramu POPPAD. Przyjrzyjmy się najpierw plikowi POPPAD.C. Zarzšdza on dwoma cišgami zna- ków z nazwami plików: pierwszy, przechowywany w procedurze WndProc pod nazwš szFileName, zawiera literę stacji dysków, pełnš œcieżkę i nazwę pliku. Dru- gi, nazwany szTitIeName, zawiera tylko nazwę pliku. W programie POPPAD z ciš- gów tych korzysta funkcja DoCaption, która wyœwietla nazwę pliku na pasku ty- tułu okna, oraz funkcje OKMessage i AskAboutSave, które wyœwietlajš okna ko- munikatów. W pliku POPFILE.C znajduje się kilka funkcji, które wyœwietlajš okna dialogowe Otwórz i Zapisz oraz wykonujš operacje wejœcia/wyjœcia. Okna dialogowe wy- œwietlane sš za pomocš funkcji GetOpenFileName i GetSaveFileName. Obie te funk- cje używajš struktury typu OPENFILENAME, zdefiniowanej w pliku COMM- DLG.H. W pliku POPFILE.C do przechowania tej struktury używana jest zmien- na globalna o nazwie ofn. Większoœć pól zmiennej ofn jest inicjowana w funkcji PopFilelnitialize, którš POPPAD.C wywołuje podczas przetwarzania komunikatu WM-CREATE w procedurze WndProc. Zmienna ofn jest definiowana jako statyczna struktura globalna, ponieważ funk- cje GetOpenFileName i GetSaveFileName zwracajš do struktury informacje, które powinny zostać użyte w kolejnych wywołaniach tych funkcji. Mimo że standardowe okna dialogowe zawierajš wiele opcji - włšczajšc w to konfigurowanie własnego szablonu okna dialogowego i zmienianie procedury okna dialogowego - okna dialogowe Otwórz i Zapisz w programie POPFILE.C zostały zastosowane w najprostszym wariancie. Jedynymi polami struktury OPENFILENAME, które ustawiamy, sš IStructSize (rozmiar struktury), hwndOw- ner (właœciciel okna dialogowego), lpstrFilter (to pole omówię), lpstrFile i nMaxFi- 508 Częœć I: Podstawy le (wskaŸnik do bufora, który odbierze pełnš nazwę pliku, oraz jego rozmiar), lpstrFileTitle i nMaxFileTitle (wskaŸnik do bufora, który odbierze samš tylko na- zwę pliku, oraz jego rozmiar), Flags (ustawia opcje okna dialogowego) i lpstrDe- fExt (ustawiane na cišg znaków, który zawiera domyœlne rozszerzenie pliku, jeœli użytkownik żadnego nie poda, wpisujšc w oknie dialogowym nazwę pliku). Gdy użytkownik wybiera opcję Open z menu File, program POPPAD3 wywołuje funkcję PopFileOpenDlg z pliku POPFILE, przekazujšc jej uchwyt okna, wskaŸniki do bufora pełnej nazwy pliku i do bufora nazwy pliku. Funkcja PopFileOpenDlg ustawia odpowiednio pola hwndOwner, lpstrFile, lpstrFileTitle i Flags (na OFNţCRE- ATEPROMPT) struktury OPENFILENAME, następnie wywołuje funkcję GetOpen- FileName, która wyœwietla znajome okno dialogowe pokazane na rysunku 11-12. Windows ApplicetionDate JDeneaplikacji iţJeve ?pplog JDnvatson JKursoy š Cavoot JFevorites JMedie J Commend ".]Fonts JMenuStart I Config Jforms ţ;:IMsApps J Cookies JHelp iNeWood J Corel ţ"ţhelpdesk ţOCCache Files ('.TXn Rysunek 11-12. Okno dialogowe Otwórz. Kiedy użytkownik zamknie to okno dialogowe, funkcja GetOpenFileName zwraca wartoœć. Flaga OFN CREATEPROMPT mówi funkcji GetOpenFileName, aby wy- œwietliła okno komunikatu z pytaniem o utworzonie pliku, jeœli wybrany plik nie istnieje. Pole listy rozwijanej w lewym dolnym rogu zawiera typy plików, które wyœwie- tlane sš na liœcie plików. Nazywa się to filtrem. Użytkownik może zmienić filtr, wybierajšc inny rodzaj pliku w polu listy rozwijanej. W funkcji PopFilelnitialize z pliku POPFILE.C definiuje filtr w zmiennej szFilter (tablica cišgów znaków) Zmi dla trzech typów plików: tekstowych z rozszerzeniem .TXT, ASCII z rozszerze- niem .ASC i dla wszystkich plików. WskaŸnik do pierwszego cišgu znaków w tej tablicy jest ustawiony na pole lpstrFilter struktury OPENFILENAME. Jeœli użytkownik zmienia filtr, gdy okno dialogowe jest aktywne, pole nFilterln- dex struktury OPENFILENAME odzwierciedla jego wybór. Ponieważ struktura ta jest przechowywana jako zmienna statyczna, przy następnym wywołaniu okna dialogowego filtr będzie nadal ustawiony na wybrany typ pliku. Rozdział 11: Okna dialogowe 509 Funkcja PopFileSaveDlg z pliku POPFILE.C działa podobnie. Ustawia parametr Flags na OFN OVERWRITEPROMPT i wywołuje funkcję GetSaveFileName, aby wyœwietlić okno dialogowe Zapisz. Flaga OFN OVERWRITEPROMPT powodu- je wyœwietlenie okna dialogowego z pytaniem o zapisanie pliku od nowa, jeżeli wybrany plik już istnieje. Operacje wejœcialwyjœcia na plikach Unicode W wielu programach w tej ksišżce nawet nie zauważysz różnicy pomiędzy wer- sjami Unicode i innymi. Na przykład w wersji Unicode programu POPPAD3 kon- trolka edycji i wszystkie standardowe okna dialogowe używajš tekstu Unicode. Gdy program ma wykonać operację wyszukania i zastšpienia, wykonywana jest ona z cišgami znaków Urucode nie wymagajšc konwersji. Program POPPAD3 wykonuje jednak operacje wejœcia/wyjœcia, co znaczy, że nie jest całkowicie hermetyczny. Jeœli wersja Unicode programu POPPAD3 pobierze zawartoœć bufora i zapisze jš z powrotem na dysk, plik będzie w formacie Unico- de. Jeżeli plik ten odczyta następnie wersja programu nie stosujšca Unicode i umie- œci go w buforze edycji, rezultatem będš przypadkowe cišgi znaków. Analogicz- nie dzieje się z plikami zapisanymi przez wersję nie stosujšcš Unicode i odczyta- nymi przez wersję zgodnš z Unicode. Rozwišzaniem jest zastosowanie identyfikacji i konwersji. Najpierw w funkcji PopFileWrite z pliku POPFILE.C zobaczysz, że wersja Unicode programu zapisu- je na poczštku pliku słowo OxFEFF. Jest to znacznik porzšdku bajtów, który wska- zuje, że plik zawiera tekst w formacie Unicode. Następnie w funkcji PopFileIZead program używa funkcji IsTextUnicode, aby okre- œlić, czy plik zawiera znacznik porzšdku bajtów. Funkcja ta sprawdza również odwrotny porzšdek bajtów, który oznacza, że plikt tekstowy Unicode został utwo- rzony na Macintoshu lub innej maszynie używajšcej odwrotnego porzšdku baj- tów niż w procesorach Intela. W takim wypadku odwrócona jest każda para baj- tów. Jeœli plik ma format Unicode, ale odczytuje go wersja programu POPPAD3 nie stosujšca Unicode, tekst zostanie przekonwertowany przez funkcję WideChar- ToMultiChar, która w rzeczywistoœci jest funkcjš konwersji rozszerzonego zesta- wu znaków na zestaw ANSI (chyba że masz dalekowschodniš wersję Windows). Dopiero po tym tekst może być umieszczony w buforze edycji. Analogicznie, jeżeli plik jest plikiem tekstowym o formacie innym niż Unicode i odczyta go wersja Unicode programu, tekst musi zostać przekonwertowany za pomocš funkcji MuItiCharToWideChar. Zmiana czcionki Czcionkom przyjrzymy się dokładniej w rozdziale 17 do ich wyboru, nic jednak nie jest lepsze od okien dialogowych. Podczas przetwarzania komunikatu WMţCREATE program POPPAD wywołuje funkcję PopFontlnitialize z pliku POPFONT.C. Funkcja ta pobiera strukturę LOG- FONT bazujšcš na czcionce systemowej, tworzy z niej czcionkę i wysyła komu- nikat WMţSETFONT do kontrolki edycji, aby ustawiła nowš czcionkę. (Domyœl- 510 ţ Częœć I: Podstawy nš czcionkš kontrolki edycji jest czcionka systemowa, ale funkcja PopFontlnitiali- ze tworzy nowš dla tej kontrolki, ponieważ na koniec czcionka ta zostanie usu- nięta, a nie jest dobrym pomysłem usuwanie czcionki systemowej). Po odebraniu komunikatu tNMţCOMMAND od opcji zmiany czcionki program POPPAD wywołuje funkcję ChooseFont, która wyœwietla okno dialogowe wybo- ru czcionki. Jeœli użytkownik naciœnie przycisk OK, funkcja ChooseFont zwróci wartoœć TRUE. Program POPPAD wywołuje wtedy funkcję PopFontSetFont, aby ustawić w kontrolce edycji nowš czcionkę. Stara czcionka jest usuwana. Na koniec, podczas przetwarzania komunikatu WM DESTROY program POP- PAD wywołuje funkcję PopFontDeinitialize, która usuwa ostatniš czcionkę utwo- rzonš przez funkcję PopFontlnitialize. Wyszukiwanie i zastępowanie Biblioteka standardowych okien dialogowych zawiera dwa okna dialogowe słu- żšce do wyszukiwania i zastępowania tekstu. Funkcje to umożliwiajšce (FindText i ReplaceText) używajš struktury typu FINDREPLACE. Plik POPFIND.C, pokaza- ny a rysunku 11-11, zawiera dwa podprogramy (PopFindFindDlg i PopFindRepla- ceDlg), które służš do wywoływania tych funkcji. Zawiera także kilka funkcji słu- żšcych do wyszukiwania i zastępowania tekstu w kontrolce edycji. Używajšc funkcji wyszukiwania i zastępowania należy wzišć pod uwagę kilka rzeczy. Po pierwsze, okna dialogowe wywoływane przez te funkcje sš niemodal- ne, co oznacza, że powinieneœ zmodyfikować pętle komunikatu, aby wywołać funkcję IsDialogMessage, gdy okna dialogowe sš aktywne. Po drugie, struktura FINDREPLACE, którš przekazujesz do funkcji FindText i RepIaceText, musi być zmiennš statycznš. Ponieważ okno dialogowe jest niemodalne, funkcje zwracajš wartoœć po zakończeniu wyœwietlania, a nie po jego zniszczeniu. Tym niemniej pocedura okna dialogowego musi mieć dalej dostęp do struktury. Po trzecie, gdy wyœwietlane sš okna dialogowe wyszukiwania i zastępowania, komunikujš się one z oknem nadrzędnym za pomocš specjalnego komunikatu. Numer tego ko- munikatu można uzyskać wywołujšc funkcję RegisterWindozvMessage z parame- trem FINDMSGSTRING. Jest to wykonywane podczas przetwarzania komuni- katu WM-CREATE w procedurze WndProc, a jego numer jest przechowywany w zmiennej statycznej. Podczas przetwarzania komunikatu domyœlnego procedura WndProc porównuje jego zmiennš z wartoœciš zwróconš przez funkcję RegisterWindowMessage. Para- metr lParam komunikatu jest wskaŸnikiem do struktury FINDREPLACE, nato- miast pole Flags wskazuje, czy użytkownik skorzystał z okna w celu odszukania, czy w celu zastšpienia tekstu oraz czy okno dialogowe jest zamykane. Program POPPAD3 wywołuje funkcje PopFindFindText i PopFindReplaceText z pliku PO- PFIND.C, aby wykonać operacje wyszukiwania i zastępowania. Program Windows zawierajšcy tyiko jedno wywołanie funkcji Do tej pory pokazałem dwa programy umożliwiajšce oglšdanie wybranych ko- lorów: COLORS1 w rozdziale 9 i COLORS2 w tym rozdziale. Teraz nadszedł czas na COLORS3 - program, który wykonuje tylko jedno wywołanie funkcji Windows. Rozdział 11: Okna dialogowe 511 Kod Ÿródłowy tego programu pokazany jest na rysunku 11-13. Jedynš funkcjšţ którš wywołuje program COLORS3, jest ChooseColor - kolejna i funkcja z biblioteki standardowych okien dialogowych. Wyœwietla ona okno dia- ' logowe pokazane na rusunku 11-14. Wybór kolorów odbywa się podobnie spo- sób, jak w programach COLORSI i COLORS2, ale jest nieco bardziej interakcyj- ny. I. COLORS3.C /* COLORS3.C - Wersja ze standardowym oknem dialogowym (c) Charles Petzold, 1998 */ I. ţţinclude 4ţinclude int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) static CHOOSECOLOR cc ; static COLORREF crCustColorsCl67 : cc.lStructSize = sizeof (CHOOSECOLOR) ; cc.hwndOwner = NULL ; cc.hInstance = NULL ; cc.rgbResult = RGB (0x80, 0x80, 0x80) : cc.lpCustColors = crCustColors ; cc.Flags = CCţRGBINIT ţ CCţFULLOPEN : cc.lCustData = 0 : cc.lpfnHook = NULL ; cc.lpTemplateName = NULL ; return ChooseColor (&cc) ; Rysunek 11-13. Program COLORS3 512 Częœć 1ţ Podstawy Funkcja ChooseColor używa struktury typu CHOOSECOLOR i tablicy 16 słów DWORD do przechowywania kolorów, które użytkownik wybierze w oknie dia- logowym. Pole rgbResult można zainicjować na wartoœć koloru, który będzie wyœwietlony, jeœli w polu Flags ustawiona zostanie flaga CC RGBINIT. Przy tra- dycyjnym zastosowaniu funkcji pole rgbResult będzie ustawione na wartoœć ko- loru wybranego przez użytkownika. Zwróć uwagę na to, że pole hwndOwner okna dialogowego Kolor jest ustawione na NULL. Gdy funkcja ChooseColor wywołuje funkcję DialogBox, aby wyœwietlić okno dialogowe, trzeci parametr tej funkcji także ustawiony jest, w pełni popraw- nie na NULL. Oznacza ono, że okno dialogowe nie ma właœciciela. Tytuł tego okna pojawi się na liœcie zadań i będzie ono funkcjonować jak zwykłe okno. Technikę tę możesz także wykorzystać przy własnych oknach dialogowych. Mo- żesz też skonstruować program Windowsowy, który będzie tworzył tylko okna dialogowe i przetwarzał wszystko w procedurze okna dialogowego. Rysunek 11-14. Ekran programu COLORS3 Rozdział 12 Schowek Schowek Microsoft Windows pozwala przenosić dane między programami. Jest to stosunkowo prosty mechanizm, nie wymagajšcy dużego wysiłku przy two- rzeniu programów, które pobierajš dane ze Schowka i umieszczajš je w nim. Z Windows 98 i Windows NT dostarczany jest program podglšd Schowka, poka- zujšcy aktualnš zawartoœć Schowka. Wiele programów, działajšcych na dokumentach lub innych danych, zawiera menu Edycja (Edit) z opcjami Wytnij (Cut), Kopiuj (Copy) i Wklej (Paste). Gdy użytkownik wybiera Wytruj lub Kopiuj, program przenosi swoje dane do Schowka. Dane majš okreœlony format, zwykle jest to tekst, bitmapa (prostokštna tablica bitów odpowiadajšcych pikselom wyœwietlanego obszaru) lub metaplik (binar- ny zbiór poleceń rysowania). Gdy użytkownik wybierze w menu opcję Wklej, program sprawdza, czy w Schowku znajdujš się jakieœ dane, których może użyć, i jeœli tak jest, zabiera je do siebie. Programy nie powinny przenosić danych do lub ze Schowka bez wyraŸ,nego po- lecenia użytkownika. Na przykład użytkownik, który wykona w programie ope- rację Wytnij lub Kopiuj ([Ctrl+X] lub [Ctrl+C]) powinien mieć pewnoœć, że dane pozostanš w Schowku aż do momentu, gdy uruchomi następnš operację Wytnij lub Kopiuj. Jak zapewne pamiętasz, menu Edit zostało zaimplementowane w wersjach pro- gramu POPPAD z rozdziałów 10 i 11. Użyto tam jednak zwykłego wysyłania komunikatów do kontrolki edycji. W większoœci przypadków nie jest to wystar- czajšce - zamiast tego musisz sam wywołać funkcje przemieszczania danych Schowka. W tym rozdziale skoncentrujemy się na przenosţeniu tekstu do i ze Schowka. W kolejnych rozdziałach zobaczysz, jak używa się go z bitmapami (rozdziały 14, 15 i 16) i metaplikami (rozdział 18). Proste zastosowanie Schowka Zaczniemy od kodu umożliwiajšcego przeniesienie danych do Schowka (Cut i Co- py) i uzyskanie dostępu do danych umieszczonych w nim wczeœniej (Paste). Standardowe formaty danych Schowka Windows obsługuje różne predefiniowane formaty danych Schowka. Ich identy- fikatory rozpoczynajš się przedrostkiem CD i zdefiniowane sš w pliku WINU- SEft.H. 514 Częœć I: Podstawy Poniżej wymienione sš trzy typy danych tekstowych, które można przechowy- wać w Schowku, oraz dodatkowy typ ustawień lokalnych. ů CF TEXT Cišg znaków z zestawu ANSI zakończony znakiem NULL i za- wierajšcy na końcu każdego wiersza znak powrotu karetki i nowego wiersza. Dane, które majš być przeniesione do Schowka, zapisane sš w bloku pamięci - przenoszony jest uchwyt do tego bloku. (Wkrótce omówimy ten mechanizm). Blok pamięci staje się własnoœciš Schowka - program, który utworzył ten blok, nie powinien go więcej używać. ů CF OEMTEXT Blok pamięci zawierajšcy dane tekstowe (podobny do CF TEXT), ale używajšcy zestawu znaków OEM. Programy Windows zwy- kle nie muszš używać tego typu. Staje się on istotny przy wykorzystaniu Schowka z programami MS-DOS uruchomionymi w oknie. ů CF-UNICODETEXT Blok pamięci zawierajšcy tekst Unicode. Podobnie jak w CF TEXT każdy wiersz zakończony jest znakiem powrotu karetki i nowego wiersza, a znak NULL (dwa bajty zerowe) wskazuje koniec danych. CF UNI- CODETEXT jest obsługiwany tylko w Windows NT. ů CF LOCALE Uchwyt do identyfikatora ustawień lokalnych zwišzanych z tek- stem Schowka. Dostępne sš dwa dodatkowe formaty Schowka, koncepcyjnie zgodne z forma- tem CF TEXT (tzn. opierajš się na tekœcie), ale ich dane nie sš zakończone zna- kiem NULL - formaty te definiujš własny koniec danych. Dzisiaj sš one rzadko używane: ů CF SYLK Blok pamięci zawierajšcy dane w formacie Symbolic Link Microso- ftu. Format ten jest używany do wymiany danych między programami Excel, Pr Chart i Multiplan Microsoftu. Jest to format ASCII, w którym każdy wiersz zakończony jest znakiem powrotu karetki i nowego wiersza. ů CF DIF Blok pamięci zawierajšcy dane w formacie Data Interchange Format (DIF). Jest to format zalecany przez Software Arts do używania przy przeno- szeniu danych do arkusza kalkulacyjnego VisiCalc, również oparty na ASCII z wierszami zakończonymi znakiem powrotu karetki i nowego wiersza. Następujšce formaty Schowka używane sš z bitmapami, które sš tablicami bi- tów odpowiadajšcych pikselom na urzšdzeniu wyjœciowym. Bitrnapy i zwišza- ne z nimi formaty Schowka omówione zostanš dokładniej w rozdziałach 14 i 15. ů CF BITMAP Bitmapa zależna od urzšdzenia. Mapa ta jest przenoszona do Schowka za pomocš jej uchwytu. Program, po przekazaniu uchwytu bitmapy do Schowka, nie powinien dalej go używać. , hGl CF DIB Blok pamięci definiujšcy bitmapę niezależnš od urzšdzenia, tak jak opisano to w rozdziale 15. Blok pamięci zaczyna się od struktury informacyj- nej bitmapy, po której następuje opcjonalna tabela kolorów i bity. ů CF PALETTE Uchwyt palety kolorów. Typ używany głównie w połšczeniu z CD DIB do definiowania palety kolorów używanej przez bitmapę niezależnš od urzšdzenia. Rozdział 12: Schowek 515 Dane bitmapy można także przechowywać w formacie standardu przemysłowe- go TIFF: ů CF TIFF Blok pamięci zawierajšcy dane typu Tag Image File Format (TIFF). Jest to format zaprojektowany przez firmę Microsoft, Aldus Corporation i Hew- lett-Packard oraz kilku innych wytwórców sprzętu komputerowego. Format ten opisany jest na stronie internetowej Hewletta-Packarda. Dostępne sš także dwa formaty metaplików, które omówię dokładniej w rozdziale 18. Metaplik jest zbiorem poleceń rysowania przechowanych w formacie binar- nym: ů CF METAFILEPICT - obraz typu metaplik oparty na starym formacie obsłu- gi metaplików w Windows ů CF ENHMETAFILE - uchwyt rozszerzonego metapliku obshzgiwanego w 32- bitowych wersjach Windows. Istnieje także kilka innych formatów Schowka przeznaczonych do różnych za- stosowań: ů CF PENDATA - używany w połšczeniu z programami obsługujšcymi pióro œwietlne w Windows ů CF WAVE - plik dŸwiękowy (wave) ů CF_RIFF - dane multimedialne w formacie Resource Interchange File Format ů CF HDROP - lista plików używana w połšczeniu z programami obshzgujš- cymi funkcje przecišgnij i upuœć (ang. drag and drop). Przydzielanie pamięci Gdy program przenosi coœ do Schowka, musi zarezerwować blok pamięci i prze- kazać go Schowkowi. Gdy potrzebowaliœmy przydzielić pamięć we wczeœniej- szych programach w tej ksišżce, używaliœmy po prostu funkcji malloc, która do- starczana jest w standardowej bibliotece C. Ponieważ jednak bloki pamięci prze- chowywane w Schowku sš wspólnie użytkowane przez różne aplikacje urucho- mione w Windows, funkcja malloc nie jest odpowiednia do tego zadania. Zamiast tego musimy skorzystać z funkcji przydzielania pamięci zaprojektowa- , nych w czasach, gdy system Windows używał 16-bitowej architektury pamięci w trybie rzeczywistym. Funkcje te sš wcišż dostępne i można ich używać, ale rzad- ko jest to potrzebne. Chcšc przydzielić blok pamięci za pomocš Windows API, możesz napisać: hGlobal = GlobalAlloc (uiFlags, dwSize> : Funkcja pobiera dwa parametry: opcjonalny cišg flag i rozmiar w bajtach przy- dzielanego bloku pamięci. Funkcja zwraca uchwyt typu HGLOBAL, nazywany uchwytem globalnego bloku pamięci lub uchwytem globalnym. Jeœli zwrócona zostanie wartoœć NLTLL, oznacza to, że nie ma wystarczajšco dużo wolnej pamięci, aby przydzielić blok. Mimo że każdy z parametrów funkcji GlobalAlloc jest zdefiniowany nieco inaczej, obydwa sš 32-bitowymi liczbami typu unsigned integer. Jeœli ustawisz pierwszy parametr na zero, użyjesz w ten sposób flagi GMEM FIXED. W takim wypadku 516 Częœć I: Podstawy uchwyt globalny zwrócony przez GlobalAlloc będzie w rzeczywistoœci wskaŸni- kiem przydzielonego bloku pamięci. Możesz użyć także flagi GMEM ZERoINIT, jeżeli chcesz, aby każdy bajt w blo- ku pamięci, był poczštkowo ustawiony na zero. Flaga GPTR zgodnie z definicjš w plikach nagłówkowych Windows jest połšczeniem flag GMEM FIXED i GMEM ZEROINIT: - Ildefine GPTR (GMEMţFIXED ţ GMEM-ZEROINIT) Dostępna jest także funkcja realokujšca: hGlobal = GlobalReAlloc (hGlobal, dwSize, uiFlags) ; Możesz wykorzystać flagę GMEM ZEROINIT, aby wyzerować nowe bajty przy powiększaniu bloku pamięci. Poniższa funkcja zwraca rozmiar bloku pamięci: hGl dwSize = GlobalSize (hGlobal) : Natomiast ta funkcja zwalnia blok pamięci: GlobalFree (hGlobal) ; W 16-bitowych wersjach Windows odradzano używania flagi GMEM FIXED, po- nieważ system operacyjny nie mógł przenieœć bloku w pamięci fizycznej. W wer- sjach 32-bitowych nie stanowi to problemu, ponieważ flaga GMEM_FIXED zwra- ca adres wirtualny, dzięki czemu system operacyjny może przenieœć blok w pamięci fizycznej zmieniajšc tabelę stron. Programistom w 16-bitowych wersjach Windows zalecano zamiast tego używanie w funkcji GlobalAlloc flagi GMEM MOVEABLE. (Zauważ, że większoœć słowników zaleca pisownię "movable" zamiast "moveable" - ja także jš preferuję w niektórych sytuacjach). W plikach nagłówkowych zdefi- niowany jest także identyfikator zerujšcy przenoœnš pamięć: Ildefine GHND (GMEM MOVEABLE ţ GMEM ZEROINIT) Flaga GMEM MOVEABLE pozwala Windows przenieœć blok w pamięci wirtu- alnej. Nie musi to oznaczać, że blok ten zostanie przemieszczony także w pamię- ci fizycznej, ale może zmienić się adres, którego aplikacja używa do zapisu i od- czytu z tego bloku. Mimo że flaga GMEM MOVEABLE była ważna w 16-bitowej wersji Windows, teraz jest znacznie mniej użyteczna. Jeœli jednak aplikacja często przydziela, re- alokuje i zwalnia bloki pamięci o różnych rozmiarach, jej wirtualna przestrzeń hGl c adresowa może stać się pofragmentowana. Z tego powodu może zabraknšć wol- nych adresów pamięci wirtualnej. Jeżeli napotkasz taki problem, możesz użyć pamięci przenoœnej w pokazany poniżej sposób. pGlc Najpierw zdefiniuj wskaŸnik (na przykład na typ int) i zmiennš typu GLOBAL- HANDLE: int * p ; for GLOBALHANDLE hGlobal ; Następnie przydziel pamięć, na przykład: hGlobal = GlobalAlloc (GHND, 1024) ; Glob Podobnie jak w przypadku każdego innego uchwytu w Windows, nie zastana- wiaj się, co oznacza ta liczba. Po prostu jš zapisz. Gdy zechcesz uzyskać dostęp do tego bloku pamięci, wpisz: Rozdział 12: Schowek 517 p = (int *) GlobalLock (hGlobal) : Powyższe wywołanie żamienia uchwyt na wskaŸnik. Kiedy blok pamięci jest zablokowany, Windows ustala adres w pamięci wirtualnej. Nie może być on wtedy przemieszczany. Gdy dostęp do bloku nie będzie już potrzebny, wywołaj: GlobalUnlock (hGlobal> ; Instrukcja ta pozwala systemowi Windows swobodnie przemieszczać blok w pa- mięci wirtualnej. Aby proces ten przeprowadzić w zupełnoœci poprawnie (i zy- skać uznanie programistów pierwszych wersji Windows), powinieneœ zabloko- wać i odblokować blok pamięci w cišgu trwania jednego komunikatu. Kiedy chcesz zwolnić pamięć, zamiast ze wskaŸnikiem wywołaj GlobalFree z uchwytem. Jeœli nie masz aktualnie do niego dostępu, użyj funkcji: hGlobal = GlobalHandle (p) : Możesz wiele razy zablokować blok pamięci przed jego odblokowaniem. Windows zarzšdza licznikiem zablokowań i aby blok mógł być przemieszczony, każda zało- żona blokada musi zostać wczeœniej odblokowana. Gdy system ten przemieszcza blok w pamięci wirtualnej, nie musi kopiować bajtów z jednego miejsca w drugie - wystarcza do tego manipulacja na tabelach stron pamięci. W 32-bitowych wersjach Windows jedynym powodem przydzielania pamięci przenoœnej do użytku przez program jest chęć uniknięcia fragmentacji pamięci wirtualnej. Pamięć przenoœnš należy również stosować przy operacjach z użyciem Schowka. Przydzielajšc pamięć dla Schowka, powinieneœ raczej używać funkcji GlobalAlloc z flagami GMEM MOVEABLE i GMEM SHARE. Flaga GMEM SHARE powo- duje, że blok pamięci dostępny jest także dla innych aplikacji Windows. Przenoszenie tekstu do Schowka Załóżmy, że chcesz przenieœć cišg znaków ANSI do Schowka. Masz wskaŸnik (nazwany pString) do tego cišgu i chcesz przenieœć iLength znaków, które mogš, ale nie muszš być zakończone znakiem NULL. Najpierw musisz użyć funkcji GlobalAlloc, aby przydzielić blok pamięci o odpo- wiednim rozmiarze, w którym zapisany zostanie cišg znaków. Zostaw przestrzeń na końcowy znak NULL: hGlobal = GlobalAlloc (GHND ţGMEM SHARE, iLength + 1) : Wartoœć hGlobal wyniesie NULL, jeżeli nie zostanie przydzielona pamięć dla blo- ku. jeœli operacja powiedzie się, zablokuj blok, aby uzyskać do niego wskaŸnik: pGlobal = GlobalLock (hGlobal) ; Skopiuj cišg znaków do globalnego bloku pamięci: for (i = 0 ; i < wLength ; i++) *pGlobal++ = *pString++ ; Nie musimy dodawać końcowego znaku NULL, ponieważ flaga GHND funkcji GlobalAlloc zeruje cały blok podczas przydzielania pamięci. Odblokuj blok: GlobalUnlock (hGlobal) ; Teraz masz uchwyt bloku globalnego, który odwołuje się do bloku zawierajšce- go tekst zakończony znakiem NULL. Aby przenieœć ten tekst do Schowka, otwórz Schowek i wyczyœć jego zawartoœć: 518 Czţœć I: Podstawy ţ Rozţ OpenClipboard (hwnd) EmptyClipboard () ; Przekaż Schowkowi uchwyt pamięci używajšc identyfikatora CF TEXT i zamknij go: SetClipboardData (CF TEXT, hGlobal) ; CloseClipboard () ; T p e To wszystko. Poniżej wymienione sš niektóre zasady dotyczšce tego procesu: ů Wywołuj funkcje OpenClipboard i CloseClipboard podczas przetwarzania jed- nego komunikatu. Nie pozostawiaj Schowka otwartego dłużej, niż jest to po- pGl trzebne. ů Nie przekazuj Schowkowi zablokowanego uchwytu pamięci. str ů Po wywołaniu funkcji SetClipboardData nie używaj więcej przekazanego bloku pamięci. Nie należy on już do programu i powinieneœ traktować jego uchwyt whi jako nieważny. jeœli wcišż potrzebny jest ci dostęp do tych danych, utwórz ich kopię lub odczytaj je ze Schowka (w sposób opisany w następnym podrozdzia- G 1 0 le). Do bloku możesz odwoływać się między wywołaniami SetClipboardData C1 0 i CloseClipboard, ale nie używaj uchwytu globalnego, który przekazałeœ do funk- cji SetClipboardData. Funkcja ta również zwraca uchwyt globalny, którego mo- żesz użyć. Aby uzyskać dostęp do pamięci, zablokuj ten uchwyt. Przed wy- wołaniem CIoseClipboard odblokuj go. ţt Pobieranie tekstu ze Schowka Pobranie tekstu ze Schowka jest tylko trochę bardziej skomplikowane niż przenie- sienie do niego tekstu. Najpierw musisz sprawdzić, czy Schowek w ogóle zawiera dane w formacie CF TEXT. Najprostszym sposobem jest użycie wywołania: bAvailable = IsClipboardFormatAvailable (CF TEXT) ; Funkcja ta zwraca wartoœć TRUE, jeœli Schowek zawiera dane CF TEXT. Użyli- œmy jej w programie POPPAD2 z rozdziału 10, aby sprawdzić, czy opcja Paste w menu Edit powinna być udostępniona czy szara. IsClipboardAvailable jest jednš z kilku funkcji, których możesz użyć bez wczeœniejszego otwierania Schowka. Je- żeli jednak póŸniej otworzysz Schowek, aby pobrać tekst, powinieneœ ponownie sprawdzić (używajšc tej samej funkcji lub jednej z innych metod), czy dane typu CF TEXT wcišż znajdujš się w Schowku. Chcšc pobrać tekst, najpierw otwórz Schowek: OpenClipboard (hwnd) ; Uzyskaj uchwyt globalnego bloku pamięci, odwohxjšc się do tekstu: hGlobal = GetClipboardData (CF TEXT) ; Uchwyt ten będzie mieć wartoœć NULL, jeœli Schowek nie zawiera danych w for- macie CF TEXT. Jest to jedna z metod sprawdzania, czy zawiera on tekst. Jeżeli funkcja GetClipboardData zwróci NULL, zamknij Schowek bez wykonywania do- datkowych czynnoœci. Uchwyt, który uzyskujesz od funkcji GetClipboardData, nie należy do programu, tylko do Schowka. Uchwyt ten jest ważny tylko między wywołaniami GetClipboardData Rozdział 12: Schowek 519 i CloseClipboard. Nie możesz zwolnić go ani zmienić danych, do których się od- wołuje. Jeœli dostęp do danych będzie ci potrzebny póŸniej, powinieneœ wykonać kopię bloku pamięci. Poniżej pokazana jest jedna z metod kopiowania danych do programu. Po prostu przydziel wskaŸnik do bloku o takim samym rozmiarze jak blok danych Schowka: pText = (char *) malloc (GlobalSize (hGlobal)) : Jak pamiętasz, hGlobal był uchwytem globalnym uzyskanym od funkcji GetClipbo- ardData. Teraz zablokuj uchwyt, aby uzyskać wskaŸnik do bloku pamięci Schow- ka: pGlobal = GlobalLock (hGlobal) ; Następnie po prostu skopiuj dane: strcpy (pText. pGlobal) : lub użyj prostego kodu: while (*pTEXT++ = *pGlobal++) : Odblokuj blok przed zamknięciem Schowka: GlobalUnlock (hGlobal) : CloseClipboard () : Teraz masz wskaŸnik nazwany pText, który odwołuje się do kopii tekstu należš- cej do programu. Otwieranie i zamykanie Schowka Schowek może być otwarty jednorazowo tylko dla jednego programu. Celem wywołania OpenClipboard jest zabezpieczenie jego zawartoœci przed zmianami, gdy program go używa. Funkcja OpenClipboard zwraca wartoœć boolowskš wska- zujšcš, czy otwarcie Schowka powiodło się. Nie zostanie otwarty, jeœli inna apli- kacja nie zamknęła go poprawnie. Gdyby wszystkie programy otwierały i zamy- kały Schowek tak szybko, jak jest to możliwe, w odpowiedzi na żšdanie użyt- kownika, prawdopodobnie nigdy nie doœwiadczyłbyœ trudnoœci zwišzanych z jego otwieraniem. Jednak w œwiecie nieuprzejmych, wielozadaniowych programów, takie proble- my mogš się pojawić. Nawet jeœli program nie stracił fokusu wejœciowego mię- dzy umieszczeniem czegoœ w Schowku a użyciem opcji Paste, nie należy zakła- dać, że dane umieszczone w nim nadal tam sš. W tym czasie dostęp do Schowka mógł uzyskać proces uruchomiony w tle. Zwróć także uwagę na bardziej subtelny problem dotyczšcy okien komunikatu: jeżeli nie możesz przydzielić wystarczajšco dużo pamięci, aby skopiować coœ do Schowka, możesz wyœwietlić okno komunikatu. Jeœli jednak nie jest ono modal- ne w systemie, użytkownik może przełšczyć się do innej aplikacji podczas jego wyœwietlania. Powinieneœ albo wyœwietlać modalne okno komunikatu, albo za- mknšć Schowek przed wyœwietleniem komunikatu. Trudnoœci możesz napotkać także wtedy, gdy pozostawisz otwarty Schowek i wy- œwietlisz okno dialogowe. Pola edycji w oknach dialogowych wykorzystujš Scho- wek do wycinania i wstawiania tekstu. 520 Częœć I, Podstawy ' Roţ Schowek i Unicode Dotšd omawiałem używanie Schowka wyłšcznie z tekstem ANSI (jeden bajt na znak). Format ten jest stosowany po użyciu identyfikatora CF TEXT. Być może, zastanawiasz się jednak, jak korzystać z identyfikatorów CF OEMTEXT i CF UNI- CODETEXT. Mam dobre wiadomoœci: wystarczy wywołać funkcje SetClipboardData i GetClip- boardData z wybranym formatem tekstu, a Windows automatycznie wykona w Schowku potrzebne konwersje. Jeœli na przykład w Windows NT program uży- wa funkcji SetClipboardData z typem danych Schowka CF TEXT, może także wy- wołać funkcję GetClipboardData z identyfikatorem CF OEMTEXT. Analogicznie można na przykład przekonwertować dane typu CF OEMTEXT na CF TEXT. W Windows NT konwersja może być wykonywana pomiędzy formatami CF UM- CODETEXT, CF TEXT i CF OEMTEXT. Program powinien wywoływać funkcję SetClipboardData, używajšc formatu, który jest dla niego najbardziej wygodny. Analogicznie funkcja GetClipboardData powinna być wywoływana z formatem wymaganym przez program. Jak zapewne wiesz, programy w tym podręczniku sš tak napisane, że mogš być kompilowane z lub bez identyfikatora UNICODE. Jeœli twoje programy używajš identyfikatora UNICODE, a CF TEXT jest niezde- finiowany, powinieneœ zaimplementować kod, który będzie wywoływał funkcje SetClipboardData i GetClipboardData z identyfikatorem CF UNICODETEXT. Program CLIPTEXT, pokazany na rysunku 12-1, przedstawia jeden ze sposobów, w jaki można to zrobić. CLIPTEXT.C /* CLIPTEXT.C - Schowek i tekst (c) Charles Petzold, 1998 */ ilinclude #include "resource.h" LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; ili fdef UNICODE lldefine CF_TCHAR CF_UNICODETEXT TCHAR sz0efaultText[] = TEXT ("Default Text - Unicode Version") ; TCHAR szCaption[] = TEXT ("Clipboard Text Transfers - Unicode Version") ; ilel se ţţdefine CF_TCHAR CF_TEXT TCHAR szDefaultText[7 = TEXT ("Default Text - ANSI Version") ; TCHAR szCaption[] = TEXT ("Clipboard Text Transfers - ANSI Version") ; ţţendi f int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, Rozdział 12: Schowek 521 PSTR szCmdLine, int iCmdShow) ( static TCHAR szAppNameC] = TEXT ("ClipText") ; HACCEL hAccel ; HWND hwnd : I MSG ms9 ' WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CS VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE BRUSH) ; wndclass.lpszMenuName = szAppName ; wndclass.lpszClassName = szAppName ; k; F.. if (!RegisterClass (&wndclass)) ( Messa9eBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MBţICONERROR) ; return 0 ; } hwnd = CreateWindow (szAppName, szCaption, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; hAccel = LoadAccelerators (hInstance, szAppName) ; while (GetMessage (&msg, NULL, 0, 0)) ( if (!TranslateAccelerator (hwnd, hAccel, &msg)) ( TranslateMessage (&msg) ; DispatchMessage (&msg) ; } } return msg.wParam ; } LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) I I k. static PTSTR pText ; BOOL bEnable ; HGLOBAL hGlobal ; HDC hdc ; PTSTR pGlobal ; PAINTSTRUCT ps ; RECT rect ; ţ;, I switch (message) ţ.:I i;, ; „:: Częœć I: Podstawy (cišg dalszy ze strony 521) ( case WM_CREATE: SendMessage (hwnd, WM COMMAND, IDM EDIT RESET, 0) ; return 0 ; case WM_INITMENUPOPUP: EnableMenuItem ((HMENU) wParam, IDM EDIT_PASTE, IsClipboardFormatAvailable (CF TCHAR) ? MFţENABLED : MF GRAYED) ; bEnable = pText ? MFţENABLED : MF GRAYED ; EnableMenuItem ((HMENU) wParam, IDM_EDIT CUT, bEnable) ; EnableMenuItem ((HMENU) wParam, IDM_EDITţCOPY, bEnable) ; EnableMenuItem ((HMENU) wParam, IDM EDIT CLEAR, bEnable) ; break ; case WM COMMAND: switch (LOWORD (wParam)) ( case IDM_EDIT_PASTE: OpenClipboard (hwnd) ; if (hGlobal = GetClipboard0ata (CF TCHAR)) f pGlobal = GlobalLock (hGlobal) ; if (pText) ( free (pText) ; pText = NULL ; } pText = malloc (GlobalSize (hGlobal)) ; lstrcpy (pText, pGlobal) ; InvalidateRect (hwnd, NULL, TRUE) ; ) CloseClipboard () ; return 0 ; case IDM_EDIT_CUT: case IDM_EDIT COPY: if (!pText) return 0 ; hGlobal = GlobalAlloc (GHND ţ GMEM_SHARE, (lstrlen (pText) + 1) * sizeof (TCHAR)) ; pGlobal = GlobalLock (hGlobal) ; //ţ lstrcpy (pGlobal, pText) ; GlobalUnlock (hGlobal) ; ţţir lli r OpenClipboard (hwnd) ; EmptyClipboard () ; // SetClipboardData (CF TCHAR, hGlobal) ; // CloseClipboard (> ; if (LOWORD (wParam) = IDMţEDITţCOPY) return 0 ; BEG // przypadek IDM EDIT CUT zostawiamy nieopracowany Rozdział 12: Schowek 523 case IDM_EDIT_CLEAR: if (pText) ( free (pText) : pText = NULL ; 1 InvalidateRect (hwnd, NULL, TRUE) ; I return 0 : case IDM_EDIT_RESET: if (pText) free (pText) : pText = NULL ; pText = malloc ((lstrlen (szDefaultText) + 1) * sizeof (TCHAR)) ; lstrcpy (pText, szDefaultText) ; InvalidateRect (hwnd, NULL, TRUE) : return 0 ; break ; case WM PAINT: hdc = BeginPaint (hwnd, &ps) : GetClientRect (hwnd, &rect) : if (pText != NULL) DrawText (hdc, pText, -1, &rect. DT EXPANDTABS ţ DT WORDBREAK) : EndPaint (hwnd, &ps) ; return 0 ; case WM DESTROY: if (pText) . free (pText) : PostOuitMessage (0) : return 0 ; 1 return DefWindowProc (hwnd, message, wParam, lParam) : ) CLIPTEXT.RC (fragmenty) //Microsoft Developer Studio generated resource script. ţţinctude "resource.h" ţţinclude "afxres.h" ////l////////////////////////////////////////////////////////////////////// // Menu CLIPTEXT MENU DISCARDABLE BEGIN POPUP "&Edit" 524 Częœć I: Podstawy (cišg dalszy ze strony 523) BEGIN MENUITEM "Cu&t\tCtrl+X", IDM_EDIT_CUT MENUITEM "&Copy\tCtrl+C", IDM EDIT_COPY MENUITEM "&Paste\tCtrl+V", IDM EDIT_PASTE MENUITEM "De&lete\tDel", IDMţEDIT_CLEAR MENUITEM SEPARATOR MENUITEM "&Reset", IDM EDIT RESET END END /////////////////////////////////////////////////////////////////////////// // Accelerator CLIPTEXT ACCELERATORS DISCARDABLE BEGIN "C", IDM_EDIT_COPY, VIRTKEY, CONTROL, NOINVERT "V", IDMţEDIT_PASTE, VIRTKEY, CONTROL, NOINVERT VK_DELETE, IDM_EDIT_CLEAR, VIRTKEY, NOINVERT "X", IDM EDIT CUT, VIRTKEY, CONTROL, NOINVERT END RESOURCE.H (fragmenty) // Microsoft Developer Studio generated include file. // Used by ClipText.rc tidefine IDM_EDIT_CUT 40001 4ldefine IDM_EDIT_COPY 40002 tldefine IDM_EDIT_PASTE 40003 ttdefine IDM_EDIT_CLEAR 40004 tldefine IDM EDIT RESET 40005 Rysunek 12-1. Program CLIPTEXT ţ Celem jest tutaj uruchomienie wersji Unicode i ANSI programu w Windows NT i zaobserwowanie, jak Schowek wykonuje konwersje między tymi zestawami znaków. Zwróć uwagę na instrukcję #ifdef na poczštku pliku CLIPT'EXT.C. Jeœli zdefiniowany jest identyfikator UNICODE, wtedy CF TCHAR (nazwa ogólne- go, utworzonego przeze mnie formatu danych Schowka) równy jest CF_UNICO- DETEXT. W przeciwnym razie CF_TCHAR zgodny jest z CF TEXT. Wywołania funkcji IsClipboardFormatAvailable, GetClipboardData i SetClipboardData w dalszej częœci programu używajš nazwy CF TCHAR do okreœlenia typu danych. Po uruchomieniu programu (lub gdy wybierzesz opcję Reset z menu Edit) zmienna pText będzie zawierała wskaŸnik do cišgu znaków Unicode "Default Text - Uni- code version" w wersji Unicode programu lub do cišgu "Default Text - ANSI version" w drugiej wersji. Możesz użyć poleceń Cut i Copy, aby przenieœć cišg tekstowy do Schowka, albo poleceń Cut i Delete, aby usunšć cišg z programu. Polecenie Paste kopiuje dowolnš zawartoœć tekstowš Schowka do pText. Cišg pText jest wyœwietlany w obszarze roboczym programu podczas przetwarzania komu- nikatu WM PAINT. Rozdział 12: Schowek 525 Jeżeli najpierw wybierzesz polecenie Copy z wersji Unicode programu CLIPTEXT, a następnie polecenie Paste z drugiej wersji, tekst został nie przekonwertowany z Unicode na ANSI. Analogicznie, jeœli wybierzesz odwrotne polecenia, tekst zo- stanie przekonwertowany z ANSI na Unicode. Poza standardowymi zastosowaniami Schowka Wiemy już, że przeniesienie tekstu ze Schowka wymaga czterech wywołań po przygotowaniu danych. OpenClipboard (hwnd) ; EmptyClipboard () ; SetClipboardData (iFormat, hGlobal) ; CloseClipboard () ; Natomiast uzyskanie dostępu do tych danych wymaga trzech wywołań: OpenClipboard (hwnd) ; hGlobal = GetClipboardData (iFormat) ; [pozostaie wiersze programuJ CloseClipboard () ; Możesz wykonać kopię danych ze Schowka lub użyć ich w inny sposób między wywołaniami GetClipboardData i CloseClipboard. Takie rozwišzanie jest wystarcza- jšce w większoœci przypadków, ale Schowek można wykorzystać także w spo- sób bardziej zaawansowany. Używanie kilku elementów danych Gdy otwierasz Schowek, aby umieœcić w nim dane, musisz wywołać funkcję Emp- tyClipboard, która powoduje, że Windows zwalnia lub usuwa jego zawartoœć. Nie możesz niczego dodać do istniejšcej zawartoœci Schowka. W takim rozumieniu prze- chowuje on tylko jeden element danych naraz. Pomiędzy wywołaniami EmptyClip- board i CIoseCliboard możesz jednak wywołać kilka razy funkcję SetClipboardData, używajšc za każdym razem różnego formatu danych. Jeœli chcesz na przykład przechować w Schowku krótki cišg tekstowy, możesz zapisać go w metapliku lub bitmapie. W ten sposób udostępniasz ten cišg nie tylko programom, które mogš odczytać tekst ze Schowka, ale także tym, które mogš odczytać metapliki i bitmapy. Oczywiœcie, programy te nie będš w stanie w prosty sposób rozpo- znać, czy metaplik lub bitmapa rzeczywiœcie zawiera cišg znaków. Jeżeli chcesz utworzyć kilka uchwytów Schowka, możesz dla każdego z nich wywołać funk- cję SetClipboardData: OpenClipboard (hwnd) ; EmptyClipboard () ; SetClipboardData (CF_TEXT, hGlobalText) ; SetClipboardData (CF_BITMAP, hBitmap) ; SetClipboardData (CF METAFILEPICT, hGlobalMFPţ , CloseClipboard () ; Gdy w Schowku znajdš się te trzy formaty danych, funkcja IsClipboardFormatA- vailable zwróci wartoœć TRUE dla argumentów CF TEXT, CF BITMAP i CF ME- TAFILEPICT. Program może uzyskać dostęp do tych uchwytów przez wywoła- nie: 526 Częœć I: Podstawy hGlobalText = GetClipboardData (CF TEXT) ; lub hBitmap = GetClipboardData (CF BITMAP) ; lub hGlobalMFP = GetClipboardData (CF METAFILEPICT) ; Przy kolejnym wywołaniu EmptyClipboard Windows zwolni lub usunie wszyst- kie trzy uchwyty używane przez Schowek. Nie używaj tej techniki, aby umieœcić w Schowku różne formaty tekstu, różne formaty bitmapy lub różne formaty metapliku. Korzystaj tylko z jednego forma- tu tekstu, jednego formatu bitmapy i jednego formatu metapliku. Jak wspomina- łem, Windows dokonuje konwersji formatu CF TEXT na CF OEMTEXT i CF UM- CODETEXT. Wykonywana jest także konwersja między formatami CF BITMAP i CF DIB oraz między CF METAFILEPICT i CF ENHMETAFILE. Program może sprawdzić, jakie formaty danych przechowywane sš w Schowku, otwierajšc go i wywołujšc funkcję EnumClipboardFormats. Rozpocznij od ustawienia zmiennej iFormat na 0: IFormat = 0 ; OpenClipboard (hwnd) ; Następnie wykonaj kolejne wywołania EnumCIipboardFormats, zaczynajšc od war- toœci 0. Funkcja ta zwróci dodatniš wartoœć iFormat dla każdego formatu prze- chowywanego aktualnie w Schowku. Gdy zwróci 0, będzie to oznaczało, że prze- tworzyłeœ wszystkie formaty: while (iFormat = EnumClipboardFormats (iFormat)) ł. Clogiczny d1a każdej wartoţci iFormat] } CloseClipboard () ; Liczbę różnych formatów dostępnych aktualnie w Schowku możesz uzyskać wywołujšc: iCount = CountClipboardFormats () : OpóŸnione przenoszenie Gdy umieszczasz dane w Schowku, tworzysz w rzeczywistoœci ich kopię i prze- kazujesz mu uchwyt globalnego bloku pamięci, zawierajšcego tę kopię. Dla bar- dzo dużych elementów danych technika ta powoduje niepotrzebne zużycie pa- mięci. Jeœli użytkownik nie zamierza umieœcić danych w innym programie, pa- mięć będzie zajęta, aż do momentu, gdy zostanš one zastšpione innymi. Problemu tego można uruknšć stosujšc technikę nazwanš opóŸnionym przeno- szeniem, w której program przekazuje dane dopiero wtedy, gdy zażšda ich inna aplikacja. Zamiast przekazywać systemowi Windows uchwyt danych, możesz użyć wartoœci NULL w wywołaniu SetClipboardData: OpenClipboard (hwnd) ; EmptyClipboard () ; SetClipboard (iFormat, NULL) ; CloseClipboard () ; Rozdział 12: Schowek 527 Możesz wpisać kilka wywołań SetClipboardData używajšc różnych wartoœci iFor- mat. W niektórych możesz wykorzystać wartoœć NULL, a w pozostałych - praw- dziwe uchwyty. Dotychczasowa częœć jest prosta, ale w tym punkcie proces staje się bardziej skom- plikowany. Gdy inny program wywołuje funkcję GetClipboardData, Windows sprawdza, czy uchwyt danego formatu jest równy NULL. jeżeli tak nie jest, Win- dows wysyła komunikat do właœciciela Schowka (twojego programu) z pytaniem o prawdziwy uchwyt danych. Program musi wtedy dostarczyć ten uchwyt. Właœciciel Schowka to w rzeczywistoœci ostatnie okno, które umieœciło dane w Schowku. Gdy program wywołuje funkcję OpenClipboard, Windows zachowu- je uchwyt wymagany przez tę funkcję. Uchwyt identyfikuje okno, które otwo- rzyło Schowek. Po odebraniu wywołania EmptyClipboard Windows nadaje temu oknu status nowego właœciciela Schowka. Program używajšcy opóŸnionego przenoszenia musi przetworzyć w procedurze okna trzy komunikaty: WMţRENDERFORMAT, WMţRENDERALLFORMATS i WM-DESTROYCLIPBOARD. Windows wysyła procedurze okna komunikat WM-RENDERFORMAT, gdy inny program wywołuje funkcję GetClipboardData. Wartoœć wParam jest wymaganym formatem. Gdy przetwarzasz komunikat WM-RENDERFORMAT, nie otwieraj i nie usuwaj zawartoœci Schowka. Po pro- stu utwórz globalny blok pamięci dla formatu okreœlonego przez wParam, prze- nieœ do niego dane i wywołaj funkcję SetClipboardData z odpowiednim formatem i uchwytem globalnym. Musisz oczywiœcie zachować te informacje w programie, aby poprawnie skonstruować dane podczas przetwarzania komunikatu WM-RENDERFORMAT. Kiedy inny program wywołuje funkcję EmptyClipboard, Windows wysyła do programu komunikat WM DESTROYCLIPBOARD. Mówi on, że informacje potrzebne do skonstruowania danych Schowka nie sš już po- trzebne. Nie jesteœ już jego właœcicielem. Jeœli program zakończy działanie, będšc właœcicielem Schowka, a Schowek za- wiera uchwyty danych o wartoœci NULL, ustawione przez funkcję SetClipboard- Data, odbierzesz komunikat WM-RENDERALLFORMATS. Powinieneœ otworzyć Schowek, usunšć jego zawartoœć, umieœcić dane w globalnych blokach pamięci, dla każdego formatu wywołać funkcję SetClipboardData i zamknšć go. Komuni- kat WM RENDERALLFORMATS jest jednym z ostatnich, które odbiera proce- dura okna. Po nim następuje WM-DESTROYCLIPBOARD (ponieważ przetwo- rzyłeœ wszystkie dane) i na koniec zwykły komunikat WM DESTROY. Jeżeli program może przekazywać do Schowka tylko jeden format danych (na przykład tekst), możesz połšczyć przetwarzanie komunikatów WM-RENDERAL- LFORMATS i WM RENDERFORMAT. Kod będzie wtedy wyglšdał podobnie do poniższego: case WMţRENDERALLFORMATS : OpenClipboard (hwnd) ; EmptyClipboard () ; , // nie powiodło się case WM_RENDERFORMAT : fumieszczenie tekstu w globalnym bloku pamięci] SetClipboardData (CF TEXT, hGlobal) ; 528 Częœć I: Podstawy if (message == WM_RENDERALLFORMATS) CloseClipboard () ; return 0 ; Jeœli program używa kilku typów danych, komunikat WMţRENDERFORMAT należy przetwarzać tylko dla formatu wymaganego przez wParam. Nie należy przetwarzać komunikatu WMţDESTROYCLIPBOARD, chyba że przeszkadza on programowi uzyskać informacje potrzebne do skonstruowania danych. Własne formaty danych Do tej pory zajmowaliœmy się tylko standardowymi formatami danychSchowka zdefiniowanymi przez Windows. Możesz jednak użyć Schowka do przechowa- nia własnych formatów danych. Wiele procesorów tekstów używa tej techniki do przechowania tekstu z informacjš o czcionce i formatowaniu. Na pierwszy rzut oka pomysł ten może wydawać się bezsensowny. Jeœli zada- niem Schowka jest przenoszenie danych między aplikacjami, dlaczego mieliby- œmy umieszczać w nim dane wykorzystywane tylko przez jeden program? Od- powiedŸ jest prosta: Schowek służy również do przenoszenia danych wewnštrz jednego programu (lub między kilkoma jego kopiami), który, oczywiœcie, właœci- wie rozpoznaje swój własny format. Dostępne jest kilka metod wykorzystania własnych formatów danych. Najłatwiej- sza używa danych w jednym ze standardowych formatów Schowka (tzn. tekst, bitmapa lub metaplik), ale majšcych znaczenie tylko dla twojego programu. W takim przypadku w wywołaniach SetClipboardData i GetClipboardData używa się jednej z następujšcych wartoœci wFormat: CF DSPTEXT, CF DSPBITMAP, CF DSPMETAFILEPICT lub CF DSPENHMETAFILE. Litery DSP to skrót od ang. display (wyœwietlanie). Formaty te pozwalajš Schowkowi Windows wyœwietlać dane jako tekst, bitmapę lub metaplik. Jeœli jednak inny program wywoła funkcję GetClipboardData przy użyciu zwykłe- go formatu CF_TEXT, CF BITMAP, CF DIB, CF METAFILEPICT lub CF_ENH- METAFILE, nie uzyska danych. Jeżeli użyjesz jednego z tych formatów, aby umieœcić dane w Schowku, musisz zastosować taki sam format, aby te dane pobrać. Skšd jednak wiemy, czy dane pochodzš z kopii twojego programu czy z innego programu, używajšcego jedne- go z tych formatów? Jednym ze sposobów jest sprawdzenie właœciciela Schowka przez wywołanie: hwndClipOwner = GetClipboardOwner () : Następnie możesz uzyskać nazwę klasy okna dla tego uchwytu okna: TCHAR szClassName [32] ; Cpozostaie wiersze programu7 GetClassName (hwndClipOwner, szClassName, 32) ; Jeœli nazwa klasy jest taka sama jak programu, wtedy dane zostały umieszczone w Schowku przez innš kopię twojego programu. Innym sposobem jest użycie własnych formatów za pomocš flagi CF OWNER- DISPLAY. Globalny uchwyt pamięci w funkcji SetClipboardData równy jest NULL: Rozdział 12: Schowek 529 SetClipboardData (CF OWNERDISPLAY, NULL) ; Metody tej używajš niektóre procesory tekstu, aby wyœwietlić sformatowany tekst w obszarze roboczym podglšdu Schowka dostarczanego z Windows. Oczywiœcie podglšd Schowka nie wie, że wyœwietla sformatowany tekst. Z używaniem flagi CF OWNERDISPLAY przez procesor tekstu zwišzane jest wyœwietlenie obszaru roboczego podglšdu Schowka. Ponieważ globalny uchwyt pamięci równy jest NULL, program wywołujšcy funkcję SetClipboardData z formatem CF OWNERDISPLAY (właœciciel Schowka) musi prze- tworzyć komunikaty opóŸnionego przenoszenia wysyłane przez Windows do wła- œciciela Schowka oraz pięć innych, dodatkowych, które wysyła podglšd Schowka: ů WM ŽSKCBFORMATNAME Podglšd Schowka wysyła ten komunikat do wła- œciciela Schowka, aby uzyskać nazwę formatu danych. Parametr lParam jest wskaŸnikiem bufora, a wParam - maksymalnš liczbš znaków dla tego bufora. Właœciciel Schowka musi skopiować nazwę formatu Schowka do tego bufora. ů WM SIZECLIPBOARD Komunikat ten mówi właœcicielowi Schowka, że zmie- nił się rozmiar obszaru roboczego podglšdu Schowka. Parametr wParam jest uchwytem tego podglšdu, a lParam - wskaŸnikiem do struktury RECT, za- wierajšcš nowy rozmiar. Jeżeli struktura RECT zawiera same zera, podglšd Schowka jest zminimalizowany lub zniszczony. Mimo że można uruchomić tylko jednš kopię danego podglšdu Schowka Windows, inne takie podglšdy także mogš przesłać ten komunikat do właœciciela Schowka. Obsługa kilku podglšdów Schowka nie jest dla niego niemożliwa (zważywszy na to, że wPa- ram identyfikuje poszczególne podglšdy), ale nie jest także łatwa. ů WM PAINTCLIPBOARD Komunikat ten mówi właœcicielowi Schowka, aby od- œwieżył obszar roboczy podglšdu Schowka. Ponownie wParam jest uchwytem do okna tego podglšdu. Parametr IParam jest uchwytem gtóbalnym do struktu- ry PAáV'TSTRUCT. Właœciciel Schowka może zablokować uchwyt i uzyskać go do kontekstu urzšdzenia podglšdu Schowka za pomocš pola hdc tej struktury. ů WM HSCROLLCLIPBOARD i WMţVSCROLLCLIPBOARD Komunikaty te in- formujš właœciciela Schowka, że użytkownik użył pasków przewijania pod- glšdu Schowka. Parametr wParam jest uchwytem okna tego podglšdu, mniej znaczšce słowo lParam jest żšdaniem przewijania, a bardziej znaczšce - po- zycjš miniatury, jeœli mniej znaczšce słowo jest równe SB THUMBPOSITION. Przetwarzanie tych komunikatów może wydawać się niewiele warte. Ma jednak pewne zalety dla użytkownika: kopiujšc tekst z procesora tekstów do Schowka, będzie czuł się pewniej, jeœli zobaczy, że tekst jest sformatowany również w ob- szarze roboczym Schowka. Kolejnym sposobem korzystania z własnych formatów danych Schowka jest za- rejestrowanie własnej nazwy formatu. Nazwę formatu podajesz systemowi Win- dows, a on zwraca programowi numer, którego używa się jako parametru w funk- cjach SetCliboardData i GetCliboardData. Programy używajšce tej metody także kopiujš dane w jednym ze standardowych formatów. Rozwišzanie to umożliwia podglšdowi Schowka wyœwietlanie danych w obszarze roboczym (z uniknięciem kłopotów występujšcych przy CF OWNERDISPLAY) i pozwala innym progra- mom kopiować dane ze Schowka. 530 Częœć I: Podstawy Załóżmy, że napisaliœmy program do grafiki wektorowej, który kopiuje dane do Schowka w formacie bitmapy, metapliku i we własnym zarejestrowanym forma- cie. Podglšd Schowka wyœwietli metaplik i bitmapę. Inne programy, mogšce od- czytać metapliki i bitmapy ze Schowka, także obsłużš te formaty. Jeżeli jednak nasz program potrzebuje odczytać dane ze Schowka, skopiuje je we własnym formacie, ponieważ zawiera on prawdopodobnie więcej informacji niż bitmapa czy metaplik. Program rejestruje nowy format Schowka przez wywołanie: iFormat = RegisterClipboardFormat (szFormatName) ; Wartoœć iFormat mieœci się w zakresie od 0xC000 do OxFFFF. Podglšd Schowka (lub program odbierajšcy wszystkie bieżšce formaty w Schowku przez wywoła- nie EnumClipboardFormats) może uzyskać nazwę ASCII formatu przez wywoła- nie: GetClipboardFormatName (iFormat, psBuffer, iMaxCount) ; Windows kopiuje maksymalnie iMaxCount znaków do psBuffer. Programiœci używajšcy tej metody do kopiowania danych do Schowka mogš udokumentować nazwę formatu i rzeczywisty format danych. Kiedy program stanie się populamy, inne programy będš mogły kopiować dane ze Schowka w tym formacie. Tworzenie podglšdu Schowka Program powiadamiany o zmianach zawartoœci Schowka nazywany jest pod- glšdem Schowka. Dostarczany jest z Windows, ale równie dobrze możesz napi- sać swój własny program. Podglšd Schowka powiadamiany jest o zmianach za- wartoœci Schowka przez komunikaty przekazywane do procedury okna pod- glšdu. Łańcuch podglšdu Schowka Jednoczeœnie można uruchomić w Windows dowolnš liczbę podglšdów Schow- ka i wszystkie z ruch można powiadomić o zmianach jego zawartoœci. Z perspek- tywy tego systemu istnieje jednak tylko jeden podglšd Schowka, który nazywam aktualnym podglšdem Schowka". Windows zarzšdza tylko jednym uchwytem " do identyfikacji aktualnego podglšdu Schowka i tylko do tego okna wysyła ko- munikaty, gdy zmieni się zawartoœć Schowka.Aplikacje tego podglšdu stanowiš częœć łańcucha podglšdu Schowka i wszystkie mogš odbierać komunikaty, które Windows wysyła do podglšdu bieżšcego. Gdy program zarejestruje się jako pod- glšd Schowka, staje się podglšdem bieżšcym. Windows nadaje mu uchwyt okna podglšdu Schowka, który poprzednio był podglšdem bieżšcym. Program zapi- suje ten uchwyt. Po odebraniu przez program komunikatu podglšdu Schowka, przesyła go do procedury okna programu, który umieszczony jest jako następny w łańcuchu tego podglšdu. Rozdział 12: Schowek 531 Funkcje i komunikaty podglšdu Schowka Program może stać się częœciš łańcucha podglšdu Schowka wywołujšc funkcję SetC- lipboardViewer. jeœli podstawowym zadaniem programu ma być działanie jako pod- glšd Schowka, może wywołać tę funkcję podczas przetwarzania komunikatu WMţCREATE. Funkcja zwraca uchwyt okna podglšdu Schowka, który poprzed- nio był bieżšcym. Program powinien zapisać ten uchwyt w zmiennej statycznej: static HWND hwndNextUiewer ; Cpozostałe wiersze programuJ case WM_CREATE : Cpozostaie wiersze programuJ hwndNextViewer = SetClipboardViewer (hwnd) ; Jeżeli twój program jako pierwszy ma stać się podglšdem Schowka podczas sesji Windows, zmienna hwndNextViewer powinna być równa NULL. Windows wysyła komunikat WM DRAWCLIPBOARD do bieżšcego podglšdu Schowka (tzn. ostatniego okna, które zrejestrowało sięjako jego podglšd) za każdym razem, gdy zmieni się zawartoœć Schowka. Każdy program w łańcuchu podglšdu Schowka powinien używać funkcji SendMessage, aby przekazać ten komunikat do następnego jego podglšdu. Ostatru program w tym łańcuchu (tzn. pierwsze okno, które zarejestrowało się jako podglšd Schowka) będzie przechowywać wartoœć hwnd- NextViewer równš NULL. Jeœli wartoœć hwndNextViewer jest równa NULL, program kończy działanie bez wysyłania komunikatu do innego programu. (Nie pomyl ze sobš komunikatów WMţDRAWCLIPBOARD i WMţPAINTCLá'BOARD. Komuni- kat WMţPAINTCLá'BOARD jest wysyłany przez podglšd Schowka do programów, które używajš formatu CF OWNERDISPLAY. Natomiast komunikat WMţDRAWC- LIl'BOARD jest wysyłany przez Windows do bieżšcego podglšdu Schowka). Najprostszym sposobem przetworzenia komunikatu WM DRAWCLIPBOARD jest wysłanie go do następnego w łańcuchu podglšdu Schowka (chyba że war- toœć hwndNextViewer jest równa NULL) i uaktualnienie obszaru roboczego twoje- go okna: case WM_DRAWCLIPBOARD : if (hwndNextViewer) SendMessage (hwndNextViewer. message, wParam, lParam) ; InvalidateRect (hwnd, NULL, TRUE) ; return 0 ; Podczas przetwarzania komunikatu WMţPAINT możesz odczytać zawartoœć Schowka używajšc zwykłych wywołań OpenClipboard, GetClipboardData i CIoseClip- board. Gdy program chce usunšć się z łańcucha podglšdu Schowka, musi wywołać funk- cję ChangeClipboardChain. Funkcja ta wymaga uchwytu okna programu, który wy- cofuje się z łańcucha podglšdu Schowka i uchwytu okna następnego w kolejno- œci jego podglšdu: ChangeClipboardChain (hwnd, hwndNextUiewer) ; Kiedy program wywohxje funkcję ChangeClipboardChain, Windows wysyła komu- nikat WM CHANGECBCHAIN do bieżšcego podglšdu Schowka. Parametr wPa- ram jest uchwytem okna, które usuwa się z łańcucha (tzn. pierwszym parame- 532 Częœć I: Podstawy trem funkcji ChangeClipboardChain), a parametr IParam jest uchwytem okna pod- glšdu Schowka następujšcego w łańcuchu za podglšdem usuwanym (tzn. dru- gim parametrem funkcji ChangeClipboardChain). Gdy program odbierze komunikat WM CHANGECBCHAIN, musi sprawdzić, czy parametr wParam jest równy wartoœci hwndNextViewer, którš wczeœniej zapi- sałeœ. Jeœli tak jest, musisz ustawić hwndNextViewer na IParam. Operacja ta zapew- nia, że do okna usuwajšcego się z łańcucha nie zostanš wysłane komunikaty 4VMţDRAWCLIPBOARD. Jeżeli wParam nie jest równy hwndNextViewer, a hwnd- NextViewer nie jest równy NULL, wyœlij komunikat do następnego podglšdu Schowka: case WM_CHANGECBCHAIN : if ((HWND) wParam = hwndNextViewer) hwndnNextViewer = (HWND) lParam else if (hwndNextViewer) SendMessage (hwndNextViewer, message, wParam, lParam) ; return 0 ; W rzeczywistoœci nie ma potrzeby używania instrukcji else if, która sprawdza, czy zmienna hwndNextViewer nie zawiera wartoœci NULL. Wartoœć NULL dla tej zmiennej oznaczałaby, że program wykonujšcy kod jest ostatnim podglšdem w łańcuchu. W takim przypadku komunikat nigdy nie powinien pojawić się tak daleko. Jeœli podczas zamykania program nadal znajduje się w łańcuchu podglšdu Schow- ka, powinien zostać z niego usunięty. Można to wykonać przetwarzajšc komuni- kat WM DESTROY przez wywołanie funkcji ChangeClipboardChain: case WM_DESTROY : ChangeClipboardChain (hwnd, hwndNextViewer) ; PostOuitMessage (0) ; return 0 ; Windows dostarcza także funkcję, która pozwala programowi uzyskać uchwyt okna pierwszego podglšdu Schowka: hwndViewer = GetClipboardViewer () ; Zwykle funkcja ta nie jest potrzebna. Jeżeli bieżšcy Schowek nie jest ustawiony, funkcja zwraca wartoœć NULL. Poniższy przykład ilustruje działanie łańcucha Schowka. Gdy Windows jest uru- chamiany po raz pierwszy, bieżšcy podglšd Schowka ma wartoœć NULL: Bieżgcy podglgd Schowka: NULL Program, którego uchwytem okna jest hwndl, wywołuje funkcję SetClipboardVie- wer. Funkcja zwraca wartoœć NULL, która staje się w programie wartoœciš hwnd- NextViewer: Bieżšcy podglgd Schowka: hwndl Następny podglgd za hwndl: NULL Drugi program, którego uchwytem okna jest hwnd2, także wywołuje funkcję Set- CipboardViewer. Zwraca ona uchwyt hwndl: Rozdział 12: Schowek 533 Bieżgcy podglgd Schowka: hwnd2 Następny podglgd za hwnd2: hwndl Nastgpny podglgd za hwndl: NULL ţ Trzeci program (hwnd3), a następnie czwarty (hwnd4) również wywołujš funkcję SetClipboardViewer, która zwraca odpowiednio hwnd2 i hwnd3: ţI Bieżgcy podglgd Schowka: hwnd4 Następny podglgd za hwnd4: hwnd3 i :'. Następny podglgd za hwnd3: hwnd2 Następny podglgd za hwnd2: hwndl Następny podglgd za hwndl: NULL Gdy zawartoœć Schowka zmieni się, Windows wysyła komunikat WM DRAWCLIP- BOAIZD do hwnd4, hwnd4 wysyła komunikat do hwnd3, hwnd3 - do hwnd2, hwnd2 - do hwndl, a hwndl kończy ten proces. Jeżeli hwnd2 zdecyduje usunšć się ze Schowka, wywołujšc funkcję: ChangeClipboardChain (hwnd2, krwndl) : to Windows wyœle do hwnd4 komunikat WM CHANGECBCHAIN z parametrem wParam równym hwnd2 i parametrem IParam równym 1. Ponieważ następnym podglšdem Schowka za hwnd4 jest hwnd3, hwnd4 wysyła komunikat do hwnd3. Następnie hwnd3 zauważa, że wParam jest równy następnemu podglšdowi (hwnd2), ustawia swój następny podglšd Schowka na równy IParam (hwndl) i koń- czy ten proces. Operacja została wykonana. Łańcuch podglšdu Schowka wyglš- da teraz następujšco: Bieżgcy podglgd Schowka: hwnd4 ' Następny podglgd za hwnd4: hwnd3 Następny podglgd za hwnd3: hwndl Następny podglgd za hwndl: NULL Prosty podglšd Schowka Podglšdy Schowka nie muszš być aż tak rozbudowane jak ten dostarczany z Win- dows. Podglšd Schowka może wyœwietlać na przykład tylko jeden format da- nych. Program CLIPVIEW, pokazany na rysunku 12-2, jest podglšdem Schowka, który wyœwietla tylko format CF TEXT. CLIPVIEW.C i* CLIPVIEW.C - Prosty podglšd Schowka (c) Charles Petzold, 1998 */ ilinclude LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; 534 Częœć 1: Podstawy (cišg dalszy ze strony 533) int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) ( static TCHAR szAppName[] = TEXT ("ClipView") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CS VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITEţBRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) ( MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MBţICONERROR) ; return 0 ; ) hwnd = CreateWindow (szAppName, TEXT ("Simple Clipboard Viewer (Text Only)"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW USEDEFAULT, CW_USEDEFAULT, CW USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) ( TranslateMessage (&msg) ; DispatchMessage (&msg) ; return msg.wParam ; ) LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ? static HWND hwndNextViewer ; HGLOBAL hGlobal ; HDC hdc ; . PTSTR pGlobal ; PAINTSTRUCT ps ; RECT rect ; switch (messa9e) ( case WMţCREATE: Rozdział 12: Schowek 535 hwndNextViewer = SetClipboardViewer (hwnd) ; return 0 ; case WM_CHANGECBCHAIN: if ((HWND) wParam == hwndNextViewer) hwndNextViewer = (HWND) lParam ; else if (hwndNextUiewer) SendMessage (hwndNextUiewer, message, wParam, lParam) ; return 0 ; case WM_DRAWCLIPBOARD: if (hwndNextViewer) SendMessage (hwndNextViewer, message, wParam, lParam) ; InvalidateRect (hwnd, NULL, TRUE) ; return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; GetClientRect (hwnd, &rect) ; OpenClipboard (hwnd) ; ţţifdef UNICODE hGlobal = GetClipboardData (CF UNICODETEXT) ; else hGlobal = GetClipboardData (CF TEXT) ; #endif if (hGlobal != NULL) pGlobal = (PTSTR) GlobalLock (hGlobal) ; DrawText (hdc, pGlobal, -1, &rect, DT EXPANDTABS) ; GlobalUnlock (hGlobal) ; 1 CloseClipboard () ; EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: ChangeClipboardChain (hwnd, hwndNextViewer) ; PostOuitMessage (0) ; return 0 ; return DefWindowProc (hwnd, message, wParam, lParam) ; J Program CLIPVIEW przetwarza komunikaty WMţCREATE, WMţCHANGECB- CHAIN, WM DRAWCLIPBOARD i WMţDESTROY w omówiony wczeœniej spo- sób. Komunikat WMţPAIN'T otwiera po prostu Schowek i używa funkcji GetC- IipboardData z formatem CF TEXT: Jeœli funkcja zwróci globalny uchwyt pamię- ci, program CLIPVIEW zablokuje go i użyje funkcji DrawText, aby wyœwietlić tekst w obszarze roboczym. Podglšd Schowka, obsługujšcy niestandardowe formaty danych (np. podglšd Schowka dostarczany z Windows), musi wykonywać dodatkowe zadania, takie 536 Częœć I: Podstawy jak wyœwietlanie nazw wszystkich formatów dostępnych w Schowku. Można to zrobić wywołujšc funkcje EnumClipboardFormats i uzyskujšc nazwy niestandar- dowych formatów za pomocš funkcji GetClipboardFormatName. Podglšd Schow- ka, używajšcy formatu CF OWNERDISPLAY, aby wyœwietlić dane, musi prze- słać następujšce cztery komunikaty do właœciciela Schowka: WM PAIN'TCLIPBOARD M VSCROLLCLIPBOARD WM SIZECLIPBOARD WM HSCROLLCLIPBOARD Jeżeli chcesz napisać taki podglšd Schowka, musisz uzyskać uchwyt okna wła- œciciela Schowka, stosujšc funkcję GetClipboardOwner, i wysłać do tego okna po- wyższe komunikaty w momencie odœwieżania obszaru roboczego podglšdu Schowka. Częœć II . ţ ra ţ a i i Rozdział 13 Druk w ni o a e Idea niezależnoœci od urzšdzeń może wydawać się w pełru przemyœlana i dobra, kiedy wykorzystujemy jš do wyœwietlania tekstu i grafiki. Ale w jakim stopniu sprawdza się w odniesieniu do drukowania? Zasadniczo wieœci sš pomyœlne. Korzystajšc z programów Microsoft Windows można drukować tekst i grafikę na papierze przy użyciu takich samych funkcji GDI, jakie stosowaliœmy do wyœwietlania na ekranie. Do większoœci zagadnień zwišzanych z niezależnoœciš od urzšdzeń, które do tej pory omówiliœmy - wiele z nich wišzało się z rozmiarem i rozdzielczoœciš obszaru wyœwietlania oraz jego możliwoœciami wyœwietlania kolorów - można podejœć w podobny sposób i po- dobnie je rozwišzać. Jednak drukarka nie jest po prostu urzšdzeniem stosujšcym do wyœwietlania papier zamiast kineskopu. Jest kilka znaczšcych różnic. Na przy- kład nigdy nie zetkniemy się z problemem monitora nie podłšczonego do karty graficznej lub zagadnieniem "skończenia się" ekranu w monitorze, natomiast sytuacje, gdy drukarka nie jest podłšczona lub gdy skończył się w niej papier, ;.. zdarzajš się często. Nie martwimy się też, czy karta graficzna będzie w stanie wykonać okreœlone operacje graficzne. Karta graficzna potrafi obsługiwać grafikę albo nie. I jeœli nie, to w ogóle nie może współpracować z systemem Windows. Pewne drukarki na- tomiast nię sš w stanie drukować grafiki (mimo to mogš współpracować z Win- dows), plotery obsługujš grafikę wektorowš, ale majš poważne problemy z bit- mapami. Oto inne zagadnienia, które należy rozważyć: ů Drukarki pracujš wolniej od monitorów. Czasami dostraja się programy tak, by uzyskać optymalnš szybkoœć ich działania, ale nie zwraca się wtedy uwa- gi na czas niezbędny na odœwieżenie ekranu. Natomiast nikt nie zechce cze- kać, aż powolna drukarka zakończy drukowanie, zarum będzie mógł powró- cić do pracy. ů Programy wielokrotnie wykorzystujš powierzchnię ekranu, gdy nadpisujš poprzednie dane wyœwietlone na ekranie nowymi danymi. Nie można tak zrobić w przypadku drukarki. Musi ona wyrzucić stronę, którš skończyła dru- kować, i dopiero wtedy przejœć do nowej. ů Na monitorze różne aplikacje umieszczane sš w oknach. Na drukarce dane pochodzšce z różnych aplikacji muszš być podzielone na odrębne dokumen- ty lub zadania drukowania. Aby do reszty GDI dodać obsługę drukarki, Windows udostępnia szereg funkcji majšcych zastosowanie wyłšcznie do drukarek. Funkcje charakterystyczne dla 540 Częœć II: Grafika drukarek - StartDoc, EndDoc, StartPage i EndPage - sš odpowiedzialne za organi- zację danych przesyłanych do drukarki w postaci stron. Program wywołuje zwy- kłš funkcję GDI do wyœwietlenia tekstu i grafiki na stronie w taki sam sposób, w jaki robi to, aby wyœwietlić je na ekranie. IW zdziały 15, 17 i 18 zawierajš dodatkowe informacje na temat drukowania bit- map, sformatowanego tekstu oraz metaplików. Podstawy drukowania Gdy w Windows używasz drukarki, rozpoczynasz złożonš interakcję, wymaga- jšcš modułu biblioteki GDI32, moduhz sterownika urzšdzenia drukujšcego (ma- jšcego rozszerzenie .DIZV) oraz buforowania wydruku systemu Windows, a tak- że kilku innych modułów, bioršcych udział w zdarzeniu. Zanim zaczniemy pro- gramowanie dla drukarki, przeœledŸmy, jak przebiega ten proces. Drukowanie a buforowanie Kiedy program drukujšcy chce rozpoczšć korzystanie z drukarki, najpierw za pomocš funkcji CreateDC lub PrintDlg uzyskuje uchwyt kontekstu urzšdzenia drukujšcego. Powoduje to wczytanie do pamięci moduhx biblioteki sterownika urzšdzenia drukujšcego (jeœli jeszcze go tam nie ma) oraz jego samoinicjację. Następnie program wywołuje funkcję StartDoc, oznaczajšcš poczštek nowego dokumentu. Funkcja StartDoc znajduje się w module GDI. Moduł ten wywohzje funkcję Control w sterowniku urzšdzenia drukujšcego, polecajšc mu przygoto- wanie się do drukowania. Wywołanie StartDoc rozpoczyna proces drukowania dokumentu, który kończy działanie, gdy program wywoła funkcję EndDoc. Te dwa wywołania działajš jak okładki ksišżki w przypadku zwykłych funkcji GDI, wyœwietlajšcych tekst lub grafikę na stronie dokumentu. Każda ze stron ograniczona jest przez wywołanie StartPage, rozpoczynajšce stronę, oraz EndPage na końcu strony. Jeżeli program ma zlecenie na przykład narysowania na stronie elipsy, najpierw wywołuje StartDoc, aby rozpoczšć zadanie drukowania, następnie StartPage, by zasygnalizować nowš stronę. Następnie wywohzje Ellipse, tak jak to czyni rysu- jšc elipsę na ekranie. Na ogół moduł GDI przechowuje każde wywołanie GDI, które program kieruje do kontekstu urzšdzenia drukujšcego, w metapliku dys- kowym o nazwie rozpoczynajšcej się od znaków ~EMF ("rozszerzony metaplik") oraz rozszerzeniu .TMP. Możliwe jest jednak, co pokrótce omówię, ominięcie tego kroku przez sterownik drukarki. Gdy program aplikacji skończy z wywołaniami GDI definiujšcymi pierwszš stro- nę, wówczas wywołuje EndPage. Teraz rozpoczyna się rzeczywista praca. Sterow- nik drukarki musi przetłumaczyć poszczególne polecenia rysujšce zapisane w me- tapliku na wyjœcie dla drukarki. Wyjœcie drukarki wymagane do zdefiniowania strony grafiki może być bardzo duże, szczególnie jeœli drukarka nie ma języka wysokiego poziomu do kompozycji strony. Na przykład drukarka laserowa o roz- dzielczoœci 600 dpi, korzystajšca z papieru formatu A4, może wymagać ponad 4 megabajtów pairuęci do zdefiniowania pojedynczej strony grafiki. Rozdział 13: Drukowanie 541 Z tego powodu sterowniki drukarek często wykorzystujš technikę zwanš "pa- smowaniem", dzielšcš stronę na prostokštne pasma. Moduł GDI otrzymuje od sterownika drukarki wymiary każdego z pasm. Wówczas ustala obszar, jaki ma być przechowywany, równy temu pasmu, a następnie wywohxje funkcję Output sterownika urzšdzenia drukujšcego dla każdej funkcji rysujšcej zawartej w me- tapliku. Proces ten nazywany jest wgrywaniem metapliku do sterownika urzš- dzenia. Moduł GDI musi wgrać do sterownika urzšdzenia cały metaplik dla każ- I, ţ dego pasma, które sterownik urzšdzenia zdefiniuje na stronie. Po zakończeniu procesu metaplik może zostać wykasowany. Dla każdego pasma sterownik urzšdzenia thzmaczy funkcje rysujšce na wyjœcie I'.. niezbędne do zrealizowania ich na drukarce. Format wyjœcia zależy od charakte- i'' rystyki drukarki. Dla drukarek mozaikowych będzie to zbiór sekwencji sterujš- cych, w tym sekwencji graficznych. (Aby uzyskać pomoc przy budowaniu wyj- œcia, sterownik drukarki może wywoływać różne procedury "pomocnicze", tak- i że znajdujšce się w module GDI). Dla drukarek laserowych o języku opisu stro- ny wysokiego poziomu (takim jak PostScript), wyjœcie na drukarkę będzie w tym języku. Sterownik drukarki przekazuje wyjœcie na drukarkę dla każdego pasma do mo- f. dułu GDI, który następnie zapisuje to wyjœcie w innym pliku tymczasowym. Plik ten rozpoczyna się od znaków ~SPL i ma rozszerzenie .TMP. Gdy strona zostanie zakończona, moduł GDI wykonuje międzyprocesowe odwołanie do bufora dru- kowania informujšc, że nowe zadanie drukowania jest gotowe. Wówczas program aplikacji przechodzi do następnej strony. Kiedy aplikacja skończy wszystkie strony, które majš być wydrukowane, wywohxje EndDoc, aby zasygnalizować zakończe- nie zadania drukowania. Rysunek 13-1 przedstawia interakcje programu, modu- łu GDI oraz sterownika drukarki. 542 Częœć II; Gţafika Rozc Program Wywolanie funkcji drukujšcych (StartDoc, StartPage) Wywofania GDI (LineTo, Rectangle i tak dalej) Modut GDl ţ ţ Dyskowy '----iţ rozszerzony Wywotania Control ţ y metaplik Instrukcje drukujšce Procedury (Output, BitBlt i tak dalej) "pomocnicze" .. ...., .,........ţ. Przekazanie żšdania Sterowrk drukarki drukowania do GDI32 Modul GDI32 Międzyprocesowe wywolanie do podsystemu bufora drukowania Tak L;; ţzy lest następna ţ Buforowanie stronaţ (SPOOLER.EXE) Nie "....... ţ .",. Wyœlij dane na drukarkę (patrz rysunek 13-2) Plik opisu zadania Rysunek 13-1. Interakcja programu aplikacji, modułu GDI, sterownika drukarki oraz bufora Bufor odcišża programy aplikacji od częœci pracy zwišzanej z drukowaniem. Element bufora Opis Bufor żšdania droku Przesyła strumień danych do dostawcy wydruku Lokalny dostawca wydruku Tworzy pliki bufora przeznaczone dla lokalnej drukarki Sieciowy dostawca wydruku Tworzy pliki bufora przeznaczone dla drukarki sieciowej Przetwarzanie wydruku Przeprowadza debuforowanie, które jest konwersjš buforowa- nych danych niezależnych od urzšdzenia do postaci charakte- rystycznej dla danej drukarki Monitorowanie portu Kontroluje port, do którego podłšczona jest drukarka Monitorowanie języka Kontroluje obsługę komunikacji dwukierunkowej z drukarkš, aby ustawić konfigurację urzšdzenia oraz monitorować stan drukarki Windows wczytuje bufor podczas uruchamiania, zatem jest on aktywny w chwi- li, gdy program rozpoczyna drukowanie. Gdy program drukuje dokument, mo- duł GDI tworzy pliki zawierajšce wyjœcie na drukarkę. Zadanie bufora druko- Rozdział 13: Drukowanie ţ3 wania to przesłanie tych plików do drukarki. Jest on powiadamiany o nowym zadaniu drukowania przez moduł GDI. Wówczas rozpoczyna odczytywanie pli- ku i przekazywanie go bezpoœrednio do drukarki. Aby przekazać pliki, bufor wy- korzystuje różne funkcje komunikacyjne dla portu szeregowego lub równoległe- go, do którego podłšczona jest drukarka. Gdy bufor zakończy przesyłanie pliku do drukarki, kasuje tymczasowy plik przechowujšcy dane wyjœciowe. Proces ten został przedstawiony na rysunku 13-2. Rysunek 13-2. Praca bufora drukowania Większa częœć tego procesu jest transparentna dla programu aplikacji. Z perspek- tywy aplikacji "drukowanie" odbywa się tylko w czasie, którego wymaga moduł GDI, aby zapisać wyjœcie na drukarkę w plikach dyskowych. Potem - lub nawet przed, jeœli drukowanie obsługiwane jest na drugim planie - program jest zwalnia- ny i może robie inne rzeczy. Odpowiedzialnoœć za rzeczywiste drukowanie doku- mentu spoczywa na buforze drukowania, a nie na programie. Użytkownik może wstrzymać zadania drukowania, zmienić ich priorytety lub anulować je, jeżeli jest , to konieczne. Takie rozwišzanie pozwala programom na szybsze "drukowanie' niż byłoby to rnożliwe, gdyby drukowały w czasie rzeczywistym i musiały czekać, aż drukarka skończy jednš stronę, zanim przystšpi do następnej. Wprawdzie opisałem ogólnie przebieg drukowania, ale istnieje kilka wariantów jego realizacji. Jectna z możliwoœci jest taka, że podczas korzystania przez system Windows z drţkarld, bufor drukowania nie musi być obecny. Zazwyczaj użytkownik może wyłšczyć buforowanie drukowania w oknie dialogowym właœciwoœci dnikarki. Częœć II: Grafika Ale dlaczego użytkownik miałby pomijać bufor Windows? Być może, dysponuje on hardwarowym lub softwarowym buforem drukowania, który działa szybciej niż bufor Windows. A może drukarka funkcjonuje w sieci majšcej swój własny bufor. Ogólnie obowišzuje zasada, że jeden bufor jest szybszy niż dwa. Usunię- cie bufora Windows przyspieszyłoby w tych przypadkach drukowanie, ponie- waż wyjœcie na drukarkę nie musiałoby być zapisywane na dysku. Może ono być przesłane bezpoœrednio na drukarkę i zostać przechwycone przez zewnętrzny sprzętowy lub programowy bufor drukowania. Jeœli bufor Windows nie jest aktywny, moduł GDI nie przechowuje w pliku wyj- œcia na drukarkę ze sterownika urzšdzenia. Zamiast tego przesyła wyjœcie bez- poœrednio do portu szeregowego lub równoległego. W odróżnieniu od drukowa- nia bez poœrednictwa bufora, drukowanie wykonywane przez GDI ma możliwoœć wstrzymania działania aplikacji (w szczególnoœci wykonujšcych drukowanie) aż do zakończenia drukowania. Oto inny przypadek: zwykle moduł GDI przechowuje wszystkie funkcje niezbędne do zdefiniowania strony w metapliku, a następnie wgrywa ten metaplik do ste- rownika drukarki kolejno dla każdego z pasm zdefiniowanych przez sterownik. Jeżeli sterowruk drukarki nie zażšda dzielenia na pasma, metaplik nie zostanie utworzony; GDI po prostu przekazuje funkcje rysujšce bezpoœrednio do sterow- nika. W kolejnym wariancie do aplikacji należy dzielenie wyjœcia na drukarkę na pasma. To czyni kod drukujšcy aplikacji bardziej złożonym, lecz zwalnia moduł GDI z tworzenia metapliku. I tym razem GDI po prostu przekazuje funkcje dla każdego z pasm do sterowruka drukarki. Teraz zapewne zaczynasz powoli rozumieć, dlaczego drukowanie z programu Windows wymaga więcej zachodu niż korzystanie z monitora. Mogš nastšpić pewne komplikacje - w szczególnoœci wtedy, gdy podczas tworzenia metapliku lub plików wyjœcia na drukarkę modułowi GDI skończy się miejsce na dysku. Możesz w tej sytuacji albo zasypać użytkownika meldunkami o pojawiajšcych się problemach i próbować jakoœ je rozwišzać, albo zachować powœcišgliwoœć. Dla aplikacji pierwszš czynnoœciš, jakš należy wykonać podczas drukowania, jest uzyskanie kontekstu urzšdzenia drukujšcego. Kontekst urzšdzenia drukujšcego Zanim będziesz mógł rysować na monitorze, musisz uzyskać uchwyt kontekstu urzšdzenia. Podobnie przed przystšpieniem do drukowania musisz uzyskać uchwyt kontekstu urzšdzenia drukujšcego. Kiedy już masz ten uchwyt (i wywo- łałeœ StartDoc, aby zapowiedzieć swój zamiar utworzenia nowego dokumentu, oraz StartPage w celu rozpoczęcia strony), możesz używać uchwytu kontekstu urzšdzenia drukujšcego w taki sam sposób, w jaki korzystasz z uchwytu kontek- stu urzšdzenia wyœwietlajšcego - czyli jako pierwszego parametru rozmaitych funkcji rysujšcych GDI. Wiele aplikacji korzysta ze standardowego okna dialogowego drukowania, otwie- ranego wywołaniem funkcji PrintDlg. (W dalszej częœci tego rozdziału pokażę, jak stosować tę funkcję). PrintDlg daje użytkownikowi okazję do zmiany drukar- ki lub okreœlenia innej charakterystyki zadania przed przystšpieniem do wydru- Rozdział 13: Drukowanie 545 ku. Następnie podaje aplikacji uchwyt kontekstu urzšdzenia drukujšcego. Funk- cja ta może oszczędzić aplikacji nieco pracy. Chociaż niektóre aplikacje (na przy- kład Notatnik) wolš uzyskiwać kontekst urzšdzenia drukujšcego bez wyœwie- tlania okna dialogowego. Zadanie takie wymaga zastosowania CreateDC. W rozdziale 5 pokazaliœmy, że można uzyskać uchwyt kontekstu urzšdzenia dla całego ekranu wywołujšc: hdc = CreateDC (TEXT ("DISPLAY"), NULL, NULL, NULL) ; Uchwyt kontekstu urzšdzenia drukujšcego uzyskasz poshzgujšc się tš samš funk- cjš. Jednak dla kontekstu urzšdzenia drukujšcego ogólna składnia CreateDC jest następujšca: hdc = CreateDC (NULL, szDeviceName. NULL, plnitializationData): Argumentowi plnitializationData z reguły także nadawana jest wartoœć NULL. Ar- gument szDeviceName okreœla łańcuch znakowy podajšcy systemowi Windows nazwę urzšdzenia drukujšcego. Zanim będziesz mógł wymienić nazwę tego urzš- dzenia, musisz sprawdzić, jakie drukarki sš dostępne. W systemie może być podłšczonych kilka drukarek. Mogš też znajdować się inne programy udajšce drukarki, na przykład oprogramowanie do wysyłania faksów. Niezależnie od liczby dołšczonych drukarek, tylko jedna jest "bieżšcš lub "do- myœlnš"- ta, którš użytkownik wybrał ostatnio. Niektóre małe programy Win- dows drukujš tylko na tej drukarce. Metody uzyskiwania kontekstu domyœlnego urzšdzenia drukujšcego ulegały zmianom na przestrzeni lat. Obecnie standardowa metoda używa funkcji Enum- Printers. Funkcja ta wypełnia tablicę struktur zawierajšcych informację na temat każdej z dołšczonych drukarek. Możesz nawet, w zależnoœci od żšdanego po- ziomu detali, wybrać rodzaj struktury użytej w funkcji. Te struktury nazywajš się PRINTER INFO x, gdzie x jest liczbš. Niestety, rodzaj użytej struktury zależy także od tego, czy twój program został uruchomiony w systemie Windows 98, czy w Microsoft Windows NT. IZysunek 13-3 prezentuje funkcję GetPrinterDC, która będzie działała w każdym z tych sys- temów operacyjnych. GETPRNDC.C /* GETPRNDC.C - Funkcja GetPrinterDC */ #include HDC GetPrinterDC (void) ( DWORD dwNeeded, dwReturned : HDC hdc : PRINTER_INFO_4 * pinfo4 : PRINTERţINFOţ5 * pinfo5 : if (GetVersion () & 0x80000000) // Windows 98 EnumPrinters (PRINTERţENUM DEFAULT, NULL, 5, NULL, 546 Częœć IIů Grafika (cišg dalszy ze strony 545) 0, &dwNeeded, RdwReturned) pinfo5 = malloc (dwNeeded) ; EnumPrinters (PRINTERţENUM_DEFAULT, NULL, 5, (PBYTE) pinfo5, dwNeeded, &dwNeeded, &dwReturned) ; hdc = CreateDC (NULL, pinfo5->pPrinterName, NULL, NULL) ; free (pinfo5) ; ) else // Windows NT ( EnumPrinters (PRINTER_ENUM_LOCAL, NULL, 4, NULL, 0, &dwNeeded, &dwReturned) ; pinfo4 = malloc (dwNeeded) ; EnumPrinters (PRINTER_ENUM_LOCAL, NULL, 4, (PBYTE) pinfo4, dwNeeded, &dwNeeded, &dwReturned) ; hdc = CreateDC (NULL, pinfo4->pPrinterName, NULL, NULL) ; free (pinfo4) ? return hdc : Rysunek 13-3. Plik GETPRNDC.C Funkcja GetPrinterDC używa funkcji GetUersion do okreœlenia, czy program uru- chomiony został w œrodowisku Windows 98, czy w systemie Windows NT. Nie- zależnie od wersji, funkcja dwukrotnie wywołuje EnumPrinters - za pierwszym razem po to, żeby uzyskać rozmiar potrzebnej struktury, a drugi raz; aby jš fak- tycznie wypelnić. W systemie Windows 98 funkcja używa struktury PRáţTI'ER IN- FO 5; w Windows NT wykorzystuje strukturę PIZINTER INFO 4. Struktury te zostały wymienione w dokumentacji EnumPrinters (/Platform SDK/Graphics and Multimedia Services/GDI/Printing and Print Spooler/Printing and Print Spooler Refe- rence/ Printing and Print Spooler Functions/EnumPrinters, bezpoœredriio przed sek- cjš przykładów) jako łatwe i wyjštkowo szybkie. Rozszerzony program DEVCAPS Pierwotny program DEVCAPS1 z rozdziału 5 wyœwietlał podstawowe informa- cje o monitorze dostępne z funkcji GetDeviceCaps. Nowa wersja, zaprezentowana na rysunku 13-4, pokazuje więcej informacji zarówno na temat monitora, jak i wszystkich drukarek dołšczonych do systemu. DEVCAPS2.C /* DEVCAPS2.C - Wyţwietl informacje o urzšdzeniach (wersja 2) (c) Charles Petzold, 1998 */ Rozdział 13: Drukowanie 547 ţţinclude !,ł ţţinclude "resource.h" LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) : void DoBasicInfo (HDC, HDC, int, int) : void DoOtherInfo (HDC, HDC, int, int) : void DoBitCodedCaps (HDC, HDC, int, int, int) : typedef struct int iMask : TCHAR * szDesc : si BITS : ţţdefine IDM DEVMODE 1000 int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) static TCHAR szAppNameC] = TEXT ("DevCaps2") : HWND hwnd : MSG msg : WNDCLASS wndclass : wndclass.style = CS_HREDRAW ţ CS UREDRAW : wndclass.lpfnWndProc = WndProc : wndclass.cbClsExtra = 0 : wndclass.cbWndExtra = 0 : wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL. IDI APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDCţARROW) : I! ;...:. wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITEţBRUSH) ; wndclass.lpszMenuName = szAppName : wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) ( MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MBţICONERROR) : return 0 ; hwnd = CreateWindow (szAppName NULL, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW USEDEFAULT, CW_USEDEFAULT, CW USEDEFAULT, NULL, NULL, hInstance, NULL) : ShowWindow (hwnd, iCmdShow) : UpdateWindow (hwnd) : while (GetMessage (&msg, NULL, 0, 0)) TranslateMessage (&msg) : DispatchMessage (&msg) ; return msg.wParam : wfi 548 Częœć II, Grafika (cišg dalszy ze strony 547) LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) . static TCHAR szDeviceC32], szWindowTextC64] ; static int cxChar, cyChar, nCurrentDevice = IDM_SCREEN, nCurrentInfo = IDM_BASIC ; static DWORD dwNeeded, dwReturned ; static PRINTER_INFO_4 * pinfo4 ; static PRINTERţINFOţ5 * pinfo5 ; DWORD i ; HDC hdc, hdcInfo ; HMENU hMenu ; HANDLE hPrint ; PAINTSTRUCT s ; P TEXTMETRIC tm ; switch (message) ( case WM_CREATE : hdc = GetDC (hwnd) ; SelectObject (hdc, GetStockObject (SYSTEMţFIXED_FONT)) ; GetTextMetrfics (hdc, &tm) ; cxChar = tm.tmAveCharWidth ; cyChar = tm.tmHeight + tm.tmExternalLeading ; ReleaseOC (hwnd, hdc) ; // ten przypadek zostawiamy nieopracowany case WM_SETTINGCHANGE: hMenu = GetSubMenu (GetMenu (hwnd), 0) ; while (GetMenuItemCount (hMenu) > 1) DeleteMenu (hMenu, 1, MF BYPOSITION) ; // Przygotuj listę wszystkich lokalnych i sieciowych // drukarek // Najpierw ustal, jak duża tablica jest potrzebna; // to wywolanie nie uda się, jeœli żţdany rozmiar // będzie pozostawiony na dwNeeded // // Następnie zarezerwuj miejsce na tablicę // z informacjami i wypelnij jd // // Wstaw nazwę drukarki do menu if (GetVersion () & 0x80000000) // Windows 98 i EnumPrinters (PRINTER_ENUM_LOCAL, NULL, 5, NULL, 0, &dwNeeded, &dwReturned) ; pinfo5 = malloc (dwNeeded) ; EnumPrinters (PRINTER_ENUM_LOCAL, NULL, 5, (PBYTE) pinfo5, dwNeeded, &dwNeeded, &dwReturned) ; Rozdział 13: Dmkowanie 549 for (i = 0 ; i < dwReturned ; i++) ( AppendMenu (hMenu, (i+1) % 16 ? 0 : MF MENUBARBREAK. i + 1, pinfo5Ci].pPrinterName) ; free (pinfo5) : ) else // Windows NT ( EnumPrinters (PRINTERţENUM_LOCAL, NULL, 4, NULL, 0. &dwNeeded, &dwReturned) ; pinfo4 = malloc (dwNeeded> ; EnumPrinters (PRINTER_ENUM_LOCAL, NULL, 4, (PBYTE) pinfo4, dwNeeded, &dwNeeded, &dwReturned) ; I for (i = 0 ; i < dwReturned ; i++) AppendMenu (hMenu, (i+1) % 16 ? 0 : MF MENUBARBREAK, i + 1, pinfo4Ci].pPrinterName) : ) free (pinfo4) ; ) AppendMenu (hMenu, MF_SEPARATOR, 0, NULL) ; AppendMenu (hMenu. 0, IDM DEVMODE, TEXT ("Properties")) : wParam = IDM SCREEN ; %/ ten przypadek zostawiamy nieopracowany case WM COMMAND : hMenu = GetMenu (hwnd) ; if (LOWORD (wParam) == IDM_SCREEN ţţ // ekran i drukarki LOWORD (wParam) < IDMţDEVMODE) CheckMenuItem (hMenu, nCurrentDevice, MF UNCHECKED) ; nCurrentDevice = LOWORD (wParam) ; CheckMenuItem (hMenu, nCurrent0evice, MFţCHECKED) ; '' 4 else if (LOWORD (wParam) == IDM DEVMODE) // wybór opcji Properties ( GetMenuString (hMenu, nCurrentDevice, sz0evice, sizeof (sz0evice) / sizeof (TCHAR), MFţBYCOMMAND); if (OpenPrinter (szDevice. &hPrint. NULL)) PrinterProperties (hwnd, hPrint) : ClosePrinter (hPrint) ; 1 else // elementy menu rodzaju informacji f CheckMenuItem (hMenu, nCurrentInfo. MF UNCHECKED) ; nCurrentInfo = LOWORD (wParam) ; CheckMenuItem (hMenu, nCurrentInfo, MF CHECKED) ; 550 Częœć II: Grafika (cišg dalszy ze strony 549) InvalidateRect (hwnd, NULL, TRUE) ; return 0 ; case WM_INITMENUPOPUP : if (lParam = 0) EnableMenuItem (GetMenu (hwnd), IDM_DEVMODE, nCurrentDevice --- IDMţSCREEN ? MF GRAYED : MF ENABLED) ; return 0 ; case WMţPAINT : lstrcpy (szWindowText, TEXT ("Device Capabilities: ")) ; if (nCurrentDevice = IDM SCREEN) lstrcpy (szDevice, TEXT ("DISPLAY")) ; hdcInfo = CreateIC (szDevice, NULL, NULL, NULL) ; ) else { hMenu = GetMenu (hwnd) ; GetMenuString (hMenu, nCurrentDevice, szDevice, sizeof (szDevice), MF_BYCOMMAND) ; hdcInfo = CreateIC (NULL, szDevice, NULL, NULL) ; l lstrcat (szWindowText, szDevice) ; SetWindowText (hwnd, szWindowText) ; hdc = BeginPaint (hwnd, &ps) ; SelectObject (hdc, GetStockObject (SYSTEM FIXEDţFONT)) ; if (hdcInfo) ( switch (nCurrentInfo) t case IDM_BASIC : DoBasicInfo (hdc, hdcInfo, cxChar, cyChar) ; break ; case IDM_OTHER : DoOtherInfo (hdc, hdcInfo, cxChar, cyChar) ; break ; case IDM_CURVE : case IDM_LINE : case IDM_POLY : case IDM TEXT : ) DoBitCodedCaps (hdc, hdcInfo, cxChar, cyChar, nCurrentInfo - IDM CURVE) ; voi break ; ) DeleteDC (hdcInfo) ; ) EndPaint (hwnd, &ps) ; return 0 ; Rozdział 13: Dmkowanie 551 case WM DESTROY : PostOuitMessage (0) ; return 0 ; l return DefWindowProc (hwnd, message, wParam, lParam) ; void DoBasicInfo (HDC hdc, HDC hdcInfo, int cxChar, int cyChar) ( static struct ( int nIndex ; TCHAR * szDesc ; ) infoC) = ( HORZSIZE, TEXT ("HORZSIZE Width in millimeters:"), VERTSIZE, TEXT ("UERTSIZE Height in millimeters:"), HORZRES, TEXT ("HORZRES Width in pixels:"), VERTRES, TEXT ("VERTRES Height in raster lines:"), BITSPIXEL, TEXT ("BITSPIXEL Color bits per pixel:"), PLANES, TEXT ("PLANES Number of color planes:"), NUMBRUSHES, TEXT ("NUMBRUSHES Number of device brushes:"), NUMPENS, TEXT ("NUMPENS Number of device pens:"), NUMMARKERS, TEXT ("NUMMARKERS Number of device markers:"), NUMFONTS, TEXT ("NUMFONTS Number of device fonts:"), NUMCOLORS. TEXT ("NUMCOLORS Number of device colors:"), PDEVICESIZE, TEXT ("PDEVICESIZE Size of device structure:"), ASPECTX, TEXT ("ASPECTX Relative width of pixel:"), ASPECTY, TEXT ("ASPECTY Relative height of pixel:"), ASPECTXY, TEXT ("ASPECTXY Relative diagonal of pixel:"), LOGPIXELSX, TEXT ("LOGPIXELSX Horizontal dots per inch:"), LOGPIXELSY, TEXT ("LOGPIXELSY Vertical dots per inch:"), SIZEPALETTE, TEXT ("SIZEPALETTE Number of palette entries:"), NUMRESERUED. TEXT ("NUMRESERVED Reserved palette entries:."), COLORRES, TEXT ("COLORRES Actual color resolution:") PHYSICALWIDTH, TEXT ("PHYSICALWIDTH Printer page pixel width:"), PHYSICALHEIGHT, TEXT ("PHYSICALHEIGHT Printer page pixel height:"), PHYSICALOFFSETX, TEXT ("PHYSICALOFFSETX Printer page x offset:"), PHYSICALOFFSETY, TEXT ("PHYSICALOFFSETY Printer page y offset:") : int i : TCHAR szBufferC80] ; for (i = 0 ; i < sizeof (info) / sizeof (infoCO)) ; i++) TextOut (hdc, cxChar, (i + 1) * cyChar, szBuffer, wsprintf (szBuffer, TEXT ("%-45s%8d"), infoCi).szDesc, GetOeviceCaps (hdcInfo, infoCi).nIndex))) ; ) void DoOtherInfo (HDC hdc, HDC hdcInfo, int cxChar, int cyChar) ( static BITS clipC) = ( CPţRECTANGLE, TEXT ("CPţRECTANGLE Can Clip To Rectangle:") 1: static BITS rasterC] = 552 Częœć II: Grafika Rozc (cišg dalszy ze strony 551) RC_BITBLT, TEXT ("RC_BITBLT Capable of simple BitBlt:"), RC_BANDING, TEXT ("RC_BANDING Requires banding support:"), RC_SCALING, TEXT ("RC_SCALING Requires scaling support:"), RC_BITMAP64, TEXT ("RC_BITMAP64 Supports bitmaps >64K:"), RC_GDI2O_OUTPUT, TEXT ("RC_GDI2O_OUTPUT Has 2.0 output calls:"), RC_DI_BITMAP, TEXT ("RC_DI_BITMAP Supports DIB to memory:"), RC_PALETTE, TEXT ("RC_PALETTE Supports a palette:"), RC_DIBTODEV, TEXT ("RC_DIBTODEV Supports bitmap conversion:"), RC_BIGFONT, TEXT ("RC_BIGFONT Supports fonts >64K:"), RC_STRETCHBLT, TEXT ("RC_STRETCHBLT Supports StretchBlt:"), RC_FLOODFILL, TEXT ("RC_FLOODFILL Supports FloodFill:"), RCţSTRETCHDIB, TEXT ("RCţSTRETCHDIB Supports StretchDIBits:") static TCHAR * szTech[J = ( TEXT ("DT_PLOTTER (Vector plotter)"), TEXT ("DT_RASDISPLAY (Raster display)"), TEXT ("DT_RASPRINTER (Raster printer)"), TEXT ("DT_RASCAMERA (Raster camera)"), TEXT ("DT_CHARSTREAM (Character stream)"), TEXT ("DT_METAFILE (Metafile)"), TEXT ("DT DISPFILE (Display file)") ) ; int i ; TCHAR szBuffer[80] ; TextOut (hdc, cxChar, cyChar, szBuffer, wsprintf (szBuffer, TEXT ("%-24s%O4XH"), TEXT ("DRIVERVERSION:"), GetDeviceCaps (hdcInfo, DRIVERVERSION))) ; TextOut (hdc, cxChar, 2 * cyChar, szBuffer, wsprintf (szBuffer, TEXT ("%-24s%-40s"), TEXT ("TECHNOLOGY:"), szTech[GetDeviceCaps (hdcInfo, TECHNOLOGY)J)) ; TextOut (hdc, cxChar, 4 * cyChar, szBuffer, wsprintf (szBuffer, TEXT ("CLIPCAPS (Clipping capabilities)"))) ; for (i = 0 ; i < sizeof (clip) / sizeof (clip[OJ) ; i++) TextOut (hdc, 9 * cxChar, (i + 6) * cyChar, szBuffer, wsprintf (szBuffer, TEXT ("%-45s %3s"), clip[i].szDesc, GetDeviceCaps (hdcInfo, CLIPCAPS) & clip[iJ.iMask ? TEXT ("Yes") : TEXT ("No"))) ; TextOut (hdc, cxChar, 8 * cyChar, szBuffer, wsprintf (szBuffer, TEXT ("RASTERCAPS (Raster capabilities)"))) ; for (i = 0 ; i < sizeof (raster) / sizeof (raster[OJ) ; i++) TextOut (hdc, 9 * cxChar, (i + 10) * cyChar, szBuffer, wsprintf (szBuffer, TEXT ("%-45s %3s"), raster[iJ.szDesc, GetDeviceCaps (hdcInfo, RASTERCAPS) & raster[iJ.iMask ? TEXT ("Yes") : TEXT ("No"))) ; void DoBitCodedCaps (HDC hdc, HDC hdcInfo, int cxChar, int cyChar, int iType) ( static BITS curves[J = ( Rozdział 13: Dţukowanie 553 CC_CIRCLES, TEXT ("CC_CIRCLES Can do circles:"), CC_PIE, TEXT ("CC_PIE Can do pie wedges:"). CC_CHORD, TEXT ("CC_CHORD Can do chord arcs:"), CC_ELLIPSES, TEXT ("CC_ELLIPSES Can do ellipses:"), CC_WIDE, TEXT ("CC_WIDE Can do wide borders:"), CC_STYLED, TEXT ("CC_STYLED Can do styled borders:"), CC_WIDESTYLED, TEXT ("CC_WIDESTYLED Can do wide and styled borders:"), CC_INTERIORS, TEXT ("CCţINTERIORS Can do interiors:") : static BITS linesC] = I LC_POLYLINE, TEXT ("LC_POLYLINE Can do polyline:"), LC_MARKER, TEXT ("LC_MARKER Can do markers:"). LC_POLYMARKER, TEXT ("LC_POLYMARKER Can do polymarkers"), LC_WIDE, TEXT ("LC_WIDE Can do wide lines:"), LC_STYLED, TEXT ("LC_STYLED Can do styled lines:"), LC_WIDESTYLED, TEXT ("LC_WIDESTYLED Can do wide and styled lines:"), LC_INTERIORS, TEXT ("LCţINTERIORS Can do interiors:") l: static BITS polyC] = ( PCţPOLYGON, TEXT ("PCţPOLYGON Can do alternate fill polygon:"), PC RECTANGLE, TEXT ("PCţRECTANGLE Can do rectangle:"), PC WINDPOLYGON, TEXT ("PC WINDPOLYGON Can do winding number fill polygon:"), PC_SCANLINE, TEXT ("PC_SCANLINE Can do scanlines:"), PC_WIDE, TEXT ("PC_WIDE Can do wide borders:"), PC_STYLED, TEXT ("PC STYLED Can do styled borders:"), PC WIDESTYLED, TEXT ("PC_WIDESTYLED Can do wide and styled borders:"), PC INTERIORS, TEXT ("PCţINTERIORS Can do interiors:") 1: static BITS textC] = ( TC OP_CHARACTţR, TEXT ("TC OP CHARACTER Can do character output precision:"), TC OP_STROKE, TEXT ("TC OP STROKE Can do stroke output precision:"), TC CP_STROKE, TEXT ("TC CP STROKE Can do stroke clip precision:"), TC CR_90 TEXT ("TCţCP 90 Can do 90 degree character rotation:"), TC CR_ANY, TEXT ("TCţCR ANY Can do any character rotation:"), TC SF_X_YINDEP, TEXT ("TC SF X YINDEP Can do scaling independent of X and Y:"), TC_SA_DOUBLE, TEXT ("TCţSŽ DOUBLE Can do doubled character for scaling:"), TC_SA_INTEGER, TEXT ("TCţSAţINTEGER Can do integer multiples for scaling:"), TC_SA_CONTIN, TEXT ("TCţSŽ CONTIN Can do any multiples for exact scaling:"), TCţEA_DOUBLE, TEXT ("TCţEŽ DOUBLE Can do double weight characters:"), Częœć II: Grafika (cišg dalszy ze strony 553) TC_IA_ABLE, TEXT ("TC_IA_ABLE Can do italicizing:"), TC_UA_ABLE, TEXT ("TC_UA_ABLE Can do underlining:"), TC_SO ABLE, TEXT ("TC_SO_ABLE Can do strikeouts:"), TC_RA_ABLE, TEXT ("TC_RA ABLE Can do raster fonts:"), TCţVA ABLE, TEXT ("TC VŽ ABLE Can do vector fonts:") static struct ( int iIndex ; TCHAR * szTitle ; BITS (*pbits)C] int iSize ; 1 bitinfo[] = ( CURVECAPS, TEXT ("CURVCAPS (Curve Capabilities)"), (BITS (*)[]) curves, sizeof (curves) / sizeof (curves[0]), LINECAPS, TEXT ("LINECAPS (Line Capabilities)"), (BITS (*)[]) lines, sizeof (lines) / sizeof (linesCO]), POLYGONALCAPS, TEXT ("POLYGONALCAPS (Polygonal Capabilities)"), (BITS (*)C]) poly, sizeof (poly) / sizeof (poly[0]), TEXTCAPS, TEXT ("TEXTCAPS (Text Capabilities)"), (BITS (*)[]) text, sizeof (text) / sizeof (textCO]) static TCHAR szBuffer[80] ; BITS (*pbits)C] = bitinfo[iType].pbits ; int i, iDevCaps = GetDeviceCaps (hdcInfo, bitinfoCiType].iIndex) ; TextOut (hdc, cxChar, cyChar, bitinfo[iType].szTitle, lstrlen (bitinfo[iType].szTitle)) ; for (i = 0 ; i < bitinfo[iType].iSize ; i++) TextOut (hdc, cxChar, (i + 3) * cyChar, szBuffer, wsprintf (szBuffer, TEXT ("%-55s %3s"), (*pbits)[i].sz0esc, iDevCaps & (*pbits)[i].iMask ? TEXT ("Yes") : TEXT ("No"))); DEUCAPS2.RC (fragmenty) // Microsoft Developer Studio generated resource script. Itinclude "resource.h" Ilinclude "afxres.h" ///////////////////////////////////////////////////////////////////////////// // Menu DEVCAPS2 MENU DISCARDABLE BEGIN POPUP "&Device" BEGIN MENUITEM "&Screen", IDM SCREEN, CHECKED END POPUP "&Capabilities" Rozdział 13: Drukowanie 555 BEGIN IDM_BASIC MENUITEM "&Basic Information", MENUITEM "&Other Information", IDM_OTHER MENUITEM "&Curve Capabilities", IDM_CURVE MENUITEM "&Line Capabilities", IDM_LINE MENUITEM "&Polygonal Capabilities", IDM_POLY t4ENUITEM "&Text Capabilities", IDM TEXT END END RESOURCE.H (fragmenty) Microsoft Developer Studio generated include file. Used by DevCaps2.rc 4ţdefine IDM_SCREEN 40001 4fdefine IDM_BASIC 40002 ţţdefine IDM_OTHER 40003 4ldefine IDM_CURVE 40004 4ţdefine IDM_LINE 40005 ţţdefine IDM_POLY 40006 ţţdefine IDM TEXT 40007 Rysunek 13-4. Program DEVCAPS2 Ponieważ DEVCAPS2 uzyskuje jedynie kontekst informacji o drukarce, możesz wybrać drukarki z menu DEVCAPS2 nawet wtedy, gdy ich port wyjœciowy usta- wiony jest na "none". Jeœli chcesz porównać możliwoœci różnych drukarek, mo- żesz najpierw użyć folderu Drukarki i dodać kilka ich sterowników. Wywołanie PrinterProperties Menu Device programu DEVCAPS2 zawiera opcję o nazwie Properties. Chcšc z niej skorzystać, najpierw wybierz drukarkę z menu Device, a następnie wybierz opcję Properties. Pojawi się okno dialogowe. Skšd się ono bierze? Wywoływane jest przez sterownik drukarki i prosi cię przynajmniej o wybranie rozmiaru pa- pieru. Większoœć drukarek daje także możliwoœć wyboru orientacji pionowej lub poziomej (ang. portrait i landscape). W trybie pionowej (który często jest ustawie- niem domyœlnym) góra wydruku będzie na krótkiej krawędzi kartki; w orientacji poziomej górnš krawędziš jest krawędŸ długa. Jeœli zmienisz tryb, zmiana ta zo- stanie odzwierciedlona w informacji uzyskiwanej od funkcji GetDeviceCaps przez program DEVCARS2: rozmiar i rozdzielczoœć pozioma zamieniana jest miejsca- mi z rozmiarem i rozdzielczoœciš pionowš. Okna dialogowe Właœciwoœci dla ko- lorowych ploterów mogš być znacznie bardziej rozbudowane, bo muszš uwzględ- nić kolory pisaków zainstalowanych w ploterze oraz typ używanego papieru (lub kalki). Wszystkie sterowniki drukarek zawierajš eksportowš funkcję o nazwie ExtDevi- ceMode, która wywołuje wspomniane okno dialogowe i zapisuje informacje wpro- wadzane przez użytkownika. Niektóre sterowruki zapisujš te informacje w swo- jej własnej sekcji ftegistry, niektóre tego nie robiš. Sterowniki zapisujšce informa- cje majš do niej dostęp podczas następnej sesji z Windows. 556 Częœć II: Grafika Programy windowsowe pozwalajšce użytkownikowi na wybór drukarki na ogół wywohzjš po prostu PrintDlg. Poshxgiwania się funkcjš PrintDlg nauczę cię w dal- szej częœci tego rozdziahz. Ta użyteczna funkcja bierze na siebie cały trud komu- nikacji z użytkownikiem i obshxguje wszelkie zmiany, jakich zażšda on podczas przygotowania do wydruku. PrintDlg także wywołuje okno dialogowe Właœci- woœci po kliknięciu przez użytkownika przycisku o tej nazwie. Program może wyœwietlić okno dialogowe z właœciwoœciami drukarki również przez bezpoœrednie wywołanie funkcji sterownika drukarki ExtDeviceMode lub ExtDeveModePropSheet. Jednakże nie polecam tej metody. Dużo lepszym sposo- bem jest wyœwietlenie okna dialogowego poœrednio, przez wywołanie PrinterPro- perties, tak jak to robi DEVCAPS2. PrinterProperties wymaga uchwytu obiektu drukarki, który możesz uzyskać wy- wołujšc funkcję OpenPrinter. Gdy użytkowruk zamknie okno dialogowe właœci- woœci, PrinterProperties kończy działanie. Możesz wówczas zamknšć uchwyt dru- karki, wywołujšc ClosePrinter. Oto jak robi to DEVCAPS2: Program najpierw odczytuje nazwę drukarki, która aktualnie jest wybrana w menu Device i zapisuje jš w tablicy znakowej szDevice: GetMenuString (hMenu, nCurrentDevice, szDevice, sizeof (szDevice) / sizeof (TCHAR), MF BYCOMMAND); Potem uzyskuje uchwyt tego urzšdzenia, wykorzystujšc OpenPrinter. Jeœli wywo- łanie się powiedzie, w następnej kolejnoœci program wywołuje PrinterProperties, aby otworzyć okno dialogowe, a na koniec CIosePrinter w celu usunięcia uchwy- tu urzšdzenia: if (OpenPrinter (sz0evice, &hPrint, NULL)) ( PrinterProperties (hwnd, hPrint); ClosePrinter (hPrint); Sprawdzanie cechy BitBlt Za pomocš funkcji GetDeviceCaps odczytasz rozmiar oraz rozdzielczoœć obszaru strony, który może być zadrukowany. (W większoœci przypadków obszar ten nie będzie się pokrywał z wielkoœciš papieru). Jeżeli chciałbyœ wykonać własne ska- lowanie, możesz także uzyskać w pikselach względnš szerokoœć i wysokoœć. Większoœć informacji o poszczególnych cechach drukarki przeznaczona jest dla GDI, a nie dla aplikacji. Często, gdy drukarka samodzielnie nie może czegoœ wy- konać, zostanie to zasymulowane przez GDI. Istnieje natomiast cecha, którš pewne aplikacje powinny sprawdzić. Jest niš charakterystyka drukarki uzyskiwana z bitu RC BITBLT wartoœci zwra- canej z GetDeviceCaps z parametrem RASTERCAPS (od ang. raster capabilities - możliwoœci rastrowe). Bit ten wskazuje, czy w przypadku tego urzšdzenia moż- liwy jest transfer bloków bitowych. Większoœć drukarek mozaikowych, lasero- wych oraz atramentowych jest zdolna do transferów bloków bitowych. Nie majš tej umiejętnoœci plotery. Urzšdzenia, które nie mogš obshzgiwać transferów blo- ków bitowych, nie obshzgujš następujšcych funkcji GDI: CreateCompatibIeDC, Cre- ateCompatibleBitmap, PatBlt, BitBlt, StretchBlt, GrayString, Drawlcon, SetPixel, Get- Rozdział 13: Drukowanie 557 Pixel, FloodFill, ExtFloodFill, FillRgn, FrameRgn, InvertRgn, PaintRgn, FiIlRect, Fra- meRect oraz InvertRect. Jest to najistotniejsza różnica między wykonywaniem wywołań GDI dla monitorów oraz używaniem ich dla drukarki. Najprostszy program drukujšcy Teraz gotowi jesteœmy do drukowania i zaczniemy najproœciej, jak to tylko możli- we. W rzeczywistoœci nasz pierwszy program drukujšcy spowoduje jedynie wy- rzucenie strony przez drukarkę. Program FORMFEED, pokazany na rysunku 13- 5, przedstawia absolutne minimm wymagane podczas drukowania. FORMFEED.C /* FORMFEED.C - Nakazuje drukarce przejœć do następnej strony (c) Charles Petzold, 1998 */ #include HDC GetPrinterDC (void) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpszCmdLine, int iCmdShow) static DOCINFO di = ( sizeof (DOCINFO), TEXT ("FormFeed") ) ; HDC hdcPrint = GetPrinterDC () ; if (hdcPrint != NULL) ( if (StartDoc (hdcPrint, &di) > 0) if (StartPage (hdcPrint) > 0 && EndPage (hdcPrint) > 0) EndDoc (hdcPrint) ; DeleteDC (hdcPrint) return 0 ; Rysunek 13-5. Program FORMFEED Ten program wymaga pliku GETPIZNDC.C pokazanego, na rysunku 13-3. Poza uzyskaniem kontekstu urzšdzenia drukujšcego (a następnie usunięciem go) program wywołuje zaledwie cztery funkcje drukujšce, omówione we wczeœniej- szej częœci tego rozdziału. FORMFEED najpierw wywołuje StartDoc, aby rozpo- czšć nowy dokument. Sprawdza wartoœć zwracanš z funkcji i kontynuuje dzia- łanie jedynie wówczas, gdy wartoœć ta jest dodatnia: if (StartOoc (hdcPrint, &di) > 0) Drugim argumentem funkcji StartDoc jest wskaŸnik do struktury DOCINFO. Zawie- ra ona w pierwszym polu rozmiar struktury, w drugim zaœ tekst "FormFeed". Gdy dokument jest drukowany lub oczekuje na wydruk, napis ten pojawia się w kolum- nie Nazwa dokumentu kolejki zadań drukarki. Zazwyczaj tekst identyfikujšcy jest połšczeniem nazwy aplikacji wykonujšcej wydruk oraz nazwy drukowanego pliku. 558 Częœć II: Grafika Jeœli StartDoc powiedzie się (co wskazuje dodatnia wartoœć zwracana), FORMFEED wywohxje StartPage, po czym następuje bezpoœrednio wywołanie do EndPage. Taka sekwencja skłania drukarkę do przejœcia do następnej strony. Tutaj także testo- wane sš wartoœci zwracane: if (StartPage (hdcPrint)> 0 && EndPage (hdcPrint)>0) Na koniec, jeœli wszystko do tego miejsca przebiegło bezbłędnie, dokument zo- staje zakończony: EndOoc (hdcPrint): Zauważ, że funkcja EndDoc wywoływana jest tylko wówczas, gdy nie wystšpiły żadne błędy wydruku. Jeżeli któraœ z poprzednich funkcji drukujšcych zwróci kod błędu, GDI zadba o przervanie wydruku dokumentu. Jeœli drukarka w tym momencie nie drukuje, taki kod błędu często powoduje jej zresetowanie. Zwykłe przetestowanie wartoœci zwracanych przez funkcje drukujšce jest najłatwiejszym sposobem sprawdzenia błędów. Jeżeli chcesz zakomunikować użytkownikowi wystšpienie jakiegoœ konkretnego błędu, musisz wywołać GetLastError, żeby móc okreœlić ten błšd. Jeœli kiedykolwiek pisałeœ prosty program przesuwajšcy papier w drukarce w sys- temie MS-DOS, wiesz, że kod ASCII 12 [Ctrl+L] aktywuje przesuwanie papieru dla większoœci drukarek. Dlaczego nie można po prostu otworzyć portu drukar- ki, korzystajšc z funkcji open z biblioteki C, a następnie wysłać kodu ASCII 12 używajšc write? Cóż, nic nie stoi na przeszkodzie, abyœ tak zrobił. Najpierw mu- sisz okreœlić, do jakiego portu równoległego lub szeregowego podłšczona jest dru- karka, a potem - czy jakiœ inny program (na przykład bufor drukarki) aktualnie nie używa drukarki. Nie chcesz chyba, aby polecenie wyrzucenia papieru zosta- ło wysłane w œrodku dokumentu drukowanego przez jakiœ inny program? Na- stępnie musisz sprawdzić, czy kod ASCII 12 jest znakiem powodujšcym wyrzu- cenie papieru dla dołšczonej drukarki, bo jak wiesz, kod ten nie jest uniwersalny. W rzeczywistoœci polecenie wyrzucenia papieru w PostScripcie to nie 12, ale sło- wo showpage. Mówišc krótko, niech nie przyjdzie ci do głowy obchodzenie Windows; podczas drukowania trzymaj się funkcji tego systemu. Drukowanie grafiki i tekstu Drukowanie z programu windowsowego z reguły wymaga więcej zabiegów, niż to pokazano w programie FORMFEED. Chcšc rzeczywiœcie coœ wydrukować, po- trzebujesz kilku wywołań GDI. Napiszmy program, który wydrukuje jednš stronę tekstu i grafiki. Rozpoczniemy od metody pokazanej w programie FORMFEED, a póŸniej rozbudujemy go. Rozpatrzymy trzy wersje tego programu, o nazwach PRINTl, PRINT2 i PRINT3. Aby uniknšć powielenia znacznej iloœci kodu Ÿró- dłowego, każdy z tych programów będzie wykorzystywał pokazany wczeœniej plik GETPRNDC.C oraz funkcje zawarte w pliku PRINT.C, przedstawionym na rysunku 13-6. Rozdział 13: Drukowanie 559 PRINT.C /* PRINT.C - Procedury wspólne dla programów Printl, Print2 i Print3 */ ţţinclude LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; BOOL PrintMyPage (HWND) ; extern HINSTANCE hInst ; extern TCHAR szAppName[] ; ;: extern TCHAR szCaptionC] ; i int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) HWND hwnd ; MSG msg ; WNDCLASS wndclass ; I wndclass.style = CS_HREDRAW ţ CSţUREDRAW ; i wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC ARROW) ; j wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITEţBRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; i if (!RegisterClass (&wndclass)) MessageBox (NULL, TEXT ("This program requires Windows NT!"), R szAppName, MBţICONERROR) ; return 0 ; hInst = hInstance ; hwnd = CreateWindow (szAppName, szCaption, WS_OIIERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) ( TranslateMessage (&msg) ; DispatchMessage (&msg) ; I return msg.wParam ; ) 560 Częœć II: Grafika (cišg dalszy ze strony 559) void Pa9eGDICaIIs (HDC hdcPrn, int cxPage, int cyPage) ( static TCHAR szTextStr[] = TEXT ("Hello, Printer!") ; Rectangle (hdcPrn, 0, 0, cxPage, cyPage) ; MoveToEx (hdcPrn, 0, 0, NULL) ; LineTo (hdcPrn, cxPage, cyPage) ; MoveToEx (hdcPrn, cxPage, 0, NULL) ; LineTo (hdcPrn, 0, cyPa9e) ; SaveDC (hdcPrn) ; SetMapMode (hdcPrn, MMţISOTROPIC) ; SetWindowExtEx (hdcPrn. 1000. 1000, NULL) ; SetViewportExtEx (hdcPrn, cxPage / 2, -cyPa9e / 2, NULL) ; SetViewportOrgEx (hdcPrn, cxPage / 2, cyPage / 2, NULL) ; Ellipse (hdcPrn, -500, 500, 500, -500) ; SetTextAlign (hdcPrn, TŽ BASELINE ţ TŽ CENTER) ; TextOut (hdcPrn, 0, 0, szTextStr, lstrlen (szTextStr)) ; RestoreDC (hdcPrn, -1) ; ) LRESULT CALLBACK WndProc (HWND hwnd, UINT message. WPARAM wParam, LPARAM lParam) t static int cxClient, cyClient ; HDC hdc ; HMENU hMenu ; PAINTSTRUCT ps ; switch (message) ( case WM CREATE: hMenu = GetSystemMenu (hwnd, FALSE) ; AppendMenu (hMenu, MF_SEPARATOR, 0, NULL) ; AppendMenu (hMenu, 0, 1, TEXT ("&Print")) ; return 0 ; case WM_SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; return 0 ; case WM_SYSCOMMAND: if (wParam = 1) ( if (!PrintMyPage (hwnd)) MessageBox (hwnd, TEXT ("Could not print page!"), szAppName, MB OK ţ MBţICONEXCLAMATION) ; return 0 ; ) break ; Rozdział 13: Drukowanie 561 case WM_PAINT : hdc = BeginPaint (hwnd, &ps) ; PageGDICaIIs (hdc. cxClient, cyClient) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY : PostOuitMessage (0) ; return 0 ; ) return DefWindowProc (hwnd, message, wParam, lParam) ; Rysunek 13-6. Plik PRINT.C, wykorzystywany w programach PRINTI, PRINTI i PIţINT3 Plik PRINT.C zawiera funkcje WinMain i WndProc, a także funkcję PageGDICaIIs, kto- ra przyjmuje jako argumenty uchwyt kontekstu urzšdzenia dmkujšcego i dwie zmien- ne, równe szerokoœci i wysokoœci strony dla drukarki. PageGDICaIIs rysuje prostokšt okalajšcy całš stronę, dwie linie między przeciwległymi narożnikami strony, elipsę w œrodku strony (jej œrednica jest równa połowie mniejszej wartoœci wysokoœci lub szerokoœci strony dla drukarki) oraz tekst "Hello, Printer!" w œrodku elipsy. W czasie przetwarzania komunikatu WMţCREATE procedura WndProc dodaje do menu systemowego opcję Print. Wybranie tej opcji powoduje wywołanie funkcji PrintMyPage, którš rozbudujemy w trakcie tworzenia kolejnych wersji programu. PrintMyPage zwróci TRUE, jeœli strona zostanie wydrukowana bez problemów, lub FALSE, jeżeli w czasie wydruku wystšpi błšd. Jeœli PrintMyPage zwraca FALSE, wówczas WndProc wyœwietla okno komunikatu, aby poinformować cię o błędzie. Drukowanie konturu PRINTI, pierwsza wersja programu drukujšcego, zaprezentowana jest na rysunku 13-7. Po skompilowaniu PIţINTI możesz go wykonać i z menu systemowego wybrac opcję Print. Zaraz potem GDI zapisze żšdane wyjœcie na drukarkę w pli- ku tymczasowym, a następnie bufor przeœle je do drukarki. PRINTl.C /* PRINTl.C - Drukowanie konturu (c) Charles Petzold, 1998 */ Ilinclude HDC GetPrinterDC (void) ; // w GETPRNDC.C void PageGDICaIIs (HDC, int, int) ; // w PRINT.C HINSTANCE hInst ; TCHAR szAppName[] = TEXT ("Printl") ; TCHAR szCaption[] = TEXT ("Print Program 1") ; 562 Częœć II: Grafika (cišg dalszy ze strony 561) BOOL PrintMyPage (HWND hwnd) ( static DOCINFO di = ( sizeof (DOCINFO), TEXT ("Printl: Printing") ) ; BOOL bSuccess = TRUE ; HDC hdcPrn ; int xPage, yPage ; if (NULL = (hdcPrn = GetPrinterDC ())) return FALSE ; xPage = GetDeviceCaps (hdcPrn, HORZRES) ; yPage = GetDeviceCaps (hdcPrn, VERTRES) : if, (StartDoc (hdcPrn, &di) > 0) ( if (StartPage (hdcPrn) > 0) ( PageGDICaIIs (hdcPrn, xPage, yPage) if (EndPage (hdcPrn) > 0) EndDoc (hdcPrn) ; else bSuccess = FALSE ; ) else bSuccess = FALSE ; DeleteDC (hdcPrn) ; return bSuccess ; Rysunek 13-7. Program PftINTI Spójrzmy na kod PRINTI.C. Jeœli PrintMyPage nie może uzyskać uchwytu kon- tekstu urzšdzenia dla drukarki, zwraca FALSE, a procedura WndProc wyœwietla okno komunikatu informujšce o wystšpieniu błędu. Jeżeli funkcji powiedzie się uzyskanie uchwytu kontekstu urzšdzenia, wówczas okreœla w pikselach pozio- my i pionowy rozmiar strony, wywołujšc GetDeviceCaps: xPage = GetDeviceCaps (hdcPrn, HORZRES) ; yPage = GetDeviceCaps (hdcPrn, VERTRES) ; Nie jest to całkowity rozmiar papieru, a tylko jego obszar przeznaczony do zadru- kowania. Po tych wywołaniach kod funkcji PrintMyPage w PRINT1 ma takš samš strukturę jak kod FORMFEED, tyle że PRáţIT'1 wywołuje między StartPage i End- Page funkcję PageGDICaIIs. PIZáţTTI wywołuje funkcję drukowania EndDoc jedynie wtedy, gdy wywołania StartDoc, StartPage i EndPage zakończš się powodzeniem. Anulowanie wydruku i procedura Abort W przypadku długich dokumentów program powinien dawać użytkownikowi dogodny sposób wstrzymania zadania drukowania, gdy aplikacja nadal druku- je. Być może użytkownik zamierzał wydrukować tylko jednš stronę dokumentu, lecz nieopatrznie wybrał wydrukowanie wszystkich 537 stron. Pomyłkę tę po- winno się dać naprawić, zanim wszystkie 537 stron zostanie wydrukowane. Rozdział 13: Drukowanie 563 Anulowanie zadania wydruku z aplikacji wymaga procedury "Abort" (przerwij). Procedura przerywajšca to niewielka funkcja eksportowa w twoim programie. Adres tej funkcji podajesz systemowi Windows jako argument funkcji SetAbort- Proc; wówczas w czasie drukowania GDI wywołuje cyklicznie tę procedurę, py- tajšc w istocie "Mam dalej drukować?" Zastanówmy się najpierw, co jest potrzebne do włšczenia procedury przerywa- jšcej w proces drukowania, i sprawdŸmy niektóre z możliwych dróg przebiegu działania. Procedurę przerywajšcš można nazwać AbortProc i nadać jej następu- jšcš postać: BOOL CALLBACK AbortProc (HDC hdcPrn, int iCode) ( [kod procedury] Przed drukowaniem musisz zarejestrować procedurę przerywajšcš przez wywo- łanie SetAbortProc: SetAbortProc (hdcPrn, AbortProc) ; Wywołanie to wykonujesz przed wywołaniem StartDoc. Nie musisz "odwoływać" procedury przerywajšcej po zakończeniu drukowania. W czasie przetwarzania wywołania EndPage (to znaczy w czasie wgrywania me- tapliku do sterownika urzšdzenia i tworzenia tymczasowych plików wyjœciowych drukarki) GDI często wywołuje procedurę przerywajšcš. Parametr hdcPrn to uchwyt kontekstu urzšdzenia drukujšcego. Parametr iCode ma wartoœć 0, jeœli wszystko przebiega prawidłowo, lub SP OUTOFDISK, jeżeli moduł GDI nie ma już miejsca na dysku na tyrnczasowe pliki wyjœciowe wydruku. AbortProc musi zwrócić TRUE (wartoœć niezerowš), jeœli drukowanie ma być kon- tynuowane, albo FALSE (0), jeżeli zadanie drukowania ma być przerwane. Pro- cedura przerywajšca może być bardzo prosta, na przykład: BOOL CALLBACK AbortProc (NDC hdcPrn, int iCode) ( MSG msg ; while (PeekMessage (&msg, NULL, 0, 0, PM REMOVE)) f TranslateMessage (&msg) ; DispatchMessage (&msg) ; 1 return TRUE; Funkcja ta może wydać się nieco dziwna. Istotnie, wyglšda podejrzanie - niczym pętla komunikatów. Co w tym miejscu robi pętla komunikatów? Tak, bo to jest ona. Zauważ przy tym, że wywołuje ona PeekMessage zamiast GetMessage. Funkcję Peek- Message omówiłem przy okazji programu RANDRECT na końcu rozdziału 5. Jak zapewne pamiętasz, PeekMessage (tak jak GetMessage) zwraca sterowanie do pro- gramu z komunikatem z kolejki komunikatów programu, jednak zwraca sterowa- nie także wówczas, gdy w kolejce tej nie ma żadnych oczekujšcych komunikatów. Pętla komunikatów w funkcji AbortProc wielokrotnie wywołuje PeekMessage, je- żeli zwraca ona TRUE. Wartoœć TRUE oznacza, że funkcja PeekMessage odebrała komunikat, który może być przesłany do jednej z procedur okien programu przy 564 Częœć II: Grafika użyciu TranslateMessage i DispatchMessage. Gdy nie ma już więcej komunikatów w kolejce komunikatów programu, wartoœć zwracana przez PeekMessage wynosi FALSE, zatem AbortProc zwraca sterowanie do Windows. Jak Windows korzysta z AbortProc Kiedy program drukuje, większoœć pracy kumuluje się w czasie wywołania End- Page. Przed tym wywołaniem moduł GDI po prostu dodaje kolejne rekordy do umieszczonego na dysku metapliku przy każdym wywołaniu przez program funkcji rysujšcej GDI. Gdy GDI otrzyma wywołanie EndPage, wówczas wgrywa metaplik do sterownika urzšdzenia, po kolei dla każdego z pasm definiowanych przez sterownik urzšdzenia na stronie. GDI zapisuje w pliku wyjœcie na drukar- kę utworzone przez sterownik drukarki. Jeœli bufor nie jest aktywny, sam moduł musi wpisać to wyjœcie na drukarkę do drukarki. W czasie wywołania EndPage moduł GDI wywohxje procedurę przerywajšcš, którš napisałeœ. Zwykle parametr iCode wynosi 0, lecz jeœli GDI spostrzeże, że skoń- czyło się miejsce na dysku z powodu innych plików tymczasowych, które do- tychczas nie zostały wydrukowane, parametr iCode będzie miał wartoœć SP OUTOFDISK. (Zwykle nie sprawdza się tej wartoœci, ale jeżeli chcesz, możesz to zrobić). Następnie procedura przerywajšca przechodzi do pętli PeekMessage, aby odebrać komunikaty z kolejki komunikatów programu. Jeœli w kolejce komunikatów programu nie ma komunikatów, PeekMessage zwra- ca FALSE. Procedura przerywajšca opuszcza wówczas pętlę komunikatów i zwra- ca wartoœć TRUE do moduhx GDI, informujšc tym samym, że drukowanie ma być kontynuowane. Teraz moduł może dalej przetwarzać wywołania EndPage. Moduł GDI zatrzymuje proces drukowania, jeżeli wystšpi błšd, a głównym ce- lem procedury przerywajšcej jest umożliwienie użytkownikowi wstrzymania drukowania. W tym celu potrzebne jest nam okno dialogowe wyœwietlajšce przy- cisk Cancel. Wykonajmy te dwie czynnoœci po kolei. Najpierw dodamy procedu- rę przerywajšcš i utworzymy program PRINT2, a następnie dodamy okno dialo- gowe z przyciskiem Cancel w PRINT3, tak aby procedura przerywajšca mogła być wykorzystana. Implementacja procedury AbortProc Popatrzmy chwilę na mechanizm procedury przerywajšcej. Można jš zdefinio- wać w następujšcy sposób: BOOL CALLBACK AbortProc (HDC hdcPrn, int iCode) ( MSG msg ; while (PeekMessage (&msg, NULL, 0, 0, PM REMOVE)) ( TranslateMessage (&msg) : DispatchMessage (&msg) ; ) return TRUE; Rozdział 13: Drukowanie 565 Aby coœ wydrukować, dajesz systemowi Windows wskaŸnik do tej procedury: SetAbortProc (hdcPrn, AbortProc) ; Wywołanie to wykonujesz przed wywołaniem StartDoc. I to wszystko. No, nie zupełnie. Pominęliœmy ważne zagadnienie pętli PeekMessage w funkcji AbortProc - to istotny problem. AbortProc jest wywoływana jedynie wówczas, gdy ' twój program jest w trakcie drukowania. Jeœli otrzymasz komunikat w procedu- rze AbortProc i przeœlesz go do procedury swojego własnego okna, możesz uzy- skać niepożšdane efekty. Użytkownik może ponownie wybrać Print z menu (a pro- gram jest właœnie w œrodku procedury druku), może wczytać nowy plik pod- , czas gdy program próbuje wydrukować poprzednik, może nawet zamknšć pro- gram! Jeżeli tak się stanie, wszystkie okna twojego programu zostanš pozamyka- ne. Ewentualnie wyjdziesz z procedury drukujšcej, lecz nie będziesz miał się gdzie udać, chyba że do nieistniejšcej już procedury okna. To dopiero łamigłówka. A twój program nie jest na niš przygotowany. Dlatego, gdy ustawiasz procedurę przerywajšcš, powinieneœ najpierw w taki sposób zdez- aktywować okna swojego programu, aby nie mogły być wprowadzane żadne dane, ani za pomocš klawiatury, ani myszy. Robi się to tak: EnableWindow (hwnd, FALSE) ; To zapobiega wprowadzeniu danych z klawiatury lub z myszy do kolejki komu- nikatów. Zatem użytkownik nie może nic z twoim programem zrobić, dopóki ten drukuje. Gdy zakończy się drukowanie, ponownie uaktywniasz okno: EnableWindow (hwnd, TRUE) ; Po co więc zawracamy sobie głowę wywołaniami TranslateMessage i DispatchMes- sage w AbortProc, skoro żadne komunikaty myszy lub klawiaturowe nie trafiš do kolejki komunikatów? Rzeczywiœcie, TranslateMessage nie jest koniecznie potrzebne (choć prawie zawsze się je dołšcza). Natomiast DispatchMessage musimy użyć na wypadek, gdyby do kolejki komunikatów trafił WMţPAINT. Jeœli WM-PAINT nie zostanie właœciwie przetworzony za pomocš BeginPaint i EndPaint w proce- durze okna, komunikat ten pozostanie w kolejce i zakorkuje przebieg prac, po- nieważ PeekMessage nigdy nie zwróci FALSE. Gdy dezaktywujesz swoje okno na czas drukowania, twój program zamiera na ekranie. Jednak użytkownik może przejœć do innego programu i wykonać w nim jakieœ prace, bufor zaœ będzie w tym czasie dalej przesyłał pliki wyjœciowe na dru- karkę. Program PRINT2, pokazany na rysunku 13-8, dodaje do PRINTI procedurę prze- rywajšcš oraz niezbędnš podstawę - wywołanie funkcji AbortProc oraz dwa wy- wołania EnableWindow. Pierwsze z nich ma na celu dezaktywację okna, zaœ dru- gie ponownie je uaktywnia. PRI NT2 . C /* PRINT2.C - Drukowanie z procedurd przerywajdcš (c) Charles Petzold, 1998 */ #include 566 Częœć II: Grafika (cišg daiszy ze strony 565) HDC GetPrinterDC (void) ; // w GETPRNDC.C void PageGDICaIIs (HDC, int, int) ; // w PRINT.C HINSTANCE hInst ; TCHAR szAppName[] = TEXT ("Print2") ; TCHAR szCaption[] = TEXT ("Print Program 2 (Abort Procedure)") ; BOOL CALLBACK AbortProc (HDC hdcPrn, int iCode) MSG msg ; while (PeekMessage (&msg, NULL, 0, 0, PMţREMOVE)) ( TranslateMessage (&msg) ; DispatchMessage (&msg) ; return TRUE ; BOOL PrintMyPage (HWND hwnd) ( static DOCINFO di = ( sizeof (DOCINFO), TEXT ("Print2: Printing") 1 ; BOOL bSuccess = TRUE ; HDC hdcPrn ; short xPage, yPage ; if (NULL = (hdcPrn = GetPrinterDC ())) return FALSE ; xPage = GetDeviceCaps (hdcPrn, HORZRES) ; yPage = GetOeviceCaps (hdcPrn, VERTRES) ; EnableWindow (hwnd, FALSE) ; SetAbortProc (hdcPrn, AbortProc) ; if (StartDoc (hdcPrn, &di) > 0) ( if (StartPage (hdcPrn) > 0) P PageGDICaIIs (hdcPrn, xPage, yPage) ; /i if (EndPage (hdcPrn) > 0) EndDoc (hdcPrn) ; else bSuccess = FALSE ; 4ţi HD else vo bSuccess = FALSE ; EnableWindow (hwnd, TRUE) ; HII DeleteDC (hdcPrn) ; TCf return bSuccess ; TCF BOC Rysunek 13-8. Program PRINT2 Rozdział 13: Drukowanie 567 Tworzenie okna dialogowego Printing Program PRINT2 nie ma jeszcze zadowalajšcej formy. Po pierwsze, nie informu- je wprost, kiedy jest w trakcie drukowania, a kiedy je zakończył. Tylko brak reak- cji na działanie myszš sugeruje, że program musi nadal przetwarzać procedurę PrintMyPage. PRINT2 także nie umożliwia użytkowrukowi przerwania wydru- ku w czasie buforowania. Przecież większoœć programów windowsowych daje użytkownikom szansę anu- lowania właœnie trwajšcej operacji wydruku. Na ekranie pojawia się małe okno dialogowe, zawierajšce tekst oraz przycisk z etykietš Anuluj. Program wyœwie- tla to okno dialogowe przez cały czas, gdy GDI zapisuje wyjœcie na drukarkę w pliku dyskowym lub (jeœli bufor jest nieaktywny) gdy drukarka drukuje. Jest to niemodalne okno dialogowe, a ty musisz napisać jego procedurę. W nazwach wspomnianego okna dialogowego i jego procedury często używane jest słowo abort (przerwij). Aby wyraŸnie odróżnić tę procedurę okna dialogowe- go od procedury przerywajšcej, będę nazywał procedurę tego okna dialogowego procedurš okna dialogowego drukowania. Procedura przerywania (o nazwie AbortProc) oraz procedura okna dialogowego drukowania (którš nazwę PrintDlg- Proc) to dwie oddzielne funkcje eksportowane. Jeżeli chcesz profesjonalnie dru- kować w stylu Windows, musisz mieć je obydwie. Oto jak będš współdziałały te funkcje: pętla PeekMessage w AbortProc musi być zmodyfikowana tak, aby przesyłać komunikaty dla niemodalnego okna dialogo- wego z procedury okna dialogowego. PrintDlgProc musi przetworzyć komuni- katy 4VMţCOMMAND, aby sprawdzić stan przycisku Cancel. Jeœli przycisk Can- cel zostanie wybrany, funkcja nada zmiennej globalnej o nazwie bUserAbort war- toœć TRUE. Wartoœć zwracana przez AbortProc jest odwrotnoœciš bUserAbort. Jak pamiętasz, AbortProc zwraca TRUE, by kontynuować drukowanie, aFALSE, by je przerwać. W PRINT2 zawsze zwracane było TRUE. Teraz, jeżeli w oknie dialo- gowym drukowania użytkownik kliknie przycisk Cancel, zwrócona zostanie wartoœć FALSE. To rozwišzanie logiczne zostało zaimplementowane w progra- mie PRINT3, pokazanym na rysunku 13-9. PRI NT3 . C ţ* PRINT3.C - Drukowanie z wykorzystaniem okna dialogowego drukowania (c) Charles Petzold, 1998 */ 4linclude HDC GetPrinterDC (void) ; // w GETPRNDC.C void PageGDICaIIs (HDC, int, int) ; // w PRINT.C HINSTANCE hInst ; TCHAR szAppNameC] = TEXT ("Print3") ; TCHAR szCaption[] = TEXT ("Print Program 3 (Dialog Box)") ; BOOL bUserAbort ; 568 Częœć II: Grafika (cišg dalszy ze strony 567) HWND hDlgPrint ; BOOL CALLBACK PrintDlgProc (HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam) ( switch (message) ( case WM_INITDIALOG: SetWindowText (hDlg, szAppName) ; EnableMenuItem (GetSystemMenu (hDlg, FALSE), SC CLOSE, MF GRAYED) ; return TRUE ; case WM_COMMAND: bUserAbort = TRUE ; EnableWindow (GetParent (hDlg), TRUE) ; DestroyWindow (hDlg) ; hDlgPrint = NULL ; return TRUE ; ) return FALSE ; ) BOOL CALLBACK AbortProc (HDC hdcPrn, int iCode) ( MSG msg ; while (!bUserAbort && PeekMessage (&msg, NULL, 0, 0, PMţREMOVE)) ( if (!hDlgPrint ţţ !IsDialogMessage (hDlgPrint, &msg)) ( TranslateMessage (&msg) ; DispatchMessage (&msg) ; 1 1 return !bUserAbort ; ) BOOL PrintMyPage (HWND hwnd) ( static DOCINFO di = ( sizeof (DOCINFO), TEXT ("Print3: Printing") 1 ; BOOL bSuccess = TRUE ; HDC hdcPrn ; int xPage, yPage ; if (NULL = (hdcPrn = GetPrinterDC ())) return FALSE ; xPage = GetDeviceCaps (hdcPrn, HORZRES) ţ; yPage = GetDeviceCaps (hdcPrn, VERTRES) ; EnableWindow (hwnd, FALSE) ; bUserAbort = FALSE ; hDlgPrint = CreateDialog (hInst, TEXT ("PrintDlgBox"), hwnd, PrintDlgProc) ; Rozdział 13: Drukowanie 569 SetAbortProc (hdcPrn, AbortProc) ; if (StartDoc (hdcPrn, &di) > 0) t if (StartPage (hdcPrn) > 0) f PageGDICaIIs (hdcPrn, xPage, yPage) ; if (EndPage (hdcPrn) > 0) EndDoc (hdcPrn) ; else bSuccess = FALSE ; ' else bSuccess = FALSE ; if (!bUserAbort) ( EnableWindow (hwnd, TRUE) ; DestroyWindow (hOlgPrint) ; J DeleteDC (hdcPrn) ; return bSuccess && !bUserAbort ; PRINT.RC (fragmenty) // Microsoft Developer Studio generated resource script. ţţinclude "resource.h" A' ţţinclude "afxres.h" ///////////////////////////////////////////////////////////////////////////// // Dialog 4 PRINTDLGBOX DIALOG DISCARDABLE 20, 20, 186, 63 STYLE DS_MODALFRAME ţ WSţPOPUP ţ WS llISIBLE ţ WS CAPTION ţ WS SYSMENU FONT 8, "MS Sans Serif" BEGIN PUSHBUTTON "Cancel",IDCANCEL,67,42,50,14 CTEXT "Cancel Printing",IDC STATIC,7,21,172,8 END Rysunek 13-9. Program PRINT3 Na czas eksperymentóţ z PRINT3 możesz wyłšczyć buforowanie druku. W prze- ciwnym wypadku przycisk Cancel, który widoczny jest jedynie wtedy, gdy bu- for odbiera dane od PRINT3, może zniknšć zbyt szybko, abyœ mógł go kliknšć. Nie dziw się, jeœli działania nie zostanš zatrzymane natychmiast po kliknięciu przycisku Cancel, w szczególnoœci w przypadku powolnej drukarki. Drukarka ma wewnętrzny bufor, który musi zostać opróżniony, zanim zostanie zatrzyma- na. Kliknięcie Cancel jedynie informuje GDI, żeby nie przesyłał więcej danych do bufora drukarki. 570 Częœć II: Grafika Do PRINT3 dodane zostały dwie zmienne globalne: zmienna boolowska o na- zwie bUserAbort oraz uchwyt okna dialogowego o nazwie hDlgPrint. Funkcja Print- MyPage nadaje poczštkowo zmiennej bUserAbort wartoœć FALSE i podobnie jak w PRINT2 główne okno programu jest zdezaktywowane. WskaŸnik do AbortProc jest używany w wywołaniu SetAbortProc, a wskaŸnik do PrintDlgProc - wyko- rzystywany w wywołaniu CreateDialog. Uchwyt okna zwracany z CreateDialog zostaje zapisany na zmiennej hDIgPrint. Oto jak wyglšda pętla komunikatu w procedurze AbortProc: while (!bUserAbort && PeekMessage (&msg, NULL, 0, 0, PM REMOVE)) ( if (!hDlgPrint ţţ !IsDialogMessage (hDlgPrint, &msg)) ( Translatehlessage (&msg) DispatchMessage (&msg> t return !bUserAbort ; Wywołuje ona funkcję PeekMessage jedynie wówczas, gdy zmienna bUserAbort ma wartoœć FALSE - to znaczy, gdy użytkownik jeszcze nie zrezygnował z operacji drukowania. Funkcja IsDialogiVlessage jest potrzebna do przesłania komunikatu do niemodalnego okna dialogowego. W przypadku niemodalnych okien dialo- gowych normalnš procedurš jest sprawdzenie uchwytu okna dialogowego przed wykonaniem wywołania. Procedura AbortProc zwraca odwrotnoœć wartoœci zmien- nej bUserAbort. Poczštkowo bUserAbort ma wartoœć FALSE, zatem AbortProc zwraca TRUE, co znaczy, że drukowanie ma być kontynuowane. Lecz zmiennej bUserA- bort można nadać wartoœć TRUE w procedurze okna dialogowego drukowania. Funkcja PrintDlgProc jest stosunkowo prosta. W czasie przetwarzania WM INIT DIALOG funkcja przypisuje nagłówkowi okna nazwę programu i dezaktywuje opcję Zamknij menu systemowego. Jeżeli użytkownik kliknie przycisk Cancel, PrintDlgProc otrzyma komunikat WM COMMAND: case WM_COMMAND: bUserAbort = TRUE ; EnableWindow (GetParent (h0lg), TRUE) ; DestroyWindow (hDlg) ; hOlgPrint = NULL ; return TRUE ; Nadanie zmiennej bUserAbort wartoœci TRUE oznacza, że użytkownik postano- wił anulować operację drukowania. Główne okno jest uaktywniane, okno dialo- gowe zaœ - niszczone. (Ważne jest, abyœ te dwa działania przeprowadził w takiej właœnie kolejnoœci. W przeciwnym wypadku jakiœ inny program działajšcy w Win- dows stanie się programem aktywnym, twój zaœ mógłby zniknšć gdzieœ w tle). Standardowo zmiennej hDlgPrint przypisywane jest NULL, aby zapobiec wywo- łaniu IsDialogMessage w pętli komunikatu. To okno dialogowe otrzymuje komunikaty, wyłšcznie wtedy, gdy AbortProc otrzy- muje komunikaty z PeekMessage i przesyła je do procedury okna dialogowego za pomocš IsDialogMessage. Natomiast procedura AbortProc wywoływana jest tylko wtedy, gdy GDI przetwarza funkcję EndPage. Jeœli moduł ten stwierdzi, że war- toœć zwracana z AbortProc to FALSE, ponownie przekazuje sterowanie z wywo- Rozdział 13: Drukowanie 571 łania EndPage do funkcji PrintMyPage. Nie zwraca kodu błędu. W tym rnomencie PrintMyPage uznaje, że strona została zakończona i wywołuje funkcję EndDoc. Jednak nic nie zostaje wydrukowane, ponieważ moduł GDI nie zakończył prze- twarzania wywołania EndPage. Trzeba jeszcze trochę posprzštać. jeżeli użytkownik nie anulował zadania dru- kowania z okna dialogowego, okno to pozostaje wyœwietlone. PrintMyPage po- nownie uaktywnia swoje okno główne oraz niszczy okno dialogowe: if (!bUserAbort) EnableWindow (hwnd, TRUE) ; DestroyWindow (hDlgPrint) ; ) O tym, co się wydarzyło, informuje stan dwóch zmiennych: bUserAbort mówi, czy użytkownik zrezygnował z zadania drukowania, bSuccess zaœ sygnalizuje wystš- pienie błędu. Możesz z tymi zmiennymi zrobić, co chcesz. PrintMyPage po pro- stu przeprowadza logicznš operację AND i zwraca jej wynik do WndProc: ! return bSuccess && !bUserAbort ; , Drukowanie w programach POPPAD Teraz jesteœmy już gotowi, aby wyposażyć w umiejętnoœć drukowania serię pro- gramów POPPAD i uznać je za zakończone. W tym celu potrzebne będš poszcze- gólne pliki POPPAD z rozdziału 11 oraz plik POPPRNT.C, przedstawiony na ry- sunku 13-10. POPPRNT.C /* POPPRNT.C - Funkcje drukujdce edytora */ iţinclude iţinclude #include "resource.h" BOOL bUserAbort ; HWND hDlgPrint ; BOOL CALLBACK PrintDlgProc (HWND hDlg, UINT msg, WPARAM wParam, LPARAM lParam) switch (msg) case WM_INITDIALOG : EnableMenuItem (GetSystemMenu (hDlg, FALSE), SC CLOSE, MF GRAYED) ; return TRUE ; case WM_COMMAND : bUserAbort = TRUE ; EnableWindow (GetParent (hDlg), TRUE) ; DestroyWindow (hDlg) ; hDlgPrint = NULL ; return TRUE ; Częœć II: Grafika ţ Rozdział ' 572 (cišg dalszy ze strony 571) ţ i f return FALSE ; ) BOOL CALLáACK AbortProc (HDC hPrinterDC. int iCode) ( MSG msg : while (!bUserAbort && PeekMessage (&msg, NULL, 0, 0, PMţREMOVE)) if (!hDlgPrint ţţ !IsOialo9Message (hDlgPrint, &msg)) t TranslateMessage (&msg) : DispatchMessage (&msg) : ) ) return !bUserAbort ; BOOL PopPrntPrintFile (HINSTANCE hInst, HWND hwnd, HWND hwndEdit, PTSTR szTitleName) ( stdtic DOCINFO di = ( sizeof (DOCINFO) ) : static PRINTDLG pd ; BOOL bSuccess ; yChar iCharsPerLine, iLinesPerPage, iTotalLines, int iTotalPages, iPage, iLine, iLineNum : PTSTR pstrBuffer : TCHAR szJobName C64 + MAXţPATH7 : TEXTMETRIC tm : WORD iColCopy, iNoiColCopy : // Wywolaj okno dialogowe Drukuj pd.lStructSize = sizeof (PRINTDLG) ; pd.hwndOwner = hwnd ; pd.hDevMode = NULL : pd.hDevNames = NULL : pd.hDC = NULL ; pd.Flags = PD_ALLPAGES ţ PD_COLLATE PD_RETURNDC ţ PD NOSELECTION : pd.nFromPage = 0 pd.nToPa9e = 0 pd.nMinPa9e pd.nMaxPage = 0 pd.nCopies = I pd.hInstance = NULL : pd.lCustOata = OL ; pd.lpfnPrintHook = NULL ; pd.lpfn5etupHook = NULL : pd.lpPrintTemplateName = NULL : pd.lpSetupTemplateName = NULL : pd.hPrintTemplate = NULL ; pd.hSetupTemplate = NULL : if (!PrintDlg (&pd)) return TRUE ; 573 Rozdział 13: Drukowanie if (0 = (iTotalLines = SendMessa e (hwndEdit, EM GETLINECOUNT, 9 ,;: 0, 0))) return TRUE ; // Oblicz dla pliku potrzebne wymiary GetTextMetrics (pd.hDC, &tm) : yChar = tm.tmHeight + tm.tmExternalLeading : iCharsPerLine = GetDeviceCaps (pd.hDC, HORZRES) / tm.tmAveCharWidth : iLinesPerPage = GetDeviceCaps (pd.hDC, VERTRES) / yChar ; g iTotalPages = (iTotalLines + iLinesPerPage - 1) / iLinesPerPa e ; // Alokuj bufor dla każdej linii tekstu + pstrBuffer = malloc (sizeof (TCHAR) * (iCharsPerLine 1)) : i: // Wyœwietl okno dialogowe drukowania EnableWindow (hwnd, FALSE) ; k; bSuccess = TRUE : bUserAbort = FALSE : 1 Print = Create0ialog (hInst, TEXT ("PrintDlgBox"), hD g hwnd, PrintDlgProc) . SetDlgItemText (hDlgPrint, IDCţFILENAME, szTitleName) : SetAbortProc (pd.hDC, AbortProc) : // Rozpocznij dokument GetWindowText (hwnd, szJobName, sizeof (szJobName)) : di.lpszDocName = szJobName : if (StartDoc (pd.hDC, &di) > 0) Py ( // Do sortowania potrzebna jest ta pętla oraz iNoiColCo for (iColCopy = 0 : ? pd.nCo ies : 1) : < ((WORD) pd.Flags & PD COLLATE p iColCopy iColCopy++) for (iPage = 0 ; iPage < iTotalPages ; iPage++) for (iNoiColCopy = 0 ' ţ , pd.nCo ies); iNoiColCopy < (pd.Flags & PD COLLATE 1 y P iNoiColCopy++) // Rozpocznij:stronę if (StartPage (pd.hDC) < 0) bSuccess = FALSE : ţ.I break ; ) 574 Częœć II: Grafika Roţ (cišg dalszy ze strony 573) // Drukuj linie na każdej stronie for (iLine = 0 ; iLine < iLinesPerPage ; iLine++) ( iLineNum = iLinesPerPage * iPage + iLine ; if (iLineNum > iTotalLines) break ; I *(int *) pstrBuffer = iCharsPerLine ; TextOut (pd.hDC, 0, yChar * iLine. pstrBuffer, (int) SendMessage (hwndEdit, EMţGETLINE, (WPARAM) iLineNum, (LPARAM) pstrBuffer)); ) if (EndPage (pd.hDC) < 0) ( bSuccess = FALSE ; break ; if (bUserAbort) break ; ) if (!bSuccesJ ţţ bUserAbort) break ; ) if (!bSuccess ţţ bUserAbort) break ; ) ) else bSuccess = FALSE ; if (bSuccess) EndDoc (pd.hDC) ; if (!bUserAbort) ( EnableWindow (hwnd, TRUE) ; DestroyWindow (hDlgPrint) ; } free (pstrBuffer) ; DeleteDC (pd.hDC) ; return bSuccess && !bUserAbort ; ? Rysunek 13-10. Plik POPPRNT.C umożliwiajšcy drukowanie z programu POP- PAD Rozdział 13: Drukowanie 575 Z odnie z arkanami sztuki, program POPPAD jest tak prosty, jak to tylko możli- we, gdyż wykorzystuje wysoki poziom "inteligencji" systemu Windows. Plik POPPRNT.C demonstruje, jak używać funkcji PrintDlg. Funkcja ta znajduje się w bibliotece podstawowych okien dialogowych i wykorzystuje strukturę typu PRIN'TDLG. Opcję Drukuj umieszcza się zazwyczaj w menu Plik programu (czyli Print w menu File). Jeœli użytkownik wybierze opcję Drukuj, program może zainicjować pola struktury PRINTDLG i wywołać PrintDlg. Funkcja PrintDlg wyœwietla okno dialogowe pozwalajšce użytkownikowi wybrać zakres stron przeznaczonych do drukowania i dlatego szczególnie przydatne w programach, które tak jak POPPAD mogš drukować dokumenty wielostroni- cowe. W oknie tym znajduje się także pole edycji, pozwalajšce na wprowadzenie liczby kopii oraz pole wyboru z etykietš Sortuj. Sortowanie kopii ma wpływ na kolejnoœć stron w przypadku drukowania kilku kopii. Jeœli na przykład, doku- ment ma trzy strony i użytkownik zażšda wydrukowania trzech kopii, program może je wydrukować na dwa sposoby. Przy sortowaniu kopii strony drukowane sš w kolejnoœci 1, 2, 3, 1, 2, 3, 1, 2, 3. Kopie niesortowane majš kolejnoœć 1, 1, 1, 2, 2, 2, 3, 3, 3. Wydrukowanie kopii we właœciwej kolejnoœci zależy od twojego pro- gramu. Okno dialogowe Drukuj umożliwia użytkownikowi także wybranie drukarki in- nej niż domyœlna. Znajduje się w nim przycisk z etykietš Właœciwoœci, wywołu- jšcy okno dialogowe trybu urzšdzenia. Pozwala ono użytkownikowi przynajmniej na wybór orientacji strony - Pionowa lub Pozioma. Po powrocie z funkcji PrintDlg pola w strukturze PRINTDLG wskazujš zakres stron przeznaczonych do wydruku oraz informujš, czy w przypadku kilku kopii majš one zostać posortowane. Struktura także dostarcza gotowy do użycia uchwyt kontekstu urzšdzenia drukujšcego. W POPPRNT.C funkcja PopPrntPrintFile (wywoływana w POPPAD, gdy użytkow- ruk wybierze opcję Print z menu File) wywołuje PrintDlg, a następnie przystępu- je do wydruku pliku. Funkcja PopPrntPrintFile wykonuje potem obliczenia, aby okreœlić liczbę znaków, które mogš zmieœcić się w linii, oraz liczbę linii, które po- winny znaleŸć się na stronie. W procesie tym wykorzystywane sš wywołania funk- cji GetTextMetrics do okreœlenia rozmiaru znaku oraz funkcji GetDeviceCaps do okreœlenia liczby znaków w linii oraz liczby linii na stronie. Całkowitš liczbę linii w dokumencie (zmienna iTotalLines) program uzyskuje po- przez przesłanie komunikatu EM GETLINECOLTNT do kontrolki edytora. Z pa- mięci lokalnej alokowany jest bufor przechowujšcy zawartoœć każdej linii. Pierw- sze słowo w buforze każdej linii ma wartoœć równš liczbie znaków w linii. Prze- słanie kontrolce edytora komunikatu EM GETLINE powoduje skopiowanie linii do bufora; następnie linia ta przesyłana jest do kontekstu urzšdzenia drukujšce- go za pomocš TextOut. (POPPRNT.C nie jest na tyle rozgarnięty, żeby zawijać li- nie wykraczajšce poza szerokoœć strony drukarki. Z technikš zawijania zbyt dłu- gich lini.i zapoznamy się w rozdziale 17). Zwróć uwagę, że schemat drukowania dokumentu obejmuje dwie pętle for, za- leżne od liczby kopii. Pierwsza z nich korzysta ze zmiennej o nazwie iColCopy 576 Częœć IIţ Grafika i działa, gdy użytkownik zażšda sortowania kopii; druga korzysta ze zmiennej iNoiCoICopy i odpowiada kopiom niesortowanym. Program wychodzi z pgtli for iterujšcej po numerach stron, jeœli StartPage lub End- Page zwrócš błšd bšdŸ jeœli bUserAbort ma wartoœć TRUE. Jeżeli wartoœciš zwra- canš z procedury przerywajšcej jest FALSE, EndPage nie zwraca błędu. Z tego powodu bUserAbort testowana jest jawnie przed rozpoczęciem następnej strony. Jeœli nie znaleziono żadnych błędów, wykonywane jest wywołanie EndDoc: if (bSuccess) EndDoc (pd.hDC) ; Możesz zrobić eksperyment i wydrukować z POPPAD dokument wielostronico- wy. Masz możliwoœć monitorowania postępu wydruku w oknie zadań wydruku systemu Windows. Najpierw plik, który jest drukowany, pokazuje się w tym oknie, gdy GDI skończy przetwarzanie pierwszego wywołania EndPage. W tym momen- cie bufor rozpoczyna przesyłanie pliku do drukarki. Gdy w tym czasie anulujesz z POPPAD zadanie drukowania, bufor także zrezygnuje z drukowania - na sku- tek zwrócenia wartoœci FAISE z procedury przerywajšcej. Gdy plik pojawi się w oknie zadań wydruku, możesz także anulować wydruk wybierajšc Anuluj dru- kowanie z menu Dokument. W tym przypadku trwajšce w POPPAD wywołanie EndPage zwraca błšd. Programiœci od niedawna pracujšcy w Windows często ulegajš niezwykłej obse- sji na tle funkcji AbortProc. Jest ona rzadko używana przy drukowaniu. Jak widać w POPPAD, użytkownik może anulować zadanie drukowania prawie w każdej chwili poprzez okno drukowania POPPAD bšdŸ przez okno zadań drukowania. Żadna z tych opcji nie wymaga od programu użycia funkcji AbortProc. Jedyne miejsce, gdzie funkcja ta byłaby dopuszczalna w POPPAD, znajduje się między wywołaniem StartDoc i pierwszym wywołaniem EndPage. Jednak kod ten wyko- nywany jest tak szybko, że AbortProc nie jest potrzebna. Rysunek 13-11 pokazuje prawidłowš sekwencję wywołań funkcji drukujšcych przy drukowaniu dokumentu wielostronicowego. Najlepsze miejsce poszukiwa- nia wartoœci TRUE dla bUserAbort jest po każdym wywołaniu EndPage. Funkcja EndDoc używana jest tylko wówczas, gdy poprzednie funkcje drukujšce przebie- gły bez błędów. Praktycznie wygenerowanie błędu przez jakiekolwiek wywoła- nie funkcji drukujšcej kończy przedstawienie i można iœć do domu. Rozdział 13: Drukowanie Rysunek 13-11. Sekwencja wywołań funkcji wydruku wielostronicowego Rozdział 14 ' ' ' Blt Bţt B ţtma Bitmapa to dwuwymiarowa prostokštna tablica bitów odpowiadajšca pikselom ob- razu. Gdy obrazy rzeczywiste sš rejestrowane jako bitmapy, obraz dzielony jest na siatkę, a jednostkš próbkowara jest piksel. Wartoœć każdego piksela w bitmapie okreœla w przybliżeniu kolor obrazu w danej jednostce siatki. Bitmapy monochro- matyczne wymagajš jedynie po jednym bicie na piksel; bitmapy w odcieniach sza- roœci oraz kolorowe bitmapy potrzebujš wielu bitów na zapisanie każdego piksela. Bitmapy stanowiš jeden z dwóch sposobów zapisu informacji graficznej w pro- gramie Windows. Drugim jest metaplik, który omówię w rozdziale 18. Metaplik to opis rysunku, a nie jego cyfrowa reprezentacja. Jak póŸniej przedstawię ze szczegółami, Microsoft Windows 3.0 wprowadził for- mat nazwany bitmapš niezależnš od urzšdzenia (ang. device-independent bitmap - DIB), który omówię w następnym rozdziale. W tym rozdziale zajmiemy się obiek= tem bitmapy GDI, który obsługuje bitmapy zaimplementowane w Windows przed nastaniem DIB. Programy przykładowe w tym rozdziale dowodzš, że obsługa bit- map, poprzedzajšcych w Windows format DIB, jest nadal użyteczna i potrzebna. Podstawowe wiadomoœci o bitmapach Zarówno bitmapy, jak i metapliki majš swoje miejsce w grafice komputerowej. Bitmapy sš często używane do zaprezentowania bardzo złożonych obrazów przedstawiajšcych œwiat rzeczywisty, takich jak digitalizowane fotografie czy zarejestrowane obrazy wideo. Metapliki lepiej się nadajš do zapisania obrazów tworzonych przez ludzi lub przez maszyny, takich jak rysunki architektoniczne. Zarówno bitmapy, jak i metapliki mogš istnieć w pamięci komputera lub zostać zapisywane na dysku w postaci plików, obydwie zaœ postacie mogš być transfe- rowane między aplikacjami Windows za poœrednictwem Schowka. ftóżnica między bitmapami i metaplikami to różnica między grafikš rastrowš a grafikš wektorowš. Grafika rastrowa traktuje urzšdzenia wyjœciowe w katego- riach pojedynczych pikseli, a grafika wektorowa jako kartezjański układ współ- rzędnych, w którym można rysować linie i obiekty wypełnione. Większoœć wyj- œciowych urzšdzeń graficznych to obecnie urzšdzenia rastrowe, na przykład monitory, drukarki mozaikowe, drukarki laserowe i drukarki atramentowe. Na- tomiast ploter pisakowy jest urzšdzeniem wyjœciowym wektorowym. Bitmapy majš dwie zasadnicze wady. Po pierwsze, podatne sš na problemy cha- rakterystyczne dla zależnoœci od urzšdzeń. Najbardziej oczywistym jej przejawem jest kolor. Wyœwietlenie kolorowej bitmapy na monochromatycznym urzšdzeniu 580 II: Grafika rzadko daje zadowalajšce efekty. Innym problemem jest narzucanie przez bitmapę okreœlonej rozdzielczoœci oraz proporcji obrazu. Wprawdzie bitmapy można roz- cišgać i kompresować, ale proces ten generalnie polega na powtórzeniu lub wy- rzucaniu rzędów albo kolumn pikseli, a to może prowadzić do zniekształceń ska- lowanego obrazu. Natomiast metaplik może być dowolnie skalowany bez znie- kształceń. Drugš poważnš wadš bitmap jest to, że do ich zapisania potrzeba dużej iloœci pamięci. Na przykład bitmapowa reprezentacja całego 16-kolorowego ekranu VGA (Video Graphics Array) 640 na 480 pikseli wymaga ponad 150 KB; obraz o wymia- rach 1024 na 768 pikseli o 24 bitach na piksel wymaga ponad 2 MB. Metapliki zwykle potrzebujš znacznie mniej miejsca na zapisanie w pamięci niż bitmapy. Wymagania pamięciowe bitmapy zależš od rozmiaru obrazu oraz liczby użytych kolorów, podczas gdy iloœć pamięci potrzebnej dla metapliku zależy od złożono- œci obrazu oraz liczby indywidualnych instrukcji GDI, które zawiera. Zdecydowanš przewagš nad metaplikami daje bitmapom szybkoœć. Kopiowa- nie bitmapy na ekrań monitora jest zwykle znacznie szybsze niż rendering meta- pliku. W ostatnich latach techniki kompresji pozwoliły na œcieœnienie bitmap do takich rozmiarów, jakie umożliwiajš efektywne transmitowanie po łšczach tele- fonicznych i szerokie stosowanie na stronach World Wide Web w Internecie. Skšd się biorš bitmapy? Obrazy bitmapowe mogš być tworzone ręcznie, na przykład przy użyciu pro- gramu Paint wchodzšcego w skład Windows 98. Osoba, która racu e w rastro- " p j wym programie "malujšcym zamiast w wektorowym programie "rysujšcym", wychodzi z założenia, że obraz będzie zbyt skomplikowany i nie warto go two- rzyć za pomocš linii i plam. Bitmapy można także tworzyć za pomocš algorytmów realizowanych przez kod komputerowy. Choć większoœć algorytmicznie generowanych obrazów może być zapisana w metapliku grafiki wektorowej, to jednak powierzchnie z dużš liczbš detali lub fraktale wymagajš bitmap. Dzisiaj bitmapy sš często stosowane do obrazów ze œwiata rzeczywistego, a róż- ne urzšdzenia pozwalajš przenosić obrazy rzeczywiste do komputera. Sprzęt ten zazwyczaj wykorzystuje coœ, co nazywa się elementem ze sprzężeniem ładunko- wym (ang. charge-coupled device, - CCD) i po naœwietleniu generuje ładunek elek- tryczny. Niekiedy te ogruwa CCD sš ułożone w tablicę, po jednym na piksel. Aby obrużyć koszty, do zeskanowania obrazu może być użyty pojedynczy rzšd CCD. Najstarszym z urzšdzeń CCD zwišzanych z komputerami jest skaner. Wykorzy- stuje on rzšd CCD, które przesuwajš się po powierzchni obrazu drukowanego , takiego jak fotografia. CCD generujš ładunki elektryczne, w zależnoœci od natę- żenia œwiatła. Konwertery analogowo-cyfrowe przekształcajš ładunki na liczby , które mogš następnie utworzyć bitmapę. Kamery wideo także używajš tablic ko- mórek CCD do rejestrowania obrazu. Generalnie, obrazy takie zapisywane sš na taœmie magnetowidowej. Można jednak przesłać wyjœcie z wideo do urzšdzenia wprowadzajšcego i zapisujšcego obrazy, które przekształca analogowy sygnał wi- Rozdział 14: Bitmapy i BitBlty deo w tablice wartoœci pikseli. Takie urzšdzenie może być używane z każdym kompatybilnym Ÿródłem wideo, takim jak magnetowid, odtwarzacz płyt lasero- wych lub DVD, a nawet bezpoœrednio z dekoderem sygnału kablowego. Od niedawna, dzięki coraz niższym cenom, aparaty cyfrowe stały się dostępne dla przeciętnego użytkownika. Często wyglšdajš one jak zwykłe aparaty, lecz zamiast filinu do utrwalenia obrazu wykorzystywana jest tablica CCD, a we- wnętrzny przetwornik analogowo-cyfrowy pozwala na zapisanie cyfrowego ob- razu bezpoœrednio w pamięci aparatu. Na ogół współpraca aparatu z kompute- rem odbywa się przez port szeregowy. Rozmiary bitmap Bitmapa ma kształt prostokšta o okreœlonych wymiarach. Wysokoœć i szerokoœć obrazu wyrażone sš w pikselach. Na przykład ta siatka może reprezentować bar- dzo małš bitmapę o szerokoœci 9 pikseli i wysokoœci 6 pikseli lub krócej 9 na 6: 0 1 2 3 4 5 6 7 8 0' 3 4,, Zgodnie z przyjętš konwencjš, w skróconym zapisie wymiarów bitmapy szero- koœć podawana jest jako pierwsza. Ta bitmapa ma łšcznie 9x6, czyli 54 piksele. Na okreœlenie szerokoœci i wysokoœci bitmapy często będę używał symboli cx oraz cy. "C" pochodzi od angielskiego count - liczyć, zatem cx i cy to odpowiednio liczba pikseli wzdłuż osi x (poziomej) oraz osi y (pionowej). Posługujšc się współrzędnymi x oraz y możemy wskazać na bitmapie jakiœ okre- œlony piksel. Zazwyczaj (lecz, jak się przekonamy, nie zawsze) za poczštek bit- mapy przyjmowany jest lewy górny róg obrazu, tak jak zostały ponumerowane piksele powyższej siatki. Piksel w prawym dolnym rogu tej bitmapy ma współ- rzędne (8, 5), czyli o jeden mniej niż szerokoœć i wysokoœć bitmapy, ponieważ numerowanie rozpoczyna się od zera. Wymiary bitmapy sš często okreœlane jako jej rozdzielczoœć, lecz ta nazwa jest problematyczna. Mówimy, że nasz monitor ma rozdzielczoœć 640 na 480, chociaż nasze drukarki laserowe majš rozdzielczoœć 300 dpi (kropek na cal). Wolę to ostat- nie zastosowanie słowa "rozdzielczoœć" -jako liczby pikseli na jednostkę miary. Możemy także mówić o rozdzielczoœci bitmapy w tym znaczeniu, majšc na my- œli, że okreœlona liczba pikseli w bitmapie odpowiada okreœlonej jednostce miary. Tym niemniej, gdy będę używał słowa "rozdzielczoœć", z kontekstu powinno wy- nikać, z której defirucji korzystam. Bitmapy sš prostokštne, ale pamięć komputera jest liniowa. Zwykle (choć nie zawsze) bitmapy sš zapisywane w pamięci rzędami, poczynajšc od górnego rzę- du pikseli, a kończšc na dolnym. (Głównym wyjštkiem od tej reguły jest DIB). 582 Częœć II: Grafika W każdym rzędzie piksele zapisywane sš od lewej do prawej, czyli tak, jak zapi- suje się poszczególne znaki w kilku liniach. Kolor i bitmapy Oprócz wymiaru na płaszczyŸnie bitmapy majš dodatkowy wymiar - kolor. Jest to pewna liczba bajtów wymaganych dla każdego piksela i niekiedy nazywa się jš głębiš koloru bitmapy lub bpp (ang. bits per pixel -liczba bitów na piksel). Każdy piksel w bitmapie ma tę samš liczbę bitów koloru. Bitmapa, w której jest jeden bit na piksel, nazywana jest bitmapš dwubarwnš lub monochromatycznš. Każdy piksel to 0 lub 1. Wartoœć zero może oznaczać czem , a 1 - biel, lecz nie zawsze tak jest. Dodatkowe kolory wymagajš więcej bitów na piksel. Liczba możliwych do uzyskania kolorów jest równa 2głębia koloru, Przy 2 bi- tach uzyskujesz 4 kolory, przy 4 bitach 16 kolorów, przy 8 bitach -już 256 kolo- rów, przy 16 bitach - 65 536 kolorów, przy 24 bitach zaœ dostajesz aż 16 777 216 kolorów. W jakim stopniu pewne kombinacje bitów koloru odpowiadajš rzeczywistym i znanym kolorom, to zagadnienie stale zajmujšce (często niepotrzebnie) umysł każdego, kto pracuje z bitmapami. Urzšdzenia rzeczywiste W zależnoœci od liczby bitów koloru bitmapy można podzielić na kategorie. Po- szczególne formaty kolorów dla bitmap zwišzane sš z możliwoœciami odtwa- rzania koloru przez powszechnie stosowane karty graficzne, które były dostępne w historii Windows. Istotnie, pamięć karty graficznej można traktować tak, jak- by zawierała dużš bitmapę -jakš widzimy patrzšc na nasze monitory. Najczęœciej spotykanymi kartţ gt.ţţyţ ţywţţ ţa Windows 1.0 były IBM Colors Graphics Adapter (CGA) oraz Hercules Graphics Card (HGC). HGC była urzšdzeniem monochromatycznym, a CGA musiała w œrodowisku Windows dzia- łać w trybie monochromatycznym. Monochromatyczne bitmapy nadal się doœć po- wszechnie spotyka (na przykład kursory myszy sš często monochromatyczne), a bit- mapy monochromatyczne majš oprócz wyœwietlania obrazów, inne zastosowania. Wraz z wprowadzeniem Enhanced Graphics Adapter (EGA) użytkownicy Windows uzyskali dostęp do 16 kolorów. Każdy piksel wymaga 4 bitów koloru. (W rzeczy- wistoœci EGA była nieco bardziej skomplikowana, gdyż zawierała paletę 64 kolo- rów, z których aplikacja mogła wybrać dowolne 16, jednakże system Windows wykorzystywał EGA w sposób najprostszy). Szesnaœcie kolorów używane w EGA to czarny, biały, dwa odcienie szaroœci oraz ciemne i jasne odcienie czerwonego , zielonego i niebieskiego (trzech barw podstawowych), morski (kombinacja niebie- skiego i zielonego), fiolet (kombinacja niebieskiego i czerwonego) oraz żółty (połš- czenie czerwonego z zielonym). Te 16 kolorów uznaje się obecnie za minimalny standard dla Windows. I tym razem 16-kolorowe bitmapy nadal pojawiajš się w tym systemie operacyjnym. Wykorzystuje je większoœć ikon. Proste obrazy w stylu ko- miksowym zwykle da się wykonać za pomocš tych 16 kolorów. Rozdział 14: Bitmapy i BitBlty 583 Kodowanie kolorów używane dla bitmap 16-kolorowych jest niekiedy nazywa- ne IRGB (ang. Intensity-Red-Green-Blue - natężenie czerwieni, zieJeni i niebieskie- go) i faktycznie wywodzi się z kolorów pierwotnie użytych w trybie znakowym karty IBM CGA. Jak cztery bity koloru IRGB, używane dla każdego piksela mapy, majš się do heksadecymalnych kolorów RGB Windows, pokazano w poniższej tabeli. IRGB Kolor RGB Nazwa koloru 0000 00-00-0o Czarny 0001 00-00-80 Ciemnoniebieski 0 O 10 00-80-00 Ciemnozielony 0011 00-80-80 Ciemnoturkusowy 010 0 80-00-00 Ciemnoczerwony O 101 80-00-80 Ciemnofiolekowy O 110 80-80-00 Ciemnożółty O 111 CO-CO-CO Jasnoszary 10 0 0 80-80-80 Ciemnoszary 10 O 1 00-00-FF Niebieski 1 O 10 00-FF-00 Zielony 1 O 11 00-FF-FF Turkusowy 1100 FF-00-00 Czerwony 1101 FF-00-FF Fioletowy 1110 FF-FF-00 Żółty 1111 FF-FF-FF Biały Pamięć w karcie graficznej EGA jest rozłożona na cztery płaszczyzny barw, co znaczy, że cztery bity definiujšce koJor każdego piksela nie stanowiš cišgu w pa- mięci. Pamięć graficzna jest zorganizowana tak, że wszystkie bity intensywnoœci zgrupowane sš razem, bity czerwieni razem i tak dalej. Brzmi to zapewne jak jakaœ wynikła z typu urzšdzenia osobliwoœć, o której wiedza nie jest niezbędna programiœcie Windows - i tak mniej więcej jest. Jednak te płaszczyzny barw ujaw- niajš się w pewnych wywołaniach API, takich jak GetDeviceCaps i CreateBitmap. Windows 98 oraz Microsoft Windows NT wymagajš karty graficznej VGA lub karty o wyższej rozdzielczoœci. Jest to minimalna standardowa platforma graficz- na, którš obecnie akceptuje ten system. Oryginalna karta Video Graphics Array (VGA) została wprowadzona przez IBM w 1987 roku, wraz z liniš komputerów osobistych PS/2. Udostępniała szereg róż- nych trybów graticznych, jednak najlepszy tryb graficzny (korzysta z niego Win- dows) wyœwietla 640 pikseli w poziomie i 480 pikseJi w pionie, stosujšc 16 kolo- rów. Aby wyœwietlić 256 kolorów, oryginalna VGA musiała być przełšczana do trybu graficznego 320 na 240, aJe przy tej liczbie pikseli system Windows nie działał poprawnie. Użytkowrucy często zapominajš o ograniczeniach koloru oryginaJ- nej karty VGA, ponieważ inni producenci sprzętu wkrótce opracowali karty Su- per-VGA (lub SVGA), majšce więcej pamięci i wyœwietlajšce 256 kolorów (a na- Częœć II: Gţafika ţ Ro wet więcej) w trybie 640 na 480. Stanowiš one obecnie standard. Szesnaœcie kolo- rów już po prostu nie wystarcza do przedstawienia obrazów ze œwiata rzeczywi- stego. Tryb karty graficznej wyœwietlajšcy 256 kolorów używa 8 bitów na piksel. Nato- miast wartoœci tych 8 bitów niekoniecznie odpowiadajš okreœlonym koJorom. Zamiast tego karta graficzna ma tabelę podglšdu palety, która pozwala oprogra- mowaniu okreœlić, jak te 8 bitów odzwierciedla kolory rzeczywiste. W Windows aplikacje nie majš bezpoœredniego dostępu do sprzętowej tabeli podglšdu pale- ty; w to miejsce system rezerwuje 20 z 256 kolorów, a programy apJikacyjne uży- wajš Windows Palette Managera do dostosowania pozostałych 236 kolorów. Znacznie więcej do powiedzenia na ten temat będę miał w rozdziale 16. Palette Manager pozwala aplikacjom wyœwietlać bitmapy ze œwiata rzeczywistego na mo- nitorach 256-kolorowych. Kolorami zarezerwowanymi przez Windows sš: Wartoœć koloru Kolor ItGB Nazwa koloru 000 0 00 0 0 00-00-0o Czarny 00000001 80-00-00 Ciemnoczerwony 00000010 00-80-00 Ciemnozielon 00000011 y 80-80-00 Ciemnożółty 00000100 00-00-80 Ciemnoniebieski 00000101 80-00-80 Ciemnofioletow 00000110 00-80-80 y Ciemnoturkusowy 00000111 CO-CO-CO Jasnoszary 00001000 CO-DC-CO Zieleń dolara 00001001 A6-CA-FO Błękit nieba 111101 10 FF-FB-FO Kremowy 1 1 11 O 111 AO-AO-A4 Szary 11 1 1 10 0 0 80-80-80 Ciemnoszar 11111001 y FF-00-00 Czerwony 1 1 1 11 O 10 00-FF-00 Zielony 1 1 1 1 1 O 1 1 FF-FF-00 Żółty 1 1 1 1 110 0 00-00-FF Niebieski 1 1 1 1 11 O l FF-00-FF Fioletowy 1 1111110 00-FF-FF Turkusowy 11 11111 1 FF-FF-FF Biały Ostatnimi laty doœć powszechne stały się karty graficzne o pełnej gamie kolorów. Używajš one albo 16, albo 24 bitów na piksel. W przypadku 16 bitów na piksel jeden bit niekiedy pozostaje nieużywany, pozostałe zaœ 15 bitów jest równo przy- porzšdkowane podstawowym barwom: czerwonej, zielonej i niebieskiej. To daje łšcznie 32 768 koJorów, powstałych z kombinacji 32 odcieni czerwieni, tyluż zie- leni i niebieskiego. Powszechniej można się spotkać z przeznaczeniem 6 bitów dla zieleni (koloru, na który oko Judzkie jest najbardziej wrażliwe) i dostępnymi 65 536 kolorami. Dla niedociekliwego użytkownika komputera, który nie życzy Rozdział 14: Bitmapy i BitBlty 585 sobie widzieć zwariowanych liczb, takich jak 32 768 lub 65 536, karty graficzne, dostarczajšce tysięcy kolorów, zwykle okreœlane sš jako karty high color. Przejœcie do 24 bitów na piksel daje nam łšcznie 16 777 216 kolorów (czyli "kolo- ry rzeczywiste"). Każdy piksel używa 3 bajtów. Będzie to prawdopodobnie stan- dard na wiele nadchodzšcych lat, ponieważ w przybliżeniu odpowiada grani- com percepcji ludzkiego oka, a także jest bardzo dogodny. Gdy wywołujesz GetDeviceCaps (tak jak w programie DEVCAPS z rozdziału 5), możesz użyć stałych BITSPIXEL i PLANES do uzyskania informacji o organizacji kolorów w twojej karcie graficznej. Oto jak te wartoœci zmieniały się przez lata. BITSPIXEL PLANES Liczba kolorów 1 1 2 1 4 16 1 256 8 15 lub 16 1 32 768 lub 65 536 24 lub 32 1 16 777 216 Wprawdzie dziœ trudno jest napotkać monitor monochromatyczny, lecz twoja aplikacja nie powinna zghxpieć, gdy na taki natrafi. Obsługa bitmap w GDI Graphics Device Interface systemu Windows obsługiwał bitmapy poczynajšc od wersji 1.0. Jednak przed wersjš 3.0 jedynymi bitmapami obsługiwanymi przez Windows były obiekty GDI, do których odwoływano się, wykorzystujšc uchwyt bitmapy. Te obiekty bitmapowe GDI były monochromatyczne lub miały tę samš organizację kolorów co rzeczywiste graficzne urzšdzenia wyjœciowe, jak moni- tor. Na przykład bitmapa kompatybilna z 16-kolorowš kartš VGA ma cztery płasz- czyzny barw. Problem w tym, że te kolorowe bitmapy nie mogš być zapisywane i używane na graficznym urzšdzeniu wyjœciowym majšcym innš organizację kolorów, na przykład 8 bitów na piksel, i zdolnym do odtworzenia 256 kolorów. Poczynajšc od Windows 3.0 został zdefiniowany nowy format bitmap, nazwany bitmapš niezależnš od urzšdzenia, czyli DIB (ang. device-independent bitmap). DIB zawiera swojš własnš tabelę kolorów, która pokazuje, jak bity pikseli korespon- dujš z kolorami iZGB. DIB mogš być wyœwietlane na każdym wyjœciowym urzš- dzeniu rastrowym. Jedynš trudnoœciš jest koniecznoœć konwersji kolorów DIB na kolory, które urzšdzenie rzeczywiœcie może odtworzyć. Wraz z DIB Windows 3.0 wprowadził także Windows Palette Managera, który pozwala programom na dostosowywanie kolorów na morutorach 256-kolorowych. Aplikacje często używajš Palette Managera w połšczeniu z wyœwietlaniem DIB, o czym przekonamy się w rozdziale 16. Microsoft rozszerzył definicję DIB w Windows 95 (i w Windows NT 4.0) oraz ponownie w Windows 98 (i Windows NT 5.0). Udoskonalenia te generalnie obję- ły zagadnienie, które zostało nazwane Image Color Management (ICM). Pozwa- 586 Częœć II: Grafika ţ Ron la ono bitmapom DIB precyzyjniej okreœlać odpowiednie kolory potrzebne dla obrazu. ICM omówię pokrótce w rozdziale 15. Mimo zalet DIB starsze obiekty bitmapowe GDI nadal odgrywajš ważnš rolę w pracy z bitmapami. Prawdopodobnie najlepszš strategiš opanowania zagad- nień zwišzanych z bitmapami jest chronologiczne studiowanie materiału, poczy- najšc od obiektu bitmapowego GDI i pojęcia transferu bloków bitowych. Transfer bloków bitowych Tak jak wspomniałem wczeœniej, możesz traktować cały ekran monitora jak jed- nš dużš bitmapę. Piksele, które widzisz na ekranie, sš reprezentowane przez bity przechowywane w pamięci karty graficznej. Dowolny prostokštny obszar na ekra- nie to także bitmapa, której rozmiarem jest liczba wierszy i kolumn, które zawie- ra. Rozpocznijmy naszš wędrówkę po œwiecie bitmap od skopiowania obrazu z jed- nego miejsca na ekranie do drugiego. Jest to zadanie dla funkcji BitBlt. BitBlt [wymawiane "bit blit"] oznacza transfer bloku bitów (ang. bit-block trans- fer). BLT powstała jako instrukcja języka asembler, dokonujšca transferu bloków pamięci w DEC PDP-10. Pojęcie "bitblt" zostało użyte w grafice po raz pierwszy w powišzaniu z systemem 5mallTalk zaprojektowanym w Xerox Palo Alto Rese- arch Center (PARC). W SmallTalku wszystkie graficzne operacje wyjœciowe opie- rajš się na bitblt. Niektórzy programiœci tworzš ze słowa "blt" czasownik, na przy- kład w zdaniu "Potem napisałem kod, który bltował smilsa na ekran i odtwarzał plik wave". Funkcja BitBlt shzży do przemieszczania pikseli. Jak się póŸniej przekonasz, po- jęcie "transfer" nie w pełru oddaje istotg funkcji BitBlt. W rzeczywistoœci funkcja przeprowadza bitowš operację na pikselach i może dać pewne ciekawe efekty. Prosty BitBlt Program BITBLT przedstawiony na rysunku 14-1 używa funkcji BitBlt do sko- piowania ikony menu systemowego programu (znajdujšcej się w górnym lewym rogu okna programu) do obszaru roboczego okna. BITBLT.C /* BITBLT.C - Demonstracja dzialania funkcji BitBlt (c) Charles Petzold, 1998 */ llinclude 0 ţ ţ y > 0) BitBlt (hdcClient, x, y, cxSource, cySource, hdcClient, 0, 0, SRCCOPY); To daje taki sam efekt, jak poprzednio pokazany BITBLT, ale lewy górny róg ob- szaru roboczego jest w pewien sposób zacieniony. 590 Częœć II: Grafika Najważniejszym ograniczeniem funkcji BitBlt jest to, że obydwa konteksty urzš- dzeń muszš być kompatybilne. Oznacza to, że albo i jedno, i drugie musi być monochromatyczne, albo obydwa muszš mieć po tyle samo bitów na piksel. Mówišc krótko; możesz wtedy uzyskać kopię czegoœ, co jest na ekranie, bltujšc to do kontekstu urzšdzenia drukujšcego. Rozcišganie bitmap W funkcji BitBlt obraz docelowy ma taki sam rozmiar jak obraz Ÿródłowy, ponie- waż funkcja ta przyjmuje tylko dwa argumenty okreœlajšce szerokoœć i wysokoœć. Jeżeli zamierzasz rozcišgnšć lub zmniejszyć rozmiar obrazu podczas jego kopio- wania, możesz użyć funkcji StretchBlt. Oto składnia funkcji StretchBlt: StretchBlt (hdcDst, xDst, yDst, cxDst, cyDst, hdcSrc, xSrc, ySrc, cxSrc, cySrc, dwROP); Funkcja ta ma dwa dodatkowe argumenty. Teraz oddzielne parametry opisujš szerokoœć oraz wysokoœć celu i Ÿródła. Funkcja StretchBlt pokazana jest w pro- gramie STIZETCH, zaprezentowanym na rysunku 14-3. STRETCH.C /* STRETCH.C - Demonstracja dzialania funkcji StretchBlt (c) Charles Petzold, 1998 */ ilinclude LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) static TCHAR szAppName C] = TEXT ("Stretch") HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CS UREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_INFORMATION) ; wndclass.hCursor = LoadCursor (NULL, IDC ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WH.ITEţBRUSH) wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) t MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MBţICONERROR) ; return 0 ; Rozdział 14: Bitmapy i BitBlty 591 hwnd = CreateWindow (szAppName, TEXT ("StretchBlt Demo"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, O)) TranslateMessage (&msg) ; DispatchMessage (&msg) ; ) return msg.wParam ; LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) static int cxClient, cyClient, cxSource, cySource ; HDC hdcClient, hdcWindow ; PAINTSTRUCT ps ; switch (message) ( case WM_CREATE: cxSource = GetSystemMetrics (SM_CXSIZEFRAME) + GetSystemMetrics (SM CXSIZE) ; cySource = GetSystemMetrics (SM_CYSIZEFRAME) + GetSystemMetrics (SM CYCAPTION) ; return 0 ; case WM_SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; return 0 ; case WM_PAINT: hdcClient = BeginPaint (hwnd, &ps) ; hdcWindow = GetWindowDC (hwnd) ; StretchBlt (hdcClient, 0, 0, cxOlient, cyClient, hdcWindow, 0, 0, cxSource, cySource, MERGECOPY) ; ReleaseDC (hwnd, hdcWindow) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: PostOuitMessage (0) ; return 0 ; ) return DefWindowProc (hwnd, message, wParam, lParam) ; Rysunek 14-3. Program STRETCH 592 Częœć II: Grafika W tym programie funkcja StretchBlt wywołana jest tylko jeden raz. Użyta jest do wypełnienia całego obszaru roboczego ikonš menu systemowego programu, tak jak pokazano to na rysunku 14-4. Rysunek 14-4. Ekran programu STRETCH Wszystkie współrzędne oraz rozmiary w funkcjach BitBlt i StretchBlt podane sš w jednostkach logicznych. Co się jednak stanie, gdy w funkcji BftBlt będš dwa różne konteksty urzšdzeń odnoszšce się do tego samego urzšdzenia rzeczywi- stego, lecz majšce dwa różne tryby odwzorowania? W takim wypadku wywoła- nie BitBlt zdaje się rozdwajać: argumenty cx i cy sš podane w jednostkach logicz- nych i znajdujš zastosowanie zarówno w stosunku do prostokšta w kontekœcie urzšdzenia Ÿródłowego, jak i do prostokšta w kontekœcie urzšdzenia docelowe- go. Zanim nastšpi rzeczywisty transfer bitów, wszystkie współrzędne i rozmiary muszš ulec konwersji na współrzędne urzšdzenia. Ponieważ wartoœci cx i cy sš używane zarówno do kontekstu urzšdzenia Ÿródłowego, jak i docelowego, mu- szš one podlegać konwersji na jednostki urzšdzenia (piksele) oddzielnie dla każ- dego kontekstu urzšdzenia. Gdy Ÿródłowy i docelowy kontekst urzšdzenia jest taki sam lub gdy oba konteksty używajš trybu odwzorowania MMţTEXT, wówczas rozmiar tego prostokšta w jed- nostkach urzšdzenia będzie taki sam w obu kontekstach urzšdzeń. Windows może wtedy dokonać prostego transferu piksel na piksel. Natomiast gdy rozmiar prosto- kšta w jednostkach urzšdzenia jest różny w obydwu kontekstach, Windows prze- kazuje zadanie do bardziej uniwersalnej funkcji StretchBlt. StretchBlt pozwala także odbić obraz symetrycznie w pionie lub w poziomie. Je- œli znaki cxSrc i cxDst (po przekształceniu na jednostki urzšdzenia) sš różne, StretchBlt tworzy lustrzane odbicie: lewa strona staje się prawš, prawa zaœ - lewš. Możesz to wypróbować w programie STRETCH, zmieniajšc argument xDst na cxClient, a argument cxDst na -cxClient. Jeœli różniš się znaki cySrc i cyDst, to Stretch- Blt odwraca obraz do góry nogami. To możesz sprawdzić w programie STRETCH, zmieniajšc argument yDst na cyClient, a cyDst na -cyClient. Rozdział 14: Bitmapy i BitBity 593 Tryb StretchBlt Funkcja StretchBlt może napotkać pewne trudnoœci, nieodłšczne od skalowana bitmap. W czasie powiększania bitmapy StretchBlt musi powielać rzędy lub ko- lumny pikseli. Jeœli wielokrotnoœci powielenia nie da się wyrazić liczbš całkowi- tš, proces może spowodować pewne zniekształcenie obrazu. Jeżeli prostokšt docelowy jest mniejszy od prostokšta Ÿródłowego, StretchBlt musi zmniejszyć obraz łšczšc dwa lub więcej wierszy i kolumn pikseli w pojedynczy wiersz lub kolumnę. Robi to na jeden z czterech sposobów, w zależnoœci od atry- butu trybu rozcišgania w kontekœcie urzšdzenia. Atrybut ten zmienia się korzy- stajšc z funkcji SetStretchBltMode: SetStretchBltMode (hdc, iMode) ; Argument iMode może przybierać następujšce wartoœci: ů BLACKONWHTTE lub STRETCH ANDSCANS (domyœlnie). Jeœli dwa lub więcej pikseli ma być połšczone w jeden piksel, StretchBlt dokonuje na nich logicznej operacji AND. Piksel wynikowy jest biały jedynie wówczas, gdy wszystkie poczštkowe były białe, co w praktyce oznacza, że piksele czarne przeważajš nad pikselami białymi. Jest to korzystne w przypadku bitmap monochroma- tycznych, w których obraz zazwyczaj jest czarny na białym tle. ů WHITEONBLACK lub STRETCH ORSCANS. Jeżeli dwa lub więcej pikseli ma być połšczone w jeden, StretchBlt wykonuję logicznš operację OR. Wynikowy piksel jest czarny jedynie wówczas, gdy wszystkie pierwotne były czarne, co oznacza, że dominujš piksele białe. Ten tryb jest korzystny dla bitmap mono- chromatycznych, w których obraz jest biały na czarnym tle. ů COLORONCOLOR lub STRETCH DELETESCANS. Funkcja StretchBlt po pro- stu eliminuje wiersze lub kolumny pikseli bez dokonywania jakiejkolwiek kombinacji logicznej. Dla kolorowych bitmap jest to najczęœciej optymalne podejœcie. ů HALFTONE lub STRETCH HALFTONE. System Windows oblicza œredni kolor docelowy na podstawie kolorów Ÿródłowych, które majš zostać połšczone. Tego trybu używa się w połšczeniu z paletš półtonów, przedstawionš w rozdziale 16. Windows zawiera także funkcję GetStretchBItMode, pozwalajšcš uzyskać infor- mację o aktualnym trybie rozcišgania. Operacje rastrowe Programy BITBLT i STRETCH po prostu kopiujš bitmapę Ÿródłowš do miejsca docelowego, dokonujšc, być może, jej rozcišgnięcia w trakcie tego procesu. Dzie- je się tak, ponieważ ostatni argument funkcji BitBlt i StretchBlt otrzymał wartoœć SRCCOPY. Jest to tylko jedna z 256 operacji rastrowych, których możesz użyć w tych funkcjach. Zbadajmy operacje rastrowe bardziej metodycznie i w progra- mie STRETCH poeksperymentujmy z kilkoma innymi wartoœciami. Spróbuj zastšpić SRCCOPY wartoœciš NOTSRCCOPY. Operacja rastrowa NOT SRCCOPY odwraca kolor bitmap w procesie kopiowania. W oknie użytkownika 594 Częœć II: Grafika wszystkie kolory zostanš odwrócone. Czarny staje się białym, biały staje się czar- nym, a niebieski staje się żółtym. Teraz spróbuj SRCINWERT. Uzyskasz ten sam efekt. Spróbuj BLACKNESS - tym razem cały obszar roboczy zostaje odmalowa- ny na czarno. WHITENESS powoduje, że staje się on biały. Teraz spróbuj zrobić tak: zastšp wywołanie StretchBlt następujšcymi trzema in- strukcjami: SelectObject (hdcClient, CreateHatchBrush (HSţDIAGCROSS, RGB (0,0,0))); StretchBlt (hdcClient, 0, 0, cxClient, cyClient, hdcWindow, 0, 0, cxSource, cySource, MERGECOPY); Delete0bject (hdcClient, GetStockObject (WHITEţBRUSH)); Tym razem zobaczysz jakby nałożonš na obraz kratkę. Co tu się dzieje? już wczeœniej wspomniałem, że funkcje BitBlt i StretchBlt nie sš jedynie transfera- mi bloków bitowych. W rzeczywistoœci dokonujš operacji bit po bicie między następujšcymi trzema obrazami: ů ródlo. Bitmapa Ÿródłowa rozcišgana lub zmniejszana (jeœli jest to konieczne) do rozmiaru prostokšta docelowego. ů Cel. Prostokšt docelowy przed wywołaniem BitBlt lub StretchBlt. ů Deseń. Aktualny pędzel wybrany w kontekœcie urzšdzenia docelowego, po- wtarzany poziomo i pionowo tak, aby pokryć cały obszar prostokšta docelo- wego. Wynik kopiowany jest do prostokšta docelowego. Operacje rastrowe koncepcyjnie sš podobne do trybów rysowania, z którymi spo- tkaliœmy się w rozdziale 5. Tryby rysowania decydujš o rodzaju kombinacji obiektu graficznego, takiego jak linia, z celem. Jak zapewne pamiętasz, mamy 16 trybów rysowania - to znaczy wszystkich niepowtarzalnych wyników uzyskiwanych, gdy zera i jedynki rysowanego obiektu kombinowane sš z zerami i jedynkami obiek- tu docelowego. W operacjach rastrowych używanych w funkcjach BitBlt i StretchBlt biorš udział trzy obiekty, wyników zaœ może być 256. Istnieje 256 sposobów kombinacji Ÿró- dłowej bitmapy, bitmapy docelowej oraz desenia. Piętnastu z tych operacji ra- strowych nadano nazwy (niektóre z nich niewiele mówiš) zdefiniowane w WING DI.H. Pozostałe majš wartoœci numeryczne, które można obejrzeć w /Platform SDK/ Graphics and Multimedia Seroices/GDI/ftaster Operation Codes/Ternary Raster Ope- rations. Oto kody 15 nazwanych ROP: Deseń (D): 1 1 1 1 0 0 0 0 ródło (Z): 1 1 0 0 1 1 0 0 Operacja Cel (C): 1 0 1 0 1 0 1 0 boolowska Kod ROP Nazwa Wynik: 0 0 0 0 0 0 0 0 0 0x000042 BLACKNESS 0 0 0 1 0 0 0 1 ~(Z I C) 0x1100A6 NOTSRCERASE 0 0 1 1 0 0 1 1 ~Z 0x330008 NOTSRCCOPY Rozdział 14: Bitmapy i BitBlty 595 Deseń (D): 1 1 1 1 0 0 0 0 ródło (Z): 1 1 0 0 1 1 0 0 Operacja Cel (C): 1 0 1 0 1 0 1 0 boolowska Kod ROP Nazwa 0 1 0 0 0 1 0 0 Z & ~C 0x440328 SRCERASE 01010101 ~C 0x550009 DSTINVERT 0 1 0 1 1 0 1 0 D ^ C 0x5A0049 PATINVERT 0 1 1 0 0 1 1 0 Z ^ C 0x660046 SRCINVERT 1 0 0 0 1 0 0 0 Z & C 0x8800C6 SRCAND 1 0 1 1 1 0 1 1 ~Z I C OxBB0226 MERGEPAINT 1 1 0 0 0 0 0 0 D & Z 0xC000CA MERGECOPY 1 1 0 0 1 1 0 0 Z OxCC0020 SRCCOPY 1 11 01 11 0 Z I C OxEE0086 SRCPAINT 1 1 1 1 0 0 0 0 D 0xF00021 PATCOPY 1 11 11 O 1 1 D I ~Z I C OxFBOAO9 PATPAINT 11111111 1 OxFF0062 WHITENESS Zrozumienie działań przeprowadzonych w tej tabeli jest ważne, gdy chce się wyjaœnić operacje rastrowe, poœwięćmy więc nieco czasu na jej zbadanie. Wartoœć podana w kolumnie Kod ROP tabeli jest liczbš przekazywanš do funkcji BitBlt lub StretchBlt jako ostatni argument. Nazwy podane w kolumnie Nazwa sš przypisane tym wartoœciom w WINGDI.H. Mniej znaczšca częœć kodu ROP jest liczbš towarzyszšcš sterownikowi urzšdzenia przy dokonywaniu operacji rastro- wej. Bardziej znaczšca częœć to liczba od 0 do 255, taka sama jak bitowy deseń w drugiej kolumnie i jest wynikiem operacji bit po bicie, przeprowadzonej mię- dzy bitami desenia, Ÿródła i celu, pokazanymi w nagłówku kolumny. W kolum- nie Operacja boolowska przy użyciu składni C pokazano, jak skombinowane sš deseń, Ÿródło i cel. Najłatwiej będzie zrozumieć tabelę przy założeniu, że masz do czynienia z syste- mem monochromatycznym (1 bit na piksel), w którym 0 oznacza czarny, a 1 to biały. Wynikiem operacji BLACKNESS sš same zera, niezależnie od Ÿródła, celu i desenia, zatem wszystkie piksele docelowe będš miały kolor czarny. Podobnie WHITENESS zawsze powoduje pokolorowanie celu na biało. Teraz załóżmy, że wykonujesz operację rastrowš PATCOPY. Po jej przeprowa- dzeniu bity wynikowe będš takie same jak bity desenia. Bitmapy Ÿródłowa i do- celowa sš ignorowane. Innymi słowy, PATCOPY po prostu kopiuje aktualny deseń do prostokšta docelowego. Operacja PATPAINT jest bardziej skomplikowana. Jej wynik jest równy alterna- tywie OR, przeprowadzonej bit po bicie między deseniem, celem oraz odwrot- noœciš Ÿródła. Gdy bitmapa Ÿródłowa jest czarna (bit 0), wyruk jest zawsze biały (bit 1). Gdy Ÿródło jest białe (1), wynik jest także biały, jeœli tylko deseń lub cel sš białe. Innymi słowy, wynik będzie czarny tylko wtedy, gdy Ÿródło jest białe, a de- seń i cel sš czarne. Monitor wielobarwny używa wielu bitów dla każdego piksela. Funkcje BitBIt i StretchBlt dokonujš'operacji bit po bicie na każdym z tych bitów koloru oddziel- 596 Częœć II: Grafika nie. jeżeli na przykład piksel docelowy jest czerwony, a Ÿródło ma kolor niebie- ski, rastrowa operacja SRCPAINT zabarwi piksel docelowy na fioletowo. Pamię- taj, że operacje dokonywane sš na bitach aktualnie przechowywanych w pamię- ci karty graficznej. Stopień, w jakim te bity odpowiadajš rzeczywistym kolorom, zależy od ustawienia palety barw karty graficznej. Wykonuje je system Windows, zatem operacje rastrowe działajš zgodnie z przewidywaniami. Natomiast po zmianie palety (co omówię w rozdziale 16) operacje rastrowe mogš dać nieocze- kiwane rezultaty. Zastosowania operacji rastrowych znajdziesz w podrozdziale "Nieprostokštne obrazy bitmapowe", w dalszej częœci tego rozdziahx. Blt desenia Oprócz funkcji BitBlt i StretchBlt w Windows zawarta jest także funkcja PatBlt (ang. pattern block transfer - transfer bloków desenia). Jest to najprostsza ze wszystkich trzech funkcji "blt". W odróżnieniu od BitBlt i StretchBlt używa jedynie kontek- stu urzšdzenia docelowego. Oto składnia funkcji PatBlt: PatBlt (hdc, x, y, cx, cy, dwROP); Argumenty x, y, cx i cy wyrażone sš w jednostkach logicznych. Punkt logiczny (x, y) okreœla lewy górny róg prostokšta, którego szerokoœć wynosi cx jednostek, a wysokoœć - cy jednostek. Jest to prostokštny obszar, zmieniany przez PatBlt. Operacja logiczna, którš funkcja PatBlt przeprowadza na pędzlu i kontekœcie urzš- dzenia docelowego, okreœlona jest przez argument dwftOP, stanowišcy podzbiór kodów ROP - to znaczy możesz używać jedynie tych kodów ROP, które nie wy- korzystujš kontekstu urzšdzenia Ÿródłowego. W poniższej tabeli pokazano 16 ope- racji rastrowych obshzgiwanych przez PatBlt. Deseń (D): 1 1 0 0 Operacja Cel (C): 1 0 1 0 boolowska Kod ROP Nazwa Wynik: 0 0 0 0 0 0x000042 BLACKNESS 0 0 0 1 ~(D I C) 0x0500A9 0 0 1 0 ~D & C 0x0A0329 0 0 1 1 ~D Ox0F0001 0 1 0 0 D & ~C 0x500325 0101 ~C 0x550009 DSTINVERT 0 1 1 0 D ^ C 0x5A0049 PATINVERT 0 1 1 1 ~(D & C) 0x5F00E9 1 0 0 0 D & C 0xA000C9 1 0 0 1 ~(D ^ C) 0xA50065 1 0 1 0 C OxAA0029 1 0 1 1 ~D I C OxAF0229 1 1 0 0 D 0xF00021 PATCOPY 1 1 0 1 D I ~C 0xF50225 1 1 1 0 D I C OxFA0089 1111 1 OxFF0062 WHITENESS Rozdział 14: Bitmapy i BitBlty 597 Niektóre z częœciej spotykanych zastosowań funkcji PatBlt przedstawiono poni- żej. Aby narysować czarny prostokšt, należy wywołać: PatBlt(hdc, x, y, cx, cy, BLACKNESS) ; Chcšc narysować biały prostokšt, wywołaj: PatBlt(hdc, x, y, cx, cy, WHITENESS) ; Funkcja: PatBlt(hdc, x, y, cx, cy, DSTINVERT) ; zawsze odwraca kolory prostokšta. Jeżeli w kontekœcie urzšdzenia jest aktualnie wybrany pędzel WHITE BRUSH, to funkcja PatBlt(hdc, x, y, cx, cy, PATINVERT) ; odwróci również sam prostokšt. Jak zapewne pamiętasz, funkcja FilIRect wypełnia prostokštny obszar jakimœ ro- dzajem pędzla: FillRect(hdc, &rect, hBrush) ; Funkcja FillRect jest równoważna następujšcemu kodowi: hBrush = SelectObject (hdc, hBrush) ; PatBlt (hdc, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, PATCOPY) ; SelectObject (hdc, hBrush) ; W rzeczywistoœci, aby wykonać funkcję FilIRect, system Windows wykonuje ten właœnie kod. Gdy wywołasz: InvertRect(hdc, &rect) ; Windows przetłumaczy to sobie na wywołanie funkcji: PatBlt (hdc, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, DSTINVERT) ; Kiedy definiowałem składnię funkcji PatBlt, powiedziałem, że punkt (x, y) okre- œla lewy górny róg prostokšta o szerokoœci cxjednostek oraz wysokoœci cy jedno- stek. To twierdzenie nie zawsze jest prawdziwe. Funkcje BitBlt, PatBlt oraz Stretch- Blt sš jedynymi funkcjami rysujšcymi GDI, które podajš logiczne prostokštne współrzędne - w znaczeniu logicznej szerokoœci oraz wysokoœci mierzonej od jed- nego narożnika. Wszystkie pozostałe funkcje rysujšce GDI, które wykorzystujš prostokštnie obramowane obszary, wymagajš, żeby były one zdefiniowane przez współrzędne lewego górnego oraz prawego dolnego narożnika. W trybie odwzo- rowania MM TEXT powyższy opis parametrów funkcji PatBlt jest prawdziwy. Jednak w metrycznym trybie odwzorowania nie jest. Jeżeli użyjesz dodatnich war- toœci cx oraz cy, to punkt (x, y) będzie lewym dolnym narożnikiem prostokšta. Jeœli chcesz, żeby punkt (x, y) znajdował się w lewym górnym rogu, to musisz ustawić argument cy na ujemnš wysokoœć prostokšta. Dokładniej mówišc, prostokšt kolorowany przez PatBlt ma logicznš szerokoœć zadanš przez wartoœć bezwzględnš cx oraz logicznš wysokoœć zadanš przez wartoœć bezwzględnš cy. Te dwa argumenty mogš być ujemne. Prostokšt jest zdefiniowany przez dwa naroţniki zadane przez dwa punkty (x, y) oraz (x + cx, y + cy). Lewy górny narożnik prostokšta zawsze należy do obszartz modyfikowa- 5gg Częœć 11: Grafika nego przez PatBlt. Prawy dolny narożnik znajduje się poza prostokštem. W za- leżnoœci od trybu odwzorowania oraz znaków parametrów cx oraz cy lewy gór- ny narożnik prostokšta może być punktem (x, y), (x, y + cy), (x + cx, y) lub (x + cx, y + cy). Jeœli ustawiłeœ tryb odwzorowania na MM LOENGLISH i zamierzasz zastoso- wać PatBlt do kwadratowego obszaru o boku 1 cala, znajdujšcego się w lewym górnym narożniku obszaru roboczego okna, możesz użyć jednej z poniższych in- strukcji: PatBlt (hdc, 0, 0, 100, -100, dwROP) ; lub PatBlt (hdc, 0, -100, 100, 100, dwROP) ; lub PatBlt (hdc, 100, 0, -100, -100, dwROP) ; lub PatBlt (hdc, 100, -100, -100, 100, dwROP) : Najprostszym sposobem poprawnego ustawienia parametrów funkcji PatBlt jest okreœlenie argumentów x i y w lewym górnym narożniku prostokšta. Jeœli twój tryb odwzorowania definiuje współrzędnš y jako przyrost przesunięcia w górę, nadaj parametrowi cy wartoœć ujemnš. Jeżeli tryb odwzorowania definiuje współ- rzędnš x jako przyrost przesunięcia w lewo (co jest właœciwie niespotykane), na- daj wartoœć ujemnš parametrowi cx. Obiekt bitmapy GDI Wspominałem wczeœniej w tym rozdziale, że system Windows już od wersji 1.0 zawiera obiekt bitmapy GDI. Z powodu wprowadzenia w Windows 3.0 bitmapy niezależnej od urzšdzeń (ang. device-independent bitmap - DIB) GDI Bitmap Ob- ject jest czasem teraz nazywany bitmapš zależnš od urzšdzeń (ang. device-depen- dent bitmap - DDB). Będę się starał nie używać pełno brzmišcej nazwy - bitmapa zależna od urzšdzeń, ponieważ w poœpiechu słowa mogš się pomylić z podob- nie brzmišcš nazwš bitmapa niezależrva od urzšdzeń. Skrót DDB jest lepszy, bo ła- twiej jest go na pierwszy rzut oka odróżnić od DIB. Istnienie dwóch różnych rodzajów bitmap wprowadzało wiele nieporozumień wœród programistów rozpoczynajšcych pracę w Windows w wersji 3.0 i następ- nych. Niejeden weteran programowania w tym systemie ma problemy z dobrym zrozumieniem zależnoœci między formatami DIB i DDB. (Obawiam się, że nawet wersja tego podręcznika przeznaczona dla Windows 3.0 nie była w stanie nic tutaj pomóc). Tak, istniejš pewne relacje między DIB a DDB: można wykonać konwer- sję DIB na format DDB i vice versa (choć nie bez utraty informacji). Formatów DIB i DDB nie da się zmienić jeden w drugi i nie sš one po prostu alternatywny- mi metodami przedstawienia tych samych danych wizualnych. Z pewnoœciš najwygodniej byłoby założyć, że wraz z wprowadzeniem DIB for- mat DDB stał się przestarzały. Tak jednak nie jest. DDB nadal odgrywajš istotnš rolę w systemie Windows, w szczególnoœci jeœli dbasz o wydajnoœć. Rozdział 14: Bitmapy i BitBlty 599 Tworzenie DDB DDB jest jednym z wielu obiektów graficznych zdefiniowanych w GDI (Graphics Device Interface) systemu Windows (sš tam także pisaki, pędzle, czcionki, meta- pliki i palety). Te obiekty graficzne sš przechowywane wewnštrz moduhz GDI i dostępne dla programów aplikacyjnych przez numeryczne uchwyty. Uchwyt do obiektu DDB przechowuje się na zmiennej typu HBITMAP (od angielskiego han- dle to a bitmap, czyli uchwyt bitmapy), na przykład: HBITMAP hBitmap ; Możesz zatem uzyskać uchwyt, wywohzjšc jednš z funkcji tworzšcych DDB, na przykład CreateBitmap. Ta funkcja alokuje w pamięci GDI i inicjuje pewnš iloœć pamięci przeznaczonš na zapisanie informacji o bitmapie oraz jej aktualnych bi- tów. Program aplikacji nie ma bezpoœredniego dostępu do tej pamięci. Bitmapa jest niezależna od kontekstu jakiegokolwiek urzšdzenia. Kiedy program skończy korzystanie z niej, powinien jš usunšć z pamięci: Delete0bject (hBitmap) ; Jeżeli wykorzystujesz DDB przez cały czas działania programu, możesz jš usu- nšć w trakcie zamykania programu. Oto funkcja CreateBitmap: hBitmap = CreateBitmap (cx, cy, cPlanes, cBitsPixel, bits) ; Pierwsze dwa argumenty okreœlajš szerokoœć i wysokoœć bitmapy w pikselach. Trzeci argument jest liczbš płaszczyzn barw, a czwarty liczbš bitów na piksel, czyli okreœla głębię koloru. Pišty argument wskazuje na bitowš tablicę zgodnš z okreœlonym formatem koloru. Jeœli nie zamierzasz nadawać poczštkowych war- toœci bitom pikseli DDB, możesz ustawić ostatni argument na NULL. Bity pikseli można ustawić póŸniej. Kiedy używasz tej funkcji, system Windows pozwoli ci utworzyć dowolny, naj- bardziej wymyœlny typ obiektu bitmapy GDI, jaki tylko zdołasz wymyœlić. Za- łóżmy, że potrzebna jest ci bitmapa o szerokoœci 7 pikseli, wysokoœci 9 pikseli, pięciu płaszczyznach koloru oraz głębi 3 bitów na piksel. Po prostu zrób tak: hBitmap = CreateBitmap (7, 9, 5, 3, NULL) ; a Windows bez słowa sprzeciwu da ci poprawny uchwyt bitmapy. Podczas tego wywołania funkcji Windows zapisuje informację, którš przesłałeœ do funkcji, i alokuje pamięć dla bitów pikseli. Z grubsza liczšc, ta bitmapa wy- maga 7 razy 9 razy 5 razy 3, czyli 954 bitów, co daje ponad 118 bajtów. Jednak kiedy Windows alokuje pamięć dla bitmapy, każdy rzšd pikseli ma pa- rzystš liczbę bajtów. Tak więc rzšd pikseli w bajtach równa się: iWidthBytes = 2 * ((cx * cBitsPixel + 15) / 16) ; lub jak wolałby napisać programista pracujšcy w C: iWidthBytes = (cx * cBitsPixel + 15) & ~15) Ż 3 ; Cała pamięć alokowana dla DDB jest więc w bajtach równa: iBitmapBytes = cy * cPlanes * iWidthBytes ; W naszym przykładzie szerokoœć bitmapy iWidthBytes jest równa 4 bajtom, a cała bitmapa iBitmapBytes jest równa 180 bajtom. 600 Częœć II: Grafika A teraz - co znaczy mieć bitmapę o 5 płaszczyznach barw i 3 bitach koloru m piksel? Nie tak znowu wiele. Nie wystarczy nawet, by nazwać to szkolnym przyţ kładem. Skłoniłeœ GDI do zarezerwowania odrobiny wewnętrznej pamięci o spe- cyficznej organizacji, ale to niczego nie oznacza i nie da się zrobić nic pożytecz- nego z takš bitmapš. W praktyce będziesz wywoływał funkcję CreateBitmap tylko z dwoma typami argumentów: ů cPlanes oraz cBitsPixel równymi 1 (co będzie oznaczało bitmapę monochroma- tycznš); lub ů cPlanes oraz cBitsPixel równymi wartoœciom okreœlonego kontekstu urzšdze- nia, które można uzyskać z funkcji GetDeviceCaps, używajšc wskazań PLA- NES oraz BITPIXEL. W praktyce będziesz wywoływał CreateBitmap jedynie w pierwszym przypadku, ponieważ drugi można znacznie uproœcić, używajšc funkcji CreateCompatibleBit- map: hBitmap = CreateCompatibleBitmap (hdc, cx, xy) ; Ta funkcja tworzy bitmapę kompatybilnš z urzšdzeniem, którego uchwyt kon- tekstu został podany jako pierwszy argument. Funkcja CreateCompatibleBitmap wykorzystuje uchwyt kontekstu urzšdzenia do uzyskania informacji z funkcji GetDeviceCaps, które następnie przekazuje do CreateBitmap. Z wyjštkiem takiej samej organizaeji pamięci jak kontekst urzšdzenia rzeczywistego, DDB w żaden sposób nie jest powišzana z kontekstem urzšdzenia. Funkcja CreateDiscardableBitmap ma takie same parametry jak CreateCompatibleBit- map i jest jej równoważna pod względem funkcjonalnoœci. We wczeœniejszych wersjach Windows CreateDiscardableBitmap tworzyła bitmapę, którš system mógł usunšć z pamięci, jeœli pamięci tej brakowało. Wtedy program musiał zregenero- wać dane bitmapy. Trzeciš funkcjš tworzšcš bitmay jest CreateBitmaplndirect: hBitmap CreateBitmapIndirect (&bitmap); gdzie bitmap to struktura typu BITMAP. Struktura BITMAP jest zdefiniowana w następujšey sposób: typedef struct tagBITMAP ( LONG bmType ; // ustawiony na 0 LONG bmWidth : // szerokoœć w pikselach LONG bmHeight ; // wysokoœć w pikselach LONG bmWidthBytes ; // szerokoœć rzędu w bajtach WORD bmPlanes ; // liczba plaszczyzn koloru WORD bmBitsPixel ; // liczba bitów na piksel LPUOID bmBits ; // wskaŸnik do bitów piksela ) BITMAP, * PBITMAP ; Przy wywołaniu funkcji CreateBitmaplndirect nie musisz okreœlać pola bmWidth- Bytes. Windows obliczy je za ciebie. Możesz także polu bmBits przypisać NULL lub adres bitów pikseli inicjujšcych bitmapę.; Rozdział 14: Bitmapy i BitBlty 601 Struktura BITMAP jest także używana w funkcji GetObject. Najpierw zdefiniuj strukturę typu BITMAP, BITMAP bitmap; i w następujšcy sposób wywołaj funkcję: GetObject (hBitmap, sizeof (BITMAP), &bitmap) ; Windows wypełni pola struktury BITMAP informacjami na temat bitmapy. Jed- nak pole bmBits będzie równe NULL. Powinieneœ, ostatecznie wywohxjšc funkcję DeleteObject, zniszczyć każdš bitma- pę, którš zbudujesz w programie. Bity bitmapy Gdy tworzysz obiekt bitmapowy GDI zależny od urzšdzenia poprzez użycie CreateBitmap lub CreateBitmaplndirect, możesz podać wskaŸnik do bitów pikseli bitmapy. Możesz także pozostawić bity bitmapy w stanie niezainicjowanym. Windows ma również dwie funkcje do pobrania i ustawienia bitów pikseli po utworzeniu bitmapy. Chcšc ustawić bity pikseli, wywołaj: SetBitmapBits (hBitmap, cBytes, &bits) ; Funkcja GetBitmapBits ma takš samš składnię: GetBitmapBits (hBitmap, cBytes, &bits) ; W obydwu funkcjach cBytes wskazuje liczbę bajtów do skopiowania, a bits to bufor o rozmiarze co najmniej cBytes. Bity pikseli w DDB zorganizowane sš poczynajšc od górnego wiersza. Tak jak wspomniałem wczeœniej, każdy z wierszy ma parzystš liczbę bajtów. Oprócz tego nie ma zbyt wiele do dodania. Jeœli bitmapa jest monochromatyczna, co oznacza, że ma jednš płaszczyznę i jeden bit na piksel, wówczas każdy z pikseli to 1 lub 0. Skrajny lewy piksel w każdym wierszu jest najbardziej znaczšcym bitem pierw- szego bajtu wiersza. W dalszej częœci tego rozdziału, gdy już dowiemy się, jak go wyœwietlić, wykonamy monochromatycznš DDB. W przypadku niemomochromatycznych bitmap powinieneœ unikać sytuacji, w których trzeba wiedzieć, co znaczš poszczególne bity pikseli. Załóżmy, że sys- tem Windows działa z 8-bitowym VGA. Wywołujesz CreateCompatibleBitmap. Za pomocš funkcji GetDeviceCaps możesz okreœlić, że masz do czynienia z urzšdze- niem majšcym jednš płaszczyznę barw i 8 bitów na piksel. Każdy piksel zapisa- ny jest w jednym bajcie. Co jednak oznacza piksel o wartoœci 0x37? Oczywiœcie dotyczy jakiegoœ koloru, ale jakiego? Piksel w rzeczywistoœci nie odnosi się do żadnego stałego, okreœlonego koloru. Jest to po prostu wartoœć. DDB nie majš tabeli kolorów. Zasadniczym pytaniem jest: jakiego koloru będzie piksel, gdy DDB zostanie wyœwietlona na ekranie? Ja- kiœ kolor to musi być, ale jaki? Wyœwietlony piksel będzie koloru IZGB okreœlone- go przez wartoœć indeksu 0x37 w tabeli podglšdu palety karty graficznej. To wła- œnie oznacza dla ciebie zależnoœć od urzšdzenia. Nie myœl jednak, że niemonochromatyczna DDB jest bezużyteczna tylko dlatego, iż nie wiemy, co oznaczajš wartoœci pikseli. Niebawem zobaczymy, jak potrzeb- 602 Częœć 11: Grafika ţ,, Ro: ne mogš się one okazać, a w następnym rozdziale dowiemy się, w jaki sposób funkcje SetBitmapBits i GetBitmapBits zostały wyparte przez bardziej użyteczne funkcje SetDIBits i GetDIBits. Podstawowa zasada jest zatem taka: nie będziesz używał CreateBitmap, CreateBit- maplndirect ani SetBitmapBits do okreœlania bitów koloru DDB. Bezpiecznie mo- żesz okreœlić bity jedynie dla DDB monochromatycznej. (Wyjštek od tej reguły jest wtedy, gdy bierzesz bity z innej DDB o tym samym formacie przez wywoła- nie GetBitmapBits). Zanim przejdziemy dalej, pozwól mi wspomnieć o funkcjach SetBitmapDimension- Ex i GetBitmapDimensionEx. Funkcje te pozwalajš ustawić (i uzyskać) metryczne wymiary bitmapy w jednostkach 0,1 milimetra. Informacja ta, zapisana w GDI wraz z definicjš bitmapy, do niczego nie jest używana. To po prostu etykieta, którš możesz wykorzystać, aby powišzać metryczny rozmiar z bitmapš DDB. Kontekst urzšdzenia pamięciowego Następnym pojęciem, z jakim musimy się uporać, jest kontekst urzšdzenia pa- mięciowego. Potrzebujesz go, żeby móc użyć obiektu bitmapy GDI. Przeważnie kontekst urzšdzenia odnosi się do okreœlonego graficznego urzšdze- nia wyjœciowego (takiego jak monitor lub drukarka) wyposażonego w sterownik urzšdzenia. Kontekst urzšdzenia pamięciowego istnieje tylko w pamięci. Nie jest to rzeczywiste urzšdzenie wyjœciowe, lecz mówi się, że jest "kompatybilne" z okreœlonym rzeczywistym urzšdzeniem. Chcšc utworzyć kontekst urzšdzenia pamięciowego, musisz najpierw mieć uchwyt h B i i kontekstu urzšdzenia dla urzšdzenia rzeczywistego. Jeœli jest to hdc, kontekst urzšdzenia pamięciowego tworzysz w następujšcy sposób: hdcMem = CreateCompatibleDC (hdc) ; Zwykle wywołanie funkcji jest jeszcze prostsze od przedstawionego. Jeœli jako argument podasz NULL, system Windows utworzy kontekst urzšdzenia pamię- ciowego kompatybilny z monitorem. Każdy kontekst urzšdzenia pamięciowego utworzony przez aplikację powinien ostatecznie zostać usunięty przez wywoła- nie DeleteDC. Kontekst urzšdzenia pamięciowego ma powierzchnię wyœwietlania, tak jak rze- czywiste urzšdzenie rastrowe. Jednak ta powierzchnia jest poczštkowo bardzo mała - monochromatyczna, o szerokoœci 1 piksel i wysokoœci 1 piksel. Powierzch- nia wyœwietlania to po prostu pojedynczy bit. Oczywiœcie, nie możesz zbyt wiele zdziałać na takim obszarze, zatem jedynš roz- sšdnš następnš czynnoœciš jest powiększenie powierzchni wyœwietlania. Robi się to wybierajšc obiekt bitmapy GDI w kontekœcie urzšdzenia pamięciowego: SelectObject (hdcMem, hBitmap) ; Jest to ta sama funkcja, której używasz do wybierania pisaków, pędzli, czcionek, regionów oraz palet w kontekstach urzšdzeń. Jednak kontekst urzšdzenia pamię- ciowego to jedyny typ kontekstu urzšdzenia, w którym możesz wybrać bitmapę. (Jeœli potrzebujesz, możesz w tym kontekœcie wybrać także inne obiekty GDI). Rozdział 14: Bitmapy i BitBlty 603 Funkcja SelectObject zadziała jedynie wówczas, gdy bitmapa, którš wybierzesz w kontekœcie urzšdzenia pamięciowego, jest monochromatyczna bšdŸ też ma tę samš organizację kolorów co urzšdzenie, z którym kontekst urzšdzenia pamię- ciowego jest kompatybilny. To dlatego tworzenie dziwacznej DDB (na przykład 0 5 płaszczyznach i 3 bitach na piksel) nie jest użyteczne. Teraz uważaj: po wywołaniu SelectObject DDB staje się powierzchniš wyœwietla- nia kontekstu urzšdzenia pamięciowego. Z takim kontekstem urzšdzenia pamię- ciowego możesz zrobić prawie wszystko to, co możesz zrobić z kontekstem urzš- dzenia rzeczywistego. Na przykład jeœli użyjesz funkcji rysujšcych GDI do ryso- wania w kontekœcie urzšdzenia pamięciowego, obrazy zostanš narysowane na bitmapie. Może to być bardzo potrzebne. Możesz także wywołać BitBlt, używa- jšc kontekstu urzšdzenia pamięciowego jako Ÿródła oraz kontekstu monitora jako miejsca docelowego. W taki sposób możesz narysować bitmapę na ekranie. Aby coœ skopiować z ekranu do bitmapy, możesz także wywołać BitBlt, korzystajšc z kontekstu monitora jako Ÿródła, a kontekstu urzšdzenia pamięciowego, jako miejsca docelowego. Przeanalizujemy wszystkie te możliwoœci. Wczytywanie bitmap z zasobów Oprócz licznych funkcji tworzenia bitmap innym sposobem uzyskania uchwytu obiektu bitmapy GDI jest funkcja LoadBitmap. Korzystajšc z tej funkcji nie musisz zajmować się formatami bitmap. Po prostu tworzysz bitmapę jako zasób w swo- im programie, podobnie jak tworzy się ikony lub kursory myszy. Funkcja Load- Bitmap ma tę samš składnię co Loadlcon i LoadCursor: hBitmap = LoadBitmap (hInstance, szBitmapName) ; Jeœli chcesz wczytać bitmapę systemowš, pierwszy argument może być równy NULL. Bitmapami systemowymi sš liczne bitmapy używane do drobnych częœci interfejsu graficznego Windows, takie jak pole Zamknij i znaki wyboru. Ich iden- tyfikatory rozpoczynajš się od liter OBM. Drugi argument może użyć makra MAKEINTRESOURCE, jeżeli bitmapa jest zwišzana z identyfikatorem całkowi- tym, nie zaœ z nazwš. Wszystkie bitmapy wczytywane za pomocš funkcji Load- Bitmap powi.nny ostatecznie zostać usunięte za pomocš funkcji DeleteObject. Jeœli bitmapa z zasobu jest monochromatyczna, uchwyt zwracany z LoadBitmap będzie wskazywał monochromatyczny obiekt bitmapy. Jeżeli zasób bitmapy nie jest monochromatyczny, wówczas uchwyt zwracany z LoadBitmap będzie wska- zywał obiekt bitmapy GDI o takiej samej organizacji kolorów jak monitor, na któ- rym działa program. Dzięki temu bitmapa jest zawsze kompatybilna z monito- rem i może zostać wybrana w kontekœcie urzšdzenia pamięciowego kompatybil- nym z monitorem. Nie zajmuj się na razie żadnymi konwersjami kolorów, które mogš przebiegać w tle podczas wywołania LoadBitmap. Już w następnym roz- dziale zrozumiesz, jak to działa. Program BRICKSI, przedstawiony na rysunku 14-5, pokazuje, jak wczytać nie- wielki monochromatyczny zasób bitmapy. Sama bitmapa na pierwszy rzut oka nie kojarzy się z cegłš (ang. brick), ale gdy zostanie powtórzona pionowo i pozio- mo, przypomina ceglany mur. 604 Częœć II: Grafika BRICKSl.C /* BRICKSI.C - Demonstracja dzialania funkcji LoadBitmap (c) Charles Petzold, 1998 */ ţţinclude LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) ( static TCHAR szAppName [7 = TEXT ("Bricksl") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CSţVREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITEţBRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass>) ( MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MBţICONERROR) ; return 0 ; hwnd = CreateWindow (szAppName, TEXT ("LoadBitmap Demo"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CWţUSEDEFAULT, CW_USEDEFAULT, CWţUSEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) ( ) TranslateMessage (&msg) ; DispatchMessage (&msg) ; B R I } return msg.wParam ; //Miţ 1 ţţi nc LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ţiţincţ ( static HBITMAP hBitmap ; ////i static int cxClient, cyClient, cxSource, cySource ; // Bi BITMAP bitmap ; BRICK Rozdział 14: Bitmapy i BitBlty 605 HDC hdc, hdcMem ; HINSTANCE hInstance ; int x, y ; PAINTSTRUCT ps ; switch (message) ( case WM_CREATE: hInstance = ((LPCREATESTRUCT) lParam)->hInstance ; hBitmap = LoadBitmap (hInstance, TEXT ("Bricks")) ; GetObject (hBitmap, sizeof (BITMAP), &bitmap) ; cxSource = bitmap.bmWidth ; cySource = bitmap.bmHeight ; return 0 ; case WM_SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; hdcMem = CreateCompatibleDC (hdc) ; SelectObject (hdcMem, hBitmap) ; for (y = 0 ;, y < cyClient ; y += cySource) for (x = 0 ; x < cxClient ; x += cxSource) ( BitBlt (hdc, x, y, cxSource, cySource, hdcMem, 0, 0, SRCCOPY) ; ) DeleteDC (hdcMem) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: Delete0bject (hBitmap) ; PostOuitMessage (0) ; return 0 ; l return DefWindowProc (hwnd, message, wParam, lParam) ; } BRICKSl.RC (fragmenty) //Microsoft Developer Studio generated resource script. ţtinclude "resource.h" 4linclude "afxres.h" ///!////////////////////////////////////////////!/////////////////// // Bitmap BRICKS BITMAP DISCARDABLE "Bricks.bmp" 606 Częœć II: Grafika ţ Rozdzi (cišg dalszy ze strony 605) ţ FOill' BRICKS.BMP Rysunek 14-5. Program BRICKS1 Podczas tworzenia bitmapy w Visual C++ Developer Studio należy podać, że zarówno wysokoœć, jak i szerokoœć bitmapy wynosi 8 pikseli, jest ona monochro- matyczna, a jej nazwa to "Bricks". Program BRICKS1 wczytuje bitmapę w czasie przetwarzania komunikatu WM_CREATE i do okreœlenia jej rozmiaru w pikse- lach używa funkcji GetObject (by program nadal działał, jeœli bitmapa nie jest kwa- dratem o boku 8 pikseli). Następnie BRICKSI usuwa uchwyt bitmapy, używajšc komunikatu WM_DESTROY. W czasie komunikatu WMţPAINT program BRICKS1 tworzy kontekst urzšdze- nia pamięciowego kompatybilny z monitorem i wybiera w nim bitmapę. Z kolei następuje seria wywołań BitBlt z kontekstu urzšdzenia pamięciowego do kon- tekstu urzšdzenia obszaru roboczego okna. Potem uchwyt kontekstu urzšdzenia pamięciowego jest wymazywany. Działajšcy program pokazany jest na rysun- ku 14-6. Przy okazji plik BRICKS.BMP tworzony przez Developer Studio jest bitmapš nie- zależnš od urzšdzeń. Jeżeli chcesz spróbować, utwórz kolorowy plik BRICKS.BMP w Developer Studio (w dowolnym wybranym przez siebie formacie koloru) i sprawdŸ, że wszystko działa prawidłowo. Tak oto rzekonaliœmy się, że DIB ulega konwersji na obiekty bitmapowe GDI, które sš kompatybilne z monitorem. W następnym rozdziale zobaczymy, jak to działa. s t a t i ţ statiţ bitmaţ hBi tmţ hBi tma SetBii hBi tma Rysunek 14-6. Okno programu BRICKS1 Rozdział 14: Bitmapy i BitBity 607 Format bitmapy monochromatycznej Jeœli pracujesz na małych obrazach monochromatycznych, nie musis tworzyć ich w postaci zasobów. W przeciwieństwie do obiektów bitmap kolorowych, format bitów monochromatycznych jest relatywnie prosty i może być wyprowadzony bezpoœrednio od obrazu, który zamierzasz utworzyć. Załóżmy, że chcesz zbudo- wać bitmapę wyglšdajšcš tak: Możesz wpisać serię bitów (0 oznacza czarny, a 1 biały) bezpoœrednio przedsta- wiajšcš tę siatkę. Czytajšc te bity od lewej do prawej możesz przypisać każdš grupę 8 bitów heksadecymalnemu bajtowi. Jeżeli szerokoœć bitmapy nie jest wie- lokrotnoœciš 16, dopełruj bajt z prawej strony zerami tak, by uzyskać parzystš liczbę bajtów: 01010001011101110001=51771000 01010111011101110101=57775000 00010011011101110101=13775000 0 1 0 1 0 1 1 1 0 1 1 1 0 1 1 1 0 1 0 1 = 57 77 50 00 01010001000100010001=51111000 Szerokoœć w pikselach jest równa 20, wysokoœć wynosi 5 linii, natomiast szero- koœć wyrażona w bajtach równa się 4. Za pomocš następujšcej instrukcji możesz ustawić strukturę typu BIT'MAP dla tej bitmapy: static BITMAP bitmap = ( 0, 20, 5, 4, 1, 1 ) ; oraz zapisać bity w tablicy bajtów: static BYTE bits C] = i 0x51, 0x77, 0x10, 0x00, 0x57, 0x77, 0x50, 0x00, 0x13, 0x77, 0x50, 0x00, 0x57, 0x77, 0x50, 0x00, 0x51, Oxll, 0x10, 0x00 ) ; Utworzenie bitmapy za pomocš CreateBitmaplndirect wymaga dwóch instrukcji: bitmap.bmBits = (PSTR) bits ; hBitmap = CreateBitmapIndirect (&bitmap) ; Oto jeszcze inne rozwišzanie: hBitmap = CreateBitmapIndirect (&bitmap) ; SetBitmapBits (hBitmap, sizeof bits, bits) ; Bitmapę możesz także utworzyć za pomocš jednej instrukcji: hBitmap = CreateBitmap (20, 5, 1. 1, bits) ; Program BRICKS2 zaprezentowany na rysunku 14-7 wykorzystuje tę technikę do utworzenia ceglanej bitmapy bezpoœrednio, bez sięgania do zasobu. 608 CzQœć II: Gţafika BRICKS2.C /* BRICKS2.C - Demonstracja dzialania funkcji CreateBitmap (c) Charles Petzold, 1998 */ ţţinclude LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) ( static TCHAR szAppName [] = TEXT ("Bricks2") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CS-VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITEţBRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!Re9isterClass (&wndclass)) ( MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MB ICONERROR) ; return 0 ; ) hwnd = CreateWindow (szAppName, TEXT ("CreateBitmap Demo"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) ( TranslateMessage (&msg) ; DispatchMessage (&msg) ; return msg.wParam ; LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ( static BITMAP bitmap = [ 0, 8, 8, 2, 1, 1 ) ; static BYTE bits [8][2] = ( OxFF, 0, OxOC, 0, OxOC, 0, OxOC, 0, Rozdział 14: Bitmapy i BitBlty 609 OxFF, 0, OxCO, 0, OxCO, 0, OxCO, 0 ) ; static HBITMAP hBitmap : static int cxClient, cyClient, cxSource, cySource : HDC hdc, hdcMem : int x, y : PAINTSTRUCT ps ; switch (message) ( case WM_CREATE: bitmap.bmBits = bits ; hBitmap = CreateBitmapIndirect (&bitmap) ; cxSource = bitmap.bmWidth ; cySource = bitmap.bmHeight ; return 0 : case WM_SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) : hdcMem = CreateCompatibleDC (hdc) ; SelectObject (hdcMem, hBitmap) ; for (y = 0 : y < cyClient ; y += cySource) for (x = 0 ; x < cxClient ; x += cxSource) ( BitBlt (hdc. x, y, cxSource, cySource, hdcMem, 0, 0, SRCCOPY) ; 1 DeleteDC (hdcMem) ; EndPaint (hwnd, &ps) : return 0 : i case WM_DESTROY: Delete0bject (hBitmap) ; PostOuitMessage (0) ; return 0 ; return DefWindowProc (hwnd, message, wParam, lParam) ; Rysunek 14-7. Program BRICKS2 Możesz mieć pokusę spróbowania podobnej sztuczki z kolorowš bitmapš. Jeżeli twój monitor działa w trybie wyœwietlania 256 kolorów, możesz wykorzystać przedstawionš wczeœniej tabelę kolorów (drugš tabelę z podrozdziału "Urzšdze- nia rzeczywiste") do zdefiniowania każdego piksela kolorowego fragmentu ce- glanego muru. Jednak taki kod nie będzie działał, gdy program zostanie urucho- miony w jakimkolwiek innym trybie wyœwietlania kolorów. Stosowanie koloro- wych bitmap w sposób niezależny od urzšdzeń wymaga użycia formatu DIB, omówionego w następnym rozdziale. 610 Częœć II: Grafika ţ Roz Bitmapowe pędzle Ostatniš wersjš programu BRICKS jest BRICKS3, zaprezentowany na rysunku 14-8. Pierwsze, pobieżne przejrzenie kodu programu może sprowokować pytanie "No dobrze, ale gdzie jest kod?" BRICKS3.C /* BRICKS3.C - Oemonstracja działania funkcji CreatePatternBrush LRE; (c) Charles Petzold, 1998 ( */ ţţinclude LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) ) ( static TCHAR szAppName [] = TEXT ("Bricks3") ; HBITMAP hBitmap ; B R. HBRUSH hBrush ; HWND hwnd ; // P MSG msg ; WNDCLASS wndclass ; ţţinc ţţi n< hBitmap = LoadBitmap (hInstance, TEXT ("Bricks")) ; hBrush = CreatePatternBrush (hBitmap) ; ///i Delete0bject (hBitmap) ; // wndclass.style = CS_HREDRAW ţ CSţVREDRAW ; BRIC wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC ARROW) ; wndclass.hbrBackground = hBrush ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) t MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MB-ICONERROR) ; return 0 ; hwnd = CreateWindow (szAppName, TEXT ("CreatePatternBrush Demo"), WS OVERLAPPEDWINDOW, CW_USEDEFAULT, CW USEDEFAULT, CW-USEDEFAULT, CW USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; Rozdział 14: Bitmapy i BitBlty while (GetMessage (&msg, NULL, 0, 0)) ( TranslateMessage (&msg) ; DispatchMessage (&msg) ; Delete0bject (hBrush) ; return msg.wParam ; LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ( switch (message) ( case WMţDESTROY: PostOuitMessage (0) ; return 0 : ) return DefWindowProc (hwnd, message, wParam, lParam) ; BRICKS3.RC (fragmenty) // Microsoft Developer Studio generated resource script ţţinclude "resource.h" iţinclude "afxres.h" ///////////////////////////////////////1//////////////////////////// // Bitmap BRICKS BITMAP DISCARDABLE "Bricks.bmp" Rysunek 14-8. Program BRICKS3 Powyższy program wykorzystuje ten sam plik BRICKS.BMP co program BRICKS1 i jego okno wyglšda tak samo. Jak widzisz, procedura okna nie robi zbyt wiele. Właœciwie BRICKS3 wykorzy- stuje wzór z cegieł jako pędzel tła klasy okna. Pędzel ten jest zdefiniowany w po- lu hbrBackground struktury WNDCLASS. Jak pewnie sam się domyœlasz, pędzle GDI sš malutkimi bitmapami, zazwyczaj w kształcie kwadratu o boku 8 pikseli. Możesz zrobić pędzel bez bitmapy, wy- wołujšc funkcję CreatePatternBrush lub CreateBrushlndirect z polem lbStyle struk- tury LOGBRUSH ustawionym na BS PATTERN. Bitmapa może mieć najwyżej 8 pikseli szerokoœci i 8 pikseli wysokoœci. jeœli będzie większa, Windows 98 wy- korzysta tylko lewy górny róg bitmapy przeznaczonej na pędzel. Natomiast Win- dows NT nie ma tego ograniczenia i wykorzysta całš bitmapę. Pamiętaj, że zarówno pędzle, jak i bitmapy sš obiektami GDI i powinieneœ usu- nšć wszystkie, które utworzyłeœ w programie, zanim zakończy on działanie. Kie- dy tworzysz pędzel na podstawie bitmapy, Windows robi kopię bitów bitmapy do użycia podczas rysowania pędzlem. Możesz więc usunšć bitmapę od razu po wywołaniu funkcji CreatePatternBrush (lub CreateBrushlndirect) bez wpływu na sam 612 Częœć II: Grafika pędzel. Podobnie możesz usunšć pędzel nie wpływajšc na oryginalnš bitmapę, z której został wycięty. Zauważ, że BRICKS3 usuwa bitmapę po utworzeniu pędz- la oraz usuwa pędzel zanim program zakończy działanie. Rysowanie na bitmapach Wykorzystywaliœmy bitmapy jako zasób w celu rysowania w naszym oknie. To wymaga wybrania bitmapy w kontekœcie urzšdzenia pamięciowego i wywoła- nia BitBlt lub StretchBlt. Można także użyć uchwytu kontekstu urzšdzenia pa- mięciowego jako pierwszego argumentu dla praktycznie wszystkich wywołań funkcji GDI. Kontekst urzšdzenia pamięciowego zachowuje się tak samo jak kon- tekst urzšdzenia rzeczywistego, z tym tylko wyjštkiem, że powierzchniš wyœwie- tlania jest bitmapa. Program HELLOBIT, przedstawiony na rysunku 14-9, pokazuje tę technikę. Pro- gram wyœwietla na małej bitmapie napis "Hello, world!", a następnie wywołuje BitBlt lub StretchBlt, w zależnoœci od wyboru z menu, i kopiuje napis z bitmapy do obszaru roboczego okna programu. HELLOBIT.C /* HELLOBIT.C - Demonstracja użycia bitmapy (c) Charles Petzold, 1998 */ tlinclude Itinclude "resource.h" LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) static TCHAR szAppName C] = TEXT ("HelloBit") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CSţVREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITEţBRUSH) wndclass.lpszMenuName = szAppName ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) ( MessageBox (NULL, TEXT ("This program requires Windows.NT!"), szAppName, MB ICONERROR) ; return 0 ; Rozdział 14: Bitmapy i BitBlty 613 3 hwnd = CreateWindow (szAppName, TEXT ("HelloBit"), WS_OVERLAPPEDWINDOW CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, ;'ł NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) : while (GetMessage (&ms9, NULL, 0, 0)) ( TranslateMessage (&msg) ; DispatchMessage (&msg) ; l return msg.wParam ; i LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam> ( static HBITMAP hBitmap : static HDC hdcMem ; static int cxBitmap, cyBitmap, cxClient, cyClient, iSize = IDMţBIG ; static TCHAR * szText = TEXT (" Hello, world! ") ; HDC hdc ; HMENU hMenu ; int x, y ; PAINTSTRUCT ps ; SIZE size ; switch (message) f case WM_CREATE: hdc = GetDC (hwnd) : hdcMem = CreateCompatibleDC (hdc) : GetTextExtentPoint32 (hdc, szText, lstrlen (szText>, &size) ; t cxBitmap = size.cx : cyBitmap = size.cy ; hBitmap = CreateCompatibleBitmap (hdc, cxBitmap, cyBitmap) ; ReleaseDC (hwnd, hdc) : SelectObject (hdcMem, hBitmap) ; TextOut (hdcMem, 0, 0, szText, lstrlen (szText)) ; return 0 ; case WM_SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; return 0 ; case WM COMMAND: hMenu = GetMenu (hwnd) : switch (LOWORD (wParam)) ( 614 Częœć II: Grafika (cišg dalszy ze strony 613) case IDM_BIG: case IDM_SMALL: CheckMenuItem (hMenu, iSize, MF UNCHECKED) ; iSize = LOWORD (wParam) ; CheckMenuItem (hMenu, iSize, MF CHECKED) ; InvalidateRect (hwnd, NULL, TRUE) ; break ; 1 return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; switch (iSize) ( case IDM_BIG: StretchBlt (hdc, 0, 0, cxClient, cyClient, hdcMem, 0, 0, cxBitmap, cyBitmap, SRCCOPY) ; break ; case IDM_SMALL: for (y = 0 ; y < cyClient ; y += cyBitmap) for (x = 0 ; x < cxClient ; x += cxBitmap) f BitBlt (hdc, x, y, cxBitmap, cyBitmap, hdcMem, 0, 0, SRCCOPY) ; ) break ; EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: DeleteDC (hdcMem) ; Delete0bject (hBitmap) ; PostOuitMessage (0) ; return 0 ; ? return DefWindowProc (hwnd, message, wParam, lParam) ; ) HELLOBIT.RC (fragmenty) // Microsoft Developer Studio generated resource script. 4linclude "resource.h" tlinclude "afxres.h" /////////////////////////////////////////////////////////////////////////// // Menu HELLOBIT MENU DISCARDABLE BEGIN POPUP "&Size" BEGIN Rozdział 14: Bitmapy i BitBlty 615 MENUITEM "&Big", IDM_BIG, CHECKED MENUITEM "&Small", IDM SMALL END END RESOURCE.H (fragmenty) // Microsoft Developer Studio generated include file. // Used by HelloBit.rc // itdefine IDM BIG 40001 itdefine IDM SMALL 40002 Rysunek 14-9. Program HELLOBIT Program zaczyna od okreœlenia rozmiarów napisu w pikselach przez wywołanie funkcji GetTextExtentPoint32. Wymiary te stanš się rozmiarem bitmapy kompaty- bilnej z monitorem. Gdy już ta bitmapa zostanie wybrana w kontekœcie urzšdze- nia pamięciowego (także kompatybilnego z monitorem), wywołanie funkcji Te- xtOut umieœci tekst na bitmapie. Kontekst urzšdzenia pamięciowego jest zacho- wywany przez cały czas trwania programu. Podczas przetwarzania komunikatu WM DESTIZOY program HELLOBIT usuwa zarówno bitmapę, jak i kontekst urzš- dzenia pamięciowego. Wybór z menu programu HELLOBIT pozwala wyœwietlić bitmapę normalnej wielkoœci, powielonš wielokrotnie w pionie i w poziomie, lub rozcišgnšć jš do rozmiaru całego obszaru roboczego, co przedstawia rysunek 14-10. Jak widać nie jest to najlepszy sposób na wyœwietlanie tekstu dużej wielkoœci. To jedynie po- większona wersja małej czcionki z wyolbrzymionymi wszystkimi szczerbami i nie- równoœciami. Rysunek 14-10. Okno programu HELLOBIT 616 Częœć II: Grafika ţ Ro: Możesz być zaskoczony, że w takim programie jak HELLOBIT może być potrzebna obsługa komunikatu WM DISPLAYCHANGE. Aplikacja otrzymuje ten komuni- kat zawsze wtedy, gdy użytkownik (albo inna aplikacja) zmieni rozmiar ekranu albo głębię koloru. Może się zdarzyć, że zmiana głębi koloru będzie miała wpływ na kontekst urzšdzenia pamięciowego oraz kontekst urzšdzenia wyœwietlajšce- go, przez co stanš się one niekompatybilne. To się jednak nie zdarzy, bo gdy zo- stanie zmieniony tryb wyœwietlania, system Windows automatycznie zmodyfi- kuje głębię koloru kontekstu urzšdzenia pamięciowego. Bitmapa wybrana w kon- tekœcie urzšdzenia pamięciowego pozostaje niezmieniona, ale to wydaje się nie stwarzać problemów. Bitmapa cieniowa Tedmika rysowania w kontekœcie urzšdzenia pamięciowego (czyli na bitmapie) jest kluczem do implementacji bitmapy cieniowej, to znaczy bitmapy zawierajšcej wszystko, co jest wyœwietlone w obszarze roboczym okna. Obróbka komunikatu 4VM PAINT redukuje się tym sposobem do prostego wywołania funkcji BitBlt. Bitmapy cieniowe sš najbardziej przydatne w programach rysujšcych. SKETCH, zaprezentowany na rysunku 14-11, nie jest najbardziej wyrafinowanym progra- mem rysujšcym na œwiecie, ale na poczštek wystarczy. SKETCH.C i* SKETCH.C - Demonstracja bitmapy cieniowej (c) Charles Petzold, 1998 voi *ţ ( tlinclude LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) static TCHAR szAppName [] = TEXT ("Sketch") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CS UREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; LRf wndclass:cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDCţARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITEţBRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) Rozdział 14: Bitmapy i BitBlty 617 MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MBţICONERROR) ; return 0 : hwnd = CreateWindow (szAppName, TEXT ("Sketch"), WS OVERLAPPEDWINDOW, CW USEDEFAULT, CW USEDEFAULT, CW USEDEFAULT, CWţUSEDEFAULT, NULL, NULL, hInstance, NULL) ; ; if (hwnd ř NULL) i. Messa9eBox (NULL, TEXT ("Not enough memory to create bitmap!"), szAppName, MB ICONERROR) ; return 0 ; 1 ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; I'. i. 4,. while (GetMessage (&msg, NULL, 0, 0)) TranslateMessage (&msg) ; DispatchMessage (&msg) ; return msg.wParam ; void GetLargestDisplayMode (int * pcxBitmap, int * pcyBitmap) DEVMODE devmode ; int iModeNum = 0 ; * pcxBitmap = * pcyBitmap = 0 ; ZeroMemory (&devmode, sizeof (DEVMODE)) ; devmode.dmSize = sizeof (DEVMODE) ; while (EnumDisplaySettings (NULL, iModeNum++, &devmode)) * pcxBitmap = max (* pcxBitmap, (int) devmode.dmPelsWidth) ; * pcyBitmap = max (* pcyBitmap. (int) devmode.dmPelsHeight) ; ) LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ( static BOOL fLeftButtonDown, fRightButtonDown ; static HBITMAP hBitmap ; static HDC hdcMem ; static int cxBitmap, cyBitmap, cxClient, cyClient, xMouse, yMouse ; HDC hdc ; PAINTSTRUCT ps ; switch (message) ( sţs Częœć II: Grafika (cišg dalszy ze strony 617) case WM_CREATE: GetLargestDisplayMode (&cxBitmap. &cyBitmap) ; hdc = GetDC (hwnd) ; hBitmap = CreateCompatibleBitmap (hdc, cxBitmap, cyBitmap) ; hdcMem = CreateCompatibleDC (hdc) ; ReleaseDC (hwnd, hdc) ; if (!hBitmap) // brak pamięci dla bitmapy f DeleteDC (hdcMem) ; return -1 ; ) SelectObject (hdcMem, hBitmap) ; PatBlt (hdcMem, 0, 0, cxBitmap, cyBitmap, WHITENESS) ; return 0 ; case WM_SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; return 0 ; case WM_LBUTTONDOWN: if (!fRightButtonDown) SetCapture (hwnd) ; xMouse = LOWORD (lParam) ; yMouse = HIWORD (lParam) ; fLeftButtonDown = TRUE ; return 0 ; case WM_LBUTTONUP: if (fLeftButton0own) SetCapture (NULL) ; fLeftButton0own = FALSE ; return 0 ; case WM_RBUTTONDOWN: if (!fLeftButton0own) SetCapture (hwnd) ; xMouse = LOWORD (lParam) ; yMouse = HIWORD (lParam) ; fRightButton0own = TRUE ; return 0 ; case WM_RBUTTONUP: if (fRightButtonDown) SetCapture (NULL) ; fRightButtonDown = FALSE ; return 0 ; case WM MOUSEMOVE: Rozdział 14: Bitmapy i BitBlty 619 if (!fLeftButtonDown && !fRightButtonDown) return 0 : hdc = GetDC (hwnd) ; 1. SelectObject (hdc, ; GetStockObject (fLeftButtonDown ? BLACK PEN : WHITE-PEN)) ; "'ţř - ił: SelectObject (hdcMem, ţ:ţ' GetStockObject (fLeftButtonDown ? BLACKţPEN : WHITE-PEN)) ; '.ţ; MoveToEx (hdc xMouse, yMouse, NULL) ; MoveToEx (hdcMem, xMouse, yMouse, NULL) ; xMouse = (short) LOWORD (lParam) ; yMouse = (short) HIWORD (lParam) ; LineTo (hdc, xMouse, yMouse) ; LineTo (hdcMem, xMouse, yMouse) ; ReleaseDC (hwnd, hdc) ; return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps> ; BitBlt (hdc, 0, 0, cxClient, cyClient, hdcMem, 0, 0. SRCCOPY) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: DeleteDC (hdcMem) ; Delete0bject (hBitmap) ; PostOuitMessage (0) ; return 0 ; '! return DefWindowProc (hwnd, messa9e, wParam, lParam) ; 'j'.,. ) Rysunek 14-11. Program SKETCH Aby w programie SKETCH narysować linię, należy nacisnšć lewy przycisk myszy i przesunšć mysz. W celu wymazania (a precyzyjniej mówišc narysowania białej linii) należy nacisnšć prawy przycisk myszy i przesunšć jš. Chcšc wyczyœcić całe okno, należy - zakończyć program, uruchomić go ponownie i zaczšć wszystko od poczštku. IZysunek 14-12 pokazuje ekran programu SKETCH w hołdzie złożonym wczesnym reklamom Macintosha firmy Apple. 620 Częœć II: Grafika Rysunek 14-12. Ekran programu SKETCH Jak duża powinna być bitmapa cieniowa? W programie SKETCH na tyle duża, żeby pokryć cały obszar roboczy zmaksymalizowanego okna. Jest to doœć łatwe do obliczenia dzięki informacjom z funkcji GetSystemMetrics, ale co się stanie, je- œli użytkownik zmieni ustawienia ekranu i spowoduje, że ekran, i co za tym idzie, rozmiar zmaksymalizowanego okna, stanie się większy? Program SKETCH im- plementuje doœć brutalne rozwišzanie "siłowe" za pomocš funkcji EnumDisplay- Settings. Ta funkcja wykorzystuje strukturę DEVMODE do uzyskania informacji na temat wszystkich dostępnych trybów pracy monitora. Podczas pierwszego wywołania funkcji EnumDisplaySettings ustaw jej drugi argument na 0 i zwięk- szaj tę wartoœć za każdym kolejnym wywołaniem. Kiedy EnumDisplaySettings zwróci FALSE, nic więcej już nie możesz zrobić. Na podstawie takiej informacji SKETCH utworzy bitmapę cieniowš, która może być ponad cztery razy większa od powierzchni aktualnego trybu pracy monitora i wymagać wielu megabajtów pamięci. Z tego powodu SKETCH sprawdza, czy bitmapa została już utworzona i jeœli stwierdzi, że nie jest, zwraca z komunikatu WMţCREATE wartoœć -1, co oznacza błšd. Program SKETCH wychwytuje ruchy myszy, gdy naciœnięty jest lewy lub prawy jej przycisk, i podczas przetwarzania komunikatu WM MOUSEMOVE rysuje li- nie zarówno w kontekœcie urzšdzenia pamięciowego, jak i w kontekœcie urzšdze- nia dla obszaru roboczego okna. Jeżeli rysunek ma być odrobinę bardziej złożo- ny, prawdopodobnie będziesz wolał zaimplementować to w funkcji, którš pro- gram wywoła dwa razy - raz dla kontekstu urzšdzenia monitora, a drugi raz dla kontekstu urzšdzenia pamięciowego. Oto ciekawy eksperyment: najpierw tak zmniejsz okno programu SKETCH, by nie zajmowało powierzchni całego ekranu. Trzymajšc naciœnięty lewy przycisk myszy narysuj coœ w oknie i pozwól jej wykroczyć poza prawš lub dolnš jego Rozdział 14: Bitmapy i BitBlty 621 krawędŸ. Ponieważ program SKETCH wychwytuje działania myszy, kontynu- uje przyjmowanie i przetwarzanie komunikatów WM MOUSEMOVE. A teraz powiększ okno. Widzisz? Bitmapa cieniowa zawiera rysunek wykonany poza granicami okna! Wykorzystanie bitmap w menu Bitmap możesz także używać do wyœwietlania elementów menu. Jeœli natych- miast przypomniały ci się rysunki folderów plików, słoików z klejem i koszy z menu, porzuć myœl o nich. Pomyœl natomiast, jak przydatne mogš być bitmapy użyte w menu programu rysujšcego. WyobraŸ sobie zastosowanie w swoim menu różnych czcionek i ich rozmiarów, szerokich linii, deseni i kolorów. Prosty program demonstrujšcy graficzne elementy menu został nazwany GRAF- MENU. Najwyższy poziom menu tego programu jest pokazany na rysunku 14- 13. Powiększone litery zostały uzyskane z plików bitmap monochromatycznych o rozmiarach 40 na 16 pikseli, utworzonych w Visual C++ Developer Studio. Wybranie menu FONT wywołuje rozwinięcie menu zawierajšcego trzy opcje - Courier New, Arial oraz Tixnes New Roman. Sš to standardowe czcionki Win- dows TrueType, a każda nazwa jest wyœwietlana tš właœnie czcionkš, co widać na rysunku 14-14. Te bitmapy zostały utworzone w programie przy użyciu kon- tekstu urzšdzenia pamięciowego. Rysunek 14-13. Najwyższy poziom menu programu GRAFMENU 622 Częœć II: Grafika Rysunek 14-14. Opcje menu FONT programu GRAFMENU Na koniec, gdy rozwiniesz menu systemowe, zauważysz, że masz dostęp do opcji pomocy, w której słowo "Help!!" odzwierciedla desperację nowego użytkowni- ka (patrz rysunek 14-15). Ta monochromatyczna bitmapa 64 na 64 piksele została utworzona w Developer Studio. Rysunek 14-15. System menu programu GRAFMENU Program GiZAFMENU wraz z czterema bitmapami utworzonymi w Developer Studio został pokazany na rysunku 14-16. Rozdział 14: Bitmapy i BitBlty 623 GRAFMENU.C )„ /* GRAFMENU.C - Demonstracja bitmapowych elementów menu (c) Charles Petzold, 1998 * ţ (:. 1# ţţinclude r, ţţinclude "resource.h" LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; void AddHelpToSys (HINSTANCE, HWND) ; HMENU CreateMyMenu (HINSTANCE) ; HBITMAP StretchBitmap (HBITMAP) ; _ HBITMAP GetBitmapFont (int) ; void DeleteAllBitmaps (HWND) ; TCHAR szAppName[] = TEXT ("GrafMenu") ; " i: int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) 6... ţ HWND hwnd ; i MSG msg ; WNDCLASS wndclass ; "'! wndclass.style = CS_HREDRAW ţ CSţVREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; `' wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITEţBRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; j ,„ if (!RegisterClass (&wndclass)) t MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MBţICONERROR) ; return 0 ; hwnd = CreateWindow (szAppName, TEXT ("Bitmap Menu Demonstration"), WS_OVERLAPPEDWINDOW, CWţUSEDEFAULT, CW USEDEFAULT, CW_USEDEFAULT, CW USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) ( TranslateMessage (&msg) ; DispatchMessage (&msg) ; ) 624 Częœć II: Grafika (cišg dalszy ze strony 623) return msg.wParam ; LRESULT CALLBACK WndProc (HWND hwnd, UINT iMsg, WPARAM wParam, LPARAM lParam) ( HMENU hMenu ; static int iCurrentFont = IDMţFONT COUR ; switch (iMsg) ( case WM_CREATE: AddHelpToSys (((LPCREATESTRUCT) lParam)->hInstance, hwnd) ; hMenu = CreateMyMenu (((LPCREATESTRUCT) lParam)->hInstance) ; SetMenu (hwnd, hMenu) ; CheckMenuItem (hMenu, iCurrentFont, MFţCHECKED) ; return 0 ; case WM SYSCOMMAND: switch (LOWORD (wParam)) ( case IDM_HELP: MessageBox (hwnd, TEXT ("Help not yet implemented!"), szAppName, MB OK ţ MBţICONEXCLAMATION) ; return 0 ; 1 break ; case WM COMMAND: switch (LOWORD (wParam)) ( case IDM_FILE_NEW: case IDM_FILE_OPEN: case IDM_FILE_SAVE: case IDM_FILE_SAVE AS: case IDM_EDIT_UNDO: case IDM_EDIT_CUT: case IDM_EDIT_COPY: case IDM_EDIT_PASTE: case IDMţEDIT_CLEAR: MessageBeep (0) ; return 0 ; case IDM_FONT_COUR: case IDM_FONT_ARIAL: case IDM_FONT_TIMES: hMenu = GetMenu (hwnd) ; CheckMenuItem (hMenu, iCurrentFont, MF UNCHECKED) ; iCurrentFont = LOWORD (wParam) ; CheckMenuItem (hMenu, iCurrentFont, MF CHECKED) ; return 0 ; ) break ; case WM_DESTROY: DeleteAllBitmaps (hwnd) ; PostOuitMessage (0) ; Rozdział 14: Bitmapy i BitBlty 625 return 0 ; return DefWindowProc (hwnd, iMsg, wParam, lParam) ; /* AddHelpToSys: Dodaje bitmapowy element Help do menu systemowego */ void AddHelpToSys (HINSTANCE hInstance, HWND hwnd) ( HBITMAP hBitmap ; HMENU hMenu ; hMenu = GetSystemMenu (hwnd, FALSE); hBitmap = StretchBitmap (LoadBitmap (hInstance, TEXT ("BitmapHelp"))) ; AppendMenu (hMenu, MF_SEPARATOR, 0, NULL) ; AppendMenu (hMenu, MFţBITMAP, IDM HELP, (PTSTR) (LONG) hBitmap) ; l /* CreateMyMenu: Sklada menu z komponentów */ HMENU CreateMyMenu (HINSTANCE hInstance) HBITMAP hBitmap ; HMENU hMenu, hMenuPopup ; int i ; hMenu = CreateMenu () ; hMenuPopup = LoadMenu (hInstance, TEXT ("MenuFile")) ; k hBitmap = StretchBitmap (LoadBitmap (hInstance, TEXT ("BitmapFile"))) ; AppendMenu (hMenu, MF_BITMAP ţ MF_POPUP, (int) hMenuPopup, (PTSTR) (LONG) hBitmap) ; ' ,, :( : hMenuPopup = LoadMenu (hInstance, TEXT ("MenuEdit")) ; hBitmap = StretchBitmap (LoadBitmap (hInstance, TEXT ("BitmapEdit"))) ; AppendMenu (hMenu, MF_BITMAP ţ MF_POPUP, (int) hMenuPopup, (PTSTR) (LONG) hBitmap) ; hMenuPopup = CreateMenu () ; for (i = 0 ; i < 3 ; i++) f hBitmap = GetBitmapFont (i) ; AppendMenu (hMenuPopup, MF_BITMAP, IDM_FONT COUR + i, (PTSTR) (LONG) hBitmap) ; 1 hBitmap = StretchBitmap (LoadBitmap (hInstance, TEXT ("BitmapFont"))) ; AppendMenu (hMenu, MF BITMAP ţ MF_POPUP, (int) hMenuPopup, (PTSTR) (LONG) hBitmap) ; return hMenu ; tţ:..., s . i 626 Częœć II: Grafika (cišg dalszy ze strony 625) /* StretchBitmap: Skaluje bitmapę zależnie od rozdzielczoœci monitora */ HBITMAP StretchBitmap (HBITMAP hBitmapl) ( BITMAP bml, bm2 ; HBITMAP hBitmap2 ; HDC hdc, hdcMeml, hdcMem2 ; int cxChar, cyChar ; // WeŸ szerokoœć i wysokoœć znaku czcionki systemowej cxChar = LOWORD (GetDialogBaseUnits ()) ; cyChar = HIWORD (GetDialogBaseUnits ()) ; // Utwórz dwa konteksty urzšdzeń pamięciowych // kompatybilne z monitorem hdc = CreateIC (TEXT ("DISPLAY"), NULL, NULL, NULL) ; hdcMeml = CreateCompatibleDC (hdc) ; hdcMem2 = CreateCompatibleDC (hdc) ; DeleteDC (hdc) ; // WeŸ wymiary bitmapy, którš należy rozcišgndć GetObject (hBitmapl, sizeof (BITMAP), (PTSTR) &bml) ; // Przeskaluj te wymiary na podstawie // rozmiaru czcionki systemowej bm2 = bml ; bm2.bmWidth = (cxChar * bm2.bmWidth) / 4 ; bm2.bmHeight = (cyChar * bm2.bmHeight) / 8 ; bm2.bmWidthBytes = ((bm2.bmWidth + 15) / 16) * 2 ; // Utwórz nowd, większd bitmapę hBitmap2 = CreateBitmapIndirect (&bm2) ; // Wybierz bitmapy w kontekstach urzddzeń pamięciowych // i wywołaj StretchBlt SelectObject (hdcMeml, hBitmapl) ; SelectObject (hdcMem2, hBitmap2) ; StretchBlt (hdcMem2, 0, 0. bm2.bmWidth, bm2.bmHeight, hdcMeml, 0, 0. bml:bmWidth, bml.bmHeight, SRCCOPY) ; // Usuń konteksty z pamięci DeleteDC (hdcMeml) ; DeleteDC (hdcMem2) ; Delete0bject (hBitmapl) ; return hBitmap2 ; Rozdział 14: Bitmapy i BitBlty - 627 /* GetBitmapFont: Buduje bitmapy z nazwami czcionek */ Iţ HBITMAP GetBitmapFont (int i) I:'ţ ( static TCHAR * szFaceNameC3] = ( TEXT ("Courier New"), "; TEXT ("Arial"), I TEXT ("Times New Roman") ) ; HBITMAP hBitmap ; HDC hdc, hdcMem ; HFONT hFont ; SIZE size ; TEXTMETRIC tm ; 1. hdc = CreateIC (TEXT ("DISPLAY"), NULL, NULL, NULL) ; GetTextMetrics (hdc, &tm) ; hdcMem = CreateCompatibleDC (hdc) ; hFont = CreateFont (2 * tm.tmHeight, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, szFaceNameCi]) ; hFont = (HFONT) SelectObject (hdcMem, hFont) ; GetTextExtentPoint32 (hdcMem, szFaceName[i], lstrlen (szFaceNameCi]), &size); hBitmap = CreateBitmap (size.cx, size.cy. 1, 1, NULL) ; SelectObject (hdcMem, hBitmap) ; I TextOut (hdcMem, 0, 0, szFaceNameCi], lstrlen (szFaceNameCi])) ; 4 Delete0bject (SelectObject (hdcMem, hFont)) ; DeleteDC (hdcMem) ; DeleteDC (hdc) ; return hBitmap ; i /* DeleteAllBitmaps: Usuwa wszystkie bitmapy z menu */ void DeleteAllBitmaps (HWND hwnd) HMENU hMenu ; int i ; MENUITEMINFO mii = ( sizeof (MENUITEMINFO), MIIMţSUBMENU ţ MIIM TYPE ) ; // Usuń bitmapę Help z menu systemowego hMenu = GetSystemMenu (hwnd, FALSE); GetMenuItemInfo (hMenu, IDM_HELP, FALSE, &mii) ; Delete0bject ((HBITMAP) mii.dwTypeData) ; // Usuń bitmapy najwyższego poziomu menu hMenu = GetMenu (hwnd) ; 628 Częœć 11: Grafika (cišg dalszy ze strony 627) for (i = 0 ; i < 3 ; i++) ( GetMenuItemInfo (hMenu, i, TRUE, &mii) ; Delete0bject ((HBITMAP) mii.dwTypeData) ; ) // Usuń bitmapowe elementy menu Font hMenu = mii.hSubMenu ;, for (i = 0 ; i < 3 ; i++) f GetMenuItemInfo (hMenu. i, TRUE, &mii) ; Delete0bject ((HBITMAP) mii.dwTypeData) ; ) GRAFMENU.RC (fragmenty) // Microsoft Developer Studio generated resource script. ţlinclude "resource.h" ţlinclude "afxres.h" ///////////////////////////////////////////////////////////////////// // Menu MENUFILE MENU DISCARDABLE BEGIN MENUITEM "&New", IDM_FILE_NEW MENUITEM "&Open... . IDM_FILE_OPEN MENUITEM "&Save". IDM_FILE_SAVE MENUITEM "Save &As... , IDMţFILE SAVE AS END MENUEDIT MENU DISCARDABLE BEGIN MENUITEM "&Undo", IDMţEDIT UNDO MENUITEM SEPARATOR MENUITEM "Cu&t", IDM_EDIT_CUT MENUITEM "&Copy", IDM_EDIT_COPY MENUITEM "&Paste". IDM_EDIT_PASTE MENUITEM "De&lete", IDMţEDIT CLEAR END ///////////////////////////////////////////////////////////////////// // Bitmap BITMAPFONT BITMAP DISCARDABLE "Fontlabl.bmp" BITMAPHELP BITMAP DISCARDABLE "Bighelp.bmp" BITMAPEDIT BITMAP DISCARDABLE "Editlabl.bmp" BITMAPFILE BITMAP DISCARDABLE "Filelabl.bmp" Rozdział 14: Bitmapy i BitBlty 629 RESOURCE.H (fragmenty) // Microsoft Developer Studio generated include file. // Used by GrafMenu.rc ţţdefine IDM_FONT_COUR 101 4ţdefine IDMţFONT_ARIAL 102 ţţdefine IDM FONT_TIMES 103 ţţdefine IDM_HELP 104 4idefine IDM_EDIT_UNDO 40005 ţţdefine IDM_EDIT_CUT 40006 ţţdefine IDM_EDIT_COPY 40007 ţţdefine IDM_EDIT_PASTE 40008 ţţdefine IDM_EDIT_CLEAR 40009 ţţdefine IDM_FILE_NEW 40010 ţţdefine IDM_FILE_OPEN 40011 ţţdefine IDM FILE_SAVE 40012 4ţdefine IDM FILE SAVE AS 40013 Rysunek 14-16. Program GRAFMENU EDITLABL.BMP a r -t r t. R , . , :-...t....: FILELABL.BMP ,..1 : „ t f 4 „ t ţ œ q P L b d t . .,,....g..,ţţ..... ........ o....,.....,.,.m,..,.........a.,.... ..,..:.."....,..,.:...,x....ţ... FONTLABL.BMP v d.t.q.q. `1'e"ţ t Y r ţ i:tř: .b,e,. k ţ -C œ. T g , . .. .,.: ....,.,.......................,.....:.. ,..... 630 Częœć II: Grafika BIGHELP.BMP Chcšc dodać bitmapę do menu, użyj funkcji AppendMenu lub InsertMenu. Bitma- pa może pochodzić z dwóch Ÿródeł. Możesz utworzyć bitmapę w Visual C++ Developer Studio, dołšczyć plik bitmapy do swojego skryptu zasobów, a w pro- gramie wczytania bitmapy do pamięci użyć funkcji LoadBitmap. Potem należy D, wywołać AppendMenu lub InsertMenu, żeby dołšczyć jš do menu. Jednak z takim rozwišzaniem wišże się pewien kłopot. Nie do każdego typu rozdzielczoœci mo- DE nitora i współczynnika kształtu obrazu bitmapa będzie pasowała. Prawdopodob- D E nie będziesz chciał rozcišgnšć wczytanš bitmapę w odpowiedni sposób. Alter- natywš jest utworzenie jej w samym programie, wybranie w kontekœcie urzšdze- nia pamięciowego, narysowanie na niej tego, co trzeba, i dołšczenie do menu. Funkcja GetBitmapFont w programie GRAFMENU pobiera parametry 0, 1 lub 2 oraz zwraca uchwyt do bitmapy. Ta bitmapa zawiera tekst "Courier New" albo "Arial", albo "Times New Roman", pisany odpowiedniš czcionkš, mniej więcej dwa razy większš od normalnej czcionki systemowej. Zobaczmy, jak funkcja Get- BitmapFont to robi. (Kod podany poniżej nie jest dokładnie taki sam, jak w pliku GRAFMENU.C. Dla zwiększenia czytelnoœci zamieniłem referencje do tablicy szFaceName na wartoœci odpowiadajšce czcionce Arial). Pierwszš rzeczš, którš trzeba zrobić, jest okreœlenie rozmiaru czcionki systemo- wej za pomocš struktury TEXTMETRIC i utworzenie kontekstu urzšdzenia pa- Get mięciowego kompatybilnego z ekranem: hdc hdc = CreateIC (TEXT ("DISPLAY"), NULL, NULL, NULL) ; hdc GetTextMetrics (hdc, &tm) ; Del hdcMem = CreateCompatibleDC (hdc) ; Funkcja CreateFont tworzy czcionkę logicznš, która jest dwa razy wyższa od sys- temowej i wyglšda jak czcionka Arial: Geti hFont = CreateFont (2 * tm.tmHeight, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, TEXT ("Arial")) ; Ta czcionka jest wybierana w kontekœcie urzšdzenia pamięciowego i zapisywa- ny jest jej domyœlny uchwyt. Rozdział 14: Bitmapy i BitBlty 631 hFont = (HFONT) SelectObject (hdcMem, hFont) ; Teraz, gdy wpisaliœmy tekst do kontekstu urzšdzenia pamięciowego, system Windows może wykorzystać czcionkę Arial TrueType, zaznaczonš w kontekœcie urzšdzenia. Jednak kontekst urzšdzenia pamięciowego ma poczštkowo postać jednopikselo- wego urzšdzenia monochromatycznego. Musimy więc utworzyć bitmapę odpo- wiednio dużš dla tekstu, który chcemy w niej wyœwietlić. Wymiary tekstu moż- na uzyskać za pomocš funkcji GetTextExtentPoint32, a potem na ich podstawie utworzyć bitmapę za pomocš CreateBitmap: GetTextExtentPoint32 (hdcMem, TEXT ("Arial"), 5, &size) ; hBitmap = CreateBitmap (size.cx, size.cy, 1, 1, NULL) ; SelectObject (hdcMem, hBitmap) ; Ten kontekst urzšdzenia ma monochromatycznš powierzchnię wyœwietlania, o rozmiarze dokładnie takim samym jak tekst. Teraz musimy jeszcze wpisać tam tekst: TextOut (hdcMem, 0, 0, TEXT ("Arial"), 5) ; Już prawie skończone, pozostaje tylko wyczyœcić to, co trzeba. W tym celu za po- mocš SelectObject ponownie wybieramy w kontekœcie urzšdzenia czcionkę syste- mowš (z uchwytem hFont) i usuwamy jej poprzedni uchwyt, który zwraca funk- cja SelectObject, czyli uchwyt czcionki Arial: Delete0bject (SelectObject (hdcMem, hFont)) ; Teraz możemy także usunšć dwa konteksty urzšdzeń: DeleteDC (hdcMem) ; DeleteDC (hdc) ; Pozostała nam bitmapa zawierajšca napis "Arial", pisany czcionkš Arial. Kontekst urzšdzenia pamięciowego jest pomocny również wtedy, gdy potrzebu- jesz przeskalować czcionki, dostosowujšc je do innej rozdzielczoœci wyœwietla- nia lub współczynnika kształtu obrazu. Cztery bitmapy używane w GRAFME- NLJ skonstruowałem tak, aby były poprawne dla monitora, dla którego wyso- koœć czcionki systemowej wynosi 8 pikseli, a szerokoœć równa jest 4 pikselom. Dla innych wymiarów czcionek systemowych bitmapa musi zostać rozcišgnięta. W GIZAFMENL1 robi się to za pomocš funkcji StretchBitmap. Pierwszym krokiem jest uzyskanie kontekstu dla ekranu, znalezienie wymiarów czcionki systemowej i utworzenie dwóch kontekstów urzšdzeń pamięciowych: hdc = CreateIC (TEXT ("DISPLAY"), NULL, NULL, NULL) ; GetTextMetrics (hdc, &tm) ; hdcMeml = CreateCompatibleDC (hdc) ; hdcMem2 = CreateCompatibleDC (hdc) ; DeleteDC (hdc) : Uchwytem bitmapy przekazywanym do funkcji jest hBitmapl. Program może uzyskać wymiary tej bitmapy korzystajšc z funkcji GetObject: GetObject (hBitmapl, sizeof (BITMAP), (PSTR) &bml) ; Ta instrukcja kopiuje wymiary do struktury bm1 typu BITMAP. Strukturze bm2 przypisywana jest bml, a następnie poszczególne pola modyfikowane sš na pod- stawie wielkoœci czcionki systemowej: 632 Częœć Ii: Grafika bm2 = bml ; bm2.bmWidth = (tm.tmAveCharWidth * bm2.bmWidth) / 4 ; bm2.bmHeight = (tm.tmHeight * bm2.bmHeight) / 8 ; bm2.bmWidthBytes = (.(bm2.bmWidth + 15) / 16) * 2 ; Potem na podstawie tych zmienionych wymiarów może zostać utworzona nowa bitmapa z uchwytem hBitmap2: hBitmap2 = CreateBitmapIndirect (&bm2) ; Wtedy będzie można wybrać te dwie bitmapy w dwóch kontekstach urzšdzeń pamięciowych: SelectObject (hdcMeml, hBitmapl) ; SelectObject (hdcMem2, hBitmap2) ; hE Chcemy skopiować pierwszš bitmapę do drugiej, rozcišgajšc jš w trakcie tego procesu. To wymaga jednego wywołania StretchBlt : qp StretchBlt (hdcMem2, 0, 0, bm2.bmWidth, bm2.bmHeight, Ap hdcMeml, 0, 0, bml.bmWidth, bml.bmHeight, SRCCOPY) ; Teraz druga bitmapa zawiera poprawnie przeskalowanš bitmapę. Jej to właœnie użyjemy w menu. Jak się zaraz przekonasz, posprzštać po tym wszystkim jest nietrudno: DeleteDC (hdcMeml) ; , DeleteDC (hdcMem2) ; Delete0bject (hBitmapl) ; Funkcja CreateMyMenu z programu GRAFMENU wykorzystuje do skonstruowa- nia menu funkcje StretchBitmap oraz GetBitmapFont. Program GRAFMENU ma już dwa menu zdefiniowane w skrypcie zasobu. Będš to rozwinięcia menu File oraz Edit. Funkcja rozpoczyna od uzyskania uchwytu pustego menu: hMenu = CreateMenu () ; Rozwinięcie menu File (zawierajšcego cztery opcje: New, Open, Save oraz Save As) jest wczytywšpe ze skryptu zasobu: hMenuPopup = LoadMenu (hInstance, TEXT ("MenuFile")) ; Bitmapa ze słowem "FILE" jest wczytywana także ze skryptu zasobu i rozcišga- na za pomocš funkcji StretchBitmap: hBitmapFile = StretchBitmap (LoadBitmap (hInstance, TEXT ("BitmapFile"))) ; Uchwyt bitmapy oraz uchwyt rozwinięcia menu stajš się argumentami w wywo- łaniu funkcji AppendMenu: AppendMenu (hMenu, MF BITMAP i MFţPOPUP, hMenuPopup, (PTSTR) (LONG) hBitmapFile) ; Ta sama procedura powtarza się dla menu Edit: hMenuPopup = LoadMenu (hInstance TEXT ("MenuEdit")) ; hBitmapEdit = StretchBitmap (LoadBitmap (hInstance, TEXT ("BitmapEdit"))) : AppendMenu (hMenu, MF BITMAP i MF POPUP, hMenuPopup, (PTSTR) (LONG) hBitmapEdit) ; Rozwinięcie menu z nazwami trzech czcionek jest konstruowane z wywołań funk- cji GetBitmapFont: hMenuPopup = CreateMenu () ; for(i=O;i<3;i++) Niep hBitmapPopFont Ci] = GetBitmapFont (i) ; AppendMenu (hMenuPopup, MFţBITMAP, IDM FONT COUR + i, (PTSTR) (LONG) hMenuPopupFont Ci]) ; Rozdział 14: Bitmapy i BitBlty 633 Następnie rozwinięcie menu jest dołšczane do menu: hBitmapFont = StretchBitmap (LoadBitmap (hInstance, "BitmapFont")) ; AppendMenu (hMenu, MF BITMAP i MFţPOPUP, hMenuPopup, (PTSTR) (LONG) hBitmapFont) ; Menu jest już gotowe, a WndProc czyni z niego menu okna przez wywołanie funkcji SetMenu. Program GRAFMENU modyfikuje także menu systemowe w funkcji AddHelpTo- Sys. Funkcja najpierw uzyskuje uchwyt menu systemowego: hMenu = GetSystemMenu (hwnd, FALSE) ; Wczytuje bitmapę ze słowem "Help" i rozcišga jš do odpowiedniego rozmiaru: h8itmapHelp = StretchBitmap (LoadBitmap (hInstance, TEXT ("BitmapHelp"))) ; Dodaje do menu systemowego linię rozdzielajšcš oraz rozcišgniętš bitmapę: AppendMenu (hMenu, MF_SEPARATOR, 0, NULL) ; AppendMenu (hMenu, MFţBITMAP, IDM HELP, (PTSTR)(LONG) hBitmapHelp) ; Zanim program GRAFMENU zakończy swoje działanie, poœwięca całš funkcję na wyczyszczenie i usunięcie wszystkich bitmap. Oto parę uwag dotyczšcych użycia bitmap w menu. Windows dopasowuje wysokoœć paska menu w taki sposób, by na najwyższym poziomie menu pomieœciło najwyższš z bitmap. Inne bitmapy (lub łańcuchy zna- kowe) wyrównywane sš do górnej krawędzi menu. Po umieszczeniu bitmap w menu najwyższego poziomu rozmiar paska menu uzyskiwany z GetSystemMe- trics ze stałš SM CYMENU traci swš aktualnoœć. Bawišc się programem GRAFMENU spostrzeżesz, że w rozwinięciach bitmapo- wych elementów menu można używać znaków wyboru, ale każdy z nich ma normalnš wielkoœć. Jeœli ci się to nie podoba, możesz utworzyć dostosowany od- powiednio znak wyboru i użyć funkcji SetMenuItemBitmaps. Innym podejœciem do "nietekstu" zastosowanego w menu (lub tekstu, lecz o czcionce różnej od systemowej) jest samodzielnie narysowane menu. Interfejs klawiatury dla takiego menu stanowi osobny problem. Gdy menu za- wiera tekst, Windows automatycznie wyposaża go w interfejs klawiatury. Ele- ment menu możesz wybrać naciskajšc klawisz [Alt] w połšczeniu z literš lub łań- cuchem znakowym. Jednak gdy w menu umieœcisz bitmapę, tracisz ten interfejs klawiatury. Nawet jeœli bitmapa coœ mówi, Windows tego nie wie. Tu właœnie przydaje się komunikat WMţMENUCHAR. Windows przesyła go do twojej procedury okna, gdy przyciskasz [Alt] z klawiszem znaku nie odpowia- dajšcego elementowi menu. GRAFMENU musiałby przechwytywać komunika- ty WMţMENUCHAR i sprawdzać wartoœć wParam (kod ASCII naciœniętego kla- wisza). Jeœli odpowiada on elementowi menu, powinien zwracać do Windows dwusłowo, którego bardziej znaczšca częœć ma wartoœć 2, a mniej znaczšca jest ustawiona na indeks elementu menu, który ma być powišzany z tym klawiszem. Resztę zrobi Windows. Nieprostokštne obrazy bitmapowe Bitmapy sš zawsze prostokštne, ale przecież nie muszš być w taki sposób wy- œwietlane. Załóżmy, że masz prostokštny obraz bitmapowy i chciałbyœ go wyœwietlić jako elipsę. l 634 Częœć II: Grafika Na pierwszy rzut oka wydaje się to łatwe. Po prostu wczytujesz obraz do Visual C++ Developer Studio lub do programu Windows Paint (albo do jakiegoœ droż- szego) i rozpoczynasz zamalowywanie białym piórem zewnętrznej powierzchni wokół obrazu. W wyniku tego zabiegu uzyskujesz eliptyczny obraz,a wszystko na zewnštrz elipsy pomalowane jest na biało. To zda egzamin, ale tylko wów- czas, gdy wyœwietlisz bitmapę na białym tle. Na tle innego koloru, uzyskasz elip- tyczny obraz na białym prostokšcie, znajdujšcym się na kolorowym tle. Nie jest więc dobrze... Istnieje bardzo rozpowszechniona technika, która służy do rozwišzywania takich problemów. Technika ta wymaga użycia bitmapy "maskownicy" oraz pewnych operacji rastrowych. Maskownica to monochromatyczna bitmapa o tych samych wymiarach co prostokštny obraz bitmapowy, który chcesz wyœwietlić. Każdy piksel maskownicy odpowiada jednemu pikselowi obrazu bitmapowego. Pikse- le maskownicy majš wartoœć 1 (biały) we wszystkich tych miejscach, w których ma być wyœwietlony oryginalny piksel bitmapy, oraz 0 (czarny) wszędzie tam, gdzie chcesz zachować docelowe tło. (Przy odpowiednich zmianach w zastoso- wanych operacjach rastrowych bitmapa maskownicy może być także odwrotna). Zobaczmy, jak to zadziała w rzeczywistoœci, w programie BITMASK, pokazanym na rysunku 14-17. BITMASK.C /* BITMASK.C - Demonstracja maskowania bitmapd (c) Charles Petzold. 1998 */ ţţinclude LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) static TCHAR szAppName C] = TEXT ("BitMask") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CS VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcan (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDCţARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (LTGRAYţBRUSH) wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) f Rozdział 14: Bitmapy i BitBlty 635 MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MBţICONERROR) ; return 0 ; 1 hwnd = CreateWindow (szAppName, TEXT ("Bitmap Masking Demo"), WS OVERLAPPEDWINDOW, !r',i.: CWţUSEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) ( i, ., i-, TranslateMessage (&msg) ; DispatchMessage (&msg) ; return msg.wParam ; i.: LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ( static HBITMAP hBitmapImag, hBitmapMask ; static HINSTANCE hInstance ; static int cxClient, cyClient, cxBitmap, cyBitmap ; BITMAP bitmap ; HDC hdc, hdcMemImag, hdcMemMask ; int x, y ; PAINTSTRUCT ps ; switch (message) ( case WM_CREATE: hInstance = ((LPCREATESTRUCT) lParam)->hInstance ; // Wczytaj oryginalny obraz i odczytaj jego wymiary hBitmapImag = LoadBitmap (hInstance, TEXT ("Matthew")) ; GetObject (hBitmapImag, sizeof (BITMAP), &bitmap) ; cxBitmap = bitmap.bmWidth ; cyBitmap = bitmap.bmHeight ; // Wybierz oryginalny obraz w kontekœcie urzšdzenia // pamięciowego hdcMemImag = CreateCompatibleDC (NULL) ; SelectObject (hdcMemImag, hBitmapImag) ; // Utwórz monochromatycznd bitmapę maskownicy // oraz kontekst urzšdzenia pamięciowego hBitmapMask = CreateBitmap (cxBitmap, cyBitmap, l, 1, NULL) ; hdcMemMask = CreateCompatibleDC (NULL) ; SelectObject (hdcMemMask, hBitmapMask) ; // Pomaluj maskownicę na czarno z białq elipsš na œrodku 636 Częœć II: Grafika ţ Rozc (cišg dalszy ze strony 635) B j 1 SelectObject (hdcMemMask, GetStockObject (BLACK BRUSH)) ; // M Rectangle (hdcMemMask, 0. 0, cxBitmap, cyBitmap) ; SelectObject (hdcMemMask, GetStockObject (WHITEţBRUSH)) ; Ellipse (hdcMemMask, 0, 0, cxBitmap, cyBitmap) ; Itinc' ţli nc // Nałóż maskę na oryginalny obraz ////i BitBlt (hdcMemImag, 0, 0, cxBitmap, cyBitmap, // Bi hdcMemMask, 0, 0, SRCAND) ; MATTH DeleteDC (hdcMemImag) ; DeleteDC (hdcMemMask) ; return 0 ; case WM_SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; // Wybierz bitmapy w kontekstach urzddzeń pamięciowych hdcMemlmag = CreateCompatibleDC (hdc) ; SelectObject (hdcMemImag, hBitmapImag) ; hdcMemMask = CreateCompatibleDC (hdc) ; ] SelectObject (hdcMemMask, hBitmapMask) ; // Wyœrodkuj obraz r x = (cxClient - cxBitmap) / 2 ; b y = (cyClient - cyBitmap) / 2 ; // Wykonaj transfer bloków bitowych Select0l BitBlt (hdc, x, y, cxBitmap, cyBitmap, hdcMemMask, 0, 0, 0x220326) ; Rectang' BitBlt (hdc, x, y, cxBitmap, cyBitmap, hdcMemImag, 0, 0, SRCPAINT) ; SelectOt Ellipse DeleteDC (hdcMemImag) ; P DeleteDC (hdcMemMask) ; EndPaint (hwnd, &ps) ; ' o return 0 ; Pć sk case WM_DESTROY: Delete0bject (hBitmapImag) ; BitBlt (i Delete0bject (hBitmapMask) ; PostOuitMessage (0) ; return 0 ; ) mi return DefWindowProc (hwnd, message, wParam, lParam) ; we cza Spć obr Rozdział 14: Bitmapy i BitBlty 637 BITMASK.RC // Microsoft Developer Studio generated resource script. itinclude "resource.h" itinclude "afxres.h" ///////////////////////////////////////////////////////////////////////////// // Bitmap MATTHEW BITMAP DISCARDABLE "matthew.bmp" Rysunek 14-17. Program BITMASK Plik MATTHHEW.BMP, do którego jest odwołanie w skrypcie zasobów, to zdy- gitalizowana czarno-biała fotografia mojego siostrzeńca. Ma ona szerokoœć 200 pikseli, wysokoœć 320 pikseli oraz głębię 8 bitów na piksel. Jednakże BITMASK jest napisany w taki sposób, że plik ten może być praktycznie dowolny. Zauważ, że BITMASK maluje tło swojego okna za pomocš pędzla jasnoszarego, abyœmy mieli pewnoœć, że właœciwie maskujemy bitmapę, a nie po prostu zama- lowujemy jej częœć na biało. Spójrzmy na przetwarzanie komunikatu WM CREATE. Program BITMASK wy- korzystuje funkcję LoadBitmap do uzyskania uchwytu oryginalnego obrazu - zmiennej hBitmapImag. Funkcja GetObject odczytuje szerokoœć i wysokoœć bitma- py. Potem w kontekœcie urzšdzenia pamięciowego, którego uchwyt jest na zmien- nej hdcMemlmag, wybierany jest uchwyt bitmapy. Następnie program tworzy monochromatycznš bitmapę o tym samym rozmia- rze co obraz pierwotny. Jej uchwyt jest zapisywany na zmiennej hBitmapMask i wy- bierany w kontekœcie urzšdzenia pamięciowego, którego uchwytem jest hdcMem- Mask. Bitmapa maskowrucy jest kolorowana w kontekœcie urzšdzenia pamięcio- wego za pomocš funkcji GDI w taki sposób, że tło jest czarne, elipsa zaœ biała. SelectObject (hdcMemMask, GetStockObject (BLACK BRUSH)) : Rectangle (hdcMemMask, 0, 0, cxBitmap, cyBitmap) : SelectObject (hdcMemMask, GetStockObject (WHITE BRUSH)) : Ellipse (hdcMemMask, 0, 0, cxBitmap, cyBitmap) : Ponieważ jest to bitmapa monochromatyczna, obszar czarny to naprawdę bity 0, obszar biały zaœ to naprawdę bity 1. PóŸniej wywołanie BitBlt zmienia obraz pierwotny przy wykorzystaniu tej ma- ski: BitBlt (hdcMemImag, 0, 0, cxBitmap, cyBitmap, hdcMemMask, 0, 0, SRCAND) ; Operacja rastrowa SRCAND dokonuje bit po bicie operacji AND pomiędzy bita- mi Ÿródła (bitmapy maskowrcy) oraz bitami docelowymi (obrazem oryginalnym). Wszędzie, gdzie bitmapa maskowrucy jest biała, zachowywane sš bity docelo- we. Tam, gdzie bitmapa maskownicy jest czarna, bity docelowe także stajš się czarne. Teraz na oryginalnym obrazie eliptyczny obszar jest otoczony czemiš. Spójrzmy z kolei na przetwarzanie WM PAINT. Zarówno zmieniona bitmapa obrazu, jak i bitmapa maskowrucy zostajš wybrane w kontekstach urzšdzeń pa- 638 Czţœć II: Grafika mięciowych. Dwa wywołania BitBlt robiš "czary mary". Pierwsze wykonuje Bit- Blt bitmapy maskownicy w oknie: BitBlt (hdc, x, y, cxBitmap, cyBitmap, hdcMemMask, 0, 0, 0x220326) ; Tu wykorzystywana jest operacja rastrowa nie majšca nazwy. Operacja logiczna to C & ~Z. Przypomnij sobie, że Ÿródło - bitmapa maskownicy - to biała elipsa (bity 1) okolona czerniš (bitami 0). Operacja rastrowa odwraca Ÿródło, tak że jest to czarna elipsa okolona bielš. Następnie operacja rastrowa AND dokonuje bit po bicie koniunkcji tego odwróconego Ÿródła i celu - powierzchni okna. Opera- cja AND punktów docelowych z bitami 1 pozostawia je bez zmian. Działanie AND z bitami 0 daje czarne bity docelowe. W taki sposób operacja BitBlt rysuje w oknie czarnš elipsę. Drugie wywołanie BitBlt rysuje na oknie bitmapę obrazu: BitBlt (hdc, x, y, cxBitmap, cyBitmap, hdcMemImag, 0, 0, SRCPAINT) ; Operacja rastrowa wykonuje bit-po-bicie działanie OR między Ÿródłem a celem. Zewnętrzna częœć bitmapy Ÿródłowej jest czarna, zatem cel pozostaje niezmie- niony. Wewnštrz elipsy miejsce docelowe jest czame, więc obraz jest kopiowany bez zmian. Efekt pokazany jest na rysunku 14-18. Rysunek 14-18. Ekran BITMASK Kilka uwag: Czasem może ci być potrzebna stosunkowo skomplikowana maskownica - na przykład taka, która wymazuje całe tło obrazu oryginalnego. Będziesz prawdo- podobnie zmuszony utworzyć jš ręcznie, za pomocš programu rysujšcego, i za- pisać w pliku. Jeœli piszesz aplikacje przeznaczone specjalnie dla Windows NT, możesz użyć funkcji MaskBlt i wykonać mniej więcej to, co program MASKBIT, ale używajšc mniej wywołań funkcji. W Windows NT mieœci się jeszcze inna funkcja, podobna do BitBlt, a niedostępna w Windows 98. Jest to funkcja PlgBlt (parallelogram blt), która pozwala obracać i pochylać obrazy bitmapowe. Rozdział 14: Bitmapy i BitBlty 639 Na koniec, jeœli uruchomiłeœ BIT'MASK na swoim własnym komputerze i zoba- czyłeœ jedynie kolory czarny, biały i kilka odcieni szaroœci, jest tak dlatego, że pracujesz w 16-kolorowym lub 256-kolorowym trybie graficznym. W przypadku trybu 16-kolorowego nie możesz zbyt wiele zrobić dla poprawienia sytuacji, lecz w przypadku aplikacji używajšcej 256 kolorów można zmieruć paletę kolorów tak, aby wyœwietlić odcienie szaroœci. Jak to zrobić, dowiesz się z rozdziału 16. Prosta animacja Ponieważ wyœwietlanie małych bitmap jest stosunkowo szybkie, możesz wyko- rzystać połšczenie bitmap z zegarem Windows i wykonać prostš animację. Nadszedł czas na odbijanie piłeczki. Program BOUNCE, przedstawiony na rysunku 14-19, tworzy piłkę, która odbija się w obszarze roboczym okna. Do taktowania piłki program wykorzystuje ze- gar. Piłka jako taka jest bitmapš. Program najpierw tworzy piłkę konstruujšc bit- mapę, wybierajšc jš w kontekœcie urzšdzenia pamięciowego, a następnie wyko- nuje proste wywołania funkcji GDI. Bitmapowš piłkę program rysuje na ekranie używajšc BitBlt z kontekstu urzšdzenia pamięciowego. BOUNCE.C i* BOUNCE.C - Program odbijajšcy pileczkę (c) Charles Petzold, 1998 iţinclude iţdefine ID TIMER 1 LRESULT CALLBACK WndProc (HWND, UINT, WPARAM. LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance. PSTR szCmdLine, int iCmdShow) static TCHAR szAppNameC] = TEXT ("Bounce") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CS VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC ARROW) : wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITEţBRUSH) wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!Re9isterClass (&wndclass)) f MessageBox (NULL, TEXT ("This program requires Windows NT!"). szAppName, MBţICONERROR) ; 640 Częœć II: Grafika Rozd (cišg dalszy ze strony 639) return 0 ; ) hwnd = CreateWindow (szAppName, TEXT ("Bouncing Ball"), WS_0llERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CWţUSEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) ( TranslateMessage (&msg) ; DispatchMessage (&msg) ; J return msg.wParam ; ) LRESULT CALLBACK WndProc (HWND hwnd, UINT iMsg, WPARAM wParam, LPARAM lParam) f static HBITMAP hBitmap ; static int cxClient, cyClient, xCenter, yCenter, cxTotal, cyTotal, cxRadius, cyRadius, cxMove, cyMove, xPixel, yPixel ; HBRUSH hBrush ; HDC hdc, hdcMem ; int iScale ; switch (iMsg) ( case WM_CREATE: hdc = GetDC (hwnd) ; xPixel = GetDeviceCaps (hdc, ASPECTX) ; yPixel = GetDeviceCaps (hdc,.ASPECTY) ; ReleaseDC (hwnd, hdc) ; SetTimer (hwnd, ID TIMER, 50, NULL) ; return 0 ; case WM_SIZE: xCenter = (cxClient = LOWORD (lParam)) / 2 ; yCenter = (cyClient = HIWORD (lParam)) / 2 ; iScale = min (cxClient * xPixel, cyClient * yPixel) / 16 ; cxRadius = iScale / xPixel ; cyRadius = iScale / yPixel ; cxMove = max (1, cxRadius / 2) ; cyMove = max (1, cyRadius / 2) ; 1 cxTotal = 2 * (cxRadius + cxMove) ; cyTotal = 2 * (cyRadius + cyMove) ; if (hBitmap) Delete0bject (hBitmap) ; Rozdział 14: Bitmapy i BitBity hdc = GetDC (hwnd) ; hdcMem = CreateCompatibleDC (hdc) ; hBitmap = CreateCompatibleBitmap (hdc, cxTotal, cyTotal) ; ReleaseDC (hwnd, hdc) ; SelectObject (hdcMem, hBitmap) ; Rectangle (hdcMem, -l, -1, cxTotal +1 cyTotal+1) ; hBrush = CreateHatchBrush (HSţDIAGCROSS, OL) ; SelectObject (hdcMem, hBrush) ; SetBkColor (hdcMem, RGB (255, 0, 255)) ; Ellipse (hdcMem, cxMove, cyMove, cxTotal - cxMove, cyTotal - cyMove) ; DeleteDC (hdcMem) ; Delete0bject (hBrush) ; return 0 ; case WM_TIMER: if (!hBitmap) break ; hdc = GetDC (hwnd) ; hdcMem = CreateCompatibleDC (hdc) ; SelectObject (hdcMem, hBitmap) ; BitBlt (hdc, xCenter - cxTotal / 2, yCenter - cyTotal / 2, cxTotal, cyTotal, hdcMem, 0, 0, SRCCOPY) ; ReleaseDC (hwnd, hdc) ; DeleteDC (hdcMem) ; xCenter += cxMove ; yCenter += cyMo.ve ; if ((xCenter + cxRadius >= cxClient) ţţ (xCenter - cxRadius <= 0)) cxMove = -cxMove ; if ((yCenter + cyRadius >= cyClient) ţţ (yCenter - cyRadius <= 0)) cyMove = -cyMove ; return 0 ; case WM_DESTROY: if (hBitmap) Delete0bject (hBitmap) ; KillTimer (hwnd, ID_TIMER) ; PostOuitMessage (0) ; return 0 : J return DefWindowProc (hwnd, iMsg, wParam, lParam) ; l Rysunek 14-19. Program BOUNCE Częœć II, Grafika ţ Roţ Program BOUNCE rekonstruuje piłkę za każdym razem, gdy otrzymuje komu- nikat WMţSIZE. Potrzebny jest w tym celu kontekst urzšdzenia pamięciowego kompatybilnego z monitorem: hdcMem = CreateCompatibleDC (hdc) ; Œrednica piłki ustawiana jest na jednš szesnastš wysokoœci lub szerokoœci obsza- ru roboczego okna, w zależnoœci od tego, który z tych wymiarów jest mniejszy. Jednak program buduje bitmapę dużo większš od piłki: z każdej strony bitmapa rozpoœciera się poza obszar wyznaczony wymiarami piłki o pół jej promienia: hBitmap = CreateCompatibleBitmap (hdc, cxTotal, cyTotal) ; Po wybraniu bitmapy w kontekœcie urzšdzenia pamięciowego całe jej tło malo- wane jest na biało: Rectangle (hdcMem, -l, -l, cxTotal + 1, cyTotal + 1) ; Te dziwne współrzędne powodujš, że obrys prostokšta znajduje się poza bitma- pš. W kontekœcie urzšdzenia pamięciowego wybierany jest pędzel w ukoœnš krat- kę, w centrum bitmapy rysowana piłka: Ellipse (hdcMem, cxMove, cyMove, cxTotal - cxMove, cyTotal - cyMove) ; Marginesy wokół piłki skutecznie pokrywajš poprzedni obraz piłki podczas jej ruchu. Ponowne narysowanie jej w innym położeniu wymaga jedynie prostego wywołania BitBlt z wartoœciš SRCCOPY kodu ROP: BitBlt (hdc, xCenter - cxTotal / 2, yCenter - cyTotal / 2, cxTotal, cyTotal, hdcMem, 0, 0, SRCCOPY) ; Program BOUNCE demonstruje najprostszy sposób poruszania obrazu po ekranie, ale w ogólnych zastosowarţiach to podejœcie nie jest zadowalajšce. Jeœli jesteœ zainte- resowany animacjš, powinieneœ przyjrzeć się niektórym z pozostałych kodów ROP (takim jak SRCINVERT), które na Ÿródle i celu przeprowadzajš alternatywę wyklu- czajšcš. Inne techruki animacji zwišzane sš z paletš Windows (i funkcjš AnimatePa- lette) oraz funkcjš CreateDIBSection. Przy bardziej skomplikowanych animacjach możesz być zmuszony porzucić GDI i zapoznać się z interfejsem DirectX. Bitmapy poza oknem Program SCRAMBLE przedstawiony na rysunku 14-20 jest bardzo prymitywny i prawdopodobnie nie powinienem ci go pokazywać. Jednak prezentuje kilka interesujšcych technik i wykorzystuje kontekst urzšdzenia pamięciowego jako miejsce do tymczasowego przechowywania przy operacjach BitBlt, które zamie- niajš zawartoœć par wyœwietlanych prostokštów. SCRAMBLE.C /* SCRAMBLE.C - Wymieszanie i uporzšdkowanie ekranu (c) Charles Petzold, 1998 */ itinclude ltdefine NUM 300 LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; Rozdział 14: Bitmapy i BitBlty 643 int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) ( static int iKeep CNUM]C4] ; HDC hdcScr, hdcMem ; int cx, cy ; HBiTMAP hBitmap ; HWND hwnd ; int i, j, xl, yl. x2, y2 ; I if (LockWindowUpdate (hwnd = GetDesktopWindow ())) _ hdcScr = GetDCEx (hwnd, NULL, DCX CACHE ţ DCXţLOCKWINDOWUPDATE) ; hdcMem = CreateCompatibleDC (hdcScr> : cx = GetSystemMetrics (SM CXSCREEN) / 10 ; cy = GetSystemMetrics (SM CYSCREEN) / 10 ; hBitmap = CreateCompatibleBitmap (hdcScr, cx, cy) ; SelectObject (hdcMem, hBitmap) ; srand ((int) GetCurrentTime ()) : for (i = 0 ; i < 2 , i++) for (j = 0 ; j < NUM ; j++) if (i == 0) ř iKeep Cj7 CO] = xl = cx * (rand () % 10) ; iKeep Cj7 C17 = yl = cy * (rand () ţ 10) ; iKeep Cj7 C27 = x2 = cx * (rand () % 10) ; iKeep Cj7 C37 = y2 = cy * (rand () % 10) ; else i p - j7 C07 : xl = iKee CNUM - 1 yl = iKeep CNUM - 1 - j7 C17 : x2 = iKeep CNUM - 1 - j7 C27 : ł y2 = iKeep CNUM - 1 - j7 C37 : ) r.: BitBlt (hdcMem, 0, 0, cx, cy, hdcScr, xl, yl, SRCCOPY) ; BitBlt (hdcScr, xl, yl, cx, cy, hdcScr, x2, y2, SRCCOPY) ; BitBlt (hdcScr, x2, y2, cx, cy, hdcMem, 0, 0, SRCCOPY) : Sleep (10) : ) DeleteDC (hdcMem) ; ReleaseDC (hwnd, hdcScr) ; Delete0bject (hBitmap) ; LockWindowUpdate (NULL) : 1 , return FALSE ; Rysunek 14-20. Program SCRAMBLE Częœć IIţ Grafika Program SCRAMBLE nie ma procedury okna. Najpierw w procedurze WinMain wywołuje LockWindowUpdate z uchwytem okna pulpitu. Funkcja ta zapobiega uaktualnieniu ekranu przez jakikolwiek inny program. Następnie SCIZAMBLE uzyskuje kontekst urzšdzenia całego ekranu, wywołujšc GetDCEx z argumentem DCX LOCKWINDOWUPDATE. To pozwala programowi SCRAMBLE pisać po ekranie w sytuacji, gdy żaden inny program nie może tego robić. Następnie SCIZAMBLE okreœla wymiary całego ekranu i dzieli je przez 10. Pro- gram używa tych wymiarów (o nazwach cx i cy) do utworzenia bitmapy, a na- stępnie wybiera tę bitmapę w kontekœcie urzšdzenia pamięciowego. Korzystajšc z funkcji rand języka C, SCRAMBLE oblicza cztery wartoœci losowe (współrzędne dwóch punktów), które sš wielokrotnoœciami wartoœci cx i cy. Dwa prostokštne bloki ekranu program zamienia miejscami, używajšc trzech funkcji BitBlt. Pierwsza kopiuje do kontekstu urzšdzenia pamięciowego prostokšt roz- poczynajšcy się w pierwszym punkcie. Druga BitBlt kopiuje prostokšt rozpoczy- najšcy się w drugim punkcie w miejsce prostokšta rozpoczynajšcego się w pierw- szym. Trzeci kopiuje prostokšt z kontekstu urzšdzenia pamięciowego do obsza- ru rozpoczynajšcego się w drugim punkcie. Proces ten zamienia miejscami za- wartoœć dwóch prostokštów na ekranie. SCRAMBLE wykonuje tę operację 300 razy, po czym ekran powinien być już całkowicie wymieszany. Nie bój się jed- nak, SCRAMBLE œledzi cały ten bałagan, a potem porzšdkuje ekran, przywraca- jšc jego normalny wyglšd (oraz odblokowuje go) przed zakończeniem działania. Konteksty urzšdzeń pamięciowych możesz także wykorzystywać do kopiowa- nia zawartoœci jednej bitmapy na innš. Załóżmy, że chcesz utworzyć bitmapę zawierajšcš tylko lewš górnš ćwiartkę innej bitmapy. Jeœli oryginalna bitmapa miała uchwyt hBitmap, możesz skopiować jej wymiary do struktury typu BITMAP: GetObject (hBitmap, sizeof (BITMAP), &bm) ; i utworzyć nowš, niezainicjowanš bitmapę wielkoœci jednej czwartej rozmiaru pierwotnego: hBitmap2 = CreateBitmap (bm.bmWidth / 2, bm.bmHeight / 2, bm.bmPlanes, bm.bmBitsPixel, NULL) ; Teraz utwórz dwa konteksty urzšdzeń pamięciowych i wybierz w nich oryginal- nš oraz nowš bitmapę: hdcMeml = CreateCompatibleDC (hdc) ; hdcMem2 = CreateCompatibleDC (hdc) : SelectObject (hdcMeml, hBitmap) ; SelectObject (hdcMem2, hBitmap2) ; Na koniec skopiuj lewš górnš ćwiartkę pierwszej bitmapy do drugiej: BitBlt (hdcMem2, 0, 0, bm.bmWidth / 2, bm.bmHeight / 2, hdcMeml, 0, 0, SRCCOPY) ; No i skończyłeœ. Trzeba jeszcze tylko posprzštać po sobie: DeleteDC (hdcMeml) ; DeleteDC (hdcMem2) ; Delete0bject (hBitmap) ; Rozdział 14: Bitmapy i BitBlty 645 , ł Program BLOWUP.C, zaprezentowany na rysunku 14-21, także wykorzystuje zablokowanie możliwoœci uaktualniania okna, żeby wyœwietlić prostokšt œcišgnię- ty z ekranu spoza obszaru jego okna. Program ten pozwala przy użyciu myszy œcišgnšć dowolny prostokštny obszar z ekranu. Następnie BLOWUP kopiuje za- wartoœć tego prostokštnego obszaru do bitmapy. W czasie przetwarzania komu- nikatu WM PAINT bitmapa kopiowana jest do obszaru roboczego okna progra- mu i, jeœli jest to konieczne, rozcišgana lub zmniejszana (patrz rysunek 14-22). BLOWUP.C /* BLOWUP.C - Program ze szklem powiększajacym (c) Charles Petzold, 1998 */ ţţinclude ţţinclude // z powodu definicji funkcji abs ţţinclude "resource.h" LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) static TCHAR szAppName [] = TEXT ("Blowup") : HACCEL hAccel ; HWND hwnd : MSG ms9 ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CS VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 : wndclass.hInstance = hInstance : wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDCţARROW) : wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITEţBRUSH) wndclass.lpszMenuName = szAppName ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) ( MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MBţICONERROR) ; return 0 ; hwnd = CreateWindow (szAppName, TEXT ("Blow-Up Mouse Demo"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; Częœć II: Grafika (cišg dalszy ze strony 645) hAccel = LoadAccelerators (hInstance, szAppName) ; while (GetMessage (&msg, NULL, 0, 0)) ( if (!TranslateAccelerator (hwnd, hAccel, &msg)) ( TranslateMessage (&msg) ; DispatchMessage (&msg) ; 1 ) return msg.wParam ; ) void InvertBlock (HWND hwndScr, HWND hwnd, POINT ptBeg, POINT ptEnd) ( HDC hdc ; hdc = GetDCEx (hwndScr, NULL, DCXţCACHE ţ DCXţLOCKWINDOWUPDATE) ; ClientToScreen (hwnd, &ptBeg) ; ClientToScreen (hwnd, &ptEnd) ; PatBlt (hdc, ptBe9.x, ptBeg.y, ptEnd.x - ptBeg.x, ptEnd.y - ptBeg.y, DSTINVERT) ; ReleaseDC (hwndScr, hdc) ; 1 HBITMAP CopyBitmap (HBITMAP hBitmapSrc) ( BITMAP bitmap ; HBITMAP hBitmapDst ; HDC hdcSrc, hdcDst ; GetObject (hBitmapSrc, sizeof (BITMAP), &bitmap) ; hBitmapDst = CreateBitmapIndirect (&bitmap) ; hdcSrc = CreateCompatibleDC (NULL) ; hdcDst = CreateCompatibleDC (NULL) ; SelectObject (hdcSrc, hBitmapSrc) ; SelectObject (hdcDst, hBitmapDst) ; BitBlt (hdcDst, 0, 0, bitmap.bmWidth, bitmap.bmHeight, hdcSrc, 0, 0, SRCCOPY) ; DeleteDC (hdcSrc) ; DeleteDC (hdcDst) ; return hBitmapDst ; ) LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) f static BOOL bCapturing, bBlocking ; static HBITMAP hBitmap ; static HWND hwndScr ; static POINT ptBeg, ptEnd ; Rozdział 14: Bitmapy i BitBlty 647 BITMAP bm ; HBITMAP hBitmapClip ; HDC hdc, hdcMem ; int iEnable ; PAINTSTRUCT ps ; RECT rect ; switch (messa9e) ( case WM_LBUTTONDOWN: if (!bCapturing) ( if (LockWindowUpdate (hwndScr = GetDesktopWindow ())) ( bCapturing = TRUE ; SetCapture (hwnd) ; SetCursor (LoadCursor (NULL, IDC CROSS)) ; ) else 1 return 0 ; MessageBeep (0) ; case WM_RBUTTONDOWN: if (bCapturing) ( bBlocking = TRUE ; ptBeg.x = LOWORD (lParam) ; ptBeg.y = HIWORD (lParam) ; ptEnd = ptBeg ; InvertBlock (hwndScr, hwnd, ptBeg, ptEnd) ; 1 return 0 ; case WM_MOUSEMOUE: if (bBlocking) ( InvertBlock (hwndScr, hwnd, ptBeg, ptEnd) ; ptEnd.x = LOWORD (lParam) ; ptEnd.y = HIWORD (lParam) ; InvertBlock (hwndScr, hwnd, ptBeg, ptEnd) ; 1 return 0 ; case WM_LBUTTONUP: case WM_RBUTTONUP: if (bBlocking) f InvertBlock (hwndScr, hwnd, ptBeg, ptEnd) ; ptEnd.x = LOWORD (lParam) ; ptEnd.y = HIWORD (lParam) ; if (hBitmap) t Delete0bject (hBitmap) ; hBitmap = NULL ; ) Częœć II: Grafika (cišg dalszy ze strony 647) hdc = GetDC (hwnd) ; hdcMem = CreateCompatibleDC (hdc) ; hBitmap = CreateCompatibleBitmap (hdc, abs (ptEnd.x - ptBeg.x), abs (ptEnd.y - ptBeg.y)) ; SelectObject (hdcMem, hBitmap) ; StretchBlt (hdcMem, 0, 0, abs (ptEnd.x - ptBeg.x), abs (ptEnd.y - ptBeg.y), hdc, ptBeg.x, ptBeg.y, ptEnd.x - ptBeg.x, ptEnd.y - ptBeg.y, SRCCOPY) ; DeleteDC (hdcMem) ; ReleaseDC (hwnd, hdc) ; InvalidateRect (hwnd, NULL, TRUE) ; ) if (bBlocking ţţ bCapturing) ( bBlocking = bCapturing = FALSE ; SetCursor (LoadCursor (NULL, IDC_ARROW)) ; ReleaseCapture () ; LockWindowUpdate (NULL) ; ) return 0 ; case WMţINITMENUPOPUP: iEnable = IsClipboardFormatAvailable (CF_BITMAP) ? MFţENABLED : MF GRAYED ; EnableMenuItem ((HMENU) wParam, IDM EDITţPASTE, iEnable) ; iEnable = hBitmap ? MF ENABLED : MF GRAYED ; EnableMenuItem ((HMENU) wParam, IDM_EDIT_CUT, iEnable) ; EnableMenuItem ((HMENU) wParam, IDM_EDITţCOPY, iEnable) ; EnableMenuItem ((HMENU) wParam, IDM EDIT DELETE, iEnable) ; return 0 ; case WM COMMAND: switch (LOWORD (wParam)) f case IDM_EDIT_CUT: case IDM_EDIT COPY: if (hBitmap) f hBitmapClip = CopyBitmap (hBitmap) ; OpenClipboard (hwnd) ; EmptyClipboard () ; SetClipboardData (CF BITMAP, hBitmapClip) ; ) if (LOWORD (wParam) = IDMţEDIT COPY) return 0 ; // Jeœli IDM_EDIT_CUT, to ten przypadek // zostawiamy nieopracowany 1 Rozdział 14: Bitmapy i BitBlty 649 case IDM_EDITţDELETE: if (hBitmap) ( Delete0bject (hBitmap) ; ' hBitmap = NULL ; 1 InvalidateRect (hwnd, NULL, TRUE) ; return 0 ; case IDM_EDIT_PASTE: if (hBitmap) ( Delete0bject (hBitmap) ; hBitmap = NULL ; OpenClipboard (hwnd) ; hBitmapClip = GetClipboardData (CF BITMAP) ; iv if (hBitmapClip) hBitmap = CopyBitmap (hBitmapClip) ; CloseClipboard () ; InvalidateRect (hwnd, NULL, TRUE) ; return 0 ; break ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; if (hBitmap) GetClientRect (hwnd, &rect) ; hdcMem = CreateCompatibleDC (hdc) ; SelectObject (hdcMem, hBitmap) ; GetObject (hBitmap, sizeof (BITMAP), (PSTR) &bm) ; SetStretchBltMode (hdc, COLORONCOLOR) ; StretchBlt (hdc, 0, 0, rect.right, rect.bottom, hdcMem, 0, 0, bm.bmWidth, bm.bmHeight, SRCCOPY) ; DeleteDC (hdcMem) ; IA EndPaint (hwnd, &ps) ; return 0 ; ,. case WM_DESTROY: if (hBitmap) Delete0bject (hBitmap) ; !a PostQuitMessage (0) ; return 0 ; 1 return DefWindowProc (hwnd, message, wParam, lParam) ; } 650 Częœć ilţ Grafika (cišg dalszy ze strony 649) BLOWUP.RC (fragmenty) // Microsoft Developer Studio generated resource script. ţlinclude "resource.h" ţlinclude "afxres.h" ///////////////////////////////////////////////////////////////////////////// // Menu BLOWUP MENU DISCARDABLE BEGIN POPUP "&Edit" BEGIN MENUITEM "Cu&t\tCtrl+Xř, IDM_EDIT_CUT MENUITEM "&Copy\tCtrl+C", IDM_EDIT_COPY MENUITEM "&Paste\tCtrl+V", IDM_EDIT_PASTE MENUITEM "De&lete\tDelete", IDM EDIT DELETE END END ///////////////////////////////////////////////////////////////////////////// // Accelerator BLOWUP ACCELERATORS DISCARDABLE BEGIN "C", IDM_EDIT_COPY, VIRTKEY, CONTROL, NOINVERT "V", IDM_EDIT_PASTE, VIRTKEY, CONTROL, NOINVERT VK_DELETE, IDM_EDIT_DELETE, VIRTKEY, NOINVERT IDMţEDIT CUT, VIRTKEY, CONTROL, NOINVERT END RESOURCE.H (fragmenty) // Microsoft Developer Studio generated include file. // Used by Blowup.rc lldefine IDM_EDITţCUT 40001 lldefine IDM_EDIT_COPY 40002 ţldefine IDM_EDIT_PASTE 40003 ţldefine IDM EDITţDELETE 40004 Rysunek 14-21. Program BLOWUP Rozdział 14: Bitmapy i BitBlty 651 Rysunek 14-22. Przykładowy ekran BLOWUP Z uwagi na ograniczenia zwišzane ze œcišganiem obrazu za pomocš myszy, uży- cie BLOWUP wydaje się poczštkowo nieco skomplikowane i przyzwyczajenie się do niego wymaga nieco czasu. Oto instrukcja obsługi programu: 1. Naciœnij lewy przycisk myszy w obszarze roboczym okna BLOWUP i przy- trzymaj go w tej pozycji. Kursor myszy zamieni się na krzyż nitkowy. 2. Przytrzymujšc naciœnięty lewy przycisk myszy, przesuń jej wskaŸnik w jakieœ inne miejsce na ekranie. Umieœć wskaŸnik myszy w górnym lewym rogu pro- stokštnego obszaru, który chcesz uchwycić. 3. Nadal przytrzymujšc naciœnięty lewy przycisk, naciœnij prawy przycisk my- szy i przecišgnij jš do prawego dolnego narożnika prostokštnego obszaru, który ma zostać œcišgnięty. Zwolnij oba przyciski myszy. (Kolejnoœć ich zwalniania nie ma znaczenia). WskaŸnik myszy ponownie zamieni się na strzałkę, zaznaczony blok zaœ zosta- nie skopiowany do obszaru roboczego okna BLOWUP i odpowiednio rozcišgnięty lub zmniejszony. Jeœli zaznaczysz prostokšt, przesuwajšc mysz od prawego górnego do lewego dol- nego rogu, BLOWUP wyœwietli obraz lustrzany. Jeżeli przesuniesz mysz od le- wego dolnego rogu do prawego górnego, BLOWUP wyœwietli obraz do góry no- gami. Ajeœli przesuniesz mysz z prawego dolnego rogu do górnego lewego, pro- gram połšczy te dwa efekty. BLOWUP pozwala także wkopiować bitmapę do Schowka oraz skopiować do- wolnš bitmapę ze Schowka do programu. By uaktywnić lub dezaktywować po- szczególne elementy menu Edit, BLOWUP przetwarza komunikat WM IMT'ME- NUPOPUP Aby obsłużyć te elementy menu, przetwarzany jest komunikat WM COMMAND. Struktura tego kodu powinna wydawać się znajoma, ponie- waż zasadniczo jest ona taka sama jak ta, którš pokazałem w rozdziale 12 przy okazji kopiowania i wklejania elementów tekstowych. 652 Częœć II: Grafika Jednak dla bitmap elementami Schowka nie sš uchwyty globalne, ale uchwyty bitmap. Gdy używasz CF BITMAP, funkcja GetClipboardData zwraca obiekt HBITMAP, a funkcja SetClipboardData przyjmuje obiekt HBITMAP. Jeœli chcesz dokonać transferu bitmapy do Schowka, ale zachować jej kopię, by używać jej w samym programie, musisz wykonać kopię bitmapy. Podobnie jeżeli wklejasz bit- mapę ze Schowka, także powinieneœ wykonać kopię. Funkcja CopyBitmap w BLO- WUP robi to, uzyskujšc strukturę BITMAP istniejšcej bitmapy i używajšc tej struk- tury w funkcji CreateBitmaplndirect do utworzenia nowej bitmapy. (Przyrostki Src i Dst nazw zmiennych pochodzš od (ang. source - Ÿródło i ang. destination - cel). Obie bitmapy wybierane sš w kontekstach urzšdzeń pamięciowych, a bity bit- map sš transferowane za pomocš wywołania BitBlt. (Chcšc skopiować bity, mo- żesz także alokować blok pamięci wielkoœci bitmapy i wywołać GetBitmapBits dla bitmapy Ÿródłowej oraz SetBitmapBits dla bitmapy docelowej). Doszedłem do wniosku, że BLOWUP jest bardzo przydatny do oglšdania z bli- ska licznych bitmap i obrazków porozrzucanych po 4Vindows i ich aplikacjach. Rozdział 15 B itm ' ' a a mezal zn e a , o u rz ze n W poprzednim rozdziale mogliœmy się przekonać, jak obiekt bitmapowy GDI (znany także jako bitmapa zależna od urzšdzeń, DDB) przydaje się przy realiza- cji wielu zadań programistycznych. Jednak nie pokazałem, jak zapisać te bitma- py na plikach dyskowych ani jak je ponownie wczytywać do pamięci. Jest to coœ, co robiono u zarania Windows, lecz czego nie czyni się dzisiaj. DDB nie jest od- powiednia do wymiany obrazów, ponieważ format bitów bitmapy w wysokim stopniu zależny jest od urzšdzeń. W DDB nie ma tabeli kolorów, która okreœlała- by zależnoœć między bitami bitmapy a kolorami. DDB ma sens tylko wówczas, gdy jest tworzona i niszczona w ramach jednej sesji Windows. Bitmapa niezależna od urzšdzeń (ang. device-independent bitmap - DIB) została wprowadzona w Windows 3.0, aby dostarczyć bardzo potrzebnego formatu pli- ków obrazu nadajšcego się do wymiany. Jak może wiesz, w Internecie znacznie powszechniej od DIB spotykane sš inne formaty plików obrazu, takie jak GIF lub JPEG. Dzieje się tak dlatego, że formaty GIF i JPEG implementujš schematy kom- presji, które znacznie zmniejszajš czas ładowania. Wprawdzie istnieje schemat kompresji zdefiniowany dla DIB, ale jest on rzadko używany. Bity w większoœci DIB nie sš skompresowane. Jeœli chcesz manipulować bitami bitmapy w swoim programie, okaże się to w rzeczywistoœci dużš zaletš. W odróżnieniu od plików GIF i JPEG, pliki DIB sš bezpoœrednio obsługiwane przez funkcje API Windows. Jeżeli masz w pamięci DIB, możesz podawać wskaŸniki do niej jako argumenty do wielu funkcji, które pozwalajš wyœwietlić DIB lub przekształcić jš do formatu DDB. Format pliku DIB Interesujšce, iż format DIB nie wywodzi się z Windows. Został on zdefiniowany po raz pierwszy w wersji 1.1 OS/2 - systemu operacyjnego pierwotnie opraco- wanego przez IBM i Microsoft w połowie lat osiemdziesištych. System ten zna- lazł się na rynku w 1988 roku i był pierwszš wersjš OS/2 wyposażonš w graficz- ny interfejs użytkownika podobny do Windows, a znany jako Presentation Ma- nager (PM). Presentation Manager zawierał Graphics Programming Interface (GPI), który definiował format bitmapy. 654 Częœć II: Grafika Ten sam format bitmapy co w OS/2 został następnie użyty w Windows 3.0 (wy- puszczonym na rynek w 1990 r.), gdzie zyskał nazwę DIB. Windows 3.0 zawierał także wariant oryginalnego formatu DIB, który miał stać się standardem w tym systemie. W Windows 95 (oraz Windows NT 4.0) i Windows 98 (oraz Windows NT 5.0) zostały zdefiniowane dodatkowe, udoskonalone wersje DIB. Omówię je w tym rozdziale. Na poczštek najlepiej zapoznać się z DIB jako z formatem plików. Pliki DIB majš rozszerzenie nazwy pliku .BMP lub, rzadziej, .DIB. Obrazy bitmapowe używane przez aplikacje Windows (na przykład grafika na przyciskach) sš tworzone jako pliki DIB i generalnie zapisywane jako zasoby tylko do odczytu w pliku wykony- walnym programu. Ikony i kursory myszy to także pliki DIB w nieco innej formie. Do cišgłego bloku pamięci program może wczytać plik DIB bez pierwszych 14 bajtów. Czasem o takim formacie bitmapy mówi się "upakowana DIB". Aplika- cje działajšce w systemie Windows mogš używać upakowanej DIB do wymiany obrazów za poœrednictwem Schowka Windows lub do tworzenia pędzli. Progra- my majš także pełen dostęp do zawartoœci DIB i mogš w dowolny sposób jš modyfikować. Programy mogš także tworzyć w pamięci swoje własne DIB, a następnie zapisy- wać je w plikach. Obrazy z tych DIB mogš być "malowane" przez program przy użyciu wywołań funkcji GDI. Program może również okreœlać wartoœć pikseli bšdŸ bezpoœrednio nimi manipulować i wykorzystać w tym procesie inne zapisane w pamięci DIB. Gdy DIB jest wczytywana do pamięci, programy mogš również używać jej da- nych za pomocš różnych wywołań funkcji API Windows, które omówię w tym rozdziale. Wywołania API zwišzane z DIB sš nieliczne i poœwięcone głównie wyœwietlaniu DIB na monitorze lub drukowaniu oraz przekształcaniu ich z i na obiekty bitmapowe GDI. Jednak gdy już wszystko zostanie powiedziane i zrobione, pozostanie jeszcze wiele, wiele zadań zwišzanych z DIB, z którymi aplikacje muszš sobie poradzić, a które nie sš rozwišzane w systemie operacyjnym Windows. Program może mieć na przykład dostęp do 24-bitowej DIB i chcieć jš przekształcić w 8-bitowš, z opty- malnš 256-kolorowš paletš. Windows nie zrobi tego za ciebie. Dlatego w tym rozdziale, a także w następnym pokażemy, co można zrobić z DIB poza funkcja- mi API Windows. DIB w stylu OSI2 Abyœmy już teraz nie uwikłali się w zbyt dużš liczbę szczegółów, zerknijmy na format DIB systemu Windows, który jest kompatybilny z formatem bitmapy wprowadzonym po raz pierwszy w OS/2 1.1. Plik DIB składa się z czterech głównych częœci: ů nagłówka pliku ů nagłówka informacyjnego ů tablicy kolorów RGB (jednak nie zawsze) ů bitów pikseli bitmapy. Rozdział 15: Bitmapa niezależna od urzšdzeń 655 O pierwszych dwóch z tych częœci możesz myœleć jak o strukturach danych C, o trzeciej zaœ - jak o tablicy struktur danych. Dokumentacja tych struktur znaj- duje się w pliku nagłówkowym Windows WINGDI.H. DIB. Upakowana DIB prze- chowywana w pamięci składa się z trzech częœci: ů nagłówka informacyjnego ů tablicy kolorów RGB (jednak nie zawsze) ů bitów pikseli bitmapy. Jest to dokładnie to samo, co w przypadku DIB zapisanej w pliku, z tš jednak różnicš, że nie ma nagłówka pliku. Plik DIB (ale nie ten przechowywany w pamięci upakowany format DIB) rozpo- czyna się od 14-bajtowego nagłówka pliku zdefiniowanego jako struktura. Oto ona: typedef struct tagBITMAPFILEHEADER // bmfh - naglówek pliku bitmapy ( WORD bfType ; // sygnatura "BM" lub 0x4D42 DWORD bfSize ; // całkowity rozmiar pliku WORD bfReservedl ; // musi być równy zeru WORD bfReserved2 ; // musi być równy zeru DWORD bfOffsetBits ; // odlegloœć do bitów pikseli w pliku DIB ) BITMAPFILEHEADER, * PBITMAPFILEHEADER ; Być może, nie jest to dokładnie taka sama struktura, jak ta zdefiniowana w pliku WINGDI.H (na przykład komentarze sš moje), ale działa tak samo. Pierwszy komentarz (to znaczy tekst "bmfh") sugeruje zalecany skrót, którego można użyć, nazywajšc strukturę tego typu danych. Jeœli w jednym z moich programów na- potkasz zmiennš o nazwie pbmfh, będzie to wskaŸnik do struktury typu BIT'MAP- FILEHEADER lub zmienna typu PBITMAPFILEHEADER. Struktura ma długoœć 14 bajtów. Rozpoczyna się od dwóch liter "BM", które majš informować, że jest to plik bitmapy. Odpowiada temu wartoœć WORD równa 0x4D42. Po sygnaturze "BM" następuje DWORD, okreœlajšce w bajtach rozmiar całego pliku wraz z nagłówkiem. Następnym dwóm polom WORD przypisano wartoœć zerowš. (W pliku kursora myszy, który pod względem formatu jest po- dobny do pliku DIB, te dwa pola używane sš do oznaczenia miejsca aktywnego kursora). Struktura kończy się DWORD, wskazujšcym w bajtach odległoœć do miejsca w pliku, gdzie rozpoczynajš się bity pikseli. Liczbę tę można odczytać z nagłówka informacyjnego DIB, lecz podawana jest tutaj dla wygody. W DIB w stylu OS/2, po strukturze BIT'MAPFILEHEADER następuje bezpoœred- nio struktura BIT'MAPCOREHEADER, dostarczajšca podstawowych informacji na temat obrazu DIB. Upakowany DIB rozpoczyna się od BTITVIAPCOREHEADER: typedef struct tagBITMAPCOREHEADER // bmch ( DWORD bcSize ; // rozmiar struktury = 12 WORD bcWidth ; // szerokoœć obrazu w pikselach WORD bcHeight ; // wysokoœć obrazu w pikselach WORD bcPlanes ; // równy 1 WORD bcBitCount ; // bitów na piksel (l, 4, 8, lub 24) 1 BITMAPCOREHEADER, * PBITMAPCOREHEADER ; 656 Częœć II: Grafika Słowo core (jšdro) brzmi w tym kontekœcie nieco dziwnie, i tak jest. Oznacza ono, iż format ten jest podstawš (stšd jšdro) innych formatów bitmap, z niego wywie- dzionych. Pole bcSize w BITMAPCOREHEADER podaje rozmiar struktury danych, w tym przypadku 12 bajtów. Pola bcWidth i bcHeight zawierajš rozmiar bitmapy w pikselach. Wprawdzie uży- cie typu WORD w tych polach sugeruje, że DIB może mieć 65 535 pikseli wyso- koœci i tyle samo szerokoœci, ale rzadko spotka się tak duże. Pole bcPlanes jest zawsze równe 1. Zawsze, zawsze, zawsze - od czasu, kiedy zostało zdefiniowane aż do teraz. Pole to jest pozostałoœciš po wczeœniejszym obiekcie bit- mapowym GDI Windows, z którym spotkaliœmy się w poprzednim rozdziale. Pole bcBitCount podaje liczbę bitów na jeden piksel. Dla DIB w stylu OS/2 może ono wynosić 1, 4, 8 lub 24. Liczba kolorów w obrazie DIB jest równa 26mch.bcBitCount lub w składni C 1 Ž bmch.bcBitCount Zatem pole bcBitCount jest równe: ů 1 dla 2-kolorowych DIB ů 4 dla 16-kolorowych DIB ů 8 dla 256-kolorowych DIB ů 24 dla pełnego zestawu barw DIB. Gdy mówię o 8-bitowej DIB, mam na myœli takš bitmapę DIB, która ma 8 bitów na każdy piksel. Dla pierwszych trzech przypadków (to znaczy dla liczby bitów równej 1, 4 i 8) po BITMAPCOREHEADER następuje tabela kolorów. Tabela kolorów nie istnieje dla DIB 24-bitowych. Tabela kolorów to tablica 3-bajtowych struktur RGBTRIPLE, po jednej dla każdego koloru obrazu: typedef struct ta9RGBTRIPLE // rgbt ( BYTE rgbtBlue ; // natężenie niebieskiego BYTE rgbtGreen ; // natężenie zielone9o BYTE rgbtRed ; // natężenie czerwonego 1 RGBTRIPLE : Zalecane jest organizowanie tablicy kolorów tak, aby najważniejsze kolory w DIB występowały jako pierwsze. Dlaczego, o tym dowiemy się z następnego rozdziału. Plik nagłówkowy WINGDI.H definiuje także następujšcš strukturę: typedef struct tagBITMAPCOREINFO // bmci ( BITMAPCOREHEADER bmciHeader ; // struktura nagłówka jddra RGBTRIPLE bmciColors[1] : // tablica kolorów l BITMAPCOREINFO, * PBITMAPCOREINFO ; Struktura ta łšczy nagłówek informacyjny z tablicš kolorów. Mimo że liczba struk- tur RGBTRIPLE w tej strukturze jest pozornie równa 1, nigdy nie napotkasz tyl- ko jednego RGBTRIPLE w pliku DIB. Rozmiar tablicy kolorów wynosi zawsze 2, Rozdziai 15: Bitmapa niezależna od urzšdzeń 657 16 lub 256 struktur RGBTRIPLE, zależnie od liczby bitów na piksel. Jeœli potrze- bujesz alokować strukturę PBITMAPCOREINFO dla 8-bitowej DIB, możesz to zrobić tak: pbmci = malloc (sizeof (BITMAPCOREINFO) + 255 * sizeof (RGBTRIPLE)) ; Jeżeli trzeba, możesz potem uzyskać dostęp do dowolnej struktury RGBTRIPLE w taki sposób: pbmci->bmciColorsCi] Ponieważ struktura RGBTRIPLE ma długoœć 3 bajtów, niektóre ze struktur RGB- TRIPLE w DIB będš zaczynać się od nieparzystych adresów. Jednak, ponieważ w pliku DIB występuje zawsze parzysta liczba struktur RGBTRIPLE, blok danych następujšcy po tablicy kolorów zawsze rozpoczyna się na granicy adresu WORD. Dane znajdujšce się po tablicy kolorów (a dla plików 24-bitowych DIB po nagłów- ku informacyjnym) to bity pikseli jako takie. Do góry nogami! Podobnie jak w większoœci formatów bitmap, piksele w DIB zorganizowane sš w poziome wiersze, niekiedy nazywane liniami skanowania, co wywodzi się z ter- minologii używanej dla monitorów ekranowych. Liczba wierszy równa jest polu bcHeight struktury BITMAPCOREHEADER. Jednak w odróżnieniu od większo- œci formatów bitmap, DIB rozpoczyna się od dolnego rogu obrazu i postępuje ku jego górze. Ustalmy zatem terminologię. Gdy mówię "górny wiersz" i "dolny wiersz", mam na myœli górny i dolny wiersz tego obrazu, który pojawia się w przypadku po- prawnego wyœwietlenia na monitorze lub wydrukowania na stronie. Na zwykłym portrecie na górze znajdujš się włosy, a na dole broda. Gdy mówię "pierwszy wiersz" mam na myœli wiersz pikseli, który znajduje się w pliku DIB bezpoœred- nio po tabeli kolorów. Natomiast gdy mówię "wiersz ostatni", mam na myœli wiersz pikseli na samym końcu pliku. Tak więc w DIB dolny wiersz obrazu to pierwszy wiersz pliku, górny zaœ wiersz obrazu to ostatni wiersz w pliku. Można powiedzieć, że jest to zorganizowane do góry nogami. Ponieważ taka orgaruzacja pliku jest sprzeczna ze zdrowym roz- sšdkiem, powstaje pytanie, dlaczego tak zrobiono. Trzeba tu powrócić do Presentation Managera z OS/2. Pewni ludzie w IBM zde- cydowali, iż wszystkie układy współrzędnych w PM - łšcznie z oknami, grafikš I'(., i bitmapami - powinny być spójne. To sprowokowało dyskusję: większoœć użyt- kowników, łšcznie z programistami pracujšcymi na pełnym ekranie w trybie tek- stowym lub w œrodowiskach okien, myœli pojęciami współrzędnych pionowych, które rosnš w miarę posuwania się w dół ekranu. Jednak twórcy grafiki kompu- terowej patrzš na ekran monitora z perspektywy matematycznej geometrii anali- tycznej. Traktujš go jak prostokštny (kartezjański) układ współrzędnych, w któ- rym współrzędne pionowe ţrosnš ku górze. Mówišc krótko, matematycy wygrali. Wszystko w PM, łšcznie ze współrzędny- mi okna, zostało umieszczone w układzie współrzędnych o poczštku w lewym dolnym rogu. I dlatego DIB jest taka, jaka jest. 658 Częœć II: Grafika Bity pikseli DIB Ostatnia sekcja pliku DIB - w większoœci przypadków największa jego częœć - składa się z właœciwych bitów pikseli DIB. Bity pikseli zorganizowane sš w po- ziome wiersze, poczynajšc od dolnego wiersza obrazu i dalej postępujšc ku gó- rze. Liczba wierszy w DIB jest równa polu bcHeight struktury BITMAPCOREHEADER. Każdy wiersz zawiera opis tylu pikseli, ile wynosi pole bcWidth struktury. Każdy wiersz rozpoczyna się od skrajnego lewego piksela i dalej postępuje po kolei w kie- runku prawej strony obrazu. Liczba bitów na piksel odczytywana jest z pola bcBit- Count i może wynosić 1, 4, 8 lub 24. Długoœć każdego wiersza, wyrażona w bajtach, jest zawsze wielokrotnoœciš 4. Dłu- goœć tę można obliczyć w następujšcy sposób: RowLength = 4 * ((bmch.bcWidth * bmch.bcBitCount + 31) / 32) : lub, nieco efektywniej w C: RowLength = ((bmch.bcWidth * bmch.bcBitCount + 31) & ~31) Ż 3 : Jeœli okaże się konieczne uzyskanie tej długoœci, wiersz dopełniany jest po pra- wej stronie (zwyczajowo zerami). Całkowita liczba bajtów danych pikseli równa jest wynikowi mnożenia RowLength i bmch.bcHeight. Chcšc zobaczyć, jak kodowane sš piksele, zbadajmy oddzielnie cztery przypad- ki. Na pokazanych poniżej diagramach bity każdego bajtu przedstawione sš w kwadratach i ponumerowane w taki sposób, że 7 oznacza bit najbardziej zna- czšcy, a 0-bit najmniej znaczšcy. Piksele zostały także ponumerowane poczšw- szy od 0 dla skrajnego lewego piksela w wierszu. Dla DIB o 1 bicie na piksel, każdy bajt odpowiada 8 pikselom. Skrajny lewy pik- sel jest najbardziej znaczšcym bitem pierwszego bajtu: Piksel: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 7 6 5 4,3 2,1,0, 7 6,5 4 3,2..1 0. 7,6,5 4 3 2 1 ţ... Każdy piksel może być 0 lub 1. Bit równy 0 oznacza, że kolor tego piksela jest zadany przez pierwszš wartoœć RGBTRIPLE w tablicy kolorów. Kolor piksela, którego bit jest równy 1, okreœla druga wartoœć w tablicy kolorów. W przypadku DIB o 4 bitach na piksel, każdy bajt odpowiada dwóm pikselom. Skrajny lewy piksel opisany jest w czterech bardziej znaczšcych bitach pierwsze- go bajtu i tak dalej: Piksel: -0- -1- -2- -3 4 5 7',.6'..5'..4 3,2 1,0 7,6,5,4 3,2 1 0 7 6 5 4 3 2 1 0 Wartoœć każdego 4-bitowego piksela należy do zakresu od 0 do 15. Wartoœć ta jest indeksem jednego z 16 elementów tablicy kolorów. Gdy DIB ma 8 bitów na piksel, każdy piksel zajmuje bajt: Piksel: -0- ţ 1 ţ . 2 7!6 5 4 3 2 1 0 7 6 5 4 3,2 1,0 7 6 5 4 3 2 1 0 Rozdział 15: Bitmapa niezależna od urzšdzeń 659 Wartoœć bajtu jest liczbš z zakresu od 0 do 255. I tym razem jest to indeks jednego z 256 elementów tablicy kolorów. Jeżeli DIB ma 24 bity na piksel, to każdy piksel wymaga 3 bajtów, po jednym dla czerwieni, zieleni i niebieskiego. Każdy wiersz pikseli to zasadniczo tablica struk- tur RGBTRIPLE, uzupełniona w miarę potrzeby zerowymi bajtami na końcu każ- dego wiersza, tak by wiersz składał się z wielokrotnoœci 4 bajtów: Piksel: - Niebieski- -Zielony - - Czerwony - 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1..0, 7 6..5'q 3 2 1,ţ, Powtórzmy jeszcze raz: DIB o 24 bitach na piksel nie ma tablicy kolorów. Rozszerzony format DIB Windows Teraz, gdy już opanowaliœmy format DIB kompatybilny z OS/2, wprowadzony w Windows 3.0, możemy spojrzeć na rozszerzonš wersję DIB, utworzonš w Win- dows w tym samym czasie. Ta postać DIB rozpoczyna się od struktury BITMAPFILEHEADER, tak jak we wczeœniejszym formacie, lecz potem zamiast struktury BITMAPCOREHEADER następuje struktura BITMAPINFOHEADER: typedef struct tagBITMAPINFOHEADER // bmih - nagłówek informacyjny bitmapy ( DWORD biSize ; // rozmiar struktury = 40 LONG biWidth ; // szerokoœć obrazu w pikselach LONG biHeight ; // wysokoœć obrazu w pikselach WORD biPlanes ; // = 1 WORD biBitCount ; // bitów na piksel (l, 4, 8, 16. 24 lub 32) DWORD biCompression ; // kod kompresji DWORD biSizeImage ; // liczba bajtów w obrazie LONG biXPelsPerMeter ; // rozdzielczoœć pozioma LONG biYPelsPerMeter ; // rozdzielczoœć pionowa DWORD biClrUsed ; // liczba użytych kolorów DWORD biClrImportant ; // liczba kolorów znaczšcych 1 BITMAPINFOHEADER, * PBITMAPINFOHEADER ; DIB kompatybilnš z OS/2 możesz odróżnić od DIB Windows sprawdzajšc pierw- sze pole struktury: wynosi ono 12 w pierwszym, a 40 w drugim przypadku. Jak widać, w strukturze tej jest szeœć dodatkowych pól. Jednak struktura BITMAP- INFOHEADER nie jest po prostu strukturš BITMAPCOREHEADER, do której dołšczono coœ na końcu. Przyjrzyj się uważnie: w strukturze BITMAPCOREHEA- DER pola bcWidth i bcHeight to 16-bitowe wartoœci WORD. W tej strukturze sš to 32-bitowe wartoœci LONG. Jest to niewielka, dokuczliwa zmiana, przez którš z pewnoœciš zejdziesz na manowce. Inna zmiana: dla 1 bitowych, 4-bitowych oraz 8-bitowych DIB używajšcych struktury BTTMAPINFOHEADER tablica kolorów nie jest tablicš struktur RGBTRIPLE. Zamiast niej po stxukturze BTTMAPINFOHEADER znajduje się tablica struktur RGBQUAD: typedef struct tagRGBOUAD // rgb ( BYTE rgbBlue ; // natężenie niebieskiego 660 Częœć II: Grafika BYTE rgbGreen ; // natężenie zielone9o BYTE rgbRed ; // natężenie czerwonego BYTE rgbReserved ; // = 0 ) RGBOUAD ; Jest ona podobna do struktury RGBTRIPLE, tyle, że zawiera czwarte pole, które zawsze ma wartoœć 0. Plik nagłówkowy WINGDI.H definiuje także następujšcš strukturę: typedef struct tagBITMAPINFO // bmi ( BITMAPINFOHEŽDER bmiHeader ; // struktura nagłówka informacyjnego RGBOUAD bmiColors[1] ; // tablica kolorów } BITMAPINFO, * PBITMAPINFO ; Zauważ, że tak jak struktura BITMAPINFO zaczyna się na granicy 32-bitowego adresu, także każda pozycja w tablicy RGBQUAD rozpoczyna się na granicy 32- bitowego adresu, ponieważ struktura BITMAPINFOHEADER ma 40 bajtów dłu- goœci. To zapewnia efektywniejsze adresowanie danych tabeli kolorów przez mikroprocesory 32-bitowe. Choć BTTMAPINFOHEADER została pierwotnie zdefiniowana dla Windows 3.0, niektóre z pól ponownie zdefiniowano w Windows 95 i Windows NT 4.0 i zmia- ny te przeniesiono do Windows 98 i Windows NT 5.0. Na przykład w aktualnej dokumentacji przeczytamy, że jeœli pole biHeight jest ujemne, to bitmapa jest "DIB z góry na dół" i jej poczštkiem jest lewy górny róg. Dobrze o tym wiedzieć. Było- by jeszcze lepiej, gdyby decyzję tę podjšł ktoœ w 1990 roku, kiedy ten format DIB był definiowany po raz pierwszy. Radzę unikać tworzenia DIB z góry na dół, bo w przeciwnym wypadku będziesz się modlił, żeby jakiœ program, napisany bez œwiadomoœci tej nowej "cechy", nie zawiesił się po napotkaniu ujemnej wartoœci pola biHeight. Natomiast programy takie jak Microsoft Photo Editor włšczony do Microsoft Word 97, po napotkaniu DIB z góry na dół będš podawały komunikat o nieprawidłowej wysokiœci obrazu - "Illegal image height" (mimo że Word 97 jako taki dobrze sobie z nimi radzi). Pole biPlanes niezmiennie wynosi 1, lecz pole biBitCount oprócz 1, 4, 8 lub 24 może teraz również równać się 16 lub 32. Była to również nowoœć w Windows 95 i Win- dows NT 4.0. Już wkrótce omówię, jak działajš te dodatkowe formaty. Pozwólcie, że chwilowo pominę pola biCompression i biSizelmage, które także wkrót- ce omówię. Pola biXPelsPerMeter i biYPeIsPerMeter okreœlajš sugerowany rzeczywisty rozmiar obrazu w dziwnych jednostkach, jakimi sš piksele na metr. (IBM chciał nazywać piksel "pel" - od picture element, czyli element obrazu). Wewnętrznie Windows nie używa tej informacji, ale aplikacja mogłaby wykorzystać jš do wyœwietlenia DIB we właœciwym rozmiarze. Pola te sš użyteczne również w przypadku DIB pochodzšcych z urzšdzeń nie majšcych kwadratowych pikseli. W przypadku większoœci DIB polom tym przypisana jest wartoœć 0, co oznacza, iż nie jest suge- rowany żaden wymiar rzeczywisty. Rozdzielczoœć 72 kropek na cal (używana nie- kiedy dla monitorów, choć rzeczywista rozdzielczoœć zależy od rozmiaru każde- go z nich) w przybliżeniu odpowiada 2835 pikselom na metr, a często spotykana rozdzielczoœć drukarek równa 300 dpi to 11811 pikseli na metr. Rozdział 15: Bitmapa niezależna od urzšdzeń 661 Pole biClrUsed jest bardzo ważne, ponieważ wpływa na liczbę pozycji w tablicy kolorów. Dla DIB 4-bitowych i 8-bitowych może ona znaczyć, że tablica kolorów zawiera mniej niż, odpowiednio, 16 lub 256 pozycji. Jest to jedna z metod zmniej- szenia rozmiaru DIB, choć nie o wiele. Załóżmy, że obraz DIB zawiera jedynie 64 odcienie szaroœci. Polu biCIrUsed nadawana jest wartoœć 64, a tablica kolorów za- wiera 64 struktury RGBQUAD, przy rozmiarze całej tablicy kolorów 256 bajtów. Wówczas wartoœci pikseli zmieniajš się od 0x00 do Ox3F. DIB nadal wymaga 1 bajtu na piksel, z tym że dwa górne bity dla każdego piksela wynoszš zero. Nadanie polu biClrUsed wartoœci 0 oznacza, że tablica kolorów zawiera pełnš liczbę pozy- cji, wynikajšcš z wartoœci pola biBitCount. Poczynajšc od Windows 95, pole biCirUsed może mieć wartoœć niezerowš dla DIB 16-bitowych, 24-bitowych lub 32-bitowych. W takim przypadku do interpretacji bitów pikseli nie jest używana przez Windows tablica kolorów. Pole biClrUsed wskazuje rozmiar tablicy kolorów w DIB, która mogłaby być wykorzystana przez programy do okreœlenia palety przy wyœwietleniu DIB na 256-kolorowych moni- torach. Jak pamiętasz, w formacie kompatybilnym z OS/2 24-bitowa DIB nie miała tablicy kolorów. Było tak również w rozszerzonym formacie wprowadzonym w Windows 3.0. Zmiana w Windows 95 powoduje, że 24-bitowa DIB może mieć tablicę kolorów, której wielkoœć wskazana jest przez pole biClrUsed. Podsumowujšc: ů Dla 1-bitowych DIB biClrUsed wynosi 0 lub 2, a tablica kolorów w tym przy- padku zawsze ma 2 pozycje. ů Dla 4-bitowych DIB, jeœli pole biClrUsed wynosi 0 lub 16, tablica kolorów za- wiera 16 pozycji. Natomiast, jeżeli jest to liczba od 2 do 15, wówczas wskazu- je ona liczbę pozycji w tablicy kolorów. Maksymalna wartoœć każdego piksela równa jest tej liczbie pomniejszonej o 1. ů Dla 8-bitowych DIB, jeœli pole biCIrUsed wynosi 0 lub 256, tablica kolorów za- wiera 256 pozycji. Natomiast jeœli jest to liczba od 2 do 255, wskazuje wów- czas liczbę pozycji w tablicy kolorów. Maksymalna wartoœć każdego piksela równa jest tej liczbie pomniejszonej o 1. ů Dla DIB 16-bitowych, 24-bitowych i 32-bitowych pole biClrUsed zwykle wy- nosi 0. Jeœli nie jest równe 0, to wskazuje liczbę pozycji w tablicy kolorów. Pozycje te mogłyby zostać użyte przez aplikację pracujšcš z 256-kolorowš kartš graficznš do okreœlenia palety dla DIB. Kolejne ostrzeżenie: programy napisane pierwotnie zgodnie z wczeœniejszš do- kumentacjš DIB nie spodziewajš się znaleŸć tablicy kolorów w 24-bitowych DIB. Jeżeli takš umieœcisz, robisz to na własne ryzyko. Mimo swojej nazwy, pole biClrlmportant jest w rzeczywistoœci znacznie mniej ważne od pola biCIrUsed. Zazwyczaj nadana mu jest wartoœć 0, aby zaznaczyć, że wszystkie kolory w tablicy kolorów sš ważne. Może też mieć nadanš wartoœć takš samš jak biClrUsed. Obydwa przypadki znaczš to samo. Jeœli wartoœć pola biCIrImportant zostanie okreœlona pomiędzy 0 a biClrUsed, wówczas obraz DIB może być rozsšdnie renderowany przy użyciu jedynie pierwszych biCIrImportant pozycji z tablicy kolorów. Może się to przydać przy wyœwietlaniu obok siebie dwóch lub więcej 8-bitowych DIB przy 256-kolorowej karcie graficznej. 662 Częœć II: Grafika Dla DIB 1-, 4-, 8- i 24-bitowych orgaruzacja bitów pikseli jest taka sama jak w DIB kompatybilnej z OS/2. Wkrótce omówię DIB 16- i 32-bitowe. Realia Czego oczekujesz od obrazu DIB, który został utworzony przez jakiœ inny pro- gram lub osobę? Mimo iż w czasach, gdy Windows 3.0 pojawił się na rynku, DIB w stylu OS/2 były powszechnie spotykane, ostatnimi laty stajš się one coraz rzadsze. Niektó- rzy programiœci, piszšcy krótkie procedury DIB, praktycznie je ignorujš. Wszyst- kie 4-bitowe DIB, na jakie trafisz, prawdopodobnie zostały utworzone w progra- mie Windows Paint przy wykorzystaniu 16-barwnej palety. W tym wypadku ta- blica kolorów będzie zawierała standardowe 16 kolorów. Prawdopodobnie DIB, które spotkasz, będš miały 8 bitów na piksel. 8-bitowe DIB będš należały do jednej z dwóch kategorii: DIB w odcieniach szaroœci lub DIB z paletš kolorów. Niestety, nic w nagłówku nie wskazuje, z jakim typem 8-bito- wej DIB masz do czynienia. Niektóre DIB w odcieniach szaroœci majš pole biClrUsed równe 64, co oznacza 64 pozycje w tablicy kolorów. Zwykle sš one uszeregowane zgodnie z rosnšcym poziomem szaroœci, to znaczy, że tablica kolorów rozpoczyna się od wartoœci RGB wynoszšcych 00-00-00, 04-04-04, 08-08-08, OC-OC-OC, a kończy na wartoœciach FO- FO-F0, F4-F4-F4, F8-F8-F8 i FC-FC-FC. Taka tablica kolorów obliczana jest przy użyciu wzoru: rgbCi].rgbRed = rgb[i].rgbGreen = rgb[i].rgbBlue = i * 256 / 64 ; gdzie rgb to tablica struktur RGBQUAD, i zmienia się zaœ od 0 do 63. Tablica ko- lorów dla odcieni szaroœci mogła również być obliczona za pomocš następujšce- go wzoru: rgb[i].rgbRed = rgb[i].rgbGreen = rgbCi].rgbBlue = i * 255 / 63 ; zatem kończy się ona na FF-FF-FF. W rzeczywistoœci nie ma znaczenia, który wzór został wykorzystany. I tak wiele kart graficznych i monitorów nie dysponuje precyzjš większš niż 6 bitów. Pierw- szy ze wzorów uwzględnia ten fakt, natomiast drugi jest bardziej odpowiedni przy generowaniu mniejszej liczby odcieni szaroœci, na przykład 16 lub 32 (w któ- rych to przypadkach dzielnik na końcu wzoru wynosi odpowiednio 15 lub 31), ponieważ zapewnia on, że ostatnia pozycja w tablicy kolorów równa się FF-FF- FF, czyli biały. Podczas gdy niektóre 8-bitowe DIB w odcieniach szaroœci majš tablicę kolorów o 64 pozycjach, inne DIB w odcieniach szaroœci majš 256 pozycji. W praktyce pole biCl- rUsed może wynosić 0 (wyznaczajšc 256 pozycji w tablicy kolorów) lub jakškol- wiek wartoœć od 2 do 256. Oczywiœcie, wartoœć biClrUsed równa 2 nie ma zbyt wie- le sensu (ponieważ taka 8-bitowa DIB może być zapisana jako DIB jednobitowa), nie ma też sensu wartoœć mniejsza lub równa 16 (ponieważ może zostać zapisana jako DIB 4-bitowa), lecz jest to dopuszczalne. Niezależnie od sytuacji, liczba ele- mentów tablicy kolorów musi być taka, jak wartoœć pola biCIrUsed (lub 256, jeœli biClrUsed wynosi 0), wartoœci pikseli zaœ mogš wynosić maksymalnie o 1 mniej od Rozdział 15: Bitmapa niezależna od urzšdzeń 663 liczby elementów w tablicy kolorów. Dzieje się tak, ponieważ wartoœci pikseli sš indeksami tablicy kolorów. Dla DIB 8-bitowych o wartoœci biClrUsed równej 64 wartoœci pikseli należš do zakresu od 0x00 do Ox3F. A oto ważna rzecz do zapamiętania: gdy 8-bitowa DIB ma tablicę kolorów skła- dajšcš się całkowicie z odcieni szaroœci (to znaczy gdy poziomy czerwieni, ziele- ni i niebieskiego sš równe) i gdy poziomy szaroœci równomiernie w niej rosnš (tak jak to opisałem wczeœniej), wówczas wartoœci pikseli jako takie reprezentujš proporcjonalne poziomy szaroœci. Czyli, jeœli biClrUsed wynosi 64, to piksel o war- toœci 0x00 jest czamy, piksel o wartoœci 0x20 to 50 procent szaroœci, a piksel o war- toœci Ox3F jest biały. Bywa to istotne w pewnych zadaniach przetwarzania obrazu, ponieważ możesz cał- kowicie zignorować tablicę kolorów i zajšć się tylko wartoœciami pikseli. Jest to na tyle użyteczne, że gdybyœmy mieli możliwoœć cofnięcia się w czasie i dokonania po- jedynczej zmiany w strukturze BTTMAPINFOHEADER, wówczas dodałbym tam fla- gę informujšcš, czy DIB jest w odcieniach szaroœci. Oznaczałoby to, że nie ma tablicy kolorów, wartoœci pi.kseli zaœ bezpoœrednio wskazujš poziom szaroœci. Barwne 8-bitowe DIB generalnie używajš całej tablicy kolorów, zatem pole biClr- Used wynosi 0 lub 256, ale spotyka się i takie, które majš mniejszš liczbę kolorów - na przykład 236. Uwzględniajš one fakt, że programy w celu dokładniejszego wyœwietlenia DIB zwykle mogš zmienić jedynie 236 pozycji w palecie kolorów Windows. Omówię to w następnym rozdziale. Rzadko trafiajš się niezerowe wartoœci biXPeIsPerMeter i biYPelsPerMeter. Nieczę- sto również spotkasz pole biCIrImportant, które zawierałoby wartoœć różnš od 0 lub wartoœć biCirUsed. Kompresţa ţ We wczeœniejszym podrozdziale odłożyłem na póŸniej omówienie pól biCompres- sion i biSizelmage struktury BITMAPINFOHEADER. Nadszedł czas, aby przyjrzeć się tym wartoœciom. ' Pole biCompression może być jednš z czterech stałych: BI RGB, BI RLE8, BI RLE4 lub BI BITFIELDS, zdefiniowanych w pliku nagłówkowym WINGDI.H jako wartoœci, odpowiednio, od 0 do 3. Pole to służy dwóm celom: dla DIB 4- i &bito- wych oznacza, że bity pikseli zostały skompresowane przy użyciu typu kodowa- nia grupowego. Dla áIB 16-bitowych i 32-bitowych mówi ono, czy do kodowa- nia bitów pikseli zostało użyte maskowanie kolorów. Ta drtxga funkcja została wprowadzona w Windows 95. Przyjrzyjmy się najplerw kompresji RLE: ů dla DIB 1-bitowycfs pole biCompression wynosi zawsze BI RGB ů dla DIB 4-bitpwych pole biCompression może równać się BI RGB lub BI RLE4 ů dla DIB 8-bitowych pole biCompression może wynosić BI,RGB lub BI RLE8 ů dla DIB 24-bitowych pole biCompression zawsze jest równe BI RGB. jeœli wartoœć tego pola wynosi BI RGB, bity pikseli zapisywane sš tak jak dIa DIB kompatybilnych z OS/2. W pr2eciwnym przypadku bity pikseli podlegaţ kom- presti za poţoeš koţowarlia grupowego. 664 Częœć II: Grafika Kodowanie grupowe (ang. run-length encoding, RLE) jest jednš z najprostszych form kompresji danych. Wykorzystuje występowanie w wierszach obrazów DIB łańcuchów identycznych pikseli. RLE oszczędza miejsce przez zakodowanie wartoœci powtarzajšcego się piksela oraz liczby jego powtórzeń. Schemat RLE używany dla DIB idzie nieco dalej, pozwalajšc na rozrzedzonš definicję prosto- kštnego obrazu DIB. Oznacza to, że pewne obszary prostokšta pozostajš niezde- finiowane. To może zostać wykorzystane przy renderingu obrazów nieprostokšt- nych. Kodowanie grupowe jest pojęciowo prostsze dla DIB 8-bitowych, zatem od nich zacznijmy. Następujšca tablica pomoże ci zrozumieć, jak kodowane sš bity pik- seli, gdy pole biCompression równe jest BI RGB8. 1 bajt 2 bajty 3 bajty 4 bajty Znaczenie 00 00 Koniec wiersza 00 Ol Koniec obrazu 00 02 dx dy PrzejdŸ do (x+dx, y+dy) 00 n = 03 do FF Użyj następnych n pikseli n = Ol do FF Piksel Powtórz piksel n razy Przy dekodowaniu skompresowanej DIB, patrz na bajty danych DIB parami, tak jak wskazujš w tabeli nagłówki "1 bajt" i "2 bajty". Tabela została skonstruowa- na tak, że wartoœci tych bajtów uszeregowane sš rosnšco, jednak więcej sensu ma omawianie jej poczynajšc od dolnej częœci. Jeœli pierwszy bajt jest różny od zera (przypadek pokazany w ostatnim wierszu tabeli), wówczas jest to współczynnik powtórzenia grupy. Następujšca po nim wartoœć piksela powtarzana jest tyle razy, na ile wskazuje 1 bajt. Na przykład para bajtów 0x05 0x27 zostanie zdekodowana do cišgu wartoœci pikseli: 0x27 0x27 0x27 0x27 0x27 Oczywiœcie DIB będzie zawierała wiele danych, które nie będš powtarzały się od piksela do piksela. To właœnie przypadek odpowiadajšcy drugiemu od dołu wier- szowi tabeli. Okreœla on liczbę pikseli następujšcych po sobie, które powinny zostać użyte dosłownie. Rozważ na przykład sekwencję: 0x00 0x06 0x45 0x32 0x77 0x34 0x59 0x90 Po zdekodowaniu otrzymujemy cišg wartoœci pikseli: 0x45 0x32 0x77 0x34 0x59 0x90 Te sekwencje sš zawsze wyrównywane do 2 bajtów. Jeœli drugi bajt jest nieparzy- sty, wówczas w sekwencji występuje dodatkowy nieużywany bajt, na przykład sekwencja: 0x00 0x05 0x45 0x32 0x77 0x34 0x59 0x00 po zdekodowaniu przybiera postać cišgu wartoœci pikseli: 0x45 0x32ţ0x77 0x34 0x59 Rozdział 15: Bitmapa niezależna od urzšdzeń 665 Tak działa kodowanie grupowe. Oczywiœcie, jeœli w obrazie DIB nie ma powta- rzajšcych się pikseli, wówczas użycie tej techniki kompresji w rzeczywistoœci zwiększy rozmiar pliku DIB. Pierwsze trzy wiersze przedstawionej wczeœniej tabeli pokazujš, jak niektóre częœci prostokštnego obrazu DIB można pozostawić bez ich definiowania. WyobraŸ sobie siebie lub program, który napisałeœ, przy rozpakowywaniu skompresowanej DIB. Podczas procedury dekompresji będziesz zapamiętywał pary liczb (y, x) poczy- najšc od (0, 0). I za każdym razem, gdy rozkodujesz piksel, będziesz zwiększał x 0 1. Po zakończeniu każdego wiersza wyzerujesz x i zwiększysz y o 1. i Gdy napotkasz bajt 0x00, po którym następuje 0x02, odczytasz dwa następne bajty i dodasz je jako przyrosty do aktualnych wartoœci x i y, po czym możesz konty- nuować dekodowanie. Kiedy odczytasz bajt 0x00, po którym następuje 0x00, skoń- czyłeœ wiersz. Nadaj x wartoœć 0 i zwiększ y. Po napotkaniu bajtu 0x00, a po nim 0x01, skończyłeœ rozkodowywanie. Takie kody pozwalajš nie definiować niektó- rych obszarów DIB, co przydaje się niekiedy przy kodowaniu nieprostokštnego obrazu lub do wykonywania cyfrowych animacji czy filmów (ponieważ każda g atka głównie zawiera informację z poprzedniej klatki i nie wymaga ponowne- o kodowania). Dla DIB 4-bitowych kodowanie jest generalnie takie samo, chociaż komplikuje się nieco, ponieważ nie ma jednoznacznej odpowiednioœci między bajtami a pik- selami. Jeżeli pierwszy bajt, jaki odczytasz, jest niezerowy, to jest to współczynnik po- wtórzenia n. Drugi bajt (który ma być powtórzony) zawiera 2 piksele naprzemien- ne w dekodowanym cišgu n pikseli, na przykład para: 0x07 0x35 dekodowana jest jako: 0x35 0x35 0x35 Ox3? gdzie znak zapytania oznacza, że nie znamy jeszcze wartoœci piksela. Jeżeli po parze 0x07 0x35 pokazanej powyżej pojawi się para: 0x05 0x24 cała rozkodowywana sekwencja przybiera postać: 0x35 0x35 0x35 0x32 0x42 0x42 Jeœli pierwszy bajt w parze wynosi 0x00, drugi zaœ jest równy 0x03 lub więcej, użyj liczby pikseli podanej w drugim bajcie, na przykład, sekwencja: 0x00 0x05 0x23 0x57 0x10 0x00 rozkodowuje się na: 0x23 0x57 Oxl? Zauważ, że zakodowana sekwencja musi zostać dopełniona do parzystej liczby bajtów. Gdy pole biCompression równa się BI RLE4 lub BI RLE8, pole biSizelmage podaje w bajtach rozmiar danych pikseli DIB. Jeżeli pole biCompression wynosi BI RGB, wówczas biSizelmage zwykle ma wartoœć 0, lecz może być okreœlona jako biHeight razy długoœć wiersza w bajtach, tak jak zostało to obliczone we wczeœniejszej częœci tego rozdziału. 666 Częœó II: Grafika W aktualnej dokumentacji znajdziesz informację, że "DIB z góry na dół" nie mogš podlegać kompresji. "DIB z góry na dół" to te, które majš ujemne wartoœci pól biHeight. Maskowanie kolorów Pole biCompression jest również używane w przypadku DIB 16- i 32-bitowych, które były nowoœciš w Windows 95. Dla nich pole biCompression może mieć albo war- toœć BI RGB, albo BI BITFIELDS (z definicji równe 3). Dla przykładu przyjrzyjmy się formatowi pikseli 24-bitowej DIB, której pole bi- Compression zawsze jest równe BI RGB: Piksel: -Niebieski- -Zielony- -Czerwony- 7.6 5 4 3 2 1 0 .7 6 5.4.3!2 1 :0 7v,6 5,4;3 2 1 U'. Oznacza to, że każdy wiersz jest zasadniczo tablicš struktur RGBTRIPLE dopeł- nionš ewentualnie na końcu wiersza, tak aby liczba bajtów w nim była wielo- krotnoœciš 4. Dla 16-bitowej DIB o polu biCompression równym BI RGB, każdy piksel wymaga dwóch bajtów. Kolory zakodowane sš w następujšcy sposób: Piksel: ...lony - - Niebieski - 0 - Czerwony - Zie... 7'.6'5'.:4'3 2`1'..0 7'6.5 4 3 2 1 0, KaŸdy kolor używa pięciu bitów. Dla pierwszego piksela w wierszu, wartoœć niebie- ska to ostatnie, najmniej znaczšce pięć bitów pierwszego bajtu. Wartoœć zieieni wy- maga bitów z pierwszego i drugiego bajtu: dwa nalbardziej znaczšce bityvartoœei zieleni to dwa mniej znaczšce bity drugżego bajtu, a trzy mniej znaczţce bity warto- œci zieleni to trzy naţardziej znaczšce bity pierwszego bajtu. Wartoœć czerwieni to bity od 2 do 6 w drugim bajcie. Naţardziej znaczšcy bit w drugim bajcie wynosi 0. Ma to o wiele więcej sensia, gdy traktujesz wartoœc piksela jako 16-bitowe słowo. Ponieważ najrnniej znaczšce bajty wielobajtowych wartoœci zapisywane sš na poczštku, słowo piksela wyglšda tak: 0 - CzervKony - - Zielony - - Ntebieski - Słowo piksera. ,15 14~ t3 12 11 i0 g , 8 , 7 , 6 .. 5 ţ ; 3 '. 2 , t .. 0 Załóżmy, że masz 16-bitowy piksel zapisany w zvPixel. Tak mo,żesz obliczyţć.ar- toœci czerwieni, aieleni i niebieskiego: Red = ((0x7C00 & wPixel) Ż 10) Ž 3 ; Green = ((0x03E0 & wPixel) Ż 5) Ž 3 ; Blue = ((0x00IF & wPixel) Ż 0) Ž 3 ; Najpierw piksel poddawany jest bit po bicie koniunkcji .4ND z wartoœcia maski. Wynik przesuwany jest w prawo 0 10 bitów dla czerwieru, 0 5 bitów dla zieleni i o 0 bitów dla niebieskiego. Będę nazywał te wartoœci przesunięć wartoœcrarni prawych przesunięe. Daje to wartoœci kolorów z zakresu od 0x00 do OxlF. Warto- œci muszš następnie zostać przesunięte w lewo 0 3 bity, zatem wynikowe warto- Rozdział 15: Bitmapa niezależna od urzšdzeń 607 œci kolorów należš do zakresu od 0x00 do OxF8. Te wartoœci przesunięć będę okre- œlał jako lewe przesunięcia. Zapamiętaj: jeœli szerokoœć 16-bitowej DIB w pikselach jest nieparzysta, każdy wiersz będzie miał na końcu dodatkowe 2 bajty dopełnienia, tak by uzyskać dłu- goœć w bajtach podzielnš przez 4. Dla DIB 32-bitowej, jeżeli biCompression równe jest BI RGB, każdy piksel wyma- ga 4 bajtów. Wartoœć koloru niebieskiego to pierwszy bajt, zieleni to bajt drugi, czerwień to trzeci, czwarty bajt zaœ równy jest 0. Innymi słowy, piksele sš tablicš struktur RGBQUAD. Ponieważ każdy piksel ma 4 bajty długoœci, nigdy nie jest potrzebne dopełnianie na końcu wiersza. Jeżeli potraktujemy każdy piksel jako 32-bitowe dwusłowo, będzie ono wyglš- dało tak: 0 -Czerwony- - Zielony - - Niebieski - Dwustowo piksela: 31 30 ... 25'24 23 22.. .:. .17 16.15 14 ... ; 9 8 7 6 . .. 1 0 , Lub, jeœli wPixel jest tym 32-bitowym dwusłowem, to s;... Red = ((0x00FF0000 & dwPixel) Ż 16) Ž 0 ; Green = ((0x0000FF00 & dwPixel) Ż 8) Ž 0 ; Blue = ((0x000000FF & dwPixel) Ż 0) Ž 0 ; Wszystkie wartoœci lewych przesunięć wynoszš zero, ponieważ dla OxFF wszystkie wartoœci kolorów sš już zmaksymalizowane. Wiedz, że to dwusłowo nie jest spójne z 32-bitowš wartoœciš COLORREF używanš dla okreœlenia koloru RGB w wy- wołaniach funkcji Windows GDI. W wartoœci COLORREF czerwień jest bajtem najmniej znaczšcym. Na razie omówiliœmy standardowy przypadek 16-bitowych i 32-bitowych DIB, gdy pole biCompression jest równe BI RGB. Jeżeli pole biCompression m.a wartoœć BI BITFIELDS, to w DIB bezpoœrednio po strukturze BITMAPINFOHEADER występujš trzy 32-bitowe maski kolorów, pierwsza dla czerwieni, druga dla zie- leni, a trzecia dla niebieskiego. Chcšc zastosować te maski do 16-bitowej lub 32- bitowej wartoœci piksela, używasz operatora C wykonujšcego bit po bicie koniunk- cję (&). Następnie przesuwasz wynik w prawo o wartoœci prawych przesunięć, które, niestety, nie sš znane do czasu, aż przyjrzysz się samym maskom. Gdy się zastanowisz, reguły rzšdzšce tymi trzema maskami kolorów powinny się stać zro- zumiałe: bity 1 w każdej masce koloru muszš stanowić cišg oraz nie mogš się pokrywać między trzema maskami. Na przykład: masz 16-bitowš DIB, a pole biCompression wynosi BI BITFIELDS. Przyglšdasz się pierwszym trzem dwusłowom następujšcym po strukturze BIT MAPINFOHEADER: 0x0000F800 0x000007E0 . 0x0000001F Zauważ, że wartoœć 1 nadano tylko bitom spoœród mniej znaczšcych 16 bitów, ponieważ jest to 16-bitowa DIB. Wartoœci te przypisujesz zmiennym dwMask[0], dwMask[1] i dwMask[2]. Teraz piszesz niewielkie procedury, które z masek obli- czajš prawe i lewe przesunięcia: 668 Częœć II: Grafika int MaskToRShift (DWORD dwMask) ( int iShift ; if (dwMask == 0) return 0 ; for (iShift = 0 ; !(dwMask & 1) , iShift++) dwMask Ż= 1 ; return iShift ; int MaskToLShift (DWORD dwMask) int iShift : if (dwMask = 0) return 0 ; while (!(dwMask & 1)) dwMask Ż= 1 ; for (iShift = 0 ; dwMask & 1 ; iShift++) dwMask Ż= 1 ; return 8 - iShift ; Następnie trzykrotnie wywołujesz funkcję MaskToRShift, aby uzyskać wartoœci prawych przesunięć: iRShiftCO] = MaskToRShift (dwMaskCO]) ; iRShiftCl] = MaskToRShift (dwMask[1]) ; iRShiftC2] = MaskToRShift (dwMaskC2]) ; Otrzymujesz odpowiednio wartoœci 11, 5 i 0. Następnie w podobny sposób mo- żesz wywołać MaskToLShţ: iLShiftCO] = MaskToLShift (dwMaskCO]) ; iLShift[1] = MaskToLShift (dwMaskCl]) ; iLShift[2] = MaskToLShift (dwMaskC2]) ; Uzyskujesz odpowiednio wartoœci 3, 2 i 3. Teraz możesz wyodrębnić każdy z ko- lorów z wartoœci piksela: Red = ((dwMaskCO] & wPixel) Ż iRShiftCO]) Ž iLShiftCO] ; Green = ((dwMaskCl] & wPixel) Ż iRShiftCl]) Ž iLShiftCl] ; Blue = ((dwMaskC2] & wPixel) Ż iRShiftC2]) Ž iLShiftC2] ; Dla 32-bitowych DIB procedura jest ta sama, tyle że maski kolorowe mogš być większe od 0x0000FFFF, czyli maksymalnej wartoœci maski dla DIB 16-bitowych. Zauważ, że zarówno dla DIB 16-bitowej, jak i dla 32-bitowej wartoœci kolorów czerwieni, zieleni i niebieskiego mogš być większe od 255. W istocie w 32-bito- wej DIB, jeœli dwie maski sš równe 0, to trzecia mogłaby wynosić OxFFFFFFFF dla 32-bitowej wartoœci koloru! Oczywiœcie jest to œmieszne, zatem nie przejmo- wałbym się tym zanadto. ţ Rozdział 15: Bitmapa niezależna od urzšdzeń 669 W odróżnieniu od NT, Windows 95 i Windows 98 majš pewne ograniczenia do- tyczšce stosowania masek kolorów. Jedyne dopuszczalne wartoœci sš podane w poniższej tabeli. 16-bitowa DIB 16-bitowa DIB 32-bitowa DIB Maska czerwona 0x00007C00 0x0000F800 0x00FF0000 Maska zielona 0x000003E0 0x000007E0 0x0000FF00 Maska niebieska 0x0000001F 0x0000001F 0x000000FF Zapis skrócony 5-5-5 5-6-5 8-8-8 Innymi słowy, możesz użyć dwóch zbiorów masek, jakie uzyskałbyœ standardo- wo przy polu biCompression równym BI RGB, oraz zbioru masek, który podałem w powyższym przykładzie. Dolny wiersz tabeli jest skróconym zapisem liczby bitów czerwieni, zieleni i niebieskiego na piksel. Nagłówek wersji 4 jeszcze nie całkiem skończyliœmy. Jak już wspomniałem, Windows 95 zmienił nieco pierwotne definicje pola BITMAPINFOHEADER. Dołšczył takŸe nowy, rozsze- rzony nagłówek informacyjny o nazwie BITMAPV4HEADER. Nazwa tej struk- tury stanie się zrozumiała, gdy uœwiadomisz sobie, że Windows 95 mogło zostać nazwane Windows 4.0 oraz że struktura ta obsługiwana była również prŸez Win- dows NT 4.0. typedef struct ( DWORD bV4Size ; // rozmiar struktury = 120 LONG bV4Width ; // szerokoœć obrazu w pikselach LONG bV4Height ; // wysokoœć obrazu w pikselach WORD bV4Planes ; // = 1 WORD bV4BitCount ; // bitów na piksel (l, 4, 8, 16, 24 lub 32) DWORD bV4Compression ; // kod kompresji DWORD bV4SizeImage ; // liczba bajtów w obrazie LONG bV4XPelsPerMeter ; // rozdzielczoœć pozioma LONG bV4YPelsPerMeter ; // rozdzielczoœć pionowa DWORD bV4ClrUsed ; // liczba użytych kolorów DWORD bV4ClrImportant ; // liczba istotnych kolorów DWORD bV4RedMask ; // maska koloru czerwonego DWORD bV4GreenMask ; // maska koloru zielonego DWORD bV4BlueMask ; // maska koloru niebieskiego DWORD bV4AlphaMask ; // maska Alpha DWORD bV4CSType ; // typ przestrzeni koloru CIEXYZTRIPLE bV4Endpoints ; // wartoœci XYZ DWORD bV4GammaRed ; // wartoœć gamma czerwieni DWORD bV4GammaGreen ; // wartoœć gamma zieleni DWORD bV4GammaBlue ; // wartoœć gamma niebieskiego 1 BITMAPV4HEADER, * PBITMAPV4HEADER ; Zauważ, że pierwsze 11 pól jest takie samo, jak w strukturze BITMAPINFOHE- ADER. Ostatnie pięć pól obsługuje technologię zgodnoœci kolorów obrazu dla Windows 95 i Windows NT 4.0. O ile nie korzystasz z ostatnich czterech pól struk- 670 Częœć II: Grafika tury BITMAPV4HEADER, powinieneœ zamiast niej używać BITMAPINFOHE- ADER (lub BITMAPVSHEADER). Wartoœci bV4RedMask, bV4GreenMask i bV4BIueMask majš zastosowanie tylko dla 16-bitowych i 32-bitowych DIB, gdy pole bV4Compression równe jest BI BITFIELDS. Spełniajš one te same funkcje co maski kolorów definiowane w strukturze BIT MAPINFOHEADER i faktycznie występujš w tym samym miejscu w pliku DIB co przy użyciu oryginalnej struktury, z tš tylko różnicš, że sš jawnymi jej polami. O ile mi wiadomo, pole bV4AIphaMask nie jest używane. Pozostałe pola struktury BITMAPV4HEADER dotyczš Windows Image Color Management, który, obawiam się, wykracza poza zakres tej ksišżki. Jednak nieco podstawowych wiadomoœci pomoże ci na starcie. Problem z użyciem schematu kolorów RGB polega na tym, że jest on zależny od technologii monitorów, kolorowych kamer i skanerów. Jeœli kolor okreœlony zo- stanie jako wartoœć RGB równa (255, 0, 0) to znaczy, że w lampie katodowej do czerwonego działa elektronowego powinno zostać przyłożone maksyůmalne na- pięcie. Wartoœć RGB równa (128, 0, 0) oznacza, że należy zastosować napięcie o połowę niższe. Monitory mogš różnie zareagować na to działanie. Co więcej, drukarki stosujš innš metodę uzyskiwania koloru, polegajšcš na wymieszaniu atramentów turkusowego, różowego, żółtego i czarnego. Metody te znane sš jako CMY (ang. cyan-magenta-yellow) i CMYK (ang. cyan-magenta-yellozu-black). Za po- mocš wzorów matematycznych można dokonać translacji wartoœci RGB na CMY i CMYK, lecz nie ma gwarancji, że kolor drukarki będzie odpowiadał kolorowi monitora. Image Color Management jest próbš uzyskania dIa kolorów standar- dów niezależnoœci od urzšdzeń. Fenomen koloru zwišzany jest z długoœciš fali œwiatła widzialnego, która może zmieniać się od 380 nanometrów (niebieski) do 780 nm (czerwony). Każde œwia- tło, jakie postrzegamy wzrokiem, jest kombinacjš różnej liczby różnych długoœci fal z widma widzialnego. W 1931 Commission Internationale de I;Eclaira.ge (Mię- dzynarodowa Komisja do Spraw Oœwietlenia), czyli CIE, opracowała metodę na- ukowej kwalifikacji kolorów. Zwišzana jest ona z użyciem trzech funkcji zgod- noœci kolorów (nazwanych x, y i z), które w swojej postaci skróconej (o warto- œciach co 5 nm) zostały opisane w tabeli 2.1 publikacji CIE z dnia 15 lutego 1986, zatytułowanej "Colorimetry, Second Edition". Skład widmowy (S) koloru jest zbiorem wartoœci okreœlajšcym natężenie fali o każ- dej długoœci. Jeœli widmo jest znane, można do niego zastosować funkcje zgod- noœci kolorów, aby obliczyć X, Y i Z: 780 ţ =380 780 A=380 780 ţ =380 Rozdziaf 15: Bitmapa niezależna od urzšdzeń 671 Funkcja zgodnoœci koloru równoważna jest odpowiedzi oka ludzkiego na zakres œwiatła w widmie widzialnym (wyglšda ona jak krzywa dzwonowa, osišgajšca 0 dla 380 nm i 780 nm). Y nazywana jest jaskrawoœciš CIE, ponieważ okreœla cał- kowitš intensywnoœć œwiatła. Jeżeli używasz struktury BITMAPV4HEADER, to polu bV4CSType musi być nada- na wartoœć LCS CALIBRATED RGB, co równoważne jest 0. Następnym czterem polom muszš być przypisane odpowiednie wartoœci. Struktura CIEXYZT'RIPLE zdefiniowana jest następujšco: typedef struct tagCIEXYZTRIPLE CIEXYZ ciexyzRed ; CIEXYZ ciexyzGreen : CIEXYZ ciexyzBlue ; ; 1 CIEXYZTRIPLE. * LPCIEXYZTRIPLE ; a struktura CIEXYZ to: typedef struct tagCIEXYZ FXPT2DOT3O ciexyzX ; FXPT2DOT3O siexyzY ; FXPT2DOT3O ciexyzZ : CIEXYZ. * LPCIEXYZ ; Trzy pola zdefiniowarţe sš jako wartoœci typu FXPT2DOT3O, co oznacza, że in- terpretawane sš jako stałoprzecinkowe wartoœci z 2-bitowš częœciš całkowitš i 30- bitowš częœciš ułamkowš. Stšd 0x40000000 to 1.0, a 0x48000000 to 1.5. Maksy- malna wartţć ţxFFFFFFFF to po prostu troszkę poniżej 4.0. I Pole bV4Endpoints dostarcza trzeeh wartoœci: X, Y i Z, odpowiadajšcych kolorom RGB (255, 0, 0), (0, 255; 0) i (0, 0, 255). Wartoœci te powinny być wstawione przez aplikację, która tworzy DIB, aby okreœlić niezależne od urzšdzenia znaczenie tych kołorţw RGB. Pozosta^e trzy pola FIT'MAPV4HEADER odnoszš się do "gamma". Gamma (mała grecka litera y) dotyczy nieliriiowoœci w okreœleniu poziomu kolorów. W DIB poziomy czerwieni, zieleni i niebieskiego należš do zakresu od 0 do 255. W kar- cie graficznej te wartoœci cyfrowe podlegajš konwersji na trzy analogowe napię- cia, które przesyłane sš do Fnonitora. Napięcia te okreœlajš intensywnoœć każde- go piksek. Jednak z uwagi na charakterystykę elementów elektronicznych dział elektronowych w Iampie katodawej nie ma finiowej zależnoœci pomiędzy inten- sywnoœciš (f) piksela a napięciem (V). Relacja ma zamiast tego postać: Iţ(V+ţr gdzie e jest poziomem ezerni monitora ustawianym przez regulację jaskrawoœci. (Preferowaxţa jest wartoœć 0). Wykładnik gamma ustawia się regulatorem kontra- stu. Dla większoœci monitorów wartoœć gamma wynosi około 2,5. Aby skompensować tę nieliniowoœć, kamery wideo tradycyjnie miały w swoich obwodach wbudowany "korektor gamma". Strumień œwiatła wchodzšcy do ka- mery modyfikowany jest o wykładnik 0,45. Co implikuje dla monitora wartoœć gamxna około 2,2. (Wyższa gamma monitorów zrnienia w pewien sposób kon- CzQœó II: Grafika trast, co zwykle nie jest niepożšdane, ponieważ œwiatło z otoczenia ma tendencję do jego zmniejszania). To nieliniowe zachowanie monitorów jest w rzeczywistoœci znacznie korzystniej- sze, niż wydaje się na pierwszy rzut oka, ponieważ ludzka reakcja na œwiatło także nie jest liniowa. Wczeœniej wspomniałem, że Y nazywana jest jaskrawoœciš CIE. Jest to liniowa miara œwiatła. CIE definiuje także wartoœć œwiatłoœci, która aprok- symuje ludzkš percepcję. Œwiatłoœć oznacza się L* (ang. lightness) i jest obliczana z Y za pomocš wzorów: 903,3 Y Y < 0,008856 L* = 1 116(Y3-16 0,008856 < gdzie Y" jest poziomem bieli. Pierwsza częœć tego wzoru to niewielki segment liniowy. Generalnie, ludzka wrażliwoœć na œwiatło jest zależna od pierwiastka trzeciego stopnia liniowej luminacji, co widać w drugim wzorze. L* należy do zakresu od 0 do 100. Zakłada się, że każdy całkowity przyrost L* jest najmniejszš zmianš œwiatłoœci dostrzegalnš dla ludzi. Korzystniej jest kodować intensywnoœci œwiatła na podstawie percepcyjnej œwia- tłoœci niż liniowej luminacji. Zmniejsza to do rozsšdnego poziomu liczbę bitów, a także zmniejsza szumy w obwodach analogowych. PrzeœledŸmy cały proces. Wartoœć piksela (P) należy do zakresu od 0 do 255. Jest ona liniowo konwertowana na poziom napięcia, co do którego możemy założyć, że jest normalizowany do wartoœci pomiędzy 0,0 i 1,0. Przyjmujšc, że poziom czer- ni monitora okreœlony jest na 0, intensywnoœć piksela wynosi: ţ _P = (255, I gdzie y prawdopodobnie równa się około 2,5. Ludzka wrażliwoœć na œwiatło (L*) opiera się na pierwiastku kwadratowym tej intensywnoœci i należy do zakresu od 0 do 100, zatem w przybliżeniu: L*= 100 P f 255 ) Wykładnik ten będzie równy około 0,85. Jeœli wykładnik byłby 1, wówczas œwia- tłoœć CIE doskonale odpowiadałaby wartoœciom pikseli. Nie mamy dokładnie takiej sytuacji, lecz jest ona znacznie bliższa, niż gdyby wartoœci pikseli wskazy- wały liniowš luminację. Ostatnie trzy pola BITMAPV4HEADER dajš programom tworzšcym DIB możli- woœć podania wartoœci gamma, którš przyjęto dla wartoœci pikseli. Wartoœci te sš interpretowane jako 16-bitowe wartoœci całkowite i 16-bitowe wartoœci ułamko- we, na przykład 0x10000 to 1,0. Jeżeli DIB jest tworzona przez rejestrację obrazu rzeczywistego, wówczas ta wartoœć gamma prawdopodobnie wynika z właœci- Rozdział 15: Bitmapa niezależna od urzšdzeń 673 woœci sprzętu i prawdopodobnie wyniesie 2,2 (kodowane jako 0x23333). Jeœli DIB jest generowany algorytmicznie przez program, dokona on za pomocš funkcji potęgowej konwersji wszelkich liniowych luminacji, których używa, na wartoœci œwiatłoœci CIE. Odwrotnoœć wykładnika będzie gammš zakodowanš w DIB. Nagłówek wersji 5 Programy napisane dla Windows 98 i Windows NT 5.0 mogš używać DIB, które majš nowš strukturę informacyjnš BITMAPV5HEADER: typedef struct ( DWORD bV5Size ; // rozmiar struktury = 120 LONG bV5Width ; // szerokoœć obrazu w pikselach LONG bU5Height ; // wysokoœć obrazu w pikselach WORD bV5Planes ; // = 1 WORD bV5BitCount ; // bitów na piksel (1, 4, 8, 16, 24 lub 32) DWORD bV5Compression ; // kod kompresji DWORD bV5SizeImage ; // liczba bajtów w obrazie LONG bU5XPelsPerMeter ; // rozdzielczoœć pozioma LONG bV5YPelsPerMeter ; // rozdzielczoœć pionowa DWORD bV5ClrUsed ; // liczba użytych kolorów DWORD bV5ClrImportant : // liczba istotnych kolorów DWORD bV5RedMask ; // maska koloru czerwone9o DWORD bV5GreenMask ; // maska koloru zielonego DWORD bU5BlueMask ; // maska koloru niebieskiego DWORD bV5AlphaMask ; // maska Alpha DWORD bV5CSType ; // typ przestrzeni koloru CIEXYZTRIPLE bV5Endpoints ; // wartoœci XYZ DWORD bV5GammaRed ; // wartoœć 9amma czerwieni DWORD bV5GammaGreen ; // wartoœć gamma zieleni DWORD bV5GammaBlue ; // wartoœć gamma niebieskiego DWORD bV5Intent ; // rendering DWORD bV5ProfileData ; // dane profilu lub nazwa pliku DWORD bV5ProfileSize ; // rozmiar danych rozszerzonych lub nazwa pliku DWORD bV5Reserved ; 1 BITMAPV5HEADER, * PBITMAPV5HEADER ; Ma ona cztery nowe pola, z których tylko trzy sš używane. Utworzenie tych pól było pomysłem International Color Consortium (założonego przez firmy Adobe, Agfa, Apple, Kodak, Microsoft, Silicon Graphics, Sun Microsystems i inne) i nada- no im nazwę ICC Profile Format Specification. Kopię możesz uzyskać pod adresem http://www.icc.org. Zasadniczo, każde urzšdzenie wejœciowe (skaner lub kame- ra), wyjœciowe (drukarka lub rejestrator danych na błonie filmowej) i wyœwietla- jšce (monitor} jest powišzane z profilem, który wišże oryginalne, zależne od urzš- dzenia kolory (generalnie RGB lub CMYK) z niezależnš od niego specyfikacjš kolorów, zasadniczo opartš na wartoœciach CIE XYZ. Nazwy plików tych profili majš rozszerzenie .ICM (ang. image color management - zarzšdzanie kolorami ob- razu). Profil może być osadzony w pliku DIB lub dołšczony do niego, aby poka- zać, jak DIB został utworzony. Więcej informacji na temat Image Color Manage- ment w Windows możesz znaleŸć w /Platform SDK/Graphics and Multimedia Se- rvices/Color Management. 674 C:ęœć IIţ Grafika Pole bV5CSType w BITMAPVSHEADER może przyjmować różne wartoœci. Jeœli jest równe LCS CALIBRATED RGB, wówczas jest kompatybilne ze strukturš BITMAPV4HEADER. Pola bV5Endpoints oraz gamma muszš być okreœlone. Jeżeli pole bV5CSType ma wartoœć LCS sRGB, żadne z pozostałych pól nie musi być okreœlone. Implikowana przestrzeń kolorów jest "standardowš" przestrze- niš kolorów RGB skonstruowanš przez Microsoft i Hewletta-Packarda w celu osiš- gnięcia pewnej relatywnej niezależnoœci od urzšdzeń, w szczególnoœci w Inter- necie, bez całej masy profili. Dokumentację na ten temat można znaleŸć w http:/ /www.color.oig/contrib/sltGB.html. Jeœli pole bVSCSType wynosi LCS_WINDOWS_COLOR SPACE, żadne z pozo- stałych pól nie musi być okreœlone. Windows podczas wyœwietlenia bitmapy uży- wa przestrzeni kolorów wynikajšcej z wywołań funkcji API. Jeżeli pole bV5CSType wynosi PROFILE_EMBEDDED, plik DIB zawiera profil ICC. Jeœli pole to ma wartoœć PROFILE_LINKED, plik DIB zawiera w pełni kwalifiko- wanš nazwę pliku do profilu ICC W każdym przypadku bV5ProfileData jest od- ległoœciš od poczštku BITMAPVSHEADER do poczštku danych o profilu lub nazwy pliku. Pole bV5ProfileSize podaje rozmiar danych lub nazwy pliku. Punk- ty końcowe i pola gamma nie wymagajš okreœlania. Wyœwietlanie informacji DIB Nadszedł czas, aby obejrzeć nieco kodu. Nie mamy dostatecznej wiedzy, by już te- raz rzeczywiœcie wyœwietlić DIB, lecz możemy przynajmniej na podstawie struktur nagłówkowych wyœwietlić informację na jej temat. Robi to program DIBHEADS, zaprezentowany na rysunku 15-1. DIBHEADS.C /* DIBHEADS.C - Wyœwietlanie informacji zawartej w pliku naglówkowym DIB (c) Charles Petzold, 1998 llinclude ţlinclude "resource.h" LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; TCHAR szAppName[] ţ TEXT ("DibHeads") ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance. PSTR szCmdLine, int iCmdShow) HACCEL hAcceT ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; */ wndclass.style = CS_HREDRAW ţ CSţVREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; Rozdziaf 15: Bitmapa niezależna od urzšdzeń 675 s ,;. wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC ARROW) : wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE BRUSH) ; wndclass.lpszMenuName = szAppName ; wţidclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) ( Messa9eBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MB ICONERROR) ; i; return 0 ; hwnd = CreateWindow (szAppName, TEXT ("DIB Headers"), WS_OVERLAPPEDWINDOW, I: CW_USEDEFAULT, CWţUSEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; ł.: hAccel = LoadAccelerators (hInstance, szAppName) : while (GetMessage (&msg, NULL, 0, 0)) if (!TranslateAccelerator (hwnd, hAccel, &msg)) TranslateMessage (&msg) : DispatchMessage (&msg) ; ) 1 return msg.wParam ; void Printf (HWND hwnd, TCHAR * szFormat, ...) i': ( TCHAR szBuffer C1024] : vaţlist pArgList ; va_start (pArgList, szFormat) ; wvsprintf (szBuffer, szFormat, pArgList) ; va ęnd (pArgList) ; SendMessage (hwnd, EM_SETSEL, (WPARAM) -1, (LPARAM) -1) ; SendMessage (hwnd, EM_REPLACESEL, FALSE, (LPARAM) szBuffer) ; SendMessage (hwnd, EMţSCROLLCARET, 0, 0) : void DisplayDibHeaders (HWND hwnd, TCHAR * szFileName) static TCHAR * szInfoName C7 = ( TEY,T ("BITMAPCOREHEADER"), TEXT ("BITMAPINFOHEADER"), ;,: TEXT ("BITMAPU4HEADER"), l" TEXT ("BITMAPV5HEADER") t : i , static TCHAR * szCompression C7 = ( TEXT ("BIţRGB"), TEXT ("BIţRLE8"), ţ.ţ„ 676 Częœć II: Grafika (cišg dalszy ze strony 675) TEXT ("BI_RLE4"), TEXT ("BI_BITFIELDS"), TEXT ("unknown") } ; BITMAPCOREHEADER * pbmch ; BITMAPFILEHEADER * pbmfh ; BITMAPV5HEADER * pbmih ; BOOL bSuccess ; DWORD dwFileSize, dwHighSize, dwBytesRead ; HANDLE hFile ; int i ; PBYTE pFile ; TCHAR * szV ; // Wyœwietla nazwę pliku Printf (hwnd, TEXT ("File: %s\r\n\r\n"), szFileName) ; // Otwórz plik hFile = CreateFile (szFileName, GENERIC_READ, FILE_SHARE_READ, NULL, OPENţEXISTING, FILEţFLAGţSEOUENTIALţSCAN, NULL) ; if (hFile = INVALIDţHANDLĘ VALUE) ( Printf (hwnd, TEXT ("Cannot open file.\r\n\r\n")) ; return ; ? // Odczytaj rozmiar pliku dwFileSize = GetFileSize (hFile, &dwHighSize) ; if (dwHighSize) ( Printf (hwnd, TEXT ("Cannot deal with >4G files.\r\n\r\n")) ; CloseHandle (hFile) ; return ; 1 // Zarezerwuj pamięć dla pliku pFile = malloc (dwFileSize) ; if (!pFile) I Printf (hwnd, TEXT ("Cannot allocate memory.\r\n\r\n")) ; CloseHandle (hFile) ; return ; 1 // Czytaj plik SetCursor (LoadCursor (NULL, IDCţWAIT)) ; ShowCursor (TRUE) ; bSuccess = ReadFile (hFile, pFile, dwFileSize, &dwBytesRead, NULL) ; Rozdział 15: Bitmapa niezależna od urzšdzeń ShowCursor (FALSE) ; SetCursor (LoadCursor (NULL, IDC ARROW)) ; if (!bSuccess ţţ (dwBytesRead != dwFileSize)) ( Printf (hwnd, TEXT ("Could not read file.\r\n\r\n")) ; CloseHandle (hFile) ; free (pFile) ; return ; l // Zamknij plik CloseHandle (hFile) ; // Wyœwietl rozmiar pliku Printf (hwnd, TEXT ("File size = %u bytes\r\n\r\n"), dwFileSize) ; // Wyœwietl strukturę BITMAPFILEHEADER pbmfh = (BITMAPFILEHEADER *) pFile ; Printf (hwnd, TEXT ("BITMAPFILEHEADER\r\n")) ; Printf (hwnd, TEXT ("\t.bfType = Ox%X\r\n"), pbmfh->bfType) ; Printf (hwnd, TEXT ("\t.bfSize = %u\r\n"), pbmfh->bfSize) ; Printf (hwnd, TEXT ("\t.bfReservedl = %u\r\n"), pbmfh->bfReservedl) ; Printf (hwnd, TEXT ("\t.bfReserved2 = %u\r\n"), pbmfh->bfReserved2) ; Printf (hwnd, TEXT ("\t.bfOffBits = %u\r\n\r\n"), pbmfh->bfOffBits) ; // SprawdŸ, którd mamy strukturę informacyjnš pbmih = (BITMAPV5HEADER *) (pFile + sizeof (BITMAPFILEHEADER)) ; switch (pbmih->bV5Size) break ; case sizeof (BITMAPCOREHEADER): i = 0 ; case sizeof (BITMAPINFOHEADER): i = 1 ; szV = TEXT ("i") , break ; case sizeof (BITMAPV4HEADER): i = 2 ; szV = TEXT ("V4") ; break ; case sizeof (BITMAPV5HEADER): i = 3 ; szV = TEXT ("V5") ; break ; default: Printf (hwnd, TEXT ("Unknown header size of %u.\r\n\r\n"), pbmih->bV5Size) ; free (pFile) ; return ; Printf (hwnd, TEXT ("%s\r\n"), szInfoName[i]) ; // Wyœwietl pola BITMAPCOREHEADER if (pbmih->bV5Size == sizeof (BITMAPCOREHEADER)) fi pbmch = (BITMAPCOREHEADER *) pbmih ; Printf (hwnd, TEXT ("\t.bcSize = %u\r\n"), pbmch->bcSize) ; Printf (hwnd, TEXT ("\t.bcWidth = %u\r\n"), pbmch->bcWidth) ; Printf (hwnd, TEXT ("\t.bcHeight = %u\r\n"), pbmch->bcNeight) ; Printf (hwnd, TEXT ("\t.bcPlanes = %u\r\n"), pbmch->bcPlanes) ; Częœć II: Grafika (cišg dalszy ze strony 677) Printf (hwnd, TEXT ("\t.bcBitCount = %u\r\n\r\n"), pbmch->bcBitCount); free (pFile) ; return ; ) // Wyœwietl pola BITMAPINFOHEADER Printf (hwnd, TEXT ("\t.b%sSize = %u\r\n"), szV, pbmih->bV5Size) ; Printf (hwnd, TEXT ("\t.b%sWidth = %i\r\n"), szV, pbmih->bV5Width); Printf (hwnd, TEXT ("\t.b%sHeight = %i\r\n"), szV, pbmih->bV5Height) ; Printf (hwnd, TEXT ("\t.b%sPlanes = %u\r\n"), szV, pbmih->bV5Planes) ; Printf (hwnd, TEXT ("\t.b%sBitCount = %u\r\n"), szV, pbmih->bV5BitCount) ; Printf (hwnd, TEXT ("\t.b%sCompression = %s\r\n"), szV, szCompression [min (4, pbmih->bV5Compression)]) ; Printf (hwnd, TEXT ("\t.b%sSizeImage = %u\r\n"), szV, pbmih->bV5SizeImage); Printf (hwnd, TEXT ("\t.b%sXPelsPerMeter = %i\r\n"), szV, pbmih->bV5XPelsPerMeter) ; Printf (hwnd, TEXT ("\t.b%sYPelsPerMeter = %i\r\n"), szV, pbmih->bV5YPelsPerMeter) ; Printf (hwnd, TEXT ("\t.b%sClrUsed = %i\r\n"), szV, pbmih->bV5ClrUsed) ; Printf (hwnd, TEXT ("\t.b%sClrImportant = %i\r\n\r\n"), szV, pbmih->bV5ClrImportant) ; if (pbmih->bV5Size == sizeof (BITMAPINFOHEADER)) ( if (pbmih->bV5Compression == BIţBITFIELDS) ( Printf (hwnd, TEXT ("Red Mask = %O8X\r\n"), pbmih->bV5RedMask) ; Printf (hwnd, TEXT ("Green Mask = %08X\r\n"), pbmih->bV5GreenMask> ; Printf (hwnd, TEXT ("Blue Mask = %08X\r\n\r\n"), pbmih->bV5BlueMask) ; ) free (pFile) ; return ; // Wyœwietl dodatkowe pola BITMAPV4HEADER Printf (hwnd, TEXT ("\t.b%sRedMask = %08X\r\n"), szV, pbmih->bV5RedMask) ; Printf (hwnd, TEXT ("\t.b%sGreenMask = %O8X\r\n"), szV, pbmih->bV5GreenMask) ; Printf (hwnd, TEXT ("\t.b%sBlueMask = %08X\r\n"), szV, pbmih->bV5BlueMask) ; Printf (hwnd, TEXT ("\t.b%sAlphaMask = %08X\r\n"), szV, pbmih->bV5AlphaMask) ; Printf (hwnd, TEXT ("\t.b%sCSType = %u\r\n"), szV, pbmih->bV5CSType) ; Printf (hwnd, TEXT ("\t.b%sEndpoints.ciexyzRed.ciexyzX = %08X\r\n"), szV, pbmih->bV5Endpoints.ciexyzRed.ciexyzX) ; Printf (hwnd, TEXT ("\t.b%sEndpoints.ciexyzRed.ciexyzY = %08X\r\n"), szV, pbmih->bV5Endpoints.ciexyzRed.ciexyzY) ; Printf' (hwnd, TEXT ("\t.b%sEndpoints.ciexyzRed.ciexyzZ = %OSX\r\n"), szV, pbmih->bV5Endpoints.ciexyzRed.ciexyzZ) ; Rozdział 15: Bitmapa niezależna od urzšdzeń Printf (hwnd, TEXT ("\t.b%sEndpoints.ciexyzGreen.ciexyzX = %O8X\r\n"), szV, pbmih->bV5Endpoints.ciexyzGreen.ciexyzX) ; Printf (hwnd, TEXT ("\t.b%sEndpoints.ciexyzGreen.ciexyzY = %08X\r\n"), szV, pbmih->bV5Endpoints.ciexyzGreen.ciexyzY) ; Printf (hwnd, TEXT ("\t.b%sEndpoints.ciexyzGreen.ciexyzZ = %08X\r\n"), szV, pbmih->bU5Endpoints.ciexyzGreen.ciexyzZ) ; Printf (hwnd, TEXT ("\t.b%sEndpoints.ciexyzBlue.ciexyzX = %08X\r\n"), szV, pbmih->bV5Endpoints.ciexyzBlue.ciexyzX) ; Printf (hwnd, TEXT ("\t.b%sEndpoints.ciexyzBlue.ciexyzY = %08X\r\n"), szV, pbmih->bV5Endpoints.ciexyzBlue.ciexyzY) ; Printf (hwnd, TEXT ("\t.b%sEndpoints.ciexyzBlue.ciexyzZ = %O8X\r\n"), szU, pbmih->bV5Endpoints.ciexyzBlue.ciexyzZ) ; Printf (hwnd, TEXT ("\t.b%sGammaRed = %08X\r\n"), szV, pbmih->bV5GammaRed) ; Printf (hwnd, TEXT ("\t.b%sGammaGreen = %08X\r\n"), szV, pbmih->bV5GammaGreen) ; Printf (hwnd, TEXT ("\t.b%sGammaBlue = %08X\r\n\r\n"), szV, pbmih->bV5GammaBlue) ; if (pbmih->bV5Size == sizeof (BITMAPV4HEADER)) ( free (pFile) ; return ; 1 // Wyœwietl dodatkowe pola BITMAPV5HEADER Printf (hwnd, TEXT ("\t.b%sIntent = %u\r\n"), szV, pbmih->bV5Intent) ; Printf (hwnd, TEXT ("\t.b%sProfileData = %u\r\n"), szV, pbmih->bV5ProfileData) ; Printf (hwnd, TEXT ("\t.b%sProfileSize = %u\r\n"), szV, pbmih->bU5ProfileSize) ; Printf (hwnd, TEXT ("\t.b%sReserved = %u\r\n\r\n"), szV, pbmih->bV5Reserved) ; free (pFile) ; return ; LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ( static HWND hwndEdit ; static OPENFILENAME ofn ; static TCHAR szFileName CMAXţPATH], szTitleName CMAXţPATH] ; static TCHAR szFilterC] = TEXT ("Bitmap Files (*.BMP)\0*.bmp\0") TEXT ("All Files (*.*)\0*.*\0\0") ; switch (message) ( case WM_CREATE: hwndEdit = CreateWindow (TEXT ("edit"), NULL, WS CHILD ţ WS_VISIBLE ţ WSţBORDER WS VSCROLL ţ WS_HSCROLL ES MULTILINE ţ ES_AUTOVSCROLL ţ ESţREADONLY, 0, 0, 0, 0, hwnd, (HMENU) 1, ((LPCREATESTRUCT) lParam)->hInstance, NULL) ; ofn.lStructSize = sizeof (OPENFILENAME) ; 680 Częœć IIţ Grafika (cišg dalszy ze strony 679) ofn.hwndOwner = hwnd ; ofn:hInstance = NULL ; ofn.lpstrFilter = szFilter ; ofn.lpstrCustomFilter = NULL ; ofn.nMaxCustFilter = 0 ; ofn.nFilterIndex = 0 ; ofn.lpstrFile = szFileName ; ofn.nMaxFile = MAX PATH ; ofn.lpstrFileTitle = szTitleName ; ofn.nMaxFileTitle = MAX_PATH ; ofn.lpstrInitialDir = NULL ; ofn.lpstrTitle = NULL ; ofn.Flags = 0 ; ofn.nFileOffset = 0 ; ofn.nFileExtension = 0 ; ofn.lpstrDefExt = TEXT ("bmp") ; ofn.lCustData = 0 ; ofn.lpfnHook = NULL ; ofn.lpTemplateName = NULL ; return 0 ; case WMţSIZE: MoveWindow (hwndEdit, 0, 0, LOWORD (lParam), HIWORD (lParam), TRUE) ; return 0 ; case WM COMMAND: switch (LOWORD (wParam)) ( case IDM_FILE_OPEN: if (GetOpenFileName (&ofn)) DisplayOibHeaders (hwndEdit, szFileName) ; return 0 ; ) break ; case WM DESTROY: PostOuitMessage (0) ; return 0 ; ) return DefWindowProc (hwnd, message, wParam, lParam) ; ) DIBHEADS.RC (fragmenty) // Microsoft Developer Studio generated resource script. Iţinclude "resource.h" Iţinclude "afxres.h" //////////////////////////////////////////////////////////////////// // Accelerator DIBHEADS ACCELERATORS DISCARDABLE BEGIN "0", IDM FILE OPEN, VIRTKEY, CONTROL, NOINVERT END Rozdział 15: Bitmapa niezależna od urzšdzeń 681 //////////////////////////////////////////////////////////////////// // Menu DIBNEADS MENU DISCARDABLE BEGIN POPUP "&File" BEGIN MENUITEM "&Open\tCtrl+0", IDMţFILE OPEN END END RESOURCE.H (fragmenty) // Microsoft Developer Studio generated include file. // Used by DibHeads.rc #define IDMţFILE OPEN 40001 Rysunek 15-1. Program DIBHEADS Program ten ma krótkš funkcję WndProc, która tworzy okno edycyjne tylko do odczytu, wypełnia jego obszar roboczy, a następnie przetwarza polecenie menu FilebOpen. Program wykorzystuje standardowe okno dialogowe FileţOpen otwierane przez funkcję GetOpenFileName, a następnie wywołuje dużš funkcję DisplayDibHeaders. Funkcja ta wczytuje cały plik DIB do pamięci i pole po polu wyœwietla całš informację z nagłówka. Wyœwietlanie i drukowanie Bitmapy sš po to, żeby je oglšdać. Ten podrozdział rozpocznę od przeglšdu dwóch funkcji dostarczanych przez Windows, a przeznaczonych do wyœwietlania DIB na ekranie monitora lub do drukowania. Chcšc uzyskać lepsze efekty i wyœwie- tlać "mniej kanciaste" bitmapy, będziesz prawdopodobnie wolał stosować jesz- cze innš metodę, którš omówię w dalszej częœci tego rozdziału. Ale logicznie te dwie funkcje stanowiš poczštek. Funkcje te noszš nazwy: SetDIBitsToDevice i StretchDIBits. Każda z nich używa DIB zapisanej w pamięci i może wyœwietlić całš DIB lub jej prostokštny fragment. Gdy stosujesz SetDIBitsToDevice, rozmiar wyœwietlanego obrazu w pikselach bę- dzie taki sam, jak rozmiar DIB w pikselach. Na przykład DIB 640 na 480 pokryje cały twój standardowy ekran VGA, lecz na twojej drukarce laserowej o rozdziel- czoœci 300 dpi będzie miała wymiary około 5,3 na 4 cm. Funkcja StretchDIBits może tak rozcišgać lub zmniejszać rozmiar wierszy i kolumn DIB, aby wyœwietlona bit- mapa miała na urzšdzeniu wyjœciowym jakiœ okreœlony rozmiar. Penetracja wnętrza DIB Gdy wywołujesz jednš z tych dwóch funkcji, aby wyœwietlić DIB, potrzebujesz kilku informacji na temat obrazu. Jak to omówiłem wczeœniej, pliki DIB składajš się z następujšcych częœci: 682 C:ęœć IIţ Grafika Nagtówek pliku Nagtówek informacyjny', Tablica kolorów Bity pikseli Plik DIB może zostać wczytany do pamięci. Jeœli cały plik, bez nagłówka pliku, zapisany jest w cišgłym bloku pamięci, wskaŸnik do poczštku tego bloku (czyli do poczštku nagłówka informacyjnego) można nazwać adresem do upakowanej DIB (patrz rysunek poniżej). Nagtówek informacyjny .: Tablica kolorów ..... , Bity pikseli Jest to format, którego używasz podczas transferowania DIB za poœrednictwem Schowka, a także format, który wykorzystujesz, tworzšc z DIB pędzel. Upako- wana DIB jest dogodnym sposobem zapisania bitmapy w pamięci, ponieważ masz możliwoœć odwołania się do całej DIB za pomocš pojedynczego wskaŸnika (na przykład pPackedDib), który może być zdefiniowany jako wskaŸnik do typu BYT'E. Używajšc definicji struktur omówionych we wczeœniejszej częœci tego rozdziału, możesz uzyskać całš informację zapisanš w DIB, łšcznie z tablicš kolorów i in- dywidualnymi bitami pikseli. Jednak uzyskanie wielu informacji wymaga kilku linii kodu. Nie możesz na przy- kład tak po prostu odczytać szerokoœci obrazu DIB w pikselach za pomocš in- strukcji: iWidth = ((PBITMAPINFOHEADER) pPackedDib)->biWidth ; Możliwe, że DIB jest zapisana w formacie kompatybilnym z OS/2. W tym for- macie upakowana DIB rozpoczyna się od struktury BITMAPCOREHEADER, a szerokoœć i wysokoœć bitmapy w pikselach zapisane sš jako 16-bitowe WORD, a nie jako 32-bitowe LONG. Musisz zatem najpierw sprawdzić, czy DIB jest zapi- sana w starym formacie, a potem odpowiednio postšpić tak: if (((PBITMAPCOREHEADER) pPackedDib)->bcSize == sizeof (BITMAPCOREHEADER)) iWidth = ((PBITMAPCOREHEADER) pPackedDib)->bcWidth ; else iWidth = ((PBITMAPINFOHEADER) pPacked0ib)->biWidth ; Oczywiœcie, ta instrukcja nie jest zła, ale na pewno nie całkiem tak łatwa, jak można się było spodziewać. Rozdział 15: Bitmapa niezależna od urzšdzeń 683 Oto zabawne ćwiczenie: majšc dany wskaŸnik do upakowanej DIB, znajdŸ war- toœć piksela o współrzędnych (5, 27). Nawet jeœli założysz, że DIB nie jest w for- macie kompatybilnym z OS/2, musisz znać szerokoœć, wysokoœć i liczbę bitów DIB. Musisz obliczyć w bajtach długoœć każdego wiersza pikseli, a także okreœlić liczbę elementów tablicy kolorów oraz czy tablica zawiera trzy 32-bitowe maski. Musisz też sprawdzić, czy DIB jest skompresowany, w którym to przypadku nie- możliwe jest bezpoœrednie adresowanie do piksela. Jeżeli potrzebny ci jest bezpoœredni dostęp do wszystkich pikseli DIB (tak jak to się dzieje, gdy wykonujesz wiele działań przetwarzajšcych obraz), może to zajšć sporo czasu przetwarzania. Z tego powodu, o ile utrzymywanie wskaŸnika do upakowanej DIB może być wygodne, na pewno nie idzie w parze ze skuteczno- œciš kodu. Doskonałym rozwišzaniem jest zdefiniowanie klasy C++ dla DIB, za- wierajšcej wystarczajšco wiele danych składowych, aby umożliwić bardzo szyb- ki bezpoœredni dostęp do pikseli bitmapy. Ponieważ jednak na wstępie tej ksišż- ki obiecałem, że nie będzie ci potrzebna znajomoœć C++, w następnym rozdziale pokażę rozwišzanie C. W przypadku funkcji SetDIBitsToDevice i StretchDIBits potrzebnš informacjš jest wskaŸnik do struktury BITMAPINFO bitmapy. Jak pamiętasz, struktura BITMA- PINFO składa się ze struktury BITMAPINFOHEADER oraz tablicy kolorów RGBQUAD. Zatem jest to po prostu odpowiednio przesunięty wskaŸnik do upa- kowanej DIB. Funkcje te wymagajš także wskaŸnika do bitów pikseli. Można do niego dojœć, korzystajšc z danych zawartych w nagłówku informacyjnym, choć kod nie bę- dzie zbyt elegancki. Zauważ, że wskaŸnik ten może zostać obliczony o wiele ła- twiej, gdy masz dostęp do pola bfOfţBits w strukturze BIT'MAPFILEHEADER. Pole bfOff Bits podaje odległoœć od poczštku pliku DIB do bitów pikseli. Mógłbyœ po prostu dodać to przesunięcie do wskaŸnika BITMAPINFO, a następnie odjšć roz- miar struktury BITMAPFILEHEADER. Jednak to ci nie pomoże, jeœli otrzymasz ze Schowka wskaŸnik do upakowanej DIP, ponieważ nie masz struktury BITMAP- FILEHEADER. Ten diagram pokazuje dwa potrzebne wskaŸniki: plnfo -ţ - Nagłówek informacyjny'.. Tablica kolorów pBits -łţ - Bity pikseli Funkcje SetDIBitsToDevice i StretchDIBits wymagajš dwóch wskaŸników do DIB, ponieważ obydwie częœci nie muszš się znajdować w jednym cišgłym bloku pa- mięci. Mógłbyœ mieć dwa bloki pamięci, takie jak te: Częœć IIţ Grafika plnfo .-ł Nagfówek mformacyţny Tablica kolorow t pBits ---ţ. -- Bity pikseli Istotnie, rozbicie DIB na dwa bloki pamięci, takie jak pokazane powyżej, jest doœć przydatne. Ale ponieważ chwilowo wolimy pracować z upakowanymi DIB, cała DIB zapisywana jest w pojedynczym bloku pamięci. Oprócz tych dwóch wskaŸników funkcje SetDIBitsToDevice i StretchDIBits zwy- kle wymagajš także szerokoœci i wysokoœci DIB w pikselach. Jeœli wyœwietlasz tylko częœć bitmapy, nie potrzebujesz akurat tych wartoœci, ale zdefiniujš one górne granice wielkoœci prostokšta, który okreœlasz w tablicy bitów pikseli DIB. Piksel na piksel Funkcja SetDIBitsToDevice wyœwietla DIB bez jakiegokolwiek rozcišgania bšdŸ œcieœniania. Każdy piksel DIB jest odwzorowywany na piksel urzšdzenia wyjœcio- wego. Obraz wyœwietlany zawsze ma prawidłowš orientację - czyli, górny wiersz obrazu znajduje się na górze. Żadne przekształcenia, mogšce mieć wpływ na okre- œlenie przez kontekst urzšdzenia punktu poczštkowego wyœwietlenia DIB, w inny sposób nie wpływajš na rozmiar lub orientację obrazu. Oto funkcja: iLines = SetDIBitsToDevice ( hdc, // uchwyt kontekstu urzšdzenia xDst, // wspólrzędna x celu yDst, // wspólrzędna y celu cxSrc, // szerokoœć prostokšta Ÿródłowego cySrc, // wysokoœć prostokšta Ÿródlowego xSrc, // wspólrzędna x Ÿródla ySrc, // wspólrzędna y Ÿródla yScan, // pierwsza linia skanowania do narysowania cyScans, // liczba linii skanowania do narysowania pBits, // wskaŸnik do bitów pikseli DIB pInfo, // wskaŸnik do informacji o DIB fClrUse) ; // flaga użycia koloru Niech cię nie zbija z tropu liczba argumentów. W większoœci zastosowań funkcja ta jest prostsza w użyciu, niż na to poczštkowo wyglšda. W pozostałych zastoso- waniach... jest to bagno, lecz będziemy nad tym pracować. Jak zwykle przy funkcjach wyœwietlajšcych GDI pierwszym argumentem dla Set- DIBitsToDevice jest uchwyt do kontekstu urzšdzenia, okreœlajšcy urzšdzenie, na którym chcesz wyœwietlić DIB. Kolejne dwa argumenty, xDst i yDst, to logiczne współrzędne urzšdzenia wyjœciowego, podajšce współrzędne punktu, w którym Rozdział 15: Bitmapa niezależna od urzšdzeń 685 ma się pojawić lewy górny róg obrazu DIB. (Mówišc "górny' mam na myœli wi- zualnš górnš częœć obrazu, a nie pierwszy wiersz pikseli w DIB). Zauważ, że sš to współrzędne logiczne, zatem mogš podlegać odwzorowaniu każdego typu, który wolno zastosować, lub - w przypadku Windows NT -jakiejkolwiek trans- formacji, którš okreœlisz. W domyœlnym trybie odwzorowania MM-TEXT, aby wy- œwietlić obraz DIB bezpoœrednio przylegajšcy do lewej strony i górnej krawędzi powierzchni ekranu, należałoby wyzerować oba argumenty. Możesz wyœwietlić cały obraz DIB lub tylko jego częœć. Takie jest przeznaczenie kolejnych czterech argumentów. Jednak właœnie w tym miejscu orientacja do góry nogami danych pikseli DIB wywołuje istnš perwersję, którš pokrótce omówię. Na razie musisz wiedzieć, że aby wyœwietlić całš DIB, podajesz xSrc i ySrc z war- toœciami 0, a cxSrc i cySrc równe, odpowiednio, szerokoœci i wysokoœci bitmapy w pikselach. Zwróć uwagę na pole biHeight struktury BITMAPINFOHEADER. Ponieważ dla DIB z góry na dół będzie ono ujemne, cySrc należy nadać wartoœć bezwzględnš pola biHeight. Dokumentacja tej funkcji (/Platform SDK/Graphics and Multimedia Services/GDI/ Bitmaps/Bitmap Reference/Bitmap Functions/SetDIBitsToDevice) mówi, że argumen- ty xSrc, ySrc, cxSrc, cySrc sš w jednostkach logicznych. To nieprawda, sš współ- rzędnymi i wymiarami w pikselach. Logiczne współrzędne i jednostki pikseli w DIB nie majš sensu. Dodatkowo, niezależnie od trybu odwzorowania, DIB wyœwietlona na urzšdzeniu wyjœciowym zawsze będzie miała cxSrc pikseli sze- rokoœci i cySrc pikseli wysokoœci. Pozwól także, że chwilowo pominę szczegółowe omówienie następnych dwóch argumentów - yScan i cyScan. Argumenty te umożliwiajš zredukowanie wyma- gań pamięci poprzez sekwencyjne wyœwietlanie DIB, po jednym bicie na raz, pod- czas ich odczytywania z pliku dyskowego lub połšczenia modemowego. Zwykle yScan ustawia się na 0, a cyScan przypisywana jest wysokoœć DIB. Argument pBits jest wskaŸnikiem do bitów pikseli DIB. Argument plnfo to wskaŸ- nik do struktury BITMAPINFO bitmapy. Choć adres struktury BITMAPINFO jest taki sam jak adres struktury BITMAPINFOHEADER, definicja funkcji SetDIBits- ToDevice mówi o użyciu struktury BITMAPINFO. Oto subtelna podpowiedŸ: dla DIB 1-, 4- i 8-bitowych po nagłówku informacyjnym musi znajdować się tablica kolorów. Choć jest zdefiniowany jako wskaŸnik do struktury BITMAPINFO, ar- gument ten może być także wskaŸnikiem do struktur: BITMAPCOREINFO, BIT MAPV4HEADER lub BITMAPV5HEADER. Ostatnim argumentem jest DIB RGB COLORS albo DIB-PAL COLORS, zdefiniowa- ny w WINGDI.H jako, odpowiednio, 0 lub 1. Na razie będziesz używał DIB-RGB CO- LORS, co oznacza, że DIB zawiera tablicę kolorów. Flaga DIB-PAL COLORS znaczy, że tablica kolorów w DIB została zastšpiona 16-bitowymi wskaŸnikami do logicz- nej palety kolorów, wybranej i zrealizowanej w kontekœcie urzšdzenia. O tej opcji będziemy się uczyli w następnym rozdziale. Na razie używaj DIB RGB COLORS lub, jeœli jesteœ leniwy, po prostu 0. Funkcja SetDIBitsToDevice zwraca liczbę linii skanowania, które wyœwietla. Zatem, aby wywołać SetDIBitsToDevice w celu wyœwietlenia całego obrazu DIB, potrzebne sš następujšce informacje: 686 Częœć II: Grafika ů hdc - uchwyt kontekstu urzšdzenia powierzchni docelowej ů xDst i yl7st - współrzędne lewego górnego rogu obrazu docelowego ů cxDib i cyDib - szerokoœć i wysokoœć DIB w pikselach, gdzie cyDib jest warto- œciš bezwzględnš pola biHeight w strukturze BITMAPINFOHEADER ů plnfo i pBits - wskaŸniki do sekcji informacyjnej bitmapy oraz bitów pikseli. Następnie, w taki sposób wywohxjesz funkcję SetDIBitsToDevice: SetDIBitsToDevice ( hdc, // uchwyt kontekstu urzddzenia xDst, // współrzędna x celu yDst, // współrzędna y celu cxDib, // szerokoœć prostokdta Ÿródłowego cyDib, // wysokoœć prostokšta Ÿródłowego 0, // współrzędna x Ÿródła 0, // współrzędna y Ÿródła 0, // pierwsza linia skanowania do narysowania cyDib, // liczba linii skanowania do narysowania pBits, // wskaŸnik do bitów pikseli DIB pInfo, // wskaŸnik do informacji o DIB 0) ; // flaga użycia koloru Zatem z dwunastu argumentów DIB czterem najczęœciej przypisywana jest war- toœć 0, a jeden się powtarza. Program SHOWDIBI zaprezentowany na rysunku 15-2 wyœwietla DIB przy użyciu funkcji SetDIBitsToDevice. SHOWDIBl.C /* SHOWDIBl.C - Wyœwietla DIB w obszarze roboczym okna (c) Charles Petzold, 1998 */ itinclude #include "dibfile.h" iţinclude "resource.h" LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; TCHAR szAppNameC] = TEXT ("ShowDibl") ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hP.revInstance, PSTR szCmdLine, int iCmdShow) HACCEL hAccel ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CS VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITEţBRUSH) Rozdział 15: Bitmapa niezależna od urzšdzeń gg7 wndclass.lpszMenuName = szAppName ; wndclass.lpszClassName = szAppName ; if (!Re9isterClass (&wndclass)) g Messa eBox (NULL, TEXT ("This program requires Windows NT "), szAppName, MBţICONERROR) ; return 0 ; ) hwnd = CreateWindow (szAppName, TEXT ("Show DIB ţţl"), WS_OVERLAPPEDWINDOW. CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; hAccel = LoadAccelerators (hInstance, szAppName) ; while (GetMessage (&msg, NULL, 0, 0)) ( if (!TranslateAccelerator (hwnd, hAccel, &msg)) ( TranslateMessage (&msg) ; DispatchMessage (&msg) ; l ) return msg.wParam ; LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ( static BITMAPFILEHEADER * pbmfh ; static BITMAPINFO * pbmi ; static BYTE * pBits ; static int cxClient, cyClient, cxDib, cy0ib ; static TCHAR szFileName CMAXţPATH), szTitleName CMAX_PATH] ; BOOL bSuccess ; HDC hdc ; PAINTSTRUCT ps ; switch (message) ( case WM_CREATE: DibFileInitialize (hwnd) ; return 0 ; case WM_SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; return 0 ; case WM_INITMENUPOPUP: EnableMenuItem ((HMENU) wParam, IDMţFILE_SAVE, pbmfh ? MF_ENABLED : MF_GRAYED) ; return 0 ; 688 Częœć II: Grafika (cišg datszy ze strony 687) case WM COMMAND: switch (LOWORD (wParam)) ( case IDMţFILE_OPEN: // Wyœwietl okno dialogowe Otwórz if (!DibFileOpenDlg (hwnd, szFileName. szTitleName)) return 0 ; // Jeœli istnieje DIB, zwolnij pamięć if (pbmfh) { free (pbmfh) ; pbmfh = NULL ; ) // Wczytaj do pamięci caly DIB SetCursor (LoadCursor (NULL, IDC WAIT)) ; ShowCursor (TRUE) ; pbmfh = DibLoadImage (szFileName) ; ShowCursor (FALSE) ; SetCursor (LoadCursor (NULL, IDC ARROW)) ; // Wyczyœć obszar roboczy okna // przed następnš aktualizacjš InvalidateRect (hwnd, NULL, TRUE) ; if (pbmfh = NULL) ( MessageBox (hwnd, TEXT ("Cannot load DIB file"), szAppName, 0) : return 0 ; ) // WeŸ wskaŸniki do struktury informacyjnej // oraz do bitów pikseli pbmi = (BITMAPINFO *) (pbmfh + i) ; pBits = (BYTE *) pbmfh + pbmfh->bfOffBits ; // Odczytaj szerokoœć i wysokoœć bitmapy if (pbmi->bmiHeader.biSize == sizeof (BITMAPCOREHEADER)) ( cxDib = ((BITMAPCOREHEADER *) pbmi)->bcWidth ; cy0ib = ((BITMAPCOREHEADER *) pbmi)->bcHeight ; ů else r cx0ib = pbmi->bmiHeader.biWidth ; cyDib = abs (pbmi->bmiHeader.biHeight) ; I return 0 ; Rozdział 15: Bitmapa niezależna od urzšdzeń 689 case IDM_FILE_SAVE: // Wyţwietl okno dialogowe Zapisz jako if (!DibFileSaveDlg (hwnd, szFileName, szTitleName)) return 0 ; // Zapisz DIB w pamięci ,.i SetCursor (LoadCursor (NULL, IDC WAIT)) ; ShowCursor (TRUE) ; bSuccess = DibSaveImage (szFileName, pbmfh) ; ShowCursor (FALSE) ; ił' SetCursor (LoadCursor (NULL, IDC ARROW)) ; if (!bSuccess) ţţł MessageBox (hwnd, TEXT ("Cannot save DIB file"), szAppName, 0) ; i,. break ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; if (pbmfh) SetDIBitsToDevice (hdc, ' 0, // xDst 0, // yDst cx0ib, // cxSrc cyDib, // cySrc 0, // xSrc 0, // ySrc 0, // pierwsza linia skanowania cy0ib, // liczba skanowanych linii pBits. pbmi, DIBţRGB COLORS) ; EndPaint (hwnd, &ps) ; return 0 ; k,. case WM_DESTROY: if (pbmfh) free (pbmfh) ; ţ... PostOuitMessage (0) ; return 0 ; ů" a... return DefWindowProc (hwnd, message, wParam, lParam) ; I DIBFILE.H ,* DIBFILE.H - Plik naglówkowy dla DIBFILE.C */ 690 Częœć II: Grafika (cišg dalszy ze strony 689) void DibFileInitialize (HWND hwnd) ; BOOL DibFileOpenDlg (HWND hwnd, PTSTR pstrFileName, PTSTR pstrTitleName) ; BOOL DibFileSaveDlg (HWND hwnd, PTSTR pstrFileName, PTSTR pstrTitleName) ; BITMAPFILEHEADER * DibLoadImage (PTSTR pstrFileName) ; BOOL DibSaveImage (PTSTR pstrFileName, BITMAPFILEHEADER *) ; DIBFILE.C /* DIBFILE.C - Funkcje plikowe DIB */ llinclude 4linclude ţlinclude "dibfile.h" static OPENFILENAME ofn ; void DibFileInitialize (HWND hwnd) ( static TCHAR szFilter[] = TEXT ("Bitmap Files (*.BMP)\0*.bmp\0") \ TEXT ("All Files (*.*)\0*.*\0\0") ; ofn.lStructSize = sizeof (OPENFILENAME) ; ofn.hwndOwner = hwnd ; ofn.hInstance = NULL ; ofn.lpstrFilter = szFilter ; ofn.lpstrCustomFilter = NULL ; ofn.nMaxCustFilter = 0 ; ofn.nFilterIndex = 0 ; ofn.lpstrFile = NULL ; // Ustawione w funkcjach Open i Close ofn.nMaxFile = MAX_PATH ; ofn.lpstrFileTitle = NULL ; // Ustawione w funkcjach Open i Close ofn.nMaxFileTitle = MAXţPATH ; ofn.lpstrInitialDir = NULL ; ofn.lpstrTitle = NULL ; ofn.Flags = 0 ; // Ustawione w funkcjach Open i Close ofn.nFileOffset = 0 ; ofn.nFileExtension = 0 ; ofn.lpstrDefExt = TEXT ("bmp") ; ofn.lCustOata = 0 ; ofn.lpfnHook = NULL ; ofn.lpTemplateName = NULL ; ) BOOL DibFileOpenDlg (HWND hwnd, PTSTR pstrFileName, PTSTR pstrTitleName) ofn.hwndOwner = hwnd ; ofn.lpstrFile = pstrFileName ; ofn.lpstrFileTitle = pstrTitleName ; ofn.Flags = 0 ; return GetOpenFileName (&ofn) ; } BOOL DibFileSaveDl9 (HWND hwnd, PTSTR pstrFileName, PTSTR pstrTitleName) Rozdział 15: Bitmapa niezależna od urzšdzeń 691 ( ofn.hwndOwner = hwnd ; ofn.lpstrFile = pstrFileName ; ofn.lpstrFileTitle = pstrTitleName ; ofn.Flags = OFN OVERWRITEPROMPT ; F r. .- return GetSaveFileName (&ofn) ; BITMAPFILEHEADER * DibLoadImage (PTSTR pstrFileName) BOOL bSuccess ; DWORD dwFileSize, dwHighSize, dwBytesRead ; HANDLE hFile ; BITMAPFILEHEADER * pbmfh ; 'I 1;'.. hFile = CreateFile (pstrFileName, GENERICţREAD FILE_SHARE_READ, NULL, OPENţEXISTING, FILEţFLAG SEOUENTIALţSCAN, NULL) ; if (hFile = INVALID HANDLE VALUE) return NULL ; dwFileSize = GetFileSize (hFile, &dwHighSize) ; if (dwHighSize) ( CloseHandle (hFile) ; return NULL ; 1 pbmfh = malloc (dwFileSize) ; if (!pbmfh) CloseHandle (hFile) ; return NULL ; bSuccess = ReadFile (hFile, pbmfh, dwFileSize, &dwBytesRead, NULL) ; CloseHandle (hFile) ; if (!bSuccess ţţ (dwBytesRead != dwFileSize) ţţ (pbmfh->bfType != * (WORD *) "BM") ţţ (pbmfh->bfSize != dwFileSize)) ( free (pbmfh) ; ţ,,. return NULL ; I return pbmfh ; ) BOOL DibSaveImage (PTSTR pstrFileName, BITMAPFILEHEADER * pbmfh) BOOL bSuccess ; DWORD dwBytesWritten ; HANDLE hFile ; hFile = CreateFile (pstrFileName, GENERIC_WRITE, 0, NULL, CREATE ALWAYS, FILE ATTRIBUTE NORMAL. NULL) ; 692 Częœć II: Grafika (cišg dalszy ze strony 691) if (hFile == INVALID HANDLE VALUE) return FALSE ; bSuccess = WriteFile (hFile, pbmfh, pbmfh->bfSize, &dwBytesWritten, NULL) ; CloseHandle (hFile) ; if (!bSuccess ţţ (dwBytesWritten != pbmfh->bfSize)) ( DeleteFile (pstrFileName) ; return FALSE ; ) return TRUE ; ) SHOWDIBl.RC (wyjštki) // Microsoft Developer Studio generated resource script. ţlinclude "resource.h" Ilinclude "afxres.h" ///////////////////////////////////////////////////////////////////////////// // Menu SHOWDIB1 MENU DISCARDABLE BEGIN POPUP "&File" BEGIN MENUITEM "&Open... , IDM_FILE_OPEN MENUITEM "&Save... , IDM FILEţSAVE END END RESOURCE.H (fragmenty) // Microsoft Developer Studio generated include file. // Used by ShowDibl.rc ţţdefine IDM_FILE_OPEN 40001 ţIdefine IDMţFILEţSAVE 40002 Rysunek 15-2. Program SHOWDIBI Plik DIBFILE.C zawiera procedury służšce do wyœwietlania okien dialogowych FileţOpen (Otwórz) oraz FilebSave (Zapisz jako), a także do wczytywania pli- ku DIB (w całoœci, łšcznie ze strukturš BIT'MAPFILEHEADEIZ) do pojedynczego bloku pamięci. Program może także zapisać taki blok pamięci w pliku. Po wczytaniu pliku DIB, podczas przetwarzania polecenia FileţaOpen program Rozdział 15: Bitmapa niezależna od urzšdzeń 693 SHOWDIBl.C oblicza offsety struktury BITMAPINFOHEADER i bitów pikseli w bloku pamięci. Następnie odczytuje także szerokoœć i wysokoœć DIB, w pikse- lach. Wszystkie te informacje zapisywane sš na zmiennych statycznych. W cza- sie przetwarzania komunikatu WMţPAINT program wyœwietla DIB wywołujšc funkcję SetDIBitsToDevice. Oczywiœcie w SHOWDIB1 brakuje kilku cech. Jeœli bitmapa jest na przykład zbyt duża w stosunku do obszaru roboczego okna, nie pojawiajš się paski przewija- nia, pozwalajšce poruszać się po obrazie. Wady te zostanš naprawione przed za- kończeniem następnego rozdziału. Postawiony na głowie œwiat DIB Przed nami ważna lekcja, a dotyczy ona nie tylko życia, ale także projektowania interfejsów aplikacji dla systemów operacyjnych. Jej konkluzja brzmi: jeœli coœ sknocisz na poczštku, to napsujesz jeszcze bardziej, gdy spróbujesz to naprawić. Dawniej, gdy w Presentation Manager OS/2 rodziła się definicja bitów pikseli do góry nogami, miała wówczas w pewnym stopniu sens, ponieważ wszystko w PM miało swój poczštek w lewym dolnym rogu. Na przykład w oknie PM domyœlny poczštek (0, 0) jest w lewym dolnym narożniku okna. Qeœli brzmi to dla ciebie dziwnie, nie jesteœ osamotniony. Jeżeli brzmi znajomo, prawdopodobnie jesteœ matematykiem). Funkcje rysujšce bitmapę także okreœlały punkty docelowe, po- sługujšc się "lewymi dolnymi" współrzędnymi. Tak więc jeœli w OS/2 okreœliłeœ docelowe współrzędne poczštku bitmapy w punk- cie (0, 0), to pojawiajšcy się obraz przylegał do lewej krawędzi i do dołu okna, tak jak na rysunku 15-3. Rysunek 15-3. W systemie OS/2 tak zostałaby wyœwietlona bitmapa w punkcie docelowym (0, 0) 694 Częœć II: Grafika Na doœć powolnej maszynie mógłbyœ zaobserwować, jak bitmapa jest rysowana - z dołu do góry. Choć układ współrzędnych w OS/2 może wydawać się dziwny, ma tę zaletę, że jest wysoce spójny. Poczštek (0, 0) bitmapy to pierwszy piksel w pierwszym wier- szu pliku bitmapy, a piksel ten zostaje odwzorowany we współrzędnych docelo- wych, okreœlonych w funkcjach rysujšcych bitmapę. W Windows problem polega na tym, że wewnętrzna spójnoœć nie została zacho- wana. Jeżeli chcesz wyœwietlić jedynie prostokštny fragment całego obrazu DIB, używasz argumentów xSrc, ySrc, cxSrc i cySrc. Te Ÿródłowe współrzędne i wy- miary okreœlone sš względem pierwszego wiersza danych DIB, a wierszem tym jest dolny wiersz obrazu, czyli tak samo jak w OS/2. Jednak w odróżnieniu od OS/2 Windows wyœwietla we współrzędnych docelowych górny wiersz obrazu. Stšd jeœli wyœwietlasz cały obraz DIB, piksel wyœwietlany we współrzędnych (xDst, yDst) jest pikselem DIB o współrzędnych (0, cyDib - 1). Jest to ostatni wiersz da- nych DIB, lecz górny wiersz obrazu. Jeżeli wyœwietlasz jedynie częœć obrazu, piksel wyœwietlony w punkcie (xDst, yDst) jest pikselem DIB o współrzędnych (xSrc, ySrc + cySrc - 1). IZysunek 15-4 przedstawia diagram, który może ci pomóc to zrozumieć. DIB po- kazana poniżej ułożona jest tak, jak możesz jš sobie wyobrazić zachowanš w pa- mięci - to znaczy do góry nogami. Poczštek układu, od którego mierzone sš współ- rzędne pokrywa się z danymi bitu pierwszego piksela w DIB. Argument xSrc funkcji SetDIBitsToDevice mierzony jest od lewej strony bitmapy, a cxSrc to szero- koœć obrazu i mierzony jest na prawo od xSrc. To proste. Argument ySrc odmie- rzany jest od pierwszego wiersza danych DIB (to znaczy od dołu obrazu), a cySrc jest wysokoœciš obrazu mierzonš od ySrc w kierunku ostatniego wiersza danych (górnego wiersza obrazu). ySrc + Rysunek 15-4. Okreœlanie współrzędnych obrazu DIB dla zwykłych (do góry no- gami) DIB Jeœli kontekst urzšdzenia docelowego ma domyœlne współrzędne pikseli wyru- kajšce z użycia trybu odwzorowania MM-T'EXT, relacja pomiędzy współrzędny- xSrc xSrc + cxSrc Rozdział 15: Bitmapa niezależna od urzšdzeń 695 mi narożników w prostokštach Ÿródłowym i docelowym będzie taka, jak poka- zana w poniższej tabeli: Prostokšt Ÿródłowy Prostokšt docelowy (xSrc, ySrc) (xDst, yDst + cySrc - 1)) (xSrc + cxSrc - 1, ySrc) (xDst + cxSrc - 1, yDst + cySrc - 1) (xSrc, ySrc + cySrc - 1) (xDst, yDst) (xSrc + cxSrc - 1, ySrc + cySrc - 1) (xDst + cxSrc - 1, yDst) Cały ten bałagan został spowodowany tym, że (xSrc, ySrc) nie jest odwzorowane na (xDst, yDst). Dla każdego innego trybu odwzorowania, punkt (xSrc, ySrc + cySrc - 1) będzie niezmiennie odwzorowywany na logiczny punkt (xDst, yDst) i obraz będzie wyglšdał tak samo jak w MM TEXT. Dotychczas omawiałem typowy przypadek, gdy pole biHeight struktury BITMAP- INFOHEADER jest dodatnie. Jeœli pole biHeight ma wartoœć ujemnš, dane DIB zorganizowane sš w sposób racjonalny, czyli z góry na dół. Jeœli wierzysz, że to rozwišże problem, jesteœ bardzo naiwny. Wyglšda to tak, jakby ktoœ zadecydował, że jeœli weŸmiesz DIB z góry na dół, odwrócisz wszystkie wiersze, a następnie nadasz polu biHeight wartoœć dodat- niš, mechanizm zadziała jak normalna DIB do góry nogami. Nie powinna być potrzebna modyfikacja żadnego kodu odwołujšcego się do prostokšta w DIB. My- œlę, że pomysł jest rozsšdny, lecz nie bierze pod uwagę tego, że programy muszš być w jakiœ sposób zmodyfikowane do obsługi DIB z góry na dół, by nie żywały ujemnej wysokoœci. Ostatecznie konsekwencje tej decyzji sš dziwne: współrzędne DIB z góry na dół majš poczštek w ostatnim wierszu danych DIB, będšcym zarazem dolnym wier- szem obrazu. Jest to sytuacja całkowicie odmienna od dotychczasowych. Piksel DIB w poczštku (0,0) nie jest już pierwszym pikselem wskazywanym przez wskaŸ- nik pBits. Nie jest też ostatnim pikselem w pliku DIB. Znajduje się gdzieœ pomię- dzy nimi. Rysunek 15-5 pokazuje diagram wyjaœniajšcy, jak okreœla się prostokšt w DIB z gó- ry na dół. I tym razem pokazano DIB tak, jak jest zachowana w pamięci: 696 Częœć II: Grafika ySrc + cySrc ySrc Rysunek 15-5. Okreœlanie współrzędnych DIB dla DIB z góry na dół Rzeczywista zaleta schematu polega na tym, że argumenty funkcji SetDIBitsTo- Device sš niezależne od orientacji danych DIB. Jeœli masz dwie DIB (jednš do góry nogami, a drugš z góry na dół), pokazujšce ten sam obraz (co oznacza, że kolej- noœć wierszy w tych dwóch plikach DIB jest przeciwna), to wybranie tego same- go fragmentu obrazu do wyœwietlenia wymţaga użycia identycznych argumen- tbw dla SetDIBitsToDevice. Zostało to zademonstrowane w programie APOLLOll, przedstawionym na ry- sunku 15-6. APOLLOII.C /* APOLLOll.C - Program œcidgajdcy ekrany (c) Charles Petzold, 1998 */ iţinclude #include "dibfile.h" LRESULT CALLBACK WndProc (HWND, UINT. WPARAM. LPARAM) ; TCHAR szAppName[) ţ TEXT ("Apolloll") ; int WINAPI WinMain (HINSTANCE hInstance. HINSTANCE hPrevInstance, PSTR szCmdLine. int iCmdShow) HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CS VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; xSrc xSrc + cxSrc Rozdział 15: Bitmapa niezależna od urzšdzeń 697 wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; `ř wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITEţBRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MBţICONERROR) ; return 0 ; hwnd = CreateWindow (szAppName, TEXT ("Apollo il"), WS_OIIERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 'I:.' CW_USEDEFAULT, CW_USEDEFAULT, I NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; ,ţ.:-: UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) TranslateMessage (&msg) ; DispatchMessage (&msg) ; ) return msg.wParam ; ) LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) static BITMAPFILEHEADER * pbmfh [2] ; static BITMAPINFO * pbmi [2] ; static BYTE * pBits (2] ; static int cxClient, cyClient, cx0ib [2], cyDib [2] ; ` HDC hdc : i PAINTSTRUCT ps ; i switch (message) ţ ' case WM_CREATE: pbmfh[0] = DibLoadImage (TEXT ("Apolloll.bmp")) ; i pbmfh(1] = DibLoadImage (TEXT ("ApolloTD.bmp")) ; if (pbmfh[0] == NULL ţţ pbmfh(1] = NULL) f MessageBox (hwnd, TEXT ("Cannot load DIB file"), szAppName, 0) ; return 0 ; ) // WeŸ wskaŸnik do struktury informacyjnej // i do bitów pikseli pbmi [0] = (BITMAPINFO *) (pbmfh[0] + i) ; pbmi [1] = (BITMAPINFO *) (pbmfh[1] + 1) ; pBits [0] = (BYTE *) pbmfh[0] + pbmfh[0]->bfOffBits ; pBits [1] = (BYTE *) pbmfh[1] + pbmfh[1]->bfOffBits ; 698 Częœć II: Grafika (cišg dalszy ze strony 697) // Odczytaj szerokoœć i wysokoœć DIB // (zakładamy BITMAPINFOHEADER) // Zauważ, że cyDib jest wartoœcid bezwzględnd // z wartoœci odczytanej z nagłówka!!! cxDib CO] = pbmi[0]->bmiHeader.biWidth ; cxDib C1] = pbmi[1]->bmiHeader.biWidth ; cyDib CO] = abs (pbmi[0]->bmiHeader.biHeight) ; cyDib C1] = abs (pbmi[1]->bmiHeader.biHeight) ; return 0 ; case WM_SIZE: cxClient = LOWORD (lParam) cyClient = HIWORD (lParam) return 0 : case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; // Calkowity rozmiar DIB do góry nogami SetDIBitsToDevice (hdc, 0, // xDst cyClient / 4, // yDst cx0ib[0], // cxSrc cyDib[0], // cySrc 0, // xSrc 0, // ySrc 0, // pierwsza linia skanowania cyDib[0], // liczba skanowanych linii pBitsCO], pbmi[0], DIB RGB COLORS) ; // Fragment DIB do góry nogami SetDIBitsToDevice (hdc, 240, // xDst cyClient / 4, // yDst 80, // cxSrc 166, // cySrc 80, // xSrc 60, // ySrc 0, // pierwsza linia skanowania cyDibCO], // liczba skanowanych linii pBitsCO], pbmiCO], DIB RGB COLORS) ; // Calkowity rozmiar DIB z góry na dól SetDIBitsToDevice (hdc, 340, // xDst cyClient / 4, // yDst cxDib[0], // cxSrc Rozdział 15: Bitmapa niezależna od urzšdzeń ggg cyDibCO7, // cySrc 0, // xSrc ;= 0. // ySrc ;',ţ. 0, // pierwsza linia skanowania cyDib[0], // liczba skanowanych linii ! pBitsCO], pbmiC07, ţ. DIB RGB COLORS) ; // Fragment DIB z góry na d61 SetDIBitsToDevice (hdc, I, 580, // xDst i . cyClient / 4, // y0st 80, // cxSrc i- 166. // cySrc 80. // xSrc 60. // ySrc ţ. 0, // pierwsza linia skanowania cyDibCl7, // liczba skanowanych linii ţ" pBitsClJ, pbmiCl7, ;., DIBţRGB COLORS) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: if (pbmfhC07) free (pbmfhCO]) ; if (pbmfhCl]) free (pbmfh[17) ; PostOuitMessage (0) ; return 0 ; t return DefWindowProc (hwnd, message, wParam, lParam) ; Rysunek 15-6. Program APOLLOl1 Program wczytuje dwie wersje DIB, o nazwach APOLLOll.BMP (do góry noga- mi) i APOLLOTD.BMP (z góry na dół). Obie majš 220 pikseli szerokoœci i 240 wysokoœci. Zauważ, że gdy program okreœla szerokoœć i wysokoœć DIB na pod- stawie nagłówkowej struktury informacyjnej, używa funkcji abs, aby znać war- toœć bezwzględnš pola biHeight. Podczas wyœwietlania DIB w całoœci lub we frag- mentach, współrzędne xSrc, ySrc, cxSrc i cySrc sš identyczne niezależnie od tego, która bitmapa jest wyœwietlana. Wyniki pokazuje rysunek 15-7. ţoo Częœć II: Grafika Rysunek 15-7. Ekran APOLLOl1 Zauważ, że argumenty "pierwsza linia skanowania' i "liczba skanowanych li- n" pozostajš niezmienione. Wkrótce do tego wrócę. Argument pBits także po- zostaje taki sam. Nie próbuj zmieniać pBits tak, żeby wskazywał jedynie ten ob- szar DIB, który chcesz wyœwietlić. Poœwięcam temu zagadnieniu wiele czasu nie dlatego, że chcę dogryŸć twórcom Windows za ich próby jak najlepszego uporzšdkowania problematycznych ob- szarów definicji API, ale po to, żebyœ się nie denerwował, jeœli zostaniesz zbity z tropu. To musi być mylšce, bo jest naprawdę pomylone. Chcę także, żebyœ był czujny i nie wierzył œlepo niektórym stwierdzeniom z do- kumentacji Windows. Na przykład o funkcji SetDIBitsToDevice możesz przeczy- tać, że poczštkiem DIB do góry nogami jest lewy dolny róg bitmapy; natomiast poczštkiem DIB z góry na dół - lewy górny róg. To jest nie tylko niejednoznacz- ne, to jest wręcz błędne. IZóżnicę moglibyœmy lepiej opisać w taki sposób: "Po- czštkiem DIB do góry nogami jest lewy dolny róg obrazu bitmapowego, a jest to pierwszy piksel pierwszego wiersza danych bitmapy. Poczštkiem DIB z góry na dół jest także dolny lewy róg obrazu bitmapowego, lecz w tym wypadku lewy dolny róg jest pierwszym pikselem ostatniego wiersza danych bitmapy". Problem nasila się wtedy, kiedy potrzebujesz napisać funkcję, która pozwala two- im programom na dostęp do indywidualnych bitów DIB. Powinna ona być spój- na ze sposobem okreœlenia współrzędnych fragmentu obrazu DIB, który ma być wyœwietlony. W moim rozwišzaniu (które zaimplementuję w bibliotece DIB w rozdziale 16) jednolicie odwołuję się do pikseli i współrzędnych DIB. Poczštek (0,0) oznacza skrajny lewy piksel górnego wiersza obrazu DIB, tak jak na niego patrzymy, gdy jest prawidłowo wyœwietlony. Rozdział 15: Bitmapa niezależna od urzšdzeń 701 Wyœwietlanie sekwencyjne Dostępnoœć dużej iloœci pamięci na pewno ułatwia programowanie. Wyœwietle- nie DIB zapisanej w pliku dyskowym można rozbić na dwa niezależne zadania: wczytanie DIB do pamięci oraz jej wyœwietlenie. Mogš jednak zdarzyć się sytuacje, w których chciałbyœ wyœwietlić DIB bez ko- niecznoœci wczytania całego pliku do pamięci. Nawet jeœli jest jej na tyle dużo, żeby zmieœciła się cała bitmapa, przeniesienie DIB do pamięci może wymusić na systemie pamięci wirtualnej Windows przerzucenie innego kodu lub danych na dysk twardy. Będzie to wandalizm, szczególnie gdy DIB ma być tylko wyœwie- tlona, a potem natychmiast wyrzucona z pamięci. Oto inny problem: załóżmy, że DIB jest zapisana na jakimœ wolnym noœniku, ta- kim jak dyskietka, lub przesyłana przez modem. Może też pochodzić z procedu- ry konwersji, która uzyskuje dane o pikselach ze skanera lub urzšdzenia œcišga- jšcego fragmenty ekranu. Czy chcesz czekać, aż do pamięci zostanie wczytana cała DIB, zanim jš wyœwietlisz? Czy może raczej wyœwietlałbyœ DIB sekwencyj- nie, w miarę napływania informacji z dysku, linii telefonicznej lub skanera? Rozwišzanie tych problemów jest zadaniem argumentów yScan i cyScans funkcji SetDIBitsToDevice. Chcšc poshxżyć się tš cechš, wykonujesz serię wywołań SetDI- BitsToDevice, zasadniczo z tymi samymi argumentami. Jednak dla każdegó wy- wołania argument pBits wskazuje różne częœci tablicy pikseli bitmapy. Argument yScan okreœla wiersz pikseli wskazywany przez pBits, argument cyScans zaœ to liczba wierszy, do której odnosi się pBits. To znacznie zmniejsza wymagania pa- mięciowe. Wystarczy alokować tyle pamięci, żeby przechować sekcję informa- cyjnš DIB (strukturę BITMAPINFOHEADER i tablicę kolorów) oraz przynajmniej jeden wiersz danych pikseli. Załóżmy, że DIB ma 23 wiersze pikseli. Ty chcesz wyœwietlić DIB blokami po 5 wierszy. Prawdopodobnie alokujesz blok pamięci, wskazany przez zmiennš plnfo w celu zapisania sekcji BTTMAPINFO bitmapy. Następnie wczytujesz jš z pliku. Po zbadaniu pól tej struktury możesz obliczyć dhxgoœć wiersza w bajtach. Pomnóż jš przez 5 i zarezerwuj inny blok pamięci tego rozmiaru (pBits). Teraz odczytaj pierw- sze 5 wierszy. Wywołaj funkcje tak, robisz to zwykle, lecz dla yScan równego 0 i cyScans równego 5. Następnie odczytaj kolejne 5 wierszy z pliku. Tym razem na- daj yScan wartoœć 5. Kontynuuj dla yScan równego 10, a potem 15. Na koniec od- czytaj ostatnie 3 wiersze do bloku adresowanego przez pBits oraz wywołaj SetDI- BitsToDevice z yScan równym 20 i cyScans równym 3. Sš jednak i złe nowiny. Po pierwsze, użycie SetDIBitsToDevice w taki sposób wy- maga dobrego zgrania procesu zdobywania danych z elementami prezentujšcy- mi dane w twoim programie. Jest to zwykle niewdzięczne zadanie, bo musisz na przemian przechodzić między pozyskiwaniem danych i ich pokazywaniem, przez co spowolnisz cały proces. Po drugie SetDIBitsToDevice jest jedynš funkcjš wy- œwietlajšcš bitmapy majšcš tę umiejętnoœć. Jak się przekonamy, funkcja Stretch- DIBits nie potrafi tego robić, zatem nie możesz jej użyć do wyœwietlania napły- , wajšcych danych DIB z innym rozmiarem w pikselach. Musiałbyœ jš "oszukać" wywołujšc wielokrotnie StretchDIBits, za każdym razem zmieniajšc informacje Częœć II Grafika i w strukturze BITMAPINFOHEADER i wyœwietlajšc wynik w różnych obszarach na ekranie. Program SEQDISP z rysunku 15-8 demonstruje zastosowanie tej umiejętnoœci. SEQDISP.C i* SEODISP.C - Sekwencyjne wyœwietlanie bitmap (c) Charles Petzold, 1998 *ţ I ţţinclude ţţinclude "resource.h" LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; TCHAR szAppNameC) = TEXT ("SeqDisp") ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) ( HACCEL hAccel ; HWND hwnd :. MSG msg ; WNDCLASS wndclass ; wndclass.style = CS HREDRAW ţ CSţVREDRAW ; wndclass.7pfnWndProc = WndPr”c ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = szAppName ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) ( MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MB ICONERROR) ; return 0 ; hwnd = CreateWindow (szAppName, TEXT ("DIB Sequential Display"), WSţOVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CWţUSEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; hAccel = LoadAccelerators (hInstance, szAppName) ; while (GetMessage (&msg, NULL, 0, 0)) Rozdział 15: Bitmapa niezależna od urzšdzeń 703 ( if (!TranslateAccelerator (hwnd, hAccel, &ms9)) ( TranslateMessage (&msg) : DispatchMessage (&msg) : ) return msg.wParam ; LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) I static BITMAPINFO * pbmi ; static BYTE * pBits ; static int cxDib, cyDib, cBits ; static OPENFILENAME ofn ; static TCHAR szFileName CMAXţPATH], szTitleName CMAX PATH] ; static TCHAR szFilterC] = TEXT ("Bitmap Files (*.BMP)\0*.bmp\0") TEXT ("Al1 Files (*.*)\0*.*\0\0") ; BITMAPFILEHEADER bmfh ; BOOL bSuccess, bTopDown ; DWORD dwBytesRead ; HANDLE hFile ; HDC hdc ; HMENU hMenu ; int iInfoSize, iBitsSize, iRowLength, y ; PAINTSTRUCT ps ; switch (message) case WM_CREATE: ofn.lStructSize = sizeof (OPENFILENAME) ; ofn.hwndOwner = hwnd : '' ofn.hInstance = NULL ; ofn.lpstrFilter = szFilter ; ofn.lpstrCustomFilter = NULL ; i ofn.nMaxCustFilter = 0 ; : ofn.nFilterIndex = 0 ; ofn.lpstrFile = szFileName ; ofn.nMaxFile = MAXţPATH ; ofn.lpstrFileTitle = szTitleName ; a,. ofn.nMaxFileTitle = MAXţPATH ; ofn.lpstrInitialDir = NULL ; ofn.lpstrTitle = NULL ; ofn.Fla9s = 0 ; ofn.nFileOffset = 0 ; ofn.nFileExtension = 0 ; ofn.lpstrOefExt = TEXT ("bmp") ; ofn.lCustData = 0 ; ofn.lpfnHook = NULL ; ofn.lpTemplateName = NULL ; return 0 ; case WM COMMAND: hMenu = GetMenu (hwnd) ; switch (LOWORD (wParam)) ( Częœć 11ţ Grafika (cišg dalszy ze strony 703) case IDMţFILE_OPEN: // Wyœwietl okno dialogowe Otwórz if (!GetOpenFileName (&ofn)) return 0 ; // Zwolnij stard DIB if (pbmi) i free (pbmi) ; pbmi = NULL ; ) if (pBits) ( free (pBits) ; pBits = NULL ; t // Generuj komunikat WM_PAINT, // żeby wyczyœcić tlo InvalidateRect (hwnd, NULL, TRUE) ; UpdateWindow (hwnd) ; // Otwórz plik hFile = CreateFile (szFileName, GENERIC_READ, FILE_SHAREţREAD. NULL, OPEN_EXISTING, FILEţFLAGţSEOUENTIAL SCAN, NULL) ; if (hFile == INVALID HANDLE VALUE) f MessageBox (hwnd, TEXT ("Cannot open file."), szAppName, MB ICONWARNING ţ MB OK) ; return 0 ; ? // Wczytaj BITMAPFILEHEADER bSuccess = ReadFile (hFile, &bmfh, sizeof (BITMAPFILEHEADER), &dwBytesRead, NULL) ; if (!bSuccess ţţ dwBytesRead != sizeof (BITMAPFILEHEADER)) ( MessageBox (hwnd, TEXT ("Cannot read file."), szAppName, MBţICONWARNING ţ MBţOK) ; CloseHandle (hFile) ; return 0 ; ) // SprawdŸ, czy to jest bitmapa if (bmfh.bfType != * (WORD *) "BM") f MessageBox (hwnd, TEXT ("File is not a bitmap."), szAppName, MBţICONWARNING ţ MB OK) ; Rozdział 15: Bitmapa niezależna od urzšdzeń 705 CloseHandle (hFile) : return 0 ; // Alokuj pamięć na naglówek i bity pikseli iInfoSize = bmfh.bfOffBits - sizeof (BITMAPFILEHEADER) : iBitsSize = bmfh.bfSize - bmfh.bfOffBits : pbmi = malloc (iInfoSize) : pBits = malloc (iBitsSize) : if (pbmi = NULL ţţ pBits = NULL) ( MessageBox (hwnd, TEXT ("Cannot allocate memory."). szAppName, MBţICONWARNING ţ MB OK) ; if (pbmi) free (pbmi) ; if (pBits) free (pBits) ţ CloseHandle (hFile) : I return 0 : ,, I // Wczytaj naglówek informacyjny bSuccess ţ ReadFile (hFile, pbmi iInfoSize, &dwBytesRead. NULL) ; ! ; if (!bSuccess ţţ (int) dwBytesRead != iInfoSize) I MessageBox (hwnd, TEXT ("Cannot read file."). szAppName. MBţICONWARNING ţ MB OK) ; if (pbmi) ' free (pbmi) ; if (pBits) free (pBits) : CloseHandle (hFile) : return 0 : ) // Odczytaj szerokoœć i wysokoœć DIB bTopDown = FALSE : if (pbmi->bmiHeader.biSize == sizeof (BITMAPCOREHEADER)) pbmi)->bcWidth : cxDib = ((BITMAPCOREHEADER *) cyDib = ((BITMAPCOREHEADER *) pbmi)->bcHeight ; cBits = ((BITMAPCOREHEADER *) pbmi)->bcBitCount : 1 else ( if (pbmi->bmiHeader.biHeight < 0) bTopDown = TRUE ; cxDib = pbmi->bmiHeader.biWidth cyDib = abs (pbmi->bmiHeader.biHeight) : cBits = pbmi->bmiHeader.biBitCount : 706 Częœć II, Grafika (cišg dalszy ze strony 705) if (pbmi->bmiHeader.biCompression != BI_RGB && pbmi->bmiHeader.biCompression != BIţBITFIELDS) f MessageBox (hwnd, TEXT ("File is compressed."), szAppName, MB ICONWARNING ţ MB OK) ; if (pbmi) free (pbmi) ; if (pBits) free (pBits) ; CloseHandle (hFile) ; return 0 ; ) // Odczytaj dlugoœć wiersza iRowLength = ((cxDib * cBits + 31) & ~31) Ż 3 ; // Czytaj i wyœwietlaj SetCursor (LoadCursor (NULL, IDC_WAIT)) ; ShowCursor (TRUE) ; hdc = GetDC (hwnd) ; for (y = 0 ; y < cyDib ; y++) ( ReadFile (hFile, pBits + y * iRowLength, iRowLength, &dwBytesRead, NULL) ; SetDIBitsToOevice (hdc, 0, // xDst 0, // y0st cxDib, // cxSrc cyDib, l/ cySrc 0, // xSrc 0, // ySrc bTopDown ? cy0ib - y - 1 : y, // pierwsza linia skanowania 1, // liczba skanowanych linii pBits + y * iRowLength, pbmi, DIB RGB COLORS) ; ReleaseDC (hwnd, hdc) ; CloseHandle (hFile) ; ShowCursor (FALSE) ; SetCursor (LoadCursor (NULL, IDC ARROW)) ; return 0 ; ? break ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; if (pbmi && pBits) Rozdział 15: Bitmapa niezależna od urzšdzeń SetDIBitsToOevice (hdc, ' 0, // xDst 0, // y0st cxDib, // cxSrc cy0ib, // cySrc 0, // xSrc 0, // ySrc 0, // pierwsza linia skanowana cyDib, // liczba skanowanych linii pBits, pbmi, DIB RGB COLORS) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: if (pbmi) free (pbmi) ; if (pBits) free (pBits) : PostOuitMessage (0) ; return 0 ; return DefWindowProc (hwnd, message, wParam, lParam) ; ,:. SEQDISP.RC (fragmenty) // Microsoft Developer Studio generated resource script. ţţinclude "resource.h" ţţinclude "afxres.h" ///////////////////////////////////////////////////////////////////////////// 4 // Accelerator L: SEODISP ACCELERATORS DISCARDABLE BEGIN ř0ř, IDM FILE OPEN, VIRTKEY, CONTROL, NOINVERT END ///////////////////////////////////////////////////////////////////////////// // Menu SEODISP MENU DISCARDABLE BEGIN POPUP "&File" BEGIN MENUITEM "&Open...\tCtrl+0", IDMţFILE OPEN END END RESOURCE.H (fragmenty) // Microsoft Developer Studio generated include file. 708 CzQœć II: Grafika (cišg dalszy ze strony 707) // Used by SeqDisp.rc ţţdefine IDM FILE OPEN 40001 Rysunek 15-8. Program SEQDISP W SEQDISP.C wszystkie operacje wejœcia/wyjœcia plików wykonywane sš pod- czas przetwarzania polecenia menu FilebOpen. Pod koniec przetwarzania ko- munikatu WMţCOMMAND program wchodzi w pętlę, która odczytuje pojedyn- cze linie pikseli i wyœwietla je za pomocš SetDIBitsToDevice. Cała DIB jest prze- trzymywana w pamięci, zatem może być także wyœwietlona w czasie przetwa- rzania WMţPAINT. Rozcišganie i dopasowywanie Funkcja SetDIBitsToDevice wyœwietla DIB na urzšdzeniu wyjœciowym piksel po pikselu. Raczej nie jest to korzystne w przypadku drukowania bitmap. Im więk- sza rozdzielczoœć drukarki, tym mniejszy będzie uzyskany obraz. Możesz w efek- cie uzyskać miniaturkę wielkoœci znaczka pocztowego. Aby wyœwietlona na urzšdzeniu wyjœciowym DIB miała jakšœ okreœlš wielkoœć, należy jš rozcišgnšć lub zmniejszyć za pomocš funkcji StretchDIBits: iLines = StretchDIBits ( hdc, // uchwyt kontekstu urzšdzenia xOst, // docelowa wspólrzędna x y0st, // docelowa współrzędna y cxDst, // szerokoœć prostokšta docelowego cy0st, // wysokoœć prostokšta docelowego xSrc, // Ÿródlowa wspólrzędna x ySrc, // Ÿródlowa wspólrzędna y cxSrc, // szerokoœć prostokšta Ÿródlowego cySrc, // wysokoœć prostokšta Ÿródlowego pBits, // wskaŸnik do bitów pikseli DIB pInfo, // wskaŸnik do częœci informacyjnej DIB fClrUse, // flaga użycia kolorów dwRop) ; // operacje rastrowe Argumenty funkcji StretchDIBits sš takie same, jak argumenty funkcji SetDIBits- ToDevice, z trzema wyjštkami: ů Sš podane współrzędne docelowe w tym logiczna szerokoœć i (cxDst) i wyso- koœć (cyDst) oraz punkty poczštkowe. ů Nie ma możliwoœci ograniczenia wymagań pamięciowych poprzez sekwen- cyjne wyœwietlenie DIB. ů Ostatni argument to operacja rastrowa, okreœlajšca relację między pikselami DIB a pikselami urzšdzenia wyjœciowego. Uczyliœmy się o tym w poprzed- nim rozdziale. Na razie dla tego argumentu będziemy używać wartoœci SRC- COPY. W rzeczywistoœci jest jeszcze jedna różnica, bardziej subtelna. Jeœli spojrzysz na deklarację SetDIBitsToDevice, zauważysz, że cxSrc i cySrc sš typu DWORD, czyli 32-bitowymi długimi liczbami całkowitymi bez znaku. Natomiast w StretchDl- Rozdział 15: Bitmapa niezależna od urzšdzeń Bits cxSrc i cySrc (jak również cxDst i cyDst) sš zdefiniowane jako liczby całkowi- te ze znakiem, z czego wynika, że mogš być ujemne. Rzeczywiœcie, jak się wkrót- ce przekonamy, mogš. Jednak jeœli zaczęliœcie się zastanawiać, czy inne argumenty mogš być ujemne, pozwólcie mi wyjaœnić pewnš rzecz: w obydwu funkcjach xSrc i ySrc sš zdefiniowane jako wartoœci int, lecz jest to błšd. Wartoœci te sš zawsze nieujemne. Prostokšt Ÿródłowy w ramach DIB odwzorowywany jest na prostokšt docelowy, tak jak pokazuje poniższa tabela. Prostokšt Ÿródłowy Prostokšt docelowy (xSrc, ySrc) (xDst, yDst + cyDst - 1) (xSrc + cxSrc - 1, ySrc) (xDst + cxDst - 1, yDst + cyDst - 1) (xSrc, ySrc + cySrc - 1) (xDst, yDst) (xSrc + cxSrc - 1, ySrc + cySrc - 1) (xDst + cxDst - 1, yDst) Odejmowanie -1 w prawej kolumnie nie zawsze jest dokładnie wykonywane, ponieważ stopień rozcišgnięcia (jak również tryb odwzorowania i inne transfor- macje) mogš nieso zmienić. Jako przykład rozważmy DIB 2x2, gdzie obydwa argumenty xSrc i ySrc funkcji StretchDIBits sš równe 0, a cxSrc i cySrc wynoszš 2. Załóżmy, że wyœwietlamy w kontekœcie urzšdzenia z trybem odwzorowania MM TEXT i bez transforma- cji. Jeœli zarówno xDst, jak i yDst będš równe 0, a cxDst i cyDst wyniosš 4, wów- czas DIB zostanie rozcišgnięta ze współczynnikiem 2. Każdy piksel Ÿródłowy (x, y) zostanie odwzorowany na cztery piksele docelowe, jak to pokazano poniżej: (0,0) -> (0,2) i (1,2) i (0,3) i (1,3) (1,0) -> (2,2) i (3,2) i (2,3) i (3,3) (0,1) -> (0,0) i (1,0) i (0,1) i (1,1) (1,1) -> (2,0) i (3,0) i (2,1) i (3,1) Tabela przedstawiona powyżej prawidłowo okreœla narożniki docelowe, które tutaj majš współrzędne (0, 3), (3, 3), (0, 0) i (3, 0). W innych przypadkach spodziewane współrzędne mogš być jedynie przybliżane. Tryb odwzorowania kontekstu urzšdzenia docelowego wpływa na SetDIBitsTo- Device o tyłe, że xDst i yDst sš współrzędnymi logicznymi. Na funkcję StretchDl- Bits tryb odwzorowania ma wpływ, w każdym aspekcie, na przykład, jeœli usta- wiłeœ jeden z metrycznych trybów odwzorowania, gdzie wartoœci y rosnš przy posuwaniu się w górę ekranu, to DIB zostanie wyœwietlona do góry nogami. Możesz to skompensować, nadajšc cyDst wartoœć ujemnš. Istotnie, aby uzyskać pionowe lub poziome odbicie symetryczne DIB, możesz uczynić dowolny z para- metrów szerokoœci lub wysokoœci ujemnym. Jeżeli cySrc i cyDst majš przeciwne znaki w trybie odwzorowania MM TEXT, to DIB zostanie obrócona wokół osi poziomej i pojawi się do góry nogami. Jeœli cxSrc i cxDst majš przeciwne znaki, DIB jest obra- cana wokół swej osi pionowej i pojawi się jako swoje lustrzane odbicie. CzQœć II: Grafika Oto kilka wyrażeń stanowišcych podsumowanie. W wyrażeniach tych xMM i yMM okreœlajš orientację trybu odwzorowara; xMM wynosi 1, jeżeli wartoœci x rosnš przy poruszaniu się w prawo, oraz -l, jeœli wartoœci x rosnš przy poru- szaniu się w lewo. Analogicznie yMM jest równe 1, jeżeli wartoœci y rosnš w dół, oraz -1, jeœli wartoœci y rosnš ku górze. Funkcje Sign zwracajš TRUE dla wartoœci dodatniej, a FALSE dla ujemnej: if (!Sign (xMM x cxSrc x cxOst)) DIB jest odbita względem osi pionowej (odbicie lustrzane) if (!Sign (yMM X cySrc X cyDst)) DIB jest odbita względem osi poziomej (do góry nogami) Jeœli masz wštpliwoœci, sięgnij do tabeli przedstawionej powyżej. Program SHOWDIB2 przedstawiony na rysunku 15-9, wyœwietla bitmapy rze- czywistej wielkoœci oraz bitmapy rozcišgnięte do rozmiarów obszaru roboczego okna programu, drukuje DIB i dokonuje transferu DIB do Schowka. SHOWDIB2.C /* SHOWDIB2.C - Wyţwietla DIB w obszarze roboczym okna (c) Charles Petzold, 1998 ţţinclude iţinclude "dibfile.h" ţţinclude "resource.h" LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; TCHAR szAppName[ţ = TEXT ("ShowDib2") ; */ int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) r HACCEL hAccel ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CS_VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITEţBRUSH) wndclass.lpszMenuName = szAppName ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) ( MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MB ICONERROR) ; return 0 ; Rozdział 15: Bitmapa niezależna od urzšdzeń 711 t hwnd = CreateWindow (szAppName, TEXT ("Show DIB ţţ2"). WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW USEDEFAULT, CW_USEDEFAULT, CW USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd. iCmdShow) ; UpdateWindow (hwnd) ; hAccel = LoadAccelerators (hInstance, szAppName) ; while (GetMessage (&msg, NULL, 0, 0)) f if (!TranslateAccelerator (hwnd, hAccel, &msg)) ( TranslateMessage (&msg) ; DispatchMessage (&msg) ; ) ) return msg.wParam ; int ShowOib (HDC hdc, BITMAPINFO * pbmi, BYTE * pBits, int cxDib, int cy0ib, int cxClient, int cyClient, WORD wShow) switch (wShow) i case IDM_SHOW_NORMAL: return SetDIBitsToDevice (hdc. 0, 0. cxDib, cyDib, 0. 0, 0, cyDib. pBits, pbmi, DIBţRGB COLORS) ; case IDM_SHOW_CENTER: return SetDIBitsToDevice (hdc, (cxClient - cx0ib) / 2. (cyClient - cyDib) / 2. cxDib, cy0ib, 0, 0, 0, cyDib, pBits, pbmi, DIBţRGB COLORS) ; case IDM_SHOW_STRETCH: SetStretchBltMode (hdc, COLORONCOLOR) ; return StretchDIBits (hdc, 0, 0, cxClient, cyClient, 0, 0. cxDib, cy0ib, pBits, pbmi, DIBţRGB COLORS, SRCCOPY) ; case IDM_SHOWţISOSTRETCH: SetStretchBltMode (hdc, COLORONCOLOR) ; SetMapMode (hdc, MMţISOTROPIC) ; SetWindowExtEx (hdc, cxDib, cyDib, NULL) ; SetViewportExtEx (hdc, cxClient, cyClient, NULL) ; SetWindowOrgEx (hdc, cxDib / 2. cyDib / 2, NULL) ; SetViewportOrgEx (hdc, cxClient / 2, cyClient / 2. NULL) ; return StretchDIBits (hdc. 0, 0, cx0ib, cyDib, 0. 0. cxDib, cyDib, pBits, pbmi, DIBţRGB COLORS, SRCCOPY) : 712 Częœć II: Grafika Roz (cišg dalszy ze strony 711) LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) static BITMAPFILEHEADER * pbmfh ; static BITMAPINFO * pbmi ; static BYTE * pBits ; static DOCINFO di = [ sizeof (DOCINFO), TEXT ("ShowDib2: Printing") ) ; static int cxClient, cyClient, cx0ib, cyDib ; static PRINTDLG printdlg = ( sizeof (PRINTDLG) ) ; static TCHAR szFileName [MAX_PATH], szTitleName [MAX_PATH] ; static WORD wShow = IDM_SHOW_NORMAL : BOOL bSuccess ; HDC hdc, hdcPrn ; HGLOBAL hGlobal ; HMENU hMenu ; int cxPage, cyPage, iEnable ; PAINTSTRUCT s ; BYTE * p pGlobal ; switch (message) ( case WM_CREATE: DibFileInitialize (hwnd) ; return 0 ; case WM_SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; return 0 ; case WM_INITMENUPOPUP: hMenu = GetMenu (hwnd) ; if (pbmfh) iEnable = MFţENABLED ; else iEnable = MF GRAYED ; EnableMenuItem (hMenu, IDM_FILE_SAVE, iEnable) ; EnableMenuItem (hMenu, IDM_FILE_PRINT, iEnable) ; EnableMenuItem (hMenu, IDM_EDIT_CUT, iEnable) ; EnableMenuItem (hMenu, IDM_EDIT_COPY, iEnable) ; EnableMenuItem (hMenu, IDM EDIT_DELETE, iEnable) ; return 0 ; case WM_COMMAND: hMenu = GetMenu (hwnd) ; switch (LOWORD (wParam)) ( case IDMţFILE_OPEN: // Wyœwietl okno dialogowe Otwórz if (!DibFile0pen0lg (hwnd, szFileName, szTitleName)) return 0 ; Rozdział 15: Bitmapa niezależna od urzšdzeń // Jeœli DIB istnieje, zwolnij pamięć if (pbmfh) ( free (pbmfh) ; pbmfh = NULL ; i ţj // Wczytaj do pamięci calš DIB SetCursor (LoadCursor (NULL, IDC WAIT)) ; ShowCursor (TRUE) ; pbmfh = DibLoadImage (szFileName) ; ShowCursor (FALSE) ; SetCursor (LoadCursor (NULL, IDC ARROW)) ; ll Wyczyœć obszar roboczy okna // przed następnd aktualizacjd InvalidateRect (hwnd, NULL, TRUE) ; if (pbmfh = NULL) ( MessageBox (hwnd, TEXT ("Cannot load DIB file"), szAppName, MBţICONEXCLAMATION ţ MB OK) ; return 0 ; ) // WeŸ wskaŸniki do struktury informacyjnej // i do bitów pikseli pbmi = (BITMAPINFO *) (pbmfh + 1) ; pBits = (BYTE *) pbmfh + pbmfh->bfOffBits ; // Odczytaj szerokoœć i wysokoœć DIB if (pbmi->bmiHeader.biSize ţ sizeof (BITMAPCOREHEADER)) ( cx0ib = ((BITMAPCOREHEADER *) pbmi)->bcWidth ; cy0ib = ((BITMAPCOREHEADER *) pbmi)->bcHeight ; else ( ; cxDib = pbmi->bmiHeader.biWidth ; cy0ib = abs (pbmi->bmiHeader.biHeight) ; i ) return 0 ; case IDM_FILEţSAVE: // Otwórz okno dialogowe Zapisz jako if (!DibFileSaveDlg (hwnd, szFileName, szTitleName)) return 0 ; // Zapisz DIB w pliku dyskowym SetCursor (LoadCursor (NULL, IDC WAIT)) ; ShowCursor (TRUE) ; 714 Częœć II: Grafika (cišg dalszy ze strony 713) bSuccess = DibSaveImage (szFileName, pbmfh) ; ShowCursor (FALSE) ; SetCursor (LoadCursor (NULL, IDC ARROW)) ; if (!bSuccess) MessageBox (hwnd, TEXT ("Cannot save DIB file"), szAppName, MBţICONEXCLAMATION ţ MBţOK) ; return 0 ; case IDM FILE_PRINT: if (!pbmfh) return 0 ; // WeŸ kontekst urzddzenia drukujdcego printdlg.Flags = PD RETURNDC ţ PD NOPAGENUMS ţ PD NOSELECTION ; if (!PrintDlg (&printdlg)) return 0 ; if (NULL == (hdcPrn = printd7g.hDC)) f .MessageBox (hwnd, TEXT ("Cannot obtain Printer DC"), szAppName, MBţICONEXCLAMATION ţ MB OK) ; return 0 ; J // SprawdŸ, czy drukarka może drukować bitmapy if (!(RCţBITBLT & GetDeviceCaps (hdcPrn, RASTERCAPS))) f DeleteDC (hdcPrn) ; MessageBox (hwnd, TEXT ("Printer cannot print bitmaps"), szAppName, MBţICONEXCLAMATION ţ MB OK) ; return 0 ; ) // Jaki obszar strony można zadrukować? cxPage = GetDeviceCaps (hdcPrn, HORZRES) ; cyPage = GetDeviceCaps (hdcPrn, IIERTRES) ; bSuccess = FALSE ; // Wyœlij DIB na drukarkę SetCursor (LoadCursor (NULL, IDC_WAIT)) ; ShowCursor (TRUE) ; if ((StartDoc (hdcPrn, &di) > 0) && (StartPage (hdcPrn) > 0)) ( ShowDib (hdcPrn, pbmi, pBits, cxDib, cyDib, cxPage, cyPage, wShow) ; if (EndPage (hdcPrn) > 0) i bSuccess = TRUE ; Rozdział 15: Bitmapa niezależna od urzšdzeń 715 EndDoc (hdcPrn) ; ) 1 ShowCursor (FALSE) ; SetCursor (LoadCursor (NULL, IDC ARROW)) : DeleteDC (hdcPrn) ; if (!bSuccess) MessageBox (hwnd, TEXT ("Could not print bitmap"), szAppName, MBţICONEXCLAMATION ţ MB OK) ; return 0 : i, case IDM_EDIT_COPY: case IDM_EDIT_CUT: if (!pbmfh) return 0 ; // Zrób kopię upakowanej DIB hGlobal = GlobalAlloc (GHND ţ GMEM_SHARE, pbmfh->bfSize - ; sizeof (BITMAPFILEHEADER)) ; pGlobal = GlobalLock (hGlobal) ; CopyMemory (pGlobal, (BYTE *) pbmfh + sizeof (BITMAPFILEHEADER), pbmfh->bfSize - sizeof (BITMAPFILEHEADER)) ; GlobalUnlock (hGlobal) ; // Przenieœ jd do Schowka OpenClipboard (hwnd) : EmptyClipboard () : SetClipboardData (CFţDIB, hGlobal) ; CloseClipboard () ; if (LOWORD (wParam) = IDMţEDIT COPY) return 0 : // Jeţli IDMţEDIT CUT, to ten przypadek // zostawiamy nieopracowany case IDM_EDIT_DELETE: if (pbmfh) F ';'r l free (pbmfh) : pbmfh = NULL : InvalidateRect (hwnd, NULL, TRUE) ; l return 0 : case IDM_SHOW_NORMAL: case IDM_SHOW_CENTER: case IDM_SHOW_STRETCH: case IDM_SHOWţISOSTRETCH: CheckMenuItem (hMenu, wShow, MF UNCHECKED) ; wShow = LOWORD (wParam) : CheckMenuItem (hMenu, wShow, MF CHECKED) ; Częœć IIţ Grafika (cišg dalszy ze strony 715) InvalidateRect (hwnd, NULL, TRUE) ; return 0 ; break ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; if (pbmfh) ShowDib (hdc, pbmi, pBits, cxDib, cyDib, cxClient, cyClient, wShow) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: if (pbmfh) free (pbmfh) ; PostOuitMessage (0) ; return 0 ; 1 return DefWindowProc (hwnd, message, wParam, lParam) ; ) SHOWDIB2.RC (fragmenty) // Microsoft Developer Studio generated resource script. ţlinclude "resource.h" ţlinclude "afxres.h" ///////////////////////////////////////////////////////////////////////////// // Menu SHOWDIB2 MENU DISCARDABLE BEGIN POPUP "&File" BEGIN MENUITEM "&Open...\tCtrl+0", IDM_FILE_OPEN MENUITEM "&Save...\tCtrl+S^, IDM_FILE_SAVE MENUITEM SEPARATOR MENUITEM "&Print\tCtrl+P", IDM FILE PRINT END POPUP "&Edit" BEGIN MENUITEM "Cu&t\tCtrl+X", IDM_EDIT_CUT MENUITEM "&Copy\tCtrl+C", IDM_EDIT_COPY MENUITEM "&Delete\tDelete", IDM EDIT DELETE END POPUP "&Show" BEGIN MENUITEM "&Actual Size", IDM_SHOW_NORMAL, CHECKED MENUITEM "&Center", IDM_SHOW_CENTER MENUITEM "&Stretch to Window", IDM SHOW_STRETCH MENUITEM "Stretch &Isotropically", IDM SHOW ISOSTRETCH Rozdział 15: Bitmapa niezależna od urzšdzeń 717 END END ///////////////////////////////////////////////////////////////////////////// // Accelerator SHOWDIB2 ACCELERATORS DISCARDABLE BEGIN "C", IDM_EDIT_COPY, VIRTKEY, CONTROL, NOINVERT "0", IOM_FILE_OPEN, VIRTKEY, CONTROL, NOINVERT "P", IDM_FILE_PRINT, VIRTKEY, CONTROL, NOINVERT "S", IOM_FILE_SAVE, VIRTKEY, CONTROL, NOINVERT UK_DELETE, IDM_EDIT_DELETE, VIRTKEY, NOINVERT "X", IDMţEDIT CUT, VIRTKEY, CONTROL, NOINVERT END RESOURCE.H (fragmenty) // Microsoft Developer Studio generated included file. // Used by ShowDib2.rc #define IDM FILE OPEN 40001 #define IDM_SHOW_NORMAL 40002 ţţdefine IDM_SHOW_CENTER 40003 #define IDM_SHOW_STRETCH 40004 #define IDM_SHOW_ISOSTRETCH 40005 4ţdefine IDMţFILE_PRINT 40006 ţţdefine IDM EDIT_COPY 40007 #define IDM EDIT_CUT 40008 #define IDM_EDIT_DELETE 40009 4ţdefine IDM FILEţSAVE 40010 Rysunek 15-9. Program SHOWDIB2 Szczególnie interesujšca jest tu funkcja ShowDib, która wyœwietla DIB w obszarze roboczyrn okna na jeden z czterech sposobów, w zależnoœci od wyboru z menu. Przy użyciu SetDIBitsToDevice DIB może być wyœwietlona albo w lewym górnym rogu obszaru roboczego okna, albo wycentrowana w tym obszarze. Program ma także dwie opcje wykorzystujšce funkcję StretchDIBits. Jedna z nich rozcišga bitmapę tak, by wypełniała cały obszar roboczy, w którym to przypadku DIB jest często zniekształ- cona. Druga wyœwietla bitmapę proporcjonalnie - bez zniekształcenia. Kopiowanie DIB do Schowka polega na wykonaniu kopii bloku pamięci upako- wanej DIB do globalnej pamięci dzielonej. Dane w Schowku sš typu CF DIB. Program nie pokazuje niestety, jak wykopiować DIB ze Schowka. Dzieje się tak dlatego, że operacja ta wymaga okreœlenia offsetu bitów pikseli, gdy mamy do dyspozycji jedynie wskaŸnik do bloku pamięci upakowanej DIB, a to wymaga nieco więcej zachodu. Przed końcem następnego rozdziału pokażę, jak to zrobić. Możesz także zauważyć jeszcze inne braki programu SHOWDIB2. Jeœli pracu- jesz w 256-kolorowym trybie graficznym Windows, prawdopodobnie napotkasz problemy przy wyœwietlaniu DIB jakichkolwiek innych niż 4-bitowe. Nie zoba- czysz poprawnych kolorów. Uzyskanie dostępu do nich będzie wymagało uży- gţł '...:' Częœć II: Grafika cia palety, a to zadanie czeka nas w następnym rozdziale. Możesz także zaobser- wować kłopoty z szybkoœciš wykonania, szczególnie przy uruchamianiu progra- mu SHOWDIB2 w systemie Windows NT. Pokażę ci, jak sobie z tym poradzić, gdy będziemy zawijać DIB i inne bitmapy w następnym rozdziale. Dołożę także paski przewijania do ekranu wyœwietlajšcego DIB, tak aby obraz wykraczajšcy poza ekran mógł być oglšdany w rzeczywistych rozmiarach. Konwersja kolorów, palety oraz szybkoœć działania Pamiętasz scenę z filmu Williama Goldmana, zatytułowanego "Wszyscy ludzie prezydenta", kiedy to Deep Throat mówi do Boba Woodwarda, że klucz do rozwi- kłania sprawy Watergate to "iœć za pieniędzmi"? Klucz do osišgnięcia maksymal- nej szybkoœci działania przy wyœwietlaniu bitmap to "iœć za bitami pikseli" i rozu- mieć, kiedy i gdzie wykonywana jest konwersja kolorów. DIB jest w formacie nie- zależnym od urzšdzeń; pamięć karty graficznej prawie na pewno jest w innym formacie. W czasie wykonywania funkcji SetDIBitsToDevice lub StretchDIBits, każ- dy piksel (a mogš ich być dosłownie miliony) musi ulec konwersji z formatu nieza- leżnego od urzšdzeń na format zależny od urzšdzenia. W wielu wypadkach konwersja ta jest doœć prosta. Na przykład jeżeli wyœwie- tlasz 24-bitowš DIB na 24-bitowym monitorze, sterownik ekranu będzie musiał co najwyżej zmienić kolejnoœć bajtów czerwieni, zieleni i niebieskiego. Wyœwie- tlanie 16-bitowej DIB na urzšdzeniu 24-bitowym wymaga nieco przesunięć bi- tów i dopełnień. Wyœwietlenie 24-bitowej DIB na 16-bitowym urzšdzenia wyma- ga kilku przesunięć bitów oraz obcinania. Wyœwietlenie 4-bitowej lub 8-bitowej DIB na 24-bitowym urzšdzeniu wymusza spojrzenie na bity pikseli w tabeli ko- lorów DIB, a następnie, być może pewne zmiany kolejnoœci bajtów. Co się jednak stanie, jeœli zechcesz wyœwietlić 16-bitowš, 24-bitowš lub 32-bito- wš DIB na 4-bitowym lub 8-bitowym monitorze? Musi zostać przeprowadzony zupełnie inny typ konwersji kolorów. Dla każdego piksela w DIB sterownik urzš- dzenia musi przeprowadzić poszukiwanie najbliższego koloru między barwš pik- sela i wszystkimi innymi dostępnymi na ekranie. Wymaga to zastosowania pętli i wykonania obliczeń. (Funkcja GDI GetNearestColor służy do znajdowania naj- bliższego koloru). Cała trójwymiarowa tablica kolorów RGB może być przedstawiona w postaci szeœcianu. Odległoœć między dowolnymi dwoma punktami w tym szeœcianie wynosi (Rz - RţJ2 + ţGz - GţJ2 + ţB2 - 8ţJ2 gdzie RlGţB1 i R2G2B2 sš dwoma kolorami. Znalezienie najbliższego wišże się z ob- liczeniem najkrótszej odległoœci od jednego koloru do zbioru innych kolorów. Na szczęœcie przy porównywaniu odległoœci między punktami szeœcianu kolorów RGB obliczanie pierwiastka nie jest konieczne. Jednak każdy piksel, który ma podlegać konwersji, musi być porównany ze wszystkimi kolorami urzšdzenia w celu znalezienia najbliższego. Jest to i tak dużo pracy. (Choć wyœwietlenie 8-bito- wej DIB na 8-bitowym urzšdzeniu także wymaga szukania najbliższego koloru, to jednak nie trzeba tego robić dla każdego piksela; wystarczy wykonać porów- Rozdział 15: Bitmapa niezależna od urzšdzeń 71 g nanie dla każdej z barw w tabeli kolorów DIB). Z tego powodu należy unikać wyœwietlania 16-bitowych, 24-bitowych i 32-bito- wych DIB na 8-bitowej karcie graficznej przy użyciu funkcji SetDIBitsToDevice lub StretchDIBits. Lepiej wykonać konwersję bitmapy na 8-bitowš DIB lub, aby uzy- skać jeszcze lepsze wyniki, na 8-bitowš DDB. Wyœwietlanie praktycznie wszyst- kich DIB pokaŸnych rozmiarów można naprawdę przyspieszyć przez ich kon- wersję na DDB i użycie w celu wyœwietlania funkcji BitBlt oraz StretchBlt. Jeœli uruchamiasz Windows z 8-bitowym monitorem (lub jeżeli właœnie włšczyłeœ tryb 8-bitowy, żeby zaobserwować różnicę szybkoœci wyœwietlania bitmapy w peł- nej gamie kolorów), możesz zauważyć inny problem: DIB nie sš wyœwietlane ze wszystkimi swoimi kolorami - każda wyœwietlana na 8-bitowym monitorze ogra- niczona jest tylko do 20 kolorów. Uzyskanie większej iloœci kolorów niż 20 to zada- nie dla Palette Managera, o którym będzie mowa w następnym rozdziale. Na koniec, jeœli uruchomisz zarówno Windows 98, jak i Windows NT na tej sa- mej maszynie, możesz zauważyć, że temu ostatniemu wyœwietlenie dużych DIB przy porównywalnych trybach graficznych zajmuje więcej czasu. Jest to konse- kwencjš architektury klient/serwer w Windows NT i cenš za duże iloœci danych przekazywanych przez API. I tutaj rozwišzaniem jest wykonanie konwersji DIB na DDB. Funkcja CreateDIBSection, którš wkrótce omówię, została specjalnie utwo- rzona, aby pomóc w tej sytuacji. Połšczenie DIB i DDB Znajšc format DIB i wywołujšc dwie funkcje rysujšce bitmapy, SetDIBitsToDevice i StretchDIBits, możesz wiele zdziałać. Masz bezpoœredni dostęp do każdego po- jedynczego bitu, bajtu i piksela w DIB. A gdy masz do dyspozycji szereg funkcji, które pozwalajš oglšdać i zmieniać te dane w sposób strukturalny, zyskujesz nie- ograniczone możliwoœci manewru. Doszliœmy jednakże do wniosku, że sš pewne ograniczenia. W poprzednim roz- dziale widzieliœmy, jak można wykorzystać funkcję GDI do rysowania obrazów na DDB. Jak na razie nic nie wskazuje na to, aby istniał sposób zrobienia tego samego z DIB. Inny kłopot: SetDIBitsToDevice i StretchDIBits nie sš nawet w przy- bliżeniu tak szybkie jak BitBlt i StretchBlt, w szczególnoœci w systemie Windows NT i wówczas, kiedy trzeba przeprowadzić poszukiwanie najbliższych kolorów (jak w przypadku, gdy 24-bitowe DIB sš wyœwietlane na 8-bitowych kartach gra- ficznych). Zatem korzystne może się okazać dokonywanie konwersji DIB na DDB. Przyj- mijmy, że mamy DIB, którš należy wielokrotnie wyœwietlić na ekranie. O ile wię- cej sensu ma w tej sytuacji przekształcenie jej na DDB, do której można zastoso- wać szybkie funkcje BitBlt i StretchBlt. Tworzenie DDB z DIB Czy jest możliwe utworzenie obiektu bitmapowego GDI z DIB? Zasadniczo wie- my, jak to zrobić: jeœli masz DIB, możesz użyć funkcji CreateCompatibleBitmap i utworzyć obiekt bitmapowy GDI o tym samym rozmiarze co DIB, kompatybil- 720 Częœć II: Grafika ny z monitorem. Następnie wybierasz obiekt bitmapowy w kontekœcie urzšdze- nia pamięciowego i wywołujesz SetDIBitsToDevice, aby w nim rysować. Wynikiem jest DDB z tym samym obrazem co DIB, lecz o organizacji kolorów kompatybil- nej z monitorem. Możesz również zrobić to, wykonujšc mniejszš liczbę czynnoœci - za pomocš Cre- ateDIBitmap. Funkcja ta ma następujšcš składnię: hBitmap = CreateDIBitmap ( hdc // uchwyt kontekstu urzšdzenia pInfoHdr, // wskaŸnik do naglówka informacyjnego DIB fInit, // 0 lub CBM_INIT pBits, // wskaŸnik do bitów pikseli DIB pInfo, // wskaŸnik do częœci informacyjnej DIB fClrUse) ; // flaga użycia kolorów Zwróć uwagę na dwa argumenty, które nazwałem plnfoHdr i plnfo. Sš one zdefi- niowane jako wskaŸniki, odpowiednio, do struktury BTTMAPINFOHEADER oraz do struktury BITMAPINFO. Jak wiemy, struktura BTIMAPINFO to struktura BIT MAPINFOHEADER, po której następuje tablica kolorów. Wkrótce zobaczymy jak to rozróżnienie działa. Ostatnim argumentem jest albo DIB RGB COLORS (rów- ny 0), albo DIB PAL COLORS, jak w funkcjach SetDIBitsToDevice. Więcej do po- wiedzenia na ten temat będę miał w następnym rozdziale. W zrozumieniu całej gamy funkcji działajšcych na bitmapach w Windows istot- ne jest, by zdać sobie sprawę, że mimo swojej nazwy funkcja CreateDIBitmap nie tworzy bitmap niezależnych od urzšdzeń. Tworzy ona bitmapę zależnš od urzš- dzeń na podstawie specyfikacji niezależnej od urzšdzeń. Zauważ, że funkcja zwraca uchwyt do obiektu bitmapowego GDI, tak samo jak CreateBitmap, Create- BitmapIndirect i CreateCompatibleBitmap. Najprostszy sposób wywołania funkcji CreateDIBitmap jest następujšcy: hBitmap = CreateDIBitmap (NULL, pbmih, 0, NULL, NULL, 0) ; Jedynym argumentem jest wskaŸnik do struktury BITMAPINFOHEADER (bez tablicy kolorów). Funkcja w tej postaci tworzy monochromatyczny obiekt bitma- powy GDI. Drugim pod względem prostoty sposobem wywołania funkcji jest hBitmap = CreateDIBitmap (hdc, pbmih, 0, NULL, NULL, 0) : W tej postaci funkcja tworzy DDB kompatybilnš z kontekstem urzšdzenia wska- zanym przez argument hdc. Na razie nie zrobiliœmy nic, czego nie moglibyœmy wykonać używajšc CreateBitmap (aby utworzyć bitmapę monochromatycznš) lub CreateCompatibleBitmap (aby utworzyć bitmapę kompatybilnš z monitorem). W tych dwóch uproszczonych postaciach CreateDIBitmap bity pikseli pozostajš nie- zainicjowane. Jeœli trzecim argumentem CreateDIBitmap jest CBM lNIT, Windows tworzšc DDB używa ostatnich trzech argumentów do inicjacji bitów bitmapy. Ar- gument plnfo jest wskaŸnikiem do struktury BITMAPINFO, obejmujšcej tablicę kolorów. Argument pBits jest wskaŸnikiem do tablicy bitów w formacie kolorów okreœlonym w strukturze BITMAPINFO. Na podstawie tablicy kolorów bity te sš konwertowane na format kolorów urzšdzenia. To wszystko dzieje się identycznie jak w przypadku funkcji SetDIBitsToDevice. Istotnie, cała funkcja CreateDIBitmap mogłaby prawdopodobnie zostać zaimplementowana następujšcym kodem: Rozdział 15: Bitmapa niezależna od urzšdzeń 7 HBITMAP CreateDIBitmap (HDC hdc, CONST BITMAPINFOHEADER * pbmih, DWORD fInit. CONST VOID * pBits, CONST BITMAPINFO * pbmi, UINT fUsage) ( HBITMAP hBitmap ; HDC hdc ; int cx, cy, iBitCount ; if (pbmih->biSize --- sizeof (BITMAPCOREHEADER)) ( cx = ((PBITMAPCOREHEADER) pbmih)->bcWidth ; . cy = ((PBITMAPCOREHEADER) pbmih)->bcHeight ; iBitCount = ((PBITMAPCOREHEADER) pbmih)->bcBitCount ; else ( cx = pbmih->biWidth ; cy = pbmih->biHeight ; iBitCount = pbmih->biBitCount ; if (hdc) hBitmap = CreateCompatibleBitmap (hdc, cx, cy) ; else hBitmap = CreateBitmap (cx, cy, 1, l, NULL) ; if (fInit ţ CBMţINIT) ( hdcMem = CreateCompatibleDC (hdc) ; SelectObject (hdcMem, hBitmap) ; SetDIBitsToDevice (hdcMem, 0, 0, cx. cy, 0, 0, 0 cy, pBits, pbmi, fUsage) ; DeleteDC (hdcMem) ; return hBitmap ; ) Jeœli zamierzasz wyœwietlać DIB tylko raz i obawiasz się o szybkoœć działania SetDIBitsToDevice, nie ma raczej sensu wywoływanie CreateDIBitmap, a następnie wyœwietlanie DDB przy użyciu BitBlt lub StretchBlt. Oba zadania zajmš tyle samo czasu, ponieważ zarówno SetDIBitsToDevice, jak i CreateDIBitmap muszš doko- nać konwersji kolorów. Jedynie jeżeli wyœwietlasz DIB wielokrotnie - co jest bar- dzo prawdopodobne przy przetwarzaniu komunikatów WNi-PAINT - konwer- sja taka ma sens. Program DIBCONV zaprezentowany na rysunku 15-10 pokazuje, jak możesz wykorzystać SetDIBitsToDevice do konwersji pliku L)IB na DDB. DIBCONU.C /* DIBCONV.C - Konwersja DIB na DDB (c) Charles Petzold, 1998 */ 4nclude 4nclude 722 Częœć II: Grafika (cišg dalszy ze strony 721) ţţinclude "resource.h" LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; TCHAR szAppNameCJ = TEXT ("DibConv") ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) ( HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CS UREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITEţBRUSH) ; wndclass.lpszMenuName = szAppName ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) ( MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MBţICONERROR) ; return 0 ; ) hwnd = CreateWindow (szAppName, TEXT ("DIB to DDB Conversion"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW USEDEFAULT, CW_USEDEFAULT, CW USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0. 0)) ( TranslateMessage (&msg) ; DispatchMessage (&msg) ; return msg.wParam ; 1 HBITMAP CreateBitmapObjectFromOibFile (HDC hdc, PTSTR szFileName) ( BITMAPFILEHEADER * pbmfh ; BOOL bSuccess ; DWORD dwFileSize. dwHighSize, dwBytesRead ; HANDLE hFile ; HBITMAP hBitmap ; Rozdziai 15: Bitmapa niezależna od urzţdzeń 723 // Otwórz plik: tylko do czytania, zabezpiecz przed zapisem hFile = CreateFile (szFileName, GENERIC_READ, FILE_SHARE_READ, NULL, OPENţEXISTING, FILEţFLAG SEOUENTIAL SCAN, NULL) ; if (hFile = INVALID HANDLE VALUE) return NULL ; d . Iţ., // Odczytaj caly plik I.., dwFileSize = GetFileSize (hFile. &dwHighSize) ; if (dwHighSize) f CloseHandle (hFile) ; return NULL ; pbmfh = malloc (dwFileSize) ; if (!pbmfh) ( CloseHandle (hFile) ; return NULL ; 1 bSuccess = ReadFile (hFile, pbmfh, dwFileSize, &dwBytesRead, NULL) ; CloseHandle (hFile) ; // SprawdŸ plik if (!bSuccess ţţ (dwBytesRead != dwFileSize) ţ (pbmfh->bfType != * (WORD *) "BM") ţ (Pbmfh->bfSize != dwFileSize)) f free (pbmfh) ; return NULL ; ? // Utwórz DDB hBitmap = CreateDIBitmap (hdc, (BITMAPINFOHEADER *) (pbmfh + 1), CBM_INIT, (BYTE *) pbmfh + pbmfh->bfOffBits, (BITMAPINFO *) (pbmfh + 1), DIB RGB COLORS) ; free (pbmfh) ; return hBitmap ; ) LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) f static HBITMAP hBitmap ; static int cxClient, cyClient ; static OPENFILENAME ofn ; static TCHAR szFileName CMAX PATH], szTitleName CMAXţPATH] ; 724 Częœć II: Grafika (cišg dalszy ze strony 723) static TCHAR szFilter[] = TEXT ("Bitmap Files (*.BMP)\0*.bmp\0") TEXT ("All Files (*.*)\0*.*\0\0") ; BITMAP bitmap ; HDC hdc, hdcMem ; PAINTSTRUCT ps ; switch (message) i case WM_CREATE: ofn.lStructSize = sizeof (OPENFILENAME) ; ofn.hwndOwner = hwnd ; ofn.hInstance = NULL ; ofn.lpstrFilter = szFilter ; ofn.lpstrCustomFilter = NULL ; ofn.nMaxCustFilter = 0 ; ofn.nFilterIndex = 0 ; ofn.lpstrFile = szFileName ; ofn.nMaxFile = MAX PATH ; ofn.lpstrFileTitle = szTitleName ; ofn.nMaxFileTitle = MAX_PATH ; ofn.lpstrInitialDir = NULL ; ofn.lpstrTitle = NULL ; ofn.Flags = 0 ; ofn.nFileOffset = 0 ; ofn.nFileExtension = 0 ; ofn.lpstrDefExt = TEXT ("bmp") ; ofn.lCustData = 0 ; ofn.lpfnHook = NULL ; ofn.lpTemplateName = NULL ; return 0 ; case WM_SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; return 0 ; case WM COMMAND: switch (LOWORD (wParam)) ( case IDMţFILE OPEN: // Wyœwietl okno dialogowe Otwórz if (!GetOpenFileName (&ofn)) return 0 ; // Jeœli jest już DIB, to go usuń if (hBitmap) ( Delete0bject (hBitmap) ; hBitmap = NULL ; ) // Utwórz DDB na podstawie DIB SetCursor (LoadCursor (NULL, IDCţWAIT)) ; Rozdział 15: Bitmapa niezależna od urzšdzeń . 725 i ShowCursor (TRUE) ; ! hdc = GetDC (hwnd) ; hBitmap = CreateBitmapObjectFromDibFile (hdc, szFileName) ; ReleaseDC (hwnd, hdc) ; ShowCursor (FALSE) ; SetCursor (LoadCursor (NULL, IDC ARROW)) ; // Wyczyœć obszar roboczy okna przed następnš aktualizacjš InvalidateRect (hwnd, NULL, TRUE) ; if (hBitmap = NULL) MessageBox (hwnd, TEXT ("Cannot load DIB file"), szAppName, MB OK ţ MB ICONEXCLAMATION) ; l ţ return 0 ; break ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; if (hBitmap) ( GetObject (hBitmap, sizeof (BITMAP), &bitmap) ; hdcMem = CreateCompatibleDC (hdc) ; SelectObject (hdcMem, hBitmap) ; BitBlt (hdc, 0, 0, bitmap.bmWidth, bitmap.bmHeight, hdcMem, 0, 0, SRCCOPY) ; DeleteDC (hdcMem) ; ) EndPaint (hwnd, &ps) ; return 0 case WM_DESTROY: if (hBitmap) Delete0bject (hBitmap) ; PostOuitMessage (0) ; return 0 ; ) return DefWindowProc (hwnd, message, wParam, lParam) ; ) DIBCONV.RC (fragmenty) // Microsoft Developer Studio generated resource script. ţiinclude "resource.h" ţIinclude "afxres.h" I Częœć II: Grafika (cišg dalszy ze strony 727) ///////////////////////////////////////////////////////////////////////////// // Menu DIBCONh MENU DISCARDABLE BEGIN POPUP "&File" BEGIN MENUITEM "&Open", IDMţFILE OPEN END END RESOURCE.H (fragmenty) // Microsoft Developer Studio generated include file. // Used by DibConv.rc Ildefine IDMţFILE OPEN 40001 Rysunek 15-10. Program DIBCONV Program DIBCONV.C stanowi całoœć i nie wymaga wczeœniejszych plików. W od- powiedzi na swoje jedyne polecenie menu (FileţOpen), WndProc wywołuje funk- cję CreateBitmapObjectFromDibFile. Funkcja ta wczytuje cały plik do pamięci i prze- kazuje wskaŸniki do bloków pamięci do funkcji CreateDIBitmap. Funkcja zwraca uchwyt do bitmapy. Blok pamięci zawierajšcy DIB może następnie zostać zwol- niony. W czasie przetwarzania komunikatu WM PAINT funkcja WndProc wybiera bitmapę w kompatybilnym kontekœcie urzšdzenia pamięciowego i używa BitBlt, zamiast SetDIBitsToDevice do wyœwietlenia bitmapy w obszarze roboczyrn. Uzy- skuje szerokoœć i wysokoœć bitmapy przez wywołanie GetObject ze strukturš BIT MAP w uchwycie bitmapy. Nie musisz inicjować bitów pikseli DDB podczas tworzenia bitmapy z CreateDIBit- map. Możesz to zrobić póŸniej, wywohxjšc SetDIBits. Funkcja ta ma następujšcš składnię: iLines = SetDIBits ( hdc, // uchwyt kontekstu urzddzenia hBitmap. // uchwyt bitmapy yScan, // pierwsza linia skanowania do konwersji cyScans, // liczba linii skanowania do konwersji pBits, // wskaŸnik do bitów pikseli pInfo, // wskaŸnik do informacji o DIB fClrUse) : // flaga użycia kolorów Funkcja SetDIBits używa tablicy kolorów w strukturze BTTMAPINFO do konwersji bitów na format zależny od urzšdzeń. Uchwyt kontekstu urzšdzenia jest wyma- gany jedynie wówczas, gdy ostatni argument ma wartoœć DIB PAL COLORS. Od DDB do DIB Funkcjš pokrewnš funkcji SetDIBits jest GetDIBits. Możesz jej użyć do konwersji DDB na DIB: Rozdział 15: Bitmapa niezależna od mzšdzeń 727 int WINAPI GetDIBits ( hdc, // uchwyt kontekstu urzddzenia hBitmap, // uchwyt bitmapy yScan, // pierwsza linia skanowania do konwersji cyScans, // liczba linii skanowania do konwersji pBits, // wskaŸnik do bitów pikseli (zwracany) pInfo, // wskaŸnik do informacji o DIB (zwracany) fClrUse) ; // flaga użycia kolorów Jednak obawiam się, że funkcja ta nie jest po prostu odwrotnoœciš SetDIBits. W przypadku ogólnym, jeœli dokonasz konwersji DIB na DDB, używajšc Create- DIBitmap oraz SetDIBits, a następnie dokonasz ponownej konwersji na DIB, uży- wajšc GetDIBits, nie uzyskasz tego, od czego zaczynałeœ. Dzieje się tak, ponie- waż częœć informacji zostaje utracona w czasie konwersji DIB na format zależny od urzšdzeń. To, ile informacji przepadnie, zależy od konkretnego trybu graficz- nego Windows, w jakim pracujesz w czasie dokonywania konwersji. Prawdopodobnie będziesz miał niewiele okazji zastosowania funkcji GetDIBits. Pomyœl o tym: w jakich okolicznoœciach twój program będzie miał uchwyt bit- mapy, nie majšc przede wszystkim danych użytych do utworzenia bitmapy? Może w Schowku? Lecz Schowek daje automatycznš konwersje na DIB. Funkcja Get- DIBits jest przydatna jedynie wtedy, gdy œcišgasz ekran, tak jak robił to program BLOWUP w rozdziale 14. Nie będę omawiał tej funkcji, ale nieco informacji na jej temat można znaleŸć w Knowledge Base article Q80080. Sekcja DIB Mam nadzieję, że teraz dobrze czujesz różnicę między bitmapami zależnymi od urzšdzeń a bitmapami od nich niezależnymi. DIB może mieć jednš z kilku orga- ruzacji kolorów; DDB musi być albo monochromatyczna, albo mieć taki sam for- mat jak rzeczywiste urzšdzenie wyjœciowe. DIB jest plikiem lub blokiem pamię- ci; DDB jest bitmapowym obiektem GDI i jest reprezentowana przez uchwyt bit- mapy. DIB może być wyœwietlons lub konwertowana na DDB i z powrotem, lecz to wišże się z procesem konwersji między bitmapami niezależnymi od urzšdzeń a bitmapami charakterystycznymi dla urzšdzenia. Teraz zbliżamy się do poznania funkcji, która zdaje się łamać te zasady. Została wprowadzona w 32-bitowych wersjach Windows i nazywa się CreateDIBSection. Oto jej składnia: hBitmap = CreateDIBSection ( hdc, // uchwyt kontekstu urzddzenia pInfo, // wskaŸnik do informacji o DIB fColorUse, // okreœlenie rodzaju danych koloru ppBits, // wskaŸnik do zmiennej wskaŸnikowej hSection, // uchwyt obiektu odwzorowania pliku dwOffset) ; // offset do bitów w obiekcie odwzorowania pliku CreateDIBSection to jedna z najważniejszych funkcji w Windows API (a przynaj- mniej, jeżeli dużo pracujesz z bitmapami), lecz jest tak niesamowita, że może zostać uznana za nader ezoterycznš i trudnš do zrozumienia. Rozpoczrujmy od samej nazwy funkcji. Wiemy, co to jest DIB, lecz czym do dia- bła jest "sekcja DIB"? Kiedy po raz pierwszy spojrzałeœ na funkcję CreateDIBSec- tion, może zaczšłeœ się zastanawiać, czy nie pracuje ona jedynie na jakiejœ częœci 728 Częœć II: Grafika DIB. Jest to bliskie prawdy. To, co robi CreateDIBSection, to istotnie utworzenie częœci (sekcji) DIB - bloku pamięci na bity pikseli bitmapy. Spójrzmy teraz na wartoœć zwracanš. Jest to uchwyt do obiektu bitmapowego GDI. Ta wartoœć zwracana jest przypuszczalnie najbardziej zwodniczym aspek- tem wywołania CreateDIBSection zdaje się sugerować, że ta funkcja działa podobnie do CreateDIBitmap. Tak, sš podobne, ale zarazem całkowicie odmienne. W rze- czywistoœci uchwyt bitmapy zwracany z CreateDIBSection znacznie różni się od uchwytu bitmapy zwracanego ze wszystkich funkcji tworzšcych bitmapy, jakie napotkaliœmy rozdziale w tym i poprzednim. Gdy już zrozumiesz charakter funkcji CreateDIBSection, możesz zastanowić się, dlaczego wartoœć zwracana nie została zdefiniowana jakoœ inaczej. Może nawet dojdziesz do wniosku, że CreateDIBSection powinna była być nazwana CreateDl- Bitmap, a ta z kolei powinna była zostać nazwana, jak wczeœniej wspomniałem, CreateDDBitmap. W pierwszym podejœciu do funkcji CreateDIBSection zastanówmy się, jak możemy jš uproœcić i natychmiast jej użyć. Po pierwsze, ostatnim dwóm argumentom, hSec- tion i dwOffset, możesz nadać wartoœci, odpowiednio, NULL i 0. Wykorzystanie tych argumentów omówię pod koniec rozdziału. Po drugie, parametr hdc używany jest jedynie wówczas, gdy parametr fColorUse ma wartoœć DIB- PAL COLORS. Jeœli fColorUse wynosi DIB RGB COLORS (lub 0), hdc jest ignorowane. (Nie jest tak w przypadku CreateDIBitmap, gdzie parametr hdc jest używany do uzyskania for- matu koloru urzšdzenia, z którym ma być kompatybilna DDB). Zatem w swojej najprostszej postaci CreateDIBSection wymaga jedynie drugiego i czwartego argumentu. Drugi argument to wskaŸnik do struktury BITMAPIN- FO. Z takim wskaŸnikiem mieliœmy już poprzednio do czynienia. Mam nadzieję, że zdefiniowanie wskaŸnika do wskaŸnika w czwartym argumencie nie odebra- ło ci zapahx. W rzeczywistoœci jest to doœć proste, gdy się używa funkcji. Załóżmy, że chcesz utworzyć DIB 384X256 bitów, 0 24 bitach na piksel. Format 24-bitowy jest najprostszy, ponieważ nie wymaga tablicy kolorów, zatem dla pa- rametru BITMAPINFO możemy zastosować strukturę BITMAPINFOHEADER. Definiujesz trzy zmienne: strukturę BITMAPINFOHEADER, wskaŸnik do BYTE i uchwyt bitmapy: BITMAPINFOHEADER bmih ; BYTE * pBits ; HBITMAP hBitmap ; Teraz zainicjuj pola struktury BITMAPINFOHEADER: bmih->biSize = sizeof (BITMAPINFOHEADER) ; bmih->biWidth = 384 ; bmih->biHeight = 256 ; bmih->biPlanes = 1 ; bmih->biBitCount = 24 ; bmih->biCompression = BIţRGB ; bmih->biSizeImage = 0 ; bmih->biXPelsPerMeter = 0 ; bmih->biYPelsPerMeter = 0 ; bmih->biClrUsed = 0 ; bmih->biClrImportant = 0 ; Rozdział 15: Bitmapa niezależna od urzšdzeń 72g Po tych ograniczonych do minimum przygotowaniach gotowi jesteœmy do wy- wołania funkcji: hBitmap = CreateDIBSection (NULL, (BITMAPINFO *) &bmih, 0, &pBits, NULL, 0) ; Zauważ, że jako drugi argument bierzemy jak zwykle adres struktury BITMA- PINFOHEADER, lecz także, co jest niezwykłe, adres pBits - wskaŸnika do BYTE. Tak więc czwarty argument jest wskaŸnikiem do wskaŸnika, jak tego wymaga funkcja. Oto co robi funkcja CreateDIBSection: bada strukturę BITMAPINFOHEADER i alo- kuje blok pamięci, który może pomieœcić bity pikseli DIB. (W tym szczególnym przypadku blok ma rozmiar 384x256x3 bajtów). W parametrze pBits, który do- starczyłeœ, zapisuje wskaŸnik do tego bloku pamięci. Funkcja zwraca także uchwyt do bitmapy, który jak powiedziałem, nie jest dokładnie taki sam, jak uchwyt zwracany przez funkcję CreateDIBitmap i inne funkcje tworzšce bitmapy. Jednak jeszcze nie skończyliœmy. Piksele bitmapy sš niezainicjowane. Czytajšc j plik DIB, możesz po prostu przekazać parametr pBits do funkcji ReadFile i wczy- tać je. Możesz je też ustawić "ręcznie" w kodzie programu. Program DIBSECT przedstawiony na rysunku 15-11 jest podobny do programu DIBCONV, z tš różnicš, że wywołuje funkcję CreateDIBSection zamiast CreateDl- Bitmap. DIBSECT.C /* DIBSECT.C - Wyœwietla sekcję DIB w obszarze roboczym okna (c) Charles Petzold, 1998 */ ţţinclude llinclude ilinclude "resource.h" LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; TCHAR szAppNameC] = TEXT ("DibSect") ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CS VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE BRUSH) wndclass.lpszMenuName = szAppName ; wndclass.lpszClassName = szAppName ; 730 Częœć II: Grafika P (cišg dalszy ze strony 729) if (!RegisterClass (&wndclass)) ( MessageBox (NULL, TEXT ("This program requires Windows NT!"), ! szAppName, MBţICONERROR) ; return 0 ; I hwnd = CreateWindow (szAppName, TEXT ("DIB Section Display"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW USEDEFAULT, j CW_USEDEFAULT, CW USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) . TranslateMessage (&msg> ; DispatchMessage (&msg) ; ) return msg.wParam ; ) HBITMAP CreateDibSectionFromOibFile (PTSTR szFileName) ( BITMAPFILEHEADER bmfh ; BITMAPINFO * pbmi ; BYTE * pBits ; BOOL bSuccess ; DWORD dwInfoSize, dwBytesRead ; HANDLE hFile ; HBITMAP hBitmap ; // Otwórz plik: tylko do odczytu, zabezpiecz przed zapisem hFile = CreateFile (szFileName, GENERIC_READ, FILEţSHAREţREAD, NULL, OPEN EXISTING, 0, NULL) ; if (hFile = INVALID HANDLEţVALUE) return NULL ; // Wczytaj BITMAPFILEHEADER bSuccess = ReadFile (hFile, &bmfh, sizeof (BITMAPFILEHEADER), &dwBytesRead, NULL) ; if (!bSuccess ţţ (dwBytesRead != sizeof (BITMAPFILEHEADER)) ţţ (bmfh.bfType != * (WORD *) "BM")) j.. CloseNandle (hFile) ; return NULL ; ) // Alokuj pamięć na strukturę BITMAPINFO i wczytaj jš dwInfoSize = bmfh.bfOffBits - sizeof (BITMAPFILEHEADER) ; Rozdział 15: Bitmapa niezależna od urzšdzeń pbmi = malloc (dwInfoSize) ; bSuccess = ReadFile (hFile, pbmi, dwInfoSize, &dwBytesRead, NULL) ; if (!bSuccess ţţ (dwBytesRead != dwInfoSize)) l 1: free (pbmi) ; CloseHandle (hFile) ; return NULL ; Iţ // Utwórz sekcję DIB hBitma = CreateDIBSection . p (NULL, pbmi, DIBţRGB COLORS, &pBits, NULL, 0) , if (hBitmap = NULL) ( free (pbmi) ; CloseHandle (hFile) ; return NULL ; // Wczytaj bity bitmapy ReadFile (hFile, pBits, bmfh.bfSize - bmfh.bfOffBits, &dwBytesRead, NULL) ; , free (Pbmi) : CloseHandle (hFile) ; return hBitmap ; 1 , ,. LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) static HBITMAP hBitmap ; ? static int cxClient, cyClient ; static OPENFILENAME ofn ; static TCHAR szFileName CMAX_PATH], szTitleName [MAX_PATH] ; static TCHAR szFilter[] = TEXT ("Bitmap Files (*.BMP)\0*.bmp\0") TEXT ("All Files (*.*)\0*.*\0\0") ; BITMAP bitmap ; HDC hdc, hdcMem ; , PAINTSTRUCT ps ; switch (message) case WM_CREATE: ofn.lStructSize = sizeof (OPENFILENAME) ; ofn.hwndOwner = hwnd ; ofn.hInstance = NULL ; j. ofn.lpstrFilter = szFilter ; ofn.lpstrCustomFilter = NULL ; ofn.nMaxCustFilter =.0 ; k ofn.nFilterIndex = 0 ; ofn.lpstrFile = szFileName ; ofn.nMaxFile = MAX PATH ; ofn.lpstrFileTitle = szTitleName ; I ofn.nMaxFileTitle = MAX PATH ; ofn.lpstrInitialDir = NULL ; ' ' Ei.;.. 732 Częœć II: Grafika (cišg dalszy ze strony 731) ofn.lpstrTitle = NULL ; ofn.Flags = 0 ; ofn.nFileOffset = 0 ; ofn.nFileExtension = 0 ; ofn.lpstrDefExt = TEXT ("bmp") ; ofn.lCustData = 0 ; ofn.lpfnHook = NULL ; ofn.lpTemplateName = NULL ; return 0 ; case WM_SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; return 0 ; case WM COMMAND: switch (LOWORD (wParam)) ( case IDM FILE OPEN: // ldyœwietl okno dialogowe Otwórz if (!GetOpenFileName (&ofn)) return 0 ; // Jeœli bitmapa już jest, usuń jd if (hBitmap) ( Delete0bject (hBitmap) hBitmap = NULL ; ) // Utwórz sekcję DIB na podstawie pliku DIB SetCursor (LoadCursor (NULL, IDC WAIT)) ; ShowCursor (TRUE) ; hBitmap = CreateDibSectionFromDibFile (szFileName) ; ShowCursor (FALSE) : SetCursor (LoadCursor (NULL, IDC ARROW)) ; // Wyczyœć obszar roboczy okna przed uaktualnieniem InvalidateRect (hwnd, NULL, TRUE) ; if (hBitmap = NULL) ( MessageBox (hwnd, TEXT ("Cannot load DIB file"), szAppName, MB OK ţ MBţICONEXCLAMATION) ; ) return 0 ; ) break ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) : Rozdział 15: Bitmapa niezależna od urzšdzeń 733 if (hBitmap) f GetObject (hBitmap, sizeof (BITMAP), &bitmap) : hdcMem = CreateCompatibleDC (hdc) ; SelectObject (hdcMem, hBitmap) : BitBlt (hdc. 0, 0. bitmap.bmWidth. bitmap.bmHeight, hdcMem, 0, 0, SRCCOPY) : DeleteDC (hdcMem) ; ) EndPaint (hwnd, &ps) : return 0 ; case WM_DESTROY: if (hBitmap) Delete0bject (hBitmap) ; PostOuitMessage (0) ; return 0 : 1 return DefWindowProc (hwnd, message, wParam, lParam) ; DIBSECT.RC (fragmenty) i // Microsoft Developer Studio generated resource script. ilinclude "resource.h" ţţinclude "afxres.h" ///////////////////////////////////////////////////////////////////////////// !/ Menu DIBSECT MENU DISCARDABLE BEGIN POPUP "&File" ' BEGIN MENUITEM "&Open", IDM FILE OPEN END END RCE.H (fra menty) RESOU 9 // Microsoft Developer Studio generated include file. // Used by DibSect.rc 4ldefine IDMţFILE OPEN 40001 Rysunek 15-11. Program DIBSECT Zwróć uwagę na różnice między ţnkcjš CţteBitmapObjectFromDibFile w DIBCONV i funkcjš CreateDibSectionFromDibFile w programie DIBSECT. DIBCONV wczytuje 734 Częœć 11: Grafika za jednym zamachem cały plik, a następnie przekazuje wskaŸniki do bloku pamię- ci DIB funkcji CreateDIBitmap. DIBSECT czyta najpierw strukturę BTTMAPFILEHE- ADER, potem okreœla rozmiar struktury BITMAPáVFO. Następnie alokuje pamięć stosownie do jej rozmiaru i wczytuje jš tam w drugim wywołaniu fteadFile. Funk- cja z kolei przekazuje do CreateDIBSection wskaŸniki do struktury BITMAPINFO i do zmiennej wskaŸnikowej pBits. Funkcja zwraca uchwyt bitmapy i ustawia pBits tak, aby wskazywał blok pamięci, w który funkcja wczytuje bity pikseli DIB. Blok pamięci wskazywany przez pBits należy do systemu. Gdy kasujesz bitmapy przez wywołanie DeleteObject, pamięć jest automatycznie zwalniana. Mimo to programy mogš używać wskaŸnika do bezpoœredniej zmiany bitów DIB. Fakt, że system jest właœcicielem tego bloku pamięci, powoduje, iż nie jest przyczynš spowolnienia, które obserwuje się w Windows NT, gdy aplikacja przekazuje duże bloki pamięci do API. Jak mówiłem wczeœniej, gdy wyœwietlasz DIB na monitorze, w jakimœ miejscu musi nastšpić konwersja pikseli niezależnych na piksele zależne od urzšdzenia. Czasem taka konwersja formatu może trwać długo. Spójrzmy na trzy metody, które wykorzystywaliœmy do wyœwietlania DIB: ů Gdy do wyœwietlenia DIB bezpoœrednio na ekranie używasz SetDIBitsToDevi- ce lub StretchDIBits, konwersja formatu zachodzi w czasie wywołania SetD1- BitsToDevice lub StretchDIBits ů Gdy dokonujesz konwersji DIB na DDB, stosujšc CreateDIBitmap i (być może) SetDIBits, a następnie korzystasz z BitBlt lub StretchBlt, żeby jš wyœwietlić, konwersja formatu następuje w czasie wykonania CreateDIBitmap, jeœli usta- wiona jest flaga CBM INIT, lub podczas wywołania SetDIBits. ů Gdy tworzysz sekcję DIB używajšc CreateDIBSection, a następnie wyœwietlasz jš wykorzystujšc BitBlt lub StretchBlt, konwersja formatu zachodzi w czasie wywołania BitBlt lub StretchBlt. Przeczytaj ponownie ostatnie zdanie i upewnij się, że zrozumiałeœ je właœciwie. Jest to jeden z aspektów odróżniajšcych uchwyt bitmapy zwracany z CreateDIB- Section od innych uchwytów bitmap, jakie do tej pory spotykaliœmy. Ten uchwyt bitmapy w rzeczywistoœci odnosi się do DIB, który zapisany jest w pamięci utrzy- mywanej przez system, do której jednak aplikacja ma dostęp. DIB jest w razie koniecznoœci konwertowana na okreœlony format koloru, co zwykle dzieje się wtedy, gdy jest wyœwietlana przy zastosowaniu BitBlt lub StretchBlt. Możesz także wybrać uchwyt bitmapy w kontekœcie urzšdzenia pamięciowego i użyć funkcji GDI, żeby w nim rysować. Wynik zostanie odzwierciedlony w bi- tach pikseli DIB wskazanych przez zmiennš pBits. Z uwagi na dzielenie na partie wywołań GDI w systemie Windows NT, zanim sięgniesz do bitów "ręcznie", po rysowaniu w kontekœcie urzšdzenia pamięciowego wykonaj GdiFlush. W programie DIBSECT zaniechaliœmy użycia zmiennej pBits, ponieważ nie była już więcej potrzebna w programie. Jeżeli potrzebujesz pozmieniać bity bezpoœred- nio, co jest głównym powodem użycia CreateDIBSection, wstrzymaj się z jej usu- nięciem. Wyglšda na to, że póŸniej, po wywołaniu CreateDIBSection, nie ma spo- sobu uzyskania wskaŸruka do bitów. Rozdział 15: Bitmapa niezależna od urzšdzeń 735 Więcej o odmiennoœci sekcji DIB Uchwyt bitmapy zwracany z CreateDIBitmap ma tę samš organizację płaszczyzn i bitów na piksel co urzšdzenie wskazane przez parametr hdc funkcji. Możesz to sprawdzić wywołujšc GetObject ze strukturš BITMAP. CreateDIBSection jest inna. Jeœli wywołasz GetObject ze strukturš BTTMAP z uchwy- tem bitmapy zwracanym z funkcji, okaże się, że bitmapa ma takš samš organiza- cję kolorów, jakš okreœlajš pola struktury BITMAPINFOHEADER. Możesz jed- nak wybrać ten uchwyt w kontekœcie urzšdzenia pamięciowego kompatybilne- go z monitorem. Oczywiœcie, jest to sprzeczne z tym, co powiedziałem w ostat- nim rozdziale na temat DDB, lecz właœnie dlatego utrzymuję, że uchwyt bitma- py sekcji DIB różni się od innych. Inna osobliwoœć: jak pamiętasz, długoœć danych dotyczšcych pikseli wiersza wyrażona w bajtach w DIB jest zawsze wielokrotnoœciš 4. Długoœć wierszy w baj- tach dla obiektów bitmapowych GDI, jakš możesz odczytać z pola bmWidthBytes struktury BITMAP używanej z funkcjš GetObject, jest zawsze wielokrotnoœciš 2. Jeżeli ustawiłeœ wyżej wymienionš strukturę BITMAPINFOHEADER tak, aby miała 24 bity na piksel i szerokoœć przykładowo 2 pikseli, a następnie wywołasz GetObject, to stwierdzisz, że pole bmWidthBytes jest równe 8, a nie 6. Z uchwytem bitmapy zwracanym z funkcji CreateDIBSection możesz także wy- wołać funkcję GetObject ze strukturš DIBSECTION w następujšcy sposób: GetObject (hBitmap, sizeof (DIBSECTION), &dibsection) : To nie zadziała z uchwytem bitmapy zwracanym z żadnej innej funkcji tworzšcej bitmapy. Struktura DIBSECTION zdefiniowana jest tak jak poniżej: typedef struct tagDIBSECTION // ds t BITMAP // struktura BITMAP dsBm BITMAPINFOHEADER dsBmih ; // naglówek informacyjny DIB DWORD dsBitfields C37 ; // maski kolorów HANDLE dshSection ; // uchwyt obiektu odwzorowania pliku DWORD dsOffset ; // offset do bitów pikseli 1 DIBSECTION, * PDIBSECTION ; Struktura ta zawiera zarówno strukturę BITMAP, jak i strukturę BITMAPINFO- HEADER. Ostatnie dwa pola sš ostatnimi dwoma argumentami przekazywany- mi do CreateDIBSection i wkrótce je omówię. Struktura DIBSECTION daje większoœć informacji na temat bitmapy, jakie mogš być potrzebne, z wyjštkiem tablicy kolorów. Gdy wybierasz uchwyt bitmapy sekcji DIB w kontekœcie urzšdzenia pamięciowego, możesz uzyskać tablicę kolorów F przez wywołanie GetDIBCoIorTable: hdcMem = CreateCompatibleDC (NULL) ; SelectObject (hdcMem, hBitmap) : GetDIBCoIorTable (hdcMem, uFirstIndex, uNumEntries, &rgb) ; DeleteDC (hdcMem) ; Podobnie, wywołujšc funkcję SetDIBCoIorTable, możesz okreœlić pozycje tablicy kolorów. 736 Częœć II, Grafika Opcja odwzorowania pliku Nie omówiłem dotychczas ostatnich dwóch argumentów funkcji CreateDIBSec- tion, będšcych uchwytami obiektu odwzorowania pliku oraz odległoœciš do miej- sca, w którym zaczynajš się w tym pliku bity bitmapy. Obiekt odwzorowania pliku pozwala traktować go tak, jakby znajdował się on w pamięci. Oznacza to, że masz dostęp do pliku przy użyciu wskaŸników, lecz on sam nie musi być w całoœci umieszczony w pamięci. W przypadku dużych DIB technika ta może pomóc w ogranicżeniu wymagań pamięciowych. Bity pikseli DIB mogš pozostać na dysku, a mimo to nadal moż- na do nich sięgać tak, jakby były w pamięci, choć z ograniczeniem szybkoœci. Pro- blem w tym, że gdy bity pikseli mogš naprawdę pozostać zapisane na dysku, nie mogš stanowić częœci rzeczywistego pliku DIB. Muszš znajdować się w jakimœ innym pliku. Zróbmy demonstrację. Funkcja pokazana poniżej jest bardzo podobna do funkcji tworzšcej sekcję DIB w programie DIBSECT, z tš różnicš, że nie wczytuje bitów pikseli do pamięci; zamiast tego przekazuje do funkcji CreateDIBSection obiekt odwzorowania pliku i offset: HBITMAP CreateDibSectionMappingFromFile (PTSTR szFileName) ( BITMAPFILEHEADER bmfh ; BITMAPINFO * pbmi ; BYTE * pBits ; BOOL bSuccess ; DWORD dwInfoSize, dwBytesRead ; HANDLE hFile, hFileMap ; HBITMAP hBitmap ; hFile = CreateFile (szFileName. GENERIC_READ ţ GENERIC_WRITE, 0ů // Nie dzielona! NULL, OPENţEXISTING, 0, NULL) ; if (hFile = INVALID HANDLE VALUE) return NULL ; bSuccess = ReadFile (hFile, &bmfh, sizeof (BITMAPFILEHEADER), &dwByt.esRead, NULL) ; if (!bSuccess ţţ (dwBytesRead != sizeof (BITMAPFILEHEADER)) ţţ (bmfh.bfType != * (WORD *) "BM")) ( CloseHandle (hFile) ; return NULL ; l dwInfoSize = bmfh.bfOffBits - sizeof (BITMAPFILEHEADER) ; pbmi = malloc (dwInfoSize) ; bSuccess = ReadFile (hFile, pbmi, dwInfoSize, &dwBytesRead, NULL) ; if (!bSuccess ţţ (dwBytesRead != dwInfoSize)) ( free (pbmi) ; CloseHandle (hFile) ; return NULL ; Rozdział 15: Bitmapa niezależna od urzšdzeń } hFileMap = CreateFileMapping (hFile, NULL, PAGEţREADWRITE, 0, 0, NULL) ; hBitmap = CreateDIBSection (NULL, pbmi, DIBţRGB COLORS, &pBits, hFileMap, bmfh.bfOffBits) ; free (pbmi) : rećurn hBitmap : Niestety, to nie działa. W dokumentacji CreateDIBSection zaznaczono, że dwOffset (ostatni argument funkcji) musi być wielokrotnoœciš rozmiaru DWORD. Choć rozmiar nagłówka informacyjnego oraz rozmiar tablicy kolorów jest zawsze wie- lokrotnoœciš 4, nagłówek pliku bitmapy nie jest. Ma on 14 bajtów. Zatem bmfh.bfOf fBits nigdy nie jest wielokrotnoœciš 4. Podsumowanie Jeœli masz małš DIB i potrzebujesz często manipulować bitami pikseli, możesz jš wyœwietlić za pomocš funkcji SetDIBitsToDevice oraz StretchDIBits. Jednak dla większych DIB technika ta wišże się ze spowolnieniem działania, w szczególno- œci na monitorach 8-bitowych i w systemie Windows NT. Konwersji DIB na DDB możesz dokonać używajšc funkcji CreateDIBitmap oraz SetDIBits. Wyœwietlanie bitmapy będzie wtedy zwišzane z szybkimi funkcjami BitBlt i StretchBlt . Jednak nie będziesz miał już dostępu do niezależnych od urzš- dzeń bitów pikseli. Funkcja CreateDIBSection stanowi niezły kompromis. Użycie uchwytu bitmapy z BitBlt i StretchBlt daje większš szybkoœć w Windows NT niż użycie SetDIBitsTo- Device lub StretchDIBits, ale jest wolne od wad DDB. Nadal masz dostęp do bi- tów pikseli DIB. W następnym rozdziale podsumujemy nasze badania bitmap po zapoznaniu się z Windows Palette Manager. Rozdział 16 Mana er I ette Pa Ten rozdział nie powstałby, gdyby nie niedoskonałoœć sprzętu. Chociaż wiele współczesnych kart graficznych obsługuje grafikę 24 bitowš (znanš również jako "true color" lub "miliony kolorów") czy też 16 bitowš ("high color" lub "tysišce kolorów"), niektóre z nich - szczególnie stosowane w komputerach typu laptop lub w systemach graficznych o wysokiej rozdzielczoœci - opisujš kolor piksela za pomocš jedynie 8 bitów. Stosowanie 8 bitów na piksel wymusza jednoczesne użycie zaledwie 256 kolorów. Co możemy zobaczyć używajšc 256 kolorów? Zastosowanie 16 kolorów oczywi- œcie nie nadaje się do opisywania rzeczywistych obrazów z otaczajšcego nas œwia- ta, użycie zaœ tysięcy czy milionów kolorów jest do tego celu aż nazbyt wystar- czajšce, natomiast obrazy 256-kolorowe wypadajš gdzieœ poœrodku tej klasyfika- i. 256 kolorów wystarcza do opisania rzeczywistego obrazu, lecz jedynie pod warunkiem, że zostanš specjalnie do niego dobrane. System operacyjny nie może po prostu wybrać standardowego zestawu 256 kolorów oczekujšc, że będš one idealnie dopasowane do każdego obrazu. To jest właœrue zadaniem Palette Managera Windows. Okreœla on zestaw kolo- rów, które majš być używane przez dany program w 8-bitowym trybie graficz- nym. Jeœli wiadomo, że program nie będzie uruchomiany w 8-bitowyrn trybie gra- ficznym, nie ma koniecznoœci stosowania Palette Managera. Stosowanie palet Powszechrue znana paleta to deseczka używana przez malarzy do mieszania farb. Słowo to może również opisywać zestaw kolorów używanych przez artystę do namalowania obrazu. W grafice komputerowej terminem "paleta" okreœlamy ze- staw kolorów możliwych do uzyskania za pomocš graficznego urzšdzenia wyj- œciowego, takiego jak monitor. Paletš może być również tablica odwzorowań karty graficznej obsługujšcej tryb 256 kolorów. 740 Czeœć II: Grafika Sprzęt wideo Tablica odwzorowań palety kolorów znajdujšca się na karcie graficznej: ţ DAC ţ -i Czervvony Piksel ţ Tablica 8-bitowyţ řţzorowań ţ ţ bitów ţ" DAC "ţ --ł Niebieski , palety ţ'==ţ: DAC ţ Zielony W 8-bitowym trybie graficznym każdy piksel reprezentowany jest przez 8 bitów. Wartoœć piksela adresuje tablicę odwzorowań, zawierajšcš 256 wartoœci RGB. Wartoœci RGB mogš mieć długoœć 24 bitów lub mniej, zwykle 18 bitów (po 6 bi- tów na każdy z kolorów podstawowych - czerwony, zielony, niebieski). Wartoœci każdego z kolorów sš wprowadzane na wejœcie przetworników cyfrowo-analo- gowych i odpowiednie sygnały docierajš na ekran monitora. Tablica odwzorowań palety kolorów może być w ogólnym przypadku wypełruo- na programowo dowolnymi wartoœciami. Sš jednak pewne przeszkody, które wiš- żš się z koniecznoœciš zapewrtienia prawidłowej obsługi palety kolorów bez wzgJędu na rodzaj karty graficznej. Po pierwsze, system Microsoft Windows musi dyspono- wać programowym interfejsem, umożliwiajšcym aplikacjom bezpoœredni dostęp do Palette Managera. Drugi problem jest bardziej poważny: ponieważ wszystkie aplikacje korzystajš z tego samego monitora, działajšc przy tym jednoczeœnie, współ- praca jednej aplikacji z tablicš odwzorowań palety mogłaby zakłócić pracę innej. Jest to moment, w którym dostrzegamy znaczenie Palette Managera Windows (wprowadzonego po raz pierwszy w Windows 3.0). System Windows rezerwuje dla własnych potrzeb 20 z 256 kolorów, pozostałe 236 może być zmieniane przez aplikacje. W szczególnych przypadkach aplikacje mogš zmieniać do 254 spoœród 256 kolorów - wszystkie oprócz czarnego i białego, które stanowiš odrębny pro- blem. Dwadzieœcia kolorów - w trybie grafiki 256-kolorowej - rezerwowanych przez Windows dJa potrzeb sytemu nazywanych jest czasem kolorami statycz- nymi; przedstawiono je poniżej. Bity piksela Wartoœć RGB Nazwa koloru Bity piksela Wartoœć RGB Nazwa koloru 00000000 00 00 00 Czarny 11111111 FF FF FF Biały 00000001 80 00 00 Ciemnoczerwony 11111110 00 FF FF Cyjan 00000010 00 80 00 Ciemnozielony 11111101 FF 00 FF Magenta 00000011 80 80 00 Ciemnożółty 11111100 00 00 FF Niebieski 00000100 00 00 80 Ciemnoniebieski 11111011 FF FF 00 Żółty 00000101 80 00 80 Ciemna magenta 11111010 00 FF 00 Zielony 00000110 00 80 80 Ciemny cyjan 11111001 FF 00 00 Czerwony 00000111 CO CO CO Jasnoszary 11111000 80 80 80 Ciemnoszary 00001000 CO DC CO Jasnozielony 11110111 AO AO A4 Œrednioszary 00001001 A6 CA FO Błękitny 11110i10 FF FR Fn u.o.......... Rozdział 16: Palette Manager 741 System Windows, pracujšc w trybach 256-kolorowych, przechowuje paletę sys- temowš, która jest identyczna ze znajdujšcš się w tablicy odwzorowań karty gra- ficznej. Domyœlna paleta systemowa przedstawiona jest w powyższej tabeli. Apli- kacje mogš zmieniać pozostałe 236 kolorów poprzez okreœlanie palet logicznych. jeœli więcej niż jedna aplikacja używa palety logicznej w tym samym czasie, Win- dows nadaje najwyższy priorytet tej, która działa w oknie aktywnym. (Jak wia- domo, okno aktywne to takie, które ma podœwietlony pasek tytułu i znajduje się na pierwszym planie, przykrywajšc wszystkie inne okna). Sprawdzimy, jak to działa, na przykładzie prostego programu. Uruchomienie programu będzie prawdopodobnie wymagało przełšczenia karty ţ, I, graficznej w tryb 256 kolorów. Kliknij pulpit prawym przyciskiem myszy, wy- bierz Właœciwoœci, a następnie kartę Ustawienia. Wyœwietlanie odcieni szaroœci Program GRAYSI, przedstawiony na rysunku 16-1 nie korzysta z Palette Mana- gera Windows, zamiast tego próbuje wyœwietlić 65 odcieni szaroœci jako gamę barw od czarnego do białego. GRAYSI.C /* GRAYSl. - Odcienie szaroœci (c) Charles Petzold, 1998 */ ţţinclude LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) static TCHAR szAppNameC] = TEXT ("Graysl") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CS VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL IDI ŽPPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC-ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITEţBRUSH) wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) f MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MBţICONERROR) ; Częœć II: Grafika (cigg dalszy ze strony 741) return 0 ; ) hwnd = CreateWindow (szAppName, TEXT ("Shades of Gray 111"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) f TranslateMessage (&msg) ; DispatchMessage (&msg) ; l return msg.wParam ; J LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ( static int cxClient, cyClient ; HBRUSH hBrush ; HDC hdc ; int i ; PAINTSTRUCT ps ; RECT rect ; switch (message) ( case WM_SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; // Rysuje gamę odcieni szaroœci for ( i = 0 ; i < 65 ; i++) ( rect.left = i * cxClient / 65 ; rect.top = 0 ; rect.right = (i + 1) * cxClient / 65 ; rect.bottom = cyClient ; hBrush = CreateSolidBrush (RGB (min (255, 4 * i), min (255, 4 * i), min (255, 4 * i))) ; FillRect (hdc, &rect, hBrush) ; Delete0bject (hBrush) ; ) EndPaint (hwnd, &ps) ; Rozdział 16: Palette Manager 743 return 0 ; case WM_DESTROY: PostOuitMessage (0) : return 0 ; ) return DefWindowProc (hwnd, message, wParam, lParam) ; Rysunek 16-1. Program GRAYSI W trakcie wykonywania fragmentu kodu zwišzanego z komunikatem WM PA- INT program wywołuje 65-krotnie funkcję FillRect, za każdym razem tworzšc pędzel o innym odcieniu szaroœci. Odcieniom szaroœci odpowiadajš następujšce wartoœci RGB: (0, 0, 0), (4, 4, 4), (8, 8, 8) itd. aż do ostatniego (255, 255, 255). Aby ta ostatnia wartoœć mogła być osišgnięta, wywoływane jest specjalne makro min w funkcji CreateSolidBrush. Jeœli uruchomisz ten program w trybie 256 kolorów, na ekranie pojawi się 65 od- cieni szaroœci, od czerni do bieli, jednak każdy z nich jest poddawany rendero- waniu przy użyciu techniki symulowania (ang. dithering). Jedyne czyste kolory to czarny, ciemnoszary (128, 128, 128), jasnoszary (192, 192, 192) i biały. Pozosta- y łe kolory to różne kombinacje ustawienia bitów w bajcie. Gdybyœmy zamiast w - pełruonych obszarów wyœwietlali linie lub tekst, używajšc 65 odcieni szaroœci, ry Windows nie zastosowałby techniki symulowania i zobaczylibyœmy tylko te czte kolory. Gdybyœmy wyœwietlili mapę bitowš, Windows użyłby około 20 standar- dowych kolorów. Możesz się o tym przekonać, uruchamiajšc jeden z programów opisanych w ostatnim rozdziale. System Windows nie stosuje techniki symulo- wania do bitmap. Program GRAYS2 rzedstawion na r sunku 16-2 demonstruje najważniejsze p y y funkcje Palette Managera. GRAYS2.C ,* GRAYS2.C - Odcienie szaroœci po zastosowaniu Palette Managera (c) Charles Petzold, 1998 */ ifinclude LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance. .. PSTR szCmdLine, int iCmdShow) ( static TCHAR szAppName[7 = TEXT ("Grays2") : HWND hwnd : MSG msg WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CS VREDRAW : wndclass.lpfnWndProc = WndProc ; 744 Częœć II: Grafika (cigg dalszy ze strony 743) wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) ( MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MBţICONERROR) ; return 0 ; hwnd = CreateWindow (szAppName, TEXT ("Shades of Gray ţ12"), WS OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW USEDEFAULT, CWţUSEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) ( TranslateMessage (&msg) ; DispatchMessage (&msg) ; ) return msg.wParam ; ) LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ( static HPALETTE hPalette ; static int cxClient, cyClient ; HBRUSH hBrush ; HDC hdc ; int i ; LOGPALETTE * p1p ; PAINTSTRUCT ps ; RECT rect ; switch (message) ( case WM CREATE: // Ustalenie wartoœci pól struktury LOGPALETTE i utworzenie palety plp = malloc (sizeof (LOGPALETTE) + 64 * sizeof (PALETTEENTRY)) ; plp->palVersion = 0x0300 ; plp->palNumEntries = 65 ; for ( i = 0 ; i < 65 ; i++) ( Rozdział 16: Palette Manager 745 plp->palPalEntryCi].peRed = (BYTE) min (255, 4 * i) ; plp->palPalEntryCi].pe0reen = (BYTE) min (255, 4 * i) ; p1p->palPalEntryCi].peBlue = (BYTE) min (255, 4 * i) ; plp->palPalEntryCi].peFlags = 0 : hPalette = CreatePalette (plp) : free (plp) ; return 0 : case WM_SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; return 0 ; j; i ţ'.. case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; // Zaznacz i zrealizuj paletę w kontekœcie urzddzenia SelectPalette (hdc, hPalette, FALSE) ; RealizePalette (hdc) : i:. // Rysuj gamę odcieni szaroœci for ( i = 0 ; i < 65 ; i++) ( rect.left = i * cxClient / 64 ; rect.top = 0 ; ! rect.right = (i + 1) * cxClient / 64 ; rect.bottom = cyClient ; hBrush = CreateSolidBrush (PALETTERGB (min (255, 4 * i), min (255, 4 * i), min (255, 4 * i))) : FillRect (hdc, &rect, hBrush) ; Delete0bject (hBrush) ; ) EndPaint (hwnd, &ps) ; return 0 ; case WM_OUERYNEWPALETTE: if (!hPalette) return FALSE : hdc = GetDC (hwnd) ; SelectPalette (hdc, hPalette, FALSE) ; RealizePalette (hdc) : InvalidateRect (hwnd, NULL, TRUE) ; ReleaseDC (hwnd, hdc) ; Ih return TRUE : 7 case WM_PALETTECHANGED: ; if (!hPalette ţţ (HWND) wParam == hwnd) break : ţi:'. hdc = GetDC (hwnd) : SelectPalette (hdc, hPalette, FALSE) : 746 Czgœć II: Grafika (cišg dalszy ze strony 745) RealizePalette (hdc) UpdateColors (hdc) ; ReleaseDC (hwnd, hdc) ; break : case WM_DESTROY: Delete0bject (hPalette) ; PostOuitMessage (0) ; return 0 ; ) return DefWindowProc (hwnd, message, wParam, lParam) ; Rysunek 16-2. Program GRAYS2 Pierwszym krokiem, który należy wykonać używajšc Palette Managera, jest utwo- rzenie logicznej palety poprzez wywołanie funkcji CreatePalette. Paleta logiczna zawiera wszystkie kolory, tzn. 236 kolorów niezbędnych dla aplikacji. Program GRAYSI wykonuje to zadanie po wystšpieniu komunikatu 4VMţCREATE. Ini- cjuje pola struktury LOGPALETTE ("paleta logiczna") i przekazuje funkcji Cre- atePalette wskaŸnik do tej struktury. Funkcja CreatePalette zwraca uchwyt palety logicznej. Uchwyt jest przechowywany w zmiennej statycznej hPalette. Struktura LOGPALETTE jest zdefiniowana następujšco: typedef struct ( WORD palVersion; WORD palNumEntries; PALETTEENTRY palPalEntry[1]; ) LOGPALETTE, *PLOGPALETTE; Pierwsze pole struktury przyjmuje wartoœć 0x0300, co oznacza zgodnoœć z systemem Windows 3.0. Drugie pole zawiera liczbę wartoœci znajdujšcych się w tablicy palety. Trzecim polem struktury LOGPALETTE jest tablica struktur typu PALETTEENTRY, przyporzšdkowana każdej pozycji palety. Struktura typy PALETTEENTRY ma na- stępujšcš postać: typedef struct ( BYTE peRed; BYTE peGreen; BYTE peBlue; BYTE peFlags; ) PALETTEENTRY, *PPALETTEENTRY; Każda ze struktur PALETTEENTRY definiuje wartoœć koloru RGB, która ma zna- leŸć się w palecie. Zauważ, że LOGPALETTE została zdefiniowana jedynie ze względu na koniecz- noœć reprezentowarua tablicy struktur PALETTEENTRY. Programujšc należy pa- miętać o zarezerwowaniu obszaru pamięci dla jednej struktury LOGPALETTE i dodatkowych struktur typu PALETTEENTRY. Program GRAYS2 operuje na 65 Rozdział 16: Palette Manager 747 odcieniach szaroœci, korueczne więc było zarezerwowanie pamięci dla struktury typu LOGPALETTE oraz 64 dodatkowych struktur typu PALETTEENTRY. Zmien- nej palNumEntries przypisano wartoœć 65. Następnie GRAYS2 wykonuje pętlę od 0 do 64, obliczajšc wartoœci kolejnych poziomów szaroœci (wynoszšce 4-krotnoœć indeksu pętli, jednak nie więcej niż 255) i przypisujšc je odpowiednim zmien- nym pefted, peGreen oraz peBlue. Polu peFlags przypisywana jest wartoœć 0. Pro- gram przekazuje wskaŸnik do tego obszaru pamięci funkcji CreatePalette, zapa- miętuje uchwyt palety jako zmiennš statycznš i zwalnia pamięć. Paleta logiczna jest obiektem GDI. Programy powinny usuwać wszystkie palety logiczne, które utworzyły. W tym wypadku usuwa je proces WndProc, wywoły- wany przez funkcję DeleteObject dla komunikatu WM DESTROY. Zauważ, że paleta logiczna nie zależy od kontekstu urzšdzenia. Zanim jednak będziesz mógł skorzystać z tej własnoœci, musisz przypisać jakiœ kontekst urzš- dzenia i "zrealizować" paletę. Po otrzymaniu komunikatu WM PAINT funkcja SelectPalette wybiera paletę logicznš w kontekœcie urzšdzerua. Działa więc po- dobnie jak funkcja SelectObject, ale ma dodatkowy, trzeci argument. Zwykle ar- gument ten przyjmuje wartoœć FALSE. Jeœli przyjmie wartoœć TRUE, paleta staje się tzw. paletš tła, co oznacza, że pobiera dowolne nieużywane dane znajdujšce się w palecie systemowej wtedy, gdy wszystkie inne programy zrealizujš już swoje palety. Jednoczeœnie tylko jedna logiczna paleta może być wybrana w kontekœcie urzš- dzenia. Wspomniana funkcja zwraca uchwyt palety logicznej zaznaczonej uprzed- nio w kontekœcie urzšdzenia. Uchwyt ten można zapisać jako zmiennš i wyko- rzystać ponownie. Za poœrednictwem funkcji RealizePalette system Windows "realizuje" paletę lo- gicznš w kontekœcie urzšdzenia, przypisujšc kolory palecie systemowej, która z kolei odpowiada bezpoœrednio rzeczywistej palecie karty graficznej. Najważ- niejsza częœć programu odbywa się podczas wywołania tej właœnie funkcji. Sys- tem musi okreœlić, czy okno wywołujšce funkcję jest aktywne, i ewentualnie za- wiadomić inne okna o zmiarue palety kolorów. Wkrótce powiem, jak się odbywa to powiadomienie. Przypomnij sobie, jak w programie GRAYS1 użyto makra RGB do okreœlenie ko- loru pędzla. Makro RGB tworzy 32 bitowš wartoœć typu całkowitego (long inte- ger) (wartoœć COLORREF), w której najbardziej znaczšcy bajt przyjmuje wartoœć 0, a pozostałe 3 odpowiadajš natężeniom kolorów czerwonego, zielonego i nie- bieskiego. Program korzystajšcy z Palette Managera Windows może w dalszym cišgu uży- wać wartoœci RGB do okreœlania kolorów. Jednak wartoœci RGB nie pozwalajš na uzyskanie dodatkowych kolorów palety logicznej. Niezbędne do tego celu oka- zało się makro PALETTERGB. Wspomniane dodatkowe kolory sš bardzo podob- ne do tradycyjnych RGB, tyle że najbardziej znaczšcy bajt słowa 32 bitowego przyj- muje wartoœć 2, a nie 0. Oto kilka ważnych zasad: Aby użyć koloru z palety logicznej, okreœl go, korzystajšc z wartoœci "Palette . RGB" lub "Palette Index' (ten drugi sposób zostanie objaœniony nieco póŸ- Częœć II: Grafika niej). Nie korzystaj ze standardowych wartoœci RGB, gdyż uzyskasz jeden ze standardowych kolorów, a nie kolor pochodzšcy z palety logicznej. ů Nie korzystaj z wartoœci "Palette RGB" ani "Palette Index", jeœli uprzednio nie wybrałeœ palety w kontekœcie urzšdzenia. ů Chociaż używajšc wartoœci, Palette RGB, możesz okreœlić kolor spoza palety logicznej, otrzymany kolor i tak będzie pochodził z tej palety. Na przykład po otrzymaniu komunikatu WMţPAINT w programie GRAYS2, po wybraniu i zrealizowaniu palety logicznej, próbujšc wyœwietlić coœ w kolorze czerwonym, otrzymasz pewien odcień szaroœci. Aby otrzymać kolory spoza pa- lety logicznej, musisz użyć standardowych wartoœci RGB. Komunikaty palety Jeżeli kilka programów Windows korzysta z Palette Managera jednoczeœnie, naj- wyższy priorytet dostępu do palety przyznawany jest oknu aktywnemu. Drugie w kolejnoœci jest okno, które było aktywne jako ostatnie przed oknem bieżšcym itd. Za każdym razem, kiedy uruchamiany jest nowy program, system musi re- organizować tablicę palety systemowej. Jeżeli program wybiera ze swojej palety logicznej kolor identyczny z jednym z 20 zarezerwowanych kolorów, Windows przyporzšdkuje go do tego koloru palety systemowej. Jeœli natomiast dwie lub więcej aplikacji wybierze ten sam kolor z wła- snych palet logicznych, będš one wspólnie korzystać z koloru należšcego do pa- lety systemowej. Program może obejœć tę własnoœć systemu, przypisujšc zmien- nej peFlags struktury PALETTEENTRY stałš PC NOCOLLAPSE. Pozostałe dwie możliwoœci to PC EXPLICIT, używana do wyœwietlenie palety systemowej, oraz PC RESERVED, stosowana w animacji palety. Działanie obu znaczników zosta- nie zademonstrowane w dalszej częœci rozdziału. Palette Manager Windows stosuje dwa komunikaty wysyłane do okna główne- go, pomocne w zarzšdzaniu paletš systemowš. Pierwszy to QMţQUERYNEWPALETTE, wysyłany do okna głównego, kiedy ma się ono stać aktywne. Jeœli twój program korzysta z Palette Managera do rysowa- nia w okrue, musi on obsłużyć ten komunikat. Sposób obsługi zademonstrowano w programie GRAYS2. Program otrzymuje uchwyt zwišzany z kontekstem urzš- dzenia, wybiera paletę, wywohxje funkcję RealizePalette, a następnie unieważnia okno, aby móc wygenerować komunikat WM PAINT. Procedura okna zwraca wartoœć TRUE, jeœli mamy do czynienia z paletš logicznš, lub FALSE w przeciw- nym wypadku. Za każdym razem, gdy w wyniku komunikatu WMţQUERYNEWPALETTE zmie- nia się paleta systemowa, Windows wysyła komunikat WM_PALETTECHAN- GED do wszystkich okien głównych, poczšwszy od okna, które było aktywne jako ostatnie. Pozwala to oknu aktywnemu na utrzymanie priorytetu dostępu do pa- lety. Parametr wParam przekazywany do procedury okna jest uchwytem okna aktywnego. Komunikat WMţQUERYNEWPALETTE powinien być obsłużony przez system tylko wówczas, gdy wartoœć wParam jest różna od wartoœci uchwy- tu okna programu. Rozdział 16: Palette Manager Zwykle każdy program używajšcy własnej palety kolorów wywołuje funkcje Se- lectPalette i RealizePalette, obsługujšc komunikat WMţPALETTECHANGED. Kie- dy kolejne okna wywołujš funkcję RealizePalette, Windows sprawdza najpierw zgodnoœć kolorów RGB znajdujšcych się w palecie logicznej z tymi, które już zo- stały załadowane do palety systemowej. Jeœli dwa programy potrzebujš tego sa- mego koloru, oba mogš skorzystać z koloru palety systemowej. Następnie sys- tem sprawdza, które kolory systemowe nie będš używane. Jeœli nie ma takich barw, przypisuje kolor palety logicznej najbardziej zbliżonemu z 20 zarezerwowanych kolorów palety systemowej. Jeœli nie troszczysz się o wyglšd innych programów, gdy twój jest nieaktywny, nie musisz obsługiwać komunikatu 4VMţPALETTECHANGED. W przeciwnym wypadku masz dwa wyjœcia. Program GRAYS2 demonstruje jedno z nich: obshz- gujšc WMţQUERYNEWPALETTE, pobiera kontekst urzšdzenia, wybiera paletę, a następnie wywołuje RealizePalette. W tym momencie można wywołać funkcję InvalidateRect, jak przy obsłudze WMţQUERYNEWPALETTE, jednak GRAYS2 wywołuje UpdateColors. Funkcja ta jest zwykle bardziej efektywna niż odœwieża- nie okna, zmieniajšc wartoœci pikseli okna twego programu. W większoœci programów korzystajšcych z Palette Managera obsługa komuni- katów WMţQUERYNEWPALETTE i WMţPALETTECHANGED będzie identycz- na z zaprezentowanš w programie GRAYS2. Indeks palety Program GRAYS3, przedstawiony na rysunku 16-3, jest bardzo podobny do GRAYS2, lecz przy obsłudze komunikatu WMţPAINT zamiast makro PALET TERGB zastosowano PALETTEINDEX. GRAYS3 . C /* GRAYS3.C - Odcienie szaroœci po zastosowaniu Palette Managera (c) Charles Petzold, 1998 */ Ilinclude LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) static TCHAR szAppName[] = TEXT ("Grays3") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CSţVREDRAW ; wndclass.lpfnWndProc = WndProc ; 750 Czgœć II: Grafika (cišg dalszy ze strony 749) wndclass.cbClsExtra = 0 : wndclass.cbWndExtra = 0 : wndclass.hInstance = hInstance wndclass.hIcon = LoadIcon (NULL, IDI ŽPPLICATION) : wndclass.hCursor = LoadCursor (NULL IDC ARROW) wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITEţBRUSH) : wndclass.lpszMenuName = NULL wndclass.lpszClassName = szAppName : if (!RegisterClass (&wndclass)) ( MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MBţICONERROR) : return 0 : ) hwnd = CreateWindow (szAppName, TEXT ("Shades of Gray ţţ3"). WS 0llERLAPPEDWINDOW CW USEDEFAULT, CW USEDEFAULT, CW USEDEFAULT, CW USEDEFAULT, NULL, NULL, hInstance, NULL) : ShowWindow (hwnd. iCmdShow) : UpdateWindow (hwnd) : while (GetMessage (&msg, NULL, 0, 0)) ( TranslateMessage (&msg) : DispatchMessage (&msg) : ) return msg.wParam : 1 LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ( static HPALETTE hPalette : static int cxClient, cyClient : HBRUSH hBrush : HDC hdc : int i ů LOGPALETTE * P1P ů PAINTSTRUCT Ps : RECT rect : switch (message) ( case WM CREATE: // Wypelnij pola struktury LOGPALETTE i utwórz paletę plp = malloc (sizeof (LOGPALETTE) + 64 * sizeof (PALETTEENTRY)) ; p1p->palUersion = 0x0300 : plp->palNumEntries = 65 : for (i = 0 ; i < 65 ; i++) l Rozdział 16: Palette IN„nageţ 751 plp->palPalEntry(i].peRed = (BYTE) min (255, 4 * i) ; plp->palPalEntry[i].peGreen = (BYTE) min (255, 4 * i) ; plp->palPalEntry(i].peBlue = (BYTE) min (255, 4 * i) ; plp->palPalEntry(i].peFlags = 0 ; 1 hPalette = CreatePalette (plp) ; free (plp) ; return 0 ; case WM_SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; t:..i // Wybierz i zrealizuj paletę w kontekœcie urzšdzenia "' SelectPalette (hdc, hPalette, FALSE) ; RealizePalette (hdc) ; // Rysuj gamę odcieni szaroœci for ( i = 0 ; i < 65 ; i++) f rect.left = i * cxClient / 64 ; rect.top = 0 ; rect.right = (i + 1) * cxClient / 64 ; rect.bottom = cyClient ; hBrush = CreateSolidBrush (PALETTEINDEX (i)) ; FillRect (hdc, &rect, hBrush) ; Delete0bject (hBrush) ; 1 EndPaint (hwnd, &ps) ; return 0 ; case WM OUERYNEWPALETTE: if (!hPalette) return FALSE ; hdc = GetDC (hwnd) ; SelectPalette (hdc, hPalette, FALSE) ; RealizePalette (hdc) ; E InvalidateRect (hwnd, NULL, TRUE) ; ;s ReleaseDC (hwnd, hdc) ; return TRUE ; case WM_PALETTECHANGED: if (!hPalette ţţ (HWND) wParam = hwnd) break ; hdc = GetDC (hwnd) ; SelectPalette (hdc, hPalette, FALSE) ; 752 Częœć 11: Grafika (cišg dalszy ze strony 751) RealizePalette (hdc) ; UpdateColors (hdc) ; ReleaseDC (hwnd, hdc) ; break ; case WM_DESTROY: Delete0bject (hPalette) ; PostQuitMessage (0) ; return 0 ; ) return DefWindowProc (hwnd, message, wParam, lParam) ; Rysunek 16-3. Program GRAYS3 Kolor definiowany za pomocš indeksu palety różni się od koloru palety RGB. Bardziej znaczšcy bajt słowa okreœlajšcego kolor przyjmuje wartoœć 1, natomiast wartoœciš niższego bajtu jest indeks bieżšcej palety logicznej wybranej w kontek- œcie urzšdzenia. W programie GRAYS3 paleta logiczna liczy 65 kolorów, dlatego jest indeksowana numerami od 0 do 64. Wartoœć: PALETTEINDEX(0) oznacza kolor czarny, PALETTEINDEX(32) to kolor szary, a PALETTEINDEX(64) to kolor biały. Stosowanie indeksów palety jest bardziej efektywne niż stosowanie wartoœci RGB, ponieważ Windows nie musi szukać i dopasowywać kolorów najbardziej zbliżo- nych do żšdanego. Zapytanie o możliwoœć obsługi palety Programy GRAYS2 i GRAYS3 działajš poprawnie, jeœli system Windows pracuje w 16- lub 24-bitowym trybie graficznym. Jednak w pewnych wypadkach, kiedy Ge aplikacja Windows współpracuje z Palette Managerem, może być potrzebne sprawdzerue, czy karta graficzna obsługuje własnoœci Palette Managera. Spraw- dzać można wywołujšc funkcję GetDeviceCaps, podajšc jako argumenty uchwyt sprzętowy zwišzany z kartš graficznš oraz parametr RASTERCAPS. Funkcja zwraca wartoœć typu liczba całkowita (ang. integer) złożonš z kilku znaczników. Aby sprawdzić, czy sprzęt obsługuje paletę, możesz poddać zwracanš wartoœć działaniu operatora logicznego AND ze stałš RC PALETTE w następujšcy spo- sób: PA L I RCţPALETTE & GetDeviceCaps (hdc, RASTERCAPS) Jeœli otrzymana wartoœć jest niezerowa, oznacza to, że sterownik karty graficznej obsługuje operacje na palecie kolorów. Dostępne sš wówczas trzy dodatkowe _ GetS sposoby wywołania funkcji GetDeviceCaps. Wywołarue funkcji: GetDeviceCaps (hdc, SIZEPALETTE> Rozdział 16: Palette INanageţ 753 zwraca całkowity rozmiar tablicy palety kolorów karty graficznej. Oznacza to, że na ekranie może być równoczeœnie wyœwietlana maksymalna liczba kolorów. Wynosi ona 256, ponieważ Palette Manager stosowany jest jedynie dla 8-bitowych trybów graficznych. Wywołanie funkcji: GetDeviceCaps (hdc, NUMRESERVED) zwraca liczbę kolorów tablicy palety rezerwowanych przez sterownik karty gra- ficznej dla potrzeb systemu. Liczba ta wynosi 20. Jeœli nie korzystamy z Palette Managera, aplikacja Windows pracujšca w trybie 256-kolorowym będzie mogła użyć jedynie tych 20 podstawowych kolorów. Aby móc stosować pozostałe 236 kolorów, program musi skorzystać z funkcji Palette Managera. Wywołanie funkcji: GetDeviceCaps (hdc, COLORRES) zwraca rozdzielczoœć (w bitach) wartoœci kolorów RGB ładowanych do sprzęto- wej tablicy palety. Bity kierowane sš na wejœcia przetworników cyfrowo-analo- gowych. Niektóre sterowniki kart graficznych stosujš przetwomiki 6-bitowe - wtedy zwracana wartoœć wynosi 18. Inne wyposażone sš w przetworniki 8-bito- we - wtedy zwracana wartoœć wynosi 24. Znajomoœć wartoœci rozdzielczoœci kolorów jest przydatna podczas programowa- nia. Jeżeli rozdzielczoœć na przykład wynosi 18, nie ma sensu wymagać od pro- gramu uzyskania 128 odcieni szaroœci, ponieważ możliwe jest wyœwietlenie je- dynie 64 odcieni. Żšdanie 128 odcieni spowoduje niepotrzebne wypełnienie ta- blicy palety kolorami powtarzajšcymi się. Paleta systemowa Jak wczeœniej wspomniano, paleta systemowa Windows koresponduje bezpoœred- nio z tablicš odwzorowań na karcie graficznej (sprzętowa tablica odwzorowań palety może mieć niższš rozdzielczoœć kolorów niż paleta systemowa). Program może uzyskać dowolny lub wszystkie kolory RGB z palety systemowej, wywo- hzjšc fttnkcję: GetSystemPaletteEntries (hdc, uStart, Unum, &pe) ; Powyższa funkcja będzie działać jedynie pod warunkiem, że karta graficzna ob- sługuje operacje na palecie kolorów. Drugi i trzeci argument sš zmiennymi typu liczba całkowita bez znaku (ang. unsigned integer) i oznaczajš odpowiednio in- deks poczštkowej wartoœci koloru palety oraz pożšdanš liczbę kolorów. Ostatni argument to wskaŸnik do struktury typu PALETTEENTRY. Funkcja ta może być użyta na wiele sposobów. Struktura PALETTEENTRY da się zdefiniować na przykład tak: PALETTEENTRY pe ; a wywołanie funkcji GetSystemPaletteEntries można zamknšć w pętli następujš- co: GetSystemPaletteEntries (hdc, i , 1, &pe) : gdzie parametr i zmienia się od 0 do wartoœci o 1 mniejszej niż zwracana przez funkcję GetDeviceCaps w wersji z argumentem SIZEPALETTE; w tym wypadku Czgœć II: Grafika będzie to 255. Program może również pobrać wszystkie kolory palety systemowej, definiujšc wskaŸnik do struktury PALETTEEN'TRY, a następnie rezerwujšc blok pa- mięci o rozmiarach wystarczajšcych, aby pomieœcić tyle struktur PALETTEEN'TRY, ile wynosi rozmiar palety. Funkcja GetSystemPaletteEntries pozwala na dokładne zbadanie sprzętowej tabli- cy palety. Kolory w palecie systemowej sš ułożone według rosnšcej wartoœci bi- towych pikseli, używanych do okreœlania kolorów w buforze karty graficznej. Mechanizm ten zostanie opisany w dalszej częœci rozdziału. Inne funkcje palety Program napisany dla Windows może zmieniać paletę systemowš, ale tylko po- œrednio. Pierwszy krok to utworzenie palety logicznej, która jest zwykle tablicš wartoœci kolorów RGB stosowanych przez dany program. Wywołanie funkcji CreatePalette nie powoduje żadnych zmian w palecie systemowej ani w tablicy palety na karcie graficznej. Paleta logiczna musi zostać wybrana w kontekœcie urzšdzenia, a następnie zrealizowana. Program może pobrać kolor z palety logicznej wywohxjšc funkcję: GetPaletteEntries (hPalette, uStart, uNum, &pe) ; Funkcja ta może być stosowana podobnie jak GetSystemPaletteEntries, ale pierw- szym parametrem jest uchwyt palety logicznej, a nie kontekstu urzšdzenia. Analogiczna funkcja pozwala na modyfikacje wartoœci utworzonej uprzednio palety logicznej: SetPaletteEntries (hPalette, uStart, uNum. &pe) ; Ponownie musisz pamiętać o tym, że wywołanie powyższej funkcji nie powodu- je żadnych zmian w palecie systemowej, nawet jeœli paleta jest wybrana w kon- tekœcie urzšdzenia. Funkcja rue zmienia rówrueż rozmiaru palety logicznej. Do tego celu stosuje się wywołania ResizePalette. Przedstawiona poniżej funkcja przyjmuje jako drugi argument odwołanie do wartoœci koloru RGB i zwraca indeks palety logicznej, który odpowiada wartoœci koloru RGB o odcieniu najbardziej zbliżonym do żšdanego: iIndex - GetNearestPaletteIndex (hPalette, cr) : Drugi argument to wartoœć COLORREF. Rzeczywistš wartoœć koloru RGB z pa- lety logicznej możesz uzyskać, wywołujšc funkcję GetPaletteEntries. Program, który potrzebuje więcej niż 236 kolorów pracujšc w &bitowym trybie graficznym, może wywołać funkcję GetSystemPaletteUse. Pozwala to na użycie aż 254 kolorów, gdyż system rezerwuje jedynie kolor biały i czamy. Program może wywołać tę funkcję jedynie wtedy, kiedy jego okno jest zmaksymalizowane i wy- pełrua cały ekran. Problem działań rastrowych Jak wiadomo z rozdziału 5, GDI pozwala na rysowanie lin i wypełruanie obsza- rów przy użyciu różnych trybów kreœlenia i działań rastrowych. Tryb rysowania ustawiany jest przez wywołanie funkcji SetROP2. Cyfra 2 oznacza binarne dzia- łanie rastrowe między dwoma obiektami; operacje trzeciorzędnego znaczenia sš Rozdzist 16: Palette Nlanager 755 stosowane przez wywołarue funkcji takich jak BitBlt. Wspomruane działarua ra- strowe okreœlajš, jakie relacje zachodzš między pikselami rysowanego obiektu a pikselami otoczenia, na przykład możesz tak narysować linię, że jej piksele po- jawiajš się na ekranie w wyniku logicznej operacji XOR z pikselami tła. Działania rastrowe odbywajš się przez działania na bitach piksela. Zmiana pale- ty może wpływać na sposób przeprowadzania działań rastrowych, jeœli bity pik- sela pozostajš w różnej relacji do bieżšcego koloru danego piksela. Możesz to sprawdzić, uruchamiajšc program GRAYS2 lub GRAYS3. Spróbuj prze- cišgnšć dolnš lub górnš krawędŸ okna, zmieruajšc jego rozmiar. Windows wy- œwietli przecišganš krawędŸ, stosujšc działanie rastrowe, które powoduje inwer- sję bitów pikseli tła. Oczywiœcie celem takiego działarua było zapewnienie wi- docznoœci przecišganej krawędzi okna. Jednak w programach GRAYS2 i GRAYS3 krawędzie będš prawdopodobrue mienić się wieloma kolorami. Kolory te odpo- wiadajš nieużywanym pozycjom tablicy palety, które sš wyrukiem inwersji bito- wej pikseli tła. Należy pamiętać o tym, że inwersji poddawane sš bity piksela danego koloru, a nie sam kolor. Jak widać w zamieszczonej tabeli, 20 standardowych zarezerwowanych kolorów umieszczane jest na poczštku i na końcu palety, więc odwrócenie ich bitów po- woduje jedynie zamianę kolorów w obrębie palety. Jednak kiedy zaczniesz zmie- ruać kolory palety - w szczególnoœci palety systemowej - operacje rastrowe mogš wprowadzić zamieszarue. Możesz być pewien tylko tego, że działania rastrowe zostanš poprawnie prze- prowadzone na pikselach o barwie białej i czarnej. Kolor czamy jest umieszczo- ny na pierwszej pozycji w palecie systemowej (wszystkie bity ustawione na 0), biały zaœ na ostatniej (wszystkie bity ustawione na 1). Te kolory nie mogš ulec zmianie. Wynik działań rastrowych na pikselach kolorowych obiektów można przewidzieć, przyglšdajšc się wartoœciom RGB, przyporzšdkowanym różnym wartoœciom zapisanym bitowo w tablicy palety systemowej. Prryglšdajšc sig palecie systemowej Programy pracujšce pod kontrolš systemu Windows używajš kolorów pochodzš- cych z palet logicznych. Kolory przechowywane w palecie systemowej sš dobra- ne tak, aby jak najlepiej służyć programom korzystajšcym z palet logicznych. Paleta systemowa odwzorowuje wartoœci ze sprzętowej tablicy odwzorowań karty gra- ficznej, dlatego przyglšdajšc się palecie systemowej łatwiej zrozumieć działarua aplikacji korzystajšcych z jej własnoœci. Poniżej przedstawione zostanš trzy przykładowe programy wyœwietlajšce kolo- ry palety systemowej, gdyż sš trzy różne sposoby podejœcia do tego problemu. W programie SYSPALI przedstawionym na rysunku 16-4 zastosowano opisanš powyżej funkcję GetSystemPaletteEntries. 756 Częœć II: Grafika SYSPALI.C /* SYSPALl.C - Wyœwietla paletę systemowa (c) Charles Petzold, 1998 */ ţţinclude LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; TCHAR szAppName [] = TEXT ("SysPall") ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) ( HWND hwnd : MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CS VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITEţBRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) ( MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MBţICONERROR) ; return 0 ; ) hwnd = CreateWindow (szAppName, TEXT ("System Palette ţţ1"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW USEDEFAULT, CW_USEDEFAULT, CW USEOEFAULT, NULL, NULL, hInstance, NULL) ; if (!hwnd) return 0 ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) TranslateMessage (&msg) ; DispatchMessage (&msg) ; ) return msg.wParam ; ) Rozdział 16: Palette Manager 757 BOOL CheckDisplay (HWND hwnd) ( HDC hdc ; int iPalSize ; hdc = GetDC (hwnd) ; iPalSize = GetOeviceCaps (hdc, SIZEPALETTE) ; ReleaseDC (hwnd, hdc) ; if (iPalSize != 256) MessageBox (hwnd, TEXT ("This program requires that the video ") TEXT ("display mode have a 256-color palette."), szAppName, MBţICONERROR) ; return FALSE ; 1 return TRUE ; LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ( static int cxClient, cyClient ; static SIZE sizeChar ; HDC hdc ; ` HPALETTE hPalette ; int i, x, y : PAINTSTRUCT ps ; PALETTEENTRY pe [256] ; V TCHAR szBuffer [16] ; ;. switch (message) j I.! case WM_CREATE: ' if (!CheckDisplay (hwnd)) return -1 ; f i. i hdc = GetDC (hwnd) ; SelectObject (hdc, GetStockObject (SYSTEM_FIXED_FONT)) ; GetTextExtentPoint32 (hdc, TEXT ("FF-FF-FF"), 10, &sizeChar) ; ReleaseDC (hwnd, hdc) ; return 0 ; case WM_DISPLAYCHANGE: if (!CheckDisplay (hwnd)) DestroyWindow (hwnd) ; return 0 ; case WM_SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; SelectObject (hdc, GetStockObject (SYSTEM FIXEDţFONT)) ; GetSystemPaletteEntries (hdc, 0, 256, pe) ; 758 Czţœć II: Grafika i (cišg dalszy ze strony 757) 1 for (i = 0. x = 0, y = 0 ; i < 256 ; i++) i wsprintf (szBuffer TEXT ("%02X-%02X-%02X"), ; pe[i7.peRed, peCiJ.peGreen, peCiJ.peBlue) ; TextOut (hdc, x, y, szBuffer, lstrlen (szBuffer)) ; if ((x += sizeChar.cx) + sizeChar.cx > cxClient) ( x = 0 ; if ((y += sizeChar.cy) > cyClient) break ; 1 EndPaint (hwnd, &ps) ; return 0 ; case WM-PALETTECHANGED: InvalidateRect (hwnd, NULL, FALSE) ; return 0 ; case WM DESTROY: PostOuitMessage (0) ; return 0 ; ) return DefWindowProc (hwnd, message, wParam, lParam) ; Rysunek 16-4. Program SYSPAL1 Podobnie jak pozostałe programy cyklu SYSPAL, SYSPAL1 rue zadziała, jeœli funk- cja GetDeviceCaps wywołana z argumentem SIZEPALETTE nie zwróci wartoœci 256. Zauważ, że obszar roboczy jest unieważruany (funkcja InvalidatelZect) zawsze, ile- kroć pojawi się komunikat WM PALETTECHANGED. W trakcie obsługi komu- rukatu WM PAINT wywoływana jest funkcja GetSystemPaletteEntries, przyjmu- jšca jako argument 256-elementowš tablicę struktur typu PALETTEENTRY. War- toœci RGB sš wyœwietlane w okrue aktywnym jako cišgi znaków. Po uruchomie- niu programu zwróć uwagę, że zarezerwowane kolory znajdujš się na poczštku (pierwsze 10 wartoœci) oraz na końcu (ostatnie 10 wartoœci) listy wartoœci RGB, podobnie jak w tabeli, zamieszczonej wczeœniej w tym rozdziale. Podczas gdy program SYSPALI z pewnoœciš wyœwietla bardzo użytecznš infor- mację w postaci cišgu liczb, zapewne chciałbyœ obejrzeć wszystkie 256 kolorów. Zadanie to realizuje program SYSPAL2 przedstawiony na rysunku 16-5. SYSPAL2.C /* SYSPAL2.C - Wyœwietla paletę systemowš (c) Charles Petzold. 1998 */ Rozdziat 16: Palette Manageţ 759 ţţinclude LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; TCHAR szAppName [] = TEXT ("SysPal2") ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) ( HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CS VREDRAW ; i'..:.. wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDCţARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITEţBRUSH) ; wndclass.lpszMenuName = NULL wndclass.lpszClassName = szAppName ; I. if (!RegisterClass (&wndclass)) MessageBox (NULL, TEXT ("This program requires Windows NT!"), ` szAppName, MBţICONERROR) ; return 0 ; hwnd = CreateWindow (szAppName, TEXT ("System Palette 4ţ2"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_US.EDEFAULT, CW USEDEFAULT, NULL, NULL, hInstance, NULL) ; if (!hwnd) i return 0 ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) ( TranslateMessage (&msg) ; DispatchMessage (&msg) ; ) return msg.wParam ; 1 BOOL CheckDisplay (HWND hwnd) ( HDC hdc ; int iPalSize ; hdc = GetDC (hwnd) ; iPalSize = GetDeviceCaps (hdc, SIZEPALETTE) ; 760 Częœć II: Grafika i (cigg dalszy ze strony 759) i ReleaseDC (hwnd, hdc) : if (iPalSize != 256) ' ( MessageBox (hwnd, TEXT ("This program requires that the video ") TEXT ("display mode have a 256-color palette."), i szAppName, MBţICONERROR) : return FALSE ; ) I return TRUE ; i ) LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ( static HPALETTE hPalette ; static int cxClient, cyClient ; HBRUSH hBrush : HDC hdc ; int i, x, y : LOGPALETTE * plp ; PAINTSTRUCT ps ; RECT rect ; switch (message) ( case WM_CREATE: if (!CheckOisplay (hwnd)) return -1 ; p1p = malloc (sizeof (LOGPALETTE) + 255 * sizeof (PALETTEENTRY)) ; plp->palVersion = 0x0300 ; plp->palNumEntries = 256 ; for (i = 0 ; i < 256 ; i++) l plp->palPalEntryCi].peRed = i ; plp->palPalEntry[i].peGreen = 0 ; plp->palPalEntryCi].peBlue = 0 : plp->palPalEntry[i].peFlags = PCţEXPLICIT ; ) hPalette = CreatePalette (plp) ; free (plp) ; return 0 ; case WM DISPLAYCHANGE: if (!CheckDisplay (hwnd)) DestroyWindow (hwnd) : return 0 : case WM_SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; return 0 ; Rozdział 16: Palette Manager 761 case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; SelectPalette (hdc, hPalette, FALSE) ; RealizePalette (hdc) ; for (y = 0 ; y < 16 ; y++) for (x = 0 ; x < 16 ; x++) hBrush = CreateSolidBrush (PALETTEINDEX (16 * y + x)) ; SetRect (&rect, x * cxClient / 16, y * cyClient / 16, (x + 1) * cxClient / 16. (y + 1) * cyClient / 16); FillRect (hdc, &rect, hBrush) ; Delete0bject (hBrush) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_PALETTECHANGED: if ((HWND) wParam != hwnd) InvalidateRect (hwnd, NULL, FALSE) ; return 0 ; case WM_DESTROY: Delete0bject (hPalette) ; PostOuitMessage (0) ; return 0 ; J return DefWindowProc (hwnd, message, wParam, lParam) ; Rysunek 16-5. Program SYSPAL2 Program SYSPAL2 tworzy paletę logicznš w trakcie obsługi komunikatu WM-CREATE. Zauważ jednak, że 256 wartoœci palety logicznej to numery in- deksów od 0 do 255, a wartoœć pola peFlags wynosi PC EXPLICIT. Umieszczenie jej w polu peFlags oznacza, że słowo niższego rzędu będzie opisywać indeks pa- lety sprzętowej. Ustawierue tego znacznika pozwala aplikacji na szybkie wyœwie- tlenie zawartoœci palety karty graficznej, a więc zrealizowanie celu, jaki chcieli- œmy osišgnšć. W trakcie obshzgi komurukatu 4VM-PAIN'T program SYSPAL2 wybiera tę paletę w kontekœcie urzšdzenia, a następrue realizuje jš. Nie powoduje to żadnych zmian w palecie systemowej, lecz pozwala programowi pozyskać kolory tej palety przez wywołanie makra PALETTEINDEX. Podobnie jak poprzednio, po uruchomieniu programu zauważysz, że 10 pierwszych i 10 ostatnich kolorów to 20 kolorów zarezerwowanych, przedstawionych w tabeli na poczštku tego rozdziahx. Jeœli urucnomisz jakiœ program, który używa własnej palety logicznej, kolory na ekra- nie ulegnš zmianie. Gdybyœ oprócz wszystkich kolorów palety chciał również obejrzeć ich wartoœci RGB, uruchom dodatkowo program WHATCLR z rozdziału 8. W trzeciej wersji programu SYSPAL zastosowałem technikę, z która zetknšłem się dopiero niedawno - czyli jakieœ siedem lat po poznaniu Palette Managera Windows. I 762 Czgœć Ii: Grafika Wszystkie funkcje GDI okreœlajš kolor - bezpoœrednio bšdŸ poœrednio - jako wartoœć RGB. Gdzieœ w głębi GDI jest ona przetwarzana na zapis bitowy, okre- t . œlajšcy kolor danego piksela. W niektórych trybach graficznych (np. 16-bitowym lub 24-bitowym) konwersja taka jest raczej prosta, w innych (4-bitowym lub & bitowym) może okazać się potrzebne stosowanie mecharuzmu wyszukiwania koloru najbardziej zbliżonego do żšdanego. Dwie funkcje GDI pozwalajš na bezpoœrednie okreœlenie koloru za pomocš zapi- su bitowego. Użycie obu jest oczywiœcie mocno zwišzane ze sprzętem. Na tyle, że mogš wyœwietlić bezpoœrednio wartoœci tablicy odwzorowań karty graficznej. Sš to funkcje BitBlt oraz StretchBlt. Program SYSPAL3, przedstawiony na rysunku 16-6, pokazuje użycie funkcji j StretchBlt do wyœwietlenia kolorów palety systemowej. i SYSPAL3.C i* SYSPAL3.C - Wyœwietla paletę systemowš (c) Charles Petzold, 1998 ţ*ţ Ilinclude LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; TCHAR szAppName [J = TEXT ("SysPal3") ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine. int iCmdShow) HWND hwnd ; MSG ms9 ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CS VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE-BRUSH) wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MB-ICONERROR) ; return 0 ; 1 hwnd = CreateWindow (szAppName, TEXT ("System Palette 113"), WS OVERLAPPEDWINDOW. CW USEDEFAULT, CW USEDEFAULT, Rozdział 16: Palette Manager 763 CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; if (!hwnd) return 0 ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) i TranslateMessage (&msg) ; DispatchMessage (&msg) ; return msg.wParam ; ) BOOL CheckDisplay (HWND hwnd) HDC hdc : int iPalSize ; I i hdc = GetDC (hwnd) ; iPalSize = GetOeviceCaps (hdc, SIZEPALETTE) ; ReleaseDC (hwnd, hdc) ; if (iPalSize != 256) s MessageBox (hwnd, TEXT ("This program requires that the video ") TEXT ("display mode have a 256-color palette."), szAppName, MB ICONERROR) ; return FALSE ; !' return TRUE ; LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) i static HBITMAP hBitmap ; static int cxClient, cyClient ; BYTE bits [256] ; HDC hdc, hdcMem ; int i ; PAINTSTRUCT ps ; i switch (message) f case WM_CREATE: if (!CheckDisplay (hwnd)) return -1 ; for (i = 0 ; i < 256 ; i++) blt5 ţ1] = 'I ; hBitmap = CreateBitmap (16, 16, 1, 8, &bits> ; return 0 ; case WM DISPLAYCHANGE: Częœć II: Grafika 7 (cišg dalszy ze strony 763) if (!CheckDisplay) DestroyWindow (hwnd) ; return 0 ; case WM_SIZE: cxClient = LOWORD (lParam) ; cyClient = NIWORD (lParam) ; return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; hdcMem = CreateCompatibleDC (hdc) ; SelectObject (hdcMem, hBitmap) ; StretchBlt (hdc, 0, 0, cxClient, cyClient, hdcMem, 0, 0, 16, 16, SRCCOPY) ; DeleteDC (hdcMem) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: Delete0bject (hBitmap) : PostOuitMessage (0) ; return 0 ; ) return DefWindowProc (hwnd, message. wParam, lParam) ; Rysunek 16-6. Program SYSPAL3 W trakcie obsługi komunikatu WM CREAT'E program SYSPAL3 wywołuje funkcję CreateBitmap w celu utworzerua mapy bitowej o rozmiarach 16 na 16 pikseli przy 8-bitach na piksel. Ostatnim argumentem funkcji jest 256-bajtowa tablica zawie- rajšca liczby od 0 do 255, które odpowiadajš 256 możliwym wartoœciom opisujš- cym piksel w trybie 8-bitowym. Podczas obsługi komunikatu 4VMţPAINT pro- gram wybiera utworzonš mapę bitowš w kontekœcie urzšdzenia, a następnie wyœwietla jš w oknie. System Windows przesyła bity opisujšce poszczególne pik- sele mapy bitowej bezpoœrednio do karty graficznej, dzięki czemu odpowiednio adresowane sš poszczególne pozycje w 256-kolorowej tablicy odwzorowań. Ob- szar roboczy nie musi być unieważniany, program zaœ nie musi się troszczyć o ob- sługę komunikatu WM PALETTECHANGED, gdyż każda zmiana palety jest natychmiast odwzorowana w oknie programu SYSPAL3. Animacja palety Widzšc słowo "animacja" w tytizle tego podrozdziału, myœlisz pewnie o szalo- nych królikach kicajšcych po ekranie monitora. Niestety, rzeczywistoœć może cię nieco rozczarować. Oczywiœcie, animacja przy użyciu Palette Managera jest moż- liwa, ale doœć specyficznego rodzaju. Rozdział 16: Palette Manager 765 Zwykle animacja w systemie Windows polega yg š yœwietlaniu map bp owţyţcnh w krótkich odstępach czasu. Animacja palety w 1 da odmiennie. Roz ocz a się od narysowarua wszystkich potrzebnych obiektów, a następrue tak manipu- j; luje paletš, aby poszczególne obiekty zmieniały barwy. Dzięki temu można uzy- skać efekt animacji bez prze sow ania obiektów, czyli bardzo szybko. 'I Utworzenie palety do animacji przebiega nieco inaczej niż w rzedstawionycš , dotychczas przykładach. Pole peFlags struktu PALETTEEN'TRY musi przyj ć wartoœć PC ftESERVED dla każdej wartoœci RGB kolorów, które będš zmieniane podczas animacji. Jak wiadomo, tworzšc paletę logicznš ustawiamy pole peFlags na 0. Pozwalga to GDI na przypisarue identycznych kolorów pochodzšcych z różnych palet lo icz- nych tej samej wartoœci palety systemowej. Przypuœćmy, że dwa programy two- rz alety logiczne zawierajšce wartoœć RGB 10-10-10. System Windows musi rzechowywać tylko jednš wartoœć 10-10-10 w palecie systemowej. Jednak jeœli eden z tych programów stosuje arumację paletyyGDI wcale yie jest dj tego po- ałożenia aruma 'a palety powinna b ć bardzo sz bka co est możli- trzebny. Z z cl y p gţ' jš y we jeœli nie sš sukcesywnie dorysowywane obiekty. Kpeţ ro am stosu c anl- mację palety dokonuje jej zmiany, nie powinno to w ł ać na działanie inn ch ' programów czy też reorganizację palety systemowej przez GDI. Wartoœć PC RE- SERVED, którš przyjmuje pole peFlags, rezerwuje danš wartoœć palety systemo- wej dla pojedynczej palety logicznej. ' Korzystajšc z animacji palety, wywołania funkcji SelectPaletPŽţltK 1 lettoekre - leży umieœcić tradycyjnie podczas obsługi komurukatu WM- œla się stosujšc makro PALETTEINDEX, które pobiera indeks koloru i umieszcza go w tablicy palety logicznej. ty p - Dla celów animacji będzie prawdopodobnie konieczna zmiana pale w od o wiedzi na komunikat WM TIMER. Aby zmienić wartoœci RGB w palecie logicz- nej, należy wywołać funkcję AnimatePalette, jako argumentu używajšc jabh',cny struk- tur typu PALETTEENTRY. Funkcja jest szyb j a, po yepważ ymienia e g ie war- toœci w palecie systemowej oraz w sprzętowe tablic alet na karcie raficznej. Podskakujšca piłka Rysunek 16-7 przedstawia fragmenty programu BOUNCE, kolejnego, kto.'yry wy- œwietla na ekranie odbijajšcš się piłkę. Dla uproszczenia piłka została na sowa- na jako elipsa i może wyglšdać różnie w zaleš'zno yi od rozmiaţrš p„ ţay t o rozdziale przedstawię kilka programów zwi zan ch z anima , g w pliku PALANIM.C znajdujš się ich najważniejsze wspólne elementy. PALANIM.C ţ* PALANIM.C - Program animacji palet (c) Charles Petzold, 1998 */ 7ţ Czţœć il: Graflka i (cišg dalszy ze strony 765) llţinclude ! extern HPALETTE CreateRoutine (HWND) ; extern void PaintRoutine (HDC, int, int) ; extern void TimerRoutine (HDC, HPALETTE) ; extern void DestroyRoutine (HWND, HPALETTE) ; LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; extern TCHAR szAppName [] ; extern TCHAR szTitle [] ; i int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, j PSTR szCmdLine, int iCmdShow) ; f HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CS VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) f MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MBţICONERROR) ; return 0 ; ) hwnd = CreateWindow (szAppName, szTitle, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW USEDEFAULT, CW_USEDEFAULT, CW USEDEFAULT, NULL, NULL, hInstance, NULL) ; if (!hwnd) return 0 ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) f TranslateMessage (&msg) ; DispatchMessage (&msg) ; ) return msg.wParam ; ) Rozdział 16: Palette Manager BOOL CheckDisplay (HWND hwnd) ' HDC hdc ; int iPalSize ; hdc = GetDC (hwnd) : iPalSize = GetDeviceCaps (hdc, SIZEPALETTE) ; ReleaseDC (hwnd, hdc) : if (iPalSize != 256) ( MessageBox (hwnd, TEXT ("This program requires that the video ") TEXT ("display mode have a 256-color palette."), szAppName, MBţICONERROR) ; return FALSE ; return TRUE ; LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) static HPALETTE hPalette ; static int cxClient, cyClient ; HDC hdc ; PAINTSTRUCT ps ; switch (message) I.,.. case WM_CREATE: if (!CheckDisplay (hwnd)) return -1 ; ; hPalette = CreateRoutine (hwnd) : return 0 ; case WM DISPLAYCHANGE: if (!CheckDisplay (hwnd)) DestroyWindow (hwnd) : return 0 ; case WM_SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; return 0 : case WMţPAINT: hdc = Be9inPaint (hwnd, &ps) ; SelectPalette (hdc, hPalette, FALSE) ; RealizePalette (hdc) ; PaintRoutine (hdc, cxClient, cyClient) : EndPaint (hwnd, &ps) : return 0 ; k'. I ţ; Częœć II: Grafika i (cišg dalszy ze strony 767) case WM_TIMER: T hdc = GetDC (hwnd) ; SelectPalette (hdc, hPalette, FALSE) ; TimerRoutine (hdc, hPalette) ; ; ReleaseDC (hwnd, hdc) ; ; return 0 ; case WMţOUERYNEWPALETTE: if (!hPalette) return FALSE ; f hdc = GetDC (hwnd) ; SelectPalette (hdc, hPalette, FALSE) ; RealizePalette (hdc) ; InvalidateRect (hwnd, NULL, TRUE) ; ReleaseDC (hwnd, hdc) ; return TRUE ; case WM_PALETTECHANGED: if (!hPalette ţţ (HWND) wParam = hwnd) break ; hdc = GetDC (hwnd) ; SelectPalette (hdc, hPalette, FALSE) ; RealizePalette (hdc) ; UpdateColors (hdc) ; ReleaseDC (hwnd, hdc) ; break ; case WMţDESTROY: DestroyRoutine (hwnd, hPalette) ; PostOuitMessage (0) ; return 0 ; 1 } return DefWindowProc (hwnd, message, wParam, lParam) ; BOUNCE.C /* BOUNCE.C - Animacja palety - demonstracja (c) Charles Petzold, 1998 */ ţlinclude ţţdefine ID TIMER 1 TCHAR szAppName [] = TEXT ("Bounce") ; TCHAR szTitle [] = TEXT ("Bounce: Palette Animation Demo") ; Rozdział 16: Palette Manager static LOGPALETTE * plp : HPALETTE CreateRoutine (HWND hwnd) HPALETTE hPalette : int i ; plp = malloc (sizeof (LOGPALETTE) + 33 * sizeof (PALETTEENTRY)) ; plp->palVersion = 0x0300 : plp->palNumEntries ţ 34 ; l. for (i = 0 : i < 34 : i++) ( plp->palPalEntryCi].peRed = 255 : plp->palPalEntryCi].peGreen = (i = 0 ? 0 : 255I ; plp->palPalEntryCi].peBlue = (i = 0 ? 0 : 255) : plp->palPalEntryCi].peFlags = (i = 33 ? 0 : PCţRESERVED) ; 1 hPalette = CreatePalette (plp) ; SetTimer (hwnd, ID TIMER, 50, NULL) ; return hPalette ; void PaintRoutine (HDC hdc, int cxClient, int cyClient) t ; HBRUSH hBrush ; int i, xl, x2,.yl, y2 : RECT rect ; ;.j // Wypelnij tlo okna używajdc koloru indeksowanego numerem 33 ' SetRect (&rect, 0, 0, cxClient, cyClient) : hBrush = CreateSolidBrush (PALETTEINDEX (33)) : i FillRect (hdc, &rect, hBrush) ; Delete0bject (hBrush) ; // Narysuj 33 pilki SelectObject (hdc, GetStockObject (NULLţPEN)) ; for (i m 0 ; i < 33 ; i++) ' ( xl = i * cxClient / 33 ; x2 = (i + 1) * cxClient / 33 : if (i < 9) ( yl = i * cyClient / 9 ; y2 = (i + 1) * cyClient / 9 ; 1 else if (i < 17) ( yl = (16 - i) * cyClient / 9 ; y2 = (17 - i) * cyClient / 9 : ) else if (i < 25) 770 - Czeœć II: Gţafika (cišg dalszy ze strony 769) i i yl = (i - 16) * cyClient / 9 ; y2 = (i - 15) * cyClient / 9 ; ) else ( i yl = (32 - i) * cyClient / 9 ; y2 = (33 - i) * cyClient / 9 ; hBrush = CreateSolidBrush (PALETTEINDEX (i)) ; SelectObject (hdc, hBrush) ; Ellipse (hdc, xl, yl, x2, y2) ; Delete0bject (SelectObject (hdc, GetStockObject (WHITE BRUSH))) ; ) return ; ) void TimerRoutine (HDC hdc, HPALETTE hPalette) t static BOOL bLeftToRight = TRUE ; static int iBall ; // Ustaw kolor starej pilki jako bialy plp->palPalEntry[iBall].peGreen = 255 ; plp->palPalEntry[iBall].peBlue = 255 ; iBall += (bLeftToRight ? 1 : -1) ; if (iBall = (bLeftToRight ? 33 : -1)) ( iBall = (bLeftToRight ? 31 : 1) ; bLeftToRight ^= TRUE ; ) // Ustaw kolor nowej pilki jako czerwony plp->palPalEntry[iBall].peGreen = 0 ; plp->palPalEntry[iBall].peBlue = 0 ; // Animuj paletę AnimatePalette (hPalette, 0, 33, plp->palPalEntry) ; return ; ) void DestroyRoutine (HWND hwnd, HPALETTE hPalette) f KillTimer (hwnd, ID TIMER) ; Delete0bject (hPalette) ; free (plp) ; return ; ) Rysunek 16-7. Program BOUNCE Rozţiat 16: Palette Manager Animacja palety nie będzie, oczywiœcie, funkcjonować poprawnie, jeœli system pracuje w trybie graticznym, który rue obsługuje współpracy z paletami. Dlatego , program PALANIM.C rozpoczyna się obsługš komunikatu WM CREATE z wy- wołaniem funkcji CheckDisplay (podobnie jak w programach SYSPAL). PALAMM.C jako fragment BOUNCE.C wywołuje cztery funkcje: CreateRoutine w trakcie obsługi komunikatu WM CREATE (podczas którego program tworzy paletę logicznš), Paintftoutine w trakcie obsługi komunikatu WM PAINT, Timer Routine podczas obshxgi komurukatu WM TIMER oraz DestroyRoutine podczas obsługi WMţDESTROY. Przed wywołaniem funkcji Paintftoutine i TimerRoutine PALANIM.C otrzymuje kontekst urzšdzerua i wybiera paletę logicznš w tym kontekœcie. Przed wywołaniem Paintftoutine realizowana jest rówrueż paleta lo- giczna. Funkcja AnimatePalette jest wywoływana z funkcji Timerftoutine i co prawda ;'I, ţ,. przed wywołaniem należy wybrać paletę w kontekœcie urzšdzenia, ale nie trze- ba jej realizować za pomocš funkcji RealizePalette. Piłka w programie BOUNCE odbija się w lewo i w prawo po lin łamanej w kształ- II . cie lite "W". Tło okna jest białe, a piłka czerwona. W każdej chwili piłka jest widoczna w jednym z 33 niepokrywajšcych się położeń. Potrzebne sš do tego 34 pozycje palety: jedna dla tła, a pozostałe 33 dla poszczególnych położeń piłki. Funkcja Createftoutine inicjuje tablicę złożonš ze struktur typu PALETTEENTRY, nadajšc pierwszej pozycji palety (odpowiadajšcej położeniu piłki w lewym gór- nym rogu okna) wartoœć koloru czerwonego, pozostałym zaœ wartoœci odpowia- dajšce bieli. Zauważ, że pole peFlags przyjmuje wartoœć PC IZESERVED dla wszystkich pozycji oprócz ostatniej, odpowiedzialnej za kolor tła. Createftoutine kończy się ustawieniem licznika Windows z częstoœciš 50 milisekund. Rysowanie obiektów realizuje funkcja Paintftoutine. Tło okna jest malowane pędz- lem w kolorze okreœlonym przez indeks o numerze 33 palety. Kolory 33 piłek pobierane sš z palety kolejno o indeksach od 0 do 32. Kiedy program po raz pierw- szy rysuje zawartoœć okna, paleta o indeksie zerowym przechowuje kolor czer- wony, pozostałe zaœ indeksy wskazujš na kolor biały. Powoduje to pojawienie się czerwonej piłki w lewym gómym narożniku okna. ; Animacja odbywa się, kiedy proces WndProc, obshxgujšc komunikat WM TIMER, wywołuje funkcję Timerftoutine. Timerftoutine kończy się wywołaniem funkcji AnimatePalette, która ma następujšcš składnię: AnimatePalette (hPalette, uStart, uNum, &pe) : Pierwszy argument to uchwyt palety, a ostatni jest wskaŸnikiem do jednej lub większej liczby struktur PALETTEENTRY, ułożonych w tablicę. Funkcja zmienia jednš lub więcej pozycji palety logicznej, poczynajšc od pozycji o numerze uStart przez kolejne uNum. Nowa pozycja uStart jest pobierana z pierwszego elementu struktury PALETTEENTRY. Musisz jednak uważać, gdyż parametr uStart ozna- cza indeks oryginalnej palety logicznej, nie zaœ tablicy struktur typu PALETTE- ENTRY. Dla wygody program BOUNCE używa tablicy struktur PALETTEENTRY, które stanowiš fragment struktury LOGPALETTE, zastosowanej podczas tworzenia alety logicznej. Bieżšce położenie piłki (jako wartoœć indeksu od 0 do 32) jest p rzechowywane w zmiennej statycznej iBall. W trakcie wykonywania funkcji Ti- p Czţœć II: Grafika merRoutine program przypisuje wartoœć koloru białego odpowiedniemu elemen- towi tablicy struktur PALETTEENTRY. Następnie wyznaczane jest nowe położe- nie piłki i pola odpowiedniej struktury przyjmujš wartoœć koloru czerwonego. Zmiana palety następuje po wywołaniu: AnimatePalette (hPalette, 0, 33, plp->palPalEntry) ; GDI zmienia pierwsze 33 pozycje palety logicznej (choć w rzeczywistoœci zmie- niajš się jedynie 2 z nich), następrue odpowiednio zmienia paletę systemowš, a na końcu tablicę palety sprzętowej karty graficznej. W ten sposób można uzyskać złudzenie ruchu piłki bez cyklicznego rysowania obiektów. Bardzo pouczajšcym ćwiczeniem jest jednoczesne uruchomienie programu BO- UNCE oraz SYSPAL2 lub SYSPAL3. Chociaż funkcja AnimatePalette działa bardzo szybko, należy unikać zmiany wszystkich pozycji palety logicznej, kiedy wystarczy zmienić jednš lub dwie. W programie BOUNCE problem jest bardziej złożony, gdyż zmienna iBall jest zwiększana lub zmniejszana w zależnoœci od kierunku poruszania się piłki. Jed- nym z rozwišzań mogłoby być wprowadzenie dwóch dodatkowych zmiennych: iBallOld (zapamiętujšcej poprzednie położenie piłki) oraz iBallMin (oznaczajšcej mniejszš z wartoœci iBall oraz iBallOld). Aby zmienić tylko dwie wartoœci palety , należy wywołać funkcję AnimalPalette w następujšcy sposób: iBallMin = min (iBall, i8a1101d) ; AnimatePalette (hPal, iBallMin, 2, plp->palPalEntry + iBallMin) ; Oto inne rozwišzanie problemu: przypuœćmy, że najpierw zdefiniujesz pojedyn- czš strukturę typu PALETTEENTRY: PALETTEENTRY pe ; Funkcja TimerRoutine przypisuje odpowiednim polom struktury wartoœć koloru białego i wywohzje funkcję AnimatePalette, aby zmienić pozycję palety logicznej o indeksie iBall: pe.peRed = 255 ; pe.peGreen = 255; pe.peBlue = 255; pe.peFlags = PC_RESERVED; AnimatePalette (hPalette. iBall, 1, &pe) ; Następnie wyliczasz nowš wartoœć iBall tak jak w przykładzie BOUNCE, przy- pisujesz polom struktury PALETTEENTRY wartoœć koloru czerwonego i wywo- łujesz ponownie funkcję AnimatePalette: pe.peRed = 255 ; pe.peGreen = 0 ; pe.peBlue = 0 ; pe.peFlags = PC_RESERVED ; AnimatePalette (hPalette, iBall, 1, &pe) ; Podskakujšca piłka jest prostym, ale niezbyt trafnym przykładem animacji pale- ty, gdyż wszystkie położenia piłki muszš być narysowane przed rozpoczęciem animacji. Nadaje się ona bardziej do przedstawiania ruchu powtarzajšcych się obiektów. Rozdział 16: Palette Manager Animacja jednej pozycji palety Jednym z bardziej interesujšcych aspektów animacji palety jest możliwoœć wy- korzystania interesujšcych technik przy zastosowaniu zaledwie jednej pozycji palety. Jednš z takich technik przedstawia na przykładzie programu FADER ry- sunek 16-8. Program FADER wymaga użycia funkcji zawartych w module PA- LAMM.C. FADER.C /* FADER.C - Animacja palety - demonstracja (c) Charles Petzold, 1998 */ ţţinclude ţţdefine ID TIMER 1 TCHAR szAppName C7 = TEXT ("Fader") ; TCHAR szTitle C7 = TEXT ("Fader: Palette Animation Demo") ; static LOGPALETTE lp ; HPALETTE CreateRoutine (HWND hwnd) f HPALETTE hPalette ; lp.palVersion = 0x0300 ; lp.palNumEntries = 1 ; lp.palPalEntryC07.peRed = 255 ; lp.palPalEntryC07.peGreen = 255 ; lp.palPalEntryC07.peBlue = 255 ; lp.palPalEntryCO].peFlags = PCţRESERVED ; hPalette = CreatePalette (&lp) ; SetTimer (hwnd, ID TIMER, 50, NULL) ; return hPalette ; void PaintRoutine (HDC hdc, int cxClient, int cyClient) ( static TCHAR szText C] = TEXT (" Fade In and Out ") ; int x, y ; SIZE sizeText ; SetTextColor (hdc, PALETTEINDEX (0)) ; GetTextExtentPoint32 (hdc, szText, lstrlen (szText), &sizeText) ; for (x = 0 ; x < cxClient ; x += sizeText.cx) for (y = 0 ; y < cyClient ; y += sizeText.cy) ( TextOut (hdc, x, y, szText, lstrlen (szText)) ; ) return ; ) 774 Częœć il: Grańka (cišg dalszy ze strony 773) void TimerRoutine (HDC hdc, HPALETTE hPalette) ( static BOOL bFadeIn = TRUE ; if (bFadeIn) ( lp.palPalEntry[0].peRed -= 4 lp.palPalEntry[0].peGreen -= 4 if (lp.palPalEntry[0].peRed = 3) bFadeIn = FALSE ; 1 else f lp.palPalEntry[0].peRed += 4 ; lp.palPalEntry[0].peGreen += 4 ; if (lp.palPalEntry[0].peRed = 255) bFadeIn = TRUE ; AnimatePalette (hPalette, 0, 1, lp.palPalEntry) ; return ; void DestroyRoutine (HWND hwnd, HPALETTE hPalette) ( KillTimer (hwnd, ID TIMER) ; Delete0bject (hPalette) ; return ; Rysunek 16-8. Program FADER Program FADER wyœwietla cišg znaków "Fade In And Out" w obszarze całego okna. Poczštkowo tekst pojawia się w kolorze białym, przez co jest niewidoczny na białym tle. Wykorzystujšc animację palety, FADER stopniowo zmienia kolor tekstu na niebieski, a następnie znów na biały, po czym powtarza cyklicznie całš sekwencję. Tekst wydaje się pojawiać i znikać na białym tle okna. Funkcja CreateRoutine tworzy paletę logicznš. Program potrzebuje tylko jednej pozycji w palecie i inicjuje jš wartoœciš koloru białego (wszystkim trzem składo- wym koloru - czerwonej, zielonej i niebieskiej - przypisywana jest wartoœć 255). Funkcja PaintRoutine (wywoływana z modułu PALANIM po utworzeniu palety logicznej, jej wybraniu i zrealizowaniu) zawiera wywołanie funkcji SetTextColor. Funkcja SetTextColor ustala kolor tekstu zgodnie z wartoœciš PALETTEINDEX(0}. Pierwsza pozycja palety (indeks 0) przyjmuje poczštkowo wartoœć koloru białe- go. Następnie program FADER wypełnia okno powtarzajšcym się tekstem "Fade In And Cţut", który w tym momencie ma kolor biały, więc nie jest widoczny na białym tle. Funkcja TimerRoutine zawiera mechanizm animacji palety przez zmianę wartoœci struktury PALETTEENTRY i przekazywarue jej w postaci argumentu do funkcji AnimatePalette. Poczštkowo program po każdym otrzymaniu komunikatu WM-TI- Rozdzial 16: Palette Manager MER zmniejsza wartoœci składowej czerwonej i zielonej koloru o 4 aż do osiš- ięcia wartoœci 3. Wtedy wartoœci koloru sš sukcesywrue zwiększane o 4 aż do . osišgnięcia 255. Wynikiem takiej manipulacji kolorami tekstu jest jego cykliczne pojawiarue się i znikarue. Program ALLCOLOR, przedstawiony na rysunku 16-9, korzystajšc z 1-pozycyj- nej palety logicznej wyœwietla wszystkie kolory, które może generować karta gra- '1 ficzna. Oczywiœcie nie sš one pokazywane jednoczeœnie, lecz sekwencyjnie. Kar- ta graficzna dysponujšca 1&bitowš rozdzielczoœciš może wyœwietlić 262144 różne kolory. Gdybyœ chciał wyœwietlić je wszystkie kolejno co 55 milisekund, spędził- l.. byœ przed ekranem cztery godziny! ` ALLCOLOR.C * / j ALLCOLOR.C - Animacja palety - demonstrac a */ (c) Charles Petzold, 1998 4pinclude 4ţdefine ID TIMER 1 TCHAR szAppName C7 = TEXT ("AllColor") ; ; TCHAR szTitle C] = TEXT ("AllColor: Palette Animation Demo") ; static int iIncr ; static PALETTEENTRY pe ; HPALETTE CreateRoutine (HWND hwnd) ( HDC hdc ; HPALETTE hPatette ; LOGPALETTE lp ; // Okreœl rozdzielczoœć koloru i ustal wartoœć iIncr ! hdc = GetDC (hwnd) ; iIncr = 1 Ž (8 - GetOeviceCaps (hdc, COLORRES) / 3) ; ReleaseDC (hwnd, hdc) ; // Utwórz paletę logicznd = 0x0300 ; lp.palVersion lp.palNumEntries = 1 ; lp.palPalEntryC07.peRed = 0 ; lp.palPalEntryC07.peGreen = 0 ; lp.palPalEntryC07.peBlue = 0 ; lp.palPalEntryC07.peFlags s PC RESERVED ; hPalette = CreatePalette (&lp) ; // Zapisz pierwszy element palety pe = lp.palPalEntryC07 : ".ţţ Częœć il, Grafika (cišg dulszy ze strony 775) SetTimer (hwnd, ID TIMER, 10, NULL) ; return hPalette ; ) void DisplayRGB (HDC hdc, PALETTEENTRY * ppe) f TCHAR szBuffer [16] ; wsprintf (szBuffer, TEXT (" %02X-%02X-%02X "), ppe->peRed, ppe->peGreen, ppe->peBlue) ; TextOut (hdc, 0, 0, szBuffer, lstrlen (szBuffer)) ; 1 void PaintRoutine (HDC hdc, int cxflient, int cyClient) f HBRUSH hBrush ; RECT rect ; // Wypelnij okno kolorem o indeksie palety 0 hBrush = CreateSolidBrush (PALETTEINDEX (0)) ; SetRect (&rect, 0, 0, cxClient, cyClient) ; FillRect (hdc, &rect, hBrush) ; Delete0bject (SelectObject (hdc, GetStockObject (WHITE BRUSH))) ; // Wyœwietl wartoœć RGB DisplayRGB (hdc, &pe) ; return ; void TimerRoutine (HDC hdc, HPALETTE hPalette) ( static BOOL bRedUp = TRUE, bGreenUp = TRUE, bBlueUp = TRUE ; // Zdefiniuj nowš wartoœć koloru pe.peBlue += (bBlueUp ? iIncr : -iIncr) ; if (pe.peBlue = (BYTE) (bBlueUp ? 0 : 256 - iIncr)) ( pe.peBlue = (bBlueUp ? 256 - iIncr : 0) ; bBlueUp ^= TRUE ; pe.peGreen += (bGreenUp ? iIncr : -iIncr) ; if (pe.peGreen == (BYTE) (bGreenUp ? 0 : 256 - iIncr)) ( pe.peGreen = (bGreenUp ? 256 - iIncr : 0) ; bGreenUp ^= TRUE ; pe.peRed += (bRedUp ? iIncr : -iIncr) ; if (pe.peRed = (BYTE) (bRedUp ? 0 : 256 - iIncr)) i pe.peRed = (bRedUp ? 256 - iIncr : 0) ; bRedUp ^= TRUE ; Rozdział 16: Palette Manager // Animuj paletę AnimatePalette (hPalette, 0, 1, &pe) ; DisplayRGB (hdc, &pe) ; return ; void DestroyRoutine (HWND hwnd, HPALETTE hPalette) ( KillTimer (hwnd, IO TIMER) ; Delete0bject (hPalette) ; return ; Rysunek 16-9. Program ALLCOLOR Program ALLCOLOR jest bardzo podobny do przedstawionego wczeœniej pro- gramu FADER. Funkcja CreateRoutine tworzy paletę zawierajšcš tylko jednš po- zycję, o wartoœci odpowiadajšcej kolorowi czarnemu (pola struktury odpowie- dzialne za kolor wypełruone sš zerami). Funkcja PaintRoutine tworzy pędzel w ko- lorze adresowanym przez PALETTEINDEX(0), a następnie wywołuje FillRect, aby wypełnić obszar okna programu za pomocš tego pędzla. Funkcja TimerRoutine programu ALLCOLOR animuje paletę, zmieniajšc kolor umieszczany w polach struktury PALETTEENTRY i wywołujšc funkcję Animate- Palette. W efekcie kolory zmieniajš się płynnie. Najpierw sukcesywnie zwiększa- na jest wartoœć składowej niebieskiej, a kiedy osišga maksimum, rozpoczyna się zwiększanie składowej zielonej. Na końcu stopniowo zmniejszana jest wartoœć składowej niebieskiej. Sposób zmniejszania i zwiększania wartoœci składowych kolorów - czerwonej, zielonej i niebieskiej - zależy od zmiennej ilncr. Zmienna wyznaczana jest w trakcie wykonywania funkcji CreateRoutine na podstawie war- toœci zwracanej przez funkcję GetDeviceCaps w wersji z argumentem COLORRES. Jeżeli na przykład GetDeviceCaps zwraca liczbę 18, zmiennej iIncr przypisywana jest liczba 4 - najmniejsza wartoœć niezbędna do uzyskania wszystkich kolorów. Program ALLCOLOR wyœwietla w lewym górnym narożniku okna bieżšcš war- toœć koloru RGB. Kod koloru został dodany dla celów testowych, lecz okazał się na tyle użyteczny, że pozostał w ostatecznej wersji programu. Aplikacje inżynierskie Animacja palety może znaleŸć zastosowanie w aplikacjach typu inżynierskiego, na przykład do wizualizacji przebiegu jakichœ procesów przemysłowych. Co in- nego tylko narysować silnik spalinowy na ekranie, a co innego ożywić go za po- mocš prostej animacji, przedstawiajšcej jego działanie jasno i przejrzyœcie. Jednym z najlepszych przykładów zastosowania animacji palety jest pokazanie płynu przepływajšcego przez rurę. W tym wypadku obraz nie musi być bardzo dokładny (płyn oglšdany jest przez œcianki przezroczystej rury), gdyż nie zależy Częœć II: Grafika nam na szczegółowym modelu, a raczej na symbolicznym przedstawieniu prze- pływu. Program PIPES przedstawiony na rysunku 16-10, to prosty przykład oma- wianej techniki animacji - wyœwietla dwa modele poziomych rur, w których płyn przemieszcza się w przeciwnych kierunkach. PIPES.C /* PIPES.C - Animacja palety - demonstracja (c) Charles Petzold, 1998 */ ţţinclude ţţdefine ID TIMER 1 TCHAR szAppName [] = TEXT ("Pipes") ; TCHAR szTitle [] = TEXT ("Pipes: Palette Animation Demo") ; static LOGPALETTE * plp ; HPALETTE CreateRoutine (HWND hwnd) t HPALETTE hpalette ; int i ; plp = malloc (sizeof (LOGPALETTE) + 32 * sizeof (PALETTEENTRY)) ; // Inicjuj pola struktury LOGPALETTE plp->palUersion = 0x300 ; plp->palNumEntries = 16 ; for (i = 0 ; i <= 8 ; i++) ( plp->palPalEntry[i].peRed = (BYTE) min (255, 0x20 * i) ; plp->palPalEntry[i].peGreen = 0 ; plp->palPalEntry[i].peBlue = (BYTE) min (255, 0x20 * i) ; plp->palPalEntry[i].peFlags = PCţRESERVED ; plp->palPalEntry[16 - i] = p1p->palPalEntry[i] ; plp->palPalEntry[16 + i] = plp->palpalEntry[i] ; plp->palPalEntry[32 - i] = plp->palPalEntry[i] ; 1 hpalette = Createpalette (plp) ; SetTimer (hwnd, ID TIMER, 100, NULL) ; return hpalette ; ) void PaintRoutine (HDC hdc, int cxClient, int cyClient) f HBRUSH hBrush ; int i ; RECT rect ; // Pomaluj tio okna Rozdzial 16: Palette Wlanager SetRect (8ţrect, 0, 0. cxClient, cyClient) : hBrush = SelectObject (hdc. GetStockObject (WHITE BRUSH)) : FillRect (hdc. &rect, hBrush) : // Rysuj zawartoœć rur for (i = 0 ; i < 128 ; i++) 1 hBrush = CreateSolidBrush (PALETTEINDEX (i % 16)) : SelectObject (hdc, hBrush) : rect.left = (127 - i) * cxClient / 128 : rect.right = (128 - i) * cxClient / 128 : rect.top = 4 * cyClient / 14 : rect.bottom = 5 * cyClient / 14 ; " FillRect (hdc, &rect, hBrush) : rect.left - i * cxClient / 128 : rect.ri9ht = (i + 1) * cxClient / 128 : rect.top - 9 * cyClient / 14 : rect.bottom = 10 * cyClient / 14 ; FillRect (hdc, &rect, hBrush) : Delete0bject (SelectObject (hdc, GetStockObject (WHITE BRUSH)>) ; 1 // Rysuj krawędzie rur MoveToEx (hdc, 0, 4 * cyClient / 14, NULL) ; LineTo (hdc, cxClient. 4 * cyClient / 14) ; MoveToEx (hdc, 0. 5 * cyClient / 14 NULL) : LineTo (hdc, cxClient, 5 * cyClient / 14) : MoveToEx (hdc. 0. 9 * cyClient / 14, NULL) : LineTo (hdc, cxClient, 9 * cyClient / 14) : MoveToEx (hdc, 0, 10 * cyClient / 14, NULL) : LineTo (hdc, cxClient, 10 * cyClient / 14) ; return ; void TimerRoutine (HDC hdc, HPALETTE hPalette) static int iIndex ; AnimatePalette (hPalette, 0, 16. plp->palPalEntry + iIndex) ; iIndex ţ (iIndex + 1) % 16 ; ) return ; void DestroyRoutine (HWND hwnd. HPALETTE hPalette) ( KillTimer (hwnd, ID TIMER) ; 0 Częœć IIţ Grafika (cišg dalszy ze strony 779) Delete0bject (hPalette) ; free (plp) ; return ; ) Rysunek 16-10. Program PIPES Animacja wykorzystuje zaledwie 16 kolorów palety, lecz można zastosować mniejszš liczbę barw. Oczywiœcie, do uzyskania odpowiedniego efektu anima- cyjnego konieczna jest pewna minimalna liczba kolorów. W końcu nawet trzy zmieniajšce się kolory sš lepsze niż nieruchoma strzałka na rysunku, wskazujšca kierunek przepływu cieczy. Program TUNNEL, przedstawiony na rysunku 16-11 jest najbardziej efektowny w tym cyklu. Do uzyskania ciekawego efektu ruchu zastosowano paletę składa- jšcš się aż ze 128 pozycji. TUNNEL.C /* TUNNEL.C - Animacja palety - demonstracja (c) Charles Petzold. 1998 */ iţinclude 4fdefine ID TIMER 1 TCHAR szAppName [] = TEXT ("Tunnel") ; TCHAR szTitle [] = TEXT ("Tunnel: Palette Animation Demo") ; static LOGPALETTE * plp ; HPALETTE CreateRoutine (HWND hwnd) ( BYTE byGrayLevel ; HPALETTE hPalette ; int i ; plp = malloc (sizeof (LOGPALETTE) + 255 * sizeof (PALETTEENTRY)) ; // Inicjuj pola struktury LOGPALETTE plp->palVersion = 0x0300 ; plp->palNumEntries = 128 ; for (i = 0 ; i < 128 ; i++) ( if (i < 64) byGrayLevel = (BYTE) (4 * i) ; else byGrayLevel = (BYTE) min (255, 4 * (128 - i)) ; plp->palPalEntry[i].peRed = byGrayLevel ; plp->palPalEntry[i].peGreen = byGrayLevel ; plp->palPalEntry[i].peBlue = byGrayLevel ; Rozdział 16: Palette Manager 781 plp->palPalEntry[i].peFlags = PCţRESERVED ; plp->palPalEntry[i + 128].peRed = byGrayLevel ; plp->palPalEntry[i + 128].peGreen = byGrayLevel ; plp->palPalEntry[i + 128].peBlue = byGrayLevel ; plp->palPalEntry[i + 128].peFla9s = PCţRESERVED ; hPalette = CreatePalette (plp) ; , SetTimer (hwnd. ID_TIMER, 50, NULL) ; ţ' return hPalette ; jj 'i void PaintRoutine (HDC hdc, int cxClient, int cyClient) ţ; HBRUSH hBrush ; int i : Iţ RECT rect ; for (i = 0 ; 'i < 127 ; i++) // Zastosuj strukturę RECT dla każdego ze 128 prostokatów rect.left - i * cxClient / 255 ; rect.top - i * cyClient / 255 ; rect.right = cxClient - i * cxClient / 255 ; rect.bottom = cyClient - i * cyClient / 255 ; i. hBrush = CreateSolidBrush (PALETTEINDEX (i)) ; // Wypelnij prostokdt i usuń pędzel FillRect (hdc, &rect, hBrush) ; Delete0bject (hBrush) ; ) return ; void TimerRoutine (HDC hdc, HPALETTE hPalette) ( static int iLevel ; iLevel = (iLevel + 1) % 128 ; AnimatePalette (hPalette, 0, 128, plp->palPalEntry + iLevel) ; return ; ) void DestroyRoutine (HWND hwnd, HPALETTE hPalette) ( KillTimer (hwnd, ID TIMER) ; Delete0bject (hPalette) ; free (plp) ; return ; ) Rysunek 16-11. Program TUNNEL ţ82 Częœć II: Grafika TUNNEL wykorzystuje 64 poruszajšce się odcienie szaroœci, pobierajšc je z pale- ty zawierajšcej 128 kolorów - od czarnego do białego, a następnie z powrotem, od białego do czarnego. Daje to w suxnie efekt przejeżdżania przez tunel. Palety i obrazy rzeczywiste Pomijajšc wyœwietlanie cišgłych odcieni kolorów i zabawy z animacjš palety, prawdziwym zadaniem Palette Managera jest umożliwienie wyœwietlania obra- zów rzeczywistych w systemach pracujšcych w 8-bitowym trybie graficznym. Jak się już pewnie zorientowałeœ, paletę stosuje się inaczej dla upakowanych DIB (mapa bitowa o formacie niezależnym od sprzętu - ang. device independent bit- map), inaczej w przypadku obiektów GDI, a jeszcze inaczej dla sekcji DIB. W sze- œciu kolejnych przykładowych programach przedstawione zostanš różne tech- niki stosowania palet w odniesieniu do map bitowych. Palety i upakowane DIB Przedstawione poniżej trzy programy będš wykorzystywać funkcje obsługujšce bloki pamięci o specyfice upakowanych map DIB. Funkcje te zostały zebrane w plikach PACKEDIB, przedstawionych na rysunku 16-12. PACKEDIB.H /* PACKEDIB.H header file for PACKEDDIB.H */ iţinclude BITMAPINFO * PackedDibLoad (PTSTR szFileName) ; int PackedDibGetWidth (BITMAPINFO * pPackedDib) ; int PackedDibGetNeight (BITMAPINFO * pPackedDib) ; int PackedDibGetBitCount (BITMAPINFO * pPackedDib) ; int PackedDibGetRowlength (BITMAPINFO * pPackedDib) ; int PackedDibGetInfoHeaderSize (BITMAPINFO * pPackedDib) ; int PackedDibGetColorsUsed (BITMAPINFO * pPackedDib) ; int PackedDibGetNumColors (BITMAPINFO * pPackedDib) ; int PackedDibGetColorTableSize (BITMAPINFO * pPackedDib) ; RGBOUAD * PackedDibGetColorTablePtr (BITMAPINFO * pPackedDib) ; RGBOUAD * PackedDibGetColorTableEntry (BITMAPINFO * pPackedDib, int i) ; BYTE * PackedDibGetBitsPtr (BITMAPINFO * pPackedDib> ; int PackedDibGetBitsSize (BITMAPINFO * pPackedDib) ; HPALETTE PackedDibCreatePalette (BITMAPINFO * pPackedDib) ; PACKEDIB.C /* PACKEDIB.C - Funkcje narzędziowe spakowanej mapy DIB (c) Charles Petzold, 1998 */ iţinclude Rozdział 16: Palette Manager 783 /* PackedDibLoad: taduje plik DIB w postaci spak”wanej do pamięci */ BITMAPINFO * PackedDibLoad (PTSTR szFileName) t BITMAPFILEHEADER bmfh ; BITMAPINFO * pbmi ; BOOL bSuccess ; DWORD dwPackedDibSize, dwBytesRead ; HANDLE hFile ; // Otwórz plik z możliwoœcid odczytu i zabronionym zapisem hFile = CreateFile (szFileName, GENERIC_READ, FILE_SHARE_READ, NULL, OPENţEXISTING. FILEţFLAGţSEOUENTIAL SCAN, NULL) ; if (hFile = INVALID HANDLĘ VALUE) return NULL ; // Wczytaj BITMAPFILEHEADER bSuccess = ReadFile (hFile, &bmfh, sizeof (BITMAPFILEHEADER), &dwBytesRead, NULL) ; if (!bSuccess ţţ (dwBytesRead != sizeof (BITMAPFILEHEADER)) ţţ (bmfh.bfType != * (WORD *) "BM")) i CloseHandle (hFi7e) ; return NULL ; ) // Zarezerwuj obszar pamięci dla upakowanej DIB i wczytaj j dwPackedDibSize = bmfh.bfSize - sizeof (BITMAPFILEHEADER) ; pbmi = malloc (dwPackedDibSize) ; bSuccess = ReadFile (hFile, pbmi, dwPackedDibSize, &dwBytesRead, NULL) ; CloseHandle (hFile) ; if (!bSuccess ţţ (dwBytesRead != dwPackedDibSize)) ( free (pbmi) ; return NULL ; ) return pbmi ; 1 /* Funkcje pozyskujšce informacje o upakowanej DIB */ int PackedDibGetWidth (BITMAPINFO * pPackedDib) ( if (pPackedDib->bmiHeader.biSize = sizeof (BITMAPCOREHEADER)) return ((PBITMAPCOREINFO)pPackedDib)->bmciHeader.bcWidth ; Czgœć 11: Grafika (cišg dalszy ze strony 783) else return pPackedDib->bmiHeader.biWidth ; ) int PackedDibGetHeight (BITMAPINFO * pPackedDib) { if (pPackedDib->bmiHeader.biSize = sizeof (BITMAPCOREHEADER)) return ((PBITMAPCOREINFO)pPackedDib)->bmciHeader.bcHeight ; else return abs (pPackedDib->bmiHeader.biHeight) ; 1 int PackedOibGetBitCount (BITMAPINFO * pPacked0ib) ( if (pPackedDib->bmiHeader.biSize --- sizeof (BITMAPCOREHEADER)) return ((PBITMAPCOREINFO)pPacked0ib)->bmciHeader.bcBitCount ; else return pPackedDib->bmiHeader.biBitCount ; 1 int PackedDibGetRowLength (BITMAPINFO * pPackedDib) ( return ((PackedDibGetWidth (pPackedDib) * PackedDibGetBitCount (pPackedDib) + 31) & ţ31) Ż 3 ; ) /* PackedDibGetInfoHeaderSize zawiera maski kolorów! */ int PackedDibGetInfoHeaderSize (BITMAPINFO * pPackedDib) ( if (pPackedDib->bmiHeader.biSize --- sizeof (BITMAPCOREHEADER)) return ((PBITMAPCOREINFO)pPacked0ib)->bmciHeader.bcSize ; ) else if (pPackedDib->bmiHeader.biSize = sizeof (BITMAPINFOHEADER)) return pPackedDib->bmiHeader.biSize + (pPackedDib->bmiHeader.biCompression = BI BITFIELDS ? 12 : 0) : else return pPackedDib->bmiHeader.biSize ; /* PackedDibGetColorsUsed zwraca wartoœć w naglówku informacyjnym; Może przyjmować wartoœć 0 oznaczajšc tablicę kolorów o niezmiennych rozmiarach! */ int PackedOibGetColorsUsed (BITMAPINFO * pPackedDib) ( if (pPacked0ib->bmiHeader.biSize = sizeof (BITMAPCOREHEADER)) return 0 ; else return pPackedDib->bmiHeader.biClrUsed ; ) Rozdział 16: Palette Manager 785 /* PackedDibGetNumColors jest liczbd elementów tablicy kolorów */ int PackedOibGetNumColors (BITMAPINFO * pPacked0ib) ` int iNumColors ; iNumColors = PackedDibGetColorsUsed (pPackedDib) ; if (iNumColors = 0 &ţ PackedOibGetBitCount (pPackedDib) < 16) i'.. iNumColors = 1 Ž PackedDibGetBitCount (pPackedDib) ; return iNumColors ; ! z int PackedDibGetColorTableSize (BITMAPINFO * pPackedDib) if (pPackedDib->bmiHeader.biSize = sizeof (BITMAPCOREHEADER)) return PackedDibGetNumColors (pPacked0ib) * sizeof (RGBTRIPLE) ; else return PackedDibGetNumColors (pPacked0ib) * sizeof (RGBOUAD) ; '' RGBOUAD * PackedDibGetColorTablePtr (BITMAPINFO * pPacked0ib) ( if (PackedDibGetNumColors (pPackedDib) = 0) return 0 ; return (RGBOUAD *) (((BYTE *) pPackedDib) + PackedDibGetInfoHeaderSize (pPackedDib)) ; RGBOUAD * PackedOibGetColorTableEntry (BITMAPINFO * pPacked0ib, int i) ( if (PackedDibGetNumColors (pPackedDib) = 0) return 0 ; if (pPacked0ib->bmiHeader.biSize = sizeof (BITMAPCOREHEADER)) return (RGBOUAD *) (((RGBTRIPLE *) PackedDibGetColorTablePtr (pPackedDib)) + i) ; else return PackedOibGetColorTablePtr (pPacked0ib) + i ; 1 /* Wreszcie PackedOibGetBitsPtr! */ - BYTE * PackedOibGetBitsPtr (BITMAPINFO * pPackedDib) f return ((BYTE *) pPacked0ib) + PackedDibGetInfoHeaderSize (pPackedDib) + PackedDibGetColorTableSize (pPackedDib) ; 1 /* ţ . Wartoœć PackedOibGetBitsSize może być obliczona na podstawie liczby wierszy oraz szerokoœci mapy bitowej, jeœli nie jest podana wprost w polu biSizeImage */ 786 Częœć II: Grafika (cišg dalszy ze strony 785) int PackedDibGetBitsSize (BITMAPINFO * pPacked0ib) ( if ((pPackedDib->bmiHeader.biSize != sizeof (BITMAPCOREHEADER)) && (pPackedDib->bmiHeader.biSizeImage != 0)) return pPackedDib->bmiHeader.biSizeImage : return PackedDibGetHeight (pPackedDib) * PackedDibGetRowLength (pPacked0ib) ; /* PackedDibCreatePalette tworzy paletę logicznd na bazie upakowanej DIB */ HPALETTE PackedOibCreatePalette (BITMAPINFO * pPackedDib) ( HPALETTE hPalette ; int i, iNumColors ; LOGPALETTE * plp ; RGBOUAD * prgb ; if (0 = (iNumColors = PackedDibOetNumColors (pPackedDib))) return NULL ; plp = malloc (sizeof (LOGPALETTE) * (iNumColors - 1) * sizeof (PALETTEENTRY)) : p1p->palVersion = 0x0300 ; plp->palNumEntries = iNumColors : for (i = 0 ; i < iNumColors ; i++) ( prgb = PackedDibGetColorTableEntry (pPackedDib, i) : plp->palPalEntryCi].peRed = prgb->rgbRed : plp->palPalEntryCi].peGreen = prgb->rgbGreen ; plp->palPalEntryCi].peBlue = prgb->rgbBlue ; p1p->palPalEntryCi].peFlags = 0 : 1 hPalette = CreatePalette (plp) : free (plp) : return hPalette ; ) Rysunek 16-12. Program PACKEDIB Pierwsza funkcja to PackedDibLoad, która jako jedyny argument przyjmuje nazwę pliku i zwraca wskaŸnik do obszaru pamięci zajmowanego przez upakowanš DIB. Pozostałe funkcje przyjmujš ów wskaŸnik jako pierwszy argument i zwracajš , informacje o mapie. Funkcje zostały ułożone w pliku w kolejnoœci "odwrotnej" gdyż każda następna wykorzystuje informacje zwracane przez poprzedniš. Rozdział 16: Palette Manager 787 Nie ukrywam, że plik PACKEDIB.C nie zawiera kompletnego zestawu funkcji, które mogš być użyteczne przy obshzdze upakowanych DIB. Wynika to z mego przekonania, iż taki sposób podejœcia do problemu nie jest najwłaœciwszy. Mo- żesz się o tym łatwo przekonać, próbujšc napisać np. funkcję: dwPixel = PackedDibGetPixel (pPackedDib, x, y) ; Zawiera ona tak wiele zagnieżdżonych wywołań innych funkcji, że staje się nie- efektywna, a przez to wolna. W dalszej częœci rozdziahx opiszę inny, efektywny sposób podejœcia do problemu upakowanych DIB. Ponadto, jak pewnie zauważyłeœ, wiele tych funkcji wymaga innej obsługi DIB dla systemu OS/2. Przez to funkcje muszš często sprawdzać, czy pierwsze pole struktury BTTMAPINFO zawiera rozmiar innej struktury typu BITMAPCOREHE- ADER. Szczególnš uwagę należy zwrócić na ostatniš funkcję, PackedDibCreatePalette. Wykorzystuje ona tablicę kolorów zawartš w obiekcie DIB do utworzenia palety. Jeœli DIB nie ma tablicy kolorów (co oznacza, że piksele DIB sš opisywane 16, 24 lub 32 bitami), paleta nie jest tworzona. Paleta utworzona z kolorów pochodzš- cych z tablicy kolorów DIB nazywana jest czasem paletš rodzimš DIB. Funkcje skupione w plikach PACKEDIB zostały zastosowane w programie SHOW- DIB3, który przedstawiono na rysunku 16-13. SHOWDIB3.C /* SHOWDIB3.C - Wyœwietla mapę DIB przy pomocy wlasnej palety (c) Charles Petzold, 1998 */ tţinclude tţinclude "PackeDib.h" ţţinclude "resource.h" LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; TCHAR szAppName[] = TEXT ("ShowDib3") ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevlnstance, PSTR szCmdLine, int iCmdShow) HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CSţVREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITEţBRUSH) wndclass.lpszMenuName = szAppName ; i Częœć II: Grafika i (cišg dalszy ze sfrony 787) wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) ( MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MBţICONERROR) ; return 0 ; hwnd = CreateWindow (szAppName, TEXT ("Show DIB ţţ3: Native Palette"), WS_0llERLAPPEDWINDOW, CW_USEDEFAULT, CW USEDEFAULT, CW_USEDEFAULT, CW USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) : while (GetMessage (&msg, NULL, 0, 0)) ( TranslateMessage (&msg) ; DispatchMessage (&msg) ; l return msg.wParam ; ) LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ( static BITMAPINFO * pPackedDib ; static HPALETTE hPalette ; static int cxClient, cyClient ; static OPENFILENAME ofn ; static TCHAR szFileName CMAXţPATH], szTitleName CMAXţPATH] ; static TCHAR szFilterC] = TEXT ("Bitmap Files (*.BMP)\0*.bmp\0") TEXT ("Al1 Files (*.*)\0*.*\0\0") ; HDC hdc ; PAINTSTRUCT ps ; switch (message) i case WM_CREATE: ofn.lStructSize = sizeof (OPENFILENAME) ; ofn.hwndOwner = hwnd ; ofn.hInstance = NULL ; ofn.lpstrFilter = szFilter ; ofn.lpstrCustomFilter = NULL ; ofn.nMaxCustFilter = 0 ; ofn.nFilterIndex = 0 ; ofn.lpstrFile = szFileName ; ofn.nMaxFile = MAX PATH ; ofn.lpstrFileTitle = szTitleName ; ofn.nMaxFileTitle = MAXţPATH ; ofn.lpstrInitialDir = NULL ; ofn.lpstrTitle = NULL ; ofn.Flags = 0 ; ofn.nFileOffset = 0 ; ofn.nFileExtension = 0 ; Rozdział 16: Palette Manager 789 ofn.lpstrDefExt = TEXT ("bmp") ; ofn.lCustData = 0 ; ofn.lpfnHook = NULL ; ofn.lpTemplateName = NULL ; ' i I return 0 ; ? case WM_SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; return 0 ; case WM COMMAND: switch (LOWORD (wParam)) case IDM FILE OPEN: // Pokaż okno dialogowe File Open if (!GetOpenFileName (&ofn)) return 0 ; // Jeœli istnieje upakowana DIB, zwolnij pamięć if (pPackedDib) i free (pPackedDib) ; pPackedDib = NULL ; // Jeœli istnieje paleta logiczna, usuń jd if (hPalette) ( Delete0bject (hPalette) ; hPalette = NULL ; // Zaladuj upakowanš DIB do pamięci SetCursor (LoadCursor (NULL. IDC WAIT)) ; ShowCursor (TRUE) ; pPackedDib = PackedDibLoad (szFileName) ; ShowCursor (FALSE) ; SetCursor (LoadCursor (NULL, IDC ŽRROW)) ; if (pPackedDib) // Utwórz paletę z istniejšcej tablicy kolorów DIB hPalette = PackedDibCreatePalette (pPackedDib) ; 1 else MessageBox (hwnd, TEXT ("Cannot load DIB file"), szAppName, 0) ; - 790 Częœć II: Grafika i (cišg dalszy ze strony 789) i InvalidateRect (hwnd, NULL, TRUE) ; ' i return 0 ; break ; I case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; ; if (hPalette) f SelectPalette (hdc, hPalette, FALSE) ; RealizePalette (hdc) ; if (pPacked0ib) SetDIBitsToDevice (hdc, 0, 0, PackedDibGetWidth (pPackedDib), PackedOibGetHeight (pPackedDib), 0, 0, 0, PackedDibGetHeight (pPackedDib), PackedOibGetBitsPtr (pPackedDib), pPackedDib, DIBţRGB COLORS) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_OUERYNEWPALETTE: if (!hPalette) return FALSE ; hdc = GetDC (hwnd) ; SelectPalette (hdc. hPalette, FALSE) ; RealizePalette (hdc) ; InvalidateRect (hwnd, NULL, TRUE) ; ReleaseDC (hwnd, hdc) ; return TRUE ; case WM_PALETTECHANGED: if (!hPalette ţţ (HWND) wParam = hwnd) break ; hdc = GetDC (hwnd) ; SelectPalette (hdc, hPalette, FALSE) ; RealizePalette (hdc) ; UpdateColors (hdc) ; ReleaseDC (hwnd, hdc) ; break ; case WM_DESTROY: if (pPackedDib) Rozdział 16: Palette Manager 791 free (pPacked0ib) ; if (hPalette) Delete0bject (hPalette) ; PostOuitMessage (0) ; return 0 ; ' ) return DefWindowProc (hwnd, message, wParam, lParam) ; SHOWDIB3.RC (fragmenty) //Microsoft Developer Studio generated resource script. ;; ilinclude "resource.h" ilinclude "afxres.h" /////////////////////////////////////////////////////////////////////////// // Menu SHOWDIB3 MENU DISCARDABLE BEGIN POPUP "&File" BEGIN MENUITEM "&Open", IDMţFILE OPEN END END RESOURCE.H (fragment) // Microsoft Developer Studio generated include file. // Used by ShowDib3.rc lldefine IDM FILĘ OPEN 40001 Rysunek 16-13. Program SHOWDIB3 Procedura okna programu SHOWDIB3 przechowuje wskaŸruk do upakowanej DIB jako zmiennš statycznš. Otrzymuje jš po wywołaniu funkcji PacketDibLoad z modułu PACKEDIB.C w trakcie wykonywania polecenia File Open. W tym sa- ' mym momencie wywoływana jest funkcja PackedDibCreatePalette w celu uzyska- ţ. nia palety dla DIB. Zauważ, że kiedy program SHOWDIB3 jest gotowy do wczy- tania nowej DIB, najpierw zwalnia pamięć po poprzedniej DIB oraz usuwa jej paletę. Obie operacje odbywajš się w trakcie obsługi komunikatu WM DESTROY. Obsługa komunikatu WM PAINT jest prosta: jeœli paleta istnieje, SHOWDIB3 wybiera jš w kontekœcie urzšdzenia i realizuje. Następnie wywohzje funkcję Set- DIBitsToDevice i przekazuje do niej informacje o DIB (takie jak szerokoœć, wyso- koœć, wskaŸnik do obszaru pamięci w którym znajduje się DIB) otrzymane po wywołaniu odpowiednich funkcji moduhz PACKEDIB.C. 792 CzgœĆ II: Graflka i Pamiętaj, że program SHOWDIB3 tworzy paletę na podstawie tablicy kolorów obiektu DIB. Jeœli tablica nie istnieje - prawie zawsze gdy DIB jest 16-, 24- łub 32- bitowa - paleta w ogóle nie zostaje utworzona. jeœli DIB jest wyœwietlana w 8- bitowym trybie graficznym, używane sš jedynie standardowe kolory zarezerwo- wane przez system. Sš dwa rozwišzania tego problemu. Pierwszy to zastosowarue palety wielozada- ruowej, pasujšcej do większoœci obrazów - możesz jš skonstruować samodziel- rue. Drugie rozwišzanie to sprawdzenie wszystkich pikseli składajšcych się na DIB i wyznaczenie optymalnego zestawu kolorów niezbędnych do jej wyœwie- tlenia. Oczywiœcie wymaga to sporo pracy (zarówno od programisty, jak i od pro- cesora), lecz zostarue zademonstrowane w dalszej częœci rozdziału. Paleta wielozadaniowa Program SHOWDIB4 przedstawiony na rysunku 16-14, konstruuje paletę wielo- zadaniowš stosowanš do wyœwietlania map DIB. SHOWDIB4 jest bardzo podobny do SHOWDIB3. SHOWDIB4.C /* SHOWDIB4.C - Wyœwietla mapę DIB przy pomocy palety wielozadaniowej (c) Charles Petzold, 1998 */ ţţinclude ţţinclude "..\\ShowDib3\\PackeDib.h" ţţinclude "resource.h" LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; TCHAR szAppNameC) = TEXT ("ShowDib4") ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CS VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITĘ BRUSH) wndclass.lpszMenuName = szAppName ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) i Rozdział 16: Palette Manager 793 MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MBţICONERROR) ; return 0 ; J hwnd = CreateWindow (szAppName, TEXT ("Show DIB ţţ4: All-Purpose Palette"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW USEDEFAULT, CW_USEDEFAULT, CW USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (dţmsg, NULL, 0, 0)) TranslateMessage (&msg) ; DispatchMessage (&msg) ; ) return msg.wParam ? /* CreateAllPurposePalette: Tworzy paletę pasujdcd do szerokiej gamy obrazów; paleta zawiera 247 pozycji, jednak 15 z nich to standardowe kolory rezerwowane dla potrzeb systemu. */ HPALETTE CreateAllPurposePalette (void) f HPALETTE hPalette ; int i, incr, R, G, B ; LOGPALETTE * plp ; plp = malloc (sizeof (LOGPALETTE) + 246 * sizeof (PALETTEENTRY)) ; plp->palVersion = 0x0300 ; plp->palNumEntries = 247 ; // Poniższa pętla wylicza wartoœci 31 odcieni szaroţci, lecz 3 z nich // sd identyczne z 20 kolorami standardowymi for (i = 0, G = 0, incr = 8 ; G <= OxFF ; i++, G += incr) H plp->palPalEntry[i].peRed = (BYTE) G ; plp->palPalEntry[i].peGreen = (BYTE) G ; plp->palPalEntry[i).peBlue = tBYTE) G ; plp->palPalEntry[i].peFlags = 0 ; incr = (incr = 9 ? 8 : 9) ; ) // Poniższa pętla wylicza 216 pozycji paiety, lecz 8 z nich // jest identyczne z kolorami standardowymi, a dalsze // 4 pokrywajd się z już wyliczonymi odcieniami szaroœci. for (R = 0 ; R <= OxFF ; R += 0x33) for (G = 0 ; G <= OxFF ; G += 0x33) 794 Częœć II: Grafika (cišg dalszy ze strony 793) for (B = 0 ; B <= OxFF ; B += 0x33) plp->palPalEntry[i].peRed = (BYTE) R ; plp->palPalEntry[i].peGreen = (BYTE) G ; plp->palPalEntry[i].peBlue = (BYTE) B ; plp->palPalEntry[i].peFlags = 0 ; i++ ; ) hPalette = CreatePalette (plp) ; free (plp) ; return hPalette ; 1 LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ( f static BITMAPINFO * pPackedDib ; static HPALETTE hPalette ; static int cxClient, cyClient ; static OPENFILENAME ofn ; static TCHAR szFileName [MAX_PATH], szTitleName [MAX_PATH] ; static TCHAR szFilter[] = TEXT ("Bitmap Files (*.BMP)\0*.bmp\0") TEXT ("Al1 Files (*.*)\0*.*\0\0") ; HDC hdc ; PAINTSTRUCT ps ; j switch (message) ( case WM_CREATE: ofn.lStructSize = sizeof (OPENFILENAME) ; ofn.hwndOwner = hwnd ; ofn.hInstance = NULL ; ofn.lpstrFilter = szFilter ; ofn.lpstrCustomFilter = NULL ; ofn.nMaxCustFilter = 0 ; ofn.nFilterIndex = 0 ; ofn.lpstrFile = szFileName ; ofn.nMaxFile = MAX PATH ; ofn.lpstrFileTitle = szTitleName ; ofn.nMaxFileTitle = MAX_PATH ; ofn.lpstrInitialDir = NULL ; ofn.lpstrTitle = NULL ; ofn.Flags = 0 ; ofn.nFileOffset = 0 ; ofn.nFileExtension = 0 ; ofn.lpstrDefExt = TEXT ("bmp") ; ofn.lCustData = 0 ; ofn.lpfnHook = NULL ; ofn.lpTemplateName = NULL ; // Utwórz paletę wielozadaniowd hPalette = CreateAllPurposePalette () ; return 0 ; case WM SIZE: Rozdział 16: Palette hlanager 795 cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; return 0 ; case WM COMMAND: switch (LOWORD (wParam)) ( case IDM FILE OPEN: // Pokaż okno dialogu File Open if (!GetOpenFileName (&ofn)) return 0 ; // Jeœli istnieje upakowana DIB, zwolnij pamięć if (pPackedDib) ' 1'. free (pPackedDib) ; pPackedDib = NULL ; // Załaduj upakowanš DIB do pamięci SetCursor (LoadCursor (NULL, IDC WAIT)) ; ShowCursor (TRUE) ; pPackedDib = PackedOibLoad (szFileName) ; ShowCursor (FALSE) ; SetCursor (LoadCursor (NULL. IDC ARROW)) ; if (!pPackedDib) MessageBox (hwnd, TEXT ("Cannot load DIB file"), szAppName, 0) ; InvalidateRect (hwnd, NULL. TRUE) ; ! return 0 ; break ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; if (pPackedDib) SelectPalette (hdc, hPalette, FALSE) ; RealizePalette (hdc) ; SetDIBitsToOevice (hdc, 0, 0, PackedDibGetWidth (pPackedDib), PackedDibGetHeight (pPackedDib), 0, 0, 0, PackedDibGetHeight (pPackedDib), 796 Częœć II: Grafika (cišg dalszy ze strony 795) PackedDibGetBitsPtr (pPackedDib), pPacked0ib, DIB RGB COLORS) ; ) EndPaint (hwnd, &ps) ; return 0 ; case WM OUERYNEWPALETTE: hdc = GetDC (hwnd) ; SelectPalette (hdc. hPalette, FALSE) ; RealizePalette (hdc) ; InvalidateRect (hwnd, NULL, TRUE) ; ReleaseDC (hwnd, hdc) ; return TRUE ; case WM_PALETTECHANGED: if ((HWND) wParam != hwnd) hdc = GetDC (hwnd) ; SelectPalette (hdc, hPalette, FALSE) ; RealizePalette (hdc) ; UpdateColors (hdc) ; ReleaseDC (hwnd, hdc) ; break ; case WM_DESTROY: if (pPacked0ib) free (pPacked0ib) ; Delete0bject (hPalette) ; PostOuitMessage (0) ; return 0 ; 1 return DefWindowProc (hwnd, message, wParam, lParam) ; 1 SHOWDIB4.RC (fragmenty) //Microsoft Developer Studio generated resource script. ilinclude "resource.h" ilinclude "afxres.h" /////////////////////////////////////////////////////////////////////////// // Menu SHOWDIB4 MENU DISCARDABLE BEGIN POPUP "&Open" BEGIN MENUITEM "&File", IDMţFILĘ OPEN END END Rozdział 16: Palette ţanager 797 RESOURCE.H (fragmenty) // Microsoft Developer Studio generated include file. // Used by ShowDib4.rc tldefine IDMţFIIĘ OPEN 40001 Rysunek 1&14. Program SHOWDIB4 W trakcie obshxgi komunikatu WM CREAT'E program SHOWDIB4 wywohzje funkcję CreateAlIPurposesPalette. Utworzona paleta przechowywana jest przez cały czas działania programu i niszczona w trakcie obsługi komunikatu WMţDE- STROY. Porueważ program wie, że w każdej chwili ma do dyspozycji paletę, nie musi tego sprawdzać podczas obsługi komunikatów WM PAINT, WMţQUERY NEWPALETTE i WM PALETTECHANGED. Może się wydawać, że funkcja CreateAllPurposesPalette tworzy paletę zawierajš- cš 247 pozycji, czyli więcej niż 236 powszechnie dostępnych dla aplikacji pozycji palety systemowej. I rzeczywiœcie tak jest, lecz jedynie z powodu konwencji ozna- czarua pozycji palety: 15 spoœród 247 pozycji to duplikaty lub odwzorowania 20 standardowych kolorów zarezerwowanych. Funkcja CreateAllPurposesPalette rozpoczyna od utworzenia 31 odcieni szaroœci o następujšcych wartoœciach RGB: 0x00, 0x09, 0x11, OxIA, 0x22, Ox2B, 0x33, Ox3C, 0x44, Ox4D, 0x55, Ox5E, 0x66, Ox6F, 0x77, 0x80, 0x88, 0x91, 0x99, OxA2, OxAA, OxB3, OxBB, OxC4, OxCC, OxD5, OxDD, OxE6, OxEE, OxF9 oraz OxFF. Zauważ, że warto- œci: pierwsza, ostatnia i œrodkowa znajdujš się wœród 20 standardowych kolorów zarezerwowanych. Następnie funkcja tworzy kolory będšce kombinacjami skła- dowych czerwonej, zielonej i niebieskiej, o wartoœciach: 0x00, 0x33, 0x66, 0x99, OxCC i OxFE Daje to w sumie 216 kolorów, z których 8 stanowi duplikaty kolo- rów standardowych, a dalsze 4 sš identyczne z już wygenerowanymi odcieniami szaroœci. Windows nie umieœci dublowanych kolorów w palecie systemowej, je- œli pole peFlags struktury PALETTEENTftY przyjmie wartoœć 0. Oczywiœcie jeœli programista nie chce wyznaczać optymalnych palet dla DIB 16- , 24- lub 32-bitowych, będzie prawdopodobnie korzystać z tablicy kolorów DIB. SHOWDIB4 wykorzystuje paletę wielozadaniowš, gdyż jest programem demon- stracyjnym, który możesz wykorzystać, do porównania efektów wyœwietlania 8- bitowej DIB z programem SHOWDIB3. Jeœli przyjrzysz się DIB przedstawiajš- cym postaci ludzkie, zauważysz, że program SHOWDIB4 rue dysponuje odpo- wiedniš liczbš kolorów, aby dokładnie przedstawić różnice w odcieruach koloru skóry. Jeœli przeprowadzisz kilka eksperymentów z funkcjš CreateAllPurposesPalette, na przykład zmruejszajšc rozmiar palety logicznej do zaledwie kilku pozycji, odkry- jesz, że w przypadku palety wybranej w kontekœcie urzšdzenia Windows użyje kolorów pochodzšcych tylko z niej, nie korzystajšc z żadnego spoœród 20 zare- zerwowanych. Częœć II: Grafika Paleta półtonowa 1 Windows API zawiera paletę wielozadaniowš, dostępnš z poziomu aplikacji przez wywołanie funkcji CreateHalftonePalette. Uzyskana w ten sposób paleta może być używana tak samo jak otrzymana przy użyciu CreateAllPurposesPalette lub w po- łšczeniu z trybem rozcišgania - ustawianym funkcjš SetStretchBItMode - zwanym HALFTONE (półton). Program SHOWDIB5 przedstawiony na rysunku 16-15 de- monstruje użycie palety półtonowej. SHOWDIB5.C i* SHOWDIB5.C - Wyœwietla mapę DIB za pomocš palety póltonowej (c) Charles Petzold, 1998 *ţ ţlinclude ţlinclude "..\\Show0ib3\\PackeOib.h" Ilinclude "resource.h" LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; TCHAR szAppNameC] = TEXT ("Show0ib5") ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) . HWND hwnd ; MSG msg ; WNOCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CS VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = szAppName ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) ( MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MBţICONERROR) ; return 0 ; ) hwnd = CreateWindow (szAppName, TEXT ("Show DIB 115: Halftone Palette"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW USEDEFAULT, NULL, NULL, hInstance. NULL) ; ShowWindow (hwnd, iCmdShow) ; Rozdział 16: Palette Manager UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) ( TranslateMessage (&msg) ; DispatchMessage (&msg) ; ) return msg.wParam ; LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) static BITMAPINFO * pPackedDib ; static HPALETTE hPalette ; static int cxClient, cyClient : static OPENFILENAME ofn ; static TCHAR szFileName CMAX_PATH], szTitleName [MAX_PATH] ; static TCHAR szFilterC] = TEXT ("Bitmap Files (*.BMP)\0*.bmp\0") TEXT ("All Files (*.*)\0*.*\0\0") ; HDC hdc ; PAINTSTRUCT ps : switch (message) case WM_CREATE: ofn.lStructSize = sizeof (OPENFILENAME) ; ofn.hwndOwner = hwnd ; ofn.hInstance = NULL ; ofn.lpstrFilter = szFilter ; ofn.lpstrCustomFilter = NULL ; ofn.nMaxCustFilter = 0 ; ofn.nFilterIndex = 0 ; ofn.lpstrFile = szFileName ; ofn.nMaxFile = MAX PATH ; ofn.lpstrFileTitle = szTitleName ; ofn.nMaxFileTitle = MAXţPATH ; ofn.lpstrInitialDir = NULL ; ofn.lpstrTitle = NULL ; ofn.Flags = 0 ; ofn.nFileOffset = 0 ; ofn.nFileExtension = 0 ; ofn.lpstrDefExt = TEXT ("bmp") ; ofn.lCustOata = 0 ; ofn.lpfnHook = NULL ; ofn.lpTemplateName = NULL ; // Utwórz paletę wielozadaniowš hdc = GetDC (hwnd> ; hPalette = CreateHalftonePalette (hdc) ; ReleaseDC (hwnd, hdc) : return 0 ; case WM_SIZE: cxClient = LOWORD (lParam) : cyClient = HIWORD (lParam) ; return 0 : Częœć 11: Grafika (cišg dalszy ze strony 799) case WM COMMAND: switch (LOWORD (wParam)) t i case IDM FILĘ OPEN: // Pokaż okno dialogu File Open 'ř if (!óetOpenFileName t&ofn)) 1 return 0 ; // Jeœli istnieje upakowana DIB, zwolnij pamięć if (pPackedDib) ( free (pPackedDib) ; pPackedDib = NULL ; ) // Zaladuj upakowand DIB do pamięci SetOursor (LoadCursor (NULL, IDC WAIT)) ; ShowCursor (TRUE) ; pPackedDib = PackedDibLoad (szFileName) ; ShowCursor (FALSE) ; SetCursor (LoadCursor (NULL, IDC ŽRROW)) ; if (!pPackedDib) ( MessageBox (hwnd, TEXT ("Cannot load DIB file"), szAppName, 0) ; 1 InvalidateRect (hwnd, NULL, TRUE) ; return 0 ; ) break ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; if (pPackedDib) ( // Ustaw tryb rozcišgania dla półtonów SetStretchBltMode (hdc, HALFTONE) ; SetBrushOrgEx (hdc, 0, 0, NULL) ; // Wybierz i zrealizuj paletę pdłtonowa SelectPalette (hdc, hPalette, FALSE) ; RealizePalette (hdc) ; // StretchDIBits zamiast SetDIBitsToOevice StretchDIBits (hdc. 0, Rozdział 16: Palette Manageţ 801 o. PackedOibGetWidth (pPackedDib), PackedDibGetHeight (pPackedDib), 0. 0, PackedDibGetWidth (pPackedDib), PackedOibGetHeight (pPacked0ib), PackedDibGetBitsPtr (pPacked0ib), pPackedDib, DIB_RGB_COLORS, SRCCOPY) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_oUERYNEWPALETTE: hdc = GetDC (hwnd) ; SelectPalette (hdc, hPalette, FALSE) ; RealizePalette (hdc) ; InvalidateRect (hwnd. NULL, TRUE) ; ReleaseDC (hwnd. hdc) ; return TRUE ; case WM_PALETTECHANGED: if ((HWND) wParam != hwnd) hdc = GetDC (hwnd) ; SelectPalette (hdc, hPalette, FALSE) ; RealizePalette (hdc) ; UpdateColors (hdc) ; ReleaseDC (hwnd, hdc) ; break ; case WM_DESTROY: if (pPackedDib) free (pPackedDib) ; Delete0bject (hPalette) ; PostouitMessage (0) ; return 0 ; ) return DefWindowProc (hwnd, message, wParam, lParam) ; SHOWDIB5.RC (fragmenty) //Microsoft Developer Studio generated resource script. ţpinclude "resource.h" ţţinclude "afxres.h" llllllllllllllllllllllllllllllllllllllllllllll!lllllllllllllllll!llllllll!lll Menu P':: . , ... 802 Częœć II: Grafika (cišg dalszy ze strony 803) SHOWDIB5 MENU DISCARDABLE BEGIN POPUP "&Open" BEGIN MENUITEM "&File", IDM_FILE_OPEN END END RESOURCE.H (fragrţţenty) // Microsoft Developer Studio generated include file. // Used by ShowDib5.rc // ţldefine IDM FILĘ OPEN 40001 Rysunek 16-15. Program SHOWDIB5 Program SHOWDIB5 jest podobny do SHOWDIB4, ale nie korzysta z tablicy ko- lorów mapy DIB, zamiast tego stosujšc paletę, która jest odpowiednia dla wielu różnych obrazów. SHOWDIB5 stosuje paletę dostarczanš przez system Windows, której uchwyt uzyskuje się po wywołaniu funkcji CreateHalftonePalette. Paleta półtonowa jest bardziej złożona niż paleta tworzona przy użyciu funkcji CreateAllPurposePalette w przykładowym programie SHOWDIB4, lecz wyniki ich zastosowania sš mocno zbliżone. Jednak jeœli wywołasz kolejno poniższe funk- cje: SetStretchBltMode(hdc, HALFTONE) ; SetBrushOrgEx(hdc, x, y, NULL) ; gdzie x i y sš współrzędnymi górnego lewego narożnika DIB, następnie wyœwie- tlisz DIB stosujšc funkcję StretchDIBits, wynik może cię zaskoczyć. Odcienie ko- loru ciała sš znacznie wierniejsze niż przy zastosowaniu palety tworzonej za pomocš funkcji CreateAlIPurposePalette czy też CreateHalftonePalette w wersji bez ustawionego trybu rozcišgania mapy bitowej. Windows stosuje pewien rodzaj symulowania danego wzorca kolorami palety półtonowej w celu lepszego przy- bliżenia kolorów oryginalnego obrazu przez 8-bitowe karty graficzne. Jak łatwo się domyœlić, lepsza jakoœć uzyskiwana jest kosztem wydłużenia czasu obliczeń. Indeksowanie palety kolorów Nadszedł czas, aby zajšć się argumentem fClrUse funkcji SetDIBitsToDevice, Stretch- DIBits, CreateDIBitmap, SetDIBits, GetDIBits oraz CreateDIBSection. Zwykle argu- ment ten przyjmuje wartoœć stałej DIB RGB COLORS, która równa się 0. Może też przyjšć wartoœć DIB PAL COLORS. W tym wypadku zakłada się, że tablica kolorów struktury BITMAPINFO jest złożona nie z wartoœci RGB, lecz z 16-bito- wych indeksów wskazujšcych elementy palety logicznej, wybranej w kontekœcie urzšdzenia, będšcym pierwszym argumentem funkcji. Jakie znaczenie ma stała DIB_PAL COLORS? Dzięki jej zastosowaniu uzysku- jesz większš wydajnoœć pracy programu. WeŸmy pod uwagę 8-bitowš DIB, wy- Rozdział 16: Palette Manager 803 œwietlanš w 8-bitowym trybie graficznym. Windows musi najpierw odnaleŸć kolory tablicy karty graficznej najbardziej zbliżone do tych, które znajdujš się w tablicy kolorów DIB. Na tej podstawie system może zbudować tablicę odwzo- rowań wartoœci pikseli DIB w odpowiednie wartoœci zwišzane z paletš karty gra- ficznej. Oznacza to 256 tego typu poszukiwań kolorów zbliżonych, czego można uniknšć, jeœli tablica kolorów DIB zawiera indeksy do palety logicznej wybranej w kontekœcie urzšdzenia. Program SHOWDIB6 przedstawiony na rysunku 16-16 działa podobnie jak SHOWDIB3, ale wykorzystuje indeksy palety. SHOWDIB6.C /* SHOWDIB6.C - Wyœwietla mapę DIB przy pomocy palety indeksowanej (c) Charles Petzold, 1998 */ ţţinclude ţţinclude "..\\ShowDib3\\PackeDib.h" ţţinclude "resource.h" LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; TCHAR szAppNameC7 = TEXT ("ShowDib6") ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CS VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITEţBRUSH) wndclass.lpszMenuName = szAppName ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) ( MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MBţICONERROR) ; return 0 ; hwnd = CreateWindow (szAppName, TEXT ("Show DIB ţţ6: Palette Indices"). WS OVERLAPPEDWINDOW, CW USEDEFAULT, CW USEDEFAULT, CW_USEDEFAULT, CW USEDEFAULT, NULL, NULL. hInstance, NULL) ; Częœć N: Grafika (cišg dalszy ze strony 803) ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) f TranslateMessage (&msg) ; DispatchMessage (&msg) ; ) return msg.wParam ; ) LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ( static BITMAPINFO * pPackedDib ; static HPALETTE hPalette ; static int cxClient, cyClient ; static OPENFILENAME ofn ; static TCHAR szFileName [MAX_PATH], szTitleName [MAX_PATH] ; static TCHAR szFilter[] = TEXT ("Bitmap Files (*.BMP)\0*.bmp\0") TEXT ("All Files (*.*)\0*.*\0\0") ; HDC hdc ; int i, iNumColors ; PAINTSTRUCT ps ; WORD * pwIndex ; switch (message) 1 case WM_CREATE: ofn.lStructSize = sizeof (OPENFILENAME) ; ofn.hwndOwner = hwnd ; ofn.hInstance = NULL ; ofn.lpstrFilter = szFilter ; ofn.lpstrCustomFilter = NULL ; ofn.nMaxCustFilter = 0 ; ofn.nFilterIndex = 0 ; ofn.lpstrFile = szFileName ; ofn.nMaxFile = MAX PATH ; ofn.lpstrFileTitle = szTitleName ; ofn.nMaxFileTitle = MAX_PATH ; ofn.lpstrInitialDir = NULL ; ofn.lpstrTitle = NULL ; ofn.Flags = 0 ; ofn.nFileOffset = 0 ; ofn.nFileExtension = 0 ; ofn.lpstrDefExt = TEXT ("bmp") ; ofn.lCustData = 0 ; ofn.lpfnHook = NULL ; ofn.lpTemplateName = NULL ; return 0 ; case WM_SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; return 0 ; case WM COMMAND: Rozdział 16: Palette Manager 805 switch (LOWORD (wParam)) t case IDMţFILĘ OPEN: // Pokaż okno dialogowe File Open if (!GetOpenFileName (&ofn)) return 0 ; // Jeœli istnieje upakowana DIB. zwolnij pamięć if (pPackedDib) !i . i free (pPackedDib) ; pPaeked0ib = NULL ; !: // Jeœli istnieje paleta logiczna, usuń jš if (hPalette) I;. Delete0bject (hPalette) ; hPalette = NULL ; I; // Zaladuj upakowanš DIB do pamięci SetCursor (LoadCursor (NULL, IDC WAIT)) ; ShowCursor (TRUE) ; pPackedDib = PackedOibLoad (szFileName) ; ShowCursor (FALSE) ; SetCursor (LoadCursor (NULL, IDC ŽRROW)) ; if (pPacked0ib) ( i // Utwórz paletę na podstawie tablicy kolorów DIB hPalette = PackedDibCreatePalette (pPacked0ib) ; ! 3 // Zastdp kolory DIB indeksami if (hPalette) s iNumColors = PackedDibGetNumColors (pPackedDib) ; 1 pwIndex = (WORD *) PackedDibGetColorTablePtr (pPackedDib) ; for (i = 0 ; i < iNumColors ; i++) pwIndex[i] = (WORD) i ; J else ( MessageBox (hwnd, TEXT ("Cannot load DIB file"), szAppName. 0) : 806 Częœć IIů Grafika (cišg dalszy ze strony 805) InvalidateRect (hwnd, NULL, TRUE) ; return 0 ; ) break ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; if (hPalette) ( SelectPalette (hdc, hPalette, FALSE) ; RealizePalette (hdc) ; 1 if (pPackedDib) SetDIBitsToDevice (hdc, 0, 0, PackedDibGetWidth (pPackedDib), PackedDibGetHeight (pPackedDib), 0, 0, 0, PackedDibGetHeight (pPackedDib>, PackedDibţetBitsPtr (pPackedDib), pPackedDib, DIBţPALţCOLORS) ; EndPaint (hwnd, &ps) ; return 0 ; case WM OUERYNEWPALETTE: if (!hPalette) return FALSE ; hdc = GetDC (hwnd) ; SelectPalette (hdc, hPalette, FALSE) ; RealizePalette (hdc) ; InvalidateRect (hwnd, NULL, TRUE) ; ReleaseDC (hwnd, hdc) ; return TRUE ; case WM_PALETTECHANGED: if (!hPalette ţţ (HWND) wParam = hwnd) break ; hdc = GetDC (hwnd) ; SelectPalette (hdc, hPalette, FALSE) ; RealizePalette (hdc) ; UpdateColors (hdc) ; ReleaseDC (hwnd, hdc) ; break ; case WMţDESTROY: Rozdział 16: Palette Manager if (pPackedDib) free (pPackedDib) ; if (hPalette) Delete0bject (hPalette) ; PostOuitMessage (0) : return 0 ; ) return DefWindowProc (hwnd, messa9e, wParam, lParam) ; SHOWDIB6.RC (fragmenty) //Microsoft Developer Studio generated resource script. ilinclude "resource.h" ilinclude "afxres.h" /////////////////////////////////////////////////////////////////////////// // Menu SNOWDIB6 MENU DISCARDABLE BEGIN POPUP "&File" BEGIN MENUITEM "&Open", IDMţFILE OPEN END END RESOURCE.H (fragmenty) // Microsoft Developer Studio generated include file. // Used by ShowDib6.rc // ttdefine IDMţFILĘ OPEN 40001 Rysunek 16-16. Program SHOWDIB6. Po załadowaniu do pamięci mapy DIB program SHOWDIB6 tworzy na jej pod- stawie paletę kolorów. Kolory tablicy kolorów DIB zastępowane sš indeksami rozpoczynajšcymi się od 0. Wartoœć zwracana przez funkcję PackedDibGetNum- Colors okreœla liczbę kolorów, a funkcja PackedDibGetColorTablePtr zwraca wskaŸ- nik do poczštku tablicy kolorów DIB. Zauważ, że technika ta jest właœciwa jedynie w przypadku, kiedy paleta została utworzona bezpoœrednio na podstawie tablicy kolorów DIB. Stosujšc paletę wie- lozadaniowš, musiałbyœ sam poszukiwać najbardziej zbliżonych kolorów, i do- piero na tej podstawie wyznaczyć indeksy dla tablicy kolorów DIB. Jednak takie podejœcie nie byłoby efektywne, ani sensowne. Korzystajšc z indeksów palety pamiętaj o zmianie wartoœci w tablicy kolorów DIB przed zapisaniem ich na dysku oraz o tym, że rue należy umieszczać DIB wypeł- nionej indeksami palety w Schowku. Najbezpieczniejszym rozwišzaniem byłoby 808 Czgœć 11: Grafi wypełnienie jej indeksami palety tuż przed jej wyœwietleniem na ekranie, a r stępnie ponowne przywrócenie wartoœci RGB. Palety i obiekty bitmap Program SHOWDIB7, przedstawiony na rysunku 16-17, demonstruje, jak stoso- wać palety w połšczeniu z DIB, skonwertowanymi do postaci obiektów map bi- towych GDI przy użyciu funkcji CreateDIBitmap. SHOWDIB7.C /* SHOWDIB7.C - Wyœwietla mapę DIB skonwertowana do formatu DDB (c) Charles Petzold, 1998 */ ţfinclude ţţinclude "..\\ShowDib3\\PackeDib.h" ţţinclude "resource.h" LRESULT CALLBACK WndProc (HWND. UINT, WPARAM, LPARAM) ; TCHAR szAppName[] = TEXT ("ShowDib7") ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CS UREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITEţBRUSH) wndclass.lpszMenuName = szAppName ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) ( MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MBţICONERROR) ; return 0 ; hwnd = CreateWindow (szAppName, TEXT ("Show DIB ţţ7: Converted to DDB"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW USEDEFAULT, CW_USEDEFAULT, CW USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; Rczdział 16: Palette Manager while (6etMessage (&msg, NULL, 0, 0)) f TranslateMessage (&msg) ; DispatchMessage (&msg) ; return msg.wParam ; 1 LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ( static HBITMAP hBitmap : static HPALETTE hPalette ; static int cxClient, cyClient : static OPENFILENAME ofn static TCHAR szFileName [MAXţPATH], szTitleName [MAXţPATH] ; static TCHAR szFilter[] = TEXT ("Bitmap Files (*.BMP)\0*.bmp\0") TEXT ("All Files (*.*)\0*.*\0\0") ; BITMAP bitmap ; BITMAPINFO * pPackedDib ; HDC hdc, hdcMem ; PAINTSTRUCT ps ; switch (message) ( case WM_CREATE: ofn.lStructSize = sizeof (OPENFILENAME) : ofn.hwndOwner = hwnd ; ofn.hInstance = NULL ; ofn.lpstrFilter = szFilter ; ofn.lpstrCustomFilter,= NULL ; ofn.nMaxCustFilter = 0 ; ofn.nFilterIndex = 0 ; ofn.lpstrFile = szFileName ; ofn.nMaxFile = MAX PATH ; ofn.lpstrFileTitle = szTitleName ; ofn.nMaxFileTitle = MAXţPATH : ofn.lpstrInitialDir = NULL : ofn.lpstrTitle = NULL : ofn.Flags = 0 ; ofn.nFileOffset = 0 ; ofn.nFileExtension = 0 ; ofn.lpstrOefExt = TEXT ("bmp") : ofn.lCustOata = 0 ; ofn.lpfnHook = NULL ; ofn.lpTemplateName = NULL ; return 0 ; case WM_SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; return 0 ; case WM COMMAND: switch (LOWORD (wParam)) ( case IDMţFILĘ OPEN: 810 Częœć II: Grafika (cišg dalszy ze strony 809) // Pokaż okno dialogowe File Open if (!GetOpenFileName (&ofn)) return 0 ; // Jeœli istnieje upakowana DIB, zwolnij pamięć if (hBitmap) ( Delete0bject (hBitmap) ; hBitmap = NULL ; ) // Jeœli istnieje paleta logiczna, usuń jš if (hPalette) ( Delete0bject (hPalette) ; hPalette = NULL ; ) // Zaladuj upakowanš DIB do pamięci SetCursor (LoadCursor (NULL, IDC WAIT)) ; ShowCursor (TRUE) ; pPackedDib = PackedDibLoad (szFileName) ; ShowCursor (FALSE) ; SetCursor (LoadCursor (NULL, IDC ARROW)) ; if (pPackedDib) ( // Utwórz paletę na podstawie DIB i wybierz jš // w kontekœcie urzšdzenia hPalette = PackedDibCreatePalette (pPacked0ib) ; hdc = GetDC (hwnd) ; if (hPalette) ( SelectPalette (hdc, hPalette, FALSE) ; RealizePalette (hdc) ; ) // Utwórz DDB na postawie DIB hBitmap = CreateDIBitmap (hdc, (PBITMAPINFOHEADER) pPackedDib, CBM_INIT, PackedOibGetBitsPtr (pPacked0ib), pPackedDib, DIBţRGB COLORS) ; ReleaseDC (hwnd, hdc) ; // Zwolnij pamięć upakowanej DIB Rozdział 16: Palette Manageţ 811 free (pPackedDib) ; J ;'' else MessageBox (hwnd, TEXT ("Cannot load DIB file"), szAppName, 0) : ) InvalidateRect (hwnd, NULL, TRUE) ; return 0 ; ) break ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) : if (hPalette) { SelectPalette (hdc, hPalette, FALSE) ; RealizePalette (hdc) ; ) if (hBitmap) ( GetObject (hBitmap, sizeof (BITMAP), &bitmap) ; hdcMem = CreateCompatibleDC (hdc) ; SelectObject (hdcMem, hBitmap) ; BitBlt (hdc, 0, 0, bitmap.bmWidth, bitmap.bmHeight, hdcMem, 0, 0, SRCCOPY) ; DeleteDC (hdcMem) ; ) EndPaint (hwnd, &ps) ; return 0 : case WM_OUERYNEWPALETTE: if (!hPalette) return FALSE ; hdc = GetDC (hwnd) ; SelectPalette (hdc, hPalette, FALSE) : RealizePalette (hdc) ; InvalidateRect (hwnd, NULL, TRUE) ; ReleaseDC (hwnd, hdc) ; return TRUE ; case WM_PALETTECHANGED: if (!hPalette ţţ (HWND) wParam --- hwnd) break ; hdc = GetDC (hwnd) ; SelectPalette (hdc, hPalette, FALSE) ; RealizePalette (hdc) ; UpdateColors (hdc) ; ReleaseDC (hwnd, hdc) ; break Częœć If: Grafika (cigg dalszy ze strony 811) case WM_DESTROY: if (hBitmap) Delete0bject (hBitmap) ; if (hPalette) Delete0bject (hPalette) ; PostOuitMessage (0) ; return 0 ; ) return DefWindowProc (hwnd, message, wParam, lParam) ; SHOWDIB7.RC (fragmenty) //Microsoft Developer Studio generated resource script. llinclude "resource.h" llinclude "afxres.h" /////////////////////////////////////////////////////////////////////////// // Menu SHOWDIB7 MENU DISCARDABLE BEGIN POPUP "&File" BEGIN MENUITEM "&Open", IDM FILE OPEN END END RESOURCE.H (fragmenty) // Microsoft Developer Studio generated include file. // Used by Show0ib7.rc ţIdefine IDM FILĘ OPEN 40001 Rysunek 1„17. Program SHOWDIB7 Tak jak w poprzednich przykładach, SHOWDIB7 uzyskuje wskaŸnik do upako- wanej DIB po wykonaniu polecenia File Open. Tworzy również paletę na pod- stawie bitmapy. Następnie - w trakcie obshxgi komunikatu WMţCOMMAND - wybiera paletę w kontekœcie urzšdzenia, po czym jš realizuje. Program SHOW- DIB7 wywołuje funkcję CreateDIBitmap, która tworzy DDB (bitmapę o formacie zależnym od sprzętu - ang. device dependent bitmap) na podstawie DIB. Jeżeli pa- leta nie została wybrana i zrealizowana w kontekœcie urzšdzenia, DDB nie bę- dzie korzystać z dodatkowych kolorów palety logicznej. Po wywołaniu funkcji CreateDIBitmap program może zwolnić pamięć zajmowa- nš przez upakowanš DIB, przy czym zmienna pPackedDib nie jest zmiennš sta- Rozdział 16: Palette lţanager 813 tycznš. Zamiast niej program SHOWDIB7 przechowuje w postaci zmiennych sta- tycznych uchwyt bitmapy (hBitmap) oraz uchwyt palety logicznej (hPalette). Podczas obsługi komunikatu WM-PAáţlT paleta ponownie jest wybierana w kon- tekœcie urzšdzenia i realizowana. Szerokoœć i długoœć bitmapy otrzymuje się po wywołaniu funkcji GetObject. Majšc te informacje, program może wyœwietlić mapę po utworzeniu odpowiedruego kontekstu urzšdzenia w pamięci, wybraniu mapy w tym kontekœcie i wywołaniu funkcji BitBlt. Wyœwietlajšc DDB należy użyć pa- lety stosowanej do wywołarua funkcji CreateDIBitmap. Kopiujšc bitmapę do Schow- ka, najkorzystniej jest doprowadzić jš uprzednio do formatu upakowanej DIB - tylko wtedy system Windows będzie mógł zapewnić swobodny dostęp do ruej z poziomu innych aplikacji. Oczywiœcie w tym celu należy skonwertować DDB do postaci DIB na podstawie bieżšcej palety systemowej. Palety i sekcje DIB Program SHOWDIB8 przedstawiony na rysunku 16-18 demonstruje, jak stoso- wać paletę kolorów, gdy mamy do czynienia z sekcjš DIB. SHOWDIB8.C /* SHOWDIB8.C - Wyœwietla mapę DIB skonwertowana do formatu sekcji DIB (c) Charles Petzold, 1998 */ ilinclude 4linclude "..\\ShowDib3\\PackeDib.h" ilinclude "resource.h" LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; TCHAR szAppNameC] = TEXT ("ShowDib8") ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CS UREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE-BRUSH) wndclass.lpszMenuName = szAppName ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) Częœć II: Grafika (cišg dalszy ze strony 813) MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MBţICONERROR) ; return 0 ; hwnd = CreateWindow (szAppName, TEXT ("Show DIB ţţ8: DIB Section"), WS_OVERLAPPEDWINDOW. CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) : UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) ( TranslateMessage (&msg) ; DispatchMessage (&msg) ; i return msg.wParam LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) f static HBITMAP hBitmap ; static HPALETTE hPalette ; static int cxClient, cyClient ; static OPENFILENAME ofn ; static PBYTE pBits ; static TCHAR szFileName [MAX_PATH], szTitleName CMAX_PATH] ; static TCHAR szFilter[) = TEXT ("Bitmap Files (*.BMP)\0*.bmp\0") TEXT ("All Files (*.*)\0*.*\0\0") : BITMAP bitmap ; BITMAPINFO * pPacked0ib ; HDC hdc, hdcMem ; PAINTSTRUCT ps ; switch (message) ( case WM_CREATE: ofn.lStructSize = sizeof (OPENFILENAME) ofn.hwndOwner = hwnd ; ofn.hInstance = NULL ; ofn.lpstrFilter = szFilter ; ofn.lpstrCustomFilter = NULL ; ofn.nMaxCustFilter = 0 ; ofn.nFilterIndex = 0 ; ofn.lpstrFile = szFileName ; ofn.nMaxFile = MAX PATH ; ofn.lpstrFileTitle = szTitleName ; ofn.nMaxFileTitle = MAXţPATH ; ofn.lpstrInitial0ir = NULL ; ofn.lpstrTitle = NULL ; ofn.Flags = 0 ; ofn.nFileOffset = 0 ; Rozdział 16: Palette Manager 815 ofn.nFileExtension = 0 ; ofn.lpstrDefExt = TEXT ("bmp") ; ofn.lCustData = 0 ; ofn.lpfnHook = NULL ; ofn.lpTemplateName = NULL ; return 0 : case WM_SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; return 0 ; case WM COMMAND: switch (LOWORD (wParam)) ( case IDM FILE OPEN: // Pokaż okno dialogowe File Open if (!GetOpenFileName (&ofn)) return 0 ; // Jeœli istnieje upakowana DIB, zwolnij pamięć if (hBitmap) ( Delete0bject (hBitmap) ; hBitmap = NULL ; // Jeœli istnieje paleta logiczna, usuń jš if (hPalette) t Delete0bject (hPalette) ; hPalette = NULL ; // Zaladuj upakowand DIB do pamięci SetCursor (LoadCursor (NULL, IDC WAIT)) ; ShowCursor (TRUE) ; pPackedDib = PackedDibLoad (szFileName) ; ShowCursor (FALSE) ; SetCursor (LoadCursor (NULL, IDC ARROW)) ; if (pPackedDib) f // Utwórz sekcję DIB na podstawie DIB hBitmap = CreateDIBSection (NULL, pPackedDib, DIBţRGBţCOLORS, &pBits, NULL, 0) ; // Skopiuj bity 816 Częœć il: Graftka (cišg daiszy ze strony 815) CopyMemory (pBits, PackedDibGetBitsPtr (pPackedDib), PackedDibGetBitsSize (pPackedDib)) ; // Utwórz paletę na podstawie DIB hPalette = PackedDibCreatePalette (pPackedDib) ; // Zwolnij pamięć upakowanej DIB ) else ( free (pPackedDib) ; MessageBox (hwnd, TEXT ("Cannot load DIB file">, szAppName, 0) ; ) InvalidateRect (hwnd, NULL, TRUE) ; return 0 ; ) break ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) : if (hPalette) ( SelectPalette (hdc, hPalette. FALSE) : RealizePalette (hdc) : 1 if (hBitmap) f GetObject (hBitmap, sizeof (BITMAP), &bitmap) ; hdcMem = CreateCompatibleDC (hdc) ; SelectObject (hdcMem, hBitmap) : BitBlt (hdc, 0, 0, bitmap.bmWidth, bitmap.bmHeight, hdcMem, 0, 0, SRCCOPY) : DeleteDC (hdcMem) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_OUERYNEWPALETTE: if (!hPalette) return FALSE : hdc = GetDC (hwnd) ; SelectPalette (hdc, hPalette, FALSE) ; RealizePalette (hdc) : InvalidateRect (hwnd, NULL, TRUE) ; ReleaseDC (hwnd, hdc) ; return TRUE ; Rozţial 16: Pslette kbnager - 817 case WM_PALETTECHANGED: if (!hPalette ţţ (HWND) wParam = hwnd) break ; hdc s GetDC (hwnd) ; SelectPalette (hdc, hPalette, FALSE) ; RealizePalette (hdc) ; UpdateColors (hdc) ; ReleaseDC (hwnd, hdc) ; break ; case WM_DESTROY: if (hBitmap) Delete0bject (hBitmap) ; if (hPalette) Delete0bject (hPalette) ; PostOuitMessage (0) ; return 0 ; 1 return DefWindowProc (hwnd, message, wParam, lParam) ; 1 SHOWDIB8.RC (fragmenty) //Microsoft Developer Studio generated resource script. ţţinclude "resource.h" ţţinclude "afxres.h" /////////////////////////////////l///////////////////////////////////////// // Menu SHOWDIB8 MENU DISCARDABLE BEGIN POPUP "&File" BEGIN MENUITEM "&Open", IDMţFILĘ OPEN END END RESOURCE.H (fragmenty) // Microsoft Developer Studio generated include file. // Used by Show0ib8.rc 4ţdefine IDMţFILĘ OPEN 40001 Rysutţc 1&18. Program SHOWDIB8. gig Częœć II: Grafika Obsługa komunikatu WM PAINT w programie SHOWDIBB jest identyczna jak w SHOWDIB7. Oba programy przechowujš uchwyt bitmapy (hBitmap) oraz uchwyt palety logicznej (hPalette) w postaci zmiennych statycznych. Paleta jest wybierana w kontekœcie urzšdzenia, a następnie realizowana, dhzgoœć i szero- koœć bitmapy otrzymywana jest po wywołaniu funkcji GetObject. Program two- rzy również w pamięci kontekst urzšdzenia, wybierajšc w nim mapę, a wyœwie- tlenie obrazu odbywa się przez wywołanie funkcji BitBlt. Wielkš różnicę między wspomnianymi programami stanowi sposób obsługi po- lecenia FileţOpen. Po otrzymaniu wskaŸnika do DIB oraz utworzeniu palety pro- gram SHOWDIB7 musi wybrać paletę w kontekœcie urzšdzenia i zrealizować jš przed wywołaniem funkcji CreateDIBitmap. SHOWDIB8 wywołuje funkcję Cre- ateDIBSection po otrzymaniu wskaŸnika do upakowanej DIB. Wybieranie palety w kontekœcie urzšdzenia nie jest konieczne, gdyż funkcja CreţteDIBSection nie konwertuje DIB do formatu DDB, zależnego od sprzętu. Pierwszego argumentu funkcji CreateDIBSection (tj. uchwytu kontekstu urzšdzenia) używa się tylko wte- dy, gdy znacznik peFlags przyjmuje wartoœć stałej DIB PAL COLOIZS. Po wywołaniu funkcji CreateDIBSection, SHOWDIB8 kopiuje bity pikseli DIB do obszaru pamięci wskazywanej wartoœciš zwracanš przez funkcję CreateDIBSec- tion. Następnie wywołana zostaje funkcja PackedDibCreatePalette. Choć jest ona wygodna w użyciu, program SHOWDIBS mógłby utworzyć paletę na podstawie informacji zwracanej przez funkcję GetDIBCoIorTable. Biblioteka obsługi DIB Dopiero teraz - po zapoznaniu się z obiektami GDI, mapami bitowymi o forma- cie niezależnym od sprzętu (DIB), sekcjš DIB i Palette Managerem Windows - możemy podjšć próbę utworzenia zestawu funkcji pomocnych w pracy z bitma- pami. Funkcje zebrane w plikach PACKEDIB stanowiš jedno z możliwych podejœć do problemu: upakowana DIB jest reprezentowana przez wskaŸnik do obszaru pa- mięci przez niš zajmowanej. Wszystkie niezbędne informacje o bitmapie mogš być pozyskane przez funkcje odczytujšce pola odpowiedniej struktury. W prak- tyce okazuje się, że to podejœcie jest mało efektywne, szczególnie przy pobiera- niu i ustalaniu wartoœci pikseli. Funkcje operujšce na obrazach z definicji muszš mieć dostęp do poszczególnych bitów mapy bitowej, co wišże się z wymaganiem jak najszybszego ich działania. Jednym z możliwych podejœć opartych na języku C++ może być utworzenie kla- sy DIB, w której wskaŸnik do upakowanej DIB byłby jednš ze zmiennych składo- wych tej klasy. Funkcje-metody klasy byłyby odpowiedzialne za szybkš obsługę operacji na bitach mapy, takich jak pozyskiwanie lub ustalanie ich wartoœci. Jed- nak zgodnie z tym, co pisałem w pierwszym rozdziale, opanowanie materiału zawartego w tej publikacji wymaga znajomoœci języka C, a nie C++. Oczywiœcie, prawie wszystko, co można napisać w języku C++, można też napi- sać w języku C. Dobrym tego przykładem sš funkcje Windows, stosujšce uchwy- ty. Co musi "wiedzieć" aplikacja poza tym, że uchwyt jest wartoœciš numerycz- Rozdział 16: Palette Manager gţg nš? Program "wie", że uchwyt zwišzany jest z jakimœ obiektem i że istnieje jakaœ funkcja operujšca na tym obiekcie. System operacyjny wykorzystuje uchwyty, aby się w jakiœ sposób odwołać do wewnętrznej informacji o obiekcie, uchwyt może więc przyjšć postać wskaŸnika do struktury danych. Przypuœćmy, że istnieje zbiór funkcji, które używajš uchwytu o nazwie HDIB. Czym jest HDIB? Można go zdefiniować w pliku nagłówkowym w następujšcy sposób: typedef void * HDIB ; Taka definicja to jak odpowiedŸ "Nie twoja sprawa!" na postawione wczeœniej pytanie "Czym jest HDIB?". HDIB może być wskaŸnikiem do struktury, która zawiera - oprócz wskaŸnika do obszaru pamięci zajmowanego przez upakowanš DIB - kilka innych informacji: typedef struct ( BITMAPINFO * pPackedDib ; int cx, cy, cBitsPerPixel, cBytesPerRow ; BYTE * pBits ; ) DIBSTRUCTURE, * PDIBSTRUCTURE ; Pozostałe pięć pól tej struktury zawiera informacje o własnoœciach DIB, a ponie- waż sš umieszczone jawnie, dostęp do nich jest bardzo szybki. Różne funkcje operujšce na mapie DIB mogłyby sięgać bezpoœrednio do zdefiniowanej w ten sposób struktury, zamiast posługiwać się wskaŸnikiem pPackedDib. Funkcja Dib- GetPixelPointer mogłaby wyglšdać w następujšcy sposób: BYTE * DibGetPixelPointer (HDIB hdib, int x, int y) ( PDIBSTRUCTURE pdib = hdib ; return pdib->pBits + y * pdib->cBytesPerRow + x * pdib->cBitsPerPixel / 8 ; Taka funkcja jest oczywiœcie o wiele szybsza niż funkcja pobierajšca wartoœć pik- sela, napisana na podstawie narzędzi pochodzšcych z pliku PACKEDIB.C. Przedstawiony sposób realizacji funkcji obsługujšcych DIB jest efektywniejszy i szybszy. W dalszych rozważaniach porzucimy temat upakowanej DIB, a biblio- teka funkcji obsługujšcych format bitmapy będzie oparta na sekcji DIB. Struktura DIBSTRUCT Plik DIBHELP C został tak nazwany, ponieważ jego zawartoœć jest pomocna w pra- cy z mapami DIB. Składa się on z ponad tysišca lin kodu i zostanie przedsta- wiony w kilku częœciach. Najpierw jednak przyjrzyjmy się strukturze wykorzy- stywanej przez funkcje modułu DIBHELP. Jest ona zdefiniowana następujšco: typedef struct ( PBYTE * ppRow ; // tablica wskaŸników wierszy int iSignature ; // = "Dib" HBITMAP hBitmap ; // uchwyt zwracany przez funkcję CreateDIBSection - Częœć II: Grafika BYTE * pBits ; // wskaŸnik do bitów mapy bitowej DIBSECTION ds ; !/ struktura DIBSECTION int iRShiftC37 ; // wartoœci przesunięcia w prawo maski kolorów int iLShiftC37 ; // wartoœci przesunięcia w lewo maski kotorów 1 DIBSTRUCT, * PDIBSTRUCT ; Na poczštek pomińxny pierwsze pole struktury. Zostało ono ustawione jako pierw- sze, by ułatwić pracę pewnym makrom, lecz zrozumienie tego będzie łatwiejsze po omówieniu pozostałych pól. W trakcie irucjowania powyższej struktury przez jednš z funkcji pochodzšcych z moduhx DIBHELPC drugiemu jej polu przypisywany jest binarny odpowied- ruk łańcucha tekstowego "Dib". Pole to jest stosowane do weryfikacji wskaŸnika do struktury przez pewne funkcje moduhz DIBHELP. Trzecie pole - hBftmap - jest uchwytem bitmapy zwracanym przez funkcję Cre- ateDIBSection. Przypomnijmy, że uchwyt ten może być stosowany na wiele spo- sobów, podobnie jak uchwyty obiektów GDI, z którymi zetknęliœmy się w roz- dziale 14. Omawiany uchwyt odnosi się do bitmapy w formacie niezależnym od sprzętu do momentu, gdy jest wyprowadzona na urzšdzenie wyjœciowe za po- mocš funkcji BitBlt i StretchBlt. Czwarte pole struktury DIBSTRUCT jest wskaŸnikiem do bitów mapy. Wartoœć ta ustalana jest również przez funkcję CreateDIBSetction. Przypomnijmy, że ob- szarem pamięci zajmowanym przez mapę zarzšdza system operacyjny, aplika- cja zaœ ma do niej jedynie dostęp. Blok pamięci jest zwalniany automatycznie po usunięciu uchwytu mapy bitowej. Pište pole struktury DIBSTRUCT to struktura DIBSECTION. Przypomnijmy, że uchwyt mapy bitowej zwracany przez funkcję CreateDIBSection może być prze- kazany jako argument funkcji GetObject w celu uzyskania informacji o mapie w po- lach struktury DIBSECTION: GetObject (hBitmap, sizeof (DIBSECTION), &ds) ; Jako przypomnierue poniżej przedstawiono strukturę DIBSECTION, zdefiniowanš w pliku WINGDI.H następujšco: typedef struct tagDIBSECTION ( BITMAP dsBm ; BITMAPINFOHEADER dsBmih ; DWORD dsBitfieldsC37 ; // Maski kolorów HANDLE dshSection ; DWORD dsOffset ; ) DIBSECTION, * PDIBSECTION ; Pierwsze pole jest strukturš typu BITMAP, używanš przez funkcję CreateBitma- plndirect w celu utworzenia obiektu bitmapy, a przez funkcję GetObject w celu uzyskania informacji o DDB. Drugie pole to struktura typu BITMAPINFOHE- ADER. Mimo że struktura zawierajšca informacje o mapie bitowej jest przekazy- wana do funkcji CreateDIBSection, opisywana tu struktura DIBSECTION będzie zawsze zawierać strukturę BITMAPINFOHEADER, a nie na przykład BITMAP- COREHEADER. Oznacza to, że funkcje modułu DIBHELPC nie muszš weryfi- kować zgodnoœci DIB w formacie OS/2, próbujšc uzyskać informacje znajdujšce się w strukturze. Rozdziat 16: Paletts Manageţ 821 Przypomnijmy, że gdy pole biCompression struktury BITiNFOHEADER przyjmo- wało wartoœć BI BTTFIELDS dla map bitowych 16- i 32-bitowych, trzy wartoœci maski były zgodne z danymi przechowywanymi w tej strukturze. Sš one odpo- wiedzialne za sposób konwersji wartoœci 16- i 32-bitowych kolorów do formatu RGB. Maski przechowywane sš w trzecim polu struktury DIBSECTION. Ostatrue dwa pola struktury DIBSECTION zwišzane sš z sekcjš DIB, a w szcze- gólnoœci z obiektem przypisanym do pliku. DIBHELP rue korzysta jednak z tej cechy funkcji CreateDIBSection, więc oba pola mogš zostać pominięte. Do omówienia pozostały ostatnie dwa pola struktury DIBSTRUCT, przechowu- jšce wartoœci lewego i prawego przesurugcia, stosowane w maskach kołorów dla DIB 16- i 32 bitowych. Wartoœci te zostały omówione w rozdziale 15. Powróćmy na chwilę do pierwszego pola omawianej struktury DIBSTRUCT. Kiedy tworzona jest DIB, pole przyjmuje wartoœć wskaŸnika zwišzanego z tablicš wskaŸ- ników, z których każdy wskazuje na wiersz pikseli bitmapy. WskaŸniki umożli- wiajš szybki dostęp do bitów poszczególnych pikseli DIB. Sš one tak zorganizo- wane, że pierwszy element tablicy wskaŸników wskazuje na pierwszy wiersz DIB. Ostatru eiement tej tablicy - zwišzany z ostatnim wierszem DIB - będzie zazwy- czaj równy polu pBits struktury DIBSTRUCT. Funkcje informacyjne Moduł DIBHELPC rozpoczyna się od zdefiniowarua struktury DIBSTRUCT. Za- wiera on funkcje umożliwiajšce aplikacji uzyskanie informacji o sekcji DIB. Pierw- sza częœć moduhx DIBHELPC przedstawiona jest na rysunku 16-19. DIBHELP.C tczęœć pierwsza) /* DIBHELP.C - Funkcje pomocnicze sekcji DIB (c) Charles Petzold, 1998 */ iţdefine WINlIER 0x0500 iţinclude 4ţinclude "dibhelp.h" iţdefine HDIB-SIGNATURE (* (int *) "Dib ") typedef struct ( PBYTE * ppRow ; // pole musi być na pierwszej pozycji dla int iSignature ; // celów makro HBITMAP hBitmap ; BYTE * pBits ; DIBSECTION ds ; int iRShift[3] ; int iLShift[3] ; ) DIBSTRUCT, * PDIBSTRUCT ; 822 Częœć II: Grafika (cišg dalszy ze strony 821) /* DibIsValid: Zwraca TRUE, jeœli hdib wskazuje na strukturę DIBSTRUCT */ BOOL DibIsValid (HDIB hdib) i PDIBSTRUCT pdib = hdib ; I if (pdib ţ NULL) return FALSE ; if (IsBadReadPtr (pdib, sizeof (DIBSTRUCT))) return FALSE ; I if (pdib->iSignature != HDIB SIGNATURE) i return FALSE ; return TRUE ; ) /* DibBitmapHandle: Zwraca uchwyt sekcji DIB */ HBITMAP DibBitmapHandle (HDIB hdib) ( if (!DibIsValid (hdib)) return NULL ; return ((PDIBSTRUCT) hdib)->hBitmap ; ) /* DibWidth: Zwraca szerokoœć bitmapy w pikselach */ int DibWidth (HDIB hdib) if (!DibIsValid (hdib)) return 0 ; return ((PDIBSTRUCT) hdib)->ds.dsBm.bmWidth ; /* DibHeight: Zwraca długoţć bitmapy w pikselach */ int DibHeight (HDIB hdib) t if (!DibIsUalid (hdib)) return 0 ; return ((PDIBSTRUCT) hdib)->ds.dsBm.bmHeight ; ) Rozdział 16: Palette Manager 823 /* DibBitCount: Zwraca liczbę bitów na piksel */ int DibBitCount (HDIB hdib) ( if (!DibIsValid (hdib)) return 0 ; return ((PDIBSTRUCT) hdib)->ds.dsBm.bmBitsPixel ; ) /* DibRowLength: Zwraca liczbę bajtów na wiersz pikseli / int DibRowLength (HDIB hdib) ( if (!DibIsValid (hdib)) return 0 ; return 4 * ((DibWidth (hdib) * DibBitCount (hdib) + 31) / 32) ; ) i /* DibNumColors: Zwraca liczbę kolorów w tablicy kolorów */ int DibNumColors (HDIB hdib) t PDIBSTRUCT pdib = hdib ; if (!DibIsValid (hdib)) 1 return 0 ; i. if (pdib->ds.dsBmih.biClrUsed != 0) ( return pdib->ds.dsBmih.biClrUsed ; else if (DibBitCount (hdib) <= 8) ( return 1 Ž DibBitCount (hdib) ; return 0 ; ? /* DibMask: Zwraca jednš z masek kolorów */ DWORD DibMask (HDIB hdib, int i) ( PDIBSTRUCT pdib = hdib ; if (!DibIsValid (hdib) ţţ i < 0 ţţ i > 2) return 0 ; return pdib->ds.dsBitfieldsCi] ; g24 Czţœć 11: Gţafika i (cišg dalszy ze strony 823) i /* DibRShift: Zwraca jednd z wartoœci przesunięcia w prawo */ ; int DibRShift (HDIB hdib. int i) i PDIBSTRUCT pdib = hdib ; if (!DibIsValid (hdib) ţţ i < 0 ţţ i > 2) return 0 ; return pdib->iRShift[i] ; t /* DibLShift: Zwraca jednd z wartoœci przesunięcia w lewo */ j I int DibLShift (HDIB hdib. int i) i PDIBSTRUCT pdib s hdib ; i if (!DibIsValid (hdib) ţţ i < 0 ţţ i > 2) j return 0 ; return pdib->iLShift[i] ; ) /* DibCompression: Zwraca wartoœć pola biCompression */ int DibCompression (HDIB hdib) ( if (!DibIsValid (hdib)) i return 0 ; return ((PDIBSTRUCT) hdib)->ds.dsBmih.biCompression ; I t /* DibIsAddressable: Zwraca wartoœć TRUE. jeœli DIB nie jest ; skompresowana */ BOOL DibIsAddressable (HDIB hdib) i int iCompression ; i if (!DibIsValid (hdib)) return FALSE ; iCompression = DibCompression (hdib) ; if (iCompręssion == BIţR6B ţţ iCompression ţ BI BITFIELDS) return TRUE ; Rozdzial 16: Palette Manageţ 825 return FALSE ; ',f ţ* Poniższe funkcje zwracajd rozmiary różnych elementów sekcji DIB w postaci, w której pojawiłyby się one w upakowanej DIB. Funkcje te pomocne sd w trakcie konwersji sekcji DIB do formatu upakowanej DIB oraz w zapisywaniu plików DIB. * / t:` DWORD DibInfoHeaderSize (HDIB hdib) if (!DibIsUalid (hdib)) return 0 ; return ((PDIBSTRUCT) hdib)->ds.dsBmih.biSize ; j: j; DWORD DibMaskSize (HDIB hdib) ( 1'.. PDIBSTRUCT pdib = hdib ; if (!DibIsValid (hdib)) return 0 ; if (pdib->ds.dsBmih.biCompression = BI BITFIELDS) return 3 * sizeof (DWORD) ; return 0 ; l DWORD DibColorSize (HDIB hdib) return DibNumColors (hdib) * sizeof (RGBOUAD) ; I DWORD DibInfoSize (HDIB hdib) return DibInfoHeaderSize(hdib) + DibMaskSize(hdib) + DibColorSize(hdib) ; ł DWORD DibBitsSize (HDIB hdib) "I PDIBSTRUCT pdib = hdib ; "j' if (!DibIsValid (hdib)) return 0 ; if (pdib->ds.dsBmih.biSizeImage != 0) t return pdib->ds.dsBmih.biSizeImage ; return DibHeight (hdib) * DibRowLength (hdib) ; ) DWORD DibTotalSize (HDIB hdib) return DibInfoSize thdib) + DibBitsSize fhdib) ; I 826 Częœć II: Grafika (cišg dalszy ze strony 825) /* Funkcje te zwracajd wskaŸniki do różnych elementów sekcji DIB. */ BITMAPINFOHEADER * DibInfoHeaderPtr (HDIB hdib) if (!DibIsValid (hdib)) j return NULL ; return & (((PDIBSTRUCT) hdib)->ds.dsBmih) ; ) DWORD * DibMaskPtr (HDIB hdib) t PDIBSTRUCT pdib = hdib ; t if (!DibIsValid (hdib)) return 0 ; return pdib->ds.dsBitfields ; i void * DibBitsPtr (HDIB hdib) if (!DibIsValid (hdib)) return NULL ; return ((PDIBSTRUCT) hdib)->pBits ; I /* DibSetColor: Pobiera element tablicy kolorów DIB */ BOOL DibGetColor (HDIB hdib, int index, RGBOUAD * prgb) PDIBSTRUCT pdib = hdib ; HDC hdcMem ; int iReturn ; if (!DibIsValid (hdib)) return 0 ; , I hdcMem = CreateCompatibleDC (NULL) ; j SelectObject (hdcMem, pdib->hBitmap) ; i iReturn = GetDIBCoIorTable (hdcMem, index, 1, prgb) ; DeleteDC (hdcMem) ; i return iReturn ? TRUE : FALSE ; ) /* DibGetColor: Ustala wartoœć elementu tablicy kolorów DIB ' */ i Rozdział 16: Palette Manager {OOL DibSetColor (HDIB hdib, int index, RGBOUAD * prgb) PDIBSTRUCT pdib = hd)b HDC hdcMem ; int iReturn : if (!DibIsValid (hdib)) return 0 : hdcMem = CreateCompatibleDC (NULL) ; SelectObject (hdcMem, pdib->hBitmap) ; iReturn = SetDIBCoIorTable (hdcMem, index, 1, prgb) ; DeleteDC (hdcMem) ; return iReturn ? TRUE : FALSE ; ) RySUnek 1„19, Pierwsza częœć moduhz DIBHELP.C. i. Funkcje zawarte w powyżej przedstawionej częœci moduhx DIBHELP.C nie wy- magajš objaœnierua. Funkcja Diblsvalid troszczy się o bezpieczeństwo całego sys- temu. Inne funkcje wywołujš Diblsvalid przed odwołaniem się do informacji umieszczonej w strukturze DIBSTRUCT. Wszystkie za pierwszy - i zwykle jedy- ' ny - argument przyjmujš parametr HDIB, który (jak się wkrótce przekonasz) jest zdefiruowany w pliku nagłówkowym DIBHELPH jako wskaŸnik typu void. Funk- cje mogš przekazać ten parametr do PDIBSTRUCT, aby uzyskać dostęp do pól struktury. Zauważ, że funkcja DibIsNotAddressable zwraca wartoœć typu BOOL. Może rów- nież wywoływać funkcja DibIsNotCompressed. Zwracana wartoœć wskazu)e, czy j poszczególne piksele mapy DIB mogš być adresowane. Funkcje, poczšwszy od DibInfoHeaderSize, pozyskujš rozmiary różnych elemen- tów sekcji DIB na wypadek gdyby pojawiły się one w upakowanej DIB. Jak się wkrótce przekonasz, funkcje te sš pomocne w konwersji sekcji DIB do formatu upakowanej DIB oraz zapisywaniu plików DIB. Kolejne funkcje odpowiadajš za otrzymywanie wskaŸników do różnych elementów DIB. ; Choć moduł DIBHELPC zawiera funkcję o nazwie DibInfoHeaderPtr, która uzy- skuje wskaŸnik do struktury BTTMAPINFOHEADER, nie istnieje funkcja uzysku- jšca wskaŸnik do BTTMAPINFO - struktury informacyjnej, po której następuje tablica kolorów DIB. Dzieje się tak, ponieważ aplikacje obshxgujšce sekcje DIB nie majš bezpoœredruego dostępu do struktury tego typu. Podczas gdy struktura BITMAPINFOHEADER i maski kolorów dostępne sš z poziomu struktury DIB- SECTION, a wskaŸnik do bitów pikseli zwracany jest przez funkcję CreateDIB- Section, tablica kolorów DIB dostępna jest jedynie poœredruo, przez wywołanie funkcji GetDIBCoIorTable i SetDIBCoIorTable. Funkcje te sš zagnieżdżone w Dib- GetColor oraz DibSetColor modułu DIBHELPC. Odczytywanie i zapisywanie pikseli Jednš z niewštpliwych zalet przechowywania bitmap w postaci upakowanych DIB lub sekcji DIB jest możliwoœć dokonywania bezpoœredruch działań na bitach g2g Częœć Ii: Grafika f Ro; poszczególnych pikseli. Druga częœć modułu DIBHELP.C, przedstawiona na ry- sunku 16-20, zawiera funkcje przeznaczone do tego celu. DIBHELP.C (częœć druga) /* DibPixelPtr: Zwraca wskafnik do piksela o wspólrzędnych (x, y) */ BYTE * DibPixelPtr (HDIB hdib, int x, int y) ( if (!DibIsAddressable (hdib)) return NULL ; if (x < 0 ţţ x >= DibWidth (hdib) ţţ y < 0 ţţ y >= DibHeight (hdib)) return NULL ; return (((PDIBSTRUCT) hdib)->ppRow)Cy7 + (x * DibBitCount (hdib) Ż 3) ; ) /* DibGetPixel: Pobiera wartoœć piksela o współrzędnych(x, y) 1 */ /* DWORD DibGetPixel (HDIB hdib, int x, int y) ( PBYTE pPixel ; if (!(pPixel = DibPixelPtr (hdib, x, y))) BOC ( return 0 ; switch (DibBitCount (hdib)) case 1: return 0x01 & (* pPixel Ż (7 - (x & 7))) ; case 4: return OxOF & (* pPixel Ż (x & 1 ? 0 : 4)) ; case 8: return * pPixel ; case 16: return * (WORD *) pPixel ; case 24: return 0xOOFFFFFF & * (DWORD *) pPixel ; case 32: return * (DWOáD *) pPixel ; ) return 0 ; /* DibSetPixel: Ustala wartoœć piksela o wspólrzędnych (x, y) */ BOOL DibSetPixel (HDIB hdib,.int x, int y, DWORD dwPixel) PBYTE pPixel ; if (!(pPixel = DibPixelPtr (hdib, x, y))) return FALSE ; switch (DibBitCount (hdib)) ( Rozdzial 16: Palette Manager 829 case 1: * pPixel &= ~(1 Ž (7 - (x & 7))) : * pPixel ţ= dwPixel Ž (7 - (x & 7)) : break ; case 4: * pPixel &= OxOF Ž (x & 1 ? 4 : 0) : * pPixel ţ= dwPixel Ž (x & 1 ? 0 : 4) ; break ; case 8: * pPixel = (BYTE) dwPixel ; break ; case 16: * (WORD *) pPixel = (WORD) dwPixel ; break ; case 24: * (RGBTRIPLE *) pPixel = * (RGBTRIPLE *) &dwPixel : break ; I i case 32: * (DWORD *) pPixel = dwPi.xel : break ; default: return FALSE ; 1 return TRUE ; /* DibGetPixelColor: Pobiera kolor piksela o współrzędnych (x, y) */ BOOL DibGetPixelColor (HDIB hdib, int x, int y, RGBOUAD * prgb) DWORD dwPixel ; int iBitCount : PDIBSTRUCT pdib = hdib ; // Pobierz liczbę bitów na piksel if (0 = (iBitCount = DibBitCount (hdib))) return FALSE ; // Pobierz wartoœć piksela dwPixel = DibGetPixel (hdib, x, y) : i // Jeœli liczba bitów na piksel wynosi 8 lub mniej, indeksuj tablicę kolorów if (iBitCount <= 8) ` return DibGetColor (hdib, (int) dwPixel, prgb) ; // Jeœli liczba bitów na piksel wynosi 24, po prostu użyj wartoœci piksela else if (iBitCount == 24) f * (RGBTRIPLE *) prgb = * (RGBTRIPLE *) & dwPixel ; prgb->rgbReserved = 0 ; ) // Jeœli liczba bitów na piksel wynosi 32 i pole biCompression przyjmuje // wartoœć BIţRGB. zastosuj wartoœć piksela f. 830 Częœć IIţ Grafika (cigg dalszy ze strony 831) i else if (iBitCount = 32 && pdib->ds.dsBmih.biCompression = BI RGB) * prgb = * (RGBOUAD *) & dwPixel ; // W przeciwnym przypadku zastosuj maski i przesunięcia bitowe // (aby uzyskać lepszš wydajnoœć nie używaj funkcji DibMask i I // DibShift) else I prgb->rgbRed = (BYTE) (((pdib->ds.dsBitfields[0] & dwPixel) Ż pdib->iRShift[0]) Ž pdib->iLShift[0]) ; prgb->rgbGreen = (BYTE) (((pdib->ds.dsBitfields[1J & dwPixel) Ż pdib->iRShift[lJ) Ž pdib->iLShift[1]) ; ; r prgb->rgbBlue = (BYTE) (((pdib->ds.dsBitfields[2] & dwPixel) Ż pdib->iRShift[2]) Ž pdib->iLShiftC2]) ; ) return TRUE ; F /* DibSetPixelColor: Ustala kolor piksela o współrzędnych(x, y) */ i BOOL DibSetPixelColor (HDIB hdib, int x, int y, RGBOUAD * prgb) ' DWORD dwPixel ; int iBitCount ; PDIBSTRUCT pdib = hdib ; // Nie stosuj tej funkcji dla DIB majšcych tablice kolorów iBitCount = DibBitCount (hdib) ; if (iBitCount <= 8) return FALSE ; i // Pozostała częœć to odwrotnoœć funkcji DibGetPixelColor I else if (iBitCount = 24) ( * (RGBTRIPLE *) & dwPixel = * (RGBTRIPLE *) prgb ; dwPixel &= 0xOOFFFFFF ; } else if (iBitCount = 32 && pdib->ds.dsBmih.biCompression = BIţRGB) ( * (RGBOUAD *) & dwPixel = * prgb ; ) else f Rozdziat 16: Palette Manager 831 dwPixel = (((DWORD) prgb->rgbRed Ż pdib->iLShiftC07) Ž pdib->iRShiftC07) ; dwPixel ţ= (((DWORD) prgb->rgbGreen Ż pdib->iLShiftCl7) Ž pdib->iRShiftCl7) ; dwPixel ţ= (((DWORD) prgb->rgbBlue Ż pdib->iLShiftC27) Ž pdib->iRShiftC27) ; DibSetPixel (hdib, x, y, dwPixel) ; return TRUE ; Rysunek 16-20. Druga częœć moduhz DIBHELPC. Ta częœć DIBHELPC rozpoczyna się funkcjš DibPixeIPtr, która uzyskuje wskaŸ- nik do bajtu, w którym przechowywane sš w całoœci (lub w częœci) poszczególne bity piksela. Przypomnij sobie, że pole ppRow struktury DIBSTRUCT wskazuje na tablicę wskaŸników, które to wskazujš kolejne wiersze DIB. Tak więc: ((PDIBSTRUCT) hdib)->ppRow)C07 jest wskazaniem na lewy skrajny piksel pierwszego wiersza DIB, a (((PDIBSTRUCT) hdib)->ppRow)Cy7 + (x * DibBitCount (hdib) Ż 3) wskazuje na piksel o współrzgdnych (x,y). Zauważ, że funkcja zwraca wartoœć NULL, jeœli DIB nie może być zaadresowana (tzn. kiedy jest upakowana) lub jeœli parametry x i y sš ujemne lub wskazujš na obszar poza grarucami DIB. Weryfi- kacja tego typu spowalnia działanie funkcji (oraz każdej funkcji, która zależy od DibPixelPtr), jednak wkrótce poznasz szybsze funkcje. Funkcje DibGetPixel i DibSetPixel, znajdujšce się w dalszej częœci modułu DIBHELP, korzystajš z DibPixelPtr. W wypadku map 8-, 16- i 32 bitowych funkcje te muszš je- dynie okreœlić odpowiedni rozmiar danej i uzyskać wartoœć piksela. Dla map 1- i 4- bitowych niezbędne jest zastosowanie maskowania i przesunięć bitowych. Funkcja DibGetColor otrzymuje kolor piksela w postaci struktury RGBQUAD. W wypadku bitmap o strukturze 1-, 4- lub 8-bitowej kolor piksela pozyskiwany jest bezpoœredruo z tablicy kolorów DIB. Aby uzyskać wartoœć koloru RGB piksela mapy 16-, 24- i 32- bitowej należy zastosować maskowanie i przesunięcia bitowe. Funkcja DibSetPixel działa odwrotnie, tzn. pozwala ustalić wartoœć piksela zadanš w strukturze RGBQU- AD. Funkcję zdefiniowano tylko w wersji dla map 16-, 24- i 32-bitowych. Tworzenie i konwersja sekcji DIB Trzecia, ostatnia częœć modułu DIBHELP przedstawiona na rysunku 1(r21, demonstruje sposób tworzenia sekcji DIB oraz ich konwersji do i z formatu upakowanej DIB. DIBHELP.C (częœć trzecia) /* Oblicza wartoœci przesunięcia na podstawie masek kolorów dla potrzeb funkcji DibCreateFromInfo. */ 832 Czgœć il: Grafika ' i (cigg datszy ze strony 831) ! static int MaskToRShift (DWORD dwMask) f int iShift ; if (dwMask ţ 0) return 0 ; for (iShift = 0 ; !(dwMask & 1) ; iShift++) dwMask Żm 1 ; return iShift ; static int MaskToLShift (DWORD dwMask) ( int iShift ; if (dwMask ţ 0) return 0 ; while (!(dwMask & 1)) dwMask Ż= 1 ; for (iShift = 0 ; dwMask & 1 ; iShift++) dwMask Ż= i ; return 8 - iShift ; 1 /* DibCreateFromInfo: Wszystkie funkcje tworzace DIB wywołuja poniższd funkcję. Jest ona odpowiedzialna za wywołanie CreateDIBSection, rezerwujšc pamięć dla struktury DIBSTRUCT i inicjujšc wskazanie na wiersz ' pikseli. */ HDIB DibCreateFromInfo (BITMAPINFO * pbmi) f BYTE * pBits ; DIBSTRUCT * pdib ; HBITMAP hBitmap ; int i, iRowLength, cy, y ; hBitmap = CreateDIBSection (NULL, pbmi, DIBţRGBţCOLORS, &pBits, NULL, 0) ; if (hBitmap ţ NULL) return NULL ; if (NULL ţ (pdib = malloc (sizeof (DIBSTRUCT)))) Delete0bject (hBitmap) ; return NULL ; 1 pdib->iSignature = HDIBţSIGNATURE ; Rozdział 16; Paletie Manager 833 pdib->hBitmap = hBitmap ; pdib->pBits = pBits ; GetObject (hBitmap, sizeof (DIBSECTION), &pdib->ds) : ? // Zauważ, że teraz możemy wykorzystać zdefiniowane powyżej // funkcje informacyjne. // Jeœli typem kompresji jest BI BITFIELDS, wylicz przesunięcia na !/ podstawie masek if (DibCompression (pdib) ţ BI BITFIELDS) t for (i = 0 ; i < 3 ; i++) ( pdib->iLShift[i] = MaskToLShift (pdib->ds.dsBitfields[i)) ; pdib->iRShift[i) = MaskToRShift (pdib->ds.dsBitfields[i7) : 1 // Jeœli typem kompresji jest BI RGB. a liczba bitów na piksel !/ wynosi 16 lub 32, ustaw odpowiednie póla bitowe i maski else if (DibCompression (pdib) ţ BIţRGB) ( if (DibBitCount (pdib) - 16) ( pdib->ds.dsBitfieldsC07 = 0x00007C00 : pdib->ds.dsBitfieldsCl) = 0x000003E0 : pdib->ds.dsBitfields[27 = 0x0000001F ; pdib->iRShift[O7 = 10 ; pdib->iRShiftCl7 = 5 ; pdib->iRShift[27 = 0 ; pdib->iLShiftCO) = 3 ; pdib->iLShift[17 = 3 ; pdib->iLShift[27 = 3 : ) else if (DibBitCount (pdib) == 24 ţ) DibBitCount (pdib) == 32) ( = 0x00FF0000 ; pdib->ds.dsBitfields[0) pdib->ds.dsBitfields[i7 = 0x0000FF00 ; pdib->ds.dsBitfields[2) = 0x000000FF ; pdib->iRShiftCO] = 16 ; pdib->iRShiftCl) = 8 ; pdib->iRShiftC27 = 0 ; pdib->iLShiftC07 = 0 : pdib->iLShiftCl) = 0 ; pdib->iLShiftC2) = 0 ; ) ) // Zarezerwuj pamięć na tablicę wskaŸników do każdego wiersza DIB cy = DibHeight (pdib) ; Częœć II: Grafika 4 (cišg dalszy ze strony 833) if (NULL = (pdib->ppRow = malloc (cy * sizeof (BYTE *)))) ! free (pdib) ; Delete0bject (hBitmap) ; j return NULL ; i // Inicjuj je iRowLength = DibRowLength (pdib) ; if (pbmi->bmiNeader.biHeight > 0) // od dołu do góry ( for (y = 0 ; y < cy ; y++) pdib->ppRow[y] = pBits + (cy - y - 1) * iRowLength ; else // od góry do dolu ( for (y = 0 ; y < cy ; y++) pdib->ppRow[y] = pBits + y * iRowLength ; 1 return pdib ; ) /* DibOelete: Zwalnia pamięć dla sekcji DIB */ BOOL DibDelete (HDIB hdib) ( DIBSTRUCT * pdib = hdib ; if (!DibIsValid (hdib)) return FALSE ; free (pdib->ppRow) ; Delete0bject (pdib->hBitmap) ; free (pdib) ; return TRUE ; ) /* DibCreate: Tworzy HDIB na podstawie argumentów podanych wprost */ HDIB DibCreate (int cx, int cy, int cBits, int cColors) ( BITMAPINFO * pbmi ; DWORD dwInfoSize ; HDIB hDib ; int cEntries ; if (cx <= 0 ţţ cy <= 0 ţţ ((cBits != i) && (cBits != 4) && (cBits != 8) && (cBits != 16) && (cBits != 24) && (cBits != 32))) ( Rozdział 16: Palette Manager 835 return NULL ; if (cColors != 0) cEntries = cColors : else if (cBits <= S) cEntries = 1 Ž cBits ; dwInfoSize = sizeof (BITMAPINFOHEADER) + (cEntries - 1) * sizeof (RGBQUAD); ` if (NULL = (pbmi = malloc (dwInfoSize))) return NULL ; ZeroMemory (pbmi, dwInfoSize) : I,: . pbmi->bmiHeader.biSize = sizeof (BITMAPINFOHEADER) : pbmi->bmiHeader.biWidth = cx ; pbmi->bmiHeader.biHeight = cy : pbmi->bmiHeader.biPlanes = 1 ; pbmi->bmiHeader.biBitCount = cBits : pbmi->bmiHeader.biCompression = BIţRGB ; pbmi->bmiHeader.biSizeImage = 0 ; pbmi->bmiHeader.biXPelsPerMeter = 0 : pbmi->bmiHeader.biYPelsPerMeter = 0 : pbmi->bmiHeader.biClrUsed = cColors ; pbmi->bmiHeader.biClrImportant = 0 ; h0ib = DibCreateFromInfo (pbmi) : free (pbmi) ; return hDib : /* ţ. DibCopyToInfo: Buduje strukturę BITMAPINFO. Używana przez funkcje DibOopy i DibCopyToDdb '= */ static BITMAPINFO * DibCopyToInfo (HDIB hdib) i BITMAPINFO * pbmi : int i, iNumColors ; . RGBOUAD * prgb : if (!DibIsllalid (hdib)) return NULL ; // Zarezerwuj pamięć if (NULL == (pbmi = malloc (DibInfoSize (hdib)))) return NULL ; // Skopiuj nagłówek informacyjny CopyMemory (pbmi, DibInfoHeaderPtr (hdib), sizeof (BITMAPINFOHEADER)); t Częœć II: Grafika (cišg dalszy ze strony 835) // Skopiuj maski koloru prgb = (RGBOUAD *) ((BYTE *) pbmi + sizeof (BITMAPINFOHEADER)) ; if (DibMaskSize (hdib)) ( CopyMemory (prgb, DibMaskPtr (hdib), 3 * sizeof (DWORD)) ; prgb = (RGBOUAD *) ((BYTE *) prgb + 3 * sizeof (DWORD)) ; ) // Skopiuj tablicę kolorów iNumColors = DibNumColors (hdib) ; for (i = 0 ; i < iNumColors ; i++) DibGetColor (hdib. i, prgb + i) ; return pbmi ; ) /* DibCopy: Tworzy nowš sekcję DIB na podstawie istniejšcej, zamieniajšc miejscami dlugoœć i szerokoœć. */ HDIB DibCopy (HDIB hdibSrc, BOOL fRotate) t BITMAPINFO * pbmi ; BYTE * pBitsSrc, * pBitsDst ; HDIB hdibDst ; if (!DibIsValid (hdibSrc)) return NULL ; if (NULL = (pbmi = DibCopyToInfo (hdibSrc))) return NULL ; if (fRotate) f pbmi->bmiHeader.biWidth = DibHeight (hdibSrc) ; pbmi->bmiHeader.biHeight = DibWidth (hdibSrc) ; ) hdibDst = DibCreateFromInfo (pbmi) ; free (pbmi) if (hdibOst = NULL) return NULL ; // Skopiuj bity if (!fRotate) f pBitsSrc = DibBitsPtr (hdibSrc) ; pBitsDst = DibBitsPtr (hdibDst) ; Rozdział 16: Palette Manager CopyMemory (pBitsDst, pBitsSrc, DibBitsSize (hdibSrc)) ; ) return hdibDst ; ) /* GibCopyToPackedDib jest stosowana do zapi'sywania DIB i przesyłania ich do Schowka. W drugim przypadku. drugi argument powinien przyjmować wartoœć TRUE tak, aby można było zarezerwować pamięć za pomocd znacznika GMEM SHARE. */ BITMAPINFO * DibCopyToPackedDib (HDIB hdib, BOOL fUseGlobal) ( BITMAPINFO * pPackedDib ; BYTE * pBits ; DWORD dwDibSize ; HDC hdcMem ; HGLOBAL hGlobal ; int iNumColors ; PDIBSTRUCT pdib = hdib ; RGBOUAD * prgb ; if (!DibIsValid (hdib)) return NULL ; // Zarezerwuj pamięć na upakowand DIB dwDibSize = DibTotalSize (hdib) ; if (fUseGlobal) ( hGlobal = GlobalAlloc (GHND ţ GMEM SHARE, dwDibSize) ; pPackedDib = GlobalLock (hGlobal) ; 1 else ( pPackedDib = malloc (dwDibSize) ; if (pPackedDib = NULL) return NULL ; // Skopiuj nagłówek informacyjny CopyMemory (pPackedDib, &pdib->ds.dsBmih, sizeof (BITMAPINFOHEADER)) ; prgb = (RGBOUAD *) ((BYTE *) pPackedDib + sizeof (BITMAPINFOHEADER)) ; // Skopiuj maski koloru if (pdib->ds.dsBmih.biCompression --- BI BITFIELDS) ( CopyMemory (prgb, pdib->ds.dsBitfields, 3 * sizeof (DWORD)) ; prgb = (RGBOUAD *) ((BYTE *) prgb + 3 * sizeof (DWORD)) ; ) // Skopiuj tablicę kolorów 838 Częœć II: Grafika (cišg dalszy ze strony 837) if (iNumColors = DibNumColors (hdib)) ( hdcMem = CreateCompatibleDC (NULL) ; SelectObject (hdcMem, pdib->hBitmap) ; GetDIBCoIorTable (hdcMem, 0, iNumColors, prgb) ; DeleteDC (hdcMem) ; ) pBits = (BYTE *) (prgb + iNumColors) ; // Skopiuj bity CopyMemory (pBits, pdib->pBits, DibBitsSize (pdib)) ; // Jeżeli ostatni argument ma wartoœć TRUE, odblokuj globalny // obszar pamięci i przypisz go wskaŸnikowi przed wykonaniem // polecenia return if (fUseGlobal) ( GlobalUnlock (hGlobal) ; pPackedDib = (BITMAPINFO *) hGlobal ; ) return pPackedDib ; 1 /* DibCopyFromPackedDib jest stosowana do wklejania DIB pochodzdcej ze Schowka. */ HDIB DibCopyFromPackedDib (BITMAPINFO * pPacked0ib) ( BYTE * pBits ; DWORD dwInfoSize, dwMaskSize, dwColorSize ; int iBitCount ; PDIBSTRUCT pdib ; // Pobierz rozmiar nagłówka informacyjnego i przeprowadŸ weryfikację dwInfoSize = pPackedDib->bmiHeader.biSize ; if (dwInfoSize != sizeof (BITMAPCOREHEADER) && dwInfoSize != sizeof (BITMAPINFOHEADER) && dwInfoSize != sizeof (BITMAPV4HEADER) && dwInfoSize != sizeof (BITMAPV5HEADER)) ( return NULL ; ) // Pobierz rozmiary masek koloru if (dwInfoSize == sizeof (BITMAPINFOHEADER) && pPackedDib->bmiHeader.biCompression == BI BITFIELDS) dwMaskSize = 3 * sizeof (DWORD) ; ) else Rozdział 16: Palette Manager 839 ' i dwMaskSize = 0 ; // Pobierz rozmiar tablicy kolorów if (dwInfoSize --- sizeof (BITMAPCOREHEADER)) ;,i iBitCount = ((BITMAPCOREHEADER *) pPackedDib)->bcBitCount ; if (iBitCount <= 8) ( dwColorSize = (1 Ž iBitCount) * sizeof (RGBTRIPLE) ; ) else dwColorSize = 0 ; 1 else // DIB niezgodne z formatem OS/2 { if (pPackedDib->bmiHeader.biClrUsed > 0) ( dwColorSize = pPackedDib->bmiHeader.biClrUsed * sizeof (RGBOUAD); 1 else if (pPackedDib->bmiHeader.biBitCount <= 8) i dwColorSize = (1 Ž pPackedDib->bmiHeader.biBitCount) * sizeof (RGBQUAD) ; else ( dwColorSize = 0 ; l J // Pobierz wskaŸnik do bitów upakowanej DIB pBits = (BYTE *) pPackedDib + dwInfoSize + dwMaskSize + dwColorSize ; // Utwórz HDIB na podstawie upakowanej DIB pdib = DibCreateFromInfo (pPackedDib) ; // Skopiuj bity piksela CopyMemory (pdib->pBits, pBits, DibBitsSize (pdib)) ; return pdib : ) /* DibFileLoad: Tworzy sekcję DIB na podstawie pliku DIB */ HDIB DibFileLoad (const TCHAR * szFileName) i BITMAPFILEHEADER bmfh ; BITMAPINFO * pbmi ; BOOL bSuccess ; DWORD dwInfoSize, dwBitsSize, dwBytesRead ; HANDLE hFile ; t;zgœć 11: Grafika (cišg dalszy ze strony 839) HDIB hDib ; // Otwórz plik w trybie do odczytu, zapis zabroniony hFile = CreateFite (szFileName, GENERIC_READ, FILE_SHARE_READ, NULL, OPENţEXISTING, FILEţFLAţ SEOUENTIAL SCAN, NULL) ; if (hFile ţ INVALID HANDLĘ VALUE) return NULL ; // Wczytaj BITMAPFILEHEADER bSuccess = ReadFile (hFile, &bmfh, sizeof (BITMAPFILEHEADER), &dwBytesRead, NULL) ; if (!bSuccess ţţ (dwBytesRead != sizeof (BITMAPFILEHEADER)) (bmfh.bfType != * (WORD *) "BM")) t CloseHandle (hFile) ; return NULL ; ) // Zarezerwuj pamięć na strukturę informacyjnš, a następnie jš wczytaj dwInfoSize = bmfh.bfOffBits - sizeof (BITMAPFILEHEADER) ; if (NULL ţ (pbmi = malloc (dwInfoSize))) ( CloseHandle (hFile) ; return NULL ; ) bSuccess = ReadFile (hFile, pbmi, dwInfoSize, &dwBytesRead, NULL) ; if (!bSuccess ţţ (dwBytesRead != dwInfoSize)) ( CloseHandle (hFile) ; free (pbmi) ; return NULL ; ) // Utwórz DIB hDib = DibCreateFromInfo (pbmi) ; free (pbmi) ; if (hDib ţ NULL) ( CloseHandle (hFile) ; return NULL ; // Wczytaj bity dwBitsSize = bmfh.bfSize - bmfh.bfOffBits ; bSuccess = ReadFile (hFile, ((PDIBSTRUCT) hDib)->pBits, dwBitsSize, &dwBytesRead, NULL) ; CloseHandle (hFile) ; Rozdziat 16: Palette kţsnager if (!bSuccess ţţ (dwBytesRead != dwBitsSize)) ( DibOelete (hDib) : return NULL : 1 return hDib ; /* DibFileSave: Zapisuje sekcję DIB w pliku */ BOOL DibFileSave (HDIB hdib, const TCHAR * szFileName) ( BITMAPFILEHEADER bmfh ; BITMAPINFO * pbmi ; BOOL bSuccess ; DWORD dwTotalSize, dwBytesWritten ; HANDLE hFile : hFile = CreateFile (szFileName, GENERIC WRITE, 0, NULL, CREATĘ ALWAYS, FILĘ ATTRIBUTĘ NORMAL, NULL) ; if (hFile == INVALID HANDLĘ VALUE) return FALSE ; dwTotalSize = DibTotalSize (hdib) : bmfh.bfType - * (WORD *) "BM" ; bmfh.bfSize ţ sizeof (BITMAPFILEHEADER) + dwTotalSize ; bmfh.bfReservedi = 0 : bmfh.bfReserved2 = 0 ; bmfh.bfOffBits = bmfh.bfSize - DibBitsSize (hdib) : // Zapisz dane w BITMAPFILEHEADER bSuccess = WriteFile (hFile. &bmfh, sizeof (BITMAPFILEHEADER). &dwBytesWritten, NULL) ; if (!bSuccess ţţ (dwBytesWritten != sizeof (BITMAPFILEHEADER))) ( CloseHandle (hFile) ; DeleteFile (szFileName) ; return FALSE ; 1 // Pobierz DIB w formacie upakowanym if (NULL ţ (pbmi = DibCopyToPacked0ib (hdib, FALSE))) ( CloseHandle (hFile) : DeleteFile (szFileName) : return FALSE ; ) /! Zapisz upakowand DIB bSuccess = WriteFile (hFile, pbmi, dwTotalSize. &dwBytesWritten, NULL) ; CloseHandle (hFile) : free (pbmi) : Częœć II: Grafika (cišg dalszy ze strony 841) if (!bSuccess ţţ (dwBytesWritten != dwTotalSize)) i DeleteFile (szFileName) ; return FALSE ; ) return TRUE /* DibCopyToDdb: Podnosi wydajnoœć wyœwietlania bitmap */ HBITMAP DibCopyToDdb (HDIB hdib, HWND hwnd, HPALETTE hPalette) ( BITMAPINFO * pbmi ; HBITMAP hBitmap ; HDC hdc ; if (!DibIsValid (hdib)) return NULL ; if (NULL = (pbmi = DibCopyToInfo (hdib))) return NULL ; hdc = GetDC (hwnd) ; if (hPalette) f SelectPalette (hdc, hPalette, FALSE) ; RealizePalette (hdc) ; hBitmap = CreateDIBitmap (hdc, DibInfoHeaderPtr (hdib), CBM_INIT, DibBitsPtr (hdib), pbmi, DIBţRGB COLORS) ; ReleaseDC (hwnd, hdc) ; free (pbmi) ; return hBitmap ; Rysunek 16-21. Trzecia i ostatnia częœć moduhz DIBHELPC. Przedstawiona powyżej ostatnia częœć modułu DIBHELP.C rozpoczyna się dwo- ma krótkimi funkcjami, które wyznaczajš wartoœci przesunięcia w lewo i w pra- wo na podstawie masek koloru 16- i 32-bitowych map DIB. Funkcje te zostały opisane w rozdziale 15 w podrozdziale "Maskowanie kolorów". W module DIBHELP tylko funkcja DibCreateFromlnfo zawiera wywołanie funkcji CreateDibSection i rezerwuje obszar pamięci dla struktury DIBSTRUCT. Jedynym argumentem wywołania funkcji DibCreateFromlnfo jest wskaŸnik do struktury BITMAPINFO. Tablica kolorów zwišzana z tš strukturš musi istrueć, lecz rue musi być wypełniona wartoœciami. Po wywołaniu CreateDibSection funkcja DibCreate- Fromlnfo inicjuje wszystkie pola struktury DIBSTRUCT. Zauważ, że w czasie usta- Rozdział 16: Palette Manager lania wartoœci pola pplZow (wskaŸnik do tablicy adresów wierszy DIB), postępo- wanie jest odmienne dla DIB o ułożeruu normalnym i odwrotnym. Pierwszym elementem ppRow jest zawsze wskazanie na pierwszy wiersz DIB. DibDelete usuwa bitmapę utworzonš przez funkcję DibCreateFromlnfo oraz zwal- nia pamięć rezerwowanš przez tę funkcję. Wywołarue funkcji DibCreate z poziomu aplikacji jest bardziej wygodne niż wy- wołarue funkcji DibCreateFromlnfo. Pierwsze trzy argumenty funkcji to szerokoœć i wysokoœć DIB (mierzona w pikselach) oraz liczba bitów przypadajšcych na pik- sel. Ostatni argument może przyjmować wartoœć 0 w wypadku tablicy kolorów o domyœlnych rozmiarach lub różnš od zera dla mniejszych tablic kolorów. Funkcja DibCopy tworzy nowš sekcję DIB na podstawie już istniejšcej. Następnie wykorzystuje funkcję DibCreatelnfo, która rezerwuje pamięć na strukturę BITMA- PINFO i wypełrua jš danymi. Od wartoœci argumentu typu BOOL funkcji DibCo- py zależy, czy szerokoœć i wysokoœć majš być zamienione miejscami w trakcie tworzenia nowej sekcji DIB. Sposób wykorzystania tej własnoœci poznasz póŸ- niej. Funkcje DibCopyToPackedDib i DibCopyFromPackedDib sš stosowane w kopiowa- niu DIB za poœrednictwem Schowka. Funkcja DibFileLoad tworzy sekcję DIB na podstawie pliku zawierajšcego DIB; DibFileSave zapisuje DIB w pliku. Wreszcie funkcja DibCopyToDdb tworzy obiekt bitmapy GDI na podstawie DIB. Zwróć uwagę, że jako argumentów funkcja wymaga uchwytów bieżšcej palety oraz okna programu. Uchwyt okna programu jest wykorzystywany do uzyska- nia kontekstu urzšdzenia, w którym wybierana i realizowana jest paleta. Stano- wi to warunek wywołania funkcji CreateDIBitmap, której działanie przedstawiali- œmy w trakcie omawiania programu SHOWDIB7. Plik nagłówkowy DIBHELP i makra Plik nagłówkowy DIBHELP.H przedstawiono na rysunku 16-22: DIBHELP.H /* DIBHELP.H plik naglówkowy do DIBHELP.C */ typedef void * HDIB ; // Funkcje modułu DIBHELP.C BOOL DibIsValid (HDIB hdib) ; HBITMAP DibBitmapHandle (HDIB hdib) ; int DibWidth (HDIB hdib) ; int DibHeight (HDIB hdib) ; int DibBitCount (HDIB hdib) ; int DibRowLength (HDIB hdib) ; int DibNumColors (HDIB hdib) ; DWORD DibMask (HDIB hdib, int i) ; int DibRShift (HDIB hdib, int i) ; int DibLShift (HDIB hdib, int i) ; T Czgœć It: Grańka (cišg dalszy ze strony 843) int DibCompression (HDIB hdib) ; BOOL DibIsAddressable (HDIB hdib) ; DWORD DibInfoHeaderSize (HDIB hdib) ; DWORD DibMaskSize (HDIB hdib) ; DWORD DibColorSize (HDIB hdib) ; DWORD DibInfoSize (HDIB hdib) ; DWORD DibBitsSize (HDIB hdib) ; DWORD DibTotalSize (HDIB hdib) ; BITMAPINFOHEADER * DibInfoHeaderPtr (HDIB hdib) ; DWORD * DibMaskPtr (HDIB hdib) ; void * DibBitsPtr (HDIB hdib) ; BOOL DibGetColor (HDIB hdib, int index. RGBOUAD * prgb) ; BOOL DibSetColor (HDIB hdib, int index, RGBQUAD * prgb) ; BYTE * DibPixelPtr (HDIB hdib, int x, int y) ; DWORD DibGetPixel (HDIB hdib, int x, int y) ; BOOL DibSetPixel (HDIB hdib, int x, int y. DWORD dwPixel) ; BOOL DibGetPixelColor (HDIB hdib, int x, int y. RGBOUAD * prgb) ; BOOL DibSetPixelColor (HDIB hdib, int x. int y. RGBOUAD * prgbJ ; HDIB DibCreateFromInfo (BITMAPINFO * pbmi) ; BOOL DibDelete (HDIB hdib) ; HDIB DibCreate (int cx, int cy, int cBits, int cColors) ; HDIB DibCopy (HDIB hdibSrc, BOOL fRotate) ; BITMAPINFO * DibOopyToPackedDib (HDIB hdib, BOOL fUseGlobal) ; HDIB DibCopyFromPackedDib (BITMAPINFO * pPackedDib) ; HDIB DibFileLoad (const TCHAR * szFileName) ; BOOL DibFileSave (HDIB hdib, const TCHAR * szFileName) ; HBITMAP DibCopyToDdb (HDIB hdib, HWND hwnd. HPALETTE hPalette) ; HDIB DibCreateFromDdb (HBITMAP hBitmap) ; /* Szybkie pobieranie i ustalanie wartoœci piksela */ ţţdefine DibPixelPtrl(hdib, x. y) (((* (PBYTE **) hdib) Cy]) + ((x) Ż 3)) ţţdefine DibPixelPtr4(hdib. x, y) (((* (PBYTE **) hdib) Cy]) + ((x) Ż 1)) ţţdefine DibPixelPtrB(hdib, x, y) (((* (PBYTE **) hdib) Cy]) + (x) ) ţţdefine DibPixelPtri6(hdib. x. y) \ ((WORD *) (((* (PBYTE **) hdib) Cyl) + (x) * 2)) ţţdefine DibPixelPtr24(hdib. x y) \ ((RGBTRIPLE *) (((* (PBYTE **) hdtb) Cy]) + (x) * 3)) 4ţdefine DibPixelPtr32(hdib. x y) \ ((DWORD *) (((* (PBYTE **) hdib) Cy]) + (x) * 4)) ţtdefine DibGetPixell(hdib, x. y) \ (0x01 & (* DibPixelPtrl (hdib, x, y) Ż (7 - ((x) & 7)))) ţţdefine DibGetPixel4(hdib, x. y) \ (OxOF & (* DibPixelPtr4 (hdib. x, y) Ż ((x) & 1 ? 0 : 4))) 4ţdefine DibGetPixel8(hdib, x. y) (* DibPixelPtr8 (hdib. x, y)) ţţdefine DibGetPixell6(hdib, x, y) (* DibPixelPtrl6 (hdib, x, y)) 4ţdefine DibGetPixel24(hdib, x. y) (* DibPixelPtr24 (hdib, x, y)) ţţdefine DibGetPixel32(hdib, x. y) (* DibPixelPtr32(hdib, x, y)) ţţdefine DibSetPixell(hdib, x, y. p) Rozdziaf 16: Palette Manager 845 ((* DibPixelPtrl (hdib, x, y) &= ~( 1 Ž (7 - ((x) & 7)))), \ (* DibPixelPtrl (hdib, x, y) ţ= ((p) Ž (7 - ((x) & 7))))) Itdefine DibSetPixel4(hdib, x, y, p) \ ((* DibPixelPtr4 (hdib, x, y) &= (OxOF Ž ((x) & 1 ? 4 : 0))), \ (* DibPixelPtr4 (hdib, x, y) ţ= ((p) Ž ((x) & 1 ? 0 : 4)))) ttdefine DibSetPixel8(hdib, x, y, p) (* DibPixelPtr8 (hdib, x, y) = p) lldefine DibSetPixell6(hdib, x, y, p) (* DibPixelPtri6 (hdib, x, y) = p) Ildefine DibSetPixel24(hdib, x, y, p) (* DibPixelPtr24 (hdib, x, y) = p) Itdefine DibSetPixel32(hdib, x, y, p) (* DibPixelPtr32 (hdib. x, y) = p) Rysunek 16-22. Plik DIBHELPH Ten plik nagłówkowy definiuje HDIB jako wskaŸruk typu void. W rzeczywisto- œci aplikacja nie musi znać wewnętrznej struktury, na którš wskazuje HDIB. Plik DIBHELP.H zawiera rówrueż deklaracje wszystkich funkcji moduhz DIBHELPC oraz definicje makr. Jeœli przypomnisz sobie działanie funkcji DibPixelPtr, DibGetPixel i DibSetPixel i spró- bujesz zwiększyć ich wydajnoœć, okaże się, że jest na to kilka sposobów. Po pierwsze, możesz pozbyć się wszelkich wanmków logicznych zakładajšc, że funkcje aplikacji będš wywoływane tylko z poprawnymi parametrami. Możesz również usunšć kil- ka wywołań funkcji, takich jak DibBitCount, uzyskujšc odpowiednie informacje przez odwołarue się bezpoœrednio do wskaŸnika do struktury DIBSTRUCT. Mniej oczywistym sposobem zwiększenia efektywnoœci jest pozbycie się warun- ków sprawdzajšcych wersje bitmapy DIB. Zamiast tego można napisać osobne funkcje dla map 1-bitowych (funkcja DibGetPixet2), dla map 4-bitowych (funkcja DibGetPixel4), itd. Kolejnym krokiem mogłoby być zastšpierue wywołań funkcji funkcjami inline oraz makrami. W pliku DIBHELP.H zdefiniowano trzy zestawy makr na podstawie funkcji Dib- PixelPtr, DibGetPixel i DibSet Pixel. Makra sš napisane w kilku wersjach, w zależ- noœci od liczby bitów przypadajšcych na piksel mapy. Program DIBBLE Przedstawiony na rysunku 16-23 program DIBBLE demonstruje zastosowanie funkcji i makr zdefiniowanych w module DIBHELP. Chociaż to najdłuższy z do- tychczas prezentowanych programów, stanowi jedynie niewielki wycinek możli- woœci, którymi dysponuje nawet najprostsza aplikacja przeznaczona do obróbki obrazu cyfrowego. jednym z ważnych usprawrueń programu DIBBLE jest dosto- sowarue do standardu MDI (ang. multiple document interface). Zagadnienie to zo- stanie omówione w rozdziale 19. DIBBLE.C /* DIBBLE.C - Program demonstrujšcy zastosowanie bitmap i palet (c) Charles Petzold, 1998 */ Czţœć II: Grafika (cišg dalszy ze strony 845) ţţinclude llinclude "dibhelp.h" llinclude "dibpal.h" ţţinclude "dibconv.h" ţţinclude "resource.h" ţţdefine WM_USER SETSCROLLS (WM_USER + 1) ţţdefine WM_USER DELETEDIB (WM_USER + 2) 4ţdefine WM_USER DELETEPAL (WM USER + 3) lldefine WM USER CREATEPAL (WM USER + 4) LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; TCHAR szAppName[] = TEXT ("Dibble") ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) ( HACCEL hAccel ; HWND hwnd ; MSG msg : WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CS UREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance : wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDCţARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITEţBRUSH) ; wndclass.lpszMenuName = szAppName ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MBţICONERROR) ; return 0 ; 1 hwnd = CreateWindow (szAppName, szAppName, WS_OUERLAPPEDWINDOW ţ WM_IISCROLL ţ WM HSCROLL, CW USEDEFAULT, CW USEDEFAULT, CW USEDEFAULT, CW USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; hAccel = LoadAccelerators (hInstance, szAppName> ; while (GetMessage (8ţmsg, NULL, 0, 0)) ( if (!TranslateAccelerator (hwnd, hAccel, &msg)) f TranslateMessage (&msg) : Rozdzial 16: Palette Manager 847 OispatchMessage (&msg) ; ) ? return msg.wParam ; ) /* Display0ib: Wyœwietla lub drukuje DIB o rozmiarach rzeczywistych bddŸ rozcidgnięta, w zależnoœci od wyboru polecenia menu */ int Display0ib (HDC hdc, HBITMAP hBitmap, int x, int y, int cxClient, int cyClient, WORD wShow, BOOL fHalftonePalette) BITMAP bitmap ; HDC hdcMem ; int cxBitmap, cyBitmap, iReturn ; GetObject (hBitmap, sizeof (BITMAP), &bitmap) ; cxBitmap = bitmap.bmWidth ; cyBitmap = bitmap.bmHeight ; SaveDC (hdc) ; if (fHalftonePalette) SetStretchBltMode (hdc, HALFTONE) ; else SetStretchBltMode (hdc, COLORONCOLOR) ; hdcMem = CreateCompatibleDC (hdc) ; SelectObject (hdcMem, hBitmap) ; switch (wShow) t case IDM_SHOW_NORMAL: if (fHalftonePalette) iReturn = StretchBlt (hdc, 0, 0, min (cxClient, cxBitmap - x), min (cyClient, cyBitmap - y), hdcMem, x, y, min (cxClient, cxBitmap - x), min (cyClient, cyBitmap - y), SRCCOPY); else iReturn = BitBlt (hdc, 0, 0, min (cxClient, cxBitmap - x), min (cyClient, cyBitmap - y), hdcMem, x, y, SRCCOPY) ; break ; case IDM_SHOW_CENTER: if (fHalftonePalette) iReturn = StretchBlt (hdc, (cxClient - cxBitmap) / 2, (cyClient - cyBitmap) / 2, cxBitmap, cyBitmap. hdcMem, 0, 0, cxBitmap, cyBitmap, SRCCOPY); else Czgœć II: Grafika (cišg dalszy ze strony 847) iReturn = BitBlt (hdc, (cxClient - cxBitmap) / 2, (cyClient - cyBitmap) / 2, cxBitmap, cyBitmap, hdcMem, 0, 0, SRCCOPY) ; break ; case IDM SHOW STRETCH: iReturn = StretchBlt (hdc, 0, 0, cxClient, cyClient, hdcMem, 0, 0, cxBitmap, cyBitmap, SRCCOPY) ; break ; case IDM SHOW_ISOSTRETCH: SetMapMode (hdc, MM ISOTROPIC) ; SetWindowExtEx (hdc, cxBitmap. cyBitmap, NULL> ; SetViewportExtEx (hdc, cxClient, cyClient, NULL) ; SetWindowOrgEx (hdc, cxBitmap / 2, cyBitmap / 2, NULL) ; SetViewportOrgEx (hdc, cxClient / 2, cyClient / 2, NULL) ; iReturn = StretchBlt (hdc, 0, 0, cxBitmap, cyBitmap, hdcMem, 0, 0, cxBitmap, cyBitmap, SRCCOPY) ; break ; 1 DeleteDC (hdcMem) ; RestoreDC (hdc, -1> ; return iReturn ; /* DibFlipHorizontal: Wywoluje nieoptymalizowane funkcje DibSetPixel i DibGetPixel */ HDIB DibFlipHorizontal (HDIB hdibSrc) ( HDIB hdibDst ; int cx, cy, x, y ; if (!DibIsAddressable (hdibSrc)) return NULL ; if (NULL == (hdibDst = DibCopy (hdibSrc, FALSE))) return NULL ; cx = DibWidth (hdibSrc) ; cy = DibHeight (hdibSrc) ; for (x = 0 ; x < cx ; x++) for (y = 0 ; y < cy ; y++) ( DibSetPixel (hdibDst, x. cy - 1 - y. DibGetPixel (hdibSrc, x, y)) ; 1 return hdibDst ; ) /* DibRotateRight: Wywoluje zoptymalizowane wersje DibSetPixelx i DibGetPixelx */ Rflzdziaţ 16: Palette lţanager 849 HDIB DibRotateRight (HDIB hdibSrc) { HDiB hdibDst ; int cx, cy, x, y ; if (!DibIsAddressable (hdibSrc)) return NULL ; if (NULL = (hdibDst = DibCopy (hdibSrc, TRUE))) return NULL ; cx = DibWidth (hdibSrc) ; cy = DibHeight (hdibSrc) ; switch (DibBitCount (hdibSrc)) 1':, t. case 1: for (x = 0 ; x < cx ; x++) ł for (y = 0 ' y < cy ; y++) DibSetPixell (hdibDst, cy - y - 1, x, DibGetPixell (hdibSrc, x, y)) ; break ; ; case 4: ł .. for (x = 0 ; x < cx ; x++) for (y = 0 : y < cy : y++) DibSetPixel4 (hdibDst, cy - y - 1, x, DibGetPixel4 (hdibSrc, x, y)) ; ', break ; i i case 8: ,' 1. for (x = 0 ; x < cx ; x++) for (y = 0 ; y < cy ; y++) DibSetPixelB (hdibDst, cy - y - 1, x, ! DibGetPixelB (hdibSrc, x, y)) ; break ; 4I case 16: for (x = 0 ; x < cx ; x++) I for (y = 0 ; y < cy ; y++) DibSetPixell6 (hdibDst, cy - y - 1, x, DibGetPixell6 (hdibSrc, x, y)) ; ; break ; case 24: for (x = 0 ; x < cx ; x++) for (y = 0 ; y < cy ; y++) DibSetPixel24 (hdibDst, cy - y - 1, x, DibGetPixel24 (hdibSrc, x, y)) ; break ; case 32: for (x = 0 ; x < cx ; x++) for (y = 0 ; y < cy ; y++) DibSetPixel32 (hdibDst, cy - y - 1, x, DibGetPixel32 (hdibSrc, x, y)) ; break ; 1 850 Częœć II: Grafika (cišg dalszy ze strony 849) return hdibDst : /* PaletteMenu: Odznacza i zaznacza polecenia menu Palette */ i void PaletteMenu (HMENU hMenu, WORD wItemNew) ( static WORD wItem = IDM PAL NONE ; CheckMenuItem (hMenu, wItem, MF UNCHECKED) ; wItem = wItemNew : CheckMenuItem (hMenu, wItem, MF CHECKED) : ) LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ( static BOOL fHalftonePalette : static DOCINFO di = ( sizeof (DOCINFO), TEXT ("Dibble: Printing") ? ; static HBITMAP hBitmap ; static HDIB hdib : static HMENU hMenu : static HPALETTE hPalette ; static int cxClient, cyClient, iVscroll, iHscroll ; static OPENFILENAME ofn ; static PRINTDLG printdlg = ( sizeof (PRINTDLG) 1 : static TCHAR szFileName CMAX_PATH], szTitleName CMAX_PATH] : static TCHAR szFilterC] = TEXT ("Bitmap Files (*.BMP)\0*.bmp\0") TEXT ("All Files (*.*)\0*.*\0\0") : static TCHAR * szCompressionC] = ( TEXT ("BI_RGB"), TEXT ("BI_RLE8"), TEXT ("BIţRLE4"), TEXT ("BI_BITFIELDS"), TEXT ("Unknown") ) : static WORD wShow = IDMţSHOWţNORMAL ; BOOL fSuccess ; BYTE * pGlobal ; HDC hdc, hdcPrn ; HGLOBAL hGlobal ; HDIB hdibNew ; int iEnable, cxPage, cyPage; iConvert : PAINTSTRUCT ps ; SCROLLINFO si ; TCHAR szBuffer [256] ; switch (message) ( case WM CREATE: // Zapisz uchwyt menu w zmiennej statycznej hMenu = GetMenu (hwnd) : // Zainicjuj strukturę OPENFILENAME dla okien // dialogowych File Open i File Save. ofn.lStructSize = sizeof (OPENFILENAME) ; Rozdział 16: Palette Manager 851 ofn.hwndOwner = hwnd ; ofn.hInstance = NULL ; ofn.lpstrFilter = szFilter ; ofn.lpstrCustomFilter = NULL ; ofn.nMaxCustFilter = 0 ; ofn.nFilterIndex = 0 ; ofn.lpstrFile = szFileName ; ofn.nMaxFile = MAX PATH ; ofn.lpstrFileTitle = szTitleName ; ofn.nMaxFileTitle = MAX_PATH ; ofn.lpstrInitialDir = NULL ; ofn.lpstrTitle = NULL ; ofn.Flags = OFN OVERWRITEPROMPT ; ofn.nFileOffset = 0 ; ofn.nFileExtension = 0 ; ofn.lpstrDefExt = TEXT ("bmp") ; ofn.lCustData = 0 ; ofn.lpfnHook = NULL ; ofn.lpTemplateName = NULL ; return 0 ; case WM_DISPLAYCHANGE: SendMessage (hwnd, WM USER DELETEPAL, 0, 0) ; SendMessage (hwnd, WM USER CREATEPAL, TRUE, 0) ; return 0 ; case WM_SIZE: // Zapisz rozmiary obszaru roboczego w zmiennych statycznych cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; ; wParam = FALSE ; // ten przypadek zostawiamy nieopracowany // WM_USER SETSCROLLS: Komunikat definiowany przez programistę // Usta1 parametry pasków przewijania. Jeœli trybem wyœwietlania // nie jest "normal", ukryj je. // Jeœli wartoœć wParam wynosi TRUE, zresetuj // położenie pasków przewijania case WM_USER_SETSCROLLS: if (hdib == NULL ţţ wShow != IDM SHOW NORMAL) ( si.cbSize = sizeof (SCROLLINFO) ; si.fMask = SIFţRANGE ; si.nMin = 0 ; si.nMax = 0 ; SetScrollInfo (hwnd, SB_VERT, &si, TRUE) ; SetScrollInfo (hwnd, SB HORZ, &si, TRUE) ; else // Najpierw przewijanie w pionie si.cbSize = sizeof (SCROLLINFO) ; si.fMask = SIF ALL ; A 3 852 Częœe 11: Grafika I (cišg dalszy ze strony 851) GetScrollInfo (hwnd, SBţUERT, &si) ; si.nMin = 0 ; si.nMax = DibHeight (hdib) ; si.nPage = cyClient ; if ((BOOL) wParam) si.nPos = 0 ; SetScrollInfo (hwnd, SB_VERT, &si, TRUE) ; GetScrollInfo (hwnd, SB VERT, &si) ; iVscroll = si.nPos ; // Następnie przewijanie w poziomie GetScrollInfo (hwnd, SB HORZ, &si) ; si.nMin = 0 ; si.nMax = DibWidth (hdib) ; si.nPage = cxClient ; if ((BOOL) wParam) si.nPos = 0 ; SetScrollInfo (hwnd, SB HORZ, &si, TRUE) ; GetScrollInfo (hwnd, SB HORZ, &si) ; iHscroll = si.nPos ; 1 return 0 ; // WM USCROLL: Przewijanie DIB w pionie case WM_USCROLL: si.cbSize = sizeof (SCROLLINFO) ; si.fMask = SIF_ALL ; GetScrollInfo (hwnd, SB VERT, &si) ; iVscroll = si.nPos ; switch (LOWORD (wParam)) ( case SBţLINEUP: si.nPos -= 1 ; break ; case SB_LINEDOWN: si.nPos += 1 ; break ; case SBţPAGEUP: si.nPos -= si.nPage ; break ; case SB_PAGEDOWN: si.nPos += si.nPage ; break ; case SBţTHUMBTRACK: si.nPos = si.nTrackPos ; break ; default: break ; ) si.fMask = SIF_POS ; SetScrollInfo (hwnd, SB VERT, &si, TRUE) ; GetScrollInfo (hwnd, SB VERT, &si) ; if (si.nPos != iVscroll) ( ScrollWindow (hwnd, 0, iVscroll - si.nPos, NULL, NULL) ; iVscroll = si.nPos ; Rozdział 16: Palette Manager 853 UpdateWindow (hwnd) ; return 0 ; // WM HSCROLL: Przewijanie DIB w poziomie case WM_HSCROLL: si.cbSize = sizeof (SCROLLINFO) ; si.fMask = SIF_ALL ; GetScrollInfo (hwnd, SB HORZ, &si) ; iHscroll = si.nPos ; switch (LOWORD (wParam)) , case SB_LINELEFT: si.nPos -= 1 ; break ; case SB_LINERIGHT: si.nPos += 1 ; break ; case SB_PAGELEFT: si.nPos -= si.nPage ; break ; case SB PAGERIGHT: si.nPos += si.nPage ; break ; case SB THUMBTRACK: si.nPos = si.nTrackPos ; break ; default: break ; i I si.fMask = SiF_POS ; ! SetScrollInfo (hwnd, SB_HORZ, &si TRUE) ; GetScrollInfo (hwnd, SB HORZ, &si) ; if (si.nPos != iHscroll) ScrollWindow (hwnd, iHscroll - si.nPos, 0, NULL, NULL) ; iHscroll = si.nPos ; UpdateWindow (hwnd) ; 1 return 0 ; // WMţINITMENUPOPUP: Uaktywnij lub wyszarz polecenia menu case WM_INITMENUPOPUP: if (hdib) iEnable = MFţENABLED ; else iEnable = MF GRAYED ; EnableMenuItem (hMenu, IDM_FILĘ SAVE, iEnable) ; EnableMenuItem (hMenu, IDM_FILE_PRINT, iEnable) ; EnableMenuItem (hMenu, IDM_FILE_PROPERTIES, iEnable) ; EnableMenuItem (hMenu, IDM_EDIT_CUT, iEnable) ; EnableMenuItem (hMenu, IDM_EDIT_COPY, iEnable) ; EnableMenuItem (hMenu, IDMţEDITţDELETE, iEnable) ; if (DibIsAddressable (hdib>) iEnable = MFţENABLED ; else iEnable = MF GRAY^D ; EnableMenuItem (hMenu, IDM_EDIT_ROTATE, iEnable) ; EnableMenuItem (hMenu, IDM EDIT_FLIP, iEnable) ; EnableMenuItem (hMenu, IDM CONVERT O1, iEnable) ; 854 ţ Częœć II: Grafika I (cišg dalszy ze strony 853) EnableMenuItem (hMenu, IDM_CONVERT_04, iEnable) ; EnableMenuItem (hMenu, IDM_CONVERT_08, iEnable) ; EnableMenuItem (hMenu, IDM_CONVERT_16, iEnable) ; EnableMenuItem (hMenu, IDM CONVERT 24, iEnable) ; EnableMenuItem (hMenu, IDM CONVERT 32, iEnable) ; i switch (DibBitCount (hdib)) case 1: EnableMenuItem (hMenu, IDM_CONVERT_O1, MF_GRAYED) ; break ; case 4: EnableMenuItem (hMenu, IDM_CONVERT_04, MF_GRAYED) ; break ; case 8: EnableMenuItem (hMenu, IDM_CONVERT_08, MF_GRAYED) ; break ; case 16: EnableMenuItem (hMenu, IDM_CONVERT_16, MF_GRAYED) ; break ; case 24: EnableMenuItem (hMenu, IDM CONVERT_24, MF GRAYED) ; break ; I case 32: EnableMenuItem (hMenu, IDM CONVERTţ32, MF GRAYED) ; break ; i if (hdib && DibColorSize (hdib) > 0) iEnable = MFţENABLED ; else iEnable = MF GRAYED ; EnableMenuItem (hMenu, IDM PALţDIBTABLE, iEnable) ; I if (DibIsAddressable (hdib) && DibBitCount (hdib) > 8) iEnable = MFţENABLED ; else iEnable = MF GRAYED ; j EnableMenuItem (hMenu, IDM_PAL_OPT_POP4, iEnable) ; EnableMenuItem (hMenu, IDM_PAL_OPT_POP5, iEnable) ; EnableMenuItem (hMenu, IDM_PAL OPT POP6, iEnable) ; EnableMenuItem (hMenu, IDMţPAL OPT MEDCUT, iEnable) ; EnableMenuItem (hMenu, IDM_EDIT_PASTE, IsClipboardFormatAvailable (CF DIB) ? MFţENABLED : MF GRAYED) ; return 0 ; // WM COMMAND: Obsluga wszystkich poleceń menu. i case WM_COMMAND: iConvert = 0 ; switch (LOWORD (wParam).) t case IDM FILE OPEN: // Pokaż okno dialogowe File Open if (!GetOpenFileName (&ofn)) return 0 ; // Jeœli istniejd jakieœ DIB, usuń je SendMessage (hwnd, WM USERţDELETEDIB, 0, 0) ; // Zaladuj DIB do pamięci Rozdziaţ 16: Palette Manager 855 SetCursor (LoadCursor (NULL, IDC WAIT)) ; ShowCursor (TRUE) ; hdib = DibFileLoad (szFileName) ; ShowCursor (FALSE) ; SetCursor (LoadCursor (NULL, IDC ARROW)) ; // Zresetuj paski przewijania SendMessage (hwnd, WM USER SETSCROLLS, TRUE, 0) ; // Utwórz paletę i DDB SendMessage (hwnd, WM USERţCREATEPAL, TRUE, 0) ; if (!hdib) ; MessageBox (hwnd, TEXT ("Cannot load DIB file!"), szAppName, MB OK ţ MBţICONEXCLAMATION) ; InvalidateRect (hwnd, NULL, TRUE) ; return 0 ; case IDM FILE SAVE: // Pokaż okno dialogowe File Save if (!GetSaveFileName (&ofn)) return 0 ; // Zapisz DIB w pamięci SetCursor (LoadCursor (NULL, IDC WAIT)) ; ShowCursor (TRUE) ; fSuccess = DibFileSave (hdib, szFileName) ; ShowCursor (FALSE) ; SetCursor (LoadCursor (NULL, IDC ŽRROW)) ; if (!fSuccess) MessageBox (hwnd, TEXT ("Cannot save DIB file!"), szAppName, MB OK ţ MBţICONEXCLAMATION) ; return 0 ; case IDM_FILE_PRINT: if (!hdib) return 0 ; // Pobierz kontekst urzddzenia drukarki printdlg.Flags = PDţRETURNDC ţ PD NOPAGENUMS ţ PD NOSELECTION ; if (!PrintDlg (&printdlg)) return 0 ; if (NULL = (hdcPrn = printdlg.hDC)) ( Częœţ : Grafika ' i (cigg dalszy ze strony 855) MessageBox (hwnd, TEXT ("Cannot obtain Printer DC"), ! szAppName, MBţICONEXCLAMATION ţ M.B OK) ; return 0 ; ) // SprawdŸ, czy drukarka drukuje bitmapy I if (!(RCţBITBLT & GetDeviceCaps (hdcPrn, RASTERCAPS))) ( ; i DeleteDC (hdcPrnl ; MessageBox fhwnd, TEXT ("Printer cannot print bitmaps"), szAppName, MBţICONEXCLAMATION ( MB OK) ; return 0 ; 1 // Pobierz rozmiary obszaru drukowania cxPage = 6etDeviceCaps (hdcPrn, HOR2RES) ; cyPage = 6etDeviceCaps (hdcPrn, VERTRES) ; i fSuccess = FALSE ; // Wyœlij DIB do drukarki SetCursor (LoadCursor (NULL, IDC_WAIT)) ; ShowOursor (TRUE) ; i if ((StartDoc (hdePrn, ţdi) > 0) ţţ (StartPage (hdcPrn) > 0)) ' { Display0ib (hdcPrn, DibBitmapMandle (hdib), 0, 0, exPage, cyPage, wShow, FALSE) ; if fEndPage (hdcPrn) > 0) ? I fSuccess = TRUE ; ; EndDoc fhdcPrn) ; I ShowCursor (FALSE) ; i SetCursor (LoadCursor (NULL, IDC ARROW)) ; DeleteDC (hdePrn) ; if (!fSuccess) ! MessageBox (hwnd, TEXT ("Could not print bitmap"), j szAppName, MBţICONEXCLAMATION ţ MB OK) ; return 0 ; case IDM_FILE_PROPERTIES:. if (!hdib) return 0 ; wsprintf (szBuffer, TEXT ("Pixel width:\t%i\n") TEXT ("Pixel height:\t%i\n") TEXT ("Bits per pixel:\t%i\n") TEXT ("Number of colors:\t%i\n") TEXT ("Compression:\t%s\n"), DibWidth (hdib), DibHeight (hdib), Rozdział 16: Palette Matţger 857 DibBitCount (hdib), DibNumColors (hdib), ' szCompression Cmin (3, DibCompression (hdib))]) ; MessageBox (hwnd, szBuffer, szAppName, MBţFCONEXCLAMATION ţ MB OK) ; return 0 ; case FDM_APP_EXIT: SendMessage (hwnd, WM CLOSE, 0, 0) ; return 0 ; case IDM_EDIT_COPY: case IDM_EDIT_CUT: if (!(hGlobal = DibCopyToPacked0ib (hdib, TRUE))) return 0 ; OpenClipboard (hwnd) ; EmptyClipboard () ; SetClipboardData (CFţDIB, hGlobal) : CloseClipboard () ; if (LOWORD (wParam) = IDMţEDIT COPY) return 0 ; // ten przypadek zostawiamy nieopracowany dla IDMţEDIT CUT case IDM_EDIT_DELETE: SendMessage fhwnd, WM USER DELETEDIB, 0, 0) ; FnvalidateRect fhwnd, NULL, TRUE) ; i' return 0 ; case IDM_EDIT_PASTE: OpenClipboard (hwnd) ; hGlobal = GetClipboardData (CF_DIB) ; pGlobal = GlobalLock (hGlobal) ; 1.. // Jeœli istnieja DIB i paleta, usuń je. // Następnie przekonwertuj upakowand DIB na format HDIB. if (pGlobal) ( SendMessage (hwnd, WM_USER_DELETEDIB, 0, 0) ; hdib = DibCopyFromPacked0ib ((BITMAPINFO *) pGlobal) ; SendMessage (hwnd, WM USER CREATEPAL, TRUE, 0) ; 1 GlobalUnlock (hGlobal) ; CloseClipboard () ; ? // Zresetuj paski przewijania SendMessage (hwnd, WM_USER_SETSCROLLS, TRUE, 0) ; InvalidateRect (hwnd, NULL, TRUE) ; return 0 ; case IDM_EDIT_ROTATE: if (hdibNew = DibRotateRight (hdib)) I k ţ'ţ 858 Częœć II: Grafika , (cišg dalszy ze strony 857) DibDelete (hdib) ; Delete0bject (hBitmap) ; hdib = hdibNew ; hBitmap = DibCopyToDdb (hdib, hwnd, hPalette) ; SendMessage (hwnd, WM_USER_SETSCROLLS, TRUE, 0) ; InvalidateRect (hwnd, NULL, TRUE) ; else MessageBox (hwnd, TEXT ("Not enough memory"), szAppName, MB OK ţ MBţICONEXCLAMATION) ; return 0 ; i case IDM_EDIT_FLIP: if (hdibNew = DibFlipHorizontal (hdib)) DibDelete (hdib) ; Delete0bject (hBitmap) ; hdib = hdibNew ; hBitmap = DibCopyToDdb (hdib. hwnd, hPalette) ; InvalidateRect (hwnd, NULL, TRUE) ; else MessageBox (hwnd, TEXT ("Not enough memory"), szAppName, MB OK ţ MBţICONEXCLAMATION) ; return 0 ; case IDM_SHOW_NORMAL: i case IDM_SHOW_CENTER: case IDM_SHOW_STRETCH: case IDM_SHOW_ISOSTRETCH: CheckMenuItem (hMenu, wShow, MF UNCHECKED) ; wShow = LOWORD (wParam) ; ' CheckMenuItem (hMenu, wShow, MF CHECKED) ; SendMessage (hwnd, WMţUSER SETSCROLLS, FALSE, 0) ; InvalidateRect (hwnd, NULL, TRUE) ; ;. return 0 ; case IDM_CONVERT_32: iConvert += 8 ; case IDM_CONVERT_24: iConvert += 8 ; case IDM_CONVERT_16: iConvert += 8 ; case IDM_CONVERT_08: iConvert += 4 ; i case IDM_CONVERT_04: iConvert += 3 ; case IDM_CONVERT_Oi: iConvert += 1 ; SetCursor (LoadCursor (NULL, IDC WAIT)) ; ShowCursor (TRUE) ; hdibNew = DibConvert (hdib. iConvert) ; ShowCursor (FALSE) ; Rozdział 16: Palette IHanager SetCursor (LoadCursor (NULL, IDC ŽRROW)) ; if (hdibNew) SendMessage (hwnd, WM USERţDELETEDIB, 0, 0) ; hdib = hdibNew ; SendMessage (hwnd, WM_USER_CREATEPAL, TRUE, 0) ; InvalidateRect (hwnd, NULL, TRUE) ; ) else ( MessageBox (hwnd, TEXT ("Not enough memory"), szAppName, MB OK ţ MBţICONEXCLAMATION) ; ) return 0 ; case IDM_APP_ABOUT: MessageBox (hwnd, TEXT ("Dibble (c) Charles Petzold, 1998">, szAppName, MBţOK ţ MBţICONEXCLAMATION) ; ) return 0 ; // Wszystkie pozostałe komunikaty WM COMMAND dotyczd palety. // Wszystkie istniejdce palety sd usuwane, a kursor // zamienia się w klepsydrę SendMessage (hwnd, WM_USER_DELETEPAL, 0, 0) ; SetCursor (LoadCursor (NULL, IDC WAIT)) ; ShowCursor (TRUE) ; // Zauważ, że wszystkie komunikaty kończd się // poleceniem break, a nie return. Pozwala to na dalsze // wykonywanie kodu programu. switch (LOWORD (wParam)) i case IDM_PAL DIBTABLE: hPalette = DibPalDibTable (hdib) ; break ; case IDM_PAL_HALFTONE: hdc = GetDC (hwnd) ; if (hPalette = CreateHalftonePalette (hdc)) fHalftonePalette = TRUE ; ReleaseDC (hwnd, hdc) ; break ; case IDM_PAL_ALLPURPOSE: hPalette = DibPalAllPurpose () ; break ; case IDM_PAL_GRAY2: hPalette = DibPalUniformGrays ( 2) ; break ; case IDM_PAL_GRAY3: hPalette = DibPalUniformGrays ( 3) ; break ; case IDM_PAL_GRAY4: hPalette = DibPalUniformGrays ( 4) ; break ; case IDM_PAL GRAY8: hPalette = DibPalUniformGrays ( 8) ; break ; case IDMţPAL GRAYl6: hPalette = DibPalUniformGrays ( 16) ; break ; cţţ u: ţraţka (cišg dalszy ze strony 859) case IDM_PAL_GRAY32: hPalette = DibPalUniformGrays ( 32) ; break ; , case IDM_PAL_GRAY64: hPalette = DibPalUniformGrays ( 64) ; break ; case IDM_PAL GRAYl2B: hPalette = DibPalUniformGrays (128) ; break ; case IDMţPAL GRAY256: hPalette = DibPalUniformGrays (256) ; break ; case IDM_PAL_RG8222: hPalette = DibPalUniformColors (2,2,2); break; case IDM_PAL_RG8333: hPalette = DibPalUniformColors (3,3,3); break; case IDM_PAL_RG8444: hPalette = DibPalUniformColors (4,4,4); break; case IDM_PAL_RG8555: hPalette = DibPalUniformColors (5,5,5); break; case IDM_PAL_RG8666: hPalette = DibPalUniformColors (6,6,6); break; l case IDM_PAL_RG8775: hPalette = DibPalUniformColors (7,7,5); break; case IDM_PAL_RG8757: hPalette = DibPalUniformColors (7,5,7J; break; case IDM_PAL_RGBS77: hPalette = DibPalUniformColors (5,7,7); break; case IDM_PAL_RG8884: hPalette = DibPalUniformColors (8,8,4); break; case IDM_PAL_RG8848: hPalette = DibPalUniformColors (8,4,8); break; case IDMţPALţRGB4BB: hPalette = DibPalUniformColors (4,8,8); break; case IDM_PAL OPT_POP4: hPalette = DibPalPopularity (hdib, 4) ; break ; case IDM_PAL_OPT_POP5: hPalette = DibPalPopularity (hdib, 5) ; break ; case IDM_PAL OPT_POP6: hPalette = DibPalPopularity (hdib, 6) ; break ; case IDM_PAL OPT_MEDCUT: hPalette = DibPalMedianCut (hdib., 6) ; break ; t /I Po obsłużeniu poleceń menu Palette przywracany jest // dawny wygldd kursora, pozycja menu jest zaznaczana, // a okno unieważniane hBitmap = DibCopyToDdb (hdib, hwnd, hPalette) ; ShowCursor (FALSE) ; SetCursor (LoadCursor (NULL, IDC ŽRROW)) ; if (hPalette) PaletteMenu (hMenu, (LOWORD (wParam))) ; InvalidateRect (hwnd, NULL, TRUE) ; return 0 ; /! Komunikat definiowany przez programistę usuwa istniejšcd // mapę DIB. Komunikat jest wysyłany w trakcie wykonywania // poleceń File Open, Edit Paste i innych. case WM_USER_DELETEDIB: if (hdib) { \ţ . ţ Rozdzial 16: Pţţte Managţ' ţ1 DibOelete (hdib) ; hdib = NULL : ) SendMessage (hwnd, WM USER DELETEPAL, 0, 0) ; return 0 ; // Komunikat definiowany przez programistę usuwa istniejdcd // DIB,przygotowujšc nowd. case WM_USER_DELETEPAL: if (hPalette) ( Delete0bject (hPalette) ; hPalette = NULL ; fHalftonePalette = FALSE ; PaletteMenu (hMenu, IDMţPAL NONE) ; 1 if (hBitmap) Delete0bject (hBitmap) ; return 0 ; // Komunikat definiowany przez programistę tworzy nowd paletę // na podstawie nowej DIB. Jeœli wParam ţ TRUE, tworzy również DDB. case WM_USER_CREATEPAL: if (hdib) { hdc = GetDC (hwnd) ; if (!(RC PALETTE & GetOeviceCaps (hdc, RASTERCAPS))) ( PaletteMenu (hMenu, IDMţPAL NONE) ; ł else if (hPalette = DibPalDibTable (hdib)) f PaletteMenu (hMenu, IDMţPAL DIBTABLE) ; l else if (hPalette = CreateHalftonePalette (hdc)) ( fHalftonePalette = TRUE ; PaletteMenu (hMenu, IDMţPAL HALFTONE) ; 1 ReleaseDC (hwnd, hdc) ; if ((BOOL) wParam) hBitmap = DibCopyToOdb (hdib, hwnd, hPalette) ; ) return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; if (hPalette) ( SelectPalette (hdc, hPalette, FALSE) ; RealizePalette (hdc) ; 862 Częœć II: Grafika 4 (cišg dalszy ze strony 861) if (hBitmap) , ( DisplayDib (hdc, fHalftonePalette ? DibBitmapHandle (hdib) : hBitmap, iHscroll, iVscroll, cxClient, cyClient wShow, fHalftonePalette) ; J EndPaint (hwnd, &ps) ; return 0 ; case WMţqUERYNEWPALETTE: ! if (!hPalette) return FALSE ; , hdc = GetDC (hwnd) ; SelectPalette (hdc, hPalette. FALSE) ; ! RealizePalette (hdc) ; InvalidateRect (hwnd, NULL, TRUE) ; ReleaseDC (hwnd, hdc) ; return TRUE ; case WM_PALETTECHANGED: if (!hPalette ţţ (HWND) wParam ţ hwnd) break ; hdc = GetDC (hwnd) ; SelectPalette (hdc, hPalette, FALSE) ; RealizePalette (hdc) ; UpdateColors (hdc) ; t ReleaseDC (hwnd, hdc) ; break ; t case WM_DESTROY: if (hdib) DibDelete (hdib) ; if (hBitmap) Delete0bject (hBitmap) ; i if (hPalette) Delete0bject (hPalette) ; PostOuitMessage (0) ; return 0 ; ) return DefWindowProc (hwnd, message, wParam, lParam) ; ; DIBBLE.RC (fragmenty) //Microsoft Developer Studio generated resource script. k i Rozdział 16: Palette Manager 863 ţţinclude "resource.h" ţţinclude "afxres.h" /////////////////////////////////////////////////////////////////////////// // Menu DIBBLE MENU DISCARDABLE BEGIN POPUP "&File" BEGIN MENUITEM "&Open...\tCtrl+0", IDM_FILE_OPEN MENUITEM "&Save...\tCtrl+S", IDMţFILEţSAllE MENUITEM SEPARATOR MENUITEM "&Print...\tCtr1+p", IDMţFILEţPRINT MENUITEM SEPARATOR MENUITEM "Propert&ies... , IDMţFILEţPROPERTIES MENUITEM SEPARATOR MENUITEM "E&xit", IDM APPţEXIT END POPUP "&Edit" BEGIN MENUITEM "Cu&t\tCtrl+X", IDM_EDIT_CUT MENUITEM "&Copy\tCtrl+C", IDM_EDIT_COPY MENUITEM "&Paste\tCtrl+U", IDM_EDIT_PASTE MENUITEM "&Delete\tDelete". IDMţEDITţDELETE MENUITEM SEPARATOR MENUITEM "&Flip", IDM_EDIT_FLIP MENUITEM "&Rotate", IDMţEDIT ROTATE END POPUP "&Show" BEGIN MENUITEM "&Actual Size", IDM_SHOW_NORMAL, CHECKED MENUITEM "&Center", IDM_SHOW_CENTER MENUITEM "&Stretch to Window", IDM SHOWţSTRETCH MENUITEM "Stretch &Isotropically", IDM SHOWţISOSTRETCH END POPUP "&Palette" BEGIN MENUITEM "&None", IDM_PAL_NONE. CHECKED MENUITEM "&Dib ColorTable", IDM_PAL_DIBTABLE MENUITEM "&Halftone", IDM_PAL HALFTONE MENUITEM "&All-Purpose", IDMţPAL ALLPURPOSE POPUP "&Gray Shades" BEGIN MENUITEM "&1. 2 Grays", IDM_PAL_GRAY2 MENUITEM "&2. 3 Grays", IDM_PAL_GRAY3 MENUITEM "&3. 4 Grays", IDM_PAL_GRAY4 MENUITEM "&4. 8 Grays", IDM_PAL_GRAY8 MENUITEM "&5. 16 Grays",. IDMţPAL_GRAYl6 MENUITEM "&6. 32 Grays", IDM_PAL_GRAY32 MENUITEM "&7. 64 Grays", IDM_PAL_GRAY64 MENUITEM "&8. 128 Grays", IDM_PAL GRAYl28 MENUITEM "&9. 256 Grays", IDMţPAL GRAY256 END POPUP "&Uniform Colors" BEGIN MENUITEM "&1. 2R x 2G x 2B (8)", IDM_PAL_RG8222 MENUITEM "&2. 3R x 3G x 38 (27)", IDMţPALţRGB333 Częœć N: Grairka (cišg clalszy ze strony 863) MENUITEM "&3. 4R x 4G x 4B (64)", IDM_PAL_RG8444 MENUITEM "&4. 5R x 5G x 5B (125)", IDM_PAL_RG8555 MENUITEM "&5. 6R x 6G x 6B (216)", IDM_PAL_RG8666 MENUITEM "&6. IR x 7G x 5B (245)",. IDM_PAL_RG8775 MENUITEM "&7. IR x 5B x 7B (245)", IDM_PALţRGB757 MENUITEM "ţ8. 5R x 7G x 7B (245)", IDM_PAL_RG8577 MENUITEM "dţ9. 8R x 8G x 4B (256)", IDM_PAL_RG8884 MENUITEM "&A. 8R x 4G x 8B (256)", IDM_PAL_RG8848 MENUITEM "&B. 4R x 8G x 8B (256)", IDM PALţRGB488 END POPUP "&Optimized" BEGIN MENUITEM "&1. Popularity Algorithm (4 bits)", IDMţPAL_OPT_POP4 MENUITEM "&2. Poputarity Algorithm (5 bits)", IDM_PAL_OPT_POP5 MENUITEM "&3. Popularity Algorithm (6 bits)", IDM_PAL_OPT_POP6 MENUITEM "&4. Median Cut Algorithm ", IDM PAL OPT MEDCUT END END POPUP "Con&vert" BEGIN MENUITEM "&1. to 1 bit per pixel", IDM CONVERT_Oi MENUITEM "&2. to 4 bits per pixel", IDátţCONVERT_04 MENUITEM "&3. to 8 bits per pixel", IDM_CONVERT_08 MENUITEM "&4. to 16 bits per pixel", IDM_CONVERT_16 MENUITEM "&5. to 24 bits per pixel", IDM CONVERT_24 . MENUITEM "&6. ta 32 bits per pixeT", IDM CONVERT 32 END POPUP "&HeTp" BEGIN MENUITEM "&About", IDM_APP ABOUT END END lllllllllllllflllllll!llllllllllllllllllllllllllllllllllllllllllllll!llllll !/ Accelerator DIBBLE ACCELERATORS DISCARDABLE BEGIN "C", IDMţEDIT_COPY, VIRTKEY, CONTROL, NOINVERT "0", IDM_FILE_OPEN, VIRTKEY, CONTROL, NOINVERT j "Př. IDM_FILE_PRINT, VIRTKEY, CONTROL, NOINVERT "S", IDM_FILE_SAVE, VIRTKEY, CONTROL, NOINVERT "V", IDM_EDIT_PASTE, VIRTKEY, CONTROL, NOINVERT VK_DELETE, IDM_EDIT_DELETE, VIRTKEY, NOINVERT "X", IDMţEDITţCUT, VIRTKEY, CONTROL, NOINVERT END RESOURCE.H (fragmenty) //{{NOţDEPENDENCIESl1 // Microsoft Developer Studio generated include file. // Used by Dibble.rc ţţdefine IDM_FILE_OPEN 40001 ţţdefine IDM_FILE_SAVE 40002 ţţdefine IDMţFILEţPRINT 40003 Rozdziáł 16: Palette Marţager #define IDM_FILE_PROPERTIES 40004 #define IDM_APP_EXIT 40005 4ldefine IDM_EDIT_CUT 40006 #define IDM_EDIT_COPY 40007 #defi.ne IDM_EDIT_PASTE 40008 #define IDM_EDIT_DEtETE 40009 #define IDM_EDIT_FLIP 40010 #define IDM_EDIT_ROTATE 40011 #define IDM_SHOW_NORFfAt 400I2 #define IDM_SHOW_CENTER 40013 #define IDM_SHOW_STRETCH 40014 #define IDM_SHOWţISOSTRETCH 40015 tddefine IDM_PAL_NONE 40016 #define IDM_PAL_DIBTABLE 40017 #define IDM_PAL_HALFTONE 400i8 #define IDM_PAL_ALLPURPOSE 40019 #define IDM_PAt_GRAY2 40020 #define IDM_PAL_GRAY3 40021 dldefine IDM_PAL_GRAY4 40022 #define IDM_PAL_GRAY8 40023 #define IDM_PAL_GRAYl6 40024 #define IDM_PAL_GRAY32 40025 #define IDM_PAL_GRAY64 40026 #define IDM_PAL_GRAYl28 40027 4ldefine IDM_PAL_GRAY256 40028 #define IDM_PAL_RG8222 40029 #define IDM_PAL_RG8333 40030 #define IDM_PAL_RG8444 40031 4idefine IDM_PAt_RG8555 40032 #define IDM_PAt_RG8666 40033 tldefine IDM_PAL_RG8775 40034 #define IDM_PAL_RG8757 40035 #define IDM_PAt_RG8577 40036 #define IDM_PAL_RG8884 40037 #define IDM_PAL_RG8848 40038 4ldefine IDM_PAL_RG8488 40039 #define IDM_PAL_OPT_POP4 40040 #define IDM_PAL_OPT_POP5 40041 tldefine IDM_PAL_OPT_POP6 40042 4ldefine IDM_PAL_OPT_MEDCUT 40043 #define IDM_CONVERT_O1 40044 #define IDM_CONVERT_04 40045 #define IDM_CONVERT_08 40046 4idefine IDM_CONVERT_16 40047 4ldefine IDM_CONVERT_24 40048 #define IDM CONVERT_32 40049 #define IDM APP ABOUT 40050 Rysunek 16-23. Program DIBBLE. Program DIBBLE korzysta z kilku innych plików, które żostanš w skrócie opisa- ne. Moduł DIBCONV C oraz plik nagłówkowy DIBCONV.H zawierajš funkcje konwersji między różnymi formatami, na przykład między formatem 24 bitowym a 8-bitowym. Pliki DIBPAL (DIBPAL.C i DIBPAL.H) tworzš palety. Program DIBBLE definiuje trzy bardzo ważne zmienne statyczne w module Wnd- Proc: uchwyt typu HDIB o nazwie hdib, uchwyt palety hPalette typu HPALETTE - 866 Czgœć II: Grafika oraz uchwyt hBitmap typu HBITMAP. Uchwyt HDIB można uzyskać wywołujšc jednš z wielu funkcji moduhz DIBHELP, HPALETTE otrzymuje się dzięki funk- cjom moduhx DIBPAL lub CreateHalftonePalette, uchwyt HBITMAP zaœ jest zwra- cany przez funkcję DibCopyToDdb, pochodzšcš z modułu DIBHELP.C i przyspie- sza działanie grafiki, szczególnie w trybie 256 kolorów. Ten ostatni uchwyt musi być ponownie tworzony za każdym razem, kiedy program tworzy nowš sekcję DIB (oczywiste) oraz kiedy tworzona jest inna paleta (już nie tak oczywiste). Funkcje moduhx DIBBLE zostanš opisane z podziałem na realizowane zadania. Ładowanie i zapisywanie plików Program DIBBLE może ładować pliki DIB i zapisywać je w odpowiedzi na ko- munikaty IDM FILĘ LOAD oraz IDM FILĘ SAVE w trakcie obsługi komunika- tu LVMţCOMMAND. Obsługujšc te komunikaty, DIBBLE wywołuje standardo- we okna dialogowe obsługi plików (funkcje GetOpenFileName i GetSaveFileName). Wykonanie polecenia menu FilebSave wymaga jedynie wywołania funkcji Dib- FileSave. Otwierajšc pliki poleceniem FilebOpen, DIBBLE musi najpierw usunšć poprzedruo używany uchwyt HDIB, paletę i obiekty map bitowych. Realizuje to wysłajšc komunikat WMţUSER DELETEDIB, obshzgiwany przez wywołania funkcji DibDelete i DeleteObject. Następnie program DIBBLE wywohxje funkcję DibFileLoad, po czym wysyła do siebie komunikaty WM USER_SETSCROLLS i WM tTSER CREATEPAL, aby zresetować paski przewijania oraz utworzyć pa- letę. W trakcie obsługi komunikatu WM_USER CREATEPAL program tworzy również nowš DDB na podstawie sekcji DIB. Wyœwietlanie, przewijanie i drukowanie Menu programu DIBBLE pozwala na wyœwietlenie DIB w rzeczywistych rozmia- rach w lewym górnym narożniku lub poœrodku okna. Możliwe jest również roz- cišgnięcie bitmapy do maksymalnych rozmiarów okna lub jego wypełnienie przy zachowaniu rzeczywistego stosunku szerokoœci do dhxgoœci. Dowolna z tych moż- liwoœci może być wybrana w menu Show programu DIBBLE. Zauważ, że cztery opisane powyżej sposoby wyœwietlania bitmap sš identyczne jak w programie SHOWDIB2, przedstawionym w poprzednim rozdziale. Podczas obsługi komunikatu WMţPAINT, jak również w trakcie wykonywania polecenia File Print, program DIBBLE wywohzje funkcję DisplayDib. Zauważ, że DisplayDib korzysta raczej z funkcji BitBlt i StretchBlt niż SetDIBitsToDevice i Stretch- DIBits. W czasie obsługi komunikatu 4VMţPAINT do funkcji jest przekazywany ten sam uchwyt bitmapy, który został utworzony przez DibCopyToDdb, wywoły- wanš w trakcie obshxgi komunikatu WMţUSEIZţCREATEPAL. DDB jest zgodna z kontekstem urzšdzenia karty graficznej. Wykonanie polecenia FilebPrint wiš- że się z wywołaniem funkcji DisplayDib z argumentem wywołania, który jest uchwytem sekcji DIB, otrzymanym dzięki funkcji DibBitmapHandle (moduł DI- BHELP.C). Zwróć również uwagę na zmiennš statycznš typu BOOL o nazwieţlalftonePalet- te, która przyjmuje wartoœć TRUE, jeœli hPalette otrzymano przez wywołarue funkcji CreateHalftonePalette. W takim wypadku funkcja DisplayDib wywołuje StretchBlt zamiast BitBlt, nawet jeœli DIB jest wyœwietlana w rzeczywistych rozmiarach. Rozdział 16: Palette Manager 867 Zmienna fHalftonePalette powoduje również, że w trakcie obsługi WM PAINT uchwyt sekcji DIB jest przekazywany do funkcji DisplayDib zamiast uchwytu bit- mapy, zwracanego przez funkcję DibCopyToDdb. Zastosowarue palety półtono- wej było omówione w poprzednich podrozdziałach i zilustrowane programem SHOWDIB5. Po raz pierwszy program DIBBLE pozwala na przewijanie bitmapy wyœwietlo- nej w oknie. Paski przewijania sš widoczne tylko wtedy, kiedy mapa jest wyœwie- tlana w swych rzeczywistych rozmiarach. WndProc przekazuje bieżšce położenie pasków przewijarua do funkcji DisplayDib w trakcie obsługi WMţPAINT. Schowek Obshxga wykonania poleceń Cut i Copy polega na wywołaniu funkcji DibCopy- ToPackedDib, pochodzšcej z moduhz DIBHELP. Pobiera ona wszystkie elementy DIB i umieszcza w bloku pamięci o dużych rozmiarach. Po raz pierwszy w programie DIBBLE zastosowano technikę wklejania DIB do Schowka. Polega ona na wywołaniu funkcji DibCopyFromPackedDib i zastšpieniu uchwytu HDIB, palety i bitmapy elementami poprzedruo zapisanymi przez pro- cedurę okna. Odwracanie i rotacja Menu Edit programu DIBBLE zawiera oprócz standardowych poleceń - Cut, Copy, Paste i Delete - dwa dodatkowe: Flip i Rotate. Polecenie Flip powoduje odwróce- nie bitmapy względem poziomej osi symetr, tzn. do góry nogami. Polecenie Rotate powoduje obrócenie mapy o 90 stopni zgodnie z ruchem wskazówek ze- gara. Funkcje obsługujšce oba polecerua wymagajš dostępu do wszystkich pik- seli DIB, bo je kopiujš. Ponieważ funkcje te nie wymagajš tworzenia nowej pale- ty, nie jest ona usuwana i tworzona na nowo. Polecenie Flip korzysta z funkcji DibFlipHorizontal, pochodzšcej z moduhz DIB- HELP.C. Funkcja ta wywohxje DibCopy w celu otrzymania dokładnej kop mapy DIB. Następnie wykonuje pętlę, w której kopiuje wszystkie piksele z oryginału do nowo utworzonej DIB tak, że obraz zostaje odwrócony. Zauważ, że funkcja ta wywohxje DibGetPixel oraz DibSetPixel, funkcje ogólnego przeznaczenia (szybkoœć ich działania pozostawia wiele do życzenia), pochodzšce z moduhz DIBHELP.C. Aby unaocznić różnicę pomiędzy funkcjami DibGetPixel, DibSetPixel z pliku DIB- HELPC oraz ich odpowiednikami, makrami DibGetPixel i DibSetPixel, zastoso- wano makra do funlccji DibRotateRight. Zauważ, przede wszystkim, że funkcja ta wywohzje DibCopy, której drugi argument przyjmuje wartoœć TRUE. Powoduje to "zamianę miejsca" dhxgoœci i szerokoœci mapy bitowej przy tworzeniu nowej DIB. Funkcja DibCopy nie kopiuje bitów pikseli, zamiast tego funkcja DibRotate- Right zawiera szeœć pętli, kopiujšcych wszystkie piksele - po jednej dla każdego z trybów mapy (1-bitowa, 4-bitowa, 8-bitowa, 16-bitowa, 24-bitowa i 32-bitowa). Kod zajmuje więcej miejsca, lecz funkcja jest znacznie szybsza. Choć możliwe jest zastosowanie poleceń Flip Horizontal (odwrócenie w poziomie) oraz Rotate Right (rotacja w prawo) do zasymulowania operacji Flip Vertical (od- wrócenie w pionie), Rotate Left (rotacja w lewo) i Rotate 180ř (rotacja o 180 stopru), wszystkie te polecenia zostanš wprowadzone bezpoœrednio w programie. Częœć II: Gra i'ka Palety proste; palety optymałne Program DiBBLE dysponuje wieloma różnymi paletami do wyœwietlania D1B w trybie 256 kolorów. Sš one zestawione w menu Palette. INszystkie palety, z wy- jštkiem półtonowej, która została utworzona bezpoœrednio przez wywołanie tunk- cji systemowej, powstały przez wywołanie ţunkcji zamieszczonych w module DiBPAL, przedstawionym na rysunicu 16-24. DiBPAL.N /* DIBPAL.Ii header file for DIBPAL.C */ HPALETTE DibPalDibTable (HDIB hdib) ; HPALETTE DibPalAllPurpose (void) ; HPALETTE DibPalUniformGrays (int iNum) ; HPALETTE DibPalUniformColors (int iNumR, int iNumG, int iNumB) ; HPALETTE DibPalUga (void) ; HPALETTE DibPalPopularity (HDIB hdib, int iRes) ; HPALETTE DibPalMedianCut (HDIB hdib. int iRes) ; DiBPAL.C /* DTBPAL.C - Funkcje tworzšce palety (c) Charles Petzold, 1998 */ ţţinclude ţtinclude "dibhelp.h" ţţinclude "dibpal.h" /* DibPalDibTable: Tworzy paletę na podstawie tablicy kolorów DIB */ HPALETTE DibPalDibTable (HDIB hdib) ( HPALETTE hPalette ; int i, iNum ; LOGPALETTE * plp ; RGBOUAD rgb ; if (0 = (iNum = DibNumColors (hdib))) return NULL ; plp = malloc (sizeof (LOGPAL^TTEł + (iNum - 1) * sizeof (PALETTEENTRY)) ; plp->palVersion = 0x0300 ; plp->palNumEntries = iNum ; for (i = 0 ; i < iNum ; i++) ( DibGetColor (hdib, i, &rgb) ; Rozdziţ i6: Pţiette lţlanager 869 p1p->palPalEntryCi].peRed = rgb.rgbRed ; plp->palPalEntryCi].peGreen = rgb.rgbGreen ; plp->palPalEntryCi].peBlue = rgb.rgbBlue ; plp->palPalEntryCi].peFlags = 0 ; 1 hPalette = CreatePalette (plp) ; free (plp) ; return hPalette ; /* DibPalAllPurpose: Tworzy paletę pasujšcd do wielu różnych obrazów; paleta sklada się z 247 elementów, z których 15 pokrywa się z kolorami s.tandardowymi. */ HPALETTE DibPalAllPurpose (void) t HPALETTE hPalette ; int i, incr, R, G, B ; LOGPALETTE * plp ; plp = malloc (sizeof (LOGPALETTE) + 246 * sizeof (PALETTEENTRY)) ; plp->palVersion = 0x0300 ; plp->palNumEntries = 247 ; // Poniższa pętla wylicza wartoœć 31 odcieni szaroœci, lecz 3 z // nich pokrywajš się z kolorami standardowymi for (i = 0, G = 0, incr = 8 ; G <= OxFF ; i++, G += incr) plp->palPalEntryCi].peRed = (BYTE) G ; plp->palPalEntryCi].peGreen = (BYTE) G ; plp->palPalEntry[i].peBlue = (BYTE) G ; plp->palPalEntry[i].peFlags = 0 ; ř l.. incr = (incr = 9 ? 8 : 9) : ' // Poniższa pętla jest odpowiedzialna za wygenerowanie 216 elementów // palety, z których 8 pokrywa się z kolorami standardowymi, zaœ // kolejne 4 odpowiadajd zdefiniowanym wyżej odcieniom szaroœci for (R = 0 ; R <= OxFF ; R += 0x33) for (G = 0 ; G <= OxFF ; G += 0x33) for (B = 0 ; B <= OxFF ; B += 0x33) plp->palPalEntryCi].peRed = (BYTE) R ; plp->palPalEntry[i].peGreen = (BYTE) G ; plp->palPalEntryCi].peBlue = (BYTE) B ; plp->palPalEntryCi].peFlags = 0 ; i++ ; ) hPalette = CreatePalette (plp) ; free (plp) : Czgœć 11: Gţafika ; (cišg dalszy ze strony 869) return hPalette ; j 1 /* DibPalUniformGrays: Tworzy paletę iNum odcieni szaroœci */ HPALETTE DibPalUniformGrays (int iNum) f HPALETTE hPalette ; int i ; LOGPALETTE * plp ; plp = malloc (sizeof (LOGPALETTE) + (iNum - 1) * sizeof (PALETTEENTRY)) ; plp->palUersion = 0x0300 ; plp->palNumEntries = iNum ; for (i = 0 ; i < iNum ; i++) ( plp->palPalEntry[i].peRed - plp->palPalEntry[i].peGreen = plp->palPalEntry[i].peBlue = (BYTE) (i * 255 / (iNum - 1)) ; plp->palPalEntry[i].peFlags = 0 ; ) hPalette = CreatePalette (p1p) ; free (p1p) ; return hPalette ; } /* DibPalUniformColors: Tworzy paletę kolorów o liczbie elementów będdcej iloczynem wartoœci zmiennych iNumR x iNumG x iNumB */ HPALETTE DibPalUniformColors (int iNumR, int iNumG, int iNumB) ( HPALETTE hPalette ; int i, iNum, R, G, B ; LOGPALETTE * plp ; iNum = iNumR * iNumG * iNumB ; plp = malloc (sizeof (LOGPALETTE> + (iNum - 1) * sizeof (PALETTEENTRY)) ; plp->palllersion = 0x0300 ; plp->palNumEntries = iNumR * iNumG * iNumB ; i = 0 ; for (R = 0 ; R < iNumR ; R++) for (G = 0 : G < iNumG ; G++) for (B = 0 ; B < iNumB ; B++) { plp->palPalEntry[i].peRed = (BYTE) (R * 255 / (iNumR - 1)) ; plp->palPalEntry[i].peGreen = (BYTE) (G * 255 / (iNumG - 1)) ; plp->palPalEntry[i].peBlue = (BYTE) (B * 255 / (iNumB -'1)) ; plp->palPalEntry[i].peFlags = 0 ; Rozdział 16: Palette Manageţ ;++ ; ) hPalette = CreatePalette (plp) ; free (plp) ; return hPalette ; J /* DibPalVga: Tworzy paletę kolorów na podstawie standardowych 16 kolorów UGA */ HPALETTE DibPalUga (void) static RGBOUAD rgb C16] = ( 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, Iţ OXSO, OXOO, OXOO, OXOO, 0x80. 0x00, 0x80, 0x00, 0x80, 0x80, 0x00, 0x00, i,'.. 0x80, 0x80, 0x80, 0x00, OxCO. OxCO, OxCO, 0x00, 0x00, 0x00, OxFF, 0x00, I a ,: 0x00, OxFF, 0x00, 0x00, 0x00, OxFF, OxFF, 0x00, OxFF, 0x00, 0x00, 0x00, OxFF, 0x00, OxFF, 0x00, OxFF, OxFF, 0x00, 0x00, OxFF, OxFF, OxFF, 0x00 ) ; HPALETTE hPalette ; int i ; i LOGPALETTE * plp ; ! i'. plp = malloc (sizeof (LOGPALETTE) + 15 * sizeof (PALETTEENTRY)) ; ; ł plp->palUersion = 0x0300 ; plp->palNumEntries = 16 ; for (i = 0 ; i < 16 ; i++) !? plp->palPalEntryCi].peRed = rgbCi].rgbRed ; plp->palPalEntryCi].peGreen = rgbCi].rgbGreen ; plp->palPalEntryCi].peBlue = rgb[i].rgbBlue ; plp->palPalEntryCi].peFlags = 0 ; hPalette = CreatePalette (plp) ; free (plp) ; return hPalette ; ) /* Makro stosowane do procedur optymalizacji palety */ 4ţdefine PACKţRGB(R,G,B,iRes) ((int) (R) ţ ((int) (G) Ž (iRes)) ţ \ ((int) (B) Ž ((iRes) + (iRes)))) 872 Częœe 11: Graţ+ka (cišg dalszy ze strony 871) /* AccumColorCounts: Wypełnia zmiennš piCount (indeksowanš przez kolory RGB) liczbš pikseli w danym kolorze. */ static void AccumColorCounts (HDIB hdib, int * piCount, int iRes) ( int x, y, cx, cy ; RGBOUAD rgb ; cx = DibWidth (hdib) ; cy = DibHeight (hdib) ; for (y = 0 ; y < cy ; y++) for (x = 0 ; x < cx ; x++) ( DibGetPixelColor (hdib, x, y, &rgb) ; rgb.rgbRed Ż= (8 - iRes) ; rgb.rgbGreen Ż= (8 - iRes) ; rgb.rgbBlue Ż= (8 - iRes) ; ++piCount [PACKţRGB (rgb.rgbRed, rgb.rgbGreen, rgb.rgbBlue, iRes)] ; ) ) /* DibPalPopularity: Algorytm oparty na "popularnoœci" */ HPALETTE DibPalPopularity (HDIB hdib, int iRes) ( HPALETTE hPalette ; int i, iArraySize, iEntry, iCount, iIndex, iMask, R, G, B ; int * piCount ; LOGPALETTE * plp ; // Sprawdzenie poprawnoœci if (DibBitCount (hdib) < 16) return NULL ; if (iRes < 3 ţţ iRes > 8) return NULL ; // Zarezerwuj tablicę do przeliczania kolorów pikseli iArraySize = 1 Ž (3 * iRes) ; iMask = (1 Ž iRes) - 1 ; if (NULL = (piCount = calloc (iArraySize, sizeof (int)))) return NULL ; // Pobierz liczby kolorów AccumColorCounts (hdib, piCount, iRes) ; Roţział 16: Palette Manager 873 // Ustal wartoœci palety plp = malloc (sizeof (LOGPALETTE) + 235 * sizeof (PALETTE^NTRY)) ; plp->palVersion = 0x0300 ; for (iEntry = 0 ; iEntry < 236 ; iEntry++) t for (i = 0, iCount = 0 ; i < iArraySize ; i++) if (piCount[i] > iCount) R iCount = piCount[i] ; iIndex = i : if (iCount = 0) break ; R = (iMask & iIndex ) Ž (8 - iRes) ; i'.. G = (iMask & (iIndex Ż iRes )) Ž (8 - iRes) ; B = (iMask & (iIndex Ż (iRes + iRes))) Ž (8 - iRes) ; plp->palPalEntry[iEntry].peRed = (BYTE) R ; plp->palPalEntry[iEntry].peGreen = (BYTE) G ; plp->palPalEntry[iEntry].peBlue = (BYTE) B ; plp->palPalEntry[iEntry].peFlags = 0 ; piCount [iIndex] = 0 ; I'.:.. // Przy wyjœciu z pętli iEntry będzie zawierać liczbę // przechowywanych elementów , plp->palNumEntries = iEntry ; // Utwórz paletę, zwolnij pamięć i zwróć uchwyt palety i hPalette = CreatePalette (plp) ; I,ţ free (piCount) ; free (plp) : i return hPalette ; /* 3. Struktury wykorzystywane do implementacji algorytmu cięcia i uœredniania */ 'l. i typedef struct // definiuje rozmiary kostki ( int Rmin, Rmax, Gmin, Gmax, Bmin, Bmax ; ) MINMAX ; typedef struct // Struktura na potrzeby funkcji Compare int iBoxCount ; RGBOUAD rgbBoxAv ; Częœć II: Grafika (cišg dalszy ze strony 873) ) BOXES ; /* FindAverageColor: Znajdowanie œredniej z wartoœci kolorów w kostce */ static int FindAverageColor (int * piCount, MINMAX mm, int iRes, RGBOUAD * prgb) ( int R, G, B, iR, iG, iB, iTotal, iCount ; // Inicjuj zmienne iTotal = iR = iG = iB = 0 ; // Pętla po wszystkich kolorach for (R = mm.Rmin ; R <= mm.Rmax ; R++) for (G = mm.Gmin ; G <= mm.Gmax ; G++) for (B = mm.Bmin ; B <= mm.Bmax ; B++) ( // Pobierz liczbę pikseli w danym kolorze iCount = piCount [PACK RGB (R, G, B, iRes)] ; // Pomnóż liczbę pikseli przez wartoœć koloru iR += iCount * R ; iG += iCount * G ; iB += iCount * B ; iTotal += iCount ; ) // ZnajdŸ œrednid z koloru prgb->rgbRed = (BYTE) ((iR / iTotal) Ž (8 - iRes)) ; prgb->rgbGreen = (BYTE) ((iG / iTotal) Ž (8 - iRes)) ; prgb->rgbBlue = (BYTE) ((iB / iTotal) Ž (8 - iRes)) ; // Zwróć całkowitš liczbę pikseli w obrębie kostki return iTotal ; ) /* CutBox: Podział kostki na dwie częœci */ static void CutBox (int * piCount, int iBoxCount, MINMAX mm, int iRes, int iLevel, BOXES * pboxes, int * piEntry) ( int iCount, R, G, B ; MINMAX mmNew ; // Jeœli kostka jest pusta, wykonaj polecenie return Rozdział 16: Palette Manager 875 if (iBoxCount = 0) return ; // Jeţli stopień zagnieżdżenia wynosi 8 lub kostka zawiera jeden // piksel, można wyznaczyć kolor œredni kostki i zapisać go w // zmiennej wraz z liczbš pikseti w tym kolorze if (iLevel ţ 8 ţţ (mm.Rmin = mm.Rmax && mm.Gmin = mm.Gmax && mm.Bmin = mm.Bmax)) pboxesC*piEntry].iBoxCount = FindAverageColor (piCount, mm, iRes, &pboxes[*piEntry].rgbBoxAv) ; (*piEntry) ++ ; // W przeciwnym razie, jeœli krawędŸ oznaczajšca kolor niebieski // jest najdłuższa, podziel jš else if ((mm.Bmax - mm.Bmin > mm.Rmax - mm.Rmin) && (mm.Bmax - mm.Bmin > mm.Gmax - mm.Gmin)) ( // Inicjuj licznik i wykonaj pętlę zwišzanš z kolorem niebieskim iCount = 0 ; for (B = mm.Bmin ; B < mm.Bmax ; B++) // Zbieraj piksele dla każdej z wartoœci odcieni koloru niebieskiego for (R = mm.Rmin ; R <= mm.Rmax ; R++) for (G = mm.Gmin ; G <= mm.Gmax ; G++) iCount += piCount CPACK RGB (R, G, B, iRes)] ; // Jeœli jesteţmy w polowie liczby kostek if (iCount >= iBoxCount / 2) break ; // Jeœli następna wartoœć koloru niebieskiego jest maksymalna if (B = mm.Bmax - 1) break ; // Przetnij dwie przedzielone kostki. // Drugi argument funkcji CutBox to nowa liczba kostek. // Trzecim argumentem sš nowe wartoœci minimalne i maksymalne. mmNew = mm ; mmNew.Bmin = mm.Bmin ; mmNew.Bmax = B ; CutBox (piCount, iCount, mmNew, iRes, iLevel + 1, pboxes, piEntry) ; mmNew.Bmin = B + 1 ; mmNew.Bmax = mm.Bmax ; CutBox (piCount, iBoxCount - iCount, mmNew, iRes, iLevel + 1, 876 Częœe fl: 6ţafika (cišg dalszy ze strony 875) pboxes, piEntry) ; ) // W przeciwnym razie, jeœli krawędŸ oznaczajšca kolor czerwony // jest najdłuższa, podziel jš (tak jak w przypadku niebieskie9o) else if (mm.Rmax - mm.Rmin > mm.Gmax - mm.Gmin) ( iCount = 0 ; for (R = mm.Rmin ; R < mm.Rmax ; R++) ( for (B = mm.Bmin ; B <= mm.Bmax ; B++) for (G = mm.Gmin ; G <= mm.Gmax ; G++) iCount += piCount CPACKţRGB (R, G, B, iRes)] ; if (iCount >= iBoxCount / 2) break ; if (R = mm.Rmax - 1) break ; ) mmNew = mm ; mmNew.Rmin = mm.Rmin ; mmNew.Rmax = R ; CutBox (piCount, iCount, mmNew, iRes, iLevel + 1, pboxes, piEntry) ; mmNew.Rmin = R + 1 ; mmNew.Rmax = mm.Rmax ; CutBox (piCount, iBoxCount - iCount, mmNew, iRes, iLevel + l, pboxes, piEntry) ; ) // W przeciwnym przypadku podziel kostkę po krawędzi // odpowiadajšcej za kolor zielony else ( iCount = 0 ; for (G = mm.Gmin ; G < mm.Gmax ; G++) for (B = mm.Bmin ; B <= mm.Bmax ; B++) for (R = mm.Rmin ; R <= mm.Rmax ; R++) iCount += piCount CPACK RGB (R, G, B, iRes)] ; if (iCount >= iBoxCount / 2) break ; if (G = mm.Gmax - 1) break ; ) mmNew = mm ; mmNew.Gmin = mm.Gmin ; mmNew.Gmax = G ; Rozdział 16: Palette Manager CutBox (piCount, iCount, mmNew, iRes, iLevel + 1, pboxes, piEntry) ; r. mmNew.Gmin = G + 1 ; mmNew.Gmax = mm.Gmax ; CutBox (piCount, iBoxCount - iCount, mmNew, iRes, iLevel + 1, pboxes, piEntry) ; ) : /* Procedura porównujšca dla sortowania qsort */ static int Compare (const BOXES * pboxl, const BOXES * pbox2) t return pboxl->iBoxCount - pbox2->iBoxCount ; /* DibPalMedianCut: Tworzy paletę na podstawie algorytmu cięcia i uœredniania */ HPALETTE DibPalMedianCut (HDIB hdib, int iRes) ( BOXES boxes C256] ; HPALETTE hPalette ; int i, iArraySize, iCount, R, G, B, iTotCount, iDim, iEntry = 0 ; ţ ' int * piCount ; LOGPALETTE * plp ; MINMAX mm ; // Sprawdzenie poprawnoœci if (DibBitCount (hdib) < 16) return NULL ; if (iRes < 3 ţţ iRes > 8) return NULL ; // Zbieraj liczbę kolorów pikseli iArraySize = 1 Ž (3 * iRes) ; if (NULL = (piCount = calloc (iArraySize, sizeof (int)))) return NULL ; ;, AccumColorCounts (hdib, piCount, iRes) ; // Wyznacz rozmiary kostki iDim = 1 Ž iRes ; mm.Rmin = mm.Gmin = mm.Bmin = iDim - 1 ; mm.Rmax = mm.Gmax = mm.Bmax = 0 ; iTotCount = 0 ; gţg Częœć II: Grafika (cišg dalszy ze strony 879) for (R = 0 ; R < iDim ; R++) for (G = 0 ; G < iDim ; G++) for (B = 0 ; B < iDim ; B++) if ((iCount = piCount CPACKţRGB (R, G, B, iRes)]) > 0) f iTotCount += iCount ; if (R < mm.Rmin) mm.Rmin = R ; if (G < mm.Gmin) mm.Gmin = G ; if (B < mm.Bmin) mm.Bmin = B ; if (R > mm.Rmax) mm.Rmax = R ; if (G > mm.Gmax) mm.Gmax = G ; if (B > mm.Bmax) mm.Bmax = B ; // Podziel pierwszš kostkę (funkcja iteracyjna). // Przy wychodzeniu z funkcji struktura będzie zawierać do 256 // wartoœci RGB, po jednej dla każdej z kostek, oraz liczbę // pikseli znajdujdcych się w każdej z nich // Wartoœć zmiennej iEntry oznaczać będzie liczbę niepustych // kostek. CutBox (piCount, iTotCount, mm, iRes, 0, boxes, &iEntry) ; free (piCount) ; // Sortuj tablicę RGB wedlug liczby pikseli w każdym z kolorów qsort (boxes, iEntry, sizeof (BOXES), Compare) ; plp = malloc (sizeof (LOGPALETTE) + (iEntry - 1) * sizeof (PALETTEENTRY)) ; if (plp = NULL) return NULL ; plp->palllersion = 0x0300 ; plp->palNumEntries = iEntry ; for (i = 0 ; i < iEntry ; i++) plp->palPalEntryCi].peRed = boxesCi].rgbBoxAv.rgbRed ; plp->palPalEntryCi].peGreen = boxesCi].rgbBoxAv.rgbGreen ; , p1p->palPalEntryCi].peBlue = boxes[i].rgbBoxAv.rgbBlue ; plp->palPalEntryCi].peFlags = 0 ; ) hPalette = CreatePalette (plp) ; free (plp) ; return hPalette ; Rysunek 1&24. Pliki programu DIBPAL Pierwsza funkcja - DibPalDibTable - powinna wyglšdać znajomo. Tworzy ona paletę na podstawie tablicy kolorów DIB. Działa podobrue jak tunkcja PackedDib- Rozdział 16: Palette Manager gţ9 CreatePalette z modułu PACKEDIB.C, zastosowana w programie SHOWDIB.3 Tak jak w SHOWDIB3, funkcja DibPalDibTable będzie działać pod warunkiem, że DIB ma tablicę kolorów. Funkcja jest nieużyteczna, kiedy próbujesz wyœwietlić 16-, 24- lub 32-bitowš DIB w 8-bitowym trybie graficznym. Domyœlnie przyjęto, że przy zastosowaniu 256-kolorowego trybu grafiki DIBBLE najpierw spróbuje wywołać funkcję DibPalDibTable w celu utworzenia palety na podstawie tablicy kolorów DIB. Jeœli okaże się, że mapa DIB nie ma tablicy kolo- rów, DIBBLE wywoła funkcję CreateHalftonePalette i nada zmiennej fHalftonePa- lette wartoœć TRUE. Opisane powyżej operacje odbywajš się w trakcie obsługi komunikatu WM LlSER CREA'TEţPAL. Moduł DIBPAL.C zawiera funkcję o nazwie DibPalAllPurpose, która powinna rów- nież wyglšdać znajomo, ponieważ jest podobna do funkcji CreateAllPurposePalet- te z modułu SHOWDIB4. Utworzona paleta może być wybrana z menu Show programu DIBBLE. Jednym z interesujšcych aspektów wyœwietlania bitmap w trybach 256 kolorów jest możliwoœć operowania kolorami używanymi przez Windows do wyœwietla- nia obrazu. Jeœli wybierzesz i zrealizujesz paletę, Windows użyje wyłšcznie ko- lorów pochodzšcych z tej palety. Możesz na przykład utworzyć paletę składajšcš się tylko z odcieni szaroœci, sto- sujšc funkcję DibPalUniformGrays. Zastosowanie dwóch odcieni szaroœci da w wy- niku paletę o 2 wartoœciach: 00-00-00 (czamy) i FF-FF-FF (biały). Wypróbuj jš z kil- koma obrazami. W efekcie otrzymasz, oczywiœcie, obraz czarno biały. Zastoso- wanie 3 odcieni szaroœci spowoduje dodanie do tego obrazu nieco koloru szare- go, użycie 4 odcieni szaroœci uwidoczni jeszcze więcej szczegółów obrazu. Stosujšc 8 odcieni szaroœci, prawdopodobnie zauważysz kontury obrazu, ale bę- dzie on, ruestety, wcišż pokryty nieregularnymi łatami w różnych odcieniach. Przyczynš jest sposób działania algorytmu wyszukujšcego kolory najbardziej zbli- żone do danego (na pewno nie kieruje się on kryterium estetyki obrazu). Powięk- szerue palety kolorów o dalsze 8 odcieni znacznie poprawi jakoœć obrazu. 32 od- cienie szaroœci wystarczajš do wyeliminowania niekorzystnych efektów kontu- rów, a 64 odcienie sš powszechnie uznawane za gómš granicę możliwoœci stoso- wanego do tych celów sprzętu. Wykroczerue poza 64 odcienie szaroœci nie po- prawia jakoœci obrazu wyœwietlanego na urzšdzeniach o rozdzielczoœci 6-bito- wej. Dotychczas do wyœwietlania 16-, 24- i 32-bitowych DIB w 8-bitowym trybie gra- fiki udało się nam zastosować paletę wielozadaniowš (odpowiedruš dla obrazów czarno-białych, lecz zwykle nieodpowiedniš dla kolorowych) i paletę półtono- wš. Zauważ, że od kiedy wybrałeœ paletę wielozadaniowš do wyœwietlania dużych, 16-, 24- i 32-bitowych DIB w 8-bitowym trybie grafiki, utworzenie obiektu GDI nie zajmuje programowi sporo czasu. Czas ten jest krótszy, gdy program tworzy DDB na podstawie DIB. Zobaczysz różnicę zestawiajšc szybkoœć działania pro- gramów SHOWDIBI i SHOWDIB4 w trakcie wyœwietlania wielkich, 24-bitowych map w 8-bitowym trybie graficznym. Dlaczego tak się dzieje? i 880 Częœó II: Grafika l OdpowiedŸ kryje się w algorytmie poszukiwania najbardziej zbliżonego koloru. I Zwykle wyœwietlajšc mapę 24-bitowš w 8-bitowym trybie graficznym (lub kon- wertujšc DIB na format DDB), GDI musi przypisać każdemu pikselowi DIB war- toœć jednego z 20 statycznych kolorów. Najłatwiej można to zrobić okreœlajšc, który kolor jest najbardziej podobny do koloru danego piksela. 4V tym celu należy ob- liczyć odległoœć między wartoœciš danego koloru i każdego z kolorów statycz- nych w trójwymiarowej "kostce" RGB. Zabiera to mnóstwo czasu, szczególnie gdy DIB składa się z milionów pikseli. Jeœli utworzysz paletę 232-kolorowš, takš jak paleta wielozadaniowa w programach DIBBLE i SHOWDIB4, czas potrzebny na przeprowadzenie najbardziej zbliżonego koloru zwiększy się ponad 11-krotnie! GDI musi teraz szukać wœród 232, zamiast wœród 20 kolorów i dlatego wyœwietlanie DIB przebiega wolno. Powyższe rozważania powinny być naukš, że należy unikać wyœwietlania 24- bitowych (również 16- i 32-bitowych) DIB w 8-bitowym trybie graficznym. Przed wyœwietleniem należy poddać je konwersji na 8-bitów przez znalezienie 256-ko- lorowej palety, która najlepiej przybliża zakres kolorów DIB. Taka paleta jest czę- sto nazywana "optymalnš". Do opisania tego problemu pomocny był artykuł autorstwa Paula Heckberta "Color Image Quantization for Frame Buffer Displays" , który ukazał się w czasopiœmie "Computer Graphics" w czerwcu 1982 r. Rozkład jednolity Najłatwiejszym podejœciem przy wyborze palety 256-kolorowej jest wybranie jed- nolitego zakresu wartoœci RGB (podejœcie podobne jak w funkcji DibPaIAIIPurpo- ses). Zaletš wspomnianego rozwišzania jest to, że nie trzeba badać poszczegól- nych pikseli DIB. Funkcjš, która tworzy paletę kolorów na podstawie jednolitego zakresu wartoœci RGB, jest DibPalCreateUniformColors. Jeden z sensownych rozkładów składa się z 8 poziomów czerwieni i zieleni oraz 4 poziomów koloru niebieskiego (na które oko jest najmniej wrażliwe). Paleta składa się z kolorów RGB, będšcych kombinacjami czerwonego i zielonego: 0x00, 0x24, 0x49, Ox6D, 0x92, OxB6, OxDB i OxFF oraz niebieskiego o wartoœciach 0x00, 0x55, OxAA oraz OxFF, co daje w wyniku 256 kolorów. Inna paleta o rozkładzie jednolitym wykorzystuje 6 odcieni każdego z kolorów RGB o następujšcych war- toœciach: 0x00, 0x33, 0x66, 0x99, OxCC i OxFF. Liczba kolorów w takiej palecie wynosi 6 do potęgi trzeciej, co daje 216. Poniżej przedstawiono dwa inne rozwišzania dostępne w programie DIBBLE. Algorytm oparty na "popularnoœci" Algorytm "popularnoœci" opiera się na dosyć oczywistym podejœciu do proble- mu palety 256-kolorowej. Należy przejrzeć wszystkie piksele obrazu wybierajšc 256 najczęœciej powtarzajšcych się wartoœci RGB. Następnie wybrane wartoœci trzeba umieœcić w palecie. Algorytm ten zaimplementowano w funkcji DibPalPo- pularity. Jednak jeœli do opisarua każdego koloru palety zastosujesz słowa 24-bitowe, two- ja tablica zajmie 64 megabajty pamięci. Co więcej, może się okazać, że na mapie bitowej nie znajdziesz powtarzajšcych się 24-bitowych wartoœci pikseli, nie bę- dzie więc kolorów najczęœciej występujšcych. Rozdział 16: Palette Manager gg1 /* Aby rozwišzać ten problem, możesz zastosować jedynie n najbardziej znaczšcych bitów każdej składowej wartoœci RGB -na przykład 6 zamiast 8 bitów. Takie dzia- łanie ma sens, gdyż większoœć kolorowych skanerów czy kart graficznych dys- ponuje rozdzielczoœciš 6-bitowš. Przeprowadzenie takiego uproszczenia zmniej- sza tablicę do bardziej sensownych rozmiarów 1 megabajta. Zastosowanie 5 bi- tów zmniejszy całkowitš liczbę kolorów do 32 768. Użycie 5 bitów jest korzyst- niejsze niż 6 bitów, o czym możesz się przekonać, uruchamiajšc program DIB- BLE i wczytujšc kilka kolorowych obrazów. Algorytm cięcia i uœredniania Funkcja DibPalMedianCut została napisana przez Paula Heckberta na podstawie algorytmu cięcia i uœredniania. Jest on dosyć prosty, choć z punktu widzenia pro- gramisty bardziej skomplikowany niż algorytm popularnoœci i doskonale nadaje się do wywołań rekurencyjnych. WyobraŸ sobie szeœcian kolorów RGB. Każdy piksel obrazu jest punktem we- wnštrz tego szeœcianu. Niektóre punkty szeœcianu mogš być reprezentacjš wielu pikseli obrazu. WyobraŸ sobie kostkę, która zawiera wszystkie punkty obrazu. OdnajdŸ jej najdłuższy wymiar i podziel kostkę na dwie częœci, z których każda zawiera jednakowš liczbę pikseli. Dla każdej z dwóch powstałych kostek wyko- naj tę samš operację. Teraz masz 4 kostki. Następnie dziel je kolejno przez 2 otrzy- mujšc 8, 16, 32, 64, 128 i wreszcie 256 kostek. Teraz masz 256 kostek, z których każda zawiera w przybliżeniu jednakowš licz- bę pikseli. Uœrednij wartoœci pikseli każdej kostki, otrzymujšc wartoœci 256-kolo- rowej palety. W rzeczywistoœci kostki nie zawierajš jednakowej liczby pikseli, na przykład pu- dełko zawierajšce jeden punkt ma wiele pikseli. Zdarza się tak w wypadku kolo- rów białego i czarnego. Czasami powstajšce kostki nie zawierajš wcale pikseli - można wówczas próbowae częstszych cięć. Innš technikš znajdowania palety optymalnej jest metoda octree quantization. Zo- stała ona opisana przez Jeffa Prosise'a w "Microsoft System Joumal" z sierpnia 1996. Konwersja formatów Program DIBBLE pozwala na konwertowanie DIB między różnymi formatami. Zastosowano do tego funkcję DibConvert, znajdujšcš się w pliku DIBCONV, przed- stawionym na rysunku 16-25. DIBCONV.H DIBCONV.H header file for DIBCONV.C */ HDIB DibConvert (HDIB hdibSrc, int iBitCountDst) ; 882 Częœć II: Grafika (cišg dalszy ze strony 881) DIBCONU.C /* DIBCONV.C - Konwertuje mapy DIB pomiędzy różnymi formatami (c) Charles Petzold, 1998 */ ţţinclude ţţinclude "dibhelp.h" ţţinclude "dibpal.h" ţţinclude "dibconv.h" HDIB DibConvert (HDIB hdibSrc, int iBitCountOst) f HDIB hdibDst ; HPALETTE hPalette ; int i, x, y, cx, cy, iBitCountSrc, cColors ; PALETTEENTRY pe ; RGBOUAD rgb ; WORD wNumEntries ; cx = OibWidth (hdibSrc) ; cy = DibHeight (hdibSrc) : iBitCountSrc = DibBitCount (hdibSrc) ; if (iBitCountSrc = iBitCountDst) return NULL ; // Konwersja DIB zawierajdcej tablicę kolorów na format DIB z // tablicd o większych rozmiarach if ((iBitCountSrc < iBitCountDst) && (iBitCountOst <= 8)) ( cColors = DibNumColors (hdibSrc) ; hdibDst = DibCreate (cx, cy. iBitCountDst, cColors) ; for (i = 0 ; i < cColors ; i++) ( DibGetColor (hdibSrc, i, &rgb) ; DibSetColor (hdibDst, i, &rgb) ; ) for (x = 0 ; x < cx ; x++) for (y = 0 ; y < cy ; y++) ( DibSetPixel (hdibDst, x, y, DibGetPixel (hdibSrc, x, y)) ; ) ) // Konwersja dowolnej DIB na format DIB bez tablicy kolorów else if (iBitCountDst >= 16) ( hdibDst = DibCreate (cx, cy, iBitCountDst, 0) ; for (x = 0 ; x < cx ; x++) for (y = 0 ; y < cy ; y++) Rozdziat 16: Palette Manager 883 i DibGetPixelColor (hdibSrc, x, y, &rgb) ; DibSetPixelColor (hdibDst, x, y, &rgb) ; I // Konwersja DIB nie majšcej tablicy kolorów na format // 8-bitowy , i else if (iBitCountSrc >= 16 && iBitCountDst = 8) hPalette = DibPalMedianCut (hdibSrc, 6) ; GetObject (hPalette, sizeof (WORD), &wNumEntries) ; hdibDst = DibCreate (cx, cy, 8, wNumEntries) ; I for (i = 0 ; i < (int) wNumEntries ; i++) ! GetPaletteEntries (hPalette, i, l, &pe) ; rgb.rgbRed = pe.peRed ; rgb.rgbGreen = pe.peGreen ; rgb.rgbBlue = pe.peBlue ; rgb.rgbReserved = 0 ; DibSetColor (hdibDst, i, &rgb) ; i ł for (x = 0 ; x < cx ; x++) for (y = 0 ; y < cy ; y++) DibGetPixelColor (hdibSrc, x, y, &rgb) ; 1 DibSetPixel (hdibDst, x, y, GetNearestPaletteIndex (hPalette, I RGB (rgb.rgbRed, rgb.rgbGreen, rgb.rgbBlue))) ; ) Delete0bject (hPalette) ; // Konwersja dowolnej DIB na format monochromatyczny else if (iBitCountDst = 1) ( hdibDst = DibCreate (cx, cy, 1, 0) ; hPalette = DibPalUniformGrays (2) ; for ( i = 0 ; i < 2 ; i++) GetPaletteEntries (hPalette, i, 1, &pe) ; rgb.rgbRed = pe.peRed ; I rgb.rgbGreen = pe.peGreen ; rgb.rgbBlue = pe.peBlue ; rgb.rgbReserved = 0 ; DibSetColor (hdibDst, i. &rgb) ; 4 - Częœć 11: Gţafika j I (cišg dalszy ze strony 883) i for (x = 0 ; x < cx ; x++) for (y = 0 ; y < cy ; y++) ; ( ! DibGetPixelColor (hdibSrc, x, y, &rgb) ; i DibSetPixel (hdibDst, x, y, f GetNearestPaletteIndex (hPalette, RGB (rgb.rgbRed, rgb.rgbGreen, rgb.rgbBlue))) ; ! Delete0bject (hPalette) ; // Konwersja mapy niemonochromatycznej na format 4-bitowy else if (iBitCountSrc >= 8 && iBitCountDst ţ 4) ( hdibDst = DibCreate (cx, cy, 4, 0) ; hPalette = DibPalVga () ; for ( i = 0 ; i < 16 ; i++) f GetPaletteEntries (hPalette, i, 1, &pe) ; rgb.rgbRed = pe.peRed ; rgb.rgbGreen = pe.peGreen ; rgb.rgbBlue = pe.peBlue ; rgb.rgbReserved = 0 ; DibSetColor (hdibDst, i, &rgb) ; J for (x = 0 ; x < cx ; x++) for (y = 0 ; y < cy ; y++) f DibGetPixelColor (hdibSrc, x, y, &rgb) ; DibSetPixel (hdibDst, x, y, GetNearestPaletteIndex (hPalette, RGB (rgb.rgbRed, rgb.rgbGreen, rgb.rgbBlue))) ; ) Delete0bject (hPalette) ; y // Nie powinno być konieczne else hdibDst = NULL ; return hdibDst ; Rysunek 16-25. Pliki programu DIBCONV Konwersja DIB między różnymi formatami wymaga zastosowania kilku strateg. Aby DIB majšcš tablicę kolorów skonwertować na format DIB o większej liczbie bitów przypadajšcych na piksel (np. mapę 1-bitowš na 4-bitowš czy 8-bitowš lub 4-bitowš na 8-bitowš), wystarczy utworzyć nowš DIB przez wywołanie funkcji Rozdziaţ 16: Palette Manager DibCreate, z pożšdanš liczbš bitów na piksel, lecz z liczbš kolorów odpowiadajš- cš oryginalnej DIB. Funkcja kopiuje poszczególne bity pikseli i kolory z tablicy kolorów. Jeœli nowo powstajšca DIB ma nie mieć tablicy kolorów (gdy liczba bitów na pik- sel wynosi 16, 24 lub 32), należy jš utworzyć w nowym formacie i skopiować bity pikseli przy użyciu funkcji DibGetPixelColor i DibSetPixelColor. Kolejny przypadek jest prawdopodobnie najbardziej powszechnie stosowany: istniejšca DIB nie ma tablicy kolorów (16-, 24- lub 32-bitowa), a po konwersji ma powstać mapa 8-bitowa. W takim wypadku funkcja DibConvert wywołuje Dib- PalMedianCut w celu utworzenia optymalnej palety dla danego obrazu. Tabli- ca kolorów nowej DIB jest wypełniana wartoœciami IZGB powstałej palety. Funk- cja DibGetPixelColor uzyskuje kolory pikseli z istniejšcej DIB. Następnie sš one konwertowane na format 8-bitowy przez wywołanie GetNearestPalettelndex i umieszczane na DIB po wywołaniu funkcji DibSetPixel. Jeœli istniejšca DIB ma być skonwertowana na format monochromatyczny, tabli- ca nowej DIB zawiera zaledwie dwa kolory - czarny i biały. Tak jak poprzednio funkcja GetNearestPalettelndex konwertuje istniejšce kolory na 0 lub 1. Kiedy mapa 8-bitowa ma być skonwertowana na 4-bitowš, tablica kolorów DIB otrzymywa- na jest przez wywołanie funkcji DibPalVga, a GetNearestPalettelndex ponownie wy- znacza wartoœci pikseli. Chociaż DIBBLE demonstruje, jak rozpoczšć pisanie programu do obróbki obra- zów, w rzeczywistoœci program taki nigdy nie jest skończony. Zawsze można wzbogacić go o kolejne operacje. Niestety, my musimy na tym zakończyć i przejœć do dalszych rozdziałów. Rozdział 17 T ' ' ' ek st ţ czcţ n o kţ Wyœwietlanie tekstu było pierwszym zadaruem, z którym zmierzyliœmy się w tej ksišżce. Teraz nadszedł czas, aby poznać zastosowarue różnych czcionek i ich rozmiarów dostępnych w Microsoft Windows oraz nauczyć się wyrównywania strumienia tekstu. Wprowadzenie czcionek TrueType w Windows 3.1 znacznie rozszerzyło możli- woœci pracy programistów i użytkowników z tekstem. TrueType jest nazwš tech- nolog wynalezionej przez Apple Computer i Microsoft Corporation, wykorzy- stywanej przez wielu producentów czcionek. Ponieważ czcionki TrueType sš ska- lowalne i mogš być używane zarówno do wyœwietlania, jak i drukowania tek- stów, Windows pozwala uzyskać na wydruku obraz rzeczywisty - jest to tzw. efekt WYSIWYG (ang. what-you-see-is-what-you-get - to, co widzisz (na ekranie), otrzymasz na wydruku). TrueType nadaje się również doskonale do wszelkich , nawet najbardziej wymyœlnych manipulacji czcionkami, jak obracanie, wypełnia- nie wzorami czy też wycinarue regionów. Wszystkie wspomruane własnoœci czcio- nek TrueType zostanš opisane w poruższym rozdziale. Proste wyœwietlanie tekstu Rozpocznijmy od przyjrzenia się funkcjom Windows przeznaczonym do wypro- wadzania tekstu na ekran monitora, atrybutom kontekstu urzšdzenia majšcym wpływ na tekst oraz sposobom użycia czcionek zapasowych. Funkcje rysowania tekstu Najbardziej popularna funkcja służšca do wyprowadzara tekstu na ekran była dotychczas wielokrotnie stosowana w wielu przykładowych programach: TextOut (hdc, xStart, yStart, pString, iCount) ; Argumenty xStart i yStart sš współrzędnymi pozycji poczštkowej łańcucha tek- stu w układzie logicznym. Zwykle od tego punktu, czyli lewego górnego naroż- nika pierwszego znaku, Windows rozpoczyna rysowanie. Funkcja TextOut wy- maga ponadto wskaŸnika do łańcucha tekstowego oraz jego długoœci, nie rozpo- znaje natomiast łańcuchów zakończonych znakiem NLTLL. Znaczenie argumen- tów xStart i yStart w funkcji TextOut można zmienić, wywołujšc funkcję SetTe- xtAlign. Znaczniki TŽ LEFT, TA_RIGHT i TŽ CENTER okreœlajš, jak xStart roz- mieœci poczštek tekstu. Domyœlnie przyjmowana jest wartoœć TAţLEFT. Jeœli za pomocš funkcji SetTextAlign ustalisz wartoœć znacznika na TA_RIGHT, po wy- wołaniu TextOut tekst zostanie rozmieszczony tak, że jego ostatni znak znajdzie I 888 Grafika się w pozycji xStart. W przypadku TA CENTER wartoœć xStart będzie oznaczać położenie œrodka łańcucha tekstowego. Analogicżnie znaczniki TA TOP, TA BOTTOM i TA BASELINE odpowiadajš za położenie tekstu w pionie. Wartoœciš domyœlnš jest TA TOP, co oznacza takie umieszczenie łańcucha znakowego, że yStart okreœla wysokoœć górnej krawędzi tekstu. Zastosowanie TA_BOTTOM oznacza, że tekst jest umieszczany ponad li- niš wyznaczanš przez yStart. Znacznik może mieć również wartoœć TA BASELI- NE, wówczas linia bazowa tekstu znajdzie się w pozycji yStart. Linia bazowa to ta, poniżej której wystajš "ogonki" małego p, q, y itp. Jeœli wywołasz funkcję SetTextAlign ze znacznikiem TA UPDATECP, Windows zignoruje argumenty xStart i yStart funkcji TextOut i zamiast nich zastosuje bie- żšcš pozycję ustalonš przez funkcje MoveToEx, LineTo lub innš, która zmienia aktualnš pozycję kursora. Znacznik TA UPDATECP sprawia ponadto, że funk- cja TextOut aktualizuje bieżšcš pozycję do końca tekstu (dla TA LEFT) bšdŸ do jego poczštku (dla TA IZIGHT). Własnoœć ta jest użyteczna w wypadku wyœwie- tlaniu wiersza tekstu za pomocš wielu wywołań funkcji TextOut. Kiedy położe- nie poziome okreœlane jest przez znacznik TŽ CENTER, bieżšca pozycja nie zmie- nia się po wywołaniu TextOut. Przypomnij sobie wyœwietlanie tekstu w kolumnach za pomocš ser programów SYSMETS w rozdziale 4. Wymagało to odrębnego wywołania funkcji TextOut dla każdej z kolumn. Alternatywnym rozwišzaniem może być zastosowanie funkcji TabbedTextOut: TabbedTextOut (hdc, xStart, yStart, pString, iCount, iNumTabs, piTabStops, xTabOrigin) : Jeœli łańcuch tekstowy zawiera znaki tabulacji ('\t' lub 0x09), TabbedTextOut roz- szerzy tabulacje, opierajšc się na tablicy typu integer przekazywanej do funkcji. Pierwsze pięć argumentów funkcji TabbedTextOut jest identyczne jak w funkcji TextOut. Argument szósty to liczba tabulacji stopu, a siódmy - tablica tabulacji stopu o wartoœciach okreœlonych w pikselach. Jeœli na przykład œrednia szerokoœć znaku wynosi 8 pikseli i chcesz umieœcić tabulację co 5 znaków, wspomniana ta- blica powinna zawierać wartoœci 40, 80, 120 itd., w porzšdku rosnšcym. Jeœli szósty i siódmy argument przyjmujš wartoœci 0 lub NULL, tabulacje stopu ustawiane sš co 8 œrednich szerokoœci znaków. Jeœli szósty argument wynosi 1, siódmy argument wskazuje na pojedynczš wartoœć typu integer, powtarzanš ro- snšco dla kolejnych tabulacji stopu (jeœli ostatni argument przyjmuje na przykład wartoœć 1, a siódmy argument wskazuje na zmiennš przechowujšcš wartoœć 30, tabulacje stopu rozmieszczone będš w pozycjach 30, 60, 90.... pikseli). Ostatni argument oznacza logicznš współrzędnš x poczštkowej pozycji, od której mie- rzone sš tabulacje stopu. Wartoœć ta może, lecz nie musi pokrywać się z pozycjš poczštkowš łańcucha tekstowego. Kolejnš zaawansowanš funkcjš, która służy do wyprowadzania tekstu na ekran, jest ExtTextOut (przedrostek Ext pochodzi od ang. extended - rozszerzony): ExtTextOut (hdc, xStart, y5tart, i0ptions, &rect, PString, iCount, pxDistance) : Rozdział 17: Tekst i czcionki 889 Pišty argument funkcji jest wskaŸnikiem do struktury opisujšcej prostokšt. Może to być prostokšt wycinajšcy, jeœli parametr iOption przyjmie wartoœć ETO_CLIP- PED, bšdŸ prostokšt tła (który można wypełnić bieżšcym kolorem tła), jeœli pa- rametr iOption przyjmie wartoœć ETO OPAQUE. iOption może przyjmować obie wartoœci bšdŸ nie przyjmować żadnej z nich. Ostatni argument jest tablicš elementów typu integer, które okreœlajš odległoœci między kolejnymi znakami łańcucha tekstowego. Pozwala to programiœcie na elastycznš regulację tych odległoœci, co może być przydatne do justowania poje- dynczego słowa w wšskiej kolumnie tekstu. Domyœlne odstępy między znakami tekstu otrzymuje się, nadajšc argumentowi wartoœć NULL. Funkcjš wyższego rzędu przeznaczonš do pisania tekstu jest DrawText, z którš zetknšłeœ się po raz pierwszy w programie HELLOWIN w rozdziale 3. Zamiast okreœlać poczštkowe współrzędne łańcucha tekstu, wystarczy zdefiniować struk- turę typu RECT, opisujšcš prostokšt, w którym ma pojawiać się tekst: l DrawText (hdc, pString, iCount, &rect, iFormat) ; Podobnie jak w wypadku innych funkcji wyprowadzania tekstu, DrawText wy- maga podania wskaŸnika do łańcucha tekstowego oraz długoœci tego łańcucha. Jeœli jednak jako argument podasz wskaŸnik do cišgu znaków zakończonego wartoœciš NULL, wystarczy, że ustalisz wartoœć iCount jako -l, a Windows auto- matycznie wyznaczy długoœć tekstu. Kiedy iFormat przyjmuje wartoœć 0, Windows interpretuje tekst jako jednolitero- we wiersze oddzielone znakiem powrotu karetki ('\r' lub OxOD) bšdŸ znakiem przesunięcia wiersza ('\n' lub OxOA). Tekst rozpoczyna się w lewym górnym narożniku prostokšta definiowanego strukturš RECT. Znak powrotu karetki bšdŸ przesunięcia wiersza jest traktowany jako znak nowego wiersza, więc Windows kończy bieżšcy wiersz i rozpoczyna nowy. Każdy nowy wiersz rozpoczyna się po lewej stronie prostokšta i jest odległy od poprzedniego o jednš wysokoœć zna- ku. Każdy, włšczajšc w to częœci liter, wystajšcy poza obszar prostokšta po pra- wej stronie lub poniżej dolnej jego krawędzi jest obcinany. Domyœlny sposób działania funkcji DrawText można zmienić, odpowiednio ma- nipulujšc argumentem iFormat, który może przyjmować jednš z wielu wartoœci. 1 Wartoœć stałej DT LEFT (przyjmowana domyœlnie) wymusza wyrównywanie tek- stu do lewej krawędzi prostokšta, DT RIGHT odpowiada za wyrównanie tekstu do jego prawej krawędzi, a DT CENTER decyduje o wyœrodkowaniu łańcucha. Ponieważ stała DT LEFT jest zdefiniowana jako 0, nie musisz jej okreœlać, jeœli chcesz, aby tekst był wyrównany do lewej. Jeœli nie chcesz, aby znaki powrotu karetki oraz przesunięcia wiersza były inter- pretowane jako znaki nowego wiersza, możesz przypisać zmiennej iFormat war- toœć stałej DT SINGLELINE i Windows potraktuje wspomniane znaki jako zwy- kłe, a nie sterujšce. Stosujšc DT SINGLELINE można również okreœlić, czy tekst ma być umieszczany przy górnej krawędzi prostokšta (DT TOP - domyœlnie), w jego dolnej częœci (DT BOTTOM) bšdŸ poœrodku (DT VCENTR, gdzie V ozna- cza vertical - w pionie). Wyœwietlajšc kilka wierszy tekstu, Windows łamie je w miejscach występowania znaków powrotu karetki bšdŸ przesunięcia wiersza. Jeœli jednak wiersze sš zbyt 890 Częœć II: Grafika długie, by zmieœcić się w obrębie prostokšta, możesz zastosować znacznik DT WORDBREAK, który powoduje łamanie tekstu po każdym słowie. Zarówno w tekstach jedno-, jak i wielowierszowych, Windows odcina fragmenty wystajš- ce poza prostokšt. To domyœlne działanie funkcji można zmienić, stosujšc znacz- nik DT NOCLIP, który jednoczeœrue przyspieszy jej pracę. Domyœlnie Windows przyjmuje odległoœć pomiędzy wierszami tekstu równš wysokoœci jednego zna- ku. Jeœli wolisz zastosować zewnętrznš interlinię, użyj znacznika DT EXTERNALLEADING. Jeœli wyœwietlany tekst zawiera znaki tabulacji ('\t' lub 0x09), należy zastosować znacznik DT EXPANDTABS. Domyœlnie tabulacje stopu ustawiane sš co 8 zna- ków. Możesz zmienić ten rozmiar, ustawiajšc znacznik DT TABSTOP. W takim wypadku bardziej znaczšcy bajt parametru iFormat zawiera liczby okreœlajšce pozycje każdej tabulacji stopu. Radzę jednak unikać stosowania tej techniki, gdyż bardziej znaczšcy bajt parametru iFormat jest używany również przez kilka in- nych znaczników. Problem znacznika DT TABSTOP jest rozwišzany przez funk- cję DrawTextEx, która przyjmuje dodatkowy argument: DrawTextEx (hdc, pString, iCount, &rect, iFormat, &drawtextparams) ; Ostatni argument jest wskaŸnikiem do struktury DRAWTEXTPARAMS, zdefinio- wanej następujšco: typedef struct tagDRAWTEXTPARAMS ( UINT cbSize ; // rozmiar struktury int iTabLength ; // rozmiar każdej tabulacji stopu int iLeftMargin ; // margines lewy int iRightMargin ; // margines prawy UINT uiLengthDrawn ; // liczba opracowanych znaków DRAWTEXTPARAMS, * LPDRAWTEXTPARAMS ; Trzy œrodkowe pola struktury okreœlane sš w jednostkach stanowišcych wielo- kro#noœci œredruej szerokoœci znaku. Atrybuty kontekstu urzţdzenia dia tekstu Poza omówionš powyżej funkctš SetTextAlign istnieje wiele innych narzędzi, któ- rych działanie wpływa na atrybuty tekstu w kontekœcie urzšdzenia. Domyœ)ny kontekst urzšdzenia zakłada czarny kolor tekstu, jędnak możesz to zmienić, sto- sujšc funkcję: SetTextColor (hdc, rgbColor) ; Podobnie jak w wypadku kolorów pisaka i pędzla, Windows konwertuje war- toœć rgbColor na format koloru czystego. BieŸšcy kolor może być uzyskany wy- wołaniem funkcji GetTextColor. Windows wyœwietla tekst wewnštrz prostokštne- go obszaru, którego kolor może, lecz nie musi pokrywać się z kolorem tegoż tła. Tryb tła może zostae włšczony za pomocš funkcji: SetBkMode (hdc. iMode) ; Gdzie iMode przyjmuje wartoœć OPAQUE lub TRANSPARENT. Domyœlnie tryb tła ustawiany jest w wersji OPAQUE, co oznacża, że Windows wykorzystuje ko- lor tła do wypełnienia prostokšta. Kolor tła można zmienić stosujšc funkcję: SetBkColor (hdc, rgbColor) ; Rozdział 17: Tekst i czcionki 891 Wartoœć rgbColor jest konwertowana na kolor czysty. Domyœlnym kolorem tła jest biały. Jeœli dwa wiersze tekstu sš do siebie zbyt zbliżone, prostokšt tła jednego z ruch może przysłonić drugi. Z tego powodu warto, aby domyœlnym trybem tła był TRANSPA- RENT (przezroczysty). W trybie TRANSPARENT Windows ignoruje kolor tła i nie wypelnia nim prostokšta. Tryb i kolor tła system wykorzystuje rówrueż w przerwach lin przerywanych i kropkowanych a także oczkach różnych wzorów stosowanych do wypełniania, o których była mowa w rozdziale 5. Wiele programów działajšcych w œrodowisku Windows stosuje WHITE_BRUSH jako pędzel do wycierania tła okna. Jest on okreœlany w strukturze klasy okna. Chcšc wypełruć tło jednym z kolorów systemowych, czyli wybieranych za po- mocš Panelu sterowania, musisz okreœlić go następujšco: Wndclass.hbrBackground = COLOR WINDOW + 1 ; Kolor tła oraz tekstu, który chcesz umieœcić w obszarze roboczym, możesz usta- lić wykorzystujšc bieżšce kolory systemowe: SetTextColor (hdc, GetSysColor (COLOR WINDOWTEXT)) ; SetBkColor (hdc, GetSysColor (C,OLOR WINDOW)) ; Możesz wówczas oczekiwać, że system zawiadomi cię o ewentualnej zmianie kolorów systemowych: case WM_SYSCOLORCHANGE: InvalidateRect (hwnd, NULL, TRUE) ; break ; Innym atrybutem kontekstu urzšdzenia, który ma wpływ na wyglšd tekstu, sš odległoœci między znakami. Domyœlnie przyjęto, że wielkoœć odstępu między kolejnymi znakami tekstu wynosi 0. Oznacza to, że Windows nie wstawia żad- nych dodatkowych spacji. Umieszczenie dodatkowych spacji między znakami można uzyskać stosujšc funkcję: SetTextCharacterExtra (hdc, iExtra) ; Argument iExtra wyrażony jest w jednostkach logicznych. Windows konwertuje tę wartoœć na jednostki pikselowe. Nie jest możliwe okreœlenie wartoœci ujemnej w celu zacieœnienia liter tekstu. W takich wypadkach jako argument przyjmowa- na jest wartoœć bezwzględna podanej liczby. Wielkoœć bieżšcych odstępów mię- dzy znakami tekstu można otrzymać, wywołujšc funkcję GetTextCharacterExtra. Przed zwróceniem odpowiedniej wartoœci jednostki wyrażone w pikselach mu- szš być skonwertowane na jednostki logiczne. Czcionki zapasowe Kiedy wywołujesz funkcje TextOut, TabbedTextOut, ExtTextOut, DrawText czy Draw- TextEx w celu wyprowadzenia tekstu na ekran, Windows korzysta z czcionki, która jest aktualrue wybrana w kontekœcie urzšdzenia. Czcionkę definiuje okreœlony krój i rozmiar. Najłatwiejszym sposobem wyœwietlenia tekstu przy użyciu różnych czcionek jest wykorzystanie tzw. czcionek zapasowych Windows. Dostępna w Windows gama czcionek zapasowych jest, ruestety, dosyć uboga. Uchwyt czcionki zapasowej można uzyskać przez wywołanie: ł HFont = GetStockObject ( iFont ) ; 892 Częœć II: Grafika gdzie iFont jest jednym z kilku identyfikatorów. Następnie możesz wybrać czcion- kę w kontekœcie urzšdzenia: SelectObject(hdc. hFont) ; lub zrealizować obie powyższe operacje w jednym kroku: SelectObject (hdc. GetStockObject (iFont)) ; Czcionka wybrana w domyœlnym kontekœcie urzšdzenia nazywana jest systemo- wš i identyfikowana przez argument SYSTEM FONT funkcji GetStockObject. Jest to zestaw czcionek proporcjonalnych ANSI. Okreœlajšc parametr SYSTEM FI- XED_FONT jako argument funkcji GetStockObject (zastosowałem go w kilku pro- gramach przedstawionych w tej ksišżce) otrzymujesz uchwyt czcionki zgodnej z systemowš, stosowanš w Windows przed wersjš 3. Taki sposób jest wygodny, jeœli chcesz, aby czcionki miały tę samš szerokoœć. Czcionka zapasowa OEM FIXED FONT, zwana również terminalowš, zawiera- jšca zestaw znaków IBM PC, jest używana przez Windows w oknach trybu MS- DOS, natomiast czcionka DEFAULT GUI FONT - do wyœwietlania tekstu pasków tytułowych, menu i w oknach dialogowych. Wybierajšc nowš czcionkę w kontekœcie urzšdzenia, musisz wyznaczyć wyso- koœć oraz œredniš szerokoœć znaku przy użyciu funkcji GetTextMetrics. Jeżeli wy- brałeœ czcionkę proporcjonalnš, pamiętaj, że szerokoœć poszczególnych znaków różni się dosyć znacznie. W dalszej częœci rozdziału nauczysz się okreœlać całko- witš długoœć łańcucha tekstowego złożonego ze znaków o różnej szerokoœci. Chociaż GetStockObject oferuje najłatwiejszy sposób dostępu do różnych czcio- nek Windows, okazuje się, że nie masz nad nimi wystarczajšcej kontroli. Wkrót- ce dowiesz się, jak dokładrue okrreœlić pożšdany krój i rozmiar czcionki. Czcionki - informacje podstawowe Większoœć pozostałej częœci tego rozdziału traktuje o pracy z różnymi czcionka- mi. Zanim jednak rozpoczniemy wspólnš analizę kodów przykładowych progra- mów, warto zapoznać się z podstawami wiedzy o czcionkach oferowanych w sys- temie Windows. Typy czcionek Windows obsługuje dwie szerokie kategorie czcionek: czcionki GDI oraz czcion- ki sprzętowe. Czcionki GDI przechowywane sš w postaci plików na dysku twar- dym komputera. Czcionki sprzętówe, jak sama nazwa wskazuje, sš œciœle zwiš- zane ze sprzętem, na przykład powszechnie stosowane drukarki majš wbudo- wane zestawy czcionek sprzętowych. Czcionki GDI dzieli się na trzy kategorie: rastrowe, wektorowe i czcionki TrueType. Czcionki rastrowe nazywane sš również bitmapowymi, ponieważ każdy znak jest przechowywany jako pikselowa matryca. Sš projektowane odrębnie dla różnych rozmiarów i proporcji. Większe rozmiary można uzyskać w Windows jedynie zwiększajšc liczbę kolumn lub wierszy pikseli wchodzšcych w skład czcionki rastrowej. Oczywiœcie takie "rozcišgarue" ma pewne granice, dlatego rastrowe Rozdział 17: Tekst i czcionki czcionki GDI nazywane sš nieskalowanymi. Nie można ich powiększyć ani zmniej- szyć do dowolnych rozmiarów. Najważniejsze zalety czcionek rastrowych to szyb- koœć ich wyœwietlania oraz czytelnoœć (zostały zaprojektowane ręcznie tak, aby były jak najbardziej wyraŸne). Czcionki identyfikuje się po nazwie kroju. Czcionki rastrowe majš następujšce nazwy krojów: System (stosowana jako SYSTEM FONT) ! FixedSys (stosowana jako SYSTEM FIXED FONT) Terminal (stosowana jako OEM FIXED FONT) Courier MS Serif MS Sans Serif (stosowana jako DEFAULT GUI FONT) Small Fonts Każda z czcionek rastrowych istrueje w kilku zaledwie (nie więcej niż szeœciu) rozmiarach. Courier to czcionka o stałej szerokoœci i wyglšdzie zbliżonym do czcionek maszyny do pisaru„. Słowo "serif" oznacza małe, zaokršglone ogonki stanowišce zakończenia znaków, na przykład takie jak w czcionkach zastosowa- nych w niniejszej ksišżce. Czcionka Sans Serif jest pozbawiona owych końcówek. We wczesnych wersjach Windows czcionki MS (Microsoft) Serif i MS Sans Serif były nazywane Tms Rmn (ze względu na podobieństwo do Times ftoman) oraz Helv (jako skrót od Helvetica). Czcionka Small Fonts została zaprojektowańa spe- cjalnie do wyœwietlania tekstu o małych rozmiarach. Przed Windows 3.1 jedynymi (prócz rastrowych) czcionkami systemowymi były tzw. czcionki wektorowe, zdefiniowane w postaci łšczšcych się odpowiednio kresek. Sš one skalowane co oznacza, że ta sama czcionka może być użyta do wyœwietlania tekstu na morutorach o różnych rozdzielczoœciach, a jej rozmiary dadzš się dowolnie zmieniać. Niestety, czytelnoœć tekstu zapisanego tš czcionkš o małych rozmiarach pozostawia wiele do życzenia, a przy dużym powiększe- ruu pogarsza się jej wyglšd. Czcionki wektorowe sš obecnie nazywane plotero- wymi, ponieważ sprawdzajš się jedynie na wydrukach wykonanych ploterem. Ich kroje to Modern, IZoman i Script. Oba rodzaje czcionek GDI, zarówno rastrowe jak i wektorowe, Windows może wzbogacić o następujšce atrybuty: pogrubierue, kursywa, podkreœlenie i przekre- œlenie, bez koruecznoœci tworzenia i przechowywania odrębnych zestawów (na przykład w wypadku kursywy, górna częœć czcionki jest przesuwana w prawo). Ostatnim rodzajem czcionek GDI jest oczywiœcie TrueType, której poœwięcę więk- szoœć pozostałej częœci tego rozdziału. ţ Czcionki TrueType Poszczególne znaki czcionki TrueType składajš się z konturów różnego kształtu, , wypełruonych kolorem. Windows może zmieniać rozmiary czcionek, modyfiku- jšc współrzędne opisujšce kontury kształtu. 894 rů.."xţ ţţ. "--ţ., Kiedy jakiœ program zaczyna korzystać z czcionki TrueType o danym rozmiarze, Windows rasteryzuje jš. Znaczy to, że skaluje współrzędne lin prostych i krzy- wych każdej czcionki, kierujšc się wskazówkami zawartymi w pliku z niš zwiš- zanym. Wskazówki te sš pomocne w kompensowaniu błędów zwišzanych ze skalowaruem czcionek. (Na przykład obie nóżki litery H powinny mieć jednako- wš gruboœć. Bezmyœlne skalowarue czcionki mogłoby spowodować, że jedna z nó- żek litery H byłaby grubsza ruż druga. Dzięki plikowi podpowiedzi unikniesz tego). Kontur powstały po powiększeniu czcionki jest wykorzystywany do utwo- rzenia bitmapy znakn, która jest przechowywana w pamięci podręcznej do póŸ- niejszego wykorzystania. Pierwsze wersje Windows były wyposażane w 13 czcionek TrueType: Courier New Courier New Bold Courier New Italic Courier New Bold Italic Times New Roman Times New Roman Bold Times New Roman Italic Times New Roman Bold Italic Arial Arial Bold Arial Italic Arial Bold Italic Symbol W ostatrch wersjach Windows lista znacznie się powiększyła, na przykład obecnie często jest wykorzystywana Lucida Sans Unicode, gdyż zawiera kilka różnych alfabetów. Trzy najważniejsze rodziny czcionek TrueType sš podobne jak w wypadku ra- strowych. Courier New ma stałš szerokoœć i wyglšdem przypomina czcionki używane w maszynach do pisania. Times New Roman jest klonem czcionki Ti- mes, zaprojektowanej pierwotnie dla gazety "Times of London", a stosowanej w znacznej częœci drukowanych publikacji i uważanej za najbardziej czytelnš. Arial jest kopiš czcionki bezszeryfowej Helvetica, a Symbol to różne przydatne sym- bole. Atrybuty czy style? Zauważyłeœ na pewno, że na przedstawionej powyżej liœcie czcionek TrueType Courier, Times New Roman oraz Arial występujš w kilku wersjach, w zależnoœci od nadanych im atrybutów. Nazewnictwo uwzględniajšce poszczególne atrybu- ty czcionki ma swe Ÿródło w sposobie ich postrzegania. Użytkownicy kompute- rów przyzwyczaili się do traktowania pogrubierua czy pochylenia (kursywy) jako pewnych własnoœci, które mogš zostać nadane czcionce już istniejšcej. Takie po- Rozdział 17: Tekst i czcionki 895 i dejœcie przyjęto w Windows jeszcze w trakcie projektowania nazw i systematy- zowania czcionek rastrowych. Dla czcionek TrueType zastosowano bardziej tra- dycyjne nazewnictwo. Niejednolitoœć nazewnictwa nie została dotychczas z Windows usunięta. Podsu- mowujšc, możesz wybierać czcionki posługujšc się ich pełnymi nazwami bšdŸ ; też skrótami, a następnie dodajšc odpowiednie atrybuty. Pozyskanie listy wszyst- kich czcionek dostępnych w systemie jest - jak można się spodziewać - nieco skomplikowane, ze względu na dwojakie podejœcie w ich nazewnictwie. Rozmiar punktowy W tradycyjnej typograf czcionka okreœlana jest za pomocš nazwy kroju oraz rozmiaru wyrażanego w jednostkach zwanych punktami. Jako wielkoœć punktu przyjmuje się najczęœciej 1/72 cala. Tekst tej ksišżki został wydrukowany czcion- kš 10-punktowš. Rozmiar czcionki jest zwykle definiowany jako wysokoœć zna- ków, która jest liczona od końca wydhxżenia górnego do końca wydhzżenia dol- nego na przykład całkowita wysokoœć liter bq. Jest to sposób wygodny, lecz zwykle niedokładny, bioršc pod uwagę powszechnie stosowane jednostki miar. Mierzerue czcionki w punktach jest tradycyjnym podejœciem typograficznym, a nie metrycznym - rzeczywisty rozmiar znaku zapisanego danš czcionkš może być mniejszy lub większy niż podany w punktach. W typograf komputerowej ist- niejš inne metody wyznaczania rzeczywistych rozmiarów znaków. Interlinia i odstţpy między znaksmi Jak pamiętasz z rozdziału 4, informacje o bieżšcej czcionce wybranej w kontekœcie urzšdzerua można otrzyxnać, wywołujšc funkcję GetTextMetrics. Rysunek 4-3. ilu- struje pionowe rozmiary czcionki umieszczane w strukturze FONTMETRIC. Inne pole struktury TEXTME'IRiC nosi nazwę tmExternalLeading. Słowo "leading" (wymawiane "leding") pochodzi od pasków ołowiu (ang. Iead - ołów), które daw- niej drukarze umieszczali między wierszami składajšcymi się z czcionek. (Słowo "leading" oznacza to samo co interlinia-przyp. tłum.). Wartoœć tmInternalLeading odpowiada zwykle odległoœci między wierszami tekstu zarezerwowanej dla zna- ków diakrytycznych; zmienna tmExternalLeading sugeruje pozostawienie dodat- kowej przestrzeni. Wartoœć interlin zewnętrznej może być zignorowana przez programujšcego. Mówišc o czcionce 8- lub 12-punktowej, mamy na myœli jej wy- sokoœć pomniejszonš o szerokoœć interlin wewnętrznej (ang. internal Ieading). Znaki diakrytyczne częœci wiehkich liter zajmujš miejsce przeznaczone na interli- nię. Wartoœć zmiennej tmHeight w strukturze TEXTMETRIC zwišzana jest raczej z interliniš niż rozmiarem czcionki. Rozmiar czcionki można uzyskać odejmujšc od wartoœci zmiennej tmHeight wartoœć tmlnternalLeading. Problem togicznego cala Jak pisałem w rozdziale 5 (w podrozdziale "Rozmiar urzšdzenia"), Windows 98 definiuje czcionki systemowe o rozmiarze 10 punktów i 12-punktowej interlin. W zależnoœci od tego, czy w oknie dialogowym Właœciwoœci: Ekran wybierzesz 896 lţţţll 11. /ţ..-:en opcję Małe czcionki czy Duże czcionki, ich rozmiary będš wynosić odpowiednio 16 lub 20 pikseli, a różnica między tmHeight a tmlnternalLeading wyniesie 13 lub 16 pikseli. Dlatego wybór czcionki implikuje rozdzielczoœć urzšdzenia wyœwie- tlajšcego jš na ekranie (mierzonš w punktach na cal) - w naszym wypadku 96 dpi dla małych czcionek i 120 dpi dla dużych czcionek. Rozdzielczoœć takš można uzyskać, wywołujšc funkcję GetDeviceCaps z argumen- tami LOGPIXELSX lub LOGPIXELY. Rzeczywista metryczna odległoœć, jakš zaj- muje 96 lub 120 pikseli na ekranie, jest nazywana calem logicznym. Jeœli zmie- rzysz dowolnš odległoœć na ekranie za pomocš zwykłej linijki, a następnie poli- czysz piksele składajšce się na tę odległoœć, prawdopodobnie okaże się, że cal logiczny jest dłuższy niż zwykły. Dlaczego? Czcionka 8-punktowa o gęstoœci w poziomie około 14 znaków na cal jest dosko- nale czytelna na papierze. Pracujšc z edytorem tekstu lub aplikacjš DTP, chciał- byœ zapewne, aby czcionka o takich rozmiarach była równie wyraŸna i czytelna na ekranie komputera. Jeœli jednak weŸmiesz pod uwagę rzeczywiste rozmiary ekranu, okaże się prawdopodobnie, że ma on niewystarczajšcš liczbę pikseli, aby przedstawić czcionki o takiej wielkoœci. Nawet gdyby rozdzielczoœć była wystar- czajšca, i tak miałbyœ problemy z odczytaniem tekstu zapisanego czcionkš 8-punk- towš. Ponadto tekst wydrukowany na papierze czytamy z odległoœci około 30 cm, a wyœwietlony na ekranie - z odległoœci dwukrotnie większej. Cal logiczny zapewnia w efekcie powiększenie rozmiarów ekranu, zapewniajšc wyœwietlanie na nim czytelnych znaków nawet o rozmiarze 8-punktów. Ekran komputera o minimalnej rozdzielczoœci 96 punktów na cal logiczny i rozmiarach 6,5 cala ma 640 pikseli w poziomie. Tyle dokładnie wynosi szerokoœć 8,5-calowej kartki papieru, z uwzględnieniem identycznych calowych marginesów po obu stronach. Dlatego też, stosujšc cal logiczny, na ekranie komputera można zmie- œcić tak wiele tekstu przy zachowaniu wystarczajšcej czytelnoœci. Jak być może przypominasz sobie z rozdziału 5, w Windows NT sprawa wyglš- da nieco inaczej. Wartoœć LOGPIXELSX (liczba pikseli na cal) otrzymywana po wywołaniu funkcji GetDeviceCaps nie jest równa wartoœci HORZREZ (w pikse- lach) podzielonej przez wartoœć HORZSIZE (w milimetrach), wszystko pomno- żone przez współczynnik 25,4. Analogicznie wartoœci LOGPIXELSY, VERTRES i VERTSIZE nie sš zgodne. Windows wykorzystuje wartoœci HORZREZ, HORZ- SIZE, VERTRES i VERTSIZE do wyznaczania przesunięć w różnych trybach od- wzorowania. Najrozsšdniejsze jest rozwišzanie, że program wyœwietlajšcy tekst na ekranie zakłada o jego rozdzielczoœci na podstawie wartoœci LOGPIXELSX i LOGPIXELSY. W Windows 98 jest to lepiej zorganizowane. Tak więc program pracujšcy w œrodowisku Windows NT nie powinien stosować trybów odwzorowania dostarczanych przez system w trakcie wyœwietlania tek- stu o rozmiarach okreœlonych w punktach. Powinien natomiast zdefiniować wła- sny tryb odwzorowania, oparty na wymiarach wyznaczanych na podstawie roz- dzielczoœci wyrażonej w pikselach na cal logiczny. Jednym z takich przydatnych trybów odwzorowania tekstu jest tzw. Logical Twips. Sposób jego ustalenia przed- stawiono poniżej: Rozdział 17: Tekst i czcionki SetMapMode (hdc, MM_ANISOTROPIC) ; SetWindowExtEx (hdc, 1440, 1440, NULL) ; SetViewportExt (hdc, GetDeviceCaps (hdc, LOGPIXELSX), GetDeviceCaps (hdc, LOGPIXELSY), NULL) ; Po ustawieniu takiego trybu odwzorowania możesz okreœlać rozmiar czcionki, podajšc rozmiary 20-krotnie większe - na przykład 240 zamiast 12 punktów. Za- uważ, że w przeciwieństwie do trybu odwzorowania MM TWIPS, zwiększajšc wartoœć y poruszamy się w dół ekranu. Ułatwia to wyœwietlanie kolejnych wier- ''; szy tekstu. Zapamiętaj, że różnica między długoœciš cala logicznego i cala rzeczywistego występuje wyłšcznie podczas wyœwietlania tekstu na ekranie. W drukarkach występuje całkowita zgodnoœć pomiędzy tymi dwoma wielkoœciami. Czcionka logiczna Po zapoznaniu się z ideš logicznego cala nadszedł czas na czcionki logiczne, obiek- ty GDI, których uchwyty przechowywane sš w zmiennej typu HFONT. Czcion- ka logiczna jest rodzajem opisu czcionki i podobnie jak logiczny pisak czy pędzel jest obiektem czysto abstrakcyjnym, który staje się rzeczywisty dopiero po jego wybraniu go w kontekœcie urzšdzenia w trakcie wywołania funkcji SelectObject. Na przykład możesz wybrać dowolny kolor pisaka logicznego, lecz po wybraniu pisaka w kontekœcie urzšdzenia Windows skonwertuje kolor na postać dostępnš dla sprzętu i dopiero wtedy może skorzystać z karty graficznej. Tworzenie i wybieranie czcionki logicznej Czcionkę logicznš tworzy się wywołujšc funkcję CreateFont lub CreateFontlndi- rect. Funkcja CreateFontlndirect przyjmuje jako argument wskaŸnik do struktury LOGFONT, zawierajšcej 14 pól. Funkcja CreateFont przyjmuje aż 14 argumentów identycznych z polami struktury LOGFONT. Tylko dwie wspomniane wyżej funk- cje służš do tworzenia czcionek logicznych. (Warto o tym wspomnieć, bo inne operacje na czcionkach mogš być realizowane przez wiele różnych funkcji). Po- nieważ trudno zapamiętać wszystkie 14 pól struktury, funkcja CreateFont jest uży- wana bardzo rzadko, skupimy więc uwagę na funkcji CreateFontlndirect. Sš trzy podstawowe metody zdefiniowania pól struktury LOGFONT przed wy- wołaniem funkcji CreateFontlndirect. ů Możesz przypisać poszczególnym polom okreœlone wartoœci. W takim wypadku, wywołujšc funkcję SelectObject, Windows wykorzystuje algorytm odwzorowania czcionek, dajšc w wyniku czcionkę, która najwiemiej odpowiada opisowi zawar- temu w polach struktury. W zależnoœci od tego, czy czcionka jest dostępna na ekranie bšdŸ drukarce, wynik może się znaczšco różnić od oczekiwań. ů Możesz wyœwietlić nazwy wszystkich czcionek dostępnych w systemie i wy- brać dowolnš nawet za pomocš okna dialogowego. Funkcje zajmujšce się uzy- skiwaniem wszystkich czcionek zostanš omówione w dalszej częœci rozdzia- łu. Niestety, nie sš zbyt często stosowane, ponieważ trzecia z opisanych me- tod wykonuje tę operację za ciebie. Częœć II: Grafika ů Możesz zastosować najprostsze podejœcie, wywohzjšc funkcję ChooseFont, któ- ra była omawiana w skrócie w rozdziale 11. W wyniku otrzymasz strukturę LOGFONT, którš możesz bezpoœrednio zastosować do utworzenia czcionki. W tym rozdziale zastosuję pierwszš i trzeciš metodę. Oto proces tworzenia, wybierania i usuwania czcionek logicznych: 1. Utwórz czcionkę logicznš, wywołujšc funkcję CreateFont lub CreateFontlndi- rect. Funkcje te zwracajš uchwyt logicznej czcionki typu HFONT. 2. Zaznacz czcionkę logicznš w kontekœcie urzšdzenia, wywohzjšc funkcję Se- lectObject. Windows wybierze czcionkę rzeczywistš najbardziej odpowiadajš- cš utworzonej czcionce logicznej. 3. Poznaj rozmiar i inne parametry czcionki rzeczywistej, wywołujšc funkcję Get- TextMetrics (być może również inne funkcje). Informacje te mogš być użytecz- ne do prawidłowego okreœlenia interlin tekstu pisanego czcionkš wybranš w kontekœcie urzšdzenia. 4. Po zakończeniu używania czcionki logicznej usuń jš, wywołujšc funkcję Dele- teObject. Nie usuwaj czcionki wybranej w ważnym kontekœcie urzšdzenia. Nie należy również usuwać czcionek zapasowych. Funkcja GetTextFace pozwala wyznaczyć nazwę kroju bieżšcej czcionki zaznaczo- nej w kontekœcie urzšdzenia: GetTextFace( hdc, sizeof((szFaceName / sizeof(TCHAR), szFaceName) ; Szczegółowš informację o własnoœciach czcionki można uzyskać wywołujšc funk- cję GetTextMetrics: GetTextMetrics (hdc, &textmetric) ; gdzie textmetric jest strukturš typu TEXTMETRIC zawierajšcš 20 pól. Pola struktur LOGFONT i TEXTMETlZIC zostanš krótko omówione w dalszej czę- œci rozdziału, na razie zapamiętaj, że LOGFONT służy do definiowania czcionki logicznej, a TEXTMETRIC do pozyskiwania informacji o bieżšcej czcionce wy- branej w kontekœcie urzšdzenia. Program PICKFONT Za pomocš programu PICKFONT przedstawionego na rysunku 17-1, możesz zde- finiować wiele pól struktury LOGFONT. Program tworzy czcionkę logicznš i wy- œwietla charakterystyki czcionki rzeczywistej po wybraniu czcionki logicznej w kontekœcie urzšdzenia. Jest przydatny do zrozumienia mechanizmu przyporzšd- kowania czcionki logicznej czcionce rzeczywistej. PICKFONT.C /* PICKFONT.C - Tworzy czcionkę logicznš (c) Charles Petzold, 1998 */ tfinclude ilinclude "resource.h" Rozdział 17: Tekst i czcionki 899 // Struktura, z której korzystajš wspólnie okno glówne i okno dialogowe typedef struct ( int iDevice. iMapMode ; BOOL fMatchAspect ; BOOL fAdvGraphics ; LOGFONT lf ; TEXTMETRIC tm : TCHAR szFaceName CLFţFULLFACESIZE] ; DLGPARAMS // Formatowanie pól typu BCHAR struktury TEXTMETRIC ţţifdef UNICODE ţţdefine BCHARFORM TEXT ("Ox%04X") ţţel se ţţdefine BCHARFORM TEXT ("Ox%02X") ţţendi f // Zmienne globalne HWND hdlg ; TCHAR szAppNameC] = TEXT ("PickFont") : // Deklaracje funkcji LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) : BOOL CALLBACK DlgProc (HWND UINT, WPARAM, LPARAM) ; void SetLogFontFromFields (HWND hdlg, DLGPARAMS * pdp> : void SetFieldsFromTextMetric (HWND hdlg, DLGPARAMS * pdp) : void MySetMapMode (HDC hdc, int iMapMode) : int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) HWND hwnd : MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CS UREDRAW : wndclass.lpfnWndProc = WndProc : wndclass.cbClsExtra = 0 : wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI ŽPPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC ARROW) wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITEţBRUSH) wndclass.lpszMenuName = szAppName ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) ( MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MBţICONERROR) ; return 0 ; 900 Częœć II Grafika I (cišg dalszy ze strony 899) ! hwnd = CreateWindow (szAppName; TEXT ("PickFont: Create Logical Font"), WS OVERLAPPEDWINDOW ţ WS_CLIPCHILDREN, CW_USEDEFAULT, CWţUSEDEFAULT, CW USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) f if (hdlg = 0 ţţ !IsDialogMessage (hdlg, &msg)) f TranslateMessage (&msg) ; DispatchMessage (&msg) ; I return msg.wParam ; ) LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) f static DLGPARAMS dp ; static TCHAR szText[) = TEXT ("\x41\x42\x43\x44\x45 ") TEXT ("\x61\x62\x63\x64\x65 ") TEXT ("\xC0\xCl\xC2\xC3\xC4\xC5 ") 1 llifdef UNICODE TEXT ("\xE0\xEl\xE2\xE3\xE4\xE5 ") 6 TEXT ("\x0390\x0391\x0392\x0393\x0394\x0395 ") TEXT ("\x0380\x0381\x03B2\x0383\x0384\x0385 ") TEXT ("\x0410\x0411\x0412\x0413\x0414\x0415 ") TEXT ("\x0430\x0433\x0432\x0433\x0434\x0435 ") TEXT ("\x5000\x5001\x5002\x5003\x5004") llendi f HDC hdc ; , ps ; PAINTSTRUCT RECT rect ; switch (message) ( case WM_CREATE: dp iDevice = IDMţDEVICEţSCREEN ; hdlg = CreateDialogParam (((LPCREATESTRUCT) lParam)->hInstance, return 0 ; szAppName, hwnd, DlgProc, (LPARAM) &dp) ; case WMţSETFOCUS: SetFocus (hdlg) ; return 0 ; Rozdział 17: Tekst i czcionki 901 case WM COMMAND: switch (LOWORD (wParam)) ( case IDM_DEVICE_SCREEN: case IDM_DEVICE_PRINTER: CheckMenuItem (GetMenu (hwnd), dp.iDevice, MF UNCHECKED) ; dp.iDevice = LOWORD (wParam) ; CheckMenuItem (GetMenu (hwnd), dp.iDevice, MF CNECKED) ; SendMessage (hwnd, WM COMMAND, IDOK, 0) ; return 0. ; J break ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; // Ustaw tryb graficzny SetGraphicsMode (hdc, dp.fAdvGraphics ? GM ŽDVANCED : GM COMPATIBLE) ; // Ustaw tryb odwzorowania i mapper MySetMapMode (hdc, dp.iMapMode) ; SetMapperFlags (hdc, dp.fMatchAspect) ; // ZnajdŸ punkt poczštkowy pisania tekstu GetClientRect (hdlg, &rect) ; rect.bottom += 1 ; DPtoLP (hdc, (PPOINT) &rect, 2) ; // Utwórz i wybierz czcionkę; wyœwietl tekst na ekranie SelectObject (hdc, CreateFontIndirect (&dp.lf)) ; TextOut (hdc, rect.left, rect.bottom, szText, lstrlen (szText)) ; Delete0bject (SelectObject (hdc, GetStockObject (SYSTEMţFONT))) ; EndPaint (hwnd, &ps) ; return 0 ; case WM DESTROY: PostOuitMessage (0) ; return 0 ; l return DefWindowProc (hwnd, message, wParam, lParam) ; 1 BOOL CALLBACK DlgProc (HWND hdlg, UINT message, WPARAM wParam, LPARAM lParam) * d static DLGPARAMS p p : static PRINTDLG pd = ( sizeof (PRINTDLG) ) ; HDC hdc0evice ; HFONT hFont ; switch (message) ( case WMţINITDIALOG: s 902 Częœć II: Grafika ! (cišg dalszy ze strony 901) // Zapisz wskaŸnik do struktury zwidzanej z parametrami dialogu w WndProc pdp = (DLGPARAMS *) lParam ; SendDlgItemMessage (hdlg, IDC_LF_FACENAME, EM_LIMITTEXT, LF FACESIZE - 1, 0) ; i CheckRadioButton (hdlg, IDC OUT DEFAULT, IDC OUT OUTLINE, IDC OUT DEFAULT) ; CheckRadioButton (hdlg, IDC DEFAULT OUALITY, IDCţPROOFţOUALITY, i IDC DEFAULT OUALITY) ; I CheckRadioButton (hdlg, IDC_DEFAULT_PITCH, IDC VARIABLE PITCH, IDCţDEFAULTţPITCH) ; CheckRadioButton (hdlg, IDC_FF_DONTCARE IDCţFFţDECORATIVE, ' IDCţFF DONTCARE) ; CheckRadioButton (hdlg, IDC_MM_TEXT, IDC MM LOGTWIPS, IDC MM TEXT) ; ; SendMessage (hdlg, WM COMMAND, IDOK, 0) ; // nie powiodło się case WM_SETFOCUS: SetFocus (GetOlgItem (hdlg, IDCţLF HEIGHT)) ; return FALSE ; case WM COMMAND: switch (LOWORD (wParam)) ( case IDCţCHARSET_HELP: MessageBox (hdlg, TEXT ("0 = Ansi\n") TEXT ("1 = Default\n") TEXT ("2 = Symbol\n") TEXT ("128 = Shift JIS (Japanese)\n") TEXT ("129 = Hangul (Korean)\n") TEXT ("130 = Johab (Korean)\n") TEXT ("134 = GB 2312 (Simplified Chinese)\n") TEXT ("136 = Chinese Big 5 (Traditional Chinese)\n") TEXT ("177 = Hebrew\n") TEXT ("178 = Arabic\n") TEXT ("161 = Greek\n") TEXT ("162 = Turkish\n") TEXT ("163 = Vietnamese\n") TEXT ("204 = Russian\n") TEXT ("222 = Thai\n") TEXT ("238 = East European\n") TEXT ("255 = OEM"), szAppName, MB OK ţ MBţICONINFORMATION) ; return TRUE ; // Te przyciski radiowe ustalajd wartoœć pola lfOutPrecision Rozdział 17: Tekst i czcionki 903 case IDC OUT_DEFAULT: pdp->lf.lfOutPrecision = OUT DEFAULTţPRECIS ; return TRUE ; case IDC_OUT_STRING: pdp->lf.lfOutPrecision = OUT STRINGţPRECIS ; return TRUE ; case IDC_OUT_CHARACTER: pdp->lf.lfOutPrecision = OUT CHARACTERţPRECIS : return TRUE ; case IDC OUT_STROKE: pdp->lf.lfOutPrecision = OUT STROKEţPRECIS : return TRUE ; case IDC_OUT_TT: pdp->lf.lfOutPrecision = OUT TTţPRECIS : " ` return TRUE ; case IDC_OUT_DEVICE: pdp->lf.lfOutPrecision = OUT DEVICEţPRECIS ; return TRUE ; case IDC_OUT_RASTER: pdp->lf.lfOutPrecision = OUTţRASTERţPRECIS ; return TRUE ; i: case IDC_OUT_TT_ONLY: pdp->lf.lfOutPrecision = OUT TT ONLYţPRECIS : return TRUE ; case IDC_OUT_OUTLINE: : pdp->lf.lfOutPrecision = OUT OUTLINEţPRECIS ; i return TRUE ; I // Te trzy przyciski radiowe ustalajd wartoœć pola lf0uality I case IDC_DEFAULT_OUALITY: ' pdp->lf.lf0uality = DEFAULT QUALITY ; return TRUE : ł case IDC_DRAFT_OUALITY: i pdp->lf.lf0uality = DRAFT QUALITY : return TRUE : case IDC_PROOF OUALITY: pdp->lf.lf0uality = PROOF OUALITY : return TRUE ; // Te trzy przyciski radiowe ustalajd wartoœć mniej // znaczacego bajtu pola lfPitchAndFamily case IDC_DEFAULT PITCH: pdp->lf.lfPitchAndFamily = ((OxFO & pdp->lf.lfPitchAndFamily) ţ DEFAULTţPITCH) : return TRUE ; s:. 904 Częœć II: Grafika (cigg dalszy ze strony 903) case IDC_FIXED_PITCH: pdp->lf.lfPitchAndFamily = (BYTE) ((OxFO & pdp->lf.lfPitchAndFamily) ţ FIXED PITCH) ; return TRUE ; case IDC VARIABLE_PITCH: pdp->lf.lfPitchAndFamily = (BYTE) ((OxFO & pdp->lf.lfPitchAndFamily) ţ VARIABLE PITCH) ; return TRUE ; // Te szeœć przycisków radiowych ustala wartoœć bardziej // znaczdcego bajtu pola lpPitchAndFamily case IDC_FF_DONTCARE: pdp->lf.lfPitchAndFamily = ((OxOF & pdp->lf.lfPitchAndFamily) ţ FF DONTCARE) ; return TRUE ; case IDCţFF_ROMAN: pdp->lf.lfPitchAndFamily = (BYTE) ((OxOF & pdp->lf.lfPitchAndFamily) ţ FF ROMAN) ; return TRUE ; case IDCţFF_SWISS: pdp->lf.lfPitchAndFamily = (BYTE) ((OxOF & pdp->lf.lfPitchAndFamily) ţ FF SWISS) ; return TRUE ; case IDCţFF_MODERN: pdp->lf.lfPitchAndFamily = (BYTE) ((OxOF & pdp->lf.lfPitchAndFamily) ţ FF MODERN) ; return TRUE ; case IDCţFF_SCRIPT: pdp->lf.lfPitchAndFamily = (BYTE) ((OxOF & pdp->lf.lfPitchAndFamily) ţ FF SCRIPT) ; return TRUE ; case IDCţFF_DECORATIVE: pdp->lf.lfPitchAndFamily = (BYTE) ((OxOF & pdp->lf.lfPitchAndFamily) ţ FF DECORATIVE) ; return TRUE ; // Tryb odwzorowania: case IDC_MM_TEXT: case IDC_MM_LOMETRIC: case IDC_MM_HIMETRIC: case IDC_MM_LOENGLISH: case IDC_MM HIENGLISH: case IDC MM TWIPS: case IDC MM LOGTWIPS: pdp->iMapMode = LOWORD (wParam) ; return TRUE ; // Naciœnięto przycisk OK // - Rozdział 17: Tekst i czcionki 905 case IDOK: // Pobierz strukturę LOGFONT SetLogFontFromFields (hdlg, pdp) ; // Ustaw znaczniki Match-Aspect and Advanced Graphics pdp->fMatchAspect = IsDlgButtonChecked (hdlg, IDC MATCH ŽSPECT) ; pdp->fAdvGraphics = IsDlgButtonChecked (hdlg, IDC ŽDVţGRAPHICS) ; I,'', // Pobierz kontekst informacyjny if (pdp->iDevice == IDMţDEVICE SCREEN) iţ hdc0evice = CreateIC (TEXT ("DISPLAY"), NULL, NULL, NULL) : 3 else ( pd.hwndOwner = hdlg ; pd.Flags = PDţRETURNDEFAULT ţ PDţRETURNIC ; pd.hOevNames = NULL ; pd.hDevMode = NULL ; PrintDlg (&pd) : ţ hdcDevice = pd.hDC : 1 1 // Ustaw tryb odwzorowania i znacznik mappera MySetMapMode (hdc0evice. pdp->iMapMode) : SetMapperFlags (hdcDevice, pdp->fMatchAspect) ; f/ Utwórz czcionkę i wybierz jd w kontekœcie urzšdzenia i hFont = CreateFontIndirect (&pdp->lf) ; SelectObject (hdcDevice, hFont) ; // Pobierz parametry czcionki i nazwę kroju i GetTextMetrics (hdcDevice, &pdp->tm) ; GetTextFace (hdcDevice, LFţFULLFACESIZE, pdp->szFaceName) : DeleteDC (hdc0evice) : " Delete0bject (hFont) ; // Uaktualnij pola okna dialogowego i unieważnij okno glówne SetFieldsFromTextMetric (hdlg, pdp) : InvalidateRect (GetParent (hdlg), NULL, TRUE) : return TRUE ; ... ) break ; ) return FALSE ; ) void SetLogFontFromFields (HWND hdlg, DLGPARAMS * pdp) ( pdp->lf.lfHeight = GetDlgItemInt (hdlg, IDC_LF HEIGHT, NULL, TRUE) ; pdp->lf.lfWidth = GetDlgItemInt (hdlg, IDCţLF WIDTH, NULL, TRUE) ; 906 Częœć II, Grafika (cigg dalszy ze strony 905) pdp->lf.lfEscapement = GetDlgItemInt (hdlg, IDC_LF_ESCAPE, NULL, TRUE) ; pdp->lf.lfOrientation = GetDlgItemInt (hdlg, IDC_LF_ORIENT, NULL, TRUE) ; pdp->lf.lfWeight = GetDlgItemInt (hdlg, IDC_LF_WEIGHT, NULL, TRUE) ; pdp->lf.lfCharSet = GetDlgItemInt (hdlg, IDCţLF CHARSET, NULL, FALSE) ; pdp->lf.lfItalic = IsDlgButtonChecked (hdlg, IDCţLF_ITALIC) == BST CHECKED ; pdp->lf.lfUnderline = IsDlgButtonChecked (hdlg, IDC LF UNDER) = BST CHECKED ; pdp->lf.lfStrike0ut = IsDlgButtonChecked (hdlg. IDCţLF STRIKE) == BST CHECKED ; GetOlgItemText (hdlg, IDCţLFţFACENAME, pdp->lf.lfFaceName, LFţFACESIZE) ; void SetFieldsFromTextMetric (HWND hdlg, DLGPARAMS * pdp) f TCHAR szBuffer [10] ; TCHAR * szYes = TEXT ("Yes") ; TCHAR * szNo = TEXT ("No") ; TCHAR * szFamily [] = ( TEXT ("Don't Know"), TEXT ("Roman"), TEXT ("Swiss"), TEXT ("Modern"), TEXT ("Script"), TEXT ("Decorative"), TEXT ("Undefined") ) ; SetDlgItemInt (hdlg, IDC_TM HEIGHT, pdp->tm.tmHeight, TRUE) ; SetOlgItemInt (hdlg, IDC_TM ASCENT, pdp->tm.tmAscent, TRUE) ; SetDlgItemInt (hdlg, IDC_TMţDESCENT, pdp->tm.tmDescent, TRUE) ; SetDlgItemInt (hdlg, IDC_TM_INTLEAD, pdp->tm.tmInternalLeading, TRUE) ; SetDlgItemInt (hdlg, IDC_TM_EXTLEAD, pdp->tm.tmExternalLeading, TRUE) ; SetDlgItemInt (hdlg, IDC_TM_AVECHAR, pdp->tm.tmAveCharWidth, TRUE) ; SetDlgItemInt (hdlg, IDC_TM_MAXCHAR, pdp->tm.tmMaxCharWidth, TRUE) ; SetDlgItemInt (hdlg, IDC_TM_WEIGHT, pdp->tm.tmWeight, TRUE) ; SetDlgItemInt (hdlg, IDC_TMţOVERHANG, pdp->tm.tmOverhang, TRUE) ; SetDlgItemInt (hdlg, IDC TM_DIGASPX, pdp->tm.tmDigitizedAspectX, TRUE) ; SetDlgItemInt (hdlg, IDC TMţDIGASPY, pdp->tm.tmDigitizedAspectY, TRUE) ; wsprintf (szBuffer, BCHARFORM, pdp->tm.tmFirstChar) ; SetDlgItemText (hdlg, IDC TMţFIRSTCHAR, szBuffer) ; wsprintf (szBuffer, BCHARFORM, pdp->tm.tmLastChar) ; SetDlgItemText (hdlg, IDC TM LASTCHAR, szBuffer) ; wsprintf (szBuffer, BCHARFORM, pdp->tm.tmDefaultChar) ; SetDlgItemText (hdlg, IDC TMţDEFCHAR, szBuffer) ; wsprintf (szBuffer, BCHARFORM, pdp->tm.tmBreakChar) ; SetDlgItemText (hdlg, IDC TM BREAKCHAR, szBuffer) ; SetDlgItemText (hdlg, IDC_TM_ITALIC, pdp->tm.tmItalic ? szYes : szNo) ; SetDlgItemText (hdlg, IDC TM_UNDER, pdp->tm.tmUnderlined ? szYes : szNo) ; SetDlgItemText (hdlg, IDC TMţSTRUCK, pdp->tm.tmStruck0ut ? szYes : szNo) ; SetDlgItemText (hdlg, IDC_TM_VARIABLE, TMPF FIXED PITCH & pdp->tm.tmPitchAndFamily ? szYes : szNo) ; SetDlgItemText (hdlg, IDC TM VECTOR, Rozdział 17: Tekst i czcionki TMPF VECTOR & pdp->tm.tmPitchAndFamily ? szYes : szNo) ; SetDlgItemText (hdlg, IDC_TM TRUETYPE, TMPF TRUETYPE & pdp->tm.tmPitchAndFamily ? szYes : szNo) ; SetDlgItemText (hdlg, IDC TM DEVICE, TMPFţDEVICE & pdp->tm.tmPitchAndFamily ? szYes : szNo) ; SetDlgItemText (hdlg, IDC TMţFAMILY, szFamily Cmin (6. pdp->tm.tmPitchAndFamily Ż 4)]) ; SetDlgItemInt (hdlg, IDC TM_CHARSET, pdp->tm.tmCharSet, FALSE) ; SetDlgItemText (hdlg, IDC TMţFACENAME, pdp->szFaceName) ; void MySetMapMode (HDC hdc, int iMapMode) ( switch (iMapMode) ( case IDC_MM_TEXT: SetMapMode (hdc, MM_TEXT) ; break ; case IDC_MM_LOMETRIC: SetMapMbde (hdc, MM_LOMETRIC) ; break ; case IDC_MM_HIMETRIC: SetMapMode (hdc, MM_HIMETRIC) ; break ; case IDC_MM_LOENGLISH: SetMapMode (hdc, MM_LOENGLISH) ; break ; case IDC_MM_HIENGLISH: SetMapMode (hdc, MM HIENGLISH) ; break ; case IDC_MM_TWIPS: SetMapMode (hdc, MM TWIPS) ; break ; case IDC_MM_LOGTWIPS: SetMapMode (hdc, MM ANISOTROPIC) ; SetWindowExtEx (hdc, 1440, 1440, NULL) ; SetUiewportExtEx (hdc, GetOeviceCaps (hdc, LOGPIXELSX), GetDeviceCaps (hdc, LOGPIXELSY), NULL) ; break ; PICKFONT.RC //Microsoft Developer Studio generated resource script. ţţinclude "resource.h" ţţinclude "afxres.h" ///////////////////////////////////////////////////////////////////////////// // Dialog PICKFONT DIALOG DISCARDABLE 0, 0, 348, 308 STYLE WS_CHILD ţ WS VISIBLE ţ WS BORDER FONT 8, "MS Sans Serif" BEGIN LTEXT "&Height:" IDC_STATIC,8,10,44 8 EDITTEXT IDCţLF_HEIGHT,64,8,24,12,ES_AUTOHSCROLL LTEXT "&Width",IDC_STATIC,8,26,44,8 EDITTEXT IDCţLF WIDTH,64,24,24,12,ES_AUTOHSCROLL LTEXT "Escapement:",IDC STATIC,8 42 44 8 EDITTEXT IDCţLF_ESCAPE,64,40,24,12,ES_AUTOHSCROLL LTEXT "Orientation:",IDC_STATIC,8 58 44 8 EDITTEXT IDC LF_ORIENT,64,56,24,12,ES AUTOHSCROLL LTEXT "Weight:",IDC STATIC,8,74,44,8 908 CZeœĆ II' Grafika (cišg dalszy ze strony 907) EDITTEXT IDCţLF_WEIGHT,64,74,24,12,ES_AUTOHSCROLL GROUPBOX "Mapping Mode",IDC_STATIC,97,3,96,90,WS_GROUP CONTROL "Text",IDC MM TEXT,"Button",BS AUTORADIOBUTTON,l04,13,56, 8 CONTROL "Low Metric",IDC MM LOMETRIC,"Button",BS AUTORADIOBUTTON, 104,24,56,8 CONTROL "High Metric",IDC_MM_HIMETRIC,"Button", BS_AUTORADIOBUTTON,l04,35,56,8 CONTROL "Low English",IDC_MM_LOENGLISH,"Button", BS AUTORADIOBUTTON,l04,46,56,8 CONTROL "High English",IDC_MM_HIENGLISH,"Button", BS_AUTORADIOBUTTON,l04,57,56,8 CONTROL "Twips",IDC MM TWIPS,"Button",BS_AUTORADIOBUTTON,l04,68, 56,8 CONTROL "Logical Twips",IDC_MMţLOGTWIPS,"Button", BS_AUTORADIOBUTTON,l04,79,64,8 CONTROL "Italic",IDC_LF_ITALIC,"Button",BS_AUTOCHECKBOX WS_TABSTOP,B,90,48,12 CONTROL "Underline",IDCţLF_UNDER,"Button",BS_AUTOCHECKBOX WS_TABSTOP,8,104,48,12 CONTROL "Strike Out",IDC_LF_STRIKE,"Button",BS_AUTOCHECKBOX WS_TABSTOP,8,118,48,12 CONTROL "Match Aspect",IDC_MATCH ASPECT,"Button",BS_AUTOCHECKBOX WS_TABSTOP,60,104,62,8 CONTROL "Adv Grfx Mode",IDC_ADV_GRAPHICS,"Button", BS AUTOCHECKBOX ţ WS_TABSTOP,60,118,62,8 LTEXT "Character Set:",IDC_STATIC,8.137,46,8 EDITTEXT IDC_LF_CHARSET,58,135,24,12,ES_AUTOHSCROLL PUSHBUTTON "?",IDC_CHARSET_HELP,90,135,14,14 GROUPBOX "Quality",IDC_STATIC,l32,98,62,48,WS_GROUP CONTROL "Default",IDC_DEFAULTţQUALITY,"Button", BS_AUTORADIOBUTTON,l36,110,40,8 CONTROL "Draft",IDC DRAFT „UALITY,"Button",BS_AUTORADIOBUTTON, 136,122,40,8 CONTROL "Proof",IDCţPROOF QUALITY,"Button",BS_AUTORADIOBUTTON, 136,134,40,8 LTEXT "Face Name:",IDC_STATIC,8,154,44,8 EDITTEXT IDCţLFţFACENAME,58,152,136,12,ES_AUTOHSCROLL GROUPBOX "Output Precision",IDC_STATIC,8,166,118,133,WS_GROUP CONTROL "OUTţDEFAULT_PRECIS",IDC_OUT_DEFAULT,"Button", BS_AUTORADIOBUTTON,l2,178,112,8 CONTROL "OUT STRING_PRECIS",IDC_OUT_STRING,"Button", BS_AUTORADIOBUTTON,l2,191,112,8 CONTROL "OUT CHARACTER_PRECIS",IDC_OUT_CHARACTER,"Button", BS_AUTORADIOBUTTON,l2,204,112,8 CONTROL "OUT STROKE_PRECIS",IDC_OUT_STROKE,"Button", BS_AUTORADIOBUTTON,l2,217,112,8 CONTROL "OUT TT PRECIS",IDC OUT TT,"Button",BS_AUTORADIOBUTTON, 12,230,112,8 CONTROL "OUTţDEVICE_PRECIS",IDC_OUT_DEVICE,"Button", BS_AUTORADIOBUTTON,l2,243,112,8 CONTROL "OUT RASTER_PRECIS",IDC_OUT_RASTER,"Button", BS_ŽUTORADIOBUTTON,l2,256,112,8 CONTROL "OUT TT ONLY_PRECIS",IDC_OUT_TT_ONLY,"Button", BS_AUTORADIOBUTTON,l2,269,112,8 CONTROL "OUT OUTLINE_PRECIS",IDC_OUT_OUTLINE,"Button", BS AUTORADIOBUTTON,l2,282,112,8 Rozdział 17: Tekst i czcionki 909 GROUPBOX "Pitch",IDC_STATIC,l32,166,62,50,WSţGROUP CONTROL "Default",IDCţDEFAULTţPITCH,"Button",BS ŽUTORADIOBUTTON, 137,176,52,8 CONTROL "Fixed",IDCţFIXEDţPITCH,"Button",BS AUTORADIOBUTTON,l37, 189,52,8 CONTROL "Variable",IDC VARIABLE_PITCH."Button", BS_AUTORADIOBUTTON,l37,203 52,8 GROUPBOX "Family",IDC_STATIC,l32,218,62,82,WS_GROUP CONTROL "Don't Care",IDCţFF DONTCARE,"Button",BS ŽUTORADIOBUTTON, 137.229,52.8 CONTROL "Roman".IDCţFF ROMAN,"Button",BS ŽUTORADIOBUTTON,l37,241, 52.8 CONTROL "Swiss",IDCţFF SWISS,"Button",BS AUTORADIOBUTTON,l37,253, 52.8 CONTROL "Modern",IDCţFF MODERN,"Button",BS ŽUTORADIOBUTTON,l37, 265,52,8 CONTROL "Script",IDCţFF SCRIPT,"Button",BS ŽUTORADIOBUTTON,l37, 277.52,8 CONTROL "Decorative",IDC_FF_DECORATIVE,"Button", BS_AUTORADIOBUTTON.l37,289,52,8 DEFPUSHBUTTON "OK",IDOK,247,286,50,14 GROUPBOX "Text Metrics",IDC_STATIC,201,2,140,272,WSţGROUP LTEXT "Height:",IDC_STATIC,207,12,64,8 LTEXT "0",IDC_TM_HEIGHT.281,12,44,8 LTEXT "Ascent:",IDC_STATIC.207,22,64,8 LTEXT "0",IDC TM_ASCENT,281,22,44,8 LTEXT "Descent:",IDC_STATIC,207,32,64,8 LTEXT "0",IDC_TM_DESCENT,281,32,44,8 LTEXT "Internal Leading:".IDC_STATIC,207,42,64,8 LTEXT "0",IDC_TM INTLEAD,281,42,44,8 LTEXT "External Leading:",IDC_STATIC,207,52,64,8 LTEXT "0",IDC_TM_EXTLEAD,281,52,44,8 LTEXT "Ave Char Width:",IDC_STATIC,207,62,64,8 LTEXT "0",IDC_TM_AVECHAR,281,62,44,8 LTEXT "Max Char Width:",IDC_STATIC,207,72,64,8 LTEXT "0",IDC TM MAXCHAR.281.72,44,8 LTEXT "Weight:",IDC_STATIC,207,82,64,8 LTEXT "0",IDC TM WEIGHT,281,82,44,8 LTEXT "Overhang:",IDC_STATIC,207,92,64,8 LTEXT "0",IDC_TM_OVERHANG,281,92,44,8 LTEXT "Digitized Aspect X:",IOC_STATIC,207,102,64,8 LTEXT "0",IDC_TM_DIGASPX,281,102,44,8 LTEXT "Digitized Aspect Y:",IDC_STATIC,207,112,64.8 LTEXT "0",IDC_TM DIGASPY,281,112,44,8 LTEXT "First Char:",IDC_STATIC,207,122,64,8 LTEXT "0",IDC_TM FIRSTCHAR,281,122,44,8 LTEXT "Last Char:",IDC_STATIC.207,132,64,8 LTEXT "0",IDC_TM_LASTCHAR,281,132,44,8 LTEXT "Default Char:",IDC_STATIC,207,142,64,8 ; LTEXT "0",IDC_TMţDEFCHAR,281,142,44,8 LTEXT "Break Char:",IDC_STATIC,207,i52,64,8 LTEXT "0",IDC TM_BREAKCHAR,281,152,44,8 LTEXT "Italic?",IDC_STATIC,207,162,64,8 LTEXT "0",IDC TM_ITALIC,281,162,44,8 LTEXT "Underlined?",IDC_STATIC,207,172.64,8 LTEXT "0",IDC_TM_UNDER,281,172,44,8 LTEXT "Struck Out?",IDC_STATIC,207,182,64,8 LTEXT "0",IDC TM STRUCK,281,182,44,8 ţzęœć II: Grafika 910 (cigg dalszy ze strony 909) "Variable Pitch?".IDC STATIC,207.192.64,E LTEXT "0".IDC TM `dARIABLE.281.192.44,8 LTEXT - - LTEXT "Vector Font?" IDC-STATIC.207,202.64,8 LTEXT "0".IDC TM VECTOR 281.202,44,8 LTEXT "TrueType Font?",IDC STATIC,207,212.64,8 LTEXT "0",IDC TM TRUETYPE,281.212,44.8 LTEXT "Device Font?".IDC STATIC,207.222.64,8 LTEXT "0".IDC TM DEVICE,281 222.44,8 LTEXT "Family:".IDC-STATIC 207.232,64,8 LTEXT "0",IDC TM_FAMILY.281,232.44,8 LTEXT "Character Set:".IDC STATIC,207.242,64,8 LTEXT "0",IDC TM CHARSET,281,242,44 8 LTEXT "0",IDC TM-FACENAME,207,262,128,8 END ///////////////////////////////////////////////////////////////////////////// // Menu PICKFONT MENU DISCARDABLE BEGIN POPUP "&Device" BEGIN MENUITEM "&Screen". IDM DEVICĘ SCREEN, CHECKED MENUITEM "&Printer", IDM DEVICĘ PRINTER END END RESOURCE.H // Microsoft Developer Studio generated include file. // Used by PickFont.rc ţţdefine IDC-LF HEIGHT 1000 ţţdefine IDC-LF WIDTH 1001 ţţdefine IDC-LF-ESCAPE 1002 ţţdefine IDCţLF ORIENT 1003 4tdefine IDC-LF WEIGHT 1004 ţtdefine IDC MM_TEXT 1005 tţdefine IDĆ MM-LOMETRIC 1006 tpdefine IDC MM HIMETRIC 1007 ţţdefine IDC MM LOENGLISH 1008 ţpdefine IDĆ MM HIENGLISH 1009 ţţdefine IDC MM TWIPS 1010 4fdefine IDC MM-LOGTWIPS 1011 tţdefine IDC-LF-ITALIC 1012 tţdefine IDCţLF UNDER 1013 tţdefine IDC-LF STRIKE 1014 tţdefine IDC MATCH ŽSPECT 1015. tţdefine IDC ADV GRAPHICS 1016 ţţdefine IDC-LF CHARSET 1017 ttdefine IDC CHARSET HELP 1018 ţtdefine IDC DEFAULT OUALITY 1019 ţpdefine IDC DRAFT ţUALITY 1020 ţţdefine IDC-PROOF „UALITY 1021 tpdefine IDC-LF-FACENAME 1022 ttdefine IDC OUT-DEFAULT 1023 Rozdział 17: Tekst i czcionki g 4ţdefine IDC_OUT_STRING 1024 ţţdefine IDC_OUT_CHARACTER 1025 ţţdefine IDC_OUT_STROKE 1026 ţţdefine IDC_OUT_TT 1027 ţţdefine IDC_OUT_DEVICE 1028 ţţdefine IDC_OUT_RASTER 1029 ţţd2fine IDC_OUT_TT_ONLY 1030 ţţdefine IDC_OUT_OUTLINE 1031 ţţdefine IDC_DEFAULT_PITCH 1032 ţţdefine IDC_FIXED_PITCH 1033 ţţdefine IDC_VARIABLE_PITCH 1034 4ţdefine IDC_FF_DONTCARE 1035 ţţdefine IDC_FF_ROMAN 1036 ţţdefine IDC_FF_SWISS 1037 ţţdefine IDC_FF_MODERN 1038 ţţdefine IDC_FF_SCRIPT 1039 ţţdefine IDC_FF_DECORATIVE 1040 ţţdefine IDC_TM_HEIGHT 1041 ţţdefine IDC_TM_ASCENT 1042 ţţdefine IDC_TM_DESCENT 1043 ţţdefine IDC_TM_INTLEAD 1044 ţţdefine IDC_TM_EXTLEAD 1045 ţţdefine IDC_TM_AVECHAR ţ1046 ţţdefine IDC_TM_MAXCHAR 1047 ţţdefine IDC_TM_WEIGHT 1048 ţţdefine IDC_TM_OVERHANG 1049 4ţdefine IDC_TM_DIGASPX 1050 ţddefine IDC_TM_DIGASPY 1051 ţţdefine IDC_TM_FIRSTCHAR 1052 ţţdefine IDC_TM_LASTCHAR 1053 ţţdefine IDC_TM_DEFCHAR 1054 ţfdefine IDC_TM_BREAKCHAR 1055 ţţdefine IDC_TM_ITALIC 1056 ţţdefine IDC_TM_UNDER 1057 ţţdefine IDC_TM_STRUCK 1058 4ţdefinę IDC_TM_VARIABLE 1059 ţţdefine IDC_TM_VECTOR 1060 ţţdefine IDC_TM_TRUETYPE 1061 ţţdefine IDC_TM_DEVICE 1062 łţdefine IDC_TM_FAMILY 1063 ţţdefine IDC_TM_CHARSET 1064 ţţdefine IDC_TM_FACENAME 1065 ţţdefine IDM_DEVICE_SCREEN 40001 ţddefine IDM DEVICEţPRINTER 40002 Rysunek 17-1. Program PICKFONT Rysunek 17-2 przedstawia typowy wynik działania programu PICKFONT. Lewš stronę ekranu zajmuje okno dialogowe, dzigki któremu można wypełnić więk- szoœć pól struktury zwišzanej z czcionkš logicznš. Prawa strona okna dialogo- wego demonstruje rezultaty działania funkcji GetTextMetrics po wybraniu czcionki w kontekœcie urzšdzenia. Poniżej program wyœwietla łańcuch znaków zapisany za pomocš tej czcionki. Ze względu na duże rozmiary okna dialogowego najko- rzystniej jest uruchomić program w trybie graficznym o rozdzielczoœci co najmruej 1024 na 768. T Częœć II: Grafika Rysunek 17-2. Typowy wyglšd ekranu w trakcie działania programu PICKFONT (wersja Unicode w systemie Windows NT) Okno dialogowe zawiera ponadto kilka opcji nie powišzanych z polami struktu- ry czcionki logicznej. Sš to tryby odwzorowania, w tym mój tryb Logical Twips; opcja Match Aspect, dzięki której Windows dopasowuje czcionkę logicznš do rze- czywistej, oraz opcja Adv Grfx Mode, która jest odpowiedzialna za ustawienie zaawansowanego trybu graficznego w Windows NT. Opcje te zostanš omówio- ne bardziej szczegółowo w dalszej częœci rozdziału. Z menu Device możesz wybrać domyœlnš drukarkę zamiast ekranu w roli urzš- dzenia wyjœciowego. Program PICKFONT wybiera wówczas czcionkę logicznš w kontekœcie urzšdzenia (drukarki) i wyœwietla zawartoœć struktury TEX'TME- TKIC zwišzanej z tš właœnie drukarkš. Następnie wybiera czcionkę logicznš w kontekœcie urzšdzenia okna, aby wyœwietlić przykładowy cišg znaków. Dlate- go tekst pod oknem dialogowym może być wyœwietlany innš czcionkš (czcionkš ekranowš) niż ta, którš opisujš pola struktury TEXTMETRIC (jest to czcionka wbudowana drukarki). Większoœć kodu programu PICKFONT zajmuje obsługa okna dialogowego, więc nie ma sensu opisywać jego szczegółów. Zamiast tego wyjaœnię, co należy zrobić po utworzeniu i wybraniu czcionki logicznej. Struktura czcionki logicznej Chcšc utworzyć czcionkę logicznš, możesz wywołać funkcję CreateFont, majšcš 14 argumentów. Oczywiœcie łatwiej jest zdefiniować strukturę typu LOGFONT: LOGFONT lf ; a następnie kolejne jej pola. Potem wystarczy wywołać funkcję CreateFontlndirect, przyjmujšcš jako argument wskaŸnik do tej struktury: hFont = CreateFontIndirect(&lf) ; Rozdział 17: Tekst i czcionki 913 ; Nie musisz ustalać wartoœci wszystkich pól struktury LOGFONT. Jeœli jest ona zdefiniowana jako zmienna statyczna, wszystkie pola zostanš automatycznie zainicjowane wartoœciš 0. Zwykle wartoœć 0 jest przyjmowana jako domyœlna. W taki sposób możesz korzystać ze struktury, nie dokonujšc w niej wielu mody- fikacji. Okreœlajšc tak przygotowanš strukturę jako argument funkcji CreateFon- tlndirect uzyskasz uchwyt utworzonej czcionki. Jeœli teraz wybierzesz czcionkę w kontekœcie urzšdzenia, uzyskasz domyœlnš czcionkę o całkiem sensownym wy- glšdzie. Wypełniajšc strukturę LOGFONT wartoœciami, możesz być tyleż dokład- ny, co nierozważny, gdyż system Windows zatroszczy się o jak najwierniejsze do- pasowarue czcionki rzeczywistej do twych oczekiwań. Warto, byœ wypróbował działanie każdego z omawianych pól struktury LOG- FONT, uruchamiajšc program PICKFONT. Po dokonaniu jakichkolwiek zmian zawsze naciskaj klawisz [Enter] lub przycisk OK. Pierwsze dwa pola struktury LOGFONT sš podawane w jednostkach logicznych, więc ich wartoœć zależy od bieżšcych ustawień trybu odwzorowania: ů lţleight Jest to pożšdana wysokoœć znaków, wyrażona w jednostkach logicz- nych. Aby uzyskać wielkoœć domyœlnš, możesz przypisać tej zmiennej war- toœć 0. Możesz rówrueż nadać jej wartoœć dodatniš bšdŸ ujemnš w zależnoœci ( od tego, co chcesz uzyskać. Jeœli zastosujesz wartoœć dodatniš, wyrażona licz- s bš wysokoœć czcionki będzie zawierać wewnętrznš interlinię (bez interlin zewnętrznej). W efekcie otrzymasz czcionkę wyglšdajšcš korzystnie, wraz z interliniš o szerokoœci równej lfHeight. Jeœli natomiast jako zmiennš lfHeight wprowadzisz liczbę ujemnš, Windows użyje wartoœci bezwzględnej tej liczby i utworzy czcionkę o żšdanej wysokoœci wyrażonej w punktach. Pamiętaj o ważnej różnicy: chcšc otrzymać czcionkę o okreœlonej wysokoœci, zamień wy- sokoœć podanš w punktach na jednostki logiczne, a następnie nadaj polu lfHe- ight wartoœć ujemnš o tej wielkoœci. Jeœli zmiennej IţIeight nadasz wartoœć dodatniš, pole tmHeight wynikowej struktury TEXTMETRIC będzie zawierać dokładnie tę wartoœć. (Czasami może między nimi być niewielka różnica, wy- nikajšca prawdopodobnie z zaokršgleń.) Jeœli pole lfHeight przyjmuje wartoœć ujemnš - będzie ono odpowiadać różnicy wartoœci pól tmHeight oraz tmlnter- leading struktury TEXTMETIZIC. ů lfWidth Jest to pożšdana szerokoœć znaków, wyrażona w jednostkach logicz- nych. Przeważnie będziesz chciał pozostawić wartoœć 0 tego pola, pozwalajšc systemowi Windows na wybranie odpowiedniej czcionki tylko na podstawie jej wysokoœci. Zastosowarue wartoœci niezerowych nie daje dobrych wyników dla czcionek rastrowych. W wypadku czcionek TrueType wartoœci lf 4Vidth różne od zera mogš być stosowane w celu uzyskiwania czcionek o znakach szer- szych lub węższych niż tradycyjne. Zmienna ta odpowiada polu tmAveChar Width struktury TEXTMETRIC. Zastosuj lfYVidth inteligentnie - najpierw na- dal lel wartoœć 0, utwórz czcionkę logicznš, wybierz jš w kontekœcie urzšdze- nia, a następnie wywołaj funkcje GetTextMetrics. Spróbuj dostroić wartoœć pola tmAveCharWidth, zmniejszajšc i zwiększajšc jš procentowo. Teraz spróbuj utwo- rzyć drugš czcionkę na podstawie wartoœci tmAveCharWidth. Kolejne dwa pola okreœlajš zakrzywienie i orientację tekstu. Teoretycznie zmien- na IfEscapement pozwala na umieszczanie łańcucha pisanego w kształcie łuku (jed- 914 Częœć II: Grafika nak linia każdej z jego liter pozostaje w poziomie), a lfOrientation umożliwia od- chylenie poszczególnych liter od poziomu. Niestety, pola te nigdy nie ftxnkcjono- wały w opisywany sposób. Nawet dzisiaj ich działanie nie jest zgodne z zamie- rzeniami poza jednym wypadkiem: gdy stosujesz czcionkę TrueType w œrodowi- sku Windows NT i jako pierwszš wywołujesz ftxnkcję SetGraphicsMode z usta- wionym znacznikiem CM ADVANCED. Ostatnie założenie może być spełnione w programie PICKFONT przez zaznaczerue pola wyboru opcji Adv Grfx Mode. Eksperymentujšc z wartoœciami opisywanych pól pamiętaj, że podstawowa jed- nostka to dziesięć stopni, a kierunek rotacji jest zgodny z ruchem wskazówek zegara. Niestety, łatwo podać wartoœci, które spowodujš zniknięcie tekstu Z tego powodu stosuj wartoœci z przedziału od 0 do - 600 lub między 3000 i 3600. ů lfEscapement Jest to kšt podawany w dziesištkach stopni, mierzony od pozio- mu w kierunku zgodnym z ruchem wskazówek zegara. Okreœla położenie kolejnych liter tekstu w trakcie jego pisania. Oto kilka przykładów: Wartoœć Położenie znaków Od lewej do prawej (domyœlne) 900 Z dołu do góry 1800 Od lewej do prawej 2700 Z góry do dołu W Windows 98 wartoœć ta decyduje zarówno o zakrzywieniu, jak i orientacji tekstu pisanego czcionkš TrueType. W Windows NT wartoœć lfEscapement jest odpowiedzialna także za oba atrybuty tekstu, lecz w wypadku wywołania funkcji SetGraphicsMode z argumentem GM ADVANCED, IfEscapement funk- cjonuje zgodnie z dokumentacjš. lfOrientation Jest to kšt podawany w dziesištkach stopni, mierzony od pozš- mu w kierunku zgodnym z ruchem wskazówek zegara. Wpływa na wygl d każdego znaku z osobna. Oto kilka przykładów: Wartoœć Położenie znaków Normalnie (domyœlnie) 900 Przekręcone o 90 stopni w prawo 1800 Odwrotnie 2700 Przekręcone o 90 stopni w lewo Wartoœć tego pola nie ma znaczenia poza pracš w WindOws NT w trybie gra- ficznym GM ŽDVANCED. Pozostałe 10 pól opisano poniżej: lf iNeight Pozwala okreœlić stopień pogrubiania tekstu. Plik nagłówkowy WINGDI.H definiuje zestaw stałych, których wartoœci mogš być przyjmowane przez to pole: Rozdział 17: Tekst i czcionki Wartoœć Identyfikator stałej FW DONTCARE 100 FW THIN 200 FWţEXTRALIGHT lub FWţULTRALIGHT 300 FWţLIGHT 400 FWţNORMAL lub FW REGULAR 500 F4VţMEDIUM 600 FWţSEMIBOLD lub FW DEMIBOLD 700 FW BOLD 800 FW EXTRABOLD lub FWţULTRABOLD 900 FWţHEAVY lub FW BLACK Przedstawiona powyżej tabela ma najbardziej złożonš budowę ze znanych mi tego typu. W rzeczywistoœci wykorzystywane sš zwykle tylko wartoœci 0 lub 400, definiujšce czcionkę zwykłš, albo 700, gdy chcemy nadać jej atrybut po- grubienia. ů Ijitalic Wartoœć niezerowa tego pola jest równoznaczna z nadaruem czcionce atrybutu kursywy. Kursywa może być symulowana przez Windows dla czcio- nek rastrowych GDI - system przesuwa górne wiersze bitmapy w prawo. W wypadku czcionek TrueType Windows stosuje właœciwš kursywę lub pochy- łš wersję danej czcionki. ů Ifllnderline Nadanie temu polu wartoœci ruezerowej oznacza zastosowanie podkreœlenia, które jest uzyskiwane w sposób sztuczny dla czcionek GDI - Windows rysuje poziome linie pod każdym znakiem tekstu, włšczajšc w to spacje. ů lfStrike0ut Wartoœć ruezerowa tego pola jest równoznaczna z przekreœleniem danej czcionki. Atrybut jest również uzyskiwany metodš syntetycznš przez GDI. ů lfCharSet Jest to wartoœć bajtowa, okreœlajšca zestaw znaków, do którego ma należeć czcionka. Więcej informacji o tym polu przedstawię w następnym podrozdziale, "Zestawy znaków i Unicode". W programie PICKFONT mo- żesz uzyskać listę kodów dostępnych zestawów znaków, naciskajšc przycisk ze znakiem zapytarua. Zauważ, że w wypadku pola ţharSetjest wartoœć zerowa nie oznacza domyœl- nej. Zero przyporzšdkowane jest wartoœci ANSI CHARSET, czyli zestawowi znaków ANSI stosowanemu w Stanach Zjednoczonych i Europie Zachodniej. Kodem domyœlnego zestawu znaków jest 1 i oznacza domyœlny zestaw zna- ków dla komputera, na którym uruchomiany jest program. IfOutPrecision Okreœla, jak system Windows ma podjšć próbę dopasowania zadanych rozmiarów i innych atrybutów czcionki do czcionki rzeczywistej. Jest to dosyć skomplikowana procedura, dlatego też prawdopodobnie nie bę- dziesz korzystać z tego pola. Więcej szczegółów znajdziesz w dokumentacji 916 Częœć 11: Grafika struktury LOGFONT. Zauważ, że możesz zastosować znacznik OUT TT ON- LY PRECIS, aby mieć pewnoœć, że otrzymana czcionka jest zawsze typu True- Type. lfClipPrecision Pole to okreœla, jak majš być obcinane znaki znajdujšce się czę- œciowo poza obszarem roboczym. Zmienna jest rzadko używana i nie została zastosowana w programie PICKFONT. IfQuality Stanowi instrukcję dla Windows, dotyczšcš dopasowania czcionki o okre- œlonych parametrach do rzeczywistej. Wartoœć tego pola ma znaczenie jedynie dla czcionek rastrowych i nie dotyczy TrueType. Znacznik DRAFT QUALTTY infor- muje, że GDI musi przeskalować czcionkę rastrowš tak, aby otrzymać wymaga- ne przez użytkownika programu rozmiary. Znacznik PROOF QUALTTY ozna- cza brak skalowania. Zastosowanie go sprawia, że czcionki sš ładniejsze, lecz mogš być mniejsze, niż oczekujesz. Pole to zostanie prawdopodobnie wypełnione war- toœciš domyœlnš DEFAULT QUALTTY, którš jest oczywiœcie 0. IfPitchAndFamily Ten bajt złożony jest z dwóch częœci. Aby połšczyć obie war- toœci mieszczšce się w tym polu, można zastosować logicznš operację OR ję- zyka C. Najmniej znaczšce dwa bity okreœlajš, czy czcionka ma stałš czy zmien- nš szerokoœć (tzn. czy szerokoœć wszystkich znaków czcionki jest stała). Wartoœć Identyfikator stałej DEFAULT PTTCH FIXED PITCH VARIABLE PITCH Bardziej znaczšca częœć bajtu okreœla rodzinę, do której należy czcionka: Wartoœć Identyfikator stałej 0x00 FW DONTCARE 0x10 FFţROMAN (zmienna szerokoœć czcionki, szeryfy) 0x20 FF SWISS (zmienna szerokoœć czcionki, brak szeryfów) 0x30 FFţMODERN (stała szerokoœć czcionki) 0x40 FFţSCRIPT (symulowane pismo ręczne) 0x50 FF DECORATIVE lţF'aceName Rzeczywista nazwa kroju pisma (taka jak Courier, Arial lub Times New Roman). Pole to jest tablicš bajtów o długoœci okreœlanej wartoœciš para- metru LF FACESIZE (32 znaki). Uzyskanie czcionki TrueType pogrubionej lub kursywy można zrealizować na dwa sposoby: używajšc pełnej nazwy kroju (na przykład Times New Roman Italic) bšdŸ też nazwy podstawowej (tzn. Times New Roman), a następnie ustalajšc odpowiednio wartoœć pola Ifltalic. Rozdział 17: Tekst i czcionki Algorytm odwzorowania czcionek Po zainicjowaniu wartoœci pól struktury wywohzjesz funkcję CreateFontlndirect, aby uzyskać uchwyt czcionki logicznej. Po wywołaruu funkcji SelectObject w celu wybrania czcionki w kontekœcie urzšdzenia, Windows znajduje czcionkę, która w największym stopniu odpowiada oczekiwaniom użytkownika. Polega to na wykonaniu algorytmu odwzorowania czcionek. Pewne pola struktury opisujšcej czcionkę logicznš sš ważruejsze niż inne. Najlepszym sposobem zapoznarua się z mechanizmem odwzorowania czcionek jest przeprowadzenie kilku eksperymentów za pomocš programu PICKFONT. Oto kilka podstawowych zasad: ů Pole lfCharSet (zestaw znaków) jest bardzo istotne. Kiedyœ po okreœleniu pa- rametru OEM CHARSET (kod 255) mogłeœ otrzymać albo czcionkę wektoro- wš, albo czcionkę o kroju Terminal, ponieważ tylko one były dostępne w ze- stawie znaków OEM. Obecnie dzięki zaletom czcionek TrueType (omówionym już w tej ksišżce) pojedyncza czcionka TrueType może być przypisana róż- nym zestawom znaków, w tym zestawowi OEM. Jeœli chcesz uzyskać czcion- kę Symbol lub Wingdings, zastosuj parametr SYMBOL CHARSET (kod 2). ů Wartoœć szerokoœci czcionki, za którš odpowiada parametr FIXED PITCH pola IjPitchAndFamily, jest równoznaczna z wydaniem Windows polecenia, że nie chcesz mieć do czyruenia z czcionkami o zmiennej szerokoœci znaków. ů Pole lfFaceName jest odpowiedzialne za dokładne okreœlenie kroju pożšdanej czcionki. Gdy pozostawisz w nim wartoœć NULL i ustalisz innš niż FF DONT CARE wartoœć zwišzanego z rodzinš czcionki pola lfPitchAndFamily, lfFace- Name stanie się o tyle ważne, że jego wartoœć zdecyduje o wyborze rodziny czcionki. ů W wypadku czcionek rastrowych Windows spróbuje dopasować wartoœć lfHe- ight, nawet jeœli trzeba będzie zwiększyć rozmiar mniejszej czcionki. Wyso- koœć uzyskanej czcionki rzeczywistej będzie zawsze mniejsza, a co najwyżej równa zamierzonej, chyba że rue istnieje czcionka na tyle mała, aby sprostać wymaganiom użytkownika. W wypadku czcionek wektowowych oraz Tru- eType system po prostu skaiuje je do pożšdanych rozmiarów. ů Windows nie będzie skalować czcionek rastrowych, jeœli polu lfQuality nadasz wartoœć PROOFţQUALITY. W ten sposób wydasz polecenie przedkładania wyglšdu czcionki nad jej dopasowanie do pożšdanych rozmiarów. ů Jeœli okreœlisz takie wartoœci pól lfHeight i ljWeight, które nie odpowiadajš da- nym proporcjom ekranu, system będzie próbował odnaleŸć czcionkę rastro- wš najbardziej zbliżonš (pod względem proporcji) do czcionki o zadanych rozmiarach. Taki sposób był niegdyœ stosowany jako sztuczka do otrzymania czcionki "grubszej" lub "cieńszej" niż standardowa. Takie zabiegi nie sš, oczy- wiœcie koreczne w przypadku czcionek TrueType. Generalrue na pewno chciał- byœ uniknšć kłopotów z dopasowaniem wyglšdu czcionki dla urzšdzerua wyj- œciowego o innych parametrach. Program PICKFONT przewiduje takš możli- woœć po wybraniu opcji Match Aspect, co powoduje wywołanie funkcji Set- MapperFlags z argumentem TRUE. Częœć II: Grafika Uzyskiwanie informacji o czcionce Po prawej strorue okna dialogowego programu PICKFONT znajduje się informa- cja uzyskana za pomocš funkcji GetTextMetrics po wybraniu czcionki w kontek- œcie urzšdzerua. Zauważ, że możesz korzystać z menu urzšdzeń majšc do wybo- ru ekran bšdŸ domyœlnš drukarkę. Wynik działania programu może się różnić, jeœli drukarka ma wbudowane inne czcionki. Ostatniš pozycjš listy jest nazwa kroju dostępna po wywołaniu tunkcji GetTextFace. Wartoœci poszczególnych rozmiarów, które Windows kopiuje do struktury TEXT METRIC, sš podawane w jednostkach logicznych, z wyjštkiem współczynników proporcji. Oto pola struktury TEXTMETRICS: ů tmHeight Wysokoœć znaku w jednostkach logicznych. Wartoœć ta powinna być zbliżona do lfHeight, okreœlonego w strukturze LOGFONT, jeœli jest ona do- datnia (wtedy okreœla raczej szerokoœć odstępu między wierszami, a nie fak- tyczny rozmiar czcionki mierzony w punktach). Jeœli natomiast wartoœć pola IfHeight struktury LOGFONT jest ujemna, różnica tmHeight i tmlnternalLeading powinna być równa bezwzględnej wartoœci pola lfHeight. ů tmAscent Podany w jednostkach logicznych rozmiar wydłużenia górnego, czyli częœci litery ponad liniš tekstu. ů tmDescent Podany w jednostkach logicznych rozmiar wydłużenia dolnego, czyli częœci litery poniżej lin tekstu. ů tmlnternaLeading Szerokoœć (zawierajšca się w tmHeight) przestrzeni zajmowa- nej zwykle przez znaki diakrytyczne stosowane w niektórych wielkich lite- rach. Ponownie możesz obliczyć rozmiar punktowy czcionki odejmujšc war- toœć pola tmInternalLeading od tmHeight. ů tmExternalLeading Dodatkowa przestrzeń między kolejnymi wierszami tekstu. ů tmAveCharWidth Œrednia szerokoœć małych liter danej czcionki. ů TmMaxCharWidth Szerokoœć najszerszego znaku dariej czcionki mierzona w jed- nostkach logicznych. W wypadku czcionek o stałej szerokoœci wartoœć ta jest równa tmAveCharWidth ů TmWeight Gruboœć kresek czcionki w zakresie od 0 do 999. W rzeczywistoœci pole przyjmuje wartoœć 400 w wypadku czcionki normalnej, a 700 - pogrubionej. ů TmOverhang Rozmiar dodatkowej odległoœci między znakami czcionki rastro- wej stosowanej przez Windows w celu symulowarua efektu kursywy lub po- grubienia. Kiedy czcionka rastrowa ulega pochyleruu, wartoœć pola tmAveChar- Width nie zmienia się, ponieważ nie zmienia się długoœć łańcucha znaków zapisanego kursywš w porównaniu z długoœciš tego samego łańcucha zapi- sanego czcionkš normalnš. W wypadku pogrubienia Windows musi nieco powiększyć szerokoœć każdego znaku. Wartoœć pola tmAveCharWidth pomruej- szona o tmOverhang jest równa wartoœci tmAveCharWidth dla tej samej czcion- ki bez pogrubierua. ů tmDigitizedAspectX i tmDigitizedAspectY Współczynruki skalowania właœciwego wyglšdu czcionki. Sš to odpowiedniki wartoœci otrzymanych po wywołaniu funkcji GetDeviceCaps z argumentami LOGPIXELSX i LOGPIXELSY. Rozdział 17: Tekst i czcionki g1g ů tmFirstChar Kod pierwszego znaku danej czcionki. ů tmLastChar Kod ostatniego znaku czcionki. Jeżeli strukturę TEXTMETRIC otrzymano po wywołaniu GetTextMetricsW (wersja funkcji dla znaków szero- kich), pole to może mieć wartoœć większš niż 255. ů TmDefaultChar Kod znaku stosowany przez Windows do wyœwietlenia zna- ków nie należšcych do czcionki (zwykle chodzi o prostokšt). ů TmBreakChar Znak stosowany przez Windows i aplikacje do okreœlania poło- żenia przerw między słowami podczas justowania tekstu. O ile nie używasz jakiœ dziwnych znaków (takich jak czcionka EBCDIC), TmBreakChar będzie miał kod spacji, czyli 32. ů Tmltalic Przyjmuje wartoœć ruezerowš w wypadku kursywy. ů TmUnderlined Przyjmuje wartoœć ruezerowš dla czcionki podkreœlonej. ů TmStruckOut Przyjmuje wartoœć niezerowš dla czcionki przekreœlonej. ů TmPitchAndFamily Cztery mniej znaczšce bity tej zmiennej sš znacznikami okreœlajšcymi niektóre własnoœci czcionki, zdefiniowane przez następujšce identyfikatory stałych w pliku WINGDI.H: Wartoœć Identyfikotor stałej 0x01 TMPF FIXED PITCH 0x02 TMPF VECTOR 0x04 TMPF TRUETYPE 0x04 TMPF DEVICE Pomimo mylšcej nazwy znacznika TMPF FIXED PITCH najmniej znaczšcy bit ma wartoœć 1 przy czcionce o zmiennej szerokoœci. Drugi bit (TMPF VECTOR) przyjmie wartoœć 1 dla czcionki TrueType i innych, które stosujš dowolnš techni- kę skalowania konturów, na przykład PostScript. Znacznik TMPG I7EVICE zwiš- zany jest z czcionkš sprzętowš (tzn. wbudowanš drukarki) w przeciwieństwie do czcionek GDI. Bardziej znaczšce 4 bity omawianego pola odpowiedzialne sš za opis rodziny czcionki i przyjmujš takie same wartoœci, jak w polu lfPitchAndFamily struktury LOGFON'T. ů tmCharSet Identyfikator zestawu znaków. Zestawy znaków i Unicode Koncepcja zestawów znaków w Windows omawiana była w rozdziale 6, w któ- rym zetknšłeœ się z niuansami ustawień międzynarodowych, m. in. klawiatury. W wypadku struktur LOGFONT i TEXTMETRIC zestaw znaków czcionki pożš- danej (lub rzeczywistej) jest identyfikowany przez liczbę jednobajtowš z zakresu od 0 do 255. Identyfikatory te sš następujšco zdefiniowane w pliku WINGDI.H: lldefine ANSI_CNARSET 0 lldefi ne DEFAULT CHARSET 1 920 Częœć II: Gţafika (cišg dalszy ze strony 921) ţţdefine SYMBOL CHARSET 2 4tdefine MAC_CHARSET 77 ţţdefine SHIFTJIS_CHARSET 128 itdefine HANGEUL CHARSET 129 ţţdefine HANGUL_CHARSET 129 ţţdefine JOHAB_CHARSET 130 ţţdefine G82312_CHARSET 134 4ţdefine CHINESEBIG5_CHARSET 136 ţţdefine GREEK CHARSET 161 ţţdefine TURKISH_CHARSET' 162 ildefine VIETNAMESE_CHARSET 163 ţţdefine HEBREW_CHARSET 177 ţţdefine ARABIC CHARSET 178 ţţdefine BALTIC CHARSET 186 ţţdefine RUSSIAN_CHARSET 204 ţţdefine THAI CHARSET 222 4ţdefine EASTEUROPE_CHARSET 238 itdefine OEM CHARSET 255 Koncepcja zestawu znaków jest podobna do strony kodowej, ale œciœle powišza- na z Windows, identyfikatory zaœ przyjmujš wartoœci z zakresu od 0 do 255. Podobnie jak w wypadku pozostałych programów przedstawionych w tej ksišż- ce, możesz skompilować kod Ÿródłowy PICKFONT niezależnie od ustawienia identyfikatora UNICODE. Na dołšczanym do ksišżki CD-ROM-ie znajdziesz obie wersje programów wynikowych w katalogach DEBUG i RELEASE. Zauważ, że łańcuch znaków wyœwietlany przez program PICKFONT ma więk- szš długoœć w wersji Unicode. W obu wersjach cišg rozpoczyna się tš samš se- kwencjš kodów znakowych: od 0x40 do 0x45 oraz od 0x60 do 0x65. Bez względu na wybór zestawu znaków (z wyjštkiem SYMBOL-CHARSET) na ekrarue poja- wi się pięţ pierwszych wielkich liter alfabetu łacińskiego, a następnie pięć ma- łych (tzn. od A do E a następnie od a do e). Po uruchomieniu programu PICKFONT w wersji non-Unicode kolejne 12 zna- ków - kody znaków od OxCO do OxC5 oraz OxEO do OxE5 - będzie zależne od wybranego zestawu. W wypadku ANSI CHARSET będš to znaki niemal iden- tyczne jak poprzednio, wzbogacone jedynie o kreskę akcentowš. Dla zestawu GREEK CODE wspomniane kody odpowiadajš literom alfabetu greckiego, a dla RUSSIAN CHARSET będš to litery cyrylicy. Zmieniajšc zestaw znaków możesz jednak spowodować zmianę czcionki, jeœli czcionka rastrowa nie ma znaków, do- stępnych w TrueType. Przypomnij sobie, że większoœć czcionek TrueType jest typu Big Fonts i zawiera znaki charakterystyczne dla wielu zestawów. Jeœli używasz dalekowschodniej wersji Windows, znaki takie będš interpretowane jako dwu- bajtowe i wyœwietlane w postaci ideografów. Po uruchomieniu programu PICKFONT w wersji Unicode w œrodowisku Win- dows NT, kody od OxCO do OxC5 oraz od OxEO do OxE5, będš zwykle (z wyjšt- kiem zestawu SYMBOL CHAlZSET) widoczne w wersji akcentowanej, ponieważ tak zostały zdefiniowane. Program wyœwietla również znaki o kodach od 0x0390 do 0x0395 oraz od 0x03B0 do 0x03B5, które ze względu na zdefiniowarue w for- macie Unicode będš zawsze widoczne jako litery alfabetu greckiego. Analogicz- nie znaki o kodach od 0x0410 do 0x0415 oraz od 0x0430 do 0x0435 będš wyœwie- Rozdział 17: Tekst i czcionki g2ţ tlane w postaci cyrylicy, choć mogš być niedostępne w wypadku czcionki do- myœlnej. Chcšc uzyskać je w tradycyjny sposób musiałbyœ wybrać odpowiedni zestaw znaków, jak GREEK CHARSET lub RUSSIAIVţCHARSET. W opisywa- nym wypadku kod zestawu znaków znajdujšcy się w strukturze LOGFONT jest wcišż ten sam - Unicode - i nie musisz go zmieniać w celu uzyskania znaków międzynarodowych. Zamiast tego identyfikator zestawu znaków sygnalizuje po- trzebę pobrania znaków z okreœlonego zestawu. Teraz wybierz HEBREW CHARSET (kod 177). Alfabet hebrajski nie wchodzi w skład zestawu Big Fonts Wmdows, dlatego system operacyjny podstawia czcionkę Lucida Sans Unicode, co widać w prawym dolnyrn narożniku okna dialogowego. Ponadto PICKFONT wyœwietla znaki o kodach od 0x5000 do 0x5004, odpowia- dajšce niektórym z chińskich, japońskich czy też koreańskich ideografów. Zoba- czysz je na ekranie pod warunkiem, że używasz dalekowschodniej wersji Win- dows lub zainstalujesz bezpłatnš wersję czcionki Unicode, której możliwoœci sš znacznie większe niż Lucida Sans Urucode, na przykład Bitstream CyberFont, dostępnš pod internetowym adresem http://www.bitstream.com/product/world/ cyberbits. (O różrucy między tymi dwoma czcionkami informuje porównare wiel- koœci ich plików: Lucida Sans Unicode zajmuje około 300K, podczas gdy Bitstre- am CyberBits - aż 13 megabajtów.) Po zainstalowaniu tej czcionki Windows wy- bierze jš automatycznie, jeœli potrzebny będzie zestaw znaków niedostępny w Lu- cida Sans Unicode, na przykład SHIFTJIS CHARSET (japoński), HAN- GUL CHARSET (koreański), JOHAB CHARSET (koreański), GB2312 CHARSET (uproszczony chiński) czy CHINESEBIG5 CHARSET (tradycyjny chiński). Program umożliwiajšce przeglšdanie znaków czcionki Unicode zostanie zapre- zentowany w dalszej częœci rozdziału. System EZFONT Wprowadzenie czcionek TrueType - o podstawach wywodzšcych się z tradycyj- nej typograf - wyposażyło Windows w potężne narzędzie do wyœwietlania tek- stu na wiele różnych sposobów. Jednak częœć funkcji Windows shzżšcych do manipulowarua czcionkami działa wcišż na podstawie starych technolog, w któ- rych czćionka rastrowa musi wyglšdem przypominać wbudowane czcionki dru- karki. W następnym podrozdziale opiszę, jak wylicza się czcionki, uzyskujšc li- stę wszystkich dostępnych czcionek ekranowych lub drukarki. Zastosowanie okna dialogowego ChooseFont (zostanie wkrótce omówione) w dużym stopniu elimi- nuje koniecznoœć wyliczania czcionek w programie. Ze względu na powszechnoœć standardu TrueType w niemal każdym systemie oraz możliwoœć stosowania go zarówno na ekrarue, jak i w drukarce, nie jest ko- rueczne wyliczanie wszystkich czcionek w celu wybrania jednej. Program może prosto i dokładnie wybrać czcionki TrueType, o których wie, że sš dostępne w sys- temie (jeœli oczywiœcie, nie zostały usunięte przez użytkownika). Do przeprowa- dzenia takiej operacji wystarczy okreœlenie nazwy czcionki (prawdopodobnie z listy 13 czcionek przedstawionej na strorue 894) oraz jej rozmiaru. Takie podej- œcie zostało zastosowane w programie EZFONT (ang. easy font - łatwa czcionka), którego oba pliki Ÿródłowe przedstawiono na rysunku 17-3. Częœć II: Grafika EZFONT.H /* EZFONT.H header file */ HFONT EzCreateFont (HDC hdc. TCHAR * szFaceName, int iDeciPtHeight, int iDeciPtWidth, int iAttributes, BOOL fLogRes) : ţţdefine EZ_ATTR_BOLD 1 ţţdefine EZ_ATTR_ITALIC 2 ţţdefine EZ ATTR UNDERLINE 4 ţţdefine EZ ATTR STRIKEOUT 8 EZFONT.C /* EZFONT.C - tatwe tworzenie czcionek (c) Charles Petzold, 1998 */ ţţinclude ţţinclude p ţţinclude "ezfont.h" HFONT EzCreateFont (HDC hdc, TCHAR * szFaceName, int iDeciPtHeight, int iDeciPtWidth, int iAttributes, BOOL fLogRes) ( FLOAT cxOpi, cyDpi ; HFONT hFont ; LOGFONT lf ; POINT pt ; TEXTMETRIC tm ; ! SaveDC (hdc) ; SetGraphicsMode (hdc, GM ADVANCED) ; ModifyWorldTransform (hdc, NULL, MWTţIDENTITY) ; SetViewportOrgEx (hdc, 0, 0, NULL) ; SetWindowOrgEx (hdc, 0, 0, NULL) ; I if (fLogRes) t cxDpi = (FLOAT) GetOeviceCaps (hdc, LOGPIXELSX) ; cyDpi = (FLOAT) GetDeviceCaps (hdc, LOGPIXELSY) ; else l cxDpi = (FLOAT) (25.4 * GetDeviceCaps (hdc, HORZRES) / GetOeviceCaps (hdc, HORZSIZE)) ; cyDpi = (FLOAT) (25.4 * GetOeviceCaps (hdc, VERTRES) / GetDeviceCaps (hdc, VERTSIZE)) ; pt.x = (int) (iDeciPtWidth * cxDpi / 72) ; pt.y = (int) (iDeciPtHeight * cy0pi / 72) : ! ţ? Rozdział 17: Tekst i czcionki DPtoLP (hdc, &pt, 1) ; lf.lfHeight - - (int) (fabs (pt.y) / 10.0 + 0.5) ; lf.lfWidth = 0 ; lf.lfEscapement = 0 ; lf.lfOrientation = 0 ; lf.lfWeight = iAttributes & EZ_ATTR_BOLD ? 700 : 0 ; lf.lfItalic = iAttributes & EZ_ATTR_ITALIC ? 1 : 0 ; lf.lfUnderline = iAttributes & EZ_ATTR_UNDERLINE ? 1 : 0 ; lf.lfStrike0ut = iAttributes & EZ_ATTR_STRIKEOUT ? 1 : 0 ; lf.lfCharSet = DEFAULT CHARSET ; lf.lfOutPrecision = 0 ; lf.lfClipPrecision = 0 ; lf.lf0uality = 0 ; lf.lfPitchAndFamily = 0 ; ! lstrcpy (lf.lfFaceName, szFaceName) ; hFont = CreateFontIndirect (&lf) ; i if (iDeciPtWidth != 0) ( hFont = (HFONT) SelectObject (hdc, hFont) ; GetTextMetrics (hdc, &tm) ; Delete0bject (SelectObject (hdc. hFont)) ; lf.lfWidth = (int) (tm.tmAveCharWidth * fabs (pt.x) / fabs (pt.y) + 0.5) ; hFont = CreateFontIndirect (&lf) ; RestoreDC (hdc, -1) ; return hFont ; Rysunek 17ů3. Pliki programu EZFONT Moduł EZFONT.C zawiera tylko funkcję EzCreateFont, której można użyć w na- stępujšcy sposób: hfont = EzCreateFont(hdc, szFaceName, iDeciPtHeight, iDeciPtWidth, iAttributtes, fLogRes) ; Funkcja zwraca uchwyt czcionki. Czcionka może być wybrana w kontekœcie urzš- dzenia przez wywołanie funkcji SelectObject. Następnie powinieneœ wywołać funk- cję GetTextMetrics lub GetOutlineTextMetrics, aby okreœlić rzeczywiste rozmiary czcionki we współrzędnych logicznych. Zanim program zakończy działanie, po- winieneœ usunšć wszystkie utworzone czcionki, wywohzjšc DeleteDbject. Argument szFaceName jest nazwš dowolnej czcionki TrueType. Trzymanie się czcio- nek standardowych zwiększa szanse, że pożšdana czcionka istnieje w systemie. Trzeci argument funkcji okreœla pożšdany rozmiar punktowy czcionki, podawa- ny w decypunktach, czyli dziesiętnych częœciach punktu. jeœli więc chcesz, aby czcionka miała wielkoœć 12,5 punktu, podaj wartoœć 125. t ř 924 Częœć II: Grańka Zwykle czwarty argument powinien przyjmować wartoœć 0 lub identycznš z ar- gumentem trzecim. Odpowiednio dostrajajšc tę wartoœć, możesz uzyskać szerszš lub węższš czcionkę TrueType. Ten argument jest czasem nazywany em-szeroko- œciš i okreœla szerokoœć czcionki w punktach. Nie należy go mylić ze œredniš szero- koœciš znaków czcionki. We wczesnych latach typograf szerokoœć wielkiej litery M była równa jej wysokoœci i tak narodziła się koncepcja em-kwadratu i em-szero- koœci. Jeœli em-szerokoœć jest równa em-wysokoœci (rozmiar punktowy), rozmiary czcionki sš zgodne z zamiarami jej projektanta. Zsrutiejszenie lub zwiększenie em- szerokoœci powoduje utworzenie znaków węższych bšdŸ szerszych. Argument iAttributes może przyjmować jednš lub kilka wartoœci zdefiniowanych w pliku nagłówkowym EZFONT.H: EZ_ATTR_BOLD EZ_ATTR_ITALIC EZ_ATTR UNDERLINE EZ ATTR STRIKEOUT Możliwe jest bšdŸ zastosowanie stałych EZ ATTR BOLD lub EZ ATTR ITALIC w postaci atrybutów, bšdŸ też odwołanie się bezpoœrednio do okreœlonego kroju TrueType. Wreszcie ostatni argument przyjmuje wartoœć TRUE, gdy chcemy uzależnić roz- miar czcionki od "rozdzielczoœci logicznej", której wartoœć zwracana jest przez funkcję GetDeviceCaps z argumentami LOGPIXELSX i LOGPIXELSY. W przeciw- nym razie rozmiar czcionki wyliczany jest na podstawie wartoœci HORZRES, HORZSIZE, VERTRES oraz VERTSIZE. Różruca między tymi dwoma podejœcia- mi jest zauważalna jedynie w Windows NT. Funkcja EzCreateFont rozpoczyna się od wykonania kilku operacji istotnych jedy- rue dla Windows NT: wywołań funkcji SetGraphicsMode oraz ModifyWorldTrans- form, których działanie jest niezauważalne dla Windows 98. Ostatnia funkcja re- alizuje transformację, która powinna powodować zmianę widocznego rozmiaru czcionki. Dlatego przed wyliczeniem rozmiarów czcionki ustawiana jest domyœl- na transformacja - czyli jej brak. Głównym zadaniem funkcji EzFontCreate jest uzupełruenie pól struktury LOGFONT, a następnie wywołanie funkcji CreateFontlndirect, która zwraca uchwyt czcionki. Znaczna częœć funkcji EzCreateFont to zamiana punktowego rozmiaru czcionki na jednostki logiczne potrzebne dla pola lfHight struktury LOGFONT. Rozmiar punk- towy musi być najpierw zamieruony na jednostki zwišzane ze sprzętem (piksele), a dopiero potem na jednostki logiczne. Przeprowadzenie pierwszego kroku wišże się z wywołaniem funkcji GetDeviceCaps. Przejœcie od pikseli do jednostek logicz- nych wymaga zastosowania funkcji DPtoLP Chcšc, by konwersja DptoLP działała poprawnie, należy zastosować ten sam tryb odwzorowania, który zostanie użyty do wyœwietlania tekstu za pomocš utworzonej czcionki. Oznacza to, że należy ustalić tryb odwzorowania przed wywołaniem funkcji EzCreateFont. W większoœci wypad- ków stosuje się tylko jeden tryb odwzorowania do rysowania na okreœlonym ob- szarze okna, tak więc to wymaganie nie powinno sprawiać kłopotów. Program EZTEST, przedstawiony na rysunku 17-4, testuje funkcje zawarte w mo- dule EZFONT. Ponadto na tym samym rysunku znajduje się moduł FONTDEMO.C, wykorzystywany w kilku programach przedstawionych w dalszej częœci ksišżki. Rozdział 17: Tekst i czcionki 925 EZTEST.C /* EZTEST.C - Test programu EZFONT (c) Charles Petzold, 1998 */ ţţinclude ţţinclude "ezfont.h" TCHAR szAppName C] = TEXT ("EZTest") ; TCHAR szTitle C7 = TEXT ("EZTest: Test of EZFONT") ; void PaintRoutine (HWND hwnd, HDC hdc, int cxArea, int cyArea) f . HFONT hFont ; int y, iPointSize ; LOGFONT lf ; TCHAR szBuffer C1007 ; TEXTMETRIC tm ; // Ustaw tryb odwzorowania Logical Twips SetMapMode (hdc, MM_ANISOTROPIC) ; SetWindowExtGx (hdc, 1440, 1440, NULL) SetViewportExtEx (hdc, GetOeviceCaps (hdc, LOGPIXELSX), GetOeviceCaps (hdc. LOGPIXELSY), NULL) ; // Wypróbuj kilka czcionek y = 0 ; for (iPointSize = 80 ; iPointSize <= 120 ; iPointSize++) ( hFont = EzCreateFont (hdc, TEXT ("Times New Roman"), iPointSize, 0, 0, TRUE) ; GetObject (hFont, sizeof (LOGFONT), &lf) ; SelectObject (hdc, hFont) ; GetTextMetrics (hdc, &tm) ; TextOut (hdc, 0, y, szBuffer, wsprintf (szBuffer. TEXT ("Times New Roman font of %i.%i points, ") TEXT ("lf.lfHeight = %i, tm.tmHeight = %i"), iPointSize / 10, iPointSize % 10, lf.lfHeight, tm.tmHeight)) : Delete0bject (SelectObject (hdc, GetStockObject (SYSTEMţFONT))) ; y += tm.tmHeight ; ) ) FONTDEMO.C /* FONTDEMO.C - Program demonstrujdcy wlasnoœci czcionek (c) Charles Petzold, 1998 */ 926 Częœć II: Grafika i, (cišg dalszy ze strony 925) ţţinclude ţţinclude "..\\EZTest\\EzFont.h" ţţinclude "..\\EZTest\\resource.h" extern void PaintRoutine (HWND, HDC, int, int) ; LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; HINSTANCE hInst ; extern TCHAR szAppName [] ; extern TCHAR szTitle [] ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) i TCHAR szResource [] = TEXT ("FontOemo") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; ' hInst = hInstance ; wndclass.style = CS_HREDRAdv' ţ CSţVREDRAW ; i wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = szResource ; I , wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) i MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MB ICONERROR) ; return 0 ; ' 1 hwnd = CreateWindow (szAppName, szTitle, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CWţUSEDEFAULT, CWţUSEDEFAULT, I NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; j while (GetMessage (&msg. NULL, 0, 0)) TranslateMessage (&msg) ; ) DispatchMessage (&msg) ; ) return msg.wParam ; Rozdział 17: Tekst i czcionki LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) , ( static DOCINFO di = f sizeof (DOCINFO), TEXT ("Font Demo: Printing") ) : static int cxClient, cyClient : ! static PRINTDLG pd = ( sizeof (PRINTDLG) ) : BOOL fSuccess ; HDC hdc. hdcPrn ; int cxPage, cyPage ; PAINTSTRUCT ps ; switch (message) case WM COMMAND: switch (wParam) i case IDMţPRINT: // Pobierz kontekst urzšdzenia drukarki pd.hwndOwner = hwnd ; pd.Flags = PDţRETURNDC ţ PDţNOPAGENUMS ţ PD NOSELECTION ; i if (!PrintDlg (&pd)) ! return 0 ; „ if (NULL = (hdcPrn = pd.hDC)) ( MessageBox (hwnd, TEXT ("Cannot obtain Printer DC"), szAppName, MBţICONEXCLAMATION ţ MB OK) ; return 0 : ) // Pobierz rozmiar wydruku na stronie cxPage = GetOeviceCaps (hdcPrn, HORZRES) ; cyPage = GetDeviceCaps (hdcPrn, VERTRES) : I i fSuccess = FALSE ; // Utwórz wydruk SetCursor (LoadCursor (NULL, IDC WAIT)) ; ShowCursor (TRUE) ; if ((StartDoc (hdcPrn, &di) > 0) && (StartPage (hdcPrn) > 0)) PaintRoutine (hwnd, hdcPrn, cxPage, cyPage) ; if (EndPage (hdcPrn) > 0) ( fSuccess = TRUE ; EndDoc (hdcPrn) ; y ţDeleteDC (hdcPrn) ; i ShowCursor (FALSE) ; SetCursor (LoadCursor (NULL, IDC ARROW)) : I 1 928 Częœć II: Gra ika i (cišg dalszy ze strony 927) if (!fSuccess) ( MessageBox (hwnd, TEXT ("Error encountered during printing"), szAppName, MB ICONEXCLAMATION ţ MB-OK) ; return 0 ; case IDM ABOUT: - t MessageBox (hwnd, TEXT ("Font Demonstration Program\n") TEXT ("(c) Charles Petzold 1998"), rotmn" ţ szAppName, MB-ICONINFORMATION ţ MB OK); I break ; I case WM_SIZE: ! cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; return 0 ; 1 case WM-PAINT: hdc = BeginPaint (hwnd, &ps) ; PaintRoutine (hwnd, hdc, cxClient, cyClient) ; EndPaint (hwnd, &ps) ; i return 0 ; i case WM-DESTROY : I PostOuitMessage (0) ; return 0 ; ) return DefWindowProc (hwnd, message, wParam, lParam) ; FONTDEMO.RC (fragmenty) //Microsoft Developer Studio generated resource script. lfinclude "resource.h" tlinclude "axfres.h" /////////////////////////////////////////////////////////////////////////// // Menu FONTDEMO MENU DISCARDABLE BEGIN POPUP "&File" BEGIN MENUITEM "&Print... , IDM PRINT END - POPUP "&Help" BEGIN MENUITEM "&About... , IDM ABOUT END - ENO Rozdział 17: Tekst i czcionki RESOURCE.H (fragmenty) // Microsoft Developer Studio generated include file. // Used by FontDemo.rc ildefine IDM PRINT 40001 itdefine IDM ABOUT 40002 Rysunek 17-4. Program EZTEST Funkcja PaintRoutine pochodzšca z modułu EZTEST.C ustawia tryb odwzorowa- nia na Logical Twips, a następnie tworzy czcionkę Times New Roman o rozmia- rach od 8 do 12 punktów z gradacjš co 0,1 punktu. Wynik działania programu po pierwszym uruchomieniu może być nieco mylšcy. Wiele wierszy tekstu wydaje się zapisane czcionkš o tyxn samym rozmiarze, co potwierdza wartoœć pola tmHe- ight struktury TEXTMETRIC. Taki efekt jest spowodowany rasteryzacjš. Rozmia- ry podawane w pikselach mogš przyjmować jedynie wartoœci naturalne. Program FONTDEMO pozwala na wydrukowanie wyników działania. Dopiero po obej- rzeniu wydruku możesz się przekonać, że rozmiary czcionek niewiele różniš się w poszczególnych wierszach tekstu. Obracanie czcionki Jak już zapewne odkryłeœ, eksperymentujšc z programem PICKFONT, pola 1fO- rientation i lfEscapement struktury LOGFONT pozwalajš na obracanie czcionek TrueType. Przyglšdajšc się temu bliżej, można dojœć do wniosku, że dla GDI ope- racje tego typu nie sš niczym nowym. Wzory służšce do obliczania współrzęd- nych punktów wokół jakiegoœ punktu ustalonego sš powszechnie znane. Chociaż EzCreateFont nie umożliwia wybrania kšta obrotu czcionki, łatwo jest uzyskać pożšdany efekt już po wywołaniu funkcji, co demonstruje program FONT ROT. Na rysunku 17-5 przedstawiono moduł FONTROT.C; program wykorzy- stuje również pliki EZFONT oraz FONTDEMO. FONTROT.C /* FONTROT.C - Obracanie czcionek (c) Charles Petzold, 1998 */ ilinclude itinclude "..\\eztest\\ezfont.h" TCHAR szAppName C] = TEXT ("FontRot") ; TCHAR szTitle C] = TEXT ("FontRot: Rotated Fonts") ; void PaintRoutine (HWND hwnd, HDC hdc, int cxArea, int cyArea) { = TEXT (" Rotation") ; static TCHAR szString C] HFONT hFont ; 930 Częœć IIţ Grafika (cišg dalszy ze strony 929) int i ; LOGFONT lf ; hFont = EzCreateFont (hdc, TEXT ("Times New Roman"), 540, 0, 0, TRUE) ; GetObject (hFont, sizeof (LOGFONT), &lf) ; Delete0bject (hFont) ; SetBkMode (hdc, TRANSPARENT) ; SetTextAlign (hdc, TŽ BASELINE) ; SetlliewportOrgEx (hdc, cxArea / 2, cyArea / 2, NULL) ; for (i = 0 ; i < 12 ; i ++) lf.lfEscapement = lf.lfOrientation = i * 300 ; SelectObject (hdc, CreateFontIndirect (&1f)) ; TextOut (hdc, 0, 0, szString, lstrlen (szString)) ; I Delete0bject (SelectObject (hdc, GetStockObject (SYSTEMţFONT))) ; ) Rysunek 17-5. Program FONTROT i Program FONTROT wywołuje funkcję EzCreateFont, by otrzymać strukturę LOG- FONT, zwišzanš z czcionkš T'unes New Roman o rozmiarze 54 punktów. Następ- nie usuwa tę czcionkę. W pętli for co 30 stopni tworzona jest, a następnie wy- ; œwietlana nowa czcionka. Wynik działara programu przedstawia rysunek 17-6. ; O 'ţ' t , ůo . ţ,ţ.o O ů uoyţţ.o ţotatlon . " ţa oţ.'ţ^ţţ ţoţ ţ`IOl1 oţ Rysunek 17-6. Wynik działania programu FONTROT Rozdział 17: Tekst i czcionki 931 Jeœli jesteœ zainteresowany bardziej ogóln5ţn podejœciem do problemu obracania i oraz innych przekształceń liniowych obiektów graficznych, a jednoczeœnie jesteœ pewien, że twoje programy będš uruchamiane wyłšcznie w systemie Windows NT, możesz wykorzystać macierz XFORM i odpowiednie funkcje transformujšce. ; Wyliczanie czcionek Wyliczanie czcionek to uzyskiwanie z GDI listy czcionek dostępnych. Następnie program może wybrać jednš z nich wprost bšdŸ wyœwietlić je w oknie dialogo- wym, pozostawiajšc wybór użytkownikowi. Najpierw krótko objaœnię działanie funkcji wyliczajšcych, a następnie zaprezentuję sposób działania funkcji ChooseFont, dzięki której wyliczanie czcionek w aplikacji nie jest bezwarunkowo konieczne. Funkcje wyliczajšce W "zamierzchłej" przeszłoœci Windows funkcje wyliczajšce wymagały zastoso- wania funkcji EnumFonts: EnumFonts (hdc, scTypeFace, EnumProc, pData) ; Program mógł wyliczać wszystkie czcionki (po przypisaniu drugiemu argumento- wi wartoœci NULL) lub tylko okreœlonego kroju. Trzeci argument jest callbackowym wywołaruem funkcji wyliczajšcej; argument czwarty to dane przekazywane opcjo- nalrue do funkcji. GDI wywohzje funkcję zwrotnš dla każdej czcionki dostępnej w systemie, przekazujšc jej struktury definiujšce czcionkę: LOGFONT i TEXTME- TRIC oraz dodatkowo kilka znaczników okreœlajšcych rodzaj tej czcionki. Funkcja EnumFontFamilies została zaprojektowana, aby sprawniej wyliczać czcionki w systemie Windows 3.1: EnumFontFamilies (hdc, szFaceName, EnumProc, pData) ; Zwykle przy pierwszym wywołaruu funkcji EnumFontFamilies jej drugi argument przyjmuje wartoœć NULL. Funkcja EnumProc wywoływana jest raz dla każdej rodziny czcionek (takiej jak Times New Roman). Następnie aplikacja wywołuje ponownie EnumFontFamilies, tym razem jako drugi argument przyjmujšc nazwę kroju czcionki. Inna jest też funkcja zwrotna. GDI wywohxje jš dla każdego członka rodziny czcionek (na przykład Times New Roman Italic). Do funkcji zwrotnej przekazywane sš struktury: ENUMLOGFONT (która ma format struktury LOG- FONT, rozbudowanej o pole zawierajšce pełnš nazwę oraz pole zawierajšce na- zwę stylu czcionki, na przykład "Italic" lub "Bold"), TEXTMETRIC dla czcionek niezgodnych z TrueType oraz NEWTEXT'METRIC dla czcionek TrueType. Ta ostat- rua jest rozbudowana o cztery pola informacyjne w stosunku do TEXTMETRIC. Funkcja EnumFontFamiliesEx jest zalecana dla aplikacji pracujšcych w œrodowi- sku 32-bitowego Windows: EnumFontFamiliesEx (hdc, &logfont, EnumProc, pData, dwFlags) : Drugim argumentem jest wskaŸnik do struktury LOGFONT, dla którego pola ifCharSet i ţaceName okreœlajš, które czcionki majšbyć wyliczane. Funkcja zwrotna pobiera informacje o każdej czcionce w formie struktur ENUMLOGFONTEX i NEWTEXTMETRICEX. 932 CţpErţ u. rţţfa Okno dialogowe ChooseFont 4V rozdziale 11 przedstawiłem wstępne informacje o wspólnym oknie dialogo- wego ChooseFont. Teraz, w trakcie omawiania wyliczania czcionek, zawartoœć funkcji ChooseFont powinna stać się oczywista. Przyjmuje ona jako jedyny argu- ment wskaŸnik do struktury CHOOSEFONT i wyœwietla okno dialogowe zawie- rajšce wszystkie czcionki. Po zakończeruu funkcji ChooseFont zawartoœć struktu- ry LOGFONT, będšcej częœciš struktury CHOOSEFONT, wystarcza do utworze- nia czcionki logicznej. Program CHOSEFONT, przedstawiony na rysunku 17-7, demonstruje zastoso- wanie funkcji ChooseFont i wyœwietla pola struktury LOGFONT, definiowanej przez tę funkcję. Program wyœwietla ten sam łańcuch znaków co PICKFONT. CHOSFONT.C /* CHOSFONT.C - ChooseFont - program demonstracyjny (c) Charles Petzold, 1998 ttinclude tţinclude "resource.h" LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; */ int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) static TCHAR szAppName[) = TEXT ("ChosFont") HWND hwnd ; MSG msg ; WNDCLASS wndclass : wndclass.style = CS_HREDRAW ţ CS VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = szAppName ; wndclass.lpszClassName = szAppName ; ' if (!RegisterClass (&wndclass)) ( MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MBţICONERROR) ; return 0 ; hwnd = CreateWindow (szAppName, TEXT ("ChooseFont"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW-USEDEFAULT, CW_USEDEFAULT, CW-USEDEFAULT, NULL, NULL, hInstance, NULL) : Rozdział 17: Tekst i czcionki 933 ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; i while (GetMessage (&msg, NULL, 0, 0)) ( TranslateMessage (&msg) : DispatchMessage (&msg) ; ) return msg.wParam : LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ' i static CHOOSEFONT cf ; static int cyChar ; static LOGFONT lf ; static TCHAR szTextC] = TEXT ("\x41\x42\x43\x44\x45 ") TEXT ("\x61\x62\x63\x64\x65 ") TEXT ("\xC0\xCl\xC2\xC3\xC4\xC5 ") TEXT ("\xE0\xEl\xE2\xE3\xE4\xE5 ") 4ţifdef UNICODE TEXT ("\x0390\x0391\x0392\x0393\x0394\x0395 ") TEXT ("\x0380\x0381\x0382\x0383\x0384\x0385 ") TEXT ("\x0410\x0411\x0412\x0413\x0414\x0415 ") TEXT ("\x0430\x0431\x0432\x0433\x0434\x0435 ") '. TEXT ("\x5000\x5001\x5002\x5003\x5004") ţţendi f HDC hdc ; int y : PAINTSTRUCT ps ; TCHAR szBuffer C64ţ ; TEXTMETRIC tm ; switch (message) ( case WM CREATE: // Pobierz wysokoœć tekstu cyChar = HIWORD (GetDialogBaseUnits ()) ; // Inicjuj strukturę LOGFONT GetObject (GetStockObject (SYSTEMţFONT), sizeof (lf), &lf) ; // Inicjuj strukturę CHOOSEFONT cf.lStructSize = sizeof (CHOOSEFONT) ; cf.hwndOwner = hwnd ; cf.hDC = NULL ; cf.lpLogFont = &lf ; cf.iPointSize = 0 ; cf.Flags = CF INITTOLOGFONTSTRUCT ĆF SCREENFONTS ţ CFţEFFECTS ; cf.rgbColors = 0 ; 9 Częœć II, Grafika (cigg dalszy ze strony 933) cf.lCustOata =o; cf.lpfnHook = NULL ; cf.lpTemplateName = NULL ; cf.hInstance = NULL ; cf.lpszStyle = NULL ; cf.nFontType = 0 ; cf.nSizeMin = 0 ; ! cf.nSizeMax = 0 ; return 0 ; case WM COMMAND: switch (LOWORD (wParam)) case IDMţFONT: if (ChooseFont (&cf)) InvalidateRect (hwnd, NULL, TRUE) ; return 0 ; ) return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; // Wyœwietl przykładowy tekst, używajšc wybranej czcionki SelectObject (hdc, CreateFontIndirect (&lf)) ; GetTextMetrics (hdc, &tm) ; SetTextColor (hdc, cf.rgbColors) ; TextOut (hdc, 0, y = tm.tmExternalLeading, szText, lstrlen (szText)) ; // Wyœwietl pola struktury LOGFONT, używajšc czcionki systemowej Delete0bject (SelectObject (hdc, GetStockObject (SYSTEM FONT))) ; SetTextColor (hdc, 0) ; TextOut (hdc 0, y += tm.tmHeight, szBuffer, wsprintf (szBuffer, TEXT ("lfHeight = %i"), lf.lfHeight)) ; TextOut (hdc, 0, y += cyChar, szBuffer, wsprintf (szBuffer, TEXT ("lfWidth = %i"), lf.lfWidth)) ; TextOut (hdc, 0, y += cyChar, szBuffer, wsprintf (szBuffer, TEXT ("lfEscapement = %i"), lf.lfEscapement)) ; TextOut (hdc, 0, y += cyChar, szBuffer, wsprintf (szBuffer, TEXT ("lfOrientation = %i"), lf.lfOrientation)) ; TextOut (hdc, 0, y += cyChar, szBuffer, wsprintf (szBuffer, TEXT ("lfWeight = %i"), lf.lfWeight)) ; TextOut (hdc 0, y += cyChar, szBuffer, wsprintf (szBuffer, TEXT ("lfItalic = %i"), lf.lfItalic)) ; TextOut (hdc 0, y += cyChar, szBuffer, wsprintf (szBuffer, TEXT ("lfUnderline = %i"), lf.lfUnderline)) ; 935 Rozdział 17: Tekst i czcionki TextOut (hdc 0, y += cyChar, szBuffer wsprintf (szBuffer, TEXT ("lfStrike0ut = %i"), lf.lfStrike0ut)) ; TextOut (hdc, 0, y += cyChar, szBuffer, wsprintf (szBuffer, TEXT ("lfCharSet = %i"), lf.lfCharSet)) ; TextOut (hdc, 0, y += cyChar. szBuffer, wsprintf (szBuffer TEXT ("lfOutPrecision = %i"), lf.lfOutPrecision)) ; TextOut (hdc, 0, y += cyChar, szBuffer, wsprintf (szBuffer, TEXT ("lfClipPrecision = %i"), lf.lfClipPrecision)) ; TextOut (hdc, 0, y += cyChar, szBuffer, wsprintf (szBuffer, TEXT ("lf0uality = %i"), lf.lf0uality)) ; TextOut (hdc, 0, y += cyChar, szBuffer, wsprintf (szBuffer. TEXT ("lfPitchAndFamily = Ox%02X"), lf.lfPitchAndFamily)) ; TextOut (hdc, 0, y += cyChar. szBuffer, wsprintf (szBuffer, TEXT ("lfFaceName = %s"), lf.lfFaceName)) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: PostOuitMessage (0) ; return 0 ; return DefWindowProc (hwnd. message. wParam. lParam) ; ) CHOSFONT.RC (fragmenty) //Microsoft Developer Studio generated resource script. ţţinclude "resource.h" ţţinclude "axfres.h" /////////////////////////////////////////////////////////////////////////// // Menu CHOSFONT MENU DISCARDABLE BEGIN MENUITEM "&Font!". IDMţFONT END RESOURCE.H (fragmenty) // Microsoft Developer Studio generated include file. // Used by ChosFont.rc ţţdefine IDMţFONT 40001 Rysunek 17"7. Program CHOSFONT 936 Częœć II: Grafika Jak zwykle w wypadku wspólnych okien dialogowych, pole Flags struktury CHOOSEFONT pozwala wybrać wiele opcji. Ustawienie znacznika CF INITFON- TLOGSTRUCT powoduje, że Windows inicjuje okno dialogowe, zaznaczajšc war- toœci okreœlone w strukturze LOGFONT, przekazywanej do funkcji ChooseFont. Odpowiednio manipulujšc poszczególnymi znacznikami, możesz żšdać wyłšcz- nie czcionek TrueType (CF TTONLY) lub tylko czcionek o stałej szerokoœci (CF FI- XEDPTTCHONLY) bšdŸ zabronić stosowania czcionek symboli (CF SCRIPTSON- LY). Możesz wyœwietlić tylko czcionki ekranowe (CF_SCREENFONTS), czcionki drukarki (CF PRINTERFONTS) lub obie wersje (CF BOTH). W dwóch ostatnich wypadkach pole hDC struktury CHOOSEFONT powinno odnosić się do kon- tekstu urzšdzenia drukarki. Program CHOSFONT stosuje znacznik CF SCREEN- FONTS. Znacznik CF EFFECTS (trzeci wykorzystywany przez CHOSFONT) powoduje pojawienie się w oknie dialogowym pól wyboru odpowiedzialnych za podkre- œlenie i przekreœlenie znaków tekstu oraz za wybór ich koloru. Wbrew pozorom wprowadzenie sposobu wybierania koloru czcionek nie jest wcale trudne - war- to spróbować. Zwróć uwagę na pole Skrypt w oknie dialogowym Czcionka, którego zwartoœć jest wyœwietlana za pomocš funkcji ChooseFont. Pozwala ono użytkownikowi na wybór zestawu znaków spoœród dostępnych dla danej czcionki; odpowiedni iden- tyfikator zestawu znaków zwracany jest w postaci jednego z pól struktury LOG- FONT. Funkcja ChooseFont wykorzystuje cal logiczny do wyznaczenia wartoœci pola IjFIe- ight na podstawie rozmiaru punktowego. Przypuœćmy, że w oknie dialogowym Właœciwoœci: Ekran wybrałeœ opcję Małe czcionki. Oznacza to, że funkcja GetDe- viceCaps, przyjmujšc jako argumenty kontekst urzšdzenia karty graficznej oraz LOGPIXELSY, zwróci wartoœć 96. Jeœli wywołujšc funkcję ChooseFont wybierzesz czcionkę Times New Roman o rozmiarze 72 punktów, w rzeczywistoœci będzie ona miała wysokoœć 1 cala. Po wykonaniu funkcji ChooseFont wartoœć pola IfHe- ight struktury LOGFONT będzie wynosić -96 (zwróć uwagę na znak), co ozna- cza, że wysokoœć czcionki jest równa 96 pikselom lub 1 calowi. Doskonale. To jest prawdopodobnie to, czego oczekiwaliœmy. Zapamiętaj jednak że: ů Gdy ustawisz jeden z trybów odwzorowania metrycznego w Windows NT, współrzędne logiczne nie będš zgodne z rzeczywistym rozmiarem czcionki. Jeœli na przykład obok tekstu zapisanego na podstawie trybu odwzorowania narysujesz linijkę, jej jednostki oraz jednostki, w których mierzy się wielkoœć tekstu, będš zupełnie inne. W takim wypadku powinieneœ użyć opisanego wczeœniej trybu odwzorowania Logical Twips. Pozwala on na rysowanie gra- fiki i umieszczanie czcionek w jednostkach, wzajemnie zgodnych. Jeœli planujesz użycie dowolnego trybu odwzorowania innego niż MM TEXT, upewnij się, że tryb odwzorowania nie jest włšczony w trakcie wybierania czcionki w kontekœcie urzšdzenia i wyœwietlania tekstu. W przeciwnym razie GDI zinterpretuje wartoœć pola lfHeight struktury LOGFONT jako wyrażonš w jednostkach logicznych. Rozdział 17: Tekst i czcionki 937 ů Wartoœć pola lfHeight struktury LOGFONT ustalana jest przez funkcję Choose- Font zawsze w pikselach i nadaje się do wyœwietlania tekstu na ekranie. Two- rzšc czcionkę w kontekœcie urzšdzerua drukarki musisz nieco zmienić tę war- toœć. Funkcja ChooseFont używa pola hDC struktury CHOOSEFONT jedynie do uzyskania czcionek drukarki i wyœwietlenia ich do wyboru w oknie dialo- gowym. Uchwyt kontekstu urzšdzenia nie wpływa na wartoœć lfHeight. Struktura CHOOSEFONT zawiera pole iPointSize, w którym umieszczony jest rozmiar wybranej czcionki w jednostkach będšcych dziesiętnymi częœciami punk- ,. tu. Bez względu na kontekst urzšdzenia i tryb odwzorowarua wartoœć ta może być zawsze zamieniona na jednostki logiczne i użyta dla potrzeb pola lfHeight. Odpowiedni fragment kodu znajduje się w module EZFONT.C. Oczywiœcie mo- żesz go uproœcić według własnych potrzeb. Inny program wykorzystujšcy funkcję ChooseFont to UNICHARS, przedstawio- ny na rysunku 17-8. Umożliwia on obejrzenie wszystkich znaków danej czcionki i jest szczególrue użyteczny do przestudiowania możliwoœci czcionki Lucida Sans ' Unicode, którš stosuje domyœlnie, bšdŸ Bitstream CyberBit. Program UNICHARS wykorzystuje funkcję TextOutW do wyœwietlania znaków czcionki, tak więc może być bez przeszkód uruchamiany zarówno w Windows NT, jak i Windows 98. ( UNICHARS.C /* UNICHARS.C - Wyœwietla 16-bit kody znakdw (c) Charles Petzold, 1998 */ include ilinclude "resource.h" LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) static TCHAR szAppNameC] = TEXT ("UniChars") ; HWND hwnd ; MSG " msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CS VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC ŽRROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITEţBRUSH) wndclass.lpszMenuName = szAppName ; wndclass.lpszClassName = szAppName : if (!RegisterClass (&wndclass)) ( 938 Częœć IIţ Grafika (cišg dalszy ze strony 937) MessageBox (NULL, TEXT ("This program requies Windows NT!"), szAppName, MBţICONERROR) ; return 0 ; hwnd = CreateWindow (szAppName, TEXT ("Unicode Characters"), WS_OVERLAPPEDWINDOW ţ WSţVSCROLL, CW_USEDEFAULT, CW USEDEFAULT, CW_USEDEFAULT, CW USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0. 0)) f TranslateMessage (&msg) ; DispatchMessage (&msg) ; ) return msg.wParam ; ) LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) f static CHOOSEFONT cf ; static int iPage ; static LOGFONT lf ; HDC hdc ; int cxChar, cyChar, x, y, i, cxLabels ; PAINTSTRUCT ps ; SIZE size ; TCHAR szBuffer [8) ; TEXTMETRIC tm ; WCHAR ch ; switch (message) ( case WM_CREATE: hdc = GetDC (hwnd) ; lf.lfHeight = - GetDeviceCaps (hdc, LOGPIXELSY) / 6 ; // 12 punktów lstrcpy (lf.lfFaceName, TEXT ("Lucida Sans Unicode")) ; ReleaseDC (hwnd, hdc) ; cf.lStructSize = sizeof (CHOOSEFONT) ; cf.hwndOwner = hwnd ; cf.lpLogFont = &lf ; cf.Flags = CFţINITTOLOGFONTSTRUCT ţ CFţSCREENFONTS ; SetScrollRange (hwnd, SB_VERT, 0, 255, FALSE) ; SetScrollPos (hwnd, SBţVERT, iPage, TRUE ) ; return 0 ; case WM COMMAND: switch (LOWORD (wParam)) f case IDMţFONT: Rozdział 17: Tekst i czcionki 939 if (ChooseFont (&cf)) InvalidateRect (hwnd. NULL, TRUE) ; return 0 ; ) return 0 ; case WM VSCROLL: switch (LOWORD (wParam)) case SB_LINEUP: iPage -- 1 ; break ; case SB_LINEDOWN: iPage += 1 ; break ; case SB_PAGEUP: iPage -= 16 ; break ; case SB PAGEDOWN: iPage += 16 ; break ; case SB THUMBPOSITION: iPage = HIWORD (wParam) ; break ; default: return 0 ; ) iPage = max (0, min (iPage, 255)) ; SetScrollPos (hwnd, SB_VERT, iPage, TRUE) ; InvalidateRect (hwnd, NULL, TRUE) ; return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; SelectObject (hdc, CreateFontIndirect (&1f)) ; GetTextMetrics (hdc, &tm) ; cxChar = tm.tmMaxCharWidth ; cyChar = tm.tmHeight + tm.tmExternalLeading ; cxLabels = 0 ; for ( i = 0 ; i < 16 ; i++) ( wsprintf (szBuffer, TEXT (" 000%1X: "), i) ; GetTextExtentPoint (hdc, szBuffer, 7, &size) ; cxLabels = max (cxLabels, size.cx) ; ) for (y = 0 ; y < 16 ; y++) f wsprintf (szBuffer, TEXT (" %03X_: "). 16 * iPage + y) ; TextOut (hdc, 0, y * cyChar, szBuffer, 7) ; for (x = 0 ; x < 16 ; x++) ( ch = (WCHAR) (256 * iPage + 16 * y + x) ; TextOutW (hdc, x * cxChar + cxLabels, y * cyChar, &ch, 1); ) ) Delete0bject (SelectObject (hdc, GetStockObject (SYSTEMţFONT))); Częœć II: Grafika (cigg dalszy ze strony 939) EndPaint (hwnd, &ps) ; return 0 ; j case WM DESTROY: PostQuitMessage (0) ; return 0 ; return DefWindowProc (hwnd, message, wParam, lParam) ; UNICHARS.RC //Microsoft Developer Studio generated resource script. llinclude "resource.h" llinclude "afxres.h" //////////!/////////////////////////////////!//////!///////////////////////// //Menu UNICHARS MENU DISCARDABLE BE6IN MENUITEM "&Font!" IDM_FONT END RESOURCE.H //Microsoft Developer Studio generated include file. //Used by Unichars.rc ţldefine IDM FONT 40001 Rysunek 17-8. Program UNICHARS Formatowanie akapitu Umiejšc wybierać i tworzyć czcionki logiczne, możemy przystšpić do zapozna- nia się ze sposobami formatowania tekstu. Formatowanie polega na rozmiesz- czaniu kolejnych wierszy tekstu między marginesami na stronie na jeden z czte- rech sposobów: wyrównanie do lewego marginesu, wyrównanie do prawego marginesu, rozmieszczerue na œrodku strony oraz justowanie, czyli takie dobra- nie jednakowych odległoœci między wyrazami w wierszu, aby tekst zajmował całš przestrzeń pomiędzy marginesami. Pierwsze trzy sposoby formatowania mogš być zrealizowane za pomocš funkcji DrawText z argumentem DT WORDBRE- AK, jednak takie podejœcie ma pewne ograniczenia, na przykład rue potrafisz okre- œlić, jakš częœć tekstu funkcja DrawText zmieœciła w obrębie prostokšta. Funkcja DrawText nadaje się do niektórych prostych zadań, bardziej skomplikowane będš wymagały zastosowania funkcji TextOut. Rozdział 17: 7ekst i czcionki 941 Proste formatowanie tekstu Jedna z najbardziej użytecznych funkcji do pracy z tekstem to GetTextExtentPo- int32, której nazwa przeszła kilka zmian od czasów pierwszych wersji Windows. Funkcja zwraca szerokoœć i wysokoœć łańcucha znaków na podstawie bieżšcej czcionki, wybranej w kontekœcie urzšdzerua: GetTextExtentPoint32 (hdc, pString, iCount, &size) : Szerokoœć i wysokoœć tekstu w jednostkach logicznych sš zwracane odpowied- nio w polach cx i cy struktury SIZE. Rozpocznijmy od przykładu z jednym wier- szem tekstu. Powiedzmy, że wybrałeœ czcionkę w kontekœcie urzšdzenia i chciał- byœ napisać tekst: TCHAR * szText C] = TEXT ("Hello, how are you? ") ; Chciałbyœ, aby tekst rozpoczynał się w punkcie o współrzędnej pionowej Y równej yStart, na stronie o marginesach okreœlonych wartoœciami zmiennych xLeft i xRight. Twoim zadaniem będzie wyznaczenie wartoœci zmiennej xStart, czyli współrzędnej poziomej punktu rozpoczęcia tekstu na stronie. Oczywiœcie, zadanie to byłoby znacznie łatwiejsze, gdybyœ miał do czynienia z tek- stem o stałej szerokoœci znaku, niestety, w omawianym wypadku nie możemy przyjšć takiego założenia. Najpierw należy pobrać współrzędne krańcowe tek- stu: GetTextExtentPoint32 (hdc, szText, lstrlen(szText), &size) ; Jeœli wartoœć size.cx jest większa niż różnica xRight - xLeft, wiersz nie mieœci się między marginesami. Założymy, że tak nie jest. Chcšc wyrównać tekst do lewego marginesu, wystarczy wstawić w zmiennej xStart wartoœć xLeft i napisać: TextOut (hdc, xStart, yStart, szText, lstrlen (szText)) ; To było całkiem proste. Teraz możesz dodać wartoœć size.cy do yStart i jesteœ goto- wy do pisania kolejnego wiersza tekstu. Aby wyrównać tekst do prawego marginesu, zainicjuj zmiennš xStart w nastę- pujšcy sposób: xStart = xRight - size.cx ; Aby umieœcić tekst na œrodku strony, użyj wzoru: xStart = (xLeft + xRight - size.cx) / 2 ; Teraz przed nami trudne zadanie - justowanie tekstu. Odległoœć między margi- nesami wynosi (xRight - xLeft). Szerokoœć tekstu niejustowanego wynosi size.cx. Różnica pomiędzy tymi dwoma wartoœciami wynosi: xRight - xLeft - size.cx i musi być równomierrue rozłożona między trzy spacje zawarte w tekœcie. Brzmi okropnie, ale wcale nie jest takie trudne. Wywołaj funkcję: SetTextJustification (hdc, xRight, - xLeft - size.cx, 3) ; Drugi argument to odległoœć, która musi być równomiemie rozłożona między spacje, trzeci to liczba spacji w tekœcie, w tym wypadku 3. Teraz podstaw xLeft do xRight i napisz tekst za pomocš funkcji TextOut: TextOut (hdc, xStart, yStart, szText, lstrlen (szText)) ; g42 Częœć 11: Grafika Tekst zostanie wyjustowany między marginesami na stronie. Każde wywołanie funkcji SetTextjustification powoduje błšd, jeœli liczba spacji nie może rozłożyć się równomiernie w wierszu. Błšd ten wpływa na sposób działa- nia funkcji GetTextExtentPoint32, dlatego rozpoczynajšc nowy wiersz, należy usu- nšć błšd wywołujšc funkcję: SetTextJustification (hdc, 0, 0) ; Praca z akapitami Pracujšc z całym akapitem tekstu szukarue spacji musisz rozpoczšć od jego po- czštku. Za każdym razem, gdy napotkasz spację (lub inny znak, który może być użyty do zakończenia wiersza), wywołuj funkcję GetTextExtentPoint32, aby spraw- dzić, czy tekst cišgle mieœci się między marginesami. Kiedy wykracza poza do- zwolony obszar, cofasz się do poprzedniej spacji. Wyznaczywszy w ten sposób łańcuch znaków, który ma być umieszczony w jednym wierszu, możesz wywo- łać kolejno funkcje SetTextjustification oraz TextOut, a następnie przejœć do kolej- nego wiersza. Program JUSTIFYl, przedstawiony na rysunku 17-9, wykonuje opisane powyżej operacje na przykładzie pierwszego akapitu ksišżki Marka Twaina Przygody Hucka. W oknie dialogowym możesz wybrać dowolnš czcionkę, i zmienić sposób wy- równania tekstu, posługujšc się opcjami menu. Rysunek 17-10 przedstawia wynik działania programu JUSTIFYl. JUSTIFYl.C /* JUSTIFYl.C - Tekst justowany - program itl (c) Charles Petzold, 1998 */ iţinclude ţţinclude "resource.h" LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; TCHAR szAppNameC] = TEXT ("Justifyl") ; int WINAPI WinMain (HINSTANCE hInstance. HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CS VREORAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITEţBRUSH) Rozdział 17: Tekst i czcionki 943 wndclass.lpszMenuName = szAppName ; '; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) { 9 q "), ; Messa eBox (NULL, TEXT ("This program re uires Windows NT! szAppName, MB ICONERROR) ; ' return 0 ; ' 3 hwnd = CreateWindow (szAppName, TEXT ("Justified Type ţţ1"), WS_OVERLAPPEDWINDOW ł CW_USEDEFAULT, CWţUSEDEFAULT, CW USEDEFAULT CW USEDEFAULT, ,. NUţLL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; `,. while (GetMessage (&msg, NULL, 0, 0)) I TranslateMessage (&msg) ; DispatchMessage (&msg) ; ) return msg.wParam ; } void DrawRuler (HDC hdc, RECT * prc) f I static int iRuleSize [16) = ( 360, 72, 144, 72, 216, 72, 144, 72, 288. 72, 144, 72, 216. 72. 144. 72 1 : int i, j ; POINT ptClient ; SaveDC (hdc) ; // Ustaw tryb odwzorowania Logical Twips ; . SetMapMode (hdc, MM ŽNISOTROPIC) ; SetWindowExtEx (hdc, 1440, 1440, NULL) ; SetUiewportExtEx (hdc, GetOeviceCaps (hdc, LOGPIXELSX), GetOeviceCaps (hdc, LOGPIXELSY), NULL) ; // Odsuń poczdtek ukladu wspólrzędnych o pół cala // od lewego górnego narożnika SetWindowOrgEx (hdc, -720, -720, NULL) ; // Ustaw prawy margines (ćwierć cala od prawej krawędzi strony) P tClient.x = prc->right ; ptClient.y = prc->bottom ; DPtoLP (hdc, &ptClient, 1) ; ptClient.x -= 360 ; // Narysuj linijki MoveToEx (hdc, 0, -360, NULL) ; 944 Częœć II: Grafika (cišg dalszy ze strony 943) LineTo (hdc, ptClient.x, -360) ; MoveToEx (hdc, -360, 0, NULL) ; LineTo (hdc, -360, ptClient.y) ; for (i = 0, j = 0 ; i <= ptClient.x ; i += 1440 / 16, j++) MoveToEx (hdc, i, -360, NULL) ; LineTo (hdc, i, -360 - iRuleSize [j % 16]) ; for (i = 0, j = 0 ; i <= ptClient.y ; i += 1440 / 16, j++) ( MoveToEx (hdc, -360, i, NULL) ; LineTo (hdc, -360 - iRuleSize [j % 16], i) ; t I RestoreDC (hdc, -1) ; f void Justify (HDC hdc, PTSTR pText, RECT * prc, int iAlign) int xStart, yStart, cSpaceChars ; PTSTR pBegin, pEnd ; i SIZE size ; yStart = prc->top ; do // dla każdego wiersza tekstu f cSpaceChars = 0 ; // inicjuj liczbę spacji w wierszu I while (*pText - ' ') // pomiń spacje poczdtkowe i pText++ ; pBegin = pText ; // ustaw wskaŸnik do lańcucha tekstu na // poczštku wiersza // dopóki znany jest wiersz pEnd = pText ; // ustaw wskaŸnik do lańcucha tekstu na końcu // wiersza // przejdŸ do kolejnej spacji while (*pText != '\0' && *pText++ != ' ') ; ! if (*pText =- '\0') ' break ; // po napotkaniu spacji, wyznacz współrzędne graniczne tekstu I f cSpaceChars++ ; GetTextExtentPoint32(hdc, pBegin, pText - pBegin - 1, &size) ; I while (size.cx < (prc->right - prc->left)) ; cSpaceChars-- , // pomiń ostatnid spację na końcu wiersza ł while (*(pEnd - 1) =- ' ') // wyeliminuj spacje końcowe Rozdział 17: Tekst i czcionki 945 pEnd-- cSpaceChars-- , // Jeœli koniec tekstu i nie ma już spacji, ustaw pEnd na końcu if (*pText == '\0' ţţ cSpaceChars <= 0) pEnd = pText ; s' GetTextExtentPoint32 (hdc, pBegin, pEnd - pBegin, &size) ; l switch (iAlign) // zastosuj wyrównanie do xStart ( case IDM_ALIGN_LEFT: xStart = prc->left ; break ; case IDM ALIGN RIGHT: ř xStart = prc->right - size.cx ; break ; ł case IDM ALIGN_CENTER: xStart = (prc->right + prc->left - size.cx) / 2 ; break : case IDM_ALIGN JUSTIFIED: if (*pText != '\0' && cSpaceChars > 0) SetTextJustification (hdc, prc->right - prc->left - size.cx, cSpaceChars) ; xStart = prc->left ; break ; ł. // wyţwietl tekst ; TextOut (hdc, xStart, yStart, pBegin, pEnd - pBegin) ; // przygotuj się do następnego wiersza SetTextJustification (hdc. 0, 0) ; yStart += size.cy ; pText = pEnd ; while (*pText && yStart < prc->bottom - size.cy) ; ` F LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) static CHOOSEFONT cf ; static DOCINFO di = ( sizeof (DOCINFO), TEXT ("Justifyl: Printing") ) ; static int iAlign = IDM ALIGNţLEFT ; static LOGFONT lf ; static PRINTDLG pd ; static TCHAR szText[] = ( TEXT ("You don't know about me, without you ") TEXT ("have read a book by the name of \"The ") TEXT ("Adventures of Tom Sawyer,\" but that ") TEXT ("ain't no matter. That book was made by ") 946 Częœć II: Grafika (cišg dalszy ze strony 945) TEXT ("Mr. Mark Twain, and he told the truth, ") TEXT ("mainly. There was things which he ") TEXT ("stretched, but mainly he told the truth. ") TEXT ("That is nothing. I never seen anybody ") TEXT ("but lied, one time or another, without ") TEXT ("it was Aunt Polly, or the widow, or ") TEXT ("maybe Mary. Aunt Polly -- Tom's Aunt ") TEXT ("Polly, she is -- and Mary, and the Widow ") TEXT ("Douglas, is all told about in that book ") TEXT ("-- which is mostly a true book; with ") TEXT ("some stretchers, as I said before.") ) ; BOOL fSuccess ; HDC hdc, hdcPrn ; HMENU hMenu ; int iSavePointSize ; ! PAINTSTRUCT ps ; RECT rect ; I switch (message) t case WM_CREATE: // Inicjuj strukturę CHOOSEFONT GetObject (GetStockObject (SYSTEM FONT), sizeof (lf), &lf) ; cf.lStructSize = sizeof (CHOOSEFONT) ; cf.hwndOwner = hwnd ; cf.hDC = NULL ; cf.lpLogFont = &lf ; cf.iPointSize = 0 ; cf.Flags = CF_INITTOLOGFONTSTRUCT ţ CF SCREENFONTS ţ ' CFţEFFECTS ; I cf.rgbColors = 0 ; cf.lCustOata = 0 ; cf.lpfnHook = NULL ; cf.lpTemplateName = NULL ; cf.hInstance = NULL ; cf.lpszStyle = NULL ; cf.nFontType = 0 ; cf.nSizeMin = 0 ; I cf.nSizeMax = 0 ; return 0 ; case WM_COMMAND: hMenu = GetMenu (hwnd) ; switch (LOWORD (wParam)) ( case IDM FILE PRINT: // Pobierz kontekst urzšdzenia drukarki pd.lStructSize = sizeof (PRINTDLG) ; pd.hwndOwner = hwnd ; pd.Flags = PDţRETURNDC ţ PD NOPAGENUMS ţ PD NOSELECTION ; 1 Rozdział 17: Tekst i czcionki g47 if'(!PrintDlg (&pd)) return 0 ; if (NULL = (hdcPrn = pd.hDC)) ( MessageBox (hwnd, TEXT ("Cannot obtain Printer DC"), szAppName, MBţICONEXCLAMATION ţ MB OK) ; return 0 ; J // Ustal szerokoœć marginesów na 1 cal rect.left = GetOeviceCaps (hdcPrn, LOGPIXELSX) - GetDeviceCaps (hdcPrn, PHYSICALOFFSETX) ; rect.top = GetOeviceCaps (hdcPrn, LOGPIXELSY) - GetDeviceCaps (hdcPrn, PHYSICALOFFSETY) ; rect.right = GetOeviceCaps (hdcPrn, PHYSICALWIDTH) - GetDeviceCaps (hdcPrn, LOGPIXELSX) - GetDeviceCaps (hdcPrn, PHYSICALOFFSETX) ; rect.bottom = GetDeviceCaps (hdcPrn, PHYSICALHEIGHT) - GetDeviceCaps (hdcPrn, LOGPIXELSY) - GetDeviceCaps (hdcPrn, PHYSICALOFFSETY) ; // Wydrukuj tekst na drukarce SetCursor (LoadCursor (NULL, IDC WAIT)) ; ShowCursor (TRUE) ; fSuccess = FALSE ; if ((StartDoc (hdcPrn, &di) > 0) && (StartPage (hdcPrn) > 0)) // Wybierz czcionkę używajšc zmiennej lfHeight iSavePointSize = lf.lfHeight ; lf.lfHeight = -(GetDeviceCaps (hdcPrn, LOGPIXELSY) * cf.iPointSize) / 720 ; SelectObject (hdcPrn, CreateFontIndirect (&lf)) ; lf.lfHeight = iSavePointSize ; // Ustal kolor tekstu SetTextColor (hdcPrn, cf.rgbColors) ; // Wyœwietl tekst Justify (hdcPrn, szText, &rect, iAlign) ; if (EndPage (hdcPrn) > 0) I f fSuccess = TRUE ; EndDoc (hdcPrn) ; ) ) ShowCursor (FALSE) ; 4 948 Częœć II: Grafika (cišg dalszy ze strony 947) SetCursor (LoadCursor (NULL, IDC ŽRROW)) ; DeleteDC (hdcPrn) ; if (!fSuccess) MessageBox (hwnd, TEXT ("Could not print text"), szAppName, MBţICONEXCLAMATION ţ MB OK) ; return 0 ; case IDM_FONT: if (ChooseFont (&cf)) InvalidateRect (hwnd, NULL, TRUE) ; return 0 ; case IDM_ALIGN_LEFT: case IDM_ALIGN_RIGHT: case IDM_ALIGN_CENTER: case IDM_ALIGN_JUSTIFIED: CheckMenuItem (hMenu, iAlign, MF UNCHECKED) ; iAlign = LOWORD (wParam) ; CheckMenuItem (hMenu, iAlign, MF_CHECKED) ; InvalidateRect (hwnd, NULL, TRUE) ; return 0 ; ) return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; GetClientRect (hwnd, &rect) ; DrawRuler (hdc, &rect) ; - rect.left += GetDeviceCaps (hdc, LOGPIXELSX) / 2 ; rect.top += GetDeviceCaps (hdc, LOGPIXELSY) / 2 ; rect.right -= GetDeviceCaps (hdc, LOGPIXELSX) / 4 ; SelectObject (hdc, CreateFontIndirect (&lf)) ; SetTextColor (hdc, cf.rgbColors) ; Justify (hdc, szText, &rect, iAlign) ; I Delete0bject (SelectObject (hdc, GetStockObject (SYSTEMţFONT)Ż ; EndPaint (hwnd, &ps) ; return 0 ; case WM DESTROY: PostOuitMessage (0) ; return 0 ; ,' 1 j return DefWindowProc (hwnd, message, wParam, lParam) ; ; ) JUSTIFYl.RC (fragmenty) //Microsoft Developer Studio 9enerated resource script. i Rozdział 17: Tekst i czcionki 949 llinclude "resource.h" lpinclude "afxres.h" /////////////////////////////////////////////////////////////////////////// // Menu JUSTIFYI MENU DISCARDABLE BEGIN POPUP "&File" BEGIN MENUITEM "&Print", IDM FILE PRINT END POPUP "&Font" BEGIN MENUITEM "&Font... , IDM FONT END POPUP "&Align" BEGIN MENUITEM "&Left", IDM_ALIGN_LEFT, CHECKED MENUITEM "&Right", IDM_ALIGN RIGHT MENUITEM "&Centered", IDM_ALIGN CENTER MENUITEM "&Justified", IDM ALIGN JUSTIFIED END END RESOURCE.H // Microsoft Developer Studio generated include file. // Used by Justifyl.rc lldefine IDM_FILE_PRINT 40001 4ldefine IDMţFONT 40002 ţldefine IDM ALIGN_LEFT 40003 lldefine IDM_ALIGN_RIGHT 40004 lldefine IDM_ALIGN_CENTER 40005 4ldefine IDM ALIGN JUSTIFIED 40006 Rysunek 17-9. Program JUSTIFYI Program JUS'TIFYI rysuje linijkę (naturalnie w calach logicznych) ponad tekstem i po jego lewej stronie za pomocš funkcji DrawRuler. Struktura opisujšca prosto- kšt tekstu zawiera parametry obszaru, w którym tekst ma być justowany. Przeważajšca częœć kodu programu odpowiedzialna za formatowarue tekstu znaj- duje się w funkcji justify. Funkcja rozpoczyna poszukiwanie spacji od paczštku tekstu wywołujšc funkcję GetTextExtentPoint32 w celu zmierzenia szerokoœci tek- stu. Jeœli przekracza ona rozmiary obszaru roboczego, funkeja zwraca poprzed- niš spację i umieszcza w bieżšcym wierszu tekst ograniczony tyrn znakiem. W za- leżnoœci od wartoœci stalej iAlign, wiersz jest wyrównywany do lewego lub pra- wego marginesu, umieszczany poœrodku bšdŸ justowany. Program JUSTIFYI nie jest, oczywiœcie, doskonały. Nie uwzględnia na przykład obsługi myœlników. Aplikacja nie potrafi również poradzić sobie z przypadkiem, gdy w wierszu znajdujš się mniej niż dwa słowa. Nawet gdy rozwišżemy ten problem, który nie jest zresztš bardzo trudny, program i tak nie zachowa się po- 950 Czgœć II: Grafika prawnie, jeœli w wierszu znajdzie się słowo o dhxgoœci przekraczajšcej szerokoœć obszaru roboczego. Oczywiœcie sytuacja może się skomplikować jeszcze bardziej, gdy w pojedynczym wierszu chciałbyœ zastosować różne czcionki (z czym z ła- twoœciš radzš sobie powszechnie znane procesory tekstu systemu Windows). Cóż, rukt nie twierdzi, że programowanie w Windows jest łatwe, upraszcza je tylko do pewnego stopnia zastosowanie istniejšcych narzędzi. you don't know about me, without you have read a book by the name of "The Adventures of Tom Sawyer,"' but that ain't no matter. Thot book was mnde by Mr. Mark Twnin, and he told the truth, mainly. There was things which he stretched, but mainly he told the truth. That is nothing. I never seen anybody but lied, one time or another, without it was Aunt Polly, or the widow, or mnybe Mcţry. Aunt Polly -- Tom's Aunt Polly, she is -- ond Mary, ond the Widow Douglas, is all told about in thcţt book -- which is mostly n true book; with some stretchers, os I said before. Rysunek 17.10. Wynik działania programu JUSTIFYI Podglšd wydruku Tekst edytuje się nie po to, by przyglšdać mu się na ekranie, ale aby go wydruko- wać. Zwykle podglšd wydruku musi dokładnie odpowiadać ustawieniom danej drukarki. Nie wystarczy na podglšdzie wydruku pokazać te same czcionki o od- powiednich rozmiarach i formatowaniu. Konieczne jest również, aby wiersze tek- stu kończyły się dokładnie w tych samych miejscach na ekranie i na wydruku. Jest to najtrudniejsze zadanie, realizowane przez technologię WYSIWYG. Program JUSTIFY1 wyposażony jest w obsługę polecerua Print, lecz zajmuje się je- dynie ustawieniem jednocalowych marginesów po prawej i lewej strorue oraz u góry i dołu obszaru roboczego. Dzięki temu formatowanie jest całkowicie niezależne od rozmiarów ekranu. Oto ciekawe cwiczenie: zrnień kilka lin kodu jUSTIFY1 tak, aby zależnoœci zwišzane z drukarkš oraz z ekranem oparte były na szeœciocalowym pro- stokšcie formatujšcym. W tym celu zmień definicje zmiennej rect.right we fragmen- tach kodu obshxgujšcych komunikat WMţPAINT oraz polecenie Print. W komunikacie WMţPAINT podstawienie wyglšda następujšco: rect.right = rect.left + 6 * GetDeviceCaps (hdc, LOGPIXELSX) ; a w poleceniu Print: rect.right = rect.left + 6 * GetDeviceCaps (hdcPrn, LOGPIXELSX) ; Rozdział 17: Tekst i czcionki g51 Jeœli wybierzesz czcionkę TrueType, wiersze powinny kończyć się na ekranie i na wydruku dokładnie w tych samych miejscach. Niestety, tak się nie dzieje. Chociaż oba urzšdzerua (karta graficzna i drukarka) używajš tej samej czcionki o tych samych rozmiarach i wyœwietlajš tekst we wnę- trzu prostokšta o takich samych rozmiarach, inna rozdzielczoœć i błędy zaokršgleń powodujš, że wiersze kończš się w różnych miejscach. Podglšdanie wydruku wy- maga więc bardziej złożonego podejœcia do podziahz tekstu na wiersze. Próbę rozwišzania tego problemu demonstruje program JUSTIFY2, przedstawio- ny na rysunku 17-11. Kod Ÿródłowy JUSTlFY2 oparty jest na programie TTHUST, napisanym przez Davida Weise'a z Microsoftu. Z kolei Tlţi(JST napisano na pod- stawie programu JUSTIFYI, prezentowanego w poprzednim wydaniu tej ksišżki. Jako symbol postępu (bioršc pod uwagę złożonoœć kolejnych programów) fragmenty prozy Marka Twaina zastšpiono urywkami Moby Dicka Hermana Melville'a. JUSTIFY2.C /* JUSTIFY2.C - Tekst justowany - program iţ2 (c) Charles Petzold, 1998 */ iţinclude iţinclude "resource.h" iţdefine OUTWIDTH 6 //.Szerokoœć formatowanego wyjœcia w calach define LASTCHAR 127 // Kod ostatniego znaku tekstu L RESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; TCHAR ,szAppNameC] = TEXT ("Justify2") ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) HWND hwnd ; MSG msg ; f WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CS VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITEţBRUSH) ; wndclass.lpszMenuName = szAppName ; ,: wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) f MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MBţICONERROR) ; return 0 ; 952 Częœó II: Grafika (cišg dalszy ze strony 951) hwnd = CreateWindow (szAppName, TEXT ("Justified Type ţţ2"), WS OVERLAPPEDWINDOW, CW USEDEFAULT, CW_USEDEFAULT, CW USEDEFAULT, CWţUSEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0>) i TranslateMessa9e (&msg) ; DispatchMessage (&msg) ; ' ) return msg.wParam ; void DrawRuler (HDC hdc, RECT * prc) static int iRuleSize C16] = ( 360, 72, 144, 72, 216, 72, 144, 72, 288, 72, 144, 72, 216, 72, 144, 72 ? ; int i, j ; POINT ptClient ; SaveDC (hdc) ; // Ustaw tryb odwzorowania Logical Twips SetMapMode (hdc, MM ŽNISOTROPIC) ; SetWindowExtEx (hdc, 1440. 1440, NULL) ; ' SetViewportExtEx (hdc, GetDeviceCaps (hdc, LOGPIXELSX), GetDeviceCaps (hdc, LOGPIXELSY), NULL) ; I // Odsuń poczdtek ukl. wspólrzędnych o pół cala od lewego górnego narożnika SetWindowOrgEx (hdc, -720, -720, NULL) ; // Ustaw prawy margines (ćwierć cala od prawej krawędzi strony) j ptClient.x = prc->right ; ptClient.y = prc->bottom ; DPtoLP (hdc, &ptClient, 1) ; ptClient.x -= 360 ; // Narysuj linijki MoveToEx (hdc, 0, -360, NULL) ; LineTo (hdc, OUTWIDTH * 1440, -360) ; MoveToEx (hdc, -360, 0, NULL) ; I LineTo (hdc, -360, ptClient.y) ; for (i = 0, j = 0 ; i <= ptClient.x && i <= OUTWIDTH * 1440 ; i += 1440 / 16, j++) MoveToEx (hdc, i, -360, NULL) ; Rozdział 17: Tekst i czcionki 953 LineTo (hdc, i, -360 - iRuleSize [j % 16]) ; ) for (i = 0, j = 0 ; i <= ptClient.y ; i += 1440 / 16, j++) MoveToEx (hdc, -360, i, NULL) ; LineTo (hdc, -360 - iRuleSize Cj % 16], i) ; ) RestoreDC (hdc, -1) ; ) /* GetCharDesignWidths: Pobiera szerokoœć znakbw czcionki zgodnie z projektem */ UINT GetCharDesignWidths (HDC hdc, UINT uFirst, UINT uLast, int * piWidths) ( i HFONT hFont, hFontDesign ; LDGFONT lf ; OUTLINETEXTMETRIC otm ; hFont = GetCurrentObject (hdc, OBJţFONT) ; GetObject (hFont, sizeof (LOGFONT), &lf) ; // Pobierz parametry tekstu (będziemy używać tylko pól // niezależnych od kontekstu urzddzenia, w którym wybrano czcionkę) otm.otmSize = sizeof (OUTLINETEXTMETRIC) ; !I GetOutlineTextMetrics (hdc, sizeof (OUTLINETEXTMETRIC), &otm) ; !, // Utwórz nowd czcionkę lf.lfHeight = - (int) otm.otmEMSquare ; lf.lfWidth = 0 ; hFontDesign = CreateFontIndirect (&lf) ; ' // Wybierz czcionkę w kontekœcie urzšdzenia i pobierz jej szerokoœć SaveDC (hdc) ; SetMapMode (hdc, MM_TEXT) ; SelectObject (hdc, hFontDesign) ; GetCharWidth (hdc, uFirst, uLast, piWidths) ; SelectObject (hdc, hFont) ; RestoreDC (hdc, -1) ; // Posprzdtaj Delete0bject (hFontDesign) ; return otm.otmEMSquare ; ! ) /* GetScaledWidths: Pobiera szerokoœć znaku dla wybranego rozmiaru czcionki */ void GetScaledWidths (HDC hdc, double * pdWidths) 954 Częœć II: Grafika (cigg dalszy ze strony 953) double dScale ; HFONT hFont ; int aiDesignWidths CLASTCHAR + 1] ; int i ; LOGFONT lf ; UINT uEMSquare ; // Wywolaj funkcję uEMSquare = GetCharDesignWidths (hdc, 0, LASTCHAR, aiDesignWidths) ; // Pobierz strukturę LOGFONT dla bieżdcej czcionki hFont = GetCurrentObject (hdc, OBJ_FONT) ; GetObject (hFont, sizeof (LOGFONT), &lf) ; // Skaluj szerokoœci i przechowuj w zmiennej typu float dScale = (double) -lf.lfHeight / (double) uEMSquare ; for (i = 0 ; i <= LASTCHAR ; i++) pdWidthsCi] = dScale * aiDesignWidthsCi7 ; ) /* GetTextExtentFloat: Wylicza szerokoœć tekstu */ double GetTextExtentFloat (double * pdWidths, PTSTR psText, int iCount) ( double dWidth = 0 ; int i ; for (i = 0 ; i < iCount ; i++) dWidth += pdWidths CpsTextCi]7 ; return dWidth ; /* i Justify: Na podstawie jednostek zgodnych z ekranem/drukarkš */ void Justify (HDC hdc, PTSTR pText, RECT * prc, int iAlign) ( double dWidth, adWidthsCLASTCHAR + 17 ; int xStart, yStart, cSpaceChars ; PTSTR pBegin, pEnd ; SIZE size ; // Wypelnij tablicę adWidths wartoœciami szerokoœci znaków GetScaledWidths (hdc, adWidths) ; yStart = prc->top ; do // dla każdego wiersza tekstu Rozdział 17: Tekst i czcionki 955 ( cSpaceChars = 0 ; // inicjuj liczbę spacji w wierszu while (*pText = ' ') // pomiń spacje na końcu wiersza pText++ ; pBegin = pText ; // ustaw wskaŸnik do łańcucha tekstu na // poczštku wiersza do // dopóki znany jest wiersz ( pEnd = pText ; // ustaw wskaŸnik do lańcucha tekstu na końcu // wiersza // przejdŸ do kolejnej spacji while (*pText != '\0' && *pText++ != ' ') ; if (*pText = '\0') break ; // po napotkaniu spacji, wyznacz wspólrzędne graniczne // tekstu cSpaceChars++ ; dWidth = GetTextExtentFloat (adWidths, pBegin, pText - pBegin - 1) ; ) while (dWidth < (double) (prc->right - prc->left)) ; cSpaceChars-- , // pomiń ostatniš spację na końcu wiersza while (*(pEnd - 1) =- ' ') // eliminate trailing spaces , pEnd-- cSpaceChars-- , J // Jeœli koniec tekstu i nie ma już spacji // ustaw pEnd na końcu if (*pText = '\0' ţţ cSpaceChars <= 0) pEnd = pText ; // Teraz pobierz wspólrzędne końca tekstu GetTextExtentPoint32(hdc, pBegin, pEnd - pBegin, &size) ; switch (iAlign) // zastosuj wyrównanie do xStart 1 case IDM_ALIGN_LEFT: xStart = prc->left ; break ; case IDM_ALIGN_RIGHT: xStart = prc->right - size.cx ; break ; Częœć II: Grafika (cišg dalszy ze strony 955) case IDM ALIGN_CENTER: xStart = (prc->right + prc->left - size.cx) / 2 ; break ; case IDM_ALIGN JUSTIFIED: if (*pText != '\0' && cSpaceChars > 0) SetTextJustification (hdc, prc->right - prc->left - size.cx, cSpaceChars) ; xStart = prc->left ; break ; // wyœwietl tekst TextOut (hdc, xStart, yStart, pBegin, pEnd - pBegin) ; // przygotuj się do następnego wiersza SetTextJustification (hdc, 0, 0) ; yStart += size.cy ; pText = pEnd ; 1 while (*pText && yStart < prc->bottom - size.cy) : LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ( static CHOOSEFONT cf ; static DOCINFO di = ( sizeof (DOCINFO), TEXT ("Justify2: Printing") ) ; static int iAlign = IDM ALIGNţLEFT ; static LOGFONT lf ; static PRINTDLG pd ; static TCHAR szText[7 = ( TEXT ("Call me Ishmael. Some years ago -- never ") TEXT ("mind how long precisely -- having little ") TEXT ("or no money in my purse, and nothing ") TEXT ("particular to interest me on shore, I ") TEXT ("thought I would sail about a little and ") TEXT ("see the watery part of the world. It is ") TEXT ('a way I have of driving off the spleen, ") TEXT ("and regulating the circulation. Whenever ") TEXT ("I find myself growing grim about the ") TEXT ("mouth; whenever it is a damp, drizzly ") TEXT ("November in my soul; whenever I find ") TEXT ("myself involuntarily pausing before ") TEXT ("coffin warehouses, and bringing up the ") TEXT ("rear of every funeral I meet; and ") TEXT ("especially whenever my hypos get such an ") TEXT ("upper hand of me, that it requires a ") TEXT ("strong moral principle to prevent me ") TEXT ("from deliberately stepping into the ") TEXT ("street, and methodically knocking ") TEXT ("people's hats off -- then, I account it ") TEXT ("high time to get to sea as soon as I ") TEXT ("can. This is my substitutg for pistol ") TEXT ("and ball. With a philosophical flourish ") , . Rozdział 17: Tekst i czcionki 957 TEXT ("Cato throws himself upon his sword; I ") TEXT ("quietly take to the ship. There is ") TEXT ("nothing surprising in this. If they but ") TEXT ("knew it, almost all men in their degree, ") TEXT ("some time or other, cherish very nearly ") TEXT ("the same feelings towards the ocean with ") TEXT ("me.") ) ; BOOL fSuccess ; HDC hdc. hdcPrn ; HMENU hMenu ; int iSavePointSize ; PAINTSTRUCT ps ; RECT rect ; switch (message) ( case WM_CREATE: // Inicjuj strukturę CHOOSEFONT hdc = GetDC (hwnd) ; lf.lfHeight = - GetOeviceCaps (hdc, LOGPIXELSY) / 6 ; lf.lfOutPrecision = OUT_TT_ONLY_PRECIS ; lstrcpy (lf.lfFaceName, TEXT ("Times New Roman")) ; ReleaseDC (hwnd, hdc) ; cf.lStructSize = sizeof (CHOOSEFONT) ; cf.hwndOwner = hwnd ; cf.hDC = NULL ; cf.lpLogFont = &lf ; cf.iPointSize = 120 ; // Ustaw znaczniki jedynie w przypadku czcionek TrueType! cf.Flags = CF_INITTOLOGFONTSTRUCT ţ CF SCREENFONTS CF TTONLY ţ CF EFFECTS ; cf.rgbColors = 0 ; cf.lCustOata = 0 ; cf.lpfnHook = NULL ; cf.lpTemplateName = NULL ; cf.hInstance = NULL ; cf.lpszStyle = NULL ; cf.nFontType = 0 ; cf.nSizeMin = 0 ; cf.nSizeMax = 0 ; return 0 ; case WM_COMMAND: hMenu = GetMenu (hwnd) ; switch (LOWORD (wParam)) ( case IDM_FILE_PRINT: // Pobierz kontekst urzšdzenia pd.lStructSize = sizeof (PRINTDLG) ; pd.hwndOwner = hwnd ; pd.Flags =.PDţRETURNDC ţ PDţNOPAGENUMS ţ PDţNOSELECTION ; 958 Częœć II: Grafika (cigg dalszy ze strony 957) if (!PrintDlg (&pd)) return 0 ; if (NULL --- (hdcPrn = pd.hDC)) ( MessageBox (hwnd, TEXT ("Cannot obtain Printer DC"), szAppName, MBţICONEXCLAMATION ţ MB OK) ; return 0 ; ) // Ustal szerokoœć marginesów na OUTWIDTH rect.left = (GetDeviceCaps (hdcPrn, PHYSICALWIDTH) - GetOeviceCaps (hdcPrn, LOGPIXELSX) * OUTWIDTH) / 2 - GetDeviceCaps (hdcPrn, PHYSICALOFFSETX) ; rect.right = rect.left + GetDeviceCaps (hdcPrn, LOGPIXELSX) * OUTWIDTH ; ' // Ustal 1-calowd szerokoœć marginesów górnego i dolnego rect.top = GetOeviceCaps (hdcPrn, LOGPIXELSY) - GetDeviceCaps (hdcPrn, PHYSICALOFFSETY) ; rect.bottom = GetDeviceCaps (hdcPrn, PHYSICALHEIGHT) - GetDeviceCaps (hdcPrn, LOGPIXELSY) - GetDeviceCaps (hdcPrn, PHYSICALOFFSETY) ; // Wydrukuj tekst SetCursor (LoadCursor (NULL, IDCţWAIT)) ; ShowCursor (TRUE) ; fSuccess = FALSE ; r if ((StartDoc (hdcPrn, &di) > 0) && (StartPage (hdcPrn) > 0)) ( // Wybierz czcionkę stosujšc lfHeight iSavePointSize = lf.lfHeight ; , lf.lfHeight = -(GetDeviceCaps (hdcPrn, LOGPIXELSY) * cf.iPointSize) / 720 ; SelectObject (hdcPrn, CreateFontIndirect (&lf)) ; lf.lfHeight = iSavePointSize ; // Ustaw kolor tekstu i SetTextColor (hdcPrn, cf.rgbColors) ; // Wyœwietl tekst r Justify (hdcPrn, szText, &rect, iAlign) ; if (EndPage (hdcPrn) > 0) i i Rozdział 17: Tekst i czcionki 959 fSuccess = TRUE ; EndDoc (hdcPrn) ; i. 7 7 f f I ShowCursor (FALSE) ; SetCursor (LoadCursor (NULL, IDC ŽRROW)) ; DeleteDC (hdcPrn) ; if (!fSuccess) MessageBox (hwnd, TEXT ("Could not print text"), I szAppName, MBţICONEXCLAMATION ţ MB OK) ; return 0 ; case IDM_FONT: if (ChooseFont (&cf)) InvalidateRect (hwnd, NULL, TRUE) ; return 0 ; case IDM_ALIGN_LEFT: case IDM_ALIGN_RIGHT: ; case IDM_ALIGN_CENTER: case IDM_ALIGN_JUSTIFIED: CheckMenuItem (hMenu, iAlign, MF UNCHECKED) ; iAlign = LOWORD (wParam) ; CheckMenuItem (hMenu, iAlign, MF_CHECKED) ; InvalidateRect (hwnd, NULL, TRUE) ; return 0 ; return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; T GetClientRect (hwnd, &rect) ; DrawRuler (hdc, &rect) ; 1 rect.left += GetDeviceCaps (hdc, LOGPIXELSX) / 2 ; rect.top += GetDeviceCaps (hdc, LOGPIXELSY) / 2 ; rect.right = rect.left + OUTWIDTH * GetDeviceCaps (hdc, LOGPIXELSX) ; SelectObject (hdc, CreateFontIndirect (&lf)) ; SetTextColor (hdc, cf.rgbColors) ; Justify (hdc, szText, &rect, iAlign) ; Delete0bject (SelectObject (hdc, GetStockObject (SYSTEM FONT))); EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: . PostOuitMessage (0) ; return 0 ; return.DefWindowProc (hwnd, message, wParam, lParam) ; 960 Częœć II: Grafika (cišg dalszy ze strony 959) JUSTIFY2.RC //Microsoft Developer Studio generated resource script. ţtinclude "resource.h" Iţinclude "afxres.h" /////////////////////////////////////////////////////////////////////////// // Menu JUSTIFY2 MENU DISCARDABLE BEGIN POPUP "&File" BEGIN MENUITEM "&Print", IDMţFILE PRINT END POPUP "&Font" BEGIN MENUITEM "&Font... , IDM_FONT END POPUP "&Align" BEGIN MENUITEM "&Left", IDM_ALIGN_LEFT, CHECKED MENUITEM "&Right", IDM_ALIGN_RIGNT MENUITEM "&Centered", IDM ALIGN_CENTER MENUITEM "&Justified", IDM ALIGN JUSTIFIED END END RESOURCE.H // Microsoft Developer Studio generated include file. // Used by Justify2.rc Ildefine IDM_FILE_PRINT 40001 Ildefine IDM_FONT 40002 ţţdefine IDM ALIGN_LEFT 40003 Ildefine IDM ŽLIGN_RIGHT 40004 Ildefine IDM_ALIGN_CENTER 40005 Ildefine IDM ALIGNţJUSTIFIED 40006 Rysunek 17ů11. Program JUSTIFY2 Program JUSTIFY2 współpracuje jedynie z czcionkami TrueType. Funkcja GetChar- DesignWidths wywołuje GetOutIineTextMetrics w celu pozyskania pozornie bez- wartoœciowej informacji. Chodzi o pole otmEMSguare struktury OUTLINETEXT METRIC. , Czcionka TrueType jest zaprojektowana na podstawie siatki em-kwadratowej (jak już wspomniano, przedrostek "em" odnosi się do szerokoœci kwadratu, w który wpisany jest znak czcionki równej szerokoœci wielkiej litery M). Wszystkie znaki dowolnej czcionki TrueType zostały zaprojektowane na podstawie tej samej siat- Rozdział 17: Tekst i czcionki 961 ki, choć w rzeczywistoœci ich szerokoœci sš różne. Pole otmEMSguare struktury OUTLINETEXTMETRIC odpowiada za nadanie tej samej szerokoœci em-kwadratu każdej czcionce. Dla większoœci czcionek TrueType wartoœć przyjmowana przez to pole wynosi 2048, co oznacza, że dana czcionka została zaprojektowana na siatce o rozmiarach 2048 na 2048. Oto wyjaœnienie. Możesz zainicjować pola struktury LOGFONT dla okreœlonego kroju czcionki TrueType, lecz pole lfHeight musi przyjšć wartoœć równš wartoœci bezwzględnej otmEMSguare, ale ze znakiem ujemnym. Po utworzeniu takiej czcion- ki i wybraniu jej w kontekœcie urzšdzenia możesz wywołać funkcję GetCharWidth. Dzięki niej otrzymujesz szerokoœć poszczególnych znaków czcionki, podawanš w jednostkach logicznych. Szerokoœci znaków nie sš podawane dokładnie, gdyż uległy skalowaniu do innego rozmiaru. Szerokoœć znaków ustalana na podsta- wie wartoœci zmiennej otmEMSquare, zawsze wyznaczana jest dokładnie, nieza- leżnie od kontekstu urzšdzenia. Funkcja GetCharDesignWidths uzyskuje w ten sposób oryginalne wymiary projek- towe czcionki i przechowuje je w tablicy elementów typu integer. Program JUSTI- FY2 wie, że wszystkie wyœwietlane znaki tekstu pochodzš z zakresu ASCII, więc tablica nie musi mieć wielkich rozmiarów. Funkcja GetScaledWidths zamienia cał- kowite wartoœci szerokoœci znaków na zxniennoprzecinkowe na podstawie rzeczy- wistego rozmiaru punktowego czcionki, wyrażonego w jednostkach logicznych zwišzanych z urzšdzeniem. Wartoœci zmiennoprzecinkowe używane sš przez funk- cję GetTextExtentFloat do wyznaczenia szerokoœci całego łańcucha tekstowego. W ten właœnie sposób funkcja Justţ wylicza szerokoœć wierszy tekstu. Trochę œmiesznych i fantazyjnych efektów Myœlenie o czcionce w kategoriach konturu otwiera przed programistš możliwo- œci zastosowania różnorodnych technik graficznych. W ostatniej częœci tego roz- działu przedstawię kilka interesujšcych efektów. Zanim rozpoczniemy, rzućmy okiem na dwa elementarne pojęcia grafiki: œcieżki graficzne oraz pisaki o rozsze- rzonych możliwoœciach. Œcieżka GDI Œcieżka jest zbiorem lin prostych i krzywych przechowanych wewnętrznie przez GDI. Œcieżki zostały wprowadzone po raz pierwszy w 32-bitowych wersjach Windows. Pojęcie œcieżki poczštkowo będzie ci przypominać regiony, rzeczywi- œcie możliwa jest konwersja œcieżki na format regionu i zastosowanie jej do wyci- nania powierzchni. Poniżej przedstawimy w skrócie najważniejsze różnice mię- dzy nimi. Aby zdefiniować œcieżkę, wywołaj funkcję: BeginPath (hdc) : Po takim wywołaniu każda narysowana przez ciebie linia (prosta, łuk, krzywa Beziera) będzie przechowywana w postaci œcieżki. Zwykle œcieżka składa się z po- łšczonych lin. Aby połšczyć linie, należy zastosować jednš z funkcji: LineTo, Po- lylineTo i BezierTo. Każda rysuje linie rozpoczynajšc od pozycji bieżšcej. Jeœli zmie- 962 Częœć II: Grafika nisz bieżšcš pozycję za pomocš funkcji MoveToEx lub wywołujšc dowolnš funk- cję rysujšcš linie bšdŸ też używajšc innej zmieruajšcej bieżšcš pozycję pisaka, utworzysz nowš podœcieżkę istniejšcej œcieżki. W ten sposób œcieżka składa się zwykle z wielu podœcieżek, które z kolei złożone sš z połšczonych lin. Każda podœcieżka może być otwierana i zamykana. W podœcieżce zamkniętej pierwszy punkt pierwszej lin składowej jest taki sam jak ostatni punkt ostatniej lin składowej. Co więcej, œcieżka zamknięta zakończona jest w programie wy- wołaniem funkcji CIoseFigure. Jeœli to konieczne, funkcja CloseFigure zamyka pod- œcieżkę liniš prostš. Każde następne wywołanie funkcji rysujšcej linię spowodu- je otwarcie nowej podœcieżki. Definicja œcieżki musi być zakończona poleceniem: EndPath (hdc) ; W tym momencie możesz wywołać jednš z pięciu przedstawionych poniżej funkcji; StrokePath (hdc) ; FillPath (hdc) ; StrokeAndFillPath (hdc) ; hRgn = PathToRegion (hdc) ; SelectClipPath (hdc, iCombine) ; Po wykonaniu każdej z tych funkcji niszczona jest definicja utworzonej œcieżki. Funkcja StrokePath rysuje œcieżkę używajšc bieżšcego pisaka. Mógłbyœ zapytać, o co chodzi? Dlaczego nie możesz oszczędzić sobie zamieszania ze œcieżkami i po pro- stu narysować lin w tradycyjny sposób? Oto krótkie wyjaœnienie. Pozostałe cztery funkcje zamykajš podœcieżki liniami prostymi. Funkcja FiIlPath wypełnia œcieżkę za pomocš bieżšcego pędzla zgodnie z trybem wypełniania wie- lokštów. StrokeAndFillPath realizuje oba zadania w jednym kroku. Możesz również zamienić œcieżkę na region lub wykorzystać jš do wycinania obszarów. Argument iCombine przyjmuje wartoœćjednej ze stałych RGN-, stosowanych dla funkcji Com- bineRgn, i okreœla, jak œcieżka łšczy się z bieżšcym obszarem wycięcia. Œcieżki sš bardziej elastyczne niż regiony, jeœli chodzi o wycinanie i wypełnianie. Regiony mogš być definiowane jako połšczenie prostokštów, elips i wielokštów, œcieżki natomiast mogš składać się z krzywych Beziera i - przynajmniej w Win- dows NT - łuków. Œcieżki i regiony sš przechowywane w GDI w odmienny spo- sób: œcieżka jako zbiór definicji lin prostych i krzywych, a region (w ogólnym sensie) jako zbiór lin wchodzšcych w jego skład. Pisaki o rozszerzonych możliwoœciach Kiedy wywołujesz funkcję StrokePath, œcieżka jest renderowana za pomocš bie- żšcego pisaka. W rozdziale 4 omówiłem funkcję CreatePen, która służyła do two- rzenia obiektu pisaka. Wraz z wprowadzeniem techniki œcieżek Windows obsłu- guje również rozszerzonš funkcję pisaka ExtCreatePen. Funkcja ta pokazuje, dla- czego w niektórych sytuacjach korzystniej jest utworzyć œcieżkę i zastosować jš do rysowania lin, niż wykonać te operacje bez udziału œcieżki. Oto wywołanie funkcji ExtCreatePen: hPen = ExtCreatePen (iStyle, iWidth, &lBrush, 0, NULL) ; Rozdział 17: Tekst i czcionki 963 Funkcja może być stosowana do rysowania lin, lecz w tym wypadku ruektóre jej możliwoœci nie sš obsługiwane przez Windows 98, podobnie jak zastosowa- nie do renderowania œcieżki, co oznaczono w powyższym wywołaniu, ustalajšc dwa ostatnie argumenty jako 0 i NULL. Jako pierwszy argument funkcji ExtCreatePen możesz użyć nazwy dowolnego ze stylów opisanych w rozdziale 4 przy omawianiu funkcji CreatePen. Możesz po- nadto łšczyć te style z PS GEOMETRIC, gdzie argument iWidth oznacza gruboœć wyrażonš w jednostkach logicznych, lub PS COSMETIC, gdzie argument iWidth musi mieć wartoœć 1. W Windows 98 pisaki rysujšce linie przerywane lub krop- kowane muszš używać atrybutu PS COSMETIC. Ograniczenie to zostało znie- sione w Windows NT. Jednym z argumentów funkcji CreatePen jest kolor pisaka, funkcja ExtCreatePen natomiast używa do kolorowarua pędzla, który może być zdefiniowany przez mapę bitowš. Rysujšc bardzo grube lire na pewno chciałbyœ wiedzieć, jak będš zakończone. Kiedy takie linie lub krzywe łšczš się ze sobš, ważne jest, jak połšczenie będzie wyglšdać. W wypadku pisaków utworzonych funkcjš CreatePen końcówki lin sš zawsze zaokršglone. Jeœli używasz ExtCreatePen, masz wybór (w Windows 98 możesz wybrać rodzaj zakończenia lin tylko dla œcieżek; sytuacja wyglšda ko- rzystruej w Windows NT). Końcówki szerokich lin mogš być definiowane za pomocš następujšcych stylów pisaka: PS ENCAP ROUND PSţENDCAP SOUARE PS ENDCAP FLAT Styl "square" różni się od "flat" tym, że linia kończy się płasko, lecz dodatkowo wydłużana jest o połowę szerokoœci. Podobnie definiuje się sposoby łšczenia li- n składajšcych się na œcieżkę: PS JOIN ROUND PS JOIN BEVEL PS JOIN MITER Styl "bevel" odcina końcówkę połšczenia, a "miter" zamienia jš w sterczšcy ko- lec. Wszystkie sposoby łšczenia lin najlepiej zademonstruje program ENDJO- IN, przedstawiony na rysunku 17-12. ENDJOIN.C /* ENDJOIN.C - Końcówki i połšczenia - program demonstracyjny (c) Charles Petzold, 1998 */ ttinclude LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) 964 Częœć II: Grafika (cišg clalszy ze strony 963) static TCHAR szAppName[] = TEXT ("EndJoin") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CS VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC ARROW) ; wndc7ass.hbrBackground = (HBRUSH) GetStockObject (WHITEţBRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; t if (!RegisterClass (&wndclass)) ( MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MBţICONERROR) ; return 0 ; 9 hwnd = CreateWindow (szAppName, TEXT ("Ends and Joins Demo"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW USEDEFAULT, CW_USEDEFAULT, CW USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) ! TranslateMessa9e (&msg) ; DispatchMessage (&msg) ; ) return msg.wParam ; t LRESULT CALLBACK WndProc (HWND hwnd, UINT iMsg, WPARAM wParam, LPARAM lParam) ( static int iEnd[] = ( PS_ENDCAP_ROUND, PS ENDCAP_SOUARE, PS_ENDCAP_FLAT ); static int iJoinE]= ( PS_JOINţROUND, PS JOIN BEVEL, PS JOIN MITER ? ; static int cxClient, cyClient ; HDC hdc ; int i ; LOGBRUSH lb ; PAINTSTRUCT ps ; switch (iMsg) case WM_SI2E: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; I Rozdział 17: Tekst i czcionki 965 return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; SetMapMode (hdc, MM ANISOTROPIC) ; SetWindowExtEx (hdc. 100, 100, NULL) ; SetViewportExtEx (hdc, cxClient, cyClient, NULL) ; lb.lbStyle = BS_SOLID ; lb.lbColor = RGB (128, 128, 128) ; lb.lbHatch = 0 ; for (i = 0 ; i < 3 ; i++) SelectObject (hdc, ExtCreatePen (PS_SOLID ţ PS_GEOMETRIC iEnd [i] ţ iJoin [i], 10, ! &lb, 0, NULL)) ; BeginPath (hdc) ; MoveToEx (hdc, 10 + 30 * i, 25, NULL) ; LineTo (hdc, 20 + 30 * i, 75) ; LineTo (hdc, 30 + 30 * i, 25) ; EndPath (hdc) ; StrokePath (hdc) ; Delete0bject ( SelectObject (hdc, GetStockObject (BLACKţPEN))) ; MoveToEx (hdc, 10 + 30 * i, 25, NULL) ; LineTo (hdc. 20 + 30 * i, 75) ; LineTo (hdc, 30 + 30 * i, 25) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: PostOuitMessage (0) ; return 0 ; return DefWindowProc (hwnd, iMsg, wParam, lParam) ; ) Rysunek 17-12. Program ENDJOIN Program rysuje szerokie linie w kształcie litery V, stosujšc różne style końcówek lin oraz połšczeń w przedstawionej powyżej kolejnoœci. Na rysunku widać rów- nież trzy identyczne linie nakreœlone za pomocš cienkiego pisaka, dzięki czemu można porównać sposób tworzerua lin o dużej gruboœci. Wyniki działania pro- gramu przedstawiono na rysunku 17-13. ţřT 966 Czeœć II: Grafika s Rysunek 17-13. Wynik działarua programu ENDJOIN Mam nadzieję, że teraz jest oczywiste, dlaczego Windows stosuje funkcje Stroke- Path. Gdybyœ musiał narysować dwie linie oddzielnie, GDI zastosowałby zakoń- czenia każdej z nich. Tylko w wypadku zdefiniowania lin w postaci œcieżki GDI wie, że sš one połšczone, i zastosuje odpowiednie style łšczenia. Cztery przykładowe programy Do czego może się przydać wiedza na temat œcieżek? Zastanówmy się. Znaki czcionek konturowych zdefiniowane sš jako współrzędne opisujšcych je punk- tów. Współrzędne definiujš linie proste i krzywe, które mogš wobec tego stać się częœciami definicji œcieżki. To powinno zadziałać! Program FONTOUTI demonstruje tę technikę na rysun- ku 17-14. FONTOUTI.C /* FONTOUTl.C - Stosowanie œcieżek w czcionkach konturowych (c) Charles Petzold, 1998 */ ttinclude ţţinclude "..\\eztest\\ezfont.h" TCHAR szAppName [] = TEXT ("Font0utl") ; TCHAR szTitle [] = TEXT ("Font0utl: Using Path to Outline Font") ; void PaintRoutine (HWND hwnd, HDC hdc, int cxArea, int cyArea) ( . static TCHAR szString [] = TEXT ("Outline") ; HFONT hFont ; SIZE size ; hFont = EzCreateFont (hdc, TEXT ("Times New Roman"), 1440, 0, 0, TRUE) ; Rozdział 17: Tekst i czcionki 967 SelectObject (hdc, hFont) ; GetTextExtentPoint32 (hdc, szString, lstrlen (szString), &size) ; BeginPath (hdc) ; TextOut (hdc, (cxArea - size.cx) / 2, (cyArea - size.cy) / 2, szStrin9, lstrlen (szString)) ; EndPath (hdc) ; StrokePath (hdc) ; SelectObject (hdc, GetStockObject (SYSTEMţFONT)> ; Delete0bject (hFont) ; Rysunek 17-14. Program FONTOUTI Powyższy program oraz pozostałe aplikacje demonstrowane w tym rozdziale korzystajš z przedstawionych wczeœniej modułów EZFONT i FONTDEMO. FONTOUT tworzy czcionkę TrueType o rozmiarze 144 punktów i wywołuje Get- TextExtentPoint32 w celu uzyskania rozmiarów okna tekstowego. Następnie wy- wohxje funkcję TextOut, która zajmuje się wyœwietleniem konturu tekstu na œrod- ku okna. Ponieważ wywołanie funkcji TextOut znajduje się między BeginPath oraz EndPath, GDI rue wyœwietla tekstu natychmiast. Jego kontury sš przechowywa- ne w definicji œcieżki. Dopiero po zakończeniu defirucji wywoływana jest funkcja StrokePath. Ponieważ nie wybrano żadnego szczególnego pisaka w kontekœcie urzšdzenia, GDI rysuje kontury tekstu za pomocš domyœlnego pisaka, jak na rysunku 17-15. O Rysunek 17-15. Wynik działania programu FONTOUT1 Co my tu mamy? Kontury tekstu, zgodnie z oczekiwaniami. Dlaczego jednak sš otoczone prostokštnš ramkš? Przypomnij sobie, że domyœlnym trybem tła jest OPAQUE, a nie TRANSPAIZENT. Prostokšt wokół tekstu to kontury ramki tekstowej. Teraz wyraŸnie widać dwa etapy rysowania przez GDI tekstu w trybie OPAQUE: najpierw prostokšt, a następnie znaki tekstu. W ten sposób kontury prostokšta stajš się częœciš definicji œcieżki. 968 Częœć II: Grafika Stosujšc funkcję ExtCreatePen możesz narysować znaki czcionki innym narzędziem niż domyœlny pisak. Demonstruję to na rysunku 17-16. FONTOUT2.C /* FONTOUT2.C - Stosowanie œcieżek w czcionkach konturowych (c) Charles Petzold, 1998 */ ţpinclude 4tinclude "..\\eztest\\ezfont.h" TCHAR szAppName [] = TEXT ("Font0ut2") ; TCHAR szTitle [] = TEXT ("Font0ut2: Using Path to Outline Font") ; ( void PaintRoutine (HWND hwnd, HDC hdc, int cxArea, int cyArea) 1 static TCHAR szString [] = TEXT ("Outline") ; HFONT hFont ; LOGBRUSH lb ; SIZE size ; hFont = EzCreateFont (hdc, TEXT ("Times New Roman"), 1440, 0, 0, TRUE) ; SelectObject (hdc, hFont) ; SetBkMode (hdc, TRANSPARENT) ; GetTextExtentPoint32 (hdc, szString, lstrlen (szString), &size) ; BeginPath (hdc) ; TextOut (hdc, (cxArea - size.cx) / 2, (cyArea - size.cy) / 2, szString, lstrlen (szString)) ; EndPath (hdc) ; lb.lbStyle = BS_SOLID ; lb.lbColor = RGB (255, 0, 0) ; lb.lbHatch = 0 ; SelectObject (hdc, ExtCreatePen (PS_GEOMETRIC ţ PS_DOT, GetOeviceCaps (hdc, LOGPIXELSX) / 24, &lb, 0, NULL)) ; ; StrokePath (hdc) ; ' Delete0bject (SelectObject (hdc, GetStockObject (BLACKţPEN))) ; SelectObject (hdc, GetStockObject (SYSTEMţFONT)) ; Delete0bject (hFont) ; ) , Rysunek 17-16. Program FONTOUT2 Program ten tworzy (i wybiera w kontekœcie urzšdzenia) czerwony pisak rysu- jšcy linie kropkowane o gruboœci 3 punktów (1/24 cala) przed wywołaruem fuxtkcji StrokePath. Wyniki programu działajšcego w œrodowisku Windows NT przedsta- wiono na rysunku 17-17. Windows 98 nie obsługuje pisaków kreœlšcych liniami niecišgłymi o gruboœci większej niż 1 punkt, więc zobaczysz tekst rysowany ciš- głš czerwonš liniš. Rozdział 17: Tekst i czcionki 969 f &X I m 5 1 ţ tw... ţů .t . ... .. . ..ů .fwT! l:..I.ůi..aa ....1. Rysunek 17-17. Wynik działania programu FONTOUT2 Œcieżki mogš być również zastosowane w celu definiowania obszarów do wy- pełruenia. Tworzysz œcieżkę podobnie jak w obu prezentowanych ostatnio pro- gramach, wybierasz rodzaj wypełnienia i wywołujesz funkcję FilIPath. Możesz rówrueż wywołać StrokeAndFilIPath, która jednoetapowo rysuje kontur tekstu i wy- pełnia go wybranym deseniem. Działanie funkcji StrokeAndFiIlPath przedstawione jest na przykładzie programu FONTFILL, pokazanego na rysunku 17-18. FONTFILL.C /* FONTFILL.C - Stosowanie œcieżek w czcionkach wypełnionych (c) Charles Petzold, 1998 */ llinclude llinclude "..\\eztest\\ezfont.h" TCHAR szAppName C] = TEXT ("FontFill") ; TCHAR szTitle C7 = TEXT ("FontFill: Using Path to Fill Font") ; void PaintRoutine (HWND hwnd, HDC hdc, int cxArea, int cyArea) ( static TCHAR szString C] = TEXT ("Filling") ; HFONT hFont ; SIZE size ; hFont = EzCreateFont (hdc, TEXT ("Times New Roman"), 1440, 0, 0, TRUE) ; SelectObject (hdc, hFont) ; SetBkMode (hdc, TRANSPARENT) ; GetTextExtentPoint32 (hdc, szString, lstrlen (szString), &size) ; BeginPath (hdc) ; TextOut (hdc, (cxArea - size.cx) / 2, (cyArea - size.cy) / 2, 9ţ0 Częœć II: Grafika j szString, lstrlen (szString)> ; EndPath (hdc) ; SelectObject (hdc, CreateHatchBrush (HSţDIAGCROSS, RGB (255, 0, 0))) ; SetBkColor (hdc, RGB (0, 0, 255)) ; SetBkMode (hdc, OPAOUE) ; StrokeAndFillPath (hdc) ; Delete0bject (SelectObject (hdc, GetStockObject (WH.ITĘ BRUSH))) ; SelectObject (hdc, GetStockObject (SYSTEMţFONT)) ; Delete0bject (hFont) ; Rysunek 17-18. Program FONTFILL Program FONTFILL używa domyœlnego pisaka do rysowania œcieżki, lecz do wypełnienia w czerwonš kratkę tworzy pędzel, używajšc stylu HS DIAGCIZOSS. Zauważ, że ustawia tryb tła jako TRANSPARENT (przezroczysty) w trakcie two- rzenia œcieżki, a następnie przestawia go na OPAQUE w czasie wypełniania kon- turów tekstu - w ten sposób udało się uzyskać niebieskie tło wypełnienia. Wynik zademonstrowano na rysunku 17-19. Możesz spróbować nieco zmodyfikować program, aby uzyskać kilka ciekawych efektów. Po pierwsze, jeœli skomentujesz pierwsze wywołanie funkcji SetBkMode, zamiast wypełnionych liter tekstu otrzymasz tło ramki tekstowej wypełnione deseniem. Oczywiœcie, nie o taki efekt nam chodziło, lecz warto przećwiczyć różne możliwoœci. W trakcie wypełniania liter w celu wycinania fragmentów tła najlepszym rozwiš- zaniem jest pozostawienie trybu wypełniania wielokštów ALTEIZNATE. Czcionki TrueType sš tak zbudowane, że nie powinno być żadnych niekorzystnych efektów (na przykład wypełruenia wnętrza litery O), nawet jeœli użyjemy trybu wypełnia- nia WINDING. Bezpieczniej jednak jest pozostać przy trybie ALTERNATE. ů ů Rysunek 17-19. Wynik działania programu FONTFILL Wreszcie œcieżka może być zastosowana do definiowania obszaru przeznaczo- nego do wycięcia. Technika ta została zademonstrowana na rysunku 17-20. Rozdziaf 17: Tekst i czcionki 971 FONTCLIP.C l /* FONTCLIP.C - Stosowanie œcieżek do wycinania czcionek (c) Charles Petzold, 1998 */ ţţinclude ţţinclude "..\\eztest\\ezfont.h" TCHAR szAppName CJ = TEXT ("FontClip") ; TCHAR szTitle [J = TEXT ("FontClip: Using Path for Clipping on Font") ; void PaintRoutine (HWND hwnd, HDC hdc, int cxArea, int cyArea) ( static TCHAR szString C] = TEXT ("Clipping") ; HFONT hFont ; int y, i0ffset ; " ' POINT pt C47 : SIZE size ; hFont = EzCreateFont (hdc, TEXT ("Times New Roman"), 1200, 0, 0, TRUE) ; ; SelectObject (hdc, hFont) ; GetTextExtentPoint32 (hdc, szString, lstrlen (szString), &size) ; ţ .: I BeginPath (hdc) ; TextOut (hdc, (cxArea - size.cx) / 2, (cyArea - size.cy) / 2, szString, lstrlen (szString)) ; EndPath (hdc) ; ! // Ustal obszar do wycięcia SelectClipPath (hdc, RGN COPY) ; i // Rysuj linie Beziera i0ffset = (cxArea + cyArea) / 4 ; for (y = -i0ffset ; y < cyArea + i0ffset ; y++) ptCO].x = 0 ; i'i ' ptCOl.y = y ; ptCl].x = cxArea / 3 ; ptCl].y = y + i0ffset ; ptC2].x = 2 * cxArea / 3 ; ptC2].y = y - i0ffset ; ptC3].x = cxArea ; ptC37.y = y : SelectObject (hdc, CreatePen (PS_SOLID, 1, RGB (rand () % 256, rand () % 256, rand () % 256))) ; PolyBezier (hdc, pt, 4) ; 972 Częœć II: Grafika Delete0bject (SelectObject (hdc, GetStockObject (BLACKţPEN))) ; Delete0bject (SelectObject (hdc, GetStockObject (WHITĘ BRUSH))) SelectObject (hdc, GetStockObject (SYSTEMţFONT)) : Delete0bject (hFont) : Rysunek 17-20. Program FONTCLIP W powyższym programie celowo pominšłem wywołanie funkcji SetBkMode, aby otrzymać odmienny efekt. Program rysuje tekst w postaci œcieżki, a następnie wywohxje funkcję SelectCIiţPath. Potem rysuje serię krzywych &ziera w losowych kolorach. Gdyby program FONTCLIP wywołał funkcję SetBkMode z atrybutem TRANSPA- RENT, krzywe &ziera znalazłyby się jedynie we wnętrzu konturu liter. Ustawienie opcji OPAQUE spowodowało uzyskarue efektu widocznego na rysunku 17-21. Rysurţek 17-21. Wynik działania programu FONTCLIP Możesz wstawić do kodu Ÿródłowego programu wywołanie funkcji SetBkMode, aby przekonać się, jaki efekt daje stosowanie opcji TRANSPARENT. Program FONTDEMO pozwala na wydrukowanie i wyœwietlenie wszystkich omawianych efektów i, co więcej, umożliwia użytkownikowi manipulowarue pa- rametrami w celu osišgnięcia efektów specjalnych. IZozdział 18 M " eta I ţ kţ Bitmapy to podstawowy obiekt grafiki rastrowej, natomiast metapliki zwišzane sš z grafikš wektorowš. Podczas gdy pliki bitmap przedstawiajš obrazy rzeczy- wiste, z otaczajšcego nas œwiata, metapliki sš tworzone całkowicie syntetycznie p , rzez człowieka wspomaganego odpowiednimi programami komputerowymi. Metaplik składa się z wielu rekordów binamych, odpowiadajšcych wywołaniom odpowiednich funkcji, na przykład rysujšcych proste, krzywe, obszary wypeł- nione i tekst. Programy malujšce tworzš bitmapy, a programy rysujšce - metapliki. W dobrze zaprojektowanym programie graficznym do rysowania możesz z łatwoœciš chwy- cić dowolny obiekt graficzny (taki jak linia) i przenieœć go w inne miejsce. Jest to możliwe dlatego, że wszystkie częœci składowe rysunku sš przechowywane w po- staci niezależnych rekordów. W programie grafiki bitmap takie operacje nie sš możliwe - użytkownik ma jedynie możliwoœć usuwania lub wstawiania prosto- kštnych kawałków mapy. Ponieważ metapliki opisujš obraz w kategoriach poleceń rysowania poszczegól- nych jego elementów, może on być dowolnie skalowany bez szkody dla rozdziel- czoœci. Bitmapy zachowujš się zgoła inaczej: jeœli powiększysz obraz dwukrot- nie, rue uzyskasz dwa razy wyższej rozdzielczoœci. Liczba pikseli bitmapy jest tylko podwajana w piore i w poziomie. Metaplik może zostać skonwertowany na format bitmapy, jednak z pewnš stratš informacji: po takiej konwersji obiekty graficzne wchodzšce w skład metapliku rue sš już niezależne, lecz mieszajš się, tworzšc jednolity obraz. Konwersja bit- mapy na format metapliku jest znaczrue bardziej złożona, a często ograniczona do bardzo prostych obrazów bitmap i wymagajšca wielkich mocy obliczeniowych w celu wykrywania krawędzi i konturów. Jednak metaplik może zawierać pole- cenie rysowania bitmapy. Metapliki sš najczęœciej stosowane do współużytkowania obrazów z poziomu różnych aplikacji za poœredructwem Schowka, choć mogš rówrueż być zapisane na dysku twardym. Ze względu na specyficzny opis obrazu w postaci wywołań funkcji graficznych metapliki zajmujš mniej miejsca i sš bardziej niezależne od sprzętu niż bitmapy. Microsoft Windows użytkuje dwa formaty metaplików i dwa zestawy obsługu- jšcych je funkcji. Najpierw omówię funkcje metaplików wprowadzone w Win- dows 1.0 i obecne do dziœ w wersjach 32-bitowych, następnie zajmiemy się meta- plikiem rozszerzonym, który został wprowadzony po raz pierwszy w 32-bitowej wersji systemu. Format rozszerzony metapliku ma wiele ulepszeń w porówna- niu z poprzednim i zaleca się jak najczęstsze stosowanie go. 974 Częœć II: Grafika Stary format metapliku Metapliki mogš istnieć w pamięci tymczasowo lub być zapisane na dysku twar- dym. Z punktu widzerua aplikacji oba te warianty wyglšdajš podobnie; w szcze- gólnoœci wszelkimi operacjami wejœcia/wyjœcia, które zwykle obsługiwane sš programowo, zajmuje się bezpoœrednio Windows. Proste zastosowanie metaplików przechowywanych w pamięci Metaplik w starym formacie powstaje w wyniku utworzenia kontekstu urzšdze- nia z nim zwišzanego przez wywołanie CreateMetaFile. Następnie wykorzystuje się funkcje GDI do rysowania obiektów graficznych w kontekœcie urzšdzenia metapliku. Wywołania funkcji nie rysujš obiektów na żadnym z urzšdzeń fizycz- nych, sš natomiast zapisywane sekwencyjrue w metapliku. Po zamknięciu kon- tekstu urzšdzerua otrzymujesz uchwyt nowo powstałego metapliku. Teraz mo- żesz odtworzyć kolejnoœć czynnoœci używajšc rzeczywistego urzšdzenia, takie- go jak monitor lub drukarka, co jest równoznaczne z wykonaniem kolejnych wywołań funkcji GDI. Funkcja CreateMetaFile przyjmuje jeden argument - wartoœć NULL lub nazwę pliku. W pierwszym wypadku metaplik jest przechowywany w pamięci, nato- miast w wypadku podania nazwy pliku - z tradycyjnym dla Windows rozsze- rzeniem .WMF (Windows Metafile) - metaplik zostanie zapisany w pliku dysko- wym. Program METAFILE, przedstawiony na rysunku 18-1, demonstruje sposób two- rzenia metapliku w pamięci w trakcie obsługi komunikatu WM-CREATE, a na- stęprue wyœwietlenia go 100 razy w trakcie obsługi komunikatu WM-PAINT. METAFILE.C /* METAFILE.C - Metapliki - program demonstracyjny (c) Charles Petzold, 1998 */ ilinclude LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) static TCHAR szAppName C7 = TEXT ("Metafite") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CS VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 : Rozdział 18: Metapliki wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE BRUSH) ; wndclass.lpszMenuName = NULL wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) ( MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MBţICONERROR) ; return 0 ; ) hwnd = CreateWindow (szAppName, TEXT ("Metafile Demonstration"), WS OVERLAPPEDWINDOW, CW USEDEFAULT, CW USEDEFAULT, CW_USEDEFAULT, CW USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) f TranslateMessage (&msg) ; DispatchMessage (&msg> ; ) return msg.wParam ; LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) f static HMETAFILE hmf ; static int cxClient, cyClient ; HBRUSH hBrush ; HDC hdc, hdcMeta ; int x. y : PAINTSTRUCT ps ; switch (message) f case WM_CREATE: hdcMeta = CreateMetaFile (NULL) ; hBrush = CreateSolidBrush (RGB (0, 0, 255)) ; Rectangle (hdcMeta, 0, 0, 100, 100) ; MoveToEx (hdcMeta, 0, 0, NULL) ; LineTo (hdcMeta, 100, 100) ; MoveToEx (hdcMeta, 0, 100, NULL) ; LineTo (hdcMeta, 100, 0) ; SelectObject (hdcMeta, hBrush) ; Ellipse (hdcMeta, 20, 20, 80, 80) ; hmf = CloseMetaFile (hdcMeta) ; 976 Częœć II: Grafika (cišg dalszy ze strony 975) Delete0bject (hBrush) ; return 0 case WM_SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; SetMapMode (hdc, MM A.NISOTROPIC) ; SetWindowExtEx (hdc, 1000, 1000, NULL) ; SetViewportExtEx (hdc, cxClient, cyClient, NULL) ; ; for (x = 0 ; x < 10 ; x++) for (y = 0 : y < 10 ; y++) ( SetWindowOrgEx (hdc, -100 * x, -100 * y, NULL) ; PlayMetaFile (hdc, hmf) ; ) EndPaint (hwnd, &ps) return 0 ; case WM_DESTROY: DeleteMetaFile (hmf) ; PostOuitMessage (0) ; return 0 ; ) return DefWindowProc (hwnd, message, wParam, lParam) ; Rysunek 1&1. Program METAFILE Program pokazuje zastosowanie czterech funkcji niezbędnych do utworzenia me- tapliku w pamięci. Pierwszš z nich, CreateMetaFile, program wywołuje z argu- mentem NULL w czasie obsługiwania komunikatu WM CREATE. Funkcja zwraca uchwyt kontekstu urzšdzenia metapliku. Następnie program rysuje dwie niebie- skie linie i niebieskš elipsę, używajšc kontekstu urzšdzenia metapliku. Dotych- czasowe wywołarua funkcji sš umieszczane w postaci binarnej w metapliku. Funk- cja CloseMetaFile zwraca uchwyt nowo powstałego pliku. Zauważ, że uchwyt ten jest przechowywany w postaci zmiennej statycznej, gdyż będzie póŸniej potrzebny. Metaplik zawiera binarnš reprezentację wywołań funkcji GDI - w tym wypadku dwa wywołania MoveToEx, dwa LineTo, jedno wywołanie SelectObject (okreœlajš- ce niebieski kolor pędzla) oraz jedno Ellipse. Nie zastosowano ani trybu odwzo- rowania, ani transformacji współrzędnych obiektów. Sš one przechowywane w metapliku w postaci liczb. W trakcie obsługi komunikatu WMţPAINT METAFILE ustala tryb odwzorowa- nia i wywołuje funkcje PlayMetaFile, aby utworzony obraz narysować w oknie 100 razy. W trakcie wywołania tej funkcji wszystkie instrukcje między CreateMe- taFile i CIoseMetaFile wydane podczas obsługi komunikatu WM CREATE powta- rzane sš sekwencyjnie 100 razy. Rozdziat 18: Metapliki Podobnie jak inne obiekty GDI, metapliki muszš być usunięte z pamięci przed zakończeniem programu. Zajmuje się tym funkcja DeleteMetaFile w czasie obsłu- giwania komunikatu WMţDESTROY. Wynik działania programu METAFILE przedstawiono na rysunku 18-2. Rysunek 18-2. Wynik działania programu METAFILE Zapisywanie metaplików na dysku W powyższym przykładzie argument NLJLL przyjmowany przez funkcję Create- MetaFile oznaczał, że metaplik ma być przechowywany w pamięci. Oczywiœcie możliwe jest rówrueż zapisanie metapliku na dysku. Metoda ta jest korzystniej- sza w wypadku wielkich metaplików ze względu na niewielkie zużycie pamięci operacyjnej. Z drugiej strony, dostęp do metapliku zapisanego na dysku trwa znacznie dłużej niż do metaplików przechowywanych w pamięci. Chcšc, by program METAFILE utworzył metaplik na dysku, wystarczy zamienić argument NULL funkcji CreateFileName na nazwę pliku. Po zakończeniu sekwencji obsługujšcej komunikat WMţCREATE należy wywołać funkcję DeleteMetaFile, przyjmujšcš jako argument uchwyt utworzonego metapliku. Po tej operacji usu- nięty zostanie uchwyt, lecz na dysku pozostanie plik. W trakcie obsługi komunikatu WMţPAINT możesz pobrać uchwyt pliku dysko- wego przez wywołanie GetMetaFile: hmf = GetMetaFile (szFileName) ; Teraz możesz odtworzyć metaplik w taki sam sposób jak poprzednio. Po zakoń- czeniu obsługi komunikatu WMţPAINT możesz usunšć uchwyt metapliku: DeleteMetaFile (hmf) Obsługa komunikatu WMţDESTROY nie wymaga usunięcia metapliku, gdyż stało się to już po zakończeniu obshxgi komunikatów WMţCREATE oraz WM PAINT. g7g Czţœć II: Grafika Powinieneœ jednak usunšć plik dyskowy w następujšcy sposób: DeleteFile (szFileName) ; oczywiœcie jeœli rue chcesz go zatrzymać. Możesz utworzyć defiruowanš przez użytkownika bibliotekę metaplików (oma- wiałem to w rozdziale 10). Majšc blok danych w formacie metapliku, możesz na jego podstawie utworzyć metaplik za pomocš funkcji: hmf = SetMetaFileBitsEx (iSize, pData) : SetMetaFileBitsEx ma bliŸniaczš funkcję GetMetaFileBitsEx, która kopiuje zawar- toœć metapliku do pamięci w postaci bloku danych. Stary format metapliku i Schowek Stary format metaplików ma poważna wadę: dysponujšc jedynie uchwytem metapliku nie jesteœ w stanie wyznaczyć jego rozmiarów w trakcie odtwarzania. Jedyny sposób to dokładne przyjrzenie się jego strukturze. Co więcej, kiedy program otrzymuje metaplik za poœrednictwem Schowka, naj- większš elastycznoœć w pracy z tym metaplikiem uzyskuje się, jeœli został zapro- jektowany do odtwarzania w trybie odwzorowania MM ISOTROPIC lub MM ŽMSOTROPIC. Program otrzymujšc metaplik może skalować obraz przez ustalenie rozmiarów pola widzenia przed odtworzeniem metapliku. Jeœli jednak tryb odwzorowania MM ISOTROPIC lub MMţAMSOTROPIC został ustawio- ny wewnštrz metapliku, program, otrzymujšc go do odtworzenia, blokuje się. Może wywołać funkcje GDI jedynie przed lub po odtworzeniu metapliku, nie może natomiast obsługiwać wywołania GDI z poziomu metapliku. Aby temu zaradzić, uchwyty metaplików zapisanych w starym formacie nie sš bezpoœrednio wklejane do Schowka i odbierane przez inne programy. Stajš się natomiast częœciš obrazu metapliku, który jest strukturš typu METAFILEPICT. Pozwala ona programowi, otrzymujšcemu obraz metapliku ze Schowka, na usta- wienie trybu odwzorowania i rozmiarów pola widzenia jeszcze przed odtworze- niem samego metapliku. Struktura METAFILEPICT ma długoœć 16 bajtów i jest zdefiniowana następujš- co: typedef struct tagMETAFILEPICT ( LONG mm; // tryb odwzorowania LONG xExt ; // szerokoœć obrazu metapliku LONG yExt ; // wysokoœć obrazu metapliku LONG hMF ; // uchwyt metapliku ) METAFILEPICT We wszystkich trybach odwzorowania z wyjštkiem MMţISOTROPIC i MM ŽM- SOTROPIC wartoœci xExt i yExt reprezentujš rozmiary obrazu w jednostkach trybu odwzorowania okreœlonych w polu mm. Program wyposażony w takš informa- cję, kopiujšc strukturę zwišzanš z obrazem metapliku ze Schowka, może okre- œlić, ile miejsca na ekranie zajmie odtwarzany metaplik. Program tworzšcy meta- plik może ustalić maksymalne wartoœci współrzędnych x i y używanych przez funkcje GDI. Rozdział 18: Metapliki Pola xExt i yExt funkcjonujš nieco inaczej w trybach odwzorowania MM_ISO- TROPIC i MM ŽNISOTROPIC. Jak pisałem w rozdziale 5, program korzysta z tych trybów, gdy musi użyć jednostek logicznych dla funkcji GDI ruezależnie od rzeczywistych rozmiarów obrazu. Stosuje tryb MMţISOTROPIC, chcšc zacho- wać proporcje obrazu bez względu na rozmiary obserwowanej sceny, a MM-AM- SOTROPIC - gdy proporcje rozmiarów nie majš żadnego znaczenia. W rozdziale 5 pisałem również, że po ustawieniu jednego z tych dwóch trybów odwzorowa- rua program wywohzje funkcje SetWindowExtEx i SetViewportExtEx. Funkcja Se- tWindowExtEx wykorzystuje jednostki logiczne do okreœlenia jednostek używa- nych przez program w trakcie rysowania. Funkcja SetViewportExtEx stosuje jed- nostki urzšdzenia opisujšc się na rozmiarach obserwowanej sceny (na przykład rozmiar okna roboczego). Jeœli program tworzy metaplik, opierajšc się na trybach odwzorowania MM ISO- TROPIC lub MM-AMSOTROPIC, i umieszcza go w Schowku, metaplik nie po- winien zawierać wywołarua funkcji SetViewportExtEx, gdyż zastosowane jednostki urzšdzenia będš oparte na rozmiarach ekranu z punktu widzenia programu two- rzšcego metaplik, a rue programu, który odczytuje go ze Schowka i odtwarza. Ważnš rolę w takiej sytuacji grajš wartoœci zmiennych xExt i yExt. Powinny one wspomagać program odczytujšcy metaplik ze Schowka w ustaleruu odpowied- ruch rozmiarów obserwowanej sceny w celu odtworzenia zawartoœci metapliku. Należy przy tym pamiętać, że w metapliku znajdujš się wywołania funkcji od- powiedzialnych za ustalerue rozmiarów okna w trybie odwzorowania MM-ISO- TROPIC lub MM ANISOTROPIC. Współrzędne stosowane przez funkcje GDI sš oparte na wyznaczonych w ten sposób rozmiarach okna. Program, który tworzy metaplik i obraz metapliku, kieruje się następujšcymi za- sadami: ů Pole mm struktury METAFILEPICT shxży do okreœlerua trybu odwzorowania. ů Dla trybów odwzorowarua innych niż MM-ISOTROPIC i MM ANISOTRO- PIC pola xExt i yExt przyjmujš odpowiednio wartoœci szerokoœci i wysokoœci obrazu w jednostkach okreœlonych polem mm. W wypadku metaplików, któ- re majš być odtwarzane w trybie MM-ISOTROPIC lub MM-AMSOTROPIC , sprawy się nieco komplikujš. Dla MM AMSOTROPIC zmienne xExt i yExt przyjmujš wartoœci zerowe, jeœli program narzuca bšdŸ rozmiar, bšdŸ proporcje obrazu. W przypadku trybów MM ISOTROPIC lub MM_ANISOTROPIC wartoœci dodatnie zmiennych xExt i yExt oznaczajš proponowane rozmiary obrazu, mierzone w jednostkach równych 0,01 mm (jednostki MM-HIME- TRIC). Negatywne wartoœci pól xExt i yExt dla trybu odwzorowania MM ISO- TROPIC oznaczajš proponowane proporcje, pozostajšc bez wpływu na roz- miar obrazu. Dla trybów odwzorowania MM-ISOTROPIC i MM-AMSOTROPIC metaplik zawiera wywołanie funkcji SetWindowExtEx i (prawdopodobnie) SetWindowOr gEx. Oznacza to, że program tworzšcy metaplik wywohxje wspomniane funkcje w kontekœcie urzšdzenia tego metapliku. Generalnie metaplik nie zawiera wywołań funkcji SetMapMode, SetViewportExtEx czy SetViewportOrgEx. Metaplik powinien być przechowywany w pamięci, a nie w pliku dyskowym. ggţ Częœć II: Grafika Poniżej przedstawiono fragmenty kodu programu tworzšcego metaplik i kopiu- jšcego go do Schowka. Jeœli program pracuje w trybie odwzorowania MMţISO- TROPIC lub MMţANISOTROPIC, wywołania funkcji umieszczone jako pierw- sze w metapliku powinny być zwišzane z ustawieniem rozmiarów okna (w in- nych trybach odwzorowania rozmiary okna sš stałe). &z względu na tryb od- wzorowarua rozmiary okna moga być rówrueż ustalone następujšco: hdcMeta = CreateMetaFile (NULL) ; SetWindowExtEx (hdcMeta, ...) ; SetWindowOrgEx (hdcMeta, ...) ; Współrzędne używane przez funkcje rysujšce metapliku wyliczane sš na pod- stawie rozmiarów okna oraz współrzędnych jego poczštku. Po narysowaniu za- wartoœci za pomocš funkcji GDI metaplik zostaje zamknięty i zwracany jest uchwyt utworzonego metapliku: hmf = CloseMetaFile (hdcMeta) ; Program musi również zdefiniować wskaŸnik do struktury typu METAFILEPICT i zarezerwować blok pamięci niezbędny do jej przechowywarua: GLOBALHANDLE hGlobal : LPMETAFILEPICT pMFP ; (inne linie kodu) hGlobal = GlobalAlloc (GHND ţ GMEM_SHARE, sizeof (METAFILEPICT)) ; pMFP = (LPMETAFILEPICT) GlobalLock (hGlobal) ; Następrue program ustala wartoœci czterech pól struktury: pMFP->mm = MM_. pMFP->xExt = pMFP->yExt = ... . pMFP->hMF = hmf GlobalUnlock (hGlobal) ; Następnie blok pamięci zawierajšcy strukturę zwišzanš z obrazem metapliku przesyłany jest do Schowka: OpenClipboard (hwnd) ; EmptyClipboard () ; SetClipboard0ata (CF METAFILEPICT, hGlobal) ; CloseClipboard () Po takiej sekwencji wywołań funkcji uchwyt hGlobal (blok pamięci zawierajšcy strukturę obrazu metapliku) oraz uchwyt hmf (sam metaplik) stajš się nieważne dla programu, który je utworzył. Teraz najtrudniejszy fragment. Kiedy program otrzymuje metaplik ze Schowka i rozpoczyna jego odtwarzanie, muszš zostać wykonane następujšce kroki: 1. Program korzysta z pola mm struktury obrazu metapliku w celu okreœlenia trybu odwzorowania. 2. Dla trybów odwzorowarua MM ISOTROPIC lub MMţANISOTROPIC, pro- gram korzysta z wartoœci pól xExt oraz yExt do okreœlenia rozmiarów prosto- kšta wycinajšcego lub w celu wyznaczenia rozmiaru obrazu. W obu trybach odwzorowania program używa zmiennych xExt oraz yExt do ustalenia roz- miarów obserwowanej sceny. 3. Następrue program odtwarza metaplik. Rozdział 18: Metapliki ggi Oto fragmenty kodu. Najpierw otwierasz Schowek, pobierasz uchwyt struktury obrazu metapliku i blokujesz go: OpenClipboard (hwnd) ; hGlobal = GetClipboardData (CF_METAFILEPICT) ; pMFP = (LPMETAFILEPICT) GlobalLock (hGlobal) ; Teraz możesz zapisać atrybuty bieżšcego kontekstu urzšdzenia i ustawić tryb odwzorowarua zgodnie z wartoœciš zmiennej mm: SaveDC (hdc) ; SetMappingMode (pMFP->mm) ; Jeœli trybem odwzorowania jest MM ISOTROPIC lub MMţAMSOTROPIC, pro- stokšt wycinajšcy może mieć rozmiary zgodne z wartoœciami zmiennych xExt i yExt. Porueważ sš one wyrażone w jednostkach logicznych, aby zamienić je na jednostki urzšdzenia, musisz zastosować funkcję LPtoDP. Możesz również zapi- sać te wartoœci w celach informacyjnych. W wypadku trybów odwzorowarua MMţISOTROPIC lub MM ŽNISOTROPIC wartoœci zmiennych xExt i yExt stosowane sš do okreœlania rozmiarów obserwo- wanej sceny. Jedna z funkcji realizujšcych to zadanie przedstawiona została po- niżej. Zakłada ona, że zmienne cxClient i cyClient reprezentujš mierzone w pikse- lach rozmiary obszaru, w którym ma pojawić się metaplik, jeœli rue zostały one wczeœniej narzucone przez wartoœci zmiennych xExt i yExt. void PrepareMetaFile (HDC hdc, LPMETAFILEPICT pmfp, int cxClient, cyClient) int xScale, yScale, iScale ; SetMapMode (hdc, pmfp->mm) ; if (pmfp->mm = MMţISOTROPIC ţţ pmfp->mm = MM ANISOTROPIC) ( if (pmfp->xExt = 0) SetViewportExtEx (hdc, cxClient. cyClient, NULL) ; else if (pmfp->xExt > 0) SetViewportExtEx (hdc, pmfp->xExt * GetOeviceCaps (hdc, HORZRES) / GetDeviceCaps (hdc, HORZSIZE) / 100), pmfp->yExt * GetOeviceCaps (hdc, VERTRES) / GetDeviceCaps (hdc, VERTSIZE) / 100), NULL) ; else if (pmfp->xExt < 0) . xScale = 100 * cxClient * GetDeviceCaps(hdc, HORZSIZE) / GetOeviceCaps (hdc, HORZRES) / -pmfp->xExt ; lScale = 100 * cyClient * GetOeviceCaps(hdc, VERTSIZE) / GetOeviceCaps (hdc, VERTRES) / -pmfp->yExt ; iScale = min (xScale, yScale) ; SetViewPortExtEx (hdc, -pmfp->xExt * iScale * GetDeviceCaps (hdc, HORZRES) GetDeviceCaps (hdc, HORZSIZE) / 100, -pmfp->yExt * iScale * GetDeviceCaps (hdc, VERTRES) GetDeviceCaps (hdc. VERTSIZE) / 100, NULL) : 982 Częœć II: Grafika Przedstawiona powyżej funkcja zakłada, że obie zmienne xExt i yExt przyjmujš jednoczeœrue wartoœci 0, większe od zera lub ujemne. W pierwszym wypadku program nie narzuca ani proporcji, ani rozmiarów obrazu. Rozmiary obserwo- wanej sceny pokrywajš się z obszarem, w którym ma pojawić się metaplik. Do- datnie wartoœci zmiennych xExt i yExt sugerujš, by rozmiary obrazu podać w jed- nostkach 0,01 mm. Funkcja GetDeviceCaps pomaga w wyznaczeruu liczby pikseli przypadajšcych na jednostkę o dhzgoœci 0,01 mm. Uzyskana wartoœć jest mnożo- na przez wartoœci rozmiarów znajdujšce się w strukturze obrazu metapliku. Ujem- ne wartoœci zmiennych xExt i yExt okreœlajš proporcje obrazu, pozostajšc bez wpływu na jego rozmiar. Wartoœć zmiennej iScale jest najpierw wyliczana na pod- stawie proporcji rozmiarów podanych w milimetrach, odpowiednio do wartoœci cxClient i cyClient. Taki współczynnik skalowania używany jest do ustalania roz- miarów obserwowanej sceny. Majšc za sobš to zadanie, możesz odtworzyć metaplik i przywrócić normalny kontekst urzšdzenia: PlayMetaFile (pMFP->hMF) ; RestoreDC (hdc, -1) ; Następnie zwolruj obszar pamięci i zamknij Schowek: GlobalUnlock (hGlobal) ; CloseClipboard () ; Jeœli twój program używa rozszerzonych metaplików, możesz zapomnieć o całej skomplikowanej procedurze. Schowek Windows zatroszczy się o konwersje mię- dzy starym formatem metapliku i formatem rozszerzonym. Wystarczy, że jeden z programów skopiuje metaplik w dowolnym formacie do Schowka, a inny za- żšda tego samego metapliku w postaci skonwertowanej. Rozszerzone metapliki Format rozszerzonego metapliku został wprowadzony w 32-bitowych wersjach Windows. Wzbogacono go o zestaw nowych funkcji, kilka nowych struktur da- nych, nowy format SchowJCa i nowe rozszerzenie plików: .EMF. Najważniejszš zmianš jest rozbudowanie nagłówka informacyjnego metapliku, łatwo dostępnego za pomocš odpowiedniej funkcji. Informacje te pomagajš apli- kacjom w wyœwietlaniu obrazów metaplików. Istniejš oczywiœcie funkcje rozszerzonego metapliku umożliwiajšce konwersje między nowym (EMF) i starym formatem, zwanym również formatem metapli- ku Windows (WMF). Konwersje te nie sš bezproblemowe, ponieważ stary for- mat nie obsługuje niektórych 32-bitowych własnoœci, wprowadzonych w meta- plikach rozszerzonych (na przykład œcieżki). Rozdział 18: Metapliki ggg Procedura podstawowa Rysunek 18-3 przedstawia program EMFl, który tworzy i wyœwietla rozszerzo- ny metaplik niemal pozbawiony efektów specjalnych. EMFl.C /* EMFl.C - Metaplik rozszerzony Demo ţţ1 (c) Charles Petzold, 1998 */ ţţinclude LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpszCmdLine, int nCmdShow) { static TCHAR szAppName[J = TEXT ("EMF1") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ( CS UREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; , wndclass.hbrBackground = GetStockObject (WHITEţBRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) f MessageBox (NULL, TEXT ("This program requires Windows NT!"), i szAppName, MBţICONERROR) ; return 0 ; ) hwnd = CreateWindow (szAppName, TEXT ("Enhanced Metafile Demo ţţ1"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW USEDEFAULT, NULL, NULL, hInstance, NULL) ; I ShowWindow (hwnd, nCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) ( TranslateMessage (&msg) ; DispatchMessage (&msg) ; i Częœó 11: Grafika (cišg dalszy ze strony 983) return msg.wParam ; LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) f static HENHMETAFILE hemf ; HDC hdc, hdcEMF ; PAINTSTRUCT ps ; RECT rect ; switch (message) case WM_CREATE: hdcEMF = CreateEnhMetaFile (NULL, NULL, NULL, NULL) ; Rectangle (hdcEMF, 100, 100, 200, 200) : I MoveToEx (hdcEMF, 100, 100, NULL) ; LineTo (hdcEMF, 200, 200) ; MoveToEx (hdcEMF, 200, 100, NULL) ; LineTo (hdcEMF, 100, 200) ; I hemf = CloseEnhMetaFile (hdcEMF) ; return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; óetClientRect (hwnd, 8ţrect) : rect.left - rect.right / 4 ; rect.right = 3 * rect.right / 4 ; rect.top - rect.bottom / 4 ; rect.bottom ţ 3 * rect.bottom / 4 ; PlayEnhMetaFile (hdc, hemf, &rect) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: DeleteEnhMetaFile (hemf) ; PostOuitMessage (0) ; return 0 ; ) return DefWindowProc (hwnd, message, wParam, lParam) : Rysunek 1&3. Program EMFI W trakcie obsługi komunikatu WMţCREATE program tworzy rozszerzony metaplik za pomocš funkcji CreateEnhMetaFile, która przyjmuje cztery argumenty, jednak wszystkie mogš mieć w tym przypadku wartoœć NULL. W dalszej częœci rozdziału omówię krótko sposób użycia tej funkcji, przyjmujšcej wartoœci różne od NULL. Rozdzial 18: Metapliki 985 Podobnie jak CreateMetaFile, funkcja CreateEnhMetaFile zwraca specjalny uchwyt kontekstu urzšdzerua. Program używa go do narysowania prostokšta z dwoma przekštnymi. Wywołania tych funkcji wraz z argumentami sš konwertowane na postać binarnš i przechowywane w metapliku. Wreszcie wywołanie CioseEnhMetaFile koxţczy proces powstawania metapliku i zwraca jego uchwyt. Jest on przechowywany w statycznej zmiennej typu HENH- METAFILE. W trakcie obshxgi komunikatu WM PAINT program EMF1 umieszcza rozmiary okna obszaru roboczego w strukturze typu RECT. Wartoœci czterech pól struktu- ry zostały tak dobrane, że rozmiary narysowanego prostokšta sš dwukrotnie _ mniejsze niż okna programu, a sam prostokšt - umieszczony poœrodku obszaru ` roboczego. Następrue EMFI wywołuje funkcję PIayEnhMetaFile. Pierwszy argu- ment wywołania jest uchwytem kontekstu urzšdzenia okna, drugi jest uchwy- tem rozszerzonego metapliku, a trzeci - wskaŸnikiem do struktury RECT. W trakcie tworzenia metapliku GDI okreœla wszystkie wymiary jego obrazu. W tym wypadku obraz ma dhzgoœć i szerokoœć równš 100 jednostkom. Podczas wyœwietlania zawartoœci metapliku GDI dopasowuje rozmiary obrazu do wiel- koœci prostokšta okreœlonego w funkcji PlayEnhMetaFile. Na rysunku 18-4 przed- stawiono trzykrotrue uruchomiony program EMFl. Rysunek 18-4. Wynik działarua programu EMFI Wreszcie podczas obshxgiwania komunikatu WM-DESTROY EMF1'usuwa me- taplik, wywołujšc funkcję DeleteEnhMetaFile. Zwróć uwagę na kilka wartych zapamiętarua szczegółów programu EMFl. Po pierwsze, wartoœci współrzędnych punktów użyte do narysowania prostokš- ta nie majš tak wielkiego znaczenia, jak mogłoby się wydawat. Możesz je dwu- krotnie zwiększyć lub odjšć od wszystkich stałš wartoœć, a wynik pozostanie bez 986 Czţœć II: Grafika zmian, porueważ poszczególne wartoœci współrzędnych sš ze sobš powišzane już w trakcie definiowania obrazu. Po drugie, obraz został dopasowany do prostokšta, który jest przekazywany jako argument do funkcji PIayEnhMetaFile. Dzięki temu obraz może być dowolnie ska- lowany, co wyraŸnie widać na rysunku 18-4. Współrzędne metapliku sugerujš, że obraz ma kształt kwadratu, jednak na ekranie widać prostokšt. Czasem jed- nak taki efekt jest korzystny. W wypadku rysunków osadzanych, na przykład w procesorach tekstu, użytkownik zwykle żšda, aby prostokšt, w którym umiesz- czany jest rysunek, mógł być elastyczrue skalowany. Tak więc dobranie właœci- wych proporcji rysunku nie powinno zajmować programisty, zostawmy to użyt- kownikowi. Czasem jednak potrzebne jest inne podejœcie do problemu. Może się okazać, że trzeba zachować oryginalne proporcje obrazu ze względu na koniecznoœć przed- stawienia pewnych szczegółów, które stałyby się bezwartoœciowe po zakłóceniu stosunku długoœci do szerokoœci (na przykład portret pamięciowy przestępcy nie powinien być zniekształcony, gdyż informacja, którš niesie, jest ważna jedynie przy zachowaniu rzeczywistych proporcji). Czasem też konieczne jest zachowa- nie pierwotnych rozmiarów obrazu (na przykład gdy użytkownik wymaga, aby obraz miał wysokoœć 5 cm i nie powiruen być reprodukowany w żadnym innym formacie). Zauważ, że przekštne kwadratu na rysunku 18-4 nie stykajš się dokładnie z wierz- chołkami prostokšta. Wynika to ze sposobu przechowywania współrzędnych prostokšta w metapliku. W dalszej częœci rozdziału postaramy się naprawić ten błšd. Zajrzyjmţ do œrodka Dobrym sposobem na zapoznanie się ze strukturš metapliku jest zajrzenie do jego wnętrza. Zadanie jest łatwe, jeœli mamy do czynienia z metaplikiem zapisanym na dysku. Program EMF2 zajmuje się utworzeniem takiego metapliku. EMF2.C EMF2.C - Enhanced Metafile Demo 4i2 (c) Charles Petzold, 1998 */ ilinclude LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpszCmdLine, int nCmdShow) static TCHAR szAppName[] = TEXT ("EMF2") ; HWNO hwnd ; MSG msg ; WNDCLASS wndclass ; Rozdział 18: Metapliki ggţ , wndclass.style = CS_HREDRAW ţ CS UREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; I. .. wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL. IDC_ARROW) ; wndclass.hbrBackground = GetStockObject (WHITEţBRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) ( MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MBţICONERROR) ; return 0 ; } hwnd = CreateWindow (szAppName TEXT ("Enhanced Metafile Demo ţţ2"), I WS_OVERLAPPEDWINDOW, !: .I CW_USEDEFAULT, CW_USEDEFAULT, I CW_USEDEFAULT, CWţUSEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, nCmdShow) ; Ÿ l UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) 1.7 TranslateMessage (&msg) ; ţ.I DispatchMessage (&msg) ; 1 return msg.wParam ; LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) HDC hdc, hdcEMF ; HENHMETAFILE hemf ; PAINTSTRUCT ps ; RECT rect ; switch (message) f case WM_CREATE: hdcEMF = CreateEnhMetaFile (NULL, TEXT ("emf2.emf"), NULL, TEXT ("EMF2\OEMF Demo ţţ2\0")) ; s !. I if (!hdcEMF) return 0 ; i Rectangle (hdcEMF, 100, 100, 200, 200) ; ţ. MoveToEx (hdcEMF, 100, 100, NULL) ; LineFo (hdcEMF, 200, 200) ; MoveToEx (hdcEMF, 200, 100, NULL) ; 988 Częœć II: Grafika (cigg dalszy ze strony 987) LineTo (hdcEMF, 100, 200) hemf = CloseEnhMetaFile (hdcEMF) ; DeleteEnhMetaFile (hemf) ; return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; GetClientRect (hwnd, &rect) ; rect.left - rect.right / 4 ; rect.right = 3 * rect.right / 4 ; rect.top - rect.bottom / 4 ; rect.bottom = 3 * rect.bottom / 4 ; if (hemf = GetEnhMetaFile (TEXT ("emf2.emf"))) ( PlayEnhMetaFile (hdc, hemf. &rect) DeleteEnhMetaFile (hemf) ; 1 EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: PostOuitMessage (0) ; return 0 ; ) return DefWindowProc (hwnd, message, wParam, lParam) ; Rysunek 1&5. Program EMF2 W programie EMFI wszystkie argumenty funkcji CreateEnhMetafile przyjmowały wartoœć NULL. W EMF2 przyjmuje jš pierwszy argument tej funkcji, który może być uchwytem kontekstu urzšdzerua. GDI używa tego argumentu do umieszcza- nia informacji metrycznej w n„główku metapliku. Jeœli argument przyjmuje war- toœć NLJLL, GDI zakłada, że informacja metryczna opiera się na kontekœcie urzš- dzenia zwišzanym z kartš graficznš. Drugim argumentem funkcji CreateEnhMetaFile jest nazwa pliku. Jeœli argument przyjmie wartoœć NULL (tak jak w programie EMFI), funkcja utworzy metaplik i umieœci go w pamięci. Program EMF2 tworzy na dysku metaplik o nazwie EMF2.EME Trzeci argument funkcji to adres struktury RECT, opisujšcej całkowite rozmiary metapliku z dokładnoœciš do 0,01 mm. Ta istotna informacja (jej brak był jednš z wad starego formatu) umieszczana jest w nagłówku metapliku. Jeœli argument przyjmie wartoœć NULL, GDI automatyczrue wyznaczy odpowiednie rozmiary. Podoba mi się, że system wyręcza mnie w niektórych sprawach, więc pozostawi- łem wartoœć NULL. Jeœli jednak ważnym atutem fwego programu ma być wy- dajnoœć, możesz wypełnić ten argument innš wartoœciš, odcišżajšc GDI. Rozdział 18: Metapliki 989 Ostatnim argumentem jest łańcuch tekstowy opisujšcy metaplik. Składa się on z dwóch częœci: pierwsza jest nazwš aplikacji (nie musi to być nazwa programu) zakończona znakiem NULL, a druga opisuje obraz i jest zakończona dwoma zna- kami NULL. Przy zastosowaniu notacji języka C, w którym NULL jest oznacza- ny jako '\0', łańcuch opisujšcy mógłby wyglšdać na przykład tak: "LoonyCad V6.4\OFlying Frogs\0\0". Ponieważ C wstawia domyœlnie NULL na końcu łań- cuchów znakowych, przykładowy cišg znaków możesz zakończyć pojedynczym '\0' (jak w programie EMF2). Po utworzeniu metapliku EMF2, podobnie jak EMFI, wywołuje kilka funkcji GDI używajšc uchwytu kontekstu urzšdzenia, który zwraca funkcja CreateEnhMeta- File. Następnie program wywołuje CloseEnhMetaFile w celu usunięcia uchwytu kontekstu urzšdzenia i uzyskania uchwytu gotowego metapliku. Następnie, cišgle w trakcie obshzgi komunikatu WM CREATE, EMF2 robi coœ, czego nie było w EMFI: zaraz po otrzymaniu uchwytu metapliku program wy- wołuje funkcję DeleteEnhMetaFile, dzięki czemu zwalniajš się zasoby pamięci, które były niezbędne do przechowywania metapliku. Metaplik na dysku pozostaje nie- naruszony (gdybyœ kiedykolwiek chciał się go pozbyć, użyj zwykłej funkcji do usuwania plików DeleteFile). Zauważ, że uchwyt metapliku rue jest przechowy- wany w postaci zmiennej statycznej, jak w EMFI, co oznacza, że nie trzeba za- chowywać metapliku między obsługš kolejnych komunikatów. Aby skorzystać z utworzonego metapliku, EMF2 musi mieć dostęp do dysku. Operacja ta jest realizowana w trakcie obsługi komunikatu WMţPAINT przez wywołarue funkcji GetEnhMetaFile. Jedynym argumentem wywołania jest nazwa metapliku. Funkcja zwraca uchwyt metapliku, który EMF2, podobnie jak EMFl, przekazuje w postaci argumentu funkcji PlayEnhMetaFile. Metaplik jest wyœwie- tlany w prostokšcie opisanym przez ostatru argument tej funkcji. jednak, w prze- ciwieństwie do EMFI, program EMF2 usuwa metaplik przed zakończeniem ob- sługi WMţPAINT. Po otrzymaniu kolejnego komunikatu WMţPAINT EMF2 po- nownie odczytuje metaplik, odtwarza go, a następnie usuwa. Zapamiętaj, że usunięcie metapliku jest równoznaczne ze zwolnieniem obszaru pamięci, w którym go przechowywano. Metaplik wcišż pozostanie na dysku, nawet po zakończeruu programu. Ponieważ EMF2 pozostawia po sobie metaplik na dysku, można się mu dokład- nie przyjrzeć. Rysunek 18-6 przedstawia zawartoœć metapliku EMF2.EMF two- rzonego przez program EMF2 w zapisie heksadecymalnym. Częœć IIţ Grafika 0000 Oi 00 00 00 88 00 00 00 64 00 00 00 64 00 00 00 ..d...d.. 0010 C8 00 00 00 C8 00 00 00 35 OC 00 00 35 OC 00 00 ...ţ.ţ .5...5..ţ 0020 6A 18 00 00 6A 18 00 00 20 45 4D 46 00 00 Ol 00 j...j.. EMF.. 0030 F4 00 00 00 07 00 00 00 O1 00 00 00 12 00 00 00 0040 64 00 00 00 00 00 00 00 00 04 00 00 00 03 00 00 d...,ţţţ." , 0050 40 O1 00 00 FO 00 00 00 00 00 00 00 00 00 00 00 @..,..,.,ţţ 0060 00 00 00 00 45 00 4D 00 46 00 32 00 00 00 45 00 .. .E.M.F.2...E. 0070 4D 00 46 00 20 00 44 00 65 00 6D 00 6F 00 20 00 M.F. .D.e.m.o. 0080 23 00 32 00 00 00 00 00 2B 00 00 00 18 00 00 00 .2.. ..+.. 0090 63 00 00 00 63 00 00 00 C6 00 00 00 C6 00 00 00 c...c.. OOAO 1B 00 00 00 10 00 00 00 64 00 00 00 64 00 00 00 .. ..d...d.., OOBO 36 00 00 00 10 00 00 OO C8 00 00 00 C8 00 00 00 6..,.. OOCO 1B 00 00 00 10 00 00 00 C8 00 00 00 64 00 00 00 OODO 36 00 00 00 10 00 00 00 64 00 00 00 C8 00 00 00 6.......d.. OOEO OE 00 00 00 14 00 00 00 00 00 00 00 10 00 00 00 00 FO 14 00 00 00 " " " " " ' Rysunek 18-6. Zawartoœć metapliku EMF2.EMF w zapisie heksadecymalnym Powinienem zaznaczyć, że rysunek 18-6 przedstawia metaplik utworzony w œro- dowisku Windows NT 4, pracujšcego w rozdzielczoœci 1024 na 768. Metaplik utworzony przez ten sam program w Windows 98 będzie o 12 bajtów krótszy. Rozdzielczoœć ekranu ma również wpływ na informację umieszczonš w nagłów- ku metapliku. Przyjrzenie się formatowi rozszerzonego metapliku pozwoli na dogłębne zrozu- mienie jego koncepcji. Rozszerzony metaplik składa się z rekordów o zmiennej długoœci. Ogólny format takiego rekordu jest opisany strukturš ENHMETARE- CORD, zdefiniowanš w pliku nagłówkowym WINGDI.H następujšco: typedef struct tagENHMETARECORD f DWORD iType ; // typ rekordu DWORD nSize ; // rozmiar rekordu DWORD dParm [1] ; // parametry 1 ENHMETARECORD : Oczywiœcie, jednoelementowa tablica w rzeczywistoœci symbolizuje tablicę o zmiennych rozmiarach. Liczba parametrów zależy od rodzaju rekordu. Pole iType może przyjmować wartoœć jednej z prawie stu stałych rozpoczynajšcych się przed- rostkiem EMR- i zdefiruowanych w pliku nagłówkowym WINGDI.H.. Zmienna nSize jest rozmiarem całego rekordu, włšczajšc pola iType, nSize oraz jedno lub kilka pól dParam. Z takš wiedzš na temat metapliku możemy przyjrzeć się dokładniej rysunkowi 18-6. Pierwszy rekord jest typu 0x00000001 i ma rozmiar 0x00000088, tak więc zajmuje pierwsze 136 bajtów pliku. Typ rekordu 1 oznacza stałš EMR HEADER. Dyskusję o nagłówku pozostawmy na póŸniej i przejdŸmy do offsetu 0x0088, na końcu pierwszego rekordu. Kolejne pięć rekordów odpowiada funkcjom GDI, wywoływanym w programie EMF2 w trakcie tworzenia metapliku. Kod typu rekordu umieszczony na pozycji offsetu 0x0088 wynosi 0x0000002B, co odpowiada wartoœci stałej EM_RECTAN- GLE, czyli rekordowi zwišzanemu w wywołaruem funkcji Rectangle. Funkcja Rec- tangle przyjmuje w rzeczywistoœci pięć argumentów, lecz pierwszy z nich - uchwyt Rozdział 18: Metapliki 991 kontekstu urzšdzerua - nie jest przechowywany w metapliku. Dwa argumenty przyjmujš wartoœci 0x00000063 (lub 99), a kolejne dwa - wartoœci 0x000000C6 (lub 198), mimo że współrzędne wierzchołków prostokšta okreœlono w progra- mie EMF2 jako (100, 100) oraz (200, 200). W metapliku utworzonym przez pro- gram EMF2 w œrodowisku Windows 98 odpowiednie wartoœci będš wynosić 0x00000064 (lub 100) i 0x000000C7 (lub 199). Oczywiœcie powodem jest to, że Windows dopasowuje współrzędne narożników prostokšta przed umieszczeruem ich w metapliku, lecz proces ten nie odbywa się cišgle. Kolejne cztery 16-bajtowe rekordy metapliku odpowiadajš dwóm wywołaniom MoveToEx (0x0000001B lub EMR MOVETOEX) oraz LineTo (0x00000036 lub EMRţLINETO). Argumenty umieszczone w metapliku sš identyczne z przeka- zanymi do odpowiedruch funkcji. Metaplik kończy się 20-bajtowym rekordem kodem typu 0x0000000E lub EMR EOF ("koniec pliku"). Rozszerzony metaplik zawsze rozpoczyna się rekordem nagłówkowym. Odpo- wiada on strukturze typu ENHMETAHEADER, zdefiniowanej w następujšcy sposób: typedef struct tagENHMETAHEADER ( DWORD iType ; // EMR_HEADER = 1 DWORD nSize ; // rozmiar struktury RECTL rclBounds ; // kontur prostokšta w pikselach RECTL rclFrame ; // rozmiar obrazu (jednostkš jest 0,01 mm) DWORD dSignature ; // ENHMETA_SIGNATURE = " EMF" DWORD nVersion ; // 0X00010000 DWORD nBytes ; // rozmiar pliku w bajtach DWORD nRecords ; // liczba rekordów WORD nHandles ; // liczba uchwytów w tablicy uchwytów WORD sReserved ; DWORD nDescription ; // dlugoœć lańcucha opisujšcego (w znakach) DWORD offDescription ; // poiożenie łańcucha opisujšcego w metapliku DWORD nPalEntries ; // liczba elementów palety SIZEL szlDevice ; // rozdzielczoœć urzšdzenia (w pikselach) SIZEL szlMillimeters ; // rozdzielczoœć urzšdzenia (w milimetrach) DWORD cbPixelFormat ; // rozmiar formatu pikselowego DWORD offPixelFormat ; // położenie formatu pikselowego w metapliku DWORD bOpenGL ; // przyjmuje wartoœć FALSE, jeœli rekordy nie sš typu // OpenGL 1 ENHMETAHEADER ; Istruenie tego rekordu nagłówkowego jest przypuszczalrue jednym z największych rozszerzeń nowego formatu metapliku. Chcšc otrzymać informację nagłówkowš, nie musisz już stosować funkcji wejœcia/wyjœcia, opartych na metapliku zapisa- nym na dysku. Majšc do dyspozycji uchwyt metapliku, możesz zastosować funkcję GetEnhMetaFileHeader: GetEnhMetaFileHeader (hemf, cbSize, &emh) ; Pierwszym argumentem jest uchwyt metapliku, a ostatnim - wskaŸnik do struk- tury ENHMETAHEADER. Drugi argument to rozmiar tej struktury. jeœli chcesz otrzymać łańcuch opisujšcy, możesz użyć podobnej funkcji o nazwie MetaFileDe- scription. Czţœć II: Grafika Zdefiniowana powyżej struktura ENHMETAHEADER ma długoœć 100 bajtów, lecz plik EMF2.EMF zawiera ponadto łańcuch opisujšcy, co daje w sumie roz- miar 0x88 lub 136 bajtów. Nagłówek metapliku w Windows 98 nie zawiera ostat- nich trzech pól struktury ENHMETAHEADER, co zmniejsza jego długoœć o 12 bajtów. Pole rclBounds jest zmiennš typu RECT, okreœlajšcš rozmiary obrazu wyrażone w pikselach. W przykładowym metapliku widać, że obraz jest ograniczony pro- stokštem o następujšcych współrzędnych wierzchołków: (1(ţ, 100) - lewy górny wierzchołek i (200, 200) - prawy dolny wierzchołek. Pole rclFrame jest kolejnš strukturš typu RECT, zawierajšcš te same informacje co poprzednio, lecz wyrażone w jednostkach równych 0,01 milimetra. W tym wypadku w metapliku można zobaczyć następujšce współrzędne graniczne pro- stokšta (OxOC35, OxOC35) i (0x186A, 0x186A ), czyli (3125, 3125) i (6250, 6250). Skšd wzięły się takie wartoœci w metapłiku? Przekonamy się o tym wkrótce. Pole dSignature przyjmuje zawsze wartoœć ENHMETŽ SIGNATURE lub 0x464D4520. Liczba wyglšda dosyć dziwnie, lecz po odwróceniu kolejnoœci baj- tów (zgodnie ze sposobem przechowywania wielobajtowych słów w pamięci przez procesor Intel) i konwersji na znaki ASCII otrzymamy cišg znaków "EMF". Pole nversion przyjmuje zawsze wartoœć 0x00010000. Kolejne pole, nByte, które w tym wypadku przyjmuje wartoœć 0x000000F4, okre- œla całkowitš wielkoœć metapliku. Pole nRecords (w tym wypadku o wartoœci 0x000ţ07) okreœla liczbę rekordów - rekord nagłówkowy, pięć wywołań funk- cji GDI oraz rekord końca pliku. Następnie mamy dwa 16-bitowe pola. Wartoœć nHandles wynosi 0x0001. Pole to okreœla zazwyczaj liczbę uchwytów obiektów graficznych, które nie sš traktowa- ne jako domyœlne (takich jak pisaki, pędzle i czcionki), lecz zostały zastosowane w metapliku. Pole nie zostało wykorzystane, więc jego wartoœć w naszym wy- padku jest równa 0. Wkrótce dowiesz się, jak uchwyty przechowywane sš w me- taplikach. Kolejne dwa pola okreœlajš długoœć łańcucha opisujšcego, podanš jako liczba znaków, oraz jego offset w metapliku, w tym wypadku 0x00000012 (lub 18) oraz 0x00000064. Jeœli metaplik nie jest wyposażony w łańcuch opisujšcy, oba pola przyjmš wartoœć zerowš. Pole nPalEntries opisuje liczbę elementów palety kolorów metapliku; w omawia- nym przykładzie nie użyto palety. Kolejnymi polami rekordu nagłówkowego sš dwie struktury typu SIZEL, zawie- rajšce dwie 32-bitowe zmienne cx i cy. Pole sziDevice (o offsecie 0x0040 w meta- pliku) okreœla rozmiar urzšdzenia wyjœciowego wyraŸony w pikselach, a szlMi- limeters (o offsecie 0x0050) jest rozmiarem urzšdzenia wyjœciowego wyrażonym w milimetrach. W dokumentacji rozszerzonego metapliku to urzšdzenie wyjœcio- we jest nazwane urzšdzeniem odniesienia. Oparte jest ono na kontekœcie urzš- dzenia okreœlanym przez uchwyt, przekazywany jako pierwszy argument funk- cji CreateEnhMetaFile. Jeœli argument przyjmuje wartoœć NULL, GDI użyje ekra- nu morutora jako urzšdzenia wyjœciowego. W trakcie tworzenia metapliku przed- stawionego na rysunku 18-6 generujšcy go program EMF2 pracował w œrodowi- Rozdział 18: kţetapliki sku Windows NT w rozdzielczoœci 1024 na 768; w omawianym przykładzie taka konfiguracja odgrywała rolę urzšdzenia odniesienia. GDI otrzymuje tego rodzaju informację po wywołaniu funkcji GetDeviceCaps. Wartoœć pola szlDevice w pliku EMF2.EMF wynosi 0x0400 na 0x03(ţ (czyli 1024 na 768) i jest zwracana przez funkcję GetDeviceCaps, przyjmujšcš argumenty HORZRES i VERTRES. Pole szlMilimeters przyjmuje wartoœć 0x140 na OxFO lub 320 na 240 zwracanš przez funkcję GetDeviceCaps z argumetami HOftZSIZE i VERTSIZE Wykonujšc proste dzielenie można wyznaczyć rozmiary piksela, które wynoszš 0,3125 milimetra długoœci i szerokoœci. Na tej podstawie GDI wyznacza rozmiary opisanego powyżej prostokšta rclFrame. Tuż za strukturš typu ENHMETAHEADER w metapliku znajduje się łańcuch opi- sujšcy, który był przyjmowany jako ostatni argument funkcji CreateEnhMetaFile. W omawianym przykładzie zmienna ma postać cišgu znaków "EMF2" zakoń- czonego znakiem NULL i cišgiem "EMF Demo #2", po którym następujš dwa znaki NULL. Łšcznie długoœć cišgu wynosi 18 znaków lub 36 bajtów, gdyż zapi- sano go czcionkami Unicode. Łańcuch opisujšcy ma zwykle postać Unicode bez względu na to, czy program pracuje w systemie Windows NT czy Windows 98. Metapki i obiekty GDł Teraz dowiesz się, w jaki sposób polecenia GDI sš przechowywane w metapliku. Najpierw przyjrzyjmy się, jak przechowywane sš obiekty GDI. Program EMF3, przedstawiony na rysunku 18-7, jest podobny do EMF2, ale tworzy niedomyœlny pisak i pędzel do rysowania obrazu. Program uwzględnia tym razem niedokład- noœci, które pojawiły się w punkcie połšczenia wierzchołka prostokšta i jego prze- kštnej. EMF3 korzysta z funkcji GetUersion do okreœlenia systemu operacyjnego, z którym działa, oraz w celu dokładruejszego dobrania parametrów wywołania funkcji. EMF3.C /* EMF3.C - Metaplik rozszerzony Demo 113 (c) Charles Petzold, 1998 */ ilinclude LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) static TCHAR szAppNameC7 ř TEXT ("EMF3") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; Częœć II, Grafika ; (cišg dalszy ze strony 993) > wndclass.style = CS HREDRAW ţ CS VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; r wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) ( MessageBox (NULL, TEXT ("This program requires Windows NT!"), r szAppName, MBţICONERROR) return 0 ; t r hwnd = CreateWindow (szAppName, TEXT ("Enhanced Metafile Demo ţţ3"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CWţUSEDEFAULT, CW_USEDEFAULT, CWţUSEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) ( TranslateMessage (&msg) ; DispatchMessage (&msg) ; return msg.wParam ; LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) f LOGBRUSH lb ; HDC hdc, hdcEMF ; HENHMETAFILE hemf ; PAINTSTRUCT ps ; RECT rect ; switch (message) case WM_CREATE: hdcEMF = CreateEnhMetaFile (NULL, TEXT ("emf3.emf"), NULL, TEXT ("EMF3\OEMF Demo ţţ3\0")) ; j SelectObject (hdcEMF, CreateSolidBrush (RGB (0, 0, 255))) ; I lb.lbStyle = BS_SOLID ; lb.lbColor = RGB (255, 0, 0) ; lb.lbHatch = 0 ; SelectObject (hdcEMF, ExtCreatePen (PS SOLID ţ PSţGEOMETRIC, 5, &lb, 0, NULL)) ; Rozdział 18: Metapliki g95 if (Getllersion () & 0x80000000) // Windows 98 Rectangle (hdcEMF, 100, 100, 201, 201) ; else // Windows NT Rectangle (hdcEMF, 101, 101, 202, 202) ; MoveToEx (hdcEMF, 100, 100, NULL) ; LineTo (hdcEMF, 200, 200) ; MoveToEx (hdcEMF, 200, 100, NULL) ; LineTo (hdcEMF, 100, 200) ; Delete0bject (SelectObject (hdcEMF, GetStockObject (BLACK PEN))) ; Delete0bject (SelectObject (hdcEMF, GetStockObject (WHITĘ BRUSH))) ; hemf = CloseEnhMetaFile (hdcEMF) ; DeleteEnhMetaFile (hemf) ; return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; GetClientRect (hwnd, &rect) ; rect.left - rect.right / 4 ; rect.right = 3 * rect.right / 4 ; rect.top - rect.bottom / 4 ; rect.bottom = 3 * rect.bottom / 4 ; hemf = GetEnhMetaFile (TEXT ("emf3.emf")) ; PlayEnhMetaFile (hdc. hemf, &rect) ; DeleteEnhMetaFile (hemf) ; EndPaint (hwnd, &ps) ; return 0 ; case WM DESTROY: PostOuitMessage (0) ; return 0 : j return DefWindowProc (hwnd, message, wParam, lParam) ; Rysunek 18-7. Program EMF3 Kiedy wywołujesz funkcje GDI korzystajšce z uchwytu kontekstu urzšdzenia, zwracanego przez funkcję CreateEnhMetaFile, sš one umieszczane najpierw w me- tapliku. Niektóre z funkcji GDI nie majš ruc wspólnego z konkretnym kontek- stem urzšdzenia. Jednš z ważnych kategor sš funkcje tworzšce obiekty graficz- ne, włšczajšc w to pisaki i pędzle. Definicje logicznych pisaków i pędzli, prze- chowywane w pamięci zarzšdzanej przez GDI, w czasie tworzerua nie sš zwiš- zane z żadnym kontekstem urzšdzenia. EMF3 wywołuje zarówno CreateSolidBrush, jak i ExtCreatePen. Ponieważ funkcje te nie wymagajš uchwytu kontekstu urzš- dzenia, GDI nie umieszcza ich w metapliku. W wypadku tradycyjnego wywoła- Częœć II: Grafika nia wspomniane funkcje tworzš obiekty GDI, rue majšc najmniejszego wpływu na metapliki. Jednak kiedy program wywołuje funkcje SelectObject, aby wybrać obiekt GDI w kon- tekœcie urzšdzenia, GDI koduje zarówno funkcję tworzšcš obiekt, jak i wywołanie SelectObject i umieszcza obie w metapliku. Aby dowiedzieć się, jak to działa, przyj- rzyjmy się metaplikowi EMF3.EMF, przedstawionemu na rysunku 18-8. 0000 O1 00 00 00 88 00 00 00 60 00 00 00 60 00 00 00 . ' ' 0010 CC 00 00 00 CC 00 00 00 B8 OB 00 00 B8 OB 00 00 0020 E7 18 00 00 E7 18 00 00 20 45 4D 46 00 00 O1 00 .........EMF.... 0030 88 O1 00 00 OF 00 00 00 03 00 00 00 12 00 00 00 0040 64 00 00 00 00 00 00 00 00 04 00 00 00 03 00 00 d......ţţ.ţ 0050 40 O1 00 00 FO 00 00 00 00 00 00 00 00 00 00 00 @..ţţ.ţ...ţ 0060 00 00 00 00 45 00 4D 00 46 00 33 00 00 00 45 00 ....E.M.F.3...E. 0070 4D 00 46 00 20 00 44 00 65 00 6D 00 6F 00 20 00 M.F. .D.e.m.o. 0080 23 00 33 00 00 00 00 00 27 00 00 00 18 00 00 00 11.3.. 0090 Oi 00 00 00 00 00 00 00 00 00 FF 00 00 00 00 00 OOAO 25 00 00 00 OC 00 00 00 O1 00 00 00 5F 00 00 00 %.....ţ..ţţ _ OOBO 34 00 00 00 02 00 00 00 34 00 00 00 00 00 00 00 4.......4.. OOCO 34 00 00.00 00 00 00 00 00 00 O1 00 05 00 00 00 4..... OODO 00 00 00 00 FF 00 00 00 00 00 00 00 00 00 00 00 OOEO 25 00 00 00 OC 00 00 00 02 00 00 00 2B 00 00 00 %...........+..ţ OOFO 18 00 00 00 63 00 00 00 63 00 00 00 C6 00 00 00 ....c...c.. 0100 C6 00 00 00 1B 00 00 00 10 00 00 00 64 00 00 00 ..d... 0110 64 00 00 00 36 00 00 00 10 00 00 00 C8 00 00 00 d...6..ţ.. 0120 C8 00 00 00 1B 00 00 00 10 00 00 00 C8 00 00 00 0130 64 00 00 00 36 00 00 00 10 00 00 00 64 00 00 00 d...6..ţ....d..ţ 0140 C8 00 00 00 25 00 00 00 OC 00 00 00 07 00 00 80 ..%....ţ 0150 28 00 00 00 OC 00 00 00 02 00 00 00 25 00 00 00 (.. .ţ...%... 0160 OC 00 00 00 00 00 00 80 28 00 00 00 OC 00 00 00 ţ....(.. 0170 O1 00 00 00 OE 00 00 00 14 00 00 00 00 00 00 00 0180 10 00 00 00 14 00 00 00 Rysunek 18-8. Zawartoœć metapliku EMF3.EMF w zapisie heksadecymalnym Na pewno chciałbyœ porównać zawartoœć tego pliku z przedstawionym wczeœruej EMF2.EMF. Pierwszš różnicš, którš zauważysz w częœci nagłówkowej, jest za- wartoœć pola rclBounds. W pliku EMF2.EMF okreœlono granice obrazu między współrzędnymi (0x64, 0x64) i (OxC8, OxC8). Dla EMF3.EMF odpowiednie warto- œci wynoszš (0x60, 0x60) i (OxCC, OxCC). Jest to wyrukiem zastosowania grub- szego pisaka. Zmieniła się również wartoœć pola rclFrame (okreœlajšca rozmiar obrazu w setnych częœciach milimetra). Podczas gdy pole nBytes (o offsecie 0x0030) w pliku EMF2.EMF wskazywało, że metaplik ma długoœć OxFa bajtów, długoœć EMF3.EMF wynosi 0x0188 bajtów. Me- taplik EMF2.EMF zawierał 7 rekordów (nagłówek, 5 wywołań funkcji GDI i re- kord końca pliku), a w metapliku EMF3.EMF jest ich aż 15. Jak się wkrótce oka- że, na 8 dodatkowych rekordów składajš się: 2 funkcje tworzšce obiekty, 4 wy- wołania SelectObject i 2 wywołarua DeleteObject. Pole nHandles (o offsecie w metapliku - 0x0038) okreœla liczbę uchwytów obiek- tów GDI. Jest ona zwykle o 1 większa niż liczba uchwytów obiektów niedomyœl- nych używanych przez metaplik. (Dokumentacja Platformy SDK okreœla to na- Rozdział 18: iNetapliki 997 stępujšco: "Indeks zerowy tej tablicy jest zarezerwowany"). Pole to przyjmuje war- toœć 1 w pliku EMF2.EMF i 3 w pliku EMF3.EMF, co oznacza dwa obiekty: pisak i pędzel. PrzejdŸmy do offsetu 0x0088. Znajduje się tam drugi rekord (pierwszy po rekor- dzie nagłówkowym). Typem rekordu jest 0x27, co odpowiada wartoœci stałej EMR-CREATEBRUSHáţlDIRECT. Jest to rekord metapliku odpowiadajšcy funkcji CreateBrushlndirect, która przyjmuje jako argument wskaŸnik do struktury LOG- BRUSH. Rozmiar rekordu wynosi 0x18 (lub 24) bajty. Każdemu "niezapasowemu" obiektowi GDI, wybieranemu w kontekœcie urzš- dzenia metapliku, nadawany jest numer poczšwszy od 1. Jest to okreœlone 4-baj- towym słowem w tym rekordzie o offsecie 0x0090 w metapliku. Kolejne trzy 4- bajtowe pola tego rekordu odpowiadajš trzem polom struktury LOGBRUSH, 0x00000000 (pole lbStyle), 0x00FF0000 (pole IbColor) oraz 0x00000000 (pole IbHatch). Kolejny rekord znajduje się w metapliku EMF3.EMF o offsecie 0x00A0. Typem rekordu jest 0x25 lub EMRţSELECTOBJECT, co odpowiada wywołaniu funkcji SelectObject. Długoœć rekordu wynosi OxOC (lub 12) bajtów, a następne pole ma numer 0x01, co oznacza, że dotyczy pierwszego obiektu GDI, którym jest logicz- ny pędzel. Następny rekord znajduje się w pliku EMF3.EMF o offsecie 0x00AC. Typ rekor- du to Ox5F lub EMR EXTCREATEPEN. Długoœć rekordu wynosi 0x34 (lub 52) bajty. Wartoœć kolejnego 4-bajtowego pola wynosi 0x02, co oznacza, że jest to drugi obiekt GDI używany przez metaplik. Nie będę udawał, że wiem, dlaczego w kolejnych czterech polach rekordu EMR EXTCREATEPEN jego rozmiar powtarza się dwukrotnie, przeplatany war- toœciš 0 - po prostu tak jest: 0x34, 0x00, 0x34 i 0x00. Kolejne pole przyjmuje war- toœć 0x00010000, co odpowiada rodzajowi pisaka - w tym wypadku PS SOLID (0x00000000) w połšczeniu z PS GEOMETRIC (0x00010000). Następnie mamy gruboœć pisaka (5 jednostek) i trzy pola, pochodzšce ze struktury opisujšcej lo- giczny pędzel i stosowanej przy wywołaniu funkcji ExtCreatePen, oraz ostatnie pole o wartoœci 0. Jeœli utworzysz własny styl pisaka o rozszerzonych możliwoœciach, rozmiary re- kordu EMR EXTCREATEPEN będš większe niż 52 bajty, co znajdzie odbicie nie tylko w drugim polu rekordu, lecz także w dwóch powtarzajšcych się polach opisujšcych rozmiar. Zmienna, która następuje po trzech polach opisujšcych struk- turę LOGBRUSH, nie będzie mieć wartoœci 0 (jak w metapliku EMF3.EMF), lecz odpowiadajšcš liczbie kresek i przerw w lin przerywanej. Potem następujš pola dokładnie opisujšce wyglšd tej lin. Następne 12-bajtowe pole pliku EMF3.EMF opisuje kolejne wywołanie funkcji SelectObject, powišzane z drugim obiektem GDI - pisakiem. Kolejne pięć rekor- dów jest takie same jak w pliku EMF2.EMF - jeden typu Ox2B (EMR RECTAN- GLE) oraz po dwa rekordy typu OxlB (EMR MOVETOEX) i 0x36 (EMR LINE- TO). Po rekordach opisujšcych funkcje rysujšce następujš dwa zestawy 12-bajtowych rekordów typu 0x25 (EMR SELECTOBJECT) i 0x28 (EMR DELETEOBJECT). Re- kordy opisujšce wybieranie obiektów przyjmujš argumenty 0x80000007 998 Częœć IIů Grafika i 0x80000000. Jeœli ustawiony jest najstarszy bit, oznacza on obiekt zapasowy - w tym przypadku 0x07 (odpowiada pisakowi czamemu - BLACK PEN), a 0x00 oznacza pędzel w kolorze białym (WHTTE-BRUSH). Funkcja DeleteObject przyjmuje wartoœci 2 i 1, odpowiadajšce numerom niedo- myœlnych obiektów GDI wykorzystywanych w metapliku. Chociaż funkcja Dele- teObject nie przyjmuje uchwytu kontekstu urzšdzerua jako pierwszego argumentu, GDI najwidoczniej troszczy się o obiekty metapliku usuwane przez program. Wr ţ zcie metaplik kończy się rekordem typu OxOE, czyli EMF EOF ("koniec pli- ku . Podsumowujšc: zawsze gdy niedomyœlny obiekt GDI jest po raz pierwszy wy- bierany w kontekœcie urzšdzera metapliku, GDI koduje do postaci rekordu obie funkcje tworzšce obiekt (w opisywanym wypadku EMR-CREATEBRUSHINDI- RECT i EMR EXTCREATEPEN). Każdemu z obiektów nadawany jest niepowta- rzajšcy się numer, poczšwszy od 1, umieszczony w trzecim polu rekordu. Kolej- nym rekordem będzie EMR SELECTOBJECT, który odwołuje się do tego nume- ru. Jeœli obiekt wybierany jest w kontekœcie urzšdzenia po raz kolejny (o ile nie został usunięty), potrzebny będzie jedyrue rekord EMR SELECTOBJECT. Metapliki i bitmapy Spróbujmy czegoœ bardziej skomplikowanego, czyli rysowania bitmapy w kon- tekœcie urzšdzenia metapliku. Demonstruje to program EMR4 przedstawiony na rysunku 18-9. EMF4.C /* EMF4.C - Metaplik rozszerzony Demo ţ14 (c) Charles Petzold, 1998 */ Ildefine OEMRESOURCE llinclude LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) static TCHAR szAppName[] = TEXT ("EMF4") HWND hwnd ; MSG msg ; WNDCLASS wndclass : wndclass.style = CS_HREDRAW ţ CSţVREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI ŽPPLICATION) Rozdział 18: Metapliki 4 wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) : wndclass.hbrBackground = GetStockObject (WHITEţBRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) MessageBox (NULL, TEXT ("This program requires Windows NT!"), , szAppName, MBţICONERROR) ; return 0 ; hwnd = CreateWindow (szAppName, TEXT ("Enhanced Metafile Demo ţţ4"), WS_OUERLAPPEDWINDOW, i CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, t NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) ( TranslateMessage (&msg) ; DispatchMessage (&msg) ; ) return msg.wParam ; ) LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ( BITMAP bm ; HBITMAP hbm ; HDC hdc, hdcEMF, hdcMem ; HENHMETAFILE hemf ; PAINTSTRUCT ps ; RECT rect ; switch (message) ( case WM_CREATE: hdcEMF = CreateEnhMetaFile (NULL, TEXT ("emf4.emf"), NULL, TEXT ("EMF4\OEMF Demo 4t4\0")) ; hbm = LoadBitmap (NULL, MAKEINTRESOURCE (OBM CLOSE)) ; GetObject (hbm. sizeof (BITMAP). &bm) ; hdcMem = CreateCompatibleDC (hdcEMF) ; SelectObject (hdcMem, hbm) ; StretchBlt (hdcEMF, 100, 100, 100, 100, hdcMem, 0, 0, bm.bmWidth, bm.bmHeight, SRCCOPY) ; DeleteDC (hdcMem) ; Delete0bject (hbm) ; 1000 Częœć II: Grafika (cišg dalszy ze strony 999) hemf = CloseEnhMetaFile (hdcEMF) ; DeleteEnhMetaFile (hemf) ; return 0 ; case WM_PAIPIT: hdc = BeginPaint (hwnd, &ps) ; GetClientRect (hwnd, &rect) ; rect.left - rect.right / 4 ; rect.right = 3 * rect.right / 4 ; rect.top - rect.bottom / 4 ; rect.bottom = 3 * rect.bottom / 4 ; hemf = GetEnhMetaFile (TEXT ("emf4.emf")) ; PlayEnhMetaFile (hdc, hemf, &rect) ; DeleteEnhMetaFile (hemf) ; EndPaint (hwnd, &ps) ; return 0 ; case WM-DESTROY: PostOuitMessage (0) ; ţeturn 0 ; 1 return DefWindowProc (hwnd, message, wParam, lParam) ; Rysunek 1&9. Program EMF4 Dla wygody EMF4 wczytuje systemowš mapę bitowš okreœlonš za pomocš sta- łej OEM CLOSE. Tradycyjnym sposobem wyœwietlenia bitmapy jest utworzenie kontekstu urzšdzenia pamięci zgodnego z danym kontekstem urzšdzenia (w tym wypadku zwišzanym z metaplikiem) poprzez wywołanie funkcji CreateCompati- bleDC. Następnie w utworzonym w ten sposób kontekœcie urzšdzenia należy wybrać bitmapę za pomocš funkcji SelectObject oraz BitBlt i StretchBlt. Po zakoń- czeniu wszystkich operacji programu trzeba usunšć zarówno kontekst urzšdze- nia zwišzany z pamięciš, jak i samš bitmapę. Na pewno zauważysz, że EMF4 wywołuje również funkcję GetObject, aby wy- znaczyć rozmiar bitmapy. Jest to niezbędne do wywołania SelectObject. Na pierwszy rzut oka mogłoby się wydawać, że przechowywanie kodu progra- mu w postaci metapliku jest nie lada wyzwaniem dla GDI. Żadna z funkcji po- przedzajšcych wywołanie StretchBlt nie zawiera odwołań do kontekstu urzšdze- nia metapliku. SprawdŸmy więc, jak jest to zrealizowane, na przykładzie meta- pliku EMF4.EMF, którego fragmenty przedstawiono na rysunku 18-10. Rozdział 18: Metaptiki 1001 0000 Oi 00 00 00 88 00 00 00 64 00 00 00 64 00 00 00 ........d...d.. 0010 C7 00 00 00 C7 00 00 00 35 OC 00 00 35 OC 00 00 ........5...5... 0020 4B 18 00 00 4B 18 00 00 20 45 4D 46 00 00 O1 00 K...K... EMF.... ů 0030 FO OE 00 00 03 00 00 00 O1 00 00 00 12 00 00 00 ................ 0040 64 00 00 00 00 00 00 00 00 04 00 00 00 03 00 00 d............... ř 0050 40 O1 00 00 FO 00 00 00 00 00 00 00 00 00 00 00 @............... ' 0060 00 00 00 00 45 00 4D 00 46 00 34 00 00 00 45 00 ....E.M.F.4...E. " 0070 4D 00 46 00 20 00 44 00 65 00 6D 00 6F 00 20 00 M.F. .D.e.m.o. . 0080 23 00 34 00 00 00 00 00 4D 00 00 00 54 OE 00 00 i1.4.....M...T... ; 0090 64 00 00 00 64 00 00 00 C7 00 00 00 C7 00 00 00 d...d........... OOAO 64 00 00 00 64 00 00 00 64 00 00 00 64 00 00 00 d...d...d...d... ' OOBO 20 00 CC 00 00 00 00 00 00 00 00 00 00 00 80 3F ..............? ? OOCO 00 00 00 00 00 00 00 00 00 00 80 3F 00 00 00 00 ...........?.... OODO 00 00 00 00 FF FF FF 00 00 00 00 00 6C 00 00 00 ............l... OOEO 28 00 00 00 94 00 00 00 CO OD 00 00 28 00 00 00 (...........(... OOFO 16 00 00 00 28 00 00 00 28 00 00 00 16 00 00 00 ....(...(....... 0100 O1 00 20 00 00 00 00 00 CO OD 00 00 00 00 00 00 .. ............. ' 0110 00 00 00 00 00 00 00 00 00 00 00 00 CO CO CO 00 ................ 0120 CO CO CO 00 CO CO CO 00 CO CO CO 00 CO CO CO 00 ................ OEDOţ CO CO CO 00 CO CO CO 00 CO CO CO 00 OE 00 00 00 ................ OEEO 14 00 00 00 00 00 00 00 10 00 00 00 14 00 00 00 ................ Rysunek 18-10. Fragmenty metapliku EMF4.EMF w zapisie heksadecymalnym Przedstawiony metaplik zawiera jedynie trzy rekordy - nagłówkowy, rekord typu Ox4D (lub EMR STRETCHBLT) o długoœci 0x0E54 oraz rekord końca pliku. Nie będę ukrywał, że nie rozszyfrowałem, co znaczy każde z pól rekordu EMR STRETCHBLT. W skrócie omówię podstawy potrzebne do zrozumienia, jak GDI tłumaczy wywołania funkcji z pliku EMF4.C w celu ich umieszczenia w po- jedynczym rekordzie metapliku. GDI przeprowadza konwersję oryginalnej bitmapy zależnej od urzšdzenia (DDB) na format mapy niezależnej od urzšdzenia (DIB). DIB przechowywana jest w strukturze o rozmiarach równych rozmiarom mapy. Podejrzewam, że kiedy nad- chodzi moment odtworzenia metapliku i wyœwietlenia bitmapy, GDI używa funk- cji StretchDIBits zamiast StretchBlt. Może również skonwertować DIB znów na format DDB za pomocš funkcji CreateDIBitmap, a następrue użyć kontekstu urzš- dzenia pamięci oraz funkcji StretchBlt. Rekord EMR STRETCHBLT rozpoczyna się pod adresem 0x0088 w metapliku. DIB jest przechowywana w metapliku na pozycji o offsecie 0x00F4 i cišgrue się aż do offsetu OxOEDC. Rozpoczyna się 40-bajtowš strukturš typu BITMAPINFO- HEADER. Po niej następujš dane od offsetu 0x011C, czyli 22 wiersze po 40 pikse- li każdy. DIB jest 32-bitowa, więc każdy piksel wymaga 4 bajtów. Wyliczanie metapliku Chcšc uzyskać dostęp do poszczególnych rekordów metapliku, musisz zastosować procedurę zwanš wyliczaniem metapliku. Proces taki demonstruje program EMF5, który przedstawiono na rysunku 18-11. EMF5 wykorzystuje metaplik do wyœwie- tlenia tego samego obrazu co EMF3, lecz stosujšc technikę wyliczania metapliku. T 1002 Częœć II: Grafika EMF5.C ! /* EMF5.C - Metaplik rozszerzony Demo ţţ5 (c) Charles Petzold, 1998 */ ţţinclude LRESULT CALLBACK WndProc (HWND, UINT. WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) ( static TCHAR szAppName[] = TEXT ("EMF5") ; HWND hwnd ; MSG msg WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CS VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) ( MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MB-ICONERROR) ; return 0 ; ) hwnd = CreateWindow (szAppName, TEXT ("Enhanced Metafile Demo ţţ5"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW USEDEFAULT, CW_USEDEFAULT, CW USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) ( TranslateMessage (&msg) ; DispatchMessage (&msg) ; return msg.wParam ; int CALLBACK EnhMetaFileProc (HDC hdc, HANDLETABLE * pHandleTable, CONST ENHMETARECORD * pEmfRecord, int iHandles, LPARAM pData) Rozdział 18: Metapliki 1003 ( PlayEnhMetaFileRecord (hdc, pHandleTable, pEmfRecord, iHandles) ; return TRUE ; LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) t HDC hdc ; HENHMETAFILE hemf ; PAINTSTRUCT ps ; RECT rect ; switch (message) ( case WM_PAINT: hdc = BeginPaint (hwnd, &ps) : GetClientRect (hwnd, &rect) ; rect.left - rect.right / 4 ; ' rect.right = 3 * rect.right / 4 ; rect.top - rect.bottom / 4 ; rect.bottom = 3 * rect.bottom / 4 ; hemf = GetEnhMetaFile (TEXT ("..\\emf3\\emf3.emf")) ; EnumEnhMetaFile (hdc, hemf, EnhMetaFileProc, NULL, &rect) ; DeleteEnhMetaFile (hemf) ; EndPaint (hwnd, &ps) ; return 0 ; case WM DESTROY: PostOuitMessage (0) ; return 0 ; l return DefWindowProc (hwnd, message, wParam, lParam) ; Rysunek 18-11. Program EMF5 Do poprawnej pracy program wymaga istnienia pliku EMF3.EMF, utworzonego ţ I za pomocš aplikacji EMF3. Oba należy uruchomić w œrodowisku programistycz- nym Visual C++ tak, aby zgadzały się œcieżki odpowiednich katalogów. Najważ- ( niejsza różnica między tymi programarni, jakš można zauważyć w kodzie obshx- gujšcym komunikat WMţPAINT, jest taka, że EMF3 wywohţje PIayEnhMetaFile, y ; a EMF5 EnumEnhMetaFile. Przypomnij sobie, że funkcja PIayEnhMetaFile ma na- stępujšcš składnię: PlayEnhMetaFile (hdc, hemf, &rect) ; Pierwszym argumentem jest uchwyt kontekstu urzšdzenia, w którym metaplik ma być odtworzony. Drugi argument jest uchwytem rozszerzonego metapliku. Ostatru, trzeci argument wywołania to wskaŸnik do struktury RECT, opisujšcej prostokšt powierzchni powišzany z dowolnym urzšdzeniem wyjœciowym. Me- taplik jest rozcišgany do rozmiarów tego prostokšta. Ž;.. Częœć If: Grafika Funkcja EnumEnhMetaFile przyjmuje pięć argumentów, z których trzy sš iden- tyczne jak w funkcji PIayEnhMetaFile. Czwarty argument jest wskaŸnikiem do danych, które mogš być przekazane do funkcji wyliczajšcej. Wartoœciš tego argu- mentu jest w naszym wypadku NLTLL. Teraz przyjrzyjmy się funkcji wyliczajšcej. Z poziomu funkcji EnumEnhMetaFile GDI wywołuje EnhMetaFileProc raz dla każdego rekordu w metapliku, nie wyłš- czajšc rekordu nagłówkowego i rekordu końca pliku. Zwykle funkcja wyliczajš- ca zwraca wartoœć TRUE, lecz w razie przerwania procesu wyliczania zwróci FALSE. Funkcja wyliczajšca przyjmuje pięć argumentów, które zostanš w skrócie opisa- ne. W przykładowym programie przekazałem pierwsze cztery z nich do funkcji PIayEnhMetaFileRecord, aby otrzymać te same wyniki co w programie EMF3 po wywołaniu funkcji PlayEnhMetaFile. Różnica tkwi w tym, że EMF5 ma dostęp do każdego rekordu metapliku w trakcie jego odtwarzania. Taka własnoœć może okazać się użyteczna. Pierwszym parametrem funkcji wyliczajšcej jest uchwyt kontekstu urzšdzenia, będšcy jednoczeœnie pierwszym argumentem funkcji EnumEnhMetaFile. Funkcja wyliczajšca przekazuje go do funkcji PlayEnhMetaFileRecord, aby okreœlić kontekst urzšdzenia, w którym metaplik będzie odtwarzany. Pozwól, że pominę trzeci argument funkcji wyliczajšcej. Jest to omówiony wcze- œniej wskaŸnik do struktury typu ENHMETARECORD, opisujšcej rzeczywisty rekord w postaci, w której został umieszczony w metapliku. Mógłbyœ spróbować pominšć kilka rekordów, nie przekazujšc ich do funkcji Play- EnhMetaFileRecord, na przykład w pliku przed wywołaruem funkcji EMF5.C mo- żesz wstawić następujšcš linię kodu: if (pEmfRecord->iType != EMRţLINETO) Przekompiluj program, uruchom go, a zobaczysz jedynie prostokšt pozbawiony przekštnych. Możesz również wstawić następujšcš linię: if (pEmfRecord->iType != EMR SELECTOBJECT) Ta mała zmiana spowoduje, Ÿe obraz będzie rysowany za pomocš obiektów do- myœlnych - a nie pisaka i pędzla, które zostały utworzone. Jedynš rzeczš, której nie powinieneœ robić, jest modyfikacja rekordu metapliku. Zanim jednak zirytuje cię to ograniczenie, przyjrzyjmy się programowi EMF6 na rysunku 18-12. EMF6.C /* EMF6.C - Metaplik rozszerzony Demo 416 (c) Charles Petzold, 1998 */ Ilinclude Rozdział 18: Metapliki 1005 LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR lpszCmdLine, int iCmdShow) ( static TCHAR szAppName[] = TEXT ("EMF6") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CS VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = GetStockObject (WHITEţBRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) ( MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MBţICONERROR) ; return 0 ; } hwnd = CreateWindow (szAppName, TEXT ("Enhanced Metafile Demo ţţ6"), ţ. WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CWţUSEDEFAULT, CW_USEDEFAULT, CW USEDEFAULT, NULL, NULL, hInstance, NULL) ; SpowWindow (hwnd, iCmdShow) ; U dateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) ( ţ. TranslateMessage (&msg) ; DispatchMessage (&msg) ; 1 i return msg.wParam ; 1 int CALLBACK EnhMetaFileProc (HDC hdc, HANDLETABLE * pHandleTable, ? CONST ENHMETARECORD * pEmfRecord, int iHandles, LPARAM pData) ( ENHMETARECORD * pEmfr ; pEmfr = (ENHMETARECORD *) malloc (pEmfRecord->nSize) ; CopyMemory (pEmfr, pEmfRecord, pEmfRecord->nSize) ; if (pEmfr->iType = EMR_RECTANGLE) pEmfr->iType = EMRţELLIPSE ; 1006 Częœć II: Grafika (cišg dalszy ze strony 1005) PlayEnhMetaFileRecord (hdc, pHandleTable, pEmfr, iHandles) ; free (pEmfr) ; return TRUE ; LRESULT CALLBACK WndProc (HWND hwnd, UINT messa9e, WPARAM wParam, LPARAM lParam) ( HDC hdc ; HENHMETAFILE hemf ; PAINTSTRUCT ps ; RECT rect ; switch (message) ( case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; GetClientRect (hwnd, &rect) ; rect.left - rect.right / 4 ; rect.right = 3 * rect.right / 4 ; rect.top - rect.bottom / 4 ; rect.bottom = 3 * rect.bottom / 4 ; hemf = GetEnhMetaFile (TEXT ("..\\emf3\\emf3.emf")) ; EnumEnhMetaFile (hdc, hemf, EnhMetaFileProc, NULL, &rect) ; DeleteEnhMetaFile (hemf) ; EndPaint (hwnd, &ps) ; return 0 ; case WM-DESTROY: PostOuitMessage (0) ; return 0 ; ) return DefWindowProc (hwnd, message, wParam, lParam) ; Rysunek 1&12. Program EMF6 Podobnie jak EMF5, program EMF6 korzysta z pliku EMF3.EMF, więc przed uru- chomieniem EMF6 uruchom EMF3. Pamiętaj również o tym, że wszystkie pro- gramy należy uruchamiać w œrodowisku Visual C++. Gdybyœ chciał zmodyfikować rekordy metapliku przed ich uruchomieniem, musisz je najpierw skopiować. Jak widać na rysunku 18-12, procedura wyliczajš- ca rozpoczyna się od użycia funkcji malloc, która rezerwuje obszar pamięci o roz- miarach rekordu metapliku okreœlonego wartoœciš pola nSize struktury pEmffte- cord. WskaŸnik do tego obszaru pamięci jest zapamiętywany w zmiennej pEmfr. Innymi słowy, jest to wskaŸnik do struktury typu ENHMETARECORD. Korzystajšc z funkcji CopyMemory, program kopiuje zawartoœć struktury wska- zywanej przez pEmfRecord do struktury wskazywanej przez pEmfr. Teraz mamy obiekt, który można poddawać modyfikacjom. Program sprawdza, czy typem Rozdział 18: Metapliki 1007 i rekordu jest EMR RECTANGLE, a jeœli tak, zamienia typ rekordu, przypisujšc zmiennej iType wartoœć stałej EMR_ELLIPSE. WskaŸ,nik pEmfr jest przekazywa- ny do funkcji PlayEnhMetaFileRecord, a następnie zwalruany. W wyniku przepro- wadzonych zmian program rysuje elipsę zamiast prostokšta. Wszystkie pozosta- łe elementy nie ulegajš modyfikacjom. Nasza mała zmiana przeprowadzona została bezboleœnie, ponieważ obie funkcje - Rectangle i Ellipse - przyjmujš takie same argumenty, definiujšce tę samš wła- snoœć: ramkę opisujšca danš figurę. Zastosowanie dalej idšcych zmian wymaga- łoby wiedzy o formatach poszczególnych rekordów w metapliku. Inna możliwoœć to wstawienie dodatkowego rekordu lub dwóch, na przykład zmodyfikuj frazę if w pliku EMF6.C następujšco: if (pEmfr->iType = EMRţRECTANGLE) f i PlayEnhMetaFileRecord (hdc, pHandleTable, pEmfr, nObjects) ; PEmfr->iType = EMRţELLIPSE ; Po każdym pojawieniu się rekordu Rectangle program najpierw go rysuje, a następnie zamienia na Ellipse i rysuje ponownie. W rezultacie otrzymujemy zarówno prosto- kšt, jak i elipsę na jednym rysunku. Zbadajmy teraz, jak traktowane sš obiekty GDI w czasie wyliczania metapliku. W nagłówku metapliku znajduje się pole o nazwie nHandles, należšce do struktury ENHMETAHEADER. Przyjmuje ono wartoœć o je- den większš niż liczba utworzonych w metapliku obiektów GDI. Dlatego w pro- gramach EMF5 i EMF6 zmienna ta ma wartoœć 3, czyli pisak, pędzel i "coœ jesz- cze . Znaczenie tego trzeciego obiektu GDI objaœnię w skrócie poniżej. Zauważ, że przedostatni argument funkcji wyliczajšcych w programach EMF5 i EMF6 nosi również nazwę nHandles. Przyjmuje on tę samš wartoœć, czyli 3. Drugim argumentem funkcji wyliczajšcej jest wskaŸnik do struktury o nazwie HANDLETABLE, zdefiniowany w pliku nagłówkowym WINGDI.H następujš- co: typedef struct tagHANDLETABLE ( HGDIOBJ objectHandle C1] ; 1 HANDLETABLE : Typ HGDIOBJ jest uogólruonym uchwytem obiektu GDI, zdefiniowanym w po- staci 32-bitowego wskaŸnika, podobnie jak inne obiekty GDI. Jak się przekonasz, jest to jedna ze struktur zawierajšca pole typu tablicowego o jednym elemencie. W rzeczywistoœci oznacza to, że dane pole ma zmiennš długoœć. Liczba elemen- tów w tablicy objectHandle jest równa wartoœci nHandles, która w naszym wypad- ku wynosi 3. W funkcji wyliczajšcej uchwyty te można otrzymać, stosujšc wyrażenie: pHandleTable->objectHandleCi] gdzie i wynosi 0, 1 lub 2 dla trzech uchwytów. Przy każdym wywołaruu funkcji wyliczajšcej pierwszy element tablicy przybie- ra wartoœć numeru uchwytu metapliku, który ma być wyliczany. To jest właœnie to "coœ jeszcze", o którym wczeœniej wspomniałem. T 1008 Częœć II: Grafika Kiedy funkcja wyliczajšca jest wywoływana po raz pierwszy, drugi i trzeci, ele- menty tablicy uchwytów przyjmujš wartoœć 0. Te miejsca sš zarezerwowane na identyfikatory uchwytów pisaka i pędzla, które zostanš wkrótce utworzone. Oto jak działa cały mechanizm: pierwszej funkcji tworzšcej obiekt w metapliku przyporzšdkowany zostaje rekord typu EMR CREATEBRUSHINDIRECT. Jest on powišzany z obiektem o numerze 1. Kiedy rekord przekazywany jest do funkcji PlayEnhMetaFileRecord, GDI tworzy pędzel i uzyskuje do niego uchwyt, który jest przechowywany w tablicy objectHandle jako element (drugi) o numerze 1. Kiedy pierwszy rekord typu EMRţSELECTOBJECT przekazywany jest do funkcji Play- EnhMetaFileRecord, GDI odnotowuje, że uchwyt znajduje się w tablicy pod nu- merem 1 i dopiero wtedy może uzyskać rzeczywisty identyfikator uchwytu, a na- stępnie wykorzystać go jako argument funkcji SelectObject. Jeœli w tym czasie obiekt pędzla zostanie usunięty z metapliku, GDI ustali wartoœć elementu tablicy objec- tHandle ponownie jako 0. Majšc dostęp do tablicy objectHandle, możesz uzyskać informację o obiektach metapliku za pomocš funkcji GetObjectType i GetObject. Osadzanie obrazów Prawdopodobnie najważruejszym zastosowaniem wyliczania metapliku jest moż- liwoœć osadzania innych obrazów (lub nawet całych metaplików) w metapliku istniejšcym. Wbrew pozorom osadzenie obrazu nie zmienia istniejšcego metapli- ku; powstaje nowy metaplik, w którym łšczš się obrazy osadzane i elementy istruejšcego metapliku. Podstawowym trikiem jest umiejętne przekazanie uchwytu kontekstu urzšdzenia w postaci pierwszego argumentu funkcji EnumEnhMetaFi- le. Pozwala to na wywołanie rekordów metapliku i funkcji GDI w tym samym kontekœcie urzšdzenia zwišzanym z metaplikiem. Najłatwiej jest osadzić nowe obrazy na poczštku i na końcu sekwencji poleceń istruejšcego metapliku - czyli tuż za rekordem typix EMRţHEADER lub tuż przed rekordem typu EMF EOF. Jednak jeœli dokładrue znasz budowę metapliku, mo- żesz osadzić nowe obrazy w dowolnie wybranym miejscu. Tak właœnie stało się w programie EMF7, przedstawionym na rysunku 18-13. EMF7 /* EMF7.C - Metaplik rozszerzony Demo 117 (c) Charles Petzold, 1998 */ i/include LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR lpszCmdLine, int iCmdShow) static TCHAR szAppNameC] = TEXT ("EMF7") ; HWND hwnd : , Rozdział 18: Metapllkl 1009 MSG msg ; WNDCLASS wndclass ; i wndclass.style = CS_HREDRAW ţ CS VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; i; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = GetStockObject (WHITE-BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; ;. ,. if (!RegisterClass (&wndclass)) ( MessageBox (NULL, TEXT ("This program requi.res Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; I : hwnd = CreateWindow (szAppName, TEXT ("Enhanced Metafile Demo Iţ7"), _ WS OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, i CW_USEDEFAULT, CW-USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; ; return msg.wParam ; int CALLBACK EnhMetaFileProc (HDC hdc, HANDLETABLE * pHandleTable, CONST ENHMETARECORD * pEmfRecord, int iHandles, LPARAM p0ata) ( HBRUSH hBrush ; HPEN hPen ; LOGBRUSH lb ; if (pEmfRecord->iType != EMR HEADER && pEmfRecord->iType != EMR EOF) PlayEnhMetaFileRecord (hdc, pHandleTable, pEmfRecord, iHandles) ; if (pEmfRecord->iType == EMR-RECTANGLE) hBrush = SelectObject (hdc, GetStockObject (NULL BRUSH)) ; - lb.lbStyle = BS_SOLID ; lb.lbColor = RGB (0, 255. 0) ; lb.lbHatch = 0 ; T 1010 Częœć II: Grafika (cišg dalszy ze strony 1009) hPen = SelectObject (hdc, ExtCreatePen (PS-SOLID ţ PS GEOMETRIC, 5, &lb, 0, NULL)) ; Ellipse (hdc, 100, 100, 200, 200) ; Delete0bject (SelectObject (hdc, hPen)) ; , SelectObject (hdc, hBrush) ; ) return TRUE ; 1 LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ( ENHMETAHEADER emh ; HDC hdc, hdcEMF ; I HENHMETAFILE hemfOld, hemf ; PAINTSTRUCT ps ; RECT rect ; switch (message) ( case WM CREATE: i // Odczytaj istniejdcy metaplik i naglówek hemfOld = GetEnhMetaFile (TEXT ("..\\emf3\\emf3.emf")) ; GetEnhMetaFileHeader (hemfOld, sizeof (ENHMETAHEADER), &emh) ; // Utwórz nowy kontekst urzšdzenia metapliku hdcEMF = CreateEnhMetaFile (NULL, TEXT ("emf7.emf"), NULL, ! TEXT ("EMF7\OEMF Demo it7\0")) ; // Wylicz istniejšcy metaplik EnumEnhMetaFile (hdcEMF, hemfOld, EnhMetaFileProc, NULL, (RECT *) & emh.rclBounds) ; // Posprzdtaj hemf = CloseEnhMetaFile (hdcEMF) ; DeleteEnhMetaFile (hemfOld) ; DeleteEnhMetaFile (hemf) ; return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; GetClientRect (hwnd, &rect) ; ; rect.left - rect.right / 4 ; rect.right = 3 * rect.right / 4 ; rect.top - rect.bottom / 4 ; rect.bottom = 3 * rect.bottom / 4 ; l Rozdział 18: Metapliki 1011 hemf = GetEnhMetaFile (TEXT ("emf7.emf")) ; PlayEnhMetaFile (hdc, hemf, &rect) ; DeleteEnhMetaFile (hemf) ; EndPaint (hwnd, &ps) ; return 0 ; ů case WM DESTROY: PostOuitMessage (0) ; return 0 ; ) return DefWindowProc'(hwnd, message, wParam, lParam) ; Rysunek 18-13. Program EMF7 EMF7 korzysta z pliku EMF3.EMF, utworzonego przez program EMF3. Zanim uruchomisz EMF7, upewnij się, czy plik EMF3.EMF już istnieje. Chociaż w trakcie obsługi komunikatu 4VM-PAINT w programie EMF7 powró- ciliœmy do funkcji PIayEnhMetaFile, rezygnujšc z EnumEnhMetaFile, obsługa WMţCREATE wyglšda nieco inaczej. Po pierwsze, program otrzymuje uchwyt metapliku EMF3.EMF przez wywołanie Get- EnhMetaFile, a nagłówek - metapliku rozszerzonego dzięki funkcji GetEnhMetaFile- Header. Nagłówek jest uzyskiwany w celu wykorzystania pola rclBounds jako argu- mentu funkcji EnumEnhMetaFile. Następnie program tworzy na dysku nowy metaplik o nazwie EMF7.EME Funk- cja CreateEnhMetaFile zwraca uchwyt kontekstu urzšdzenia metapliku. Funkcja EnumEnhMetaFile wykorzystuje uchwyty kontekstu urzšdzenia metaplików EMF7.EMF oraz EMF3.EMF. Teraz przyjrzyjmy się funkcji EnhMetaFileProc. Jeœli wyliczany rekord nie jest na- główkowy ani końcowy, funkcja wywołuje PIayEnhMetaFileRecord w celu prze- kazania tego rekordu do kontekstu urzšdzerua zwišzanego z nowym metapli- kiem (wyłšczanie rekordów nagłówkowego i końca pliku nie jest konieczne, lecz powiększajš one rozmiary metapliku). Jeœli przesłany rekord jest zwišzany z wywołaniem Rectangle, funkcja tworzy pi- sak, aby narysować ziehnš elipsę o przezroczystym wnętrzu. Zwróć uwagę, jak program przywraca stan kontekstu urzšdzenia przez zapisanie poprzednich wartoœci uchwytów pisaka i pędzla. W tym momencie wszystkie dodatkowe funk- cje sš wstawiane do metapliku (możesz rówrueż użyć funkcji PlayEnhMetaFile, aby wstawić metaplik do już istniejšcego). Wracajšc do obsługi komunikatu WM-CREATE, program wywołuje funkcję Clo- seEnhMetaFile w celu otrzymania uchwytu nowego metapliku. Następnie usuwa uchwyty obu metaplików, pozostawiajšc na dysku pliki EMF3.EMF oraz EMF7.EME Z punktu widzenia programu jest oczywiste, że najpierw rysowany jest prosto- kšt, następnie elipsa, a na końcu przekštne. 1012 Częœć II: Grafika Podglšd metapliku rozszerzonego i wydruki Zastosowanie Schowka do przesyłana metaplików jest dosyć łatwe. Typ Schow- ka to CF ENHMETAFILE. Funkcja GetClipboardData zwraca uchwyt metapliku rozszerzonego; funkcja SetClipboardData również korzysta z uchwytu metapliku. Jeœli potrzebujesz kop metapliku, zastosuj funkcję CopyEnhMetaFile. Gdy umie- œcisz rozszerzony metaplik w Schowku, Windows udostępni metaplik w starym formacie programom, które go zażšdajš. Jeœli umieœcisz tam metaplik w starym formacie, Windows zatroszczy się o to, aby był on dostępny w formacie rozsze- rzonym. Program EMFVIEW, przedstawiony na rysunku 18-14, demonstruje, jaki przesy- łać metapliki do i ze Schowka. Pozwala również na wczytywanie, zapisywanie i drukowanie metaplików. EMFUIEW.C /* EMFVIEW.C - Podgldd metaplików rozszerzonych (c) Charles Petzold, 1998 */ ilinclude ilinclude ilinclude "resource.h" LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; TCHAR szAppName[7 = TEXT ("EmfView") ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) HACCEL hAccel ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CS VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITEţBRUSH) wndclass.lpszMenuName = szAppName ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) ( MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MBţICONERROR) ; return 0 ; Rozdział 18: Metapliki 1013 hwnd = CreateWindow (szAppName, TEXT ("Enhanced Metafile Viewer"), ,.. WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, i _ i; CWţUSEDEFAULT, CW USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; '. . UpdateWindow (hwnd) ; hAccel = LoadAccelerators (hInstance, szAppNameO ; while (GetMessage (&msg, NULL, 0, 0)) ( if (!TranslateAccelerator (hwnd, hAccel, &msg)) ( TranslateMessage (&msg) ; DispatchMessage (&msg) ; ) return msg.wParam ; ) HPALETTE CreatePaletteFromMetaFile (HENHMETAFILE hemf) ( HPALETTE hPalette ; int iNum ; LOGPALETTE * plp ; if (!hemf) return NULL ; if (0 = (iNum = GetEnhMetaFilePaletteEntries (hemf, 0, NULL))) return NULL ; ! ; plp = malloc (sizeof (LOGPALETTE) + (iNum - 1) * sizeof (PALETTEENTRY)) ; ; plp->palVersion = 0x0300 ; plp->palNumEntries = iNum ; GetEnhMetaFilePaletteEntries (hemf, iNum, plp->palPalEntry) ; hPalette = CreatePalette (plp) ; free (plp) ; return hPalette ; ) LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ( static DOCINFO di = ( sizeof (DOCINFO), TEXT ("EmfView: Printing") ) ; static HENHMETAFILE hemf ; static OPENFILENAME ofn ; static PRINTDLG printdlg = ( sizeof (PRINTDLG) static TCHAR szFileName [MAXţPATH], szTitleName [MAX PATH] ; static TCHAR szFilter[] = TEXT ("Enhanced Metafiles (*.EMF)\0*.emf\0") 1014 Częœć II: Grafika (cišg dalszy ze strony 1013) TEXT ("All Files (*.*)\0*.*\0\0") ; BOOL bSuccess ; ENHMETAHEADER header ; HDC hdc, hdcPrn ; HENHMETAFILE hemfCopy ; HMENU hMenu ; ' HPALETTE hPalette ; int i, iLength, iEnable ; PAINTSTRUCT ps ; RECT rect ; PTSTR pBuffer ; switch (message) case WM CREATE: // Inicjuj pola struktury OPENFILENAME ofn.lStructSize = sizeof (OPENFILENAME) ; ofn.hwndOwner = hwnd ; ofn.hInstance = NULL ; ofn.lpstrFilter = szFilter ; ofn.lpstrCustomFilter ţ NULL ; ofn.nMaxCustFilter = 0 ; ' ofn.nFilterIndex = 0 ; ofn.lpstrFile = szFileName ; ofn.nMaxFile = MAX PATH ; ofn.lpstrFileTitle = szTitleName ; ofn.nMaxFileTitle = MAXţPATH ; ofn.lpstrInitialDir = NULL ; ofn.lpstrTitle = NULL ; ofn.Flags = 0 ; ofn.nFileOffset = 0 ; ofn.nFileExtension = 0 ; ofn.lpstrDefExt = TEXT ("emf") ; ' ofn.lCustOata = 0 ; ofn.lpfnHook = NULL ; ofn.lpTemplateName = NULL ; return 0 ; case WMţINITMENUPOPUP: hMenu = GetMenu (hwnd) ; iEnable = hemf ? MFţENABLED : MF GRAYED ; EnableMenuItem (hMenu, IDM_FILE_SAVE_AS, iEnable) ; EnableMenuItem (hMenu, IDM_FILE_PRINT, iEnable) ; EnableMenuItem (hMenu, IDM_FILE_PROPERTIES, iEnable) ; EnableMenuItem (hMenu, IDM_EDIT_CUT, iEnable) ; EnableMenuItem (hMenu, IDM_EDIT COPY, iEnable) ; EnableMenuItem (hMenu, IDMţEDIT DELETE, iEnable) ; EnableMenuItem (hMenu, IDM_EDIT_PASTE, IsClipboardFormatAvailable (CFţENHMETAFILE) ? MF_ENABLED : MF GRAYED) ; return 0 ; case WM COMMAND: Rozdział 18: INetapliki 1015 switch (LOWORD (wParam)) case IDM_FILE_OPEN: // Pokaż okno dialogowe Open File ofn.Flags = 0 ; if (!GetOpenFileName (&ofn)) return 0 ; // Jeœli istnieje plik EMF, pozbšdŸ się go 7 . if (hemf) _ t DeleteEnhMetaFile (hemf) ; , hemf = NULL ; ) // Wczytaj EMF do pamięci SetCursor (LoadCursor (NULL, IDC_WAIT)) ; ShowCursor (TRUE) ; hemf = GetEnhMetaFile (szFileName) ; ShowCursor (FALSE) ; SetCursor (LoadCursor (NULL, IDC ARROW)) ; // Unieważnij obszar roboczy InvalidateRect (hwnd, NULL, TRUE) ; if (hemf = NULL) MessageBox (hwnd, TEXT ("Cannot load metafile"), szAppName, MB ICONEXCLAMATION ţ MB OK) ; return 0 ; case IDM_FILE_SAVĘ AS: if (!hemf) return 0 ; // Pokaż okno dialogowe File Save ofn.Flags = OFN OVERWRITEPROMPT ; if (!GetSaveFileName (&ofn)) return 0 ; // Zapisz plik EMF na dysku SetCursor (LoadCursor (NULL, IDC WAIT)) ; ShowCursor (TRUE) ; hemfCopy = CopyEnhMetaFile (hemf, szFileName) ; ShowCursor (FALSE) ; SetCursor (LoadCursor (NULL, IDC ARROW)) 1016 Czţœć II: Grafika (cišg dalszy ze strony 2025) if (hemfCopy) ( DeleteEnhMetaFile (hemf) ; hemf = hemfCopy : ) else ' MessageBox (hwnd, TEXT ("Cannot save metafile"), szAppName, MBţICONEXCLAMATION ţ MB OK) ; return 0 ; case IDM_FILEţPRINT: // Pokaż okno dialogowe Print i pobierz kontekst urzšdzenia zwidzany z drukarkţ printdl9.Flags = PDţRETURNDC ţ PD NOPAGENUMS ţ PD NOSELECTION ; if (!Print0lg (&printdlg)) return 0 ; ! if (NULL = (hdcPrn = printdlg.hDC)) ( MessageBox (hwnd, TEXT ("Cannot obtain printer DC"), szAppName, MBţICONEXCLAMATION ţ MB OK) ; return 0 ; ) // Pobierz rozmiar obszaru wydruku rect.left = 0 ; rect.right = GetDeviceGaps (hdcPrn, HORZRES) ; rect.top = 0 ; rect.bottom = GetDeviceCaps (hdcPrn, ţERTRES) ; bSuccess = FALSE ; // Odtwórz metaplik na drukarce SetCursor (LoadCursor (NULL, IDC WAIT)) ; ShowCursor (TRUE> ; if ((StartDoc (hdcPrn, &di) > 0) && (StartPage (hdcPrn) > 0)) PlayEnhMetaFile (hdcPrn, hemf, &rect) ; if (EndPage (hdcPrn) > 0) ( bSuccess = TRUE ; EndDoc (hdcPrn) ; ) , ShowCursor (FALSE) ; SetCursor (LoadCursor (NULL, IDC ŽRROW)) ; DeleteDC (hdcPrn) ; if (!bSuccess) MessageBox (hwnd. TEXT ("Could not print metafile"), szAppName, MBţICONEXCLAMATION ţ MBţOK) ; return 0 ; Rozdział 18: Metapliki 1017 case IDM FILE_PROPERTIES: if (!hemf) return 0 ; iLength = GetEnhMetaFileDescription (hemf, 0, NULL) ; ' pBuffer = malloc ((iLength + 256) * sizeof (TCHAR)) ; ' i ' 3 7 '; ' GetEnhMetaFileNeader (hemf, sizeof (ENHMETAHEADER), &header) ; // Sformatuj informację naglówka pliku i = wsprintf (pBuffer, ,": i. : TEXT ("Bounds = (%i, %i) to (%i, %i) pixels\n"), ł ' header.rclBounds.left, header.rclBounds.top, header.rclBounds.right, header.rclBounds.bottom) ; i += wsprintf (pBuffer + i, TEXT ("Frame = (%i, %i) to (%i, %i) mms\n"), header.rclFrame.left, header.rclFrame.top, header.rclFrame.right, header.rclFrame.bottom) ; i += wsprintf (pBuffer + i, TEXT ("Resolution = (%i, %i) pixels") TEXT (" = (%i, %i) mms\n"), header.szlDevice.cx, header.szlDevice.cy, header.szlMillimeters.cx, header.szlMillimeters.cy) ; i += wsprintf (pBuffer + i, TEXT ("Size = %i, Records = %i, ") TEXT ("Handles = %i, Palette entries = %i\n"), header.nBytes. header.nRecords, ' ' 14 '. header.nHandles, header.nPalEntries) ; // Doldcz opis metapliku if (iLength) i += wsprintf (pBuffer + i, TEXT ("Description = ")) ; j GetEnhMetaFileDescription (hemf, iLength, pBuffer + i) ; Buffer [lstrlen (pBuffer)] = '\t' , P MessageBox (hwnd, pBuffer, TEXT ("Metafile Properties"), MBţOK) ; free (pBuffer) ; return 0 ; case IDM_EDIT_COPY: ' case IDM_EDIT_CUT: 1 if (!hemf) return 0 ; ! // Przeœlij kopie metapliku do Schowka hemfCopy = CopyEnhMetaFile (hemf, NULL) ; OpenClipboard (hwnd) ; EmptyClipboard () ; SetClipboardData (CF ENHMETAFILE, hemfCopy) ; I'ţ11 1018 Częœć II: Grafika (cišg dalszy ze strony 1017) CloseClipboard () ; if (LOWORD (wParam) = IDM EDIT COPY) return 0 ; // jeżeli IDMţEDITţCOPY, ten przypadek pozostawiamy nieopracowany case IDM_EDIT_DELETE: if (hemf) i DeleteEnhMetaFile (hemf) ; hemf = NULL ; InvalidateRect (hwnd, NULL, TRUE) ; return 0 ; case IDM_EDIT_PASTE: , OpenClipboard (hwnd) ; hemfCopy = GetClipboard0ata (CFţENHMETAFILE) ; CloseClipboard () ; if (hemfCopy && hemf) f DeleteEnhMetaFile (hemf) ; hemf = NULL ; J hemf = CopyEnhMetaFile (hemfCopy, NULL) ; InvalidateRect (hwnd, NULL, TRUE) ; return 0 ; case IDM APP_ABOUT: MessageBox (hwnd, TEXT ("Enhanced Metafile Viewer\n") TEXT ("(c) Charles Petzold, 1998"), szAppName, MB OK) ; return 0 ; case IDM_APP EXIT: SendMessage (hwnd, WM CLOSE, 0, OL) ; return 0 ; 1 break ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; if (hemf) if (hPalette = CreatePaletteFromMetaFile (hemf)) ( SelectPalette (hdc, hPalette, FALSE) ; , RealizePalette (hdc) ; ) GetClientRect (hwnd, &rect) ; PlayEnhMetaFile (hdc, hemf, &rect) ; Rozdział 18: Metapliki 1019 if (hPalette) Delete0bject (hPalette) ; EndPaint (hwnd, &ps) ; return 0 ; case WM OUERYNEWPALETTE: if (!hemf ţţ !(hPalette = CreatePaletteFromMetaFile (hemf))) return FALSE ; hdc = GetDC (hwnd) ; SelectPalette (hdc, hPalette, FALSE) ; RealizePalette (hdc) ; InvalidateRect (hwnd, NULL, FALSE) ; Delete0bject (hPalette) ; ReleaseDC (hwnd, hdc) ; return TRUE ; case WM_PALETTECHANGED: if ((HWND) wParam = hwnd) break ; if (!hemf ţţ !(hPalette = CreatePaletteFromMetaFile (hemf))) ' break ; hdc = GetDC (hwnd) ; SelectPalette (hdc, hPalette, FALSE) ; RealizePalette (hdc) ; UpdateColors (hdc) ; Delete0bject (hPalette) ; , ReleaseDC (hwnd, hdc) ; break ; case WM_DESTROY: if (hemf) DeleteEnhMetaFile (hemf) ; PostOuitMessage (0) ; return 0 ; return DefWindowProc (hwnd, message, wParam, lParam) ; EMFvIEw.Rc (fragmenty) //Microsoft Developer Studio generated resource script. ţţinclude "resource.h" ţfinclude "afxres.h" /////////////////////////////////////////////////////////////////////////// // Menu EMFVIEW MENU DISCARDABLE 1 1 1020 Częœć II: Grafika (cišg dalszy ze strony 1019) BEGIN POPUP "&File" BEGIN MENUITEM "&Open\tCtrl+0", IDM_FILE_OPEN I MENUITEM "Save &As... , IDMţFILE SAVĘ AS MENUITEM SEPARATOR " MENUITEM "&Print...\tCtrl+p", IDMţFILEţPRINT MENUITEM SEPARATOR ' MENUITEM "&Properties", IDMţFILEţPROPERTIES j MENUITEM SEPARATOR MENUITEM "E&xit", IDM APPţEXIT END POPUP "&Edit" BEGIN MENUITEM "Cu&t\tCtrl+X", IDM_EDIT_CUT MENUITEM "&Copy\tCtrl+C", IDM_EDIT_COPY MENUITEM "&Paste\tCtrl+V", IDM_EDIT_PASTE MENUITEM "&Delete\tDel", IDMţEDIT DELETE END POPUP "Help" BEGIN MENUITEM "&About EmfView... , IDM APP ABOUT END END /////////////////////////////////////////////////////////////////////////// // Accelerator EMFVIEW ACCELERATORS DISCARDABLE BEGIN ^C~, IDM_EDIT_COPY, VIRTKEY, CONTROL, NOINVERT ~0^, IDM_FILE_OPEN, VIRTKEY, CONTROL, NOINVERT "p", IDM_FILE_PRINT, VIRTKEY, CONTROL, NOINVERT "V~, IDM_EDIT_PASTE, VIRTKEY, CONTROL, NOINVERT UK DELETE, IDM_EDIT_DELETE, VIRTKEY, NOINVERT "Xř, IDM EDIT CUT, VIRTKEY, CONTROL, NOINVERT END RESOURCE.H (fragmenty) // Microsoft Developer Studio generated include file. // Used by EmfView.rc ţtdefine IDM_FILE_OPEN 40001 ţtdefine IDM_FILE_SAVE_AS '40002 ţIdefine IDM_FILE_PRINT 40003 ţpdefine IDM_FILE_PROPERTIES 40004 ţIdefine IDM_APP_EXIT 40005 ţIdefine IDM_EDIT_CUT 40006 ţtdefine IDM_EDIT_COPY 40007 ţţdefine IDM_EDIT_PASTE 40008 tldefine IDM_EDIT_DELETE 40009 ţpdefine IDM ŽPP ABOUT 40010 Rysunek 18-14. Program EMFVIEW Rozdział 18: Metapliki 1021 Program EMFVIEW zawiera również pełnš obsługę palety, na wypadek koniecz- noœci zastosowania palety w metapliku (obsługa została włšczona przez wywo- łanie funkcji SelectPalette.) Program uzyskuje paletę za pomocš funkcji CreatePa- letteFromMetaFile, wywoływanej w trakcie obsługi komunikatów WM PAINT, WMţQUERYNEWPALETTE i WMţCHANGEPALETTE. W odpowiedzi na polecenie Print EMFVIEW wyœwietla odpowiednie okno dia- logowe, a następnie uzyskuje rozmiary obszaru wydruku. Metaplik jest rozciš- gnięty do rozmiarów tego obszaru i podobrue wyœwietlany w oknie programu. Polecenie Properties z menu File wyœwietla okno komurukatu, zawierajšce infor- macje z nagłówka metapliku. Jeœli wydrukujesz metaplik EMF2.EMF, może się okazać, że na drukarce o wyso- kiej rozdzielczoœci linie obrazu sš bardzo cienkie, a nawet ledwie widoczne. Pro- blem tkwi w tym, że obrazy wektorowe powinny być rysowane dla celów wy- druku zdecydowanie grubszš kreskš (na przykład 1-punktowš). W dalszej czę- œci rozdziału zobaczysz linijkę narysowanš przy zachowaniu tego zalecenia. Wyœwietlanie szczegółów w metaplikach Wielkš zaletš obrazów przechowywanych w metaplikach jest możliwoœć ich ska- lowania do dowolnych rozmiarów przy zachowaniu wiemoœci oryginału. Jest tak, ponieważ metaplik składa się z wielu podstawowych elementów grafiki wekto- rowej, jak linie, wypełnione obszary czy czcionki. Powiększanie lub zmniejsza- rue obrazu polega na skalowaniu współrzędnych punktów definiujšcych te ele- menty. Inaczej odbywa się to w wypadku bitmap - zmniejszanie może powodo- wać utratę całych wierszy i kolumn pikseli. Oczywiœcie, zmniejszanie obrazów wektorowych ma również swoje wady. Na co dzień używamy graficznych urzšdzeń wyjœciowych o skończenie małych rozmia- rach piksela. Obraz metapliku składajšcy się z wielu lin po odpowiednim zxnniej- szeruu zaczšłby wyglšdać jak rozmazana plama. Podobne problemy napotkamy, jeœli metaplik zawiera osadzone bitmapy czy czcionki rastrowe. W większoœci wypadków jednak metapliki można uważać za w pełni skalowal- ne. Cecha ta jest szczególnie użyteczna, kiedy wklejamy rysunek do dokumentu procesora tekstu lub programu DTP. Zwykle po zaznaczeniu rysunku w jednej z takich aplikacji zobaczysz otaczajšcy go czarny prostokšt, którego rozmiary moż- na dowolnie zmieniać. Zmienione rozmiary obrazu zostanš, oczywiœcie, zacho- wane po wydrukowaniu na drukarce. Sš jednak sytuacje, w których skalowanie obrazu nie jest korzystne. WyobraŸ sobie system bankowy przechowujšcy oryginalne podpisy klientów w postaci obrazów w metaplikach. Zmiana rozmiarów takiego obrazu zniekształciłaby podpis, więc majšc do czynienia z takimi obrazami należy zachować chociaż proporcje. W przedstawionych dotychczas przykładowych programach prostokšt zwišza- ny z funkcjš PIayEnhMetaFile oparty był na rozmiarach obszaru roboczego, dlate- go zmiana rozmiarów okna programu pocišgała za sobš odpowiednie przeska- lowanie obrazu. Koncepcyjnie funkcjonuje to w podobny sposób, jak mechanizm skalowania metapliku osadzonego w dokumencie procesora tekstu. 1022 Czgœć II, Grafika Dokładne wyœwietlenie obrazu metapliku - przy zachowaniu odpowiednich pro- porcji bšdŸ wystarczajšco dużych rozmiarów - wymaga wykorzystara informa- r cji znajdujšcej się w nagłówku metapliku i odpowiedniego ustalenia wartoœci struktury opisujšcej prostokšt. Przykładowe programy zaprezentowane w pozo- stałej częœci tego rozdziału wykorzystujš program o nazwie EMF.C, zawierajšcy obshzgę drukarki, skrypt zasobów o nazwie EMF.RC oraz plik nagłówkowy RE- SOURCE.H. Rysunek 18-15 przedstawia te pliki wraz z modułem EMF8.C, który wyœwietla na ekrarue 6-calowš linijkę. EMF8.C /* EMF8.C - Metaplik rozszerzony Demo ţţ8 (c) Charles Petzold, 1998 */ lţinclude TCHAR szClass [] = TEXT ("EMF8") ; TCHAR szTitle [] = TEXT ("EMF8: Enhanced Metafile Demo ţţ8") ; void DrawRuler (HDC hdc, int cx, int cy) int iAdj, i, iHeight ; LOGFONT lf ; - TCHAR ch ; iAdj = GetVersion () & 0x80000000 ? 0 : 1 ; // Czarny pisak o gruboœci 1 punktu SelectObject (hdc, CreatePen (PS SOLID, cx / 72 / 6, 0)) ; // Prostokšt otaczajšcy caly pisak Rectangle (hdc, iAdj, iAdj, cx + iAdj + 1, cy + iAdj + 1) ; // Skala linijki for ( i = 1 ; i < 96 ; i++) f if (i % 16 = 0) iHeight = cy / 2 ; // cale else if (i % 8 == 0) iHeight = cy / 3 ; // pół cale else if (i % 4 == 0) iHeight = cy / 5 ; // ćwierć cale else if (i % 2 ţ 0) iHeight = cy / 8 ; // ósme częœci cala else iHeight = cy / 12 ; // szesnaste częœci cala MoveToEx (hdc, i * cx / 96, cy, NULL) ; % LineTo (hdc, i * cx / 96, cy - iHeight) ; } // Utwórz czcionkę logicznš FillMemory (&lf, sizeof (lf), 0) ; lf.lfHeight = cy / 2 ; lstrcpy (lf.lfFaceName, TEXT ("Times New Roman")) ; Rozdział 18: Metapliki 1023 SelectObject (hdc. CreateFontIndirect (&lf)) ; SetTextAlign (hdc, TAţBOTTOM ţ TŽ CENTER) ; SetBkMode (hdc, TRANSPARENT) ; // Wyœwietl liczby podziałki for (i = 1 ; i <= 5 ; i++) . ch = (TCHAR) (i + '0') ; TextOut (hdc, i * cx / 6, cy / 2. &ch. 1) ; // Posprzataj Delete0bject (SelectObject (hdc, GetStockObject (SYSTEM_FONT)>) ; Delete0bject (SelectObject (hdc, GetStockObject (BLACKţPEN))) ; void CreateRoutine (HWND hwnd) ' HDC hdc.EMF ; HENHMETAFILE hemf ; int cxMms, cyMms, cxPix, cyPix, xDpi, yDpi ; hdcEMF = CreateEnhMetaFile (NULL, TEXT ("emf8.emf"), NULL, TEXT ("EMF8\OEMF Demo ţţ8\0")) ; if (hdcEMF == NULL) return ; cxMms = GetDeviceCaps (hdcEMF, HORZSIZE) ; cyMms = GetOeviceCaps (hdcEMF, VERTSIZE) ; cxPix = GetOeviceCaps (hdcEMF, HORZRES) ; cyPix = GetDeviceCaps (hdcEMF, VERTRES) ; t xOpi = cxPix * 254 / cxMms / 10 ; yDpi = cyPix * 254 / cyMms / 10 ; ' DrawRuler (hdcEMF, 6 * xDpi, yDpi) ; hemf = CloseEnhMetaFile (hdcEMF) ; ' ' 1 ! .., DeleteEnhMetaFile (hemf) ; void PaintRoutine (HWND hwnd, HDC hdc, int cxArea, int cyArea) 3 ;:. r. ENHMETAHEADER emh ; HENHMETAFILE hemf ; ` int cxImage, cyImage ; RECT rect ; hemf = GetEnhMetaFile (TEXT ("emf8.emf")) ; ? GetEnhMetaFileHeader (hemf, sizeof (emh), &emh) ; cxImage = emh.rclBounds.right - emh.rclBounds.left ; cyImage = emh.rclBounds.bottom - emh.rclBounds.top ; rect.left = (cxArea - cxImage) / 2 ; - 1024 Częœć II: Grafika (cišg dalszy ze strony 1023) rect.right = (cxArea + cxImage) / 2 ; ' rect.top = (cyArea - cyImage) / 2 ; rect.bottom = (cyArea + cyImage) / 2 ; PlayEnhMetaFile (hdc, hemf, &rect) ; DeleteEnhMetaFile (hemf) ; EMF.C /* EMF.C - Metaplik rozszerzony - program demonstracyjny (c) Charles Petzold, 1998 */ ţIinclude ! ţlinclude ţţinclude "..\\emf8\\resource.h" extern void CreateRoutine (HWND) ; extern void PaintRoutine (HWND, HDC, int, int) ; , LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; HANDLE hInst ; extern TCHAR szClass [] ; extern TCHAR szTitle [] ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) ( TCHAR szResource [] = TEXT ("EMF") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; hInst = hInstance ; wndclass.style = CS HREDRAW ţ CSţIIREDRAW ; wndclass.lpfnWndProc = WndProe ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = GetStockObject (WHITE BRUSH) ; wndclass.lpszMenuName = szResource ; wndclass.7pszClassName = szClass ; , if (!RegisterClass (&wndclass)) ( MessageBox (NULL, TEXT ("This program requires Windows NT!"), szClass, MBţICONERROR) ; return 0 ; , Rozdział 18: Metapliki 1025 ' hwnd = CreateWindow (szClass, szTitle, WS_OUERLAPPEDWINDOW, CW_USEDEFAULT, CW USEDEFAULT, CW_USEDEFAULT, CW USEDEFAULT, NULL. NULL, hInstance, NULL) ; i ShowWindow (hwnd, iCmdShow) : UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) ; TranslateMessage (&msg) ; i DispatchMessage (&msg) : return msg.wParam ; i BOOL PrintRoutine (HWND hwnd) I static DOCINFO di ; i static PRINTDLG printdlg = ( sizeof (PRINTDLG) ) ; static TCHAR szMessage [32] ; BOOL bSuccess = FALSE ; HDC hdcPrn ; int cxPage, cyPage : printdlg.Flags = PDţRETURNDC ţ PDţNOPAGENUMS ţ PD NOSELECTION ; if (!PrintDlg (&printdlg)) return TRUE ; , if (NULL = (hdcPrn = printdlg.hDC)) return FALSE : cxPage = GetDeviceCaps (hdcPrn, HORZRES) ; cyPage = GetDeviceCaps (hdcPrn, VERTRES) ; lstrcpy (szMessage, szClass) ; lstrcat (szMessage, TEXT (": Printing")) ; i di.cbSize = sizeof (DOCINFO) ; di.lpszDocName = szMessage ; if (StartDoc (hdcPrn, &di) > 0) a ( if (StartPage (hdcPrn) > 0) ( PaintRoutine (hwnd, hdcPrn, cxPage, cyPage) : if (EndPage (hdcPrn) > 0) t EndDoc (hdcPrn) ; bSuccess = TRUE ; ;:. ) ? 1026 Częœć IIţ Grafika (cigg dalszy ze strony 1025) DeleteDC (hdcPrn) ; return bSuccess ; ) LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) t ( BOOL bSuccess ; static int cxClient, cyClient ; HDC hdc ; PAINTSTRUCT ps ; switch (message) ( r case WMţCREATE: CreateRoutine (hwnd) ; return 0 ; r case WM COMMAND: switch (wParam) ( case IDM_PRINT: SetCursor (LoadCursor (NULL, IDC_WAIT)) ; ShowCursor (TRUE) ; bSuccess = PrintRoutine (hwnd) ; ShowCursor (FALSE) ; SetCursor (LoadCursor (NULL, IDC ARROW)) ; if (!bSuccess) MessageBox (hwnd, TEXT ("Error encountered during printing"), szClass, MB ICONASTERISK ţ MB OK) ; return 0 ; case IDM_EXIT: SendMessage (hwnd, WM CLOSE, 0, 0) ; return 0 ; case IDM ABOUT: MessageBox (hwnd, TEXT ("Enhanced Metafile Demo Program\n") TEXT ("Copyright (c) Charles Petzold, 1998"), szClass, MBţICONINFORMATION ţ MB OK) ; return 0 ; ) break ; case WM_SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; Rozdział 18: INetapliki 1027 1 PaintRoutine (hwnd, hdc, cxClient, cyClient) ; ', i '. ' EndPaint (hwnd. &ps) ; return 0 ; case WMţDESTROY : PostOuitMessage (0) ; ţ, return 0 ; return DefWindowProc (hwnd, message, wParam, lParam) ; 1 EMF.RC (fragmenty) //Microsoft Developer Studio generated resource script. ttinclude "resource.h" ţ nclude "afxres.h" /////////////////////////////////////////////////////////////////////////// // Menu EMF MENU DISCARDABLE BEGIN POPUP "&File" BEGIN MENUITEM "&Print... , IDMţPRINT MENUITEM SEPARATOR MENUITEM "E&xit", IDMţEXIT END POPUP "&Help" BEGIN MENUITEM "&About... , IDM ŽBOUT END END RESOURCE.H (fragmenty) // Microsoft Developer Studio generated include file. // Used by Emf.rc ţţdefine IDM_PRINT 40001 i ţtdefine IDM EXIT 40002 ţţdefine IDM ŽBOUT 40003 Rysunek 18-15. Program EMF8 W trakcie obsługi komunikatu WMţCREATE EMF.C wywołuje zewnętrznš funkcję o nazwie CreateRoutine, która tworzy metaplik. EMEC wywołuje funkcję PaintRo- utine dwukrotnie: w trakcie obsługi komunikatu WMţPAINT i w czasie wykony- wania funkcji PrintRoutine, w odpowiedzi na polecenie drukowania obrazu. Ponieważ obecne drukarki dysponujš zwykle wyższš rozdzielczoœciš niż moru- tory komputera, zdolnoœć wydrukowania obrazu może odgrywać rolę narzędzia testujšcego możliwoœć uzyskania obrazu o okreœlonych rozmiarach. Program 1028 Czţœć II: Grafika EMF8 tworzy metaplik zawierajšcy obraz, którego wyœwietlenie ma sens jedynie przy zachowaniu œciœle okreœlonych rozmiarów. Przedstawia on linijkę o długo- œci 6 cali i szerokoœci 1 cala wraz z podziałkš co jednš szesnastš cala i liczbami od 1 do 5, zapisanymi czcionkš TrueType. Aby narysować 6-calowš linijkę, trzeba było znać rozdzielczoœć urzšdzenia. Funk- cja CreateRoutine modułu EMFB.C rozpoczyna się od utworzenia metapliku i czte- rokrotnego wywołania funkcji GetDeviceCaps, wykorzystujšcej uchwyt kontekstu urzšdzenia zwracany przez CreateEnhMetaFile. Wywołania te dajš w wyniku dłu- goœć i szerokoœć ekranu, podanš zarówno w milimetrach, jak i pikselach. Być może, brzmi to nieco dziwrue. Kontekst urzšdzenia metapliku jest zwykle postrzegany jako medium przechowujšce polecenia GDI. Nie jest to urzšdzenie rzeczywiste, takie jak ekran komputera czy drukarka, więc jak może mieć dłu- goœć i szerokoœć? Cóż, jak sobie pewnie przypominasz, pierwszy argument funkcji CreateEnhMe- taFile jest znany pod nazwš kontekstu odniesienia urzšdzenia. GDI wykorzystu- je go do ustalenia charakterystyki urzšdzenia dla celów metapliku. Jeœli argument przyjmuje wartoœć NULL (tak jak w EMF8), GDI wykorzystuje ekran monitora jako kontekst odniesienia. Dlatego kiedy EMF wywołuje funkcję GetDeviceCaps wykorzystujšc kontekst metapliku, otrzymuje w rzeczywistoœci informacje o ekra- nie komputerowym. EMF8.C oblicza rozdzielczoœć w punktach na cal, dzielšc pikselowy rozmiar ekra- nu przez rozmiar wyrażony w milimetrach, a otrzymany współczynnik mnożšc przez stałš 25,4, czyli liczbę milimetrów przypadajšcych na cal. Chociaż dokła- daliœmy wszelkich starań, aby narysować linijkę o rzeczywistych rozmiarach, to jeszcze nie koniec pracy. Kiedy następuje moment odtworzenia metapliku, funk- cja PIayEnhMetaFile wyœwietli obraz dopasowany do prostokšta o rozmiarach okre- œlonych w odpowiedniej strukturze, przekazanej jako jej ostatni argument. Pro- stokšt musi mieć oczekiwane rozmiary linijki. Z tego powodu funkcja PaintRoutine w programie EMF8 wywołuje GetEnhMeta- FileHeader w celu uzyskania informacji znajdujšcej się w nagłówku metapliku. Pole rclBounds struktury ENHMETAHEADER okreœla rozmiary prostokšta otaczajš- cego obraz, podane w pikselach. Program wykorzystuje tę informację do usta- wienia obrazu w œrodku obszaru roboczego, jak pokazano na rysunku 18-16. Rozdział 18: Metapliki 1029 Rysunek 18-16. Wynik działania programu EMFB ' Po przyłożeniu do ekranu prawdziwej linijki może się okazać, że podziałka nie jest idealnie dokładna. Ekran jedynie przybliża rzeczywiste rozmiary (pisałem o tym w rozdziale 5). Można uznać, że zastosowana technika dała zadowalajšce rezultaty. Spróbujmy teraz wydrukować obraz. Ooops! Jeœli masz drukarkę laserowš o rozdzielczoœci 300 dpi, linijka będzie miała na wydruku jeden i jednš trzeciš cala szerokoœci, ) ponieważ zastosowaliœmy wymiarowarue oparte na pikselach ekranowych. Cho- ciaż wydrukowana linijka wyglšda bardzo ładnie, na pewno nie oczekiwaliœmy takich rozxniarów. Spróbujmy jeszcze raz. , Struktura ENHMETAHEADER zawiera dwie struktury opisujšce rozmiar obra- zu. Pierwsza z nich to używane przez EMF8 pole rclBounds. Zawiera ono roz- miar obrazu w pikselach. Drugim jest pole rclFrame, zawierajšce ten sam rozmiar podany w setnych częœciach milimetra. Zwišzkiem między tymi dwoma polami zarzšdza kontekst odniesienia urzšdzenia, stosowany w trakcie tworzenia obra- zu - w tym przypadku chodzi o ekran monitora. Nagłówek metapliku zawiera również dwa pola o nazwach szlDevice i szlMilime- ters, które sš strukturami typu SIZEL, okreœlajšcymi rozmiary urzšdzenia odnie- sienia w pikselach i milimetrach; te same informacje można uzyskać po wywoła- niu funkcji GetDeviceCaps. Informacja o rozmiarach obrazu podanych w milimetrach jest wykorzystywana przez program EMF9, przedstawiony na rysunku 18-17. 1030 Częœć il, Grafika r EMF9.C ' ,* EMF9.C - Metaplik rozszerŸony Demo ţi9 (c) Charles Petzold, 1998 */ ţţinclude I ţţinclude TCHAR szClass [] = TEXT ("EMF9") ; TCHAR szTitle C] = TEXT ("EMF9: Enhanced Metafile Demo ţţ9") ; void CreateRoutine (HWND hwnd) ( ) void PaintRoutine (HWND hwnd, HDC hdc, int cxArea, int cyArea) i ENHMETAHEADER emh ; HENHMETAFILE hemf ; int cxMms, cyMms, cxPix, cyPix, cxImage, cyImage ; RECT rect ; cxMms = GetOeviceCaps (hdc, HORZSIZE) ; cyMms = GetDeviceCaps (hdc, VERTSIZE) ; cxPix = GetDeviceCaps (hdc, HORZRES) ; cyPix = GetDeviceCaps (hdc, VERTRES) ; hemf = GetEnhMetaFile (TEXT ("..\\emf8\\emf8.emf")) ; GetEnhMetaFileHeader (hemf, sizeof (emh), &emh) ; cxImage = emh.rclFrame.right - emh.rclFrame.left ; cyImage = emh.rclFrame.bottom - emh.rclFrame.top ; r cxImage = cxImage * cxPix / cxMms / 100 ; cyImage = cyImage * cyPix / cyMms / 100 ; rect.left = (cxArea - cxImage) / 2 ; rect.right = (cxArea + cxImage) / 2 ; I rect.top = (cyArea - cyImage) / 2 ; rect.bottom = (cyArea + cyImage) / 2 ; PlayEnhMetaFile (hdc, hemf, &rect) ; DeleteEnhMetaFile (hemf) ; Rysunek 18-17. Program EMF9 : EMF9 korzysta z metapliku utworzonego za pomocš EMF8, dlatego musisz uru- chomić najpierw ten drugi program. Funkcja PaintRoutine programu EMF9 rozpoczyna się od czterokrotnego wywo- łania funkcji GetDeviceCaps. Podobnie jak w wypadku funkcji CreateKoutine w pro- gramie EMF8, te cztery wywołania majš na celu uzyskanie informacji o rozdziel- Rozdział 18: Metapliki 1031 czoœci urzšdzenia. Po otrzymaniu uchwytu metapliku program odczytuje struk- turę nagłówkowš i używa pola rclFrame do obliczenia rozmiarów obrazu meta- pliku wyrażonych w 0,01 mrn. To był pierwszy krok. Następnie funkcja zamienia wyliczone rozmiary na piksele, mnożšc je przez pik- selowy rozmiar urzšdzenia wyjœciowego; a następnie dzielšc przez rozmiar po- dany w milimetrach oraz dodatkowo przez 100, aby uzyskać metryczne jednost- ki w 0,01 mm. Funkcja PaintRoutine ma teraz do dyspozycji rozmiary linijki po- dane w pikselach - lecz właœciwych nie dla ekranu monitora, a dla urzšdzenia docelowego. Od tej pory ustawienie obrazu w centrum okna roboczego nie po- winno sprawiać kłopotów. Wpatrujšc się w ekran nietrudno zauważyć, że wynik działania programu EMF9 jest identyczny jak EMF8. Jednak gdy wydrukujesz linijkę utworzonš w progra- mie EMF9, zobaczysz różnicę - rozmiary linijki sš zgodne z jej nominałem. Skalowanie i proporcje rozmiarów Może okazać się potrzebne wykorzystanie linijki utworzonej w programie EMF8, lecz niekoniecznie w oryginalnych rozmiarach. Korzystnie byłoby jednak zacho- wać projektowane proporcje rozmiarów linijki, czyli 6 do 1. Jak już wspomnia- łem wczeœniej, wygodnym sposobem modyfikacji rozmiarów obrazu osadzone- go w dokumencie procesora tekstu jest zastosowanie ramki wymiarujšcej. Jed- nak przy okazji wymiarowania możemy utracić cennš informację o proporcjach rozmiarów. Aplikacje tego typu powinny utrzymywać stały stosunek długoœci do szerokoœci, bez względu na zmianę wielkoœci obrazu, aby wymiary obrazu zmie- niane przez użytkownika za pomocš ramki wymiarujšcej nie wpływały bezpo- œrednio na wartoœci w strukturze przekazywanej do funkcji PIayEnhMetaFile. Zbadajmy, jak tego dokonać, na przykładzie programu EMFlO, przedstawione- go na rysunku 18-18. EMFlO.C /* EMFlO.C - Metaplik rozszerzony Demo i110 (c) Charles Petzold, 1998 */ ilinclude TCHAR szClass C] = TEXT ("EMFlO") ; TCHAR szTitle C] = TEXT ("EMFlO: Enhanced Metafile Demo 4t10") ; void CreateRoutine (HWND hwnd) ( void PaintRoutine (HWND hwnd, HDC hdc, int cxArea, int cyArea) ( ENHMETAHEADER emh ; float fScale ; HENHMETAFILE hemf ; 1032 Częœć Ii: Grafika ; (cišg dalszy ze strony 1031) int cxMms, cyMms, cxPix, cyPix, cxImage, cyImage ; i RECT rect ; cxMms = GetDeviceCaps (hdc, HORZSIZE) ; cyMms = GetOeviceCaps (hdc, VERTSIZE) ; cxPix = GetOeviceCaps (hdc, HORZRES) ; cyPix = GetOeviceCaps (hdc, VERTRES) ; hemf = GetEnhMetaFile (TEXT ("..\\emf8\\emf8.emf")) ; GetEnhMetaFileHeader (hemf, sizeof (emh), &emh) ; cxImage = emh.rclFrame.right - emh.rclFrame.left ; cyImage = emh.rclFrame.bottom - emh.rclFrame.top ; 1 cxImage = cxImage * cxPix / cxMms / 100 ; cyImage = cyImage * cyPix / cyMms / 100 ; t fScale = min ((float) cxArea / cxImage, (float) cyArea / cyImage) ; cxImage = (int) (fScale * cxImage) ; cyImage = (int) (fScale * cyImage) ; rect.left = (cxArea - cxImage) / 2 ; ů rect.right = (cxArea + cxImage) / 2 ; rect.top = (cyArea - cyImage) / 2 ; rect.bottom = (cyArea + cyImage) / 2 ; Play^nhMetaFile (hdc, hemf, &rect) ; DeleteEnhMetaFile (hemf) ; i Rysunek 18-18. Program EMFlO EMFlO rozcišga obraz linijki, dopasowujšc go do rozmiarów okna roboczego (lub obszaru wydruku), lecz nie zniekształca rzeczywistych proporcji. Linijka zajmu- je całš szerokoœć okna roboczego i jest położona w połowie jego wysokoœci. Jeœli poszerzysz okno, linijka będzie miała jego wysokoœć i znajdzie się w połowie sze- rokoœci. Istnieje wiele sposobów wyliczerua właœciwych rozmiarów prostokšta otaczajš- cego okno robocze, jednak zdecydowałem się oprzeć na podejœciu wprowadzo- nym w programie EMF9. Funkcja PaintRoutine modułu EMFlO.C rozpoczyna się, podobnie jak EMF9.C, przez wyznaczere rozmiaru pikselowego obrazu o szero- koœci 6 cali, właœciwego dla docelowego urzšdzenia wyjœciowego. ' ; Program wyznacza następnie zmiennoprzecinkowš wartoœć o nazwie fScale, któ- ra jest minimalnym stosunkiem szerokoœci okna roboczego do szerokoœci obrazu, 1 oraz analogicznš wartoœć zwišzanš z długoœciami okna roboczego i obrazu. 4Vy- liczone współczynniki sš wykorzystywane do zwiększania rozmiarów pikselo- wych obrazu przed wyznaczeniem prostokšta otaczajšcego obraz. ' 1033 Rozdział 18: Metapliki Tryby odwzorowania i metapliki Rysowaliœmy linijkę o podziałce calowej, mieliœmy również do czynienia z roz- miarami obrazu wyrażonymi w milimetrach. Do realizacji takich zadań dosko- nale nadajš się różne tryby odwzorowania, dostępne w technolog GDI. Co wię- cej, nalegam na stosowanie wymiarów pikselowych i przeprowadzanie wszelkich wyliczeń "ręcznie". Dlaczego? OdpowiedŸ jest prosta. Zastosowanie trybów odwzorowania w metaplikach może wprowadzić nieco zamieszarua. Przekonaj się jednak sam. Kiedy wywołujesz funkcję SetMapMode stosujšc kontekst urzšdzenia metapliku, funkcja jest kodowana w metapliku tak samo jak każda inna funkcja GDI. Zade- monstrowaliœmy to w programie EMFll, przedstawionym na rysunku 18-19. EMFll.C /* EMFll.C - Metaplik rozszerzony Demo 4ţ11 (c) Charles Petzold, 1998 */ ţţinclude TCHAR szClass C] = TEXT ("EMFl1") ; TCHAR szTitle C] = TEXT ("EMFll: Enhanced Metafile Demo ţţ11") ; void DrawRuler (HDC hdc, int cx, int cy) int i, iHeight ; LOGFONT lf ; TCHAR ch ; // Czarny pisak o gruboœci 1 punktu SelectObject (hdc, CreatePen (PSţSOLID, cx / 72 / 6, 0)) ; // Prostokšt otaczajšcy caly pisak if (GetVersion () & 0x80000000) // Windows 98 Rectangle (hdc, 0, -2, cx + 2, cy) ; else // Windows NT Rectangle (hdc, 0, -1, cx + 1, cy) ; // Podzialka for (i = 1 ; i < 96 ; i++) { if (i % 16 = 0) iHeight = cy / 2 ; // inches else if (i % 8 == 0) iHeight = cy / 3 ; // half inches else if (i % 4 == 0) iHeight = cy / 5 ; // quarter inches else if (i % 2 = 0) iHeight = cy / 8 ; // eighths else iHeight = cy / 12 ; // sixteenths MoveToEx (hdc. i * cx / 96, 0, NULL) : LineTo (hdc, i * cx / 96, iHeight) ; T 1034 Częœć II, Grafika (cišg dalszy ze strony 1033) // Utwórz czcionkę logicznd FillMemory (&lf, sizeof (lf), 0) ; lf.lfHeight = cy / 2 ; lstrcpy (lf.lfFaceName, TEXT ("Times New Roman")) ; ( SelectObject (hdc, CreateFontIndirect (&lf)) ; r SetTextAlign (hdc, TA_BOTTOM ţ TŽ CENTER) ; SetBkMode (hdc, TRANSPARENT) ; // Wyœwietl liczby podziałki for (i = 1 ; i <= 5 ; i++) i ch = (TCHAR) (i + '0') : TextOut (hdc, i * cx / 6, cy / 2, &ch, 1) ; // Posprzštaj Delete0bject (SelectObject (hdc, GetStockObject (SYSTEM_FONT))) ; Delete0bject (SelectObject (hdc, GetStockObject (BLACK PEN))) ; 1 ţ void CreateRoutine (HWND hwnd) j ( HDC hdcEMF ; HENHMETAFILE hemf ; hdcEMF = CreateEnhMetaFile (NULL, TEXT ("emfll.emf"), NULL, TEXT ("EMFl1\OEMF Demo ţtll\0")) ; t SetMapMode (hdcEMF, MM LOENGLISH) ; I DrawRuler (hdcEMF, 600, 100) ; hemf = CloseEnhMetaFile (hdcEMF) ; DeleteEnhMetaFile (hemf) ; , void PaintRoutine (HWND hwnd, HDC hdc, int cxArea, int cyArea) ( ENHMETAHEADER emh ; HENHMETAFILE hemf ; int cxMms, cyMms, cxPix, cyPix, cxImage, cyImage ; RECT rect ; cxMms = GetDeviceCaps (hdc, HORZSIZE) ; cyMms = GetDeviceCaps (hdc. VERTSIZE) ; cxPix = GetDeviceCaps (hdc, HORZRES) ; cyPix = GetOeviceCaps (hdc, VERTRES) ; hemf = GetEnhMetaFile (TEXT ("emfll.emf")) ; GetEnhMetaFileHeader (hemf, sizeof (emh), &emh) ; Rozdział 18: Metapliki 1035 cxIma9e = emh.rclFrame.right - emh.rclFrame.left ; i, cyImage = emh.rclFrame.bottom - emh.rclFrame.top ; cxImage = cxImage * cxPix / cxMms / 100 ; cyImage = cyImage * cyPix / cyMms / 100 ; rect.left = (cxArea - cxImage) / 2 ; rect.top = (cyArea - cyImage) / 2 ; rect.right = (cxArea + cxImage) / 2 ; rect.bottom = (cyArea + cyImage) / 2 ; PlayEnhMetaFile (hdc, hemf, &rect) ; DeleteEnhMetaFile (hemf) ; Rysunek 1&19. Program EMFl1 Budowa funkcji CreateRoutine w programie EMFl1 jest prostsza niż w EMF8 (pierw- szym programie rysujšcym linijkę), gdyż nie zawiera wywołania funkcji GetDevi- ceCaps do ustalenia rozdzielczoœci ekranu monitora w punktach na cal. Zamiast tego EMFl1 wywołuje SetMapMode, aby ustawić tryb odwzorowarua jako MMţLOEN- GLISH, w którym jednostkš logicznš jest 0,01 cala. Dlatego linijka ma rozmiary 600 na 100 jednostek. Wartoœci te przekazywane sš do funkcji DrawRuler. Funkcja DrawRuler programu EMFl1 wyglšda niemal identycznie jak ta umiesz- czona w programie EMF9. IZóżnice tkwiš w sposobie wywołania funkcji MoveTo- Ex oraz LineTo, rysujšcych kreski podziałki. Podczas rysowania linijki przy uży- ciu jednostek pikselowych (domyœlny tryb odwzorowania MM TEXT), ich war- toœci rosnš na osi pionowej, kiedy poruszamy się z góry na dół. W trybie MM-LO- ENGLISH (i innych metrycznych trybach odwzorowania) wartoœci rosnš od dołu do góry ekranu, dlatego konieczna była modyfikacja argumentów funkcji rysujš- cych. Zmieniono również wartoœci argumentów funkcji Rectangle. Funkcja PaintRoutine programu EMFl1 jest w zasadzie taka sama jak w progra- mie EMF9, w którym udało się wyœwietlić linijkę o rzeczywistych rozmiarach na ekrarue oraz wydrukować na drukarce. Jedyna różnica jest taka, że EMFl1 uży- wa metapliku EMFll.EMF, podczas gdy EMF9 - metapliku o nazwie EMF8.EMF, utworzonego przez program EMF8. Obraz wyœwietlany przez EMFl1 jest w zasadzie taki sam jak w EMF9. Porów- nujšc oba programy możemy zobaczyć, jak zastosowanie funkcji SetMapMode upraszcza tworzerue metapliku, nie wpływajšc jednoczeœnie na mechanizm od- twarzarua metapliku w jego oryginalnych rozmiarach. Odwzorowanie i odtwarzanie Wyliczanie rozmiarów prostokšta w programie EMFII wymaga zastosowania kilku wywołań GetDeviceCaps. Naszym drugim celem jest wyeliminowanie tych wywołań i zastšpierue ich trybami odwzorowania. GDI traktuje współrzędne pro- stokšta docelowego jako współrzędne logiczne. Zastosowarue trybu odwzorowa- nia MM HIMETRIC będzie odpowiednie do tego celu, gdyż wišże się ze wpro- 1036 Częœć ilţ Grafika wadzeniem jednostki o rozmiarach 0,01 milimetra czyli takiej jakš stosowano , , do prostokšta otaczajšcego rozszerzone metapliki. Program EMFl2, przedstawiony na rysunku 18-20 ponownie stosuje funkcję Draw- Ruler w pierwotnej postaci, użytej po raz pierwszy w programie EMF8, lecz tym razem do wyœwietlenia metapliku użyto trybu odwzorowania MM-HIMETRIC. EMFl2.C /* I EMFl2.C - Metaplik rozszerzony Demo ţţ12 (c) Charles Petzold, 1998 */ ţţinclude I I TCHAR szClass [] = TEXT ("EMFl2") ; TCHAR szTitle [] = TEXT ("EMFl2: Enhanced Metafile Demo ţţ12") ; void DrawRuler (HDC hdc, int cx, int cy) ( int iAdj, i, iHeight ; LOGFONT lf ; TCHAR ch ; iAdj = GetVersion.() & 0x80000000 ? 0 : 1 ; I // Czarny pisak o gruboœci 1 punktu SelectObject (hdc, CreatePen (PS SOLID, cx / 72 / 6, 0)) ; // Prostokšt otaczajšcy caly pisak Rectangle (hdc, iAdj, iAdj, cx + iAdj + l, cy + iAdj + 1) ; // Podziałka for ( i = 1 ; i < 96 ; i++) if (i % 16 = 0) iHeight = cy / 2 ; // inches else if (i % 8 == 0) iHeight = cy / 3 ; // half inches else if (i % 4 == 0) iHeight = cy / 5 ; // quarter inches else if (i % 2 == 0) iHeight = cy / 8 ; // eighths else iHeight = cy / 12 ; // sixteenths MoveToEx (hdc, i * cx / 96, cy, NULL) ; I LineTo (hdc, i * cx / 96, cy - iHeight) ; ) // Utwórz czcionkę logicznš FillMemory (&lf, sizeof (lf), 0) ; lf.lfNeight = cy / 2 ; lstrcpy (lf.lfFaceName, TEXT ("Times New Roman")) ; I SelectObject (hdc, CreateFontIndirect (&lf)) ; SetTextAlign (hdc, TAţBOTTOM ţ TAţCENTER) ; Rozdziat 18: Metapliki 1037 SetBkMode (hdc, TRANSPARENT) ; // Wyœwietl liczby podziałki for ( i = 1 ; i <= 5 ; i++) ch = (TCHAR) (i + '0') ; TextOut (hdc, i * cx / 6, cy / Z, ţch, 1) ; ) // Posprzštaj Delete0bject (SelectObject (hdc, GetStockObject (SYSTEM_FONT))) ; Delete0bject (SelectObject (hdc, GetStockObject (BLACKţPEN))) ; ) void CreateRoutine (HWND hwnd) ( HDC hdcEMF ; HENHMETAFILE hemf ; int cxMms, cyMms, cxPix, cyPix, xDpi, yDpi ; hdcEMF = CreateEnhMetaFile (NULL, TEXT ("emfl2.emf"), NULL, TEXT ("EMFl3\OEMF Demo ţţ12\0")) ; cxMms = GetDeviceCaps (hdcEMF, HORZSIZE) ; cyMms = GetDeviceCaps (hdcEMF, VERTSIZE) ; cxPix = GetDeviceCaps (hdcEMF, HORZRES) ; cyPix = GetDeviceCaps (hdcEMF, VERTRES) ; xDpi = cxPix * 254 / cxMms / 10 ; yDpi = cyPix * 254 / cyMms / 10 ; DrawRuler (hdcEMF, 6 * xDpi, yDpi) ; hemf = CloseEnhMetaFile (hdcEMF) ; DeleteEnhMetaFile (hemf) ; void PaintRoutine (HWND hwnd, HDC hdc, int cxArea, int cyArea) ENHMETAHEADER emh ; HENHMETAFILE hemf ; POINT pt ; int cxImage, cyImage ; RECT rect ; SetMapMode (hdc, MM HIMETRIC) ; SetViewportOrgEx (hdc, 0, cyArea, NULL) ; pt.x = cxArea ; 0 ; DPtoLP (hdc, &pt, 1) ; hemf = GetEnhMetaFile (TEXT ("emfl2.emf")) ; 1038 Częœć II: Grafika (cišg dalszy ze strony 1037) GetEnhMetaFileHeader (hemf, sizeof (emh), &emh) ; cxImage = emh.rclFrame.right - emh.rclFrame.left ; cyImage = emh.rclFrame.bottom - emh.rclFrame.top ; rect.left = (pt.x - cxImage) / 2 ; rect.top = (pt.y + cyImage) / 2 ; rect.right = (pt.x + cxImage) / 2 ; rect.bottom = (pt.y - cyImage) / 2 : PlayEnhMetaFile (hdc, hemf, &rect) ; DeleteEnhMetaFile (hemf) ; Rysunek 18-20: Program EMFl2 Funkcja PaintIZoutine w programie EMFl2 najpierw ustala tryb odwzorowania jako MM HIMETIZIC. Podobnie jak w innych trybach odwzorowania, wartoœci y ro- snš od dołu ku górze ekranu. Jednak poczštek układu współrzędnych znajduje się w lewym górnym narożniku okna, co oznacza, że y w obrębie okna roboczego przyjmuje wartoœci ujemne. Aby to zmienić, program wywołuje funkcję SetView- portOrgEx, aby ustalić poczštek układu współrzędnych w lewym dolnym naroż- niku okna. Punkt 0 urzšdzenia (cxArea, 0) jest umieszczony w prawym górnym narożniku ekranu. Przekazujšc ten punkt do funkcji DPtoLP otrzymujemy rozmiar okna roboczego podany w jednostkach równych 0,01 milimetra. Następnie program ładuje metaplik, uzyskuje jego nagłówek i wreszcie odnajdu- je rozmiary metapliku w jednostkach 0,01 milimetra. Teraz widać, jak możemy wykorzystać tryb odwzorowania w trakcie tworzenia i wyœwietlania metapliku. Program EMFl3 (przedstawiony na rysunku 18-21) demonstruje efekt zastoso- wania trybu odwzorowania. EMFl3.C /* EMFl3.C - Metaplik rozszerzony Demo 13 (c) Charles Petzold, 1998 */ ilinclude TCHAR szClass C7 = TEXT ("EMFl3") ; TCHAR szTitle C7 = TEXT ("EMFl3: Enhanced Metafile Demo itl3") ; void CreateRoutine (HWND hwnd) ( ) Rozdział 18: Metapłiki 1039 void PaintRoutine (HWND hwnd, HDC hdc, int cxArea, int cyArea) i ENHMETAHEADER emh ; HENHMETAFILE hemf ; POINT pt ; int cxImage, cyImage ; RECT rect ; SetMapMode (hdc, MM HIMETRIC) ; SetViewportOrgEx (hdc, 0, cyArea, NULL) ; pt.x = cxArea ; pt.y = 0 ; DPtoLP (hdc, &pt, 1) ; hemf = GetEnhMetaFile (TEXT ("..\\emfll\\emfll.emf")) ; GetEnhMetaFileHeader (hemf, sizeof (emh), &emh) ; cxImage = emh.rclFrame.right - emh.rclFrame.left ; cyImage = emh.rclFrame.bottom - emh.rclFrame.top ; rect.left = (pt.x - cxImage) / 2 ; rect.top = (pt.y + cyImage) / 2 ; rect.right = (pt.x + cxImage) / 2 ; rect.bottom = (pt.y - cyImage) / 2 ; PlayEnhMetaFile (hdc, hemf, &rect) ; DeleteEnhMetaFile (hemf) ; Rysunek 18-21. Program EMFl3 W programie EMFl3 nie trzeba tworzyć metapliku zawierajšcego obraz linijki, gdyż został on już utworzony w programie EMFII. EMFl3 po prostu wczytuje istniejšcy plik i stosuje tryb odwzorowania do wyliczenia rozmiarów prostokšta docelowego, podobnie jak EMFII. Teraz możemy ustanowić kilka zasad. Kiedy tworzony jest metaplik, GDI wyko- rzystuje tryby odwzorowania do wyznaczenia rozmiarów metapliku w pikselach ; i milimetrach. Rozmiar ten jest przechowywany w rekordzie nagłówkowym. Od- twarzajšc metaplik, GDI ustal„ fizyczne położenie prostokšta docelowego na podstawie trybu odwzorowania w funkcji PlayEnhMetaFile. Położenia nie może zmienić żaden z parametrów znajdujšcych się w samym metapliku. i Częœć III Zagadnienia zaawa nsowane Rozdział 19 In ' te rf es wi I e od ok umentow Interfejs wielodokumentowy (Multiple-Document Interface, MDI) stanowi specyfi- kację dla aplikacji obsługujšcych dokumenty systemu Microsoft Windows. Spe- cyfikacja ta opisuje strukturę okna oraz interfejs użytkownika, umożliwiajšce korzystanie z wielu dokumentów z poziomu jednej aplikacji (na przykład doku- mentów tekstowych w edytorze lub tabel w arkuszu kalkulacyjnym). Mówišc prosto, podobnie jak Windows pozwala otwierać wiele okien aplikacji, tak apli- kacja MDI pozwala otwierać wiele okien dokumentów w jednym obszarze robo- czym. Pierwszš aplikacjš MDI dla Windows była pierwsza windowsowa wersja programu Microsoft Excel. Wkrótce po niej pojawiły się kolejne. Koncepcja MDI Choć specyfikacja MDI funkcjonuje już od czasów Windows 2.0, nie od razu jš wykorzystywano, ponieważ poczštkowo pisanie aplikacji MDI było trudne i wy- magało bardzo szeroko zakrojonych prac programistycznych. Od pojawienia się Windows 3.0 większoœć ucišżliwych zadań przejšł system. Podwaliny tego sys- temu, wzbogacone o rozszerzenia z Windows 95, zostały przeniesione do Win- dows 98 i Microsoft Windows NT. Elementy MDI Główne okno programu MDI ma tradycyjny wyglšd: zawiera pasek tytułu, menu, obramowanie, którym można regulować wielkoœć okna, ikonę menu systemowe- go, a także przyciski minimalizacji, maksymalizacji i zamykania. Obszar klienta jest jednak nazywany często obszarem roboczym i nie służy bezpoœrednio do wyœwietlania danych. Obszar roboczy może zawierać okna potomne; w każdym z nich może być wyœwietlany jeden dokument. Okna potomne przypominajš zwykłe okna aplikacyjne, czyli między innymi głów- ne okno programu MDI. One także zawierajš paski tytułu, obramowanie umoż- liwiajšce regulowanie wielkoœci okna, ikonę menu systemowego, przyciski mini- malizacji, maksymalizacji i zamykania oraz ewentualnie paski przewijania. Żad- ne z okien dokumentów nie ma jednak własnego menu. Operacje na oknach do- kumentów wykonuje się z głównego menu aplikacji. 1044 Częœć III: Zagadnienia zaawansowane W każdej chwili aktywne pozostaje tylko jedno okno dokumentu (można je roz- poznać po zmienionym w stosunku do innych okien kolorze paska tytuhx oraz po tym, że znajduje się na wierzchu pozostałych okien dokumentów). Wszystkie okna potomne dokumentów sš kadrowane do wielkoœci obszaru roboczego i ni- gdy nie wystajš poza okno aplikacji. Z punktu widzenia programisty Windows specyfikacja MDI wydaje się poczšt- kowo doœć przejrzysta. Wystarczy utworzyć okno WS CHILD dla każdego do- kumentu, czynišc jego oknem nadrzędnym główne okno aplikacji. Jednak po bardziej dogłębnej analizie istniejšcych aplikacji MDI pojawiš się pewne kompli- kacje - fragmenty bardziej złożonego kodu. ů Okno dokumentu MDI można minimalizować. Zminimalizowane okno do- kumentu jest przedstawiane u dołu obszaru roboczego w postaci małego pa- ska tytułu z ikonš. Na ogół aplikacja MDI zawiera inne definicje ikon dla głów- nego okna aplikacji i inne dla poszczególnych typów okien dokumentów. ů Okno dokumentu MDI moŸna maksyxnalizować. Zmaksymalizowane okno dokumentu jest pozbawione paska tytułu (który służy zazwyczaj jako miejsce na nazwę pliku lub dokumentu), tekst wyœwietlany dotšd na pasku okna do- kumentu jest dopisywany do tekstu paska okna aplikacji. Ikona menu syste- mowego okna dokumentu staje się pierwszym elementem paska menu okna aplikacji. Przycisk zamykajšcy okno dokumentu staje się ostatnim elementem paska menu okna aplikacji i trafia na jego prawy skraj. ů Klawisz skrótu zamykajšcy okno dokumentu jest prawie identyczny z klawi- szem skrótu zamykajšcym okno aplikacji, z tym że pierwszy jest kombinacjš zawierajšcš klawisz [Ctrl], a drugi kombinacjš zawierajšcš klawisz [Alt]. Kon- kretnie rzecz ujmujšc, okno aplikacji można zamknšć za pomocš [Alt+F4], a okno dokumentu za pomocš [Ctrl+F4]. Dodatkowo dostępny jest skrót [Ctrl+F6], który pozwala przełšczać aktywne okna bieżšcej aplikacji MDI. [Alt+Spacja] wywołuje menu systemowe głównego okna - jak zawsze. [Alt+- (minus)] wywołuje menu systemowe okna aktywnego dokumentu. ů Przechodzšc przez kolejne pozycje paska menu za pomocš klawiszy strzałek, zazwyczaj pokonuje się poszczególne polecenia menu głównego oraz ikonę menu systemowego (pierwszy element paska menu). W aplikacji MDI stero- wanie z menu sterujšcego aplikacji jest przekazywane dodatkowo do menu sterujšcego aktywnego dokumentu (stanowišcego - jak wspomniano wyżej - pierwszš pozycję paska menu). ů Jeżeli aplikacja może obsłużyć kilka typów okien potomnych (na przykład Microsoft Excel obsługuje dokumenty z arkuszami kalkulacyjnymi i dokumenty z wykresami), menu powinno odzwierciedlać operacje skojarzone z bieżšcym typem dokumentu. Wymaga to przełšczania przez program menu przy wy- borze (uaktywnieniu) dokumentu innego typu. Dodatkowym wymogiem jest zawężenie dostępnych opcji menu - w przypadku gdy nie jest otwarty żaden dokuxnent - do wyłšcznie tych, które majš sens w programie bez otwartego dokumentu, czyli na przykład do poleceń służšcych do tworzenia nowych dokumentów i otwierania istruejšcych. Rozdział 19: Interfejs wielodokumentowy 1045 ů W menu głównym znajduje się pozycja o nazwie Okno. Tradycyjnie jest to ostatnia pozycja menu głównego, no, przedostatnia, jeżeli uwzględnić pozy- cję Pomoc. Podmenu Okno zawiera opcje pomocne przy definiowaniu ukła- du okien obszaru roboczego. Okna dokumentów mogš być ułożone kaskado- wo od lewego górnego rogu do prawego dolnego albo sšsiadujšco (czyli tak, że żadne z okien nie jest przykryte innym). To podmenu zawiera również li- stę wszystkich otwartych okien dokumentów. Wybranie jednego z okien po- woduje uaktywnienie go (przesunięcie na pierwszy plan). Każdy z powyższych aspektów MDI jest obsługiwany przez Windows 98. Nie- które funkcje wymagajš rueco większego nakładu pracy (jak zostanie pokazane w programie przykładowym), ale nakład ten i tak jest nieporównywalnie mniej- szy od tego, jaki należałoby ponieœć, aby poszczególne funkcje zaimplemento- wać samodzielnie. Obsługa MDI Przed omówieniem obsługi MDI przez Windows MDI trzeba zaprezentować kil- ka nowych pojęć. Główne okno aplikacji jest nazywane oknem ramowym (frame window) i stanowi, podobnie jak w tradycyjnych programach Windows, okno typu WS OVERLAPPEDWINDOW, Aplikacja MDI musi również utworzyć okno-klienta, bazujšc na predefiniowanej klasie okien MDICLIENT. Okno-klienta tworzy się za pomocš wywołania Create- Window z wymieruonš wyżej klasš okna i stylem WS CHILD. Ostatnim argumen- tem funkcji CreateWindow jest wskaŸnik do małej struktury typu CLIENTCREATE- STRUCT. Okno-klient zajmuje obszar roboczy okna ramowego i jest odpowiedzial- ne za większoœć obsługi MDI. Kolor tego okna-klienta jest identyczny z kolorem COLOR APPWORKSPACE. Okna dokumentów, jak można się zorientować, sš nazywane oknami potomny- mi. Tworzy się je, inicjujšc strukturę typu MDICREATESTRUCT i wysyłajšc oknu- klientowi komunikat WMţMDICREATE ze wskaŸnikiem do tej struktury. Okna dokumentów sš potomkami okna-klienta, które z kolei jest potomkiem okna ramowego. Omawianš hierarchię okien można obejrzeć na rysunku 19-1. T 1046 Częœć III: Zagadnienia zaawansowane okno ramowe (glówne okno aplikacji) .. okno-klient Rysunek 19-1. Hierarchia okien w aplikacji MDI (dokument windows) Klasa okna (i procedura okna) jest potrzebna nie tylko dla okna ramowego, ale i dla każdego okna ţotomnego obsługiwanego przez aplikację. Okno-klient nie wymaga procedury okna, ponieważ jego klasa jest już wstępnie zarejestrowana. Obsługa MDI w Windows 98 obejmuje definicje: klasy okna, pięciu funkcji, dwóch struktur danych i dwunastu komunikatów. Wspomniałem już, że klasa nowego okna to MDICLIENT, a struktury danych to CLIENTCREATESTRUCT i MDICRE- ATESTRUCT. W aplikacjach MDI funkcję DefWindowProc zastępujš dwie z pięciu funkcji: zamiast wywoływać dla wszystkich nieobsłużonych komunikatów funk- cję Def tNindowProc, procedura okna ramowego wywohxje funkcję DefFrameProc, a procedura okna potomnego - funkcję DefMDIChildProc. Inna funkcja typowa dla MDI, TransIateMDISysAccel, jest używana w taki sam sposób jak TransIateAccele- rator, omówiona w rozdziale 10. Obsługa MDI obejmuje również funkcję Arran- gelconicWindows, ale jeden z k”munikatów specjalnych MDI sprawia, że w pro- gramach MDI staje się ona zbyteczna. Pištš funkcjš MDI jest CreateMDlWindow. Umożliwia ona tworzenie okna potom- nego w osobnym wštku. Funkcja ta nie przydaje się w programach jednowštko- wych, co zaprezentuję w dalszej częœci ksišżki. W programie przykładowym, który przedstawię dalej, zademonstruję dziewięć z dwunastu komunikatów MDI (pozostałe trzy sš na ogół zbędne). Wszystkie zaczynajš się prefiksem WM MDI. Okno ramowe wysyła je do okna-klienta, aby wykonać jakšœ operację na oknie potomnym albo uzyskać od niego jakieœ dane (okno ramowe może na przykład wysłać do okna potomnego komunikat WM MDICREATE, powodujšcy utworzenie okna potomnego). Wyjštkiem jest komunikat WM MDIACTIVATE: choć okno ramowe może wysyłać go do okna- klienta w celu uaktywniania okien potomnych, okno-klient może go wysyłać rów- nież do uaktywnianych lub dezaktywowanych okien potomnych w celu poinfor- mowania ich o powstałych zmianach. Rozdział 19: Intertejs wislodorcumen#awy 1047 Prrykładowa impieţentaeja MDI Program MDIDEMO, pokazany na rysunku 19-2, demonstruje podstawy pisania aplikacji MDI. ' MDIDEMO.C /* mnutrţu.u - Uemonstracja interfejsu wielodokumentowego (MDI) (c) Charles Petzold, 1998 */ ţdinclude #ţinclude "resource.h" 4ţdefine INIT MENUţPOS 0 4ţdefine HELLO MENUţPOS 2 ţţdefine RECT MENU POS 1 4ţdefine IDM FIRSTCHILD 50000 , LRESULT CALLBACK Frame4ţndProc (HWND, UINT, WPARAM, LPARAM) ; BOOL CALLBACK CloseEnumProc (HWND, LPARAM) ; LRESULT CALLBACK HelloWndProc (HWND, UINT, WPARAM, LPARAM) ; LRESULT CALLBACK RectWndProc (HWND, UINT, WPARAM, LPARAM) ; // struktura przeznaczona na prywatne dane każdego z okien potomnych Hello typedef struct tagHELLODATA UINT iColor ; _ COLORREF clrText ; 1 HELLODATA, * PHELLODATA ; // struktura przeznaczona na prywatne dane każdego z okien potomnych Rect typedef struct tagRECTDATA f short cxClient ; short cyClient ; 1 RECTDATA, * PRECTDATA ; // zmienne globalne TCHAR szAppName[] = TEXT f"MDIDemo") ; TCHAR szframeClass[] = TEXT ("MdiFrame") ; TCHAR szHelloClass[] = TEXT ("MdiHelloChild") ; TCHAR szRectClass[] = TEXT ("MdiRectChild") ; HINSTANCE hInst ; I HMENU hMenuInit, hMenuHello, hMenuRect ; HMENU hMenuInitWindow, hMenuHelloWindow, hMenuRectWindow ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInsta.nce, PSTR szCmdLine, int iCmdShow) ( 1048 Czţœć III: Zagadnienis zaawansowane (cišg dalszy ze strony 1047) HACCEL hAccel ; HWND hwndFrame. hwndClient : MSG msg : WNDCLASS wndclass : hInst = hInstance ; // Rejestrowanie klasy okna ramowego wndclass.style ţ CS HREDRAW ţ CS VREDRAW ; wndclass.lpfnWndProc = FrameWndProc : wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon a LoadIcon (NULL, IDI_APPLICATION) wndclass.hCursor s LoadCursor (NULL, IDC_ARROW) : wndclass.hbrBackground = (HBRUSH) (COLOR APPWORKSPACE + 1) wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szFrameClass ; if (!RegisterClass (&wndclass)) ( MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MBţICONERROR) ; return 0 ; // Rejestrowanie klasy okna potomnego Hello wndclass.style = CS_HREDRAW ţ CS VREDRAW : wndclass.lpfnWndProc = HelloWndProc ; wndclass.cbClsExtra = 0 : wndclass.cbWndExtra = sizeof (HANDLE) : wndclass.hInstance = hInstanee ; wndclass.hIcon = LoadIcon (NULL. IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL. IDC ŽRROW) : wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITĘ BRUSH) wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szHelloClass : RegisterClass (&wndclass) ; // Rejestrowanie klasy okna potomnego Rect wndclass.style = CS HREDRAW ţ CS VREDRAW ; wndclass.lpfnWndProc = RectWndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = sizeof (HANDLE) : wndclass.hInstance = hInstance : wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor ř LoadCursor (NULL. IDC ŽRROW) : wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITEţBRUSH) wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szRectClass : RegisterClass (&wndclass) ; Rozdział 19: Intartejs wielodokumentowy // Pozyskanie uchwytów do trzech menu & podmenu hMenuInit = LoadMenu (hInstance, TEXT ("MdiMenuInit")) ; hMenuHello = LoadMenu (hInstance, TEXT ("MdiMenuHello")) ; hMenuRect = LoadMenu (hInstance, TEXT ("MdiMenuRect")) ; ' hMenuInitWindow ţ GetSubMenu (hMenuInit, INIT_MENU_POS) ; hMenuHelloWindow = GetSubMenu (hMenuHello, HELLO_MENUţPOS) ; ,ţ.ř hMenuRectWindow = GetSubMenu (hMenuRect. RECT MENUţPOS) ; // Wczytanie tablicy skrótów klawiaturowych hAccel = LoadAccelerators (hInstance, szAppName) ; , // Utworzenie okna ramowego I hwndFrame = CreateWindow (szFrameClass, TEXT ("MDI Demonstration"), ' WS OIIERLAPPEDWINDOW ţ WS_CLIPCHILDREN, j CW USEDEFAULT, CW USEDEFAULT, CW USEDEFAULT, CW USEDEFAULT, NULL, hMenuInit, hInstance, NULL) ; hwndClient = GetWindow (hwndFrame, GW CHILD) ; ShowWindow (hwndFrame, iCmdShow) ; UpdateWindow (hwndFrame) ; // Wejœcie do zmodyfikowanej pętli komunikatów while (GetMessage (&msg, NULL, 0, 0)) 1 if (!TranslateMDISysAccel (hwndClient, &msg) && !TranslateAccelerator (hwndFrame, hAccel, &msg)) ( TranslateMessage (&msg) ; ' DispatchMessage (&msg) ; ) 1 // Sprzštanie poprzez usuwanie niepodłdczonych menu DestroyMenu (hMenuHello) ; DestroyMenu (hMenuRect) ; return msg.wParam ; LRESULT CALLBACK FrameWndProc (HWND hwnd. UINT message, WPARAM wParam, LPARAM lParam) static HWND hwndClient ; CLIENTCREATESTRUCT clientcreate ; HWND hwndChild ; I MDICREATESTRUCT mdicreate ; switch (message) f case WM CREATE: // Utworzenie okna-klienta 1050 Czţœć ţ1: ZsţdMenia zaaaaansowane (cišg dalszy ze strony 1049) r clientcreate.hWindowMenu = hMenuInitWindow ; clientcreate.idFirstChild = IDMţFIRSTCHILD ; hwndClient = CreateWindow (TEXT ("MDICLIENT"), NULL, WS_CHILD ( WS_CLIPCHILDREN ţ WS_VISIBLE, ' 0, 0, 0, 0, hwnd, (HMENU) l, hInst, (PSTR) &clientcreate) ; return 0 ; case WM COMMAND: switch (LOWORD (wParam)) case IDMţFILE NEWHELLO: // Utworzenie okna potomnego Hello mdicreate.szClass = szHelloClass ; mdicreate.szTitle = TEXT ("Hello") ; mdicreate.hOwner = hInst ; , mdicreate.x = CW USEDEFAULT : mdicreate.y = CWţUSEDEFAULT ; mdicreate.cx = CW USEDEFAULT ; mdicreate.cy = CW USEDEFAULT ; mdicreate.style = 0 ; mdicreate.lParam = 0 ; hwndChild = (HWND) SendMessage (hwndClient, WM_MDICR^ATE, 0, (LPARAM) (LPMDICREATESTRUCT) &mdicreate) ; return 0 ; case IDMţFILE NEWRECT: // Utworzenie okna potomnega Rect r mdicreate.szClass = szRectClass ; mdicreate.szTitle = TEXT ("Rectangles") ; mdicreate.hOwner = hInst ; , mdicreate.x = CW_USED.EFAULT ; mdicreate.y = CW_USEDEFAULT ; mdicreate.cx = CW_USEDEFAULT : mdicreate.cy = CW USEDEFAULT ; mdicreate.style = 0 ; mdicreate.lParam = 0 : hwndChild = (HWND) SendMessage (hwndClient, WM_MDICREATE, 0, (LPARAM) (LPMDICREATESTRUCT) &mdicreate) return 0 : case IDMţFILEţCLOSE: // Zamykanie aktywnego okna hwndChild = (HWND) SendMessage (hwndClient, Wţ1_MDI6ETACTIV^, 0, 0) ; , if (SendMessage (hwndChild, WM_OUERYENDSESSION, 0, 0)) SendMessage (hwndClient, WM_MDIDESTROY, (WPARAM) hwndChild, 0) : return 0 ; case IDM ŽPPţEXIT: // Kończenie programu Rozdział 19: Interfejs wielodokumentowy 1051 SendMessage (hwnd, WMţCLOSE, 0, 0) ; return 0 ; // komunikat modyfikacji ukladu okien case IDM_WINDOW_TILE: SendMessage (hwndClient, WM_MDITILE, 0, 0) ; return 0 ; case IDM_WINDOW CASCADE: SendMessage (hwndClient, WM MDICASCADE, 0, 0) ; return 0 ; case IDM WINDOW ARRANGE: SendMessage (hwndClient, WM MDIICONARRANGE, 0, 0) ; return 0 ; case IDM WINDOWţCLOSEALL: // Próba zamknięcia wszystkich potomków EnumChildWindows (hwndClient, CloseEnumProc, 0) ; return 0 ; default: // Przekazanie do aktywnego okna potomnego... i I hwndChild = (HWND) SendMessage (hwndClient, WM MDIGETACTIVE, 0, 0) ; if (IsWindow (hwndChild)) SendMessage (hwndChild, WM COMMAND, wParam, lParam) ; break ; // ..,a potem do DefFrameProc ) break ; case WM OUERYENDSESSION: case WMţCLOSE: // Próba zamknięcia wszystkich potomków SendMessage (hwnd, WM COMMANO, IDM WINDOW CLOSEALL, 0) ; if (NULL != GetWindow (hwndClient, GW CHILD)) return 0 ; i break ; // tj. wywolanie DefFrameProc case WM DESTROY: PostOuitMessage (0) ; return 0 ; // Przekazanie nieobsłużonych komunikatów do DefFrameProc ! // (nie do DefWindowProc) 7 return DefFrameProc (hwnd, hwndClient, message, wParam, lParam) I 1 BOOL CALLBACK CloseEnumProc (HWND hwnd, LPARAM lParam) if (GetWindow (hwnd, GW OWNER)) // Sprawdzenie tytulu ikony return TRUE ; ;ţłl 1052 Częœć III: Zagadnienia zaawansowane (cišg dalszy ze strony 1051) SendMessage (GetParent (hwnd), WM MDIRESTORE, (WPARAM) hwnd, 0> ; if (!SendMessage (hwnd, WM OUERYENDSESSION, 0, 0)) return TRUE ; SendMessage (GetParent (hwnd), WM MDIDESTROY, (WPARAM) hwnd, 0) ; return TRUE ; 1 LRESULT CALLBACK HelloWndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ( static COLORREF clrTextArrayC] = ( RGB (0, 0, 0), RGB (255, 0, 0), RGB (0, 255, 0), RGB ( 0, 0, 255), RGB (255, 255, 255) 1 ; static HWND hwndClient, hwndFrame ; HDC hdc ; HMENU hMenu ; PHELLODATA pHelloData ; PAINTSTRUCT ps ; RECT rect ; switch (message) t case WM_CREATE: // Zarezerwowanie pamięci na prywatne dane okna pNelloData = (PHELLODATA) HeapAlloc (GetProcessHeap (), HEAP ZERO_MEMORY, sizeof (HELLODATA)) ; pNelloData->iColor = IDM_COLOR_BLACK ; pHelloData->clrText = RGB (0, 0, 0) ; SetWindowLong (hwnd, 0, (long) pHelloData) ; // Zachowanie niektórych uchwytów okien hwndClient = GetParent (hwnd) ; hwndFrame = GetParent (hwndClient) ; return 0 ; case WM COMMAND: , switch (LOWORD (wParam)) ( case IDM_COLOR_BLACK: case IDM_COLOR_RED: case IDM_COLOR_GREEN: case IDM COLOR BLUE: case IDM COLOR WHITE: // Zmiana koloru tekstu pNelloData = (PHELLODATA) GetWindowLong (hwnd, 0) ; hMenu = GetMenu (hwndFrame) ; CheckMenuItem (hMenu, pHelloData->iColor, MF UNCHECKED) ; pHelloData->iColor = wParam ; Rozdział 19: Interfejs wielodokumentowy 1053 CheckMenuItem (hMenu, pHelloData->iColor, MF CHECKED) ; pHelloData->clrText = clrTextArrayCwParam - IDM COLORţBLACK] ; InvalidateRect (hwnd, NULL, FALSE) ; ) return 0 ; " case WMţPAINT: // Malowanie okna hdc = BeginPaint (hwnd, &ps) ; pHelloOata = (PHELLODATA) GetWindowLong (hwnd, 0) ; SetTextColor (hdc, pHelloData->clrText) ; GetClientRect (hwnd, &rect) ; ; DrawText (hdc, TEXT ("Hello, World!"). -1, &rect, DT SINGLELINE ţ,DT CENTER ţ DT VCENTER) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_MDIACTIVATE: // Ustaw menu Hello w przypadku otrzymania fokusu if (lParam == (LPARAM) hwnd) SendMessage (hwndClient, WM MDISETMENU, (WPARAM) hMenuHello, (LPARAM) hMenuHelloWindow) ; // Postaw lub usuń znaczek obok opcji menu pHelloData = (PHELLODATA) GetWindowLong (hwnd, 0) ; CheckMenuItem (hMenuHello, pHelloData->iColor, a, (lParam == (LPARAM) hwnd) ? MF CHECKED : MF UNCHECKED) ; ?ţ; // Ustaw menu Init w przypadku utraty fokusu if (lParam != (LPARAM) hwnd) SendMessage (hwndClient, WM MDISETMENU, (WPARAM) hMenuInit, (LPARAM) hMenuInitWindow) ; DrawMenuBar (hwndFrame) ; i.. return 0 ; s case WM_OUERYENDSESSION: case WM_CLOSE: if (IDOK != MessageBox (hwnd, TEXT ("OK to close window "). TEXT ( Hello"), " MBţICONOUESTION ţ MB OKCANCEL)) return 0 ; break ; // tj. wywolanie DefMDIChildProc case WM_DESTROY: pHelloData = (PHELLODATA) GetWindowLong (hwnd, 0) : HeapFree (GetProcessHeap (), 0, pHelloData) ; 1054 Częœć III: Zagadnienia zaawansowane (cišg dalszy ze strony 1053) return 0 ; , I // Przekazanie nieobsłużonych komunikatów do DefMDIChildProc return DefMDIChildProc (hwnd, message, wParam, lParam) ; 1 r LRESULT CALLBACK RectWndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ( static HWND hwndClient, hwndFrame ; HBRUSH hBrush ; HDC hdc ; PRECTDATA pRectData ; PAINTSTRUCT ps ; int xLeft, xRight, yTop, yBottom ; r short nRed, nGreen, nBlue ; switch (message) r i case WMţCREATE: // Zarezerwowanie pamięci na prywatne dane okna pRectData = (PRECTDATA) HeapAlloc (GetProcessHeap (), HEAP ZERO MEMORY, sizeof (RECTDATA)) ; SetWindowLong (hwnd, 0, (long) pRectData) ; // Uruchomienie czasomierza SetTimer (hwnd, 1, 250, NULL) ; // Zachowanie niektórych uchwytów okien hwndClient = GetParent (hwnd) ; hwndFrame = GetParent (hwndClient) ; return 0 ; case WM SIZE: // Jeżeli nie zminimalizowane, zapisanie rozmiaru okna if (wParam != SIZĘ MINIMIZED) ( pRectData = (PRECTDATA) GetWindowLong (hwnd, 0) ; pRectOata->cxClient = LOWORD (lParam) ; pRectOata->cyClient = HIWORD (lParam) ; ) break ; // WMţSIZE musi być obslużony przez DefMDIChildProc case WM TIMER: // Wyœwiet.lenie losowego prostokţta pRectData = (PRECTDATA) GetWindowLong (hwnd, 0) ; r xLeft = rand () % pRectData->cxClient ; xRight = rand () % pRectOata->cxClient ; yTop = rand () % pRectOata->cyClient ; yBottom = rand () % pRectData->cyClient ; nRed = rand () & 255 ; Rozdział 19: In#ertejs wielodokumen#owy 1055 , nGreen = rand () & 255 ; nBlue = rand () & 255 ; hdc = GetDC (hwnd) ; hBrush = CreateSolidBrush (RGB (nRed, nGreen, nBlue)) ; SelectObject (hdc, hBrush) ; Rectangle (hdc, min (xLeft, xRight), min (yTop, yBottom), max (xLeft, xRight), max (yTop, yBottom)) ; ReleaseDC (hwnd, hdc) ; Delete0bject (hBrush) ; i return 0 ; case WM_PAINT: // Wyczyszczenie okna InvalidateRect (hwnd, NULL, TRUE) ; hdc = BeginPaint (hwnd, &ps) ; EndPaint (hwnd,, &ps) ; return 0 ; case W1'1_MDIACTIIIATE: // Ustawienie wlaœciwego menu if (lParam == (LPARAM) hwnd) SendMessage (hwndClient, WM_MDISETMENU, (WPARAM) hMenuRect, (LPARAM) hMenuRectWindow) ; ' else SendMessage (hwndClient, WM_MDISETMENU, (WPARAM) hMenuInit, i (LPARAM) hMenuInitWindow) ; DrawMenuBar (hwndFrame) ; return 0 ; , case WM DESTROY: pRectData = (PRECTDATA) GetWindowLong (hwnd, 0) ; HeapFree (GetProcessHeap (), 0, pRectOata) ; ů KillTimer (hwnd, 1) ; return 0 : l // Przekazanie nieobslużonych kotţunikatów do DefMDIChildProc return DefMDIChildProc (hwnd, message, wParam, lParam) ; l IţDiDţMţ.RC (friţgţnty? //Microsoft Developer Studio generated resource script. #ţinclude "resource.h" ? #include "afxres.h" //////////////////////////////////////////////////////////////////////./////// // Menu I MDIMENUINIT MENU DISCARDABLE ! BEGIN POPUP "ţFile" Czţœć III: Zagadnienia zaawansowane (cišg dulszy ze strony I055) BEGIN MENUITEM "New &Hel7o`, IDM_FILE-NEWHELLO MENUITEM "New &Rectangle", IDM-FILE-NEWRECT MENUITEM SEPARATOR MENUITEM "E&xit", IDM APP EXIT END - - END MDIMENUHELLO MENU DISCARDABLE BEGIN POPUP "&File" BEGIN MENUITEM "New &Hello", IDM-FILE-NEWHELLO MENUITEM "New &Rectangle", IDMţFILĘ NEWRECT MENUITEM "&Close", IDM-FILE-CLOSE MENUITEM SEPARATOR MENUITEM "E&xit", IDM APP-EXIT END - , POPUP "&Color" BEGIN MENUITEM "&Black", IDM COLOR_BLACK MENUITEM "&Red", IDM COLOR-RED MENUITEM "&Green", IDM COLOR GREEN MENUITEM "B&lue", IDM COLOR_BLUE MENUITEM "&White", IDM COLOR WHITE END - POPUP "&Window" BEGIN MENUITEM "&Cascade\tShift+F5", IDM-WINDOW_CASCADE MENUITEM "&Tile\tShift+F4", IDM-WINDOW_TILE MENUITEM "Arrange &Icons", IDM WINDOW_ARRANGE MENUITEM "Close &All", IDM WINDOW CLOSEALL END - END MDIMENURECT MENU DISCARDABLE BEGIN POPUP "&File" BEGIN MENUITEM "New &Hello", IDM-FILE_NEWHELLO MENUITEM "New &Rectangle", IDM_FILE_NEWRECT MENUITEM "&Close", IDM FILE-CLOSE MENUITEM SEPARATOR - MENUITEM "E&xit", IDM APP-EXIT END - POPUP "&Window" BEGIN MENUITEM "&Cascade\tShift+F5", IDM_WINDOW_CASCADE MENUITEM "&Tile\tShift+F4", IDM_WINDOW TILE MENUITEM "Arrange &Icons", IDM-WINDOW_ARRANGE MENUITEM "Close &A11", IDM WINDOW CLOSEALL END - , END lllllll!lllllllllllllll!llll!llllllllllllllllllllllllllllllllllll!!ll!lllllll // Accelerator Rozdział 19: Interfejs wielodokumentowy 1057 MDIDEMO ACCELERATORS DISCARDABLE BEGIN UKţF4, IDM WINDOW_TILE: UIRTKEY, SHIFT, NOINUERT VKţF5, IDM WINDOWţCASCADE, VIRTKEY, SHIFT, NOINVERT END RESOURCE.H (firagmenty) // Microsoft Developer Studio generated include file. // Used by MDIDemo.rc ţţdefine IDM_FILE_NEWHELLO 40001 dldefine IDM FILE_NEWRECT 40002 4idefine IDM APP_EXIT 40003 ţldefine IDM_FILE CLOSE 40004 #define IDM_COLOR BLACK 40005 ţţdefine IDM_COLOR_RED 40006 ţţdefine IDM_COLOR GREEN 40007 #define IDM_COLOR BLUE 40008 ţţdefine IDM_COLOR WHITE 40009 ţţdefine IDM_WINDOW_CASCADE 40010 ţţdefine IDM WINDOW TILE 40011 ţţdefine IDM_WINDOW ARRANGE 40012 ţţdefine IDM WINDOW CLOSEALL 40013 Rysunek 19-2. Program MDIDEMO Program MDIDEMO obsługuje dwa typy bardzo prostych okien dokumentów: jeden służšcy do wyœwietlania tekstu "Hello, World!" w œrodku obszaru robo- czego, a drugi do wyţwietlania cišgu losowych prostokštów (w kodzie Ÿródło- wym oknom przypisano identyfikatory Hello i Rect). Z każdym z dwóch typów okien skojarzone jest inne menu. Okno dokumentu, w którym wyœwietlany jest napis "Hello, World!", zawiera menu pozwalajšce zmieniać kolor tekstu. Trzy menu Przeanalizujmy najpierw skrypt zasobów o nazwie MDIDEMO.RC. Zawiera on definicję trzech szablonów menu używanych w programie. Jeżeli nie sš otwarte żadne okna dokumentów, w programie aktywne jest menu MdiMenuInit. Umożliwia ono tworzenie nowych dokumentów oraz kończenie działania programu. Z oknem dokumentu wyœwietlajšcym napis "Hello, World!" jest skojarzone menu MdiMenuHello. Podmenu Plik pozwala na otwieranie nowych dokumentów obu typów, zamykanie aktywnych dokumentów i kończenie działania programu. Podmenu Kolor umożliwia ustawiarue koloru tekstu. Podmenu Okno zawiera polecenia służšce do definiowania układu oki.en dokumentów (okna można ukła- daE kaskadowo lub sšsiadujšeo) oraz układu ikon dokumentów, a także do za- mykarua wszystkich okien. To podmenu zawiera dodatkowo listę wszystkich utworzonych okien dokumentów. Częœć l: Zagadnienia zaawansowane Menu MdiMenuRect jest skojarzone z oknem, w którym sš wyœwietlane losowe prostokšty. Ma ono definicję zbliżonš do menu MdiMenuHello, tyle że nie za- wiera podmenu Kolor. Wszystkie identyfikatory menu sš jak zwykle zdefiniowane w pliku nagłówko- wym RESOURCE.H. W pliku MDIDEMO.C zdefiniowano oprócz tego trzy na- stępujšce stałe: ţţdefine INIT_MENU_POS 4ţdefine HELLO MENU_POS ţidefine RECT MENUţPOS Powyższe identyfikatory okreœlajš położenie podmenu Okno w każdym z trzech szablonów drzew menu. Dane te sš potrzebne w programie do informowania okna-klienta o miejscu, w którym ma się pojawiać wykaz dokumentów. Menu MdiMenuInit nie zawiera, oczywiœcie, podmenu Okno, diatego miejsce wykazu dokumentów zostało okreœlone jako 0 (potencjalnie pojawiajšcy się wykaz zosta- nie dopisany do pierwszego podmenu). W rzeczywistoœci żaden wykaz nie zo- stanie wygeneroţvany (dlaczego trzeba tak zrobić, okaże się daiej, gdy wyjaœnię kanstrukcję i działanie programu). Identyfikator IDM EIRSTCHILD, zdefiniowany w pliku MDIáEMO.C, nie od- powiada żadnej pozycji menu. Zostanie skojarzony z pierwszym oknem doku- mentu z wykazu, który pojawi sig w podmenu Okno. Powinien być większy niż wszystkie inne identyfikatory. Procedura WinMaţn z płiku MDIDEMO.C zaczyna się od zarejestrowania klas okien dla okna ramowego i dwóch okien potomnych. Procedury okien nazywajš się FrameWndProc, HetloWndProc i RectWndProc. Zazwyczaj z każdš klasš okien powinna być zwišzana inna ikona. Aby nie pogarszać przejrzys#aœci przykładu, zarówno dla olţa ramowego, jak i dla okien ramowyeh została użyta standardo- wa ikona IDI APPLICATION. Zauważmy, że pole hbrBackground struktury WNDCLASS dla klasy okna rţno- wego zostało zdefiniowane jako COLOR APPWORKSPACE (kołor systemowy). Nie jest to konieczne, ponţważ cały obszar roboczy okna ramowego jest przy- kryty przez okno-klienta, które i tak ma właœnie taki kolor. Zastosowanie tego koloru daje jecţak nieco lepszy rezultat, widvcţy podczas pierwszego hryœwie- tlenia vkna raxnowego. Pole tpszMenuName dla każdego z tych trzech klaţs okien ma przypisywarnš war- toœć NULL. Dła klas okien po#onnnych Heţ i ltect jQst to normalrse. Dła klasy okna ramowego postanowiłeţn podczas twvrzenia okna wskazać uchwyt rzĽenu w funkcţi CreateWindow. W klasach okien Hello i Rect przewidziane jest dodatkowe miejsce na każde okno używajšce niezerowej wartoœci pola cb Wnd^xBra struktury WNDCLASS. W miej- scu tym znajdzie się wskaŸnik odsyłajšcy do bloku pamięci (o wielkoœci rówrtej wielkoœci struktury HELLODATA lub RECTDATA, zdefţniowanych na poczš#ku Rozdział 19: Interfejs wielodokumentowy 1059 pliku MDIDEMO.C) służšcego do przechowywania prywatnych danych każde- o z okien dokumentów. g Następnie w WinMain znajduje się wywołanie funkcji LoadMenu, służšce załado- waniu trzech menu i zapisaniu ich uchwytów w zmiennych globalnych. Trzy wywołania GetSubMenu służš pozyskaniu uchwytów do podmenu menu Okno, do którego zostanie dopisany wykaz dokumentów. Pozyskane uchwyty sš także zapisywane w zmiennych globalnych. Funkcja LoadAccelerators powoduje zała- dowanie tablicy skrótów klawiaturowych. Nowe okno ramowe jest powoływane do życia poprzez wywołanie funkcji Cre- ateWindow z WinMain. Podczas działania FrameWndProc okno ramowe powołuje do życia okno-klienta. Wymaga to kolejnego wywołania funkcji CreateWindow. Klasa okna jest ustawiana na MDICLIENT, wstępnie zarejestrowanš klasę okien- klientów MDI. W klasie MDICLIENT zawarta jest większoœć obsługi MDI syste- mu Windows. Procedura okna-klienta służy jako warstwa poœredniczšca między oknem ramowym i różnymi oknami dokumentów. Wywołujšc CreateWindow w ce- lu utworzenia okna-klienta, jako ostatni argument należy podać wskaŸnik do struktury typu CLIENTCREATESTRUCT. Struktura ta ma dwa pola: ů hWindowMenu - uchwyt podmenu, do którego zostanie dołšczony wykaz do- kumentów. W MDIDEMO jest nim hMenuInitWindow, uzyskany w WinMain. Modyfikacje tego menu zostanš opisane dalej. ů idFirstChild - identyfikator menu, które ma być skojarzone z pierwszym oknem dokumentu z wykazu dokumentów. Jest nim po prostu IDM FIRSTCHILD. W procedurze WinMain program wyœwietla nowo utworzone okno ramowe i roz- poczyna przetwarzanie pętli komunikatów. Pętla ta różni się nieco od normalnej: program MDI przekazuje komunikat uzyskany z kolejki za pomocš wywołania funkcji GetMessage do TransIateMDISysAccel (i do TranslateAccelerator, jeżeli - jak MDIDEMO ma zdefiniowane skróty klawiaturowe). Funkcja TranslateMDISysAccel tłumaczy wszelkie naciœnięcia klawiszy, które mogš korespondować ze skrótami klawiaturowymi MDI (na przykład [Ctrl+F6)) na komunikaty WM SYSCOMMAND. Jeżeli TranslateMDISysAccel albo TranslateAc- celerator zwróci TRUE (co oznacza, że funkcja dokonała translacji komunikatu), nie trzeba wywoływać TransIateMessage ani DispatchMessage. Zwróć uwagę na dwa uchwyty okien przekazane do TranslateMDISysAccel i Trans- lateAccelerator, odpowiednio: hwndClient oraz hwndFrame. Funkcja WinMain uzy- skuje uchwyt okna hwndClient za poœrednictwem wywołania funkcji GetWindow z argumentem GW CHILD. Tworzenie okien potomnych Większa częœć procedury FrameWndProc jest poœwięcona przetwarzaniu komu- nikatów WM COMMAND, sygnalizujšcych wybory menu. Numer identyfika- cyjny menu zawiera jak zwykle młodsze słowo parametru wParam funkcji Fra- meWndProc. Dla numerów identyfikacyjnych menu IDM FILĘ NEWHELLO i IDM FI- LĘ NEWRECT procedura FrameWńdProc musi tworzyć nowe okna dokumentów. T 1060 Częœć III: Zagadnienia zaawansowane Konstruuje się je, inicjujšc pola struktur typu MDICREATESTRUCT (których więk- szoœć pól odpowiada argumentom CreateWindow) i wysyłajšc oknu-klientowi komunikat 4VMţMDICREATE z parametrem lParam ustawionym na wskaŸnik do tej struktury. Następnie okno-klient tworzy potomne okno dokumentu (można też skorzystać z funkcji CreateMDIWindow). Pole szTitle struktury MDICREATESTRUCT jest zazwyczaj nazwš pliku skojarzonš z dokumentem. Polu stylu można nadać jeden ze stylów WS_HSCROLL lub WS VSCROLL albo oba te style jednoczeœnie (co spowoduje umieszczenie w oknie pasków przewijarua). Pole stylu może zawierać również znacznik WS MINIMI- ZE lub WS MAXIMIZE, aby pierwotnie wyœwietlone okno dokumentu było zmi- nimalizowane lub zmaksymalizowane. Pole IParam struktury MDICREATESTRUCT zapewnia oknu ramowemu i oknu- klientowi sposób współużytkowania niektórych zmiennych. Powinno się mu nadać wartoœć wskaŸnika do bloku pamięci zawierajšcego strukturę. Podczas przetwarzania komunikatu WMţCREATE w potomnym oknie dokumentu IPa- ram jest wskaŸnikiem do struktury CREATESTRUCT, której pole IpCreateParams jest z kolei wskaŸnikiem do struktury MDICREATESTRUCT, która posłużyła do utworzenia okna. Po odebraniu komunikatu WMţMDICREATE okno-klient tworzy potomne okno dokumentu i dopisuje jego tytuł u dołu podmenu okreœlonego w strukturze MDICLIENTSTRUCT, która posłużyła do utworzenia okna-klienta. Kiedy pro- gram MDIDEMO tworzy swoje pierwsze okno dokumentu, jest to podmenu Plik menu MdiMenulnit. Jak wykaz dokumentów jest przenoszony do podmenu Okno menu MdiMenuHello i MdiMenuRect, zostanie opisane póŸniej. Wykaz dokumentów może zawierać maksymalnie 9 pozycji, z których każda jest poprzedzana automatycznie podkreœlonym numerem od 1 do 9. Jeżeli utworzo- nych zostanie więcej dokumentów, po owych dziewięciu pozycjach pojawia się jeszcze jedna, dodatkowa: Więcej okien. Wybranie jej otwiera okno dialogowe z listš wszystkich okien dokumentów. Automatyczna (praktycznie bez udziału programisty) obshzga wykazu dokumentów jest jednym z najprzyjemniejszych elementów obshzgi MDI przez Windows. Przetwarzanie komunikatu Wigcej okien Zanim zajmiemy się potomnymi oknami dokumentów, pocišgnijmy jeszcze tro- chę wštek przetwarzania komunikatów w FrameWndProc. Wybrarue pozycji Zamknij z menu Plik powoduje zamknięcie przez MDIDEMO aktywnego okna potomnego. Uchwyt aktywnego okna potomnego program uzy- skuje wysyłajšc oknu-klientowi komunikat WMţMDIGETACTIVE. Jeżeli okno potomne na komunikat WMţQUERYENDSESSION odpowie twierdzšco, wów- czas MDIDEMO wysyła oknu-klientowi komunikat 4VMţMDIDESTROY, zamy- kajšcy okno potomne. Przetwarzanie opcji Zakończ z menu Plik wymaga tylko, aby procedura okna ramowego wysłała sobie samej komunikat WMţCLOSE. Przetwarzanie opcji Sšsiadujšco, Kaskadowo i Rozmieœć ikony z podmenu Okno Rozdział 19: interfejs wielodokumentowy 1061 jest proste i wymaga tylko wysyłania do okna-klienta odpowiednio komunika- tów: 4VMţMDITILE, WM MDICASCADE i WM MDIICONARRANGE. Opcja Zamknij wszystkie jest trochę bardziej złożona, wymaga bowiem wywoła- nia z FrameWndProc funkcji EnumChildWindows i przekazania jej wskaŸnika do funkcji CloseEnumProc. Funkcja ta wysyła do każdego z okien potomnych komu- nikat WM MDIRESTORE oraz ewentualnie komunikaty WMţQUERYENDSES- SION i WM MDIDESTROY. Nie jest to realizowane dla okna zmirumalizowane- go do postaci paska tytułu, rozpoznawanego przez niepustš (różnš od NULL) wartoœć zwrotnš funkcji GetWindow wywołanej z argumentem GWţOWNER. Jak można zauważyć, w procedurze FrameWndProc nie sš przetwarzane żadne komunikaty WM COMMAND; które sygnalizujš wybranie jednego z kolorów menu Kolor. Komunikaty te sš w rzeczywistoœci obsługiwane przez okno doku- mentu. Z tego powodu procedura FrameWndProc przesyła wszellcie nieobshxżo- ne komunikaty WMţCOMMAND do aktywnego okna potomnego, aby mogło się zajšć tymi komunikatami, które go dotyczš. Wszystkie komunikaty, których procedura okna ramowego postanowi nie obsłu- giwać, muszš być przekazane do DefFrameProc. Funkcja ta zastępuje DefWindow- Proc w procedurze okna ramowego. Jeżeli procedura okna ramowego przechwy- ci choc'by jeden z komunikatów WM MENUCHAR, WM SETFOCUS lub WMţSI- ZE, i tak musi go przekazać do DefFrameProc. Do procedury DefFrameProc muszš być również przekazywane nieobsłużone ko- munikaty WM COMMAND. W szczególnoœci w procedurze FrameWndProc nie sš przetwarzane żadne komunikaty WM COMMAND, których przyczynš jest wybranie przez użytkownika któregoœ dokumentu z listy podmenu Okno (war- toœci wParam dla tych opcji zaczynajš się prefiksem IDM FIRSTCHILD). Komu- nikaty te sš przekazywane do DefFrameProc i tam przetwarzane. Zauważ, że w oknie ramowym nie jest potrzebna żadna ewidencja uchwytów okien dla tworzonych w nim dokumentów. Jeżeli kiedykolwiek uchwyty te oka- żš się potrzebne (na przykład podczas przetwarzania opcji Zamknij wszystkie), można je uzyskać za pomocš EnumChildWindows. Okna potomne, okna dokumentów Przyjrzyjmy się teraz procedurze HelloWndProc, która jest procedurš okna wyko- rzystywanš przez okna dokumentów wyœwietlajšce napis "Hello, World!" Podobnie jak w każdej klasie okna używanej dla kilku okien jednoczeœnie, zmienne statyczne zdefiniowane w procedurze okna (albo w dowolnej funkcji wywoły- wanej z procedury okna) sš wspólne dla wszystkich okien tworzonych na pod- stawie tej klasy. Dane prywatne każdego okna muszš więc być przechowywane jakoœ inaczej niż w zmiennych statycznych. Jedna ze stosowanych technik wykorzystuje właœci- woœci okien. Inna - którš ja zastosowałem - wykorzystuje miejsce w pamięci zarezerwowane przez zdefiniowanie niezerowej wartoœci w polu cbWndExtra struktury WNDCLASS, służšcej do rejestrowania klasy okna. W programie MDIDEMO miejsca tego używam do przechowania wskaŸnika do 1062 CZeœć III: 2aaadnienia bloku pamięci o rozmiarze HELLODATA. Miejsce na ten blok jest rezerwowane w pamięci przez HeIloWndProc podczas rozpatrywania komunikatu WM CRE- ! ATE; wtedy również inicjowane sš oba pola struktury (oznaczajšce obecnie wy- branš opcję menu i kolor tekstu) oraz zapamiętywany jest wskaŸnik (za pomocš SetWindowLong). Podczas przetwarzania komunikatu WM_COMMAND zmieniajšcego kolor tek- y r stu (jak pamiętam , komunikaty tego typu majš poczštek w procedurze okn„ ramowego) procedura HelloWndProc pozyskuje za pomocš GetWindowLong wskaŸ- nik do bloku pamięci zawierajšcy strukturę HELLODATA. Używajšc tej struktu- ry, HelloWndProc likwiduje zaznaczenie opcji menu, zaznacza wybranš pozycję menu i zapisuje nowy kolor. Procedura okna dokumentu odbiera komunikat WM-MDIACTIVATE wtedy, gdy okno staje się aktywne lub nieaktywne (co można rozpoznać po tym, czy lParam zawiera uchwyt okna). Jak pamiętamy, program MDIDEMO ma trzy różne menu: MdiMenuInit jest używane dla okna aplikacji pozbawionego okien dokumentów, MdiMenuHello dla aktywnego okna Hello, a MdiMenuRect dla aktywnego okna Rect. Możliwoœć modyfikowania menu gwarantuje oknu dokumentu komunikat WM MDIACTIVATE. Jeżeli IParam zawiera uchwyt okna (co oznacza, że okno staje się aktywne), wówczas HelloWndProc zmienia menu na MdiMenuHello. Je- żeli lParam zawiera uchwyt innego okna, HelloWndProc zmienia menu na Mdi- MenuInit. HelloWndProc dokonuje zmiany menu, wysyłajšc do okna-klienta komunikat WM MDISETMENU. Okno-klient przetwarza ten komunikat, usuwajšc wykaz dokumentów z bieżšcego menu i dopisujšc go do nowego menu. W ten oto spo- sób wykaz dokumentów jest przenoszony z menu MdiMenuInit (które jest włš- czone w chwili utworzenia pierwszego dokumentu) do menu MdiMenuHello. W aplikacjach MDI nie należy zmieniać menu za pomocš funkcji SetMenu. Inny ciekawy patent zwišzany jest ze znacznikami pojawiajšcymi się obok opcji podmenu Kolor. Ustawienie znacznika obok tej, a nie innej opcji, stanowi infor- mację zwišzanš z konkretnym oknem dokumentu, powinno bowiem być możli- we ustawienie czarnego koloru tekstu w jednym z okien i czerwonego w innym. Znaczniki obok pozycji menu powinny ponadto wiernie odzwierciedlać opcję wybranš dla danego okna. Z tego powodu HelloWndProc likwiduje zaznaczenie w chwili, gdy okno staje się nieaktywne, i ponownie ustawia znacznik (obok wła- œciwej pozycji) wtedy, gdy okno staje się aktywne. Wartoœci wParam i IParam komunikatu WM_MDIACTIVATE to dwa uchwyty - odpowiednio - okien dezaktywowanych i aktywowanych. Procedura okna uzy- skuje pierwszy komunikat WM_MDIACTIVATE lParam ustawiony na uchwyt okna. Kiedy zaœ okno jest niszczone, otrzymuje ostatni komunikat lParam usta- wiony na innš wartoœć. Jeżeli użytkownik przełšcza się z jednego dokumentu na inny, pierwsze okno dokumentu otrzymuje komunikat WM_MDIACTIVATE z pa- rametrem IParam ustawionym na uchwyt pierwszego okna, przy którym proce- dura okna ustawia menu na MdiMenuInit. Drugie okno dokumentu odbiera ko- munikat WM MDIACTIVATE z parametrem lParam ustawionym na uchwyt Rozdział 19: Interfejs wielodokumentowy 1063 drugiego okna, przy którym procedura okna ustawia menu odpowiednio na MdiMenuHello lub MdiMenuRect. Jeżeli wszystkie okna sš zamknięte, pozosta- je menu MdiMenuInit. Jako się rzekło, kiedy użytkownik wybierze z menu polecenie Zamknij albo Za- mknij wœży'ţtkie; procedr„ pr„meWndProc wysyła do okna potomnego komuni- kat WMţQUERYENDSESSION. HelloWndProc przetwarza komunikaty WMţQU- ERYENDSESSION i WM CLOSE, wyœwietlajšc okno komunikatu i pytajšc użyt- kownika, czy można je zamknšć (w rzeczywistym programie to okno może za- wierać pytanie o koruecznoœć zapisania pliku dokumentu). Jeżeli użytkownik zadecyduje, że okno nie może zostać zamknięte, procedura okna zwróci 0. I Podczas załatwiania komunikatu WM_DESTROY HelloWndProc zwalnia blok pamięci zarezerwowany podczas obsługi komunikatu WM CREATE. Wszystkie nieobsłużone komunikaty muszš zostać przekazane do DefMDIChild- Proc (nie do Def4VindowProc) i poddane przetwarzaniu domyœlnemu. Kilka ko- munikatów należy przekazać do DefMDIChildProc niezależnie od tego, czy pro- cedura okna potomnego ma z nimi coœ wspólnego czy nie. Sš to WMţCHILDAC- TIVATE, WMţGETMINMAXINFO, WM MENUCHAR, WM MOVE, WM SET FOCUS, WM SIZE i 4VMţSYSCOMMAND. Procedury IZectWndProc nie będę omawiał dokładniej, bo choć jest ona doœć po- dobna do HelloWndProc, zwłaszcza pod względem nadbudowy, jest od niej nieco prostsza (nie ma mowy o żadnych opcjach menu, użytkownik nie jest pytany o zgodę na zamknięcie okna). Warto jednak zauważyć, że ftectWndProc rozgałę- zia się po obsłużeniu WMţSIZE, aby komunikat mógł trafić DefMDIChildProc. Sprzštanie W procedurze WinMain programu MDIDEMO trzy menu zdefiniowane w skrypcie zasobów sš ładowane za pomocš LoadMenu. Zwykle Windows niszczy menu pod- czas likwidowania okna, z którym jest ono skojarzone. Dotyczy to menu Init. Ist- niejš jednak menu niepodłšczone do żadnego okna, które powinny być niszczo- ne jawnie. Z tego powodu w MDIDEMO znajdujš się dwa wywołania Destroy- Menu pod koniec procedury WinMain - za ich pomocš program pozbywa się menu Hello i Rect. Rozdział 20 , . , , z mowosc Wţloaa e .. " w k wosc m I to eo Wielowštkowoœć to cecha systemu operacyjnego polegajšca na możliwoœci jed- noczesnego (współbieżnego, równoległego) uruchamiania programów. W rzeczy- I wistoœci system operacyjny komputera z jednym procesorem przydziela poszcze- gólnym procesom (działajšcym współbieżrue programom) "porcje czasu". Jeżeli kwanty czasu sš wystarczajšco małe - a komputer rue jest obłożony zbyt wielo- ma programami - użytkownik może odnosić wrażerue, że wszystkie programy J działajš równolegle. i Wielozadaniowoœć nie jest niczym nowym. W dużych komputerach typu main- frame cecha ta jest powszechna. Do systemów takich podłšczone sš często dzie- sištki terminali, a każdy użytkowruk terminala ma wrażenie, że dysponuje zaso- bami systemu na wyłšcznoœć. Systemy operacyjne mainframe pozwalajš również I swoim użytkownikom umieszczać zadania w tle" dzie s kon ane rzez " , g š ţ'ţi' 5'H' P komputer w sposób nie angażujšcy uwagi użytkownika, pozwalajšcy mu uru- ' chamiać inne zadania. Wielozadaniowoœć w komputerach osobistych pojawiała się długo i rodziła się w bólach. Teraz często uznajemy jš w komputerach PC za coœ oczywistego. Jak napiszę nieco dalej, 16-bitowe wersje Microsoft Windows obsługiwały wieloza- daniowoœć, ale w bardzo ograniczonym zakresie. Wszystkie 32 bitowe wersje Win- dows obsługujš zarówno prawdziwš wielozadaniowoœć, jak i inne dobrodziej- stwo - wielowštkowoœć. Wielowštkowoœć to wielozadaruowoœć na poziomie jednego programu. Program może być podzielony na równolegle wykonywane podprogramy - wštki. Techni- ka wielowštkowego działania programów może poczštkowo wydawać się mało przydatna, okazuje się jednak, że warto jš stosować podczas wykonywarua dhz- go trwajšcych zadań, aby nie zmuszać użytkownika do robienia przerw w pracy. Czasem oczywiœcie może to być niewskazane: wycieczka do wentylatora albo do okna poprawia kršżerue i sprzyja zdrowiu! Tak czy owak, użytkownikowi nie powinno się odbierać (choćby czasowo) możliwoœci wykonywania żadnych za- dań w jego komputerze, nawet gdy i tak wiadomo, że z ruej nie skorzysta. 1066 Częœć III: Zagadnienia zaawansowane Historia wielozadaniowoœci Kiedy rodziły się komputery PC, wiele osób popierało wielozadaniowoœć, ale też wiele zadawało sobie pytanie o przeciwnym wydŸwięku: po co wielozadanio- woœć w komputerze osobistym przeznaczonym dla jednego użytkownika? Cóż, okazało się, że wielozadaniowoœci chcieli nawet i ci użytkownicy, którzy nie mieli o niej zielonego pojęcia. wielozadaniowoœć w systemie DOS? Mikroprocesor Intel 8088, używany w pierwszych komputerach PC, nie był pro- jektowany jako serce systemu wielozadaniowego. Zasadniczy problem miał zwiš- zek z niewłaœciwym zarzšdzaruem pamięciš. Kiedy uruchamiane sš i wyłšczane kolejne programy, wielozadaniowy system operacyjny powinien wkraczać do akcji i przesuwać (konsolidować) bloki pamięciowe, zwalniajšc większe cišgłe obsza- ry pamięci. Na 8088 nie można tego zrobić w sposób przezroczysty dla aplikacji. Sam DOS nie pomógł. Zaprojektowany był jako mały system, który miał nie wcho- dzić w drogę aplikacjom; obsługiwał niewiele ponad podstawowe funkcje łado- wania programów i mechanizmy dostępu do systemu plików. Utalentowani programiœci z czasów, gdy na pecetach królował DOS, znaleŸli jed- nak pewien sposób pokonania jednozadaniowoœci: programy rezydentne (termi- nate-and-stay-resident, TSR). Niektóre z nich, na przykład bufory drukowania, podpinały się pod przerwarua sprzętowego zegara i w procedurach obshxgi prze- rwań wykonywały zadania uruchomione w tle. Inne, takie jak SideKick, były w stanie wykonywać pewnego rodzaju przełšczanie zadań - zawieszały działa- nie aplikacji na czas uruchamiarua własnego kodu. DOS został również dobrze wyposażony w narzędzia wspomagajšce pracę programów rezydentnych TSR. Niektórzy producenci podejmowali próby opracowania własnych powłok wielo- zadaniowych, działajšcych jako nakładki na DOS (przykładem niech będzie De- sqView firmy Quarterdeck), ale tylko jeden z tych produktów ostatecznie zyskał sobie duży udział w rynku. Chodzi oczywiœcie o Windows. wielozadaniowoœć bez wywłaszczania Kiedy Microsoft w roku 1985 wprowadził Windows 1.0, było to najbardziej za- awansowane rozwišzanie, jakie powstało, majšce wyrugować ograruczenia na- kładane przez DOS. Wtedy też Windows wkroczył w œwiat trybu rzeczywistego , ale mimo to przenoszenie bloków pamięci w pamięci fizycznej - warunek reali- zowania wielozadaniowego systemu operacyjnego - nie odbywało się całkowi- cie przezroczyœcie dla aplikacji - niemniej było zaimplementowane znoœnie. Wielozadaniowoœć ma większy sens w graficznym œrodowisku z oknami niż w sys- temie z liniš poleceń, na dodatek przeznaczonym dla tylko jednego użytkowni- ka. Na przykład w systemie UNIX, klasycznym systemie wielozadaniowym, z li- rui poleceń można uruchamiać programy działajšce następnie w tle. Jednak wy- nik działania takiego programu musi być kierowany do pliku, a nie na ekran, gdzie utworzyłby mozaikę z tym, co robi akurat użytkownik. Rozdział 20: Wielozadaniowoœć i wielowštkowoœć 1067 Œrodowisko z oknami umożliwia uruchamianie na jednym ekranie jednoczeœnie wielu programów. Przełšczanie się tam i z powrotem staje się trywialne; możli- we jest również szybkie przenoszenie danych między programami (obraz gra- ficzny utworzony w programie rysunkowym można na przykład umieœcić w pli- j ku tekstowym obsługiwanym przez edytor tekstów). Transfer danych został w Windows zrealizowany na różne sposoby, poczštkowo poprzez Schowek, po- tem przez DDE (Dynamic Data Exclurnge), a obecnie przez OLE (Object Linking and Embedding). Wielozadaniowoœć zaimplementowana we wczesnych wersjach Windows nie była jednak typowa, czyli taka jak w tradycyjnych systemach operacyjnych obsługu- jšcych wielu użytkowników. W systemach tradycyjnych poszczególne zadania były co pewien interwał (wyznaczany przerwaniami zegara sprzętowego) prze- rywane i przełšczane. 16-bitowe wersje Windows były wyposażone w coœ, co nazywało się wielozadaniowoœciš bez wywłaszczania. Ten typ wielozadaniowo- œci był możliwy do zrealizowania ze względu na opartš na komunikatach archi- tekturę Windows. W ogólnym przypadku program uruchomiony w Windows pozostaje uœpiony w pamięci, aż otrzyma jakiœ komunikat. Komunikaty bywajš bezpoœrednimi i poœrednimi wynikami działalnoœci użytkownika, a konkretnie poruszania przez niego myszš i naciskania klawiszy. Po "załatwieniu" komuni- j katu program zwraca sterowanie do Windows. 16-bitowe wersje Windows nie przełšczały sterowarua arbitralnie między progra- mami-klientami na podstawie przerwań zegara. Przełšczanie zadań odbywało się wtedy, gdy dany program skończył obsługiwanie otrzymanego komunikatu i zwrócił sterowanie do Windows. Ta technika wielozadaniowoœci bez wywłasz- czania jest również nazywana "wielozadaniowoœciš kooperacyjnš", ponieważ wymaga pewnej współpracy ze strony aplikacji. Jeden niepokorny program Win- dows mógł zablokować cały system, jeżeli zbyt długo przetwarzał jakiœ komuni- kat. Choć regułš w 16-bitowych wersjach Windows była wielozadaniowoœć bez wy- właszczania, systemy te miały pewne zalšżki form wielozadaruowoœci z wywłasz- czaniem. W Windows wielozadaniowoœć z wywłaszczaniem służyła do urucha- miania programów DOS i była dozwolona bibliotekom dynamicznym, które mogły odbierać przerwania zegarowe i za ich pomocš synchronizować multimedia. 16-bitowe wersje Windows zawierały kilka funkcji pomocnych programistom przy rozwišzywaniu problemów zwišzanych z wielozadaniowoœciš bez wywłaszcza- nia - a przynajmniej eliminowaniu pewnych mniejszych jej ucišżliwoœci. Naj- większym utrapieniem użytkowników był wskaŸnik myszy z klepsydrš. Nie było to, oczywiœcie, żadne rozwišzanie, ale sposób informowania użytkownika o tym, że program jest zajęty i że pracuje nad poważniejszym zadaniem, czytaj: będzie przez dłuższš chwilę niedostępny. Innš prowizorkę Windows, zegar, można było zaprogramować tak, aby jakiœ program otrzymał komunikaty w okreœlonych in- terwałach. Zegar często wykorzystywano do synchronizowania aplikacji wyko- rzystujšcych animację. Innym rozwišzaruem łatajšcym dziury systemu z wielozadaniowoœciš bez wy- właszczania jest funkcja PeekMessage, przedstawiona w rozdziale 5, w programie 1068 Częœć III: Zagadnienia zaawansowane RANDRECT. Program do odbierania kolejnych komurukatów z kolejki używa zazwyczaj wywołarua GetMessage. Jeżeli jednak nie ma żadnych kolejnych komu- nikatów, funkcja GetMessage nie kończy swojego działania, blokujšc cały program do czasu pojawienia się jakiegoœ komunikatu. Antidotum okazuje się właœnie funkcja PeekMessage, która zwraca sterowarue do programu niezależnie od tego, czy jakiœ komunikat został odebran cz nie. Pro ram może wi c w kon ać I y y g ę y yw długie zadania i włšczać w nie wywołarua PeekMessage. W ten sposób w czasie, gdy nie ma żadnych komunikatów, którymi miałby się zajšć program, mogš być wykonywane jakieœ czasochłonne zadarua. Menedżer prezentacji i szeregowa kolejka kómunikatów Pierwszš podjętš przez Microsoft (we współpracy z firmš IBM) próbš zaimple- mentowania wielozadaniowoœci w œrodowisku DOS/Windows był system OS/2 i jego składnik Presentation Manager (PM - Menedżer prezentacji). System OS/2 obsługiwał rzeczywistš wielozadaniowoœć z wywłaszczaruem, często wydawało się, że tym wywłaszczaruem obarczono program Presentation Manager. Problem polegał na tym, że PM szeregował komunikaty wejœciowe z klawiatury i myszy, tworzšc szeregowš kolejkę komunikatów (ang. serialized message guegue). Ozna- cza to, że nie mógł dostarczać komunikatów z klawiatury ani z myszy do pro- gramu, dopóki obsługa poprzedruego komunikatu wejœciowego nie zakończyła się sukcesem. Choć komunikaty klawiatury i myszy stanowiš tylko częœć wszystkich komuni- katów, jakie program PM (lub Windows) może otrzymać, większoœć pozostałych stanowiły komunikaty będšce poœredriim skutkiem zdarzeń zwišzanych z kla- wiaturš i myszš. Na przykład komunikat wybrania polecenia menu jest skutkiem tego, że użytkownik wybrał opcję menu, a mógł to zrobić tylko za pomocš my- szy bšdŸ klawiatury. Komunikat klawiatury lub myszy nie będzie więc w pełni obsłużony, dopóki nie zostarue obsłużony komunikat polecenia menu. Głównym powodem szeregowania komunikatów w kolejce było umożliwienie przewidywania operacji wykonywanych przez użytkownika za pomocš klawia- tury (type-ahead) i myszy (mouse-ahead). Jeżeli jeden z komunikatów klawiatury lub myszy powodował przełšczenie fokusu z jednego okna do drugiego, kolejne komunikaty klawiaturowe powinny były trafiać do tego, które otrzymało fokus. Jak widać, system nie wie, gdzie ma wysłać kolejne komunikaty wejœciowe od użytkownika, dopóki nie obsłuży wszystkich wczeœruejszych. Dziœ rueodłšcznym atrybutem dobrego systemu operacyjnego jest to, że żadna pojedyncza aplikacja rue może zablokować całego systemu, a to wymaga niesze- regowej kolejki komunikatów - takiej, jakš zrealizowano w 32-bitowych wersjach Windows. Jeżeli jakiœ program jest zajęty wykonywaniem czasochłonnego zada- nia, fokus wejœciowy można przełšczyć na inny. Rozwišzanie wielowštkowe Wspominałem o Menedżerze prezentacji z systemu OS/2 tylko dlatego, że sta- nowił on œrodowisko, które zapewruło weteranom programowania w Windows Rozdział 20: Wielozadaniowoœć i wielowštkowoœć 1069 (do jakich się zaliczam) pierwszy kontakt z wielowštkowoœciš. Najciekawsze, że właœnie ograniczenia implementacji Menedżera prezentacji w dziedzinie wielo- wštkowoœci dały programistom do myœlenia i że za ich sprawš opracowano za- łożenia, jakie musi spełniać poprawnie funkcjonujšca architektura programów wielowštkowych. Choć ograniczenia te zostały w dużej mierze wyeliminowane w 32-bitowych wersjach Windows, wnioski wycišgnięte na podstawie działania tych prostszych œrodowisk operacyjnych sš wcišż bardzo na czasie. Zacznijmy więc. W œrodowisku wielowštkowym programy mogš się dzielić na osobne kawałki , nazywane "wštkami" i działajšce współbieżnie. Obsługa wštków okazała się najlepszym lekarstwem na szeregowanie kolejki komunikatów w Menedżerze prezentacji i obecnie spełnia ważnš rolę w Windows. Z punktu widzenia programisty wštek jest reprezentowany przez funkcję, która może również wywoływać inne funkcje programu. Program zaczyna działanie jako główny (i poczštkowo jedyny) wštek, który w tradycyjnym programie C jest funkcjš main, a w Windows WinMain. Działajšcy już program może powoływać do życia nowe wštki, wywołujšc specjalnš funkcję (CreateThread), której podaje nazwę pierwszej funkcji Thread. System operacyjny przełšcza sterowanie między wštkami na zasadach wywłaszczania, podobnie jak w rasowym systemie z prze- łšczaniem zadań. W Menedżerze prezentacji systemu OS/2 każdy wštek może utworzyć kolejkę komunikatów, ale nie musi. Wštek PM musi utworzyć kolejkę, jeżeli chce, aby z tego wštku były tworzone okna. W przeciwnym razie (jeżeli program miał na przykład tylko prowadzić jakieœ obliczerua albo wyœwietlać grafikę) tworzenie kolejki jest zbędne. Ponieważ wštki bez kolejki komunikatów nie zajmujš się ob- sługš komunikatów, nie mogš zawiesić systemu. Jedynym ograniczeniem jest to, że pozbawiony kolejki wštek nie może wysyłać komunikatu do okna utworzo- nego w wštku z kolejkš ani wywołać żadnej funkcji powodujšcej wysłanie takie- go komunikatu (może jednak wysyłać komunikaty do samego wštku z kolejkš). Tak więc programiœci piszšcy "pod Menedżer prezentacji" nauczyli się dzielić swoje programy na jeden wštek z kolejkš komunikatów, który otwierał wszystkie okna i przetwarzał zwišzane z nimi komunikaty, oraz jeden lub więcej wštków bez wła- snych kolejek komurukatów, przeznaczonych do wykonywania czasochłonnych operacji w tle. Programiœci ci nauczyli się również stosować zasadę "jednej dziesiš- tej sekundy", mówišcš, że wštek z kolejkš komurukatów nie powiruen przezna- czać więcej czasu na przetwarzanie komunikatów niż jedna dziesišta sekundy. Wszystko, co ma trwać dłużej, powinno być wykonywane w osobnym wštku. Je- żeli wszyscy programiœci postępowaliby zgodnie z tš regułš, żaden program Me- nedżera prezentacji nie zawiesiłby systemu na dłużej niż jedna dziesišta sekundy. Architektura wielowštkowa Napisałem, że ograniczenia Menedżera prezentacji dostarczyły programistom cennych wskazówek, jak posługiwać się wštkami w programach działajšcych w œrodowiskach graficznych. Oto co polecam jako architekturę dla programów wielowštkowych: wštek główny powinien tworzyć wszystkie okna używane w 1070 Częœć III: Zagadnienia zaawansowane programie, łšcznie ze wszystkimi stosownymi procedurami okien, i przetwarzać wszystkie zwišzane z nimi komunikaty. Resztš działania programu powinny zajmować się pozostałe wštki, przy czym nie mogš one wymieniać informacji z użytkownikiem, chyba że poprzez komunikację z wštkiem głównym. Ujmujšc to prosto, główny wštek ma się zajmować obsługš wejœcia od użytkow- nika (i innymi komunikatami), tworzšc ewentualnie wštki poboczne, realizujšce konkretne zadania. Owe wštki poboczne majš pełnić funkcje niezwišzane z za- daniami użytkownika. Inaczej mówišc, wštek główny programu jest kierownikiem, a wštki poboczne jego pracownikami. Kierownik zleca wszystkie poważne zadania swoim pracow- nikom, utrzymujšc kontakt z nimi i ze œwiatem zewnętrznym. Pracownicy nie mogš jednak prowadzić własnych konferencji prasowych. Sumiennie i z dala od fleszy aparatów reporterskich wykonujš zlecone przez szefa prace, a kiedy je kończš, czekajš na nowe. Wštki stanowiš integralnš częœć programu, więc mogš korzystać z jego zasobów, takich jak pamięć i otwarte pliki. Mogš używać również wspólnych zmiennych statycznych. Każdy z nich ma jednak swój własny stos, dlatego zmienne automa- tyczne sš prywatne dla każdego wštku. Każdy wštek ma swój własny rekord ze stanem procesora (oraz rekord ze stanem koprocesora), zapisywany i odtwarza- ny podczas przełšczania wštków. Ucišżliwoœć wštków Poprawne zaprojektowanie, zakodowanie i zdebugowanie skomplikowanej apli- kacji wielowštkowej to z pewnoœciš jedno z najtrudniejszych zadań, przed jaki- mi stajš programiœci. Ponieważ wielozadaniowy system z wywłaszczaniem może przerwać każdy wštek w dowolnym punkcie i przekazać sterowanie do innego wštku, wszelkie niepożšdane interakcje między oboma wštkami mogš ujawniać się nie od razu, losowo i efemerycznie. Jeden z częstszych błędów występujšcych w programach wielowštkowych jest nazywany wyœcigami. Do wyœcigów dochodzi wtedy, gdy programista zakłada, że jakiœ wštek ukończy pracę nad czymœ - na przykład nad przygotowywaniem danych - zanim inny wštek będzie owych danych potrzebował. Aby pomóc w koordynacji działań wštków, systemy operacyjne wymagajš różnych form syn- chronizacji. Jednym z narzędzi służšcych synchronizacji jest tzw. semafor. Pozwala on programiœcie blokować wykonanie jakiegoœ wštku w okreœlonym punkcie kodu do czasu, kiedy inny wštek da mu na to pozwolenie. Do semaforów podobne sš sekcje krytyczne - fragmenty kodu, których system operacyjny nie może prze- rwać. Z semaforami wišţ:e się jeszcze inne niebezpieczeństwo: zakleszczenie. Docho- dzi do niego wtedy, gdy dwa wštki blokujš się wzajemnie i każdy mógłby zostać odblokowany tylko wtedy, gdyby drugi się wykonał. Na szczęœcie programy 32-bitowe sš bardziej odporne na pewne problemy zwiš- zane z wštkami niż 16-bitowe. Załóżmy, że jakiœ wštek wykonuje prostš instruk- c7ę lCount++; Rozdział 20: Wielozadaniowoœć i wielowštkowoœć 1071 gdzie lCount jest 32-bitowš zmiennš globalnš używanš przez inny wštek. W pro- gramie 16-bitowym ta pojedyncza instrukcja C jest kompilowana na dwie instruk- cje kodu maszynowego: pierwsza zwiększa młodsze 16 bitów zmiennej, a druga jej starsze 16 bitów. Załóżmy dalej, że system operacyjny przerwał wštek wła- œnie w punkcie między tymi dwoma instrukcjami. Jeżeli zmienna lCount miałaby poczštkowo wartoœć 0x0000FFFF, to po wystšpieniu przerwania wštku jej war- toœć wyniosłaby zero - przynajmniej takš wartoœć odczytałby inny wštek. Po wznowieniu przerwanego wštku program dokończyłby zwiększanie lCount, która ostatecznie przyjęłaby poprawnš wartoœć 0x00010000. To był przykład jednego z błędów, które mogš powodować problemy tak rzadko, że mogš długo umykać niezauważone. W programie 16-bitowym właœciwym spo- sobem rozwišzania tego problemu jest umieszczenie instrukcji inkrementacyjnej w sekcji krytycznej (podczas której wštek nie może być przerywany). W progra- mie 32-bitowym instrukcja inkrementacji może pozostać bez zmian, ponieważ w wyniku kompilacji powstaje tylko jedna instrukcja kodu maszynowego. Przewaga Windows 32-bitowe wersje Windows (w tym Microsoft Windows NT i Windows 98) majš nie- szeregowe kolejki komunikatów. Implementacje tych kolejek sš bardzo dobre: gdy obsługa komunikatu zajmuje programowi więcej czasu, wskaŸnilc myszy zmienia się w klepsydrę, jeœli znajduje się nad obszarem roboczym okna programu, albo pozo- staje normalnš strzałkš, jeœli znajduje się nad obszarem innego okna programu. To drugie okno można przenieœć na pierwszy plan za pomocš prostego kliknięcia. Użytkownik nadal jednak nie może pracować z programem wykonujšcym cza- sochłonne zadanie, ponieważ nie pozwala ono programowi odbierać innych ko- t munikatów. Jest to efekt niepożšdany. Program powinien być zawsze otwarty na komunikaty, a to często wymaga stosowania wštków pobocznych. ( W Windows NT i Windows 98 nie ma rozróżnienia między wštkami z kolejkš i wštkami bez kolejki. Każdy wštek otrzymuje swojš kolejkę w chwili, gdy po- wstaje. Eliminuje to pewne dziwne reguły obowišzujšce wštki z programów PM. W większoœci przypadków komunikaty wejœciowe i tak sš przetwarzane za po- mocš procedur jednego wštku, a zadania czasochłonne - delegowane do innych wštków, tych nie obsługujšcych okien. Jak się wkrótce okaże, taka struktura w większoœci przypadków się sprawdza. A oto i więcej dobrych wieœci: Windows NT i Windows 98 zawierajš funkcję umożliwiajšcš jednemu wštkowi zabicie innego wštku tego samego procesu. Jak się okaże podczas pisania kodu wielowštkowego, jest to czasem wygodne. Pierw- sze wersje OS/2 nie zawierały funkcji typu "zabij wštek". ( Ostatniš dobrš nowinš (przynajmniej na ten temat) jest to, że w Windows NT i Windows 98 zaimplementowano lokalnš pamięć wštku (thread local storage, TLS). Aby to pojšć, przypomnij sobie, jak pisałem wczeœniej, że zmienne statyczne, za- równo globalne, jak i lokalne dla funkcji, sš wspólne dla wštków, ponieważ sš ulokowane w przestrzeni adresowej procesu. Zmienne automatyczne (które sš zawsze lokalne dla funkcji) sš prywatne dla każdego wštku, ponieważ zajmujš przestrzeń na stosie, a każdy wštek ma swój osobny stos. T Częœć III: Zagadnienia zaawansowane Czasem jest wygodnie, gdy kilka wštków korzysta z tej samej funkcji; dobrze jest również, gdy wštki wykorzystujš zmienne statyczne prywatne dla wštków. Takš funkcję pełru właœnie pamięć lokalna. Wišże się z niš kilka nowych funkcji Win- dows, ale Microsoft dodał również do kompilatora C rozszerzenie, dzięki które- mu korzystanie z TLS jest bardziej przezroczyste dla programisty. Nowoœţ! Ulepszona formuła! Dostępne z wštkami! Teraz, kiedy już rozdmuchałem kwestie wštków, umieœćmy je w odpowiedniej perspektywie. Niektórzy programiœci majš tendencję do używania każdej funk- cji, jakš dany system operacyjny ma do zaoferowania. Z gorszym przypadkiem masz do czynienia, kiedy do twojego biurka podchodzi szef i mówi "Słyszałem, że ta nowa funkcja Jakjejtam jest naprawdę œwietna. Może byœmy zastosowali jš w naszym programie?". Spędzasz więc cały tydzień na rozpracowaniu, co do- brego funkcja jakjejtam może ewentualnie zrobić dla twojej aplikacji. Chodzi o to, że nie ma sensu wyposażać w wielowštkowoœć programu, który się œwietnie sprawuje bez niej. Niektóre aplikacje po prostu nie nadajš się do stoso- wania rozwišzań wielowštkowych. Jeżeli twój program wyœwietla wskaŸnik myszy z klepsydrš przez niepokojšco dhxgi czas albo jeżeli posługuje się wywo- łaniem PeekMessage w celu uniknięcia wskaŸnika z klepsydrš, wówczas zrefor- mowanie go i wyposażenie w wštki będzie z pewnoœciš rozsšdnš propozycjš. W przeciwnym jednak razie po prostu utrudniasz sobie życie i całkiem możliwe, że wprowadzasz do kodu nowe błędy. Istnieje nawet kilka przypadków, kiedy wskaŸnik z klepsydrš może być całko- wicie uzasadniony. Wspomniałem wczeœniej o zasadzie jednej dziesištej sekun- dy. Cóż, ładowanie dużego pliku do pamięci zajmuje więcej niż jednš dziesištš sekundy. Czy oznacza to zatem koniecznoœć tworzenia osobnego wštku ładujš- cego pliki? Niekoniecznie. Kiedy użytkownik nakazuje programowi otworzyć plik, chce zazwyczaj, aby żšdana operacja została zrealizowana od razu. Umieszcze- nie procedur ładujšcych pliki w osobnych wštkach spowodowałoby zbędne opóŸ- nienia. Po prostu nie warto, nawet jeżeli chcesz się pochwalić przed kolegami, że piszesz programy wielowštkowe! Wielowštkowoœć w Windows Funkcja API tworzšca nowy wštek nosi nazwę CreateThread. Ma ona następujšcš składnię: hThread = CreateThread (&security „ttributes, dwStackSize, ThreadProc, pParam, dwFlags, &idThread): Jej pierwszym argumentem jest wskaŸnik do struktury typu SECURIT'YţATTRI- BUT'ES. Ten argument jest ignorowany w Windows 98. Można mu również nadać wartoœć NULL w Windows NT. Drugi argument jest poczštkowš wielkoœciš sto- su dla nowego wštku; można mu przypisać 0, aby otrzymać wartoœć domyœlnš. Windows i tak dynamicznie modyfikuje długoœć stosu, jeżeli to konieczne. Trzeci argument CreateThread to wskaŸnik do funkcji Thread. Funkcja może mieć dowolnš nazwę, ale musi mieć składnię: Rozdział 20: Wielozadaniowoœć i wielowštkowoœć 1073 DWORD WINAPI ThreadProc (PVOID pParam); Czwarty argument CreateThread staje się argumentem ThreadProc. Oto jak wštek główny może współużytkować dane. Pištym argumentem CreateThread jest z reguły 0, ale można podać znacznik CRE- ATĘ SUSPENDED, jeżeli wštek ma zostać utworzony, ale nie od razu urucho- miony. Wštek pozostanie zawieszony do czasu wywołania ResumeThread. Szósty argument to wskaŸnik do zmiennej, która otrzyma wartoœć identyfikatora wšt- ku. Większoœć programistów Windows woli używać funkcji beginthread z biblioteki czasu wykonywania języka C, deklarowanej w pliku nagłówkowym PROCESS.H. Funkcja ma następujšcš składnię: hThread = beginthread (ThreadProc, uiStackSize, pParam); Jest ona nieco prostsza i w większoœci aplikacji całkowicie wystarczajšca. Funk- cja Thread ma następujšcš składruę: void ţcdecl ThreadProc (void * pParam); Losowe prostokšty raz jeszcze Program RNDRCTMT pokazany na rysunku 20-1 jest wielowštkowš wersjš pro- gramu RANDRECT z rozdziału 5. Jak pamiętamy, w RANDRECT użyta została pętla PeekMessage, w której wyœwietlany był cišg losowych prostokštów. RNDRCTMT.C /* */ ţţinclude ţţinclude LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; HWND hwnd ; int cxClient, cyClient ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) RNDRCTMT.C - Losowe prostokdty (c) Charles Petzold, 1998 static TCHAR szAppNameC) = TEXT ("RndRctMT") MSG msg ; WNDCLASS wndclass : wndclass.style = CS_HREDRAW ţ CSţVREDRAW wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) wndclass.hCursor = LoadCursor (NULL, IDC ARROW) ; 1074 Częœć III: Zagadnienia zaawansowane (cišg dalszy ze strony 1073) wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITĘ BRUSH) ; wndclas5.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) i Messa9eBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MBţICONERROR) ; return 0 ; ) hwnd = CreateWindow (szAppName, TEXT ("Random Rectangles"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW USEDEFAULT, CW_USEDEFAULT, CW USEDEFAULT,. , NULL, NULL, hInstance, NULL) : ShowWindow (hwnd, iCmdShow) ; ' UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) ( TranslateMessage (&msg) ; i DispatchMessage (&msg) ; ) return msg.wParam ; ) VOID Thread (PVOID pvoid) ( HBRUSH hBrush ; HDC hdc ; ' int xLeft, xRi9ht, yTop, yBottom, iRed, iGreen, iBlue ; while (TRUE) ( if (cxClient != 0 ţţ cyClient != 0) ( xLeft = rand () % cxClient : xRight = rand () % cxClient ; yTop = rand () % cyClient ; yBottom = rand () % cyClient ; iRed = rand () & 255 ; iGreen = rand () & 255 : iBlue = rand () & 255 : hdc = GetDC (hwnd) ; hBrush = CreateSolidBrush (RGB (iRed, iGreen, iBlue)) ; SelectObject (hdc, hBrush) ; ů Rectangle (hdc, min (xLeft. xRight), min (yTop, yBottom), max (xLeft, xRight), max (yTop, yBottom)) ; ReleaseDC (hwnd, hdc) ; Delete0bject (hBrush) ; ) , Rozdział 20: Wielozadaniowoœć i wielowštkowoœć 1075 ) LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ( switch (message) ( case WM_CREATE: ţbeginthread (Thread, 0, NULL) ; return 0 : case WM SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; return 0 : case WM DESTROY: PostQuitMessage (0) ; return 0 ; ) return DefWindowProc (hwnd, message, wParam, lParam) ; ) Rysunek 20-1. Program RNDRCTMT Za każdym razem, kiedy przygotowuje się wielowštkowy program Windows, trzeba zmienić pewne opcje w oknie dialogowym ustawień projektu. Uaktywnij kartę C/C++ i wybierz opcję Code Generation z pola Category. W polu Run-Time Library powinny się pojawić konfiguracje Single-Threaded for the Release i De- bug Single-Threaded for the Debug. Zmień je odpowiednio na Multithreaded i De- bug Multithreaded. Spowoduje to zmianę opcji kompilatora na /MT, niezbędnš do kompilowania aplikacji wielowštkowych. Uruchomiony z tš opcjš kompila- tor wstawia w pliku OBJ odniesienie do pliku LIBCMT.LIB, a nie do LIBC.LIB. Konsolidator używa tej nazwy do konsolidacji z funkcjami bibliotecznymi czasu wykonywania. Pliki LIBC.LIB i LIBCMT.LIB zawierajš funkcje biblioteczne C. Niektóre z nich majš własne dane statyczne. Na przykład funkcja strtok jest pomyœlana tak, aby jš wywoływać kilka razy z rzędu, i pamięta wskaŸnik do pamięci statycznej. Dla- tego każdy wštek programu wielowštkowego musi mieć w funkcji strtok własny wskaŸnik statyczny. Wielowštkowa wersja tej funkcji strtok będzie więc nieco inna niż jednowštkowa. Można również zauważyć, że w pliku RNDRCTMT.C włšczany jest plik nagłów- kowy PROCESS.H, zawierajšcy deklarację funkcji beginthread, która uruchamia nowy wštek. Funkcja ta nie jest deklarowana, jeżeli nie zostanie zdefiniowany identyfikator MT, a to z kolei jest skutkiem użycia opcji /MT kompilatora. Wartoœć hwnd zwrócona z CreateWindow w funkcji WinMain pliku RNDRCTMT.C zapisywana jest w zmiennej globalnej. To samo dotyczy wartoœci cxClient i cyClient uzyskiwanych z komunikatu WMţSIZE w procedurze okna. Procedura okna wywołuje beginthread w najprostszy możliwy sposób, czyli tyl- ko z adresem funkcji wštkowej (o nazwie Thread) jako pierwszym argumentem i z zerami jako argumentami pozostałymi. Funkcja wštkowa zwraca VOID i ma argument będšcy wskaŸnikiem do VOID. Funkcja Thread z RNDRCTMT nie ma tego argumentu. T 1076 Częœć III: Zagadnienia zaawansowane Od chwili wywołania beginthread kod tej funkcji (razem z kodem z innych wy- woływanych ewentualnie przez niš funkcji) rozpoczyna działanie współbieżnie z pozostałym kodem programu. Tej samej funkcji może używać kilka wštków tego samego procesu. W tym przypadku automatyczne zmienne lokalne (pamiętane na stosie) sš prywatne dla każdego wštku; wszystkie zaœ zmienne statyczne sš wspólne wszystkim wštkom danego procesu. W taki właœnie sposób procedura okna może nadawać wartoœci zmiennym globalnym cxClient i cyClient, a funkcja Thread z nich korzystać. Czasem potrzebne sš trwałe dane prywatne dla więcej niż jednego wštku. Zwy- kle trwałe dane sš zwišzane ze zmiennymi statycznymi, ale w Windows 98 ist- nieje możliwoœć korzystania z danych TLS, o których wspomniałem i które omó- wię szczegółowiej w dalszej częœci tego rozdziału. Zadanie z konkursu programistycznego Microsoftu Trzeciego paŸdziemika 1986 roku firma Microsoft zorganizowała trwajšcy cały dzień briefing dla redaktorów technicznych oraz autorów ksišżek i artykułów do maga- zynów informatycznych. Konferencja poœwięcona była prezentacji bieżšcych pro- duktów języków programowania, między innymi pierwszemu interakcyjnemu œro- dowisku programistycznemu QuickBASIC 2.0. Windows 1.0 miał wtedy mniej niż rok i nikt nie wiedział, czy kiedykolwiek pojawi się analogiczny produkt dla tego œrodowiska (trzeba było poczekać kilka lat). Na konferencji miało miejsce jedno cie- kawe wydarzenie: pracownicy public relations Microsoftu przygotowali tumiej pro- gramistyczny pod tytułem "Storm the Gates" (pokonaj Gatesa). Bill Gates miał używać programu QuickBASIC 2.0, a ochotnicy rekrutowani spoœród uczestników konferencji mieli do dyspozycji dowolny przyniesiony przez siebie produkt. Zadanie konkursowe zostało wyjęte z kapelusza, a œciœlej wylosowane spoœród kilku przedstawionych przez uczestników. Miało zajšć około pół godziny pro- gramowania, a brzmiało mniej więcej tak: Napisz wielowštkowš symulację złożonš z czterech okien. W pierwszym oknie należy wyœwietlać serię rosnšcych liczb, w drugim serię rosnšcych liczb pierw- szych, a w trzecim wartoœci cišgu Fibonacciego. Cišg Fibonacciego zdefiniowany jest tak: a(0)=0, a(1)=1, a(n>1)=a(n-2)+a(n-1). Oznacza to, że każdy element cišgu oprócz zerowego i pierwszego jest sumš dwóch poprzednich (kilka pierwszych elementów cišgu to: 0, 1, l, 2, 3, 5, 8...). Okna po zapełnieniu wartoœciami powin- ny być albo przewijane, albo czyszczone. W czwartym oknie należy wyœwietlać okręgi o losowo wybieranych promieniach. Program powinien się zakończyć, je- żeli użytkownik naciœnie klawisz [Esc]. Oczywiœcie w paŸdzierniku 1986 roku taki program działajšcy w systemie DOS mógł być jedynie symulacjš wielowštkowš i nikt z uczestników konkursu nie był wystarczajšco mocny - a większoœć nie miała po prostu odpowiedniej wiedzy - aby zakodować go dla Windows. Ponadto napisanie takiego programu od zera z pewnoœciš zajęłoby więcej niż pół godziny! Większoœć uczestników napisała program, który dzielił ekran na cztery częœci. Najważniejszym elementem była pętla, która sekwencyjnie aktualizowała poszcze- gólne okna i sprawdzała, czy nie został naciœnięty klawisz [Esc]. Program zużywał 100 procent zasobów CPU, co w przypadku aplikacji dosowej było normalne. Rozdział 20: Wielozadaniowoœć i wielowštkowoœć 1077 Jeżeli program zostałby napisany dla Windows 1.0, wynik przypominałby mniej więcej program MULTI1 z rysunku 20-2. Piszę "mniej więcej", ponieważ prezen- towana poniżej aplikacja jest przeznaczona dla systemu 32-bitowego. Niemniej sama struktura i większoœć zasadniczego kodu - nie liczšc definicji funkcji i zmiennych ani obsługi unikodu - miałaby prawdopodobnie właœnie takš po- stać. MULTII.C /* MULTIl.C - Demo wielowdtkowe (c) Charles Petzold, 1998 , */ ţţinclude ţţinclude LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int cyChar ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) ( static TCHAR szAppName[] = TEXT ("Multil") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CSţVREDRAW ; ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; , wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = NULL ; I wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) ( MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MBţICONERROR) ; return 0 ; ) - hwnd = CreateWindow (szAppName, TEXT ("Multitasking Demo"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; T ţ ţţg Częœć III: Zagadnienia zaawansowane (cišg dalszy ze strony 1077) while (GetMessage (&msg, NULL, 0, 0)) ( TranslateMessage (&msg) ; DispatchMessage (&msg) ; return msg.wParam ; ) int CheckBottom (HWND hwnd, int cyClient, int iLine) ( if (iLine * cyChar + cyChar > cyClient) ( InvalidateRect (hwnd, NULL, TRUE) ; UpdateWindow (hwnd) : , iLine = 0 ; ) return iLine ; ) // // Okno 1: wyœwietlanie ciţgu rosndcych liczb calkowitych // LRESULT APIENTRY WndProcl (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ( static int iNum, iLine, cyClient ; HDC hdc ; TCHAR szBufferCl67 ; switch (message) ( r case WM_SIZE: cyClient = HIWORD (lParam) ; return 0 ; ! case WM_TIMER: if (iNum < 0) iNum = 0 ; iLine = CheckBottom (hwnd, cyClient, iLine) ; hdc = GetDC (hwnd) ; TextOut (hdc, 0, iLine * cyChar, szBuffer, wsprintf (szBuffer, TEXT ("%d"), iNum++)) ; ReleaseDC (hwnd, hdc) ; iLine++ ; return 0 ; ) return DefWindowProc (hwnd, message, wParam, lParam) ; ' l // // Okno 2: wyœwietlanie cišgu rosndcych liczb pierwszych // LRESULT APIENTRY WndProc2 (HWND hwnd, UINT messa9e, WPARAM wParam, LPARAM lParam) , Rozdział 20: Wielozadaniowoœć i wielowštkowoœć 1079 static int iNum = 1, iLine, cyClient ; HDC hdc : int i, iSqrt ; TCHAR szBuff ] ; er[16 s.. switch (message) ţ f ,.,. .. case WM_SIZE: cyClient = HIWORD (lParam) ; return 0 ; case WM TIMER: do ( if (++iNum < 0) iNum = 0 ; iSqrt = (int) sqrt (iNum) ; ' i ,: for (i = 2 ; i <= iSqrt ; i++) if (iNum % i == 0) break ; } while (i <= iSqrt) ; iLine = CheckBottom (hwnd, cyClient, iLine) ; hdc = GetDC (hwnd) ; i .::; TextOut (hdc, 0, iLine * cyChar, szBuffer, wsprintf (szBuffer, TEXT ("%d"), iNum)) ; ReleaseDC (hwnd, hdc) ; iLine++ ţ return 0,; i ;, :. I return DefWindowProc (hwnd, message, wParam, lParam) ; 1 iţ i ;.r,, // // Okno 3: wyœwietlanie cišgu Fibonacciego // j ::: LRESULT APIENTRY WndProc3 (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) static int iNum = 0, iNext = l, iLine, cyClient ; HDC hdc ; P: I int iTem TCHAR szBuffer[16] ; i switch (message) i ţ ( ! case WM_SIZE: cyClient = HIWORD (lParam) ; return 0 ; case WM_TIMER: I if (iNum < 0) iNum = 0 ; i ţ iNext = 1 ; T 1080 Częœć III: Zagadnienia zaawansowane (cišg dalszy ze strony 1079) iLine = CheckBottom (hwnd, cyClient, iLine) ; hdc = GetDC (hwnd) ; TextOut (hdc, 0, iLine * cyChar, szBuffer, ' wsprintf (szBuffer, "%d", iNum)) ; ReleaseDC (hwnd, hdc) ; iTemp = iNum ; iNum = iNext ; iNext += iTemp ; iLine++ ; return 0 ; , return DefWindowProc (hwnd, message, wParam, lParam) ; ) // // Okno 4: wyœwietlanie okręgów o losowych promieniach // LRESULT APIENTRY WndProc4 (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ' ( static int cxClient, cyClient ; HDC hdc ; int iDiameter ; switch (message) f case WM_SIZE: r cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; return 0 ; ! case WM_TIMER: InvalidateRect (hwnd, NULL, TRUE) ; UpdateWindow (hwnd) ; iDiameter = rand() % (max (1, min (cxClient, cyClient))) ; hdc = GetDC (hwnd) ; Ellipse (hdc, (cxClient - iDiameter) / 2, (cyClient - iDiameter) / 2, (cxClient + iDiameter) / 2, (cyClient + iDiameter) / 2) ; ReleaseDC (hwnd, hdc) ; return 0 ; ) return DefWindowProc (hwnd, message, wParam, lParam) ; // // Glówne okno tworzdce okna potomne // , Rozdziat 20: Wielozadaniowoœć i wielowštkowoœć 1081 LRESULT APIENTRY WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ( static HWND hwndChild[4] ; static TCHAR * szChildClass[] = ( TEXT ("Childl"), TEXT ("Child2"), TEXT ("Child3"), TEXT ("Child4") static WNDPROC ChildProc[] = ( WndProcl, WndProc2, WndProc3, WndProc4 ) ; , HINSTANCE hInstance ; int i, cxClient, cyClient ; WNDCLASS wndclass ; switch (message) ( case WM_CREATE: hInstance = (HINSTANCE) GetWindowLong (hwnd, GWL HINSTANCE) ; wndclass.style = CS HREDRAW ţ CSţVREDRAW ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = NULL ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = NULL ; - for ( i = 0 ; i < 4 ; i++) ( wndclass.lpfnWndProc = ChildProc[i] ; wndclass.lpszClassName = szChildClass[i] ; RegisterClass (&wndclass) ; hwndChild[i] = CreateWindow (szChildClass[i], NULL, WS_CHILDWINDOW ţ WS_BORDER ţ WSţVISIBLE, 0, 0, 0, 0. , 1 hwnd, (HMENU) i, hInstance, NULL) ; cyChar = HIWORD (GetOialogBaseUnits ()) ; SetTimer (hwnd, 1, 10, NULL) ; return 0 ; case WM_SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; for (i = 0 ; i < 4 ; i++) MoveWindow (hwndChild[i], (i % 2) * cxClient / 2, (i > 1) * cyClient / 2, , cxClient / 2, cyClient / 2, TRUE) ; return 0 ; case WM TIMER: for (i = 0 ; i < 4 ; i++) SendMessage (hwndChild[i], WM TIMER, wParam, lParam) ; return 0 ; case WMţCHAR: T 1082 Częœć III: Zagadnienia zaawansowane (cišg dalszy ze strony 1081) if (wParam --- '\x1B') DestroyWindow (hwnd) ; return 0 ; case WM_DESTROY: KillTimer (hwnd, 1) ; PostOuitMessage (0) ; return 0 ; 1 return DefWindowProc (hwnd, message, wParam, lParam> ; Rysunek 20-2. Program MULTII Powyższy program nie prezentuje niczego, czego byœmy wczeœniej nie widzieli. Okno główne tworzy cztery okna potomne, z których każde zajmuje jednš czwartš obszaru roboczego. Ustawia również zegar Windows i wysyła komunikaty WM TTMER do każdego z czterech okien potomnych. Program Windows powiruen przechowywać informacje wystarczajšce do odtwo- rzenia zawartoœci swojego okna podczas komunikatu WM PAINT. MULTI1 tego nie robi, ale okna sš rysowane i usuwane tak szybko, że uznałem to za zbędne. Generator liczb pierwszych z WndProc2 nie jest może zbyt efektywny, ale za to działa. Liczbš pierwszš jest każda liczba natui'alna większa od 1, która dzieli się tylko przez 1 i przez samš siebie. Aby sprawdzić, czy dana liczba jest liczbš pierw- szš, re trzeba dzielić jej przez wszystkie liczby mniejsze od niej - wystarczy przez pierwszych pierwiastek z n liczb. To właœnie wyliczanie pierwiastka jest powo- dem włšczania bibliotek matematycznych w bšdŸ co bšdŸ całkowitoliczbowym programie. W programie MULTII nie ma nic złego. Korzystarue z zegara Windows jest wła- œciwym sposobem symulowania wielowštkowoœci w przypadku zarówno wcze- œniejszych wersji Windows, jak i Windows 98. Korzystanie z zegara często spo- walrua jednak program. Jeżeli program jest w stanie aktualizować wszystkie swoje okna w czasie jednego komunikatu WM TIMER, i to z zapasem, oznacza to, że nie wykorzystuje w pełni zasobów systemowych. Ulepszona wersja programu powinna na przykład ţykonywać dwie aktualiza- cje podczas jednego łcomunikatu WM TIMER. A może trzy, cztery... kto da wię- cej? Mnożnik powinien być zależny od prędkoœci komputera, która skšdinšd może być bardzo różna. Jednakowoż pisanie programu dopasowanego do 25-MHz komputera 386, 50-MHz komputera 486 albo 100-GHz Pentium 7 nie ma więk- szego sensu. Rozwišzanie wielowštkowe Przyjrzyjmy się wielowštkowemu rozwišzaniu zadarua. Na rysunku 20-3 przed- stawiono je w postaci programu MULTI2. Rozdział 20: Wielozadaniowoœć i wielowštkowoœć 1083 MULTI2. C ţ'` ţ* MULTI2.C - Demo wielowqtkowe (c) Charles Petzold, 1998 *ţ , ţţinclude ţţi ncl ude ţ..:...... ţţi ncl ude ;..,' . typedef struct HWND hwnd ; int cxClient ; int cyClient ; j, int cyChar : BOOL bKill ; PARAMS, *PPARAMS ; LRESULT APIENTRY WndProc (HWND, UINT, WPARAM, LPARAM) ; I , int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) i f static TCHAR szAppName[] = TEXT ("Multi2") ; HWND hwnd ; MSG msg ; ,. WNDCLASS wndclass ; L:. wndclass.style = CS_HREDRAW ţ CS-VREDRAW ; ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE BRUSH) ; ' ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) 1 '... :-.. MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MB-ICONERROR) ; return 0 ; ) C hwnd = CreateWindow (szAppName, TEXT ("Multitasking Demo"), ` WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CWţUSEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; 3 :;.:::,.. ShowWindow (hwnd, iCmdShow) ; - 1084 Częœć III: Zagadnienia zaawansowane (cišg dalszy ze strony 1083) UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) ( TranslateMessage (&msg) ; DispatchMessage (&msg) ; ) return msg.wParam ; int CheckBottom (HWND hwnd, int cyClient, int cyChar, int iLine) ( if (iLine * cyChar + cyChar > cyClient) ( InvalidateRect (hwnd, NULL, TRUE) ; UpdateWindow (hwnd) ; iLine = 0 ; 1 return iLine ; // // Okno 1: wyœwietlanie cidgu rosndcych liczb calkowitych // void Threadl (PVOID pvoid) ( HDC hdc ; int iNum = 0, iLine = 0 ; PPARAMS pparams ; TCHAR szBufferCl6] ; pparams = (PPARAMS) pvoid ; while (!pparams->bKill) ( if (iNum < 0) iNum = 0 ; iLine = CheckBottom (pparams->hwnd, pparams->cyClient, pparams->cyChar, iLine) ; hdc = GetDC (pparams->hwnd) ; TextOut (hdc, O, iLine * pparams->cyChar, szBuffer, wsprintf (szBuffer, TEXT ("%d"), iNum++)) ; ReleaseDC (pparams->hwnd, hdc) ; , iLine++ ; ) endthread () ; ) LRESULT APIENTRY WndProcl (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ( static PARAMS params ; ůţa Rozdział 20: Wielozadaniowoœć i wielowštkowoœć 1085 switch (message) ( case WM_CREATE: params.hwnd = hwnd ; params.cyChar = HIWORD (GetDialogBaseUnits ()) ; ţbeginthread (Threadl, 0, ¶ms) ; return 0 ; case WM_SIZE: params.cyClient = HIWORD (lParam) ; return 0 ; case WM_DESTROY: params.bkill = TRUE ; return 0 ; 1 return DefWindowProc (hwnd, message, wParam, lParam) ; // // Okno 2: wyœwietlanie cišgu rosnšcych liczb pierwszych // void Thread2 (PVOID pvoid) ( HDC hdc ; int iNum = 1, iLine = 0, i, iSqrt ; PPARAMS pparams ; TCHAR szBuffer[16] ; pparams = (PPARAMS) pvoid ; while (!pparams->bKill) ( do ( if (++iNum < 0) iNum = 0 ; iSqrt = (int) sqrt (iNum) ; for (i = 2 ; i <= iSqrt ; i++) if (iNum % i == 0) break ; ) while (i <= iSqrt) ; iLine = CheckBottom (pparams->hwnd, pparams->cyClient, pparams->cyChar, iLine) ; hdc = GetDC (pparams->hwnd) ; TextOut (hdc, 0, iLine * pparams->cyChar, szBuffer, wsprintf (szBuffer, TEXT ("%d"), iNum)) ; ReleaseDC (pparams->hwnd, hdc) ; iLine++ ; 1086 Częœć III: Zagadnienia zaawansowane (cišg dalszy ze strony 1085) ţendthread () ; ) LRESULT APIENTRY WndProc2 (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ( static PARAMS params ; r switch (message) ( case WM CREATE: params.hwnd = hwnd ; params.cyChar = HIWORD (GetDialogBaseUnits ()) ; ţbeginthread (Thread2, 0, ¶ms) ; return 0 ; case WM_SIZE: params.cyClient = HIWORD (lParam) ; return 0 ; , case WM DESTROY: params.bKill = TRUE ; return 0 ; return DefWindowProc (hwnd, message, wParam, lParam) ; ) // // Okno 3: wyœwietlanie cišgu Fibonacciego // void Thread3 (PVOID pvoid) I HDC hdc ; int iNum = 0, iNext = 1, iLine = 0, iTemp ; PPARAMS pparams ; TCHAR szBuffer[16) ; pparams = (PPARAMS) pvoid ; while (!pparams->bKill) if (iNum < 0) ( iNum = 0 ; iNext = 1 ; ) iLine = CheckBottom (pparams->hwnd, pparams->cyClient, pparams->cyChar, iLine) ; hdc = GetDC (pparams->hwnd) ; TextOut (hdc, 0, iLine * pparams->cyChar, szBuffer, ' wsprintf (szBuffer, TEXT ("ţd"), iNum)) ; ReleaseDC (pparams->hwnd, hdc) ; iTemp = iNum ; Rozdział 20: Wielozadaniowoœć i wielowštkowoœć 1087 iNum = iNext ; iNext += iTemp : ' iLine++ ; endthread () ; LRESULT APIENTRY WndProc3 (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) f static PARAMS params ; switch (message) case WMţCREATE: params.hwnd = hwnd ; params.cyChar = HIWORD (GetOialogBaseUnits ()) ; _beginthread (Thread3, 0, ¶ms) ; return 0 ; case WM_SIZE: params.cyClient = HIWORO (lParam) ; return 0 ; case WMţDESTROY: params.bKill = TRUE ; return 0 ; return DefWindowProc (hwnd, messa9e, wParam, lParam) ; l SI !:: // // Okno 4: wyœwietlanie okręgów o losowych promieniach // void Thread4 (PVOID pvoid) HDC hdc ; int iDiameter ; PPARAMS pparams ; f :: pparams = (PPARAMS) pvoid ; while (!PParams->bKill) ( InvalidateRect (pparams->hwnd, NULL, TRUE) ; ' UpdateWindow (pparams->hwnd) ; k::; iDiameter = rand() % (max (i, min (pparams->cxClient, pparams->cyClient))) ; f ::. hdc = GetDC (pparams->hwnd) ; Ellipse (hdc, (pparams->cxClient - iDiameter) / 2, ` (pparams->cyClient - iDiameter) / 2, (pparams->cxClient + iDiameter) / 2, (pparams->cyClient + iDiameter) / 2) ; ReleaseDC (pparams->hwnd, hdc) ; 1088 Częœć IIIţ Zagadnienia zaawansowane (cigg dalszy ze strony 1087) ţendthread () ; } LRESULT APIENTRY WndProc4 (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ( static PARAMS params ; switch (message) ( case WMţCREATE: params.hwnd = hwnd ; params.cyChar = HIWORD (GetDialogBaseUnits ()) ; ţbeginthread (Thread4, 0, ¶ms) ; return 0 ; case WM SIZE: params.cxClient = LOWORD (lParam) ; params.cy0lient = HIWORD (lParam) ; return 0 ; case WMţDESTROY: params.bKill = TRUE ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; } // // Glówne okno tworzšce okna potomne // LRESULT APIENTRY WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) , ( static HWND hwndChildC4] ; static TCHAR * szChildClassC] = t TEXT ("Childl"), TEXT ("Child2"), , TEXT ("Child3"), TEXT ("Child4") } ; static WNDPROC ChildProc[] = t WndProcl, WndProc2, WndProc3, WndProc4 } ; HINSTANCE hInstance ; int i, cxClient, cyClient ; WNDCLASS wndclass ; switch (message) ( case WM_CREATE: hInstance = (HINSTANCE) GetWindowLong (hwnd, GWL_HINSTANCE) ; wndclass.style = CS HREDRAW ţ CS_UREDRAW ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = NULL ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE BRUSH) ; r wndclass:lpszMenuName = NULL ; for (i = 0 ; i < 4 ; i+ń) ( wndclass.lpfnWndProc = ChildProcCi] ; Rozdział 20: Wielozadaniowoœć i wielowštkowoœć 1089 wndclass.lpszClassName = szChildClassCi] ; RegisterClass (&wndclass) ; hwndChildCi] = CreateWindow (szChildClassCi], NULL, WS_CHILDWINDOW ţ WS BORDER ţ WSţVISIBLE, 0, 0, 0, 0, hwnd, (HMENU) i, hInstance, NULL) ; return 0 ; case WM_SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; for (i = 0 ; i < 4 ; i++) MoveWindow (hwndChildCi], (i % 2) * cxClient / 2, (i > 1) * cyClient / 2, cxClient / 2, cyClient / 2, TRUE) ; return 0 ; case WM_CHAR: if (wParam = '\x1B') DestroyWindow (hwnd) ; return 0 ; case WM DESTROY: PostOuitMessage (0) ; return 0 ; ) return DefWindowProc (hwnd, message, wParam, lParam) ; Rysunek 20-3. Program MULTI2 Funkcja WinMain i funkcje WndProc z programu MULTI2.C sš bardzo podobne do swoich odpowiedników z MULTIl.C. WndProc rejestrujš cztery klasy okien, tworzš okna i zmieniajš ich wymiary podczas komunikatu WM SIZE. Ta wersja ? różni się od poprzedruej jedynie tym, że w procedurze WndProc nie jest progra- mowany zegar ani nie sš przetwarzane komunikaty WMţTIMER. Istotnš różnicš w MULTI2 jest to, że każda z procedur okien potomnych tworzy inny wštek, wywołujšc podczas komunikatu WM CREATE funkcję beginthread. Program MULTI2 ma w sumie pięć działajšcych współbieżnie wštków. Wštek główny to procedura głównego okna, a wštki robocze to cztery procedury okien potomnych. Wštki robocze używajš funkcji o nazwach Threadl, Thread2 itd. Te ř' cztery wštki sš odpowiedzialne za rysowanie czterech okien. W kodzie wielowštkowyrn z programu RNDRCTMT nie był wykorzystywany trzeci argument beginthread. Pozwala on wštkowi przekazywać do utworzone- go przezeń wštku dane za poœrednictwem 32 bitowych zmiennych. Zazwyczaj zmiennš takš jest wskaŸnik do struktury. Pozwala to wštkowi tworzšcemu oraz utworzonemu korzystać ze wspólnych zmiennych globalnych. Jak widać, w pro- gramie MULTI2 nie ma żadnych zmiennych globalnych. T 1090 Częœć IIIţ Zagadnienia zaawansowane Dla programu MULTI2 zdefiniowałem strukturę o nazwie PARAMS (na poczšt- ku programu) oraz wskaŸnik do tej struktury o nazwie PPARAMS. Struktura ma pięć pól - uchwyt okna, szerokoœć i wysokoœć okna, szerokoœć i wysokoœć zna- ku oraz zmiennš logicznš o nazwie bKill. Ostatnie pole struktury umożliwia wšt- kowi tworzšcemu informowanie utworzonych przezeń wštków o tym, że nad- szedł czas zakończenia pracy. Przyjrzyjmy się procedurze WndProcl, procedurze okna potomnego, w którym wyœwietlane sš rosnšce liczby całkowite. Procedura się uproœciła. Jedynš zmien- nš lokalnš jest struktura PARAMS. Podczas obshxgi komunikatu WM_CREATE procedura okna nadaje wartoœci polom hwnd i cyChar tej struktury i wywołuje funkcję beginthread, która ma utworzyć nowy wštek używajšcy funkcji Threadl, przekazujšc jej zarazem wskaŸnik do struktury. Podczas przetwarzania komuni- katu WMţSIZE procedura WndProcl nadaje wartoœć polu cyClient struktury, a pod- czas przetwarzania komunikatu WM DESTROY ustawia pole bKill na TRUE. Funkcja Thread kończy się wywołaniem _endthread. Nie jest to konieczne, ponie- waż po zakończeniu funkcji Thread wštek i tak jest niszczony automatycznie. Funkcja ęndthread jest jednak przydatna do kończenia działania wštku na skró- ty, z głębi wielu poziomów zagnieżdżerua kodu. Właœciwš częœć programu, czyli rysoware w oknie, wykonuje funkcja Threadl, działajšca współbieżnie z pozostałymi wštkami. Otrzymuje ona wskaŸnik do struktury PARAMS. Jej główne zadanie, przebiegajšce w pętli while, polega na sprawdzaniu w każdym obrocie pętli, czy pole bKill ma wartoœć TRUE czy FAL- SE. Jeżeli ma wartoœć FALSE, funkcja wykonuje te same operacje co podczas prze- twarzarua komunikatu WMţTIMER w MULTIl.C: formatuje liczbę, pozyskuje uchwyt do kontekstu urzšdzenia i wyœwietla liczbę za pomocš TextOut. Jak zobaczysz, uruchamiajšc MULTI2 w Windows 98, okna sš aktualizowane o wiele szybciej niż w MULTIl. Oznacza to, że program bardziej efektywnie wy- korzystuje moc przetwarzania procesora. Jest jeszcze jedna różnica między MUL- TIl i MULTI2: na ogół, kiedy przesuwa się lub wymiaruje okno, domyœlna proce- dura okna wchodzi do pętli modalnej i wyœwietlanie w oknach się kończy. W pro- gramie MULTI2 wyœwietlanie się nie kończy. Jakieœ problemy? Może się wydawać, że MULTI2 nie jest tak kuloodporny, jak mógłby być. Aby zrozumieć, do czego zmierzam, przyjrzyjmy się ruektórym "usterkom" progra- mu MULTI2.C. Rozpatrzymy je na przykładzie funkcji WndProcl i Threadl. WndProcl działa jako wštek główny MULTI2, a Threadl jako roboczy. W chwi- lach, kiedy Windows przełšcza się między nimi, wštki sš zmienne i nieprzewi- dywalne. Załóżmy, że działa Threadl i że wykonał się właœnie jego kod spraw- dzajšcy, czy pole bKill struktury PARAMS ma w„rtoœć TRUE. Nie ma, ale potem Windows przełšcza sterowanie do wštku głównego, w którym użytkownik koń- ' czy program. WndProcl otrzymuje komunikat WMţDESTROY i nadaje argumen- towi bKill wartoœć TRUE. Łups! Za póŸno! Nagle system operacyjny przełšcza się na Threadl i funkcja próbuje pozyskać uchwyt kontekstu urzšdzenia do nieist- niejšcego okna. Rozdział 20: Wielozadaniowoœć i wielowštkowoœć 1091 Okazuje się, że to nie problem. Sam Windowţ 98 jest na tyle odporny, że funkcje graficzne po prostu kończš się niepowodzeniem, nie powodujšc poważniejszych konsekwencji. Poprawne programowanie wielowštkowe opiera się na technikach synchroniza- cji wštków (a szczególnie na sekcjach krytycznych), które omówię szczegółowo już zaraz. Sekcje krytyczne sš to fragmenty kodu rozpoczynajšce się wywołaniem EnterCriticalSection i kończšce się wywołaniem LeaveCriticalSection. Jeżeli jeden z wštków wejdzie do sekcji krytycznej, inny nie może już tego zrobić. Drugi wš- tek jest zablokowany na funkcji EnterCriticalSection, co trwa do czasu wywołania przez pierwszy funkcji LeaveCriticalSection. Innym problemem z MLTLTI2 jest to, że w czasie, gdy wštek roboczy generuje dane wyjœciowe (rysuje), wštek główny może odebrać komunikat WMţERASEBKGND lub WMţPAINT. Stosujšc sekcję krytycznš, można zapobiec sytuacji, w której w tym samym oknie miałyby rysować jednoczeœnie dwa wštki. Ale eksperymen- ty pokazujš, że Windows 98 szereguje poprawnie dostęp do funkcji graficznych. Oznacza to, że jeden wštek nie może rysować w oknie, dopóki drugi nie przesta- nie. W dokumentacji Windows 98 można się natknšć na ostrzeżenia informujšce o jed- nym obszarze, w którym funkcje graficzne nie sš szeregowane, ale dotyczy to korzystania z obiektów GDI, czyli na przykład piór, pędzli, czcionek, bitmap, re- gionów i palet. Możliwe jest, że jeden wštek zniszczy obiekt używany przez inny. Rozwišzanie tego problemu wymaga zastosowania sekcji krytycznej albo jeszcze lepiej - niewspółużytkowania obiektów GDI przez wštki. Zalety snu Omówiłem, co uważam za najlepszš architekturę dla programu wielowštkowe- go: wštek główny powinien tworzyć wszystkie okna programu, zawierać wszyst- kie procedury okien i przetwarzać wszystkie przesyłane do nich komunikaty. Wštki robocze powinny zaœ zajmować się głównym przetwarzaniem i brać na siebie najbardziej czasochłonne i najcięższe zadania. Załóżmy jednak, że chcemy w wštku roboczym zrealizować animację. W Win- dows animacje tworzy się na ogół za pomocš komunikatów WMţTIMER. Ale jeżeli wštek roboczy nie tworzy okna, nie może również otrzymywać tych komunika- tów. Animacja pozbawiona sygnałów zegarowych będzie jednak prawdopodob- nie za szybka. Rozwišzaniem jest funkcja Sleep. W efekcie wštek wywołuje funkcję Sleep w celu dobrowolnego zawieszenia swojego działania. Jedynym argumentem tej funkcji jest czas nieaktywnoœci (snu) liczony w milisekundach. Funkcja Sleep nie kończy się, dopóki nie minie zadany czas. W tyxn czasie wštek jest zawieszony i nie otrzy- muje przydziałowych kwantów czasu procesora (choć oczywiste jest, że wyma- ga małej iloœci czasu przetwarzania podczas tykruęć zegara, gdy system spraw- dza, czy wštek powinien już zostać odwieszony). Funkcja Sleep z argumentem 0 powoduje oddanie przez wštek pozostałej częœci jego kwantu czasu. Zawieszany jest tylko ten wštek, w którym wywołana została funkcja Sleep. Sys- : tem wcišż zapewnia działanie pozostałym wštkom, zarówno tego samego pro- 1092 Częœć IIIţ Zagadnienia zaawansowane cesu, jak i innych. Funkcji Sleep użyłem w programie SCRAMBLE z rozdziału 14. Za jej pomocš spowolniłem operację mieszania zawartoœci graficznej ekranu. Funkcji Sleep nie umieszcza się z reguły w wštku głównym, ponieważ zwalnia to przetwarzanie. W programie SCRAMBLE nie były tworzone żadne okna, więc taki problem nie istnieje. Synchronizacja wštków Mniej więcej raz do roku przestajš działać œwiatła na ruchliwym skrzyżowaniu za moim oknem. Skutkiem jest chaos. Mimo iż samochody z reguły unikajš zde- rzeń, często niewiele do tego brakuje. Skrzyżowanie dwóch dróg możemy nazwać sekcjš krytycznš. Samochód nad- jeżdżajšcy z pohxdnia i samochód nadjeżdżajšcy z zachodu nie mogš jednocze- œnie przecišć skrzyżowania bezkolizyjnie. W zależnoœci od natężenia ruchu sto- suje się różne rozwišzania tego problemu. W przypadku zastosowania œwiateł na skrzyżowaniu o dużej widocznoœci można zaufać kierowcom, że będš się pra- widłowo włšczali do ruchu. Bardziej nasilony ruch może wymagać stosowania znaku stopu, a przy jeszcze większym niezbędne jest stosowanie œwiateł. Œwiatła służš do organizowania pracy skrzyżowania (o ile oczywiœcie działajš). Sekcja krytyczna Tradycyjne programy komputerowe w systemie ednozadaniowym nie wymaga- jš "œwiateł", które koordynowałyby ich pracę. Działajš tak, jakby należała do nich cała droga. Nie istnieje nic, z czym mogłaby nastšpić kolizja. Nawet w systemie wielozadaniowym większoœć programów działa rzekomo nie- zależnie od siebie. Niemniej pewne trudnoœci mogš występować. Oba programy mogš na przykład podjšć próbę odczytania i zapisania danych do jednego pliku jednoczeœnie. W takich przypadkach system operacyjny zapewnia współużytko- wanie plików oraz mechanizm blokowania rekordów. W systemie operacyjnym obsługujšcym wielowštkowoœć sytuacja się kompliku- je i staje się potencjalnie niebezpieczna. Nierzadko dwa lub kilka wštków korzy- sta z tych samych danych. Jeden wštek może na przykład modyfikować jakieœ zmienne, a inny z nich korzystać. Czasem staje się to Ÿródłem problemów, a cza- sem nie. Należy pamiętać, że system operacyjny może przełšczać sterowanie z jed- nego wštku na inny tylko między instrukcjami kodu maszynowego. Jeżeli współużytkowana przez wštki jest tylko jedna liczba całkowita, wówczas mody- fikacje tej zmiennej sš z reguły realizowane pojedynczymi instrukcjami maszy- nowymi, a więc potencjalne błędy sš raczej wykluczone. Załóżmy jednak, że wštki używajš kilku wspólnych zmiennych całkowitych lub też całych struktur danych. Często konieczne jest zachowanie wewnętrznej spój- noœci między polami struktury albo między całymi zmiennymi. System opera- cyjny mógłby przerwać wštek w połowie aktualizacji tych zmiennych. Wówczas wštek odczytujšcy owe dane stanšłby w obliczu niespójnoœci. Rozdział 20: Wielozadaniowoœć i wielowštkowoœć 1093 Skutkiem tego byłaby kolizja; nietrudno sobie wyobrazić, jak podobny błšd mógł- by zniszczyć program. Aby temu zapobiec, potrzebny jest programistyczny od- powiednik œwiateł koordynujšcych i synchronizujšcych ruch na skrzyżowaniach (tutaj: pracę wštków). Jest nim sekcja krytyczna: blok kodu, którego nie można przerwać. Sekcje krytyczne pełniš cztery funkcje. Aby z nich skorzystać, trzeba najpierw zdefiniować obiekt sekcji krytycznej, którym jest zmienna globalna typu CRITI- CAL SECTION. Oto przykładowa deklaracja: CRITICALţSECTION cs; Ów typ danych o nazwie CRITICAL SECTION to struktura, której pola sš uży- wane tylko wewnętrznie przez Windows. Obiekt sekcji krytycznej musi zostać najpierw zainicjowany przez jeden z wštków programu za pomocš wywołania: InitializeCriticalSection (&cs); Powyższa instrukcja powoduje utworzenie obiektu sekcji krytycznej o nazwie cs. Dokumentacja elektroniczna tej funkcji zawiera następujšce ostrzeżenia: "Obiek- tu sekcji krytycznej nie można ani przenosić, ani kopiować. Proces nie może rów- nież modyfikować obiektu i powinien go traktować jako logicznš czarnš skrzyn- kę". Można to sformułować inaczej: "Nie kombinuj nic z tym obiektem, nawet na niego nie patrz". Kiedy obiekt sekcji krytycznej zostanie już zainicjowany, wštek może wejœć w sek- cję krytycznš za pomocš wywołania: EnterCriticalSection (&cs); Teraz o wštku można powiedzieć, że "posiadł" obiekt sekcji krytycznej. Obiektu tego nie mogš posiadać żadne dwa wštki jednoczeœnie. Jeżeli więc jakiœ inny wštek wej- dzie w sekcję krytycznš, następny wštek, który wywoła EnterCriticalSection z obiek- tem tej samej sekcji, zostanie na tym wywołaniu zawieszony. Funkcja skończy dzia- łanie dopiero wtedy, gdy pierwszy wštek opuœci sekcję krytycznš, wywołujšc: LeaveCriticalSection (&cs); Teraz sekcję krytycznšbędzie posiadał drugi wštek-zawieszony dotšd na swoim wywołaniu EnterCriticalSection. Skończy się również wywołanie funkcji, co po- zwoli wštkowi kontynuować działanie. Kiedy obiekt sekcji krytycznej nie jest już więcej potrzebny programowi, można go usunšć wywołaniem: DeleteCriticalSection (&cs); Zwalnia ono wszelkie zasoby systemowe, które zostały zarezerwowane na po- trzeby obiektu sekcji. Omówiony tu mechanizm sekcji krytycznej wišże się z pojęciem wzajemnego wykluczania, które pojawi się ponownie podczas dalszego zgłębiania tajemnic synchronizacji wštków. W danej chwili tylko jeden wštek może posiadać danš sekcję krytycznš. Tak więc jeden wštek może wejœć do sekcji krytycznej, ustawić pola struktury i opuœcić sekcję. Potem kolejny wštek korzystajšcy ze struktury może również wejœć do sekcji krytycznej, zrealizować dostęp do pól struktury i opuœcić sekcję. 1094 Częœć III: Zagadnienia zaawansowane Zauważmy, że można definiować wiele obiektów sekcji krytycznych - na przy- kład cs1 i cs2. Jeżeli jakiœ program ma cztery wštki, z których dwa pierwsze uży- wajš wspólnych danych, jeden obiekt sekcji będzie dla nich; jeżeli ponadto drugie dwa wštki współużytkujš jakieœ dane, one też otrzymajš jeden (inny) obiekt sekcji. Należy także pamiętać o zachowaniu ostrożnoœci podczas wykorzystywania sekcji krytycznych w wštku głównym. Jeżeli bowiem wštek roboczy będzie przetrzy- mywał sekcję krytycznš, może zawiesić główny wštek na długi czas. W wštkach roboczych sekcje krytyczne stosuje się na ogół do kopiowania pól struktur do zmiennych lokalnych. Jednym z ograniczeń sekcji krytycznych jest to, że mogš być one używane do koordynowania wštków tylko jednego procesu. Sš na przykład sytuacje, w któ- rych niezbędne jest koordynowanie wštków z dwóch różnych procesów używa- jšcych wspólnego zasobu (na przykład pamięci wspólnej). Do tego celu nie moż- na zastosować sekcji krytycznych. Służš do tego obiekty nazywane doœć dziwnie " muteksami". Nazwa ta jest zlepkiem angielskiego okreœlenia wzajemnego wy- kluczania (mutex od mutual exclusion). Rozwinięcie to mówi dokładnie, o co cho- dzi. Chodzi o to, aby zabezpieczyć wštki programu przed przerwaniem w chwili aktualizowania lub używania pamięci wspólnej czy innych zasobów. Sygnalizowanie zdarzeń Najczęstszym zastosowaniem wielowštkowoœci w programach jest realizowanie pewnych czasochłonnych zadań przetwarzania. Możemy je nazywać poważnymi zadaniami - pod tš nazwš kryje się wszystko, co dany program ma do wykonania w czasie dłuższym niż jedna dziesišta sekundy. Do typowych poważnych zadań zalicza się: sprawdzanie pisowni w procesorze tekstów, sortowanie lub indekso- wanie plików baz danych, odœwieżanie zawartoœci arkuszy kalkulacyjnych, dru- kowanie, a nawet bardziej złożone rysowania. Oczywiœcie, najlepszym sposobem na spełnianie reguły jednej dziesištej sekundy, jak już pisałem, jest delegowanie poważnych zadań wštkom roboczym. Wštki te nie tworzš okien, więc nie obowiš- zuje ich reguła. Czasem dobrze jest, kiedy wštek roboczy może poinformować wštek główny o za- kończeniu swojej pracy albo kiedy wštek główny może przerwać działanie wšt- ku roboczego. Tym właœnie zajmiemy się obecnie. Program BIGJOB1 Jako symulacji poważnego zadarua użyję cišgu kalkulacji zmiennoprzecinkowych , nazywanych czasem "dzikim testem". Wyliczenia obejmujš inkrementację liczby całkowitej realizowanš przez: podnoszenie jej do kwadratu i wycišgnięcie z wyni- ku pierwiastka (znoszšcego potęgowanie), zastosowanie funkcji log i exp (które też się znoszš) oraz funkcji atan i tan (podobnie), a na końcu dodanie do wyniku 1. Program BIGJOBI przedstawiono na rysunku 20-4. Rozdział 20: Wielozadaniowoœć i wielowštkowoœć 1095 BIGJOBI.C /* BIGJOBI.C - Demo wielowdtkowe (c) Charles Petzold, 1998 */ C..:: , ţţinclude ţţinclude ţţinclude ţţdefine REP 1000000 ţţdefine STATUS_READY 0 ţţdefine STATUS_WORKING 1 ţţdefine STATUS DONE 2 ţţdefine WM CALC DONE (WM_USER + 0) ţţdefine WM CALC ABORTED (WM USER + 1) iţ typedef struct !.,,. ( HWND hwnd ; BOOL bContinue ; ) PARAMS, *PPARAMS : ' LRESULT APIENTRY WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, I PSTR szCmdLine, int iCmdShow) static TCHAR szAppNameC] = TEXT ("BigJobl") ; HWND hwnd ; ţ,., MSG msg : ''.:.':. i i:. WNDCLASS wndclass ; f. :::: . ' wndclass.style = CS_HREDRAW ţ CS VREDRAW ; wndclass.lpfnWndProc = WndProc ; j wndclass.cbClsExtra = 0 ; I wndclass.cbWndExtra = 0 ; ' wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; i,.: i wndclass.hCursor = LoadCursor (NULL, IDC ARROW) ; ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITEţBRUSH) : wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; ! if (!RegisterClass (&wndclass)) MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MB-ICONERROR) ; return 0 ; ) i > hwnd = CreateWindow (szAppName, TEXT ("Multithreading Demo"), f,, WSţOVERLAPPEDWINDOW, 1096 Częœć Iil: Zagadnienia zaawansowane (cišg dalszy ze strony 1095) CWţUSEDEFAULT, CWţUSEDEFAULT, CW_USEDEFAULT, CW USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) ( TranslateMessage (&msg) ; DispatchMessage (&msg) ; ) return msg.wParam ; ) ! void Thread (PVOID pvoid) ( double A = 1.0 ; INT i ; LONG lTime ; volatile PPARAMS pparams ; pparams = (PPARAMS) pvoid ; lTime = GetCurrentTime () ; for (i = 0 ; i < REP && pparams->bContinue ; i++) A = tan (atan (exp (log (sqrt (A * A))))) + 1.0 ; if (i == REP) ( lTime = GetCurrentTime () - lTime ; SendMessage (pparams->hwnd, WM CALC DONE, 0, lTime) ; else SendMessage (pparams->hwnd, WM CALC ABORTED, 0, 0) ; ţendthread () ; ) LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ( static INT iStatus ; static LONG lTime ; static PARAMS params ; static TCHAR * szMessage[] = ( TEXT ("Ready (left mouse button begins)"), TEXT ("Working (right mouse button ends)"), TEXT ("%d repetitions in %ld msec") ) ; HDC hdc ; PAINTSTRUCT ps ; ' RECT rect ; ! TCHAR szBuffer[64] ; switch (message) ( case WM_LBUTTONDOWN: if (iStatus == STATUSţWORKING) Rozdział 20: Wielozadaniowoœć i wielowštkowoœć 1097 i MessageBeep (0) ; ; return 0 ; iStatus = STATUS WORKING ; I params.hwnd = hwnd ; params.bContinue = TRUE ; ; ţbeginthread (Thread, 0, ¶ms) ; InvalidateRect (hwnd, NULL, TRUE) ; return 0 ; case WM_RBUTTONDOWN: params.bContinue = FALSE ; return 0 ; i case WM CALC DONE: I lTime = lParam ; iStatus = STATUS_DONE ; InvalidateRect (hwnd, NULL, TRUE) ; return 0 ; case WM_CALC_ABORTED: iStatus = STATUS_READY ; InvalidateRect (hwnd, NULL, TRUE) ; ! return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; i. '; ; GetClientRect (hwnd, &rect) ; ţI'.:. wsprintf (szBuffer, szMessageCiStatus], REP, lTime) ; i` DrawText (hdc, szBuffer, -1, &rect, DT SINGLELINE ţ DT CENTER ţ DT UCENTER) ; EndPaint (hwnd, &ps) ; ` return 0 ; iiţ.. case WM_DESTROY: I..':.. PostOuitMessage (0) ; return 0 ; returp DefWindowProc (hwnd, message, wParam, lParam) ; ' Rysunek 20-4. Program BIGJOB1 ; Jest to doœć prosty program, ale myœlę, że wystarczy do zilustrowania ogólnego sposobu realizowania poważnych zadań w programach wielowštkowych. Aby użyć programu BIGJOBl, kliknij lewym przyciskiem myszy obszar roboczy jego okna. Rozpocznie to cykl 1 000 000 powtórzeń "dzikich wyliczeń". W kompute- rze z procesorem Pentium II 300 MHz zajmuje to około 2 sekund. Po zakończe- 1098 Częœć Iil: Zagadnienia zaawansowane niu wyliczeń w oknie wyœwietlany jest czas, jaki upłynšł. Kliknięcie prawym przy- ciskiem myszy obszaru roboczego podczas trwania wyliczeń umożliwia przerwa- nie programu. Przyjrzyjmy się więc, jak jest to zrealizowane: Procedura okna zawiera zmiennš statycznš o nazwie iStatus (której można nadać wartoœć jednej z trzech stałych zdefiniowanych na poczštku programu, rozpo- czynajšcych się prefiksem STATUS), informujšcš, czy program jest gotowy do przeprowadzenia wyliczeń, przeprowadza wyliczenia czy już je zakończył. Pro- gram używa zmiennej iStatus podczas obsługi komunikatu WMţPAINT do wy- œwietlania odpowiedniego łańcucha znaków na œrodku obszaru roboczego. Procedura okna zawiera ponadto strukturę statycznš (typu PARAMS, także zde- finiowanš na poczštku programu) do wymiany danych między procedurš okna i wštkiem roboczym. Struktura ma tylko dwa pola: hwnd (uchwyt do okna pro- gramu) i bContinue (zmienna logiczna informujšca wštek, czy ma kontynuować wyliczenia czy nie). Kliknięcie obszaru roboczego lewym przyciskiem myszy powoduje nadanie zmiennej iStatus i dwóm polom struktury PARAMS wartoœci STATUS WORKING. Pole hwnd tej struktury jest ustawiane na uchwyt okna, a pole bContinue na TRUE. Procedura okna wywołuje funkcję beginthread. Funkcja wštku roboczego Thread zaczyna się od wywołania funkcji GetCurrentTime w celu odczytania, ile czasu (w milisekundach) upłynęło od momentu uruchomienia Windows. Następnie wy- konywana jest pętla for, w której 1 000 000 razy przeprowadzane sš "dzikie wyli- czenia . Jak można zauważyć, wštek wypadnie z pętli, jeżeli bContinue osišgnie wartoœć FALSE. Po pętli for funkcja Thread sprawdza, czy rzeczywiœcie przeprowadziła 1 000 000 iteracji wyliczeń. Jeżeli tak, wywołuje ponownie funkcję GetCurrentTime, po czym za pomocš SendMessage wysyła do procedury okna zdefiniowany na potrzeby tego programu komunikat WMţUSER DONE, podajšc w nim jako lParam czas trwa- nia wyliczeń. Jeżeli wyliczenia skończyłyby się przedwczeœnie (czyli jeżeli pole bContinue struktury PARAMS w trakcie trwania pętli osišgnęłoby wartoœć FAL- SE), wštek wysłałby do procedury okna komunikat WM USER ABORTED. Wš- tek kończy poprawnie swoje działanie za pomocš wywołania ęndthread. W procedurze okna, kiedy obszar roboczy zostanie klilalięty prawym przyciskiem myszy, pole bContinue struktury PARAMS otrzymuje wartoœć FALSE. W ten oto spo- sób można przerwać wyliczenia, zanim pętla wštku dojdzie samoczynnie do końca. Zauważmy, że zmienna pparams w procedurze Thread jest zdefiniowana jako ulotna (ang. volatile). Kwalifikator typu informuje kompilator, że zmienna może być mo- dyfikowana inaczej, niż wynikałoby to z instrukcji programu (powiedzmy przez inny wštek). Jest to informacja dla kompilatora, że nie powinien czynić (w ra- mach procedur optymalizacyjnych) żadnych założeń co do wartoœci wyrażenia pparams->bContinue. Optymalizacja polega na przykład na tym, że przez cały czas trwania pętli for wyrażenie to mogłoby mieć niezmiennš wartoœć, a więc nie ma sensu jej sprawdzać w każdej iteracji pętli. Tak więc słowo kluczowe volatile jest niezbędne, gdyż wyłšcza takie optymalizacje w odniesieniu do definiowanej zmiennej. Rozdział 20: Wielozadaniowoœć i wielowštkowoœć 1099 Procedura okna zaczyna przetwarzanie komunikatu WMţUSER DONE od za- pisania znacznika czasu. Zarówno przetwarzanie komunikatu WM LTSER DO- NE, jak i komunikatu WM USER ABORTED obejmuje w dalszej częœci wywoła- nie funkcji InvalidateRect w celu wygenerowania komunikatu WM PAINT i wy- œwietlenia nowego łańcucha znaków w obszarze roboczym. Zawsze warto przewidzieć w programie takš zmiennš logicznš jak bContinue, aby wštek mógł zakończyć się poprawnie. Funkcji KiIlThread należy używać tylko wtedy, gdy poprawne kończenie jest skomplikowane, ponieważ wštki mogš re- zerwować w systemie zasoby, na przykład pamięć. Jeżeli zarezerwowany przez wštek blok pamięci rue zostanie zwolniony w chwili kończenia wštku, pozosta- nie wcišż zarezerwowany, a więc bezużyteczny. Wštki to nie procesy: rezerwo- wane przez nie zasoby sš wspólne dla wszystkich wštków procesu, dlatego też nie sš automatycznie zwalniane z chwilš kończenia wštku. Dobra praktyka pro- gramistyczna nakazuje, aby każdy wštek zwalniał wszystkie zasoby, które sam zarezerwuje. Zauważmy, że podczas gdy działa jeszcze drugi wštek, może zostać utworzony trzeci, na przykład jeżeli Windows przełšczy sterowanie z drugiego wštku na pierwszy między wywołaniem SendMessage i ęndthread, a procedura okna utwo- rzy nowy wštek w odpowiedzi na kliknięcie myszš. Akurat w naszym przykła- dzie nie stanowi to problemu, ale gdyby zdarzyło się w poważnej aplikacji, nale- żałoby zastosować sekcję krytycznš eliminujšcš kolizję wštków. Obiekt zdarzenia W programie BIGJOB1 nowy wštek jest tworzony za każdym razem, kiedy po- trzebne jest przeprowadzenie wyliczeń; wštek kończy działanie wraz z ukończe- niem wyliczeń. Rozwišzanie alternatywne to utrzymywać przez cały czas trwania programu aktywny wštek i włšczać w nim obliczenia tylko wtedy, gdy trzeba. jest to ideal- ne zastosowanie dla obiektu zdarzenia. Obiekt zdarzenia jest albo zasygnalizowany (inaczej: ustawiony), albo niezasy- gnalizowany (inaczej: zresetowany). Tworzy się go wywołaniem: hEvent = CreateEvent (&sa. fManual. fInitial, pszName); Pierwszy argument (wskaŸnik do struktury SECURITYţATTRIBUTES) i argument ostatni (nazwa obiektu zdarzenia) sš uwzględniane tylko wtedy, gdy obiekty zdarzeń sš współużytkowane przez procesy. W jednym procesie argumenty te sš z reguły ustawiane na NULL. Argumentowi flnitial należy nadać wartoœć TRUE, jeżeli obiekt zdarzenia ma być poczštkowo zasygnalizowany, a wartoœć FALSE, jeżeli ma być poczštkowo niezasygnalizowany. Teraz pokrótce zajmę się argu- mentem fManual. Aby zasygnalizować istniejšcy obiekt zdarzenia, należy wywołać: SetEvent (hEvent); Aby zresetować (odsygnalizować) istniejšcy obiekt zdarzenia, należy wywołać: ResetEvent (hEvent); T i 1100 Częœć III: Zagadnienia zaawansowane I W programie funkcję: WaitForSingle0bject (hEvent, dwTimeOut); wywołuje się zazwyczaj z drugim argumentem równym INFINITE. Funkcja ta wraca natychmiast, jeżeli obiekt zdarzenia jest już ustawiony (zasygnalizowany). W przeciwnym razie zawiesza działanie wštku do czasu, gdy jego status zmieni się na zasygnalizowany. Drugiemu argumentowi można nadać wartoœć limitu czasu w milisekundach, aby funkcja kończyła działanie, jeżeli w cišgu zadanego czasu obiekt nie zmieni się w zasygnalizowany. Jeżeli argument fManual pierwszego wywołania CreateEvent będzie równy FAL- SE, obiekt zdarzenia stanie się automatycznie zasygnalizowany po zakończeniu funkcji Wai#ForSingleObject. Ta właœciwoœć sprawia, że funkcja ResetEvent staje się zbędna. Teraz jesteœmy odpowiednio przygotowani na spotkanie z programem BIGJOB2.C. Przedstawiono go na rysunku 20-5. BIGJOB2.C /* BIGJOB2.C - Demo wielowţtkowe (c) Charles Petzold, 1998 */ iţinclude iţinclude itinclude ildefine REP 1000000 iţdefine STATUS_READY 0 ildefine STATUS_WORKING 1 lldefine STATUS-DONE 2 ţţdefine WM_CALC_DONE (WM_USER + 0) ţţdefine WM CALC ABORTED ,(WM USER + 1) typedef struct ( HWND hwnd ; HANDLE hEvent ; BOOL bContinue ; ) PARAMS, *PPARAMS ; LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) static TCHAR szAppName[7 = TEXT ("BigJob2") HWND hwnd ; MSG msg ; WNDCLASS wndclass : wndclass.style = CSţHREDRAW ţ CSţVREDRAW ; Rozdział 20: Wielozadaniowoœć i wielowštkowoœć 1 101 wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; . wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) ( MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MB ICONERROR) ; hwnd = CreateWindow (szAppName, TEXT ("Multithreading Demo"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CWţUSEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) ( TranslateMessage (&msg) ; DispatchMessage (&msg) ; ) return msg.wParam ; ) void Thread (PVOID pvoid) ( double A = 1.0 ; INT i ; LONG lTime ; volatile PPARAMS pparams ; pparams = (PPARAMS) pvoid ; while (TRUE) ( WaitForSingle0bject (pparams->hEvent, INFINITE) ; lTime = GetCurrentTime () ; for (i = 0 ; i < REP && pparams->bContinue ; i++) A = tan (atan (exp (1og (sqrt (A * A))))) + 1.0 ; if (i ţ REP) ( lTime = GetCurrentTime () - lTime ; PostMessage (pparams->hwnd, WM CALC DONE, 0, lTime) ; 1102 Częœć III: Zagadnienia zaawansowane ( (cišg dalszy ze strony 1101) else s PostMessage (pparams->hwnd, WM CALC ŽBORTED, 0, 0) ; ) LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ( static HANDLE hEvent ; static INT iStatus ; static LONG lTime ; static PARAMS params ; ' static TCHAR * szMessa9eC7 = f TEXT ("Ready (left mouse button begins)"), ; TEXT ("Working (right mouse button ends)"), TEXT ("ţd repetitions in %ld msec") ) ; HDC hdc ; ( PAINTSTRUCT ps ; RECT rect ; TCHAR szBufferCó4] ; l switch (message) , I case WM_CREATE: ; hEvent = CreateEvent (NULL, FALSE, FALSE, NULL) ; params.hwnd = hwnd ; ; params.hEvent = hEvent ; ; params.bContinue = FALSE ; , ţbeginthread (Thread, 0, ¶ms) ; I return 0 ; case WM_LBUTTONDOWN: I if (iStatus == STATUS WORKING) MessageBeep (0) ; return 0 ; I iStatus = STATUS WORKING ; params.bContinue = TRUE ; SetEvent (hEvent) ; InvalidateRect (hwnd, NULL, TRUE) ; return 0 ; case WM_RBUTTONDOWN: params.bContinue = FALSE ; return 0 ; r case WM CALC DONE: lTime = lParam ; iStatus = STATUSţDONE ; i InvalidateRect (hwnd, NULL, TRUE) ; I return 0 ; ', case WM_CALC ABORTED: iStatus = STATUS READY ; Rozdział 20: Wielozadaniowoœć i wielowštkowoœć 1103 InvalidateRect (hwnd, NULL, TRUE) ; return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; GetClientRect (hwnd, &rect) ; wsprintf (szBuffer, szMessage[iStatus], REP, lTime) ; DrawText (hdc, szBuffer, -l, &rect, DTţSINGLELINE ţ DT CENTER ţ DT VCENTER) ; EndPaint (hwnd, &ps) ; return 0 ; case WMţDESTROY: PostOuitMessage (0) ; return 0 ; ) return DefWindowProc (hwnd, message, wParam, lParam) ; Rysunek 20-5. Program BIGJOB2 Procedura okna przetwarza komunikat WM CREATE tworzšc najpierw obiekt zdarzenia inicjowany jako niezasygnalizowany (zresetowany). Następnie proce- dura tworzy wštek. Funkcja Thread rozpoczyna nieskończonš pętlę while, na pocżštku której wywo- ływana jest funkcja WaitForSingleObject. Warto zauważyć, że w strukturze PA- RAMS jest trzecie pole zawierajšce uchwyt obiektu zdarzenia. Ponieważ obiekt zdarzenia jest poczštkowo niezasygnalizowany, wštek zostanie na wywołaniu tej funkcji zawieszony. Kliknięcie lewego przycisku myszy powoduje, że proce- dura okna wywoła funkcję SetEvent. To z kolei zwolni drugi wštek z czekania w funkcji WaitForSingleObject i włšczy wyliczenia. Po zakończeniu wyliczeń wš- tek wywoła ponownie WaitForSingleObject, ale obiekt zdarzenia za sprawš pierw- szego wywołania stał się niezasygnalizowany. Zostanie więc zawieszony do cza- su kolejnego kliknięcia myszš. Reszta programu jest prawie identyczna z BIGJOBI. Lokalna pamięć wštku (TLS) Zmienne globalne programu wielowštkowego (tak samo jak wszelkie rezerwo- wane obszary pamięci) sš wspólne dla jego wszystkich wštków. Lokalne zmien- ne statyczne funkcji sš również wspólne wszystkim wštkom, które korzystajš z funkcji. Lokalne zmienne automatyczne funkcji sš prywatne dla każdego wšt- ku, jako przechowywane na stosie, a każdy wštek ma swój własny stos. Przydałoby się mieć takie miejsce w pamięci, które byłoby zarazem prywatne dla danego wštku i trwałe. Takiego miejsca wymaga na przykład wspomniana we wczeœniejszej częœci tego rozdziału funkcja strtok. Niestety, w definicji języka C nie przewidziano takich zmiennych. Za to system Windows wyposażono w czte- 1104 Częœć III: Zagadnienia zaawansowane ry funkcje, za pomocš których można zaimplementować odpowiedni mechanizm. Rozwišzanie problemu zapewniajš również rozszerzenia języka C opracowane przez Microsoft. Jak już wiadomo, mowa o obszarach TLS. Oto jak działa API: Najpierw definiuje się strukturę zawierajšcš wszystkie dane, które majš być pry- watne dla wštków, na przykład: typedef struct ( int a; int b; 1 DATA, * PDATA; Wštek główny wywołuje funkcję TlsAlloc, w wyniku której otrzymuje indeks: dwTlsIndex = TlsAlloc (); Ta wartoœć indeksowa może być zapisana w zmiennej globalnej albo przekazy- wana funkcji Thread w strukturze argumentów. Funkcja Thread zaczyna działanie od zarezerwowania w pamięci miejsca na struk- turę danych. Rezerwacji dokonuje przez wywołanie funkcji TlsSetvalue z uzyska- nym wczeœniej indeksem jako parametrem: TlsSetValue (dwTlsIndex, GlobalAlloc (GPTR, sizeof (DATA)); Powoduje to skojarzenie wskaŸnika z okreœlonym wštkiem i konkretnym indek- sem wštku. Teraz dowolna funkcja, łšcznie z samš funkcjš Thread, może zrobić użytek ze wskaŸnika, wykonujšc następujšcy kod: PDATA pdata; pdata = (PDATA) TlsGetValue (dwTlsIndex); Teraz może modyfikować lub odczytywać wartoœci pdata->a i pdata->b. Zanim funkcja Thread zakończy działanie, zwalnia zarezerwowanš pamięć: GlobalFree (TlsGetValue (dwTlsIndex)); Kiedy zakończš się wszystkie wštki używajšce danych, wštek główny zwalnia indeks: TlsFree (dwTlsIndex); Ten proces może poczštkowo wydawać się skomplikowany, dlatego z pewno- œciš warto przeanalizować przykładowš implementację pamięci TLS. Nie wiem, jak robi to Windows, ale wydaje mi się, że poniższy opis jest prawdopodobny. Najpierw TlsAlloc może rezerwować blok pamięci (o długoœci zero) i zwracać in- deks będšcy wskaŸnikiem do tego bloku. Każde wywołanie TlsSetvalue z tym indeksem powoduje zwiększenie bloku o 8 bajtów przez realokację. Na owych oœmiu bajtach zapisywane sš identyfikator wštku wywołujšcego funkcję - uzy- skany z wywołania GetCurrentThreadld- oraz wskaŸnik przekazany funkcji Tls- Setvalue. Funkcja TlsGetvalue przeszukuje tabelę, używajšc identyfikatora wšt- ku, po czym zwraca wskaŸnik. TlsFree zwalnia blok pamięci. Jak więc widać, mechanizm można łatwo zaimplementować samemu, ale miło mieć go już poda- ny na tacy. Rozdział 20: Wielozadaniowoœć i wielowštkowoœć 1105 Korzystajšc z rozszerzeń języka C opracowanych przez Microsoft, problem moż- na rozwišzać jeszcze proœciej. Wystarczy poprzedzić zmiennš, która ma być pry- watna dla każdego wštku, prefiksem declspec (thread), na przykład tak: ţdeclspec (thread) int iGlobal = 1; w przypadku zmiennych statycznych zewnętrznych względem funkcji albo tak: ţdeclspec (thread) static int iLocal = 2; w przypadku zmiennych statycznych wewnętrznych względem funkcji. Rozdział 21 Bi ' ' ' Iţ ote kţ n ţ amiczne Yn ( ţ^' y y , k-:... Biblioteki d amiczne naz ane bibliotekami konsolidowan mi d namicznie bibliotekami łšczonymi dynamicznie, modułami bibliotecznymi czy po prostu 'ţţ;ţ ; DLL-ami) to jedne z najważniejszych składników strukturalnych systemów Mi- crosoft Windows. Większoœć plików skojarzonych z Windows to albo programy (pliki wykonywalne), albo moduły bibliotek dynamicznych. Do tej pory pisali- ţ . œmy tylko programy Windows, teraz nadszedł czas zajšć się pisaniem bibliotek dynamicznych. Wiele zasad, które dotyczš pisania programów, odnosi się rów- nież do pisania bibliotek, niemniej sš pewne istotne różnice. ;ţ. .. . Podstawowe informacje o bibliotekach Jak już wiadomo, program działajšcy w Windows to zawarty w pliku wykony- walnym kod tworzšcy jakieœ okna i wykorzystujšcy pętlę komunikatów do od- bierania danych wejœciowych od użytkownika. Biblioteki dynamiczne sš gene- ralnie obiektami, których nie można bezpoœrednio uruchamiać i które nie mogš otrzymywać komunikatów. Sš to osobne pliki, zawierajšce funkcje wywoływane przez programy i inne biblioteki w celu wykonania konkretnych zadań. Bibliote- ka dynamiczna pojawia się tylko wtedy, gdy jakiœ program powoła się na zawar- tš w niej funkcję. Pojęcie konsolidacji dynamicznej oznacza proces, podczas którego Windows kon- soliduje (albo mówišc żargonowo: linkuje) wywołanie funkcji z jakiegoœ mo- duhx z konkretnš funkcjš zawartš w bibliotece. Konsolidacja statyczna to etap pisania programów, podczas którego z plików obiektowych (OBJ), bibliotecznych (LIB) i na ogół również z plików skompilowanych zasobów (RES) uzyskuje się plik wykonywalny (EXE). Konsolidacja dynamiczna, jak wskazuje nazwa, odby- wa się podczas działania programu. Bibliotekami dynamicznymi sš między innymi KERNEL32.DLL, USER32.DLL i GDI32.DLL, a także różne pliki sterowników, takie jak KEYBOARD.DRV, SYS- TEM.DRV czy MOUSE.DRV. Sš to biblioteki, których używajš wszystkie progra- my Windows. Niektóre biblioteki dynamiczne (na przykład pliki czcionek) sš nazywane czysto zasobowymi, ponieważ zawierajš same dane (zazwyczaj w postaci zasobów), bez kodu. Tak więc przeznaczeniem biblioteki dynamicznej jest zapewnianie funkcji i zasobów wielu różnym programom. W tradycyjnym systemie operacyjnym wszystkie procedury używane przez programy użytkowe udostępnia wyłšcznie 1108 Częœć III: Zagadnienia zaawansowane system. W Windows proces wywoływania jednych modułów przez inne został uogólruony. W efekcie, piszšc bibliotekę dynamicznš, tworzymy rozszerzenie systemu Windows. Z drugiej strony DLL-e (również te tworzšce Windows) moż- na również postrzegać jako rozszerzenie programu użytkowego. Choć moduł biblioteki dynamicznej może mieć dowolne rozszerzenie (na przy- kład .EXE lub .FON), standardowo używa się rozszerzenia .DLL. Windows auto- matycznie ładuje jednak tylko biblioteki z rozszerzeniem .DLL. Jeżeli plik ma inne rozszerzenie, program musi go ładować jawnie, czyli za pomocš funkcji LoadLi- brary lub LoadLibraryEx. Sensownym zastosowaniem dla bibliotek dynamicznych sš raczej większe apli- kacje. Załóżmy, że piszesz duży pakiet finansowo-księgowy, który będzie zawie- rał wiele różnych programów. Z pewnoœciš stwierdzisz, że majš one wiele wspól- nych procedur. W takim wypadku można te wspólne procedury umieœcić w nor- malnej bibliotece statycznej (z rozszerzeniem .LIB) i skonsolidować z niš statycz- nie poszczególne moduły programowe. Ale takie rozwišzanie jest marnotraw- stwem, ponieważ każdy z programów pakietu zawiera dla identycznych proce- dur ten sam kod (występuje duża nadmiarowoœć). Ponadto, jeżeli zmodyfikujesz jednš z procedur, musisz dokonać ponownej konsolidacji wszystkich programów, w których ona występowała. Jeżeli jednak zdecydujesz się umieœcić wspólne pro- cedury w bibliotece dynamicznej, na przykład zawartej w pliku FK.DLL, rozwiš- żesz w ten sposób oba przedstawione powyżej problemy. Procedury używane w wielu programach trafiš do jednego tylko modułu, przez co podczas jednocze- snej pracy kilku aplikacji zmniejszy się zapotrzebowanie na miejsce dyskowe ca- łego pakietu oraz na pamięć operacyjnš. A jeżeli przyjdzie ci wprowadzić jakieœ zmiany, nie będziesz musiał ponownie kompilować poszczególnych programów. Biblioteki dynamiczne mogš stanowić osobny produkt. Załóżmy, że napiszesz kolekcję interesujšcych procedur do rysowania trójwymiarowych obiektów gra- ficznych i umieœcisz jš w pliku DLL o nazwie GDI3.DLL. Jeżeli następnie zainte- resujesz swojš bibliotekš innych programistów, zaproponuj im sprzedaż licencji, aby mogli odpłatnie korzystać z twoich procedur w swoich programach graficz- nych. Użytkownik, który korzystałby z kilku takich programów, będzie potrze- bował tylko jednego pliku GDI3.DLL. Biblioteka - temat-rzeka Częœć nieporozumień wokół bibliotek dynamicznych bierze się z tego, że słowo biblioteka" występuje w kilku różnych kontekstach. Oprócz bibliotek dynamicz- " nych sš również biblioteki obiektowe (statyczne) i biblioteki importowe. Biblioteka obiektowa to plik z rozszerzeniem .LIB, który zawiera kod dopisywa- ny fizycznie do pliku z rozszerzeniem .EXE podczas procesu zwanego konsoli- i dacjš statycznš. Przykładem takiej biblioteki z pakietu Microsoft Visual C++ jest normalna biblioteka czasu wykonywania o nazwie LIBC.LIB, którš konsoliduje się z własnymi programami. Biblioteki importowe to szczególna postać bibliotek obiektowych. Majš, jak obiek- towe, rozszerzenie .LIB i sš używane przez konsolidator do rozwikływania wy- wołań funkcji w kodzie. Biblioteki importowe nie zawierajš jednak kodu. Dostar- Rozdział 21: Biblioteki dynamiczne 1109 czajš jedynie konsolidatorowi danych niezbędnych do skonstruowania tablic re- lokacji w pliku EXE dla potrzeb dynamicznego konsolidowania. Bibliotekami importowymi dla funkcji Windows sš KERNEL32.LIB, USER32.LIB i GDI32.LIB - pliki dostarczane z kompilatorem Microsoftu. Jeżeli wywoła się z programu funkcję Rectangle, GDI32.LIB informuje LINK, że funkcja ta jest zawarta w biblio- tece dynamicznej GDI32.DLL. Dane te trafiajš do pliku EXE, aby Windows mógł w chwili działania programu przeprowadzić dynamicznš konsolidację z biblio- tekš GDI32.DLL. Biblioteki obiektowe i importowe sţ; używane tylko w fazie pisania programu. W fazie wykonywania używane sš biblioteki dynamiczne. W chwili, kiedy wy- konuje się program używajšcy biblioteki dynamicznej, musi ona być obecna na dysku. Windows musi załadować moduł DLL, zanim uruchomi program, który go wymaga. Plik biblioteki musi znajdować się w katalogu zawierajšcym plik EXE, w katalogu bieżšcym, w katalogu systemowym Windows, w katalogu Windows albo w dowolnym z katalogów wymienionych w definicji zmiennej œrodowisko- wej PATH. Powyższe katalogi sš przeszukiwane właœnie w tej kolejnoœci. Przykładowy DLL Choć cały pomysł bibliotek dynamicznych polega na tym, że może z nich korzy- stać wiele aplikacji, w rzeczywistoœci na ogół projektuje się je, przynajmniej po- czštkowo, z myœlš o jednej aplikacji - najczęœciej o programie testowym. To właœnie zaraz zrobimy. Napiszemy bibliotekę dynamicznš o nazwie EDR- LIB.DLL (EDR to skrót od ang. easy drawing routines - proste procedury rysunko- we). Nasza wersja EDRLIB będzie zawierać tylko jednš funkcję (EdrCenterText), ale w przyszłoœci można dodać do niej więcej funkcji upraszczajšcych rysowa- nie. Z funkcji zawartych w EDRLIB.DLL będzie korzystać aplikacja EDRTEST.EXE. Napisanie biblioteki wymaga nieco odmiennego podejœcia niż to, które poznali- œmy. Pierwszy raz zajmiemy się możliwoœciš Visual C++. W œrodowisku tym wyróżnia się obszary robocze i projekty. Projekt jest na ogół skojarzony z tworze- niem aplikacji (plików EXE) lub bibliotek dynamicznych (plików DLL). Obszar roboczy może zawierać kilka projektów. Dotychczas każdy z naszych obszarów roboczych zawierał tylko jeden projekt. Teraz utworzymy obszar roboczy o na- zwie EDRTEST, który będzie zawierał dwa projekty - jeden dla EDRTEST.EXE i jeden dla EDRLIB.DLL, biblioteki dynamicznej wykorzystywanej przez EDR- TEST. Do dzieła. Wybierz w Visual C++ polecenie New z menu File. Zaznacz kartę Workspaces (jeszcze jej nie zaznaczaliœmy). W polu Location wybierz katalog, w którym chciałbyœ umieœcić swój obszar roboczy, a w polu Workspace Name wpisz: EDRTEST. Naciœnij [Enter]. Spowoduje to utworzenie pustego obszaru roboczego. Developer Studio utwo- rzy podkatalog o nazwie EDRTEST i plik obszaru roboczego EDRTEST.DSW (i pa- rę innych plików). Teraz w tym obszarze utworzymy projekt. Wybierz polecenie New z menu File i zaznacz kartę Projects. Tam, gdzie do tej pory zawsze wybierałeœ opcję Win32 Application, tym razem wybierz Win32 Dynamic-Link Library. Kliknij również 1110 Częœć III: Zagadnienia zaawansowane przycisk opcji Add To Current Workspace. W ten sposób bieżšcy projekt zostanie dodany do obszaru roboczego EDRTEST: ţNpisz w polu Project Name EDRLIB, ale nie naciskaj jeszcze przycisku OK. Kiedy to uczynisz, Visual C++ zmieni pole Location, w którym EDRLIB pojawi się jako podkatalog EDRTEST. Ale nie o to chodzi! Usuń z pola Location podkatalog EDRLIB, aby projekt został utworzony w katalogu EDRTEST. Naciœnij OK. Powstanie plik projektu o nazwie EDRLIB.DSP i (jeżeli wybrałeœ opcję Export Makefile na karcie Build okna dialogowego Tools Options) plik EDRLIB.MAK. Teraz możesz dodać do projektu kilka plików. Wybierz z menu File polecenie New i uaktywnij kartę Files. Zaznacz C/C++ Header File i wpisz EDRLIB.H. Wpisz plik EDRLIB.H z rysunku 21-1 (albo skopiuj go z CD-ROM-u dołšczonego do tej ksišżki). Ponownie wybierz z menu File polecenie New i kliknij kartę Files. Tym razem wybierz C++ Source File i wpisz EDRLIB.C. Wpisz treœć pliku EDRLIB.C przedstawionego również na rysunku 21-1. EDRLIB.H /* Plik nagłówkowy EDRLIB.H */ ţţi fdef ţcpl uspl us 4ţdefine EXPORT extern "C" declspec (dllexport) ţţel se ţţdefine EXPORT declspec (dllexport) ţţendi f EXPORT BOOL CALLBACK EdrCenterTextA (HDC, PRECT, PCSTR) ; EXPORT BOOL CALLBACK EdrCenterTextW (HDC, PRECT, PCWSTR) ; ţţifdef UNICODE 4kdefine EdrCenterText EdrCenterTextW ţţel se ţţdefine EdrCenterText EdrCenterTextA ţţendi f EDRLIB.C /* EDRLIB.C - Modul z prostš biblioteka procedur rysunkowych (c) Charles Petzold, 1998 */ iţinclude windows.h> ţţinclude "edrlib.h" int WINAPI DllMain (HINSTANCE hInstance, DWORD fdwReason, PVOID pvReserved) ( return TRUE ; EXPORT BOOL CALLBACK EdrCenterTextA (HDC hdc, PRECT prc, PCSTR pString) ( Rozdział 21: Biblioteki dynamiczne 1111 int iLength ; SIZE size ; iLength = lstrlenA (pString) ; GetTextExtentPoint32A (hdc, pString, iLength, &size) ; return TextOutA (hdc, (prc->right - prc->left - size.cx) / 2, (prc->bottom - prc->top - size.cy) / 2, pString, iLength) ; EXPORT BOOL CALLBACK EdrCenterTextW (HDC hdc, PRECT prc, PCWSTR pString) ( int iLength ; SIZE size ; iLength = lstrlenW (pString) ; GetTextExtentPoint32W (hdc, pString, iLength, &size) ; return TextOutW (hdc, (prc->right - prc->left - size.cx) / 2, (prc->bottom - prc->top - size.cy) / 2, pString, iLength) ; Rysunek 21-1. Biblioteka EDRLIB Teraz możesz zbudować plik EDRLIB.DLL w konfiguracji Release lub Debug. Po zbudowaniu katalogi RELEASE i DEBUG będš zawierały plik EDRLIB.LIB, któ- ry jest bibliotekš importowš dla biblioteki dynamicznej, oraz plik EDRLIB.DLL, właœciwš bibliotekę dynamicznš. W całej ksišżce tworzyliœmy programy, które można kompilować dla łańcuchów unikodowych lub nieunikodowych, w zależnoœci od definicji identyfikatora UNI- CODE. DLL powinien jednak zawierać obie wersje każdej funkcji majšcej argu- menty zwišzane ze znakami i łańcuchami znaków. Tak więc EDRLIB.C zawiera funkcje o nazwach EdrCenterTextA (wersja ANSI) i EdrCenterTextW (wersja dla zna- ków szerokich). Funkcja EdrCenterTextA wymaga parametru PCSTR (wskaŸnika do łańcucha zwykłych znaków typu const), a EdrCenterTextW - parametru PCWSTR (wskaŸnika do łańcucha znaków szerokich typu const). Funkcja EdrCenterTextA wywołuje jawnie lstrlenA, GetTextExtentPoint32A i TextOutA. Natomiast funkcja EdrCenterTextW wywołuje jawnie lstrlenW, GetTextExtentPoint32W i TextOutW. W pliku EDRLIB.H zdefiniowano EdrCenterText jako EdrCenterTextW, gdy identyfi- kator UNICODE jest zdefiniowany, a jako EdrCenterTextA w przypadku, gdy nie jest zdefiniowany. Tak wyglšdajš właœnie pliki nagłówkowe Windows. EDRLIB.H zawiera również funkcję o nazwie DIlMain, która zastępuje w DLL-u funkcję WinMain i służy do przeprowadzania czynnoœci inicjacyjnych oraz zamy- kajšcych, o których piszę w dalszej częœci tego rozdziału. Dla naszych celów wystarczy, że funkcja DllMain zwróci TRUE. Jedynš niewyjaœnionš zagadkš w powyższych dwóch plikach mogš być defini- cje z identyfikatorem EXPORT. Funkcje biblioteki DLL używane przez aplikacje 1112 Częœć III: Zagadnienia zaawansowane zewnętrzne muszš być wyeksportowane. Nie chodzi tu o żadne taryfy celne ani przepisy handlowe, lecz o kilka słów kluczowych gwarantujšcych, że nazwy pli- ków zostanš dodane do EDRLIB.LIB (aby linker mógł rozwikłać je podczas kon- solidowania aplikacji korzystajšcych z funkcji) i że funkcje będš widoczne z po- ziomu EDRLIB.DLL. Słowo kluczowe EXPORT zawiera specyfikator klasy pamięci declspec (dllexport) oraz extern "C" , jeżeli plik nagłówkowy jest kompilowany w trybie C++. Zapobiega to zwyczajowemu "międleniu nazw" funkcji C++ przez kompilator, a tym samym umożliwia używanie biblioteki DLL zarówno progra- mom C, jak i C++. Punkt wejœcia i wyjœcia biblioteki Przed pierwszym użyciem biblioteki i po zakończeniu jej działania uruchamiana jest funkcja DllMain. Pierwszy parametr DllMain to uchwyt instancyjny bibliote- ki. Jeżeli biblioteka używa zasobów wymagajšcych uchwytów instancyjnych (ta- kich jak DialogBox), wówczas hlnstance powinno się zapisać w jakiejœ zmiennej globalnej. Ostatni parametr DIlMain jest zastrzeżony dla systemu. Parametr fdwReason może mieć jednš z czterech wartoœci oznaczajšcych powód wywołania przez Windows funkcji DIlMain. Czytajšc poniższe omówienie, pa- miętaj, że jeden program może być ładowany wiele razy i działać współbieżnie w Windows. Każde załadowanie programu jest uznawane za osobny proces. Wartoœć fdwReason DLL PROCESS ATTACH oznacza, że biblioteka dynamicz- na została odwzorowana w przestrzeń adresowš jakiegoœ procesu. Jest to wska- zówka dla biblioteki, aby przeprowadziła wszelkie zadania inicjacyjne niezbęd- ne do obsługi przyszłych żšdań pochodzšcych od procesu. Takie inicjacje mogš obejmować rezerwacje pamięci i inne tego typu sprawy. W czasie działania pro- cesu funkcja DIlMain jest wywoływana z parametrem DLL PROCESS ATTACH tylko raz. Wszelkie inne procesy używajšce tego samego DLL-a wywołujš dalsze wywołania funkcji DIlMain z parametrem DLL PROCESS ATTACH, ale to dzie- je się już w odniesieniu do innego procesu. Jeżeli inicjacja zakończy się powodzeniem, funkcja DllMain powinna zwracać wartoœć niezerowš. Zwracanie wartoœci zero spowoduje, że Windows nie uru- chomi programu. Jeżeli fdwReason ma wartoœć DLL PROCESS DETACH, oznacza to, że biblioteka nie jest już potrzebna procesowi, co daje jej możliwoœć posprzštania po sobie. W 32-bitowych wersjach Windows często bywa to zbędne, a zawsze stanowi dobrš praktykę programistycznš. Podobnie, jeżeli DIIMain jest wywoływana z parametrem fdwReason równym DLL THREAD ATTACH, oznacza to, że skojarzony proces utworzył nowy wš- tek. Kiedy kończy się wštek, Windows wywołuje DIIMain z parametrem fdwRe- ason równym DLL THREAD DETACH. Warto wiedzieć, że możliwe jest otrzy- manie wywołania DLL THREAD DETACH bez wczeœniejszego wywołania DLL THREAD ŽTTACH. Dochodzi do tego wówczas, gdy biblioteka jest koja- rzona z procesem już po utworzeniu wštku. Wštek nadal istnieje, kiedy funkcja DllMain jest wywoływana z parametrem DLL THREAD DETACH. Podczas tego procesu funkcja może nawet wysyłać /* Rozdział 21: Biblioteki dynamiczne 1113 wštkowi komunikaty, ale nie powinna używać funkcji PostMessage, ponieważ wštek przed dotarciem komunikatu mógłby zniknšć. Program testowy Utworzymy teraz drugi projekt obszaru roboczego EDRTEST, tym razem dla pro- gramu o nazwie EDRTEST, który będzie użytkownikiem biblioteki EDRLIB.DLL. Majšc załadowany obszar roboczy EDRTEST w Visual C++, wybierz z menu File polecenie New. Zaznacz kartę Projects okna dialogowego New. Tym razem wy- bierz opcję Win32 Application. Zaznacz przycisk Add To Current Workspace. Jako nazwę projektu podaj EDRTEST. Znów musisz w polu Locations usunšć drugi podkatalog EDRTEST. Naciœnij-OK, a w następnym oknie dialogowym wybierz An Empty Project. Naciœnij przycisk Firush. Wybierz ponownie z menu File polecenie New. Zaznacz kartę Files i opcję C++ Source File. Pole listy Add To Project powinno zawierać EDRTEST, a nie EDR- LIB. Wpisz nazwę pliku EDRTEST.C, a następnie treœć pliku z rysunku 21-2. Pro- gram ten wyœrodkowuje tekst wyœwietlany w obszarze roboczym za pomocš funk- cji EdrCenterText. EDRTEST.C tuKitSI.C - Program używajšcy biblioteki dynamicznej EDRLIB (c) Charles Petzold, 1998 ţtinclude ttinclude "edrlib.h" */ LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) static TCHAR szAppName[] = TEXT ("StrProg") HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CS UREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) , wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) f MessageBox (NULL, TEXT ("This program requires Windows NT!"), 1114 Częœć III: Zagadnienia zaawansowane (cigg dalszy ze strony 1113) szAppName, MB ICONERROR) ; ` ) hwnd = CreateWindow (szAppName, TEXT ("DLL Demonstration Program"), WS_OVERLAPPEDWINDOW, CWţUSEDEFAULT, CW USEDEFAULT, CW_USEDEFAULT, CW USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) f TranslateMessage (&msg) ; DispatchMessage (&msg) ; ) return msg.wParam ; ) LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ( HDC hdc ; PAINTSTRUCT ps ; RECT rect ; switch (message) ( case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; ` GetClientRect (hwnd, &rect) ; EdrCenterText (hdc, &rect, TEXT ("This string was displayed by a DLL")) ; EndPaint (hwnd, &ps) ; return 0 ; , case WM_DESTROY: PostOuitMessage (0) ; return 0 ; 1 return DefWindowProc (hwnd, messa9e, wParam, lParam> ; ) Rysunek 21-2. Program EDRTEST Zauważ, że plik EDRTEST.C zawiera dyrektywę włšczajšcš plik nagłówkowy EDRLIB.H. Znajduje się w nim definicja funkcji EdrCenterText, wywoływanej pod- ` czas obsługi komunikatu WMţPAINT. Zanim skompilujesz ten program, powinieneœ dowiedzieć się kilku rzeczy. Po pierwsze wybierz z menu Project polecenie Select Active Project. Powinieneœ zo- Rozdział 21: Biblioteki dynamiczne 1115 baczyć EDRLIB i EDRTEST. Wybierz EDRTEST. Podczas budowania tego obsza- ru roboczego będziesz chciał w rzeczywistoœci budować program. Z menu Pro- ject wybierz także polecenie Dependencies. Z listy Select Project To Modify wy- bierz EDRTEST. Na liœcie Dependent On The Following Project(s) zaznacz EDR- LIB. Oznacza to, że EDRTEST wymaga biblioteki dynamicznej EDRLIB. Podczas każdego budowania projektu EDRTEST przed kompilacjš i konsolidacjš przebu- dowywany będzie również projekt EDRLIB, okaże się to konieczne. Wybierz z menu Project polecenie Settings. Zaznacz kartę General. Kiedy wybie- rzesz projekt EDRLIB lub EDRTEST w lewym panelu, pokazane w prawym pane- lu pliki Intermediate Files i Output Files powinny stanowić katalog RELEASE dla konfiguracji Win32 Release i katalog DEBUG dla konfiguracji Win32 Debug. Jeżeli nie sš, zmień je. Dzięki temu plik EDRLIB.DLL trafi do tego samego katalogu co EDRTEST.EXE, a więc program będzie mógł bez problemu korzystać z biblioteki. Będšc wcišż w oknie dialogowym Project Setting i majšc zaznaczony projekt EDRTEST, kliknij kartę C/C++. W Preprocessor Definitions dodaj UNICODE do konfiguracji Debug, tak jak w przypadku wszystkich programów z tej ksišżki. Teraz powinno ci się udać zbudować plik EDRTEST.EXE w obu konfiguracjach (Debug i Release). Najpierw zostanie skompilowany i skonsolidowany projekt EDRLIB, o ile będzie to niezbędne. Katalogi RELEASE i DEBUG będš zawierały bibliotekę (importowš) EDRLIB.LIB oraz EDRLIB.DLL. Po przeprowadzeniu kon- solidacji EDRTEST, biblioteka importowa zostanie włšczona automatycznie. Należy pamiętać, że kod EdrCenterText nie jest włšczany do pliku EDRTEST.EXE. Plik wykonywalny zawiera jedynie proste odwołania do pliku EDRLIB.DLL oraz funkcji EdrCenterText. Plik EDRTEST.EXE wymaga do poprawnej pracy pliku EDRLIB.DLL. Podczas wykonywania EDRTEST.EXE Windows przeprowadza korekty odwo- łań do funkcji z zewnętrznych modułów bibliotecznych. Wiele z tych funkcji znaj- duje się w normalnych bibliotekach dynamicznych Windows. Ale system widzi również, że program wywołuje funkcję z EDRLIB.LIB, więc ładuje ten plik do pamięci i wywołuje procedurę inicjacyjnš biblioteki. Wywołanie z programu EDRTEST funkcji EdrCenterText jest dynamicznie zestawiane z kodem funkcji z bi- blioteki EDRLIB. Włšczenie pliku EDRLIB.H do pliku EDRTEST.C jest podobne do włšczania pli- ku WINDOWS.H. Konsolidowanie z EDRLIB.LIB przypomina konsolidowanie z bibliotekami importowymi Windows (takimi jak USER32.LIB). Uruchomiony program jest konsolidowany z EDRLIB.DLL tak samo jak z USER32.DLL. Gratu- lacje! Utworzyłeœ własne rozszerzenie Windows! Zanim przejdziemy dalej, jeszcze kilka słów na temat bibliotek dynamicznych. Po pierwsze, choć nazwałem właœnie bibliotekę DLL rozszerzeniem Windows , z drugiej strony można jš postrzegać jako rozszerzenie aplikacji. Wszystko, co robi DLL, odbywa się poprzez aplikację, na przykład cała pamięć, jakš rezerwuje, należy do aplikacji. Wszystkie okna utworzone przez DLL znajdujš się we wła- daniu aplikacji. To samo dotyczy wszystkich otwieranych plików - do nich też prawa ma aplikacja. Z jednej biblioteki dynamicznej może korzystać jednocze- œnie wiele aplikacji, ale Windows chroni je przed wzajemnymi interferencjami. 1116 Częţć III: Zagadnienia zaawansowane Ten sam kod biblioteki dynamicznej może współużytkować wiele procesów. Jed- nak dane utrzymywane przez DLL sš dla każdego procesu różne. Każdy ma wła- snš przestrzeń adresów na dane używane przez DLL. Współużytkowanie pamięci przez procesy wymaga, jak się okaże w następnym podrozdziale, dodatkowej pracy. Biblioteki dynamiczne a pamigć wspólna To bardzo miło, że Windows izoluje aplikacje wykorzystujšce współbieżnie tę samš bibliotekę dynamicznš. Nie zawsze jest to jednak pożšdane. Załóżmy, że chcesz napisać DLL zawierajšcy jakšœ pamięć, która mogłaby być współużytkowana przez kilka aplikacji albo przez wiele instancji tej samej aplikacji. Dotyczy to ko- rzystania z pamięci wspólnej, która jest w rzeczywistoœci plikiem odwzorowa- nym w pamięci. Sprawdzimy, jak to działa, za pomocš programu o nazwie STRPROG (skrót od ang. string program) oraz biblioteki dynamicznej o nazwie STRLIB (od ang. string Library). STRLIB ma trzy funkcje eksportowe, z których korzysta STRPROG. Aby urozmaicić ten przykład, jedna z funkcji STRLIB będzie używała funkcji zwrot- nej (call-back) zdefiniowanej w STRPROG. STRLIB to moduł biblioteki dynamicznej przechowujšcy i sortujšcy do 256 łań- cuchów znaków. Łańcuchy sš kapitalikowane i przetrzymywane w pamięci wspól- nej STRLIB. STRPROG może za pomocš trzech funkcji STRLIB dodawać łańcuch, usuwać łańcuch oraz uzyskiwać bieżšce łańcuchy z STRLIB. Program testowy STR- PROG ma dwie pozycje menu, które otwierajš okna dialogowe służšce do doda- wania i usuwania łańcuchów. STRPROG wyœwietla na swoim obszarze roboczym wszystkie obecnie zapisane w STRLIB łańcuchy. Oto zdefiniowana w STRLIB funkcja, która dodaje nowy łańcuch do pamięci wspólnej biblioteki STRLIB: EXPORT BOOL CALLBACK AddString (pStringIn) Argument pStringln jest wskaŸnikiem do łańcucha. Łańcuch jest kapitalikowany za pomocš funkcji AddString. Jeżeli na liœcie łańcuchów STRLIB istnieje już iden- tyczny łańcuch, funkcja dodaje kolejnš jego kopię. Funkcja AddString zwraca war- toœć TRUE (wartoœć niezerowš), jeżeli zakończy się powodzeniem, a FALSE (0) w przeciwnym razie. Wartoœć zwrotnš FALSE można otrzymać wtedy, gdy łań- cuch miał długoœć 0, gdy nie udało się zarezerwować wystarczajšco dużo pamię- ci na łańcuch albo gdy zapisano już 256 łańcuchów. Oto funkcja STRLIB usuwajšca łańcuch z pamięci wspólnej STRLIB: EXPORT BOOL CALLBACK DeleteString (pStringIn) Znów argument pStringln jest wskaŸnikiem do łańcucha. Jeżeli pasujš przynaj- mniej dwa łańcuchy, usuwany jest tylko pierwszy. Funkcja DeleteString zwraca wartoœć TRUE (wartoœć niezerowš), jeżeli zakończy się powodzeniem, a FALSE (0) w przeciwnym razie. Zwrócenie FALSE oznacza, że długoœć łańcucha wyno- siła 0 albo że nie można było odnaleŸć pasujšcego łańcucha. Oto funkcja STRLIB używajšca funkcji zwrotnej ulokowanej w programie wywo- łujšcym i wyliczajšcej łańcuchy zapisane dotšd w pamięci STRLIB: Rozdział 21: Biblioteki dynamiczne 1117 EXPORT int CALLBACK GetStrings (pfnGetStrCallBack, pParam) Funkcja zwrotna musi być zdefiniowana w programie wywołujšcym w następu- jšcy sposób: EXPORT BOOL CALLBACK GetStrCallBack (PSTR pString, PVOID pParam) Na funkcję zwrotnš wskazuje argument pfnGetStrCallBack funkcji GetStrings. Get- Strings wywołuje GetStrCalIBack raz dla każdego łańcucha, aż funkcja zwrotna zacznie zwracać FALSE (czyli 0). GetStrings zwraca liczbę łańcuchów przekaza- nych do funkcji zwrotnej. Parametr pParam jest dalekim wskaŸnikiem do danych definiowanych przez programistę. Oczywiœcie, wszystko dodatkowo komplikuje unikod, a raczej koniecznoœć za- pewniania w STRLIB obshxgi aplikacji zarówno unikodowych, jak i nieunikodo- wych. Podobnie jak z EDRLIB, znajdujš się tu wersje A i W wszystkich funkcji. Wszystkie łańcuchy sš wewnętrznie w STRLIB przechowywane jako łańcuchy unikodowe. Jeżeli biblioteki STRLIB używjš programu nieunikodowego (czyli wy- wołujšcego funkcje AddStringA, DeleteStringA lub GetStringsA), wówczas łańcu- chy sš przekształcane na i z wersji unikodu. Obszar roboczy skojarzony z projektami STRPROG i STRLIB nazywa się STR- PROG. Pliki sš zbudowane tak samo jak obszar EDRTEST. Na rysunku 21-3 przed- stawiono dwa pliki niezbędne do utworzenia moduhx biblioteki dynamicznej STRLIB.DLL. STRLIB.H /* Plik nagłówkowy STRLIB.H */ lli fdef ţcpl uspl us lldefine EXPORT extern "C" declspec (dllexport) llel se lldefine EXPORT ţdeclspec (dllexport) Itendif // Maksymalna liczba lańcuchów, jakš przechowa STRLIB, i ich dlugoœć Ildefine MAX_STRINGS 256 ţIdefine MAX LENGTH 63 // W definicji typu funkcji zwrotnej użyto zwyklych łańcuchów typedef BOOL (CALLBACK * GETSTRCB) (PCTSTR, PVOID) ; // Każda funkcja ma wersję ANSI i unikodowd EXPORT BOOL CALLBACK AddStringA (PCSTR) ; EXPORT BOOL CALLBACK AddStringW (PCWSTR) ; EXPORT BOOL CALLBACK DeleteStringA (PCSTR) ; EXPORT BOOL CALLBACK DeleteStringW (PCWSTR) ; 1118 Częœć III, Zagadnienia zaawansowane (cišg dalszy ze strony 1117) EXPORT int CALLBACK GetStringsA (GETSTRCB, PVOID) ; r EXPORT int CALLBACK GetStringsW (GETSTRCB, PVOID) ; ll Użyć należy wersji wlaœciwej dla danego identyfikatora UNICODE 4tifdef UNICODE ţtdefine AddString AddStringW tfdefine DeleteString DeleteStringW ttdefine GetStrings GetStringsW ltel se ţţdefine AddString AddStringA ţtdefine DeleteString DeleteStringA ttdefine GetStrings GetStringsA ţţendi f STRLIB.C /* STRLIB.C - Modul biblioteki dla programu STRPROG (c) Charles Petzold, 1998 */ ţţinclude ltinclude // dla funkcji na lańcuchach szerokich znaków ' iţinclude "strlib.h" // sekcja pamięci wspólnej (wymaga w opcjach konsolidacji cišgu // SECTION:shared,RWS) lţpragma data_seg ("shared") int iTotal = 0 ; WCHAR szStrings [MAX_STRINGS][MAX_LENGTH + 1] = ( \0ţ ) ; ţţpragma dataţ seg ( ) ' tţpragma comment(linkerţ,"/SECTION:shared,RWS") , int WINAPI DllMain (HINSTANCE hInstance, DWORD fdwReason, PVOID pvReserved) ( return TRUE ; 1 EXPORT BOOL CALLBACK AddStringA (PCSTR pStringIn) ( BOOL bReturn ; int iLength ; PWSTR pWideStr ; // Przeksztalcenie ciagu na unikod i wywolanie AddStringW iLength = MultiByteToWideChar (CP ACP, 0, pStringIn, -1, NULL, 0) ; pWideStr = malloc (iLength) ; MultiByteToWideChar (CP ACP, 0, pStringIn, -1, pWideStr, iLength) ; , bReturn = AddStringW (pWideStr) ; free (pWideStr) ; return bReturn ; ? Rozdział 21: Biblioteki dynamiczne 1119 EXPORT BOOL CALLBACK AddStringW (PCWSTR pStringIn) PWSTR pString ; int i, iLength ; if (iTotal = MAX_STRINGS - 1) return FALSE ; if ((iLength = wcslen (pStringIn)) = 0) return FALSE ; // Rezerwacja pamięci na cišg, skopiowanie lańcucha, ' // konwersja na duże litery pString = malloc (sizeof (WCHAR) * (1 + iLength)) ; ţ wcscpy (pString, pStringIn) ; wcsupr (pString) ; ' // Alfabetyzacja ciagów = for (i = iTotal ; i > 0 ; i ) ( if (wcscmp (pString, szStringsCi - 1]) >= 0) break ; wcscpy (szStrings[i], szStringsCi - 1]) ; wcscpy (szStringsCi], pString) ; iTotal++ ; free (pString) ; return TRUE ; i EXPORT BOOL CALLBACK DeleteStringA (PCSTR pStringIn) s. BOOL bReturn ; int iLength ; PWSTR pWideStr ; // Przeksztalcenie łańcucha na unikod i wywolanie DeleteStringW i, iLength = MultiByteToWideChar (CP ACP, 0, pStringIn, -1, NULL, 0) ; pWideStr = malloc (iLength) ; MultiByteToWideChar (CP_ACP, 0, pStringIn, -1, pWideStr, iLength) ; bReturn = DeleteStringW (pWideStr) ; t . free (PWideStr) ; F':. . ' return b.Return ; EXPORT BOOL CALLBACK DeleteStringW (PCWSTR pStringIn) int i, j ; if (0 == wcslen (pStringIn>) 1120 Częœć III: Zagadnienia zaawansowane (cišg dalszy ze strony 1117) return FALSE ; for (i = 0 ; i < iTotal ; i++) ( if ( wcsicmp (szStrings[i], pStringIn) == 0) break ; ) // Jeżeli danego lšńcucha nie ma na liœcie, // powrót bez podejmowania działań if (i == iTotal) return FALSE ; // W przeciwnym wypadku skorygowanie listy for (j = i ; j < iTotal ; j++) wcscpy (szStrings[j], szStrings[j + 1]) ; szStrings[iTotal ][0] = ţ\0ţ , return TRUE ; ) EXPORT int CALLBACK GetStringsA (GETSTRCB pfnGetStrCallBack, PVOID pParam) ( BOOL bReturn ; int i, iLength ; PSTR pAnsiStr ; for (i = 0 ; i < iTotal ; i++) ( // Przekształcenie lańcucha z unikodu iLength = WideCharToMultiByte (CP_ACP, 0, szStrings[i], -1, NULL, 0, NULL, NULL) ; pAnsiStr = malloc (iLength) ; WideCharToMultiByte (CP ACP, 0, szStrings[i], -1, pAnsiStr, iLength, NULL, NULL) ; // Wywołanie funkcji zwrotnej bReturn = pfnGetStrCallBack (pAnsiStr, pParam) ; if (bReturn == FALSE) return i + 1 ; free (pAnsiStr) ; ) return iTotal ; ) EXPORT int CALLBACK GetStringsW (GETSTRCB pfnGetStrCallBack, PVOID pParam) BOOL bReturn ; int i ; for (i = 0 ; i < iTotal ; i++) Rozdział 21: Biblioteki dynamiczne 1121 ( bReturn = pfnGetStrCallBack (szStringsCi7, pParam) ; if (bReturn == FALSE> return i + 1 ; ) return iTotal ; Rysunek 21-3. Biblioteka STRLIB Oprócz funkcji DllMain STRLIB zawiera jeszcze tylko szeœć irmych funkcji. Będš one wyeksportowane do użytku przez inne programy. Wszystkie te funkcje sš zdefiniowane słowem kluczowym EXPORT, dzięki czemu zostanš umieszczone na liœcie w bibliotece STRLIB.LIB. Program STRPROG ' Program STRPROG, pokazany na rysunku 21-4, jest doœć prosty. Jego menu za- wiera dwie opcje: Enter i Delete, które wyœwietlajš okna dialogowe umożliwiajš- ce wpisanie łańcucha. STRPROG wywołuje AddString lub DeleteString. Kiedy pro- gram musi dokonać aktualizacji obszaru roboczego, wywołuje funkcję GetStrings i wyœwietla listę łańcuchów za pomocš funkcji GetStrCallBack. STRPROG.C /* STRPROG.C - Program używajšcy biblioteki dynamicznej STRLIB (c) Charles Petzold, 1998 */ ` ttinclude ilinclude "strlib.h" itinclude "resource.h" typedef struct ( HDC hdc : int xText ; int yText ; int xStart ; a int yStart ; int xIncr ; int yIncr ; int xMax : int yMax ; I t. CBPARAM ; 1 = LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; I TCHAR szAppName C] = TEXT ("StrProg") ; TCHAR szString CMAXţLENGTH + 17 : int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow> ţ... c 1122 Częœć III: Zagadnienia zaawansowane (cišg dalszy ze strony 1121) t HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CS VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION> ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = szAppName ; wndclass.lpszClassName = szAppName ; , if (!RegisterClass (&wndclass)) MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MB ICONERROR) ; return 0 ; 1 hwnd = CreateWindow (szAppName, TEXT ("DLL Demonstration Program"), WS_OVERLAPPEDWINDOW, CW USEDEFAULT, CW_USEDEFAULT, CW USEDEFAULT, CW USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) ( TranslateMessage (&msg) ; DispatchMessage (&msg) ; ) return msg.wParam ; ? BOOL CALLBACK DlgProc (HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam) ( switch (message) ( case WM_INITDIALOG: SendDlgItemMessage (hDlg, IDCţSTRING, EM_LIMITTEXT, MAX LENGTH, 0) ; return TRUE ; case WM COMMAND: switch (wParam) r case IDOK: GetDlgItemText (hDlg, IDCţSTRING, szString, MAX__LENGTH) ; EndDialog (hDlg, TRUE) ; return TRUE ; Rozdział 21: Biblioteki dynamiczne 1123 , case IDCANCEL: EndDialog (hDlg, FALSE) ; return TRUE ; l return FALSE ; BOOL CALLBACK GetStrCallBack (PTSTR pString, CBPARAM * pcbp) ( TextOut (pcbp->hdc, pcbp->xText, pcbp->yText, pString, lstrlen (pString)) ; if ((pcbp->yText += pcbp->yIncr) > pcbp->yMax) , pcbp->yText = pcbp->yStart ; if ((pcbp->xText += pcbp->xIncr) > pcbp->xMax) return FALSE ; ,: return TRUE ; ) LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) , static HINSTANCE hInst ; static int cxChar, cyChar, cxClient, cyClient ; static UINT iDataChangeMsg ; CBPARAM cbparam ; HDC hdc ; PAINTSTRUCT ps ; TEXTMETRIC tm ; œ ts switch (message) case WM CREATE: hInst = ((LPCREATESTRUCT) lParam)->hInstance ; hdc = GetDC (hwnd) ; GetTextMetrics (hdc, &tm) ; cxChar = (int) tm.tmAveCharWidth ; cyChar = (int) (tm.tmHeight + tm.tmExternalLeading) ; I; ReleaseDC (hwnd, hdc) ; // Rejestrowanie komunikatu powiadamiajšcego o zmianach danych iDataChangeMsg = RegisterWindowMessage (TEXT ("StrProgOataChange")) ; ,'i return 0 ; i3: i case WM COMMAND: =. switch (wParam) case IDM_ENTER: 5 if (DialogBox (hInst, TEXT ("EnterDlg"), hwnd, &DlgProc)) ! if (AddString (szString)) PostMessage (HWND BROADCAST, iDataChangeMsg, 0, 0) ; else MessageBeep (0) ; , 1124 Częœć IIIţ Zagadnienia zaawansowane (cigg dalszy ze strony 1223) 1 c break ; case IDM_DELETE: if (DialogBox (hInst, TEXT ("DeleteDlg"), hwnd, &DlgProc)) ( if (DeleteString (szString)) ~ PostMessage (HWNDţBROADCAST, iDataChangeMsg, 0, 0) ; else MessageBeep (0) ; l break ; ) return 0 ; case WM_SIZE: cxClient = (int) LOWORD (lParam) ; cyClient = (int) HIWORD (lParam) ; ! return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; l cbparam.hdc = hdc ; cbparam.xText = cbparam.xStart = cxChar ; cbparam.yText = cbparam.yStart = cyChar ; cbparam.xIncr = cxChar * MAX_LENGTH ; cbparam.yIncr = cyChar ; cbparam.xMax = cbparam.xIncr * (1 + cxClient / cbparam.xIncr) ; cbparam.yMax = cyChar * (cyClient / cyChar - 1) ; I GetStrings ((GETSTRCB) GetStrCallBack, (PVOID) &cbparam) ; EndPaint (hwnd, &ps) ; return 0 ; case WMţDESTROY: PostOuitMessage (0) ; return 0 ; default: if (message == iDataChangeMsg) InvalidateRect (hwnd, NULL, TRUE) ; break ; ) return DefWindowProc (hwnd, message, wParam, lParam) ; STRPROG.RC (fragmenty) //Microsoft Developer Studio generated resource script. Ilinclude "resource.h" Ilinclude "afxres.h" ///////////////////////////////////////////////////////////////////////////// // Dialog Rozdział 21: Biblioteki dynamiczne 1125 ENTERDLG DIALOG DISCARDABLE 20, 20, 186, 47 STYLE DSţMODALFRAME ţ WSţPOPUP ţ WS CAPTION ţ WSţSYSMENU CAPTION "Enter" FONT 8, "MS Sans Serif" BEGIN LTEXT "&Enter:",IDC_STATIC,7,7,26,9 EDITTEXT IDC_STRING,31,7,148,12,ES AUTOHSCROLL DEFPUSHBUTTON "OK",IDOK,32,26,50,14 PUSHBUTTON "Cancel",IDCANCEL,l04,26,50,14 END DELETEDLG DIALOG DISCARDABLE 20, 20, 186, 47 STYLE DS MODALFRAME ţ WSţPOPUP ţ WS CAPTION ţ WS SYSMENU CAPTION "Delete" FONT 8, "MS Sans Serif" BEGIN LTEXT "&Delete:",IDC_STATIC,7,7,26,9 EDITTEXT IDC_STRING,31,7,148,12,ES ŽUTOHSCROLL DEFPUSHBUTTON "OK",IDOK,32,26,50,14 PUSHBUTTON "Cancel",IDCANCEL,l04,26,50,14 END ///////////////////////////////////////////////////////////////////////////// // Menu STRPROG MENU DISCARDABLE BEGIN MENUITEM "&Enter!" IDM_ENTER MENUITEM "&Delete!", IDM_DELETE END RESOURCE.H (fragmenty) // Microsoft Developer Studio generated ţinclude file. // Used by StrProg.rc ţţdefine IDC_STRING 1000 ţţdefine IDM_ENTER 40001 ţţdefine IDM_DELETE 40002 ţţdefine IDC STATIC -1 Rysunek 21-4. Program STRPROG STRPROG.C zawiera dyrektywę włšczajšcš plik nagłówkowy STRLIB.H. W pli- ku tym sš zdefiniowane trzy funkcje z STRLIB, których będzie używał STRPROG. Najciekawsza właœciwoœć tego programu ujawnia się dopiero wtedy, kiedy uru- chomi się jego kilka instancji. STRLIB zapamiętuje łańcuchy znaków i wskaŸniki do nich w pamięci wspólnej, do której mogš mieć dostęp wszystkie instancje STR- PROG. Zobaczmy, jak się to dzieje. 1 126 Częœć III: Zagadnienia zaawansowane Współużytkowanie danych przez instancje programu STRPROG Windows tworzy mur wokół przestrzeni adresowej procesu Win32. Normalnie dane z tej przestrzeni sš prywafie, niewidoczne z innych procesów. Ale urucho- mienie kilku instancji STRPROG dowodzi, że STRLIB wszystkim instancjom pro- gramu pozwala na bezproblemowe współużytkowanie danych. Dodanie lub usu- nięcie łańcucha w oknie STRPROG jest natychmiast odzwierciedlane w pozosta- łych oknach. Dwie zmienne STRLIB sš dostępne wszystkim instancjom: tablica łańcuchów i liczba całkowita informujšca o liczbie zapisanych łańcuchów. Obie zmienne sš przechowywane w specjalnej sekcji pamięci, która została oznaczona jako wspólna: ilpragma data_seg ("shared") int iTotal = 0 ; WCHAR szStrings CMAX STRINGS]CMAX LENGTH + 1] = ( _\0ţ ) ; ţlpragma data seg () Pierwsza instrukcja #pragma tworzy sekcję danych, tutaj o nazwie shared. Sekcje można nazywać zupełnie dowolrue. Wszystkie zmienne zainicjowane po instrukcji #pragma trafiajš do sekcji shared. Druga instrukcja #pragma zamyka sekcję. Bar- dzo ważne jest odpowiednie (szczególne) zainicjowanie zmiennych; bez niego kompilator umieœci je w sekcji normalnej, niezainicjowanej, a nie w sekcji shared. O sekcji shared należy też poinformować program konsolidujšcy. W oknie dialo- gowym Project Settings zaznacz kartę Link. W polu Project Options dla projektu STRLIB (w konfiguracjach Release i Debug) należy umieœcić następujšcy argu- ment konsolidatora: /SECTION:shared,RWS Litery RWS oznaczajš, że sekcja ma atrybuty odczytu (R), zapisu (W) i współ- użytkowania (S). Ewentualnie opcje konsolidacji można okreœlić bezpoœrednio w Ÿródle biblioteki dynamicznej, jak to jest zrealizowane w STRLIB.C: ilpragma comment(linker,"/SECTION:shared,RWS") Sekcja pamięci wspólnej umożliwia współużytkowanie zmiennych iTotal (liczba całkowita) i szStrings (tablica łańcuchów) przez wszystkie instancje STRLIB. Po- nieważ MAXţSTRINGS równa się 256, a MAX LENGTH 63, sekcja pamięci wspól- nej ma 32 772 bajty długoœci: 4 bajty zajmuje zmienna iTotal, a 128 bajtów każdy z 256 wskaŸników. Korzystanie z sekcji pamięci wspólnej jest prawdopodobnie najprostszym sposo- bem współużytkowania danych przez kilka aplikacji. Jeżeli obszar pamięci wspól- nej trzeba rezerwować dynamicznie, należy się przyjrzeć używaniu obiektów odwzorowań w pliki, udokumentowanych w temacie /Platform SDK/Windows Base Services/Interprocess Communication/File Mapping. Różne tematy zwišzane z bibliotekami dynamicznymi Wspomniałem już wczeœniej, że moduł biblioteki dynamicznej nie może otrzy- mywać komunikatów. Może on jednak wywoływać funkcje GetMessage i PeekMes- sage. Komunikaty wycišgane przez bibliotekę z kolejki za pomocš tych funkcji Rozdział 21: Biblioteki dynamiczne 1127 stanowiš komunikaty adresowane do programu, który wywołuje funkcje biblio- teki. Ogólnie rzecz bioršc, biblioteka działa w imieniu programu wywołujšcego jej funkcje - zasada ta obowišzuje większoœć funkcji Windows wywoływanych przez bibliotekę. Biblioteka dynamiczna może ładować zasoby (na przykład ikony, łańcuchy zna- ków i bitmapy) - albo z pliku biblioteki, albo z pliku programu wywołujšcego. Funkcje służšce do ładowania zasobów wymagajš uchwytów instancyjnych. Je- żeli biblioteka używa swojego własnego uchwytu instancyjnego (który otrzymu- je podczas inicjacji), otrzymuje zasoby ze swojego własnego pliku. Aby załado- wać zasoby z pliku EXE programu wywołujšcego, biblioteka potrzebuje uchwy- tu instancyjnego tego programu. Rejestrowanie klas okien i tworzenie okien z poziomu biblioteki jest doœć cieka- we. Uchwytu instancji wymaga zarówno struktura klasy okna, jak i funkcja Cre- ateWindow. Choć podczas tworzenia klasy okna oraz okna można użyć uchwytu biblioteki, komunikaty okien będš i tak przechodzić przez kolejkę komunikatów programu wywołujšcego, jeżeli to biblioteka utworzy okno. Gdy trzeba utworzyć klasę okna lub okno z poziomu biblioteki, prawdopodobnie najlepiej jest użyć uchwytu instancyjnego programu wywołujšcego. Ponieważ komunikaty dla modalnych okien dialogowych sš odbierane spoza pętli komunikatów programu, okno modalne z poziomu biblioteki można utworzyć za pomocš funkcji DialogBox. Uchwytem instancyjnym może być uchwyt biblio- teki, a argumentem hwndParent funkcji DialogBox może być NULL. Dynamiczna konsolidacja bez importu Zazwyczaj to system Windows wykonuje dynamicznš konsolidację. Robi to w chwili załadowania programu do pamięci. Ale konsolidowanie programu z mo- dułem bibliotecznym możliwe jest w trakcie działania programu. Na przykład funkcję Rectangle z reguły wywołuje się tak: Rectangle (hdc, xLeft, yTop, xRight, yBottom); Takie wywołanie działa dlatego, że program został skonsolidowany z bibliotekš importowš GDI32.LIB, która zapewniła adres funkcji Rectangle. Ale funkcję Rectangle można wywołać okrężnš drogš. Należy najpierw, używa- jšc dyrektywy typedef, zdefiniować typ funkcji dla Rectangle: typedef BOOL (WINAPI * PFNRECT) (HDC, int, int, int, int); Następnie należy zdefiniować dwie zmienne: HANDLE hLibrary; PFNRECT pfnRectangle; Teraz hLibrary nadaje się wartoœć uchwytu biblioteki, a IpfnRectangle wartoœć ad- resu funkcji Rectangle: hLibrary = L.oadLibrary (TEXT ("GDI32.DLL")) pfnRectangle = (PFNPRECT) GetProcAddress (hLibrary, TEXT ("Rectangle")) Funkcja LoadLibrarţ zwraca NULL, jeżeli nie można odnaleŸć pliku biblioteki albo jeżeli wystšpiš inne trudnoœci. Teraz można wywołać funkcję i zwolnić bibliote- kę: Częœć III: Zagadnienia zaawansowane pfnRectangle (hdc, xLeft, yTop, xRight, yBottom); FreeLibrary (hLibrary); Choć omawiana technika dynamicznej konsolidacji podczas działania programu nie ma specjalnego sensu w przypadku funkcji ftectangle, może się przydać wów- czas, gdy nazwa biblioteki dynamicznej jest niewiadoma do momentu urucho- mierua programu. W powyższym kodzie użyto funkcji LoadLibrary i FreeLibrary. Windows ewiden- cjonuje dla wszystkich modułów bibliotek liczniki odwołań. Funkcja LoadLibrary zwiększa licznik odwołań. Licznik jest również zwiększany wtedy, gdy Windows ładuje program używajšcy biblioteki. FreeLibrary zmniejsza licznik odwołań. Po- woduje jš również zakończenie instancji programu korzystajšcego z biblioteki. Kiedy licznik odwołań ma wartoœć 0, Windows może usunšć bibliotekę z pamię- ci, ponieważ biblioteka nie jest już potrzebna. Biblioteki z samymi zasobami Każda funkcja biblioteki dynamicznej, z której może korzystać program Windows czy inna biblioteka, musi zostać wyeksportowana. Biblioteka dynamiczna może jednak nie zawierać żadnych funkcji eksportowych. Co więc może zawierać? Otóż zasoby. Załóżmy, że pracujesz nad aplikacjš windowsowš, która wymaga kilku bitmap. Zazwyczaj umieszcza się je w skrypcie zasobów programu i ładuje do pamięci za pomocš funkcji LoadBitmap. Ale co zrobić, jeœli trzeba utworzyć kilka zesta- wów map bitowych, z których każdy będzie przeznaczony dla innej rozdzielczo- œci? Dobrze by było zapisać różne zestawy map bitowych w osobnych plikach , ponieważ użytkownik będzie potrzebował tylko jednego zestawu. Takimi plika- mi sš biblioteki z samymi zasobami. Na rysunku 21-5 pokazano, jak można utworzyć bibliotekę z samymi zasobami o nazwie BITLIB.DLL, zawierajšcš dziewięć bitmap. Poszczególne bitmapy wymie- nione sš w pliku BTTLIB.RC. Tam też każda z nich otrzymuje swój numer. Aby utwo- rzyć plik BITLIB.DLL, potrzeba dziewięciu bitmap o nazwach BTTMAPI.BMP, BIT MAP2.BMP itd. Możesz użyć bitmap z CD-ROM-u dołšczonego do tej ksišżki albo utworzyć własne grafiki za pomocš Visual C++. Bitmapy majš przypisane iden- tyfikatory od 1 do 9. BITLIB.C /* BITLIB.C - Punkt wejœcia dla biblioteki BITLIB (c) Charles Petzold, 1998 */ ilinclude int WINAPI DllMain (NINSTANCE hInstance, DWORD fdwReason, PVOID pvReserved) ( return TRUE ; řT Rozdział 21: Biblioteki dynamiczne 1129 ; BITLIB.RC (fragmenty) //Microsoft Developer Studio generated resource script. ţţinclude "resource.h" ţţinclude "afxres.h" ///////////////////////////////////////////////////////////////////////////// // Bitmap 1 BITMAP DISCARDABLE "bitmapl.bmp" 2 BITMAP DISCARDABLE "bitmap2.bmp" 3 BITMAP DISCARDABLE "bitmap3.bmp" 4 BITMAP DISCARDABLE "bitmap4.bmp" i 5 BITMAP DISCARDABLE "bitmap5.bmp" 6 BITMAP DISCARDABLE "bitmap6.bmp" 7 BITMAP DISCARDABLE "bitmap7.bmp" 8 BITMAP DISCARDABLE "bitmap8.bmp" 9 BITMAP DISCARDABLE "bitmap9.bmp" Rysunek 21-5. Biblioteka BITLIB ! Utwórz projekt BITLIB w przestrzeni roboczej SHOWBIT. W kolejnym projekcie o nazwie SHOWBIT (jak przedtem) utwórz program SHOWBIT, pokazany na rysunku 21-6. Nie definiuj jednak BITLIB jako obiektu zależnego SHOWBIT; w przeciwnym razie bowiem na etapie konsolidacji będzie potrzebny plik BI- TLIB.LIB, a ten nie jest tworzony, ponieważ BITLIB rue ma funkcji eksportowych. Zbuduj BITLIB i SHOWBIT osobno, nadajšc na przemian każdemu z nich status projektu aktywnego. = Program SHOWBIT.C odczytuje zasoby bitmap z BITLIB i wyœwietla je w obsza- rze roboczym. Można je przełšczać, naciskajšc dowolny klawisz. ! SHOWBIT.C /* SHOWBIT.C - Wyœwietlanie bitmap z biblioteki BITLIB (c) Charles Petzold, 1998 */ ţţinclude LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; TCHAR szAppName [) = TEXT ("ShowBit") ; I int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) i HWND hwnd ; MSG msg ; WNDCLASS wndclass ; . 1130 Częœć III: Zagadnienia zaawansowane (cišg dalszy ze strony 1129) wndclass.style = CS_HREDRAW ţ CSţVREDRAW ; S wndclass.lpfnWndProc = WndProc ; wndc7ass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; ' wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) ( MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MBţICONERROR) ; ! 1 i hwnd = CreateWindow (szAppName, TEXT ("Show Bitmaps from BITLIB (Press Key)"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; if (!hwnd) return 0 ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) . TranslateMessage (&msg) ; DispatchMessage (&msg) ; ) return msg.wParam ; ) void DrawBitmap (HDC hdc, int xStart, int yStart, HBITMAP hBitmap) BITMAP bm ; HDC hMemDC ; POINT pt ; hMemDC = CreateCompatibleDC (hdc) ; SelectObject (hMemDC, hBitmap) ; GetObject (hBitmap, sizeof (BITMAP), &bm) ; pt.x = bm.bmWidth ; pt.y = bm.bmHeight ; ) BitBlt (hdc, xStart, yStart, pt.x, pt.y, hMemDC, 0, 0, SRCCOPY) ; , DeleteDC (hMemDC) ; ) LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) Rozdział 21: Biblioteki dynamiczne 1131 I static HINSTANCE hLibrary ; static int iCurrent = 1 ; HBITMAP hBitmap ; HDC hdc ; PAINTSTRUCT ps ; switch (message) ( case WM_CREATE: if ((hLibrary = LoadLibrary (TEXT ("BITLIB.DLL"))) == NULL) MessageBox (hwnd, TEXT ("Can't load BITLIB.DLL."), szAppName, 0) ; return -1 ; return 0 ; case WM_CHAR: if (hLibrary) ( iCurrent ++ InvalidateRect (hwnd, NULL, TRUE) ; 1 return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; if (hLibrary) ( hBitmap = LoadBitmap (hLibrary, MAKEINTRESOURCE (iCurrent)) ; 'Œ if (!hBitmap) ( iCurrent = 1 ; hBitmap = LoadBitmap (hLibrary, MAKEINTRESOURCE (iCurrent)) ; 1 if (hBitmap) ( DrawBitmap (hdc, 0, 0, hBitmap) ; Delete0bject (hBitmap) ; ) ) EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: if (hLibrary) FreeLibrary (hLibrary) ; PostQuitMessage (0) ; return 0 ; ) return DefWindowProc (hwnd, message, wParam, lParam) ; ) Rysunek 21-6. Program SHOWBIT 1132 Częœć III: Zagadnienia zaawansowane Podczas obsługi komunikatu WMţCREATE SHOWBIT otrzymuje uchwyt do BIT LIB.DLL: if ((hLibrary = LoadLibrary (TEXT ("BITLIB.DLL"))) == NULL) Jeżeli biblioteka BITLIB.DLL nie będzie się znajdowała w tym samym katalogu co SHOWBIT.EXE, Windows rozpocznie szukania jej we wszystkich stosownych katalogach (w jakich, informowałem w tym rozdziale). Jeżeli funkcja LoadLibrary zwróci NULL, wówczas SHOWBTT wyœwietli okno komunikatu informujšce o błę- dzie i z obshzgi komunikatu WM CREATE zwróci -1. Spowoduje to, że Create- Window wywoła funkcję WinMain, która zwróci NULL, i program się zakończy. SHOWBIT może pozyskać uchwyt do bitmapy wywołaniem LoadBitmap z uchwy- tem biblioteki i numerem bitmapy jako argumentami: hBitmap = LoadBitmap (hLibrary, MAKEINTRESOURCE (iCurrent)); Spowoduje to zwrócenie błędu, jeżeli bitmapa odpowiadajšca numerowi iCur- rent jest nieprawidłowa albo jeżeli zabrakło pamięci na załadowanie tej mapy. Program SHOWBIT zwalnia bibliotekę podczas obsługi komunikatu WM DE- STROY: FreeLibrary (hLibrary); Kiedy zakończy się instancja programu SHOWBIT, licznik odwołań biblioteki BITLIB.DLL osišgnie wartoœć 0 i pamięć zajmowana przez niš zostanie zwolnio- na. Jak widać, jest to prosta metoda zaimplementowania programu do "klipar- tów", który mógłby wczytywać jakieœ gotowe grafiki bitowe (bšdŸ też metapliki albo metapliki rozszerzone) do Schowka, aby korzystały z nich inne programy. Rozdział 22 DŸ ' ' m ki muz k a Integracja dŸwięku, muzyki i wideo w Microsoft Windows odbywała się ewolu- cyjnie. Obsługa multimediów rozpoczęła się od systemu zwanego rozszerzenia- mi multimedialnymi Windows (Multimedia Extensions to Windows). Pojawił się on w 1991 roku. W 1992 roku wydanie Windows 3.1 uczyniło z obsługi multime- diów jeszcze jednš kategorię API. Ostatnimi laty CD-ROM-y i karty dŸwiękowe, które na poczštku lat dziewięćdziesištych należały do rzadkoœci, stały się stan- dardem wœród komputerów osobistych. Niewielu ludzi trzeba specjalnie prze- konywać do tego, że multimedia stanowiš o użytecznym wymiarze aplikacji gra- ficznych Windows, dzięki któremu komputer staje się czymœ więcej niż tylko licz- bołamaczem i maszynš do pisania. Windows i multimedia W pewnym sensie w multimediach chodzi o uzyskanie dostępu do różnych skład- ruków sprzętowych za poœredructwem funkcji niezależnych od urzšdzeń. Przyj- rzyjmy się więc najpierw tym urzšdzeniom, a potem strukturze interfejsu API zwišzanego z multimedialnš częœciš Windows. Urzšdzenia systemów multimedialnych Jednym z urzšdzeń najczęœciej spotykanych w systemach multimedialnych jest odtwarzacz dŸwięku zapisanego w postaci kształtu fali (ang. waveform audio de- vice), nazywany powszechnie kartš dŸwiękowš lub kartš muzycznš. Urzšdzenie waveform audio przekształca wejœcie mikrofonowe lub dowolne inne analogo- we wejœcie dŸwiękowe na próbki cyfrowe, które zapisuje w pamięci lub plikach z rozszerzeniem .WAV. Może również dokonać konwersji odwrotnej: z postaci waveform do dŸwięku analogowego, który można potem usłyszeć przez głoœni- ki. Karty dŸwiękowe zawierajš z reguły również urzšdzenie MIDI. MIDI to standar- dowy interfejs instrumentów muzycznych (ang. Musical Instrument Digital Inter- face). Urzšdzenia MIDI potrafiš odtwarzać nuty na konkretnych instrumentach w odpowiedzi na krótkie komunikaty binarne. Zwykle mogš też współpracować z innymi urzšdzeniami MIDI, które podłšcza się kablem na wejœciu karty, na przy- kład z klawiaturami. Do kart dŸwiękowych można często podłšczać zewnętrzne syntezatory MIDI. 1134 Częœć III: Zagadnienia zaawansowane Stacja CD-ROM dołšczana do większoœci sprzedawanych dzisiaj komputerów riajczęœciej odtwarza również zwykłe płyty audio. Urzšdzenie takie nazywa się CD Audio. Wyjœcia z urzšdzeń waveform audio, MIDI i CD audio sš miksowane pod kontrolš użytkownika w aplikacji o nazwie Głoœnoœć (Volume Control). Jest kilka innych urzšdzeń systemu multimedialnego, które nie wymagajš dodat- kowego sprzętu. Urzšdzenie AVI (Video for Windows) shzży do odtwarzania pli- ków z filmami i animacjami, z rozszerzeniem .AVI (ang. audio-video interleave). Urzšdzenie ActiveMovie służy do odtwarzania innych typów filmów, w tym QuickTime i MPEG. Wyspecjalizowane urzšdzenia wspomagajšce ich odtwarza- nie może zawierać karta graficzna komputera PC. Rzadziej spotyka się użytkowników majšcych odtwarzacze dysków laserowych Pioneera albo magnetowidy kasetowe VISCA firmy Sony. Urzšdzenia te majš interfejsy cyfrowe, a więc mogš być obsługiwane przez oprogramowanie PC. Niektóre karty graficzne majš funkcję o nazwie wideo w oknie, dzięki której można w jednym z okien wyœwietlać zewnętrzny sygnał wizualny. To również uznaje się za urzšdzenie systemów multimedialnych. Przeglšd API Przewidziana w API obsługa systemu multimedialnego Windows opiera się na dwóch interfejsach: niskiego i wysokiego poziomu. Interfejsy niskiego poziomu stanowiš zbiór funkcji, które zaczynajš się krótkim opisowym prefiksem i sš wymienione (obok funkcji wysokiego poziomu) w /Plat- form SDK/Graphics and Multimedia Seroices/Multimedia Reference/Multimedia Func- tions. W tym rozdziale przyjrzymy się niskopoziomowym funkcjom wejœcia i wyjœcia waveform audio. Zaczynajš się one prefiksami waveln lub wave0ut. Również w tym rozdziale opisane zostanš funkcje midi0ut, za pomocš których można ste- rować wyjœciowym urzšdzeniem MIDI. API zawiera także funkcje midiln i midi- Stream. Omówię ponadto funkcje, których nazwy zaczynajš się prefiksem time. Umożli- wiajš one skonfigurowanie wywłaszczajšcej procedury zegara o wysokiej rozdziel- czoœci, odmierzajšcego w dół co 1 milisekundę. Mechanizm ten jest przewidzia- ny głównie do odtwarzania sekwencji MIDI. Jest jeszcze kilka innych grup funk- cji, które wišżš się z kompresjš dŸwięku, kompresjš obrazu, animacjami i sekwen- cjami wideo. Niestety, zabrakło już na nie miejsca w tym rozdziale. Na liœcie funkcji multimedialnych znajduje się także siedem funkcji z prefiksem mci. Umożliwiajš one dostęp do interfejsu MCI (Media Control Interface). Jest to otwarty interfejs wysokiego poziomu, przeznaczony do sterowania sprzętem multimediów komputera. MCI zawiera kilka poleceń wspólnych wszystkim urzš- dzeniom systemu multimedialnego. Jest to możliwe dlatego, że pod metaforš odtwarzania/rejestrowania można ukryć wiele aspektów multimedialnych. Naj- pierw należy otworzyć urzšdzenie do odczytu lub zapisu, potem można rejestro- wać (zapis) lub odtwarzać (odczyt) dane, a na końcu urzšdzenie należy zamknšć. MCI ma dwie postacie. W pierwszej do MCI wysyła się komunikaty podobne do -ţr Rozdziat 22: Dtwlęk i muryka 1135 komunikatów Windows. Zawierajš one zakodowane bitowo flagi i struktury da- nych języka C. W drugiej postaci do MCI wysyła się łańcuchy znaków. Tę opcję przewidziano głównie dla języków skryptowych, które majš z reguły rozbudo- wane funkcje manipulacji na łańcuchach znaków, ale nie gwarantujš obshzgi wy- wołań API systemu Windows. Druga postać MCI, oparta na łańcuchach znaków, nadaje się również wyœmierucie do interakcyjnego eksplorowania MCI i do na- uki. Zaraz się tym zajmiemy. Nazwy urzšdzeń MCI to cdaudio, waveaudio, sequen- cer (MIDI), videodisc, vcr, overlay (analogowe wideo w oknie), dat (cyfrowa taœma audio) i digftalvideo. Urzšdzenia MCI sš podzielone na proste i złożone. Proste (takie jak cdaudio) nie używajš plików. Złożone (takie jak waveaudio) używajš pli- ków, w przypadku waveform audio plików z rozszerzeniem .WAV. Innym sposobem uzyskiwania dostępu do urzšdzeń multimedialnych jest posłu- giwanie się systemem DirectX API, który wykracza niestety poza ramy tej ksišż- ki. Wspomrueć należy jeszcze o dwóch innych wysokopoziomowych funkcjach mul- timedialnych: MessageBeep i PlaySound. Zostały one zaprezentowane w rozdzia- le 3. MessageBeep shxży do odtwarzania dŸwięków okreœlonych apletem DŸwięki z Panelu sterowania. PIaySound może odtwarzać pliki .WAV z dysku, z pamięci albo z załadowanych zasobów. Funkcja PlaySound pojawi się jeszcze raz, w dal- szej częœci tego rozdziału. Eksplorujemy MCI - program TESTMCI Kiedyœ, na poczštku ery multimediów w Windows, pakiet do pisania programów windowsowych zawierał program w C o nazwie MCITEST. Pozwalał on progra- mistom wpisywać interakcyjrue polecenia MCI i sprawdzać, jak one działajš. Pro- gram ten, przynajmruej w swojej wersji C, ostatnio zniknšł. Zrekonstruowałem więc go w postaci programu TESTMCI (patrz rysunek 22-1). Interfejs użytkowni- ka oparty jest na starym, sprawdzonym programie MCITEST, ale właœciwy kod jest nowy, choć nie sšdzę, że się dużo różni. TESTMCI.C /* TESTMCI.C - Program do testowania poleceń MCI (c) Charles Petzold, 1998 */ lţinclude ţţinclude "resource.h" ţţdefine ID TIMER 1 BOOL CALLBACK DlgProc (HWND, UINT, WPARAM, LPARAM) ; TCHAR szAppName C] = TEXT ("TestMci") ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) ;:. , 1136 Częœć III: Zagadnienia zaawansowane (cišg dalszy ze strony 1135) i if (-1 = DialogBox (hInstance, szAppName, NULL, DlgProc)) MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MBţICONERROR) ; return 0 ; BOOL CALLBACK DlgProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { static HWND hwndEdit ; int iCharBe9, iCharEnd, iLineBeg, iLineEnd, iChar, iLine, iLength ; MCIERROR error ; ' RECT rect ; TCHAR szCommand C1024], szReturn C1024], szError C1024], szBuffer C32] ; switch (message) ( case WMţINITDIALOG: // wyœrodkowanie okna na ekranie `GetWindowRect (hwnd, &rect) ; SetWindowPos (hwnd, NULL, (GetSystemMetrics (SM_CXSCREEN) - rect.right + rect.left) / 2, (GetSystemMetrics (SM_CYSCREEN) - rect.bottom + rect.top) / 2. 0, 0, SWPţNOZORDER ţ SWPţNOSIZE) ; hwndEdit = GetDlgItem (hwnd, IDC MAINţEDIT) ; SetFocus (hwndEdit) ; ; return FALSE ; case WM COMMAND: switch (LOWORD (wParam)) ( case IDOK: // odnalezienie numerów linii odpowiadajacych zaznaczeniu i SendMessage (hwndEdit, EM GETSEL, (WPARAM) &iCharBeg, ' (LPARAM) &iCharEnd) ; iLineBeg = SendMessage (hwndEdit, EM_LINEFROMCHAR, iCharBeg, 0) ; iLineEnd = SendMessage (hwndEdit, EM LINEFROMCHAR, iCharEnd, 0) ; // przejœcie w pętli przez wszystkie linie for (iLine = iLineBeg ; iLine <= iLineEnd ; iLine++) i // pobranie i zakończenie linii; ignorowanie pustych * (WORD *) szCommand = sizeof (szCommand) / sizeof (TCHAR) ; iLength = SendMessage (hwndEdit, EM_GETLINE, iLine, (LPARAM) szCommand) ; szCommand CiLength] = '\0' , 1 Rozdział 22: Diwiţk i muryka 1137 if (iLength ţ 0) continue ; // wysłanie polecenia MCI error = mciSendString (szCommand, szReturn, sizeof (szReturn) / sizeof (TCHAR), hwnd) ; // ustawienie pola Return String SetDlgItemText (hwnd, IDCţRETURN STRING, szReturn) ; // ustawienie pola Error String (nawet jeżeli nie ma błędu) mciGetErrorString (error, szError, , sizeof (szError) / sizeof (TCHAR)) ; SetDlgItemText (hwnd, IDC ERROR STRING, szError) ; // wysłanie karetki na koniec ostatnio zaznaczonej linii iChar = SendMessage (hwndEdit, EM_LINEINDEX, iLineEnd, 0) ; iChar += SendMessage (hwndEdit EM LINELENGTH, iCharEnd, 0) ; SendMessage (hwndEdit, EM SETSEL, iChar, iChar) ; // wstawienie CR/LF SendMessage (hwndEdit, EMţREPLACESEL, FALSE, (LPARAM) TEXT ("\r\n")) ; SetFocus (hwndEdit) ; return TRUE ; case IDCANCEL: EndDialog (hwnd, 0) ; return TRUE ; case IDCţMAIN_EDIT: if (HIWORD (wParam) = EN ERRSPACE) MessageBox (hwnd, TEXT ("Error control out of space."), szAppName, MB OK ţ MBţICONINFORMATION) ; return TRUE ; break ; l break ; case MM_MCINOTIFY: EnableWindow (GetDlgItem (hwnd, IDCţNOTIFV MESSAGE), TRUE) ; wsprintf (szBuffer, TEXT ("Device ID = %i"), lParam) ; SetDlgItemText (hwnd, IDC_NOTIFY_ID, szBuffer) ; EnableWindow (GetDlgItem (hwnd, IDCţNOTIFY ID), TRUE) ; EnableWindow (GetDlgItem (hwnd, IDC_NOTIFY SUCCESSFUL), wParam & MCIţNOTIFY SUCCESSFUL) ; EnableWindow (GetDlgItem (hwnd, IDCţNOTIFY_SUPERSEDED), '' wParam & MCI NOTIFY SUPERSEDED) ; 1138 Częœć III: Zagadnienia zaawansowane (cišg dalszy ze strony 1137) EnableWindow (GetDlgItem (hwnd, IDC_NOTIFY_ABORTED), wParam & MCI NOTIFY ŽBORTED) ; EnableWindow (GetDlgItem (hwnd, IDC_NOTIFY_FAILURE), wParam & MCI NOTIFY FAILURE) ; SetTimer (hwnd, IDţTIMER, 5000, NULL) ; return TRUE ; case WM_TIMER: KillTimer (hwnd, ID TIMER) ; EnableWindow (GetDlgItem (hwnd, IDC_NOTIFY_MESSAGE), FALSE) ; EnableWindow (GetDlgItem (hwnd, IDC_NOTIFY_ID), FALSE) ; ? EnableWindow (GetDlgItem (hwnd, IDC_NOTIFY_SUCCESSFUL), FALSE) ; EnableWindow (GetDlgItem (hwnd, IDC_NOTIFY_SUPERSEDED), FALSE) ; EnableWindow (GetDlgItem (hwnd, IDC NOTIFY_ABORTED), FALSE) ; ! EnableWindow (GetDlgItem (hwnd, IDCţNOTIFYţFAILURE), FALSE) ; return TRUE ; case WM SYSCOMMAND: switch (LOWORD (wParam)) ? case SC CLOS EndOialog (hwnd, 0) ; return TRUE ; ) break ; ) return FALSE ; TESTMCI.RC (fragmenty) //Microsoft Developer Studio generated resource script. ţţinclude "resource.h" ţţinclude "afxres.h" ///////////////////////////////////////////////////////////////////////////// // Dialog TESTMCI DIALOG DISCARDABLE 0, 0, 270, 276 STYLE WS_MINIMIZEBOX ţ WS VISIBLE ţ WS CAPTION ţ WS SYSMENU CAPTION "MCI Tester" FONT 8, "MS Sans Serif" BEGIN EDITTEXT IDC_MAIN_EDIT,8,8,254,100,ES MULTILINE ţ ES AUTOHSCROLL WS_VSCROLL ů LTEXT "Return String:",IDC_STATIC,8,114,60,8 EDITTEXT IDC_RETURN_STRING,B,126,120,50,ES_MULTILINE ES_AUTOVSCROLL ( ES_READONLY ţ WS_GROUP ţ NOT WS TABSTOP LTEXT "Error String:",IDCţSTATIC,l42,114,60,8 Rozdział 22: DŸwięk i muryka 1139 EDITTEXT IDCţERROR_STRING,l42,126,120,50,ES_MULTILINE ES AUTOVSCROLL ţ ESţREADONLY ţ NOT WS_TABSTOP GROUPBOX "MM_MCINOTIFY Message",IDC_STATIC,9,186,254,58 LTEXT "',IDC_NOTIFY_ID,26,198,100,8 LTEXT "MCI_NOTIFY SUCCESSFUL",IDCţNOTIFY_SUCCESSFUL,26,212,100 8,WS_DISABLED LTEXT "MCI_NOTIFY SUPERSEDED",IDCţNOTIFY_SUPERSEDED,26,226,100 8,WS_DISABLED LTEXT "MCIţNOTIFY_ABORTED",IDCţNOTIFY ŽBORTED,l44,212,100,8, WS_DISABLED LTEXT "MCI NOTIFY_FAILURE",IDCţNOTIFY FAILURE,l44,226,100,8, WS_DISABLED DEFPUSHBUTTON "OK",IDOK,57,255,50,14 PUSHBUTTON "Close",IDCANCEL,l62,255,50,14 END RESOURCE.H (fragmenty) // Microsoft Developer Studio generated include file. // Used by TestMci.rc ţţdefine IDC_MAIN_EDIT 1000 tidefine IDC_NOTIFY_MESSAGE 1005 ttdefine IDC_NOTIFY_ID 1006 ţţdefine IDC_NOTIFY_SUCCESSFUL 1007 ţţdefine IDC_NOTIFY_SUPERSEDED 1008 ţţdefine IDC_NOTIFY_ABORTED 1009 ţţdefine IDC_NOTIFY FAILURE 1010 4ţdefine IDC_SIGNAL_MESSAGE 1011 ţţdefine IDC_SIGNAL_ID 1012 iţdefine IDC_SIGNAL_PARAM 1013 define IDC_RETURN_STRING 1014 ţţdefine IDC_ERROR_STRING 1015 iţdefine IDC_DEVICES 1016 ţţdefine IDC STATIC -1 Rysunek 22-1. Program TESTMCI Głównym oknem programu TESTMCI, podobnie jak wielu innych programów z tego rozdziału, jest beztrybowe okno dialogowe. Program TESTMCI, jak wszyst- kie programy z tego rozdziału, wymaga, aby na stronie Links okna dialogowego Projects Settings w Microsoft Visual C++ znajdowała się biblioteka importowa WINMM.LIB. W programie zastosowano dwie najważniejsze funkcje multimedialne. Sš to mci- SendString i mciGetErrorText. Jeżeli wpisze się cokolwiek w głównyrn polu edycji okna TESTMCI i naciœnie [Enter) (albo kliknie przycisk OK), program przekaże wpisany łańcuch do funkcji mciSendString jako jej pierwszy argument: error = mciSendString (szCommand, szReturn, sizeof (szReturn) / sizeof (TCHAR), hwnd) : Jeżeli w oknie zaznaczonych będzie kilka lin, program wyœle je sekwencyjnie do funkcji mciSendString. Drugim argumentem jest adres łańcucha pobierajšcego informacje zwrotne z funkcji. Program wyœwietla te informacje w częœci okna o na- 1140 Częœć III: Zagadnienia zaawansowane zwie Return String. Kod błędu zwrócony z mciSendString jest przekazywany do funkcji mciGetErrorString w celu pobrania tekstowego opisu błędu; opis jest wy- œwietlany w częœci okna TESTMCI o nazwie Komunikat o błędzie. MCITEXT a CD audio Polecenia MCI można œwietnie poznać, przejmujšc kontrolę nad stacjš CD-ROM i odtwarzajšc płyty CD. Dobrze jest zaczšć właœnie od tego, ponieważ polecenia sterujšce odtwarzaniem CD-ROM-u sš często doœć proste i uzyskuje się wymier- ny efekt: możliwoœć słuchania muzyki. Podczas wykonywania tego ćwiczenia warto sobie otworzyć wykaz poleceń MCI: /Platform SDK/Graphics and Multime- dia Services/Multimedia Reference/Multimedia Command Strings. Podłšcz wyjœcie audio twojej stacji CD-ROM do głoœników albo słuchawek i włóż do niej płytę audio, na przykład Born to Run Bruce'a Springsteena. W Windows 98 może się automatycznie uruchomić Odtwarzacz, który bez namysłu rozpocz- nie granie płyty od pierwszego utworu. Przerwij wówczas odtwarzanie i wyłšcz tę aplikację. Uruchom TESTMCI i wpisz następujšce polecenie: open cdaudio Naciœruj Enter. Słowo open jest poleceniem MCI, a słowo cdaudio - nazwš urzš- dzenia, które MCI rozpoznaje jako stację CD-ROM (zakładam, że w systemie masz tylko jednš; jeżeli jestem w błędzie, nazwy poszczególnych stacji CD-ROM moż- na uzyskać za pomocš polecenia sysinfo). W częœci o nazwie Return String pojawi się komunikat, który system wysyła zwrotnie do programu za pomocš funkcji mciSendString. Jeżeli polecenie open zadziała, będzie to po prostu numer 1. W częœci pod tytułem Komunikat o błę- dzie pojawi się to, co mciGetErrorString zwróci dla wartoœci z mciSendString. Jeże- li mciSendString nie zwróci kodu błędu, pojawi się tam komunikat mówišcy, iż wydane polecenie zostało wykonane. Jeżeli polecenie open zostało przeprowadzone bezbłędnie, wpisz teraz: play cdaudio Odtwarzacz zacznie grać "Thunder Road", pierwszy utwór z płyty. Odtwarza- nie można zatrzymać za pomocš polecenia: pause cdaudio albo stop cdaudio W przypadku urzšdzenia cdaudio obie instrukcje majš identyczny skutek. Odtwa- rzanie można ponowić poleceniem: play cdaudio Na razie wszystkie łańcuchy, które pojawiły się w ćwiczeniu, składały się z pole- cenia i nazwy urzšdzenia. W niektórych poleceniach można podawać opcje. Wpisz na przykład: status cdaudio position W zależnoœci od tego, jak dhzgo słuchasz płyty, w polu Return String pojawi się coœ takiego: 01:15:25 Rozdział 22: DŸwięk i muryka 1141 Co to jest? Oczywiœcie nie godziny, minuty, sekundy, ponieważ płyta CD nie jest aż tak długa. Aby poznać format czasu, wpisz: status cdaudio time format W polu Return String pojawi się tekst: msf Oznacza on minuty, sekundy, ramki. Urzšdzenie CD audio odtwarza 75 ramek na sekundę. Liczba ramek może więc należeć do przedziału od 0 do 74. Polecenie status ma mnóstwo opcji. Za jego pomocš można poznać na przykład całkowitš długoœć płyty CD w formacie msf. Służy do tego następujšce polece- nie: status cdaudio length Dla płyty "Born to Run" będzie to: 39:28:19 Oznacza to 39 minut, 28 sekund i 19 ramek. Teraz spróbuj: status cdaudio number of tracks Program powinien wyœwietlić: 8 Z okładki wiem, że utwór tytułowy jest pišty na płycie. Utwory sš w poleceniach MCI numerowane od 1. Ile trwa piosenka "Born to Run", możemy więc spraw- dzić za pomocš polecenia: status cdaudio length track 5 Program powinien wyœwietlić: 04:30:22 Możemy również sprawdzić, gdzie się ona zaczyna: status cdaudio position track 5 Program powinien wyœwietlić: 17:36:35 Majšc tę informację, możemy skoczyć dokładnie do tego miejsca płyty: play cdaudio from 17:36:35 to 22:06:57 Polecenie to spowoduje odtworzenie jednej piosenki, a następnie zatrzymanie odtwarzania. Drugi parametr czasowy został obliczony przez dodanie 4:30:22 ńd„ugoœci utworu) do 17:36:35. Ale można go było wyznaczyć za pomocš polece- status cdaudio position track 6 Można też ewentualnie przełšczyć format czasu na utwory, minuty, sekundy, ramki. Robi się to tak: set cdaudio time format tmsf Potem pišty utwór można uruchomić tak: play cdaudio from 5:0:0:0 to 6:0:0:0 albo jeszcze proœciej: play cdaudio from 5 to 6 1142 Częœć III: Zagadnienia zaawansowane Te składniki parametru okreœlajšcego czas, które majš wartoœć 0, wolno opuœcić. Format czasu można ustawić na milisekundy. Na końcu każdego polecenia MCI można umieœcić opcję wait lub notify (albo i jed- nš, i drugš). Załóżmy, że chcesz odtworzyć tylko pierwszych 10 sekund piosenki Born to Run, a potem program ma zrobić coœ innego. Oto jak to można zrobić (przy założeniu, że format czasu jest ustawiony na tmsf): play cdaudio from 5:0:0 to 5:0:10 wait W tym przypadku funkcja mciSendString nie kończy działania, dopóki nie zosta- nie wykonane zlecone jej zadanie, to jest dopóki nie mirue 10 sekund piosenki Born to Run. Nie jest to oczywiœcie rozwišzanie eleganckie w programie jednowštkowym. Je- żeli przypadkiem wpiszesz: play cdaudio wait funkcja mciSendString nie odda sterowania, dopóki nie zostarue odtworzona cała płyta. Jeżeli musisz korzystać z opcji wait (co przydaje się podczas uruchamiania na œlepo skryptów MCI i co za chwilę zaprezentuję), użyj najpierw polecerua break. Pozwala ono zdefiniować kod klawisza, którym będzie można przerwać funkcję mciSendString i zwrócić sterowanie do programu. Aby zdefiniować do tego celu klawisz [Esc], należy poshxżyć się poleceniem: break cdaudio on 27 gdzie 27 jest dziesiętnš wartoœciš VKţESCAPE. Teraz opcja notify: play cdaudio from 5:0:0 to 5:0:10 notify W tym wypadku funkcja mciSendString zakończy działanie natychmiast, ale kie- dy zlecona poleceniem MCI operacja się zakończy, okno, którego uchwyt jest okreœlony jako ostatni argument mciSendString, otrzyma komunikat MM MCI- NOTIFY. Program TESTMCI wyœwietla rezultat tego komunikatu w polu MM MCINOTIFY. Aby uniknšć konfuzji - mogłeœ przecież wpisać w międzyczasie inne polecenia - program TESTMCI po 5 sekundach zatrzymuje wyœwietlanie rezultatów komunikatu MM MCINOTIFY. Słów kluczowych wait i notţ można używać łšcznie, ale raczej nie ma powo- dów, aby to robić. Bez nich bowiem domyœlne działarue polega na nieczekaniu i niepowiadamianiu, a więc na tym, o co nam najczęœciej chodzi. Kiedy przestanie cię bawić odtwarzanie płyty za pomocš poleceń MCI, zatrzy- maj Odtwarzacz następujšcym polecţniem: stop cdaudio Jeżeli przed zamknięciem programu rue zatrzyma się urzšdzenia, odtwarzanie będzie trwało nadal. Teraz wypróbuj jeszcze coœ, co może, ale nie musi działać w przypadku twojego urzšdzenia: eject cdaudio Na końcu zamknij urzšdzenie: close cdaudio Choć 'TESTMCI nie ma opcji wczytywarua ani zapisywania plików tekstowych, Rozdział 22: DŸwigk i muryka 1143 dhxższy tekst można przekopiować do programu przez Schowek. Dowolny tekst wpisany w TESTMCI można skopiować do Schowka (za pomocš kombinacji [Ctrl+Cj), wkleić w Notatruku, a potem zapisać w postaci pliku. Odwracajšc ten proces, do TESTMCI można załadować mały skrypt poleceń MCI. Jeżeli zazna- czy się kilka poleceń i naciœnie OK (albo klawisz [Enter]), program TESTMCI wy- kona je wszystkie naraz. Pozwala to konstruować skrypty, stanowišce po prostu listę poleceń MCI. Załóżmy, że chcesz poshzchać piosenek Jungleland (ostatnia na płycie), Thunder Road i Born to Run, w takiej właœnie kolejnoœci. Napisz więc skrypt: open cdaudio set cdaudio time format tmsf break cdaudio on 27 play cdaudio from 8 wait play cdaudio from 1 to 2 wait play cdaudio from 5 to 6 wait stop cdaudio eject cdaudio close cdaudio Bez dyrektyw wait skrypt nie działałby prawidłowo, bo funkcja mciSendString kończyłaby zaraz działanie po każdym poleceniu i natychmiast przechodziła do wykonywania następnego. Teraz chyba jesteœ już dobrze przygotowany do napisania prostego odtwarzacza płyt CD. Twój program może wyznaczyć liczbę utworów i długoœci każdego z nich. Może również pozwalać użytkownikowi uruchamiać odtwarzanie od dowolnego miejsca. Pamiętaj jednak, że funkcja mciSendString zawsze zwraca informacje w po- staci łańcucha znaków, więc będziesz musiał napisać parser, który przekonwertu- je je na liczby. Taki program może z pewnoœciš używać również zegara Windows. Podczas obsługi komunikatów WMţTIMER program może wywoływać: status cdaudio mode i sprawdzać w ten sposób, czy stacja CD-ROM zakończyła już odtwarzanie. Po- lecenie status cdaudio position pozwala programowi aktualizować ekran, aby użytkownik mógł obserwować bieżšce położenie płyty. Jest jeszcze inna ciekawa rzecz: jeżeli program zna poło- żenie czasowe kluczowych punktów muzyki, może z muzykš CD synchronizo- wać wyœwietlanš na ekranie grafikę. Nadaje się to œwietnie do tworzenia dŸwię- kowych instrukcji, własnych filinów graficzno-muzycznych oraz do gier. DŸwięk wave DŸwięk wave to jedna z najczęœciej używanych funkcji mulrimedialnych Windows. Urzšdzenia waveform andio mogš przechwytywać dŸwięki z mikrofonu, prze- kształcać je na postać cyfrowš i zapisywać w pamięci lub na dysku w postaci pli- ków wave (z rozszerzeniem .WAV). Tak zapisane dŸwięki można odtwarzać. 1144 Częœć III: Zagadnienia zaawansowane DŸwięk a kształt fali Przed zagłębieniem się w szczegóły zwišzane z funkcjami interfejsu API obsłu- gujšcymi waveform audio, należałoby poznać podstawy fizyki dŸwięku, mecha- nizm jego percepcji oraz sposób, w jaki dŸwięki dostajš się do komputera i w jaki się z niego wydostajš. DŸwięk to drgania. Ludzkie ucho odbiera je jako zmiany ciœnienia powietrza wywieranego na bębenki. Drgania może odbierać również mikrofon, który prze- kształca je na pršd elektryczny. Pršd ten można przesłać przez wzmacniacz do głoœników, które przywrócš pierwotnš postać dŸwięku - wibrację. W tradycyj- nych (analogowych) urzšdzeruach do zapisu dŸwięku (takich jak magnetofon taœmowy czy gramofon na płyty winylowe) drgania sš zapisywane jako impulsy magnetyczne lub uformowane specjalnie rowki. DŸwięk przekształcony w pršd elektryczny jest reprezentowany przez kształt fali powstajšcej na skutek zmiany napięcia elektrycznego w czasie. Najbardziej elementarnym kształtem fali jest sinusoida, której jeden okres pokazano we wczeœruejszej częœci ksišżki, na rysun- ku 5-7. Fala sinusoidalna charakteryzuje się dwoma parametrami: amplitudš (rozpięto- œciš między maksymalnym i minimalnym wychyleniem fali) oraz częstotliwoœciš. Amplitudę odbieramy jako głoœnoœć, a częstotliwoœć jako wysokoœć tonu. Ludz- kie uszy odbierajš dŸwięki o częstotliwoœciach z zakresu od 20 Hz (20 okresów na sekundę) do 20 000 Hz, chociaż nie każdy człowiek słyszy w całym przedzia- le, a dodatkowo zdolnoœci w tej mierze pogarszajš się z wiekiem. Człowiek odbiera częstotliwoœci w sposób logarytmiczny, nie liniowy. Oznacza to, że zmianę częstotliwoœci z 20 Hz na 40 Hz odbieramy tak samo jak zmianę z 40 Hz na 80 Hz. W muzyce podwojona częstotliwoœć wyznacza oktawę. Tak więc ludzkie ucho reaguje na około 10 oktaw. Zakres częstotliwoœci objęty piani- nem to nieco ponad 7 oktaw, od 27,5 Hz do 4186 Hz. Fale sinusoidalne, choć reprezentujš najbardziej naturalny typ drgań, występujš stosunkowo rzadko w naturze. Poza tym fala sinusoidalna nie ma interesujšcego brzmienia. Większoœć dŸwięków ma znacznie bardziej złożonš naturę. Każdy przebieg okresowy (to jest cyklicznie powtarzalny) można rozłożyć na wiele fal sinusoidalnych o częstotliwoœciach będšcych całkowitymi wielokrotnoœciami pewnej częstotliwoœci podstawowej. Nazywa się to rozkładem Fouriera, od na- zwiska francuskiego matematyka i fizyka imieniem Jean Baptiste Joseph Fourier (1768-1830). Częstotliwoœć podstawowa jest nazywana również bazowš. Pozo- stałe fale majš częstotliwoœci będšce 2-, 3-, 4-krotnoœciami (itd.) częstotliwoœci podstawowej. Sš to tak zwane górne tony składowe. Częstotliwoœć podstawowa jest nazywana także pierwszš harmonicznš. Pierwszy gómy ton składowy jest drugš harmonicznš, drugi ton jest trzeciš harmonicznš itd. O wynikowym brzmieniu dŸwięku decydujš wzajemne amplitudy harmonicz- nych. Cecha ta nosi miano barwy dŸwięku. To ona sprawia, że tršbka brzmi jak tršbka, a pianino jak pianino. Kiedyœ uważano, że elektroniczne syntezowanie barw tradycyjnych instrumen- tów muzycznych możliwe jest przez rozbicie dŸwięków oryginalnych na harmo- Rozdział 22: DŸwigk i muryka 1145 niczne i zrekonstruowanie ich za pomocš wielu zsumowanych przebiegów sinu- soidalnych. Okazało się jednak, że dŸwięki z rzeczywistego œwiata nie sš tak pro- ste. Po prostu dŸwięki instrumentów naturalnych nie sš z reguły wcale okreso- we. Względne intensywnoœci harmonicznych sš różne w zakresie dŸwięków wydawanych przez jeden instrument, a udział poszczególnych harmonicznych zmienia się (inaczej) podczas grania poszczególnych nut. Istotne znaczenie dla naszego odbierara barwy danego instrumentu ma poczštek brzmienia danej nuty , nazywany atakiem. Sam atak może mieć bardzo złożonš naturę. Ze względu na postęp w technikach zapisywania i przechowywania danych, z ja- kim mamy do czynienia w cišgu ostatnich kilku lat, możliwe stało się zapisywa- nie dŸwięków od razu w postaci cyfrowych kształtów fal, bez koniecznoœci prze- chodzenia przez ułomny etap odtwarzania kształtu fali. Modulacja kodowo-impulsowa Komputery operujš na liczbach, aby więc zmusić je do odtwarzarua dŸwięków, niezbędne jest opracowanie mechanizmu konwersji dŸwięku na liczby i odwrot- nie, z liczb na dŸwięk. Najpowszechniejszym typem takiej konwersji bez kom- presji jest PCM (ang. pulse code moduiation), modulacja kodowo-impulsowa. PCM wykorzystuje się w odtwarzaczach płyt kompaktowych, magnetofonach cyfro- wych i w Windows. Jest to wyszukany termin oznaczajšcy łatwy do zrozumienia proces. Modulacja kodowo-impulsowa oznacza próbkowanie sygnału dŸwiękowego ze stałš częstotliwoœciš, wynoszšcš na ogół dziesięciotysięczne częœci sekundy. Mie- rzy się amplitudę każdej próbki. Urzšdzenia zajmujšce się takš konwersjš am- plitud próbek na wartoœci liczbowe nazywa się konwerterem analogowo-cyfro- wym (ang. analog-to-digitai converter ADC). Cišg wartoœci liczbowych można prze- kształcić z powrotem na sygnał cyfrowy za pomocš konwertera cyfrowo-analo- gowego (ang. digital-to-analog converter DAC). Wynik takiej konwersji nie jest do- kładnie tym samym, co zostało poddane obróbce poczštkowo. Wynikowe sygna- ły majš poszarpane, kanciaste kształty, którym odpowiadajš dodatkowe wyso- kie harmoniczne. Z tego powodu w urzšdzeniach odtwarzajšcych za konwerte- rem cyfrowo-analogowym umieszcza się z reguły filtry dolnoprzepustowe. Filtr taki eliminuje wyższe częstotliwoœci i wygładza sygnał wyjœciowy. Po stronie wejœcia filtr dolnoprzepustowy stosuje się przed konwerterem analogowo-cyfro- wym. Modulacja kodowo-impulsowa ma dwa parametry: częstotliwoœć próbkowania (liczba pomiarów amplitudy na jednš sekundę) i wielkoœć próbki (liczba bitów ' p rzeznaczonych na zapisanie wartoœci amplitudy jednej próliki). Jak można się spodziewać, im szybsze próbkowanie i im większy rozmiar próbki (amplituda jest zapisywana przy użyciu większej liczby bitów), tym lepiej odtwarzany jest oryginalny dŸwięk. Jest jednak punkt, w którym poprawianie częstotliwoœci prób- kowania i wielkoœci próbki staje się bezcelowe, ponieważ wykracza poza ludzkš zdolnoœć percepcji. Z drugiej strony zastosowanie zbyt małej częstotliwoœci prób- kowania lub zbyt małego rozmiaru próbek skutkuje zbyt niskš jakoœciš dŸwięku. 1146 Częœć III: Zagadnienia zaawansowane Częstotliwoţć próbkowania Częstotliwoœć próbkowania okreœla maksymalnš częstotliwoœć, z jakš można di- gityzować i zapisywać dŸwięk. W szczególnoœci częstotliwoœć próbkowania po- winna być przynajmruej dwa razy większa niż częstotliwoœć najwyższego prób- kowanego tonu. Jest to tak zwana nierównoœć Nyquista (od nazwiska Harry'ego Nyquista, fizyka, który prowadził badania nad próbkowaniem w latach trzydzie- stych dwudziestego wieku). Jeżeli falę sinusoidalnš będziemy próbkować ze zbyt niskš częstotliwoœciš, wy- nikowy sygnał (po przeksztţłceniu do postaci cyfrowej i z powrotem do analo- gowej), będzie miał niższš częstotliwoœć niż oryginalny. Jest to tzw. aliasing. Aby uniknšć aliasingu, na wejœciu stosuje się filtry dolnoprzepustowe, blokujšce wszystkie częstotliwoœci większe niż pół częstotliwoœci próbkowania. Kanciaste krawędzie fali, utworzone po stronie wyjœciowej na skutek konwersji cyfrowo- analogowej, sš w rzeczywistoœci wyższymi harmonicznymi o częstotliwoœciach większych niż połowa częstotliwoœci próbkowania. Dlatego również na wyjœciu stosuje się filtr dolnoprzepustowy. Blokuje on wszystkie częstotliwoœci większe niż pół częstotliwoœci próbkowania. Częstotliwoœć próbkowania używana w płytach CD audio wynosi 44,1 kHz, co oznacza 44 100 próbek na sekundę. Skšd bierze się taka dziwna liczba? Ludzkie ucho słyszy częstotliwoœci do 20 kHz, aby więc uchwycić całe pasmo słyszalne, należy stosować częstotliwoœć próbkowania równš 40 kHz. Ponieważ jednak filtry dolnoprzepustowe dajš spadek wzmocnienia, częstotliwoœć prób- kowania powinna być o około 10 procent wyższa. Otrzymujemy więc 44 kHz. Na wypadek, gdybyœmy chcieli rejestrować cyfrowo dŸwięk razem z obrazem wideo, częstotliwoœć próbkowania powinna być całkowitš wielokrotnoœciš ame- rykańskich i europejskich częstotliwoœci ramek, odpowiednio 30 Hz i 25 Hz. W ten sposób dochodzimy do częstotliwoœci 44,1 kHz. Częstotliwoœć próbkowarua dysków kompaktowych (44,1 kHz) daje w efekcie mnóstwo danych, które sš nadmiarowe dla niektórych zastosowań, na przykład dla rejestrowarua głosu ludzkiego, a nie muzyki hi-fi. 7.mniejszenie częstotliwo- œci próbkowania o połowę, czyli do 22,05 kHz, redukuje górny zakres reprodu- kowalnych dŸwięków o jednš oktawę, czyli do częstotliwoœci 10 kHz. Zmniej- szenie jej czterokrotne, do 11,025 kHz, pozwala na wierne odtwarzarue częstotli- woœci z zakresu do 5 kHz. Częstotliwoœci próbkowania 44,1 kHz, 22,05 kHz i 11,025 kHz, a także 8 kHz sš standardami często używanymi w urzšdzeniach (progra- mowych) audio. Można uznać, że częstotliwoœc próbkowarua równa 11,025 kHz nadaje się do re- jestrowania pianina, ponieważ jego najwyższa częstotliwoœć to 41á6 Hz. Często- tliwoœć 4186 Hz to jednak najwyższa częstotliwoœć podstawowa, jakš może wy- dawać pianino. Wyeliminowanie wszystkich sygnałów sinusoidalnych powyżej 5000 Hz oznacza usuruęcie wyższych harmonicznych w odtworzonym dŸwięku, a więc niewłaœciwš reprodukcję barwy. Rozdział 22: DŸwięk i muryka 1147 Rozmiar próbki Drugim parametrem modulacji kodowo-impulsowej jest mierzony liczbš bitów rozmiar próbki. Pozwala on zróżnicować próbki od najcichszych do najgłoœniej- szych. Zakres próbek jest nazywany zakresem dynamiki. Natężenie dŸwięku jest kwadratem amplitudy sygnału (czyli wartoœciš złożonš z maksymalnych amplitud osišganych przez poszczególne fale sinusoidalne w trakcie trwania jednego cyklu). Percepcja natężenia dŸwięku przez ludzkie ucho jest logarytmiczna, podobnie jak percepcja częstotliwoœci. Różnicę między natężeniami dwóch dŸwięków mierzy się w belach [B] (od na- zwiska Aleksandra Grahama Bella, wynalazcy telefonu) i decybelach [dB]. Jeden bel oznacza dziesięciokrotny wzrost natężenia dŸwięku. Jeden decybel jest jednš dziesištš bela, liczonš w równych krokach multiplikatywnych. Tak więc jeden decybel oznacza, że natężenie dŸwięku wzrosło 1,26 (pierwiastek dziesištego stop- nia z 10) razy albo amplituda sygnału wzrosła 1,12 (pierwiastek dwudziestego stopnia z 10) razy. Decybel jest najmniejszym wzrostem natężenia dŸwięku, jaki może wychwycić ludzkie ucho. Różnica natężeń dŸwięków słyszalnych i tych na granicy bólu wynosi około 100 dB. Zakres dynamiki (w decybelach) między dwoma dŸwiękami można wyliczyć następujšcym wzorem: dB = 20 ů log (At/Az) gdzie Al i Az to amplitudy obu dŸwięków. W prostym przypadku, kiedy próbka ma rozmiar 1 bita, zakres dynamiki jest zerowy, ponieważ możliwa jest tylko jedna amplituda. W przypadku próbki o rozmiarze 8 bitów możliwych jest 256 różnych amplitud. Zakres dynamiki wynosi więc: dB = 20 ů log (256) czyli 48 decybeli. Zakres dynamiki 48 dB odpowiada różnicy natężenia od ciche- go pomieszczenia do hałaœliwej kosiarki do trawy. Podwojenie rozmiaru próbki do 16 bitów daje zakres dynamiki: dB = 20 ů log (65536) czyli 96 decybeli, a więc bardzo blisko różnicy między dŸwiękiem słyszalnym i granicš bólu. Dlatego zakres taki jest uznawany za idealny do re rodukcji mu- zyki. P Zarówno 8-bitowe, jaki i 16-bitowe próbki sš obshzgiwane w Windows. Oœmiobi- towe próbki sš zapisywane jako liczby jednobajtowe bez znaku. Cisza może być zapisywana jako cišg liczb 0x80. Szesnastobitowe próbki sš zapisywane jako liczby całkowite ze znakiem, dlatego cisza może być zapisywana jako cišg zer. Aby wyliczyć miejsce wymagane do zapisania nieskompresowanego dŸwięku, należy pomnożyć czas jego trwania (w sekundach) przez częstotliwoœć próbko- wania. Jeżeli próbka ma 8 bitów, otrzymana wartoœć wyraża liczbę bajtów, jeżeli 16 bitów - wynik należy pomnożyć przez dwa. Wynik podwaja się jeszcze raz, jeżeli nagrywany jest dŸwięk stereofoniczny. Jedna godzina (3600 sekund) dŸwięku o jakoœci CD (44 100 stereoforucznych 16-bitowych próbek) wymaga 635 mega- 1148 Częœć III: Zagadnienia zaawansowane bajtów miejsca na dysku, bardzo blisko pojemnoœci płyty CD (na płycie mieszczš się 74 minuty muzyki - przyp. thxm. ). Programowe generowanie sygnałów sinusoidalnych Nasze pierwsze ćwiczenie z dŸwiękami wave będzie polegało na zapisywaniu ich w plikach i odtwarzaniu zapisanych dŸwięków. Za pomocš niskopoziomo- wych funkcji waveform audio API (czyli tych z prefiksem'wave0ut) napiszemy generator dŸwięków sinusoidalnych o nazwie SINEWAVE. Program ten będzie wytwarzał sygnały sinusoidalne o częstotliwoœciach od 20 Hz (dolna granica per- cepcji przez ludzkie ucho) do 5000 Hz (dwie oktawy przed osišgnięciem górnej granicy) w odległoœciach 1 Hz. Jak wiadomo, standardowa biblioteka C czasu wykonywania zawiera funkcję o nazwie sin, która zwraca sinus kšta okreœlonego w radianach (2n radianów rów- na się 360 stopni). Funkcja sin zwraca wartoœć z przedziału od -1 do 1 (została ona już wykorzystana w innym programie, SINEWAVE z rozdziahz 5). Tak więc łatwo będzie za jej pomocš wygenerować dane fali sinusoidalnej, aby je przesłać do wyjœciowego urzšdzenia waveform audio. Należy po prostu wypełnić jakiœ bufor danymi reprezentujšcymi kształt fali (w tym wypadku sinusoidalnej) i prze- kazać go do API (jest to nieco bardziej skomplikowane, ale szczegóły omówię póŸniej). Kiedy urzšdzenie waveform audio zakończy odtwarzanie zawartoœci bufora, należy przekazać mu drugi bufor itd. Rozważajšc ten problem pierwszy raz (rue wiedzšc nic o PCM), można stwier- dzić, że rozsšdnie byłoby podzielić jeden okres sinusa na ustalonš liczbę próbek, na przykład na 360. W przypadku sinusa 20 Hz otrzymuje się 7200 próbki na sekundę, w przypadku sinusa 200 Hz - dziesięć razy więcej, czyli 72 000 próbki na sekundę. Nie jest to jednak właœciwy sposób postępowania. Dla sinusa 5000 Hz potrzebnych będzie 1 800 000 próbek w cišgu każdej sekundy, co z pewno- œciš przytkałoby konwerter cyfrowo-analogowy! Ponadto przy wyższych często- tliwoœciach stosuje się o wiele większš precyzję, niż jest to konieczne. Przy modulacji kodowo-impulsowej liczba próbek na sekundę jest stała. Załóż- my, że częstotliwoœć próbek wynosi 11 025 Hz, ponieważ takiej użyję w progra- mie SINEWAVE. Jeżeli chcesz generować sinusy o częstotliwoœci 2 756,25 Hz (do- kładnie jedna czwarta częstotliwoœci próbek), każdy okres fali będzie złożony z tylko 4 próbek. Natomiast każdy okres fali sinusoidalnej o częstotliwoœci 25 Hz będzie wyxnagał 441 próbek. Ogólnie liczba próbek przypadajšcych na jeden okres to ich częstotliwoœć podzielona przez żšdanš częstotliwoœć sygnahx. Znajšc licz- bę próbek na jeden okres fali, można podzielić 2 n radianów przez tę liczbę i wy- nik zastosować jako argument funkcji sin, aby otrzymać poszczególne wartoœci próbek. Następnie wystarczy powtarzać ten okres w pętli, aby uzyskać cišgły prze- bieg sinusoidalny. Pojawia się jednak pewien problem: liczba próbek jednego okresu fali nie może być ułamkowa, więc to rozwišzanie również odpada. Na końcu każdego okresu powstaje zaburzenie cišgłoœci. Kluczem do uzyskarua prawidłowego rozwišzania jest utrzymywanie statycznej zmiennej kšta fazowego. Kšt ten inicjuje się wartoœciš 0. Pierwsza próbka ma Rozdział 22: DŸwięk i muryka 1149 wartoœć sin(0). Następnie kšt zwiększa się o 2n razy żšdana częstotliwoœć i dzie- li przez częstotliwoœć próbek. Sinus tak otrzymanego kšta tworzy wartoœć dru- giej próbki. Jeżeli kšt fazowy przekroczy 2n radianów, należy od niego odjšć 2n, nie należy go jednak ponownie zerować. Załóżmy, że chcesz wygenerować sygnał sinusoidalny o częstotliwoœci 1000 Hz przy częstotliwoœci próbek 11 025 Hz. Daje to około 11 próbek na jeden okres. Kšty fazowe (podaję je w stopniach, aby poprawić czytelnoœć przykładu) będš wynosiły mniej więcej: 0; 32,65; 65,31; 97,96; 130,61; 163,27; 195,92; 228,57; 261,22; 293,88; 326,53; 359,18; 31,84; 64,49; 97,14; 129,80; 162,45; 195,10 itd. Umieszczane w buforze dane tworzšce kształt fali sš sinusami powyższych kštów wyskalo- wanymi do liczby bitów przypadajšcych na próbkę. Tworzšc dane kolejnego bufora, należy nadal zwiększać ostatni kšt fazowy, nie zerujšc go. Odbywa się to w funkcji FilIBuffer. Cały program SINEWAVE pokazano na ry- sunku 22-2. SINEWAVE.C /* SINEWAVE.C - Generator sygnalów sinusoidalnych (c) Charles Petzold, 1998 */ llinclude llinclude lţinclude "resource.h" ildefine SAMPLEţRATE 11025 define FRE„ MIN 20 ildefine FREO_MAX 5000 lldefine FREOţINIT 440 lţdefine OUT BUFFERţSIZE 4096 ţţdefine PI 3.14159 BOOL CALLBACK DlgProc (HWND, UINT, WPARAM, LPARAM) ; TCHAR szAppName C] = TEXT ("SineWave") ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) if (-1 --- DialogBox (hInstance, szAppName, NULL, DlgProc)) ( MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MBţICONERROR) ; ) return 0 ; VOID FillBuffer (PBYTE pBuffer, int iFreq) { static double fAngle ; int i ; 1150 Częœć III, Zagadnienia zaawansowane (cigg dalszy ze strony 1149) for (i = 0 ; i < OUT BUFFER SIZE ; i++) - - . pBuffer [i] = (BYTE) (127 + 127 * sin (fAngle)) ; fAngle += 2 * pI * iFreq / SAMPLE-RATE ; if (fAngle > 2 * PI) r fAngle -= 2 * PI ; } BOOL CALLBACK Dlgproc (HWND hwnd, UINT message, WPARAM wparam, LPARAM lParam) ( static BOOL bShutOff, bClosing ; static HWAVEOUT hWaveOut ; static HWND hwndScroll ; static int iFreq = FREOţINIT ; ů static PBYTE pBufferl, pBuffer2 ; static PWAVEHDR pWaveHdrl, pWaveHdr2 ; static WAVEFORMATEX waveformat ; int iDummy ; switch (message) ( case WM_INITDIALOG: hwndScroll = GetDlgItem (hwnd, IDC_SCROLL) ; SetScrollRange (hwndScroll, SB_CTL, FREO_MIN, FREO_MAX, FALSE) ; SetScrollPos (hwndScroll, SB_CTL, FREO_INIT, TRUE) ; SetDlgItemInt (hwnd, IDC TEXT, FREOţINIT, FALSE) ; return TRUE ; case WM HSCROLL: switch (LOWORD (wparam)) . ( case SB_LINELEFT: iFreq -- 1 ; break ; case SB_LINERIGHT: iFreq += 1 ; break ; case SB_PAGELEFT: iFreq /= 2 ; break ; case SB-PAGERIGHT: iFreq *= 2 ; break ; case SB_THUMBTRACK: iFreq = HIWORD (wparam) ; break ; case SB TOP: GetScrollRange (hwndScroll, SB CTL, &iFreq, &iDummy) ; break ; case SB_BOTTOM: GetScrollRange (hwndScroll, SB CTL, &iDummy, &iFreq) ; break ; . iFreq = max (FREO MIN, min (FREO MAX, iFreq)) ; Rozdział 22: Diwięk i muryka 1151 , SetScrollPos (hwndScroll, SB_CTL, iFreq, TRUE) ; SetDlgItemInt (hwnd, IDC TEXT, iFreq, FALSE) ; return TRUE ; case WM COMMAND: switch (LOWORD (wParam)) case IDC_ONOFF: // przy wlšczaniu hWaveOut = NULL if (hWaveOut = NULL) // rezerwacja pamięci na 2 naglówki i 2 bufory pWaveHdrl = malloc (sizeof (WAVEHDR)) ; pWaveNdr2 = malloc (sizeof (WAVEHDR)) ; pBufferl = malloc (OUT_BUFFER_SIZE) ; pBuffer2 = malloc (OUT BUFFER SIZE) ; if (!pWaveHdrl ţţ !pWaveHdr2 ţţ !pBufferl ţţ !pBuffer2) if (!pWaveHdrl> free (pWaveHdrl> ; if (!pWaveHdr2) free (pWaveHdr2) ; if (!pBufferl) free (pBufferl) ; if (!pBuffer2) free (pBuffer2) ; MessageBeep (MB_ICONEXCLAMATION) ; MessageBox (hwnd, TEXT ("Error allocating memory!"), szAppName, MB ICONEXCLAMATION ţ MB OK) ; return TRUE ; // zmienna wskazujšca naciœnięcie przycisku Off , bShutOff = FALSE ; // otwarcie urzddzenia wyjœciowego waveform audio waveformat.wFormatTag = WAVEţFORMATţPCM ; waveformat.nChannels = 1 ; waveformat.nSamplesPerSec = SAMPLE_RATE ; waveformat.nAvgBytesPerSec = SAMPLE RATE ; waveformat,nBlockAlign = 1 ; waveformat.wBitsPerSample = 8 ; waveformat.cbSize = 0 ; if (waveOutOpen (&hWaveOut, WAVE_MAPPER, &waveformat, (DWORD) hwnd, 0, CALLBACK WINDOW) != MMSYSERR NOERROR) { ,. free (pWaveHdrl) ; „ . free (pWaveHdr2) ; free (pBufferl) ; free (pBuffer2) ; hWaveOut = NULL ; MessageBeep (MB_ICONEXCLAMATION) ; MessageBox (hwnd, 1152 Częœć III: Zagadnienia zaawansowane (cigg dalszy ze strony 1151) TEXT ("Error opening waveform audio device!"), szAppName, MBţICONEXCLAMATION ţ MB OK) ; return TRUE ; ) // szykowanie nagłówków pWaveHdrl->lp0ata = pBufferl ; pWaveHdrl->dwBufferLength = OUTţBUFFER SIZE ; pWaveHdrl->dwBytesRecorded = 0 ; pWaveHdri->dwUser = 0 ; pWaveHdrl->dwFlags = 0 ; pWaveHdrl->dwLoops = 1 ; pWaveHdrl->lpNext = NULL ; pWaveHdrl->reserved = 0 ; waveOutPrepareHeader (hWaveOut, pWaveHdrl, sizeof (WAVEHDR)) ; pWaveHdr2->lpData = pBuffer2 ; pWaveNdr2->dwBufferLength = OUT BUFFER SIZE ; pWaveHdr2->dwBytesRecorded = 0 ; pWaveHdr2->dwUser = 0 ; pWaveHdr2->dwFlags = 0 ; pWaveHdr2->dwloops = 1 ; pWaveHdr2->lpNext = NULL ; pWaveHdr2->reserved = 0 ; waveOutPrepareHeader (hWaveOut, pWaveHdr2, sizeof (WAVEHDR)) ; ) // przy wylšczaniu trzeba zresetować waveform audio ' else ( bShutOff = TRUE ; waveOutReset (hWaveOut) ; } return TRUE ; ) break ; ' // komunikat wygenerowany przez waveOutOpen case MM WOM_OPEN: SetDlgItemText (hwnd, IDCţONOFF, TEXT ("Turn Off")) ; // wyslanie dwóch buforów do urzţdzenia wyjœciowego waveform audio FillBuffer (pBufferl, iFreq) ; waveOutWrite (hWaveOut, pWaveHdrl, sizeof (WAVEHDR)) ; , FillBuffer (pBuffer2, iFreq) ; waveOutWrite (hWaveOut, pWaveHdr2, sizeof (WAVEHDR)) ; return TRUE ; // komunikat wygenerowany po wyczerpaniu się bufora Rozdział 22: DŸwigk i muryka 1153 case MM_WOM_DONE: if (bShutOff) , waveOutClose (hWaveOut) ; return TRUE ; // wypelnienie i wyslanie nowego bufora FillBuffer (((PWAVEHDR) lParam)->lpData, iFreq) ; waveOutWrite (hWaveOut. (PWAVEHDR) lParam, sizeof (WAVEHDR)) ; return TRUE ; case MM WOM_CLOSE: , waveOutUnprepareHeader (hWaveOut, pWaveHdrl, sizeof (WAVEHDR)) ; waveOutUnprepareHeader (hWaveOut, pWaveHdr2, sizeof (WAVEHDR)) ; free (pWaveHdrl) ; free (pWaveHdr2) ; free (pBufferl) ; free (pBuffer2) ; hWaveOut = NULL ; SetDl9ItemText (hwnd, IDC ONOFF, TEXT ("Turn On")) ; if (bClosing) EndDialog (hwnd, 0) ; return TRUE ; ; case WM SYSCOMMAND: switch (wParam) case SC_CLOSE: if (hWaveOut != NULL) bShutOff = TRUE ; bClosing = TRUE ; ' waveOutReset (hWaveOut) ; 1 else EndDialog (hwnd, 0) ; return TRUE ; ) break ; , ) return FALSE ; ) SINEWAVE.RC (fragmenty) //Microsoft'Developer Studio generated resource script. ţţinclude "ţesource.h" 1154 Częœć 111ţ Zagadnienia zaawansowane (cigg dalszy ze strony 1153) 4linclude "afxres.h" ///////////////////////////////////////////////////////////////////////////// // Dialog SINEWAVE DIALOG DISCARDABLE 100, 100, 200, 50 STYLE WS MINIMIZ^BOX ţ WS_VISIBLE ţ WS CAPTION ţ WS SYSMENU CAPTION "Sine Wave Generator" FONT 8, "MS Sans Serif" BEGIN SCROLLBAR IDC_SCROLL,8,8,150,12 RTEXT "440",IDC_TEXT,l60,10,20,8 LTEXT "Hz",IDC_STATIC,l82,10,12,8 PUSHBUTTON "Turn On",IDC ONOFF,80,28,40,14 END RESOURCE.H (fragmenty) // Microsoft Developer Studio generated include file. // Used by SineWave.rc ţfdefine IDC_STATIC -1 ţţdefine IDC_SCROLL 1000 ţţdefine IDC_TEXT 1001 Iţdefine IDC ONOFF 1002 Rysunek 22-2. Program SINEWAVE Na poczštku programu zdefiniowane sš identyfikatory OUT_BUFFER_SIZE , SAMPLĘ RATE i PI, używane w procedurze FiIlBuffer. Argument iFreq funkcji FilIBuffer oznacza żšdanš częstotliwoœć w hercach. Wynik funkcji sin jest skalo- , wany do wartoœci z przedziału od 0 do 254. Dla każdej próbki argument fAngle funkcji sin jest zwiększany o 2n radianów razy żšdana częstotliwoœć, podzielone przez częstotliwoœć próbek. Okno programu SINEWAVE zawiera trzy kontrolki: poziomy pasek przewijania służšcy do ustalania częstotliwoœci, statyczne pole tekstowe informujšce o wy- branej obecnie częstotliwoœci i przycisk z napisem "Turn On". Po kliknięciu przy- cisku powinieneœ usłyszeć sygnał sinusoidalny (o ile masz działajšcš kartę dŸwię- kowš z prawidłowo podłšczonymi głoœnikami). Napis na przycisku zmieni się na "Turn Off . Częstotliwoœć sygnału można zmieniać za pomocš paska przewi- jania (myszš lub klawiszami). Chcšc wyłšczyć odtwarzanie sygnału, należy po- nownie kliknšć przycisk. Podczas obsługi komunikatu WMţINTTDIALOG pasek przewijara jest ustawia- ny na cżęstotliwoœć minimalnš 20 Hz i maksymalnš 5000 Hz. Poczštkowo jego wartoœć wynosi 440 Hz. W języku muzyki jest to A powyżej œrodkowego C, nuta używana przez orkiestry do strojenia instrumentów. W procedurze DIgProc zmien- na statyczna iFreq zmieniana jest po otrzymaniu komunikatu 4VMţHSCROLL. Zauważ, że klawisze [PgDn] i [PgUp] powodujš odpowiednio zmniejszenie i zwiększenie częstotliwoœci o jednš oktawę. T Rozdział 22: DŸwięk i muryka 1155 Kiedy procedura DlgProc otrzymuje komunikat WM COMMAND od przycisku, najpierw rezerwuje 4 bloki pamięci: 2 na struktury WAVEHDR, które zaraz opi- szę, i 2 na bufory (pBufferl i pBuffer2), służšce do przechowywarua danych defi- niujšcych kształt fali. Program SINEWAVE otwiera urzšdzenie waveform audio dla zapisu, wywołu- jšc funkcję waveOutOpen, która ma następujšcš postać: waveOutOpen (&hWaveOut. w0eviceID. &waveformat. dwCallBack. dwCallBackOata. dwFlags) ; Pierwszy argument ma wskazywać zmiennš typu HWAVEOUT (uchwyt wyjœcio- wego urzšdzenia waveform audio). Po powrocie z funkcji zmienna ta będzie miała wartoœć uchwytu, który zostaniewykorzystany w kolejnych odwołaniach do urzš- dzenia. Drugi argument waveOutOpen to identyfikator urzšdzenia. Umożliwia on uży- wanie funkcji w komputerach z wieloma zainstalowanymi kartami dŸwiękowy- mi. Argument ten może mieć wartoœć z zakresu od zera do o jeden mniej niż licz- ba urzšdzeń wyjœciowych typu waveform audio. Liczbę urzšdzeń można uzy- skać za pomocš funkcji waveOutGetNumDevs, a informacje o każdym z nich - za pomocš funkcji waveOutGetDevCaps. Chcšc uniknšć kłopotów zwišzanych z wie- loœciš urzšdzeń, można użyć zmiennej WAVĘ MAPPER (zdefiniowanej jako -1). Spowoduje to wybranie urzšdzenia wskazanego przez użytkownika jako prefe- rowanego na karcie Audio apletu Multimedia w Panelu sterowania. Ewentual- nie system może wybrać inne urzšdzenie, jeżeli to wskazane jako preferowane nie może obsłużyć żšdanej operacji, a inne może. Trzecim argumentem jest wskaŸnik do struktury WAVEFORMATEX (więcej in- formacji na ten temat za chwilę). Czwartym argumentem jest albo uchwyt okna, albo wskaŸnik do funkcji zwrotnej z biblioteki dynamicznej. Argument ten wska- zuje okno albo funkcję zwrotnš odbierajšcš komunikaty wyjœciowe urzšdzenia waveform audio. Jeżeli użyje się funkcji zwrotnej, w pištym argumencie można podać jakieœ dane zdefiniowane w programie. Jako argument dwFlags można podać CALLBACK WINDOW lub CALLBACK_FUNCTION, stałe definiujšce znaczenie czwartego argumentu. Podajšc stałš WAVĘ FORMATţQUERY, spraw- dza się, czy urzšdzenie może być otwarte bez otwierania. Dostępnych jest jesz- cze kilka innych stałych. Trzecim argumentem waveOutOpen jest wskaŸnik do struktury typu WAVEFOR- MAT'EX, zdefiniowanej w pliku MMSYSTEM.H w następujšcy sposób: typedef struct waveformat tag ( WORD wFormatTag ; // format wave = WAVEţFORMATţPCM WORD nChannels ; // liczba kanalów = 1 lub 2 DWORD nSamplesPerSec ; // częstotliwoœć próbek DWORD nAvgBytesPerSec ; // liczba bajtów na sekundę WORD nBlockAlign ţ // wyrównanie bloku WORD wBitsPerSample ; // liczba bitów na próbkę = 8 lub 16 WORD cbSize ; // 0 dla PCM WAVEFORMATEX. * PWAVEFORMATEX; 1156 Częœć IIIţ Zagadnienia zaawansowane W strukturze tej definiuje się częstotliwoœć próbek (nSamplesPerSec), rozmiar próbki (wBitsPerSample) oraz liczbę kanałów (nChunnels), 1 dla mono i 2 dla stereo. Nie- które dane mogš wydawać się nadmiarowe, ale struktura jest przeznaczona rów- nież dla metod próbkowania innych niż PCM - wtedy ostatnie pole przyjmuje wartoœć niezerowš. W przypadku PCM polu nBlockAlign należy nadać wartoœć iloczynu nChannels i wBitsPerSample podzielonego przez 8. Jest to całkowita liczba bajtów przypada- jšcych na jednš próbkę. Polu nAvgBytesPerSec trzeba nadać wartoœć iloczynu pól nSamplesPerSec i nBlockAlign. Program SINEWAVE inicjuje pola struktury WAVEFORMATEX i wywołuje wa- veOutOpen: waveOutOpen (&hWaveOut, WAVE_MAPPER, &waveformat, (DWORD) hwnd, 0, CALLBACK WINDOW) Funkcja waveOutOpen zwraca wartoœć MMSYSERiZţNOERROR (zdefiniowanš jako 0), jeżeli zakończy się sukcesem, i wartoœć niezerowego kodu błędu w prze- ciwnym razie. Jeżeli waveOutOpen zwróci wartoœć niezerowš, SINEWAVE wy- czyœci i wyœwietli komunikat informujšcy o błędzie. Po otwarciu urzšdzenia inicjowane sš pola dwóch struktur WAVEHDR, które słu- żš do przekazywania buforów poprzez API. WAVEHDR ma następujšcš defini- typedef struct wavehdr tag ( LPSTR lpData; // wskaŸnik do bufora z danymi DWORD dwBufferLength; // dlugoœć bufora DWORD dwBytesRecorded; // używane do rejestrowania DWORD dwUser; // do użytku przez program DWORD dwFlags; // flagi DWORD dwLoops; // liczba powtórzeń struct wavehdr_tag FAR *lpNext; // zastrzeżone DWORD reserved; // zastrzeżone ) WAVEHDR, *PWAVEHDR; SINEWAVE przypisuje polu lpData adres bufora, który będzie zawierał dane, polu dwBufferLength rozmiar tego bufora, a polu dwLoops wartoœć 1. Pozostałe pola mogš mieć wartoœci 0 lub NULL. Aby odtwarzać dŸwięk w pętli, należy zdefiniować pola dwFlags i dwLoops. Następnie wywoływana jest funkcja waveOutPrepareHeader dla dwóch nagłów- ków. Wywołanie to zapobiega złożeniu zawartoœci struktury i bufora na dysku w ramach operacji wymiany (swap). Wszystkie przygotowania były do tej pory realizowane w odpowiedzi na klika- nie przycisku włšczajšcego dŸwięk, ale w kolejce komunikatów programu czeka jakiœ komunikat. Ponieważ w waveOutOpen okreœliliœmy, że chcemy, aby proce- dura okna odbierała komunikaty wyjœciowe urzšdzenia waveform audio, funk- cja waveOutOpen wysłała do kolejki komunikatów programu komunikat MM WOM OPEN. Parametrem komunikatu wParam jest uchwyt wyjœciowego urzšdzenia waveform audio. Aby obshxżyć komunikat MM_WOM OPEN, pro- gram SINEWAVE wywołuje dwa razy FillBuffer w celu zapełnienia bufora pBuf Rozdział 22: DŸwięk i muryka 1157 fer danymi definiujšcymi kształt fali. SINEWAVE przekazuje następnie do wave- OutWrite dwie struktury WAVEHDR. Jest to funkcja, która de facto zaczyna od- twarzanie dŸwięku, przekazujšc dane do urzšdzenia audio. Kiedy urzšdzenie zakończy odtwarzanie danych przekazanych mu przez funk- cję waveOutWrite, okno otrzymuje komunikat MM WOM DONE. Parametr wPa- ram jest uchwytem urzšdzenia waveform audio, a IParam wskaŸnikiem do struk- tury WAVEHDR. SINEWAVE przetwarza ten komunikat, wyliczajšc nowy bufor i podsyłajšc go za pomocš funkcji waveOutWrite. Można było użyć tylko jednej struktury WAVEHDR i tylko jednego bufora. Mię- dzy chwilš, w której urzšdzenie kończyłoby odtwarzanie danych, a chwilš po zakończeniu przetwarzania komunikatu MM WOM DONE, skutkujšcego wy- słaniem nowego komunikatu, występowałaby jednak minimalna przerwa. Tech- nika podwójnego bufora zapobiega powstawaniu przerw dŸwięku. Kiedy użytkownik klika przycisk z napisem "Turn Off", DlgProc otrzymuje kolej- ny komunikat WM COMMAND. Podczas jego obsługi DlgProc nadaje zmiennej bShutOff wartoœć TRUE i wywohzje waveOutReset. Funkcja waveOutReset zatrzy- muje odtwarzanie dŸwięku i generuje komunikat MM WOM DONE. Jeœli bShu- tOff ma wartoœć TRUE, program przetwarza komunikat MM WOM DONE, wywołujšc waveOutClose. To z kolei powoduje wygenerowanie komunikatu MM WOM CLOSE. Przetwarzanie MM WOM CLOSE polega najczęœciej na sprzštaniu. SINEWAVE wywohxje waveOutUnprepareHeader dla dwóch struktur WAVEHDR, zwalnia wszystkie bloki pamięci i zmienia napis przycisku z powro- tem na "Turn On". Jeżeli urzšdzenie waveform audio nadal odtwarza bufor, samo wywołanie funk- cji waveOutClose nie odniesie żadnego efektu. Trzeba najpierw wywołać waveOutRe- set, aby zatrzymać odtwarzanie i wygenerować komunikat MMţWOM DONE. Kiedy wParam ma wartoœć SC CLOSE, w procedurze DIgProc przetwarzany jest również komunikat WMţSYSCOMMAND. Zostało to spowodowane wybraniem przez użytkownika polecenia Zamknij z menu systemowego. Jeżeli urzšdzenie waveform audio nadal odtwarza dŸwięk, DIgProc wywołuje waveOutReset. Nie- zależrue od tego, ostatecznie wywoływana jest funkcja EndDialog, zamykajšca okno dialogowe i kończšca program. Cyfrowy odtwarzacz dŸwięku W Windows znajduje się program o nazwie Rejestrator dŸwięku. Służy on do cyfrowego rejestrowania i odtwarzania dŸwięków. Program pokazany na rysun- ku 22-3 (RECORD1) jest mniej wyrafinowany, ponieważ nie pozwala na żadne operacje wejœcia/wyjœcia ani edycji. Nie to jest jednak ważne. Program ten wy- œmienicie nadaje się do zaprezentowania niskopoziomowych funkcji API do ob- sługi zapisu i odtwarzania dŸwięku za pomocš urzšdzenia waveform audio. 1158 Częœć III: Zagadnienia zaawansowane RECORDl.C /* RECORDl.C - Cyfrowy rejestrator dŸwięku (c) Charles Petzold, 1998 */ ţţinclude I ţţinclude "resource.h" ţţdefine INP BUFFER SIZE 16384 BOOL CALLBACK DlgProc (HWND, UINT, WPARAM, LPARAM> ; TCHAR szAppName [] = TEXT ("Recordl") ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) l if (-1 = DialogBox (hInstance, TEXT ("Record"), NULL, OlgProc)) ( MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MB ICONERROR) ; ) ) return 0 ; void ReverseMemory (BYTE * pBuffer, int iLength) ( BYTE b ; int i ; for (i = 0 ; i < iLength / 2 ; i++) b = pBuffer [i] ; pBuffer [i] = pBuffer [iLength - i - 1] ; pBuffer [iLength - i - 1] = b ; ) }. BOOL CALLBACK DlgProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ( static BOOL bRecording, bPlaying, bReverse, bPaused, bEnding, bTerminating ; static DWORD dwDataLength, dwRepetitions = 1 ; static HWAVEIN hWaveIn ; static HWAVEOUT hWaveOut ; static PBYTE pBufferl, pBuffer2, pSaveBuffer, pNewBuffer ; static PWAVEHDR pWaveHdrl, pWaveHdr2 ; static TCHAR szOpenError[] = TEXT ("Error opening waveform audio!"); static TCHAR szMemError [] = TEXT ("Error allocating memory!") ; static WAVEFORMATEX waveform ; switch (message) f case WM INITDIALOG: T Rozdziat 22: Dtwigk i muryka 1159 // rezerwacja pamięci na nagłówek wave pWaveHdrl = malloc (sizeof (WAVEHDR)) ; pWaveHdr2 = malloc (sizeof (WAUEHDR)) ; // rezerwacja pamięci na bufor danych pSaveBuffer = malloc (1) ; ' return TRUE ; case WM COMMAND: switch (LOWORD (wParam)) case IDC RECORD BEG: ' // rezerwacja pamięci na bufor pBufferl = malloc (INP_BUFFER_SIZE) ; pBuffer2 = malloc (INP BUFFER SIZE) ; if (!pBufferl ţţ !pBuffer2) if (pBufferl) free (pBufferl) ; if (pBuffer2) free (pBuffer2) ; MessageBeep (MB_ICONEXCLAMATION) ; ' MessageBox (hwnd, szMemError, szAppName, MBţICONEXCLAMATION ţ MB OK) ; return TRUE ; 1 // otwarcie urzddzenia wejœciowego waveform audio waveform.wFormatTag = WAVEţFORMATţPCM ; . waveform.nChannels = 1 ; waveform.nSamplesPerSec = 11025 ; waveform.nAvgBytesPerSec = 11025 ; waveform.nBlockAlign = 1 ; waveform.wBitsPerSample = 8 ; waveform.cbSize = 0 ; if (waveInOpen (&hWaveIn, WAVE_MAPPER, &waveform, (DWORD) hwnd. 0, CALLBACK WINDOW)) I ( free (pBufferl) ; free (pBuffer2) ; MessageBeep (MB_ICONEXCLAMATION) ; MessageBox (hwnd, szOpenError, szAppName, MBţICONEXCLAMATION ţ MB OK) ; ) // szykowanie nagłówków pWaveHdrl->lpData = pBufferl ; pWaveHdrl->dwBufferLength = INP BUFFER SIZE ; pWaveHdrl->dwBytesRecorded = 0 ; pWaveHdrl->dwUser = 0 ; pWaveHdrl->dwFlags = 0 ; pWaveHdrl->dwLoops = 1 ; pWaveHdrl->lpNext = NULL ; 1160 Częţć III: Zagadnienia zaawansowane (cišg dalszy ze strony 1159) pWaveHdrl->reserved = 0 ; j waveInPrepareHeader (hWaveIn, pWaveHdrl, sizeof (WAVEHDR)) ; pWaveHdr2->lpData = pBuffer2 ; pWaveHdr2->dwBufferLength = INP BUFFERţSIZE ; pWaveHdr2->dwBytesRecorded = 0 ; pWaveHdr2->dwUser = 0 ; pWaveHdr2->dwFlags = 0 ; pWaveHdr2->dwLoops = 1 ; pWaveHdr2->lpNext = NULL ; pWaveHdr2->reserved = 0 ; waveInPrepareHeader (hWaveIn, pWaveHdr2, sizeof (WAVEHDR)) ; return TRUE ; case IDCţRECORD END: // zresetowanie wejœcia w celu uzyskania ostatniego bufora bEnding = TRUE ; waveInReset (hWaveIn) ; return TRUE ; case IDCţPLAY_BEG: // otwarcie urzddzenia wyjœciowego waveform audio waveform.wFormatTag = WAVEţFORMAT PCM ; waveform.nChannels = 1 ; waveform.nSamplesPerSec = 11025 ; waveform.nAvgBytesPerSec = 11025 ; waveform.nBlockAlign = 1 ; waveform.wBitsPerSample = 8 ; waveform.cbSize = 0 ; if (waveOutOpen (&hWaveOut, WAVE_MAPPER, &waveform, (DWORD) hwnd, 0, CALLBACK WINDOW)) t MessageBeep (MBţICONEXCLAMATION) ; MessageBox (hwnd, szOpenError, szAppName, MBţICONEXCLAMATION ţ MB OK) ; ) return TRUE ; case IDCţPLAY PAUSE: /% zatrzymanie lub zrestartowanie wyjœcia if (!bPaused) f waveOutPause (hWaveOut) ; SetDlgItemText (hwnd, IDCţPLAYţPAUSE, TEXT ("Resume">) ; bPaused = TRUE ; } else f waveOutRestart (hWaveOut) ; SetDlgItemText (hwnd, IDCţPLAY PAUSE, TEXT ("Pause")) ; bPaused = FALSE ; Rozdział 22: Dtwięk i muryka 1161 ) return TRUE ; case IDCţPLAYţEND: // resetowanie wyjœcia w ramach przygotowań do zamknięcia bEnding = TRUE ; waveOutReset (hWaveOutT ; return TRUE ; case IDC_PLAY_REV: // na poczdtek bufora i właczenie odtwarzania bReverse = TRUE ; ReverseMemory (pSaveBuffer, dwOataLength) ; ! SendMessage (hwnd, WM COMMAND, IDCţPLAY BEG, 0) ; return TRUE ; case IDC_PLAY_REP: // ustawienie nieskończonego powtarzania // i włdczenie odtwarzania dwRepetitions = -1 ; , SendMessage (hwnd, WM COMMAND, IDCţPLAYţBEG, 0) ; ' return TRUE ; case IDC_PLAY_SPEED: // otwarcie urzšdzenia waveform do szybkiego odtwarzania ; waveform.wFormatTag = WAVEţFORMAT PCM ; waveform.nChannels = 1 ; waveform.nSamplesPerSec = 22050 ; , waveform.nAvgBytesPerSec = 22050 ; waveform.nBlockAlign = 1 ; waveform.wBitsPerSample = 8 ; waveform.cbSize = 0 ; if (waveOutOpen (&hWaveOut, 0, &waveform, (DWORD) hwnd, 0, CALLBACK WINDOW)) MessageBeep (MB_ICONEXCLAMATION) ; MessageBox (hwnd, szOpenError, szAppName, MB ICONEXCLAMATION ţ MB OK) ; , return TRUE ; break ; case MM_WIM_OPEN: // skurczenie bufora 1 pSaveBuffer = realloc (pSaveBuffer, 1) ; ' // blokowanie i odblokowywanie przycisków EnableWindow (GetDlgItem (hwnd, IDC_RECORD_BEG), FALSE) ; EnableWindow (GetOlgItem (hwnd, IDC_RECORD_END), TRUE) , EnableWindow (GetDlgItem (hwnd, IDCţPLAY BEG), FALSE) ; 1162 Częœć III: Zagadnienia zaawansowane (cišg dalszy ze strony 1161) EnableWindow (GetDlgItem (hwnd, IDC_PLAY_PAUSE), FALSE) ; EnableWindow (GetDlgItem (hwnd, IDC_PLAY_END), FALSE) ; EnableWindow (GetDlgItem (hwnd, IDC_PLAY_REV), FALSE) ; EnableWindow (GetDlgItem (hwnd, IDC_PLAY_REP), FALSE) ; EnableWindow (GetDlgItem (hwnd, IDC_PLAY_SPEED), FALSE) ; SetFocus (GetDlgItem (hwnd, IDCţRECORDţEND)) ; // dodanie buforów waveInAddBuffer (hWaveIn, pWaveHdrl, sizeof (WAVEHDR)) ; waveInAddBuffer (hWaveIn, pWaveHdr2, sizeof (WAVEHDR)) ; // poczštek próbkowania bRecording = TRUE ; bEnding = FALSE ; ' dwDataLength = 0 ; waveInStart (hWaveIn) ; r return TRUE ; case MM WIM DATA: // realokacja pamięci na bufor pNewBuffer = realloc (pSaveBuffer, dwDataLength + ((PWAVEHDR) lParam)->dwBytesRecorded) ; if (pNewBuffer = NULL) i waveInClose (hWaveIn) ; MessageBeep (MB_ICONEXCLAMATION) ; MessageBox (hwnd, szMemError, szAppName, MBţICONEXCLAMATION ţ MB OK) ; return TRUE ; pSaveBuffer = pNewBuffer ; CopyMemory (pSaveBuffer + dwDataLength, ((PWAVEHDR) lParam)->lpData, ((PWAVEHDR) lParam)->dwBytesRecorded) ; dwDataLength += ((pWAVEHDR) lParam)->dwBytesRecorded ; if (bEnding) ( waveInClose (hWaveIn) ; return TRUE ; t // wyslanie nowego bufora , waveInAddBuffer (hWaveIn, (PWAVEHDR) lParam, sizeof (WAVEHDR)) ; return TRUE ; , case MM WIM_CLOSE: // zwolnienie pamięci bufora Rozdział 22: DŸwięk i muryka 1163 , waveInUnprepareNeader (hWaveIn, pWaveNdrl, sizeof (WAVEHDR)) ; waveInUnprepareHeader (hWaveIn, pWaveHdr2, sizeof (WAVEHDR)) ; free (pBufferl) ; free (pBuffer2) ; // blokowanie i odblokowywanie przycisków EnableWindow (GetDlgItem (hwnd, IDC_RECORD_BEG), TRUE) ; EnableWindow (GetDlgItem (hwnd, IDC_RECORD_END), FALSE) ; SetFocus (GetDlgItem (hwnd, IDCţRECORDţBEG>) ; if (dwOataLength > 0) , f EnableWindow (GetDlgItem (hwnd, IDC_PLAY BEG), TRUE) , EnableWindow (GetDlgItem (hwnd, IDC_PLAY_PAUSE), FALSE) ; ' EnableWindow (GetOlgItem (hwnd, IDC_PLAY_END), FALSE) ; EnableWindow (GetDlgItem (hwnd, IDC_PLAY_REP), TRUE) , , EnableWindow (GetDlgItem (hwnd, IDC_PLAY_REV), TRUE) ř EnableWindow (GetDlgItem (hwnd, IDC_PLAY_SPEED), TRUE) , SetFocus (GetDlgItem (hwnd, IDCţPLAY BEG)) ; J bRecording = FALSE ; ) if (bTerminating) SendMessage (hwnd, WM SYSCOMMAND, SCţCLOSE, OL) ; return TRUE ; case MM_WOM_OPEN: // blokowanie i odblokowywanie przycisków EnableWindow (GetDlgItem (hwnd, IDC_RECORD_BEG), FALSE) ; , EnableWindow (GetOlgItem (hwnd, IDC_RECORDţEND), FALSE) ; EnableWindow (GetDlgItem (hwnd, IDC_PLAY_BEG), FALSE) ; EnableWindow (GetDlgItem (hwnd, IDC_PLAY_PAUSE), TRUE) EnableWindow (GetDlgItem (hwnd, IDC_PLAY END), TRUE) , EnableWindow (GetOlgItem (hwnd, IDC_PLAY REP), FALSE) ; EnableWindow (GetDlgItem (hwnd, IDC_PLAY REV), FALSE) ; EnableWindow (GetDlgItem (hwnd, IDC_PLAY SPEED), FALSE) ; SetFocus (GetDlgItem (hwnd, IDC PLAY END)) ; // inicjowanie naglówka pWaveNdrl->lpData = pSaveBuffer ; pWaveHdrl->dwBufferLength = dwOataLength ; pWaveHdrl->dwBytesRecorded = 0 ; pWaveHdrl->dwUser = 0 ; pWaveHdrl->dwFlags = WHDRţBEGINLOOP ţ WHDR ENDLOOP ; pWaveHdrl->dwLoops = dwRepetitions ; ' pWaveHdrl->lpNext = NULL ; pWaveHdrl->reserved = 0 ; // przygotowanie i zapisanie waveOutPrepareHeader (hWaveOut, pWaveHdrl, sizeof (WAVEHDR)) ; waveOutWrite (hWaveOut, pWaveHdrl, sizeof (WAVEHDR)) ; 1164 Częœć III: Zagadnienia zaawansowane (cišg dalszy ze strony 1163) bEnding = FALSE ; bPlaying = TRUE ; return TRUE ; case MM_WOM_DONE: waveOutUnprepareHeader (hWaveOut, pWaveHdrl, sizeof (WAVEHDR)) ; waveOutClose (hWaveOut) ; return TRUE ; case MM_WOM_CLOSE: // blokowanie i odblokowywanie przycisków EnableWindow (GetDlgItem (hwnd, IDC_RECORD_BEG), TRUE) , EnableWindow (GetDlgItem (hwnd, IDC_RECORD_END), TRUE) , EnableWindow (GetDlgItem (hwnd, IDCţPLAY_BEG), TRUE) , EnableWindow (GetOlgItem (hwnd, IDC_PLAY_PAUSE), FALSE) ; EnableWindow (GetDlgItem (hwnd, IDC_PLAY_END), FALSE) ; EnableWindow (GetDlgItem (hwnd, IDC_PLAY_REV), TRUE) , EnableWindow (GetDlgItem (hwnd, IDC_PLAY_REP), TRUE) , EnableWindow (GetDlgItem (hwnd, IDCţPLAY_SPEED>, TRUE) , SetFocus (GetDlgItem (hwnd, IDCţPLAY BEG)) ; SetDlgItemText (hwnd, IDCţPLAY PAUSE, TEXT ("Pause")) ; bPaused = FALSE ; dwRepetitions = 1 ; bPlaying = FALSE ; if (bReverse) f ReverseMemory (pSaveBuffer, dwDataLength) ; bReverse = FALSE ; if (bTerminating) SendMessage (hwnd, WM SYSCOMMAND, SCţCLOSE, OL) ; return TRUE ; case WM SYSCOMMAND: switch (LOWORD (wParam)) ( case SC_CLOSE: if (bRecording) ( bTerminating = TRUE ; bEnding = TRUE ; waveInReset (hWaveIn) ; return TRUE ; ) if (bPlayi.ng) ( bTerminating = TRUE ; bEnding = TRUE ; waveOutReset (hWaveOut) ; return TRUE ; Rozdział 22: Dtwięk i muryka 1165 free (pWaveHdrl) ; free (pWaveHdr2) ; ř. free (pSaveBuffer) ; EndDialog (hwnd, 0) ; return TRUE ; J break ; l return FALSE ; RECORD.RC (fragmenty) //Microsoft Developer Studio generated resource script. ţţinclude "resource.h" ţţinclude "afxres.h" ///////////////////////////////////////////////////////////////////////////// // Dialog RECORD DIALOG DISCARDABLE 100, 100, 152, 74 STYLE WS MINIMIZEBOX ţ WSţVISIBLE ţ WS CAPTION ţ WSţSYSMENU CAPTION "Waveform Audio Recorder" FONT 8, "MS Sans Serif" BEGIN PUSHBUTTON "Record",IDC_RECORD_BEG,28,8,40,14 PUSHBUTTON "End",IDC_RECORD_END,76,8,40,14,WS_DISABLED PUSHBUTTON "Play",IDC_PLAY BEG,8,30,40,14,WS_DISABLED PUSHBUTTON "Pause",IDC_PLAY PAUSE,56,30,40,14,WS_DISABLED PUSHBUTTON "End",IDC_PLAY_END,104,30,40,14,WS_DISABLED PUSHBUTTON "Reverse",IDC_PLAY_REV,8,52,40,14,WS_DISABLED PUSHBUTTON "Repeat",IDC_PLAY_REP,56,52,40,14,WS_DISABLED PUSHBUTTON "Speedup",IDCţPLAYţSPEED,l04,52,40,14,WS DISABLED END RESOURCE.H (fragmenty) // Microsoft Developer Studio generated include file. // Used by Record.rc ţţdefine IDC_RECORD_BEG 1000 ţţdefine IDC_RECORD_END 1001 ţţdefine IDC_PLAY_BEG 1002 ţţdefine IDC_PLAY_PAUSE 1003 ţţdefine IDC_PLAY_END 1004 ţţdefine IDC_PLAY_REV 1005 ' ţţdefine IDC_PLAY_REP 1006 ţţdefine IDCţPLAY SPEED 1007 Rysunek 22-3. Program RECORDI 1166 Częœó III: Zagadnienia zaawansowane Pliki RECORD.RC i RESOURCE.H będš wykorzystane również w programach RECORD2 i RECORD3. Okno programu RECORDI ma osiem przycisków. Kiedy program zostanie uru- chomiony po raz pierwszy, dostępny będzie tylko przycisk Record. Kliknięcie go uruchamia proces rejestrowania. Przycisk Record staje się niedostępny, ale za to uaktywnia się przycisk End, za pomocš którego można przerwać rejestrowanie. Od tego momentu uaktywniajš się przyciski Play, Reverse, Repeat i Speedup. Naciœnięcie dowolnego z nich powoduje odtworzenie dŸwięku: Play skutkuje odtworzeruem w zwykły sposób, Reverse - od tyłu, Repeat - odtwarzaniem ciš- głyxn, a Speedup - dwukrotnie przyspieszonym. Odtwarzanie można przerwać za pomocš drugiego przycisku End albo wstrzymać za pomocš przycisku Pause. Przycisk Pause po kliknięciu zmienia się w przycisk Resume (przerwane odtwa- rzanie dŸwięku). Jeżeli zarejestruje się nowy dŸwięk, zastšpi on poprzednio za- pisany w pamięci. Tylko dostępne przyciski pozwalajš przeprowadzać operacje. Wymaga to wielu wywołań funkcji Enable4Vindow w kodzie Ÿródłowym programu RECORDl, ale program nie musi sprawdzać, czy dana operacja jest dozwolona. Blokowanie przy- cisków sprawia, że korzystanie z programu staje się bardziej intuicyjne. W programie zastosowano kilka skrótów upraszczajšcych kod. Po pierwsze, je- żeli w komputerze znajduje się wiele urzšdzeń audio, program użyje domyœlne- go. Po drugie, program rejestruje i odtwarza dŸwięk z częstotliwoœciš próbko- wania 11,025 kHz i z próbkami o wielkoœci 8 bitów, niezależnie od tego, czy inne (lepsze) parametry sš dostępne. Jedynym wyjštkiem jest funkcja przyspieszania, podczas której RECORDI odtwarza dŸwięk z prędkoœciš 22,050 kHz. Jest to pro- sty sposób podwojenia częstotliwoœci dŸwięku, a więc zagrania o oktawę wyżej. Rejestrowanie dŸwięku wymaga otwarcia wejœciowego urzšdzenia waveform audio i przekazania do API buforów, które odbiorš dane. W RECORD1 przewidziano kilka bloków pamięci. Trzy z nich sš bardzo małe, przynajmniej poczštkowo, i alokowane podczas obsługi komunikatu WMţINIT DIALOG w DlgProc. Program rezerwuje miejsce na dwie struktury WAVEHDR, na które wskazujš pWaveHdrl i plNaveHdr2. Struktury te służš do przekazywania buforów do API. WskaŸnik pSaveBuffer wskazuje na bufor przeznaczony na zapi- sywanie w pełni zarejestrowanego dŸwięku; poczštkowo jest on alokowany jako 1-bajtowy. Potem, podczas rejestrowania bufor jest powiększany tak, aby zmie- œciły się w nixn wszystkie dane dŸwiękowe. jeżeli rejestruje się zbyt dhzgo, pro- gram potrafi wyjœć z opresji z gracjš: przerwie rejestrowanie w punkcie, w któ- rym zabrakło miejsca w pamięci i pozwoli na odtwarzanie do tego miejsca. Wspo- mruany bufor będę nazywał buforem danych, porueważ służy on właœnie do prze- chowywarua danych. Dwa dodatkowe bloki pamięci, po 16 KB każdy, wskazy- wane przez pBufferl i pBuffer2, sš alokowane podczas rejestrowania danych dŸwię- kowych. Bufory te sš zwalniane po zakończeruu rejestrowania. Każdy z oœmiu przycisków generuje komunikat WM COMMAND przesyłany do DlgProc, procedury okna REPORTl. Poczštkowo dostępny jest tylko przycisk Record. Naciœnięcie go generuje komunikat WM COMMAND z parametrem wParam równym IDC RECORD BEG. Aby obsłużyć ten komunikat, RECORD1 1 Rozdział 22: Dtwięk i muryka 1167 alokuje dwa bufory po 16 KB, przeznaczone na odebranie danych dŸwiękowych, inicjuje pola struktury WAVEFORMATEX, przekazuje strukturę funkcji wavelnO- pen i konfiguruje dwie struktury WAVEHDR. Funkcja wavelnOpen generuje komunikat MMţWávIţOPEN. Podczas obshzgi tego komunikatu RECORDI zmniejsza bufor danych do 1 bajtu, aby przygotować się na odebranie danych. Oczywiœcie podczas pierwszego rejestrowania bufor ma już rozmiar 1, ale podczas kolejnych zapisów może być o wiele większy. Podczas obsługi komunikatu MMţWIM OPEN program również blokuje i odblokowuje stosowne przyciski. Następnie program przekazuje do API dwie struktury WA- VEHDR i bufory, używajšc funkcji wavelnAddBuffer. Ustawiane sš niektóre znacz- niki i zaczyna się rejestrowanie (za pomocš wywołania wavelnStart). Przy częstotliwoœci próbek 11,025 kHz i 8-bitowych próbkach 16-kilobajtowy bufor zostanie zapełruony w czasie około 1,5 sekundy. W tym momencie RECORD1 otrzyma komunikat MM WIM DATA. W odpowiedzi nań program dokona re- alokacji bufora danych, opierajšc się na zmiennej dwDataLength i polu dwBytes- Recorded struktury WAVEHDR. Jeżeli realokacja się nie powiedzie, RECORD1 wywoła wavelnClose, aby zatrzymać rejestrowanie. Jeżeli zakończy się powodzeniem, program kopiuje dane z 16-kilobajtowego bu- fora do bufora danych. Następnie ponownie wywołuje funkcję wavelnAddBuffer. Proces ten trwa do czasu, aż zabraknie pamięci na bufor danych albo aż użyt- kownik kliknie przycisk End. Przycisk End generuje komunikat WM COMMAND z parametrem wParam równym IDC RECORD END. Przekazanie tego komunikatu jest proste. RECORDI ustawia znacznik bEnding na TRUE i wywołuje wavelnReset. Funkcja wavelnReset powoduje przerwanie rejestrowania i wygenerowanie komunikatu MMţWINIţDATA, zawie- rajšcego częœciowo zapełniony bufor. RECORD1 odpowiada na ten ostatni ko- munikat MM WIM DATA jak zwykle, tylko że dodatkowo zamyka wejœciowe urzšdzenie waveform audio (wywołujšc wavelnClose). Komunikat wavelnClose generuje komunikat MM WIM CLOSE. RECORDI od- powiada nań zwolrueniem 16-kilobajtowego bufora wejœciowego i zablokowaruem bšdŸ odblokowaniem stosownych przycisków. Na przykład wszystkie przyciski zwišzane z odtwarzaniem sš odblokowane, jeżeli bufor danych zawiera dane, czyli prawie zawsze (œciœlej: zanim nie dojdzie do błędnego zakończenia pierw- szej realokacji). Po zarejestrowaniu dŸwięku bufor danych zawiera wszystkie zakuxnulowane dane dŸwiękowe. Kiedy użytkownik kliknie przycisk Play, procedura DlgProc otrzy- muje komunikat 4VMţCOMMAND z wParam równym IDC PLAY BEG. Program odpowiada, inicjujšc pole struktury WAVEFORMATEX i wywohxjšc waveOutO- pen. Kolejne wywołanie waveOutOpen generuje komunikat MM WOM OPEN. Pod- czas jego obsługi RECORD1 blokuje i odblokowuje odpowiednie przyciski (ze- zwalajšc tylko na zatrzymanie i zakończenie), inicjuje pola struktury WAVEHDR, przygotowuje bufor za pomocš waveOutPrepareHeader i zaczyna odtwarzanie, wywołujšc funkcję waveOutWrite. 1168 Częœć III: Zagadnienia zaawansowane Zwykle dŸwięk będzie trwał do czasu wyczerpania wszystkich danych z bufora, po czym zostanie wgenerowany komunikat MMţWOM DONE. Jeżeli sš jakieœ dodatkowe bufory do odtworzenia, w tym momencie program może je przeka- zać do API. RECORDI odtwarza tylko jeden duży bufor, więc program po pro- stu sprzšta po nagłówku i wywołuje waveOutClose. Funkcja waveOutClose gene- ruje komunikat MM WOM CLOSE. Podczas jego obshzgi RECORD1 blokuje i od- blokowuje odpowiednie przyciski, zezwalajšc znów na odtwarzanie dŸwięku ist- niejšcego albo na zarejestrowanie nowego. Przewidziałem również drugi przycisk End, aby użytkownik mógł zatrzymać odtwarzanie dŸwięku w dowolnej chwili, zanim wyczerpie bufor. Ten przycisk End generuje komunikat WM COMMAND z wParam równym IDC PLAY END, na który program odpowiada wywołaniem funkcji waveOutReset. Generuje ona komunikat MM WOM DONE, który jest odtwarzany normalnie. Okno programu RECORDI zawiera także przycisk Pause. Mechanizm jego dzia- łania jest prosty. Kiedy zostanie kliknięty pierwszy raz, wywoływana jest funk- cja waveOutPause, która przerywa odtwarzarue dŸwięku i zmienia napis na przy- cisku na Resume. Kliknięcie przycisku "Resume" powoduje ponowienie odtwa- rzania (od miejsca, w którym zostało przerwane), co jest możliwe dzięki funkcji waveOutRestart. Aby uatrakcyjnić nieco program, zaprojektowałem przyciski Reverse, Repeat i Speedup. Generujš one komunikat WMţCOMMAND z parametrem wParam równym odpowiednio II7CţPLAY REV, IDC PLAY REP i IDC PLAY SPEED. Odtwarzanie dŸwięku od tyłu wymaga odwrócenia kolejnoœci bajtów bufora danych i normalnego odtworzenia dŸwięku. RECORDI zawiera małš funkcję o nazwie ReverseMemory, służšcš do odwracania kolejnoœci bajtów. Jest ona wy- woływana podczas obshxgi komunikatu WM COMMAND przed odtworzeniem bloku i drugi raz pod koruec komunikatu MM WOM CLOSE (w celu przywró- cenia normalnej kolejnoœci danych). Przycisk Repeat odtwarza dŸwięk zapętlony, czyli bez końca, co nie jest trudne do osišgnięcia, ponieważ API zawiera opcję przeznaczonš specjalnie do tego celu. Wystarczy polu dwLoops struktury WAVEHDR przypisać liczbę powtórzeń, polu dwFlags struktury WHDR BEGINLOOP - poczštek bufora, a polu dwFlags struk- tury WHDRţENDLOOP - koniec bufora. Ponieważ RECORD1 odtwarza dŸwięk tylko z jednego bufora, w polu dwFlags połšczone sš obie flagi. Dwukrotne odtwarzanie dŸwięku jest również proste. Wystarczy, że podczas ini- cjacji pól struktury WAVEFORMATEX realizowanej w ramach przygotowań do otwarcia wyjœciowego urzšdzenia waveform audio pola nSamplesPerSec i nAvg- BytesPerSec otrzymajš wartoœci 22 050, a nie 11 025. Rozwišzanie alternatywne z zastosowaniem MCI Zapewne podobnie jak ja stwierdzisz, że program RECORDI jest zbyt skompli- kowany. Szczególnie złożona jest interakcja między wywołaniami waveform au- dio i komunikatami, które generujš, a także zamieszarue z pamięciš, której w każ- dej chwili może zabraknšć. Dlatego właœnie program ten nazwałem interfejsem Rozdział 22: Dtwięk i muzyka ţ 1 gg niskopoziomowym. Jak wspomniałem wczeœniej w tym rozdziale, Windows ofe- ruje również interfejs wysokopoziomowy, zwany MCI (Media Control Interface). W przypadku urzšdzeń waveform audio najważniejsza różnica między interfej- sem niskopoziomowym i MCI jest taka, że MCI rejestruje dŸwięk bezpoœrednio do pliku wave i odtwarza dŸwięk również z pliku. Utrudnia to realizowanie efek- tów specjalnych, które tak ładnie dało się zaimplementować w RECORDl. W przy- padku MCI trzeba odczytać plik, obrobić go i zapisać w zmienionej postaci do pliku, a dopiero potem wywołać mechanizm odtwarzania dŸwięku. Takie sš kon- sekwencje wygód oferowanych przez interfejsy wysokiego poziomu. Interfejs niskiego poziomu daje dużš swobodę, a MCI (w większej częœci) jest prostszy w obsłudze. MCI ma dwie różne, ale zbliżone implementacje. Pierwsza opiera się na komuni- katach i strukturach danych, wysyłaniu poleceń do urzšdzeń multimediów i od- bieraniu od nich danych. Druga na łańcuchach ASCá. Interfejs tekstowy został utwo- rzony z myœlš o obsłudze multimediów za poœrednictwem prostych języków skryp- towych, ale zapewnia bardzo prosty sposób interakcyjnego sterowania systemem multimediów. Można się o tym przekonać na przykładzie programu TESTMCI, pre- zentowanego na poczštku tego rozdziahz. Program RECORD2, przedstawiony na rysunku 22-4, oparty jest na komunika- tach i strukturach. Jest on nowš realizacjš programu do cyfrowego rejestrowania i odtwarzania dŸwięku, opartš na MCI. Choć w programie wykorzystano to samo okno dialogowe co w RECORDI, nie przewidziano w nim trzech przycisków do efektów specjalnych. RECORD2.C l* RECORD2.C - Cyfrowy rejestrator dŸwięku (c) Charles Petzold, 1998 *l ţţinclude llinclude "..\\recordl\\resource.h" BOOL CALLBACK DlgProc (HWND, UINT, WPARAM, LPARAP1) ; TCHAR szAppName [] = TEXT ("Record2") ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) if (-1 ţ DialogBox (hInstance, TEXT ("Record"), NULL, DlgProc)) MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MB ICONERROR) ; } return 0 ; Częœć III: Zagadnienia zaawansowane (cišg dalszy ze strony 1169) void ShowError (HWND hwnd, DWORD dwError) f TCHAR szErrorStr C1024] ; mciGetErrorString (dwError, szErrorStr, sizeof (szErrorStr) / sizeof (TCHAR)) ; MessageBeep (MB_ICONEXCLAMATION) ; MessageBox (hwnd, szErrorStr, szAppName, MB OK ţ MBţICONEXCLAMATION) ; ) BOOL CALLBACK DlgProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ( static BOOL bRecording, bPlaying, bPaused ; static TCHAR szFileNameC] = TEXT ("record2.wav") ; static WORD wDeviceID ; DWORD dwError ; MCI_GENERIC_PARMS mciGeneric ; MCI_OPEN_PARMS mci0pen ; MCI_PLAY_PARMS mciPlay ; MCI_RECORDţPARMS mciRecord ; MCIţSAVEţPARMS mciSave ; switch (message) ; ( case WM COMMAND: switch (wParam) ( case IDC_RECORD_BEG: // usuwanie istniejdcego pliku wave DeleteFile (szFileName) ; // otwarcie urzadzenia audio mci0pen.dwCallback = 0 ; mci0pen.wDeviceID = 0 ; mci0pen.lpstrDeviceType = TEXT ("waveaudio") ; mci0pen.lpstrElementName = TEXT ("") ; mci0pen.lpstrAlias = NULL ; dwError = mciSendCommand (0, MCI_OPEN, MCI_WAIT ţ MCI_OPEN TYPE ţ MCI OPEN_ELEMENT, (DWORD) (LPMCIţOPENţPARMS) &mci0pen) ; if (dwError != 0) ( ShowError (hwnd, dwError) ; return TRUE ; // zapisanie identyfikatora urzšdzenia w0eviceID = mci0pen.wUeviceID ; // poczdtek rejestrowania mciRecord.dwCallback = (DWORD) hwnd ; mciRecord.dwFrom = 0 ; mciRecord.dwTo = 0 ; Rozdział 22: Dtwięk i muryka 1171 mciSendCommand (wDeviceID, MCI_RECORD, MCI_NOTIFY, (DWORD) (LPMCIţRECORD PARMS) &mciRecord> ; // blokowanie i odblokowywanie przycisków EnableWindow (GetDlgItem (hwnd, IDC_RECDRD_BEG), FALSE); EnableWindow (GetDlgItem (hwnd, IDC_RECORD_END), TRUE) ; EnableWindow (GetDlgItem (hwnd, IDC_PLAY_BEG), FALSE); EnableWindow (GetDlgItem (hwnd, IDC_PLAY_PAUSE), FALSE); EnableWindow (GetDlgItem (hwnd, IDC_PLAY_END), FALSE); SetFocus (GetDlgItem (hwnd, IDCţRECORD END)) ; bRecording = TRUE ; return TRUE ; case IDCţRECORDţEND: // przerwanie rejestrowania mciGeneric.dwCallback = 0 ; mciSendCommand (wDeviceID, MCI_STOP, MCI_WAIT, (DWORD) (LPMCIţGENERIC PARMS) &mciGeneric) ; // zapisanie pliku mciSave.dwCallback = 0 ; mciSave.lpfilename = szFileName ; mciSendCommand (wDeviceID, MCI_SAVE, MCI_WAIT ţ MCI_SAVEţFILE, (DWORD) (LPMCI SAVEţPARMS) &mciSave) ; // zamknięcie urzddzenia waveform device mciSendCommand (w0eviceID, MCI_CLOSE, MCI_WAIT, (DWDRD) (LPMCI GENERIC PARMS) &mciGeneric) ; // blokowanie i odblokowywanie przycisków EnableWindow (GetDlgItem (hwnd, IDC_RECORD_BEG), TRUE) ; EnableWindow (GetDlgItem (hwnd, IDC_RECORD_END), FALSE); EnableWindow (GetDlgItem (hwnd, IDC_PLAY_BEG), TRUE) ; EnableWindow (GetDlgItem (hwnd, IDC_PLAY_PAUSE), FALSE); EnableWindow (GetDlgItem (hwnd, IDC_PLAY_END), FALSE); SetFocus (GetDlgItem (hwnd, IDCţPLAYţBEG)) ; bRecording = FALSE ; return TRUE ; ' case IDCţPLAY BEG: // otwarcie urzddzenia waveform audio mci0pen.dwCallback = 0 ; mci0pen.wDeviceID = 0 ; mci0pen.lpstrDeviceType = NULL ; mci0pen.lpstrElementName = szFileName ; mci0pen.lpstrAlias = NULL ; 1172 Częœć III: Zagadnienia zaawansowane (cišg dalszy ze strony 1171) dwError = mciSendCommand (0, MCI_OPEN, MCI_WAIT ţ MCI_OPEN_ELEMENT, (DWORD) (LPMCI OPENţPARMS) &mci0pen) ; if (dwError != 0) ShowError (hwnd, dwError) ; return TRUE ; ) // zapisanie identyfikatora urzddzenia wDeviceID = mci0pen.wDeviceID ; // poczdtek odtwarzania mciPlay.dwCallback = (DWORD) hwnd ; mciPlay.dwFrom = 0 ; mciPlay.dwTo = 0 ; mciSendCommand (wDeviceID, MCI_PLAY, MCI_NOTIFY, (DWORD) (LPMCIţPLAY PARMS) &mciPlay) ; // blokowanie i odblokowywanie przycisków EnableWindow (GetDlgItem (hwnd, IDC_RECORD_BEG), FALSE); EnableWindow (GetDlgItem (hwnd, IDCţRECORD_END), FALSE); EnableWindow (GetDlgItem (hwnd, IDC_PLAY_BEG), FALSE); EnableWindow (GetDlgItem (hwnd, IDC_PLAY_PAUSE), TRUE) ; EnableWindow (GetDlgItem (hwnd, IDCţPLAY_END), TRUE) ; SetFocus (GetDlgItem (hwnd, IDCţPLAY END)) ; bPlaying = TRUE ; return TRUE ; case IDC_PLAY_PAUSE: if (!bPaused) // zatrzymanie odtwarzania mciGeneric.dwCallback = 0 ; , mciSendCommand (wDeviceID, MCI_PAUSE, MCIţWAIT, (DWORD) (LPMCIţGENERICţPARMS) & mciGeneric); SetDlgItemText (hwnd, IDCţPLAYţPAUSE, TEXT ("Resume")) ; bPaused = TRUE ; ) else // ponowne rozpoczęcie odtwarzania ', ( mciPlay.dwCallback = (DWORD) hwnd ; mciPlay.dwFrom = 0 ; mciPlay.dwTo = 0 ; mciSendCommand (wDeviceID, MCI_PLAY, MCI_NOTIFY, (DWORD) (LPMCIţPLAYţPARMS) &mciPlay) ; Rozdział 22: Diwięk i muryka 1173 SetDlgItemText (hwnd, IDC PLAY_PAUSE, TEXT ("Pause")) ; bPaused = FALSE ; return TRUE ; case IDCţPLAY END: /% zatrzymanie i zamknięcie mciGeneric.dwCallback = 0 ; mciSendCommand (wDeviceID, MCI_STOP, MCI_WAIT, (DWORD) (LPMCIţGENERICţPARMS) &mciGeneric) ; mciSendCommand (wDeviceID, MCIţCLOSE, MCI_WAIT, (DWORD) (LPMCI GENERICţPARMS) &mciGeneric) ; // blokowanie i odblokowywanie przycisków EnableWindow (GetDlgItem (hwnd, IDCţRECORD_BEG), TRUE) ; EnableWindow (GetDlgItem (hwnd, IDCţRECORD_END), FALSE); EnableWindow (GetDlgItem (hwnd, IDC_PLAY_BEG), TRUE) ; EnableWindow (GetDlgItem (hwnd, IDC_PLAY_PAUSE), FALSE); EnableWindow (GetDlgItem (hwnd, IDC_PLAY_END), FALSE); SetFocus (GetD7gItem (hwnd, IDCţPLAYţBEG)) ; bPlaying = FALSE ; bPaused = FALSE ; return TRUE ; ) break ; case MM MCINOTIFY: switch (wParam) f case MCI_NOTIFY SUCCESSFUL: if (bPlaying) ŒendMessage (hwnd, WMţCOMMAND, IDCţPLAY END, 0) ; if (bRecording) SendMessage (hwnd, WM COMMAND, IDC RECORDţEND, 0); return TRUE ; ? break ; case WM SYSCOMMAND: switch (wParam) . ( case SC_CLOSE: if (bRecording) SendMessage (hwnd, WM COMMAND, IDC RECORD END, OL) ; if (bPlaying) SendMessage (hwnd, WM COMMAND, IDCţPLAY END, OL) ; EndOialog (hwnd, 0) ; return TRUE ; 1174 Częœć III: Zagadnienia zaawansowane (cišg dalszy ze strony 1173) ) break ; ) return FALSE ; Rysunek 22-4. Program RECORD2 W programie RECORD2 występujš tylko dwa wywołania funkcji MCI, z których ważniejsze to: error = mciSendCommand (wDeviceID. message, dwFlags, dwParam) Pierwszy argument jest liczbowym identyfikatorem urzšdzenia. Używa się go jak uchwytu, a uzyskuje podczas otwierania urzšdzenia. Można go następrue wyko- rzystywać w kolejnych wywołaniach mciSendCommand. Drugi argument to stała zaczynajšca się prefiksem MCI i oznaczajšca polecenie MCI. Istnieje kilka takich stałych. W programie RECORD2 występuje ich aż siedem: MCI OPEN, MCI RE- CORD, MCI STOP, MCI SAVE, MCI PLAY, MCI PAUSE i MCI CLOSE. Argument dwFlags może być zerem albo kilkoma stałymi, połšczonymi operato- rem bitowej alternatywy. Stałe wskazujš najczęœciej różne opcje, niektóre charak- terystyczne dla wybranych poleceń, inne ogólne i odnoszšce się do wszystkich. Argument dwParam jest z reguły długim wskaŸnikiem do struktury danych słu- żšcej do przekazywania opcji i pobierania danych od urzšdzenia. Wiele komuni- katów MCI ma przypisane nietypowe struktury danych. Funkcja mciSendCommand zakończona powodzeniem zwraca zero, a w przeciw- nym razie kod błędów. Do poinformowania użytkownika o błędzie można wy- korzystać następujšcš funkcję zwracajšcš treœć komunikatu o błędzie: mciGetErrorString (error. szBuffer. dwLength) Ta sama funkcja została użyta w programie TESTMCI. Kiedy użytkownik naciska przycisk Record, procedura okna programu RECORD2 otrzymuje komunikat WM COMMAND z parametrem wParam równym IDC RE- CORD BEG. Program zaczyna od otwarcia urzšdzenia. Wymaga to zainicjowa- nia pól struktury MCI OPEN PARMS i wywołania funkcji mciSendCommand z ko- munikatem MCI OPEN. Do rejestrowania pole lpstrDeviceType ustawia się na łań- cuch "waveaudio", oznaczajšcy typ urzšdzenia. Pole lpstrElementName jest usta- wiane na cišg zerowej długoœci. Sterownik MCI używa domyœlnej częstotliwoœci próbek i domyœlnego rozmiaru próbek, ale parametry te można zmienić za po- mocš polecenia MCI SET. Rejestrowany dŸwięk jest zapisywany na dysku twar- dym w pliku tymczasowym. Dane sš składowane w formacie pliku wave. For- mat ten omówię w dalszej częœci rozdziału. Do odtwarzania dŸwięku MCI uży- wa częstotliwoœci próbek i rozmiarów próbek zdefiniowanych w pliku wave. Jeżeli RECORD2 nie może otworzyć urzšdzerua, informuje o tym użytkownika za pomocš mciGetErrorString i MessageBox. Jeżeli urzšdzenie zostanie otwarte, po po- wrocie z funkcji mciSendCommand pole wDevicelD struktury MCI OPEN PARMS będzie zawierało identyfcator urzšdzenia (przeznaczony do używania w kolej- nych wywołaniaeh). Rozdział 22: DŸwięk i muryka 1175 Aby rozpoczšć rejestrowanie, RECORD2 wywołuje mciSendCommand z komuni- katem MCI RECORD i strukturš danych MCI WAVĘ RECORD PARMS. Moż- na ewentualnie tak ustawić pola dwFrom i dwTo tej struktury (użyć znaczników bitowych), aby wstawić dŸwięk do istniejšcego pliku. Wówczas nazwę tego pli- ku okreœla się w polu IpstrElementName struktury MCI OPEN PARMS. Domyœl- nie każdy nowy dŸwięk jest wstawiany na poczštku istniejšcego pliku. W programie RECORD2 polu dwCallback struktury MCI_WAVE_RE- CORD PARMS przypisany został uchwyt okna programu, a w wywołaniu mci- SendCommand - umieszczony znacznik MCI NOTIFY. W ten sposób komunikat wysłany po zakończeniu rejestrowania zostanie skierowany do procedury okna. Komunikat ten omówię nieco dalej. Po zakończeniu rejestrowania użytkownik klika przycisk End. Powoduje to wy- generowanie komunikatu WM_COMMAND z parametrem wParam równym IDC RECORD END. Procedura okna odpowiada trzykrotnym wywołaniem mci- SendCommand: polecenie MCI STOP zatrzymuje rejestrowanie, polecenie MCI_SA- VE przekazuje dane dŸwiękowe z pliku tymczasowego do pliku wskazanego w strukturze MCI SAVĘ PARMS ("record2.wav"), a polecenie MCI_CLOSE usu- wa wszelkie pliki tymczasowe i bloki pamięci, które zostały wczeœniej utworzo- ne, i zamyka urzšdzenie. W przypadku odtwarzania polu lpstrElementName struktury MCI OPEN PARMS przypisywana jest nazwa pliku "record2.wav". Znacznik MCI OPEN ELEMENT umieszczony w trzecirn argumencie mciSendCommand oznacza, że pole lpstrEle- mentName jest nazwš istniejšcego pliku. MCI rozpoznaje, że chodzi o otwarcie urzšdzenia waveform audio, na podstawie rozszerzenia .WAV. Jeżeli w kompu- terze zainstalowano wiele urzšdzeń waveform audio, otwarte zostanie pierwsze z nich (inne urzšdzenie można otworzyć, ustawiajšc odpowiednio pole IpstrDe- viceType struktury MCI OPEN PARMS). Odtwarzanie wymaga wywołania mciSendCommand z poleceniem MCI_PLAY i strukturš MCI PLAY PARMS. Można odtworzyć dowolnš częœć pliku, niemniej RECORD2 odtwarza cały plik. RECORD2 zawiera także przycisk Pause. Za jego pomocš użytkownik może za- trzymać odtwarzanie pliku. Przycisk ten generuje komunikat WM COMMAND z parametrem wParam równym IDC_PLAY_PAUSE. Program odpowiada wywo- łaruem mciSendCommand z kornunikatem poleceniowym MCI PAUSE i strukturš MCI GENERIC PARMS. Struktura MCI GENERIC PARMS jest używana łšcz- nie z każdym komunikatem, który nie wymaga żadnych danych oprócz opcjo- nalnego uchwytu okna przyjmujšcego potwierdzenia. Jeżeli odtwarzanie jest w danej chwili zatrzymane, przycisk wznawia je. Jest to realizowane ponownym wywołaniem mciSendCommand z poleceniem MCI PLAY. Odtwarzarue można również zakończyć drugim przyciskiem End. Powodizje to wygenerowanie komunikatu WMţCOMMAND z parametrem wParam równym IDC PLAY END. Procedura okna odpowiada dwukrotnym wywolaniem mciSend- Command: pierwsze zawiera polecenie MCI STOP, a drugie polecenie MCHCLO- SE. 1176 Częœć III: Zagadnienia zaawansowane A teraz jeszcze pewien problem: odtwarzanie można przerwać ręcznie za pomo- cš przycisku End, ale jeœli użytkownik zechce odtworzyć cały plik, skšd program ma wiedzieć, że plik się skończył? Jest to zadarue dla komunikatu potwierdzajš- cego MCI. W wywołaruach mciSendCommand z komunikatami MCI RECORD i MCI PLAY program RECORD2 włšcza znaczrk MCI NOTIFY i przypisuje polu dwCallback uchwyt okna programu. W ten sposób po zakończeniu rejestrowania do proce- dury okna zostanie skierowany komunikat MM MCINOTIFY. Parametr wParam komunikatu zawiera zakodowane informacje o stanie, a argument lParam iden- tyfikator urzšdzenia. Kiedy funkcja mciSendCommand zostarue wywołana z MCI STOP lub MCI PAU- SE, program otrzyma komunikat MMţMCINOTIFY z wParam równym MCI NO- TIFY ABORTED. Dochodzi do tego wtedy, gdy zostanie kliknięty przycisk Pau- se albo jeden z dwóch przycisków End. RECORD2 może ignorować ten przypa- dek, ponieważ już i tak obsługuje poprawnie powyższe przyciski. Po zakończe- niu odtwarzarua program otrzymuje komunikat MM MCINOTIFY z wParam rów- nym MCI NOTIFY SUCCESSFUL. W celu obsłużera tego przypadu procedura okna wysyła do siebie komunikat WMţCOMMAND z wParam równym IDCţPLAY END, aby zasymulować kliknięcie przez użytkownika przycisku End. Procedura okna odpowiada normalnie, czyli zatrzymujšc odtwarzanie i zamy- kajšc urzšdzenie. Jeżeli podczas rejestrowarua zabraknie miejsca na dysku (w pliku tymczasowym), wówczas program otrzymuje komunikat MM MCINOTIFY z wParam równym MCI NOTIFYţSUCCESSFUL (nie nazwałbym tego rejestrowaniem zakończonym sukcesem, ale tak się ów identyfikator nazywa). Procedura okna odpowiada wysłaniem do siebie komunikatu WM_COMMAND z wParam równym IDC RE- CORD END. Procedura okna zachowuje się jak zwykle: zatrzymuje rejestrowa- nie, zapisuje plik i zamyka urzšdzenie. Rozwišzanie ze znakowymi poleceniami MCI Kiedyœ interfejs multimediów Windows zawierał funkcję o nazwie mciExecute, która miała następujšcš składnię: bSuccess = mciExecute (szCommand); Jej jedynym argumentem był łańcuch znaków tworzšcy polecenie MCI. Funkcja zwracała wartoœć logicznš: niezerowš, jeżeli zakończyła się powodzeniem, lub zerowš w przeciwnym razie. Funkcja mciExecute była odpowiednikiem wywoła- nia mciSendString (opartego na łańcuchach znaków funkcji MCI zastosowanej w programie TESTMCI) z wartoœciš NULL lub zero dla trzech ostatnich argumen- tów. Gdyby pojawiły się błędy, należało użyć najpierw funkcji mciGetErrorString, a potem MessageBox. Choć funkcja mciExecute nie należy już do API, podobnej funkcji użyłem w pro- gramie RECORD3 - trzeciej wersji magnetofonu cyfrowego. Listing programu znajduje się na rysunku 22-5. RECORD3, podobnie jak RECORD2, używa plików RECORD.RC i RESOURCE.H dołšczonych do programu RECORDl. Rozdział 22: Dtwięk i muryka ţ 177 RECORD3.C /* RECORD3.C - Cyfrowy rejestrator dŸwięku (c) Charles Petzold, 1998 */ ţţinclude 4ţinclude "..\\recordl\\resource.h" BOOL CALLBACK DlgProc (HWND, UINT, WPARAM, LPARAM) ; TCHAR szAppName [] = TEXT ("Record3") ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) ( if (-1 = DialogBox (hInstance, TEXT ("Record"), NULL, DlgProc)) ( MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MBţICONERROR) ; ) return 0 ; BOOL mciExecute (LPCTSTR szCommand) f MCIERROR error ; TCHAR szErrorStr [1024] ; if (error = mciSendString (szCommand, NULL, 0, NULL)) ( mciGetErrorString (error, szErrorStr, sizeof (szErrorStr) / sizeof (TCHAR)) ; MessageBeep (MB_ICONEXCLAMATION) ; MessageBox (NULL, szErrorStr, TEXT ("MCI Error"), MB OK ţ MB-ICONEXCLAMATION) ; ) return error == 0 ; BOOL CALLBACK DlgProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) static BOOL bRecording, bPlaying, bPaused ; switch (message) ( case WM COMMAND: switch (wParam) case IDCţRECORDţBEG: // usuwanie istniejšcego pliku wave DeleteFile (TEXT ("record3.wav")) ; // otwarcie urzšdzenia waveform audio i wlšczenie rejestrowania 1178 Częœć Iil: Zagadnienia zaawansowane (cigg dalszy ze strony 1177) if (!mciExecute (TEXT ("open new type waveaudio alias mysound" Ż ) return TRUE ; mciExecute (TEXT ("record mysound")) ; // blokowanie i odblokowywanie przycisków EnableWindow (GetDlgItem (hwnd, IDC_RECORD_BEG), FALSE); EnableWindow (GetDlgItem (hwnd, IDC_RECORD_END), TRUE> ; EnableWindow (GetDlgItem (hwnd, IDC_PLAY_BEG), FALSE); EnableWindow (GetDlgItem (hwnd, IDC_PLAY_PAUSE), FALSE); EnableWindow (GetOlgItem (hwnd, IDC_PLAY_END), FALSE); SetFocus (GetDlg,Item (hwnd, IDCţRECORDţEND)) ; bRecording = TRUE ; return TRUE ; case IDC_RECORD_END: // zatrzymaj, zapisz i zamknij mciExecute (TEXT ("stop mysound")) ; mciExecute (TEXT ("save mysound record3.wav")) ; mciExecute (TEXT ("close mysound")) ; // blokowanie i odblokowywanie przycisków EnableWindow (GetDlgItem (hwnd, IDC_RECORD_BEG), TRUE) ; EnableWindow (GetDlgItem (hwnd, IDC_RECORD_END), FALSE); EnableWindow (GetDlgItem (hwnd, IDC_PLAY_BEG), TRUE) ; EnableWindow (GetDlgItem (hwnd, IDC_PLAY_PAUSE), FALSE); EnableWindow (GetDlgItem (hwnd, IDCţPLAY_END), FALSE); SetFocus (GetOlgItem (hwnd, IDCţPLAY BEG)) ; bRecording = FALSE ; return TRUE ; case IDC_PLAY_BEG: // otwarcie urzddzenia waveform audio i włšczenie odtwarzania if (!mciExecute (TEXT ("open record3.wav alias mysound"))) return TRUE ; mciExecute (TEXT ("play mysound")) ; // blokowanie i odblokowywanie przycisków EnableWindow (GetDlgItem (hwnd, IDC_RECORD_BEG), FALSE); EnableWindow (GetDlgItem (hwnd, IDC_RECORD_END), FALSE); EnableWindow (GetDlgItem (hwnd, IDC_PLAY_BEG), FALSE); EnableWindow (GetDlgItem (hwnd, IDC_PLAY_PAUSE), TRUE) ; , EnableWindow (GetDlgItem (hwnd, IDC_PLAY_END), TRUE) ; SetFocus (GetDlgltem (hwnd; IDCţPLAYţEND)) ; bPlaying = TRUE ; return TRUE ; Rozdział 22: DŸwięk i muryka 1179 case IDCţPLAY PAUSE: if (!bPaused) // zatrzymanie odtwarzania ( mciExecute (TEXT ("pause mysound")) ; SetDlgItemText (hwnd, IDCţPLAYţPAUSE, TEXT ("Resume")) ; bPaused = TRUE ; ) else // ponowne rozpoczęcie odtwarzania ( mciExecute (TEXT ("play mysound")) ; SetDlgItemText (hwnd, IDCţPLAY PAUSE, TEXT ("Pause")) ; bPaused = FALSE ; 1 return TRUE ; case IDCţPLAY END: // zatrzymanie i zamknięcie mciExecute (TEXT ("stop mysound")) ; mciExecute (TEXT ("close mysound")) ; // blokowanie i odblokowywanie przycisków EnableWindow (GetDlgItem (hwnd, IDC_RECORD_BEG), TRUE) ; EnableWindow (GetOlgItem (hwnd, IDC_RECORD_END), FALSE); EnableWindow (GetDlgItem (hwnd, IDC_PLAY BEG), TRUE) ; EnableWindow (GetDlgItem (hwnd, IDC_PLAY PAUSE), FALSE); EnableWindow (GetOlgItem (hwnd, IDC_PLAY_END), FALSE); SetFocus (GetDlgItem (hwnd, IDC PLAY BEG)) ; bPlaying = FALSE ; bPaused = FALSE ; return TRUE ; ) break ; case WM SYSCOMMAND: switch (wParam) ( case SC_CLOSE: if (bRecording) SendMessage (hwnd, WM COMMAND, IDCţRECORD END, OL); if (bPlaying) SendMessage (hwnd, WM COMMAND, IDCţPLAYţEND, OL) ; EndDialog (hwnd, 0) ; return TRUE ; ) break ; ) return FALSE ; ) Rysunek 22ůţ. Program RECORD3 1180 Częœć III: Zagadnienia zaawansowane Kiedy zaczniesz analizować i porównywać komunikatowe i znakowe interfejsy MCI, zauważysz, że sš bardzo podobne. Nietrudno zgadnšć, że MCI przekłada polecenia znakowe na ich odpowiedniki w postaci komunikatów i struktur da- nych. W programie RECORD3 można było zastosować komunikaty MMţMCI- NOTIFY, podobnie jak w RECORD2, ale tak się nie stało - ze względu na funk- cję mciExecute. W konsekwencji program nie potrafi rozpoznać końca odtwarza- nego pliku, tak więc przyciski nie sš automatycznie blokowane/odblokowywa- ne po osišgruęciu końca pliku i trzeba za każdym razem ręcznie wskazać koniec odtwarzania, klikajšc przycisk End. W poleceniu MCI open występuje dyrektywa alias. Dzięki niej wszystkie kolejne polecenia MCI będš się odnosiły do urzšdzenia za pomocš zdefiniowanego pseu- donimu. Format pliku wave Jeżeli przyjrzeć się nieskompresowanym (czyli PCM-owym) plikom WAV w po- staci szesnastkowej, można łatwo stwierdzić, że majš one format przedstawiony poniżej. Przesunięcie Liczba bajtów Dane 0000 4 "RIFF" 0004 4 rozmiar danych opisujšcych kształt fali (rozmiar pliku minus 8) 0008 4 "WAVE" OOOC 4 "fmt" 0010 4 rozmiar częœci opisujšcej format (16 bajtów) 0014 2 wf.wFormatTag = WAVE FORMAT PCM = 1 0016 2 wf.nChannels 0018 4 wf.nSamplesPerSec OOlC 4 wf.nAvgBytesPerSec 0020 2 wf.nBlockAlign 0022 2 wf.wBitsPerSample 0024 4 "data" 0028 4 rozmiar danych dŸwiękowych 002C dane dŸwiękowe Przedstawiony format jest przykładem ogólniejszego formatu o nazwie RIFF (ang. Resource Interchange File Format-format pliku wymiany zasobów), który miał się stać formatem obejmujšcym wszystkie typy plików multimedialnych. Jest to for- mat pliku z tagami: dane w nim sš ułożone w kawałki identyfikowane przez 4- znakowš nazwę kawałka (zapisanš w ASCá) oraz 4-bajtowy (32-bitowy) rozmiar kawałka. Wartoœć rozmiaru kawałka nie uwzglgdrua 8 bajtów niezbędnych do zapisania nazwy i rozmiaru. Plik wave zaczyna się od napisu "RIFF", który identyfikuje plik jako typu RIFF. Po napisie znajduje się 32-bitowy rozmiar kawałka, stanowišcy rozmiar pozosta- łej częœci pliku, czyli rozmiar pliku minus 8 bajtów. Rozdział 22: DŸwięk i muzyka 1181 Dane tworzšce kawałek zaczyn„jš się napisem "WAVE", identyfikujšcym je jako dane typu waveform audio. Następnie w pliku znajduje się łańcuch "fmt " (zwróć uwagę na dodatkowš spację, dzięki której napis ma 4 znaki), identyfikujšcy for- mat podkawałka zawierajšcego dane waveform audio. Po łańcuchu "fmt " wy- stępuje rozmiar, który w tym wypadku wynosi 16 bajtów. Pierwszych 16 bajtów struktury WAVEFORMATEX albo (jak pierwotrue zdefiniowano) struktury PCM- WAVEFORMAT zawierajšcej strukturę WAVEFORMAT stanowiš informacje o for- macie Pole nChannels ma wartoœć 1 lub 2, odpowiednio dla dŸwięku mono lub stereo. Pole nSamplesPerSec to liczba próbek na sekundę; jego standardowe wartoœci to 11 025, 22 050 i 44 100. Pole nAvgBytesPerSec zawiera iloczyn częstotliwoœci pró- bek, liczby kanałów i rozmiaru pojedynczej próbki w bitach, podzielonego przez 8 i zaokršglonego w górę. Standardowe rozmiary próbek to 8 i 16 bitów. Pole nBlockAlign jest liczbš kanałów przemnożonš przez rozmiar próbki w bitach, podzielonš przez 8 i zaokršglonš w górę. Opis formatu kończy się polem wBit- sPerSample, zawierajšcym liczbę kanałów razy rozmiar próbki w bitach. Po danych definiujšcych format znajduje się łańcuch "data", po którym wystę- pujš: 32-bitowy rozmiar danych oraz same dane dŸwiękowe. Sš one kolejnymi wartoœciami próbek w tym samym formacie, jaki stosowany jest w niskopozio- mowych funkcjach waveform audio. Jeżeli próbki majš rozmiar 8 lub mniej bi- tów, każda składa się z 1 bajtu dla dŸwięku monofonicznego lub 2 bajtów dla stereofonicznego. Jeżeli próbki majš rozmiar między 9 a 16 bitów, każda składa sie z 2 bajtów dla dŸwięku mońofonicznego lub 4 bajtów dla stereofonicznego. Dla danych stereofonicznych każda próbka zawiera najpierw wartoœć głoœnoœci lewego kanału, a potem prawego. Jeżeli rozmiar próbki wynosi 8 bitów lub mniej, próbka jest interpretowana jako liczba bez znaku. Na przykład przy 8-bitowych próbkach ciszę reprezentuje cišg 0x80 bajtów. Próbka o wielkoœci 9 lub więcej bitów jest interpretowana jako licz- ba ze znakiem, tak więc ciszę reprezentuje cišg zer. Jedna z ważniejszych zasad zwišzanych z odczytem plików z tagami nakazuje ignorowanie tych kawałków, na które urzšdzenie nie jest przygotowane. Choć plik waveform audio wymaga podkawałków "fmt " i "data" (w takiej kolejno- œci), może również zawierać podkawałki innego typu. W szczególnoœci plik wa- veform audio może zawierać podkawałki o nazwach "INFO", a także zawarte w tych podkawałkach podpodkawałki, które zapewniajš informacje o pliku wa- veform audio. Eksperymenty z syntezš addytywnš Od wielu lat - że wspomnę choćby Pitagorasa - ludzie próbowali analizować dŸwięki muzyczne. Poczštkowo wydaje się to proste, ale z czasem zaczyna się komplikować. Wybacz mi, drogi Czytelniku, ale powtórzę kilka wiadomoœci o dŸwięku, które już podałem wczeœniej. DŸwięki muzyczne, wyjšwszy instrumenty perkusyjne, majš okreœlonš wysokoœć (czytaj: częstotliwoœć). Częstotliwoœć ta może należeć do przedziału od 20 Hz do 20 000 Hz, czyli przedziału uznawanego za spektrum percepcji ludzkiego ucha. 1182 Częœć III: Zagadnienia zaawansowane DŸwięki pianina majš na przykład częstotliwoœci z od 27,5 Hz do 4186 Hz. In- nym parametrem charakteryzujšcym dŸwięk jest głoœnoœć (czytaj: natężenie). Odpowiada to amplitudzie fali dŸwiękowej. Zmianę amplitudy mierzy się w de- cybelach. No, na razie idzie nam całkiem, całkiem. Teraz parametr nazywany "barwš". Prosto mówišc, barwa to cecha dŸwięku, która pozwala odróżnić pianino od skrzypiec i tršbki grajšcych tę samš nutę z tš samš głoœnoœciš. Francuski matematyk Fourier odkrył, że przebiegi okresowe - niezależnie od złożonoœci - można przedstawiać w postaci sumy fal sinusoidalnych o często- tliwoœciach stanowišcych całkowite wielokrotnoœci częstotliwoœci podstawowej. Częstotliwoœć podstawowa jest zwana również pierwszš harmonicznš. Pierwszy górny ton składowy, zwany rówrueż drugš harmonicznš, ma częstotliwoœć dwa razy większš niż podstawowa; drugi górny ton składowy, zwany trzeciš harmo- nicznš, ma z kolei częstotliwoœć trzy razy większš niż podstawowa itd. O kształ- cie fali okresowej decyduje wzajemny stosunek poszczególnych harmonicznych. Przebieg prostokštny można przedstawić na przykład jako sumę fal sinusoidal- nych, których parzyste harmoniczne (2, 4, 6 itd.) majš amplitudy zerowe, a har- moniczne nieparzyste (1, 3, 5 itd.) - amplitudy o proporcjach 1, 1/3, 1/5 itd. Prze- bieg piłokształtny można przedstawić w postaci sumy harmonicznych o ampli- tudach majšcych proporcje 1, 1/2, 1/3, 1/4 itd. Dla niemieckiego naukowca Hermanna Helinholtza (1821-1894) stało się to klu- czem do zrozumienia istoty barwy. W swojej klasycznej ksišżce pt. On the Sensa- tions of Tone (z 1885 roku, przedruk Dover Press w 1954 roku) Helmholtz postu- lował, że ucho wespół z mózgiem dokonuje rozkładu dŸwięków na składowe fale sinusoidalne, przekształcajšc ich względne natężenia na to, co odczuwamy jako barwę dŸwięku. Niestety, okazało się to nie do końca shzszne. Syntezatory elektroniczne zdobyły rozgłos w roku 1968 wraz z wydaniem płyty Wendy Carlos pt. Switched on Bach. Dostępne wtedy instrumenty elektroniczne (takie jak Moog) były syntezatorami analogowymi. Zastosowane w nich obwody analogowe generowały sygnały dŸwiękowe o kilku typowych kształtach: prosto- kštne, trójkštne i piłokształtne. Aby upodobnić te sygnały do dŸwięków rzeczy- wistych instrumentów muzycznych, poddawano je modulacji na przestrzeni trwa- nia każdej nuty. Natężenie dŸwięku sygnału zmieniane było zgodnie z obwied- niš. Na poczštku trwania nuty amplituda zaczyna się zmieniać od zera i narasta do pewnego poziomu (w przypadku gitary ów wzrost jest gwałtowny, w przy- padku skrzypiec - łagodny). Ten fragment zmian amplitudy nazywa się atakiem (ang. attack). Amplituda pozostaje następnie na pewnym stałym poziomie. Ten fragment jej zmian nazywa się podtrzymaruem (ang. sustain). Następnie, kiedy nuta się kończy, amplituda spada znów do zera. Ten fragment nazywa się wy- brzmiewaniem (ang. reiease). Sygnały dŸwiękowe przepuszcza się przez filtry tłumišce niektóre harmoniczne i zmieruajšce proste przeiţiegi w bardziej złożone, a więc muzycznie ciekawsze. Częstotliwoœciami wycinanymi przez te filtry można sterować za pomocš abwied- ni, więc harmoniczna zawartoœć dŸwięku może się zmieniać w trakcie trwania nuty. Rozdział 22: DŸwięk i muryka 1183 Porueważ syntezatory analogowe zaczynajš generowanie dŸwięku od bogatych harmonicznie sygnałów, a niektóre harmoniczne sš tłumione za pomocš filtrów, taki sposób otrzymywania dŸwięków nazwano syntezš substrakcyjnš. Już za czasów popularnoœci syntezy substrakcyjnej wiele osób wiele sobie obie- cywało po zapowiadajšcej się dobrze syntezie addytywnej. W syntezie addytywnej wychodzi się od pewnej liczby generatorów sinusoidalnych o częstotliwoœciach zwielokrotnianych w układach mnożšcych częstotliwoœć. Każ- dy generator ma za zadanie dostarczać sygnał odpowiadajšcy wybranej harmo- nicznej tonu podstawowego. Amplituda każdej harmonicznej może być zmienia- na niezależnie przez układ formowania obwiedni. Synteza addytywna nie za bar- dzo nadawała się do zrealizowania w technice analogowej, ponieważ dla każdej nuty potrzebnych jest od 8 do 24 generatorów sinusoidalnych, a ich częstotliwoœci pokrewne musiałyby być niezwykle precyzyjnie dostrojone. Analogowe generato- ry sinusoidalne sš jednak bardzo niestabilne i podatne na dryft częstotliwoœci. W syntezatorach cyfrowych (w których generowanie sygnałów odbywa się cy- frowo, przez odczyt tablic danych) i sygnałach wytwarzanych cyfrowo dryft czę- stotliwoœci nie stanowi problemu, synteza addytywna jest więc możliwa. Oto ogólna zasada postępowania: należy zarejestrować rzeczywisty dŸwięk i prze- prowadzić jego rozkład na harmoniczne za pomocš analizy Fouriera. Następnie można wyznaczyć względne poziomy głoœnoœci poszczególnych harmonicznych i odtworzyć cyfrowo dŸwięk za pomocš kilku sygnałów sinusoidalnych. Kiedy zaczęto przeprowadzać eksperymenty z analizš Fouriera rzeczywistych dŸwięków i próby odtworzenia dŸwięków wyjœciowych z kilku fal sinusoidal- nych, okazało się, że barwa to niedokładnie to, o czym myœlał Helmholtz. Poważnym problemem jest między innymi to, że zależnoœci natężeń harmonicz- nych w rzeczywistych dŸwiękach nie sš całkowitoliczbowe. Okazuje się nawet, że samo pojęcie harmonicznej nie jest odpowiednie w przypadku rzeczywistych dŸwięków. Różne sygnały składowe sš aharmoniczne i powinny być nazywane po prostu składowymi. Odkryto, że kluczem do uzyskania naturalnie brzmišcej barwy dŸwięku jest ahar- monicznoœć składowych. Œ isła harmonicznoœć daje w rezultacie barwy brzmiš- ce "elektronicznie". Każda składowa zmierua amplitudę i częstotliwoœć w trak- cie brzmienia jednej nuty. Zależnoœci częstotliwoœci i arnplitudy dla poszczegól- nych składowych sš różne w przypadku dŸwięków o różnych wysokoœciach w ob- rębie tego samego instrumentu. Do najbardziej złożonych zjawisk w brzmieniu rzeczywistego irţstrumentu dochodzi podczas fazy ataku, kiedy pojawia się wie- le aharmonieznych. Odkryto, że te złożone zależnoœci w fazie ataku decydujš, jak ludzkie ucho odbiera barwę. Krótko mówišc, barwa rzeczywistego instrumentu muzycznego jest o wiele bar- dziej złożona, niż się ludziom pierwotrue wydawało. Pomysł analizy dŸwięków muzycznych i stosunkowo niewyszukany sposób sterowania amplitudami oraz częstotliwoœciami składowych za pomocš prostych obwiedni nie był zbyt prak- tyczny. fţiţka analiz instrumexttów muzycznych opublikowano we wczesnych wydaniach (z 1977 i 1978 rokuj czasopisma "Computer Music Journat" (wtedy wydawanego 1184 Częœć III: Zagadnienia zaawansowane przez People's Computer Company, a obecnie przez MIT Press). W trzyczęœcio- wej ser pt. Lexicon of Analyzed Tones, autorstwa Jamesa A. Moorera, Johna Greya i Johna Strawna, zaprezentowano amplitudy i częstotliwoœci składowych poje- dynczej nuty (trwajšcej krócej niż jednš sekundę) zagranej na skrzypcach, oboju, klarnecie i tršbce. Rozpatrywano nutę Es powyżej œrodkowego C. W przypadku skrzypiec badano 20 składowych, w przypadku oboju i klametu 21, a w przy- padku tršbki 12. W tomie á w numerze 2 (z wrzeœnia 1978 roku) wspomnianego czasopisma można znaleŸć aproksymacje odcinkami różnych obwiedni częstotli- woœciowych i amplitudowych dla oboju, klarnetu i tršbki. Majšc do dyspozycji obsługę urzšdzeń waveform audio w Windows, możemy po pros#u wpisać zaczerpruęte z czasopisma dane do programu, wygenerować kilka sygnałów sinusoidalnych dla poszczególnych składowych, dodać je do sie- bie, wysłać do urzšdzerua waveform audio (karty dŸwiękowej) i odtworzyć w ten sposób barwę zarejestrowanš kiedyœ, 20 lat temu. Pomysł ten znalazł implemen- tację w postaci programu ADDSYNT'H (ang. additive synthesis), przedstawionego na rysunku 22-6. ADDSYNTH.C /* ADDSYNTH.C - Generowanie dŸwięku za pomocš syntezy addytywnej (c) Charles Petzold, 1998 */ itinclude itinclude itinclude "addsynth.h" include "resource.h" itdefine ID_TIMER i ţţdefine SAMPLE_RATE 22050 ţţdefine MAXţPARTIALS 21 ţţdefine PI 3.14159 BOOL CALLBACK DlgProc (HWND, UINT, WPARAM, LPARAM) ; TCHAR szAppName [] = TEXT ("AddSynth") ; // generator przebiegów sinusoidalnych // - double SineGenerator (double dFreq, double * pdAngle) ( double dAmp ; dAmp = sin (* pdAngle) ; * pdAngle += 2 * PI * dFreq / SAMPLEţRATE ; if (* pdAngle >= 2 * PI) * pdAngle -= 2 * PI ; return dAmp ; Rozdziat 22: DŸwięk i muryka 1185 // // zapełnianie bufora przebiegiem złożonym // VOID FillBuffer (INS ins, PBYTE pBuffer, int iNumSamples) ( static double dAngle CMAX_PARTIALS] ; double dAmp, dFrq, dComp, dFrac ; int i, iPrt, iMsecTime, iCompMaxAmp, iMaxAmp, iSmp ; // wyliczanie maksymalnej amplitudy przebiegu złóżonego iCompMaxAmp = 0 ; for (iPrt = 0 ; iPrt < ins.iNumPartials ; iPrt++) iMaxAmp = 0 ; for (i = 0 ; i < ins.pprt[iPrt].iNumAmp ; i++) iMaxAmp = max (iMaxAmp, ins.pprt[iPrt].pEnvAmp[i].iValue) ; iCompMaxAmp += iMaxAmp ; ) // pętla przez każdd próbkę for (iSmp = 0 ; iSmp < iNumSamples ; iSmp++) ( dComp = 0 ; iMsecTime = (int) (1000 * iSmp / SAMPLEţRATE) ; // pętla przez każdš składowd for (iPrt = 0 ; iPrt < ins.iNumParti„ls ; iPrt++) ( dAmp = 0 ; dFrq = 0 ; for (i = 0 ; i < ins.pprtCiPrt].iNumAmp - 1 ; i++) ( if (iMsecTime >= ins.pprt[iPrt].pEnvAmpCi ].iTime &R iMsecTime <= ins.pprt[iPrt].pEnvAmp[i+1].iTime) ( dFrac = (double) (iMsecTime - ins.pprt(iPrt].pEnvAmpCi ].iTime) / (ins.pprtCiPrt].pEnvAmpCi+1].iTime - ins.pprt[iPrt].pEnvAmp[i ].iTime) ; dAmp = dFrac * ins.pprt[iPrt].pEnvAmp[i+1].iValue + (1-dFrac) * ins.pprtCiPrt].pEnvAmpCi ].iValue ; ) ) break ; j for (i = 0 ; i < ins.pprt[iPrt].iNumFrq - 1 ; i++) ( if (iMsecTime >= ins.pprtCiPrt].pEnvFrq[i ].iTime && Częœć III: Zagadnienia zaawansowane (cigg dalszy ze strony 1185) iMsecTime <= ins.pprt[iPrt].pEnvFrq[i+1].iTime) ( dFrac = (double) (iMsecTime - ins.pprt[iPrt].pEnvFrq[i ].iTime) / (ins.pprt[iPrt].pEnvFrq[i+1].iTime - ins.pprtCiPrt].pEnvFrqCi ].iTime) ; dFrq = dFrac * ins.pprtCiPrt].pEnvFrq[i+1].iValue + (1-dFrac) * ins.pprtCiPrt].pEnvFrq[i ].iValue ; break ; ) dComp += dAmp * SineGenerator (dFrq, dAngle + iPrt) ; ) pBuffer[iSmp] = (BYTE) (127 + 127 * dComp / iCompMaxAmp) ; 1 // // utworzenie pliku wave // BOOL MakeWaveFile (INS ins, TCHAR * szFileName) ( DWORD dwWritten ; HANDLE hFile ; int iChunkSize, iPcmSize, iNumSamples ; PBYTE pBuffer ; WAVEFORMATEX waveform ; hFile = CreateFile (szFileName, GENERIC WRITE, 0, NULL, CREATĘ ALWAYS, FILĘ ATTRIBUTĘ NORMAL, NULL) ; if (hFile == NULL) return FALSE ; iNumSamples = ((long) ins.iMsecTime * SAMPLEţRATE / 1000 + 1) / 2 * 2 ; iPcmSize = sizeof (PCMWAVEFORMAT) ; iChunkSize = 12 + iPcmSize + g + iNumSamples ; if (NULL ţ (pBuffer = malloc (iNumSamples))) . CloseHandle (hFile) return FALSE ; l FillBuffer (ins, pBuffer, iNumSamples) ; waveform.wFormatTag = WAVEţFORMATţPCM ; waveform.nChannels = 1 ; waveform.nSamplesPerSec = SAMPLE_RATE ; waveform.nAvgBytesPerSec = SAMPLEţRATE ; waveform.nBlockAlign = 1 ; waveform.wBitsPerSample = 8 ; waveform.cbSize = 0 ; ' ţ1ţ1 Rozdział 22: DŸwięk i muryka 1187 WriteFile (hFile, "RIFF", 4, &dwWritten, NULL) ; WriteFile (hFile, &iChunkSize, 4, &dwWritten, NULL) ; WriteFile (hFile, "WAVEfmt ", 8, &dwWritten, NULL) ; WriteFile (hFile, &iPcmSize, 4, &dwWritten, NULL) ; WriteFile fhFile, &waveform, sizeof (WAVEFORMATEX) - 2, &dwWritten, NULL) ; WriteFile (hFile, "data", 4, &dwWritten, NULL) ; WriteFile (hFile, &iNumSamples, 4, &dwWritten, NULL) ; WriteFile (hFile, pBuffer, iNumSamples, &dwWritten, NULL) ; CloseHandle (hFile) ; free (pBuffer) ; if ((int) dwWritten != iNumSamples) ( DeleteFile (szFileName) ; return FALSE ; ) return TRUE ; void TestAndCreateFile (HWND hwnd, INS ins, TCHAR * szFileName, int idButton) ( TCHAR szMessage [64] ; ! if (-1 != GetFileAttributes (szFileName)) EnableWindow (GetDlgltem (hwnd, idButton), TRUE) ; else ( if (MakeWaveFile (ins, szFileName)) EnableWindow (GetDlgItem (hwnd, idButton), TRUE) ; else ( wsprintf (szMessage, TEXT ("Could not create %x."), szFileName) ; MessageBeep (MB_ICONEXCLAMATION) ; MessageBox (hwnd, szMessage, szAppName, , MB OK ţ MBţICONEXCLAMATION) ; 1 l int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, i PSTR szCmdLine, int iCmdShow) ( if (-1 = DialogBox (hInstance, szAppName, NULL, DlgProc)) , MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MBţICONERROR) ; > return 0 ; BOOL CALLBACK DlgProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) , static TCHAR * szTrum = TEXT ("Trumpet.wav") ; static TCHAR * szOboe = TEXT ("Oboe.wav") ; static TCHAR * szClar = TEXT ("Clarinet.wav") ; switch (message) Częœć III: Zagadnienia zaawansowane (cigg dalszy ze strony 2287) i case WM_INITDIALOG: SetTimer (hwnd, ID TIMER, 1, NULL) ; return TRUE ; case WM_TIMER: KillTimer (hwnd, ID TIMER) ;. SetCursor (LoadCursor (NULL, IDC WAIT)) ; ShowCursor (TRUE) ; TestAndCreateFile (hwnd, insTrum, szTrum, IDC_TRUMPET) ; TestAndCreateFile (hwnd, insOboe, szOboe, IDC_OBOE) ; TestAndCreateFile (hwnd, insClar, szClar, IDC CLARINET) ; SetDlgItemText (hwnd, IDC_TEXT, TEXT (" ")) ; SetFocus (GetDlgItem (hwnd, IDC TRUMPET)) ; ShowCursor (FALSE) ; SetCursor (LoadCursor (NULL, IDC ARROW)) ; return TRUE ; case WM COMMAND: switch (LOWORD (wParam)) f . case IDC_TRUMPET: PlaySound (szTrum, NULL, SND FILENAME ţ SND SYNC) ; return TRUE ; case IDC_OBOE: PlaySound (szOboe, NULL, SNDţFILENAME ţ SND SYNC) ; return TRUE ; case IDC_CLARINET: ` PlaySound (szClar, NULL, SNDţFILENAME ţSND SYNC) ; return TRUE ; . break ; case WM SYSCOMMAND: switch (LOWORD (wParam)) ( case SC_CLOSE: EndDialog (hwnd, 0) ; return TRUE ; 1 break ; t return FALSE ; ? ADDSYNTH.RC (fragmenty) //Microsoft Developer Studio generated resource script. ttinclude "resource.h" Rozdział 22: DŸwięk i muryka ţ 1 gg ţfinclude "afxres.h" ///////////////////////////////////////////////////////////////////////////// // Dialo9 ADDSYNTH DIALOG DISCARDABLE 100, 100, 176, 49 STYLE WS MINIMIZEBOX ţ WSţCAPTION ţ WSţSYSMENU CAPTION "Additive Synthesis" FONT 8, "MS Sans Serif" BEGIN PUSHBUTTON "Trumpet",IDC_TRUMPET.8,8,48,16 PUSHBUTTON "Oboe",IDC_OBOE,64,8,48,16 PUSHBUTTON "Clarinet",IDC CLARINET,l20,8,48,16 LTEXT "Preparing Data... ,IDC TEXT,8,32,100,8 END RESOURCE.H (fragmenty) // Microsoft Developer Studio generated include file. // Used by AddSynth.rc ţţdefine IDC_TRUMPET 1000 ţţdefine IDC_OBOE 1001 ţţdefine IDC_CLARINET 1002 ţţdefine IDCţTEXT 1003 Rysunek 22-6. Program ADDSYNTH Dodatkowy plik o nazwie ADDSYNTH.H nie został tu pokazany, ponieważ za- wiera kilkaset lin nudnych rzeczy. Można go znaleŸć na CD-ROM-ie dołšczo- nym do tej ksišżki. Na poczštku pliku nagłówkowego ADDSYNTH.H zdefinio- wałem trzy struktury wykorzystywane do przechowania parametrów obwiedni. Każda obwiednia amplitudowa i częstotliwoœciowa jest zapisana w postaci tabli- cy struktur typu ENV. Sš to pary liczb składajšce się z wartoœci czasu (w milise- kundach) oraz wartoœci amplitudy (w arbitralnie obranej skali) albo częstotliwo- œci (w okresach na sekundę). Sš to tablice o zmiennej długoœci, wynoszšcej od 6 do 14. Poszczególne wartoœci amplitud i częstotliwoœci majš być połšczone od- cinkami. Każdy z instrumentów został zamodelowany pewnym zestawem składowych (12 w przypadku tršbki, a 21 w przypadku oboju i klarnetu), zapisanym jako tablica struktur typu PRT. Struktura PRT zawiera liczbę punktów tworzšcych obwied- nię amplitudowš lub częstotliwoœciowš oraz wskaŸnik do tablicy ENV. Struktu- ra INS zawiera całkowity czas tonu w milisekundach, liczbę składowych oraz wskaŸnik do tablicy PRT, w której zawarte sš składowe. Interfejs programu ADDSYNTH składa się z trzech przycisków, odpowiednio o nazwach Trumpet, Oboe i Clarinet. Komputery osobiste nie sš jeszcze wystar- czajšco szybkie, aby można było za ich pomocš realizować syntezę addytywnš w czasie rzeczywistym, dlatego też podczas pierwszego uruchomienia progra- mu ADDSYNTH generowane sš pliki z wyliczonymi próbkami (TRUMPET.WAV, OBOE.WAV i CLARINET.WAV). Trzy przyciski interfejsu nie zostanš odbloko- 1190 Częœć III: Zagadnienia zaawansowane wane, dopóki program nie wygeneruje próbek. Kiedy przyciski zostanš odblo- kowane, można za ich pomocš odtworzyć trzy dŸwięki (wykorzystano do tego funkcję PIaySound). Program uruchomiony po raz kolejny sprawdza istnienie pli- ków wave i jeżeli je znajdzie, nie będzie musiał ich tworzyć ponownie. Większa częœć pracy programu ADDSYNTH wykonywana jest w funkcji FillBuf fer, która zaczyna się od wyliczenia maksymalnej amplitudy przebiegu złożone- go. Obliczenie to polega na przejrzeniu w pętli składowych, wyznaczeniu mak- symalnych amplitud wszystkich składowych i dodaniu ich do siebie. Wartoœć ta jest następnie używana do skalowarua próbek do rozmiaru 8 bitów. Następnie wyliczana jest wartoœć każdej próbki. Każda próbka koresponduje z pewnš wartoœciš czasu (w milisekundach) zależnš od częstotliwoœci próbko- wania (w przypadku częstotliwoœci 22,05 kHz każde 22 próbki korespondujš z tš samš liczbš milisekund). W dalszej kolejnoœci funkcja FillBuffer przeglšda w pętli wszystkie składniki. Dla każdej częstotliwoœci i amplitudy znajduje odcinki ob- wiedni korespondujšce z odpowiedniš liczbš milisekund i przeprowadza inter- polację liruowš. Wartoœć częstotliwoœci wraz z kštem fazowym przekazywana jest do funkcji Si- neGenerator. Jak napisałem we wczeœniejszej częœci tego rozdziału, cyfrowe gene- rowanie przebiegów sinusoidalnych wymaga wartoœci kšta fazy zwiększanego zgodnie z wartoœciš częstotliwoœci. Po powrocie z funkcji SineGenerator wartoœć sinusa jest mnożona przez amplitudę składowej i akumulowana. Po dodaniu wszystkich składowych cała próbka jest skalowana do rozmiaru jednego bajtu. Budzenie za pomocš dŸwięków wave Program WAKEUP, przedstawiony na rysunku 22-7, jest jednym z tych progra- mów, których kod Ÿródłowy wyglšda na niedokończony. Okno programu wy- glšda jak okno dialogowe, ale nie ma skryptu zasobów (wiemy już, jak to zrobić), a program używa czegoœ, co wyglšda jak plik wave, ale na dysku nie ma żadne- go pliku wave. Mimo to program wydaje donoœny i dokuczliwy dŸwięk. WA- KEUP jest budzikiem i z pewnoœciš potrafi obudzić. WAKEUP.C /* WAKEUP.C - Zegar z budzikiem (c) Charles Petzold, 1998 */ tţinclude Ilinclude // identyfikatory trzech okien potomnych Ildefine ID_TIMEPICK 0 Ildefine ID_CHECKBOX 1 Ildefine ID-PUSHBTN 2 Rozdział 22: DŸwięk i muryka 1191 // identyfikator zegara ţţdefine ID TIMER 1 // liczba 100-nanosekundowych kroków (tyknięć FILETIME) w jednej godzinie ţţdefine FTTICKSPERHOUR (60 * 60 * (LONGLONG) 10000000) // definicje i struktury dla "pliku" wave ţţdefine SAMPRATE 11025 ţţdefine NUMSAMPS (3 * SAMPRATE) ţţdefine HALFSAMPS (NUMSAMPS / 2) typedef struct ( char chRiffC4] ; DWORD dwRiffSize ; char chWaveC4] ; ł char chFmt C4] ; DWORD dwFmtSize ; PCMWAVEFORMAT pwf ; char chOataC4] ; DWORD dwDataSize ; BYTE byDataCO] ; ) WAVEFORM ; // procedura okna i podklasy LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; LRESULT CALLBACK SubProc (HWND, UINT, WPARAM, LPARAM) ; // adres oryginalnej procedury okna dla okien z podklasami WNDPROC SubbedProc C3] ; ł // bieżdce okno potomne z fokusem wejœciowym HWND hwndFocus ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInst, PSTR szCmdLine, int iCmdShow) ( static TCHAR szAppName C] = TEXT ("WakeUp") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = 0 ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) (1 + COLOR BTNFACE) ; wndclass.lpszMenuName = NULL ; 1192 Częœć III: Zagadnienia zaawansowane (cišg dalszy ze strony 1191) wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) ( MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MBţICONERROR) ; return 0 ; ' 1 hwnd = CreateWindow (szAppName, szAppName, WS OVERLAPPED ( WS_CAPTION WS_SYSMENU ţ WS MINIMIZEBOX, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CWţUSEDEFAULT, NULL, NULL, hInstance, NULL) ; ' ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; . while (GetMessage (&msg, NULL, 0, 0)) ( TranslateMessage (&msg) ; DispatchMessage (&msg) ; return msg.wParam ; ) LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ( static HWND hwndDTP, hwndCheck, hwndPush ; static WAVEFORM waveform = ( "RIFF", NUMSAMPS + 0x24, "WAVE", "fmt ", sizeof (PCMWAVEFORMAT), l, l, SAMPRATE, SAMPRATE, 1, 8, "data", NUMSAMPS 1 ; static WAVEFORM * pwaveform ; FILETIME ft ; HINSTANCE hInstance ; INITCOMMONCONTROLSEX icex ; int i, cxChar, cyChar ; LARGE_INTEGER li ; SYSTEMTIME st ; switch (message) ( case WM_CREATE: // inicjacje hInstance = (HINSTANCE) GetWindowLong (hwnd, GWL HINSTANCE) ; icex.dwSize = sizeof (icex) ; icex.dwICC = ICC_DATE_CLASSES ; InitCommonControlsEx (&icex) ; // tworzenie pliku wave ze zmiennymi przebiegami prostokdtnymi pwaveform = malloc (sizeof (WAVEFORM) + NUMSAMPS) ; * pwaveform = waveform ; for (i = 0 ; i < HALFSAMPS ; i++) Rozdział 22: DŸwięk i muryka 1193 if (i % 600 < 300) if (i % 16 < 8) pwaveform->byDataCi] = 25 ; else else pwaveform->byDataCi] = 230 ; if (i % 8 < 4) pwaveform->byDataCi] = 25 ; else pwaveform->byDataCi] = 230 ; // pobranie rozmiaru znaków i okreœlenie rozmiaru okna cxChar = LOWORD (GetDialogBaseUnits ()) ; cyChar = HIWORD (GetDialogBaseUnits (>) ; SetWindowPos (hwnd, NULL, 0, 0, 42 * cxChar, 10 * cyChar / 3 + 2 * GetSystemMetrics (SM_CYBORDER) + GetSystemMetrics (SM_CYCAPTION), SWP NOMOVE ţ SWP NOZORDER ţ SWP NOACTIVATE) ; // utworzenie trzech okien potomnych hwndDTP = CreateWindow (DATETIMEPICK_CLASS, TEXT (""), WSţBORDER ţ WS_CHILD ţ WSţVISIBLE ţ DTS_TIMEFORMAT, 2 * cxChar, cyChar, 12 * cxChar, 4 * cyChar / 3, hwnd, (HMENU) ID TIMEPICK, hInstance, NULL) ; hwndCheck = CreateWindow (TEXT ("Button"), TEXT ("Set Alarm"), WS_CHILD ţ WSţVISIBLE ţ BS AUTOCHECKBOX, 16 * cxChar, cyChar, 12 * cxChar, 4 * cyChar / 3, hwnd, (HMENU) ID CHECKBOX, hInstance, NULL) ; hwndPush = CreateWindow (TEXT ("Button"), TEXT ("Turn Off"), WS_CHILD ţ WSţVISIBLE ţ BSţPUSHBUTTON ţ WS_DISABLED, 28 * cxChar. cyChar, 12 * cxChar, 4 * cyChar / 3, hwnd, (HMENU) ID PUSHBTN, hInstance, NULL) ; hwndFocus = hwndDTP ; // podklasy trzech okien potomnych SubbedProc CID_TIMEPICK] = (WNDPROC) SetWindowLong (hwndDTP, GWL WNDPROC, (LONG) SubProc) ; SubbedProc CID CHECKBOX] = (WNDPROC) SetWindowLong (hwndCheck, GWL WNDPROC, (LONG) SubProc); SubbedProc CID_PUSHBTN] = (WNDPROC) SetWindowLong (hwndPush, GWL WNDPROC, (LONG) SubProc) ; // ustawienie kontrolki daty i czasu na bieżšcy czas // plus 9 godzin, z zaokršgleniem w dół do pełnej godziny GetLocalTime (&st) ; SystemTimeToFileTime (&st, &ft) ; li = * (LARGEţINTEGER *) &ft ; li.OuadPart += 9 * FTTICKSPERHOUR ; ft = * (FILETIME *) &li ; Częœć III: Zagadnienia zaawansowane (cišg dalszy ze strony 1193) ; FileTimeToSystemTime (&ft, &st) ; st.wMinute = st.wSecond = st.wMilliseconds = 0 ; SendMessage (hwndDTP, DTMţSETSYSTEMTIME. 0, (LPARAM) &st) ; return 0 ; case WM_SETFOCUS: 1 SetFocus (hwndFocus) ; return 0 ; case WM COMMAND: switch (LOWORD (wParam)) // identyfikator kontrolki ( case ID CHECKBOX: // Kiedy użytkownik zaznaczy pole "Set Alarm", pobierz ! // czas z kontrolki daty i czasu, po czym odejmij od // bieżšcego czasu komputera. if (SendMessage (hwndCheck, BM GETCHECK, 0, 0)) ů' ( SendMessage (hwndDTP, DTMţGETSYSTEMTIME, 0, (LPARAM) &st) ; SystemTimeToFileTime (&st, &ft) ; li = * (LARGEţINTEGER *) &ft ; GetLocalTime (&st) ; SystemTimeToFileTime (&st, &ft) ; li.OuadPart -= ((LARGE INTEGER *) &ft)->OuadPart ; // SprawdŸ, czy czas w przedziale 0-24 godzin! // Te male poprawki pozwalajš calkowicie ignorować // datę struktury SYSTEMTIME. while (li.OuadPart < 0) li.OuadPart += 24 * FTTICKSPERHOUR ; li.OuadPart ţ6= 24 * FTTICKSPERHOUR ; // ustaw timer! (do zobaczenia rano). l else SetTimer (hwnd, IDţTIMER, (int) (li.OuadPart / 10000), 0) ; // Jeżeli zostanie zlikwidowane zaznaczenie pola, // zabij proces zegara KillTimer (hwnd, ID TIMER) ; return 0 ; // Przycisk "Turn Off" wylšcza dzwonienie budzika, powoduje // zlikwidowanie zaznaczenia pola "Set Alarm" i // zablokowanie przycisku. case ID_PUSHBTN: PlaySound (NULL, NULL, 0> ; SendMessage (hwndCheck, BM_SETCHECK, 0, 0) ; EnableWindow (hwndDTP, TRUE) ; Rozdział 22: DŸwięk i muryka 1195 EnableWindow (hwndCheck, TRUE) ; EnableWindow (hwndPush, FALSE) ; SetFocus (hwndDTP) ; return 0 ; ) return 0 ; // Komunikat WM_NOTIFY pochodzi od kontrolki daty/czasu. // Jeżeli użytkownik zaznaczy "Set Alarm" i zacznie // zmieniać czas alarmu, może pojawić się niezgodnoœć między // czasem wyœwietlanym i ustawionym w timerze. Program likwiduje // więc zaznaczenie pola "Set Alarm" i zabija timer. case WM NOTIFY: switch (wParam) // identyfikator kontrolki ( case ID TIMEPICK: switch (((NMHDR *) lParam)->code) // kod potwierdz. ( case DTN_DATETIMECHANGE: if (SendMessage (hwndCheck, BM GETCHECK, 0, 0)) ( KillTimer (hwnd, ID_TIMER) ; SendMessage (hwndCheck, BM SETCHECK, 0, 0) ; ) return 0 ; ) return 0 ; // Komunikat WMţCOMMAND pochodzi od dwóch przycisków case WM TIMER: // Kiedy pojawi się komunikat timera, zabij timer // i rozpocznij odtwarzanie dokuczliwego dŸwięku. KillTimer (hwnd, IDţTIMER) ; PlaySound ((PTSTR) pwaveform, NULL, SND MEMORY ţ SNDţLOOP ţ SND ŽSYNC); // Pozwólmy œpišcemu użytkownikowi wyldczyć budzik spacjţ // Jeżeli okno jest zminimalizowane, zostanie przywrócone; // następnie trafi na pierwszy plan; następnie uaktywni się // przycisk, który otrzyma fokus wejœciowy. EnableWindow (hwndDTP, FALSE) ; EnableWindow (hwndCheck, FALSE) ; EnableWindow (hwndPush, TRUE) ; hwndFocus = hwndPush ; ShowWindow (hwnd, SW_RESTORE) ; SetForegroundWindow (hwnd) ; return 0 ; // posprzdtaj, jeżeli budzik dzwoni lub timer jest nadal ustawiony case WM_DESTROY: free (pwaveform) ; 11% Częœć III: Zagadnienia zaawansowane (cišg dalszy ze strony 1195) i if (IsWindowEnabled (hwndPush)) PlaySound (NULL, NULL, 0) ; if (SendMessage (hwndCheck, BM_GETCHECK, 0, 0)) KillTimer (hwnd, ID TIMER) ; PostOuitMessage (0) ; return 0 ; 1 return DefWindowProc (hwnd, message, wParam, lParam) ; LRESULT CALLBACK SubProc (HWND hwnd, UINT messa9e, WPARAM wParam, LPARAM lParam) f int idNext, id = GetWindowLong (hwnd, GWLţID) ; switch (message) case WM_CHAR: if (wParam = '\t') ( idNext = id ; do idNext = (idNext + (GetKeyState (VK_SHIFT) < 0 ? 2 : 1)) % 3 ; while (!IsWindowEnabled (GetOlgItem (GetParent (hwnd), idNext))); SetFocus (GetDlgItem (GetParent (hwnd), idNext)) ; return 0 ; ) break ; case WM_SETFOCUS: hwndFocus = hwnd ; break ; ) return CallWindowProc (SubbedProc Cid], hwnd, message, wParam, lParam> ; ) Rysunek 22-7. Program WAKEUP Sygnał, za pomocš którego WAKEUP budzi, to dwie stosowane naprzemiennie fale prostokštne. Rzeczywisty sygnał jest wyliczany podczas obsługi przez pro- cedurę WndProc komunikatu WMţCREAT'E. Cały plik wave jest zapisany w pa- mięci; wskaŸnik do niego jest przekazywany funkcji PIaySound, której argumen- tami sš SND MEMOftY, SIVDţLOOP i SIVDţASYNC. W programie zastosowano kontrolkę umożliwiajšcš wybór daty i godziny. WA- KEUP używa jej jednak tylko do wyboru godziny. Program może pobierać i usta- wiać czas za pomocš struktury SYST'EMTTME, używanej do odczytywania i usta- wiania zegara komputera. Aby przekonać się, jak wygodne jest korzystanie z kon- trolki, spróbuj utworzyć takie samo okno bez niej. Rozdział 22: Dtwigk i muryka Zwróć uwagę na logikę na końcu obsługi komunikatu WMţCREATE: w progra- mie założono, że użytkownik uruchamia budzik tuż przed pójœciem spać i że chce przespać równe 8 godzin, liczšc od wybicia pełnej godziny. Teraz można, oczywiœcie, pobrać bieżšcy czas za pomocš funkcji GetLocalTime do struktury SYSTEMTIME i zwiększyć go ręcznie. Ale w ogólnym przypadku wy- liczenie wymaga sprawdzenia, czy wynikowa godzina nie jest większa niż 24, co oznacza, że konieczne będzie zwiększenie numeru dnia, a to może się wišzać ze zwiększeniem numeru miesišca (stšd logika kontrolujšca liczbę dni każdego miesišca i czy rok jest przestępny), a to z kolei ze zmianš numeru roku. Lepiej zastosować metodę zalecanš (patrz: /Platform SDK/Windows Base Services/ General Library Time/Time Reference/Time Structures/SYSTEMTIME), która polega na konwersji struktury SYSTEMTIME na strukturę FILETIME (za pomocš funk- cji SystemTimeToFileTime), rzutowaniu struktury FILETIME na strukturę LAR- GĘ INTEGER, wykonaniu obliczeń na danych tego typu, rzutowaniu z powro- tem na strukturę FILETIME i konwersji na SYSTEMTIME (za pomocš funkcji File- TimeToSystemTime). Struktura FILETIME, jak wskazuje jej nazwa, służy do pobierania i ustawiania godziny ostatruej modyfikacji pliku. Ma ona następujšcš postać: type struct ţFILETIME // ft f DWORD dwLowOateTime ; DWORD dwHighOateTime ; 1 FILETIME ; Oba pola łšcznie tworzš liczbę 64-bitowš oznaczajšcš liczbę 100-nanosekundo- wych chwil, które upłynęły od 1.01.1601 roku. Kompilator Microsoft C/C++ obsługuje 64-bitowe liczby całkowite w ramach niestandardowego rozszerzenia ANSI C. Typ danych nazywa się intó4. Na licz- bach i danych tego typu można przeprowadzać wszystkie normalne operacje aryt- metyczne. Typ obsługujš również niektóre funkcje biblioteczne. W pliku nagłów- kowym WINNT.H znajdujš się następujšce definicje: typedef ţint64 LONGLONG; typedef unsigned ţintó4 DWORDLONG: W Windows typ int64 jest czasem nazywany czterosłowem albo po prostu dużš liczbš całkowitš. Jest również zdefiniowana następujšca unia: typedef union ţLARGEţINTEGER f struct ( DWORD LowPart; LONG HighPart; ) : LONGLONG OuadPart; ? LARGEţINTEGER; Wszystkie te informacje można znaleŸć w dokumentacji: /Platform SDK/Windows Base Services/General Library/Large Integer Operations. Unia pozwala używać dużych liczb całkowitych jako dwóch liczb 32-bitowych albo jako jednej liczby 64-bitowej. ţ 1 gg Częœć III: Zagadnienia zaawansowane Muzyka MIDI Interfejs MIDI (ang. Musical Instrument Digital Interface) został opracowany na po- czštku lat osiemdziesištych przez konsorcjum producentów elektronicznych syn- tezatorów muzycznych. Jest protokołem przewidzianym do łšczenia elektronicz- nych instrumentów muzycznych ze sobš i z komputerami. MIDI to niezwykle ważny standard z dziedziny muzyki elektronicznej. Jego specyfikacja jest utrzy- mywana przez Stowarzyszerue Producentów MIDI (MIDI Manufacturers Asso- ciation, MMA), z siedzibš pod adresem http://www.midi.org. Działanie MIDI MIDI definiuje protokół przekazywania poleceń przez kablowe połšczenia cy- frowe między zgodnymi z tym standardem urzšdzeniami. Kabel MIDI jest z re- guły zakończony 5-bolcowš wtyczkš, ale w rzeczywistoœci wykorzystywane sš tylko trzy żyły. Jedna to masa, druga to pętla pršdowa, a trzecia to sygnał. Proto- kół MIDI jest jednokierunkowy i pozwala przesyłać dane z prędkoœciš 31 250 bi- tów na sekundę. Każdy bajt danych zaczyna się bitem startu, kończy bitem sto- pu. Efektywna prędkoœć transmisji wynosi 3125 bajtów na sekundę. Należy pamiętać, że przez łšcze MIDI nie przesyła się żadnych dŸwięków - ani w analogowej, ani w cyfrowej postaci. Przez kabel przechodzš z reguły proste komunikaty o dhzgoœci 1, 2 lub 3 bajtów. Prosta konfiguracja MIDI może się składać z tylko dwóch urzšdzeń zgodnych z MIDI. Jednym może być klawiatura MIDI, która nie może shzżyć jako samo- dzielny instrument, lecz wyłšcznie do generowania komunikatów MIDI. Klawia- tura taka ma wyjœciowy port MIDI, oznaczony napisem "MIDI out". Drugi ko- niec kabla podłšczonego do tego portu powinien być wetknięty do portu wej- œciowego MIDI, oznaczonego napisem "MIDI in" (w syntezatorze lub innym in- strumencie). Syntezator może być pozbawiony własnej klawiatury i mieć wyglšd prostopadłoœciennej skrzynki z kilkoma przyciskami. Kiedy muzyk naciœnie klawisz (na przykład œrodkowe C), klawiatura wyœle 3- bajtowy komunikat do portu MIDI out. W zapisie szesnastkowym będzie to na- stępujšca sekwencja: 90 3C 40 Pierwszy bajt (90) to komunikat Note On (włšcz nutę). Drugi okreœla numer kla- wisza (3C oznacza dokładnie œrodkowe C). Trzeci bajt sygnalizuje siłę (prędkoœć), z jakš naciœruęto klawisz (może mieć wartoœć od 1 do 127). Tak się składa, że nie używamy klawiatury dynamicznej, reagujšcej na sposób naciskania klawiszy, więc przesłana została wartoœć przeciętnej prędkoœci (siły) naciskania. Ten 3-bajtowy komunikat przechodzi przez kabel MIDI do portu MIDI in syntezatora. Synteza- tor odpowiada, odtwarzajšc œrodkowe C. Kiedy klawisz zostanie zwolniony, klawiatura wyœle do portu MIDI out kolejny 3-bajtowy komunikat: 90 3C 00 Rozdział 22: DŸwięk i muryka Jest to taki sam komunikat jak Note On, tylko z prędkoœciš naciskania równš zero. Bajt zero oznacza polecerue Note Off (wyłšcz nutg), powodujšce przerwanie gra- nia dŸwięku. Syntezator odpowiada przerwaniem grania œrodkowego C. Jeżeli mamy do dyspozycji instrument polifoniczny (mogšcy wydawać kilka dŸwięków równolegle), możemy grać na nim akordy. Klawiatura generuje wów- czas wiele komunikatów Note On, a syntezator odgrywa je wszystkie naraz. Po zwolnieniu klawiszy tworzšcych akord klawiatura przesyła kilka komunikatów Note Off. Mówišc ogólrue, klawiatura w omawianej konfiguracji pełni funkcję kontrolera MIDI (tak się przyjęło to urzšdzenie nazywać, choć bardziej odpowiedniš nazwš byłby sterownik MIDI - przyp. tłum.), jest bowiem odpowiedzialna za generowa- nie komunikatów MIDI sterujšcych działaniem syntezatora. Nie każdy kontroler MIDI jest klawiaturš. Istniejš dęte kontrolery MIDI, przypominajšce klarnety czy saksofony oraz gitarowe, inne strunowe, a nawet bębnowe (perkusyjne). Nieza- leżnie od postaci, w jakiej występujš, wszystkie kontrolery MIDI generujš 3-baj- towe komunikaty Note On i Note Off (i wiele innych). Kontrolerem MIDI może być jeszcze inne urzšdzenie: sekwencer. Jest to produkt elektroniczny, w którym można zapisywać i z którego można odtwarzać sekwencje komunikatów MIDI. Sekwencery wolno stojšce sš obecnie rzadziej używane niż kiedyœ, ponieważ zastšpiły je komputery. Dowolny komputer wyposażony w kartę MIDI może generować komunikaty MIDI sterujšce syntezatorem równie dobrze jak urzšdzenie specjalizowane. Dostępne jest również oprogramowanie do two- rzenia muzyki w zapisie MIDI. Pozwala ono komponować na ekranie, zapisy- wać komunikaty MIDI odczytywane z kontrolera MIDI, manipulować nimi i wy- syłać z powrotem do syntezatora. Syntezator nazywa się czasem modułem dŸwiękowym albo generatorem tonu. Specyfikacja MIDI nie okreœla sposobu generowania dŸwięku. Syntezator może tworzyć brzmienia za pomocš dowolnej z licznych technik. W rzeczywistym œwiecie tylko bardzo proste kontrolery MIDI (na przykład dęte) sš wyposażone wyłšcznie w port wyjœciowy MIDI. Kontrolery klawiaturowe majš bowiem często wbudowane syntezatory, a więc z reguły trzy porty: MIDI in, MIDI out i MIDI thru. Port MIDI in przyjmuje komunikaty do odtwarzania dŸwięków w wewnętrznym syntezatorze. Port MIDI out służy do wysyłania własnych ko- munikatów do urzšdzeń zewnętrznych. Port MIDI thru jest z kolei portem wyj- œciowym duplikujšcym komunikaty otrzymywane w porcie wejœciowym (MIDI in) - wszystko, co trafi do portu MIDI in, pojawia się automatycznie na porcie MIDI thru (port MIDI thru nie ma nic wspólnego z komunikatami wysyłanymi przez port MIDI out). Sš tylko dwa sposoby podłšczania urzšdzeń MIDI za pomocš kabli: port MIDI out jednego urzšdzenia z portem MIDI in drugiego albo MIDI thru jednego urzš- dzenia z portem MIDI in drugiego. Port MIDI thru umożliwia tworzenie konfi- guracji łańcucha urzšdzeń MIDI (ang. daisy-chain). 1200 Częœć III: Zagadnienia zaawansowane Zmiana programu CO pp Jaki dŸwięk wydaje syntezator - pianina, skrzypiec, tršbki czy statku kosmiczne- go? Z reguły dŸwięki, jakie może wydawać syntezator, zapisane sš w pamięci ROM albo gdzie indziej. Sš one nazywane "barwami" lub popularnie "instru- mentami" albo "patchami". Słowo "patch" pochodzi z czasów syntezatorów ana- logowych, kiedy barwy uzyskiwano poprzez odpowiedniš konfigurację krosow- nicy, czyli łšczerua (ang. patch) kablami różnych gniazdek na panelu instrumen- tu. Barwy w języku MIDI nazywa się "programami". Zmiana programu odbywa się przez wysłanie do syntezatora komunikatu Program Change: gdzie pp jest liczbš z przedziału od 0 do 127. Klawiatury MIDI sš często wyposa- żone w ponumerowane przyciski służšce do generowania komunikatów Program Change. Naciskajšc je, można zmieruać barwy syntezatora z poziomu klawiatu- ry. Numery te zaczynajš się na ogół od 1, a nie od 0, niemniej jednak przycisk numer 1 wysyła komunikat Program Change z argumentem 0. Specyfikacja MIDI nie okreœla, jakim numerom majš odpowiadać dane instrumen- ty. Pierwsze trzy programy syntezatora Yamaha DX7 nazywajš się na przykład "Warm Strings', "Mellow Horn" i "Pick Guitar"; w generatorze tonów Yamaha TX8lZ sš to "Grand Piano", "Upright Piano" i "Deep Grand"; w module dŸwię- kowym Roland MT 32 "Acoustic Piano 1", "Acoustic Piano 2" i "Acoustic Piano 3" itd. Tak więc nie zdziw się, zmieniajšc program z klawiatury, że barwa, jakš usłyszysz, będzie zależała od instrumentu, którego akurat przyjdzie ci używać. Z plikami MIDI zawierajšcymi komunikaty Program Change można mieć poważ- ne problemy - nie sš one niezależne od instrumentów, ponieważ odtwarzane na innych syntezatorach brzmiš inaczej. Pewien porzšdek wprowadził ustano- wiony swego czasu standard General MIDI (GM). Jest on obshxgiwany przez Windows. Jeżeli jakiœ syntezator nie jest kompatybilny ze specyfikacjš General MIDI, można mu jš zaemulować za pomocš odpowiednich przypisań programo- wych. Kanały MIDI Do tej pory omówiliœmy tylko dwa komunikaty MIDI. Pierwszy to Note On: 90 kk vv gdzie kk jest numerem klawisza (od 0 do 127), a vv prędkoœciš naciskania klawi- sza (od 0 do 127). Prędkoœć zerowa oznacza polecenie Note Off. Drugim komuni- katem jest Program Change: CO pp gdzie pp jest liczbš z przedziału od 0 do 127. To sš dwa typowe komunikaty MIDI. Pierwszy bajt nazywa się bajtem statusu. Po bajcie statusu występuje 0, 1 lub 2 bajty danych (wyjštkiem sš komunikaty systemowe, o których napiszę nieco da- lej). Bajt statusu łatwo odróżnić od bajtu danych: jego najstarszy bit jest zawsze równy 1 (bajty danych majš najstarszy bit równy 0). Rozdział 22: DŸwięk i muryka 1201 Nie omówiłem jeszcze jednak uogólruonej postaci komunikatów. Dla komunika- tu Note On jest ona taka: 9n kk vv a komunikatu Program Change taka: Cn pp CO ol C1 05 W obu przypadkach n oznacza młodsze 4 bity bajtu stanu (a więc liczbę z prze- dziahz od 0 do 15). Tworzš one tzw. kanał MIDI. Kanały sš numerowane od 1, więc n = 0 oznacza kanał numer 1. Używanie 16 różnych kanałów pozwala przesyłać przez kabel MIDI komunikaty 16 różnych barw. Na ogół obserwuje się cišgi komunikatów MIDI zaczynajšce się od polecenia Program Change, zmieniajšcego barwy używanych kanałów, i za- wierajšce liczne polecenia Note On i Note Off. PóŸniej sekwencja może zawierać kolejne polecenia Program Change. W danej chwili każdy kanał ma przypisanš jednak tylko jednš barwę. Rozważmy prosty przykład: załóżmy, że klawiaturowy sterownik MIDI, o któ- rym pisałem, jest w stanie generować komunikaty MIDI dla dwóch różnych ka- nałów jednoczeœnie: dla kanahz numer 1 i 2. Grę można zaczšć od wybrania in- strumentów, czyli naciœnięcia przycisków wyboru barwy wysyłajšcych do synte- zatora następujšce komunikaty Program Change: Kanał 1 ma teraz przypisany program numer 2, a kanał 2 program numer 6 (jak pamiętasz, programy sš numerowane od 1, ale kodowane w komunikatach od 0). Naciœnięcie dowolnego klawisza spowoduje teraz wysłanie dwóch komuni- katów Note On, po jednym dla każdego kanału: 90 kk vv 91 kk vv W ten sposób można grać dwoma barwami instrumentów unisono. Niektóre klawiatury majš tzw. split (sš podzielone na dwie częœci). Klawisze z le- wej strony podziału mogš generować komunikaty Note On dla kanału 1, a z pra- wej strony - komunikaty Note On dla kanału 2. Pozwala to grać za pomocš jed- nej klawiatury na dwóch instrumentach niezależnie. Możliwoœć korzystania z 16 kanałów przydaje się w przypadku stosowania se- kwencerów. Każdy kanał może odpowiadać innemu instrumentowi. Jeżeli dys- ponuje się syntezatorem, który jest w stanie odgrywać 16 różnych instrumentów jednoczeœnie, można za jego pomocš odtwarzać utwory napisane na 16-osobowš orkiestrę. Komunikaty MIDI Komunikaty Note On i Program Change to jedne z najczęœciej używanych, ale nie jedyne, jakie ma do zaoferowania MIDI. Poniżej pokazano tabelę komunika- tów MIDI zwišzanych z operacjami na kanałach (n oznacza numer kanału od 0 do 15). Jak wspomniałem, bajt statusu ma zawsze najstarszy bit równy l, a bajt danych 0. Oznacza to, że wartoœci bajtów statusu należš do przedziahx od 0x80 do OxFF, a bajtów danych od 0 do Ox7F. 1202 Częœć 111: Zagadnienia zaawansowane Komunikat MIDI Bajt danych Wartoœci Note Off (wyłšcz nutę) Sn kk w kk = numer klawisza (0-127) w = prędkoœć naciskania (0-127) Note On (włšcz nutę) 9n kk w kk = numer klawisza (0-127) w = prędkoœć naciskania (1-127, 0 = Note Off) Polyphonic After Touch An kk tt kk = numer klawisza (0-127) (dociœnięcie polifoniczne) tt = wartoœć dociœnięcia (0-127) Control Change Bn cc xx cc = kontroler (0-121) (zmiana kontrolera) xx = wartoœć (0-127) Channel Mode Local Control Bn 7A xx xx = 0 (wył.), 127 (wł.) (tryb kanahx lokalnego kontrolera) All Notes Off Bn 7B 00 (wyłšcz wszystkie nuty) Omni Mode Off Bn 7C 00 (wyłšcz tryb omni) Omni Mode On Bn 7D 00 (włšcz tryb omni) Mono Mode On Bn 7E cc cc = liczba kanałów (włšcz tryb mono) Poly Mode On Bn 7F 00 (włšcz tryb polifon) Program Change Cn pp pp = program (0-127) (zmiana programu) Channel After Touch Dn tt tt = wartoœć dociœnięcia (0-127) (dociœnięcie dla kanahx) Pitch Wheel Change En 11 hh II = młodsze 7 bitów (0-127) (zmiana położenia hh = starsze 7 bitów (0-127) potencjometru wysokoœci tonu) Numery klawiszy odpowiadajš na ogół tradycyjnym nutom muzyki zachodniej, cho- ciaż niekoniecznie. W przypadku instrumentów perkusyjnych każdemu klawiszowi może być przypisany inny instrument perkusyjny. Jeżeli numery klawiszy odpowia- dajš klawiaturze pianina, klawisz 60 (dziesištkowo) jest przypisany do œrodkowego C. Numery klawiszy MIDI obejmujš 21 nut poniżej i 19 nut powyżej zakresu trady- cyjnego, 88-klawiszowego pianina. Liczba okreœlajšca prędkoœć naciœnięcia kla- wisza w przypadku pianina wpływa na głoœnoœć i harmoniczny charakter dŸwię- ku. IZeakcja na parametr prędkoœci zależy od typu instrumentu. W pokazanych wczeœniej przykładach komunikat Note On z bajtem prędkoœci równym zero występował w roli komunikatu Note Off. Istnieje jeszcze inne pole- cerue Note Off, w którym podaje się prędkoœć zwalniania klawisza. Jest ono jed- nak używane niezmiernie rzadko. W tabeli występujš dwa komunikaty After Touch (dociœnięcia). After-touch to funkcja pozwalajšca zmienić w jakiœ sposób dŸwięk przez dociœnięcie klawisza już naciœniętego. Jeden z komunikatów (status = OxDn) definiuje efekt after-to- uch stosowany dla wszystkich nut odgrywanych na danym kanale; w taki spo- sób z tego efektu korzysta się najczęœciej. Bajt statusu OxAn oznacza stosowanie efektu after-touch do każdego klawisza niezależnie. Rozdział 22: DŸwięk i muryka 1203 Klawiatury sš z reguły wyposażone w różne pokrętła i przełšczniki pozwalajšce na sterowanie dŸwiękiem. Sš to kontrolery. Zmiany ich położerua sygnalizuje bajt statusu OxBn. Kontrolery sš identyfikowane numerami od 0 do 121. Bajt statusu OxBn jest również używany w komunikatach Channel Mode, które informujš, jak syntezator powinien odpowiadać na kilka nut wysyłanych jednoczeœnie do jed- nego kanahx. Bardzo ważnym kontrolerem jest potencjometr shzżšcy do podnoszenia i obniża- nia wysokoœci dŸwięku. Odpowiada mu osobny komunikat MIDI z bajtem statu- su równym OxEn. W tabeli nie ma komunikatów zaczynajšcych się od bajtów statusu od FO do FF. Sš to tak zwane komunikaty systemowe, ponieważ odnoszš się one do całego systemu MIDI, a nie do konkretnego kanahx. Komunikaty systemowe sš używa- ne najczęœciej do synchronizacji, uruchamiania sekwencerów, resetowania urzš- dzeń i pobierania danych. Wiele kontrolerów MIDI wysyła nieustannie bajty statusu OxFE, czyli komunika- ty Active Sensing (wykrywanie aktywnoœci). Informujš one system, że kontroler MIDI jest wcišż do niego podłšczony. Jednym z ważnych komunikatów systemowych jest komunikat zaczynajšcy się od bajtu statusu OxFO. Jest on używany do transferowania kawałków danych do syn- tezatora w formacie zależnym od producenta i syntezatora (w ten sposób można na przykład przekazać z komputera do syntezatora definicję nowej barwy). Tylko komunikat systemowy może zawierać więcej niż 2 bajty danych. Liczba bajtów danych jest w rzeczywistoœci zmienna, ale każdy bajt musi mieć najstarszy bit usta- wiony na 0. Koniec komurukatu systemowego wskazuje bajt statusu OxF7. ' Komunikaty systemowe shzżš również do zrzucania danych (na przykład defini- cji barw) z syntezatora. Dane opuszczajš instrument poprzez port MIDI out. Je- żeli chcesz oprogramować MIDI w sposób niezależny od urzšdzenia, powinie- neœ wystrzegać się komunikatów systemowych. Sš one jednak doœć przydatne podczas definiowania nowych barw syntezatorów. Plik MIDI (z rozszerzeniem .MID) jest zbiorem komunikatów MIDI wzbogaco- nych o informacje synchronizujšce. Pliki MIDI można odgrywać za pomocš MCI. Jednakże w pozostałej częœci tego rozdziału zajmę się niskopoziomowymi funk- cjami midi0ut. Wprowadzenie do sekwencji MIDI Niskopoziomowy interfejs API dla MIDI zawiera funkcje o nazwach z prefiksem midiln (do odczytu sekwencji MIDI wychodzšcych z kontrolerów wyjœciowych) i midi0ut (do odtwarzania muzyki z wewnętrznego lub zewnętrznego synteza- tora). Choć mowa o funkcjach niskopoziomowych, korzystajšc z nich, nie trzeba nic wiedzieć o interfejsie sprzętowym MIDI. Chcšc otworzyć wyjœciowe urzšdzenie MIDI do odtwarzarua muzyki, należy wywołać funkcję midiOutOpen: error = midiOutOpen (&hMidiOut, w0eviceID, dwCallBack, dwCallBackData, dwFlags): ţ 204 Częœć III: Zagadnienia zaawansowane Funkcja ta zwraca 0 w przypadku zakończenia powodzeniem lub kod błędu. Je- żeli jej argumenty poda się właœciwie, ewentualny komunikat o błędzie będzie informował, że urzšdzerue MIDI jest już używane przez inny program. Pierwszym argumentem jest wskaŸnik do zmiennej typu HMIDIOUT, która od- biera uchwyt wyjœciowego urzšdzenia MIDI do użycia w przyszłych wywoła- niach funkcji wyjœciowych MIDI. Drugim argumentem jest identyfikator urzšdze- nia. Aby użyć jednego z rzeczywistych urzšdzeń, należy argument ten okreœlić jako liczbę z zakresu od 0 do o jeden mniejszej niż liczba uzyskana z midiOutGet- NumDevs. Można ewentualnie użyć stałej MIDIMAPPER, zdefiniowanej w pliku nagłówkowym MMSYSTEM.H jako -1. W większoœci przypadków za ostatnie trzy argumenty midiOutOpen podaje się wartoœć NULL lub 0. Po otwarciu wyjœciowego urzšdzenia MIDI i uzyskaniu uchwytu można rozpo- czšć wysyłanie do niego komunikatów MIDI. Robi się to za pomocš wywołania: error = midiOutShortMsg (hMidiOut, dwMessage); Pierwszym parametrem tej funkcji jest wartoœć uzyskiwana z midiOutOpen. Dru- gi parametr to jedno-, dwu- lub trzybajtowy komunikat MIDI zapakowany w 32- bitowš zmiennš DWORD. Jak napisałem wczeœniej, komunikaty MIDI zaczynajš się od bajtu statusu, po którym następuje 0, 1 lub 2 bajty danych. Bajt statusu jest najmniej znaczšcym bajtem zmiennej dwMessage, pierwszy bajt danych - bardziej znaczšcym bajtem tej zmiennej, a drugi bajt danych - najbardziej znaczšcym. Najbardziej znaczšcy bajt dwMessage jest równy 0. Aby zagrać na przykład œrodkowe C (nutę Ox3C) na 5 kanale MIDI z prędkoœciš naciœruęcia klawisza Ox7F, należy wysłać następujšcy trzybajtowy komunikat Note On: 0x95 Ox3C Ox7F Parametr dwMessage funkcji midiOutShortMsg będzie więc równy 0x007F3C95. Trzy podstawowe komunikaty MIDI to Program (zmiana programu, czyli barwy przypisanej danemu kanałowi), Note On (włšczenie nuty) i Note Off (wyłšcze- nie nuty). Po otwarciu wyjœciowego urzšdzenia MIDI należy zawsze rozpoczšć sekwencję od komunikatu zmieniajšcego program (Program Change). Należy także pamiętać o tym, by wysyłać równš liczbę komunikatów Note On i Note Off. Po zakończeniu odtwarzania muzyki można zresetować urzšdzenie MIDI, aby upewnić się, że wszystkie nuty zostanš wyłšczone: midiOutReset (hMidiOut): Na końcu należy zamknšć urzšdzenie: midiOutClose (hMidiOut): Najważniejsze z niskopoziomowej obshzgi wyjœciowych urzšdzeń MIDI sš funk- cje midiOutOpen, midiOutShortMsg, midiOutReset i midiOutClose. Do roboty więc! Program BACHTOCC, przedstawion;ţ na rysunku 22-8, odgry- wa pierwsze takty Toccaty i Fugi D-mol, organowego dzieła Jana Sebastiana Ba- cha. Rozdziat 22: Dtwięk i muzyka 1205 BACHTOCC.C /* BACHTOCC.C - Toccata D-mo1 J. S. Bacha (c) Charles Petzold, 1998 */ 4ţinclude 4ţdefine ID TIMER 1 LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; TCHAR szAppNameC) ţ TEXT ("BachTocc") ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) f HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS HREDRAW ţ CS_VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC ARROW) ; wndclass.hbrBackground = GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) ( MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MBţICONERROR) ; return 0 ; ) hwnd = CreateWindow (szAppName, TEXT ("Bach Toccata in D Minor (First Bar)"), WS OVERLAPPEDWINDOW, CW USEDEFAULT, CW_USEDEFAULT, CW USEDEFAULT, CWţUSEDEFAULT, NULL, NULL, hInstance, NULL) ; if (!hwnd) return 0 ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) ( TranslateMessage (dţmsg) ; 1206 Częœć III: Zagadnienia zaawansowane (cišg dalszy ze strony 1205) DispatchMessage (&msg) ; ) return msg.wParam ; ) DWORD MidiOutMessage (HMIDIOUT hMidi, int iStatus, int iChannel, int iDatal, int iData2) ( DWORD dwMessage = iStatus ţ iChannel ţ (iDatal Ž 8) ţ (iData2 Ž 16) ; return midiOutShortMsg (hMidi, dwMessage) ; 1 LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) f static struct int iDur ; int iNote [27 ; ) noteseq C7 = t 110, 69. 81, 110, 67, 79. 990, 69, 81, 220, -1, -1, 110. 67. 79. 110, 65, 77, 110, 64, 76, 110, 62, 74, 220, 61, 73. 440, 62, 74, 1980, -1, -1, 110, 57, 69. 110, 55, 67, 990, 57, 69, 220, -1, -1, 220, 52. 64, 220, 53, 65, 220, 49. 61, 440. 50, 62, 1980, -1, -1 ) ; static HMIDIOUT hMidiOut ; static int iIndex ; int i : switch (message) ; ( case WM CREATE: // otwarcie urzddzenia MIDIMAPPER if (midiOutOpen (&hMidiOut. MIDIMAPPER, 0, 0, 0)) Messa9eBeep (MB_ICONEXCLAMATION) ; MessageBox (hwnd, TEXT ("Cannot open MIDI output device!"), szAppName, MBţICONEXCLAMATION ţ MB OK) ; ! return -1 ; 1 // wyslanie komunikatów Program Change ("organy koœcielne") MidiOutMessage (hMidiOut, OxCO, 0, 19, 0) ; MidiOutMessage (hMidiOut, OxCO, 12, 19. 0) ; SetTimer (hwnd. ID TIMER, 1000, NULL) ; return 0 ; case WM TIMER: // pętla dla 2-nutowej polifonii for (i = 0 ; i < 2 ; i++) f // komunikaty Note Off dla poprzedniej nuty Rozdział 22: DŸwigk i muryka 1207 if (iIndex != 0 && noteseq[iIndex - 1].iNote[i] != -1) i MidiOutMessage (hMidiOut, 0x80, 0, noteseq[iIndex - 1].iNote[i], 0) ; MidiOutMessage (hMidiOut, 0x80, 12, noteseq[iIndex - 1].iNote[i], 0) ; ) // komunikaty Note On dla nowej nuty if (iIndex != sizeof (noteseq) / sizeof (noteseq[0]) && noteseq[iIndex].iNote[i] != -1) ( MidiOutMessage (hMidiOut, 0x90, 0, noteseq[iIndex].iNote[i], 127) ; , MidiOutMessage (hMidiOut, 0x90, 12, noteseq[iIndex].iNote[i], 127) ; ) ) if (iIndex != sizeof (noteseq) / sizeof (noteseq[0])) . SetTimer (hwnd, ID TIMER, noteseq[iIndex++].iDur - l, NULL) ; ) else ( KillTimer (hwnd, ID_TIMER) ; DestroyWindow (hwnd) ; ) return 0 ; case WM_DESTROY: , midiOutReset (hMidiOut) ; midiOutClose (hMidiOut) ; PostOuitMessage (0) ; return 0 ; J return DefWindowProc (hwnd, message, wParam, lParam) ; Rysunek 22-8. Program BACHTOCC 1208 Częţć III: Zagadnienia zaawansowane Nuty do granego przez program fragmentu toccaty przedstawiono na rysunku 22-9. Adagio. + Manuale. Pedale. Rysunek 22-9. Pierwsze takty Toccaty i Fugi d-mol Jana Sebastiana Bacha Naszym zadaniem jest tutaj przełożenie muzyki na serię liczb - a konkretnie numerów klawiszy i danych synchronizacyjnych informujšcych program, kiedy ma wysyłać komunikaty Note On (równoważne naciœnięciom klawiszy organów) i Note Off (równoważne zwolnieniom klawiszy). Ponieważ klawiatura organów nie jest czuła na sposób naciskania klawiszy, możemy zagrać wszystkie nuty z takš samš prędkoœciš naciskania. Innym uproszczeniem jest ignorowanie różnic mię- dzy staccato (pozostawiarue krótkich przerw ciszy między nutami) i legato (zle- waniem się ze sobš kolejno odgrywanych dŸwięków). Założymy, że koniec każ- dej nuty pokrywa się z poczštkiem następnej. Jeżeli umiesz odczytywać nuty, zauważysz, że poczštek toccaty składa się z rów- noległych nut oddalonych o oktawę. Dlatego też utworzyłem strukturę o nazwie noteseq, w której zapisałem szereg czasów trwarua nut i dwa numery klawiszy. Niestety, do kontynuowania utworu potrzebne byłoby bardziej ogólne rozwiš- zanie problemu zapisu danych. Zdecydowałem, że cwierćnuta powinna trwać 1760 milisekund, co oznacza, że ósemka (nuta z jednš choršgiewkš) powinna trwać 880 milisekund, szesnastka (nuta z dwoma choršgiewkami) 440, trzydziestodwój- ka (nuta z trzema choršgiewkami) 220, a szeœćdziesięcioczwórka (nuta z cztere- ma choršgiewkami) 110. W odgrywanym fragmencie znajdujš się dwa mordenty - jeden nad pierwszš nutš, a drugi w połowie frazy. Sš one oznaczone poziomymi liniami falistymi z krótkš pionowš poprzeczkš. W muzyce barokowej mordent oznaczał, że nuta miała być grana jako trzy nuty: dana nuta, nuta o jeden pełny ton niżej i dana nuta. Pierwsze dwie nuty powinny być odegrane szybko, a trzecia pozostawiona do wybrzmiewania. Pierwszš nutš jest na przykład A z mordentem. Należy jš zagrać jako A, G, A. Zdecydowałem pierwsze dwie nuty mordentu zagrać jako szeœćdziesięcioczwórki, czyli o długoœci 110 milisekund. W odgrywanej frazie sš również cztery fermaty. Zostały one oznaczone hxkami z kropkš w œrodku. Fermata oznacza, że nuta powinna być przytrzymana dłużej, Rozdział 22: DŸwięk i muryka 1209 niż wynikałoby to z zapisu, z reguły tak dhzgo, jak uzna grajšcy. Aby uwzględ- nić fermaty, postanowiłem zwiększyć czas trwania nut o 50 procent. Jak widać, translacja tak krótkiego i prostego fragmentu muzyki, jakim jest po- czštek Toccaty i Fugi d-mol Bacha, nie jest wcale prosta ani oczywista! Tablica struktur noteseq zawiera trzy liczby dla każdych dwóch równolegle gra- nych nut. Sš to: czas trwarua nuty lub pauzy oraz dwa numery klawiszy MIDI (dla dwu równoległych nut), na przykład pierwsza odgrywana jest nuta A o cza- sie trwania 110 milisekund. Ponieważ œrodkowe C ma numer klawisza MIDI rów- ny 60, to A powyżej œrodkowego C będzie miało numer klawisza 69, a A oktawę wyżej - numer 81. Tak więc pierwsze trzy wartoœci tablicy noteseq wynoszš: 110, 69 i 81. Pauzę oznaczyłem liczbš -l. Podczas obsługi komunikatu WM-CREATE program ustawia zegar Windows na 1000 milisekund (co oznacza, że muzyka zacznie się za 1 sekundę), by następnie wywołać funkcję midiOutOpen z identyfikatorem urzšdzenia MIDIMAPPER. BACHTOCC wymaga tylko jednego instrumentu (organów), potrzebny więc bę- dzie tylko jeden kanał. Aby uproœcić wysyłanie komunikatów MIDI, zdefiniowa- łem krótkš funkcję o nazwie MidiOutMessage. Na jej wejœciu podaje się uchwyt urzšdzenia wyjœciowego MIDI, bajt statusu, numer kanału i dwa bajty danych. Funkcja na podstawie otrzymanych danych buduje 32-bitowy komunikat i wy- wołuje midiOutShortMsg. Pod koniec przetwarzania komunikatu WM-CREATE program wysyła komuni- kat Program Change wybierajšcy jako instrument organy koœcielne. Aby wybrać za pomocš komunikatu Program Change głos organów koœcielnych ze specyfi- kacji General MIDI, należy użyć bajtu danych 19. Samo granie nut odbywa się podczas obshzgi komunikatu WM-TIMER. Pętla obsługuje dwunutowš polifo- nię. Jeżeli poprzedra nuta jeszcze trwa, program wysyła dla niej komunikat Note Off. Następnie, jeżeli nowa nuta nie jest pauzš, wysyła komunikaty Note On do kanałów 0 i 12. Potem resetuje zegar Windows, programujšc w nim czas trwania nuty odczytany ze struktury noteseq. Po osišgruęciu końca materiału muzycznego program likwiduje okno. Podczas przetwarzarua komunikatu WM DESTROY program wywołuje funkcje midi0ut- Reset i midiOutClose, a potem kończy działanie. Choć BACHTOCC działa i wydaje dŸwięki (nie można tego jednak nazwać gra- niem), okazuje się, że zegar Windows nie nadaje się do odtwarzania muzyki. Pro- blem polega na tym, że zegar ten jest oparty na zegarze komputera, a więc ta- kim, którego rozdzielczoœć jest za mała dla zastosowań muzycznych. Poza tym zegar Windows nie jest asynchroniczny. Komunikaty WM-TIMER mogš docie- rać z pewnym opóŸnieniem, jeżeli w tle działajš jakieœ inne programy. Niektóre mogš nawet być odrzucane, jeœli program nie jest w stanie ich obsłużyć natych- miast. Wówczas utwór przestanie przypominać sensownš muzykę. Program BACHTOCC jest więc dobrym przykładem użycia niskopoziomowych funkcji wyjœciowych MIDI, ale zastosowanie zegara Windows nie było w nim właœciwe. Oto dlaczego Windows oferuje jeszcze dodatkowy zestaw funkcji syn- chronizacyjnych, z których można korzystać w programach zbudowanych na 1210 Częœć III: Zagadnienia zaawansowane podstawie niskopoziomowej funkcji MIDI. Funkcje tego zestawu zaczynajš się prefiksem time. Zegar można za ich pomocš ustawiać z rozdzielczoœciš 1 milise- kundy. Jak się nimi posługiwać, pokażę na przykładzie programu DRUM, pod koniec tego rozdziahz. Symulowanie syntezatora MIDI z klawiatury PC Ponieważ większoœć użytkowników komputerów raczej rue dysponuje klawia- turš MIDI, którš można by przyłšczyć do karty dŸwiękowej, dobrze byłoby za- stšpić jš czymœ, co ma każdy, czyli klawiaturš komputera. Na rysunku 22-10 przedstawiono program o nazwie KBMIDI. Umożliwia on wydobywanie za po- mocš klawiatury PC dŸwięków z syntezatora - albo wbudowanego w kartę dŸwiękowš, albo podłšczonego zewnętrznie przez port MIDI out. KBMIDI daje pełnš kontrolę nad wyjœciowym urzšdzeniem MIDI (czyli wewnętrznym bšdŸ zewnętrznym syntezatorem), kanałami MIDI i przypisanymi do nich instrumen- tami. Program, oprócz tego, że jest zabawny, nadaje się do zgłębiania tajników obshzgi MIDI w Windows. KBMIDI.C /* KBMIDI.C - Klawiaturowy grajek MIDI (c) Charles Petzold, 1998 */ ţtinclude // definicje idnetyfikatorów menu // ţtdefine IDM_OPEN 0x100 ttdefine IDM_CLOSE 0x101 ttdefine IDM_DEVICE 0x200 4tdefine IDM CHANNEL 0x300 ttdefine IDM VOICE 0x400 LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM); TCHAR szAppName [7 = TEXT ("KBMidi") ; HMIDIOUT hMidiOut ; int iDevice = MIDIMAPPER, iChannel = 0, iVoice = 0, iVelocity = 64 ; int cxCaps, cyChar, xOffset, y0ffset ; // // struktury i dane do prezentacji rodzin instrumentów w postaci menu // typedef struct f TCHAR * szInst ; i.nt iVoice ; l INSTRUMENT ; Rozdział 22: DŸwięk i muryka 1211 typedef struct f TCHAR * szFam ; INSTRUMENT inst [8] ; ) FAMILY ; FAMILY fam [16] = [ TEXT ("Piano"), TEXT ("Acoustic Grand Piano"), 0, TEXT ("Bright Acoustic Piano"), 1, TEXT ("Electric Grand Piano"), 2, TEXT ("Honky-tonk Piano"), 3, TEXT ("Rhodes Piano"), 4, TEXT ("Chorused Piano"), TEXT ("Harpsichord"), 6, TEXT ("Clavinet"), TEXT ("Chromatic Percussion"), TEXT ("Celesta"), g, TEXT ("Glockenspiel"), g, TEXT ("Music Box"), 10, TEXT ("Vibraphone"), 11, TEXT ("Marimba"), 12, TEXT ("Xylophone"), 13, TEXT ("Tubular Bells"), 14, TEXT ("Dulcimer"), 15, TEXT ("Organ"), ' TEXT ("Hammond Organ"), 16, TEXT ("Percussive Organ"), 17, TEXT ("Rock Organ"), lg, TEXT ("Church Organ"), lg, TEXT ("Reed Organ"), 20, TEXT ("Accordian"), 21, TEXT ("Harmonica"), 22, TEXT ("Tango Accordian"), 23, I TEXT ("Guitar"), TEXT ("Acoustic Guitar (nylon)"), 24, TEXT ("Acoustic Guitar (steel)"), 25, TEXT ("Electric Guitar (jazz)"), 26, TEXT ("Electric Guitar (clean)"), 27, . TEXT ("Electric Guitar (muted)"), 2g, TEXT ("Overdriven Guitar"), 29, TEXT ("Distortion Guitar"), 30, TEXT ("Guitar Harmonics"), 31, TEXT ("Bass"), TEXT ("Acoustic Bass"), 32, TEXT ("Electric Bass (finger)"), 33, TEXT ("Electric Bass (pick)"), 34, 1212 Częœć III: Zagadnienia zaawansowane (cigg dalszy ze strony 1211) TEXT ("Fretless Bass"), 35, TEXT ("Slap Bass 1"), 36, TEXT ("Slap Bass 2"), 37, TEXT ("Synth Bass 1"), 38, TEXT ("Synth Bass 2"), 39, TEXT ("Strings"). TEXT ("Violin"), 40, TEXT ("Uiola"), 41, TEXT ("Cello"), 42, TEXT ("Contrabass"), 43, TEXT ("Tremolo Strings"). 44, TEXT ("Pizzicato Strings"), 45. TEXT ("Orchestral Harp"), 46, TEXT ("Timpani"), 47, TEXT ("Ensemble"), TEXT ("String Ensemble 1"), 48, TEXT ("String Ensemble 2"). 49, TEXT ("Synth Strings 1"), 50, TEXT ("Synth Strings 2"), 51, TEXT ("Choir Aahs"), 52, TEXT ("Uoice Oohs"), 53, TEXT ("Synth Voice"). 54, TEXT ("Orchestra Hit"), 55, TEXT ("Brass"). TEXT ("Trumpet"), 56, TEXT ("Trombone"), 57, TEXT ("Tuba"), 58, TEXT ("Muted Trumpet"), 59, TEXT ("French Horn"), 60, TEXT ("Brass Section"), 61. TEXT ("Synth Brass 1"), 62, TEXT ("Synth Brass 2"), 63, TEXT ("Reed"), TEXT ("Soprano Sax"), 64, TEXT ("Alto Sax"), 65, TEXT ("Tenor Sax"), 66, TEXT ("Baritone Sax"), 67. TEXT ("Oboe"), 68, TEXT ("English Horn"), 69, TEXT ("Bassoon"), 70, TEXT ("Clarinet"), 71, TEXT ("Pipe"), TEXT ("Piccolo"), 72, TEXT ("Flute "), 73, TEXT ("Recorder"), 74, 1'EXT ("Pan Flute"). 75, Rozdziat 22: Diwięk i muryka 1213 TEXT ("Bottle Blow"), 76, TEXT ("Shakuhachi"), 77. TEXT ("Whistle"), 78, TEXT ("Ocarina"), 79. TEXT ("Synth Lead"), TEXT ("Lead 1 (square)"), 80, TEXT ("Lead 2 (sawtooth)"), 81, TEXT ("Lead 3 (caliope lead)"). 82. TEXT ("Lead 4 (chiff lead)"), 83, TEXT ("Lead 5 (charang)"), 84, TEXT ("Lead 6 (voice)"). 85, TEXT ("Lead 7 (fifths)"), 86, TEXT ("Lead 8 (brass + lead)"), 87. TEXT ("Synth Pad"), TEXT ("Pad 1 (new age)"). 88. TEXT ("Pad 2 (warm)"), 89. TEXT ("Pad 3 (polysynth)"), 90. TEXT ("Pad 4 (choir)"), 91, TEXT ("Pad 5 (bowed)"), 92, TEXT ("Pad 6 (metallic)"). 93, TEXT ("Pad 7 (halo)"), 94, TEXT ("Pad 8 (sweep)"), 95, TEXT ("Synth Effects"), TEXT ("FX 1 (rain)"), 96. TEXT ("FX 2 (soundtrack)"), 97, TEXT ("FX 3 (crystal)"), 98. TEXT ("FX 4 (atmosphere)"), 99. TEXT ("FX 5 (brightness)"). 100, TEXT ("FX 6 (goblins)"). 101, TEXT ("FX 7 (echoes)">, 102. TEXT ("FX 8 (sci-fi)"), 103, TEXT ("Ethnic"). TEXT ("Sitar"), 104, TEXT ("Banjo"). 105, TEXT ("Shamisen"), 106, TEXT ("Koto"), 107. TEXT ("Kalimba"). 109. TEXT ("Bagpipe"). 109, TEXT ("Fiddle"), 110, TEXT ("Shanai"), 111, TEXT ("Percussive"), TEXT ("Tinkle Bell"), 112, TEXT ("Agogo"). 113, TEXT ("Steel Drums"), 114, TEXT ("Woodblock"), 115, TEXT ("Taiko Drum"), 116, TEXT ("Melodic Tom"), 117, TEXT ("Synth Drum"), 118, 1214 Częœć III: Zagadnienia zaawansowane (cišg dalszy ze strony 1213) TEXT ("Reverse Cymbal"), 119, TEXT ("Sound Effects"), TEXT ("Guitar Fret Noise"), 120, TEXT ("Breath Noise"), 121, TEXT ("Seashore"), 122, ů TEXT ("Bird Tweet"), 123, TEXT ("Telephone Ring"), 124, TEXT ("Helicopter"), 125, TEXT ("Applause"), 126, TEXT ("Gunshot"), 127 ) ; // // dane do konwersji kodów skaningowych na oktawy i nuty // lldefine NUMSCANS (sizeof key / sizeof key[07) struct ( int i0ctave ; int iNote ; int yPos ; int xPos ; TCHAR * szKey ; ) key [] = ( // skan znak okt. nuta // - - ţ - -1, -1, -1, -1, NULL, // 0 brak -l, -1, -i, -1, NULL, // 1 Esc -1. -1, 0. 0, TEXT (""), // 2 1 5, 1, 0, 2, TEXT ("Cll"), ll 3 2 5 C4l 5, 3, 0, 4, TEXT ("Dţţ"), // 4 3 5 Dţţ -1. -l, 0, 6, TEXT (""), // 5 4 5, 6, 0, 8, TEXT ("Fll"), ll 6 5 5 Fll 5, 8, 0, 10, TEXT ("Gll"), ll 7 6 5 Gll 5, 10, 0, 12, TEXT ("A4l"), // g 7 5 All -1. -1, 0, 14, TEXT (""), // g g 6. i, 0, 16, TEXT ("Cll"), // 10 9 6 ClE 6, 3. 0, 18, TEXT ("Dll"), // 11 0 6 Dlf -1, -1, 0, 20, TEXT (""), // 12 - 6, 6, 0, 22, TEXT ("Fll"), // 13 - 6 Fll -1, -1, -1, -1, NULL, // 14 Back -1, -1, -1, -1, NULL, // 15 Tab 5, 0, l, 1, TEXT ("C"), // 16 q 5 C 5, 2, 1, 3, TEXT ("D"), // 17 w 5 D 5, 4, 1, 5, TEXT ("E"), // 18 e 5 E 5. 5, 1, 7, TEXT ("F"), // 19 r 5 F ' 5. 7. 1, 9. TEXT ("G"), // 20 t 5 G 5. 9. 1, 11, TEXT ("A"), // 21 y 5 A 5, 11, 1. 13, TEXT ("B"), // 22 u 5 B 6, 0, 1, 15, TEXT ("C"), // 23 i 6 C Rozdział 22: DŸwięk i muryka 1215 6, 2, 1, 17, TEXT ("D"), // 24 0 6 D 6, 4, 1, 19, TEXT ("E"), // 25 p 6 E 6, 5, 1, 21, TEXT ("F"), // 26 [ 6 F 6, 7, 1, 23, TEXT ("G"), // 27 ] 6 G -1, -l, -1, -1, NULL, // 28 Ent -1, -1, -1, -1, NULL, // 29 Ctrl ' 3, 8, 2, 2, TEXT ("Gţţ"), // 30 a 3 Gţţ 3, 10, 2, 4, TEXT ("Aţţ"), // 31 s 3 Aţţ -1. -1, 2, 6, TEXT ("">, // 32 d 4, 1, 2, 8, TEXT ("Cţţ"), // 33 f 4 Cţţ 4, 3, 2, 10, TEXT ("Dţţ"), // 34 g 4 Dţţ -1, -1, 2, 12, TEXT (""), // 35 h 4, 6, 2, 14, TEXT ("Fţţ"), // 36 j 4 Fţţ 4, 8, 2, 16, TEXT ("Gţţ"), // 37 k 4 Gţţ 4, 10, 2, 18, TEXT ("Aţţ"), // 38 1 4 Aţţ -1, -1, 2, 20, TEXT ("">, // 39 5, 1, 2, 22, TEXT ("Cţţ"), // 40 ' 5 Cţţ -l, -l, -1, -l, NULL, // 41 Ž -1, -1, -l, -1, NULL, // 42 Shift -1, -l, -l, -1, NULL, // 43 \ (nie kontynuacja wiersza) 3, 9, 3, 3, TEXT ("A"), // 44 z 3 A 3, 11, 3, 5, TEXT ("B"), // 45 x 3 B 4, 0, 3, 7, TEXT ("C"), // 46 c 4 C 4, 2, 3, 9, TEXT ("D"), // 47 v 4 D 4, 4, 3, 11, TEXT ("E"), // 48 b 4 E 4, 5, 3, 13, TEXT ("F"), // 49 n 4 F 4, 7, 3, 15, TEXT ("G"), // 50 m 4 G 4, 9, 3, 17, TEXT ("A"), // 51 , 4 A 4, 11, 3, 19, TEXT ("B"), // 52 . 4 B 5, 0, 3, 21, TEXT ("C") // 53 / 5 C l: int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) , ( MSG msg; HWND hwnd ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ţ CS VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = GetStockObject (WHITĘ BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) , ( MessageBox (NULL; TEXT ("This program requires Windows NT!"), szAppName, MBţICONERROR) ; return 0 ; 1216 Częœć III: Zagadnienia zaawansowane (cigg dalszy ze strony 1215) hwnd = CreateWindow (szAppName, TEXT ("Keyboard MIDI Player"), WS_OVERLAPPEDWINDOW ţ WS_HSCROLL ţ WS-USCROLL, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CWţUSEDEFAULT, NULL, NULL, hInstance, NULL) ; r if (!hwnd) return 0 ; ShowWindow (hwnd. iCmdShow) ; UpdateWindow (hwnd); while (GetMessage (&msg, NULL, 0, 0)) ( TranslateMessage (&msg) ; DispatchMessage (&msg) ; r return msg.wParam ; ) // // Tworzenie menu programu (procedura wywoływania z WndProc, WM CREATE) // - HMENU CreateTheMenu (int iNumDevs) ( TCHAR szBuffer [32] ; HMENU hMenu, hMenuPopup, hMenuSubPopup ; int i, iFam, iIns ; MIDIOUTCAPS moc ; hMenu = CreateMenu () ; // tworzenie menu wl./wyl. hMenuPopup = CreateMenu () ; AppendMenu (hMenuPopup, MF_STRING, IDM_OPEN, TEXT ("&Open")) ; AppendMenu (hMenuPopup, MF_STRING ţ MF_CHECKED, IDM CLOSE, TEXT ("&Closed")) ; AppendMenu (hMenu, MF_STRING ţ MF_POPUP, (UINT) hMenuPopup, TEXT ("&Status")) ; // Tworzenie menu Device hMenuPopup = CreateMenu () ; // umieszczenie urzšdzenia MIDI Mapper w menu if (!midiOutGetDevCaps (MIDIMAPPER, &moc, sizeof (moc))) AppendMenu (hMenuPopup, MF STRING, IDMţDEVICE + (int) MIDIMAPPER, moc.szPname) ; else iDevice = 0 ; // i dodanie pozostalych urzšdzeń Rozdział 22: DŸwięk i muryka 1217 ; for (i = 0 ; i < iNumDevs ; i++) ( midiOutGetDevCaps (i, &moc, sizeof (moc)) ; AppendMenu (hMenuPopup, MF STRING, IDM DEVICE + i, moc.szPname) ; ) CheckMenuItem (hMenuPopup, 0, MFţBYPOSITION ţ MF_CHECKED) ; AppendMenu (hMenu, MFţSTRING ţ MF_POPUP, (UINT) hMenuPopup, TEXT ("&Device")) ; // utworzenie menu Channel hMenuPopup = CreateMenu () ; for (i = 0 ; i < 16 ; i++) wsprintf (szBuffer, TEXT ("%d"), i + 1) ; AppendMenu (hMenuPopup, MFţSTRING ţ (i ? MF_UNCHECKED : MF CHECKED), IDM CHANNEL + i, szBuffer) ; ) AppendMenu (hMenu, MF_STRING ţ MF_POPUP, (UINT) hMenuPopup, TEXT ("&Channel")) ; // utworzenie menu Voice hMenuPopup = CreateMenu () ; for (iFam = 0 ; iFam < 16 ; iFam++) ( hMenuSubPopup = CreateMenu () ; for (iIns = 0 ; iIns < 8 ; iIns++) ( wsprintf (szBuffer, TEXT ("&%d.\t%s"), iIns + l, ' fam[iFam].inst[iIns].szInst) ; AppendMenu (hMenuSubPopup, MFţSTRING ţ (famCiFam].instCiIns].iVoice ? MF_UNCHECKED : MF_CHECKED), fam[iFam].inst[iIns].iVoice + IDM VOICE, szBuffer) ; wsprintf (szBuffer, TEXT ("&%c.\t%s"), 'A' + iFam, fam(iFam].szFam) ; AppendMenu (hMenuPopup, MF_STRING ţ MFţPOPUP, (UINT) hMenuSubPopup, szBuffer) ; ) AppendMenu (hMenu, MF_STRING ţ MF_POPUP, (UINT) hMenuPopup, TEXT ("&Voice")) ; return hMenu ; 1 // // procedury upraszczajdce korzystanie z MIDI // DWORD MidiOutMessage (HMIDIOUT hMidi, int i5tatus, int iChannel, 1218 Częœć IIIţ Zagadnienia zaawansowane (cigg dalszy ze strony 1217) I int iDatal, int iData2) ( DWORD dwMessage ; dwMessage = iStatus ţ iChannel ţ (iDatal Ž 8) ţ (iData2 Ž 16) ; r return midiOutShortMsg (hMidi, dwMessage) ; l DWORD MidiNoteOff (HMIDIOUT hMidi, int iChannel, int i0ct, int iNote, int iVel) ( return MidiOutMessage (hMidi, 0x080, iChannel, 12 * i0ct + iNote, iVel) ; 1 r DWORD MidiNoteOn (HMIDIOUT hMidi, int iChannel, int i0ct, int iNote, int iVel) ( return MidiOutMessage (hMidi, 0x090, iChannel, 12 * i0ct + iNote, iVel) ; r ) DWORD MidiSetPatch (HMIDIOUT hMidi, int iChannel, int iVoice) ( return MidiOutMessage (hMidi, OxOCO, iChannel, iVoice, 0) ; DWORD MidiPitchBend (HMIDIOUT hMidi, int iChannel, int iBend) ( return MidiOutMessage (hMidi, 0x0E0, iChannel, iBend & Ox7F, iBend Ż 7) ; ) // // rysowanie pojedynczego klawisza w oknie // VOID DrawKey (HDC hdc, int iScanCode, BOOL fInvert) , RECT rc ; rc.left = 3 * cxCaps * key[iScanCode].xPos / 2 + xOffset ; rc.top = 3 * cyChar * key[iScanCode].yPos / 2 + y0ffset ; rc.right = rc.left + 3 * cxCaps ; rc.bottom = rc.top + 3 * cyChar / 2 ; SetTextColor (hdc, fInvert ? 0xOOFFFFFFu1 : 0x00000000u1) ; SetBkColor (hdc, fInvert ? 0x00000000u1 : 0xOOFFFFFFuI) ; FillRect (hdc, &rc, GetStockObject (fInvert ? BLACK BRUSH : WHITĘ BRUSH)) ; DrawText (hdc, key[iScanCdde].szKey, -1, &rc, DTţSINGLELINE ţ DT CENTER ţ DTţUCENTER) ; FrameRect (hdc, &rc, GetSt.ockObject (BLACK BRUSH)) ; r // // przetwarzanie komunikatdw Key Up i Key Down // Rozdział 22: Dfwięk i muryka 1219 VOID ProcessKey (HDC hdc, UINT messa9e, LPARAM lParam) ( int iScanCode, i0ctave, iNote ; iScanCode = OxOFF & HIWORD (lParam) ; if (iScanCode >= NUMSCANS) // bez kodów 53 return ; if ((i0ctave = key[iScanCode].i0ctave) = -1) // klawisz niemuzyczny return ; if (GetKeyState (VK_SHIFT) < 0) i0ctave += 0x20000000 & lParam ? 2 : 1 ; if (GetKeyState (VK_CONTROL) < 0) i0ctave -= 0x20000000 & lParam ? 2 : 1 ; iNote = key(iScanCode].iNote ; if (message = WM KEYUP) // klawisz w górę ( MidiNoteOff (hMidiOut, iChannel, i0ctave, iNote, 0) ; // Note Off DrawKey (hdc, iScanCode, FALSE) ; ? return ; ) if (0x40000000 & lParam) // ignoruj powtarzanie return ; MidiNoteOn (hMidiOut, iChannel, i0ctave, iNote, iVelocity) ; // Note On DrawKey (hdc, iScanCode, TRUE) ; // narysuj w negatywie // // procedura okna // LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ( static BOOL bOpened = FALSE ; ! HDC hdc ; HMENU hMenu ; int i, iNumDevs, iPitchBend, cxClient, cyClient ; MIDIOUTCAPS moc ; PAINTSTRUCT ps ; SIZE size ; TCHAR szBuffer (16] ; switch (message) t case WMţCREATE: r // pobranie rozmiaru dużych liter czcionki systemowej hdc = GetDC (hwnd) ; GetTextExtentPoint (hdc, TEXT ("M"). 1, &size) ; cxCaps = size.cx ; 1220 Częœć III: Zagadnienia zaawansowane (cišg dalszy ze strony 1219) cyChar = size.cy ; I ReleaseDC (hwnd, hdc) ; // inicjacja paska głoœnoœci SetScrollRange (hwnd, SB_HORZ, 1, 127, FALSE) ; SetScrollPos (hwnd, SBţHORZ, iVelocity, TRUE) ; // inicjacja paska wysokoœci tonu SetScrollRange (hwnd, SBţVERT, 0, 16383, FALSE) ; SetScrollPos (hwnd, SBţVERT, 8192, TRUE) ; // pobranie liczby wyj. urzddzeń MIDI i utworzenie menu if (0 = (iNumDevs = midiOutGetNumDevs ())) I MessageBeep (MBţICONSTOP) ; MessageBox (hwnd, TEXT ("No MIDI output devices!"), SzAppName, MB OK ţ MBţICONSTOP) ; return -1 ; ) SetMenu (hwnd, CreateTheMenu (iNumDevs)) ; return 0 ; case WM SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; xOffset = (cxClient - 25 * 3 * cxCaps / 2> / 2 ; y0ffset = (cyClient - 11 * cyChar) / 2 + 5 * cyChar ; return 0 ; case WM_COMMAND: ' hMenu = GetMenu (hwnd) ; // polecenie Otwarte if (LOWORD (wParam) = IDM OPEN && !bOpened) if (midiOutOpen (&hMidiOut, iDevice, 0, 0, 0)) f MessageBeep (MB_ICONEXCLAMATION) ; MessageBox (hwnd, TEXT ("Cannot open MIDI device"), szAppName, MB OK ţ MBţICONEXCLAMATION) ; t else t CheckMenuItem (hMenu, IDM_OPEN, MF_CHECKED) ; CheckMenuItem (hMenu, IDMţCLOSE, MF UNCHECKED) ; , MidiSetPatch (hMidiOut, iChannel, iVoice) ; bOpened = TRUE ; ) Rozdziat 22: Dtwięk i muryka 1221 ; // polecenie Zamknięte else if (LOWORD (wParam) = IDM CLOSE && bOpened) ( CheckMenuItem (hMenu, IDM_OPEN, MF UNCHECKED) ; CheckMenuItem (hMenu, IDMţCLOSE, MF CHECKED) ; ? // wyldczenie wszystkich nut i zamknięcie urzddzenia for (i = 0 ; i < 16 ; i++) MidiOutMessage (hMidiOut, OxBO, i, 123, 0) ; midiOutClose (hMidiOut) ; bOpened = FALSE ; 1 ' // zmiana urzddzenia MIDI (z menu) else if (LOWORD (wParam) >= IDM DEVICE - 1 && LOWORD (wParam) < IDM CHANNEL) ( CheckMenuItem (hMenu, IDM_DEVICE + iDevice, MF UNCHECKED) ; iDevice = LOWORD (wParam) - IDM_DEVICE ; CheckMenuItem (hMenu, IDMţDEVICE + iDevice, MFţCHECKED) ; // zamknięcie i ponowne otwarcie urzddzenia MIDI if (bOpened) t SendMessa9e (hwnd, WM COMMAND, IDM CLOSE, OL) ; SendMessage (hwnd, WM COMMAND, IDM OPEN, OL) ; // zmiana kanalu MIDI (z menu) else if (LOWORD (wParam) >= IDM_CHANNEL && LOWORD (wParam) < IDMţVOICE) ( CheckMenuItem (hMenu, IDM CHANNEL + iChannel, MF UNCHECKED); iChannel = LOWORD (wParam) - IDM CHANNEL ; ! CheckMenuItem (hMenu, IDMţCHANNEL + iChannel, MF CHECKED) ; if (bOpened> MidiSetPatch (hMidiOut, iChannel, iVoice) ; 1 // zmiana instrumentu (z menu) else if (LOWORD (wParam) >= IDM VOICE) CheckMenuItem (hMenu, IDMţVOICE + iVoice, MF UNCHECKED) ; ! iVoice = LOWORD (wParam) - IDM_VOICE ; CheckMenuItem (hMenu, IDMţVOICE + iVoice, MFţCHECKED) ; if (bOpened) MidiSetPatch (hMidiOut, iChannel, iVoice) ; ) 1222 Częœć III: Zagadnienia zaawansowane (cišg dalszy ze strony 1221) InvalidateRect (hwnd, NULL, TRUE) ; return 0 ; // przetwarzanie komunikatów Key Up i Key Down case WM_KEYUP: r case WM_KEYDOWN: ` hdc = GetDC (hwnd) ; if (bOpened) ProcessKey (hdc, message, lParam) ; ReleaseDC (hwnd, hdc) ; return 0 ; // [Esc]: wylšczyć wszystkie nuty i odœwieżyć okno case WM_CHAR: ! if (bOpened && wParam == 27) f for ( i = 0 ; i < 16 ; i++) MidiOutMessage (hMidiOut, OxBO, i, 123, 0) ; InvalidateRect (hwnd, NULL, TRUE) ; ) return 0 ; // pasek poziomy: prędkoœć case WM HSCROLL: switch (LOWORD (wParam)) case SB_LINEUP: iVelocity -= 1 ; break ; case SB_LINEDOWN: iVelocity += 1 ; break ; case SB_PAGEUP: iVelocity -= 8 ; break ; case SB_PAGEDOWN: iVelocity += g ; break ; case SB THUMBPOSITION: iVelocity = HIWORD (wParam) ; break ; default: return 0 ; ) iVelocity = max (1, min (iVelocity, 127)) ; SetScrollPos (hwnd, SB HORZ, iVelocity, TRUE) ; return 0 ; // pasek pionowy: wysokoœć dŸwięku case WM VSCROLL: switch (LOWORD (wParam)) ( case SB THUMBTRACK: iPitchBend = 16383 - HIWORD (wParam) ; break ; case SB THUMBPOSITION: iPitchBend = 8191 ; break ; default: return 0 ; iPitchBend = max (0, min (iPitchBend, 16383)) ; SetScrollPos (hwnd, SBţVERT, 16383 - iPitchBend, TRUE) ; if (bOpened) MidiPitchBend (hMidiOut, iChannel, iPitchBend) ; T Rozdział 22: DŸwięk i muryka 1223 return 0 ; case WMţPAINT: hdc = BeginPaint (hwnd, &ps) ; for (i = 0 ; i < NUMSCANS ; i++) if (keyCi7.xPos != -1) DrawKey (hdc, i, FALSE) ; midiOutGetDevCaps (iDevice, &moc, sizeof (MIDIOUTCAPS)) ; wsprintf (szBuffer, TEXT ("Channel %i"), iChannel + 1) ; TextOut (hdc, cxCaps, 1 * cyChar, bOpened ? TEXT ("Open") : TEXT ("Closed"), bOpened ? 4 : 6) ; TextOut (hdc. cxCaps, 2 * cyChar, moc.szPname, lstrlen (moc.szPname)) ; TextOut (hdc, cxCaps, 3 * cyChar, szBuffer, lstrlen (szBuffer)) TextOut (hdc, cxCaps, 4 * cyChar, famCiVoice / 87.instCiVoice % 87.szInst, lstrlen (famCiVoice / 87.instCiVoice % 87.szInst)) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY : SendMessage (hwnd, WM COMMAND, IDM CLOSE, OL) ; PostOuitMessage (0) ; return 0 ; ) return DefWindowProc (hwnd, message, wParam, lParam) ; Rysunek 22-10. Program KBMIDI Program KBMIDI uruchamia się w oknie przedstawiajšcym graficznie, jak kla- wisze klawiatury komputera odpowiadajš klawiszom tradycyjnego instrumen- tu, takiego jak pianino czy organy. Klawisz [Z], w lewym dolnym rogu, odtwa- rza dŸwięk A (o częstotliwoœci 110 Hz). Przechodzšc wzdłuż dolnej krawędzi klawiatury, dojdziesz do œrodkowego C po prawej stronie (w drugim wierszu od , dołu znajdujš się krzyżyki i bemole). Kontynuację skali (od C do Gis) znajdziesz w dwóch gómych wierszach. Cała klawiatura obejmuje więc 3 oktawy. Naciœnię- cie klawisza [Ctrl] opuszcza całoœć o 1 oktawę, a klawisza [Shift] podnosi o 1 okta- wę, co daje w etekcie 5 oktaw. Jeżeli zechcesz od razu wypróbować program i zagrać coœ, niczego nie usłyszysz. Najpierw trzeba z menu Status urzšdzenia wybrać polecenie Otwarte. Powoduje ono otwarcie wyjœciowego urzšdzenia MIDI. Jeżeli port zostanie otwarty z po- , wodzeniem, naciœnięcie klawisza spowoduje wysłanie komunikatu Note On przez MIDI do syntezatora, a zwolnienie - wysłanie komunikatu Note Off. W zależno- ; œci od parametrów twojej klawiatury, przy sprzyjajšcym układzie będziesz w sta- nie zagrać kilka nut jednoczeœnie. Urzšdzenie MIDI zamyka polecenie Close z menu Status. Jest to wygodny spo- sób, jeżeli chce się uruchomić jakieœ inne oprogramowanie MIDI w Windows bez kończenia działania programu KBMIDI. 1224 Częœć III: Zagadnienia zaawansowane W menu Device znajduje się lista zainstalowanych urzšdzeń wyjœciowych MIDI. Sš one uzyskiwane za pomocš funkcji midiOutGetDevCaps. Jednym z nich będzie najprawdopodobniej port wyjœciowy MIDI, prowadzšcy do zewnętrznego syn- tezatora (który może, ale nie musi być podłšczony). Lista zawiera również urzš- dzenie MIDI Mapper. Jest to syntezator MIDI wybrany w aplecie Multimedia z Pa- nelu sterowania. Za pomocš menu Channel wybiera się kanał MIDI (od 1 do 16). Domyœlnie za- znaczony jest kanał numer 1. Do wybranego tu kanału wysyłane będš wszystkie komunikaty MIDI generowane przez program KBMIDI. Ostatrue menu nazywa się Voice. Jest to podwójnie zagnieżdżone menu, z które- go można wybrać jeden ze 128 instrumentów zdefiniowanych w specyfikacji General MIDI, a więc zaimplementowanych w Windows. 128 instrumentów po- dzielono na 16 rodzin, z których każda zawiera 8 barw. Instrumenty te nazywa się barwami melodycznymi, ponieważ różne numery klawiszy MIDI odpowia- dajš różnym wysokoœciom dŸwięków. W ramach General MIDI zdefiniowano także liczne instrumenty niemelodyczne - perkusyjne. Aby użyć instrumentów perkusyjnych, należy wybrać kanał numer 10 i z menu Voice wybrać pierwszš barwę (Acoustic Grand Piano). Teraz każdy klawisz będzie grał inny instrument perkusyjny. Dostępnych jest 47 różnych dŸwięków perkusyjnych, od klawisza MIDI numer 35 (B dwie oktawy poniżej œrodkowego C) do klawisza numer 81 (A prawie dwie oktawy wyżej niż œrodko- we C). Z kanału instrumentów perkusyjnych skorzystamy jeszcze niebawem w programie DRUM. Program KBMIDI ma poziomy i pionowy pasek przewijania. Ponieważ kompu- tery osobiste nie sš wyposażone w klawiatury dynamiczne, prędkoœć naciskania klawiszy można definiować za pomocš poziomego paska przewijania. Parametr ten można w przybliżeniu nazwać głoœnoœciš. Bajty danych odpowiadajšce usta- wieniu poziomego paska przewijania wysyłane sš we wszystkich komunikatach Note On. Pionowy pasek przewijania generuje komunikat MIDI o nazwie Pitch Bend. Aby z niego skorzystać, naciœnij jakiœ klawisz i przesuń suwak paska myszš. Podno- szenie suwaka sprawia, że częstotliwoœci nut rosnš, a opuszczanie, że malejš. Puszczenie suwaka powoduje powrót wysokoœci tonu do normalnego poziomu. Manipulacje paskami nie należš do najprostszych: podczas nich komunikaty kla- wiaturowe rue przechodzš przez pętlę komunikatów. Dlatego jeœli naciœniesz kla- wisz, zaczniesz manipulować jednym z pasków myszš i w trakcie tej czynnoœci zwolnisz klawisz, nuta będzie nadal brzmiała. Z tego powodu nie powinno się podczas przesuwania pasków naciskać ani zwalniać klawiszy. Podobna zasada ma zastosowanie do menu - nie próbuj wybierać poleceń menu, majšc wciœnię- ty klawisz. Nie zmieniaj też oktawy (klawiszami [Shift] czy [Ctrl]) między naci- œnięciami i zwolnieniami klawiszy. Jeżeli jakaœ nuta nie przestanie grać mimo zwolnienia klawisza, można jš wyłš- czyć za pomocš [Esc]. Klawisz ten wyłšcza wszystkie dŸwięki, wysyłajšc 16 ko- munikatów All Notes Off do każdego z 16 kanałów syntezatora MIDI. Rozdział 22: DŸwięk i muryka 1225 W KBMIDI nie zastosowano skryptu zasobów. Program tworzy menu dynamicz- nie. Nazwy urzšdzeń sš pobierane za pomocš funkcji midiOutGetDevCaps, a na- zwy instrumentów i ich rodzin - przechowywane w dużej strukturze danych. W programie znalazło się kilka funkcji upraszczajšcych wysyłanie komunikatów MIDI. Omówiłem już wszystkie komunikaty oprócz Pitch Bend. W jego argumen- tach występujš dwie 7-bitowe liczby okreœlajšce łšcznie 14-bitowy poziom wyso- koœci tonu. Liczby od 0 do OxlFFF zmniejszajš wysokoœć dŸwięku, a od 0x2001 do Ox3FFF zwiększajš jš. Wybranie polecenia Otwarte z menu Status urzšdzenia powoduje wywołanie funkcji midiOutOpen dla wybranego urzšdzenia, a następrue funkcji MidiSetPatch. Zmiana urzšdzenia musi być poprzedzona zamkruęciem poprzedniego (jeżeli było otwarte) i musi poprzedzać otwarcie nowego urzšdzenia. kiedy użytkownik zmierua urzšdzenie MIDI, kanał MIDI lub instrument, KBMIDI musi również wywołać MidiSetPatch. Program generuje komunikaty WMţKEYUP i WMţKEYDOWN, włšczajšce i wy- łšczajšce nuty. Musi także zapewniać konwersję kodów klawiaturowych na okta- wy i nuty. Na przykład klawisz [Z] (w układzie klawiatury English US) ma kod 44, a więc program przekształci ten kod na 3 oktawę i nutę 9 (dŸwięk A). W funkcji MidiNoteOn programu KBMIDI obie te liczby sš zmieniane w numer klawisza MIDI 45 (12 razy 3 plus 9). Ten sam sposób konwersji jest wykorzystywany do rysowania klawiszy w oknie: każdy klawisz ma jakieœ współrzędne poziome i pio- nowe oraz napis. Obsługa poziomego paska przewijania jest nieskomplikowana: wystarczy zapi- sać nowš prędkoœć naciskania i ustawić nowe położenie paska. Przypadek pio- nowego paska, sterujšcego wysokoœciš dŸwięku, jest jednak nieco nietypowy. Jedyne obsługiwane dla niego polecenia to SB THUMBTRACK (komunikat ten jest wysyłany na skutek manipulacji paskiem za pomocš myszy) i SB THUMB- POSITION (komunikat informujšcy o zakończeniu manipulacji). Przetwarzanie polecenia SB THUMBPOSITION polega na ustawieniu suwaka w œrodkowym (domyœlnym) położeniu i wywołaniu funkcji MidiPitchBend z wartoœciš 8192. Automat perkusyjny MIDI Niektóre instrumenty perkusyjne, takie jak ksylofon czy timpani, nazywa się "melodycznymi" albo "chromatycznymi", ponieważ za ich pomocš można grać różne melodie. Ksylofon to pewien rodzaj drewnianych cymbałów (każdy daje wydŸwięk o innej wysokoœci). Timpani też można nastroić. Instrumenty te, po- dobnie jak kilka innych melodycznych instrumentów perkusyjnych, można wy- brać z menu Voice programu KBMIDI. Jest jednak wiele instrumentów niemelodycznych. Nie można ich stroić i wydajš często zbyt wiele szumu, aby można je było skojarzyć z dŸwiękiem o okreœlonej wysokoœci. W specyfikacji General MIDI sš one dostępne przez kanał 10. Każde- mu z 47 różnych numerów klawiszy odpowiada inny instrument perkusyjny. Porużej, na rysunku 22-11, znajduje się listing programu o nazwie DRUM. Jest to programowa symulacja automatu perkusyjnego. Program pozwala konstruować 32-nutowe sekwencje perkusyjne z 47 różnych barw perkusyjnych. Sekwencja jest automatycznie zapętlona i można zmieniać jej tempo oraz głoœnoœć. 1226 Częœć Iil: Zagadnienia zaawansowane DRUM.C /* 1 DRUM.C - Automat perkusyjny MIDI (c) Charles Petzold, 1998 */ ţţinclude ţţinclude ţţinclude ţţinclude ;ţinclude "drumtime.h" ţţinclude "drumfile.h" ţţinclude "resource.h" LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; BOOL CALLBACK AboutProc (HWND, UINT, WPARAM, LPARAM) ; void DrawRectangle (HDC, int, int, DWORD * DWORD *) ; void ErrorMessage (HWND, TCHAR *, TCHAR *) ; void DoCaption (HWND, TCHAR *) ; int AskAboutSave (HWND, TCHAR *) ; TCHAR * szPerc [NUM PERC] = ( TEXT ("Acoustic Bass Drum"), TEXT ("Bass Drum 1"), TEXT ("Side Stick"), TEXT ("Acoustic Snare"), TEXT ("Hand Clap"), TEXT ("Electric Snare"), TEXT ("Low Floor Tom"), TEXT ("Closed High Hat"), TEXT ("High Floor Tom"), TEXT ("Pedal High Hat"), TEXT ("Low Tom"), TEXT ("Open High Hat"), TEXT ("Low-Mid Tom"), TEXT ("High-Mid Tom"), TEXT ("Crash Cymbal 1"), TEXT ("High Tom"), ; TEXT ("Ride Cymbal 1"), TEXT ("Chinese Cymbal"), TEXT ("Ride Bell"), TEXT ("Tambourine"), TEXT ("Splash Cymbal"), TEXT ("Cowbell"), TEXT ("Crash Cymbal 2"), TEXT ("Vibraslap"), TEXT ("Ride Cymbal 2"), TEXT ("High Bongo"), TEXT ("Low Bongo"), TEXT ("Mute High Conga"), TEXT ("Open High Conga"), TEXT ("Low Conga"), TEXT ("High Timbale"), TEXT ("Low Timbale"), TEXT ("High Agogo"), TEXT ("Low Agogo"), TEXT ("Cabasa"), TEXT ("Maracas"), TEXT ("Short Whistle"), TEXT ("Long Whistle"), TEXT ("Short Guiro"), TEXT ("Long Guiro"), TEXT ("Claves"), TEXT ("High Wood Block"), TEXT ("Low Wood Block"), TEXT ("Mute Cuica"), TEXT ("Open Cuica"), TEXT ("Mute Triangle"), TEXT ("Open Triangle") ; TCHAR szAppName [] = TEXT ("Drum") ; TCHAR szUntitled [] = TEXT ("(Untitled)") ; . TCHAR szBuffer [80 + MAX_PATH] ; HANDLE hInst ; int cxChar, cyChar ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, Rozdział 22: Dtwigk i muryka , PSTR szCmdLine, int iCmdShow) , ' HWND hwnd ; MSG msg ; WNDCLASS wndclass ; hInst = hInstance ; wndclass.style = CS_HREDRAW ţ CSţVREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (hInstance, szAppName) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = GetStockObject (WHITĘ BRUSH) ; ' wndclass.lpszMenuName = szAppName ; wndclass.lpszClassName = szAppName ; ' if (!RegisterClass (&wndclass)) ( MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MBţICONERROR) ; return 0 ; i hwnd = CreateWindow (szAppName, NULL, WS OVERLAPPED ţ WS_CAPTION ţ WS_SYSMENU WS_MINIMIZEBOX ţ WS_HSCROLL ţ WSţVSCROLL, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CWţUSEDEFAULT, NULL, NULL, hInstance, szCmdLine) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; t while (GetMessage (&msg, NULL, 0, 0)) TranslateMessage (&msg) ; DispatchMessage (&msg) ; return msg.wParam ; LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) static BOOL bNeedSave ; static DRUM drum ; static HMENU hMenu ; static int iTempo = 50, iIndexLast ; static TCHAR szFileName [MAXţPATH], szTitleName [MAX PATH] ; HDC hdc ; int i, x, y ; PAINTSTRUCT ps ; POINT point ; RECT rect ; TCHAR * szError ; Czgœć 111: Zagadnienia zaawansowane (cišg dalszy ze strony 1227) switch (message) ( case WM CREATE: // inicjacja struktury DRUM drum.iMsecPerBeat = 100 ; drum.iVelocity - 64 ; drum.iNumBeats - 32 ; DrumSetParams (&drum> ; // inne inicjacje cxChar = LOWORD (GetDialogBaseUnits ()) ; cyChar = HIWORD (GetDialogBaseUnits ()) ; GetWindowRect (hwnd, &rect) ; MoveWindow (hwnd, rect.left, rect.top, 77 * cxChar, 29 * cyChar, FALSE) ; hMenu = GetMenu (hwnd) ; // inicjacja paska gloœnoœci SetScrollRange (hwnd, SB_HORZ, i, 127, FALSE) ; SetScrollPos (hwnd, SB HORZ. drum.iVelocity, TRUE) ; // inicjacja paska tempa SetScrollRange (hwnd, SB VERT, 0, 100, FALSE) ; SetScrollPos (hwnd. SB VERT, iTempo, TRUE) ; DoCaption (hwnd, szTitleName) ; return 0 ; case WM COMMAND: switch (LOWORD (wParam)) ( case IDM_FILE_NEW: if (bNeedSave && IDCANCEL = AskAboutSave (hwnd, szTitleName)) return 0 ; // wyczyszczenie sekwencji for (i = 0 ; i < NUM-PERC ; i++) t drum.dwSeqPerc Ci) = 0 ; drum.dwSeqPian Ci) = 0 ; ) InvalidateRect (hwnd, NULL, FALSE) ; DrumSetParams (&drum) ; bNeedSave = FALSE ; return 0 ; case IDMţFILĘ OPEN: Rozdział 22: DŸwigk i muryka 1229 // zapisanie poprzedniego pliku if (bNeedSave && IDCANCEL = AskAboutSave (hwnd, szTitleName)) return 0 ; // otwarcie wybranego pliku if (DrumFileOpenDlg (hwnd, szFileName, szTitleName)) ( szError = DrumFileRead (&drum, szFileName) ; if (szError != NULL) ( ErrorMessage (hwnd, szError, szTitleName) ; szTitleName [0] = '\0' ) , else ( // ustawienie nowych parametrów iTempo = (int) (50 * (1og10 (drum.iMsecPerBeat) - i)) ; SetScrollPos (hwnd, SB VERT, iTempo, TRUE) ; SetScrollPos (hwnd, SB HORZ, drum.iVelocity, TRUE) ; DrumSetParams (&drum) ; InvalidateRect (hwnd, NULL, FALSE) ; bNeedSave = FALSE ; ) DoCaption (hwnd, szTitleName) ; ) return 0 ; case IDM_FILE_SAVE: case IDM FILEţSAVĘ AS: // zapisanie pliku if ((LOWORD (wParam) == IDM FILE SAVE && szTitleName [0]) ţţ DrumFileSaveDlg (hwnd, szFileName, szTitleName)) ( szError = DrumFileWrite (&drum, szFileName) ; if (szError != NULL) ( ErrorMessage (hwnd, szError, szTitleName) ; szTitleName [0] = '\0' . else bNeedSave = FALSE ; DoCaption (hwnd, szTitleName) ; ) return 0 ; case IDM APP EXIT: 1230 Częœć III: Zagadnienia zaawansowane (cišg dalszy ze strony 1229) SendMessage (hwnd, WM-SYSCOMMAND, SC CLOSE, OL) ; ( return 0 ; case IDM SEQUENCE-RUNNING: // poczdtek sekwencji if (!DrumBeginSequence (hwnd)) ( ErrorMessa9e (hwnd, TEXT ("Could not start MIDI sequence - ") TEXT ("MIDI Mapper device is unavailable!"), szTitleName) ; ) else I CheckMenuItem (hMenu, IDM_SEOUENCE_RUNNING, MF_CHECKED) ; CheckMenuItem (hMenu, IDM SEOUENCE STOPPED, MF UNCHECKED) ; return 0 ; case IDM-SEOUENCE_STOPPED: // koniec na końcu sekwencji DrumEndSequence (FALSE) ; return 0 ; case IDM_APP_ABOUT: DialogBox (hInst, TEXT ("AboutBox"), hwnd, AboutProc) ; return 0 ; return 0 ; case WM_LBUTTONDOWN: case WM_RBUTTONDOWN: hdc = GetDC (hwnd) ; // konwersja wspólrzędnych myszy na wspólrzędne kratek x = LOWORD (lParam) / cxChar - 40 ; y = 2 * HIWORD (lParam) / cyChar - 2 ; // ustawienie nowej liczby uderzeń if (x > 0 && x <= 32 && y < 0) SetTextColor (hdc, RGB (255, 255, 255)) : TextOut (hdc, (40 + drum.iNumBeats) * cxChar, 0, TEXT (":ţ"), 2); i SetTextColor (hdc, RGB (0, 0, 0)) ; J ; if (drum.iNumBeats % 4 == 0) TextOut (hdc, (40 + drum.iNumBeats) * cxChar, 0, TEXT ("."), 1) ; p I drum.iNumBeats = x ; TextOut (hdc, (40 + drum.iNumBeats) * cxChar, 0, TEXT (":ţ"), 2); Rozdział 22: DŸwięk i muryka 1231 ) bNeedSave = TRUE ; // ustawienie lub zlikwidowanie uderzenia w instrument if (x >= 0 && x < 32 && y >= 0 && y < NUM PERC) if (message = WM LBUTTONDOWN) drum.dwSeqPerc[y] ^= (1 Ž x) ; else drum.dwSeqPian[y] ^= (1 Ž x) ; DrawRectangle (hdc, x, y, drum.dwSeqPerc, drum.dwSeqPian) ; bNeedSave = TRUE ; 1 ReleaseDC (hwnd, hdc) ; DrumSetParams (&drum) ; return 0 ; case WM HSCROLL: // zmiana sily (prędkoœci) uderzenia switch (LOWORD (wParam)) case SB_LINEUP: drum.iVelocity -= 1 ; break ; case SB_LINEDOWN: drum.iVelocity += 1 ; break ; case SB_PAGEUP: drum.iVelocity -= 8 ; break ; case SB_PAGEDOWN: drum.iVelocity += g ; break ; case SB THUMBPOSITION: drum.iVelocity = HIWORD (wParam) ; break ; default: return 0 ; drum.iVelocity = max (1, min (drum.iVelocity, 127)) ; SetScrollPos (hwnd, SB HORZ, drum.iVelocity, TRUE) ; DrumSetParams (&drum) ; bNeedSave = TRUE ; return 0 ; case WM-VSCROLL: // zmiana tempa switch (LOWORD (wParam)) ( case SB_LINEUP: iTempo -- 1 ; break ; case SB_LINEDOWN: iTempo += i ; break ; case SB_PAGEUP: iTempo -= 10 ; break ; case SB_PAGEDOWN: iTempo += 10 ; break ; case SB_THUMBPOSITION: iTempo = HIWORD (wParam) ; break ; default: 1232 Częœć III: Zagadnienia zaawansowane (cišg dalszy ze strony 1231) return 0 ; ' ) iTempo = max (0, min (iTempo, 100)) ; SetScrollPos (hwnd, SBţVERT, iTempo, TRUE) ; drum.iMsecPerBeat = (WORD) (10 * pow (100, iTempo / 100.0)) ; DrumSetParams (&drum) ; bNeedSave = TRUE ; return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; SetTextAlign (hdc, TA_UPDATECP) ; SetBkMode (hdc, TRANSPARENT) ; // rysowanie napisów i poziomych linii for (i = 0 ; i < NUMţPERC ; i++) ( MoveToEx (hdc, i & 1 ? 20 * cxChar : cxChar, (2 * i + 3) * cyChar / 4, NULL) ; TextOut (hdc, 0. 0, szPerc [i]. lstrlen (szPerc [i])) : GetCurrentPositionEx (hdc, &point) ; MoveToEx (hdc, point.x + cxChar, point.y + cyChar / 2, NULL) ; LineTo (hdc, 39 * cxChar, point.y + cyChar / 2) ; 1 SetTextAlign (hdc, 0) ; // rysowanie kratek, znacznika powtórzeń i znacznika uderzeń for (x = 0 ; x < 32 ; x++) ( for (y = 0 ; y < NUM_PERC ; y++) DrawRectangle (hdc, x, y, drum.dwSeqPerc, drum.dwSeqPian) ; SetTextColor (hdc, x == drum.iNumBeats - 1 ? RGB (0, 0, 0) : RGB (255, 255, 255)) : TextOut (hdc, (41 + x) * cxChar, 0, TEXT (":ţ"), 2) : SetTextColor (hdc, RGB (0, 0, 0)) ; if (x % 4 - 0) TextOut (hdc, (40 + x) * cxChar, 0, TEXT ("."). 1) ; ) i EndPaint (hwnd, &ps) ; return 0 ; case WM USERţNOTIFY: Rozdział 22: DŸwięk i muryka 1233 // rysowanie skaczšcej piłeczki hdc = GetDC (hwnd) ; SelectObject (hdc, GetStockObject (NULL_PEN)) ; SelectObject (hdc, GetStockObject (WHITE BRUSH)) ; for ( i = 0 ; i < 2 ; i++) ( x = iIndexLast ; y = NUM PERC + 1 ; Ellipse (hdc, (x + 40) * cxChar, (2 * y + 3) * cyChar / 4, (x + 41) * cxChar, (2 * y + 5) * cyChar / 4); iIndexLast = wParam ; SelectObject (hdc, GetStockObject (BLACK BRUSH)) ; J ReleaseDC (hwnd, hdc) ; return 0 ; case WM USERţERROR: ErrorMessage (hwnd, TEXT ("Can't set timer event for tempo"), szTitleName) ; // Ten przypadek zostawiamy niopracowany case WM USER_FINISHED: DrumEndSequence (TRUE) ; CheckMenuItem (hMenu, IDM_SEOUENCE_RUNNING, MF_UNCHECKED) ; CheckMenuItem (hMenu, IDM SEOUENCEţSTOPPED, MF CHECKED) ; return 0 ; case WM_CLOSE: if (!bNeedSave (ţ IDCANCEL != AskAboutSave (hwnd, szTitleName)) DestroyWindow (hwnd) ; return 0 ; case WM QUERYENDSESSION: if (!bNeedSave (ţ IDCANCEL != AskAboutSave (hwnd, szTitleName)) ; return 1L ; return 0 ; case WM DESTROY: DrumEndSequence (TRUE) ; PostQuitMessa9e (0) ; return 0 ; ' return DefWindowProc (hwnd, message, wParam, lParam) ; 1 BOOL CALLBACK AboutProc (HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam) ( switch (message) { case WMţINITDIALOG: 1234 Częœć Iil: Zagadnienia zaawansowane (cišg dalszy ze strony 1233) return TRUE ; case WM COMMAND: switch (LOWORD (wParam)) ( case IDOK: EndDialog (hDlg, 0) ; return TRUE ; ) break ; ) return FALSE ; void DrawRectangle (HDC hdc, int x, int y, DWORD * dwSeqPerc, DWORD * dwSeqPian) { int iBrush ; if (dwSeqPerc Cy] & dwSeqPian Cy] & (1L Ž x)) iBrush = BLACK BRUSH ; else if (dwSeqPerc Cy] & (1L Ž x)) iBrush = DKGRAY BRUSH ; else if (dwSeqPian Cy] & (1L Ž x)) iBrush = LTGRAY BRUSH ; else iBrush = WHITEţBRUSH ; SelectObject (hdc, GetStockObject (iBrush)) ; Rectangle (hdc, (x + 40) * cxChar (2 * y + 4> * cyChar / 4, (x + 41) * cxChar + 1, (2 * y + 6) * cyChar / 4 + 1) ; ? void ErrorMessage (HWND hwnd, TCHAR * szError, TCHAR * szTitleName) ( wsprintf (szBuffer, szError, (LPSTR) (szTitleName CO] ? szTitleName : szUntitled)) ; MessageBeep (MB_ICONEXCLAMATION) ; MessageBox (hwnd, szBuffer, szAppName, MB OK ţ MBţICONEXCLAMATION) ; ) void DoCaption (HWND hwnd, TCHAR * szTitleName) f wsprintf (szBuffer, TEXT ("MIDI Drum Machine - %s"), (LPSTR) (szTitleName CO] ? szTitleName : szUntitled)) ; . SetWindowText (hwnd, szBuffer) ; 0 int AskAboutSave (HWND hwnd, TCHAR * szTitleName) ( int iReturn ; Rozdział 22: DŸwięk i muryka 1235 wsprintf (szBuffer, TEXT ("Save current changes in %s?"), (LPSTR) (szTitleName [0] ? szTitleName : szUntitled)) ; iReturn = MessageBox (hwnd, szBuffer, szAppName, MB YESNOCANCEL ţ MB-ICONOUESTION) ; if (iReturn = IDYES) if (!SendMessage (hwnd, WMţCOMMAND, IDMţFILEţSAVE, 0)) iReturn = IDCANCEL ; return iReturn ; DRUMTIME.H /* Plik naglówkowy DRUMTIME.H dla programu DRUM (z funkcjami zegara) */ ţţdefine NUM_PERC 47 ţţdefine WM USER_NOTIFY (WM_USER + 1) ţţdefine WM_USER_FINISHED (WM_USER + 2) ţţdefine WM USER ERROR (WM USER + 3) ţţpragma pack(push, 2) typedef struct ( short iMsecPerBeat ; short iVelocity ; short iNumBeats ; DWORD dwSeqPerc CNUM_PERC] ; DWORD dwSeqPian CNUMţPERC] ; ) DRUM, * PDRUM ; ţţpragma pack(pop) void DrumSetParams (PDRUM) ; , BOOL DrumBeginSequence (HWND) , = void DrumEndSequence (BOOL) , DRUMTIME.C /* DRUMFILE.C - Procedury zegarowe programu DRUM (c) Charles Petzold, 1998 */ , ţţinclude 4ţinclude "drumtime.h" ţţdefine minmax(a,x,b) (min (max (x, a), b)) 1236 Częœć III: Zagadnienia zaawansowane (cišg dalszy ze strony 1235) ţţdefine TIMERţRES 5 void CALLBACK DrumTimerFunc (UINT, UINT, DWORD, DWORD, DWORD) ; BOOL bSequenceGoing, bEndSequence ; DRUM drum ; HMIDIOUT hMidiOut ; HWND hwndNotify ; int iIndex ; UINT uTimerRes, uTimerID ; DWORD MidiOutMessage (HMIDIOUT hMidi, int iStatus, int iChannel, int iDatal, int iData2) ( DWORD dwMessage ; dwMessage = iStatus ţ iChannel ţ (iDatal Ž 8) ţ (iData2 Ž 16) ; return midiOutShortMsg (hMidi, dwMessage) ; void DrumSetParams (PDRUM pdrum) CopyMemory (&drum, pdrum, sizeof (DRUM)) ; ) BOOL DrumBeginSequence (HWND hwnd) ( TIMECAPS tc ; hwndNotify = hwnd ; // zapamiętanie uchwytu okna dla powiadomień DrumEndSequence (TRUE) ; // zatrzymanie bieżacej sekwencji // otwarcie portu wyjœciowego MIDI Mapper if (midiOutOpen (&hMidiOut, MIDIMAPPER, 0, 0, 0)) return FALSE ; // wyslanie komunikatów Program Change dla kanałów 9 i 0 MidiOutMessage (hMidiOut, OxCO, 9, 0, 0) ; ' MidiOutMessage (hMidiOut, OxCO, 0, 0, 0) ; // rozpoczęcie sekwencji przez zaprogramowanie zdarzenia zegara timeGetDevCaps (&tc, sizeof (TIMECAPS)) ; uTimerRes = minmax (tc.wPeriodMin, TIMER RES, tc.wPeriodMax) ; timeBeginPeriod (uTimerRes) ; uTimerID = timeSetEvent (max ((UINT) uTimerRes, (UINT) drum.iMsecPerBeat), uTimerRes, DrumTimerFunc, 0, TIME ONESHOT) ; if (uTimerID == 0) l timeEndPeriod (uTimerRes) ; midiOutClose (hMidiOut) ; Rozdział 22: DŸwięk i muryka 12g7 return FALSE ; ) iIndex = -1 ; bEndSequence = FALSE ; bSequenceGoing = TRUE ; return TRUE ; ) void DrumEndSequence (BOOL bRightAway) f if (bRightAway) if (bSequenceGoing) ( // zatrzymanie zegara if (uTimerID) timeKillEvent (uTimerID) ; timeEndPeriod (uTimerRes) ; // wylšczenie wszystkich nut MidiOutMessage (hMidiOut, OxBO, 9, 123, 0) ; MidiOutMessage (hMidiOut, OxBO, 0, 123, 0) ; // zamknięcie portu MIDI midiOutClose (hMidiOut) ; bSequenceGoing = FALSE ; } } else bEndSequence = TRUE ; l void CALLBACK DrumTimerFunc (UINT uID, UINT uMsg, DWORD dwUser, DWORD dwl, DWORD dw2) ( static DWORD dwSeqPercLast [NUM PERC], dwSeqPianLast [NUM PERC] ; int i ; // wysłanie komunikatów Note Off dla kanalów 9 i 0 if (iIndex != -1) ( for (i = 0 ; i < NUM PERC ; i++) if (dwSeqPercLastCi] & 1 Ž iIndex) MidiOutMessage (hMidiOut, 0x80, 9, i + 35, 0) ; if (dwSeqPianLast[i] & 1 Ž iIndex) MidiOutMessage (hMidiOut, 0x80, 0, i + 35, 0) ; 1 ) // zwiększenie indeksu i powiadomienie okna // o uruchomieniu skaczšcej pileczki 1238 Częœć III: Zagadnienia zaawansowane (cišg dalszy ze strony 1237) iIndex = (iIndex + 1) % drum.iNumBeats ; PostMessage (hwndNotify, WM USERţNOTIFY, iIndex, timeGetTime ()) ; // sprawdzenie, czy należy skończyć sekwencję if (bEndSequence && iIndex == 0) ( PostMessage (hwndNotify, WM USER FINISHED, 0, OL) ; return ; ) // wyslanie komunikatów Note On dla kanałów 9 i 0 for (i = 0 ; i < NUM PERC ; i++) if (drum.dwSeqPercCi] & 1 Ž iIndex) MidiOutMessage (hMidiOut, 0x90, 9, i + 35, drum.iVelocity) ; if (drum.dwSeqPian(i] & 1 Ž iIndex) MidiOutMessage (hMidiOut, 0x90, 0, i + 35, drum.iVelocity) ; dwSeqPercLastCi] = drum.dwSeqPercCi] ; dwSeqPianLast[i] = drum.dwSeqPian[i] ; ) // zaprogramowanie nowego zdarzenia zegara uTimerID = timeSetEvent (max ((int) uTimerRes, drum.iMsecPerBeat), uTimerRes, DrumTimerFunc, 0, TIME ONESHOT) ; if (uTimerID = 0) i PostMessage (hwndNotify, WM USER ERROR, 0, 0) ; l DRUMFILE.H /* Plik naglówkowy DRUMFILE.H dla programu DRUM (procedury wejœcia/wyjœcia) */ BOOL DrumFileOpenDlg (HWND', TCHAR *, TCHAR *) ; BOOL DrumFileSaveDlg (HWND, TCHAR *, TCHAR *) ; TCHAR * DrumFileWrite (DRUM *, TCHAR *) ; TCHAR * DrumFileRead (DRUM *, TCHAR *) : DRUMFILE.C /* DRUMFILE.C - Procedury wejœcia/wyjœcia programu DRUM (c) Charles Petzold, 1998 */ Rozdział 22: DŸwięk i muryka 1239 ţţinclude ţţinclude ţţinclude "drumtime.h" ţţinclude "drumfile.h" OPENFILENAME ofn = ( sizeof (OPENFILENAME) ) ; TCHAR * szFilter[] = ( TEXT ("Drum Files (*.DRM)"), TEXT ("*.drm"), TEXT ("") } ; TCHAR szDrumID [] = TEXT ("DRUM") ; TCHAR szListID [] = TEXT ("LIST") ; TCHAR szInfoID [] = TEXT ("INFO") ; TCHAR szSoftID [] = TEXT ("ISFT") ; TCHAR szDateID [] = TEXT ("ISCD") ; TCHAR szFmtID [] = TEXT ("fmt ") ; TCHAR szDataID [] = TEXT ("data") ; char szSoftware [] = "DRUM by Charles Petzold, Programming Windows" , TCHAR szErrorNoCreate [] = TEXT ("File %s could not be opened for writing."); TCHAR szErrorCannotWrite [] = TEXT ("File %s could not be written to. ") ; TCHAR szErrorNotFound [] = TEXT ("File %s not found or cannot be opened.") ; TCHAR szErrorNotDrum [] = TEXT ("File %s is not a standard DRUM file.") ; TCHAR szErrorUnsupported [] = TEXT ("File %s is not a supported DRUM file.") ; TCHAR szErrorCannotRead [] = TEXT ("File %s cannot be read.") ; BOOL DrumFileOpenDlg (HWND hwnd, TCHAR * szFileName, TCHAR * szTitleName) ( ofn.hwndOwner = hwnd ; ofn.lpstrFilter = szFilter [0] ; ofn.lpstrFile = szFileName ; ofn.nMaxFile = MAX PATH ; ofn.lpstrFileTitle = szTitleName ; ofn.nMaxFileTitle = MAX_PATH ; ofn.Flags = OFN_CREATEPROMPT ; ofn.lpstrOefExt = TEXT ("drm") ; return GetOpenFileName (&ofn) ; ) BOOL DrumFileSaveDlg (HWND hwnd, TCHAR * szFileName, TCHAR * szTitleName) ofn.hwndOwner = hwnd ; ofn.lpstrFilter = szFilter [0] ; ofn.lpstrFile = szFileName ; ofn.nMaxFile = MAX PATH ; ofn.lpstrFileTitle = szTitleName ; ofn.nMaxFileTitle = MAX_PATH ; ofn.Flags = OFN_OVERWRITEPROMPT ; ofn.lpstrOefExt = TEXT ("drm") ; return GetSaveFileName (&ofn) ; . TCHAR * DrumFileWrite (DRUM * pdrum, TCHAR * szFileName) ( char szDateBuf [16] ; HMMIO hmmio ; 1240 Częœć III: Zagadnienia zaawansowane (cišg dalszy ze strony 1239) int iFormat = 2 ; MMCKINFO mmckinfo C3] ; SYSTEMTIME st ; WORD wError = 0 ; memset (mmckinfo, 0, 3 * sizeof (MMCKINFO)) ; // ponowne utworzenie pliku dla zapisu if ((hmmio = mmio0pen (szFileName, NULL, MMIO_CREATE ţ MMIO WRITE ţ MMIO ŽLLOCBUF)) == NULL) return szErrorNoCreate ; // utworzenie kawałka "RIFF" typu "CPDR" mmckinfoCO].fccType = mmioStringToFOURCC (szDrumID, 0) ; wError ţ= mmioCreateChunk (hmmio, &mmckinfoCO], MMIO CREATERIFF) ; // utworzenie podkawałka "LIST" typu "INFO" mmckinfoClJ.fccType = mmioStringToFOURCC (szInfoID, 0) ; wError ţ= mmioCreateChunk (hmmio, &mmckinfoCl], MMIOţCREATELIST) ; // utworzenie podpodkawałka "ISFT" mmckinfoC27.ckid = mmioStringToFOURCC (szSoftID, 0) ; wError ţ= mmioCreateChunk (hmmio, &mmckinfoC27, 0) ; wError ţ= (mmioWrite (hmmio, szSoftware, sizeof (szSoftware)) != sizeof (szSoftware>) ; wError ţ= mmioAscend (hmmio, &mmckinfoC2], 0) ; // utworzenie łańcucha zegara GetLocalTime (&st) ; wsprintfA (szDateBuf, "%04d-%02d-%02d", st.wYear, st.wMonth, st.wDay) ; // utworzenie podpodkawałka "ISCD" mmckinfoC2].ckid = mmioStringToFOURCC (szDateID, 0> ; wError ţ= mmioCreateChunk (hmmio, &mmckinfoC27, 0) ; wError ţ= (mmioWrite (hmmio, szDateBuf, (strlen (szOateBuf) + 1)) != (int) (strlen (szDateBuf) + 1)) ; wError ţ= mmioAscend (hmmio, &mmckinfoC2], 0) ; wError ţ= mmioAscend (hmmio, &mmckinfoCl], 0) ; // utworzenie podkawałka "fmt " mmckinfoCl].ckid = mmioStringToFOURCC (szFmtID, 0) ; wError ţ= mmioCreateChunk (hmmio, &mmckinfoCl], 0) ; wError ţ= (mmioWrite (hmmio, (PSTR) &iFormat, sizeof (int)) != sizeof (int)) ; Rozdział 22: DŸwięk i muryka 1241 wError ţ= mmioAscend (hmmio, &mmckinfo[1], 0) ; // utworzenie podkawalka "data" mmckinfo[1].ckid = mmioStringToFOURCC (szOataID, 0) ; wError ţ= mmioCreateChunk (hmmio, &mmckinfo[1], 0) ; wError ţ= (mmioWrite (hmmio, (PSTR) pdrum, sizeof (DRUM)) != sizeof (DRUM)) ; wError ţ= mmioAscend (hmmio, &mmckinfo[17, 0) ; wError ţ= mmioAscend (hmmio, &mmckinfo[0], 0) ; // sprzdtanie i powrót wError ţ= mmioClose (hmmio, 0) ; if (wError) ( mmio0pen (szFileName, NULL, MMIO_DELETE) ; return szErrorCannotWrite ; ) return NULL ; } TCHAR * DrumFileRead (DRUM * pdrum, TCHAR * szFileName) f DRUM drum ; HMMIO hmmio ; int i, iFormat ; MMCKINFO mmckinfo [3] ; ZeroMemory (mmckinfo, 2 * sizeof (MMCKINFO)) ; // otwarcie pliku if ((hmmio = mmio0pen (szFileName, NULL, MMIO_READ)) == NULL) return szErrorNotFound ; // odnajdywanie kawalka "RIFF" typu "DRUM" mmckinfo[0].ckid = mmioStringToFOURCC (szOrumID, 0) ; if (mmio0escend (hmmio, &mmckinfo[0], NULL, MMIO FINDRIFF)) mmioClose (hmmio, 0) ; return szErrorNotDrum ; 1 // odnalezienie, odczytanie i zweryfikowanie podkawalka "fmt " mmckinfo[1].ckid = mmioStringToFOURCC (szFmtID, 0) ; if (mmioDescend (hmmio, &mmckinfo[1], &mmckinfo[0], MMIO FINDCHUNK)) ( mmioClose (hmmio, 0) ; return szErrorNotDrum ; 1 1242 Częœć III: Zagadnienia zaawansowane (cišg dalszy ze strony 1241) if (mmckinfoCl].cksize != sizeof (int)) ( mmioClose (hmmio, 0) ; return szErrorUnsupported ; 1 if (mmioRead (hmmio, (PSTR) &iFormat, sizeof (int)) != sizeof (int)) t mmioClose (hmmio, 0) ; return szErrorCannotRead ; ) if (iFormat != 1 && iFormat != 2) ( mmioClose (hmmio, 0) ; return szErrorUnsupported ; ) // na koniec podkawalka "fmt " mmioAscend (hmmio, &mmckinfoCl], 0) ; // odnalezienie, odczytanie i zweryfikowanie podkawałka "data " mmckinfoCl].ckid = mmioStringToFOURCC (szDataID, 0) ; if (mmioDescend (hmmio, &mmckinfoCl], &mmckinfoCO], MMIOţFINDCHUNK)) ( mmioClose (hmmio, 0) ; return szErrorNotOrum ; if (mmckinfoCl7.cksize != sizeof (DRUM)) ( mmioClose (hmmio, 0) ; return szErrorUnsupported ; ) if (mmioRead (hmmio, (LPSTR) &drum, sizeof (DRUM)) != sizeof (DRUM)) ( mmioClose (hmmio, 0) ; return szErrorCannotRead ; ) // zamknięcie pliku mmioClose (hmmio, 0) ; // konwersja formatu 1 na format 2 i skopiowanie struktury DRUM if (iFormat = 1) ( for (i = 0 ; i < NUM_PERC ; i++) ( drum.dwSeqPerc Ci] = drum.dwSeqPian Ci] ; drum.dwSeqPian Ci] = 0 ; Rozdziaf 22: Dfwięk i muryka 1243 ) memcpy (pdrum, &drum, sizeof (DRUM)) ; return NULL ; ) DRUM.RC (fragmenty) //Microsoft Developer Studio generated resource script. ţţinclude "resource.h" ţţinclude "afxres.h" ///////////////////////////////////////////////////////////////////////////// // Menu DRUM MENU DISCARDABLE BEGIN POPUP "&File" BEGIN MENUITEM "&New". IDM_FILE_NEW MENUITEM "&Open... , IDM_FILE_OPEN MENUITEM "&Save", IDM_FILE_SAVE MENUITEM "Save &As... , IDMţFILEţSAVE ŽS MENUITEM SEPARATOR MENUITEM "E&xit", IDM APPţEXIT END POPUP "&Sequence" BEGIN MENUITEM "&Running", IDM_SEOUENCE_RUNNING MENUITEM "&Stopped", IDMţSEOUENCEţSTOPPED , CHECKED END POPUP "&Help" BEGIN MENUITEM "&About... , IDM ŽPP ABOUT END END ///////////////////////////////////////////////////////////////////////////// // Icon DRUM ICON DISCARDABLE "drum.ico" lllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll // Dialog ABOUTBOX DIALOG DISCARDABLE 20, 20, 160, 164 STYLE DS_MODALFRAME ţ WSţPOPUP ţ WSţCAPTION ţ WS SYSMENU CAPTION "Dialog" FONT 8. "MS Sans Serif" BEGIN DEFPUSHBUTTON "OK",IDOK,54,143,50,14 ICON "DRUM",IDC_STATIC,8,8,21,20 CTEXT "DRUM",IDC_STATIC.34,12,90,8 CTEXT "MIDI Drum Machine",IDCţSTATIC,7,36,144,8 1244 Częœć III: Zagadnienia zaawansowane (cišg dalszy ze strony 1243) CONTROL " ,IDC_STATIC,"Static",SS_BLACKFRAME,8,88,144,46 LTEXT "Left Button:\t\tDrum sounds",IDC_STATIC,l2,92,136,8 LTEXT "Right Button:\t\tPiano sounds",IDC_STATIC,l2,102,136,8 LTEXT "Horizontal Scroll:\t\tVelocity",IDC STATIC,l2,112,136,8 LTEXT "Vertical Scroll:\t\tTempo",IDC_STATIC,l2,122,136,8 CTEXT "Copyright (c) Charles Petzold, 1998",IDC STATIC,8,48, 144,8 CTEXT """Programming Windows,"" 5th Edition",IDCţSTATIC,8,60, 144,8 END RESOURCE.H (fragmenty) // Microsoft Developer Studio generated include file. // Used by Drum.rc ţţdefine IDM_FILE_NEW 40001 ţtdefine IDM_FILE_OPEN 40002 ttdefine IDM_FILE_SAVE 40003 ţţdefine IDMţFILE_SAVE ŽS 40004 ţţdefine IDM_APP_EXIT 40005 ţţdefine IDM_SEQUENCE_RUNNING 40006 ţţdefine IDM SEOUENCE_STOPPED 40007 ţtdefine IDM ŽPP ABOUT 40008 Rysunek 22-11. Program DRUM Kiedy uruchomisz program DRUM, w dwóch kolumnach po lewej stronie okna zobaczysz 47 różnych instrumentów perkusyjnych wymienionych z nazwy. Po prawej strorue znajduje się dwuwymiarowa macierz, w której instrumenty krzy- żujš się z kwantami czasu sekwencji. Każdy instrument jest skojarzony z innym wierszem macierzy. Macierz jest kwadratowa: ma 32 kolumny i 32 kwanty. Jeżeli 32 kwanty rozmieœci się z takcie 4/4 (cztery czwarte), to każdy z nich będzie re- prezentował trzydziestodwójkę. Kiedy z menu Sequence wybierze się polecenie Running, program spróbuje otwo- rzyć urzšdzenie MIDI Mapper. Jeżeli operacja ta zakończy się niepowodzeniem, zostanie wyœwietlone okno z komunikatem. W przeciwnym razie u dołu okna programu pojawi się mała skaczšca piłeczka, informujšca o poszczególnych od- twarzanych dŸwiękach perkusyjnych. Klikajšc lewym przyciskiem myszy dowolne miejsce w macierzy, można zazna- czyć instrument, którego odtwarzarue będzie miało miejsce we wskazanym punk- cie sekwencji. Kwadrat zmieni kolor na ciemnoszary. Używajšc prawego przyci- sku myszy, można dodać rówrueż nuty pianina. Kwadrat zmieni kolor na jasno- szary. Jeżeli kliknie się oboma przyciskami, jednoczeœnie lub jeden po drugim, kwadrat zmieni kolor na czarny. Skutkiem tej operacji będzie jednoczesne ode- granie dŸwięku pianina i instrumentu perkusyjnego. Ponowne kliknięcie którym- kolwiek z przycisków wyłšcza dany dŸwięk. U góry macierzy, co 4 uderzenia (kwanty) znajduje się kropka. Kropki majš po- móc w lepszym orientowaniu się w położeniach w obrębie macierzy bez cišgłe- Rozdziaf 22: Dtwięk i muryka 1245 go liczenia. W prawym górnym rogu macierzy znajduje się dwukropek i piono- wa kreska, które razem przypominajš znak powtarzania używany w tradycyj- nym zapisie muzycznym. Oznacza to koniec sekwencji. Klikajšc myszš w innym miejscu powyżej macierzy, można zmieruć położenie znaku powtarzania. Sekwen- cja będzie odtwarzana do miejsca wyznaczonego przez znak powtarzania (bez niego). Aby utworzyć rytm walca, znak powtarzania należy umieœcić w miejscu oddalonym od poczštku sekwencji o liczbę kwantów podzielnš przez 3. Poziomy pasek przewijania steruje wartoœciš bajtu decydujšcego o sile naciska- nia klawiszy, dołšczanego do wszystkich komunikatów Note On. Ma on w zasa- dzie wpływ na głoœnoœć, choć zdarza się (w lepszych syntezatorach), że może również wpływać na barwę. Poczštkowo program ustawia suwak siły w pozycji œrodkowej. Pasek pionowy steruje tempem. Jest to pasek ze skalš logarytmicznš o zakresie od 1 sekundy na jedno uderzenie (1 kwant = 1 sekunda), kiedy suwak jest na samym dole, do 10 milisekund na jedno uderzenie w położeniu przeciw- nym. Program poczštkowo ustawia suwak w położeniu œrodkowym, odpowia- dajšcym tempu 100 milisekund (0,1 sekundy) na jedno uderzenie. Menu Plik zawiera polecenia służšce do zapisywania i odczytywania plików z roz- szerzeniem .DRM, definiujšcym format, który sam wymyœliłem. Te pliki małych rozmiarów sš zgodne z formatem ftIFF, zalecanym dla wszystkich plików mul- timedialnych. Opcja O programie z menu Pomoc wyœwietla okno dialogowe zawierajšce zestawienie informacji o programie, w tym o sposobie obsługi pro- gramu za pomocš myszy i o paskach przewijania. Muzykę zatrzymuje się za pomocš opcji Stopped z menu Sequence. Po zakoń- czeniu bieżšcej sekwencji opcja ta zamyka urzšdzenie MIDI Mapper. Multimedialne funkcje zwišzane z czasem Z pewnoœciš zauważyłeœ, że DRUM.C nie wywołuje żadnych funkcji multime- dialnych. Wszystko dzieje się w module DRIJMTIME. Zwykły zegar Windows jest z pewnoœciš prosty w użyciu, ale fatalny z punktu widzenia aplikacji, w których czas jest elementem krytycznym. Jak zobaczyliœmy na podstawie programu BACHTOCC, w odgrywaniu muzyki czas jest elemen- tem krytycznym i zegar Windows nie nadaje się do tego zadania. Aby zapewnić dokładnoœć niezbędnš do odtwarzania sekwencji MIDI w komputerze PC, w in- terfejsie API obsługujšcym multimedia przewidziano zegar wysokiej rozdzielczo- œci. Został on zaimplementowany za poœrednictwem siedmiu funkcji z prefiksem time. Jedna z nich jest zbędna. W programie DRUMTIME zademonstrowano użycie szeœciu użytecznych. Funkcje zegara współpracujš z funkcjš zwrotnš, działajšcš w osobnym wštku. Jest ona wywoływana przez system w zależnoœci od wartoœci opóŸnienia okreœlonej przez program. Korzystajšc z zegara multimediów, definiuje się dwa różne parametry czasowe, oba w milisekundach. Pierwszy jest opóŸnieniem, a drugi rozdzielczoœciš. Roz- dzielczoœć można traktować jako maksymalny błšd, jeszcze do zaakceptowania. Jeżeli okreœli się opóŸnienie równe 100 milisekund przy rozdzielczoœci 10 milise- kund, rzeczywisty czas opóŸnienia może wynieœć od 90 do 110 milisekund. 1246 Częœć III: Zagadnienia zaawansowane Zanim rozpocznie się korzystanie z zegara, należy odczytać możliwoœci zegara. Robi się to tak: , timeGetDevCaps (&timecaps, uSize); Pierwszym argumentem powyższej funkcji jest wskaŸnilc do struktury TIMECAPS, a drugim rozmiar tej struktury. Struktura TIMECAPS ma tylko dwa pola: wPe- riodMin i wPeriodMax. Sš to minimalna i maksymalna rozdzielczoœć obsługiwana przez urzšdzenie zegara. Jeżeli przyjrzeć się tym wartoœciom po wywołaniu ti- meGetDevCaps, okazuje się, że wPeriodMin ma wartoœć l, a wPeriodMax 65 535 , więc ta funkcja może rue wydawać się ważna. Warto jednak odczytać parametry rozdzielczoœci i korzystać z ruch w wywołaniach innych funkcji. Następnym etapem jest wywołanie: timeBeginPeriod (uResolution); w celu zasygnalizowarua najmniejszej rozdzielczoœci zegara, jakiej będzie wymagał program. Wartoœć ta powinna należeć do przedziału zdefiniowanego w struktu- rze TIMECAPS. Wywołanie umożliwia optymalizację pracy urzšdzenia zegara, który po zgromadzeniu niezbędnych informacji od kilku działajšcych programów będzie w stanie zapewniać im najlepszy sposób obsługi. Każde wywołanie funk- cji timeBeginPeriod musi być zwišzane z póŸniejszym wywołaniem funkcji time- EndPeriod, którš niebawem opiszę. Teraz jesteœ gotowy do zaprogramowania zdarzeń zegara: idTimer = timeSetEvent (uDelay, uResolution, CallBackFunc, dwData, uFlag); Jeżeli wystšpi błšd, zwrócona z tego wywołania wartoœć idTimer będzie równa zero. Od tego wywołania po czasie uDelay milisekund Windows wywoła funkcję CalIBackFunc z dozwolonym błędem, okreœlonym przez uResolution. Wartoœć uRe- solution musi być większa lub równa rozdzielczoœci przekazanej do timeBeginPe- riod. Parametr dwData jest wartoœciš definiowanš przez program, przekazywanš póŸniej do funkcji CalIBackFunc. Ostatni parametrem może być albo TIME_ONE- SHOT (aby uzyskać pojedyncze wywołanie funkcji CallBackFunc w uDelay mili- ' sekund), albo TIMĘ PERIODIC (aby uzyskać wiele wywołań funkcji CallBackFunc co uDelay milisekund). Chcšc zatrzymać harmonogram jednorazowy zegara, zanim zostanie.wywołana funkcja CallBackFunc, albo harmonogram okresowy, należy wywołać funkcję: timeKillEvent (idTimer); Harmonogramu jednorazowego nie trzeba zabijać, jeœli funkcja CaIlBackFunc zo- stała już wywołana. Po zakończeniu korzystania z zegara, należy wywołać funk- timeEndPeriod (wResolution); z tym samym argumentem co w przypadku timeBeginPeriod. Dwie pozostałe funkcje także zaczynajš się prefiksem time. Funkcja dwSysTime = timeGetTime (); , zwraca czas systemowy w milisekundach od pierwszego uruchomierua Windows. Funkcja: timeGetSystemTime (&mmtime. uSize); Rozdział 22: DŸwięk i muryka 1247 wymaga jako pierwszego argumentu wskaŸnika do struktury MMTIME, a jako drugiego rozmiaru tej struktury. Struktura MMTIME może w innych okoliczno- œciach posłużyć do odczytarua czasu systemowego w innym formacie niż w mili- sekundach, ale w tych okolicznoœciach nie może. Tak więc funkcja timeGetSystem- Time jest zbędna. W Windows z poziomu funkcji zwrotnej rue można wywołać dowolnej funkcji. Funkcja zwrotna może wywoływać PostMessage, cztery funkcje zegarowe (time- SetEvent, timeKillEvent, timeGetTime i zbędnš timeGetSystemTime), dwie funkcje wyjœciowe MIDI (midiOutShortMsg i midiOutLongMsg) oraz funkcję testowš Out- putDebugStr. Oczywiœcie, zegar multimediów został zaprojektowany specjalnie do odtwarza- nia sekwencji MIDI i ma bardzo ograniczone zastosowanie w innych sytuacjach. Można za pomocš PostMessage informować procedurę okna o zdarzeniach zega- ra, po czym procedura okna może sobie zrobić, co tylko zechce, ale jej reakcja nie będzie tak precyzyjnie umiejscowiona w czasie, jak byłaby umiejscowiona reak- cja samej zegarowej funkcji zwrotnej. Funkcja zwrotna ma pięć parametrów, ale używa tylko dwóch z nich: numeru identyfikacyjnego zegara zwróconego z timeSetEvent oraz wartoœci dwData, prze- kazanej wczeœruej w postaci argumentu do timeSetEvent. Moduł DRUM.C w programie DRUMTIME.C wywołuje funkcję DrumSetParams kilka razy: podczas tworzenia okna programu DRUM, gdy użytkownik kliknie macierz albo zmieni położenie paska przewijania, kiedy program załaduje plik .DRM oraz kiedy czyszczona jest macierz. Jedynym argumentem DrumSetParams jest wskaŸnik do struktury typu DRUM, zdefiniowanej w pliku nagłówkowym DRUMTIME.H. Struktura ta służy do przechowywania długoœci trwania kwan- tu czasu (w milisekundach), siły naciskania klawiszy (w uproszczeniu: głoœno- œci), liczby kwantów w sekwencji, a także dwóch zestawów czterdziestu siedmiu 32-bitowych liczb całkowitych tworzšcych macierz (instrumenty perkusyjne x kwanty czasu). Każda z tych liczb odpowiada jednemu uderzeniu w instrument perkusyjny. Struktura typu DRUM w module DRUM.C jest przechowywana w pa- mięci statycznej, do DrumSetParams zostaje przekazany jedynie wskaŸnik do struk- tury. Funkcja DrumSetParams kopiuje po prostu jej zawartoœć. Aby rozpoczšć działanie sekwencji, DRUM wywołuje funkcję DrumBeginSequen- ce (w DRUMTTME). Jej jedynym argumentem jest uchwyt okna. Służy on do po- wiadamiania. DrumBeginSequence otwiera urzšdzenie wyjœciowe MIDI Mapper i wysyła komunikaty Program Change, wybierajšce instrument numer 0 dla ka- nałów MIDI numer 0 i 9. Instrumenty sš numerowane od zera, więc 9 oznacza w rzeczywistoœci kanał MIDI numer 10, czyli perkusyjny. Drugi kanał (a raczej pierwszy) służy do uzyskiwania nut pianina. Następnie (nadal w DrumBeginSe- guence) wywoływane sš funkcje timeGetDevCaps i timeBeginPeriod. Żšdanš rozdziel- czoœciš, zdefiniowanš za pomocš stałej TIMER RES, jest 5 milisekund, ale zdefi- niowałem makro o nazwie minmax, które ułatwia wyliczanie rozdzielczoœci w gra- nicach uzyskanych z timeGetDevCaps. Następnie wywoływana jest funkcja timeSetEvent, w której okreœlono czas kwan- tu czasu, wyliczonš rozdzielczoœć, funkcję zwrotnš DrumTimerFunc oraz stałš 1248 Częœć III: Zagadnienia zaawansowane TIMĘ ONESHOT. W programie DRUMTIME zegar programowany jest na zda- rzerua jednorazowe, a nie okresowe, aby można było dynamicznie zmieniać tem- po podczas trwania sekwencji. Po wywołaniu funkcji timeSetEvent, kiedy minie już zaprogramowany czas zwłoki, urzšdzenie zegara wywoła funkcję DrumTi- merFunc. Najwięcej dzieje się w funkcji zwrotnej DrumTimerFunc. W zmiennej ilndex zapi- sana jest bieżšca wartoœć kwantu sekwencji. Funkcja zaczyna się od przesłania komunikatów Note Off dla obecnie odgrywanych dŸwięków. Poczštkowa war- toœć ilndex wynoszšca -1 zapobiega tej operacji podczas pierwszego uruchamia- nia sekwencji. Następnie zmienna ilndex jest zwiększana, a jej wartoœć dostarczana do procedu- ry okna programu DRUM za pomocš komunikatu niestandardowego o nazwie WM LISEIRNOTTFY. Argumentowi komunikatu wParam przypisywana jest war- toœć ilndex, aby procedura WndProc z DRUM.C mogła uaktualnić położenie "ska- czšcej piłeczki". Funkcja DrumTimerFunc kończy się wysłaniem komunikatów Note On do kana- łów syntezatora o numerach 0 i 9 (wartoœci z kratek sš zapamiętywane, aby nuty można było wyłšczyć w następnym przebiegu pętli) oraz zaprogramowaniem zegara jednorazowego za pomocš funkcji timeSetEvent. Aby zatrzymać sekwencję, program DRUM wywohzje funkcję DrumEndSequence z jednym argumentem, który może mieć wartoœć TRUE lub FALSE. Jeżeli ma wartoœć TRUE, funkcja DrumEndSequence kończy sekwencję od razu: zabijajšc wszelkie oczekujšce zdarzenia zegara, wywołujšc funkcję timeEndPeriod, wysy- łajšc komunikaty wyłšczajšce wszystkie nuty do dwóch kanałów MIDI i zamy- kajšc wyjœciowy port MIDI. Funkcja DrumEndSequence jest wywoływana z para- metrem TRUE wtedy, gdy użytkownik zdecyduje się zakończyć program. Jeœli jednak wybierze polecenie Stopped z menu Sequence, program wywoła funk- cję DrumEndSequence z argumentem FALSE. W ten sposób sekwencja przed za- trzymaniem zostanie dokończona. Funkcja DrumEndSeguence odpowiada na to wywołanie przypisaniem zmiennej globalnej bEndSequence wartoœci NULL. Jeże- li bEndSeguence ma wartoœć TRUE i indeks uderzeń osišgnie zero, DrumTimerFunc wysyła użytkownikowi do procedury WndProc komunikat niestandardowy WM LTSER FINISHED. WndProc musi nań odpowiedzieć wywołaniem funkcji DrumEndSequence z argumentem TRUE, aby korzystanie z zegara i portu MIDI zakończyło się prawidłowo. Operacje na plikach typu RIFF Program DRUM może również zapisywać i odczytywać pliki zawierajšce dane ze struktury DRUM. Pliki te sš w formacie RIFF (Resource Interchange File For- mat), zalecanym do zastosowań multimedialnych. Pliki RIFF można obsługiwać za pomocš standardowych funkcji wejœcia/wyjœcia, ale proœciej jest skorzystać z funkcji z prefiksem mmio (od ang. multimedia input/output). Jak widzieliœmy na przykładzie pliku .WAV, format RIFF jest typem z tagami. Oznacza to, że dane sš poorganizowane w bloki o różnej długoœci (zwane kawał- kami), z których każdy jest opisany tagiem (etykietš). Tagiem jest po postu 4-baj- Rozdział 22: DŸwięk i muryka 1249 towy łańcuch ASCII. Dzięki temu łatwiej jest porównywać tagi z 32-bitowymi licz- bami całkowitymi. Po każdym tagu występuje długoœć kawałka oraz sam kawa- łek (porcja danych). Formaty z tagami sš uniwersalne, ponieważ danych nie umieszcza się w ustalonych odległoœciach od poczštku pliku i można je zlokali- zować za pomocš tagów. Poza tym format tego typu można łatwo rozszerzyć, dodajšc nowe typy tagów. Programy czytajšce rozszerzony w ten sposób plik się- gajš tylko po te dane, które sš ixn potrzebne, omijajšc tagi, których nie rozumiejš. Plik RIFF składa się z samych kawałków, które stanowiš bloki danych. Kawałek składa się z opisu typu danych, rozmiaru danych i danych. Opis typu danych to 4-znakowy tag ASCII. Nie może zawierać spacji, ale może być uzupełruany spacjami, jeżeli jest krótszy. Rozmiar kawałka to 4-bajtowa (32 bitowa) liczba, oznaczajšca wielkoœć da- nych (bez 8 bajtów narzutu). Dane kawałka muszš zajmować parzystš liczbę bajtów i sš w razie potrzeby uzupełruane dodatkowym zerem. Tak więc każdy składnik kawałka jest wyrównany na granicy słowa z poczštkiem pliku. Rozmiar kawałka nie obejmuje 8 bajtów wymaganych definicjš formatu kawałka (opisujšcych rozmiar oraz typ danych kawałka) ani nie uwzględnia uzupełruania danych. Niektóre typy kawałków mogš mieć taki sam rozmiar niezależnie od pliku. Dzieje się tak wtedy, gdy kawałek danych jest zapisem struktury danych o stałej długo- œci. W pozostałych przypadkach rozmiar kawałka jest zmienny i zależny od kon- kretnego pliku. Sš dwa szczególne typy kawałków: RIFF i LIST. W kawałku RIFF dane zaczynajš się od 4-znakowego opisu typu kawałka, po którym występujš kolejne podka- wałki. Kawałek LIST jest podobny, z tym że dane zaczynajš się od 4-znakowego typu listy. Kawałek RIFF służy jako uogólniony pojemnik na dane typu RIFF, a ka- wałek LIST ma za zadanie konsolidować pokrewne podkawałki. Plik RIFF jest po prostu kawałkiem RIFF, więc zaczyna się od łańcucha znaków "RIFF" i 32-bitowej wartoœci oznaczajšcej wielkoœć pliku minus 8 bajtów. Praw- dę mówišc, plik może być o jeden bajt dłuższy, jeżeli zostanie uzupełniony do parzystej liczby bajtów. Interfejs API dla multimediów zawiera 16 funkcji zaczynajšcych się prefiksem mmio. Sš to pliki przeznaczone specjalnie do obsługi plików RIFF. Kilku z nich użyłem w pliku DRUMFILE.C do odczytywania i zapisywania danych progra- mu DRUM. Aby otworzyć plik za pomocš funkcji mmio, należy najpierw wywołać funkcję mmioOpen, która zwraca uchwyt pliku. Funkcja mmioCreateChunk służy do two- rzenia kawałka w pliku. Nazwa i parametry kawałka sš definiowane za pomocš struktury MMCKINFO. Funkcja mmioWrite zapisuje dane kawałka. Po zapisaniu danych można wywołać funkcję mmioAscend. Przekazywana do mmioAscend struk- tura MMCKINFO musi być tš samš strukturš, która została wczeœniej przekaza- na do mmioCreateChunk w celu utworzenia kawałka. Funkcja mmioAscend działa na zasadzie odejmowania wartoœci z pola dwDataOffset struktury od bieżšcego położenia wskaŸnika pliku ulokowanego na końcu danych kawałka i zapisywa- nia otrzymanej różnicy w czterech bajtach poprzedzajšcych dane. Funkcja mmio- Ascend zajmuje się również uzupełnianiem danych, jeœli rozmiar danych kawał- ka nie jest podzielny przez dwa. 1250 Częœć III: Zagadnienia zaawansowane Pliki RIFF składajš się z zagnieżdżonych poziomów kawałków. Aby funkcja mmioAscend działała prawidłowo, potrzeba wielu struktur MMCKINFO, po jed- nej dla każdego poziomu. Pliki danych programu DRUM majš trzy poziomy za- gnieżdżenia. Dlatego też w funkcji DrumFileWrite w pliku DRUMFILE.C zdefi- niowałem tablicę trzech struktur MMCKINFO. Do jej elementów można sięgać za poœrednictwem identyfikatorów: mmckinfo(0], mmckinfo(1] i mmckinfo(2]. Struk- tura mmckinfo(0] jest wykorzystywana w pierwszym wywołaniu mmioCreateChunk, ' tworzšcym kawałek RIFF typu DRUM. Potem następuje drugie wywołanie mmio- CreateChunk, dla mmckinfo(1], tworzšce kawałek typu LIST z listš typu INFO. Trzecie wywołanie mmioCreateChunk realizowane jest dla struktury mmckinfo(2]. Za jego pomocš tworzony jest kawałek typu ISFT, identyfikujšcy oprogramowa- nie, które utworzyło plik danych. Po wywołaniu mmioWrite zapisujšcym łańcuch szSoftware występuje wywołanie funkcji mmioAscent ze strukturš mmckinfo(2] jako argumentem. Wypełnia ono pole rozmiaru kawałka. Tak otrzymujemy pierwszy kompletny kawałek. Następny jest również zawarty w kawałku LIST. Program wywołuje jeszcze raz mmioCreateChunk, aby utworzyć kawałek typu ISCD (cre- ation data), znów używajšc struktury mmckinfo(2] jako parametru. Po wywołaniu mmioWrite, zapisujšcym dane kawałka, następuje wywołanie mmioAscend używa- jšce struktury mmckinfo(2], które uzupełnia rozmiar kawałka. To kolejny kompletny kawałek i jednoczeœnie koniec kawałka nadrzędnego (typu LIST). Aby więc wy- pełnić pole rozmiaru kawałka LIST, realizowane jest wywołanie mmioAscend, tym razem dla struktury mmckinfo(1], która została wczeœniej użyta właœnie do utwo- rzenia kawałka typu LIST. Aby utworzyć kawałki fmt i data, wywoływana jest funkcja mmioCreateChunk z mmckinfoll]. Po wywołaniach mmioWrite również występuje mmioWrite, tym ra- zem z mmckinfo(1]. W tym momencie wypełnione (uzupełnione) sš już wszystkie rozmiary kawałków oprócz samego kawałka RIFF. Konieczne jest więc jeszcze jedno wywołanie funkcji mmioAscend z parametrem mmckinfo(0]. W programie jest tylko jeszcze jedno wywołanie: mmioClose. ' Może się zdawać, że wywołanie mmioAscend zmienia bieżšce położenie wskaŸ- nika pliku. I rzeczywiœcie tak się dzieje, ale tylko na czas wewnętrznych operacji tej funkcji. Po powrocie z funkcji wskaŸnik zajmuje znów swoje miejsce, na koń- cu kawałka danych (z ewentualnym uwzględrueniem jednobajtowego uzupełnie- nia). Z punktu widzenia aplikacji wszystkie zapisy do pliku odbywajš się od po- czštku do końca sekwencyjnie. Po zakończonym powodzeniem wywołaniu mmioOpen nic w zasadzie nie może pójœć Ÿle, chyba że zabraknie miejsea na dysku. W zmiennej wError akumulowa- ne sš kody błędów z wywołań mmioCreateChunk, mmioWrite, mmioAscend i mmioC- lose. Każde z nich może zakończyć się niepowodzeniem, jeœli zabraknie miejsca na dysku. Jeżeli do tego dojdzie, plik jest usuwany za pomocš mmioOpen ze stałš MMIO DELETE, a do funkcji wywołujšcej zwracany jest komunikat o błędzie. Odczytywanie pliku RIFF jest podobne do tworzenia, tylko że zamiast funkcji mmioWrite wywoływana jest funkcja mmioftead, a zamiast funkcji mmioCreateChunk - funkcja mmioDescend. Zagłębienie się (ang. descend) w kawałek oznacza odnale- zienie kawałka i umieszczenie wskaŸriika pliku po polu oznaczajšcym rozmiar Rozdział 22: DŸwięk i muryka 1251 jego danych (albo po polu typu w przypadku kawałka RIFF lub LIST). Wyjœcie (ang. ascend) z kawałka oznacza natomiast przesunięcie wskaŸnika pliku na ko- niec kawałka. Ani funkcja mmioDescend, ani mmioAscend nie przesuwa wskaŸni- ka pliku wstecz. Wczeœniejsza wersja programu DRUM została opublikowana w "PC Magazine" w 1992 roku. Wtedy Windows obshxgiwał różne poziomy syntezatorów MIDI (na- zywane podstawowymi i rozszerzonymi). Pliki zapisane za pomocš tamtej wer- sji programu majš identyfikator formatu l. Program DRUM z tego rozdziału ksišż- ki ustawia identyfikator typu na 2. Może on odczytywać pliki w starszym forma- cie i konwertować je. Operację tę realizuje procedura DrumFileRead. Rozdział 23 Smak Internetu Internet - ogromna sieć łšczšca komputery z całego œwiata, implementujšca różne protokoły wymiany danych - zmieniła sposób pojmowania wielu aspektów in- formatyki ostatnich lat. Choć serwisy elektroniczne i systemy poczty elektronicz- nej istniały jeszcze przed boomem internetowym, nie były tak atrakcyjne jak dzi- siejsze. Przede wszystkim dlatego, że były ograruczone do trybu tekstowego i sta- nowiły osobne systemy. Do każdego systemu informacyjnego należało na przy- kład wybrać inny numer telefonu (modemu), inaczej trzeba się było logować itd. Każdy system pocztowy pozwalał na wysyłanie i odbieranie poczty wyłšcznie między osobami zapisanymi do ruego. Dziœ wybierajšc jeden numer telefonu, uzyskujemy dostęp do wszystkich ogól- noœwiatowych zasobów Intemetu oraz do uniwersalnego systemu wymiany pocz- ty. Zakres i uniwersalnoœć usług elektronicznych rozszerzyła w istotny sposób sieć World Wide Web, a także zastosowanie hipertekstu, grafiki i multimediów (obejmujšcych dŸwięk, muzykę i wideo). Kompletny podręcznik uwzględniajšcy wszystkie tematy programistyczne Mi- crosoft Windows zwišzane z Internetem zajšłby prawdopodobnie kilka opasłych tomów. W tym rozdziale poruszam dwa typy zagadnień, które mogš się okazać przydatne w małych aplikacjach Microsoft Windows pobierajšcych dane z Inter- netu. Mowa w nim będzie o korzystaniu z systemu WinInet (Windows Internet API) za pomocš API do obsługi gniazd Windows (WinSock od Windows Sockets) oraz z API do obsługi protokołu FTP (File Transfer Protocol). Gniazda Windows Koncepcję gniazd (ang. sockets) opracowano w University of Califomia w Berkeley. Grazda miały uzupełruć system operacyjny UNIX o obshzgę komunikacji sieciowej. Opracowany tam interfejs API znany jest pod nazwš interfejsu gniazd z Berkeley. Gniazda a TCP/IP Gniazda sš generalnie (choć nie tylko) używane łšcznie z protokołami TCP/IP (Transmission Control Protocol/Internet Protocol), które zdominowały komunikację internetowš. Protokół IP (Internet Protocol), stanowišcy składnik TCP/IP, definiu- je pakowanie danych w datagramy, które rue zawierajš informacji nagłówkowych identyfikujšcych adres Ÿródłowy i docelowy danych. Niezawodny transport da- tagramów IP oraz kontrolę błędów zapewnia protokół TCP (Transmission Control Protocol). 1254 Częœć III: Zagadnienia zaawansowane W TCP/IP punkt końcowy komunikacji jest definiowany przez adres IP i numer portu. Adres IP składa się z 4 bajtów identyfikujšcych serwer w Internecie. Ma on tak zwany format z kropkami, który polega na zapisie owych czterech bajtów w postaci liczb dziesiętnych oddzielonych kropkami, na przykład 209.86.105.231. Numer portu identyfikuje konkretnš usługę prowadzonš przez serwer. Niektóre numery portów sš ustandaryzowane, co oznacza, że pewna grupa usług ma nu- mery portów przypisane na stałe. Gniazdo w kontekœcie TCP/IP oznacza punkt końcowy komunikacji TCP/IP Można je więc identyfikować za pomocš adresu IP i numeru portu. Sieciowe usługi zwišzane z czasem Program przykładowy, który za chwilę omówię, łšczy się z serwerem interneto- wym oferujšcym usługę zwanš Time Protocol. Program odczytuje bieżšcš datę i godzinę i synchronizuje z odczytanymi parametrami komputer osobisty, w któ- rym działa. W Stanach Zjednoczonych za utrzymanie prawidłowego czasu (w połšczeniu z innymi biurami z całego œwiata) odpowiedzialny jest National Institute of Stan- dards and Technology (NIST - Narodowy Instytut Norm i Techniki), funkcjonu- jšcy kiedyœ pod nazwš National Bureau of Standards (Narodowego Biura Norm). Dokładny czas jest udostępniany przez publicznych nadawców radiowych, tele- foniczne systemy informacyjne, elektroruczne systemy komputerowe dostępne przez modem oraz przez Internet. Dokumentację wszystkich tych Ÿródeł można znaleŸć na stronie WWW http://www.bldrdoc.gov/timefreq. Nazwa domeny bldr- doc oznacza Boulder, Colorado, siedzibę oddziału NIST zajmujšcego się czasem (Time and Frequency Division). Nas interesuje przede wszystkim usługa o nazwie NIST Network Time Service, której dokładniejszy opis można znaleŸć na stronie WWW http://www.bldr- doc.gov/timefreq/service/nts.htm. Zawiera ona listę dziesięciu serwerów œwiad- czšcych usługi "zegarynkowe" NIST. Pierwszy nazywa się time-a.timefreq.bldr- doc.gov i ma adres IP 132.163.135.130. Napisałem również program, który korzysta z nieintemetowego serwera NIST. Opublikował go "PC Magazine". Jest on dostępny ze strony WWW Ziff-Davis httpJ/www.zdnet.com/pcmag/pctech/content/16/20/ut1620.001.html. Program ten polecam wszystkim, którzy pragnš zgłębić Windows Telephony API. Przez Internet dostępne sš trzy różne usługi. Wszystkie sš opisane w dokumen- tach RFC (Request for Comment), materiałach Ÿródłowych opisujšcych standardy internetowe. Daytime Protocol (RFC-867) udostępnia łańcuch ASCII zawierajšcy dokładnš datę i godzinę. Dokładny format łańcucha ASCII nie jest całkiem stan- dardowy, a to dlatego, że został pomyœlany jako czytelny dla ludzi. Time Proto- col (RFC-868) udostępnia 32-bitowš liczbę oznaczajšcš liczbę sekund od 1 stycz- nia 1900 roku. Ten czas jest w formacie LTTC (który mimo złej kolejnoœci liter skrótu oznacza koordynowany czas uniwersalny, ang. Coordinated Universal Time), bar- dzo podobny do formatu nazywanego kiedyœ GMT lub Greenwich Mean Time - czas z Greenwich. Trzeci protokół nazywa się Network Time Protocol (RFC-1305) i jest doœć złożony. Rozdział 23: Smak Internetu 1255 Dla naszych potrzeb - którymi sš poznanie gniazd i skuteczna aktualizacja cza- ! su komputera osobistego - najlepszy jest Time Protocol. RFC-868 to krótki, dwu- stronicowy dokument, który można streœcić, podajšc trzy kolejne czynnoœci, ja- kie powinien wykonać program, aby uzyskać dokładny czas poprzez TCP: l. Połšczyć się z portem numer 37 serwera oferujšcego usługę. 2. Odebrać 32-bitowy czas. 3. Zamknšć połšczenie. Mamy więc wszystko, co jest potrzebne do napisania aplikacji opartej na techni- ce gniazd, która będzie odczytywała godzinę z serwera-zegarynki. Program NETTIME Windowsowy interfejs API dla gniazd, nazywany powszechnie WinSockiem, jest kompatybilny z API dla gniazd z Berkeley. Oznacza to, że programy systemu UMX korzystajšce z gniazd powinny prawie bez problemów dać się przenieœć do Windows. Implementacja z Windows jest jednak rozszerzona w stosunku do implementacji Berkeley - zmieniona została postać nazw funkcji, które w Win- dows zaczynajš się od prefiksu WSA (WinSock API). Ich przeglšd i opis znajduje się w /Platform SDIC/IVetworking and Distributed Services/Windows Sockets Uersion 2. Program NETTIME, przedstawiony na rysunku 23-1, demonstruje, jak korzystać z WinSock API. NETTIME.C /* NETTIME.C - Program synchronizujdcy zegar systemowy z godzinš odczytanš z usług internetowych (c) Charles Petzold, 1998 */ ilinclude ilinclude "resource.h" 4ldefine WM SOCKET NOTIFY (WM USER + 1) ifdefine ID TIMER 1 LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; BOOL CALLBACK MainDlg (HWND, UINT, WPARAM, LPARAM) ; BOOL CALLBACK ServerDlg (HWND, UINT, WPARAM, LPARAM) ; void ChangeSystemTime (HWND hwndEdit, ULON6 ulTime) ; void FormatUpdatedTime (HWND hwndEdit, SYSTEMTIME * pstOld, SYSTEMTIME * pstNew) ; void EditPrintf (HWND hwndEdit, TCHAR * szFormat, ...) ; HINSTANCE hInst ; HWND hwndModeless ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) 1256 Częœć III: Zagadnienia zaawansowane (cišg dalszy ze strony 1255) static TCHAR szAppName[] = TEXT ("NetTime") ; HWND hwnd ; MSG msg ; RECT rect ; WNDCLASS wndclass ; hInst = hInstance ; wndclass.style = 0 ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI ŽPPLICATION) ; ' wndclass.hCursor = NULL ; wndclass.hbrBackground = NULL ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) ( MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MBţICONERROR) ; return 0 ; hwnd = CreateWindow (szAppName, TEXT ("Set System Clock from Internet"), WS OVERLAPPED ţ WS_CAPTION ţ WS_SYSMENU WS_BORDER ţ WS_MINIMIZEBOX, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CWţUSEDEFAULT, ! NULL, NULL, hInstance, NULL) ; // utworzenie okna potomnego hwndModeless = CreateDialog (hInstance, szAppName, hwnd, MainDlg) ; // rozmiar okna nadrzędnego równy rozmiarowi okna dialogowego // wyœwietlenie obu okien GetWindowRect (hwndModeless, &rect) ; AdjustWindowRect (&rect, WSţCAPTION ţ WSţBORDER, FALSE) ; SetWindowPos (hwnd, NULL, 0, 0, rect.right - rect.left, rect.bottom - rect.top, SWP NOMOVE) ; ShowWindow (hwndModeless, SW SHOW) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; // zwykla pętla komunikatów while (GetMessage (&msg, NULL, 0, 0)) ( if (hwndModeless = 0 ţţ !IsDialogMessage (hwndModeless, &msg)) Rozdział 23: Smak Internetu ( TranslateMessage (&msg) ; DispatchMessage (&msg) ; l l return msg.wParam ; ) LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) f switch (message) ( case WM_SETFOCUS: SetFocus (hwndModeless) ; return 0 ; % case WMţDESTROY: PostOuitMessage (0> ; return 0 ; , return DefWindowProc (hwnd, message, wParam, lParam) ; ) BOOL CALLBACK MainDlg (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) 1 static char szIPAddrC327 = ( "132.163.135.130" 1 ; static HWND hwndButton, hwndEdit ; static SOCKET sock ; static struct sockaddr_in sa ; static TCHAR szOKLabelC327 ; int iError, iSize ; unsigned long ulTime ; WORD wEvent, wError ; WSADATA WSAData ; , switch (message) , case WM_INITDIALOG: hwndButton = GetDlgItem (hwnd, IDOK) ; hwndEdit = GetDlgItem (hwnd, IDCţTEXTOUT) ; return TRUE ; case WM COMMAND: switch (LOWORD (wParam)) ( case IDC_SERVER: DialogBoxParam (hInst, TEXT ("Servers"), hwnd, ServerDlg, (LPARAM) szIPAddr) ; return TRUE ; case IDOK: // wywołanie "WSAStartup" i wyœwietlenie opisu if (iError = WSAStartup (MAKEWORD(2.0), &WSAData)) ( EditPrintf (hwndEdit, TEXT ("Startup error ţţ%i.\r\n"), iError) ; return TRUE ; Częœć III: Zagadnienia zaawansowane (cišg dalszy ze strony 1257) EditPrintf (hwndEdit, TEXT ("Started up %hs\r\n"), WSAData.szDescription); // wywołanie "socket" sock = socket (AF INET, SOCK STREAM. IPPROTO TCP) ; ' if (sock == INVALID SOCKET) ( EditPrintf (hwndEdit, TEXT ("Error creating socket ţl%i.\r\n"), WSAGetLastError ()) ; WSACleanup () ; return TRUE ; , EditPrintf (hwndEdit, TEXT ("Socket %i created.\r\n"), sock) ; // wywolanie funkcji "WSAAsyncSelect" if (SOCKETţERROR = WSAAsyncSelect (sock, hwnd, WM_SOCKET_NOTIFY, FDţCONNECT ţ FD READ)) EditPrintf (hwndEdit, , TEXT ("WSAAsyncSelect error ţl%i.\r\n"), WSAGetLastError ()) ; closesocket (sock) ; WSACleanup () ; return TRUE ; ) // wywolanie "connect" z adresem IP ! // i numerem portu serwera sa.sin family = AFţINET ; sa.sinţport = htons (IPPORT_TIMESERVER) ; sa.sin „ddr.S un.S „ddr = inet addr (szIPAddr) ; connect(sock, (SOCKADDR *) &sa, sizeof (sa)) ; // Funkcja "connect" zwróci SOCKET ERROR i nawet jeżeli // zakończy się powodzeniem, będzie wymagala blokowania. // Poniżej raportowanie tylko blędów niespodziewanych. if (WSAEWOULDBLOCK != (iError = WSAGetLastError ())) t EditPrintf (hwndEdit, TEXT ("Connect error it%i.\r\n"), iError) ; closesocket (sock) ; WSACleanup () ; return TRUE ; ) EditPrintf (hwndEdit, TEXT ("Connecting to %hs..."). szIPAddr) ; // Wynik funkcji "connect" będzie zwrócony // poprzez komunikat WMţSOCKET NOTIFY. Rozdział 23: Smak Internetu 1259 // Ustawienie zegara i zmiana przycisku na "Cancel" SetTimer (hwnd, ID_TIMER, 1000, NULL) ; GetWindowText (hwndButton, szOKLabel, sizeof (szOKLabel) / sizeof (TCHAR>) ; SetWindowText (hwndButton, TEXT ("Cancel")) ; SetWindowLong (hwndButton, GWLţID, IDCANCEL) ; return TRUE ; case IDCANCEL: closesocket (sock) ; sock = 0 ; WSACleanup () ; SetWindowText (hwndButton, szOKLabel) ; SetWindowLong (hwndButton, GWL ID, IDOK) ; KillTimer (hwnd, ID_TIMER) ; EditPrintf (hwndEdit, TEXT ("\r\nSocket closed.\r\n")) ; return TRUE ; case IDC_CLOSE: if (sock) SendMessage (hwnd, WM COMMAND, IDCANCEL, 0) ; DestroyWindow (GetParent (hwnd)) ; return TRUE ; ) return FALSE ; case WM TIMER: EditPrintf (hwndEdit, TEXT (".")) ; return TRUE ; case WM_SOCKET_NOTIFY: wEvent = WSAGETSELECTEVENT (lParam) ; // ie, LOWORD wError = WSAGETSELECTERROR (lParam) ; // ie, HIWORD // przetwarzanie dwóch zdarzeń WSAAsyncSelect switch (wEvent) f // to zdarzenie występuje na skutek wywolania "connect" case FD CONNECT: EditPrintf (hwndEdit, TEXT ("\r\n")) ; if (wError) ( EditPrintf (hwndEdit, TEXT ("Connect error ţţ%i."). wError) ; SendMessage (hwnd, WM COMMAND, IDCANCEL, 0) ; return TRUE ; ) EditPrintf (hwndEdit, TEXT ("Connected to %hs.\r\n"), szIPAddr) ; // Próba odebrania danych. Wywolanie spowoduje błšd // WSAEWOULDBLOCK i zdarzenie FD READ 1260 Częœć III: Zagadnienia zaawansowane (cišg dalszy ze strony 1259) recv (sock, (char *) &ulTime, 4, MSGţPEEK) ; EditPrintf (hwndEdit, TEXT ("Waiting to receive...")) ; return TRUE ; I // do tego zdarzenia dochodzi nawet wtedy, // kiedy można wywołać "recv" case FD_READ: I KillTimer (hwnd, IDţTIMER) ; EditPrintf (hwndEdit, TEXT ("\r\n")) ; if (wError) ( EditPrintf (hwndEdit, TEXT ("FDţREAD error %i.">, wError) ; SendMessage (hwnd, WM COMMAND, IDCANCEL, 0) ; return TRUE ; l // odczytanie czasu i zamiana bajtów iSize = recv (sock, (char *) &ulTime, 4, 0) ; ulTime = ntohl (ulTime) ; EditPrintf (hwndEdit, TEXT ("Received current time of %u seconds ") TEXT ("since Jan. 1 1900.\r\n"), ulTime) ; // zmiana daty systemowej ChangeSystemTime (hwndEdit, ulTime) ; SendMessage (hwnd, WM COMMAND, IDCANCEL, 0) ; return TRUE ; ) return FALSE ; ? return FALSE ; ) BOOL CALLBACK ServerDlg (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ( static char * szServer ; static WORD wServer = IDC SERVER1 ; char szLabel [64) ; switch (message) case WM_INITDIALOG: szServer = (char *) lParam ; CheckRadioButton (hwnd, IDC SERVERl, IDCţSERVERlO, wServer) ; return TRUE ; case WM COMMAND: switch (LOWORD (wParam)) ( case IDC_SERVERl: case IDC_SERVER2: case IDC SERVER3: I Rozdział 23: Smak Internetu 1261 case IDC_SERVER4: case IDC_SERVER5: case IDC_SERVER6: case IDC_SERVER7: case IDC_SERVERB: case IDC_SERVER9: case IDC_SERVERlO: wServer = LOWORD (wParam) ; return TRUE ; case IDOK: GetDlgItemTextA (hwnd, wServer, szLabel, sizeof (szLabel)) ; strtok (szLabel, "(") ; strcpy (szServer, strtok (NULL, ")")) ; EndDialog (hwnd, TRUE) ; return TRUE ; case IDCANCEL: EndDialog (hwnd, FALSE) ; return TRUE ; ) break ; ) return FALSE ; void ChangeSystemTime (HWND hwndEdit, ULONG ulTime) ( FILETIME ftNew ; LARGE_INTEGER li ; SYSTEMTIME stOld, stNew ; GetLocalTime (&stOld) ; stNew.wYear = 1900 ; stNew.wMonth = 1 ; stNew.wDay = 1 ; stNew.wHour = 0 ; stNew.wMinute = 0 ; stNew.wSecond = 0 ; stNew.wMilliseconds = 0 ; SystemTimeToFileTime (&stNew, &ftNew) ; li = * (LARGEţINTEGER *) &ftNew ; li.OuadPart += (LONGLONG) 10000000 * ulTime ; ftNew = * (FILETIME *) &li ; FileTimeToSystemTime (&ftNew, &stNew) ; if (SetSystemTime (&stNew)) ( GetLocalTime (&stNew) ; FormatUpdatedTime (hwndEdit, &stOld, &stNew) ; 1 else EditPrintf (hwndEdit, TEXT ("Could NOT set new date and time.")) ; 1 void FormatUpdatedTime (HWND hwndEdit, SYSTEMTIME * pstOld, SYSTEMTIME * pstNew) ( 12g2 Częœć III: Zagadnienia zaawansowane (cišg dalszy ze strony 1261) TCHAR szDate0ld C64], szTime0ld C64], szDateNew [64], szTimeNew C647 ; GetDateFormat (LOCALE_USER_DEFAULT, LOCALE_NOUSEROVERRIDE ţ DATE_SHORTDATE, pstOld, NULL, szDate0ld, sizeof (szDate0ld)) ; GetTimeFormat (LOCALE_USER_DEFAULT, LOCALE_NOUSEROVERRIDE ţ TIME_NOTIMEMARKER ţ TIME_FORCE24HOURFORMAT, pstOld, NULL, szTime0ld, sizeof (szTime0ld)) ; GetDateFormat (LOCALĘ USER DEFAULT, LOCALĘ NOUSEROVERRIDE ţ DATEţSHORTDATE, pstNew, NULL, szDateNew, sizeof (szDateNew)) ; GetTimeFormat (LOCALE_USER_DEFAULT, LOCALE_NOUSEROVERRIDE ţ TIME_NOTIMEMARKER ţ TIME_FORCE24HOURFORMAT, pstNew, NULL, szTimeNew, sizeof (szTimeNew)) ; EditPrintf (hwndEdit, TEXT ("System date and time successfully changed ") TEXT ("z\r\n\t%s, %s.%03i na\r\n\t%s, %s.%03i."), szDate0ld, szTime0ld, pstOld->wMilliseconds, szDateNew, szTimeNew, pstNew->wMilliseconds) ; ) void EditPrintf (HWND hwndEdit, TCHAR * szFormat, ...) ( TCHAR szBuffer [1024] ; vaţlist pArgList ; vaţstart (pArgList, szFormat) ; wvsprintf (szBuffer, szFormat, pArgList) ; vaţend (pArgList) ; SendMessage (hwndEdit, EM_SETSEL, (WPARAM) -1, (LPARAM) -1> ; SendMessage (hwndEdit, EM_REPLACESEL, FALSE, (LPARAM) szBuffer) ; SendMessage (hwndEdit, EMţSCROLLCARET, 0, 0) ; ) NETTIME.RC (fragmenty) . //Microsoft Developer Studio generated resource script. ilinclude "resource.h" tlinclude "afxres.h" /////////////i/////////////////////////////////////////////////////////////// // Dialog SERVERS DIALOG DISCARDABLE 20, 20, 274, 202 STYLE DS_MODALFRAME ţ WS_POPUP ţ WS_CAPTION ţ WS SYSMENU CAPTION "NIST Time Service Servers" FONT 8, "MS Sans Serif" BEGIN DEFPUSHBUTTON "OK",IDOK,73,181,50,14 Rozdział 23: Smak Internetu 1263 PUSHBUTTON "Cancel",IDCANCEL,l50,181,50,14 CONTROL "time-a.timefreq.bldrdoc.gov (132.163.135.130) NIST, Boulder, Colorado", IDCţSERVERl,"Button",BS AUTORADIOBUTTON,9,7,256,16 CONTROL "time-b.timefreq.bldrdoc.gov (132.163.135.131) NIST, Boulder, Colorado", IDC SERVER2,"Button",BS AUTORADIOBUTTON,9,24,256,16 CONTROL "time-c.timefreq.bldrdoc.gov (132.163.135.132) Boulder, Colorado, , IDC SERVER3,"Button",BS ŽUTORADIOBUTTON,9,41,256,16 CONTROL "utcnist.colorado.edu (128.138.140.44) University of Colorado, Boulder", IDC SERVER4,"Button",BS AUTORADIOBUTTON,9,58,256,16 CONTROL CONTROL CONTROL CONTROL CONTROL CONTROL END "time.nist.gov (192.43.244.18) NCAR, Boulder, Colorado", IDCţSERVER5,"Button",BS AUTORADIOBUTTON,9,75,256,16 "time-a.nist.gov (129.6.16.35) NIST, Gaithersburg, Maryland", IDC SERVER6,"Button",BS ŽUTORADIOBUTTON,9,92,256,16 "time-b.nist.gov (129.6.16.36) NIST, Gaithersburg, Maryland", IDCţSERVER7,"Button",BS AUTORADIOBUTTON,9,109,256,16 "time-nw.nist.gov (131.107.1.10) Microsoft, Redmond, Washington", IDCţSERVER8,"Button",BS AUTORADIOBUTTON,9,126,256,16 "utcnist.reston.mci.net (204.70.131.13) MCI, Reston, Virginia", IDCţSERVER9,"Button",BS AUTORADIOBUTTON,9,143,256,16 "nistl.data.com (209.0.72.7) Datum, San Jose, California", IDCţSERVERlO,"Button",BS AUTORADIOBUTTON,9,160,256,16 NETTIME DIALOG DISCARDABLE 0, 0, 270, 150 STYLE WS_CHILD FONT 8, "MS Sans Serif" BEGIN DEFPUSHBUTTON "Set Correct Time",IDOK,95,129,80,14 PUSHBUTTON "Close",IDC CLOSE,l83,129,80,14 PUSHBUTTON "Select Server... ,IDC_SERVER,7,129,80,14 EDITTEXT IDC_TEXTOUT,7,7,253,110,ES_MULTILINE ţ ES_AUTOVSCROLL ESţREADONLY ţ WS VSCROLL ţ NOT WS TABSTOP END RESOURCE.H (fragmenty) // Microsoft Developer Studio generated include file. // Used by NetTime.rc ţţdefine IDC_TEXTOUT 101 ţţdefine IDC_SERVER1 1001 ţţdefine IDC_SERVER2 1002 ţţdefine IDC_SERVER3 1003 ţţdefine IDC_SERVER4 1004 ţţdefine IDC_SERVER5 1005 ţţdefine IDC SERVER6 1006 1264 Częœć III: Zagadnienia zaawansowane (cišg dalszy ze strony 1263) tldefine IDC_SERVER7 1007 Ildefine IDC_SERVER8 1008 tldefine IDC_SERVER9 1009 ildefine IDC_SERVERlO 1010 ipdefine IDC_SERVER 1011 ţIdefine IDCţCLOSE 1012 Rysunek 23-1. Program NETTIME Program NETTIME tworzy beztrybowe okno dialogowe oparte na wzorcu NET TIME z NETITME.RC. Okno programu jest wymiarowane tak, aby okno beztry- bowe pokrywało cały obszar roboczy programu. Okno dialogowe składa się z pola edycji działajšcego w trybie tylko do odczytu (do którego program wpisuje in- formacje tekstowe) oraz przycisków: Select Server, Set Correct Time i Close. Przy- cisk Close kończy działanie programu. Do przechowywania adresu serwera służy zmienna szIPAddr z MainDlg. Domyœl- nie jest tam wpisany cišg 132.163.135.130. Przycisk Select Server wywołuje okno dialogowe oparte na szablonie SERVERS z pliku NETTIME.RC. Zmienna szIPAddr jest przekazywana jako ostatni argument do DialogBoxParam. Okno dialogowe Serwery zawiera listę dziesięciu serwerów (skopiowanš prawie znak w znak ze strony WWW instytutu NIST) oferujšcych interesujšcš nas usługę. Kiedy użyt- kownik wybierze jeden z nich, ServerDlg przetwarza tekst przycisku, aby uzy- skać adres IP. Nowy adres jest zapisywany w zmiennej szIPAddr. Kiedy użytkownik naciœnie przycisk Set Correct Time, generowany jest komuni- kat WMţCOMMAND z młodszym słowem wParam równym IDOK. Przetwarza- nie IDOK w MainDlg odbywa się tam, gdzie większoœć poczštkowych operacji zwišzanych z gniazdami. Pierwsza funkcja, która musi być wywołana przez każdy program Windows uży- wajšcy Windows Sockets API, to: iError = WSAStartup (wVersion, &WSAData); W NETTIME jej pierwszy argument jest ustawiany na 0x0200 (co oznacza wersję 2.0). Po powrocie struktura WSAData zawiera dane o implementacji Windows Sockets, a NETTIME wyœwietla łańcuch szDescription. W ten sposób otrzymuje- my po prostu informację o wersji. NETTIME wywołuje następnie funkcję socket, mniej więcej tak: sock = socket (AFţINET, SOCK STREAM, IPPROTO TCP); Pierwszym argumentem jest rodzina adresów, oznaczona tutaj jako pewien ro- dzaj adresu internetowego. Drugi argument oznacza, że dane majš zostać zwró- cone w postaci strumienia, a nie w datagramach (dane, których oczekujemy, to tylko 4 bajty; datagramów używa się w przypadku większych porcji danych). Ostatni argument to protokół. W programie używamy protokołu internetowego o nazwie TCP (Transmission Control Protocol), jednego z dwóch wymienionych w dokumencie RFC-868. Zwrotna wartoœć funkcji socket jest zapisywana w zmien- nej typu SOCKET. Jest ona następnie wykorzystywana w póŸniejszych wywoła- niach funkcji. Rozdział 23: Smak Internetu 1265 Program NETTIME wywohzje następnie WSAAsynchSelect, kolejnš funkcję gniaz- dowš charakterystycznš dla Windows, aby uniknšć zawieszenia aplikacji spo- wodowanego długim czasem odpowiedzi Intemetu. W dokumentacji WinSock niektóre funkcje sš nazywane "blokujšcymi". Oznacza to, że te nie oddajš stero- wania do programu, dopóki nie zrealizujš zleconego im zadania. Funkcja WSA- AsyncSelect ma za zadanie wymusić na funkcjach, które normalnie sš blokujšce, aby stały się nieblokujšce, czyli zwracały sterowanie do programu, zanim ukoń- czš wykonywanie operacji. Wynik funkcji nieblokujšcej wraca do programu w postaci komunikatu. Funkcja WSAAsyncSelect pozwala aplikacji okreœlić wartoœć liczbowš komunikatu oraz okno, które ma otrzymać komunikat. Funkcja ma na- stępujšcš ogólnš składnię: WSAAsyncSelect (sock, hwnd, message, iConditions); W NETTIME zastosowano do tego celu komunikat niestandardowy WMţSOC- KET NOTIFY. Ponadto okreœlono za pomocš ostatniego argumentu WSAAsync- Select warunki, w jakich ma zostać wysłany komunikat: nawišzanie połšczenia i odebranie danych (FD CONNECT I FD READ). Kolejna funkcja WinSock w NETTIME to connect. Wymaga ona wskaŸnika do struktury z adresem gniazda, która może być różna dla różnych protokołów. NETTIME używa wersji struktury przeznaczonej dla TCP/IP: struct sockaddr in ( short sin_family; uţshort sinţport; struct in „ddr sinţaddr; char sin zero[8]; ? ; przy czym in „ddr to unia, w której adres internetowy można okreœlić jako 4 baj- . ty, jako 2 zmienne unsigned short lub jako zmiennš unsigned long. W NETTIME pole sinţ'amily jest równe AF INET, co oznacza rodzinę adresów. Pole sinţport ma wartoœć numeru portu, w tym przypadku numeru portu dla protokołu Time Protocol, który w dokumencie RFC-868 okreœlono na 37. Nie na- leży jednak po prostu przypisać temu polu wartoœci 37, jak to pierwotnie zrobi- łem. Jak większoœć liczb przesyłanych przez Intemet, pole z numerem portu musi być w formacie big-endian: najbardziej znaczšcy bit pierwszy. W procesorach Intela stosowany jest format little-endian. Na szczęœcie jest funkcja htons (host-to- network short), która zamienia bajty, więc polu sinţport struktury sockaddr in na- leży przypisać wartoœć: htons (IPPORT TIMESERVER) Powyższa stała jest w WINSOCK2.H zdefiniowana jako 37. Program dokonuje za pomocš funkcji inet „ddr konwersji adresu serwera zapisanego w łańcuchu szIPAddr na liczbę typu unsigned long, którš następnie przypisuje polu sin „ddr tej struktury. Jeżeli jakaœ aplikacja wywołuje w Windows 98 funkcję connect, a Windows nie jest w danej chwili podłšczony do Internetu, ukazuje się okno kreatora połšczeń. Jest to funkcja znana jako AutoDial. Nie ma jej w Windows NT 4.0, jeżeli więc używa się NT, przed uruchomieniem NETTIME należy uaktywnić połšczenie z Internetem. 1266 CzęœE III: Zagadnienia zaawansowane Funkcja connect jest zazwyczaj blokujšca, ponieważ zanim zostanie nawišzane po- łšczenie, upływa na ogół sporo czasu. Ponieważ jednak wywołana została funkcja WSAAsyncSelect, funkcja connect nie będzie czekała na połšczenie, lecz natychmiast zwróci sterowarue i kod błędu SOCKET ERROR. Nie jest to w rzeczywistoœci ża- den błšd - funkcja informuje jedynie w ten sposób o tym, że nie zostało nawišza- ne żadne połšczenie. W NETTIME nie sprawdzamy nawet kodu powrotu, tylko wywołujemy funkcję WSAGetLnstError. Jeżeli zwróci ona wartoœć WSAEWOULD- BLOCK (oznaczajšcš, że spowodowałaby zablokowanie programu, ale tego nie zrobi), oznacza to, że wszystko gra. NETTIME zmienia przycisk Set Correct Time na przycisk Anuluj i ustawia zegar na 1 sekundę. Przetwarzanie komunikatu WMţTIMER polega na wyœwietlaniu w oknie programu kropek, informujšcych użytkowruka, że coœ się dzieje i że program się nie zawiesił. Kiedy w końcu zostanie nawišzane połšczenie, procedura MainDlg otrzyma ko- munikat WM SOCKET NOTIFY - zdefiniowany wczeœniej w programie za pomocš funkcji WSAAsyncSelect. Młodsze słowo IParam będzie równe FD CON- NECT, a starsze może wskazywać na błšd. Błšd na tym etapie oznacza, że pro- gram nie mógł uzyskać połšczenia z serwerem. NETTIME pozwala wybrać je- den z dziesięciu serwerów, więc można sobie popróbować! Jeżeli wszystko pójdzie dobrze, NETTIME wywoła funkcję recv (receive), która ma odebrać dane: recv (sock, (char *) &ulTime, 4, MSGţPEEK); Oznacza to, że do zmiennej ulTime majš trafić 4 bajty. Ostatni argument okreœla, że dane majš zostać tylko odczytane, a nie usunięte z kolejki wejœciowej. Podob- nie jak connect, funkcja recv zwraca kod błędu ozrtaczajšcy, że normalnie funkcja zablokowałaby program, ale nie zrobi tego ze względu na wczeœniejsze zalece- nia programu. Teoretycznie (choć jest to mało prawdopodobne) funkcja może zwrócić tylko częœć danych. Musiałaby wówczas zostać wywołana ponownie w celu pobrania brakujšcych bajtów 32-bitowej wartoœci. Dlatego właœnie jest wy- woływana z opcjš MSG PEEK. Funkcja recv, również podobnie jak connect, generuje komunikat WM SOC- KET NOTIFY, tym razem z kodem zdarzenia równym FD READ. NETTIME odpowiada na to ponownym wywołaniem recv, tym razem z ostatnim argumen- tem 0, aby usunšć dane z kolejki. Teraz opiszę pokrótce, co program robi z otrzy- manš wartoœciš ulTime. Zauważ, że NETTIME kończy przetwarzanie komunika- tu wysłaniem do siebie samego komunikatu WM COMMAND z parametrem wParam równym IDCANCEL. Procedura okna dialogowego odpowiada na to wywołaniem closesocket i WSACleanup. Przypomnij sobie, że 32-bitowa wartoœć ulTime, którš otrzymuje NETITME, jest liczbš sekund, jakie upłynęły od 0:00 UTC 1 stycznia 1900 roku. Ale najbardziej znaczšcy bajt jest pierwszy, więc liczbę trzeba przepuœcić przez funkcję ntohl (network-to- host long), zmieniajšcš kolejnoœć bajtów do postaci akceptowalnej dla mikroproce- sorów Intela. NETTIME wywołuje następnie funkcję ChangeSystemTime. Funkcja ChangeSystemTime zaczyna się od uzyskania bieżšcego czasu lokalnego - to znaczy bieżšcego czasu systemowego wyrażonego w formacie właœciwym dla strefy czasowej użytkownika i uwzględniajšcym czas letni/zimowy. Następ- Rozdział 23: Smak Internetu 1267 nie inicjowana jest struktura SYSTEMTIME. Program przypisuje jej czas godziny zero z 1 stycznia 1900 roku. Struktura SYSTEMTIME jest przekazywana do funk- cji SystemTimeToFileTime, która dokonuje konwersji danych na strukturę FILETI- ME. FILETIME to po prostu dwa podwójne słowa (DWORD, liczby 32-bitowe) tworzšce łšcznie wartoœć 64 bitowš, która oznacza liczbę 100-nanosekundowych jednostek, które upłynęły od 1 stycznia 1601 roku. W funkcji ChangeSystemTime struktura FILETIME jest rzutowana na LARGĘ IN- TEGER, czyli na unię, za pomocš której do liczby 64-bitowej można uzyskać do- stęp poprzez dwie liczby 32-bitowe albo poprzez jednš liczbę 64-bitowš typu intó4 (ten typ to wprowadzone przez Microsoft rozszerzenie standardu ANSI C). Tak więc wartoœć ta stanowi liczbę 100-nanosekundowych jednostek czasu między 1 stycznia 1601 roku i 1 stycznia 1900 roku. Do wartoœci tej dodawana jest liczba 100-nanosekundowych jednostek czasu od 1 stycznia 1900 roku do dziœ -10 000 000 razy ulTime. Wynikowa wartoœć FILETIME jest następnie przekształcana z powrotem na struk- turę SYSTEMTIME (za pomocš wywołania FileTimeToSystemTime). Ponieważ Time Protocol zwraca bieżšcy czas UTC, program NETTIME ustawia czas za pomocš wywołania SetSystemTime, funkcji operujšcej również na czasie w formacie UTC. Aby go wyœwietlić, program uzyskuje zaktualizowany czas, wywołujšc funkcję GetLocalTime. NETTIME przekazuje pierwotny czas lokalny oraz nowy czas do funkcji FormatUpdatedTime, która za pomocš funkcji GetTimeFormat i GetDateFor- mat przekształca obie wartoœci do postaci przystępnych łańcuchów znaków ASCII. Funkcja SetSystemTime może zakończyć się błędnie, jeżeli program zostanie uru- chomiony w Windows NT, a użytkownik nie będzie miał stosownych uprawnień do modyfikowania czasu. W takim przypadku program powiadomi użytkowni- ka o problemie za pomocš komunikatu informujšcego, iż czas systemowy nie został zmieniony. Winlnet i FTP WinInet (Windows Internet) API to zbiór wysokopoziomowych funkcji pomagajš- cych programiœcie korzystać z popularnych protokołów Internetu: HTTP (Hyper- text Transfer Protocol, protokołu World Wide Web), FTP (File Transfer Protocol) i Go- pher (mniej popularny system dostępu do rozproszonych informacji). Składnia funkcji WinInet przypomina składnię zwykłych funkcji plikowych Windows, przez co posługiwanie się tymi protokołami jest prawie tak samo proste jako korzysta- nie z plików z dysku lokalnego. Winlnet API udokumentowano w /Platform SDK/ Internet, Intranet, Extranet Services/Internet Tools and Technologies/Winlnet API. Program przykładowy UPDDEMO zaprezentuje, jak korzystać z elementów Wi- nInet API zapewniajšcych obsługę FTP Wiele firm posiadajšcych strony WWW oferuje również dostęp do anonimowego serwera FTP, z którego użytkownicy mogš œcišgać pliki bez logowania. Jeżeli w polu adresowym programu Internet Explorer wpiszesz na przykład adres ftp://ftp.microsoft.com, uzyskasz dostęp do anonimowego FTP firmy Microsoft. Jeżeli użyjesz adresu ftp://ftp.cpetzold.com/ cpetzold.com/ProgWin/UpdDemo, otrzymasz listę plików dostępnych z mojego 1268 Czgœć III: Zagadnienia zaawansowane anonimowego FTP (zwišzanych z programem przykładowym, który niebawem omówię). Serwery FTP sš dziœ uważane przez wielu użytkowników Internetu za niezbyt przyjazne, ale wcišż sš bardzo przydatne. Program użytkowy może pobierać dane FTP prawie automatycznie, bez interwencji użytkownika. Na podstawie takiego pomyshz napisałem program UPDDEMO (update demonstration), którym zajmie- my się za chwilę. Przeglšd FTP API Program korzystajšcy z WinInet musi w każdym pliku Ÿródłowym z wywoła- niem funkcji WinInet zawierać dyrektywy włšczajšce plik nagłówkowy WINI- NET.H. Musi być ponadto skonsolidowany z bibliotekš WININET.LIB. Sprawę konsolidacji z bibliotekš można załatwić w Microsoft Visual C++, na karcie Link w oknie dialogowym Project Settings. W czasie wykonywania program jest kon- solidowany z bibliotekš dynamicznš WININET.DLL. W poniższym omówieniu nie będę się wdawał w szczegóły dotyczšce składni funkcji, ponieważ niektóre funkcje majš skomplikowanš postać, z wieloma róż- nymi opcjami. Aby rozpoczšć używanie WinInet, możesz wykorzystać kod Ÿró- dłowy programu UPDDEMO jako swego rodzaju ksišżkę kucharskš. Na razie najważniejsze jest zrozumienie poszczególnych etapów zwišzanych z używaniem funkcji FTP. Chcšc skorzystać z dowolnej z funkcji Windows Internet API, należy najpierw wywołać InternetOpen. Protokołów obshxgiwanych przez WinInet można używać dopiero po wywołaniu tej funkcji. InternetOpen zwraca uchwyt do sesji interneto- wej; jego wartoœć należy zapisać w zmiennej typu HINTERNET. Po zakończeniu używania WinInet API uchwyt ten należy zamknšć, wywohxjšc funkcję InternetC- IoseHandle. Aby użyć FTP, należy wywołać funkcję InternetConnect. Wymaga ona uchwytu sesji internetowej uzyskanego z InternetOpen, a zwraca uchwyt do sesji FTP. Tego uchwytu używa się z kolei jako pierwszego argumentu we wszystkich wywoła- niach funkcji z prefiksem Ftp. W argumentach funkcji InternetConnect okreœla się zapotrzebowanie na korzystanie z FTP oraz nazwę serwera, na przykład ftp.cpet- zold.com. Funkcja wymaga również podania nazwy i hasła użytkownika. Jako argumenty te można wysłać wartoœci NULL, jeœli serwer FTP zapewnia dostęp anonimowy. Jeżeli komputer jest podłšczony do Internetu, w chwili wywołania funkcji InternetConnect Windows 98 wyœwietli okno kreatora połšczenia. Po za- kończeniu pracy z FTP program powinien zamknšć uchwyt za pomocš funkcji InternetCIoseHandle. Teraz można rozpoczšć wywoływanie funkcji z prefiksem Ftp. Jak się okaże, sš one bardzo podobne do zwykłych funkcji plikowych Windows. Aby uniknšć powtarzania się tych samych funkcji dla różnych protokołów, przewidziano kil- ka funkcji z prefiksem Internet; można ich również używać do obshxgi FTP. Następujšce cztery funkcje pozwalajš wykonywać operacje na katalogach: Rozdział 23: Smak Internetu 1269 fSuccess = FtpCreateDirectory (hFtpSession, szDirectory); fSuccess = FtpRemoveDirectory (hFtpSession, szDirectory); fSuccess = FtpSetCurrentDirectory (hFtpSession, sz0irectory); fSuccess = FtpGetCurrentDirectory (hFtpSession, szDirectory, &dwCharacterCount); Zauważ, że sš one bardzo podobne do znanych funkcji CreateDirectory, Remove- Directory, SetCurrentDirectory i GetCurrentDirectory, oferowanych przez Windows do obsługi katalogów w lokalnym systemie plików. Aplikacje korzystajšce z anonimowych serwerów FTP nie mogš oczywiœcie ani tworzyć, ani usuwać katalogów. Nie mogš ponadto zakładać, że katalog FTP ma takš samš strukturę drzewiastš, jakš majš systemy plików Windows. W szczegól- noœci program okreœlajšcy katalog za pomocš względnej œcieżki nie powinien czy- nić żadnych założeń dotyczšcych pełnej œcieżki. Po wywołaniu SetCurrentDirectory powinno następować wywołanie GetCurrentDirectory, aby program wiedział na bieżšco, jaka jest pełna œcieżka do wynikowego katalogu. Znakowy argument funkcji GetCurrentDirectorţy powinien mieœcić przynajmniej MA7CţPATH znaków, a ostatni argument - wskazywać na zmiennš zawierajšca właœciwš wartoœć. Poniższe dwie funkcje umożliwiajš usuwanie plików i modyfikowanie ich nazw (nie w przypadku serwerów anonimowych): fSuccess = FtpDeleteFile (hFtpSession, szFileName); fSuccess = FtpRenameFile (hFtpSession, szOldName, szNewName); Istnieje możliwoœć wyszukiwania pliku (lub plików okreœlonych maskš z symbo- lami wieloznacznyrni). W tym celu należy najpierw wywołać funkcję FtpFindFirst- File. Jest ona bardzo podobna do funkcji FindFirstFile i używa nawet tej samej struk- tury WIN32 FINDţDATA. Funkcja zwraca uchwyt pliku. Przekazuje się go do funk- cji InternetFindNextFile, za pomocš której można uzyskać dalsze wystšpienia pli- ków. Na końcu uchwyt należy zamknšć, wywołujšc InternetCloseHandle. Aby otworzyć plik, należy wywołać funkcję FtpFileOpen. Zwraca ona uchwyt (pli- ku), którego można użyć w funkcjach InternetReadFile, InternetReadFileEx, Interne- tWrite i InternetSetFilePointer. Znów na końcu należy zamknšć uchwyt, wywołu- jšc uniwersalnš funkcję InternetCIoseHandle. Teraz jeszcze dwie inne szczególnie przydatne funkcje wysokopoziomowe: Ftp- GetFile umożliwia kopiowanie plików z serwera FTP do lokalnego katalogu. Jej działanie oparte jest na funkcjach: FtpFileOpen, FileCreate, InternetReadFile, Write- File, InternetCIoseHandle i CIoseHandle. Jednym z argumentów funkcji FtpGetFile jest znacznik nakazujšcy funkcji przerwanie działania, jeżeli istnieje już plik lo- kalny o identycznej nazwie. Podobnie działa funkcja FtpPutFile: ta służy jednak do kopiowania pliku lokalnego do serwera FTP, czyli działa w drugš stronę. Program demonstrujšcy œcišganie plików z serwera FTP Rysunek 23-2 przedstawia program UPDDEMO (update demo), pokazujšcy, jak za pomocš funkcji WinInet FTP ulokowanych w osobnym wštku można œcišgać pliki z anonimowego serwera FTP. UPDDEMO.C , 1270 Częœć III: Zagadnienia zaawansowane /* UPDDEMO.C - Demonstracja anonimowego FTP (c) Charles Petzold, 1998 */ ţţinclude ţţinclude ţţinclude 4ţinclude "resource.h" // komunikat niestandardowy używany w WndProc ţţdefine WM USER CHECKFILES (WM USER + 1) ţţdefine WM USER GETFILES (WM USER + 2) // informacje dla operacji œcišgania 4ţdefine FTPSERVER TEXT ("ftp.cpetzold.com") 4ţdefine DIRECTORY TEXT ("cpetzold.com/ProgWin/UpdDemo") ţţdefine TEMPLATE TEXT ("UD??????.TXT") // struktury używane do pamiętania nazw plików i treœci typedef struct { TCHAR * szFilename ; char * szContents ; ) FILEINFO ; typedef struct ( int iNum ; FILEINFO info[1] ; ) FILELIST ; // struktura używana przez drugi wštek typedef struct BOOL bContinue ; i HWND hwnd ; ) PARAMS ; , // deklaracje wszystkich funkcji programu LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; BOOL CALLBACK DlgProc (HWND, UINT, WPARAM, LPARAM) ; VOID FtpThread (PVOID) ; VOID ButtonSwitch (HWND, HWND, TCHAR *) ; FILELIST * GetFileList (VOID) ; int Compare (const FILEINFO *, const FILEINFO *) ; // kilka zmiennych globalnych f Rozdziat 23: Smaic ţtarneiu 1271 HINSTANCE hInst ; TCHAR szAppNameC] = TEXT ("UpdDemo"> ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) ( HWND hwnd ; MSG msg ; WNDCLASS wndclass ; hInst = hInstance ; wndclass.style = 0 ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = NULL ; wndclass.hbrBackground = GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) f c MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MBţICONERROR) ; return 0 ; hwnd = CreateWindow (szAppName TEXT ("Update Demo with Anonymous FTP"), WS OVERLAPPEDWINDOW ţ WS_VSCROLL, CW USEDEFAULT, CW USEDEFAULT, CW USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; /1 po wyœwietleniu okna sprawdzenie, czy istnieje ostatni plik SendMessage (hwnd, WM USER CHECKFILES, 0, 0) ; while (GetMessage (&msg, NULL, 0, 0)) ( TranslateMessage (&msg) ; DispatchMessage (&msg) ; 1 return msg.wParam ; ) LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ( static FILELIST * plist ; static int cxClient, cyClient, cxChar, cyChar ; HDC hdc ; int i ; PAINTSTRUCT ps ; SCROLLINFO si ; A f 1272 Częœć III: Zagadnienia zaawansowane (cišg dalszy ze strony 1271) SYSTEMTIME st ; TCHAR szFilename [MAXţPATH] ; switch (message) ( case WM_CREATE: cxChar = LOWORD (GetDialogBaseUnits ()) ; cyChar = HIWORD (GetDialogBaseUnits ()) ; return 0 ; case WM_SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; si.cbSize = sizeof (SCROLLINFO) ; si.fMask = SIFţRANGE ţ SIF PAGE ; si.nMin = 0 ; si.nMax = plist ? plist->iNum - 1 : 0 ; si.nPage = cyClient / cyChar ; SetScrollInfo (hwnd, SB VERT, &si, TRUE) ; return 0 ; case WM_VSCROLL: si.cbSize = sizeof (SCROLLINFO) ; si.fMask = SIF POS ţ SIF RANGE ţ SIFţPAGE ; GetScrollInfo (hwnd, SB VERT, &si) ; switch (LOWORD (wParam)) ( case SB_LINEDOWN: si.nPos += 1 ; break ; case SBţLINEUP: si.nPos -= 1 ; break ; case SBţPAGEDOWN: si.nPos += si.nPage ; break ; case SB_PAGEUP: si.nPos -= si.nPage ; break ; case SB_THUMBPOSITION: si.nPos = HIWORD (wParam) ; break ; default: return 0 ; ) si.fMask = SIF_POS ; SetScrollInfo (hwnd, SB VERT, &si, TRUE) ; InvalidateRect (hwnd, NULL, TRUE) ; return 0 ; case WM_USER_CHECKFILES: // pobranie daty systemowej GetSystemTime (&st) ; wsprintf (szFilename, TEXT ("UD%04i%02i.TXT"), st.wYear, st.wMonth) ; // sprawdzenie, czy plik istnieje; jeżeli tak, odczytanie wszystkich plików if (GetFileAttributes (szFilename) != (DWORD) -1) ( SendMessage (hwnd, WM USER GETFILES, 0, 0) ; return 0 ; ) // W przeciwnym razie pobranie plików z Internetu. Rozdział 23: Smak Internetu 1273 // Ale najpierw trzeba sprawdzić, czy nie kopiujemy plików na CD-ROM! if (GetDriveType (NULL) == DRIVEţCDROM) ( MessageBox (hwnd, TEXT ("Cannot run this program from CD-ROM!"), szAppName, MB OK ( MB ICONEXCLAMATION) ; return 0 ; l // zapytaj użytkownika, czy chce się polţczyć z Internetem if (IDYES == MessageBox (hwnd, TEXT ("Update information from Internet?"), szAppName, MB YESNO ţ MB ICONOUESTION)) // otwarcie okna dialogowego DialogBox (hInst, szAppName, hwnd, DlgProc) ; // aktualizacja SendMessage (hwnd, WM USER GETFILES, 0, 0) ; return 0 ; case WM_USER GETFILES: SetCursor (LoadCursor (NULL, IDCţWAIT)) ; ShowCursor (TRUE) ; // odczytanie wszystkich plików plist = GetFileList () ; ShowCursor (FALSE) ; ' SetCursor (LoadCursor (NULL, IDC ARROW)) ; // symulacja komunikatu WMţSIZE w celu zmiany polożenia // paska przewijania i przemalowania ekranu SendMessage (hwnd, WM_SIZE, 0, MAKELONG (cxClient, cyClient)) ; InvalidateRect (hwnd, NULL, TRUE) ; return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; SetTextAlign (hdc, TA UPDATECP) ; si.cbSize = sizeof (SCROLLINFO) ; si.fMask = SIF_POS ; GetScrollInfo (hwnd, SB VERT, &si) ; if (plist) for (i = 0 ; i < plist->iNum ; i++) f MoveToEx (hdc, cxChar, (i - si.nPos) * cyChar, NULL) ; TextOut (hdc, 0, 0, plist->info[i].szFilename, lstrlen (plist->info[i].szFilename)) ; TextOut (hdc, 0, 0, TEXT (": "), 2) ; TextOutA (hdc, 0, 0, plist->info[i].szContents, 1274 Czţœć M: Zagadnienia zaawansowane (cišg dalszy ze strony 1273) strlen (plist->infoCi).szContents)) ; 1 ) EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: PostOuitMessage (0) ; return 0 ; 1 return DefWindowProc (hwnd, message, wParam, lParam) ; l BOOL CALLBACK DlgProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) , static PARAMS params ; switch (message) ł case WM_INITDIALOG: params.bContinue = TRUE ; params.hwnd = hwnd ; ţbeginthread (FtpThread, 0, ¶ms) ; return TRUE ; case WM COMMAND: switch (LOWORD (wParam)) case IDCANCEL: // przycisk pozwalajacy przerwać œciaganie params.bContinue = FALSE ; return TRUE ; case IDOK: // przycisk zamykajacy okno EndDialog (hwnd, 0) ; return TRUE ; ) ) return FALSE ; 1 /* FtpThread: odczytuje pliki z serwera FTP i kopiuje je na dysk lokalny */ void FtpThread (PVOID parg) f BOOL bSuccess ; HINTERNET hIntSession, hFtpSession, hFind ; ! HWND hwndStatus, hwndButton ; PARAMS * pparams ; TCHAR szBuffer [64ţ ; WIN32ţFIND DATA finddata ; pparams = parg ; hwndStatus = GetDlgItem (pparams->hwnd, IDC STATUS) ; Rozdział 23: Smak tntemetu 1275 hwndButton = GetDlgItem (pparams->hwnd, IDCANCEL) ; // otwarcie sesji internetowej hIntSession = InternetOpen (szAppName, INTERNET_OPEN TYPE_PRECONFIG, NULL, NULL, INTERNETţFLAG ASYNC) ; if (hIntSession = NULL) ( wsprintf (szBuffer, TEXT ("InternetOpen error %i"), GetLastError ()) ; ButtonSwitch (hwndStatus, hwndButton, szBuffer) ; ęndthread () ; ) SetWindowText (hwndStatus, TEXT ("Internet session opened...")) ; // sprawdŸ, czy użytkownik nacisndł Anuluj if (!pparams->bContinue) ( InternetCloseHandle (hIntSession) ; ButtonSwitch (hwndStatus, hwndButton, NULL) ; ęndthread () ; 1 // otwarcie sesji FTP hFtpSession = InternetConnect (hIntSession, FTPSERVER, INTERNET_DEFAULT_FTP_PORT, NULL, NULL, INTERNET SERVICE_FTP, 0, 0) ; if (hFtpSession = NULL) t InternetCloseHandle (hIntSession) ; wsprintf (szBuffer, TEXT ("InternetConnect error %i"), GetLastError ()) ; ButtonSwitch (hwndStatus, hwndButton, szBuffer) ; ţendthread () ; ) SetWindowText (hwndStatus, TEXT ("FTP Session opened...")) ; // sprawdŸ, czy użytkownik nacisnšł Anuluj if (!pparams->bContinue) ( InternetCloseHandle (hFtpSession) ; InternetCloseHandle (hIntSession) ; ButtonSwitch (hwndStatus, hwndButton, NULL) ; ţendthread () ; f ) // przejdŸ do katalogu bSuccess = FtpSetCurrentDirectory (hFtpSession, DIRECTORY) ; if (!bSuccess) f InternetCloseHandle (hFtpSession) ; 1276 Częœć III: Zagadnienia zaawansowane (cišg dalszy ze strony 1275) InternetCloseHandle (hIntSession) ; wsprintf (szBuffer, TEXT ("Cannot set directory to %s"), DIRECTORY) ; ButtonSwitch (hwndStatus, hwndButton, szBuffer) ; ţendthread () ; SetWindowText (hwndStatus, TEXT ("Directory found...")) ; // sprawdŸ, czy użytkownik nacisnšl Anuluj if (!pparams->bContinue) InternetCloseHandle (hFtpSession) ; InternetCloseHandle (hlntSession) ; ButtonSwitch (hwndStatus, hwndButton, NULL) ; endthread () ; I // pobranie pierwszego pliku odpowiadajšcego szablonowi hFind = FtpFindFirstFile (hFtpSession, TEMPLATE, &finddata, 0, 0) ; if (hFind == NULL) ( InternetCloseHandle (hFtpSession) ; InternetCloseHandle (hIntSession) ; ButtonSwitch (hwndStatus, hwndButton, TEXT ("Cannot find files")) ; ţendthread () ; ) do f // sprawdŸ, czy użytkownik nacisnšł Anuluj if (!pparams->bContinue) ( InternetCloseHandle (hFind) ; InternetCloseHandle (hFtpSession) ; InternetCloseHandle (hIntSession) ; ButtonSwitch (hwndStatus, hwndButton, NULL) ; ţendthread () ; 1 // kopiowanie pliku z Internetu na dysk lokalny, ale nie wtedy, // gdy plik już istnieje na dysku wsprintf (szBuffer, TEXT ("Reading file %s..."), finddata.cFileName) ; SetWindowText (hwndStatus, szBuffer) ; FtpGetFile (hFtpSession, finddata.cFileName, finddata.cFileName, TRUE, FILĘ ATTRIBUTE NORMAL, FTP TRANSFER TYPE BINARY, 0) ; ř ) while (InternetFindNextFile (hFind, &finddata)) ; InternetCloseHandle (hFind) ; Rozdział 23: Smak Internetu 1277 InternetCloseHandle (hFtpSession) ; InternetCloseNandle (hIntSession) ; ButtonSwitch (hwndStatus, hwndButton, TEXT ("Internet Download Complete")); 1 /* ButtonSwitch: wyœwietla ostateczny komunikat o stanie i zmienia Anuluj w OK */ VOID ButtonSwitch (HWND hwndStatus, HWND hwndButton, TCHAR * szText) ( if (szText) SetWindowText (hwndStatus, szText) ; else SetWindowText (hwndStatus, TEXT ("Internet Session Cancelled")) ; SetWindowText (hwndButton, TEXT ("OK")) ; SetWindowLong (hwndButton, GWL ID, IDOK) ; /* GetFileList: odczytuje pliki z dysku i zapisuje ich nazwy oraz zawartoœci FILELIST * GetFileList (void) ( DWORD dwRead ; FILELIST * plist ; HANDLE hFile, hFind ; int iSize, iNum WIN32ţFINDţDATA finddata ; hFind = FindFirstFile (TEMPLATE, &finddata) ; if (hFind == INVALID HANDLEţVALUE) return NULL ; plist = NULL ; iNum = 0 ; do f // otwarcie pliku i odczytanie jego rozmiaru hFile = CreateFile (finddata.cFileName, GENERIC_READ, FILE_SHARE READ, NULL, OPENţEXISTING. 0, NULL) ; if (hFile - INVALID HANDLE VALUE) continue ; iSize = GetFileSize (hFile, NULL) ; if (iSize = (DWORD) -1) f CloseHandle (hFile) ; continue ; ) */ 1278 C:ţœć l: Zagadnienia zaawansowane (cišg dalszy ze strony 2277) // realokacja struktury FILELIST (miejsce na nowy element) plist = realloc (plist, sizeof (FILELIST) + iNum * sizeof (FILEINFO)); // rezerwacja pamięci na nazwę pliku i zapamiętanie tej nazwy plist->infoCiNum].szFilename = malloc (lstrlen (finddata.cFileName) + sizeof (TCHAR)) ; lstrcpy (plist->info[iNum].szFilename, finddata.cFileName) ; // rezerwacja miejsca i zapisanie treœci pliku plist->infoCiNum].szContents = malloc (iSize + 1> ; ReadFile (hFile, plist->infoCiNum].szContents, iSize, &dwRead, NULL); plist->info[iNum].szContentsCiSize] = 0 ; CloseHandle (hFile) ; iNum ++ ; while (FindNextFile (hFind, &finddata)) ; FindClose (hFind) ; // sortowanie plików wedlug nazw qsort (plist->info, iNum, sizeof (FILEINFO), Compare) ; plist->iNum = iNum ; return plist ; 1 /* Funkcja porównujdca dla qsort */ int Compare (const FILEINFO * pinfol, const FILEINFO * pinfo2) return lstrcmp (pinfo2->szFilename, pinfol->szFilename) ; 1 i UPDDEMO.RC (fragmenty) //Microsoft Developer Studio generated resource script. ţţinclude "resource.h" ţţinclude "afxres.h" /////////////////////////////////////////////l/////////////////////////////// // Dialog UPDDEMO DIALOG DISCARDABLE 20, 20, 186, 95 STYLE DS MODALFRAME ţ WSţPOPUP ţ WS CAPTION ţ WS SYSMENU Ro:dzial 23: Snţk Interrţ 1279 CAPTION "Internet Download" FONT 8, "MS Sans Serif" BEGIN PUSHBUTTON "Cancel",IDCANCEL,69,74,50,14 CTEXT "",IDCţSTATUS,7,29,172,21 END RESOURCE.H (fragmentyţ // Microsoft Developer Studio generated include file. // Used by UpdDemo.rc ţţdefine IDCţSTATUS 40001 Rysuţk 23-2 Program UPDDEMO 4V programie UPDDEMO używane sš pliki o nazwach UáyYYyţnm.TXT, gdzie yyyţy to czterocyfrowy rok (prograrn jest oczywiœcie przygotowany na rok 2000), , a mm to dwucyfrowy mżesiše. Poczyniono tu założenie, że program aktualizuje pliki raz na miesišc. Możemy sobie wyobrazić, że œcišga co rniesišc kolejne wy- dania jakiegoœ miesięcznika. Tak więc WinMain, po wywołaniu ShowWindow i LIpdateWindow w celu wyœwie- tlerua głównego alaia programu UPDDEMO, wysyła do WndPrţac komunikat nie- standardowy WM IJSER CHECKFILES. Procedura WndPrac przetwarza go, po- bierajšc bieżšcy rok i rniesišc, a następnie sprawdzajšc, czy w domyslnym kata- logu znajduje się plik UDYYyYţ'n,'ţ'XT dla tego roku i miesišca. Jeżeli płik istnie- je, oznacza to, że UPDDEMO jest zaktualizowanY, (Alie do końca. Może brako- wać jakichœ wczeœniejszych plików. tAl tym miejscu można rozţbudować algorytm sprawdzania). tN takim wypadku program wYsyła do siebie komunikat WMţUSER GETFILES, który następnie obsh.ţguje, wywolujšc fumkcję GetFrleList. Jest to przydţługa i mało interesujšcaţfunkeja UPDDEMO.C. Odczytuee ona tYlko wszystkie pliki UDyyYymrn.TXT do alokowanej dynamicznie struktury typu FI- LELIST, zdefiruowarţej na poczštku programu. Prograrn wYœwietla następnie za- wartoœć tych plików w swoim obszarze klienckim. Jeżeli UPDDEMO n'te odnajdzie najbardziej aktualnego pliku, musi sięgnšć po niego do Internetu. Program pyta najpierw użYtkownika, ezY może to zţbić. Je- żeli odpowiedŸ jest twierdzšca, wyœwietla proste ołcno dialogowe z przyciskiem Anuluj i statycznym polem tekstowym zawierajšcym identyfikator z IDC_STA- TUS. Okno będzie inforrnowało użytkownika o stanie ţcišgania i pozwalało mu przerwać cały proces. Procedura okna nazywa się átgProc i jest bardzo krótka, obejmuje bowiem irucjţa- cję stnxktury PARAMS zawierajšcej uchwyt jej własnego olena oraz zmiennš typu BOOL o na.zwie óContinue, a także wywołanie funkeji beginthread w celu uru- chomienia drugiego wštku. Samym transferem zajmuje się funkcja FtpThread, wykorzYstujšca funkcj,e: Fnter- 1280 Częœć III: Zagadnienia zaawansowane netOpen, InternetConnect, FtpSetCurrentDirectory, FtpFindFirstFile, InternetFindNe- xtFile, FtpGetFile i InternetCloseHandle (trzy razy). Jak bywa z większoœciš kodu, procedura wštku mogłaby być o wiele prostsza, gdyby nie musiała cišgle spraw- dzać błędów, informować użytkownika o postępie prac i pozwalać mu przery- wać działanie. Funkcja FtpThread powiadamia użytkownika o postępie prac, uży- wajšc wywołań SetWindowText z uchwytem hwndStatus, odnoszšcym się do sta- tycznego pola tekstowego w œrodku okna. Wštek może się zakończyć na trzy sposoby: Po pierwsze, FtpThread może napotkać w jednej z funkcji WinInet kod powrotu oznaczajšcy błšd. W takim wypadku funkcja posprzšta, sformatuje cišg komuni- katu o błędzie i przekaże go (razem z uchwytami okna dialogowego, pola teksto- wego i przycisku Anuluj) do funkcji ButtonSwitch. ButtonSwitch to mała funkcja wyœwietlajšca łańcuch znaków i zmieniajšca przycisk Anuluj w przycisk OK - nie tylko napis przycisku, lecz także identyfikator. Umożliwia to użytkownikowi naciœnięcie przycisku OK i zamknięcie okna dialogowego. Po drugie, FtpThread może zakończyć działanie bezbłędnie. Taki przypadek jest obsługiwany tak samo jak przypadek, kiedy funkcja napotka błędy, tylko że w oknie dialogowym wyœwietla tekst "Internet Download Complete". Po trzecie, œcišganie może anulować użytkownik. W takiej sytuacji procedura DlgProc nadaje polu bContinue struktury PARAMS wartoœć FALSE. Funkcja Ftp- Thread okresowo sprawdza tę wartoœć. Jeżeli bContinue ma wartoœć FALSE, funk- cja sprzšta po sobie i wywołuje ButtonSwitch z argumentem tekstowym równym NLlLL, co oznacza, że zostanie wyœwietlony cišg "Internet Session Cancelled". Użytkownik musi nacisnšć przycisk "OK", aby pozbyć się okna. Choć UPDDEMO ma wyœwietlać tylko po jednej lin z każdego pliku, można program rozbudować. Za pomocš tego programu mógłbym na przykład (ja, au- tor tej ksišżki) informować cię (ciebie, czytelnika) o ewentualnych uaktualnieniach treœci tego podręcznika albo dostarczać ci informacji z mojej strony WWW. W ten sposób UPDDEMO mógłby stać się sposobem rozsyłania informacji i kontynu- owania tej ksišżki poza stronami papierowymi...